Skip to content

# PR: プロジェクト「課題・行動・成果」の統合 と 職務経歴書 UI 改善/ # PR: プロジェクトの在籍期間を複数対応(resume_project_periods 子テーブル化)#277

Merged
yusuke0610 merged 8 commits into
mainfrom
dev
May 28, 2026

Conversation

@yusuke0610

@yusuke0610 yusuke0610 commented May 28, 2026

Copy link
Copy Markdown
Owner

Summary

職務経歴書(Resume)プロジェクト欄の入力モデルを単純化し、あわせて職務経歴書ページ/プロジェクトモーダルの UI を整理した PR。大きく 2 つの変更からなる。

  1. プロジェクトの「課題 / 行動 / 成果」3 欄を単一の自由記述欄「詳細」(description) に統合
  2. 職務経歴書まわりの UI 改善(CSS 変数化・ダークモード対応・モーダル全面化+スプリッター・PDF 最大化機能の削除 など)

1. 課題・行動・成果 → 「詳細」へ統合 (73eed31)

プロジェクトの「課題」「行動」「成果」は分割する必要がないと判断し、単一の自由記述欄「詳細」(description) に統合した。

Backend

  • マイグレーション 0037_merge_project_caf_into_description を新設。
    • resume_projectschallenge / action / result を DROP し、description (Text, NOT NULL, default "") を追加。
    • libSQL (SQLite 互換) は DROP COLUMN を直接サポートしないため batch_alter_table(テーブル再作成)で入れ替え。
    • 統合対象の本番データが存在しないため、3 カラム連結のデータ移行は行わないdown_revision: 0036_...)。
  • app/models/resume.pyResumeProject のカラム定義を description に置換。
  • app/schemas/resume.pyProject スキーマを description: str (max_length=4500) に変更(旧 3 欄の 1500×3 を集約)。
  • app/repositories/resume.py — payload マッピングを description に統一。
  • app/services/markdown/generators/resume_generator.py / app/services/pdf/generators/resume_generator.py — 出力見出しを 課題 / 行動 / 成果 の 3 ブロックから 詳細 単一ブロックに変更。

Frontend

  • 型定義を description に統一: types.ts (CareerProject)、formTypes.ts (CareerProjectFieldKey)、payloadBuilders.ts (CareerProjectForm + buildProject + 非空フィルタ hasAnyText([p.name, p.description]))、constants.ts (blankCareerProject)。
  • ProjectModal.tsx — 3 つの MarkdownTextarea(課題/行動/成果, rows=2)を 1 つの「詳細」(rows=6) に置換。
  • hooks/career/useProjectFormDirty.ts — dirty 判定フィールドを description に統一。

テスト更新

  • BE: test_delete_documents.py / test_endpoints.py / test_schemas.py
  • FE: ProjectModal.test.tsx / useProjectFormDirty.test.ts / useProjectModalForm.test.ts / useProjectModalState.test.ts / payloadBuilders.test.ts / payloadBuilders.test.cjs / e2e/career-dirty-indicator.spec.ts
  • いずれも fixture を旧 3 欄から description へ更新(payloadBuilders.test.cjsdescription の trim を検証する assert を追加)。

2. 職務経歴書 UI 改善 (4967105)

レイアウト定数の CSS 変数化

  • styles.css--sidebar-width: 220px / --topbar-height: 48px を追加し、App.module.css のサイドバー幅・メインコンテンツ余白・モバイル上部バー高を変数参照に統一。
  • 警告色 --warning-text / --warning-bg / --warning-border をライト/ダーク両方に定義。

ダークモード対応(ハードコード色の排除)

  • ResumePdfTracePanel.module.css の固定色(#f1f3f5 等)を CSS 変数に置換。ズームボタンはグローバルの button { color:#fff } を打ち消すため color を明示。

プロジェクトモーダルの全面化 + スプリッター

  • ProjectModal.module.css / ProjectModal.tsx — モーダルを固定幅(max-width:800px/modalWide:1150px)からコンテンツ領域いっぱいleft: var(--sidebar-width) 起点で全幅・全高)に変更。サイドバーは覆わず操作可能なまま残す。
  • 入力フォームと PDF カラムの間にスプリッターを追加し、usePdfPanelLayout でドラッグ左右リサイズ(initialWidth:440 等)。狭幅時(max-width:700px)は縦積み・スプリッター非表示に切替。モバイル(max-width:768px)は上部バー下から全幅表示。

PDF パネルの「最大化」機能を削除

  • usePdfPanelLayout.ts から maximized state と toggleMaximize を削除し、リサイズのみに簡素化。
  • ResumePdfTracePanel.tsx から最大化ボタン/ダブルクリック・関連 props を削除。CareerResumeForm.tsx の利用箇所・CareerResumeForm.module.css.maximized CSS・messages.tsMAXIMIZE / RESTORE / MAXIMIZE_HINT を削除。

その他

  • 数値入力に min="0" を付与(CareerExperienceEditor の従業員数・資本金、ProjectModal の体制人数・メンバー人数)。
  • CareerResumeForm.module.css の splitter hover 色を var(--accent) に統一。

テスト更新

  • usePdfPanelLayout.test.ts — 最大化トグルの検証を削除し「初期幅を返す」に縮小。

Test plan

  • make migrate0037 の up/down が libSQL で通る)
  • make cilint + test + build-frontend
  • make test-backend — Resume の round-trip / 削除 / スキーマテスト
  • make test-frontend — ProjectModal / dirty / payloadBuilders
  • E2E(職務経歴書フローに影響: npm run test:e2e)— モーダル全面化・スプリッター・PDF パネル
  • 手動: プロジェクトモーダルで「詳細」入力 → 保存 → Markdown/PDF 出力に「詳細」が反映されること
  • 手動: ダークモードで PDF トレースパネル/警告表示の色が破綻しないこと

補足

  • 0037descriptionserver_default="" 付きで追加するため、既存行があっても NOT NULL 制約に抵触しない。
  • 旧 3 欄を 1 欄へ統合する破壊的スキーマ変更だが、本番データなしの前提で連結移行を省略している点はレビュー時に要確認。

Summary by CodeRabbit

  • New Features

    • Resizable PDF side panel with zoom controls in the project editor modal
    • Projects now support multiple periods (multiple start/end ranges)
  • New Inputs / UI

    • Consolidated narrative into a single "詳細" (description) field
    • Period editor UI to add/remove/edit multiple periods; responsive modal layout
  • Improvements

    • Per-period date validation and better handling of in-progress periods
    • Enforced non-negative numeric inputs and CSS variables for consistent layout and theming

Review Change Stack

yusuke0610 and others added 3 commits May 27, 2026 19:49
# PR: プロジェクト「課題・行動・成果」の統合 と 職務経歴書 UI 改善
@coderabbitai

coderabbitai Bot commented May 28, 2026

Copy link
Copy Markdown

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: 5b118c60-8c62-4d06-acab-dadbebf901dd

📥 Commits

Reviewing files that changed from the base of the PR and between da500c9 and 08b5222.

📒 Files selected for processing (8)
  • backend/alembic_migrations/versions/0037_merge_project_caf_into_description.py
  • frontend/src/components/forms/CareerFormEditors/CareerExperienceEditor.tsx
  • frontend/src/components/forms/ProjectModal.tsx
  • frontend/src/components/forms/sections/CareerQualificationsSection.tsx
  • frontend/src/hooks/career/usePdfPanelLayout.ts
  • frontend/src/hooks/career/useProjectFormDirty.test.ts
  • frontend/src/payloadBuilders.test.ts
  • frontend/src/payloadBuilders.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • frontend/src/hooks/career/usePdfPanelLayout.ts
  • backend/alembic_migrations/versions/0037_merge_project_caf_into_description.py
  • frontend/src/components/forms/ProjectModal.tsx

📝 Walkthrough

Walkthrough

This PR consolidates project narrative fields (challenge, action, result) into a single description field, models project periods as arrays, updates migrations/models/repos/schemas/renderers, refactors frontend types/payloads/forms/hooks/UI/CSS to the new shape, and replaces PDF panel maximize with a left-right resizable splitter.

Changes

Project Description Consolidation & PDF Panel Layout

Layer / File(s) Summary
Database migration and schema updates
backend/alembic_migrations/versions/0037_merge_project_caf_into_description.py, backend/alembic_migrations/versions/0038_add_project_periods_table.py, backend/app/models/resume.py, backend/app/models/__init__.py, backend/app/schemas/resume.py
Adds description column and normalizes project periods into resume_project_periods; updates ResumeProject/ResumeProjectPeriod models and Project/ProjectPeriod schemas to use description and periods arrays.
Backend data mapping and rendering
backend/app/repositories/resume.py, backend/app/services/markdown/generators/resume_generator.py, backend/app/services/pdf/generators/resume_generator.py
Repository now maps payload["periods"] into ResumeProject.period_rows and payload["description"] into model; markdown and PDF generators format multiple periods and render description under "詳細" instead of separate sections.
Backend test updates
backend/tests/test_delete_documents.py, backend/tests/test_endpoints.py, backend/tests/test_schemas.py
Updated test fixtures and assertions to use description and periods[...] instead of challenge/action/result and top-level date fields.
Frontend type definitions and form shape
frontend/src/types.ts, frontend/src/payloadBuilders.ts, frontend/src/formTypes.ts, frontend/src/constants.ts
CareerProject and form types now include periods: ProjectPeriod[] and description: string; new ProjectPeriod and CareerProjectPeriodForm types and blankCareerProjectPeriod template added.
Frontend form field binding and validation
frontend/src/components/forms/ProjectModal.tsx, frontend/src/components/forms/CareerFormEditors/CareerExperienceEditor.tsx, frontend/src/payloadBuilders.ts, frontend/src/hooks/career/useProjectFormDirty.ts, frontend/src/hooks/career/useProjectModalForm.ts
ProjectModal UI now edits local.periods list and local.description; numeric inputs enforce min="0"; validatePeriods validates each period; dirty tracking now includes description.
PDF panel hook refactoring and maximize removal
frontend/src/hooks/career/usePdfPanelLayout.ts, frontend/src/hooks/career/usePdfPanelLayout.test.ts, frontend/src/components/forms/ResumePdfTracePanel.tsx
usePdfPanelLayout now exposes only width and startResize (no maximize/toggle); ResumePdfTracePanel accepts only assist and removes maximize UI.
Modal layout integration with resizing
frontend/src/components/forms/ProjectModal.tsx, frontend/src/components/forms/CareerResumeForm.tsx, frontend/src/components/forms/ProjectModal.module.css
ProjectModal integrates usePdfPanelLayout, sets --pdf-col-width, and adds a draggable vertical splitter; CareerResumeForm consumes width/startResize; responsive stacking at 700px.
CSS variables and theme updates
frontend/src/styles.css, frontend/src/App.module.css, frontend/src/components/forms/CareerResumeForm.module.css, frontend/src/components/forms/ResumePdfTracePanel.module.css
Adds --sidebar-width/--topbar-height and warning color variables; replaces hard-coded sizes/colors with CSS variables; removes maximized-state CSS rules.
Frontend test fixture updates
frontend/e2e/career-dirty-indicator.spec.ts, frontend/src/components/forms/ProjectModal.test.tsx, frontend/src/hooks/career/useProjectFormDirty.test.ts, frontend/src/hooks/career/useProjectModalForm.test.ts, frontend/src/hooks/career/useProjectModalState.test.ts, frontend/src/payloadBuilders.test.ts, frontend/tests/payloadBuilders.test.cjs
Updated test fixtures to use description and periods and adjusted assertions to match new validation and normalization behavior.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • yusuke0610/devforge#261: Both PRs are related: they touch the same career project form/payload plumbing—frontend/src/hooks/career/useProjectModalForm.ts initialization via structuredClone(blankCareerProject) and the shared blankCareerProject/periods+description shape used by payload builders.
  • yusuke0610/devforge#265: Both PRs touch frontend project dirty-field UI wiring and field consolidation.

