Merge remote-tracking branch 'origin/dev' into sub_laptop_3

This commit is contained in:
2026-11-02 19:48:03 +09:00
31 changed files with 1224 additions and 75 deletions
+3 -3
View File
@@ -225,10 +225,10 @@ export function createSpreadsheet(
}
function dispatch(command: Command): void {
if (readOnly) return;
if (readOnly && command.종류 !== "열폭") return; // 읽기 전용도 열 폭은 바꿈(보기만 · 저장 · 되돌리기 없음)
const eff = apply(command);
if (!eff) return;
ctx.history.push(command, eff.undo);
if (!readOnly) ctx.history.push(command, eff.undo);
after(command, eff);
}
@@ -252,7 +252,7 @@ export function createSpreadsheet(
else grid.render();
}
select(ctx.selection, false, true); // 명령이 바꾼 뒤 — 알약 점 등 문서 종속 부품도 다시(81-2)
opts.onChange?.(structuredClone(ctx.book));
if (!readOnly) opts.onChange?.(structuredClone(ctx.book));
}
/** 칸 명령이 건드린 칸(서식만 바뀐 칸도 다시 그리게) */
+33 -12
View File
@@ -13,7 +13,8 @@
* · 확대 · 축소 — 값 자체를 저장하지 않는 화면 전용 CSS 배율(C 격자 뿌리에 transform) · 계약 밖.
* ========================================================================== */
import type { Workbook } from "./spreadsheet_types";
import { colIndex, colName, parseRange } from "./spreadsheet_address";
import type { Sheet, Workbook } from "./spreadsheet_types";
import type { SpreadsheetContext } from "./spreadsheet_view_types";
export type TextMeasurer = (text: string) => number;
@@ -30,13 +31,27 @@ const defaultMeasure: TextMeasurer = (text) => {
const colFromKey = (key: string): number => {
const m = /^([A-Z]+)\d+$/.exec(key);
if (!m) return -1;
let col = 0;
for (const ch of m[1]) col = col * 26 + (ch.charCodeAt(0) - 64);
return col - 1;
return m ? colIndex(m[1]) : -1;
};
/** 그 열에 든 칸 글자(식 칸은 식 글로 어림 — 계산값은 여기서 모름) 중 가장 넓은 것 + 여백. */
/** 열마다 가장 넓은 칸 글자 폭(식 칸은 식 글로 어림 — 계산값은 여기서 모름) · 여러 열 병합 칸은 뺌(제목 줄). */
function widestByColumn(sheet: Sheet, measure: TextMeasurer): Map<number, number> {
const wide = new Set<string>();
for (const text of sheet.병합 ?? []) {
const r = parseRange(text);
if (r && r.c1 > r.c0) wide.add(`${colName(r.c0)}${r.r0 + 1}`);
}
const out = new Map<number, number>();
for (const [key, cell] of Object.entries(sheet.칸)) {
const text = cell.식 !== undefined ? cell.식 : cell.값 !== undefined ? String(cell.값) : "";
const col = colFromKey(key);
if (!text || col < 0 || wide.has(key)) continue;
out.set(col, Math.max(out.get(col) ?? 0, measure(text)));
}
return out;
}
/** 그 열에 든 칸 글자 중 가장 넓은 것 + 여백. */
export function estimateColumnWidth(
book: Workbook,
sheetId: string,
@@ -45,13 +60,19 @@ export function estimateColumnWidth(
): number {
const sheet = book.시트.find((s) => s.id === sheetId);
if (!sheet) return MIN_WIDTH;
let widest = 0;
for (const [key, cell] of Object.entries(sheet.칸)) {
if (colFromKey(key) !== col) continue;
const text = cell.식 !== undefined ? cell.식 : cell.값 !== undefined ? String(cell.값) : "";
if (text) widest = Math.max(widest, measure(text));
return Math.max(MIN_WIDTH, Math.ceil((widestByColumn(sheet, measure).get(col) ?? 0) + PADDING));
}
/** 폭 정보 없는 열(엑셀 폭을 못 읽은 파일)에 글 길이로 처음 폭을 채움 — 최소 · 최대 안(읽기 전용 보기). */
export function fillMissingWidths(book: Workbook, min = 6, max = 60): void {
for (const sheet of book.시트) {
for (const [col, widest] of widestByColumn(sheet, defaultMeasure)) {
const letter = colName(col);
if (sheet.열?.[letter]?.폭 !== undefined) continue;
const 폭 = Math.min(max, Math.max(min, Math.ceil(widest + PADDING)));
sheet.열 = { ...sheet.열, [letter]: { ...sheet.열?.[letter], 폭 } };
}
}
return Math.max(MIN_WIDTH, Math.ceil(widest + PADDING));
}
/** 폭을 자동 맞춤 값으로 바꿈(열넓이 명령 하나). */
+4 -2
View File
@@ -572,8 +572,10 @@ export function createGrid(ctx: SpreadsheetContext): GridHandle {
const contentY =
y < headRowH() ? -1 : y - headRowH() + (y - headRowH() > frozenH ? paneBR.scrollTop : 0);
if (y < headRowH()) {
const c = colGeo.indexAt(contentX);
const edge = Math.abs(colGeo.offset(c + 1) - contentX) <= RESIZE_HOTZONE;
const at = colGeo.indexAt(contentX);
const after = at > 0 && contentX - colGeo.offset(at) <= RESIZE_HOTZONE; // 경계 오른쪽 반 = 앞 열 경계
const c = after ? at - 1 : at;
const edge = after || Math.abs(colGeo.offset(c + 1) - contentX) <= RESIZE_HOTZONE;
return { kind: edge ? "colEdge" : "colHead", r: 0, c };
}
if (x < headColW()) {
+2 -2
View File
@@ -233,9 +233,9 @@ export function attachMouse(
}
function onDbl(e: MouseEvent): void {
if (e.target === ed.ta || ctx.readOnly) return;
if (e.target === ed.ta) return;
const hit = ctx.grid.hitTest(e.clientX, e.clientY);
if (!hit) return;
if (!hit || (ctx.readOnly && hit.kind !== "colEdge")) return; // 읽기 전용 = 열 경계 맞춤만
if (hit.kind === "fillHandle") return fillDown();
if (hit.kind === "colEdge") {
// 고른 열 전체의 경계면 고른 열 모두(엑셀)
+3 -2
View File
@@ -168,6 +168,7 @@ class Builder:
raise _bad(f"없는 호표 「제{no}호표」", 404)
self.src, self.dry, self._sk, self._maker = Path(source), False, None, None
self.strict = True # 남은 줄 있으면 409(붙여넣기 `build_all` = 거짓)
self.doc_id = ledger.doc_id(source, key) # 739-1 이 불러오기에서 이은 짝
# ── 재검사 ───────────────────────────────────────────────────────
def _target(self, raw: dict[str, Any], where: str, line: dict | None = None) -> dict[str, Any]:
@@ -395,11 +396,11 @@ class Builder:
count |= {"빠짐": sum(1 for x in missing if x["까닭"] != TWICE), "이음": links}
count["단가산출"] = sum(1 for x in lines if _is_sk(x))
if not save:
ledger.write(self.folder, book, self.who, file or None)
ledger.write(self.folder, book, self.who, file or None, self.doc_id)
head = {"문서": doc, "빠진줄": missing, "남은줄": left}
return {**head, "줄수": {**count, "이음후보": linker.near}}
ref, again = _save(doc)
wrote = ledger.write(self.folder, book, self.who, file or None)
wrote = ledger.write(self.folder, book, self.who, file or None, self.doc_id)
out = {"참조": ref, "다시씀": again, "빠진줄": missing, "장부": {"씀": wrote}}
return {**out, "줄수": {**count, "이음후보": linker.near}}
+47 -3
View File
@@ -6,16 +6,19 @@ A00 산출근거 스프레드시트 Workbook(`A00_Common/spreadsheet/spreadsheet
xls = xlrd `formatting_info` — 값 · 병합 · 열 폭(÷256 글자) ·
행 높이(÷20 pt · 시트 기본과 다른 것만) · 서식 표(글꼴 · 굵게 · 맞춤 · 줄바꿈 · 테두리 · 채움 ·
숫자 형식) · 틀고정(파일 값 · 없으면 머리 4행).
xlsx = 값만 · 넓은 시트는 머리 글(앞 4 줄) 마지막 열까지 · STmate 숨은 칸 글(「_x0005_`BDCOD…」) 뺌.
xlsx = 값 · 열 폭(`<cols>`) · 넓은 시트는 머리 글(앞 4 줄) 마지막 열까지 ·
STmate 숨은 칸 글(「_x0005_`BDCOD…」) 뺌.
결과 `{책, 호표: [{번, 이름, 행}], 단산: [{번, 이름, 행}]}` — 행 = 엑셀 1부터 [시작, 끝].
"""
from __future__ import annotations
import io
import zipfile
from typing import Any
from openpyxl.utils import get_column_letter
from openpyxl.xml.functions import fromstring
from M02_MasterTemplete import M02_Hopyo_Basis as hopyo_basis
from M02_MasterTemplete import M02_Hopyo_Excel as hopyo_excel
@@ -178,9 +181,42 @@ def _xls(data: bytes) -> dict[str, Any]:
}
def _xlsx_widths(data: bytes) -> dict[str, dict[str, dict[str, Any]]]:
"""시트 이름 → 열 글자 → `{폭, 숨김}` — 시트 XML `<cols>` 를 읽음(read_only 는 폭을 안 줌)."""
out: dict[str, dict[str, dict[str, Any]]] = {}
try:
with zipfile.ZipFile(io.BytesIO(data)) as zf:
tag = lambda node: node.tag.rsplit("}", 1)[-1] # noqa: E731
rels = {
r.get("Id"): r.get("Target", "")
for r in fromstring(zf.read("xl/_rels/workbook.xml.rels"))
}
for sheet in fromstring(zf.read("xl/workbook.xml")).iter():
if tag(sheet) != "sheet":
continue
rid = next((v for k, v in sheet.attrib.items() if k.endswith("}id")), None)
path = (rels.get(rid) or "").lstrip("/")
path = path if path.startswith("xl/") else f"xl/{path}"
cols: dict[str, dict[str, Any]] = {}
for col in fromstring(zf.read(path)).iter():
if tag(col) != "col":
continue
first, last = int(col.get("min", 0)), min(int(col.get("max", 0)), 200)
one = {"폭": round(float(col.get("width", 0)), 2)}
if col.get("hidden") in ("1", "true"):
one["숨김"] = True
for c in range(first, last + 1):
cols[get_column_letter(c)] = one
out[sheet.get("name", "").strip()] = cols
except (KeyError, ValueError, zipfile.BadZipFile):
pass # 폭을 못 읽으면 폭 없음 — 화면이 글 길이로 맞춤
return out
def _xlsx(name: str, data: bytes) -> dict[str, Any]:
# ponytail: xlsx 는 값만(병합 · 폭 · 서식은 openpyxl 비 read_only 로 무거움) — 필요하면 그때
# ponytail: xlsx 는 값 · 열 폭만(병합 · 서식은 openpyxl 비 read_only 로 무거움) — 필요하면 그때
found = hopyo_excel._sheets(name, data)
widths = _xlsx_widths(data)
sheets = []
for at, title in enumerate((n for n in SHEETS if n in found), 1):
rows = found[title]
@@ -196,7 +232,15 @@ def _xlsx(name: str, data: bytes) -> dict[str, Any]:
for c, v in enumerate(row[:width])
if v not in (None, "") and not str(v).startswith("_x000") # STmate 숨은 칸 글
}
sheets.append({"id": f"s{at}", "이름": title, "칸": cells, "틀고정": {"행": HEAD_ROWS}})
sheets.append(
{
"id": f"s{at}",
"이름": title,
"칸": cells,
"열": widths.get(title, {}),
"틀고정": {"행": HEAD_ROWS},
}
)
return {"서식": [{}], "시트": sheets}
+53 -11
View File
@@ -6,8 +6,10 @@
직접 줄 호표 통째 = 열쇠 「호표|품명|규격|단위」.
옛 줄 {로직|자재|빼기, 입력값, 환산, 확정} = 읽을 때 새 꼴로 바꿔 읽음(빼기 = 「안 씀」 ·
파일은 다음 쓰기 때 새 꼴).
읽는 차례 = 같은 층 → 위 층(개인 → 회사 → 마스터) · 아래 층이 이김 · 정확 열쇠가 먼저 · 없으면
품명|단위 느슨 열쇠(`규격다름`).
중앙 관리(739-1) — 자동 짝 = 마스터 층(M01 별칭 장부 승인) · 이 불러오기에서 이은 짝
(줄 `불러오기` = `doc_id`)만 · 회사 · 개인 층 짝 = 「내 제안」(`제안` True · 자동 아님 ·
후보 맨 위 · M01 승인 대기).
읽는 차례 = 자동 정확 열쇠(같은 층 → 위 층) → 제안 정확 열쇠 → 품명|단위 느슨 열쇠(`규격다름`).
쓰는 때 = [확인하고 사전조합 만들기] · 장부 화면 [저장]만 · 쓰기 = 그 층만(권한은 길 몫) ·
짝은 서버가 다시 검사(돈 로직 · 그 로직 입력 이름 · M01 자재품목 · 배 0 넘음 · 몫 (0, 1]).
"""
@@ -101,32 +103,69 @@ def books(folder: str | Path) -> list[tuple[str, str | None, dict[str, dict[str,
return [(*refs.where(one), read(one)) for one in chain(folder)]
def doc_id(folder: str | Path, doc: str) -> str:
"""불러오기 하나 열쇠 — 「층/주인/HP 키」(장부 줄 `불러오기` · 같은 불러오기 안 자동)."""
layer, owner = refs.where(folder)
return f"{layer}/{owner or ''}/{doc}"
def recall(
line: dict[str, Any], folder: str | Path, got: list | None = None, whole: bool = False
line: dict[str, Any],
folder: str | Path,
got: list | None = None,
whole: bool = False,
doc: str | None = None,
) -> dict[str, Any] | None:
"""줄(`whole` = 호표 통째)의 장부 짝 — `{…줄, 층, 주인, 규격다름}` · 없으면 None.
정확 열쇠(아래 층 먼저) → 품명|단위 느슨(규격다름 True · 체크 안 함)."""
"""줄(`whole` = 호표 통째)의 장부 짝 — `{…줄, 층, 주인, 규격다름, 제안}` · 없으면 None.
자동 정확 열쇠(마스터 · `doc` 불러오기에서 이은 짝) → 제안 정확 열쇠(회사 · 개인 층 ·
`제안` True · 자동 아님) → 품명|단위 느슨(규격다름 True · 체크 안 함)."""
full = hopyo_key(line) if whole else key(line)
got = books(folder) if got is None else got
for layer, owner, book in got:
if full in book:
return {**book[full], "층": layer, "주인": owner, "규격다름": False}
hits = [(layer, owner, book[full]) for layer, owner, book in got if full in book]
for layer, owner, row in hits:
if layer == "system" or (doc and row.get("불러오기") == doc):
return {**row, "층": layer, "주인": owner, "규격다름": False, "제안": False}
if hits:
layer, owner, row = hits[0]
return {**row, "층": layer, "주인": owner, "규격다름": False, "제안": True}
loose = _loose(full)
for layer, owner, book in got:
for one, row in book.items():
if one.startswith(HOPYO) == whole and _loose(one) == loose:
return {**row, "층": layer, "주인": owner, "규격다름": True}
return {**row, "층": layer, "주인": owner, "규격다름": True, "제안": False}
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
@@ -239,9 +278,10 @@ def write(
entries: dict[str, dict[str, Any] | None],
who: Any,
file: str | None = None,
doc: str | None = None,
) -> int:
"""검사 끝난 줄을 층 장부에 씀(None = 지움) — 같은 열쇠는 덮음 · 처음파일은 있던 값 둠 ·
옛 꼴 줄은 이때 새 꼴로 → 쓴 줄 수."""
`doc` = 이은 불러오기(`doc_id` · 없으면 있던 값) · 옛 꼴 줄은 이때 새 꼴로 → 쓴 줄 수."""
if not entries:
return 0
have = read(folder)
@@ -254,6 +294,8 @@ def write(
continue
first = (have.get(full) or {}).get("처음파일") or file
out[full] = {**row, "누가": who, "때": stamp, "처음파일": first, "판": version}
if came := doc or (have.get(full) or {}).get("불러오기"):
out[full]["불러오기"] = came
hopyo_store.remember(folder, out)
return len(entries)
+22 -5
View File
@@ -17,6 +17,9 @@
입력값 출처(칸마다) = 별칭 > 블록(옵션 찾기 값이 규격 읽기와 다름) > 규격(`spec_inputs`) > 자동값.
체크 = 처음 제안일 뿐 — 들어감은 [확인하고 사전조합 만들기](다 창)만. 저장 없음 · 셈은 서버 단독.
장부 = `M02_Hopyo_Ledger`(다 창) · 확인된 사전조합 = `M02_Hopyo_Build.confirmed`(다 창).
④ ⑤ 장부 짝 = 자동 짝만(마스터 · 이 불러오기에서 이은 짝 · 739-1) · 회사 · 개인 층 짝 =
「내 제안」 — 숫자 판정 그대로 + 꼬리표 「내 제안」 + `제안 {갈래, 짝, 대상, 층, 때}`
(고르기 창 맨 위 · 한 번 눌러 고름).
"""
from __future__ import annotations
@@ -40,6 +43,7 @@ from M02_MasterTemplete import M02_Labor_Ref as refs
from M02_MasterTemplete import M02_MasterTemplete_Store as store
SKIP = ("우리에 없음", "안 씀")
MINE = "내 제안"
LOOSE = "별칭 · 규격 다름"
LEDGER_OFF = "장부와 다름"
OUTSIDE = ("%", "노임", "뺌") # 연결 셈 밖(358-3 ②)
@@ -55,6 +59,7 @@ class Ctx:
self.got = hm.targets()
self.units = {int(h["번"]): h for h in self.doc.get("호표") or []}
self.books = ledger.books(self.to)
self.doc_id = ledger.doc_id(self.folder, key)
self.calc = fit.ensure(folder, key)
self.cache = (fit.result(folder, key) or {}).get("블록") or {}
self.done = hopyo_build.confirmed(self.to, refs.where(self.folder))
@@ -255,7 +260,8 @@ def match_line(line: dict[str, Any], ctx: Ctx, full: bool = True) -> dict[str, A
return {**out, "상태": "호표", "체크": ok, "대상": [target], "글": text}
if kind == "노임":
return {**out, "상태": "노임", "글": "노임 · 그릇 없음"}
alias = ledger.recall(line, ctx.to, ctx.books)
alias = ledger.recall(line, ctx.to, ctx.books, doc=ctx.doc_id)
mine, alias = (alias, None) if alias and alias.get("제안") else (None, alias)
loose, alias = (alias, None) if alias and alias.get("규격다름") else (None, alias)
entry = ctx.cache.get(fit.unit_key(line)) if kind not in hm.MATERIAL_KINDS else None
if alias and alias.get("갈래") in SKIP:
@@ -273,9 +279,20 @@ def match_line(line: dict[str, Any], ctx: Ctx, full: bool = True) -> dict[str, A
why = "셈 중" if not ctx.calc["끝"] else fit.NO_NUMBER
got = _names_only(line, ctx, why, full)
got = _loose(got, loose, line, ctx) if loose and full else got
got = _mine(got, mine, line, ctx) if mine else got
return {**out, **(_grouped(got, line, ctx) if full and kind not in hm.MATERIAL_KINDS else got)}
def _mine(got: dict, mine: dict, line: dict, ctx: Ctx) -> dict:
"""내 제안(739-1 · 승인 전 회사 · 개인 층 짝) — 자동 아님 · 꼬리표 + `제안`(고르기 맨 위)."""
pairs = [p for p in mine.get("짝") or [] if p.get("로직") or p.get("자재")]
kind = mine.get("갈래")
word = MINE if kind == "짝" else f"{MINE} — {kind}"
head = {"갈래": kind, "짝": pairs, "층": mine.get("층"), "때": mine.get("때")}
targets = [_pair(p, line, ctx) for p in pairs]
return {**got, "꼬리표": [*(got.get("꼬리표") or []), word], "제안": {**head, "대상": targets}}
def _grouped(got: dict, line: dict, ctx: Ctx) -> dict:
"""묶음 표시(358-1) — 후보 · 이름후보(옛 324 다섯)마다 뜻겹침 · 출처같음(조항 ≥ 0.9) · 책 ·
차례 · 무게 · 상태 그대로(아무것도 숨기지 않음 · 묶음은 화면 몫)."""
@@ -356,12 +373,12 @@ def _aliased(line: dict, ctx: Ctx, alias: dict, entry: dict | None) -> dict:
def whole(hopyo: dict, ctx: Ctx) -> dict | None:
"""직접 줄 호표 통째 제안 — 장부 짝(호표 열쇠) 먼저 · 맞춤셈 「호표:번」."""
head = {k: hopyo.get(k) for k in ("이름", "규격", "단위")}
alias = ledger.recall(head, ctx.to, ctx.books, whole=True)
alias = ledger.recall(head, ctx.to, ctx.books, whole=True, doc=ctx.doc_id)
if alias and alias.get("갈래") == "짝" and alias.get("짝") and not alias.get("규격다름"):
return {
return { # 내 제안(739-1) = 체크 안 함 · 한 번 눌러 고름
"대상": [_pair(p, head, ctx) for p in alias["짝"]],
"글": "별칭 장부 짝",
"체크": True,
"글": MINE if alias.get("제안") else "별칭 장부 짝",
"체크": not alias.get("제안"),
}
entry = ctx.cache.get(f"호표:{hopyo['번']}")
if not entry or not entry.get("차례"):
+3 -1
View File
@@ -306,6 +306,8 @@ def build(folder: Path, key: str, who: Any = None, picks: Any = None) -> dict[st
found = ledger.recall(line, folder, books)
if no in chosen or found is None or found.get("규격다름"):
alias[ledger.key(line)] = _alias(line, plan)
out["별칭"] = ledger.write(folder, alias, who, maker.hp.get("파일") or None)
out["별칭"] = ledger.write(
folder, alias, who, maker.hp.get("파일") or None, ledger.doc_id(folder, key)
)
out["같은것씀"], out["위층같음"] = maker.reused, maker.above
return out
+7
View File
@@ -11,6 +11,7 @@ from __future__ import annotations
from fastapi import APIRouter, HTTPException
from M01_MasterData import M01_MasterData_Router as m01
from M02_MasterTemplete import M02_Machine_Hourly as machine
router = APIRouter(prefix="/api/m02/logic-lib", tags=["M02 Logic Lib Read"])
@@ -71,3 +72,9 @@ def post_calc(body: m01.CalcBody) -> dict:
@router.post("/text")
def post_text(body: m01.CalcBody) -> dict:
return m01.post_text(body)
@router.get("/machine-hourly")
def get_machine_hourly(key: str) -> dict:
"""기계(EQ) 한 시간 값 노 · 재 · 경(736-3 · [항목 추가] 기계 고정 줄 단가)."""
return machine.hourly(key)
+72
View File
@@ -0,0 +1,72 @@
"""M02 기계(M01 EQ) 한 시간 값 — [항목 추가] 창에서 기계를 고르면 고정 줄 단가(PLAN 736-3).
기계 줄 = 일위대가 · 단가산출 더한줄의 고정 줄(이음 기계 EQ · 수량 1) — 단가 = 이 값(노 · 재 · 경).
셈 = M01 「건설기계」 장 기계경비 로직 가운데 그 기종을 받는 짝 차례 첫째
(`로직(GC, 기계='원문번호')` · 옵션은 기본값). 짝 차례 · 셈은
`DataFlow/M02_DF_Values._machine_hourly` 와 같은 규칙(그 폴더는 시험 길이라 따로 둠).
마스터 파일은 읽기만 · 로그인 사용자 모두.
"""
from __future__ import annotations
from typing import Any
from M01_MasterData import M01_MasterData_Store as store
cm = store.cm
_PLAIN = {"기계", "지역", "보정작업", "조종원직종"} # 이 밖의 입력 = 부착 장비 전용
def _family() -> list[dict]:
"""기계 시간당 경비 로직 — 「건설기계」 장 파일 안 · 입력 「기계」 고르기가 있는 돈 로직."""
return [
row
for name, data in store.loaded()[0].items()
if data.get("그룹") == "로직" and "건설기계" in name
for row in data.get("줄") or []
if str(row.get("결과단위") or "").startswith("원")
and any(i.get("이름") == "기계" and i.get("고르기") for i in row.get("입력") or [])
]
def _codes(row: dict) -> list:
return next(i["고르기"] for i in row["입력"] if i.get("이름") == "기계")
def _rank(row: dict) -> tuple:
"""부착 전용 · 손료만 · 보정 · 기종 수 · 키 — 작을수록 먼저."""
names = [i.get("이름") for i in row.get("입력") or []]
attach = any(n not in _PLAIN for n in names)
loss = "지역" not in names
return (attach, loss, loss and "보정작업" in names, len(_codes(row)), str(row.get("키")))
def hourly(key: str) -> dict[str, Any]:
"""`{ok, 이름, 규격, 노무비, 재료비, 경비, 계}` · 못 셈 = `{ok: False, reason}`."""
if not key.startswith("EQ"):
return {"ok": False, "reason": f"「{key}」 는 기계 아님"}
try:
row = store.loaded()[1].get(key)
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:
return {"ok": False, "reason": f"「{row.get('이름')}」 은 시간당 경비를 셀 자료가 없음"}
logic = min(fit, key=_rank)
args = "".join(
f", {i['이름']}='{(i.get('고르기') or [''])[0]}'"
for i in logic.get("입력") or []
if i.get("이름") != "기계"
)
call = f"로직({logic['키']}, 기계='{code}'{args})"
line = {"종류": "로직", "요소": call, "이름": "기계", "단위": "시간", "수량": "1"}
holder = {
"키": "", "원문번호": "", "이름": "기계 시간", "결과단위": "원/식", "출처": "자체",
"입력": [], "중간": [], "호표": [line], "덧줄": [], "끝수": None,
} # fmt: skip
got = store.calc("", {}, holder, None)
if not got.get("ok") or "sums" not in got:
return {"ok": False, "reason": got.get("reason") or "셈 못 함"}
sums = {k: float(got["sums"][k]) for k in (*cm.mf.COST_ITEMS, "계")}
return {"ok": True, "이름": row.get("이름"), "규격": row.get("규격"), **sums}
@@ -49,6 +49,8 @@ export interface M01Element {
규격: string;
단위: string;
값: number | null;
/** 기계(736-3) — 한 시간 노 · 재 · 경(서버 `/logic-lib/machine-hourly` · 고르는 순간 받음) */
단가?: Record<"노무비" | "재료비" | "경비", number>;
}
/** 고를 수 있는 종류 — logic = M01 단가산출 · 기초 로직 · labor = 인력 · material = 재료 · machine = 기계 · sk = M02 단가산출 · pre = 일위대가 */
@@ -80,6 +82,16 @@ export const logicHeads = (rows: StandardRow[]): StandardRow[] =>
/** 일위대가 고정 줄(705-1 이음 꼴) — 인력 = 노임 LB · 재료 = 자재 MT · 단가 = 지금 값(셈은 단가 책 값) */
export function fixedOf(e: M01Element): LaborFixed {
if (e.종류 === "기계") {
// 736-3 기계 = 이음 기계 EQ · 단가 = 한 시간 노 · 재 · 경(셈은 줄 단가 그대로)
return {
이름: e.이름,
규격: e.규격,
단위: e.단위,
단가: e.단가 ?? { 노무비: 0, 재료비: 0, 경비: 0 },
이음: { 노임: null, 자재: null, 기계: e.코드 },
};
}
const labor = e.종류 === "인력";
return {
이름: e.이름,
@@ -171,6 +183,8 @@ export function openFindPicker(opt: FindPickOptions): void {
rowClass: (i) => (shown[i]?.dim ? "is-dim" : null),
onRowClick: (i) => shown[i]?.go(),
});
const fail = (error: unknown): void =>
showToast(error instanceof Error ? error.message : tc("Load_Failed"), "error");
const take = (f: () => void) => (): void => {
close();
f();
@@ -199,6 +213,20 @@ export function openFindPicker(opt: FindPickOptions): void {
},
];
});
/** 기계 고름 — 한 시간 값을 받아 단가로(못 셈 = 까닭 알림 · 창 그대로) */
const hourly = async (e: M01Element): Promise<M01Element | null> => {
try {
const got = await call<{ ok: boolean; reason?: string } & Record<string, number>>(
`/logic-lib/machine-hourly?key=${encodeURIComponent(e.코드)}`,
);
if (!got.ok) throw new Error(got.reason ?? tc("Load_Failed"));
const { 노무비, 재료비, 경비, 계 } = got;
return { ...e, 값: 계, 단가: { 노무비, 재료비, 경비 } };
} catch (error) {
fail(error);
return null;
}
};
const elementLines = (kind: M01Element["종류"], items: PickItem[]): Line[] =>
items.map((m) => {
const e: M01Element = {
@@ -223,7 +251,11 @@ export function openFindPicker(opt: FindPickOptions): void {
구분: join(kind, m.구분, put ? "" : L("M02_FindElementLater")),
출처: `M01 ${m.ref}`,
dim: !put,
go: put ? take(() => put(e)) : () => showToast(L("M02_FindElementLater"), "warning"),
go: !put
? () => showToast(L("M02_FindElementLater"), "warning")
: kind === "기계"
? () => void hourly(e).then((x) => x && take(() => put(x))())
: take(() => put(e)),
};
});
const docLines = (rows: SangeunRow[], kind: string): Line[] =>
@@ -346,8 +378,6 @@ export function openFindPicker(opt: FindPickOptions): void {
search.input.focus();
draw();
const fail = (error: unknown): void =>
showToast(error instanceof Error ? error.message : tc("Load_Failed"), "error");
void Promise.all([fetchLibLogics(), fetchStandard().catch(() => null)])
.then(([l, s]) => {
logics = l;
@@ -153,6 +153,8 @@ export interface HopyoMatch {
갈리는칸?: string[];
별칭?: { 층: Layer; 주인: string | null; 규격다름?: boolean; 근거다름?: boolean };
뺌?: string;
/** 739-1 내 제안(승인 전 회사 · 개인 층 장부 짝) — 자동 아님 · 고르기 창 맨 위 */
제안?: { 갈래: string; 짝: LedgerPair[]; 대상: HopyoPart[]; 층: Layer; 때: string | null };
}
/** 숫자 판정 셈 — 끝 · 한 블록 · 모두 · 까닭(우리 로직 준비 중 등) */
@@ -405,6 +407,8 @@ export interface LedgerRow extends LedgerEntry {
층: Layer;
주인: string | null;
고침: boolean;
/** 739-1 M01 승인 상태 — 마스터 줄 = 승인 */
상태?: "승인" | "대기" | "반려";
}
const enc = encodeURIComponent;
@@ -189,7 +189,9 @@ function actsCell(line: HopyoLine, parts: HopyoPart[], ctx: RowsContext): HTMLEl
return el("td", {
className: "m02-hopyo__acts",
children: [
...(ctx.canWrite && m.상태 !== "%" ? [ghost("고르기", () => ctx.choose(line))] : []),
...(ctx.canWrite && m.상태 !== "%"
? [ghost(m.제안?.짝.length ? "고르기 · 내 제안" : "고르기", () => ctx.choose(line))]
: []),
ghost("원문 보기", () => ctx.source(line)),
...(line.근거 ? [ghost("엑셀 보기", () => ctx.sheet(line.근거!.행))] : []),
...options,
@@ -224,6 +224,37 @@ export function openHopyoChoose(opt: ChooseOptions): void {
),
);
const groups = candGroups(line.맞춤, material);
// 739-1 내 제안 — 맨 위 · 한 번 눌러 짝 통째로(입력값 · 배 · 몫 그대로)
const mine = line.맞춤.제안?.짝.length ? line.맞춤.제안 : null;
const mineRow = mine
? el("div", {
attrs: { "data-group": "내 제안" },
children: [
note("내 제안 — 승인 전(M01 별칭 장부 승인 뒤 자동)"),
el("div", {
className: "m02-hopyo__cand",
attrs: { "data-cand": "내 제안" },
children: [
createButton({
label: mine.대상.map((c) => candText(c, new Set())).join(" + "),
variant: "ghost",
size: "sm",
onClick: () =>
finish(
mine.짝.map((p) => ({
...(p.로직 ? { 로직: p.로직 } : { 자재: p.자재 }),
입력값: p.입력값 ?? {},
배: p.배,
몫: p.몫 ?? null,
})),
),
}),
el("span", { className: "m02-hopyo__state is-별칭", text: "내 제안" }),
],
}),
],
})
: null;
const twins = sameNames(groups);
const row = (c: HopyoTarget): HTMLElement => {
const share = el("input", {
@@ -332,6 +363,7 @@ export function openHopyoChoose(opt: ChooseOptions): void {
? "후보 — 하나 고름 · 여럿은 체크(몫 칸) 뒤 [일괄 적용]"
: "가까운 것 없음 — 아래 검색",
),
...(mineRow ? [mineRow] : []),
...sections,
el("div", { className: "ui-toolbar m02-hopyo__bar", children: [together, ...more] }),
);
@@ -7,8 +7,11 @@
* 위 층 줄 [내 층 복사] · 갈래 글 = 확인 창 드롭다운과 같음(짝 · 로직없음 · 사용안함 · 저장 값 그대로)
* · 고친 것 = 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";
@@ -38,6 +41,9 @@ 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 };
@@ -139,6 +145,7 @@ export function buildHopyoLedger(opt: LedgerOptions): HTMLElement {
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", {
@@ -249,7 +256,7 @@ export function buildHopyoLedger(opt: LedgerOptions): HTMLElement {
if (mine)
acts.push(
createButton({
label: "삭제",
label: master ? "삭제" : "거두기",
variant: "danger",
size: "sm",
onClick: () => {
@@ -282,6 +289,10 @@ export function buildHopyoLedger(opt: LedgerOptions): HTMLElement {
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 }),
],
});
@@ -296,7 +307,7 @@ export function buildHopyoLedger(opt: LedgerOptions): HTMLElement {
body.replaceChildren(
...(shown.length
? shown.map(rowOf)
: [el("tr", { children: [el("td", { text: "장부 줄 없음", attrs: { colspan: "8" } })] })]),
: [el("tr", { children: [el("td", { text: "장부 줄 없음", attrs: { colspan: "9" } })] })]),
);
}
search.addEventListener("input", paint);
@@ -332,8 +343,12 @@ export function buildHopyoLedger(opt: LedgerOptions): HTMLElement {
el("div", {
className: "ui-toolbar m02-hopyo__bar",
children: [
el("strong", { text: LEDGER_TITLE }),
note(`${LEDGER_HINT} · ${L("M02_PrecomboTitle")} 생성 때 남음 · 아래 층이 이김`),
el("strong", { text: master ? LEDGER_TITLE : MINE_TITLE }),
note(
master
? `${LEDGER_HINT} · 여기 짝 = 모든 사용자 불러오기에 자동`
: `내 층 짝 = 내 제안(다음 불러오기에서 후보 맨 위 · 자동 아님) · M01 별칭 장부 승인 뒤 자동 · ${L("M02_PrecomboTitle")} 생성 때 남음`,
),
search,
kind,
...(opt.canWrite ? [save, drop] : []),
@@ -356,6 +371,7 @@ export function buildHopyoLedger(opt: LedgerOptions): HTMLElement {
"우리 로직 · 배 · 몫 · 옵션",
"누가 · 때 · 처음 파일",
"층",
"승인",
"",
].map((t) => el("th", { text: t })),
}),
@@ -336,8 +336,10 @@ export function openHopyoSheet(opt: HopyoSheetOptions): void {
async function mountSheet(grid: HopyoGrid): Promise<void> {
const { createSpreadsheet } = await import("../A00_Common/spreadsheet/spreadsheet");
const { fillMissingWidths } = await import("../A00_Common/spreadsheet/spreadsheet_autofit");
if (closed) return;
const book = structuredClone(grid.책);
fillMissingWidths(book); // 엑셀 폭이 없는 열 = 글 길이로 처음 폭
const first = book.시트.find((s) => s.이름 === ILWI) ?? book.시트[0];
if (!first) return void left.replaceChildren(note("시트 없음"));
ilwi = first.id;
@@ -449,7 +449,10 @@ export function createItemCards(opts: ItemCardsOptions): ItemCardsHandle {
const { createSpreadsheet } = await loadSpreadsheet();
if (mine !== seq || !c.doc) return;
if (opts.sheetEdit?.can()) return await paintEditable(c, mine, createSpreadsheet);
c.handle = createSpreadsheet(c.host, c.doc, { readOnly: true, tabs: false });
c.handle = createSpreadsheet(c.host, structuredClone(c.doc), {
readOnly: true,
tabs: false,
}); // 열 폭을 끌어 바꿔도 원본 안 건드림
} catch (error) {
if (mine === seq) c.shell.fail(fill(TEXT.noSheet, why(error)), reload);
}
@@ -109,6 +109,7 @@ export function openLogicPicker(
logic: done,
ref: lib.pick,
element: fixed && ((e) => fixed(fixedOf(e), e.이름)),
machine: fixed && ((e) => fixed(fixedOf(e), e.이름)), // 736-3 기계 = 이음 기계 고정 줄
material: lib.material,
});
}
@@ -9,8 +9,8 @@
* ========================================================================== */
import { call, type Layer } from "./M02_MasterTemplete_Api_Fetch";
import { openFindPicker } from "./M02_MasterTemplete_Find_Pick";
import type { LaborRef } from "./M02_MasterTemplete_Labor_Calc";
import { fixedOf, openFindPicker } from "./M02_MasterTemplete_Find_Pick";
import type { LaborFixed, LaborRef } from "./M02_MasterTemplete_Labor_Calc";
export const SANGEUN_KIND = "sangeun";
/** 층 차례 — 위에서 아래 */
@@ -73,7 +73,8 @@ export async function listLayerDocs(
export type SangeunLine =
| { 종류: "로직"; 로직: string; 입력값: Record<string, never>; 수량: number }
| { 참조: LaborRef; 수량: number }
| { 종류: "인력" | "재료"; 요소: string; 이름: string | null; 단위: string | null; 수량: number };
| { 종류: "인력" | "재료"; 요소: string; 이름: string | null; 단위: string | null; 수량: number }
| { 고정: LaborFixed; 이름: string; 단위: string | null; 수량: number };
/** 단가산출 [항목 추가] — `scope` = 그 SK 문서 층(고를 수 있는 서고 = 위 층만) */
export function openSangeunLinePicker(
@@ -87,5 +88,7 @@ export function openSangeunLinePicker(
element: (e) =>
e.종류 !== "기계" &&
done({ 종류: e.종류, 요소: e.코드, 이름: e.이름, 단위: e.단위, 수량: 1 }),
// 736-3 기계 = 고정 줄(이음 기계 EQ · 단가 = 한 시간 노 · 재 · 경) · 수량 1
machine: (e) => done({ 고정: fixedOf(e), 이름: e.이름, 단위: e.단위, 수량: 1 }),
});
}
@@ -0,0 +1,159 @@
# 분석 582-1 · 2 · 3 — 단가산출 수량 원복 · 불러오기 수량 뜻 · 불러온 자료 휘발 (11-02 · 조사 대리 · 읽기만)
실측 = 서버 셈 함수 직접 부름(`M02_Sangeun_Calc.run` · 저장 없음) · storage 읽기만 · 실험 파일 `tmp/brain_582/sk_scan.py` · `add_probe.py` (결과 `sk_scan.out`).
582-2 뒷절(비고 열) · 582-4(REA) · 582-5(열 폭) = 이 조사 밖.
## 한눈에
- 「원복 요청」 기록 = 장부 · 대화 어디에도 「원복」 없음 · 가장 가까운 것 = **내역 19-2**(11-02 07:16 · 「기준 단위 1 오해 · 1 로 전부 표현한 값 아님」) → PLAN 727(기초 넣기 = 세부 줄) · 728(단위 환산). 데스크탑 PC 대화는 못 봄.
- **SK 첫 기초 세부 품 = M01 그대로** — GF000099 = 보통인부 0.023 · 굴착기 0.01546(사용자 「테스트1」 1/3 SK-000002 도 같음). SK 문서 자체를 723-1 이 지우는 길 없음(`M02_Labor_Ref.py:159-162` SK 는 `no_qty` 앞에서 빠짐).
- 그래도 **화면 수량이 M01 과 달라 보이는 길 넷** + **723-1 이 SK 로 번지는 길 하나**(SK 안 일위대가 줄 = 「수량 없음」 → 그 SK 를 쓰는 곳 통째 기준 없음) + **되돌림 빈틈 셋**.
- 723-1 은 지금 **출처 · 줄 갈래 가리지 않고** 일위대가 안 줄 수량 전부 저장 안 함(비율 줄만 남김). 582-2 「불러오기 경우」와 17-1 「단가산출행 = 연결」 사이에 **M01 요소 줄(인력 · 재료 · 기계 품)** 이 낀 회색 지대.
- 불러오기 [끝내기] 뒤에도 남는 것 = 별칭 장부(뜻대로) · 끝낸 기록(같은 엑셀 409) · 옛 HP 다섯 · 만든 SC · SK 안 엑셀 사본(실무수량 · 엑셀품 · 메모 「실무 …」).
## A. (582-1) 기준값 삭제 → 원복
### A-1 원복 요청 흔적
| 찾은 곳 | 결과 |
| --- | --- |
| `docs/raw/요구_1102` · `요구_내역서` · `docs/raw` 전체 「원복」 | 582 자신뿐 |
| 브레인 · sub1~8 대화(11-01 15:00Z ~ 11-02 09:38Z 사용자 글 55 줄) | 「원복 · 기준값」 0 · 「기준 단위 1 · 1로 전부」 = 내역 19-2 하나(11-01T22:16Z) |
| `tmp/brain_565/_분석_내역19-A.md:7 · 53` | 그때 상태 = 기초를 [항목 추가]로 넣으면 「한 줄 · 수량 1 · 세부 안 보임」 → 727 이 고침 |
| `_합친것/`(735 지움 · 733 합치기 되돌림 자리) | 1/3(엄상돈) `SK-000004 강관비계`(바탕 GC000022 · 덮개 0 · 입력 높이 10m이하 · 보호망 시공 안 함) **11-02 17:05:32 엄상돈 지움** · 고친곳 0 — 입력값이 1/3 SC-000002 강관비계 줄과 같음 = 그 줄 [단가산출로 저장]으로 만든 뒤 지운 것으로 보임 · **되돌림 됨**(`POST /layers/personal/sangeun-same/undo {키}` · `M02_Library_Same_Router.py:373`) |
| 1/10(엄상준) `_합친것/` | SC 15 개 지움(18:00~18:02 · 엄상준) |
### A-2 기초 넣기 → 화면 수량 (M01 대비)
| 길 | 지금 화면 | M01 과 같음? | 근거 |
| --- | --- | --- | --- |
| 첫 기초(바탕) · 새 SK(단위 「식」) | 세부 품 = M01 1 기준단위 품 · 단위 = 기초 기준단위 | 같음 | `Sangeun_Detail.ts:137-142 · 473-476` |
| SK 단위 ≠ 기초 기준단위(사용자가 먼저 고침 · 불러오기 엑셀 단위) | 세부 품 = M01 ÷ 배 | 다름(뜻한 환산 · 728-2) | `M02_Sangeun_Calc.py:74-93 · 381-403` · 예 1/10 SK-000011 단끊기(GF000298 100m → m) 5.17 → **0.0517** |
| 둘째 기초(더한줄 로직) | 세부 품 = M01 × 처음 수량(바탕 기준단위 ÷ 그 기초 기준단위) · 그 비율 줄은 화면에 안 보임 | 다름 | `Sangeun_Detail.ts:118-135` · `Cover_Model.ts:155-157`(기초 줄을 세부로 갈음) · 실측 바탕 ㎥ + GF000316(100㎥) = 처음 0.01 → 세부 0.871 이 **0.00871** · ㎥ 바탕에서 이런 기초 6 · 앞 수 있는 기준단위 기초 83 |
| [항목 추가] 「M02 단가산출」 탭(SK 넣기) | 한 줄 · 수량 1 · 세부 안 보임 | 세부 없음 | 727 확정 「SK 넣기 = 한 줄」 · 581 → PLAN 740-1 펼침 예정 |
| [항목 추가] 「일위대가」 탭(SC 넣기) | 한 줄 · **「—」 수량 없음 — 쓰는 자리에서 연결** · 계 빠짐 | 값 없음 | 아래 A-4 |
| 불러오기로 만든 SK | 세부 품 = 엑셀 품(덮개 · 원값 = M01) · 엑셀 품이 기초 둘로 갈림 | 다름(뜻한 보존 · 729-1) | 1/10 SK-000007 철근 = 철근공 1.714 + 0.667 = 엑셀 2.381 |
| 옵션(입력값)이 M01 시험 기본과 다름 | 식 값 다름 | 다름(옵션 몫) | 1/10 레미콘 SK-000002 0.0952 · SK-000005 0.0476 |
### A-3 지우기 → 되돌리기 길 (SK 상세)
| 지운 것 | 남는 꼴 | 되돌림 | 근거 |
| --- | --- | --- | --- |
| 바탕 세부 줄 [줄 빼기] | `뺀줄` | 뺀 줄 띠 [되살림] 됨 | `Cover_Model.ts:347 · 364-373` · `Sangeun_Cover.ts` removedStrip |
| 둘째 기초 세부 줄 빼기 | 그 더한줄 `뺀줄` | [되살림] 됨 | 같음 |
| 품 0 · 다른 수 | 덮개 `{qty, 원값}`(0 받음 · 음수만 막음) | 모달 [되돌림] 됨 | `M02_Sangeun_Doc.py:134-149` · `LogicLab_Cover.ts:147-156` |
| 품 칸 비움 | 고침 아님 → 고침 풂 = M01 값 | 됨 | `LogicLab_Cover.ts:98-101 · 129-141` |
| **[기초 빼기](둘째 기초 통째)** | 더한줄 지움 | **없음** — 다시 넣으면 세부 덮개 잃음 | `Cover_Model.ts:376-378` |
| **더한줄 요소 줄 빼기**(인력 · 재료 · 참조) | 더한줄 지움 | **없음** | `Cover_Model.ts:343-345` |
| **더한줄 요소 수량 고침 → [되돌림]** | 단가만 풂 · **수량 그대로**(처음 1 로 안 돌아감) | **반쪽** | `Cover_Model.ts:314-320` · `hoView` 가 더한줄을 늘 「고침 있음」으로 봄 `:508` |
| SK 문서 지움 | `_합친것/` | [중복 찾기] 되돌림 됨 | 733-3 · 735-1 |
| 첫 기초 통째 | 단추 없음(세부 줄마다 빼기만) | — | `Sangeun_Detail.ts:380-412` |
화면 되돌리기(Ctrl+Z) 없음 · [저장] 전이면 다시 열어 버림 · 한 번에 M01 로 = 740-2 [초기화](아직).
### A-4 723-1 이 SK 로 번지는 길
- SK 문서 저장 · 불러오기 SK 만들기 = 품 안 지움(`M02_Labor_Ref.py:159-162` · `M02_Hopyo_SkPatch` 덮개 · 더한줄 수 그대로).
- **번짐 = SK 안 일위대가(SC) 줄** — SC 안 줄 수량 null(`M02_Labor_Ref.py:181-185`) → 펼침 기준 없음(`M02_Labor_Expand.py:155-158`) → SK 그 줄 `_gap` 「수량 없음 — 쓰는 자리에서 연결」(`M02_Sangeun_Calc.py:281-293`) → **그 SK 를 일위대가 · 공사가 쓰면 통째 기준 없음**(`:354` 「빠진 줄 있으면 통째」).
- 실측: 바탕 GF000099 + 더한줄 참조 1/3 SC-000001 → add:#0 금액 None · 빠진줄 1 · 계 = 바탕만.
- 740-1(sub3 · SK · SC 넣기 = M01 키 줄 + 덮개 사본 펼침)이 SC 를 SK 더한줄로 펼칠 때 **SC 안 줄 수량이 없음** → SK 더한줄 `수량` 은 수 필수(`M02_Sangeun_Doc.py:156` 「수가 아님」 400) → 펼칠 수량 꼴을 B 갈래로 먼저 정해야 함.
### A-5 storage SK(모든 층 · 18 문서 · 마스터 층 SK 없음)
| 층(누구) | 키 · 이름 | 바탕 | M01 과 다른 것 |
| --- | --- | --- | --- |
| 1/3 엄상돈 | SK-000002 테스트1 | GF000099 | 없음(0.023 · 0.01546) · 더한줄 보통인부 0.2 |
| 1/3 | SK-000003 테스트2 | GC000211 | 없음(옵션 몫 0.168 · 0.067 · 0.062) |
| 1/3 | SK-000001 wd | 없음 | 빈 SK(계 0) |
| 1/3 `_합친것` | SK-000004 강관비계 | GC000022 | 지움(17:05) · 덮개 0 |
| 1/10 엄상준 | SK-000007 · 013 철근가공 및 조립 | GC000207 + GC000206 | 덮개 품 3(철근공 1.714 ← 2.083 · 보통인부 0.571 ← 0.833 · 결속선 6.5 ← 9.5 / 2.083 ← 3.529 · 0.828 ← 1.176 · 8 ← 6.5) |
| 1/10 | SK-000009 레미콘타설(장비사용) | GC000198 | 덮개 3(0.09 ← 0.0952 · 0.02 ← 0.0317 · 굴착기 0.31 ← 0.254) |
| 1/10 | SK-000010 평떼붙임 | GC000143 | 덮개 보통인부 0.0291 ← 0.0267 |
| 1/10 | SK-000011 단끊기 | GF000298 | 단위 m(기준 100m) → 화면 ÷100 |
| 1/10 | SK-000002 · 003 · 004 · 005 · 006 · 008 · 012 | GC000198 · 211 · 214 | 덮개 0(옵션 몫만) |
| 1/10 | SK-000001 덤프 운반 · SK-000014 800mm 파형강관 매설 | 없음 | **빈 SK**(더한줄 0 · 계 0 · 16:46 · 18:25) |
| 3/15 브레인사용자 | SK-000001 뒷채움돌… | GC000106 | 덮개 보통인부 0.039 ← 0.0091 · 뺀줄 살수차 · 더한줄 기초 셋(시험 표본) |
1/10 SK-000002~013 = 17:50 한 번에(불러오기) · 1/3 SC 둘 = 17:00 · 17:05(거창 파일 불러오기 · 줄 9 개 모두 수량 null · 메모 「실무 0.33㎥ × 1」 등).
### A-6 가름
- SK 첫 기초 = 원복 됨(727). 사용자가 「안 됨」으로 볼 만한 자리 = ① 일위대가 안 기초 · 일위대가 줄 수량 빈칸(723-1 · 사용자 1/3 일위대가 두 문서 9 줄 모두) ② SK 안 일위대가 줄 「—」 ③ 둘째 기초 세부가 비율로 작아짐 ④ SK 넣기 = 한 줄(740 몫) ⑤ 단위 바꾼 SK ÷배 · 불러온 SK 엑셀 품.
- 어느 화면인지 확정 못 함(사용자 화면 · 데스크탑 대화 못 봄) — 고칠 길은 ①~④ 를 모두 덮게 짬(D).
## B. (582-2) 「수량 삭제」 원뜻 대 지금 723-1
### B-1 지금 꼴
| 일위대가 안 줄 | 저장 | 화면(일위대가표) | 셈 |
| --- | --- | --- | --- |
| 기초 로직 줄 | null(`no_qty`) | 수량 칸 = 휘발 임시 입력(`Labor_Table.ts:6 · 80-102`) | 알약 · 공사 값 없으면 「수량 없음」 |
| SK · SC 참조 줄 | null | 같음 | 같음 |
| 인력 · 재료 · 기계 고정 줄(품) | **null** | 같음 | 같음 |
| 비율 고정 줄 | 율 남김 | — | 셈 |
| 자재 | null | 같음 | 같음 |
| 불러오기 엑셀 수량 | null + `본뜬.실무수량` · 줄 메모 「실무 N × 1」 | — | 엑셀 대조만(`M02_Hopyo_Build.py:100-106 · 481-498`) |
출처(사용자가 만듦 · 불러오기 · 복사 · 공유 승인) 가리지 않음 · 옛 손은 읽을 때 무시(`M02_Labor_Expand.py:43-50`) · 마스터 SC-000001 「테스트 사전조합」 옛 손 2 줄(읽을 때 무시).
### B-2 원문 맞물림
- 16-8 「일위대가에 수량산출 수기 정보 필요 없음 · 불러오기에서도 필요 없음 · 구조물 조합 · 프로젝트에서 입력 · 입력하면 휘발 확인」 · 17-1 「일위대가 **단가산출행** 수기 = 기준단위 당 수량 · 구조물 조합 알약 · 프로젝트 계산값과 연결 · 만들 때 휘발 임시 입력」 → 723-1 은 이 둘을 **모든 줄**로 넓혀 구현.
- 582-2 「수량 삭제 = 불러오기 엑셀 경우 · 엑셀은 일위대가 · 단가산출 경계가 모호(단가산출에 덧붙임 · 일위대가에 M01 요소) · 사용자가 완벽히 고른 경우만」 → 17-1 을 거두는 말로도, 불러오기 쪽 까닭을 다시 푼 말로도 읽힘.
- 겹치지 않는 자리 = **일위대가에 넣은 M01 요소 줄(보통인부 0.5 인 등) 품** — 17-1 「단가산출행」 밖 · 582-2 「일위대가에 M01 요소」가 바로 이것 · 지금은 저장 안 됨(사용자가 만든 일위대가도).
- 582-1 「수량 유지 · 사용자가 바꿈」은 SK(단가산출) 몫 — SK 는 지킴. 단 740-1 이 SC 를 SK 로 펼치면 이 갈래가 SK 로 들어옴.
### B-3 갈래 = 아래 D 물음 1.
## C. (582-3) 불러오기에서 넣은 자료가 남는 곳
| 무엇 | 어디 | 언제 지워지나 | 582-3 뜻 |
| --- | --- | --- | --- |
| 읽은 엑셀 결과(HP · 원본 엑셀은 안 둠) | `{층}/실무자료/HP-######.json`(층마다 4~5 MB) | [끝내기] · 엑셀 보기 [삭제](`M02_Labor_Router.py:501-527` · `Hopyo_Store.py:140-157`) · 안 누르면 계속(작업 중 이어서) | 작업 중 보관 — 갈래(물음 2) |
| 엑셀 보기 격자 | `실무자료/격자/HP-*.json` | HP 와 같이 | 같음 |
| 맞춤 셈 캐시 | `tmp/m02_hopyo/맞춤_{hp}_*.json`(git 밖) | [끝내기] | 맞음 |
| **옛 HP(지문 없음 · 718 전)** | 1/10 HP-000001 · 002 · 1/3 HP-000002 · 1/9 HP-000001 · 회사 1 HP-000001(+ 격자) | **안 지워짐**(목록에서 숨김만 · `Hopyo_Store.py:71-78`) | 어긋남 — 찌꺼기 |
| 작업 중 HP(지금) | 1/3 HP-000003 거창(11-01) · HP-000004 기번3(11-02 18:24) · 1/10 HP-000003 · 004 | [끝내기] 전 | 갈래 |
| **끝낸 기록** | `실무자료/불러옴.json` {번, 지문 {파일, 키, 끝낸때, 만든}} | 안 지워짐 | 어긋남 — 같은 엑셀 다시 올리면 409 「이미 불러온 엑셀」(`M02_Labor_Router.py:230-239`) |
| 별칭 장부(맞춤 기억 · 내 제안 · M01 승인 대기) | `실무자료/맞춤.json` · M01 중앙(739) | 안 지워짐 | **남김이 뜻**(내역 12 · 580) |
| 만든 SC 의 엑셀 사본 | `본뜬.실무수량` · 줄 `메모` 「실무 0.33㎥ × 1 · 규격」 · `별칭` = 엑셀 품명 · `구분` 「실무자료」 · `상세구분` = 파일 이름 · `본뜬 {키 「HP 제N호표」, 파일, 원본, 확인, 품셈, 줄수, 전국자재}` | SC 와 같이 | 실무수량 · 메모 수량 = 별도 저장 |
| 만든 SK 의 엑셀 사본 | `본뜬 {키 「HP 단산N」, 파일, 원본, 확인, 갈래, 엑셀품, 못옮김, 놓음?, 또?}`(`M02_Hopyo_Sangeun.py:145-153`) | SK 와 같이 | 엑셀품 = 별도 저장(품은 이미 덮개에) |
| 확인 창 고름 | sessionStorage `m02-hopyo-check:{층}:{주인}:{키}:{번}` | 탭 닫으면 | 맞음 |
### C-1 「SK · SC 로 저장된 것만 남고 나머지는 휘발」로 가려면
- 지울 것: [끝내기] 때 만든 SC · SK 본뜬의 엑셀 사본 칸(실무수량 · 엑셀품 · 못옮김 · 놓음 · 품셈 · 줄수 · 비교) · SC 줄 메모 「실무 …」 · 옛 HP 다섯 + 격자(한 번) · 끝낸 기록 지문 막음(409 → 알림만 · 같은 내용 SK · SC 는 733 이 이미 기존 것 씀).
- 남길 것: SC · SK 문서 · 별칭 장부 · 출처 최소(파일 · 확인 누가 · 때 = 740 출처 꼬리표 꼴) · 별칭(줄 이름).
- 끝내기 전(작업 중)은 지금 칸 모두 필요 — 걷는 때 = [끝내기] 한 번.
### C-2 영향
| 기능 | 기대는 것 | 걷으면 |
| --- | --- | --- |
| 733-1 중복(지문) | 지문에 본뜬 · 메모 · 별칭 안 듦(`M02_Library_Same.py:26`) | 그대로 |
| 733-3 「수량만 다름」 | `본뜬.실무수량`(`M02_Library_Same_Router.py:128-130`) | 끝낸 SC 끼리는 못 가름 — 723-1 뒤엔 저장 수량이 없어 원래 거의 같음 |
| 739 별칭 장부 | `맞춤.json` · 이은 짝의 불러오기 열쇠(HP 키) | 남김 · HP 키는 글로만 남음 |
| 729-5 남은못옮김 · 729-3 [끝내기] 막음 | SK `본뜬.못옮김` · HP 살아 있을 때 | [끝내기] 뒤 걷으면 무관(끝내기는 남은 줄 0 일 때만) |
| 다시 열기(이어서) | HP 지문 · SC · SK 본뜬 키 · 확인(`Hopyo_Build.confirmed` · `Hopyo_Sangeun.made`) | 끝내기 전엔 둠 |
| 705-2 「이 공사 단가로 다시 잇기」 | `본뜬.실무수량`으로 1 단위 계 전후(`M02_Hopyo_Relink.py:114-119`) | 끝낸 SC 는 전후 계 = 수량 없음 → 다시 잇기 = 이음만 바꾸고 값 비교는 알약 · 임시 수량으로 |
| 옛 [로직 추가] 「실무 호표」(`Hopyo_Pick.ts` · `Hopyo_Match.py:477-490`) | 작업 중 HP(이 층 · 위 층) | 그대로 · 물음 2 (나)면 창 닫을 때 사라짐 |
## D. 고칠 길 · 물을 갈래
### 고칠 길(일감 · 파일 경계 · 차례)
1. **582-B 일위대가 안 줄 수량 갈래**(물음 1 답 뒤 · 서버 먼저) — `M02_Labor_Ref.no_qty`(갈래 거름) · `M02_Hopyo_Build._save` · `M02_Labor_Expand._qty` · 화면 `Labor_Table.ts` 수량 칸(저장 칸 ↔ 휘발 칸) · 시험 723 묶음 · 공사 사본은 723-4 길(굳힌 값 먼저) 확인만. **740-1 SC 펼침 수량 꼴이 이 답에 매임** → sub3 에 SC 펼침 수량만 멈춤 알림.
2. **740-1 · 740-2 에 582-1 덧**(sub3 · sub2 · 같은 파일) — SK 안 일위대가 줄 「수량 없음」 = 740 펼침이 없앰(물음 1 꼴로 수량) · 둘째 기초 = 세부 품은 M01 1 기준단위 그대로 보이고 기초 줄에 「× 0.01(100㎥ → ㎥)」 따로 · 되돌림 빈틈 셋(더한줄 요소 [되돌림] = 처음 수량 · [기초 빼기] · 더한줄 빼기 = 뺀 줄 띠 [되살림]) — `logic/M02_Sangeun_Cover_Model.ts`(312-353 · 376 · 508) · `Sangeun_Detail.ts`(118-135 · 380-412) · `M02_Sangeun_Calc._detail`. 확인 = GF000099 · GF000316 넣기 · 지우기 → 되살림 · 계 = API.
3. **582-C 불러오기 휘발**(sub1 739 · sub5 741 과 파일 겹침 — 739 끝난 뒤) — `M02_Labor_Router._finish`(본뜬 엑셀 칸 · 메모 걷음) · `M02_Hopyo_Store`(옛 HP 정리 · 지문 409 → 알림) · `Hopyo_Wizard.ts:268` 끝내기 물음 글 · 물음 2 (나)면 창 떠날 때 HP 지움 · 705-2 다시 잇기 값 비교 길 바꿈 · 시험 718 · 729-3 · 733 · 739.
4. 차례 = 물음 1 · 2 답 → 1 ∥ 3 → 740-1 → 740-2(+2). 1/3 강관비계 SK 는 사용자가 원하면 [중복 찾기] 되돌림 한 번.
### 물을 갈래(꼭 필요한 둘)
**물음 1 — 일위대가 안에 넣은 줄의 수량(품)을 저장할까?**
- (가) 지금처럼 모두 저장 안 함 — 구조물 조합 · 공사에서 이어 줌 · 만들 때 칸은 확인용
- (나) 인력 · 재료 · 기계 줄 수량은 저장(바꿀 수 있음) · 단가산출 · 기초 · 일위대가 줄 수량만 구조물 조합 · 공사에서 이어 줌 — 권함
- (다) 직접 만든 일위대가는 모두 저장 · 엑셀 불러오기 때만 비움
**물음 2 — 불러오기 도중 자료(올린 엑셀 읽은 것)는?**
- (가) [끝내기] 누를 때까지 보관 — 나갔다 와도 이어서 · 끝내면 만든 일위대가 · 단가산출 · 별칭 장부만 남음 — 권함
- (나) 불러오기 화면을 떠나면 버림 — 이어서 없음
@@ -0,0 +1,207 @@
# 분석 582-24 — 일위대가 비고 열 · 경쟁사 REA 분석을 실무 엑셀 연결에 쓰는 길 (조사 · 읽기만)
조사 = 여덟 파일 비고 덤프(`tmp/brain_582/p24/dump_note.py` → `notes.json`) · 골든 정답 177 줄로 신호 잼(`eval_hint.py` → `eval_hint.json` · 지금 코드 `helper_m02_hopyo_golden.evaluate` 그대로) · 지식DB `경쟁사 프로그램/` 읽기. 코드 · storage · git 안 고침.
## 0. 요약
1. 비고 = **「이 줄이 무엇을 부르나」 표지** — 산근 번 · 호표 번 · 자원 코드 · 별산. **품셈 조항 번호는 1,350 줄 중 0 줄.** 조항은 비고가 가리키는 산근 블록 글(출처)에만 있음.
2. 지금 코드가 비고 표지를 이미 다 씀 — 엑셀꼴(EST) 684 줄 중 443 줄(64.8 %)의 갈래 · 산근 이음이 비고에서 옴 · STmate 꼴은 비고 칸(M 열)도 코드 찾는 열에 듦. 비고 → 블록 → 블록 인용 조항 = 지금 「근거」 길(206 블록 중 인용 있음 174).
3. 비고를 더 써서 나아질 몫 ≈ 0 — 골든 1위 틀림 14 줄의 비고는 모두 이미 쓰는 표지 · 빈칸. 남는 새 신호 = 봉화 한 파일 「2024년 개정」 등 판 표시 50 줄(블록 글엔 판 표시 없음 49).
4. 경계 모호(사용자 원문)는 줄 단위가 아니라 **내용 단위** — 비고로 줄 갈래는 갈리나 「산근 블록 = 기초 + 덧 항목」 · 「호표 = 기초를 맨 줄로 적음(직접 줄 호표 55 / 245)」은 비고로 안 보임 · 둘 다 이미 숫자 지문 맞춤 · 직접 줄 호표 통째 맞춤이 받음.
5. 출력 프로그램은 시트 이름으로 바로 갈림 — 북부청 둘 = **EST Plus**(시트 12 = EST 엑셀변환 항목 12 · 비고 「단산N참조」 = EST 산근 글 표지) · 거창 = EST 시트 + 산림조합 자체 시트 섞임 · 나머지 다섯 = **STmate**.
6. 경쟁사 코드는 **프로젝트마다 다름**(STmate 여러 파일에 나온 코드 36 개 중 36 개가 다른 품목) · EST 코드는 엑셀에 안 찍힘 → 코드로 우리 기초를 바로 잇는 길 없음.
7. 경쟁사 대응 자료(STmate 레시피 · 현행 대조 · 판 대응표)는 같은 실무 파일에서 뽑은 것 · 현행 절은 대부분 이름 검색 후보 — 골든에서 레시피 현행 절 = 정답 11 / 30(37 %) · 1위 틀림 14 줄 고침 0 · 판 대응표(확정 13)는 하위 글자(가 · 사)까지 맞춰야 함(그대로 쓰면 구조물터파기 2 / 2 틀림).
8. 권함 = 자동 확정 · 점수 덤이 아니라 **고르기 창 표시 + 미리 고름(점만)** — 인용 조항 = 1위 기초 조항이면 1위 맞음 117 / 124(94.4 %) · 체크 35 % · 이어짐 50.5 % · 고름 대기 84~89 수는 그대로(기대 변화 0) · 얻는 것 = 사용자 고르는 걸음 · 잘못 고름 막기.
---
## A. 일위대가 비고 열
### A-1. 여덟 파일 비고 갈래
| 갈래 | 줄 | % | 파일(줄) | 예 |
| --- | --- | --- | --- | --- |
| 산근 가리킴 · 번 | 542 | 40.1 | 북부청임도 268 · 북부청사방 139 · 거창 21 · 울진대흥 69 · 울진소광 43 · 영덕 2 | 「단산33참조」 · 「산근 4호표」 |
| 빈칸 | 332 | 24.6 | 북부청임도 146 · 북부청사방 60 · 거창 33 · 봉화 83 · 울진소광 9 · 영덕 1 | 엑셀꼴 빈칸 = 자재 128 · 별도계상 85 · 노임 12 · 기계 6 · % 8 |
| 자원 번호 · 코드 | 145 | 10.7 | 영월 27 · 울진대흥 58 · 영덕 23 · 울진소광 37 | 「L00002」 · 「M00024」 · 「노무 6」 · 「중기 20」 |
| 산근 가리킴 · 코드 | 90 | 6.7 | 영월 90 | 「D02058」 |
| 호표 가리킴 · 번 | 65 | 4.8 | 거창 12 · 울진대흥 28 · 울진소광 19 · 그 밖 6 | 「일위4참조」 · 「대가 3호표」 |
| 판 · 근거 표시 | 62 | 4.6 | 봉화 62 | 「2024년 개정」 49 · 「품」 9 · 「08' 신규」 2 · 「임도품셈」 1 · 「24년보완」 1 |
| 별산 + 번호 · 코드 | 43 | 3.2 | 영월 18 · 울진대흥 16 · 울진소광 9 | 「별산M00985」 · 「별산자재 14」 |
| 단가 시점(물가 월) | 34 | 2.5 | 봉화 34 | 자재 줄 「2024.01」 |
| 11 자리 수 | 30 | 2.2 | 봉화 30 | 노임 줄 「01000000075」(대한건설협회 직종코드 1002 꼴 아님 · 뜻 확인 못 함) |
| 식 | 2 | 0.1 | 거창 2 | 「8hr/31.5㎥」 · 「0.59본×6%」 |
| 할증 · 비율 코드 · 호표 코드 | 5 | 0.4 | 영덕 2 · 영월 3 | 「W00005」 · 「B00260」 |
| **품셈 조항 번호(「9-12-1」 꼴)** | **0** | **0** | — | — |
- 비고 꼴은 출력 프로그램 · 그 설정마다 다름 — EST 「단산N참조 · 일위N참조」(빈칸 = 맨 자원) · STmate 설정 하나는 코드(「D02058」 · 「M00024」) · 다른 설정은 코드 사전 이름(「산근 1호표」 · 「노무 6」) · 봉화는 설계자가 손으로 적은 글.
- 울진대흥 「노무 6」 · 「자재 3」 의 수 = 목록표 차례 번호(코드 아님) · 산근 「N호표」 = 산근 시트 인쇄 차례.
### A-2. 비고로 알 수 있는 것
| 물음 | 비고로 | 근거 |
| --- | --- | --- |
| 이 줄이 산근(단가산출)을 부르나 · 호표를 부르나 · 맨 자원인가 | **예** — 표지 있는 줄 700 + 자원 145 + 별산 43 | A-1 |
| 어느 산근 블록 · 어느 호표 | **예** — 번(EST · STmate 이름 꼴) · 코드(STmate 코드 꼴) | A-1 |
| 별도계상(관급) 인가 | 예 — 「별산」 머리 43 | `M02_Hopyo_Stmate.py:173` |
| 어느 품셈 조항(= 우리 기초 키) | **아니오** — 0 줄. 조항은 블록 글 「건설표준품셈 6-1-1 적용」 꼴 | `M02_Hopyo_Basis.py:71-84` |
| 어느 품셈 판 | 봉화만(50 줄) | A-5 |
### A-3. 지금 코드가 비고를 쓰는 곳
| 자리 | 하는 일 |
| --- | --- |
| `M02_Hopyo_Excel.py:41` `_REF` · `:272-285` | 엑셀꼴 비고 「단산 · 산근 · 단가산출 N(참조 · 호표)」 → 단산 N · 「일위 · 대가 N」 → 호표 N |
| `M02_Hopyo_Excel.py:145-164` `kind_of` | 참조 종류로 갈래 단가산출 · 호표 |
| `M02_Hopyo_Excel.py:225-237` `_by_name` | 비고 빈 줄 + 노무비 · 경비 단가 → 품명 + 단위 같은 블록 하나면 이음(여덟 파일 실측 0 줄 — 빈칸 줄은 모두 자재 · 별도계상 · 노임 · 기계 · %) |
| `M02_Hopyo_Stmate.py:42` `NOTE` · `:62-66` `_code` | 비고 칸(M 열 = 12)을 줄 코드 찾는 열(12~25)에 넣음 — 비고에 코드가 찍힌 파일은 그것이 줄 코드 |
| `M02_Hopyo_Stmate.py:168-177` `_kind` | 비고 「별산」 → 별도계상 |
| `M02_Hopyo_Material.py:171` · `:287` | 비고 한글 낱말 → 관급 · 사급 · 지역 · 「운반비 미포함」 |
| `M02_Hopyo_Paste.py:172` · `:231` | 붙여넣기 꼴 비고 열(경 + 2) 같은 표지 |
| 안 씀 | `M02_Hopyo_Match.py:345` `_OLD_TEXT`(옛 판 글)는 블록 글 앞 다섯 줄만 봄 — 비고 판 표시 안 봄 |
### A-4. 실측 — 비고가 이미 하는 몫
| 잼 | 수 |
| --- | --- |
| 엑셀꼴(EST) 줄 중 갈래 · 이음이 비고에서 온 줄 | 443 / 684(64.8 %) — 비고가 없으면 산근 이음 · 블록 근거가 통째로 끊김 |
| 일위대가가 부르는 산근 블록 · 인용 조항 있음 | 206 · 174(84 %) — 건설품셈 대부분 · 산림품셈 34 |
| 골든 177 줄 · 블록 인용 있음 | 158 — 인용 = 정답 기초 조항 111(70 %) · 윗마디 10 · 어긋남 37 |
| 인용 조항 = 1위 기초 조항 | 124 줄 → 1위 맞음 117(94.4 %) · 판정 맞음 65/66 · 다른 점 42/45 · 후보 10/13 |
| 인용 있음 · 1위와 다름 | 34 줄 → 1위 맞음 28(82 %) |
| 인용 없음 | 19 줄 → 1위 맞음 18 |
| 지금 1위 · 다섯 안 · 판정 맞음 | 163 / 177(92.1 %) · 173 · 78(44 %) |
→ 체크 35 %(단가산출 줄 257/735)를 누르는 것은 1위 찾기(92 %)가 아니라 **판정(엑셀 품 ≠ 우리 기초 품)** — 비고는 품 숫자를 바꾸지 못함.
### A-5. 비고를 더 쓰면 — 실측
| 신호 | 대상 | 결과 |
| --- | --- | --- |
| 골든 1위 틀림 14 줄의 비고 | 단산N참조 4 · D 코드 5 · 산근N호표 3 · 빈칸 2 | 이미 쓰는 표지 · 고칠 글 0 |
| 봉화 판 표시 「2024년 개정」 · 「24년보완」 | 단가산출 50 줄 · 블록 글엔 판 표시 없음 49 | 새 신호 — 「옛 판」 꼬리표 근거만 늘어남 · 이 블록들 인용(건설 6-1-1 · 6-3-x · 6-2)은 2026 판 번호와 같아 1위 · 판정 변화 없음(골든 봉화 1위 틀림 2 = 강관동바리 · 강관비계 · 비고 빈칸) |
| 봉화 「품」 | 호표 부름 9 줄(깬잡석찰쌓기 · 메붙임 …) | 그 호표 = 품셈 품을 맨 줄로 적은 직접 줄 호표 — 이미 `M02_Hopyo_Finger.py:279` `direct` · `M02_Hopyo_Fit.py:321` 통째 맞춤 |
| 거창 식 | 2 줄 | 기계 시간 환산 근거(8 hr / 31.5 ㎥) · 손료 식 — 표시 쓸모만 |
| 물가 월 · 11 자리 수 | 64 줄 | 연결과 무관 |
- 기대: 체크 35 % · 이어짐 50.5 %(SK 참조 151 · 고정 이음없음 596) · 고름 대기 84~89 블록 → **자동 변화 0**.
### A-6. 「경계 모호」 실측 — 줄이 아니라 내용
| 꼴 | 여덟 파일 | 지금 받는 길 |
| --- | --- | --- |
| 호표 = 산근을 부름 | 184 / 245 | 산근 블록 숫자 지문 → 기초 · 차이 SK(729-1) |
| 호표 = 맨 노임 · 기계를 적음(직접 줄 · 기초꼴) | 55 / 245 — 봉화 15 · 울진대흥 16 · 울진소광 9 · 영덕 7 · 거창 5 | 직접 줄 호표 통째 맞춤(장부 열쇠 「호표\|품명\|규격\|단위」) |
| 호표 = 산근 + 맨 노임 · 기계 섞임 | 7 | 줄마다 따로 |
| 호표가 호표를 부름 | 60 | SC 참조 |
→ 비고는 위 넷을 줄 단위로 가르는 데만 쓰임(이미 씀) · 「산근 블록이 기초 + 덧 항목인지」 · 「직접 줄 호표가 어느 기초인지」는 숫자 지문 몫.
---
## B. 경쟁사 REA 분석
### B-1. 자료
| 자료 | 자리 | 일위대가 연결에 |
| --- | --- | --- |
| EST Plus 2(다산소프트 · EST PLUS5 5.2.6) REA 정적 분석 | `경쟁사 프로그램/EST Plus 2/REA 분석/ESTPlus2_REA_최종보고서_20261030.md` · `02_화면구성/captions.json` · `07_수치검증_작업자료_20261025/*Dat.decoded_candidates.json` | 표 구조 · 코드 · 비고 설정 · 엑셀 변환 항목 |
| STmate(경영정보) 분석 + REA | `STmate 분석/20_분석/` · `REA 분석/STMATE_REA_최종보고서_20261029.md` · `30_원자료/` | 코드 사전 · 숨은 칸 · 레시피 · 현행 대조 · 판 대응표 |
| 오솔길 ARX REA | `오솔길 분석/` | CAD 단면 · 토적 · 수량 매크로 — 일위대가 무관 |
### B-2. 실무 엑셀이 어느 프로그램 출력인가 — 알아볼 표시
| 표시 | EST Plus | STmate |
| --- | --- | --- |
| 시트 | 원가계산서 · 내역서총괄표 · 내역서 · 일위대가총괄표 · 일위대가 · 단가산출총괄표 · 단가산출 · 기계경비총괄표 · 기계경비 · 기계경비적용기준 · 자재단가 · 노임단가(= EST 「엑셀로 변환」 항목 12 · `captions.json` TFRMEXCELCONVERT) | 〓 목 차 〓 · ※※안내※※ · 〓 INITIAL 〓 · 일위대가표 · 단가산출근거 · 노무비 · 중기 · 재료비 · 경비 · 단가산출근거 목록표 |
| 비고 | 「단산N참조」 · 「일위N참조」(EST 산근 글 「*단산7참조*」 · `CostMainDat.decoded_candidates.json`) · 빈칸 = 맨 자원 · 설정 「비고란에 발췌근거 표시」(내역 · 일위대가 · 자재) · 「근거자료(일위,단산 등)」 | 코드 사전 `COD_FMT` 노무 · 자재 · 경비 · 대가 · 중기 · 산근 · 기타 · 실적(`STmate 분석/20_분석/20_설정파일과_보고서_카탈로그.md:76-83`) · 「별산」 머리 · 설정에 따라 코드 그대로 |
| 코드 | 내부 L · M · D(산근) · U(일위) + 10 자리 — 엑셀에 안 찍힘 | 숨은 칸 「BDCOD\|B…」 · 「COD\|L…」 · 줄 코드 [LMXSWDB] + 5 자리 · PR |
| 산근 머리 | 「N.이름[단위]」 | 「BDCOD\|D…」 머리 · 「총계」 끝 |
| 여덟 파일 | 북부청임도 · 북부청사방 · 거창(EST 시트 + 산림조합 자체 시트 BUDGE · daega · junggi · noyim · unitpric 섞임 · 시트 50) | 봉화 · 동부청영월 · 울진대흥 · 영덕계류 · 울진소광 |
- 지금 판별 = STmate 만(`M02_Hopyo_Stmate.is_stmate` · 시트 둘) · 그 밖 = 「엑셀꼴」 — EST 를 따로 부르지 않음.
### B-3. 연결 구조
| | EST Plus | STmate |
| --- | --- | --- |
| 표 | 표 17 · 목록(Title) = 결과 요약 · 상세(Main) = 구성 행 · 다른 표는 목록을 Code 로 참조 | COSTN(코드 · 명칭 · 규격 · 단위 · 네 안 단가) · BDQTY(구성 · 암호화) · 목록표 |
| 열쇠 | Code 우선 → 못 찾으면 품명 · 규격 · 단위 | 코드 · 이음 칸 BQCODE · EQCODE · CONTCODE · WQCODE · JDCODE |
| 줄 종류 | LinkIndex 5 자재 · 6 노임 · 21 표준시장단가 · 101 견적 · 103 일위대가 · 104 단가산출 · 105 기계 · 58 · 62 · 67 비율 행 | 코드 머리 L 노무 · M 자재 · X 중기 · S · W 경비 · 별도 · D 산근 · B 대가 · PR % |
| 셈 차례 | 원천 → 기계 → 산근 → 일위(다른 호표 · 산근 참조 시 재귀) → 내역 → 원가 | 같음(산근 · 대가 2 단 이상 150 / 350) |
| 품셈 근거 | 산근 글 안 글자뿐(코드 · 칸 없음) | 단가산출근거 글 안 글자뿐 — 레시피 350 중 근거 글 82 |
| 엑셀 가져오기 | 「엑셀데이타를 EST로 변환」 = 시트명 · 시작행 · 끝행 · 칼럼 이름을 사용자가 정함(TFRMEXCELTOEST) — 자동 판별 없음 | 조달청 XML 변환만 |
→ 두 프로그램 모두 **품셈 조항을 구조 칸으로 안 가짐** — 우리 기초 키로 가는 열쇠는 산근 글 인용뿐(= 지금 근거 길).
### B-4. 대응 자료(STmate 분석 `30_원자료/`)
| 자료 | 크기 | 무엇 | 경계 |
| --- | --- | --- | --- |
| `일위대가_레시피.json` | 350(일위대가 132 · 단가산출 136 · 중기 82) · 구성행 1,729 | 실무 6 건 STC 에서 뽑은 조합 · 수량 = 그 시점 값(금액이 섞인 줄 있음 — 「강관동바리」 구성 수량 274,978) | 같은 실무 파일에서 뽑음 → 우리 여덟 파일과 순환 |
| `일위대가_현행품셈_대조.json` | 350 | 산림품셈 현행 절 후보 · 상태 = 원문 확인 2 · 절 확인 11 · **이름 검색 후보 161** · 위치 미확인 69 · 근거 없음 107 | 건설품셈 인용은 대조 안 함 |
| `임도품셈_판_대응표.csv` | 14(확정 13 · 미확인 1) | 2017 임도품셈 → 2018 통합 → 2024 개정 → 2026 현행 절 · FP 코드 · 표 ID | 산림 · 임도 품셈만 · 하위 글자(가 · 사) 단위 · 「번호만 같은 다른 절 자동 연결 금지」(`33_임도품셈_판_대응표.md`) |
| `특수_단가산출_21개.json` | 21 | 운반 사이클 · Q 식 · 인자식 원문 | 값 아님 · 식 꼴 |
| `stmate_code_name_list.csv` | 894 | 7 건 STC 코드 · 이름 합집합(처음 본 것 남김) | 코드가 프로젝트마다 달라 사전으로 못 씀(B-5) |
- 우리 M01 로직 출처 꼴 = 「산림품셈 9-13-7」(예 GF000265) — 판 대응표 `현행_절` 과 같은 표기 → 책 + 절로 바로 이음.
### B-5. 실측 — 대응 자료로 엑셀 줄 → 우리 기초
| 잼 | 수 |
| --- | --- |
| STmate 여러 파일에 나온 코드 · 같은 품목 | **0 / 36**(L 5 · D 22 · B 4 · M 4 · P 1 모두 다른 품목 — 예 D00002 = 봉화 구조물터파기(토사) · 영월 구조물터파기(인력)) |
| 일위대가가 부르는 블록 206 · 레시피 이름 짝(이름 + 규격 · 앞머리) | 101 · 그중 현행 절 40 · 우리 로직 걸림 35 |
| 골든 · 레시피 현행 절 있음 | 30 줄 → 정답 조항 11(37 %) · 레미콘타설 → 13-5-2 · 철근 → 5-24-1 · 거푸집 → 12-4 · 동바리 → 12-20 꼴 어긋남 |
| 골든 1위 틀림 14 줄을 레시피 · 판 대응표가 고침 | **0** |
| 판 대응표가 걸리는 인용(여덟 파일) | 산림 5-1-2(봉화 · 영월 구조물터파기) · 4-16-2(봉화 · 울진소광 단끊기 → 5-16-1 = GF000298) |
| 그 가운데 5-1-2 | 실무 = 「가. 육상토사」 · 표 = 「사. 암」 → 9-13-7 · 골든 정답 = 9-13-1 → **번호만 보고 쓰면 2 / 2 틀림** |
### B-6. 판 차이 — 실측
| 인용 | 파일 | 우리 M01 에서 |
| --- | --- | --- |
| 2017 임도표준품셈 5-1-2 · 5-5 · 7-1-1 · 7-1-2 | 봉화 · 영월 | 산림 5-1-2 = GF000293 교목굴취(번호만 같은 다른 공종) · 5-5 · 7-1-1 없음 |
| 옛 건설품셈 11-3(굴삭기) · 9-3 · 9-1 · 9-7 · 8-2 · 1-10 | 봉화 7 · 울진대흥 3 · 영월 12 · 영덕 1 · 울진소광 1 · 북부청임도 3 | 건설 11-3-x = 가스 · 도료 · 9-3-x = 측량 · 충전 — 번호만 같은 다른 공종 · 굴착기 작업량 기초 없음 |
| 그 밖 어긋남 | 건설 6-2(철근) 10 곳 | 우리 = 6-2-2 · 6-2-3(윗마디로 잡힘) |
- 지금 막음: 이름 같음 + 옛 판 글이면 조항 검사 안 함(`M02_Hopyo_Match.py:345` · `:416`) · 근거 후보는 직종 · 낱말 안 겹치면 버림(`M02_Hopyo_Basis.py:242`) — 번호만 같은 다른 공종은 대개 걸러짐.
- 빈틈: 건설품셈 안 부문(공통 · 토목 · 건축 · 기계설비 …)을 조항 견줌에서 안 가름(`M02_Hopyo_Basis.py:191-205`) · 건설품셈 판 대응표는 지식DB 에 없음(레시피 근거 글에 「11-3(굴삭기)」 · 「8-2-3(굴삭기)」 두 꼴이 함께 있을 뿐).
---
## C. 권함
### C-1. 맞춤 판정에 넣는 길 — 자동 확정 아님
전제 = 729-3 그대로(남은 줄 있으면 409 · [끝내기] 막음 · 사용자가 고른 것만 만듦). 비고 · 경쟁사 신호는 **고르기 창의 보여주기 · 첫 후보 점(미리 고름)** 에만 · `M02_Hopyo_Line._check`(`:134-146` 체크 제안 조건)에는 안 넣음.
| # | 길 | 파일 · 함수 | 단계 | 사용자 제약과 |
| --- | --- | --- | --- | --- |
| ① | 고르기 창에 비고 원문 · 블록 인용 조항 · 판 표시 · 「인용 조항 = 1위 기초 조항」 표지 | 서버 `M02_Hopyo_Line._numeric`(응답 칸) · `M02_Hopyo_Sangeun`(고름대기 후보) · 화면 `M02_MasterTemplete_Hopyo_Check.ts` · `_Choose.ts` | 남은 줄 · 고름 대기 보여줄 때 | 보여주기만 |
| ② | 미리 고름 — 인용 조항 = 1위 기초 조항이면 첫 후보에 점(체크 아님 · 사용자가 [고름]) | 같은 응답에 `미리` 칸 하나 | 판정 다른 점 · 후보 줄 | 사용자가 눌러야 만듦 · 골든 정밀도 94.4 %(117/124) · 후보 상태만 77 %(10/13) |
| ③ | 비고 판 표시 → 옛 판 검사 | `M02_Hopyo_Match.b_order`(`:416`) 가 블록 글 + 줄 비고를 함께 봄 | 이름 · 근거 차례 | 꼬리표 「옛 판(비고)」 — 봉화 50 줄 · 1위 변화 0 |
| ④ | 판 대응표 → 현행 절 후보 | `M02_Hopyo_Basis.sources` 뒤 · 열쇠 = (판, 책, 절, 하위 글자) · 확정 13 줄만 · 하위 글자 안 맞으면 안 씀 | 근거 후보 | 꼬리표 「판 대응(경쟁사 분석)」 · 점수 덤 없음(보여주기) — 걸리는 블록 넷(단끊기 둘만 바로 맞음) |
| ⑤ | 출력 프로그램 판별 | `M02_Hopyo_Excel.read_book` `꼴` 에 「EST」(시트 12 이름) | 읽기 | 화면 띠에 「EST Plus 출력 · 비고 = 단산 · 일위 참조」 표시 |
- 안 함: 레시피 · 현행 대조 JSON 의 현행 절을 후보 순위에 넣기(정답 37 % < 지금 1위 92 %) · STmate 코드 사전으로 이름 잇기(코드가 프로젝트마다 다름) · 판 대응표 번호만으로 바꿈.
### C-2. 지식DB 원칙 안에서 쓰는 꼴
- 지식DB 표는 **읽기 전용 근거** — 코드가 값(품 · 수량 · 단가)을 가져오지 않음 · 꼬리표 글 · 출처(파일 · 줄)만 보여줌.
- 확정 상태만 씀 — 판 대응표 「확정」 13 줄 · 현행 대조 「현행원문_수량문자_전부확인」 2 · 「현행절_확인」 11 · 「후보_수동대조필요」 161 · 「미확인」 은 안 씀.
- 실무 · 경쟁사 값이 현행과 다르면 현행 우선 · 어긋남은 꼬리표로 남기고 사용자가 고름(`00_운영지침.md` 근본 원칙).
- 지식DB 표를 런타임 자료로 옮길 때는 사본(M02 쪽 읽기 전용 표 + 원문 경로 · 판 표시) · 지식DB 원본은 안 고침.
### C-3. 일감 · 기대치
| 일감 | 파일 | 시험 | 기대(여덟 파일) |
| --- | --- | --- | --- |
| ① 고르기 창 보여주기 + ② 미리 고름 | `M02_Hopyo_Line.py` · `M02_Hopyo_Sangeun.py`(응답 칸) · `M02_MasterTemplete_Hopyo_Check.ts` · `_Choose.ts` | `test_729_3_*` · `test_m02_hopyo_screen` · 골든(미리 고름 정밀도 ≥ 94 %) | 체크 35 % · 이어짐 50.5 % · 고름 대기 84~89 그대로 · 미리 고름 대상 = 인용 일치 다른 점 · 후보 줄(골든 58 줄 중 52 맞음) |
| ③ 비고 판 표시 | `M02_Hopyo_Match.py` | 골든 1위 163 그대로 | 꼬리표 50 줄 · 1위 변화 0 |
| ④ 판 대응 꼬리표 | `M02_Hopyo_Basis.py` + 읽기 전용 표 사본 | 골든 · 구조물터파기 5-1-2 가 = 안 걸림 시험 | 걸림 단끊기 2 블록 |
| ⑤ EST 판별 | `M02_Hopyo_Excel.py` | `test_m02_hopyo_excel` | 북부청 둘 · 거창 「EST(섞임)」 |
- 권함 순서 = ① · ② 먼저(사용자 고르기 걸음 줄임 · 「완벽히 고른 경우에만」 과 그대로 맞물림) · ③ · ⑤ 는 작음 · ④ 는 표가 14 줄뿐이라 보류 가능.
- 체크율을 올리는 길은 이 둘이 아님 — 판정이 막는 몫(골든 1위 맞음인데 판정 다른 점 · 후보 87 줄)은 옛 판 품 차이 · 설계자 변형이라 729-1 차이 SK 와 별칭 장부(사용자 고름이 쌓이면 두 번째부터 자동 체크)가 받음.
- 물을 갈래: 미리 고름 점을 「인용 조항 일치」 하나로 둘지 · 판정 후보(77 %)는 빼고 다른 점(93 %)만 둘지.
@@ -0,0 +1,192 @@
# 분석 585 — 일위대가 불러오기 맞춤을 「항목 하나하나 ↔ M01 요소 · 기초」로 (조사 · 읽기만)
조사 = 코드 따라감 + 여덟 파일 산근 블록(일위대가가 부르는 첫 줄 · 206) · 골든 정답 177 줄을 지금 함수 그대로 불러 셈(`tmp/brain_585/probe585.py` → `out585.json` · 모음 `agg585.py` · 우리 지문 캐시 = 사본 `tmp/brain_585/cache`). 코드 · storage · git 안 고침.
## 0. 요약
1. 일위대가(호표)는 지금도 **줄마다** 맞춤 — 자재 → MT · 노임 → LB · 기계 → EQ · 호표 → 하위 · 단가산출 → 그 산근 블록. 「일위대가 = 한 로직 그룹」은 아님.
2. 덩어리로 다루는 자리 = 산근 블록 **판정**(블록 통째 0.5 % · 3 % 문턱) · 후보 블록(줄 견줌 없음 · 고름 대기 89) · 못옮김(요소 후보 없음) · 직접 줄 호표 통째 제안.
3. 블록 찾기는 이미 요소 벡터(직종 = LB 이름 · 장비 = EQ 갈래 · 자재 낱말)를 가장 무겁게 씀 · 절마다 조합 · 드문 요소 색인까지 있음. 다른 점 블록은 이미 줄 단위 차이 SK(729-1).
4. 요소로 기초를 거꾸로 찾으면 **나빠짐** — 골든 1위 163 → 107(요소만) · 36(색인) · 144(거름) — 같은 요소 집합 기초가 중앙 10 개 · 가르는 것은 이름 · 조항.
5. 후보 블록에 1위 기초와 줄 견줌을 넓히면 **좋아짐** — 고름 대기 89 중 72 블록(부르는 줄 318 중 272)에 항목별 비교가 생김 · 골든 1위 맞음 42 / 47(89 %) · 꼴 = 같은 항목 · 다른 품 25 · 엑셀이 더함 18 · 우리만 14 · 더함 + 뺌 12.
6. 「같은 줄 비율」은 판정 문턱으로 못 씀(SK · 후보 모두 중앙 0 — 품이 다 다름) · 블록 줄 → 요소 키는 인력 95 % · 장비 32 % · 자재 10 %(후보만 77).
7. 자동 이어짐 50.5 % · 골든 1위 · 판정 수는 그대로(582-2 · 584-3 · 743 · 739 · 740 과 맞음) — 얻는 것 = 사용자 고르는 걸음.
8. 권함 = 일감 셋(고름 대기 줄 비교 서버 · 그 화면 · 못옮김 · 기계 요소 후보) · 찾기 · 판정 문턱은 안 바꿈 · 물을 갈래 하나(D-4).
---
## A. 지금 맞춤이 무엇을 단위로 견주나
### A-1. 단계
| # | 단계 | 무엇 ↔ 무엇 | 신호 | 결과 갈래 | 근거 |
| --- | --- | --- | --- | --- | --- |
| 1 | 일위대가 줄 가름 | **일위대가 줄 하나하나** → 갈래(% · 호표 · 노임 · 자재 · 별도계상 · 단가산출 · 기계) | 비고 표지(단산N · 일위N) · 단위 · 직종 이름 | 줄마다 다른 길 | `M02_Hopyo_Line.py:244-283` · `M02_Hopyo_Finger.py:303-309` |
| 2 | 자재 · 별도계상 줄 | 줄 하나 ↔ **M01 자재(MT)** | 이름 · 규격 같음 → 바탕 이름 · 규격 수 같은 한 품목 | 맞음(체크) · 후보 · 없음 | `Line.py:207-232` · `M02_Hopyo_Material.py:206` |
| 3 | 노임 줄 | 줄 하나 ↔ **M01 인력(LB)** | 이름(옛이름 · 괄호 풂) 같은 LB 하나 | 이음(만들기 때) · 못 이음 = 남은 줄 | `M02_Hopyo_Fixed.py:91-98` |
| 4 | 기계 줄 | 줄 하나 ↔ **M01 기계(EQ)** + 기계경비 기초 | 이름 · 규격 글 같음 › 갈래 + 규격 수 · 블록 없는 기계 지문(1 시간) | EQ 이음 · 판정 | `Fixed.py:105-116` · `Finger.py:145-155` |
| 5 | 호표 줄 | 줄 하나 ↔ 확인된 하위 일위대가 | 번 · 단위 환산 | 참조 | `Line.py:251-260` |
| 6 | 단가산출 줄 | 줄 하나 → 그 **산근 블록 통째** ↔ **기초 하나**(+ 절 조합) | 블록 지문 = 직종 품(LB 이름) · 장비 갈래 · 규격 · 시간(EQ 갈래 낱말) · 자재 낱말 · 율 · 조항 · 조건 · 이름 · 단위 | 순위 다섯 | `Fit.py:303-326`(셈 단위 = 블록 + 줄) · `Finger.py:158-235` · `Fit.py:190-224` |
| 6-1 | 순위 점수 | 블록 요소 벡터 ↔ 기초 요소 벡터(변형마다) | 인력 3 · 장비 2 · 조항 1.5 · 자재 1 · 이름 0.6 · 조건 0.6 · 단위 0.5 · 율 0.3 + 이름 · 근거 차례 덤 | 위 다섯 · 같음 1위 고정 | `Fit.py:35-44` · `:255-264` · `Verdict.py:111-120` |
| 6-2 | 절 조합 | 블록 **절(1. 2. 가. 나.)마다** 노임 · 장비 벡터 ↔ 기초 | 1위가 못 덮는 절(< 0.6) = 절 1위 · 몫 · 장비만 든 절 = 기계 시간당 기계경비 · **드문 직종 · 장비 색인**(요소 → 기초) | 조합(덧 로직) | `Combine.py:178-184` · `:240-300` |
| 6-3 | 판정 | 1위 기초 옵션 거꾸로 찾은 품 ↔ 엑셀 품(**블록 통째 문턱**) | 수준 ≤ 1(0.5 % + 반올림) 맞음 · 수준 2(3 %) 또는 직종 같음 + 조항 같음 = 다른 점 · 그 밖 후보 · 숫자 하나뿐 = 후보 | 맞음 · 다른 점 · 후보 | `Verdict.py:80-88` · `Fit.py:267-272` |
| 6-4 | 체크 제안 | 판정 | 맞음 + 환산 배 확실 | 체크 · 안 함 | `Line.py:139-150` |
| 7 | 단가산출 만들기 | 블록(부르는 **첫** 줄의 맞춤셈) | 판정 맞음 = 기초 · 다른 점 = 1위 기초 + **줄 단위 차이**(직종 · 장비 · 자재마다 덮개 · 더한줄 · 뺀줄 · 못옮김) · 후보 = 고름 대기(SK 안 만듦) | 기초 · SK · 고름 대기 | `Sangeun.py:85-95` · `:203-214` · `SkPatch.py:343-406` |
| 7-1 | 줄 단위 차이 | 엑셀 블록 직종 · 장비 · 자재 줄 ↔ 기초 셈 줄(cid) | 직종 이름 = LB 키 · 엑셀만 장비 = 기계경비 로직 + EQ(옵션 거꾸로) · 엑셀만 자재 = **못옮김(MT 찾기 없음 · 후보 [])** | 덮개 · 더한줄 · 뺀줄 · 못옮김 | `SkPatch.py:184-215` · `:248-263` · `:265-282` |
| 8 | 직접 줄 호표 | **호표 통째**(노임 · 기계 줄 수량 합) ↔ 기초 하나 | 같은 지문 · 판정 | 통째 제안(줄 맞춤과 따로) | `Finger.py:279-300` · `Line.py:373-389` |
| 9 | 일위대가 만들기 | 확인 안 한 단가산출 줄 → 그 블록 SK 참조 · 노임 → LB · 기계 → EQ | 남은 줄 = 409 | 이음 · 남은 줄 | `SkLink.py:61-94` · `Fixed.py:71-140` |
### A-2. 사용자 걱정 「일위대가를 하나의 로직 그룹으로」 — 맞는 곳 · 안 맞는 곳
| 자리 | 지금 | 걱정과 |
| --- | --- | --- |
| 일위대가(호표) 한 장 | 줄마다 따로 맞춤(1~5 · 9) — 호표 통째를 기초 하나에 견주지 않음 | **안 맞음** — 이미 항목별 |
| 일위대가 자재 · 노임 · 기계 줄 | 줄 하나 ↔ M01 MT · LB · EQ 직접(2 · 3 · 4) | **안 맞음** — 사용자 안 (1) 이 이미 있음 |
| 산근(단가산출) 블록 · 찾기 | 블록을 요소 벡터(직종 = LB 이름 · 장비 = EQ 갈래 · 자재 낱말)로 풀어 기초 요소 벡터와 견줌 · 절마다 조합 · 드문 요소 색인 | 반쯤 — 「통째 덩어리」가 아니라 요소 벡터 · 다만 줄을 **M01 키**로 잇지는 않음(이름 · 낱말) |
| 산근 블록 · 판정 | **블록 통째 문턱** — 한 직종 품이 3 % 넘게 달라도 블록 전체가 다른 점 · 후보 | **맞음** — 이 자리가 「그룹」 |
| 후보(고름 대기) 블록 | 줄 단위 견줌을 아예 안 함(SK 안 만듦 · 「맞는 기초 없음」) | **맞음** — 같은 줄 몇 개인지 안 보임 |
| 다른 점 블록 | 1위 기초 + 줄 단위 차이 SK(729-1) | 안 맞음 — 이미 항목별 차이 |
| 못옮김(엑셀만 자재 · 못 찾은 장비 · 우리에 없는 직종) | 후보 [] — M01 요소를 찾아 주지 않음 | **맞음** — 줄 → 요소 잇기 빠짐 |
| 직접 줄 호표 | 호표 통째 ↔ 기초 하나 제안(줄 맞춤과 함께) | 맞음 — 다만 줄마다 LB · EQ 이음도 같이 돎 |
| 블록을 부르는 둘째 줄부터 | 만들기는 **첫 줄** 맞춤셈만 씀 | 작음 |
→ 「일위대가 = 한 덩어리」는 아님. 덩어리로 다루는 자리 = **산근 블록 판정 문턱 · 후보 블록 · 못옮김 · 직접 줄 호표 통째** 넷.
---
## B. 사용자 안 구체화 — 항목(줄) ↔ M01 요소 · 기초
### B-1. 세 꼴
| # | 꼴 | 지금 있나 | 새로 할 것 |
| --- | --- | --- | --- |
| (1) | 일위대가 줄 → M01 요소 직접(노임 이름 · 단가 · 자재 이름 · 규격 · 기계) | **있음**(A-1 2 · 3 · 4 · 729-2) | 없음(실측 C-5 로 남은 몫만 확인) |
| (2) | 산근 블록 안 줄 → M01 요소 키(LB · EQ · MT) 먼저 → 요소 묶음 · 품 비율로 기초를 거꾸로 찾음 | 반쯤 — 지문이 곧 요소 벡터 · 절 색인(`Combine.pool_for`) · 키로 잇지는 않음 | 줄마다 키 잇기(EQ · MT) · 요소 색인 순위 |
| (3) | 블록 = 기초 + 더한 줄 → 차이를 줄 단위로 | **있음**(다른 점 → SK · 729-1) | 후보 블록에도 1위 기초와 줄 단위 견줌 · 못옮김에 MT 후보 |
### B-2. 판정에서 바꿀 자리(안)
| 자리 | 지금 | 안 |
| --- | --- | --- |
| `Verdict._state`(`:80-88`) | 블록 통째 수준 · 다른 점 = 수준 2 또는 직종 같음 + 조항 같음 | 그대로(찾기 · 맞음 문턱 안 건드림) |
| `SkPatch.plan` 후보 길(`:362-366`) | 후보 = `_wait`(줄 견줌 없음) | 후보도 1위(다섯)와 줄 단위 견줌 → 「같은 줄 n / 엑셀 줄 m」 · 차이 줄을 고름 대기 응답에 붙임(만들지 않음) |
| `SkPatch._Plan.materials` · `miss`(`:265-282` · `:152-158`) | 엑셀만 자재 = 못옮김 · 후보 [] | 못옮김 `후보` = M01 자재(MT) 찾기(`Material.sure` · `candidates`) |
| `SkPatch._add_machine`(`:248-263`) | 기계경비 로직 수준 ≤ 1 못 찾으면 못옮김 | 못옮김 `후보` = EQ 갈래 같은 것 |
| 고르기 창(고름 대기 · 다른 점) | 기초 후보 다섯 · 판정 글 | 후보마다 「같은 줄 · 다른 줄 · 엑셀만 · 우리만」 줄 표(같은 응답 칸) |
---
## C. 실측 — 여덟 파일 · 골든 177
### C-1. 바탕(지금) — 729 기록과 같음
| 잼 | 수 |
| --- | --- |
| 산근 블록(일위대가가 부름) | 206 = 기초 80 · SK 37 · 고름 대기 89(729-4 기록 그대로) · 부르는 일위대가 줄 280 · 137 · 318 |
| 고름 대기 까닭 | 맞는 기초 없음 83 · 단위 성격 다름 5 · 숫자 없음 1 · 상태 = 후보 84 · 다른 점 5 |
| 못옮김 | SK 8 블록 9 줄(자재 8 · 기계 1) · 부르는 줄 15 |
| 골든 1위 | 163 / 177(92.1 %) · 다섯 안 173 · 상태별 1위 맞음 = 맞음 76/78 · 다른 점 42/45 · 후보 45/54 |
| 골든 갈래 | 기초 79 · SK 36 · 고름 대기 62 — **고름 대기 62 중 1위 맞음 53** |
### C-2. 블록 안 줄 → M01 요소 키(B-1 (2) 앞 단계)
| 갈래 | 줄 | 유일 | 여럿 · 후보만 | 없음 | 까닭 |
| --- | --- | --- | --- | --- | --- |
| 인력 → LB | 314 | 299(95 %) | — | 15 | 15 = 「작업반장」 LB 이름 둘(키 2) |
| 장비 → EQ(갈래 + 규격 수) | 183 | 59(32 %) | 여럿 114 · 갈래만 10 | — | 갈래 낱말이 타이어 · 무한궤도 · 습지를 못 가름 · EQ 630 중 361 이 같은 (갈래, 규격 수) 무리 |
| 자재 → MT | 89 | 9(10 %) | 후보만 77 | 3 | 블록 글 자재는 「이름 규격」이 한 칸 · 지역 · 관급 변형 |
| 합 | 586 | 367(63 %) | 201 | 18 | |
- 일위대가 줄(사용자 안 (1) · 지금 길): 노임 105 → LB 105(100 %) · 기계 36 → EQ 4(11 %) · 자재 219 → 맞음 91 · 별도계상 135 → 맞음 56(지역 변형) · 나머지 = 후보 · 없음.
- 기계 줄 EQ 시험(이름 낱말 덤 · 규격 수 겹침): 4 → 18 / 36 — 「대형브레이카 + 굴착기(0.7㎥)」 처럼 한 줄 두 장비 · 「타이어형 ↔ 타이어」 낱말 다름이 남음(정밀도 정답 없음 · 시험만).
### C-3. 요소로 기초를 거꾸로 찾기(B-1 (2)) — 골든 1위
| 찾기 | 1위 | 다섯 안 | 지금과 |
| --- | --- | --- | --- |
| 지금(요소 벡터 + 이름 · 조항 · 조건 · 단위) | **163** | 173 | — |
| 요소만(인력 · 장비 · 자재 채널 · 품 비율 · 무게 그대로) | 107 | 142 | 고침 2 · 깨짐 58 |
| 요소 색인(품 없이 직종 ∪ 장비 갈래 Jaccard) | 36 | 84 | — |
| 지금 + 드문 요소 겹침으로 거름 | 144 | — | −19 |
- 까닭 = 요소 지문이 겹침 — 같은 요소 집합(직종 ∪ 장비 갈래)을 가진 기초 수 중앙 **10** · 10 넘는 줄 91 / 177 · 하나뿐 64 · 숫자 하나뿐 줄(보통인부 하나 등) 26.
- 요소가 같은 기초 사이를 가르는 것은 이름 · 인용 조항 · 조건 — 지금 점수가 이미 요소 벡터(인력 3 · 장비 2 · 자재 1)를 가장 무겁게 씀(`Fit.py:35-44`).
### C-4. 줄 단위 견줌을 후보(고름 대기)로 넓힘(B-1 (3)) — 1위 기초에 `SkPatch.plan(pick = 1위)`
| 고름 대기 89 블록 → 1위와 줄 견줌 | 블록 | 부르는 줄 |
| --- | --- | --- |
| 모두 같음(셋 다 엑셀 줄 하나 · 숫자 하나뿐 규칙으로 후보 — 흙깎기 · 잔토처리 · 구조물터파기) | 3 | 13 |
| 구성 같음 · 품만 다름(덮개만) | 25 | 75 |
| 엑셀이 줄 더함(더한줄 · 「기초 + 더한 항목」) | 18 | 89 |
| 우리만 줄 있음(뺀줄) | 14 | 49 |
| 더함 + 뺌 | 12 | 46 |
| 견줌 못함(단위 성격 다름 — 100개 ↔ ㎡ · 본 ↔ M · ㎡ ↔ 조 …) | 17 | 46 |
- 골든 고름 대기 62 → 견줌 47 · **1위 맞음 42(89 %)** — 모두 같음 2/2 · 구성 같음 10/12 · 엑셀이 더함 14/15 · 우리만 11/12 · 더함 + 뺌 5/6 · 견줌 못함 15(1위 맞음 11).
- 견준 줄 「같은 줄 수」는 문턱으로 못 씀 — 고름 대기 61 · 지금 SK 도 중앙 0(직종 · 장비는 같고 품이 다 다름 = 품셈 판 · 설계자 품) · 같은 줄 ≥ 1 로 거르면 6 줄만 남음.
- 지금 SK 37 꼴 = 구성 같음 13 · 엑셀이 더함 13 · 더함 + 뺌 8 · 우리만 3 → 「기초 + 더한 항목」(사용자 그림)은 SK · 후보 모두에서 셋 중 하나쯤 · 가장 많은 꼴 = 「같은 항목 · 다른 품」.
### C-5. 못옮김 → M01 요소(B-2 둘째 줄)
- 못옮김 9 = 자재 8(MT 후보 있음 7 · 없음 1) · 기계 1 → 후보를 붙이면 7 이 한 번 고름 · 부르는 일위대가 줄 15.
### C-6. 일위대가 남은 줄 · 수 변화
| 잼 | 지금 | 안(B-2 · 만들지 않음) | 까닭 |
| --- | --- | --- | --- |
| 자동 이어짐(일위대가 줄) | 50.5 % | **같음** | 582-2 「완벽히 고른 경우에만」 — 안은 보여주기 · 후보 · 미리 셈만 |
| 고름 대기 블록에 줄 비교가 보임 | 0 / 89 | 72 / 89 | 견줌 못함 17 = 단위 계수 고름(지금 길) |
| 그 블록을 부르는 일위대가 줄 | 318 줄 = 후보 다섯 중 고름 | 272 줄 = 1위 줄 비교 보고 한 번 누름 | 누르면 SK 참조로 이어짐(729-2 길) |
| 못옮김 후보 | 0 | 7 / 9 | |
| 골든 1위 | 163 | 163 | 찾기 안 바꿈 |
| 「다른 점 · 후보 · 고름 대기」 판정 수 | 45 · 54 · 62(골든) | 그대로(가) · 고름 대기 → 다른 점(줄) 47(나) | D-4 |
### C-7. 좋아지는 곳 · 나빠지는 곳
| | 곳 | 까닭 |
| --- | --- | --- |
| 좋아짐 | 고름 대기 72 블록 · 318 줄 | 「맞는 기초 없음 — 후보 5」 대신 1위 기초와 항목마다 같음 · 품 다름 · 엑셀만 · 우리만 · 누르면 곧 SK(엑셀 품 보존) — 1위 맞음 89 % |
| 그대로 | 숫자 하나뿐 줄 3 블록 | 줄 단위로 모두 같으나 한 줄이라 우연 맞음 — 지금 막음(`Fit.single`) 그대로 둠 |
| 좋아짐 | 못옮김 7 · 기계 줄 EQ | 요소 후보 · 낱말 덤 |
| 나빠짐(안 함) | 찾기를 요소 지문으로 바꿈 | 골든 1위 163 → 107 · 거름만 넣어도 144 — 요소 같은 기초가 10 개쯤 |
| 나빠짐(안 함) | 「같은 줄 비율」로 판정 문턱 | 거의 모든 블록이 0 — 판정이 갈리지 않음 |
| 주의 | 1위 틀림 5 / 47 을 누르면 바탕이 틀린 SK | 값은 엑셀 품 그대로지만 740 [초기화] 는 바탕 M01 기초 값으로 돌아감 → 누름은 사용자 몫으로 둠 |
| 셈 시간 | 고름 대기 블록마다 엔진 견줌 한 번 더 | 측정 안 함(지금 북부청 맞춤 셈 35~52 초) |
---
## D. 권함
### D-1. 넣을 것 · 안 넣을 것
- **넣음** — 판정 뒤 단계(만들기 앞 보여주기)에만: ① 고름 대기 블록에 1위 기초(고르면 그 후보)와 항목별 줄 비교 ② 못옮김 · 남은 기계 줄에 M01 요소 후보.
- **안 넣음** — 찾기(순위)를 요소 지문 · 색인으로 바꾸기 · 판정 문턱을 「같은 줄 비율」로 바꾸기 · 일위대가 줄 → 요소(이미 있음).
- 사용자 걱정에 대한 답 = 일위대가는 이미 항목마다 맞춤 · 덩어리로 보는 자리는 산근 블록 판정 하나 → 거기에 항목별 비교를 붙이는 것이 사용자 안의 실효 부분.
### D-2. 일감
| 일감 | 파일 | 단계 | 크기 | 시험 |
| --- | --- | --- | --- | --- |
| ① 고름 대기 줄 비교(서버) | `M02_Hopyo_SkPatch.py` `_wait`(`:312-319`) — 후보 1위에 `plan(pick)` 요약 `줄비교 {같음, 품다름[{갈래, 이름, 엑셀, 우리}], 엑셀만, 우리만, 못옮김}` · 만들지 않음 · `M02_Hopyo_Sangeun.build` 응답 `고름대기[].후보[0].줄비교` | [단가산출 만들기] · 남은 줄 | 서버 40 줄 안 | `test_729_hopyo_sk_link` · `test_718_1_hopyo_import` + 새 1(북부청 고름 대기 한 블록 줄비교 = 엑셀 품) |
| ② 고름 대기 창 줄 표(화면) | `M02_MasterTemplete_Hopyo_WizardSk.ts` · 고르기 창 `_Choose.ts` — 후보 옆 줄 표 · [이 기초로] = 지금 `바탕[번].로직` 몸 그대로 | 마법사 ② | 화면 60 줄 안 | `test_m02_hopyo_screen` · ORCA 사진 |
| ③ 못옮김 · 기계 요소 후보 | `SkPatch._Plan.miss` 부르는 곳(`:246` · `:263` · `:282`) 후보 = `Material.candidates` · EQ 갈래 + 규격 수 + 이름 낱말 · `M02_Hopyo_Fixed.Linker._machine`(`:105-116`) 낱말 덤 | 만들기 · 남은 줄 | 서버 20 줄 안 | `test_705_3_hopyo_fixed` · `test_729_hopyo_sk_link` |
- 차례 = ① → ② (sub1 → sub5 · 745 미리 고름과 같은 고르기 창이라 745 와 묶음) · ③ 은 따로 작음.
- 곁 발견(585 밖 · 한 줄): 단위 모듈이 「hr ↔ 시간」 · 「개 ↔ 개소」를 성격 다름으로 봄(`master_unit.swap`) — 맞춤 쪽 `Fit.unit_fit` 은 같다고 봄 · 고름 대기 1~3 블록이 이것 탓.
### D-3. 맞물림
| 제약 | 안과 |
| --- | --- |
| 582-2 사용자가 완벽히 고른 경우에만 만듦 | 맞음 — 자동 만들기 · 체크 없음 · 보여주기 · 후보만 |
| 584-3 미리 고름 = 다른 점만 | 고름 대기는 점 없음(가) — 올릴지는 D-4 |
| 584-2 · 743 불러온 엑셀 보관 안 함 | 맞음 — 줄 비교는 불러오기 동안 임시 자리 블록 글로만 셈 · 만든 SK 덮개 = 엑셀 품(값) · 본뜬 엑셀 사본은 743 이 걷음 · 끝낸 뒤 다시 맞춤 안 함 = 다시 불러옴 |
| 739 별칭 중앙 | 짝 = 기초까지만(그대로) · 줄 비교 · 못옮김 이음은 장부에 안 넣음(블록 안 줄은 일위대가 줄 열쇠가 아님 · 9 줄뿐) |
| 740 연결 = M01 키 | 맞음 — 줄 비교 결과가 곧 SK 덮개 · 더한줄(LB · 기계경비 로직 + EQ · MT) |
### D-4. 사용자에게 물을 갈래(하나)
「맞는 기초 없음」 단가산출 72 블록에 1위 기초와 항목별 비교를 보여줄 때:
- (가) 비교만 보여줌 · 미리 고름 점 없음 · 사용자가 보고 누름 — **권함**(지금 「다른 점」 정확도 93 % 보다 낮은 89 %)
- (나) 「다른 점」으로 올려 미리 고름 점까지(584-3 대상이 47 줄 늘어남)
@@ -89,3 +89,27 @@
## 요구 581 (11-02 · 사용자 · 579-2 복사 범위 물음 답)
서로간의 독립성을 확보할 생각임. 여러군데에서 서로 연결할 경우 확인이 어려우며 하나를 바꾼다면 자연스럽게 연결로 데이터가 바뀌니 참조가 어려움. 다시말해서 단가산출과 일위대가의 연결고리정보는 M01의 기초데이터의 키 값으로 연결 정보를 알 수만 있게하고 데이터를 독립성을 가져야함. 이때 M02에서 만들어져 있던 정보들은 M02의 키값이 아니라 M01의 키값을 가져가야한다는 것임. 그리고 만약 단가산출과 일위대가에서 사용자가 변경한 값을 초기화를 한다면 연결된 키 값을 보고 다시 데이터를 가져오면 됨. 이해했음? 단가산출에서 초기화는 기본정보와 별도로 입력된 항목을 한번에 연결된 M01 키의 값으로 대체하면 됨. 초기화버튼은 삭제버튼 좌측에 배치.
## 요구 582 (11-02 · 사용자)
1. 오늘 단가산출의 상세에서 실수로 기준값을 삭제하고 원복해달라고 요청했었음. 그런데 제대로 원복이 되지않음. 현재 M01의 단가산출 기초를 데이터를 보면 단가산출 기초의 수량은 제대로 되어 있음. 재확인 필요. 다시 말하면 M02의 단가산출에서 단가산출을 가져온다면 당연히 선택된 M01의 단가산출을 복사해서 가져와야함. 이때 수량은 유지되고 있어야함. 하지만 사용자의 선택에 따라 해당 수량을 바꿀수 있어야함.
2. 오늘 원래 내가 수량을 삭제해달라고 했던 부분에 대해 다시 설명하겠음. 일위대가 불러오기의 기능에서 다른 프로그램으로 출력한 엑셀데이터 불러올 경우임. 사용자가 넣어줄 엑셀데이터에는 일위대가와 단가산출의 경계가 모호함. 단가산출에서도 추가한 경우가 있고 일위대가에 M01요소들을 넣는 경우가 있음. 그래서 자동화가 어려움. 그래서 제약으로 사용자가 선택을 완벽하게 한경우에만 할수 있게 할 생각임. 하지만 하나 확인할수 있는 기능은 일위대가의 비고 열임. 이걸 참고한다면 내부에서 서로 연결을 할때 우리 로직화 할때 도움이 되지 않을까하는데 조사부터 해줘.
3. 일위대가 불러오기에서 넣은 데이터는 별도로 저장하지 않는다고 내가 요청했었음. 일위대가나 단가산출로 저장되지 않는다면 그냥 휘발성 데이터일 뿐임.
4. 경쟁사 프로그램을 REA를 통해 출력되는 정보들의 연결을 분석한자료가 기술정보 DB에 있어. 이걸 활용하면 엑셀 데이터를 우리 단가산출에 연결할때 도움이 될것같은데 조사행줘.
5. 일위대가 불러오기에서 엑셀의 내용이 열조절이 안되어 있어. 내용확인이 어렵과 열 폭 수정이 안됨. 확인
## 요구 583 (11-02 · 사용자)
다음조사가 끝나는 시점에서 내가 결정할 사항을 한번에 보여줘.
## 요구 584 (11-02 · 사용자 · 582 조사 뒤 결정 넷 답)
1. 일위대가 안 줄 수량 → 하나의 일위대가는 결국 품셈에 있는 정보가 아님. 여러 단가산출 기초의 조합이나 요소의 조합일 확율이 높음. 일위대가 아래 하나의 단가산출 항목이라면 1 기준단위에 대한 정보(단가산출 내부 공종들의 수량이 0이란 말이 아님)가 되고 그 그값은 당연히 구조물 조합이나 프로젝트 내부에서 연결되는것이 맞음. 그렇다면 당연히 하나의 일위대가에는 얼마의 단가산출이 되는지는 사용자가 입력하는게 맞아보임. 단가산출에도 기준단위에 대한 단가와 수량이 들어갔을때의 가격을 별도로 넣어주잖아. 혹시 잘못이해한게 있으면 다시 요청
2. 불러오기 도중 자료 → 내말은 일위대가 불러오기 엑셀자료 자체를 보관할 필요가 없으며 필요한 경우 다시 불러와서 사용해야한다는 것임.
3. 미리 고름 → 「다른 점」 줄만
4. 강관비계 되살림 → 결국 M02의 데이터 중 시스템관리자인 내가 삭제하고 수정할 수 있음. 그러니 삭제된 내용이라면 그냥둬.
## 요구 585 (11-02 · 사용자 · 아이디어 검토)
또하나 일위대가 가져오기의 매칭과 관련하여 아이디어 검토
- 내가 생각했을때의 경쟁사 프로그램으로 출력한 엑셀파일에서 우리가 일위대가를 단순히 하나의 로직 그룹으로 보고 접근하게 문제가 되지 않나 생각되서 말이야. 우리의 기초로직은 M01에서 요소들과 단가산출 기초로 된걸 일위대가의 항목 하나하나와 비교하면 더 찾기 쉬운게 아닌가라는 생각이 듬.
@@ -1,10 +1,11 @@
/* =============================================================================
* test_stage2_autofit.ts — spreadsheet_autofit 시험. 돌리기: node --experimental-strip-types <이 파일>
* test_stage2_autofit.ts — spreadsheet_autofit 시험. 돌리기: esbuild --bundle --platform=node <이 파일> → node (주소 import 가 확장자 없음)
* ========================================================================== */
import {
autofitColumn,
estimateColumnWidth,
fillMissingWidths,
} from "../../../A00_Common/spreadsheet/spreadsheet_autofit.ts";
import { assertEqual, makeBook, makeCtx, makeSheet, v } from "./test_stage2_helpers.ts";
@@ -28,5 +29,23 @@ assertEqual(sheet.열?.A.폭, widthA, "autofitColumn 이 열폭 명령으로 반
const fixedWidth = estimateColumnWidth(book, "s1", 0, () => 7);
assertEqual(fixedWidth, 9, "measure 를 넘기면 그걸로 어림(7 + 여백 2)");
// 741 — 폭 없는 열만 글 길이로 채움(최소 · 최대) · 있는 폭 · 여러 열 병합(제목) 칸은 그대로 · 뺌
const longText = "가".repeat(80);
const sheet741 = makeSheet("s2", {
A1: v("제목 한 줄이 매우 길어도 병합이라 뺌 ".repeat(5)),
A2: v("짧음"),
B1: v(longText),
C1: v("x"),
D1: v("이미 있음"),
});
sheet741.병합 = ["A1:D1"];
sheet741.열 = { D: { 폭: 33 } };
const book741 = makeBook([sheet741]);
fillMissingWidths(book741);
assertEqual(sheet741.열.D.폭, 33, "있는 폭은 그대로");
assertEqual(sheet741.열.B.폭, 60, "너무 긴 글은 최대 60");
assertEqual(sheet741.열.C.폭, 6, "짧은 글은 최소 6");
assertEqual(sheet741.열.A.폭 < 20, true, "병합 제목 칸은 폭에서 뺌");
if (process.exitCode) process.exit(process.exitCode);
console.log("spreadsheet_autofit 시험 끝");
+40 -1
View File
@@ -63,7 +63,7 @@ def test_721_2_부르는_곳_셋만_새_창():
def test_721_2_고정_줄_꼴():
find = _text(M02 / "M02_MasterTemplete_Find_Pick.ts")
assert "이음: { 노임: labor ? e.코드 : null, 자재: labor ? null : e.코드, 기계: null }" in find
assert "go: put ? take(() => put(e))" in find
assert "take(() => put(e))" in find and "!put" in find
assert "dim: !put," in find # 받는 쪽 없으면 회색 막힘
@@ -151,3 +151,42 @@ def test_736_2_빠진_길_둘_신규_자재_기계_탭():
assert '"machine"' in lost and "pickMachine" not in lost
unlinked = _text(M02 / "M02_MasterTemplete_Labor_Unlinked.ts")
assert 'machine: (e) => done("기계", e.코드)' in unlinked and "pickMachine" not in unlinked
def test_736_3_기계_고정_줄_금액():
"""736-3 — [항목 추가] 기계(EQ) = 고정 줄(이음 기계 · 단가 = 한 시간 노 · 재 · 경) — 단가산출 · 일위대가가 같은 금액."""
from M02_MasterTemplete import M02_Machine_Hourly as machine
EQ = "EQ000019" # 굴착기(무한궤도) 0.6㎥
got = machine.hourly(EQ)
assert got["ok"] and got["계"] > 0
assert got["계"] == pytest.approx(got["노무비"] + got["재료비"] + got["경비"])
assert not machine.hourly(LB)["ok"] and not machine.hourly("EQ999999")["ok"]
fx = {
"이름": got["이름"],
"규격": got["규격"],
"단위": "시간",
"단가": {k: got[k] for k in ("노무비", "재료비", "경비")},
"이음": {"노임": None, "자재": None, "기계": EQ},
}
sk = calc.calc_doc(
{
"키": "SK-000001",
"이름": "시험",
"단위": "시간",
"바탕": None,
"더한줄": [{"고정": fx, "이름": got["이름"], "단위": "시간", "수량": 1}],
}
)["1단위"]
doc = labor.clean({"담은로직": [{"로직": "", "고정": fx, "수량": {"손": 1}}]})
assert sk["계"] == pytest.approx(got["계"])
assert labor.calc(doc, {})["반영합"]["계"] == pytest.approx(got["계"])
find = _text(M02 / "M02_MasterTemplete_Find_Pick.ts")
assert (
"/logic-lib/machine-hourly?key=" in find
and "이음: { 노임: null, 자재: null, 기계: e.코드 }" in find
)
assert "machine: fixed &&" in _text(M02 / "M02_MasterTemplete_Labor_Pick.ts")
assert "machine: (e) => done({ 고정: fixedOf(e)" in _text(
M02 / "M02_MasterTemplete_Sangeun_Pick.ts"
)
@@ -0,0 +1,147 @@
"""별칭 장부 중앙 관리(PLAN 739-1 · `M02_Hopyo_Ledger` · `M02_Hopyo_Line` ·
`M01_MasterData_Alias`) — 자동 짝 = 마스터(승인) · 이 불러오기에서 이은 짝만 · 회사 · 개인 층 짝 =
「내 제안」(자동 0 · 꼬리표 · 고르기 맨 위 `제안`) · 이은 짝 → 층 장부(불러오기 표시) → M01 후보 ·
승인 뒤 다음 불러오기 자동 · 장부 화면 승인 상태(대기 · 승인 · 반려) · master_data 원본 그대로.
층 · storage 는 임시 폴더.
"""
from M01_MasterData import M01_MasterData_Alias as alias
from M02_MasterTemplete import M02_Hopyo_Fit as fit
from M02_MasterTemplete import M02_Hopyo_Ledger as ledger
from M02_MasterTemplete import M02_Hopyo_Store as hs
from M02_MasterTemplete import M02_Template_Layers as layers
# 올리기 길 시험의 `client` 마당(임시 storage · 마스터 층)을 같이 씀
# ruff: noqa: F811
from resources.tester.test_m02_hopyo_excel import URL, _post, client, master_hash # noqa: F401
DIG = {"이름": "구조물터파기(인력100%)", "규격": "토사", "단위": "㎥"}
DIG_KEY = "구조물터파기(인력100%)|토사|m3"
WHO = {"id": 42, "이름": "갑"}
ADMIN = {"id": 1, "이름": "관리자"}
LEDGER = "/api/m02/layers/{}/hopyo-ledger"
FROM_LEDGER = ("별칭", "뺌") # 장부가 자동으로 정한 줄 상태
def _pair(code):
return ledger.check(DIG_KEY, {"갈래": "짝", "짝": [{"로직": code, "배": 1}]})
def _candidate(key, code):
got = [
x
for x in alias.gather()["후보"]
if x["열쇠"] == key and x["종류"] == alias.ALIAS and x["짝"][0]["로직"] == code
]
assert len(got) == 1, got
return got[0]
def test_장부_층_짝은_제안_이_불러오기_짝은_자동_승인_뒤_모두_자동(client):
before = master_hash()
mine = layers.personal_dir(7, 42)
here = ledger.doc_id(mine, "HP-000001")
assert here == "personal/7/42/HP-000001"
ledger.write(mine, {DIG_KEY: _pair("GF000101")}, WHO, "가.xls", here)
got = ledger.recall(DIG, mine, doc=here)
assert (got["층"], got["제안"]) == ("personal", False) # 이 불러오기에서 이은 짝 = 바로 씀
later = ledger.doc_id(mine, "HP-000002") # 다음 불러오기
for doc in (later, None):
got = ledger.recall(DIG, mine, doc=doc)
assert (got["층"], got["짝"][0]["로직"], got["제안"]) == ("personal", "GF000101", True)
ledger.put(mine, {DIG_KEY: {**_pair("GF000101"), "엑셀조항": []}}, WHO) # 장부 화면 고침
assert ledger.read(mine)[DIG_KEY]["불러오기"] == here # 이은 불러오기 표시는 남음
assert [r["상태"] for r in ledger.rows(mine, True)] == ["대기"]
# M01 후보에 보임 → 승인 → 마스터 → 다음 불러오기 · 다른 회사 새 사용자도 자동
sig = _candidate(DIG_KEY, "GF000101")["서명"]
alias.approve(alias.ALIAS, DIG_KEY, sig, ADMIN)
for folder, doc in ((mine, later), (layers.personal_dir(9, 99), None)):
got = ledger.recall(DIG, folder, doc=doc)
assert (got["층"], got["짝"][0]["로직"], got["제안"]) == ("system", "GF000101", False)
assert [(r["층"], r["상태"]) for r in ledger.rows(mine, True)] == [
("personal", "승인"),
("system", "승인"),
]
# 다른 짝 제안 → 반려 = 상태 반려 · 여전히 내 제안(자동 아님)
other = layers.personal_dir(7, 43)
ledger.write(other, {DIG_KEY: _pair("GF000103")}, {"id": 43}, "나.xls")
alias.reject(alias.ALIAS, DIG_KEY, _candidate(DIG_KEY, "GF000103")["서명"], ADMIN)
assert ledger.rows(other, True)[0]["상태"] == "반려"
got = ledger.recall(DIG, other)
assert (got["층"], got["짝"][0]["로직"]) == ("system", "GF000101") # 자동 = 마스터
assert master_hash() == before
def _hopyo(client, no):
res = client.get(f"{URL.format('personal')}/HP-000001/{no}")
assert res.status_code == 200, res.text
return res.json()["호표"]
def _lines(client, no):
return {one["이름"]: one["맞춤"] for one in _hopyo(client, no)["줄"]}
def _auto(client, no):
"""장부가 자동으로 정한 줄 수."""
return sum(1 for m in _lines(client, no).values() if m["상태"] in FROM_LEDGER)
def test_불러오기_내_제안_후보_이은_짝_승인_흐름(client):
before = master_hash()
assert _post(client).status_code == 200
mine = layers.personal_dir(7, 42)
fit.run_job(mine, "HP-000001")
plain = _lines(client, 3)["구조물터파기(인력100%)"]
assert plain["상태"] not in FROM_LEDGER and "제안" not in plain
# 개인 층 장부 짝(옛 층 장부 · 승인 전) = 자동 0 · 꼬리표 「내 제안」 · 고르기 맨 위 `제안`
row = {"갈래": "짝", "짝": [{"로직": "GC000083", "배": 1}]}
res = client.put(LEDGER.format("personal"), json={"줄": {DIG_KEY: row}})
assert res.status_code == 200, res.text
assert _auto(client, 3) == 0
got = _lines(client, 3)["구조물터파기(인력100%)"]
assert (got["상태"], got["체크"]) == (plain["상태"], plain["체크"]) # 숫자 판정 그대로
assert "내 제안" in got["꼬리표"] and "별칭" not in got
assert got["제안"]["짝"][0]["로직"] == "GC000083" and got["제안"]["층"] == "personal"
assert got["제안"]["대상"][0]["로직"] == "GC000083"
# 확인하고 만들기 = 이은 짝 → 이 불러오기 안에서는 바로 자동 · 층 장부(제안)에 불러오기 표시
hopyo = _hopyo(client, 3)
at = next(x["차례"] for x in hopyo["줄"] if x["이름"] == "구조물터파기(인력100%)")
body = {"층": "personal", "키": "HP-000001", "번": 3}
lines = [{"차례": at, "대상": [{"로직": "GC000083", "입력값": {}, "배": 1}]}]
left = client.post(
"/api/m02/layers/personal/hopyo-unlinked", json={**body, "줄": lines}
).json()["남은줄"]
lines += [{"차례": x["차례"], "뺌": "우리에 없음"} for x in left]
res = client.post("/api/m02/layers/personal/hopyo-build", json={**body, "줄": lines})
assert res.status_code == 200, res.text
book = ledger.read(mine)
assert book[DIG_KEY]["불러오기"] == "personal/7/42/HP-000001"
got = _lines(client, 3)["구조물터파기(인력100%)"]
assert got["상태"] == "별칭" and got["별칭"]["층"] == "personal"
# 다음 불러오기(다른 HP) = 승인 전 자동 0
nxt = {k: {**v, "불러오기": "personal/7/42/HP-000009"} for k, v in book.items()}
hs.remember(mine, nxt) # 장부 파일 그대로 — 다른 불러오기에서 이은 짝
assert _auto(client, 3) == 0
# M01 후보에 보임 → 승인 → 다음 불러오기 자동(마스터)
sig = _candidate(DIG_KEY, "GC000083")["서명"]
alias.approve(alias.ALIAS, DIG_KEY, sig, ADMIN)
got = _lines(client, 3)["구조물터파기(인력100%)"]
assert got["상태"] == "별칭" and got["별칭"]["층"] == "system" and "제안" not in got
assert _auto(client, 3) == 1
assert master_hash() == before
def test_화면_내_제안_글():
"""고르기 창 맨 위 「내 제안」(한 번 눌러 짝 통째) · 장부 화면 = 내 제안 목록 · 승인 칸."""
from pathlib import Path
m02 = Path(__file__).resolve().parents[2] / "M02_MasterTemplete"
choose = (m02 / "M02_MasterTemplete_Hopyo_Choose.ts").read_text(encoding="utf-8")
assert choose.index("...(mineRow ? [mineRow] : [])") < choose.index("...sections")
assert '"data-group": "내 제안"' in choose and "mine.짝.map((p)" in choose
rows = (m02 / "M02_MasterTemplete_Hopyo_CheckRows.ts").read_text(encoding="utf-8")
assert '"고르기 · 내 제안"' in rows
book = (m02 / "M02_MasterTemplete_Hopyo_Ledger.ts").read_text(encoding="utf-8")
assert 'MINE_TITLE = "내 제안 목록"' in book and '"승인",' in book
assert "createBadge(r.상태, STATE_TONE[r.상태])" in book
+49
View File
@@ -0,0 +1,49 @@
"""741-1 지킴 — 일위대가 엑셀 보기 열 폭: xlsx 도 파일 열 폭을 서버가 보냄 · 열 끌기 · 두 번 눌러
맞춤은 읽기 전용에서도 열림(화면 수치 판정은 tmp 일회 검증)."""
import io
from pathlib import Path
import openpyxl
from M02_MasterTemplete import M02_Hopyo_Grid as grid
ROOT = Path(__file__).parents[2]
SS = ROOT / "A00_Common/spreadsheet"
def _xlsx(widths: dict[str, float]) -> bytes:
wb = openpyxl.Workbook()
ws = wb.active
ws.title = "일위대가"
ws["A1"] = "x"
for letter, width in widths.items():
ws.column_dimensions[letter].width = width
ws.column_dimensions["E"].hidden = True
out = io.BytesIO()
wb.save(out)
return out.getvalue()
def test_xlsx_widths_read_from_cols():
got = grid._xlsx_widths(_xlsx({"A": 30, "C": 12.5}))["일위대가"]
assert got["A"] == {"폭": 30.0} and got["C"] == {"폭": 12.5}
assert got["E"].get("숨김") is True
def test_xlsx_without_widths_is_empty_and_garbage_is_safe():
assert grid._xlsx_widths(_xlsx({}))["일위대가"].keys() <= {"E"}
assert grid._xlsx_widths(b"not a zip") == {}
def test_xlsx_sheet_carries_widths():
data = _xlsx({"A": 30})
sheets = grid._xlsx("a.xlsx", data)["시트"]
assert sheets and sheets[0]["열"]["A"]["폭"] == 30.0
def test_readonly_still_resizes_columns():
spread = (SS / "spreadsheet.ts").read_text(encoding="utf-8")
assert 'readOnly && command.종류 !== "열폭"' in spread
mouse = (SS / "spreadsheet_mouse.ts").read_text(encoding="utf-8")
assert 'ctx.readOnly && hit.kind !== "colEdge"' in mouse
+18 -14
View File
@@ -1,5 +1,5 @@
"""일위대가 실무자료 별칭 장부(PLAN 338-3 · `M02_Hopyo_Ledger`) — 옛 꼴 바꿔 읽음 · 새 꼴 ·
층 차례(아래 층이 이김) · 느슨 찾기(규격다름) · 우리에 없음 · 안 씀 · 호표 통째 열쇠 ·
층 차례(739-1 자동 = 마스터 · 이 불러오기 짝 · 아래 층 = 제안) · 느슨 찾기(규격다름) · 우리에 없음 · 안 씀 · 호표 통째 열쇠 ·
위 층 읽기만 · 재검사 · 쓰기(처음파일 둠 · 옛 꼴 새로 씀) · master_data 원본 그대로. 층은 임시 폴더.
"""
@@ -65,21 +65,26 @@ def test_옛_꼴_바꿔_읽음(client):
assert got["표지석||개"]["짝"] == [{"자재": "MT004258", "입력값": {}, "배": 1, "몫": None}]
def test_층_차례_아래_층이_이김_느슨_찾기(client):
def test_층_차례_마스터만_자동_아래_층은_제안_느슨_찾기(client):
mine, company, system = ledger.chain(layers.personal_dir(7, 42))
assert (company, system) == (layers.company_dir(7), store.FOLDER)
new = {"갈래": "짝", "짝": [{"로직": "GF000103", "입력값": {}, "배": 1, "몫": None}]}
_put_raw(system, {DIG_KEY: new, "잔토처리(인력100%)||m3": {"갈래": "우리에 없음"}})
_put_raw(company, {DIG_KEY: {**new, "짝": [{"로직": "GF000102", "배": 1}]}})
got = ledger.recall(DIG, mine)
assert (got["층"], got["주인"], got["짝"][0]["로직"], got["규격다름"]) == (
"company",
"7",
"GF000102",
got = ledger.recall(DIG, mine) # 739-1 — 회사 층 짝이 있어도 자동 = 마스터(승인)
assert (got["층"], got["주인"], got["짝"][0]["로직"], got["규격다름"], got["제안"]) == (
"system",
None,
"GF000103",
False,
False,
)
_put_raw(system, {"잔토처리(인력100%)||m3": {"갈래": "우리에 없음"}})
got = ledger.recall(DIG, mine)
assert (got["층"], got["짝"][0]["로직"], got["제안"]) == ("company", "GF000102", True)
_put_raw(mine, {DIG_KEY: {"로직": "GF000101", "환산": 1}})
assert ledger.recall(DIG, mine)["짝"][0]["로직"] == "GF000101" # 같은 층 먼저
got = ledger.recall(DIG, mine)
assert (got["짝"][0]["로직"], got["제안"]) == ("GF000101", True) # 제안 = 같은 층 먼저
rock = {**DIG, "규격": "경암"}
loose = ledger.recall(rock, mine)
assert loose["규격다름"] is True and loose["층"] == "personal" # 품명|단위 느슨
@@ -90,12 +95,11 @@ def test_층_차례_아래_층이_이김_느슨_찾기(client):
books = ledger.books(mine)
assert ledger.recall(DIG, mine, books)["짝"][0]["로직"] == "GF000101" # 한 번 읽어 넘김
rows = ledger.rows(mine, True)
assert [(r["층"], r["고침"]) for r in rows] == [
("personal", True),
("company", False),
("system", False),
("system", False),
] # 위 층 = 읽기만
assert [(r["층"], r["고침"], r["상태"]) for r in rows] == [
("personal", True, "대기"),
("company", False, "대기"),
("system", False, "승인"),
] # 위 층 = 읽기만 · 739-1 승인 상태
assert not any(r["고침"] for r in ledger.rows(mine, False))
+11 -4
View File
@@ -157,11 +157,18 @@ def _ledger(client, rows, layer="personal"):
assert res.status_code == 200, res.text
def _master(client, rows):
"""739-1 자동 짝 = 마스터(승인) 장부만 — 시스템 관리자로 마스터 층에 씀."""
client.who.update(role="SYSTEM_ADMIN")
_ledger(client, rows, "system")
client.who.update(role="USER")
def test_별칭_체크_규칙(client):
_ready(client)
dig = "구조물터파기(인력100%)|토사|m3"
remicon = "레미콘타설(장비사용)|철근구조물-소량타설|m3"
_ledger(
_master(
client,
{
dig: {"갈래": "짝", "짝": [{"로직": "GC000083", "배": 1}]},
@@ -172,7 +179,7 @@ def test_별칭_체크_규칙(client):
)
got = _hopyo(client, 3)["구조물터파기(인력100%)"]
assert got["상태"] == "별칭" and got["체크"] is True and got["대상"][0]["출처"] == {}
assert got["별칭"]["층"] == "personal" and not got["별칭"]["근거다름"]
assert got["별칭"]["층"] == "system" and not got["별칭"]["근거다름"]
first = _hopyo(client, 1)
other = first["레미콘타설(장비사용)"] # 숫자 판정 맞음 = GC000198 ≠ 장부 짝 → 판정 먼저(358-3)
assert other["상태"] == "맞음" and other["체크"] is True and "별칭" not in other
@@ -182,14 +189,14 @@ def test_별칭_체크_규칙(client):
assert loose["상태"] == "맞음" and "별칭" not in loose and "별칭 · 규격 다름" in loose["꼬리표"]
assert all("별칭" not in t["출처"].values() for t in loose["대상"])
assert "GC000211" in {c["로직"] for c in loose["후보"]} | {t["로직"] for t in loose["대상"]}
_ledger(
_master(
client, {remicon: {"갈래": "짝", "짝": [{"로직": "GC000198", "입력값": {"소량감률": 50}}]}}
)
plain = _hopyo(client, 7)["레미콘타설(장비사용)"] # 무근구조물 ↔ 장부 철근구조물-소량타설
assert plain["상태"] == "맞음" and plain["체크"] is True and "별칭" not in plain
assert plain["대상"][0]["로직"] == "GC000198" and plain["대상"][0]["입력값"]["소량감률"] == 0
assert "별칭 · 규격 다름" in plain["꼬리표"]
_ledger(client, {dig: {"갈래": "짝", "짝": [{"로직": "GF000101", "배": 1}]}})
_master(client, {dig: {"갈래": "짝", "짝": [{"로직": "GF000101", "배": 1}]}})
off = _hopyo(client, 3)["구조물터파기(인력100%)"] # 산림 로직 ↔ 엑셀 건설 3-2-1
assert off["별칭"]["근거다름"] and off["체크"] is False and "근거 다름" in off["꼬리표"]
+3 -2
View File
@@ -514,10 +514,11 @@ def test_명사형_단추와_별칭_장부_풀이_359():
갈래 글 = 확인 창 드롭다운과 같음(저장 값 그대로)."""
ledger = _text("M02_MasterTemplete_Hopyo_Ledger.ts")
hint = "엑셀 품명 ↔ 우리 로직 · 자재 짝을 기억해 두었다가 같은 품명이 나오면 먼저 제안"
assert hint in ledger and "note(`${LEDGER_HINT}" in ledger
assert hint in ledger and "`${LEDGER_HINT}" in ledger
side = _text("M02_MasterTemplete_Hopyo_Side.ts")
assert "title: LEDGER_HINT" in side
for label in ('"변경 취소"', '"항목 추가"', '"내 층 복사"', '"삭제"'):
assert 'label: master ? "삭제" : "거두기"' in ledger # 739-1 내 제안 = 거두기
for label in ('"변경 취소"', '"항목 추가"', '"내 층 복사"'):
assert f"label: {label}" in ledger, label
assert '"우리에 없음": "로직없음"' in ledger and '"안 씀": "사용안함"' in ledger
mine = ("Hopyo_Side", "Hopyo_Sheet", "Hopyo_Check", "Hopyo_CheckRows", "Hopyo_Ledger")