Skip to content

経歴書 保存時の左右 diff プレビュー(VSCode 風)を追加#306

Merged
yusuke0610 merged 6 commits into
mainfrom
claude/resume-diff-ui-discussion-i2n0V
Jun 5, 2026
Merged

経歴書 保存時の左右 diff プレビュー(VSCode 風)を追加#306
yusuke0610 merged 6 commits into
mainfrom
claude/resume-diff-ui-discussion-i2n0V

Conversation

@yusuke0610

@yusuke0610 yusuke0610 commented Jun 5, 2026

Copy link
Copy Markdown
Owner

概要

経歴書(職務経歴書)の保存ボタン押下時に、保存済み(左)と編集中(右)を PDF プレビューと同じ整形レイアウトで左右に並べて比較できる diff モーダルを追加しました。VSCode フォークの diff のように、変更箇所をハイライトし、項目ごとに編集前へ戻せます。

主な機能

  • 左右 diff: 左=保存済み(baseline) / 右=編集中(form) を整形 HTML(iframe srcdoc)で並べて表示。
  • 変更ハイライト: 緑=追加 / 赤=削除 / 黄=修正 をインライン着色。
  • 変更点サイドバー: 旧→新の一覧。各行の 矢印(↩)で項目別ロールバック(編集前の値へ即時に戻す)。
  • 変更なし領域の折りたたみ: 長い経歴書では未変更項目を <details> で「▸ 変更なし N 項目を表示」に畳み、差分に集中できる。
  • 削除プレースホルダ: 削除した項目は編集中ペインにも「(削除)…」を表示して左右の位置を揃える。

実装

Backend

  • POST /api/resumes/preview(認証必須・DB 非更新)を新設。ResumeCreate を受けて整形 HTML と画面用 CSS を返す。
  • _build_html の各値ノードに data-fp(FE careerDiff のパスと一致)、各項目に data-unit を付与(PDF 出力は不変)。CSS 読込を _load_css(for_screen) に分離。

Frontend

  • CareerDiffModal(全画面・iframe sandbox)/ useResumeDiffPreview(baseline は1回・編集中はロールバック都度=即時取得)/ diffHighlight(着色・折りたたみ・削除スタブ)を追加。
  • 旧「変更点確認ダイアログ(要約リスト)」は本モーダルへ置換。差分計算 careerDiff / setAtPath は流用。

テスト

  • frontend: vitest(242)/ ESLint / lint-frontend-messages / build すべて green。
  • backend: 新規エンドポイント(200/401/422)・_build_htmldata-fp/data-unit 付与・PDF 回帰のテストを追加(実行は nix/WeasyPrint 環境が前提)。

補足 / 残課題

  • ハイライトは値ノード/ブロック単位(文字レベルの語句 diff は未対応)。
  • ロールバック後の右ペイン反映は、整形が backend 正本のためプレビュー API 往復のぶんだけ待つ(人工的な debounce は撤去済み)。
  • 保存フロー変更のため E2E トリガー該当。E2E(保存→diff→元に戻す→保存)の追加・実行は nix/テスト用バックエンド環境での確認が必要。

https://claude.ai/code/session_01NKa8AgchLkcdc3fXi737AF


Generated by Claude Code

Summary by CodeRabbit

  • New Features

    • In-app resume preview: generate annotated HTML/CSS previews (screen CSS omits embedded fonts) via the preview API for side-by-side diffing.
    • Three-column diff modal with navigation: highlights modified/added/removed fields and scrolls to selected changes.
    • Change rollback: revert individual changes from the modal; saving is gated behind confirmation when changes exist.
  • Tests / E2E

    • Added unit and E2E coverage for preview, annotation, diffing, and save-confirmation flows.

claude added 4 commits June 4, 2026 10:58
保存ボタン押下時に baseline(保存済み)と編集中フォームの差分を一覧表示し、
旧→新/追加・削除・修正を区別して確認できるダイアログを追加。各変更行で
「元に戻す」を押すと、その項目だけ baseline 値へ戻し、他の変更は保持したまま保存できる。

- utils/setAtPath.ts: immutable な path 単位の set/insert/remove(ロールバック適用の土台)
- utils/careerDiff.ts: baseline vs form の変更点リスト生成(既存 isDeepEqual を再利用、
  配列は useCareerDirty と同じ index 突合。途中削除は近似表示)
