Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 0 additions & 17 deletions backend/app/routers/resumes.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
from ..repositories import ResumeRepository
from ..schemas import (
ResumeCreate,
ResumePreviewResponse,
ResumeResponse,
ResumeUpdate,
)
Expand All @@ -20,7 +19,6 @@
)
from ..services.pdf.generators.resume_generator import (
build_resume_pdf,
build_resume_preview,
)
from .download_utils import stream_markdown, stream_pdf

Expand Down Expand Up @@ -70,21 +68,6 @@ def create_resume(
) from exc


@router.post("/preview", response_model=ResumePreviewResponse)
def preview_resume(
payload: ResumeCreate,
current_user: User = Depends(get_current_user),
) -> ResumePreviewResponse:
"""保存せずに、職務経歴書を PDF と同じレイアウトに整形した HTML と画面用 CSS を返す。

左右 diff プレビュー(左=保存済み / 右=編集中)の描画に使う。HTML 内の各値ノードには
form パス(``data-fp``)が付与され、FE が変更箇所のハイライト・スクロール先特定に使う。
DB は更新しない。WeasyPrint を通さず HTML 文字列生成のみのため軽量。
"""
html, css = build_resume_preview(payload.model_dump())
return ResumePreviewResponse(html=html, css=css)


@router.get("/latest", response_model=ResumeResponse)
def get_latest_resume(
db: Session = Depends(get_db),
Expand Down
2 changes: 0 additions & 2 deletions backend/app/schemas/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@
Project,
ProjectTeam,
ResumeCreate,
ResumePreviewResponse,
ResumeQualificationItem,
ResumeResponse,
ResumeUpdate,
Expand Down Expand Up @@ -59,7 +58,6 @@
"Project",
"ProjectTeam",
"ResumeCreate",
"ResumePreviewResponse",
"ResumeQualificationItem",
"ResumeResponse",
"ResumeUpdate",
Expand Down
12 changes: 0 additions & 12 deletions backend/app/schemas/resume.py
Original file line number Diff line number Diff line change
Expand Up @@ -280,15 +280,3 @@ class ResumeResponse(ResumeBase):
def serialize_as_jst(self, dt: datetime) -> str:
"""UTC datetime を JST (UTC+9) の ISO 8601 文字列にシリアライズする。"""
return to_jst(dt).isoformat()


class ResumePreviewResponse(BaseModel):
"""保存前プレビュー(左右 diff 表示)用の整形済み HTML と画面用 CSS。

DB を更新せず、編集中 payload を PDF と同じレイアウトに整形した HTML を返す。
HTML 内の各値ノードには form パス(``data-fp``)が付与され、FE が変更箇所の
ハイライト・スクロール先特定に使う。
"""

html: str
css: str
26 changes: 3 additions & 23 deletions backend/app/services/pdf/generators/resume_generator.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import re
from datetime import datetime
from html import escape as _html_escape
from pathlib import Path
Expand All @@ -22,9 +21,6 @@
Path(__file__).resolve().parent.parent.parent.parent / "fonts" / "NotoSansJP-Regular.ttf"
)

# 画面プレビュー(左右 diff)で除去する PDF 専用フォント定義。url 解決が画面では不要なため。
_FONT_FACE_RE = re.compile(r"@font-face\s*\{[^}]*\}")


def _esc(text: str) -> str:
"""HTMLエスケープのショートカット"""
Expand Down Expand Up @@ -367,34 +363,18 @@ def _build_html(resume: dict) -> str:
return "\n".join(parts)


def _load_css(for_screen: bool) -> str:
"""resume.css を読み込む。

for_screen=True(左右 diff プレビュー): PDF 用フォント(@font-face)を除去し、
body の font-family フォールバック(sans-serif)に任せる。
for_screen=False(PDF 生成): {{ font_path }} を実フォントの URI に置換する。
"""
def _load_css() -> str:
"""resume.css を読み込み、{{ font_path }} を実フォントの URI に置換する(PDF 生成用)。"""
css_text = _CSS_PATH.read_text(encoding="utf-8")
if for_screen:
return _FONT_FACE_RE.sub("", css_text)
return css_text.replace("{{ font_path }}", _FONT_PATH.as_uri())


def build_resume_preview(resume: dict) -> tuple[str, str]:
"""保存前プレビュー(左右 diff)用に (body HTML, 画面用 CSS) を返す。

HTML は data-fp 付き(`_build_html`)。CSS は PDF 専用フォントを除いた画面表示版。
frontend は両者を iframe srcdoc に流し込んで baseline / 編集中を並べて描画する。
"""
return _build_html(resume), _load_css(for_screen=True)


def build_resume_pdf(resume: dict) -> bytes:
"""職務経歴書データからPDFバイト列を生成する"""
html_body = _build_html(resume)

# CSSテンプレートを読み込み、フォントパスを埋め込む
css_text = _load_css(for_screen=False)
css_text = _load_css()

full_html = (
"<!DOCTYPE html>"
Expand Down
79 changes: 0 additions & 79 deletions backend/tests/test_endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
"method,path",
[
("post", "/api/resumes"),
("post", "/api/resumes/preview"),
("get", "/api/resumes/latest"),
],
)
Expand Down Expand Up @@ -347,81 +346,3 @@ def test_resume_not_found(client: TestClient) -> None:
headers=headers,
)
assert resp.status_code == 404


# ── 保存前プレビュー(左右 diff 用 HTML)────────────────────────


def test_resume_preview_returns_annotated_html(client: TestClient) -> None:
"""保存せずに整形 HTML(data-fp 付き)と CSS を返す。"""
headers = auth_header(client, "resume-preview-user")
resp = client.post(
"/api/resumes/preview",
json={
"full_name": "山田太郎",
"email": "yamada@example.com",
"career_summary": "キャリアサマリー",
"self_pr": "自己PR",
"experiences": [
{
"company": "Example株式会社",
"business_description": "SES事業",
"start_date": "2020-04",
"end_date": "2024-03",
"is_current": False,
"is_it_company": True,
"clients": [
{
"name": "取引先A",
"has_client": True,
"projects": [
{
"name": "案件X",
"role": "SE",
"periods": [
{
"start_date": "2020-04",
"end_date": "2021-03",
"is_current": False,
}
],
}
],
}
],
}
],
"qualifications": [{"acquired_date": "2020-04", "name": "応用情報技術者"}],
},
headers=headers,
)
assert resp.status_code == 200
body = resp.json()
html = body["html"]
# form パス(data-fp)が careerDiff のパス文字列と一致して付与されていること
assert 'data-fp="full_name"' in html
assert 'data-fp="experiences.0.company"' in html
assert 'data-fp="experiences.0.clients.0.name"' in html
assert 'data-fp="experiences.0.clients.0.projects.0.role"' in html
assert 'data-fp="qualifications.0.name"' in html
# 画面用 CSS は PDF 専用フォント定義を含まない
assert "@font-face" not in body["css"]
assert ".project-table" in body["css"]


def test_resume_preview_validation_error(client: TestClient) -> None:
"""必須欠落(full_name 空)は 422 を返す。"""
headers = auth_header(client, "resume-preview-invalid-user")
resp = client.post(
"/api/resumes/preview",
json={
"full_name": "",
"email": "yamada@example.com",
"career_summary": "x",
"self_pr": "y",
"experiences": [],
"qualifications": [],
},
headers=headers,
)
assert resp.status_code == 422
16 changes: 0 additions & 16 deletions backend/tests/test_pdf_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
from app.services.pdf.generators.resume_generator import (
_build_html,
build_resume_pdf,
build_resume_preview,
)
from app.services.pdf.utils.pdf_utils import (
decode_photo as _decode_photo,
Expand Down Expand Up @@ -249,21 +248,6 @@ def test_build_resume_pdf_still_works_with_annotations() -> None:
assert pdf_bytes.startswith(b"%PDF")


def test_build_resume_preview_returns_html_and_screen_css() -> None:
"""プレビューは (data-fp 付き HTML, 画面用 CSS) を返し、CSS は @font-face を含まない。"""
payload = {
"full_name": "山田太郎",
"career_summary": "要約",
"self_pr": "PR",
"qualifications": [],
"experiences": [],
}
html, css = build_resume_preview(payload)
assert 'data-fp="full_name"' in html
assert "@font-face" not in css
assert ".company" in css


def test_parse_date_ym() -> None:
assert _parse_date_ym("2020-04") == ("2020", "4")
assert _parse_date_ym("2020-12-01") == ("2020", "12")
Expand Down
15 changes: 1 addition & 14 deletions web/e2e/career-dirty-indicator.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,18 +43,6 @@ async function setupResumeApi(page: Page) {
route.fulfill({ status: 200, contentType: "application/json", body: "[]" }),
);

// 保存前プレビュー(左右 diff モーダル)。整形 HTML と CSS を返す。
await page.route("**/api/resumes/preview", (route) =>
route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({
html: '<div class="meta">氏名 <span data-fp="full_name">山田 太郎</span></div>',
css: "",
}),
}),
);

