Merge remote-tracking branches 'origin/sub_laptop_1', 'origin/sub_laptop_2', 'origin/sub_laptop_3', 'origin/sub_laptop_5' and 'origin/sub_laptop_7' into main_laptop_1

This commit is contained in:
43 changed files with 4769 additions and 161 deletions
+16 -1
View File
@@ -40,8 +40,20 @@ def _logics(codes: list[str]) -> tuple[list[dict], list[str]]:
return found, missing
def _labels(whole: Any, spec: dict) -> dict[str, str]:
"""고르기 원문번호 → 보이는 글(85-1) — 기계 「이름 규격」 · 직종(입력 이름에 「직종」) 「이름」 · 모르는 번호는 뺌."""
table = "LB" if "직종" in str(spec.get("이름") or "") else "EQ"
rows = whole.numbers.get(table, {})
out = {}
for value in spec.get("고르기") or []:
row = rows.get(str(value)) if isinstance(value, str) else None
if row:
out[value] = " ".join(str(row[k]) for k in ("이름", "규격") if row.get(k))
return out
def _listed(logics: list[dict]) -> list[dict[str, Any]]:
"""로직 줄마다 `{로직, 이름, 결과단위, 출처, 입력: [{이름, 설명, 단위, 고르기?, 범위?, 자동값}]}`."""
"""로직 줄마다 `{로직, 이름, 결과단위, 출처, 입력: [{이름, 설명, 단위, 고르기?, 고르기이름?, 범위?, 자동값}]}`."""
whole = m01.loaded()[1]
out = []
for logic in logics:
@@ -51,6 +63,9 @@ def _listed(logics: list[dict]) -> list[dict[str, Any]]:
one = {k: spec[k] for k in _SPEC if k in spec}
one.setdefault("설명", None)
one.setdefault("단위", None)
labels = _labels(whole, spec)
if labels:
one["고르기이름"] = labels
specs.append({**one, "자동값": auto.get(spec["이름"])})
out.append(
{
+14 -3
View File
@@ -13,6 +13,7 @@ import {
choicesOf,
inputKey,
isOutside,
labelOf,
rangeOf,
storedOf,
type ValueDoc,
@@ -65,7 +66,12 @@ export function createValueControl(
if (control instanceof HTMLSelectElement) {
control.append(
...(auto === null ? [el("option", { attrs: { value: "" }, text: "—" })] : []),
...choices.map((c) => el("option", { attrs: { value: c.값 }, text: c.이름 })),
...choices.map((c) =>
el("option", {
attrs: { value: c.값, ...(c.이름 !== c.값 ? { title: c.값 } : {}) },
text: c.이름,
}),
),
);
}
const empty = value === null || value === "";
@@ -194,10 +200,15 @@ export function createInputsValueCard(opts: InputsValueOptions): InputsValueHand
};
if (opts.mode === "view") {
const text = isEmpty(stored.값) ? auto : stored.값;
const label = text === null ? "—" : labelOf(input, text);
const shown = el("span", {
className: `m02-inputval__value${isEmpty(stored.값) ? " is-auto" : ""}`,
text: text === null ? "—" : String(text),
attrs: isEmpty(stored.값) ? { title: VALUE_TEXT.auto } : {},
text: label,
attrs: isEmpty(stored.값)
? { title: VALUE_TEXT.auto }
: label !== String(text)
? { title: String(text) }
: {},
});
paintState();
return el("div", { className: "m02-inputval__cell", children: [shown, state] });
+12 -3
View File
@@ -14,9 +14,14 @@ const text = (value: unknown): string =>
? JSON.stringify(value)
: String(value);
/** 값 → 보이는 글(85-1) — 원문번호면 「이름 규격」 · 그 밖은 값 그대로 */
export const labelOf = (input: ItemInput, value: unknown): string =>
input.고르기이름?.[text(value)] ?? text(value);
/** 고르기 · 범위 표시 — 선택지 목록이면 「a / b / c」 · 범위(배열 둘 · {최소, 최대})면 「최소 ~ 최대」 */
export function valueHint(input: ItemInput): string {
if (Array.isArray(input.고르기) && input.고르기.length) return input.고르기.map(text).join(" / ");
if (Array.isArray(input.고르기) && input.고르기.length)
return input.고르기.map((one) => labelOf(input, one)).join(" / ");
const range = input.범위;
if (Array.isArray(range) && range.length >= 2) return `${text(range[0])} ~ ${text(range[1])}`;
if (range && typeof range === "object") {
@@ -60,7 +65,11 @@ export function choicesOf(input: ItemInput): { 값: string; 이름: string; raw:
const object =
typeof one === "object" && one !== null ? (one as Record<string, unknown>) : null;
const raw = (object ? (object.값 ?? object.이름 ?? "") : one) as string | number;
return { 값: String(raw), 이름: object ? String(object.이름 ?? raw) : String(one), raw };
return {
값: String(raw),
이름: object ? String(object.이름 ?? raw) : labelOf(input, one),
raw,
};
});
}
@@ -112,7 +121,7 @@ export function valueLine(
for (const input of inputs) {
const value = values?.[input.이름];
if (value !== undefined && value !== "") {
parts.push(`${input.이름} ${value}${input.단위 ? input.단위 : ""}`);
parts.push(`${input.이름} ${labelOf(input, value)}${input.단위 ? input.단위 : ""}`);
} else if (autoText(input) === "") empty += 1;
}
return { text: parts.join(" · "), empty };
@@ -330,6 +330,8 @@ export interface ItemInput {
설명?: string;
단위?: string;
고르기?: unknown[];
/** 고르기 원문번호 → 보이는 글(85-1 · 기계 「이름 규격」 · 직종 이름) */
고르기이름?: Record<string, string>;
범위?: unknown;
자동값?: unknown;
}
@@ -12,6 +12,7 @@
* ========================================================================== */
import { createButton, el, showConfirmDialog } from "@ui/ui_template_elements";
import { leaveOrStay } from "@ui/ui_template_leave_dialog";
import { attachCollapsible } from "@ui/ui_template_collapsible";
import { readStateRaw, writeStateRaw } from "../A00_Common/b_page_state";
import type {
@@ -68,7 +69,7 @@ const TEXT = {
noFieldNumber: "{value} 없음",
noSheet: "스프레드시트를 못 띄움 — {value}",
readFailed: "{value} 파일을 못 읽음 — {why}",
discard: "카드에 저장 안 한 고침이 있음 — 버리고 다른 항목을 열까요?",
discard: "카드에 저장 안 한 고침이 있음 — 버리고 넘어갈까?",
pills: "알약",
pillsTip: "아래 알약 줄 보임 · 숨김",
take: "가져오기",
@@ -179,6 +180,9 @@ export function createItemCards(opts: ItemCardsOptions): ItemCardsHandle {
const numberOf = (kind: CardKind, id: string): string => opts.numberOf?.(kind, id) ?? "";
const anyDirty = (): boolean => cards.some((c) => c.saver.dirty());
const saveAll = async (): Promise<void> => {
for (const c of cards) if (c.saver.dirty() && !(await c.saver.run())) throw new Error();
};
const reload = (): void => void load(column ? { id: column, label } : null, true);
/** 카드 틀 하나 — 제목 옆 = ? · 제목 줄 오른쪽 = [저장] · [〇〇 수정](빈 단추 한 모양) · 부제 = 번호 · 이름 */
@@ -461,7 +465,12 @@ export function createItemCards(opts: ItemCardsOptions): ItemCardsHandle {
async function load(col: { id: string; label: string } | null, again = false): Promise<void> {
// 카드 고침을 저장 안 했으면 버릴지 물음(한 번) — 취소면 보던 열 그대로
if (!again && anyDirty() && col?.id !== column && !(await showConfirmDialog(TEXT.discard)))
if (
!again &&
anyDirty() &&
col?.id !== column &&
(await leaveOrStay(TEXT.discard, { save: saveAll, stillDirty: anyDirty })) === "stay"
)
return;
const mine = ++seq;
drop();
@@ -530,9 +539,7 @@ export function createItemCards(opts: ItemCardsOptions): ItemCardsHandle {
},
column: () => column,
dirty: anyDirty,
saveAll: async () => {
for (const c of cards) if (c.saver.dirty() && !(await c.saver.run())) throw new Error();
},
saveAll,
discard: () => {
drop();
opts.onDirty?.(false);
@@ -98,7 +98,7 @@ export interface LaborDiagramSource {
로직: string;
이름: string;
결과단위?: string;
입력: { 이름: string; 단위?: string; 자동값?: unknown }[];
입력: { 이름: string; 단위?: string; 자동값?: unknown; 고르기이름?: Record<string, string> }[];
}[];
/** calc 답 · 아직이면 null */
calc: {
@@ -137,10 +137,11 @@ export function buildLaborDiagram(src: LaborDiagramSource): DiagramData {
inputs: (def?.입력 ?? []).map((x) => {
const mine = r.입력값[x.이름];
const has = mine !== undefined && mine !== "";
const raw = has ? String(mine) : autoOf(x.자동값);
return {
name: x.이름,
unit: x.단위 ?? "",
value: has ? String(mine) : autoOf(x.자동값),
value: x.고르기이름?.[raw] ?? raw, // 85-1 원문번호 → 「이름 규격」
...(has || !autoOf(x.자동값) ? {} : { auto: true }),
};
}),
@@ -14,7 +14,8 @@
* 글자는 이 파일에 둠(도면 부품 `M02_MasterTemplete_Drawing.ts` 와 같은 방식).
* ========================================================================== */
import { showConfirmDialog, showToast } from "@ui/ui_template_elements";
import { showToast } from "@ui/ui_template_elements";
import { leaveOrStay } from "@ui/ui_template_leave_dialog";
import type { LinkOptions } from "../A00_Common/spreadsheet/spreadsheet_links";
import {
clearPart,
@@ -252,9 +253,12 @@ export function createStructurePanel(opts: StructurePanelOptions): StructurePane
if ((col?.id ?? null) !== column && cards.dirty()) {
// 카드에 저장 안 한 고침 — 버릴지 물음 · 머물면 표의 고른 열을 되돌림
const from = column;
void showConfirmDialog(TEXT.discardCards).then((ok) => {
if (!ok) return void (from && opts.keepColumn?.(from));
cards.discard(); // 한 번 물었음 — 카드가 다시 묻지 않게
void leaveOrStay(TEXT.discardCards, {
save: cards.saveAll,
stillDirty: cards.dirty,
discard: cards.discard, // 한 번 물었음 — 카드가 다시 묻지 않게
}).then((got) => {
if (got === "stay") return void (from && opts.keepColumn?.(from));
void load(col);
});
return;
@@ -272,7 +276,12 @@ export function createStructurePanel(opts: StructurePanelOptions): StructurePane
const toNo = to && numbers[to.id]?.[PART_FIELD[kind]];
if (!to || !toNo) return void showToast(TEXT.saveFirst, "error");
// 카드에 저장 안 한 고침 — 가져온 뒤 카드를 다시 읽어 버려지니 먼저 물음
if (cards.dirty() && !(await showConfirmDialog(TEXT.discardCards))) return;
if (
cards.dirty() &&
(await leaveOrStay(TEXT.discardCards, { save: cards.saveAll, stillDirty: cards.dirty })) ===
"stay"
)
return;
const target = `${toNo} ${columnHead(to)}`;
const here = { layer: getActiveLayer(), owner: getActiveOwner() };
const same = (from: From): boolean => from.layer === here.layer && from.owner === here.owner;
@@ -1,6 +1,6 @@
/* =============================================================================
* M02_MasterTemplete_TreePanel_Dialogs.ts
* 구조물 라이브러리 오른쪽 클릭 메뉴가 띄우는 작은 창 — 글 받기 · 옮길 폴더 고르기 · 저장/버림 물음.
* 구조물 라이브러리 오른쪽 클릭 메뉴가 띄우는 작은 창 — 글 받기 · 옮길 폴더 고르기.
* 모두 약속(Promise)으로 답함 — 닫기 · Esc · 바깥 누르기 = 취소(null / "cancel").
* ========================================================================== */
@@ -183,36 +183,3 @@ export function askFolder(
});
});
}
export type Settle = "save" | "discard" | "cancel";
/** 저장 안 한 표 고침 — 저장하고 계속 · 버리고 계속 · 취소 */
export function askSettle(): Promise<Settle> {
return new Promise((resolve) => {
let done = false;
const finish = (value: Settle): void => {
if (done) return;
done = true;
resolve(value);
};
openModal({
title: L("M02_TreeUnsaved"),
closeLabel: L("M02_TreeCancel"),
dialogClass: "m02-modal m02-treedlg",
beforeClose: () => (finish("cancel"), true),
mount: (body, close) => {
const choose = (value: Settle) => () => {
finish(value);
close();
};
const save = createButton({ label: L("M02_TreeSaveGo"), variant: "filled" });
const discard = createButton({ label: L("M02_TreeDiscardGo"), variant: "danger" });
const cancel = createButton({ label: L("M02_TreeCancel"), variant: "ghost" });
save.addEventListener("click", choose("save"));
discard.addEventListener("click", choose("discard"));
cancel.addEventListener("click", choose("cancel"));
body.append(el("div", { className: "m02-side__row", children: [save, discard, cancel] }));
},
});
});
}
@@ -6,6 +6,7 @@
import { setLeaveGuard } from "../A00_Common/router";
import { createButton, el, showConfirmDialog, showToast } from "@ui/ui_template_elements";
import { leaveOrStay } from "@ui/ui_template_leave_dialog";
import { loadingError, showLoading } from "@ui/ui_template_loading";
import { t as L } from "@ui/ui_template_locale";
import {
@@ -26,7 +27,6 @@ import {
type StructureColumn,
type StructurePanelHandle,
} from "./M02_MasterTemplete_Structure";
import { askSettle } from "./M02_MasterTemplete_TreePanel_Dialogs";
import { buildAreaPanel, type AreaPanel } from "./M02_MasterTemplete_AreaPanel";
import { createAreaToDrawing, sentRanges } from "./M02_MasterTemplete_AreaToDrawing";
import { detailHelp, HELP_TITLE } from "./M02_MasterTemplete_CardHelp";
@@ -202,19 +202,26 @@ export function createMain(
event.returnValue = "";
};
window.addEventListener("beforeunload", guard);
const confirmLeave = async (): Promise<boolean> => {
if (!busy()) return true; // 카드 고침(도면 · 엑셀)도 같은 물음
if (!(await showConfirmDialog(L("M02_DiscardConfirm")))) {
// 취소 — 초점을 시트(고른 칸 입력 자리) · 집계표 · 도면으로 돌려 키보드 그대로
host
.querySelector<HTMLElement>(".ss-input, .ui-sheet, iframe")
?.focus({ preventScroll: true });
return false;
}
dirty = false;
toDrawing.drop();
return true;
/**
* 저장 안 한 고침 물음(떠나기 · 나무 고치기 한 몸) — 단추 셋 [취소] [버리고 넘어감] [저장 후 넘어감].
* "clean" = 고침 없음 · "saved" · "discarded" · false = 머묾(취소 · 저장 실패 · 판 다름 · 카드 저장 실패).
* 열린 양식이 없는 화면(옵션 정의 등)은 저장할 것이 없어 2단추 그대로.
*/
const askUnsaved = async (message: string): Promise<false | "clean" | "saved" | "discarded"> => {
if (!busy()) return "clean"; // 카드 고침(도면 · 엑셀)도 같은 물음
const drop_ = (): void => {
dirty = false;
toDrawing.drop();
};
let got: "saved" | "discarded" | "stay";
if (sel) got = await leaveOrStay(message, { save: doSave, stillDirty: busy, discard: drop_ });
else got = (await showConfirmDialog(message)) ? (drop_(), "discarded") : "stay";
if (got !== "stay") return got;
// 머묾 — 초점을 시트(고른 칸 입력 자리) · 집계표 · 도면으로 돌려 키보드 그대로
host.querySelector<HTMLElement>(".ss-input, .ui-sheet, iframe")?.focus({ preventScroll: true });
return false;
};
const confirmLeave = async (): Promise<boolean> => !!(await askUnsaved(L("M02_DiscardConfirm")));
setLeaveGuard(confirmLeave); // 머리 메뉴 · 뒤로 가기로 떠날 때도 물음
const showNotice = (nodes: (HTMLElement | string)[]): void => {
@@ -573,15 +580,9 @@ export function createMain(
}
async function settle(): Promise<false | "clean" | "changed"> {
if (!busy()) return "clean";
const choice = await askSettle();
if (choice === "cancel") return false;
if (choice === "save") {
await doSave();
return dirty ? false : "changed"; // 저장이 막히면(판 다름 등) 나무도 안 고침
}
dirty = false; // 버림 — 화면을 서버 그대로 다시 읽음
if (sel) await open(sel);
const got = await askUnsaved(L("M02_TreeUnsaved"));
if (got === false || got === "clean") return got;
if (got === "discarded" && sel) await open(sel); // 버림 — 화면을 서버 그대로 다시 읽음
return "changed";
}
+4 -4
View File
@@ -194,11 +194,11 @@ def _label(key: str, value: Any) -> str:
def _length(item: dict[str, Any]) -> float | None:
stated = _number(item["options"].get("length_m"))
if stated:
return stated
# 구간형은 시작~종료가 먼저(노면공 87-2) — 옛 length_m 은 범위를 고쳐도 안 따라옴
start, end = _number(item.get("start_m")), _number(item.get("end_m"))
return abs(end - start) if start is not None and end is not None else None
if start is not None and end is not None:
return abs(end - start)
return _number(item["options"].get("length_m")) or None
def _source_value(source: dict[str, Any], item: dict[str, Any]) -> float | None: