fix(808-1): 줄 쌓기 수정 3차 — 수량 칸 식 글 칸 + 출처 꼬리표 · 글꼴 M01 = M02 · M02 머리 [단가 갱신][재생성] · 일위대가 줄 모달 일원화

- 수량 칸(요구 701-1) = 늘 식 글 칸(Q_특별인부) + 출처 꼬리표(표 = 표 고르기) · 눌러도 안 바뀜 · 표 값 기호에 수 = 그 값만(직접 입력) · 꼬리표 한 곳(sourceTag)
- 글꼴(701-2) = 줄 쌓기 화면이 정함(14px · M02 가 부모 16px 을 받던 것)
- M02 머리(701-3) = [초기화] 와 [되돌림] 뜻 다름 → [초기화] 이름 「M01 값으로」 · [단가 갱신][재생성] = 보기 고르기 옆 머리 칸(수식 보기만)
- 일위대가 단가산출 줄 모달(701-5) = 페이지 머리와 같음(보기 고르기 · [단가 갱신] · [재생성] · [저장])
- 일위대가 로직 줄 모달(701-4 · 5) = 옛 [옵션] · [단가산출로도 저장] · [항목 추가] · [율 줄 추가] 걷음 · 보기 고르기 없이 상세만 + 안내(온 단가산출) ·
  줄 문서 키 SK-000000 이 수식 문서를 읽어 404 · 「못 읽음」 이던 것 고침