- components/forms/CareerSaveConfirmDialog: 確認+項目別ロールバック UI
- CareerResumeForm: 保存フローにダイアログを差し込み(setForm/save を流用)
- constants/messages.ts: CAREER_DIFF_LABELS / DIFF_DIALOG_MESSAGES を追加
- careerDiff.test.ts / setAtPath.test.ts: ユニットテスト

https://claude.ai/code/session_01NKa8AgchLkcdc3fXi737AF
第1弾のフィールド要約ダイアログを、PDF プレビューのレイアウトで左右に並べる
diff モーダルに作り替えた。左=保存済み / 右=編集中を整形 HTML で並べ、変更箇所を
緑(追加)/赤(削除)/黄(修正)でハイライト。変更点サイドバーから矢印(↩)で項目別ロールバック。

主な変更:
- backend: POST /api/resumes/preview(認証・DB非更新)を新設し整形 HTML+CSS を返す。
  _build_html の各値ノードに data-fp(careerDiff パスと一致)、各項目に data-unit を付与
  (PDF 出力は不変)。CSS 読込を _load_css(for_screen) に分離。
- frontend: CareerDiffModal(iframe srcdoc・sandbox)/ useResumeDiffPreview(baseline 1回・
  編集中 debounce)/ diffHighlight(着色・変更なし領域の折りたたみ・削除プレースホルダ)を追加。
  CareerSaveConfirmDialog は廃止。careerDiff / setAtPath は流用。
- 変更なし項目は <details> で折りたたみ、削除項目は編集中側に「(削除)…」スタブを表示。

検証: frontend は vitest(238)/eslint/messages-lint/build green。
backend は新規エンドポイント/生成系のテストを追加(実行は nix/WeasyPrint 環境が必要)。

https://claude.ai/code/session_01NKa8AgchLkcdc3fXi737AF
diff モーダル表示中に form が変わるのはロールバック操作のみで連続入力が無いため、
編集中プレビュー再取得の debounce を 300ms→0ms にし、矢印クリック直後に右ペインへ反映する。

https://claude.ai/code/session_01NKa8AgchLkcdc3fXi737AF
…-discussion-i2n0V

# Conflicts:
#	backend/app/services/pdf/generators/resume_generator.py
@coderabbitai

coderabbitai Bot commented Jun 5, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: fa5cdd1d-c391-4ac1-be28-2cb16e96471c

📥 Commits

Reviewing files that changed from the base of the PR and between 6c7c07c and 106a8fa.

📒 Files selected for processing (9)
  • backend/app/services/pdf/generators/resume_generator.py
  • backend/tests/test_pdf_generator.py
  • frontend/src/api/resumes.ts
  • frontend/src/api/types.ts
  • frontend/src/components/forms/CareerDiffModal.tsx
  • frontend/src/hooks/career/useResumeDiffPreview.test.ts
  • frontend/src/hooks/career/useResumeDiffPreview.ts
  • frontend/src/utils/diffHighlight.test.ts
  • frontend/src/utils/diffHighlight.ts
🚧 Files skipped from review as they are similar to previous changes (7)
  • frontend/src/api/resumes.ts
  • frontend/src/utils/diffHighlight.test.ts
  • backend/tests/test_pdf_generator.py
  • frontend/src/components/forms/CareerDiffModal.tsx
  • frontend/src/utils/diffHighlight.ts
  • frontend/src/hooks/career/useResumeDiffPreview.ts
  • backend/app/services/pdf/generators/resume_generator.py

📝 Walkthrough

Walkthrough

This PR adds a backend preview endpoint that returns annotated resume HTML and screen CSS, and a frontend save-confirmation flow that computes diffs, shows a side-by-side modal preview with rollback controls, and integrates tests and E2E wiring.

Changes

Resume Preview and Diff-Confirmation Flow

