From 365fef4fd461ff5dd3badb3189e61c4bbcd5cb8d Mon Sep 17 00:00:00 2001 From: umsangdon Date: Fri, 9 Oct 2026 23:49:25 +0900 Subject: [PATCH] =?UTF-8?q?feat(m02):=20=EC=9E=90=EC=9E=AC=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D=20=EC=84=B8=20=EC=B9=B8=20=EA=B8=B0=EB=B3=B8=EA=B0=92?= =?UTF-8?q?=20=E2=80=94=20=E3=80=8C=EC=9D=BC=EB=B0=98=E3=80=8D=20=EC=A7=80?= =?UTF-8?q?=EC=9B=80=20=C2=B7=20=EB=B9=88=20=EC=B9=B8=20=EC=82=AC=EA=B8=89?= =?UTF-8?q?=20=C2=B7=20=EB=B3=84=EB=8F=84=EA=B3=84=EC=83=81=20=C2=B7=20?= =?UTF-8?q?=ED=8F=AC=ED=95=A8=20=C2=B7=20=EB=B3=84=EB=8F=84=EA=B3=84?= =?UTF-8?q?=EC=83=81=EC=9D=B4=EB=A9=B4=20=EC=9A=B4=EB=B0=98=EB=B9=84=20?= =?UTF-8?q?=EA=BA=BC=EC=A7=90(162-1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01SE9MxeXTu2uaDZ55BJowAY --- M02_MasterTemplete/M02_Labor_Doc.py | 11 +++++- .../M02_MasterTemplete_Labor_Api.ts | 10 ++++- .../M02_MasterTemplete_Labor_MatMarks.ts | 38 ++++++++++--------- .../M02_MasterTemplete_Labor_Text.ts | 1 - resources/tester/test_m02_labor_mat_marks.py | 36 +++++++++++++++++- 5 files changed, 73 insertions(+), 23 deletions(-) diff --git a/M02_MasterTemplete/M02_Labor_Doc.py b/M02_MasterTemplete/M02_Labor_Doc.py index 2e7694970..0692a0245 100644 --- a/M02_MasterTemplete/M02_Labor_Doc.py +++ b/M02_MasterTemplete/M02_Labor_Doc.py @@ -21,7 +21,8 @@ 같은 품목 두 번은 [저장] 때 400. 승격(134-1) = 로직 줄 「단가승격」 표시(`true` 만 · 없음 = 단가산출) · 금액 무관 · B09 호표 몫. 자재 세 칸(148-4) = `MARKS` 허용 값만(그 밖 · 빈 것 버림 · 금액 무관) — 사용자 자재 = 줄 덧칸 · -로직 자재 = 로직 줄 `자재표시 {호표 차례: {세 칸}}`(차례가 재료 줄인 것만 · 틀리면 버림). +로직 자재 = 로직 줄 `자재표시 {호표 차례: {세 칸}}`(차례가 재료 줄인 것만 · 틀리면 버림) · +빈 칸 = 읽을 때 `MARK_DEFAULT`(`mark_of` · 저장은 비움 · 옛 「일반」 = 버림 → 사급 · 162-1). % 덧줄(149-4) = 식 「X * Y / 100」 덧줄에 덧칸 `율`(Y) · `규격`(「노무비의 %」 등 · `pct_extras`) — 표기만 · 단위 · 수량 · 금액 그대로(「식 · 1」) · 화면이 율 · % 로 그림 · M01 시험계산 표도 이 함수. """ @@ -46,10 +47,11 @@ COSTS = mf.COST_ITEMS # 노무비 · 재료비 · 경비 MATERIAL_HEAD = "자재|" _MAT_COLS = ("코드", "이름", "규격", "단위") MARKS = { - "자재구분": ("일반", "관급", "사급"), + "자재구분": ("관급", "사급"), "별도계상": ("별도계상", "직접계산"), "운반비": ("포함", "미포함"), } +MARK_DEFAULT = {"자재구분": "사급", "별도계상": "별도계상", "운반비": "포함"} # 읽을 때 기본(162-1) ALIAS_MAX = 40 # 별칭(44-6) 글자 수 상한 — 화면 칸과 같음 _BRACE = re.compile(r"\{([^{}]+)\}") # 품목 조건의 {입력} @@ -217,6 +219,11 @@ def marks(one: Any) -> dict[str, str]: return {k: got[k] for k, allowed in MARKS.items() if got.get(k) in allowed} +def mark_of(one: Any, key: str) -> str: + """세 칸 한 칸 읽기(162-1) — 허용 값 아니면(빈 · 옛 「일반」) 기본값.""" + return marks(one).get(key) or MARK_DEFAULT[key] + + def _mat_marks(value: Any, logic: dict) -> dict[str, dict[str, str]] | None: """로직 자재 세 칸(148-4) `{호표 차례: {세 칸}}` — 맨 위 층 재료 줄 차례 · 허용 값만 · 빈 것 = None.""" diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_Api.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_Api.ts index 341987933..0df44097f 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_Api.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_Api.ts @@ -138,14 +138,20 @@ export interface LaborRow { /** 로직 자재 세 칸(148-4) — {호표 차례: 세 칸} · 금액 무관 */ 자재표시?: Record; } -/** 자재 세 칸(148-4 · 서버 `M02_Labor_Doc.MARKS` 와 같음) — 처음 값 없음(「—」) · 금액 무관 */ +/** 자재 세 칸(148-4 · 서버 `M02_Labor_Doc.MARKS` 와 같음) — 빈 칸 = 읽을 때 기본값 · 금액 무관 */ export const MAT_MARKS = { - 자재구분: ["일반", "관급", "사급"], + 자재구분: ["관급", "사급"], 별도계상: ["별도계상", "직접계산"], 운반비: ["포함", "미포함"], } as const; export type MatMarkKey = keyof typeof MAT_MARKS; export type MatMarks = { [K in MatMarkKey]?: (typeof MAT_MARKS)[K][number] }; +/** 읽을 때 기본값(162-1 · 서버 `MARK_DEFAULT` 와 같음) — 저장은 비움 */ +export const MAT_MARK_DEFAULT: Required = { + 자재구분: "사급", + 별도계상: "별도계상", + 운반비: "포함", +}; /** 허용 값만 남김(서버 `marks` 와 같은 규칙) */ export function cleanMarks(v: unknown): MatMarks { const got = (v && typeof v === "object" ? v : {}) as Record; diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_MatMarks.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_MatMarks.ts index c9dcf8a0a..50005f7da 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_MatMarks.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_MatMarks.ts @@ -1,12 +1,14 @@ /* ============================================================================= * M02_MasterTemplete_Labor_MatMarks.ts - * 자재목록 세 칸(148-4) — 자재구분(일반 · 관급 · 사급) · 별도계상(별도계상 · 직접계산) · 운반비(포함 · 미포함) - * · 고칠 수 있으면 드롭다운(처음 값 빈 「—」 · 줄 누름과 따로) · 아니면 글 · 금액 무관 + * 자재목록 세 칸(148-4) — 자재구분(관급 · 사급) · 별도계상(별도계상 · 직접계산) · 운반비(포함 · 미포함) + * · 고칠 수 있으면 드롭다운(줄 누름과 따로) · 아니면 글 · 금액 무관 + * · 빈 칸 = 기본값 표시(162-1 · `MAT_MARK_DEFAULT` · 저장은 비움) · 별도계상이면 운반비 드롭다운 꺼짐(값 그대로) * · 사용자 자재 = 줄 덧칸 · 로직 자재 = 로직 줄 `자재표시 {호표 차례: 세 칸}`(`Labor_Api` 정리 · 서버 `clean`) * ========================================================================== */ import { createSelectField, el } from "@ui/ui_template_elements"; import { + MAT_MARK_DEFAULT, MAT_MARKS, type LaborRow, type MatMarkKey, @@ -21,7 +23,6 @@ const HEAD: Record = { }; /** 선택지 글(저장 값은 한국어 그대로) */ const WORD: Record = { - 일반: "Mark_General", 관급: "Mark_Gov", 사급: "Mark_Private", 별도계상: "Mark_Separate", @@ -33,33 +34,36 @@ const KEYS = Object.keys(MAT_MARKS) as MatMarkKey[]; export const markHeads = (): string[] => KEYS.map((k) => tc(HEAD[k])); -/** 세 칸 — `set` 없으면 글만(빈 = 「—」) */ +/** 세 칸 — `set` 없으면 글만(빈 = 기본값 글) */ export function markCells( value: MatMarks, set?: (key: MatMarkKey, v: string) => void, ): HTMLElement[] { - return KEYS.map((key) => { - const now = value[key] ?? ""; + const picks: Partial> = {}; + const cells = KEYS.map((key) => { + const now = value[key] ?? MAT_MARK_DEFAULT[key]; const attrs = { "data-label": tc(HEAD[key]), "data-mat-mark": key }; - if (!set) - return el("td", { - className: "m02-labrow__mark", - text: now ? tc(WORD[now]) : "—", - attrs, - }); + if (!set) return el("td", { className: "m02-labrow__mark", text: tc(WORD[now]), attrs }); const pick = createSelectField({ - options: [ - { value: "", text: "—" }, - ...MAT_MARKS[key].map((v) => ({ value: v, text: tc(WORD[v]) })), - ], + options: MAT_MARKS[key].map((v) => ({ value: v, text: tc(WORD[v]) })), value: now, compact: true, ariaLabel: tc(HEAD[key]), - onChange: (v) => set(key, v), + onChange: (v) => { + set(key, v); + if (key === "별도계상") haulOff(v); + }, }); + picks[key] = pick.select; pick.root.addEventListener("click", (e) => e.stopPropagation()); // 줄 누름과 따로 return el("td", { className: "m02-labrow__mark", attrs, children: [pick.root] }); }); + // 별도계상이면 운반비 꺼짐 — 값은 그대로 + const haulOff = (sep: string) => { + if (picks.운반비) picks.운반비.disabled = sep === "별도계상"; + }; + haulOff(value.별도계상 ?? MAT_MARK_DEFAULT.별도계상); + return cells; } /** 한 칸 바꿈 — 빈 값 = 칸 지움 */ diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_Text.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_Text.ts index 1c2655062..e5b3b3ce6 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_Text.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_Text.ts @@ -118,7 +118,6 @@ const TEXT = { Mark_Kind: ["자재구분", "Material type"], Mark_Separate: ["별도계상", "Separate"], Mark_Haul: ["운반비", "Haulage"], - Mark_General: ["일반", "General"], Mark_Gov: ["관급", "Gov-supplied"], Mark_Private: ["사급", "Contractor-supplied"], Mark_Direct: ["직접계산", "Direct"], diff --git a/resources/tester/test_m02_labor_mat_marks.py b/resources/tester/test_m02_labor_mat_marks.py index eb1752081..ffac374a3 100644 --- a/resources/tester/test_m02_labor_mat_marks.py +++ b/resources/tester/test_m02_labor_mat_marks.py @@ -4,6 +4,8 @@ 로직 자재 = 로직 줄 `자재표시 {호표 차례: 세 칸}`(차례가 재료 줄인 것만 · 틀려도 400 아님) - 기준합 · 반영합 그대로 · 저장 왕복에 남음 · 화면 `normalizeLabor` 도 같은 규칙(Node 헬퍼) - 화면 칸 부품 = 새 파일 · 사용자 줄 · 로직 줄 같은 칸 · 사전조합 줄엔 안 넘김(소스 검사) +- 162-1 = 「일반」 지움(관급 · 사급) · 빈 칸 = 읽을 때 기본(사급 · 별도계상 · 포함 · 저장은 비움) · + 옛 「일반」 = 사급으로 읽음 · 별도계상이면 운반비 드롭다운 꺼짐(값 그대로) """ import json @@ -116,7 +118,7 @@ def _src(name): def test_화면_칸_부품_사용자_줄_로직_줄_같은_칸(): marks = _src("M02_MasterTemplete_Labor_MatMarks.ts") - assert '{ value: "", text: "—" }' in marks + assert '"—"' not in marks # 「—」 항목 없음(162-1) assert "e.stopPropagation()" in marks # 줄 누름과 따로 rows = _src("M02_MasterTemplete_Labor_Rows.ts") assert "...markCells(mat, on?.mark" in rows and "...markHeads()," in rows @@ -128,3 +130,35 @@ def test_화면_칸_부품_사용자_줄_로직_줄_같은_칸(): api = _src("M02_MasterTemplete_Labor_Api.ts") for key, allowed in labor.MARKS.items(): assert f"{key}: [{', '.join(json.dumps(v, ensure_ascii=False) for v in allowed)}]" in api + + +def test_빈_칸_기본값_허용_값_그대로(): + assert labor.MARK_DEFAULT == {"자재구분": "사급", "별도계상": "별도계상", "운반비": "포함"} + assert {k: labor.mark_of({}, k) for k in labor.MARKS} == labor.MARK_DEFAULT + assert {k: labor.mark_of(MARKS, k) for k in labor.MARKS} == MARKS + assert labor.mark_of(None, "운반비") == "포함" + + +def test_옛_일반은_사급으로_읽음(): + assert "일반" not in labor.MARKS["자재구분"] + got = labor.clean( + _doc( + 담은로직=[{"로직": NET, "자재표시": {"3": {"자재구분": "일반", "운반비": "미포함"}}}], + 자재=[{"코드": WIRE, "자재구분": "일반"}], + ) + ) + assert got["담은로직"][0]["자재표시"] == {"3": {"운반비": "미포함"}} # 저장은 채우지 않음 + assert "자재구분" not in got["자재"][0] + assert labor.mark_of(got["자재"][0], "자재구분") == "사급" + assert labor.mark_of({"자재구분": "일반"}, "자재구분") == "사급" + + +def test_화면_기본값_한_벌_운반비_꺼짐(): + api = _src("M02_MasterTemplete_Labor_Api.ts") + for key, v in labor.MARK_DEFAULT.items(): + assert f'{key}: "{v}",' in api.split("MAT_MARK_DEFAULT")[1].split("};")[0] + marks = _src("M02_MasterTemplete_Labor_MatMarks.ts") + assert "value[key] ?? MAT_MARK_DEFAULT[key]" in marks + assert 'picks.운반비.disabled = sep === "별도계상"' in marks + assert "haulOff(value.별도계상 ?? MAT_MARK_DEFAULT.별도계상)" in marks + assert '일반: "Mark_General"' not in marks