Skip to content
Merged
4 changes: 2 additions & 2 deletions src/entities/resume/model/resume.mutations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ export const useUpdateResume = (workspaceId: string, resumeId: string) => {
const { updateResume } = await resumeAPI.updateResume({ workspaceId, resumeId, input })
return updateResume
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: resumeKeys.detail(workspaceId, resumeId) })
onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: resumeKeys.detail(workspaceId, resumeId) })
queryClient.invalidateQueries({ queryKey: resumeKeys.lists() })
queryClient.invalidateQueries({ queryKey: resumeKeys.counts(workspaceId) })
Comment on lines +46 to 49

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Locate file =="
git ls-files | rg '(^|/)resume\.mutations\.ts$' || true

echo "== File excerpt =="
cat -n src/entities/resume/model/resume.mutations.ts | sed -n '1,120p'

echo "== Resolvable package info =="
for f in package.json pnpm-lock.yaml package-lock.json yarn.lock; do
  if [ -f "$f" ]; then
    echo "--- $f ---"
    if [ "$f" = "package.json" ]; then
      node - <<'JS'
const fs=require('fs');
const p=JSON.parse(fs.readFileSync('package.json','utf8'));
console.log(JSON.stringify({packageManager:p.packageManager, dependencies:p.dependencies, devDependencies:p.devDependencies}, null, 2));
JS
    else
      rg -n "`@tanstack/query-core`|invalidateQueries|`@tanstack`" "$f" | head -80 || true
    fi
  fi
done

echo "== Query usage =="
rg -n "invalidateQueries|resumeKeys|resumeId|workspaceId|onSuccess" src/entities/resume/model/resume.mutations.ts src -g '*.ts' -g '*.tsx' | head -200

Repository: YAPP-Github/28th-Web-Team-1-FE

Length of output: 30472


🌐 Web query:

TanStack Query v5 queryClient invalidateQueries returns Promise useMutation documentation

💡 Result:

