diff --git a/A00_Common/line_stack.css b/A00_Common/line_stack.css index 37fa8fe6b..ab78bcaa0 100644 --- a/A00_Common/line_stack.css +++ b/A00_Common/line_stack.css @@ -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; diff --git a/A00_Common/line_stack_edit.ts b/A00_Common/line_stack_edit.ts index 2c2250eef..eab8ae79a 100644 --- a/A00_Common/line_stack_edit.ts +++ b/A00_Common/line_stack_edit.ts @@ -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(`[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] }); } /** [+ 줄] 고르개 값 → 새 줄 */ diff --git a/A00_Common/line_stack_rows.ts b/A00_Common/line_stack_rows.ts index 68e4c2abc..3a29fefa2 100644 --- a/A00_Common/line_stack_rows.ts +++ b/A00_Common/line_stack_rows.ts @@ -64,6 +64,60 @@ const LINK_TAG: Record = { const clipTag = (text: string): HTMLElement => el("span", { className: "lstk-tag", text, attrs: { title: text } }); +type Link = NonNullable[number]["연결"]>; + +/** 표 값 — [표 · 칸] 단추 → 표 고르기 창(804-3) */ +function tableBtn(h: EditHooks, id: string, link: Link): HTMLElement { + const vals = (h.answer.값 ?? {}) as Record; + 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[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( diff --git a/A00_Common/line_stack_screen.ts b/A00_Common/line_stack_screen.ts index 42e216cdf..f47beced3 100644 --- a/A00_Common/line_stack_screen.ts +++ b/A00_Common/line_stack_screen.ts @@ -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; @@ -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( diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_RowModal.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_RowModal.ts index 8a5e0eecc..f00457903 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_RowModal.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_RowModal.ts @@ -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): 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(); diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_RowSk.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_RowSk.ts index 8cc11acac..e42d29c13 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_RowSk.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_RowSk.ts @@ -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 | 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 { - const line: Record = { ...row }; - delete line.패치; - delete line.수량; - if (patch) line.패치 = patch; - const got = await sangeunFromLine({ layer: "personal", owner: null }, line, name, unit || "식"); - return got?.이름 ?? null; -} diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_SkEdit.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_SkEdit.ts index d7a23a544..ad1d96947 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_SkEdit.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_SkEdit.ts @@ -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 { }, 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 { 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 { ), host, ); + head.append(slot); if (!opt.readOnly) head.append(ok); // [저장] = 머리 [닫기] 왼쪽 }, }); diff --git a/M02_MasterTemplete/M02_MasterTemplete_Main_Actions.ts b/M02_MasterTemplete/M02_MasterTemplete_Main_Actions.ts index df56bf19c..34ed52ec7 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Main_Actions.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Main_Actions.ts @@ -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", diff --git a/M02_MasterTemplete/M02_MasterTemplete_Sangeun.css b/M02_MasterTemplete/M02_MasterTemplete_Sangeun.css index a65ca19e7..fb1493f93 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Sangeun.css +++ b/M02_MasterTemplete/M02_MasterTemplete_Sangeun.css @@ -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; } diff --git a/M02_MasterTemplete/M02_MasterTemplete_Sangeun_Detail.ts b/M02_MasterTemplete/M02_MasterTemplete_Sangeun_Detail.ts index 070beb1a1..91b515372 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Sangeun_Detail.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Sangeun_Detail.ts @@ -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 ?? {}, diff --git a/M02_MasterTemplete/M02_MasterTemplete_Sangeun_Tabs.ts b/M02_MasterTemplete/M02_MasterTemplete_Sangeun_Tabs.ts index 8e66583fd..b7861ad5b 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Sangeun_Tabs.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Sangeun_Tabs.ts @@ -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; discard: () => Promise; } { + 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(), diff --git a/resources/tester/test_804_7_line_stack_fix.py b/resources/tester/test_804_7_line_stack_fix.py index 0f9e54a32..f43e0e478 100644 --- a/resources/tester/test_804_7_line_stack_fix.py +++ b/resources/tester/test_804_7_line_stack_fix.py @@ -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" + ) diff --git a/resources/tester/test_806_1_labor_view.py b/resources/tester/test_806_1_labor_view.py index f6aec72a9..5f769b4e2 100644 --- a/resources/tester/test_806_1_labor_view.py +++ b/resources/tester/test_806_1_labor_view.py @@ -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