fix(b09-print): 도면 영역 이어 붙임 — 초안 router 가 스스로 단 /api/projects 머리를 사본 길에서 떼고 끼움(앞머리 두 번 겹침 없앰) · 못 넣은 그림 수 알림 (302-1)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QfHKQs7J3hQELKMWQMUJEy
This commit is contained in:
2026-10-13 08:06:32 +09:00
co-authored by Claude Opus 5.5
parent feea843595
commit a53b499878
3 changed files with 45 additions and 4 deletions
+12 -2
View File
@@ -29,6 +29,7 @@
from __future__ import annotations
import asyncio
import copy
import importlib
import logging
from pathlib import Path
@@ -347,11 +348,20 @@ router.include_router(quotes_router)
# 302 초안 길(draft/*_Router.py 의 router · 폴더째 지우면 길도 없음) — 같은 머리 · 같은 권한
def include_drafts(target: APIRouter, folder: Path, package: str) -> list[str]:
"""folder 의 *_Router.py 마다 router 를 끼움 → 끼운 파일 이름(못 읽은 초안은 건너뜀)."""
"""folder 의 *_Router.py 마다 router 를 끼움 → 끼운 파일 이름(못 읽은 초안은 건너뜀).
초안이 같은 머리(/api/projects)를 스스로 달았으면 떼고 끼움 — 머리가 두 번 붙지 않게."""
done = []
for path in sorted(folder.glob("*_Router.py")):
try:
target.include_router(importlib.import_module(f"{package}.{path.stem}").router)
draft = importlib.import_module(f"{package}.{path.stem}").router
if target.prefix and draft.prefix.startswith(target.prefix):
# 길은 만들 때 머리가 박힘 — 사본 길에서 떼어 냄(원본은 앱에 바로 붙일 때 그대로)
shim, cut = copy.copy(draft), len(target.prefix)
shim.routes = [copy.copy(route) for route in draft.routes]
for route in shim.routes:
route.path = route.path[cut:]
draft = shim
target.include_router(draft)
done.append(path.stem)
except (ImportError, AttributeError) as error: # 한 초안이 막혀도 B09 는 뜸
logging.getLogger(__name__).warning("초안 길 %s 못 읽음 — %s", path.name, error)
+9 -2
View File
@@ -49,6 +49,8 @@ interface Area {
items: DrawingItem[];
pdf: (ids: string[]) => Promise<Blob>;
xlsx?: (ids: string[]) => Promise<Blob>;
/** 그 PDF 곁 알림(도면 = 못 넣은 그림 수) */
note?: (pdf: Blob) => string;
}
async function post(projectId: string, path: string, body: unknown): Promise<Blob> {
@@ -88,7 +90,12 @@ async function loadArea(projectId: string, id: AreaId): Promise<Area | null> {
const list = mod.fetchDrawingList as FetchDrawingList | undefined;
const pdf = mod.fetchDrawingsPdf as FetchDrawingsPdf | undefined;
if (!list || !pdf) return null;
return { items: await list(projectId), pdf: (ids) => pdf(projectId, ids) };
const skipped = mod.skippedImagesOf as ((b: Blob) => Record<string, number>) | undefined;
const note = (b: Blob) => {
const n = Object.values(skipped?.(b) ?? {}).reduce((a, v) => a + v, 0);
return n ? ` · 못 넣은 그림 ${n}개` : "";
};
return { items: await list(projectId), pdf: (ids) => pdf(projectId, ids), note };
}
const collect = mod[id === "quantity" ? "collectQuantitySheets" : "collectCostSheets"] as
CollectSheets | undefined;
@@ -160,7 +167,7 @@ export function renderB09Print(root: HTMLElement): MountedPage {
if (my !== seq) return;
shown!.url = URL.createObjectURL(b);
frame.src = shown!.url;
say(`${label()} · ${ids.length}쪽 묶음`);
say(`${label()} · ${ids.length}장 묶음${area?.note?.(b) ?? ""}`);
},
(error: unknown) => my === seq && say(`PDF 를 못 만들었습니다 — ${errText(error)}`, true),
);
@@ -132,6 +132,21 @@ def test_서버_초안_길_끼우기_폴더_없으면_길_없음(tmp_path, monke
got = APIRouter()
assert b09.include_drafts(got, pkg / "draft", "fakepkg.draft") == ["X_Router"]
assert [r.path for r in got.routes] == ["/x"]
# 초안이 같은 머리를 스스로 달았으면 한 번만(도면 302-2 꼴)
(pkg / "draft/Z_Router.py").write_text(
"from fastapi import APIRouter\nrouter = APIRouter(prefix='/api/projects')\n"
"@router.get('/{project_id}/z')\ndef z():\n return 1\n"
)
head = APIRouter(prefix="/api/projects")
assert b09.include_drafts(head, pkg / "draft", "fakepkg.draft") == ["X_Router", "Z_Router"]
assert {r.path for r in head.routes} == {"/api/projects/x", "/api/projects/{project_id}/z"}
assert [r.path for r in sys.modules["fakepkg.draft.Z_Router"].router.routes] == [
"/api/projects/{project_id}/z"
] # 원본 그대로(앱에 바로 붙이는 시험)
assert (
"/api/projects/{project_id}/print/drawings-pdf" in paths
or not (DRAFT / "B09_Draft_Print_Drawings_Router.py").exists()
)
def test_로그인_회사_권한은_B09_길과_같음():
@@ -313,3 +328,12 @@ def test_원가_영역_PDF_시트_이름_가로(cost_bodies):
text = "".join(p.get_text() for p in doc)
assert all(s["name"] in text for s in body["sheets"])
assert all(p.rect.width > p.rect.height for p in doc)
def test_앱_전체_길_목록에_겹친_앞머리_없음():
import main
paths = [getattr(r, "path", "") for r in main.app.routes]
assert not [p for p in paths if "/api/projects/api/projects" in p]
for tail in ("drawings", "drawings-pdf", "table-pdf", "table-xlsx"):
assert paths.count(f"/api/projects/{{project_id}}/print/{tail}") == 1, tail