diff --git a/backend/app/services/markdown/generators/resume_generator.py b/backend/app/services/markdown/generators/resume_generator.py index e9f35c58..131b5f4f 100644 --- a/backend/app/services/markdown/generators/resume_generator.py +++ b/backend/app/services/markdown/generators/resume_generator.py @@ -144,7 +144,7 @@ def build_resume_markdown(payload: dict[str, Any]) -> str: f"{CATEGORY_LABELS.get(c, c)}: {', '.join(ns)}" for c, ns in grouped.items() ] - lines.append(field_line("技術スタック", " / ".join(parts))) + lines.append(field_line("スキルセット", " / ".join(parts))) lines.append("") self_pr = payload.get("self_pr", "") diff --git a/backend/app/services/pdf/generators/resume_generator.py b/backend/app/services/pdf/generators/resume_generator.py index 56ad0c5e..2b62093f 100644 --- a/backend/app/services/pdf/generators/resume_generator.py +++ b/backend/app/services/pdf/generators/resume_generator.py @@ -54,6 +54,24 @@ def _format_periods(periods: list) -> str: return "、".join(parts) +def _format_team(project) -> str: + """体制の人数情報を「6名(PM:1名、SE:6名)」形式の1行文字列にする。 + + 後方互換(旧 scale → team)の正規化は shared に集約。情報が無ければ空文字を返す。 + """ + team = normalize_team(project) + if not team: + return "" + total = _a(team, "total") + total_text = f"{_esc(total)}名" if total else "" + members = _a(team, "members", []) + member_strs = [ + f"{_esc(_a(m, 'role'))}:{_esc(_a(m, 'count', 0))}名" for m in members if _a(m, "role") + ] + member_text = f"({'、'.join(member_strs)})" if member_strs else "" + return f"{total_text}{member_text}" + + def _build_project_html(project) -> str: """プロジェクト1件分のHTMLを組み立てる""" # ヘッダー(3行構成: 期間/プロジェクト名、役割、工程) @@ -71,8 +89,11 @@ def _build_project_html(project) -> str: line1_parts.append(_esc(name)) line1 = " / ".join(line1_parts) if line1_parts else "" - # 2行目: 役割 - line2 = f"役割:{_esc(role)}" if role else "" + # 2行目: 役割(体制の人数情報を「、体制:…」として役割の右側に併記する) + team_text = _format_team(project) + role_part = f"役割:{_esc(role)}" if role else "" + team_part = f"体制:{team_text}" if team_text else "" + line2 = "、".join(p for p in [role_part, team_part] if p) # 3行目: 工程 line3 = "" @@ -91,7 +112,7 @@ def _build_project_html(project) -> str: left_parts.append(_md(description)) left_content = "".join(left_parts) if left_parts else "-" - # 右カラム: 開発環境(技術スタック) + # 右カラム: スキルセット(技術スタックのカテゴリ別表示) stacks = _a(project, "technology_stacks", []) grouped = group_stacks_by_category(stacks) right_parts: list[str] = [] @@ -102,28 +123,13 @@ def _build_project_html(project) -> str: ) right_content = "
".join(right_parts) if right_parts else "-" - # 体制(後方互換: 旧 scale → team の正規化は shared に集約) - team = normalize_team(project) - team_parts: list[str] = [] - if team: - total = _a(team, "total") - if total: - team_parts.append(f"{_esc(total)}名") - members = _a(team, "members", []) - member_strs = [ - f"{_esc(_a(m, 'role'))}:{_a(m, 'count', 0)}" for m in members if _a(m, "role") - ] - if member_strs: - team_parts.append(" / ".join(member_strs)) - team_text = "
".join(team_parts) if team_parts else "-" - + # 体制は表のカラムではなく役割行に併記する(line2 で処理済み)。 return ( f'
{header_html}' f'' - f"" - f'' - f'' - f'' + f"" + f'' + f'' f"
業務内容開発環境体制
{left_content}{right_content}{team_text}
業務内容スキルセット
{left_content}{right_content}
" ) @@ -223,7 +229,7 @@ def _build_html(resume: dict) -> str: client_name = _a(client, "name") if client_name: parts.append( - f'
' f"取引先名:{_esc(client_name)}
", + f'
' f"案件名:{_esc(client_name)}
", ) projects = _a(client, "projects", []) for proj in projects: diff --git a/backend/app/services/pdf/templates/resume.css b/backend/app/services/pdf/templates/resume.css index 819c7455..e868d469 100644 --- a/backend/app/services/pdf/templates/resume.css +++ b/backend/app/services/pdf/templates/resume.css @@ -14,6 +14,7 @@ } body { + margin: 0; font-family: "NotoSansJP", sans-serif; font-size: 9pt; line-height: 1.6; @@ -39,13 +40,17 @@ h2 { border-bottom: 2px solid #333; padding-bottom: 1mm; margin: 6mm 0 3mm 0; + /* 見出しがページ末尾に孤立して直後の本文だけ次ページに行くのを防ぐ */ + page-break-after: avoid; } /* 会社ブロック */ .company { border: 0.5px solid #666; margin-bottom: 3mm; - page-break-inside: avoid; + /* 会社ブロック全体を 1 ページに固めると、収まらない時に丸ごと次ページへ送られ、 + 直前(職務要約など)に大きな空白が残る。ページ跨ぎを許可してページを詰める。 + 代わりに分割を避けるのは下位の .project 単位で行う。 */ } .company-header { @@ -54,6 +59,8 @@ h2 { font-size: 10pt; font-weight: bold; border-bottom: 0.3px solid #999; + /* ヘッダがページ末尾に孤立し本文だけ次ページに分かれるのを防ぐ */ + page-break-after: avoid; } .company-info { @@ -104,11 +111,17 @@ h2 { margin: 3mm 0 2mm 0; padding-bottom: 1mm; border-bottom: 2px solid #333; + /* 案件名見出しの孤立防止 */ + page-break-after: avoid; } /* プロジェクトテーブル */ .project { margin-bottom: 2mm; + /* 案件が残りスペースに収まらない時に丸ごと次ページへ送られると、 + 枠付き company 内に大きな余白が残る。ページを詰めるため分割を許可する + (長い案件はページ境界で割れるが、余白を残すより詰めることを優先)。 + 分割時の列見出しは project-table の thead で繰り返す。 */ } .project-header { @@ -139,17 +152,14 @@ h2 { } .project-table td.desc { - width: 60%; + /* 体制カラムを廃止した分を業務内容へ割り当てる */ + width: 75%; } .project-table td.env { width: 25%; } -.project-table td.team { - width: 15%; -} - /* 業務内容カラム内のラベル */ .project-table td.desc p { margin: 0 0 0.5em 0; diff --git a/frontend/package-lock.json b/frontend/package-lock.json index ab197200..1995faa9 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -6807,9 +6807,9 @@ } }, "node_modules/react-router": { - "version": "7.13.1", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.13.1.tgz", - "integrity": "sha512-td+xP4X2/6BJvZoX6xw++A2DdEi++YypA69bJUV5oVvqf6/9/9nNlD70YO1e9d3MyamJEBQFEzk6mbfDYbqrSA==", + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.17.0.tgz", + "integrity": "sha512-FDELK7rTMlCHO5+reyXsPlmfr7N1F91lPHsWYfMEGQm/KQ+F4JFM8jGoeQDmDvdTs93Fw9aSilH+uKRb4/jXvQ==", "license": "MIT", "dependencies": { "cookie": "^1.0.1", @@ -6829,12 +6829,12 @@ } }, "node_modules/react-router-dom": { - "version": "7.13.1", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.13.1.tgz", - "integrity": "sha512-UJnV3Rxc5TgUPJt2KJpo1Jpy0OKQr0AjgbZzBFjaPJcFOb2Y8jA5H3LT8HUJAiRLlWrEXWHbF1Z4SCZaQjWDHw==", + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.17.0.tgz", + "integrity": "sha512-fyU2yjGups/hE6Xz0I5ZYbVL8Gx29eCjgpHaRaTaVU+OOAdfRX05KsvyRm0GO8YQwOkhpU3MurW1jyMUJn+zSw==", "license": "MIT", "dependencies": { - "react-router": "7.13.1" + "react-router": "7.17.0" }, "engines": { "node": ">=20.0.0" diff --git a/frontend/src/components/forms/CareerFormEditors/CareerExperienceEditor.tsx b/frontend/src/components/forms/CareerFormEditors/CareerExperienceEditor.tsx index 79e68cba..dad36b5f 100644 --- a/frontend/src/components/forms/CareerFormEditors/CareerExperienceEditor.tsx +++ b/frontend/src/components/forms/CareerFormEditors/CareerExperienceEditor.tsx @@ -85,7 +85,8 @@ export function CareerExperienceEditor({ } > -
+ {/* 会社名:事業内容:IT企業 = 4.5:5:0.5 の幅比で配置 */} +
+ {/* IT企業かどうか(非ITは案件を持たず詳細のみ)。会社名・事業内容と同じ行に配置。 + 上段は labelText のスペーサで隣のラベル行と高さを合わせ、下段のトグルを入力欄と横並びにする。 */} +
@@ -215,19 +236,6 @@ export function CareerExperienceEditor({
- {/* IT企業かどうか(非ITは取引先を持たず詳細のみ) */} - - {!exp.is_it_company && (