Layer / File(s) Summary
Backend schema and API contract
backend/app/schemas/resume.py, backend/app/schemas/__init__.py
New ResumePreviewResponse Pydantic model exports html and css string fields for preview payloads.
Resume HTML generation with diff annotations
backend/app/services/pdf/generators/resume_generator.py
Resume generator now emits data-fp attributes (targeting paths for scroll/diff navigation) and data-unit markers (folding anchors) on HTML elements, and uses regex-based CSS processing to strip @font-face rules when generating screen previews.
Preview endpoint and backend tests
backend/app/routers/resumes.py, backend/tests/test_endpoints.py, backend/tests/test_pdf_generator.py
New POST /api/resumes/preview endpoint generates preview HTML/CSS without database persistence; backend tests validate annotations and CSS output and input validation.
Frontend API types and endpoints
frontend/src/api/generated.ts, frontend/src/api/paths.ts, frontend/src/api/resumes.ts, frontend/src/api/types.ts
TypeScript definitions and API wrapper for the preview endpoint; generated API contract includes new ResumePreviewResponse schema and operation types.
Diff computation engine
frontend/src/utils/careerDiff.ts, frontend/src/utils/careerDiff.test.ts
Pure diff logic compares form vs baseline state, producing itemized changes with rollback functions and human-readable labels for modified/added/removed fields at all nesting levels.
Path-based immutable form updates
frontend/src/utils/setAtPath.ts, frontend/src/utils/setAtPath.test.ts
Utilities for immutable nested structure updates via path arrays, enabling form state changes and rollback operations.
Diff highlighting and HTML transformation
frontend/src/utils/diffHighlight.ts, frontend/src/utils/diffHighlight.test.ts
Utilities to annotate HTML with diff CSS classes, inject removed-item placeholders, and fold unchanged sections into collapsible <details> blocks.
Resume preview fetching hook
frontend/src/hooks/career/useResumeDiffPreview.ts, frontend/src/hooks/career/useResumeDiffPreview.test.ts
Hook fetches baseline and edited preview HTML/CSS from backend with debounced updates and error prioritization for invalid form input.
Diff modal UI component and styling
frontend/src/components/forms/CareerDiffModal.tsx, frontend/src/components/forms/CareerDiffModal.module.css
React component renders side-by-side diff preview in iframes with annotation/folding/placeholder injection; CSS styles the 3-column modal layout and change visualization.
Form save-confirmation integration
frontend/src/components/forms/CareerResumeForm.tsx
Integrates diff modal into form submission: computes changes, conditionally shows confirmation modal, and supports rollback of individual changes via modal callbacks.
Diff UI message constants
frontend/src/constants/messages.ts
Human-readable labels for diff field names, dialog messages, and utility functions for folded section and removed-item placeholder text.
E2E test updates
frontend/e2e/career-dirty-indicator.spec.ts
Adds preview endpoint mock and updates save-confirmation test to verify dirty indicator clearing through the full modal flow.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • yusuke0610/devforge#277: Both PRs modify resume project HTML rendering (resume_generator.py) and related period/field formatting.
  • yusuke0610/devforge#302: Both PRs touch the resume PDF/HTML generation layer including template/CSS formatting that overlaps with these annotation changes.

"🐇 With whiskers twitched and nose in the code,
I stitch HTML and CSS down the preview road.
Diffs hop side-by-side, marked neat and bright,
Rollback a nibble — then save it just right. ✨"

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 78.95% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and specifically describes the main feature: adding a side-by-side diff preview modal for resume saves with VSCode-style presentation, which aligns with the core objective of the changeset.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/resume-diff-ui-discussion-i2n0V

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

- generated.ts: 新規 /api/resumes/preview の path / schema(ResumePreviewResponse) /
  operation を OpenAPI 生成物に反映(codegen-drift 解消、ADR-0007)。
- e2e career-dirty-indicator: 保存ボタンが diff モーダルを開く挙動に変わったため、
  プレビュー API モックを追加し「この内容で保存」で確定するフローに更新。

https://claude.ai/code/session_01NKa8AgchLkcdc3fXi737AF

Copy link
Copy Markdown
Owner Author

@coderabbitai review


Generated by Claude Code

@coderabbitai

coderabbitai Bot commented Jun 5, 2026

Copy link
Copy Markdown

@yusuke0610 Sure! I'll start reviewing the PR now.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🧹 Nitpick comments (1)
frontend/src/hooks/career/useResumeDiffPreview.test.ts (1)

36-77: ⚡ Quick win

Add regression tests for cancel/stale-state paths.

Please add cases for (1) baseline transitioning to null after an initial successful fetch and (2) disabling/unmounting during an in-flight edited fetch to assert loading resets. These paths are currently untested and are easy to regress.

