Refactor/#131 pdf 미리보기 페이지 넘어가는 단위 수정 - #132
Conversation
- 제목과 첫 항목을 묶어 페이지 분리 시 제목 고립 방지 처리 추가 - Children.toArray를 활용한 항목 분리 및 렌더링 로직 최적화
- `onSuccess` 내부 쿼리 무효화 호출 시 비동기 처리로 변경 - 업데이트된 이력서 정보 및 목록, 카운트 데이터를 최신 상태로 유지하도록 개선
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning Review limit reached
Next review available in: 50 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (5)
Walkthrough이 변경은 이력서 수정 성공 시 상세 쿼리 무효화를 await하도록 조정하고, PDF 섹션 렌더링을 제목·첫 항목과 후속 항목 행으로 분리해 페이지 경계 처리를 변경합니다. Changes이력서 캐시 무효화
이력서 PDF 섹션 레이아웃
Estimated code review effort: 2 (Simple) | ~10 minutes Possibly related PRs
Suggested labels: Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches 💡 1⚔️ Resolve merge conflicts 💡
📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with 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.
Inline comments:
In `@src/entities/resume/model/resume.mutations.ts`:
- Around line 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.
In `@src/features/resume_pdf_download/ui/ResumePdfDocument.tsx`:
- Around line 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.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 8ff1bda7-4444-4361-b1d6-7ee37b15f6b8
📒 Files selected for processing (2)
src/entities/resume/model/resume.mutations.tssrc/features/resume_pdf_download/ui/ResumePdfDocument.tsx
| onSuccess: async () => { | ||
| await queryClient.invalidateQueries({ queryKey: resumeKeys.detail(workspaceId, resumeId) }) | ||
| queryClient.invalidateQueries({ queryKey: resumeKeys.lists() }) | ||
| queryClient.invalidateQueries({ queryKey: resumeKeys.counts(workspaceId) }) |
There was a problem hiding this comment.
🎯 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 -200Repository: 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:
- 1: https://tanstack.com/query/v5/docs/reference/QueryClient
- 2: https://tanstack.com/query/latest/docs/reference/QueryClient
- 3: https://tanstack.com/query/v5/docs/framework/react/guides/query-invalidation
- 4: Invalidate/Refetch Promises not resolving when paused TanStack/query#5054
- 5: https://tanstack.com/query/v5/docs/framework/react/guides/invalidations-from-mutations
- 6: https://tanstack.com/query/latest/docs/framework/react/guides/invalidations-from-mutations
- 7: why queryClient.invalidateQueries response type Promise TanStack/query#5978
목록·카운트 무효화도 모두 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.
| 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.
| 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> |
There was a problem hiding this comment.
🎯 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 || trueRepository: 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:
- 1: https://react-pdf.org/advanced
- 2: https://github.com/trailofbits/skills-curated/blob/main/plugins/react-pdf/skills/react-pdf/references/components.md
- 3: wrap={false} but for components that span across multiple pages diegomura/react-pdf#3341
- 4: Unbreakable elements taller than the page don't correctly start on next page and wrap, as expected diegomura/react-pdf#1676
- 5: Hangs when wrap={false} too big for one page diegomura/react-pdf#874
- 6: Requested Feature #3341 = breakWhenNeeded diegomura/react-pdf#3372
- 7: https://stackoverflow.com/questions/75039805/how-to-break-a-page-conditionally-with-react-pdf-renderer
🏁 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 || trueRepository: 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
optshj
left a comment
There was a problem hiding this comment.
첫 섹션의 내용이 너무 길면
첫번째 사진의 예시처럼 동일하게 큰 공백이 생기는건가요?
현재 페이지 남은 공간에 제목은 들어가나 내용이 다 들어가지 못하면 제목과 내용이 분리되는 문제가 발생했습니다 |
아하아하 알겠습니다~~ |
- 이력서 파일 `{기업명}_{직무명}_이력서.pdf` 형태로 파일명으로 저장
- 다운로드 완료 시 사용자에게 성공 메시지 표시
#️⃣연관된 이슈
📝작업 내용
1. PDF 섹션이 페이지를 넘어갈 때 '섹션 제목'이 고립되던 문제
PDF가 A4 여러 장으로 나뉠 때, 미리보기의 2단(제목 좌 / 항목 우) 레이아웃 특성상 섹션이 페이지 경계에서 나뉘면 제목만 페이지 맨 아래에 홀로 남는(고립) 현상이 있었습니다.
섹션 전체를
wrap={false}로 묶으면, A4 한 장을 넘는 큰 섹션이 다음 장으로 못 가고 겹쳐서 렌더되는 문제가 있었습니다(쪼갤 수 없는 블록이 페이지보다 크면 배치 불가).
아이템 단위로만 분리되지 않도록 하면
섹션(카테고리) 단위로 분리되지 않도록 하면
(하나의 섹션이 하나의 페이지를 넘어가서 필수적으로 절단이 필요하지만 절단되지않아서 겹쳐서 나옴)
해결방법
섹션을
[제목 + 첫 항목](→wrap={false}로 묶음) +[빈 열 + 나머지 항목]두 줄로 재구성했습니다.Children.toArray로 첫 항목과 나머지를 분리2. 이력서 완료 후 상세로 이동해도 최신 내용(PDF)이 바로 안 보이던 문제
완료 뮤테이션
onSuccess의invalidateQueries가await없이 실행돼, 상세 refetch가 끝나기 전에 페이지가 이동해 stale 캐시(수정 전 데이터) 가 보였습니다.→
onSuccess를async로 바꿔 상세 무효화만await처리. 이동 시점에 최신 데이터가 보장됩니다스크린샷 (선택)
💬리뷰 요구사항(선택)
Summary by CodeRabbit