From 8ca741e482c753e7bf6dfb7eb2a9ae74652b3ee0 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sat, 17 Oct 2026 11:49:03 +0900 Subject: [PATCH 1/3] =?UTF-8?q?feat(=EC=9D=B4=EB=A6=84):=20=EC=A2=85?= =?UTF-8?q?=EB=8B=A8=EC=84=A4=EA=B3=84=20=E2=86=92=20=EB=85=B8=EC=84=A0?= =?UTF-8?q?=EC=84=A4=EA=B3=84=20=C2=B7=20=EC=A2=85=EB=8B=A8=20=EB=B3=B4?= =?UTF-8?q?=EA=B8=B0=20=E2=86=92=20=EB=85=B8=EC=84=A0=20=EB=B3=B4=EA=B8=B0?= =?UTF-8?q?=20(372-1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit B05 좌측 패널 제목 · ☰ 단계 · 안내 · 메일 · 도움말 제목 · 버튼 이름 · 시험 짝. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01XWh5wNNycMkbwMpACz1rte --- B03_FileInput/B03_FileInput_Email.py | 6 +++--- resources/help/B05/00_설명서.md | 2 +- resources/tester/helper_design_view_parity.cjs | 2 +- resources/tester/test_design_view_parity.py | 2 +- ui_template/ui_template_locale_b2.ts | 6 +++--- ui_template/ui_template_locale_common.ts | 2 +- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/B03_FileInput/B03_FileInput_Email.py b/B03_FileInput/B03_FileInput_Email.py index 4da466963..2c273837a 100644 --- a/B03_FileInput/B03_FileInput_Email.py +++ b/B03_FileInput/B03_FileInput_Email.py @@ -218,14 +218,14 @@ async def send_initial_analysis_complete_email( {design_rows}
- 기본값으로 계산한 결과입니다. 종단설계 화면에서 검토·수정한 뒤 횡단설계에서 + 기본값으로 계산한 결과입니다. 노선설계 화면에서 검토·수정한 뒤 횡단설계에서 [확정]을 누르면 다음 단계(수량 산출)로 넘어갑니다.
""" else: design_block = """
- 초기 노선·종횡단 자동 계산은 완료되지 않았습니다. 종단설계 화면에서 직접 + 초기 노선·종횡단 자동 계산은 완료되지 않았습니다. 노선설계 화면에서 직접 경로를 계산해 주세요.
""" @@ -239,7 +239,7 @@ async def send_initial_analysis_complete_email( {design_block}

- 종단설계 화면 열기 + 노선설계 화면 열기

로그인이 필요한 경우 인증 후 같은 화면으로 이동합니다. diff --git a/resources/help/B05/00_설명서.md b/resources/help/B05/00_설명서.md index 89349f545..31c6e3a65 100644 --- a/resources/help/B05/00_설명서.md +++ b/resources/help/B05/00_설명서.md @@ -1,4 +1,4 @@ -# 종단설계 설명서 +# 노선설계 설명서 ## 이 화면에서 하는 일 diff --git a/resources/tester/helper_design_view_parity.cjs b/resources/tester/helper_design_view_parity.cjs index 14d3a32da..17faa20ad 100644 --- a/resources/tester/helper_design_view_parity.cjs +++ b/resources/tester/helper_design_view_parity.cjs @@ -125,7 +125,7 @@ const LOCALES = { B06_Profile_Btn_Save: ["저장", "Save"], B06_Profile_Btn_Confirm: ["확정", "Confirm"], Design_Btn_ToCross: ["횡단 보기", "Cross view"], - Design_Btn_ToProfile: ["종단 보기", "Profile view"], + Design_Btn_ToProfile: ["노선 보기", "Route view"], }; const STUBS = { "@ui/ui_template_locale": { ui_locales: LOCALES, currentLanguageIndex: 0 }, diff --git a/resources/tester/test_design_view_parity.py b/resources/tester/test_design_view_parity.py index 7618fb22d..b2bcb5af3 100644 --- a/resources/tester/test_design_view_parity.py +++ b/resources/tester/test_design_view_parity.py @@ -95,7 +95,7 @@ def test_아래_단추는_네_개_보기_전환_글자만_따라감(results): # [확정]은 종단 보기에서도 켜진다(한 벌 저장이 횡단 자료를 같은 페이지에서 얻음). assert views[name]["dockDisabled"] == [False, False, False, False] assert views["profile"]["dockLabels"][3] == "횡단 보기" - assert views["cross"]["dockLabels"][3] == "종단 보기" + assert views["cross"]["dockLabels"][3] == "노선 보기" assert views["back"]["dockLabels"][3] == "횡단 보기" diff --git a/ui_template/ui_template_locale_b2.ts b/ui_template/ui_template_locale_b2.ts index 3470286fa..639b14279 100644 --- a/ui_template/ui_template_locale_b2.ts +++ b/ui_template/ui_template_locale_b2.ts @@ -108,7 +108,7 @@ export const ui_locales_b2 = { ], /* --- B05_Profile 경로 설계 --- */ - B05_Route_Title: ["종단설계", "Profile Design"], + B05_Route_Title: ["노선설계", "Route Design"], B05_Route_Group_Points: ["경로 제어점", "Route Control Points"], B05_Route_Point_BP: ["시점 (BP)", "Begin (BP)"], B05_Route_Point_EP: ["종점 (EP)", "End (EP)"], @@ -146,7 +146,7 @@ export const ui_locales_b2 = { B05_Route_Btn_GoCross: ["횡단 이동", "Go to Cross"], /* 좌측 패널 아래 단추 [보기 전환] — 가려는 쪽 이름(계획서 24-2b). */ Design_Btn_ToCross: ["횡단 보기", "Cross view"], - Design_Btn_ToProfile: ["종단 보기", "Profile view"], + Design_Btn_ToProfile: ["노선 보기", "Route view"], B05_Route_TempSave_Success: ["변경 내용을 저장했습니다.", "Changes saved."], B05_Route_TempSave_Failed: ["저장에 실패했습니다.", "Failed to save."], B05_Route_Reset_Confirm: [ @@ -800,7 +800,7 @@ export const ui_locales_b2 = { "Failed to load the material summary.", ], B08_Quantity_Haul_Missing: [ - "운반계획이 아직 없습니다. 종단설계에서 [확정]을 누르면 만들어집니다.", + "운반계획이 아직 없습니다. 노선설계에서 [확정]을 누르면 만들어집니다.", "No haul plan yet. Press [Confirm] on the profile design to build it.", ], B08_Quantity_Haul_Excluded: ["내역 제외", "Not billed"], diff --git a/ui_template/ui_template_locale_common.ts b/ui_template/ui_template_locale_common.ts index ddad8eb97..b7eb7c462 100644 --- a/ui_template/ui_template_locale_common.ts +++ b/ui_template/ui_template_locale_common.ts @@ -80,7 +80,7 @@ export const ui_locales_common = { * 워크플로우 공통 (B04~B09 상단 진행 단계 라벨) * ------------------------------------------------------------------------ */ WF_Step_Surface: ["전처리", "Preprocess"], - WF_Step_Route: ["종단설계", "Profile Design"], + WF_Step_Route: ["노선설계", "Route Design"], WF_Step_ProfileCross: ["횡단설계", "Cross Design"], WF_Step_DesignDetail: ["상세설계", "Detail Design"], WF_Step_Quantity: ["수량산출", "Quantity"], From 373f6703fec45d4e154728524927d23f7c6c1d65 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sat, 17 Oct 2026 11:56:28 +0900 Subject: [PATCH 2/3] =?UTF-8?q?feat(M02):=20=EC=8B=A4=EB=AC=B4=EC=9E=90?= =?UTF-8?q?=EB=A3=8C=20[=EC=A0=80=EC=9E=A5]=20=3D=20=EA=B0=9C=EC=9D=B8=20?= =?UTF-8?q?=EC=B8=B5=20=C2=B7=20=EB=B3=B5=EC=82=AC=20=EB=95=8C=20=EB=B3=B8?= =?UTF-8?q?=EB=9C=AC=20=EC=A7=80=ED=82=B4=20=C2=B7=20=EB=8B=A8=EA=B0=80?= =?UTF-8?q?=EC=82=B0=EC=B6=9C=20=EC=A7=80=EC=A0=95=20=EC=98=81=EC=97=AD=20?= =?UTF-8?q?B~F=20=EC=97=B4=20(372-2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 엑셀 보기 창 만들기 대상 개인 층 고정 · [삭제]는 화면 층 쓰기 권한 때만 · 층 사이 복사가 실무자료 본뜬(키 · 전국자재)을 덮지 않음(원본 참조 = 복사원본 칸). Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01XWh5wNNycMkbwMpACz1rte --- M02_MasterTemplete/M02_Labor_Ref.py | 10 +++++++++- M02_MasterTemplete/M02_Labor_Router.py | 2 +- .../M02_MasterTemplete_Hopyo_Sheet.ts | 4 ++-- .../M02_MasterTemplete_Hopyo_Side.ts | 7 +++++-- resources/tester/test_m02_hopyo_screen.py | 15 +++++++++++++-- resources/tester/test_m02_hopyo_seam.py | 2 +- resources/tester/test_m02_labor_national.py | 12 ++++++++++++ 7 files changed, 43 insertions(+), 9 deletions(-) diff --git a/M02_MasterTemplete/M02_Labor_Ref.py b/M02_MasterTemplete/M02_Labor_Ref.py index 722b87ef3..03f62c17c 100644 --- a/M02_MasterTemplete/M02_Labor_Ref.py +++ b/M02_MasterTemplete/M02_Labor_Ref.py @@ -326,6 +326,14 @@ def refuse_used(folder: Path, kind: str, key: str) -> None: # ── 층 사이 복사 ────────────────────────────────────────────────────── +def origin_mark(doc: dict[str, Any], ref: Ref) -> dict[str, Any]: + """복사 문서의 `본뜬` — 실무자료 본뜬(키 · 전국자재 …)은 지키고 원본 참조는 `복사원본` 칸에 둠.""" + own = doc.get("본뜬") + if isinstance(own, dict) and own.get("층") == "실무자료": + return {**doc, "본뜬": {**own, "복사원본": ref}} + return {**doc, "본뜬": ref} + + def carry(doc: dict[str, Any], source: Here) -> dict[str, Any]: """복사(원본 층 `source` → 지금 층) — 지금 층이 못 읽는 참조는 그 문서를 지금 층에 새 키로 같이 복사(`본뜬` = 원래 참조) · 참조를 새 키로 바꿈. 원본 층 읽기 권한은 부르는 쪽 몫.""" @@ -341,7 +349,7 @@ def carry(doc: dict[str, Any], source: Here) -> dict[str, Any]: got = read_ref(ref) if got is None: raise _bad(f"없는 참조 「{ref['키']}」") - body = {**got, "담은로직": _moved(got.get("담은로직"), move, depth + 1), "본뜬": ref} + body = origin_mark({**got, "담은로직": _moved(got.get("담은로직"), move, depth + 1)}, ref) made = create(kind_of(ref["키"]), body) done[ref_id(ref)] = {"층": target[0], "주인": target[1], "키": made["이름"]} return done[ref_id(ref)] diff --git a/M02_MasterTemplete/M02_Labor_Router.py b/M02_MasterTemplete/M02_Labor_Router.py index e0ad37249..7e278e7c4 100644 --- a/M02_MasterTemplete/M02_Labor_Router.py +++ b/M02_MasterTemplete/M02_Labor_Router.py @@ -97,7 +97,7 @@ class LibraryBody(BaseModel): def _copy(kind: str, doc: dict[str, Any], source: refs.Here, ref: dict) -> dict[str, Any]: - return refs.create(kind, refs.carry({**doc, "본뜬": ref}, source)) + return refs.create(kind, refs.carry(refs.origin_mark(doc, ref), source)) @router.post("/layers/{layer}/library/{kind}") diff --git a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Sheet.ts b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Sheet.ts index 687db5ab1..6702b1564 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Sheet.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Sheet.ts @@ -127,9 +127,9 @@ export function buildOrder(ones: { 번: number; 아래: number[] }[]): number[] return out; } -/** 단가산출 행 → 행 전체 범위 A1(`4:9`) */ +/** 단가산출 행 → B ~ F 열 범위 A1(`B4:F9`) */ export const rowsText = (행: number | [number, number]): string => - typeof 행 === "number" ? `${행}:${행}` : `${행[0]}:${행[1]}`; + typeof 행 === "number" ? `B${행}:F${행}` : `B${행[0]}:F${행[1]}`; export function openHopyoSheet(opt: HopyoSheetOptions): void { let sheet: SpreadsheetHandle | null = null; diff --git a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Side.ts b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Side.ts index 8e6585aa8..72d9c4a88 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Side.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Side.ts @@ -111,12 +111,15 @@ export function buildHopyoSection(opt: HopyoSectionOptions): HopyoSection { scope, key: file.키, 파일: file.파일, - canWrite: opt.canWrite, + // 사전조합은 어느 화면 층에서든 로그인한 사람 개인 층에 만듦 — 회사 · 마스터는 구조물 카드 [가져오기] + target: { layer: "personal", owner: null }, + canWrite: true, onClose: () => { if (sheetKey === file.키) sheetKey = null; paintList(); }, - onDeleted: () => void refresh(), + // [삭제]는 실무자료 파일이 있는 화면 층 쓰기 권한 때만 + onDeleted: opt.canWrite ? () => void refresh() : undefined, }); } diff --git a/resources/tester/test_m02_hopyo_screen.py b/resources/tester/test_m02_hopyo_screen.py index d36d273b2..33ac9f123 100644 --- a/resources/tester/test_m02_hopyo_screen.py +++ b/resources/tester/test_m02_hopyo_screen.py @@ -287,7 +287,7 @@ def test_엑셀_보기_창_행과_호표(): assert sheet["at"] == [None, 3, 3, 3, 4, 4, None] # 22행 → 제3호표 · 범위 밖 = 없음 assert sheet["span"] == "B20:O24" # 제3호표 영역 = B열 ~ 끝 열(열 폭 정보 O) assert sheet["last"] == ["AA", "A"] - assert sheet["rows"] == ["4345:4345", "93:101"] # 단가산출 행 전체 + assert sheet["rows"] == ["B4345:F4345", "B93:F101"] # 단가산출 행 B~F 열 # ── 소스 ────────────────────────────────────────────────────────────── @@ -348,7 +348,10 @@ def test_좌측_판_자리와_권한(): assert "size:" not in up and "엑셀 올리기" not in hopyo_side assert "buildHopyoView" not in hopyo_side assert 'row.addEventListener("click", () => void showSheet(f));' in hopyo_side - assert "onClose:" in hopyo_side and "onDeleted: () => void refresh()," in hopyo_side + assert ( + "onClose:" in hopyo_side + and "onDeleted: opt.canWrite ? () => void refresh() : undefined," in hopyo_side + ) assert 'accept: ".xls,.xlsx"' in hopyo_side and "input.hidden = true;" in hopyo_side assert "disabled: !opt.canWrite," in hopyo_side and "pageScope()" in hopyo_side assert "buildHopyoLedger({ scope, canWrite: opt.canWrite })" in hopyo_side # [별칭 장부] @@ -524,3 +527,11 @@ def test_명사형_단추와_별칭_장부_풀이_359(): 'label: "지우기"', ): assert gone not in every, gone + + +def test_372_2_저장은_개인_층에_삭제는_화면_층_권한(): + side = _text("M02_MasterTemplete_Hopyo_Side.ts") + open_ = side.split("sheet.openHopyoSheet({")[1].split("});")[0] + assert 'target: { layer: "personal", owner: null },' in open_ + assert "canWrite: true," in open_ # 만들기 = 개인 층 · 화면 층 무관 + assert "onDeleted: opt.canWrite ? () => void refresh() : undefined," in open_ diff --git a/resources/tester/test_m02_hopyo_seam.py b/resources/tester/test_m02_hopyo_seam.py index ad7c77fac..01e8b0895 100644 --- a/resources/tester/test_m02_hopyo_seam.py +++ b/resources/tester/test_m02_hopyo_seam.py @@ -53,7 +53,7 @@ def test_실제_응답_화면_도우미(client): assert states == {"맞음": 122, "다른 점": 86, "후보": 60} # 268 줄 · 셈 끝 assert got["count"]["top"] == 9 # 「후보」 줄 1순위 근거 맞음(근거 맞는 줄은 거의 숫자로 풀림) assert got["at"] == [None, 3, 3, 3, None, 4] # 25행 = 호표 사이 빈 줄 - assert got["span"] == "B20:O24" and got["rows"] == "4229:4244" + assert got["span"] == "B20:O24" and got["rows"] == "B4229:F4244" by = {x["이름"]: x for x in got["lines"]} dig = by["구조물터파기(인력100%)"] assert dig["source"] == "출처 건설품셈 3-2-1 · 직종 특별인부 · 단가산출 4229–4244행" diff --git a/resources/tester/test_m02_labor_national.py b/resources/tester/test_m02_labor_national.py index 80f18a17c..3bf5cc93a 100644 --- a/resources/tester/test_m02_labor_national.py +++ b/resources/tester/test_m02_labor_national.py @@ -120,3 +120,15 @@ def test_확인_창_세_칸_만들기_값(client): assert seoul["대상"][0]["세칸"]["자재구분"] == "사급" # 고른 자재 이름(-사급) 먼저 rows = (M02 / "M02_MasterTemplete_Hopyo_CheckRows.ts").read_text(encoding="utf-8") assert "...markCells(p?.세칸 ??" in rows + + +def test_복사_때_실무자료_본뜬과_전국자재_지킴(): + from M02_MasterTemplete import M02_Labor_Ref as refs + + ref = {"층": "personal", "주인": "7/42", "키": "SC-000001"} + mine = {"층": "실무자료", "키": "HP-000001 제1호표", "전국자재": [REMICON]} + kept = refs.origin_mark({"본뜬": mine}, ref)["본뜬"] + assert kept["키"] == "HP-000001 제1호표" and kept["전국자재"] == [REMICON] # 덮지 않음 + assert kept["복사원본"] == ref # 원본 참조는 다른 칸 + assert refs.origin_mark({"본뜬": None}, ref)["본뜬"] == ref # 보통 문서 = 옛 그대로 + assert refs.origin_mark({"본뜬": {"층": "system", "키": "A"}}, ref)["본뜬"] == ref From 98396f9939ef6ca6e3040b56b31c04ee1a5a57e6 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sat, 17 Oct 2026 11:57:41 +0900 Subject: [PATCH 3/3] =?UTF-8?q?feat(B06=20CAD=20=EA=B8=80):=20=EC=A1=B0?= =?UTF-8?q?=ED=95=A9=20=EB=AF=B8=EC=A7=80=EC=A0=95=20=EA=B5=AC=EC=A1=B0?= =?UTF-8?q?=EB=AC=BC=20=3D=20=EC=A2=85=EB=A5=98=20=EC=9D=B4=EB=A6=84?= =?UTF-8?q?=EB=A7=8C=20=C2=B7=20=EA=B0=99=EC=9D=80=20=EC=B8=A1=EC=A0=90=20?= =?UTF-8?q?=EA=B0=99=EC=9D=80=20=EC=A2=85=EB=A5=98=20=ED=95=9C=20=EC=A4=84?= =?UTF-8?q?=20(372-3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TS ↔ 파이썬 짝 · 거울 시험에 중복 사례 추가. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01XWh5wNNycMkbwMpACz1rte --- B06_Section/B06_Section_UI_Cross_Detail_Cad.ts | 10 ++++++---- .../B07_DesignDetail_Engine_Cad_CulvertNote.py | 10 ++++++---- resources/tester/test_166_1_note_options.py | 2 +- resources/tester/test_265_note_cad.py | 18 +++++++++++++++--- 4 files changed, 28 insertions(+), 12 deletions(-) diff --git a/B06_Section/B06_Section_UI_Cross_Detail_Cad.ts b/B06_Section/B06_Section_UI_Cross_Detail_Cad.ts index a61eb785d..d7fd17bed 100644 --- a/B06_Section/B06_Section_UI_Cross_Detail_Cad.ts +++ b/B06_Section/B06_Section_UI_Cross_Detail_Cad.ts @@ -7,7 +7,7 @@ * (`_Engine_Cad_Table.cross_note_cad_entities`). * 처음 열 때 칸이 없으면 — 옛 글(`structure_note`)이 있으면 줄마다 Text 로 옮김 · 없으면 미리 넣음 * (265-2 · 관 주인 측점 = 원형 마크 · 그 측점 구조물마다 지정된 구조물 조합 이름 줄 · 미지정 = - * 「이름 조합 미지정」 · 이어서 좌측 패널 옵션 줄 = `structureOptionLines` · 166-1) · 그 밖 = 빈 도면. + * 종류 이름만(같은 측점 같은 종류 한 줄) · 이어서 좌측 패널 옵션 줄 = `structureOptionLines` · 166-1) · 그 밖 = 빈 도면. * ⚠ 짝(①) = `B07_DesignDetail/B07_DesignDetail_Engine_Cad_CulvertNote.py` — `noteEntities` · * `structureNoteLines` · `stationNoLabel` 와 같은 입력 → 같은 엔티티(id 만 다름) · 모달을 안 연 * 측점은 B07 이 그것으로 그림 · 거울 시험 `test_260_note_cad.py` · `test_265_note_cad.py`. @@ -158,10 +158,11 @@ export const thingsAt = (things: readonly NoteThing[], chainageM: number): NoteT /** * 구조물 조합 줄 — 구조물마다 지정된 조합(본체 → 그룹 차례) 이름 한 줄씩(이름 = 일위대가 이름 + - * 조합 경로 옵션 값) · 같은 구조물의 같은 이름은 한 줄 · 미지정 = 「{구조물 이름} 조합 미지정」 · + * 조합 경로 옵션 값) · 같은 구조물의 같은 이름은 한 줄 · 미지정 = 종류 이름만(같은 측점 같은 종류는 한 줄) · * 배관은 본체 줄 없음(원형 마크) — 보호공 · 흙막이 · 도수로 조합만 · 이어서 그 구조물 좌측 패널 옵션 줄(166-1). */ export function structureNoteLines(things: readonly NoteThing[], chainageM: number): string[] { + const kinds = new Set(); return thingsAt(things, chainageM).flatMap((thing) => { const own = readSummaryItem(thing.summary_item); const slots: Array<[string, SummaryItem]> = Object.entries(readGroupItems(thing.group_items)); @@ -171,9 +172,10 @@ export function structureNoteLines(things: readonly NoteThing[], chainageM: numb .map(([, item]) => summaryLabel(item)); const combos = slots.length ? [...new Set(names)] - : thing.pipe + : thing.pipe || kinds.has(thing.name) ? [] - : [`${thing.name} 조합 미지정`]; + : [thing.name]; + if (!slots.length) kinds.add(thing.name); return [...combos, ...(thing.optionLines ?? [])]; }); } diff --git a/B07_DesignDetail/B07_DesignDetail_Engine_Cad_CulvertNote.py b/B07_DesignDetail/B07_DesignDetail_Engine_Cad_CulvertNote.py index d8f3038f4..2406f9922 100644 --- a/B07_DesignDetail/B07_DesignDetail_Engine_Cad_CulvertNote.py +++ b/B07_DesignDetail/B07_DesignDetail_Engine_Cad_CulvertNote.py @@ -7,7 +7,7 @@ B06 [상세] 모달을 한 번도 안 연 측점(칸 = 없음)에 B07 이 같은 (빈 엔티티)은 덮지 않음 · 옛 글(`structure_note`)이 있으면 옛 글이 먼저 (모달이 처음 열 때 옮기는 차례와 같음). 그림 = 관 주인 측점 원형 마크(배수관 · Φ · L 정수) + 측점 · 그 측점 구조물마다 지정된 -구조물 조합 이름 줄(미지정 = 「이름 조합 미지정」) + 그 구조물 좌측 패널 옵션 줄(166-1). +구조물 조합 이름 줄(미지정 = 종류 이름만) + 그 구조물 좌측 패널 옵션 줄(166-1). 옵션 줄은 파이썬으로 다시 짜지 않음 — 화면과 같은 TS(`structureOptionLines`)를 Node 진입점(`B07_DesignDetail_Note_Options_Node.ts` · CLAUDE.md 5장 ②)으로 돌려 받음 · 실패는 비치명(조합 줄만). @@ -109,10 +109,11 @@ def things_at(things: list[Thing], chainage_m: float) -> list[Thing]: def structure_note_lines(things: list[Thing], chainage_m: float) -> list[str]: """구조물 조합 줄 — TS `structureNoteLines` 짝. - 본체 → 그룹 차례 · 같은 이름 한 줄 · 미지정 = 「이름 조합 미지정」 · 배관 본체 뺌 · + 본체 → 그룹 차례 · 같은 이름 한 줄 · 미지정 = 종류 이름만(같은 측점 같은 종류 한 줄) · 배관 본체 뺌 · 이어서 그 구조물 좌측 패널 옵션 줄(`option_lines` · 166-1). """ lines: list[str] = [] + kinds: set[str] = set() for thing in things_at(things, chainage_m): own = _summary_item(thing.get("summary_item")) slots = ([("", own)] if own else []) + _group_items(thing.get("group_items")) @@ -123,8 +124,9 @@ def structure_note_lines(things: list[Thing], chainage_m: float) -> list[str]: ] if slots: lines += list(dict.fromkeys(names)) - elif not thing["pipe"]: - lines.append(f"{thing['name']} 조합 미지정") + elif not thing["pipe"] and thing["name"] not in kinds: + kinds.add(thing["name"]) + lines.append(thing["name"]) lines += [str(line) for line in thing.get("option_lines") or []] return lines diff --git a/resources/tester/test_166_1_note_options.py b/resources/tester/test_166_1_note_options.py index 94536b3ed..ee1341e67 100644 --- a/resources/tester/test_166_1_note_options.py +++ b/resources/tester/test_166_1_note_options.py @@ -107,7 +107,7 @@ def test_조합_줄_뒤에_옵션_줄_미지정도_옵션_줄(): "group_items": None, "option_lines": ["옹벽: 높이 2.5m"], } - assert structure_note_lines([thing], 40.0) == ["옹벽 조합 미지정", "옹벽: 높이 2.5m"] + assert structure_note_lines([thing], 40.0) == ["옹벽", "옹벽: 높이 2.5m"] def test_화면은_구조물마다_옵션_줄을_붙임(): diff --git a/resources/tester/test_265_note_cad.py b/resources/tester/test_265_note_cad.py index 79a8fee33..4199da976 100644 --- a/resources/tester/test_265_note_cad.py +++ b/resources/tester/test_265_note_cad.py @@ -124,12 +124,12 @@ _LINES = [ "기슭막이 메쌓기 1.2m 기초무", "돌붙임 찰붙임", "옹벽 콘크리트 H=2.0", - "돌쌓기(찰) 조합 미지정", - "가드레일 조합 미지정", + "돌쌓기(찰)", + "가드레일", ] -def test_구조물_여럿_글은_조합_이름_미지정_배관_본체_뺌_TS_파이썬_같음(): +def test_구조물_여럿_글은_조합_이름_미지정은_종류_이름만_배관_본체_뺌_TS_파이썬_같음(): culvert = {**_culvert_set(_PIPE), "chainage_m": 40.2} cases = [ _case(_THINGS, 40.0, culvert, {"pipe_length_m": 8.5}), @@ -229,3 +229,15 @@ def test_원형_마크_L_은_사사오입_정수_TS_파이썬_같음(length, lab py = note_entities(40.0, culvert, design, "NO.2", []) assert _labels(ts["entities"])[2] == label assert _labels(py)[2] == label + + +def test_미지정_같은_종류는_한_줄_조합_있는_같은_이름은_따로_TS_파이썬_같음(): + things = [ + {"name": "옹벽", "pipe": False, "at": 40.0}, + {"name": "옹벽", "pipe": False, "at": 40.1}, + {"name": "돌쌓기", "pipe": False, "at": 40.0, "summary_item": _B}, + {"name": "돌쌓기", "pipe": False, "at": 40.0}, + ] + [ts] = _helper("mirror", [_case(things, 40.0, None, {})]) + lines = structure_note_lines(things, 40.0) + assert ts["lines"] == lines == ["옹벽", "돌붙임 찰붙임", "돌쌓기"]