Suggested labels

feature

Poem

🐰 Three notes became one line of song,
Periods now march where dates belong.
Description tells the story whole,
Splitter helps the PDF scroll—
A rabbit nibbles, cheerful and strong.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 47.06% 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 PR title clearly summarizes the two main changes: consolidating project challenge/action/result fields into description, and supporting multiple project periods with a child table structure.
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 dev

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

@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: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
frontend/src/hooks/career/usePdfPanelLayout.ts (1)

63-66: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reset global drag styles on unmount as well.

If unmount happens while dragging, document.body.style.userSelect / cursor can remain locked because cleanup removes listeners without restoring styles.

🔧 Suggested fix
   useEffect(() => {
@@
     return () => {
       window.removeEventListener("mousemove", onMove);
       window.removeEventListener("mouseup", onUp);
+      if (resizingRef.current) {
+        resizingRef.current = false;
+        document.body.style.userSelect = "";
+        document.body.style.cursor = "";
+      }
     };
   }, [containerRef, minWidth, minFormWidth, reservedGap]);
🤖 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/usePdfPanelLayout.ts` around lines 63 - 66, The
cleanup returned from the drag effect currently removes event listeners but
doesn't restore document-level styles, so if the component unmounts mid-drag
userSelect/cursor can stay locked; update the cleanup in usePdfPanelLayout so it
also resets document.body.style.userSelect and document.body.style.cursor back
to their defaults (the same restoration logic used in onUp) when removing the
"mousemove" and "mouseup" listeners for onMove and onUp to guarantee styles are
cleared on unmount.
frontend/src/components/forms/CareerFormEditors/CareerExperienceEditor.tsx (1)

164-172: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Replace UI-only min="0" with backend/payload validation for employee_count and capital.

backend/app/schemas/resume.py defines employee_count and capital as free-form str fields with only max_length (no numeric/min validators). Backend tests also allow empty values and strings like "300名" / "1億円", so negative or non-numeric inputs can still reach the repositories/models even with min="0" on the HTML inputs.

Add numeric parsing/normalization (enforce >= 0, and likely integer for employee_count) in the payload builder and/or the Pydantic schema, and adjust/update tests to match the intended accepted formats. Optionally add step="1" for employee_count.

🤖 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/components/forms/CareerFormEditors/CareerExperienceEditor.tsx`
around lines 164 - 172, The UI uses min="0" on the employee_count input but
validation/parsing must happen server-side and in payload construction: update
the payload builder handling experiences (where onUpdateExperienceField / the
form submission builds the payload) to parse/normalize employee_count (coerce
numeric strings like "300名" to integer >=0, apply Math.floor/parseInt and
default/omit on invalid) and normalize capital similarly (strip non-numeric
characters, ensure >=0 or accept original formatted string per spec), add
step="1" to the CareerExperienceEditor input for employee_count, and add
corresponding validation to the Pydantic schema in backend/app/schemas/resume.py
(either change employee_count to an int with ge=0 or add a custom validator that
accepts the allowed formatted strings and enforces non-negative numeric values),
then update tests to reflect the normalized/validated formats; reference
CareerExperienceEditor, employee_count, capital, the payload builder function
used on form submit, and backend/app/schemas/resume.py in your changes.
♻️ Duplicate comments (1)
frontend/src/components/forms/CareerFormEditors/CareerExperienceEditor.tsx (1)

182-190: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Same validation issue as employee_count above.

The min="0" attribute on the capital field has the same insufficient validation issue noted in the employee_count comment above (lines 164-172). Frontend numeric validation should be added in the payload builder, and backend validation should be verified.

🤖 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/components/forms/CareerFormEditors/CareerExperienceEditor.tsx`
around lines 182 - 190, The capital input's client-side min attribute is
insufficient; update the payload builder that constructs the form submission
(the function that reads values including exp.capital) to coerce and validate
numeric fields (e.g., ensure capital is parsed to a non-negative number, reject
or normalize invalid/empty values) before sending, mirror the same validation
added for employee_count, and ensure onUpdateExperienceField still accepts
string input but the payload conversion enforces the numeric check; also confirm
the backend validates capital as a non-negative number.
🧹 Nitpick comments (2)
frontend/src/hooks/career/useProjectFormDirty.test.ts (1)

16-16: ⚡ Quick win

Add an explicit dirty check test for description.

This fixture change is good, but there’s still no direct assertion that description diffs flip fields.description and any.

Proposed test addition
 describe("useProjectFormDirty", () => {
+  it("description を変更すると fields.description と any が true", () => {
+    const original = buildProject({ description: "旧" });
+    const local = buildProject({ description: "新" });
+    const { result } = renderHook(() => useProjectFormDirty(local, original));
+    expect(result.current.fields.description).toBe(true);
+    expect(result.current.any).toBe(true);
+  });
🤖 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/useProjectFormDirty.test.ts` at line 16, Add an
explicit unit test in frontend/src/hooks/career/useProjectFormDirty.test.ts that
exercises the useProjectFormDirty hook's handling of the description field:
render/use the hook (useProjectFormDirty) with a fixture that contains a
non-empty description, then programmatically change the description to a
different string and assert that state.fields.description === true and state.any
=== true; next, set description back to the original value (or to the same value
as initial) and assert state.fields.description === false and state.any ===
false. Ensure the test references the hook by name (useProjectFormDirty) and
checks the fields.description and fields.any flags directly.
frontend/src/payloadBuilders.test.ts (1)

