diff --git a/B08_Quantity/B08_Quantity_Engine_StructureLibrary.py b/B08_Quantity/B08_Quantity_Engine_StructureLibrary.py index 7878a655..2e730dae 100644 --- a/B08_Quantity/B08_Quantity_Engine_StructureLibrary.py +++ b/B08_Quantity/B08_Quantity_Engine_StructureLibrary.py @@ -28,6 +28,9 @@ LIBRARY_SUBDIR = "library" #: 프로그램 기본 작업본 자리 — 회사 번호 폴더와 안 겹치게 밑줄 이름. PROGRAM_SUBDIR = "_program" +#: 프로그램 기본 발행본에서 뺄 출처 칸 — 어느 공사인지(2026-09-14 브레인 판정 ②). +ORIGIN_MASKED_KEYS = ("project", "file") +ORIGIN_MASKED_NOTE = "원문에서 뽑음 — 공사명은 발행 시 가림" def program_library_dir() -> Path: @@ -194,6 +197,10 @@ def save_personal( for row in overridden_rows(template, overrides) ] item = {k: v for k, v in template.items() if k != "imported_from"} + if tier == "program" and isinstance(item.get("origin"), dict): + # 모든 회사로 가는 발행본 — 원문 공사명·파일명은 안 실음(원본·회사 단은 그대로 · 브레인 ②③). + kept = {k: v for k, v in item["origin"].items() if k not in ORIGIN_MASKED_KEYS} + item["origin"] = {**kept, "masked": ORIGIN_MASKED_NOTE} if unit_price_rows is not None: item["unit_price"] = {**(item.get("unit_price") or {}), "rows": unit_price_rows} _write(folder, {**item, "code": code, "library_tier": tier, "rows": rows}) diff --git a/B08_Quantity/B08_Quantity_Engine_StructureTemplate.py b/B08_Quantity/B08_Quantity_Engine_StructureTemplate.py index 1f8c704f..8e089bd6 100644 --- a/B08_Quantity/B08_Quantity_Engine_StructureTemplate.py +++ b/B08_Quantity/B08_Quantity_Engine_StructureTemplate.py @@ -441,6 +441,8 @@ def apply_templates( "name": template.get("name"), "code": template.get("code"), "imported_from": template.get("imported_from"), + # 발행 확인창이 「○○공사에서 뽑은 것 — 공사명은 빼고 발행」을 보이게(브레인 ②). + "origin_project": (template.get("origin") or {}).get("project"), } sheet["formula_sheet"] = body # 실무 관측값 같은 「대안 후보」 — 값을 바꾸지 않고 칸 옆에 보이기만(판정 Ⓑ). diff --git a/B08_Quantity/B08_Quantity_UI_StructureSheet.ts b/B08_Quantity/B08_Quantity_UI_StructureSheet.ts index 72e5c75a..4529fc4b 100644 --- a/B08_Quantity/B08_Quantity_UI_StructureSheet.ts +++ b/B08_Quantity/B08_Quantity_UI_StructureSheet.ts @@ -86,6 +86,8 @@ export interface StructureSheet extends StandardSheetSpec { name: string; code?: string | null; imported_from?: string | null; + /** 뽑아 온 원문 공사명(STmate 고정형) — 프로그램 기본 발행 때 가림. */ + origin_project?: string | null; }; /** 화면이 조작 중 왕복 없이 다시 풀 장 한 벌(L=1, 고친 식 얹힘). */ formula_sheet?: FormulaSheet; @@ -422,6 +424,7 @@ export function renderStructureSheets(projectId: string | null): HTMLElement { sheetKey: sheet.key, typeId: sheet.library_item.type_id, currentCode: sheet.library_item.code ?? null, + originProject: sheet.library_item.origin_project, isDirty: () => dirty, confirmTake: () => { const edited = editedRows(sheet); diff --git a/B08_Quantity/B08_Quantity_UI_StructureSheet_Library.ts b/B08_Quantity/B08_Quantity_UI_StructureSheet_Library.ts index e3bad0ed..8ef3da1e 100644 --- a/B08_Quantity/B08_Quantity_UI_StructureSheet_Library.ts +++ b/B08_Quantity/B08_Quantity_UI_StructureSheet_Library.ts @@ -52,6 +52,8 @@ export interface LibraryPanelOptions { /** 저장 안 한 식이 있으면 [내 라이브러리에 저장]을 막음 — 저장된 식만 개인 단으로 감. */ isDirty: () => boolean; onImported: (notes: string[]) => Promise; + /** 이 장 양식이 뽑아 온 원문 공사명 — 프로그램 기본 발행 확인창에 「빼고 발행」을 알림. */ + originProject?: string | null; } /** 가져오기 · [내 라이브러리에 저장] · [내 것 지우기] 칸. 개인 단 두 단추는 프로젝트를 안 바꿈. */ @@ -243,7 +245,15 @@ export function buildLibraryPanel(options: LibraryPanelOptions): HTMLElement { const button = personal(label, async () => { if (isDirty()) return "저장 안 한 식이 있음 — [식 저장] 먼저"; const whom = tier === "program" ? "모든 회사가 쓰는 프로그램 기본" : "우리 회사 라이브러리"; - if (!window.confirm(`이 장의 양식과 고친 식을 ${whom}에 발행 — 같은 종류가 있으면 덮어씀`)) { + const masked = + tier === "program" && options.originProject + ? `\n이 항목은 「${options.originProject}」에서 뽑은 것 — 공사명은 빼고 발행됩니다` + : ""; + if ( + !window.confirm( + `이 장의 양식과 고친 식을 ${whom}에 발행 — 같은 종류가 있으면 덮어씀${masked}`, + ) + ) { return ""; } await readJson( diff --git a/resources/tester/test_b08_structure_library.py b/resources/tester/test_b08_structure_library.py index ef5af844..8fab215f 100644 --- a/resources/tester/test_b08_structure_library.py +++ b/resources/tester/test_b08_structure_library.py @@ -261,6 +261,41 @@ def test_발행은_권한대로_회사_기본_단에_같은_모양으로(client: ] +def test_프로그램_기본_발행본엔_원문_공사명_파일명을_안_싣는다(tmp_path: Path) -> None: + """2026-09-14 브레인 ② — 모든 회사로 가는 발행본에서만 가림 · 원본·회사 단은 그대로(③).""" + origin = { + "kind": "stmate_xlsx", + "file": "봉화.xlsx", + "project": "2024년 봉화 임도", + "hopyo_no": 6, + } + template = {**load_template("masonry_wet"), "origin": origin} + program = library_module.save_personal(tmp_path / "program", template, None, None, "program") + company = library_module.save_personal(tmp_path / "company", template, None, None, "company") + published = json.loads((tmp_path / "program" / f"{program}.json").read_text(encoding="utf-8")) + kept = json.loads((tmp_path / "company" / f"{company}.json").read_text(encoding="utf-8")) + assert "project" not in published["origin"] and "file" not in published["origin"] + assert published["origin"]["masked"] == "원문에서 뽑음 — 공사명은 발행 시 가림" + assert published["origin"]["kind"] == "stmate_xlsx" + assert kept["origin"] == origin + assert template["origin"] == origin # 원본은 안 건드림 + + +def test_발행_확인창이_가릴_공사명을_보인다() -> None: + ui = (ROOT / "B08_Quantity" / "B08_Quantity_UI_StructureSheet_Library.ts").read_text( + encoding="utf-8" + ) + assert "originProject" in ui and "공사명은 빼고 발행됩니다" in ui + sheet_ui = (ROOT / "B08_Quantity" / "B08_Quantity_UI_StructureSheet.ts").read_text( + encoding="utf-8" + ) + assert "originProject: sheet.library_item.origin_project" in sheet_ui + template_py = (ROOT / "B08_Quantity" / "B08_Quantity_Engine_StructureTemplate.py").read_text( + encoding="utf-8" + ) + assert '"origin_project"' in template_py + + def test_회사_없는_사람은_개인_단을_못_쓴다(client: TestClient) -> None: client.app.dependency_overrides[verify_session] = lambda: {"company_id": None, "user_id": 1} sheet = _sheet(client)