Merge remote-tracking branch 'origin/sub_laptop_2' into main_laptop_1

This commit is contained in:
2026-11-04 20:17:47 +09:00
7 changed files with 164 additions and 10 deletions
@@ -172,6 +172,19 @@
width: 6em;
}
.m02-hopyo__unit {
display: inline-flex;
align-items: center;
gap: var(--spacing-4);
}
/* 단위 환산 칸 앞뒤 글 — 「엑셀 1 ㎡ = 우리 [ ] 본」 */
.m02-hopyo__unit-text {
color: var(--color-text-secondary);
font-size: var(--text-sm);
white-space: nowrap;
}
.m02-hopyo__acts {
white-space: nowrap;
}
@@ -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,28 +110,50 @@ const stateChip = (line: HopyoLine): HTMLElement => {
});
};
/** 대상마다 한 줄 — 키 · 이름 · × 배 칸 · 몫 */
/** 단위 환산 칸 — 「엑셀 1 ㎡ = 우리 [ ] 본」(장부 화면도 같은 꼴) */
export function unitField(input: HTMLInputElement, v: Pick<UnitView, "from" | "to">): HTMLElement {
input.title = L("M02_HopyoUnitTip");
const word = (text: string): HTMLElement =>
el("span", { className: "m02-hopyo__unit-text", text });
return el("span", {
className: "m02-hopyo__unit",
children: [
word(L("M02_HopyoUnitLead").replace("{from}", v.from || "?")),
input,
...(v.to ? [word(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,
...(p.몫 != null ? [small(`몫 ${p.몫}`)] : []),
...(v.mode === "same" ? [small(L("M02_HopyoUnitSame"))] : []),
...(v.mode === "input" ? [unitField(times, v)] : []),
...(note ? [small(note)] : []),
...(p.몫 != null && p.몫 !== 1 ? [small(`몫 ${p.몫}`)] : []),
],
});
});
@@ -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 {
"규격",
"단위",
"갈래",
"우리 로직 · 배 · 몫 · 옵션",
"우리 로직 · 단위 환산 · 몫 · 옵션",
"누가 · 때 · 처음 파일",
"층",
"승인",
@@ -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 && p.몫 !== 1; // 몫 1 = 몫 없음과 같음
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,
};
}
@@ -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))));
+35
View File
@@ -588,3 +588,38 @@ 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": "본"},
{"part": {"배": 1, "몫": 1, "환산": {"배": 1, "갈래": "같음"}}, "from": "㎡", "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", "same",
] # 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
+8
View File
@@ -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"],