From fe788494638e5697cffbed00dc0d258973b9f027 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 2 Nov 2026 11:57:47 +0900 Subject: [PATCH] =?UTF-8?q?feat(729-3):=20=EB=8B=A8=EA=B0=80=EC=82=B0?= =?UTF-8?q?=EC=B6=9C=20=EB=AA=BB=EC=98=AE=EA=B9=80=20=EC=9E=87=EA=B8=B0=20?= =?UTF-8?q?=EC=B0=BD=20=C2=B7=20=EB=8B=A8=EC=9C=84=20=EC=84=B1=EA=B2=A9=20?= =?UTF-8?q?=EB=8B=A4=EB=A5=B8=20=ED=9B=84=EB=B3=B4=20=EA=B3=84=EC=88=98=20?= =?UTF-8?q?=EC=B9=B8=20=C2=B7=20=EB=82=A8=EC=9D=80=20=EC=A4=84=20=ED=91=9C?= =?UTF-8?q?=20=EB=AA=BB=EC=98=AE=EA=B9=80=20=EB=8B=A8=EC=B6=94(729-4=20?= =?UTF-8?q?=EC=9D=B4=EC=9D=8C)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 새 Hopyo_Lost: 못옮김 줄마다 찾기(노임 LB · 자재 MT) · 기계(EQ) · 우리에 없음(뺌) → 캐시 m02-hopyo-lost 모두를 [잇기 만들기] 한 번에(서버가 부를 때마다 그 블록 잇기 전부로 다시 셈) · 바탕 = 그 SK 문서 바탕(로직 · 입력값 · 환산) - 단가산출 칸: 만든 줄 표 못옮김 열 · [잇기 (n)] · 만든 줄 모아 둠 · 고름 대기 후보 계수(성격 다른 단위) = 계수 칸 → 환산 {계수} · 729-5 남은못옮김 칸 미리 받음 - 안 이은 줄 표: 못옮김 남은 SK 를 쓰는 줄(단산 · SK · 못옮김) = [못옮김 잇기 (n)] - 시험 test_729_3_hopyo_left_screen 소스 확인 더함(계수 · 잇기 모두 · 바탕) Co-Authored-By: Claude Opus 5.5 --- .../M02_MasterTemplete_Hopyo_Api.ts | 24 ++- .../M02_MasterTemplete_Hopyo_Left.ts | 13 ++ .../M02_MasterTemplete_Hopyo_Lost.ts | 178 ++++++++++++++++++ .../M02_MasterTemplete_Hopyo_WizardSk.ts | 65 ++++++- .../tester/test_729_3_hopyo_left_screen.py | 12 +- ui_template/ui_template_locale_m2.ts | 22 +++ 6 files changed, 304 insertions(+), 10 deletions(-) create mode 100644 M02_MasterTemplete/M02_MasterTemplete_Hopyo_Lost.ts diff --git a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Api.ts b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Api.ts index 830e4444c..7660e74c1 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Api.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Api.ts @@ -228,6 +228,8 @@ export interface SangeunCand { 단위: string; 판정: string; 글: string; + /** 729-4 — 기준단위 ↔ 엑셀 단위 성격 다름 = 환산 계수 필요(가 1 = 나 몇) */ + 계수?: { 가: string; 나: string; 갈래: string } | null; } /** 안 이은 줄(729-2) — 만들면 남을 엑셀 줄 · 단가산출 줄 후보 = 고름 대기 기초 후보 */ @@ -240,9 +242,13 @@ export interface HopyoLeft { 갈래: string; 까닭: string; 후보: SangeunCand[]; + /** 729-4 — 못옮김 남은 SK 를 쓰는 줄 = 그 블록 · SK · 못옮김(잇기 = [단가산출 만들기]) */ + 단산?: number; + SK?: string; + 못옮김?: SangeunLost[]; } -/** 옮기지 못한 엑셀 줄(729-4) — M01 요소로 이으면 SK 더한줄 */ +/** 옮기지 못한 엑셀 줄(729-4) — M01 요소로 이으면 SK 더한줄 · id = 「갈래:이름」 */ export interface SangeunLost { id: string; 이름: string; @@ -250,9 +256,15 @@ export interface SangeunLost { 단위?: string; 수량?: number | null; 갈래: "자재" | "기계" | "노임"; + 글?: string; 후보?: unknown[]; } +/** 못옮김 잇기 한 줄(729-4) — 이음 키(LB · EQ · MT) 또는 뺌 까닭 */ +export type SangeunLink = { id: string; 뺌?: string } & Partial< + Record<"노임" | "기계" | "자재", string> +>; + /** [단가산출 만들기](729-1) 답 — 만듦 = 기초 · SK · 고름 대기 = 블록마다 후보 */ export interface SangeunBuilt { 블록: number; @@ -269,12 +281,20 @@ export interface SangeunBuilt { 후보: SangeunCand[]; }[]; 별칭: number; + /** 729-5 — 이미 만든(그대로) SK 중 못옮김 남은 것 */ + 남은못옮김?: { 번: number; 이름: string; 키: string; 못옮김: SangeunLost[] }[]; } /** 고른 기초(블록 번 → 로직) · 729-4 잇기 */ export type SangeunPick = Record< number, - { 로직: string; 입력값?: Record; 잇기?: Record[] } + { + 로직?: string; + 입력값?: Record; + 환산?: { 계수?: number }; + /** 그 블록 잇기 전부(부를 때마다 다시 셈 · 앞 잇기 안 남음) */ + 잇기?: SangeunLink[]; + } >; /** 확인한 줄 — 대상 목록 또는 뺌 까닭 */ diff --git a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Left.ts b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Left.ts index a12aa6b09..712d6c1af 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Left.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Left.ts @@ -6,6 +6,7 @@ * [고르기] = 확인 창 고르기 창(후보 · 기초 · 자재 찾기 · 옵션은 서버 `guess`) · [찾기] = 기초 · 노임(LB) · 자재(MT) · * [기계] = M01 기계(EQ) · % 줄 = 밑수 비목 · [우리에 없음] = 엑셀 숫자 그대로(사용자가 고른 것) * · 단가산출 고름 대기 줄 = 후보 기초 고름 → [SK 만들기](서버 `sangeun` 바탕 · 엑셀 차이 보존) → 그 줄은 SK 참조 + * · 못옮김 남은 SK 를 쓰는 줄(729-4 `단산` · `SK` · `못옮김`) = [못옮김 잇기 (n)] → `Hopyo_Lost` 창 * · 남은 수 = 안 만든 호표만(만든 호표는 서버가 남은 줄 있으면 안 만듦) — 마법사 [끝내기] 막음 · 띠 * 계산 · 판정은 서버 — 화면은 보이기 · 고름만. * ========================================================================== */ @@ -36,6 +37,7 @@ import { import { bodyOf, draftKey, loadDraft, saveDraft, toTarget } from "./M02_MasterTemplete_Hopyo_Check"; import { openHopyoChoose } from "./M02_MasterTemplete_Hopyo_Choose"; import { openFindPicker } from "./M02_MasterTemplete_Find_Pick"; +import { openLost } from "./M02_MasterTemplete_Hopyo_Lost"; /** 파일 층 · 만들 층 · 파일 키 */ export interface LeftScope { @@ -245,6 +247,17 @@ export function openLeft( go.dataset.leftSk = String(block.번); box.append(pick.root, go); } + if (row.못옮김?.length && row.단산 !== undefined && row.SK) { + const lost = createButton({ + label: `${L("M02_LostTitle")} (${row.못옮김.length})`, + size: 24, + onClick: () => + openLost({ scope: at.scope, key: at.key, 번: row.단산!, sk: row.SK!, 이름: row.이름, + lost: row.못옮김!, done: () => void again(no) }), // prettier-ignore + }); + lost.dataset.leftLost = String(row.단산); + box.append(lost); + } if (row.갈래 === "%" && line) { const base = createSelectField({ options: [{ value: "", text: L("M02_LeftBase") }].concat( diff --git a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Lost.ts b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Lost.ts new file mode 100644 index 000000000..54815aa70 --- /dev/null +++ b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Lost.ts @@ -0,0 +1,178 @@ +/* ============================================================================= + * M02_MasterTemplete_Hopyo_Lost.ts + * 단가산출 못옮김 잇기 창(PLAN 729-3 · 729-4 이음 · 요구서 내역19 2장 3 · 4) — 실무 블록을 SK 로 옮길 때 우리 M01 에 + * 바로 못 옮긴 엑셀 줄(노임 · 기계 · 자재)을 줄마다 M01 요소로 이음 → SK 더한줄(엑셀 수량) · [뺌] = 놓음(까닭 남김). + * · 고름 = 캐시 `m02-hopyo-lost:{층}:{주인}:{키}:{번}` `{id: 잇기}` — 서버는 부를 때마다 그 블록 잇기 전부로 다시 셈 + * (앞 잇기 안 남음) → [잇기 만들기] 는 캐시 모두를 보냄 · 바탕 = 그 SK 문서 바탕(로직 · 입력값 · 환산 그대로) + * · 남은 못옮김 = 서버 답 그대로 표 · 0 이면 그 SK 를 쓰는 일위대가 줄이 이어짐(남으면 만들기 409) + * 계산은 서버 — 화면은 고름 · 보이기만 · 저장은 [잇기 만들기] 누를 때만. + * ========================================================================== */ + +import { createButton, el, showToast } from "@ui/ui_template_elements"; +import { t as L } from "@ui/ui_template_locale"; +import { openModal } from "@ui/ui_template_modal"; +import { createTable } from "@ui/ui_template_table"; +import { createText } from "@ui/ui_template_text"; +import { readTemplate } from "./M02_MasterTemplete_Api_Fetch"; +import { openFindPicker } from "./M02_MasterTemplete_Find_Pick"; +import { + makeSangeun, + type HopyoScope, + type SangeunBuilt, + type SangeunLink, + type SangeunLost, +} from "./M02_MasterTemplete_Hopyo_Api"; +import { pickMachine } from "./M02_MasterTemplete_Hopyo_Left"; + +const why = (error: unknown): string => (error instanceof Error ? error.message : String(error)); +const NONE = "우리에 없음"; + +export const lostKey = (scope: HopyoScope, key: string, no: number): string => + `m02-hopyo-lost:${scope.layer}:${scope.owner ?? ""}:${key}:${no}`; + +/** 캐시 잇기 `{id: 잇기}` */ +export function loadLinks(k: string): Record { + try { + const got = JSON.parse(sessionStorage.getItem(k) ?? "{}") as unknown; + return got && typeof got === "object" ? (got as Record) : {}; + } catch { + return {}; + } +} +const saveLinks = (k: string, v: Record): void => + sessionStorage.setItem(k, JSON.stringify(v)); + +/** 고른 것 글 — 키 또는 뺌 */ +const linkText = (x: SangeunLink | undefined): string => + !x ? "" : x.뺌 ? `${L("M02_LostDrop")} · ${x.뺌}` : (x.노임 ?? x.기계 ?? x.자재 ?? ""); + +export interface LostOptions { + scope: HopyoScope; + key: string; + /** 블록 번(단산N) */ + 번: number; + /** 그 블록 SK 키 — 바탕을 읽음 */ + sk: string; + 이름?: string; + lost: SangeunLost[]; + /** 잇기 만든 뒤(서버 답) */ + done?: (got: SangeunBuilt) => void; +} + +/** 못옮김 잇기 창 */ +export function openLost(opt: LostOptions): void { + const k = lostKey(opt.scope, opt.key, opt.번); + let lost = opt.lost; + openModal({ + title: `${L("M02_LostTitle")} — 단산${opt.번} ${opt.이름 ?? ""}`.trim(), + closeLabel: L("M02_Close"), + width: "960px", + dialogClass: "m02-modal", + mount: (body, _close, actions) => { + const list = el("div", { attrs: { "data-lost": "" } }); + const put = (id: string, link: SangeunLink | null): void => { + const all = loadLinks(k); + if (link) all[id] = link; + else delete all[id]; + saveLinks(k, all); + draw(); + }; + const make = createButton({ + label: L("M02_LostMake"), + variant: "filled", + onClick: () => void run(), + }); + make.dataset.lostMake = ""; + actions.append(make); + body.append(createText(L("M02_LostHint"), "hint", 14), list); + + async function run(): Promise { + const links = Object.values(loadLinks(k)); + if (!links.length) return showToast(L("M02_WizSkPickFirst"), "warning"); + make.disabled = true; + try { + const doc = (await readTemplate(opt.scope.layer, "sangeun", opt.sk, null, opt.scope.owner)) + .문서 as { 바탕?: { 로직?: string } | null; 입력값?: Record; 환산?: { 계수?: number } }; // prettier-ignore + const pick = { + ...(doc.바탕?.로직 ? { 로직: doc.바탕.로직, 입력값: doc.입력값 ?? {} } : {}), + ...(doc.환산?.계수 ? { 환산: { 계수: doc.환산.계수 } } : {}), + 잇기: links, + }; + const got = await makeSangeun(opt.scope, opt.key, { [opt.번]: pick }); + lost = got.만듦.find((x) => x.번 === opt.번)?.못옮김 ?? []; + showToast(L("M02_LostLeft").replace("{n}", String(lost.length)), lost.length ? "warning" : "success"); // prettier-ignore + opt.done?.(got); + draw(); + } catch (error) { + showToast(why(error), "error"); + } finally { + make.disabled = false; + } + } + + function draw(): void { + const all = loadLinks(k); + list.dataset.lost = String(lost.length); + const rows = lost.map((x) => { + const kind = x.갈래; + const find = createButton({ + label: kind === "기계" ? L("M02_LeftMachine") : L("M02_LeftFind"), + size: 24, + variant: "ghost", + onClick: () => + kind === "기계" + ? pickMachine(x, (code) => put(x.id, { id: x.id, 기계: code })) + : openFindPicker({ + layer: opt.scope.layer, + owner: opt.scope.owner, + title: `${x.이름} ${x.규격 ?? ""}`.trim(), + logic: () => showToast(L("M02_GridLinkNoLogic"), "info"), + element: (e) => + e.종류 === (kind === "노임" ? "인력" : "재료") + ? put(x.id, { id: x.id, [kind]: e.코드 }) + : showToast(L("M02_LostKind").replace("{k}", kind), "warning"), + }), + }); + const drop = createButton({ + label: NONE, + size: 24, + variant: "ghost", + onClick: () => put(x.id, { id: x.id, 뺌: NONE }), + }); + drop.title = L("M02_LostDropTip"); + const clear = createButton({ + label: L("M02_LostClear"), + size: 24, + variant: "link", + onClick: () => put(x.id, null), + }); + return [ + kind, + x.이름, + x.규격 ?? "", + `${x.수량 ?? ""} ${x.단위 ?? ""}`.trim(), + x.글 ?? "", + linkText(all[x.id]), + el("div", { className: "ui-toolbar", children: [find, drop, ...(all[x.id] ? [clear] : [])] }), // prettier-ignore + ]; + }); + list.replaceChildren( + createTable( + [ + { label: L("M02_LeftKind"), width: "56px" }, + { label: L("M02_SangeunColName") }, + { label: L("M02_LeftSpec") }, + { label: L("M02_SangeunColQty"), width: "90px" }, + { label: L("M02_LeftWhy") }, + { label: L("M02_LeftPicked") }, + { label: L("M02_LeftPick"), width: "220px" }, + ], + rows, + { density: "data", empty: L("M02_LostZero") }, + ), + ); + } + draw(); + }, + }); +} diff --git a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_WizardSk.ts b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_WizardSk.ts index adf0bb19f..0835a1e4d 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_WizardSk.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_WizardSk.ts @@ -3,14 +3,23 @@ * 일위대가 불러오기 마법사 ② 단가산출 단계(PLAN 729-3 · 요구서 내역19 1장 7 · 2장 3) — * · [단가산출 만들기] = 서버 `sangeun`(산근 블록마다 매칭 → 같으면 기초 · 다르면 SK = 기초 + 엑셀 차이 · 맞는 기초 없음 = 고름 대기) * · 만든 줄 표 = 블록 · 이름 · 키 · 갈래(기초 · SK 차이 있음) · 이미 만든 블록은 「그대로」 수만 - * · 고름 대기 표 = 블록마다 후보(판정 · 글) 고르기 칸 → [고른 기초로 만들기](바탕 = 고른 기초 · 엑셀 품 견줘 SK) + * · 고름 대기 표 = 블록마다 후보(판정 · 글) 고르기 칸 → [고른 기초로 만들기](바탕 = 고른 기초 · 엑셀 품 견줘 SK) · + * 단위 성격 다른 후보(729-4 `계수`) = 계수 칸(가 1 = 나 몇) 함께 → `환산 {계수}` + * · 못옮김(729-4) = 만든 줄마다 수 · [잇기] = 못옮김 잇기 창(`Hopyo_Lost`) · 만든 줄은 이 칸이 모아 둠(누를 때마다 더함) · + * 이미 만든 SK 의 남은 못옮김(729-5 `남은못옮김`)도 같은 표 * · 별칭 제안 = 이 층 별칭 장부에 남음 · 모두에게 쓰이려면 시스템 관리자가 M01 › 별칭 장부에서 승인(링크 · 716 길 그대로) * 계산 · 판정은 서버 — 화면은 보이기 · 고름만 · 자동 저장 없음(누를 때만 서버). * ========================================================================== */ import { ROUTES } from "@config/config_frontend"; import { createBadge } from "@ui/ui_template_badge"; -import { createButton, createSelectField, el, showToast } from "@ui/ui_template_elements"; +import { + createButton, + createInputField, + createSelectField, + el, + showToast, +} from "@ui/ui_template_elements"; import { t as L } from "@ui/ui_template_locale"; import { createTable } from "@ui/ui_template_table"; import { createText } from "@ui/ui_template_text"; @@ -20,6 +29,9 @@ import { type SangeunBuilt, type SangeunPick, } from "./M02_MasterTemplete_Hopyo_Api"; +import { openLost } from "./M02_MasterTemplete_Hopyo_Lost"; + +type Made = SangeunBuilt["만듦"][number]; const why = (error: unknown): string => (error instanceof Error ? error.message : String(error)); @@ -48,6 +60,7 @@ export function sangeunStep(scope: HopyoScope, key: string): { root: HTMLElement children: [el("div", { className: "ui-toolbar", children: [make] }), out], }); + const made = new Map(); // 이 칸에서 만든 줄(누를 때마다 더함) async function run(picks?: SangeunPick): Promise { make.disabled = true; try { @@ -69,18 +82,39 @@ export function sangeunStep(scope: HopyoScope, key: string): { root: HTMLElement .replace("{keep}", String(got.그대로)) .replace("{wait}", String(got.고름대기.length)); + /** 못옮김 칸 — 수 + [잇기](남은 것 있을 때) */ + const lostCell = (x: Made, got: SangeunBuilt): HTMLElement => { + const n = x.못옮김?.length ?? 0; + if (!n) return el("span", { text: "0" }); + const go = createButton({ + label: `${L("M02_LostOpen")} (${n})`, + size: 24, + onClick: () => + openLost({ scope, key, 번: x.번, sk: x.키, 이름: x.이름, lost: x.못옮김 ?? [], + done: (next) => draw({ ...got, 만듦: next.만듦, 별칭: got.별칭 }) }), // prettier-ignore + }); + go.dataset.wizLost = String(x.번); + return go; + }; + function draw(got: SangeunBuilt): void { - const made = createTable( + for (const x of got.남은못옮김 ?? []) if (!made.has(x.번)) made.set(x.번, { ...x, 갈래: "SK" }); + for (const x of got.만듦) made.set(x.번, x); + const table = createTable( [ { label: L("M02_WizSkBlock"), width: "80px" }, { label: L("M02_SangeunColName") }, { label: L("M02_WizSkKey"), width: "110px" }, { label: L("M02_WizSkKind"), width: "120px" }, + { label: L("M02_LostCol"), width: "120px" }, ], - got.만듦.map((x) => [`단산${x.번}`, x.이름, x.키, kindBadge(x.갈래)]), + [...made.values()] + .sort((a, b) => a.번 - b.번) + .map((x) => [`단산${x.번}`, x.이름, x.키, kindBadge(x.갈래), lostCell(x, got)]), { density: "data", empty: L("M02_WizSkNoneMade") }, ); const selects = new Map(); + const coefs = new Map(); const wait = createTable( [ { label: L("M02_WizSkBlock"), width: "80px" }, @@ -100,7 +134,18 @@ export function sangeunStep(scope: HopyoScope, key: string): { root: HTMLElement }); pick.select.dataset.wizWait = String(x.번); selects.set(x.번, pick.select); - return [`단산${x.번}`, x.이름, x.단위 ?? "", x.까닭, pick.root]; + const coef = createInputField({ type: "number", min: 0, compact: true, size: 24, + placeholder: L("M02_WizSkCoef") }); // prettier-ignore + coef.input.dataset.wizCoef = String(x.번); + const need = (): void => { + const c = x.후보.find((y) => y.로직 === pick.select.value)?.계수; + coef.root.hidden = !c; + coef.input.title = c ? `${c.가} 1 = ${c.나} ?` : ""; + }; + pick.select.addEventListener("change", need); + need(); + coefs.set(x.번, coef.input); + return [`단산${x.번}`, x.이름, x.단위 ?? "", x.까닭, el("div", { className: "ui-toolbar", children: [pick.root, coef.root] })]; // prettier-ignore }), { density: "data", empty: L("M02_WizSkNoWait") }, ); @@ -110,7 +155,13 @@ export function sangeunStep(scope: HopyoScope, key: string): { root: HTMLElement size: 28, onClick: () => { const picks: SangeunPick = {}; - for (const [no, s] of selects) if (s.value) picks[no] = { 로직: s.value }; + for (const [no, s] of selects) { + if (!s.value) continue; + const c = Number(coefs.get(no)?.value); + const need = got.고름대기.find((x) => x.번 === no)?.후보.find((y) => y.로직 === s.value)?.계수; // prettier-ignore + if (need && !(c > 0)) return showToast(L("M02_WizSkCoefNeed").replace("{n}", String(no)), "warning"); // prettier-ignore + picks[no] = { 로직: s.value, ...(need ? { 환산: { 계수: c } } : {}) }; + } if (!Object.keys(picks).length) return showToast(L("M02_WizSkPickFirst"), "warning"); void run(picks); }, @@ -123,7 +174,7 @@ export function sangeunStep(scope: HopyoScope, key: string): { root: HTMLElement }); out.replaceChildren( createText(summary(got), "muted", 14), - made, + table, ...(got.고름대기.length ? [ createText(L("M02_WizSkWaitHint"), "hint", 14), diff --git a/resources/tester/test_729_3_hopyo_left_screen.py b/resources/tester/test_729_3_hopyo_left_screen.py index b344e21da..40feabb03 100644 --- a/resources/tester/test_729_3_hopyo_left_screen.py +++ b/resources/tester/test_729_3_hopyo_left_screen.py @@ -100,7 +100,16 @@ def test_마법사_끝내기_막음_단가산출_칸_목록_열(): assert "finish.disabled = !!n?.줄;" in wizard # 띠 · 남은 수 · [끝내기] 막음 assert "sangeunStep(scope, file.키)" in wizard and "/sangeun`" not in wizard sk = _text("M02_MasterTemplete_Hopyo_WizardSk.ts") - assert "makeSangeun(scope, key, picks)" in sk and "picks[no] = { 로직: s.value }" in sk + assert "makeSangeun(scope, key, picks)" in sk + assert "picks[no] = { 로직: s.value, ...(need ? { 환산: { 계수: c } } : {}) };" in sk # 계수 칸 + lost = _text( + "M02_MasterTemplete_Hopyo_Lost.ts" + ) # 못옮김 잇기 = 캐시 모두 · 바탕 = SK 문서 바탕 + assert "const links = Object.values(loadLinks(k));" in lost and "잇기: links," in lost + assert "로직: doc.바탕.로직, 입력값: doc.입력값 ?? {}" in lost + assert "row.못옮김?.length && row.단산 !== undefined && row.SK" in _text( + "M02_MasterTemplete_Hopyo_Left.ts" + ) lib = _text("M02_MasterTemplete_Library_List.ts") assert 'L("M02_LibUnlinked"), (r) => (r.안이은줄' in lib assert "unlinkedBar(doc, edit)" in _text("M02_MasterTemplete_Labor_Grid.ts") @@ -108,6 +117,7 @@ def test_마법사_끝내기_막음_단가산출_칸_목록_열(): "M02_MasterTemplete_Hopyo_Left.ts", "M02_MasterTemplete_Hopyo_WizardSk.ts", "M02_MasterTemplete_Labor_Unlinked.ts", + "M02_MasterTemplete_Hopyo_Lost.ts", ): src = _text(name) assert len(src.splitlines()) <= 700, name diff --git a/ui_template/ui_template_locale_m2.ts b/ui_template/ui_template_locale_m2.ts index 7cd24189c..066615d77 100644 --- a/ui_template/ui_template_locale_m2.ts +++ b/ui_template/ui_template_locale_m2.ts @@ -327,4 +327,26 @@ export const ui_locales_m2 = { "Fixed rows link to labor, material or machine only", ], M02_LibUnlinked: ["안 이은 줄", "Unlinked"], + M02_WizSkCoef: ["환산 계수", "Coefficient"], + M02_WizSkCoefNeed: [ + "단산{n} — 단위 성격 다름 · 환산 계수(0 보다 큼) 필요", + "Block {n} — units differ · enter a coefficient", + ], + M02_LostCol: ["못옮김", "Not moved"], + M02_LostOpen: ["잇기", "Link"], + M02_LostTitle: ["못옮김 잇기", "Link unmoved rows"], + M02_LostHint: [ + "엑셀에만 있어 SK 로 못 옮긴 줄 — 줄마다 M01 노임 · 기계 · 자재로 이음(SK 더한줄 · 엑셀 수량) 또는 우리에 없음 · 고른 것 모두를 [잇기 만들기]로 보냄", + "Excel-only rows not moved into the SK — link each to M01 labor, machine or material, then make", + ], + M02_LostMake: ["잇기 만들기", "Make links"], + M02_LostLeft: ["남은 못옮김 {n}", "{n} unmoved rows left"], + M02_LostZero: ["못옮김 0", "Nothing unmoved"], + M02_LostDrop: ["뺌", "Dropped"], + M02_LostDropTip: [ + "우리 M01 에 없음 — SK 에 안 넣고 까닭만 남김(사용자가 고른 것)", + "Not in M01 — kept out of the SK with a note", + ], + M02_LostClear: ["고름 지움", "Clear"], + M02_LostKind: ["{k} 줄 — 같은 갈래 요소만 이음", "{k} row — link the same kind only"], } as const;