In TanStack Query v5, the queryClient.invalidateQueries method returns a Promise [1][2]. This Promise resolves once the invalidation process—which includes marking the matching queries as stale and potentially refetching active queries—has completed [1][3][4]. Awaiting this Promise is particularly useful when used within mutation callbacks like onSuccess [5][6]. By returning (or awaiting) this Promise inside the mutation lifecycle, you ensure that the mutation remains in a pending state (e.g., isPending remains true) until the invalidated queries have finished refetching [5][6]. Key points regarding this behavior: - Purpose of the Promise: It allows you to synchronize your code with the completion of the background refetching triggered by the invalidation [7][4]. - Mutation Integration: You can use async/await within useMutation callbacks (such as onSuccess) to wait for invalidations to finish before the mutation is considered fully settled [5][6]. - Granularity: If invalidating multiple queries, you can wrap multiple queryClient.invalidateQueries calls in Promise.all to await them all simultaneously [5][6]. Example usage within useMutation: const mutation = useMutation({ mutationFn: addTodo, onSuccess: async => { // Wait for the query to be invalidated and refetched await queryClient.invalidateQueries({ queryKey: ['todos'] }); }, });

Citations:


목록·카운트 무효화도 모두 await 처리해 주세요.

queryClient.invalidateQueries()는 TanStack Query v5에서 Promise를 반환하므로, 목록·카운트 무효화도 Promise.all로 병렬 await해야 mutation 성공의 refetch 완료 시점이 보장됩니다.

       await queryClient.invalidateQueries({ queryKey: resumeKeys.detail(workspaceId, resumeId) })
-      queryClient.invalidateQueries({ queryKey: resumeKeys.lists() })
-      queryClient.invalidateQueries({ queryKey: resumeKeys.counts(workspaceId) })
+      await Promise.all([
+        queryClient.invalidateQueries({ queryKey: resumeKeys.lists() }),
+        queryClient.invalidateQueries({ queryKey: resumeKeys.counts(workspaceId) })
+      ])
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: resumeKeys.detail(workspaceId, resumeId) })
queryClient.invalidateQueries({ queryKey: resumeKeys.lists() })
queryClient.invalidateQueries({ queryKey: resumeKeys.counts(workspaceId) })
onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: resumeKeys.detail(workspaceId, resumeId) })
await Promise.all([
queryClient.invalidateQueries({ queryKey: resumeKeys.lists() }),
queryClient.invalidateQueries({ queryKey: resumeKeys.counts(workspaceId) })
])
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/entities/resume/model/resume.mutations.ts` around lines 46 - 49, Update
the onSuccess callback to await the list and count invalidations alongside the
detail invalidation, using Promise.all for resumeKeys.lists() and
resumeKeys.counts(workspaceId) so the mutation does not complete until all
refetches finish.

}
Expand Down
31 changes: 23 additions & 8 deletions src/features/resume_pdf_download/ui/ResumePdfDocument.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Document, Page, StyleSheet, Text, View } from '@react-pdf/renderer'
import { Fragment, type ReactNode } from 'react'
import { Children, Fragment, type ReactNode } from 'react'
import type { ResumeBasicInfoFieldsFragment } from '@shared/lib/gql/graphql'
import { formatDate, formatYYYYMM } from '@shared/lib'
import { payloadsOf, visibleItems, type ResumeSectionData } from '@entities/resume'
Expand Down Expand Up @@ -33,7 +33,9 @@ const styles = StyleSheet.create({
divider: { borderBottomWidth: 1, borderBottomColor: COLOR.divider, marginTop: 12, marginBottom: 24 },

sections: { flexDirection: 'column', gap: 44 },
section: { flexDirection: 'row', gap: 48 },
// 섹션 = [제목+첫 항목 줄] + [빈 열+나머지 항목 줄]을 세로로 쌓음. 세로 gap은 항목 간격(24)과 동일.
section: { flexDirection: 'column', gap: 24 },
sectionLine: { flexDirection: 'row', gap: 48 }, // 제목(또는 빈 열) | 본문
sectionTitle: { width: 72, flexShrink: 0, fontSize: 13, color: COLOR.subtler },
sectionBody: { flex: 1, flexDirection: 'column', gap: 24 },

Expand Down Expand Up @@ -74,12 +76,25 @@ const Subtitle = ({ parts }: { parts: Array<string | null | undefined> }) => {
)
}

const SectionRow = ({ title, children }: { title: string; children: ReactNode }) => (
<View style={styles.section}>
<Text style={styles.sectionTitle}>{title}</Text>
<View style={styles.sectionBody}>{children}</View>
</View>
)
const SectionRow = ({ title, children }: { title: string; children: ReactNode }) => {
const [first, ...rest] = Children.toArray(children)
return (
// '제목 + 첫 항목'을 wrap={false}로 묶어, 페이지 경계에서 제목만 홀로 남는 분리를 막는다(묶음은 항목 하나 크기라
// 항상 한 페이지에 들어감 → 겹침 없음). 나머지 항목은 자유롭게 나뉘어(빈 좌측 열로 정렬 유지) 큰 섹션도 이어진다.
<View style={styles.section}>
<View style={styles.sectionLine} wrap={false}>
<Text style={styles.sectionTitle}>{title}</Text>
<View style={styles.sectionBody}>{first}</View>
Comment on lines +80 to +87

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Locate file =="
fd -a 'ResumePdfDocument\.tsx$' . || true

echo "== File excerpt =="
if [ -f src/features/resume_pdf_download/ui/ResumePdfDocument.tsx ]; then
  wc -l src/features/resume_pdf_download/ui/ResumePdfDocument.tsx
  sed -n '1,180p' src/features/resume_pdf_download/ui/ResumePdfDocument.tsx | cat -n
fi

echo "== Relevant occurrences =="
rg -n "FirstElement|Children.toArray|wrap=\{false\}|SkillGrid|skillGrid|ResumeItem|`@react-pdf/renderer`" src/features/resume_pdf_download -S || true

Repository: YAPP-Github/28th-Web-Team-1-FE

Length of output: 12033


🌐 Web query:

@react-pdf/renderer 2 wrap false prevents component from being split across pages

💡 Result:

In @react-pdf/renderer, setting the wrap={false} prop on a component (such as View) instructs the engine to attempt to render the component as a single, unbreakable block [1][2]. If the component fits within the remaining space on the current page, it will be rendered there [1]. If it does not fit, it will be moved to the beginning of the next page [1]. However, there are significant technical limitations and known behaviors associated with this prop: 1. Potential for Overflow: If the content of a wrap={false} component is larger than the available height of a single page, the component will not automatically split across multiple pages [3][4]. Instead, it may overflow off the bottom of the page [4] or, in some cases, cause rendering issues or process hangs [5]. 2. Not a "Break When Needed" Feature: wrap={false} is intended to keep elements together, not to force a component to start on a new page if it would otherwise split, while still allowing it to wrap if it is too large [3][6]. Requests for a feature like breakWhenNeeded—which would trigger a page break only if an element doesn't fit, but allow it to span pages if it is larger than a single page—have been discussed in the project's issue tracker as a distinct requirement from the current behavior [3][6]. For scenarios where you need to prevent splitting but avoid the pitfalls of wrap={false} on very large content, common community workarounds include: * Using the break prop to manually force page breaks before specific components when necessary [1][7]. * Breaking down large components into smaller, individual units that are each set to wrap={false} [5]. * Keeping headers or parent containers "fixed" so that they persist across page breaks when their children are allowed to wrap [1][3].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== ResumePdfDocument tail =="
sed -n '175,230p' src/features/resume_pdf_download/ui/ResumePdfDocument.tsx | cat -n

echo "== package dependencies =="
if [ -f package.json ]; then
  node - <<'JS'
const fs = require('fs')
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'))
for (const sec of ['dependencies','devDependencies']) {
  if (!pkg[sec]) continue
  for (const [k,v] of Object.entries(pkg[sec])) {
    if (k === '`@react-pdf/renderer`' || k === 'react-pdf') console.log(`${k}: ${v}`)
  }
}
JS
fi

echo "== tests related to pdf/download =="
fd -i '.*pdf.*|.*download.*|.*resume.*' src test tests __tests__ 2>/dev/null | head -80 || true
rg -n "ResumePdfDocument|CoreSkill|CORE_SKILL|wrap=\{false\}|minPresenceAhead|Page" src test tests 2>/dev/null || true

Repository: YAPP-Github/28th-Web-Team-1-FE

Length of output: 27744


첫 자녀 단위로 wrap={false}를 적용하지 마세요.

SectionRow가 렌더한 첫 번째 React child까지 wrap={false}로 묶으니 CORE_SKILL의 전체 텍스트 열 하나와, SKILL의 전체 skillGrid를 모두 단일 비분할 블록으로 만듭니다. 더 큰 문제는 ResumeItem 전체와 skillGrid 같은 긴 블록이 남은 페이지 높이보다 클 수 있는데도 두 번째 페이지로만 미뤄지고 실제 문단/스킬 단위는 여러 페이지로 분할되지 않습니다. 제목 고아 방지 규칙은 타입별/단위별 렌더링이나 minPresenceAhead로 분리하고, 첫 항목과 스킬 그리드에는 wrap={false}를 적용하지 마세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/features/resume_pdf_download/ui/ResumePdfDocument.tsx` around lines 80 -
87, Remove the first-child wrapping from the SectionRow rendering in
ResumePdfDocument, so the title is not grouped with the entire first content
block under wrap={false}. Keep long ResumeItem text and skillGrid content
splittable across pages, and implement title orphan prevention through the
existing type/unit rendering or minPresenceAhead mechanism instead.

Source: MCP tools

</View>
{rest.length > 0 && (
<View style={styles.sectionLine}>
<View style={styles.sectionTitle} />
<View style={styles.sectionBody}>{rest}</View>
</View>
)}
</View>
)
}

/**
* 제목 + 부제(+ 선택적 본문) 구조의 공통 아이템 블록.
Expand Down