Compare commits
261
Commits
284ea58419
...
CODEX
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
863a958398 | ||
|
|
4706229100 | ||
|
|
812928a51f | ||
|
|
7b765c2599 | ||
|
|
67cb82d8d2 | ||
|
|
3e7c477631 | ||
|
|
6369c31eef | ||
|
|
6dc9fb84c5 | ||
|
|
aa58f3ef7a | ||
|
|
3722bfa23c | ||
|
|
c05916369c | ||
|
|
892285843d | ||
|
|
5270eb335f | ||
|
|
fb1ddb1be6 | ||
|
|
fa63c96ddc | ||
|
|
db9bc2dee4 | ||
|
|
b97ecf47de | ||
|
|
0f353f8606 | ||
|
|
515b683b82 | ||
|
|
3900d88a7c | ||
|
|
009c58a72d | ||
|
|
86a1ad3144 | ||
|
|
14f960dde1 | ||
|
|
d515aa1eb1 | ||
|
|
edd86767d2 | ||
|
|
053fc83ccf | ||
|
|
04cba1082c | ||
|
|
d515875322 | ||
|
|
42bb744455 | ||
|
|
b3f5cc2b7b | ||
|
|
d7682c69c6 | ||
|
|
684e0e7684 | ||
|
|
8be1ff6135 | ||
|
|
785e96809f | ||
|
|
84f9eb4248 | ||
|
|
c500ac8281 | ||
|
|
854e251129 | ||
|
|
21f49df818 | ||
|
|
9de0cb665a | ||
|
|
dc425995bc | ||
|
|
cc1a6bcbde | ||
|
|
7222577442 | ||
|
|
5e6c2988fc | ||
|
|
627e04e5a2 | ||
|
|
399c8fea49 | ||
|
|
596fcf99e0 | ||
|
|
d38d2a2893 | ||
|
|
787ff7b0b8 | ||
|
|
8231f8a2a2 | ||
|
|
bce7e245e0 | ||
|
|
f1bc5fbec5 | ||
|
|
89f8ae1c07 | ||
|
|
0392cd12ad | ||
|
|
345f23a058 | ||
|
|
b51af5e4a5 | ||
|
|
88b9f2454a | ||
|
|
d2ac897b97 | ||
|
|
0c3f1145c2 | ||
|
|
c392aabed5 | ||
|
|
f5484e8906 | ||
|
|
51cc9b4098 | ||
|
|
7553e52852 | ||
|
|
de80022c3e | ||
|
|
f454f46112 | ||
|
|
db78a06b8b | ||
|
|
98e6bb73f8 | ||
|
|
39c147a8a4 | ||
|
|
6c3d5e0291 | ||
|
|
5e7613eef7 | ||
|
|
423c09ad42 | ||
|
|
a16f3b4c47 | ||
|
|
fea0c4aa77 | ||
|
|
c08e025134 | ||
|
|
ceab1ab8c5 | ||
|
|
24b3c5f26b | ||
|
|
e41a20b89b | ||
|
|
a80101826c | ||
|
|
48dc72e71a | ||
|
|
44593e82fc | ||
|
|
0ea097da24 | ||
|
|
552cda6cd7 | ||
|
|
100f05a9a3 | ||
|
|
bb243af270 | ||
|
|
f8f4be7c75 | ||
|
|
78f9d941b0 | ||
|
|
83e5eca1a3 | ||
|
|
507b5a5166 | ||
|
|
d88eff4886 | ||
|
|
9954efbed8 | ||
|
|
ea50105ce5 | ||
|
|
36816da760 | ||
|
|
7418617e55 | ||
|
|
c0d49e0bbb | ||
|
|
21cae74df9 | ||
|
|
944e38377e | ||
|
|
6f32263ea3 | ||
|
|
5edff04fa8 | ||
|
|
d9337a0714 | ||
|
|
ff19138727 | ||
|
|
5b2ebeb676 | ||
|
|
98ba72319f | ||
|
|
bbb240ab30 | ||
|
|
11a3cc53cf | ||
|
|
b06b737437 | ||
|
|
0b7923c3c1 | ||
|
|
f7b68be72a | ||
|
|
27869d7b81 | ||
|
|
8c3a3d4267 | ||
|
|
26a18a1709 | ||
|
|
42f36953e2 | ||
|
|
d998772176 | ||
|
|
20300af829 | ||
|
|
4be09d66c2 | ||
|
|
4cfdef9f49 | ||
|
|
31ca6fe1bf | ||
|
|
6e93dcf3aa | ||
|
|
6475cf1ea8 | ||
|
|
fcba24e32d | ||
|
|
7f00e4067d | ||
|
|
6518238ef1 | ||
|
|
dc365e2140 | ||
|
|
5e9b6a7bea | ||
|
|
4876ae1e75 | ||
|
|
62fc670914 | ||
|
|
ae8d098a10 | ||
|
|
fb07a81527 | ||
|
|
1c9c4319fd | ||
|
|
2ba29dcc60 | ||
|
|
029c7dab5a | ||
|
|
91d2fff24d | ||
|
|
f34c63ef46 | ||
|
|
e7e59fc513 | ||
|
|
6206b2cace | ||
|
|
fce824b471 | ||
|
|
285332c44a | ||
|
|
fc3bab4ea7 | ||
|
|
8edd8ad6ee | ||
|
|
982ba07fea | ||
|
|
02ceb1e456 | ||
|
|
fe59907f56 | ||
|
|
bd3d258616 | ||
|
|
030a843f07 | ||
|
|
d25183e89d | ||
|
|
8e6246493f | ||
|
|
3e361dc08d | ||
|
|
c2b9ce859e | ||
|
|
4549e28fa5 | ||
|
|
55ad1bfee0 | ||
|
|
afb3fcff8d | ||
|
|
510316e65d | ||
|
|
408c93c56c | ||
|
|
9559d3a15a | ||
|
|
fbd1145623 | ||
|
|
c776db2f5e | ||
|
|
c66ebb1593 | ||
|
|
cdf18e1262 | ||
|
|
95951d57c9 | ||
|
|
b3ebdd2b73 | ||
|
|
eeb6b0635f | ||
|
|
8de7165912 | ||
|
|
a9fd9b9713 | ||
|
|
414593d78f | ||
|
|
7a97e87a71 | ||
|
|
748f3842c8 | ||
|
|
6ed63ba310 | ||
|
|
7ff08c3663 | ||
|
|
7fc3f098c7 | ||
|
|
0c2a0b1971 | ||
|
|
9ed50f2b0f | ||
|
|
e4f0250c74 | ||
|
|
bed35e3bc3 | ||
|
|
8914b21bad | ||
|
|
56a3e0538f | ||
|
|
4bd9e1484c | ||
|
|
a70788e585 | ||
|
|
335abf5b22 | ||
|
|
05fc3fc849 | ||
|
|
89c6b8edc4 | ||
|
|
4c30347139 | ||
|
|
542abb86a1 | ||
|
|
a74cc19ef3 | ||
|
|
fedd522b21 | ||
|
|
2e8d8e0d6c | ||
|
|
716f68391f | ||
|
|
5bf08ac5af | ||
|
|
282ec87512 | ||
|
|
32043f3c01 | ||
|
|
5520e3c575 | ||
|
|
a65677ffca | ||
|
|
d1c6806024 | ||
|
|
ff92a46936 | ||
|
|
4e766701e8 | ||
|
|
b606c06a9d | ||
|
|
9ddb4467d2 | ||
|
|
28acddb4b9 | ||
|
|
ae31ac00fb | ||
|
|
6ac48240a1 | ||
|
|
0e97fbb0f8 | ||
|
|
8474dbfb4f | ||
|
|
c3ab4bdc21 | ||
|
|
e404924aef | ||
|
|
3d4b2ec1d1 | ||
|
|
f204844fec | ||
|
|
0b91e37806 | ||
|
|
58a3e039b3 | ||
|
|
9592e1aec5 | ||
|
|
ad9a50c584 | ||
|
|
603c9654ef | ||
|
|
23b12ef94e | ||
|
|
c1872fe41b | ||
|
|
1a0eaf1dee | ||
|
|
cbbe03e81d | ||
|
|
3439e1ec6f | ||
|
|
741805ffde | ||
|
|
da8867b0e7 | ||
|
|
8e3072b95e | ||
|
|
7b8a5850df | ||
|
|
806d5ecc7d | ||
|
|
7317dbe4e5 | ||
|
|
5b4d6e9daf | ||
|
|
a07ad33306 | ||
|
|
09f4de0607 | ||
|
|
5c551c3105 | ||
|
|
221ac33e81 | ||
|
|
aef35a456b | ||
|
|
1724d98ae9 | ||
|
|
8a3ac5ad78 | ||
|
|
cdf0e46914 | ||
|
|
b0c0c923fc | ||
|
|
462e79abe2 | ||
|
|
dbc1263232 | ||
|
|
c2e84eaa3d | ||
|
|
7dd5bd7608 | ||
|
|
85408c100d | ||
|
|
937c5f9a56 | ||
|
|
e6cbe3ddac | ||
|
|
f072aab248 | ||
|
|
852aedf849 | ||
|
|
d25894a473 | ||
|
|
f60c593686 | ||
|
|
252fbf7fee | ||
|
|
af156a3c3a | ||
|
|
fd30e71dba | ||
|
|
b8cd50fd63 | ||
|
|
74faf9cf40 | ||
|
|
eaab09896b | ||
|
|
0c27c0793a | ||
|
|
a83b923363 | ||
|
|
9e59cffbbe | ||
|
|
4e7ebf7f87 | ||
|
|
043bc1ee78 | ||
|
|
2094436577 | ||
|
|
79769e6a47 | ||
|
|
22588ea7c4 | ||
|
|
fbaf2f1d28 | ||
|
|
f1abfc2fb1 | ||
|
|
61bb15fbde | ||
|
|
1e4ce036c6 | ||
|
|
5779943540 | ||
|
|
74446990f8 | ||
|
|
8cd7baea5e |
@@ -452,27 +452,3 @@
|
||||
width: auto;
|
||||
min-width: 8em;
|
||||
}
|
||||
|
||||
/* 가져온 식 값 · 추정 칸(810-3) — 칸 + 꼬리표 */
|
||||
.lstk-src {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: var(--spacing-4);
|
||||
}
|
||||
|
||||
.lstk-src > .ui-field {
|
||||
width: 7em;
|
||||
}
|
||||
|
||||
.lstk-src > [data-guess] {
|
||||
width: auto;
|
||||
min-width: 18em;
|
||||
}
|
||||
|
||||
.lstk-src > [data-guess] input {
|
||||
min-width: 18em;
|
||||
}
|
||||
|
||||
.lstk-src > .ui-badge {
|
||||
margin-bottom: var(--spacing-4);
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
* 고르면 줄이 생김(닫으면 안 생김) · 있는 줄 = 자원 이름 단추
|
||||
* ========================================================================== */
|
||||
|
||||
import { createBadge } from "@ui/ui_template_badge";
|
||||
import { createButton, createIconButton } from "@ui/ui_template_button";
|
||||
import { createRowMenu } from "@ui/ui_template_context_menu";
|
||||
import { eqText } from "@ui/ui_template_equation";
|
||||
@@ -97,6 +98,36 @@ const KIND_KEY: Record<MoneyKind, LsKey> = {
|
||||
율: "K_율",
|
||||
};
|
||||
|
||||
/** 가져온 기계 찾기 판정(서버 기계 찾기 · 문서 `가져온.기계`) — 그 단가 기호 칸 */
|
||||
interface FoundMach {
|
||||
기호?: string;
|
||||
판정?: string;
|
||||
찾기어?: string;
|
||||
조합?: boolean;
|
||||
후보?: { 이름?: string; 규격?: string }[];
|
||||
}
|
||||
|
||||
function machineFound(d: FormulaDoc, sym: string): FoundMach | undefined {
|
||||
const head = (d as { 가져온?: { 기계?: FoundMach[] } }).가져온;
|
||||
return head?.기계?.find((x) => x.기호 === sym);
|
||||
}
|
||||
|
||||
/** 못 이은 가져온 기계 줄 표지(고르면 사라짐 — 연결이 단가산출로 바뀜) · 조합 기계 다음 줄 */
|
||||
function machineMark(f: FoundMach | undefined, link: { 갈래?: string }): HTMLElement | null {
|
||||
if (!f) return null;
|
||||
const picked = link.갈래 === "단가산출";
|
||||
const key = f.판정 === "후보 여럿" ? "Mach_Many" : f.판정 === "못 찾음" ? "Mach_None" : null;
|
||||
const badge =
|
||||
key && !picked
|
||||
? createBadge(lt(key), "warning", { size: 18 })
|
||||
: f.조합
|
||||
? createBadge(lt("Mach_Combo"), "neutral", { size: 18 })
|
||||
: null;
|
||||
if (badge && key && f.후보?.length)
|
||||
badge.title = f.후보.map((c) => `${c.이름 ?? ""} ${c.규격 ?? ""}`.trim()).join(" · ");
|
||||
return badge;
|
||||
}
|
||||
|
||||
/** 금액 줄 몸 — 한 줄(804-7 · 요구 687-7): [종류][이름 단추 | 이름 칸 · 단가 식][수량][단위][비목] · 좁으면 접힘 */
|
||||
function moneyBody(h: EditHooks, i: number): HTMLElement[] {
|
||||
const d = h.doc;
|
||||
@@ -122,6 +153,8 @@ function moneyBody(h: EditHooks, i: number): HTMLElement[] {
|
||||
? (h.subDoc(link.키)?.이름 ?? h.name(link.키))
|
||||
: h.name(link.키)
|
||||
: "";
|
||||
// 가져온 기계 줄(812-9) — 찾기 판정 · 조회어 = 기종 이름만(규격 뺌 · 이은 우리 기계 › 정규화 이름)
|
||||
const found = kind === "기계" ? machineFound(d, own) : undefined;
|
||||
const pick = createButton({
|
||||
label: (size && !name.includes(size) ? `${name} ${size}` : name) || "—",
|
||||
variant: "ghost",
|
||||
@@ -133,13 +166,15 @@ function moneyBody(h: EditHooks, i: number): HTMLElement[] {
|
||||
kind,
|
||||
(p) => h.change((doc) => repick(doc, i, own, kind, p)),
|
||||
undefined,
|
||||
String(mat?.검색어 ?? line.이름 ?? ""),
|
||||
found?.찾기어 ?? String(mat?.검색어 ?? line.이름 ?? ""),
|
||||
),
|
||||
});
|
||||
pick.classList.add("lstk-res");
|
||||
pick.title = `${lt("Repick")}${res ? ` — ${res}` : ""}`;
|
||||
pick.dataset.repick = String(i);
|
||||
head.push(pick);
|
||||
const mark = machineMark(found, link);
|
||||
if (mark) head.push(mark);
|
||||
} else
|
||||
head.push(
|
||||
field(lt("Name"), line.이름, (t) => put((l) => (l.이름 = t)), "m"),
|
||||
|
||||
@@ -248,7 +248,11 @@ function rows(
|
||||
}
|
||||
});
|
||||
if (Object.keys(at).length) scope.번지 = at;
|
||||
const name = [line.이름, (line as { 규격?: string }).규격].filter(Boolean).join(" ");
|
||||
const spec = (line as { 규격?: string }).규격;
|
||||
const name =
|
||||
spec && !String(line.이름 ?? "").includes(spec)
|
||||
? `${line.이름 ?? ""} ${spec}`.trim()
|
||||
: String(line.이름 ?? ""); // 이름에 규격이 들면 한 번만(814-1)
|
||||
const qty =
|
||||
line.수량 !== undefined && line.수량 !== null && line.수량 !== "" ? line.수량 : null;
|
||||
const qtyNode = (cost?: string): (string | HTMLElement)[] =>
|
||||
|
||||
@@ -48,7 +48,6 @@ import {
|
||||
} from "./formula_doc_copy";
|
||||
import { docFoot } from "./formula_doc_foot";
|
||||
import { buildOptions } from "./formula_doc_options";
|
||||
import { buildSourced } from "./line_stack_sourced";
|
||||
import { ft } from "./formula_doc_text";
|
||||
import { addTaken, type Taken } from "../common_util/common_util_line_stack_copy";
|
||||
import type { Layer } from "../M02_MasterTemplete/M02_MasterTemplete_Api_Fetch";
|
||||
@@ -281,10 +280,10 @@ export function createStackScreen(opt: StackScreenOptions) {
|
||||
recalc();
|
||||
draw();
|
||||
};
|
||||
const options = [
|
||||
...buildOptions(doc, given, !!cp && !opt.admin, setOption),
|
||||
...buildSourced(doc, !!cp && !opt.admin, setOption), // 가져온 식 값 · 추정 칸(810-3)
|
||||
];
|
||||
// 가져온 식 = 옵션 가로 안 그림(819-1 · 요구 787 — 값은 왼쪽 공식 묶음에 같이 보임)
|
||||
const options = (doc as { 가져온?: unknown }).가져온
|
||||
? []
|
||||
: buildOptions(doc, given, !!cp && !opt.admin, setOption);
|
||||
const busy = dirty();
|
||||
const tools = [
|
||||
...(mark?.단가기준 && opt.page ? [basisPick(mark.단가기준)] : []), // 모달 = 머리 판 단추(793-3b)
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
/* =============================================================================
|
||||
* line_stack_sourced.ts
|
||||
* 가져온 식 옵션 가로 칸(PLAN 810-3 · 요구 705 · 703) — 줄 쌓기 위 옵션 가로(`formula_doc_options.ts`) 뒤에 붙음
|
||||
* 값 칸 = 경쟁사 파일에 값으로 드러난 수(q · K · E · Cm · Q …) · 꼬리표 「원파일」 · 「엑셀」(`sourceOf`) ·
|
||||
* 고침 = 그 숫자 연결 값(부른 쪽 `set` · 캐시 → [저장])
|
||||
* 규격 칸 = 원파일 블록 규격(읽기만) · 추정 칸 = 값 없이 이름만 적힌 품셈 조건 글(문서 `가져온.추정` · 읽기만 ·
|
||||
* M01 기초에 묶지 않음 · 703)
|
||||
* ========================================================================== */
|
||||
|
||||
import { createBadge } from "@ui/ui_template_badge";
|
||||
import { el } from "@ui/ui_template_elements";
|
||||
import { createInputField } from "@ui/ui_template_input";
|
||||
import { varId, type DocVar, type FormulaDoc } from "../common_util/common_util_formula_doc";
|
||||
import { sourceOf } from "../common_util/common_util_line_stack";
|
||||
import { lt } from "./line_stack_text";
|
||||
|
||||
interface Head {
|
||||
원파일?: boolean;
|
||||
규격?: string;
|
||||
추정?: { 행?: number; 글?: string }[];
|
||||
}
|
||||
|
||||
const cell = (field: HTMLElement, tag: string, tone: "info" | "warning", hint: string) =>
|
||||
el("div", {
|
||||
className: "lstk-src",
|
||||
attrs: { title: hint },
|
||||
children: [field, createBadge(tag, tone)],
|
||||
});
|
||||
|
||||
export function buildSourced(
|
||||
doc: FormulaDoc | null,
|
||||
lock: boolean,
|
||||
set: (key: string, v: DocVar, value: unknown) => void,
|
||||
): HTMLElement[] {
|
||||
const head = (doc as { 가져온?: Head } | null)?.가져온;
|
||||
if (!doc || !head) return [];
|
||||
const out: HTMLElement[] = [];
|
||||
for (const v of doc.변수 ?? []) {
|
||||
const tag = sourceOf(doc, v);
|
||||
if (!tag) continue;
|
||||
const id = varId(v);
|
||||
const f = createInputField({
|
||||
label: v.이름 || id,
|
||||
value: String(v.연결?.값 ?? ""),
|
||||
compact: true,
|
||||
size: 28,
|
||||
unit: v.단위 || undefined,
|
||||
});
|
||||
f.input.disabled = lock;
|
||||
f.input.addEventListener("change", () => {
|
||||
const n = Number(f.input.value.trim().replace(/,/g, ""));
|
||||
if (f.input.value.trim() === "" || !Number.isFinite(n))
|
||||
return void (f.input.value = String(v.연결?.값 ?? "")); // 수만 — 빈 값 · 글 = 그대로
|
||||
set(id, v, n); // 수 그대로(글 「0.7」 = 셈 「—」 · 공식 묶음 값 칸 `setRow` 와 같은 꼴 · 812-3)
|
||||
});
|
||||
f.root.dataset.option = id;
|
||||
const tagText = tag === "원파일" ? lt("Src_Native") : tag === "엑셀" ? lt("Src_Excel") : tag;
|
||||
out.push(cell(f.root, tagText, "info", lt("Src_Hint")));
|
||||
}
|
||||
const ro = (label: string, value: string, mark: string, guess: boolean) => {
|
||||
const f = createInputField({ label, value, compact: true, size: 28 });
|
||||
f.input.readOnly = true;
|
||||
f.input.title = value; // 긴 조건 글 = 칸 위에 올리면 다 보임
|
||||
if (guess) f.root.dataset.guess = mark;
|
||||
return guess
|
||||
? cell(f.root, lt("Guess"), "warning", lt("Guess_Hint"))
|
||||
: cell(f.root, lt("Src_Native"), "info", lt("Src_Hint"));
|
||||
};
|
||||
if (head.규격) out.push(ro(lt("Spec"), head.규격, "", false));
|
||||
for (const g of head.추정 ?? [])
|
||||
out.push(
|
||||
ro(
|
||||
`${lt("Guess_Label")}${g.행 !== undefined ? ` · ${g.행}행` : ""}`,
|
||||
String(g.글 ?? "").replace(/^※\s*/, ""),
|
||||
String(g.행 ?? ""),
|
||||
true,
|
||||
),
|
||||
);
|
||||
return out;
|
||||
}
|
||||
@@ -14,7 +14,7 @@ const TEXT = {
|
||||
Add_Item: ["+ 그룹", "+ Group"],
|
||||
Add_Line: ["줄 더함", "Add line"],
|
||||
Add_율: ["율 — 그룹 소계의 %", "Rate — % of subtotal"],
|
||||
Add_식: ["식 — 공식 틀", "Formula"],
|
||||
Add_식: ["식", "Formula"],
|
||||
Del_Item: ["그룹 지움", "Delete group"],
|
||||
Del: ["지움", "Delete"],
|
||||
Item: ["그룹", "Group"],
|
||||
@@ -38,17 +38,6 @@ const TEXT = {
|
||||
K_소계: ["소계", "Subtotal"],
|
||||
Src_Native: ["원파일", "Source file"],
|
||||
Src_Excel: ["엑셀", "Excel"],
|
||||
Src_Hint: [
|
||||
"경쟁사 파일에 값으로 적힌 수 — 고치면 이 단가산출 값",
|
||||
"Value written in the competitor file — edit to change",
|
||||
],
|
||||
Guess: ["추정", "Guess"],
|
||||
Guess_Label: ["품셈 조건", "Condition"],
|
||||
Guess_Hint: [
|
||||
"값 없이 이름만 적힌 품셈 조건 — 우리 기초에 묶지 않음",
|
||||
"Condition named without a value — not bound",
|
||||
],
|
||||
Spec: ["규격", "Spec"],
|
||||
K_공식: ["공식", "Formula"],
|
||||
K_표값: ["표 값", "Table values"],
|
||||
K_글: ["근거 글", "Basis note"],
|
||||
@@ -87,6 +76,9 @@ const TEXT = {
|
||||
],
|
||||
Direct_Hint: ["표 값을 손으로 고침 — 표 연결 끊김", "Table value typed by hand — table link off"],
|
||||
Tbl_Missing: ["표를 못 읽음", "Table not found"],
|
||||
Mach_Many: ["기계 후보 여럿 — 고르기", "Several machines — pick one"],
|
||||
Mach_None: ["기계 못 찾음 — 고르기", "Machine not found — pick one"],
|
||||
Mach_Combo: ["조합 기계 — 경비는 첫 줄", "Combined machine — cost on first line"],
|
||||
Tbl_Hint: ["값 칸을 누르면 그 값이 들어감", "Click a value cell to use it"],
|
||||
Tbl_Bound: [
|
||||
"이 값의 줄은 위 옵션이 고름(다른 줄은 옵션에서 바꿈 · 여기선 칸만)",
|
||||
|
||||
@@ -5,14 +5,10 @@
|
||||
저절로: 지난번 이음 · 하나 수 · 남은 · 자리 · 이은(`board`)}
|
||||
POST /api/projects/{id}/estimation/link/apply
|
||||
{pairs [{line, target, mode?}], auto, mode, counts?, user?} · 알림 남은 = 안 이은 항목 수
|
||||
잇기(`link`) — `auto` = 지난번 이음 · 하나도 더함 →
|
||||
{user(새 캐시), 알림, payload(그 캐시로 셈)} ·
|
||||
이은 짝 = 맞춤 기억에 씀(회사 층 = 회사 관리자 · 아니면 개인 층)
|
||||
POST …/link/undo {target, user?} → {user, 알림, payload} — 잇기 풀기(맞춤 기억 그대로)
|
||||
POST …/link/forget {key, user?} → {잊음, 기억층, 남음} — 이 짝 잊기(불러온 줄 · 이은 줄 키 →
|
||||
맞춤 열쇠 · 쓸 수 있는 층에 잊음 표시 `{잊음: True}` — 느슨한 기억도 안 불러옴 ·
|
||||
개인 층 표시가 회사 기억을 덮음 · 남음 = 그래도 살아 있는 기억)
|
||||
맞춤 기억 = 층 `실무자료/내역맞춤.json`(실무자료 맞춤 꼴) · 읽기 = 회사 + 개인(개인이 이김).
|
||||
잇기(`link`) → {user(새 캐시), 알림, payload(그 캐시로 셈)}
|
||||
POST …/link/undo {target, user?} → {user, 알림, payload} — 잇기 풀기
|
||||
공사 내역 맞춤 기억(`실무자료/내역맞춤.json` · 지난번 이음 · 잊기)은 걷음(815-2 · 요구 765-2) —
|
||||
제안은 짝 표만 · 옛 기억 파일은 읽지도 쓰지도 않음(지우지 않음).
|
||||
끼워 넣기 = `B09_Estimation_Router`(같은 머리 · 같은 권한) · 화면 = `B09_Estimation_UI_Link`.
|
||||
"""
|
||||
|
||||
@@ -30,42 +26,9 @@ from B08_Quantity.B08_Quantity_Router_Hydro import _project_root
|
||||
from B09_Estimation import B09_Estimation_Bill_Link as linker
|
||||
from B09_Estimation import B09_Estimation_Store as store
|
||||
from common_util.common_util_auth import verify_session
|
||||
from M02_MasterTemplete import M02_Hopyo_Store as hopyo_store
|
||||
from M02_MasterTemplete import M02_Layer_Access as access
|
||||
from M02_MasterTemplete import M02_MasterTemplete_Store as m02_store
|
||||
|
||||
router = APIRouter()
|
||||
PATH = "/{project_id}/estimation/link"
|
||||
LAYERS = ("company", "personal")
|
||||
|
||||
|
||||
def _folders(session: dict[str, Any]) -> dict[str, tuple[Path, bool]]:
|
||||
"""{층: (폴더, 쓰기)} — 회사 없음 등 = 그 층 건너뜀."""
|
||||
out = {}
|
||||
for layer in LAYERS:
|
||||
try:
|
||||
got = access.grant(session, layer)
|
||||
out[layer] = (access.folder(got, create=True), got.write)
|
||||
except HTTPException:
|
||||
continue
|
||||
return out
|
||||
|
||||
|
||||
def recalled(session: dict[str, Any]) -> dict[str, dict[str, Any]]:
|
||||
"""맞춤 기억 — 마스터(시스템 관리자 승인 · 718-2) 위에 회사 · 그 위에 개인을 덮음."""
|
||||
out: dict[str, dict[str, Any]] = dict(hopyo_store.memory(m02_store.FOLDER, linker.MEMORY))
|
||||
for folder, _write in _folders(session).values():
|
||||
out.update(hopyo_store.memory(folder, linker.MEMORY))
|
||||
return out
|
||||
|
||||
|
||||
def keep(session: dict[str, Any], entries: dict[str, dict[str, Any]]) -> str | None:
|
||||
"""이은 짝 → 회사 층(쓸 수 있으면) · 아니면 개인 층 → 쓴 층(없음 = None)."""
|
||||
folders = _folders(session)
|
||||
layer = next((n for n in LAYERS if n in folders and folders[n][1]), None)
|
||||
if entries and layer:
|
||||
hopyo_store.remember(folders[layer][0], entries, linker.MEMORY)
|
||||
return layer if entries else None
|
||||
|
||||
|
||||
def _draft(raw: Any) -> dict[str, Any] | None:
|
||||
@@ -87,11 +50,6 @@ class UndoBody(BaseModel):
|
||||
user: dict[str, Any] | None = None
|
||||
|
||||
|
||||
class ForgetBody(BaseModel):
|
||||
key: str = Field(min_length=1, max_length=200)
|
||||
user: dict[str, Any] | None = None
|
||||
|
||||
|
||||
class ApplyBody(BaseModel):
|
||||
pairs: list[Pair] = Field(default_factory=list, max_length=2000)
|
||||
auto: bool = False
|
||||
@@ -115,8 +73,7 @@ async def suggest(
|
||||
rows = await _rows(project_id, root, draft)
|
||||
board = linker.board(draft, rows)
|
||||
left = {r["key"] for r in board["남은"]}
|
||||
memory = await asyncio.to_thread(recalled, session)
|
||||
lines = [s for s in linker.suggest(rows, memory) if s["key"] in left]
|
||||
lines = [s for s in linker.suggest(rows, {}) if s["key"] in left]
|
||||
return {"줄": lines, "저절로": len(linker.auto_pairs(lines)), **board}
|
||||
|
||||
|
||||
@@ -134,15 +91,14 @@ async def apply(
|
||||
rows = await _rows(project_id, root, draft)
|
||||
pairs = [(p.line, p.target) for p in body.pairs]
|
||||
if body.auto:
|
||||
memory = await asyncio.to_thread(recalled, session)
|
||||
pairs += [p for p in linker.auto_pairs(linker.suggest(rows, memory)) if p not in pairs]
|
||||
pairs += [p for p in linker.auto_pairs(linker.suggest(rows, {})) if p not in pairs]
|
||||
made, report = linker.link(draft, rows, pairs, body.mode, body.counts, modes)
|
||||
made = store.clean_user(made)
|
||||
layer = await asyncio.to_thread(keep, session, report.pop("기억"))
|
||||
report.pop("기억")
|
||||
out = await payload(project_id, root, made, True)
|
||||
left = linker.board(made, out["sheets"]["bill"]["rows"])["남은"]
|
||||
report["남은"] = sum(1 for r in left if r["kind"] == "item")
|
||||
return {"user": made, "알림": {**report, "기억층": layer}, "payload": out}
|
||||
return {"user": made, "알림": report, "payload": out}
|
||||
|
||||
|
||||
@router.post(PATH + "/undo")
|
||||
@@ -162,21 +118,3 @@ async def undo(
|
||||
"알림": {"풀음": 1},
|
||||
"payload": await payload(project_id, root, made, True),
|
||||
}
|
||||
|
||||
|
||||
@router.post(PATH + "/forget")
|
||||
async def forget(
|
||||
project_id: UUID, body: ForgetBody, session: dict[str, Any] = Depends(verify_session)
|
||||
) -> dict[str, Any]:
|
||||
root = await _project_root(project_id)
|
||||
draft = _draft(body.user) or await asyncio.to_thread(store.read_user, root)
|
||||
rows = await _rows(project_id, root, draft)
|
||||
board = linker.board(draft, rows)
|
||||
hit = next((r for r in board["남은"] if r["key"] == body.key), None)
|
||||
memo = linker.key_of(hit) if hit else None
|
||||
memo = memo or next((r["memo"] for r in board["이은"] if r["key"] == body.key), None)
|
||||
if not memo:
|
||||
raise HTTPException(status_code=400, detail="불러온 줄 · 이은 줄이 아님")
|
||||
layer = await asyncio.to_thread(keep, session, {memo: {linker.FORGOT: True}})
|
||||
left = bool((await asyncio.to_thread(recalled, session)).get(memo, {}).get("확정"))
|
||||
return {"잊음": memo, "기억층": layer, "남음": left}
|
||||
|
||||
@@ -4,10 +4,9 @@
|
||||
* 우리 자동 줄(토공 · 운반 · 구조물)에 이어 B08 수량이 붙게. 저장 안 함(받은 캐시 → [저장] 때 서버 정본).
|
||||
*
|
||||
* 길 = `B09_Estimation_Router_Link.py` link/suggest(제안 · 남은 줄 · 빈 자리 · 이은 줄) ·
|
||||
* link/apply(잇기 · 맞춤 기억 씀) · link/undo(잇기 풀기) · link/forget(이 짝 잊기) — 셈 = 서버 단독
|
||||
* link/apply(잇기) · link/undo(잇기 풀기) — 셈 = 서버 단독 · 맞춤 기억 · 잊기는 걷음(815-2)
|
||||
* 창 = 제안 줄(꼬리표 지난번 이음 · 짝 표 · 이름 같음 · 후보 고름 · 수량 갈래 · 묶음 개수 · [잇기]) ·
|
||||
* 제안 없는 줄([우리 공종으로 잇기] = 단가연결 고르기 창 재사용 · 우리 줄만) · 이은 줄([잇기 풀기] ·
|
||||
* [이 짝 잊기]) · 머리 [모두 제안대로](짧은 길 = 불러오기 → 모두 제안대로 → [저장])
|
||||
* 제안 없는 줄([우리 공종으로 잇기] = 단가연결 고르기 창 재사용 · 우리 줄만) · 이은 줄([잇기 풀기]) · 머리 [모두 제안대로](짧은 길 = 불러오기 → 모두 제안대로 → [저장])
|
||||
* 수량 갈래 기본 = 불러올 때 고른 것(가 넘겨받기 · 나 단가만) · 줄에서 바꿈 ·
|
||||
* 묶음 나 = 개수 칸(빈칸 = B08 수량 · 다르면 수량 칸 붉게)
|
||||
* 고리 = 불러오기 끝(`…_UI_Import`) · 설계내역서 우클릭(`…_UI_Tabs_Bill_Edit`)
|
||||
@@ -185,14 +184,6 @@ export async function openBillLink(
|
||||
.map((s) => [s.key, Number(pickOf(s).count)])
|
||||
.filter(([, n]) => Number.isFinite(n) && (n as number) > 0),
|
||||
);
|
||||
const forget = (key: string): void =>
|
||||
run(async () => {
|
||||
const got = await post("forget", { key, user: cur });
|
||||
showToast(
|
||||
got.남음 ? "이 짝 잊음 — 회사 기억이 남음(회사 관리자만 지움)" : "이 짝 잊음",
|
||||
got.남음 ? "warning" : "success",
|
||||
);
|
||||
});
|
||||
|
||||
const suggestRow = (s: Row): HTMLElement => {
|
||||
const p = pickOf(s);
|
||||
@@ -233,10 +224,6 @@ export async function openBillLink(
|
||||
onClick: () => run(() => linkPairs(cur, done, [pairOf(s)], countsOf([s]))),
|
||||
}),
|
||||
);
|
||||
if (s.꼬리표 === "지난번 이음")
|
||||
items.push(
|
||||
createButton({ label: "이 짝 잊기", variant: "ghost", onClick: () => forget(s.key) }),
|
||||
);
|
||||
return createListRow({
|
||||
label: label(s),
|
||||
sub: s.까닭 || (s.kind === "item" ? "" : "묶음 → 구조물 줄 통째"),
|
||||
@@ -269,7 +256,6 @@ export async function openBillLink(
|
||||
variant: "ghost",
|
||||
onClick: () => run(() => undoLink(cur, done, r.key)),
|
||||
}),
|
||||
createButton({ label: "이 짝 잊기", variant: "ghost", onClick: () => forget(r.key) }),
|
||||
],
|
||||
}),
|
||||
});
|
||||
@@ -326,23 +312,15 @@ async function undoLink(user: Row, done: LinkDone, target: string): Promise<Row>
|
||||
return got.user;
|
||||
}
|
||||
|
||||
/** 설계내역서 우클릭 — 불러온 줄 = 잇기 · 이은 줄(`was`) = 풀기 · 잊기 */
|
||||
/** 설계내역서 우클릭 — 불러온 줄 = 잇기 · 이은 줄(`was`) = 풀기 */
|
||||
export function linkMenu(ctx: TabContext, key: string): MapContextMenuItem[] {
|
||||
const done: LinkDone = (payload, user) => {
|
||||
Object.assign(ctx.payload, payload);
|
||||
ctx.setUser(user);
|
||||
};
|
||||
const entry = ((ctx.user.bill?.rows as Row[]) || []).find((e) => e.key === key);
|
||||
const forget = (): void =>
|
||||
void post("forget", { key, user: ctx.user }).then(
|
||||
(got) => showToast(got.남음 ? "회사 기억이 남음(회사 관리자만 지움)" : "이 짝 잊음", "info"),
|
||||
fail("못 잊음"),
|
||||
);
|
||||
if (entry?.was)
|
||||
return [
|
||||
["잇기 풀기", () => void undoLink(ctx.user, done, key).catch(fail("못 풂"))],
|
||||
["이 짝 잊기", forget],
|
||||
];
|
||||
return [["잇기 풀기", () => void undoLink(ctx.user, done, key).catch(fail("못 풂"))]];
|
||||
if (!key.startsWith("hand:x")) return [];
|
||||
return [["우리 공종으로 잇기", () => void openBillLink(ctx.user, done)]];
|
||||
}
|
||||
|
||||
@@ -1,17 +1,22 @@
|
||||
"""M01 별칭 장부 시스템 모으기(PLAN 718-2 · 요구서 내역12 4장 23 · 4-1 확정 6).
|
||||
"""M01 별칭 모아 보기 · 제안 → 승인(PLAN 815-2 · 요구 764 · 765) — 서버 단독.
|
||||
|
||||
모든 회사 · 개인 층의 별칭 장부(`실무자료/맞춤.json` · `M02_Hopyo_Ledger`)와 B09 공사 내역 맞춤
|
||||
기억(`실무자료/내역맞춤.json` · 707-4)을 읽어 후보로 모음 — 읽기만(공유 storage 안 씀).
|
||||
후보 = (종류, 열쇠, 서명) · 서명 = 짝 내용 지문(같은 짝이면 같은 서명) · 회사 수 · 사람 수만
|
||||
(누가 · 처음파일 · 층 주인은 안 내보냄 = 익명) · 충돌 = 같은 열쇠 다른 서명 · 마스터와 다름.
|
||||
승인 = 시스템 관리자만 → 마스터 층에 씀(맞춤 = `ledger.put` 재검사 · 내역맞춤 = 기억 파일) ·
|
||||
반려 = 마스터 층 `실무자료/별칭반려.json` 에 서명 기억(같은 짝은 다시 안 보임) · 자동 반영 없음.
|
||||
별칭 = 요소 · 로직 줄 칸 `별칭` 하나(콤마 여러 개 · 풀이 정본 `master_alias`).
|
||||
GET /api/m01/alias → {줄: 별칭이 적힌 줄 전부(갈래 · 키 · 이름 · 규격 · 별칭 · 조각 ·
|
||||
들어옴 수기/승인), 대기: 승인 대기 제안}
|
||||
POST /api/m01/alias/save {키, 별칭} → 그 줄 `별칭` 만 고침(M01 저장 길 `make.save`)
|
||||
POST /api/m01/alias/approve {id} → 그 요소 `별칭` 에 콤마로 더함 · 제안 = 「승인」 기록
|
||||
POST /api/m01/alias/reject {id} → 제안 지움
|
||||
POST /api/m01/alias/suggest {키, 이름, 규격?, 단위?(갈래는 서버가 키로 정함)} → 제안 하나(로그인만 · 일반 계정도)
|
||||
제안 파일 = `storage/_shared/별칭제안.json` `{제안: {id: {…, 상태 대기|승인}}}`.
|
||||
권한 = `router` 시스템 관리자 · `suggest_router` 로그인만(`main.py`).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import threading
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
@@ -20,160 +25,161 @@ from pydantic import BaseModel, Field
|
||||
|
||||
from common_util.common_util_auth import verify_session
|
||||
from common_util.common_util_time import utc_now_text
|
||||
from M02_MasterTemplete import M02_Hopyo_Ledger as ledger
|
||||
from M02_MasterTemplete import M02_Hopyo_Store as hopyo_store
|
||||
from M02_MasterTemplete import M02_MasterTemplete_Store as store
|
||||
from M01_MasterData import M01_MasterData_Store as store
|
||||
from M01_MasterData import M01_MasterData_Store_Make as make
|
||||
from M02_MasterTemplete import M02_Template_Layers as layers
|
||||
|
||||
ALIAS, BILL = "맞춤", "내역맞춤"
|
||||
KINDS = (ALIAS, BILL)
|
||||
REJECT = "별칭반려"
|
||||
import master_alias as ma # noqa: E402 (`store` 가 scripts 를 길에 둠)
|
||||
|
||||
router = APIRouter(prefix="/api/m01/alias-ledger", tags=["M01 별칭 장부"])
|
||||
ALIAS = "별칭"
|
||||
FUEL = "재료_유가전력.json"
|
||||
ELEMENTS = ("인력.json", "기계.json", "재료_자재품목.json", FUEL) # 제안을 받는 요소 파일
|
||||
_LOCK = threading.Lock()
|
||||
|
||||
router = APIRouter(prefix="/api/m01/alias", tags=["M01 별칭"])
|
||||
suggest_router = APIRouter(prefix="/api/m01/alias", tags=["M01 별칭"])
|
||||
|
||||
|
||||
# ── 읽기 ─────────────────────────────────────────────────────────────
|
||||
def layer_folders() -> list[tuple[str, str | None, Path]]:
|
||||
"""(회사, 사용자 | None, 층 폴더) — storage 의 회사 · 개인 층(숫자 폴더만 · 두 단만 훑음)."""
|
||||
root = layers.storage_root()
|
||||
out: list[tuple[str, str | None, Path]] = []
|
||||
for c in sorted(root.iterdir() if root.is_dir() else []):
|
||||
if not (c.is_dir() and c.name.isdigit()):
|
||||
def kind_of(file: str, group: str) -> str:
|
||||
if file == FUEL:
|
||||
return "유가"
|
||||
return "단가산출 기초" if group == "로직" else group
|
||||
|
||||
|
||||
# ── 제안 파일 ─────────────────────────────────────────────────────────
|
||||
def suggest_path() -> Path:
|
||||
return layers.storage_root() / "_shared" / "별칭제안.json"
|
||||
|
||||
|
||||
def _book() -> dict[str, dict[str, Any]]:
|
||||
try:
|
||||
got = json.loads(suggest_path().read_text(encoding="utf-8")).get("제안")
|
||||
except (OSError, ValueError, AttributeError):
|
||||
return {}
|
||||
return got if isinstance(got, dict) else {}
|
||||
|
||||
|
||||
def _keep(book: dict[str, dict[str, Any]]) -> None:
|
||||
path = suggest_path()
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
tmp = path.with_suffix(".tmp")
|
||||
tmp.write_text(json.dumps({"제안": book}, ensure_ascii=False, indent=1), encoding="utf-8")
|
||||
os.replace(tmp, path)
|
||||
|
||||
|
||||
# ── 줄 찾기 · 고치기 ───────────────────────────────────────────────────
|
||||
def _find(key: str, files: tuple[str, ...] | None = None) -> tuple[str, dict[str, Any]]:
|
||||
loaded, _ = store.loaded()
|
||||
for name, data in loaded.items():
|
||||
if files and name not in files:
|
||||
continue
|
||||
out.append((c.name, None, layers.company_dir(c.name)))
|
||||
for u in sorted(c.iterdir()):
|
||||
if u.is_dir() and u.name.isdigit():
|
||||
out.append((c.name, u.name, layers.personal_dir(c.name, u.name)))
|
||||
return out
|
||||
for row in data.get("줄") or []:
|
||||
if str(row.get("키")) == key:
|
||||
return name, row
|
||||
raise store.StoreError(404, f"없는 키 「{key}」")
|
||||
|
||||
|
||||
def _body(kind: str, row: dict[str, Any]) -> dict[str, Any]:
|
||||
"""서명에 드는 내용 — 맞춤 = 갈래 · 짝 · 내역맞춤 = 확정 줄 키 · 이름."""
|
||||
if kind == ALIAS:
|
||||
return {"갈래": row.get("갈래"), "짝": row.get("짝") or []}
|
||||
return {"확정": row.get("확정"), "이름": row.get("이름")}
|
||||
def set_alias(key: str, text: str) -> dict[str, Any]:
|
||||
"""그 줄 `별칭` 만 바꿔 M01 저장 길로(빈 글 = 칸 없음 · 별칭만이라 정본 개정 확인 안 물음)."""
|
||||
file, _ = _find(key)
|
||||
data, version = store.read(file)
|
||||
row = dict(next(r for r in data["줄"] if str(r.get("키")) == key))
|
||||
row[ALIAS] = text
|
||||
change = {"op": "edit", "key": key, "row": row}
|
||||
make.save([{"file": file, "version": version, "changes": [change]}], True)
|
||||
return board()
|
||||
|
||||
|
||||
def sign(kind: str, row: dict[str, Any]) -> str:
|
||||
raw = json.dumps(_body(kind, row), ensure_ascii=False, sort_keys=True)
|
||||
return hashlib.sha256(raw.encode("utf-8")).hexdigest()[:16]
|
||||
def _quoted(piece: str) -> str:
|
||||
return f'"{piece}"' if any(s in piece for s in (",", ",")) else piece
|
||||
|
||||
|
||||
def _book(kind: str, folder: Path) -> dict[str, dict[str, Any]]:
|
||||
if kind == ALIAS:
|
||||
return ledger.read(folder)
|
||||
got = hopyo_store.memory(folder, BILL)
|
||||
return {
|
||||
k: v
|
||||
for k, v in got.items()
|
||||
if isinstance(v, dict) and v.get("확정") and str(k).count("|") == 2
|
||||
} # 잊음 · 깨진 열쇠 뺌
|
||||
def _joined(text: Any, piece: str) -> str:
|
||||
"""별칭 글 끝에 한 조각 — 이미 있으면(정규화 같음) 그대로."""
|
||||
raw = str(text or "").strip()
|
||||
if ma.norm(piece) in {ma.norm(x) for x in ma.names(raw)}:
|
||||
return raw
|
||||
return f"{raw}, {_quoted(piece)}" if raw else _quoted(piece)
|
||||
|
||||
|
||||
def _origin(full: str, row: dict[str, Any]) -> dict[str, str]:
|
||||
if isinstance(row.get("원래"), dict):
|
||||
return {k: str(row["원래"].get(k) or "") for k in ("품명", "규격", "단위")}
|
||||
return ledger._plain_of(full)
|
||||
# ── 모아 보기 ─────────────────────────────────────────────────────────
|
||||
def board() -> dict[str, Any]:
|
||||
book = _book()
|
||||
approved = {
|
||||
(str(v.get("키")), ma.norm(v.get(ALIAS))) for v in book.values() if v.get("상태") == "승인"
|
||||
}
|
||||
rows = []
|
||||
loaded, _ = store.loaded()
|
||||
for file, data in loaded.items():
|
||||
kind = kind_of(file, str(data.get("그룹") or ""))
|
||||
for row in data.get("줄") or []:
|
||||
if not str(row.get(ALIAS) or "").strip():
|
||||
continue
|
||||
key = str(row.get("키"))
|
||||
pieces = [
|
||||
{"글": p, "승인": (key, ma.norm(p)) in approved} for p in ma.names(row[ALIAS])
|
||||
]
|
||||
rows.append(
|
||||
{
|
||||
"갈래": kind,
|
||||
"파일": file,
|
||||
"키": key,
|
||||
"이름": str(row.get("이름") or ""),
|
||||
"규격": str(row.get("규격") or ""),
|
||||
ALIAS: str(row[ALIAS]),
|
||||
"조각": pieces,
|
||||
"들어옴": "승인" if any(p["승인"] for p in pieces) else "수기",
|
||||
}
|
||||
)
|
||||
waiting = [{"id": k, **v} for k, v in book.items() if v.get("상태") == "대기"]
|
||||
waiting.sort(key=lambda x: str(x.get("때") or ""))
|
||||
return {"줄": rows, "대기": waiting}
|
||||
|
||||
|
||||
def rejected() -> dict[str, dict[str, Any]]:
|
||||
return hopyo_store.memory(store.FOLDER, REJECT)
|
||||
# ── 제안 · 승인 · 반려 ────────────────────────────────────────────────
|
||||
def suggest(key: str, name: str, spec: str, unit: str, who: Any) -> dict[str, Any]:
|
||||
file, row = _find(key, ELEMENTS)
|
||||
piece = " ".join(x for x in (name.strip(), spec.strip()) if x)
|
||||
if not piece:
|
||||
raise store.StoreError(400, "가져온 이름이 비었음")
|
||||
if _joined(row.get(ALIAS), piece) == str(row.get(ALIAS) or "").strip():
|
||||
return {"이미": True} # 이미 그 별칭이 있음
|
||||
sid = hashlib.sha256(f"{key}|{ma.norm(piece)}".encode()).hexdigest()[:16]
|
||||
with _LOCK:
|
||||
book = _book()
|
||||
if book.get(sid, {}).get("상태") != "승인":
|
||||
book[sid] = {
|
||||
"갈래": kind_of(file, str(store.loaded()[0][file].get("그룹") or "")),
|
||||
"키": key,
|
||||
"이름": name.strip(),
|
||||
"규격": spec.strip(),
|
||||
"단위": unit.strip(),
|
||||
ALIAS: piece,
|
||||
"상태": "대기",
|
||||
"누가": who,
|
||||
"때": utc_now_text(),
|
||||
}
|
||||
_keep(book)
|
||||
return {"id": sid}
|
||||
|
||||
|
||||
def _reject_id(kind: str, full: str, sig: str) -> str:
|
||||
return f"{kind}|{sig}|{full}"
|
||||
def decide(sid: str, ok: bool, who: Any) -> dict[str, Any]:
|
||||
with _LOCK:
|
||||
book = _book()
|
||||
one = book.get(sid)
|
||||
if not one or one.get("상태") != "대기":
|
||||
raise store.StoreError(404, "대기 중인 제안이 없음(이미 승인 · 반려)")
|
||||
if ok:
|
||||
_, row = _find(str(one["키"]), ELEMENTS)
|
||||
set_alias(str(one["키"]), _joined(row.get(ALIAS), str(one[ALIAS])))
|
||||
one.update(상태="승인", 승인={"누가": who, "때": utc_now_text()})
|
||||
else:
|
||||
del book[sid]
|
||||
_keep(book)
|
||||
return board()
|
||||
|
||||
|
||||
def gather() -> dict[str, Any]:
|
||||
"""후보 모음 `{후보, 모은곳{회사, 사람}, 반려}` — 마스터와 같은 짝 · 반려한 짝은 뺌."""
|
||||
seen: dict[tuple[str, str, str], dict[str, Any]] = {}
|
||||
firms, people = set(), set()
|
||||
for company, user, folder in layer_folders():
|
||||
for kind in KINDS:
|
||||
for full, row in _book(kind, folder).items():
|
||||
sig = sign(kind, row)
|
||||
who = (
|
||||
(row.get("누가") or {}).get("id") if isinstance(row.get("누가"), dict) else None
|
||||
)
|
||||
person = f"{company}/{user}" if user else f"{company}/{who or '-'}"
|
||||
one = seen.setdefault(
|
||||
(kind, full, sig),
|
||||
{"종류": kind, "열쇠": full, "서명": sig, "원래": _origin(full, row),
|
||||
**_body(kind, row), "회사": set(), "사람": set(), "때": None},
|
||||
) # fmt: skip
|
||||
one["회사"].add(company)
|
||||
one["사람"].add(person)
|
||||
firms.add(company)
|
||||
people.add(person)
|
||||
stamp = row.get("때")
|
||||
if stamp and (one["때"] is None or str(stamp) > str(one["때"])):
|
||||
one["때"] = stamp
|
||||
master = {kind: _book(kind, Path(store.FOLDER)) for kind in KINDS}
|
||||
gone = rejected()
|
||||
live = []
|
||||
for (kind, full, sig), one in seen.items():
|
||||
mine = master[kind].get(full)
|
||||
if (mine and sign(kind, mine) == sig) or _reject_id(kind, full, sig) in gone:
|
||||
continue
|
||||
one["마스터"] = "다름" if mine else None
|
||||
live.append(one)
|
||||
many: dict[tuple[str, str], int] = {}
|
||||
for one in live:
|
||||
many[(one["종류"], one["열쇠"])] = many.get((one["종류"], one["열쇠"]), 0) + 1
|
||||
out = []
|
||||
for one in sorted(live, key=lambda x: (x["종류"], x["열쇠"], -len(x["사람"]), x["서명"])):
|
||||
clash = many[(one["종류"], one["열쇠"])] > 1 or one["마스터"] == "다름"
|
||||
out.append({**one, "회사": len(one["회사"]), "사람": len(one["사람"]), "충돌": clash})
|
||||
return {"후보": out, "모은곳": {"회사": len(firms), "사람": len(people)}, "반려": len(gone)}
|
||||
|
||||
|
||||
def _find(kind: str, full: str, sig: str) -> dict[str, Any]:
|
||||
if kind not in KINDS:
|
||||
raise store.StoreError(400, f"종류는 {' · '.join(KINDS)}")
|
||||
got = next(
|
||||
(x for x in gather()["후보"] if (x["종류"], x["열쇠"], x["서명"]) == (kind, full, sig)),
|
||||
None,
|
||||
)
|
||||
if got is None:
|
||||
raise store.StoreError(404, f"후보가 없음(이미 승인 · 반려 · 바뀜) 「{full}」")
|
||||
return got
|
||||
|
||||
|
||||
# ── 쓰기(마스터 층만) ─────────────────────────────────────────────────
|
||||
def approve(kind: str, full: str, sig: str, who: Any) -> dict[str, Any]:
|
||||
"""후보 하나 → 마스터 장부(서버가 다시 모아 찾음 · 맞춤은 `ledger.put` 재검사)."""
|
||||
one = _find(kind, full, sig)
|
||||
if kind == ALIAS:
|
||||
ledger.put(store.FOLDER, {full: {"원래": one["원래"], **_body(kind, one)}}, who)
|
||||
else:
|
||||
row = {**_body(kind, one), "누가": who, "때": utc_now_text()}
|
||||
hopyo_store.remember(store.FOLDER, {full: row}, BILL)
|
||||
return gather()
|
||||
|
||||
|
||||
def reject(kind: str, full: str, sig: str, who: Any) -> dict[str, Any]:
|
||||
_find(kind, full, sig)
|
||||
row = {"종류": kind, "열쇠": full, "서명": sig, "누가": who, "때": utc_now_text()}
|
||||
hopyo_store.remember(store.FOLDER, {_reject_id(kind, full, sig): row}, REJECT)
|
||||
return gather()
|
||||
|
||||
|
||||
def master_bill() -> list[dict[str, Any]]:
|
||||
"""마스터 내역맞춤 줄(보기 · 지움) — 맞춤 장부는 `/api/m02/layers/system/hopyo-ledger`."""
|
||||
book = _book(BILL, Path(store.FOLDER))
|
||||
return [
|
||||
{"열쇠": k, "원래": ledger._plain_of(k), **_body(BILL, v)} for k, v in sorted(book.items())
|
||||
]
|
||||
|
||||
|
||||
def forget_bill(full: str) -> list[dict[str, Any]]:
|
||||
if full not in _book(BILL, Path(store.FOLDER)):
|
||||
raise store.StoreError(404, f"마스터 내역맞춤에 없음 「{full}」")
|
||||
hopyo_store.remember(store.FOLDER, {full: None}, BILL)
|
||||
return master_bill()
|
||||
|
||||
|
||||
# ── 길(시스템 관리자만 — main.py 에서 system_admin_only) ───────────────
|
||||
# ── 길 ───────────────────────────────────────────────────────────────
|
||||
def _who(session: dict[str, Any]) -> dict[str, Any]:
|
||||
return {"id": session.get("user_id"), "이름": session.get("name")}
|
||||
|
||||
@@ -185,31 +191,44 @@ def _call(fn, *args):
|
||||
raise HTTPException(error.status, error.detail) from error
|
||||
|
||||
|
||||
class Pick(BaseModel):
|
||||
종류: str = Field(max_length=10)
|
||||
열쇠: str = Field(min_length=1, max_length=400)
|
||||
서명: str = Field(min_length=16, max_length=16)
|
||||
class AliasBody(BaseModel):
|
||||
키: str = Field(min_length=1, max_length=20)
|
||||
별칭: str = Field(max_length=2000)
|
||||
|
||||
|
||||
class Decide(BaseModel):
|
||||
id: str = Field(min_length=16, max_length=16)
|
||||
|
||||
|
||||
class Suggest(BaseModel):
|
||||
갈래: str = Field(default="", max_length=20)
|
||||
키: str = Field(min_length=1, max_length=20)
|
||||
이름: str = Field(min_length=1, max_length=200)
|
||||
규격: str = Field(default="", max_length=200)
|
||||
단위: str = Field(default="", max_length=40)
|
||||
|
||||
|
||||
@router.get("")
|
||||
def candidates() -> dict[str, Any]:
|
||||
return {**gather(), "마스터내역": master_bill()}
|
||||
def get_board() -> dict[str, Any]:
|
||||
return board()
|
||||
|
||||
|
||||
@router.post("/save")
|
||||
def post_save(body: AliasBody) -> dict[str, Any]:
|
||||
return _call(set_alias, body.키, body.별칭)
|
||||
|
||||
|
||||
@router.post("/approve")
|
||||
def approve_one(body: Pick, session: dict = Depends(verify_session)) -> dict[str, Any]:
|
||||
return _call(approve, body.종류, body.열쇠, body.서명, _who(session))
|
||||
def post_approve(body: Decide, session: dict = Depends(verify_session)) -> dict[str, Any]:
|
||||
return _call(decide, body.id, True, _who(session))
|
||||
|
||||
|
||||
@router.post("/reject")
|
||||
def reject_one(body: Pick, session: dict = Depends(verify_session)) -> dict[str, Any]:
|
||||
return _call(reject, body.종류, body.열쇠, body.서명, _who(session))
|
||||
def post_reject(body: Decide, session: dict = Depends(verify_session)) -> dict[str, Any]:
|
||||
return _call(decide, body.id, False, _who(session))
|
||||
|
||||
|
||||
class Forget(BaseModel):
|
||||
열쇠: str = Field(min_length=1, max_length=400)
|
||||
|
||||
|
||||
@router.post("/master-bill/forget")
|
||||
def forget_one(body: Forget) -> dict[str, Any]:
|
||||
return {"마스터내역": _call(forget_bill, body.열쇠)}
|
||||
@suggest_router.post("/suggest")
|
||||
def post_suggest(body: Suggest, session: dict = Depends(verify_session)) -> dict[str, Any]:
|
||||
args = (body.키, body.이름, body.규격, body.단위, _who(session))
|
||||
return _call(suggest, *args)
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
"""부품 문서(PLAN 813-2 · 요구 722-3 · 724~735) — 수식 문서 머리 칸 `부품` 읽기.
|
||||
|
||||
갈래 = 기계경비식(기계 시간당 경비 셈 방식) · 작업량 틀(`기종` = 기계표 상세구분 목록 ·
|
||||
`규격` = 고른 기계 규격 수를 받는 변수 열쇠) · 계수식(기계 무관 · 돈 아닌 식).
|
||||
틀 = `_수식문서_틀.md` 9 장.
|
||||
- 단가산출 기초 목록 · 찾아 넣기 「다른 단가산출」 목록에서 뺌(`strip`) — 키로 열기 · 셈 · 부르기는 그대로
|
||||
- 기종 → 그 기종 기계가 쓰는 기계경비식 · 작업량 틀(`of_kind` · M01 「기계」 화면)
|
||||
- 기계 원문번호 → 그 기종 작업량 틀 문서(`of_machine` · 줄 쌓기 기계 넣기)
|
||||
쓰기 없음 · 로그인 사용자 모두(마스터 읽기).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from M01_MasterData import M01_MasterData_Doc as doc_store
|
||||
from M01_MasterData import M01_MasterData_Store as store
|
||||
|
||||
KINDS = ("기계경비식", "작업량 틀", "계수식")
|
||||
|
||||
|
||||
def parts() -> dict[str, dict]:
|
||||
"""{문서 키: 부품 표지}."""
|
||||
return {k: d["부품"] for k, d in doc_store.docs().items() if isinstance(d.get("부품"), dict)}
|
||||
|
||||
|
||||
def strip(rows: list[dict]) -> list[dict]:
|
||||
"""목록에서 부품 문서를 뺌."""
|
||||
have = parts()
|
||||
return [r for r in rows if str(r.get("키")) not in have]
|
||||
|
||||
|
||||
def _brief(key: str, d: dict) -> dict[str, Any]:
|
||||
tag = d.get("부품") or {}
|
||||
return {
|
||||
"키": key,
|
||||
"이름": d.get("이름") or "",
|
||||
"결과단위": d.get("결과단위") or "",
|
||||
"갈래": tag.get("갈래") or "",
|
||||
"규격": tag.get("규격") or "",
|
||||
}
|
||||
|
||||
|
||||
def _machines() -> list[dict]:
|
||||
files = store.loaded()[0]
|
||||
return list((files.get("기계.json") or {}).get("줄") or [])
|
||||
|
||||
|
||||
def _templates(kind: str) -> list[tuple[str, dict]]:
|
||||
return [
|
||||
(k, d)
|
||||
for k, d in doc_store.docs().items()
|
||||
if (d.get("부품") or {}).get("갈래") == "작업량 틀" and kind in d["부품"].get("기종", [])
|
||||
]
|
||||
|
||||
|
||||
def of_kind(group: str, detail: str = "") -> dict[str, list]:
|
||||
"""M01 화면 — 계수식 = 그 넷 · 기계 = 상세구분 `detail` 기계가 쓰는 기계경비식(기계 수)
|
||||
+ 작업량 틀."""
|
||||
docs = doc_store.docs()
|
||||
if group == "계수식":
|
||||
return {
|
||||
"계수식": [
|
||||
_brief(k, d) for k, d in docs.items() if parts().get(k, {}).get("갈래") == "계수식"
|
||||
]
|
||||
}
|
||||
from M02_MasterTemplete import M02_Machine_Hourly as hourly # M02 가 M01 을 부름 — 늦게
|
||||
|
||||
used: dict[str, int] = {}
|
||||
for m in _machines():
|
||||
if m.get("상세구분") != detail:
|
||||
continue
|
||||
got = hourly.logic_of(str(m.get("원문번호") or ""))
|
||||
if got:
|
||||
used[str(got["키"])] = used.get(str(got["키"]), 0) + 1
|
||||
rule = [{**_brief(k, docs[k]), "기계수": n} for k, n in used.items() if k in docs]
|
||||
return {"기계경비식": rule, "작업량 틀": [_brief(k, d) for k, d in _templates(detail)]}
|
||||
|
||||
|
||||
def of_machine(no: str) -> dict[str, Any]:
|
||||
"""줄 쌓기 기계 넣기 — 기계 원문번호 → {기계 {이름 · 규격 · 상세구분}, 틀 [{… 문서}]}."""
|
||||
row = next((m for m in _machines() if str(m.get("원문번호")) == str(no)), None)
|
||||
if not row:
|
||||
raise store.StoreError(404, f"없는 기계 「{no}」")
|
||||
kind = str(row.get("상세구분") or "")
|
||||
return {
|
||||
"기계": {"이름": row.get("이름"), "규격": row.get("규격") or "", "상세구분": kind},
|
||||
"틀": [{**_brief(k, d), "문서": d} for k, d in _templates(kind)],
|
||||
}
|
||||
@@ -14,6 +14,7 @@ from pydantic import BaseModel
|
||||
|
||||
from M01_MasterData import M01_MasterData_Doc as doc_screen
|
||||
from M01_MasterData import M01_MasterData_Equation as equation
|
||||
from M01_MasterData import M01_MasterData_Parts as parts
|
||||
from M01_MasterData import M01_MasterData_Sheet as line_sheet
|
||||
from M01_MasterData import M01_MasterData_Store as store
|
||||
from M01_MasterData import M01_MasterData_Store_Combo as combo_store
|
||||
@@ -205,7 +206,14 @@ def get_logics(
|
||||
sub: str = "", detail: str = "", q: str = "", blocked: int | None = None, owner: str = ""
|
||||
) -> dict:
|
||||
"""줄마다 덧칸(묶음 · 작업량 · 부르는/불리는 수 · 관련 · 713-3) — 읽기만."""
|
||||
return {"logics": store_list.attach(_call(store.logics, sub, detail, q, blocked, owner))}
|
||||
rows = parts.strip(_call(store.logics, sub, detail, q, blocked, owner)) # 부품 문서 뺌(813-2)
|
||||
return {"logics": store_list.attach(rows)}
|
||||
|
||||
|
||||
@router.get("/parts")
|
||||
def get_parts(group: str, detail: str = "") -> dict:
|
||||
"""부품 문서(813-2) — 계수식 · 기계 상세구분의 기계경비식 · 작업량 틀."""
|
||||
return _call(parts.of_kind, group, detail)
|
||||
|
||||
|
||||
@router.get("/logic")
|
||||
|
||||
@@ -283,6 +283,15 @@ def save(batch: list[dict], confirmed: bool = False) -> dict:
|
||||
"""`/save` — 로직 정본 줄 고침은 `confirmed`(「원문 개정 반영」) 없으면 409(`Store_Revise`) ·
|
||||
자체 파일 새 줄(키 없음)은 서버가 칸을 잡고 GX 다음 키 — {files, added, warnings}."""
|
||||
revise.guard(batch, confirmed)
|
||||
for part in batch:
|
||||
for ch in part.get("changes") or []:
|
||||
row = ch.get("row")
|
||||
if (
|
||||
isinstance(row, dict) and "별칭" in row
|
||||
): # 별칭(815-2) = 앞뒤 빈칸만 뗌 · 빈 글 = 칸 없음
|
||||
text = str(row["별칭"] or "").strip()
|
||||
row.update(별칭=text) if text else row.pop("별칭")
|
||||
_keep_same(part)
|
||||
fresh = False
|
||||
for part in batch:
|
||||
if part.get("file") != OWN:
|
||||
@@ -308,6 +317,20 @@ def save(batch: list[dict], confirmed: bool = False) -> dict:
|
||||
return {"files": files, "added": added, "warnings": []}
|
||||
|
||||
|
||||
def _keep_same(part: dict) -> None:
|
||||
"""고친 줄의 값이 같은 칸 = 파일 값 그대로(화면 왕복 「9.0」 → 「9」 막음 · 그 칸만 바뀜)."""
|
||||
try:
|
||||
data = store.read(str(part.get("file") or ""))[0]
|
||||
except store.StoreError:
|
||||
return
|
||||
rows = {str(r.get("키")): r for r in data.get(store.items_key(data)) or []}
|
||||
for ch in part.get("changes") or []:
|
||||
old, row = rows.get(str(ch.get("key"))), ch.get("row")
|
||||
if ch.get("op") == "edit" and isinstance(old, dict) and isinstance(row, dict):
|
||||
same = {k for k, v in row.items() if k in old and store._dec(v) == old[k]}
|
||||
ch["row"] = {k: old[k] if k in same else v for k, v in row.items()}
|
||||
|
||||
|
||||
def _file_keys(file: str) -> list[str]:
|
||||
try:
|
||||
data = store.read(file)[0]
|
||||
|
||||
@@ -1,67 +1,55 @@
|
||||
/* =============================================================================
|
||||
* M01_MasterData_UI_Alias.ts
|
||||
* M01 별칭 장부(PLAN 718-3 · 요구서 내역12 4장 23 · 4-1 확정 6) — 본 칸 · 시스템 관리자만.
|
||||
* · 「모인 후보」 = 모든 회사 · 개인 층 별칭 장부(일위대가 맞춤) + 공사 내역 맞춤을 서버가 모음
|
||||
* (`/api/m01/alias-ledger` · 718-2) · 이름 · 짝 · 회사 수 · 사람 수(누군지 안 보임) · 충돌 ·
|
||||
* [승인] = 마스터 장부에 넣음(모든 사용자 제안에 쓰임) · [반려] = 다시 안 보임 · 둘 다 확인 창
|
||||
* · 「마스터 장부」 = M02 별칭 장부 부품(`buildHopyoLedger` · 마스터 층) 그대로 + 마스터 공사 내역 맞춤 줄 [지움]
|
||||
* · 자동 반영 없음 — 승인한 것만 모두에게 · 승인 · 반려 · 지움은 누를 때 바로 서버(확인 창 뒤)
|
||||
* M01 별칭 모아 보기(PLAN 815-2 · 요구 764 · 765) — 본 칸 · 시스템 관리자만.
|
||||
* · 위 = 「승인 대기 n」 — 일반 사용자 제안(가져온 이름 · 규격 → 우리 요소) · [승인] = 그 요소 별칭에
|
||||
* 콤마로 더함 · [반려] = 지움(둘 다 확인 창 뒤 바로 서버)
|
||||
* · 아래 = 별칭이 적힌 줄 전부(인력 · 기계 · 재료 · 유가 · 단가산출 기초 — 수기로 넣은 것도 바로) ·
|
||||
* 갈래 거름 · 찾기 · 별칭 칸 눌러 고침(콤마로 여러 개) → [저장](그 줄 별칭만 · `/api/m01/alias/save`) ·
|
||||
* 줄을 누르면 그 요소 화면 그 줄 · 단가산출 기초는 로직 탭
|
||||
* · 서버 = `M01_MasterData_Alias.py` · 조각 나누기 = 서버(`master_alias.names`)
|
||||
* ========================================================================== */
|
||||
|
||||
import { API_BASE_URL } from "@config/config_frontend";
|
||||
import { createBadge } from "@ui/ui_template_badge";
|
||||
import { askConfirm, createButton, el, showToast } from "@ui/ui_template_elements";
|
||||
import { createInputField } from "@ui/ui_template_input";
|
||||
import { loadingError, showLoading } from "@ui/ui_template_loading";
|
||||
import { createPanelHead } from "@ui/ui_template_panel_head";
|
||||
import { createSegment } from "@ui/ui_template_segment";
|
||||
import { createTable, type Column } from "@ui/ui_template_table";
|
||||
import { createTabs } from "@ui/ui_template_tabs";
|
||||
import "@ui/ui_template_general_layout.css"; // 마스터 장부 부품(M02 Hopyo_Ledger) 표 꼴 — M01 엔 안 실려 있음
|
||||
import type { LedgerPair } from "../M02_MasterTemplete/M02_MasterTemplete_Hopyo_Api";
|
||||
import {
|
||||
buildHopyoLedger,
|
||||
KIND_TEXT,
|
||||
LEDGER_HINT,
|
||||
pairText,
|
||||
} from "../M02_MasterTemplete/M02_MasterTemplete_Hopyo_Ledger";
|
||||
import { startEdit } from "./M01_MasterData_UI_Cells";
|
||||
|
||||
export const ALIAS_TITLE = "별칭 장부";
|
||||
const KIND_NAME: Record<string, string> = { 맞춤: "일위대가", 내역맞춤: "공사 내역" };
|
||||
export const ALIAS_TITLE = "별칭 모아 보기";
|
||||
const KINDS = ["인력", "기계", "재료", "유가", "단가산출 기초"];
|
||||
|
||||
interface Origin {
|
||||
품명: string;
|
||||
export interface AliasRow {
|
||||
갈래: string;
|
||||
파일: string;
|
||||
키: string;
|
||||
이름: string;
|
||||
규격: string;
|
||||
별칭: string;
|
||||
조각: { 글: string; 승인: boolean }[];
|
||||
들어옴: "수기" | "승인";
|
||||
}
|
||||
interface Waiting {
|
||||
id: string;
|
||||
갈래: string;
|
||||
키: string;
|
||||
이름: string;
|
||||
규격: string;
|
||||
단위: string;
|
||||
별칭: string;
|
||||
누가?: { 이름?: string } | null;
|
||||
때?: string;
|
||||
}
|
||||
export interface AliasCandidate {
|
||||
종류: "맞춤" | "내역맞춤";
|
||||
열쇠: string;
|
||||
서명: string;
|
||||
원래: Origin;
|
||||
갈래?: string;
|
||||
짝?: LedgerPair[];
|
||||
확정?: string;
|
||||
이름?: string;
|
||||
회사: number;
|
||||
사람: number;
|
||||
때: string | null;
|
||||
충돌: boolean;
|
||||
마스터: "다름" | null;
|
||||
}
|
||||
interface MasterBill {
|
||||
열쇠: string;
|
||||
원래: Origin;
|
||||
확정: string;
|
||||
이름?: string;
|
||||
}
|
||||
export interface AliasBoard {
|
||||
후보: AliasCandidate[];
|
||||
모은곳: { 회사: number; 사람: number };
|
||||
반려: number;
|
||||
마스터내역?: MasterBill[];
|
||||
interface Board {
|
||||
줄: AliasRow[];
|
||||
대기: Waiting[];
|
||||
}
|
||||
|
||||
async function call<T>(path: string, body?: unknown): Promise<T> {
|
||||
const res = await fetch(`${API_BASE_URL}/m01/alias-ledger${path}`, {
|
||||
const res = await fetch(`${API_BASE_URL}/m01/alias${path}`, {
|
||||
method: body === undefined ? "GET" : "POST",
|
||||
credentials: "include",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
@@ -72,72 +60,75 @@ async function call<T>(path: string, body?: unknown): Promise<T> {
|
||||
return got;
|
||||
}
|
||||
|
||||
/** 짝 글 — 일위대가 = 갈래 · 우리 로직 · 공사 내역 = 우리 공종 이름 */
|
||||
export const candidateText = (c: AliasCandidate): string =>
|
||||
c.종류 === "내역맞춤"
|
||||
? `${c.이름 ?? ""} (${c.확정 ?? ""})`
|
||||
: c.갈래 === "짝"
|
||||
? (c.짝 ?? []).map(pairText).join(" + ")
|
||||
: (KIND_TEXT[c.갈래 ?? ""] ?? c.갈래 ?? "");
|
||||
|
||||
const why = (error: unknown): string => (error instanceof Error ? error.message : String(error));
|
||||
const pick = (c: AliasCandidate) => ({ 종류: c.종류, 열쇠: c.열쇠, 서명: c.서명 });
|
||||
|
||||
const CANDIDATE_COLS: Column[] = [
|
||||
{ label: "종류", width: "84px" },
|
||||
{ label: "엑셀 품명" },
|
||||
{ label: "규격" },
|
||||
const WAIT_COLS: Column[] = [
|
||||
{ label: "갈래", width: "96px" },
|
||||
{ label: "가져온 이름 · 규격" },
|
||||
{ label: "단위", width: "64px" },
|
||||
{ label: "짝" },
|
||||
{ label: "회사", width: "56px", align: "end" },
|
||||
{ label: "사람", width: "56px", align: "end" },
|
||||
{ label: "충돌", width: "96px" },
|
||||
{ label: "우리 요소" },
|
||||
{ label: "누가 · 때", width: "180px" },
|
||||
{ label: "", width: "136px" },
|
||||
];
|
||||
const BILL_COLS: Column[] = [
|
||||
{ label: "엑셀 품명" },
|
||||
{ label: "규격" },
|
||||
{ label: "단위", width: "64px" },
|
||||
{ label: "우리 공종" },
|
||||
{ label: "", width: "72px" },
|
||||
const ROW_COLS: Column[] = [
|
||||
{ label: "갈래", width: "96px" },
|
||||
{ label: "키", width: "96px" },
|
||||
{ label: "이름 · 규격" },
|
||||
{ label: "별칭" },
|
||||
{ label: "들어옴", width: "72px" },
|
||||
];
|
||||
|
||||
export function buildAliasLedger(): HTMLElement {
|
||||
const root = el("div", { className: "m01-alias", attrs: { "data-alias-ledger": "" } });
|
||||
const body = el("div", { className: "m01-alias__body" });
|
||||
let view = "candidates";
|
||||
/** `open` = 줄을 눌렀을 때(왼쪽 판이 그 요소 화면 · 로직 탭으로) · `setDirty` = 저장 안 한 고침 표시 */
|
||||
export function buildAliasBoard(opts: {
|
||||
open: (row: AliasRow) => void;
|
||||
setDirty?: (dirty: boolean) => void;
|
||||
}): HTMLElement {
|
||||
const root = el("div", { className: "m01-alias", attrs: { "data-alias-board": "" } });
|
||||
const waitHost = el("div");
|
||||
const listHost = el("div");
|
||||
const edits = new Map<string, string>();
|
||||
let board: Board = { 줄: [], 대기: [] };
|
||||
let kind = "";
|
||||
let q = "";
|
||||
|
||||
async function act(c: AliasCandidate, ok: boolean): Promise<void> {
|
||||
const name = `${c.원래.품명} ${c.원래.규격}`.trim();
|
||||
const save = createButton({ label: "저장", variant: "filled", disabled: true });
|
||||
const undo = createButton({ label: "되돌림", variant: "ghost", disabled: true });
|
||||
const mark = (): void => {
|
||||
save.disabled = undo.disabled = edits.size === 0;
|
||||
opts.setDirty?.(edits.size > 0);
|
||||
};
|
||||
|
||||
async function decide(w: Waiting, ok: boolean): Promise<void> {
|
||||
const yes = await askConfirm({
|
||||
title: ok ? "마스터 장부에 승인" : "후보 반려",
|
||||
title: ok ? "별칭 승인" : "제안 반려",
|
||||
message: ok
|
||||
? `「${name}」 → ${candidateText(c)}\n마스터 장부에 넣으면 모든 사용자 제안에 쓰임(각자 장부가 먼저).`
|
||||
: `「${name}」 → ${candidateText(c)}\n반려하면 같은 짝은 후보에 다시 안 보임.`,
|
||||
? `「${w.별칭}」 → ${w.키}\n그 요소 별칭에 더함(다음 불러오기부터 이어짐).`
|
||||
: `「${w.별칭}」 → ${w.키}\n반려하면 제안이 지워짐.`,
|
||||
okLabel: ok ? "승인" : "반려",
|
||||
danger: !ok,
|
||||
});
|
||||
if (!yes) return;
|
||||
try {
|
||||
const got = await call<AliasBoard>(ok ? "/approve" : "/reject", pick(c));
|
||||
showToast(ok ? "마스터 장부에 넣음" : "반려함", "success");
|
||||
paintCandidates(got);
|
||||
board = await call<Board>(ok ? "/approve" : "/reject", { id: w.id });
|
||||
showToast(ok ? "별칭에 더함" : "반려함", "success");
|
||||
paint();
|
||||
} catch (error) {
|
||||
showToast(`${ok ? "승인" : "반려"} 못 함 — ${why(error)}`, "error");
|
||||
}
|
||||
}
|
||||
|
||||
function candidateRow(c: AliasCandidate): (string | Node)[] {
|
||||
const clash = c.마스터 === "다름" ? "마스터와 다름" : c.충돌 ? "다른 짝 있음" : "";
|
||||
return [
|
||||
KIND_NAME[c.종류] ?? c.종류,
|
||||
c.원래.품명,
|
||||
c.원래.규격,
|
||||
c.원래.단위,
|
||||
candidateText(c),
|
||||
String(c.회사),
|
||||
String(c.사람),
|
||||
clash ? createBadge(clash, "warning") : "",
|
||||
const target = (key: string): string => {
|
||||
const one = board.줄.find((r) => r.키 === key);
|
||||
return one ? `${key} · ${one.이름} ${one.규격}`.trim() : key;
|
||||
};
|
||||
|
||||
function paintWaiting(): void {
|
||||
const rows = board.대기.map((w) => [
|
||||
w.갈래,
|
||||
`${w.이름} ${w.규격}`.trim(),
|
||||
w.단위,
|
||||
target(w.키),
|
||||
`${w.누가?.이름 ?? ""} · ${(w.때 ?? "").slice(0, 16).replace("T", " ")}`,
|
||||
el("div", {
|
||||
className: "ui-toolbar",
|
||||
children: [
|
||||
@@ -145,93 +136,137 @@ export function buildAliasLedger(): HTMLElement {
|
||||
label: "승인",
|
||||
variant: "filled",
|
||||
size: "sm",
|
||||
onClick: () => void act(c, true),
|
||||
onClick: () => void decide(w, true),
|
||||
}),
|
||||
createButton({
|
||||
label: "반려",
|
||||
variant: "ghost",
|
||||
size: "sm",
|
||||
onClick: () => void act(c, false),
|
||||
onClick: () => void decide(w, false),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
function paintCandidates(board: AliasBoard): void {
|
||||
const head = createPanelHead({
|
||||
title: "모인 후보",
|
||||
sub: `${LEDGER_HINT} · 회사 ${board.모은곳.회사} · 사람 ${board.모은곳.사람}의 장부에서 모음 · 반려 ${board.반려} · 승인한 것만 모두에게 · 누가 적었는지는 안 보임`,
|
||||
});
|
||||
body.replaceChildren(
|
||||
head.root,
|
||||
createTable(CANDIDATE_COLS, board.후보.map(candidateRow), {
|
||||
density: "data",
|
||||
empty: "모인 후보 없음",
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async function forget(b: MasterBill): Promise<void> {
|
||||
const yes = await askConfirm({
|
||||
title: "마스터 공사 내역 맞춤 지움",
|
||||
message: `「${b.원래.품명} ${b.원래.규격}」 → ${b.이름 ?? b.확정}\n지우면 모든 사용자 제안에서 빠짐.`,
|
||||
okLabel: "지움",
|
||||
danger: true,
|
||||
});
|
||||
if (!yes) return;
|
||||
try {
|
||||
const got = await call<{ 마스터내역: MasterBill[] }>("/master-bill/forget", { 열쇠: b.열쇠 });
|
||||
showToast("지움", "success");
|
||||
paintMaster(got.마스터내역);
|
||||
} catch (error) {
|
||||
showToast(`지우지 못 함 — ${why(error)}`, "error");
|
||||
}
|
||||
}
|
||||
|
||||
function paintMaster(bill: MasterBill[]): void {
|
||||
const rows = bill.map((b) => [
|
||||
b.원래.품명,
|
||||
b.원래.규격,
|
||||
b.원래.단위,
|
||||
`${b.이름 ?? ""} (${b.확정})`,
|
||||
createButton({ label: "지움", variant: "danger", size: "sm", onClick: () => void forget(b) }),
|
||||
]);
|
||||
body.replaceChildren(
|
||||
buildHopyoLedger({ scope: { layer: "system", owner: null }, canWrite: true }),
|
||||
waitHost.replaceChildren(
|
||||
createPanelHead({
|
||||
title: "마스터 공사 내역 맞춤",
|
||||
sub: "B09 내역 줄 ↔ 우리 공종(각자 기억이 먼저)",
|
||||
title: `승인 대기 ${board.대기.length}`,
|
||||
sub: "M02 에서 못 이은 줄을 사용자가 우리 요소로 고른 제안 · 승인 = 그 요소 별칭에 더함",
|
||||
}).root,
|
||||
createTable(BILL_COLS, rows, { density: "data", empty: "마스터 공사 내역 맞춤 없음" }),
|
||||
createTable(WAIT_COLS, rows, { density: "data", empty: "승인 대기 없음" }),
|
||||
);
|
||||
}
|
||||
|
||||
function aliasCell(r: AliasRow): HTMLElement {
|
||||
const box = el("div", { className: "ui-toolbar", attrs: { "data-alias-cell": r.키 } });
|
||||
const edited = edits.get(r.키);
|
||||
if (edited !== undefined) box.append(createBadge(edited || "(지움)", "warning"));
|
||||
else for (const p of r.조각) box.append(createBadge(p.글, p.승인 ? "success" : "neutral"));
|
||||
box.title = "눌러 고침 — 콤마로 여러 개";
|
||||
box.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
startEdit(box, edited ?? r.별칭, (text) => {
|
||||
if (text.trim() === r.별칭.trim()) edits.delete(r.키);
|
||||
else edits.set(r.키, text.trim());
|
||||
mark();
|
||||
paintList();
|
||||
});
|
||||
});
|
||||
return box;
|
||||
}
|
||||
|
||||
function paintList(): void {
|
||||
const words = q.toLowerCase().split(/\s+/).filter(Boolean);
|
||||
const shown = board.줄.filter(
|
||||
(r) =>
|
||||
(!kind || r.갈래 === kind) &&
|
||||
words.every((w) => `${r.키} ${r.이름} ${r.규격} ${r.별칭}`.toLowerCase().includes(w)),
|
||||
);
|
||||
const table = createTable(
|
||||
ROW_COLS,
|
||||
shown.map((r) => [
|
||||
r.갈래,
|
||||
r.키,
|
||||
`${r.이름} ${r.규격}`.trim(),
|
||||
aliasCell(r),
|
||||
createBadge(r.들어옴, r.들어옴 === "승인" ? "success" : "neutral"),
|
||||
]),
|
||||
{ density: "data", empty: "별칭이 적힌 줄 없음" },
|
||||
);
|
||||
table.querySelectorAll("tbody tr").forEach((tr, i) => {
|
||||
const r = shown[i];
|
||||
if (!r) return;
|
||||
tr.setAttribute("data-key", r.키);
|
||||
tr.addEventListener("click", () => opts.open(r));
|
||||
});
|
||||
listHost.replaceChildren(table);
|
||||
}
|
||||
|
||||
function paint(): void {
|
||||
paintWaiting();
|
||||
paintList();
|
||||
}
|
||||
|
||||
async function load(): Promise<void> {
|
||||
const stop = showLoading(body);
|
||||
const stop = showLoading(listHost);
|
||||
try {
|
||||
const got = await call<AliasBoard>("");
|
||||
if (view === "candidates") paintCandidates(got);
|
||||
else paintMaster(got.마스터내역 ?? []);
|
||||
board = await call<Board>("");
|
||||
paint();
|
||||
} catch (error) {
|
||||
body.replaceChildren(loadingError(why(error), () => void load()));
|
||||
listHost.replaceChildren(loadingError(why(error), () => void load()));
|
||||
} finally {
|
||||
stop();
|
||||
}
|
||||
}
|
||||
|
||||
const tabs = createTabs({
|
||||
tabs: [
|
||||
{ key: "candidates", label: "모인 후보" },
|
||||
{ key: "master", label: "마스터 장부" },
|
||||
],
|
||||
value: view,
|
||||
onChange: (key) => {
|
||||
view = key;
|
||||
void load();
|
||||
save.addEventListener("click", async () => {
|
||||
save.disabled = true;
|
||||
try {
|
||||
for (const [key, text] of [...edits]) {
|
||||
board = await call<Board>("/save", { 키: key, 별칭: text });
|
||||
edits.delete(key);
|
||||
}
|
||||
showToast("별칭 저장함", "success");
|
||||
} catch (error) {
|
||||
showToast(`저장 못 함 — ${why(error)}`, "error");
|
||||
} finally {
|
||||
mark();
|
||||
paint();
|
||||
}
|
||||
});
|
||||
undo.addEventListener("click", () => {
|
||||
edits.clear();
|
||||
mark();
|
||||
paintList();
|
||||
});
|
||||
|
||||
const kinds = createSegment({
|
||||
options: [{ value: "", label: "전체" }, ...KINDS.map((k) => ({ value: k, label: k }))],
|
||||
value: kind,
|
||||
onChange: (v) => {
|
||||
kind = v;
|
||||
paintList();
|
||||
},
|
||||
ariaLabel: "갈래",
|
||||
});
|
||||
const find = createInputField({
|
||||
type: "search",
|
||||
placeholder: "키 · 이름 · 별칭 찾기",
|
||||
compact: true,
|
||||
onInput: (v) => {
|
||||
q = v.trim();
|
||||
paintList();
|
||||
},
|
||||
});
|
||||
root.append(tabs.root, body); // 제목 = 본 칸 머리(ALIAS_TITLE · 왼쪽 판이 넘김)
|
||||
const listHead = createPanelHead({
|
||||
title: "별칭이 적힌 줄",
|
||||
sub: "수기로 넣은 것 · 승인한 것 모두 · 줄을 누르면 그 요소 화면 · 별칭 칸을 눌러 고친 뒤 [저장]",
|
||||
});
|
||||
root.append(
|
||||
waitHost,
|
||||
listHead.root,
|
||||
el("div", { className: "ui-toolbar", children: [kinds.root, find.root, undo, save] }),
|
||||
listHost,
|
||||
);
|
||||
void load();
|
||||
return root;
|
||||
}
|
||||
|
||||
@@ -571,7 +571,13 @@ export async function mountM01LogicLab(
|
||||
showLoadingOverlay();
|
||||
const openKeyOn = async (key: string): Promise<void> => {
|
||||
if (!(await leave())) return;
|
||||
const found = allLogics.find((x) => x.키 === key);
|
||||
// 목록에 없는 부품 문서(813-2) = 키로 읽어 엶
|
||||
const found =
|
||||
allLogics.find((x) => x.키 === key) ??
|
||||
(await fetchLogic(key).then(
|
||||
(o) => o.logic as unknown as { 구분: string; 키: string },
|
||||
() => null,
|
||||
));
|
||||
if (found) await open(logicId(found.구분, found.키), found.구분, found.키);
|
||||
else showToast(tx("Load_Failed"), "error");
|
||||
};
|
||||
|
||||
@@ -18,9 +18,7 @@
|
||||
.m01lab-src__title {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow-wrap: anywhere; /* 제목 한 줄이 길면 접음 */
|
||||
}
|
||||
|
||||
.m01lab-src__body {
|
||||
@@ -40,6 +38,11 @@
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* [주] 글 — 본문(--text-body)보다 한 단 작게 */
|
||||
.m01lab-src__note {
|
||||
font-size: var(--text-body-sm);
|
||||
}
|
||||
|
||||
.m01lab-src__file {
|
||||
margin: 0;
|
||||
font-size: var(--text-body-sm);
|
||||
@@ -56,6 +59,11 @@
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
/* 표 머리 · 칸 = 본문과 같은 16 — 좁으면 둘레 칸(m01-logic__scroll)이 가로로 굴림 */
|
||||
.m01lab-src .m01lab-src__table :is(td, th) {
|
||||
font-size: var(--text-body);
|
||||
}
|
||||
|
||||
/* 출처가 가리킨 항 · 주 — 옅은 바탕 + 왼쪽 막대 */
|
||||
.m01lab-src__hit {
|
||||
background: color-mix(in srgb, var(--color-accent) 14%, transparent);
|
||||
|
||||
@@ -38,6 +38,9 @@ interface Found {
|
||||
item?: string;
|
||||
}
|
||||
|
||||
/** 절 제목 블록의 글만 「번호 이름」 꼴로 */
|
||||
const headFixed = (b: Block): Block => (b.h ? { ...b, text: headText(b.text ?? "") } : b);
|
||||
|
||||
function blockNode(b: Block): HTMLElement {
|
||||
const node = rawNode(b);
|
||||
if (b.hit) node.classList.add("m01lab-src__hit");
|
||||
@@ -65,6 +68,27 @@ function rawNode(b: Block): HTMLElement {
|
||||
return el("p", { className: "m01lab-src__p", text: b.p ?? "" });
|
||||
}
|
||||
|
||||
/** 출처 글에서 첫 번호 앞까지 = 책 · 부문(예 「건설품셈 공통」) — 제목 한 줄의 앞 */
|
||||
const prefixOf = (src: string): string => {
|
||||
const at = src.search(/\d+(?:-\d+)+/);
|
||||
return (at < 0 ? src : src.slice(0, at)).trim();
|
||||
};
|
||||
|
||||
/** 절 제목 줄 「9-7-1. 이름」 → 「9-7-1 이름」 */
|
||||
const headText = (t: string): string => t.replace(/^(\d+(?:-\d+)*)\.\s*/, "$1 ");
|
||||
|
||||
/** 「[주]」 줄부터 다음 제목 앞까지 글 줄 = 한 단 작은 글자(`m01lab-src__note`) */
|
||||
function noteMarked(blocks: Block[]): HTMLElement[] {
|
||||
let inNote = false;
|
||||
return blocks.map((b) => {
|
||||
if (b.h) inNote = false;
|
||||
else if (b.p?.trim().startsWith("[주]")) inNote = true;
|
||||
const node = blockNode(b);
|
||||
if (inNote && b.p !== undefined) node.classList.add("m01lab-src__note");
|
||||
return node;
|
||||
});
|
||||
}
|
||||
|
||||
export interface SourceView {
|
||||
root: HTMLElement;
|
||||
/** 그 출처 글의 원문을 그림 — 지금 보이는 것과 같으면 안 부름 */
|
||||
@@ -121,20 +145,28 @@ export function createSourceView(opts: SourceViewOptions = {}): SourceView {
|
||||
.then(({ found }) => {
|
||||
if (shown !== src) return;
|
||||
if (!found.length) return none();
|
||||
// 제목 한 줄 = 「책 부문 + 절 번호 · 이름(개정 꼬리)」 — 절 하나면 원문 제목 줄을 숨김 · 여럿이면 절마다 번호 · 이름 줄 둠
|
||||
const lone = found.length === 1 && found[0].blocks[0]?.h ? found[0] : null;
|
||||
if (found.length > 1 || lone) {
|
||||
const pre = prefixOf(src);
|
||||
title.textContent = lone ? `${pre} ${headText(lone.blocks[0].text ?? "")}`.trim() : pre;
|
||||
}
|
||||
body.replaceChildren(
|
||||
...found.flatMap((f) => [
|
||||
el("p", {
|
||||
className: "ui-text ui-text--muted m01lab-src__file",
|
||||
text: [
|
||||
f.file,
|
||||
f.matched === f.number ? "" : ts("Near", f.number),
|
||||
f.item ? (/^\d+$/.test(f.item) ? `${f.item}항` : f.item) : "",
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" · "),
|
||||
}),
|
||||
...f.blocks.map(blockNode),
|
||||
]),
|
||||
...found.flatMap((f) => {
|
||||
// 파일 경로는 안 보임 — 가까운 절 · 항 안내만
|
||||
const note = [
|
||||
f.matched === f.number ? "" : ts("Near", f.number),
|
||||
f.item ? (/^\d+$/.test(f.item) ? `${f.item}항` : f.item) : "",
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" · ");
|
||||
return [
|
||||
...(note
|
||||
? [el("p", { className: "ui-text ui-text--muted m01lab-src__file", text: note })]
|
||||
: []),
|
||||
...noteMarked(f === lone ? f.blocks.slice(1) : f.blocks.map(headFixed)),
|
||||
];
|
||||
}),
|
||||
);
|
||||
// 강조 항을 패널 몸 가운데로 — 몸만 굴림(85-5 · scrollIntoView 는 메인 창까지 굴림)
|
||||
requestAnimationFrame(() => {
|
||||
|
||||
@@ -77,7 +77,7 @@ export function createSourceFind(opts: {
|
||||
unit: x.결과단위 ?? "",
|
||||
onPick: () => {
|
||||
find.setRows([]);
|
||||
find.hint.textContent = `${x.원문번호 || x.키} ${x.이름}`;
|
||||
find.hint.textContent = ""; // 고른 기초 이름은 원문 제목 한 줄이 보임
|
||||
opts.onPick(x);
|
||||
},
|
||||
})),
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* M01_MasterData_UI_Page.ts
|
||||
* 마스터 요소 화면 — 좌측 도킹 패널(컨테이너: 인력·재료·기계·소요량·계수·환율·요율·단가산출 로직)
|
||||
* · 일위대가 조합은 M02 마스터 템플릿(PLAN 27-2b) · 구조물 옵션 정의(716-1 · 화면은 M02 `StructOptions/`)
|
||||
* / 우측 요소 표 · 표 목록 · 로직 편집 · 옵션 정의 · 별칭 장부(718-3) ·
|
||||
* / 우측 요소 표 · 표 목록 · 로직 편집 · 옵션 정의 · 별칭 모아 보기(815-2) ·
|
||||
* 주소 `?open=로직|옵션|별칭` = 그 칸을 고른 채 열림(716-2)
|
||||
*
|
||||
* 시스템 관리자만. 고친 것은 초안(sessionStorage)에 쌓이고 [저장] 한 번에 서버로 —
|
||||
@@ -48,6 +48,7 @@ import {
|
||||
} from "./M01_MasterData_UI_Side";
|
||||
import { loadTable } from "./M01_MasterData_UI_Logic_Note";
|
||||
import { tl } from "./M01_MasterData_UI_LogicLab_Text";
|
||||
import { renderParts } from "./M01_MasterData_UI_Parts";
|
||||
import { renderTables } from "./M01_MasterData_UI_Tables";
|
||||
import type { LogicHandle } from "./M01_MasterData_UI_LogicLab";
|
||||
import "./M01_MasterData_UI_Style.css";
|
||||
@@ -84,6 +85,7 @@ function buildPage(): HTMLElement {
|
||||
attrs: { hidden: "" },
|
||||
});
|
||||
const body = el("div", { className: "m01-master__body" });
|
||||
const partsHost = el("div"); // 기계 · 계수 표 아래 부품 문서(813-2)
|
||||
// 제목 줄 · 찾기 줄 = 「단가산출 로직」 화면과 같은 공용 조각
|
||||
const headBar = buildHeadBar({ title, status: [summary], buttons: [drop, save] });
|
||||
const head = el("div", {
|
||||
@@ -99,6 +101,7 @@ function buildPage(): HTMLElement {
|
||||
notice,
|
||||
el("p", { className: "m01-master__hint", text: L("M01_WideHint") }),
|
||||
body,
|
||||
partsHost,
|
||||
],
|
||||
});
|
||||
|
||||
@@ -173,6 +176,7 @@ function buildPage(): HTMLElement {
|
||||
: pick.group === "요율"
|
||||
? renderRateRows(body, pick, query)
|
||||
: renderRows(body, pick, query);
|
||||
renderParts(partsHost, pick, (key) => openLogicTab(key));
|
||||
showNotice(isStale(pick.file.file, pick.file.version) ? [L("M01_FileStale")] : []);
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
/* =============================================================================
|
||||
* M01_MasterData_UI_Parts.ts
|
||||
* M01 「기계」 · 「계수」 표 아래 부품 문서(PLAN 813-2 · 요구 722-3 · 724~732) — 단가산출 기초 목록에서 뺀 보조 문서 자리
|
||||
* 기계 · 상세구분(기종) 고름 = 「시간당 기계경비 계산식」(그 기종 기계가 쓰는 방식 · 기계 수) · 「작업량 공식 틀」(그 기종 틀)
|
||||
* 계수 = 「계수식」(기계 무관 · 돈 아닌 식 넷)
|
||||
* 이름 누름 = 줄 쌓기 읽기 창(`createStackScreen` · 고침 없음) · [단가산출 기초에서 열기] = 지금 수식 탭 그대로 고침
|
||||
* 서버 = `GET /api/m01/parts?group&detail`(`M01_MasterData_Parts.of_kind`)
|
||||
* ========================================================================== */
|
||||
|
||||
import { API_BASE_URL } from "@config/config_frontend";
|
||||
import { createButton } from "@ui/ui_template_button";
|
||||
import { el } from "@ui/ui_template_elements";
|
||||
import { openModal } from "@ui/ui_template_modal";
|
||||
import { currentLanguageIndex } from "@ui/ui_template_locale";
|
||||
import { createText } from "@ui/ui_template_text";
|
||||
import { createStackScreen } from "../A00_Common/line_stack_screen";
|
||||
import type { Pick } from "./M01_MasterData_UI_Side";
|
||||
|
||||
interface Part {
|
||||
키: string;
|
||||
이름: string;
|
||||
결과단위: string;
|
||||
기계수?: number;
|
||||
}
|
||||
type Parts = Partial<Record<"기계경비식" | "작업량 틀" | "계수식", Part[]>>;
|
||||
|
||||
const T = {
|
||||
기계경비식: ["시간당 기계경비 계산식", "Hourly machine cost formula"],
|
||||
"작업량 틀": ["작업량 공식 틀", "Output formula templates"],
|
||||
계수식: ["계수식", "Coefficient formulas"],
|
||||
Open: ["단가산출 기초에서 열기", "Open in unit price basis"],
|
||||
Machines: ["기계", "machines"],
|
||||
None: ["없음", "None"],
|
||||
Close: ["닫기", "Close"],
|
||||
} as const;
|
||||
const tx = (k: keyof typeof T): string => T[k][currentLanguageIndex === 1 ? 1 : 0];
|
||||
const KINDS = ["기계경비식", "작업량 틀", "계수식"] as const;
|
||||
|
||||
/** 표 아래 부품 칸 — 기계(상세구분 있음) · 계수만 · 그 밖 = 비움 */
|
||||
export function renderParts(host: HTMLElement, pick: Pick, openLogic: (key: string) => void): void {
|
||||
host.replaceChildren();
|
||||
const group = pick.group === "계수" ? "계수식" : pick.group === "기계" ? "기계" : "";
|
||||
if (!group || (group === "기계" && !pick.detail)) return;
|
||||
let stack: ReturnType<typeof createStackScreen> | null = null;
|
||||
/** 줄 쌓기 읽기 창 — 고침 없음 · [단가산출 기초에서 열기] = 수식 탭에서 고침 */
|
||||
const show = (p: Part): void => {
|
||||
stack ??= createStackScreen({ base: "/m01/logic/doc", admin: false, cache: "m01_parts_view" });
|
||||
const view = stack;
|
||||
openModal({
|
||||
title: `${p.키} · ${p.이름}`,
|
||||
closeLabel: tx("Close"),
|
||||
width: "1100px",
|
||||
wide: true,
|
||||
mount: (body, close, actions) => {
|
||||
actions.append(
|
||||
createButton({
|
||||
label: tx("Open"),
|
||||
variant: "ghost",
|
||||
size: 28,
|
||||
onClick: () => {
|
||||
close();
|
||||
openLogic(p.키);
|
||||
},
|
||||
}),
|
||||
);
|
||||
body.append(view.root);
|
||||
view.draw(p.키, true);
|
||||
},
|
||||
});
|
||||
};
|
||||
const q = new URLSearchParams({ group, detail: pick.detail ?? "" });
|
||||
const mine = (host.dataset.partsAsk = q.toString()); // 늦게 온 옛 답은 버림
|
||||
void fetch(`${API_BASE_URL}/m01/parts?${q}`, { credentials: "include" })
|
||||
.then((r): Promise<Parts> | Parts => (r.ok ? (r.json() as Promise<Parts>) : {}))
|
||||
.catch((): Parts => ({}))
|
||||
.then((got) => {
|
||||
if (host.dataset.partsAsk !== mine) return;
|
||||
const blocks = KINDS.filter((k) => k in got).map((k) => {
|
||||
const list = got[k] ?? [];
|
||||
const items = list.map((p) => {
|
||||
const b = createButton({
|
||||
label: `${p.이름}${p.기계수 ? ` · ${tx("Machines")} ${p.기계수}` : ""}`,
|
||||
variant: "ghost",
|
||||
tone: "link",
|
||||
size: 28,
|
||||
onClick: () => show(p),
|
||||
});
|
||||
b.dataset.part = p.키;
|
||||
return b;
|
||||
});
|
||||
return el("section", {
|
||||
className: "m01-parts__block",
|
||||
attrs: { "data-parts": k },
|
||||
children: [
|
||||
el("h3", { className: "m01-parts__title", text: tx(k) }),
|
||||
items.length
|
||||
? el("div", { className: "m01-parts__list", children: items })
|
||||
: createText(tx("None")),
|
||||
],
|
||||
});
|
||||
});
|
||||
if (blocks.length)
|
||||
host.replaceChildren(el("div", { className: "m01-parts", children: blocks }));
|
||||
});
|
||||
}
|
||||
@@ -37,6 +37,9 @@ const MARKET = "재료_자재품목.json";
|
||||
const LINKED = "재료_품셈재료.json";
|
||||
const JOB = ["인력.json"];
|
||||
const NOTE = "비고"; // 끝 열(인력은 준용 앞) · 줄에 없으면 빈 칸 · 눌러 적음
|
||||
// 별칭(815-2 · 요구 764) = 비고 앞 한 열 · 콤마로 여러 개 · 빈 글이면 칸을 지움 · 이 넷만
|
||||
const ALIAS = "별칭";
|
||||
const ALIASED = ["인력.json", "기계.json", MARKET, "재료_유가전력.json"];
|
||||
const FALLBACK: Row = { 키: "", 원문번호: "", 이름: "", 규격: "", 단위: "", 값: null, 출처: "" };
|
||||
|
||||
/** 새 줄 바탕 — 본 줄과 같은 칸 · 글 칸은 "" · 수·값 칸은 null. */
|
||||
@@ -44,16 +47,16 @@ function blank(sample: Row): Row {
|
||||
const out: Row = {};
|
||||
for (const [k, v] of Object.entries(sample)) {
|
||||
if (v && typeof v === "object" && !Array.isArray(v)) out[k] = blank(v as Row);
|
||||
else out[k] = typeof v === "string" && k !== "값" ? "" : null;
|
||||
else if (k !== ALIAS) out[k] = typeof v === "string" && k !== "값" ? "" : null;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 비고 칸 고침 — 빈 글이면 칸을 지움 */
|
||||
function withNote(row: Row, text: string): Row {
|
||||
/** 비고 · 별칭 칸 고침 — 빈 글이면 칸을 지움(별칭은 앞뒤 빈칸 뗌) */
|
||||
function withNote(row: Row, text: string, col = NOTE): Row {
|
||||
const out = { ...row };
|
||||
if (text.trim()) out[NOTE] = text;
|
||||
else delete out[NOTE];
|
||||
if (text.trim()) out[col] = col === ALIAS ? text.trim() : text;
|
||||
else delete out[col];
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -117,8 +120,9 @@ export function renderRows(
|
||||
const cols: string[] = [];
|
||||
for (const row of [...adds, ...d.rows]) {
|
||||
for (const k of Object.keys(flatten(row)))
|
||||
if (!hidden(file, k) && k !== NOTE && !cols.includes(k)) cols.push(k);
|
||||
if (!hidden(file, k) && k !== NOTE && k !== ALIAS && !cols.includes(k)) cols.push(k);
|
||||
}
|
||||
const aliased = ALIASED.includes(file);
|
||||
const heads = (HEADS[file] ?? []).map((h) =>
|
||||
el("th", { text: h.startsWith("M01_") ? L(h as "M01_PriceCol") : h }),
|
||||
);
|
||||
@@ -127,6 +131,7 @@ export function renderRows(
|
||||
el("th"),
|
||||
...cols.map((c) => el("th", { text: c })),
|
||||
...(JOB.includes(file) ? [] : heads),
|
||||
...(aliased ? [el("th", { text: ALIAS })] : []),
|
||||
el("th", { text: NOTE }),
|
||||
...(JOB.includes(file) ? heads : []),
|
||||
],
|
||||
@@ -145,12 +150,13 @@ export function renderRows(
|
||||
}),
|
||||
);
|
||||
}
|
||||
tr.append(
|
||||
buildCell(show(row[NOTE]), {
|
||||
changed: true,
|
||||
onEdit: (text) => setAdd(file, i, withNote(row, text)),
|
||||
}),
|
||||
);
|
||||
for (const col of aliased ? [ALIAS, NOTE] : [NOTE])
|
||||
tr.append(
|
||||
buildCell(show(row[col]), {
|
||||
changed: true,
|
||||
onEdit: (text) => setAdd(file, i, withNote(row, text, col)),
|
||||
}),
|
||||
);
|
||||
if (JOB.includes(file)) tr.append(el("td"));
|
||||
body.append(tr);
|
||||
});
|
||||
@@ -186,15 +192,16 @@ export function renderRows(
|
||||
? undefined
|
||||
: (next: Row): void => editRow(file, d.version, key, row, next);
|
||||
if (file === LINKED) tr.append(linkedCell(row, cur, put));
|
||||
tr.append(
|
||||
buildCell(show(cur[NOTE]), {
|
||||
changed: !same(cur[NOTE], row[NOTE]),
|
||||
onEdit:
|
||||
deleted || !isScalar(row[NOTE])
|
||||
? undefined
|
||||
: (text) => editRow(file, d.version, key, row, withNote(cur, text)),
|
||||
}),
|
||||
);
|
||||
for (const col of aliased ? [ALIAS, NOTE] : [NOTE])
|
||||
tr.append(
|
||||
buildCell(show(cur[col]), {
|
||||
changed: !same(cur[col], row[col]),
|
||||
onEdit:
|
||||
deleted || !isScalar(row[col])
|
||||
? undefined
|
||||
: (text) => editRow(file, d.version, key, row, withNote(cur, text, col)),
|
||||
}),
|
||||
);
|
||||
if (JOB.includes(file)) tr.append(wageCell(row, cur, put));
|
||||
body.append(tr);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* 왼쪽 패널 — 공용 접기 컨테이너(`ui-collapsible ui-sidebar-section`)
|
||||
* 인력 · 재료 · 기계 · 소요량 · 계수 · 환율 · 요율 · 단가산출 로직(일위대가 조합은 M02 · PLAN 27-2b)
|
||||
* ─ 구분선 ─ 구조물 옵션 정의(716-1 · 화면 코드는 M02 `StructOptions/` 그대로 · 입구만 여기)
|
||||
* · 별칭 장부(718-3 · 누르면 본 칸에 모인 후보 · 마스터 장부 · `M01_MasterData_UI_Alias`)
|
||||
* · 별칭 모아 보기(815-2 · 누르면 본 칸에 별칭이 적힌 줄 · 승인 대기 · `M01_MasterData_UI_Alias`)
|
||||
* 펼치면 하위 목록(거름 · 파일) · 환율/요율은 누르면 바로 표 · 로직은 안에 로직 목록
|
||||
* ========================================================================== */
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
buildOptionsSection,
|
||||
type OptionsSectionOptions,
|
||||
} from "../M02_MasterTemplete/StructOptions/M02_SO_Options";
|
||||
import { ALIAS_TITLE, buildAliasLedger } from "./M01_MasterData_UI_Alias";
|
||||
import { ALIAS_TITLE, buildAliasBoard, type AliasRow } from "./M01_MasterData_UI_Alias";
|
||||
import { fetchFiles, fetchSubsAll, type FileInfo, type SubInfo } from "./M01_MasterData_Api_Fetch";
|
||||
import { tx } from "./M01_MasterData_UI_Logic_Text";
|
||||
import { renderTree, type MakeRow } from "./M01_MasterData_UI_Tree";
|
||||
@@ -504,13 +504,24 @@ ${first.file}`
|
||||
// 구조물 옵션 정의 — 로직 칸 아래 구분선 + 칸 하나(시스템 관리자 화면이라 고침 가능)
|
||||
const options = buildOptionsSection({ ...optionsOpt, isAdmin: true });
|
||||
const optionsSec = section(options.title, options.body);
|
||||
// 별칭 장부(718-3) — 옵션 칸 아래 칸 하나 · 누르면 본 칸(옵션 정의와 같은 자리)
|
||||
// 별칭 모아 보기(815-2) — 옵션 칸 아래 칸 하나 · 누르면 본 칸(옵션 정의와 같은 자리)
|
||||
const aliasSec = section(ALIAS_TITLE, null);
|
||||
aliasSec.classList.add("m01-side__leaf");
|
||||
buttons.set(ALIAS_ID, aliasSec);
|
||||
/** 모아 보기 줄 → 그 요소 화면 그 줄 · 단가산출 기초는 로직 탭 */
|
||||
const openAliasRow = (row: AliasRow): void => {
|
||||
if (row.갈래 === "단가산출 기초") {
|
||||
setPendingLogicKey(row.키);
|
||||
return reveal("로직");
|
||||
}
|
||||
const group = row.갈래 === "유가" ? FUEL_GROUP : row.갈래;
|
||||
jumpToMaster({ kind: "element", group, ref: row.키, file: row.파일 });
|
||||
};
|
||||
const openAlias = async (): Promise<void> => {
|
||||
if (!(await optionsOpt.confirmLeave())) return;
|
||||
optionsOpt.show(ALIAS_TITLE, buildAliasLedger());
|
||||
let setDirty: (dirty: boolean) => void = () => {};
|
||||
const board = buildAliasBoard({ open: openAliasRow, setDirty: (d) => setDirty(d) });
|
||||
setDirty = optionsOpt.show(ALIAS_TITLE, board);
|
||||
options.clear();
|
||||
setActive(ALIAS_ID);
|
||||
};
|
||||
|
||||
@@ -170,3 +170,21 @@ table.m01-master__grid {
|
||||
gap: var(--spacing-8);
|
||||
padding: var(--spacing-4) var(--spacing-8);
|
||||
}
|
||||
|
||||
/* 부품 문서(813-2) — 기계 · 계수 표 아래 */
|
||||
.m01-parts {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-12);
|
||||
margin-top: var(--spacing-16);
|
||||
}
|
||||
.m01-parts__title {
|
||||
margin: 0 0 var(--spacing-4);
|
||||
font-size: var(--text-body);
|
||||
color: var(--color-text);
|
||||
}
|
||||
.m01-parts__list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--spacing-4);
|
||||
}
|
||||
|
||||
@@ -7,9 +7,8 @@
|
||||
STmate = 원문 그대로 `M02_San_Emit.stmate`(원파일 원문 = 엑셀 숨은 G 열 · 단가 = COSTN ·
|
||||
끝전 = 옵션 SAN_TYP 1 비목버림 / 0 합버림_큰나머지 · 810-1 저장값 대조로 확인).
|
||||
블록 번 = 엑셀 길과 같음(EST = 산근 표 차례 1부터 = 「단산N」 · STmate = 코드 숫자).
|
||||
같은 공사 = 엑셀 산근 블록이 모두 원파일 블록과 금액 · 이름이 같음(`same`) — 둘 다 있으면 원파일.
|
||||
일위대가(EST HeadMainDat) → 실무자료 호표 꼴(`hopyo`) · STmate 일위대가 구성은 못 읽음(BDQTY 난독화) →
|
||||
호표 없음(엑셀을 같이 올리면 엑셀 호표 + 원파일 산근).
|
||||
올리기 = 파일 하나(814-1 · 요구 742-1 — 같은 공사 엑셀 붙이기 걷음) · 원파일 = 단가산출표 판(`grid` ·
|
||||
산출근거 머리) + 산출근거 · 일위대가 안 만듦(요구 743-2 · `hopyo` = 단산만).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -50,19 +49,30 @@ def read(name: str, data: bytes) -> dict[str, Any]:
|
||||
def pack(got: dict, name: str) -> dict[str, Any]:
|
||||
"""파서 답 → 둘 꼴(셈 · 변환에 쓰는 칸만 · JSON)."""
|
||||
out: dict[str, Any] = {"꼴": got["꼴"], "파일": name, "표지": got.get("표지") or []}
|
||||
head = ("코드", "이름", "규격", "단위", "저장")
|
||||
out |= {"엑셀": True} if got.get("엑셀") else {} # 812-1 엑셀 읽기기(`M02_San_Xlsx`) — 같은 꼴
|
||||
head = ("코드", "번", "이름", "규격", "단위", "저장")
|
||||
if got.get(
|
||||
"엑셀"
|
||||
): # 엑셀 읽기기 — 줄 행 = 시트 행(원문 카드 · 옛 엑셀 길과 같은 자리) · 블록 표지
|
||||
head += ("표지", "몸", "꼬리")
|
||||
got = {**got, "산근": [_rows(b) for b in got["산근"]]}
|
||||
if got["꼴"] == "EST":
|
||||
out["산근"] = [
|
||||
{**{k: b.get(k) for k in head}, "줄": [_est_line(ln) for ln in b["줄"]]}
|
||||
{**{k: b.get(k) for k in head if k in b}, "줄": [_est_line(ln) for ln in b["줄"]]}
|
||||
for b in got["산근"]
|
||||
]
|
||||
out["일위"] = [{**{k: b.get(k) for k in head}, "줄": b["줄"]} for b in got["일위"]]
|
||||
out["일위"] = [
|
||||
{**{k: b.get(k) for k in head if k in b}, "줄": b["줄"]} for b in got["일위"]
|
||||
]
|
||||
return out
|
||||
out["산근"] = [
|
||||
{**{k: b.get(k) for k in head}, "줄": [{"행": ln["행"], "글": ln["글"]} for ln in b["줄"]]}
|
||||
{
|
||||
**{k: b.get(k) for k in head if k in b},
|
||||
"줄": [{"행": ln["행"], "글": ln["글"]} for ln in b["줄"]],
|
||||
}
|
||||
for b in got["산근"]
|
||||
]
|
||||
out["일위"] = [{k: b.get(k) for k in head} for b in got["일위"]]
|
||||
out["일위"] = [{k: b.get(k) for k in head if k in b} for b in got["일위"]]
|
||||
res = got.get("자원") or {}
|
||||
out["단가"] = {
|
||||
code: {**p, **{k: (res.get(code) or {}).get(k) for k in ("이름", "규격", "단위")}}
|
||||
@@ -72,18 +82,24 @@ def pack(got: dict, name: str) -> dict[str, Any]:
|
||||
return out
|
||||
|
||||
|
||||
def _rows(b: dict) -> dict:
|
||||
return {**b, "줄": [{**ln, "행": ln.get("엑셀행") or ln["행"]} for ln in b["줄"]]}
|
||||
|
||||
|
||||
def _est_line(ln: dict) -> dict:
|
||||
one = {k: ln.get(k) for k in _EST_KEYS if ln.get(k) not in (None, [], False, "")}
|
||||
if one.get("자원"):
|
||||
one["자원"] = {k: ln["자원"].get(k) for k in _RES_KEYS}
|
||||
if ln["자원"].get("같은구성"): # 816-2 같은 파일에서 구성이 같은 단독 기계
|
||||
one["자원"]["같은구성"] = ln["자원"]["같은구성"]
|
||||
return one
|
||||
|
||||
|
||||
# ── 블록 ──────────────────────────────────────────────────────────────
|
||||
def numbers(saved: dict) -> dict[int, dict]:
|
||||
"""블록 번 → 산근 블록(엑셀 길 번과 같음)."""
|
||||
"""블록 번 → 산근 블록(엑셀 길 번과 같음 · 엑셀 읽기기 EST = 시트 「N.」 번)."""
|
||||
if saved["꼴"] == "EST":
|
||||
return {i: b for i, b in enumerate(saved["산근"], 1)}
|
||||
return {int(b.get("번") or i): b for i, b in enumerate(saved["산근"], 1)}
|
||||
return {int(str(b["코드"])[1:]): b for b in saved["산근"] if str(b["코드"])[1:].isdigit()}
|
||||
|
||||
|
||||
@@ -91,21 +107,35 @@ def emit(saved: dict, no: int, keys: bool = True) -> dict[str, Any]:
|
||||
"""원파일 산근 블록 하나 → `{로직, 원본}`(원본 `원파일` True · 코드 · 확정 수) · 없는 번 = KeyError."""
|
||||
blk = numbers(saved)[int(no)]
|
||||
if saved["꼴"] == "EST":
|
||||
names = {b["코드"]: i for i, b in enumerate(saved["산근"], 1)}
|
||||
names = {b["코드"]: n for n, b in numbers(saved).items()}
|
||||
out = ne.block(blk, int(no), keys, saved["파일"], names)
|
||||
out["원본"] |= {"원코드": blk["코드"]} | ({"규격": blk["규격"]} if blk.get("규격") else {})
|
||||
return out
|
||||
return _excel(saved, out, blk)
|
||||
from M02_MasterTemplete import M02_San_Stmate as stm
|
||||
|
||||
nodes = [{"행": ln["행"], "글": ln["글"], **stm.parse_line(ln["글"])} for ln in blk["줄"]]
|
||||
saved_amt = blk.get("저장") or {}
|
||||
head = {"C": saved_amt.get("T"), "D": saved_amt.get("L"), "E": saved_amt.get("M")}
|
||||
head["F"] = saved_amt.get("S")
|
||||
block = {"코드": blk["코드"], "이름": blk.get("이름"), "몸": [], "꼬리": [], "머리": head}
|
||||
block = {"코드": blk["코드"], "이름": blk.get("이름"), "머리": head}
|
||||
block |= {"몸": blk.get("몸") or [], "꼬리": blk.get("꼬리") or []} # 엑셀 = 줄 금액 · 계 꼬리
|
||||
out = em.stmate(block, nodes, saved["단가"], saved["끝전"], keys, saved["파일"])
|
||||
marks = sum(1 for one in out["원본"]["글줄"] if one.get("표지"))
|
||||
sure = len(out["로직"]["호표"]) + len(out["로직"].get("덧줄") or []) + len(out["로직"]["중간"])
|
||||
out["원본"] |= {"원파일": True, "갈래": "STmate", "확정": {"확정": sure, "표지": marks}}
|
||||
out["원본"] |= (
|
||||
{"규격": blk["규격"]} if blk.get("규격") else {}
|
||||
) # 연결 찾기 근거 · 이름 꼬리(814-1)
|
||||
return _excel(saved, out, blk)
|
||||
|
||||
|
||||
def _excel(saved: dict, out: dict, blk: dict) -> dict[str, Any]:
|
||||
"""엑셀 읽기기 꼴(812-1) — 원본 `원파일` 거짓 · `엑셀` 참(화면 꼬리표 「엑셀」 · 원코드 = 단산 번)."""
|
||||
if saved.get("엑셀"):
|
||||
out["원본"] |= {"원파일": False, "엑셀": True}
|
||||
out["원본"] |= {"시트": "단가산출"} if saved["꼴"] == "EST" else {}
|
||||
out["원본"].pop("원코드", None)
|
||||
out["원본"]["표지"] += [m for m in blk.get("표지") or [] if m.startswith("참고 · ")]
|
||||
return out
|
||||
|
||||
|
||||
@@ -128,89 +158,52 @@ def inner(saved: dict, logic: dict) -> list[tuple[dict, int | None, str | None]]
|
||||
return out
|
||||
|
||||
|
||||
# ── 같은 공사 · 일위대가 ───────────────────────────────────────────────
|
||||
def same(saved: dict, sheets: dict) -> bool:
|
||||
"""엑셀 산근 블록이 모두 원파일 같은 번 블록과 계 금액(1 원 안)이 같음 = 같은 공사."""
|
||||
from M02_MasterTemplete import M02_San_Book as book
|
||||
|
||||
have = numbers(saved)
|
||||
kind = book.kind_of(sheets)
|
||||
if not kind or (kind == "STmate") != (saved["꼴"] == "STmate"):
|
||||
return False
|
||||
if kind == "STmate":
|
||||
excel = {int(b["코드"][1:]): b["머리"].get("C") for b in book.stmate_blocks(sheets)}
|
||||
else:
|
||||
excel = {int(n): t.get("T") for n, t in book.est_prices(sheets)[1].items()}
|
||||
if not excel:
|
||||
return False
|
||||
for n, total in excel.items():
|
||||
blk = have.get(int(n))
|
||||
if blk is None or abs(float(total or 0) - float((blk.get("저장") or {}).get("T") or 0)) > 1:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
GALLAE = {6: "노임", 5: "자재", 105: "기계", 104: "단가산출", 103: "호표", 21: "기타", 101: "기타"}
|
||||
# ── 단가산출표 판(814-1 · 요구 742-2 · 743-2) ───────────────────────────
|
||||
LIST = "단가산출표"
|
||||
_LIST_HEAD = ("코드", "이름", "규격", "단위", "재료비", "노무비", "경비", "합계")
|
||||
_LIST_COST = ("M", "L", "S", "T")
|
||||
|
||||
|
||||
def hopyo(saved: dict) -> dict[str, Any]:
|
||||
"""EST 일위대가(HeadMainDat) → 실무자료 읽기 꼴(`M02_Hopyo_Excel.read_book` 짝 · 호표 · 단산) ·
|
||||
STmate = 호표 없음(구성 못 읽음)."""
|
||||
blocks = numbers(saved)
|
||||
names = {str(b.get("코드")): n for n, b in blocks.items()}
|
||||
heads = {str(h.get("코드")): n for n, h in enumerate(saved.get("일위") or [], 1)}
|
||||
"""원파일 → 실무자료 읽기 꼴 — 단산(산출근거 머리)만 · 일위대가 안 만듦(요구 743-2 · HeadMainDat 안 씀)."""
|
||||
out: dict[str, Any] = {"꼴": "원파일", "시트": saved["꼴"], "호표": [], "검산": {"어긋남": []}}
|
||||
out["단산"] = [
|
||||
{"번": n, "이름": b.get("이름"), "단위": b.get("단위"), "금액": _cost(b.get("저장"))}
|
||||
for n, b in blocks.items()
|
||||
for n, b in numbers(saved).items()
|
||||
]
|
||||
if saved["꼴"] != "EST":
|
||||
return out
|
||||
for n, h in enumerate(saved.get("일위") or [], 1):
|
||||
lines = []
|
||||
for k, ln in enumerate(h.get("줄") or []):
|
||||
link = ln.get("연결")
|
||||
code = str((ln.get("자원") or {}).get("코드") or "")
|
||||
ref = None
|
||||
if link == 104 and code in names:
|
||||
ref = {"종류": "단산", "번": names[code]}
|
||||
elif link == 103 and code in heads:
|
||||
ref = {"종류": "일위", "번": heads[code]}
|
||||
kind = "%" if ln.get("연결") in (52, 58, 62) else GALLAE.get(link or 0, "기타")
|
||||
if kind == "자재" and str(ln.get("단위") or "") == "인":
|
||||
kind = "노임"
|
||||
if not any((ln.get("단가") or {}).values()) and kind not in ("%",):
|
||||
kind = "별도계상" if not ref else kind
|
||||
lines.append(
|
||||
{
|
||||
"차례": k,
|
||||
"이름": ln.get("품명") or "",
|
||||
"규격": ln.get("규격") or "",
|
||||
"수량": _dec(ln.get("수량")),
|
||||
"단위": ln.get("단위") or "",
|
||||
"단가": _cost(ln.get("단가")),
|
||||
"금액": _cost(ln.get("저장")),
|
||||
"비고": "",
|
||||
"참조": ref,
|
||||
"갈래": kind,
|
||||
"행": ln.get("행"),
|
||||
"코드": code or None,
|
||||
}
|
||||
)
|
||||
out["호표"].append(
|
||||
{
|
||||
"번": n,
|
||||
"이름": h.get("이름"),
|
||||
"규격": h.get("규격") or "",
|
||||
"단위": h.get("단위") or "",
|
||||
"금액": _cost(h.get("저장")),
|
||||
"행": [n, n],
|
||||
"줄": lines,
|
||||
}
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
def grid(saved: dict) -> dict[str, Any]:
|
||||
"""원파일 → 단가산출표 한 시트 격자(`M02_Hopyo_Grid.read` 꼴) — 산출근거 머리(코드 · 이름 · 규격 ·
|
||||
단위 · 저장 금액) 한 줄씩 · 행 = 1 머리 + 블록 차례."""
|
||||
cells: dict[str, dict] = {}
|
||||
cols = "ABCDEFGH"
|
||||
for c, head in zip(cols, _LIST_HEAD):
|
||||
cells[f"{c}1"] = {"값": head}
|
||||
rows = []
|
||||
for at, (n, b) in enumerate(numbers(saved).items(), 2):
|
||||
cost = b.get("저장") or {}
|
||||
vals = [b.get("코드"), b.get("이름"), b.get("규격"), b.get("단위")]
|
||||
vals += [_dec(cost.get(k)) for k in _LIST_COST]
|
||||
for c, v in zip(cols, vals):
|
||||
if v not in (None, ""):
|
||||
cells[f"{c}{at}"] = {"값": v if isinstance(v, (int, float)) else str(v)}
|
||||
rows.append({"번": n, "이름": b.get("이름"), "행": [at, at]})
|
||||
sheet = {"id": "s1", "이름": LIST, "칸": cells, "틀고정": {"행": 1}}
|
||||
sheet["열"] = {c: {"폭": w} for c, w in zip(cols, (10, 40, 18, 6, 12, 12, 12, 12))}
|
||||
book = {"종류": "통합문서", "판": 1, "열": "", "서식": [{}], "시트": [sheet], "활성": "s1"}
|
||||
return {
|
||||
"책": book,
|
||||
"호표": [],
|
||||
"단산": rows,
|
||||
"보기": {LIST: "s1"},
|
||||
"원본": {LIST: LIST},
|
||||
"목록": {"호표": [], "단산": [{"번": r["번"], "행": r["행"][0]} for r in rows]},
|
||||
"줄": [],
|
||||
}
|
||||
|
||||
|
||||
def _dec(v: Any) -> int | float | None:
|
||||
"""수 → 엑셀 읽기와 같은 꼴(정수 = int · 아니면 float · 없음 = None)."""
|
||||
if v is None:
|
||||
|
||||
@@ -193,11 +193,10 @@ def hourly_link(eq: str, part: str) -> dict | None:
|
||||
if part not in imp.COSTS or not imp.hourly_of(eq).get("ok"):
|
||||
return None
|
||||
code = str(imp._whole().get(eq).get("원문번호") or "")
|
||||
fit = [r for r in machine._family() if code in [str(c) for c in machine._codes(r)]]
|
||||
if not fit:
|
||||
logic = machine.logic_of(code)
|
||||
if not logic:
|
||||
return None
|
||||
logic = min(fit, key=machine._rank)
|
||||
given = {"기계": _lit(code)}
|
||||
given = {"기계": _lit(code)} if machine.takes_machine(logic) else {}
|
||||
for spec in logic.get("입력") or []:
|
||||
if spec.get("이름") != "기계":
|
||||
given[spec["이름"]] = _lit((spec.get("고르기") or [""])[0])
|
||||
@@ -226,6 +225,8 @@ def _link_of(item: dict) -> tuple[dict | None, str]:
|
||||
}
|
||||
return link | ({"칸": item["참조비목"]} if item.get("참조비목") else {}), ""
|
||||
key = str(item.get("이음") or "")
|
||||
if not key and (item.get("자원") or {}).get("조합식"):
|
||||
return None, f"우리 조합 식 {item['자원']['조합식']} 에 듦(첫 줄) — 이 줄 값 0"
|
||||
if not key:
|
||||
return None, ""
|
||||
if item.get("종류") == "로직":
|
||||
@@ -233,12 +234,74 @@ def _link_of(item: dict) -> tuple[dict | None, str]:
|
||||
if key.startswith("EQ"):
|
||||
got = hourly_link(key, str(item.get("비목") or ""))
|
||||
return got, "" if got else f"기계 {key} 시간당 경비 자료 없음 — 엑셀 단가 그대로"
|
||||
if key.startswith("MO") and (got := _oil(key)):
|
||||
return got, ""
|
||||
link = {"갈래": "요소", "키": key}
|
||||
if key[:2] == dc.cv.mm.LINKED: # 품셈재료 = 줄 단위로 값이 갈림(M01 변환과 같음)
|
||||
link["단위"] = str(item.get("단위") or "")
|
||||
return link, ""
|
||||
|
||||
|
||||
OPTION = "_옵션" # 연결이 부르는 옵션 변수(문서에 없으면 `_attach` 가 더함)
|
||||
|
||||
|
||||
def _oil(key: str) -> dict | None:
|
||||
"""유가 · 전력 이음(814-1 · 요구 748) — 우리 기초 꼴: 유가 = `MO:경유:{S_지역}` + 지역 옵션(기본
|
||||
전국평균) · 전력 = 전력량요금 계약종별 고르기 + 계약종별 옵션(기본 일반용(갑)Ⅰ 저압)."""
|
||||
whole = imp._whole()
|
||||
try:
|
||||
row = whole.get(key)
|
||||
except Exception: # noqa: BLE001 — 없는 키 = 보통 요소 연결
|
||||
return None
|
||||
rows = list(whole.index.get("MO", {}).values())
|
||||
if row.get("구분") == "전력":
|
||||
kinds = list(dict.fromkeys(str(r.get("상세구분")) for r in rows if r.get("구분") == "전력" and r.get("이름") == row.get("이름"))) # fmt: skip
|
||||
var = _choice("전력 계약종별", "전력계약종별", kinds, str(row.get("상세구분")))
|
||||
link = {
|
||||
"갈래": "요소",
|
||||
"재료": {"이름": row.get("이름"), "계약종별": "입력"},
|
||||
"단위": "㎾h",
|
||||
}
|
||||
return link | {"계약종별": dc.md.var_id(var), OPTION: var}
|
||||
head = str(row.get("원문번호") or "").split(":")[0]
|
||||
if not head or row.get("구분") != "유류":
|
||||
return None
|
||||
places = [
|
||||
str(r.get("상세구분")) for r in rows if str(r.get("원문번호") or "").split(":")[0] == head
|
||||
]
|
||||
places.sort(key=lambda x: x != "전국평균")
|
||||
var = _choice("지역", "지역", places, str(row.get("상세구분")))
|
||||
return {"갈래": "요소", "키": f"MO:{head}:{{{dc.md.var_id(var)}}}", OPTION: var}
|
||||
|
||||
|
||||
def _choice(key: str, below: str, items: list[str], value: str) -> dict:
|
||||
link = {"갈래": "선택", "목록": items, "값": value}
|
||||
return {
|
||||
"열쇠": key,
|
||||
"기호": "S",
|
||||
"아래": below,
|
||||
"위": "",
|
||||
"이름": key,
|
||||
"단위": "",
|
||||
"연결": link,
|
||||
}
|
||||
|
||||
|
||||
MACHINES = "_기계" # 문서 셈 동안 잠깐 — `derive` 가 머리 `가져온.기계` 로 옮김
|
||||
|
||||
|
||||
def _machine_entry(item: dict, sym: str) -> dict:
|
||||
"""기계 줄 찾기 판정 한 칸 — {기호, 원래 {이름, 규격, 단위}, 판정, 키, 후보, 찾기어, 까닭, 조합?}."""
|
||||
res = item["자원"]
|
||||
got = res["찾기"]
|
||||
one = {
|
||||
"기호": sym,
|
||||
"원래": {"이름": got["이름"], "규격": got["규격"], "단위": item.get("단위")},
|
||||
}
|
||||
one |= {k: got.get(k) for k in ("판정", "키", "후보", "찾기어", "까닭")}
|
||||
return one | {k: True for k in ("조합", "조합첫") if res.get(k)}
|
||||
|
||||
|
||||
def _excel(item: dict) -> Any:
|
||||
if item.get("값묶음"):
|
||||
return {k: Decimal(str(item["값묶음"].get(k) or 0)) for k in imp.COSTS}
|
||||
@@ -246,12 +309,16 @@ def _excel(item: dict) -> Any:
|
||||
|
||||
|
||||
def _attach(doc: dict, row: dict, ids: list[str], src: dict[str, dict]) -> None:
|
||||
"""금액 줄 단가 숫자(엑셀 값) → 이은 연결 + 엑셀 값 기억(SK 고침으로 바뀐 값 = 숫자 그대로)."""
|
||||
"""금액 줄 단가 숫자(엑셀 값) → 이은 연결 + 엑셀 값 기억(SK 고침으로 바뀐 값 = 숫자 그대로) ·
|
||||
기계 찾기 판정(812-9) = 문서 `가져온.기계`(단가 기호마다 · 화면 고르기 · 저장 때 별칭)."""
|
||||
by_id = {dc.md.var_id(v): v for v in doc.get("변수") or []}
|
||||
found: list[dict] = doc.setdefault(MACHINES, [])
|
||||
for at, line in enumerate((doc.get("금액") or [])[: len(row["호표"])]):
|
||||
item = src.get(ids[at]) if at < len(ids) else None
|
||||
price = str(line.get("단가") or "")
|
||||
var = by_id.get(price.removesuffix(".계"))
|
||||
if item is not None and var is not None and (item.get("자원") or {}).get("찾기"):
|
||||
found.append(_machine_entry(item, dc.md.var_id(var)))
|
||||
if item is None or var is None or (var.get("연결") or {}).get("갈래") != "숫자":
|
||||
continue
|
||||
excel = _excel(item)
|
||||
@@ -261,7 +328,18 @@ def _attach(doc: dict, row: dict, ids: list[str], src: dict[str, dict]) -> None:
|
||||
if note:
|
||||
var["설명"] = note
|
||||
if link is None:
|
||||
if not note and not item.get("이음"): # 못 이은 까닭(814-1 — 흠 아님 · 우리 정보 없음)
|
||||
from M02_MasterTemplete import M02_San_Link as san_link
|
||||
|
||||
why = san_link.why(item)
|
||||
var["설명"] = (
|
||||
why if why == san_link.QUOTE else f"이음 없음 — {why} · 엑셀 단가 그대로"
|
||||
)
|
||||
continue
|
||||
if (opt := link.pop(OPTION, None)) and opt["열쇠"] not in {
|
||||
v.get("열쇠") for v in doc["변수"]
|
||||
}:
|
||||
doc["변수"].append(opt) # 유가 지역 · 전력 계약종별 옵션(문서에 하나)
|
||||
if item.get("참조비목") and isinstance(excel, dict):
|
||||
excel = excel[item["참조비목"]]
|
||||
line["단가"], line["비목"] = dc.md.var_id(var), item["참조비목"]
|
||||
@@ -304,11 +382,16 @@ def derive(sk: dict, ctx: tuple | None = None, whole: Any = None) -> dict:
|
||||
doc, notes = dc.cv.convert(_Row(master, key, row), row, given)
|
||||
_attach(doc, row, ids, src)
|
||||
_numbers(doc, keep)
|
||||
if (meta.get("원본") or {}).get("원파일"): # 원파일(810-2) — 소계 비목 칸 비움 · 값 변수 출처
|
||||
_native(doc)
|
||||
native = bool((meta.get("원본") or {}).get("원파일"))
|
||||
if native or (meta.get("원본") or {}).get(
|
||||
"엑셀"
|
||||
): # 810-2 · 812-1 — 소계 비목 칸 비움 · 값 변수 출처
|
||||
_native(doc, native)
|
||||
head = {k: (meta.get("원본") or {}).get(k) for k in HEAD if (meta.get("원본") or {}).get(k)}
|
||||
marks = list((meta.get("원본") or {}).get("표지") or [])
|
||||
if (meta.get("원본") or {}).get("원파일"): # 810-3 — 품셈 조건 글(값 없음) = 화면 「추정」 칸
|
||||
if native or (meta.get("원본") or {}).get(
|
||||
"엑셀"
|
||||
): # 810-3 — 품셈 조건 글(값 없음) = 「추정」 칸
|
||||
head["추정"] = [
|
||||
{"행": x.get("행"), "글": " ".join(str(x.get("글") or "").split())}
|
||||
for x in meta["원본"].get("글줄") or []
|
||||
@@ -317,18 +400,24 @@ def derive(sk: dict, ctx: tuple | None = None, whole: Any = None) -> dict:
|
||||
if not (doc.get("금액") or doc.get("결과") or row["호표"] or row.get("덧줄")):
|
||||
_empty(doc) # 빈 산근 = 엑셀 0 원(줄 없음 합) — 「—」 아님
|
||||
marks.append(EMPTY)
|
||||
found = doc.pop(MACHINES, None)
|
||||
head |= {"기계": found} if found else {} # 812-9 기계 찾기 판정(줄 쌓기 고르기 · 저장 때 별칭)
|
||||
doc["가져온"] = head | {"표지": marks} | ({"알림": notes} if notes else {})
|
||||
shape.shape(doc, meta) # 809-1 · 요구 704 — 지은 이름 → 엑셀 글 머리 · EST 기계 세 줄 = 한 줄
|
||||
if found: # 세 줄을 한 줄로 합쳐 사라진 기호의 기계 판정은 뺌
|
||||
alive = {dc.md.var_id(v) for v in doc.get("변수") or []}
|
||||
doc["가져온"]["기계"] = [x for x in found if x["기호"] in alive]
|
||||
return {**doc, "키": str(sk.get("키") or key), "이름": str(sk.get("이름") or doc.get("이름"))}
|
||||
|
||||
|
||||
def _native(doc: dict) -> None:
|
||||
"""원파일 문서 — 소계 덧줄(합 밖) 비목 칸 비움(금액만) · 수 하나 변수(옵션 칸) 출처 「원파일」."""
|
||||
def _native(doc: dict, native: bool = True) -> None:
|
||||
"""원파일 · 엑셀 읽기기 문서 — 소계 덧줄(합 밖) 비목 칸 비움(금액만) · 수 하나 변수(옵션 칸) 출처
|
||||
「원파일」(엑셀 = 출처 없음 → 화면 꼬리표 「엑셀」)."""
|
||||
for line in doc.get("금액") or []:
|
||||
if line.get("출처") == SUB and line.get("합밖"):
|
||||
line.pop("비목", None)
|
||||
for var in doc.get("변수") or []:
|
||||
if (var.get("연결") or {}).get("갈래") == "숫자" and XL not in var["연결"]:
|
||||
if native and (var.get("연결") or {}).get("갈래") == "숫자" and XL not in var["연결"]:
|
||||
var["출처"] = "원파일"
|
||||
|
||||
|
||||
|
||||
@@ -16,7 +16,8 @@ from decimal import Decimal
|
||||
from typing import Any
|
||||
|
||||
COSTS = ("노무비", "재료비", "경비")
|
||||
_MADE = re.compile(r"^(?:줄|노무비|재료비|경비|계|소계|합계)?_\d+$")
|
||||
# 지은 이름 — 「줄_2 · 노무비_2」 · 읽기기 율 줄 「율」
|
||||
_MADE = re.compile(r"^(?:(?:줄|노무비|재료비|경비|계|소계|합계)?_\d+|율(?:_\d+)?)$")
|
||||
_SECTION = re.compile(r"^\s*\d+\s*\.\s*(.+?)\s*$")
|
||||
|
||||
|
||||
@@ -68,20 +69,26 @@ def _rename_steps(doc: dict, swap: dict[str, str]) -> None:
|
||||
st["줄"] = got
|
||||
|
||||
|
||||
def _bundle_of(doc: dict, at: int) -> list[int] | None:
|
||||
"""at 에서 시작하는 기계 세 줄(노무비 · 재료비 · 경비) — 같은 기계 이음 · 같은 수량."""
|
||||
def _bundle_of(doc: dict, at: int, who: dict[str, str] | None = None) -> list[int] | None:
|
||||
"""at 에서 시작하는 기계 세 줄(노무비 · 재료비 · 경비 · 차례 아무렇게) — 같은 기계 이음 · 같은 수량 ·
|
||||
시간당 경비 이음 없음(숫자)이면 같은 기계 자원(`who` 줄 이름 → 자원 · 814-1)."""
|
||||
money = doc.get("금액") or []
|
||||
three = money[at : at + 3]
|
||||
if len(three) < 3 or [x.get("비목") for x in three] != list(COSTS):
|
||||
if len(three) < 3 or sorted(str(x.get("비목")) for x in three) != sorted(COSTS):
|
||||
return None
|
||||
vars_ = {v.get("열쇠"): v for v in doc.get("변수") or []}
|
||||
links = [(vars_.get(str(x.get("단가"))) or {}).get("연결") or {} for x in three]
|
||||
if any(x.get("갈래") != "단가산출" or "기계" not in (x.get("입력") or {}) for x in links):
|
||||
if all(x.get("갈래") == "숫자" and not isinstance(x.get("값"), dict) for x in links):
|
||||
mine = {(who or {}).get(str(x.get("이름"))) for x in three}
|
||||
if len(mine) != 1 or None in mine:
|
||||
return None
|
||||
elif any(x.get("갈래") != "단가산출" or "기계" not in (x.get("입력") or {}) for x in links):
|
||||
return None
|
||||
same = {(x.get("키"), str(x.get("입력"))) for x in links}
|
||||
if len(same) != 1 or [x.get("칸") for x in links] != list(COSTS):
|
||||
elif len({(x.get("키"), str(x.get("입력"))) for x in links}) != 1 or [
|
||||
x.get("칸") for x in links
|
||||
] != [x.get("비목") for x in three]:
|
||||
return None
|
||||
for k in ("수량", "규격", "자리", "합밖"):
|
||||
for k in ("수량", "규격", "자리", "합밖", "번지"):
|
||||
if len({str(x.get(k) or "") for x in three}) != 1:
|
||||
return None
|
||||
names = {str(x.get("이름")) for x in three}
|
||||
@@ -113,30 +120,70 @@ def _rewrite(text: Any, swap: dict[str, str]) -> Any:
|
||||
return pat.sub(lambda m: swap[m.group(0)], text)
|
||||
|
||||
|
||||
def machines(doc: dict, title: dict[str, str]) -> None:
|
||||
"""② EST 기계 세 줄 → 한 기계 줄(비목 묶음) · 부르던 식 = 비목 칸. `title` = {줄 이름: 자원 이름}."""
|
||||
AFTER = "_뒤" # 옮긴 조합 다음 줄 — 차림이 원본 행 차례로 다시 섞지 않게(다듬기 사이 임시)
|
||||
|
||||
|
||||
def regroup(doc: dict, who: dict[str, str], parts: set[str]) -> None:
|
||||
"""814-1 — 조합 기계 비목 세 줄 사이에 낀 다음 줄(값 0)을 세 줄 뒤로 · 같은 기계끼리 모음
|
||||
(첫 줄 · 다음 줄이 엇갈려 항목이 겹치지 않게 · 값 0 줄이라 셈 그대로). `parts` = 조합 다음 줄 이름."""
|
||||
money = doc.get("금액") or []
|
||||
at = 0
|
||||
while at < len(money):
|
||||
head = who.get(str(money[at].get("이름")))
|
||||
if head is None or str(money[at].get("이름")) in parts:
|
||||
at += 1
|
||||
continue
|
||||
end, mains, rest = at, [], []
|
||||
while end < len(money):
|
||||
name = str(money[end].get("이름"))
|
||||
if name in parts:
|
||||
rest.append(money[end])
|
||||
elif who.get(name) == head and len(mains) < 3:
|
||||
mains.append(money[end])
|
||||
else:
|
||||
break
|
||||
end += 1
|
||||
if len(mains) > 1 and rest:
|
||||
seen = list(dict.fromkeys(who.get(str(x.get("이름"))) for x in rest))
|
||||
rest.sort(key=lambda x: seen.index(who.get(str(x.get("이름"))))) # 같은 기계끼리
|
||||
for x in rest:
|
||||
x[AFTER] = True # 차림(`NativeLayout.apply`) 원본 행 = 앞 줄 행 뒤
|
||||
money[at:end] = mains + rest
|
||||
at = max(end, at + 1)
|
||||
|
||||
|
||||
def machines(doc: dict, title: dict[str, str], who: dict[str, str] | None = None) -> None:
|
||||
"""② 기계 세 줄 → 한 기계 줄(비목 묶음) · 부르던 식 = 비목 칸. `title` = {줄 이름: 자원 이름} ·
|
||||
`who` = {줄 이름: 기계 자원 열쇠}(시간당 경비 이음 없는 세 줄 · 814-1)."""
|
||||
money = doc.get("금액") or []
|
||||
vars_ = {v.get("열쇠"): v for v in doc.get("변수") or []}
|
||||
out, swap, at, used = [], {}, 0, set()
|
||||
while at < len(money):
|
||||
idx = _bundle_of(doc, at)
|
||||
idx = _bundle_of(doc, at, who)
|
||||
if idx is None:
|
||||
out.append(money[at])
|
||||
at += 1
|
||||
continue
|
||||
three = [money[i] for i in idx]
|
||||
first = vars_[str(three[0]["단가"])]
|
||||
link = {k: v for k, v in first["연결"].items() if k not in ("칸", "엑셀")}
|
||||
excel = {
|
||||
c: Decimal(str((vars_[str(x["단가"])]["연결"]).get("엑셀") or 0))
|
||||
for c, x in zip(COSTS, three)
|
||||
}
|
||||
link["엑셀"] = excel
|
||||
name = title.get(str(three[0].get("이름"))) or str(three[0].get("이름"))
|
||||
three = sorted((money[i] for i in idx), key=lambda x: COSTS.index(x["비목"]))
|
||||
first = vars_[str(money[idx[0]]["단가"])]
|
||||
if first["연결"].get("갈래") == "숫자": # 시간당 경비 이음 없음 = 엑셀 값 묶음
|
||||
link = {
|
||||
"갈래": "숫자",
|
||||
"값": {c: (vars_[str(x["단가"])]["연결"]).get("값") for c, x in zip(COSTS, three)},
|
||||
}
|
||||
else:
|
||||
link = {k: v for k, v in first["연결"].items() if k not in ("칸", "엑셀")}
|
||||
link["엑셀"] = {
|
||||
c: Decimal(str((vars_[str(x["단가"])]["연결"]).get("엑셀") or 0))
|
||||
for c, x in zip(COSTS, three)
|
||||
}
|
||||
lead = money[idx[0]]
|
||||
name = title.get(str(lead.get("이름"))) or str(lead.get("이름"))
|
||||
first["연결"], first["이름"] = link, f"{name} 단가"
|
||||
if name in {str(x.get("이름")) for x in money} - {str(x.get("이름")) for x in three}:
|
||||
name = f"{name} ({at + 1})" # 같은 이름 줄이 있으면 겹치지 않게
|
||||
line = {k: v for k, v in three[0].items() if k != "기호"} | {"이름": name, "비목": "묶음"}
|
||||
line = {k: v for k, v in lead.items() if k != "기호"} | {"이름": name, "비목": "묶음"}
|
||||
line["단가"] = lead["단가"]
|
||||
_rename_steps(doc, {str(x.get("이름")): name for x in three})
|
||||
if any(x.get("기호") for x in three): # 부르던 식 = 번지 칸(엔진 `번지.X.노무비`)
|
||||
place = _place(doc, name, used)
|
||||
@@ -147,7 +194,8 @@ def machines(doc: dict, title: dict[str, str]) -> None:
|
||||
doc["변수"] = [
|
||||
v
|
||||
for v in doc["변수"]
|
||||
if v is first or v.get("열쇠") not in {str(x["단가"]) for x in three[1:]}
|
||||
if v is first
|
||||
or v.get("열쇠") not in {str(x["단가"]) for x in three} - {str(lead["단가"])}
|
||||
]
|
||||
out.append(line)
|
||||
at += 3
|
||||
@@ -166,11 +214,18 @@ def shape(doc: dict, meta: dict) -> dict:
|
||||
src = meta.get("원본") or {}
|
||||
lay.tag(doc, meta) # 812-3 원파일 차림 — 금액 줄 ↔ 로직 줄(끝에 `apply`)
|
||||
names(doc, list(src.get("글줄") or []))
|
||||
title = {}
|
||||
title, who, parts = {}, {}, set()
|
||||
for item in (meta.get("로직") or {}).get("호표") or []:
|
||||
res = item.get("자원") or {}
|
||||
if res.get("갈래") == "기계" and res.get("이름"):
|
||||
title[str(item.get("이름"))] = " ".join(x for x in (res["이름"], res.get("규격")) if x)
|
||||
machines(doc, title)
|
||||
key = (res.get("코드"), res.get("이름"), res.get("규격"))
|
||||
who[str(item.get("이름"))] = repr(key)
|
||||
if res.get("조합"):
|
||||
parts.add(str(item.get("이름")))
|
||||
regroup(doc, who, parts)
|
||||
machines(doc, title, who)
|
||||
lay.apply(doc, meta)
|
||||
for line in doc.get("금액") or []:
|
||||
line.pop(AFTER, None)
|
||||
return doc
|
||||
|
||||
@@ -1,622 +0,0 @@
|
||||
"""가져온 식 ↔ 우리 M01 기초 연결 찾기(PLAN 810-4 · 요구 707 · 709 · 703) — 서버 단독.
|
||||
|
||||
가져온 식 문서(원파일 확정 정보 · 우리 단가로 셈) 하나를 M01 수식 문서 전부와 맞대어
|
||||
「확정」 · 「근접」 · 「없음」 을 냄 — 지어 맞추기 없음(값 보정 · 허용 오차 없음 · 원 단위 같아야 확정).
|
||||
① 후보 = 결과단위 같음 + 노임 키 ⊂ M01 노임 키 + 재료 · 기계 있으면 M01 에도 ·
|
||||
품셈 절 번호(가져온 「추정」 글 · M01 `출처`)가 겹치는 문서가 있으면 그것만.
|
||||
② 옵션 풀이 = 그 문서의 선택 · 입력 변수 값을 줄마다 좁힘(줄 수량 · 단가가 의존하는 변수만 돌림 ·
|
||||
값이 가져온 줄 수량 · 단가 묶음에 있거나 0(꺼진 줄)인 값만 남김) → 남은 조합(상한 `CAP_ALL`)을
|
||||
문서 셈 · 입력 변수 후보 = 기본값 · 0 · 가져온 식에 나온 수 · 선택은 가져온 글에 든 값부터.
|
||||
③ 판정 = 줄 대조(수량 · 단가 · 비목 금액 · 율 줄) + 노 · 재 · 경 · 계 —
|
||||
다른 항 0 = 「확정」 · 같은 줄이 하나 넘고 핵심 줄 반 넘게 짝 = 「근접」(다른 항 목록) · 그 밖 「없음」.
|
||||
결과 `{판정, 키, 이름, 옵션, 다른, 금액 {가져온, 우리}, 후보수}` = 가져온 식 원본 `연결후보`.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import functools
|
||||
import itertools
|
||||
import json
|
||||
import re
|
||||
from decimal import Decimal, InvalidOperation
|
||||
from typing import Any
|
||||
|
||||
from M02_MasterTemplete import M02_Doc_Copy as dc
|
||||
|
||||
md, mc, ml = dc.md, dc.mc, dc.ml
|
||||
if not hasattr(
|
||||
md.parse, "cache_info"
|
||||
): # 식 글 → 나무 = 순수 함수 · 조합마다 같은 글을 다시 풀지 않음
|
||||
md.parse = functools.lru_cache(maxsize=50000)(md.parse)
|
||||
|
||||
KEY = "연결후보"
|
||||
SURE, NEAR, NONE = "확정", "근접", "없음"
|
||||
CAP_LINE = 3000 # 줄 하나 좁힐 때 돌리는 조합 상한(넘으면 그 줄은 좁힘에 안 씀)
|
||||
CAP_ALL = 192 # 문서 하나 끝 셈 조합 상한
|
||||
CAP_INPUT = 12 # 입력 변수 후보 수 상한
|
||||
NICE = (2, 3, 4, 5, 6, 8, 10, 12, 100, 1000) # 단위 환산으로 보는 배
|
||||
_SEC = re.compile(r"(?<![\d-])(\d{1,2}(?:-\d{1,2}){1,3})(?![\d-])")
|
||||
_NUMS = re.compile(r"\d+(?:\.\d+)?")
|
||||
_UNIT = {"m3": "㎥", "m2": "㎡", "m": "m", "ea": "개", "톤": "ton", "t": "ton", "hr": "시간"}
|
||||
|
||||
|
||||
# ── 수 ────────────────────────────────────────────────────────────────
|
||||
def _key(v: Any) -> Any:
|
||||
"""비교 열쇠 — 수 = 유효 12 자리(Decimal 셈 자리 차이만 지움) · 묶음 = 비목 튜플."""
|
||||
if isinstance(v, dict):
|
||||
return tuple(_key(v.get(k)) for k in md.COSTS)
|
||||
if isinstance(v, Decimal):
|
||||
try:
|
||||
return v.normalize() if v == 0 else Decimal(f"{v:.12g}").normalize()
|
||||
except InvalidOperation:
|
||||
return None
|
||||
return v
|
||||
|
||||
|
||||
def unit_of(text: Any) -> str:
|
||||
"""결과단위 「원/㎡」 → 「㎡」(소문자 · m3 → ㎥ 등) · 돈 아닌 단위 = ""."""
|
||||
text = str(text or "").strip()
|
||||
if not text.startswith("원/"):
|
||||
return ""
|
||||
unit = text[2:].strip().lower().replace(" ", "")
|
||||
return _UNIT.get(unit, unit)
|
||||
|
||||
|
||||
def sections(texts: list[str]) -> set[str]:
|
||||
"""품셈 절 번호(「6-3-1」) — 앞 둘 · 셋 마디 모두."""
|
||||
out = set()
|
||||
for t in texts:
|
||||
for s in _SEC.findall(str(t or "")):
|
||||
parts = s.split("-")
|
||||
out |= {"-".join(parts[:n]) for n in range(2, len(parts) + 1)}
|
||||
return out
|
||||
|
||||
|
||||
# ── 줄 사실 ───────────────────────────────────────────────────────────
|
||||
def _single(text: Any) -> str | None:
|
||||
try:
|
||||
node = md.parse(text)
|
||||
except md.DocError:
|
||||
return None
|
||||
return node[1] if node[0] == "var" else None
|
||||
|
||||
|
||||
def _what(doc: dict, line: dict) -> tuple[str, str]:
|
||||
"""금액 줄 자원 → (갈래, 코드) · 갈래 = 노임 · 재료 · 기계 · 산출 · 율 · 수."""
|
||||
sym = _single(line.get("단가"))
|
||||
var = next((v for v in doc.get("변수") or [] if sym and md.var_id(v) == sym), None)
|
||||
if var is None:
|
||||
return "율", ""
|
||||
link = var.get("연결") or {}
|
||||
kind, key = link.get("갈래"), str(link.get("키") or "")
|
||||
if kind == "요소":
|
||||
if key.startswith("LB"):
|
||||
return "노임", key
|
||||
if key.startswith("EQ"):
|
||||
return "기계", key
|
||||
return "재료", key
|
||||
if kind == "단가산출":
|
||||
given = link.get("입력") or {}
|
||||
code = str(given.get("기계") or "").strip("'")
|
||||
return ("기계", code) if "기계" in given else ("산출", key)
|
||||
if kind == "숫자" and isinstance(link.get("값"), dict):
|
||||
return "산출", ""
|
||||
if kind == "식":
|
||||
return "율", ""
|
||||
return "수", ""
|
||||
|
||||
|
||||
def facts(doc: dict, got: dict) -> list[dict]:
|
||||
"""셈 답 줄 → 대조 사실 — 소계 보임(비목 없는 합 밖) · 꺼진 줄(수량 0 · 금액 0) 뺌."""
|
||||
out = []
|
||||
money = doc.get("금액") or []
|
||||
for i, (line, row) in enumerate(zip(money, got.get("줄") or [])):
|
||||
if line.get("합밖") and not line.get("비목"):
|
||||
continue
|
||||
if row.get("금액") is not None and row["금액"] == 0 and row.get("수량") == 0:
|
||||
continue
|
||||
kind, code = _what(doc, line)
|
||||
if kind == "기계" and code in (got.get("값") or {}): # 옵션 기호 = 그 값(기계 코드)
|
||||
code = mc.plain_text(got["값"][code])
|
||||
if kind == "율" or (kind == "수" and line.get("수량") in (None, "")):
|
||||
kind = "율"
|
||||
out.append(
|
||||
{
|
||||
"번": i,
|
||||
"이름": str(line.get("이름") or ""),
|
||||
"갈래": kind,
|
||||
"코드": code,
|
||||
"수량": _key(row.get("수량")) if kind != "율" else None,
|
||||
"단가": _key(row.get("단가")) if kind != "율" else None,
|
||||
"비목": {k: _key(v) for k, v in (row.get("비목") or {}).items() if v},
|
||||
"금액": _key(row.get("금액")),
|
||||
"합밖": bool(row.get("합밖")),
|
||||
}
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
def resources(doc: dict) -> dict[str, set]:
|
||||
"""문서가 쓸 수 있는 자원(옵션 무관) — {노임 키, 재료, 기계}."""
|
||||
out: dict[str, set] = {"노임": set(), "재료": set(), "기계": set()}
|
||||
for line in doc.get("금액") or []:
|
||||
kind, code = _what(doc, line)
|
||||
if kind == "노임":
|
||||
out["노임"].add(code)
|
||||
elif kind == "재료":
|
||||
out["재료"].add(code or "고름")
|
||||
elif kind in ("기계", "산출"):
|
||||
out["기계"].add(code or kind)
|
||||
return out
|
||||
|
||||
|
||||
# ── 연결 기억 ─────────────────────────────────────────────────────────
|
||||
class Memo:
|
||||
"""M01 연결 + 셈 기억(요소 · 표 · 부른 단가산출) — 같은 값을 조합마다 다시 찾지 않음."""
|
||||
|
||||
def __init__(self, links: Any):
|
||||
self.links, self.hits, self.calls = links, {}, {}
|
||||
|
||||
def _hit(self, tag: str, p: dict, fn: Any) -> Any:
|
||||
key = (tag, json.dumps(p, sort_keys=True, ensure_ascii=False, default=str))
|
||||
if key not in self.hits:
|
||||
try:
|
||||
self.hits[key] = (True, fn(p))
|
||||
except (mc.Gap, mc.Broken) as e:
|
||||
self.hits[key] = (False, e)
|
||||
ok, got = self.hits[key]
|
||||
if not ok:
|
||||
raise got
|
||||
return got
|
||||
|
||||
def element(self, p: dict) -> Any:
|
||||
return self._hit("요소", p, self.links.element)
|
||||
|
||||
def table(self, p: dict) -> Any:
|
||||
return self._hit("표", p, self.links.table)
|
||||
|
||||
def doc(self, key: str) -> dict | None:
|
||||
return self.links.doc(key)
|
||||
|
||||
|
||||
# ── ① 후보 ────────────────────────────────────────────────────────────
|
||||
class Index:
|
||||
"""M01 수식 문서 색인(결과단위 · 자원 · 절) — 프로세스 안 한 번."""
|
||||
|
||||
def __init__(self, docs: dict[str, dict]):
|
||||
self.docs = docs
|
||||
self.by_unit: dict[str, list[str]] = {}
|
||||
self.res: dict[str, dict[str, set]] = {}
|
||||
self.secs: dict[str, set[str]] = {}
|
||||
self.regions: set[str] = set()
|
||||
for key, doc in docs.items():
|
||||
for v in doc.get("변수") or []:
|
||||
if str(v.get("열쇠") or "").endswith("지역"):
|
||||
self.regions |= {str(x) for x in (v.get("연결") or {}).get("목록") or []}
|
||||
unit = unit_of(doc.get("결과단위"))
|
||||
if not unit or not doc.get("금액"):
|
||||
continue
|
||||
self.by_unit.setdefault(unit, []).append(key)
|
||||
self.res[key] = resources(doc)
|
||||
self.secs[key] = sections([doc.get("출처")])
|
||||
|
||||
def candidates(self, doc: dict) -> list[str]:
|
||||
mine = resources(doc)
|
||||
keys = []
|
||||
for key in self.by_unit.get(unit_of(doc.get("결과단위")), []):
|
||||
have = self.res[key]
|
||||
if not mine["노임"] <= have["노임"]:
|
||||
continue
|
||||
if mine["재료"] and not have["재료"]:
|
||||
continue
|
||||
if mine["기계"] and not have["기계"]:
|
||||
continue
|
||||
if not (mine["노임"] or mine["재료"] or mine["기계"]):
|
||||
continue
|
||||
keys.append(key)
|
||||
secs = sections([x.get("글") for x in (doc.get("가져온") or {}).get("추정") or []])
|
||||
hit = [k for k in keys if secs & self.secs[k]]
|
||||
return hit or keys
|
||||
|
||||
|
||||
# ── ② 옵션 풀이 ───────────────────────────────────────────────────────
|
||||
def _closure(doc: dict) -> dict[str, set[str]]:
|
||||
"""기호 → 그 기호가 (거쳐서) 부르는 이름 전부."""
|
||||
direct: dict[str, set[str]] = {}
|
||||
for v in doc.get("변수") or []:
|
||||
direct[md.var_id(v)] = set(md.link_names(v.get("연결") or {}))
|
||||
for line in doc.get("식") or []:
|
||||
try:
|
||||
direct[str(line.get("기호"))] = set(md.names(md.parse(line.get("식"))))
|
||||
except md.DocError:
|
||||
direct[str(line.get("기호"))] = set()
|
||||
memo: dict[str, set[str]] = {}
|
||||
|
||||
def reach(name: str, seen: tuple = ()) -> set[str]:
|
||||
if name in memo:
|
||||
return memo[name]
|
||||
out = set()
|
||||
for n in direct.get(name, ()):
|
||||
if n not in seen:
|
||||
out |= {n} | reach(n, (*seen, name))
|
||||
memo[name] = out
|
||||
return out
|
||||
|
||||
return {n: reach(n) for n in direct}
|
||||
|
||||
|
||||
def _uses(text: Any, close: dict) -> set[str] | None:
|
||||
try:
|
||||
names = md.names(md.parse(text)) if text not in (None, "") else []
|
||||
except md.DocError:
|
||||
return None
|
||||
out = set(names)
|
||||
for n in names:
|
||||
out |= close.get(n, set())
|
||||
return out
|
||||
|
||||
|
||||
def numbers_of(doc: dict) -> list[Decimal]:
|
||||
"""가져온 식에 나온 수(숫자 변수 · 식 · 수량 글 · 규격 · 이름)."""
|
||||
got: list[Decimal] = []
|
||||
texts = [doc.get("이름"), (doc.get("가져온") or {}).get("규격")]
|
||||
for v in doc.get("변수") or []:
|
||||
val = (v.get("연결") or {}).get("값")
|
||||
if not isinstance(val, dict):
|
||||
texts.append(val)
|
||||
texts += [x.get("식") for x in doc.get("식") or []]
|
||||
texts += [x.get("수량") for x in doc.get("금액") or []]
|
||||
texts += [x.get("규격") for x in doc.get("금액") or []]
|
||||
for t in texts:
|
||||
for s in _NUMS.findall(str(t or "")):
|
||||
d = Decimal(s).normalize()
|
||||
if d not in got:
|
||||
got.append(d)
|
||||
return got
|
||||
|
||||
|
||||
def _words(doc: dict) -> str:
|
||||
"""가져온 식 글(이름 · 규격 · 줄 규격 · 부름 입력 값 — 줄 이름 뺌) — 선택 값의 글 근거."""
|
||||
bits = [doc.get("이름"), (doc.get("가져온") or {}).get("규격")]
|
||||
bits += [x.get("규격") for x in doc.get("금액") or []]
|
||||
for v in doc.get("변수") or []:
|
||||
bits += [str(x) for x in ((v.get("연결") or {}).get("입력") or {}).values()]
|
||||
return " ".join(str(b or "") for b in bits).replace(" ", "")
|
||||
|
||||
|
||||
def domains(
|
||||
doc: dict, imported: dict, regions: set[str] = frozenset()
|
||||
) -> tuple[dict[str, list], dict[str, set]]:
|
||||
"""선택 · 입력 변수 → (돌릴 값, 글 근거) — 기본 먼저 · 가져온 글에 든 선택 값(근거) 먼저 ·
|
||||
글 입력 지역 = 가져온 글에 든 지역 이름."""
|
||||
words, nums = _words(imported), numbers_of(imported)
|
||||
out: dict[str, list] = {}
|
||||
ev: dict[str, set] = {}
|
||||
for v in doc.get("변수") or []:
|
||||
link = v.get("연결") or {}
|
||||
kind, base = link.get("갈래"), link.get("값")
|
||||
if kind == "선택":
|
||||
opts = list(link.get("목록") or [])
|
||||
seen = [o for o in opts if str(o).replace(" ", "") in words and len(str(o)) > 1]
|
||||
seen.sort(key=lambda o: -len(str(o)))
|
||||
if seen:
|
||||
ev[md.var_id(v)] = set(seen)
|
||||
rest = [o for o in opts if o not in seen]
|
||||
first = [base] if base in rest else []
|
||||
out[md.var_id(v)] = seen + first + [o for o in rest if o != base]
|
||||
elif kind == "입력":
|
||||
try:
|
||||
d = Decimal(str(base).replace(",", "")).normalize()
|
||||
except (InvalidOperation, ValueError):
|
||||
if str(v.get("열쇠") or "").endswith("지역"):
|
||||
seen = sorted((r for r in regions if len(r) > 1 and r in words), key=len)
|
||||
out[md.var_id(v)] = list(dict.fromkeys([base, *seen[::-1]]))
|
||||
continue # 그 밖 글 입력 = 기본 그대로
|
||||
low, high = (mc.number(x) for x in (link.get("범위") or [None, None]))
|
||||
vals = [d, Decimal(0)] + nums
|
||||
vals = [x for x in vals if (low is None or x >= low) and (high is None or x <= high)]
|
||||
out[md.var_id(v)] = list(dict.fromkeys(vals))[:CAP_INPUT]
|
||||
return out, ev
|
||||
|
||||
|
||||
def _given(doc: dict, pick: dict) -> dict:
|
||||
ids = {md.var_id(v): v.get("열쇠") or md.var_id(v) for v in doc.get("변수") or []}
|
||||
return {ids[k]: v for k, v in pick.items()}
|
||||
|
||||
|
||||
def _eval(doc: dict, links: Any, pick: dict, text: Any, memo: dict) -> Any:
|
||||
run = mc._Run(doc, links, _given(doc, pick), (str(doc.get("키")),), links.calls)
|
||||
got = run.expr(text, "줄")
|
||||
if isinstance(got, Decimal) and got == 0:
|
||||
return 0
|
||||
return _key(got) if isinstance(got, (Decimal, dict)) else None
|
||||
|
||||
|
||||
def narrow(
|
||||
doc: dict, links: Any, dom: dict, want: dict[str, set], ev: dict[str, set] | None = None
|
||||
) -> dict[str, list]:
|
||||
"""줄마다 의존 변수만 돌려 가져온 값(수량 · 단가) · 0 에 닿는 값만 남김(두 바퀴) ·
|
||||
글 근거 값(`ev`)은 안 지움(값이 달라도 그 조합을 셈 — 다른 항으로 드러냄)."""
|
||||
ev = ev or {}
|
||||
close = _closure(doc)
|
||||
memo: dict = {}
|
||||
base = {k: vs[0] for k, vs in dom.items()}
|
||||
jobs = []
|
||||
for line in doc.get("금액") or []:
|
||||
for slot, field in (("수량", "수량"), ("단가", "단가")):
|
||||
uses = _uses(line.get(field), close)
|
||||
if uses is None or uses & (set(md.MONEY_NAMES) | _money_syms(doc)):
|
||||
continue
|
||||
free = sorted(u for u in uses if u in dom)
|
||||
if free:
|
||||
jobs.append((line.get(field), free, want[slot]))
|
||||
for _ in range(2):
|
||||
for text, free, targets in jobs:
|
||||
space = [dom[f] for f in free]
|
||||
if _size(space) > CAP_LINE:
|
||||
continue
|
||||
ok: dict[str, set] = {f: set() for f in free}
|
||||
for combo in itertools.product(*space):
|
||||
pick = {**base, **dict(zip(free, combo))}
|
||||
got = _eval(doc, links, pick, text, memo)
|
||||
if got == 0 or got in targets:
|
||||
for f, c in zip(free, combo):
|
||||
ok[f].add(c)
|
||||
if all(ok.values()):
|
||||
for f in free:
|
||||
ok[f] |= ev.get(f, set())
|
||||
dom[f] = [x for x in dom[f] if x in ok[f]]
|
||||
base[f] = dom[f][0]
|
||||
return dom
|
||||
|
||||
|
||||
def _money_syms(doc: dict) -> set[str]:
|
||||
return {str(x["기호"]) for x in doc.get("금액") or [] if x.get("기호")}
|
||||
|
||||
|
||||
def _size(space: list[list]) -> int:
|
||||
n = 1
|
||||
for s in space:
|
||||
n *= max(len(s), 1)
|
||||
return n
|
||||
|
||||
|
||||
def combos(doc: dict, dom: dict) -> list[dict]:
|
||||
"""좁힌 값 조합(기본 쪽 먼저 · 상한 `CAP_ALL`) — 한 값뿐인 변수는 그 값."""
|
||||
keys = [k for k, v in dom.items() if v]
|
||||
out = []
|
||||
for combo in itertools.product(*(dom[k] for k in keys)):
|
||||
out.append(dict(zip(keys, combo)))
|
||||
if len(out) >= CAP_ALL:
|
||||
break
|
||||
return out
|
||||
|
||||
|
||||
# ── ③ 줄 대조 ─────────────────────────────────────────────────────────
|
||||
def _ratio(a: Any, b: Any) -> str | None:
|
||||
if not isinstance(a, Decimal) or not isinstance(b, Decimal) or not a or not b:
|
||||
return None
|
||||
for n in NICE:
|
||||
if _key(a * n) == b:
|
||||
return f"× {n}"
|
||||
if _key(b * n) == a:
|
||||
return f"÷ {n}"
|
||||
return None
|
||||
|
||||
|
||||
def compare(mine: list[dict], ours: list[dict]) -> dict[str, Any]:
|
||||
"""가져온 줄(mine) ↔ M01 줄(ours) → {같음, 짝, 핵심, 다른 [{항, 글}]}."""
|
||||
left, right = list(mine), list(ours)
|
||||
same = paired = 0
|
||||
diffs: list[dict] = []
|
||||
|
||||
def take(test) -> list[tuple[dict, dict]]:
|
||||
hits = []
|
||||
for a in list(left):
|
||||
b = next((x for x in right if test(a, x)), None)
|
||||
if b is not None:
|
||||
left.remove(a)
|
||||
right.remove(b)
|
||||
hits.append((a, b))
|
||||
return hits
|
||||
|
||||
def core(a: dict, b: dict) -> bool:
|
||||
return a["갈래"] != "율" and b["갈래"] != "율"
|
||||
|
||||
def cls(a: dict, b: dict) -> bool:
|
||||
return a["갈래"] == b["갈래"] or {a["갈래"], b["갈래"]} <= {"기계", "산출"}
|
||||
|
||||
exact = take(
|
||||
lambda a, b: (
|
||||
a["수량"] == b["수량"] and a["단가"] == b["단가"] and a["비목"] == b["비목"]
|
||||
if core(a, b)
|
||||
else a["갈래"] == b["갈래"] == "율" and a["비목"] == b["비목"]
|
||||
)
|
||||
)
|
||||
same += sum(1 for a, _ in exact if a["갈래"] != "율")
|
||||
for a, b in take(lambda a, b: core(a, b) and cls(a, b) and a["수량"] == b["수량"]):
|
||||
paired += 1
|
||||
if a["갈래"] == "기계" and b["갈래"] == "기계" and a["코드"] != b["코드"]:
|
||||
diffs.append({"항": "기계 규격", "글": f"{a['이름']} {a['코드']} ↔ {b['코드']}"})
|
||||
elif a["단가"] != b["단가"]:
|
||||
diffs.append({"항": "단가 차이", "글": f"{a['이름']} 단가 {a['단가']} ↔ {b['단가']}"})
|
||||
else:
|
||||
diffs.append({"항": "비목", "글": f"{a['이름']} 비목 다름"})
|
||||
for a, b in take(lambda a, b: core(a, b) and cls(a, b) and a["단가"] == b["단가"]):
|
||||
paired += 1
|
||||
how = _ratio(a["수량"], b["수량"])
|
||||
what = "단위 환산" if how else "수량"
|
||||
diffs.append({"항": what, "글": f"{a['이름']} 수량 {a['수량']} ↔ {b['수량']} {how or ''}"})
|
||||
for a, b in take(lambda a, b: core(a, b) and a["갈래"] == b["갈래"] == "기계"):
|
||||
paired += 1
|
||||
diffs.append(
|
||||
{"항": "기계 규격", "글": f"{a['이름']} {a['코드']} ↔ {b['코드']} · 수량 다름"}
|
||||
)
|
||||
for a, b in take(lambda a, b: a["갈래"] == b["갈래"] == "율" and a["금액"] == b["금액"]):
|
||||
diffs.append({"항": "비목", "글": f"율 줄 {a['이름']} 비목 다름"})
|
||||
for a, b in take(lambda a, b: a["갈래"] == b["갈래"] == "율"):
|
||||
diffs.append({"항": "율 값", "글": f"{a['이름']} {a['금액']} ↔ {b['이름']} {b['금액']}"})
|
||||
for side, rows in (("가져온 쪽만", left), ("우리 쪽만", right)):
|
||||
for x in rows:
|
||||
if x["갈래"] == "율":
|
||||
what = "공구손료 유무" if "공구" in x["이름"] else "율 줄"
|
||||
else:
|
||||
what = "줄 없음"
|
||||
diffs.append({"항": what, "글": f"{side} — {x['이름']}"})
|
||||
n_core = sum(1 for a in mine if a["갈래"] != "율")
|
||||
return {"같음": same, "짝": same + paired, "핵심": n_core, "다른": diffs}
|
||||
|
||||
|
||||
def _totals(got: dict) -> dict:
|
||||
return {k: _key(mc.number(got.get(k))) for k in md.BUNDLE}
|
||||
|
||||
|
||||
EXPLAINED = (
|
||||
"이름 다름",
|
||||
"단가 차이",
|
||||
"단위 환산",
|
||||
"기계 규격",
|
||||
"비목",
|
||||
"율 값",
|
||||
"율 줄",
|
||||
"공구손료 유무",
|
||||
"끝수",
|
||||
)
|
||||
|
||||
|
||||
def verdict(
|
||||
mine: list[dict], ours: list[dict], got_a: dict, got_b: dict, off: list[dict] = ()
|
||||
) -> dict:
|
||||
"""확정 = 다른 항 0(옵션 글 근거까지 맞음) · 근접 = 핵심 줄 반 넘게 짝 + (같은 줄 있음 또는
|
||||
다른 항이 모두 까닭 있는 갈래 — 단가 · 단위 환산 · 기계 규격 · 비목 · 율) · 그 밖 없음."""
|
||||
res = compare(mine, ours)
|
||||
res["다른"] = [*off, *res["다른"]]
|
||||
ta, tb = _totals(got_a), _totals(got_b)
|
||||
if not res["다른"] and ta != tb:
|
||||
res["다른"].append({"항": "끝수", "글": f"줄은 같고 합이 다름 {ta} ↔ {tb}"})
|
||||
if not res["다른"] and res["같음"]:
|
||||
res["판정"] = SURE
|
||||
elif res["짝"] * 2 >= res["핵심"] + 1 and (
|
||||
res["같음"] or all(d["항"] in EXPLAINED for d in res["다른"])
|
||||
):
|
||||
res["판정"] = NEAR
|
||||
else:
|
||||
res["판정"] = NONE
|
||||
return res
|
||||
|
||||
|
||||
# ── 한 문서 ───────────────────────────────────────────────────────────
|
||||
def _calc(doc: dict, links: Any, given: dict) -> dict | None:
|
||||
try:
|
||||
got = mc.calc(doc, links, given, (), getattr(links, "calls", None))
|
||||
except Exception: # noqa: BLE001 — 못 셈 = 그 조합 버림
|
||||
return None
|
||||
return got if got.get("셈됨") else None
|
||||
|
||||
|
||||
def try_doc(
|
||||
base: dict, links: Any, imported: dict, got_imp: dict, mine: list[dict], regions: set[str]
|
||||
) -> dict:
|
||||
"""M01 문서 하나 — 옵션 풀이 · 가장 나은 조합 판정(이름 · 절 근거 없으면 「이름 다름」 = 확정 아님)."""
|
||||
want = {
|
||||
"수량": {x["수량"] for x in mine if x["수량"] is not None},
|
||||
"단가": {x["단가"] for x in mine if x["단가"] is not None},
|
||||
}
|
||||
dom, ev = domains(base, imported, regions)
|
||||
dom = narrow(base, links, dom, want, ev)
|
||||
named = [] if same_name(imported, base) else [_unnamed(imported, base)]
|
||||
best: dict | None = None
|
||||
for pick in combos(base, dom) or [{}]:
|
||||
got = _calc(base, links, _given(base, pick))
|
||||
if got is None:
|
||||
continue
|
||||
off = [_off(base, k, pick[k], ev[k]) for k in ev if k in pick and pick[k] not in ev[k]]
|
||||
off += named
|
||||
res = verdict(mine, facts(base, got), got_imp, got, off)
|
||||
rank = (res["판정"] == SURE, -len(off), res["같음"], res["짝"], -len(res["다른"]))
|
||||
if best is None or rank > best["순위"]:
|
||||
best = {**res, "순위": rank, "옵션": pick, "금액": _totals(got)}
|
||||
if res["판정"] == SURE:
|
||||
break
|
||||
return best or {"판정": NONE, "같음": 0, "짝": 0, "다른": [], "순위": (False, -9, 0, 0, 0)}
|
||||
|
||||
|
||||
def _off(doc: dict, vid: str, value: Any, seen: set) -> dict:
|
||||
"""옵션 값이 가져온 글 근거와 다름 — 다른 항 「옵션 글」."""
|
||||
var = next((v for v in doc.get("변수") or [] if md.var_id(v) == vid), {})
|
||||
name = var.get("이름") or vid
|
||||
return {"항": "옵션 글", "글": f"{name} 글 {'·'.join(map(str, seen))} ↔ {value}"}
|
||||
|
||||
|
||||
def match(imported: dict, links_imp: Any, index: Index, links: Any) -> dict[str, Any]:
|
||||
"""가져온 식 문서 → 연결후보 `{판정, 키, 이름, 옵션, 다른, 금액, 후보수}`."""
|
||||
links = links if isinstance(links, Memo) else Memo(links)
|
||||
got_imp = _calc(imported, links_imp, {})
|
||||
if got_imp is None:
|
||||
return {"판정": NONE, "까닭": "가져온 식을 우리 단가로 못 셈", "후보수": 0}
|
||||
mine = facts(imported, got_imp)
|
||||
keys = index.candidates(imported)
|
||||
best, best_key = None, None
|
||||
for key in keys:
|
||||
res = try_doc(index.docs[key], links, imported, got_imp, mine, index.regions)
|
||||
if best is None or res["순위"] > best["순위"]:
|
||||
best, best_key = res, key
|
||||
if res["판정"] == SURE:
|
||||
break
|
||||
if best is None:
|
||||
return {"판정": NONE, "후보수": 0}
|
||||
base = index.docs[best_key]
|
||||
if best["판정"] == NONE: # 가까운 후보(보이기만 — 단추 없음)
|
||||
near = {"키": best_key, "이름": base.get("이름"), "다른": best["다른"][:12]}
|
||||
return {"판정": NONE, "후보수": len(keys), "가까움": near}
|
||||
return {
|
||||
"판정": best["판정"],
|
||||
"키": best_key,
|
||||
"이름": base.get("이름"),
|
||||
"옵션": _shown(base, best["옵션"]),
|
||||
"다른": best["다른"],
|
||||
"금액": {"가져온": _plain(_totals(got_imp)), "우리": _plain(best["금액"])},
|
||||
"후보수": len(keys),
|
||||
}
|
||||
|
||||
|
||||
def _shown(doc: dict, pick: dict) -> dict[str, Any]:
|
||||
"""고른 옵션(기본과 다른 것) — {열쇠: 값}(수 = 수 · 선택 목록의 수 값과 같은 꼴 · SK 입력값)."""
|
||||
out = {}
|
||||
for v in doc.get("변수") or []:
|
||||
vid = md.var_id(v)
|
||||
if vid in pick and mc.plain_text(mc.number(pick[vid])) != mc.plain_text(
|
||||
mc.number((v.get("연결") or {}).get("값"))
|
||||
):
|
||||
out[str(v.get("열쇠") or vid)] = ml._norm(mc.number(pick[vid]))
|
||||
return out
|
||||
|
||||
|
||||
_STOP = {"설치", "해체", "인력", "기계", "작업", "운반", "타설", "제작"}
|
||||
|
||||
|
||||
def _grams(texts: list[Any]) -> set[str]:
|
||||
"""이름 낱말 — 한글 두 글자 묶음 · 영문 · 수 낱말(일반 낱말 뺌)."""
|
||||
out: set[str] = set()
|
||||
for t in texts:
|
||||
t = re.sub(r"\s|및", "", str(t or ""))
|
||||
for run in re.findall(r"[가-힣]+", t):
|
||||
out |= {run[i : i + 2] for i in range(len(run) - 1)}
|
||||
out |= {w.lower() for w in re.findall(r"[A-Za-z]{2,}", t)}
|
||||
return out - _STOP
|
||||
|
||||
|
||||
def same_name(imported: dict, base: dict) -> bool:
|
||||
"""뜻 근거 — 품셈 절 번호가 같거나 이름 낱말이 겹침(값만 같은 남의 기초를 확정으로 안 묶음)."""
|
||||
secs = sections([x.get("글") for x in (imported.get("가져온") or {}).get("추정") or []])
|
||||
if secs & sections([base.get("출처")]):
|
||||
return True
|
||||
return bool(_grams([imported.get("이름")]) & _grams([base.get("이름")]))
|
||||
|
||||
|
||||
def _unnamed(imported: dict, base: dict) -> dict:
|
||||
return {"항": "이름 다름", "글": f"{imported.get('이름')} ↔ {base.get('이름')}"}
|
||||
|
||||
|
||||
def _plain(d: dict) -> dict:
|
||||
return {k: (None if v is None else float(v)) for k, v in d.items()}
|
||||
@@ -1,104 +0,0 @@
|
||||
"""가져온 식 SK ↔ 우리 M01 기초 — 연결 찾기 기록 · 「확정」 때만 우리 기초 사본으로 바꾸기 · 되돌리기
|
||||
(PLAN 810-4 · 요구 707 · 708 · 709 · 703) — 서버 단독.
|
||||
|
||||
찾기(`find`) = 가져온 식 문서(우리 단가로 셈) → `M02_Doc_Match.match` → SK `가져온.원본.연결후보` 에 적음.
|
||||
바꾸기(`swap`) = 서버가 다시 찾아 「확정」 일 때만 — 같은 SK 키에 M01 사본 SK(바탕 = 그 기초 ·
|
||||
입력값 = 찾은 옵션 · 802-4a 꼴 · 수식 사본은 저장 때 서버가 찍음)를 씀 · 원래 가져온 식 SK 몸은
|
||||
`가져온보관` 칸에 그대로 둠 · 일위대가 줄 참조 = 키 그대로(SK 내용만 바뀜 → 금액 다시 셈).
|
||||
되돌리기(`back`) = `가져온보관` 몸을 그 키에 다시 씀(가져온 식 금액 그대로).
|
||||
우리 M01 기초는 어느 갈래에서도 안 바뀜(708) · 근접 · 없음 = 바꾸기 409(703).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from common_util.common_util_time import utc_now_text
|
||||
from M02_MasterTemplete import M02_Doc_Copy as dc
|
||||
from M02_MasterTemplete import M02_Doc_Import as dim
|
||||
from M02_MasterTemplete import M02_Doc_Match as dm
|
||||
from M02_MasterTemplete import M02_MasterTemplete_Store as store
|
||||
from M02_MasterTemplete import M02_Sangeun_Doc as sd
|
||||
from M02_MasterTemplete import M02_Sangeun_Imported as imp
|
||||
|
||||
KEEP = "가져온보관" # 바꾼 SK 의 원래 가져온 식 몸(되돌리기)
|
||||
_INDEX: dict[str, Any] = {"문서": None, "색인": None}
|
||||
|
||||
|
||||
def _bad(why: str, status: int = 400) -> store.StoreError:
|
||||
return store.StoreError(status, why)
|
||||
|
||||
|
||||
def index() -> dm.Index:
|
||||
"""M01 수식 문서 색인 — 폴더가 바뀌면(M01 관리자 고침) 다시."""
|
||||
docs = dc.folder()
|
||||
if _INDEX["문서"] is not docs:
|
||||
_INDEX["문서"], _INDEX["색인"] = docs, dm.Index(docs)
|
||||
return _INDEX["색인"]
|
||||
|
||||
|
||||
def _read(key: str) -> dict[str, Any]:
|
||||
if not sd.KEY.match(str(key or "")):
|
||||
raise _bad("키(SK-######)")
|
||||
return store.read(sd.KIND, key)
|
||||
|
||||
|
||||
def judge(sk: dict) -> dict[str, Any]:
|
||||
"""가져온 식 SK → 연결후보(저장 없음)."""
|
||||
whole = imp._whole()
|
||||
links_imp = dim.Links(whole, "우리")
|
||||
doc = dim.of(sk)
|
||||
links_imp.learn(doc)
|
||||
got = dm.match(doc, links_imp, index(), dm.ml.Links(whole, dc.folder()))
|
||||
return {**got, "때": utc_now_text()}
|
||||
|
||||
|
||||
def find(key: str, version: str) -> dict[str, Any]:
|
||||
"""연결 찾기 → SK `가져온.원본.연결후보` 에 적음 → {연결후보, 판}."""
|
||||
got = _read(key)
|
||||
sk = got["문서"]
|
||||
if not dim.is_imported(sk):
|
||||
raise _bad("가져온 식 단가산출만 연결을 찾음")
|
||||
found = judge(sk)
|
||||
meta = sk[imp.KEY]
|
||||
new = {**sk, imp.KEY: {**meta, "원본": {**(meta.get("원본") or {}), dm.KEY: found}}}
|
||||
info = store.write(sd.KIND, key, version or got["판"], new)
|
||||
return {dm.KEY: found, "판": info.get("판")}
|
||||
|
||||
|
||||
def copy_body(sk: dict, found: dict) -> dict[str, Any]:
|
||||
"""M01 사본 SK 몸(이름 · 단위 · 구분 그대로 · 바탕 = 찾은 기초 · 입력값 = 찾은 옵션)."""
|
||||
keep = {k: sk.get(k) for k in ("키", "이름", "단위", "구분", "상세구분") if sk.get(k)}
|
||||
old = {k: v for k, v in sk.items() if k not in ("키", "종류", KEEP)}
|
||||
body = {
|
||||
"덮개": {},
|
||||
"뺀줄": [],
|
||||
"더한줄": [],
|
||||
"고름": {},
|
||||
"자재표시": {},
|
||||
} # M01 그대로(고침 없음)
|
||||
base = {"바탕": {"로직": found["키"]}, "입력값": dict(found.get("옵션") or {})}
|
||||
return {**keep, **body, **base, KEEP: old}
|
||||
|
||||
|
||||
def swap(key: str, version: str) -> dict[str, Any]:
|
||||
"""「확정」 일 때만 우리 기초 사본으로 — 서버가 다시 찾음(화면 판정을 믿지 않음)."""
|
||||
got = _read(key)
|
||||
sk = got["문서"]
|
||||
if not dim.is_imported(sk):
|
||||
raise _bad("가져온 식 단가산출만 바꿈")
|
||||
found = judge(sk)
|
||||
if found.get("판정") != dm.SURE:
|
||||
raise _bad(f"우리 기초와 「확정」 일치가 아님({found.get('판정')}) — 바꾸지 않음", 409)
|
||||
info = store.write(sd.KIND, key, version or got["판"], copy_body(sk, found))
|
||||
return {dm.KEY: found, "판": info.get("판")}
|
||||
|
||||
|
||||
def back(key: str, version: str) -> dict[str, Any]:
|
||||
"""우리 기초 사본 → 원래 가져온 식(보관 몸 그대로)."""
|
||||
got = _read(key)
|
||||
old = got["문서"].get(KEEP)
|
||||
if not isinstance(old, dict) or not old.get(imp.KEY):
|
||||
raise _bad("되돌릴 가져온 식 없음")
|
||||
info = store.write(sd.KIND, key, version or got["판"], {**old, "키": key})
|
||||
return {"판": info.get("판")}
|
||||
@@ -79,6 +79,7 @@ class Block:
|
||||
self.got = {"확정": 0, "표지": 0, "추정": 0}
|
||||
self.memo: list[int] = [] # 글 줄 행(812-3 차림 · 분수 그림 위아래 줄 뺌)
|
||||
self.frac: set[int] = set()
|
||||
self.noted: set[tuple] = set() # 기계 표지 단 자원(812-9 · 블록에 한 번)
|
||||
|
||||
# ── 작은 도구 ──────────────────────────────────────────────────
|
||||
def mark(self, ln: dict, why: str) -> None:
|
||||
@@ -273,6 +274,10 @@ class Block:
|
||||
out["옮김"] = part if part != nat else None
|
||||
return out
|
||||
|
||||
def quote(self, r: dict) -> tuple[str | None, str | None]:
|
||||
"""견적 자원(갈래 고정) → 우리 자재 이음 · 표지(`M02_San_Link.quote`) · 그 밖 = (None, None)."""
|
||||
return em._m01("고정", r) if self.keys and r.get("갈래") == "고정" else (None, None)
|
||||
|
||||
def register(self, ln: dict, call: str) -> None:
|
||||
if ln.get("결과"):
|
||||
self.done.append((ln["결과"][-1], call, ln))
|
||||
@@ -298,21 +303,27 @@ class Block:
|
||||
self.got["확정"] += bool(got)
|
||||
self.register(ln, f"줄.'{name}'")
|
||||
|
||||
def triple(self, rows: list[dict], total: dict | None) -> None:
|
||||
def triple(self, rows: list[dict], total: dict | None, back: int = 0) -> None:
|
||||
"""기계 · 산근 · 견적 세 줄 → 로직 값묶음 한 줄(수량 식 · 자리 같을 때) · 아니면 줄마다."""
|
||||
got = [self.money(ln) for ln in rows]
|
||||
parts = [ln["비목"] for ln in rows]
|
||||
same = all(got) and len({g for g in got}) == 1 and sorted(parts) == ["L", "M", "S"]
|
||||
same = all(got) and len({repr(g) for g in got}) == 1 and sorted(parts) == ["L", "M", "S"]
|
||||
if not same:
|
||||
for ln, g in zip(rows, got):
|
||||
self.machine_line(ln, g)
|
||||
if total:
|
||||
self.subtotal(total, [call for _, call, _ in self.done[-len(rows) :]])
|
||||
if total: # 소계 = 세 줄 + 바로 앞 같은 기계 낱 줄(`back`)
|
||||
self.subtotal(total, [call for _, call, _ in self.done[-(len(rows) + back) :]])
|
||||
return
|
||||
qty, place = got[0]
|
||||
prices = {ln["비목"]: (ln.get("연결단가") or [0])[0] for ln in rows}
|
||||
r = self.res(rows[0], None, None)
|
||||
r |= {"단가": prices}
|
||||
if quoted := self.quote(r)[1]: # 815-1 K5 — 세 줄 견적 = 값묶음 그대로 · 표지 한 번
|
||||
self.mark(rows[0], quoted.removeprefix(NOTE))
|
||||
make = (rows[0].get("자원") or {}).get("같은구성") # 816-2 같은 파일 구성 대조
|
||||
parts = em.machine_parts(r, make) if self.keys else [] # 812-9 기계 찾기 · 조합 = 줄 둘
|
||||
r |= {"찾기": parts[0]} if parts else {}
|
||||
r |= {"조합첫": True} if len(parts) > 1 else {}
|
||||
head = next(
|
||||
(b for b in reversed(self.lines[: self.lines.index(rows[0])])
|
||||
if (b.get("자원") or {}).get("코드") == r.get("코드") and not b.get("비목")),
|
||||
@@ -321,7 +332,10 @@ class Block:
|
||||
title = head.get("품명") or r.get("원이름") or r.get("이름") or "줄"
|
||||
name = self.o.names.item(title)
|
||||
one = {"이름": name, "규격": r.get("규격") or "", "단위": r.get("단위") or "", "수량": qty}
|
||||
one |= {"종류": "로직", "값묶음": em.bundle(r), "이음": self.logic_link(r), "자원": r}
|
||||
one |= {"종류": "로직", "값묶음": em.bundle(r), "이음": None, "자원": r}
|
||||
em.first_spec(one, parts)
|
||||
if parts: # 814-1 조합(굴착기 + 브레이커 · 집게) = 우리 조합 식 한 줄
|
||||
one["이음"] = em.combo_link(one, parts) or em.hourly_text(parts[0]["키"])
|
||||
one |= {"자리": place} if place else {}
|
||||
value = total["결과"][-1] if total and total.get("결과") else None
|
||||
for ln in rows: # 노 · 재 · 경 글 = 펼침(맞댈 값은 한 줄 계 — 소계 글 · 없으면 첫 줄)
|
||||
@@ -329,37 +343,43 @@ class Block:
|
||||
at = total or rows[0]
|
||||
value = value if value is not None else sum(ln["결과"][-1] for ln in rows)
|
||||
self.o.add_line(one, True, at["행"], at.get("보임") or "", value)
|
||||
call = self.parts(one, parts, at, rows[0])
|
||||
if total:
|
||||
self.register(total, f"줄.'{name}'")
|
||||
self.register(total, call)
|
||||
self.got["확정"] += 3
|
||||
|
||||
def parts(self, one: dict, parts: list[dict], at: dict, ln: dict) -> str:
|
||||
"""조합 기계 다음 줄(값 0 · 제 이음) · 기계 표지(블록에 한 번) → 이 기계 부름 글(줄 합)."""
|
||||
calls = [f"줄.'{one['이름']}'"]
|
||||
for p in parts[1:]:
|
||||
extra = em.part_line(one, p, self.o.names)
|
||||
self.o.add_line(extra, True, at["행"], at.get("보임") or "", None)
|
||||
calls.append(f"줄.'{extra['이름']}'")
|
||||
seen = (one["자원"].get("이름"), one["자원"].get("규격"))
|
||||
if seen not in self.noted:
|
||||
self.noted.add(seen)
|
||||
for m in em.part_marks(one["자원"], parts):
|
||||
self.mark(ln, m.removeprefix(NOTE))
|
||||
return calls[0] if len(calls) == 1 else f"({' + '.join(calls)})"
|
||||
|
||||
def machine_line(self, ln: dict, got: tuple | None) -> None:
|
||||
qty, place = got if got else ("1", self.frozen(ln, "수량 식 못 읽음")[1])
|
||||
price = (ln.get("연결단가") or [ln["결과"][-1]])[0]
|
||||
r = self.res(ln, ln["비목"], price if got else ln["결과"][-1])
|
||||
r["갈래"] = "기계" if r["갈래"] == "기계" else r["갈래"]
|
||||
name = self.o.names.item(f"{ln.get('품명') or r.get('이름') or '줄'} {COMP[ln['비목']]}")
|
||||
link, _ = em._m01(r["갈래"], r) if self.keys else (None, None)
|
||||
make = (ln.get("자원") or {}).get("같은구성") # 816-2 같은 파일 구성 대조
|
||||
parts = em.machine_parts(r, make) if self.keys else [] # 812-9 기계 찾기 · 조합 = 줄 둘
|
||||
r |= {"찾기": parts[0]} if parts else {}
|
||||
r |= {"조합첫": True} if len(parts) > 1 else {}
|
||||
link, many = (parts[0]["키"], None) if parts else self.quote(r)
|
||||
one = em.resource(name, r.get("단위"), qty, r, link)
|
||||
em.first_spec(one, parts)
|
||||
one |= {"자리": place} if place else {}
|
||||
self.o.add_line(one, True, ln["행"], ln.get("보임") or "", ln["결과"][-1])
|
||||
if many:
|
||||
self.mark(ln, many.removeprefix(NOTE))
|
||||
self.got["확정"] += bool(got)
|
||||
self.register(ln, f"줄.'{name}'")
|
||||
|
||||
def logic_link(self, r: dict) -> str | None:
|
||||
"""기계 = 시간당 기계경비 로직 부름 글(797-1 짝 고름) · 그 밖 = 없음(산근 = 형제 SK 참조)."""
|
||||
if not self.keys or r.get("갈래") != "기계":
|
||||
return None
|
||||
key, _ = em._m01("기계", r)
|
||||
if not key:
|
||||
return None
|
||||
from M02_MasterTemplete import M02_Doc_Import as dim
|
||||
|
||||
got = dim.hourly_link(key, "노무비")
|
||||
if not got:
|
||||
return None
|
||||
args = "".join(f", {k}={v}" for k, v in got["입력"].items())
|
||||
return f"로직({got['키']}{args})"
|
||||
self.register(ln, self.parts(one, parts, ln, ln))
|
||||
|
||||
def subtotal(
|
||||
self, ln: dict, kids: list[str], part: str | None = None, glyph: bool = True
|
||||
@@ -542,8 +562,22 @@ class Block:
|
||||
rows.append(lines[i + len(rows)])
|
||||
nxt = lines[i + len(rows)] if i + len(rows) < len(lines) else None
|
||||
total = nxt if nxt and nxt.get("연결") == link and nxt.get("비목") == "T" else None
|
||||
back, seen = (
|
||||
0,
|
||||
{r.get("비목") for r in rows},
|
||||
) # 바로 앞 같은 기계 낱 줄(얼린 줄 · 814-1)
|
||||
while i - back > 0 and len(seen) < 3:
|
||||
b = lines[i - back - 1]
|
||||
if (
|
||||
(b.get("자원") or {}).get("코드") != code
|
||||
or b.get("비목") in seen
|
||||
or not b.get("결과")
|
||||
):
|
||||
break
|
||||
seen.add(b.get("비목"))
|
||||
back += 1
|
||||
if all(r.get("결과") for r in rows):
|
||||
self.triple(rows, total)
|
||||
self.triple(rows, total, back)
|
||||
else:
|
||||
for r in rows:
|
||||
self.mark(r, "결과 없는 세 줄")
|
||||
|
||||
@@ -85,6 +85,9 @@ def apply(doc: dict, meta: dict) -> None:
|
||||
if j is not None:
|
||||
at[j] = min(at.get(j, t["행"]), t["행"])
|
||||
rows = _fill([at.get(j) for j in range(len(money))])
|
||||
for j in range(1, len(rows)): # 조합 다음 줄을 세 줄 뒤로 옮긴 것(814-1 `Import_Shape.regroup`)
|
||||
if money[j].get("_뒤"):
|
||||
rows[j] = max(rows[j], rows[j - 1])
|
||||
order = sorted(range(len(money)), key=lambda j: (rows[j], j))
|
||||
if order != list(range(len(money))) and _safe(doc, order):
|
||||
doc["금액"] = money = [money[j] for j in order]
|
||||
@@ -166,6 +169,12 @@ def _items(doc: dict, texts: list[dict], rows: list[int], stm: bool) -> list[dic
|
||||
items: list[dict] = []
|
||||
cur: dict | None = None
|
||||
cut = False # STmate 계 줄 뒤 — 다음 첫 글 줄 = 제목
|
||||
money = doc.get("금액") or []
|
||||
mach = {
|
||||
str(x.get("기호")): x
|
||||
for x in (doc.get("가져온") or {}).get("기계") or []
|
||||
if isinstance(x, dict) and (x.get("조합") or x.get("조합첫"))
|
||||
} # 조합 기계(812-9 · 요구 738-3) = 기계마다 항목 하나 · 제목 = 그 기계
|
||||
|
||||
def new(title: str = "", sub: bool = False) -> dict:
|
||||
it = {"제목": title, "가나다": sub, "줄": [], "합계": False}
|
||||
@@ -186,6 +195,12 @@ def _items(doc: dict, texts: list[dict], rows: list[int], stm: bool) -> list[dic
|
||||
cur = new(title, sub)
|
||||
cut = False
|
||||
else:
|
||||
one = mach.get(price_sym(money[val]) or "") if kind == "금액" else None
|
||||
if one and one.get("조합") and not (cur and cur.get("_조합") == _mach_title(one)):
|
||||
cur = new(_mach_title(one), bool(cur and cur["가나다"]))
|
||||
cur["_조합"] = _mach_title(one) # 같은 기계 다음 줄(비목 셋)은 한 항목
|
||||
elif one and cur is not None and "+" in cur["제목"]:
|
||||
cur["제목"] = _mach_title(one)
|
||||
cur = cur if cur is not None else new()
|
||||
if kind == "공식" and cur["줄"] and cur["줄"][-1][0] == "공식":
|
||||
cur["줄"][-1][1].extend(x for x in val if x not in cur["줄"][-1][1])
|
||||
@@ -200,6 +215,16 @@ def _items(doc: dict, texts: list[dict], rows: list[int], stm: bool) -> list[dic
|
||||
return keep
|
||||
|
||||
|
||||
def _mach_title(one: dict) -> str:
|
||||
"""조합 기계 항목 제목 — 이은 우리 기계 이름 · 규격 › 가져온 이름 · 규격."""
|
||||
first = (one.get("후보") or [{}])[0] if one.get("키") else {}
|
||||
said = first or {
|
||||
"이름": (one.get("원래") or {}).get("이름"),
|
||||
"규격": (one.get("원래") or {}).get("규격"),
|
||||
}
|
||||
return " ".join(str(said.get(k) or "") for k in ("이름", "규격")).strip()
|
||||
|
||||
|
||||
def _save_item(it: dict, money: list[dict]) -> dict:
|
||||
out: dict[str, Any] = {}
|
||||
if it["제목"]:
|
||||
@@ -269,6 +294,12 @@ def price_sym(line: dict) -> str | None:
|
||||
return None
|
||||
|
||||
|
||||
def machine_syms(doc: dict) -> set[str]:
|
||||
"""가져온 기계 줄 단가 기호(812-9 `가져온.기계`) — 못 이어 숫자 연결이어도 그 줄 기계(이름 단추 · 고르기)."""
|
||||
got = (doc.get("가져온") or {}).get("기계") if isinstance(doc.get("가져온"), dict) else None
|
||||
return {str(x.get("기호")) for x in got or [] if isinstance(x, dict)}
|
||||
|
||||
|
||||
def owned_of(doc: dict) -> dict[int, str]:
|
||||
count: dict[str, int] = {}
|
||||
for ns in (
|
||||
@@ -280,11 +311,17 @@ def owned_of(doc: dict) -> dict[int, str]:
|
||||
count[n] = count.get(n, 0) + 1
|
||||
vars_ = {md.var_id(v): v for v in doc.get("변수") or []}
|
||||
lines = {str(x.get("기호") or "") for x in doc.get("식") or []}
|
||||
mach = machine_syms(doc)
|
||||
out = {}
|
||||
for i, line in enumerate(doc.get("금액") or []):
|
||||
s = price_sym(line)
|
||||
kind = ((vars_.get(s) or {}).get("연결") or {}).get("갈래") if s else None
|
||||
if s and kind in ("요소", "단가산출") and count.get(s) == 1 and s not in lines:
|
||||
if (
|
||||
s
|
||||
and (kind in ("요소", "단가산출") or s in mach)
|
||||
and count.get(s) == 1
|
||||
and s not in lines
|
||||
):
|
||||
out[i] = s
|
||||
return out
|
||||
|
||||
|
||||
@@ -4,8 +4,7 @@
|
||||
POST /api/m02/layers/{층}/sangeun/{키}/doc/preview 미리 셈(층 쓰기) {문서?, 끊음?, 할일?}
|
||||
→ {…, 전, 후, 바뀐자원?, 버림?}
|
||||
POST /api/m02/layers/{층}/sangeun/{키}/doc/save 저장(층 쓰기) {문서?, 끊음?, 할일?, 판} → 읽기 꼴
|
||||
POST /api/m02/layers/{층}/sangeun/{키}/match {할일 찾기|바꾸기|되돌리기, 판?}(층 쓰기 · 810-4)
|
||||
→ {연결후보?, 판}(바꾸기 = 확정만 · 아니면 409)
|
||||
옛 별칭 장부 쓰기(812-9) · 연결 찾기 길(810-4)은 걷음(815-2 · 요구 762-1 · 765).
|
||||
⚠ 등록은 `main.py`(로그인만 · 층 권한은 길 안에서).
|
||||
"""
|
||||
|
||||
@@ -50,31 +49,6 @@ def preview_doc(
|
||||
return _call(folder, path.preview, key, body.model_dump(exclude_none=True))
|
||||
|
||||
|
||||
class MatchBody(BaseModel):
|
||||
할일: str = "찾기" # 찾기 | 바꾸기 | 되돌리기(810-4)
|
||||
판: str | None = None
|
||||
|
||||
|
||||
@router.post("/layers/{layer}/sangeun/{key}/match")
|
||||
def match_doc(
|
||||
layer: Layer,
|
||||
key: str,
|
||||
body: MatchBody,
|
||||
owner: str | None = None,
|
||||
session: Session = Depends(verify_session),
|
||||
) -> dict:
|
||||
"""가져온 식 ↔ 우리 기초(810-4) — 찾기(연결후보 적음) · 바꾸기(확정만 · M01 사본 SK) · 되돌리기."""
|
||||
from fastapi import HTTPException
|
||||
|
||||
from M02_MasterTemplete import M02_Doc_MatchSk as msk
|
||||
|
||||
jobs = {"찾기": msk.find, "바꾸기": msk.swap, "되돌리기": msk.back}
|
||||
if body.할일 not in jobs:
|
||||
raise HTTPException(status_code=400, detail="찾기 · 바꾸기 · 되돌리기")
|
||||
folder = _root(session, layer, owner, write=True)
|
||||
return _call(folder, jobs[body.할일], key, body.판 or "")
|
||||
|
||||
|
||||
@router.post("/layers/{layer}/sangeun/{key}/doc/save")
|
||||
def save_doc(
|
||||
layer: Layer,
|
||||
|
||||
@@ -140,7 +140,7 @@ def _fixed(fx: dict, qty: str | None, rates: list[dict]) -> list[dict]:
|
||||
got = mh.hourly(str(link["기계"]))
|
||||
if not got.get("ok") or not got.get("로직"):
|
||||
return [_warn(f"기계 「{name}」 — {got.get('reason') or '시간당 경비 못 셈'}")]
|
||||
machine = {"기계": f"'{got['원문번호']}'"}
|
||||
machine = {"기계": f"'{got['원문번호']}'"} if mh.takes_machine(mh.logic_of(got["원문번호"]) or {}) else {} # fmt: skip
|
||||
return _pick("기계", qty, 키=got["로직"], 이름=name, 규격=spec, 입력=machine)
|
||||
price = fx.get("단가") or {}
|
||||
costs = [c for c in COSTS if Decimal(str(price.get(c) or 0))] or ["재료비"]
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
"""M02 불러오기 요소 잇기 색인(PLAN 815-1 · 요구 755 ~ 764) — M01 요소 줄 「별칭」 칸 · EST 참조 코드 ·
|
||||
재료 정확 일치. 서버 단독 · 읽기만(M01 판이 바뀌면 다시 읽음 · `id(whole)`).
|
||||
|
||||
- `keys(표, 이름, 규격)` = 별칭으로 이어지는 키들(풀이 = `master_alias` · 표 LB 인력 · EQ 기계 · MT 재료 ·
|
||||
MO 유가) — 부르는 곳: 단가산출 불러오기(`M02_San_Link.m01`) · 1차 줄 판정(`M02_Hopyo_Row.m01_picks`) ·
|
||||
기계 찾기(`M02_Machine_Find.alias`) · 일위대가 호표 줄(`M02_Hopyo_Units`)
|
||||
- `est_code(코드)` = EST 원파일 자재 자원 코드(M…) ↔ 우리 자재 줄 `출처` 칸 「자재품목 M…」 같은 줄 ·
|
||||
공사 직접 번호(M0000000…)는 빼고 이름 길
|
||||
- `exact(이름, 규격)` = 지역 꼬리 뗀 이름 · 규격이 `master_alias.norm` 으로 같은 자재 줄(가까운 규격 아님)
|
||||
- `one(줄, 키들)` = 같은 품목(지역 꼬리 뗀 이름 · 규격 같음)의 지역 줄 여럿 → 줄 지역 › 서울 › 같은 단위 ›
|
||||
키 차례 하나 · 다른 품목이 섞이면 None(후보 여럿)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from functools import lru_cache
|
||||
from typing import Any
|
||||
|
||||
from M01_MasterData import M01_MasterData_Store as m01
|
||||
|
||||
import master_alias as am # noqa: E402 — M01 store 가 scripts 경로를 넣음
|
||||
|
||||
MARKET = "MT"
|
||||
PRICES = ("물가자료", "유통물가", "물가정보", "거래가격", "관급") # 자재 값 칸 다섯
|
||||
BASE_REGION = "서울"
|
||||
_CODE = re.compile(r"\bM\d{10}\b")
|
||||
LOCAL = "M0000000" # 공사 직접 번호(파일마다 다른 품목 · 거창 치즐 = 우리 표 Air Hammer) = 코드로 안 이음
|
||||
_REGION = re.compile(r"\s*,\s*([가-힣]{2,4})\s*$") # 자재 이름 지역 꼬리 「, 서울」
|
||||
|
||||
|
||||
def region_of(name: Any) -> tuple[str, str]:
|
||||
"""자재 이름 → (지역 꼬리 뗀 이름, 지역 | 빈 글)."""
|
||||
text = str(name or "")
|
||||
m = _REGION.search(text)
|
||||
return (text[: m.start()], m.group(1)) if m else (text, "")
|
||||
|
||||
|
||||
def _same(name: Any, spec: Any) -> tuple[str, str]:
|
||||
return am.norm(region_of(name)[0]), am.norm(spec)
|
||||
|
||||
|
||||
@lru_cache(maxsize=2)
|
||||
def _book(_whole: int) -> dict[str, Any]:
|
||||
whole = m01.loaded()[1]
|
||||
tables = {t: whole.numbers.get(t, {}) for t in ("LB", "EQ", "MO")}
|
||||
tables[MARKET] = whole.index.get(MARKET, {})
|
||||
rows = {t: {str(r.get("키") or k): r for k, r in got.items()} for t, got in tables.items()}
|
||||
alias = {t: am.index({"키": k, "별칭": r.get("별칭")} for k, r in got.items()) for t, got in rows.items()} # fmt: skip
|
||||
code: dict[str, list[str]] = {}
|
||||
exact: dict[tuple[str, str], list[str]] = {}
|
||||
for k, r in rows[MARKET].items():
|
||||
for c in _CODE.findall(str(r.get("출처") or "")):
|
||||
code.setdefault(c, []).append(k)
|
||||
exact.setdefault(_same(r.get("이름"), r.get("규격")), []).append(k)
|
||||
return {"줄": rows, "별칭": alias, "코드": code, "같음": exact}
|
||||
|
||||
|
||||
def book() -> dict[str, Any]:
|
||||
return _book(id(m01.loaded()[1]))
|
||||
|
||||
|
||||
def row(table: str, key: Any) -> dict[str, Any]:
|
||||
return book()["줄"].get(table, {}).get(str(key)) or {}
|
||||
|
||||
|
||||
def keys(table: str, name: Any, spec: Any = "") -> list[str]:
|
||||
"""별칭 칸으로 이어지는 키들(없음 = [])."""
|
||||
b = book()
|
||||
got = b["줄"].get(table, {})
|
||||
return am.match(b["별칭"].get(table, {}), lambda k: (got.get(k) or {}).get("규격"), name, spec)
|
||||
|
||||
|
||||
def priced(key: Any) -> bool:
|
||||
"""우리 자재 줄에 값 칸 다섯 중 하나라도 있음."""
|
||||
return any(row(MARKET, key).get(c) is not None for c in PRICES)
|
||||
|
||||
|
||||
def est_code(code: Any) -> list[str]:
|
||||
"""EST 자재 코드(M + 수 열) → 우리 자재 키들(출처 칸 같은 코드)."""
|
||||
text = str(code or "").strip()
|
||||
if not _CODE.fullmatch(text) or text.startswith(LOCAL):
|
||||
return []
|
||||
return list(book()["코드"].get(text, []))
|
||||
|
||||
|
||||
def exact(name: Any, spec: Any) -> list[str]:
|
||||
"""지역 꼬리 뗀 이름 · 규격 정규화 같음 자재 키들."""
|
||||
return list(book()["같음"].get(_same(name, spec), []))
|
||||
|
||||
|
||||
def one(line: dict[str, Any], found: list[str]) -> str | None:
|
||||
"""같은 품목 지역 줄 여럿 → 하나(줄 지역 › 서울 › 같은 단위 › 키) · 다른 품목 섞임 = None."""
|
||||
rows = {k: row(MARKET, k) for k in found}
|
||||
if not rows or len({_same(r.get("이름"), r.get("규격")) for r in rows.values()}) != 1:
|
||||
return None
|
||||
mine, unit = region_of(line.get("이름"))[1], am.norm(line.get("단위"))
|
||||
|
||||
def rank(k: str) -> tuple:
|
||||
where = region_of(rows[k].get("이름"))[1]
|
||||
return (where != mine if mine else True, where != BASE_REGION, am.norm(rows[k].get("단위")) != unit, k) # fmt: skip
|
||||
|
||||
return min(rows, key=rank)
|
||||
|
||||
|
||||
def material(line: dict[str, Any], code: Any = None) -> tuple[str | None, str]:
|
||||
"""재료 줄 → (우리 자재 키, 길 「코드」 · 「별칭」 · 「정확 일치」) · 별칭 여럿 = (None, 「별칭 후보 여럿」) ·
|
||||
없음 = (None, "")."""
|
||||
if hit := est_code(code):
|
||||
return one(line, hit) or hit[0], "코드"
|
||||
hit = keys(MARKET, line.get("이름"), line.get("규격"))
|
||||
if hit:
|
||||
got = hit[0] if len(hit) == 1 else one(line, hit)
|
||||
return (got, "별칭") if got else (None, "별칭 후보 여럿")
|
||||
got = one(line, exact(line.get("이름"), line.get("규격")))
|
||||
return (got, "정확 일치") if got else (None, "")
|
||||
@@ -19,8 +19,8 @@
|
||||
받아 `본뜬.전국자재 [{이름, 규격, 단위, 수량, 세 칸}]` 에 적고 빠진 줄에 까닭(금액 없음 ·
|
||||
지역은 프로젝트에서) · 장부엔 안 남김.
|
||||
같은 본뜬 사전조합이 있으면 확인한 내용으로 덮음(`다시씀`) · 없으면 같은 내용 SC 재사용(`재사용` · `M02_Hopyo_Kids.save`) ·
|
||||
`하위` = 부르는 하위 호표 `[{번, 키, 새로}]`(부모 줄 = 하위 사본) · 확인한 줄은 별칭 장부에 남음
|
||||
(`M02_Hopyo_Ledger` · 호표 참조 줄 뺌).
|
||||
`하위` = 부르는 하위 호표 `[{번, 키, 새로}]`(부모 줄 = 하위 사본) · 별칭 장부엔 안 남김(815-2 걷음 ·
|
||||
`M02_Hopyo_Ledger` 는 짝 검사 · 열쇠 도우미만).
|
||||
빠짐 0(705-3) — 확인 안 한 줄 · 노임 · % · 「우리에 없음」 줄 = 고정 줄(`M02_Hopyo_Fixed` · 엑셀 숫자
|
||||
그대로) · 확인 안 한 호표 줄 = 확인된 하위 사전조합 참조(단위 같음 · 아니면 고정) · 빠진 줄 = 「안 씀」 ·
|
||||
통째 대신 · 로직 자재 겹침 · 전국만. `본뜬.품셈` = 품셈으로 넣은 엑셀 줄 차례([품셈으로 바꾸기] 첫 체크) ·
|
||||
@@ -248,18 +248,14 @@ class Builder:
|
||||
nation, mat_lines = body.nation, body.mat_lines
|
||||
poomsem, kid_n = [], 0
|
||||
|
||||
book: dict[str, dict[str, Any]] = {}
|
||||
if whole:
|
||||
for one in whole:
|
||||
put({"수량": 1, "단위": hopyo.get("단위"), "이름": hopyo.get("이름")}, one, None)
|
||||
book[ledger.hopyo_key(hopyo)] = self._row(hopyo, "짝", whole)
|
||||
for at, line in enumerate(rows):
|
||||
got = checked.get(at)
|
||||
if whole:
|
||||
missing.append(_missing(line, WHOLE))
|
||||
continue
|
||||
if got and got.get("뺌"):
|
||||
book[ledger.key(line)] = self._row(line, got["뺌"], [])
|
||||
if got and got.get("뺌") == "안 씀":
|
||||
missing.append(_missing(line, f"뺌 — {got['뺌']}"))
|
||||
continue
|
||||
@@ -277,9 +273,6 @@ class Builder:
|
||||
alias = str(line.get("이름") or "")[: labor.ALIAS_MAX].strip() or None
|
||||
for one in got["대상"]:
|
||||
put(line, one, alias)
|
||||
pairs = [p for p in got["대상"] if "호표" not in p and "전국" not in p]
|
||||
if pairs:
|
||||
book[ledger.key(line)] = self._row(line, "짝", pairs)
|
||||
for code in self._inside(lines, mats): # 로직 자재와 같은 품목 = 세 칸만 로직 자재표시로
|
||||
missing.append(_missing(mat_lines[code], TWICE))
|
||||
name = " ".join(x for x in (hopyo["이름"], hopyo.get("규격")) if x)
|
||||
@@ -306,14 +299,11 @@ class Builder:
|
||||
count["단가산출"] = sum(1 for x in lines if _is_sk(x))
|
||||
sub = [{"번": n, "키": self.done.get(mark(self.key, n))} for n in sorted(kids)]
|
||||
if not save:
|
||||
ledger.write(self.folder, book, self.who, file or None, self.doc_id)
|
||||
head = {"문서": doc, "빠진줄": missing, "남은줄": left, "하위": sub}
|
||||
return {**head, "줄수": {**count, "이음후보": linker.near}}
|
||||
ref, again, self.reused = hkids.save(doc, self.same)
|
||||
wrote = ledger.write(self.folder, book, self.who, file or None, self.doc_id)
|
||||
out = {"참조": ref, "다시씀": again, "재사용": self.reused, "빠진줄": missing, "하위": sub}
|
||||
out["남은줄"] = left
|
||||
out["장부"] = {"씀": wrote}
|
||||
return {**out, "줄수": {**count, "이음후보": linker.near}}
|
||||
|
||||
def _kid(self, line: dict[str, Any], kids: set[int]) -> dict[str, Any] | None:
|
||||
@@ -351,17 +341,6 @@ class Builder:
|
||||
mats.pop(code)
|
||||
return moved
|
||||
|
||||
def _row(self, line: dict[str, Any], kind: str, pairs: list[dict[str, Any]]) -> dict[str, Any]:
|
||||
"""장부 한 줄 — 엑셀조항 = 줄 근거 출처(있으면)."""
|
||||
origin = {"품명": line.get("이름"), "규격": line.get("규격"), "단위": line.get("단위")}
|
||||
clauses = (line.get("근거") or {}).get("출처") or []
|
||||
return {
|
||||
"원래": {k: str(v or "") for k, v in origin.items()},
|
||||
"갈래": kind,
|
||||
"짝": pairs,
|
||||
"엑셀조항": clauses if isinstance(clauses, list) else [],
|
||||
}
|
||||
|
||||
|
||||
def _order(units: dict[int, dict[str, Any]], start: int | None = None) -> list[int]:
|
||||
"""하위 호표 먼저(호표 줄 참조) · 고리는 끊음 · `start` = 그 호표와 하위만."""
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
고르면 지금 잇기 길 `{노임|기계|자재|유가: 키}`).
|
||||
자재 = `M02_Hopyo_Material.candidates` 점수(이름 글자 묶음 · 규격) · 단가 = M01 자재 값(엑셀 줄 단위)
|
||||
노임 = LB 이름 글자 묶음 닮음 · 단가 = LB 값
|
||||
기계 = EQ 갈래 겹침 · 규격 수(같음 1 · 겹침 0.5) · 이름 낱말 덤 · 괄호 낱말 어긋남 깎음 ·
|
||||
단가 없음(기계경비는 로직 셈) · `machine` = 하나로 가름(778-1)
|
||||
기계 = 기계 찾기(812-9 `M02_Machine_Find` — 기계표 전체 · 표기 정규화 · 괄호 낱말 · 규격) 후보 ·
|
||||
단가 없음(기계경비는 로직 셈) · `machine` = 하나로 정할 때만
|
||||
유가(785-3) = MO 이름에 글 이름이 듦(경유 ⊂ 자동차용경유) · 전국 먼저 · 단가 = MO 값
|
||||
차례 = 점수(0.1 단위로 접음) › 엑셀 단가에 가까운 것(엑셀 단가 = 블록 단위 줄 단가 · 모르면 점수만).
|
||||
서버 단독 · 읽기만.
|
||||
@@ -21,18 +21,11 @@ from M01_MasterData import M01_MasterData_Store as m01
|
||||
from M02_MasterTemplete import M02_Hopyo_Match as hm
|
||||
from M02_MasterTemplete import M02_Hopyo_Material as hmat
|
||||
from M02_MasterTemplete import M02_Hopyo_UnitLine as unit_line
|
||||
from M02_MasterTemplete.M02_Hopyo_Words import mtypes, nfkc, specnums
|
||||
from M02_MasterTemplete import M02_Machine_Find as mfind
|
||||
from M02_MasterTemplete.M02_Hopyo_Words import nfkc
|
||||
|
||||
TOP = 3
|
||||
POOL = 5
|
||||
# 괄호 낱말(778-1) — 글에 있으면 EQ 이름에도 있어야 함 · 부속 = 글에 없으면 본 장비 아님
|
||||
FORMS = (
|
||||
("타이어", ("타이어",)),
|
||||
("무한", ("무한궤도",)),
|
||||
("습지", ("습지",)),
|
||||
("트럭탑재", ("트럭탑재",)),
|
||||
)
|
||||
PARTS = ("자동덮개", "부착용", "콤팩터", "습지")
|
||||
|
||||
|
||||
def prices(block: dict[str, Any] | None) -> dict[str, float]:
|
||||
@@ -125,57 +118,24 @@ def oils(name: str) -> list[dict[str, Any]]:
|
||||
]
|
||||
|
||||
|
||||
def _form(text: str, name: Any) -> tuple[bool, bool]:
|
||||
"""괄호 낱말(778-1) — (글 형식 낱말이 EQ 이름에 다 있음, EQ 이름에 글에 없는 부속 있음)."""
|
||||
t, n = nfkc(text), nfkc(name)
|
||||
ok = all(any(w in n for w in ws) for k, ws in FORMS if k in t)
|
||||
return ok, any(w in n and w not in t for w in PARTS)
|
||||
def _eq(one: dict[str, Any]) -> dict[str, Any]:
|
||||
raw = m01.loaded()[1].numbers.get("EQ", {}).get(str(one.get("원문번호")), {})
|
||||
unit = (raw.get("단위") or {}).get("내용시간") if isinstance(raw.get("단위"), dict) else ""
|
||||
return {"키": one["키"], "갈래": "기계", "이름": one["이름"], "규격": one.get("규격") or "",
|
||||
"단위": unit or "시간", "단가": None} # fmt: skip
|
||||
|
||||
|
||||
def narrow(text: str, rows: list[tuple[str, Any]]) -> list[str]:
|
||||
"""EQ `[(키, 이름)]` → 괄호 낱말로 가른 키 — 형식 다름 뺌 · 부속은 남는 것 있을 때만 뺌."""
|
||||
keep = [(c, _form(text, n)[1]) for c, n in rows if _form(text, n)[0]]
|
||||
return [c for c, part in keep if not part] or [c for c, _ in keep]
|
||||
def machine(text: str, *_: Any) -> dict | None:
|
||||
"""기계 글(이름 규격) → EQ 한 줄 — 기계 찾기(812-9 `M02_Machine_Find`)가 하나로 정할 때만."""
|
||||
got = mfind.find(text)
|
||||
if got["판정"] not in (mfind.LINKED, mfind.ALIAS):
|
||||
return None
|
||||
return m01.loaded()[1].numbers.get("EQ", {}).get(str(got["후보"][0]["원문번호"]))
|
||||
|
||||
|
||||
def machine(text: str, kinds: set | None = None, nums: set | None = None) -> dict | None:
|
||||
"""기계 글 → EQ 한 줄 — 갈래 겹침 + 규격 수 같음 · 괄호 낱말로 하나일 때만."""
|
||||
kinds = set(kinds or ()) or mtypes(text)
|
||||
nums = set(nums or ()) or set(specnums(text))
|
||||
rows = {
|
||||
str(r.get("키")): r
|
||||
for r in m01.loaded()[1].numbers.get("EQ", {}).values()
|
||||
if nums
|
||||
and kinds & mtypes(f"{r.get('상세구분')} {r.get('이름')}")
|
||||
and nums == set(specnums(r.get("규격")))
|
||||
}
|
||||
got = narrow(text, [(k, r.get("이름")) for k, r in rows.items()])
|
||||
return rows[got[0]] if len(got) == 1 else None
|
||||
|
||||
|
||||
def machines(text: str, kinds: set | None = None, nums: set | None = None) -> list[dict]:
|
||||
"""기계 글(이름 규격) → EQ 후보 — 갈래 · 규격 수 없으면 글에서 읽음 · 괄호 낱말 어긋남 깎음."""
|
||||
whole = m01.loaded()[1]
|
||||
kinds = set(kinds or ()) or mtypes(text)
|
||||
nums = set(nums or ()) or set(specnums(text))
|
||||
mine = hm.words(text)
|
||||
rows = []
|
||||
for row in whole.numbers.get("EQ", {}).values():
|
||||
types = mtypes(f"{row.get('상세구분')} {row.get('이름')}")
|
||||
if not kinds & types:
|
||||
continue
|
||||
spec = set(specnums(row.get("규격")))
|
||||
score = len(kinds & types) / len(kinds | types)
|
||||
score += 1.0 if nums and nums == spec else 0.5 if nums & spec else 0.0
|
||||
score += 0.3 * len(mine & hm.words(row.get("이름")))
|
||||
ok, part = _form(text, row.get("이름"))
|
||||
score -= (0.0 if ok else 1.0) + (0.5 if part else 0.0)
|
||||
one = {"키": str(row.get("키")), "갈래": "기계", "이름": row.get("이름")}
|
||||
unit = (row.get("단위") or {}).get("내용시간") if isinstance(row.get("단위"), dict) else ""
|
||||
rows.append(
|
||||
(score, {**one, "규격": row.get("규격") or "", "단위": unit or "시간", "단가": None})
|
||||
)
|
||||
return _order(rows, None)
|
||||
def machines(text: str, *_: Any) -> list[dict]:
|
||||
"""기계 글 → EQ 후보(보여주기만 · 812-9) — 이음 · 후보 여럿 = 그것 · 못 찾음 = 같은 기종(가까운 규격 먼저)."""
|
||||
return [_eq(one) for one in mfind.find(text)["후보"][:TOP]]
|
||||
|
||||
|
||||
def lost(one: dict[str, Any], want: float | None = None) -> list[dict[str, Any]]:
|
||||
|
||||
@@ -20,17 +20,17 @@ from decimal import Decimal
|
||||
from typing import Any
|
||||
|
||||
from M01_MasterData import M01_MasterData_Store as m01
|
||||
from M02_MasterTemplete import M02_Hopyo_Elem as elem
|
||||
from M02_MasterTemplete import M02_Hopyo_Match as hm
|
||||
from M02_MasterTemplete import M02_Hopyo_Material as hmat
|
||||
from M02_MasterTemplete import M02_Hopyo_Shares as shares
|
||||
from M02_MasterTemplete import M02_Labor_Doc as labor
|
||||
from M02_MasterTemplete import M02_Labor_Fixed as fixed
|
||||
from M02_MasterTemplete.M02_Hopyo_Words import mtypes, specnums
|
||||
from M02_MasterTemplete import M02_Machine_Find as mfind
|
||||
|
||||
COSTS = ("노무비", "재료비", "경비")
|
||||
_BASE = re.compile(r"(?<!주)(노무비|재료비|경비|인건비)\s*의")
|
||||
GAP = Decimal(1) # 밑수 · 단가 같음 판정 끝수(원)
|
||||
_RATE = re.compile(r"\s*\(\s*\d+(?:\.\d+)?\s*%\s*\)\s*$") # 「…[hr] (90%)」 꼬리
|
||||
|
||||
|
||||
_d, _plain = fixed._num, fixed._plain
|
||||
@@ -93,13 +93,9 @@ class Linker:
|
||||
pair = (str(row.get("키")), _d(row.get("값")))
|
||||
self.labor.setdefault(str(row.get("이름") or ""), []).append(pair)
|
||||
self.machines = [
|
||||
(str(row.get("키")), mtypes(f"{row.get('상세구분')} {row.get('이름')}"),
|
||||
set(specnums(row.get("규격"))), mm_plain(row.get("이름")), mm_plain(row.get("규격")))
|
||||
(str(row.get("키")), mm_plain(row.get("이름")), mm_plain(row.get("규격")))
|
||||
for row in self.whole.numbers.get("EQ", {}).values()
|
||||
] # fmt: skip
|
||||
self.eq_names = {
|
||||
str(r.get("키")): r.get("이름") for r in self.whole.numbers.get("EQ", {}).values()
|
||||
}
|
||||
]
|
||||
self.near = 0 # 못 이은 이음 후보 수
|
||||
|
||||
def link(self, line: dict[str, Any], price: dict[str, Decimal]) -> dict[str, str | None]:
|
||||
@@ -117,14 +113,17 @@ class Linker:
|
||||
return out
|
||||
|
||||
def _eqs(self, line: dict[str, Any]) -> list[str]:
|
||||
"""기계 줄 → EQ 후보 — 이름 · 규격 글 같음 › 갈래 + 규격 수 · 괄호 낱말."""
|
||||
"""기계 줄 → EQ 후보 — 이름 · 규격 글 같음 › 기계 찾기(812-9 `M02_Machine_Find`)."""
|
||||
name, spec = mm_plain(line.get("이름")), mm_plain(line.get("규격"))
|
||||
same = [c for c, _, _, n, s in self.machines if n == name and s == spec]
|
||||
same = [c for c, n, s in self.machines if n == name and s == spec]
|
||||
if len(same) != 1:
|
||||
text = f"{line.get('이름') or ''} {line.get('규격') or ''}"
|
||||
kinds, nums = mtypes(text), set(specnums(text))
|
||||
same = [c for c, t, s, _, _ in self.machines if kinds & t and nums and nums == s]
|
||||
same = elem.narrow(text, [(c, self.eq_names.get(c)) for c in same])
|
||||
text = str(line.get("이름") or "")
|
||||
# 엑셀 호표 글 「운 반 : 덤프트럭(4.5Ton)」 · 「굴착기(…)[hr] : 90%」 = 쌍점 조각마다
|
||||
parts = [text, *(text.split(":") if ":" in text else [])]
|
||||
tries = [mfind.find(_RATE.sub("", p).strip(), line.get("규격")) for p in parts]
|
||||
got = next((g for g in tries if g["판정"] in (mfind.LINKED, mfind.ALIAS)), None)
|
||||
got = got or next((g for g in tries if g["판정"] != mfind.NONE), tries[0])
|
||||
same = [] if got["판정"] == mfind.NONE else [c["키"] for c in got["후보"]]
|
||||
return same
|
||||
|
||||
def candidates(self, line: dict[str, Any]) -> list[str]:
|
||||
|
||||
@@ -11,7 +11,8 @@
|
||||
(줄 `불러오기` = `doc_id`)만 · 회사 · 개인 층 짝 = 「내 제안」(`제안` True · 자동 아님 ·
|
||||
후보 맨 위 · M01 승인 대기).
|
||||
읽는 차례 = 자동 정확 열쇠(같은 층 → 위 층) → 제안 정확 열쇠 → 품명|단위 느슨 열쇠(`규격다름`).
|
||||
쓰는 때 = [확인하고 사전조합 만들기] · 장부 화면 [저장]만 · 쓰기 = 그 층만(권한은 길 몫) ·
|
||||
⚠ 815-2 — 장부 화면 · M01 승인 · 불러오기 확인 때 쓰기는 걷음(별칭 = M01 요소 「별칭」 칸) ·
|
||||
남은 것 = 짝 검사 · 열쇠 도우미 · 요소 잇기의 옛 장부 읽기(sub3 815-1 이 뗌) · 쓰기 = 그 층만 ·
|
||||
짝은 서버가 다시 검사(돈 로직 · 그 로직 입력 이름 · M01 자재품목 · 배 0 넘음 · 몫 (0, 1]).
|
||||
"""
|
||||
|
||||
@@ -137,39 +138,6 @@ def recall(
|
||||
return None
|
||||
|
||||
|
||||
def _states(rows_: list[dict[str, Any]]) -> None:
|
||||
"""내 제안 승인 상태(739-1) — 마스터 줄 = 승인 · 아래 층 줄 = 마스터에 같은 짝(서명) 승인 ·
|
||||
M01 반려 기억 반려 · 그 밖 대기(M01 별칭 장부 `sign` · 반려 기억 그대로)."""
|
||||
from M01_MasterData import M01_MasterData_Alias as alias # M01 이 장부를 부름 — 늦게 읽음
|
||||
|
||||
master = read(store.FOLDER)
|
||||
gone = alias.rejected()
|
||||
for row in rows_:
|
||||
if row["층"] == "system":
|
||||
row["상태"] = "승인"
|
||||
continue
|
||||
sig, mine = alias.sign(alias.ALIAS, row), master.get(row["열쇠"])
|
||||
if mine and alias.sign(alias.ALIAS, mine) == sig:
|
||||
row["상태"] = "승인"
|
||||
elif alias._reject_id(alias.ALIAS, row["열쇠"], sig) in gone:
|
||||
row["상태"] = "반려"
|
||||
else:
|
||||
row["상태"] = "대기"
|
||||
|
||||
|
||||
def rows(folder: str | Path, writable: bool) -> list[dict[str, Any]]:
|
||||
"""장부 화면 — 이 층 + 위 층 줄 `{열쇠, …줄, 층, 주인, 고침, 상태}`(고침 = 이 층 · 쓰기 권한 ·
|
||||
상태 = 승인 · 대기 · 반려)."""
|
||||
out = []
|
||||
for at, (layer, owner, book) in enumerate(books(folder)):
|
||||
for full, row in sorted(book.items()):
|
||||
out.append(
|
||||
{"열쇠": full, **row, "층": layer, "주인": owner, "고침": writable and not at}
|
||||
)
|
||||
_states(out)
|
||||
return out
|
||||
|
||||
|
||||
# ── 검사 · 쓰기 ───────────────────────────────────────────────────────
|
||||
def _bad(text: str) -> store.StoreError:
|
||||
return store.StoreError(400, text)
|
||||
@@ -350,9 +318,9 @@ def write(
|
||||
|
||||
|
||||
def put(folder: str | Path, entries: Any, who: Any) -> dict[str, Any]:
|
||||
"""장부 화면 [저장] — `{열쇠: 줄 | None}` 재검사 뒤 이 층에 씀 → `{씀, 줄}`."""
|
||||
"""`{열쇠: 줄 | None}` 재검사 뒤 이 층에 씀 → `{씀}`(장부 화면 · M01 승인은 걷음 · 815-2)."""
|
||||
if not isinstance(entries, dict) or not entries or len(entries) > 2000:
|
||||
raise _bad("저장할 줄이 없음")
|
||||
known = _known()
|
||||
clean = {str(k): None if v is None else check(str(k), v, known) for k, v in entries.items()}
|
||||
return {"씀": write(folder, clean, who), "줄": rows(folder, True)}
|
||||
return {"씀": write(folder, clean, who)}
|
||||
|
||||
@@ -0,0 +1,256 @@
|
||||
"""M02 불러오기 재료 표기 다름 찾기(PLAN 817-1 · 요구 773-5) — `M02_Hopyo_Row._m01_picks` 재료 길이 부름.
|
||||
|
||||
서버 단독 · 읽기만 · 별칭 · 정확 일치 · 바탕 이름 유일 짝(`M02_Hopyo_Material.sure`)이 못 찾은 줄만.
|
||||
- `same(줄)` ㉮ 이음 = 이름이 같은 품목(표기만 다름) · 규격 수 묶음 같음 · 규격 낱말 안 부딪힘 · 그런 우리
|
||||
품목 하나(지역 · 관급/사급 줄은 한 품목) → 그 자재 줄 · 아니면 None.
|
||||
- `narrow(줄)` ㉯ 후보 좁힘 = 이름 같은 품목 묶음을 규격 낱말(어근 「초본류」 → 초본)을 품은 줄 · 수 묶음
|
||||
닮은 줄 · 덧말 적은 줄 차례로(자동 이음 아님 · 1 순위 맨 위).
|
||||
- `spec_first(후보, 줄)` = 후보 가운데 규격 수 묶음(둘 이상)이 줄과 같은 것 먼저.
|
||||
|
||||
이름 같음: 바탕(괄호 · 지역 · 관급/사급 꼬리 · 등급 「1급」 뗌 · 빈칸 · 점 「P.V.C」 · 소리 표기 `SPELL`) 같음 ·
|
||||
괄호 차례 「패널(유로폼)」 = 「유로폼패널」(글자 묶음 같음) · 괄호 이름 「서비스볼트(고장력볼트)」 = 고장력볼트 ·
|
||||
(약한 같음 — 수 묶음이 같을 때만) 끝 낱말 같음 「지수판」 ↔ 「PVC지수판」 · 앞 낱말 「구역화물자동차」 ↔ 구역화물 ·
|
||||
규격에 든 이름 「안고초」 ↔ 산림종자 안고초(가져온 규격 빔).
|
||||
수 묶음: 규격 + 이름 괄호의 수(차례 무관) · KS 번호 · 무게(kg · ㎏/포 · m/kg) · 제곱 꼬리(㎡ · ㎠) 뗌 ·
|
||||
천 단위 쉼표 · 같음 > 낱마다 단위 배(1 · 10 · 100 · 1000 — 3.6×3.0×3.0 m = 30×30×3600 mm) > 한쪽이 다른 쪽에 듦
|
||||
(이름 같을 때만 · 덧글 「2000 길이 · 20~30mm」).
|
||||
규격 낱말 부딪힘 = 두 쪽 다 괄호 · 규격 한글 낱말이 있고 두 글자 묶음이 하나도 안 겹침(「연강용」 ↔ 「주철용」) —
|
||||
상대 이름에 든 낱말은 뺌.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import unicodedata
|
||||
from functools import lru_cache
|
||||
from itertools import permutations
|
||||
from typing import Any
|
||||
|
||||
from M02_MasterTemplete import M02_Hopyo_Match as hm
|
||||
from M02_MasterTemplete import M02_Hopyo_Material as hmat
|
||||
|
||||
SPELL = (
|
||||
(r"칠렌", "틸렌"),
|
||||
(r"매쉬", "메쉬"),
|
||||
(r"신나(?![가-힣])", "신너"),
|
||||
(r"pipe", "파이프"),
|
||||
) # 소리 · 영문 표기
|
||||
NOISE = frozenset({"이내", "이하", "이상", "국산", "포함"})
|
||||
SCALES = (1, 10, 100, 1000)
|
||||
_STEM = re.compile(r"(?<=..)[류형용]$")
|
||||
_KS = re.compile(r"ks\s*[a-z]?\s*-?\s*\d+")
|
||||
_SQUARE = re.compile(r"(?:c|m)?m[23](?![\d.])")
|
||||
_THOUSAND = re.compile(r"(?<=\d),(?=\d{3}(?!\d))")
|
||||
_NUM = re.compile(r"\d+(?:\.\d+)?")
|
||||
_PAREN = re.compile(r"\(([^()]*)\)")
|
||||
|
||||
|
||||
def _low(text: Any) -> str:
|
||||
t = unicodedata.normalize("NFKC", str(text or "")).lower().replace("m/m", "mm")
|
||||
return hmat._APART.sub("", t)
|
||||
|
||||
|
||||
def _tidy(text: str) -> str:
|
||||
t = re.sub(r"(?<=[a-z])[.\s]+(?=[a-z])", "", text)
|
||||
for a, b in SPELL:
|
||||
t = re.sub(a, b, t)
|
||||
t = re.sub(r"\d+급", "", t)
|
||||
return re.sub(r"[^가-힣a-z0-9]", "", t)
|
||||
|
||||
|
||||
def _bag(text: str) -> tuple[float, ...]:
|
||||
t = _THOUSAND.sub("", _SQUARE.sub("", _KS.sub("", text)))
|
||||
return tuple(sorted(float(x) for x in _NUM.findall(t)))
|
||||
|
||||
|
||||
def _quals(text: str) -> frozenset[str]:
|
||||
words = (w for w in re.findall(r"[가-힣]{2,}", text) if not hmat._region(w))
|
||||
return frozenset(w for w in words if w not in hmat.SUPPLY and w not in NOISE)
|
||||
|
||||
|
||||
def _item(name: Any, spec: Any) -> dict[str, Any]:
|
||||
"""이름 · 규격 → 바탕 · 괄호 조각 · 수 묶음 · 규격 낱말 · 온 이름 글."""
|
||||
low, spec = _low(name), _low(spec)
|
||||
parens = _PAREN.findall(low)
|
||||
pieces = tuple(p for p in (_tidy(x) for x in parens for x in x.split(",")) if p)
|
||||
core = _tidy(hmat._bare(low))
|
||||
return {
|
||||
"core": core,
|
||||
"pieces": pieces,
|
||||
"chars": "".join(sorted(core + "".join(pieces))),
|
||||
"bag": _bag(f"{spec} {' '.join(parens)}"),
|
||||
"quals": _quals(f"{' '.join(pieces)} {spec}"),
|
||||
"whole": core + "".join(pieces),
|
||||
"spec": _tidy(spec),
|
||||
}
|
||||
|
||||
|
||||
@lru_cache(maxsize=2)
|
||||
def _book(_got: int) -> dict[str, Any]:
|
||||
rows, _ = hmat.pool()
|
||||
out: list[dict[str, Any]] = []
|
||||
by: dict[str, dict[str, list[int]]] = {
|
||||
"core": {},
|
||||
"chars": {},
|
||||
"piece": {},
|
||||
"tail": {},
|
||||
"spec": {},
|
||||
}
|
||||
for at, r in enumerate(rows):
|
||||
one = {**_item(r["one"]["이름"], r["one"]["규격"]), "row": r}
|
||||
out.append(one)
|
||||
c = one["core"]
|
||||
by["core"].setdefault(c, []).append(at)
|
||||
by["chars"].setdefault(one["chars"], []).append(at)
|
||||
by["spec"].setdefault(one["spec"], []).append(at)
|
||||
for p in one["pieces"]:
|
||||
by["piece"].setdefault(p, []).append(at)
|
||||
for i in range(1, len(c) - 1): # 끝 낱말(두 글자 이상)
|
||||
by["tail"].setdefault(c[i:], []).append(at)
|
||||
return {"줄": out, "찾기": by}
|
||||
|
||||
|
||||
def book() -> dict[str, Any]:
|
||||
return _book(id(hm.targets()))
|
||||
|
||||
|
||||
def _related(me: dict[str, Any]) -> dict[int, bool]:
|
||||
"""이름 같은 우리 줄 → {차례: 굳셈(True) · 약함(False)}."""
|
||||
by, c = book()["찾기"], me["core"]
|
||||
strong = [*by["core"].get(c, []), *by["chars"].get(me["chars"], []), *by["piece"].get(c, [])]
|
||||
if (
|
||||
c not in by["core"]
|
||||
): # 괄호 이름(「서비스볼트(고장력볼트)」) — 바탕 이름이 우리 표에 없을 때만
|
||||
strong += [i for p in me["pieces"] for i in by["core"].get(p, [])]
|
||||
weak = (
|
||||
[*by["tail"].get(c, [])] if len(c) >= 2 else []
|
||||
) # 우리 이름 끝 = 가져온 이름(「PVC지수판」)
|
||||
for k in range(
|
||||
2, len(c) - 1
|
||||
): # 우리 이름 = 가져온 이름 앞(「구역화물자동차」) · 뒤 덧말이 우리 낱말 아님
|
||||
if c[k:] not in by["core"] and c[k:] not in by["tail"]:
|
||||
weak += by["core"].get(c[:k], [])
|
||||
out = dict.fromkeys(weak, False)
|
||||
out.update(dict.fromkeys(strong, True))
|
||||
return out
|
||||
|
||||
|
||||
def _bigrams(words: frozenset[str]) -> set[str]:
|
||||
return {w[i : i + 2] for w in words for i in range(len(w) - 1)}
|
||||
|
||||
|
||||
def _qual_fit(me: dict[str, Any], x: dict[str, Any]) -> tuple[bool, bool, bool]:
|
||||
"""(부딪힘, 겹침, 낱말 있음) — 상대 온 이름에 든 낱말은 뺌."""
|
||||
a = frozenset(w for w in me["quals"] if _tidy(w) not in x["whole"])
|
||||
b = frozenset(w for w in x["quals"] if _tidy(w) not in me["whole"])
|
||||
share = bool(_bigrams(a) & _bigrams(b))
|
||||
return bool(a and b and not share), share, bool(a or b)
|
||||
|
||||
|
||||
def _near(x: float, y: float) -> bool:
|
||||
return any(abs(x * s - y) <= 1e-6 * max(abs(y), 1) or abs(y * s - x) <= 1e-6 * max(abs(x), 1) for s in SCALES) # fmt: skip
|
||||
|
||||
|
||||
def num_fit(a: tuple[float, ...], b: tuple[float, ...]) -> tuple[int, int]:
|
||||
"""(갈래 3 같음 · 2 단위 배 · 1 한쪽이 듦 · 0 다름, 맞은 수 개수)."""
|
||||
if a == b:
|
||||
return 3, len(a)
|
||||
if a and len(a) == len(b) and len(a) <= 6:
|
||||
if any(all(_near(x, y) for x, y in zip(a, p)) for p in permutations(b)):
|
||||
return 2, len(a)
|
||||
small, big = sorted((list(a), list(b)), key=len)
|
||||
if small and len(small) < len(big):
|
||||
rest = list(big)
|
||||
for x in small:
|
||||
if x not in rest:
|
||||
return 0, 0
|
||||
rest.remove(x)
|
||||
return 1, len(small)
|
||||
return 0, 0
|
||||
|
||||
|
||||
def _fold(x: dict[str, Any]) -> tuple:
|
||||
r = x["row"]
|
||||
return r["base"], r["key"][0], r["key"][2]
|
||||
|
||||
|
||||
def _scored(line: dict[str, Any]) -> tuple[dict[str, Any], list[tuple[tuple, dict[str, Any]]]]:
|
||||
me = _item(line.get("이름"), line.get("규격"))
|
||||
rows = book()["줄"]
|
||||
out = []
|
||||
for at, strong in _related(me).items():
|
||||
x = rows[at]
|
||||
clash, share, words = _qual_fit(me, x)
|
||||
if not clash:
|
||||
out.append(
|
||||
((strong, share, *num_fit(me["bag"], x["bag"]), words, x["core"] == me["core"]), x)
|
||||
)
|
||||
return me, out
|
||||
|
||||
|
||||
def _pick(group: list[dict[str, Any]], line: dict[str, Any]) -> dict[str, Any]:
|
||||
"""한 품목의 변형 줄들 → 하나(줄 관급/사급 › 줄 지역 › 서울 › 같은 단위 › 엑셀 차례 — `M02_Elem_Alias.one` 과 같은 차례)."""
|
||||
supply, regions = hmat._wants(line)
|
||||
unit = hm.mm.plain_unit(line.get("단위"))
|
||||
|
||||
def rank(r: dict[str, Any]) -> tuple:
|
||||
return (
|
||||
supply is not None and r["supply"] not in (None, supply),
|
||||
bool(regions) and not hmat._hit(r, regions),
|
||||
"서울" not in r["region"],
|
||||
r["key"][1] != unit,
|
||||
r["at"],
|
||||
)
|
||||
|
||||
return min((x["row"] for x in group), key=rank)["one"]
|
||||
|
||||
|
||||
def same(line: dict[str, Any]) -> dict[str, Any] | None:
|
||||
"""㉮ 표기만 다름 — 우리 품목 하나면 그 자재 줄(`hm.targets` 꼴) · 아니면 None."""
|
||||
me, scored = _scored(line)
|
||||
if not me["spec"]: # 가져온 규격 빔 = 규격에 든 이름(「안고초」)만 · 그 밖은 ㉯
|
||||
hits = [book()["줄"][i] for i in book()["찾기"]["spec"].get(me["whole"], [])]
|
||||
hits = [x for x in hits if not x["bag"] and not me["bag"]]
|
||||
return _pick(hits, line) if hits and len({_fold(x) for x in hits}) == 1 else None
|
||||
ok = []
|
||||
for (strong, share, tier, common, words, same_core), x in scored:
|
||||
if not tier or (tier == 1 and not strong):
|
||||
continue
|
||||
if not me["bag"] and not x["bag"] and not share and words:
|
||||
continue # 수 없는 규격 = 규격 낱말이 겹쳐야
|
||||
ok.append(((tier, common, share, strong, same_core), x))
|
||||
if not ok:
|
||||
return None
|
||||
best = max(k for k, _ in ok)
|
||||
group = [x for k, x in ok if k == best]
|
||||
return _pick(group, line) if len({_fold(x) for x in group}) == 1 else None
|
||||
|
||||
|
||||
def _stems(me: dict[str, Any]) -> set[str]:
|
||||
return {_STEM.sub("", w) for w in me["quals"]}
|
||||
|
||||
|
||||
def narrow(line: dict[str, Any], limit: int = 5) -> list[dict[str, Any]]:
|
||||
"""㉯ 이름 같은 품목 묶음(굳센 같음) → 규격 낱말 어근 품은 줄 › 수 묶음 닮음 › 덧말 적음 차례(품목마다 하나)."""
|
||||
me, scored = _scored(line)
|
||||
stems = _stems(me)
|
||||
rows = [(k, x) for k, x in scored if k[0]]
|
||||
rank = []
|
||||
for (_strong, _share, tier, common, _words, same_core), x in rows:
|
||||
text = x["whole"] + x["spec"]
|
||||
hit = sum(s in text for s in stems)
|
||||
rank.append(((-hit, -tier, -common, not same_core, len(x["spec"]), x["row"]["at"]), x))
|
||||
seen, out = set(), []
|
||||
for _k, x in sorted(rank, key=lambda t: t[0]):
|
||||
if (f := _fold(x)) not in seen:
|
||||
seen.add(f)
|
||||
out.append(_pick([y for _, y in rank if _fold(y) == f], line))
|
||||
return out[:limit]
|
||||
|
||||
|
||||
def spec_first(cands: list[dict[str, Any]], line: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
"""후보 차례 — 규격 수 묶음(둘 이상)이 줄과 같은 것 먼저(「중하중그레이팅 395×995×52」)."""
|
||||
mine = _item(line.get("이름"), line.get("규격"))["bag"]
|
||||
if len(mine) < 2:
|
||||
return cands
|
||||
return sorted(cands, key=lambda c: _item(c.get("이름"), c.get("규격"))["bag"] != mine)
|
||||
@@ -4,7 +4,7 @@
|
||||
엑셀 보기 머리 · 목록 = `View.listing`(줄 표지) · 옮기기 = [내 일위대가로] `hopyo-build-all {번}`(807-1).
|
||||
옛 판정 · 검산 · 옮기기 · 근거 제안 길(785-5 `hopyo-judge` · `check` · `move` · `price` · 793-3a `hopyo-basis`)은
|
||||
809-1 에서 지움(요구 703 — 기초로 바꾸기 없음).
|
||||
그대로 쓰는 길(목록 · 올림 · 엑셀 보기 머리 · 격자 · 지우기 · 장부 · 모두 고정 · 붙여넣기 · 다시 잇기 ·
|
||||
그대로 쓰는 길(목록 · 올림 · 엑셀 보기 머리 · 격자 · 지우기 · 모두 고정 · 붙여넣기 · 다시 잇기 ·
|
||||
구조물 조합 · 칠 = B09 · M01 이 씀) · 옛 맞춤 길(호표 하나 맞춤 · 미리 셈 · 실무 호표 · 확인 만들기 · 안 이은 줄 ·
|
||||
단가산출 만들기)은 785-7 에서 지움.
|
||||
층 권한 = `Router_Items._root` · ⚠ 등록은 `main.py`(로그인만).
|
||||
@@ -25,13 +25,11 @@ from M02_MasterTemplete import M02_Hopyo_Excel as hopyo_excel
|
||||
from M02_MasterTemplete import M02_Hopyo_Forget as hopyo_forget
|
||||
from M02_MasterTemplete import M02_Hopyo_Grid as hopyo_grid
|
||||
from M02_MasterTemplete import M02_Hopyo_Item as hopyo_item
|
||||
from M02_MasterTemplete import M02_Hopyo_Ledger as hopyo_ledger
|
||||
from M02_MasterTemplete import M02_Hopyo_Marks as hopyo_marks
|
||||
from M02_MasterTemplete import M02_Hopyo_View as hopyo_view_
|
||||
from M02_MasterTemplete import M02_Hopyo_Paste as hopyo_paste
|
||||
from M02_MasterTemplete import M02_Hopyo_Relink as hopyo_relink
|
||||
from M02_MasterTemplete import M02_Hopyo_Store as hopyo_store
|
||||
from M02_MasterTemplete import M02_Layer_Access as access
|
||||
from M02_MasterTemplete import M02_MasterTemplete_Store as store
|
||||
from M02_MasterTemplete import M02_Sangeun_FromHopyo as from_hopyo
|
||||
from M02_MasterTemplete.M02_MasterTemplete_Router_Items import Layer, Session, _call, _root
|
||||
@@ -82,13 +80,9 @@ def _upload(name: str, data: bytes, who: dict[str, Any]) -> dict[str, Any]:
|
||||
grid = hopyo_grid.read(name, data, parsed)
|
||||
except Exception: # 격자는 보기용 덤 — 못 만들어도 올리기는 됨(보기 창 404)
|
||||
grid = None
|
||||
row = hopyo_store.save(store.root(), name, parsed, who, grid, mark)
|
||||
cannot = from_hopyo.keep(
|
||||
store.root(), row["키"], name, data
|
||||
) # 산근 시트(793-3b 가져온 식 열기)
|
||||
return (
|
||||
{**row, "읽기불가": cannot} if cannot else row
|
||||
) # 797-1 가져온 식 못 씀 사유 → 화면 토스트
|
||||
# 산근 시트(793-3b 가져온 식 열기) = HP 를 목록에 보이기 전에 보관(814-1) · 못 씀 사유 → 화면 토스트(797-1)
|
||||
keep = lambda key: from_hopyo.keep(store.root(), key, name, data) # noqa: E731
|
||||
return hopyo_store.save(store.root(), name, parsed, who, grid, mark, keep)
|
||||
|
||||
|
||||
@router.get("/layers/{layer}/hopyo")
|
||||
@@ -211,35 +205,6 @@ def hopyo_make_all(
|
||||
return _call(folder, hopyo_build.build_all, *args)
|
||||
|
||||
|
||||
@router.get("/layers/{layer}/hopyo-ledger")
|
||||
def hopyo_ledger_read(
|
||||
layer: Layer, owner: str | None = None, session: Session = Depends(verify_session)
|
||||
) -> dict:
|
||||
"""별칭 장부 — 이 층 + 위 층 줄 `{열쇠, 원래, 갈래, 짝, 누가, 때, 처음파일, 층, 주인, 고침}`
|
||||
(고침 = 이 층 · 쓰기 권한)."""
|
||||
folder = _root(session, layer, owner)
|
||||
writable = access.grant(session, layer, owner).write
|
||||
return {"줄": _call(folder, hopyo_ledger.rows, folder, writable)}
|
||||
|
||||
|
||||
class LedgerBody(_Strict):
|
||||
"""`줄` = {열쇠: 장부 줄 | null(지움)} — 이 층만 · 서버가 짝을 다시 검사."""
|
||||
|
||||
줄: dict[str, Any]
|
||||
|
||||
|
||||
@router.put("/layers/{layer}/hopyo-ledger")
|
||||
def hopyo_ledger_write(
|
||||
layer: Layer,
|
||||
body: LedgerBody,
|
||||
owner: str | None = None,
|
||||
session: Session = Depends(verify_session),
|
||||
) -> dict:
|
||||
"""별칭 장부 [저장] — 고친 줄 · 지운 줄(층 쓰기) → `{씀, 줄}`."""
|
||||
folder = _root(session, layer, owner, write=True)
|
||||
return _call(folder, hopyo_ledger.put, folder, body.줄, _who(session))
|
||||
|
||||
|
||||
@router.delete("/layers/{layer}/hopyo/{key}")
|
||||
def hopyo_delete(
|
||||
layer: Layer, key: str, owner: str | None = None, session: Session = Depends(verify_session)
|
||||
|
||||
@@ -20,6 +20,7 @@ from M02_MasterTemplete import M02_Hopyo_Elem as elem
|
||||
from M02_MasterTemplete import M02_Hopyo_Match as hm
|
||||
from M02_MasterTemplete import M02_Hopyo_Material as hmat
|
||||
from M02_MasterTemplete import M02_Hopyo_ReadEst as est
|
||||
from M02_MasterTemplete import M02_Machine_Find as mfind
|
||||
|
||||
mm, mp = hm.mm, hm.mp
|
||||
COSTS = ("노무비", "재료비", "경비")
|
||||
@@ -65,12 +66,19 @@ def _rows(table: str) -> dict[str, dict[str, Any]]:
|
||||
|
||||
def m01_picks(kind: str, line: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
"""갈래 하나 → M01 고른 것 후보(LB · MT · MO · EQ · FX) — 유일 짝이면 하나 · 같은 글 한 번."""
|
||||
if kind == "기계": # 812-9 기계 찾기 — 별칭 장부가 바뀌므로 lru 밖(찾기가 기계표를 기억)
|
||||
got = mfind.find(line.get("이름"), line.get("규격"))
|
||||
rows = [] if got["판정"] == mfind.NONE else got["후보"]
|
||||
unit = str(line.get("단위") or "") or "hr"
|
||||
return [pick("기계", x["키"], x["이름"], x.get("규격"), unit) for x in rows]
|
||||
text = tuple(str(line.get(k) or "") for k in ("이름", "규격", "단위"))
|
||||
return copy.deepcopy(_m01_picks(kind, *text, id(m01.loaded()[1])))
|
||||
|
||||
|
||||
@lru_cache(maxsize=4096)
|
||||
def _m01_picks(kind: str, name: str, spec: str, unit: str, _whole: int) -> list[dict[str, Any]]:
|
||||
if kind in ("인력", "재료") and (named := _alias(kind, name, spec)):
|
||||
return named # 815-1 K2 — M01 줄 「별칭」 칸 먼저
|
||||
if kind == "인력":
|
||||
trade = est.trade(name)
|
||||
hit = [r for r in _rows("LB").values() if trade and r.get("이름") == trade]
|
||||
@@ -80,13 +88,6 @@ def _m01_picks(kind: str, name: str, spec: str, unit: str, _whole: int) -> list[
|
||||
]
|
||||
got = elem.labor(str(name or ""))
|
||||
return [pick("인력", x["키"], x["이름"], "", "인", 단가=x["단가"]) for x in got]
|
||||
if kind == "기계":
|
||||
text = f"{name} {spec}"
|
||||
one = elem.machine(text)
|
||||
rows = [one] if one else []
|
||||
got = [{"키": r["키"], "이름": r["이름"], "규격": r.get("규격")} for r in rows]
|
||||
got = got or elem.machines(text)
|
||||
return [pick("기계", x["키"], x["이름"], x.get("규격"), unit or "hr") for x in got]
|
||||
if kind == "유가":
|
||||
return [
|
||||
pick("유가", x["키"], x["이름"], x["규격"], x["단위"], 단가=x["단가"])
|
||||
@@ -102,7 +103,7 @@ def _m01_picks(kind: str, name: str, spec: str, unit: str, _whole: int) -> list[
|
||||
clean, apart = hmat.clean({"이름": name, "규격": spec, "단위": unit})
|
||||
sure, _ = hmat.sure(clean)
|
||||
got = [r["one"] for r in sure[:1] if not r["one"].get("전국")]
|
||||
got = got or hmat.candidates(clean, TOP)
|
||||
got = got or _found(clean)
|
||||
more = {"별도계상": True} if apart else {}
|
||||
return [
|
||||
pick("재료", x.get("자재"), x["이름"], x.get("규격"), x.get("단위"), **more)
|
||||
@@ -112,6 +113,40 @@ def _m01_picks(kind: str, name: str, spec: str, unit: str, _whole: int) -> list[
|
||||
return []
|
||||
|
||||
|
||||
def _found(line: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
"""817-1 재료 — ㉮ 표기만 다름 하나 › 옛 후보 하나 › ㉯ 이름 같은 묶음 좁힘 + 규격 수 같은 후보 먼저 + 옛 후보."""
|
||||
from M02_MasterTemplete import M02_Hopyo_MatSame as ms
|
||||
|
||||
if one := ms.same(line):
|
||||
return [one]
|
||||
cands = hmat.candidates(line, 4 * TOP)
|
||||
if len({x.get("자재") for x in cands[:TOP]}) == 1:
|
||||
return cands[:TOP]
|
||||
out, seen = [], set()
|
||||
for x in [*ms.narrow(line, TOP), *ms.spec_first(cands, line)[:TOP], *cands[:TOP]]:
|
||||
if x.get("자재") not in seen:
|
||||
seen.add(x.get("자재"))
|
||||
out.append(x)
|
||||
return out
|
||||
|
||||
|
||||
def _alias(kind: str, name: str, spec: str) -> list[dict[str, Any]]:
|
||||
"""M01 별칭 칸 이음(`M02_Elem_Alias`) — 인력 = 이름 · 재료 = 이름 + 규격 › 표기 정규화 정확 일치 ·
|
||||
같은 품목 지역 줄은 하나."""
|
||||
from M02_MasterTemplete import M02_Elem_Alias as ea
|
||||
|
||||
table = "LB" if kind == "인력" else ea.MARKET
|
||||
keys = ea.keys(table, name, "" if kind == "인력" else spec)
|
||||
if kind == "재료" and len(keys) > 1:
|
||||
keys = [k] if (k := ea.one({"이름": name, "규격": spec}, keys)) else keys
|
||||
if kind == "재료" and not keys: # 815-1 K3 — 표기 정규화 정확 일치(바탕 이름 길보다 먼저)
|
||||
keys = [k] if (k := ea.one({"이름": name, "규격": spec}, ea.exact(name, spec))) else []
|
||||
rows = [(k, ea.row(table, k)) for k in keys]
|
||||
if kind == "인력":
|
||||
return [pick("인력", k, r.get("이름"), "", "인", 단가=_plain(r.get("값"))) for k, r in rows]
|
||||
return [pick("재료", k, r.get("이름"), r.get("규격"), r.get("단위")) for k, r in rows]
|
||||
|
||||
|
||||
# ── M02 층 SK · SC(같은 이름 · 참고) ────────────────────────────────────────
|
||||
|
||||
|
||||
|
||||
@@ -23,7 +23,6 @@ from common_util.common_util_time import utc_now_text
|
||||
from M01_MasterData import M01_MasterData_Store as m01
|
||||
from M02_MasterTemplete import M02_Hopyo_Elem as elem
|
||||
from M02_MasterTemplete import M02_Hopyo_Fixed as hfix
|
||||
from M02_MasterTemplete import M02_Hopyo_Ledger as ledger
|
||||
from M02_MasterTemplete import M02_Hopyo_Store as hopyo_store
|
||||
from M02_MasterTemplete import M02_Hopyo_Units as units
|
||||
from M02_MasterTemplete import M02_Labor_Doc as labor
|
||||
@@ -144,9 +143,8 @@ class Maker:
|
||||
self._units: units.Units | None = None
|
||||
|
||||
def units(self) -> units.Units:
|
||||
if self._units is None:
|
||||
books = ledger.books(self.source)
|
||||
self._units = units.Units(books, ledger.doc_id(self.source, self.key))
|
||||
if self._units is None: # 815-1 — 옛 별칭 장부 안 읽음(M01 별칭 칸은 Units 가 읽음)
|
||||
self._units = units.Units()
|
||||
return self._units
|
||||
|
||||
def ref_of(self, sub: dict[str, Any]) -> dict[str, Any] | None:
|
||||
|
||||
@@ -124,8 +124,10 @@ def save(
|
||||
who: Any,
|
||||
grid: dict[str, Any] | None = None,
|
||||
mark: str | None = None,
|
||||
first: Any = None,
|
||||
) -> dict[str, Any]:
|
||||
"""읽은 결과(+ 격자) → 새 키로 씀 → 목록 한 줄 · `mark` = 올린 엑셀 지문."""
|
||||
"""읽은 결과(+ 격자) → 새 키로 씀 → 목록 한 줄 · `mark` = 올린 엑셀 지문 ·
|
||||
`first(키)` = HP 를 목록에 보이기 전에 할 일(산근 보관 — 올린 직후 409 막음 · 814-1) · 돌려준 값 = 답 `읽기불가`."""
|
||||
with _LOCK:
|
||||
have = [
|
||||
int(m.group(1)) for p in _dir(folder).glob("HP-*.json") if (m := _KEY.match(p.stem))
|
||||
@@ -145,9 +147,14 @@ def save(
|
||||
if grid is not None:
|
||||
book = {**grid["책"], "열": key}
|
||||
atomic_write_json(_grid_path(folder, key), {**grid, "책": book})
|
||||
cannot = first(key) if first else None
|
||||
atomic_write_json(_path(folder, key), doc)
|
||||
_put_done(folder, log)
|
||||
return {**brief(doc), **({"지문": mark} if mark else {})}
|
||||
return {
|
||||
**brief(doc),
|
||||
**({"지문": mark} if mark else {}),
|
||||
**({"읽기불가": cannot} if cannot else {}),
|
||||
}
|
||||
|
||||
|
||||
def grid(folder: str | Path, key: str) -> dict[str, Any]:
|
||||
|
||||
@@ -6,13 +6,13 @@
|
||||
L 노무 = 직종 LB(이름 · 옛이름) · 같은 이름 여럿 = 건설업 먼저(작업반장 · 요구 624-1) · 단위 `{노임}`
|
||||
X 기계 = EQ(`Fixed.Linker._machine` · 778-1 괄호 낱말) → 기계경비 기초 로직(`mach_logics` ·
|
||||
778-2) · 단위 `{기계, 로직, 할증?}` · 없는 규격 = 가까운 규격 후보만(`Elem.machines` · 자동 고름 없음)
|
||||
M 자재 = MT(`material` — 별칭 장부 › 이름 · 규격 같음 › 바탕 이름 · 숫자 규격 같음(치수 차례 · 구분자
|
||||
M 자재 = MT(`material` — M01 별칭 칸 › 이름 · 규격 같음 › 바탕 이름 · 숫자 규격 같음(치수 차례 · 구분자
|
||||
무관) · 글자 든 규격 = 후보로만) · 손료율 = 방법 「수량 × 손료율 × 단가」 · 별도 = 단위 `{자재?,
|
||||
별도계상}` · 단위 `{자재}`
|
||||
R 율 = 778-4 꼴 `{기준, 비목}` — 기준 = `기준꼴`(인력 · 재료 묶음 · 노무비 · 줄 목록 `{행: [...]}`) ·
|
||||
우리에 없는 율 항목(설치비 · 제경비)도 율 줄 · 기준 못 찾음 = 고를 것(글 기준 제안)
|
||||
S 하위 = `{하위 {종류 단산|일위, 번}}`(SK · SC 참조) · E 값 = `{값: 금액}`(고정 금액 줄)
|
||||
별칭 장부 짝(자재) = 숫자보다 먼저. 서버 단독 · 읽기만.
|
||||
M01 줄 「별칭」 칸(자재 · 노임 · 기계 = 기계 찾기 안) = 숫자보다 먼저(815-1). 서버 단독 · 읽기만.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -22,7 +22,7 @@ from typing import Any
|
||||
|
||||
from M02_MasterTemplete import M02_Hopyo_Elem as elem
|
||||
from M02_MasterTemplete import M02_Hopyo_Fixed as hfix
|
||||
from M02_MasterTemplete import M02_Hopyo_Ledger as ledger
|
||||
from M02_MasterTemplete import M02_Elem_Alias as ea
|
||||
from M02_MasterTemplete import M02_Hopyo_Match as hm
|
||||
from M02_MasterTemplete import M02_Hopyo_Material as hmat
|
||||
from M02_MasterTemplete import M02_Hopyo_Shares as shares
|
||||
@@ -109,6 +109,16 @@ def material(line: dict[str, Any], got: dict[str, Any], full: bool = True) -> di
|
||||
found = hm._exact(me, "자재", got)
|
||||
if len(found) == 1:
|
||||
return {"상태": LINKED, "찾음": found, "글": "이름 · 규격 같음", "꼬리표": tags, "후보": []}
|
||||
hits = ea.exact(
|
||||
me.get("이름"), me.get("규격")
|
||||
) # 815-1 K3 표기 정규화 같음 한 줄(지역 변형 = 아래 길)
|
||||
if len(hits) == 1:
|
||||
key = hits[0]
|
||||
one = ea.row(ea.MARKET, key)
|
||||
found = [
|
||||
{"자재": key, "이름": one.get("이름"), "규격": one.get("규격"), "단위": one.get("단위")}
|
||||
]
|
||||
return {"상태": LINKED, "찾음": found, "글": "표기 정규화 같음", "꼬리표": tags, "후보": []}
|
||||
tries = [me, *_split(me)]
|
||||
for one in tries:
|
||||
sure, more = hmat.sure(one)
|
||||
@@ -150,7 +160,7 @@ def _text_base(one: dict[str, Any]) -> str:
|
||||
|
||||
|
||||
class Units:
|
||||
"""한 번 잇기 — M01 · 목록 · 장부를 한 번 읽음(`books` = `Ledger.books` · 없으면 장부 안 봄)."""
|
||||
"""한 번 잇기 — M01 · 목록을 한 번 읽음(`books` · `doc` = 옛 장부 자리 · 815-1 뒤 안 씀)."""
|
||||
|
||||
def __init__(self, books: list | None = None, doc: str | None = None) -> None:
|
||||
self.linker = hfix.Linker(by_name=True)
|
||||
@@ -168,6 +178,9 @@ class Units:
|
||||
return {"방법": how, "후보": [], "꼬리표": [], "글": "", **got}
|
||||
|
||||
def _L(self, one: dict[str, Any]) -> dict[str, Any]:
|
||||
named = ea.keys("LB", one.get("직종") or one.get("이름")) # 815-1 M01 별칭 칸 먼저
|
||||
if len(named) == 1:
|
||||
return {"상태": LINKED, "단위": {"노임": named[0]}, "꼬리표": ["별칭"]}
|
||||
name = shares.trade(one.get("직종") or one.get("이름")) or ""
|
||||
rows = self.lb.get(name, [])
|
||||
cands = [{"키": str(r.get("키")), "갈래": "노임", "이름": r.get("이름"),
|
||||
@@ -211,7 +224,7 @@ class Units:
|
||||
pair = self._alias(line)
|
||||
if pair:
|
||||
unit = {"자재": pair} | ({"별도계상": True} if apart else {})
|
||||
return {"상태": LINKED, "방법": how, "단위": unit, "꼬리표": ["별칭 장부 짝"]}
|
||||
return {"상태": LINKED, "방법": how, "단위": unit, "꼬리표": ["별칭"]}
|
||||
got = material(line, self.got)
|
||||
apart = apart or hmat.APART in got["꼬리표"]
|
||||
found = [r for r in got["찾음"] if r.get("자재")]
|
||||
@@ -231,14 +244,9 @@ class Units:
|
||||
"글": got["글"], "꼬리표": got["꼬리표"]} # fmt: skip
|
||||
|
||||
def _alias(self, line: dict[str, Any]) -> str | None:
|
||||
"""별칭 장부 자재 짝(자동 정확 열쇠 · 하나) — 숫자보다 먼저."""
|
||||
if self.books is None:
|
||||
return None
|
||||
got = ledger.recall(line, "", self.books, doc=self.doc)
|
||||
if not got or got.get("제안") or got.get("규격다름") or got.get("갈래") != "짝":
|
||||
return None
|
||||
pairs = [p.get("자재") for p in got.get("짝") or []]
|
||||
return pairs[0] if len(pairs) == 1 and pairs[0] else None
|
||||
"""M01 자재 줄 「별칭」 칸(815-1 · 이름 + 규격 · 같은 품목 지역 줄은 하나) — 숫자보다 먼저."""
|
||||
keys = ea.keys(ea.MARKET, line.get("이름"), line.get("규격"))
|
||||
return keys[0] if len(keys) == 1 else ea.one(line, keys) if keys else None
|
||||
|
||||
def _R(self, one: dict[str, Any]) -> dict[str, Any]:
|
||||
part = rate_part(one.get("이름"))
|
||||
|
||||
@@ -363,7 +363,9 @@ def create(kind: str, doc: dict[str, Any]) -> dict[str, Any]:
|
||||
raise _bad("새 키를 못 잡음 — 다시 누름", 409)
|
||||
|
||||
|
||||
def summary(folder: Path, kind: str, key: str, expand: Expand | None = None) -> dict[str, Any]:
|
||||
def summary(
|
||||
folder: Path, kind: str, key: str, expand: Expand | None = None, files: UseFiles | None = None
|
||||
) -> dict[str, Any]:
|
||||
"""층 목록 한 줄에 붙일 것 — `{제목, 단위, 구분, 상세구분, 연결}`(51-12) · `expand` 면
|
||||
1 단위 기준 계(`기준금액` · 51-4)."""
|
||||
try:
|
||||
@@ -392,8 +394,8 @@ def summary(folder: Path, kind: str, key: str, expand: Expand | None = None) ->
|
||||
except Exception: # noqa: BLE001 — 없는 로직 = 키만(셈 때 까닭)
|
||||
pass
|
||||
out["고친줄수"] = sum(len(doc.get(k) or ()) for k in ("덮개", "더한줄", "뺀줄"))
|
||||
# ponytail: SK 마다 모든 층 훑음(users) — 목록이 느려지면 층 훑기 한 번 셈으로
|
||||
out["쓰는곳수"] = len(users({"층": layer, "주인": owner, "키": key}))
|
||||
# SK 마다 모든 층 훑음(users) — 파일 읽기는 판 기억(`_ids_of`)이라 처음 한 번만 무거움
|
||||
out["쓰는곳수"] = len(users({"층": layer, "주인": owner, "키": key}, files))
|
||||
return out
|
||||
|
||||
|
||||
@@ -403,29 +405,58 @@ def summaries(folder: Path, rows: list[dict[str, Any]]) -> None:
|
||||
from M02_MasterTemplete.M02_Labor_Expand import Expand
|
||||
|
||||
expand = Expand(m01.loaded()[1])
|
||||
files = use_files() if any(row["종류"] == "sangeun" for row in rows) else None
|
||||
for row in rows:
|
||||
if row["종류"] in layers.LIBRARY_KINDS:
|
||||
row.update(summary(folder, row["종류"], row["이름"], expand))
|
||||
row.update(summary(folder, row["종류"], row["이름"], expand, files))
|
||||
|
||||
|
||||
# ── 쓰는 곳 · 지우기 ──────────────────────────────────────────────────
|
||||
def users(ref: Ref) -> list[str]:
|
||||
UseFiles = list[tuple[str, str, Path]]
|
||||
|
||||
|
||||
def use_files() -> UseFiles:
|
||||
"""쓰는 곳을 찾을 파일 — (무엇, 어디, 파일) · 목록 한 번에 한 번 찾음(818-1)."""
|
||||
# 「일위대가」 = 사전조합 새 이름(내역 14 확정 1) · 구조물 조합 일위대가 = 「구조물 조합」
|
||||
kinds = (("labor", "구조물 조합"), ("precombo", "일위대가"), ("sangeun", "단가산출"))
|
||||
return [(what, at, path) for kind, what in kinds for at, path in layers.layer_files(kind)]
|
||||
|
||||
|
||||
def users(ref: Ref, files: UseFiles | None = None) -> list[str]:
|
||||
"""`ref` 를 담은 일위대가 · 사전조합 · 단가산출(바탕 · 더한줄) — 「어디 이름」 목록(모든 층 훑음 ·
|
||||
프로젝트 뺌)."""
|
||||
rid, out = ref_id(ref), []
|
||||
# 「일위대가」 = 사전조합 새 이름(내역 14 확정 1) · 구조물 조합 일위대가 = 「구조물 조합」
|
||||
for kind, what in (("labor", "구조물 조합"), ("precombo", "일위대가"), ("sangeun", "단가산출")):
|
||||
for at, path in layers.layer_files(kind):
|
||||
try:
|
||||
doc = json.loads(path.read_bytes())
|
||||
except (OSError, ValueError):
|
||||
continue
|
||||
if any(ref_id(one) == rid for one in refs_in(doc)):
|
||||
name = doc.get("일위대가번") or doc.get("이름") or path.stem
|
||||
out.append(f"{what} {at} 「{name}」")
|
||||
for what, at, path in use_files() if files is None else files:
|
||||
got = _ids_of(path)
|
||||
if got and rid in got[1]:
|
||||
out.append(f"{what} {at} 「{got[0]}」")
|
||||
return out
|
||||
|
||||
|
||||
_IDS: dict[Path, tuple[tuple[int, int], tuple[str, frozenset[str]]]] = {}
|
||||
|
||||
|
||||
def _ids_of(path: Path) -> tuple[str, frozenset[str]] | None:
|
||||
"""파일 하나의 (이름, 참조 이름표들) — 판(고친 때 · 크기)이 같으면 기억한 것(818-1 · 로직 사본으로
|
||||
SK 파일이 MB 단위라 목록 줄마다 모든 층을 다시 읽으면 분 단위)."""
|
||||
try:
|
||||
st = path.stat()
|
||||
stamp = (st.st_mtime_ns, st.st_size)
|
||||
if (hit := _IDS.get(path)) and hit[0] == stamp:
|
||||
return hit[1]
|
||||
doc = json.loads(path.read_bytes())
|
||||
except (OSError, ValueError):
|
||||
return None
|
||||
name = (
|
||||
(doc.get("일위대가번") or doc.get("이름") or path.stem)
|
||||
if isinstance(doc, dict)
|
||||
else path.stem
|
||||
)
|
||||
got = (str(name), frozenset(ref_id(one) for one in refs_in(doc)))
|
||||
_IDS[path] = (stamp, got)
|
||||
return got
|
||||
|
||||
|
||||
def refuse_used(folder: Path, kind: str, key: str) -> None:
|
||||
"""쓰는 곳이 있으면 409 + 이름(서고 종류만)."""
|
||||
if kind not in PREFIX:
|
||||
|
||||
@@ -10,6 +10,7 @@ from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
|
||||
from M01_MasterData import M01_MasterData_Parts as parts
|
||||
from M01_MasterData import M01_MasterData_Router as m01
|
||||
from M02_MasterTemplete import M02_Machine_Hourly as machine
|
||||
|
||||
@@ -72,3 +73,9 @@ def post_text(body: m01.CalcBody) -> dict:
|
||||
def get_machine_hourly(key: str) -> dict:
|
||||
"""기계(EQ) 한 시간 값 노 · 재 · 경(736-3 · [항목 추가] 기계 고정 줄 단가)."""
|
||||
return machine.hourly(key)
|
||||
|
||||
|
||||
@router.get("/machine-parts")
|
||||
def get_machine_parts(no: str) -> dict:
|
||||
"""줄 쌓기 기계 넣기(813-2) — 기계 원문번호 → 그 기종 작업량 틀(문서째)."""
|
||||
return m01._call(parts.of_machine, no)
|
||||
|
||||
@@ -0,0 +1,358 @@
|
||||
"""M02 기계 찾기(PLAN 812-9 · 요구 718 · 719) — 가져온 기계 글(이름 · 규격) → 우리 EQ · 서버 단독.
|
||||
|
||||
과녁 = M01 기계표 전체(EQ) · 부르는 곳 = 불러오기 이음(`M02_Hopyo_Row.m01_picks` · `M02_San_Emit._m01`) ·
|
||||
일위대가 호표(`M02_Hopyo_Elem.machine(s)` · `M02_Hopyo_Fixed._eqs`).
|
||||
차례:
|
||||
1. 별칭(M01 기계 줄 「별칭」 칸 · 815-1 `M02_Elem_Alias`) — 「이름 규격」 통째 별칭 = 그 EQ(여럿 =
|
||||
후보 여럿) · 이름만 별칭 = 그 EQ 기종으로 바꿔 2 로
|
||||
2. 기종 = EQ 이름(괄호 뗌) · 괄호 붙임 · 상세구분과 같은 글 › 그 글을 품은 EQ(표기 정규화 `norm`)
|
||||
3. 괄호 낱말 — 기계표 괄호에 있는 낱말(무한궤도 · 타이어 · 자주식 …)만 거름 · 표에 없는 낱말(발파 · 리핑)은 안 봄
|
||||
4. 규격 = 수 + 단위(범위 · 여럿) 모두 EQ 규격에 같은 값(단위 한쪽 없음 = 봄) › 없으면 EQ 범위 안
|
||||
하나 = 이음 · 여럿 = 「후보 여럿」 · 0 = 「못 찾음」(가까운 규격으로 대신 잇지 않음)
|
||||
조합 기계(「+」 · 괄호 속 다른 기종 · 「…조합」 · 「덤프트럭(자동덮개)」) = `parts` 로 갈라 각각 찾음 ·
|
||||
규격 없는 쪽은 앞 쪽 규격.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
import re
|
||||
import time
|
||||
import unicodedata
|
||||
from functools import lru_cache
|
||||
from typing import Any
|
||||
|
||||
from M01_MasterData import M01_MasterData_Store as m01
|
||||
|
||||
# 소리만 다른 표기 → 한 글(양쪽에 같이 씀 · 차례대로)
|
||||
SOUND = (
|
||||
("로우더", "로더"), ("로울러", "롤러"), ("브레이카", "브레이커"), ("브레카", "브레이커"),
|
||||
("굴삭기", "굴착기"), ("굴삭", "굴착"), ("불도우저", "불도저"), ("램머", "래머"),
|
||||
("크로울러", "크롤러"), ("콤프레샤", "공기압축기"), ("컴프레셔", "공기압축기"),
|
||||
("컴프레서", "공기압축기"), ("컴팩터", "콤팩터"), ("타이어형", "타이어"), ("젯트", "제트"),
|
||||
("후렉시블", "플렉시블"), ("스프레야", "스프레이어"), ("코어", "코아"),
|
||||
) # fmt: skip
|
||||
_UNIT = (
|
||||
("m3/min", "m3/min"), ("m3/분", "m3/min"), ("m3/hr", "m3/hr"), ("m3/h", "m3/hr"),
|
||||
("km/hr", "km/hr"), ("km/h", "km/hr"), ("l/min", "l/min"), ("t/hr", "t/hr"),
|
||||
("kg/cm2", "kg/cm2"), ("kg/min", "kg/min"), ("m3", "m3"), ("톤용", "ton"), ("ton", "ton"),
|
||||
("톤", "ton"), ("t", "ton"), ("kw", "kw"), ("mm", "mm"), ("cm", "cm"), ("kg", "kg"),
|
||||
("km", "km"), ("m", "m"), ("l", "l"), ("hp", "hp"), ("amp", "a"), ("cc", "cc"), ('"', "in"),
|
||||
("inch", "in"), ("인치", "in"),
|
||||
) # fmt: skip
|
||||
_NUM = r"\d+(?:\.\d+)?"
|
||||
_TOKEN = re.compile(
|
||||
rf"({_NUM})(?:\s*([~∼\-])\s*({_NUM}))?\s*("
|
||||
+ "|".join(re.escape(u) for u, _ in _UNIT)
|
||||
+ r")?(?![a-z])"
|
||||
)
|
||||
_PAREN = re.compile(r"[(\[]([^()\[\]]*)[)\]]")
|
||||
_EXTRA = re.compile(
|
||||
r"[:,]?\s*\(?할증율?[^)]*\)?"
|
||||
) # 「0.2㎥:할증120%」 · 「(할증125%)」 = 율(규격 아님)
|
||||
NOTE = "참고 · "
|
||||
LINKED, ALIAS, MANY, NONE = "이어짐", "별칭", "후보 여럿", "못 찾음"
|
||||
SHOW = 8 # 후보 글 · 목록 상한
|
||||
|
||||
|
||||
def norm(text: Any) -> str:
|
||||
"""표기 정규화 — NFKC · 작은 글자 · 빈칸 뺌 · 소리 표기 한 글."""
|
||||
t = re.sub(r"\s+", "", unicodedata.normalize("NFKC", str(text or "")).lower())
|
||||
for old, new in SOUND:
|
||||
t = t.replace(old, new)
|
||||
return t
|
||||
|
||||
|
||||
def _clean(text: Any) -> str:
|
||||
t = unicodedata.normalize("NFKC", str(text or "")).lower()
|
||||
t = _EXTRA.sub(" ", t)
|
||||
return re.sub(r"(?<=\d),(?=\d{3}(?!\d))", "", t) # 천 자리 쉼표
|
||||
|
||||
|
||||
def tokens(text: Any) -> list[tuple[float, float, str | None]]:
|
||||
"""규격 글 → [(아래, 위, 단위)] — 범위 「10~12」 · 여럿 「32m,80~95㎥/hr」 · 단위 없음 = None."""
|
||||
out = []
|
||||
for m in _TOKEN.finditer(_clean(text)):
|
||||
lo, sep, hi, unit = float(m.group(1)), m.group(2), m.group(3), m.group(4)
|
||||
top = float(hi) if hi else lo
|
||||
if sep == "-" and top <= lo: # 「50-2.6m」 = 범위 아님 — 앞 수만
|
||||
top = lo
|
||||
canon = dict(_UNIT).get(unit) if unit else None
|
||||
if canon == "in": # 인치 = ㎝(×2.54 · 우리 표는 ㎝)
|
||||
lo, top, canon = round(lo * 2.54, 6), round(top * 2.54, 6), "cm"
|
||||
out.append((lo, top, canon))
|
||||
return out
|
||||
|
||||
|
||||
def _unit_ok(a: str | None, b: str | None) -> bool:
|
||||
return a is None or b is None or a == b or {a, b} <= {"ton", "t/hr"}
|
||||
|
||||
|
||||
def spec_fit(mine: list, theirs: list) -> str | None:
|
||||
"""가져온 규격 수가 EQ 규격에 다 있음 = 「같음」 · EQ 범위 안 = 「안」 · 아니면 None."""
|
||||
if not mine:
|
||||
return "같음" if not theirs else None
|
||||
same = all(
|
||||
any(abs(a - c) < 1e-9 and abs(b - d) < 1e-9 and _unit_ok(u, w) for c, d, w in theirs)
|
||||
for a, b, u in mine
|
||||
)
|
||||
if same:
|
||||
return "같음"
|
||||
inside = all(
|
||||
any(c - 1e-9 <= a and b <= d + 1e-9 and c < d and _unit_ok(u, w) for c, d, w in theirs)
|
||||
for a, b, u in mine
|
||||
)
|
||||
return "안" if inside else None
|
||||
|
||||
|
||||
def _words(inner: str) -> list[str]:
|
||||
return [norm(w) for w in re.split(r"[\s,;·/]+", inner) if w.strip()]
|
||||
|
||||
|
||||
def split_name(text: Any) -> tuple[str, list[list[str]], list[str]]:
|
||||
"""이름(+규격) 글 → (기종 글 정규화, 괄호 낱말 묶음들, 규격 조각들) — 괄호 속 수 = 규격."""
|
||||
raw = _clean(text)
|
||||
groups, specs = [], []
|
||||
for inner in _PAREN.findall(raw):
|
||||
bits = [b for b in re.split(r"[,;]", inner) if b.strip()]
|
||||
specs += [b for b in bits if re.search(r"\d", b)]
|
||||
groups.append([w for b in bits if not re.search(r"\d", b) for w in _words(b)])
|
||||
base = _PAREN.sub(" ", raw)
|
||||
specs += [m.group(0) for m in _TOKEN.finditer(base)]
|
||||
base = _TOKEN.sub(" ", base)
|
||||
return norm(base), groups, specs
|
||||
|
||||
|
||||
# ── 기계표 ────────────────────────────────────────────────────────────
|
||||
class Row:
|
||||
__slots__ = ("raw", "key", "base", "detail", "joined", "full", "words", "spec", "said")
|
||||
|
||||
def __init__(self, r: dict) -> None:
|
||||
name = str(r.get("이름") or "")
|
||||
self.raw, self.key = r, str(r.get("키"))
|
||||
inner = _PAREN.findall(unicodedata.normalize("NFKC", name))
|
||||
self.words = {w for x in inner for w in _words(x)}
|
||||
self.base = norm(_PAREN.sub("", unicodedata.normalize("NFKC", name)))
|
||||
self.detail = norm(r.get("상세구분"))
|
||||
self.joined = {w + self.base for w in self.words} | {self.base + w for w in self.words}
|
||||
self.full = norm(name)
|
||||
self.spec = tokens(r.get("규격"))
|
||||
self.said = norm(
|
||||
_TOKEN.sub(" ", _clean(r.get("규격")))
|
||||
) # 규격 글 낱말(「더블」 · 「엔진식」)
|
||||
|
||||
|
||||
@lru_cache(maxsize=2)
|
||||
def _table(_whole: int) -> tuple[list[Row], frozenset[str], dict[str, Row]]:
|
||||
rows = [Row(r) for r in m01.loaded()[1].numbers.get("EQ", {}).values()]
|
||||
words = frozenset(w for r in rows for w in r.words)
|
||||
return rows, words, {r.key: r for r in rows}
|
||||
|
||||
|
||||
_SEEN: list = [0.0, None] # (때, 표) — M01 판 확인(파일 도장)은 1 초에 한 번
|
||||
|
||||
|
||||
def table() -> tuple[list[Row], frozenset[str], dict[str, Row]]:
|
||||
now = time.monotonic()
|
||||
if _SEEN[1] is None or now - _SEEN[0] > 1.0:
|
||||
_SEEN[:] = [now, _table(id(m01.loaded()[1]))]
|
||||
return _SEEN[1]
|
||||
|
||||
|
||||
def kind_rows(base: str) -> list[Row]:
|
||||
"""기종 글 → EQ 줄 — 이름 · 괄호 붙임 같음 › 상세구분 같음 › 그 글을 품음(첫 층만)."""
|
||||
if len(base) < 2:
|
||||
return []
|
||||
rows = table()[0]
|
||||
for test in (
|
||||
lambda r: base == r.base or base in r.joined,
|
||||
lambda r: base == r.detail,
|
||||
lambda r: base in r.full or base in r.detail,
|
||||
):
|
||||
got = [r for r in rows if test(r)]
|
||||
if got:
|
||||
return got
|
||||
return []
|
||||
|
||||
|
||||
def _kind_word(word: str, base: str) -> str | None:
|
||||
"""괄호 낱말이 다른 기종(조합)이면 그 기종 글 — 「부착용집게」 · 「브레이커조합」 · 「자동덮개」."""
|
||||
w = word.removesuffix("조합")
|
||||
if len(w) < 2 or w == base:
|
||||
return None
|
||||
rows = table()[0]
|
||||
if any(w in (r.base, r.detail) for r in rows):
|
||||
return w
|
||||
if any(r.base.startswith(base + w) for r in rows):
|
||||
return base + w
|
||||
return None
|
||||
|
||||
|
||||
# ── 조합 기계 ─────────────────────────────────────────────────────────
|
||||
def parts(name: Any, spec: Any = "") -> list[tuple[str, str]]:
|
||||
"""기계 글 → [(이름, 규격)] — 「+」 · 괄호 속 다른 기종이면 둘 이상 · 규격 없는 쪽 = 앞 쪽 규격 ·
|
||||
하나면 받은 그대로."""
|
||||
text = f"{name or ''} {spec or ''}".strip()
|
||||
known = table()[1]
|
||||
out: list[tuple[str, str]] = []
|
||||
for piece in [p for p in text.split("+") if p.strip()] or [text]:
|
||||
main, base, extra = _clean(piece), split_name(piece)[0], []
|
||||
for m in _PAREN.finditer(_clean(piece)):
|
||||
bits = [b for b in re.split(r"[,;]", m.group(1)) if b.strip()]
|
||||
words = [w for b in bits if not re.search(r"\d", b) for w in _words(b)]
|
||||
kind = next((k for w in words if w not in known and (k := _kind_word(w, base))), None)
|
||||
if kind:
|
||||
main = main.replace(m.group(0), " ")
|
||||
extra.append((kind, ",".join(b for b in bits if re.search(r"\d", b))))
|
||||
bare = _TOKEN.sub(
|
||||
" ", _PAREN.sub(lambda m: " " if re.search(r"\d", m.group(1)) else m.group(0), main)
|
||||
)
|
||||
own = " ".join(split_name(main)[2])
|
||||
out += [(" ".join(bare.split()).strip(" ,"), own), *extra]
|
||||
if len(out) < 2:
|
||||
return [(str(name or ""), str(spec or ""))]
|
||||
first = next((s for _, s in out if s), "")
|
||||
return [(n, s or first) for n, s in out]
|
||||
|
||||
|
||||
# ── 별칭(M01 기계 줄 「별칭」 칸 · 815-1) ───────────────────────────────────
|
||||
def alias(name: Any, spec: Any) -> list[str]:
|
||||
"""M01 기계 줄 별칭(`M02_Elem_Alias`) — 규격이 있으면 「이름 규격」 통째 별칭(규격 속 할증 글은 뺌) ·
|
||||
규격 빔 = 이름 별칭 · 없으면 []."""
|
||||
from M02_MasterTemplete import M02_Elem_Alias as ea
|
||||
|
||||
rows = ea.book()["별칭"].get("EQ", {})
|
||||
return list(rows.get(ea.am.norm(name) + ea.am.norm(_clean(spec)), []))
|
||||
|
||||
|
||||
# ── 찾기 ──────────────────────────────────────────────────────────────
|
||||
def _brief(r: Row) -> dict[str, Any]:
|
||||
raw = r.raw
|
||||
return {"키": r.key, "이름": raw.get("이름"), "규격": raw.get("규격") or "",
|
||||
"원문번호": raw.get("원문번호")} # fmt: skip
|
||||
|
||||
|
||||
def _query(rows: list[Row], base: str) -> str:
|
||||
names = {str(r.raw.get("이름")) for r in rows}
|
||||
if len(names) == 1:
|
||||
return names.pop()
|
||||
details = {str(r.raw.get("상세구분")) for r in rows}
|
||||
return details.pop() if len(details) == 1 else base
|
||||
|
||||
|
||||
def _ans(kind: str, rows: list[Row], query: str, why: str = "") -> dict[str, Any]:
|
||||
key = rows[0].key if kind in (LINKED, ALIAS) else None
|
||||
return {"판정": kind, "키": key, "후보": [_brief(r) for r in rows[:SHOW]], "찾기어": query,
|
||||
"까닭": why, "수": len(rows)} # fmt: skip
|
||||
|
||||
|
||||
def find(name: Any, spec: Any = "") -> dict[str, Any]:
|
||||
"""기계 하나(조합 아님) → `{판정, 키, 후보, 찾기어, 까닭, 수}`."""
|
||||
rows_by = table()[2]
|
||||
hit = [rows_by[k] for k in alias(name, spec) if k in rows_by]
|
||||
if hit: # 별칭 하나 = 그 EQ · 여럿 = 후보 여럿
|
||||
if len(hit) == 1:
|
||||
return _ans(ALIAS, hit, str(hit[0].raw.get("이름")))
|
||||
return _ans(MANY, hit, _query(hit, hit[0].base), f"후보 {len(hit)}")
|
||||
base = split_name(name)[0]
|
||||
_, groups, specs = split_name(f"{name or ''} {spec or ''}") # 규격 괄호 낱말 「3.6m(일반용)」
|
||||
groups.append(
|
||||
_words(_TOKEN.sub(" ", _PAREN.sub(" ", _clean(spec))))
|
||||
) # 규격 글 낱말 「탑승유압식」
|
||||
named = [rows_by[k] for k in alias(name, "") if k in rows_by]
|
||||
if named and len({r.base for r in named}) == 1: # 이름만 별칭 = 그 EQ 기종으로
|
||||
base = named[0].base
|
||||
mine = tokens(" ".join(specs))
|
||||
kinds = kind_rows(base)
|
||||
known = table()[1]
|
||||
rows = kinds
|
||||
for w in (w for g in groups for w in g):
|
||||
if w in known: # 기계표 괄호 낱말 = 꼭 맞아야 함
|
||||
rows = [r for r in rows if w in r.full]
|
||||
elif len(w) > 1 and (kept := [r for r in rows if w in r.said]): # 규격 글 낱말 = 좁힐 때만
|
||||
rows = kept
|
||||
if not mine: # 규격 수 없음 = 그 기종 전부(괄호 · 규격 낱말로 거른 것)
|
||||
got = rows
|
||||
else:
|
||||
same = [r for r in rows if spec_fit(mine, r.spec) == "같음"]
|
||||
got = same or [r for r in rows if spec_fit(mine, r.spec) == "안"]
|
||||
if len(got) == 1: # 이름만 별칭으로 기종을 정했으면 「별칭」
|
||||
return _ans(ALIAS if named else LINKED, got, str(got[0].raw.get("이름")))
|
||||
if got:
|
||||
return _ans(MANY, got, _query(got, base), f"후보 {len(got)}")
|
||||
if not kinds:
|
||||
return _ans(NONE, [], base, "기종 모름 — 별칭")
|
||||
why = "우리 표에 없음(규격)" if rows else "우리 표에 없음(괄호 낱말)"
|
||||
return _ans(
|
||||
NONE, sorted(rows or kinds, key=lambda r: _gap(mine, r.spec)), _query(kinds, base), why
|
||||
)
|
||||
|
||||
|
||||
def _gap(mine: list, theirs: list) -> float:
|
||||
"""가까운 규격 차례(보여주기만) — 첫 수끼리 비."""
|
||||
if not mine or not theirs or mine[0][0] <= 0 or theirs[0][0] <= 0:
|
||||
return math.inf
|
||||
return abs(math.log(mine[0][0] / theirs[0][0]))
|
||||
|
||||
|
||||
def find_all(name: Any, spec: Any = "") -> list[dict[str, Any]]:
|
||||
"""기계 글 → 부분마다 `find` + {이름, 규격}(조합 = 둘 이상)."""
|
||||
out: list[dict[str, Any]] = []
|
||||
for n, s in parts(name, spec):
|
||||
if out and s and not split_name(n)[2] and s == out[0]["규격"] and out[0]["키"]:
|
||||
s = str(out[0]["후보"][0]["규격"]) # 앞 줄 규격을 받은 쪽 = 이은 우리 규격(단위까지)
|
||||
out.append({**find(n, s), "이름": n, "규격": s})
|
||||
return out
|
||||
|
||||
|
||||
# ── 같은 파일 안 구성 대조로 가름(816-2 · 요구 773-3) ───────────────────────────
|
||||
MADE = "같은 파일 구성 같음"
|
||||
|
||||
|
||||
def peers(machines: dict[Any, tuple[str, str, dict]]) -> dict[Any, list[list[str]]]:
|
||||
"""같은 파일 기계 {코드: (이름, 규격, 구성 {손료 [[계수, 가격]], 주연료, 조종원})} → {코드: 구성이 같은
|
||||
단독 기계(손료 줄 하나) [이름, 규격] 들} — 낱 기계 = 구성 통째 같음 · 조합 = 손료 줄마다 그 줄 하나뿐인
|
||||
단독 기계. 같은 파일 · 같은 때의 값끼리(우리 표 값과 안 맞댐)."""
|
||||
alone = {c: m for c, m in machines.items() if len(m[2].get("손료") or []) == 1}
|
||||
out: dict[Any, list[list[str]]] = {}
|
||||
for c, (_, _, make) in machines.items():
|
||||
loss = make.get("손료") or []
|
||||
if len(loss) == 1:
|
||||
got = [o for o, m in alone.items() if o != c and m[2] == make]
|
||||
else:
|
||||
got = [o for o, m in alone.items() if o != c and m[2]["손료"][0] in loss]
|
||||
if got:
|
||||
out[c] = [[alone[o][0], alone[o][1]] for o in got]
|
||||
return out
|
||||
|
||||
|
||||
def narrow(parts: list[dict[str, Any]], same: list | None) -> list[dict[str, Any]]:
|
||||
"""「후보 여럿」 부분 → 같은 파일에서 구성이 같은 단독 기계 이름 · 규격으로 다시 찾아(`find`) 그 후보
|
||||
가운데 키 하나로 모이면 그 줄 · 여럿 · 없음 · 대조 없음 = 그대로(우리 값으로 넘겨짚지 않음)."""
|
||||
if not same:
|
||||
return parts
|
||||
found = [(n, s, find(n, s)) for n, s in same]
|
||||
out = []
|
||||
for p in parts:
|
||||
if p["판정"] == MANY:
|
||||
mine = {c["키"] for c in p["후보"]}
|
||||
keys = {
|
||||
g["키"] for _, _, g in found if g["판정"] in (LINKED, ALIAS) and g["키"] in mine
|
||||
}
|
||||
if len(keys) == 1:
|
||||
key = keys.pop()
|
||||
n, s = next((n, s) for n, s, g in found if g["키"] == key)
|
||||
p = {**p, **_ans(LINKED, [table()[2][key]], n, f"{MADE} — {n} {s}".strip())}
|
||||
out.append(p)
|
||||
return out
|
||||
|
||||
|
||||
def mark(got: dict[str, Any], name: Any, spec: Any) -> str | None:
|
||||
"""못 이은 기계 표지 글(「참고 · 」 머리) — 이음 · 별칭 = None."""
|
||||
said = f"「{' '.join(x for x in (str(name or ''), str(spec or '')) if x)}」"
|
||||
if got["판정"] == MANY:
|
||||
names = " · ".join(f"{c['이름']} {c['규격']}".strip() for c in got["후보"])
|
||||
return f"{NOTE}기계 후보 {got['수']} — 고르기 {said}: {names}"
|
||||
if got["판정"] == NONE:
|
||||
return f"{NOTE}기계 못 찾음 — {said} {got['까닭']}"
|
||||
return None
|
||||
@@ -41,6 +41,51 @@ def _rank(row: dict) -> tuple:
|
||||
return (attach, loss, loss and "보정작업" in names, len(_codes(row)), str(row.get("키")))
|
||||
|
||||
|
||||
def _operated(code: str) -> bool:
|
||||
"""기계표(원문 8-4) 조종원 > 0 — 그 원문번호 기계 가운데 하나라도."""
|
||||
rows = store.loaded()[1].index.get("EQ", {}).values()
|
||||
return any(
|
||||
str(r.get("원문번호")) == code and float((r.get("값") or {}).get("조종원") or 0) > 0
|
||||
for r in rows
|
||||
)
|
||||
|
||||
|
||||
def logic_of(code: str) -> dict | None:
|
||||
"""기계 원문번호 → 그 기계 시간당 경비 로직(짝 차례 첫째) · 없으면 None(813-2 M01 화면 ·
|
||||
불러오기 `M02_Doc_Import.hourly_link` 도 씀). 기계표 조종원 > 0 = 조종원 호표 있는 식 먼저
|
||||
(「조종원 없는 기종」 목록에 겹친 기계 — 진동롤러(핸드가이드식) · 고소작업차 …)."""
|
||||
fit = [r for r in _family() if code in [str(c) for c in _codes(r)]]
|
||||
if not fit:
|
||||
return _own(code)
|
||||
op = _operated(code)
|
||||
|
||||
def order(row: dict) -> tuple:
|
||||
has = any(x.get("이름") == "조종원" for x in row.get("호표") or [])
|
||||
rank = _rank(row)
|
||||
return (*rank[:3], op and not has, *rank[3:])
|
||||
|
||||
return min(fit, key=order)
|
||||
|
||||
|
||||
def _own(code: str) -> dict | None:
|
||||
"""그 기계만의 시간당 경비 식(816-1 · 4륜구동 1톤 덤프트럭 = GF000145 · 816-3 체인톱 = GF000337) —
|
||||
「기계」 입력 없는 기계경비식이 그 원문번호 기계 줄 값(`EQ….가격` · `EQ….손료계수`)을 읽으면 그 식."""
|
||||
rows = store.loaded()[1].index.get("EQ", {}).values()
|
||||
keys = [f"{r.get('키')}." for r in rows if code and str(r.get("원문번호")) == code]
|
||||
for data in store.loaded()[0].values():
|
||||
for row in data.get("줄") or [] if data.get("그룹") == "로직" else []:
|
||||
if (row.get("부품") or {}).get("갈래") != "기계경비식" or takes_machine(row):
|
||||
continue
|
||||
if any(k in str(row.get("중간")) + str(row.get("호표")) for k in keys):
|
||||
return row
|
||||
return None
|
||||
|
||||
|
||||
def takes_machine(logic: dict) -> bool:
|
||||
"""식이 「기계」 입력(원문번호 고르기)을 받는지 — 안 받는 식(GF000145)엔 기계='…' 를 안 넘김."""
|
||||
return any(i.get("이름") == "기계" for i in logic.get("입력") or [])
|
||||
|
||||
|
||||
def hourly(key: str) -> dict[str, Any]:
|
||||
"""`{ok, 이름, 규격, 로직, 원문번호, 노무비, 재료비, 경비, 계}` · 못 셈 = `{ok: False, reason}`."""
|
||||
if not key.startswith("EQ"):
|
||||
@@ -50,16 +95,16 @@ def hourly(key: str) -> dict[str, Any]:
|
||||
except cm.mf.FormulaError as e:
|
||||
return {"ok": False, "reason": str(e)}
|
||||
code = str(row.get("원문번호") or "")
|
||||
fit = [r for r in _family() if code in [str(c) for c in _codes(r)]]
|
||||
if not fit:
|
||||
logic = logic_of(code)
|
||||
if not logic:
|
||||
return {"ok": False, "reason": f"「{row.get('이름')}」 은 시간당 경비를 셀 자료가 없음"}
|
||||
logic = min(fit, key=_rank)
|
||||
args = "".join(
|
||||
f", {i['이름']}='{(i.get('고르기') or [''])[0]}'"
|
||||
args = "".join( # 빈칸 든 입력 이름(「전력 계약종별」 · 820-1 파워트로웰)은 따옴표로
|
||||
f", {n if (n := i['이름']).isidentifier() else repr(n)}='{(i.get('고르기') or [''])[0]}'"
|
||||
for i in logic.get("입력") or []
|
||||
if i.get("이름") != "기계"
|
||||
)
|
||||
call = f"로직({logic['키']}, 기계='{code}'{args})"
|
||||
mine = f", 기계='{code}'" if takes_machine(logic) else ""
|
||||
call = f"로직({logic['키']}{mine}{args})"
|
||||
line = {"종류": "로직", "요소": call, "이름": "기계", "단위": "시간", "수량": "1"}
|
||||
holder = {
|
||||
"키": "", "원문번호": "", "이름": "기계 시간", "결과단위": "원/식", "출처": "자체",
|
||||
@@ -78,3 +123,24 @@ def hourly(key: str) -> dict[str, Any]:
|
||||
"원문번호": code,
|
||||
**sums,
|
||||
}
|
||||
|
||||
|
||||
def combo(main: str, part: str) -> tuple[str, str] | None:
|
||||
"""조합 기계(굴착기 + 대형 브레이커 · 부착용 집게 · 814-1) → 우리 조합 식 (키, 부름 글)
|
||||
`로직(GC000271, 기계='…', 브레이커='…', 지역='전국평균', …)` — 두 기계가 그 식 고르기에 있을 때만."""
|
||||
try:
|
||||
codes = [str(store.loaded()[1].get(k).get("원문번호") or "") for k in (main, part)]
|
||||
except cm.mf.FormulaError:
|
||||
return None
|
||||
for row in sorted(_family(), key=lambda r: str(r.get("키"))):
|
||||
ins = row.get("입력") or []
|
||||
attach = [i for i in ins if i.get("이름") not in _PLAIN and i.get("고르기")]
|
||||
if len(attach) != 1 or codes[0] not in [str(c) for c in _codes(row)]:
|
||||
continue
|
||||
if codes[1] not in [str(c) for c in attach[0]["고르기"]]:
|
||||
continue
|
||||
given = {"기계": codes[0], attach[0]["이름"]: codes[1]}
|
||||
given |= {i["이름"]: (i.get("고르기") or [""])[0] for i in ins if i["이름"] not in given}
|
||||
args = "".join(f", {k}='{v}'" for k, v in given.items())
|
||||
return str(row["키"]), f"로직({row['키']}{args})"
|
||||
return None
|
||||
|
||||
@@ -34,14 +34,6 @@
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.m02-hopyo-ledger__table td:nth-child(1) {
|
||||
min-width: 7em;
|
||||
}
|
||||
|
||||
.m02-hopyo-ledger__table td:nth-child(2) {
|
||||
min-width: 6em;
|
||||
}
|
||||
|
||||
/* 우리 로직 칸 — 대상 여럿 = 칸 안 여러 줄(이름 · 키 · 몫 · 배 칸 · [옵션]) */
|
||||
.m02-hopyo__parts {
|
||||
min-width: 11em;
|
||||
@@ -83,15 +75,6 @@
|
||||
margin-left: var(--spacing-4);
|
||||
}
|
||||
|
||||
/* 별칭 장부(338) — 위 층 줄 = 회색 읽기만 · 고친 줄 = 왼쪽 띠 */
|
||||
.m02-hopyo-ledger__row.is-upper {
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.m02-hopyo-ledger__row.is-edited > td:first-child {
|
||||
box-shadow: inset 3px 0 0 var(--color-accent);
|
||||
}
|
||||
|
||||
/* 엑셀 보기(329) — 왼쪽 읽기 전용 시트 · 오른쪽 호표 목록 */
|
||||
.m02-hopyo-sheet {
|
||||
display: grid;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* M02_MasterTemplete_Hopyo_Api.ts
|
||||
* 일위대가 실무자료(PLAN 324 · 338) 서버 길 — `/api/m02/layers/{층}/hopyo…` · 층 = 화면 층(`Api_Fetch` 지금 층) ·
|
||||
* 올리기 = FormData(숨은 file 입력) · 그 밖 = 공용 `call`(서버 까닭 글 그대로) ·
|
||||
* 목록 · 올림 · 엑셀 보기 머리(809-1 줄 표지 줄상태) · 격자 · 지우기 · 별칭 장부 · 붙여넣기 · 다시 잇기 —
|
||||
* 목록 · 올림 · 엑셀 보기 머리(809-1 줄 표지 줄상태) · 격자 · 지우기 · 붙여넣기 · 다시 잇기 —
|
||||
* [수정] 보기 = `Hopyo_View`(809-1) · 판정 · 검산 · 옮기기 길(785)은 809-1 에서 지움(요구 703).
|
||||
* ========================================================================== */
|
||||
|
||||
@@ -97,36 +97,6 @@ export interface HopyoBuiltAll {
|
||||
만듦?: { 번: number; 이름: string; 키: string }[];
|
||||
}
|
||||
|
||||
/** 별칭 장부 짝 하나 */
|
||||
export interface LedgerPair {
|
||||
로직?: string;
|
||||
자재?: string;
|
||||
입력값: Record<string, unknown>;
|
||||
배: number;
|
||||
몫: number | null;
|
||||
}
|
||||
|
||||
/** 별칭 장부 한 줄(쓰기 꼴) */
|
||||
export interface LedgerEntry {
|
||||
원래: { 품명: string; 규격: string; 단위: string };
|
||||
갈래: "짝" | "우리에 없음" | "안 씀";
|
||||
짝: LedgerPair[];
|
||||
엑셀조항?: unknown[];
|
||||
}
|
||||
|
||||
/** 별칭 장부 보기 한 줄 — 층 · 고침(이 층 · 쓰기 권한) */
|
||||
export interface LedgerRow extends LedgerEntry {
|
||||
열쇠: string;
|
||||
누가: { id: number; 이름: string | null } | null;
|
||||
때: string | null;
|
||||
처음파일: string | null;
|
||||
층: Layer;
|
||||
주인: string | null;
|
||||
고침: boolean;
|
||||
/** 739-1 M01 승인 상태 — 마스터 줄 = 승인 */
|
||||
상태?: "승인" | "대기" | "반려";
|
||||
}
|
||||
|
||||
const enc = encodeURIComponent;
|
||||
/** 지금 화면 층(구조물 조합 층) */
|
||||
export const pageScope = (): HopyoScope => ({ layer: getActiveLayer(), owner: getActiveOwner() });
|
||||
@@ -146,21 +116,6 @@ export const readHopyo = (scope: HopyoScope, key: string): Promise<HopyoView> =>
|
||||
export const readHopyoGrid = (scope: HopyoScope, key: string): Promise<HopyoGrid> =>
|
||||
call(at(scope, `/hopyo-grid/${enc(key)}`));
|
||||
|
||||
export const readLedger = async (scope: HopyoScope): Promise<LedgerRow[]> =>
|
||||
(await call<{ 줄: LedgerRow[] }>(at(scope, "/hopyo-ledger"))).줄;
|
||||
|
||||
/** 별칭 장부 [저장] — 고친 줄 · null(지움) · 이 층만 */
|
||||
export const saveLedger = async (
|
||||
scope: HopyoScope,
|
||||
줄: Record<string, LedgerEntry | null>,
|
||||
): Promise<LedgerRow[]> =>
|
||||
(
|
||||
await call<{ 줄: LedgerRow[] }>(at(scope, "/hopyo-ledger"), {
|
||||
method: "PUT",
|
||||
body: JSON.stringify({ 줄 }),
|
||||
})
|
||||
).줄;
|
||||
|
||||
/** [끝내기] · 화면 떠남(743-1) — `keepalive` = 페이지를 닫아도 요청은 감 */
|
||||
export const deleteHopyo = (scope: HopyoScope, key: string, keepalive = false): Promise<unknown> =>
|
||||
call(at(scope, `/hopyo/${enc(key)}`), { method: "DELETE", keepalive });
|
||||
|
||||
@@ -61,7 +61,7 @@ interface EditAnswer {
|
||||
문서: ImportedDoc;
|
||||
전?: Money4 | null;
|
||||
후?: Money4 | null;
|
||||
이음없음?: { 자리: string; 이름: string }[];
|
||||
이음없음?: { 자리: string; 이름: string; 까닭?: string; 견적?: boolean }[];
|
||||
/** qty 답(797-2) */
|
||||
줄?: QtyRow[];
|
||||
집계?: QtyTally;
|
||||
|
||||
@@ -42,6 +42,7 @@ const TEXT = {
|
||||
Excel: ["엑셀", "Excel"],
|
||||
Now: ["지금", "Now"],
|
||||
Unlinked: ["이음 없는 자원 {n}", "Unlinked resources {n}"],
|
||||
Quote: ["견적 {n}", "Quotes {n}"],
|
||||
Link: ["이음 고르기", "Pick link"],
|
||||
Gap: ["바꾸기 전 {a} → 후 {b} ({d})", "Before {a} → after {b} ({d})"],
|
||||
QtyLinks: ["소요량 · 계수 이음 {n}", "Quantity / coefficient links {n}"],
|
||||
@@ -54,7 +55,7 @@ export const tx = (k: keyof typeof TEXT): string =>
|
||||
export interface PanelState {
|
||||
doc: ImportedDoc;
|
||||
/** 이음 없는 자원(서버 price 답) */
|
||||
unlinked: { 자리: string; 이름: string }[];
|
||||
unlinked: { 자리: string; 이름: string; 까닭?: string; 견적?: boolean }[];
|
||||
/** 소요량 · 계수 이음 집계(서버 qty 답 · 797-2) */
|
||||
qty: QtyTally | null;
|
||||
/** 마지막 단가 바꿈 전후 */
|
||||
@@ -149,12 +150,18 @@ export function importedPanel(s: PanelState, a: PanelActs): HTMLElement {
|
||||
const kids: HTMLElement[] = [createToolbar({ items: head })];
|
||||
if (here && s.unlinked.length) {
|
||||
const items = s.unlinked.map((u) => {
|
||||
const b = createButton({ label: `${u.이름} · ${tx("Link")}`, size: 24, variant: "ghost", disabled: s.readOnly || s.busy, onClick: () => a.link(u.자리, u.이름) }); // prettier-ignore
|
||||
const label = `${u.이름}${u.까닭 ? ` (${u.까닭})` : ""} · ${tx("Link")}`; // 못 이은 까닭(814-1)
|
||||
const b = createButton({ label, size: 24, variant: "ghost", disabled: s.readOnly || s.busy, onClick: () => a.link(u.자리, u.이름) }); // prettier-ignore
|
||||
b.dataset.importedLink = u.자리;
|
||||
return b;
|
||||
});
|
||||
const tag = createBadge(tx("Unlinked").replace("{n}", String(s.unlinked.length)), "warning");
|
||||
kids.push(createToolbar({ items: [tag, ...items] }));
|
||||
const quotes = s.unlinked.filter((u) => u.견적).length; // 815-1 견적 = 그대로 쓰는 값 · 못 이음 수에서 뺌
|
||||
const left = s.unlinked.length - quotes;
|
||||
const tags = [
|
||||
...(left ? [createBadge(tx("Unlinked").replace("{n}", String(left)), "warning")] : []),
|
||||
...(quotes ? [createBadge(tx("Quote").replace("{n}", String(quotes)), "info")] : []),
|
||||
];
|
||||
kids.push(createToolbar({ items: [...tags, ...items] }));
|
||||
}
|
||||
if (here && s.qty?.자리) {
|
||||
const open = createButton({ label: tx("QtyOpen"), size: 24, variant: "ghost", onClick: a.qty });
|
||||
|
||||
@@ -1,407 +0,0 @@
|
||||
/* =============================================================================
|
||||
* M02_MasterTemplete_Hopyo_Ledger.ts
|
||||
* 일위대가 실무자료 별칭 장부 화면(PLAN 338-3 · 설계 4-4) — 본 칸 · 좌측 판 [별칭 장부] 로 엶.
|
||||
* · 표 = 엑셀 품명 · 규격 · 단위 | 갈래 | 우리 로직(여럿) · 옵션 · 단위 환산 · 몫 | 누가 · 때 · 처음 파일 | 층(위 층 회색 읽기만)
|
||||
* · 뜻(359-4 · 머리 한 줄 · 좌측 단추 title) = 엑셀 품명 ↔ 우리 로직 · 자재 짝을 기억해 두었다가 같은 품명이 나오면 먼저 제안
|
||||
* · 할 일 = 찾기 · 갈래 거름 · 갈래 바꿈 · [로직 바꿈] · [항목 추가] · 단위 환산 · 몫 · [옵션] · [삭제] ·
|
||||
* 위 층 줄 [내 층 복사] · 갈래 글 = 확인 창 드롭다운과 같음(짝 · 로직없음 · 사용안함 · 저장 값 그대로)
|
||||
* · 고친 것 = sessionStorage `m02-hopyo-ledger:{층}:{주인}` → [저장] 때만 `PUT`(서버가 짝을 다시 검사)
|
||||
* · 안 둠 = 발주처 칸 · 자동 올림 · 자동 합침 · 쓰인 횟수 · 회사로 올리기.
|
||||
* · 739-1 중앙 관리 — 회사 · 개인 층 = 「내 제안 목록」(보기 · 고치기 · [거두기] · 승인 상태 대기 · 승인 · 반려) ·
|
||||
* 자동 짝 = 마스터(M01 별칭 장부 승인)만 · 마스터 층(M01 화면) = 별칭 장부 그대로
|
||||
* ========================================================================== */
|
||||
|
||||
import { createBadge, type BadgeTone } from "@ui/ui_template_badge";
|
||||
import { createButton, el, showToast } from "@ui/ui_template_elements";
|
||||
import { loadingError, showLoading } from "@ui/ui_template_loading";
|
||||
import { t as L } from "@ui/ui_template_locale";
|
||||
import {
|
||||
readLedger,
|
||||
saveLedger,
|
||||
type HopyoScope,
|
||||
type LedgerEntry,
|
||||
type LedgerPair,
|
||||
type LedgerRow,
|
||||
} from "./M02_MasterTemplete_Hopyo_Api";
|
||||
import { fetchLogicInputs } from "./M02_MasterTemplete_Labor_Api";
|
||||
import { dateTimeText } from "../common_util/common_util_time";
|
||||
import { openLaborInput } from "./M02_MasterTemplete_Labor_Input";
|
||||
import { openFindPicker } from "./M02_MasterTemplete_Find_Pick";
|
||||
import { formatNumber } from "../common_util/common_util_money";
|
||||
import "./M02_MasterTemplete_Hopyo.css";
|
||||
|
||||
export const LEDGER_TITLE = "별칭 장부";
|
||||
/** 별칭 장부 뜻 한 줄(359-4) — 좌측 단추 title · 화면 머리 */
|
||||
export const LEDGER_HINT =
|
||||
"엑셀 품명 ↔ 우리 로직 · 자재 짝을 기억해 두었다가 같은 품명이 나오면 먼저 제안";
|
||||
const KINDS = ["짝", "우리에 없음", "안 씀"] as const;
|
||||
/** 갈래 화면 글 — 확인 창 드롭다운과 같음(저장 값 그대로) */
|
||||
export const KIND_TEXT: Record<string, string> = {
|
||||
짝: "짝",
|
||||
"우리에 없음": "로직없음",
|
||||
"안 씀": "사용안함",
|
||||
};
|
||||
const LAYER_TEXT: Record<string, string> = { system: "마스터", company: "회사", personal: "개인" };
|
||||
/** 739-1 회사 · 개인 층 장부 화면 제목 */
|
||||
export const MINE_TITLE = "내 제안 목록";
|
||||
const STATE_TONE: Record<string, BadgeTone> = { 승인: "success", 대기: "warning", 반려: "danger" };
|
||||
type Edits = Record<string, LedgerEntry | null>;
|
||||
/** 화면 한 줄 — 서버 줄 위에 고친 것 */
|
||||
export type LedgerView = LedgerRow & { 고친?: boolean };
|
||||
|
||||
export const ledgerKey = (scope: HopyoScope): string =>
|
||||
`m02-hopyo-ledger:${scope.layer}:${scope.owner ?? ""}`;
|
||||
function loadEdits(k: string): Edits {
|
||||
try {
|
||||
return JSON.parse(sessionStorage.getItem(k) ?? "{}") as Edits;
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
function saveEdits(k: string, edits: Edits): void {
|
||||
try {
|
||||
if (Object.keys(edits).length) sessionStorage.setItem(k, JSON.stringify(edits));
|
||||
else sessionStorage.removeItem(k);
|
||||
} catch {
|
||||
/* 캐시 못 씀 = 이 화면에만 남음 */
|
||||
}
|
||||
}
|
||||
|
||||
/** 쓰기 꼴 — 보기 칸(층 · 누가 · 때 …) 뺌 */
|
||||
export const entryOf = (row: LedgerEntry): LedgerEntry => ({
|
||||
원래: row.원래,
|
||||
갈래: row.갈래,
|
||||
짝:
|
||||
row.갈래 === "짝"
|
||||
? row.짝.map((p) => ({ ...p, 입력값: p.입력값 ?? {}, 몫: p.몫 ?? null }))
|
||||
: [],
|
||||
엑셀조항: row.엑셀조항 ?? [],
|
||||
});
|
||||
|
||||
/** 서버 줄 + 고친 것 → 화면 줄 — 이 층 줄은 고친 값 · 지운 것 뺌 · 위 층에서 옮긴 줄은 이 층 줄로 더함 */
|
||||
export function mergeRows(rows: LedgerRow[], edits: Edits, scope: HopyoScope): LedgerView[] {
|
||||
const mine = rows.filter((r) => r.고침);
|
||||
const out: LedgerView[] = [];
|
||||
for (const r of mine) {
|
||||
if (!(r.열쇠 in edits)) out.push(r);
|
||||
else if (edits[r.열쇠]) out.push({ ...r, ...edits[r.열쇠]!, 고친: true });
|
||||
}
|
||||
for (const [key, entry] of Object.entries(edits)) {
|
||||
if (!entry || mine.some((r) => r.열쇠 === key)) continue;
|
||||
out.push({
|
||||
...entry,
|
||||
열쇠: key,
|
||||
누가: null,
|
||||
때: null,
|
||||
처음파일: null,
|
||||
층: scope.layer,
|
||||
주인: scope.owner,
|
||||
고침: true,
|
||||
고친: true,
|
||||
});
|
||||
}
|
||||
return [...out, ...rows.filter((r) => !r.고침)];
|
||||
}
|
||||
|
||||
/** 짝 글 — 「GC000198 × 1 · 몫 0.1 · 소량감률=50」 */
|
||||
export const pairText = (p: LedgerPair): string =>
|
||||
[
|
||||
`${p.로직 ?? p.자재 ?? ""} × ${p.배}`,
|
||||
p.몫 != null ? `몫 ${p.몫}` : "",
|
||||
Object.entries(p.입력값 ?? {})
|
||||
.map(([k, v]) => `${k}=${formatNumber(v)}`)
|
||||
.join(", "),
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" · ");
|
||||
|
||||
/** 찾기 · 갈래 거름 */
|
||||
export const filterRows = (rows: LedgerView[], q: string, kind: string): LedgerView[] => {
|
||||
const want = q.trim().toLowerCase();
|
||||
return rows.filter(
|
||||
(r) =>
|
||||
(!kind || r.갈래 === kind) &&
|
||||
(!want ||
|
||||
`${r.원래.품명} ${r.원래.규격} ${r.짝.map(pairText).join(" ")}`
|
||||
.toLowerCase()
|
||||
.includes(want)),
|
||||
);
|
||||
};
|
||||
|
||||
const why = (error: unknown): string => (error instanceof Error ? error.message : String(error));
|
||||
const td = (text: string, cls = ""): HTMLElement =>
|
||||
el("td", { text, className: cls, attrs: { title: text } });
|
||||
const note = (text: string): HTMLElement =>
|
||||
el("span", { className: "ui-text ui-text--muted", text, attrs: { "data-text": "14" } });
|
||||
|
||||
export interface LedgerOptions {
|
||||
scope: HopyoScope;
|
||||
/** 이 층 쓰기 권한 — 없으면 보기만 */
|
||||
canWrite: boolean;
|
||||
}
|
||||
|
||||
export function buildHopyoLedger(opt: LedgerOptions): HTMLElement {
|
||||
const root = el("div", {
|
||||
className: "m02-hopyo m02-hopyo-ledger",
|
||||
attrs: { "data-hopyo-ledger": "" },
|
||||
});
|
||||
const k = ledgerKey(opt.scope);
|
||||
const master = opt.scope.layer === "system";
|
||||
let edits = loadEdits(k);
|
||||
let rows: LedgerRow[] = [];
|
||||
const search = el("input", {
|
||||
className: "ui-input ui-input--sm m02-hopyo__search",
|
||||
attrs: { type: "search", placeholder: "품명 · 규격 · 로직 찾기" },
|
||||
}) as HTMLInputElement;
|
||||
const kind = el("select", {
|
||||
className: "ui-input ui-input--sm",
|
||||
children: [
|
||||
el("option", { text: "갈래 전체", attrs: { value: "" } }),
|
||||
...KINDS.map((x) => el("option", { text: KIND_TEXT[x], attrs: { value: x } })),
|
||||
],
|
||||
}) as HTMLSelectElement;
|
||||
const save = createButton({ label: "저장", variant: "filled", size: "sm" });
|
||||
const drop = createButton({ label: "변경 취소", variant: "ghost", size: "sm" });
|
||||
const body = el("tbody");
|
||||
const keep = (): void => {
|
||||
saveEdits(k, edits);
|
||||
paint();
|
||||
};
|
||||
const edit = (r: LedgerView, next: Partial<LedgerEntry>): void => {
|
||||
edits[r.열쇠] = entryOf({ ...entryOf(r), ...next });
|
||||
keep();
|
||||
};
|
||||
|
||||
function pairCell(r: LedgerView, mine: boolean): HTMLElement {
|
||||
const pairs: HTMLElement[] = r.짝.map((p, at) => {
|
||||
const parts: HTMLElement[] = [el("span", { text: pairText(p) })];
|
||||
if (mine) {
|
||||
const times = el("input", {
|
||||
className: "ui-input ui-input--sm m02-hopyo__times",
|
||||
attrs: { type: "number", step: "any", min: "0", value: String(p.배) },
|
||||
}) as HTMLInputElement;
|
||||
times.addEventListener("change", () =>
|
||||
edit(r, { 짝: r.짝.map((x, i) => (i === at ? { ...x, 배: Number(times.value) } : x)) }),
|
||||
);
|
||||
parts.push(unitField(times, { from: r.원래.단위, to: "" }));
|
||||
if (p.로직)
|
||||
parts.push(
|
||||
createButton({
|
||||
label: "옵션",
|
||||
variant: "ghost",
|
||||
size: "sm",
|
||||
onClick: () =>
|
||||
void fetchLogicInputs([p.로직!]).then(
|
||||
(got) =>
|
||||
openLaborInput({
|
||||
title: p.로직!,
|
||||
logic: got.로직[0] ?? null,
|
||||
values: p.입력값 as never,
|
||||
memo: null,
|
||||
readOnly: false,
|
||||
done: (res) =>
|
||||
edit(r, {
|
||||
짝: r.짝.map((x, i) => (i === at ? { ...x, 입력값: res.입력값 } : x)),
|
||||
}),
|
||||
}),
|
||||
(error) => showToast(why(error), "error"),
|
||||
),
|
||||
}),
|
||||
createButton({
|
||||
label: "×",
|
||||
variant: "ghost",
|
||||
size: "sm",
|
||||
onClick: () => edit(r, { 짝: r.짝.filter((_, i) => i !== at) }),
|
||||
}),
|
||||
);
|
||||
}
|
||||
return el("div", { className: "m02-hopyo__part", children: parts });
|
||||
});
|
||||
if (mine && r.갈래 === "짝")
|
||||
pairs.push(
|
||||
createButton({
|
||||
label: "항목 추가",
|
||||
variant: "ghost",
|
||||
size: "sm",
|
||||
onClick: () =>
|
||||
// 736-1 별칭 장부가 가리킬 수 있는 것 = 기초 로직 · 재료 둘(탭 막음)
|
||||
openFindPicker({
|
||||
layer: opt.scope.layer,
|
||||
owner: opt.scope.owner,
|
||||
kinds: ["logic", "material"],
|
||||
logic: (x) => edit(r, { 짝: [...r.짝, { 로직: x.키, 입력값: {}, 배: 1, 몫: null }] }),
|
||||
material: (e) =>
|
||||
edit(r, { 짝: [...r.짝, { 자재: e.코드, 입력값: {}, 배: 1, 몫: null }] }),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
return el("td", { className: "m02-hopyo__parts", children: pairs });
|
||||
}
|
||||
|
||||
function rowOf(r: LedgerView): HTMLElement {
|
||||
const mine = opt.canWrite && r.고침;
|
||||
const kindCell = mine
|
||||
? (() => {
|
||||
const s = el("select", {
|
||||
className: "ui-input ui-input--sm",
|
||||
children: KINDS.map((x) => el("option", { text: KIND_TEXT[x], attrs: { value: x } })),
|
||||
}) as HTMLSelectElement;
|
||||
s.value = r.갈래;
|
||||
s.addEventListener("change", () =>
|
||||
edit(r, { 갈래: s.value as LedgerEntry["갈래"], 짝: s.value === "짝" ? r.짝 : [] }),
|
||||
);
|
||||
return el("td", { children: [s] });
|
||||
})()
|
||||
: td(KIND_TEXT[r.갈래] ?? r.갈래);
|
||||
const acts: HTMLElement[] = [];
|
||||
if (mine)
|
||||
acts.push(
|
||||
createButton({
|
||||
label: master ? "삭제" : "거두기",
|
||||
variant: "danger",
|
||||
size: "sm",
|
||||
onClick: () => {
|
||||
edits[r.열쇠] = null;
|
||||
keep();
|
||||
},
|
||||
}),
|
||||
);
|
||||
else if (opt.canWrite && !rows.some((x) => x.고침 && x.열쇠 === r.열쇠) && !edits[r.열쇠])
|
||||
acts.push(
|
||||
createButton({
|
||||
label: "내 층 복사",
|
||||
variant: "ghost",
|
||||
size: "sm",
|
||||
onClick: () => {
|
||||
edits[r.열쇠] = entryOf(r);
|
||||
keep();
|
||||
},
|
||||
}),
|
||||
);
|
||||
const when = [r.누가?.이름, dateTimeText(r.때, ""), r.처음파일].filter(Boolean).join(" · ");
|
||||
return el("tr", {
|
||||
className: `m02-hopyo-ledger__row${r.고침 ? "" : " is-upper"}${r.고친 ? " is-edited" : ""}`,
|
||||
attrs: { "data-ledger": r.열쇠 },
|
||||
children: [
|
||||
td(r.원래.품명),
|
||||
td(r.원래.규격),
|
||||
td(r.원래.단위),
|
||||
kindCell,
|
||||
pairCell(r, mine),
|
||||
td(when),
|
||||
td(r.고침 ? "내 층" : (LAYER_TEXT[r.층] ?? r.층)),
|
||||
el("td", {
|
||||
attrs: { "data-state": r.상태 ?? "" },
|
||||
children: r.고친 || !r.상태 ? [] : [createBadge(r.상태, STATE_TONE[r.상태])],
|
||||
}),
|
||||
el("td", { className: "m02-hopyo__acts", children: acts }),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
function paint(): void {
|
||||
const all = mergeRows(rows, edits, opt.scope);
|
||||
const shown = filterRows(all, search.value, kind.value);
|
||||
const n = Object.keys(edits).length;
|
||||
save.querySelector(".ui-btn__label")!.textContent = n ? `저장 (${n})` : "저장";
|
||||
save.disabled = drop.disabled = !n;
|
||||
body.replaceChildren(
|
||||
...(shown.length
|
||||
? shown.map(rowOf)
|
||||
: [el("tr", { children: [el("td", { text: "장부 줄 없음", attrs: { colspan: "9" } })] })]),
|
||||
);
|
||||
}
|
||||
search.addEventListener("input", paint);
|
||||
kind.addEventListener("change", paint);
|
||||
drop.addEventListener("click", () => {
|
||||
edits = {};
|
||||
keep();
|
||||
});
|
||||
save.addEventListener("click", async () => {
|
||||
save.disabled = true;
|
||||
try {
|
||||
rows = await saveLedger(opt.scope, edits);
|
||||
edits = {};
|
||||
showToast("장부 저장", "success");
|
||||
keep();
|
||||
} catch (error) {
|
||||
showToast(`저장 못 함 — ${why(error)}`, "error");
|
||||
paint();
|
||||
}
|
||||
});
|
||||
|
||||
async function load(): Promise<void> {
|
||||
const stop = showLoading(root);
|
||||
try {
|
||||
rows = await readLedger(opt.scope);
|
||||
} catch (error) {
|
||||
root.replaceChildren(loadingError(why(error), () => void load()));
|
||||
return;
|
||||
} finally {
|
||||
stop();
|
||||
}
|
||||
root.replaceChildren(
|
||||
el("div", {
|
||||
className: "ui-toolbar m02-hopyo__bar",
|
||||
children: [
|
||||
el("strong", { text: master ? LEDGER_TITLE : MINE_TITLE }),
|
||||
note(
|
||||
master
|
||||
? `${LEDGER_HINT} · 여기 짝 = 모든 사용자 불러오기에 자동`
|
||||
: `내 층 짝 = 내 제안(다음 불러오기에서 후보 맨 위 · 자동 아님) · M01 별칭 장부 승인 뒤 자동 · ${L("M02_PrecomboTitle")} 생성 때 남음`,
|
||||
),
|
||||
search,
|
||||
kind,
|
||||
...(opt.canWrite ? [save, drop] : []),
|
||||
],
|
||||
}),
|
||||
el("div", {
|
||||
className: "ui-general-block__table-wrap ui-general-block__table-wrap--sticky-head",
|
||||
children: [
|
||||
el("table", {
|
||||
className: "ui-general-block__table m02-hopyo__table m02-hopyo-ledger__table",
|
||||
children: [
|
||||
el("thead", {
|
||||
children: [
|
||||
el("tr", {
|
||||
children: [
|
||||
"엑셀 품명",
|
||||
"규격",
|
||||
"단위",
|
||||
"갈래",
|
||||
"우리 로직 · 단위 환산 · 몫 · 옵션",
|
||||
"누가 · 때 · 처음 파일",
|
||||
"층",
|
||||
"승인",
|
||||
"",
|
||||
].map((t) => el("th", { text: t })),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
body,
|
||||
],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
);
|
||||
paint();
|
||||
}
|
||||
|
||||
void load();
|
||||
return root;
|
||||
}
|
||||
|
||||
/** 단위 환산 칸 — 「엑셀 1 {from} =」 입력 「{to}」(옛 확인 창 `CheckRows.unitField` 옮김 · 785-7) */
|
||||
function unitField(input: HTMLInputElement, v: { from: string; to: string }): HTMLElement {
|
||||
input.title = L("M02_HopyoUnitTip");
|
||||
const word = (text: string): HTMLElement =>
|
||||
el("span", { className: "m02-hopyo__unit-text", text });
|
||||
return el("span", {
|
||||
className: "m02-hopyo__unit",
|
||||
children: [
|
||||
word(L("M02_HopyoUnitLead").replace("{from}", v.from || "?")),
|
||||
input,
|
||||
...(v.to ? [word(v.to)] : []),
|
||||
],
|
||||
});
|
||||
}
|
||||
@@ -275,7 +275,7 @@ export function buildHopyoSheet(opt: HopyoSheetOptions): HopyoSheetBody {
|
||||
const lastOf = (sid: string): string =>
|
||||
lastColumn(book.시트.find((x) => x.id === sid) ?? first);
|
||||
const more = { ...opt, target, grid, last: lastOf, sheet: () => sheet, host: left };
|
||||
nav = ids.일위대가표 ? Nav.createSheetNav(more) : null;
|
||||
nav = ids.일위대가표 || ids.단가산출표 ? Nav.createSheetNav(more) : null; // 원파일 = 단가산출표 판만(814-1)
|
||||
if (nav) warn.after(nav.root);
|
||||
sk = nav ? createSheetSk({ ...opt, target, nav, side: right, ...(opt.onSkChange ? { onChange: opt.onSkChange } : {}) }) : null; // prettier-ignore
|
||||
sheet = createSpreadsheet(left, book, {
|
||||
@@ -290,6 +290,7 @@ export function buildHopyoSheet(opt: HopyoSheetOptions): HopyoSheetBody {
|
||||
if (no !== null && no !== current) mark(no);
|
||||
},
|
||||
});
|
||||
if (!ids.일위대가표 && ids.단가산출표) sk?.at({ 시트: first.id, 칸: "A1" }); // 일위대가 없음 = 단가산출 목록 먼저
|
||||
nav?.marks(briefs);
|
||||
// 머리 행 칸을 눌러 뗌 → 그 호표 영역 고름(끌기 · 방향키 도중엔 안 바꿈 · 줄 행은 그 줄 그대로)
|
||||
left.querySelector(".ss-grid-host")?.addEventListener("mouseup", (e) => {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
/* =============================================================================
|
||||
* M02_MasterTemplete_Hopyo_Side.ts
|
||||
* 좌측 「일위대가 불러오기」 컨테이너(PLAN 324-4 · 718-1 한시 · 785-4) —
|
||||
* · [불러오기](구조물 조합 층 단추 꼴 `m02-layers__btn`) = 바로 엑셀 고르기 창 → 올림 → 본 칸 불러오기 보기
|
||||
* · [불러오기](구조물 조합 층 단추 꼴 `m02-layers__btn`) = 바로 파일 하나 고르기 창(엑셀 · 경쟁사 .estx · .STC · 814-1) → 올림 → 본 칸 불러오기 보기
|
||||
* (`M02_MasterTemplete_Hopyo_Import` · 엑셀 + 오른쪽 호표 목록 · 가운데 상세 페이지 없음 · 누름 하나)
|
||||
* · 결과 층 고름 = 작은 고름(쓸 수 있는 층 둘 이상일 때만 · 일반 = 개인 하나라 안 보임)
|
||||
* · 「작업 중」 줄 = 층마다 서버 목록(같은 엑셀 작업 중 이어서) · 누르면 본 칸 불러오기 보기
|
||||
* · 다른 엑셀 [불러오기] · 다른 화면으로 = 지금 보기 떠남(올린 파일 지움 · 743-1) — 고르고 안 만든 확인 캐시가 있으면 물음 한 번
|
||||
* 아래 [별칭 장부](338-3) → 본 칸 장부 화면(`M02_MasterTemplete_Hopyo_Ledger` · 층 = 구조물 조합 층).
|
||||
* 옛 [별칭 장부](338-3)는 걷음(815-2) — 별칭은 M01 요소 「별칭」 칸 · M01 별칭 모아 보기.
|
||||
* ========================================================================== */
|
||||
|
||||
import { createButton, el, showConfirmDialog, showToast } from "@ui/ui_template_elements";
|
||||
@@ -16,12 +16,10 @@ import { createSegment } from "@ui/ui_template_segment";
|
||||
import { t as L } from "@ui/ui_template_locale";
|
||||
import {
|
||||
listHopyo,
|
||||
pageScope,
|
||||
uploadHopyo,
|
||||
type HopyoFile,
|
||||
type HopyoScope,
|
||||
} from "./M02_MasterTemplete_Hopyo_Api";
|
||||
import { buildHopyoLedger, LEDGER_HINT, LEDGER_TITLE } from "./M02_MasterTemplete_Hopyo_Ledger";
|
||||
import {
|
||||
buildHopyoImport,
|
||||
LAYERS,
|
||||
@@ -32,8 +30,6 @@ import {
|
||||
import type { ManageLayer } from "./M02_MasterTemplete_Layer";
|
||||
|
||||
export interface HopyoSectionOptions {
|
||||
/** 이 층 쓰기 권한 — 별칭 장부 고침(구조물 조합 층이 제자리에서 바뀌어 열 때 읽음 · 720-4) */
|
||||
canWrite: () => boolean;
|
||||
/** 저장 안 한 고침을 버릴지 확인 — 취소면 false */
|
||||
confirmLeave: () => Promise<boolean>;
|
||||
/** 메인 칸에 화면을 띄움 */
|
||||
@@ -67,25 +63,14 @@ export function buildHopyoSection(opt: HopyoSectionOptions): HopyoSection {
|
||||
});
|
||||
start.classList.add("m02-layers__btn");
|
||||
start.title =
|
||||
"다른 프로그램 일위대가 엑셀 → 우리 일위대가(한시 · 끝내거나 떠나면 올린 파일 지움)";
|
||||
"파일 하나 — 경쟁사 엑셀(.xls · .xlsx) 또는 경쟁사 파일(.estx · .STC) → 우리 정보로 다시 만듦(한시 · 끝내거나 떠나면 올린 파일 지움)";
|
||||
start.dataset.hopyoWizardOpen = "";
|
||||
const seg = el("div", { className: "m02-side__row" });
|
||||
seg.hidden = true;
|
||||
const jobs = el("div", { className: "m02-hopyo-side", attrs: { "data-hopyo-jobs": "" } });
|
||||
const ledger = el("button", {
|
||||
className: "m02-side__item ui-sidebar-row",
|
||||
text: LEDGER_TITLE,
|
||||
attrs: { type: "button", title: LEDGER_HINT, "data-hopyo-ledger-open": "" },
|
||||
});
|
||||
ledger.addEventListener("click", () => void showLedger());
|
||||
const body = el("div", {
|
||||
className: "m02-hopyo-side",
|
||||
children: [
|
||||
el("div", { className: "m02-side__row", children: [start, input] }),
|
||||
seg,
|
||||
jobs,
|
||||
ledger,
|
||||
],
|
||||
children: [el("div", { className: "m02-side__row", children: [start, input] }), seg, jobs],
|
||||
});
|
||||
let open: { key: string; view: HTMLElement; imp?: HopyoImport } | null = null;
|
||||
|
||||
@@ -145,14 +130,10 @@ export function buildHopyoSection(opt: HopyoSectionOptions): HopyoSection {
|
||||
if (!file) return;
|
||||
start.disabled = true;
|
||||
uploadHopyo(scopeOf(pick), file)
|
||||
.then((got: HopyoFile & { 이어서?: boolean; 원파일?: boolean; 붙음?: string[] | null }) => {
|
||||
.then((got: HopyoFile & { 이어서?: boolean; 원파일?: boolean }) => {
|
||||
const tool = /\.estx$/i.test(file.name) ? "EST" : "STmate"; // 810-3 원파일 올림 안내
|
||||
if (got.원파일)
|
||||
showToast(
|
||||
`원파일 · ${tool} — 호표 ${got.호표수}${got.붙음?.length ? ` · 같은 공사 엑셀 ${got.붙음.join(", ")} 에 붙음` : ""}`,
|
||||
"success",
|
||||
);
|
||||
else if (got.이어서) showToast(`같은 엑셀 작업 중 — ${got.키} 이어서`, "info");
|
||||
if (got.이어서) showToast(`같은 파일 작업 중 — ${got.키} 이어서`, "info");
|
||||
else if (got.원파일) showToast(`${tool} 파일 — 단가산출표 · 산출근거`, "success");
|
||||
else showToast(`올림 — 호표 ${got.호표수}`, got.어긋남 ? "warning" : "success");
|
||||
if (got.읽기불가) showToast(`가져온 식으로 열 수 없음 — ${got.읽기불가}`, "warning");
|
||||
return show(pick, got, true);
|
||||
@@ -181,7 +162,6 @@ export function buildHopyoSection(opt: HopyoSectionOptions): HopyoSection {
|
||||
file,
|
||||
onEnded: () => {
|
||||
open = null;
|
||||
ledger.classList.remove("is-active");
|
||||
opt.show(title, el("div", { className: "m02-hopyo" })); // 끝냄 — 빈 본 칸
|
||||
void refresh();
|
||||
},
|
||||
@@ -189,19 +169,6 @@ export function buildHopyoSection(opt: HopyoSectionOptions): HopyoSection {
|
||||
opt.show(`${title} — ${file.파일}`, imp.root);
|
||||
opt.onOpen();
|
||||
open = { key: file.키, view: imp.root, imp };
|
||||
ledger.classList.remove("is-active");
|
||||
void refresh();
|
||||
}
|
||||
|
||||
async function showLedger(): Promise<void> {
|
||||
if (open?.key === LEDGER_TITLE && open.view.isConnected) return;
|
||||
if (!(await leaveOk())) return;
|
||||
open?.imp?.leave();
|
||||
const view = buildHopyoLedger({ scope: pageScope(), canWrite: opt.canWrite() });
|
||||
opt.show(`${title} — ${LEDGER_TITLE}`, view);
|
||||
opt.onOpen();
|
||||
open = { key: LEDGER_TITLE, view };
|
||||
ledger.classList.add("is-active");
|
||||
void refresh();
|
||||
}
|
||||
|
||||
@@ -209,7 +176,6 @@ export function buildHopyoSection(opt: HopyoSectionOptions): HopyoSection {
|
||||
if (open && !open.view.isConnected) {
|
||||
open.imp?.leave(); // 다른 화면으로 = 불러오기 떠남
|
||||
open = null;
|
||||
ledger.classList.remove("is-active");
|
||||
void refresh();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -19,6 +19,7 @@ from pydantic import BaseModel
|
||||
|
||||
from common_util.common_util_auth import verify_session
|
||||
from common_util.common_util_http_cache import cached_file_response
|
||||
from M01_MasterData import M01_MasterData_Parts as parts
|
||||
from M01_MasterData import M01_MasterData_Store as m01
|
||||
from M01_MasterData import M01_MasterData_Store_Make as make
|
||||
from M02_MasterTemplete import M02_Labor_Inputs as labor_inputs
|
||||
@@ -40,7 +41,7 @@ def _read(fn, *args):
|
||||
|
||||
@router.get("/logic-lib/logics", dependencies=[Depends(verify_session)])
|
||||
def get_logics() -> dict:
|
||||
return {"logics": _read(m01.logics, "", "", "", None)}
|
||||
return {"logics": parts.strip(_read(m01.logics, "", "", "", None))} # 부품 문서 뺌(813-2)
|
||||
|
||||
|
||||
@router.get("/logic-lib/subs", dependencies=[Depends(verify_session)])
|
||||
|
||||
@@ -69,7 +69,6 @@ export interface SangeunDetailOptions {
|
||||
/** 수식 탭 저장 뒤 그 SK 새 판(페이지 [저장] 409 막음 · 802-4b-2) */
|
||||
onVersion?: (version: string) => void;
|
||||
slot?: HTMLElement; // 페이지 머리 줄 빈 자리(804-8) — 보기 고르기를 M01 처럼 머리 줄에
|
||||
match?: Parameters<typeof createSangeunSide>[0]; // 원문 패널 판정 띠 단추(810-4)
|
||||
}
|
||||
|
||||
const LIB = { read: "/m02/logic-lib", calc: "/m02/logic-lib" };
|
||||
@@ -104,7 +103,7 @@ export function mountSangeunDetail(
|
||||
const inputsOf = new Map<string, LogicInput[]>();
|
||||
const infoOf = new Map<string, SourceTab>(); // 더한줄 로직 → 원문 탭(752-1)
|
||||
let extraKeys: string[] = [];
|
||||
const side = opt.source ? createSangeunSide(opt.match) : null;
|
||||
const side = opt.source ? createSangeunSide() : null;
|
||||
let seq = 0;
|
||||
let alive = true;
|
||||
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
/* =============================================================================
|
||||
* M02_MasterTemplete_Sangeun_MatchBand.ts
|
||||
* 가져온 식 ↔ 우리 기초 판정 띠(PLAN 810-4 · 요구 707 · 709 · 703) — 원문 패널 「어디서 왔나」 카드 맨 위.
|
||||
* 글 · 단추 = `logic/M02_Sangeun_Match.ts` `matchBand` · 길 POST `/m02/layers/{층}/sangeun/{키}/match`
|
||||
* {할일 찾기 | 바꾸기 | 되돌리기} — 서버가 다시 찾아 「확정」 일 때만 바꿈(화면 판정을 믿지 않음) ·
|
||||
* 바꾸기 · 되돌리기 = 확인창 · 저장 안 한 고침이 있으면 막음(먼저 [저장]) · 끝나면 `done(판)` = 그 SK 를 다시 엶
|
||||
* 층 쓰기 권한 없음(읽기만) = 단추 없음
|
||||
* ========================================================================== */
|
||||
|
||||
import { createBanner } from "@ui/ui_template_banner";
|
||||
import { createButton } from "@ui/ui_template_button";
|
||||
import { el } from "@ui/ui_template_elements";
|
||||
import { askConfirm } from "@ui/ui_template_modal";
|
||||
import { createText } from "@ui/ui_template_text";
|
||||
import { showToast } from "@ui/ui_template_toast";
|
||||
import { call, type Layer } from "./M02_MasterTemplete_Api_Fetch";
|
||||
import { matchBand, type MatchAction } from "./logic/M02_Sangeun_Match";
|
||||
|
||||
export interface MatchCtx {
|
||||
layer: Layer;
|
||||
owner: string | null;
|
||||
key: string;
|
||||
readOnly: boolean;
|
||||
/** 저장 안 한 고침 있음 */
|
||||
dirty: () => boolean;
|
||||
/** 서버가 SK 를 고친 뒤(새 판) — 그 SK 다시 엶 */
|
||||
done: (version: string) => void;
|
||||
}
|
||||
|
||||
const LABEL: Record<Exclude<MatchAction, null>, string> = {
|
||||
찾기: "우리 기초와 연결 찾기",
|
||||
바꾸기: "우리 기초 사본으로 바꾸기",
|
||||
되돌리기: "가져온 식으로 되돌리기",
|
||||
};
|
||||
const ASK: Partial<Record<Exclude<MatchAction, null>, string>> = {
|
||||
바꾸기:
|
||||
"이 단가산출을 우리 기초 사본(찾은 옵션)으로 바꿈 — 키 그대로 · 일위대가 금액이 다시 셈됨 · 원래 가져온 식은 보관",
|
||||
되돌리기: "우리 기초 사본을 원래 가져온 식으로 되돌림 — 가져온 식 금액 그대로",
|
||||
};
|
||||
|
||||
/** 판정 띠 — 가져온 식 · 바꾼 SK 가 아니면 null */
|
||||
export function createMatchBand(sk: unknown, ctx: MatchCtx | undefined): HTMLElement | null {
|
||||
const band = matchBand(sk as Parameters<typeof matchBand>[0]);
|
||||
if (!band) return null;
|
||||
const act = band.action;
|
||||
const actions =
|
||||
act && ctx && !ctx.readOnly && ctx.key
|
||||
? [
|
||||
createButton({
|
||||
label: LABEL[act],
|
||||
variant: act === "바꾸기" ? "filled" : "ghost",
|
||||
size: 24,
|
||||
onClick: (ev) => void run(act, ctx, ev.currentTarget as HTMLButtonElement),
|
||||
}),
|
||||
]
|
||||
: [];
|
||||
const details = band.details.length
|
||||
? el("div", { children: band.details.map((x) => createText(x, "muted", 12)) })
|
||||
: undefined;
|
||||
const root = createBanner({ tone: band.tone, text: band.text, details, actions });
|
||||
root.setAttribute("data-sk-match", act ?? "");
|
||||
return root;
|
||||
}
|
||||
|
||||
async function run(act: Exclude<MatchAction, null>, ctx: MatchCtx, btn: HTMLButtonElement) {
|
||||
if (ctx.dirty()) return void showToast("저장 안 한 고침이 있음 — 먼저 [저장]", "warning");
|
||||
const ask = ASK[act];
|
||||
if (ask && !(await askConfirm({ message: ask, okLabel: LABEL[act] }))) return;
|
||||
const label = btn.textContent;
|
||||
btn.disabled = true;
|
||||
btn.textContent = act === "찾기" ? "찾는 중…" : "바꾸는 중…";
|
||||
try {
|
||||
const owner = ctx.owner ? `?owner=${encodeURIComponent(ctx.owner)}` : "";
|
||||
const path = `/layers/${ctx.layer}/sangeun/${encodeURIComponent(ctx.key)}/match${owner}`;
|
||||
const got = await call<{ 판?: string }>(path, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ 할일: act }),
|
||||
});
|
||||
ctx.done(got.판 ?? "");
|
||||
} catch (e) {
|
||||
showToast(e instanceof Error ? e.message : String(e), "error");
|
||||
btn.disabled = false;
|
||||
btn.textContent = label;
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@
|
||||
* 맨 위 「단가산출 기초 찾기」(807-1 · M01 원문 패널과 같은 `LogicLab_SourceFind`) — 고르면 그 기초 원문(보기만 · 기억 안 함) ·
|
||||
* 지우면 처음 화면 · 처음 화면 = 우리 SK 는 기초 원문 · 가져온 식 SK 는 「어디서 왔나」 카드
|
||||
* (프로그램 · 파일 · 블록 · 행 · 가져온 때 · 표지 요약 + 엑셀 글의 품셈 절 글만 — 찾기 · 잇기 없음)
|
||||
* 카드 맨 위 = 우리 기초 판정 띠(810-4 · `Sangeun_MatchBand`) · 우리 기초 사본으로 바꾼 SK = 보관한 가져온 식 카드
|
||||
* 옛 우리 기초 판정 띠(810-4)는 걷음(815-2) · 옛 바꾼 SK(`가져온보관`) = 보관한 가져온 식 카드(읽기만)
|
||||
* 일위대가 줄 모달 안 상세는 안 만듦(부르는 쪽이 `source` 를 안 켬).
|
||||
* ========================================================================== */
|
||||
|
||||
@@ -23,7 +23,6 @@ import { createSourceView } from "../M01_MasterData/M01_MasterData_UI_LogicLab_S
|
||||
import { createSourceFind } from "../M01_MasterData/M01_MasterData_UI_LogicLab_SourceFind";
|
||||
import { originOf, type Origin, type OriginMeta } from "./logic/M02_Sangeun_Origin";
|
||||
import { tc } from "./M02_MasterTemplete_Labor_Text";
|
||||
import { createMatchBand, type MatchCtx } from "./M02_MasterTemplete_Sangeun_MatchBand";
|
||||
|
||||
export interface SourceTab {
|
||||
label: string;
|
||||
@@ -63,7 +62,7 @@ const tx = (k: keyof typeof TEXT, n = 0): string =>
|
||||
TEXT[k][currentLanguageIndex === 1 ? 1 : 0].replace("{n}", String(n));
|
||||
|
||||
/** 「어디서 왔나」 카드 — 표지는 셋까지(더 = 단추로 펼침) */
|
||||
function originCard(o: Origin, band: HTMLElement | null): HTMLElement {
|
||||
function originCard(o: Origin): HTMLElement {
|
||||
const marks = el("div");
|
||||
const showMarks = (all: boolean): void =>
|
||||
marks.replaceChildren(
|
||||
@@ -82,7 +81,6 @@ function originCard(o: Origin, band: HTMLElement | null): HTMLElement {
|
||||
title: tx("Origin"),
|
||||
pad: 12,
|
||||
body: [
|
||||
...(band ? [band] : []),
|
||||
...(o.요약 ? [el("strong", { text: o.요약, attrs: { "data-origin-native": "" } })] : []),
|
||||
createKv([
|
||||
{ label: tx("Program"), value: o.프로그램 },
|
||||
@@ -91,7 +89,7 @@ function originCard(o: Origin, band: HTMLElement | null): HTMLElement {
|
||||
{ label: tx("Rows"), value: o.행 },
|
||||
{ label: tx("When"), value: o.때 },
|
||||
{ label: tx("Marks"), value: tx("MarksN", o.표지.length) },
|
||||
...(o.확정 ? [{ label: tx("Sure"), value: o.확정 }] : []),
|
||||
...(o.확정 ? [{ label: o.확정이름 ?? tx("Sure"), value: o.확정 }] : []),
|
||||
]),
|
||||
marks,
|
||||
el("strong", { text: tx("Clauses") }),
|
||||
@@ -103,9 +101,9 @@ function originCard(o: Origin, band: HTMLElement | null): HTMLElement {
|
||||
return card;
|
||||
}
|
||||
|
||||
export function createSangeunSide(match?: MatchCtx): {
|
||||
export function createSangeunSide(): {
|
||||
wrap: (main: HTMLElement) => HTMLElement;
|
||||
/** 원문 탭 · SK 문서(`가져온` · 바꾼 SK `가져온보관` — 있으면 첫 화면 = 카드 + 판정 띠) */
|
||||
/** 원문 탭 · SK 문서(`가져온` · 옛 바꾼 SK `가져온보관` — 있으면 첫 화면 = 카드) */
|
||||
set: (tabs: SourceTab[], sk?: unknown) => void;
|
||||
} {
|
||||
const view = createSourceView({
|
||||
@@ -140,8 +138,7 @@ export function createSangeunSide(match?: MatchCtx): {
|
||||
view.show(found);
|
||||
return void content.replaceChildren(view.root);
|
||||
}
|
||||
if (origin)
|
||||
return void content.replaceChildren(originCard(origin, createMatchBand(skDoc, match)));
|
||||
if (origin) return void content.replaceChildren(originCard(origin));
|
||||
if (!tabs.length)
|
||||
return void content.replaceChildren(createText(L("M02_SangeunSourceNone"), "muted"));
|
||||
at = Math.min(at, tabs.length - 1);
|
||||
|
||||
@@ -3,10 +3,8 @@
|
||||
* 단가산출(SK) 상세 자리(717-2a) — 편집 칸 종류 sangeun 이 여는 곳(UI_Main 갈래 한 줄).
|
||||
* 717-2b = 상세 · 고침(`Sangeun_Detail.ts` mountSangeunDetail) · [저장]은 그 손잡이 `getDoc` · 고침 알림 `opt.onChanged`.
|
||||
* 804-8 = 페이지 [저장] 한 번에 수식도(`flush` 먼저 · 새 판) · 보기 고르기는 머리 줄 `slot`.
|
||||
* 810-4 = 원문 패널 판정 띠 [바꾸기] · [되돌리기] · [연결 찾기] 뒤 그 SK 를 다시 읽어 상세를 다시 엶(새 판).
|
||||
* ========================================================================== */
|
||||
|
||||
import { readTemplate } from "./M02_MasterTemplete_Api_Fetch";
|
||||
import { mountSangeunDetail } from "./M02_MasterTemplete_Sangeun_Detail";
|
||||
import type { Selection } from "./M02_MasterTemplete_UI_Main";
|
||||
|
||||
@@ -25,37 +23,9 @@ export function mountSangeunView(
|
||||
doc: unknown,
|
||||
opt: SangeunViewOptions,
|
||||
): { getDoc: () => Promise<unknown>; destroy: () => void } {
|
||||
let dirty = false;
|
||||
const onChanged = (d: boolean): void => ((dirty = d), opt.onChanged(d));
|
||||
/** 판정 띠 단추(810-4) — 서버가 SK 를 고친 뒤 새 판 · 그 SK 를 다시 읽어 상세를 다시 엶 */
|
||||
const match = {
|
||||
layer: opt.layer,
|
||||
owner: opt.owner ?? null,
|
||||
key: opt.name,
|
||||
readOnly: opt.readOnly,
|
||||
dirty: () => dirty,
|
||||
done: (version: string): void => {
|
||||
opt.onVersion?.(version);
|
||||
void readTemplate(opt.layer, "sangeun", opt.name, null, opt.owner ?? null).then((got) => {
|
||||
if (!alive) return;
|
||||
one.destroy();
|
||||
one = mount(got.문서);
|
||||
onChanged(false);
|
||||
});
|
||||
},
|
||||
};
|
||||
const mount = (d: unknown) =>
|
||||
mountSangeunDetail(host, d, {
|
||||
...opt,
|
||||
onChanged,
|
||||
owner: opt.owner ?? null,
|
||||
source: true,
|
||||
match,
|
||||
}); // 752-1
|
||||
let alive = true;
|
||||
let one = mount(doc);
|
||||
const one = mountSangeunDetail(host, doc, { ...opt, owner: opt.owner ?? null, source: true }); // 752-1
|
||||
return {
|
||||
getDoc: () => one.flush().then(() => one.getDoc()),
|
||||
destroy: () => ((alive = false), one.destroy()),
|
||||
destroy: () => one.destroy(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ const STUB_DRAWING = {
|
||||
export interface SideOptions {
|
||||
/** 시스템 관리자 — 도면 양식(시스템 층에만 있음)을 늘 보임(720-4) */
|
||||
isSystem: boolean;
|
||||
/** 지금 구조물 조합 층을 고칠 수 있나(층이 제자리에서 바뀜) — 일위대가 불러오기 장부 */
|
||||
/** 지금 구조물 조합 층을 고칠 수 있나(층이 제자리에서 바뀜) */
|
||||
canWrite: () => boolean;
|
||||
onOpen: (sel: Selection | null) => void;
|
||||
getOpen: () => Selection | null;
|
||||
@@ -164,7 +164,6 @@ export function buildSide(opt: SideOptions): SideHandle {
|
||||
const drawing = section(L("M02_KindDrawing"), bodies.get("drawing")!, ...actionRow("drawing"));
|
||||
// 일위대가 불러오기(324-4 · 제목은 718-1) · 층 = 구조물 조합 층 · 쓰기 권한 없으면 올리기 꺼짐
|
||||
const hopyo = buildHopyoSection({
|
||||
canWrite: opt.canWrite,
|
||||
confirmLeave: opt.confirmLeave,
|
||||
show: opt.showView,
|
||||
onOpen: paint,
|
||||
|
||||
@@ -12,11 +12,8 @@ from __future__ import annotations
|
||||
import io
|
||||
import re
|
||||
import warnings
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from M02_MasterTemplete import M02_Hopyo_ReadEst as read_est
|
||||
|
||||
STM_SHEETS = (
|
||||
"단가산출근거",
|
||||
"단가산출근거목록표",
|
||||
@@ -25,7 +22,15 @@ STM_SHEETS = (
|
||||
"경비목록표",
|
||||
"중기목록표",
|
||||
)
|
||||
EST_SHEETS = ("단가산출총괄표", "단가산출", "기계경비총괄표", "자재단가", "노임단가", "견적단가")
|
||||
EST_SHEETS = (
|
||||
"단가산출총괄표",
|
||||
"단가산출",
|
||||
"기계경비총괄표",
|
||||
"기계경비",
|
||||
"자재단가",
|
||||
"노임단가",
|
||||
"견적단가",
|
||||
)
|
||||
_X7, _X5 = "_x0007_", "_x0005_"
|
||||
_HEAD = re.compile(r"BDCOD\|([A-Z]\d+).*?POSS\|(\d+).*?POSE\|(\d+)")
|
||||
|
||||
@@ -233,23 +238,3 @@ def est_prices(book: dict[str, list]) -> tuple[list[dict[str, Any]], dict[int, d
|
||||
one = {"종류": "산근", "이름": f"단산{n}", "규격": ""}
|
||||
out.append({**one, "비목": k, "값": san[n][k]})
|
||||
return out, san
|
||||
|
||||
|
||||
def est_blocks(book: dict[str, list]) -> list[dict[str, Any]]:
|
||||
"""→ [{번, 이름, 단위, 글 [줄], 글행 [행]}]."""
|
||||
blocks = read_est.read(book.get(read_est.SHEET))
|
||||
keep = ("번", "이름", "단위", "글", "글행")
|
||||
return [{k: b[k] for k in keep} for b in blocks.values()]
|
||||
|
||||
|
||||
def load(path: str | Path) -> dict[str, Any]:
|
||||
"""파일 → {꼴, 블록, 단가(STmate dict | EST 목록), 총괄(EST)}."""
|
||||
path = Path(path)
|
||||
book = sheets(path.name, path.read_bytes())
|
||||
kind = kind_of(book)
|
||||
if kind == "STmate":
|
||||
return {"꼴": kind, "블록": stmate_blocks(book), "단가": stmate_prices(book)}
|
||||
if kind == "EST":
|
||||
prices, san = est_prices(book)
|
||||
return {"꼴": kind, "블록": est_blocks(book), "단가": prices, "총괄": san}
|
||||
raise ValueError("STmate · EST 산근 시트 없음")
|
||||
|
||||
+146
-126
@@ -1,6 +1,6 @@
|
||||
"""경쟁사 산근 → 우리 엔진 로직 줄 꼴 내보내기(PLAN 793-2a · 요구서_652 · eval 없음).
|
||||
|
||||
793-1 줄 나무(`M02_San_Stmate` · `M02_San_Est`) → `{로직, 원본}` — 꼴 = 793-2 가져온 식 SK
|
||||
793-1 줄 나무(`M02_San_Stmate`) → `{로직, 원본}`(EST = 원파일 · 엑셀 둘 다 `M02_Doc_NativeEst` · 812-1) — 꼴 = 793-2 가져온 식 SK
|
||||
만들기 몸(`M02_Sangeun_Imported.py` 머리) · 로직 = `_틀.md` 7 장 꼴(셈 = `master_formula.run` →
|
||||
`master_cut.engine` · `요소` 는 SK 서버가 채움).
|
||||
- 로직 = {이름, 결과단위, 입력 [], 중간 [{이름, 식, 표시?}], 호표,
|
||||
@@ -28,12 +28,13 @@ from typing import Any
|
||||
|
||||
from M02_MasterTemplete import M02_San_Eval as ev
|
||||
from M02_MasterTemplete import M02_San_Expr as ex
|
||||
from M02_MasterTemplete.M02_San_Link import combo_link, hourly_text # noqa: F401 — 밖에서 em. 으로 부름
|
||||
from M02_MasterTemplete.M02_San_Link import m01 as _m01
|
||||
|
||||
COMP = {"L": "노무비", "M": "재료비", "S": "경비"}
|
||||
_CODE = {v: k for k, v in COMP.items()}
|
||||
_SUM = "@합{}@" # 누계 % 밑 자리(블록 끝에 `finish` 가 채움)
|
||||
KIND = {"L": "인력", "M": "재료", "X": "기계", "D": "단가산출", "B": "단가산출"}
|
||||
KIND_EST = {"노임": "인력", "자재": "재료", "기계": "기계", "견적": "고정", "산근": "단가산출"}
|
||||
RESERVED = {"노무비", "재료비", "경비", "계", "번지", "단계", "줄", "찾기", "로직", "그리고"}
|
||||
RESERVED |= {"또는", "만약", "올림", "버림", "반올림", "최소", "최대"}
|
||||
_KEY = re.compile(r"[A-Z]{2}\d{6}")
|
||||
@@ -176,40 +177,55 @@ def reprice(logic: dict, where: dict, price: dict[str, float]) -> int:
|
||||
return n
|
||||
|
||||
|
||||
def _m01(kind: str, src: dict) -> tuple[str | None, str | None]:
|
||||
"""1차 줄 판정 유일 짝 M01 키 → (키, 표지) — 없음 = (None, None) · 여럿 = (None, 표지 · 유가 ·
|
||||
환율만 — 재료 · 인력 · 기계는 지금처럼 조용히 이음 없음). 재료 줄 이름이 M01 유가전력(MO)이면
|
||||
유가 · 화폐 이름 그대로(달러 · USD …)면 환율(FX)로 판정(797-1 · 「유로폼」 같은 글 속 낱말은 아님)."""
|
||||
if kind not in ("인력", "재료", "기계"):
|
||||
return None, None
|
||||
from M02_MasterTemplete import M02_Hopyo_Elem as elem
|
||||
from M02_MasterTemplete import M02_Hopyo_Row as hrow
|
||||
# ── 기계 부분(812-9 · 조합 기계 = 줄 둘) ─────────────────────────────────
|
||||
def machine_parts(src: dict, same: list | None = None) -> list[dict]:
|
||||
"""기계 자원 → 부분 찾기(`M02_Machine_Find.find_all` · 조합 = 둘 이상) · 기계 아님 = [] ·
|
||||
same = 같은 파일에서 구성이 같은 단독 기계 [[이름, 규격]](816-2 · 있으면 「후보 여럿」을 가름)."""
|
||||
if src.get("갈래") != "기계":
|
||||
return []
|
||||
from M02_MasterTemplete import M02_Machine_Find as mfind
|
||||
|
||||
line = {k: src.get(k) or "" for k in ("이름", "규격", "단위")}
|
||||
if kind == "재료":
|
||||
if elem.oil_rows(line["이름"]):
|
||||
kind = "유가"
|
||||
elif line["이름"].strip().upper() in {x.upper() for pair in hrow.CURRENCY for x in pair}:
|
||||
kind = "환율"
|
||||
if kind == "유가": # MO 전부 → 줄 규격이 상세구분(전국 · 지역)이면 그것만
|
||||
got = [
|
||||
{"키": r["키"], "규격": str(r.get("상세구분") or "")}
|
||||
for r in elem.oil_rows(line["이름"])
|
||||
]
|
||||
spec = line["규격"].replace(" ", "")
|
||||
got = (
|
||||
[x for x in got if x["규격"] == spec]
|
||||
or [x for x in got if spec and spec in x["규격"]]
|
||||
or got
|
||||
)
|
||||
return mfind.narrow(mfind.find_all(src.get("이름"), src.get("규격")), same)
|
||||
|
||||
|
||||
def first_spec(one: dict, parts: list[dict]) -> dict:
|
||||
"""조합 기계 첫 줄 규격 = 그 기계 규격(칩 글 · 「0.7+브레이카」 아님 · 814-1) — 자원 원문은 그대로."""
|
||||
if len(parts) > 1 and parts[0].get("규격") is not None:
|
||||
one["규격"] = parts[0]["규격"]
|
||||
return one
|
||||
|
||||
|
||||
def part_line(main: dict, part: dict, names: "Names") -> dict:
|
||||
"""조합 기계 다음 줄 — 같은 수량 식 · 값 0(파일 경비 = 한 덩이 = 첫 줄) · 제 이음."""
|
||||
res = {**main["자원"], "이름": part["이름"], "규격": part["규격"], "찾기": part, "조합": True}
|
||||
res.pop("조합첫", None)
|
||||
res["단가"] = dict.fromkeys(res.get("단가") or COMP, 0)
|
||||
title = part["후보"][0]["이름"] if part["키"] else part["이름"]
|
||||
one = {"이름": names.item(str(title)), "규격": part["규격"], "단위": main.get("단위") or ""}
|
||||
one |= {"수량": main["수량"], "자원": res}
|
||||
if main.get("종류") == "로직": # 우리 조합 식(814-1 `조합식`)에 든 다음 줄 = 이음 없음 · 값 0
|
||||
link = None if res.get("조합식") else hourly_text(part["키"])
|
||||
one |= {"종류": "로직", "값묶음": bundle(res), "이음": link}
|
||||
else:
|
||||
got = hrow.m01_picks(kind, line)
|
||||
keys = {x.get("키") for x in got if x.get("키")}
|
||||
if len(keys) == 1:
|
||||
return keys.pop(), None
|
||||
if len(keys) > 1 and kind in ("유가", "환율"):
|
||||
return None, f"{NOTE}{kind} 후보 여럿({len(keys)}) · 사람 확인"
|
||||
return None, None
|
||||
one |= {"종류": main["종류"], "비목": main["비목"], "단가": Decimal(0), "이음": part["키"]}
|
||||
return one | ({"자리": main["자리"]} if main.get("자리") else {})
|
||||
|
||||
|
||||
def part_marks(src: dict, parts: list[dict]) -> list[str]:
|
||||
"""부분 표지 — 못 이은 부분(후보 여럿 · 못 찾음) · 조합 기계(경비 한 덩이 = 첫 줄 · 다음 줄 0)."""
|
||||
from M02_MasterTemplete import M02_Machine_Find as mfind
|
||||
|
||||
out = [m for p in parts if (m := mfind.mark(p, p["이름"], p["규격"]))]
|
||||
if len(parts) > 1:
|
||||
said = " ".join(x for x in (str(src.get("이름") or ""), str(src.get("규격") or "")) if x)
|
||||
names = " + ".join(
|
||||
f"{p['후보'][0]['이름']} {p['후보'][0]['규격']}" if p["키"] else p["이름"]
|
||||
for p in parts
|
||||
)
|
||||
out.append(
|
||||
f"{NOTE}조합 기계 — 경비는 한 덩이 「{said}」 = {names} · 엑셀 값은 첫 줄 · 다음 줄 0"
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
# ── 내보낼 로직 틀 ─────────────────────────────────────────────────────
|
||||
@@ -291,6 +307,7 @@ class _Stm:
|
||||
self.open: list[tuple[int, list[str]]] = [] # 위 단계가 아직 안 담은 단계(엔진 `_cover`)
|
||||
self.out: dict[str, int] = {} # 제외 단계 줄 → 그 계 줄까지 줄 수(합에서 빠지는 때)
|
||||
self.fix: list[tuple[dict, int]] = [] # 누계 % 덧줄 · 그때 줄 수
|
||||
self.noted: set[tuple] = set() # 기계 표지 단 자원(812-9 · 블록에 한 번)
|
||||
|
||||
def brackets(self, node: tuple, src: dict, kept: dict) -> None:
|
||||
"""끼움 `[Q=0.208]` → 중간(왼쪽부터 · 원문 차례)."""
|
||||
@@ -378,6 +395,8 @@ class _Stm:
|
||||
unt = (pr.get("meta") or {}).get("UNT")
|
||||
one_, to = node["자원"]["비목"], node["자원"]["옮김"]
|
||||
name = _label(node) or str(pr.get("이름") or code)
|
||||
if node.get(_MERGED): # 814-1 — 기계 세 줄(노 · 재 · 경) = 기계 한 줄 · 이름 = 기계
|
||||
name = str(pr.get("이름") or code)
|
||||
if unt in ev.SPECIAL: # 특수 % — 처음부터 비목 누계 · 앞 계표시
|
||||
dst = to or {"L": "L", "M": "M", "S": "S"}.get(code[0])
|
||||
if unt == "T%":
|
||||
@@ -396,11 +415,26 @@ class _Stm:
|
||||
if not one_ and len(sole) == 1: # 비목 꼬리 없어도 값 든 비목 하나면 단가 줄(셈 같음)
|
||||
one_, to = sole[0], None
|
||||
res |= {"비목": one_, "옮김": to} if one_ else {}
|
||||
link, many = _m01(res["갈래"], res) if self.keys else (None, None)
|
||||
one = resource(o.names.item(name), pr.get("단위"), cf, res, link)
|
||||
parts = machine_parts(res) if self.keys else [] # 812-9 기계 찾기 · 조합 = 줄 둘
|
||||
if parts:
|
||||
res["찾기"], link, many = parts[0], parts[0]["키"], None
|
||||
res |= {"조합첫": True} if len(parts) > 1 else {}
|
||||
else:
|
||||
link, many = _m01(res["갈래"], res) if self.keys else (None, None)
|
||||
one = first_spec(resource(o.names.item(name), pr.get("단위"), cf, res, link), parts)
|
||||
if parts and one["종류"] == "로직":
|
||||
one["이음"] = combo_link(one, parts) or hourly_text(link)
|
||||
self.line(one, node, True, src)
|
||||
if many:
|
||||
o.glyph(src["행"], src["글"], mark=many)
|
||||
for at, glyph in node.get(_MERGED) or []: # 묶은 다른 두 줄 원문 = 같은 자리
|
||||
o.glyph(at, glyph, one["이름"], self.rows.get(at, {}).get("C"))
|
||||
bare = {k: v for k, v in node.items() if k not in ("번지", _MERGED)}
|
||||
for p in parts[1:]:
|
||||
self.line(part_line(one, p, o.names), bare, True, src)
|
||||
seen = (res.get("이름"), res.get("규격"))
|
||||
marks = [] if seen in self.noted else part_marks(res, parts)
|
||||
self.noted.add(seen)
|
||||
for m in [*marks, *([many] if many else [])]:
|
||||
o.glyph(src["행"], src["글"], mark=m)
|
||||
|
||||
def rate(self, node: dict, src: dict, cf: str) -> None:
|
||||
rate, o = node["율"], self.o
|
||||
@@ -478,11 +512,84 @@ def _ratios(meta: dict) -> str:
|
||||
return " · ".join(f"{n} {v}" for n, v in pairs if v is not None and abs(_num(v) - 1) > 1e-9)
|
||||
|
||||
|
||||
_MERGED = "_묶음줄" # 세 줄을 묶은 줄 나무 — 나머지 두 줄 [(행, 글)]
|
||||
_BAD = ("셈 못 함", "엔진 꼴 없음")
|
||||
|
||||
|
||||
def triples(nodes: list[dict]) -> list[list[int]]:
|
||||
"""814-1 — 같은 기계(X 코드)의 노 · 재 · 경 세 참조 줄(같은 수량 식 · 번지 · 빼기 · 옮김 없음) 자리
|
||||
[[i, j, k], …] — 사이 빈 줄은 건너뜀."""
|
||||
out, k = [], 0
|
||||
while k < len(nodes):
|
||||
at, j = [k], k + 1
|
||||
while len(at) < 3 and j < len(nodes):
|
||||
if nodes[j].get("종류") == "빈" and not str(nodes[j].get("글") or "").strip():
|
||||
j += 1
|
||||
continue
|
||||
at.append(j)
|
||||
j += 1
|
||||
if len(at) == 3 and _same_machine([nodes[i] for i in at]):
|
||||
out.append(at)
|
||||
k = j
|
||||
else:
|
||||
k += 1
|
||||
return out
|
||||
|
||||
|
||||
def merge(nodes: list[dict], groups: list[list[int]]) -> list[dict]:
|
||||
"""세 줄 자리 → 비목 없는 한 줄(값묶음 · 조합이면 그 뒤 기계마다 줄) · 나머지 두 줄 원문은 같은 자리."""
|
||||
first = {g[0]: g for g in groups}
|
||||
drop = {i for g in groups for i in g[1:]}
|
||||
out = []
|
||||
for i, node in enumerate(nodes):
|
||||
if i in first:
|
||||
res = {**node["자원"], "비목": None, "옮김": None}
|
||||
rest = [(nodes[j].get("행"), nodes[j].get("글") or "") for j in first[i][1:]]
|
||||
out.append({**node, "자원": res, _MERGED: rest})
|
||||
elif i not in drop:
|
||||
out.append(node)
|
||||
return out
|
||||
|
||||
|
||||
def _same_machine(three: list[dict]) -> bool:
|
||||
if any(x.get("종류") != "참조" or x.get("오류") or not x.get("자원") for x in three):
|
||||
return False
|
||||
res = [x["자원"] for x in three]
|
||||
if not str(res[0].get("코드") or "").startswith("X") or len({r["코드"] for r in res}) != 1:
|
||||
return False
|
||||
if sorted(str(r.get("비목")) for r in res) != ["L", "M", "S"] or any(
|
||||
r.get("옮김") for r in res
|
||||
):
|
||||
return False
|
||||
return all(len({repr(x.get(k)) for x in three}) == 1 for k in ("식", "번지", "빼기"))
|
||||
|
||||
|
||||
def stmate(
|
||||
block: dict, nodes: list[dict], prices: dict, rule: str, keys: bool = False, file: str = ""
|
||||
) -> dict:
|
||||
"""STmate 블록(`M02_San_Book.stmate_blocks` 한 개) + 줄 나무 + 같은 책 단가 → {로직, 원본}.
|
||||
rule = 블록 끝전(비목버림 | 합버림_큰나머지 · 파일 설정) · keys = 자원 M01 키(이음) 찾기."""
|
||||
rule = 블록 끝전(비목버림 | 합버림_큰나머지 · 파일 설정) · keys = 자원 M01 키(이음) 찾기 ·
|
||||
기계 세 줄 = 한 줄(`triples` · 814-1) — 묶어서 셈 못 하는 줄이 생기면 묶지 않은 꼴."""
|
||||
groups = triples(nodes)
|
||||
out = _stmate(block, merge(nodes, groups), prices, rule, keys, file)
|
||||
bad = _bad(out)
|
||||
if not groups or not bad:
|
||||
return out
|
||||
took: list[
|
||||
list[int]
|
||||
] = [] # 묶어서 셈 못 하는 세 줄만 그대로(율 범위 C · 누계 % 가 비목 하나를 부름)
|
||||
base = _bad(_stmate(block, nodes, prices, rule, keys, file))
|
||||
for g in groups:
|
||||
if _bad(_stmate(block, merge(nodes, [*took, g]), prices, rule, keys, file)) <= base:
|
||||
took.append(g)
|
||||
return _stmate(block, merge(nodes, took), prices, rule, keys, file)
|
||||
|
||||
|
||||
def _bad(out: dict) -> int:
|
||||
return sum(1 for m in out["원본"]["표지"] if any(b in m for b in _BAD))
|
||||
|
||||
|
||||
def _stmate(block: dict, nodes: list[dict], prices: dict, rule: str, keys: bool, file: str) -> dict:
|
||||
me = prices.get(block["코드"]) or {}
|
||||
meta = me.get("meta") or {}
|
||||
o = Out(block.get("이름"), me.get("단위"), "STmate", block["코드"])
|
||||
@@ -531,90 +638,3 @@ def stmate(
|
||||
more["계약"] = {"낙찰률": float(meta["NAG"]), "금액": _cols(block["머리"])}
|
||||
end = "큰나머지" if rule.startswith("합버림") else "비목버림"
|
||||
return o.done(end, adjust, _cols(kye), "단가산출근거", file, **more)
|
||||
|
||||
|
||||
# ── EST ────────────────────────────────────────────────────────────────
|
||||
_VAR = re.compile(r"([A-Za-z][A-Za-z0-9_]*)\s*(?:\([^)]*\))?\s*=")
|
||||
|
||||
|
||||
def _est_res(role: dict, value: float, comp: str | None) -> dict:
|
||||
"""EST 단가 토큰 정체 → 자원 이음 열쇠(옮김 = 성분 비목이 단가 비목과 다를 때)."""
|
||||
p = role["정체"]
|
||||
pc = p["비목"] if p["비목"] in COMP else (comp or "S")
|
||||
res = {"꼴": "EST", "갈래": KIND_EST.get(p["종류"], "고정"), "종류": p["종류"]}
|
||||
res |= {"이름": p["이름"], "규격": p["규격"], "단위": None}
|
||||
res |= {"단가": {pc: value}}
|
||||
res |= {"비목": pc, "옮김": comp if comp and comp != pc else None, "열쇠비목": p["비목"]}
|
||||
return res | ({"모호": True} if role.get("모호") else {})
|
||||
|
||||
|
||||
def est(block: dict, lines: list[dict], want: dict, keys: bool = False, file: str = "") -> dict:
|
||||
"""EST 블록(`M02_San_Book.est_blocks` 한 개) + 복원 줄(`M02_San_Est.parse_block`) +
|
||||
총괄표 성분 want {T, L, M, S} → {로직, 원본} · 성분을 낸 줄만 합(나머지 = 합밖)."""
|
||||
des = ev.designate(lines, want)
|
||||
mine = {k: c for c, k in des.items() if isinstance(k, int)}
|
||||
o = Out(block.get("이름"), block.get("단위"), "EST", block.get("번"))
|
||||
rows = block.get("글행") or []
|
||||
if want.get("칸"): # 당초 칸 사용 · 적용 ≠ 당초(797-1a)
|
||||
o.marks.append(f"{NOTE}{want['칸']}")
|
||||
for c, k in des.items():
|
||||
if k is None:
|
||||
o.marks.append(f"성분 {COMP[c]} {want[c]} 을 낸 줄 없음")
|
||||
val = ev.run_est(lines)
|
||||
ref: dict[int, str] = {} # 차례 → 엔진 부름 글(앞줄 · 숨은 합)
|
||||
for L in lines:
|
||||
k, form, kind = L["차례"], L["꼴"], L.get("종류")
|
||||
at, glyph = rows[k] if k < len(rows) else k, L["원글"]
|
||||
comp = mine.get(k)
|
||||
if form == "빈":
|
||||
continue
|
||||
if form == "변수" and L.get("값") is not None:
|
||||
m = _VAR.search(L["원글"])
|
||||
eng = o.names.line(m.group(1) if m else "변수")
|
||||
o.middle(eng, num(L["값"]), None, at, glyph) # 얼린 값(금액엔 보인 값이 박힘)
|
||||
continue
|
||||
if form not in ("돈", "값"):
|
||||
shown = L.get("보인") if form == "글끝" else L.get("값") if form == "식못읽음" else None
|
||||
if shown is None:
|
||||
o.glyph(at, glyph)
|
||||
else: # 금액 줄 못 읽음 — 보인 값 얼림(글줄 값으로 보존)
|
||||
o.glyph(at, glyph, value=shown, mark=f"{NOTE}금액 줄 못 읽음 · 보인 값 얼림")
|
||||
continue
|
||||
head = {} if comp else {"합밖": True}
|
||||
try:
|
||||
place = _place(L.get("규칙"))
|
||||
if k not in val:
|
||||
raise EmitError("보인 값이 되는 식 · 자리 못 찾음 — 보인 값으로 얼림")
|
||||
if kind == "단가줄":
|
||||
i, role = next((i, r) for i, r in L["역할"].items() if r["갈래"] == "단가")
|
||||
price = next(n[1] for n in ex.numbers(L["식"]) if n[2] == i)
|
||||
res = _est_res(role, price, comp)
|
||||
link, many = _m01(res["갈래"], res) if keys else (None, None)
|
||||
qty = text(L["식"], {}, {i: "1"})
|
||||
one = resource(o.names.item(L.get("라벨") or ""), None, qty, res, link)
|
||||
one |= {**head, **({"자리": place} if place else {})}
|
||||
o.add_line(one, True, at, glyph, L["값"])
|
||||
if many:
|
||||
o.glyph(at, glyph, mark=many)
|
||||
if role.get("모호"): # 임의 선택 아님 — 후보를 사람이 고름
|
||||
o.glyph(at, glyph, mark=f"{NOTE}단가 후보 여럿({role['남은']}) · 사람 확인")
|
||||
elif role.get("갈림") in ("문맥", "기계 세 줄"):
|
||||
o.glyph(at, glyph, mark=f"{NOTE}추정 — 단가 정체 근거 {role['갈림']}")
|
||||
ref[k] = f"줄.'{one['이름']}'"
|
||||
continue
|
||||
if kind == "값줄":
|
||||
parts = [ref.get(j) or num(lines[j]["값"]) for j in L["숨은"]["줄"]]
|
||||
formula = " + ".join(parts)
|
||||
else:
|
||||
swap = {i: ref[r["줄"]] for i, r in L["역할"].items() if r.get("줄") in ref}
|
||||
formula = text(L["식"], {}, swap)
|
||||
except EmitError as error:
|
||||
o.glyph(at, glyph, mark=str(error))
|
||||
formula, place = num(L["값"]), None
|
||||
name = o.names.line(L.get("라벨") or "줄")
|
||||
one = {"이름": name, "식": formula, "비목": COMP[comp or L.get("비목") or "S"]}
|
||||
o.add_line(one | head | ({"자리": place} if place else {}), False, at, glyph, L["값"])
|
||||
if k in val:
|
||||
ref[k] = name
|
||||
check = _money(want) | {"계": want["T"]}
|
||||
return o.done("비목버림", None, check, "단가산출", file)
|
||||
|
||||
@@ -1,42 +1,19 @@
|
||||
"""EST 산근 보이는 글 → 식 복원(PLAN 793-1 · 요구서_652 · 문법 = `_분석_648-A` 3 장 · eval 없음).
|
||||
"""EST 식 글 토큰(PLAN 793-1 · 812-1 · eval 없음) — 원파일 산근 변환(`M02_Doc_NativeEst`)이 쓰는 수 · 토큰 읽기만.
|
||||
|
||||
엑셀엔 식이 없고 인쇄 글뿐 — 「라벨 : 식 = 결과」 로 나눠 식을 되살림. 셈은 `M02_San_Eval.run_est`.
|
||||
|
||||
줄 꼴(`꼴`) — 빈 · 그림(분수 ─) · 글 · 글끝(끝이 결과 아님) · 식못읽음 · 변수 · 값 · 돈.
|
||||
- 돈(`종류` 단가줄 · 참조줄 · 상수줄) — 라벨 · 비목(라벨 끝 노무비 · 재료비 · 경비) ·
|
||||
값(보인 결과) · 식(`M02_San_Expr` 나무) ·
|
||||
역할 {수 토큰 차례: 단가(후보 · 정체 · 모호) | 앞줄(같은 블록 앞 결과 줄 차례) | 상수} ·
|
||||
규칙(보인 결과가 되는 줄 자리 속성 · 기본 버림1)
|
||||
- 값(`종류` 값줄 「소 계 : 1,355.9」) — 숨은 = {갈래 복사 | 합, 줄 [차례], 규칙}(앞 결과 하나 또는
|
||||
바로 앞 돈 · 값 줄 이음 합)
|
||||
- 변수(`종류` 변수식 · 변수값 · 변수기호 · 변수글) — 값 · 규칙 = 얼림(금액엔 반올림된 값이 박힘)
|
||||
옛 엑셀 글 추정 사슬(줄 꼴 · 역할 · 값 역색인 · `parse_block`)은 812-1 엑셀 읽기기(`M02_San_Xlsx`)로 바뀌어 걷음.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import collections
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
from M02_MasterTemplete import M02_San_Expr as ex
|
||||
from M02_MasterTemplete.M02_San_Book import flat
|
||||
|
||||
COMPW = {"노무비": "L", "재료비": "M", "경비": "S"}
|
||||
NUMRE = r"\d{1,3}(?:,\d{3})+(?:\.\d*)?|\d+(?:\.\d*)?|\.\d+"
|
||||
EPS = ex.EPS
|
||||
OPS = {"×": "*", "x": "*", "X": "*", "*": "*", "÷": "/", "/": "/", "+": "+", "-": "-"}
|
||||
OPS |= {"-": "-", "−": "-"}
|
||||
_UNIT = re.compile(r"\s*(?:[A-Za-z가-힣㎡㎥㎏ℓ㎝㎜°˚’'\"Øø]+(?:\s*/\s*[A-Za-z가-힣㎡㎥㎏ℓ]+)?)")
|
||||
VARLINE = re.compile(r"^\s*(?:[○●◦·\-]\s*)?[A-Za-z][A-Za-z0-9_]*\s*(?:\([^)]*\))?\s*=")
|
||||
RES = re.compile(
|
||||
r"^\s*(?:[A-Z]\s+)?("
|
||||
+ NUMRE
|
||||
+ r")\s*\.?\s*(?:원)?\s*(?:[₩\\]?\s*/\s*\S+)?\s*(?:\([^()]*\))?\s*$"
|
||||
)
|
||||
_PLAIN = re.compile(r"[\s\d.,×÷+\-()A-Za-z가-힣㎡㎥]*")
|
||||
_LABEL = re.compile(r"^(?P<l>[^::=]{0,40}?)\s*[::]\s*(?P<r>.*)$")
|
||||
TAIL = re.compile(r"^\s*(" + NUMRE + r")\s*\.?\s*[::][^=]*$") # 결과 뒤 꼬리 글 「3,094.2 : 채집」
|
||||
_HEADING = re.compile(r"^\s*(\d+|[가-하])\s*[.)]")
|
||||
|
||||
|
||||
def num(s: str) -> float:
|
||||
@@ -112,214 +89,3 @@ def tokens(seg: str) -> list[tuple] | None:
|
||||
if depth != 0 or want or not any(t[0] in ("연산", "%") for t in out):
|
||||
return None
|
||||
return out
|
||||
|
||||
|
||||
def calc(node: tuple, sub: dict | None = None) -> float | None:
|
||||
try:
|
||||
return ex.value(node, {"V": {}, "sub": sub})
|
||||
except ex.ExprError:
|
||||
return None
|
||||
|
||||
|
||||
def label_comp(label: str) -> str | None:
|
||||
w = flat(label)
|
||||
return next((v for k, v in COMPW.items() if w.endswith(k)), None)
|
||||
|
||||
|
||||
def parse_line(text: str) -> dict[str, Any]:
|
||||
"""보이는 글 한 줄 → 꼴 · 라벨 · 결과 · 식 나무(역할 전)."""
|
||||
t = text.strip()
|
||||
if not t:
|
||||
return {"꼴": "빈"}
|
||||
if "─" in t or _PLAIN.fullmatch(t) and "=" not in t and ":" not in t:
|
||||
return {"꼴": "그림" if "─" in t else "글"}
|
||||
label, rhs = "", t
|
||||
mc = _LABEL.match(t)
|
||||
if mc and not re.search(r"\d\s*$", mc.group("l")):
|
||||
label, rhs = mc.group("l"), mc.group("r")
|
||||
if VARLINE.match(rhs) or VARLINE.match(t):
|
||||
return {"꼴": "변수", "조각": rhs.split("=")}
|
||||
if "=" not in rhs:
|
||||
m = re.match(r"^\s*(" + NUMRE + r")\s*\.?\s*$", rhs)
|
||||
if label and m:
|
||||
return {"꼴": "값", "라벨": label, "비목": label_comp(label), "값": num(m.group(1))}
|
||||
return {"꼴": "글"}
|
||||
segs = rhs.split("=")
|
||||
mres = RES.match(segs[-1]) or TAIL.match(segs[-1]) # 꼬리 글은 떼고 읽음(797-1a)
|
||||
if not mres:
|
||||
lead = re.match(r"\s*(" + NUMRE + r")", segs[-1])
|
||||
if (
|
||||
label and lead and re.search(r"\d", "=".join(segs[:-1]))
|
||||
): # 금액 줄 못 읽음 — 보인 값 보존
|
||||
return {"꼴": "글끝", "라벨": label, "보인": num(lead.group(1))}
|
||||
return {"꼴": "글끝"}
|
||||
shown = num(mres.group(1))
|
||||
toks = None
|
||||
for seg in reversed(segs[:-1]):
|
||||
toks = tokens(seg)
|
||||
if toks is None: # 라벨 없는 줄 — 왼쪽 글을 깎아 가며
|
||||
starts = {m.start() for m in re.finditer(NUMRE, seg)}
|
||||
starts |= {m.start() for m in re.finditer(r"\(\s*\d", seg)}
|
||||
for p in sorted(starts):
|
||||
if p and (seg[p - 1].isdigit() or seg[p - 1] in ".,"):
|
||||
continue
|
||||
toks = tokens(seg[p:])
|
||||
if toks:
|
||||
label = label or seg[:p]
|
||||
break
|
||||
if toks:
|
||||
break
|
||||
if not toks:
|
||||
return {"꼴": "식못읽음", "라벨": label, "값": shown}
|
||||
return {"꼴": "돈", "라벨": label, "비목": label_comp(label), "값": shown, "식": ex.parse(toks)}
|
||||
|
||||
|
||||
# ── 블록 풀이 ──────────────────────────────────────────────────────────
|
||||
def _ident(p: dict[str, Any]) -> tuple:
|
||||
return (p["종류"], p["이름"], p["규격"])
|
||||
|
||||
|
||||
def _money(L: dict, prices_by_value: dict, results: list) -> None:
|
||||
"""돈 줄 — 수 토큰 역할(단가 · 앞줄 · 상수) · 다시 셈 · 자리 규칙."""
|
||||
comp = L["비목"]
|
||||
nums = [(n[2], n[1]) for n in ex.numbers(L["식"])]
|
||||
roles: dict[int, dict[str, Any]] = {}
|
||||
price_at = None
|
||||
for i, v in nums:
|
||||
if v >= 1 and any(abs(r[1] - v) < EPS for r in results):
|
||||
continue # 앞 결과와 같은 수 = 참조(단가표 우연 일치보다 먼저)
|
||||
cands = [p for p in prices_by_value.get(v, []) if v >= 1 or (v == 0 and comp)]
|
||||
if comp:
|
||||
cands = [p for p in cands if p["비목"] == comp] or cands
|
||||
if v == 0 and comp and not cands:
|
||||
cands = [{"종류": "0", "이름": "-", "규격": "", "비목": comp, "값": 0.0}]
|
||||
first = i == nums[0][0]
|
||||
if cands and (v >= 100 or (first and v > 0)) or (v == 0 and cands):
|
||||
price_at = i
|
||||
roles[i] = {"갈래": "단가", "후보": cands}
|
||||
break
|
||||
for i, v in nums:
|
||||
if i in roles:
|
||||
continue
|
||||
hit = [r for r in reversed(results) if abs(r[1] - v) < EPS]
|
||||
roles[i] = {"갈래": "앞줄", "줄": hit[0][0]} if hit and v >= 1 else {"갈래": "상수"}
|
||||
L["역할"] = roles
|
||||
L["셈"] = calc(L["식"])
|
||||
L["규칙"] = ex.fit(L["셈"], L["값"])
|
||||
if price_at is not None:
|
||||
L["종류"] = "단가줄"
|
||||
pc = roles[price_at]["후보"]
|
||||
L["비목"] = L["비목"] or (pc[0]["비목"] if len({p["비목"] for p in pc}) == 1 else None)
|
||||
L["후보수"] = len({_ident(p) for p in pc})
|
||||
elif any(r["갈래"] == "앞줄" for r in roles.values()):
|
||||
L["종류"] = "참조줄"
|
||||
else:
|
||||
L["종류"] = "상수줄"
|
||||
|
||||
|
||||
def _value(L: dict, lines: list, results: list) -> None:
|
||||
"""값 줄 — 앞 결과 하나(자리 처리) 또는 바로 앞 돈 · 값 줄 이음 합."""
|
||||
v, how = L["값"], None
|
||||
for r in reversed(results):
|
||||
rule = ex.fit(r[1], v)
|
||||
if rule:
|
||||
how = {"갈래": "복사", "줄": [r[0]], "규칙": rule}
|
||||
break
|
||||
if not how and flat(L["라벨"]) in ("계", "합계"):
|
||||
got: dict[str, dict] = {}
|
||||
for x in reversed(lines):
|
||||
c = x.get("비목")
|
||||
if c and x.get("꼴") in ("돈", "값") and c not in got:
|
||||
got[c] = x
|
||||
if len(got) == 3:
|
||||
break
|
||||
if got:
|
||||
rule = ex.fit(sum(x["값"] for x in got.values()), v)
|
||||
if rule:
|
||||
how = {"갈래": "합", "줄": sorted(x["차례"] for x in got.values()), "규칙": rule}
|
||||
if not how:
|
||||
acc, used = 0.0, []
|
||||
for x in reversed([x for x in lines if x.get("꼴") in ("돈", "값")]):
|
||||
if len(used) >= 25:
|
||||
break
|
||||
acc += x["값"]
|
||||
used.append(x["차례"])
|
||||
rule = ex.fit(acc, v)
|
||||
if rule and len(used) > 1:
|
||||
how = {"갈래": "합", "줄": used[::-1], "규칙": rule}
|
||||
break
|
||||
L["종류"] = "값줄"
|
||||
L["숨은"] = how
|
||||
L["규칙"] = how["규칙"] if how else None
|
||||
|
||||
|
||||
def _var(L: dict) -> None:
|
||||
segs = L.pop("조각")
|
||||
m = re.match(r"^\s*(" + NUMRE + r")", segs[-1]) if len(segs) > 1 else None
|
||||
if not m:
|
||||
L["종류"] = "변수글"
|
||||
return
|
||||
shown, toks = num(m.group(1)), None
|
||||
for seg in reversed(segs[1:-1]):
|
||||
toks = tokens(seg)
|
||||
if toks:
|
||||
break
|
||||
L["값"] = shown
|
||||
L["규칙"] = ex.fit(calc(ex.parse(toks)), shown) if toks else None
|
||||
L["종류"] = "변수식" if toks else ("변수값" if len(segs) == 2 else "변수기호")
|
||||
|
||||
|
||||
def parse_block(text: list[str], prices: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
"""블록 글 줄 → 복원 줄 목록(역할 · 숨은 합 · 자리 규칙 · 단가 정체)."""
|
||||
by_value = collections.defaultdict(list)
|
||||
for p in prices:
|
||||
by_value[p["값"]].append(p)
|
||||
lines: list[dict[str, Any]] = []
|
||||
results: list[tuple] = [] # (차례, 값, 비목)
|
||||
for k, raw in enumerate(text):
|
||||
L = {"차례": k, "원글": raw, **parse_line(raw)}
|
||||
if L["꼴"] == "돈":
|
||||
_money(L, by_value, results)
|
||||
results.append((k, L["값"], L["비목"]))
|
||||
elif L["꼴"] == "값":
|
||||
_value(L, lines, results)
|
||||
results.append((k, L["값"], L["비목"]))
|
||||
elif L["꼴"] == "변수":
|
||||
_var(L)
|
||||
lines.append(L)
|
||||
identify(lines)
|
||||
return lines
|
||||
|
||||
|
||||
def identify(lines: list[dict[str, Any]]) -> None:
|
||||
"""단가 토큰 정체 — 값 하나 · 기계 세 줄 겹침 · 문맥(라벨 · 머리 글 이름) · 남으면 모호 표지."""
|
||||
head = ""
|
||||
for L in lines:
|
||||
if L["꼴"] in ("글", "글끝") or _HEADING.match(L["원글"]):
|
||||
head = L["원글"]
|
||||
if L.get("종류") != "단가줄":
|
||||
continue
|
||||
role = next(r for r in L["역할"].values() if r["갈래"] == "단가")
|
||||
cands = role["후보"]
|
||||
ids = {_ident(p) for p in cands}
|
||||
how = "값"
|
||||
if len(ids) > 1 and cands[0]["종류"] in ("기계", "산근"):
|
||||
k0, inter = L["차례"], set(ids)
|
||||
for M in lines[max(0, k0 - 3) : k0 + 4]:
|
||||
if M is L or M.get("종류") != "단가줄":
|
||||
continue
|
||||
r2 = next(r for r in M["역할"].values() if r["갈래"] == "단가")
|
||||
ids2 = {_ident(p) for p in r2["후보"]}
|
||||
if ids2 & inter and len(ids2 & inter) < len(inter):
|
||||
inter &= ids2
|
||||
ids, how = inter, "기계 세 줄"
|
||||
if len(ids) > 1:
|
||||
ctx = flat(L["원글"]) + "|" + flat(head)
|
||||
hit = {x for x in ids if x[1] and (x[1] in ctx or x[1][:3] in ctx)}
|
||||
ids, how = ({x for x in hit if not x[2] or x[2] in ctx} or hit or ids), "문맥"
|
||||
role["모호"] = len(ids) != 1
|
||||
role["갈림"] = how
|
||||
role["남은"] = len(ids)
|
||||
pick = sorted(ids)[0]
|
||||
p = next(p for p in cands if _ident(p) == pick)
|
||||
role["정체"] = {"종류": p["종류"], "이름": p["이름"], "규격": p["규격"], "비목": p["비목"]}
|
||||
|
||||
@@ -228,9 +228,36 @@ def marks(text: str) -> dict[str, Any]:
|
||||
return out
|
||||
|
||||
|
||||
def makes(t: dict[str, list[dict[str, Any]]]) -> dict[str, dict[str, Any]]:
|
||||
"""기계 구성(816-2 · MachMainDat — TitleCode 머리 다음 줄부터 빈 줄까지) → {코드: {손료 [[계수, 가격]
|
||||
(Index -3 GN · TU)] · 주연료 ℓ(-2 「주연료」 GN) · 조종원(-1 GN > 0)}} — 손료 줄 없는 기계는 뺌."""
|
||||
out: dict[str, dict[str, Any]] = {}
|
||||
cur = None
|
||||
for r in t.get("MachMainDat", []):
|
||||
if r.get("TitleCode"):
|
||||
cur = out.setdefault(r["TitleCode"], {"손료": [], "주연료": None, "조종원": False})
|
||||
elif not r.get("Name"):
|
||||
cur = None
|
||||
elif cur is not None:
|
||||
i, gn = r.get("Index"), r.get("GN")
|
||||
if i == -3 and gn:
|
||||
cur["손료"].append([gn, r.get("TU")])
|
||||
elif i == -2 and cur["주연료"] is None and re.sub(r"\s", "", r["Name"]) == "주연료":
|
||||
cur["주연료"] = gn
|
||||
elif i == -1 and (gn or 0) > 0:
|
||||
cur["조종원"] = True
|
||||
return {k: v for k, v in out.items() if v["손료"]}
|
||||
|
||||
|
||||
def resources(t: dict[str, list[dict[str, Any]]]) -> dict[tuple[int, str], dict[str, Any]]:
|
||||
"""(LinkIndex, 코드) → 자원 머리(표 · 이름 · 규격 · 단위 · 저장 금액)."""
|
||||
"""(LinkIndex, 코드) → 자원 머리(표 · 이름 · 규격 · 단위 · 저장 금액 · 기계 = 같은 파일에서 구성이
|
||||
같은 단독 기계 `같은구성` [[이름, 규격]] · 816-2)."""
|
||||
from M02_MasterTemplete import M02_Machine_Find as mfind
|
||||
|
||||
out = {}
|
||||
heads = {r.get("Code"): r for r in t.get("MachTitleDat", [])}
|
||||
made = {c: (heads[c].get("Name"), heads[c].get("Size"), m) for c, m in makes(t).items() if c in heads} # fmt: skip
|
||||
same = mfind.peers(made)
|
||||
for link, name in SOURCE.items():
|
||||
for r in t.get(name, []):
|
||||
if r.get("Code"):
|
||||
@@ -240,7 +267,11 @@ def resources(t: dict[str, list[dict[str, Any]]]) -> dict[tuple[int, str], dict[
|
||||
"규격": r.get("Size"),
|
||||
"단위": r.get("Unite"),
|
||||
"저장": _amounts(r, AMT),
|
||||
}
|
||||
} | (
|
||||
{"같은구성": same[r["Code"]]}
|
||||
if name == "MachTitleDat" and r["Code"] in same
|
||||
else {}
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
|
||||
@@ -7,9 +7,8 @@ STmate 셈 규칙(`_분석_649-G2` 1 장) — 줄 금액 = 단가 × (계수 + 1
|
||||
T% = 바로 앞 계표시 값 × 수량 · 계표시 = 아래 단 누계 · `(-=)` = 찍되 위 합에서 뺌 ·
|
||||
`->` = 번지엔 쌓되 합에서 뺌 · 블록 끝전 두 갈래(비목 버림 / 계 버림 + 큰 나머지) ·
|
||||
결과금액 조정(SBM = 계를 한 비목으로) · 계약단가 = 비목 × 낙찰률 원 버림.
|
||||
EST 셈 규칙(같은 문서 2 장) — 줄 = 복원 식(단가 · 앞줄 결과 · 상수) → 줄 자리 속성 ·
|
||||
값 줄 = 숨은 복사 · 합 → 자리 속성 · 블록 성분 = 그 값을 낸 마지막 줄.
|
||||
단가 바뀜 전파 — STmate = 단가 dict 를 바꿔 다시 셈 · EST = `run_est(lines, {정체 열쇠: 새 값})`.
|
||||
단가 바뀜 전파 — 단가 dict 를 바꿔 다시 셈. 옛 EST 엑셀 셈 · 엑셀 검산(`run_est` · `est_check` ·
|
||||
`stmate_check`)은 812-1 엑셀 읽기기로 걷음(EST = `M02_Doc_NativeEst`).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -22,7 +21,6 @@ from M02_MasterTemplete import M02_San_Expr as ex
|
||||
|
||||
COMPS = ("L", "M", "S")
|
||||
SPECIAL = ("L%", "M%", "S%", "T%")
|
||||
TOL_LINE = 0.05
|
||||
|
||||
|
||||
def _zero() -> dict[str, float]:
|
||||
@@ -179,115 +177,3 @@ def stmate_block(block: dict, nodes: list[dict], prices: dict, rule: str) -> dic
|
||||
fin = {c: math.floor(round(kye[c] * nag / 100, 6)) for c in COMPS}
|
||||
fin["T"] = fin["L"] + fin["M"] + fin["S"]
|
||||
return {"줄": lines, "합": tot, "계": kye, "금액": fin, "낙찰": nag}
|
||||
|
||||
|
||||
def stmate_check(block: dict, result: dict) -> dict[str, Any]:
|
||||
"""줄마다 · 블록 검산 — 금액 줄(참조 · 율 · 특수) C~F 대조 · 계표시 줄 · 블록 머리 · 꼬리 계."""
|
||||
bad, n_amt, n_tot, ok_tot = [], 0, 0, 0
|
||||
for r, ln in zip(result["줄"], block["몸"], strict=True):
|
||||
want = {"L": ln["D"] or 0, "M": ln["E"] or 0, "S": ln["F"] or 0}
|
||||
if r["종류"] in ("참조", "율", "특수"):
|
||||
n_amt += 1
|
||||
if not r["값"] or any(abs(r["값"][c] - want[c]) >= TOL_LINE for c in COMPS):
|
||||
bad.append({"행": ln["행"], "엑셀": want, "셈": r["값"], "오류": r["오류"]})
|
||||
elif r["종류"] == "계" and ln["C"] is not None:
|
||||
n_tot += 1
|
||||
ok_tot += all(abs(r["값"][c] - want[c]) < 0.15 for c in COMPS)
|
||||
head = block["머리"]
|
||||
want = {"T": head["C"] or 0, "L": head["D"] or 0, "M": head["E"] or 0, "S": head["F"] or 0}
|
||||
kye = next((t for t in block["꼬리"] if t["A"] == "계"), None)
|
||||
want_k = {"T": kye["C"], "L": kye["D"], "M": kye["E"], "S": kye["F"]} if kye else None
|
||||
same = lambda x, y: all(abs((x[k] or 0) - (y[k] or 0)) < 0.5 for k in "TLMS") # noqa: E731
|
||||
ok = same(result["금액"], want) and (want_k is None or same(result["계"], want_k))
|
||||
return {
|
||||
"맞음": ok,
|
||||
"엑셀": want,
|
||||
"엑셀계": want_k,
|
||||
"금액줄": n_amt,
|
||||
"틀린줄": bad,
|
||||
"계줄": n_tot,
|
||||
"계줄맞음": ok_tot,
|
||||
}
|
||||
|
||||
|
||||
# ── EST ────────────────────────────────────────────────────────────────
|
||||
def key_of(ident: dict[str, Any]) -> tuple:
|
||||
"""EST 단가 정체 → 전파 열쇠 (종류, 이름, 규격, 비목)."""
|
||||
return (ident["종류"], ident["이름"], ident["규격"], ident["비목"])
|
||||
|
||||
|
||||
def run_est(lines: list[dict], newp: dict[tuple, float] | None = None) -> dict[int, float | None]:
|
||||
"""복원 줄 → {차례: 다시 셈 값}(줄 자리 속성 그대로) · newp = {정체 열쇠: 새 단가}."""
|
||||
newp = newp or {}
|
||||
val: dict[int, float | None] = {}
|
||||
for L in lines:
|
||||
k = L["차례"]
|
||||
if L.get("꼴") == "돈" and L.get("규칙"):
|
||||
sub = {}
|
||||
for i, r in L["역할"].items():
|
||||
if r["갈래"] == "단가" and key_of(r["정체"]) in newp:
|
||||
sub[i] = newp[key_of(r["정체"])]
|
||||
elif r["갈래"] == "앞줄" and val.get(r["줄"]) is not None:
|
||||
sub[i] = val[r["줄"]]
|
||||
v = _est_calc(L["식"], sub)
|
||||
val[k] = ex.RULE[L["규칙"]](v) if v is not None else None
|
||||
elif L.get("종류") == "값줄" and L.get("숨은"):
|
||||
how = L["숨은"]
|
||||
s = sum(
|
||||
(val.get(j) if val.get(j) is not None else lines[j]["값"]) or 0 for j in how["줄"]
|
||||
)
|
||||
val[k] = ex.RULE[how["규칙"]](s)
|
||||
return val
|
||||
|
||||
|
||||
def _est_calc(node: tuple, sub: dict) -> float | None:
|
||||
try:
|
||||
return ex.value(node, {"V": {}, "sub": sub})
|
||||
except ex.ExprError:
|
||||
return None
|
||||
|
||||
|
||||
def designate(lines: list[dict], want: dict[str, float]) -> dict[str, Any]:
|
||||
"""블록 성분(총괄표) → 그 값을 낸 줄(마지막 · 라벨 비목 우선) · 0 = 「영」 · 없음 = None."""
|
||||
out: dict[str, Any] = {}
|
||||
kinds = ("단가줄", "참조줄", "상수줄", "값줄")
|
||||
for c in COMPS:
|
||||
w = want[c]
|
||||
if not w:
|
||||
out[c] = "영"
|
||||
continue
|
||||
cand = [L for L in lines if L.get("종류") in kinds and abs(L["값"] - w) < ex.EPS]
|
||||
pick = [L for L in cand if L.get("비목") == c] or cand
|
||||
out[c] = pick[-1]["차례"] if pick else None
|
||||
return out
|
||||
|
||||
|
||||
def est_block(lines: list[dict], des: dict[str, Any], newp: dict | None = None) -> dict[str, Any]:
|
||||
"""성분 줄 → 블록 금액(성분 원 버림 · 계 = 합) · 줄 값."""
|
||||
val = run_est(lines, newp)
|
||||
raw = {
|
||||
c: (0.0 if k == "영" else (val.get(k) if k is not None else None)) for c, k in des.items()
|
||||
}
|
||||
if any(v is None for v in raw.values()):
|
||||
return {"줄": val, "성분": raw, "금액": None}
|
||||
fin = {c: math.floor(round(raw[c], 6)) for c in COMPS}
|
||||
fin["T"] = fin["L"] + fin["M"] + fin["S"]
|
||||
return {"줄": val, "성분": raw, "금액": fin}
|
||||
|
||||
|
||||
def est_check(lines: list[dict], want: dict[str, float]) -> dict[str, Any]:
|
||||
"""블록 검산 — 다시 셈 성분 = 총괄표 · 줄마다 보인 값 대조(돈 · 값 줄)."""
|
||||
des = designate(lines, want)
|
||||
got = est_block(lines, des)
|
||||
val = got["줄"]
|
||||
n, bad = 0, []
|
||||
for L in lines:
|
||||
if L.get("꼴") == "돈" or L.get("종류") == "값줄":
|
||||
n += 1
|
||||
v = val.get(L["차례"])
|
||||
if v is None or abs(v - L["값"]) >= ex.EPS:
|
||||
bad.append({"차례": L["차례"], "글": L["원글"].strip(), "보인": L["값"], "셈": v})
|
||||
fin = got["금액"]
|
||||
ok = fin is not None and all(abs(fin[c] - want[c]) < 0.5 for c in COMPS)
|
||||
ok = ok and abs(want["L"] + want["M"] + want["S"] - want["T"]) < 0.5
|
||||
return {"맞음": ok, "성분줄": des, "금액": fin, "줄수": n, "틀린줄": bad}
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
"""경쟁사 산근 줄 → M01 요소 이음(PLAN 814-1 · `M02_San_Emit` 에서 뗌 — 셈 꼴 글 · 표지 그대로).
|
||||
|
||||
`m01` = 1차 줄 판정 유일 짝(인력 · 재료 · 기계 · 유가 · 환율) · `hourly_text` = 기계 시간당 경비 부름 글 ·
|
||||
`combo_link` = 조합 기계(굴착기 + 브레이커 · 집게) 우리 조합 식 부름 글.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
NOTE = "참고 · " # `M02_San_Emit.NOTE` 와 같음(사람 확인 표지 머리)
|
||||
OIL_BASE = ("전국평균", "일반용(갑)Ⅰ 저압") # 유가 · 전력 기본(요구 748 · 우리 기초 기본값)
|
||||
QUOTE = "견적 — 그대로(이름 · 값) · [단가 갱신] 대상 아님" # 815-1 K5 · 요구 762-3
|
||||
APART = "별도계상 · 품목은 프로젝트에서" # 817-1 — 일위대가 길(`M02_CheckApart`)과 같은 글 · 요소 잇기 없음
|
||||
|
||||
|
||||
def m01(kind: str, src: dict) -> tuple[str | None, str | None]:
|
||||
"""1차 줄 판정 유일 짝 M01 키 → (키, 표지) — 없음 = (None, None) · 여럿 = (None, 표지 · 유가 ·
|
||||
환율만 — 재료 · 인력은 조용히 이음 없음) · 기계 = 기계 찾기(812-9) 못 이으면 늘 표지. 재료 줄 이름이 M01 유가전력(MO)이면
|
||||
유가 · 화폐 이름 그대로(달러 · USD …)면 환율(FX)로 판정(797-1 · 「유로폼」 같은 글 속 낱말은 아님)."""
|
||||
if kind == "고정":
|
||||
return quote(src)
|
||||
if kind not in ("인력", "재료", "기계"):
|
||||
return None, None
|
||||
from M02_MasterTemplete import M02_Elem_Alias as ea
|
||||
from M02_MasterTemplete import M02_Hopyo_Elem as elem
|
||||
from M02_MasterTemplete import M02_Hopyo_Row as hrow
|
||||
|
||||
line = {k: src.get(k) or "" for k in ("이름", "규격", "단위")}
|
||||
if kind == "재료" and apart(
|
||||
line
|
||||
): # 817-1 — 원가 합 밖(원본 금액 그대로) · 품목은 프로젝트에서 이음
|
||||
return None, f"{NOTE}{APART}"
|
||||
if (
|
||||
kind == "기계"
|
||||
): # 812-9 — 하나 = 이음 · 아니면 표지(후보 여럿 · 못 찾음 · 조용히 넘기지 않음)
|
||||
from M02_MasterTemplete import M02_Machine_Find as mfind
|
||||
|
||||
got = mfind.find(line["이름"], line["규격"])
|
||||
# 찾기 판정 없는 기계 줄 막음(814-1 · 줄 쌓기 표지 · 별칭)
|
||||
src.setdefault("찾기", {**got, "이름": got.get("이름") or line["이름"], "규격": got.get("규격") or line["규격"]}) # fmt: skip
|
||||
return got["키"], mfind.mark(got, line["이름"], line["규격"])
|
||||
if kind == "재료" and src.get("종류") == "자재" and (hit := ea.est_code(src.get("코드"))):
|
||||
return ea.one(line, hit) or hit[0], None # 815-1 K1 — EST 원파일 자재 = 우리 자재 출처 코드
|
||||
oil = ea.keys("MO", line["이름"], line["규격"]) if kind == "재료" else []
|
||||
if kind == "재료":
|
||||
if oil or elem.oil_rows(line["이름"]):
|
||||
kind = "유가"
|
||||
elif line["이름"].strip().upper() in {x.upper() for pair in hrow.CURRENCY for x in pair}:
|
||||
kind = "환율"
|
||||
if kind == "유가": # MO 전부 → 줄 규격이 상세구분(전국 · 지역)이면 그것만
|
||||
rows = [ea.row("MO", k) for k in oil] or elem.oil_rows(line["이름"]) # 815-1 별칭 먼저
|
||||
got = [{"키": r["키"], "규격": str(r.get("상세구분") or "")} for r in rows]
|
||||
spec = line["규격"].replace(" ", "")
|
||||
got = (
|
||||
[x for x in got if x["규격"] == spec]
|
||||
or [x for x in got if spec and spec in x["규격"]]
|
||||
or got
|
||||
)
|
||||
else:
|
||||
got = hrow.m01_picks(kind, line)
|
||||
keys = {x.get("키") for x in got if x.get("키")}
|
||||
if (
|
||||
len(keys) > 1 and kind == "유가"
|
||||
): # 814-1 — 전국평균 · 전력 = 일반용(갑)Ⅰ 저압(지역 · 계약종별은 옵션)
|
||||
keys = {x["키"] for x in got if x["규격"] in OIL_BASE} or keys
|
||||
elif len(keys) > 1 and kind == "재료": # 814-1 — 지역 꼬리 뗀 이름 · 규격 정확 일치
|
||||
keys = exact(line, got) or keys
|
||||
if not keys and kind == "재료": # 815-1 K3 — 자재표 전체에서 표기 정규화 정확 일치
|
||||
keys = {k} if (k := ea.one(line, ea.exact(line["이름"], line["규격"]))) else keys
|
||||
if len(keys) == 1:
|
||||
return keys.pop(), None
|
||||
if len(keys) > 1 and kind in ("유가", "환율"):
|
||||
return None, f"{NOTE}{kind} 후보 여럿({len(keys)}) · 사람 확인"
|
||||
return None, None
|
||||
|
||||
|
||||
def apart(line: dict) -> bool:
|
||||
"""줄 이름 · 규격에 「별도계상」(`M02_Hopyo_Material.clean`)."""
|
||||
from M02_MasterTemplete import M02_Hopyo_Material as hmat
|
||||
|
||||
return hmat.clean(line)[1]
|
||||
|
||||
|
||||
def quote(src: dict) -> tuple[str | None, str | None]:
|
||||
"""견적 · 운임 · 수수료 꼴 줄(갈래 밖 — EST 견적 · STmate 고정 코드 · 815-1 K5) → 우리 자재에 이어지면
|
||||
(코드 · 별칭 · 정확 일치 · 단가 줄 · 우리 값 있을 때) 그 키 + 갈래 재료(우리 값 · 엑셀 값은 검산) ·
|
||||
아니면 그대로 + 표지."""
|
||||
from M02_MasterTemplete import M02_Elem_Alias as ea
|
||||
|
||||
src["견적"] = True
|
||||
line = {k: src.get(k) or "" for k in ("이름", "규격", "단위")}
|
||||
key, _how = ea.material(line, src.get("코드"))
|
||||
if key and src.get("비목") and ea.priced(key): # 우리 값 없는 자재 = 견적 값 그대로
|
||||
src["갈래"] = "재료"
|
||||
return key, None
|
||||
return None, f"{NOTE}{QUOTE}"
|
||||
|
||||
|
||||
def exact(line: dict, got: list[dict]) -> set[str]:
|
||||
"""재료 후보 가운데 지역 꼬리 뗀 이름 · 규격이 줄과 정확히 같은 것(표기 정규화 `master_alias.norm`) —
|
||||
지역마다 여럿이면 줄 지역 › 서울 › 같은 단위 › 키 차례 첫째 하나 · 없음 = 빈 모음(사람 확인)."""
|
||||
from M02_MasterTemplete import M02_Elem_Alias as ea
|
||||
|
||||
want = ea._same(line.get("이름"), line.get("규격"))
|
||||
hits = [x["키"] for x in got if x.get("키") and ea._same(x.get("이름"), x.get("규격")) == want]
|
||||
return {k} if (k := ea.one(line, hits)) else set()
|
||||
|
||||
|
||||
def hourly_text(key: str | None) -> str | None:
|
||||
"""기계 EQ → 시간당 기계경비 로직 부름 글(797-1 짝 고름) · 키 · 자료 없음 = None."""
|
||||
if not key:
|
||||
return None
|
||||
from M02_MasterTemplete import M02_Doc_Import as dim
|
||||
|
||||
got = dim.hourly_link(key, "노무비")
|
||||
if not got:
|
||||
return None
|
||||
args = "".join(f", {dim._q(k)}={v}" for k, v in got["입력"].items()) # 빈칸 든 이름은 따옴표
|
||||
return f"로직({got['키']}{args})"
|
||||
|
||||
|
||||
def combo_link(one: dict, parts: list[dict]) -> str | None:
|
||||
"""조합 기계 둘이 우리 조합 식(GC000270 · 271) 고르기에 있으면 그 부름 글 · 줄 자원에 `조합식` 키."""
|
||||
from M02_MasterTemplete import M02_Machine_Hourly as mh
|
||||
|
||||
got = mh.combo(parts[0]["키"], parts[1]["키"]) if len(parts) == 2 and all(p["키"] for p in parts) else None # fmt: skip
|
||||
if got:
|
||||
one["자원"]["조합식"] = got[0]
|
||||
return got[1] if got else None
|
||||
|
||||
|
||||
# ── 못 이은 줄 까닭(814-1 · 요구 745 C2) ─────────────────────────────────
|
||||
FREIGHT = ("운임", "화물", "운반비", "하차")
|
||||
|
||||
|
||||
def why(item: dict) -> str:
|
||||
"""이음 없는 가져온 줄 → 까닭 한 마디(문서 설명 · 띠 · 원문 카드) — 「없음」은 흠이 아니라 우리 정보로
|
||||
못 이은 까닭(자재 없음 · 운임표 없음 · 기계 표에 없음 · 별칭 대기 · 후보 여럿 · 시간당 경비 자료 없음 · 갈래 밖)."""
|
||||
res = item.get("자원") or {}
|
||||
kind = res.get("갈래") or item.get("종류")
|
||||
name = str(res.get("이름") or item.get("이름") or "")
|
||||
if res.get("조합식"):
|
||||
return f"조합 — 우리 조합 식 {res['조합식']} 에 듦"
|
||||
if kind == "기계":
|
||||
found = res.get("찾기") or {}
|
||||
verdict = found.get("판정")
|
||||
if verdict == "못 찾음":
|
||||
return (
|
||||
"별칭 대기"
|
||||
if str(found.get("까닭") or "").startswith("기종 모름")
|
||||
else "기계 표에 없음"
|
||||
)
|
||||
if verdict == "후보 여럿":
|
||||
return "기계 후보 여럿 — 고름"
|
||||
return "시간당 경비 자료 없음" if found.get("키") else "기계 찾기 없음"
|
||||
if kind == "재료":
|
||||
from M02_MasterTemplete import M02_Hopyo_Elem as elem
|
||||
from M02_MasterTemplete import M02_Hopyo_Row as hrow
|
||||
|
||||
if apart({"이름": name, "규격": res.get("규격") or ""}):
|
||||
return APART
|
||||
if elem.oil_rows(name):
|
||||
return "유가 후보 여럿 — 고름"
|
||||
line = {"이름": name, "규격": res.get("규격") or "", "단위": res.get("단위") or ""}
|
||||
if picks := hrow.m01_picks("재료", line): # 817-1 ㉯ 1 순위를 글에(고르기 창은 이름 찾기)
|
||||
return f"자재 후보 여럿 — 고름 · 1 순위 {picks[0]['이름']} {picks[0]['규격']}".strip()
|
||||
return "운임표 없음" if any(w in name for w in FREIGHT) else "자재 없음(우리 자재표)"
|
||||
if kind == "인력":
|
||||
return "노임 없음"
|
||||
if kind == "단가산출":
|
||||
return "안쪽 단가산출 못 이음"
|
||||
return QUOTE
|
||||
|
||||
|
||||
def tally(logic: dict) -> dict:
|
||||
"""우리 정보로 셈 — 자원 줄 {됨, 줄, 조합(형제 단가산출 부름뿐)} (연결 판정 「없음」 띠 · 814-1)."""
|
||||
items = logic.get("호표") or []
|
||||
done = sum(
|
||||
1 for x in items if x.get("이음") or x.get("참조") or (x.get("자원") or {}).get("조합식")
|
||||
)
|
||||
inner = [x for x in items if (x.get("자원") or {}).get("갈래") == "단가산출" or x.get("참조")]
|
||||
return {"됨": done, "줄": len(items), "조합": bool(items) and len(inner) == len(items)}
|
||||
@@ -0,0 +1,208 @@
|
||||
"""경쟁사 산근 엑셀(.xlsx · .xls) 읽기기 — 810-1 원파일 파서와 같은 「구조 줄」 꼴(PLAN 812-1 · 요구 711-2 ·
|
||||
712 · 713 · 읽기만 · 서버 단독).
|
||||
|
||||
`read(name, data)` = `M02_San_Estx.read` · `M02_San_Stc.read` 와 같은 칸 이름 · 같은 뜻
|
||||
(칸 뜻 `resources/master_data/_원파일파서_틀.md`) → 그 뒤는 원파일 길(`M02_Doc_FromNative.pack` →
|
||||
`emit` → `M02_Doc_NativeEst` · `M02_San_Emit.stmate` → 수식 문서) 그대로.
|
||||
EST = `M02_San_Xlsx_Est`(단가산출 시트 글 · 금액 칸 · 같은 책 단가표) ·
|
||||
STmate = `M02_San_Xlsx_Stmate`(숨은 G 열 원문 = 원파일 원문 · 단가 = 목록표 · 끝전 = 파일 투표).
|
||||
엑셀이 못 주는 칸(자원 코드 · LinkIndex · 자리 Diget)은 비움 + 표지 「엑셀 — …」 · 글에서 읽어 정한 것은
|
||||
`연결뜻` 에 적고 줄 `추정` 참 + 블록 표지 「엑셀 추정」.
|
||||
여기 = 공통(시트 → 글 줄 · 금액 칸 · 자원 표).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import unicodedata
|
||||
from typing import Any
|
||||
|
||||
from M02_MasterTemplete import M02_San_Book as book
|
||||
|
||||
GUESS = "엑셀 추정"
|
||||
NOTE = "엑셀 — "
|
||||
|
||||
|
||||
class XlsxError(ValueError):
|
||||
"""산근 시트가 없는 엑셀."""
|
||||
|
||||
|
||||
def flat(v: Any) -> str:
|
||||
return re.sub(r"\s+", "", str(v or ""))
|
||||
|
||||
|
||||
def number(v: Any) -> float | None:
|
||||
"""칸 값 → 수(「3,442」 · 「8.」 · 수 칸) · 아니면 None."""
|
||||
if isinstance(v, bool) or v is None:
|
||||
return None
|
||||
if isinstance(v, (int, float)):
|
||||
return float(v)
|
||||
s = str(v).strip().replace(",", "")
|
||||
if not re.fullmatch(r"-?\d+(?:\.\d*)?|-?\.\d+", s):
|
||||
return None
|
||||
return float(s.rstrip(".") or 0)
|
||||
|
||||
|
||||
def _cell(v: Any) -> str:
|
||||
if isinstance(v, float) and v == int(v):
|
||||
return str(int(v))
|
||||
return "" if v is None else str(v)
|
||||
|
||||
|
||||
def _width(ch: str) -> int:
|
||||
return 2 if unicodedata.east_asian_width(ch) in "WFA" else 1
|
||||
|
||||
|
||||
def layout(cells: list[tuple[int, str]], base: int) -> str:
|
||||
"""글자 칸 나눔 줄(거창 꼴 · 열 = 화면 자리) → 한 줄 글(열 차례 · 모자란 자리 = 빈칸 · 겹친 자리(수 끝 점)
|
||||
= 빈칸 하나)."""
|
||||
out, end = "", 0
|
||||
for col, text in cells:
|
||||
want = col - base
|
||||
if want > end:
|
||||
out += " " * (want - end)
|
||||
elif out and want < end and not out.endswith(" "):
|
||||
out += " "
|
||||
out += text
|
||||
end = want + sum(_width(c) for c in text)
|
||||
return out
|
||||
|
||||
|
||||
# ── 시트 머리 ──────────────────────────────────────────────────────────
|
||||
MONEY = ("합계", "노무비", "재료비", "경비")
|
||||
|
||||
|
||||
def money_cols(rows: list[list[Any]]) -> list[int] | None:
|
||||
"""머리 줄의 맨 오른쪽 「합계 · 노무비 · 재료비 · 경비」 네 칸 열 → [T L M S]."""
|
||||
for r in rows[:6]:
|
||||
names = [flat(v) for v in r]
|
||||
hits = [j for j in range(len(names) - 3) if tuple(names[j : j + 4]) == MONEY]
|
||||
if hits:
|
||||
return list(range(hits[-1], hits[-1] + 4))
|
||||
return None
|
||||
|
||||
|
||||
def money_of(row: list[Any], cols: list[int]) -> dict[str, float] | None:
|
||||
got = {c: number(row[j]) if j < len(row) else None for c, j in zip("TLMS", cols)}
|
||||
got = {c: v for c, v in got.items() if v is not None}
|
||||
return got or None
|
||||
|
||||
|
||||
def head_col(rows: list[list[Any]], *words: str) -> int | None:
|
||||
for r in rows[:6]:
|
||||
for j, v in enumerate(r):
|
||||
if flat(v) in words:
|
||||
return j
|
||||
return None
|
||||
|
||||
|
||||
# ── 자원 표(EST) ───────────────────────────────────────────────────────
|
||||
def _table(rows: list[list[Any]], price: tuple[str, ...]) -> list[dict[str, Any]]:
|
||||
"""단가 시트 → [{번, 이름, 규격, 단위, 줄 (원 줄)}](품명 · 규격 · 단위 · 값 열 = 머리 글)."""
|
||||
c_no, c_name = head_col(rows, "No"), head_col(rows, "품명")
|
||||
c_size, c_unit = head_col(rows, "규격"), head_col(rows, "단위")
|
||||
if c_name is None:
|
||||
return []
|
||||
out = []
|
||||
for r in rows:
|
||||
no = str(r[c_no]).strip() if c_no is not None and c_no < len(r) else ""
|
||||
if not no.isdigit() or c_name >= len(r) or not str(r[c_name] or "").strip():
|
||||
continue
|
||||
out.append(
|
||||
{
|
||||
"번": int(no),
|
||||
"이름": str(r[c_name]).strip(),
|
||||
"규격": str(r[c_size] or "").strip() if c_size is not None else "",
|
||||
"단위": str(r[c_unit] or "").strip() if c_unit is not None else "",
|
||||
"줄": r,
|
||||
}
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
def resources(sheets: dict[str, list]) -> dict[str, list[dict[str, Any]]]:
|
||||
"""같은 책 단가표 → {종류: [{번 · 이름 · 규격 · 단위 · 표 · 값 {T L M S}}]}(노임 · 자재 = 적용단가 ·
|
||||
기계 · 견적 · 산근 = 적용 칸(합이 안 맞으면 당초 · `M02_San_Book.applied`))."""
|
||||
out: dict[str, list[dict[str, Any]]] = {}
|
||||
for kind, sheet, comp in (("노임", "노임단가", "L"), ("자재", "자재단가", "M")):
|
||||
rows = sheets.get(sheet) or []
|
||||
c = head_col(rows, "적용단가")
|
||||
got = []
|
||||
for one in _table(rows, ("적용단가",)):
|
||||
v = number(one["줄"][c]) if c is not None and c < len(one["줄"]) else None
|
||||
if v is not None:
|
||||
got.append({**_drop(one), "표": sheet, "값": {"T": v, comp: v}})
|
||||
out[kind] = got
|
||||
for kind, sheet in (
|
||||
("기계", "기계경비총괄표"),
|
||||
("견적", "견적단가"),
|
||||
("산근", "단가산출총괄표"),
|
||||
):
|
||||
got = []
|
||||
for one in _table(sheets.get(sheet) or [], ()):
|
||||
r = list(one["줄"]) + [None] * 30
|
||||
vals, note = book.applied_how(r)
|
||||
vals = {k: float(v or 0) for k, v in vals.items()}
|
||||
got.append({**_drop(one), "표": sheet, "값": vals} | ({"칸": note} if note else {}))
|
||||
out[kind] = got
|
||||
from M02_MasterTemplete import M02_Machine_Find as mfind
|
||||
|
||||
same = mfind.peers(makes(sheets.get("기계경비") or []))
|
||||
for one in out["기계"]: # 816-2 같은 파일에서 구성이 같은 단독 기계
|
||||
if got := same.get((flat(one["이름"]), flat(one["규격"]))):
|
||||
one["같은구성"] = got
|
||||
return out
|
||||
|
||||
|
||||
def makes(rows: list[list[Any]]) -> dict[tuple[str, str], dict[str, Any]]:
|
||||
"""EST 엑셀 「기계경비」 시트(816-2) — 「N. 이름 · 규격」 머리 아래 빈 줄까지: 「손료 가격 ₩× 계수 ×
|
||||
10(-7)」 · 「주연료 종류 ℓ」 · 「직종 1 인」 → {(이름, 규격) 붙임: (이름, 규격, {손료 [[계수, 가격]] ·
|
||||
주연료 ℓ · 조종원})}."""
|
||||
out: dict[tuple[str, str], tuple[str, str, dict[str, Any]]] = {}
|
||||
cur = None
|
||||
for row in rows:
|
||||
v = [c for c in row if c not in (None, "")]
|
||||
head = re.match(r"\d+\.\s*(.+)", str(v[0])) if v else None
|
||||
if head:
|
||||
spec = v[1] if len(v) > 1 and isinstance(v[1], str) else ""
|
||||
name = head.group(1).strip()
|
||||
make = {"손료": [], "주연료": None, "조종원": False}
|
||||
cur = out.setdefault((flat(name), flat(spec)), (name, spec, make))[2]
|
||||
elif not v:
|
||||
cur = None
|
||||
elif cur is not None:
|
||||
word = flat(v[0])
|
||||
if word.startswith("손료") and "₩×" in v and (at := v.index("₩×") + 1) < len(v):
|
||||
cur["손료"].append([number(v[at]), number(v[1])])
|
||||
elif word == "주연료" and len(v) > 2 and cur["주연료"] is None:
|
||||
cur["주연료"] = number(v[2])
|
||||
elif len(v) > 2 and v[2] == "인" and (number(v[1]) or 0) > 0:
|
||||
cur["조종원"] = True
|
||||
return {k: m for k, m in out.items() if m[2]["손료"] and None not in m[2]["손료"][0]}
|
||||
|
||||
|
||||
def _drop(one: dict) -> dict:
|
||||
return {k: v for k, v in one.items() if k != "줄"}
|
||||
|
||||
|
||||
def kind_of(sheets: dict[str, list]) -> str | None:
|
||||
return book.kind_of(sheets)
|
||||
|
||||
|
||||
def read(name: str, data: bytes) -> dict[str, Any]:
|
||||
"""엑셀 → {꼴, 산근, 일위, 자원, 내역, 표지(, 단가 · 옵션 STmate)} — 산근 시트 없음 = XlsxError."""
|
||||
sheets = book.sheets(name, data)
|
||||
return read_sheets(sheets)
|
||||
|
||||
|
||||
def read_sheets(sheets: dict[str, list]) -> dict[str, Any]:
|
||||
kind = kind_of(sheets)
|
||||
if kind == "EST":
|
||||
from M02_MasterTemplete import M02_San_Xlsx_Est as xe
|
||||
|
||||
return xe.read(sheets) | {"엑셀": True}
|
||||
if kind == "STmate":
|
||||
from M02_MasterTemplete import M02_San_Xlsx_Stmate as xs
|
||||
|
||||
return xs.read(sheets) | {"엑셀": True}
|
||||
raise XlsxError("STmate · EST 산근 시트 없음")
|
||||
@@ -0,0 +1,667 @@
|
||||
"""EST 산근 엑셀 → 810-1 구조 줄(PLAN 812-1 · `M02_San_Estx.cost_line` 과 같은 칸 · 읽기만 · 서버 단독).
|
||||
|
||||
엑셀 「단가산출」 시트 = 원파일 LineData 를 보인 글(제어문자 표지 없음) + 줄 금액 칸(원파일 줄 TC NC GC JC ·
|
||||
0 = 빈칸 · 소계 61 = 비목 칸만) → 표지를 글 · 금액 칸 · 같은 책 단가표(노임 · 자재 · 기계 · 견적 · 단가산출
|
||||
총괄표)로 다시 정함(`연결뜻` · 표지 「엑셀 추정」 · 자원 코드 · 자리 = 비움):
|
||||
변수 줄 「이름 = 식 = 값」 → 값 = 결과(`\\x12`) · 「이름 = 값」 → 대입(결과 없음) · 금액 줄 첫 식 조각의 수가
|
||||
앞 대입 값과 같으면 변수(`\\x11` · 상수 아님) ·
|
||||
합계 머리(「합 계」) 아래 노무비 · 재료비 · 경비 · 계 줄 = 합계(`\\x1a` · 비목 L M S T) ·
|
||||
「* 단산 N 참조 *」 · 「* 견적 N 참조 *」 · 기계 이름(+규격) = 세 줄 머리(104 · 101 · 105) → 아래
|
||||
「노무비 · 재료비 · 경비 : 단가 …」(단가 = 그 자원 비목 값) = 세 줄 · 「소 계」 = 계 줄 ·
|
||||
노임 · 자재 이름 + 그 단가 수 = 자원 줄(5 · 6 · 비목 = 금액 칸) ·
|
||||
「…소계 : 수」(금액 칸 = 비목만) = 소계 61(줄참조) · 「수 × n% = 값」 + 금액 칸 = 율 58(줄참조) ·
|
||||
「(A 수 + 수) × n%」 = 율 62(구간 합) · 「= A수」 = 구간 표지(`\\x06A\\x06`) · 그 밖 = 글 줄.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
from M02_MasterTemplete import M02_San_Xlsx as sx
|
||||
|
||||
SHEET = "단가산출"
|
||||
LINK = {"자재": 5, "노임": 6, "견적": 101, "산근": 104, "기계": 105, "소계": 61, "율": 58}
|
||||
MEAN = {5: "자재", 6: "노임", 101: "견적", 104: "산근", 105: "기계", 61: "소계", 58: "율"}
|
||||
MEAN[62] = "율"
|
||||
LABEL = {"노무비": "L", "재료비": "M", "경비": "S", "계": "T"}
|
||||
NUM = r"\d{1,3}(?:,\d{3})+(?:\.\d+)?|\d+(?:\.\d+)?|\.\d+" # 「.1」 = 엑셀 꼴 0.1
|
||||
_NUM = re.compile(r"(?<![A-Za-z0-9_.,])(-?(?:" + NUM + r"))\.?(?![\d,])")
|
||||
_HEAD = re.compile(r"^(\d+)\.(.*?)\[([^\[\]]*)\]\s*$")
|
||||
_HEADING = re.compile(r"^\s*(?:\(\d+\)|(?:\d+|[가-하])\s*[.)])\s*")
|
||||
_REF = re.compile(r"\*\s*(단산|견적)\s*(\d+)\s*참조\s*\*")
|
||||
_ASSIGN = re.compile(r"(?<![A-Za-z0-9_])([A-Za-z][A-Za-z0-9]*)\s*(?:\([^)=]*\))?\s*=(?!=)")
|
||||
_GROUP62 = re.compile(r"^\s*\(?\s*([A-Z])\s*(" + NUM + r")((?:\s*\+\s*(?:" + NUM + r"))*)")
|
||||
_UNITS = {"hr", "m", "km", "kg", "ton", "sec", "min", "mm", "cm", "ea", "t"}
|
||||
_COLON = re.compile(r"[::]")
|
||||
|
||||
|
||||
def _v(s: str) -> float:
|
||||
return float(s.replace(",", "").rstrip("."))
|
||||
|
||||
|
||||
def _close(a: float, b: float) -> bool:
|
||||
return abs(a - b) < 1e-6 * max(1.0, abs(b))
|
||||
|
||||
|
||||
def _span(text: str, name: str, start: int = 0) -> tuple[int, int] | None:
|
||||
"""빈칸을 건너 같은 글자 차례 자리(이름 · 규격) · 없음 None."""
|
||||
chars = [re.escape(c) for c in sx.flat(name)]
|
||||
if not chars:
|
||||
return None
|
||||
m = re.compile(r"\s*".join(chars)).search(text, start)
|
||||
return (m.start(), m.end()) if m else None
|
||||
|
||||
|
||||
# ── 시트 → 블록 ───────────────────────────────────────────────────────
|
||||
def blocks(rows: list[list[Any]]) -> list[dict[str, Any]]:
|
||||
"""단가산출 시트 → [{번, 머리, 행, 저장, 줄 [{행, 엑셀행, 보임, 돈}]}](머리 = B 열 「N.이름[단위]」)."""
|
||||
cols = sx.money_cols(rows) or [2, 3, 4, 5]
|
||||
split = cols[0] > 6 # 거창 꼴 — 글자 칸 나눔(열 = 화면 자리 · B 열 = 머리만)
|
||||
out: list[dict[str, Any]] = []
|
||||
for i, r in enumerate(rows):
|
||||
head = r[1] if len(r) > 1 else None
|
||||
m = _HEAD.match(head) if isinstance(head, str) and not head[:1].isspace() else None
|
||||
if m and i > 2:
|
||||
out.append({"번": int(m.group(1)), "머리": head, "행": i + 1, "줄": []})
|
||||
out[-1]["저장"] = sx.money_of(r, cols)
|
||||
continue
|
||||
if not out:
|
||||
continue
|
||||
if split:
|
||||
cells = [(j, sx._cell(v)) for j, v in enumerate(r[2 : cols[0]], 2)]
|
||||
text = sx.layout([(j, v) for j, v in cells if v.strip()], 2)
|
||||
else:
|
||||
text = sx._cell(head)
|
||||
one = {"행": len(out[-1]["줄"]) + 1, "엑셀행": i + 1, "보임": text, "칸": split}
|
||||
out[-1]["줄"].append(one | {"돈": sx.money_of(r, cols)})
|
||||
for b in out: # 끝 빈 줄(시트 꼬리)
|
||||
while b["줄"] and not b["줄"][-1]["보임"].strip() and not b["줄"][-1]["돈"]:
|
||||
b["줄"].pop()
|
||||
return out
|
||||
|
||||
|
||||
# ── 줄 ────────────────────────────────────────────────────────────────
|
||||
def _label(text: str) -> str | None:
|
||||
"""「노무비 :」 · 「경 비 :」 · 「계 :」 → 비목 글자 · 아니면 None."""
|
||||
m = _COLON.search(text)
|
||||
if not m:
|
||||
return None
|
||||
return LABEL.get(sx.flat(_HEADING.sub("", text[: m.start()])))
|
||||
|
||||
|
||||
def _nums(text: str, lo: int = 0, hi: int | None = None) -> list[tuple[int, int, float]]:
|
||||
hi = len(text) if hi is None else hi
|
||||
return [(m.start(1), m.end(1), _v(m.group(1))) for m in _NUM.finditer(text, lo, hi)]
|
||||
|
||||
|
||||
def _result(text: str) -> tuple[int, int, float, int | None] | None:
|
||||
"""마지막 「=」 뒤 첫 수(구간 글자 「A」 붙음 허용) → (시작, 끝, 값, 글자 자리)."""
|
||||
eq = text.rfind("=")
|
||||
if eq < 0:
|
||||
return None
|
||||
m = re.match(r"\s*(?:([A-Z])\s*)?(-?(?:" + NUM + r"))\.?(?![\d,])", text[eq + 1 :])
|
||||
if not m:
|
||||
return None
|
||||
at = eq + 1
|
||||
return at + m.start(2), at + m.end(2), _v(m.group(2)), at + m.start(1) if m.group(1) else None
|
||||
|
||||
|
||||
class Lines:
|
||||
"""블록 한 벌 줄 → 구조 줄."""
|
||||
|
||||
def __init__(self, res: dict[str, list[dict]], tags: list[str]) -> None:
|
||||
self.res, self.tags = res, tags
|
||||
self.vars: dict[str, float] = {} # 이름 → 마지막 대입 값
|
||||
self.ctx: dict | None = None # 세 줄 머리 자원 {종류, 자원}
|
||||
self.triple = False # 바로 앞이 세 줄
|
||||
self.heads: list[dict] = [] # 세 줄 머리 줄들
|
||||
self.lines: list[dict[str, Any]] = []
|
||||
|
||||
# ── 꼴 ─────────────────────────────────────────────────────────
|
||||
def blank(self, src: dict) -> dict[str, Any]:
|
||||
return {
|
||||
"행": src["행"],
|
||||
"엑셀행": src["엑셀행"],
|
||||
"글": src["보임"],
|
||||
"보임": src["보임"],
|
||||
"품명": None,
|
||||
"규격": None,
|
||||
"단위": None,
|
||||
"단": 0,
|
||||
"색인": 0,
|
||||
"연결": 0,
|
||||
"연결뜻": None,
|
||||
"비목": None,
|
||||
"자원": None,
|
||||
"연결단가": [],
|
||||
"변수": [],
|
||||
"줄참조": [],
|
||||
"결과": [],
|
||||
"합계": False,
|
||||
"자리": {"자리수": None, "방법": None},
|
||||
"표지": None,
|
||||
"기계": None,
|
||||
"산근": None,
|
||||
"저장": src.get("돈"),
|
||||
"추정": False,
|
||||
"_marks": [],
|
||||
"_칸": src.get("칸"),
|
||||
}
|
||||
|
||||
def resource(self, kind: str, one: dict) -> dict[str, Any]:
|
||||
code = (
|
||||
f"단산{one['번']}" if kind == "산근" else f"{one['표']}#{one['번']}"
|
||||
) # 원 코드 없음 = 표 줄 번
|
||||
out = {"코드": code, "종류": kind, "표": one["표"]}
|
||||
out |= {
|
||||
k: one[k]
|
||||
for k in ("이름", "규격", "단위") + (("같은구성",) if "같은구성" in one else ())
|
||||
}
|
||||
return out | {"저장": dict(one["값"])}
|
||||
|
||||
def link(self, ln: dict, kind: str, one: dict | None) -> None:
|
||||
ln["연결"] = LINK[kind] if kind in LINK else 62
|
||||
ln["연결뜻"] = MEAN.get(ln["연결"])
|
||||
ln["추정"] = True
|
||||
if one is not None:
|
||||
ln["자원"] = self.resource(kind, one)
|
||||
if kind == "산근":
|
||||
ln["산근"] = ln["자원"]["코드"]
|
||||
|
||||
@staticmethod
|
||||
def money_part(ln: dict) -> str | None:
|
||||
"""금액 칸 비목 — 노 · 재 · 경 중 값 하나 · 셋 다 = T · 없음 = None."""
|
||||
got = [c for c in "LMS" if (ln["저장"] or {}).get(c)]
|
||||
return got[0] if len(got) == 1 else ("T" if got else None)
|
||||
|
||||
@staticmethod
|
||||
def closed(text: str, end: int) -> bool:
|
||||
"""칸 나눔 꼴 — 표지 붙은 수 뒤는 닫는 표지 2 칸(빈칸 · 수 끝 점 2 이상 · 줄 끝)."""
|
||||
gap = len(text[end:]) - len(text[end:].lstrip(" ."))
|
||||
return gap >= 2 or not text[end:].strip()
|
||||
|
||||
def wrap(self, ln: dict, s: int, e: int, ch: str) -> None:
|
||||
ln["_marks"].append((s, e, ch))
|
||||
|
||||
# ── 자원 찾기 ─────────────────────────────────────────────────
|
||||
def names(self, ln: dict, one: dict, start: int = 0) -> int | None:
|
||||
"""이름 · 규격 · 단위 자리 표지(`\\x0e` · `\\x13` · `\\x15`) · 끝 자리."""
|
||||
text = ln["보임"]
|
||||
sp = _span(text, one["이름"], start)
|
||||
if not sp:
|
||||
return None
|
||||
self.wrap(ln, *sp, "\x0e")
|
||||
ln["품명"] = text[sp[0] : sp[1]]
|
||||
end = sp[1]
|
||||
if one.get("규격"):
|
||||
m = re.match(r"\s*", text[end:])
|
||||
at = end + m.end()
|
||||
size = _span(text, one["규격"], at)
|
||||
if size and size[0] - at <= 1:
|
||||
lo = size[0] - (1 if text[size[0] - 1 : size[0]] in "([" and size[0] > at else 0)
|
||||
hi = size[1] + (1 if text[size[1] : size[1] + 1] in ")]" and lo < size[0] else 0)
|
||||
self.wrap(ln, lo, hi, "\x13")
|
||||
ln["규격"] = text[lo:hi]
|
||||
end = hi
|
||||
unit = re.match(r"\s*(\[[^\[\]]*\])", text[end:])
|
||||
if unit:
|
||||
self.wrap(ln, end + unit.start(1), end + unit.end(1), "\x15")
|
||||
ln["단위"] = unit.group(1)
|
||||
end += unit.end(1)
|
||||
return end
|
||||
|
||||
def find_item(self, ln: dict) -> tuple[str, dict, tuple[int, int, float]] | None:
|
||||
"""노임 · 자재 — 글 속 수가 그 단가이고 이름이 그 수 앞에 있음(가장 긴 이름 · 규격 맞음 먼저)."""
|
||||
text, body = ln["보임"], sx.flat(ln["보임"])
|
||||
best = None
|
||||
for hit in _nums(text):
|
||||
for kind, one in self.res["_값"].get(round(hit[2], 4), ()):
|
||||
name = sx.flat(one["이름"])
|
||||
if not name or name not in body:
|
||||
continue
|
||||
sp = _span(text, one["이름"])
|
||||
if not sp or sp[1] > hit[0]:
|
||||
continue
|
||||
key = (not one["규격"] or sx.flat(one["규격"]) in body, len(name))
|
||||
if best is None or key > best[0]:
|
||||
best = (key, kind, one, hit)
|
||||
return best[1:] if best else None
|
||||
|
||||
def find_head(self, ln: dict) -> tuple[str, dict] | None:
|
||||
"""세 줄 머리 — 「* 단산 N 참조 *」 · 「* 견적 N 참조 *」 · 기계 이름(+규격)."""
|
||||
text = ln["보임"]
|
||||
m = _REF.search(text)
|
||||
if m:
|
||||
kind = "산근" if m.group(1) == "단산" else "견적"
|
||||
one = self.res["_번"].get((kind, int(m.group(2))))
|
||||
if kind == "견적" and (not one or sx.flat(one["이름"]) not in sx.flat(text)):
|
||||
body = sx.flat(text[: m.start()]) # 견적 번 ≠ 표 차례 — 이름으로(가장 긴 것)
|
||||
hits = [x for x in self.res.get("견적") or [] if sx.flat(x["이름"]) in body]
|
||||
one = (
|
||||
max(hits, key=lambda x: len(sx.flat(x["이름"]) + sx.flat(x["규격"])))
|
||||
if hits
|
||||
else None
|
||||
)
|
||||
return (kind, one) if one else None
|
||||
if "=" in text or self.res["_기계틀"] is None or text.lstrip()[:1] in ("※", "*"):
|
||||
return None
|
||||
body, best = sx.flat(text), None
|
||||
for at in self.res["_기계틀"].finditer(body):
|
||||
name = at.group(1)
|
||||
for one in self.res["_기계"][name]:
|
||||
size = sx.flat(one["규격"])
|
||||
tail = body[at.start() + len(name) : at.start() + len(name) + len(size) + 2]
|
||||
if size and size not in tail:
|
||||
continue
|
||||
key = len(name) + len(size)
|
||||
if best is None or key > best[0]:
|
||||
best = (key, one)
|
||||
return ("기계", best[1]) if best else None
|
||||
|
||||
# ── 줄 갈래 ───────────────────────────────────────────────────
|
||||
def run(self, src: dict) -> dict[str, Any]:
|
||||
ln = self.blank(src)
|
||||
text = ln["보임"]
|
||||
if not text.strip() and not ln["저장"]:
|
||||
ln["연결"] = None
|
||||
return self.done(ln)
|
||||
heading = bool(_HEADING.match(text))
|
||||
part = _label(text)
|
||||
if heading:
|
||||
ln["단"] = 1
|
||||
title = sx.flat(_HEADING.sub("", text))
|
||||
if re.search(r"[합소총]계|^계", title):
|
||||
self.heads = [] # 합계 절 — 세 줄 머리 닫음
|
||||
self.triple = False
|
||||
if self.triple and "소계" in sx.flat(text.split(":")[0]) and self.ctx:
|
||||
return self.done(self.triple_total(ln))
|
||||
self.triple = False
|
||||
if part in ("L", "M", "S") and not heading and self.triple_line(ln, part):
|
||||
return self.done(ln)
|
||||
got = self.find_item(ln)
|
||||
total = ln["저장"] if part == "T" else not ln["저장"] # 합계 = 계 줄만 금액 칸
|
||||
if part and not heading and got is None and total:
|
||||
return self.done(self.total_line(ln, part))
|
||||
if got is not None:
|
||||
return self.done(self.item(ln, *got))
|
||||
if (head := self.find_head(ln)) is not None:
|
||||
kind, one = head
|
||||
self.link(ln, kind, one)
|
||||
end = self.names(ln, one)
|
||||
loose = end is None or (one["규격"] and ln["규격"] is None) # 규격이 이름 곁에 없음
|
||||
loose = loose or text[end or 0 :].lstrip()[:1] in (":", ":")
|
||||
if ln["단"] == 0 and kind == "기계" and loose: # 설명 글 속 장비 이름
|
||||
ln |= {"연결": 0, "연결뜻": None, "자원": None, "추정": False, "품명": None}
|
||||
ln |= {"규격": None, "단위": None, "_marks": []}
|
||||
self.plain(ln)
|
||||
return self.done(ln)
|
||||
self.heads.append({"종류": kind, "자원": one, "줄": ln, "씀": False})
|
||||
self.ctx = self.heads[-1]
|
||||
return self.done(ln)
|
||||
if self.subtotal(ln) or self.rate62(ln) or self.rate(ln):
|
||||
return self.done(ln)
|
||||
self.plain(ln)
|
||||
return self.done(ln)
|
||||
|
||||
def sections(self, saved: dict) -> None:
|
||||
"""절 소계(가 · 나 … 머리 아래 합계 줄 「노무비 : a + b = X」)가 뺀 줄 하나 = 합에 안 든 줄(비목 없음) —
|
||||
절(앞 숫자 머리부터) 안 그 비목 줄 결과 합 − X 가 꼭 한 줄 값이고 나머지 줄 값은 모두 덧셈 글에 있을
|
||||
때만 · 블록 금액도 그 줄을 빼야 맞을 때만(엑셀 글 덧셈 · 원파일 SCheck 0)."""
|
||||
lines, start = self.lines, 0
|
||||
for i, ln in enumerate(lines):
|
||||
text = ln["보임"]
|
||||
if ln["단"] == 1 and re.match(r"^\s*\d", text):
|
||||
start = i
|
||||
if not (ln["합계"] and ln["비목"] in ("L", "M", "S") and ln["결과"] and "+" in text):
|
||||
continue
|
||||
head = next((k for k in range(i - 1, start - 1, -1) if lines[k]["단"] == 1), None)
|
||||
if head is None or not re.match(r"^\s*[가-하]", lines[head]["보임"]):
|
||||
continue
|
||||
part, want = ln["비목"], ln["결과"][-1]
|
||||
kids = [
|
||||
x for x in lines[start:head] if x["비목"] == part and x["결과"] and not x["합계"]
|
||||
]
|
||||
kids += [
|
||||
x for x in lines[start:head] if x["비목"] == part and x["줄참조"] and not x["결과"]
|
||||
]
|
||||
value = {id(x): (x["결과"] or x["줄참조"])[-1] for x in kids}
|
||||
extra = sum(value.values()) - want
|
||||
odd = [x for x in kids if abs(value[id(x)] - extra) < 0.5]
|
||||
colon = _COLON.search(text)
|
||||
adds = [v for _, _, v in _nums(text, colon.end() if colon else 0, text.rfind("="))]
|
||||
rest = [value[id(x)] for x in kids if odd and x is not odd[0]]
|
||||
listed = all(any(_close(v, a) for a in adds) for v in rest)
|
||||
if len(odd) != 1 or abs(extra) < 0.5 or not listed:
|
||||
continue
|
||||
every = sum(
|
||||
(x["결과"] or x["줄참조"])[-1]
|
||||
for x in lines
|
||||
if x["비목"] == part and not x["합계"] and (x["결과"] or x["줄참조"])
|
||||
)
|
||||
block = float(saved.get(part) or 0) # 블록 금액이 그 줄을 빼야 맞을 때만
|
||||
if abs(every - block) >= 1 and abs(every - extra - block) < 1:
|
||||
odd[0]["비목"] = None
|
||||
|
||||
def unused(self) -> None:
|
||||
"""머리 아닌 줄의 기계 이름 — 아래 세 줄이 안 쓰면 글 줄(설명 속 장비 이름)."""
|
||||
for h in self.heads:
|
||||
ln = h["줄"]
|
||||
if h["종류"] == "기계" and not h["씀"] and ln["단"] == 0:
|
||||
ln |= {"연결": 0, "연결뜻": None, "자원": None, "추정": False, "품명": None}
|
||||
ln |= {"규격": None, "단위": None, "_marks": []}
|
||||
|
||||
def done(self, ln: dict) -> dict[str, Any]:
|
||||
self.lines.append(ln)
|
||||
return ln
|
||||
|
||||
def total_line(self, ln: dict, part: str) -> dict:
|
||||
"""합계 줄 — 「노무비 : 식 = 값」 · 「재료비 : 값」 · 「계 : 값」(계 = 결과)."""
|
||||
text = ln["보임"]
|
||||
ln |= {"합계": True, "비목": part, "단": -1}
|
||||
lead = len(text) - len(text.lstrip())
|
||||
res = _result(text)
|
||||
if res:
|
||||
self.wrap(ln, lead, text.rfind("=") + 1, "\x1a")
|
||||
self.result(ln, res)
|
||||
elif part == "T":
|
||||
colon = _COLON.search(text).end()
|
||||
self.wrap(ln, lead, colon, "\x1a")
|
||||
nums = _nums(text, colon)
|
||||
if nums:
|
||||
self.wrap(ln, nums[0][0], nums[0][1], "\x12")
|
||||
ln["결과"] = [nums[0][2]]
|
||||
else:
|
||||
self.wrap(ln, lead, len(text.rstrip()), "\x1a")
|
||||
return ln
|
||||
|
||||
def result(self, ln: dict, res: tuple) -> None:
|
||||
s, e, v, letter = res
|
||||
if letter is not None:
|
||||
self.wrap(ln, letter, letter + 1, "\x06")
|
||||
self.wrap(ln, s, e, "\x12")
|
||||
ln["결과"].append(v)
|
||||
|
||||
def triple_line(self, ln: dict, part: str) -> bool:
|
||||
"""세 줄 — 단가 = 머리 자원(기계 · 산근 · 견적)의 그 비목 값."""
|
||||
text = ln["보임"]
|
||||
colon = _COLON.search(text).end()
|
||||
nums = _nums(text, colon)
|
||||
if not nums or not self.heads:
|
||||
return False
|
||||
price = nums[0]
|
||||
tail = text[price[1] :]
|
||||
if ("=" not in tail and tail.strip()) or tail.lstrip()[:1] == "+":
|
||||
return False
|
||||
fit = [
|
||||
h
|
||||
for h in reversed(self.heads)
|
||||
if _close(price[2], float(h["자원"]["값"].get(part) or 0))
|
||||
]
|
||||
if not fit:
|
||||
return False
|
||||
self.ctx = fit[0]
|
||||
self.ctx["씀"] = True
|
||||
one = self.ctx["자원"]
|
||||
self.link(ln, self.ctx["종류"], one)
|
||||
ln["비목"] = part
|
||||
if self.ctx["종류"] == "기계":
|
||||
ln["기계"] = True
|
||||
self.money(ln, price)
|
||||
self.triple = True
|
||||
return True
|
||||
|
||||
def triple_total(self, ln: dict) -> dict:
|
||||
one = self.ctx["자원"]
|
||||
self.link(ln, self.ctx["종류"], one)
|
||||
ln["비목"] = "T"
|
||||
text = ln["보임"]
|
||||
res = _result(text)
|
||||
if res:
|
||||
self.result(ln, res)
|
||||
else:
|
||||
m = re.search(r"[::]\s*(?:([A-Z])\s*)?(" + NUM + r")", text)
|
||||
if m:
|
||||
self.result(
|
||||
ln, (m.start(2), m.end(2), _v(m.group(2)), m.start(1) if m.group(1) else None)
|
||||
)
|
||||
money = ln["저장"] or {}
|
||||
rows = []
|
||||
for one in reversed(self.lines):
|
||||
if one["연결"] != ln["연결"] or one["비목"] not in ("L", "M", "S"):
|
||||
break
|
||||
rows.append(one)
|
||||
cols = [c for c in "LMS" if money.get(c)]
|
||||
total = sum((one["결과"] or [0])[-1] for one in rows)
|
||||
if len(cols) == 1 and money.get("T") and abs(total - money["T"]) < 1:
|
||||
for one in rows: # 계 줄 금액 칸 비목 하나 = 값 있는 세 줄 모두 그 비목으로 옮김
|
||||
if (one["결과"] or [0])[-1]:
|
||||
one["비목"] = cols[0]
|
||||
elif money:
|
||||
for one in rows: # 계 줄 금액 칸이 빈 비목 = 합에 안 든 줄(비목 없음)
|
||||
if not money.get(one["비목"]) and one["결과"] and one["결과"][-1]:
|
||||
one["비목"] = None
|
||||
self.triple = False
|
||||
return ln
|
||||
|
||||
def item(self, ln: dict, kind: str, one: dict, price: tuple) -> dict:
|
||||
self.link(ln, kind, one)
|
||||
self.names(ln, one)
|
||||
part = self.money_part(ln)
|
||||
ln["비목"] = part if part in ("L", "M", "S") else None
|
||||
self.money(ln, price)
|
||||
return ln
|
||||
|
||||
def money(self, ln: dict, price: tuple[int, int, float]) -> None:
|
||||
"""금액 줄 — 단가(`\\x03`) · 첫 식 조각의 앞 대입 값 = 변수(`\\x11`) · 끝 = 결과."""
|
||||
text = ln["보임"]
|
||||
s, e, v = price
|
||||
self.wrap(ln, s, e, "\x03")
|
||||
ln["연결단가"] = [v]
|
||||
self.var(ln, e)
|
||||
res = _result(text)
|
||||
if res and res[0] > e:
|
||||
self.result(ln, res)
|
||||
|
||||
def var(self, ln: dict, start: int) -> None:
|
||||
"""첫 식 조각(start ~ 첫 「=」)의 수 중 가장 늦게 대입한 변수 값 하나 = 변수(`\\x11` · 원파일 = 줄마다 하나) ·
|
||||
칸 나눔 꼴은 닫는 표지 자리(2 칸)가 있는 수만."""
|
||||
text = ln["보임"]
|
||||
eq = text.find("=", start)
|
||||
order = list(self.vars.values())
|
||||
best = None
|
||||
for a, b, val in _nums(text, start, eq if eq >= 0 else None):
|
||||
rank = max((i for i, x in enumerate(order) if _close(val, x)), default=-1)
|
||||
if ln["_칸"] and not self.closed(text, b):
|
||||
continue
|
||||
if rank >= 0 and (best is None or rank > best[0]):
|
||||
best = (rank, a, b, val)
|
||||
if best:
|
||||
self.wrap(ln, best[1], best[2], "")
|
||||
ln["변수"].append(best[3])
|
||||
|
||||
def subtotal(self, ln: dict) -> bool:
|
||||
"""「…소계 : 수」 — 금액 칸 계 없음 · 비목 하나(61) · 금액 칸 없음 = 비목 없음."""
|
||||
text = ln["보임"]
|
||||
m = _COLON.search(text)
|
||||
if not m or "소계" not in sx.flat(text[: m.start()]):
|
||||
return False
|
||||
money = ln["저장"] or {}
|
||||
parts = [c for c in "LMS" if money.get(c)]
|
||||
if len(parts) != 1 or (
|
||||
money.get("T") is not None and not _close(money["T"], money[parts[0]])
|
||||
):
|
||||
return False # 소계 61 = 금액 칸 비목 하나(계 칸 없음 · 있으면 같은 값)
|
||||
got = re.match(r"\s*(?:([A-Z])\s*)?(" + NUM + r")\.?(?![\d,])", text[m.end() :])
|
||||
if not got:
|
||||
return False
|
||||
at = m.end()
|
||||
if got.group(1):
|
||||
self.wrap(ln, at + got.start(1), at + got.end(1), "\x06")
|
||||
self.wrap(ln, at + got.start(2), at + got.end(2), "\x0c")
|
||||
ln["줄참조"] = [_v(got.group(2))]
|
||||
ln["저장"] = {**money, "T": money[parts[0]]}
|
||||
self.link(ln, "소계", None)
|
||||
ln["비목"] = self.money_part(ln)
|
||||
ln["표지"] = "소계"
|
||||
self.var(ln, at + got.end(2))
|
||||
res = _result(text)
|
||||
if res and res[0] > at + got.end(2):
|
||||
self.result(ln, res)
|
||||
return True
|
||||
|
||||
def rate62(self, ln: dict) -> bool:
|
||||
"""「(A 수 + 수) × n% = 값」 · 「A 수 + 수 = 합 × n% = 값」 — 구간 합 율(62 · 합계 표지)."""
|
||||
text = ln["보임"]
|
||||
m = _GROUP62.search(text)
|
||||
if not m or not re.search(r"[×xX*]", text[m.end() :]) or not ln["저장"]:
|
||||
return False
|
||||
self.wrap(ln, m.start(1), m.end(1), "\x07")
|
||||
self.wrap(ln, m.start(2), m.end(3), "\x1a")
|
||||
ln |= {"합계": True}
|
||||
self.link(ln, "율62", None)
|
||||
ln["비목"] = self.money_part(ln)
|
||||
ln["표지"] = "율"
|
||||
for r in re.finditer(r"=\s*(?:([A-Z])\s*)?(" + NUM + r")\.?(?![\d,])", text):
|
||||
self.result(
|
||||
ln, (r.start(2), r.end(2), _v(r.group(2)), r.start(1) if r.group(1) else None)
|
||||
)
|
||||
return True
|
||||
|
||||
def rate(self, ln: dict) -> bool:
|
||||
"""「수 × n% = 값」 + 금액 칸 — 율(58 · 첫 수 = 줄참조)."""
|
||||
text = ln["보임"]
|
||||
res = _result(text)
|
||||
if not ln["저장"] or not res:
|
||||
return False
|
||||
colon = _COLON.search(text)
|
||||
nums = _nums(text, colon.end() if colon else 0, text.rfind("="))
|
||||
seen = [v for x in self.lines for v in x["결과"] + x["줄참조"]]
|
||||
before = any(_close(nums[0][2], v) for v in seen) if nums else False
|
||||
lead = text[colon.end() if colon else 0 :].lstrip(" (")
|
||||
before = before and lead[:1].isdigit()
|
||||
if not nums or ("%" not in text[: text.rfind("=")] and not before):
|
||||
return False
|
||||
self.wrap(ln, nums[0][0], nums[0][1], "\x0c")
|
||||
ln["줄참조"] = [nums[0][2]]
|
||||
self.link(ln, "율", None)
|
||||
ln["비목"] = self.money_part(ln)
|
||||
ln["표지"] = "율"
|
||||
self.result(ln, res)
|
||||
return True
|
||||
|
||||
def plain(self, ln: dict) -> None:
|
||||
"""글 · 변수 줄 — 대입마다 「이름 = 식 = 값」 = 결과 · 그 밖 「… = 값」(식 있는 줄) = 결과."""
|
||||
text = ln["보임"]
|
||||
ln["연결"] = 0
|
||||
part = self.money_part(ln)
|
||||
ln["비목"] = part
|
||||
hits: list[re.Match] = []
|
||||
for m in _ASSIGN.finditer(text):
|
||||
if m.group(1).lower() in _UNITS and re.search(r"\d\.?\s?$", text[: m.start()]):
|
||||
continue # 「8 hr =」 = 단위
|
||||
gap = text[hits[-1].end() : m.start()] if hits else ""
|
||||
if not hits or (re.search(r"\d", gap) and not re.search(r"[×÷+\-*/(=]\s*$", gap)):
|
||||
hits.append(m)
|
||||
lead = text[: hits[0].start()].strip(" ○●·-{") if hits else ""
|
||||
if hits and (not lead or re.search(r"[::]$", lead)):
|
||||
for n, m in enumerate(hits):
|
||||
end = hits[n + 1].start() if n + 1 < len(hits) else len(text)
|
||||
seg = text[m.end() : end]
|
||||
parts = seg.split("=")
|
||||
val = re.match(r"\s*(?:[A-Z](?=\d))?(" + NUM + r")", parts[-1])
|
||||
if not val:
|
||||
continue
|
||||
at = m.end() + len(seg) - len(parts[-1])
|
||||
if len(parts) >= 2:
|
||||
res = _result(text[:end])
|
||||
if res and res[0] >= at:
|
||||
self.result(ln, res)
|
||||
self.vars.pop(m.group(1), None)
|
||||
self.vars[m.group(1)] = _v(val.group(1))
|
||||
return
|
||||
eq = text.rfind("=")
|
||||
seg = re.split(r"[=::]", text[:eq])[-1]
|
||||
if eq > 0 and re.search(r"[×÷+*]|\d\s*/|/\s*\d", seg):
|
||||
res = _result(text)
|
||||
if res:
|
||||
self.result(ln, res)
|
||||
|
||||
|
||||
# ── 책 ────────────────────────────────────────────────────────────────
|
||||
def _glyph(text: str, marks: list[tuple[int, int, str]]) -> str:
|
||||
"""보인 글 + 표지 자리 → 원파일 LineData 꼴(닫는 표지 먼저)."""
|
||||
ins: list[tuple[int, int, str]] = []
|
||||
for s, e, ch in marks:
|
||||
ins.append((s, 1, ch))
|
||||
ins.append((e, 0, ch))
|
||||
out, last = [], 0
|
||||
for at, _, ch in sorted(ins, key=lambda x: (x[0], x[1])):
|
||||
out.append(text[last:at])
|
||||
out.append(ch)
|
||||
last = at
|
||||
out.append(text[last:])
|
||||
return "".join(out)
|
||||
|
||||
|
||||
def index(res: dict[str, list[dict]]) -> dict[str, Any]:
|
||||
"""자원 표 → 찾기 색인(단가 → 노임 · 자재 · 이름 → 기계 · (종류, 번) → 산근 · 견적)."""
|
||||
out: dict[str, Any] = dict(res)
|
||||
out["_값"] = {}
|
||||
for kind in ("노임", "자재"):
|
||||
for one in res.get(kind) or []:
|
||||
out["_값"].setdefault(round(one["값"]["T"], 4), []).append((kind, one))
|
||||
out["_기계"] = {}
|
||||
for one in res.get("기계") or []:
|
||||
if sx.flat(one["이름"]):
|
||||
out["_기계"].setdefault(sx.flat(one["이름"]), []).append(one)
|
||||
names = sorted(out["_기계"], key=len, reverse=True)
|
||||
out["_기계틀"] = re.compile("(?=(" + "|".join(map(re.escape, names)) + "))") if names else None
|
||||
out["_번"] = {(k, one["번"]): one for k in ("산근", "견적") for one in res.get(k) or []}
|
||||
return out
|
||||
|
||||
|
||||
def read(sheets: dict[str, list]) -> dict[str, Any]:
|
||||
"""EST 엑셀 시트 → {꼴, 산근, 일위, 자원, 내역, 표지}(`M02_San_Estx.read` 꼴 · 일위 = 호표 길)."""
|
||||
res = sx.resources(sheets)
|
||||
look = index(res)
|
||||
total = {one["번"]: one for one in res.get("산근") or []}
|
||||
out: dict[str, Any] = {"꼴": "EST", "산근": [], "일위": [], "내역": None}
|
||||
for b in blocks(sheets.get(SHEET) or []):
|
||||
tags: list[str] = []
|
||||
one = total.get(b["번"])
|
||||
m = _HEAD.match(b["머리"])
|
||||
head = {
|
||||
"코드": f"단산{b['번']}",
|
||||
"이름": one["이름"] if one else m.group(2).strip(),
|
||||
"규격": one["규격"] if one else "",
|
||||
"단위": one["단위"] if one else m.group(3).strip(),
|
||||
"저장": b["저장"] or (dict(one["값"]) if one else None),
|
||||
}
|
||||
if not one:
|
||||
tags.append(f"{sx.NOTE}총괄표에 단산{b['번']} 없음")
|
||||
elif one.get("칸"): # 총괄표 당초 / 적용 칸(797-1a)
|
||||
tags.append(f"참고 · {one['칸']}")
|
||||
work = Lines(look, tags)
|
||||
for src in b["줄"]:
|
||||
work.run(src)
|
||||
work.unused()
|
||||
work.sections(head["저장"] or {})
|
||||
lines = []
|
||||
for ln in work.lines:
|
||||
marks = ln.pop("_marks")
|
||||
ln.pop("_칸", None)
|
||||
ln["글"] = _glyph(ln["보임"], marks)
|
||||
lines.append(ln)
|
||||
out["산근"].append({**head, "번": b["번"], "엑셀행": b["행"], "줄": lines, "표지": tags})
|
||||
out["자원"] = {
|
||||
f"{kind}:{one['번']}": {k: one[k] for k in ("이름", "규격", "단위", "표")}
|
||||
| {"저장": one["값"]}
|
||||
for kind, rows in res.items()
|
||||
for one in rows
|
||||
}
|
||||
out["표지"] = [
|
||||
f"{sx.NOTE}자원 코드 · LinkIndex · 자리(Diget) 없음 — 연결뜻 = 글 · 금액 칸 · 단가표로 정함({sx.GUESS})",
|
||||
f"{sx.NOTE}자원 코드 = 단가표 이름#No(산근 = 단산 번)",
|
||||
]
|
||||
return out
|
||||
@@ -0,0 +1,77 @@
|
||||
"""STmate 산근 엑셀 → 810-1 구조 줄(PLAN 812-1 · `M02_San_Stc.read` 와 같은 꼴 · 읽기만 · 서버 단독).
|
||||
|
||||
엑셀 「단가산출근거」 숨은 G 열 = 원파일 COSTN.SMT 원문 그대로 → 줄 = `M02_San_Stc.stc_line`(풀이 =
|
||||
`M02_San_Stmate.parse_line` · 다시 짜지 않음) · 블록 머리 금액(C~F) = 원파일 저장(TAMT1 …) ·
|
||||
단가 = 목록표(노무비 · 재료비 · 경비 · 중기 · 단가산출근거 · 숨은 메타 = COSTN 칸) ·
|
||||
끝전 = 파일 투표(`_원파일파서_틀.md` 4 장 SAN_TYP 두 갈래 중 블록 계가 맞는 수가 많은 쪽 · 파일에 하나 ·
|
||||
같으면 비목버림).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from M02_MasterTemplete import M02_San_Book as book
|
||||
from M02_MasterTemplete import M02_San_Eval as ev
|
||||
from M02_MasterTemplete import M02_San_Stc as stc
|
||||
from M02_MasterTemplete import M02_San_Xlsx as sx
|
||||
|
||||
RULE = {"비목버림": "1", "합버림_큰나머지": "0"} # 끝전 갈래 → SAN_TYP
|
||||
|
||||
|
||||
def _amt(head: dict) -> dict[str, float]:
|
||||
return {c: float(head.get(k) or 0) for c, k in zip("TLMS", "CDEF")}
|
||||
|
||||
|
||||
def vote(blocks: list[dict], prices: dict) -> tuple[str, dict[str, int]]:
|
||||
"""파일 끝전 — 블록마다 두 갈래로 셈해(낙찰 · 조정 뒤) 엑셀 머리 계(C)와 맞는 수 · 많은 쪽(같으면
|
||||
비목버림)."""
|
||||
got = dict.fromkeys(RULE, 0)
|
||||
for blk in blocks:
|
||||
nodes = [ln["풀이"] for ln in blk["줄"]]
|
||||
want = (blk.get("저장") or {}).get("T")
|
||||
for rule in RULE:
|
||||
try:
|
||||
total = ev.stmate_block({"코드": blk["코드"]}, nodes, prices, rule)["금액"]["T"]
|
||||
except Exception: # noqa: BLE001 — 못 셈 = 표 없음
|
||||
continue
|
||||
got[rule] += want is not None and abs(float(total) - float(want)) < 0.5
|
||||
best = max(RULE, key=lambda r: (got[r], r == "비목버림"))
|
||||
return best, got
|
||||
|
||||
|
||||
def read(sheets: dict[str, list]) -> dict[str, Any]:
|
||||
"""STmate 엑셀 시트 → {꼴, 산근, 일위, 자원, 내역, 단가, 옵션, 표지}."""
|
||||
found = book.stmate_prices(sheets)
|
||||
prices = {c: {k: p.get(k) for k in ("T", "L", "M", "S", "meta")} for c, p in found.items()}
|
||||
res = {
|
||||
c: {"종류": stc.KIND.get(c[:1]), **{k: p.get(k) for k in ("이름", "규격", "단위")}}
|
||||
| {"저장": {k: p.get(k) for k in "TLMS"}}
|
||||
for c, p in found.items()
|
||||
}
|
||||
out: dict[str, Any] = {"꼴": "STmate", "산근": [], "일위": [], "자원": res, "단가": prices}
|
||||
for blk in book.stmate_blocks(sheets):
|
||||
tags: list[str] = []
|
||||
lines = [stc.stc_line(i, ln["G"], res, tags) for i, ln in enumerate(blk["몸"], 1)]
|
||||
for one, src in zip(lines, blk["몸"]):
|
||||
one["엑셀행"] = src["행"]
|
||||
me = found.get(blk["코드"]) or {}
|
||||
head = {"코드": blk["코드"], "이름": blk.get("이름") or me.get("이름")}
|
||||
head |= {"규격": me.get("규격"), "단위": me.get("단위"), "저장": _amt(blk["머리"])}
|
||||
_, tot = ev.run_stmate([ln["풀이"] for ln in lines], prices)
|
||||
body = [{k: ln.get(k) for k in ("행", "B", "C", "D", "E", "F")} for ln in blk["몸"]]
|
||||
out["산근"].append(
|
||||
{**head, "줄": lines, "줄합": tot, "표지": tags, "엑셀행": blk["행"]}
|
||||
| {
|
||||
"몸": body,
|
||||
"꼬리": blk["꼬리"],
|
||||
} # 엑셀 줄 금액 · 계 꼬리(검산 · `M02_San_Emit.stmate`)
|
||||
)
|
||||
rule, got = vote(out["산근"], prices)
|
||||
out["옵션"] = {"SAN_TYP": RULE[rule]}
|
||||
out["내역"] = None
|
||||
out["표지"] = [
|
||||
f"{sx.NOTE}일위대가 구성 · 내역은 호표 길(산근만 읽음)",
|
||||
f"{sx.NOTE}끝전 = 파일 투표 {rule}(비목버림 {got['비목버림']} · 합버림 {got['합버림_큰나머지']} 블록)",
|
||||
]
|
||||
return out
|
||||
@@ -1,23 +1,21 @@
|
||||
"""M02 불러오기 단산 블록 → 가져온 식 단가산출(PLAN 793-3b · 요구 646 · 647 · 652) — 서버 단독.
|
||||
|
||||
올림 때 경쟁사 산근 시트(`M02_San_Book.sheets` · STmate 숨은 G 열 포함)를
|
||||
`실무자료/산근/<HP>.json` 에 둠
|
||||
(원본 엑셀은 안 둠 · HP 지울 때 같이 지움) → [가져온 식으로 열기] = 그 블록을 793-2a 내보내기
|
||||
(`M02_San_Emit` · 이음 = 1차 줄 판정) → 793-2 만들기(`M02_Sangeun_Imported.create` · 개인 층 ·
|
||||
올림 때 경쟁사 산근 시트(`M02_San_Book.sheets` · STmate 숨은 G 열 포함)와 그 엑셀 읽기기 둘 꼴
|
||||
(812-1 `M02_San_Xlsx` → `M02_Doc_FromNative.pack` · 칸 `엑셀`)을 `실무자료/산근/<HP>.json` 에 둠
|
||||
(원본 엑셀은 안 둠 · HP 지울 때 같이 지움) → [가져온 식으로 열기] = 그 블록을 원파일과 같은 길
|
||||
(`M02_Doc_FromNative.emit` · 원본 `엑셀` 참) → 793-2 만들기(`M02_Sangeun_Imported.create` · 개인 층 ·
|
||||
엑셀 단가 그대로 · 경쟁사 로직 인정 — 근거 기초 제안 없음 · 809-1 · 요구 703) ·
|
||||
같은 내용 SK 가 있으면 그것(다시씀).
|
||||
블록 번 = 판정 블록 번(STmate = 코드 「D00102」 숫자 · EST = 단가산출 블록 번).
|
||||
STmate 블록 끝전(비목버림 | 합버림_큰나머지)은 파일 설정 — 참조 평가기로 둘 다 셈해
|
||||
엑셀 계와 맞는 것.
|
||||
원파일(.estx · .stc · 810-2 `M02_Doc_FromNative`) = 같은 자리 칸 `원파일`(둘 꼴) — 있으면 원파일 우선
|
||||
(그 블록 번이 원파일에 있을 때) · 엑셀 올림 때 같은 공사 원파일이 다른 HP 에 있으면 붙임 · 원파일 올림 =
|
||||
같은 공사 엑셀 HP 에 붙임(없으면 원파일로 새 HP · `upload_native`).
|
||||
STmate 블록 끝전 = 파일 투표(`M02_San_Xlsx_Stmate.vote`).
|
||||
원파일(.estx · .stc · 810-2 `M02_Doc_FromNative`) = 같은 자리 칸 `원파일`(둘 꼴) · 올리기 = 파일 하나
|
||||
(814-1 · 요구 742-1 — 같은 공사 엑셀 · 원파일 붙이기 걷음) · 원파일 = 새 HP(단가산출표 판 격자 · 일위대가 없음 ·
|
||||
`upload_native`).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
@@ -30,7 +28,6 @@ from M02_MasterTemplete import M02_San_Emit as em
|
||||
|
||||
DIRNAME = "산근"
|
||||
RELOAD = "가져온 식 자료 없음 — 엑셀을 다시 올림"
|
||||
RULES = ("비목버림", "합버림_큰나머지")
|
||||
MAX_INNER = 6 # 안쪽 산근 깊이 상한
|
||||
|
||||
|
||||
@@ -60,32 +57,34 @@ def keep(folder: str | Path, key: str, name: str, data: bytes) -> str | None:
|
||||
return f"엑셀 형식 오류 — {type(e).__name__}: {str(e)[:80]}"
|
||||
if not book.kind_of(sheets):
|
||||
return None
|
||||
try:
|
||||
excel = _excel(name, sheets)
|
||||
except Exception as e: # noqa: BLE001 — 산근 읽기 실패 = 사유(올리기는 됨)
|
||||
return f"산근 시트 읽기 오류 — {type(e).__name__}: {str(e)[:80]}"
|
||||
path = _path(folder, key)
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
got = {"파일": name, "시트": sheets}
|
||||
native = next((n for n in _natives(folder) if fn.same(n, sheets)), None)
|
||||
atomic_write_json(path, got | ({"원파일": native} if native else {}))
|
||||
atomic_write_json(path, {"파일": name, "시트": sheets, "엑셀": excel})
|
||||
return None
|
||||
|
||||
|
||||
def _excel(name: str, sheets: dict) -> dict:
|
||||
"""산근 시트 → 원파일과 같은 둘 꼴(812-1 엑셀 읽기기 `M02_San_Xlsx` → `M02_Doc_FromNative.pack`)."""
|
||||
from M02_MasterTemplete import M02_San_Xlsx as sx
|
||||
|
||||
return fn.pack(sx.read_sheets(sheets), name)
|
||||
|
||||
|
||||
def _excel_of(path: Path, saved: dict) -> dict:
|
||||
"""저장 HP 의 엑셀 둘 꼴 — 옛 저장(시트만)은 한 번 지어 그 자리에 둠."""
|
||||
if not isinstance(saved.get("엑셀"), dict):
|
||||
saved["엑셀"] = _excel(str(saved.get("파일") or ""), saved["시트"])
|
||||
atomic_write_json(path, saved)
|
||||
return saved["엑셀"]
|
||||
|
||||
|
||||
# ── 원파일(810-2) ──────────────────────────────────────────────────────
|
||||
def _saved(folder: str | Path) -> list[tuple[Path, dict]]:
|
||||
out = []
|
||||
for path in sorted((hopyo_store._dir(folder) / DIRNAME).glob("HP-*.json")):
|
||||
try:
|
||||
out.append((path, json.loads(path.read_bytes())))
|
||||
except (OSError, ValueError):
|
||||
continue
|
||||
return out
|
||||
|
||||
|
||||
def _natives(folder: str | Path) -> list[dict]:
|
||||
return [d["원파일"] for _, d in _saved(folder) if isinstance(d.get("원파일"), dict)]
|
||||
|
||||
|
||||
def upload_native(folder: str | Path, name: str, data: bytes, who: Any) -> dict[str, Any]:
|
||||
"""원파일 올림 — 같은 공사 엑셀 HP 에 원파일을 붙임(`붙음` 키들 · 첫 HP 답) · 없으면 원파일로
|
||||
새 HP(EST = 일위대가 호표 · STmate = 호표 없음 · 산근만). 같은 파일 다시 = 그 HP(`이어서`)."""
|
||||
"""원파일 올림 — 새 HP(단가산출표 판 격자 · 산근만 · 일위대가 없음 · 814-1) · 같은 파일 다시 = 그 HP(`이어서`)."""
|
||||
mark = hopyo_store.fingerprint(data)
|
||||
if same := next((r for r in hopyo_store.rows(folder) if r.get("지문") == mark), None):
|
||||
return {**same, "이어서": True, "원파일": True}
|
||||
@@ -95,18 +94,13 @@ def upload_native(folder: str | Path, name: str, data: bytes, who: Any) -> dict[
|
||||
native = fn.read(name, data)
|
||||
except fn.NativeError as e:
|
||||
raise store.StoreError(400, str(e)) from e
|
||||
joined = []
|
||||
for path, got in _saved(folder):
|
||||
if got.get("시트") and fn.same(native, got["시트"]):
|
||||
atomic_write_json(path, {**got, "원파일": native})
|
||||
joined.append(path.stem)
|
||||
rows = {r["키"]: r for r in hopyo_store.rows(folder)}
|
||||
if joined and joined[0] in rows:
|
||||
return {**rows[joined[0]], "원파일": True, "붙음": joined}
|
||||
row = hopyo_store.save(folder, name, fn.hopyo(native), who, None, mark)
|
||||
path = _path(folder, row["키"])
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
atomic_write_json(path, {"파일": name, "원파일": native})
|
||||
|
||||
def first(key: str) -> None: # HP 를 목록에 보이기 전에 산근 보관(814-1)
|
||||
path = _path(folder, key)
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
atomic_write_json(path, {"파일": name, "원파일": native})
|
||||
|
||||
row = hopyo_store.save(folder, name, fn.hopyo(native), who, fn.grid(native), mark, first)
|
||||
return {**row, "원파일": True}
|
||||
|
||||
|
||||
@@ -114,32 +108,12 @@ def has(folder: str | Path, key: str) -> bool:
|
||||
return _path(folder, key).is_file()
|
||||
|
||||
|
||||
def _rule(blk: dict, nodes: list, prices: dict) -> str:
|
||||
"""STmate 블록 끝전 — 엑셀 계(꼬리 · 없으면 머리)와 맞는 쪽 · 둘 다 아니면 비목버림."""
|
||||
from M02_MasterTemplete import M02_San_Eval as ev
|
||||
|
||||
tails = [t for t in blk["꼬리"] if t["A"].startswith("계") and not t["A"].startswith("계약")]
|
||||
want = (tails or [blk["머리"]])[0].get("C")
|
||||
for rule in RULES:
|
||||
try:
|
||||
got = ev.stmate_block(blk, nodes, prices, rule)["계"]["T"]
|
||||
except Exception: # noqa: BLE001 — 못 셈 = 다음 갈래
|
||||
continue
|
||||
if want is not None and abs(float(got) - float(want)) < 0.5:
|
||||
return rule
|
||||
return RULES[0]
|
||||
|
||||
|
||||
def emit(folder: str | Path, key: str, no: int) -> dict[str, Any]:
|
||||
"""HP 단산 블록 하나 → `{로직, 원본}`(793-2a 꼴 · 이음 = M01 키)."""
|
||||
from M02_MasterTemplete import M02_San_Book as book
|
||||
from M02_MasterTemplete import M02_San_Emit as em
|
||||
from M02_MasterTemplete import M02_San_Est as est
|
||||
from M02_MasterTemplete import M02_San_Stmate as stm
|
||||
|
||||
"""HP 단산 블록 하나 → `{로직, 원본}`(원파일 우선 · 아니면 엑셀 읽기기 같은 길 · 이음 = M01 키)."""
|
||||
hopyo_store.read(folder, key) # 없는 HP = 404
|
||||
path = _path(folder, key)
|
||||
try:
|
||||
saved = json.loads(_path(folder, key).read_bytes())
|
||||
saved = json.loads(path.read_bytes())
|
||||
except (OSError, ValueError) as e:
|
||||
raise store.StoreError(409, RELOAD) from e
|
||||
native = saved.get("원파일")
|
||||
@@ -147,52 +121,22 @@ def emit(folder: str | Path, key: str, no: int) -> dict[str, Any]:
|
||||
return fn.emit(native, int(no))
|
||||
if not saved.get("시트"):
|
||||
raise store.StoreError(404, f"없는 단산{no}")
|
||||
sheets, name = saved["시트"], str(saved.get("파일") or "")
|
||||
if book.kind_of(sheets) == "STmate":
|
||||
blk = next((b for b in book.stmate_blocks(sheets) if int(b["코드"][1:]) == int(no)), None)
|
||||
if blk is None:
|
||||
raise store.StoreError(404, f"없는 단산{no}")
|
||||
prices = book.stmate_prices(sheets)
|
||||
nodes = stm.parse_block(blk["몸"])
|
||||
return em.stmate(blk, nodes, prices, _rule(blk, nodes, prices), True, name)
|
||||
prices, total = book.est_prices(sheets)
|
||||
blk = next((b for b in book.est_blocks(sheets) if int(b["번"]) == int(no)), None)
|
||||
if blk is None or blk["번"] not in total:
|
||||
excel = _excel_of(path, saved)
|
||||
if int(no) not in fn.numbers(excel):
|
||||
raise store.StoreError(404, f"없는 단산{no}")
|
||||
return em.est(blk, est.parse_block(blk["글"], prices), total[blk["번"]], True, name)
|
||||
return fn.emit(excel, int(no))
|
||||
|
||||
|
||||
def _inner(
|
||||
source: Path, key: str, logic: dict, native: bool = False
|
||||
) -> list[tuple[dict, int | None, str | None]]:
|
||||
"""안쪽 산근 줄 `[(호표 줄, 같은 엑셀 안쪽 블록 번, 못 잇는 까닭)]` — STmate = 줄 코드가 블록 코드 ·
|
||||
EST = 줄 이름 「단산N」(N 은 총괄표에 있는 블록). 비목 하나만 부른 줄은 그 비목이 그대로(옮김 없음)일 때만."""
|
||||
from M02_MasterTemplete import M02_San_Book as book
|
||||
|
||||
saved = json.loads(_path(source, key).read_bytes())
|
||||
"""안쪽 산근 줄 `[(호표 줄, 같은 파일 안쪽 블록 번, 못 잇는 까닭)]` — 원파일 · 엑셀 둘 꼴 같은 길
|
||||
(`M02_Doc_FromNative.inner` · 줄 자원 코드 = 블록 코드)."""
|
||||
path = _path(source, key)
|
||||
saved = json.loads(path.read_bytes())
|
||||
if native and isinstance(saved.get("원파일"), dict):
|
||||
return fn.inner(saved["원파일"], logic)
|
||||
sheets = saved["시트"]
|
||||
stmate = book.kind_of(sheets) == "STmate"
|
||||
have = {b["코드"]: int(b["코드"][1:]) for b in book.stmate_blocks(sheets)} if stmate else {}
|
||||
total = set() if stmate else set(book.est_prices(sheets)[1])
|
||||
out = []
|
||||
for line in logic["호표"]:
|
||||
res = line.get("자원") or {}
|
||||
if res.get("갈래") != "단가산출":
|
||||
continue
|
||||
if stmate:
|
||||
n = have.get(str(res.get("코드")))
|
||||
else:
|
||||
hit = re.fullmatch(r"단산(\d+)", str(res.get("이름")))
|
||||
n = int(hit.group(1)) if hit and int(hit.group(1)) in total else None
|
||||
if n is None:
|
||||
out.append((line, None, "같은 엑셀에 안쪽 블록 없음"))
|
||||
elif res.get("옮김") or (res.get("꼴") == "EST" and res.get("열쇠비목") not in em.COMP):
|
||||
out.append((line, n, "비목이 바뀐 부름"))
|
||||
else:
|
||||
out.append((line, n, None))
|
||||
return out
|
||||
return fn.inner(_excel_of(path, saved), logic)
|
||||
|
||||
|
||||
def _link_inner(source: Path, key: str, no: int, out: dict, trail: tuple) -> None:
|
||||
|
||||
@@ -304,11 +304,15 @@ def same_part(doc: Any) -> dict | None:
|
||||
|
||||
|
||||
def unlinked(doc: dict) -> list[dict]:
|
||||
"""이음 없는 자원 줄(표지 · 사람이 고름) `[{자리, 이름}]`."""
|
||||
"""이음 없는 자원 줄(표지 · 사람이 고름) `[{자리, 이름, 까닭, 견적?}]`(까닭 = 814-1 `M02_San_Link.why` ·
|
||||
견적 = 815-1 K5 그대로 쓰는 견적 · 운임 · 수수료 줄 — 못 이음 수에서 뺌)."""
|
||||
from M02_MasterTemplete import M02_San_Link as link
|
||||
|
||||
row = row_of(doc) or {}
|
||||
ids = sd.line_ids(row)
|
||||
return [
|
||||
{"자리": ids[n], "이름": one.get("이름") or ""}
|
||||
{"자리": ids[n], "이름": one.get("이름") or "", "까닭": link.why(one)}
|
||||
| ({"견적": True} if (one.get("자원") or {}).get("갈래") == "고정" else {})
|
||||
for n, one in enumerate(doc[KEY]["로직"].get("호표") or [])
|
||||
if not one.get("이음") and not one.get("참조")
|
||||
]
|
||||
|
||||
@@ -1,88 +0,0 @@
|
||||
/* =============================================================================
|
||||
* M02_Sangeun_Match.ts
|
||||
* 가져온 식 ↔ 우리 M01 기초 연결 판정 띠 자료(PLAN 810-4 · 요구 707 · 709 · 703) — 원문 패널 카드 위.
|
||||
* SK `가져온.원본.연결후보`(서버 `M02_Doc_Match` 가 적음) → 띠 글 · 다른 항 · 단추 하나:
|
||||
* 확정 = [우리 기초 사본으로 바꾸기] · 근접 · 없음 = 단추 없음(703) · 안 찾음 = [연결 찾기] ·
|
||||
* 바꾼 SK(`가져온보관` 있음) = 「우리 기초 사본 ○○」 + [가져온 식으로 되돌리기]
|
||||
* 화면 · DOM · fetch 없음
|
||||
* ========================================================================== */
|
||||
|
||||
export type MatchVerdict = "확정" | "근접" | "없음";
|
||||
|
||||
export interface MatchFound {
|
||||
판정?: MatchVerdict;
|
||||
키?: string;
|
||||
이름?: string;
|
||||
옵션?: Record<string, string>;
|
||||
다른?: { 항: string; 글: string }[];
|
||||
금액?: { 가져온?: Record<string, number | null>; 우리?: Record<string, number | null> };
|
||||
후보수?: number;
|
||||
가까움?: { 키?: string; 이름?: string; 다른?: { 항: string; 글: string }[] };
|
||||
}
|
||||
|
||||
export type MatchAction = "찾기" | "바꾸기" | "되돌리기" | null;
|
||||
|
||||
export interface MatchBand {
|
||||
tone: "success" | "info" | "warning";
|
||||
text: string;
|
||||
/** 다른 항 글(근접 · 없음의 가까운 후보) */
|
||||
details: string[];
|
||||
action: MatchAction;
|
||||
}
|
||||
|
||||
interface SkLike {
|
||||
바탕?: { 로직?: string } | null;
|
||||
입력값?: Record<string, unknown>;
|
||||
가져온?: { 로직?: unknown; 원본?: { 연결후보?: MatchFound } } | null;
|
||||
가져온보관?: { 가져온?: { 원본?: { 연결후보?: MatchFound } } } | null;
|
||||
}
|
||||
|
||||
const opts = (o?: Record<string, unknown>): string => {
|
||||
const list = Object.entries(o ?? {}).map(([k, v]) => `${k} ${String(v)}`);
|
||||
return list.length ? `(${list.join(" · ")})` : "(기본 옵션)";
|
||||
};
|
||||
const lines = (d?: { 항: string; 글: string }[]): string[] =>
|
||||
(d ?? []).map((x) => `${x.항} — ${x.글}`);
|
||||
|
||||
/** SK → 띠 자료 · 가져온 식도 · 바꾼 SK 도 아니면 null */
|
||||
export function matchBand(sk: SkLike | null | undefined): MatchBand | null {
|
||||
const kept = sk?.가져온보관;
|
||||
if (kept && sk?.바탕?.로직) {
|
||||
return {
|
||||
tone: "info",
|
||||
text: `우리 기초 사본 ${sk.바탕.로직} ${opts(sk.입력값)} — 원래 가져온 식은 보관`,
|
||||
details: [],
|
||||
action: "되돌리기",
|
||||
};
|
||||
}
|
||||
if (!sk?.가져온?.로직) return null;
|
||||
const f = sk.가져온.원본?.연결후보;
|
||||
if (!f?.판정) {
|
||||
return { tone: "info", text: "우리 기초와 연결을 아직 안 찾음", details: [], action: "찾기" };
|
||||
}
|
||||
const base = `${f.키 ?? ""} ${f.이름 ?? ""} ${opts(f.옵션)}`.trim();
|
||||
if (f.판정 === "확정") {
|
||||
return {
|
||||
tone: "success",
|
||||
text: `우리 기초 ${base} 와 확정 일치`,
|
||||
details: [],
|
||||
action: "바꾸기",
|
||||
};
|
||||
}
|
||||
if (f.판정 === "근접") {
|
||||
const kinds = [...new Set((f.다른 ?? []).map((x) => x.항))].join(" · ");
|
||||
return {
|
||||
tone: "warning",
|
||||
text: `근접 — ${base} · 다른 항: ${kinds}`,
|
||||
details: lines(f.다른),
|
||||
action: null,
|
||||
};
|
||||
}
|
||||
const near = f.가까움;
|
||||
return {
|
||||
tone: "info",
|
||||
text: `없음 — 우리 기초 후보 ${f.후보수 ?? 0} 개와 맞지 않음`,
|
||||
details: near ? [`가장 가까운 ${near.키 ?? ""} ${near.이름 ?? ""}`, ...lines(near.다른)] : [],
|
||||
action: null,
|
||||
};
|
||||
}
|
||||
@@ -42,6 +42,8 @@ export interface Origin {
|
||||
요약?: string;
|
||||
/** 원파일 확정 「확정 N · 표지 N · 추정 N · 확정률 N %」 */
|
||||
확정?: string;
|
||||
/** 확정 칸 이름 — 원파일 = 「원파일 확정」 · 엑셀 = 「엑셀 읽기 확정」(814-1) */
|
||||
확정이름?: string;
|
||||
}
|
||||
|
||||
const BOOKS: [RegExp, string][] = [
|
||||
@@ -111,6 +113,7 @@ export function originOf(meta: OriginMeta | null | undefined): Origin | null {
|
||||
...(sure
|
||||
? {
|
||||
확정: `확정 ${sure.확정 ?? 0} · 표지 ${sure.표지 ?? 0} · 추정 ${sure.추정 ?? 0} · 확정률 ${(Math.floor(rate * 1000) / 10).toFixed(1)} %`,
|
||||
확정이름: src.원파일 ? "원파일 확정" : "엑셀 읽기 확정",
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
|
||||
@@ -99,6 +99,12 @@ export function priceSym(line: MoneyLine): string | null {
|
||||
}
|
||||
|
||||
/** 금액 줄 하나만 단가로 쓰는 연결 기호(요소 · 단가산출) — {줄 차례: 기호} */
|
||||
/** 가져온 기계 줄 단가 기호(812-9 `가져온.기계`) — 못 이어 숫자 연결이어도 그 줄 기계(이름 단추 · 고르기) */
|
||||
export function machineSyms(doc: FormulaDoc): Set<string> {
|
||||
const got = (doc as { 가져온?: { 기계?: { 기호?: string }[] } }).가져온?.기계;
|
||||
return new Set((Array.isArray(got) ? got : []).map((x) => String(x?.기호 ?? "")));
|
||||
}
|
||||
|
||||
export function ownedOf(doc: FormulaDoc): Map<number, string> {
|
||||
const count = new Map<string, number>();
|
||||
const bump = (ns: string[]): void => ns.forEach((n) => count.set(n, (count.get(n) ?? 0) + 1));
|
||||
@@ -107,12 +113,13 @@ export function ownedOf(doc: FormulaDoc): Map<number, string> {
|
||||
if (doc.결과) bump([doc.결과]);
|
||||
const vars = new Map((doc.변수 ?? []).map((v) => [varId(v), v]));
|
||||
const lines = new Set((doc.식 ?? []).map((l) => String(l.기호 ?? "")));
|
||||
const mach = machineSyms(doc);
|
||||
const out = new Map<number, string>();
|
||||
(doc.금액 ?? []).forEach((l, i) => {
|
||||
const s = priceSym(l);
|
||||
const kind = s ? vars.get(s)?.연결?.갈래 : undefined;
|
||||
if (s && (kind === "요소" || kind === "단가산출") && count.get(s) === 1 && !lines.has(s))
|
||||
out.set(i, s);
|
||||
const mine = kind === "요소" || kind === "단가산출" || (!!s && mach.has(s));
|
||||
if (s && mine && count.get(s) === 1 && !lines.has(s)) out.set(i, s);
|
||||
});
|
||||
return out;
|
||||
}
|
||||
@@ -137,6 +144,7 @@ export function kindOf(
|
||||
const lb = link.키 ? String(link.키).startsWith("LB") : line.비목 === "노무비"; // 고르기 전 = 비목으로
|
||||
return lb ? "인력" : "재료";
|
||||
}
|
||||
if (s && machineSyms(doc).has(s)) return "기계"; // 못 이은 가져온 기계 줄(812-9)
|
||||
return "율";
|
||||
}
|
||||
|
||||
|
||||
@@ -138,6 +138,11 @@ export function repick(doc: FormulaDoc, i: number, id: string, kind: MoneyKind,
|
||||
const now = linkOf(kind, p);
|
||||
if (now.갈래 === "요소" && was.지역) now.지역 = was.지역; // 지역 옵션 이음 그대로
|
||||
if (now.갈래 === "단가산출" && was.키 === now.키) now.입력 = { ...was.입력, ...now.입력 }; // 같은 로직 = 다른 넘길 입력 그대로
|
||||
// 가져온 기계 줄(812-9) — 엑셀 값 기억 그대로(엑셀 단가 기준 금액 안 바뀜 · 못 이은 기계 = 숫자 값 · 묶음)
|
||||
const kept = (was as { 엑셀?: unknown }).엑셀 ?? (was.갈래 === "숫자" ? was.값 : undefined);
|
||||
if (kind === "기계" && kept !== undefined) (now as { 엑셀?: unknown }).엑셀 = kept;
|
||||
const cost = (line as { 비목?: string }).비목 ?? "";
|
||||
if (kind === "기계" && ["노무비", "재료비", "경비"].includes(cost)) now.칸 = cost; // 비목 한 칸 줄 = 그 칸
|
||||
v.연결 = now;
|
||||
v.이름 = p.이름;
|
||||
line.이름 = p.이름;
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"token": "ya29.a0AX07Cms_Mr_cIxMqI7d3tZp5yvLwc557onouF07rOxMcSChG2hhLwughRmStGGgViEQK7Wf81_mW-u6W6y7_SPMnXy2BPI0KjSpBd8W9c6rELzIgW7w-kUBfOqDMaa-Gc0htcHUTISoPtNbLDbUMFNrmKXlVqkp80DM-YKlQc7pSnM2XMHqjzOzQXC5NsJAaQ0gd4HIaCgYKAawSARMSFQHGX2MijTO-loPa-7a97WycOEat1A0206", "refresh_token": "1//0eA6qtRCSTkERCgYIARAAGA4SNwF-L9Ir5SgGy2MpsHmXX1uNa8hJc_-i_zkyDdRh2-TuplZyeayJKwIegMpeWLjvwtl-I-9RTgA", "token_uri": "https://oauth2.googleapis.com/token", "client_id": "222243854357-eg5aquofa0epa3ri78k5ngpfclroc7rg.apps.googleusercontent.com", "client_secret": "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", "scopes": ["https://www.googleapis.com/auth/spreadsheets", "https://www.googleapis.com/auth/script.projects"], "universe_domain": "googleapis.com", "account": "", "expiry": "2026-10-12T08:55:12Z"}
|
||||
{"token": "ya29.a0AehfTOlu7Fq0d_Zdgt-0GfZcA2S0671qpvV4n8cNlaKptSyctvQAAACvLpNkr-TISeBg67hmsCDHMjfztwpLUTokWo-AGoFhwK00SKCQ0btNAlDGr21fslVUAVWzep33843KvYILyP_6uFg_2I4sOoDEd4qn5ufRHd44RqJXVwRKOZSPQsxgHlu_mB_-OjHEa6GkLtEaCgYKAfoSARMSFQHGX2Mi0cQPJxIBf5cUoDp4_2LWOg0206", "refresh_token": "1//0eG7w85ZCkaY1CgYIARAAGA4SNwF-L9IriAMPwI3fHOObs-f7m62qLNtFB3yiHcTmfnP675RXTOY5cvwTqLqN7UbEo60fubuy300", "token_uri": "https://oauth2.googleapis.com/token", "client_id": "222243854357-eg5aquofa0epa3ri78k5ngpfclroc7rg.apps.googleusercontent.com", "client_secret": "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", "scopes": ["https://www.googleapis.com/auth/spreadsheets", "https://www.googleapis.com/auth/script.projects"], "universe_domain": "googleapis.com", "account": "", "expiry": "2026-11-11T13:24:03Z"}
|
||||
@@ -0,0 +1,101 @@
|
||||
행 가져온 이름 가져온 규격 쓰임 기대 우리 키 우리 이름 우리 규격 확신
|
||||
1 접착제 11 후보 사람이 고름 — 틀린 줄에 자동으로 잇지 않음
|
||||
2 패널(유로폼) 600*1200mm 10 이음 MT000467 유로폼패널 600×1200㎜ 표기만 다름
|
||||
3 각재 외송 8 이음 MT000393 각 재(외송) 30×30×3600mm 눈으로 추정
|
||||
4 요소 8 이음 MT006763 요 소 N:46% 눈으로 추정
|
||||
5 내부코너패널(유로폼) (100x200)x1200mm 7 이음 MT000470 내부코너패널 100×200×1200㎜ 표기만 다름
|
||||
6 결속선(철선) #20 , 0.9M/M 7 후보 사람이 고름 — 틀린 줄에 자동으로 잇지 않음
|
||||
7 각재 (외송) 6 이음 MT000393 각 재(외송) 30×30×3600mm 눈으로 추정
|
||||
8 파형강관 별도계상 6 잇지 않음 별도계상
|
||||
9 금강사 5 잇지 않음 우리 표에 없음
|
||||
10 고무테이프 0.5mm×19mm×10m 5 이음 MT009202 절연 면고무테이프 0.5mm×19mm×10m 눈으로 추정
|
||||
11 낙석방지책 H2500×W2000(표준) 5 후보 사람이 고름 — 틀린 줄에 자동으로 잇지 않음
|
||||
12 낙석방지책 H2500×W2000(단부) 5 후보 사람이 고름 — 틀린 줄에 자동으로 잇지 않음
|
||||
13 종자 초본류 5 후보 1 순위 MT006646 종 자 초본위주형 좁힘(자동 이음 아님)
|
||||
14 종자 목본류 5 후보 1 순위 MT006648 종 자 목본군락형 좁힘(자동 이음 아님)
|
||||
15 어닐링철선(결속선) #20 0.9mm 200.4m/kg 5 이음 MT000209 어닐링철선(결속선),서울 #20, 0.9mm 표기만 다름
|
||||
16 내수합판1급 12mmx1220x2440 4 이음 MT000018 합판(내수), 서울 12t×1220×2440mm 표기만 다름
|
||||
17 어닐링철선(결속선) #20 0.9M/M 4 이음 MT000209 어닐링철선(결속선),서울 #20, 0.9mm 표기만 다름
|
||||
18 유로폼(패널) 600*1200 4 이음 MT000467 유로폼패널 600×1200㎜ 표기만 다름
|
||||
19 내부코너패널 (200+200)*1200 4 이음 MT000472 내부코너패널 (200+200)×1200㎜ 표기만 다름
|
||||
20 합판(내수) 12×1220×2440 4 이음 MT000018 합판(내수), 서울 12t×1220×2440mm 표기만 다름
|
||||
21 조임철물 (직교자재) 4 이음 MT000499 조임철물,서울 직교,자재 표기만 다름
|
||||
22 에폭시수지 YD-128 4 후보 사람이 고름 — 틀린 줄에 자동으로 잇지 않음
|
||||
23 종자 생태복원형,초본류 3 후보 1 순위 MT006646 종 자 초본위주형 좁힘(자동 이음 아님)
|
||||
24 종자 생태복원형,목본류 3 후보 1 순위 MT006648 종 자 목본군락형 좁힘(자동 이음 아님)
|
||||
25 내부코너패널(유로폼) (100+200)*1200mm 3 이음 MT000470 내부코너패널 100×200×1200㎜ 표기만 다름
|
||||
26 유로폼(패널) 600×1200 3 이음 MT000467 유로폼패널 600×1200㎜ 표기만 다름
|
||||
27 콘테이너하우스 창고용(3.0m×6.0m×2.6m) 3 이음 MT007111 콘테이너(창고) 6.0*3.0*2.6m 표기만 다름
|
||||
28 전기뇌관(MS) 3.5m×500개(하이데토) 2 이음 MT005677 전기뇌관 3.5m 눈으로 추정
|
||||
29 BUTTON BIT Ø38×76㎜ 2 후보 사람이 고름 — 틀린 줄에 자동으로 잇지 않음
|
||||
30 유압드릴샹크 T38×CM470 2 후보 사람이 고름 — 틀린 줄에 자동으로 잇지 않음
|
||||
31 유압식드릴스리브 T38×190mm 2 이음 MT005687 유압드릴스리브 T38×190mm 표기만 다름
|
||||
32 미송(판재) 2 후보 사람이 고름 — 틀린 줄에 자동으로 잇지 않음
|
||||
33 산림종자 참싸리(박피) 2 이음 MT006664 산림종자 참싸리(풍화암,강마사토) 눈으로 추정
|
||||
34 유기질퇴비 2 후보 사람이 고름 — 틀린 줄에 자동으로 잇지 않음
|
||||
35 PVC지수판 200x5T KSM3805 2 이음 MT006544 PVC지수판 200×5T 표기만 다름
|
||||
36 PVC용접봉 Ø3.2 (국산) 2 후보 사람이 고름 — 틀린 줄에 자동으로 잇지 않음
|
||||
37 PVC파이프(일반용VG2) 50㎜ × 4m 2 이음 MT002926 PVC파이프(일반관),서울 D=50mm(VG2) 표기만 다름
|
||||
38 혼합골재 영월 2 후보 사람이 고름 — 틀린 줄에 자동으로 잇지 않음
|
||||
39 이형철근 별도계상 2 잇지 않음 별도계상
|
||||
40 안전표지판 삼각,D=900 2 이음 MT003202 교통안전표지판 900,삼각(고휘도) 눈으로 추정
|
||||
41 표지판지주 D=76.3mm, 3.5M 2 후보 사람이 고름 — 틀린 줄에 자동으로 잇지 않음
|
||||
42 블레이드(커터) "d=320 400mm 14""xt3.2" 2 이음 MT005749 블레이드 "14""×3.2mm" 표기만 다름
|
||||
43 떼 2 잇지 않음 우리 표에 없음
|
||||
44 강관비계 φ48.6m/m×2.3mm 2 이음 MT000481 강관비계,서울 Φ48.6*2.3*4.0m 표기만 다름
|
||||
45 철물 2 후보 사람이 고름 — 틀린 줄에 자동으로 잇지 않음
|
||||
46 각재(말뚝) Φ60 2 후보 사람이 고름 — 틀린 줄에 자동으로 잇지 않음
|
||||
47 강관써포트 V4 2600-4000mm 14.3kg 2 이음 MT000487 강관동바리,서울 Φ48.6*2.4,L=2.6~4.0 눈으로 추정
|
||||
48 안고초 2 이음 MT006667 산림종자 안고초 표기만 다름
|
||||
49 비수리 2 이음 MT006665 산림종자 비수리 표기만 다름
|
||||
50 내수합판1급 12m/m 1220×2440 2 이음 MT000018 합판(내수), 서울 12t×1220×2440mm 표기만 다름
|
||||
51 각재(외송) 3.6×3.0×3.0 2 이음 MT000393 각 재(외송) 30×30×3600mm 눈으로 추정
|
||||
52 내수합판1급 12mm 1220×2440 2 이음 MT000018 합판(내수), 서울 12t×1220×2440mm 표기만 다름
|
||||
53 각재(거푸집용,외송) 3.6×3.0×3.0 2 이음 MT000393 각 재(외송) 30×30×3600mm 눈으로 추정
|
||||
54 다이아몬드톱날 "Ø14""×3.2t(콘크리트용)" 2 이음 MT005749 블레이드 "14""×3.2mm" 눈으로 추정
|
||||
55 아세틸렌 용접용 2 이음 MT003163 아세틸렌 98% (용접용) 표기만 다름
|
||||
56 산소 고압가스 2 이음 MT003166 산 소 고압상태 눈으로 추정
|
||||
57 신나 KSM6060 2종(조합페인트류) 2 이음 MT003398 신너(조합페인트용) KSM-6060(2종) 표기만 다름
|
||||
58 초류종자(국내산) 참싸리,비수리,낭아초등 1 후보 사람이 고름 — 틀린 줄에 자동으로 잇지 않음
|
||||
59 종 자 초본형 1 후보 1 순위 MT006646 종 자 초본위주형 좁힘(자동 이음 아님)
|
||||
60 소나무우드칩(멀칭재) 1 후보 사람이 고름 — 틀린 줄에 자동으로 잇지 않음
|
||||
61 3단코어비트 "5""(125mm)" 1 후보 사람이 고름 — 틀린 줄에 자동으로 잇지 않음
|
||||
62 P V C PIPE Φ50M/M,VG1 1 이음 MT002861 PVC파이프(일반관),서울 D=50mm(VG1) 표기만 다름
|
||||
63 콘테이너(창 고) 3×6×2.6 1 이음 MT007111 콘테이너(창고) 6.0*3.0*2.6m 표기만 다름
|
||||
64 강관비계 48.6mm×2.3mm×6m 1 후보 사람이 고름 — 틀린 줄에 자동으로 잇지 않음
|
||||
65 철물 직교자재 1 이음 MT000499 조임철물,서울 직교,자재 눈으로 추정
|
||||
66 문양거푸집(ABS코팅품) 600×1200 1 이음 MT000453 문양거푸집(플라스틱 ABS코팅폼) 600×1200 표기만 다름
|
||||
67 와이어매쉬 #6 100×100 1 이음 MT007130 와이어메쉬(용접철망)관급 #6, 100×100 표기만 다름
|
||||
68 지수판 T5×200 1 이음 MT006544 PVC지수판 200×5T 표기만 다름
|
||||
69 용접봉(연강용) 3.2m/m 1 후보 사람이 고름 — 틀린 줄에 자동으로 잇지 않음
|
||||
70 어닐링철선 (결속선) #6 4.0mm 10.1m/kg 1 후보 사람이 고름 — 틀린 줄에 자동으로 잇지 않음
|
||||
71 구역화물자동차(4.5Ton) 80km이내 1 이음 MT023493 구역화물 4.5톤, 80km 표기만 다름
|
||||
72 구역화물자동차(4.5Ton) 60km이내 1 이음 MT023491 구역화물 4.5톤, 60km 표기만 다름
|
||||
73 산소(6000ℓ) 99%,120kg/㎠ 1 이음 MT003165 산 소 6000ℓ/병 표기만 다름
|
||||
74 아세칠렌 AC 98%,용접용 1 이음 MT003163 아세틸렌 98% (용접용) 표기만 다름
|
||||
75 서비스볼트(고장력볼트) M20×60 1 이음 MT002536 고장력볼트 M20*60mm 표기만 다름
|
||||
76 보조강재(열연강판) T=9-12 1 후보 사람이 고름 — 틀린 줄에 자동으로 잇지 않음
|
||||
77 구역화물자동차(8Ton) 10km이내 1 이음 MT023654 구역화물 8.0톤, 10km 표기만 다름
|
||||
78 구역화물자동차(8Ton) 20km이내 1 이음 MT023655 구역화물 8.0톤, 20km 표기만 다름
|
||||
79 참싸리(박피) 1 이음 MT006664 산림종자 참싸리(풍화암,강마사토) 눈으로 추정
|
||||
80 부산물비료 20kg/포 1 후보 사람이 고름 — 틀린 줄에 자동으로 잇지 않음
|
||||
81 참싸리 박피 1 이음 MT006664 산림종자 참싸리(풍화암,강마사토) 눈으로 추정
|
||||
82 복합비료 21-17-17 20kg/포 1 이음 MT006490 복합비료 21-17-17 표기만 다름
|
||||
83 퇴비(천연부엽토) 20㎏/포 1 이음 MT006788 천연부엽토 20kg/포 눈으로 추정
|
||||
84 문양거푸집(ABS코팅폼) 600×1200×20~30mm 1 이음 MT000453 문양거푸집(플라스틱 ABS코팅폼) 600×1200 표기만 다름
|
||||
85 P.V.C PIPE VG1(50mm) 1 이음 MT002861 PVC파이프(일반관),서울 D=50mm(VG1) 표기만 다름
|
||||
86 다웰바(DOWEL BAR) Ø32×1,000 1 이음 MT006540 다 웰 바 Φ32*1000 표기만 다름
|
||||
87 P.V.C PIPE VG1(35mm) 1 이음 MT002859 PVC파이프(일반관),서울 D=35mm(VG1) 표기만 다름
|
||||
88 휠라 T=15mm,1000×1000mm 1 후보 사람이 고름 — 틀린 줄에 자동으로 잇지 않음
|
||||
89 측구수로관 300×300×2000×120,앵글포함 1 이음 MT015883 측구수로관(앵글부착) 300×300×120 표기만 다름
|
||||
90 중하중그레이팅 395×995×52 1 이음 MT007025 스틸그레이팅(중하중용) 395×995×52 표기만 다름
|
||||
91 측구수로관 500×500×2000×150,앵글포함 1 이음 MT015887 측구수로관(앵글부착) 500×500×150 표기만 다름
|
||||
92 중하중그레이팅 595×995×67 1 이음 MT007032 스틸그레이팅(중하중용) 595×995×67 표기만 다름
|
||||
93 잔디 0.18×0.18×0.01 1 후보 사람이 고름 — 틀린 줄에 자동으로 잇지 않음
|
||||
94 강관비계 4000×48.6mm×2.3mm 1 이음 MT000481 강관비계,서울 Φ48.6*2.3*4.0m 표기만 다름
|
||||
95 삼각표지판 삼각,900mm 1 이음 MT003202 교통안전표지판 900,삼각(고휘도) 눈으로 추정
|
||||
96 표지판지주 D=76.3mm×3.0m 1 이음 MT003241 표지판지주 Φ76.3×3000 표기만 다름
|
||||
97 공사안내표지판 300×1500 1 후보 사람이 고름 — 틀린 줄에 자동으로 잇지 않음
|
||||
98 공사안전PE드럼 500×800 1 이음 MT003311 PE드럼(일체형) Φ500×800 표기만 다름
|
||||
99 윙카호스 10cm 1 후보 사람이 고름 — 틀린 줄에 자동으로 잇지 않음
|
||||
100 조합페인트 KSM6020 2급,백색 1 이음 MT003372 조합페인트 KSM-6020 2급(백색) 표기만 다름
|
||||
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,224 @@
|
||||
# 불러오기 방향 장부 (요구 743 ~) — 결정만 쌓음 · 코드는 한 번에
|
||||
|
||||
## 0. 기준(요구 745)
|
||||
|
||||
- 불러오기의 목적 = 우리 단가산출 기초를 만드는 것이 아님. **경쟁사 단가산출을 우리 인력 · 자재 · 기계 · 다른 단가산출 정보로 다시 만들어 내는 것**(우리 기초와 같은 꼴로).
|
||||
- 그러므로 잣대 = 「우리 특정 기초와 같은가」가 아니라 **「우리 정보로 만들어 낼 수 있는가」** = 블록의 모든 줄이 우리 요소(노임 · 자재 · 기계 · 유가 · 환율 · 형제 단가산출 · 요율표)에 이어져 [우리 단가로] 셈이 되는가.
|
||||
- 연결 찾기(우리 기초와 맞대기)는 보조 — 확정이면 [바꾸기]로 우리 기초 사본을 쓸 수 있을 뿐, 「없음」이 흠은 아님.
|
||||
- 그래서 앞으로 재는 수 = 요소 이음률(지금 인력 100 · 기계 90.8 · 재료 72.0 · 유가 0 · 형제 100 %) · [우리 단가로] 전부 우리 값(원파일 71.9 · 엑셀 74.6 %) · 못 이은 줄의 까닭별 수.
|
||||
|
||||
기준 = 812-12 정합성 보고(`_보고_812-12_불러오기정합성.md`) · 아티팩트 네 묶음(정합성 · 연결 · 흠 16 · 정할 것 4). 줄마다 상태 = 결정 · 협의 중 · 조사 필요.
|
||||
|
||||
## A. 올리기 · 표현
|
||||
|
||||
| # | 항목 | 방향 | 상태 | 근거 |
|
||||
|---|---|---|---|---|
|
||||
| A1 | 올리는 파일 | 하나만(엑셀 또는 경쟁사 확장자) · 「같은 공사 원파일 우선 붙이기」 걷음 | 결정 | 요구 742-1 · 743-1 |
|
||||
| A2 | 확장자 파일(.estx · .STC)의 표현 | 단가산출표(산출근거 머리 목록) + 산출근거만 · 일위대가 제외 | 결정 | 요구 743-2 · 흠 5 |
|
||||
| A3 | 엑셀 단가산출 이름의 규격 · 단위 꼬리 | 보이는 이름은 그대로 · 우리 기초 연결 때만 꼬리 뗀 이름으로 맞댐 | 결정 | 요구 743-3 · 흠 9 |
|
||||
|
||||
## B. 읽기 흠(심각)
|
||||
|
||||
| # | 항목 | 방향 | 상태 | 근거 |
|
||||
|---|---|---|---|---|
|
||||
| B1 | STmate 기계 비목 세 줄(노 무 비 · 재 료 비 · 경 비) | 기계 한 줄로 묶음 · 조합이면 묶은 뒤 기계마다 그룹 · 금액 그대로 | 결정 | 요구 744 · 흠 1 · 285 블록 |
|
||||
| B2 | EST 덤프 「노무비 · 재료비 · 경비」 세 줄 + 조합 엇갈림 | 세 줄 묶은 뒤 조합 가름 | 결정 | 요구 744 · 흠 2 · 83 블록 |
|
||||
| B3 | 시간당 경비 계산식이 안 붙는 기계 26 종(대형 브레이커 · 부착용 집게 · 자동덮개시설 · 진동파일 해머 · 로더(타이어) 1.72 …) | 조사(요구 746) = 품셈 원문에 다 있음(손료 산정표 8-3-1 · 8-5-1 에 그 기계 · 8-4-8 주⑤ 부착 장비 조합 셈법) · 우리 기계표 630 대 모두 자료 있음 · 빠진 것 = 시간당 경비 계산식(GC000267~276)의 「기계 목록」이 246 대만 담음 → 384 대(집재장비 · 스크린 · 엔진 · 준설선 · 크러셔 · 부착 장비 …)가 어느 식에도 없음 · 26 종은 그중 경쟁사 파일에 쓰인 것 → 안: 계산식 기계 목록을 기계표 전체로(자료에 따라 식 가족 자동 — 연료 · 조종원 있음 = 기계경비 식 · 없음 = 손료 식) · 이름으로 이음 · 굴착기 + 브레이커 · + 집게는 우리 조합 식(GC000270 · 271) | 결정 — 원문에 있는데 빠진 기계만 계산식 목록에 더함(지어 넣는 자료 0) | 요구 744 · 745 · 746 · 747 · 흠 4 |
|
||||
| B4 | M02 단가산출 목록 65~75 초 | 나중에 화면 속도와 한 번에 | 보류(요구 749) | 흠 3(불러오기 밖 · 화면) |
|
||||
|
||||
## C. 연결 찾기
|
||||
|
||||
| # | 항목 | 방향 | 상태 | 근거 |
|
||||
|---|---|---|---|---|
|
||||
| C1 | 공구손료 율의 비목(실무 재료비 ↔ 우리 경비) | 값(참고) · 확정 · [바꾸기] 하면 우리 비목(경비)으로 바꿈 | 결정 | 요구 744 · 정할 것 1 |
|
||||
| C2 | 「없음」 블록의 처리 | 기준 0 으로 봄 — 「품셈 밖」 표지 안 함 · 우리 기초와 맞추지 않음 · 그 블록의 줄이 우리 요소로 다 이어지면 「만들어짐」 · 못 이은 줄만 까닭(자재 없음 · 운임표 없음 · 지자체 품 …)을 표시 · 조합 묶음은 안의 참조로 | 결정 | 요구 745 |
|
||||
| C3 | 후보 고르기(이름 · 절 근거 먼저 · 덧말 뺀 이름 · 엉뚱한 근접 막기) | — | 협의 중 | 흠 6 · 7 · 8 |
|
||||
| C4-유가 | 유가 · 전력 29 줄이 전부 「후보 여럿」 | 조사(요구 747) = 이름은 다 맞음(경유 → 자동차용경유 · 등유 → 실내등유 · 전력 → 전력량요금) · 우리 표가 유종마다 전국평균 + 전국 + 16 지역 = 18 줄 · 전력은 계약종별 2 줄이라 「하나일 때만 잇는」 규칙에 늘 걸림 → 안: 유가 = 전국평균 줄에 잇고 지역은 문서 지역 옵션으로(우리 기초와 같은 꼴) · 전력 = 우리 기초가 쓰는 기본 계약종별에 잇고 계약종별 옵션 · 엑셀 값은 검산용으로 기억 | 결정(요구 748) — 유가 = 전국(우리 기초 기본값과 같은 「전국평균」 줄 · 지역은 옵션 · 사용자가 나중에 바꿈) · 전력 = 「일반용(갑)Ⅰ 저압」 기본(공사용 전력 · 주택용 아님) · 계약종별 옵션 · 엑셀 값은 검산용 | 흠 10 |
|
||||
| C4-재료 | 재료 후보 여럿(EST 원파일 자재 쓰임 1,553 중 289 · 53 종) | 조사(요구 748) = 53 종 가운데 50 종(쓰임 283)은 후보 다섯 안에 **이름 · 규격이 정확히 같은 줄**이 있음(유리알 1호 고휘도 · 합판(내수) 12t · 철못 N 25 · 구역화물 8.0톤 30km · 급수호스 2"×10m · 잔디 0.3 …) — 우리 자재표가 이름 뒤에 「, 서울」 같은 지역 꼬리를 달거나 색 · 규격이 다른 형제를 같이 내놓아 「하나」가 못 됨 · 나머지 3 종(고무테이프 · 초류종자 · PVC D=100)만 진짜 없음 → 안: 지역 꼬리 뗀 뒤 이름 · 규격이 정확히 같은 줄이 있으면 그것으로 잇고 지역은 옵션(유가와 같은 꼴) · 정확 일치가 없을 때만 「사람 확인」 | 결정(요구 749) | 흠 10 |
|
||||
|
||||
## D. 가벼움 · 도구
|
||||
|
||||
| # | 항목 | 방향 | 상태 | 근거 |
|
||||
|---|---|---|---|---|
|
||||
| D1 | 조합 첫 줄 기계 칩 글 · 소계 검산 표지 · 찾기 없는 기계 줄 7 · 올린 직후 409 · 엑셀 블록 「원파일 확정」 글 · 전수 도우미 str() | 코드 한 번에 할 때 같이 | 결정(묶음) | 흠 11 ~ 16 |
|
||||
|
||||
## E. 수정 사항 정리(코드 한 번에 할 때의 목록 · 요구 749)
|
||||
|
||||
| # | 고칠 것 | 근거 |
|
||||
|---|---|---|
|
||||
| 1 | 올리기 = 파일 하나(엑셀 또는 확장자) · 「같은 공사 원파일 우선 붙이기」 걷음 | A1 |
|
||||
| 2 | 확장자 파일(.estx · .STC) = 산출근거 머리로 단가산출표 판 + 산출근거 · 일위대가 안 만듦 | A2 |
|
||||
| 3 | 연결 찾기 · 요소 잇기 때 엑셀 이름의 규격 · 단위 꼬리 뗀 이름으로 맞댐(보이는 이름 그대로) | A3 |
|
||||
| 4 | STmate X 코드 L · M · S 세 줄 · EST 「〇〇 노무비 · 재료비 · 경비」 세 줄 = 기계 한 줄로 묶음 → 조합이면 그 뒤 기계마다 그룹 | B1 · B2 |
|
||||
| 5 | 시간당 경비 계산식(GC000267~276)의 기계 목록에 원문에 있는데 빠진 기계 더함(자료에 따라 기계경비 식 / 손료 식) · 조합(굴착기 + 브레이커 · + 집게)은 우리 조합 식 | B3 |
|
||||
| 6 | 공구손료 율의 비목 차이 = 값(참고) · [바꾸기] 때 우리 비목 | C1 |
|
||||
| 7 | 「없음」 = 흠 아님 · 「품셈 밖」 표지 없음 · 못 이은 줄만 까닭 표시 · 조합 묶음은 안의 참조로 · 잣대 = 요소 이음률 · [우리 단가로] | C2 · 기준 0 |
|
||||
| 8 | 유가 = 전국평균 줄 + 지역 옵션 · 전력 = 일반용(갑)Ⅰ 저압 + 계약종별 옵션 | C4-유가 |
|
||||
| 9 | 재료 = 지역 꼬리 뗀 이름 · 규격 정확 일치면 잇기 + 지역 옵션 · 없을 때만 사람 확인 | C4-재료 |
|
||||
| 10 | 가벼움 묶음 — 조합 첫 줄 칩 글 · 소계 검산 표지 · 찾기 없는 기계 줄 7 · 올린 직후 409 · 엑셀 블록 「원파일 확정」 글 · 전수 도우미 str() | D1 |
|
||||
| 뒤 | 화면 속도(목록 70 초) · 후보 고르기(흠 6 · 7 · 8) | B4 보류 · C3 협의 중 |
|
||||
|
||||
## F. 예상 정합성(요구 749 · 어림 — 812-12 수치에 결정 사항을 얹어 셈)
|
||||
|
||||
| 잣대 | 지금 | 뒤(어림) | 셈 근거 |
|
||||
|---|---|---|---|
|
||||
| 금액 · 줄 읽기 · 차림 왕복 | 100 % | 100 % | 그대로 |
|
||||
| 좌측 차림 조합 엇갈림 | 83 블록 흠 | 0 | #4 |
|
||||
| 인력 · 형제 단가산출 이음 | 100 % | 100 % | 그대로 |
|
||||
| 기계 이음 | 90.8 %(5,699 / 6,279) | 96 ~ 99 % | #5 로 349 줄 + · 별칭 승인 65 · 후보 여럿 88 은 사용자 고름 · 표에 없음 71 남음 |
|
||||
| 재료 이음 | 72.0 %(1,466 / 2,036) | 86 ~ 98 % | #9 — EST 확인분 283 줄 확실(86 %) · 후보 여럿 556 줄 대부분 같은 꼴이면 98 % |
|
||||
| 유가 이음 | 0 %(0 / 29) | 100 % | #8 |
|
||||
| [우리 단가로] 블록 전부 우리 값(= 우리 정보로 만들어짐) | 원파일 71.9 % · 엑셀 74.6 % | 88 ~ 93 % | 0 원 줄 252 + 177 (#4 · #5) · 재료 · 유가(#8 · #9) 풀림 · 남는 것 = 우리 자재 값 0(88 줄) · 표에 없는 기계 71 · 별칭 대기 · 견적 운임 |
|
||||
| 연결 찾기 확정(보조) | 15.3 %(339) | 25 ~ 30 % | #4 로 「없음」 중 기계 세 줄 탓 24 %(어림) 풀림 · #6 공구손료 · #3 꼬리 18 · 「없음」 818 중 품셈 밖 38 % 는 그대로 |
|
||||
|
||||
## G. 814-1 뒤 협의(요구 753 · 754)
|
||||
|
||||
| # | 항목 | 방향 | 상태 | 근거 |
|
||||
|---|---|---|---|---|
|
||||
| G1 | 기초 찾기(연결 찾기) 전체 | **걷음** — 요소를 조합해 만들기로 했고 값 업데이트도 되므로 후보 · [우리 기초 사본으로 바꾸기]까지 의미 없음 · 찾는 데 자원만 듦 → 코드 한 번에 할 때 판정 띠 · 바꾸기 · 되돌리기 · 연결후보 칸 · 전수 도우미를 걷음(810-4 · 812-2 · 812-4 · 812-11 몫) | 결정 | 요구 762-1 · 763-1 |
|
||||
| G2 | 재료 후보 여럿 252 줄(105 종) | ① 표기만 다른 것(결속선 · 유로폼 패널 · 낙석방지책) = 표기 맞추기 넓힘 ② 규격이 비어 못 정하는 것(접착제 · 요소 · 각재 외송) = 사용자가 고르면 별칭으로 기억(기계와 같은 길) ③ 자재표에 없음(금강사 등 14 줄) = 자재표 보강 후보 목록 · G1 보다 먼저 | 협의 중(브레인 안) | 요구 753 |
|
||||
| G3 | 재료 지역 | 지역 옵션 칸을 새로 만들지 않음 — 우리 자재표가 지역별 줄이라 재료 줄을 눌러 다른 지역 줄로 바꾸면 됨(지금 다시 고르기) · 기본 = 우리 기초와 같은 서울 줄 | 결정 | 요구 754 |
|
||||
| G4 | EST 원파일 자재 = 코드로 잇기 | 조사(요구 755) = 우리 자재표 30,711 줄 = 다산소프트 EST 2026-07 main.estx 자재 DB(출처 칸에 EST 코드 M… 23,977) + 품셈재료 119 · EST 원파일 여섯의 자재 자원 24,219 가운데 23,981(99 %)이 그 코드와 같음 · 안 맞는 238 = 그 공사가 직접 넣은 자재(관급 레미콘 · 사용자 추가 M0000000xxx) · 지금 엔진은 코드를 안 쓰고 이름으로만 이음 → 안: EST 원파일 자재는 **코드 우선**(출처 코드 ↔ `자재:M…`) · 코드 없는 238 만 이름 · 못 찾으면 「자재 없음 · 엑셀 값」 · 엑셀(EST 내보내기 — 코드 없음) · STC(STmate 코드 다름)는 이름 규칙 G2 ①②③ · 814-1 뒤 재료 못 이음 252 = EST 10 · 엑셀 162 · STC 80 | 협의 중(브레인 안) | 요구 755 |
|
||||
| G5 | 경쟁사 두 DB 재료를 맞대어 우리 자재 줄에 서로의 연결 정보를 둠(요구 757-2) | 조사 = ① EST: 전역 코드(M…) 있음 · 우리 표 출처 칸에 23,977 개 이미 보관 · 원파일 자재 99 % 일치 → 코드로 바로 ② STmate: 프로젝트 파일(COSTN)의 M 코드는 **공사마다 새로 매기는 번호**(일곱 파일에서 같은 코드 · 다른 품목 60 대 같은 품목 30) · 전역 코드 사전(JDCODE_M 57,580 · 17 자)은 암호화 · 프로젝트 파일에 안 실림(NAME · SIZE · UNIT · BQCODE = 물가지 연월뿐) → STmate 쪽 연결 정보 = 코드가 아니라 **이름 · 규격 글(별칭)** ③ 일곱 STC 의 자재 228 품목을 우리 표와 글로 맞대면 이름 · 규격 다 같음 24 · 이름 같음 규격 다름 52 · 이름 없음 152 → 자동 10 % · 나머지는 사람이 한 번 이어 줌 ④ EST 출력 엑셀 못 이음 96 줄 = 이름 없음 58 · 이름 있음 규격 다름 38(북부청임도 43 · 북부청사방 31 = 옛 판 품목명 「P V C PIPE」 「유로폼(패널)」 「와이어매쉬」 · 공사가 직접 넣은 품목) → 안: 우리 자재 줄에 「연결」 칸 = EST 코드(있는 것) + 별칭 글 목록(EST 옛 이름 · STmate 이름 · 규격) · 같은 품목이면 새 줄 안 만들고 별칭만 더함(중복 최소) · 우리에 없는 품목만 새 줄(출처 표시) · 쌓는 길 = 불러올 때 못 이은 줄을 사람이 한 번 이으면 별칭으로 기억(별칭 장부 · 승인) · 실제 쓰인 것부터 | 협의 중(브레인 안) | 요구 757 |
|
||||
| G6 | 공사가 직접 넣은 품목(우리 표에 없는 자재 · 기계) | 그대로 유지해 단가산출 · 일위대가에 넣음(이름 · 규격 · 단위 · 엑셀 값) → 만들어진 M02 단가산출 · 일위대가에서 바꾸도록 유도: 그 줄에 「우리 자재 아님」 표시 · 문서 위에 「우리 정보로 안 이어진 줄 n」 띠(누르면 그 줄로) · 줄 이름 단추 = 찾아 넣기로 우리 자재 고름 → 값이 우리 값 · 고른 짝은 별칭으로 기억 · 안 바꾸면 엑셀 값 그대로 쓰되 [단가 갱신] 대상 아님을 표시 | 미룸(요구 759 — 끝내 안 이어질 때 쓰는 길) | 요구 758 · 759 |
|
||||
|
||||
## H. 814-1 뒤 수정 후보 정리(요구 759 · 사용자 확인 전)
|
||||
|
||||
| # | 고칠 것 | 상태 |
|
||||
|---|---|---|
|
||||
| H1 | EST 원파일 자재 = 우리 자재표의 참조 코드(출처 칸 EST 코드)로 바로 잇기 · 코드 없는 것만 이름 | 협의 중(G4) |
|
||||
| H2 | 우리 자재 줄에 「연결」 칸 = EST 코드 + 별칭 글 목록(EST 옛 이름 · STmate 이름 · 규격) · 같은 품목이면 새 줄 없이 별칭만 · 엑셀 · STmate 는 별칭 글로 잇기 | 협의 중(G5) |
|
||||
| H3 | 재료 표기 맞추기 넓힘(결속선(철선) ↔ 보통철선(결속선) · 유로폼 패널 · 낙석방지책 규격 표기) | 협의 중(G2 ①) |
|
||||
| H4 | 못 이은 재료를 사용자가 한 번 고르면 별칭으로 기억(기계와 같은 길 · 승인) | 협의 중(G2 ②) |
|
||||
| — | 재료 지역 = 옵션 칸 안 만듦 · 줄 바꾸기로 | 결정(G3 · 코드 없음) |
|
||||
| 미룸 | 공사 추가 품목 유도 띠(G6) · 화면 속도(B4 — 요구 761 보류) | 미룸 |
|
||||
| 협의 | 후보 고르기(G1) · 자재표 보강(J 장) | 협의 중 |
|
||||
|
||||
## I. H1 ~ H4 + 후보 고르기(G1)까지 했을 때 예상(요구 760 · 어림 — 812-12 표본 쉰 · 서른 · 814-1 수치 바탕)
|
||||
|
||||
| 잣대 | 814-1 뒤(지금) | H1 ~ H4 뒤 | + 후보 고르기 |
|
||||
|---|---|---|---|
|
||||
| 재료 이음 | 87.6 % | 바로 90 % 안팎 · 별칭이 쌓이면 96 ~ 98 % | 같음(영향 없음) |
|
||||
| 블록이 우리 정보로 만들어짐 | 원파일 85.5 % · 엑셀 79.7 % | 바로 87 · 82 % 안팎 · 별칭 쌓이면 93 ~ 95 · 90 % | 같음(영향 없음) |
|
||||
| 연결 「확정」 수(원파일 2,220) | 463(21 %) | 같음 | 550 ~ 650(25 ~ 29 %) |
|
||||
| 「확정」 판정이 맞는 비율 | 100 %(무작위 30 거짓 0) | 같음 | 100 % 유지(줄 구성이 같아야 확정) |
|
||||
| 「근접」 판정이 맞는 비율 | 37 % 안팎(서른 중 맞는 근접 11) | 같음 | 80 ~ 90 %(엉뚱한 근접 · 틀린 후보 빠짐) |
|
||||
| 「없음」 판정이 맞는 비율 | 50 % 안팎(쉰 중 맞는 없음 19 · 세 줄 탓 12 는 814-1 로 풀림) | 같음 | 70 ~ 80 %(이름 비슷한 기초를 후보로 냄) |
|
||||
| 근접 · 없음 수 | 784 · 761 | 같음 | 근접 450 ~ 550 · 없음 850 ~ 950(엉뚱한 근접이 없음으로 옮겨 감) |
|
||||
|
||||
## J. 우리 자재표 보강까지 한다면(요구 761 · 어림)
|
||||
|
||||
- 보강 대상(지금 스물네 파일 기준) = ① 우리 표에 줄은 있는데 값 칸 다섯이 모두 빈 것 — 표 전체 464 줄 · 쓰인 것 102 줄(급수호스 · 공기주입기 · 공기호스 · 수직시험관 · 시스템동바리 · Air Hammer · Button Bit …) = EST DB 자체에 값이 없는 품목 ② 우리 표에 없는 품목 — 못 이은 재료 252 줄 = 105 종(이름 없음 약 130 줄 · 이름 있음 규격 다름 약 122 줄 — 뒤쪽은 H2 ~ H4 별칭으로 먼저 풀림)
|
||||
- 정할 것 = **보강 값의 출처**: 경쟁사 파일에 적힌 값(그 공사 때 값 · [단가 갱신] 안 됨) / 물가지 · 나라장터에서 찾아 넣음(사람 작업 · 갱신 됨)
|
||||
|
||||
| 잣대 | 814-1 뒤 | H1 ~ H4 + 별칭 쌓임 | + 자재표 보강 |
|
||||
|---|---|---|---|
|
||||
| 재료 이음 | 87.6 % | 96 ~ 98 % | 98 ~ 99 % |
|
||||
| 재료 0 원 줄 | 102 | 102 | 0 가까이 |
|
||||
| 블록이 우리 정보로 만들어짐 | 원파일 85.5 % · 엑셀 79.7 % | 93 ~ 95 % · 90 % | 95 ~ 97 % · 92 ~ 95 % |
|
||||
| 그래도 남는 것 | — | — | 기계 후보 여럿 88 · 별칭 대기 65(사용자 고름) · 기계 표에 없음 53 · 견적 운임 59 · 새 공사가 들고 오는 새 품목 |
|
||||
| G7 | 견적 정보(협력사 견적 · 구역화물 운임 · 수수료 등 프로젝트 안에서 지정하는 금액) | 단가산출로 만들 때 그대로 보임(이름 · 값 · 「견적」 표시) · 못 이음 · 흠으로 세지 않음 · 우리 정보로 안 바꾸고 프로젝트에 쓰면 그 가격 · **우리 자재가 있는 품목이면 견적 값 대신 우리 DB 값**(견적 값은 검산용) · 견적 지정 기능은 나중에 프로젝트 안에서 따로 · 못 찾는 품목도 나중에 같은 방식(보류) | 결정 | 요구 762-3 · 763-3 |
|
||||
| G8 | 별칭 승인 리스트 지우기 | 실행(11-11) — 개인 층 `storage/3/15` 맞춤 38 줄 · `storage/1/10` 맞춤 5 줄 비움 · 백업 `tmp/backup_762_별칭후보/` · 마스터 기계 별칭 다섯 · 회사 1 공사 내역 맞춤 10 줄은 둠 → M01 별칭 장부 「모인 후보」에 공사 내역 맞춤 10 줄이 아직 보임(지울지 사용자 확인) | 결정 · 실행 | 요구 762-2 · 763-2 |
|
||||
| G9 | 별칭이 웹앱에 들어가는지 · 수기 입력이 반영되는지(요구 763 추가 검토) | 조사 = ① 승인하면 마스터 층 `resources/master_template/실무자료/맞춤.json` 에 들어가고 M01 별칭 장부 「마스터 장부」 탭에 보임(M02 별칭 장부 부품 그대로) ② 수기 = 그 탭에서 있는 줄의 갈래 · 짝을 고치고 [저장](서버가 짝 다시 검사) · [삭제] 됨 · **새 별칭 줄을 손으로 만드는 단추는 없음**(줄은 불러오기 확인에서만 생김) · 기계 짝을 고르는 칸은 확인 못 함(부품이 로직 · 자재 짝 위주) ③ 반영 = 기계: 불러올 때 마스터 + 자기 층 장부를 먼저 봄(812-9 · 반영됨) · 재료: 일위대가 호표 줄에서만 읽음 — **단가산출 불러오기의 재료는 아직 장부를 안 읽음**(H4 몫) ④ 반영 시점 = 다음 불러오기부터(이미 만든 단가산출은 안 바뀜) | 조사 끝 | 요구 763 |
|
||||
|
||||
## K. 다음 작업 리스트(요구 764 반영 · 사용자 「진행」 전)
|
||||
|
||||
- 별칭 기준(요구 764) = **한 열로만** — 줄마다 「별칭」 칸 하나 · 콤마로 여러 개 · 손으로 입력(M01 단가산출 기초 목록의 「별칭」 칸과 같은 꼴 · 요소 표 인력 · 기계 · 자재에는 지금 그 칸이 없음).
|
||||
|
||||
| # | 작업 | 근거 |
|
||||
|---|---|---|
|
||||
| K1 | EST 원파일 자재 = 자재표 참조 코드(출처 칸 EST 코드)로 잇기 · 코드 없는 것만 이름 | G4 · H1 |
|
||||
| K2 | M01 요소 표(자재 · 기계 · 인력)에 「별칭」 한 열 — 콤마로 여러 개 · 손 입력 · 불러오기(단가산출 · 일위대가 · 재료 · 기계)가 이름보다 먼저 이 열을 봄 · 마스터 기계 별칭 다섯을 이 열로 옮김 | 요구 764 · G5 · G9 · H2 · H4 |
|
||||
| K3 | 재료 표기 맞추기 넓힘(결속선 · 유로폼 패널 · 낙석방지책 규격 표기) | G2 ① · H3 |
|
||||
| K4 | 기초 찾기(연결 찾기) 전체 걷음 — 판정 띠 · 바꾸기 · 되돌리기 · 연결후보 칸 · 전수 도우미 | G1 |
|
||||
| K5 | 견적 줄 = 「견적」 표시로 그대로 보임 · 우리 자재가 있으면 우리 값 · 못 이음 수에서 뺌 | G7 |
|
||||
| 확인 | 옛 별칭 장부(층 장부 · M01 승인 화면 · 모인 후보)를 K2 로 갈음해 걷을지 · 공사 내역 맞춤 10 줄 지울지 | 요구 764 · G8 |
|
||||
| 미룸 | 공사 추가 품목 유도 띠(G6) · 화면 속도(B4) · 자재표 보강(J · 값 출처 미정) | — |
|
||||
|
||||
## L. 815 뒤 질문 · 조사(요구 767 ~)
|
||||
|
||||
| # | 물음 | 조사 결과 | 상태 |
|
||||
|---|---|---|---|
|
||||
| L1 | 덤프 1 톤은 원문에 없나(요구 767) | 건설품셈 기계 목록 = 2.5 ton 부터(1 톤 줄 없음) · 산림품셈 10-9 머리글에 「4륜구동 1톤 덤프트럭 — 작업량 건설품셈 · 중기가격 조달청 등재가격 · 주 연료 2.5톤의 80 % · 손료 · 운전경비 2.5톤 적용」 · 우리 = 계산식 GF000145 는 있으나 기계표에 1 톤 줄이 없어 가져온 「덤프트럭 1.0Ton」 을 이을 줄이 없음 → 기계표에 줄 더하면 별칭 가능(안) | 조사 끝 |
|
||||
| L2 | EST main 기계 — 형식 없는 이름은 값으로 알 수 있지 않나(요구 768) | 맞음 — main 기계 1,049 대 가운데 「후보 여럿」 35 대(조합 「대형브레이카 +굴착기(1.0㎥)」 · 옛 이름 「굴삭기(유압식백호) 0.6㎥」 …) · 같은 파일 안에 **노 · 재 · 경 · 계 넷이 원 단위로 같은 또렷한 기계**가 하나 있으면 그것으로 가름: 대형브레이카 +굴착기(1.0㎥) 148,725(59,020 · 41,145 · 48,560) = 굴착기(무한궤도) 1.0+브레이카 148,725 ≠ 타이어형 154,114 · 35 대 중 26 대 가름 · 9 대(스크레이퍼(견인식) · 콘크리트 믹서트럭 6.0㎥(L) …)는 같은 값 없음 → 안: 기계 찾기에 「같은 파일 값 같음」 가름 더함(허용 오차 0) | 조사 끝 · 안 |
|
||||
| L3 | 발전기 5.5kW 는 원문에 없나(요구 769-3) | 없음 — 현행 건설품셈 기계 목록 발전기 = 25 · 50 · 100 · 125 · 150 · 200 · 250 · 350 · 450 · 500 · 700㎾ · 산림품셈에도 없음(5.5㎾ 는 펌프 설치 품 · 용접기 규격에만) · 가져온 「발전기 5.5 · 7 · 10kW」 는 2016 사방 엑셀에서만 나옴(옛 판 또는 공사가 넣은 기계) | 조사 끝 |
|
||||
|
||||
## M. 다음 묶음 후보(요구 769 까지 · 사용자 「진행」 전)
|
||||
|
||||
| # | 작업 | 상태 |
|
||||
|---|---|---|
|
||||
| M1 | 기계표에 「4륜구동 1톤 덤프트럭」 줄 추가(근거 산림품셈 10-9 · 계산식 GF000145 와 짝) → 「덤프트럭 1.0Ton」 별칭 가능 | 결정(요구 769-1) |
|
||||
| M2 | 기계 찾기 — 형식 없는 조각(조합 속 「굴착기 1.0㎥」 등)은 **같은 파일 안 값(노 · 재 · 경 · 계)이 원 단위로 같은 또렷한 기계**로 가름(허용 오차 0) · main 35 대 중 26 대 | 안(요구 768 · 769-2) |
|
||||
| M3 | 조합 기계 = 좌측 그룹 둘로(지금대로) · [우리 단가로] 가 엑셀과 같아지려면 굴착기 줄이 **조합용 값**(원문 8-4-8 주⑤ — 조합 때 굴착기 잡재료 비율 16 % · 단독 22 %)이어야 함: 단독 131,915 + 브레이커 18,939 = 150,854 ≠ 조합 148,725(재료비 43,274 → 41,145) — 우리 조합 계산식(GC000270 · 271)이 그 값을 냄 | 확인(요구 769-2) |
|
||||
|
||||
## N. 요구 770 조사(출처 프로그램 분석 · 엔진톱)
|
||||
|
||||
| # | 물음 | 조사 결과 | 상태 |
|
||||
|---|---|---|---|
|
||||
| N1 | 형식 없는 기계 · 조합 기계가 출처 프로그램(EST) 안에서 어떻게 쓰였나(요구 770-2) | EST 원파일 기계경비 구성 표(MachMainDat — 기계 머리 1,049 + 구성 줄 3,195 · 머리 다음 줄부터 빈 줄까지가 그 기계의 구성) · 「대형브레이카 +굴착기(1.0㎥)」 = 굴삭기손료 2,085 × 142,069,000 원 + 브레이카손료 6,601 × 28,692,000 원 + 주연료 경유 19.5ℓ + 잡품 16 % + 건설기계운전사 1 인 → 굴착기 줄의 **손료계수 · 가격 · 연료량이 굴착기(무한궤도) 1.0㎥(2,085 · 142,069 천원 · 19.5ℓ)와 같음**(타이어 = 2,279 · 144,360 · 20.5ℓ) → 무한궤도로 확정 · 우리 기계표 EQ000022 도 같은 값 · 조합 97 대의 손료 줄 200 가운데 197 이 같은 파일의 단독 기계와 계수 · 가격 같음(없음 3 = 크램셀) · 형식 없는 조합 속 굴착기 23 = 전부 무한궤도 · 「크레인(트럭) 10 · 15Ton」 = 「크레인(타이어) 10 · 15ton」 과 구성 같음(EQ000128 · EQ000129) · 조합 잡품 16 %(단독 22 · 24 %) = 원문 8-4-8 주⑤ 그대로 | 조사 끝 |
|
||||
| N2 | 엔진톱 내용이 원문에 있나(요구 770-4) | 있음(이름은 「체인톱」) — 산림품셈 2-1-1(휘발유 5.6ℓ/대/일 · 체인오일 2.1ℓ · 잡품 40 %) · 2-2-1(체인톱 45cc 손료계수 0.0084 — 1 일 1 대당) · 4-1-1 임업용 동력기계톱 · 건설품셈 3-9-1 주⑤ 「엔진톱」 = 경장비로 인력품의 10 %(따로 기계경비 없음) · 우리 기계표 EQ000620 체인톱 45cc · 2016 사방 엑셀 「엔진톱 18"」 = 같은 것을 **시간당**으로 적음(손료 10,500 × 10⁻⁷ × 8 시간 = 0.0084 · 휘발유 0.7ℓ/hr × 8 = 5.6ℓ) · 다른 것 = 잡품 50 % 대 40 %(값) · 규격 표기 18" 대 45cc · 단위(시간당 대 1 일 1 대당) | 조사 끝 |
|
||||
|
||||
- M2 고침(안): 가름의 잣대 = 금액(노 · 재 · 경 · 계)보다 **구성 줄(손료계수 · 주연료량 · 조종원 — 품셈 값)** — 원파일은 구성 표 · 엑셀은 「기계경비」 시트(산출근거 줄)에 있음 · 가격 · 유가는 값이라 잣대에서 뺌.
|
||||
- M1 = PLAN 816-1 로 진행(요구 770-1).
|
||||
|
||||
## O. 구글 시트 「수동연결(자재)」 · 「수동연결(기계)」 검토(요구 772 · 시트 = AISLO_이슈관리대장 · 읽기 = 드라이브 커넥터로 xlsx 받아 `tmp/q772_sheet.xlsx`)
|
||||
|
||||
기계 30 줄(사용자 메모 대조):
|
||||
|
||||
| 갈래 | 줄 | 내용 |
|
||||
|---|---|---|
|
||||
| 이미 이어짐(목록이 옛 것 — 815-2 별칭 다섯 합치기 전에 뜬 목록) | 4 | 바이브레이트 → EQ000248 · 버킷준설기 → EQ000425 · 아스팔트페이버(피니셔) 3.0m → EQ000193 · 1.7m → EQ000192 |
|
||||
| 816 에서 처리 중 | 8 | 덤프트럭 1.0Ton · 굴착기 1.0 · 0.6㎥ · 굴삭기(1.0m3) 둘 · 크레인(트럭) 10 · 15Ton · 엔진톱 18" |
|
||||
| 메모 글 검색으로 바로 나옴(별칭만 넣으면 됨) | 3 | 덤프자동덮개 15톤 → EQ000078 덤프트럭 자동덮개시설 15톤용 · 24톤 → EQ000080 · 트럭트랙터및트레일러 20톤 → EQ000183 트럭 트랙터 및 평판트레일러 20ton |
|
||||
| 확인 필요 | 2 | 커터(콘크리트 및 아스팔트용) 320-440mm ↔ EQ000234 320~400(규격 범위 다름) · 크로울러드릴 탑승유압식 ↔ EQ000337 110㎾ / EQ000338 150㎾(가져온 쪽 규격 없음) |
|
||||
| 우리 표 · 현행 품셈에 없음 | 13 | 발전기 5.5 · 7 · 10kW · 브레이카 0.18㎥ · 모노펌프 · 에어 호스 · 파워트로웰 · 콘크리트펌프용 파이프 여섯(EST 는 기계 손료로 둠) |
|
||||
|
||||
- 크레인(트럭) 10 · 15Ton: 사용자 메모 = 트럭탑재형 크레인 · EST 구성 줄 = 크레인(타이어)와 같은 값(2,298 · 136,400 천원 · 3.8ℓ — 트럭탑재형 10ton 은 2,598 · 91,593 천원 · 10.3ℓ) → 사용자 판단 대기.
|
||||
|
||||
자재 100 줄(브레인이 한 줄씩 읽음 · 사용자 메모 칸은 비어 있음):
|
||||
|
||||
- 우리 줄 하나로 또렷(표기만 다름 — 별칭만 넣으면 됨) 65 줄: 2 · 18 · 26 → MT000467 / 5 · 25 → MT000470 / 19 → MT000472 / 15 · 17 → MT000209 / 16 · 50 · 52 · 20 → MT000018 / 21 · 65 → MT000499 / 3 · 7 · 51 · 53 → MT000393 / 10 → MT009202 / 27 · 63 → MT007111 / 28 → MT005677 / 31 → MT005687 / 35 · 68 → MT006544 / 37 → MT002926 / 62 · 85 → MT002861 / 87 → MT002859 / 40 · 95 → MT003202 / 96 → MT003241 / 42 · 54 → MT005749 / 44 · 94 → MT000481 / 47 → MT000487 / 55 · 74 → MT003163 / 73 → MT003165 / 56 → MT003166 / 66 · 84 → MT000453 / 67 → MT007130 / 71 → MT023493 / 72 → MT023491 / 77 → MT023654 / 78 → MT023655 / 75 → MT002536 / 82 → MT006490 / 86 → MT006540 / 89 → MT015883 / 91 → MT015887 / 90 → MT007025 / 92 → MT007032 / 98 → MT003311 / 100 → MT003372 / 57 → MT003398 / 4 → MT006763 / 49 → MT006665 / 48 → MT006667 / 33 · 79 · 81 → MT006664 / 83 → MT006788 (번호 = 시트 행 차례 · 머리 뺀 1 ~ 100)
|
||||
- 사람이 골라야 함(우리 줄 여럿 · 규격이 조금 다름) 31 줄: 1 접착제 · 6 결속선(보통철선 대 어닐링철선) · 13 · 14 · 23 · 24 · 58 · 59 종자 · 32 미송(판재) · 45 철물 · 70 어닐링철선 #6 · 36 PVC용접봉 · 69 용접봉(연강용) · 29 BUTTON BIT · 41 표지판지주 3.5M · 64 강관비계 6m · 93 잔디 · 22 에폭시수지 · 46 각재(말뚝) · 76 보조강재 · 97 공사안내표지판 · 99 윙카호스 · 11 · 12 낙석방지책 · 38 혼합골재 · 61 코어비트 · 30 샹크 · 34 유기질퇴비 · 60 우드칩 · 80 부산물비료 · 88 휠라
|
||||
- 잇지 않음 · 없음 4 줄: 8 파형강관 · 39 이형철근(「별도계상」 = 자리만) · 9 금강사 · 43 떼
|
||||
- 못 이은 까닭 = 정확 일치만 잇는 규칙(이름 낱말 차례 「유로폼(패널)」 ↔ 「유로폼패널」 · 「내수합판1급」 ↔ 「합판(내수)」 · 규격 표기 12mm ↔ 12t · 차례 뒤집힘 T5×200 ↔ 200×5T · 덧글 KSM · 국산 · 지역 꼬리) · 가까운 줄 제안이 틀린 것도 있음(신나 → 신나무 · 중하중그레이팅 → 다른 치수).
|
||||
- 구글 시트 직접 쓰기 토큰 만료(invalid_grant) — 사용자가 데스크탑에 있어 랩탑 로그인 창을 못 누름 · 읽기는 드라이브 커넥터로 됨.
|
||||
|
||||
## P. 요구 773 ~ 776 결정
|
||||
|
||||
| # | 결정 | 상태 |
|
||||
|---|---|---|
|
||||
| P1 | 기계 가름은 값 비교가 아니라 로직 — 우리 기계표 값과 맞대지 않고 **같은 파일 안 단독 기계와 대조**해 그 이름으로 찾음(요구 773-3) | 816-2 진행 |
|
||||
| P2 | 기계 별칭 = 브레인이 제안 → 승인 길로 넣음(요구 776): 덤프자동덮개 15톤 → EQ000078 · 24톤 → EQ000080 · 트럭트랙터및트레일러 20톤 → EQ000183 · 커터(콘크리트 및 아스팔트용) 320-440mm → EQ000234 · 크로울러드릴 탑승유압식 → EQ000337(110㎾) · 크레인(트럭) 10Ton → EQ000128 · 15Ton → EQ000129 | 넣음 · 찾기 확인 |
|
||||
| P3 | 엔진톱 18" → 체인톱 EQ000620 별칭 = 인치 따옴표 풀이(816-3) 합친 뒤 넣음 | 대기(816 합친 뒤) |
|
||||
| P4 | 재료 찾기 엔진 개선 — 표기만 다름 = 자동 이음 · 종자류 = 후보 좁힘 1 순위 · 별도계상 = 단가산출 뒤 프로젝트에서 연결(요구 773-5) | 817-1 진행 |
|
||||
| P5 | 값이 빈 우리 자재 줄(치즐 0.7 · 0.4 등)에 이어진 줄 = 이음 완료(로직 문제 아님) · 0 원 줄 흠에서 뺌 · 자재표 값 보강은 27 년 초 프로그램 막바지(요구 776) — J 장 닫음 | 완료 처리 |
|
||||
| P6 | 콘크리트펌프용 파이프 = EST 「레미콘타설(펌프차)(배관타설)」 의 압송관 손료(직관 1 · 2 · 3m · 곡관 45 · 60 · 90° · 손료계수 4,500) · 현행 품셈 기계 목록에 없음 | 조사 끝 |
|
||||
| P7 | STmate 재료 DB 있나(요구 778) | 있음 — 설치 폴더 `ST_BSC\자재단가\시설공통자재\` 넷(2025-상 · 2025-하 · 2026-상 · 2026-상(06) 시설공통자재.STBSC · 각 약 180 KB) · 꼴 = 암호 걸린 ZIP(속 `COST.ST` 등) → 내용은 못 읽음 · 파일 자체는 우리 저장소에 없음(목록만 `STmate/old/30_원자료/system_inventory.csv`) · 공사 파일(.STC) 안 `COSTN` 표의 M 코드 자재(값 여섯 칸)는 지금도 읽음 | 조사 끝 |
|
||||
|
||||
## Q. 요구 781 결정
|
||||
|
||||
| # | 내용 | 상태 |
|
||||
|---|---|---|
|
||||
| Q1 | 없는 기계 경쟁사 계산(요구 781-1) — 전부 **표준 꼴**: 손료(손료계수 × 중기가격) + 주연료 + 잡품 + 조종원(파워트로웰 = +전력 · 브레이카 · 에어호스 · 콘크리트펌프 파이프 · 모노펌프 = 손료만) · **발전기 5.5kW 는 EST main 기계 DB 에 이미 있음**(계 37,760 = 손료 2,362 × 가격 + 경유 0.625ℓ + 잡품 11 % + 일반기계운전사 1인 · 2016 사방 엑셀도 같은 꼴) — 현행 품셈판에만 없음 · 압송관(콘크리트펌프 파이프) · 에어호스 등도 EST DB 에 손료 줄로 있음 | 조사 끝 |
|
||||
| Q2 | 별도계상 = 완료 처리(요구 781-2) — 단가산출에서 금액 안 매기고 프로젝트에서 연결(이미 반영) · 흠 · 미결 목록에서 뺌 | 완료 |
|
||||
| Q3 | 구글 시트 「수동연결(기계)」 · 「수동연결(자재)」 내용 지우고 미해결만 넣기(요구 781-3) — 기계 미해결 13(전부 「우리 표에 없음」 — 새 줄 필요) · 자재 미해결 49 종 | 시트 쓰기(토큰 재로그인 필요) |
|
||||
| Q4 | 자재표 값 보강 대상(요구 781-4) — 이어졌으나 우리 값 빈 자재 50 종(치즐 0.7㎥ 45 · 0.4㎥ 6 · 공기주입기 · 급수호스 · 시스템동바리 부품 · Air Hammer · Button Bit · 흙·골재·아스팔트·콘크리트 시험류 = 시험·견적 성격) · 우리 자재표 값 빈 줄 전체 464 / 30,711 | 시트에 넣기 |
|
||||
|
||||
## R. 없는 기계 값 출처(요구 782 ~ 784)
|
||||
|
||||
- 손료 = 손료계수(품셈) × 중기가격(가격 자료) + 주연료 + 잡품 + 조종원 — 가격 하나로는 못 셈 · 둘 다 필요(사용자 확인).
|
||||
- 공공데이터포털 키(DATA_GO_KR) = 정상(인증 통과) · 그러나 **조달청/나라장터에 건설기계 취득가격 전용 오픈API 없음**(종합쇼핑몰 품목 API = 서비스 폐기 응답 · 건설기계 중기가격 실시간 API 없음).
|
||||
- 공식 가격 출처 = **연간 고시 파일**: 행정안전부 「건설기계 표준시장가격」(원가계산·손료용 · 품셈 8-05 근거) · 「시가표준액표」(지방세용 — 성격 다름) · 연도별 「건설기계경비 산출표」(PDF · 손료계수 + 가격 함께) — 모두 API 아님(파일).
|
||||
- 손료계수 = 품셈 8장(상각·정비·관리 계수) · 소형(발전기 5.5·7·10kW)은 현행 품셈 가격표(25kW~)·손료계수표에 없음 → 같은 기종 계수(발전기 2,362 · 브레이커 6,601) 차용 + 가격은 행안부 표준시장가격 파일.
|
||||
- 사용자 뜻(요구 783 · 784): EST 숫자 안 씀 · 조달청/행안부 공식 자료로 · 행안부 시가표준액표가 있으면 사용.
|
||||
- 미정: 행안부 표준시장가격 파일을 (가) 사용자가 받아 줄지 (나) 브레인이 공개 연도별 「건설기계경비 산출표」에서 해당 기계 값만 뽑을지.
|
||||
|
||||
## S. 품셈 밖 기계·재료 = 개인 관리 검토(요구 790 · 사용자 결정 전)
|
||||
|
||||
- 기계표 668 줄 구성 꼴: 손료+연료+조종원 281 · 손료만 267 · 손료+연료 58 · 손료+조종원 19 · 가격 없는 줄 약 40 · 전력 1 → 네 꼴이 94 % → 신규 기계 입력 칸 = 이름·규격 · 가격 · 손료계수 · 주연료(종류 · ℓ/hr) · 잡품 % · 조종원(직종 · 인) (+ 전력 kWh) · 꼴은 채운 칸으로 자동.
|
||||
- 안 S1(1 번): M01 = 품셈 정본만 · 품셈 밖 기계 = M02 개인 층 「기계 시간당 경비」 단가산출 한 장(손료 = 가격 × 계수 · 주연료 → 우리 유가 · 잡품 % · 조종원 → 우리 노임) · 쓰는 단가산출은 그 장 참조 · 불러오기에서 못 이은 기계는 그 파일 구성(EST 원파일 구성표 · 엑셀 기계경비 시트)으로 자동 생성 · 구성 없는 파일(STmate 원파일)은 값 한 줄 장.
|
||||
- 안 S2(2 번): M01 기계 표 = 목록 · 줄 누르면 상세(값 묶음 · 시간당 계산식 미리보기 · 별칭 · 쓰는 곳) · S1 양식과 같은 부품.
|
||||
- 안 S3(3 번): 층마다 요소 표(저장소)를 새로 두는 것은 비권장(키 둘 · 찾는 차례 · 갱신 책임 · 동봉 · 삭제 깨짐) · 대신 「내 기계 · 내 품목」 모아 보기(읽기 중심 · 같은 이름 묶음 · 쓰는 곳 수) + 만들 때 같은 이름 안내.
|
||||
- 안 S4(4 번): 820-1 의 13 줄(EQ000656 ~ 668) · GC001195 는 S1 방향이면 지움 · 덤프 1 톤(산림품셈 10-9) · 체인톱 시간당 식(산림품셈 2 장) · 별칭 10 은 둠 · 시점 = S1 기능 넣는 묶음에서 같이(공백 없음).
|
||||
- S5(5 번): 차례 = 표기 엔진 · 별칭 · 같은 파일 대조로 품셈 요소에 최대한 → 남는 것만 개인 장 → 반복되면 마스터 승격 제안 · 불러오기 결과에 「개인 생성 n」 표시.
|
||||
- 참고: 현행 품셈 8 장에 조종원 노임 ×16/12 ×25/20 없음(우리 = ÷8) · EST 구성은 옛 환산 포함 → 자동 생성은 우리 틀에 값만 담는 쪽 권장.
|
||||
@@ -0,0 +1,185 @@
|
||||
# 812-12 단가산출 불러오기 정합성 — 전수 보고(요구 738-5)
|
||||
|
||||
- 대상 = 원파일 EST 6 · STC 7(산근 2,220 블록) + 실무 엑셀 11(1,021 블록) · 812-1 · 812-2 · 812-4 · 812-9 · 812-11 · 813-1 · 813-2 반영 판
|
||||
- 수치 = 전수로 잰 것 · 「어림」 = 무작위 쉰 · 서른을 사람 눈으로 가른 비율
|
||||
- 자세한 표 = `_목록_812-12_정합성.xlsx`(시트 금액 · 줄 읽기 · 요소 이음 · 우리 단가 · 차림 · 연결 판정 · 없음 쉰 · 근접 서른 · 흠)
|
||||
- 코드 · 마스터 · storage 고침 없음 · 화면에서 만든 SK 열셋 · 올린 파일 모두 지움
|
||||
|
||||
## 요약
|
||||
|
||||
| 항목 | 수 | 비율 | 판정 |
|
||||
|---|---|---|---|
|
||||
| 1 금액(엑셀 단가 그대로 셈 = 원본) | 원파일 2,220 / 2,220 · 엑셀 1,021 / 1,021 | 100 % · 100 % | 맞음 |
|
||||
| 2 줄 읽기 — 못 읽음 표지 | 0 / 78,308 줄 | 0 % | 맞음 |
|
||||
| 2 엑셀 ↔ 원파일 실제 짝(거창 · STC 다섯) | 블록 264 · 줄 10,744 다름 0 | 100 % | 맞음 |
|
||||
| 2 기계 비목 세 줄(노 · 재 · 경 따로) | STC 285 / 285 기계 블록 · 엑셀 277 · EST 155 | STC 100 % | **흠** |
|
||||
| 3 인력 이음 | 5,089 / 5,089 줄 | 100 % | 맞음 |
|
||||
| 3 기계 이음(이어짐 · 별칭 · 조합 가름) | 5,699 / 6,279 줄 | 90.8 % | 보통 |
|
||||
| 3 기계 — 찾았으나 시간당 경비 이음 없음 | 349 줄 · 26 종 | 5.6 % | **흠** |
|
||||
| 3 재료 이음 | 1,466 / 2,036 줄 | 72.0 % | 보통 |
|
||||
| 3 유가 이음 | 0 / 29 줄(모두 후보 여럿) | 0 % | 흠 |
|
||||
| 3 형제 단가산출 이음 | 402 / 402 줄 | 100 % | 맞음 |
|
||||
| 4 [우리 단가로] 모든 줄 우리 값 | 원파일 1,596 / 2,220 · 엑셀 762 / 1,021 | 71.9 % · 74.6 % | 보통 |
|
||||
| 4 [우리 단가로] 0 원 줄 있는 블록 | 원파일 236 · 엑셀 47 | 10.6 % · 4.6 % | **흠** |
|
||||
| 5 차림 왕복(문서 → 줄 → 문서) · 거울 | 3,241 / 3,241 | 100 % | 맞음 |
|
||||
| 5 조합 기계 항목 엇갈림(비목 세 줄 + 조합) | 83 블록 | 2.6 % | 흠 |
|
||||
| 6 연결 판정(원파일) 확정 · 근접 · 없음 · 기계 작업 | 339 · 865 · 818 · 198 | 15.3 · 39.0 · 36.8 · 8.9 % | — |
|
||||
| 6 연결 판정(엑셀) 확정 · 근접 · 없음 · 기계 작업 | 205 · 260 · 472 · 84 | 20.1 · 25.5 · 46.2 · 8.2 % | — |
|
||||
| 6 「없음」 — 우리 기초 있는데 못 찾음 | 쉰 중 31 | 62 %(어림) | **흠** |
|
||||
| 6 「근접」 — 연결 찾기가 더 붙일 수 있음 | 서른 중 15 | 50 %(어림) | 흠 |
|
||||
| 7 화면 블록 열 개 금액 = 원본 · 콘솔 오류 | 10 / 10 · 0 | 100 % | 맞음 |
|
||||
| 흠 | 심각 4 · 보통 7 · 가벼움 5 | 16 | — |
|
||||
|
||||
## 1 금액
|
||||
|
||||
| 원천 | 블록 | 셈 = 원본 | 다름 | 오류 |
|
||||
|---|---|---|---|---|
|
||||
| EST 원파일 6 | 1,849 | 1,849 | 0 | 0 |
|
||||
| STC 원파일 7 | 371 | 371 | 0 | 0 |
|
||||
| 실무 엑셀 11 | 1,021 | 1,021 | 0 | 0 |
|
||||
|
||||
- 검산 줄 틀림 3 줄 — 표지 있음 2(main 1308 · 원파일 자체 엇갈림) · 표지 없음 1(북부청임도 33 소계 · 블록 금액은 같음)
|
||||
|
||||
## 2 줄 읽기
|
||||
|
||||
| 줄 갈래 | EST | STC | 엑셀 |
|
||||
|---|---|---|---|
|
||||
| 제목 · 합계 | 19,107 | — | 5,846 |
|
||||
| 자원 | 7,609 | 1,918 | 3,485 |
|
||||
| 자원(형제 단가산출) | 356 | 9 | 37 |
|
||||
| 변수(값 · 공식) | 6,655 | 2,601 | 2,961 |
|
||||
| 율 | 1,125 | 114 | 511 |
|
||||
| 소계 | 1,795 | 1,072 | 1,296 |
|
||||
| 글 | 15,973 | 2,003 | 3,835 |
|
||||
| 표지 — 못 읽음 · 못 옮김 | 0 | 0 | 0 |
|
||||
| 표지 — 추정(품셈 조건 글) | 2,182 | 0 | 324 |
|
||||
| 표지 — 참고(조합 기계 · 당초 칸 · 후보 여럿 등) | 471 | 42 | 823 |
|
||||
|
||||
| 엑셀 ↔ 원파일 실제 짝 | 블록 | 줄 | 다름 |
|
||||
|---|---|---|---|
|
||||
| 거창 xlsx ↔ .estx | 61 | 1,467 | 0 |
|
||||
| STC 다섯(봉화 · 동부청영월 · 울진대흥 · 영덕계류 · 울진소광) | 203 | 9,277 | 0 |
|
||||
|
||||
- 울진대흥 엑셀 블록 33 = 원파일에 없는 블록(다른 판 파일) — 짝 대상 밖
|
||||
|
||||
| 기계 비목 세 줄 | EST | STC | 엑셀 |
|
||||
|---|---|---|---|
|
||||
| 블록 | 155 | 285 | 277 |
|
||||
| 줄 — 이름 = 「노 무 비 · 재 료 비 · 경 비」 | 0 | 1,391 | 1,109 |
|
||||
| 줄 — 「덤프트럭 노무비」 꼴 | 476 | 1 | 160 |
|
||||
|
||||
## 3 요소 이음(줄 수 · 이름 수)
|
||||
|
||||
| 갈래 | 쓰임 | 이어짐 | 별칭 | 조합 가름 | 찾음 · 시간당 이음 없음 | 후보 여럿 | 못 찾음(별칭 필요) | 우리 표에 없음 |
|
||||
|---|---|---|---|---|---|---|---|---|
|
||||
| 인력 | 5,089 | 5,089(47 이름) | — | — | — | 0 | 0 | 0 |
|
||||
| 기계 | 6,279 | 4,932(192) | 62(8) | 705(44) | 349(26 종) | 88 | 65(13) | 71(5) |
|
||||
| 재료 | 2,036 | 1,466(348) | — | — | — | 556(156) | 14(7) | — |
|
||||
| 유가 | 29 | 0 | — | — | — | 29(3) | — | — |
|
||||
| 환율 | 0 | — | — | — | — | — | — | — |
|
||||
| 형제 단가산출 | 402 | 402(97) | — | — | — | — | — | — |
|
||||
|
||||
- 기계 판정 없음 7 줄 · 고정 줄(갈래 밖) 59
|
||||
- 못 이은 것 많이 나오는 스무 이름 = 목록 「요소 이음」 시트(기계 = 대형브레이카 73 · 부착용집게 68 · 굴착기(타이어형) 56 · 진동파일해머 32 …)
|
||||
|
||||
## 4 [우리 단가로]
|
||||
|
||||
| 블록 판정 | 원파일 | 엑셀 |
|
||||
|---|---|---|
|
||||
| 모든 줄 우리 값 | 1,596 | 762 |
|
||||
| 이음 없는 줄 = 엑셀 단가 남음 | 388 | 212 |
|
||||
| 0 원 줄 있음 | 236 | 47 |
|
||||
| 셈 못 함 | 0 | 0 |
|
||||
|
||||
| 0 원 줄 까닭 | 줄 |
|
||||
|---|---|
|
||||
| 조합 다음 줄 · 시간당 경비 이음 없음(대형 브레이커 · 덤프트럭 자동덮개시설 등) | 252 |
|
||||
| 조합 다음 줄 · 기계 못 이음(부착용 집게 등) | 177 |
|
||||
| 재료 이음 · M01 값 0(공기주입기 · 시스템동바리 등) | 88 |
|
||||
| 기계 낱 줄 | 8 |
|
||||
|
||||
## 5 좌측 차림
|
||||
|
||||
| 항 | EST | STC | 엑셀 |
|
||||
|---|---|---|---|
|
||||
| 블록 | 1,849 | 371 | 1,021 |
|
||||
| 항목 · 제목 있는 항목 | 8,290 · 7,218 | 1,188 · 825 | 2,861 · 2,306 |
|
||||
| 가나다 소항목 | 1,725 | 28 | 298 |
|
||||
| 공식 묶음 · 글 줄 | 2,917 · 6,221 | 722 · 1,252 | 1,136 · 1,992 |
|
||||
| 조합 기계 · 그 항목 | 634 · 552 | 150 · 100 | 176 · 140 |
|
||||
| 제목 없는 블록 | 0 | 86 | 69 |
|
||||
| 왕복 같음 · TS = 파이썬 거울 | 1,849 · 1,849 | 371 · 371 | 1,021 · 1,021 |
|
||||
|
||||
- 조합 기계 수 > 항목 수 = 비목 세 줄 기계가 조합일 때 같은 제목 항목이 겹치고 줄이 엇갈림(83 블록 · 흠 2)
|
||||
|
||||
## 6 연결 찾기
|
||||
|
||||
| 원천 | 확정 | 근접 | 없음 | 기계 작업 | 합 |
|
||||
|---|---|---|---|---|---|
|
||||
| EST 원파일 | 316 | 771 | 636 | 126 | 1,849 |
|
||||
| STC 원파일 | 23 | 94 | 182 | 72 | 371 |
|
||||
| 실무 엑셀 | 205 | 260 | 472 | 84 | 1,021 |
|
||||
|
||||
- 파일별 수 = 목록 「연결 판정」 시트 · 원파일 「못 셈」 134 = 전수 도우미 흠 → 고친 사본으로 다시 잼(확정 2 · 근접 17 · 없음 115) · 엑셀 「못 셈」 17 = 같은 흠(다시 안 잼)
|
||||
- 확정 참고(블록) — 수량 114 · 단가 105 · 율 값 41 · 수량 끝자리 39 · 규격 37 · 옵션 확인 34 · 품 숫자 18 · 끝수 1 · 확정 다시 셈 다름 0
|
||||
- 근접 까닭(블록) — 줄 없음 536 · 기계 규격 296 · 율 줄 239 · 비목 223 · 이름 다름 186 · 율 대상 77 · 공구손료 유무 46 · 자원 25 · 단위 환산 4
|
||||
- 엑셀 ↔ 원파일 같은 블록 판정 — 264 중 246 같음(93.2 %)
|
||||
- 원파일 「없음」 818 구조 — 후보 0 = 294 · 가까운 후보 있으나 줄 짝 반 못 넘음 = 524
|
||||
|
||||
| 「없음」 무작위 쉰(어림) | 수 | 비율 |
|
||||
|---|---|---|
|
||||
| 우리 기초에 그 공종 없음(구역화물 운임 · 하차 요금 · 지자체 기준 · 자체 품 · 구조물 묶음 · 부품만) | 19 | 38 % |
|
||||
| 우리 기초 있는데 못 찾음 | 31 | 62 % |
|
||||
| └ 기계 비목 세 줄 탓 | 12 | 24 % |
|
||||
| └ 줄 구성 · 율 · 이름 다름(손율 재료 · 유리알 더함 · 이름 덧말) | 15 | 30 % |
|
||||
| └ 형제 묶음 · 못 셈 | 4 | 8 % |
|
||||
|
||||
| 「근접」 무작위 서른(어림) | 수 | 비율 |
|
||||
|---|---|---|
|
||||
| 같은 공종 · 차이는 표현(율 비목 · 비목 세 줄 · 조합 한 줄) — 더 붙일 수 있음 | 7 | 23 % |
|
||||
| 맞는 우리 기초가 따로 있음 — 후보 고르기 틀림 | 8 | 27 % |
|
||||
| 같은 공종 · 로직 실제로 다름 — 근접 맞음 | 4 | 13 % |
|
||||
| 우리 기초에 공종 없음 — 엉뚱한 근접 | 11 | 37 % |
|
||||
|
||||
## 7 화면(8005 · 5178 · 일반 계정 개인 층)
|
||||
|
||||
| 파일 | 블록 | 금액 = 원본 | 확인 |
|
||||
|---|---|---|---|
|
||||
| 거창 .xlsx | 1 측구터파기 | 5,323 | [연결 찾기] 확정 GF000099 → [바꾸기] 5,286 → [되돌리기] 5,323 |
|
||||
| 거창 .xlsx | 2 파쇄암 | 30,381 | 조합 가 · 나 · 다 항목 · 근접 GF000268 = 전수 · [우리 단가로] 23,747 = 전수(브레이커 0 원) |
|
||||
| 거창 .xlsx | 15 · 41 · 60 | 94,215 · 52,796 · 1,675 | 15 = 없음(후보 14 · 「못 셈」 아님) · 60 = 「기계 한 줄 단가산출」 띠 |
|
||||
| 거창 .estx | 25 장비운반 | 271,869 | 엑셀 HP 에 붙음 |
|
||||
| 봉화 .STC | 14 · 55 · 93 | 26,144 · 16,629 · 36,282 | 55 기계 = 「재 료 비 · 노 무 비 · 경 비」 세 줄 · 93 = 없음(후보 17) |
|
||||
| 북부청임도 .xls | 16 · 31 | 2,264 · 134,304 | 31 조합 나 · 다 항목(어두움) |
|
||||
|
||||
- 콘솔 오류 0(ResizeObserver 알림만) · 밝음 · 어두움 · 가로 넘침 0(1274 폭)
|
||||
|
||||
## 흠
|
||||
|
||||
| # | 심각 | 흠 | 예 | 고칠 길 |
|
||||
|---|---|---|---|---|
|
||||
| 1 | 심각 | STmate 기계 = 비목 세 줄(이름 「노 무 비」 · 단위 X) | 봉화 55 크로울러드릴 세 줄 · STC 285 블록 1,391 줄 · 엑셀 277 블록 | X 코드 L · M · S 세 줄 → 기계 한 묶음 줄 |
|
||||
| 2 | 심각 | EST 「덤프트럭 노무비 · 재료비 · 경비」 세 줄 · 조합이면 항목 엇갈림 | main 360 같은 제목 항목 셋 · 155 블록 · 엇갈림 83 블록 | 세 줄 묶은 뒤 조합 가름 |
|
||||
| 3 | 심각 | 단가산출 목록 열기 65~75 초 | SK 16 개 층 = 파일 읽기 4,137 번(`M02_Labor_Ref.users`) | 쓰는 곳 색인 한 번 |
|
||||
| 4 | 심각 | 찾은 기계 26 종 시간당 경비 이음 없음 → 우리 단가 0 원 · 엑셀 값 남음 | 거창 2 브레이커 0 원 · 349 줄 · 0 원 블록 283 | M01 시간당 짝 채움 · 못 이으면 엑셀 값 + 표지 |
|
||||
| 5 | 보통 | 원파일만 올리면 블록 목록 판 없음(STC 호표 0) | sub3임시_봉화.STC | 격자 없어도 목록 판 |
|
||||
| 6 | 보통 | 맞는 우리 기초 두고 다른 후보 근접 | 플륨관 설치 → 강관압입추진 · 서른 중 8 | 이름 · 절 근거 후보 먼저 |
|
||||
| 7 | 보통 | 엉뚱한 근접(인부 한 줄 짝) | 떼 하차비 → 텍코팅 · 서른 중 11 | 근접에 이름 근거 또는 핵심 줄 둘 |
|
||||
| 8 | 보통 | 이름 거의 같은 기초 두고 후보 0 | 비탈규준틀 ↔ GF000151 · 후보 0 = 294 | 덧말 뺀 이름 · 절 번호로 모음 |
|
||||
| 9 | 보통 | 엑셀 ↔ 원파일 같은 블록 판정 다름 | 동부청영월 2 · 264 중 18 | 이름 맞대기 전 규격 · 단위 꼬리 뗌 |
|
||||
| 10 | 보통 | 재료 · 유가 후보 여럿 → 이음 없음 | 재료 556 줄 · 유가 29 줄 | 규격 · 단위로 좁힘 · 유가 전국 기본 |
|
||||
| 11 | 보통 | 전수 도우미 형제 값 글자 → 「못 셈」(시험 도구만 · 화면 정상) | 원파일 134 · 엑셀 17 블록 | `helper_810_4_sweep` str() 뗌 |
|
||||
| 12 | 가벼움 | 조합 첫 줄 기계 칩 = 원래 조합 글 | 「굴삭기(무한궤도) 0.7+브레이카」 | 칩도 그 기계 이름 |
|
||||
| 13 | 가벼움 | 소계 줄 검산 틀림 · 표지 없음 | 북부청임도 33 · 478.8 ↔ 3,897.1 | 얼린 줄도 소계 식에 |
|
||||
| 14 | 가벼움 | 기계 줄 찾기 판정 없음 | 진동 로울러(핸드가이드식) · 7 줄 | 찾기 붙임 |
|
||||
| 15 | 가벼움 | 올린 직후 몇 초 [내 단가산출로] 409 | 거창 xlsx 블록 넷 | 올림 답 전 보관 끝냄 |
|
||||
| 16 | 가벼움 | 엑셀 블록에도 「원파일 확정」 글 | 거창 xlsx 2 | 갈래 맞는 글 |
|
||||
|
||||
## 사용자가 정할 것
|
||||
|
||||
| # | 정할 것 | 갈래 |
|
||||
|---|---|---|
|
||||
| 1 | 공구손료 율을 실무는 재료비 · 우리는 경비 — 이 비목 차이를 로직(근접)으로 볼지 · 값(참고 · 확정)으로 볼지 | 근접 서른 중 4 블록이 이것 하나로 근접(1 블록은 자재 이름도) |
|
||||
| 2 | 원파일(.estx · .STC)만 올린 경우에도 단가산출 블록 목록을 열지 | 지금 STC 단독은 화면 길 없음 |
|
||||
| 3 | 시간당 경비 자료 없는 기계 26 종 — [우리 단가로] 때 엑셀 값 그대로 · 0 원 + 표지 · M01 자료 채움 중 | 대형 브레이커 · 부착용 집게 · 굴착기(타이어형) 등 |
|
||||
| 4 | 구역화물 운임 · 하차 요금 · 지자체 기준 · 자체 품 블록(없음 쉰 중 38 % 어림)을 「품셈 밖」 표지로 연결 대상에서 뺄지 | 연결 「없음」 해석 |
|
||||
@@ -0,0 +1,79 @@
|
||||
# 814-1 불러오기 결정 반영 — 전후 전수 보고(요구 742 ~ 750 · 방향 장부 E 표 1 ~ 10)
|
||||
|
||||
- 대상 = 812-12 와 같은 자 — 원파일 EST 6 · STC 7(산근 2,220 블록) + 실무 엑셀 11(1,021 블록)
|
||||
- 전 = 812-12 보고 수 + 같은 자로 다시 잰 수(코드 바꾸기 전) · 후 = 814-1 반영 판(커밋 56a3e05 · 8914b21 · bed35e3)
|
||||
- 자세한 표 = `_목록_814-1_정합성.xlsx`(요약 전 · 후 · 연결 · 못 이은 줄 · 0 원 줄)
|
||||
- 화면에서 만든 SK 둘 · 올린 파일 셋 모두 지움 · 마스터 별칭 · 별칭 장부 · storage 층 사본 안 건드림
|
||||
|
||||
## 요약
|
||||
|
||||
| 잣대 | 전 | 후 | 판정 |
|
||||
|---|---|---|---|
|
||||
| 금액(엑셀 단가 그대로 셈 = 원본) | 원파일 2,220 · 엑셀 1,021 = 100 % | 원파일 2,220 · 엑셀 1,021 = 100 % | 그대로 |
|
||||
| 못 읽음 표지 | 0 | 0 | 그대로 |
|
||||
| 인력 · 형제 단가산출 이음 | 100 % · 100 % | 100 % · 100 % | 그대로 |
|
||||
| 기계 이음 | 90.8 %(5,699 / 6,279 줄) | **95.7 %**(4,601 / 4,807 줄 — 세 줄 묶은 뒤 줄 수) | 좋아짐 |
|
||||
| 기계 — 찾았는데 시간당 경비 없음 | 349 줄(26 종) | **0** | 풀림 |
|
||||
| 재료 이음 | 72.0 %(1,466 / 2,036) | **87.6 %**(1,784 / 2,036) | 좋아짐 |
|
||||
| 유가 · 전력 이음 | 0 %(0 / 29) | **100 %**(29 / 29) | 풀림 |
|
||||
| [우리 단가로] 블록 전부 우리 값(= 우리 정보로 만들어짐) | 원파일 71.9 %(1,596) · 엑셀 74.6 %(762) | 원파일 **85.5 %**(1,899) · 엑셀 **79.7 %**(814) | 좋아짐 |
|
||||
| [우리 단가로] 0 원 줄 있는 블록 | 원파일 236 · 엑셀 47 | 원파일 115 · 엑셀 4 | 줄어듦 |
|
||||
| STmate 기계 비목 세 줄(문서 줄 쌓기) | STC 285 블록 · 1,391 줄 | 남음 STC 2 블록 · 엑셀 6 블록(번지가 셋으로 갈린 꼴) | 풀림 |
|
||||
| 조합 기계 항목 겹침(같은 기계 항목이 여럿) | 83 블록 | **0** | 풀림 |
|
||||
| 차림 왕복 · TS 거울 | 100 % | 100 %(812-3 시험 · 7 통과) | 그대로 |
|
||||
| 1,513 M01 문서 금액 | — | 기본 1,513 같음 · 옵션 표본 18,797 중 다름 1(채택된 차이 그대로) | 그대로 |
|
||||
| 연결 판정(보조) 원파일 확정 · 근접 · 없음 · 기계 작업 | 339 · 865 · 818 · 198(15.3 %) | **463** · 784 · 761 · 212(20.9 %) · 못 셈 0 · 확정 다시 셈 다름 0 | 좋아짐 |
|
||||
| 연결 판정(보조) 엑셀 확정 · 근접 · 없음 · 기계 작업 | 205 · 260 · 472 · 84(20.1 %) | 211 · 271 · 447 · 92(20.7 %) · 못 셈 0 | 조금 |
|
||||
|
||||
## 항목별(E 표)
|
||||
|
||||
| # | 고친 것 | 화면 · 수 |
|
||||
|---|---|---|
|
||||
| 1 | 올리기 = 파일 하나 · 같은 공사 붙이기 걷음 | 엑셀이든 .estx · .STC 든 따로 새 작업 · 「같은 공사 엑셀에 붙음」 글 없음 |
|
||||
| 2 | 확장자 파일 = 단가산출표 판(코드 · 이름 · 규격 · 단위 · 재료비 · 노무비 · 경비 · 합계) + 산출근거 | 봉화 .STC 하나 → 65 블록 목록 · [내 단가산출로] = 산출근거 · 일위대가 안 만듦(EST 도 HeadMainDat 안 씀) |
|
||||
| 3 | 연결 찾기 = 엑셀 이름 규격 · 단위 꼬리 뗀 이름(「구조물터파기(인력) 경질토사 / M3」 → 「구조물터파기(인력)」) | 보이는 이름 그대로 · STmate 블록 규격도 근거 칸에 |
|
||||
| 4 | 기계 세 줄 = 기계 한 줄 · 조합 다음 줄 기계마다 한 항목 | 봉화 55 크롤러드릴 「노 무 비 · 재 료 비 · 경 비」 → 「크롤러드릴(탑승유압식) 110㎾」 한 줄 · main 360 덤프 자동덮개 시설 항목 셋 → 하나 · 금액 그대로 |
|
||||
| 5 | 시간당 경비 계산식 기계 목록에 빠진 기계 더함 | 384 대 가운데 351 대 더함(기계경비 92 · 조종원 직종 입력 18 · 조종원 없음 43 · 연료 없음 7 · 손료만 198) · 못 넣음 33 대(기계표 가격 · 계수 · 연료 종류 빔 · 잡재료율 「13∼18」 범위) · 조합(굴착기 + 대형 브레이커 · 부착용 집게) = 우리 조합 식 GC000271 · 270 한 줄 — 거창 2 [우리 단가로] 23,747 → 25,772(브레이커 0 원 풀림) |
|
||||
| 6 | 공구손료 율 비목 차이(재료비 ↔ 경비) = 참고 | 거창 41 합판거푸집 = 근접 → 확정(참고 「공구손료 비목」) |
|
||||
| 7 | 못 이은 줄 까닭 · 「없음」 띠 | 이음 없는 자원 띠 = 「대형 브레이커 (조합 — 우리 조합 식 GC000271 에 듦)」 꼴 · 문서 줄 설명 = 「이음 없음 — 자재 없음 · 엑셀 단가 그대로」 꼴 · 연결 「없음」 = 「우리 기초 없음 — 우리 정보로 셈 n/m 줄」 · 형제만 = 「조합」 |
|
||||
| 8 | 유가 = 전국평균 + 지역 옵션 · 전력 = 일반용(갑)Ⅰ 저압 + 계약종별 옵션 | 경유 · 등유 → `MO:경유:{S_지역}`(기본 전국평균) · 전력 → 114.4 원(엑셀 114.8 = 검산 칸) |
|
||||
| 9 | 재료 = 지역 꼬리 뗀 이름 · 규격 정확 일치 | 유리알 1호 고휘도 · 합판(내수) 12t(서울) · 외송각재 · 철못 N 25 · 구역화물 8.0톤 30km 이음 |
|
||||
| 10 | 가벼움 여섯 | 조합 첫 줄 칩 = 「굴삭기(무한궤도) 0.7」 · 북부청임도 33 소계 검산 맞음 · 기계 찾기 판정 없는 줄 7 → 0 · 올린 직후 409 = HP 보이기 전 산근 보관 · 엑셀 블록 「엑셀 읽기 확정」 · 전수 도우미 str() 뗌 |
|
||||
|
||||
## 남은 못 이음 — 까닭별(후)
|
||||
|
||||
| 갈래 | 까닭 | 줄 |
|
||||
|---|---|---|
|
||||
| 기계 | 후보 여럿(사람 고름 · C3 협의 중) | 88 |
|
||||
| 기계 | 못 찾음 — 별칭 대기 | 65 |
|
||||
| 기계 | 우리 기계표에 없음 | 53 |
|
||||
| 재료 | 후보 여럿(정확 일치 없음 — 접착제 · 유로폼 패널 · 각재 외송 …) | 238 |
|
||||
| 재료 | 우리 자재표에 없음 | 14 |
|
||||
| 갈래 밖 | 견적 · 운임 · 수수료(구역화물자동차 · 도선료 · 치즐 견적 …) | 59 |
|
||||
|
||||
| [우리 단가로] 0 원 줄(후) | 줄 | 뜻 |
|
||||
|---|---|---|
|
||||
| 조합 다음 줄 이음 · 덤프트럭 자동덮개시설(노무비 · 재료비 칸) | 82 | 부착 장비 = 손료(경비)만 — 노무 · 재료 칸 0 이 맞음 |
|
||||
| 조합 다음 줄 못 이음(굴착기 후보 여럿 · 별칭) | 46 | 사람 고름 대기 |
|
||||
| 재료 이음 · 우리 값 0(급수호스 · 시험 수수료 · 공기주입기 · 시스템동바리 …) | 102 | 우리 자재표 값 없음 |
|
||||
| 기계 낱 줄(발전기 노무비 칸 등) | 11 | 그 기계에 그 비목 없음 |
|
||||
|
||||
- 연결 「확정」 참고(원파일) — 수량 269 · 공구손료 비목 154(6 번) · 단가 차이 97 · 옵션 확인 94 · 수량 끝자리 72 · 품 숫자 65
|
||||
- 「없음」 = 흠 아님(요구 745) — 띠 글 「우리 기초 없음 — 우리 정보로 셈 n/m 줄」 · 근접 까닭 1위 = 줄 없음 1,361 · 기계 규격 475(후보 고르기 C3 협의 몫)
|
||||
|
||||
## 남은 흠
|
||||
|
||||
| # | 흠 | 예 | 갈래 |
|
||||
|---|---|---|---|
|
||||
| 1 | EST 「〇〇 노무비 · 재료비 · 경비」 가운데 수량 식이 다른 세 줄은 못 묶음 | main 360 덤프 재료비 × To | 원파일 로직 그대로(값 묶음으로 못 접음) |
|
||||
| 2 | STmate 세 줄이 번지 셋(A01 · A02 · A03)으로 갈린 꼴은 안 묶음 | 울진대흥 6 크로울러드릴 | 묶으면 번지 율 식이 깨짐 · 8 블록 |
|
||||
| 3 | 재료 지역 옵션 화면 칸 없음 — 서울 줄 기본으로 이음 | 합판(내수) 서울 | 자재표 = 키마다 지역(우리 기초도 서울 고정 검색어) |
|
||||
| 4 | 원파일 단가산출 목록 줄 아래 글 「일위대가 밖」 | 봉화 .STC 목록 | 일위대가를 안 만드니 늘 그 글 |
|
||||
| 5 | 연결 판정 띠(「우리 기초 없음 — 우리 정보로 셈」)는 [연결 찾기] 다시 누른 SK 부터 | 옛 SK | 기록된 판정에 칸 없음 |
|
||||
|
||||
## 화면(8005 · 5178 · 일반 계정 개인 층 · 「sub3임시」)
|
||||
|
||||
- 봉화 .STC 하나만 올림 → 단가산출표 판 65 블록 · 55 암발파 = 크롤러드릴 한 줄 · 검산 같음 · [우리 단가로] 16,629 → 16,020
|
||||
- 거창 .xlsx 하나만 올림 → 2 측구터파기(연암) = 가 · 굴착기 / 나 · 대형 브레이커 / 다 · 치즐 항목 · 브레이커 띠 「조합 — 우리 조합 식 GC000271 에 듦」 · [우리 단가로] 30,381 → 25,772 · 「같은 공사」 글 없음
|
||||
- 밝음 · 어두움 · 1366×768 가로 넘침 0 · 콘솔 오류 0(로그인 전 세션 401 하나 뺌) · 사진 = tmp(셋)
|
||||
- 만든 SK 둘 · 올린 HP 셋 지움
|
||||
@@ -0,0 +1,85 @@
|
||||
# 815-1 불러오기 이음 — 전후 전수 보고(요구 753 ~ 765 · 방향 장부 K 장 K1 · K2 · K3 · K5)
|
||||
|
||||
- 대상 = 814-1 과 같은 자 — 원파일 EST 6 · STC 7(산근 2,220 블록) + 실무 엑셀 11(1,021 블록)
|
||||
- 전 = 814-1 뒤 판(지금 코드로 다시 잼 · 814-1 보고 「후」와 수 같음) · 후 = 815-1 판(커밋 55ad1bf · 030a843 · 8edd8ad)
|
||||
- 자세한 표 = `_목록_815-1_정합성.xlsx`(요약 전후 · 못 이은 줄 = 별칭 넣을 후보 · 견적 줄 · 0 원 줄)
|
||||
- 마스터 자료 · 별칭 값 · storage 층 사본 안 건드림 · 별칭 이음은 시험 안 임시 별칭으로만 확인
|
||||
|
||||
## 요약
|
||||
|
||||
| 잣대 | 전(814-1 뒤) | 후(815-1) | 합친 뒤(815-1 + 815-2 + 815-3) | 판정 |
|
||||
|---|---|---|---|---|
|
||||
| 금액(엑셀 단가 그대로 셈 = 원본) | 원파일 2,220 · 엑셀 1,021 = 100 % | 같음 100 % | 같음 100 % | 그대로 |
|
||||
| 못 읽음 표지 | 0 | 0 | 0 | 그대로 |
|
||||
| 인력 · 형제 · 유가 이음 | 100 % · 100 % · 100 % | 같음 | 같음 | 그대로 |
|
||||
| 기계 이음 | 95.7 %(4,601 / 4,807) | 94.9 %(4,564) | **95.7 %(4,601)** — 다섯 칸 옮김으로 37 줄 돌아옴 | 옛 마스터 별칭 다섯이 아직 기계 줄 「별칭」 칸에 안 옮겨짐(815-2 몫) — 옮기면 37 줄 돌아와 95.7 % |
|
||||
| 재료 이음 | 87.6 %(1,784 / 2,036) | **88.0 %**(1,792) | 88.0 %(1,792) | 표기 맞추기로 8 줄 더 |
|
||||
| 못 이은 재료 | 252 줄 · 105 종 | **244 줄 · 100 종** | 244 줄 · 100 종 | 다섯 종 풀림 |
|
||||
| 견적 · 운임 · 수수료 줄 | 59 줄 = 「갈래 밖」 못 이음 | **59 줄 = 「견적」 그대로**(못 이음에서 뺌) | 59 줄 = 「견적」 그대로 | 우리 값 있는 자재로 이어진 것 0 |
|
||||
| [우리 단가로] 모두 우리 값 | 원파일 1,899(85.5 %) · 엑셀 814(79.7 %) | 원파일 1,853(83.5 %) + 견적만 남음 19 · 엑셀 815(79.8 %) + 12 | 원파일 **1,891(85.2 %)** + 19 · 엑셀 815(79.8 %) + 12 | 아래 「줄어든 까닭」 |
|
||||
| [우리 단가로] 0 원 줄 있는 블록 | 원파일 115 · 엑셀 4 | 원파일 155 · 엑셀 9 | 원파일 **147** · 엑셀 9 | 아래 「줄어든 까닭」 |
|
||||
| 차림 왕복 · 시험 | 100 % | 100 %(812-3 · 810-2 · 810-3 통과) | 100 % | 그대로 |
|
||||
|
||||
### 합친 뒤 달라진 수(815-3 · 같은 자로 다시 잼)
|
||||
|
||||
- 기계 이음 94.9 → **95.7 %** — 815-2 가 옛 마스터 별칭 다섯을 기계 줄 「별칭」 칸으로 옮김 · 못 찾음(별칭 필요) 102 → 65 줄 · 풀린 기계 = 아스팔트페이버(피니셔) 1.7m · 3.0m · 버킷준설기 7.46kW · 바이브레이트 봉상후렉시블
|
||||
- [우리 단가로] 모두 우리 값(원파일) 1,853 → **1,891** — 엑셀 단가 남음 → 우리 값 30 블록(위 다섯 별칭) · 0 원 줄 → 우리 값 8 블록(H-PILE · POST-PILE — 발전기 조종원 노무비 참)
|
||||
- 0 원 줄 블록(원파일) 155 → **147** — 위 8 블록
|
||||
- 우리 계가 바뀐 블록 128(올라감 98 · 내려감 30) — 올라감 = 815-3 시간당 경비 식 고름(기계표 조종원 > 0 = 조종원 든 식 먼저 · 래머 80kg · 플레이트 콤팩터 1.5ton · 진동롤러(핸드가이드식) 0.7ton · 발전기 100kW · 예 EST 되메우기및다짐(램머) 1,569 → 6,535 · 엑셀 원본 10,141 쪽으로) · 내려감 = 별칭 이음 블록
|
||||
- 금액(엑셀 단가 그대로) · 재료 · 견적 · 차림 = 그대로
|
||||
|
||||
### 「모두 우리 값」이 줄어든 까닭(블록 옮김 전수)
|
||||
|
||||
| 옮김 | 블록 | 까닭 |
|
||||
|---|---|---|
|
||||
| 우리 값 → 엑셀 단가 남음 | 30 | 아스팔트페이버(피니셔) 29 · 버킷준설기 1 — 옛 마스터 별칭 장부를 안 읽게 됨 · 815-2 가 다섯을 기계 줄 칸으로 옮기면 그대로 돌아옴 |
|
||||
| → 0 원 줄 | 45 | 치즐(0.7㎥) 39 · 치즐(0.4㎥) 6 — 전에는 「바탕 이름」 길이 **엉뚱한 치즐 0.2㎥ 줄**로 이어 값이 있어 보였음(지어 맞추기) · 이제 정확히 같은 치즐 0.7 · 0.4 줄로 잇는데 그 줄 값 칸 다섯이 빔(EST DB 자체에 값 없음 · 비고에만 「가격정보 223,000」) = 자재표 보강(J 장) 대상 |
|
||||
| 엑셀 단가 남음 → 우리 값 | 4 | 표기 맞추기 |
|
||||
|
||||
## K1 EST 원파일 자재 = 참조 코드
|
||||
|
||||
| 원파일 | 자재 자원 | 공사 직접 번호(M0000000…) | 우리 표 코드와 같음 → 그 줄로 이음 |
|
||||
|---|---|---|---|
|
||||
| main.estx(EST DB 판 그 자체) | 23,980 | 8 | **23,965 / 23,965**(다른 줄 0) |
|
||||
| 거창 · 장수 · 진안 · 임도용역 · 통영 | 239 | **235** | 0 — 나머지 4 도 우리 표에 없는 코드(아연도철선 M0004113991 …) |
|
||||
|
||||
- 찾은 것: 방향 장부 G4 의 「24,219 중 23,981(99 %)」은 거의 다 main.estx(우리 자재표를 만든 바로 그 EST DB 파일) 몫 · **실제 공사 파일 다섯의 자재는 99 % 가 공사가 매긴 직접 번호** → 코드로 잇는 몫 0 · 이름 · 별칭 길로 이음
|
||||
- 공사 직접 번호는 파일마다 다른 품목(거창 「치즐(0.7㎥)」 = M0000000001 ↔ 우리 표 M0000000001 = 「Air Hammer」) → 코드 잇기에서 뺌(시험에 굳힘)
|
||||
- 화면: 거창 .estx 단산10 줄떼심기 = 천연잔디 이름 길로 이음 · 이음 없는 자원 0
|
||||
|
||||
## K2 「별칭」 칸 읽기(풀이 정본 `master_alias.py` · 잇기 `M02_Elem_Alias.py`)
|
||||
|
||||
- 풀이 = 콤마 · 전각 콤마 「,」 · 큰따옴표 조각 통째(속 콤마 보존) · 앞뒤 빈칸 뗌 · 같은 것 한 번
|
||||
- 뜻 = 이름만 적은 별칭 → 가져온 규격이 그 줄 규격과 정규화해 같을 때 · 「이름 규격」을 적은 별칭 → 통째 같을 때 · 같은 별칭이 여러 줄 = 같은 품목 지역 줄이면 줄 지역 › 서울 · 아니면 「후보 여럿」
|
||||
- 읽는 곳(모두 같은 길) = 단가산출 불러오기 재료 · 인력 · 유가 · 기계 찾기 · 일위대가 호표 줄(자재 · 노임 · 기계)
|
||||
- 옛 층 별칭 장부를 요소 잇기에서 읽는 곳 걷음 — 기계 찾기 · 호표 자재 짝 · 단가산출 호표(장부 import 뗌)
|
||||
- 남긴 곳(815-2 몫) — 기계 [저장] 때 고른 짝을 옛 장부에 쓰는 `M02_Machine_Find.learn` · 일위대가 [확정]의 장부 쓰기(`M02_Hopyo_Build`) · 장부 화면 · 길
|
||||
- 시험(임시 별칭): 「"크레인(트럭) 15Ton"」 → 그 크레인 · 「아스팔트페이버(피니셔)」 → 규격으로 피니셔 3m · 「결속선(철선)」(서울 · 부산 줄) + #20, 0.9M/M → 서울 줄 · 규격 #8 = 안 이음 · 「특인」 → 특별인부 · 「디젤유」 → 경유 전국평균 · 같은 별칭 두 기계 = 후보 여럿
|
||||
|
||||
## K3 재료 표기 맞추기
|
||||
|
||||
- 정규화 = M/M · m/m · ㎜ = mm · 곱셈표 × · x · X · * · 지름 Φ · φ · ø · D= · 괄호 · 빈칸 · 전각 · # · 지역 꼬리(「, 서울」) — 정확히 같을 때만(가까운 규격 대신 잇지 않음) · 낱말 순서 뒤집힘(결속선(철선) ↔ 보통철선(결속선))은 별칭 몫
|
||||
- 정확 일치를 옛 「바탕 이름」 길보다 먼저 봄(엉뚱한 치즐 0.2 잇기 바로잡음)
|
||||
- 풀린 다섯 종 = PVC파이프(일반관),서울 D=100mm(VG1) · 강관동바리 Φ48.6x2.4,L=2.6~4.0 · 보통철선(결속선) #20 , 0.9M/M · 산림종자(비수리) · 산림종자(안고초)
|
||||
- 남은 못 이은 재료 244 줄 · 100 종 = 후보 여럿 230 · 자재표에 없음 14 → 목록 「못 이은 줄」 시트(가져온 이름 · 규격 · 쓰임 수 · 가까운 우리 줄 셋 · 넣을 별칭 예)
|
||||
|
||||
## K5 견적 줄
|
||||
|
||||
- 대상 = 원파일 EST 견적(국가지점번호판 · 측량 수수료 · 도선료) · STmate · 엑셀 고정 코드 줄(구역화물 · 하차비 · 덤프자동덮개 · 치즐 견적) — 59 줄
|
||||
- 우리 자재에 이어지고(코드 · 별칭 · 정확 일치) **우리 값이 있으면** 재료 줄(우리 값 · 엑셀 값은 검산) · 아니면 그대로(이름 · 값) + 표지 「견적 — 그대로(이름 · 값) · [단가 갱신] 대상 아님」 · 문서 줄 설명 같은 글 · 화면 「견적 n」 배지(「이음 없는 자원」 수에서 뺌)
|
||||
- 지금 59 줄 모두 그대로 — 치즐(0.7m3) 견적 5 줄은 우리 치즐 0.7 줄이 있으나 값 칸이 비어 견적 값 유지(0 원 막음)
|
||||
|
||||
## 화면(8005 · 5178 · 일반 계정 개인 층 · 「sub3임시_거창.estx」)
|
||||
|
||||
- 단산22 국가지점번호판 → 띠 「견적 2」(파랑) · 「국가지점번호판 (견적 — 그대로(이름 · 값) · [단가 갱신] 대상 아님) · 이음 고르기」 · 검산 같음 · 이음 없는 자원 배지 없음
|
||||
- 밝음 · 어두움 · 1366×768 가로 넘침 0 · 콘솔 오류 0(로그인 전 세션 401 하나 뺌) · ORCA 자기 탭 확인 · 사진 = tmp(셋)
|
||||
- 만든 SK 셋 · 올린 HP 둘 지움(같은 계정 다른 창이 만든 SK-000004 ~ 7 은 그대로)
|
||||
|
||||
## 남은 흠 · 알림
|
||||
|
||||
| # | 내용 | 갈래 |
|
||||
|---|---|---|
|
||||
| 1 | 옛 마스터 기계 별칭 다섯이 칸으로 옮겨지기 전까지 기계 37 줄 · 30 블록 못 이음 | **풀림**(합친 뒤 95.7 %) |
|
||||
| 2 | 치즐 0.7 · 0.4 등 우리 자재 줄 값 칸 빔 — 정확히 이으니 0 원 줄 102 → 153 | 자재표 보강(J 장 · 값 출처 미정) |
|
||||
| 3 | 견적 값묶음 줄 공식 칸에 「[object Object]」 글(814-1 전부터 · 값묶음 줄 꼴) | 화면 흠 · 이번 고침 밖 |
|
||||
| 4 | test_778_5 일위대가 기계 줄 표(이어짐 162 · 고를 것 3 · 못 이음 25)가 굳힌 수(180 · 10 · 0)와 다름 — 고치기 전 판도 같은 수(812-9 뒤부터) | **풀림(815-3)** — 진짜 흠: 호표 기계 글 「운 반 : 덤프트럭(4.5Ton)」 · 「…[hr] : 90%」 머리 · 꼬리를 812-9 찾기가 안 뗌 → 뗌 · 이어짐 187 · 고를 것 1 · 못 이음 2(조합 한 줄 · 괄호 깨짐) |
|
||||
@@ -0,0 +1,100 @@
|
||||
# 817-1 불러오기 재료 찾기 — 전후 전수 보고(요구 773-5 · 774 · 방향 장부 O 장)
|
||||
|
||||
- 대상 = 815 와 같은 자 — 원파일 EST 6 · STmate 7(2,220 블록) + 실무 엑셀 11(1,021 블록) · 재는 법 = 815-1 과 같음(블록마다 금액 · 요소 이음 · [우리 단가로])
|
||||
- 전 = 817-1 앞 판 · 후 = 커밋 bb243af
|
||||
- 자세한 표 = `_목록_817-1_재료찾기.xlsx`(과녁 100 줄 · 새로 이어진 줄 · 남은 못 이음 · 전후)
|
||||
- 별칭 값 · M01 마스터 자료 · storage 층 사본 안 건드림 · 인력 · 기계 · 유가 길 그대로
|
||||
|
||||
## 요약
|
||||
|
||||
| 잣대 | 전 | 후 |
|
||||
|---|---|---|
|
||||
| 재료 이음(전체) | 88.0 %(1,792 / 2,036) | **93.5 %(1,904 / 2,036)** |
|
||||
| 못 이은 재료 | 244 줄 · 100 종 | **132 줄 · 49 종** |
|
||||
| 새로 이어진 줄 | — | 112 줄 · 51 종(목록 시트 「새로 이어진 줄」) |
|
||||
| 끊긴 이음 | — | 0 |
|
||||
| 이은 키가 바뀐 줄 | — | 28 줄 — 치즐 0.7㎥ 이 엉뚱한 「치즐(0.2㎥)」 에서 「치즐(0.7㎥)」 로(바로잡음) |
|
||||
| 금액(엑셀 단가 그대로 셈) | 3,241 블록 100 % | 같음 100 %(셈 바뀐 블록 0) |
|
||||
| 검산 틀린 줄 | 929 | 929(그대로) |
|
||||
|
||||
### 원천 셋
|
||||
|
||||
| 원천 | 전후 | 인력 | 기계 | 재료 | 유가 | 형제 | 모두 우리 값 | 우리 값 + 견적 | 이어짐 · 우리 값 빔 | 엑셀 단가 남음 | 0 원 줄 |
|
||||
|---|---|---|---|---|---|---|---|---|---|---|---|
|
||||
| EST 원파일(1,849) | 전 | 100 | 97.2 | 99.4 | 100 | 100 | 1,622 | 6 | 100 | 75 | 46 |
|
||||
| | 후 | 100 | 97.2 | **99.7** | 100 | 100 | 1,622 | 6 | 100 | 75 | 46 |
|
||||
| STmate 원파일(371) | 전 | 100 | 95.2 | 40.2 | — | 100 | 278 | 13 | 1 | 79 | 0 |
|
||||
| | 후 | 100 | 95.2 | **59.8** | — | 100 | **285** | 13 | 11 | **62** | 0 |
|
||||
| 실무 엑셀(1,021) | 전 | 100 | 93.2 | 54.3 | 100 | 100 | 823 | 12 | 9 | 177 | 0 |
|
||||
| | 후 | 100 | 93.2 | **78.2** | 100 | 100 | **854** | 12 | 27 | **128** | 0 |
|
||||
|
||||
- 「이어짐 · 우리 값 빔」 = 우리 자재 줄에 이었는데 그 줄 값 칸이 빈 블록(요구 776 — 흠 아님 · 자재표 보강 몫) · 늘어난 28 블록 = 모두 치즐 0.7㎥(값 칸 빔)
|
||||
- 블록 옮김: 엑셀 단가 남음 → 모두 우리 값 43(STmate 7 · 엑셀 36) · 엑셀 단가 남음 → 이어짐 · 우리 값 빔 23 · 모두 우리 값 → 이어짐 · 우리 값 빔 5(치즐 0.2 → 0.7 바로잡음)
|
||||
|
||||
## 엔진 — 새 단 셋(`M02_Hopyo_MatSame` · 재료 길 = 별칭 › 정확 일치 › 바탕 이름 유일 짝 › ㉮ › ㉯)
|
||||
|
||||
- ㉮ 이음(자동) = 이름이 같은 품목 + 규격 수 묶음 같음 + 규격 낱말 안 부딪힘 + 그런 우리 품목 하나(지역 · 관급/사급 줄은 한 품목)
|
||||
- 이름 같음: 빈칸 · 괄호 · 지역 · 관급/사급 꼬리 · 등급(1급) 뗌 · 점 · 빈칸 영문(P.V.C) · 소리 표기 넷(칠렌 → 틸렌 · 매쉬 → 메쉬 · 신나 → 신너 · pipe → 파이프) · 괄호 차례(유로폼(패널) = 유로폼패널) · 괄호 이름(서비스볼트(고장력볼트) → 고장력볼트 — 가져온 바탕 이름이 우리 표에 없을 때만) · 규격에 든 이름(안고초 → 산림종자 안고초)
|
||||
- 약한 같음(수 묶음 같음 · 단위 배일 때만): 우리 이름 끝 = 가져온 이름(지수판 → PVC지수판 · 고무테이프 → 절연 면고무테이프 · 철물 직교자재 → 조임철물) · 우리 이름 = 가져온 이름 앞(구역화물자동차 → 구역화물 · 뒤 덧말이 우리 낱말이 아닐 때만)
|
||||
- 수 묶음: 차례 무관 · KS 번호 · 제곱 꼬리 뗌 · 천 단위 쉼표 · 같음 › 낱마다 단위 배(3.6×3.0×3.0 = 30×30×3600) › 한쪽이 다른 쪽에 듦(측구수로관 300×300×2000×120 → 300×300×120 · 이름이 굳세게 같을 때만)
|
||||
- 낱말 부딪힘 = 두 쪽 다 괄호 · 규격 한글 낱말이 있고 두 글자 묶음이 안 겹침(연강용 ↔ 주철용 · 표준 ↔ 단부구간 · 스티로폼 ↔ ABS코팅폼)
|
||||
- ㉯ 후보 좁힘 = 이름 같은 묶음에서 규격 어근(초본류 → 초본) 품은 줄 › 수 묶음 닮음 › 바탕 같음 › 덧말 적은 줄 차례 + 옛 후보 가운데 규격 수 묶음(둘 이상) 같은 줄 먼저 · 자동 이음 아님
|
||||
- 화면: 못 이은 재료 까닭 글에 1 순위(「자재 후보 여럿 — 고름 · 1 순위 종 자 초본위주형」) — [이음 고르기] 창은 이름 찾기 그대로
|
||||
- 사전 = 소리 · 영문 넷 · 덧글 낱말 다섯(이내 · 이하 · 이상 · 국산 · 포함) 한 곳 · 자재마다 박은 예외 없음
|
||||
|
||||
## 과녁 100 줄(브레인 과녁을 우리 자재표로 다시 확인)
|
||||
|
||||
| 과녁 | 줄 | 같음 | 다름 |
|
||||
|---|---|---|---|
|
||||
| 이음 | 65 | 49 | 16 — 1 순위는 맞음 10 · 그 밖 6 |
|
||||
| 후보 1 순위(종자) | 5 | 5 | 0 |
|
||||
| 후보(사람이 고름) | 26 | 25 | 1 |
|
||||
| 잇지 않음 | 4 | 4 | 0 |
|
||||
|
||||
과녁과 다른 줄(맞추지 않음 · 까닭):
|
||||
|
||||
| 행 | 가져온 줄 | 과녁 | 결과 | 까닭 |
|
||||
|---|---|---|---|---|
|
||||
| 3 · 7 | 각재 외송 · 각재 (외송) | 이음 | ㉯ 1 순위 각재(외송) | 규격 수 없음 · 빔 → 수 묶음 다름 |
|
||||
| 4 | 요소 | 이음 | ㉯ 1 순위 요소 N:46% | 가져온 규격 빔 |
|
||||
| 31 | 유압식드릴스리브 T38×190mm | 이음 | 후보 1 순위 유압드릴스리브 | 「유압식」 ↔ 「유압」 이름 다름 |
|
||||
| 40 | 안전표지판 삼각,D=900 | 이음 | ㉯ 1 순위 교통안전표지판 900,삼각 | 같은 수 · 낱말 줄 둘(LED 교통안전표지판(태양광) 900,삼각) |
|
||||
| 47 | 강관써포트 V4 2600-4000mm | 이음 | 후보 1 순위 강관동바리 | 이름 다름 |
|
||||
| 55 | 아세틸렌 용접용 | 이음 | ㉯ 1 순위 아세틸렌 98%(용접용) | 규격 수 없음 ↔ 98% |
|
||||
| 90 · 92 | 중하중그레이팅 395×995×52 · 595×995×67 | 이음 | 후보 1 순위 스틸그레이팅(중하중용) 같은 치수 | 이름 다름 — 같은 수 묶음 후보 먼저(전에는 다른 치수 OK그레이팅이 1 순위) |
|
||||
| 98 | 공사안전PE드럼 500×800 | 이음 | 후보 1 순위 PE드럼(일체형) Φ500×800 | 가져온 이름 앞 덧말은 안 뗌(PVC용접봉 ↔ 용접봉 틀린 이음 막음) |
|
||||
| 67 | 와이어매쉬 #6 100×100 | 이음 MT007130(관급) | 이음 MT007136(서울) | 같은 품목 변형 — 별칭 · 정확 일치 길과 같은 차례(서울 먼저) |
|
||||
| 54 | 다이아몬드톱날 Ø14"×3.2t | 이음 블레이드 | 후보(1 순위 다름) | 이름 다름 — 별칭 몫 |
|
||||
| 79 · 81 | 참싸리(박피) · 참싸리 박피 | 이음 산림종자 참싸리 | 못 찾음 | 박피 ↔ 풍화암,강마사토 덧말 부딪힘 — 별칭 몫 |
|
||||
| 83 | 퇴비(천연부엽토) 20㎏/포 | 이음 천연부엽토 | 후보(1 순위 퇴비 1등급) | 「퇴비」가 우리 표에 따로 있어 괄호 이름 길 안 씀(보조강재(열연강판) → 열연강판 같은 이음 막음) |
|
||||
| 95 | 삼각표지판 삼각,900mm | 이음 | 후보(1 순위 다름) | 이름 다름 — 별칭 몫 |
|
||||
| 93 | 잔디 0.18×0.18×0.01 | 후보 | **이음 천연잔디(한국형) 0.18*0.18*0.01** | 우리 표에 수 묶음 같은 줄 하나 — 과녁은 잔디 0.03 줄만 봄 · 브레인 확인 필요 |
|
||||
|
||||
- 「가까운 줄 제안」 바로잡음: 신나 → 신너(조합페인트용) KSM-6060(2종) ㉮(전 = 신나무 1 순위) · 중하중그레이팅 → 같은 치수 1 순위
|
||||
- 틀린 이음 막는 견본(시험): 용접봉(연강용) ↔ 주철용 · PVC용접봉 ↔ 용접봉 · 어닐링철선 #6 ↔ #8 · 잔디 0.01 ↔ 0.03 · 표지판지주 3.5M ↔ 3000 · 강관비계 6m ↔ 4.0m · 신나 ↔ 신나무 — 모두 안 이음
|
||||
|
||||
## 새로 이어진 줄(51 종 · 112 줄) — 틀린 이음 0
|
||||
|
||||
- 전부 목록 시트(가져온 이름 · 규격 → 우리 키 · 이름 · 규격 · 맞은 까닭) · 한 줄씩 읽음 — 과녁과 다른 이음은 93 잔디(위 표) · 67 와이어매쉬 변형뿐
|
||||
- 과녁 밖에서 새로 이어진 것 = 0(새 이음 51 종 모두 과녁 100 줄 안)
|
||||
|
||||
## 별도계상
|
||||
|
||||
- 단가산출 불러오기 길(엑셀 산근 · 원파일): 전에는 표지 없이 「자재 후보 여럿」 못 이음 → 이제 요소 잇기 없음 + 표지 「참고 · 별도계상 · 품목은 프로젝트에서」(일위대가 호표 길 `M02_CheckApart` 와 같은 글) · 못 이음 띠 「파형강관(별도계상) (별도계상 · 품목은 프로젝트에서) · 이음 고르기」
|
||||
- 원가 합 밖 = 그대로(줄 값 0 · 원본 금액 그대로) · 0 원 줄 수에 안 듦(엑셀 값 0 줄) · 대상 = 동부청영월 파형강관 · 이형철근 8 줄(엑셀 · STmate)
|
||||
- 일위대가 호표 길 · 프로젝트로 복사(`M02_Doc_Copy` 합밖) 은 전부터 되어 있음
|
||||
|
||||
## 화면(8001 · 5174 · 일반 계정 개인 층 · 「sub1임시_동부청영월.xlsx」 · 「sub1임시_울진소광.xlsx」)
|
||||
|
||||
- 단산58 유로폼 설치 · 해체 — 「유로폼(패널) 600*1200」 줄 → MT000467 유로폼패널(저장된 SK 이음 키로 확인) · 이음 없는 자원 0
|
||||
- 단산71 파형강관부설 — 「파형강관(별도계상) (별도계상 · 품목은 프로젝트에서)」 · 금액 15,898 그대로
|
||||
- 단산1492 씨뿌리기(울진소광) — 「종자(초본류) … 1 순위 종 자 초본위주형」 · 「종자(목본류) … 1 순위 종 자 목본군락형」(ORCA 탭 사진 `tmp/q17/ui/orca_1492.png`)
|
||||
- 밝음 · 어두움 · 1366×768 가로 넘침 0 · 콘솔 오류 0(로그인 전 세션 401 하나 뺌) · 만든 SK 셋 · 올린 HP 둘 지움
|
||||
|
||||
## 남은 것
|
||||
|
||||
| # | 내용 | 갈래 |
|
||||
|---|---|---|
|
||||
| 1 | 일위대가 호표 화면(일위대가표 시트) 줄 판정은 다른 길(`M02_Hopyo_Units`) — 안고초 · 참싸리 등 아직 분홍(못 이음) | 이번 고침 밖 |
|
||||
| 2 | 남은 못 이은 재료 132 줄 · 49 종 = 이름 다름 · 규격 다름 · 우리 표에 없음 → 별칭 몫(목록 시트) | 사용자 별칭 |
|
||||
| 3 | 93 잔디 0.01 → 천연잔디(한국형) 0.01 이음 맞는지 | 브레인 확인 |
|
||||
@@ -565,3 +565,431 @@ M01은 지금 상당히 잘 개발이 되었어. 일위대가 불러오기의
|
||||
## 요구 713 (11-09 · 사용자 · 제안대로 진행(엑셀 읽기기 새로 + 연결 기준 = 로직) · 가져온 단가산출이 좌측에서 읽히게(나열만 X))
|
||||
|
||||
다음으로 중요한 점은 경쟁사의 단가산출을 가져왔다 가정하고 현재의 경쟁사 단가산출은 좌측영역에서 제대로 읽기가 힘든 그냥 노무비, 경비, 재료 1,2, 3 형태로 죽 나열만 되어있음. 분석도 중요하지만 보여지는것도 중요함. 너의 제안대로 잘 끝까지 진행해줘
|
||||
|
||||
## 요구 714 (11-09 · 사용자 · 판단 셋 답 — 1 보류 · 2-1 율 값 = 값(로직 아님) · 2-2 기계 이름 = 별칭 조사 · 3 1 원은 로직만 맞으면 됨)
|
||||
|
||||
1번은 생각할 시간이 필요.
|
||||
2-1번 율값 관련 내가 로직과 값을 구분하라고 했잖아. 로직은 어떤 계산을 할때 뭐를 곱하는지 뭐를 더할지 같은 것들이고 여기에 사용된 값은 결국 언제든 업데이트 될수 있다고 불러오기 기능들에서에서 초기에 검산을 위해서는 겂을 사용하겠지만 결국은 우리의 m01의 값을 사용할 수 있어야해. 그게 현재 m02의 단가산출에 있는 값 업데이트 기능이고 이해해?
|
||||
2-2번 기계 이름이 다를경우 별칭을 사용하는게 어떨까해. 우리는 품셈원문의 이름을 사용하고 있기도 하고 m01 단가산출에도 별칭으로 경쟁사와 정보가 다를경우 승인하고 별칭으로 등록하는게 있잖아. 조사해봐
|
||||
3. 1원 차이는 로직만 맞다면 문제 없음.
|
||||
|
||||
## 요구 715 (11-09 · 사용자 · 수량 추가 설명 요청 · 1번 계속 대기 · 기계 230 블록 뜻 물음)
|
||||
|
||||
수량 관련은 추가 설명 요청
|
||||
|
||||
1번 계속 대기
|
||||
2번 기계 230 블럭은 경쟁사 프로그램에서 만들어둔 기계의리스트라는 말임?
|
||||
|
||||
## 요구 716 (11-09 · 사용자 · 하나씩 — 기계 230 블록: 굴삭기 0.7 = 바스켓 크기 · 기계 옵션값 변경 또는 미리 만들어 두기 · 확인부터)
|
||||
|
||||
일단 하나씩 하자. 기계230블럭에서 굴삭기 0.7에서 0.7은 바스켓의 사이즈로 보이는데 기계의 옵션값 변경을 하면 되는거 아닌가? 아니면 미리 만들어 두던가 확인부터 해봐.
|
||||
|
||||
## 요구 717 (11-09 · 사용자 · 우리 단가산출에서 기계 · 옵션 변경이 안 되는 게 문제 — 기계가 들어오면 상단 옵션에 기계 선택 · 하위 옵션 · 그 뒤 연결 찾기 방향 · 확인부터)
|
||||
|
||||
1. 그것보다 중요한 건 우리 단가산출에서 기계를 변경하거나 옵션을 변경하는게 안되는게 문제아니야? 그렇다면 기계가 단가산출에 들어오면 상단 옵션에 기계선택이나 하위 옵션선택이 되어야하는거 아닌가?
|
||||
2. 그리고 나서 연결찾기의 방향이 결정될것같은데?
|
||||
3. 확인부터 해봐.
|
||||
|
||||
## 요구 718 (11-09 · 사용자 · 기계 줄 누르면 목록이 나옴 → 옵션 없어도 됨 · 문제는 연결을 못 찾는 것 · 조회어 「굴착기(무한궤도) 0.7㎥」 가 문제 아닌지 · 기계 못 연결하는 이유 다시 확인)
|
||||
|
||||
지금 확인했는데 좌측영역에서 기계 줄에서 지정된 기계를 눌렀더니 옵션 별로 항목 리스트가 나오는데 이러면 옵션이 없어도 되는거잖아. 그럼 문제는 옵션 여부가 아니라 연결을 제대로 찾지 못하는게 아닌가? 그리고 추가로 좌측영역 기계를 눌렀을때 굴착기(무한궤도) 0.7㎥ 이렇게 조회를 하는데 이게 문제 아니야? 기계를 못 연결하는 이유를 다시 확인해봐.
|
||||
|
||||
## 요구 719 (11-09 · 사용자 · 기계 예들 직접 찾아 보니 대형 브레이커 + 굴착기 빼고 다 찾을 수 있음 · 옛 이름은 별칭 사용 가능 · 조합 기계 = 설비 둘을 각각 가져오기)
|
||||
|
||||
너의 알려준 낱말을 모르는 예와 후보가 여럿인 예를 내가 직접찾아 봤는데 대형 브레이커 + 굴착기 만 제외하면 다 려었이 아니라 다찾을 수 있겠는데? 그리고 경쟁사 프로그램이 예전프로그램이라 찾기 힘들면 별칭을 사용해도됨. 다음으로 2개의 기계의 조합을 대현 브레이커, 굴착기로 표현한게 아닐까? 우리는 설비 2개를 각각 가져오면 맞을 것같다는 생각이 드네.
|
||||
|
||||
## 요구 720 (11-09 · 사용자 · 수정을 어떻게 요청했는지 물음)
|
||||
|
||||
뭐지 어떻게 수정을 요청한거야?
|
||||
|
||||
## 요구 721 (11-09 · 사용자 · 작업 이어서 · 기계 · 옵션 때문에 품셈 원문 제목과 다르게 표현된 단가산출 기초 조사 — 200 개쯤 예상 · 1,300 → 1,500 늘어난 까닭)
|
||||
|
||||
같은 기종인데 규격만 다른 경우은 지금의 수정으로 가능한거 아니야?
|
||||
|
||||
1. 작업은 이어서 진행, 문제는 없어보임.
|
||||
2. 우리의 단가산출은 기계나 특정 옵션에 의해 품셈 원문 제목과는 다르게 표현된 항목들이 거기에 해당 될것 같은데. 그것때문에 제대로 연결이 안되는게 아닐까? 어떤게 있는지 먼저 조사가 필요해봐. 그렇다 하더라도 품셈원문 제목에 품셈원문 제목이 있다면 모를까 하지만 제목에 기계가 들어가는 경우는 드물것같아. . 내가 예상하기로는 200개 정도 될것같은데 확인해봐. 이전에는 1300개정도에서 갑자기 1500개로 단가산출 기초가 늘었거든.
|
||||
|
||||
## 요구 722 (11-09 · 사용자 · 기초 이름 정리 방향 다섯 — 수정 말고 대답만 · 방향성부터)
|
||||
|
||||
1. 제목 + 옵션, 조건 덧말은 조심해야 원문제목과 대조할 필요가 있어보임
|
||||
2. 제목과 다르지만 분기가 필요해 셋으로 나눈건 괜찮음.
|
||||
3. 보조문서는 기계와 연계된 정보이니 단가산출이 아닌걸로 봐야하는게 아닌지. 자동으로 연결될 것같은데?
|
||||
4. 기계 이름이 품셈 제목에 없는데붙는 경우는 수정해야 될것같음.
|
||||
5. 위에처럼 정리하고 나면 이름으로 구분안되는 정보들이 나올텐데 이때는 삭제하는게 맞겠지?
|
||||
수정하지말고 대답해줘. 방향성 부터 봐야지.
|
||||
|
||||
## 요구 723 (11-09 · 사용자 · 방향 다섯 동의(3 은 둘 자리 협의) · 원문 패널 md 경로 삭제 · [주] 글자 크기 반대로 · 반영 · 다시 조사 · 작업 이어서 — sub3 과 충돌 없으면 다른 워크트리에)
|
||||
|
||||
1. 동의
|
||||
2. 동의
|
||||
3. 동의는 하나, 단가산출 기초 리스트에서 빼서 어디에 둘건지는 협의 필요
|
||||
4. 동의
|
||||
5. 동의
|
||||
추가로
|
||||
6. 지금 단가산출의 우측 공통 원문 패널보기 내용 중에 md파일의 경로를 알려주고 있음. 이부분은 다른 워크트리에게 경로를 볼필요없으니 삭제해달라고 해줘.
|
||||
7. 원문 패널의 내용 중 [주] 내용의 글자가 더크고 본문의 내용이 작음. 반대가 되는게 맞음.
|
||||
위 내용을 반영하고 다시 조사하고 작업을 이어서 하는데 너가 판단해보고 현재 작업중인 서브3과 충돌이 없다면 다른 워크트리에게 작업을 시키자.
|
||||
|
||||
## 요구 724 (11-09 · 사용자 · 보조 문서를 단가산출 목록에서 빼면 어디에 둘지 의견 물음)
|
||||
|
||||
단가산출 목록에서 빼면 어디다 두는게 좋을것같아?
|
||||
|
||||
## 요구 725 (11-09 · 사용자 · 보조 문서 자리 = 권장으로 해도 될 듯 · 조사해 맞는지 먼저 보고 판단 · 「정할 것」 설명 필요)
|
||||
|
||||
1. 권장으로 해도 될것같음. 하지만 조사해보고 맞는 먼저 보고 판단하자.
|
||||
2. 정할것에 대한 설명이 필요.
|
||||
|
||||
## 요구 726 (11-09 · 사용자 · 전용 작업량도 기계 항목으로 · 작업량당 기계경비 예시 요청 · 기계 무관 = 제안대로 · 토사절취(백호 0.7㎥) 대화창 출력 · 1~4 한 번에 진행 예정 — 일단 대기)
|
||||
|
||||
1. 전용 일지라도 결과적으로는 다른곳에서 사용할수도 있으니 기계라면 당연히 기계항목으로 들어가는게 맞아보임. 현재는 하나지만 나중에는 늘어날수도 있잖아.
|
||||
2. 작업량당 기계경비는 어떤걸말하는것임? 예를 들어 보여줘.
|
||||
3. 기계무관은 너의 제안대로
|
||||
4. 토사절취(백호 0.7㎥) 단가산출을 대화창에 출력해줘.
|
||||
5. 1~4번은 한번에 진행할 생각이야. 그러니 일단대기하고 4번을 대화창에 출력
|
||||
|
||||
## 요구 727 (11-09 · 사용자 · 토사절취 예시의 문제가 무엇인지 물음 — 시간당이 아니라 단위당으로 나오는 것이 문제냐)
|
||||
|
||||
이게 뭐가 문제야? 원래는 기계의 돈은 시간당으로 나와야하는데 단위면적당으로 나온다는 거야?
|
||||
|
||||
## 요구 728 (11-09 · 사용자 · 신규 단가산출에 기계 하나만 지정하면 작업량당 기계경비와 같은 것)
|
||||
|
||||
아니 그러니까 내가 만약에 신규 단가산출을 만들고 거기에 기계만 하나 지정하면 같은게 되는거잖아.
|
||||
|
||||
## 요구 729 (11-09 · 사용자 · 결국 표현 단위 차이 — 단위 환산 로직을 단가산출 기초에 두는 게 맞지 않나)
|
||||
|
||||
결국은 표현의 단위차이라는 말 아니야? 그렇다면 표현하는 단위 환산로직을 단가산출 기초에 넣어두는게 더 맍지 않아?
|
||||
|
||||
## 요구 730 (11-09 · 사용자 · 표로 다시 · 짧게)
|
||||
|
||||
테이블로 다시 표현해봐 . 이해하기 힘들어. 짧게 설명해줘.
|
||||
|
||||
## 요구 731 (11-09 · 사용자 · 나눔 맞는 것 같음 · 단가산출 기초 화면의 공식 틀 추가는 필요 없어진 것인지)
|
||||
|
||||
맞는것같네. 그럼 이제 단가산출 기초화면에서 공식 틀 추가는 필요없어진 것임?
|
||||
|
||||
## 요구 732 (11-09 · 사용자 · 「식」으로 변경 · 작업 진행 · 진행 뒤 엑셀 불러오기에서 더 결정할 것 알려 달라)
|
||||
|
||||
식으로 변경하자. 이제 작업 진행하자.
|
||||
진행 후 엑셀불러오기와 관련하여 뭐를 더 결정해야하는지 알랴줘.
|
||||
|
||||
## 요구 733 (11-09 · 사용자 · 결정 목록 여섯에 대한 답 · 물음 + 드래그 그룹 이동 확인 — 수정 아님 · 조사부터)
|
||||
|
||||
1. 이유 짧게 설명
|
||||
2. 규격이 다른 같으 기종의 경우 가져온 걸로 우리의 기계값을 바꾸면 되는거 아닌지.
|
||||
3. 반올림 자리만 다른 경우 계산에는 정밀도 높게 계산하는지 확인하고 계산에 문제 없다면 확정. 품숫자가 다른 4개라는건 뭔지 짧게 설명
|
||||
4. 작업량 당 기계경비의 경우에는 단가산출이 어떤식으로 제목이 나오는지 확인. 기계만 있는 단가산출도 필요는 해보이는데 문제는 우리의 단가산출은 자유롭게 기계를 변경할 수 있음. 그렇다면 만약 단가산출 기초 제목으로 M02에서 찾을텐데 이때 기계명이 있는 이름의 단가산출이 있다면 사용자는 해당 단가산출 정보만 있을것으로 오해할것이기 때문에 6개의 단가산출이 기계명으로만 구분되어 있는 단가산출이라면 그냥 통합도 가능할 것으로 예상되어 물어봄(수정하라는 말은 아님)
|
||||
5. 별칭 승인의 승인요청 시점이 사용자들이 저장하여 단가산출을 저장한 시점으로 보이는데 맞음? 그리고 별칭은 있어야 되는건 맞는데 혹시 경쟁사 프로그램의 출력물로 찾을 경우 이제 빈도가 줄었을 것으로 보임. 어느정도 수준인 확인
|
||||
6. 굴삭기 + 브레이카 는 사용자가 그룹 구분 없이 사용한다면 한 덩이로 그룹으로 구분한다면 자동으로 구분되겠지. 일반적으로는 구분되게 사용하는게 맞는것같음.
|
||||
7. 드래그로 그룹이동 되는지 확인
|
||||
바로 수정작업 아님. 조사부터 진행.
|
||||
|
||||
## 요구 734 (11-09 · 사용자 · 결정 답 — 소계는 우리 양식 · 수량 끝자리 뜻 물음 · 품 숫자 = 우리 값이 최신(가져오면 우리 값) · 작업량당 기계경비 단가산출 불필요 의견 물음 · 별칭 8 목록 요청(초기 별칭 직접 · 승인까지) · 6 · 7 문제 없음 · 1~5 한 번에 진행 예정 — 먼저 대답)
|
||||
|
||||
1. 소계는 결국 항목들의 합계형식일 뿐 우리양식 맞춰 표기해도 문제 없을것같다는 생각이 듬.
|
||||
2. 수량 끝자리라는게 뭐임?
|
||||
3. 품 숫자가 다르다는건 결국 품셈이나 요소들에 지정된 값이 다르다라는 의미지? 그렇다면 우리가 더 최신이 맞음. 그래서 가져오기 전 비교할때는 해당정보로 계산하겠지만 가져오면 우리값으로 업데이트 되는게 맞다고 대답한 적이 있음. 그리고 만약 그래도 사용자가 이전값으로 사용하겠다면 우리 단가산출 양식에는 값변경을 할수 있으니 OK
|
||||
4. 작업량당 기계경비는 결국 이걸 활용하여 다른 단가산출들에서 사용하고 있는거잖아. 기계로 넣어서 사용하고 있는거잖아. 그렇다면 작업량당 기계경비라는 단가산출을 만들어둘 필요가 없어보이네 너의 생각은? 어차피 경쟁사 데이터 불러오기 기능에도 우리 단가산출 표현에 문제도 없을거고 있으면 되려 혼란을 줄것같아서 말이야.
|
||||
5. 별칭 필요한 8개의 리스트를 보여주면 초기 별칭을 내가 넣어줄게. 승인처리까지 했으면 함.
|
||||
6. 문제 없음.
|
||||
7. 문제 없음.
|
||||
1 ~ 5번은 한번에 진행할거야. 가능한 경우에는 다른 워크트리를 사용해도됨. 그러니 먼저 대답부터
|
||||
|
||||
## 요구 735 (11-09 · 사용자 · 1~3 동의 · 작업량당 기계경비 = 필요하면 기계 하나 넣어 만들어 씀 · 별칭 아홉 답)
|
||||
|
||||
1. 동의
|
||||
2. 동의
|
||||
3. 동의
|
||||
4. 작업량당 기계경비는 본인이 필요한 경우 기계하나만 넣고 만들어 사용하면 됨.
|
||||
5. 별칭 확인
|
||||
1) 후보맞음.
|
||||
2) 나중에 내가 승인(어디 단가산출에 있는지 대화창에 출력)
|
||||
3) 후보맞음.
|
||||
4) 엔진(디젤) 이렇게 되어 있음. 가솔린은 엔진(가솔린)으로 되어 있음.
|
||||
5) 나중에 내가 승인(어디 단가산출에 있는지 대화창에 출력)
|
||||
6) 나중에 내가 승인(어디 단가산출에 있는지 대화창에 출력)
|
||||
7) 워터젯트
|
||||
8) 나중에 내가 승인(어디 단가산출에 있는지 대화창에 출력)
|
||||
9) 후보맞음
|
||||
|
||||
## 요구 736 (11-09 · 사용자 · 바이브레이터 규격이 하나면 엔진식)
|
||||
|
||||
바이브 레이터에 규격은 하나임? 하나라면 엔진식이 맞음.
|
||||
|
||||
## 요구 737 (11-09 · 사용자 · 남은 결정 사항 물음)
|
||||
|
||||
남은 결정사항은 뭐가 있어?
|
||||
|
||||
## 요구 738 (11-09 · 사용자 · 별칭 넷 = 단가산출 만들다 나오면 승인(할 일 없음) · 원문 패널 표 글자 16 · 제목 통합(그림 `요구738_원문패널_제목중복.png`) · 조합 기계 = 개별 그룹으로 · 공식 연결 분리 가능한지 조사 · 반영 뒤 단가산출 불러오기 정합성 확인)
|
||||
|
||||
지금 정할 수 있는 것.
|
||||
1. 지금 하겠다는게 아니라 단가산출을 만들면서 해당내용이 나오면 내가 승인하겠다는거야. 그러니 이제 너가 할 일은 없음.
|
||||
2 .표의 글자도 16으로 변경
|
||||
2-1. [Image #1]중복표기가 많음 제목을 통합해줘. 건설품셈 공통 7-1-1 메쌓기('12, '19, '26년 보완) 형태면 될것같음.
|
||||
위 내용도 같이 반영, 반영완료
|
||||
3. 조합기계는 우리 단가산출로 가져올때 개별 그룹으로 하나씩 가져오는게 좋을것같음.
|
||||
4. 공식을 연결해야 단가가 나올것같은데 분리할 수 없을것같은데 조사
|
||||
5. 위내용까지 반영 후 일단 단가산출에 대한 불러오기 정합성 확인
|
||||
|
||||
## 요구 739 (11-09 · 사용자 · 4 조사 결과 — 일단 현재 유지가 좋겠음 · 지금은 원문 기준으로 만든 품셈이 맞는지)
|
||||
|
||||
4 조가결과에서 일단 현재를 유지하는게 좋을것같은데 일단 현재는 원문을 기준으로 만들어진 품셈이잖아. 맞지?
|
||||
|
||||
## 요구 740 (11-09 · 사용자 · 현재 유지 범위 확인)
|
||||
|
||||
현재 유지는 지금의 범위가 맞음
|
||||
|
||||
## 요구 741 (11-10 · 사용자 · 정합성 · 연결 · 흠 · 정할 것을 아티팩트로)
|
||||
|
||||
불러오기 정합성, 우리기초와 연결, 찾음 흠, 정할것을 별도의 아티팩트로 저장.
|
||||
|
||||
## 요구 742 (11-10 · 사용자 · 엑셀 = 경쟁사 프로그램 결과물인데 왜 다른가 · 파일은 하나만 넣게 · STmate 단가산출표 = 산출근거 상단 제목 — 조사)
|
||||
|
||||
1.일단 하나만 물어보자. tool 폴더에는 경쟁사 복제프로그램을 만들어뒀어. 그렇다면 엑셀 파일은 이 경쟁사 프로그램에 의해 결정될텐데 서로 다르다라는게 이해가 되지않아서 말이야. 엑셀이라면 먼저 경쟁사 분석엔진을 돌고 그다음은 경쟁사 프로그램 확장자로 넣은 결과와 같다는 거지. 그럼 복수의 파일을 받는게 아니라 사용자는 엑셀이든 경쟁사 확장자 파일이든 하나만 넣게할거야.
|
||||
2. stmate 확장자인경우 단가산출표가 없다고 했는데 단가산출표는 산출근거의 상단제목만 뽑은게 단가산출표야.
|
||||
조사해봐
|
||||
|
||||
## 요구 743 (11-10 · 사용자 · 이제부터 불러오기는 방향 결정만 · 코드 수정은 한 번에 · 아티팩트 기준으로 하나씩 협의 · 1 동의 · 2 확장자 파일 = 단가산출표 + 산출근거만(일위대가 제외) · 3 이름 꼬리 = 연결용 분리면 문제없음)
|
||||
|
||||
이제부터 불러오기에 대한 방향성 결정만 하고 코드 수정은 한번에 진행할 생각임. 이전 대화에서 공유해준 아티팩트를 기준으로 하나하나 방향성을 협의하고 조사하고 방향성을 수립해두면 됨.
|
||||
1. 동의함.
|
||||
2. 경쟁사 확장자일 경우 단가산출표와 산출근거만 표현. 일위대가는 제외
|
||||
3. 엑셀이름 꼬리는 우리단가 연결을 위한 분리작업이라면 문제없음.
|
||||
|
||||
## 요구 744 (11-10 · 사용자 · B1 · B2 동의 · B3 동의 안 됨(우리 프로그램엔 있음 · 이름으로 따라가고 우리 단가산출화 때 조합으로) · C1 비목을 바꾸면 됨 · C2 품셈 밖이 아니라 품셈 기본으로 조합하고 이름만 넣은 자료 아닌지)
|
||||
|
||||
b1 b2 동의
|
||||
c3는 동의 안됨. 우리프로그램은 있음. 경쟁사 프로그램에 정보가 없다는말? 그렇다면 그냥 이름만 보고 따라간다면? 우리 단가산출화 했을때는 따로 조합으로 보려주면 될듯한데?
|
||||
c1 비못을 바꾸면 되지 않음?
|
||||
c2 품셈밖이 아니라 결국 품셈을 기본으로하여 조합하고 이름만 넣은 자료가 아닌지.
|
||||
|
||||
## 요구 745 (11-10 · 사용자 · B3 = 경쟁사 목록에 없다는 뜻인지(맞으면 제안 OK) · C2 = 목적은 우리 기초 만들기가 아니라 서로의 인력/자재/기계/다른 정보로 단가산출을 만드는 것 — 특정 기초와 맞추기보다 우리 정보로 만들어 낼 수 있는지)
|
||||
|
||||
b3 에서 경쟁사 목록에 해당내용이 없다는건지? 그게 맞다면 제안이 맞음
|
||||
c2 지금 우리가 하는 작업은 우리단가산출 기초를 만들기 위함이 아님. 우리의 단가산출 기초처럼 서로의 인력/자재/기계/다른 정보로 단가산출을 만들기 위함임. 그러니 우리의 특정단가산출 기초와 동일하게 맞추기보다는 우리의 정보로 만들어 낼수 있는지를 봐야함.
|
||||
|
||||
## 요구 746 (11-10 · 사용자 · B3 = 품셈 원문에 없어서 안 만든 것인지 · B2 맞는 방향)
|
||||
|
||||
b3 그럼 품셈 원문에 해당 내용이 없어서 아뉴만들었다는게 되는건데 맞아?
|
||||
b2 맞는 방향인것으로 판단됨
|
||||
|
||||
## 요구 747 (11-10 · 사용자 · B3 = 원문에 있는데 누락된 것만 추가하면 되는지 · 다음 = 유가가 왜 연결 안 되는지 조사)
|
||||
|
||||
b3 품셈 원문에 있는데 누락된 것만 추가하면 된다는거지?
|
||||
다음 유가 부분이 왜 연결안되는지 조사
|
||||
|
||||
## 요구 748 (11-10 · 사용자 · 유가 = 전국으로 · 전력 = 토목공사에 맞는 기본값으로 결정 · 이상하면 사용자가 나중에 바꿈)
|
||||
|
||||
유가는 경쟁사는 분리안되어있다는 말인것같고 일단 전국으로 해줘. 나중에 필요하면 변경하겠지. 전력도 어떤 항목이 토목공사에 더 작합한지 고민해보고 그냥 기본값으로 결정하면 됨. 이후 둘다 이상하면 사용자가 나중에 변걍할거야
|
||||
|
||||
## 요구 749 (11-10 · 사용자 · 재료 안 맞음 · 화면 속도는 나중에 한 번에 · 결정 사항 수정 정리 + 예상 정합성 조사)
|
||||
|
||||
일단은 맞는것같아.
|
||||
화면속도는 나중에 한번에 진행항 생각.
|
||||
이제까지 결정사항에 대한 수정사항을 정리해보고 어느정도 정합성이 나올건지 조사해줘
|
||||
꽤 많이 개선된걸로 보임
|
||||
|
||||
## 요구 750 (11-10 · 사용자 · 진행)
|
||||
|
||||
진행
|
||||
|
||||
## 요구 751 (11-10 · 사용자 · 다른 워크트리와 나눠서 작업하면 나을지 물음)
|
||||
|
||||
다른 워크트리와 나눠서 작업하면 좀 나을려나?
|
||||
|
||||
## 요구 752 (11-10 · 사용자 · 혼자(한 창)로 하는 게 좋겠음 · 기다림)
|
||||
|
||||
그냥 혼자하는게 좋겠네. 기다릴께
|
||||
|
||||
## 요구 753 (11-10 · 사용자 · 다음 협의 1 · 2 번 설명 요청 · 재확인 = 우리 기초와 맞추기가 아니라 우리 기능 · 요소로 단가산출을 만드는 것)
|
||||
|
||||
1번 2번 설명.
|
||||
잘 이해해야하는 부분은 우리의 일위대가 기초와 동일하게 맞추는게 아니라 우리가 가지고 있는 기능과 요소로 단가산출을 만드는 거야. 이해하지? 이전 대화에서 했던 말이야. 재확인차 물어봄.
|
||||
|
||||
## 요구 754 (11-10 · 사용자 · 재료 지역 = 우리 재료가 지역별로 구분되어 있으니 재료를 바꾸면 되지 않나)
|
||||
|
||||
추가항목 지역관련해서는 우리 재료자체가 지역 구분되어있어 재료를 변경하면 문제없을 것 어떻게 생각함?
|
||||
|
||||
## 요구 755 (11-11 · 사용자 · 결정 전 확인 — 재료 DB 는 EST 정보 + 나라장터 정보로 만듦 · 그렇다면 EST 는 문제없지 않나)
|
||||
|
||||
결정하기전에 하나더 확인. 현재 재효 db는 est에서 가져온 정보와 나라잩터에서 가져온 정보를 추가하여 사용하고 있음. 그렇다면 est는 문제없을것같은데 어때?
|
||||
|
||||
## 요구 756 (11-11 · 사용자 · 계속 대화 — 1 EST 원래 DB 와 키가 같은지(별도 DB · 뒤에서 참조 키) · 2 EST DB 가 있는데 EST 출력 자료를 못 찾는 게 이해 안 됨 · 3 실무 엑셀 · 원파일 작업하며 DB 에 넣은 자료가 있는지)
|
||||
|
||||
계속 대화해보자.
|
||||
1. est원래db 와 완전히 키값이 같다는 말인건가? 복베문제도 있으니 별도의 db로 만들려고 했었는데 물런 뒤에서는 참조로 키값을 가지고 있어. 연결정보를 가지고 있으면 좋겠지만 말이야.
|
||||
2. 이해안되는 부분이 원래 est db도 있는데 est 프로그램에서 출력된 자료를 못 찾는게 이해가 안되네.
|
||||
3. 추가로 혹시 실무문서 엑셀, 원파일로 작업하면서 db에 넣은 자료가 있다는 말이야?
|
||||
|
||||
## 요구 757 (11-11 · 사용자 · 1 EST 출력 엑셀을 넣으면 분석 엔진으로 뭔지 알 수 있는 것 아닌지 · 2 경쟁사 두 DB 의 재료를 매칭해 중복을 줄이고 우리 재료 DB 에 서로의 키를 연결 정보로 갖는 안)
|
||||
|
||||
1. est출력 엑셀의 내용을 넣었을때 분석엔진을 통해 뭔지 알수 있는거 아니야?
|
||||
2. 경쟁사 2개의 db에서 재료의 매칭을 해서 중복을 최소화하고 우리 재료 db 에 서로의 키값을 연결 종보를 가지고 있는다면 좋을것같은데 어떻게 생각함?
|
||||
|
||||
## 요구 758 (11-11 · 사용자 · 공사가 직접 넣은 품목 = 그대로 유지해 단가산출 · 일위대가에 넣고, 만들어진 M02 단가산출 · 일위대가에서 바꾸도록 유도 — 추가 검토)
|
||||
|
||||
그 공사가 직접 넣은 품목은 그대로 유지해서 단가산출이나 일위대가로 넣어주되 만들어진 m02 단가산출/일위대가에서 해당정보를 변경하게 유도하는건 어떤지 추가 검토
|
||||
|
||||
## 요구 759 (11-11 · 사용자 · 3번(공사 추가 품목 유도)은 최종적으로 안 될 때 쓰는 방식 — 미룸 · 지금까지 대화에서 수정할 것 짧게 정리)
|
||||
|
||||
결국 3번은 최종적으로 안될경우 사용하는 방식이야. 일단은 미뤄두는게 좋음.
|
||||
일단 어떤게 수정할지 현재까지 대화내용을 짧게 작선해서 보여줘
|
||||
|
||||
## 요구 760 (11-11 · 사용자 · 후보 고르기도 추가하면 예측 정확도 예상 수치)
|
||||
|
||||
후보고르기도 추가된다면 예측정확도의 예상추치가 어떻게됨?
|
||||
|
||||
## 요구 761 (11-11 · 사용자 · 화면 속도 보류 · 우리 자재표 보강도 진행한다면)
|
||||
|
||||
화면속도는 보류, 우리 자재표 보강도 진행한다면?
|
||||
|
||||
## 요구 762 (11-11 · 사용자 · 1 기초 찾기 후보가 필요한지(요소 조합으로 만들기로 함) · 2 별칭 승인 리스트 전부 지움(이전 자료 연결 요청 · 오지정 가능) · 3 견적 정보 = 프로젝트 안에서 지정하는 금액 — 그대로 보여 주고 우리 정보로 안 바꾸면 그 가격 · 바꾸면 우리 값)
|
||||
|
||||
1. 단가산출근거의 기초 찾기 후보가 있을필요가 있음? 어차피 요소들을 조합해서 만들기로 했잖아.
|
||||
2. 일단 별칭 승인 리스트는 전부 지야함. 이전 자료에서 연결요청했던 내용이고 오지정이 있을 수 있음.
|
||||
3. 견적정보는 그때 그때 협력사에 받는 정보로 프로젝트 내부에서나 지정하는 금액임. 그러니 단가산출로 만들때는 해당정보를 그냥 보여주고 사용자가 우리 정보 지정없이 프로젝트에 사용한다면 그가격을 넣고 우리 정보로 변경했다면 문제 없는거고
|
||||
|
||||
## 요구 763 (11-11 · 사용자 · 추가 검토 = 별칭 승인이 웹앱 별칭에 들어가는지 · 수기 입력하면 반영되는지 · 1 기초 찾기 후보는 사본으로 바꾸기도 의미 없음(값 업데이트 됨 · 자원 소모) · 2 일단 동의 · 3 견적은 나중에 못 찾는 정보에도 비슷하게 · 자재가 있으면 견적 대신 우리 DB · 프로젝트 안에서 따로 지정 예정)
|
||||
|
||||
추가 검토 내용
|
||||
별칭이 승인이 된다면 웹앱의 별칭에 해당정보가 입력이 되는지 그리고 만약 내가 별칭에 수기 입력늘 한다면 반영이 되는지 확임
|
||||
1. 기초찾기 후보는 사본으로 바꾸기도 의미없어보임. 어차피 값 업데이트도 되고 찾는데 자원소모만 됨.
|
||||
2. 일단 동의
|
||||
3.결국 견적의 기능은 나중에 못찾는 정보들도 유사하게 반영이 될거야. 아직보류했지만. 반대로 자재가 있는 경우에도 견적정보가 들어오면 견적정보는 필요없고 우리 db정보로 대체하면 됨. 나중에 프로젝트 내부에서 별도로 지정할 생각.
|
||||
|
||||
## 요구 764 (11-11 · 사용자 · 별칭 열은 한 열로만 운영 · 콤마로 여러 개 추가 · 반영해 작업 리스트 짧게)
|
||||
|
||||
별칭열은 한 열로만 운영. 대신 구분기호 콤마를 사용하여 추가할수 있게 변경.
|
||||
위내용 반영하여 작업리스트를 짧게 표기
|
||||
|
||||
## 요구 765 (11-11 · 사용자 · 옛 별칭 장부 걷기 가능 · M01 별칭은 승인 여부나 별칭이 적힌 것만 모아 볼 수 있게(수기 포함) · 남은 공사 내역 맞춤도 지움 · 진행 — 한 워크트리 또는 적당히 분리(토큰 조심))
|
||||
|
||||
1. 옛 별칭장부를 걷는건 가능. 하지만 M01 별칭은 승인 후 여부나 별칭이 작성된 것만 모아서 볼수 있게 반영하는게 어떤지. 없어진다면 각 요소들과 컨테이너를 접근하여 하나하나 확인해야함. (수기로 넣더라도 보이게 해야함.)
|
||||
2. 남은 공사내역 맞춤도 일단 지워줘.
|
||||
진행할 때 필요한 경우에는 하나의 워크트리에서 진행가능. 하지만 분리하는게 유리하다면 적당히 분리하여 다른 워크트리에게 지시하는것도 검토. (너무 많이 분리하면 토큰 소모 심해짐 조심.)
|
||||
|
||||
## 요구 766 (11-11 · 사용자 · 못 이은 재료와 전체 항목을 엑셀로 루트 폴더에 — 별칭을 수동으로 지정해 봄)
|
||||
|
||||
못이은 재료와 전체 항목을 엑셀로 만들어서 루트 폴더에 넣어줘. 내가 별칭으로 수동으로 지정해 볼게
|
||||
|
||||
## 요구 767 (11-11 · 사용자 · 잇는 작업은 끝나면 넣어 줌 · 이제부터 질문 — 덤프 1 톤은 품셈 원문에 없는지)
|
||||
|
||||
잇는 작업은 완료되면 넣어줄께. 이제부터는 질문을 할거야.
|
||||
덤프1톤은 품셈원문에 내용이 없어?
|
||||
|
||||
## 요구 768 (11-11 · 사용자 · EST main 의 기계류는 분류가 달라 모르는 것 아닌지 · 옵션 없는 기계 이름만 들어 있음 · 내부 단가가 옵션마다 달라 알 수 있을 듯)
|
||||
|
||||
추가 질문 EST main에 기계류들은 분류가 틀려서 모르는거 아니야? 옵션없는 기계이름만 들어가 있는걸로 보여. 그리고 내부를 보면 단가가 옵션에 따라 조금씩 달라서 알수 있을것같아서 말이야.
|
||||
|
||||
## 요구 769 (11-11 · 사용자 · 1 덤프 1 톤 = 기계 추가 · 2 엑셀 맨 위 굴착기가 무한궤도 · 타이어로 나뉘어 있는데 왜 못 찾는지 물은 것 · 대형 브레이커 + 굴착기는 조합일 뿐 — 좌측 그룹에서 분리해 추가하면 동일 · 3 발전기 5.5kW 는 원문에 없는지)
|
||||
|
||||
1. 덤프 1톤은 기계 추가
|
||||
2. 내말은 너가 만들어준 엑셀의 제일 상단이 굴착기가 무한궤도와 타이어로 분리되어있는데 왜 못찾냐고 물어본거야. 그리고 기계의 경우 대형 브레이커 + 굴착기 는 조합일뿐 우리 데이터로 가져온 경우에는 단가산출화면의 좌측 영역의 그룹에서 분리해서 추가하면 동일해짐. 이해한거야?
|
||||
3. 발전기 사양은 원문에서 5.5kw가 없는거야?
|
||||
|
||||
## 요구 770 (11-11 · 사용자 · 1 작업 시작(덤프 1 톤 기계 추가) · 2 엑셀엔 자료가 없는 게 맞고 출처 프로그램 분석을 보면 어떻게 쓰였는지 알 수 있음 — 확인 · 3 대기 · 4 엔진톱 내용이 원문에 있는지 확인)
|
||||
|
||||
1번 작업시작
|
||||
2번 엑셀에는 당연히 자료가 없는게 맞고 엑셀을 만든 출처 프로그램의 분석을 확인하면 어떻게 사용되었는지 알수 있잔항. 확인해봐.
|
||||
3. 대기
|
||||
4. 엔진톱 내용 원문에 있는지 확인
|
||||
|
||||
## 요구 771 (11-11 · 사용자 · 엔진톱 = 체인톱이 맞아 보임 · 2 번도 반영)
|
||||
|
||||
엔진톱은 체인톱이 맞아보임.
|
||||
일단 2번도 반영
|
||||
|
||||
## 요구 772 (11-11 · 사용자 · 공유 엑셀을 구글 시트 「수동연결(자재)」 · 「수동연결(기계)」 탭으로 넣음 · 기계는 행 메모에 의견 · 이미 별칭으로 연결된 것 · 간단한 글 검색으로 분류되는 것 있음 — 확인 · 재료 메모 의견은 직접 읽고 대화)
|
||||
|
||||
1. 다음으로 너가 공유해준 엑셀파일을 구글 스프레드 시트로 가져와 수동연결(자재)탭과 수동연결(기계)탭으로 넣었고 기계와 관련해서는 해당 행의 메모에 의견을 작성해 뒀음. 이때 벌써 별칭으로 연결된 정보도 있었고 간단한 텍스트 검색으로도 분류가 되는 경우가 있어. 확인해봐.
|
||||
2. 재료에 대한 메모 의견은 다시 한번 너도 직접 일어봐봐. 읽어보고 대화하자. 대부분 연결되는 것글이 많은 것같은데?
|
||||
|
||||
## 요구 773 (11-11 · 사용자 · 1 크롤러드릴 기본 110kW · 2 커터 320~440 인정 → 우리 320~400 · 3 크레인(트럭) = 크레인(타이어) · 값 비교로 잇지 말고 로직으로 판단 · 4 콘크리트펌프용 파이프 = 손료 — 어떤 장비 손료인지 확인 · 5 자재: 표기 다름은 분석 엔진 개선 · 종자는 로직 개선으로 찾을 수 있을 듯(종자 & 초본류 조회) · 별도계상은 단가산출 뒤 연결)
|
||||
|
||||
1. 크롤러는 기본 110kw로 지정
|
||||
2. 커터는 320~440 인정, 우리 자료의 320~400으로 지정
|
||||
3. 그럼 크레인 타이어가 맞지. (대신 값을 비교해서 연결하면 문제가 됨. 값은 업데이트 되기때문 과거 자료일수록 더 값이 안맞음. 그러니 로직으로 판단하게 되어야함. 굴삭기가 그렇게 되어 있을것으로 판단됨. )
|
||||
4. 콘크리트펌프용 파이프는 손료가 맞는것같음. 어떤 장비의 손료인지. 확인
|
||||
5. 자재에서
|
||||
- 표기 다름은 잘 찾도록 분석엔진 개선
|
||||
- 사람이 골라야하는 정보들 중 종자 같은건 로직 개선치 찾을 수 있을것 같음. 종자 & 초본류로 조회해봐.
|
||||
- 별도계상은 단가산출에서는 원가를 합산하지는 않지만 나중에 프로젝트 내부에서는 연결이 되어야함. 그러니 별도계상은 항목도 연결이 불가능에 가까움. 사용자가 별도계상하더라도 옵션은 단가산출을 만든 후 연결하게 해야함. 벌써 반영되어 있을것으로 예상
|
||||
|
||||
## 요구 774 (11-11 · 사용자 · 별칭은 사용자가 지정 · 브레인은 분석 엔진 업데이트에 집중)
|
||||
|
||||
그리고 별칭은 내가 지정할거야. 너는 분석 엔진 업데이트에 집중해줘.
|
||||
|
||||
## 요구 775 (11-11 · 사용자 · 이렇게 반영되면 엑셀 · 경쟁사 두 확장자 파일을 각각 넣었을 때 우리 단가산출 형태로 만드는 진행율이 어느 정도인지)
|
||||
|
||||
이렇게 반영되면 엑셀, 경쟁사 2개 확장자 파일을 각각 넣었을때 우리 프로그램의 단가산출 형태로 만드는 진행율이 어느정도 됨?
|
||||
|
||||
## 요구 776 (11-11 · 사용자 · 기계 별칭 입력은 브레인이 · 승인까지 · 자재표 값 빈 줄은 로직 문제 아님 = 완료 처리 · 자재표 보강은 27 년 초 프로그램 막바지)
|
||||
|
||||
기계 별칭 입력은 너가 해줘. 승인까지 진행
|
||||
자재표의 값이 없어 빈값을 출력하는 정보는 연결은 되었지만 못찾아서가 아니라 값이 없어서인지라 로직에는 문제가 없음. 그러니 완료 처리 자재표 보강은 27년 초에 진행예정. 프로그램 막바지에 진행예정. 너는 완료처리
|
||||
|
||||
## 요구 777 (11-11 · 사용자 · STmate 굴삭기(1.0m3) = 무한궤도)
|
||||
|
||||
STmate 의 「굴삭기(1.0m3)은 무한궤도
|
||||
|
||||
## 요구 778 (11-11 · 사용자 · STmate 에도 재료 DB 가 있을 것 같음 — 있는지만 확인)
|
||||
|
||||
추가로 하나만 확인해봐.stmate에도 재료 db가 있을것같은데 있는지만 확인해줘
|
||||
|
||||
## 요구 779 (11-11 · 사용자 · 암호 압축을 풀 수도 있지 않나)
|
||||
|
||||
암호 압축을 풀수도 있지않아?
|
||||
|
||||
## 요구 780 (11-11 · 사용자 · 자재표 값 보강 · 별칭 · 미룬 자료를 다 반영했다고 가정하면 몇 %인가)
|
||||
|
||||
자재표 값 보강을 했다고 별칭과 미룬 자료를 반영했다고 가정하면 몇 프로인거야?
|
||||
|
||||
## 요구 781 (11-11 · 사용자 · 1 기계에서 없는 것들은 경쟁사가 어떻게 계산했는지 확인 · 2 별도계상 완료 처리 · 3 구글 시트 기계 · 재료 탭 내용 지우고 별칭 안 들어간 기계 · 해결 안 된 자재 넣기 · 4 자재표 값 보강 대상 확인해 시트에 넣기)
|
||||
|
||||
1. 일단 기계에서 없는 것들은 경쟁사들은 어떻게 계산되었는지 확인
|
||||
2. 별도 계상은 완료처리하는게 맞는걸로 보임.
|
||||
3. 구글 스프레드시트의 내용 중 기계와 재료의 내용을 삭제하고 기계 별칭안들어간 항목과 해결되지 않은 자재들을 넣어줘.
|
||||
4. 자재표 값 보강이 어떤게 있는지 확인해서 스프레드시트에 넣어줘.
|
||||
|
||||
## 요구 782 (11-11 · 사용자 · 서버 로그인 확인 · 랩탑 앞 · 시트 재인증 · 1·2 기계 새로 추가하되 EST 말고 조달청 자료로 가능한지)
|
||||
|
||||
(서버 재시작 · 구글 재인증 · 시트 세 탭 갱신 요청 포함)
|
||||
1,2 기계를 새로 추가하는게 좋을것같아. 대신 EST 자료 말고 조달청 자료들로 하고 싶은데 가능할까?
|
||||
|
||||
## 요구 783 (11-11 · 사용자 · 1 조달청 장비 가격 API 로 확인 · 2 다른 워크트리에게 — M02 좌측 패널의 단가산출·일위대가 컨테이너 내부 내용 안 보임 확인)
|
||||
|
||||
2번 API로 일단 확인해봐.
|
||||
추가로 다른 워크트리에게 M02의 좌측 패널의 단가산출과 일위대가 컨테이너 내부 내용이 안보임. 확인
|
||||
|
||||
## 요구 784 (11-11 · 사용자 · 행안부 건설기계 시가표준액표가 있으면 쓰는 것도 좋아 보임)
|
||||
|
||||
행안부 건설기계 시가표준액표가 있다면 사용하는것도 좋아보임.
|
||||
|
||||
## 요구 785 (11-11 · 사용자 · 우리 단가에 계산식도 표현돼야 하는데 가능한지 · 가능하면 이 자료를 기술정보 DB 에 md 화해 보관해야 하는지 검토)
|
||||
|
||||
그런데 우리 단가에는 계산식도 표현이 되어야하는데 가능한거지? 그리고 가능하다면 이자료를 기술정보 DB에 md화해서 보관해야하는건 아닌지 검토
|
||||
|
||||
## 요구 786 (11-11 · 사용자 · 확인했음 · 품셈 원문엔 우리가 찾는 정보 없음 · 일단 확인 진행)
|
||||
|
||||
내가 확인했음. 품셈원문을 기준으로 해서 우리가 찾아야하는 정보가 없기는 함. 일단 확인은 진행해봐.
|
||||
|
||||
## 요구 787 (11-11 · 사용자 · 1 기계 만들기 맞음 · 일단 만들어 두고 가격만 확인(어디서 받을지 확인) · 2 엑셀 불러오기 테스트 — 상단 변수 영역은 옵션 선택지 있을 때만 쓰는 곳 · 외부 단가산출 불러오면 옵션 못 쓰고 고정 항목으로 제어 · 상단 값은 좌측 영역에도 있으니 중복 표기 불필요 · 좌측 영역 내용은 유지)
|
||||
|
||||
일단은 기계를 만드는건 맞는것같음. 일단 만들두고 가격만 확인하면 되니까 어디서 받을지 확인해줘.
|
||||
다음 엑셀불러오기로 테스트 해봤음. 이렇게 나오는데 위에 영역은 옵션 변경에따른 선택지가 있는 경우에나 사용되는 영역. 하지만 외부의 단가산출을 불러올 경우 옵션을 사용할 수 없고 고정된 항목들로 제어해야되는 상황임. 그러니 상단의 값들은 사실 좌측영역에도 들어가 있으니 중복표기할 필요없음. 좌측 영역의 내용은 유지.
|
||||
|
||||
## 요구 788 (11-11 · 사용자 · 요청 업무 다 끝나면 랩탑 워크트리 깃 싱크 · 데스크탑 워크트리 제외)
|
||||
|
||||
내가 요청한 업므 다 끝나면 랩탑 워크트리 깃싱크 . 데스크탑 워크트리 제외
|
||||
|
||||
## 요구 789 (11-12 · 사용자 · 경쟁사 세 파일 현재 매칭율 표시)
|
||||
|
||||
경쟁사 3개 파일 현재 매칭율 표시
|
||||
|
||||
## 요구 790 (11-12 · 사용자 · 정밀 검토 요청 — 1 우리 기계는 품셈 기준 그대로 · 새 기계·재료는 M02 단가산출로 개인 관리 · 기계 패턴 분석해 쉬운 신규 기계 생성 로직 · 2 M01 기계 컨테이너 = 표라 값 확인·수정 불편 → 별도 페이지 · 3 M02 에도 기계·재료 개인 관리 기능(중복 확인 · 단가산출 만들 때 복사) — 관리 안 될 것 같으면 반영 안 함 · 4 매칭 안 돼 추가한 기계 10 여 개 삭제 여부 · 5 특수한 경우 빼면 매칭을 높여 개인 관리 최소화)
|
||||
|
||||
1. 잠시 고민해봤는데 조금 문제가 있네. 나중에 다른 기계나 신규 재료 품목이 들어올 경우가 문제네. 우리의 기계는 품셈을 기준으로 그냥두고 새로운 기계나 재료의 경우 m02의 단가산출로 개인관리하게 하는건 어떤가해서? 자재는 품목과 값만 있으면 되니까 문제가 없는데 기계의 경우 조금 복잡함. 기계들의 패턴을 분석해서 쉽게 만드는 방법이 없는지 검토. 이런 기능이 있으면 경쟁사 프로그램에서 가져오는 정보 중 매칭이 안되는 것이 있다고 하더라도 문제없을것같아서 말이야. 그렇다면 단순한 신규 기계 생성로직이 필요한거야.
|
||||
2. m01의 기계 컨테이너의 페이지가 테이블로 구성되어 있음. 리스트는 문제가 없으나값확인이나 수정이 불편함. 별도의 페이지로 접근하게 반영.
|
||||
3. m02에 m01에서와 동일하게 기계와 재료를 개인이 관리할 수 있는 기능이 있는게 어떤지 그럼 중복확인이 쉬울것같은데. 이 역시도 단가산출을 생성할때는 복사하여 사용하는거지. 어떻게 생각함. 관리가 안될것같으면 반영안할거야.
|
||||
4. 이전 반영 중 매칭이 안되서 추가한 기계 10여개는 삭제하는게 어떤지.
|
||||
5. 하지만 특수한 경우를 제외하면 매칭을 높여 개인관리를 최소화해야하는건 맞아.
|
||||
일단 정밀 검토해보자.
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 33 KiB |
@@ -549,10 +549,9 @@ app.include_router(b09_estimation_router, dependencies=protected_with_company)
|
||||
# 마스터 데이터 — 회사·프로젝트가 아니라 시스템 관리자만 본다.
|
||||
system_admin_only = [Depends(verify_session), Depends(require_system_admin)]
|
||||
app.include_router(m01_master_data_router, dependencies=system_admin_only)
|
||||
app.include_router(
|
||||
__import__("M01_MasterData.M01_MasterData_Alias", fromlist=["router"]).router,
|
||||
dependencies=system_admin_only,
|
||||
) # 별칭 장부 모으기(718-2)
|
||||
_m01_alias = __import__("M01_MasterData.M01_MasterData_Alias", fromlist=["router"])
|
||||
app.include_router(_m01_alias.router, dependencies=system_admin_only) # 별칭 모아 보기(815-2)
|
||||
app.include_router(_m01_alias.suggest_router, dependencies=protected) # 별칭 제안 — 로그인만
|
||||
# 단위 · 금액 표준 표 읽기(`GET /api/m02/df/standards` · 단위 고르기 `M02_DF_UnitPick`) — 읽기만 · 로그인만(M02 줄 쌓기 단위)
|
||||
app.include_router(
|
||||
__import__("M02_MasterTemplete.DataFlow.M02_DF_Router", fromlist=["router"]).router,
|
||||
|
||||
@@ -96,3 +96,22 @@ last_updated: 2026-08-15
|
||||
| 운전사 3구분·수송비·시공능력 기본식 | 건설품셈 8-1-3 ~ 8-1-4 | 〃 |
|
||||
| 환율·유가 원천과 스냅샷 | ECOS·오피넷 실사 2026-08-14 | [원가_기초데이터_원천 §4](원가_기초데이터_원천.md) |
|
||||
| ESTX 기계경비 조립 관측 | 2026-07 배포본 재검증 | [원가_기초데이터_원천 §3](원가_기초데이터_원천.md) |
|
||||
|
||||
## 7. 품셈에 없는 소형·보조 기계 — EST 구성 기준 · 가격 갱신 필요
|
||||
|
||||
현행 품셈 기계 목록(발전기 25㎾~ 등)에 없는 기계를 EST 단가 구성대로 우리 기계표에 둔 줄. 손료계수 · 연료 · 잡품 · 조종원 = 품셈 값(같은 계열 차용) · 가격 = EST 씨앗(임시) — 조달청 · 행안부 건설기계 표준시장가격 · 제조사 값으로 바꿀 것.
|
||||
|
||||
| 기종 | 규격 | 손료계수(10⁻⁷/hr) | 연료 · 잡품 · 전력 | 조종원 | 가격 씨앗(천원) | 시간당 식 | 출처 |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| 발전기 | 5.5㎾ | 2,362(발전기 계열) | 경유 0.625ℓ · 11 % | 일반기계운전사 1 | 1,063 | GC000268 | EST · 2016 사방품셈(안) 같은 꼴 |
|
||||
| 발전기 | 7㎾ | 2,362 | — | — | 1,828.9 | GC000273(손료만) | 2016 사방품셈(안) 기계경비 |
|
||||
| 발전기 | 10㎾ | 2,362 | — | — | 3,213 | GC000273 | 2016 사방품셈(안) 기계경비 |
|
||||
| 대형 브레이커 | 0.18㎥ | 6,601(0.2㎥ 계수) | — | — | 4,559(0.2㎥ 값) | GC000273 | 굴착기 부속 · 우리 0.2㎥ 줄 |
|
||||
| 에어 호스 | 1.91㎝ | 5,625 | — | — | 105 | GC000273 | EST |
|
||||
| 모노펌프 | 5-20㎥/hr | 4,346 | — | — | 4,900 | GC000273 | EST |
|
||||
| 파워트로웰 | 3.73㎾ | 5,313 | 휘발유 1.0ℓ · 10 % · 전력 3.73㎾h | — | 2,680 | GC001195 | EST |
|
||||
| 콘크리트펌프용 파이프(압송관) | 150㎜ × 1 · 2 · 3m · 곡관 45 · 60 · 90° | 4,500 | — | — | $25 · 37 · 46 · 67 · 84 · 103 × 환율 | GC000273 | EST($ 표기) · 환율 FX000001 |
|
||||
|
||||
- 손료 = 버림(가격 × 1,000 × 손료계수 ÷ 10⁷) · 조종원 = 하루 노임 × 1/8(품셈 본문 · EST 의 16/12 · 25/20 뺌).
|
||||
- 파워트로웰 잡품 = 주연료 줄의 % (전력 줄엔 안 붙임).
|
||||
- 파이프 가격 = 달러 표기 → 환율 적용값 · 원화 갱신 필요.
|
||||
|
||||
@@ -118,3 +118,14 @@
|
||||
- 끝수 결과 · 중간 `표시` → 식 안 `반올림` · 끝수 계 · 끝전 · 단계 · 조정 · 번지 · 합밖 · 자리 → 문서 칸 그대로.
|
||||
- 기호 = 수식 탭 기호(800-1 · 800-5) · 한 문서 안 겹침 = 아래첨자 끝 숫자.
|
||||
- 저장 = 대조(기본 옵션 + 옵션 표본)가 모두 같고 부르는 단가산출도 저장되는 문서만 · 다른 것 = `docs/raw/요구_1104/_목록_802-2_변환대조.xlsx`.
|
||||
|
||||
## 9. 부품 표지(813-2) — 단가산출이 아닌 보조 문서
|
||||
|
||||
- 머리 칸 `부품` = `{갈래, 기종?, 규격?}` · 옛 로직 줄 `부품` 을 변환이 그대로 옮김 · 셈 · 부르기 · 키로 열기는 그대로.
|
||||
- 단가산출 기초 목록 · 찾아 넣기 「다른 단가산출」 목록에 안 보임(`M01_MasterData_Parts.strip`).
|
||||
|
||||
| 갈래 | 뜻 | 칸 | 자리 |
|
||||
| --- | --- | --- | --- |
|
||||
| 기계경비식 | 기계 시간당 경비 셈 방식(기계 줄이 부름) | — | M01 「기계」 기종 아래 |
|
||||
| 작업량 틀 | 기종 공식(Q = 3,600 × q × K × f × E ÷ Cm 등) | `기종` = 기계표 상세구분 목록 · `규격` = 기계 규격 수를 받는 변수 열쇠 | M01 「기계」 기종 아래 |
|
||||
| 계수식 | 기계 무관 · 돈 아닌 식 | — | M01 「계수」 아래 |
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"다음": {"LB": 409, "EQ": 655, "RT": 20, "MN": 7000, "MT": 30719, "MO": 74, "MP": 364, "FX": 6, "CF": 159, "QF": 477, "GF": 337, "CC": 528, "GC": 1195, "QC": 2060, "GX": 5, "UA": 38},
|
||||
"폐기": {"MT000001": "자재품목 휘발유 — 값이 비고 글뿐 · 유가 테이블로 감", "MT000002": "자재품목 경유 — 값이 비고 글뿐 · 유가 테이블로 감", "MT000003": "자재품목 선박용경유 — 값이 비고 글뿐 · 유가 테이블로 감", "MT000004": "자재품목 중유 — 값이 비고 글뿐 · 유가 테이블로 감", "MT000005": "자재품목 등유 — 값이 비고 글뿐 · 유가 테이블로 감", "LB000399": "이름 「〃」 — 직종이 아니라 되풀이 기호 · 소요량 13-6-9 에서 윗줄 직종으로 바로잡음", "MN": "재료_나라장터자재 — old 로 옮김 · 로직은 자재품목·품셈재료로 갈아 끼움", "LB000372": "미확보 고급기능사 — 측량노임 항공사진고급기능사 옛이름으로 옮김", "LB000393": "미확보 리베팅공 — 건설노임 철골공 옛이름으로 옮김(2010 통합표 연번10)", "LB000405": "미확보 Belt Conveyor 설치공 — 품셈 13-10-1 주② 배분으로 다섯 직종에 나눔", "MT000006": "자재품목 전력 — 재료_유가전력 전력 줄로 감", "MT000007": "자재품목 전력 — 재료_유가전력 전력 줄로 감", "LB000368": "미확보 기술사 — 기술사(건설) 옛이름으로 옮김", "LB000369": "미확보 특급기술자 — 특급기술자(건설) 옛이름으로 옮김", "LB000370": "미확보 고급기술자 — 고급기술자(건설) 옛이름으로 옮김", "LB000361": "미확보 중급기술자 — 중급기술자(건설) 옛이름으로 옮김", "LB000364": "미확보 초급기술자 — 초급기술자(건설) 옛이름으로 옮김", "LB000365": "미확보 고급숙련기술자 — 고급숙련기술자(건설) 옛이름으로 옮김", "LB000366": "미확보 중급숙련기술자 — 중급숙련기술자(건설) 옛이름으로 옮김", "LB000367": "미확보 초급숙련기술자 — 초급숙련기술자(건설) 옛이름으로 옮김", "LB000389": "미확보 기계기사 — 초급기술자(기계·설비) 옛이름으로 옮김", "LB000388": "미확보 기계산업기사 — 초급숙련기술자(기계·설비) 옛이름으로 옮김", "MO000055": {"원문번호": "주택용저압:기본_200이하", "파일": "재료_유가전력.json"}, "MO000056": {"원문번호": "주택용저압:기본_201~400", "파일": "재료_유가전력.json"}, "MO000057": {"원문번호": "주택용저압:기본_400초과", "파일": "재료_유가전력.json"}, "MO000059": {"원문번호": "주택용저압:전력량_다음200", "파일": "재료_유가전력.json"}, "MO000060": {"원문번호": "주택용저압:전력량_400초과", "파일": "재료_유가전력.json"}, "MO000061": {"원문번호": "주택용저압:월간최저요금", "파일": "재료_유가전력.json"}, "MO000062": {"원문번호": "일반용갑I저압:기본", "파일": "재료_유가전력.json"}, "MO000063": {"원문번호": "일반용갑I저압:전력량_여름철", "파일": "재료_유가전력.json"}, "MO000064": {"원문번호": "일반용갑I저압:전력량_봄가을철", "파일": "재료_유가전력.json"}, "MO000065": {"원문번호": "일반용갑I저압:전력량_겨울철", "파일": "재료_유가전력.json"}, "MO000066": {"원문번호": "산업용갑I저압:기본", "파일": "재료_유가전력.json"}, "MO000067": {"원문번호": "산업용갑I저압:전력량_여름철", "파일": "재료_유가전력.json"}, "MO000068": {"원문번호": "산업용갑I저압:전력량_봄가을철", "파일": "재료_유가전력.json"}, "MO000069": {"원문번호": "산업용갑I저압:전력량_겨울철", "파일": "재료_유가전력.json"}, "MO000070": {"원문번호": "임시전력갑:기본요금적용", "파일": "재료_유가전력.json"}, "MO000071": {"원문번호": "임시전력갑:전력량요금적용", "파일": "재료_유가전력.json"}, "MO000072": {"원문번호": "임시전력갑:월간최저요금", "파일": "재료_유가전력.json"}},
|
||||
"다음": {"LB": 409, "EQ": 669, "RT": 20, "MN": 7000, "MT": 30719, "MO": 74, "MP": 364, "FX": 6, "CF": 159, "QF": 477, "GF": 338, "CC": 528, "GC": 1196, "QC": 2060, "GX": 5, "UA": 38},
|
||||
"폐기": {"MT000001": "자재품목 휘발유 — 값이 비고 글뿐 · 유가 테이블로 감", "MT000002": "자재품목 경유 — 값이 비고 글뿐 · 유가 테이블로 감", "MT000003": "자재품목 선박용경유 — 값이 비고 글뿐 · 유가 테이블로 감", "MT000004": "자재품목 중유 — 값이 비고 글뿐 · 유가 테이블로 감", "MT000005": "자재품목 등유 — 값이 비고 글뿐 · 유가 테이블로 감", "LB000399": "이름 「〃」 — 직종이 아니라 되풀이 기호 · 소요량 13-6-9 에서 윗줄 직종으로 바로잡음", "MN": "재료_나라장터자재 — old 로 옮김 · 로직은 자재품목·품셈재료로 갈아 끼움", "LB000372": "미확보 고급기능사 — 측량노임 항공사진고급기능사 옛이름으로 옮김", "LB000393": "미확보 리베팅공 — 건설노임 철골공 옛이름으로 옮김(2010 통합표 연번10)", "LB000405": "미확보 Belt Conveyor 설치공 — 품셈 13-10-1 주② 배분으로 다섯 직종에 나눔", "MT000006": "자재품목 전력 — 재료_유가전력 전력 줄로 감", "MT000007": "자재품목 전력 — 재료_유가전력 전력 줄로 감", "LB000368": "미확보 기술사 — 기술사(건설) 옛이름으로 옮김", "LB000369": "미확보 특급기술자 — 특급기술자(건설) 옛이름으로 옮김", "LB000370": "미확보 고급기술자 — 고급기술자(건설) 옛이름으로 옮김", "LB000361": "미확보 중급기술자 — 중급기술자(건설) 옛이름으로 옮김", "LB000364": "미확보 초급기술자 — 초급기술자(건설) 옛이름으로 옮김", "LB000365": "미확보 고급숙련기술자 — 고급숙련기술자(건설) 옛이름으로 옮김", "LB000366": "미확보 중급숙련기술자 — 중급숙련기술자(건설) 옛이름으로 옮김", "LB000367": "미확보 초급숙련기술자 — 초급숙련기술자(건설) 옛이름으로 옮김", "LB000389": "미확보 기계기사 — 초급기술자(기계·설비) 옛이름으로 옮김", "LB000388": "미확보 기계산업기사 — 초급숙련기술자(기계·설비) 옛이름으로 옮김", "MO000055": {"원문번호": "주택용저압:기본_200이하", "파일": "재료_유가전력.json"}, "MO000056": {"원문번호": "주택용저압:기본_201~400", "파일": "재료_유가전력.json"}, "MO000057": {"원문번호": "주택용저압:기본_400초과", "파일": "재료_유가전력.json"}, "MO000059": {"원문번호": "주택용저압:전력량_다음200", "파일": "재료_유가전력.json"}, "MO000060": {"원문번호": "주택용저압:전력량_400초과", "파일": "재료_유가전력.json"}, "MO000061": {"원문번호": "주택용저압:월간최저요금", "파일": "재료_유가전력.json"}, "MO000062": {"원문번호": "일반용갑I저압:기본", "파일": "재료_유가전력.json"}, "MO000063": {"원문번호": "일반용갑I저압:전력량_여름철", "파일": "재료_유가전력.json"}, "MO000064": {"원문번호": "일반용갑I저압:전력량_봄가을철", "파일": "재료_유가전력.json"}, "MO000065": {"원문번호": "일반용갑I저압:전력량_겨울철", "파일": "재료_유가전력.json"}, "MO000066": {"원문번호": "산업용갑I저압:기본", "파일": "재료_유가전력.json"}, "MO000067": {"원문번호": "산업용갑I저압:전력량_여름철", "파일": "재료_유가전력.json"}, "MO000068": {"원문번호": "산업용갑I저압:전력량_봄가을철", "파일": "재료_유가전력.json"}, "MO000069": {"원문번호": "산업용갑I저압:전력량_겨울철", "파일": "재료_유가전력.json"}, "MO000070": {"원문번호": "임시전력갑:기본요금적용", "파일": "재료_유가전력.json"}, "MO000071": {"원문번호": "임시전력갑:전력량요금적용", "파일": "재료_유가전력.json"}, "MO000072": {"원문번호": "임시전력갑:월간최저요금", "파일": "재료_유가전력.json"}, "GC001145": "작업량당 기계경비 — 부르는 곳 0 · 필요하면 단가산출에 기계 하나 넣어 만듦(요구 734-4 · 735-4 · 813-2)", "GC001148": "작업량당 기계경비 — 부르는 곳 0 · 필요하면 단가산출에 기계 하나 넣어 만듦(요구 734-4 · 735-4 · 813-2)", "GC001151": "작업량당 기계경비 — 부르는 곳 0 · 필요하면 단가산출에 기계 하나 넣어 만듦(요구 734-4 · 735-4 · 813-2)", "GC001156": "작업량당 기계경비 — 부르는 곳 0 · 필요하면 단가산출에 기계 하나 넣어 만듦(요구 734-4 · 735-4 · 813-2)", "GC001159": "작업량당 기계경비 — 부르는 곳 0 · 필요하면 단가산출에 기계 하나 넣어 만듦(요구 734-4 · 735-4 · 813-2)", "GC001166": "작업량당 기계경비 — 부르는 곳 0 · 필요하면 단가산출에 기계 하나 넣어 만듦(요구 734-4 · 735-4 · 813-2)"},
|
||||
"키": {
|
||||
"CC000001": {"원문번호": "공통 1-3-1 콘크리트 및 포장용 재료", "파일": "계수_건설품셈_01장_적용기준.json"},
|
||||
"CC000002": {"원문번호": "공통 1-3-1 노상 및 노반재료", "파일": "계수_건설품셈_01장_적용기준.json"},
|
||||
@@ -1341,6 +1341,20 @@
|
||||
"EQ000652": {"원문번호": "2-2-11 부착용집게(0.2~0.8㎥)", "파일": "기계.json"},
|
||||
"EQ000653": {"원문번호": "2-2-11 트럭탑재형크레인", "파일": "기계.json"},
|
||||
"EQ000654": {"원문번호": "2-2-11 타이어형크레인", "파일": "기계.json"},
|
||||
"EQ000655": {"원문번호": "10-9 덤프트럭(4륜구동) 1ton", "파일": "기계.json"},
|
||||
"EQ000656": {"원문번호": "자체 발전기 5.5㎾", "파일": "기계.json"},
|
||||
"EQ000657": {"원문번호": "자체 발전기 7㎾", "파일": "기계.json"},
|
||||
"EQ000658": {"원문번호": "자체 발전기 10㎾", "파일": "기계.json"},
|
||||
"EQ000659": {"원문번호": "자체 대형 브레이커 0.18㎥", "파일": "기계.json"},
|
||||
"EQ000660": {"원문번호": "자체 에어 호스 1.91㎝", "파일": "기계.json"},
|
||||
"EQ000661": {"원문번호": "자체 모노펌프 5-20㎥/hr", "파일": "기계.json"},
|
||||
"EQ000662": {"원문번호": "자체 파워트로웰 3.73㎾", "파일": "기계.json"},
|
||||
"EQ000663": {"원문번호": "자체 콘크리트펌프용 파이프 150㎜×1m", "파일": "기계.json"},
|
||||
"EQ000664": {"원문번호": "자체 콘크리트펌프용 파이프 150㎜×2m", "파일": "기계.json"},
|
||||
"EQ000665": {"원문번호": "자체 콘크리트펌프용 파이프 150㎜×3m", "파일": "기계.json"},
|
||||
"EQ000666": {"원문번호": "자체 콘크리트펌프용 파이프 곡관 150㎜ 45°", "파일": "기계.json"},
|
||||
"EQ000667": {"원문번호": "자체 콘크리트펌프용 파이프 곡관 150㎜ 60°", "파일": "기계.json"},
|
||||
"EQ000668": {"원문번호": "자체 콘크리트펌프용 파이프 곡관 150㎜ 90°", "파일": "기계.json"},
|
||||
"FX000001": {"원문번호": "0000001", "파일": "환율_한국은행환율.json"},
|
||||
"FX000002": {"원문번호": "0000002", "파일": "환율_한국은행환율.json"},
|
||||
"FX000003": {"원문번호": "0000003", "파일": "환율_한국은행환율.json"},
|
||||
@@ -2490,28 +2504,22 @@
|
||||
"GC001142": {"원문번호": "공통 1-2-7 벽돌운반", "파일": "로직_건설품셈_01장_적용기준.json"},
|
||||
"GC001143": {"원문번호": "공통 8-1-4 기본식", "파일": "로직_건설품셈_08장_건설기계.json"},
|
||||
"GC001144": {"원문번호": "공통 8-2-1 불도저", "파일": "로직_건설품셈_08장_건설기계.json"},
|
||||
"GC001145": {"원문번호": "공통 8-2-1 불도저경비", "파일": "로직_건설품셈_08장_건설기계.json"},
|
||||
"GC001146": {"원문번호": "공통 8-2-2 리퍼", "파일": "로직_건설품셈_08장_건설기계.json"},
|
||||
"GC001147": {"원문번호": "공통 8-2-3 굴착기", "파일": "로직_건설품셈_08장_건설기계.json"},
|
||||
"GC001148": {"원문번호": "공통 8-2-3 굴착기경비", "파일": "로직_건설품셈_08장_건설기계.json"},
|
||||
"GC001149": {"원문번호": "공통 8-2-4 트랜처", "파일": "로직_건설품셈_08장_건설기계.json"},
|
||||
"GC001150": {"원문번호": "공통 8-2-5 로더", "파일": "로직_건설품셈_08장_건설기계.json"},
|
||||
"GC001151": {"원문번호": "공통 8-2-5 로더경비", "파일": "로직_건설품셈_08장_건설기계.json"},
|
||||
"GC001152": {"원문번호": "공통 8-2-6 스크레이퍼", "파일": "로직_건설품셈_08장_건설기계.json"},
|
||||
"GC001153": {"원문번호": "공통 8-2-7 그레이더면적", "파일": "로직_건설품셈_08장_건설기계.json"},
|
||||
"GC001154": {"원문번호": "공통 8-2-7 그레이더토량", "파일": "로직_건설품셈_08장_건설기계.json"},
|
||||
"GC001155": {"원문번호": "공통 8-2-8 덤프트럭", "파일": "로직_건설품셈_08장_건설기계.json"},
|
||||
"GC001156": {"원문번호": "공통 8-2-8 덤프트럭경비", "파일": "로직_건설품셈_08장_건설기계.json"},
|
||||
"GC001157": {"원문번호": "공통 8-2-9 롤러토량", "파일": "로직_건설품셈_08장_건설기계.json"},
|
||||
"GC001158": {"원문번호": "공통 8-2-9 롤러면적", "파일": "로직_건설품셈_08장_건설기계.json"},
|
||||
"GC001159": {"원문번호": "공통 8-2-9 롤러경비", "파일": "로직_건설품셈_08장_건설기계.json"},
|
||||
"GC001160": {"원문번호": "공통 8-2-10 아스팔트플랜트", "파일": "로직_건설품셈_08장_건설기계.json"},
|
||||
"GC001161": {"원문번호": "공통 8-2-11 스테이빌라이저", "파일": "로직_건설품셈_08장_건설기계.json"},
|
||||
"GC001162": {"원문번호": "공통 8-2-12 죠", "파일": "로직_건설품셈_08장_건설기계.json"},
|
||||
"GC001163": {"원문번호": "공통 8-2-12 롤", "파일": "로직_건설품셈_08장_건설기계.json"},
|
||||
"GC001164": {"원문번호": "공통 8-2-12 이동식", "파일": "로직_건설품셈_08장_건설기계.json"},
|
||||
"GC001165": {"원문번호": "공통 8-2-12 스크린", "파일": "로직_건설품셈_08장_건설기계.json"},
|
||||
"GC001166": {"원문번호": "공통 8-2-13 브레이커경비", "파일": "로직_건설품셈_08장_건설기계.json"},
|
||||
"GC001167": {"원문번호": "공통 8-2-14 압쇄기", "파일": "로직_건설품셈_08장_건설기계.json"},
|
||||
"GC001168": {"원문번호": "공통 8-2-15 법면다짐기", "파일": "로직_건설품셈_08장_건설기계.json"},
|
||||
"GC001169": {"원문번호": "공통 8-2-16 골재세척", "파일": "로직_건설품셈_08장_건설기계.json"},
|
||||
@@ -2540,6 +2548,7 @@
|
||||
"GC001192": {"원문번호": "공통 1-5-4 건설폐기물 발생량", "파일": "로직_건설품셈_01장_적용기준.json"},
|
||||
"GC001193": {"원문번호": "공통 1-5-6 공사용수", "파일": "로직_건설품셈_01장_적용기준.json"},
|
||||
"GC001194": {"원문번호": "공통 3-2-1 잔토처리(인력)", "파일": "로직_건설품셈_03장_토공사.json"},
|
||||
"GC001195": {"원문번호": "공통 8 파워트로웰 시간당 경비", "파일": "로직_건설품셈_08장_건설기계.json"},
|
||||
"GF000001": {"원문번호": "3-1 경계표시", "파일": "로직_산림품셈_03장_작업장_관리.json"},
|
||||
"GF000002": {"원문번호": "3-3 작업로 설치", "파일": "로직_산림품셈_03장_작업장_관리.json"},
|
||||
"GF000003": {"원문번호": "3-4-2 임산물 운반로 신설 작업량", "파일": "로직_산림품셈_03장_작업장_관리.json"},
|
||||
@@ -2791,16 +2800,16 @@
|
||||
"GF000249": {"원문번호": "9-14-2 다짐", "파일": "로직_산림품셈_09장_토공.json"},
|
||||
"GF000250": {"원문번호": "9-15-2 표토제거 답외구간", "파일": "로직_산림품셈_09장_토공.json"},
|
||||
"GF000251": {"원문번호": "9-17-2 비다짐", "파일": "로직_산림품셈_09장_토공.json"},
|
||||
"GF000252": {"원문번호": "9-4-1 암파쇄(대형브레이커 · 굴착기 0.7㎥)", "파일": "로직_산림품셈_09장_토공.json"},
|
||||
"GF000253": {"원문번호": "9-5-2 발파암 깎기 90%(대형브레이커 · 굴착기 0.7㎥)", "파일": "로직_산림품셈_09장_토공.json"},
|
||||
"GF000252": {"원문번호": "9-4-1 암파쇄", "파일": "로직_산림품셈_09장_토공.json"},
|
||||
"GF000253": {"원문번호": "9-5-2 발파암 깎기(90%)", "파일": "로직_산림품셈_09장_토공.json"},
|
||||
"GF000254": {"원문번호": "9-5-1 육상 암석 절취(발파 10%)", "파일": "로직_산림품셈_09장_토공.json"},
|
||||
"GF000255": {"원문번호": "9-6-1 발파암 기계소할(대형브레이커 · 굴착기 0.7㎥)", "파일": "로직_산림품셈_09장_토공.json"},
|
||||
"GF000256": {"원문번호": "9-7-1 무근콘크리트 깨기(T=30㎝ 미만) 깨기(대형브레이커 · 굴착기 0.7㎥)", "파일": "로직_산림품셈_09장_토공.json"},
|
||||
"GF000257": {"원문번호": "9-7-2 무근콘크리트 깨기(T=30㎝ 이상) 깨기(대형브레이커 · 굴착기 0.7㎥)", "파일": "로직_산림품셈_09장_토공.json"},
|
||||
"GF000258": {"원문번호": "9-8-1 철근콘크리트 깨기(T=30㎝ 미만) 깨기(대형브레이커 · 굴착기 0.7㎥)", "파일": "로직_산림품셈_09장_토공.json"},
|
||||
"GF000259": {"원문번호": "9-8-2 철근콘크리트 깨기(T=30㎝ 이상) 깨기(대형브레이커 · 굴착기 0.7㎥)", "파일": "로직_산림품셈_09장_토공.json"},
|
||||
"GF000260": {"원문번호": "9-10-1 기존포장 깨기 콘크리트 깨기(대형브레이커 · 굴착기 0.7㎥)", "파일": "로직_산림품셈_09장_토공.json"},
|
||||
"GF000261": {"원문번호": "9-10-2 기존포장 깨기 아스팔트 깨기(대형브레이커 · 굴착기 0.7㎥)", "파일": "로직_산림품셈_09장_토공.json"},
|
||||
"GF000255": {"원문번호": "9-6-1 발파암 기계소할(15%)", "파일": "로직_산림품셈_09장_토공.json"},
|
||||
"GF000256": {"원문번호": "9-7-1 무근콘크리트 깨기(T=30㎝ 미만)", "파일": "로직_산림품셈_09장_토공.json"},
|
||||
"GF000257": {"원문번호": "9-7-2 무근콘크리트 깨기(T=30㎝ 이상)", "파일": "로직_산림품셈_09장_토공.json"},
|
||||
"GF000258": {"원문번호": "9-8-1 철근콘크리트 깨기(T=30㎝ 미만)", "파일": "로직_산림품셈_09장_토공.json"},
|
||||
"GF000259": {"원문번호": "9-8-2 철근콘크리트 깨기(T=30㎝ 이상)", "파일": "로직_산림품셈_09장_토공.json"},
|
||||
"GF000260": {"원문번호": "9-10-1 기존포장 깨기 콘크리트", "파일": "로직_산림품셈_09장_토공.json"},
|
||||
"GF000261": {"원문번호": "9-10-2 기존포장 깨기 아스팔트", "파일": "로직_산림품셈_09장_토공.json"},
|
||||
"GF000262": {"원문번호": "9-11-1 포장절단 콘크리트(기계)", "파일": "로직_산림품셈_09장_토공.json"},
|
||||
"GF000263": {"원문번호": "9-12-2 측구터파기 암절취", "파일": "로직_산림품셈_09장_토공.json"},
|
||||
"GF000264": {"원문번호": "9-12-3 측구터파기 발파암", "파일": "로직_산림품셈_09장_토공.json"},
|
||||
@@ -2822,13 +2831,13 @@
|
||||
"GF000280": {"원문번호": "10-6-2 인력운반 돌(지게)", "파일": "로직_산림품셈_10장_자재․장비_운반.json"},
|
||||
"GF000281": {"원문번호": "10-6-3 인력운반 기타 임업자재", "파일": "로직_산림품셈_10장_자재․장비_운반.json"},
|
||||
"GF000282": {"원문번호": "10-8-1 케이블 크레인 짐내리기", "파일": "로직_산림품셈_10장_자재․장비_운반.json"},
|
||||
"GF000283": {"원문번호": "10-9-1 덤프트럭 자재 운반(1회 표준 운반량)", "파일": "로직_산림품셈_10장_자재․장비_운반.json"},
|
||||
"GF000284": {"원문번호": "10-12-1 덤프운반(굴착기 0.7㎥ 적재 · 덤프트럭 15ton)", "파일": "로직_산림품셈_10장_자재․장비_운반.json"},
|
||||
"GF000283": {"원문번호": "10-9-1 덤프트럭 운반(자재의 1회당 표준 운반량)", "파일": "로직_산림품셈_10장_자재․장비_운반.json"},
|
||||
"GF000284": {"원문번호": "10-12-1 덤프운반 토사", "파일": "로직_산림품셈_10장_자재․장비_운반.json"},
|
||||
"GF000285": {"원문번호": "10-13-2 묘목 인력운반", "파일": "로직_산림품셈_10장_자재․장비_운반.json"},
|
||||
"GF000286": {"원문번호": "4-1-1 수확베기 임업용 동력기계톱 벌목", "파일": "로직_산림품셈_04장_나무베기.json"},
|
||||
"GF000287": {"원문번호": "4-2-1 단목베기(100본당)", "파일": "로직_산림품셈_04장_나무베기.json"},
|
||||
"GF000288": {"원문번호": "4-2-2 단목베기(1,000㎡당 · 굴착기 0.2㎥ + 부착용 집게)", "파일": "로직_산림품셈_04장_나무베기.json"},
|
||||
"GF000289": {"원문번호": "4-3 위험목 베기(굴착기 우드그랩 0.2㎥ · 크레인 5톤)", "파일": "로직_산림품셈_04장_나무베기.json"},
|
||||
"GF000288": {"원문번호": "4-2-2 단목베기(1,000㎡당)", "파일": "로직_산림품셈_04장_나무베기.json"},
|
||||
"GF000289": {"원문번호": "4-3 위험목 베기", "파일": "로직_산림품셈_04장_나무베기.json"},
|
||||
"GF000290": {"원문번호": "5-1-1 관목굴취", "파일": "로직_산림품셈_05장_식재_및_녹화.json"},
|
||||
"GF000291": {"원문번호": "5-3-2 관목식재(단식)", "파일": "로직_산림품셈_05장_식재_및_녹화.json"},
|
||||
"GF000292": {"원문번호": "5-3-3 관목식재(군식)", "파일": "로직_산림품셈_05장_식재_및_녹화.json"},
|
||||
@@ -2860,7 +2869,7 @@
|
||||
"GF000318": {"원문번호": "8-1-2 나무주사 약제주입병", "파일": "로직_산림품셈_08장_방제.json"},
|
||||
"GF000319": {"원문번호": "8-4 끈끈이 롤 트랩 설치·제거", "파일": "로직_산림품셈_08장_방제.json"},
|
||||
"GF000320": {"원문번호": "8-6-2 드론방제(무인헬기 · 멀티콥터)", "파일": "로직_산림품셈_08장_방제.json"},
|
||||
"GF000321": {"원문번호": "8-6-3 지상방제 차량살포(동력분무기)", "파일": "로직_산림품셈_08장_방제.json"},
|
||||
"GF000321": {"원문번호": "8-6-3 지상방제 차량살포", "파일": "로직_산림품셈_08장_방제.json"},
|
||||
"GF000322": {"원문번호": "8-10 그물망 피복", "파일": "로직_산림품셈_08장_방제.json"},
|
||||
"GF000323": {"원문번호": "8-11 이동식 임목 파쇄(93.25KW)", "파일": "로직_산림품셈_08장_방제.json"},
|
||||
"GF000324": {"원문번호": "13-4-6 밑돌 및 천단 돌쌓기", "파일": "로직_산림품셈_13장_구조물.json"},
|
||||
@@ -2876,6 +2885,7 @@
|
||||
"GF000334": {"원문번호": "8-3 원목 박피", "파일": "로직_산림품셈_08장_방제.json"},
|
||||
"GF000335": {"원문번호": "10-1", "파일": "로직_산림품셈_10장_자재․장비_운반.json"},
|
||||
"GF000336": {"원문번호": "10-2", "파일": "로직_산림품셈_10장_자재․장비_운반.json"},
|
||||
"GF000337": {"원문번호": "2-2-1 체인톱 시간당 기계경비", "파일": "로직_산림품셈_02장_소요재료_및_기계손료.json"},
|
||||
"GX000001": {"원문번호": "13-4-1 돌쌓기 메쌓기(인력)", "파일": "로직_자체.json"},
|
||||
"GX000002": {"원문번호": "12-34-4 암거이음받침 채움재(T=20㎜)", "파일": "로직_자체.json"},
|
||||
"GX000003": {"원문번호": "13-4-1 돌쌓기 메쌓기(인력) (2)", "파일": "로직_자체.json"},
|
||||
|
||||
@@ -26,6 +26,7 @@ def marine(C) -> list[dict]:
|
||||
)
|
||||
row.pop("결과")
|
||||
row.pop("시험입력")
|
||||
row["이름"] = "수중펌프 설치 및 해체" # 813-2 원문 제목 · 윗절
|
||||
row["호표"] = [
|
||||
{
|
||||
"종류": "인력",
|
||||
|
||||
@@ -0,0 +1,241 @@
|
||||
"""품셈에 없는 소형 · 보조 기계를 우리 기계표 줄로(PLAN 820-1 · 요구 785 ~ 787 · 방향 장부 Q1 · R).
|
||||
|
||||
EST 단가 구성 → 우리 줄 — 손료계수 · 연료 · 잡품 · 조종원 = 품셈 값(발전기 계열 2,362 · 대형 브레이커 6,601) ·
|
||||
가격 = EST 씨앗(출처 「자체」 · 비고 「갱신 필요」) · 콘크리트펌프용 파이프 = 달러값 × 한국은행 환율(FX000001).
|
||||
계산식:
|
||||
손료만(브레이커 0.18 · 에어 호스 · 모노펌프 · 파이프 · 발전기 7 · 10㎾) = GC000273 고르기에 더함
|
||||
발전기 5.5㎾(경유 · 잡품 · 일반기계운전사) = GC000268 고르기에 더함
|
||||
→ 고르기 더하기는 `build_기계목록_빠진기계_814_1`(로직 · 수식 문서 목록 같이)
|
||||
파워트로웰(휘발유 · 잡품 · 전력 · 조종원 없음) = 새 기계경비식 GC001195 — 기계 줄 값을 바로 읽음
|
||||
(`M02_Machine_Hourly._own` 이 고름) · 잡품 = 주연료 줄의 % (전력 줄엔 안 붙임)
|
||||
더하기만 — 다른 줄 · 서식 그대로 · 두 번 돌려도 같음.
|
||||
|
||||
쓰기: venv python resources/master_data/scripts/build_기계_소형보조_820_1.py
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
DATA = HERE.parent
|
||||
ROOT = DATA.parents[1]
|
||||
MACHINE = DATA / "기계.json"
|
||||
BOOK = DATA / "_키대장.json"
|
||||
LOGIC = DATA / "로직_건설품셈_08장_건설기계.json"
|
||||
DOC = DATA / "수식문서" / "수식_건설품셈_08장_건설기계.json"
|
||||
|
||||
USD = 1414.9 # 원/USD — 한국은행 환율 FX000001(2026-08-14)
|
||||
SEED = "EST 단가 구성 → 우리 줄 · 손료계수·연료 = 품셈 값 · 가격 = EST 씨앗(갱신 필요)"
|
||||
GEN = {"상각비계수": 1125, "정비비계수": 563, "관리비계수": 674, "손료계수": 2362} # 발전기 계열
|
||||
BRK = {"상각비계수": 3000, "정비비계수": 2833, "관리비계수": 768, "손료계수": 6601} # 0230-0002
|
||||
UNIT = {
|
||||
"가격": "천원", "상각비계수": "10^-7/시간", "정비비계수": "10^-7/시간",
|
||||
"관리비계수": "10^-7/시간", "손료계수": "10^-7/시간", "주연료": "ℓ/hr", "주연료종류": "",
|
||||
"잡재료율": "주연료의 %", "조종원": "인/일", "조종원직종": "", "전력": "㎾h/hr",
|
||||
} # fmt: skip
|
||||
FIX_SRC = {"발전기": "건설품셈 공통 8-3-8", "브레이커": "건설품셈 공통 8-3-1"}
|
||||
WAGE = "건설품셈 공통 8-1-3 5 · 건설노임 1050"
|
||||
SAB = "2016 사방사업표준품셈(안) 기계경비"
|
||||
PIPE = {"150㎜×1m": 25, "150㎜×2m": 37, "150㎜×3m": 46,
|
||||
"곡관 150㎜ 45°": 67, "곡관 150㎜ 60°": 84, "곡관 150㎜ 90°": 103} # fmt: skip
|
||||
|
||||
|
||||
def _row(n, gubun, detail, name, spec, value, note, fix=None):
|
||||
src = {k: (FIX_SRC[fix] if fix and k in GEN else "자체") for k in value}
|
||||
if "조종원직종" in value:
|
||||
src["조종원직종"] = WAGE
|
||||
return {
|
||||
"키": f"EQ{n:06d}",
|
||||
"원문번호": f"자체 {name} {spec}",
|
||||
"원문": "건설품셈",
|
||||
"구분": gubun,
|
||||
"상세구분": detail,
|
||||
"이름": name,
|
||||
"규격": spec,
|
||||
"단위": {k: UNIT[k] for k in value},
|
||||
"값": value,
|
||||
"출처": src,
|
||||
"비고": f"{SEED} · {note}",
|
||||
}
|
||||
|
||||
|
||||
def rows() -> list[dict]:
|
||||
g, c, t = "[70]기타기계", "[40]콘크리트기계", "[00]토공기계"
|
||||
fuel5 = {
|
||||
"주연료": 0.625,
|
||||
"주연료종류": "경유",
|
||||
"잡재료율": 11,
|
||||
"조종원": 1,
|
||||
"조종원직종": "1050",
|
||||
}
|
||||
out = [
|
||||
_row(656, g, "발전기", "발전기", "5.5㎾", {"가격": 1063, **GEN, **fuel5},
|
||||
"EST 계 37,760 · 주연료 · 잡품 · 조종원(일반기계운전사) = " + SAB + "과 같음", "발전기"),
|
||||
_row(657, g, "발전기", "발전기", "7㎾", {"가격": 1828.9, **GEN},
|
||||
f"가격 = {SAB}(1,828,900 원) · 그 표 = 손료만(연료 · 조종원 없음)", "발전기"),
|
||||
_row(658, g, "발전기", "발전기", "10㎾", {"가격": 3213, **GEN},
|
||||
f"가격 = {SAB}(3,213,000 원) · 그 표 = 손료만(연료 · 조종원 없음)", "발전기"),
|
||||
_row(659, t, "브레이커", "대형 브레이커", "0.18㎥", {"가격": 4559, **BRK},
|
||||
"손료만(굴착기 부속) · 가격 = 0.2㎥(0230-0002) 값 · 계수 = 0.2㎥ 계수", "브레이커"),
|
||||
_row(660, g, "에어 호스", "에어 호스", "1.91㎝", {"가격": 105, "손료계수": 5625},
|
||||
"손료만 · EST 계 59"),
|
||||
_row(661, g, "모노펌프", "모노펌프", "5-20㎥/hr", {"가격": 4900, "손료계수": 4346},
|
||||
"손료만 · EST 계 2,129"),
|
||||
_row(662, c, "파워트로웰", "파워트로웰", "3.73㎾",
|
||||
{"가격": 2680, "손료계수": 5313, "주연료": 1.0, "주연료종류": "휘발유", "잡재료율": 10, "전력": 3.73},
|
||||
"주연료 휘발유 · 잡품 · 전력 3.73 ㎾h · 조종원 없음 · EST 계 3,858 · 시간당 경비 = GC001195"),
|
||||
] # fmt: skip
|
||||
for i, (spec, usd) in enumerate(PIPE.items()):
|
||||
out.append(
|
||||
_row(663 + i, c, "콘크리트펌프용 파이프", "콘크리트펌프용 파이프", spec,
|
||||
{"가격": round(usd * USD / 1000, 4), "손료계수": 4500},
|
||||
f"압송관 · 손료만 · ${usd} 표기 · 환율 적용({USD:,} 원/USD · 한국은행 2026-08-14 FX000001) 또는 원화 갱신 필요")
|
||||
) # fmt: skip
|
||||
return out
|
||||
|
||||
|
||||
EQ = "EQ000662"
|
||||
LOGIC_ROW = {
|
||||
"키": "GC001195",
|
||||
"원문번호": "8",
|
||||
"구분": "건설품셈 공통",
|
||||
"상세구분": "08장 건설기계",
|
||||
"이름": "파워트로웰 시간당 기계경비",
|
||||
"결과단위": "원/시간",
|
||||
"출처": "건설품셈 공통 8-1-5 · 8-1-6",
|
||||
"부품": {"갈래": "기계경비식"},
|
||||
"소유": "공용",
|
||||
"비고": f"품셈에 없는 기계 — {SEED} · 기계표 {EQ} 파워트로웰 3.73㎾ 값을 읽음 · 손료 = 가격 × 손료계수 · 잡품 = 주연료 줄의 % · 전력 = 기계표 전력 칸 · 조종원 없음",
|
||||
"입력": [
|
||||
{
|
||||
"이름": "지역",
|
||||
"고르기": ["전국평균", "전국", "서울", "경기", "강원", "충북", "충남", "전북", "경북", "경남", "부산", "제주", "대구", "인천", "대전", "울산", "세종", "전남광주"],
|
||||
"출처": "건설품셈 공통 8-1-7 5",
|
||||
},
|
||||
{
|
||||
"이름": "전력 계약종별",
|
||||
"고르기": ["주택용 저압", "일반용(갑)Ⅰ 저압"],
|
||||
"출처": "유가전력 한전 전기요금표 2025-01-01",
|
||||
"비고": "품셈·계약예규에 계약종별 규정 없음(전력비는 실비 계상) · 임시전력(갑)은 주택용 저압 요금 그대로(요금표 6.가)",
|
||||
},
|
||||
],
|
||||
"중간": [
|
||||
{"이름": "손료", "식": f"버림({EQ}.가격 * 1000 * {EQ}.손료계수 / 10000000, 0)", "출처": "건설품셈 공통 8-1-5 12 · 8-1-6 4"},
|
||||
],
|
||||
"호표": [
|
||||
{"종류": "재료", "요소": "MO:휘발유:{지역}", "이름": "주연료", "단위": "ℓ", "수량": f"{EQ}.주연료", "비목": "재료비", "출처": "건설품셈 공통 8-4-1"},
|
||||
{"종류": "재료", "요소": {"이름": "전력량요금", "계약종별": "입력"}, "이름": "전력", "단위": "㎾h", "수량": f"{EQ}.전력", "비목": "재료비", "출처": "자체"},
|
||||
],
|
||||
"덧줄": [
|
||||
{"이름": "잡재료비", "식": f"줄.'주연료' * {EQ}.잡재료율 / 100", "비목": "재료비", "출처": "건설품셈 공통 8-4 주②"},
|
||||
{"이름": "기계손료", "식": "손료", "비목": "경비", "출처": "건설품셈 공통 8-1-6 2"},
|
||||
],
|
||||
"끝수": "손료 원 미만 버림(건설품셈 공통 8-1-5 12) · 합계 원문 규정 없음",
|
||||
"변수표": {
|
||||
"지역": {"기호": "S", "아래": "지역"},
|
||||
"전력 계약종별": {"기호": "S", "아래": "전력계약종별"},
|
||||
"손료": {"기호": "C", "아래": "손료"},
|
||||
},
|
||||
} # fmt: skip
|
||||
|
||||
|
||||
def _block(obj: dict, nl: str) -> str:
|
||||
"""줄 하나 — 두 칸 들여쓴 JSON 을 넷 칸 자리에."""
|
||||
text = json.dumps(obj, ensure_ascii=False, indent=2)
|
||||
return nl.join(" " + line for line in text.split("\n"))
|
||||
|
||||
|
||||
def _insert_tail(path: Path, objs: list[dict]) -> None:
|
||||
"""「줄」 목록 끝(`]` 앞)에 더함 — 이미 있는 키는 건너뜀."""
|
||||
text = path.read_text("utf-8")
|
||||
have = {r["키"] for r in json.loads(text)["줄"]}
|
||||
new = [o for o in objs if o["키"] not in have]
|
||||
if not new:
|
||||
return
|
||||
nl = "\r\n" if "\r\n" in text else "\n"
|
||||
close = text.rstrip().rindex("]")
|
||||
head = text[:close].rstrip()
|
||||
body = "".join(f",{nl}{_block(o, nl)}" for o in new)
|
||||
out = head + body + nl + " " + text[close:]
|
||||
json.loads(out)
|
||||
path.write_text(out, "utf-8", newline="")
|
||||
|
||||
|
||||
def _insert_after(path: Path, after_key: str, obj: dict) -> None:
|
||||
"""키 `after_key` 블록 뒤에 줄 하나 — 이미 있으면 건너뜀."""
|
||||
text = path.read_text("utf-8")
|
||||
if f'"키": "{obj["키"]}"' in text:
|
||||
return
|
||||
nl = "\r\n" if "\r\n" in text else "\n"
|
||||
data = json.loads(text)
|
||||
at = text.index(f'"키": "{after_key}"')
|
||||
nxt = [r["키"] for r in data["줄"]]
|
||||
follow = nxt[nxt.index(after_key) + 1]
|
||||
cut = text.rindex("{", 0, text.index(f'"키": "{follow}"'))
|
||||
cut = text.rindex(",", at, cut) + 1
|
||||
out = text[:cut] + nl + _block(obj, nl) + "," + text[cut:]
|
||||
json.loads(out)
|
||||
path.write_text(out, "utf-8", newline="")
|
||||
|
||||
|
||||
def _book(machines: list[dict]) -> None:
|
||||
text = BOOK.read_text("utf-8")
|
||||
nl = "\r\n" if "\r\n" in text else "\n"
|
||||
adds = [(r["키"], r["원문번호"], "기계.json", "EQ000655") for r in machines]
|
||||
adds.append(("GC001195", "공통 8 파워트로웰 시간당 경비", LOGIC.name, "GC001194"))
|
||||
for key, number, file, after in adds:
|
||||
if f'"{key}":' in text:
|
||||
continue
|
||||
line = f' "{key}": {json.dumps({"원문번호": number, "파일": file}, ensure_ascii=False)},'
|
||||
prev = max((k for k, *_ in adds if k[:2] == key[:2] and f'"{k}":' in text), default=after)
|
||||
at = text.index(f' "{prev}":')
|
||||
end = text.index(nl, at) + len(nl)
|
||||
text = text[:end] + line + nl + text[end:]
|
||||
book = json.loads(text)
|
||||
nums = {"EQ": 669, "GC": 1196}
|
||||
for tid, n in nums.items():
|
||||
old = book["다음"][tid]
|
||||
if old < n:
|
||||
text = text.replace(f'"{tid}": {old},', f'"{tid}": {n},', 1)
|
||||
json.loads(text)
|
||||
BOOK.write_text(text, "utf-8", newline="")
|
||||
|
||||
|
||||
def _doc() -> None:
|
||||
"""GC001195 수식 문서 — 옛 로직 변환(`master_doc_convert`) · 파일 끝에 더함."""
|
||||
for p in (str(ROOT), str(HERE)):
|
||||
if p not in sys.path:
|
||||
sys.path.insert(0, p)
|
||||
import master_doc_convert as cv
|
||||
import master_doc_link as ml
|
||||
|
||||
from M01_MasterData import M01_MasterData_Store as m01
|
||||
|
||||
data = json.loads(DOC.read_text("utf-8"))
|
||||
if any(d.get("키") == "GC001195" for d in data["줄"]):
|
||||
return
|
||||
files, whole = m01.loaded()
|
||||
doc, notes = cv.convert(whole, LOGIC_ROW)
|
||||
if notes:
|
||||
print("변환 알림:", notes)
|
||||
data["줄"].append(doc)
|
||||
DOC.write_text(ml.dump(data), "utf-8", newline="")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
machines = rows()
|
||||
_insert_tail(MACHINE, machines)
|
||||
_book(machines)
|
||||
_insert_after(LOGIC, "GC000276", LOGIC_ROW)
|
||||
_doc()
|
||||
import build_기계목록_빠진기계_814_1 as b814
|
||||
|
||||
b814.main()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.path.insert(0, str(HERE))
|
||||
main()
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user