From d46fafd1ba3ee35cd39ac5a26ea4688221e11fcc Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 2 Nov 2026 18:39:41 +0900 Subject: [PATCH 1/9] =?UTF-8?q?knowledge(=EC=9A=94=EA=B5=AC):=20=EC=9A=94?= =?UTF-8?q?=EA=B5=AC=20582=20=EC=9B=90=EB=AC=B8=20=EA=B8=B0=EB=A1=9D=20?= =?UTF-8?q?=E2=80=94=20=EB=8B=A8=EA=B0=80=EC=82=B0=EC=B6=9C=20=EA=B8=B0?= =?UTF-8?q?=EC=B4=88=20=EC=88=98=EB=9F=89=20=EC=9B=90=EB=B3=B5=20=C2=B7=20?= =?UTF-8?q?=EB=B6=88=EB=9F=AC=EC=98=A4=EA=B8=B0=20=EC=88=98=EB=9F=89=20?= =?UTF-8?q?=EB=9C=BB=20=EB=8B=A4=EC=8B=9C=20=C2=B7=20=EB=B9=84=EA=B3=A0=20?= =?UTF-8?q?=EC=97=B4=20=C2=B7=20=EB=B6=88=EB=9F=AC=EC=98=A8=20=EC=9E=90?= =?UTF-8?q?=EB=A3=8C=20=ED=9C=98=EB=B0=9C=20=C2=B7=20=EA=B2=BD=EC=9F=81?= =?UTF-8?q?=EC=82=AC=20REA=20=EB=B6=84=EC=84=9D=20=C2=B7=20=EC=97=91?= =?UTF-8?q?=EC=85=80=20=EC=97=B4=20=ED=8F=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015uTVHFZkNcUa1SR4cNcENM --- docs/raw/요구_1102/사용자요구_1102.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/raw/요구_1102/사용자요구_1102.md b/docs/raw/요구_1102/사용자요구_1102.md index 6c4c1edc0..10362b698 100644 --- a/docs/raw/요구_1102/사용자요구_1102.md +++ b/docs/raw/요구_1102/사용자요구_1102.md @@ -89,3 +89,11 @@ ## 요구 581 (11-02 · 사용자 · 579-2 복사 범위 물음 답) 서로간의 독립성을 확보할 생각임. 여러군데에서 서로 연결할 경우 확인이 어려우며 하나를 바꾼다면 자연스럽게 연결로 데이터가 바뀌니 참조가 어려움. 다시말해서 단가산출과 일위대가의 연결고리정보는 M01의 기초데이터의 키 값으로 연결 정보를 알 수만 있게하고 데이터를 독립성을 가져야함. 이때 M02에서 만들어져 있던 정보들은 M02의 키값이 아니라 M01의 키값을 가져가야한다는 것임. 그리고 만약 단가산출과 일위대가에서 사용자가 변경한 값을 초기화를 한다면 연결된 키 값을 보고 다시 데이터를 가져오면 됨. 이해했음? 단가산출에서 초기화는 기본정보와 별도로 입력된 항목을 한번에 연결된 M01 키의 값으로 대체하면 됨. 초기화버튼은 삭제버튼 좌측에 배치. + +## 요구 582 (11-02 · 사용자) + +1. 오늘 단가산출의 상세에서 실수로 기준값을 삭제하고 원복해달라고 요청했었음. 그런데 제대로 원복이 되지않음. 현재 M01의 단가산출 기초를 데이터를 보면 단가산출 기초의 수량은 제대로 되어 있음. 재확인 필요. 다시 말하면 M02의 단가산출에서 단가산출을 가져온다면 당연히 선택된 M01의 단가산출을 복사해서 가져와야함. 이때 수량은 유지되고 있어야함. 하지만 사용자의 선택에 따라 해당 수량을 바꿀수 있어야함. +2. 오늘 원래 내가 수량을 삭제해달라고 했던 부분에 대해 다시 설명하겠음. 일위대가 불러오기의 기능에서 다른 프로그램으로 출력한 엑셀데이터 불러올 경우임. 사용자가 넣어줄 엑셀데이터에는 일위대가와 단가산출의 경계가 모호함. 단가산출에서도 추가한 경우가 있고 일위대가에 M01요소들을 넣는 경우가 있음. 그래서 자동화가 어려움. 그래서 제약으로 사용자가 선택을 완벽하게 한경우에만 할수 있게 할 생각임. 하지만 하나 확인할수 있는 기능은 일위대가의 비고 열임. 이걸 참고한다면 내부에서 서로 연결을 할때 우리 로직화 할때 도움이 되지 않을까하는데 조사부터 해줘. +3. 일위대가 불러오기에서 넣은 데이터는 별도로 저장하지 않는다고 내가 요청했었음. 일위대가나 단가산출로 저장되지 않는다면 그냥 휘발성 데이터일 뿐임. +4. 경쟁사 프로그램을 REA를 통해 출력되는 정보들의 연결을 분석한자료가 기술정보 DB에 있어. 이걸 활용하면 엑셀 데이터를 우리 단가산출에 연결할때 도움이 될것같은데 조사행줘. +5. 일위대가 불러오기에서 엑셀의 내용이 열조절이 안되어 있어. 내용확인이 어렵과 열 폭 수정이 안됨. 확인 From b1b979494674b97752e9bc6693f5c645c75a08c4 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 2 Nov 2026 18:40:45 +0900 Subject: [PATCH 2/9] =?UTF-8?q?feat(739-1):=20=EB=B3=84=EC=B9=AD=20?= =?UTF-8?q?=EC=9E=A5=EB=B6=80=20=EC=A4=91=EC=95=99=20=EA=B4=80=EB=A6=AC=20?= =?UTF-8?q?=E2=80=94=20=EC=9E=90=EB=8F=99=20=EC=A7=9D=20=3D=20=EB=A7=88?= =?UTF-8?q?=EC=8A=A4=ED=84=B0(=EC=8A=B9=EC=9D=B8)=20=C2=B7=20=EC=9D=B4=20?= =?UTF-8?q?=EB=B6=88=EB=9F=AC=EC=98=A4=EA=B8=B0=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EC=9D=B4=EC=9D=80=20=EC=A7=9D=EB=A7=8C=20=C2=B7=20=ED=9A=8C?= =?UTF-8?q?=EC=82=AC=20=C2=B7=20=EA=B0=9C=EC=9D=B8=20=EC=B8=B5=20=EC=A7=9D?= =?UTF-8?q?=20=3D=20=EB=82=B4=20=EC=A0=9C=EC=95=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 장부 읽는 차례: 자동 정확 열쇠(마스터 · 같은 불러오기 `불러오기` 표시) → 제안 정확 열쇠(`제안` True · 자동 아님) → 품명|단위 느슨 - 만들기 · 단가산출 만들기가 이은 짝에 불러오기 열쇠(`doc_id` 층/주인/HP 키) 남김 · 장부 화면 고침은 그 표시 그대로 둠 - 줄 맞춤: 내 제안 = 숫자 판정 그대로 + 꼬리표 「내 제안」 + `제안`(고르기 창 맨 위 · 한 번 눌러 짝 통째) · 호표 통째 제안 = 체크 안 함 - 장부 화면(회사 · 개인 층) = 「내 제안 목록」 · 승인 칸(대기 · 승인 · 반려 = M01 서명 · 반려 기억) · [삭제] → [거두기] · 마스터 층(M01) 그대로 - M01 별칭 장부 모으기 · 승인 · 반려 길 그대로 · 기존 층 장부 파일 안 지움(제안으로 읽힘) - 낡은 시험 고침: hopyo ledger(아래 층이 이김 → 마스터만 자동 · 제안 · 승인 상태) · hopyo match(별칭 자동 규칙 = 마스터 층 장부로) · hopyo screen(머리 글 · 거두기 단추) - 새 시험 test_739_1_alias_central(층 짝 자동 0 · 내 제안 · 이은 짝 → 제안 · M01 후보 · 승인 뒤 자동 · 반려 상태) Co-Authored-By: Claude Opus 5.5 --- M02_MasterTemplete/M02_Hopyo_Build.py | 5 +- M02_MasterTemplete/M02_Hopyo_Ledger.py | 64 ++++++-- M02_MasterTemplete/M02_Hopyo_Line.py | 27 +++- M02_MasterTemplete/M02_Hopyo_Sangeun.py | 4 +- .../M02_MasterTemplete_Hopyo_Api.ts | 4 + .../M02_MasterTemplete_Hopyo_CheckRows.ts | 4 +- .../M02_MasterTemplete_Hopyo_Choose.ts | 32 ++++ .../M02_MasterTemplete_Hopyo_Ledger.ts | 24 ++- resources/tester/test_739_1_alias_central.py | 147 ++++++++++++++++++ resources/tester/test_m02_hopyo_ledger.py | 32 ++-- resources/tester/test_m02_hopyo_match.py | 15 +- resources/tester/test_m02_hopyo_screen.py | 5 +- 12 files changed, 319 insertions(+), 44 deletions(-) create mode 100644 resources/tester/test_739_1_alias_central.py diff --git a/M02_MasterTemplete/M02_Hopyo_Build.py b/M02_MasterTemplete/M02_Hopyo_Build.py index 615dda78f..1713e0323 100644 --- a/M02_MasterTemplete/M02_Hopyo_Build.py +++ b/M02_MasterTemplete/M02_Hopyo_Build.py @@ -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}} diff --git a/M02_MasterTemplete/M02_Hopyo_Ledger.py b/M02_MasterTemplete/M02_Hopyo_Ledger.py index 91312db61..853b4d368 100644 --- a/M02_MasterTemplete/M02_Hopyo_Ledger.py +++ b/M02_MasterTemplete/M02_Hopyo_Ledger.py @@ -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) diff --git a/M02_MasterTemplete/M02_Hopyo_Line.py b/M02_MasterTemplete/M02_Hopyo_Line.py index 501c1633a..f2b98770a 100644 --- a/M02_MasterTemplete/M02_Hopyo_Line.py +++ b/M02_MasterTemplete/M02_Hopyo_Line.py @@ -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("차례"): diff --git a/M02_MasterTemplete/M02_Hopyo_Sangeun.py b/M02_MasterTemplete/M02_Hopyo_Sangeun.py index e5fa7d86c..9634dc0a2 100644 --- a/M02_MasterTemplete/M02_Hopyo_Sangeun.py +++ b/M02_MasterTemplete/M02_Hopyo_Sangeun.py @@ -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 diff --git a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Api.ts b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Api.ts index 3b925e3f0..eb3b487db 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Api.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Api.ts @@ -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; diff --git a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_CheckRows.ts b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_CheckRows.ts index 577378746..6f93225ef 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_CheckRows.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_CheckRows.ts @@ -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, diff --git a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Choose.ts b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Choose.ts index 4b75325b0..0b299e0c1 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Choose.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Choose.ts @@ -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] }), ); diff --git a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Ledger.ts b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Ledger.ts index c75e7e7c1..523be4af8 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Ledger.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Ledger.ts @@ -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 = { "안 씀": "사용안함", }; const LAYER_TEXT: Record = { system: "마스터", company: "회사", personal: "개인" }; +/** 739-1 회사 · 개인 층 장부 화면 제목 */ +export const MINE_TITLE = "내 제안 목록"; +const STATE_TONE: Record = { 승인: "success", 대기: "warning", 반려: "danger" }; type Edits = Record; /** 화면 한 줄 — 서버 줄 위에 고친 것 */ 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 })), }), diff --git a/resources/tester/test_739_1_alias_central.py b/resources/tester/test_739_1_alias_central.py new file mode 100644 index 000000000..9f1949ab9 --- /dev/null +++ b/resources/tester/test_739_1_alias_central.py @@ -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 diff --git a/resources/tester/test_m02_hopyo_ledger.py b/resources/tester/test_m02_hopyo_ledger.py index d60b5b239..761a74110 100644 --- a/resources/tester/test_m02_hopyo_ledger.py +++ b/resources/tester/test_m02_hopyo_ledger.py @@ -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)) diff --git a/resources/tester/test_m02_hopyo_match.py b/resources/tester/test_m02_hopyo_match.py index 111e375b9..dfc80ec41 100644 --- a/resources/tester/test_m02_hopyo_match.py +++ b/resources/tester/test_m02_hopyo_match.py @@ -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["꼬리표"] diff --git a/resources/tester/test_m02_hopyo_screen.py b/resources/tester/test_m02_hopyo_screen.py index 605f3d987..429ecb39a 100644 --- a/resources/tester/test_m02_hopyo_screen.py +++ b/resources/tester/test_m02_hopyo_screen.py @@ -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") From 5e2ac2ae79aa95415cb4d1309776928401708d46 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 2 Nov 2026 18:44:12 +0900 Subject: [PATCH 3/9] =?UTF-8?q?feat(736-3):=20=EB=8B=A8=EA=B0=80=EC=82=B0?= =?UTF-8?q?=EC=B6=9C=20=C2=B7=20=EC=9D=BC=EC=9C=84=EB=8C=80=EA=B0=80=20[?= =?UTF-8?q?=ED=95=AD=EB=AA=A9=20=EC=B6=94=EA=B0=80]=20=EA=B8=B0=EA=B3=84(M?= =?UTF-8?q?01=20EQ)=20=EA=B3=A0=EB=A6=84=20=E2=80=94=20=EA=B3=A0=EC=A0=95?= =?UTF-8?q?=20=EC=A4=84(=EC=9D=B4=EC=9D=8C=20=EA=B8=B0=EA=B3=84=20=C2=B7?= =?UTF-8?q?=20=EB=8B=A8=EA=B0=80=20=3D=20=ED=95=9C=20=EC=8B=9C=EA=B0=84=20?= =?UTF-8?q?=EB=85=B8=20=C2=B7=20=EC=9E=AC=20=C2=B7=20=EA=B2=BD)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 찾아 넣기 창 기계 줄 누름 = 서버 `/logic-lib/machine-hourly` 로 한 시간 값을 받아 고정 줄 단가로 - 일위대가 = 이음 기계 고정 줄 · 단가산출 = 더한줄 고정 줄(수량 1) — 같은 금액 - 서버 `M02_Machine_Hourly`(기계경비 로직 짝 차례 첫째 · 읽기만) · 시험 추가 Co-Authored-By: Claude Sonnet 5.5 --- M02_MasterTemplete/M02_Logic_Lib_Read.py | 7 ++ M02_MasterTemplete/M02_Machine_Hourly.py | 72 +++++++++++++++++++ .../M02_MasterTemplete_Find_Pick.ts | 36 +++++++++- .../M02_MasterTemplete_Labor_Pick.ts | 1 + .../M02_MasterTemplete_Sangeun_Pick.ts | 9 ++- resources/tester/test_721_find_pick.py | 41 ++++++++++- 6 files changed, 159 insertions(+), 7 deletions(-) create mode 100644 M02_MasterTemplete/M02_Machine_Hourly.py diff --git a/M02_MasterTemplete/M02_Logic_Lib_Read.py b/M02_MasterTemplete/M02_Logic_Lib_Read.py index ae6231d52..751134d50 100644 --- a/M02_MasterTemplete/M02_Logic_Lib_Read.py +++ b/M02_MasterTemplete/M02_Logic_Lib_Read.py @@ -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) diff --git a/M02_MasterTemplete/M02_Machine_Hourly.py b/M02_MasterTemplete/M02_Machine_Hourly.py new file mode 100644 index 000000000..71b6955b1 --- /dev/null +++ b/M02_MasterTemplete/M02_Machine_Hourly.py @@ -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} diff --git a/M02_MasterTemplete/M02_MasterTemplete_Find_Pick.ts b/M02_MasterTemplete/M02_MasterTemplete_Find_Pick.ts index 569c17244..21852349c 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Find_Pick.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Find_Pick.ts @@ -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 => { + try { + const got = await call<{ ok: boolean; reason?: string } & Record>( + `/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; diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_Pick.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_Pick.ts index f9c08a56e..98962e502 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_Pick.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_Pick.ts @@ -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, }); } diff --git a/M02_MasterTemplete/M02_MasterTemplete_Sangeun_Pick.ts b/M02_MasterTemplete/M02_MasterTemplete_Sangeun_Pick.ts index c45c64732..776eb0c34 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Sangeun_Pick.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Sangeun_Pick.ts @@ -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; 수량: 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 }), }); } diff --git a/resources/tester/test_721_find_pick.py b/resources/tester/test_721_find_pick.py index 4f8b5133a..b81649914 100644 --- a/resources/tester/test_721_find_pick.py +++ b/resources/tester/test_721_find_pick.py @@ -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" + ) From 78be3966d91139baf50e3f9192f437964122cd33 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 2 Nov 2026 18:55:16 +0900 Subject: [PATCH 4/9] =?UTF-8?q?knowledge(=EC=9A=94=EA=B5=AC):=20=EC=9A=94?= =?UTF-8?q?=EA=B5=AC=20583=20=EC=9B=90=EB=AC=B8=20=EA=B8=B0=EB=A1=9D=20?= =?UTF-8?q?=E2=80=94=20=EC=A1=B0=EC=82=AC=20=EB=81=9D=EB=82=98=EB=A9=B4=20?= =?UTF-8?q?=EA=B2=B0=EC=A0=95=20=EC=82=AC=ED=95=AD=20=ED=95=9C=20=EB=B2=88?= =?UTF-8?q?=EC=97=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015uTVHFZkNcUa1SR4cNcENM --- docs/raw/요구_1102/사용자요구_1102.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/raw/요구_1102/사용자요구_1102.md b/docs/raw/요구_1102/사용자요구_1102.md index 10362b698..00847d00c 100644 --- a/docs/raw/요구_1102/사용자요구_1102.md +++ b/docs/raw/요구_1102/사용자요구_1102.md @@ -97,3 +97,7 @@ 3. 일위대가 불러오기에서 넣은 데이터는 별도로 저장하지 않는다고 내가 요청했었음. 일위대가나 단가산출로 저장되지 않는다면 그냥 휘발성 데이터일 뿐임. 4. 경쟁사 프로그램을 REA를 통해 출력되는 정보들의 연결을 분석한자료가 기술정보 DB에 있어. 이걸 활용하면 엑셀 데이터를 우리 단가산출에 연결할때 도움이 될것같은데 조사행줘. 5. 일위대가 불러오기에서 엑셀의 내용이 열조절이 안되어 있어. 내용확인이 어렵과 열 폭 수정이 안됨. 확인 + +## 요구 583 (11-02 · 사용자) + +다음조사가 끝나는 시점에서 내가 결정할 사항을 한번에 보여줘. From afb971042380d11a9f573c76876a20cfce65438d Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 2 Nov 2026 18:56:36 +0900 Subject: [PATCH 5/9] =?UTF-8?q?feat(741-1):=20=EB=B6=88=EB=9F=AC=EC=98=A4?= =?UTF-8?q?=EA=B8=B0=20=EC=97=91=EC=85=80=20=EB=B3=B4=EA=B8=B0=20=EC=97=B4?= =?UTF-8?q?=20=ED=8F=AD=20=E2=80=94=20xlsx=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EC=97=B4=20=ED=8F=AD=20=C2=B7=20=ED=8F=AD=20=EC=97=86=EB=8A=94?= =?UTF-8?q?=20=EC=97=B4=EC=9D=80=20=EA=B8=80=20=EA=B8=B8=EC=9D=B4=EB=A1=9C?= =?UTF-8?q?=20=C2=B7=20=EB=A8=B8=EB=A6=AC=20=EA=B2=BD=EA=B3=84=20=EB=81=8C?= =?UTF-8?q?=EA=B8=B0=20=C2=B7=20=EB=91=90=20=EB=B2=88=20=EB=88=8C=EB=9F=AC?= =?UTF-8?q?=20=EB=A7=9E=EC=B6=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 서버: xlsx 시트 XML 열 폭 · 숨김을 보냄(엑셀 보기 격자 `열`) - 화면: 폭 없는 열은 글 길이로 처음 폭(최소 6 · 최대 60 · 여러 열 병합 제목 칸은 뺌) - 읽기 전용 격자도 열 폭 끌기 · 경계 두 번 누름 맞춤(보기만 · 저장 · 되돌리기 · onChange 없음) - 열 경계 오른쪽 반에서도 경계로 읽음 · 단가 카드 미리보기는 사본으로 보임 - 시험: test_741_sheet_widths · test_stage2_autofit(esbuild 묶어 node) Co-Authored-By: Claude Sonnet 5.5 --- A00_Common/spreadsheet/spreadsheet.ts | 6 +-- A00_Common/spreadsheet/spreadsheet_autofit.ts | 45 ++++++++++++----- A00_Common/spreadsheet/spreadsheet_grid.ts | 6 ++- A00_Common/spreadsheet/spreadsheet_mouse.ts | 4 +- M02_MasterTemplete/M02_Hopyo_Grid.py | 50 +++++++++++++++++-- .../M02_MasterTemplete_Hopyo_Sheet.ts | 2 + .../M02_MasterTemplete_ItemCards.ts | 5 +- .../tester/spreadsheet/test_stage2_autofit.ts | 21 +++++++- resources/tester/test_741_sheet_widths.py | 49 ++++++++++++++++++ 9 files changed, 164 insertions(+), 24 deletions(-) create mode 100644 resources/tester/test_741_sheet_widths.py diff --git a/A00_Common/spreadsheet/spreadsheet.ts b/A00_Common/spreadsheet/spreadsheet.ts index ef629b80f..4ae39fd85 100644 --- a/A00_Common/spreadsheet/spreadsheet.ts +++ b/A00_Common/spreadsheet/spreadsheet.ts @@ -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)); } /** 칸 명령이 건드린 칸(서식만 바뀐 칸도 다시 그리게) */ diff --git a/A00_Common/spreadsheet/spreadsheet_autofit.ts b/A00_Common/spreadsheet/spreadsheet_autofit.ts index aea51e494..fd65722b5 100644 --- a/A00_Common/spreadsheet/spreadsheet_autofit.ts +++ b/A00_Common/spreadsheet/spreadsheet_autofit.ts @@ -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 { + const wide = new Set(); + 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(); + 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)); } /** 폭을 자동 맞춤 값으로 바꿈(열넓이 명령 하나). */ diff --git a/A00_Common/spreadsheet/spreadsheet_grid.ts b/A00_Common/spreadsheet/spreadsheet_grid.ts index 416f5dd62..08aa55635 100644 --- a/A00_Common/spreadsheet/spreadsheet_grid.ts +++ b/A00_Common/spreadsheet/spreadsheet_grid.ts @@ -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()) { diff --git a/A00_Common/spreadsheet/spreadsheet_mouse.ts b/A00_Common/spreadsheet/spreadsheet_mouse.ts index 9bff078dd..86e77b8bf 100644 --- a/A00_Common/spreadsheet/spreadsheet_mouse.ts +++ b/A00_Common/spreadsheet/spreadsheet_mouse.ts @@ -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") { // 고른 열 전체의 경계면 고른 열 모두(엑셀) diff --git a/M02_MasterTemplete/M02_Hopyo_Grid.py b/M02_MasterTemplete/M02_Hopyo_Grid.py index b41f477f2..4c63b1d4a 100644 --- a/M02_MasterTemplete/M02_Hopyo_Grid.py +++ b/M02_MasterTemplete/M02_Hopyo_Grid.py @@ -6,16 +6,19 @@ A00 산출근거 스프레드시트 Workbook(`A00_Common/spreadsheet/spreadsheet xls = xlrd `formatting_info` — 값 · 병합 · 열 폭(÷256 글자) · 행 높이(÷20 pt · 시트 기본과 다른 것만) · 서식 표(글꼴 · 굵게 · 맞춤 · 줄바꿈 · 테두리 · 채움 · 숫자 형식) · 틀고정(파일 값 · 없으면 머리 4행). -xlsx = 값만 · 넓은 시트는 머리 글(앞 4 줄) 마지막 열까지 · STmate 숨은 칸 글(「_x0005_`BDCOD…」) 뺌. +xlsx = 값 · 열 폭(``) · 넓은 시트는 머리 글(앞 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 `` 를 읽음(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} diff --git a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Sheet.ts b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Sheet.ts index f8f5b65c5..e585953e5 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Sheet.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Sheet.ts @@ -336,8 +336,10 @@ export function openHopyoSheet(opt: HopyoSheetOptions): void { async function mountSheet(grid: HopyoGrid): Promise { 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; diff --git a/M02_MasterTemplete/M02_MasterTemplete_ItemCards.ts b/M02_MasterTemplete/M02_MasterTemplete_ItemCards.ts index 8c41aceb2..d84d38233 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_ItemCards.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_ItemCards.ts @@ -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); } diff --git a/resources/tester/spreadsheet/test_stage2_autofit.ts b/resources/tester/spreadsheet/test_stage2_autofit.ts index ec62db198..4482db19f 100644 --- a/resources/tester/spreadsheet/test_stage2_autofit.ts +++ b/resources/tester/spreadsheet/test_stage2_autofit.ts @@ -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 시험 끝"); diff --git a/resources/tester/test_741_sheet_widths.py b/resources/tester/test_741_sheet_widths.py new file mode 100644 index 000000000..6c25f8d35 --- /dev/null +++ b/resources/tester/test_741_sheet_widths.py @@ -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 From f23fab697e1ce996e6bd23c3695674a239776c07 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 2 Nov 2026 19:09:04 +0900 Subject: [PATCH 6/9] =?UTF-8?q?knowledge(=EC=9A=94=EA=B5=AC):=20=EC=9A=94?= =?UTF-8?q?=EA=B5=AC=20584=20=EC=9B=90=EB=AC=B8=20=EA=B8=B0=EB=A1=9D=20?= =?UTF-8?q?=E2=80=94=20=EC=9D=BC=EC=9C=84=EB=8C=80=EA=B0=80=20=EC=95=88=20?= =?UTF-8?q?=EC=A4=84=20=EC=88=98=EB=9F=89=20=3D=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=EC=9E=90=20=EC=9E=85=EB=A0=A5=20=C2=B7=20=EB=B6=88=EB=9F=AC?= =?UTF-8?q?=EC=98=A8=20=EC=97=91=EC=85=80=20=EB=B3=B4=EA=B4=80=20=EC=95=88?= =?UTF-8?q?=20=ED=95=A8=20=C2=B7=20=EB=AF=B8=EB=A6=AC=20=EA=B3=A0=EB=A6=84?= =?UTF-8?q?=20=EB=8B=A4=EB=A5=B8=20=EC=A0=90=EB=A7=8C=20=C2=B7=20=EA=B0=95?= =?UTF-8?q?=EA=B4=80=EB=B9=84=EA=B3=84=20=EA=B7=B8=EB=8C=80=EB=A1=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015uTVHFZkNcUa1SR4cNcENM --- docs/raw/요구_1102/사용자요구_1102.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/raw/요구_1102/사용자요구_1102.md b/docs/raw/요구_1102/사용자요구_1102.md index 00847d00c..da648de3c 100644 --- a/docs/raw/요구_1102/사용자요구_1102.md +++ b/docs/raw/요구_1102/사용자요구_1102.md @@ -101,3 +101,10 @@ ## 요구 583 (11-02 · 사용자) 다음조사가 끝나는 시점에서 내가 결정할 사항을 한번에 보여줘. + +## 요구 584 (11-02 · 사용자 · 582 조사 뒤 결정 넷 답) + +1. 일위대가 안 줄 수량 → 하나의 일위대가는 결국 품셈에 있는 정보가 아님. 여러 단가산출 기초의 조합이나 요소의 조합일 확율이 높음. 일위대가 아래 하나의 단가산출 항목이라면 1 기준단위에 대한 정보(단가산출 내부 공종들의 수량이 0이란 말이 아님)가 되고 그 그값은 당연히 구조물 조합이나 프로젝트 내부에서 연결되는것이 맞음. 그렇다면 당연히 하나의 일위대가에는 얼마의 단가산출이 되는지는 사용자가 입력하는게 맞아보임. 단가산출에도 기준단위에 대한 단가와 수량이 들어갔을때의 가격을 별도로 넣어주잖아. 혹시 잘못이해한게 있으면 다시 요청 +2. 불러오기 도중 자료 → 내말은 일위대가 불러오기 엑셀자료 자체를 보관할 필요가 없으며 필요한 경우 다시 불러와서 사용해야한다는 것임. +3. 미리 고름 → 「다른 점」 줄만 +4. 강관비계 되살림 → 결국 M02의 데이터 중 시스템관리자인 내가 삭제하고 수정할 수 있음. 그러니 삭제된 내용이라면 그냥둬. From 7eb176eb0036208f6995e71df446325387dd9780 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 2 Nov 2026 19:09:05 +0900 Subject: [PATCH 7/9] =?UTF-8?q?knowledge(=EC=9A=94=EA=B5=AC):=20=EB=B6=84?= =?UTF-8?q?=EC=84=9D=20582=20=EB=91=98=20=E2=80=94=20=EC=88=98=EB=9F=89=20?= =?UTF-8?q?=C2=B7=20=ED=9C=98=EB=B0=9C=20=C2=B7=20=EB=B9=84=EA=B3=A0=20?= =?UTF-8?q?=C2=B7=20=EA=B2=BD=EC=9F=81=EC=82=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015uTVHFZkNcUa1SR4cNcENM --- docs/raw/요구_1102/_분석_582-123_수량_휘발.md | 159 ++++++++++++++ .../raw/요구_1102/_분석_582-24_비고_경쟁사.md | 207 ++++++++++++++++++ 2 files changed, 366 insertions(+) create mode 100644 docs/raw/요구_1102/_분석_582-123_수량_휘발.md create mode 100644 docs/raw/요구_1102/_분석_582-24_비고_경쟁사.md diff --git a/docs/raw/요구_1102/_분석_582-123_수량_휘발.md b/docs/raw/요구_1102/_분석_582-123_수량_휘발.md new file mode 100644 index 000000000..4b41c4295 --- /dev/null +++ b/docs/raw/요구_1102/_분석_582-123_수량_휘발.md @@ -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 — 불러오기 도중 자료(올린 엑셀 읽은 것)는?** +- (가) [끝내기] 누를 때까지 보관 — 나갔다 와도 이어서 · 끝내면 만든 일위대가 · 단가산출 · 별칭 장부만 남음 — 권함 +- (나) 불러오기 화면을 떠나면 버림 — 이어서 없음 diff --git a/docs/raw/요구_1102/_분석_582-24_비고_경쟁사.md b/docs/raw/요구_1102/_분석_582-24_비고_경쟁사.md new file mode 100644 index 000000000..39c094686 --- /dev/null +++ b/docs/raw/요구_1102/_분석_582-24_비고_경쟁사.md @@ -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 %)만 둘지. From 005640da3d2ac6101af20b6f0424a3fdaf49fdc7 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 2 Nov 2026 19:10:33 +0900 Subject: [PATCH 8/9] =?UTF-8?q?knowledge(=EC=9A=94=EA=B5=AC):=20=EC=9A=94?= =?UTF-8?q?=EA=B5=AC=20585=20=EC=9B=90=EB=AC=B8=20=EA=B8=B0=EB=A1=9D=20?= =?UTF-8?q?=E2=80=94=20=EB=B6=88=EB=9F=AC=EC=98=A4=EA=B8=B0=20=EB=A7=9E?= =?UTF-8?q?=EC=B6=A4=20=3D=20=EC=9D=BC=EC=9C=84=EB=8C=80=EA=B0=80=20?= =?UTF-8?q?=ED=95=AD=EB=AA=A9=20=ED=95=98=EB=82=98=ED=95=98=EB=82=98?= =?UTF-8?q?=EB=A5=BC=20M01=20=EC=9A=94=EC=86=8C=20=C2=B7=20=EA=B8=B0?= =?UTF-8?q?=EC=B4=88=EC=99=80=20=EA=B2=AC=EC=A4=8C=20=EC=95=84=EC=9D=B4?= =?UTF-8?q?=EB=94=94=EC=96=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015uTVHFZkNcUa1SR4cNcENM --- docs/raw/요구_1102/사용자요구_1102.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/raw/요구_1102/사용자요구_1102.md b/docs/raw/요구_1102/사용자요구_1102.md index da648de3c..b365fd990 100644 --- a/docs/raw/요구_1102/사용자요구_1102.md +++ b/docs/raw/요구_1102/사용자요구_1102.md @@ -108,3 +108,8 @@ 2. 불러오기 도중 자료 → 내말은 일위대가 불러오기 엑셀자료 자체를 보관할 필요가 없으며 필요한 경우 다시 불러와서 사용해야한다는 것임. 3. 미리 고름 → 「다른 점」 줄만 4. 강관비계 되살림 → 결국 M02의 데이터 중 시스템관리자인 내가 삭제하고 수정할 수 있음. 그러니 삭제된 내용이라면 그냥둬. + +## 요구 585 (11-02 · 사용자 · 아이디어 검토) + +또하나 일위대가 가져오기의 매칭과 관련하여 아이디어 검토 +- 내가 생각했을때의 경쟁사 프로그램으로 출력한 엑셀파일에서 우리가 일위대가를 단순히 하나의 로직 그룹으로 보고 접근하게 문제가 되지 않나 생각되서 말이야. 우리의 기초로직은 M01에서 요소들과 단가산출 기초로 된걸 일위대가의 항목 하나하나와 비교하면 더 찾기 쉬운게 아닌가라는 생각이 듬. From 2fb39bd50ffb735014f281802a134780f36a2d7d Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 2 Nov 2026 19:29:51 +0900 Subject: [PATCH 9/9] =?UTF-8?q?knowledge(=EC=9A=94=EA=B5=AC):=20=EB=B6=84?= =?UTF-8?q?=EC=84=9D=20585=20=E2=80=94=20=EB=B6=88=EB=9F=AC=EC=98=A4?= =?UTF-8?q?=EA=B8=B0=20=ED=95=AD=EB=AA=A9=EB=B3=84=20=EB=A7=9E=EC=B6=A4=20?= =?UTF-8?q?=EA=B2=80=ED=86=A0(=EC=A7=80=EA=B8=88=EB=8F=84=20=EC=A4=84?= =?UTF-8?q?=EB=A7=88=EB=8B=A4=20=C2=B7=20=EC=9A=94=EC=86=8C=EB=A1=9C=20?= =?UTF-8?q?=EA=B8=B0=EC=B4=88=20=EA=B1=B0=EA=BE=B8=EB=A1=9C=20=EC=B0=BE?= =?UTF-8?q?=EA=B8=B0=EB=8A=94=20=EB=82=98=EB=B9=A0=EC=A7=90=20=C2=B7=20?= =?UTF-8?q?=ED=9B=84=EB=B3=B4=20=EB=B8=94=EB=A1=9D=20=EC=A4=84=20=EB=B9=84?= =?UTF-8?q?=EA=B5=90=20=EB=84=93=ED=9E=98=20=EA=B6=8C=ED=95=A8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015uTVHFZkNcUa1SR4cNcENM --- docs/raw/요구_1102/_분석_585_항목별맞춤.md | 192 +++++++++++++++++++++ 1 file changed, 192 insertions(+) create mode 100644 docs/raw/요구_1102/_분석_585_항목별맞춤.md diff --git a/docs/raw/요구_1102/_분석_585_항목별맞춤.md b/docs/raw/요구_1102/_분석_585_항목별맞춤.md new file mode 100644 index 000000000..3187baf54 --- /dev/null +++ b/docs/raw/요구_1102/_분석_585_항목별맞춤.md @@ -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 줄 늘어남)