🤖 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 `@frontend/src/hooks/career/useResumeDiffPreview.test.ts` around lines 36 - 77,
Add two tests to cover regression paths: (1) "baseline -> null after success":
renderHook(() => useResumeDiffPreview(validForm, baseline, true)), advance
timers to let initial baseline/edited fetch resolve, assert baselineHtml is set,
then rerender with baseline = null and assert baselineHtml becomes null and no
new baseline fetch is triggered (verify mockPreview call counts); (2)
"cancel/unmount during in-flight edited fetch resets loading": start the hook
enabled, advance timers just enough to start the edited fetch (use
vi.advanceTimersByTimeAsync to simulate delay), then either rerender with
enabled=false or call unmount(), await remaining timers, and assert
result.current.loading is false and that in-flight resolution does not set
editedHtml (also verify mockPreview call counts to ensure no stray state
updates). Use existing helpers
renderHook/act/mockPreview/useResumeDiffPreview/validForm/baseline names to
locate and implement the tests.
🤖 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 `@backend/app/services/pdf/generators/resume_generator.py`:
- Around line 165-166: The period display spans in resume_generator.py (the
f-strings that produce the "vacation-header" and "vacation-body" nodes keyed to
f"{fp}.vacation_start_date") only bind to start_date so edits to end_date or
is_current won't be highlighted; update these renderings in the generate resume
HTML to either (a) bind the aggregated period node to the parent path (use
data-fp="{fp}" or data-fp="{parent_fp}" so changes to any child field map) or
(b) split the period into separate spans each with its own data-fp for
start_date, end_date and is_current (e.g., data-fp="{fp}.vacation_start_date",
data-fp="{fp}.vacation_end_date", data-fp="{fp}.vacation_is_current"); apply the
same fix to the other occurrence noted around lines 239-240 so all
period/experience date ranges are properly highlighted.

In `@frontend/src/api/resumes.ts`:
- Around line 6-9: The comment for the ResumePreviewResponse mirror is outdated;
update the doc comment to remove the claim that OpenAPI has not reflected
ResumePreviewResponse and instead note that the type is available from the
generated API types (ResumePreviewResponse in generated API types), or
completely remove the manual-mirror remark so future readers don't duplicate
types; change the comment near the top of the file that references
ResumePreviewResponse accordingly and, if appropriate, replace any hand-written
type usage with the generated ResumePreviewResponse type.

In `@frontend/src/components/forms/CareerDiffModal.tsx`:
- Line 113: The backdrop click currently calls onCancel unconditionally in the
CareerDiffModal (<div className={styles.overlay} onClick={onCancel}>), allowing
the modal to close while a save is in progress; update the overlay click handler
to check the saving prop/state and only invoke onCancel when saving is false
(i.e., ignore/backdrop-click when saving === true) so the modal cannot be
dismissed while an in-flight save is happening.

In `@frontend/src/hooks/career/useResumeDiffPreview.ts`:
- Around line 82-101: The cleanup currently flips the local `active` flag to
false so the in-flight promise handlers skip state updates, which can leave
`loading` stuck true; ensure `loading` is reset deterministically by either (a)
removing the `active` guard around the finally branch so `setLoading(false)`
always runs after `getCareerResumePreview` resolves/rejects, or (b) explicitly
call `setLoading(false)` inside the cleanup function in addition to `active =
false`; locate the debounce block using `handle`, `active`, `setLoading`,
`setFetchError`, and `getCareerResumePreview` (and `EDITED_DEBOUNCE_MS`) and
apply one of these fixes so cleanup cannot leave `loading` true.
- Around line 107-113: baselineHtml currently returns stale HTML from cached
baselinePreview even when the current baseline/preview is disabled or null;
update useResumeDiffPreview so baselineHtml is gated by the current inputs
(e.g., return baselineHtml: (enabled && baselinePayload) ? baselinePreview?.html
?? null : null) or explicitly clear/reset baselinePreview when baseline or
enabled becomes falsy; modify the return object and/or the effect that sets
baselinePreview (referencing baselineHtml, baselinePreview, baselinePayload,
enabled in useResumeDiffPreview) so the left-pane cannot show stale baseline
HTML.