20-20: ⚡ Quick win

Add a focused test for description-only project content.

Given this schema shift, add one case where name is blank but description is non-empty to assert project inclusion/validation behavior explicitly.

Proposed test addition
 describe("buildCareerPayload (projects/clients/team)", () => {
+  it("name が空でも description に文字があれば project は中身ありとして扱う", () => {
+    const payload = buildCareerPayload(
+      baseState({
+        experiences: [
+          blankExperience({
+            clients: [
+              {
+                name: "顧客A",
+                has_client: true,
+                projects: [
+                  blankProject({
+                    name: "   ",
+                    description: "詳細あり",
+                    start_date: "2024-01",
+                    end_date: "2024-06",
+                    is_current: false,
+                  }),
+                ],
+              },
+            ],
+          }),
+        ],
+      }),
+    );
+    expect(payload.experiences[0].clients[0].projects).toHaveLength(1);
+    expect(payload.experiences[0].clients[0].projects[0].description).toBe("詳細あり");
+  });
🤖 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/payloadBuilders.test.ts` at line 20, Add a focused unit test in
payloadBuilders.test.ts that covers a project object with an empty name and a
non-empty description to assert inclusion/validation behavior; create a test
case calling the same builder under test (e.g., buildPayload or
buildProjectPayload) with a project like { name: "", description: "some text" }
and assert the expected output/validation result (include expected project
presence or error) to ensure description-only projects are handled explicitly.
🤖 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/alembic_migrations/versions/0037_merge_project_caf_into_description.py`:
- Around line 34-36: The migration unconditionally drops legacy columns
"challenge", "action", and "result" using batch_op.drop_column which can
irreversibly lose data; add a fail-fast precondition before those drops in the
0037_merge_project_caf_into_description migration that queries the table for any
non-NULL values (e.g. use op.get_bind() / connection.execute to run SELECT
COUNT(*) FROM <table> WHERE challenge IS NOT NULL OR action IS NOT NULL OR
result IS NOT NULL) and if the count > 0 raise an exception or abort the
migration with a clear message so the destructive drops only run when the table
is confirmed empty of those columns' data.

---

Outside diff comments:
In `@frontend/src/components/forms/CareerFormEditors/CareerExperienceEditor.tsx`:
- Around line 164-172: The UI uses min="0" on the employee_count input but
validation/parsing must happen server-side and in payload construction: update
the payload builder handling experiences (where onUpdateExperienceField / the
form submission builds the payload) to parse/normalize employee_count (coerce
numeric strings like "300名" to integer >=0, apply Math.floor/parseInt and
default/omit on invalid) and normalize capital similarly (strip non-numeric
characters, ensure >=0 or accept original formatted string per spec), add
step="1" to the CareerExperienceEditor input for employee_count, and add
corresponding validation to the Pydantic schema in backend/app/schemas/resume.py
(either change employee_count to an int with ge=0 or add a custom validator that
accepts the allowed formatted strings and enforces non-negative numeric values),
then update tests to reflect the normalized/validated formats; reference
CareerExperienceEditor, employee_count, capital, the payload builder function
used on form submit, and backend/app/schemas/resume.py in your changes.

In `@frontend/src/hooks/career/usePdfPanelLayout.ts`:
- Around line 63-66: The cleanup returned from the drag effect currently removes
event listeners but doesn't restore document-level styles, so if the component
unmounts mid-drag userSelect/cursor can stay locked; update the cleanup in
usePdfPanelLayout so it also resets document.body.style.userSelect and
document.body.style.cursor back to their defaults (the same restoration logic
used in onUp) when removing the "mousemove" and "mouseup" listeners for onMove
and onUp to guarantee styles are cleared on unmount.

---

Duplicate comments:
In `@frontend/src/components/forms/CareerFormEditors/CareerExperienceEditor.tsx`:
- Around line 182-190: The capital input's client-side min attribute is
insufficient; update the payload builder that constructs the form submission
(the function that reads values including exp.capital) to coerce and validate
numeric fields (e.g., ensure capital is parsed to a non-negative number, reject
or normalize invalid/empty values) before sending, mirror the same validation
added for employee_count, and ensure onUpdateExperienceField still accepts
string input but the payload conversion enforces the numeric check; also confirm
the backend validates capital as a non-negative number.

---

Nitpick comments:
In `@frontend/src/hooks/career/useProjectFormDirty.test.ts`:
- Line 16: Add an explicit unit test in
frontend/src/hooks/career/useProjectFormDirty.test.ts that exercises the
useProjectFormDirty hook's handling of the description field: render/use the
hook (useProjectFormDirty) with a fixture that contains a non-empty description,
then programmatically change the description to a different string and assert
that state.fields.description === true and state.any === true; next, set
description back to the original value (or to the same value as initial) and
assert state.fields.description === false and state.any === false. Ensure the
test references the hook by name (useProjectFormDirty) and checks the
fields.description and fields.any flags directly.

In `@frontend/src/payloadBuilders.test.ts`:
- Line 20: Add a focused unit test in payloadBuilders.test.ts that covers a
project object with an empty name and a non-empty description to assert
inclusion/validation behavior; create a test case calling the same builder under
test (e.g., buildPayload or buildProjectPayload) with a project like { name: "",
description: "some text" } and assert the expected output/validation result
(include expected project presence or error) to ensure description-only projects
are handled explicitly.
🪄 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: 13082f7d-048e-4ffe-827e-9e22f82b0a6b

📥 Commits

Reviewing files that changed from the base of the PR and between 2b03523 and e21c902.

📒 Files selected for processing (33)
  • backend/alembic_migrations/versions/0037_merge_project_caf_into_description.py
  • backend/app/models/resume.py
  • backend/app/repositories/resume.py
  • backend/app/schemas/resume.py
  • backend/app/services/markdown/generators/resume_generator.py
  • backend/app/services/pdf/generators/resume_generator.py
  • backend/tests/test_delete_documents.py
  • backend/tests/test_endpoints.py
  • backend/tests/test_schemas.py
  • frontend/e2e/career-dirty-indicator.spec.ts
  • frontend/src/App.module.css
  • frontend/src/components/forms/CareerFormEditors/CareerExperienceEditor.tsx
  • frontend/src/components/forms/CareerResumeForm.module.css
  • frontend/src/components/forms/CareerResumeForm.tsx
  • frontend/src/components/forms/ProjectModal.module.css
  • frontend/src/components/forms/ProjectModal.test.tsx
  • frontend/src/components/forms/ProjectModal.tsx
  • frontend/src/components/forms/ResumePdfTracePanel.module.css
  • frontend/src/components/forms/ResumePdfTracePanel.tsx
  • frontend/src/constants.ts
  • frontend/src/constants/messages.ts
  • frontend/src/formTypes.ts
  • frontend/src/hooks/career/usePdfPanelLayout.test.ts
  • frontend/src/hooks/career/usePdfPanelLayout.ts
  • frontend/src/hooks/career/useProjectFormDirty.test.ts
  • frontend/src/hooks/career/useProjectFormDirty.ts
  • frontend/src/hooks/career/useProjectModalForm.test.ts
  • frontend/src/hooks/career/useProjectModalState.test.ts
  • frontend/src/payloadBuilders.test.ts
  • frontend/src/payloadBuilders.ts
  • frontend/src/styles.css
  • frontend/src/types.ts
  • frontend/tests/payloadBuilders.test.cjs
💤 Files with no reviewable changes (1)
  • frontend/src/constants/messages.ts

@yusuke0610 yusuke0610 changed the title # PR: プロジェクト「課題・行動・成果」の統合 と 職務経歴書 UI 改善 # PR: プロジェクト「課題・行動・成果」の統合 と 職務経歴書 UI 改善/ # PR: プロジェクトの在籍期間を複数対応(resume_project_periods 子テーブル化) May 28, 2026
@yusuke0610

yusuke0610 commented May 28, 2026

Copy link
Copy Markdown
Owner Author

Summary

職務経歴書(Resume)のプロジェクトが持つ在籍期間を、1 案件 = 1 期間から 1 案件 = 複数期間へ拡張する PR。

これまで resume_projects テーブルに直接持っていた start_date / end_date / is_current を、子テーブル resume_project_periods に正規化して切り出す。これにより、同一案件で「2024/01〜2024/12、2025/06〜現在」のように離散した複数の在籍期間を表現できるようになる。

あわせて、本 PR とは独立した文書追加として ADR-0006「TanStack Query 導入検討」(Proposed) を含む。


1. データモデル: 期間の子テーブル化 (Backend)

マイグレーション 0038_add_project_periods_table

backend/alembic_migrations/versions/0038_add_project_periods_table.py を新設。

  • resume_project_periods テーブルを新規作成(id / project_id(FK, ondelete=CASCADE, index) / sort_order / start_date / end_date / is_current)。
  • 既存データ移行: resume_projects の各行の start_date / end_date / is_currentsort_order=0 の 1 期間として resume_project_periods に INSERT。
  • その後 resume_projects から start_date / end_date / is_current を DROP。libSQL (SQLite 互換) は DROP COLUMN を直接サポートしないため batch_alter_table(テーブル再作成)を使用。
  • downgrade() も実装済み: 3 カラムを resume_projects に再追加し、sort_order=0 の期間を書き戻してから子テーブルを drop。
  • down_revision: 0037_merge_project_caf_into_description

モデル app/models/resume.py

  • ResumeProject から start_date_value / end_date_value / is_current実カラムを削除し、period_rows リレーション(cascade="all, delete-orphan", order_by=sort_order)を追加。
  • 後方互換・ソート用に プロパティを追加:
    • periodsperiod_rows のリスト
    • is_current — いずれかの期間が参画中なら True
    • start_date_value — 全期間のうち最も新しい開始日(ソート用)
    • end_date_value — 参画中なら None、それ以外は全期間の最大終了日(ソート用)
  • 新モデル ResumeProjectPeriod を追加(start_date / end_date プロパティで format_year_month 整形)。
  • app/models/__init__.pyResumeProjectPeriod を export 追加。

スキーマ app/schemas/resume.py

  • 新スキーマ ProjectPeriodstart_date / end_date / is_current + 日付バリデーション)を追加。日付必須・範囲チェックは Project からこちらへ移設。
  • Project から日付 3 フィールドを除去し、periods: list[ProjectPeriod] を追加。
  • _migrate_legacy_fields(before validator)で後方互換を担保:
    • scale → team(従来どおり)
    • フラットな start_date / end_date / is_currentperiods[0] へ自動変換

リポジトリ app/repositories/resume.py

  • eager load に ResumeProject.period_rowsselectinload を追加。
  • _build_project_rowperiod_rows を payload の periods から構築。
  • プロジェクトのソートを sort_by_period_desc(単一期間前提)から、複数期間対応の _project_sort_key に置換。参画中を最優先、次に最大終了日・最大開始日の降順。

出力ジェネレータ

  • Markdown (services/markdown/generators/resume_generator.py): 単一期間表示を、各 periods を「、」区切りで連結する形に変更。
  • PDF (services/pdf/generators/resume_generator.py): _format_periods() を新設し複数期間を「、」連結。あわせて詳細欄の 【詳細】 見出しラベルを除去(本文のみ表示)。

2. フロントエンド: 期間リストの追加・削除 UI

型定義

  • types.tsProjectPeriod 型を新設、CareerProjectperiods: ProjectPeriod[] に変更。
  • payloadBuilders.tsCareerProjectPeriodForm 型を追加、CareerProjectFormperiods 保持に変更。
  • formTypes.tsCareerProjectFieldKey から日付キーを除去し、CareerProjectPeriodFieldKeystart_date / end_date / is_current)を新設。
  • constants.tsblankCareerProjectPeriod を追加、blankCareerProjectperiods: [period 1 件] を初期値に。

