feat(M01): 로직 개선 시험 상세 화면을 컨테이너 넷으로 · 호표를 인력·자재·경비 묶음과 소계로 · 줄을 누르면 쓰는 자료(표·단가) 모달

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WRFJmmhPi4exAzHgPZHMT
This commit is contained in:
2026-09-21 19:36:07 +09:00
co-authored by Claude Sonnet 5
parent 5b69324207
commit 4a93cfdf8c
6 changed files with 627 additions and 66 deletions
+6 -66
View File
@@ -19,23 +19,20 @@ import {
import {
ApiError,
fetchLogic,
fetchLogicFiles,
fetchLogics,
fetchLogicSubs,
saveFiles,
type CalcLine,
type ElementBrief,
type LogicFile,
type LogicRow,
type LogicSummary,
type SaveFile,
} from "./M01_MasterData_UI_Logic_Api";
import { buildCalc } from "./M01_MasterData_UI_Logic_Calc";
import { buildEditor } from "./M01_MasterData_UI_Logic_Edit";
import { buildList, logicId, type ListItem, type ListMark } from "./M01_MasterData_UI_Logic_List";
import { openPicker } from "./M01_MasterData_UI_Logic_Pick";
import type { SideHandle } from "./M01_MasterData_UI_Side";
import { tx } from "./M01_MasterData_UI_Logic_Text";
import { buildLabDetail } from "./M01_MasterData_UI_LogicLab_Detail";
import "./M01_MasterData_UI_Logic_Style.css";
/** 저장 안 한 로직 하나 — origKey null = 새 로직 · row null = 지움 */
@@ -80,12 +77,11 @@ export async function mountM01LogicLab(
openKey?: string,
): Promise<LogicHandle> {
let drafts = loadDrafts();
let files: LogicFile[] = [];
let opened: Opened | null = null;
let calcInputs = "";
const editor = el("div", { className: "m01-logic__editor" });
const errors = el("div", { className: "m01-logic__reasons", attrs: { hidden: "" } });
const calc = el("aside", { className: "m01-logic__calc" });
const calc = el("div", { className: "m01lab__calc" });
const filterHost = el("div");
const waiting = el("p", { className: "m01-logic__muted", text: tx("Loading") });
let loaded = false; // 첫 목록이 오기 전에는 「0 줄」 빈 표를 보이지 않음
@@ -168,7 +164,6 @@ export async function mountM01LogicLab(
list.root.hidden = !listing || !loaded;
waiting.hidden = loaded;
editor.hidden = back.hidden = listing;
calc.hidden = listing;
if (!opened) return;
if (!opened.row) {
editor.replaceChildren(
@@ -177,29 +172,18 @@ export async function mountM01LogicLab(
return;
}
const current = opened;
buildEditor(editor, {
buildLabDetail(editor, {
row: current.row as LogicRow,
file: current.file,
isNew: current.origKey === null,
files: files.map((f) => f.file),
reasons: current.reasons,
prices: current.prices,
lines: current.lines,
values: current.values,
onChange: touch,
onFile: (file) => {
const f = files.find((x) => x.file === file);
if (!f) return;
Object.assign(current, { file: f.file, sub: subOf(f), version: f.version });
touch();
},
onPick: openPicker,
calcHost: calc,
});
};
/** 로직 파일 → 구분(원문 + 부문) — 「공통 03장 토공사」 의 앞말이 부문 */
const subOf = (f: LogicFile): string =>
f.book === "건설품셈" ? `${f.book} ${f.chapter.split(" ")[0]}` : String(f.book);
const show = (next: Opened | null): void => {
opened = next;
errors.hidden = true;
@@ -236,12 +220,7 @@ export async function mountM01LogicLab(
let allLogics: LogicSummary[] = [];
const reload = async (): Promise<void> => {
const [logics, logicFiles, subs] = await Promise.all([
fetchLogics(),
fetchLogicFiles(),
fetchLogicSubs(),
]);
files = logicFiles;
const [logics, subs] = await Promise.all([fetchLogics(), fetchLogicSubs()]);
allLogics = logics;
list.setItems(logics);
// 왼쪽 「전체」 + 구분 → 상세구분 거름 — 고르면 목록으로 돌아와 그 범위만
@@ -263,43 +242,6 @@ export async function mountM01LogicLab(
persist();
};
/** 옛 컨테이너의 「새로 만들기」 — 빈 줄 하나를 초안으로 세움(저장 때 서버가 키를 줌) */
const onDraftNew = (): void => {
const f = files.find((x) => x.file === opened?.file) ?? files[0];
if (!f) return;
const id = `new:${Date.now()}`;
// 원문번호는 절 번호만(부문은 구분 칸) · 키는 저장 때 서버가 줌
const row: LogicRow = {
: "",
: `${tx("New_Key")} ${Object.keys(drafts).length + 1}`,
: "",
: "원/",
: "",
: "공용",
: [],
: [],
: [],
: [],
끝수: null,
};
const next: Opened = {
id,
sub: subOf(f),
file: f.file,
version: f.version,
origKey: null,
row,
original: null,
prices: {},
reasons: [],
lines: null,
values: {},
};
drafts[id] = pick(next, clone(row));
persist();
show(next);
};
const onDelete = async (): Promise<void> => {
if (!opened?.row) return;
if (
@@ -389,7 +331,6 @@ export async function mountM01LogicLab(
back,
el("h2", { text: tx("Title") }),
pending,
createButton({ label: tx("Bar_New"), variant: "ghost", onClick: onDraftNew }),
createButton({ label: tx("Bar_Delete"), variant: "danger", onClick: () => void onDelete() }),
discardButton,
saveButton,
@@ -403,7 +344,6 @@ export async function mountM01LogicLab(
className: "m01-logic__main",
children: [bar, errors, waiting, list.root, editor],
}),
calc,
],
}),
);