Compare commits
102
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d39eece45e | ||
|
|
53aeccc179 | ||
|
|
389ab245e7 | ||
|
|
87433105f7 | ||
|
|
eb64bf64ac | ||
|
|
ef5fb0632f | ||
|
|
fcf5f457f0 | ||
|
|
1fee1e950c | ||
|
|
e699d95857 | ||
|
|
ef3cc96509 | ||
|
|
af2d9826cf | ||
|
|
abe722f378 | ||
|
|
944f52cd6f | ||
|
|
fd188fdf2b | ||
|
|
89292a75df | ||
|
|
e5e34d82fb | ||
|
|
53a33b07c1 | ||
|
|
ebcec5ffa0 | ||
|
|
47f4a729b4 | ||
|
|
640903d6ef | ||
|
|
1beaefa529 | ||
|
|
2885a51854 | ||
|
|
db1d976ea8 | ||
|
|
32208c3577 | ||
|
|
8406fd2367 | ||
|
|
5799ccbe90 | ||
|
|
5e72aff136 | ||
|
|
9eae77d8f2 | ||
|
|
c530ac694c | ||
|
|
cc1c1c659d | ||
|
|
132371f2f3 | ||
|
|
36589d1a36 | ||
|
|
fb54335811 | ||
|
|
7d6d9769d5 | ||
|
|
041d4e653e | ||
|
|
fd6e2e6933 | ||
|
|
aac94d6508 | ||
|
|
c788960bf2 | ||
|
|
f6e7095e3e | ||
|
|
d02fa37723 | ||
|
|
8d4d23e783 | ||
|
|
1d1f27bdff | ||
|
|
7d9837a081 | ||
|
|
94c7d29b11 | ||
|
|
07a292a009 | ||
|
|
2bcb8bf9b9 | ||
|
|
600d7b0ab1 | ||
|
|
f915e0c33b | ||
|
|
ba95a6d606 | ||
|
|
fb460cd0a3 | ||
|
|
20ef5a6a7f | ||
|
|
d2a8889475 | ||
|
|
cb0a4fbfec | ||
|
|
46b3c93f66 | ||
|
|
083ad3b46b | ||
|
|
2cf1f17c2a | ||
|
|
539aa04712 | ||
|
|
83d3d43f10 | ||
|
|
b6a3392bc4 | ||
|
|
ad01861c48 | ||
|
|
db02fa9994 | ||
|
|
cfe78e2e3b | ||
|
|
12950205f6 | ||
|
|
0098ba58e2 | ||
|
|
078fe9034f | ||
|
|
9787b4b1f6 | ||
|
|
7422a70ac4 | ||
|
|
c1a4b9a178 | ||
|
|
448b92276f | ||
|
|
5302684c93 | ||
|
|
4f33cafb12 | ||
|
|
0f635cef0f | ||
|
|
eb3dba0e9e | ||
|
|
5f9b5432ec | ||
|
|
e4b7322bb6 | ||
|
|
95c7acb7d4 | ||
|
|
cffeb84ce4 | ||
|
|
b3bd093d42 | ||
|
|
3a10256e69 | ||
|
|
35d3cc936c | ||
|
|
a0461bf042 | ||
|
|
bc41c8716f | ||
|
|
50d66fae78 | ||
|
|
2f20e29d31 | ||
|
|
1d02b0205a | ||
|
|
ab5a07ff34 | ||
|
|
a8c20c3f19 | ||
|
|
f20cbe44d1 | ||
|
|
922c9a05a1 | ||
|
|
4e50e2fe94 | ||
|
|
b279b964ce | ||
|
|
b6d98ac9d5 | ||
|
|
9380080afe | ||
|
|
19d7093050 | ||
|
|
8c1da2c29e | ||
|
|
4c6bb9d70a | ||
|
|
864ff1f55b | ||
|
|
6cea0c97f2 | ||
|
|
042d574fee | ||
|
|
19097b7255 | ||
|
|
c03929e369 | ||
|
|
170086a5b1 |
@@ -73,7 +73,7 @@ const TEXT = {
|
||||
Before_After: ["계", "Sum"],
|
||||
Job_Done: ["적용함", "Applied"],
|
||||
Dirty_First: [
|
||||
"저장 안 한 고침이 있음 — [수식 저장] 또는 [고침 버림] 먼저",
|
||||
"저장 안 한 고침이 있음 — [저장] 또는 [되돌림] 먼저",
|
||||
"Save or discard edits first",
|
||||
],
|
||||
No_Key: [
|
||||
|
||||
+103
-20
@@ -38,7 +38,21 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 산출근거(요구 683) — 왼쪽을 내려도 화면에 고정 · 길면 자기 스크롤(높이 = 스크롤 영역 · fit()) */
|
||||
/* 합계 상자(요구 687-3) — 스크롤해도 화면 아래 고정 */
|
||||
.lstk__foot {
|
||||
position: sticky;
|
||||
inset-block-end: 0;
|
||||
z-index: 2;
|
||||
padding-block: var(--spacing-8);
|
||||
background: var(--color-surface);
|
||||
border-block-start: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.lstk__foot:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 산출근거(요구 683) — 왼쪽을 내려도 화면에 고정 · 길면 자기 스크롤(높이 = 스크롤 영역 − 합계 상자 · fit()) */
|
||||
.lstk[data-edit="1"] .lstk__right {
|
||||
position: sticky;
|
||||
inset-block-start: 0;
|
||||
@@ -81,7 +95,9 @@
|
||||
margin-block-start: calc(-1 * var(--spacing-8));
|
||||
}
|
||||
|
||||
/* 그룹 머리 한 줄(요구 687-6 · 688) — 이름 칸이 늘고 줄어 [+] · X 는 늘 오른쪽 끝(접어도 그대로) */
|
||||
.lstk-ihead {
|
||||
flex-wrap: nowrap;
|
||||
align-items: center;
|
||||
padding: var(--spacing-4) var(--spacing-8);
|
||||
border-inline-start: 4px solid var(--color-accent);
|
||||
@@ -93,7 +109,18 @@
|
||||
border-inline-start-color: var(--color-border-strong);
|
||||
}
|
||||
|
||||
.lstk-ihead > .lstk-f-l {
|
||||
flex: 1 1 auto;
|
||||
min-width: 4em;
|
||||
}
|
||||
|
||||
.lstk-count {
|
||||
flex: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.lstk-no {
|
||||
flex: none;
|
||||
min-width: 1.5em;
|
||||
font-weight: var(--font-weight-bold);
|
||||
color: var(--color-text);
|
||||
@@ -109,14 +136,12 @@
|
||||
}
|
||||
|
||||
.lstk-ihead,
|
||||
.lstk-row,
|
||||
.lstk-fhead,
|
||||
.lstk-frow,
|
||||
.lstk-find,
|
||||
.lstk-ltools {
|
||||
.lstk-find {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-end;
|
||||
align-items: center;
|
||||
gap: var(--spacing-4) var(--spacing-8);
|
||||
min-width: 0;
|
||||
}
|
||||
@@ -125,28 +150,74 @@
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* 줄 카드 — 금액 · 글 = 한 줄(요구 687-7 · [손잡이][종류][이름][수량][단위][비목][X] · 좁으면 접힘) · 공식 = 머리 줄 + 식 줄 */
|
||||
.lstk-line {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-4);
|
||||
padding: var(--spacing-8);
|
||||
padding-inline: var(--spacing-24) var(--spacing-32);
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: var(--spacing-4) var(--spacing-8);
|
||||
padding: var(--spacing-4) var(--spacing-8);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--color-surface-raised);
|
||||
}
|
||||
|
||||
/* 글 · 근거 칸 = 한 줄 높이(세로 묶음에서 늘지 않게) */
|
||||
.lstk-line > .lstk-f-l {
|
||||
.lstk-line.k-공식 {
|
||||
flex-direction: column;
|
||||
flex-wrap: nowrap;
|
||||
align-items: stretch;
|
||||
position: relative;
|
||||
padding-inline-start: var(--spacing-24);
|
||||
}
|
||||
|
||||
.lstk-line > .lstk-x,
|
||||
.lstk-fhead > .lstk-x {
|
||||
margin-inline-start: auto;
|
||||
}
|
||||
|
||||
.lstk-line > .lstk-f-l,
|
||||
.lstk-line > .lstk-qty {
|
||||
flex: 1 1 8em;
|
||||
min-width: 6em;
|
||||
}
|
||||
|
||||
.lstk-line > .lstk-f-m {
|
||||
flex: 1 1 7em;
|
||||
min-width: 6em;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.lstk-line .lstk-kind {
|
||||
flex: none;
|
||||
min-width: 2.5em;
|
||||
}
|
||||
|
||||
.lstk-wide {
|
||||
flex-basis: 100%;
|
||||
}
|
||||
|
||||
.lstk-qty.ui-btn {
|
||||
justify-content: flex-start;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.lstk-upick {
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.lstk-ltools {
|
||||
position: absolute;
|
||||
inset-block-start: var(--spacing-4);
|
||||
inset-inline-end: var(--spacing-4);
|
||||
gap: 0;
|
||||
/* 보이는 글을 눌러 고침(요구 687-14) */
|
||||
.lstk-tap {
|
||||
cursor: text;
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.lstk-tap:hover,
|
||||
.lstk-tap:focus-visible {
|
||||
background: var(--color-surface-sunken);
|
||||
text-decoration: underline dotted;
|
||||
}
|
||||
|
||||
/* 끌기(804-5) — 손잡이 · 끄는 것 반투명 · 놓을 자리 = 위 · 아래 선 · 빈 항목 = 점선 */
|
||||
@@ -155,9 +226,13 @@
|
||||
}
|
||||
|
||||
.lstk-line > .lstk-grip {
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.lstk-line.k-공식 > .lstk-grip {
|
||||
position: absolute;
|
||||
inset-block-start: var(--spacing-12);
|
||||
inset-inline-start: var(--spacing-8);
|
||||
inset-block-start: var(--spacing-8);
|
||||
inset-inline-start: var(--spacing-4);
|
||||
}
|
||||
|
||||
.lstk-edit[data-dragging] {
|
||||
@@ -185,6 +260,8 @@
|
||||
}
|
||||
|
||||
.lstk-res {
|
||||
flex: 0 1 auto;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
@@ -264,8 +341,14 @@
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
/* 펼친 문서 제목 = 보통 글(링크 꼴 없음 · 요구 687-11) */
|
||||
.lstk-out tr.subhead .lstk-h {
|
||||
color: var(--color-accent);
|
||||
font-weight: var(--font-weight-medium);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.lstk-ftitle {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.lstk-gap {
|
||||
|
||||
+161
-102
@@ -11,11 +11,13 @@
|
||||
* ========================================================================== */
|
||||
|
||||
import { createButton, createIconButton } from "@ui/ui_template_button";
|
||||
import { createCheckbox } from "@ui/ui_template_checkbox";
|
||||
import { createRowMenu } from "@ui/ui_template_context_menu";
|
||||
import { eqText } from "@ui/ui_template_equation";
|
||||
import { el, showToast } from "@ui/ui_template_elements";
|
||||
import { createInputField, createSelectField } from "@ui/ui_template_input";
|
||||
import { varId, type FormulaDoc, type MoneyLine } from "../common_util/common_util_formula_doc";
|
||||
import type { DocAnswer } from "../common_util/common_util_formula_doc_calc";
|
||||
import { isNum, type DocAnswer } from "../common_util/common_util_formula_doc_calc";
|
||||
import { lineMap, offLine, type Values } from "../common_util/common_util_formula_doc_view";
|
||||
import type { DocTable } from "../common_util/common_util_formula_doc_table";
|
||||
import {
|
||||
dropSyms,
|
||||
@@ -36,8 +38,11 @@ import {
|
||||
addPicked,
|
||||
aliasText,
|
||||
repick,
|
||||
shownUnit,
|
||||
tableAlias,
|
||||
} from "../common_util/common_util_line_stack_pick";
|
||||
import { createUnitPick, NO_UNIT } from "../M02_MasterTemplete/DataFlow/M02_DF_UnitPick";
|
||||
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";
|
||||
@@ -67,12 +72,14 @@ const field = (
|
||||
size: "s" | "m" | "l" = "m",
|
||||
): HTMLElement => {
|
||||
const f = createInputField({
|
||||
label,
|
||||
placeholder: label,
|
||||
value: value === undefined || value === null ? "" : String(value),
|
||||
compact: true,
|
||||
size: 24,
|
||||
});
|
||||
f.root.classList.add(`lstk-f-${size}`);
|
||||
f.input.setAttribute("aria-label", label);
|
||||
f.input.title = label;
|
||||
f.input.addEventListener("change", () => set(f.input.value));
|
||||
return f.root;
|
||||
};
|
||||
@@ -87,7 +94,7 @@ const KIND_KEY: Record<MoneyKind, LsKey> = {
|
||||
율: "K_율",
|
||||
};
|
||||
|
||||
/** 금액 줄 몸 */
|
||||
/** 금액 줄 몸 — 한 줄(804-7 · 요구 687-7): [종류][이름 단추 | 이름 칸 · 단가 식][수량][단위][비목] · 좁으면 접힘 */
|
||||
function moneyBody(h: EditHooks, i: number): HTMLElement[] {
|
||||
const d = h.doc;
|
||||
const line = (d.금액 ?? [])[i];
|
||||
@@ -96,64 +103,45 @@ function moneyBody(h: EditHooks, i: number): HTMLElement[] {
|
||||
const v = own ? d.변수?.find((x) => varId(x) === own) : undefined;
|
||||
const put = (fn: (l: MoneyLine & { 규격?: string }) => void): void =>
|
||||
h.change((doc) => fn(doc.금액![i]));
|
||||
const head = el("div", {
|
||||
className: "lstk-row",
|
||||
children: [
|
||||
field(lt("Name"), line.이름, (t) => put((l) => (l.이름 = t)), "l"),
|
||||
field(lt("Unit"), line.단위, (t) => put((l) => (l.단위 = t)), "s"),
|
||||
...(line.비목 === "묶음"
|
||||
? []
|
||||
: [
|
||||
createSelectField({
|
||||
label: lt("Cost"),
|
||||
options: ["노무비", "재료비", "경비"].map((x) => ({ value: x, text: x })),
|
||||
value: line.비목 ?? "노무비",
|
||||
compact: true,
|
||||
size: 24,
|
||||
onChange: (x) => put((l) => (l.비목 = x)),
|
||||
}).root,
|
||||
]),
|
||||
],
|
||||
});
|
||||
// 표 값 기호 = 화면 이름(요구 683) — 칸 글은 화면 이름 ↔ 기호로 오감
|
||||
const alias = tableAlias(d);
|
||||
const shown = (t: unknown): string => aliasText(String(t ?? ""), alias);
|
||||
const real = (t: string): string => aliasText(t.trim(), alias, true);
|
||||
const qty = field(
|
||||
lt("Qty"),
|
||||
shown(line.수량),
|
||||
(t) => put((l) => (t.trim() ? (l.수량 = real(t)) : delete l.수량)),
|
||||
"l",
|
||||
);
|
||||
const price: HTMLElement[] = [];
|
||||
const head: HTMLElement[] = [];
|
||||
if (own && v) {
|
||||
// 이름 단추(요구 687-9) — 누르면 찾아 넣기 창이 그 이름을 찾은 채 열림 · 고르면 바꿈
|
||||
const link = v.연결 ?? {};
|
||||
const mat = link.재료 as Record<string, unknown> | undefined;
|
||||
const spec = [line.이름, (line as { 규격?: string }).규격].filter(Boolean).join(" ");
|
||||
const label = link.키
|
||||
? kind === "기계" && spec
|
||||
? spec // 기계 = 기종 이름 · 규격(679 — 코드 번호 아님)
|
||||
: link.갈래 === "단가산출"
|
||||
? (h.subDoc(link.키)?.이름 ?? h.name(link.키))
|
||||
: h.name(link.키)
|
||||
: mat
|
||||
? String(mat.검색어 ?? mat.이름 ?? mat.상세구분 ?? "")
|
||||
: "";
|
||||
const size = (line as { 규격?: string }).규격;
|
||||
const name = String(line.이름 ?? "") || String(mat?.검색어 ?? mat?.이름 ?? "");
|
||||
const res = link.키
|
||||
? link.갈래 === "단가산출"
|
||||
? (h.subDoc(link.키)?.이름 ?? h.name(link.키))
|
||||
: h.name(link.키)
|
||||
: "";
|
||||
const pick = createButton({
|
||||
label: (size && !label.includes(size) ? `${label} ${size}` : label) || "—",
|
||||
label: (size && !name.includes(size) ? `${name} ${size}` : name) || "—",
|
||||
variant: "ghost",
|
||||
tone: "link",
|
||||
size: 24,
|
||||
iconStart: "search",
|
||||
onClick: () => openStackPick(kind, (p) => h.change((doc) => repick(doc, i, own, kind, p))),
|
||||
onClick: () =>
|
||||
openStackPick(
|
||||
kind,
|
||||
(p) => h.change((doc) => repick(doc, i, own, kind, p)),
|
||||
undefined,
|
||||
String(mat?.검색어 ?? line.이름 ?? ""),
|
||||
),
|
||||
});
|
||||
pick.classList.add("lstk-res");
|
||||
pick.title = lt("Repick");
|
||||
pick.title = `${lt("Repick")}${res ? ` — ${res}` : ""}`;
|
||||
pick.dataset.repick = String(i);
|
||||
price.push(pick);
|
||||
head.push(pick);
|
||||
} else
|
||||
price.push(field(lt("Price"), shown(line.단가), (t) => put((l) => (l.단가 = real(t))), "l"));
|
||||
head.push(
|
||||
field(lt("Name"), line.이름, (t) => put((l) => (l.이름 = t)), "m"),
|
||||
field(lt("Price"), shown(line.단가), (t) => put((l) => (l.단가 = real(t))), "l"),
|
||||
);
|
||||
const why = (h.answer.줄?.[i] as { 까닭?: string } | undefined)?.까닭;
|
||||
// 율 밑 번지(복사해 온 조각) — 칸 아래 읽는 이름(804-5)
|
||||
const places = /번지\./.test(`${line.단가} ${line.수량 ?? ""}`) ? placeNames(d, h.items) : null;
|
||||
@@ -161,25 +149,97 @@ function moneyBody(h: EditHooks, i: number): HTMLElement[] {
|
||||
shown(t).replace(/번지\.([^.\s()]+)\./g, (all, p: string) =>
|
||||
places?.[p] ? `${places[p]} ` : all,
|
||||
);
|
||||
const unit = el("span", { className: "lstk-upick" });
|
||||
const drawUnit = (): void =>
|
||||
unit.replaceChildren(
|
||||
createUnitPick({
|
||||
value: String(line.단위 ?? "") || NO_UNIT,
|
||||
size: 24,
|
||||
free: true,
|
||||
ariaLabel: lt("Unit"),
|
||||
extra: [...new Set((d.금액 ?? []).map((m) => String(m.단위 ?? "")).filter(Boolean))],
|
||||
onPick: (u) => put((l) => (u === NO_UNIT ? delete l.단위 : (l.단위 = u))),
|
||||
}),
|
||||
);
|
||||
drawUnit();
|
||||
return [
|
||||
el("span", { className: "lstk-kind", text: lt(KIND_KEY[kind]) }),
|
||||
head,
|
||||
el("div", {
|
||||
className: "lstk-row",
|
||||
children: [qty, el("span", { className: "lstk-unit", text: "×" }), ...price],
|
||||
}),
|
||||
...head,
|
||||
// 율 · 식 줄(자원 없음)에 수량이 없으면 칸 안 보임(한 줄 유지)
|
||||
...(own || String(line.수량 ?? "") ? [qtyBox(h, i, shown, real)] : []),
|
||||
unit,
|
||||
...(line.비목 === "묶음"
|
||||
? []
|
||||
: [
|
||||
createSelectField({
|
||||
options: ["노무비", "재료비", "경비"].map((x) => ({ value: x, text: x })),
|
||||
value: line.비목 ?? "노무비",
|
||||
compact: true,
|
||||
size: 24,
|
||||
ariaLabel: lt("Cost"),
|
||||
onChange: (x) => put((l) => (l.비목 = x)),
|
||||
}).root,
|
||||
]),
|
||||
...(places
|
||||
? [
|
||||
el("div", {
|
||||
className: "lstk-hint",
|
||||
className: "lstk-hint lstk-wide",
|
||||
text: `= ${[line.수량, line.단가].filter(Boolean).map(read).join(" × ")}`,
|
||||
}),
|
||||
]
|
||||
: []),
|
||||
...(why ? [el("div", { className: "lstk-miss", text: why })] : []),
|
||||
...(why ? [el("div", { className: "lstk-miss lstk-wide", text: why })] : []),
|
||||
];
|
||||
}
|
||||
|
||||
/** 수량 — 기호 하나면 값 + 출처 꼬리표(「0.39 ← 표 · 잡석 수량」 · 요구 687-12 · 기호는 마우스 글) · 누르면 그 자리에서 식 고침 */
|
||||
function qtyBox(
|
||||
h: EditHooks,
|
||||
i: number,
|
||||
shown: (t: unknown) => string,
|
||||
real: (t: string) => string,
|
||||
): HTMLElement {
|
||||
const line = (h.doc.금액 ?? [])[i];
|
||||
const text = String(line.수량 ?? "");
|
||||
const v = h.doc.변수?.find((x) => varId(x) === text);
|
||||
const at = `qty:${i}`;
|
||||
const edit = (): HTMLElement => {
|
||||
const f = field(lt("Qty"), shown(text), (t) =>
|
||||
h.change((doc) => {
|
||||
h.open.delete(at);
|
||||
const l = doc.금액![i];
|
||||
if (t.trim()) l.수량 = real(t);
|
||||
else delete l.수량;
|
||||
}),
|
||||
);
|
||||
f.classList.add("lstk-qty");
|
||||
f.dataset.qty = String(i);
|
||||
return f;
|
||||
};
|
||||
if (!v || h.open.has(at)) return edit();
|
||||
const val = (h.answer.값 ?? {})[text];
|
||||
const link = v.연결 ?? {};
|
||||
const kind = link.갈래 ?? "식";
|
||||
const from =
|
||||
v.이름 || (link.키 || link.표 ? h.name(String(link.키 ?? link.표)) : "") || shown(text);
|
||||
const b = createButton({
|
||||
label: `${isNum(val) ? eqText(val) : "—"}${v.단위 && shownUnit(v.단위) ? ` ${shownUnit(v.단위)}` : ""} ← ${kind} · ${from}`,
|
||||
variant: "ghost",
|
||||
size: 24,
|
||||
onClick: () => {
|
||||
h.open.add(at);
|
||||
h.change(() => undefined);
|
||||
requestAnimationFrame(() =>
|
||||
document.querySelector<HTMLInputElement>(`[data-qty="${i}"] input`)?.focus(),
|
||||
);
|
||||
},
|
||||
});
|
||||
b.classList.add("lstk-qty");
|
||||
b.title = `${lt("Qty")} = ${shown(text)}`;
|
||||
b.dataset.qty = String(i);
|
||||
return b;
|
||||
}
|
||||
|
||||
/** [+ 줄] 고르개 값 → 새 줄 */
|
||||
function addLine(h: EditHooks, at: number, kind: string): void {
|
||||
if (["인력", "재료", "기계", "단가산출"].includes(kind))
|
||||
@@ -202,10 +262,6 @@ function addLine(h: EditHooks, at: number, kind: string): void {
|
||||
(d.변수 ??= []).push(newVar(id, { 갈래: "숫자", 값: 0 }));
|
||||
it.줄.push({ 꼴: "공식", 기호: [id] });
|
||||
h.open.add(`${at}.${it.줄.length - 1}`);
|
||||
} else if (kind === "틀") {
|
||||
const id = freshId(d, "Q", "틀");
|
||||
(d.변수 ??= []).push(newVar(id, { 갈래: "단가산출", 키: "", 입력: {} }));
|
||||
it.줄.push({ 꼴: "공식", 기호: [id] });
|
||||
} else if (kind === "글") it.줄.push({ 꼴: "글", 글: "" });
|
||||
});
|
||||
}
|
||||
@@ -221,28 +277,23 @@ function dropLine(d: StackDoc, items: StackItem[], i: number, j: number): void {
|
||||
}
|
||||
|
||||
export function buildEdit(h: EditHooks): HTMLElement {
|
||||
// [+] 줄 더함 메뉴(요구 687-13 · 18) — 인력 · 재료 · 기계 · 단가산출 · 율 · 식 · 근거 글
|
||||
const add = (at: number): HTMLElement => {
|
||||
const pick = createSelectField({
|
||||
options: [
|
||||
{ value: "", text: `${lt("Add_Line")} ▾` },
|
||||
...(["인력", "재료", "기계", "단가산출", "율"] as MoneyKind[]).map((k) => ({
|
||||
value: k,
|
||||
text: lt(k === "단가산출" && h.take ? "K_복사" : KIND_KEY[k]),
|
||||
})),
|
||||
{ value: "공식", text: lt("K_새공식") },
|
||||
{ value: "틀", text: lt("K_틀") },
|
||||
{ value: "글", text: lt("K_글") },
|
||||
],
|
||||
value: "",
|
||||
compact: true,
|
||||
size: 24,
|
||||
ariaLabel: lt("Add_Line"),
|
||||
onChange: (x) => {
|
||||
pick.select.value = ""; // 창을 닫아도 [+ 줄 ▾] 로 돌아옴
|
||||
if (x) addLine(h, at, x);
|
||||
},
|
||||
});
|
||||
return pick.root;
|
||||
const kinds: [string, LsKey][] = [
|
||||
["인력", "K_인력"],
|
||||
["재료", "K_재료"],
|
||||
["기계", "K_기계"],
|
||||
["단가산출", h.take ? "K_복사" : "K_단가산출"],
|
||||
["율", "Add_율"],
|
||||
["공식", "Add_식"],
|
||||
["글", "K_글"],
|
||||
];
|
||||
const menu = createRowMenu(
|
||||
kinds.map(([k, label]) => ({ label: lt(label), onClick: () => addLine(h, at, k) })),
|
||||
{ icon: "plus", title: lt("Add_Line"), size: 24 },
|
||||
);
|
||||
menu.classList.add("lstk-add");
|
||||
return menu;
|
||||
};
|
||||
// X 지움 — 확인 없이 · 알림 누르면 되돌림(그 뒤 다른 고침이 없을 때만)
|
||||
const drop = (what: string, fn: (d: StackDoc, items: StackItem[]) => void): void => {
|
||||
@@ -278,7 +329,7 @@ export function buildEdit(h: EditHooks): HTMLElement {
|
||||
h.items.forEach((_, k) => h.open.delete(`fold:${k}`));
|
||||
flags.forEach((f, k) => f && h.open.add(`fold:${k}`));
|
||||
};
|
||||
// 항목 번호 — 산출근거와 같은 셈(제목 있는 항목 1. 2. · 가나다 = 가. 나.)
|
||||
// 항목 번호 — 산출근거와 같은 셈(제목 있는 항목 1. 2. · 옛 가나다 항목 = 가. 나. · 체크 걷음 요구 687-5)
|
||||
let n = 0;
|
||||
let sub = 0;
|
||||
const nos = h.items.map((it) => {
|
||||
@@ -287,9 +338,21 @@ export function buildEdit(h: EditHooks): HTMLElement {
|
||||
if (!it.가나다) sub = 0;
|
||||
return no;
|
||||
});
|
||||
// 만약(IF) 안 탄 갈래(요구 687-10) — 갈래를 다 열면 닿고 지금 값으로는 안 닿는 기호 · 꺼진 금액 줄 = 숨김(산출근거와 같음)
|
||||
const vals = (h.answer.값 ?? {}) as Values;
|
||||
const taken = new Set(reach(h.doc, vals));
|
||||
const every = new Set(reach(h.doc, {}, true));
|
||||
const steps = lineMap(h.doc);
|
||||
const off = (line: StackItem["줄"][number]): boolean =>
|
||||
line.꼴 === "금액"
|
||||
? offLine((h.doc.금액 ?? [])[line.i] ?? {}, vals, steps)
|
||||
: line.꼴 === "공식" &&
|
||||
line.기호.length > 0 &&
|
||||
line.기호.every((id) => every.has(id) && !taken.has(id));
|
||||
const items = h.items.map((it, i) => {
|
||||
const folded = h.open.has(`fold:${i}`);
|
||||
const itemGrip = grip();
|
||||
// 그룹 머리 한 줄(요구 687-6 · 688) — 손잡이 · 접기 · 번호 · 이름 · [+] · X · 접어도 자리 그대로
|
||||
const head = el("div", {
|
||||
className: "lstk-ihead",
|
||||
children: [
|
||||
@@ -306,15 +369,10 @@ export function buildEdit(h: EditHooks): HTMLElement {
|
||||
}),
|
||||
el("b", { className: "lstk-no", text: nos[i] }),
|
||||
field(lt("Title"), it.제목, (t) => h.change((_, items) => (items[i].제목 = t.trim())), "l"),
|
||||
createCheckbox(lt("Gana"), {
|
||||
checked: !!it.가나다,
|
||||
size: 24,
|
||||
onChange: (c) =>
|
||||
h.change((_, items) => (c ? (items[i].가나다 = true) : delete items[i].가나다)),
|
||||
}).root,
|
||||
...(folded
|
||||
? [el("span", { className: "lstk-hint", text: `${lt("K_줄")} ${it.줄.length}` })]
|
||||
: []),
|
||||
el("span", {
|
||||
className: "lstk-hint lstk-count",
|
||||
text: folded ? `${lt("K_줄")} ${it.줄.length}` : "",
|
||||
}),
|
||||
add(i),
|
||||
x(lt("Del_Item"), () =>
|
||||
drop(it.제목.trim() || lt("Item"), (d, items) => {
|
||||
@@ -325,34 +383,35 @@ export function buildEdit(h: EditHooks): HTMLElement {
|
||||
),
|
||||
],
|
||||
});
|
||||
const lines = it.줄.map((line, j) => {
|
||||
const lines = it.줄.flatMap((line, j) => {
|
||||
if (off(line)) return [];
|
||||
const lineGrip = grip();
|
||||
const tools = el("div", {
|
||||
className: "lstk-ltools",
|
||||
children: [
|
||||
x(lt("Del"), () =>
|
||||
drop(line.꼴 === "금액" ? String(h.doc.금액?.[line.i]?.이름 ?? "") : "", (d, items) =>
|
||||
dropLine(d, items, i, j),
|
||||
),
|
||||
),
|
||||
],
|
||||
});
|
||||
const del = x(lt("Del"), () =>
|
||||
drop(line.꼴 === "금액" ? String(h.doc.금액?.[line.i]?.이름 ?? "") : "", (d, items) =>
|
||||
dropLine(d, items, i, j),
|
||||
),
|
||||
);
|
||||
const body =
|
||||
line.꼴 === "금액"
|
||||
? moneyBody(h, line.i)
|
||||
: line.꼴 === "글"
|
||||
? [
|
||||
el("span", { className: "lstk-kind", text: lt("K_글") }),
|
||||
field(
|
||||
lt("K_글"),
|
||||
lt("Text_Ph"),
|
||||
line.글,
|
||||
(t) => h.change((_, items) => ((items[i].줄[j] as { 글: string }).글 = t)),
|
||||
"l",
|
||||
),
|
||||
]
|
||||
: buildBlock(h, i, j, line);
|
||||
: buildBlock(h, i, j, line, (id) => every.has(id) && !taken.has(id));
|
||||
// 금액 · 글 = 한 줄(X 끝) · 공식 묶음 = 머리 줄 끝에 X
|
||||
const card = el("div", {
|
||||
className: `lstk-line k-${line.꼴}`,
|
||||
children: [lineGrip, tools, ...body],
|
||||
children:
|
||||
line.꼴 === "공식"
|
||||
? [lineGrip, ...body.slice(0, 1).map((hd) => (hd.append(del), hd)), ...body.slice(1)]
|
||||
: [lineGrip, ...body, del],
|
||||
});
|
||||
card.dataset.loc = `${i}.${j}`;
|
||||
card.addEventListener("pointerdown", () => h.pair(`${i}.${j}`));
|
||||
@@ -365,7 +424,7 @@ export function buildEdit(h: EditHooks): HTMLElement {
|
||||
h.change((_, items) => moveLine(items, i, j, k, p));
|
||||
},
|
||||
});
|
||||
return card;
|
||||
return [card];
|
||||
});
|
||||
const box = el("div", {
|
||||
className: `lstk-item${it.가나다 ? " is-sub" : ""}${folded ? " is-folded" : ""}`,
|
||||
|
||||
@@ -2,12 +2,13 @@
|
||||
* line_stack_out.ts
|
||||
* 줄 쌓기 산출근거 출력(PLAN 804-2 · 요구 670-1 · 677) — 경쟁사 「단가산출」 시트 꼴 표(산출근거 · 합계 · 노무비 · 재료비 · 경비)
|
||||
* 공식 줄 = 「기호 = 기호식 = 값식 = 결과 단위」(값만인 줄은 넷까지 한 줄) · 금액 줄 = 「이름 : 단가 × 수량 단위 = 금액」
|
||||
* 기계 · 단가산출 줄 = 비목마다 「노무비 : 59,020 × … = 금액」 — 단가 수 누름(▸) = 그 문서(기계 시간당 경비 등)를
|
||||
* 같은 계산기로 셈해 같은 꼴로 펼침(손료 식 · 연료 · 운전사 · 노 · 재 · 경 · 계) · 접으면 한 줄 · 출력엔 펼친 것만
|
||||
* 기계 · 단가산출 줄 = 비목마다 「노무비 : 59,020 × … = 금액」 — 제목 줄(「굴착기 0.6㎥」) 누름 = 그 문서(기계 시간당 경비 등)를
|
||||
* 같은 계산기로 셈해 같은 꼴로 펼침(손료 식 · 연료 · 운전사 · 노 · 재 · 경 · 계) · 비목 수는 글(링크 꼴 없음 · 요구 687-11) ·
|
||||
* 공식 줄 단가산출 결과 = 수 누름(▸) · 접으면 한 줄 · 출력엔 펼친 것만
|
||||
* 값 = 계산기 셈 답 그대로(`calcDoc` · 줄 `answer.줄` · 합 `노무비` …) — 여기서 다시 셈 안 함 · 조판 = KaTeX(`line_stack_tex.ts`)
|
||||
* ========================================================================== */
|
||||
|
||||
import { createButton } from "@ui/ui_template_button";
|
||||
import { createButton, createIconButton } from "@ui/ui_template_button";
|
||||
import { eqText } from "@ui/ui_template_equation";
|
||||
import { el } from "@ui/ui_template_elements";
|
||||
import { tableRow, tableShell, type Column } from "@ui/ui_template_table";
|
||||
@@ -127,6 +128,21 @@ function rows(
|
||||
b.dataset.expand = path;
|
||||
return b;
|
||||
};
|
||||
/** 기계 · 단가산출 제목 줄 — 접기 단추 + 제목(누르면 펼침 · 요구 687-11) */
|
||||
const fold = (id: string, title: string): HTMLElement[] => {
|
||||
const path = `${at}/${id}`;
|
||||
const on = h.open.has(path);
|
||||
const b = createIconButton({
|
||||
icon: on ? "chevronDown" : "chevronRight",
|
||||
title: lt(on ? "Fold_Sub" : "Unfold_Sub"),
|
||||
size: 20,
|
||||
onClick: (e) => (e.stopPropagation(), h.toggle(path)),
|
||||
});
|
||||
b.dataset.expand = path;
|
||||
const t = el("span", { className: "lstk-ftitle", text: title });
|
||||
t.addEventListener("click", (e) => (e.stopPropagation(), h.toggle(path)));
|
||||
return [b, t];
|
||||
};
|
||||
const expand = (id: string): void => {
|
||||
const path = `${at}/${id}`;
|
||||
const sub = subOf(id);
|
||||
@@ -251,18 +267,14 @@ function rows(
|
||||
const s = priceSym(line);
|
||||
const bundle = (r.단가 ?? {}) as Values;
|
||||
const cols: Cols = {};
|
||||
push({ h: [name || (s ? (subOf(s)?.이름 ?? s) : "")], cls: "lname" });
|
||||
const title = name || (s ? (subOf(s)?.이름 ?? s) : "");
|
||||
push({ h: s && subOf(s) ? fold(s, title) : [title], cls: "lname" });
|
||||
for (const c of COSTS) {
|
||||
const part = r.비목?.[c];
|
||||
if (!isNum(part) || (!part && !bundle[c])) continue;
|
||||
cols[c] = part;
|
||||
push({
|
||||
h: [
|
||||
`${COST_SHOW[c]} : `,
|
||||
s ? opener(s, eqText(bundle[c])) : eqText(bundle[c]),
|
||||
...qtyNode(c),
|
||||
` = ${one(part)}`,
|
||||
],
|
||||
h: [`${COST_SHOW[c]} : `, eqText(bundle[c]), ...qtyNode(c), ` = ${one(part)}`],
|
||||
});
|
||||
}
|
||||
if (s) expand(s);
|
||||
|
||||
@@ -39,8 +39,14 @@ export interface StackTake {
|
||||
take: (src: string) => void;
|
||||
}
|
||||
|
||||
/** 찾아 넣기 — 고르면 `done(고른 것)` · 닫으면 아무것도 안 함 · `m02` 있고 다른 단가산출 = 복사해 펼침 */
|
||||
export function openStackPick(kind: MoneyKind, done: (p: Picked) => void, m02?: StackTake): void {
|
||||
/** 찾아 넣기 — 고르면 `done(고른 것)` · 닫으면 아무것도 안 함 · `m02` 있고 다른 단가산출 = 복사해 펼침 ·
|
||||
* `query` = 찾기 칸에 넣고 연 결과(이름 단추 · 요구 687-9) */
|
||||
export function openStackPick(
|
||||
kind: MoneyKind,
|
||||
done: (p: Picked) => void,
|
||||
m02?: StackTake,
|
||||
query = "",
|
||||
): void {
|
||||
const want = KINDS[kind];
|
||||
if (!want) return;
|
||||
const spec = (e: { 규격: string }) => (e.규격 ? { 규격: e.규격 } : {});
|
||||
@@ -50,6 +56,7 @@ export function openStackPick(kind: MoneyKind, done: (p: Picked) => void, m02?:
|
||||
owner: copy?.owner ?? null,
|
||||
title: `${lt("Find_Title")} — ${lt(copy ? "K_복사" : (`K_${kind}` as never))}`,
|
||||
kinds: copy ? ["logic", "sk", "pre"] : [want], // M01 = M01 전체만(680-1)
|
||||
query: query.trim() || undefined,
|
||||
ref: copy
|
||||
? (r) =>
|
||||
r.키 === copy.me && r.층 === copy.layer && (!copy.owner || r.주인 === copy.owner)
|
||||
|
||||
+158
-125
@@ -1,14 +1,16 @@
|
||||
/* =============================================================================
|
||||
* line_stack_rows.ts
|
||||
* 줄 쌓기 공식 묶음(PLAN 804-2 · 요구 675) — 「이름 = 값」 · 「이름 = 식」 줄 묶음 · 틀 이름표 · [식으로 풀기] · [틀로 저장]
|
||||
* 접힘 = 값 줄은 값 칸(연결 값을 고쳐 적으면 숫자 = 고정값 · 요구 664) · 식 줄은 「이름 = 식 = 값」 글 · 단가산출 결과 기호 = [식으로 풀기]
|
||||
* 풀림 = 줄마다 이름 · 식 · 단위 칸 + 지움 · [+ 줄] — 이름 바꿈 = 문서 전부(`rename`) · 식 = 식 줄 · 수 = 숫자 연결
|
||||
* 보임 = 값 줄은 값 칸(연결 값을 고쳐 적으면 숫자 = 고정값 · 요구 664) · 식 줄은 「이름 = 식 = 값」 글 · 단가산출 결과 기호 = [식으로 풀기]
|
||||
* 고침(804-7 · 요구 687-14) = [식 고치기] 걷음 — 이름 · 식 글을 누르면 그 줄만 그 자리에서 이름 · 식 · 단위 칸 + 지움 ·
|
||||
* 바꾸면 닫힘 · 머리 [+] = 식 줄 더함 — 이름 바꿈 = 문서 전부(`rename`) · 식 = 식 줄 · 수 = 숫자 연결
|
||||
* 만약 안 탄 갈래 기호(`hide` · 요구 687-10) = 안 보임(식 글은 그대로)
|
||||
* 틀 = 단가산출 결과 문서(굴착기 Q 등)를 이 문서 식으로 옮김(`unfold`) — 꼴이 바뀌면 「내 틀 (틀 이름 에서 변경)」 ·
|
||||
* 빈 틀 = [찾아 넣기](M01 단가산출 결과 — 작업량 등) · [틀로 저장] · 내 틀 목록 걷음(요구 685) · 표 값만 = 머리 「표 값」
|
||||
* 표 값(804-3) = [표 · 칸] 단추 → 표 고르기 창(`line_stack_pick.ts`) · 값 칸 누름 = 그 줄 · 칸(`pickCell`)
|
||||
* ========================================================================== */
|
||||
|
||||
import { createButton } from "@ui/ui_template_button";
|
||||
import { createButton, createIconButton } from "@ui/ui_template_button";
|
||||
import { eqText } from "@ui/ui_template_equation";
|
||||
import { el, showToast } from "@ui/ui_template_elements";
|
||||
import { createInputField } from "@ui/ui_template_input";
|
||||
@@ -66,10 +68,21 @@ const input = (
|
||||
return f.root;
|
||||
};
|
||||
|
||||
export function buildBlock(h: EditHooks, i: number, j: number, line: Block): HTMLElement[] {
|
||||
export function buildBlock(
|
||||
h: EditHooks,
|
||||
i: number,
|
||||
j: number,
|
||||
line: Block,
|
||||
hide: (id: string) => boolean = () => false,
|
||||
): HTMLElement[] {
|
||||
const d = h.doc;
|
||||
const at = `${i}.${j}`;
|
||||
const open = h.open.has(at);
|
||||
const editing = (id: string): boolean => h.open.has(`${at}:${id}`);
|
||||
/** 그 줄 고침 켬 · 끔(화면 상태) */
|
||||
const flip = (id: string, on: boolean): void => {
|
||||
if (on) h.open.add(`${at}:${id}`);
|
||||
else h.open.delete(`${at}:${id}`);
|
||||
};
|
||||
const vals = (h.answer.값 ?? {}) as Record<string, unknown>;
|
||||
const vars = new Map((d.변수 ?? []).map((v) => [varId(v), v]));
|
||||
const lines = new Map((d.식 ?? []).map((l) => [String(l.기호 ?? ""), String(l.식 ?? "")]));
|
||||
@@ -104,15 +117,17 @@ export function buildBlock(h: EditHooks, i: number, j: number, line: Block): HTM
|
||||
className: "lstk-fhead",
|
||||
children: [
|
||||
el("span", { className: "lstk-kind", text: label }),
|
||||
createButton({
|
||||
label: open ? lt("Fold") : lt("Unfold"),
|
||||
variant: "ghost",
|
||||
createIconButton({
|
||||
icon: "plus",
|
||||
title: lt("Add_Row"),
|
||||
size: 24,
|
||||
onClick: () => {
|
||||
if (open) h.open.delete(at);
|
||||
else h.open.add(at);
|
||||
h.change(() => undefined);
|
||||
},
|
||||
onClick: () =>
|
||||
h.change((doc, items) => {
|
||||
const id = freshId(doc, "x");
|
||||
(doc.변수 ??= []).push(newVar(id, { 갈래: "숫자", 값: 0 }));
|
||||
blockOf(items).기호.push(id);
|
||||
flip(id, true);
|
||||
}),
|
||||
}),
|
||||
],
|
||||
});
|
||||
@@ -177,118 +192,136 @@ export function buildBlock(h: EditHooks, i: number, j: number, line: Block): HTM
|
||||
items[i].줄.splice(j + 1, 0, { 꼴: "공식", 기호: moved, 틀: key, 틀꼴: shapeOf(doc, moved) });
|
||||
if (!blk.기호.length) items[i].줄.splice(j, 1);
|
||||
});
|
||||
const rows = line.기호.map((id) => {
|
||||
const v = vars.get(id);
|
||||
const link = v?.연결;
|
||||
const expr = lines.get(id);
|
||||
const sub = link?.갈래 === "단가산출" && link.키 ? h.subDoc(link.키) : null;
|
||||
const tag =
|
||||
link?.갈래 === "표" && !expr
|
||||
? tableBtn(id, link)
|
||||
: link && LINK_TAG[link.갈래 ?? ""] && !expr
|
||||
? el("span", {
|
||||
className: "lstk-tag",
|
||||
text: `${LINK_TAG[link.갈래!]}${link.키 && !link.키.includes("{") ? ` · ${sub?.이름 ?? h.name(link.키) ?? ""}` : ""}`,
|
||||
})
|
||||
: null;
|
||||
const tools: HTMLElement[] = [];
|
||||
if (sub?.결과 && link?.키)
|
||||
tools.push(
|
||||
createButton({
|
||||
label: lt("Unfold"),
|
||||
variant: "ghost",
|
||||
size: 24,
|
||||
onClick: () => unfoldRow(id, sub, link.키!),
|
||||
}),
|
||||
);
|
||||
if (link?.갈래 === "단가산출" && !link.키)
|
||||
tools.push(
|
||||
createButton({
|
||||
label: lt("Find_Title"),
|
||||
variant: "ghost",
|
||||
size: 24,
|
||||
onClick: () =>
|
||||
openFindPicker({
|
||||
layer: "system",
|
||||
owner: null,
|
||||
title: lt("K_틀"),
|
||||
kinds: ["logic"],
|
||||
results: true,
|
||||
logic: (x) =>
|
||||
h.change((doc) => {
|
||||
const y = doc.변수?.find((z) => varId(z) === id);
|
||||
if (y) y.연결 = { 갈래: "단가산출", 키: x.키, 입력: {} };
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
const name = el("b", { className: "lstk-sym", text: show(id) });
|
||||
const valueOf =
|
||||
expr ??
|
||||
(link?.갈래 === "숫자" ? String(link.값 ?? "") : isNum(vals[id]) ? String(vals[id]) : "");
|
||||
let body: (HTMLElement | string)[];
|
||||
if (open)
|
||||
body = [
|
||||
input(show(id), lt("Row_Name"), "lstk-f-s", (t) =>
|
||||
renameRow(id, t.trim() === show(id) ? id : t),
|
||||
),
|
||||
"=",
|
||||
input(shownText(valueOf), lt("Row_Expr"), "lstk-f-l", (t) =>
|
||||
h.change((doc) => setRow(doc, id, realText(t))),
|
||||
),
|
||||
input(v?.단위 ?? "", lt("Unit"), "lstk-f-s", (t) =>
|
||||
h.change((doc) => {
|
||||
let x = doc.변수?.find((y) => varId(y) === id);
|
||||
if (!x) (doc.변수 ??= []).push((x = newVar(id, { 갈래: "식" })));
|
||||
x.단위 = t.trim();
|
||||
/** 누르면 그 줄 고침(보이는 글을 고침) */
|
||||
const tap = (node: HTMLElement, id: string): HTMLElement => {
|
||||
node.classList.add("lstk-tap");
|
||||
node.tabIndex = 0;
|
||||
node.setAttribute("role", "button");
|
||||
node.title = lt("Edit_Hint");
|
||||
const go = (): void => {
|
||||
flip(id, true);
|
||||
h.change(() => undefined);
|
||||
};
|
||||
node.addEventListener("click", go);
|
||||
node.addEventListener("keydown", (e) => e.key === "Enter" && go());
|
||||
return node;
|
||||
};
|
||||
/** 고친 뒤 닫음 */
|
||||
const done = (id: string, fn: () => void): void => {
|
||||
flip(id, false);
|
||||
fn();
|
||||
};
|
||||
const rows = line.기호
|
||||
.filter((id) => !hide(id))
|
||||
.map((id) => {
|
||||
const v = vars.get(id);
|
||||
const link = v?.연결;
|
||||
const expr = lines.get(id);
|
||||
const sub = link?.갈래 === "단가산출" && link.키 ? h.subDoc(link.키) : null;
|
||||
const tag =
|
||||
link?.갈래 === "표" && !expr
|
||||
? tableBtn(id, link)
|
||||
: link && LINK_TAG[link.갈래 ?? ""] && !expr
|
||||
? el("span", {
|
||||
className: "lstk-tag",
|
||||
text: `${LINK_TAG[link.갈래!]}${link.키 && !link.키.includes("{") ? ` · ${sub?.이름 ?? h.name(link.키) ?? ""}` : ""}`,
|
||||
})
|
||||
: null;
|
||||
const tools: HTMLElement[] = [];
|
||||
if (sub?.결과 && link?.키)
|
||||
tools.push(
|
||||
createButton({
|
||||
label: lt("Unfold"),
|
||||
variant: "ghost",
|
||||
size: 24,
|
||||
onClick: () => unfoldRow(id, sub, link.키!),
|
||||
}),
|
||||
),
|
||||
el("span", { className: "lstk-val", text: `= ${valText(id)}` }),
|
||||
createButton({
|
||||
label: "×",
|
||||
variant: "danger-ghost",
|
||||
size: 24,
|
||||
onClick: () => h.change((doc) => dropSyms(doc, [id])),
|
||||
}),
|
||||
];
|
||||
else if (expr !== undefined || sub || link?.갈래 === "단가산출")
|
||||
body = [
|
||||
name,
|
||||
`= ${shownText(expr ?? "")}${expr ? " =" : ""}`,
|
||||
el("span", { className: "lstk-val", text: valText(id) }),
|
||||
];
|
||||
else
|
||||
body = [
|
||||
name,
|
||||
"=",
|
||||
input(shownText(valueOf), valText(id), "lstk-f-m", (t) =>
|
||||
h.change((doc) => setRow(doc, id, realText(t))),
|
||||
),
|
||||
...(shownUnit(v?.단위)
|
||||
? [el("span", { className: "lstk-unit", text: shownUnit(v?.단위) })]
|
||||
: []),
|
||||
];
|
||||
const row = el("div", {
|
||||
className: `lstk-frow ${open ? "is-open" : ""}`,
|
||||
children: [...body, ...(tag ? [tag] : []), ...tools],
|
||||
);
|
||||
if (link?.갈래 === "단가산출" && !link.키)
|
||||
tools.push(
|
||||
createButton({
|
||||
label: lt("Find_Title"),
|
||||
variant: "ghost",
|
||||
size: 24,
|
||||
onClick: () =>
|
||||
openFindPicker({
|
||||
layer: "system",
|
||||
owner: null,
|
||||
title: lt("K_단가산출"),
|
||||
kinds: ["logic"],
|
||||
results: true,
|
||||
logic: (x) =>
|
||||
h.change((doc) => {
|
||||
const y = doc.변수?.find((z) => varId(z) === id);
|
||||
if (y) y.연결 = { 갈래: "단가산출", 키: x.키, 입력: {} };
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
const name = tap(el("b", { className: "lstk-sym", text: show(id) }), id);
|
||||
const valueOf =
|
||||
expr ??
|
||||
(link?.갈래 === "숫자" ? String(link.값 ?? "") : isNum(vals[id]) ? String(vals[id]) : "");
|
||||
let body: (HTMLElement | string)[];
|
||||
const open = editing(id);
|
||||
if (open)
|
||||
body = [
|
||||
input(show(id), lt("Row_Name"), "lstk-f-s", (t) =>
|
||||
done(id, () => renameRow(id, t.trim() === show(id) ? id : t)),
|
||||
),
|
||||
"=",
|
||||
input(shownText(valueOf), lt("Row_Expr"), "lstk-f-l", (t) =>
|
||||
done(id, () => h.change((doc) => setRow(doc, id, realText(t)))),
|
||||
),
|
||||
input(v?.단위 ?? "", lt("Unit"), "lstk-f-s", (t) =>
|
||||
done(id, () =>
|
||||
h.change((doc) => {
|
||||
let x = doc.변수?.find((y) => varId(y) === id);
|
||||
if (!x) (doc.변수 ??= []).push((x = newVar(id, { 갈래: "식" })));
|
||||
x.단위 = t.trim();
|
||||
}),
|
||||
),
|
||||
),
|
||||
el("span", { className: "lstk-val", text: `= ${valText(id)}` }),
|
||||
createButton({
|
||||
label: "×",
|
||||
variant: "danger-ghost",
|
||||
size: 24,
|
||||
onClick: () => done(id, () => h.change((doc) => dropSyms(doc, [id]))),
|
||||
}),
|
||||
createIconButton({
|
||||
icon: "check",
|
||||
title: lt("Close"),
|
||||
size: 24,
|
||||
onClick: () => done(id, () => h.change(() => undefined)),
|
||||
}),
|
||||
];
|
||||
else if (expr !== undefined || sub || link?.갈래 === "단가산출")
|
||||
body = [
|
||||
name,
|
||||
"=",
|
||||
...(expr
|
||||
? [tap(el("span", { className: "lstk-expr", text: shownText(expr) }), id), "="]
|
||||
: []),
|
||||
el("span", { className: "lstk-val", text: valText(id) }),
|
||||
];
|
||||
else
|
||||
body = [
|
||||
name,
|
||||
"=",
|
||||
input(shownText(valueOf), valText(id), "lstk-f-m", (t) =>
|
||||
h.change((doc) => setRow(doc, id, realText(t))),
|
||||
),
|
||||
...(shownUnit(v?.단위)
|
||||
? [el("span", { className: "lstk-unit", text: shownUnit(v?.단위) })]
|
||||
: []),
|
||||
];
|
||||
const row = el("div", {
|
||||
className: `lstk-frow ${open ? "is-open" : ""}`,
|
||||
children: [...body, ...(tag ? [tag] : []), ...tools],
|
||||
});
|
||||
row.dataset.sym = id;
|
||||
return row;
|
||||
});
|
||||
row.dataset.sym = id;
|
||||
return row;
|
||||
});
|
||||
const more = open
|
||||
? [
|
||||
createButton({
|
||||
label: lt("Add_Row"),
|
||||
variant: "ghost",
|
||||
size: 24,
|
||||
onClick: () =>
|
||||
h.change((doc, items) => {
|
||||
const id = freshId(doc, "x");
|
||||
(doc.변수 ??= []).push(newVar(id, { 갈래: "숫자", 값: 0 }));
|
||||
blockOf(items).기호.push(id);
|
||||
}),
|
||||
}),
|
||||
]
|
||||
: [];
|
||||
return [head, ...rows, ...more];
|
||||
return [head, ...rows];
|
||||
}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
/* =============================================================================
|
||||
* line_stack_screen.ts
|
||||
* 단가산출 「줄 쌓기」 탭(PLAN 804-2 · 요구 670~677) — 공용(M01 단가산출 기초 · M02 단가산출 = 길 앞머리만 바꿈)
|
||||
* 위 = 옵션 가로(`formula_doc_options.ts`) · [고침 버림] · [수식 저장](관리자 · 층 쓰기) · M02 사본 [단가 갱신] · [재생성]
|
||||
* 위 = 옵션 가로(`formula_doc_options.ts`) · M02 사본 [단가 갱신] · [재생성] · 「M01 새 판」 배지(층 쓰기)
|
||||
* `page` 있음(M01 804-7 · M02 804-8) = 저장 · 되돌림 단추 없음 — 페이지 [저장] · [되돌림]이 `save` · `discard` 를 부름 ·
|
||||
* 고침이 바뀌면 `page()` · `page` 없음(일위대가 줄 모달 등) = 위에 [고침 버림] · [수식 저장]
|
||||
* 가운데 = 왼쪽 줄 쌓기 편집(`line_stack_edit.ts`) | 오른쪽 산출근거 출력(`line_stack_out.ts` · 기계 시간당 경비 펼침)
|
||||
* 아래 = 합계 상자(노무비 · 재료비 · 경비 · 계)
|
||||
* 아래 = 합계 상자(노무비 · 재료비 · 경비 · 계 · 화면 아래 고정 · 요구 687-3)
|
||||
* 정본 = 수식 문서(탭을 바꾸면 다시 읽음) · 줄 쌓기 차림 = 문서 칸 `쌓기` · 804-6 옛 수식 문서 탭 걷음 = 이 화면이 「수식」 탭
|
||||
* 셈 = 브라우저 계산기(`calcDoc`) 즉시 · 캐시에 없는 연결만 뒤에서 `POST {길}/preview`(옛 요청 끊음)
|
||||
* 고침 = 캐시(sessionStorage)에 쌓고 [수식 저장] 한 번에 `POST {길}/save`(서버가 정본으로 다시 셈) · 자동저장 없음
|
||||
* 고침 = 캐시(sessionStorage)에 쌓고 [저장] 한 번에 `POST {길}/save`(서버가 정본으로 다시 셈) · 자동저장 없음
|
||||
* M02 사본(`copy`) — 저장 몸 {문서, 끊음(받은 그대로), 판} · 판 바뀜(409)은 문서가 그대로면 새 판으로 한 번 더
|
||||
* [단가 갱신] · [재생성] = 미리 셈 → 알림 → 저장(`formula_doc_copy.ts` · 고친 것 먼저 저장)
|
||||
* ========================================================================== */
|
||||
@@ -27,7 +29,6 @@ import type { DocTable } from "../common_util/common_util_formula_doc_table";
|
||||
import { createDocCache, type DocRecord } from "../common_util/common_util_formula_doc_cache";
|
||||
import {
|
||||
docOf,
|
||||
notesOf,
|
||||
stackOf,
|
||||
type StackDoc,
|
||||
type StackItem,
|
||||
@@ -60,6 +61,8 @@ export interface StackScreenOptions {
|
||||
copy?: CopyOptions;
|
||||
/** M02 SK 층(804-4) — 줄 쌓기 다른 단가산출 · 일위대가 복사 길 · 없으면(M01) 복사 없음 */
|
||||
scope?: { layer: Layer; owner: string | null };
|
||||
/** 페이지가 저장 · 되돌림을 맡음(M01 804-7 · M02 804-8) — 고침 있음이 바뀔 때 부름 · 화면 위 [고침 버림] · [수식 저장] 없음 */
|
||||
page?: () => void;
|
||||
}
|
||||
|
||||
type Docs = Record<string, FormulaDoc>;
|
||||
@@ -77,20 +80,20 @@ const clone = <T>(v: T): T => JSON.parse(JSON.stringify(v)) as T;
|
||||
|
||||
export function createStackScreen(opt: StackScreenOptions) {
|
||||
const head = el("div", { className: "lstk__head" });
|
||||
const notes = el("div", { className: "lstk__notes" });
|
||||
const left = el("div", { className: "lstk__left" });
|
||||
const right = el("div", { className: "lstk__right" });
|
||||
const foot = el("div", { className: "lstk__foot" });
|
||||
const root = el("div", {
|
||||
className: "lstk",
|
||||
attrs: { "data-line-stack": "" },
|
||||
children: [head, notes, el("div", { className: "lstk__areas", children: [left, right] }), foot],
|
||||
children: [head, el("div", { className: "lstk__areas", children: [left, right] }), foot],
|
||||
});
|
||||
// 산출근거 고정 높이(요구 683) — 스크롤 조상(없으면 창) 높이 · 넓을 때만 고정(line_stack.css)
|
||||
const fit = (): void => {
|
||||
let p = root.parentElement;
|
||||
while (p && !/(auto|scroll)/.test(getComputedStyle(p).overflowY)) p = p.parentElement;
|
||||
root.style.setProperty("--lstk-view", `${p?.clientHeight ?? innerHeight}px`);
|
||||
const view = (p?.clientHeight ?? innerHeight) - foot.offsetHeight;
|
||||
root.style.setProperty("--lstk-view", `${Math.max(240, view)}px`);
|
||||
};
|
||||
window.addEventListener("resize", fit);
|
||||
const cp = opt.copy;
|
||||
@@ -148,6 +151,7 @@ export function createStackScreen(opt: StackScreenOptions) {
|
||||
} catch {
|
||||
/* 캐시가 막혀도 화면 안 고침은 남음 */
|
||||
}
|
||||
opt.page?.();
|
||||
};
|
||||
|
||||
// ── 셈 ──
|
||||
@@ -240,7 +244,7 @@ export function createStackScreen(opt: StackScreenOptions) {
|
||||
if (from === "left") r[0]?.scrollIntoView({ block: "nearest", behavior: "smooth" });
|
||||
};
|
||||
|
||||
/** 복사해 펼침(804-4) — 서버 조각 → 새 항목(고침 = 캐시 · [수식 저장] 때 서버가 다시 셈) */
|
||||
/** 복사해 펼침(804-4) — 서버 조각 → 새 항목(고침 = 캐시 · [저장] 때 서버가 다시 셈) */
|
||||
const takeIn = async (src: string): Promise<void> => {
|
||||
try {
|
||||
const got = await call<Taken>(`/take?src=${encodeURIComponent(src)}`);
|
||||
@@ -258,7 +262,7 @@ export function createStackScreen(opt: StackScreenOptions) {
|
||||
const edit = (): boolean => opt.admin && (!!key || !cp);
|
||||
const draw = (): void => {
|
||||
if (!doc || !answer) {
|
||||
[head, notes, left, right, foot].forEach((x) => x.replaceChildren());
|
||||
[head, left, right, foot].forEach((x) => x.replaceChildren());
|
||||
return;
|
||||
}
|
||||
const options = buildOptions(doc, given, !!cp && !opt.admin, (k, v, x) => {
|
||||
@@ -269,10 +273,10 @@ export function createStackScreen(opt: StackScreenOptions) {
|
||||
draw();
|
||||
});
|
||||
const busy = dirty();
|
||||
const tools =
|
||||
edit() && key
|
||||
const tools = [
|
||||
...(cp && edit() && key ? copyTools(true, (job) => void runJob(job)) : []),
|
||||
...(edit() && key && !opt.page
|
||||
? [
|
||||
...(cp ? copyTools(true, (job) => void runJob(job)) : []),
|
||||
createButton({
|
||||
label: ft("Discard"),
|
||||
variant: "ghost",
|
||||
@@ -288,7 +292,8 @@ export function createStackScreen(opt: StackScreenOptions) {
|
||||
onClick: () => void save(),
|
||||
}),
|
||||
]
|
||||
: [];
|
||||
: []),
|
||||
];
|
||||
head.replaceChildren(
|
||||
...(options.length ? [el("div", { className: "fdoc__options", children: options })] : []),
|
||||
el("div", {
|
||||
@@ -305,17 +310,6 @@ export function createStackScreen(opt: StackScreenOptions) {
|
||||
],
|
||||
}),
|
||||
);
|
||||
const kept = notesOf(doc);
|
||||
notes.replaceChildren(
|
||||
...(kept.length
|
||||
? [
|
||||
el("p", {
|
||||
className: "ui-text ui-text--muted",
|
||||
text: `${lt("Notes")} — ${kept.join(" · ")}`,
|
||||
}),
|
||||
]
|
||||
: []),
|
||||
);
|
||||
const items = stackOf(doc);
|
||||
root.dataset.edit = edit() ? "1" : "";
|
||||
left.replaceChildren(
|
||||
@@ -384,8 +378,10 @@ export function createStackScreen(opt: StackScreenOptions) {
|
||||
if (await askJob(job, got, cache.names)) await save({ 할일: job, 판: version }, ft("Job_Done"));
|
||||
};
|
||||
|
||||
const save = async (job?: Record<string, unknown>, done = ft("Saved")): Promise<void> => {
|
||||
if (!doc || (!job && !dirty())) return;
|
||||
let loading: Promise<void> = Promise.resolve();
|
||||
const save = async (job?: Record<string, unknown>, done = ft("Saved")): Promise<boolean> => {
|
||||
await loading; // 새 로직 저장 뒤 여는 중이면 다 읽고(캐시 이어 받음)
|
||||
if (!doc || (!job && !dirty())) return true;
|
||||
const send = job ?? (cp ? { 문서: doc, 끊음: cut, 판: version } : { 문서: doc });
|
||||
const was = saved;
|
||||
showLoadingOverlay();
|
||||
@@ -402,9 +398,11 @@ export function createStackScreen(opt: StackScreenOptions) {
|
||||
recalc();
|
||||
draw();
|
||||
if (cp && got.판) cp.onSaved?.(got.판);
|
||||
showToast(done, "success");
|
||||
if (done) showToast(done, "success");
|
||||
return true;
|
||||
} catch (e) {
|
||||
showToast(`${ft("Save_Failed")} — ${e instanceof Error ? e.message : ""}`, "error");
|
||||
return false;
|
||||
} finally {
|
||||
hideLoadingOverlay();
|
||||
}
|
||||
@@ -459,6 +457,7 @@ export function createStackScreen(opt: StackScreenOptions) {
|
||||
if (all[next]) doc = clone(all[next] as StackDoc);
|
||||
recalc();
|
||||
draw();
|
||||
opt.page?.(); // 캐시에서 이어 받은 고침 — 페이지 [저장] · [되돌림] 켬
|
||||
} catch (e) {
|
||||
if (mine === seq)
|
||||
head.replaceChildren(
|
||||
@@ -483,9 +482,14 @@ export function createStackScreen(opt: StackScreenOptions) {
|
||||
root,
|
||||
/** 로직 · SK 키로 그림(빈 키 = 저장 전 새 것) — 같은 키는 그대로 · `force` = 다시 읽음(탭 바꿈) */
|
||||
draw: (next: string, force = false): void => {
|
||||
if (force || next !== shown) void open(next);
|
||||
if (force || next !== shown) loading = open(next);
|
||||
},
|
||||
canLeave,
|
||||
dirty,
|
||||
/** 페이지 [저장](804-7) — 고침 있으면 서버 정본 저장 · 알림은 페이지가(`done` 빈 글) · 실패 = false */
|
||||
save: (): Promise<boolean> => save(undefined, ""),
|
||||
/** 페이지 [되돌림] — 저장 뒤 상태로(캐시 지움) */
|
||||
discard: (): Promise<void> => discard(),
|
||||
/** 시험 · 확인용 */
|
||||
state: () => ({ doc, answer, dirty: dirty(), items: doc ? stackOf(doc) : [] }),
|
||||
};
|
||||
|
||||
@@ -12,7 +12,9 @@ const TEXT = {
|
||||
Sum: ["계", "Sum"],
|
||||
Sub_Sum: ["소 계", "Subtotal"],
|
||||
Add_Item: ["+ 그룹", "+ Group"],
|
||||
Add_Line: ["+ 줄", "+ Line"],
|
||||
Add_Line: ["줄 더함", "Add line"],
|
||||
Add_율: ["율 — 그룹 소계의 %", "Rate — % of subtotal"],
|
||||
Add_식: ["식 — 공식 틀", "Formula"],
|
||||
Del_Item: ["그룹 지움", "Delete group"],
|
||||
Del: ["지움", "Delete"],
|
||||
Item: ["그룹", "Group"],
|
||||
@@ -20,23 +22,22 @@ const TEXT = {
|
||||
Drag: ["끌어 옮기기", "Drag to move"],
|
||||
Fold_Item: ["그룹 접기", "Collapse group"],
|
||||
Unfold_Item: ["그룹 펼치기", "Expand group"],
|
||||
Unfold_Sub: ["시간당 경비 · 산출 펼치기", "Show breakdown"],
|
||||
Fold_Sub: ["접기", "Hide breakdown"],
|
||||
Dropped: ["지움 — 눌러 되돌리기", "deleted — click to undo"],
|
||||
Undo_Late: ["그 뒤 고친 것이 있어 못 되돌림", "Changed since — cannot undo"],
|
||||
Title: ["그룹 이름(비우면 번호 없음)", "Group title"],
|
||||
Gana: ["가나다", "Sub"],
|
||||
Title: ["그룹 이름", "Group title"],
|
||||
K_인력: ["인력", "Labor"],
|
||||
K_재료: ["재료", "Material"],
|
||||
K_기계: ["기계", "Machine"],
|
||||
K_단가산출: ["다른 단가산출", "Unit price"],
|
||||
K_복사: ["다른 단가산출 · 일위대가 복사", "Copy unit price · combo"],
|
||||
K_단가산출: ["단가산출", "Unit price"],
|
||||
K_복사: ["단가산출 · 일위대가 복사", "Copy unit price · combo"],
|
||||
Self_Take: ["지금 단가산출 자신은 못 가져옴", "Cannot copy itself"],
|
||||
Take_Failed: ["복사 못 함", "Copy failed"],
|
||||
K_율: ["율 · 식", "Rate"],
|
||||
K_율: ["율", "Rate"],
|
||||
K_공식: ["공식", "Formula"],
|
||||
K_표값: ["표 값", "Table values"],
|
||||
K_새공식: ["공식 — 새 공식(빈 묶음)", "Formula — new"],
|
||||
K_틀: ["공식 — 틀 찾기(단가산출 결과)", "Formula — template"],
|
||||
K_글: ["글 · 근거", "Text"],
|
||||
K_글: ["근거 글", "Basis note"],
|
||||
Name: ["이름", "Name"],
|
||||
Unit: ["단위", "Unit"],
|
||||
Cost: ["비목", "Cost"],
|
||||
@@ -46,23 +47,19 @@ const TEXT = {
|
||||
Find: ["찾기", "Find"],
|
||||
Pick: ["고름 ▾", "Pick ▾"],
|
||||
None_Found: ["찾은 것 없음", "Nothing found"],
|
||||
Unfold: ["식 고치기", "Edit formula"],
|
||||
Fold: ["접기", "Fold"],
|
||||
Unfold: ["식으로 풀기", "Unfold"],
|
||||
Edit_Hint: ["눌러 고침", "Click to edit"],
|
||||
Add_Row: ["식 줄 더함", "Add row"],
|
||||
New_Formula: ["새 공식", "New formula"],
|
||||
My_Formula: ["내 공식", "My formula"],
|
||||
My_Tpl: ["내 틀", "My template"],
|
||||
Changed_From: ["에서 변경", "changed"],
|
||||
Add_Row: ["+ 「이름 = 식」 줄", "+ Row"],
|
||||
Row_Name: ["이름", "Name"],
|
||||
Row_Expr: ["값 또는 식", "Value or expr"],
|
||||
Bad_Name: ["못 쓰는 이름 · 같은 이름 있음", "Bad or duplicate name"],
|
||||
Empty_Item: ["줄 없음 — [+ 줄]", "No lines — [+ Line]"],
|
||||
Empty_Item: ["줄 없음 — [+]", "No lines — [+]"],
|
||||
Empty: ["빈 단가산출 — [+ 그룹] 으로 시작", "Empty — start with [+ Group]"],
|
||||
Text_Ph: ["< 품셈 근거 > · ※ 메모 · 별산", "Note"],
|
||||
Notes: [
|
||||
"줄 쌓기에서 못 고치는 칸(보존됨 · 수식 탭에서 고침)",
|
||||
"Kept fields (edit in Equation tab)",
|
||||
],
|
||||
Unsaved_New: ["저장한 단가산출만 — 페이지 [저장] 뒤", "Saved unit prices only"],
|
||||
Find_Title: ["찾아 넣기", "Find and add"],
|
||||
Repick: ["찾아 바꾸기", "Find and change"],
|
||||
|
||||
@@ -143,3 +143,57 @@ def table(key: str) -> dict:
|
||||
rec = mr.Rec(ml.Links(store.loaded()[1], {}))
|
||||
rec.see_table(key)
|
||||
return rec.표[key]
|
||||
|
||||
|
||||
# ── 로직 지우기(804-7 · 요구 687-1) — 로직 줄 + 수식 문서를 서버 정본에서 함께 ──
|
||||
def users(key: str) -> list[str]:
|
||||
"""이 로직을 부르는 곳 — 다른 수식 문서(단가산출 연결) · 일위대가 조합 · M02 단가산출(바탕).
|
||||
다른 로직 줄이 부르는 것은 로직 저장 검사(422)가 막음."""
|
||||
from M01_MasterData import M01_MasterData_Store_Combo as combo_store
|
||||
from M02_MasterTemplete import M02_Template_Layers as layers # M02 가 M01 을 부름 — 늦게
|
||||
|
||||
out = []
|
||||
for k, d in docs().items():
|
||||
if k != key and any(
|
||||
(v.get("연결") or {}).get("갈래") == "단가산출"
|
||||
and str((v.get("연결") or {}).get("키")) == key
|
||||
for v in d.get("변수") or []
|
||||
):
|
||||
out.append(f"단가산출 {k} {d.get('이름') or ''}".strip())
|
||||
for c in combo_store.combos_of_logic(key)["combos"]:
|
||||
out.append(f"일위대가 조합 {c.get('키')} {c.get('이름') or ''}".strip())
|
||||
for where, path in layers.layer_files("sangeun"):
|
||||
try:
|
||||
sk = json.loads(path.read_bytes())
|
||||
except (OSError, ValueError):
|
||||
continue
|
||||
if str((sk.get("바탕") or {}).get("로직") or "") == key:
|
||||
out.append(f"M02 단가산출 {where} {sk.get('이름') or path.stem}")
|
||||
return out
|
||||
|
||||
|
||||
def drop(key: str) -> None:
|
||||
"""수식 문서 줄 하나 지움(없으면 그냥)."""
|
||||
name = _where(key)
|
||||
if not name:
|
||||
return
|
||||
path = folder() / name
|
||||
data = ml.load_file(path)
|
||||
data["줄"] = [d for d in data.get("줄") or [] if str(d.get("키")) != key]
|
||||
path.write_text(ml.dump(data), encoding="utf-8", newline="\n")
|
||||
_memo.clear()
|
||||
|
||||
|
||||
def remove(key: str, file: str, version: str, confirmed: bool) -> dict:
|
||||
"""로직 지우기 — 부르는 곳이 있으면 409 {쓰는곳} · 로직 저장 길(개정 확인 · 판 · 검사) 뒤 문서도."""
|
||||
from M01_MasterData import M01_MasterData_Store_Make as make
|
||||
|
||||
used = users(key)
|
||||
if used:
|
||||
raise store.StoreError(409, {"쓰는곳": used})
|
||||
got = make.save(
|
||||
[{"file": file, "version": version, "changes": [{"op": "delete", "key": key}]}],
|
||||
confirmed,
|
||||
)
|
||||
drop(key)
|
||||
return got
|
||||
|
||||
@@ -415,6 +415,25 @@ def post_logic_doc_save(body: DocBody) -> dict:
|
||||
return _call(doc_screen.save, body.문서)
|
||||
|
||||
|
||||
class RemoveBody(BaseModel):
|
||||
key: str
|
||||
file: str
|
||||
version: str
|
||||
개정확인: bool = False
|
||||
|
||||
|
||||
@router.get("/logic/users")
|
||||
def get_logic_users(key: str) -> dict:
|
||||
"""로직을 부르는 곳(지우기 전 확인 · 읽기만) — {쓰는곳}."""
|
||||
return {"쓰는곳": doc_screen.users(key)}
|
||||
|
||||
|
||||
@router.post("/logic/remove")
|
||||
def post_logic_remove(body: RemoveBody) -> dict:
|
||||
"""로직 지우기(804-7) — 로직 줄 + 수식 문서 · 부르는 곳 있으면 409 {쓰는곳}."""
|
||||
return _call(doc_screen.remove, body.key, body.file, body.version, body.개정확인)
|
||||
|
||||
|
||||
@router.post("/logic/doc/links")
|
||||
def post_logic_doc_links(body: LinksBody) -> dict:
|
||||
"""연결 후보 — 요소 · 표 · 단가산출(수식 문서) 이름 찾기(읽기만)."""
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
* 제목 크기 · 자리 · 단추 차례 · 공용 단추 크기 · 찾기 칸 꼴이 한 벌로 같음.
|
||||
* ========================================================================== */
|
||||
|
||||
import { fitButtonRow } from "@ui/ui_template_button";
|
||||
import { el } from "@ui/ui_template_elements";
|
||||
import "./M01_MasterData_UI_Head.css";
|
||||
|
||||
@@ -19,7 +20,7 @@ export interface HeadOptions {
|
||||
buttons: HTMLElement[];
|
||||
}
|
||||
|
||||
/** 제목 줄 — 제목은 18px · 왼쪽 · 남는 자리를 차지해 단추를 오른쪽으로 밂 */
|
||||
/** 제목 줄 — 제목은 18px · 왼쪽 · 남는 자리를 차지해 단추를 오른쪽으로 밂 · 넘치면 아이콘 단추는 아이콘만(804-8) */
|
||||
export function buildHeadBar(opts: HeadOptions): { root: HTMLElement; title: HTMLElement } {
|
||||
const title = typeof opts.title === "string" ? el("h2", { text: opts.title }) : opts.title;
|
||||
title.classList.add("m01-head__title");
|
||||
@@ -27,6 +28,7 @@ export function buildHeadBar(opts: HeadOptions): { root: HTMLElement; title: HTM
|
||||
className: "m01-head",
|
||||
children: [...(opts.lead ?? []), title, ...(opts.status ?? []), ...opts.buttons],
|
||||
});
|
||||
fitButtonRow(root);
|
||||
return { root, title };
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
*
|
||||
* 데이터 흐름(CLAUDE.md 5장) — 고친 것은 캐시(sessionStorage)에 쌓고 [저장] 한 번에 `POST /save`.
|
||||
* 409 = 그 사이 파일이 바뀜 · 422 = 검사 걸림(아무것도 안 씀) — 까닭은 서버 글 그대로 보임.
|
||||
* 머리 단추(804-7 · 요구 687-2) = [신규 로직] · [로직 복사/생성] · [로직 삭제] · [되돌림] · [저장]
|
||||
* [저장] = 기본정보 · 별칭(로직 줄 `/save`) + 수식(줄 쌓기 `/logic/doc/save`) 한 번에 · [되돌림] = 둘 다 저장 뒤 상태로
|
||||
* [로직 삭제] = 확인 → 서버 정본에서 바로(`/logic/remove` — 로직 줄 + 수식 문서 · 부르는 곳 있으면 막고 까닭) → 로직 목록
|
||||
* ========================================================================== */
|
||||
|
||||
import { t as L } from "@ui/ui_template_locale";
|
||||
@@ -23,6 +26,8 @@ import {
|
||||
fetchLogicFiles,
|
||||
fetchLogics,
|
||||
fetchLogicSubs,
|
||||
fetchLogicUsers,
|
||||
removeLogic,
|
||||
saveFiles,
|
||||
type CalcLine,
|
||||
type TextAnswer,
|
||||
@@ -108,7 +113,12 @@ export async function mountM01LogicLab(
|
||||
let recalc: number | undefined;
|
||||
const drive = createDrive();
|
||||
// 수식 = 줄 쌓기(804-2 · 804-6 옛 수식 문서 탭 걷음) — 수식 문서 · 캐시 m01_doc_drafts
|
||||
const stack = createStackScreen({ base: "/m01/logic/doc", admin, cache: "m01_doc_drafts" });
|
||||
const stack = createStackScreen({
|
||||
base: "/m01/logic/doc",
|
||||
admin,
|
||||
cache: "m01_doc_drafts",
|
||||
page: () => persist(),
|
||||
});
|
||||
const leave = (): Promise<boolean> => stack.canLeave();
|
||||
const editor = el("div", { className: "m01-logic__editor" });
|
||||
// 로직 상세 — 왼쪽 지금 내용 · 오른쪽 접는 패널 「원문」
|
||||
@@ -166,8 +176,7 @@ export async function mountM01LogicLab(
|
||||
list.setMarks(marks, extra, aliases);
|
||||
const n = Object.keys(drafts).length;
|
||||
pending.textContent = n ? tx("Bar_Pending", { n }) : "";
|
||||
saveButton.disabled = discardButton.disabled = n === 0;
|
||||
saveButton.hidden = discardButton.hidden = n === 0; // 저장 안 한 고친 것이 없으면 숨김
|
||||
saveButton.disabled = discardButton.disabled = n === 0 && !stack.dirty(); // 로직 줄 · 수식 둘 다 봄
|
||||
};
|
||||
|
||||
const touch = (): void => {
|
||||
@@ -307,6 +316,7 @@ export async function mountM01LogicLab(
|
||||
};
|
||||
|
||||
let allLogics: LogicSummary[] = [];
|
||||
let saving = false; // [저장] 뒤 목록 다시 읽는 중 — 거름 되살림이 연 로직을 떠나지 않게(저장 뒤 다시 엶)
|
||||
|
||||
const reload = async (): Promise<void> => {
|
||||
const [logics, subs] = await Promise.all([fetchLogics(), fetchLogicSubs()]);
|
||||
@@ -324,7 +334,7 @@ export async function mountM01LogicLab(
|
||||
restore: true,
|
||||
onPick: (sub, detail) => {
|
||||
list.setPick(sub, detail);
|
||||
if (opened) void leave().then((ok) => ok && show(null));
|
||||
if (opened && !saving) void leave().then((ok) => ok && show(null));
|
||||
},
|
||||
}),
|
||||
);
|
||||
@@ -351,8 +361,21 @@ export async function mountM01LogicLab(
|
||||
}
|
||||
};
|
||||
|
||||
/** 부르는 곳 까닭 띠 */
|
||||
const usedBy = (used: string[]): void => {
|
||||
errors.replaceChildren(
|
||||
el("strong", { text: tx("Delete_Used") }),
|
||||
...used.map((x) => el("div", { text: x })),
|
||||
);
|
||||
errors.hidden = false;
|
||||
};
|
||||
|
||||
const onDelete = async (): Promise<void> => {
|
||||
if (!opened?.row) return;
|
||||
if (opened.origKey) {
|
||||
const used = (await fetchLogicUsers(opened.origKey).catch(() => null))?.쓰는곳 ?? [];
|
||||
if (used.length) return usedBy(used); // 확인 창 전에 막음
|
||||
}
|
||||
if (
|
||||
!(await showConfirmDialog(
|
||||
tx("Confirm_Delete", { v: opened.row.원문번호 || opened.row.키 }),
|
||||
@@ -360,21 +383,38 @@ export async function mountM01LogicLab(
|
||||
))
|
||||
)
|
||||
return;
|
||||
if (opened.origKey === null) {
|
||||
delete drafts[opened.id];
|
||||
const was = opened;
|
||||
if (was.origKey === null) {
|
||||
delete drafts[was.id];
|
||||
persist();
|
||||
show(null);
|
||||
return;
|
||||
}
|
||||
drafts[opened.id] = pick(opened, null);
|
||||
opened.row = null;
|
||||
persist();
|
||||
show(opened);
|
||||
const key = was.origKey;
|
||||
showLoadingOverlay();
|
||||
try {
|
||||
const one = await fetchLogic(key); // 지금 판(캐시에 옛 판이 남았어도)
|
||||
const done = await saveRevised((ok) => removeLogic(key, one.file, one.version, ok));
|
||||
if (!done) return; // 개정 확인 취소
|
||||
delete drafts[was.id];
|
||||
await stack.discard(); // 그 로직 수식 캐시도 비움
|
||||
await reload();
|
||||
show(null); // 로직 목록
|
||||
showToast(tx("Delete_Done", { v: was.row?.원문번호 || key }), "success");
|
||||
} catch (error) {
|
||||
const used =
|
||||
error instanceof ApiError ? (error.detail as { 쓰는곳?: string[] })?.쓰는곳 : null;
|
||||
if (used?.length) usedBy(used);
|
||||
else failed(error);
|
||||
} finally {
|
||||
hideLoadingOverlay();
|
||||
}
|
||||
};
|
||||
|
||||
const onDiscard = async (): Promise<void> => {
|
||||
if (!(await showConfirmDialog(tx("Confirm_Discard"), tx("Bar_Discard")))) return;
|
||||
drafts = {};
|
||||
await stack.discard();
|
||||
persist();
|
||||
const was = opened;
|
||||
if (was && was.origKey !== null)
|
||||
@@ -393,7 +433,10 @@ export async function mountM01LogicLab(
|
||||
byFile.set(d.file, part);
|
||||
}
|
||||
if (!byFile.size) {
|
||||
showToast(tx("Save_Nothing"), "info");
|
||||
// 수식만 고침 — 줄 쌓기 정본 저장
|
||||
if (!stack.dirty()) return void showToast(tx("Save_Nothing"), "info");
|
||||
if (await stack.save()) showToast(tx("Save_Done"), "success");
|
||||
persist();
|
||||
return;
|
||||
}
|
||||
showLoadingOverlay();
|
||||
@@ -403,13 +446,16 @@ export async function mountM01LogicLab(
|
||||
if (!saved) return; // 개정 확인 취소 = 캐시 그대로
|
||||
const was = opened;
|
||||
drafts = {};
|
||||
await reload();
|
||||
saving = true;
|
||||
await reload().finally(() => (saving = false));
|
||||
showToast(tx("Save_Done"), "success");
|
||||
const newKey = was?.row && !was.row.키 ? saved.added?.at(-1)?.key : undefined;
|
||||
const made = newKey ? allLogics.find((x) => x.키 === newKey) : undefined;
|
||||
if (made) await open(logicId(made.구분, made.키), made.구분, made.키);
|
||||
else if (was?.row?.키) await open(logicId(was.sub, was.row.키), was.sub, was.row.키);
|
||||
else show(null);
|
||||
if (opened) await stack.save(); // 수식 고침도 같은 [저장]에(여는 중이면 다 읽고 · 새 로직 = 새 키로 이어 받은 뒤 · 고침 없으면 그냥)
|
||||
persist();
|
||||
} catch (error) {
|
||||
failed(error);
|
||||
} finally {
|
||||
@@ -456,15 +502,21 @@ export async function mountM01LogicLab(
|
||||
}
|
||||
};
|
||||
|
||||
const saveButton = createButton({ label: tx("Bar_Save"), onClick: () => void onSave() });
|
||||
const saveButton = createButton({
|
||||
label: tx("Bar_Save"),
|
||||
iconStart: "check",
|
||||
onClick: () => void onSave(),
|
||||
});
|
||||
const discardButton = createButton({
|
||||
label: tx("Bar_Discard"),
|
||||
variant: "ghost",
|
||||
iconStart: "undo",
|
||||
onClick: () => void onDiscard(),
|
||||
});
|
||||
const copyButton = createButton({
|
||||
label: tl("Copy_Open"),
|
||||
variant: "ghost",
|
||||
iconStart: "copy",
|
||||
onClick: () =>
|
||||
opened?.origKey
|
||||
? void openCopyScreen({
|
||||
@@ -483,9 +535,19 @@ export async function mountM01LogicLab(
|
||||
status: [pending],
|
||||
buttons: [
|
||||
views.root,
|
||||
createButton({ label: tl("New"), variant: "filled", onClick: () => void onNew() }),
|
||||
createButton({
|
||||
label: tl("New"),
|
||||
variant: "filled",
|
||||
iconStart: "plus",
|
||||
onClick: () => void onNew(),
|
||||
}),
|
||||
copyButton,
|
||||
createButton({ label: tx("Bar_Delete"), variant: "danger", onClick: () => void onDelete() }),
|
||||
createButton({
|
||||
label: tx("Bar_Delete"),
|
||||
variant: "danger",
|
||||
iconStart: "trash",
|
||||
onClick: () => void onDelete(),
|
||||
}),
|
||||
discardButton,
|
||||
saveButton,
|
||||
],
|
||||
|
||||
@@ -61,3 +61,8 @@
|
||||
background: color-mix(in srgb, var(--color-accent) 14%, transparent);
|
||||
box-shadow: -3px 0 0 var(--color-accent);
|
||||
}
|
||||
|
||||
/* 원문 손잡이 = 브라우저 오른쪽 끝(요구 687-17) — 칸을 페이지 오른쪽 여백(로직 칸 12 + 패널 24)만큼 밀어 붙임 · 본문이 그 자리까지 */
|
||||
html:not([data-size="compact"], [data-size="medium"]) .m01-master__panel .m01-logic .m01lab-side {
|
||||
margin-inline-end: calc(-1 * (var(--spacing-12) + var(--spacing-24)));
|
||||
}
|
||||
|
||||
@@ -307,6 +307,18 @@ export const saveFiles = (
|
||||
개정확인 = false,
|
||||
): Promise<{ files: { file: string; version: string }[] }> => request("/save", { files, 개정확인 });
|
||||
|
||||
/** 로직을 부르는 곳(지우기 전 · 804-7) — 다른 수식 문서 · 일위대가 조합 · M02 단가산출 */
|
||||
export const fetchLogicUsers = (key: string): Promise<{ 쓰는곳: string[] }> =>
|
||||
request(`/logic/users?key=${encodeURIComponent(key)}`);
|
||||
|
||||
/** 로직 지우기(804-7) — 로직 줄 + 수식 문서 · 409 {쓰는곳} = 부르는 곳 있어 막음 · 409 {개정} = 정본 개정 확인 */
|
||||
export const removeLogic = (
|
||||
key: string,
|
||||
file: string,
|
||||
version: string,
|
||||
개정확인 = false,
|
||||
): Promise<unknown> => request("/logic/remove", { key, file, version, 개정확인 });
|
||||
|
||||
/** 본떠 만들기 — 정본·자체 둘 다 본뜸 · 새 키(`GX…`)는 서버가 줌(계약 5장) */
|
||||
export const copyLogic = (
|
||||
key: string,
|
||||
|
||||
@@ -27,7 +27,7 @@ const TEXT = {
|
||||
Bar_Back: ["← 로직 목록", "← Logics"],
|
||||
Bar_Delete: ["로직 삭제", "Delete logic"],
|
||||
Bar_Save: ["저장", "Save"],
|
||||
Bar_Discard: ["캐시 삭제", "Clear cache"],
|
||||
Bar_Discard: ["되돌림", "Revert"],
|
||||
Bar_Pending: ["저장 안 한 로직 {n} 개", "{n} unsaved logics"],
|
||||
Head_File: ["파일", "File"],
|
||||
Head_Key: ["키", "Key"],
|
||||
@@ -101,12 +101,14 @@ const TEXT = {
|
||||
Lab_Title: ["단가산출 기초", "Unit-price logic"],
|
||||
Loading: ["불러오는 중", "Loading"],
|
||||
Confirm_Delete: [
|
||||
"로직 「{v}」 을 지울까요? [저장] 때 파일에서 빠짐",
|
||||
"Delete logic “{v}”? Removed on save",
|
||||
"로직 「{v}」 을 지울까요? 로직 · 수식이 서버에서 바로 지워짐",
|
||||
"Delete logic “{v}”? Logic and formula are removed on the server now",
|
||||
],
|
||||
Delete_Done: ["로직 「{v}」 지움", "Deleted logic “{v}”"],
|
||||
Delete_Used: ["부르는 곳이 있어 못 지움", "In use — not deleted"],
|
||||
Confirm_Discard: [
|
||||
"저장 안 한 고친 것(캐시)을 모두 지울까요?",
|
||||
"Clear all unsaved edits (cache)?",
|
||||
"저장 안 한 고친 것(기본정보 · 수식)을 버리고 저장 뒤 상태로 되돌릴까요?",
|
||||
"Discard unsaved edits (info · formula) and revert to the saved state?",
|
||||
],
|
||||
Confirm_Leave: [
|
||||
"저장 안 한 로직이 있음 — 다른 로직을 열어도 고친 것은 남음",
|
||||
|
||||
@@ -517,15 +517,9 @@ ${first.file}`
|
||||
aliasSec
|
||||
.querySelector(".ui-collapsible__title")
|
||||
?.addEventListener("click", () => void openAlias());
|
||||
// 시험 페이지 「줄 쌓기 단가산출」(PLAN 804-1) — 새 탭 · 지울 때 이 덩이 + main.py 마운트 + vite.config.ts 줄 + tools/line-stack-test 폴더
|
||||
const lineStackSec = section("시험: 줄 쌓기", null);
|
||||
lineStackSec.classList.add("m01-side__leaf");
|
||||
lineStackSec
|
||||
.querySelector(".ui-collapsible__title")
|
||||
?.addEventListener("click", () => window.open("/tools/line-stack-test/", "_blank"));
|
||||
const root = el("div", {
|
||||
className: "m01-side ui-sidebar-panel",
|
||||
children: [...groups, logic, createDivider(), optionsSec, aliasSec, lineStackSec],
|
||||
children: [...groups, logic, createDivider(), optionsSec, aliasSec],
|
||||
});
|
||||
attachCollapsible(root);
|
||||
void Promise.all(GROUPS.filter((g) => g !== FUEL_GROUP).map(refresh)).catch((error) =>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""단위 · 금액 표준 표 읽기 — `GET /api/m02/df/standards` → {unit, money, saved}.
|
||||
|
||||
부르는 곳 = 단위 고르기(`M02_DF_UnitPick.ts`) · 끝수 표준(`M02_DF_Round.ts`).
|
||||
표 = `_standard/` 파일(git 이 따라감) · 권한 = 시스템 관리자(`main.py` 한 줄).
|
||||
표 = `_standard/` 파일(git 이 따라감) · 권한 = 로그인만(읽기만 · `main.py` 한 줄 · M02 줄 쌓기 단위 고르기).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* =============================================================================
|
||||
* M02_DF_UnitPick.ts
|
||||
* 단가산출 흐름도 공용 단위 고르기 — 치면 걸러진 목록 · 고르기만(손글 안 받음) · 「-」 = 없음
|
||||
* 단가산출 흐름도 공용 단위 고르기 — 치면 걸러진 목록 · 고르기만(손글 안 받음 · `free` = 목록에 없는 친 글도 씀) · 「-」 = 없음
|
||||
* 목록 = 단위 표준(산림품셈 1-2-2) 단위 + 부른 쪽이 주는 마스터 결과단위 등 + 원
|
||||
* 표준 표 읽는 길은 `setUnitSource` 로 바꿔 끼움(기본 = 서버 /standards) — 다른 판도 이것을 씀
|
||||
* 글 [한, 영] 짝은 아래 TEXT
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
import { currentLanguageIndex } from "@ui/ui_template_locale";
|
||||
import { createButton, el } from "@ui/ui_template_elements";
|
||||
import type { UiSize } from "@ui/ui_template_button";
|
||||
import { cfStandards } from "./M02_DF_Api";
|
||||
import "./M02_DF_UnitPick.css";
|
||||
import { createEmpty } from "@ui/ui_template_empty";
|
||||
@@ -17,6 +18,7 @@ const TEXT = {
|
||||
None: ["없음", "none"],
|
||||
Filter: ["단위를 쳐서 찾음", "Type to filter units"],
|
||||
Empty: ["맞는 단위가 없음 — 목록에서만 고름", "No match — pick from the list only"],
|
||||
Free: ["「{u}」 그대로 씀", "Use “{u}”"],
|
||||
Pick: ["단위 고르기", "Pick a unit"],
|
||||
} as const;
|
||||
|
||||
@@ -99,6 +101,10 @@ export interface UnitPickOptions {
|
||||
extra?: string[];
|
||||
readOnly?: boolean;
|
||||
ariaLabel?: string;
|
||||
/** 크기 단(줄 쌓기 24 등) — 없으면 옛 꼴 */
|
||||
size?: UiSize;
|
||||
/** 목록에 없는 친 글도 단위로 씀(줄 쌓기 · 804-7) */
|
||||
free?: boolean;
|
||||
}
|
||||
|
||||
/** 단위 고르기 — 단추를 누르면 걸러지는 목록 · 목록에서 고르기만 됨 */
|
||||
@@ -108,6 +114,7 @@ export function createUnitPick(opts: UnitPickOptions): HTMLElement {
|
||||
const open = createButton({
|
||||
label: opts.value || NO_UNIT,
|
||||
variant: "ghost",
|
||||
size: opts.size,
|
||||
disabled: opts.readOnly,
|
||||
onClick: () => toggle(),
|
||||
});
|
||||
@@ -136,13 +143,18 @@ export function createUnitPick(opts: UnitPickOptions): HTMLElement {
|
||||
const q = input.value.trim().toLowerCase();
|
||||
shown = all.filter((u) => u === NO_UNIT || !q || u.toLowerCase().includes(q));
|
||||
if (q && !NO_UNIT.includes(q)) shown = shown.filter((u) => u !== NO_UNIT);
|
||||
const typed = input.value.trim();
|
||||
if (opts.free && typed && !all.includes(typed)) shown.push(typed); // 맨 끝 「그대로 씀」 줄
|
||||
at = Math.min(at, Math.max(0, shown.length - 1));
|
||||
list.replaceChildren(
|
||||
...(shown.length
|
||||
? shown.map((u, i) => {
|
||||
const b = el("button", {
|
||||
className: `ui-list-row ui-sidebar-row m02-df-unitpick__item${i === at ? " is-on" : ""}${u === (opts.value || NO_UNIT) ? " is-now" : ""}`,
|
||||
text: label(u),
|
||||
text:
|
||||
opts.free && u === input.value.trim() && !all.includes(u)
|
||||
? tu("Free").replace("{u}", u)
|
||||
: label(u),
|
||||
attrs: { type: "button", role: "option" },
|
||||
});
|
||||
b.addEventListener("click", () => pick(u));
|
||||
|
||||
@@ -5,9 +5,11 @@
|
||||
* [초기화] = 서버 `-reset` 미리 보기(걷힐 고침 수) → 물음 → 그 층에 씀 → 다시 읽음.
|
||||
* 덮개 · 뺀줄 · 더한줄 단가를 M01 키 값으로 되돌림 · 옵션(입력값) · 기본정보는 그대로(서버 740-1).
|
||||
* 둘 다 쓰기 권한이 있는 층에서만 보임(`update`) · 상세에 저장 안 한 고침이 있으면 먼저 물음.
|
||||
* 단가산출 상세(804-8 · M01 단가산출 기초와 같은 단추 규칙) = [새로고침] 숨김 · [되돌림] = 물음 → 다시 읽음(수식 캐시도 비움) ·
|
||||
* [되돌림] · [저장] = 고침 있을 때만(`dim`)
|
||||
* ========================================================================== */
|
||||
|
||||
import { createButton, showToast } from "@ui/ui_template_elements";
|
||||
import { createButton, showConfirmDialog, showToast } from "@ui/ui_template_elements";
|
||||
import { currentLanguageIndex } from "@ui/ui_template_locale";
|
||||
import { askConfirm } from "@ui/ui_template_modal";
|
||||
import {
|
||||
@@ -41,6 +43,11 @@ const TEXT = {
|
||||
Done: ["M01 값으로 되돌렸음", "Reverted to M01 values"],
|
||||
DeleteAsk: ["「{name}」 을(를) 지울까?", "Delete 「{name}」?"],
|
||||
Deleted: ["지웠음", "Deleted"],
|
||||
Undo: ["되돌림", "Revert"],
|
||||
UndoAsk: [
|
||||
"저장 안 한 고친 것(기본정보 · 수식)을 버리고 저장 뒤 상태로 되돌릴까?",
|
||||
"Discard unsaved edits (info · formula) and revert to the saved state?",
|
||||
],
|
||||
Failed: ["못 했음 — {why}", "Failed — {why}"],
|
||||
} as const;
|
||||
const tx = (key: keyof typeof TEXT): string =>
|
||||
@@ -58,6 +65,8 @@ export interface MainActionsCtx {
|
||||
open: (sel: Selection | null) => Promise<void>;
|
||||
/** 좌측 목록을 다시 읽음 */
|
||||
changed: () => void;
|
||||
/** 페이지 머리 단추 — 단가산출 상세에서 숨김 · 흐림 */
|
||||
page: { reload: HTMLButtonElement; save: HTMLButtonElement };
|
||||
}
|
||||
|
||||
/** 문서 안 모든 `뺀줄` 길이 합(단가산출 · 일위대가 줄 안 덮개까지) */
|
||||
@@ -74,10 +83,15 @@ const why = (error: unknown): string => (error instanceof Error ? error.message
|
||||
export function createMainActions(ctx: MainActionsCtx): {
|
||||
reset: HTMLButtonElement;
|
||||
del: HTMLButtonElement;
|
||||
undo: HTMLButtonElement;
|
||||
update: (writable: boolean) => void;
|
||||
/** 고침 있음 — 단가산출 상세의 [되돌림] · [저장] 흐림 */
|
||||
dim: (dirty: boolean) => void;
|
||||
} {
|
||||
const reset = createButton({ label: tx("Reset"), variant: "ghost" });
|
||||
const del = createButton({ label: tx("Delete"), variant: "danger-ghost" });
|
||||
const reset = createButton({ label: tx("Reset"), variant: "ghost", iconStart: "rotateCcw" });
|
||||
const del = createButton({ label: tx("Delete"), variant: "danger-ghost", iconStart: "trash" });
|
||||
const undo = createButton({ label: tx("Undo"), variant: "ghost", iconStart: "undo" });
|
||||
undo.dataset.mainUndo = "";
|
||||
reset.title = tx("ResetTip");
|
||||
del.title = tx("DeleteTip");
|
||||
reset.dataset.mainReset = "";
|
||||
@@ -145,12 +159,27 @@ export function createMainActions(ctx: MainActionsCtx): {
|
||||
}
|
||||
});
|
||||
|
||||
undo.addEventListener("click", async () => {
|
||||
const at = ctx.sel();
|
||||
if (at && (await showConfirmDialog(tx("UndoAsk"), tx("Undo")))) await ctx.open(at);
|
||||
});
|
||||
|
||||
const sk = (): boolean => target()?.kind === "sangeun";
|
||||
const dim = (dirty: boolean): void => {
|
||||
undo.disabled = !dirty;
|
||||
ctx.page.save.disabled = sk() && !dirty;
|
||||
};
|
||||
return {
|
||||
reset,
|
||||
del,
|
||||
undo,
|
||||
update: (writable) => {
|
||||
const on = writable && !!target();
|
||||
reset.hidden = del.hidden = !on;
|
||||
undo.hidden = !on || !sk();
|
||||
if (sk()) ctx.page.reload.hidden = true;
|
||||
dim(false);
|
||||
},
|
||||
dim,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
/* =============================================================================
|
||||
* M02_MasterTemplete_Sangeun_Detail.ts
|
||||
* M02 단가산출(SK) 상세(PLAN 717-2b · 724-1 · 요구서 4장 3 · 4 · 13 · 17 · 24) — M01 「단가산출 로직」 상세 부품을 M02 길로.
|
||||
* 보기 둘(`_Sangeun_Tabs` · 799-2) = 수식(공용 수식 문서 화면 · 802-4b-2 · 저장은 그 탭 [수식 저장] → 판 · 수식 칸 새로) · 단가산출 상세(공용 상세 `LogicLab_Ho` · SK 셈 답 줄 = `hoView` · 줄 모달 그 자리 고침 =
|
||||
* 보기 둘(`_Sangeun_Tabs` · 799-2) = 수식(공용 수식 문서 화면 · 802-4b-2 · 저장 = 페이지 [저장] 한 번(804-8 · 수식 먼저 → 판 · 수식 칸 새로)) · 단가산출 상세(공용 상세 `LogicLab_Ho` · SK 셈 답 줄 = `hoView` · 줄 모달 그 자리 고침 =
|
||||
* HoEdit → SK 덮개 · 더한줄 · 줄 빼기 · 별도계상) · 기본정보 = 바탕 로직(맨 위 접힘) — 시트 · 인력·재료·경비 · 시험 계산 · 텍스트 수식은 걷음
|
||||
* 고침 = SK 덮개 · 더한줄(`Sangeun_Pick`) · 뺀줄(꼴 = 서버 `M02_Sangeun_Doc`) · 값 = 서버 셈 길 최신 응답만
|
||||
* 옵션 칸 = SK `입력값`(727-3 · 저장 · 열린입력만 시험 값) · [저장] = 화면 문서 → 서버가 다시 셈(원값 · 바탕판)
|
||||
* 둘째 기초(727-2) = 서버 `펼침` 세부 줄 · 인력 · 재료 · 경비 상자에 함께 + 꼬리표(기초 둘 이상) · 세부 덮개 ·
|
||||
* 뺀줄 · [기초 빼기] · M01 개정 알림 · 옵션 칸 = 바탕 입력 + 더한줄 기초 입력
|
||||
* [항목 추가] 기초 = 바탕 비었으면 바탕(727-2 · 단위가 비거나 「식」이면 기초 결과단위) · 있으면 더한줄 · 바탕 없이 줄만도 됨
|
||||
* 단가산출 화면(`source` · 804-8) = M01 과 같은 꼴 — 보기 고르기는 페이지 머리 줄(`slot`) · [항목 추가] · [율 줄 추가] 걷음
|
||||
* (줄 더하기 = 수식 탭 [+] 단가산출 복사) · 수식 고침도 페이지 [저장] 한 번에(`flush` 먼저) · 고침 알림 = 기본 + 수식
|
||||
* ========================================================================== */
|
||||
|
||||
import { createBadge } from "@ui/ui_template_badge";
|
||||
@@ -36,6 +38,7 @@ import { calcSangeun, removedStrip } from "./M02_MasterTemplete_Sangeun_Cover";
|
||||
import { openSangeunLinePicker } from "./M02_MasterTemplete_Sangeun_Pick";
|
||||
import { openRateLine } from "./M02_MasterTemplete_Sangeun_Rate";
|
||||
import { createSangeunTabs } from "./M02_MasterTemplete_Sangeun_Tabs";
|
||||
import { tl } from "../M01_MasterData/M01_MasterData_UI_LogicLab_Text";
|
||||
import { createSangeunSide, tabOf, type SourceTab } from "./M02_MasterTemplete_Sangeun_Side";
|
||||
import { baseUnitOf, buildUnitSwap, type SwapHow } from "./M02_MasterTemplete_Sangeun_Unit";
|
||||
import {
|
||||
@@ -65,6 +68,8 @@ export interface SangeunDetailOptions {
|
||||
source?: boolean;
|
||||
/** 수식 탭 저장 뒤 그 SK 새 판(페이지 [저장] 409 막음 · 802-4b-2) */
|
||||
onVersion?: (version: string) => void;
|
||||
/** 페이지 머리 줄 빈 자리(804-8) — 보기 고르기를 M01 처럼 머리 줄에 */
|
||||
slot?: HTMLElement;
|
||||
}
|
||||
|
||||
const LIB = { read: "/m02/logic-lib", calc: "/m02/logic-lib" };
|
||||
@@ -74,7 +79,13 @@ export function mountSangeunDetail(
|
||||
host: HTMLElement,
|
||||
doc0: unknown,
|
||||
opt: SangeunDetailOptions,
|
||||
): { getDoc: () => SkDoc; getValues: () => Record<string, string>; destroy: () => void } {
|
||||
): {
|
||||
getDoc: () => SkDoc;
|
||||
getValues: () => Record<string, string>;
|
||||
/** 페이지 [저장] 앞 — 수식 고침 저장 · 실패면 던짐 */
|
||||
flush: () => Promise<void>;
|
||||
destroy: () => void;
|
||||
} {
|
||||
setLogicApiPaths(LIB); // 상세 부품 길 = M02 로그인 길(717-1)
|
||||
const doc = clone((doc0 ?? {}) as SkDoc);
|
||||
let saved = JSON.stringify(doc);
|
||||
@@ -100,7 +111,13 @@ export function mountSangeunDetail(
|
||||
const head = el("div", { className: "m02-sk__head", attrs: { "data-sk-head": "" } });
|
||||
const notes = el("div");
|
||||
const editor = el("div", { className: "m01-logic__editor" });
|
||||
const tabs = createSangeunTabs(editor, { edit, scope, onSaved: (v) => void formulaSaved(v) });
|
||||
let refreshing: Promise<void> = Promise.resolve();
|
||||
const tabs = createSangeunTabs(editor, {
|
||||
edit,
|
||||
scope,
|
||||
onSaved: (v) => void (refreshing = formulaSaved(v)),
|
||||
page: opt.source ? () => report() : undefined, // 804-8 페이지 [저장] · [되돌림]
|
||||
});
|
||||
/** 수식 탭 저장 = 파일 판 · `수식` 칸이 바뀜 — 페이지 판 · 이 화면 문서 `수식` 을 새로(상세 탭 셈이 씀) */
|
||||
async function formulaSaved(version: string): Promise<void> {
|
||||
opt.onVersion?.(version);
|
||||
@@ -132,7 +149,7 @@ export function mountSangeunDetail(
|
||||
touch();
|
||||
}),
|
||||
});
|
||||
addButton.hidden = !edit;
|
||||
addButton.hidden = !edit || !!opt.source;
|
||||
// 778-4 율 줄 — 기준 줄 후보 = 지금 셈 답 줄(세부 줄 뺌) · 새 줄은 맨 끝(앞 줄만 가리킴)
|
||||
const rateButton = createButton({
|
||||
label: L("M02_SkRateAdd"),
|
||||
@@ -144,7 +161,7 @@ export function mountSangeunDetail(
|
||||
(line) => ((doc.더한줄 = [...(doc.더한줄 ?? []), withFirst(line)]), touch()),
|
||||
),
|
||||
});
|
||||
rateButton.hidden = !edit;
|
||||
rateButton.hidden = !edit || !!opt.source;
|
||||
/** 둘째 기초(728-2) — 수량 처음값 = 바탕 기준단위 1 개 = 그 기초 기준단위 몇 개(같은 성격 · 다르면 1) */
|
||||
async function addBase(line: { 로직: string; 수량: number }): Promise<void> {
|
||||
let qty = 1;
|
||||
@@ -177,12 +194,22 @@ export function mountSangeunDetail(
|
||||
children: [
|
||||
head,
|
||||
notes,
|
||||
el("div", { className: "m02-sk__head", children: [tabs.picker, addButton, rateButton] }),
|
||||
...(opt.slot
|
||||
? []
|
||||
: [
|
||||
el("div", {
|
||||
className: "m02-sk__head",
|
||||
children: [tabs.picker, addButton, rateButton],
|
||||
}),
|
||||
]),
|
||||
tabs.host,
|
||||
editor,
|
||||
],
|
||||
});
|
||||
host.replaceChildren(side ? side.wrap(main) : main);
|
||||
opt.slot?.replaceChildren(
|
||||
el("span", { className: "ui-toolbar m02-main__tools", children: [tabs.picker] }),
|
||||
);
|
||||
|
||||
const opened = (): boolean => (doc.열린입력 ?? []).length > 0;
|
||||
// 바탕 없는 SK 의 지문 = 이름 · 단위 따라 바뀜 — 바탕이 있을 때만 M01 개정으로 봄(725-3) · 둘째 기초 지문(727-2)
|
||||
@@ -229,8 +256,12 @@ export function mountSangeunDetail(
|
||||
.filter((i) => !seen.has(i.이름) && seen.add(i.이름));
|
||||
}
|
||||
|
||||
/** 고침 알림 — 기본(이름 · 단위 · 옵션 · 덮개) + 수식 탭 */
|
||||
function report(): void {
|
||||
if (alive) opt.onChanged(edit && (JSON.stringify(doc) !== saved || tabs.dirty()));
|
||||
}
|
||||
const touch = (again = true): void => {
|
||||
opt.onChanged(edit && JSON.stringify(doc) !== saved);
|
||||
report();
|
||||
if (again) void recalc();
|
||||
else drawHead();
|
||||
};
|
||||
@@ -516,8 +547,14 @@ export function mountSangeunDetail(
|
||||
return clone(doc);
|
||||
},
|
||||
getValues: () => ({ ...values }), // 723-3 — 일위대가 줄 모달 [적용] = 옵션을 그 줄에 남김
|
||||
flush: async () => {
|
||||
if (!tabs.dirty()) return;
|
||||
if (!(await tabs.save())) throw new Error(tl("View_Equation"));
|
||||
await refreshing; // 새 판 · `수식` 칸 받은 뒤 페이지가 저장
|
||||
},
|
||||
destroy: () => {
|
||||
alive = false;
|
||||
if (tabs.dirty()) void tabs.discard(); // 페이지가 버리고 떠남 — 수식 캐시도 비움
|
||||
host.replaceChildren();
|
||||
},
|
||||
};
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
* M02_MasterTemplete_Sangeun_Tabs.ts
|
||||
* M02 단가산출(SK) 상세 보기 고르기(PLAN 799-2 · 804-2 · 804-6 · 요구 686-1) — 수식(첫 탭 · 줄 쌓기) · 단가산출 상세.
|
||||
* 수식 = 공용 줄 쌓기 화면(`A00_Common/line_stack_screen.ts` · M01 과 같은 꼴) — 길 앞머리만 M02 사본 길
|
||||
* (`/m02/layers/{층}/sangeun/{키}/doc` · 802-4a) · 층 쓰기 권한 없으면 읽기만 · 저장 = 그 탭 [수식 저장](페이지 [저장] 아님)
|
||||
* (`/m02/layers/{층}/sangeun/{키}/doc` · 802-4a) · 층 쓰기 권한 없으면 읽기만
|
||||
* 저장(804-8 · `page` 있음 = 단가산출 화면) = 페이지 [저장] · [되돌림] 한 번에 — 탭 위 저장 단추 없음(M01 과 같음)
|
||||
* 옛 수식 문서 탭(802-4b-2)은 804-6 에 걷음 · 단가산출 상세 = 상세가 쥔 `editor`(수식일 때 숨김)
|
||||
* 저장 안 한 SK(일위대가 줄 모달 · 새 SK) = 「저장한 단가산출만」 글
|
||||
* ========================================================================== */
|
||||
@@ -18,8 +19,10 @@ type View = "stack" | "ho";
|
||||
export interface SkEquationOptions {
|
||||
scope: { layer: Layer; owner: string | null };
|
||||
edit: boolean;
|
||||
/** [수식 저장] · [단가 갱신] · [재생성] 뒤 그 SK 새 판 */
|
||||
/** 수식 저장 · [단가 갱신] · [재생성] 뒤 그 SK 새 판 */
|
||||
onSaved: (version: string) => void;
|
||||
/** 페이지가 저장 · 되돌림을 맡음(804-8) — 수식 고침 있음이 바뀔 때 부름 */
|
||||
page?: () => void;
|
||||
}
|
||||
|
||||
/** SK 수식 길 — 꼬리 물음(`/table?key=…`)에 주인 물음을 이어 붙임 */
|
||||
@@ -42,6 +45,9 @@ export function createSangeunTabs(
|
||||
/** 저장한 SK 키(빈 키 = 저장 전) */
|
||||
draw: (key: string) => void;
|
||||
canLeave: () => Promise<boolean>;
|
||||
dirty: () => boolean;
|
||||
save: () => Promise<boolean>;
|
||||
discard: () => Promise<void>;
|
||||
} {
|
||||
const stack = createStackScreen({
|
||||
base: "",
|
||||
@@ -49,6 +55,7 @@ export function createSangeunTabs(
|
||||
cache: "m02_sk_doc_drafts",
|
||||
copy: { route: (k, p) => skDocPath(opt.scope, k, p), onSaved: opt.onSaved },
|
||||
scope: opt.scope, // 804-4 다른 단가산출 · 일위대가 복사
|
||||
page: opt.page,
|
||||
});
|
||||
let key = "";
|
||||
let view: View = "stack";
|
||||
@@ -81,5 +88,8 @@ export function createSangeunTabs(
|
||||
if (view === "stack") stack.draw(k);
|
||||
},
|
||||
canLeave: () => stack.canLeave(),
|
||||
dirty: () => stack.dirty(),
|
||||
save: () => stack.save(),
|
||||
discard: () => stack.discard(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
* M02_MasterTemplete_Sangeun_View.ts
|
||||
* 단가산출(SK) 상세 자리(717-2a) — 편집 칸 종류 sangeun 이 여는 곳(UI_Main 갈래 한 줄).
|
||||
* 717-2b = 상세 · 고침(`Sangeun_Detail.ts` mountSangeunDetail) · [저장]은 그 손잡이 `getDoc` · 고침 알림 `opt.onChanged`.
|
||||
* 804-8 = 페이지 [저장] 한 번에 수식도(`flush` 먼저 · 새 판) · 보기 고르기는 머리 줄 `slot`.
|
||||
* ========================================================================== */
|
||||
|
||||
import { mountSangeunDetail } from "./M02_MasterTemplete_Sangeun_Detail";
|
||||
@@ -13,12 +14,15 @@ export type SangeunViewOptions = Pick<Selection, "layer" | "owner" | "name"> & {
|
||||
onChanged: (dirty: boolean) => void;
|
||||
/** 수식 탭 저장 뒤 그 SK 새 판(802-4b-2) */
|
||||
onVersion?: (version: string) => void;
|
||||
/** 머리 줄 빈 자리(804-8) */
|
||||
slot?: HTMLElement;
|
||||
};
|
||||
|
||||
export function mountSangeunView(
|
||||
host: HTMLElement,
|
||||
doc: unknown,
|
||||
opt: SangeunViewOptions,
|
||||
): { getDoc: () => unknown; destroy: () => void } {
|
||||
return mountSangeunDetail(host, doc, { ...opt, owner: opt.owner ?? null, source: true }); // 752-1
|
||||
): { getDoc: () => Promise<unknown>; destroy: () => void } {
|
||||
const one = mountSangeunDetail(host, doc, { ...opt, owner: opt.owner ?? null, source: true }); // 752-1
|
||||
return { getDoc: () => one.flush().then(() => one.getDoc()), destroy: one.destroy };
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ import { isPrecombo } from "./M02_MasterTemplete_UI_Side_Precombo";
|
||||
import type { SpreadsheetHandle } from "../A00_Common/spreadsheet/spreadsheet";
|
||||
import type { LinkOptions } from "../A00_Common/spreadsheet/spreadsheet_links";
|
||||
import { createEmpty } from "@ui/ui_template_empty";
|
||||
import { fitButtonRow } from "@ui/ui_template_button";
|
||||
import { createMainActions } from "./M02_MasterTemplete_Main_Actions";
|
||||
export interface Selection {
|
||||
layer: Layer;
|
||||
@@ -140,8 +141,8 @@ export function createMain(
|
||||
extras: MainExtras = {},
|
||||
): MainHandle {
|
||||
const title = el("h2", { className: "m02-main__title", text: L("M02_PickTemplate") });
|
||||
const reload = createButton({ label: L("M02_Reload"), variant: "ghost" });
|
||||
const save = createButton({ label: L("M02_Save"), variant: "filled" });
|
||||
const reload = createButton({ label: L("M02_Reload"), variant: "ghost", iconStart: "refresh" });
|
||||
const save = createButton({ label: L("M02_Save"), variant: "filled", iconStart: "check" });
|
||||
// 단가산출 · 일위대가 상세 [초기화] [삭제](740-2 · 738-2) — 차례 … [초기화] [삭제] [저장]
|
||||
const actions = createMainActions({
|
||||
sel: () => sel,
|
||||
@@ -150,6 +151,7 @@ export function createMain(
|
||||
confirmLeave: () => confirmLeave(),
|
||||
open: (at) => open(at),
|
||||
changed: () => onSaved?.(),
|
||||
page: { reload, save },
|
||||
});
|
||||
// 구조물 도면 · 일위대가 · 면적산출 상세 화면 → 구조물집계표(그 열 고른 채) · 일반 단추(43-5 · 알약 · ghost 아님)
|
||||
const back = createButton({ label: L("M02_Back") });
|
||||
@@ -183,6 +185,7 @@ export function createMain(
|
||||
reload,
|
||||
actions.reset,
|
||||
actions.del,
|
||||
actions.undo,
|
||||
save,
|
||||
...(extras.newItem ? [extras.newItem] : []),
|
||||
],
|
||||
@@ -193,6 +196,7 @@ export function createMain(
|
||||
],
|
||||
});
|
||||
const root = column;
|
||||
fitButtonRow(column.querySelector<HTMLElement>(".m02-main__head")!); // 넘치면 단추 글 대신 아이콘(804-8)
|
||||
const isCombo = (at: Selection | null): boolean =>
|
||||
!!at && at.kind === "table" && at.name === STRUCTURE_TABLE && at.layer !== "project";
|
||||
// 「새 항목」 · 표 접기(제목)는 「구조물 조합」일 때만
|
||||
@@ -373,10 +377,10 @@ export function createMain(
|
||||
host.replaceChildren();
|
||||
if (at.kind === "sangeun") {
|
||||
const { mountSangeunView } = await import("./M02_MasterTemplete_Sangeun_View"); // 717-2a
|
||||
const onChanged = (d: boolean): boolean => (dirty = d && !readOnly);
|
||||
const onChanged = (d: boolean): void => actions.dim((dirty = d && !readOnly));
|
||||
const onVersion = (v: string): string => (version = v); // 수식 탭 저장(802-4b-2)
|
||||
if (mine === seq)
|
||||
editor = mountSangeunView(host, doc, { ...at, readOnly, onChanged, onVersion });
|
||||
editor = mountSangeunView(host, doc, { ...at, readOnly, onChanged, onVersion, slot });
|
||||
return;
|
||||
}
|
||||
if (at.kind === "table") {
|
||||
@@ -601,7 +605,7 @@ export function createMain(
|
||||
);
|
||||
version = info.판;
|
||||
loaded = doc;
|
||||
dirty = false;
|
||||
actions.dim((dirty = false));
|
||||
toDrawing.drop(); // 놓기 기다림 · 보내기 취소는 이 저장으로 끝
|
||||
// 구조물집계표 — 서버가 새 열 파일 · 번호를 만듦 · 번호를 다시 받아 도면 줄 · 카드를 새로
|
||||
// (안 하면 새 열은 다시 열 때까지 「도면 없음」 · 번호 빈칸)
|
||||
|
||||
@@ -52,6 +52,15 @@ html.is-bm-route .m02-main__head {
|
||||
font-size: 18px; /* M01 머리 제목과 같음(39-5) */
|
||||
}
|
||||
|
||||
/* 단가산출 상세(804-8) — 제목이 남는 자리를 차지 · 긴 이름은 제목 안에서 줄바꿈(단추가 아래로 안 떨어짐) */
|
||||
.m02-main__head:has(> .m02-main__slot [data-sk-views]) > .m02-main__title {
|
||||
flex: 1 1 8rem;
|
||||
}
|
||||
|
||||
.m02-main__head:has(> .m02-main__slot [data-sk-views]) > .m02-main__slot {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.m02-main__head .ui-btn {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@@ -281,7 +281,7 @@ export function docOf(doc: StackDoc, items: StackItem[]): StackDoc {
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 줄 쌓기가 못 고치는 문서 칸(보존은 됨 · 수식 탭에서 고침) */
|
||||
/** 줄 쌓기가 못 고치는 문서 칸(보존은 됨 · 시험이 봄 · 화면 안내 줄은 804-8 에 걷음) */
|
||||
export function notesOf(doc: FormulaDoc): string[] {
|
||||
const out: string[] = [];
|
||||
if (doc.단계?.length) out.push("단계");
|
||||
|
||||
@@ -407,3 +407,40 @@ M02 의 엑셀 불러오기에서 경쟁사 2개의 프로그램의 출력을
|
||||
|
||||
1. 현 단가산출의 표현 중 수식 탭은 삭제하고 현재 줄 쌓기 탭을 기본보기로 변걍 후 탭 제목을 수식으로 변경.
|
||||
2. 전역 깃 싱크. 현재 데스크탑 워크트리에 기술정보 db폴더변경되었고 tool 폴더 하위 est 테스트 프로그램은 개발 중이나 전역 깃 싱크.
|
||||
|
||||
## 요구 687 (11-08 · 사용자 · 전역 싱크 뒤 줄 쌓기 수정 18 — 그림 `요구687_1_기계줄.png` · `요구687_2_재료수량.png` · `요구687_3_요소키.png`)
|
||||
|
||||
전역 깃 싱크 후 이어서 작업
|
||||
1. M01 단가산출 기초의 항목에서 로직 삭제 안됨. 캐시에만 삭제하는것으로 보임. 삭제후 리스트로 이동
|
||||
2. M01 단가산출 기초의 페이지에서 상단의 신규로직 부터 저장 버튼까지 역할 점검, 고침 버림과 수식 저장도 같이 확인
|
||||
3. M01 단가산출 기초의 하단 합계 컨테이너 상시보이기
|
||||
4. 좌측 영역 그룹이(비우면 번호 없음) 라벨 삭제, 의미 없음.
|
||||
5. 좌측영역 체크박스와 가나다 라벨 필요없음.
|
||||
6. 좌측 영역 그룹제목 영역 행배치 정렬 안됨.
|
||||
7. 좌측영역의 세부항목을 잘하면 1행 구조로도 표현할 수 있을것같은데... 검토
|
||||
8. 좌측 영역의에서 재료인 경우 수량은
|
||||
9. 좌측영역에서 재료, 인력,기계인 경우 버튼을 누르면 조회창에 해당 단어를 검색한 결과가 보이게 변경
|
||||
10. 좌측영역에서 IF문에 의해 결정되는 경우 전체를 나열하는걸로 보이는데 옵션이나 값에 의해 결정된 항목만 보여주는게 어떤지. 공식 표기는 일단 유지.
|
||||
11. [Image #1] 지난 수정에서 잘못 수정한 부분이 그림의 노무비, 재료비, 경비의 항목을 선택하면 아래 공통항목이 드롭다운으로 열리는게 아니라 상단 굴착기+부작용 집게 0.6을 누르면 보이게 변경. 열린 내부의 제목이 링크 걸린걸로 보임
|
||||
12. [Image #2] 수량 부분이 옵션에 결려 있는걸로 보임. 다른 표기법이 있을지 검토
|
||||
13. 좌측영역의 +줄 드랍다운 메뉴는 + 버튼으로 변경
|
||||
14. 좌측영역에서 식 고치기가 의미가 있는지 검토
|
||||
15. 단위는 드롭다운 항목으로 변경하는게 있을 것 같음. 다른 곳에 적용된 이력있음. 대신 누락된 단위가 있는지는 검토 필요.
|
||||
16. M01의 좌측 패널에서 시험:줄 쌓기는 이제 필요없어보임.
|
||||
17. M01의 단가산출 항목 내부의 페이지를 보면 우측에 원문 펼침 버튼이 중간에 애매하게 배치되어 있음. 좌측 여유때문으로 보여짐. 브라우져 우측에 붙게하여 내부공간을 더 효율적으로 사용
|
||||
18. + 버튼으로 변경된 + 줄 드롭다운의 메뉴 중
|
||||
- 율/식은 인력/기계/재료 등에 종속되어 있는 정보가 아닌지.
|
||||
- 다른 단가산출의 이름을 단가산출로 변경해도 무관해보임.
|
||||
- 틀찾기는 이제 삭제한 기능으로 보임
|
||||
- 글/근거의 의미는 모르겠음.
|
||||
- [Image #3] 요소 키값을 사용하고 있는데 확인 어려움. 원인 분석
|
||||
|
||||
## 요구 688 (11-08 · 사용자 · 그룹 닫으면 삭제 단추 밀림)
|
||||
|
||||
추가 좌측영역의 그룹을 닫으면 삭제 버튼이 아래로 밀림 개선 필요
|
||||
|
||||
## 요구 689 (11-08 · 사용자 · M02 단가산출 보기 = M01 과 같게 · 저장 단추 줄바꿈 · 전역 싱크)
|
||||
|
||||
1. m02 단가산출의 보기방식을 m01 의 단가산출 기초와 동일하게 변경. 이때 코드사용은 일원화하되 항목추가에서 m02에서만 가능한 기능이 있르니 조심.(오늘 대화한 적 있음)
|
||||
2. 저장버튼이 아래로 내려가는건 레이아웃이 안깨지게 변경
|
||||
3. 1,2 내용 반영 후 전역 깃싱크.
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 60 KiB |
@@ -411,17 +411,6 @@ app.mount(
|
||||
)
|
||||
logger.info(f"✓ B07 CAD 정적 서빙 경로 등록: /b07-cad → {B07_CAD_DIST_DIR}")
|
||||
|
||||
# 시험 페이지 「줄 쌓기 단가산출」(PLAN 804-1) — 지울 때 이 덩이 + tools/line-stack-test 폴더 + vite.config.ts 「/tools」 줄 + M01 왼쪽 패널 「시험: 줄 쌓기」 덩이
|
||||
app.mount(
|
||||
"/tools/line-stack-test",
|
||||
_NoCacheHtmlStatic(
|
||||
directory=str(Path(__file__).parent / "tools" / "line-stack-test"),
|
||||
html=True,
|
||||
check_dir=False,
|
||||
),
|
||||
name="line-stack-test",
|
||||
)
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
# 기본 엔드포인트
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
@@ -564,10 +553,10 @@ app.include_router(
|
||||
__import__("M01_MasterData.M01_MasterData_Alias", fromlist=["router"]).router,
|
||||
dependencies=system_admin_only,
|
||||
) # 별칭 장부 모으기(718-2)
|
||||
# 단위 · 금액 표준 표 읽기(`GET /api/m02/df/standards` · 단위 고르기 `M02_DF_UnitPick`)
|
||||
# 단위 · 금액 표준 표 읽기(`GET /api/m02/df/standards` · 단위 고르기 `M02_DF_UnitPick`) — 읽기만 · 로그인만(M02 줄 쌓기 단위)
|
||||
app.include_router(
|
||||
__import__("M02_MasterTemplete.DataFlow.M02_DF_Router", fromlist=["router"]).router,
|
||||
dependencies=system_admin_only,
|
||||
dependencies=protected,
|
||||
)
|
||||
# 마스터 템플릿 — 시스템 층은 시스템 관리자만 · 층(회사·개인·프로젝트)·도면 길은 로그인만(권한은 길 안에서).
|
||||
app.include_router(m02_master_template_router, dependencies=system_admin_only)
|
||||
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user