Merge branch 'sub_laptop_3' of C:/Aislo_wt/sub_laptop_3 into main_laptop_1

This commit is contained in:
2026-11-08 18:56:36 +09:00
13 changed files with 225 additions and 166 deletions
+20 -4
View File
@@ -1,5 +1,7 @@
/* 줄 쌓기 탭(804-2) — 위 옵션 · 가운데 왼쪽 편집 | 오른쪽 산출근거 · 맨 아래 합계 상자 · 색은 공통 값만 */
/* 줄 쌓기 탭(804-2) — 위 옵션 · 가운데 왼쪽 편집 | 오른쪽 산출근거 · 맨 아래 합계 상자 · 색은 공통 값만
* 글꼴 = 화면이 정함(M01 · M02 · 일위대가 모달 같음 · 요구 701-2 · 부모 상속 안 받음) */
.lstk {
font-size: var(--text-body-sm);
display: flex;
flex-direction: column;
gap: var(--spacing-12);
@@ -237,9 +239,23 @@
flex-basis: 100%;
}
.lstk-qty.ui-btn {
justify-content: flex-start;
max-width: 100%;
/* 수량 = 식 글 칸 + 출처 꼬리표(요구 701-1) — 꼬리표는 줄어 줄임표 */
.lstk-qty {
display: flex;
align-items: center;
gap: var(--spacing-4);
}
.lstk-qty > :first-child {
flex: 1 1 6em;
min-width: 4em;
}
.lstk-qty > .lstk-tag,
.lstk-qty > .ui-btn {
flex: 0 1 auto;
min-width: 0;
max-width: 60%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
+19 -38
View File
@@ -28,6 +28,7 @@ import {
moveLine,
newVar,
ownedOf,
setRow,
stackOf,
type MoneyKind,
type StackDoc,
@@ -45,7 +46,7 @@ import { createUnitPick, NO_UNIT } from "../M02_MasterTemplete/DataFlow/M02_DF_U
import { reach } from "./formula_doc_render";
import { attachDrag } from "./line_stack_drag";
import { openStackPick, type StackTake } from "./line_stack_pick";
import { buildBlock } from "./line_stack_rows";
import { buildBlock, sourceTag } from "./line_stack_rows";
import { lt, type LsKey } from "./line_stack_text";
export interface EditHooks {
@@ -194,7 +195,9 @@ function moneyBody(h: EditHooks, i: number): HTMLElement[] {
];
}
/** 수량 — 기호 하나면 값 + 출처 꼬리표(「0.39 ← 표 · 잡석 수량」 · 요구 687-12 · 기호는 마우스 글) · 누르면 그 자리에서 식 고침 */
/** 수량 = 늘 식 글 칸(기호 하나면 그 이름 · 요구 701-1) + 그 기호에 연결이 있으면 옆 출처 꼬리표(표 = 표 고르기) ·
* 칸을 눌러도 안 바뀜 — 글을 실제로 고칠 때만: 표 값 기호 자리에 수를 치면 그 기호 값(별칭 · 「직접 입력」 · 804-9) ·
* 그 밖 = 수량 식 */
function qtyBox(
h: EditHooks,
i: number,
@@ -204,42 +207,20 @@ function qtyBox(
const line = (h.doc.금액 ?? [])[i];
const text = String(line.수량 ?? "");
const v = h.doc.변수?.find((x) => varId(x) === text);
const at = `qty:${i}`;
const edit = (): HTMLElement => {
const f = field(lt("Qty"), shown(text), (t) =>
h.change((doc) => {
h.open.delete(at);
const l = doc.금액![i];
if (t.trim()) l.수량 = real(t);
else delete l.수량;
}),
);
f.classList.add("lstk-qty");
f.dataset.qty = String(i);
return f;
};
if (!v || h.open.has(at)) return edit();
const val = (h.answer.값 ?? {})[text];
const link = v.연결 ?? {};
const kind = link.갈래 ?? "식";
const from =
v.이름 || (link.키 || link.표 ? h.name(String(link.키 ?? link.표)) : "") || shown(text);
const b = createButton({
label: `${isNum(val) ? eqText(val) : "—"}${v.단위 && shownUnit(v.단위) ? ` ${shownUnit(v.단위)}` : ""} ← ${kind} · ${from}`,
variant: "ghost",
size: 24,
onClick: () => {
h.open.add(at);
h.change(() => undefined);
requestAnimationFrame(() =>
document.querySelector<HTMLInputElement>(`[data-qty="${i}"] input`)?.focus(),
);
},
});
b.classList.add("lstk-qty");
b.title = `${lt("Qty")} = ${shown(text)}`;
b.dataset.qty = String(i);
return b;
const f = field(lt("Qty"), shown(text), (t) =>
h.change((doc) => {
const want = real(t);
if (v?.연결?.표 && /^-?[\d.,]+$/.test(want)) return setRow(doc, text, want);
if (want) doc.금액![i].수량 = want;
else delete doc.금액![i].수량;
}),
);
f.dataset.qty = String(i);
const val = v ? (h.answer.값 ?? {})[text] : undefined;
const tag = v ? sourceTag(h, text, v.연결) : null;
if (tag && isNum(val))
tag.title = `${eqText(val)}${shownUnit(v?.단위) ? ` ${shownUnit(v?.단위)}` : ""} — ${tag.title || tag.textContent}`;
return el("span", { className: "lstk-qty", children: tag ? [f, tag] : [f] });
}
/** [+ 줄] 고르개 값 → 새 줄 */
+55 -47
View File
@@ -64,6 +64,60 @@ const LINK_TAG: Record<string, string> = {
const clipTag = (text: string): HTMLElement =>
el("span", { className: "lstk-tag", text, attrs: { title: text } });
type Link = NonNullable<NonNullable<FormulaDoc["변수"]>[number]["연결"]>;
/** 표 값 — [표 · 칸] 단추 → 표 고르기 창(804-3) */
function tableBtn(h: EditHooks, id: string, link: Link): HTMLElement {
const vals = (h.answer.값 ?? {}) as Record<string, unknown>;
const b = createButton({
label:
[
lt("Tbl"),
...Object.values(link.조건 ?? {}).flatMap((t) =>
/^'.*'$/.test(String(t)) ? [String(t).slice(1, -1)] : [],
),
link.칸,
]
.filter(Boolean)
.join(" · ") + " ▾", // 고른 줄(글 조건) · 칸
variant: "ghost",
size: 24,
clip: true, // 긴 줄 이름 = 칸 안에서 줄임(요구 692-3)
onClick: () =>
void h.table(String(link.표 ?? "")).then((t) => {
if (!t) return void showToast(lt("Tbl_Missing"), "warning");
openTablePick({
table: t,
now: currentRow(t, link, vals),
col: String(link.칸 ?? ""),
bound: boundConds(h.doc, link),
done: (row, col) => h.change((doc) => pickCell(doc, id, t, row, col)),
});
}),
});
b.dataset.tablePick = id;
b.title = `${b.title} — ${h.name(String(link.표 ?? "")) || String(link.표 ?? "")}`;
return b;
}
/** 값 기호의 출처 꼬리표(식 없는 기호) — 표 = 표 고르기 단추 · 표 값 직접 침 = 「직접 입력」 · 그 밖 연결 = 글 ·
* 공식 묶음 값 줄 · 금액 줄 수량 칸(요구 701-1) 같은 꼴 */
export function sourceTag(h: EditHooks, id: string, link: Link | undefined): HTMLElement | null {
if (!link) return null;
if (link.갈래 === "표") return tableBtn(h, id, link);
if (link.갈래 === "숫자" && link.표)
return el("span", {
className: "lstk-tag",
text: lt("Direct"),
attrs: { title: lt("Direct_Hint") },
});
if (!LINK_TAG[link.갈래 ?? ""]) return null;
const sub = link.갈래 === "단가산출" && link.키 ? h.subDoc(link.키) : null;
return clipTag(
`${LINK_TAG[link.갈래!]}${link.키 && !link.키.includes("{") ? ` · ${sub?.이름 ?? h.name(link.키) ?? ""}` : ""}`,
);
}
const input = (
value: string,
placeholder: string,
@@ -141,39 +195,6 @@ export function buildBlock(
});
head.dataset.tpl = changed ? "changed" : line.틀 ? "tpl" : "";
// ── 표 값 — [표 · 칸] → 표 고르기 ──
const tableBtn = (id: string, link: NonNullable<FormulaDoc["변수"]>[number]["연결"] & object) => {
const b = createButton({
label:
[
lt("Tbl"),
...Object.values(link.조건 ?? {}).flatMap((t) =>
/^'.*'$/.test(String(t)) ? [String(t).slice(1, -1)] : [],
),
link.칸,
]
.filter(Boolean)
.join(" · ") + " ▾", // 고른 줄(글 조건) · 칸
variant: "ghost",
size: 24,
clip: true, // 긴 줄 이름 = 칸 안에서 줄임(요구 692-3)
onClick: () =>
void h.table(String(link.표 ?? "")).then((t) => {
if (!t) return void showToast(lt("Tbl_Missing"), "warning");
openTablePick({
table: t,
now: currentRow(t, link, vals),
col: String(link.칸 ?? ""),
bound: boundConds(d, link),
done: (row, col) => h.change((doc) => pickCell(doc, id, t, row, col)),
});
}),
});
b.dataset.tablePick = id;
b.title = `${b.title} — ${h.name(String(link.표 ?? "")) || String(link.표 ?? "")}`;
return b;
};
// ── 줄 ──
const renameRow = (old: string, now: string): void => {
const t = now.trim();
@@ -233,20 +254,7 @@ export function buildBlock(
const was = h.before?.get(id);
const freed = !iffy && raw !== undefined && !!was && hasIf(tryParse(was) ?? ["num", 0, "0"]);
const sub = link?.갈래 === "단가산출" && link.키 ? h.subDoc(link.키) : null;
const tag =
link?.갈래 === "표" && !expr
? tableBtn(id, link)
: link?.갈래 === "숫자" && link.표 && !expr
? el("span", {
className: "lstk-tag",
text: lt("Direct"),
attrs: { title: lt("Direct_Hint") },
})
: link && LINK_TAG[link.갈래 ?? ""] && !expr
? clipTag(
`${LINK_TAG[link.갈래!]}${link.키 && !link.키.includes("{") ? ` · ${sub?.이름 ?? h.name(link.키) ?? ""}` : ""}`,
)
: null;
const tag = expr ? null : sourceTag(h, id, link);
const tools: HTMLElement[] = [];
if (sub?.결과 && link?.키)
tools.push(
+7 -2
View File
@@ -3,7 +3,8 @@
* 단가산출 「줄 쌓기」 탭(PLAN 804-2 · 요구 670~677) — 공용(M01 단가산출 기초 · M02 단가산출 = 길 앞머리만 바꿈)
* 위 = 옵션 가로(`formula_doc_options.ts`) · M02 사본 [단가 갱신] · [재생성] · 「M01 새 판」 배지(층 쓰기)
* `page` 있음(M01 804-7 · M02 804-8) = 저장 · 되돌림 단추 없음 — 페이지 [저장] · [되돌림]이 `save` · `discard` 를 부름 ·
* 고침이 바뀌면 `page()` · `page` 없음(일위대가 줄 모달 등) = 위에 [고침 버림] · [수식 저장]
* 고침이 바뀌면 `page()` · `page` 없음(일위대가 줄 모달 등) = 위에 [고침 버림] · [수식 저장] ·
* `tools` 있음(808-1) = 화면 단추를 페이지 머리 칸에
* 가운데 = 왼쪽 줄 쌓기 편집(`line_stack_edit.ts`) | 오른쪽 산출근거 출력(`line_stack_out.ts` · 기계 시간당 경비 펼침)
* 아래 = 합계 상자(노무비 · 재료비 · 경비 · 계 · 화면 아래 고정 · 요구 687-3)
* 정본 = 수식 문서(탭을 바꾸면 다시 읽음) · 줄 쌓기 차림 = 문서 칸 `쌓기` · 804-6 옛 수식 문서 탭 걷음 = 이 화면이 「수식」 탭
@@ -66,6 +67,8 @@ export interface StackScreenOptions {
scope?: { layer: Layer; owner: string | null };
/** 페이지가 저장 · 되돌림을 맡음(M01 804-7 · M02 804-8) — 고침 있음이 바뀔 때 부름 · 화면 위 [고침 버림] · [수식 저장] 없음 */
page?: () => void;
/** 페이지 머리 칸(808-1 · 요구 701-3) — 있으면 [단가 갱신] · [재생성] 등 화면 단추를 여기에(화면 위 단추 줄 대신) */
tools?: HTMLElement;
}
type Docs = Record<string, FormulaDoc>;
@@ -267,6 +270,7 @@ export function createStackScreen(opt: StackScreenOptions) {
const draw = (): void => {
if (!doc || !answer) {
[head, left, right, foot].forEach((x) => x.replaceChildren());
opt.tools?.replaceChildren();
return;
}
const options = buildOptions(doc, given, !!cp && !opt.admin, (k, v, x) => {
@@ -311,10 +315,11 @@ export function createStackScreen(opt: StackScreenOptions) {
...(cp ? copyBadges(mark, opt.admin) : []),
],
}),
el("div", { className: "lstk__tools", children: tools }),
el("div", { className: "lstk__tools", children: opt.tools ? [] : tools }),
],
}),
);
opt.tools?.replaceChildren(...tools);
const items = stackOf(doc);
root.dataset.edit = edit() ? "1" : "";
left.replaceChildren(
@@ -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();
@@ -5,14 +5,14 @@
* (보기 넷 · 공용 상세(724-1 `LogicLab_Ho`) 줄 모달 품 · 단가 덮개 · 줄 빼기 · 더하기 · 박스 옵션 · 입력 칩)
* · `take()` = [적용] 때 그 줄에 남길 것 — 패치 `{덮개, 뺀줄, 더한줄}`(M02 변경 항목 · M01 안 바뀜 · 내역 17-2) +
* 옵션(시험 입력 → 그 줄 `입력값` · 정함 5)
* · [단가산출로도 저장](기본 꺼짐) = `saveAsSk` — 개인 층 새 SK(`from-line`)
* · 옛 [단가산출로도 저장] 걷음(808-1 · 요구 701-4 — 일위대가 줄은 단가산출을 담아서만)
* ========================================================================== */
import { el } from "@ui/ui_template_elements";
import { createText } from "@ui/ui_template_text";
import { fetchAuto } from "../M01_MasterData/M01_MasterData_UI_Logic_Api";
import type { LaborRow } from "./M02_MasterTemplete_Labor_Api";
import { calcSangeun, sangeunFromLine, type SkScope } from "./M02_MasterTemplete_Sangeun_Cover";
import { calcSangeun, type SkScope } from "./M02_MasterTemplete_Sangeun_Cover";
import { mountSangeunDetail } from "./M02_MasterTemplete_Sangeun_Detail";
import { cleanPatch, lineDoc, needsIds, type LinePatch } from "./logic/M02_Sangeun_Cover_Model";
@@ -27,7 +27,15 @@ const same = (a: unknown, b: unknown): boolean => JSON.stringify(a) === JSON.str
/** 줄 모드 SK 상세를 `host` 에 — 바탕 줄 정체가 필요하면 먼저 셈 */
export function mountRowSk(
host: HTMLElement,
o: { row: LaborRow; scope: SkScope; title: string; unit: string; readOnly: boolean },
o: {
row: LaborRow;
scope: SkScope;
title: string;
unit: string;
readOnly: boolean;
/** 모달 머리 칸(808-1) — 보기 고르기 · 수식 단추(옛 [항목 추가] · [율 줄 추가] 줄 없음) */
slot?: HTMLElement;
},
): RowSkHandle {
let handle: ReturnType<typeof mountSangeunDetail> | null = null;
let alive = true;
@@ -60,6 +68,7 @@ export function mountRowSk(
owner: o.scope.owner,
readOnly: o.readOnly,
onChanged: () => undefined,
slot: o.slot,
});
if (o.row.로직 && !o.row.참조) auto = await fetchAuto(o.row.로직).catch(() => ({}));
})();
@@ -90,19 +99,3 @@ export function mountRowSk(
},
};
}
/** [단가산출로도 저장] — 그 줄(고친 패치 포함)을 개인 층 새 SK 로 · 새 이름 돌려줌 ·
* 같은 내용 물음(733-3)에서 그것을 씀 = 그 키 · 취소 = null */
export async function saveAsSk(
row: LaborRow,
patch: LinePatch | null,
name: string,
unit: string,
): Promise<string | null> {
const line: Record<string, unknown> = { ...row };
delete line.패치;
delete line.수량;
if (patch) line.패치 = patch;
const got = await sangeunFromLine({ layer: "personal", owner: null }, line, name, unit || "식");
return got?.이름 ?? null;
}
@@ -4,6 +4,7 @@
* 단가산출 줄을 고르면 그 단가산출 문서를 그대로 엶 = M02 단가산출 상세(`Sangeun_Detail` · 첫 탭 「수식」 줄 쌓기) 부품 그대로.
* · [저장] = 수식 저장(`flush`) → 그 SK 문서 저장(`saveTemplate` · 그 층 · 그 주인) → 닫음 → 부르는 쪽이 일위대가 금액을 다시 셈
* · 그 층을 못 고치면(위 층 · 남의 주인) 읽기만 · [저장] 없음 · 고친 것 있이 닫으면 물음(버리면 수식 캐시도 비움)
* · 머리(808-1 · 요구 701-4 · 5) = 단가산출 페이지 머리와 같음 — 보기 고르기 · [단가 갱신] · [재생성] · [저장] · [닫기]
* 짝 모달 = `Labor_RefEdit`(일위조합 SC 줄).
* ========================================================================== */
@@ -69,6 +70,7 @@ export async function openSkEdit(opt: SkEditOptions): Promise<void> {
},
mount: (body, close, head) => {
const host = el("div", { attrs: { "data-labor-sk": opt.ref.키 } });
const slot = el("span", { className: "m02-main__tools" }); // 보기 고르기 · [단가 갱신] · [재생성](808-1 · 페이지 머리와 같음)
handle = mountSangeunDetail(host, doc, {
layer,
owner: opt.ref.주인,
@@ -76,6 +78,7 @@ export async function openSkEdit(opt: SkEditOptions): Promise<void> {
onChanged: (d) => (dirty = d && !opt.readOnly),
onVersion: (v) => (version = v), // 수식 저장이 파일 판을 바꿈 — 문서 저장이 이어 받음
source: true, // 페이지와 같은 길 — 수식 저장 단추는 이 [저장] 한 번에 모임
slot,
});
const ok = createButton({
label: tc("Save"),
@@ -112,6 +115,7 @@ export async function openSkEdit(opt: SkEditOptions): Promise<void> {
),
host,
);
head.append(slot);
if (!opt.readOnly) head.append(ok); // [저장] = 머리 [닫기] 왼쪽
},
});
@@ -7,6 +7,7 @@
* 둘 다 쓰기 권한이 있는 층에서만 보임(`update`) · 상세에 저장 안 한 고침이 있으면 먼저 물음.
* 단가산출 상세(804-8 · M01 단가산출 기초와 같은 단추 규칙) = [새로고침] 숨김 · [되돌림] = 물음 → 다시 읽음(수식 캐시도 비움) ·
* [되돌림] · [저장] = 고침 있을 때만(`dim`)
* 808-1(요구 701-3) — [초기화] 이름 = 「M01 값으로」(뜻대로 · [되돌림] 과 구분) · 그 왼쪽 = 수식 [단가 갱신] · [재생성](머리 칸)
* ========================================================================== */
import { createButton, showConfirmDialog, showToast } from "@ui/ui_template_elements";
@@ -24,7 +25,7 @@ import { sangeunList } from "./M02_MasterTemplete_UI_Side_Sangeun";
import type { Selection } from "./M02_MasterTemplete_UI_Main";
const TEXT = {
Reset: ["초기화", "Reset"],
Reset: ["M01 값으로", "To M01"], // 808-1 — 저장본을 M01 키 값으로 씀([되돌림] = 저장 안 한 고침 버림과 다름)
ResetTip: [
"고친 값을 연결된 M01 키 값으로 되돌림 — 옵션 · 기본정보는 그대로",
"Revert edited values to their linked M01 key values — options and basic info stay",
@@ -13,6 +13,18 @@
gap: 8px;
}
/* 수식 화면 단추 [단가 갱신] · [재생성](808-1) — 보기 고르기 옆 */
.m02-sk__tools {
display: inline-flex;
flex-wrap: nowrap;
align-items: center;
gap: 8px;
}
.m02-sk__tools[hidden] {
display: none;
}
.m02-sk__head .ui-field {
min-width: 120px;
}
@@ -199,7 +199,7 @@ export function mountSangeunDetail(
: [
el("div", {
className: "m02-sk__head",
children: [tabs.picker, addButton, rateButton],
children: [tabs.picker, tabs.tools, addButton, rateButton],
}),
]),
tabs.host,
@@ -208,7 +208,7 @@ export function mountSangeunDetail(
});
host.replaceChildren(side ? side.wrap(main) : main);
opt.slot?.replaceChildren(
el("span", { className: "ui-toolbar m02-main__tools", children: [tabs.picker] }),
el("span", { className: "ui-toolbar m02-main__tools", children: [tabs.picker, tabs.tools] }),
);
const opened = (): boolean => (doc.열린입력 ?? []).length > 0;
@@ -376,7 +376,7 @@ export function mountSangeunDetail(
];
const row = base ? { ...base.logic, 입력: [...(base.logic.입력 ?? []), ...extra] } : standIn();
const view = ans ? hoView(ans, bare, base?.logic ?? null, doc) : null;
tabs.draw(/^SK-\d{6}$/.test(doc.키 ?? "") ? (doc.키 ?? "") : "");
tabs.draw(/^SK-(?!0{6})\d{6}$/.test(doc.키 ?? "") ? (doc.키 ?? "") : ""); // SK-000000 = 줄 문서(808-1)
const hoCtx = {
row: view ? { ...row, 호표: view.호표, 덧줄: view.덧줄 } : row,
prices: base?.prices ?? {},
@@ -6,6 +6,7 @@
* 저장(804-8 · `page` 있음 = 단가산출 화면) = 페이지 [저장] · [되돌림] 한 번에 — 탭 위 저장 단추 없음(M01 과 같음)
* 옛 수식 문서 탭(802-4b-2)은 804-6 에 걷음 · 단가산출 상세 = 상세가 쥔 `editor`(수식일 때 숨김)
* 저장 안 한 SK(일위대가 줄 모달 · 새 SK) = 「저장한 단가산출만」 글
* [단가 갱신] · [재생성](808-1 · 요구 701-3) = `tools` — 보기 고르기 옆 머리 줄(수식 보기일 때만)
* ========================================================================== */
import { createSegment } from "@ui/ui_template_segment";
@@ -41,6 +42,8 @@ export function createSangeunTabs(
opt: SkEquationOptions,
): {
picker: HTMLElement;
/** 수식 화면 단추([단가 갱신] · [재생성]) — 보기 고르기 옆(머리 줄) */
tools: HTMLElement;
host: HTMLElement;
/** 저장한 SK 키(빈 키 = 저장 전) */
draw: (key: string) => void;
@@ -49,6 +52,7 @@ export function createSangeunTabs(
save: () => Promise<boolean>;
discard: () => Promise<void>;
} {
const tools = el("span", { className: "m02-sk__tools", attrs: { "data-sk-tools": "" } }); // [단가 갱신] · [재생성](808-1)
const stack = createStackScreen({
base: "",
admin: opt.edit,
@@ -56,12 +60,13 @@ export function createSangeunTabs(
copy: { route: (k, p) => skDocPath(opt.scope, k, p), onSaved: opt.onSaved },
scope: opt.scope, // 804-4 다른 단가산출 · 일위대가 복사
page: opt.page,
tools,
});
let key = "";
let view: View = "stack";
const show = (v: View): void => {
view = v;
stack.root.hidden = v !== "stack";
stack.root.hidden = tools.hidden = v !== "stack";
editor.hidden = v !== "ho";
if (!editor.hidden) editor.dataset.view = v;
};
@@ -82,9 +87,15 @@ export function createSangeunTabs(
show("stack");
return {
picker: picker.root,
tools,
host: el("div", { className: "m02-sk__views", children: [stack.root] }),
draw: (k: string) => {
key = k;
// 문서 없는 줄(일위대가 로직 줄 모달 등 · 페이지 아님) = 단가산출 상세부터(808-1 · 수식은 문서가 있어야)
if (!k && !opt.page && view === "stack") {
picker.set("ho");
return show("ho");
}
if (view === "stack") stack.draw(k);
},
canLeave: () => stack.canLeave(),
+21 -1
View File
@@ -309,7 +309,8 @@ def test_표값_직접_침_별칭_그대로() -> None:
], k
assert a.get("계") and a.get("계") != docs_sum.get(k), (k, a.get("계"))
rows = _src("A00_Common/line_stack_rows.ts")
assert 'link?.갈래 === "숫자" && link.표 && !expr' in rows
assert 'if (link.갈래 === "숫자" && link.표)' in rows # 808-1 — 출처 꼬리표 한 곳(`sourceTag`)
assert "const tag = expr ? null : sourceTag(h, id, link);" in rows
assert "const from = v?.연결?.표 ?" in _src("common_util/common_util_line_stack.ts")
@@ -323,3 +324,22 @@ def test_공식_묶음_격자_긴_이름_줄임() -> None:
rows = _src("A00_Common/line_stack_rows.ts")
assert "clip: true," in rows and "attrs: { title: text }" in rows
assert "name.title = `${show(id)} — ${name.title}`;" in rows
def test_808_1_수량_칸_식_글_꼬리표_글꼴_머리():
"""808-1(요구 701) — 수량 칸 = 늘 식 글 칸 + 출처 꼬리표(표 = 표 고르기 · 누름으로 안 바뀜) · 표 값 기호에 수 = 그 값만(별칭) ·
글꼴 = 화면이 정함(M01 · M02 같음) · [단가 갱신] · [재생성] = 페이지 머리 칸 · [초기화] 이름 = 뜻대로."""
edit = _src("A00_Common/line_stack_edit.ts")
body = edit[edit.index("function qtyBox(") : edit.index("/** [+ 줄] 고르개 값")]
assert "createButton" not in body and "h.open" not in body # 단추 꼴 · 눌러 바뀜 없음
assert "sourceTag(h, text, v.연결)" in body and "setRow(doc, text, want)" in body
css = _src("A00_Common/line_stack.css")
assert ".lstk {\n font-size: var(--text-body-sm);" in css
screen = _src("A00_Common/line_stack_screen.ts")
assert "opt.tools?.replaceChildren(...tools);" in screen
assert "children: opt.tools ? [] : tools" in screen
tabs = _src("M02_MasterTemplete/M02_MasterTemplete_Sangeun_Tabs.ts")
assert 'stack.root.hidden = tools.hidden = v !== "stack";' in tabs
assert 'Reset: ["M01 값으로", "To M01"]' in _src(
"M02_MasterTemplete/M02_MasterTemplete_Main_Actions.ts"
)
+19
View File
@@ -119,3 +119,22 @@ def test_옛_판_저장_409(world):
with pytest.raises(store.StoreError) as why:
store.write("sangeun", key, got["판"], got["문서"])
assert why.value.status == 409
def test_808_1_모달_머리_일원화():
"""808-1(요구 701-4 · 5) — 단가산출 줄 모달 머리 = 페이지 머리(보기 고르기 · [단가 갱신] · [재생성]) ·
로직 줄 모달(저장된 일위대가 줄 = 펼친 로직 줄 · 740-1) = 옛 [옵션] · [단가산출로도 저장] · [항목 추가] · [율 줄 추가] 없음 ·
보기 고르기 없이 상세만 + 안내(온 단가산출) · 줄 문서 키 SK-000000 = 수식 문서 안 읽음(404 없음)."""
sk = _src("M02_MasterTemplete_Labor_SkEdit.ts")
assert "slot,\n" in sk and "head.append(slot);" in sk
row = _src("M02_MasterTemplete_Labor_RowModal.ts")
for gone in ("AlsoSk", "createCheckbox", "openLaborInput", "saveAsSk", "Labor_Option"):
assert gone not in row, gone
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 "children: [tabs.picker, tabs.tools]" in detail
assert r"/^SK-(?!0{6})\d{6}$/" in detail
tabs = _src("M02_MasterTemplete_Sangeun_Tabs.ts")
assert 'if (!k && !opt.page && view === "stack")' in tabs