refactor(840-1): 상세 보기에만 있던 기능 넷 판정 · 죽은 코드 걷음 — 수식 보기가 대신하는 것 지움

- 판정 = 자체 로직 편집 · 시험 값 고침 · M02 줄 덮음 · 비목 박스 소계 모두 수식 보기로 됨(살릴 것 없음)
- M01 상세: 숨은 단가산출 상세 덩이 · 자체 로직 시트 편집기 · 시험 고침 · 셈 구동(createDrive) · 보기 이름표(View.ts) · data-view CSS 걷음
- M02 불러오기 모달 머리 [항목 추가] · [율 줄 추가] 단추와 그 창(Rate.ts · openSangeunLinePicker) 걷음
- 글 정리 = Info_Own 새 뜻 · 죽은 글 키 · No_Key · 항목 추가 글 걷음
- 낡은 시험 고침 = 722(시험 고침 · 덮기 셈) · 799-2 · 804-2 · 804-6 · 804-7 · 806-1 · 717-2b · 717-4 · 721 · 728-2 — 걷은 것이 없음 확인으로
- 남김 = M02 수식 문서 없는 줄의 단가산출 상세 몸(cover · buildHo)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-15 15:45:07 +09:00
co-authored by Claude Sonnet 5.5
parent 8bbdcb3a12
commit 39338d9e33
24 changed files with 72 additions and 593 deletions
-4
View File
@@ -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"],
+2 -78
View File
@@ -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<string, ElementBrief | null>;
reasons: string[];
lines: CalcLine[] | null;
text: TextAnswer | null;
/** 마지막으로 잘 받은 읽는 식 — 멈춘 뒤에도 수량 칸 조각을 이걸로 */
lastText?: TextAnswer;
ownHost: HTMLElement;
values: Record<string, string>;
}
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<LogicHandle> {
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<string, string>): 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<void> => {
@@ -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");
@@ -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<R extends { 호표?: object[] }>(
row: R,
covers: Record<number, LineCover>,
): R {
const ho = (row.호표 ?? []).map((raw, i) => {
const c = covers[i];
if (!c) return raw;
const item = raw as Record<string, unknown>;
const out: Record<string, unknown> = { ...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<object, Record<number, LineCover>>();
export const trial = {
of(row: object): Record<number, LineCover> {
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<R extends object | null>(row: R): R {
return row && trial.has(row) ? (applyCovers(row, trials.get(row)!) as R) : row;
},
};
const TEXT = {
Head: ["그 자리 고침 (시험 값)", "Edit here (trial value)"],
Note: [
@@ -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<typeof lineOpener> };
/** 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<HTMLInputElement>(`[data-own="${own}"]`) : kept?.node;
@@ -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<string, string>;
/** 박스 옵션 · 입력 칩 고침 — 시험 계산 입력 고침 + 다시 셈(없으면 고르기 · 칩 없음) */
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)
? {
@@ -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;
@@ -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"],
@@ -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 = <T extends HTMLElement>(node: T, view: LabView): T => {
node.dataset.block = view;
return node;
};
@@ -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<boolean> {
if (ctx.savedKey === null) return false;
@@ -88,49 +74,3 @@ export async function autoFill(ctx: DriveContext): Promise<boolean> {
for (const n of names) ctx.values[n] = String(auto[n]);
return names.length > 0;
}
/** 로직마다 첫 셈을 한 번만(다시 그려도 안 부름) */
const first = new WeakSet<Record<string, string>>();
/** 로직 상세용 구동 — `start` = 로직을 열거나 입력 칸이 바뀔 때 · `run` = 지금 값으로 다시 셈 */
export function createDrive(): {
start: (ctx: DriveContext) => void;
run: () => Promise<void>;
stop: () => void;
} {
let seq = 0; // 로직을 바꾸면 늦게 온 응답을 거름
let now: DriveContext | null = null;
let ran = false; // 셈 답이 이미 왔으면 뒤늦은 미리보기로 안 덮음
const run = async (): Promise<void> => {
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++;
},
};
}
@@ -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<SkDoc["더한줄"]>[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<void> {
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<SkDoc["더한줄"]>[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([
@@ -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<string, never>; 수량: 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 }),
});
}
@@ -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);
},
});
}
@@ -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 {
@@ -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
+3 -4
View File
@@ -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):
+4 -7
View File
@@ -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"
)
+11 -26
View File
@@ -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"}]
+1 -1
View File
@@ -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):
+8 -12
View File
@@ -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"
) # 로직 복사 화면이 쓰는 시험 계산 칸 — 구동은 공용
+2 -2
View File
@@ -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")
@@ -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")
@@ -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")
+4 -2
View File
@@ -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 문서 없는 줄 = 상세 몸만
-2
View File
@@ -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"],