From 4ae8d742f259ad06ae637af01e9bf5e80bab278a Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sun, 15 Nov 2026 11:54:05 +0900 Subject: [PATCH] =?UTF-8?q?fix(835-1):=20M02=20=EB=B6=88=EB=9F=AC=EC=98=A4?= =?UTF-8?q?=EA=B8=B0=20=ED=99=94=EB=A9=B4=20=EC=97=AC=EC=84=AF=20=E2=80=94?= =?UTF-8?q?=20=EB=B3=B4=EA=B8=B0=20=EC=9E=90=EB=8F=99=20=EC=A0=80=EC=9E=A5?= =?UTF-8?q?=20=EC=B7=A8=EC=86=8C=20=C2=B7=20=EB=B9=84=EB=AA=A9=20=EC=A4=84?= =?UTF-8?q?=20=EA=B8=88=EC=95=A1=20=EC=B9=B8=20=C2=B7=20=EC=83=81=EC=84=B8?= =?UTF-8?q?=20=EC=B4=88=EB=A1=9D=20=EC=B9=A0=20=EA=B1=B7=EC=9D=8C=20=C2=B7?= =?UTF-8?q?=20=EC=A0=80=EC=9E=A5=20=EB=8B=A8=EC=B6=94=20=E2=86=92=20?= =?UTF-8?q?=EA=B3=A0=EB=A5=B4=EB=A9=B4=20=EB=AA=A8=EB=8B=AC=20=C2=B7=20?= =?UTF-8?q?=EB=AA=A8=EB=8B=AC=201400?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 자동 저장 취소(요구 852-1) — 가져온 식 열기 = 미리 보기 `…/sangeun/from-hopyo/preview`(층 · 재료 고름 안 씀 · 같은 내용 SK 있으면 그것을 읽음) · [저장] = `from-hopyo` 몸 `문서`(고친 문서째 · 안쪽 산근 참조 · 내 기계 그때) · 옛 길: 단가산출표 목록 [내 단가산출로] · [수정] 모달 단산 줄 누름이 서버 make 로 SK 를 바로 만들었음 (`FromHopyo.make` 를 `_prep` · `draft` · `make(doc)` 로 · `Imported.new_doc` · `keep` · `draft` · `MatPick.merge`) - 산출근거 표 비목 줄(노무비 : … = 금액) = 합계 · 그 비목 칸에 금액(엑셀 단가산출 시트와 같음 · 852-2 · `line_stack_out`) - 단가산출 상세 시트 블록 안 줄 이어짐 초록 안 칠함 — 출처 행 = 인력 · 기계 노무비 행(852-3 · 다른 칠 · 머리 행 그대로) - 단가산출 목록 [내 단가산출로] 걷음 → 줄 고르면 개인 층 가져온 식 모달(852-4) - 일위대가표 목록 [내 일위대가로] 걷음 → 줄 고르면 [수정] 모달(852-5 · 모달 머리 [내 일위대가로] 그대로) - 부품 상자 모달 큰 중간 창 `1400px`(1400 × 880) — 불러오기 [수정] · 가져온 식 모달(852-6) - 시험: 새 test_835_1(미리 보기 쓰기 0 · [저장] = 옛 길과 같은 SK) · 낡은 시험 고침 — test_627(저장 단추 걷음 · MatPick 가짜 없어 import.meta 로 죽던 것) · test_779_5(이어짐 초록 안 칠함) · test_793_3b · test_809_1(수정 모달 단산 줄 = 가져온 식 모달) Co-Authored-By: Claude Opus 5.5 --- A00_Common/line_stack_out.ts | 1 + .../M02_MasterTemplete_Hopyo_Imported.ts | 57 +++++++++---- .../M02_MasterTemplete_Hopyo_Sheet.ts | 33 ++++---- .../M02_MasterTemplete_Hopyo_SheetSk.ts | 16 ++-- .../M02_MasterTemplete_Hopyo_View.ts | 29 +++---- M02_MasterTemplete/M02_San_MatPick.py | 14 +++- M02_MasterTemplete/M02_Sangeun_FromHopyo.py | 63 ++++++++++++--- M02_MasterTemplete/M02_Sangeun_Imported.py | 41 ++++++++-- M02_MasterTemplete/M02_Sangeun_Router.py | 25 +++++- .../M02_MasterTemplete_Hopyo_SheetMap.ts | 5 +- resources/tester/helper_778_5_sk_pick.cjs | 7 +- resources/tester/test_627_sheet_sk.py | 18 +++-- .../tester/test_779_5_block_line_tone.py | 11 +-- resources/tester/test_793_3b_import_screen.py | 7 +- resources/tester/test_809_1_import_labor.py | 4 +- resources/tester/test_835_1_import_preview.py | 79 +++++++++++++++++++ ui_template/ui_template_modal.css | 6 ++ ui_template/ui_template_modal.ts | 6 +- 18 files changed, 317 insertions(+), 105 deletions(-) create mode 100644 resources/tester/test_835_1_import_preview.py diff --git a/A00_Common/line_stack_out.ts b/A00_Common/line_stack_out.ts index 86b8c9b0b..0d17543b4 100644 --- a/A00_Common/line_stack_out.ts +++ b/A00_Common/line_stack_out.ts @@ -279,6 +279,7 @@ function rows( cols[c] = part; push({ h: [`${COST_SHOW[c]} : `, eqText(bundle[c]), ...qtyNode(c), ` = ${one(part)}`], + cols: { sum: part, [c]: part }, // 비목 줄 금액 = 합계 · 그 비목 칸(엑셀 단가산출 시트와 같음 · 835-1) }); } if (s) expand(s); diff --git a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Imported.ts b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Imported.ts index 0729d7bf1..105398dad 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Imported.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Imported.ts @@ -2,12 +2,14 @@ * M02_MasterTemplete_Hopyo_Imported.ts * 불러오기 단산 블록 [가져온 식으로 열기] 모달(PLAN 793-3b · 809-1 · 요구 646 · 702) — * 엑셀 보기 단가산출 탭 목록(`Hopyo_SheetSk`)이 엶 · 경쟁사 로직 그대로(기초로 바꾸기 없음 · 요구 703). - * · 열기 = 서버 `…/sangeun/from-hopyo`(그 블록 → 가져온 식 SK · 만들 층 · 엑셀 단가 그대로 · 같은 내용 = 그 SK 다시 씀) - * → 그 SK 를 읽어 우리 단가산출 화면(`Sangeun_Detail` 시트 · 고침 = 시트 그대로) + 머리 판(`Hopyo_ImportedPanel`) + * · 열기 = 미리 보기(835-1 · 요구 852-1 — 층에 안 씀) 서버 `…/sangeun/from-hopyo/preview`(그 블록 → 가져온 식 문서 · + * 엑셀 단가 그대로 · 같은 내용 SK 가 있으면 그 SK 를 읽음) → 우리 단가산출 화면(`Sangeun_Detail` 시트 · 고침 = 시트 그대로) + * + 머리 판(`Hopyo_ImportedPanel`) * · [우리 단가로] · [엑셀 단가로] · [이음 고르기] = 서버 `…/sangeun/imported/{price|link}` * (화면 캐시 문서 고침 · 저장 없음) → 시트 다시 엶 * · [이음 보기](797-2) = 소요량 · 계수 후보 모달(`Hopyo_QtyLinks`) — 고르기 · 버리기 = 서버 `…/imported/{qty|qty-pick|qty-drop}`(기록만 · 금액 그대로) - * · [저장] = 그 SK 저장(서버가 다시 셈) + * · [저장] = 아직 없으면 `…/sangeun/from-hopyo` 몸 `문서`(고친 문서째 SK 만듦 · 재료 고름도 이때 저장) · + * 있으면 그 SK 저장(서버가 다시 셈) — 보는 동안 디스크 쓰기 없음 * ========================================================================== */ import { createButton } from "@ui/ui_template_button"; @@ -28,6 +30,7 @@ const TEXT = { Title: ["가져온 식 단가산출 — 단산{n}", "Imported build-up — block {n}"], Reused: ["같은 내용 {k} 를 다시 씀", "Reusing same content {k}"], Made: ["{k} 만듦(엑셀 단가 그대로)", "{k} made (Excel prices)"], + Have: ["같은 내용 {k} 가 이미 있음 — 그것을 엶", "Same content {k} exists — opened"], Save: ["저장", "Save"], Saved: ["{k} 저장함", "{k} saved"], Discard: ["저장 안 한 고침이 있음 — 버리고 닫을까?", "Discard unsaved edits?"], @@ -56,11 +59,23 @@ interface Made { 다시씀: boolean; } -const fromHopyo = (o: ImportedOptions): Promise => - call(at(o.target, "/sangeun/from-hopyo"), { - method: "POST", - body: JSON.stringify({ 층: o.file.layer, ...(o.file.owner ? { 주인: o.file.owner } : {}), 키: o.key, 번: o.no, ...(o.picks ? { 고름: o.picks } : {}) }), // prettier-ignore - }); +/** 미리 보기 답(835-1) — 같은것 = 같은 내용 SK 키(있으면 그것을 엶) */ +interface Draft { + 문서: ImportedDoc; + 같은것: string | null; +} + +/** 서버 몸 — 파일 층 · HP · 블록 · 저장 안 한 재료 고름 · `문서` = [저장] 때 고친 문서 */ +const fromBody = (o: ImportedOptions, doc?: ImportedDoc): string => + JSON.stringify({ 층: o.file.layer, ...(o.file.owner ? { 주인: o.file.owner } : {}), 키: o.key, 번: o.no, ...(o.picks ? { 고름: o.picks } : {}), ...(doc ? { 문서: doc } : {}) }); // prettier-ignore + +/** 보기(쓰기 없음) */ +export const previewImported = (o: ImportedOptions): Promise => + call(at(o.target, "/sangeun/from-hopyo/preview"), { method: "POST", body: fromBody(o) }); + +/** [저장] 처음 — 고친 문서째 SK 만듦 */ +const fromHopyo = (o: ImportedOptions, doc: ImportedDoc): Promise => + call(at(o.target, "/sangeun/from-hopyo"), { method: "POST", body: fromBody(o, doc) }); interface EditAnswer { 문서: ImportedDoc; @@ -196,6 +211,17 @@ export function openImported(o: ImportedOptions): void { async function save(): Promise { try { const doc = now(); + if (!sk) { + const made = await fromHopyo(o, doc); // 처음 [저장] = 만듦(835-1) + o.onMade?.(); + sk = made.이름; + const got = await readTemplate(o.target.layer, "sangeun", sk, null, o.target.owner); + 판 = got.판 ?? ""; + mount(got.문서 as ImportedDoc); + dirty = false; + showToast((made.다시씀 ? t("Reused") : t("Made")).replace("{k}", sk), "success"); + return true; + } const info = await saveTemplate(o.target.layer, "sangeun", sk, null, 판, doc, o.target.owner); 판 = info.판; st.doc = doc; @@ -215,14 +241,13 @@ export function openImported(o: ImportedOptions): void { async function load(): Promise { const stop = showLoading(root); try { - const made = await fromHopyo(o); - o.onMade?.(); - sk = made.이름; - showToast((made.다시씀 ? t("Reused") : t("Made")).replace("{k}", sk), "info"); - const got = await readTemplate(o.target.layer, "sangeun", sk, null, o.target.owner); - 판 = got.판 ?? ""; + const got = await previewImported(o); // 보기 = 쓰기 없음(835-1) + sk = got.같은것 ?? ""; + const have = sk ? await readTemplate(o.target.layer, "sangeun", sk, null, o.target.owner) : null; // prettier-ignore + 판 = have?.판 ?? ""; + if (sk) showToast(t("Have").replace("{k}", sk), "info"); root.replaceChildren(top, host); - mount(got.문서 as ImportedDoc); + mount((have?.문서 ?? got.문서) as ImportedDoc); void unlinked(); void qtyTally(); } catch (e) { @@ -235,7 +260,7 @@ export function openImported(o: ImportedOptions): void { openModal({ title: t("Title").replace("{n}", String(o.no)), closeLabel: t("Close"), - width: "1100px", + width: "1400px", wide: true, dialogClass: "m02-modal m02-hopyo__imported-modal", actions: [saveBtn], diff --git a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Sheet.ts b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Sheet.ts index 4c43295b4..f5538e345 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Sheet.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Sheet.ts @@ -4,11 +4,11 @@ * 머리 막대 = 시트 · 호표 수 · 검산 · 연결 n / 줄 m(809-1 줄 표지 · 연결됨 + 로직 줄) + 부르는 쪽 덧붙임(`headExtra` — 층 · [끝내기]) · * 왼쪽 = 올린 엑셀을 연 것처럼 읽기 전용 시트(A00 산출근거 스프레드시트 · 탭 셋 일위대가표 · 단가산출표 · 단가산출 상세 · * 칠 · 범례 · 뛰기 · 뒤로 = `Hopyo_SheetNav` · 779-4 · 같은 파일 격자 다시 씀 · 779-6) · - * 오른쪽 = 호표 목록(제N호표 [수정] · [내 일위대가로] · 찾기 · 연결 n/m — 다 연결이면 녹색) · + * 오른쪽 = 호표 목록(제N호표 [수정] · 찾기 · 연결 n/m — 다 연결이면 녹색 · 줄 고름 = [수정] 모달 · 835-1) · * 단가산출표 · 상세 탭이면 단가산출 목록으로 바뀜(780-2 · `Hopyo_SheetSk`). * · 목록 누름 → 시트에서 그 호표 영역(B열 ~ 끝 열) 고름 · 칸 누름 → 그 행 호표를 목록에서 강조 · 머리 행이면 영역 고름 * · [수정] = 일위대가 컨테이너 꼴 모달(809-1 `Hopyo_View` · 경쟁사 로직 인정 · 요소만 연결 · 줄 누름과 따로) · - * 옮기기 = [내 일위대가로](807-1) 하나 · 단가산출 탭 블록 = [가져온 식으로 열기](`Hopyo_SheetSk`) · 기초 판정 · [옮기기] 판 없음(요구 703) + * 옮기기 = 모달 머리 [내 일위대가로](807-1 · 목록 줄 단추는 835-1 에서 걷음) · 단가산출 탭 블록 = `Hopyo_SheetSk` · 기초 판정 · [옮기기] 판 없음(요구 703) * · 격자 = 서버 `/hopyo-grid/{키}` · 옛 파일(격자 없음)은 시트 자리에 까닭 글 · 목록은 됨 * · `buildHopyoSheet` = 몸(머리 · 띠 · 시트 + 목록) · 옛 [저장] · [모두 고정으로 들여오기] · 모달 껍데기는 785-7 에서 지움 * (모두 고정 서버 길은 B09 · 붙여넣기가 씀) @@ -27,7 +27,6 @@ import { type HopyoView, type HopyoScope, } from "./M02_MasterTemplete_Hopyo_Api"; -import { mineButton } from "./M02_MasterTemplete_Hopyo_Mine"; import { openHopyoView } from "./M02_MasterTemplete_Hopyo_View"; import * as Nav from "./M02_MasterTemplete_Hopyo_SheetNav"; import { createSheetSk, type SheetSk } from "./M02_MasterTemplete_Hopyo_SheetSk"; @@ -207,7 +206,19 @@ export function buildHopyoSheet(opt: HopyoSheetOptions): HopyoSheetBody { if (at.시트 !== ilwi || at.범위 !== text) sheet.select(text, ilwi); } - /** [수정] — 그 호표 자리 + 일위대가 컨테이너 꼴 모달(809-1 · 줄 누름과 따로) */ + /** 그 호표 자리 + 일위대가 컨테이너 꼴 모달(809-1) — [수정] · 목록 줄 고름(835-1) */ + const openView = (h: HopyoBrief): void => { + focus(h.번); + openHopyoView({ + file: opt.scope, + key: opt.key, + target, + no: h.번, + brief: h, + canWrite: opt.canWrite, + onSheet: () => focus(h.번), + }); + }; const editButton = (h: HopyoBrief): HTMLElement => { const b = createButton({ label: "수정", @@ -215,16 +226,7 @@ export function buildHopyoSheet(opt: HopyoSheetOptions): HopyoSheetBody { size: "sm", onClick: (e) => { e.stopPropagation(); - focus(h.번); - openHopyoView({ - file: opt.scope, - key: opt.key, - target, - no: h.번, - brief: h, - canWrite: opt.canWrite, - onSheet: () => focus(h.번), - }); + openView(h); }, }); b.setAttribute("data-hopyo-edit", String(h.번)); @@ -247,12 +249,11 @@ export function buildHopyoSheet(opt: HopyoSheetOptions): HopyoSheetBody { children: [ el("span", { className: "m02-hopyo-sheet__no", text: `제${h.번}호표` }), editButton(h), - mineButton(opt.scope, opt.key, h), // [내 일위대가로](807-1) el("span", { className: "m02-hopyo-sheet__name", text: `${h.이름} ${h.규격}`.trim() }), linked, ], }); - row.addEventListener("click", () => focus(h.번)); + row.addEventListener("click", () => openView(h)); items.set(h.번, row); return row; }), diff --git a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_SheetSk.ts b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_SheetSk.ts index 3d16affb4..0f2c379a7 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_SheetSk.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_SheetSk.ts @@ -3,7 +3,8 @@ * 엑셀 보기 창 오른쪽 판 바꾸기(PLAN 780-2 · 809-1 · 요구 627 · 702) — 탭 따라: * · 일위대가표 = 호표 목록(`Hopyo_Sheet` 판 그대로) · 단가산출표 · 단가산출 상세 = 단가산출 목록 * (블록 줄 = 단산 N 이름 · 단위 · 부름 수 / 일위대가 밖 · 만든 가져온 식 SK · 재료 = `hopyo-marks`) - * · 블록 줄 누름 → 지금 탭의 그 블록 자리 · 줄 끝 [가져온 식으로 열기](만들 층) · [내 단가산출로](개인 층 · 807-1) = `Hopyo_Imported` 모달 + * · 블록 줄 누름 → 지금 탭의 그 블록 자리 + 개인 층 `Hopyo_Imported` 모달(미리 보기 · 저장은 모달 [저장] · 835-1 · + * 옛 [내 단가산출로] 단추 걷음) · 줄 끝 [가져온 식으로 열기](만들 층이 개인 밖일 때만) * (경쟁사 로직 그대로 · 엑셀 단가 · 요소 이음 · 소요량 이음) * · 시트 칸 고름 → 그 행 블록을 목록에서 강조(`SheetNav.blockAt`) * · 목록은 처음 단가산출 탭에 올 때 읽음 · 기초 판정 · 블록 판 · [옮기기] 없음(요구 703) @@ -47,7 +48,7 @@ export function createSheetSk(opt: SheetSkOptions) { let sheet = ""; let current: number | null = null; - const mineAt = { layer: "personal", owner: null } as const; // [내 단가산출로](807-1) = 늘 개인 층 + const mineAt = { layer: "personal", owner: null } as const; // 줄 고름 모달(807-1 · 835-1) = 늘 개인 층 const open = (no: number, mine: boolean): void => openImported({ file: opt.scope, key: opt.key, no, target: mine ? mineAt : opt.target, canWrite: mine || opt.canWrite, picks: mat.pending(), onMade: mat.reload }); // prettier-ignore @@ -59,10 +60,8 @@ export function createSheetSk(opt: SheetSkOptions) { const sub = [b.단위 ?? "", calls ? `${L("M02_JudgeCalls")} ${calls}` : L("M02_JudgeOutside")].filter(Boolean).join(" · "); // prettier-ignore const go = createButton({ label: L("M02_CheckImported"), size: 24, variant: "ghost", onClick: (e) => { e.stopPropagation(); open(b.번, false); } }); // prettier-ignore go.dataset.skOpen = String(b.번); - go.hidden = opt.target.layer === "personal" && !opt.target.owner; // 만들 층 = 개인이면 [내 단가산출로]와 같은 일 - const mine = createButton({ label: L("M02_ToMySk"), size: 24, variant: "ghost", onClick: (e) => { e.stopPropagation(); open(b.번, true); } }); // prettier-ignore - mine.dataset.skMine = String(b.번); - const tail = el("span", { className: "ui-toolbar", children: [...(b.SK ? [createBadge(`${L("M02_JudgeMade")} ${b.SK}`, "success")] : []), go, mine] }); // prettier-ignore + go.hidden = opt.target.layer === "personal" && !opt.target.owner; // 만들 층 = 개인이면 줄 고름과 같은 일 + const tail = el("span", { className: "ui-toolbar", children: [...(b.SK ? [createBadge(`${L("M02_JudgeMade")} ${b.SK}`, "success")] : []), go] }); // prettier-ignore const row = createListRow({ label: `${L("M02_JudgeBlock").replace("{n}", String(b.번))} ${b.이름 ?? ""}`, sub, @@ -70,7 +69,10 @@ export function createSheetSk(opt: SheetSkOptions) { stripe: b.SK ? STRIPE.done : calls ? STRIPE.ready : STRIPE.out, active: b.번 === current, size: 36, - onClick: () => pick(b.번), + onClick: () => { + pick(b.번); + open(b.번, true); // 고르면 모달 — 저장할지는 모달에서(835-1) + }, }); row.dataset.judgeSk = String(b.번); return row; diff --git a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_View.ts b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_View.ts index dd80fa9a6..c55102313 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_View.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_View.ts @@ -5,15 +5,15 @@ * · 표지 = 로직 줄 「단가산출 N」 · 「일위대가 N」 칩 / 요소 줄 「✓ 연결됨」 · 「후보 n」 · 「못 이음」(엑셀 값 그대로) * · 엑셀 금액 = 그 줄 엑셀 금액 계 * 줄 = 서버 `GET …/hopyo-view/{키}/{번}`(`M02_Hopyo_View` · 엑셀 숫자 그대로 고정 줄 · 저장 없음 · 금액 = 엑셀). - * · 단가산출 줄 누름 = 그 블록 가져온 식 SK(서버 `…/sangeun/from-hopyo` · 만들 층 · 같은 내용 = 있는 것) → - * 단가산출 「수식」 탭 모달(`Labor_SkEdit` 부르기만 · [엑셀 단가 그대로 | 우리 단가로]) + * · 단가산출 줄 누름 = 그 블록 가져온 식 모달(`Hopyo_Imported` · 미리 보기 · 층에 안 씀 · 저장 = 모달 [저장] · + * 835-1 — 옛 「누르면 SK 만듦」 걷음 · [엑셀 단가 그대로 | 우리 단가로]) * · 일위대가 줄 누름 = 그 하위 호표 [수정] 모달 · 후보 줄 누름 = 후보 목록(고르기 = [내 일위대가로] 뒤 일위대가 [연결]) * · 머리 = 표지 수 · 엑셀 단가 · [내 일위대가로](807-1 `Hopyo_Mine`) · [구조물 조합으로](만든 SC 있을 때 · `Hopyo_ToItem`) · [엑셀 보기] * 기초 판정 · 「우리 기초로 바꾸기」 없음(요구 703). * ========================================================================== */ import { createBadge } from "@ui/ui_template_badge"; -import { createButton, el, showToast } from "@ui/ui_template_elements"; +import { createButton, el } from "@ui/ui_template_elements"; import { loadingError, showLoading } from "@ui/ui_template_loading"; import { currentLanguageIndex, t as L } from "@ui/ui_template_locale"; import { openModal } from "@ui/ui_template_modal"; @@ -25,8 +25,8 @@ import { call } from "./M02_MasterTemplete_Api_Fetch"; import { at, type HopyoBrief, type HopyoScope } from "./M02_MasterTemplete_Hopyo_Api"; import { mineButton } from "./M02_MasterTemplete_Hopyo_Mine"; import { toItem } from "./M02_MasterTemplete_Hopyo_ToItem"; +import { openImported } from "./M02_MasterTemplete_Hopyo_Imported"; import type { LaborDetailHandle } from "./M02_MasterTemplete_Labor_Detail"; -import { openSkEdit } from "./M02_MasterTemplete_Labor_SkEdit"; import { mountLaborTable } from "./M02_MasterTemplete_Labor_Table"; const TEXT = { @@ -137,25 +137,14 @@ export function openHopyoView(o: ViewOptions): void { let table: LaborDetailHandle | null = null; let close = (): void => {}; - /** 단가산출 줄 — 가져온 식 SK(만들 층) → 수식 탭 모달 */ - async function openSk(m: ViewMark, name: string, head: string): Promise { - try { - const made = await call<{ 이름: string }>(at(o.target, "/sangeun/from-hopyo"), { - method: "POST", - body: JSON.stringify({ 층: o.file.layer, ...(o.file.owner ? { 주인: o.file.owner } : {}), 키: o.key, 번: m.번 }), // prettier-ignore - }); - const ref = { 층: o.target.layer, 주인: o.target.owner, 키: made.이름 }; - void openSkEdit({ ref, title: `${name} — ${made.이름}`, head, readOnly: !o.canWrite, closed: () => {} }); // prettier-ignore - } catch (e) { - showToast(why(e), "error"); - } - } + /** 단가산출 줄 — 가져온 식 모달(미리 보기 · 저장은 모달 [저장] · 835-1) */ + const openSk = (m: ViewMark): void => + openImported({ file: o.file, key: o.key, no: m.번!, target: o.target, canWrite: o.canWrite }); function draw(r: ViewReply): void { const count = (k: ViewMark["표지"]): number => r.줄.filter((m) => m.표지 === k).length; const logic = r.줄.filter((m) => m.갈래 === "로직").length; const name = [r.이름, r.규격].filter(Boolean).join(" "); - const head = [name, r.단위 ? `1 ${r.단위} 당` : ""].filter(Boolean).join(" · "); const badges = [ createBadge(t("Logic").replace("{n}", String(logic)), "accent"), createBadge(`${t("Linked")} ${count("연결됨")}`, "success"), @@ -199,7 +188,7 @@ export function openHopyoView(o: ViewOptions): void { const m = r.줄[n]; const line = (r.문서 as { 담은로직?: { 별칭?: string | null }[] }).담은로직?.[n]; const label = line?.별칭 ?? name; - if (m?.표지 === "단가산출" && m.번) return (void openSk(m, label, head), true); + if (m?.표지 === "단가산출" && m.번) return (openSk(m), true); if (m?.표지 === "일위대가" && m.번) return void openHopyoView({ ...o, no: m.번, brief: undefined }), true; // prettier-ignore if (m?.표지 === "후보") return (void showNear(label, m), true); return true; // 요소 줄 = 엑셀 숫자 그대로(모달 없음) @@ -223,7 +212,7 @@ export function openHopyoView(o: ViewOptions): void { title: t("Title").replace("{n}", String(o.no)), closeLabel: t("Close"), wide: true, - width: "1100px", + width: "1400px", dialogClass: "m02-modal m02-hopyo__view-modal", beforeClose: () => { table?.destroy(); diff --git a/M02_MasterTemplete/M02_San_MatPick.py b/M02_MasterTemplete/M02_San_MatPick.py index 64db53f44..8de03be7b 100644 --- a/M02_MasterTemplete/M02_San_MatPick.py +++ b/M02_MasterTemplete/M02_San_MatPick.py @@ -82,15 +82,21 @@ def read(folder: str | Path, fp: str) -> dict[str, Any]: } -def write(folder: str | Path, fp: str, name: str, picks: dict) -> dict[str, dict]: - """종마다 고른 것(None = 고름 풀기)을 얹어 씀 → 지금 고름 전부.""" - now = read(folder, fp) - mine = dict(now["고름"]) +def merge(now: dict, picks: dict | None) -> dict[str, dict]: + """있던 고름 + 새 고름(None = 풀기) — 안 씀(미리 보기 · 835-1).""" + mine = dict(now) for k, v in (picks or {}).items(): if (p := _pick(v)) is None: mine.pop(str(k), None) else: mine[str(k)] = p + return mine + + +def write(folder: str | Path, fp: str, name: str, picks: dict) -> dict[str, dict]: + """종마다 고른 것(None = 고름 풀기)을 얹어 씀 → 지금 고름 전부.""" + now = read(folder, fp) + mine = merge(now["고름"], picks) if len(mine) > MAX_PICKS: raise store.StoreError(400, "고름이 너무 많음") path = _path(folder, fp) diff --git a/M02_MasterTemplete/M02_Sangeun_FromHopyo.py b/M02_MasterTemplete/M02_Sangeun_FromHopyo.py index 8f717ab09..9022a235d 100644 --- a/M02_MasterTemplete/M02_Sangeun_FromHopyo.py +++ b/M02_MasterTemplete/M02_Sangeun_FromHopyo.py @@ -6,6 +6,7 @@ (`M02_Doc_FromNative.emit` · 원본 `엑셀` 참) → 793-2 만들기(`M02_Sangeun_Imported.create` · 개인 층 · 엑셀 단가 그대로 · 경쟁사 로직 인정 — 근거 기초 제안 없음 · 809-1 · 요구 703) · 같은 내용 SK 가 있으면 그것(다시씀). +열기 = 미리 보기(`draft` · 층에 안 씀 · 835-1 · 요구 852-1) → [저장] = `make`(고친 문서째). 블록 번 = 판정 블록 번(STmate = 코드 「D00102」 숫자 · EST = 단가산출 블록 번). STmate 블록 끝전 = 파일 투표(`M02_San_Xlsx_Stmate.vote`). 원파일(.estx · .stc · 810-2 `M02_Doc_FromNative`) = 같은 자리 칸 `원파일`(둘 꼴) · 올리기 = 파일 하나 @@ -182,31 +183,69 @@ def _link_inner(source: Path, key: str, no: int, out: dict, trail: tuple) -> Non line["참조비목"] = em.COMP[res["비목"]] -def make(source: Path, key: str, no: int, name: str = "", _trail: tuple = ()) -> dict[str, Any]: - """지금 층(만들 층)에 가져온 식 SK — 같은 내용이 있으면 그 SK(`다시씀`) · - 안쪽 산근 블록은 형제 SK 를 먼저 만들어 참조(797-1).""" - from M02_MasterTemplete import M02_Sangeun_Imported as imp - +def _prep(source: Path, key: str, no: int) -> tuple[dict[str, Any], dict]: + """블록 → `{로직, 원본}`(가져온때 · 지문) + 그 파일 기계 구성 — 층에 안 씀.""" out, makes = _emit(source, key, no) out["원본"]["가져온때"] = utc_now_text() # 원문 패널 「어디서 왔나」(807-1) if fp := hopyo_store.read(source, key).get("지문"): # 827-3 그 파일 고름 · 이미 만든 SK 찾기 out["원본"]["지문"] = fp - _link_inner(source, key, no, out, _trail) - try: # 소요량 · 계수 후보 표지(797-2 · 사람 확인) — 못 셈 = 표지 없이 만듦 + return out, makes + + +def _flag(out: dict) -> None: + """소요량 · 계수 후보 표지(797-2 · 사람 확인) — 못 셈 = 표지 없이 만듦.""" + try: from M02_MasterTemplete import M02_Sangeun_QtyLinks as qty out["원본"]["표지"] += qty.flags(out["로직"]) except Exception: # noqa: BLE001 — 역색인 짓기 실패가 가져온 식 만들기를 막지 않음 pass + + +def _title(out: dict, no: int, name: str) -> str: + return str(name or "").strip() or str(out["로직"].get("이름") or f"단산{no}") + + +def draft(source: Path, key: str, no: int, name: str = "") -> dict[str, Any]: + """[가져온 식으로 열기] 미리 보기(835-1 · 요구 852-1) — `make` 와 같은 문서 · 층에 안 씀 + (안쪽 산근 줄 = 엑셀 값 · 이음은 [저장] 때 · 못 이은 기계 = 원본 `내기계예정`) · + 답 = {문서, 검산, 같은것(같은 내용 SK 키 | None)}.""" + from M02_MasterTemplete import M02_Sangeun_Imported as imp + + out, makes = _prep(source, key, no) + _flag(out) + mm.preview(out, makes) + return imp.draft(imp.new_doc(out["로직"], out["원본"], _title(out, no, name))) + + +def make( + source: Path, key: str, no: int, name: str = "", _trail: tuple = (), doc: dict | None = None +) -> dict[str, Any]: + """지금 층(만들 층)에 가져온 식 SK — 같은 내용이 있으면 그 SK(`다시씀`) · + 안쪽 산근 블록은 형제 SK 를 먼저 만들어 참조(797-1) · `doc` = 미리 보기에서 고친 문서([저장] · 835-1 — + 그 문서 로직 줄에 안쪽 참조 · 내 기계를 이어 저장).""" + from M02_MasterTemplete import M02_Sangeun_Imported as imp + + if doc is None: + out, makes = _prep(source, key, no) + else: + meta = doc.get(imp.KEY) if isinstance(doc, dict) else None + if not isinstance(meta, dict) or not isinstance(meta.get("로직"), dict): + raise store.StoreError(400, "가져온 식 문서 아님") + meta["원본"] = {**(meta.get("원본") or {}), "표지": list((meta.get("원본") or {}).get("표지") or [])} # fmt: skip + meta["원본"].pop("내기계예정", None) + out, makes = {"로직": meta["로직"], "원본": meta["원본"]}, _emit(source, key, no)[1] + _link_inner(source, key, no, out, _trail) + if doc is None: + _flag(out) # 고친 문서 = 미리 보기 때 붙은 표지 그대로 made = mm.attach(out, makes) # 821-4 못 이은 기계 = 내 기계(그 층 같은 것 · 새로) mine = {"내기계": made} if made else {} - title = str(name or "").strip() or str(out["로직"].get("이름") or f"단산{no}") + if doc is None: + doc = imp.new_doc(out["로직"], out["원본"], _title(out, no, name)) try: - got = imp.create(out["로직"], out["원본"], title) - return {**got, "다시씀": False, **mine} + return {**imp.keep(doc), "다시씀": False, **mine} except store.StoreError as e: same = (e.detail or {}).get("같은것") if isinstance(e.detail, dict) else None if e.status != 409 or not same: raise - got = imp.create(out["로직"], out["원본"], title, use=same[0]["키"]) - return {**got, "다시씀": True, **mine} + return {**imp.keep(doc, use=same[0]["키"]), "다시씀": True, **mine} diff --git a/M02_MasterTemplete/M02_Sangeun_Imported.py b/M02_MasterTemplete/M02_Sangeun_Imported.py index 899fc75dc..61ee815a2 100644 --- a/M02_MasterTemplete/M02_Sangeun_Imported.py +++ b/M02_MasterTemplete/M02_Sangeun_Imported.py @@ -562,11 +562,14 @@ def create( ) -> dict[str, Any]: # fmt: skip """엔진 로직 줄 + 원본 → 지금 층 새 가져온 식 SK — 서버가 다시 셈해 엑셀과 맞댐(검산 · 다르면 `같음` False 로 표지 · 막지 않음) · 같은 층 같은 내용 = 409(`ask` · 733-2).""" - from M02_MasterTemplete import M02_Labor_Ref as refs - from M02_MasterTemplete import M02_Library_Ask as ask + return keep(new_doc(logic, source, name, unit, head), new, use) - whole = _whole() - row = clean_row(logic, whole) + +def new_doc( + logic: Any, source: Any, name: str = "", unit: str = "", head: dict | None = None +) -> dict[str, Any]: + """가져온 식 SK 문서(키 없음 · 층에 안 씀).""" + row = clean_row(logic, _whole()) unit = str(unit or "").strip() or row["결과단위"].split("/", 1)[1] doc = {"키": "SK-000000", "이름": str(name or "").strip() or row["이름"], "단위": unit} doc |= {k: str((head or {}).get(k) or "") for k in ("구분", "상세구분")} @@ -575,6 +578,34 @@ def create( if source not in (None, {}): meta["원본"] = _meta(source, "원본") doc[KEY] = meta + return doc + + +def keep(doc: dict, new: bool = False, use: str | None = None) -> dict[str, Any]: + """문서 → 지금 층 SK(같은 내용 = 409 · `use` = 그것을 씀 · 안 씀).""" + from M02_MasterTemplete import M02_Labor_Ref as refs + from M02_MasterTemplete import M02_Library_Ask as ask + + whole = _whole() got = check(sd.clean(doc, None, whole), whole) key = ask.ask(sd.KIND, doc, new, use) or refs.create(sd.KIND, doc)["이름"] - return {"이름": key, "검산": got, "단가기준": "엑셀"} + return {"이름": key, "검산": got, "단가기준": (doc.get(KEY) or {}).get("단가기준") or "엑셀"} + + +def draft(doc: dict) -> dict[str, Any]: + """미리 보기(835-1 · 요구 852-1) — 층에 안 씀 · 답 = {문서, 검산, 같은것(같은 내용 SK 키 | None)}.""" + from M02_MasterTemplete import M02_Labor_Ref as refs + from M02_MasterTemplete import M02_Library_Ask as ask + from M02_MasterTemplete import M02_MasterTemplete_Store as store + + whole = _whole() + got = check(sd.clean(doc, None, whole), whole) + try: + ask.ask(sd.KIND, doc) + same = None + except store.StoreError as e: + rows = (e.detail or {}).get("같은것") if isinstance(e.detail, dict) else None + if e.status != 409 or not rows: + raise + same = rows[0]["키"] + return {"문서": refs.clean_doc(sd.KIND, doc), "검산": got, "같은것": same} diff --git a/M02_MasterTemplete/M02_Sangeun_Router.py b/M02_MasterTemplete/M02_Sangeun_Router.py index d5bcbf51c..98b4ec47e 100644 --- a/M02_MasterTemplete/M02_Sangeun_Router.py +++ b/M02_MasterTemplete/M02_Sangeun_Router.py @@ -5,7 +5,7 @@ 가져온 식(793-2 · `M02_Sangeun_Imported`) `…/sangeun/imported`(만들기 · 층 쓰기) · `/price` · `/link`(화면 캐시 문서 고침 · 저장 없음 · 층 읽기 · 기초로 바꾸기 없음 · 809-1) · 불러오기 단산 블록 → 가져온 식(793-3b · `M02_Sangeun_FromHopyo`) `…/sangeun/from-hopyo` -(만들 층 쓰기). +(만들 층 쓰기 · 몸 `문서` = 미리 보기에서 고친 문서) · 미리 보기 `…/from-hopyo/preview`(층 읽기 · 안 씀 · 835-1). 로직 사본(795-1 · `M02_Sangeun_Copy`) `…/sangeun/copy/price` [단가 갱신] · `/regen` [재생성] (화면 캐시 문서에 할일 얹음 · 저장 없음 · 층 읽기). 수식 탭(796-2b · `M02_Sangeun_Equation`) `…/sangeun/equation`(층 읽기) · `/preview`(고침 미리 셈 · @@ -164,6 +164,7 @@ class FromHopyoBody(BaseModel): 고름: dict[str, dict[str, Any] | None] | None = ( None # 827-3 판에서 저장 안 한 재료 고름(만들 때 저장) ) + 문서: dict[str, Any] | None = None # 835-1 미리 보기에서 고친 문서([저장] = 그 문서째 만듦) @router.post("/layers/{layer}/sangeun/from-hopyo") @@ -187,6 +188,28 @@ def sangeun_from_hopyo( with mat_pick.using(picks): return _sd(from_hopyo.make)(*args) + return _call(folder, run, source, body.키, body.번, body.이름, (), body.문서) + + +@router.post("/layers/{layer}/sangeun/from-hopyo/preview") +def sangeun_from_hopyo_preview( + layer: Layer, + body: FromHopyoBody, + owner: str | None = None, + session: Session = Depends(verify_session), +) -> dict: + """[가져온 식으로 열기] 미리 보기(835-1 · 요구 852-1) — 층에 안 씀(재료 고름도 메모리) · + 답 = {문서, 검산, 같은것(같은 내용 SK 키 | None)} · [저장] = `from-hopyo` 몸 `문서`.""" + folder = _root(session, layer, owner) + source = _root(session, body.층, body.주인) + mine = _root(session, "personal", None) + fp, _ = _call(source, mat_pick.fingerprint, source, body.키) + picks = mat_pick.merge(mat_pick.read(mine, fp)["고름"], body.고름) if fp else {} + + def run(*args: Any) -> Any: + with mat_pick.using(picks): + return _sd(from_hopyo.draft)(*args) + return _call(folder, run, source, body.키, body.번, body.이름) diff --git a/M02_MasterTemplete/logic/M02_MasterTemplete_Hopyo_SheetMap.ts b/M02_MasterTemplete/logic/M02_MasterTemplete_Hopyo_SheetMap.ts index 589d07cef..9f2a440da 100644 --- a/M02_MasterTemplete/logic/M02_MasterTemplete_Hopyo_SheetMap.ts +++ b/M02_MasterTemplete/logic/M02_MasterTemplete_Hopyo_SheetMap.ts @@ -6,7 +6,8 @@ * · 칠 = success 이어짐 · warning 이어짐 · 엑셀과 다름 · info 고를 것 · danger 못 이음 · muted 밖 + mine(내가 고름 띠) * · 「이어짐」 = 줄 표지 연결됨 · 로직 줄(809-1 `Hopyo_View` · 경쟁사 로직 인정) · 만든 SC · 못 이음 = 빨강 · 후보 = 파랑 * · 호표 머리 행 = 그 호표 줄 칠 중 가장 나쁜 것 · 블록 = 만든 SK 갈래 › 없으면 부르는 첫 줄 판정 「예상」 · 안 불림 = 밖 - * · 블록 안 줄(779-5) = 블록 `줄 [[행, 상태, 차]]`(Marks · D-2 줄 잇기 상태) → 그 출처 행만 칠 · 머리 행 = 줄 가장 나쁜 칠 · + * · 블록 안 줄(779-5) = 블록 `줄 [[행, 상태, 차]]`(Marks · D-2 줄 잇기 상태) → 그 출처 행만 칠(이어짐 초록은 안 칠함 — + * 출처 행 = 인력 · 기계 노무비 행이라 뜻 없음 · 835-1 · 요구 852-3) · 머리 행 = 줄 가장 나쁜 칠 · * 「엑셀과 다름」 = 이어졌는데 금액 참고 차만 있을 때 · 줄 재료 없으면 옛 블록 칠(범위 통째) * · 엑셀 셈 어긋남(778-5 D-6) = 블록 `어긋남`(Marks) → 상세 시트 행 차례 목록 — 칠 · 판정에 안 씀(띠 · 글만) * · 뛸 곳 = 일위대가 단산 줄 → 상세 블록 · 목록 행 · (여럿이면) 다른 부르는 줄 / 일위 줄 → 하위 호표 / 목록 행 → 상세 · 부르는 줄 / 상세 → 부르는 줄 · 목록 @@ -260,7 +261,7 @@ export function paintOf( const tones = mark.줄.map(([row, state, diff]) => { const tone = unitTone(state, diff); const more = diff && state === "이어짐" ? " · 엑셀 금액과 차(참고)" : ""; - if (a <= row && row <= b) put(detail, row, { tone, 글: `단산${no} 줄 · ${state} → ${WORD[tone]}${more}` }); // prettier-ignore + if (a <= row && row <= b && tone !== "success") put(detail, row, { tone, 글: `단산${no} 줄 · ${state} → ${WORD[tone]}${more}` }); // prettier-ignore return tone; }); const tone = worst(tones); diff --git a/resources/tester/helper_778_5_sk_pick.cjs b/resources/tester/helper_778_5_sk_pick.cjs index 7b0cd8e60..096120144 100644 --- a/resources/tester/helper_778_5_sk_pick.cjs +++ b/resources/tester/helper_778_5_sk_pick.cjs @@ -36,6 +36,9 @@ const STUBS = { }, }, "./M02_MasterTemplete_Hopyo_Imported": { openImported: (o) => opened.push(o) }, + "./M02_MasterTemplete_Hopyo_MatPick": { + createMatPick: () => ({ root: fakeEl(), ensure() {}, pending: () => undefined, reload() {}, dirty: () => false }), + }, "@config/config_frontend": { API_BASE_URL: "/api", ROUTES: {} }, "@ui/ui_template_elements": { createButton: (o) => ({ label: o.label, dataset: {}, hidden: false, click: () => o.onClick({ stopPropagation() {} }) }), @@ -107,8 +110,8 @@ async function sheetSk({ same, canWrite, picks }) { }); if (sheet !== "s1") { const row = rows().find((r) => r.opts.label.startsWith("단산8 ")); - row?.opts.onClick(); - row?.opts.trailing.kids.at(-2).click(); // [가져온 식으로 열기](만들 층) + row?.opts.onClick(); // 줄 고름 = 뛰기 + 개인 층 모달(835-1) + row?.opts.trailing.kids.at(-1).click(); // [가져온 식으로 열기](만들 층) } } return { diff --git a/resources/tester/test_627_sheet_sk.py b/resources/tester/test_627_sheet_sk.py index f4cb260ff..65bd93327 100644 --- a/resources/tester/test_627_sheet_sk.py +++ b/resources/tester/test_627_sheet_sk.py @@ -1,7 +1,8 @@ """780-2 지킴 — 엑셀 보기 단가산출표 · 상세 탭 = 오른쪽 단가산출 목록(요구 627). 서버 없이 잡음: 탭(역할) 따라 판 바꿈(일위대가표 = 호표 목록 · 단가산출 탭 = 단가산출 목록 · 칸은 처음 한 번만 지음) · -시트 행 → 목록 강조 · 목록 줄 → 지금 탭의 블록 자리 · 줄 끝 [가져온 식으로 열기](809-1 · 판정 블록 판 걷음) · +시트 행 → 목록 강조 · 목록 줄 → 지금 탭의 블록 자리 + 개인 층 가져온 식 모달(835-1 · [내 단가산출로] 단추 걷음) · +줄 끝 [가져온 식으로 열기](809-1 · 판정 블록 판 걷음) · [끝내기] = 막지 않고 물음창 글만 · 화면 = 부품 상자만 · 700 줄. """ @@ -53,22 +54,23 @@ def test_판_바꿈_목록_지음_강조_뛰기(): "단산8 거푸집", None, ] - # 목록 줄 누름 = 지금 탭의 자리(단가산출표 = 목록 행 · 상세 = 블록 범위) · 줄 끝 = 가져온 식 모달 + # 목록 줄 누름 = 지금 탭의 자리(단가산출표 = 목록 행 · 상세 = 블록 범위) + 모달 · 줄 끝 = 가져온 식 모달 assert got["jumps"] == [["s2", [8, 9]], ["s3", [8, 9]], ["s3", [8, 9]]] - assert got["opened"] == [{"no": 8, "canWrite": True, "to": "personal"}] * 3 + assert got["opened"] == [{"no": 8, "canWrite": True, "to": "personal"}] * 6 rows = {r["label"]: r for r in got["rows"]} assert rows["단산8 거푸집"]["sub"] == "㎡ · M02_JudgeCalls 1" - assert rows["단산8 거푸집"]["tail"] == [ - "M02_ToMySk" - ] # 만들 층 = 개인 → [가져온 식으로 열기] 숨김 + assert rows["단산8 거푸집"]["tail"] == [] # 만들 층 = 개인 → [가져온 식으로 열기] 숨김 · 저장 단추 없음 assert rows["단산9 잡석"]["sub"] == "㎥ · M02_JudgeOutside" - assert rows["단산10 운반"]["tail"] == ["success:M02_JudgeMade SK-000001", "M02_ToMySk"] + assert rows["단산10 운반"]["tail"] == ["success:M02_JudgeMade SK-000001"] assert got["count"] == {"블록": 2, "밖": 1, "줄": 0} # 만든 SK 뺌 def test_권한_그대로_넘김(): got = _node({"sheetSk": {"same": False, "canWrite": False, "picks": [["s2", "B5"]]}}) - assert got["opened"][0] == {"no": 8, "canWrite": False, "to": "company"} + assert got["opened"] == [ # 줄 고름 = 개인 층(누구나 씀) · 줄 끝 = 만들 층 권한 그대로 + {"no": 8, "canWrite": True, "to": "personal"}, + {"no": 8, "canWrite": False, "to": "company"}, + ] def _src(name: str) -> str: diff --git a/resources/tester/test_779_5_block_line_tone.py b/resources/tester/test_779_5_block_line_tone.py index 88a43222e..98b3b49d3 100644 --- a/resources/tester/test_779_5_block_line_tone.py +++ b/resources/tester/test_779_5_block_line_tone.py @@ -1,5 +1,6 @@ """779-5 블록 안 줄 칠(`logic/M02_MasterTemplete_Hopyo_SheetMap.ts` · Node) — 블록 `줄 [[행, 상태, 차]]` → -출처 행만 칠(이어짐 초록 · 차 있으면 주황 · 후보 파랑 · 못 이음 빨강 · 밖 흐림) · 머리 행 = 가장 나쁜 칠 · +출처 행만 칠(이어짐 초록은 안 칠함 — 출처 행 = 인력 · 기계 노무비 행 · 835-1 · 차 있으면 주황 · 후보 파랑 · +못 이음 빨강 · 밖 흐림) · 머리 행 = 가장 나쁜 칠 · 목록 행 = 머리와 같음 · 줄 재료 없으면 옛 블록 칠(범위 통째) · 실물(북부청 임도 · 영월) 줄 칠 수 = 단위 줄 수 (780-1 모든 블록 · 일위대가 밖 포함) · 상태 = 줄 잇기(D-2) · 참고 차 = STmate 옛 노임만.""" @@ -36,14 +37,14 @@ def test_줄_칠_머리_가장_나쁨_옛_칠(): lines = [[12, "이어짐", False], [13, "이어짐", True], [15, "고를 것", False], [16, "밖", False]] got = _node({"grid": GRID, "blocks": [_mark(1, lines), _mark(2)]})["paint"] detail, listing = got["s3"], got["s2"] - assert {r: detail[str(r)] for r in (12, 13, 15, 16)} == { - 12: "success", + assert {r: detail.get(str(r)) for r in (12, 13, 15, 16)} == { + 12: None, # 이어짐 초록 안 칠함(835-1) 13: "warning", 15: "info", 16: "muted", } assert detail["10"] == "info" and listing["5"] == "info" # 머리 · 목록 = 가장 나쁜 줄 - assert [r for r in range(10, 21) if str(r) in detail] == [10, 12, 13, 15, 16] # 줄 행만 + assert [r for r in range(10, 21) if str(r) in detail] == [10, 13, 15, 16] # 줄 행만 assert all(detail[str(r)] == "success" for r in range(21, 31)) # 줄 재료 없음 = 옛 칠 worst = _node( {"grid": GRID, "blocks": [_mark(1, [[12, "이어짐", False], [14, "못 이음", False]])]} @@ -78,7 +79,7 @@ def test_실물_줄_칠_수_단위_줄_수(tag, units): ) detail = _node({"grid": grid, "blocks": marks})["paint"][grid["보기"]["단가산출상세"]] heads = {b["행"][0] for b in grid["단산"]} - unit_rows = {x[0] for m in marks for x in m["줄"]} + unit_rows = {r for m in marks for r, s, d in m["줄"] if s != "이어짐" or d} # 초록 빼고(835-1) spans = [b["행"] for b in grid["단산"] if any(x["번"] == b["번"] and x["줄"] for x in marks)] painted = {int(r) for r in detail if any(x <= int(r) <= y for x, y in spans)} - heads assert unit_rows - heads == painted and want == sum(len(m["줄"]) for m in marks) diff --git a/resources/tester/test_793_3b_import_screen.py b/resources/tester/test_793_3b_import_screen.py index 08004334b..c6f08a4d7 100644 --- a/resources/tester/test_793_3b_import_screen.py +++ b/resources/tester/test_793_3b_import_screen.py @@ -104,10 +104,11 @@ def test_단산_줄_길_잇기(): sk, view = _src("Hopyo_SheetSk"), _src("Hopyo_View") assert "openImported({" in sk and "dataset.skOpen" in sk # 단가산출 탭 목록 = 가져온 식 모달 assert ( - '"/sangeun/from-hopyo"' in view and "openSkEdit(" in view - ) # [수정] 단산 줄 = 수식 탭 모달 + '"/sangeun/from-hopyo"' not in view and "openImported({" in view + ) # [수정] 단산 줄 = 가져온 식 모달(미리 보기 · 835-1) imp = _src("Hopyo_Imported") - assert '"/sangeun/from-hopyo"' in imp and "/sangeun/imported/${how}" in imp + assert '"/sangeun/from-hopyo/preview"' in imp and '"/sangeun/from-hopyo"' in imp + assert "/sangeun/imported/${how}" in imp assert "mountSangeunDetail(" in imp and "saveTemplate(" in imp and "openModal({" in imp for how in ('run("price"', 'run("link"'): assert how in imp, how diff --git a/resources/tester/test_809_1_import_labor.py b/resources/tester/test_809_1_import_labor.py index dfb0f189b..738905254 100644 --- a/resources/tester/test_809_1_import_labor.py +++ b/resources/tester/test_809_1_import_labor.py @@ -115,7 +115,9 @@ def test_logic_line_is_imported_sk(world) -> None: def test_modal_is_labor_table() -> None: view, sheet, table = _ts("Hopyo_View"), _ts("Hopyo_Sheet"), _ts("Labor_Table") assert "mountLaborTable(host, r.문서" in view and "readOnly: true" in view - assert 'mode: "precombo"' in view and "openSkEdit(" in view and "mineButton(" in view + assert ( + 'mode: "precombo"' in view and "openImported({" in view and "mineButton(" in view + ) # 835-1 assert "/hopyo-view/" in view and "x_mark" in view and "x_excel" in view assert "openHopyoView({" in sheet and "openHopyoCheck" not in sheet assert "opt.view?.onRow?.(n)" in table and "viewRows(g)" in table # 덧칸 · 읽기만 diff --git a/resources/tester/test_835_1_import_preview.py b/resources/tester/test_835_1_import_preview.py new file mode 100644 index 000000000..194a11bb3 --- /dev/null +++ b/resources/tester/test_835_1_import_preview.py @@ -0,0 +1,79 @@ +# ruff: noqa: F811 — 임시 층 fixture(world)를 test_m02_labor_ref 에서 받아 씀 +"""835-1 불러오기 보기 = 쓰기 없음(요구 852) — 임시 storage · M01 그대로. + +① 가져온 식 미리 보기(`FromHopyo.draft`) = 층 파일 0 바뀜 · [저장](`make` 몸 문서) = 그때 SK · + 옛 길(`make` 문서 없음)과 같은 내용(같은 SK 다시 씀 = 금액 · 이음 그대로) · 다시 보기 = 같은것 키 +② 화면 — 보기 길은 미리 보기만 부름(만들기는 [저장]) · 목록 줄 저장 단추 없음 · 고르면 모달 · + 모달 큰 중간 창(1400) · 산출근거 표 비목 줄 금액 칸 +실무 엑셀 없으면 ① skip. +""" + +from __future__ import annotations + +import re +from pathlib import Path + +import helper_m02_hopyo_golden as golden +import pytest + +from M02_MasterTemplete import M02_Hopyo_Excel as hx +from M02_MasterTemplete import M02_Hopyo_Store as hs +from M02_MasterTemplete import M02_MasterTemplete_Store as store +from M02_MasterTemplete import M02_Sangeun_FromHopyo as fh +from resources.tester.test_m02_labor_ref import world # noqa: F401 — 같은 임시 층 + +ROOT = Path(__file__).resolve().parents[2] +M02 = ROOT / "M02_MasterTemplete" + + +def _files(folder: Path) -> dict[str, int]: + return {str(p.relative_to(folder)): p.stat().st_mtime_ns for p in folder.rglob("*") if p.is_file()} # fmt: skip + + +def test_preview_writes_nothing_save_makes_same(world) -> None: + src = golden.FILES.get("봉화") + if src is None or not src.exists(): + pytest.skip("실무 엑셀 없음 봉화") + folder, data = world["personal"], src.read_bytes() + with store.at(folder): + parsed = hx.read_book(src.name, data) + row = hs.save(folder, src.name, parsed, {"이름": "sub1임시"}, None, hs.fingerprint(data)) + fh.keep(folder, row["키"], src.name, data) + with store.at(folder): + before = _files(folder) + seen = fh.draft(folder, row["키"], 3) # 단산3 깬잡석채집 — 기계 줄 + assert _files(folder) == before # 보기 = 디스크 쓰기 0 + assert seen["같은것"] is None and seen["문서"]["가져온"]["로직"]["호표"] + assert seen["검산"]["같음"] is True + made = fh.make(folder, row["키"], 3, doc=seen["문서"]) # [저장] + assert made["다시씀"] is False and (folder / "sangeun" / f"{made['이름']}.json").is_file() + old = fh.make(folder, row["키"], 3) # 옛 길 = 같은 내용 → 같은 SK + assert old["다시씀"] is True and old["이름"] == made["이름"] + assert old["검산"] == made["검산"] + again = fh.draft(folder, row["키"], 3) + assert again["같은것"] == made["이름"] + + +def _src(name: str) -> str: + return (M02 / f"M02_MasterTemplete_Hopyo_{name}.ts").read_text(encoding="utf-8") + + +def test_screen_view_paths_do_not_save() -> None: + imported = _src("Imported") + load = imported.split("async function load()", 1)[1].split("openModal(", 1)[0] + assert "previewImported(o)" in load and "fromHopyo(" not in load # 열기 = 미리 보기 + save = imported.split("async function save()", 1)[1].split("const saveBtn", 1)[0] + assert "fromHopyo(o, doc)" in save # 만들기 = [저장] + view = _src("View") + assert "/sangeun/from-hopyo" not in view and "openImported(" in view + sk = _src("SheetSk") + assert "M02_ToMySk" not in sk and "skMine" not in sk + assert re.search(r"pick\(b\.번\);\s*open\(b\.번, true\)", sk) # 고르면 모달 + sheet = _src("Sheet") + assert "mineButton" not in sheet and 'addEventListener("click", () => openView(h))' in sheet + for name in ("Imported", "View"): + assert 'width: "1400px"' in _src(name), name + css = (ROOT / "ui_template" / "ui_template_modal.css").read_text(encoding="utf-8") + assert '.ui-modal.ui-modal--wide[data-width="1400px"]' in css + out = (ROOT / "A00_Common" / "line_stack_out.ts").read_text(encoding="utf-8") + assert "cols: { sum: part, [c]: part }" in out # 비목 줄 금액 칸(엑셀과 같음) diff --git a/ui_template/ui_template_modal.css b/ui_template/ui_template_modal.css index 19cc2746f..5eb110914 100644 --- a/ui_template/ui_template_modal.css +++ b/ui_template/ui_template_modal.css @@ -94,6 +94,12 @@ width: min(1100px, 94vw); height: var(--modal-max); } +/* 큰 중간 창(835-1) — wide 꼴 + 1400 × 880 */ +.ui-modal.ui-modal--wide[data-width="1400px"] { + --modal-max: min(880px, 90vh); + width: min(1400px, 94vw); + height: var(--modal-max); +} /* 확인창 글 — 줄바꿈( ) 그대로(쓰는 곳 목록 · 바뀐 자원 줄) */ .ui-confirm__message { diff --git a/ui_template/ui_template_modal.ts b/ui_template/ui_template_modal.ts index 1a7b396b0..760a356a0 100644 --- a/ui_template/ui_template_modal.ts +++ b/ui_template/ui_template_modal.ts @@ -2,7 +2,7 @@ * ui_template_modal.ts * 부품 「모달 · 확인창」(552-3 · 목록 22) — 화면을 가리고 위에 뜨는 창. * 꼴 = 모달(`openModal` · 제목줄 · 닫기 · 몸은 `mount`) · 넓음(`wide` · 96vw × 94vh) · 폭 단(`width`: 24rem · 40rem · 960px · 기본 720) - * · 넓은 중간 창(`wide` + `width: "1100px"` · 1100 × 680 · 몸 넘침만 스크롤) + * · 넓은 중간 창(`wide` + `width: "1100px"` · 1100 × 680 · 몸 넘침만 스크롤) · 큰 중간 창(`"1400px"` · 1400 × 880) * · 확인창(`askConfirm`) · 위험 확인(`danger` · 확인 단추 빨강 = `showConfirmDialog`) · 글 묻기(`askText` · window.prompt 대신) * · 갈래 묻기(`askChoice` · 취소 + 단추 여럿 · 고른 값 · 취소 = null — 733-3 「그것을 씀 / 그래도 새로」) * 크기 = 모달 여백 16 · 둥글기 12 · 확인창 폭 300 ~ 420 · 단추 = 단추 부품 기본 @@ -28,8 +28,8 @@ export interface ModalOptions { actions?: HTMLElement[]; /** 큰 창 — 96vw × 94vh · 몸 넘침만 스크롤 */ wide?: boolean; - /** 폭 단 — 없으면 720 · 1100px 은 `wide` 와 함께(높이 680) */ - width?: "24rem" | "40rem" | "960px" | "1100px"; + /** 폭 단 — 없으면 720 · 1100px · 1400px 은 `wide` 와 함께(높이 680 · 880) */ + width?: "24rem" | "40rem" | "960px" | "1100px" | "1400px"; backdropClass?: string; /** 닫기 단추 · Esc · 바깥 누르기 전에 물음 — false 면 안 닫음(몸의 `close` 는 묻지 않고 닫음) */ beforeClose?: () => boolean | Promise;