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'| {left_content} | '
- f'{right_content} | '
- f'{team_text} |
'
+ f"| 業務内容 | スキルセット |
"
+ f'| {left_content} | '
+ f'{right_content} |
'
f"
"
)
@@ -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 && (