フォームロジック

  • hooks/career/useProjectModalForm.tsaddPeriod / removePeriod(最後の 1 件は空行で残す)/ updatePeriodFieldis_current ON で end_date クリア)を追加。日付検証は validatePeriods に切替。
  • hooks/career/useProjectFormDirty.ts — フィールド単位の日付 dirty を廃止し、periods 配列の deep-equal による dirty 判定に変更。
  • payloadBuilders.tsbuildPeriod() を追加、buildProject()periods をマップ。buildCareerPayload の事前バリデーションを各 period ループに変更。validatePeriods() を新規 export。
  • formMappers.ts — レスポンスの periods が空なら空 1 件で補完。

UI

  • components/forms/ProjectModal.tsx — 単一の開始/終了/参画状況フォームを、「期間」セクション + 期間行のリスト + 「+ 期間を追加」/ 行削除ボタンに置換。
  • components/forms/sections/CareerExperienceSection.tsx — 期間サマリーを複数期間「、」区切りで生成。

3. 文書追加

  • docs/adr/0006-tanstack-query.mdADR-0006「TanStack Query 導入検討」(Proposed) を新規追加。サーバ状態管理ライブラリ導入の是非・Redux との責務境界・段階移行プラン(Phase 0〜3)を整理。本コミットの期間対応とは独立した検討文書。
  • CONTRIBUTING.md — ADR 一覧に ADR-0006 行を追記。

テスト更新

  • BE: tests/test_schemas.py — fixture を periods 構造へ更新、後方互換(フラット → periods)/ 複数期間のバリデーションを検証。
  • FE: ProjectModal.test.tsx / useProjectModalForm.test.ts / useProjectFormDirty.test.ts / useProjectModalState.test.ts / payloadBuilders.test.ts / tests/payloadBuilders.test.cjsperiods 配列前提に更新。

Test plan

  • make migrate0038 の up/down が libSQL で通る(既存プロジェクトの 1 期間移行・書き戻しを含む)
  • make cilint + test + build-frontend
  • make test-backend — Resume round-trip / スキーマ後方互換 / 複数期間ソート
  • make test-frontend — ProjectModal 期間追加削除 / dirty / payloadBuilders
  • E2E(職務経歴書フローに影響: npm run test:e2e)— プロジェクトモーダルの期間追加・削除・保存
  • 手動: 1 案件に複数期間を登録 → 保存 → Markdown / PDF 出力で期間が「、」区切りで表示されること
  • 手動: 旧データ(単一期間)が periods[0] として正しく読み込まれること

補足・レビュー観点

  • 破壊的スキーマ変更: resume_projects から 3 カラムを物理 DROP する。batch_alter_table でのテーブル再作成と既存データ移行が本番想定で正しいか要確認。
  • 後方互換は二重に担保: スキーマ層(_migrate_legacy_fields)とマイグレーション層(データ移行)の両方。旧形式 JSON 入力も受理できる。
  • ソートロジックの移行: sort_by_period_desc から _project_sort_key への置換で、複数期間時の並びが意図どおりか(参画中優先 → 最大終了日 → 最大開始日)を確認。
  • ADR-0006 は Proposed であり実装を伴わない。コミットメッセージ「project date list add」のスコープ外の同梱物である点に留意。

@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: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
backend/app/services/markdown/generators/resume_generator.py (1)

94-96: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Render description as a block instead of an inline field line.

description now comes from a MarkdownTextarea, so it can contain multi-paragraph Markdown, lists, or tables. Wrapping it with field_line("詳細", description) prepends **詳細:** to the first line and breaks block-level Markdown rendering for the new field.

Suggested change
                     description = _a(proj, "description")
                     if description:
-                        lines.append(field_line("詳細", description))
+                        lines.append("**詳細:**")
+                        lines.append("")
+                        lines.append(description)
🤖 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 `@backend/app/services/markdown/generators/resume_generator.py` around lines 94
- 96, The current code uses field_line("詳細", description) which forces the
description into an inline field and breaks block-level Markdown; instead, stop
calling field_line for the description variable and render it as a block: append
a block label/header (e.g. a bold "詳細" or a heading) to lines, ensure a blank
line after the header, then append the raw description content (preserving its
multi-line Markdown) and a trailing blank line so lists/tables render correctly;
update the handling where proj and description are used and remove the
field_line("詳細", ...) usage.
backend/app/schemas/resume.py (1)

69-90: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Validate the YYYY-MM shape before this reaches parse_year_month().

validate_dates() only checks blank/current/range. Payloads like "2024/1" or "foo" still pass here, then backend/app/repositories/resume.py calls parse_year_month(period["start_date"]) and turns that bad input into a 500. Add a format validator (regex or parse-based) on ProjectPeriod so malformed API payloads fail as 422 at the schema boundary.