In `@frontend/src/utils/diffHighlight.ts`:
- Around line 121-131: The anchor lookup in the loop that handles changes
(variables: change, index, prefix, anchor, lastByPrefix, doc) only searches
backward from index-1 so removals at index 0 (or when earlier siblings are
missing) never render a removed-stub; update the logic after the backward search
to, if anchor is still null, perform a forward search for the next existing
sibling by querying doc for data-unit="${prefix}.${k}" with k=index+1.. until
found, and if found insert the removed stub before that forward anchor (or
otherwise fall back to existing behavior), ensuring removed-stubs are shown for
head deletions and when earlier siblings are absent.

---

Nitpick comments:
In `@frontend/src/hooks/career/useResumeDiffPreview.test.ts`:
- Around line 36-77: Add two tests to cover regression paths: (1) "baseline ->
null after success": renderHook(() => useResumeDiffPreview(validForm, baseline,
true)), advance timers to let initial baseline/edited fetch resolve, assert
baselineHtml is set, then rerender with baseline = null and assert baselineHtml
becomes null and no new baseline fetch is triggered (verify mockPreview call
counts); (2) "cancel/unmount during in-flight edited fetch resets loading":
start the hook enabled, advance timers just enough to start the edited fetch
(use vi.advanceTimersByTimeAsync to simulate delay), then either rerender with
enabled=false or call unmount(), await remaining timers, and assert
result.current.loading is false and that in-flight resolution does not set
editedHtml (also verify mockPreview call counts to ensure no stray state
updates). Use existing helpers
renderHook/act/mockPreview/useResumeDiffPreview/validForm/baseline names to
locate and implement the tests.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 527a9958-d927-41a2-bff5-d2f288ba8eff

📥 Commits

Reviewing files that changed from the base of the PR and between baae9f9 and 6c7c07c.

📒 Files selected for processing (22)
  • backend/app/routers/resumes.py
  • backend/app/schemas/__init__.py
  • backend/app/schemas/resume.py
  • backend/app/services/pdf/generators/resume_generator.py
  • backend/tests/test_endpoints.py
  • backend/tests/test_pdf_generator.py
  • frontend/e2e/career-dirty-indicator.spec.ts
  • frontend/src/api/generated.ts
  • frontend/src/api/paths.ts
  • frontend/src/api/resumes.ts
  • frontend/src/components/forms/CareerDiffModal.module.css
  • frontend/src/components/forms/CareerDiffModal.tsx
  • frontend/src/components/forms/CareerResumeForm.tsx
  • frontend/src/constants/messages.ts
  • frontend/src/hooks/career/useResumeDiffPreview.test.ts
  • frontend/src/hooks/career/useResumeDiffPreview.ts
  • frontend/src/utils/careerDiff.test.ts
  • frontend/src/utils/careerDiff.ts
  • frontend/src/utils/diffHighlight.test.ts
  • frontend/src/utils/diffHighlight.ts
  • frontend/src/utils/setAtPath.test.ts
  • frontend/src/utils/setAtPath.ts

Comment thread backend/app/services/pdf/generators/resume_generator.py Outdated
Comment thread frontend/src/api/resumes.ts Outdated
Comment thread frontend/src/components/forms/CareerDiffModal.tsx Outdated
Comment thread frontend/src/hooks/career/useResumeDiffPreview.ts
Comment thread frontend/src/hooks/career/useResumeDiffPreview.ts
Comment thread frontend/src/utils/diffHighlight.ts
- 期間ハイライトをフィールド単位に分割し end_date / is_current の編集も着色
  (_format_period_html を新設。experiences / vacation に適用。PDF レイアウトは不変)
- ResumePreviewResponse を OpenAPI 生成型から再利用(手書きミラーと古いコメントを削除)
- 保存中は背景クリックでモーダルを閉じないようガード
- プレビュー取得のキャンセル時に loading を確定的に下ろす
- baseline 無効化後に古い左ペイン HTML を出さないよう baselineHtml をゲート
- 先頭削除時に前方アンカーの直前へ削除スタブを挿入する fallback を追加

https://claude.ai/code/session_01NDGfhgFWa2nxHpU598xVJN
@yusuke0610

Copy link
Copy Markdown
Owner Author

@coderabbitai pvして!

@coderabbitai

coderabbitai Bot commented Jun 5, 2026

Copy link
Copy Markdown

@yusuke0610 もちろん!今すぐレビューします 🐇

(ノ´ヮ`)ノ*: ・゚

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants