Merge remote-tracking branch 'origin/sub_laptop_1' into main_laptop_1

This commit is contained in:
2026-11-15 11:54:38 +09:00
18 changed files with 317 additions and 105 deletions
+1
View File
@@ -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);
@@ -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<Made> =>
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<Draft> =>
call(at(o.target, "/sangeun/from-hopyo/preview"), { method: "POST", body: fromBody(o) });
/** [저장] 처음 — 고친 문서째 SK 만듦 */
const fromHopyo = (o: ImportedOptions, doc: ImportedDoc): Promise<Made> =>
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<boolean> {
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<void> {
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],
@@ -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;
}),
@@ -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;
@@ -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<void> {
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();
+10 -4
View File
@@ -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)
+51 -12
View File
@@ -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}
+36 -5
View File
@@ -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}
+24 -1
View File
@@ -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.이름)
@@ -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);
+5 -2
View File
@@ -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 {
+10 -8
View File
@@ -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:
@@ -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)
@@ -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
+3 -1
View File
@@ -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 # 덧칸 · 읽기만
@@ -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 # 비목 줄 금액 칸(엑셀과 같음)
+6
View File
@@ -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 {
+3 -3
View File
@@ -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<boolean>;