diff --git a/B09_Estimation/B09_Estimation_Router.py b/B09_Estimation/B09_Estimation_Router.py index 392b11005..0da23dee3 100644 --- a/B09_Estimation/B09_Estimation_Router.py +++ b/B09_Estimation/B09_Estimation_Router.py @@ -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) diff --git a/B09_Estimation/draft/B09_Draft_Print_Page.ts b/B09_Estimation/draft/B09_Draft_Print_Page.ts index c22a9b6dd..5007b325c 100644 --- a/B09_Estimation/draft/B09_Draft_Print_Page.ts +++ b/B09_Estimation/draft/B09_Draft_Print_Page.ts @@ -49,6 +49,8 @@ interface Area { items: DrawingItem[]; pdf: (ids: string[]) => Promise; xlsx?: (ids: string[]) => Promise; + /** 그 PDF 곁 알림(도면 = 못 넣은 그림 수) */ + note?: (pdf: Blob) => string; } async function post(projectId: string, path: string, body: unknown): Promise { @@ -88,7 +90,12 @@ async function loadArea(projectId: string, id: AreaId): Promise { 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) | 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), ); diff --git a/resources/tester/test_302_1_print_frame.py b/resources/tester/test_302_1_print_frame.py index e64df7bb9..a5c4fe0ff 100644 --- a/resources/tester/test_302_1_print_frame.py +++ b/resources/tester/test_302_1_print_frame.py @@ -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