From d4458ec8949d148bb3feb6c586dde043b55568df Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sun, 4 Oct 2026 22:05:58 +0900 Subject: [PATCH] =?UTF-8?q?fix(M02):=20=EC=9D=BC=EC=9C=84=EB=8C=80?= =?UTF-8?q?=EA=B0=80=20=EB=AA=A9=EB=A1=9D=20=ED=95=A9=EA=B3=84=20=C2=B7=20?= =?UTF-8?q?=EB=8B=A8=EC=9C=84=20=C2=B7=20=EC=83=88=20=ED=95=AD=EB=AA=A9=20?= =?UTF-8?q?=EB=91=90=20=EB=8B=A8=EC=B6=94=20=E2=80=94=20=EC=88=98=EB=9F=89?= =?UTF-8?q?=20=EC=97=86=EC=9C=BC=EB=A9=B4=20=EA=B8=B0=EC=A4=80=20=ED=95=A9?= =?UTF-8?q?(=EC=9E=91=EC=9D=80=20=EA=B8=80=20=E3=80=8C=EA=B8=B0=EC=A4=80?= =?UTF-8?q?=E3=80=8D)=20=C2=B7=20=EB=8B=B4=EC=9D=80=20=EB=A1=9C=EC=A7=81?= =?UTF-8?q?=200=20=E3=80=8C=E2=80=94=E3=80=8D=20=C2=B7=20=EB=8B=A8?= =?UTF-8?q?=EC=9C=84=20=3D=20=EC=9D=BC=EC=9C=84=EB=8C=80=EA=B0=80=20?= =?UTF-8?q?=EB=AC=B8=EC=84=9C=20=EB=8B=A8=EC=9C=84=20=C2=B7=20=EB=88=84?= =?UTF-8?q?=EB=A5=B8=20=EB=8B=A8=EC=B6=94=EA=B0=80=20=EC=A7=84=ED=95=98?= =?UTF-8?q?=EA=B2=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 39-8 B2 · B3 · B4 — 서버 목록 칸 labor 에 기준 · 단위 · laborSum · Col note · buildTwoWay choose. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016PSXsvyDxjNmufteN5bqmt --- .../M02_MasterTemplete_ItemList.ts | 35 ++++++++++++++++--- .../M02_MasterTemplete_ItemPick.ts | 15 ++++++-- .../M02_MasterTemplete_Items.py | 22 ++++++++---- resources/tester/helper_m02_item_list.cjs | 9 ++++- resources/tester/test_m02_item_list.py | 33 ++++++++++++++--- 5 files changed, 96 insertions(+), 18 deletions(-) diff --git a/M02_MasterTemplete/M02_MasterTemplete_ItemList.ts b/M02_MasterTemplete/M02_MasterTemplete_ItemList.ts index c52f0345c..1df2b007c 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_ItemList.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_ItemList.ts @@ -22,12 +22,21 @@ export interface ItemRow extends TreeItem { 도번?: string | null; 면적번?: string | null; 수정일?: Partial>; - labor?: { 번호: string | null; 로직수: number; 합계: number | null } | null; + labor?: { + 번호: string | null; + 로직수: number; + /** 수량 반영 합 · 기준(단가) 합 · 일위대가 문서 단위 */ + 합계: number | null; + 기준?: number | null; + 단위?: string | null; + } | null; } export interface Col { head: string; get: (row: R) => string; + /** 값 뒤 작은 글(예 「기준」) — 없으면 없음 */ + note?: (row: R) => string; } const TEXT = { @@ -51,6 +60,17 @@ const latest = (row: ItemRow): string => .pop(), ); +/** 일위대가 합계(39-8 B2) — 수량 반영 합이 있으면 그것 · 없으면 기준(단가) 합 + 작은 글 「기준」 · + * 담은 로직 0 · 계산값 없음 = 「—」 */ +export function laborSum(row: ItemRow): { text: string; note: string } { + const l = row.labor; + if (!l?.로직수) return { text: DASH, note: "" }; + if (l.합계) return { text: money(l.합계), note: "" }; + return typeof l.기준 === "number" + ? { text: money(l.기준), note: "기준" } + : { text: DASH, note: "" }; +} + const NAME: Col = { head: "이름", get: (r) => leafName(r) }; const FOLDER: Col = { head: "폴더", get: (r) => folderOf(r).join(" / ") }; const UNIT: Col = { head: "단위", get: (r) => r.단위 ?? "" }; @@ -73,9 +93,9 @@ export const ITEM_COLS: Record[]> = { { head: "일위대가번", get: (r) => r.labor?.번호 || DASH }, NAME, FOLDER, - UNIT, + { head: "단위", get: (r) => r.labor?.단위 ?? "" }, // 일위대가 문서 단위(39-8 B3) { head: "담은 로직 수", get: (r) => (r.labor ? String(r.labor.로직수) : DASH) }, - { head: "합계", get: (r) => money(r.labor?.합계) }, + { head: "합계", get: (r) => laborSum(r).text, note: (r) => laborSum(r).note }, changed("labor"), ], item: [ @@ -184,7 +204,14 @@ export function buildItemList(): ItemListHandle { className: "m01-master__table-row", children: cols.map((c) => { const text = c.get(row); - return el("td", { text, attrs: { title: text } }); + const note = c.note?.(row) ?? ""; + return el("td", { + text, + attrs: { title: note ? `${text} ${note}` : text }, + children: note + ? [el("small", { className: "m01-master__muted", text: ` ${note}` })] + : [], + }); }), }); // 고를 수 없는 줄 — 흐리게 · 누르면 까닭 한 줄(고르기 안 함) diff --git a/M02_MasterTemplete/M02_MasterTemplete_ItemPick.ts b/M02_MasterTemplete/M02_MasterTemplete_ItemPick.ts index 7d010b5f8..34deef6f7 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_ItemPick.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_ItemPick.ts @@ -185,10 +185,21 @@ export function buildTwoWay(opt: TwoWayOptions): HTMLElement { const copy = createButton({ label: TEXT.makeCopy, variant: "ghost" }); const below = el("div", { className: "m02-itempick__below" }); let busy = false; - fresh.addEventListener("click", opt.onNew); + /** 누른 단추 = 지금 화면 — 진한 단추가 고른 쪽 · 복사 생성 목록은 그쪽일 때만 보임 */ + const choose = (which: HTMLElement): void => { + for (const b of [fresh, copy]) { + b.classList.toggle("ui-btn--filled", b === which); + b.classList.toggle("ui-btn--ghost", b !== which); + } + below.hidden = which !== copy; + }; + fresh.addEventListener("click", () => { + choose(fresh); + opt.onNew(); + }); copy.addEventListener("click", () => { + choose(copy); if (below.firstChild) return; - copy.classList.add("is-active"); const picker = buildPicker({ ...opt.picker, onPick: (picked) => diff --git a/M02_MasterTemplete/M02_MasterTemplete_Items.py b/M02_MasterTemplete/M02_MasterTemplete_Items.py index b3899ec42..4edfb4f89 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Items.py +++ b/M02_MasterTemplete/M02_MasterTemplete_Items.py @@ -86,9 +86,19 @@ def items(folder: Path) -> list[dict[str, Any]]: return out +def _number(sums: Any) -> float | None: + """`계산값` 합 칸의 `계` — 수 아닌 값(옛 꼴) · 없음 = null(「—」).""" + total = sums.get("계") if isinstance(sums, dict) else None + try: + return None if total is None or isinstance(total, bool) else float(total) + except (TypeError, ValueError): + return None + + def _list_extras(folder: Path, col: str) -> dict[str, Any]: """목록 칸(39-5) — `수정일` = 도면 · 면적산출 · 일위대가 파일 시각 · `labor` = 일위대가 문서의 - 번호 · 담은 로직 수 · 합계(`계산값.반영합.계` · 없으면 null) — 파일이 없으면 칸 없음.""" + 번호 · 담은 로직 수 · 합계(`계산값.반영합.계`) · 기준(`계산값.기준합.계`) · 단위 — + 파일이 없으면 칸 없음.""" stamps: dict[str, str] = {} for kind in ("structure", "area", "labor"): path = folder / kind / f"{col}.json" @@ -103,15 +113,13 @@ def _list_extras(folder: Path, col: str) -> dict[str, Any]: doc = json.loads(path.read_bytes()) except ValueError: doc = {} - total = ((doc.get("계산값") or {}).get("반영합") or {}).get("계") - try: - total = None if total is None or isinstance(total, bool) else float(total) - except (TypeError, ValueError): - total = None # 수 아닌 값(옛 꼴)은 「—」 + calc = doc.get("계산값") or {} labor = { "번호": doc.get("일위대가번"), "로직수": len(doc.get("담은로직") or []), - "합계": total, + "합계": _number(calc.get("반영합")), + "기준": _number(calc.get("기준합")), # 수량 없는 일위대가 = 단가 합(39-8 B2) + "단위": doc.get("단위"), # 일위대가 문서 단위(구조물 열 단위 아님 · B3) } return {"수정일": stamps, "labor": labor} diff --git a/resources/tester/helper_m02_item_list.cjs b/resources/tester/helper_m02_item_list.cjs index dcf2777a7..b1f6baee2 100644 --- a/resources/tester/helper_m02_item_list.cjs +++ b/resources/tester/helper_m02_item_list.cjs @@ -33,7 +33,7 @@ const row = (열, 경로, extra = {}) => ({ }); const rows = [ row("c1", ["기슭막이", "메쌓기", "1.2m"], { - labor: { 번호: "SU-000001", 로직수: 3, 합계: 1234567.4 }, + labor: { 번호: "SU-000001", 로직수: 3, 합계: 1234567.4, 기준: 99, 단위: "개소" }, 수정일: { structure: "2026-10-01T09:00:00", labor: "2026-10-04T11:00:00" }, }), row("c2", ["기슭막이", "찰쌓기"], { labor: { 번호: "SU-000002", 로직수: 0, 합계: null } }), @@ -62,6 +62,13 @@ process.stdout.write( pageOver: m.pageOf(many, m.ITEM_COLS.structure, "", 9).page, pageEmpty: m.pageOf([], m.ITEM_COLS.structure, "", 2).page, // 그 부분 문서 없는 줄(39-8 H3) — c1 = 일위대가 있음 · 면적 수정일 없음 · c3 = 수정일 · 일위대가 없음 + sums: [ + rows[0], // 반영 합 있음 + { labor: { 로직수: 14, 합계: 0, 기준: 167746.6 } }, // 수량 없음 = 기준 + rows[1], // 담은 로직 0 + { labor: { 로직수: 2, 합계: null, 기준: null } }, // 계산값 없음 + rows[2], // 일위대가 없음 + ].map((r) => m.laborSum(r)), missing: ["structure", "area", "labor", "item"].map((k) => rows.map((r) => m.missingPart(k, r)), ), diff --git a/resources/tester/test_m02_item_list.py b/resources/tester/test_m02_item_list.py index 935b8576c..604f3d7ca 100644 --- a/resources/tester/test_m02_item_list.py +++ b/resources/tester/test_m02_item_list.py @@ -54,11 +54,23 @@ def test_칸_값_빈_번호_합계_날짜(ts): s, labor = ts["cells"]["structure"], ts["cells"]["labor"] assert s[0] == ["SD-c1", "1.2m", "기슭막이 / 메쌓기", "m", "2026-10-01"] assert s[2][0] == "—" and s[2][2] == "" # 빈 도번 · 맨 위 항목(폴더 없음) - assert labor[0][3:] == ["m", "3", "1,234,567", "2026-10-04"] # 합계 원 단위 반올림 + assert labor[0][3:] == ["개소", "3", "1,234,567", "2026-10-04"] # 일위대가 단위 · 원 단위 assert labor[1][5] == "—" and labor[2][0] == labor[2][4] == "—" # 합계 없음 · 일위대가 없음 assert ts["cells"]["item"][0][-1] == "2026-10-04" # 항목 통째 = 가장 늦게 고친 날 +def test_일위대가_합계_반영_기준_빈(ts): + """39-8 B2 — 반영 합이 있으면 그것 · 없으면 기준 합 + 「기준」 · + 담은 로직 0 · 계산값 없음 = 「—」.""" + assert ts["sums"] == [ + {"text": "1,234,567", "note": ""}, + {"text": "167,747", "note": "기준"}, + {"text": "—", "note": ""}, + {"text": "—", "note": ""}, + {"text": "—", "note": ""}, + ] + + def test_찾기_쪽(ts): assert ts["find"] == ["c2"] # 낱말 둘 AND(폴더 · 이름 칸) assert ts["findLabor"] == ["c1"] # 번호 칸도 찾음 @@ -91,14 +103,27 @@ def test_서버_목록_칸_수정일_labor(world): doc = { "일위대가번": "SU-000001", "담은로직": [{"로직": "GF000219"}, {"로직": "GF000137"}], - "계산값": {"반영합": {"계": 12345.6}}, + "단위": "개소", + "계산값": {"반영합": {"계": 12345.6}, "기준합": {"계": 2469.12}}, } (root / "labor" / f"{ditch['열']}.json").write_text(json.dumps(doc), encoding="utf-8") (root / "labor" / f"{wall['열']}.json").write_text("{깨짐", encoding="utf-8") rows = {r["열"]: r for r in t.client.get(t.url).json()["items"]} - assert rows[ditch["열"]]["labor"] == {"번호": "SU-000001", "로직수": 2, "합계": 12345.6} + assert rows[ditch["열"]]["labor"] == { + "번호": "SU-000001", + "로직수": 2, + "합계": 12345.6, + "기준": 2469.12, + "단위": "개소", + } assert "labor" in rows[ditch["열"]]["수정일"] - assert rows[wall["열"]]["labor"] == {"번호": None, "로직수": 0, "합계": None} + assert rows[wall["열"]]["labor"] == { + "번호": None, + "로직수": 0, + "합계": None, + "기준": None, + "단위": None, + } doc["계산값"] = {"반영합": {"계": "글"}} (root / "labor" / f"{ditch['열']}.json").write_text(json.dumps(doc), encoding="utf-8") again = {r["열"]: r for r in t.client.get(t.url).json()["items"]}