- 시험: test_804_7 낡은 소스 기대(꼬리표 조건 글) 고침 + 808-1 · test_806_1 808-1 모달

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-08 18:52:03 +09:00
co-authored by Claude Opus 5.5
parent 6f727c9054
commit f5d3347d1e
13 changed files with 225 additions and 166 deletions
@@ -1,26 +1,27 @@
/* =============================================================================
* M02_MasterTemplete_Labor_RowModal.ts
* 일위대가표 줄 누름 모달(PLAN 723-2 · 723-3 · 요구서_내역16 2장 4 · 5) — 일위대가표에서 빠진 줄 동작을 모달 안으로.
* · 로직 줄 · 단가산출(SK) 참조 줄 = 머리(이름 · 「1 ㎡ 당」) · [옵션](메모 · 켜쌓기율 · `Labor_Input`) ·
* 단가산출 상세 줄 모드(717-2b 보기 넷 · `Labor_RowSk` · 717-3 두 보기 갈음) ·
* · 로직 줄(단가산출 문서 없음) · 셈 주입 SK 줄 = 머리(이름 · 「1 ㎡ 당」) ·
* 단가산출 상세 줄 모드(717-2b · `Labor_RowSk` · 보기 고르기 = 모달 머리 · 옛 [항목 추가] · [율 줄 추가] 없음) ·
* 로직 자재(바꾸기 · 기본 · 세 칸 · `Labor_LogicMats`)
* · [적용] = 그 줄 덮개(M02 변경 항목) + 옵션(시험 입력 → 그 줄 입력값 · 규격 칸) · [단가산출로도 저장](기본 꺼짐)
* · [적용] = 그 줄 덮개(M02 변경 항목) + 옵션(상세 옵션 칸 → 그 줄 입력값)
* · 808-1(요구 701-4 · 5) — 옛 [옵션] · [단가산출로도 저장] · [항목 추가] · [율 줄 추가] 걷음 · 일위대가 줄 = 저장 때
* M01 로직 줄 + 고침으로 펼쳐 담김(740-1 · `본뜬` = 온 단가산출) → 수식 문서 없음 = 수식 탭 · 보기 고르기 없이 단가산출 상세만 +
* 안내(온 단가산출 이름 · 수식은 그 단가산출에서 고쳐 다시 담음)
* · 사용자 자재 줄 = 자재 모달(세 칸 · `Labor_MatTable` 한 줄)
* · 고침 = 그 줄 화면 캐시(저장은 [저장]) · 금액은 표가 다시 셈 → `watch` 로 받아 다시 그림
* ========================================================================== */
import { createCheckbox } from "@ui/ui_template_checkbox";
import { createBanner } from "@ui/ui_template_banner";
import { createButton, el, showToast } from "@ui/ui_template_elements";
import { askConfirm, openModal } from "@ui/ui_template_modal";
import { createText } from "@ui/ui_template_text";
import type { ItemLogic } from "./M02_MasterTemplete_Api_Fetch";
import { HOPYO_BUILT } from "./M02_MasterTemplete_Hopyo_Api";
import type { LaborCalc, LaborMat, LaborRow, MatMarkKey } from "./M02_MasterTemplete_Labor_Api";
import { openLaborInput } from "./M02_MasterTemplete_Labor_Input";
import { logicMatRows } from "./M02_MasterTemplete_Labor_LogicMats";
import { setLogicMark, setMark } from "./M02_MasterTemplete_Labor_MatMarks";
import { matTable } from "./M02_MasterTemplete_Labor_MatTable";
import { mountRowSk, saveAsSk, type RowSkHandle } from "./M02_MasterTemplete_Labor_RowSk";
import { mountRowSk, type RowSkHandle } from "./M02_MasterTemplete_Labor_RowSk";
import { tc } from "./M02_MasterTemplete_Labor_Text";
import type { SkScope } from "./M02_MasterTemplete_Sangeun_Cover";
import { releaseTied } from "./M02_Labor_LogicMats_Model";
@@ -50,8 +51,14 @@ export interface RowModalOptions {
const TEXT = {
Apply: ["적용", "Apply"],
AlsoSk: ["단가산출로도 저장", "Also save as build-up"],
Saved: ["{k} 저장함(내 개인 층)", "{k} saved (my layer)"],
NoDoc: [
"M01 로직을 바로 쓴 줄 — 수식 탭은 단가산출 문서가 있어야 열림(단가산출을 만들어 담음) · 여기서는 단가산출 상세만",
"Uses the M01 logic directly — the equation tab needs a build-up document",
],
FromSk: [
"단가산출 「{k}」 에서 담아 온 줄(담을 때 펼침) — 수식은 그 단가산출에서 고쳐 다시 담음 · 여기서는 단가산출 상세만",
"Copied from build-up 「{k}」 — edit its equation there and re-add",
],
Discard: ["적용 안 한 고침이 있음 — 버리고 닫을까?", "Discard unapplied edits?"],
} as const;
const t = (k: keyof typeof TEXT): string => TEXT[k][0];
@@ -110,30 +117,6 @@ export function openRowModal(o: RowModalOptions): void {
);
};
const option = createButton({
label: tc("Labor_Option"),
variant: "ghost",
onClick: () =>
openLaborInput({
title: o.title,
logic: o.logic,
values: row.입력값,
memo: row.메모,
rate: row.켜쌓기율 ?? null,
readOnly: o.readOnly,
done: (next) => {
const same = setValues(next.입력값) && next.켜쌓기율 === (row.켜쌓기율 ?? null);
row.메모 = next.메모;
if (next.켜쌓기율 === null) delete row.켜쌓기율;
else row.켜쌓기율 = next.켜쌓기율;
o.done(!same);
draw();
if (!same) skMount(); // 바뀐 옵션으로 상세 다시
},
}),
});
option.dataset.rowOption = "";
/** 옵션 → 그 줄 입력값(로직 줄만 · 아는 입력만) · 같으면 참 */
function setValues(next: Record<string, string | number>): boolean {
if (row.참조) return true;
@@ -145,25 +128,31 @@ export function openRowModal(o: RowModalOptions): void {
return false;
}
// 단가산출 상세 줄 모드(723-3) — 셈 주입(프로젝트 복사본)엔 없음
// 단가산출 상세 줄 모드(723-3) — 셈 주입(프로젝트 복사본)엔 없음 · 보기 고르기 = 모달 머리(808-1)
const slot = el("span", { className: "m02-main__tools" });
const skHost = el("div", { attrs: { "data-row-sk-host": "" } });
let sk: RowSkHandle | null = null;
const skMount = (): void => {
if (o.inject) return;
sk?.destroy();
sk = mountRowSk(skHost, { row, scope: o.scope, title: o.title, unit: o.unit, readOnly: !edit });
sk = mountRowSk(skHost, {
row,
scope: o.scope,
title: o.title,
unit: o.unit,
readOnly: !edit,
slot,
});
};
let close = (): void => {};
const finish = (): void => {
stop();
sk?.destroy();
};
const also = createCheckbox(t("AlsoSk"), { checked: false });
also.input.dataset.rowAlsoSk = "";
const apply = createButton({
label: t("Apply"),
variant: "filled",
onClick: async () => {
onClick: () => {
const got = sk?.take() ?? null;
if (got) {
if (got.patch) row.패치 = got.patch;
@@ -172,37 +161,37 @@ export function openRowModal(o: RowModalOptions): void {
o.done(true);
draw();
}
if (also.input.checked) {
try {
const name = await saveAsSk(row, got?.patch ?? row.패치 ?? null, o.title, o.unit);
if (name) showToast(t("Saved").replace("{k}", name), "success"); // null = 같은 내용 물음 취소
document.dispatchEvent(new Event(HOPYO_BUILT)); // 좌측 단가산출 나무 다시 읽음(725-4)
} catch (error) {
return showToast(error instanceof Error ? error.message : String(error), "error");
}
}
finish(); // [적용] = 그 줄에 남기고 닫음(저장은 페이지 [저장]) · 코드 닫기는 beforeClose 안 거침
close();
},
});
// 수식 문서 없는 줄(808-1) — 안내 한 줄(온 단가산출 = `본뜬`)
const from = row.본뜬 && typeof row.본뜬.이름 === "string" ? row.본뜬.이름 : "";
const noDoc =
!row.참조 && !o.inject
? createBanner({
tone: "info",
text: from ? t("FromSk").replace("{k}", from) : t("NoDoc"),
})
: null;
apply.dataset.rowApply = "";
const stop = o.watch((c) => {
calc = c;
draw();
});
const tools = edit && !o.inject ? [also.root, apply] : [];
const tools = edit && !o.inject ? [apply] : [];
({ close } = openModal({
title: o.title,
closeLabel: tc("In_Close"),
width: "1100px",
wide: true,
actions: [...(row.참조 || !edit ? [] : [option]), ...tools],
actions: row.참조 ? [slot, ...tools] : tools, // 로직 줄 = 보기 고르기 없음(상세만)
beforeClose: async () => {
if (sk?.take() && !(await askConfirm({ message: t("Discard"), danger: true }))) return false;
finish();
return true;
},
mount: (body) => body.append(info, skHost, mats),
mount: (body) => body.append(info, ...(noDoc ? [noDoc] : []), skHost, mats),
}));
draw();
skMount();