diff --git a/A00_Common/formula_doc_text.ts b/A00_Common/formula_doc_text.ts index a20e06c60..2a4a8e06c 100644 --- a/A00_Common/formula_doc_text.ts +++ b/A00_Common/formula_doc_text.ts @@ -94,10 +94,6 @@ const TEXT = { "저장 안 한 고침이 있음 — [저장] 또는 [되돌림] 먼저", "Save or discard edits first", ], - No_Key: [ - "저장한 단가산출만 수식 문서를 엶 — [단가산출 상세] 탭", - "Saved items only — use the detail tab", - ], Read_Only: ["읽기만", "Read only"], Tools: ["수식 도구", "Equation tools"], Tool_Frac: ["분수", "Fraction"], diff --git a/M01_MasterData/M01_MasterData_UI_LogicLab.ts b/M01_MasterData/M01_MasterData_UI_LogicLab.ts index 2e60bc6e8..a406920eb 100644 --- a/M01_MasterData/M01_MasterData_UI_LogicLab.ts +++ b/M01_MasterData/M01_MasterData_UI_LogicLab.ts @@ -1,7 +1,6 @@ /* ============================================================================= * M01_MasterData_UI_LogicLab.ts - * 관리자 화면 — 로직 칸. 로직을 열면 두 칸 — 왼쪽 수식(줄 쌓기 `A00_Common/line_stack_screen.ts` · 804-6) · 단가산출 상세 / 오른쪽 원문 - * 셈 = 화면 칸 없이 구동(`_Logic_Drive.ts` · 799-2) — 열 때 첫 셈 · 입력 · 시험 고침 · 재료 고름이 `drive.run` + * 관리자 화면 — 로직 칸. 로직을 열면 두 칸 — 왼쪽 수식(줄 쌓기 `A00_Common/line_stack_screen.ts` · 804-6) / 오른쪽 원문 * 로직 목록(원문·장 고르기 · 찾기 · 막힘 거름)은 왼쪽 패널 「단가산출 로직」 컨테이너(`listHost`) 안 * * 데이터 흐름(CLAUDE.md 5장) — 고친 것은 캐시(sessionStorage)에 쌓고 [저장] 한 번에 `POST /save`. @@ -29,18 +28,14 @@ import { fetchLogicUsers, removeLogic, saveFiles, - type CalcLine, - type TextAnswer, - type ElementBrief, type LogicRow, type LogicSummary, type SaveFile, } from "./M01_MasterData_UI_Logic_Api"; -import { createDrive } from "./M01_MasterData_UI_Logic_Drive"; import { buildList, logicId, type ListItem, type ListMark } from "./M01_MasterData_UI_Logic_List"; import type { SideHandle } from "./M01_MasterData_UI_Side"; import { tx } from "./M01_MasterData_UI_Logic_Text"; -import { buildLabDetail, isOwnLogic, trial } from "./M01_MasterData_UI_LogicLab_Detail"; +import { buildLabDetail } from "./M01_MasterData_UI_LogicLab_Detail"; import { setLogicOpener } from "./M01_MasterData_UI_LogicLab_Modal"; import { openCopyScreen } from "./M01_MasterData_UI_LogicLab_Copy"; import { tl } from "./M01_MasterData_UI_LogicLab_Text"; @@ -67,19 +62,11 @@ interface Opened extends Draft { id: string; row: LogicRow | null; original: string | null; - prices: Record; reasons: string[]; - lines: CalcLine[] | null; - text: TextAnswer | null; - /** 마지막으로 잘 받은 읽는 식 — 멈춘 뒤에도 수량 칸 조각을 이걸로 */ - lastText?: TextAnswer; ownHost: HTMLElement; - values: Record; } const OWN_FILE = "로직_자체.json"; -/** 시험 계산에 넘길 줄 — 시험 고침(722-2) 덮은 사본 */ -const calcRow = (row: LogicRow | null): LogicRow | null => trial.apply(row); const cacheKey = "m01_logic_drafts_lab"; const ownHost = (): HTMLElement => el("div", { className: "m01lab__own" }); @@ -105,12 +92,9 @@ export async function mountM01LogicLab( ): Promise { let drafts = loadDrafts(); let opened: Opened | null = null; - let calcInputs = ""; const admin = await fetchSessionUser() .then((u) => u?.role === "SYSTEM_ADMIN") .catch(() => false); - let recalc: number | undefined; - const drive = createDrive(); // 수식 = 줄 쌓기(804-2 · 804-6 옛 수식 문서 탭 걷음) — 수식 문서 · 캐시 m01_doc_drafts const stack = createStackScreen({ base: "/m01/logic/doc", @@ -122,7 +106,6 @@ export async function mountM01LogicLab( const editor = el("div", { className: "m01-logic__editor" }); // 로직 상세 — 왼쪽 지금 내용 · 오른쪽 접는 패널 「원문」 const panes = buildLogicSide(editor); - editor.dataset.view = "stack"; // 보기 = 수식 하나(836-3) — 「단가산출 상세」 덩이는 CSS 로 숨김 const errors = el("div", { className: "ui-banner ui-banner--danger m01-logic__reasons", attrs: { hidden: "" }, @@ -181,13 +164,6 @@ export async function mountM01LogicLab( if (opened.origKey !== null && now === opened.original) delete drafts[opened.id]; else drafts[opened.id] = pick(opened, clone(opened.row)); persist(); - const inputs = JSON.stringify(calcRow(opened.row)?.입력 ?? []); - if (inputs !== calcInputs) drawCalc(); - if (isOwnLogic(opened.row)) { - // 자체 로직 — 고친 뒤 잠시 쉬면 다시 계산해 호표 소계와 텍스트 수식을 새로 그림 - window.clearTimeout(recalc); - recalc = window.setTimeout(() => void drive.run(), 700); - } }; const pick = (o: Opened, row: LogicRow | null): Draft => ({ @@ -198,32 +174,6 @@ export async function mountM01LogicLab( row, }); - /** 구동을 지금 로직으로 (다시) 시작 — 읽는 식 미리보기 · 첫 셈 */ - const drawCalc = (): void => { - if (!opened?.row) { - drive.stop(); - return; - } - const current = opened; - calcInputs = JSON.stringify(calcRow(current.row)?.입력 ?? []); - drive.start({ - savedKey: current.origKey, - file: current.file, - row: calcRow(current.row) as LogicRow, - dirty: () => current.id in drafts || trial.has(current.row), - values: current.values, - onText: (text) => { - current.text = text; - if (text.ok) current.lastText = text; - drawEditor(); // [계산] 전 미리보기 응답도 왼쪽 이름 식 칸에 바로 비치게 — onLines 와 같은 자리 - }, - onLines: (lines) => { - current.lines = lines; - drawEditor(); - }, - }); - }; - const drawEditor = (): void => { const listing = !opened; // 고른 로직이 없으면 목록 · 있으면 호표 표 + 시험 계산 list.root.hidden = !listing || !loaded; @@ -242,37 +192,19 @@ export async function mountM01LogicLab( row: current.row as LogicRow, file: current.file, reasons: current.reasons, - prices: current.prices, - lines: current.lines, - text: current.text, - lastText: current.lastText, - values: current.values, ownHost: current.ownHost, onChange: touch, - onInput: (name, v) => syncLeft(current, { [name]: v }), - onTrial: drawCalc, - rerun: () => void drive.run(), admin, stack: stack.root, }); stack.draw(current.origKey ?? ""); }; - /** 단가산출 상세 옵션 입력 → 시험 입력(이름이 있는 칸만) · 바뀌면 다시 셈 */ - const syncLeft = (o: Opened, ex: Record): void => { - const names = new Set((calcRow(o.row)?.입력 ?? []).map((x) => x.이름)); - const changed = Object.entries(ex).filter(([k, v]) => names.has(k) && o.values[k] !== v); - if (!changed.length) return; - for (const [k, v] of changed) o.values[k] = v; - void drive.run(); - }; - const show = (next: Opened | null): void => { opened = next; panes.showSource(next?.row?.출처 ?? ""); errors.hidden = true; drawEditor(); - drawCalc(); }; const open = async (id: string, sub: string, key: string): Promise => { @@ -282,12 +214,8 @@ export async function mountM01LogicLab( ...draft, id, original: null, - prices: {}, reasons: [], - lines: null, - text: null, ownHost: ownHost(), - values: {}, }); return; } @@ -301,12 +229,8 @@ export async function mountM01LogicLab( origKey: key, row: draft ? (draft.row === null ? null : clone(draft.row)) : clone(one.logic), original: JSON.stringify(one.logic), - prices: one.prices, reasons: one.reasons, - lines: null, - text: null, ownHost: ownHost(), - values: {}, }); } catch (error) { showToast(error instanceof Error ? error.message : tx("Load_Failed"), "error"); diff --git a/M01_MasterData/M01_MasterData_UI_LogicLab_Cover.ts b/M01_MasterData/M01_MasterData_UI_LogicLab_Cover.ts index deb5fec0e..5c0b98b30 100644 --- a/M01_MasterData/M01_MasterData_UI_LogicLab_Cover.ts +++ b/M01_MasterData/M01_MasterData_UI_LogicLab_Cover.ts @@ -2,9 +2,7 @@ * M01_MasterData_UI_LogicLab_Cover.ts * 단가산출 상세 줄 「그 자리 고침」(PLAN 722-2 · 내역 16 · 17-2) — 줄 모달 안 수량 · 단가 칸. * 고침 꼴 = M02 단가산출 덮개 칸과 같음(`qty` · `labor` · `material` · `expense` — `M02_Sangeun_Doc._covered` 규칙 중 품 · 단가) - * M01 = 시험 값만 — 고친 값은 화면 메모리(로직 줄 객체 열쇠)에만 · 셈 = 줄 사본에 덮어 서버로(`/calc` · `/text` row) - * · 정본 · 요소(노임 · 자재) · 캐시(초안) 안 바뀜 · 로직을 다시 열면 사라짐 - * M02(724) = 같은 고침 칸 · 같은 꼴을 고침 고리(`HoEdit.set`)로 받아 덮개로 보관 + * M02(724) = 고침 칸의 값을 고침 고리(`HoEdit.set`)로 받아 덮개로 보관 — M01 시험 고침은 840-1 에 걷음 * 서버 셈 = 줄 `단가`(손 단가) · 로직 줄 `값묶음`(노 · 재 · 경) · `수량` 글 — `master_pick._priced` · `bundled` 가 읽음 * ========================================================================== */ @@ -23,47 +21,8 @@ export interface LineCover { export const COVER_PARTS = { labor: "노무비", material: "재료비", expense: "경비" } as const; export type CoverPart = keyof typeof COVER_PARTS; -/** 지수 꼴 없는 수 글(엔진 식 칸) — 1e-7 → 0.0000001 */ -const plainNum = (v: number): string => - v.toLocaleString("en-US", { useGrouping: false, maximumFractionDigits: 20 }); - -/** 호표 줄(차례 열쇠)에 고침을 덮은 줄 사본 — 원본은 안 바뀜 · 덧줄은 고침 대상 밖 */ -export function applyCovers( - row: R, - covers: Record, -): R { - const ho = (row.호표 ?? []).map((raw, i) => { - const c = covers[i]; - if (!c) return raw; - const item = raw as Record; - const out: Record = { ...item }; - if (c.qty !== undefined) out.수량 = plainNum(c.qty); - const parts = (Object.keys(COVER_PARTS) as CoverPart[]).filter((k) => c[k] !== undefined); - if (parts.length && item.종류 === "로직") { - const given = parts.map((k) => [COVER_PARTS[k], c[k]]); - out.값묶음 = { ...((item.값묶음 as object) ?? {}), ...Object.fromEntries(given) }; - } else if (parts.length) out.단가 = c[parts[0]]; - return out; - }); - return { ...row, 호표: ho }; -} /* ── 순수 셈 끝 ── */ -/** 시험 고침 — 로직 줄 객체마다(다시 열면 새 객체 = 사라짐) */ -const trials = new WeakMap>(); -export const trial = { - of(row: object): Record { - let got = trials.get(row); - if (!got) trials.set(row, (got = {})); - return got; - }, - has: (row: object | null): boolean => !!row && Object.keys(trials.get(row) ?? {}).length > 0, - /** 시험 계산에 넘길 줄 — 고침 없으면 원본 그대로(같은 객체) */ - apply(row: R): R { - return row && trial.has(row) ? (applyCovers(row, trials.get(row)!) as R) : row; - }, -}; - const TEXT = { Head: ["그 자리 고침 (시험 값)", "Edit here (trial value)"], Note: [ diff --git a/M01_MasterData/M01_MasterData_UI_LogicLab_Detail.ts b/M01_MasterData/M01_MasterData_UI_LogicLab_Detail.ts index f91ac4350..f711da5bb 100644 --- a/M01_MasterData/M01_MasterData_UI_LogicLab_Detail.ts +++ b/M01_MasterData/M01_MasterData_UI_LogicLab_Detail.ts @@ -1,41 +1,33 @@ /* ============================================================================= * M01_MasterData_UI_LogicLab_Detail.ts * 「단가산출 로직」 상세 화면 — 컨테이너를 위에서 아래로 - * ① 기본정보(정본은 읽기 전용 · 비고만 고침) ② 수식(줄 쌓기 804-2 · 804-6) ③ 단가산출 상세(공용 부품 `_LogicLab_Ho.ts` — 하위 박스 · 계산식 · 박스 옵션 · 그 자리 고침) - * 시트 · 인력/재료/경비 · 시험 계산 · 텍스트 수식 보기는 799-2 에 걷음 — 셈은 화면 칸 없이 `_Logic_Drive.ts` 구동 - * 그 자리 고침(722-2) = 시험 값만(`_LogicLab_Cover.ts` trial — 줄 사본으로 셈 · 정본 · 요소 · 캐시 안 바뀜) + * ① 기본정보(정본은 읽기 전용 · 비고만 고침 · 자체 로직은 이름 · 단위 · 출처도) ② 수식(줄 쌓기 804-2 · 804-6) + * ③ M02 수식 문서 없는 줄(일위대가 줄 모달 등)만 단가산출 상세 몸(`cover` — 공용 부품 `_LogicLab_Ho.ts`) + * 단가산출 상세 보기 · 자체 로직 시트 편집기 · 시험 고침은 840-1 에 걷음 — 자체 로직도 수식(줄 쌓기)에서 만들고 고침 * ========================================================================== */ import { el } from "@ui/ui_template_elements"; import type { LogicRow } from "./M01_MasterData_UI_Logic_Api"; -import { buildEditor } from "./M01_MasterData_UI_Logic_Edit"; -import { openPicker } from "./M01_MasterData_UI_Logic_Pick"; import { safeReason, tx } from "./M01_MasterData_UI_Logic_Text"; -import { buildHo, lineOpener, type HoContext, type HoEdit } from "./M01_MasterData_UI_LogicLab_Ho"; -import { trial } from "./M01_MasterData_UI_LogicLab_Cover"; import { tl } from "./M01_MasterData_UI_LogicLab_Text"; import "./M01_MasterData_UI_LogicLab_Style.css"; import "./M01_MasterData_UI_LogicLab_Pill.css"; -import { block } from "./M01_MasterData_UI_LogicLab_View"; /** 원문 절 머리 개정 연도 글 — [2008, 2020, 2025] → 「'08 · '20 · '25」(없으면 빈 글) */ const revisionText = (row: LogicRow): string => (row.개정 ?? []).map((y) => `'${String(y).slice(-2)}`).join(" · "); -export { trial }; - -export interface DetailContext extends HoContext { +export interface DetailContext { + row: LogicRow; file: string; reasons: string[]; - /** 자체 로직 편집기 자리 — 다시 그려도 같은 칸을 씀(고치던 값이 안 날아감) */ + /** 기본정보 접힘 기억 자리 — 다시 그려도 같은 칸을 씀 */ ownHost: HTMLElement; onChange: () => void; /** 시스템 관리자 — 기본 로직 안내 글이 다름(시트에서 고침 / 보기만) */ admin?: boolean; - /** 722-2 시험 고침 뒤 — 구동을 고친 줄로 다시 시작(있으면 그 자리 고침 켜짐 · 셈은 `rerun`) */ - onTrial?: () => void; - /** 717-2b M02 단가산출 — 단가산출 상세 덩이를 이 칸으로(SK 셈 답 · 고침 표) */ - cover?: { ho: HTMLElement; open?: ReturnType }; + /** 717-2b M02 단가산출 — 수식 문서 없는 줄의 단가산출 상세 덩이(SK 셈 답 · 고침 표) */ + cover?: { ho: HTMLElement }; /** 수식 탭 = 줄 쌓기 그릇(`A00_Common/line_stack_screen.ts` · 804-2 · 804-6) */ stack?: HTMLElement; } @@ -148,88 +140,18 @@ function infoBox(ctx: DetailContext): HTMLElement[] { /** 자체 로직(키 GX…) — 정본은 아님 */ export const isOwnLogic = (row: LogicRow): boolean => row.키.startsWith("GX"); -/** 자체 로직은 정본 화면과 같은 편집기를 호표 아래에 폄(머리 칸은 기본정보에서 고치므로 숨김) */ -function ownEditor(ctx: DetailContext): HTMLElement { - if (!ctx.ownHost.childElementCount) - buildEditor(ctx.ownHost, { - row: ctx.row, - file: ctx.file, - isNew: false, - files: [], - reasons: [], - prices: ctx.prices, - lines: ctx.lines, - onChange: ctx.onChange, - onFile: () => undefined, - onPick: openPicker, - }); - return el("details", { - className: "m01lab__edit", - attrs: { open: "" }, - children: [ - el("summary", { text: tl("Edit_Title") }), - el("p", { className: "ui-text ui-text--muted", text: tl("Edit_SheetHint") }), - ctx.ownHost, - ], - }); -} - -/** M01 그 자리 고침 고리 — 시험 값(줄 객체 열쇠) · 고치면 시험 계산을 다시 그리고 셈 */ -function trialEdit(ctx: DetailContext): HoEdit | undefined { - const redraw = ctx.onTrial; - if (!redraw) return undefined; - const covers = trial.of(ctx.row); - return { - covers, - scope: "m01", - set: (idx, cover) => { - if (cover) covers[idx] = cover; - else delete covers[idx]; - redraw(); - ctx.rerun?.(); - }, - }; -} - -function hoBox(ctx: DetailContext): HTMLElement { - const money = !("결과" in ctx.row) && (ctx.row.결과단위 ?? "").startsWith("원"); - if (!money) { - return block( - section(tl("Ho_Title"), [ - el("p", { className: "ui-text ui-text--muted", text: tl("Not_Money") }), - el("pre", { className: "m01lab__expr", text: ctx.row.결과?.식 ?? "" }), - ]), - "ho", - ); - } - const body = buildHo({ ...ctx, edit: trialEdit(ctx) }); - const totalLine = body.pop()!; // 계 줄 — 자체 로직은 접힌 표 밖에 둠 - // 자체 로직 — 고치는 자리는 편집기 하나 · 읽기용 박스는 접어 둠(펼치면 소계가 보임) - const fold = el("details", { - className: "m01lab__fold", - attrs: ctx.ownHost.dataset.fold ? { open: "" } : {}, - children: [el("summary", { text: tl("Ho_Fold") }), ...body], - }); - fold.addEventListener("toggle", () => (ctx.ownHost.dataset.fold = fold.open ? "1" : "")); - const blocks = isOwnLogic(ctx.row) ? [ownEditor(ctx), fold, totalLine] : [...body, totalLine]; - const calculated = ctx.lines !== null; - const hint = calculated ? tl("Click_Hint") : `${tl("Click_Hint")} · ${tl("Calc_First")}`; - return block(section(tl("Ho_Title"), blocks, hint), "ho"); -} - -/** 상세 화면(기본정보 · 수식 · 단가산출 상세)을 `host` 에 쌓음 */ +/** 상세 화면(기본정보 · 수식 · M02 단가산출 상세 몸)을 `host` 에 쌓음 */ export function buildLabDetail(host: HTMLElement, ctx: DetailContext): void { - // 다시 그리면 포커스가 빠짐 — 고치던 칸(편집기 · 기본정보)을 기억해 되돌림 + // 다시 그리면 포커스가 빠짐 — 고치던 기본정보 칸을 기억해 되돌림 const active = document.activeElement; const kept = active instanceof HTMLInputElement || active instanceof HTMLTextAreaElement ? { node: active, from: active.selectionStart, to: active.selectionEnd } : null; - // 보기 고르기(`_View.ts`) — 덩이마다 이름표 · 고른 덩이만 보임 host.replaceChildren( ...infoBox(ctx), - ...(ctx.stack ? [block(ctx.stack, "stack")] : []), - ctx.cover ? block(section(tl("Ho_Title"), [ctx.cover.ho]), "ho") : hoBox(ctx), + ...(ctx.stack ? [ctx.stack] : []), + ...(ctx.cover ? [section(tl("Ho_Title"), [ctx.cover.ho])] : []), ); const own = kept?.node.dataset.own; // 기본정보 칸은 새로 만들어지므로 이름표로 다시 찾음 const back = own ? host.querySelector(`[data-own="${own}"]`) : kept?.node; diff --git a/M01_MasterData/M01_MasterData_UI_LogicLab_Ho.ts b/M01_MasterData/M01_MasterData_UI_LogicLab_Ho.ts index babe52457..93f28b3c5 100644 --- a/M01_MasterData/M01_MasterData_UI_LogicLab_Ho.ts +++ b/M01_MasterData/M01_MasterData_UI_LogicLab_Ho.ts @@ -31,7 +31,6 @@ import { pctView } from "./M01_MasterData_UI_Logic_Api"; import { formatNumber } from "./M01_MasterData_UI_Logic_Edit"; import { formatLineMoney, formatMoney, formatPrice } from "./M01_MasterData_UI_Logic_Money"; import { tx } from "./M01_MasterData_UI_Logic_Text"; -import { pickMaterial } from "./M01_MasterData_UI_Logic_CalcPick"; import { coverForm, type CoverPart, type LineCover } from "./M01_MasterData_UI_LogicLab_Cover"; import { condText, @@ -144,8 +143,6 @@ export interface HoContext { values: Record; /** 박스 옵션 · 입력 칩 고침 — 시험 계산 입력 고침 + 다시 셈(없으면 고르기 · 칩 없음) */ onInput?: (name: string, value: string) => void; - /** 지금 값으로 다시 셈(구동) — 재료 후보 고르기가 씀(없으면 후보만 보임) */ - rerun?: () => void; /** 그 자리 고침 고리(없으면 모달은 보기만) */ edit?: HoEdit; /** (M02 750-2) Ho 줄 차례 → SK 줄 cid — 읽는 식 줄을 `cid` 로 이음(이름 차례 추측 안 함) */ @@ -279,11 +276,7 @@ function modalOf(e: Entry, ctx: HoContext, extra: { cond?: PickCond; item?: HoLi // 재료 줄 = 시험 계산 자재 칸과 같은 모달 — 고르면 시험 입력으로 다시 셈(정본 안 바뀜) kind: item?.종류, usedSlot: line?.품목?.값칸, - ...(item?.종류 === "재료" && ctx.pickMat - ? { onPick: ctx.pickMat(e.idx) } - : item?.종류 === "재료" && ctx.rerun - ? { onPick: (it) => pickMaterial(ctx.values, e.idx, it, ctx.rerun!) } - : {}), + ...(item?.종류 === "재료" && ctx.pickMat ? { onPick: ctx.pickMat(e.idx) } : {}), // 덧줄(%) = M02 덮개만 율 고침(M01 시험 값은 호표 줄만) ...(edit && (e.extra ? edit.scope === "m02" : item) ? { diff --git a/M01_MasterData/M01_MasterData_UI_LogicLab_Style.css b/M01_MasterData/M01_MasterData_UI_LogicLab_Style.css index 8446e0132..5da731c80 100644 --- a/M01_MasterData/M01_MasterData_UI_LogicLab_Style.css +++ b/M01_MasterData/M01_MasterData_UI_LogicLab_Style.css @@ -177,12 +177,6 @@ font-weight: 600; } -/* 보기 고르기(710-2 · `_View.ts`) — 그릇 data-view 와 다른 이름표 덩이는 숨김 */ -.m01-logic__editor[data-view="stack"] [data-block]:not([data-block="stack"]), -.m01-logic__editor[data-view="ho"] [data-block]:not([data-block="ho"]) { - display: none; -} - /* 기본정보 — 늘 맨 위 접힌 한 줄 */ .m01lab__infofold > summary { display: flex; diff --git a/M01_MasterData/M01_MasterData_UI_LogicLab_Text.ts b/M01_MasterData/M01_MasterData_UI_LogicLab_Text.ts index a5bc204c8..8498cea68 100644 --- a/M01_MasterData/M01_MasterData_UI_LogicLab_Text.ts +++ b/M01_MasterData/M01_MasterData_UI_LogicLab_Text.ts @@ -17,19 +17,11 @@ const TEXT = { "Base logic — view only · only the note can be edited", ], Info_Own: [ - "자체 로직 — 칸을 고치면 잠시 뒤 호표 소계가 다시 계산됨", - "Own logic — edits recalculate the subtotals shortly", + "자체 로직 — 이름 · 단위 · 출처 · 비고는 여기서 · 식은 수식에서 고침", + "Own logic — edit the info here · formulas in the Equation view", ], - Edit_Title: [ - "고치기 (호표 · 중간 값 · 덧줄 · 끝수)", - "Edit (lines · middles · extras · rounding)", - ], - Ho_Fold: ["묶음 소계 표 (읽기용 — 고치기는 위 편집기)", "Group subtotals (read-only)"], Ho_Title: ["단가산출 상세", "Unit-price detail"], - Calc_Title: ["시험 계산", "Trial calculation"], View_Equation: ["수식", "Equation"], - Edit_SheetHint: ["식은 「수식」 보기에서 고침", "Edit formulas in the Equation view"], - View_Label: ["보기", "View"], G_labor: ["인력", "Labor"], G_material: ["재료", "Material"], G_cost: ["경비", "Expense"], @@ -46,9 +38,6 @@ const TEXT = { Col_Price: ["단가", "Price"], Col_Amount: ["금액", "Amount"], Empty_Group: ["줄 없음", "No lines"], - Not_Money: ["호표가 없는 로직 — 결과 식", "No unit-cost lines — result formula"], - Calc_First: ["금액은 셈이 끝나면 채워짐", "Amounts fill when the calculation finishes"], - Click_Hint: ["줄을 누르면 그 줄이 쓰는 자료가 뜸", "Click a line to see the data it uses"], Modal_Close: ["닫기", "Close"], Pill_Tone: ["요소 연결", "Linked element"], Modal_Sum: ["한 줄 풀이", "In one line"], diff --git a/M01_MasterData/M01_MasterData_UI_LogicLab_View.ts b/M01_MasterData/M01_MasterData_UI_LogicLab_View.ts deleted file mode 100644 index 9dabc3291..000000000 --- a/M01_MasterData/M01_MasterData_UI_LogicLab_View.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* ============================================================================= - * M01_MasterData_UI_LogicLab_View.ts - * 「단가산출 로직」 상세 보기 이름표 — 보기 = 수식(줄 쌓기 `A00_Common/line_stack_screen.ts`) 하나 · 고르기 단추 없음(836-3) - * 그릇 `data-view="stack"` 고정 — `data-block="ho"` 덩이(단가산출 상세 표)는 CSS 로 숨김 - * 덩이 = `data-block` 이름표 · 숨김 = 그릇 `data-view` + CSS(`LogicLab_Style.css`) — 다시 그려도 고름 유지 - * 셈 구동은 화면 칸 없이 `_Logic_Drive.ts` · 기본정보는 이름표 없음(늘 맨 위) - * ========================================================================== */ - -export type LabView = "stack" | "ho"; - -/** 덩이에 보기 이름표를 붙여 돌려줌 */ -export const block = (node: T, view: LabView): T => { - node.dataset.block = view; - return node; -}; diff --git a/M01_MasterData/M01_MasterData_UI_Logic_Drive.ts b/M01_MasterData/M01_MasterData_UI_Logic_Drive.ts index de932e8bb..80d7d5255 100644 --- a/M01_MasterData/M01_MasterData_UI_Logic_Drive.ts +++ b/M01_MasterData/M01_MasterData_UI_Logic_Drive.ts @@ -1,12 +1,10 @@ /* ============================================================================= * M01_MasterData_UI_Logic_Drive.ts - * 셈 구동(PLAN 799-2) — 화면 없이 「입력값 → 서버 셈 → 답」을 돌림. 시험 계산 칸(`Logic_Calc`)과 로직 상세(`LogicLab`)가 같이 씀. - * `driveRun` = 한 번 셈(`/calc` + `/text`) · `previewText` = 열 때 읽는 식 미리보기 · `autoFill` = 자동값 채움 - * `createDrive` = 로직 상세용 묶음 — 열 때 미리보기 + 첫 셈(입력 없으면 바로 · 있으면 자동값 채운 뒤) · `run` 은 칸 없이 다시 셈 + * 셈 구동(PLAN 799-2) — 화면 없이 「입력값 → 서버 셈 → 답」을 돌림. 시험 계산 칸(`Logic_Calc`)이 씀 + * `driveRun` = 한 번 셈(`/calc` + `/text`) · `autoFill` = 자동값 채움 — 로직 상세 구동은 840-1 에 걷음 * 셈 자리 = 서버(CLAUDE.md 5 장 ③) · 화면 · sessionStorage 없음 · 고른 재료(`CalcPick`) 는 값 객체에 딸림 * ========================================================================== */ -import { showToast } from "@ui/ui_template_elements"; import { fetchAuto, runCalc, @@ -16,8 +14,7 @@ import { type LogicRow, type TextAnswer, } from "./M01_MasterData_UI_Logic_Api"; -import { isMaterialKeyInput, pickBody } from "./M01_MasterData_UI_Logic_CalcPick"; -import { tx } from "./M01_MasterData_UI_Logic_Text"; +import { pickBody } from "./M01_MasterData_UI_Logic_CalcPick"; export interface DriveContext { /** 저장된 키 — 새 로직은 null */ @@ -67,17 +64,6 @@ export async function driveRun( return { answer, draft }; } -/** 열 때 읽는 식 미리보기 — [계산] 전에도 이름 식 칸이 비치게 · 늦게 온 응답은 `live()` 로 거름 */ -export function previewText(ctx: DriveContext, live: () => boolean): void { - if (!ctx.onText) return; - const body = { key: ctx.savedKey ?? ctx.row.키, inputs: {}, ...draftBody(ctx) }; - void runText(body).then((answer) => { - if (!live()) return; - (answer as TextAnswer & { 계산됨?: boolean }).계산됨 = false; - ctx.onText?.(answer); - }); -} - /** 자동값으로 빈 칸 채움 — 채웠으면 true */ export async function autoFill(ctx: DriveContext): Promise { if (ctx.savedKey === null) return false; @@ -88,49 +74,3 @@ export async function autoFill(ctx: DriveContext): Promise { for (const n of names) ctx.values[n] = String(auto[n]); return names.length > 0; } - -/** 로직마다 첫 셈을 한 번만(다시 그려도 안 부름) */ -const first = new WeakSet>(); - -/** 로직 상세용 구동 — `start` = 로직을 열거나 입력 칸이 바뀔 때 · `run` = 지금 값으로 다시 셈 */ -export function createDrive(): { - start: (ctx: DriveContext) => void; - run: () => Promise; - stop: () => void; -} { - let seq = 0; // 로직을 바꾸면 늦게 온 응답을 거름 - let now: DriveContext | null = null; - let ran = false; // 셈 답이 이미 왔으면 뒤늦은 미리보기로 안 덮음 - const run = async (): Promise => { - const ctx = now; - if (!ctx) return; - ran = true; - try { - await driveRun(ctx); - } catch (error) { - showToast(error instanceof Error ? error.message : tx("Load_Failed"), "error"); - } - }; - const start = (ctx: DriveContext): void => { - now = ctx; - ran = false; - const mine = ++seq; - previewText(ctx, () => !ran && mine === seq); - if (first.has(ctx.values)) return; - if (!(ctx.row.입력 ?? []).some((spec) => !isMaterialKeyInput(spec))) { - first.add(ctx.values); // 입력이 없으면 채울 것이 없음 — 열 때 한 번 바로 셈 - queueMicrotask(() => mine === seq && void run()); - } else if (ctx.savedKey !== null) { - first.add(ctx.values); - void autoFill(ctx).then((filled) => filled && mine === seq && void run()); - } - }; - return { - start, - run, - stop: () => { - now = null; - seq++; - }, - }; -} diff --git a/M02_MasterTemplete/M02_MasterTemplete_Sangeun_Detail.ts b/M02_MasterTemplete/M02_MasterTemplete_Sangeun_Detail.ts index fb7a2c50a..8dd2e7d24 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Sangeun_Detail.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Sangeun_Detail.ts @@ -20,7 +20,6 @@ import { el } from "@ui/ui_template_elements"; import { createInputField } from "@ui/ui_template_input"; import { t as L } from "@ui/ui_template_locale"; import { createText } from "@ui/ui_template_text"; -import { swapUnit } from "../A00_Common/master_unit_swap"; import { fetchLogic, setLogicApiPaths, @@ -31,12 +30,10 @@ import { type TextAnswer, } from "../M01_MasterData/M01_MasterData_UI_Logic_Api"; import { buildLabDetail } from "../M01_MasterData/M01_MasterData_UI_LogicLab_Detail"; -import { buildHo, lineOpener, type HoEdit } from "../M01_MasterData/M01_MasterData_UI_LogicLab_Ho"; +import { buildHo, type HoEdit } from "../M01_MasterData/M01_MasterData_UI_LogicLab_Ho"; import { readTemplate, type Layer } from "./M02_MasterTemplete_Api_Fetch"; import { baseHeads } from "./M02_MasterTemplete_Sangeun_BaseHead"; import { calcSangeun, removedStrip } from "./M02_MasterTemplete_Sangeun_Cover"; -import { openSangeunLinePicker } from "./M02_MasterTemplete_Sangeun_Pick"; -import { openRateLine } from "./M02_MasterTemplete_Sangeun_Rate"; import { createSangeunTabs } from "./M02_MasterTemplete_Sangeun_Tabs"; import { tl } from "../M01_MasterData/M01_MasterData_UI_LogicLab_Text"; import { createSangeunSide, tabOf, type SourceTab } from "./M02_MasterTemplete_Sangeun_Side"; @@ -57,7 +54,7 @@ import { type SkAnswer, type SkDoc, } from "./logic/M02_Sangeun_Cover_Model"; -import { resetQty, setQty, withFirst } from "./logic/M02_Sangeun_Undo"; +import { resetQty, setQty } from "./logic/M02_Sangeun_Undo"; export interface SangeunDetailOptions { layer: Layer; @@ -138,74 +135,13 @@ export function mountSangeunDetail( } } const ownHost = el("div"); - const addButton = createButton({ - label: L("M02_SangeunAddLine"), - variant: "ghost", - size: 28, - onClick: () => - openSangeunLinePicker(scope, (line) => { - if ("종류" in line && line.종류 === "로직" && !doc.바탕) return setBase(line.로직); - if ("종류" in line && line.종류 === "로직") return void addBase(line); - doc.더한줄 = [ - ...(doc.더한줄 ?? []), - withFirst(line as unknown as NonNullable[number]), - ]; - touch(); - }), - }); - addButton.hidden = !edit || !!opt.source; - // 778-4 율 줄 — 기준 줄 후보 = 지금 셈 답 줄(세부 줄 뺌) · 새 줄은 맨 끝(앞 줄만 가리킴) - const rateButton = createButton({ - label: L("M02_SkRateAdd"), - variant: "ghost", - size: 28, - onClick: () => - openRateLine( - (ans?.줄 ?? []).map((l) => ({ cid: l.cid, 이름: l.이름 })), - (line) => ((doc.더한줄 = [...(doc.더한줄 ?? []), withFirst(line)]), touch()), - ), - }); - rateButton.hidden = !edit || !!opt.source; - /** 둘째 기초(728-2) — 수량 처음값 = 바탕 기준단위 1 개 = 그 기초 기준단위 몇 개(같은 성격 · 다르면 1) */ - async function addBase(line: { 로직: string; 수량: number }): Promise { - let qty = 1; - const have = base ? baseUnitOf(base.logic.결과단위) : ""; - try { - const swap = have - ? swapUnit(have, baseUnitOf((await fetchLogic(line.로직)).logic.결과단위)) - : null; - if (swap?.배 != null && swap.계수 === null) qty = swap.배; - } catch { - /* 못 읽음 = 1 그대로 */ - } - if (!alive) return; - doc.더한줄 = [ - ...(doc.더한줄 ?? []), - withFirst({ ...line, 수량: qty } as unknown as NonNullable[number]), - ]; - touch(); - } - /** 첫 기초 = 바탕(옛 [기본 로직 선택] 동작 · 단위는 `loadBase` 가 기초 결과단위로) */ - function setBase(key: string): void { - doc.바탕 = { 로직: key }; - delete doc.바탕판; // 새 바탕 — 개정 견줄 판 없음 - touch(false); - void loadBase(true); - } const main = el("div", { className: "m02-sk", attrs: { "data-sangeun-detail": doc.키 ?? "" }, children: [ head, notes, - ...(opt.slot - ? [] - : [ - el("div", { - className: "m02-sk__head", - children: [tabs.tools, addButton, rateButton], - }), - ]), + ...(opt.slot ? [] : [el("div", { className: "m02-sk__head", children: [tabs.tools] })]), tabs.host, editor, ], @@ -420,15 +356,9 @@ export function mountSangeunDetail( row: base?.logic ?? row, file: base?.file ?? "", reasons: base?.reasons ?? [], - prices: base?.prices ?? {}, - lines: null, - text, - lastText, - values, ownHost, onChange: () => undefined, - onInput, - cover: { ho, open: lineOpener(hoCtx) }, + cover: { ho }, }); // prettier-ignore side?.set([ diff --git a/M02_MasterTemplete/M02_MasterTemplete_Sangeun_Pick.ts b/M02_MasterTemplete/M02_MasterTemplete_Sangeun_Pick.ts index 0f925f56b..bf5a8920d 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Sangeun_Pick.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Sangeun_Pick.ts @@ -4,13 +4,10 @@ * · `listLayerDocs` = 층 목록 길(`/layers/{층}/templates` · 종류 하나)을 마스터 › 회사 › 개인 차례로 이음 · * 금액 = 서버가 붙인 1 단위 `기준금액` · `listSangeun` = 종류 `sangeun` · 찾아 넣기 창(`Find_Pick`) 탭 · B09 재료 창고 출처가 씀 * · 고른 SK = 참조 `{층, 주인, 키: SK-…}` — 일위대가 줄은 옛 참조 줄 꼴(서버 `M02_Labor_Expand` 가 펼침) - * · `openSangeunLinePicker` = 단가산출 [항목 추가] — 찾아 넣기 창(721-2 · M01 로직 · 인력 · 재료 · 단가산출 · 일위대가) - * → SK 문서 `더한줄` 한 줄(PLAN 715 이음 꼴 · 인력 · 재료 = 요소 키 LB · MT · 수량 1) * ========================================================================== */ -import { call, type Layer } from "./M02_MasterTemplete_Api_Fetch"; -import { fixedOf, openFindPicker } from "./M02_MasterTemplete_Find_Pick"; -import type { LaborFixed, LaborRef } from "./M02_MasterTemplete_Labor_Calc"; +import { call } from "./M02_MasterTemplete_Api_Fetch"; +import type { LaborRef } from "./M02_MasterTemplete_Labor_Calc"; export const SANGEUN_KIND = "sangeun"; /** 층 차례 — 위에서 아래 */ @@ -68,29 +65,3 @@ export async function listLayerDocs( ); return lists.flat(); } - -/** 단가산출 더한줄 한 줄(PLAN 715 이음 꼴) */ -export type SangeunLine = - | { 종류: "로직"; 로직: string; 입력값: Record; 수량: number } - | { 참조: LaborRef; 수량: number } - | { 종류: "인력" | "재료"; 요소: string; 이름: string | null; 단위: string | null; 수량: number } - | { 고정: LaborFixed; 이름: string; 단위: string | null; 수량: number }; - -/** 단가산출 [항목 추가] — `scope` = 그 SK 문서 층(고를 수 있는 서고 = 위 층만) */ -export function openSangeunLinePicker( - scope: { layer: Layer; owner: string | null }, - done: (line: SangeunLine) => void, -): void { - openFindPicker({ - ...scope, - logic: (x) => done({ 종류: "로직", 로직: x.키, 입력값: {}, 수량: 1 }), - ref: (ref) => done({ 참조: ref, 수량: 1 }), - element: (e) => - (e.종류 === "인력" || e.종류 === "재료") && - done({ 종류: e.종류, 요소: e.코드, 이름: e.이름, 단위: e.단위, 수량: 1 }), - // 785-3 유가전력 = 더한줄 재료 요소 MO - oil: (e) => done({ 종류: "재료", 요소: e.코드, 이름: e.이름, 단위: e.단위, 수량: 1 }), - // 736-3 기계 = 고정 줄(이음 기계 EQ · 단가 = 한 시간 노 · 재 · 경) · 수량 1 - machine: (e) => done({ 고정: fixedOf(e), 이름: e.이름, 단위: e.단위, 수량: 1 }), - }); -} diff --git a/M02_MasterTemplete/M02_MasterTemplete_Sangeun_Rate.ts b/M02_MasterTemplete/M02_MasterTemplete_Sangeun_Rate.ts deleted file mode 100644 index cfcf7e580..000000000 --- a/M02_MasterTemplete/M02_MasterTemplete_Sangeun_Rate.ts +++ /dev/null @@ -1,103 +0,0 @@ -/* ============================================================================= - * M02_MasterTemplete_Sangeun_Rate.ts - * M02 단가산출(SK) 율 줄 넣기 창(PLAN 778-4 · 요구 623) — [율 줄 추가] → 이름 · 율 % · 기준 · 금액이 드는 칸. - * 기준 = 묶음(인력 · 재료 · 장비 줄 합) · 비목 전체(노무비 · 재료비 · 경비) · 줄 고르기(이 SK 줄 cid 목록 · - * 앞 율 줄 고르면 율 위 율) → 더한줄 `{고정: {이름, 단위 %, 단가 0, 이음 없음, 비율 {기준, 비목}}, 수량 = 율}` - * (꼴 = 서버 `M02_Sangeun_Doc` · 셈 = 서버 `M02_Sangeun_Calc._rated`) · 부품 상자만. - * ========================================================================== */ - -import { createButton } from "@ui/ui_template_button"; -import { createCheckbox } from "@ui/ui_template_checkbox"; -import { el } from "@ui/ui_template_elements"; -import { createFormGrid } from "@ui/ui_template_general_layout"; -import { createInputField, createSelectField } from "@ui/ui_template_input"; -import { t as L } from "@ui/ui_template_locale"; -import { openModal } from "@ui/ui_template_modal"; -import type { SkAdded } from "./logic/M02_Sangeun_Cover_Model"; - -const LINES = "줄"; - -/** 율 줄 넣기 — `rows` = 지금 셈 답 줄(기준 줄 고르기 후보) · `done` = 새 더한줄 한 줄 */ -export function openRateLine( - rows: { cid: string; 이름: string }[], - done: (line: SkAdded) => void, -): void { - const name = createInputField({ label: L("M02_SkRateName"), value: L("M02_SkRateDefault") }); - const rate = createInputField({ - label: L("M02_SkRateRate"), - type: "number", - value: "2", - unit: "%", - min: 0, - }); - const picks = rows.map((r) => ({ - cid: r.cid, - box: createCheckbox(r.이름 || r.cid, { chip: true, size: 28 }), - })); - const list = el("div", { - className: "m02-sk__tags", - attrs: { "data-sk-rate-lines": "" }, - children: picks.length - ? picks.map((p) => p.box.root) - : [el("span", { text: L("M02_SkRateNoLines") })], - }); - list.hidden = true; - const basis = createSelectField({ - label: L("M02_SkRateBasis"), - value: "인력", - options: [ - { value: "인력", text: L("M02_SkRateGroupLabor") }, - { value: "재료", text: L("M02_SkRateGroupMat") }, - { value: "장비", text: L("M02_SkRateGroupMach") }, - { value: "노무비", text: L("M02_SkRateCostLabor") }, - { value: "재료비", text: L("M02_SkRateCostMat") }, - { value: "경비", text: L("M02_SkRateCostExp") }, - { value: LINES, text: L("M02_SkRateBasisLines") }, - ], - onChange: (v) => (list.hidden = v !== LINES), - }); - const part = createSelectField({ - label: L("M02_SkRatePart"), - value: "경비", - options: [ - { value: "경비", text: L("M02_SkRatePartExp") }, - { value: "재료비", text: L("M02_SkRatePartMat") }, - { value: "노무비", text: L("M02_SkRatePartLabor") }, - ], - }); - openModal({ - title: L("M02_SkRateTitle"), - closeLabel: L("M02_SkRateClose"), - width: "40rem", - mount: (body, close, actions) => { - const ok = createButton({ - label: L("M02_SkRateOk"), - variant: "filled", - onClick: () => { - const label = name.input.value.trim(); - const q = Number(rate.input.value); - const keys = picks.filter((p) => p.box.input.checked).map((p) => p.cid); - const lines = basis.select.value === LINES; - name.setError(label ? undefined : L("M02_SkRateNeedName")); - rate.setError(rate.input.value.trim() && q >= 0 ? undefined : L("M02_SkRateBadRate")); - if (lines && !keys.length) return void rate.setError(L("M02_SkRateNeedLine")); - if (!label || !rate.input.value.trim() || !(q >= 0)) return; - const fx = { - 이름: label, - 단위: "%", - 단가: { 노무비: 0, 재료비: 0, 경비: 0 }, - 이음: { 노임: null, 자재: null, 기계: null }, - 비율: { 기준: lines ? keys : basis.select.value, 비목: part.select.value }, - }; - done({ 고정: fx, 이름: label, 단위: "%", 수량: q }); - close(); - }, - }); - ok.dataset.skRateOk = ""; - actions.append(ok); - const grid = createFormGrid([name.root, rate.root, basis.root, part.root]); - grid.dataset.skRate = ""; - body.append(grid, list); - }, - }); -} diff --git a/M02_MasterTemplete/M02_MasterTemplete_Sangeun_Tabs.ts b/M02_MasterTemplete/M02_MasterTemplete_Sangeun_Tabs.ts index 0875e1fb0..75df34578 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Sangeun_Tabs.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Sangeun_Tabs.ts @@ -63,7 +63,6 @@ export function createSangeunTabs( const show = (doc: boolean): void => { stack.root.hidden = tools.hidden = !doc; editor.hidden = doc; - if (!doc) editor.dataset.view = "ho"; }; show(true); return { diff --git a/resources/tester/test_717_2b_sangeun_detail.py b/resources/tester/test_717_2b_sangeun_detail.py index 72df8a4be..d4b2807db 100644 --- a/resources/tester/test_717_2b_sangeun_detail.py +++ b/resources/tester/test_717_2b_sangeun_detail.py @@ -165,9 +165,11 @@ def test_상세_부품_길_부품상자(): cover = _text(M02 / "M02_MasterTemplete_Sangeun_Cover.ts") assert "mountSangeunDetail(host, doc," in view and "createEmpty" not in view assert 'read: "/m02/logic-lib"' in detail and "setLogicApiPaths(LIB)" in detail - assert "buildLabDetail(editor" in detail and "cover: { ho, open: lineOpener(hoCtx) }" in detail + assert "buildLabDetail(editor" in detail and "cover: { ho }," in detail assert "buildCalc" not in detail # 799-2 시험 계산 보기 걷음 - assert "openSangeunLinePicker(scope" in detail + assert ( + "openSangeunLinePicker" not in detail and "openRateLine" not in detail + ) # 840-1 [항목 추가] · [율 줄 추가] 걷음 assert "if (mine !== seq || !alive) return" in detail # 최신 응답만 # 724-1 — 단가산출 상세 = 공용 상세(Ho) · 고침 고리 = SK 덮개 · 옛 고침 표 걷음 assert "...buildHo(hoCtx)" in detail and 'scope: "m02",' in detail diff --git a/resources/tester/test_717_4_sangeun_pick.py b/resources/tester/test_717_4_sangeun_pick.py index 2d0021bd3..445a1933c 100644 --- a/resources/tester/test_717_4_sangeun_pick.py +++ b/resources/tester/test_717_4_sangeun_pick.py @@ -38,10 +38,9 @@ def test_sk_list_layer_order_and_ref_shape(): ) # 일위대가 [로직 추가] → 찾아 넣기 창(785-7 `withSangeun`) store_ts = _text(ROOT / "B09_Estimation" / "B09_Estimation_UI_Store.ts") assert '["sangeun", "단가산출"],' in store_ts and "sangeun: sangeunItems," in store_ts - # 더한줄 고르기 — 715 이음 꼴 셋(인력 · 재료 = 요소 키) - assert 'logic: (x) => done({ 종류: "로직", 로직: x.키, 입력값: {}, 수량: 1 })' in pick - assert "ref: (ref) => done({ 참조: ref, 수량: 1 })" in pick - assert "done({ 종류: e.종류, 요소: e.코드, 이름: e.이름, 단위: e.단위, 수량: 1 })" in pick + assert ( + "openSangeunLinePicker" not in pick + ) # 840-1 단가산출 [항목 추가] 걷음 — 줄 더하기 = 수식 [+] def _sk(folder, name): diff --git a/resources/tester/test_721_find_pick.py b/resources/tester/test_721_find_pick.py index 43c00c285..bdec67e96 100644 --- a/resources/tester/test_721_find_pick.py +++ b/resources/tester/test_721_find_pick.py @@ -27,12 +27,12 @@ def _text(path: Path) -> str: def test_721_1_단추_이름과_자리(): detail = _text(M02 / "M02_MasterTemplete_Sangeun_Detail.ts") assert ( - "children: [tabs.picker, tabs.tools, addButton, rateButton]" in detail - ) # 750-2 안내 걷음 · 778-4 율 줄 추가 · 799-2 보기 · 808-1 [단가 갱신] + "addButton" not in detail and "rateButton" not in detail + ) # 840-1 [항목 추가] · [율 줄 추가] 걷음 assert "M02_SangeunPickBase" not in detail # 727-2 단추 걷음 locale = _text(ROOT / "ui_template" / "ui_template_locale_m2.ts") assert "M02_SangeunPickBase" not in locale - assert 'M02_SangeunAddLine: ["항목 추가"' in locale + assert "M02_SangeunAddLine" not in locale and "M02_SkRateAdd" not in locale def test_721_2_부르는_곳_셋만_새_창(): @@ -43,7 +43,7 @@ def test_721_2_부르는_곳_셋만_새_창(): assert "findSangeun" not in labor_pick # 옛 창의 단가산출 탭 걷음 assert "logicOnly" not in _text(M02 / "M02_MasterTemplete_Sangeun_Detail.ts") # 727-2 assert "logicOnly" not in find - assert "openFindPicker({" in _text(M02 / "M02_MasterTemplete_Sangeun_Pick.ts") + assert "openFindPicker" not in _text(M02 / "M02_MasterTemplete_Sangeun_Pick.ts") # 840-1 assert "sangeun: true }" in labor_pick # 일위대가(785-7 `withSangeun`) # 옛 창 그대로 — B09 단가 연결은 sangeun 없이 부름(736-1 불러오기 · 별칭 장부는 찾아 넣기 창으로) for f in ( @@ -171,6 +171,3 @@ def test_736_3_기계_고정_줄_금액(): and "이음: { 노임: null, 자재: null, 기계: e.코드 }" in find ) assert "machine: fixed &&" in _text(M02 / "M02_MasterTemplete_Labor_Pick.ts") - assert "machine: (e) => done({ 고정: fixedOf(e)" in _text( - M02 / "M02_MasterTemplete_Sangeun_Pick.ts" - ) diff --git a/resources/tester/test_722_ho_detail.py b/resources/tester/test_722_ho_detail.py index b24a171d7..0b9a26a18 100644 --- a/resources/tester/test_722_ho_detail.py +++ b/resources/tester/test_722_ho_detail.py @@ -38,7 +38,12 @@ def _node(code: str): def test_상세가_공용_부품을_씀(): detail = _text(M01 / "M01_MasterData_UI_LogicLab_Detail.ts") - assert "buildHo({ ...ctx, edit: trialEdit(ctx) })" in detail + assert ( + "buildHo" not in detail and "trialEdit" not in detail + ) # 840-1 M01 상세 보기 걷음 — M02 줄 모달만 씀 + assert "...buildHo(hoCtx)" in _text( + ROOT / "M02_MasterTemplete" / "M02_MasterTemplete_Sangeun_Detail.ts" + ) ho = _text(HO) # 부품 상자만 — 표 · 카드 · 계산 줄 · 변수 칸(calc_sheet) · 입력 칩(텍스트 수식) for part in ( @@ -60,13 +65,13 @@ def test_상세가_공용_부품을_씀(): ) -def test_시험_고침은_캐시_밖(): +def test_시험_고침은_M01_에서_걷음(): lab = _text(M01 / "M01_MasterData_UI_LogicLab.ts") - # 시험 계산에만 덮은 사본 · 고침이 있으면 줄을 보냄 · drafts(캐시)에는 원본 줄 - assert "=> trial.apply(row);" in lab - assert "dirty: () => current.id in drafts || trial.has(current.row)," in lab - assert "onTrial: drawCalc," in lab and "rerun: () => void drive.run()," in lab # 799-2 셈 구동 + assert ( + "trial" not in lab and "drive" not in lab + ) # 840-1 — 시험 값 · 구동 걷음(수식에서 값을 바꾸고 [되돌림]) cover = _text(COVER) + assert "export const trial" not in cover and "applyCovers" not in cover assert "drafts" not in cover and "touch" not in cover and "/save" not in cover modal = _text(M01 / "M01_MasterData_UI_LogicLab_Modal.ts") assert "...(target.edit ? [target.edit(close, head)] : [])" in modal @@ -91,23 +96,3 @@ console.log(JSON.stringify(got)); assert got[3]["줄"] == [[2, 5, 2]] empty = _node(_pure(HO) + '\nconsole.log(JSON.stringify(boxesOf([], ["인력"])))') assert empty == [{"묶음": "인력", "줄": [], "소계": 0}] - - -@pytest.mark.skipif(shutil.which("node") is None, reason="node 가 없음") -def test_고침_덮기(): - body = """ -const row = { 키: "GF1", 호표: [ - { 종류: "인력", 요소: "LB1", 수량: "찾기(Q).수량" }, - { 종류: "로직", 요소: "로직(GC1)", 수량: "1 / 작업량", 값묶음: { 경비: 3 } }, - { 종류: "재료", 요소: "MT1", 수량: "2" } ], 덧줄: [{ 이름: "공구", 식: "노무비 * 3 / 100" }] }; -const out = applyCovers(row, { 0: { qty: 0.0000001, labor: 100000 }, 1: { labor: 50000 }, 9: { qty: 1 } }); -console.log(JSON.stringify({ out, same: row.호표[0].수량, keep: out.호표[2] === row.호표[2] })); -""" - got = _node(_pure(COVER) + "\n" + body) - out = got["out"] - assert ( - out["호표"][0]["수량"] == "0.0000001" and out["호표"][0]["단가"] == 100000 - ) # 지수 꼴 없음 · 손 단가 - assert out["호표"][1]["값묶음"] == {"경비": 3, "노무비": 50000} and "단가" not in out["호표"][1] - assert got["keep"] and got["same"] == "찾기(Q).수량" # 원본 안 바뀜 · 안 고친 줄 그대로 - assert out["덧줄"] == [{"이름": "공구", "식": "노무비 * 3 / 100"}] diff --git a/resources/tester/test_728_2_sk_unit.py b/resources/tester/test_728_2_sk_unit.py index bbf694329..826b3f7d7 100644 --- a/resources/tester/test_728_2_sk_unit.py +++ b/resources/tester/test_728_2_sk_unit.py @@ -90,7 +90,7 @@ def test_둘째_기초_수량_처음값_배(whole): src = (ROOT / "M02_MasterTemplete/M02_MasterTemplete_Sangeun_Detail.ts").read_text( encoding="utf-8" ) - assert "swapUnit(" in src and "addBase(" in src and "baseUnitOf(" in src + assert "addBase(" not in src and "baseUnitOf(" in src # 840-1 [항목 추가] 걷음 def test_돈_아닌_기초_고르기_창에서_뺌(whole): diff --git a/resources/tester/test_799_2_old_tabs_gone.py b/resources/tester/test_799_2_old_tabs_gone.py index 671be4d3c..175583b17 100644 --- a/resources/tester/test_799_2_old_tabs_gone.py +++ b/resources/tester/test_799_2_old_tabs_gone.py @@ -57,27 +57,23 @@ def test_걷은_파일_없고_부르는_곳_없음(): def test_보기는_수식_하나_고르기_없음(): - m01 = _text(M01 / "M01_MasterData_UI_LogicLab_View.ts") m02 = _text(M02 / "M02_MasterTemplete_Sangeun_Tabs.ts") - for src in (m01, m02): - assert not re.findall(r'\{ value: "(\w+)", label: ', src) and "createSegment" not in src - assert "createViewPicker" not in _text(M01 / "M01_MasterData_UI_LogicLab.ts") # 836-3 - assert 'editor.dataset.view = "stack";' in _text(M01 / "M01_MasterData_UI_LogicLab.ts") - assert 'export type LabView = "stack" | "ho";' in m01 + assert not re.findall(r'\{ value: "(\w+)", label: ', m02) and "createSegment" not in m02 + assert not (M01 / "M01_MasterData_UI_LogicLab_View.ts").exists() # 840-1 보기 이름표 걷음 + lab = _text(M01 / "M01_MasterData_UI_LogicLab.ts") + assert "createViewPicker" not in lab and "dataset.view" not in lab # 836-3 · 840-1 css = _text(M01 / "M01_MasterData_UI_LogicLab_Style.css") - assert '[data-view="ho"] [data-block]:not([data-block="ho"])' in css - assert 'data-view="sheet"' not in css and 'data-view="calc"' not in css + assert "data-view" not in css and "data-block" not in css # 840-1 보기 고르기 규칙 걷음 def test_셈_구동은_화면_없이_상세는_구동만(): drive = _text(M01 / "M01_MasterData_UI_Logic_Drive.ts") assert "document." not in drive and "sessionStorage." not in drive - assert "export async function driveRun(" in drive and "export function createDrive(" in drive + assert "export async function driveRun(" in drive and "createDrive" not in drive # 840-1 lab = _text(M01 / "M01_MasterData_UI_LogicLab.ts") - assert "createDrive()" in lab and "buildCalc" not in lab and "data-calc-run" not in lab - assert "rerun: () => void drive.run()," in lab + assert "createDrive" not in lab and "buildCalc" not in lab and "data-calc-run" not in lab detail = _text(M01 / "M01_MasterData_UI_LogicLab_Detail.ts") - assert "calcHost" not in detail and "data-calc-run" not in detail and "ctx.rerun?.()" in detail + assert "calcHost" not in detail and "data-calc-run" not in detail and "rerun" not in detail calc = _text( M01 / "M01_MasterData_UI_Logic_Calc.ts" ) # 로직 복사 화면이 쓰는 시험 계산 칸 — 구동은 공용 diff --git a/resources/tester/test_804_2_line_stack.py b/resources/tester/test_804_2_line_stack.py index 67385f733..e9b159991 100644 --- a/resources/tester/test_804_2_line_stack.py +++ b/resources/tester/test_804_2_line_stack.py @@ -240,8 +240,8 @@ def test_화면_이음(): 'base: "/m01/logic/doc",' in lab and 'cache: "m01_doc_drafts",' in lab ) # 804-7 page = 페이지 [저장] assert "stack: stack.root" in lab and "=> stack.canLeave();" in lab # 804-6 수식 탭 = 줄 쌓기 - assert 'editor.dataset.view = "stack";' in lab # 836-3 보기 고르기 없음 · 수식 하나 - assert '[data-view="stack"]' in _src("M01_MasterData/M01_MasterData_UI_LogicLab_Style.css") + assert "dataset.view" not in lab # 836-3 보기 고르기 없음 · 840-1 이름표 · CSS 걷음 + assert "data-view" not in _src("M01_MasterData/M01_MasterData_UI_LogicLab_Style.css") tabs = _src("M02_MasterTemplete/M02_MasterTemplete_Sangeun_Tabs.ts") assert 'cache: "m02_sk_doc_drafts"' in tabs and tabs.count("skDocPath(opt.scope, k, p)") == 1 vite = _src("vite.config.ts") diff --git a/resources/tester/test_804_6_formula_tab_gone.py b/resources/tester/test_804_6_formula_tab_gone.py index 032cbaecd..ae42fde34 100644 --- a/resources/tester/test_804_6_formula_tab_gone.py +++ b/resources/tester/test_804_6_formula_tab_gone.py @@ -51,10 +51,7 @@ def test_걷은_파일_없고_부르는_곳_없음(): def test_보기_수식은_줄쌓기_첫탭(): - for rel in ( - "M01_MasterData/M01_MasterData_UI_LogicLab_View.ts", - "M02_MasterTemplete/M02_MasterTemplete_Sangeun_Tabs.ts", - ): + for rel in ("M02_MasterTemplete/M02_MasterTemplete_Sangeun_Tabs.ts",): src = _text(rel) assert "createSegment" not in src and '"equation"' not in src, rel # 836-3 고르기 없음 lab = _text("M01_MasterData/M01_MasterData_UI_LogicLab.ts") diff --git a/resources/tester/test_804_7_line_stack_fix.py b/resources/tester/test_804_7_line_stack_fix.py index 573105ddd..ae1dc5be3 100644 --- a/resources/tester/test_804_7_line_stack_fix.py +++ b/resources/tester/test_804_7_line_stack_fix.py @@ -209,7 +209,10 @@ def test_시험_페이지_걷음_화면_차림() -> None: def test_M02_수식탭_M01과_같은_단추_저장_한_번() -> None: """804-8 — M02 단가산출 수식 탭 = M01 과 같은 공용 화면 · 페이지 [되돌림] · [저장] 한 번 · 머리 줄 한 줄.""" screen = _src("A00_Common/line_stack_screen.ts") - assert "...(cp && edit() && key && !mark?.자동 ? copyTools(true, (job) => void runJob(job)) : [])," in screen # 836-2 자동 층 = 단추 없음 · 프로젝트 층 = 있음 + assert ( + "...(cp && edit() && key && !mark?.자동 ? copyTools(true, (job) => void runJob(job)) : [])," + in screen + ) # 836-2 자동 층 = 단추 없음 · 프로젝트 층 = 있음 assert "notesOf" not in screen and "lstk__notes" not in screen # 못 고치는 칸 안내 줄 걷음 assert "Notes:" not in _src("A00_Common/line_stack_text.ts") tabs = _src("M02_MasterTemplete/M02_MasterTemplete_Sangeun_Tabs.ts") @@ -217,8 +220,9 @@ def test_M02_수식탭_M01과_같은_단추_저장_한_번() -> None: detail = _src("M02_MasterTemplete/M02_MasterTemplete_Sangeun_Detail.ts") assert "page: opt.source ? () => report() : undefined," in detail assert "JSON.stringify(doc) !== saved || tabs.dirty()" in detail # 고침 알림 = 기본 + 수식 - assert "addButton.hidden = !edit || !!opt.source;" in detail # [항목 추가] · [율 줄 추가] 걷음 - assert "rateButton.hidden = !edit || !!opt.source;" in detail + assert ( + "addButton" not in detail and "rateButton" not in detail + ) # 840-1 [항목 추가] · [율 줄 추가] 걷음 assert "if (!(await tabs.save())) throw" in detail and "await refreshing;" in detail assert "if (tabs.dirty()) void tabs.discard();" in detail # 버리고 떠남 = 수식 캐시도 view = _src("M02_MasterTemplete/M02_MasterTemplete_Sangeun_View.ts") diff --git a/resources/tester/test_806_1_labor_view.py b/resources/tester/test_806_1_labor_view.py index 9472964a1..ffe0f1dca 100644 --- a/resources/tester/test_806_1_labor_view.py +++ b/resources/tester/test_806_1_labor_view.py @@ -133,8 +133,10 @@ def test_808_1_모달_머리_일원화(): assert "actions: row.참조 ? [slot, ...tools] : tools," in row and 't("FromSk")' in row assert "slot: o.slot," in _src("M02_MasterTemplete_Labor_RowSk.ts") detail = _src("M02_MasterTemplete_Sangeun_Detail.ts") - assert "...(opt.slot\n ? []" in detail # 머리 칸 있으면 옛 단추 줄 없음 + assert "...(opt.slot ? [] : [" in detail # 머리 칸 있으면 머리 줄 없음 assert "children: [tabs.tools]" in detail and "tabs.picker" not in detail assert r"/^SK-(?!0{6})\d{6}$/" in detail tabs = _src("M02_MasterTemplete_Sangeun_Tabs.ts") - assert "const doc = !!k || !!opt.page || !!opt.memory?.();" in tabs # 836-3 · 836-5 문서 없는 줄 = 상세 몸만 + assert ( + "const doc = !!k || !!opt.page || !!opt.memory?.();" in tabs + ) # 836-3 · 836-5 문서 없는 줄 = 상세 몸만 diff --git a/ui_template/ui_template_locale_m2.ts b/ui_template/ui_template_locale_m2.ts index 05a6bfbc9..b02ce51ea 100644 --- a/ui_template/ui_template_locale_m2.ts +++ b/ui_template/ui_template_locale_m2.ts @@ -187,7 +187,6 @@ export const ui_locales_m2 = { M02_SangeunSwapCoef: ["환산 계수", "Conversion factor"], M02_SangeunSwapWeight: ["단위중량", "Unit weight"], M02_SangeunSwapPick: ["고르세요", "Choose"], - M02_SangeunAddLine: ["항목 추가", "Add item"], M02_SangeunDrop: ["삭제", "Delete"], M02_SangeunDropBase: ["기초 빼기", "Remove base"], M02_SangeunBases: ["더한 기초", "Added bases"], @@ -368,7 +367,6 @@ export const ui_locales_m2 = { M02_ToneDanger: ["못 이음", "Not linked"], M02_ToneMuted: ["밖", "Outside"], M02_ToneMine: ["내가 고름", "My pick"], - M02_SkRateAdd: ["율 줄 추가", "Add % line"], M02_SkRateTitle: ["율 줄 — 기준 × 율 %", "% line — base × rate %"], M02_SkRateName: ["이름", "Name"], M02_SkRateDefault: ["공구손료", "Tool cost"],