// 最新取得
await page.route("**/api/resumes/latest", (route) =>
route.fulfill({
Expand Down Expand Up @@ -247,9 +235,8 @@ test.describe("職務経歴書 未保存マーク", () => {
const dirtyCountAfterReturn = await page.getByTestId("dirty-dot").count();
expect(dirtyCountAfterReturn).toBeGreaterThanOrEqual(1);

// 4. 保存ボタン → 変更点 diff モーダルが開く → 「この内容で保存」で確定 → 🔴 が全消失
// 4. 保存ボタン → そのまま保存され 🔴 が全消失
await page.getByRole("button", { name: /更新する|保存する/ }).click();
await page.getByRole("button", { name: "この内容で保存" }).click();
await expect(page.getByTestId("dirty-dot")).toHaveCount(0);
});
});
71 changes: 0 additions & 71 deletions web/src/api/generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -663,30 +663,6 @@ export interface paths {
patch?: never;
trace?: never;
};
"/api/resumes/preview": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
/**
* Preview Resume
* @description 保存せずに、職務経歴書を PDF と同じレイアウトに整形した HTML と画面用 CSS を返す。
*
* 左右 diff プレビュー(左=保存済み / 右=編集中)の描画に使う。HTML 内の各値ノードには
* form パス(``data-fp``)が付与され、FE が変更箇所のハイライト・スクロール先特定に使う。
* DB は更新しない。WeasyPrint を通さず HTML 文字列生成のみのため軽量。
*/
post: operations["preview_resume_api_resumes_preview_post"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/resumes/{resume_id}": {
parameters: {
query?: never;
Expand Down Expand Up @@ -1789,20 +1765,6 @@ export interface components {
/** Self Pr */
self_pr: string;
};
/**
* ResumePreviewResponse
* @description 保存前プレビュー(左右 diff 表示)用の整形済み HTML と画面用 CSS。
*
* DB を更新せず、編集中 payload を PDF と同じレイアウトに整形した HTML を返す。
* HTML 内の各値ノードには form パス(``data-fp``)が付与され、FE が変更箇所の
* ハイライト・スクロール先特定に使う。
*/
ResumePreviewResponse: {
/** Css */
css: string;
/** Html */
html: string;
};
/** ResumeQualificationItem */
ResumeQualificationItem: {
/** Acquired Date */
Expand Down Expand Up @@ -3066,39 +3028,6 @@ export interface operations {
};
};
};
preview_resume_api_resumes_preview_post: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody: {
content: {
"application/json": components["schemas"]["ResumeCreate"];
};
};
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ResumePreviewResponse"];
};
};
/** @description Validation Error */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
get_resume_api_resumes__resume_id__get: {
parameters: {
query?: never;
Expand Down
1 change: 0 additions & 1 deletion web/src/api/paths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ export const PATHS = {
resumes: {
base: "/api/resumes",
latest: "/api/resumes/latest",
preview: "/api/resumes/preview",
byId: (id: string) => `/api/resumes/${id}`,
pdf: (id: string) => `/api/resumes/${id}/pdf`,
markdown: (id: string) => `/api/resumes/${id}/markdown`,
Expand Down
Loading
Loading