Merge remote-tracking branch 'origin/sub_laptop_7' into main_laptop_1
This commit is contained in:
@@ -7,8 +7,9 @@
|
||||
* · M02 단가산출 · 일위대가 = 위 층(마스터 › 회사 › 개인) 서고 목록(`/layers/{층}/templates`) · 고름 = 참조 {층 · 주인 · 키}
|
||||
* · 인력 · 재료 고름 = `element` · 일위대가 고정 줄 꼴 = `fixedOf`(705-1 이음 노임 LB · 자재 MT · 수량 1 은 부르는 쪽) ·
|
||||
* `element` 없으면 그 줄 회색 막힘(까닭 = 일위대가 표 개편 뒤 · 723)
|
||||
* · 부르는 곳 = M02 단가산출 [기본 로직 선택](`logicOnly`) · [항목 추가](`Sangeun_Pick`) · 일위대가 [로직 추가]
|
||||
* · 부르는 곳 = M02 단가산출 [항목 추가](`Sangeun_Pick`) · 일위대가 [로직 추가]
|
||||
* (`Labor_Pick` 이 `lib.sangeun` 이면 이 창) — B09 · 불러오기는 옛 창(`Labor_Pick`)
|
||||
* · (727-1) 기초 = 로직마다 한 줄(`logicHeads` · 옵션은 넣은 뒤 고름 · 옛 창도 같이) · 서버 목록 그대로
|
||||
* ========================================================================== */
|
||||
|
||||
import { createDataTable } from "@ui/ui_template_data_table";
|
||||
@@ -27,6 +28,7 @@ import { openStandardSheet } from "./M02_MasterTemplete_Labor_Sheet";
|
||||
import { tc } from "./M02_MasterTemplete_Labor_Text";
|
||||
import { ownOwner } from "./M02_MasterTemplete_Layer";
|
||||
import { listLayerDocs, SANGEUN_KIND, type SangeunRow } from "./M02_MasterTemplete_Sangeun_Pick";
|
||||
import { logicHeads as heads } from "./logic/M02_Find_Heads";
|
||||
import "../M01_MasterData/M01_MasterData_UI_Logic_Style.css"; // 창 겉틀
|
||||
import "./M02_MasterTemplete_Labor.css";
|
||||
|
||||
@@ -53,10 +55,12 @@ export interface FindPickOptions {
|
||||
ref?: (ref: LaborRef, name: string) => void;
|
||||
/** M01 인력 · 재료 고름 — 없으면 그 줄 회색 막힘 */
|
||||
element?: (e: M01Element) => void;
|
||||
/** 로직 줄만(탭 · 인력 · 재료 없음 — [기본 로직 선택]) */
|
||||
logicOnly?: boolean;
|
||||
}
|
||||
|
||||
/** 727-1 기초마다 한 줄(옛 창 `Labor_Pick` 도 씀) — 규격 칸 = 「옵션 N」 */
|
||||
export const logicHeads = (rows: StandardRow[]): StandardRow[] =>
|
||||
heads(rows, (n) => L("M02_FindOptions").replace("{n}", String(n)));
|
||||
|
||||
/** 일위대가 고정 줄(705-1 이음 꼴) — 인력 = 노임 LB · 재료 = 자재 MT · 단가 = 지금 값(셈은 단가 책 값) */
|
||||
export function fixedOf(e: M01Element): LaborFixed {
|
||||
const labor = e.종류 === "인력";
|
||||
@@ -202,9 +206,7 @@ export function openFindPicker(opt: FindPickOptions): void {
|
||||
function draw(): void {
|
||||
const q = search.input.value.trim();
|
||||
if (tab === "m01") {
|
||||
shown = logicLines(q);
|
||||
if (!opt.logicOnly)
|
||||
shown.push(...elementLines("인력", people), ...elementLines("재료", goods));
|
||||
shown = [...logicLines(q), ...elementLines("인력", people), ...elementLines("재료", goods)];
|
||||
} else {
|
||||
const words = q.toLowerCase().split(/\s+/).filter(Boolean);
|
||||
const kind = tab === "sk" ? L("M02_PickSangeun") : L("M02_PrecomboTitle");
|
||||
@@ -220,7 +222,6 @@ export function openFindPicker(opt: FindPickOptions): void {
|
||||
|
||||
/** 인력 · 재료 = 서버 찾기(친 낱말) — 재료는 낱말 있을 때만 · 늦게 온 답은 버림 */
|
||||
async function findElements(): Promise<void> {
|
||||
if (opt.logicOnly) return;
|
||||
const mine = ++seq;
|
||||
const q = search.input.value.trim();
|
||||
const get = (kind: "job" | "price"): Promise<PickItem[]> =>
|
||||
@@ -244,21 +245,19 @@ export function openFindPicker(opt: FindPickOptions): void {
|
||||
timer = window.setTimeout(() => void findElements(), 250);
|
||||
});
|
||||
|
||||
const tabs = opt.logicOnly
|
||||
? null
|
||||
: createTabs({
|
||||
tabs: [
|
||||
{ key: "m01", label: L("M02_FindM01") },
|
||||
{ key: "sk", label: L("M02_PickSangeun") },
|
||||
{ key: "pre", label: L("M02_PrecomboTitle") },
|
||||
],
|
||||
value: tab,
|
||||
kind: "line",
|
||||
onChange: (key) => {
|
||||
tab = key as Tab;
|
||||
draw();
|
||||
},
|
||||
});
|
||||
const tabs = createTabs({
|
||||
tabs: [
|
||||
{ key: "m01", label: L("M02_FindM01") },
|
||||
{ key: "sk", label: L("M02_PickSangeun") },
|
||||
{ key: "pre", label: L("M02_PrecomboTitle") },
|
||||
],
|
||||
value: tab,
|
||||
kind: "line",
|
||||
onChange: (key) => {
|
||||
tab = key as Tab;
|
||||
draw();
|
||||
},
|
||||
});
|
||||
|
||||
({ close } = openModal({
|
||||
title: opt.title ?? L("M02_FindTitle"),
|
||||
@@ -271,7 +270,7 @@ export function openFindPicker(opt: FindPickOptions): void {
|
||||
el("div", {
|
||||
className: "m02-std-pick",
|
||||
attrs: { "data-find-pick": "" },
|
||||
children: [...(tabs ? [tabs.root] : []), search.root, hint, table.root],
|
||||
children: [tabs.root, search.root, hint, table.root],
|
||||
}),
|
||||
),
|
||||
}));
|
||||
@@ -283,29 +282,28 @@ export function openFindPicker(opt: FindPickOptions): void {
|
||||
void Promise.all([fetchLibLogics(), fetchStandard().catch(() => null)])
|
||||
.then(([l, s]) => {
|
||||
logics = l;
|
||||
std = s?.줄.length ? s.줄 : null; // 빈 줄 = 서버가 아직 셈 — 로직 줄 그대로
|
||||
std = s?.줄.length ? logicHeads(s.줄) : null; // 빈 줄 = 서버가 아직 셈 — 로직 줄 그대로
|
||||
draw();
|
||||
})
|
||||
.catch(fail);
|
||||
void findElements();
|
||||
if (tabs)
|
||||
void upper(opt.layer, opt.owner)
|
||||
.then((scopes) =>
|
||||
Promise.all(
|
||||
(["sk", "pre"] as const).map((t) =>
|
||||
listLayerDocs(scopes, t === "sk" ? SANGEUN_KIND : "precombo").then((rows) => {
|
||||
lists[t] = rows;
|
||||
tabs.setBadge(t, rows.length);
|
||||
}),
|
||||
),
|
||||
void upper(opt.layer, opt.owner)
|
||||
.then((scopes) =>
|
||||
Promise.all(
|
||||
(["sk", "pre"] as const).map((t) =>
|
||||
listLayerDocs(scopes, t === "sk" ? SANGEUN_KIND : "precombo").then((rows) => {
|
||||
lists[t] = rows;
|
||||
tabs.setBadge(t, rows.length);
|
||||
}),
|
||||
),
|
||||
)
|
||||
.catch((error) => {
|
||||
lists.sk ??= [];
|
||||
lists.pre ??= [];
|
||||
fail(error);
|
||||
})
|
||||
.finally(draw);
|
||||
),
|
||||
)
|
||||
.catch((error) => {
|
||||
lists.sk ??= [];
|
||||
lists.pre ??= [];
|
||||
fail(error);
|
||||
})
|
||||
.finally(draw);
|
||||
}
|
||||
|
||||
/** 기준 목록이 오기 전 · 못 받으면 — 로직 하나 = 줄 하나(금액 빈칸) */
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
* 704-1 단가산출 줄 = 기준 목록 표(품명 · 규격 · 단위 · 금액 · 출처 · 대표 규격 펼침 · 공종 거름 ·
|
||||
* 실무 낱말 = 서버가 별칭에 덧붙임) · 금액 = 서버 M01 엔진(`/logic-lib/standard`) · 오기 전엔 로직 줄만 ·
|
||||
* 줄 누름 = 계산서 보기(704-2 · `Labor_Sheet`) → [담기] = 지금 담기 동작(`done(로직)`)
|
||||
* 727-1 기초 = 로직마다 한 줄(`logicHeads` — 옵션 줄을 고르면 옵션을 버리던 흠 걷음 · 옵션은 담은 뒤 고름)
|
||||
* 721-2 `lib.sangeun`(M02 일위대가 [로직 추가] · `withHopyo`) = 찾아 넣기 창(`Find_Pick` · 탭 M01 전체 · 단가산출 ·
|
||||
* 일위대가 · 실무 호표 줄 없음) · M01 인력 · 재료 = `lib.fixed`(일위대가 고정 줄 · 없으면 회색 막힘) ·
|
||||
* B09 · 불러오기는 이 창 그대로
|
||||
@@ -38,7 +39,7 @@ import "../M01_MasterData/M01_MasterData_UI_Logic_Style.css"; // 창 겉틀
|
||||
import "./M02_MasterTemplete_Labor.css";
|
||||
import { openModal } from "@ui/ui_template_modal";
|
||||
import { createText } from "@ui/ui_template_text";
|
||||
import { fixedOf, openFindPicker, upper } from "./M02_MasterTemplete_Find_Pick";
|
||||
import { fixedOf, logicHeads, openFindPicker, upper } from "./M02_MasterTemplete_Find_Pick";
|
||||
|
||||
export { withHopyo } from "./M02_MasterTemplete_Hopyo_Pick";
|
||||
|
||||
@@ -356,7 +357,7 @@ export function openLogicPicker(
|
||||
drawSubs();
|
||||
find();
|
||||
return fetchStandard().then((got) => {
|
||||
std = got.줄.length ? got.줄 : null; // 빈 줄 = 서버가 아직 셈 — 로직 줄 그대로
|
||||
std = got.줄.length ? logicHeads(got.줄) : null; // 빈 줄 = 서버가 아직 셈 · 727-1 기초마다 한 줄
|
||||
work.setOptions(opts(got.공종), "");
|
||||
find();
|
||||
});
|
||||
|
||||
@@ -51,3 +51,13 @@
|
||||
text-align: end;
|
||||
font-weight: var(--font-weight-bold);
|
||||
}
|
||||
|
||||
/* 727-2 기초 출처 꼬리표 — 칸 폭 고정 · 넘치면 말줄임(풍선 = 온 글) · 이름 칸 폭을 안 바꿈 */
|
||||
.m02-sk .m01lab__src {
|
||||
display: inline-block;
|
||||
max-width: 6em;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
@@ -5,8 +5,10 @@
|
||||
* HoEdit → SK 덮개 · 더한줄 · 줄 빼기 · 별도계상) · 인력·재료·경비(공용 계산서 · SK 셈 답) ·
|
||||
* 시험 계산(`Logic_Calc` · 바탕 M01 로직 + 덮개 품 · 단가 사본) · 텍스트 수식(같은 사본 식) · 기본정보 = 바탕 로직(맨 위 접힘)
|
||||
* 고침 = SK 덮개 · 더한줄(`Sangeun_Pick`) · 뺀줄(꼴 = 서버 `M02_Sangeun_Doc`) · 값 = 서버 셈 길 최신 응답만
|
||||
* 시험 입력 = SK 셈 `입력값`(저장 안 함) · [저장] = 화면 문서 → 서버가 다시 셈(원값 · 바탕판)
|
||||
* 바탕 없는 SK = [기본 로직 선택] → 바탕 · 단위 · 바탕 없이 줄만도 됨
|
||||
* 옵션 칸 = SK `입력값`(727-3 · 저장 · 열린입력만 시험 값) · [저장] = 화면 문서 → 서버가 다시 셈(원값 · 바탕판)
|
||||
* 둘째 기초(727-2) = 서버 `펼침` 세부 줄 · 인력 · 재료 · 경비 상자에 함께 + 꼬리표(기초 둘 이상) · 세부 덮개 ·
|
||||
* 뺀줄 · [기초 빼기] · M01 개정 알림 · 옵션 칸 = 바탕 입력 + 더한줄 기초 입력
|
||||
* [항목 추가] 기초 = 바탕 비었으면 바탕(727-2 · 단위가 비거나 「식」이면 기초 결과단위) · 있으면 더한줄 · 바탕 없이 줄만도 됨
|
||||
* ========================================================================== */
|
||||
|
||||
import { createBadge } from "@ui/ui_template_badge";
|
||||
@@ -33,15 +35,17 @@ import { buildLabDetail } from "../M01_MasterData/M01_MasterData_UI_LogicLab_Det
|
||||
import { buildHo, type HoEdit } from "../M01_MasterData/M01_MasterData_UI_LogicLab_Ho";
|
||||
import { createViewPicker } from "../M01_MasterData/M01_MasterData_UI_LogicLab_View";
|
||||
import type { Layer } from "./M02_MasterTemplete_Api_Fetch";
|
||||
import { openFindPicker } from "./M02_MasterTemplete_Find_Pick";
|
||||
import { calcSangeun, removedStrip } from "./M02_MasterTemplete_Sangeun_Cover";
|
||||
import { openSangeunLinePicker } from "./M02_MasterTemplete_Sangeun_Pick";
|
||||
import {
|
||||
baseCovers,
|
||||
dropBase,
|
||||
dropLine,
|
||||
dropText,
|
||||
hoView,
|
||||
removedSubs,
|
||||
restoreLine,
|
||||
revisedAdds,
|
||||
rowsOf,
|
||||
setApart,
|
||||
setLineCover,
|
||||
@@ -83,7 +87,7 @@ export function mountSangeunDetail(
|
||||
let ans: SkAnswer | null = null;
|
||||
let bare: SkAnswer | null = null;
|
||||
let fail = "";
|
||||
let extra: LogicInput[] = []; // 바탕 없는 SK — 더한줄 로직 입력 모음 = 공통 옵션 칸(725-6)
|
||||
let extra: LogicInput[] = []; // 더한줄 로직 입력 중 바탕에 없는 것 = 옵션 칸(725-6 · 727-3)
|
||||
const inputsOf = new Map<string, LogicInput[]>();
|
||||
let seq = 0;
|
||||
let alive = true;
|
||||
@@ -100,6 +104,7 @@ export function mountSangeunDetail(
|
||||
size: 28,
|
||||
onClick: () =>
|
||||
openSangeunLinePicker(scope, (line) => {
|
||||
if ("종류" in line && line.종류 === "로직" && !doc.바탕) return setBase(line.로직);
|
||||
doc.더한줄 = [
|
||||
...(doc.더한줄 ?? []),
|
||||
line as unknown as NonNullable<SkDoc["더한줄"]>[number],
|
||||
@@ -108,25 +113,13 @@ export function mountSangeunDetail(
|
||||
}),
|
||||
});
|
||||
addButton.hidden = !edit;
|
||||
const pick = createButton({
|
||||
label: L("M02_SangeunPickBase"),
|
||||
variant: "filled",
|
||||
size: 28,
|
||||
onClick: () =>
|
||||
openFindPicker({
|
||||
...scope,
|
||||
logicOnly: true,
|
||||
title: L("M02_SangeunPickBase"),
|
||||
logic: (x) => {
|
||||
doc.바탕 = { 로직: x.키 };
|
||||
delete doc.바탕판; // 새 바탕 — 개정 견줄 판 없음
|
||||
if (!doc.단위 || doc.단위 === "식") doc.단위 = unitOf(x.결과단위) || doc.단위;
|
||||
touch(false);
|
||||
void loadBase();
|
||||
},
|
||||
}),
|
||||
});
|
||||
pick.hidden = true; // 보임은 drawHead 가 정함
|
||||
/** 첫 기초 = 바탕(옛 [기본 로직 선택] 동작 · 단위는 `loadBase` 가 기초 결과단위로) */
|
||||
function setBase(key: string): void {
|
||||
doc.바탕 = { 로직: key };
|
||||
delete doc.바탕판; // 새 바탕 — 개정 견줄 판 없음
|
||||
touch(false);
|
||||
void loadBase(true);
|
||||
}
|
||||
const baseHint = createText(L("M02_SangeunBaseViews"));
|
||||
host.replaceChildren(
|
||||
el("div", {
|
||||
@@ -135,16 +128,17 @@ export function mountSangeunDetail(
|
||||
children: [
|
||||
head,
|
||||
notes,
|
||||
el("div", { className: "m02-sk__head", children: [views.root, pick, addButton, baseHint] }),
|
||||
el("div", { className: "m02-sk__head", children: [views.root, addButton, baseHint] }),
|
||||
editor,
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
const opened = (): boolean => (doc.열린입력 ?? []).length > 0;
|
||||
// 바탕 없는 SK 의 지문 = 이름 · 단위 따라 바뀜 — 바탕이 있을 때만 M01 개정으로 봄(725-3)
|
||||
// 바탕 없는 SK 의 지문 = 이름 · 단위 따라 바뀜 — 바탕이 있을 때만 M01 개정으로 봄(725-3) · 둘째 기초 지문(727-2)
|
||||
const revisedBase = (): boolean =>
|
||||
!!(doc.바탕 && doc.바탕판 && ans?.바탕판 && doc.바탕판 !== ans.바탕판);
|
||||
!!(doc.바탕 && doc.바탕판 && ans?.바탕판 && doc.바탕판 !== ans.바탕판) ||
|
||||
revisedAdds(ans, doc).length > 0;
|
||||
const baseKey = (): string | null => doc.바탕?.로직 ?? null;
|
||||
|
||||
/** 바탕 없는 · 참조 바탕 SK 의 기본정보 자리 줄 */
|
||||
@@ -160,12 +154,10 @@ export function mountSangeunDetail(
|
||||
덧줄: [],
|
||||
});
|
||||
|
||||
/** 바탕 없을 때만 — 더한줄 로직들의 입력을 모음(같은 이름은 하나) · 바탕 있으면 바탕 입력 그대로 */
|
||||
/** 더한줄 로직들의 입력을 모음(같은 이름은 하나 · 바탕 입력에 있는 이름 뺌) */
|
||||
async function syncExtra(): Promise<void> {
|
||||
const keys = [
|
||||
...new Set(
|
||||
doc.바탕 ? [] : (doc.더한줄 ?? []).flatMap((x) => ("로직" in x ? [String(x.로직)] : [])),
|
||||
),
|
||||
...new Set((doc.더한줄 ?? []).flatMap((x) => ("로직" in x ? [String(x.로직)] : []))),
|
||||
];
|
||||
await Promise.all(
|
||||
keys
|
||||
@@ -177,7 +169,7 @@ export function mountSangeunDetail(
|
||||
),
|
||||
),
|
||||
);
|
||||
const seen = new Set<string>();
|
||||
const seen = new Set<string>((base?.logic.입력 ?? []).map((i) => i.이름));
|
||||
extra = keys
|
||||
.flatMap((k) => inputsOf.get(k) ?? [])
|
||||
.filter((i) => !seen.has(i.이름) && seen.add(i.이름));
|
||||
@@ -223,7 +215,6 @@ export function mountSangeunDetail(
|
||||
const baseName = base ? `${base.logic.키} ${base.logic.이름}` : null;
|
||||
const baseText = baseName ?? doc.바탕?.참조?.키 ?? L("M02_SangeunNoBase");
|
||||
const revised = revisedBase();
|
||||
pick.hidden = !edit || !!doc.바탕 || !!doc.뺀줄?.length || !!Object.keys(doc.덮개 ?? {}).length;
|
||||
head.replaceChildren(
|
||||
field(L("M02_Name"), "이름"),
|
||||
field(L("M02_SangeunUnit"), "단위"),
|
||||
@@ -278,8 +269,11 @@ export function mountSangeunDetail(
|
||||
baseHint.hidden = !base;
|
||||
const rows = ans ? rowsOf(ans, bare, doc) : [];
|
||||
const names = new Map((bare ? rowsOf(bare, null, {}) : []).map((r) => [r.cid, r.이름]));
|
||||
const removed = (doc.뺀줄 ?? []).map((cid) => ({ cid, 이름: names.get(cid) ?? cid }));
|
||||
const row = base?.logic ?? standIn();
|
||||
const removed = [
|
||||
...(doc.뺀줄 ?? []).map((cid) => ({ cid, 이름: names.get(cid) ?? cid })),
|
||||
...removedSubs(ans),
|
||||
];
|
||||
const row = base ? { ...base.logic, 입력: [...(base.logic.입력 ?? []), ...extra] } : standIn();
|
||||
const view = ans ? hoView(ans, bare, base?.logic ?? null, doc) : null;
|
||||
const baseNames = [...(row.호표 ?? []), ...(row.덧줄 ?? [])].map((x) =>
|
||||
String(x.이름 ?? ("요소" in x ? x.요소 : "")),
|
||||
@@ -318,7 +312,7 @@ export function mountSangeunDetail(
|
||||
: [createText(L("M02_SangeunWait"))],
|
||||
});
|
||||
buildLabDetail(editor, {
|
||||
row,
|
||||
row: base?.logic ?? row,
|
||||
file: base?.file ?? "",
|
||||
reasons: base?.reasons ?? [],
|
||||
prices: base?.prices ?? {},
|
||||
@@ -359,6 +353,17 @@ export function mountSangeunDetail(
|
||||
onClick: () => (close(), dropLine(doc, r), touch()),
|
||||
});
|
||||
drop.dataset.skDrop = "";
|
||||
const sub = r.sub;
|
||||
const whole = sub
|
||||
? [
|
||||
createButton({
|
||||
label: L("M02_SangeunDropBase"),
|
||||
variant: "danger-ghost",
|
||||
size: 28,
|
||||
onClick: () => (close(), dropBase(doc, sub.k), touch()),
|
||||
}),
|
||||
]
|
||||
: [];
|
||||
const apart = r.apartPick
|
||||
? [
|
||||
createCheckbox(L("M02_SangeunApart"), {
|
||||
@@ -369,7 +374,7 @@ export function mountSangeunDetail(
|
||||
}).root,
|
||||
]
|
||||
: [];
|
||||
return [el("div", { className: "m02-sk__tags", children: [...apart, drop] })];
|
||||
return [el("div", { className: "m02-sk__tags", children: [...apart, drop, ...whole] })];
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -379,9 +384,17 @@ export function mountSangeunDetail(
|
||||
calcHost.querySelector<HTMLButtonElement>("[data-calc-run]")?.click();
|
||||
}
|
||||
|
||||
/** 텍스트 수식 · 계산서 입력 단추 — 시험 입력 고침 → 시험 계산 다시 그림 · 셈 */
|
||||
/** 옵션 칸 · 텍스트 수식 · 계산서 입력 단추 — 입력 고침 → SK `입력값`(727-3 · 열린입력 = 시험 값만) ·
|
||||
* 시험 계산 다시 그림 · 셈 */
|
||||
function onInput(name: string, v: string): void {
|
||||
values[name] = v;
|
||||
if (edit && !(doc.열린입력 ?? []).includes(name)) {
|
||||
const kept = { ...(doc.입력값 ?? {}) };
|
||||
if (String(v).trim()) kept[name] = v;
|
||||
else delete kept[name];
|
||||
doc.입력값 = kept;
|
||||
touch(false);
|
||||
}
|
||||
drawCalc();
|
||||
calcHost.querySelector<HTMLButtonElement>("[data-calc-run]")?.click();
|
||||
if (!base) void recalc(); // 바탕 없음 — 시험 계산 단추 없음 · SK 셈을 바로
|
||||
@@ -410,7 +423,8 @@ export function mountSangeunDetail(
|
||||
});
|
||||
}
|
||||
|
||||
async function loadBase(): Promise<void> {
|
||||
/** `fresh` = 막 넣은 바탕 — 단위가 비거나 「식」이면 기초 결과단위(앞 수 뗌 · 배는 서버가 나눔) */
|
||||
async function loadBase(fresh = false): Promise<void> {
|
||||
const key = baseKey();
|
||||
base = null;
|
||||
text = null;
|
||||
@@ -422,6 +436,10 @@ export function mountSangeunDetail(
|
||||
}
|
||||
}
|
||||
if (!alive) return;
|
||||
if (fresh && base && (!doc.단위 || doc.단위 === "식")) {
|
||||
doc.단위 = unitOf(base.logic.결과단위) || doc.단위;
|
||||
touch(false);
|
||||
}
|
||||
draw();
|
||||
await recalc(); // 바탕 정체(bare) 먼저 — 시험 계산 · 텍스트 수식 = 덮개 사본
|
||||
if (alive) drawCalc();
|
||||
@@ -431,6 +449,12 @@ export function mountSangeunDetail(
|
||||
return {
|
||||
getDoc: () => {
|
||||
if (ans?.바탕판 && doc.바탕) doc.바탕판 = ans.바탕판; // 저장 = 지금 바탕 확인(서버가 다시 적음)
|
||||
for (const l of ans?.줄 ?? []) {
|
||||
const one = l.바탕판
|
||||
? doc.더한줄?.[Number(/^add:#(\d+)$/.exec(l.cid)?.[1] ?? -1)]
|
||||
: undefined;
|
||||
if (one) one.바탕판 = l.바탕판; // 둘째 기초도 같음(727-2)
|
||||
}
|
||||
saved = JSON.stringify(doc);
|
||||
return clone(doc);
|
||||
},
|
||||
|
||||
@@ -7,6 +7,10 @@
|
||||
켜쌓기율 · 고름 · % 덧줄을 엔진이 다시 셈. 1 단위 = 기준 ÷ 배(바탕 로직 결과단위 앞 수).
|
||||
계산 자리(CLAUDE.md 5장 ③) = 서버 단독 · 화면은 셈 길(저장 안 함 · 최신 응답만).
|
||||
저장(`clean_doc`) = 꼴 검사 + 참조 위 층 · 있는 문서 · 순환 422 + 서버가 `원값` · `바탕판` 적음.
|
||||
둘째 기초(727-2 · 더한줄 로직) = 세부 덮개 · 뺀줄 있으면 고친 기초 줄을 엔진으로 셈한 `값묶음` 줄
|
||||
(B09 굳힘도 그 값) · 화면 셈 길(`calc_doc`) = 그 줄 덧칸 `펼침`(세부 줄 × 수량) · `뺀세부` ·
|
||||
`바탕판` ·
|
||||
`꼬리표`(원문번호) · 답 `꼬리표` = 바탕 원문번호.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -98,9 +102,14 @@ def resolve(
|
||||
hit = re.match(r"^add:#(\d+)$", cid)
|
||||
if hit and int(hit[1]) in lack:
|
||||
items[at] = _gap(items[at], lack[int(hit[1])])
|
||||
elif hit and sd.sub_patched(doc["더한줄"][int(hit[1])]): # 고친 둘째 기초 = 셈한 값묶음
|
||||
sub = sub_run(doc["더한줄"][int(hit[1])], whole)
|
||||
got["짝잃음"] += [{**x, "cid": f"{cid}/{x['cid']}"} for x in sub["짝잃음"]]
|
||||
items[at] = _bundled(items[at], sub)
|
||||
rate = doc.get("켜쌓기율", base["켜쌓기율"])
|
||||
return {
|
||||
**got,
|
||||
"채운더한줄": doc["더한줄"],
|
||||
"짝잃음": base["짝잃음"] + got["짝잃음"],
|
||||
"입력값": {**base["입력값"], **(doc.get("입력값") or {})},
|
||||
"켜쌓기율": rate,
|
||||
@@ -160,6 +169,78 @@ def _gap(item: dict, why: str) -> dict:
|
||||
return {**head, "종류": "로직", "요소": None, "수량": item["수량"], "값묶음": zero, "빠짐": why}
|
||||
|
||||
|
||||
# ── 둘째 기초 세부(727-2) ─────────────────────────────────────────────────
|
||||
def sub_run(one: dict, whole: Any, patched: bool = True) -> dict[str, Any]:
|
||||
"""더한줄 로직 하나 = 그 기초 줄(+ 세부 덮개 · 뺀줄)을 엔진으로 — `run_line` 답 + 정체 ·
|
||||
짝잃음 ·
|
||||
바탕판(덮기 전 기초 지문) · 입력 = 채운 줄 값(`_fill`). `patched` False = 덮기 전."""
|
||||
code = str(one["로직"])
|
||||
try:
|
||||
row = whole.logic(code)
|
||||
except Exception as e: # noqa: BLE001 — 엔진 FormulaError
|
||||
return {"기준": None, "까닭": str(e), "정체": [], "짝잃음": [], "바탕판": ""}
|
||||
got = sd.patched(row, one if patched else {})
|
||||
over = _Over(whole, code, {**got["줄"], "키": code})
|
||||
out = labor.run_line(over, code, one.get("입력값"), None, got["고름"] or None)
|
||||
return {**out, "정체": got["정체"], "짝잃음": got["짝잃음"], "바탕판": fingerprint(row)}
|
||||
|
||||
|
||||
def _bundled(item: dict, sub: dict) -> dict:
|
||||
"""고친 둘째 기초 줄 → 셈한 1 기준단위 노 · 재 · 경 `값묶음` 줄(수량 그대로) ·
|
||||
못 셈 = `_gap`."""
|
||||
if not sub.get("기준"):
|
||||
return _gap(item, str(sub.get("까닭") or "돈 아닌 로직"))
|
||||
head = {k: item.get(k) or "" for k in ("이름", "단위")}
|
||||
bundle = {k: Decimal(str(sub["기준"][k])) for k in COSTS}
|
||||
return {**head, "종류": "로직", "요소": None, "수량": item["수량"], "값묶음": bundle}
|
||||
|
||||
|
||||
def tag_of(whole: Any, code: str) -> str:
|
||||
"""꼬리표 = 기초 원문번호(「9-12-1」) · 없으면 키."""
|
||||
try:
|
||||
return str(whole.logic(code).get("원문번호") or code)
|
||||
except Exception: # noqa: BLE001
|
||||
return code
|
||||
|
||||
|
||||
def _detail(one: dict, at: int, whole: Any) -> dict[str, Any]:
|
||||
"""둘째 기초 세부 줄(화면) — 수량 · 금액 · 비목 = 세부 × 더한줄 수량 · cid `add:#k/세부 cid` ·
|
||||
`바탕값` = 덮기 전 1 기준단위 품 · 단가 · `뺀세부` = 뺀 줄 이름."""
|
||||
sub = sub_run(one, whole)
|
||||
plain = sub_run(one, whole, False) if sd.sub_patched(one) else sub
|
||||
was = dict(zip(plain["정체"], plain.get("줄") or []))
|
||||
covers, times = one.get("덮개") or {}, Decimal(str(one.get("수량") or 0))
|
||||
rows = []
|
||||
for cid, line in zip(sub["정체"], sub.get("줄") or []):
|
||||
mine = covers.get(cid) or {}
|
||||
scaled = {
|
||||
k: None if line.get(k) is None else Decimal(str(line[k])) * times
|
||||
for k in ("수량", "금액")
|
||||
}
|
||||
parts = {
|
||||
k: None if v is None else Decimal(str(v)) * times
|
||||
for k, v in (line.get("비목") or {}).items()
|
||||
}
|
||||
rows.append(
|
||||
{
|
||||
**line,
|
||||
**scaled,
|
||||
"비목": parts,
|
||||
"cid": f"add:#{at}/{cid}",
|
||||
"바탕값": _was(was[cid]) if cid in was else None,
|
||||
**({"덮개": [k for k in sd.COVER_KEYS if k in mine]} if mine else {}),
|
||||
**({"원값": mine["원값"]} if "원값" in mine else {}),
|
||||
}
|
||||
)
|
||||
gone = [
|
||||
{"cid": f"add:#{at}/{c}", "이름": was[c].get("이름") or ""}
|
||||
for c in one.get("뺀줄") or []
|
||||
if c in was
|
||||
]
|
||||
tag = tag_of(whole, str(one["로직"]))
|
||||
return {"펼침": rows, "뺀세부": gone, "바탕판": sub["바탕판"], "꼬리표": tag}
|
||||
|
||||
|
||||
def _bundles(row: dict, whole: Any, expand: Any, trail: tuple) -> None:
|
||||
"""더한줄 참조 · 고정 줄 `값묶음` = 1 단위 노 · 재 · 경 — 못 셈 줄 = `_gap`(나머지 셈 계속)."""
|
||||
for at, item in enumerate(row["호표"]):
|
||||
@@ -182,9 +263,11 @@ def run(
|
||||
read: Any = refs.read_ref,
|
||||
expand: Any = None,
|
||||
trail: tuple = (),
|
||||
detail: bool = False,
|
||||
) -> dict[str, Any]:
|
||||
"""SK 셈(저장 안 함) — `run_line` 답 + `{1단위, 배, 정체, 짝잃음, 바탕판}` · 줄마다 `cid` ·
|
||||
고친 칸 `덮개`(칸 이름) · `원값`. 틀린 문서 · 순환 = `sd.DocError`."""
|
||||
고친 칸 `덮개`(칸 이름) · `원값`. 틀린 문서 · 순환 = `sd.DocError` ·
|
||||
`detail` = 둘째 기초 세부 펼침(`_detail` · 화면 셈 길만)."""
|
||||
from M02_MasterTemplete.M02_Labor_Expand import Expand
|
||||
|
||||
got = resolve(doc, whole, read, trail, given)
|
||||
@@ -205,6 +288,10 @@ def run(
|
||||
mark = {"덮개": [k for k in sd.COVER_KEYS if k in mine]} if mine else {}
|
||||
if at in gaps: # 못 셈 줄 = 금액 — · 까닭(725)
|
||||
mark |= {"단가": None, "금액": None, "비목": {}, "까닭": gaps[at]["빠짐"]}
|
||||
hit = re.match(r"^add:#(\d+)$", cid)
|
||||
one = got["채운더한줄"][int(hit[1])] if hit else {}
|
||||
if detail and one.get("종류") == "로직" and at not in gaps:
|
||||
mark |= _detail(one, int(hit[1]), whole)
|
||||
lines.append(
|
||||
{**line, "cid": cid, **mark, **({"원값": mine["원값"]} if "원값" in mine else {})}
|
||||
)
|
||||
@@ -215,7 +302,9 @@ def run(
|
||||
{"cid": got["정체"][at], "이름": x.get("이름") or "", "까닭": x["빠짐"]}
|
||||
for at, x in gaps.items()
|
||||
]
|
||||
return {**out, **head, "1단위": unit, "빠진줄": gone}
|
||||
base = doc.get("바탕") or {}
|
||||
tag = tag_of(whole, base["로직"]) if "로직" in base else (base.get("참조") or {}).get("키")
|
||||
return {**out, **head, "1단위": unit, "빠진줄": gone, "꼬리표": tag}
|
||||
|
||||
|
||||
def unit(doc: dict, whole: Any, expand: Any, trail: tuple) -> dict[str, Any]:
|
||||
@@ -285,11 +374,28 @@ def clean_doc(doc: Any, here_: refs.Here) -> dict[str, Any]:
|
||||
if cid in was:
|
||||
one["원값"] = _was(was[cid])
|
||||
out["바탕판"] = fingerprint(base["줄"])
|
||||
_stamp_adds(out, base, whole)
|
||||
except sd.DocError as e:
|
||||
raise _fail(e) from e
|
||||
return out
|
||||
|
||||
|
||||
def _stamp_adds(out: dict, base: dict, whole: Any) -> None:
|
||||
"""둘째 기초 저장 칸(727-2) — `바탕판` = 그 기초 지문 · 세부 덮개 `원값` = 덮기 전
|
||||
1 기준단위 값."""
|
||||
filled, _ = _fill(out, whole, {**base["입력값"], **out["입력값"]})
|
||||
for one, mine in zip(filled["더한줄"], out["더한줄"]):
|
||||
if one.get("종류") != "로직":
|
||||
continue
|
||||
plain = sub_run(one, whole, False)
|
||||
if plain["바탕판"]:
|
||||
mine["바탕판"] = plain["바탕판"]
|
||||
was = dict(zip(plain["정체"], plain.get("줄") or []))
|
||||
for cid, cover in (mine.get("덮개") or {}).items():
|
||||
if cid in was:
|
||||
cover["원값"] = _was(was[cid])
|
||||
|
||||
|
||||
def _was(line: dict) -> dict[str, Any]:
|
||||
keep = {"qty": line.get("율", line.get("수량")), "단가": line.get("단가")}
|
||||
return {k: sd.plain(Decimal(str(v))) for k, v in keep.items() if v is not None}
|
||||
@@ -304,7 +410,7 @@ def calc_doc(doc: Any, given: dict | None = None) -> dict[str, Any]:
|
||||
clean = sd.clean(doc, None, whole)
|
||||
_check_refs(clean, refs.here()) # 저장 전 문서도 위 층만 · 있는 문서 · 순환 422
|
||||
me = refs.ref_id({"층": refs.here()[0], "주인": refs.here()[1], "키": clean["키"]})
|
||||
got = run(clean, whole, given, trail=(me,))
|
||||
got = run(clean, whole, given, trail=(me,), detail=True)
|
||||
except sd.DocError as e:
|
||||
raise _fail(e) from e
|
||||
return labor._json({**got, "머리": head_of(clean)})
|
||||
|
||||
@@ -8,8 +8,13 @@
|
||||
덮개 {cid: {qty | 배율, labor, material, expense, 원값}},
|
||||
더한줄 [{종류: 인력|재료|기계, 요소, 이름, 단위, 수량, 비목?, 단가?,
|
||||
별도계상?(재료 · 없음 = 계 안)}
|
||||
| {종류: 로직, 로직, 입력값, 수량} | {참조 | 고정, 이름?, 단위?, 수량}],
|
||||
| {종류: 로직, 로직, 입력값, 수량, 덮개?, 뺀줄?, 바탕판?}
|
||||
| {참조 | 고정, 이름?, 단위?, 수량}],
|
||||
뺀줄 [cid], 바탕판}
|
||||
둘째 기초(727-2) = 더한줄 로직 — 수량 = 그 기초 기준단위 몇 개 · `덮개` {세부 cid: 덮개} ·
|
||||
`뺀줄` [세부 cid](세부 cid = 그 기초 줄 `kind:키#n` · 화면 열쇠 `add:#k/세부 cid`) ·
|
||||
`바탕판` = 저장 때 서버가 그 기초 지문.
|
||||
SK `입력값` = 바탕 입력 + 더한줄 로직 입력(727-3 · 넣은 뒤 고른 옵션).
|
||||
줄 정체 cid = `kind:키#n`(B09 702 덮개 꼴) — kind = labor · material · machine · logic · pct(덧줄) ·
|
||||
키 = 요소 키(로직 줄 = 부르는 GC 키 · 조건 재료 = 이름 · 덧줄 = 이름 · 더한줄 참조 = 그 키) ·
|
||||
n = 같은 머리 차례 · 바탕 줄(M01 로직 줄 · 바탕 SK 의 고친 줄)에서 셈 — 뺀줄 · 더한줄 뒤 같은 줄.
|
||||
@@ -171,6 +176,7 @@ def _added(raw: Any, where: str, whole: Any) -> dict:
|
||||
if row is not None:
|
||||
head["입력값"] = {k: v for k, v in given.items() if k in names}
|
||||
name = name or str(row.get("이름") or code)
|
||||
head |= _sub_patch(raw, where)
|
||||
elif raw.get("종류") in ADD_PART:
|
||||
key = str(raw.get("요소") or "")
|
||||
got = _element(whole, key, where)
|
||||
@@ -189,6 +195,41 @@ def _added(raw: Any, where: str, whole: Any) -> dict:
|
||||
return {**head, "이름": name, "단위": unit, "수량": qty}
|
||||
|
||||
|
||||
def _sub_patch(raw: dict, where: str) -> dict:
|
||||
"""둘째 기초 세부 고침(727-2) — `덮개` {세부 cid: 덮개} · `뺀줄` [세부 cid] · 빈 칸은 안 둠 ·
|
||||
`원값` · `바탕판` = 서버 몫(버림)."""
|
||||
covers = {
|
||||
_cid(c, f"{where} 덮개"): _cover(one, f"{where} 덮개 「{c}」", False)
|
||||
for c, one in (raw.get("덮개") or {}).items()
|
||||
}
|
||||
drop = [_cid(x, f"{where} 뺀줄") for x in raw.get("뺀줄") or []]
|
||||
if len(set(drop)) != len(drop):
|
||||
raise DocError(f"{where} · 뺀줄에 같은 줄 두 번")
|
||||
return {**({"덮개": covers} if covers else {}), **({"뺀줄": drop} if drop else {})}
|
||||
|
||||
|
||||
def sub_patched(one: dict) -> bool:
|
||||
"""고친 둘째 기초 — 더한줄 로직에 세부 덮개 · 뺀줄."""
|
||||
return one.get("종류") == "로직" and bool(one.get("덮개") or one.get("뺀줄"))
|
||||
|
||||
|
||||
def _added_names(whole: Any, adds: Any) -> set[str]:
|
||||
"""더한줄 로직 입력 이름(727-3 · SK 입력값에 남길 옵션) — 못 읽는 로직은 뺌
|
||||
(검사가 따로 막음)."""
|
||||
out: set[str] = set()
|
||||
if whole is None:
|
||||
return out
|
||||
for one in adds or []:
|
||||
if isinstance(one, dict) and one.get("종류") == "로직":
|
||||
try:
|
||||
out |= {
|
||||
str(s.get("이름")) for s in whole.logic(str(one.get("로직"))).get("입력") or []
|
||||
}
|
||||
except Exception: # noqa: BLE001
|
||||
continue
|
||||
return out
|
||||
|
||||
|
||||
def _logic(whole: Any, code: str, where: str) -> dict | None:
|
||||
if whole is None:
|
||||
return None
|
||||
@@ -230,7 +271,8 @@ def clean(doc: Any, base_row: dict | None = None, whole: Any = None) -> dict[str
|
||||
given = _given(doc.get("입력값"), "입력값")
|
||||
opened = [str(x) for x in doc.get("열린입력") or []]
|
||||
if base_row is not None:
|
||||
given = {k: v for k, v in given.items() if k in names}
|
||||
keep = names | _added_names(whole, doc.get("더한줄"))
|
||||
given = {k: v for k, v in given.items() if k in keep}
|
||||
bad = [x for x in opened if x not in names or x in given]
|
||||
if bad:
|
||||
raise DocError(f"열린입력은 바탕 입력 중 값 안 넣은 것만 {bad}")
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
/* =============================================================================
|
||||
* M02_MasterTemplete/logic/M02_Find_Heads.ts
|
||||
* 727-1 기준 목록(`/logic-lib/standard` · 로직 하나를 옵션 축마다 펼친 줄) → 기초마다 한 줄 —
|
||||
* 바탕 줄(`바꿈` 빈 줄)만 · 규격 칸 = 옵션 축 수(`label(n)` · 없으면 빈칸) · 옵션은 넣은 뒤 고름.
|
||||
* 쓰는 곳 = 찾아 넣기 창(`Find_Pick`) · 옛 창(`Labor_Pick`). 화면 · fetch 없음(`CLAUDE.md` 4장).
|
||||
* ========================================================================== */
|
||||
|
||||
export interface HeadRow {
|
||||
키: string;
|
||||
규격: string;
|
||||
바꿈: Record<string, string | number>;
|
||||
}
|
||||
|
||||
export function logicHeads<T extends HeadRow>(rows: T[], label: (n: number) => string): T[] {
|
||||
const axes = new Map<string, Set<string>>();
|
||||
for (const r of rows)
|
||||
for (const k of Object.keys(r.바꿈 ?? {})) {
|
||||
if (!axes.has(r.키)) axes.set(r.키, new Set());
|
||||
axes.get(r.키)!.add(k);
|
||||
}
|
||||
return rows
|
||||
.filter((r) => !Object.keys(r.바꿈 ?? {}).length)
|
||||
.map((r) => {
|
||||
const n = axes.get(r.키)?.size ?? 0;
|
||||
return { ...r, 규격: n ? label(n) : "" };
|
||||
});
|
||||
}
|
||||
@@ -7,6 +7,8 @@
|
||||
* 줄 정체 cid = `kind:키#n`(바탕 줄) · `add:#k`(더한줄 k 째) · 덮개 원값 = 저장 때 서버가 다시 적음
|
||||
* (화면은 고친 첫때 바탕 값을 적어 둠 — 곁 글용) · 열린입력 있는 SK = 품은 배율만
|
||||
* 일위대가 줄 → 이름 없는 SK 문서 = `lineDoc`(서버 `M02_Labor_Patch.as_doc` 꼴)
|
||||
* 둘째 기초(727-2) = 더한줄 로직 줄을 서버 `펼침`(세부 줄)으로 바꿔 놓음 · 열쇠 `add:#k/세부 cid` ·
|
||||
* 고침 = 그 더한줄의 `덮개` · `뺀줄`(품 = 화면 품 ÷ 수량 · 1 기준단위 몫) · 꼬리표 = 기초 둘 이상일 때만
|
||||
* 화면 · fetch · sessionStorage 없음(`CLAUDE.md` 4장).
|
||||
* ========================================================================== */
|
||||
|
||||
@@ -33,6 +35,10 @@ export interface SkAdded {
|
||||
종류?: string;
|
||||
수량: number;
|
||||
단가?: number;
|
||||
/** 둘째 기초 세부 고침(727-2) — 열쇠 = 세부 cid */
|
||||
덮개?: Record<string, SkCover>;
|
||||
뺀줄?: string[];
|
||||
바탕판?: string;
|
||||
[k: string]: unknown;
|
||||
}
|
||||
|
||||
@@ -65,6 +71,13 @@ export interface SkLine {
|
||||
율?: number;
|
||||
/** 못 셈 줄(725) — 금액 없음 · 계에 안 듦 */
|
||||
까닭?: string;
|
||||
/** 둘째 기초(727-2) — 세부 줄 · 뺀 세부 · 그 기초 지문 · 원문번호 */
|
||||
펼침?: SkLine[];
|
||||
뺀세부?: { cid: string; 이름: string }[];
|
||||
바탕판?: string;
|
||||
꼬리표?: string;
|
||||
/** 세부 줄 — 덮기 전 1 기준단위 품 · 단가 */
|
||||
바탕값?: { qty?: number; 단가?: number } | null;
|
||||
}
|
||||
|
||||
export interface SkMat {
|
||||
@@ -87,6 +100,8 @@ export interface SkAnswer {
|
||||
짝잃음: { cid: string; 까닭: string; 바탕?: string }[];
|
||||
바탕판: string;
|
||||
머리?: string;
|
||||
/** 바탕 원문번호(참조 바탕 = 그 키) */
|
||||
꼬리표?: string | null;
|
||||
}
|
||||
|
||||
export interface CoverRow {
|
||||
@@ -115,11 +130,57 @@ export interface CoverRow {
|
||||
priceEdit: boolean;
|
||||
/** 더한 재료 줄 — 별도계상 고름(기본 계 안) */
|
||||
apartPick: boolean;
|
||||
/** 둘째 기초 세부 줄 — 더한줄 차례 · 세부 cid · 수량(배) · 바탕 줄 · 그 밖 = null */
|
||||
sub: { k: number; cid: string; scale: number } | null;
|
||||
}
|
||||
|
||||
/* ── 순수 셈 ── */
|
||||
const BUNDLE_OF: Record<string, string> = { 노무비: "인력", 재료비: "재료", 경비: "경비" };
|
||||
const ADD_RE = /^add:#(\d+)$/;
|
||||
const SUB_RE = /^add:#(\d+)\/(.+)$/;
|
||||
|
||||
/** 세부 열쇠 `add:#k/세부 cid` → {k, cid} */
|
||||
export function subOf(key: string): { k: number; cid: string } | null {
|
||||
const m = SUB_RE.exec(key);
|
||||
return m ? { k: Number(m[1]), cid: m[2] } : null;
|
||||
}
|
||||
|
||||
/** 셈 답 줄 — 둘째 기초 줄은 세부 줄로 바꿔 놓음(정체도 같은 차례) */
|
||||
export function flatLines(ans: SkAnswer): SkLine[] {
|
||||
return ans.줄.flatMap((l) => l.펼침 ?? [l]);
|
||||
}
|
||||
function flatIds(ans: SkAnswer): string[] {
|
||||
const by = new Map(ans.줄.map((l) => [l.cid, l]));
|
||||
return ans.정체.flatMap((c) => by.get(c)?.펼침?.map((x) => x.cid) ?? [c]);
|
||||
}
|
||||
|
||||
/** 기초 수 — 로직 바탕 · 더한줄 로직 */
|
||||
const baseCount = (doc: SkDoc): number =>
|
||||
(doc.바탕?.로직 ? 1 : 0) + (doc.더한줄 ?? []).filter((x) => x.종류 === "로직").length;
|
||||
|
||||
/** 줄 꼬리표(기초 둘 이상일 때만) — 바탕 줄 = 바탕 원문번호 · 세부 줄 = 그 기초 원문번호 · 그 밖 = "" */
|
||||
export function tagsOf(ans: SkAnswer, doc: SkDoc): Map<string, string> {
|
||||
const out = new Map<string, string>();
|
||||
if (baseCount(doc) < 2) return out;
|
||||
for (const l of ans.줄) {
|
||||
if (l.펼침) for (const x of l.펼침) out.set(x.cid, l.꼬리표 ?? "");
|
||||
else if (!ADD_RE.test(l.cid) && ans.꼬리표) out.set(l.cid, ans.꼬리표);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 뺀 세부 줄(되살림 띠) */
|
||||
export const removedSubs = (ans: SkAnswer | null): { cid: string; 이름: string }[] =>
|
||||
(ans?.줄 ?? []).flatMap((l) => l.뺀세부 ?? []);
|
||||
|
||||
/** M01 개정된 둘째 기초 — 저장 때 지문 ≠ 지금 지문 */
|
||||
export function revisedAdds(ans: SkAnswer | null, doc: SkDoc): number[] {
|
||||
return (ans?.줄 ?? []).flatMap((l) => {
|
||||
const hit = ADD_RE.exec(l.cid);
|
||||
const one = hit ? doc.더한줄?.[Number(hit[1])] : undefined;
|
||||
return one?.바탕판 && l.바탕판 && one.바탕판 !== l.바탕판 ? [Number(hit![1])] : [];
|
||||
});
|
||||
}
|
||||
const COVER_KEYS = ["qty", "배율", ...PARTS] as const;
|
||||
|
||||
const partsOf = (l: { 비목?: Record<string, number | null> }): Record<string, number> =>
|
||||
@@ -156,15 +217,17 @@ export function rowsOf(ans: SkAnswer, bare: SkAnswer | null, doc: SkDoc): CoverR
|
||||
금액: number | null;
|
||||
},
|
||||
parts: Record<string, number>,
|
||||
extra: { 묶음?: string | null; 율?: number; apart?: boolean },
|
||||
extra: { 묶음?: string | null; 율?: number; apart?: boolean; 바탕값?: SkLine["바탕값"] },
|
||||
): CoverRow => {
|
||||
const hit = ADD_RE.exec(cid);
|
||||
const added = hit ? Number(hit[1]) : null;
|
||||
const at = subOf(cid);
|
||||
const one = added === null ? undefined : doc.더한줄?.[added];
|
||||
const cover = added === null ? doc.덮개?.[cid] : undefined;
|
||||
const pct = cid.startsWith("pct:");
|
||||
const owner = at ? doc.더한줄?.[at.k] : undefined;
|
||||
const cover = at ? owner?.덮개?.[at.cid] : added === null ? doc.덮개?.[cid] : undefined;
|
||||
const pct = (at?.cid ?? cid).startsWith("pct:");
|
||||
const keys = Object.keys(parts);
|
||||
const base = added === null ? (was.get(cid) ?? null) : null;
|
||||
const base = at ? subWas(extra.바탕값) : added === null ? (was.get(cid) ?? null) : null;
|
||||
const old = cover?.원값;
|
||||
return {
|
||||
cid,
|
||||
@@ -191,9 +254,10 @@ export function rowsOf(ans: SkAnswer, bare: SkAnswer | null, doc: SkDoc): CoverR
|
||||
(old.단가 != null && !near(old.단가, base.단가))),
|
||||
priceEdit: !pct && (added === null || ["인력", "재료", "기계"].includes(String(one?.종류))),
|
||||
apartPick: one?.종류 === "재료",
|
||||
sub: at ? { ...at, scale: Number(owner?.수량 ?? 1) } : null,
|
||||
};
|
||||
};
|
||||
for (const l of ans.줄) rows.push(row(l.cid, l, partsOf(l), l));
|
||||
for (const l of flatLines(ans)) rows.push(row(l.cid, l, partsOf(l), l));
|
||||
const seen = new Set(rows.map((r) => r.cid));
|
||||
for (const m of ans.재료 ?? []) {
|
||||
const cid = ans.정체[m.차례];
|
||||
@@ -203,6 +267,9 @@ export function rowsOf(ans: SkAnswer, bare: SkAnswer | null, doc: SkDoc): CoverR
|
||||
return rows;
|
||||
}
|
||||
|
||||
const subWas = (v: SkLine["바탕값"]): CoverRow["was"] =>
|
||||
v ? { qty: v.qty ?? null, 단가: v.단가 ?? null } : null;
|
||||
|
||||
const memo = (base: CoverRow["was"]): SkCover["원값"] | undefined => {
|
||||
if (!base) return undefined;
|
||||
const out: { qty?: number; 단가?: number } = {};
|
||||
@@ -212,12 +279,16 @@ const memo = (base: CoverRow["was"]): SkCover["원값"] | undefined => {
|
||||
};
|
||||
|
||||
function putCover(doc: SkDoc, r: CoverRow, cover: SkCover): void {
|
||||
const covers = { ...(doc.덮개 ?? {}) };
|
||||
const owner = r.sub ? doc.더한줄?.[r.sub.k] : doc;
|
||||
if (!owner) return;
|
||||
const key = r.sub?.cid ?? r.cid;
|
||||
const covers = { ...(owner.덮개 ?? {}) };
|
||||
if (COVER_KEYS.some((k) => cover[k] != null)) {
|
||||
const old = cover.원값 ?? memo(r.was);
|
||||
covers[r.cid] = { ...cover, ...(old ? { 원값: old } : {}) };
|
||||
} else delete covers[r.cid];
|
||||
doc.덮개 = covers;
|
||||
covers[key] = { ...cover, ...(old ? { 원값: old } : {}) };
|
||||
} else delete covers[key];
|
||||
if (r.sub && !Object.keys(covers).length) delete owner.덮개;
|
||||
else owner.덮개 = covers;
|
||||
}
|
||||
|
||||
/** 공용 상세 고침 칸(`LogicLab_Cover.LineCover` 와 같은 꼴) — 품 · 비목 단가(로직 줄 = 1 단위 노 · 재 · 경) */
|
||||
@@ -236,10 +307,12 @@ export function setLineCover(doc: SkDoc, r: CoverRow, c: LineCover | null, opene
|
||||
return;
|
||||
}
|
||||
const cover: SkCover = {};
|
||||
const qty = c?.qty;
|
||||
// 세부 줄 품 = 화면 품 ÷ 더한줄 수량(1 기준단위 몫 · 수량 0 = 그대로)
|
||||
const qty = r.sub && c?.qty != null && r.sub.scale ? c.qty / r.sub.scale : c?.qty;
|
||||
const open = opened && !r.sub; // 열린입력 = 바탕 입력 — 세부 줄은 품 그대로
|
||||
// ponytail: 열린입력 배율 = 고친 품 ÷ 바탕 품(바탕 품 0 · 없음 = 품 고침 못 함)
|
||||
if (qty != null && opened && r.was?.qty) cover.배율 = qty / r.was.qty;
|
||||
if (qty != null && !opened && !near(qty, r.was?.qty)) cover.qty = qty;
|
||||
if (qty != null && open && r.was?.qty) cover.배율 = qty / r.was.qty;
|
||||
if (qty != null && !open && !near(qty, r.was?.qty)) cover.qty = qty;
|
||||
if (r.priceEdit) for (const k of PARTS) if (c?.[k] != null) cover[k] = c[k];
|
||||
if (cover.배율 != null && near(cover.배율, 1)) delete cover.배율;
|
||||
putCover(doc, r, cover);
|
||||
@@ -247,6 +320,12 @@ export function setLineCover(doc: SkDoc, r: CoverRow, c: LineCover | null, opene
|
||||
|
||||
/** 줄 빼기 — 더한줄 = 그 줄 지움 · 바탕 줄 = 뺀줄에 cid(덮개 · 고름 · 자재표시도 지움) */
|
||||
export function dropLine(doc: SkDoc, r: CoverRow): void {
|
||||
const one = r.sub ? doc.더한줄?.[r.sub.k] : undefined;
|
||||
if (one && r.sub) {
|
||||
one.뺀줄 = [...(one.뺀줄 ?? []).filter((c) => c !== r.sub!.cid), r.sub.cid];
|
||||
if (one.덮개) delete one.덮개[r.sub.cid];
|
||||
return;
|
||||
}
|
||||
if (r.added !== null) {
|
||||
doc.더한줄 = (doc.더한줄 ?? []).filter((_, i) => i !== r.added);
|
||||
return;
|
||||
@@ -267,11 +346,23 @@ export function setApart(doc: SkDoc, r: CoverRow, on: boolean): void {
|
||||
else delete one.별도계상;
|
||||
}
|
||||
|
||||
/** 뺀 줄 되살림 */
|
||||
/** 뺀 줄 되살림 — 세부 열쇠면 그 더한줄의 뺀줄 */
|
||||
export function restoreLine(doc: SkDoc, cid: string): void {
|
||||
const at = subOf(cid);
|
||||
const one = at ? doc.더한줄?.[at.k] : undefined;
|
||||
if (one && at) {
|
||||
one.뺀줄 = (one.뺀줄 ?? []).filter((c) => c !== at.cid);
|
||||
if (!one.뺀줄.length) delete one.뺀줄;
|
||||
return;
|
||||
}
|
||||
doc.뺀줄 = (doc.뺀줄 ?? []).filter((c) => c !== cid);
|
||||
}
|
||||
|
||||
/** 둘째 기초 통째 빼기 — 그 더한줄 지움 */
|
||||
export function dropBase(doc: SkDoc, k: number): void {
|
||||
doc.더한줄 = (doc.더한줄 ?? []).filter((_, i) => i !== k);
|
||||
}
|
||||
|
||||
const NUM = new Intl.NumberFormat("ko-KR", { maximumFractionDigits: 4 });
|
||||
const num = (v: number | null): string => (v == null ? "—" : NUM.format(v));
|
||||
|
||||
@@ -295,7 +386,7 @@ export function sheetGroups(rows: CoverRow[]): SheetGroup[] {
|
||||
글: r.pct ? `${num(r.수량)} %` : `${num(share)} * ${num(r.수량)} = ${num(amount)}`,
|
||||
금액: amount,
|
||||
묶음: r.묶음,
|
||||
장비: entries.length > 1 && r.cid.startsWith("logic:"),
|
||||
장비: entries.length > 1 && (r.sub?.cid ?? r.cid).startsWith("logic:"),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -313,11 +404,14 @@ export interface HoItem {
|
||||
단위?: string;
|
||||
수량: string;
|
||||
비목?: string;
|
||||
/** 출처 꼬리표(727-2 · 기초 둘 이상) */
|
||||
꼬리표?: string;
|
||||
}
|
||||
export interface HoExtra {
|
||||
이름: string;
|
||||
식: string;
|
||||
비목?: string;
|
||||
꼬리표?: string;
|
||||
}
|
||||
export interface HoView {
|
||||
호표: HoItem[];
|
||||
@@ -349,18 +443,28 @@ export function hoView(
|
||||
const it = i < head.length ? head[i] : base.덧줄?.[i - head.length];
|
||||
if (it) known.set(cid, it);
|
||||
});
|
||||
const lineBy = new Map(ans.줄.map((l) => [l.cid, l]));
|
||||
const lineBy = new Map(flatLines(ans).map((l) => [l.cid, l]));
|
||||
const tags = tagsOf(ans, doc);
|
||||
const single = (l?: SkLine): string | undefined => {
|
||||
const parts = Object.entries(l?.비목 ?? {}).filter(([, v]) => v);
|
||||
return parts.length === 1 ? parts[0][0] : undefined;
|
||||
};
|
||||
const ho: [string, HoItem][] = [];
|
||||
const extra: [string, HoExtra][] = [];
|
||||
for (const cid of ans.정체) {
|
||||
for (const cid of flatIds(ans)) {
|
||||
const l = lineBy.get(cid);
|
||||
const name = l?.이름 ?? cid;
|
||||
if (cid.startsWith("pct:")) {
|
||||
extra.push([cid, (known.get(cid) as HoExtra) ?? { 이름: name, 식: "" }]);
|
||||
const at = subOf(cid);
|
||||
const tag = tags.has(cid) ? { 꼬리표: tags.get(cid) } : {};
|
||||
if ((at?.cid ?? cid).startsWith("pct:")) {
|
||||
extra.push([cid, { ...((known.get(cid) as HoExtra) ?? { 이름: name, 식: "" }), ...tag }]);
|
||||
continue;
|
||||
}
|
||||
if (at) {
|
||||
const kind = KIND_OF[at.cid.split(":")[0]] ?? "로직";
|
||||
const qty = String(l?.율 ?? l?.수량 ?? "");
|
||||
const item = { 종류: kind, 요소: "", 이름: name, 단위: l?.단위 ?? "", 수량: qty };
|
||||
ho.push([cid, { ...item, 비목: single(l), ...tag }]);
|
||||
continue;
|
||||
}
|
||||
const hit = ADD_RE.exec(cid);
|
||||
@@ -378,7 +482,7 @@ export function hoView(
|
||||
수량: String(a?.수량 ?? l?.율 ?? l?.수량 ?? ""),
|
||||
비목: typeof a?.비목 === "string" ? a.비목 : single(l),
|
||||
};
|
||||
ho.push([cid, a ? made : ((known.get(cid) as HoItem) ?? made)]);
|
||||
ho.push([cid, { ...(a ? made : ((known.get(cid) as HoItem) ?? made)), ...tag }]);
|
||||
}
|
||||
const cids = [...ho, ...extra].map(([cid]) => cid);
|
||||
const covers: Record<number, LineCover> = {};
|
||||
@@ -387,11 +491,13 @@ export function hoView(
|
||||
const a = hit ? doc.더한줄?.[Number(hit[1])] : undefined;
|
||||
const part = PART_OF[String(a?.비목 ?? "")];
|
||||
if (a) covers[i] = { qty: a.수량, ...(a.단가 != null && part ? { [part]: a.단가 } : {}) };
|
||||
const c = hit ? undefined : doc.덮개?.[cid];
|
||||
const at = subOf(cid);
|
||||
const owner = at ? doc.더한줄?.[at.k] : undefined;
|
||||
const c = hit ? undefined : at ? owner?.덮개?.[at.cid] : doc.덮개?.[cid];
|
||||
if (!c) return;
|
||||
const l = lineBy.get(cid);
|
||||
const out: LineCover = {};
|
||||
if (c.qty != null) out.qty = c.qty;
|
||||
if (c.qty != null) out.qty = at ? c.qty * Number(owner?.수량 ?? 1) : c.qty;
|
||||
else if (c.배율 != null && l) out.qty = l.율 ?? l.수량 ?? undefined;
|
||||
for (const k of PARTS) if (c[k] != null) out[k] = c[k];
|
||||
covers[i] = out;
|
||||
|
||||
Reference in New Issue
Block a user