diff --git a/backend/alembic_migrations/versions/0037_merge_project_caf_into_description.py b/backend/alembic_migrations/versions/0037_merge_project_caf_into_description.py new file mode 100644 index 00000000..327a603c --- /dev/null +++ b/backend/alembic_migrations/versions/0037_merge_project_caf_into_description.py @@ -0,0 +1,50 @@ +"""merge resume_projects challenge/action/result into description(課題・行動・成果の統合) + +プロジェクトの「課題」「行動」「成果」は分割する必要がないため、単一の自由記述欄 +「詳細」(description) に統合した。これに伴い resume_projects の challenge / action / +result カラムを削除し、description カラムを新設する。 + +統合対象の既存本番データは存在しないため、データ移行(3 カラムの連結)は行わない。 + +libSQL (SQLite 互換) は ALTER COLUMN / DROP COLUMN を直接サポートしないため、 +batch_alter_table(テーブル再作成)でカラムを入れ替える。 + +Revision ID: 0037_merge_project_caf_into_description +Revises: 0036_rename_github_analysis_to_github_link +Create Date: 2026-05-27 00:00:00.000000 +""" + +from typing import Sequence, Union + +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "0037_merge_project_caf_into_description" +down_revision: Union[str, None] = "0036_rename_github_analysis_to_github_link" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + with op.batch_alter_table("resume_projects") as batch_op: + batch_op.add_column( + sa.Column("description", sa.Text(), nullable=False, server_default=""), + ) + batch_op.drop_column("challenge") + batch_op.drop_column("action") + batch_op.drop_column("result") + + +def downgrade() -> None: + with op.batch_alter_table("resume_projects") as batch_op: + batch_op.add_column( + sa.Column("challenge", sa.Text(), nullable=False, server_default=""), + ) + batch_op.add_column( + sa.Column("action", sa.Text(), nullable=False, server_default=""), + ) + batch_op.add_column( + sa.Column("result", sa.Text(), nullable=False, server_default=""), + ) + batch_op.drop_column("description") diff --git a/backend/app/models/resume.py b/backend/app/models/resume.py index 6f574962..0211fa96 100644 --- a/backend/app/models/resume.py +++ b/backend/app/models/resume.py @@ -167,9 +167,8 @@ class ResumeProject(Base): end_date_value: Mapped[date | None] = mapped_column("end_date", Date, nullable=True) is_current: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) role: Mapped[str] = mapped_column(String(200), nullable=False, default="") - challenge: Mapped[str] = mapped_column(Text, nullable=False, default="") - action: Mapped[str] = mapped_column(Text, nullable=False, default="") - result: Mapped[str] = mapped_column(Text, nullable=False, default="") + # 課題・行動・成果を統合した自由記述欄。見出しは「詳細」。 + description: Mapped[str] = mapped_column(Text, nullable=False, default="") team_total: Mapped[str] = mapped_column(String(60), nullable=False, default="") team_member_rows: Mapped[list["ResumeProjectTeamMember"]] = relationship( back_populates="project", diff --git a/backend/app/repositories/resume.py b/backend/app/repositories/resume.py index f777790b..ebe96b41 100644 --- a/backend/app/repositories/resume.py +++ b/backend/app/repositories/resume.py @@ -105,9 +105,7 @@ def _build_project_row(self, index: int, payload: dict[str, object]) -> ResumePr ), is_current=payload.get("is_current", False), role=payload.get("role", ""), - challenge=payload.get("challenge", ""), - action=payload.get("action", ""), - result=payload.get("result", ""), + description=payload.get("description", ""), team_total=team.get("total", ""), team_member_rows=[ ResumeProjectTeamMember( diff --git a/backend/app/schemas/resume.py b/backend/app/schemas/resume.py index 6dc13960..40bbe11f 100644 --- a/backend/app/schemas/resume.py +++ b/backend/app/schemas/resume.py @@ -74,9 +74,8 @@ class Project(BaseModel): end_date: str = Field(default="", max_length=30) is_current: bool = False role: str = Field(max_length=200, default="") - challenge: str = Field(max_length=1500, default="") - action: str = Field(max_length=1500, default="") - result: str = Field(max_length=1500, 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) diff --git a/backend/app/services/markdown/generators/resume_generator.py b/backend/app/services/markdown/generators/resume_generator.py index 9962da03..ee2e23f8 100644 --- a/backend/app/services/markdown/generators/resume_generator.py +++ b/backend/app/services/markdown/generators/resume_generator.py @@ -84,15 +84,9 @@ def build_resume_markdown(payload: dict[str, Any]) -> str: role = _a(proj, "role") if role: lines.append(field_line("担当", role)) - challenge = _a(proj, "challenge") - if challenge: - lines.append(field_line("課題", challenge)) - action = _a(proj, "action") - if action: - lines.append(field_line("行動", action)) - result = _a(proj, "result") - if result: - lines.append(field_line("成果", result)) + description = _a(proj, "description") + if description: + lines.append(field_line("詳細", description)) # 体制(後方互換: 旧 scale → team の正規化は shared に集約) team = normalize_team(proj) if team: diff --git a/backend/app/services/pdf/generators/resume_generator.py b/backend/app/services/pdf/generators/resume_generator.py index 8a7e1a42..b4d87e07 100644 --- a/backend/app/services/pdf/generators/resume_generator.py +++ b/backend/app/services/pdf/generators/resume_generator.py @@ -78,15 +78,9 @@ def _build_project_html(project) -> str: # 左カラム: 業務内容 left_parts: list[str] = [] - challenge = _a(project, "challenge") - if challenge: - left_parts.append(f"【課題】{_md(challenge)}") - action = _a(project, "action") - if action: - left_parts.append(f"【行動】{_md(action)}") - result = _a(project, "result") - if result: - left_parts.append(f"【成果】{_md(result)}") + description = _a(project, "description") + if description: + left_parts.append(f"【詳細】{_md(description)}") left_content = "".join(left_parts) if left_parts else "-" # 右カラム: 開発環境(技術スタック) diff --git a/backend/tests/test_delete_documents.py b/backend/tests/test_delete_documents.py index e8b9e1dc..0f0d1c56 100644 --- a/backend/tests/test_delete_documents.py +++ b/backend/tests/test_delete_documents.py @@ -29,9 +29,7 @@ "end_date": "2022-03", "is_current": False, "role": "SE", - "challenge": "", - "action": "", - "result": "", + "description": "", "team": {"total": "5", "members": []}, "technology_stacks": [], "phases": [], diff --git a/backend/tests/test_endpoints.py b/backend/tests/test_endpoints.py index ee9d75f3..96f29848 100644 --- a/backend/tests/test_endpoints.py +++ b/backend/tests/test_endpoints.py @@ -140,9 +140,7 @@ def test_resume_round_trips_nested_structure(client: TestClient) -> None: "end_date": "2024-03", "is_current": False, "role": "SE", - "challenge": "性能改善", - "action": "非同期化", - "result": "応答時間短縮", + "description": "性能改善のため非同期化し応答時間を短縮", "team": { "total": "5", "members": [{"role": "SE", "count": 3}], diff --git a/backend/tests/test_schemas.py b/backend/tests/test_schemas.py index 5d729765..b03cc774 100644 --- a/backend/tests/test_schemas.py +++ b/backend/tests/test_schemas.py @@ -26,10 +26,7 @@ def experience_payload() -> dict: "end_date": "2024-03", "is_current": False, "role": "メンバー", - "description": "API開発", - "challenge": "課題", - "action": "行動", - "result": "処理速度を改善", + "description": "課題・行動・成果をまとめた詳細", "team": { "total": "5", "members": [ diff --git a/frontend/e2e/career-dirty-indicator.spec.ts b/frontend/e2e/career-dirty-indicator.spec.ts index bea316bb..3ea29b10 100644 --- a/frontend/e2e/career-dirty-indicator.spec.ts +++ b/frontend/e2e/career-dirty-indicator.spec.ts @@ -104,9 +104,7 @@ test.describe("職務経歴書 未保存マーク", () => { end_date: "2022-03", is_current: false, role: "Eng", - challenge: "", - action: "", - result: "", + description: "", team: { total: "5", members: [] }, technology_stacks: [], phases: [], diff --git a/frontend/src/App.module.css b/frontend/src/App.module.css index 4fdc30a2..e6ba56fd 100644 --- a/frontend/src/App.module.css +++ b/frontend/src/App.module.css @@ -5,7 +5,7 @@ } .sidebar { - width: 220px; + width: var(--sidebar-width); background: var(--sidebar-bg); color: #fff; display: flex; @@ -73,7 +73,7 @@ .mainContent { flex: 1; - margin-left: 220px; + margin-left: var(--sidebar-width); overflow-y: auto; height: 100vh; background: var(--bg-page); @@ -135,6 +135,6 @@ .mainContent { margin-left: 0; - margin-top: 48px; + margin-top: var(--topbar-height); } } diff --git a/frontend/src/components/forms/CareerFormEditors/CareerExperienceEditor.tsx b/frontend/src/components/forms/CareerFormEditors/CareerExperienceEditor.tsx index 0a3749fa..3e2c6226 100644 --- a/frontend/src/components/forms/CareerFormEditors/CareerExperienceEditor.tsx +++ b/frontend/src/components/forms/CareerFormEditors/CareerExperienceEditor.tsx @@ -163,6 +163,7 @@ export function CareerExperienceEditor({
onUpdateExperienceField(expIndex, "employee_count", e.target.value) @@ -180,6 +181,7 @@ export function CareerExperienceEditor({
onUpdateExperienceField(expIndex, "capital", e.target.value)} placeholder="例: 5" diff --git a/frontend/src/components/forms/CareerResumeForm.module.css b/frontend/src/components/forms/CareerResumeForm.module.css index 9758f87b..fec443a2 100644 --- a/frontend/src/components/forms/CareerResumeForm.module.css +++ b/frontend/src/components/forms/CareerResumeForm.module.css @@ -125,7 +125,7 @@ } .splitter:hover { - background: #b7c5e0; + background: var(--accent); } /* 右: PDF カラム(幅は CSS 変数 --pdf-col-width で可変。中の panel が縦に伸びる)。 @@ -140,17 +140,6 @@ padding-left: 1rem; } -/* 最大化中: フォームとスプリッターを隠し、PDF をフォームに覆い被せて全幅表示する */ -.maximized .formCol, -.maximized .splitter { - display: none; -} - -.maximized .pdfCol { - flex: 1; - width: auto; -} - /* 横幅が足りない時(2 カラムを並べると最小幅を割る幅)は縦積みに切り替える。 閾値 = PDF 最小(280) + フォーム最小(360) + gap/splitter(38) ≒ 680px。 これ未満では左右に並べず、フォームの下に PDF を全幅で積む(要素が切れない)。 */ diff --git a/frontend/src/components/forms/CareerResumeForm.tsx b/frontend/src/components/forms/CareerResumeForm.tsx index 19cd081d..0970ee5e 100644 --- a/frontend/src/components/forms/CareerResumeForm.tsx +++ b/frontend/src/components/forms/CareerResumeForm.tsx @@ -35,12 +35,7 @@ export function CareerResumeForm() { const assist = useResumeImportAssist(); const pdfInputRef = useRef(null); const splitRef = useRef(null); - const { - width: pdfWidth, - maximized: pdfMaximized, - startResize, - toggleMaximize, - } = usePdfPanelLayout(splitRef); + const { width: pdfWidth, startResize } = usePdfPanelLayout(splitRef); const { form, setForm, @@ -180,14 +175,14 @@ export function CareerResumeForm() {
- {/* 左=入力フォーム / 右=PDF 原本ビュー。スプリッターで左右リサイズ、最大化で覆い被せる。 + {/* 左=入力フォーム / 右=PDF 原本ビュー。スプリッターで左右リサイズする。 splitWrap はコンテナクエリの基準(= split に割り当てられる実幅)。 横幅が足りない時は split を縦積みに切り替える。PDF カラム幅は CSS 変数で渡し、 縦積み時は CSS 側で全幅に上書きする(inline style だと上書きできないため変数経由)。 */}
{/* 左: 入力フォーム(選択中フィールドは緑枠 = import-assign-form の :focus CSS) */} @@ -257,11 +252,7 @@ export function CareerResumeForm() { {/* 右: PDF 原本ビュー(独立スクロール)。文字を選択して入力欄へ流し込む。 幅は CSS 変数 --pdf-col-width を CSS 側で参照(縦積み時は全幅へ上書き)。 */}
diff --git a/frontend/src/components/forms/ProjectModal.module.css b/frontend/src/components/forms/ProjectModal.module.css index 6e4de69e..57710055 100644 --- a/frontend/src/components/forms/ProjectModal.module.css +++ b/frontend/src/components/forms/ProjectModal.module.css @@ -1,30 +1,38 @@ +/* サイドバーを覆わず(操作可能なまま残し)、その右のコンテンツ領域いっぱいにモーダルを開く。 + overlay 自体を left:サイドバー幅 から始めるので、z-index がサイドバー(100)より上でも + サイドバーは露出したままになる。 */ .overlay { position: fixed; inset: 0; + left: var(--sidebar-width); background: var(--overlay-bg); display: flex; - align-items: center; - justify-content: center; + padding: 1.2rem; z-index: 1000; } +/* モーダルはコンテンツ領域いっぱいに広げる(職務経歴書ページと同等の作業領域を確保)。 */ .modal { background: var(--bg-card); border-radius: 12px; - width: 90vw; - max-width: 800px; - max-height: 85vh; + width: 100%; + height: 100%; display: flex; flex-direction: column; overflow: hidden; } -/* 取り込みパネルを併置する分だけ横幅を広げる */ -.modalWide { - max-width: 1150px; +/* モバイルはサイドバーが上部バーになるため、その下から全幅で開く。 */ +@media (max-width: 768px) { + .overlay { + left: 0; + top: var(--topbar-height); + padding: 0.8rem; + } } -/* ヘッダー下の領域。左=入力フォーム / 右=取り込みブロックの 2 カラム */ +/* ヘッダー下の領域。左=入力フォーム / 右=取り込みブロックの 2 カラム。 + スプリッターのドラッグで左右の比率を変えられる。 */ .bodyWrap { flex: 1; display: flex; @@ -32,9 +40,26 @@ overflow: hidden; } -/* モーダル内に再掲する PDF 原本ビュー(タブ固定・ページ部のみスクロール) */ +/* 入力フォームと PDF カラムの境界。ドラッグで左右比率を変える。 */ +.splitter { + flex-shrink: 0; + align-self: stretch; + width: 6px; + border-radius: 3px; + background: var(--border); + cursor: col-resize; + transition: background 0.15s; +} + +.splitter:hover { + background: var(--accent); +} + +/* モーダル内に再掲する PDF 原本ビュー(タブ固定・ページ部のみスクロール)。 + 幅は CSS 変数 --pdf-col-width で可変(縦積み時は下のメディアクエリで全幅に上書き)。 + inline style ではなく変数経由にすることで、縦積み時の全幅化を CSS 側で上書きできる。 */ .blocksColumn { - width: 320px; + width: var(--pdf-col-width, 320px); flex-shrink: 0; display: flex; flex-direction: column; @@ -44,6 +69,33 @@ padding: 1rem; } +/* 横幅が狭い時は左右 2 カラムをやめ、フォームの下に PDF を全幅で縦積みする。 + モーダルは width:90vw なのでビューポート幅で判定する。 */ +@media (max-width: 700px) { + .bodyWrap { + flex-direction: column; + overflow-y: auto; + } + + /* 縦積み時はモーダル全体スクロールに委ねる(カラム個別スクロールを解除) */ + .body { + overflow-y: visible; + } + + /* 縦積み時はドラッグでの左右リサイズを無効化 */ + .splitter { + display: none; + } + + /* 幅指定を上書きして全幅。区切りは左ボーダーから上ボーダーへ。 */ + .blocksColumn { + width: 100%; + min-height: 60vh; + border-left: none; + border-top: 1px solid var(--border); + } +} + .header { display: flex; justify-content: space-between; diff --git a/frontend/src/components/forms/ProjectModal.test.tsx b/frontend/src/components/forms/ProjectModal.test.tsx index fd2c5960..669d38bb 100644 --- a/frontend/src/components/forms/ProjectModal.test.tsx +++ b/frontend/src/components/forms/ProjectModal.test.tsx @@ -17,9 +17,7 @@ const invalidDateProject: CareerProjectForm = { end_date: "2024-01", is_current: false, role: "エンジニア", - challenge: "", - action: "", - result: "", + description: "", team: { total: "", members: [] }, technology_stacks: [], phases: [], @@ -31,9 +29,7 @@ const emptyProject: CareerProjectForm = { end_date: "", is_current: false, role: "", - challenge: "", - action: "", - result: "", + description: "", team: { total: "", members: [] }, technology_stacks: [], phases: [], diff --git a/frontend/src/components/forms/ProjectModal.tsx b/frontend/src/components/forms/ProjectModal.tsx index 7efc00ea..0875f0ba 100644 --- a/frontend/src/components/forms/ProjectModal.tsx +++ b/frontend/src/components/forms/ProjectModal.tsx @@ -1,3 +1,5 @@ +import { useRef, type CSSProperties } from "react"; + import type { CareerProjectForm } from "../../payloadBuilders"; import { careerTechnologyStackCategories, @@ -7,6 +9,7 @@ import { } from "../../constants"; import { useProjectFormDirty } from "../../hooks/career/useProjectFormDirty"; import { useProjectModalForm } from "../../hooks/career/useProjectModalForm"; +import { usePdfPanelLayout } from "../../hooks/career/usePdfPanelLayout"; import type { UseResumeImportAssistReturn } from "../../hooks/career/useResumeImportAssist"; import { Combobox } from "./Combobox"; import { MarkdownTextarea } from "./MarkdownTextarea"; @@ -58,12 +61,19 @@ export function ProjectModal({ /** PDF が選択されている時だけモーダル内に原本ビューを表示する */ const showPdf = !!assist && !!assist.file; + // 入力フォームと PDF カラムの比率をスプリッターのドラッグで変える。 + // PDF カラムは右側なので「幅 = コンテナ右端 - マウス X」。reservedGap はカラム間のスプリッター(6px)。 + const bodyWrapRef = useRef(null); + const { width: pdfWidth, startResize } = usePdfPanelLayout(bodyWrapRef, { + initialWidth: 440, + minWidth: 240, + minFormWidth: 320, + reservedGap: 6, + }); + return (
-
e.stopPropagation()} - > +
e.stopPropagation()}>
{project ? "プロジェクト編集" : "プロジェクト追加"} @@ -84,7 +94,11 @@ export function ProjectModal({
-
+
{showPdf && assist && ( - + <> + {/* 入力フォームと PDF カラムの境界。ドラッグで左右比率を変える(縦積み時は CSS で非表示)。 */} +
+ + )}
diff --git a/frontend/src/components/forms/ResumePdfTracePanel.module.css b/frontend/src/components/forms/ResumePdfTracePanel.module.css index 1d7814ab..e280b2e4 100644 --- a/frontend/src/components/forms/ResumePdfTracePanel.module.css +++ b/frontend/src/components/forms/ResumePdfTracePanel.module.css @@ -7,7 +7,7 @@ min-width: 0; } -/* VSCode タブ風ヘッダー(ファイル名 + ズーム + 最大化)。常に上部に固定。 */ +/* VSCode タブ風ヘッダー(ファイル名 + ズーム)。常に上部に固定。 */ .tab { display: flex; align-items: center; @@ -16,15 +16,15 @@ flex-shrink: 0; padding: 0.3rem 0.5rem; margin-bottom: 0.5rem; - background: #f1f3f5; - border: 1px solid #d0d7de; + background: var(--bg-header); + border: 1px solid var(--border); border-radius: 6px; user-select: none; } .tabName { font-size: 0.8rem; - color: #1a1a1a; + color: var(--text-primary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; @@ -39,23 +39,26 @@ .zoomLabel { font-size: 0.72rem; - color: #444; + color: var(--text-muted); min-width: 2.8em; text-align: center; } +/* ズームボタン。グローバルの button { color:#fff } を打ち消すため color を明示する + (未指定だと白背景×白文字で見えなくなる)。 */ .iconButton { font-size: 0.8rem; line-height: 1; padding: 0.15rem 0.45rem; - border: 1px solid #c0c6cc; + border: 1px solid var(--border-input); border-radius: 4px; - background: #fff; + background: var(--bg-input); + color: var(--text-primary); cursor: pointer; } .iconButton:hover { - background: #eef1f4; + background: var(--ghost-hover); } .iconButton:disabled { @@ -65,7 +68,7 @@ .hint { font-size: 0.8rem; - color: #333; + color: var(--text-secondary); margin: 0 0 0.5rem; flex-shrink: 0; } @@ -78,15 +81,15 @@ .empty { font-size: 0.85rem; - color: #666; + color: var(--text-muted); } /* スキャン PDF(テキストレイヤーなし)の警告 */ .warning { font-size: 0.85rem; - color: #b45309; - background: #fff7ed; - border: 1px solid #fed7aa; + color: var(--warning-text); + background: var(--warning-bg); + border: 1px solid var(--warning-border); border-radius: 6px; padding: 0.4rem 0.5rem; margin: 0 0 0.6rem; @@ -106,7 +109,7 @@ /* react-pdf の各ページ(canvas + テキストレイヤー)。選択しやすいよう枠で区切る。 */ .page { - border: 1px solid #e5e5e5; + border: 1px solid var(--border); border-radius: 4px; overflow: hidden; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08); diff --git a/frontend/src/components/forms/ResumePdfTracePanel.tsx b/frontend/src/components/forms/ResumePdfTracePanel.tsx index 66fa1d43..36af58ac 100644 --- a/frontend/src/components/forms/ResumePdfTracePanel.tsx +++ b/frontend/src/components/forms/ResumePdfTracePanel.tsx @@ -14,10 +14,6 @@ const ZOOM_STEP = 0.25; type Props = { assist: UseResumeImportAssistReturn; - /** 最大化(フォームに覆い被せて全幅表示)中か。タブのラベル切替に使う。 */ - maximized?: boolean; - /** 最大化トグル。渡された時だけタブに最大化ボタン/ダブルクリックを出す(モーダルでは渡さない)。 */ - onToggleMaximize?: () => void; }; /** @@ -25,9 +21,9 @@ type Props = { * * 選択された PDF を原本のまま描画し、ユーザーが文字をドラッグ選択すると、選択中(緑枠)の * 入力欄へ流し込む。「どの項目をどの粒度で取り込むか」は人間が PDF 上の選択で決める。 - * 上部のタブ風ヘッダーでズーム(拡大/縮小)と最大化(フォームに覆い被せる)を操作する。 + * 上部のタブ風ヘッダーでズーム(拡大/縮小)を操作する。 */ -export function ResumePdfTracePanel({ assist, maximized = false, onToggleMaximize }: Props) { +export function ResumePdfTracePanel({ assist }: Props) { const { file, fileName, error, fillSelection, setError } = assist; const [zoom, setZoom] = useState(1); @@ -38,12 +34,7 @@ export function ResumePdfTracePanel({ assist, maximized = false, onToggleMaximiz
{file && (
- + {fileName ?? IMPORT_ASSIST_MESSAGES.TAB_FALLBACK}
@@ -66,11 +57,6 @@ export function ResumePdfTracePanel({ assist, maximized = false, onToggleMaximiz > + - {onToggleMaximize && ( - - )}
)} diff --git a/frontend/src/constants.ts b/frontend/src/constants.ts index d577c7a3..b6f46b0b 100644 --- a/frontend/src/constants.ts +++ b/frontend/src/constants.ts @@ -86,9 +86,7 @@ export const blankCareerProject: CareerProjectForm = { end_date: "", is_current: false, role: "", - challenge: "", - action: "", - result: "", + description: "", team: { total: "", members: [] }, technology_stacks: [{ ...blankCareerTechnologyStack }], phases: [], diff --git a/frontend/src/constants/messages.ts b/frontend/src/constants/messages.ts index 22e93fa0..872428ce 100644 --- a/frontend/src/constants/messages.ts +++ b/frontend/src/constants/messages.ts @@ -81,9 +81,6 @@ export const IMPORT_ASSIST_MESSAGES = { RENDER_FAILED: "PDFの表示に失敗しました。別のファイルをお試しください。", NO_TARGET: "先にフォームの入力欄をクリックして、流し込み先を選んでください。", TAB_FALLBACK: "PDF", - MAXIMIZE: "最大化", - RESTORE: "元に戻す", - MAXIMIZE_HINT: "ダブルクリックで最大化/元に戻す", ZOOM_IN: "拡大", ZOOM_OUT: "縮小", } as const; diff --git a/frontend/src/formTypes.ts b/frontend/src/formTypes.ts index 0dc03374..661c0747 100644 --- a/frontend/src/formTypes.ts +++ b/frontend/src/formTypes.ts @@ -14,6 +14,4 @@ export type CareerProjectFieldKey = | "end_date" | "is_current" | "role" - | "challenge" - | "action" - | "result"; + | "description"; diff --git a/frontend/src/hooks/career/usePdfPanelLayout.test.ts b/frontend/src/hooks/career/usePdfPanelLayout.test.ts index 18198124..7a06a8c9 100644 --- a/frontend/src/hooks/career/usePdfPanelLayout.test.ts +++ b/frontend/src/hooks/career/usePdfPanelLayout.test.ts @@ -35,20 +35,13 @@ describe("usePdfPanelLayout", () => { document.body.style.userSelect = ""; }); - it("初期幅を返し、toggleMaximize で最大化を反転する", () => { + it("初期幅を返す", () => { const ref = makeContainerRef(1000, 900); const { result } = renderHook(() => usePdfPanelLayout(ref, { initialWidth: 360, minWidth: 280, minFormWidth: 360 }), ); expect(result.current.width).toBe(360); - expect(result.current.maximized).toBe(false); - - act(() => result.current.toggleMaximize()); - expect(result.current.maximized).toBe(true); - - act(() => result.current.toggleMaximize()); - expect(result.current.maximized).toBe(false); }); it("ドラッグで幅が変わり、最小/最大でクランプされる", () => { diff --git a/frontend/src/hooks/career/usePdfPanelLayout.ts b/frontend/src/hooks/career/usePdfPanelLayout.ts index 2011f2a6..77ba6be2 100644 --- a/frontend/src/hooks/career/usePdfPanelLayout.ts +++ b/frontend/src/hooks/career/usePdfPanelLayout.ts @@ -2,12 +2,10 @@ import { useCallback, useEffect, useRef, useState } from "react"; import type { MouseEvent as ReactMouseEvent, RefObject } from "react"; /** - * PDF 取り込みパネルのレイアウト(左右リサイズ + 最大化)を管理するフック。 + * PDF 取り込みパネルのレイアウト(左右リサイズ)を管理するフック。 * * - リサイズ: フォームとパネルの間のスプリッターをドラッグして PDF カラム幅を変える。 * PDF は右カラムなので「幅 = コンテナ右端 - マウス X」。左にドラッグするほど広がる。 - * - 最大化: VSCode のタブをダブルクリックした時のように、PDF をフォームに覆い被せて全幅表示する - * (maximized=true)。再度トグルで元の 2 カラムに戻す。 * * 幅は最小 PDF 幅とフォームの最小確保幅でクランプする。 */ @@ -29,14 +27,10 @@ type Options = { }; export type UsePdfPanelLayoutReturn = { - /** PDF カラムの現在幅(px)。最大化中は無視される。 */ + /** PDF カラムの現在幅(px) */ width: number; - /** 最大化(フォームに覆い被せて全幅表示)中か */ - maximized: boolean; /** スプリッターの mousedown で呼ぶ。ドラッグ中の幅追従を開始する。 */ startResize: (e: ReactMouseEvent) => void; - /** 最大化⇔元の 2 カラムをトグルする。 */ - toggleMaximize: () => void; }; export function usePdfPanelLayout( @@ -46,7 +40,6 @@ export function usePdfPanelLayout( const { initialWidth = 360, minWidth = 280, minFormWidth = 360, reservedGap = 38 } = options; const [width, setWidth] = useState(initialWidth); - const [maximized, setMaximized] = useState(false); const resizingRef = useRef(false); // mousemove / mouseup は document 全体で拾う(スプリッター外へカーソルが出ても追従するため)。 @@ -81,7 +74,5 @@ export function usePdfPanelLayout( document.body.style.cursor = "col-resize"; }, []); - const toggleMaximize = useCallback(() => setMaximized((prev) => !prev), []); - - return { width, maximized, startResize, toggleMaximize }; + return { width, startResize }; } diff --git a/frontend/src/hooks/career/useProjectFormDirty.test.ts b/frontend/src/hooks/career/useProjectFormDirty.test.ts index 49d43c63..ff688ae3 100644 --- a/frontend/src/hooks/career/useProjectFormDirty.test.ts +++ b/frontend/src/hooks/career/useProjectFormDirty.test.ts @@ -13,9 +13,7 @@ function buildProject(overrides: Partial = {}): CareerProject end_date: "2022-03", is_current: false, role: "Eng", - challenge: "課題", - action: "行動", - result: "成果", + description: "課題・行動・成果", team: { total: "5", members: [{ role: "PM", count: "1" }] }, technology_stacks: [{ category: "language", name: "TypeScript" }], phases: ["要件定義", "開発"], diff --git a/frontend/src/hooks/career/useProjectFormDirty.ts b/frontend/src/hooks/career/useProjectFormDirty.ts index b0c88dbf..7c749778 100644 --- a/frontend/src/hooks/career/useProjectFormDirty.ts +++ b/frontend/src/hooks/career/useProjectFormDirty.ts @@ -15,9 +15,7 @@ export type ProjectFormDirty = { end_date: boolean; is_current: boolean; role: boolean; - challenge: boolean; - action: boolean; - result: boolean; + description: boolean; }; /** 体制セクションの dirty(全体人数 or メンバー配列が変化) */ team: boolean; @@ -47,9 +45,7 @@ export function useProjectFormDirty( end_date: local.end_date !== base.end_date, is_current: local.is_current !== base.is_current, role: local.role !== base.role, - challenge: local.challenge !== base.challenge, - action: local.action !== base.action, - result: local.result !== base.result, + description: local.description !== base.description, }; const team = !isDeepEqual(local.team, base.team); diff --git a/frontend/src/hooks/career/useProjectModalForm.test.ts b/frontend/src/hooks/career/useProjectModalForm.test.ts index ab1fffad..3b67487e 100644 --- a/frontend/src/hooks/career/useProjectModalForm.test.ts +++ b/frontend/src/hooks/career/useProjectModalForm.test.ts @@ -10,9 +10,7 @@ const sampleProject: CareerProjectForm = { end_date: "2024-12", is_current: false, role: "Backend", - challenge: "", - action: "", - result: "", + description: "", team: { total: "5", members: [{ role: "SE", count: "3" }] }, technology_stacks: [{ category: "language", name: "Python" }], phases: ["設計"], diff --git a/frontend/src/hooks/career/useProjectModalState.test.ts b/frontend/src/hooks/career/useProjectModalState.test.ts index 2b848591..6d181e09 100644 --- a/frontend/src/hooks/career/useProjectModalState.test.ts +++ b/frontend/src/hooks/career/useProjectModalState.test.ts @@ -9,9 +9,7 @@ const dummyProject: CareerProjectForm = { end_date: "2024-12", is_current: false, role: "エンジニア", - challenge: "", - action: "", - result: "", + description: "", team: { total: "", members: [] }, technology_stacks: [], phases: [], diff --git a/frontend/src/payloadBuilders.test.ts b/frontend/src/payloadBuilders.test.ts index b60fd72f..d05991fa 100644 --- a/frontend/src/payloadBuilders.test.ts +++ b/frontend/src/payloadBuilders.test.ts @@ -17,9 +17,7 @@ const blankProject = (overrides: Partial = {}): CareerProject end_date: "2024-06", is_current: false, role: "", - challenge: "", - action: "", - result: "", + description: "", team: { total: "", members: [] }, technology_stacks: [], phases: [], diff --git a/frontend/src/payloadBuilders.ts b/frontend/src/payloadBuilders.ts index a3f8badf..b6c99fa1 100644 --- a/frontend/src/payloadBuilders.ts +++ b/frontend/src/payloadBuilders.ts @@ -21,9 +21,7 @@ export type CareerProjectForm = { end_date: string; is_current: boolean; role: string; - challenge: string; - action: string; - result: string; + description: string; team: { total: string; members: TeamMemberForm[]; @@ -89,9 +87,7 @@ function buildProject(proj: CareerProjectForm): CareerProject { end_date: proj.is_current ? "" : proj.end_date.trim(), is_current: proj.is_current, role: proj.role.trim(), - challenge: proj.challenge.trim(), - action: proj.action.trim(), - result: proj.result.trim(), + description: proj.description.trim(), team: buildTeam(proj.team), technology_stacks: proj.technology_stacks .map((stack) => ({ @@ -109,7 +105,7 @@ function buildClient(client: CareerClientForm): CareerClient { has_client: client.has_client, projects: client.projects .map(buildProject) - .filter((p) => hasAnyText([p.name, p.challenge, p.action, p.result])), + .filter((p) => hasAnyText([p.name, p.description])), }; } diff --git a/frontend/src/styles.css b/frontend/src/styles.css index 6aea5446..93527f89 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -2,6 +2,10 @@ color-scheme: light; font-family: "Noto Sans JP", "Hiragino Kaku Gothic ProN", sans-serif; + /* Layout(サイドバー幅・モバイル上部バー高。AuthenticatedLayout とモーダルのオフセットで共用) */ + --sidebar-width: 220px; + --topbar-height: 48px; + /* Backgrounds */ --bg-page: #f9f6f1; --bg-card: #ffffff; @@ -40,6 +44,11 @@ --error: #a72323; --success: #2e7d4f; + /* Warning (PDF スキャン警告などの注意喚起) */ + --warning-text: #b45309; + --warning-bg: #fff7ed; + --warning-border: #fed7aa; + /* Shadow */ --shadow-card: 0 14px 30px rgba(80, 60, 30, 0.06); @@ -101,6 +110,10 @@ --error: #f87171; --success: #4ade80; + --warning-text: #fbbf24; + --warning-bg: #422006; + --warning-border: #854d0e; + --shadow-card: 0 14px 30px rgba(0, 0, 0, 0.3); --header-btn-hover: rgba(255, 255, 255, 0.06); diff --git a/frontend/src/types.ts b/frontend/src/types.ts index f5774681..74b76f68 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -53,9 +53,8 @@ export type CareerProject = { end_date: string; is_current: boolean; role: string; - challenge: string; - action: string; - result: string; + /** 課題・行動・成果を統合した自由記述欄(見出し「詳細」) */ + description: string; team: ProjectTeam; technology_stacks: CareerTechnologyStack[]; phases: string[]; diff --git a/frontend/tests/payloadBuilders.test.cjs b/frontend/tests/payloadBuilders.test.cjs index a1c556de..5afbdf1d 100644 --- a/frontend/tests/payloadBuilders.test.cjs +++ b/frontend/tests/payloadBuilders.test.cjs @@ -28,9 +28,7 @@ test("buildCareerPayload trims data and keeps only non-empty technology stacks", end_date: "2021-03", is_current: false, role: " メンバー ", - challenge: " 課題テスト ", - action: " 行動テスト ", - result: " パフォーマンス改善 ", + description: " 課題・行動・成果テスト ", team: { total: " 5 ", members: [ @@ -79,6 +77,10 @@ test("buildCareerPayload trims data and keeps only non-empty technology stacks", assert.equal(payload.experiences[0].clients[0].projects.length, 1); assert.equal(payload.experiences[0].clients[0].projects[0].name, "プロジェクトA"); assert.equal(payload.experiences[0].clients[0].projects[0].role, "メンバー"); + assert.equal( + payload.experiences[0].clients[0].projects[0].description, + "課題・行動・成果テスト" + ); assert.deepEqual(payload.experiences[0].clients[0].projects[0].technology_stacks, [ { category: "framework",