🤖 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 `@backend/app/schemas/resume.py` around lines 69 - 90,
ProjectPeriod.validate_dates currently only checks presence and ordering but
allows malformed strings; update validate_dates to assert that start_date (and
end_date when is_current is False) match the YYYY-MM shape before any parsing
(e.g. use a regex like ^\d{4}-(0[1-9]|1[0-2])$ or datetime.strptime check) and
raise a ValueError via get_error("validation.invalid_year_month") (or an
appropriate existing key) if the format is wrong so bad payloads fail with 422
at the schema boundary rather than bubbling into parse_year_month.
🧹 Nitpick comments (2)
frontend/src/hooks/career/useProjectModalForm.ts (1)

59-73: ⚡ Quick win

Tighten updatePeriodField typing to prevent future truthy-coercion misuse

Current "is_current" call sites already pass booleans (e.g., e.target.value === "current" and test uses true), so the "false"true trap won’t occur today. Still, the value: string | boolean signature makes it easy to introduce this bug later—constrain the type based on the key.

💡 Suggested fix
-  const updatePeriodField = (
-    periodIndex: number,
-    key: CareerProjectPeriodFieldKey,
-    value: string | boolean,
-  ) => {
+  const updatePeriodField = <K extends CareerProjectPeriodFieldKey>(
+    periodIndex: number,
+    key: K,
+    value: CareerProjectPeriodForm[K],
+  ) => {
     setLocal((prev) => ({
       ...prev,
       periods: prev.periods.map((p, i): CareerProjectPeriodForm => {
         if (i !== periodIndex) return p;
         if (key === "is_current") {
-          const isCurrent = Boolean(value);
+          const isCurrent = value as boolean;
           return { ...p, is_current: isCurrent, end_date: isCurrent ? "" : p.end_date };
         }
-        return { ...p, [key]: value };
+        return { ...p, [key]: value } as CareerProjectPeriodForm;
       }),
     }));
   };
🤖 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/useProjectModalForm.ts` around lines 59 - 73, The
updatePeriodField currently accepts value: string | boolean which allows
accidental truthy coercion for the "is_current" key; change its signature to use
two overloads (or a conditional/discriminated union) so that when key is
"is_current" value must be boolean and for other CareerProjectPeriodFieldKey
values value must be string, then keep the implementation body (setLocal and the
periods.map logic) the same but rely on the narrowed types so the branch that
handles key === "is_current" uses a boolean without coercion; reference:
updatePeriodField, CareerProjectPeriodFieldKey, and the setLocal/periods.map
block.
frontend/tests/payloadBuilders.test.cjs (1)

27-29: ⚡ Quick win

Assert the new periods mapping in this regression test.

This fixture was updated to the new shape, but the test never checks projects[0].periods. A regression in buildPeriod would still pass here.

Suggested test addition
-                  { start_date: "2020-04", end_date: "2021-03", is_current: false }
+                  { start_date: " 2020-04 ", end_date: " 2021-03 ", is_current: false }
                 ],
@@
   assert.equal(payload.experiences[0].clients[0].projects[0].name, "プロジェクトA");
+  assert.deepEqual(payload.experiences[0].clients[0].projects[0].periods, [
+    { start_date: "2020-04", end_date: "2021-03", is_current: false }
+  ]);
   assert.equal(payload.experiences[0].clients[0].projects[0].role, "メンバー");

Also applies to: 77-89

🤖 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/tests/payloadBuilders.test.cjs` around lines 27 - 29, Add assertions
in the test file frontend/tests/payloadBuilders.test.cjs to verify the new
periods mapping produced by buildPeriod: after building payloads, assert
projects[0].periods strictly equals the expected array shape (e.g. [{
start_date: "2020-04", end_date: "2021-03", is_current: false }]); do the same
for the other fixture case referenced around the 77-89 block so both tests
validate projects[0].periods rather than only relying on the fixture shape. Use
the existing test helpers/assertion style in the file to compare the periods
array.
🤖 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/schemas/resume.py`:
- Around line 93-125: The Project model must guarantee at least one period:
change the periods Field declaration in class Project so its default is a
single-element list (e.g., default_factory=lambda: [ProjectPeriod()]) and/or add
Field(min_items=1) to enforce non-empty lists; keep the _migrate_legacy_fields
logic (which still populates periods when start_date is present) but ensure
creating Project() or validation without legacy dates yields one empty
ProjectPeriod instead of an empty list so ResumeRepository._build_project_row()
always sees at least one period.

In `@frontend/src/payloadBuilders.ts`:
- Around line 177-193: The current validation loop over proj.periods only checks
existing entries and misses the case where proj.periods is empty; ensure
projects with zero periods are rejected by adding a check before the loop that
throws VALIDATION_MESSAGES.PROJECT_START_DATE_REQUIRED or a new message (e.g.,
PROJECT_PERIODS_REQUIRED) when proj.periods is missing or has length === 0, then
continue to validate each period as done in the existing block (references:
proj.periods, VALIDATION_MESSAGES.* and the period validation logic).

---

Outside diff comments:
In `@backend/app/schemas/resume.py`:
- Around line 69-90: ProjectPeriod.validate_dates currently only checks presence
and ordering but allows malformed strings; update validate_dates to assert that
start_date (and end_date when is_current is False) match the YYYY-MM shape
before any parsing (e.g. use a regex like ^\d{4}-(0[1-9]|1[0-2])$ or
datetime.strptime check) and raise a ValueError via
get_error("validation.invalid_year_month") (or an appropriate existing key) if
the format is wrong so bad payloads fail with 422 at the schema boundary rather
than bubbling into parse_year_month.

In `@backend/app/services/markdown/generators/resume_generator.py`:
- Around line 94-96: The current code uses field_line("詳細", description) which
forces the description into an inline field and breaks block-level Markdown;
instead, stop calling field_line for the description variable and render it as a
block: append a block label/header (e.g. a bold "詳細" or a heading) to lines,
ensure a blank line after the header, then append the raw description content
(preserving its multi-line Markdown) and a trailing blank line so lists/tables
render correctly; update the handling where proj and description are used and
remove the field_line("詳細", ...) usage.

---

Nitpick comments:
In `@frontend/src/hooks/career/useProjectModalForm.ts`:
- Around line 59-73: The updatePeriodField currently accepts value: string |
boolean which allows accidental truthy coercion for the "is_current" key; change
its signature to use two overloads (or a conditional/discriminated union) so
that when key is "is_current" value must be boolean and for other
CareerProjectPeriodFieldKey values value must be string, then keep the
implementation body (setLocal and the periods.map logic) the same but rely on
the narrowed types so the branch that handles key === "is_current" uses a
boolean without coercion; reference: updatePeriodField,
CareerProjectPeriodFieldKey, and the setLocal/periods.map block.

In `@frontend/tests/payloadBuilders.test.cjs`:
- Around line 27-29: Add assertions in the test file
frontend/tests/payloadBuilders.test.cjs to verify the new periods mapping
produced by buildPeriod: after building payloads, assert projects[0].periods
strictly equals the expected array shape (e.g. [{ start_date: "2020-04",
end_date: "2021-03", is_current: false }]); do the same for the other fixture
case referenced around the 77-89 block so both tests validate
projects[0].periods rather than only relying on the fixture shape. Use the
existing test helpers/assertion style in the file to compare the periods array.
🪄 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: 73c0ed99-3634-44d8-b7b2-8b37e5cb22e1

📥 Commits

Reviewing files that changed from the base of the PR and between e21c902 and da500c9.

📒 Files selected for processing (25)
  • CONTRIBUTING.md
  • backend/alembic_migrations/versions/0038_add_project_periods_table.py
  • backend/app/models/__init__.py
  • backend/app/models/resume.py
  • backend/app/repositories/resume.py
  • backend/app/schemas/resume.py
  • backend/app/services/markdown/generators/resume_generator.py
  • backend/app/services/pdf/generators/resume_generator.py
  • backend/tests/test_schemas.py
  • docs/adr/0006-tanstack-query.md
  • frontend/src/components/forms/ProjectModal.test.tsx
  • frontend/src/components/forms/ProjectModal.tsx
  • frontend/src/components/forms/sections/CareerExperienceSection.tsx
  • frontend/src/constants.ts
  • frontend/src/formMappers.ts
  • frontend/src/formTypes.ts
  • frontend/src/hooks/career/useProjectFormDirty.test.ts
  • frontend/src/hooks/career/useProjectFormDirty.ts
  • frontend/src/hooks/career/useProjectModalForm.test.ts
  • frontend/src/hooks/career/useProjectModalForm.ts
  • frontend/src/hooks/career/useProjectModalState.test.ts
  • frontend/src/payloadBuilders.test.ts
  • frontend/src/payloadBuilders.ts
  • frontend/src/types.ts
  • frontend/tests/payloadBuilders.test.cjs
✅ Files skipped from review due to trivial changes (3)
  • CONTRIBUTING.md
  • backend/app/models/init.py
  • docs/adr/0006-tanstack-query.md
🚧 Files skipped from review as they are similar to previous changes (3)
  • frontend/src/components/forms/ProjectModal.test.tsx
  • frontend/src/hooks/career/useProjectFormDirty.ts
  • frontend/src/hooks/career/useProjectModalState.test.ts

Comment on lines +93 to +125
class Project(BaseModel):
name: str = Field(max_length=200, default="")
periods: list[ProjectPeriod] = Field(default_factory=list)
role: str = Field(max_length=200, default="")
# 課題・行動・成果を統合した自由記述欄(見出し「詳細」)
description: str = Field(max_length=4500, default="")
team: ProjectTeam = Field(default_factory=ProjectTeam)
technology_stacks: list[TechnologyStackItem] = Field(default_factory=list)
phases: list[str] = Field(default_factory=list)

model_config = ConfigDict(from_attributes=True)

@model_validator(mode="before")
@classmethod
def _migrate_legacy_fields(cls, data: dict) -> dict:
"""旧形式の後方互換処理。
- scale → team
- start_date/end_date/is_current(フラット)→ periods[0]
"""
if not isinstance(data, dict):
return data
if "scale" in data and "team" not in data:
scale = data.pop("scale")
data["team"] = {"total": str(scale) if scale else "", "members": []}
if "start_date" in data and "periods" not in data:
data["periods"] = [
{
"start_date": data.pop("start_date", ""),
"end_date": data.pop("end_date", ""),
"is_current": data.pop("is_current", False),
}
]
return data

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Make periods non-empty again.

Project used to guarantee at least one date range via the flat start_date field. With periods: list[ProjectPeriod] = Field(default_factory=list), Project(periods=[]) now validates, and ResumeRepository._build_project_row() will persist a project with no period_rows, so period-based sorting/rendering silently loses its duration. Please require at least one period here instead of defaulting to an empty list.

🤖 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 `@backend/app/schemas/resume.py` around lines 93 - 125, The Project model must
guarantee at least one period: change the periods Field declaration in class
Project so its default is a single-element list (e.g., default_factory=lambda:
[ProjectPeriod()]) and/or add Field(min_items=1) to enforce non-empty lists;
keep the _migrate_legacy_fields logic (which still populates periods when
start_date is present) but ensure creating Project() or validation without
legacy dates yields one empty ProjectPeriod instead of an empty list so
ResumeRepository._build_project_row() always sees at least one period.

Comment thread frontend/src/payloadBuilders.ts
@coderabbitai

coderabbitai Bot commented May 28, 2026

Copy link
Copy Markdown

Actionable comments posted: 0

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.

1 participant