diff --git a/M02_MasterTemplete/M02_MasterTemplete_Hopyo.css b/M02_MasterTemplete/M02_MasterTemplete_Hopyo.css index ddb2d50e6..22a432272 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Hopyo.css +++ b/M02_MasterTemplete/M02_MasterTemplete_Hopyo.css @@ -172,6 +172,13 @@ width: 6em; } +/* 단위 환산 칸 앞뒤 글 — 「엑셀 1 ㎡ = 우리 [ ] 본」 */ +.m02-hopyo__unit-text { + color: var(--color-text-secondary); + font-size: var(--text-sm); + white-space: nowrap; +} + .m02-hopyo__acts { white-space: nowrap; } diff --git a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_CheckRows.ts b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_CheckRows.ts index 35dee7c48..7845dda03 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_CheckRows.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_CheckRows.ts @@ -1,7 +1,7 @@ /* ============================================================================= * M02_MasterTemplete_Hopyo_CheckRows.ts * 확인 창 줄 표(PLAN 359-3) — 구조물 조합 일위대가 상세 꼴(`Labor_Rows` logicTable · matTable)에 품 비교 · 단추만 더함. - * · 로직 표 = [✓] · 차례 · 구분(엑셀 갈래 + 상태) · 이름(우리 로직 · 배 · 옵션 글) · 별칭(엑셀 품명 = 만들 때 별칭) · + * · 로직 표 = [✓] · 차례 · 구분(엑셀 갈래 + 상태) · 이름(우리 로직 · 단위 환산 · 옵션 글) · 별칭(엑셀 품명 = 만들 때 별칭) · * 수량산출(위 엑셀 / 아래 우리 = 엑셀 × 배) · 결과 단위 · 노무비 · 재료비 · 경비 · 합계 · 품 비교 · 단추 * · 자재 표 = [✓] · 차례 · 코드 · 이름 · 규격 · 수량산출 · 단위 · 세 칸(자재구분 · 별도계상 · 운반비 · 글만) · * 가격 다섯 · 엑셀 / 우리 금액 · 단추 @@ -28,6 +28,7 @@ import { moneyCell, PRICE_SLOTS } from "./M02_MasterTemplete_Labor_Rows"; import { tc } from "./M02_MasterTemplete_Labor_Text"; import { formatNumber, formatPrice } from "../common_util/common_util_money"; import { tableOf } from "./logic/M02_MasterTemplete_Hopyo_Draft"; +import { unitView, type UnitView } from "./logic/M02_MasterTemplete_Hopyo_Unit"; /** 드롭다운 — 저장 값(서버 뺌 까닭) → 화면 글 */ export const OUT_TEXT: [string, string][] = [ @@ -109,27 +110,50 @@ const stateChip = (line: HopyoLine): HTMLElement => { }); }; -/** 대상마다 한 줄 — 키 · 이름 · × 배 칸 · 몫 */ +/** 단위 환산 칸 — 「엑셀 1 ㎡ = 우리 [ ] 본」(장부 화면도 같은 꼴) */ +export function unitField( + input: HTMLInputElement, + v: Pick, +): HTMLElement[] { + input.title = L("M02_HopyoUnitTip"); + return [ + el("span", { + className: "m02-hopyo__unit-text", + text: L("M02_HopyoUnitLead").replace("{from}", v.from || "?"), + }), + input, + ...(v.to ? [el("span", { className: "m02-hopyo__unit-text", text: v.to })] : []), + ]; +} + +/** 대상마다 한 줄 — 키 · 이름 · 단위 환산(같음 = 글만 · 몫 줄 = 몫만 · 빈칸 · 추정 = 입력 칸) · 몫 */ function partLines(line: HopyoLine, parts: HopyoPart[], ctx: RowsContext): HTMLElement[] { return parts.map((p, at) => { + const v = unitView(p, line.단위, p.단위); const times = el("input", { className: "ui-input ui-input--sm m02-hopyo__times", attrs: { type: "number", step: "any", min: "0", value: p.배 == null ? "" : String(p.배) }, }) as HTMLInputElement; - times.title = "배 — 우리 수량 = 엑셀 수량 × 배"; times.disabled = !ctx.canWrite; times.addEventListener("change", () => ctx.times(line, at, times.value === "" ? null : Number(times.value)), ); const key = p.로직 ?? p.자재 ?? (p.호표 !== undefined ? `제${p.호표}호표` : p.전국 ? "전국" : ""); + const note = v.estimated + ? v.thick !== null + ? L("M02_HopyoUnitThick").replace("{n}", String(v.thick)) + : L("M02_HopyoUnitEst") + : ""; return el("div", { className: "m02-hopyo__part", attrs: { title: p.어디 ?? "" }, children: [ el("span", { className: "m02-labrow__key", text: key }), el("strong", { text: p.이름 ?? "" }), - times, + ...(v.mode === "same" ? [small(L("M02_HopyoUnitSame"))] : []), + ...(v.mode === "input" ? unitField(times, v) : []), + ...(note ? [small(note)] : []), ...(p.몫 != null ? [small(`몫 ${p.몫}`)] : []), ], }); diff --git a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Ledger.ts b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Ledger.ts index 33cde0fb8..f345d4754 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Ledger.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Ledger.ts @@ -1,9 +1,9 @@ /* ============================================================================= * M02_MasterTemplete_Hopyo_Ledger.ts * 일위대가 실무자료 별칭 장부 화면(PLAN 338-3 · 설계 4-4) — 본 칸 · 좌측 판 [별칭 장부] 로 엶. - * · 표 = 엑셀 품명 · 규격 · 단위 | 갈래 | 우리 로직(여럿) · 옵션 · 배 · 몫 | 누가 · 때 · 처음 파일 | 층(위 층 회색 읽기만) + * · 표 = 엑셀 품명 · 규격 · 단위 | 갈래 | 우리 로직(여럿) · 옵션 · 단위 환산 · 몫 | 누가 · 때 · 처음 파일 | 층(위 층 회색 읽기만) * · 뜻(359-4 · 머리 한 줄 · 좌측 단추 title) = 엑셀 품명 ↔ 우리 로직 · 자재 짝을 기억해 두었다가 같은 품명이 나오면 먼저 제안 - * · 할 일 = 찾기 · 갈래 거름 · 갈래 바꿈 · [로직 바꿈] · [항목 추가] · 배 · 몫 · [옵션] · [삭제] · + * · 할 일 = 찾기 · 갈래 거름 · 갈래 바꿈 · [로직 바꿈] · [항목 추가] · 단위 환산 · 몫 · [옵션] · [삭제] · * 위 층 줄 [내 층 복사] · 갈래 글 = 확인 창 드롭다운과 같음(짝 · 로직없음 · 사용안함 · 저장 값 그대로) * · 고친 것 = sessionStorage `m02-hopyo-ledger:{층}:{주인}` → [저장] 때만 `PUT`(서버가 짝을 다시 검사) * · 안 둠 = 발주처 칸 · 자동 올림 · 자동 합침 · 쓰인 횟수 · 회사로 올리기. @@ -27,6 +27,7 @@ import { fetchLogicInputs } from "./M02_MasterTemplete_Labor_Api"; import { dateTimeText } from "../common_util/common_util_time"; import { openLaborInput } from "./M02_MasterTemplete_Labor_Input"; import { openFindPicker } from "./M02_MasterTemplete_Find_Pick"; +import { unitField } from "./M02_MasterTemplete_Hopyo_CheckRows"; import { formatNumber } from "../common_util/common_util_money"; import "./M02_MasterTemplete_Hopyo.css"; @@ -178,12 +179,12 @@ export function buildHopyoLedger(opt: LedgerOptions): HTMLElement { if (mine) { const times = el("input", { className: "ui-input ui-input--sm m02-hopyo__times", - attrs: { type: "number", step: "any", min: "0", value: String(p.배), title: "배" }, + attrs: { type: "number", step: "any", min: "0", value: String(p.배) }, }) as HTMLInputElement; times.addEventListener("change", () => edit(r, { 짝: r.짝.map((x, i) => (i === at ? { ...x, 배: Number(times.value) } : x)) }), ); - parts.push(times); + parts.push(...unitField(times, { from: r.원래.단위, to: "" })); if (p.로직) parts.push( createButton({ @@ -369,7 +370,7 @@ export function buildHopyoLedger(opt: LedgerOptions): HTMLElement { "규격", "단위", "갈래", - "우리 로직 · 배 · 몫 · 옵션", + "우리 로직 · 단위 환산 · 몫 · 옵션", "누가 · 때 · 처음 파일", "층", "승인", diff --git a/M02_MasterTemplete/logic/M02_MasterTemplete_Hopyo_Unit.ts b/M02_MasterTemplete/logic/M02_MasterTemplete_Hopyo_Unit.ts new file mode 100644 index 000000000..751939cb3 --- /dev/null +++ b/M02_MasterTemplete/logic/M02_MasterTemplete_Hopyo_Unit.ts @@ -0,0 +1,61 @@ +/* ============================================================================= + * M02_MasterTemplete_Hopyo_Unit.ts — 확인 창 대상 이름 옆 「단위 환산」 칸의 보이기 판정(PLAN 784-2 · 요구 635-2). + * 순수 계산(화면 · fetch 없음) — 숨김 조건 · 두 단위 글 재료 · 추정 두께. + * · same = 배 1 · 단위 같음(서버 갈래 같음 · 두 단위 · 단위표 1) → 칸 숨김 · 「단위 같음 ×1」 + * · quota = 배가 몫(0.1)과 같음 → 칸 숨김 · 「몫 0.1」 만 + * · input = 빈칸 · 추정 · 그 밖 → 입력 칸 「엑셀 1 ㎡ = 우리 [ ] 본」 + * 값 자체(배)는 안 바꿈 — 서버 `Match.factor` 가 낸 배 · 갈래를 보이기만 다르게. + * ========================================================================== */ + +export interface UnitPart { + 배: number | null; + 몫?: number | null; + 환산?: { 배: number | null; 갈래: string } | null; + 단위?: string | null; +} + +export type UnitMode = "same" | "quota" | "input"; + +export interface UnitView { + mode: UnitMode; + /** 엑셀 단위 · 우리 단위(빈 글 = 모름) */ + from: string; + to: string; + /** 추정 갈래 — 두께 m(㎡ → ㎥ 일 때만 · 아니면 null) */ + thick: number | null; + estimated: boolean; +} + +const plain = (u: string | null | undefined): string => + String(u ?? "") + .replace(/\s+/g, "") + .replace("㎡", "m2") + .replace("㎥", "m3") + .toLowerCase(); +const close = (a: number, b: number): boolean => Math.abs(a - b) < 1e-9; + +export function unitView( + p: UnitPart, + from: string | null | undefined, + to?: string | null, +): UnitView { + const f = String(from ?? "").trim(); + const t = String(to ?? p.단위 ?? "").trim(); + const quota = p.몫 != null; + const kind = p.환산?.갈래; + const unitSame = kind + ? kind === "같음" || kind === "두 단위" || (kind === "단위표" && p.환산?.배 === 1) + : plain(f) !== "" && plain(f) === plain(t); + const at = quota ? (p.몫 as number) : 1; + const hidden = p.배 !== null && close(p.배, at) && unitSame; + const estimated = kind === "추정"; + const thick = + estimated && plain(f) === "m2" && plain(t) === "m3" && p.환산?.배 != null ? p.환산.배 : null; + return { + mode: hidden ? (quota ? "quota" : "same") : "input", + from: f, + to: t, + thick, + estimated, + }; +} diff --git a/resources/tester/helper_m02_hopyo_unit.cjs b/resources/tester/helper_m02_hopyo_unit.cjs new file mode 100644 index 000000000..f7b9c8c5f --- /dev/null +++ b/resources/tester/helper_m02_hopyo_unit.cjs @@ -0,0 +1,13 @@ +/* 784-2 — `logic/M02_MasterTemplete_Hopyo_Unit.ts` 단위 환산 보이기 판정을 트랜스파일해 Node 로 돌림. + * stdin = {cases: [{part, from, to}]} · stdout = unitView 결과 배열 JSON. */ +const fs = require("fs"); +const path = require("path"); +const { makeLoader } = require("./helper_ts_stub_loader.cjs"); + +const ROOT = path.join(__dirname, "..", ".."); +const { loadTs } = makeLoader({ root: ROOT }); +const unit = loadTs( + path.join(ROOT, "M02_MasterTemplete", "logic", "M02_MasterTemplete_Hopyo_Unit.ts"), +); +const input = JSON.parse(fs.readFileSync(0, "utf8")); +process.stdout.write(JSON.stringify(input.cases.map((c) => unit.unitView(c.part, c.from, c.to)))); diff --git a/resources/tester/test_m02_hopyo_screen.py b/resources/tester/test_m02_hopyo_screen.py index 40e7e00f0..cd629bc87 100644 --- a/resources/tester/test_m02_hopyo_screen.py +++ b/resources/tester/test_m02_hopyo_screen.py @@ -588,3 +588,37 @@ def test_779_세_보기_칠_뛰기_창_다시_씀(): len(check.splitlines()) <= 663 and len(_text("M02_MasterTemplete_Hopyo_Api.ts").splitlines()) <= 676 ) + + +def test_단위_환산_칸_보이기_784_2(): + """784-2 — 배 1 · 단위 같음 = 칸 숨김 · 몫 줄 = 몫만 · 빈칸 · 추정 · 단위 다름 = 입력 칸 + 두 단위 글 재료 · + 화면(CheckRows · Ledger)은 「배」 대신 「단위 환산」 · 「엑셀 1 ㎡ = 우리 [ ] 본」 꼴.""" + cases = [ + {"part": {"배": 1, "환산": {"배": 1, "갈래": "같음"}}, "from": "㎡", "to": "㎡"}, + {"part": {"배": 1, "환산": {"배": 1, "갈래": "두 단위"}}, "from": "공/㎥", "to": "공㎥"}, + {"part": {"배": 0.1, "몫": 0.1, "환산": {"배": 1, "갈래": "같음"}}, "from": "㎥", "to": "㎥"}, + {"part": {"배": None, "환산": {"배": None, "갈래": "입력"}}, "from": "㎡", "to": "본"}, + {"part": {"배": 1, "환산": {"배": None, "갈래": "입력"}}, "from": "m", "to": "본"}, + {"part": {"배": 0.2, "환산": {"배": 0.2, "갈래": "추정"}}, "from": "㎡", "to": "㎥"}, + {"part": {"배": 100, "환산": {"배": 100, "갈래": "단위표"}}, "from": "m", "to": "cm"}, + {"part": {"배": 0.01, "몫": 0.1, "환산": {"배": 0.1, "갈래": "단위표"}}, "from": "a", "to": "b"}, + {"part": {"배": 1}, "from": "m2", "to": "㎡"}, + {"part": {"배": 1}, "from": "m", "to": "본"}, + ] # fmt: skip + got = _node("helper_m02_hopyo_unit.cjs", {"cases": cases}) + assert [g["mode"] for g in got] == [ + "same", "same", "quota", "input", "input", "input", "input", "input", "same", "input", + ] # fmt: skip + assert (got[3]["from"], got[3]["to"]) == ("㎡", "본") # 「엑셀 1 ㎡ = 우리 [ ] 본」 + assert got[5]["estimated"] and got[5]["thick"] == 0.2 # 「두께 0.2m 로 추정」 + assert not got[3]["estimated"] and got[3]["thick"] is None + rows = _text("M02_MasterTemplete_Hopyo_CheckRows.ts") + assert "unitView(p, line.단위, p.단위)" in rows and "M02_HopyoUnitSame" in rows + assert "배 — 우리 수량" not in rows + ledger = _text("M02_MasterTemplete_Hopyo_Ledger.ts") + assert "unitField(times, { from: r.원래.단위" in ledger and 'title: "배"' not in ledger + pure = _text("logic/M02_MasterTemplete_Hopyo_Unit.ts").split("*/", 1)[1] + for word in ("document", "fetch", "sessionStorage", "import "): + assert word not in pure, word + for name in ("CheckRows", "Ledger"): + assert len(_text(f"M02_MasterTemplete_Hopyo_{name}.ts").splitlines()) <= 700 diff --git a/ui_template/ui_template_locale_m2.ts b/ui_template/ui_template_locale_m2.ts index f165dfac7..4638c0214 100644 --- a/ui_template/ui_template_locale_m2.ts +++ b/ui_template/ui_template_locale_m2.ts @@ -163,6 +163,14 @@ export const ui_locales_m2 = { "Tree updated — name already taken, saved as “{value}”", ], M02_HopyoImport: ["일위대가 불러오기", "Import unit-price sheets"], + M02_HopyoUnitTip: [ + "단위 환산 — 우리 수량 = 엑셀 수량 × 이 값", + "Unit conversion — our qty = Excel qty × this value", + ], + M02_HopyoUnitSame: ["단위 같음 ×1", "Same unit ×1"], + M02_HopyoUnitLead: ["엑셀 1 {from} = 우리", "Excel 1 {from} = ours"], + M02_HopyoUnitThick: ["두께 {n}m 로 추정", "Estimated from thickness {n}m"], + M02_HopyoUnitEst: ["규격으로 추정", "Estimated from the spec"], B01_Dashboard_CardPrecombo: ["일위대가", "Unit-cost items"], M02_PrecomboTitle: ["일위대가", "Unit-cost items"], B01_Dashboard_CardSangeun: ["단가산출", "Unit-price analyses"],