From 475ad7875e30039311210d168611fc8129011263 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 14 Sep 2026 19:44:18 +0900 Subject: [PATCH] =?UTF-8?q?feat(b08=C2=B7b05=C2=B7b09):=2010-A=20=EC=9A=B0?= =?UTF-8?q?=EB=A6=AC=EA=B0=80=20=EC=A0=95=ED=95=9C=20SW=20=EA=B7=9C?= =?UTF-8?q?=EC=B9=99=EC=9D=84=20=ED=99=94=EB=A9=B4=EC=97=90=20=EB=93=9C?= =?UTF-8?q?=EB=9F=AC=EB=83=84=20=E2=80=94=20=ED=9B=91=EA=B8=B0=2021?= =?UTF-8?q?=EA=B1=B4=20=EC=A4=91=20=EC=95=88=20=EB=B3=B4=EC=9D=B4=EA=B1=B0?= =?UTF-8?q?=EB=82=98=20=EB=B0=98=EB=A7=8C=20=EB=B3=B4=EC=9D=B4=EB=8D=98=20?= =?UTF-8?q?13=EA=B1=B4=20=EC=B1=84=EC=9B=80=20+=209-21=20=EC=A0=9C?= =?UTF-8?q?=EA=B7=BC=20=EA=B5=B4=EC=B0=A9=EA=B8=B0=20=ED=81=AC=EA=B8=B0=20?= =?UTF-8?q?=EC=B9=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 문구 못박음 셋: ① 옹벽 높이 칸 밑 「기본값 · 소광리 도면 H=2.0 · 바꿀 수 있음」(등록부 default_basis 를 모든 칸 밑 회색 한 줄로) · ② 일반관리비 「임도가 어느 쪽인지 규정이 없어 (주)공사 기본 · 칸에서 바꿀 수 있음」 · ③ 모르타르 배합 「산림품셈에 배합 절이 없어 건설품셈 [건축] 9-1-1 을 씀(교차 참조)」 - B08: 막자갈 비고 「칸을 새로 두는 것은 B05 등록부 몫」 · 일위대가 머리 5단 한도 · 야면석 계수 칸 밑 「돌 종류는 그대로」 · 식 저장 전 상태 줄 「제원을 바꿔도 따라감」 · 양식 장 머리 「반올림은 m당 값에 걸고 수량 = m당 × 연장」 - 산출 조건 제근 굴착기 크기 칸(임목축적 등급 밑 · 「안 정함」 · 회색 제안 0.7㎥ + 근거 + [제안값 넣기] 누른 때만) — 936be972 소림 + 0.7 → 뿌리뽑기 55원/㎡ × 17,873.80㎡ = 983,057원(되돌림) - 라이브러리 이름표: [내 라이브러리에 저장]·[발행] 창이 장 이름(종류 + 제원 요약)을 채워 묻고 고친 이름으로 저장 · 취소면 안 씀 · 비우면 양식 이름 - B05 계곡 통과 시설 폼 머리 「[저장]은 이 폼에 있는 칸만 바꿈 — 폼에 없는 칸은 그대로 둠」 · B09 폐기물처리비 자리 칸 밑 「법정경비 밑수에는 안 넣음」 · 내역서 「금액을 못 세운 줄」 첫 줄 「사유는 아래 단계가 낸 것을 그대로 옮김」 - 남은 하나(표 형태 66표 까닭 form_basis 화면)는 자원 축 파일을 거쳐야 해 브레인 물음 Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_016VBGFXB9AbJBwXP19z75Qq --- B05_Profile/B05_Profile_Api_Structures.ts | 2 + B05_Profile/B05_Profile_Structure_Types.json | 2 +- .../B05_Profile_UI_Drainage_Facility.ts | 7 +++ .../B05_Profile_UI_Structures_Fields.ts | 15 ++++- .../B05_Profile_UI_Structures_Panel.ts | 2 +- .../B05_Profile_UI_Style_Structures.css | 12 ++++ .../B08_Quantity_Engine_StructureLibrary.py | 5 ++ ...08_Quantity_Engine_UnitQuantity_Masonry.py | 2 +- .../B08_Quantity_Router_StructureSheet.py | 9 ++- B08_Quantity/B08_Quantity_UI_EarthworkGrid.ts | 7 +++ B08_Quantity/B08_Quantity_UI_Page.ts | 11 ++++ .../B08_Quantity_UI_Side_RootRemoval.ts | 59 +++++++++++++++++++ .../B08_Quantity_UI_StructureSheet.ts | 10 +++- .../B08_Quantity_UI_StructureSheet_Formula.ts | 2 +- .../B08_Quantity_UI_StructureSheet_Library.ts | 30 +++++----- .../B08_Quantity_UI_StructureSheet_Spec.ts | 6 +- ...08_Quantity_UI_StructureSheet_UnitPrice.ts | 2 +- B09_Estimation/B09_Estimation_CostSheet.py | 6 +- B09_Estimation/B09_Estimation_UI_Tab_Bill.ts | 2 + B09_Estimation/B09_Estimation_WorkItems_AX.py | 5 +- resources/tester/test_b08_library_name_tag.py | 56 ++++++++++++++++++ .../tester/test_b10a_rules_visible_b05_b09.py | 59 +++++++++++++++++++ .../tester/test_b10a_rules_visible_b08.py | 46 +++++++++++++++ ui_template/ui_template_locale_b2.ts | 4 ++ ui_template/ui_template_locale_b3.ts | 4 ++ 25 files changed, 334 insertions(+), 31 deletions(-) create mode 100644 B08_Quantity/B08_Quantity_UI_Side_RootRemoval.ts create mode 100644 resources/tester/test_b08_library_name_tag.py create mode 100644 resources/tester/test_b10a_rules_visible_b05_b09.py create mode 100644 resources/tester/test_b10a_rules_visible_b08.py diff --git a/B05_Profile/B05_Profile_Api_Structures.ts b/B05_Profile/B05_Profile_Api_Structures.ts index 28951438..d87ba88f 100644 --- a/B05_Profile/B05_Profile_Api_Structures.ts +++ b/B05_Profile/B05_Profile_Api_Structures.ts @@ -40,6 +40,8 @@ export interface StructureOptionField { warn_message?: string | null; /** 원단위 표가 아직 안 읽는 칸 — 칸 이름 옆 「표에 안 쓰임」 + 툴팁 사유. */ not_in_table?: string | null; + /** 기본값의 뜻(도메인 확정값이 아닐 때) — 칸 밑 근거 한 줄. */ + default_basis?: string | null; } /** 구조물 배치 폼을 어느 화면이 쓰는가 — B05 는 유무·종류·위치만, **B06/B07 은 상세 diff --git a/B05_Profile/B05_Profile_Structure_Types.json b/B05_Profile/B05_Profile_Structure_Types.json index a8a2a37b..9a246491 100644 --- a/B05_Profile/B05_Profile_Structure_Types.json +++ b/B05_Profile/B05_Profile_Structure_Types.json @@ -727,7 +727,7 @@ "input": "number", "unit": "m", "default": 2.0, - "default_basis": "제안값 2.0 — 관측 원단위 자료가 반중력식 H=2.0 한 벌뿐이라 그 값(2026-09-14 A4 · 옛 2.5 는 자료 없음)", + "default_basis": "기본값 · 소광리 도면 H=2.0 · 바꿀 수 있음", "required": false, "phase": "b05", "empty_means": "비워도 놓임 — 줄만 서고 미확정이라 금액에 안 들어감(높이를 적으면 섬 · 2026-09-14 브레인 판정 ①)" diff --git a/B05_Profile/B05_Profile_UI_Drainage_Facility.ts b/B05_Profile/B05_Profile_UI_Drainage_Facility.ts index a4f121fc..1716a776 100644 --- a/B05_Profile/B05_Profile_UI_Drainage_Facility.ts +++ b/B05_Profile/B05_Profile_UI_Drainage_Facility.ts @@ -361,10 +361,17 @@ export function createFacilityOptionsForm( emit(); }); + // 10-A ⑲ 저장 규칙을 폼 머리에 드러냄 — 병합은 `_Drainage_Facility_Merge`(2026-09-14 브레인 판정). + const saveNote = document.createElement("p"); + saveNote.className = "b05-structure__owner-note"; + saveNote.textContent = + "[저장]은 이 폼에 있는 칸만 바꿈 — 폼에 없는 칸(집계표·구조물도로 적은 값)은 그대로 둠 · 시설 종류를 바꾸면 새로 씀"; + // 세월교·물넘이 항목은 **관종·관경 바로 다음**에 둔다(2026-08-30 사용자 지시 2) — // 월류 폭·높이 → 바닥 경사 → 수량 → 개략 단면 결과. 다른 시설에서는 전부 숨는다. root.append( grid(suggest), + saveNote, pipeRow, wingRow, ford.widthRow, diff --git a/B05_Profile/B05_Profile_UI_Structures_Fields.ts b/B05_Profile/B05_Profile_UI_Structures_Fields.ts index 00a0104b..bcba79c8 100644 --- a/B05_Profile/B05_Profile_UI_Structures_Fields.ts +++ b/B05_Profile/B05_Profile_UI_Structures_Fields.ts @@ -9,12 +9,23 @@ import { chainageToStation, stationToChainage } from "./B05_Profile_Util_Station"; -export function field(labelText: string, input: HTMLElement): HTMLLabelElement { +export function field( + labelText: string, + input: HTMLElement, + basis?: string | null, +): HTMLLabelElement { const wrapper = document.createElement("label"); wrapper.className = "b05-route__field"; const caption = document.createElement("span"); caption.textContent = labelText; wrapper.append(caption, input); + // 칸 밑 근거 한 줄 — 등록부 `default_basis`(기본값의 뜻 · 10-A 2026-09-14 사용자 확정). + if (basis) { + const note = document.createElement("small"); + note.className = "b05-structure__basis"; + note.textContent = basis; + wrapper.append(note); + } return wrapper; } @@ -45,6 +56,8 @@ interface OptionShape { warn_message?: string | null; /** 원단위 표가 아직 안 읽는 칸 — 툴팁이 이 사유를 먼저 보임. */ not_in_table?: string | null; + /** 기본값의 뜻 — 칸 밑 근거 한 줄. */ + default_basis?: string | null; } /** diff --git a/B05_Profile/B05_Profile_UI_Structures_Panel.ts b/B05_Profile/B05_Profile_UI_Structures_Panel.ts index 92f25339..7e393dca 100644 --- a/B05_Profile/B05_Profile_UI_Structures_Panel.ts +++ b/B05_Profile/B05_Profile_UI_Structures_Panel.ts @@ -332,7 +332,7 @@ export function createStructuresSection( input.classList.add("is-locked"); input.title = "지금은 고를 수 없는 항목입니다."; } - optionRow.append(field(label, input)); + optionRow.append(field(label, input, option.default_basis)); input.addEventListener("change", () => liveCommit()); optionInputs.push({ key: option.key, diff --git a/B05_Profile/B05_Profile_UI_Style_Structures.css b/B05_Profile/B05_Profile_UI_Style_Structures.css index 6ddb3807..1bf7a509 100644 --- a/B05_Profile/B05_Profile_UI_Style_Structures.css +++ b/B05_Profile/B05_Profile_UI_Style_Structures.css @@ -214,6 +214,18 @@ line-height: 1.5; } +/* 칸 밑 근거 한 줄 — 등록부 `default_basis`(10-A). 회색 작은 글씨. */ +.b05-structure__basis { + color: var(--color-text-muted, #9aa1ad); + font-size: var(--text-caption, 12px); + line-height: 1.3; +} + +/* 근거 줄로 옆 칸이 길어져도 [제안값 넣기]가 세로로 늘지 않게. */ +.b05-structure__suggest { + align-self: start; +} + /* 시작·기준·종료 측점 = 3행. 한 행은 [라벨][측점][+거리] 가로 배치 * (2026-08-17 사용자 지시 2). */ .b05-structure__position-row { diff --git a/B08_Quantity/B08_Quantity_Engine_StructureLibrary.py b/B08_Quantity/B08_Quantity_Engine_StructureLibrary.py index ff5f1979..433fd3bb 100644 --- a/B08_Quantity/B08_Quantity_Engine_StructureLibrary.py +++ b/B08_Quantity/B08_Quantity_Engine_StructureLibrary.py @@ -180,9 +180,12 @@ def save_personal( overrides: dict[str, Any] | None, unit_price_rows: list[dict[str, Any]] | None = None, tier: str = "personal", + name: str | None = None, ) -> str: """[내 라이브러리에 저장]·발행 — 양식 + 고친 식·줄 조합을 **그 단에 한 벌**로 씀. 코드. + ⚠ `name` — 이름표(화면이 「종류 + 제원 요약」을 제안 · 사용자가 고침 · 10-A ⑫). 비면 양식 이름. + ⚠ 반대 방향(작업본 → 라이브러리)이라 프로젝트는 안 바꿈(브레인 판정 Ⓑ). ⚠ 같은 종류가 있으면 **그 코드로 덮어씀** — 지금은 종류당 하나(판정 Ⓐ). ⚠ `tier` — 회사(MASTER)·프로그램 기본(SYSTEM_ADMIN) 발행도 같은 모양(2026-09-14 브레인 승인). @@ -199,6 +202,8 @@ def save_personal( for row in overridden_rows(template, overrides) ] item = {k: v for k, v in template.items() if k != "imported_from"} + if name and name.strip(): + item["name"] = name.strip() if tier == "program" and isinstance(item.get("origin"), dict): # 모든 회사로 가는 발행본 — 원문 공사명·파일명은 안 실음(원본·회사 단은 그대로 · 브레인 ②③). kept = {k: v for k, v in item["origin"].items() if k not in ORIGIN_MASKED_KEYS} diff --git a/B08_Quantity/B08_Quantity_Engine_UnitQuantity_Masonry.py b/B08_Quantity/B08_Quantity_Engine_UnitQuantity_Masonry.py index 83157d30..af71e507 100644 --- a/B08_Quantity/B08_Quantity_Engine_UnitQuantity_Masonry.py +++ b/B08_Quantity/B08_Quantity_Engine_UnitQuantity_Masonry.py @@ -422,7 +422,7 @@ def stone_masonry( notes.append( f"막자갈 뒷채움 폭이 정본(`04.구조도(기슭막이).xls`) 값 상 {backfill_top:g} · " f"하 {backfill_bottom:g}m 붙박이입니다 — 구조물 제원에 뒷채움 폭 칸이 없습니다" - "(소광리는 같은 식에 0.30/0.60 을 씁니다)" + "(소광리는 같은 식에 0.30/0.60 을 씁니다) · 칸을 새로 두는 것은 B05 등록부 몫" ) # ⚠ 근거 구간 밖은 **값이 서되 그 사실이 보여야 한다** — 값은 계속 나오므로 사유가 # 없으면 아무도 못 본다(2026-09-09 그물 침). diff --git a/B08_Quantity/B08_Quantity_Router_StructureSheet.py b/B08_Quantity/B08_Quantity_Router_StructureSheet.py index 0a66b219..69e834ba 100644 --- a/B08_Quantity/B08_Quantity_Router_StructureSheet.py +++ b/B08_Quantity/B08_Quantity_Router_StructureSheet.py @@ -417,6 +417,7 @@ class LibrarySaveRequest(BaseModel): sheet_key: str tier: Literal["personal", "company", "program"] = "personal" + name: str | None = Field(default=None, max_length=200) # 이름표(10-A ⑫) · 비면 양식 이름 @router.put("/{project_id}/quantity/structure-sheets/library/personal") @@ -430,10 +431,7 @@ async def put_structure_library_personal( ⚠ 프로젝트 작업본은 안 바꿈 — 반대 방향(작업본 → 개인 단)이라(판정 Ⓑ). """ - from B08_Quantity.B08_Quantity_Engine_StructureLibrary import ( - project_templates, - save_personal, - ) + from B08_Quantity.B08_Quantity_Engine_StructureLibrary import project_templates, save_personal from B08_Quantity.B08_Quantity_Engine_StructureTemplate import OVERRIDES_KEY, template_of from B08_Quantity.B08_Quantity_Engine_StructureUnitPrice import ROWS_KEY from common_util.common_util_project_settings import quantity_settings @@ -461,7 +459,8 @@ async def put_structure_library_personal( overrides = (settings.get(OVERRIDES_KEY) or {}).get(type_id) # ⛔ 수동 단가(`MANUAL_KEY`)는 안 넘김 — 프로젝트의 값(브레인 판정). rows = (settings.get(ROWS_KEY) or {}).get(type_id) - code = await asyncio.to_thread(save_personal, folder, template, overrides, rows, payload.tier) + tier, name = payload.tier, payload.name + code = await asyncio.to_thread(save_personal, folder, template, overrides, rows, tier, name) return JSONResponse(content={"status": "success", "code": code, "edited": len(overrides or {})}) diff --git a/B08_Quantity/B08_Quantity_UI_EarthworkGrid.ts b/B08_Quantity/B08_Quantity_UI_EarthworkGrid.ts index 0f0c20d1..173c3c33 100644 --- a/B08_Quantity/B08_Quantity_UI_EarthworkGrid.ts +++ b/B08_Quantity/B08_Quantity_UI_EarthworkGrid.ts @@ -100,6 +100,8 @@ export interface QuantitySettings { ancillary_counts?: Record; /** 임목축적 등급 — `"소림"`·`"중림"`·`"밀림"`(품셈 9-21 [주]①). 본수가 아니라 축적이다. */ stand_volume_class?: string | null; + /** 제근 굴착기 크기 — `"0.2"`·`"0.7"`(서버 선택지 안) · 비면 「안 정함」. */ + root_removal_excavator_m3?: string | null; /** 면고르기 갈래(서버 선택지 안) · 면적 덮어쓰기(㎡, 없음 = 파종 면적). */ face_dressing_cut_class?: string | null; face_dressing_fill_class?: string | null; @@ -162,6 +164,11 @@ export interface EarthworkTable { conversion_factor_choices?: Record; /** 면고르기 갈래 선택지 — 품셈 9-19-1 원문 표 두 벌(서버 한 곳). 화면은 그대로 보임. */ face_dressing_choices?: { cut: string[]; fill: string[] }; + /** 제근 굴착기 크기 선택지·제안(회색 · [제안값 넣기]) — 서버 한 곳. */ + root_removal_excavator_choices?: { + choices: string[]; + suggested?: { value: string; basis: string }; + }; /** 품셈 암종별 범위(안내용). 정의처가 서버라 내려받아 보인다. */ conversion_factor_pumsem_ranges?: PumsemRange[]; /** 도쟈 한계거리 — 지금 값·기본값·근거(서버가 정본). 종무대 20 m 는 규정이라 값만 보인다. */ diff --git a/B08_Quantity/B08_Quantity_UI_Page.ts b/B08_Quantity/B08_Quantity_UI_Page.ts index cbbde4fd..b24cfe04 100644 --- a/B08_Quantity/B08_Quantity_UI_Page.ts +++ b/B08_Quantity/B08_Quantity_UI_Page.ts @@ -42,6 +42,7 @@ import { renderStructureSheets } from "./B08_Quantity_UI_StructureSheet"; import { renderStructureSummary } from "./B08_Quantity_UI_StructureSummary"; import { appendTreeWasteFields } from "./B08_Quantity_UI_Side_TreeWaste"; import { appendFaceDressingFields } from "./B08_Quantity_UI_Side_FaceDressing"; +import { appendRootRemovalFields } from "./B08_Quantity_UI_Side_RootRemoval"; import { appendBenchCutFields } from "./B08_Quantity_UI_Side_BenchCut"; /** locale 헬퍼 */ @@ -109,6 +110,7 @@ async function saveQuantitySettings(projectId: string, draft: DraftSettings): Pr // `""` 는 기본(노면 + 절토)으로 되돌림 — 서버가 None 으로 둔다. topsoil_target: draft.topsoil_target, stand_volume_class: draft.stand_volume_class, + root_removal_excavator_m3: draft.root_removal_excavator_m3, // 면고르기 — 갈래 `""`·면적 `null` 도 그대로(「안 정함」·「파종 면적 그대로」로 되돌리는 길). face_dressing_cut_class: draft.face_dressing_cut_class, face_dressing_fill_class: draft.face_dressing_fill_class, @@ -307,6 +309,8 @@ interface DraftSettings { ancillary_counts: Record; // 임목축적 등급 — "소림"·"중림"·"밀림". ⚠ 본수가 아니라 축적이다(품셈 9-21 [주]①). stand_volume_class: string; + // 제근 굴착기 크기 — "0.2"·"0.7"(서버 선택지) · `""` 는 「안 정함」. 칸은 `_Side_RootRemoval`. + root_removal_excavator_m3: string; // 면고르기 — 갈래 둘(서버 선택지) · 면적 덮어쓰기 둘(`null` = 파종 면적). 칸은 `_Side_FaceDressing`. face_dressing_cut_class: string; face_dressing_fill_class: string; @@ -637,6 +641,12 @@ function buildQuantitySidePanel( ), ); panel.append(hintRow(L("B08_Quantity_Side_StandVolume_Hint"))); + appendRootRemovalFields(panel, draft, table?.root_removal_excavator_choices, { + field, + optionalNumberField, + selectField, + hintRow, + }); // ── 부대시설 개소 — ⚠ **산식으로 만들지 않는다**(확정 13). 넣어야 줄이 선다 ── panel.append(field(L("B08_Quantity_Side_Ancillary"), "")); @@ -1021,6 +1031,7 @@ export async function renderB08Quantity(root: HTMLElement): Promise { topsoil_haul_distance_m: (stored.topsoil_haul_distance_m as number | null) ?? null, topsoil_target: (stored.topsoil_target as string) ?? "", stand_volume_class: (stored.stand_volume_class as string) ?? "", + root_removal_excavator_m3: (stored.root_removal_excavator_m3 as string) ?? "", face_dressing_cut_class: (stored.face_dressing_cut_class as string) ?? "", face_dressing_fill_class: (stored.face_dressing_fill_class as string) ?? "", face_dressing_fill_area_m2: (stored.face_dressing_fill_area_m2 as number | null) ?? null, diff --git a/B08_Quantity/B08_Quantity_UI_Side_RootRemoval.ts b/B08_Quantity/B08_Quantity_UI_Side_RootRemoval.ts new file mode 100644 index 00000000..5c0eccd2 --- /dev/null +++ b/B08_Quantity/B08_Quantity_UI_Side_RootRemoval.ts @@ -0,0 +1,59 @@ +/* ============================================================================= + * B08_Quantity_UI_Side_RootRemoval.ts + * 산출 조건 「제근 굴착기 크기」 칸 — 임목축적 등급 바로 밑(PLAN 10-A · 9-21 크기 칸 서버 64a52df6). + * + * 페이지 본문(`B08_Quantity_UI_Page.ts`)이 700줄을 넘어 새 칸을 이 파일로 뺌(CLAUDE.md 4장). + * ⚠ 선택지·제안은 **서버가 내려준 그대로**(`root_removal_excavator_choices`) — 화면에 다시 적지 않음. + * ⚠ 스스로 안 고름 — 첫 보기가 「안 정함」이고 비면 뿌리뽑기 줄이 입력 사유로 섬. 제안 0.7 은 + * 회색 근거 + [제안값 넣기]를 **누른 때만**(면고르기 성토면과 같은 모양). + * ========================================================================== */ + +import { ui_locales, currentLanguageIndex } from "@ui/ui_template_locale"; +import { createButton } from "@ui/ui_template_elements"; +import type { SideFieldHelpers } from "./B08_Quantity_UI_Side_TreeWaste"; + +function L(key: keyof typeof ui_locales): string { + return ui_locales[key][currentLanguageIndex]; +} + +export interface RootRemovalChoices { + choices: string[]; + suggested?: { value: string; basis: string }; +} + +export function appendRootRemovalFields( + panel: HTMLElement, + draft: { root_removal_excavator_m3: string; dirty: boolean }, + choices: RootRemovalChoices | undefined, + h: SideFieldHelpers, +): void { + if (!choices) return; + const row = h.selectField( + L("B08_Quantity_Side_RootRemoval_Label"), + draft.root_removal_excavator_m3, + [ + { value: "", label: L("B08_Quantity_RootRemoval_Unset") }, + ...choices.choices.map((size) => ({ value: size, label: `${size}㎥` })), + ], + (picked) => { + draft.root_removal_excavator_m3 = picked; + draft.dirty = true; + }, + ); + panel.append(row); + const suggested = choices.suggested; + if (!suggested?.value) return; + panel.append( + h.hintRow(`${L("B08_Quantity_RootRemoval_Suggest")} ${suggested.value}㎥ — ${suggested.basis}`), + createButton({ + label: L("B08_Quantity_RootRemoval_FillSuggested"), + variant: "ghost", + onClick: () => { + const select = row.querySelector("select"); + if (select) select.value = suggested.value; + draft.root_removal_excavator_m3 = suggested.value; + draft.dirty = true; + }, + }), + ); +} diff --git a/B08_Quantity/B08_Quantity_UI_StructureSheet.ts b/B08_Quantity/B08_Quantity_UI_StructureSheet.ts index 4529fc4b..45306b08 100644 --- a/B08_Quantity/B08_Quantity_UI_StructureSheet.ts +++ b/B08_Quantity/B08_Quantity_UI_StructureSheet.ts @@ -262,7 +262,14 @@ function sheetBody( : "양식 없음(지금 전개)"), ), // 실무 시트 머리의 「m당」·「개소당」 — 종류마다 다름(통일하지 않음). - el("span", "b08-grid__caption", sheet.unit_label), + el( + "span", + "b08-grid__caption", + // 10-A ⑯ 양식은 L=1 로 풂(`replace_with_templates` · m당 양식만) — 규칙을 머리에 드러냄. + sheet.library_item && sheet.unit_label === "m당" + ? `m당 — 반올림은 m당 값에 걸고 수량 = m당 × 연장` + : sheet.unit_label, + ), ); // ㉱ 규격 다름 — 미확정과 같은 급(빨간 테두리 + 배지). 금액은 서고 막지 않음. if (sheet.spec_mismatch) { @@ -425,6 +432,7 @@ export function renderStructureSheets(projectId: string | null): HTMLElement { typeId: sheet.library_item.type_id, currentCode: sheet.library_item.code ?? null, originProject: sheet.library_item.origin_project, + defaultName: sheet.title, isDirty: () => dirty, confirmTake: () => { const edited = editedRows(sheet); diff --git a/B08_Quantity/B08_Quantity_UI_StructureSheet_Formula.ts b/B08_Quantity/B08_Quantity_UI_StructureSheet_Formula.ts index 2bd55cfd..3b9384e9 100644 --- a/B08_Quantity/B08_Quantity_UI_StructureSheet_Formula.ts +++ b/B08_Quantity/B08_Quantity_UI_StructureSheet_Formula.ts @@ -171,7 +171,7 @@ export function formulaTable( saveButton.disabled = !dirty; discardButton.disabled = !dirty; status.textContent = dirty - ? "저장 안 한 식 있음 — 값은 이 화면 계산(저장하면 서버가 다시 셈 · 같은 양식의 장 모두에 걸림)" + ? "저장 안 한 식 있음 — 값은 이 화면 계산(저장하면 서버가 다시 셈 · 같은 양식의 장 모두에 걸림 · 고친 식은 이 양식·프로젝트에 묶여 제원(높이 등)을 바꿔도 따라감)" : ""; onDirty(dirty); }; diff --git a/B08_Quantity/B08_Quantity_UI_StructureSheet_Library.ts b/B08_Quantity/B08_Quantity_UI_StructureSheet_Library.ts index 470c86d5..5607b487 100644 --- a/B08_Quantity/B08_Quantity_UI_StructureSheet_Library.ts +++ b/B08_Quantity/B08_Quantity_UI_StructureSheet_Library.ts @@ -18,6 +18,8 @@ const TIER_LABELS: Record = { }; /** 항목 종류 배지 — 양식형 = 제원을 바꾸면 수량이 다시 남 · 고정형 = 박힌 수량(명세 13장). */ const KIND_LABELS: Record = { form: "양식형", fixed: "고정형" }; +/** 이름표 — 「종류 + 제원 요약」을 미리 채워 두고 고치게 함(10-A ⑫ · 코드는 난수라 이름이 흔들려도 안전). */ +const NAME_TAG_HINT = "이름표(종류 + 제원 요약 · 고칠 수 있음 · 비우면 양식 이름):"; interface LibraryItem { tier: string; @@ -59,6 +61,8 @@ export interface LibraryPanelOptions { onImported: (notes: string[]) => Promise; /** 이 장 양식이 뽑아 온 원문 공사명 — 프로그램 기본 발행 확인창에 「빼고 발행」을 알림. */ originProject?: string | null; + /** 저장·발행 이름표 제안 — 장 이름(종류 + 제원 요약). */ + defaultName: string; } /** 가져오기 · [내 라이브러리에 저장] · [내 것 지우기] 칸. 개인 단 두 단추는 프로젝트를 안 바꿈. */ @@ -272,17 +276,17 @@ export function buildLibraryPanel(options: LibraryPanelOptions): HTMLElement { }; const save = personal("내 라이브러리에 저장", async () => { if (isDirty()) return "저장 안 한 식이 있음 — [식 저장] 먼저"; - if ( - !window.confirm("이 장의 양식과 고친 식을 내 라이브러리에 저장 — 같은 종류가 있으면 덮어씀") - ) { - return ""; - } + const tag = window.prompt( + `이 장의 양식과 고친 식을 내 라이브러리에 저장 — 같은 종류가 있으면 덮어씀\n${NAME_TAG_HINT}`, + options.defaultName, + ); + if (tag === null) return ""; const result = await readJson<{ edited: number }>( await fetch(libraryUrl(projectId, "/personal"), { method: "PUT", credentials: "include", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ sheet_key: sheetKey }), + body: JSON.stringify({ sheet_key: sheetKey, name: tag }), }), ); return `내 라이브러리에 저장함${result.edited ? ` · 고친 식 ${result.edited}줄 포함` : ""}`; @@ -307,19 +311,17 @@ export function buildLibraryPanel(options: LibraryPanelOptions): HTMLElement { tier === "program" && options.originProject ? `\n이 항목은 「${options.originProject}」에서 뽑은 것 — 공사명은 빼고 발행됩니다` : ""; - if ( - !window.confirm( - `이 장의 양식과 고친 식을 ${whom}에 발행 — 같은 종류가 있으면 덮어씀${masked}`, - ) - ) { - return ""; - } + const tag = window.prompt( + `이 장의 양식과 고친 식을 ${whom}에 발행 — 같은 종류가 있으면 덮어씀${masked}\n${NAME_TAG_HINT}`, + options.defaultName, + ); + if (tag === null) return ""; await readJson( await fetch(libraryUrl(projectId, "/publish"), { method: "PUT", credentials: "include", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ sheet_key: sheetKey, tier }), + body: JSON.stringify({ sheet_key: sheetKey, tier, name: tag }), }), ); return `${whom}에 발행함`; diff --git a/B08_Quantity/B08_Quantity_UI_StructureSheet_Spec.ts b/B08_Quantity/B08_Quantity_UI_StructureSheet_Spec.ts index 49398984..dcb2a633 100644 --- a/B08_Quantity/B08_Quantity_UI_StructureSheet_Spec.ts +++ b/B08_Quantity/B08_Quantity_UI_StructureSheet_Spec.ts @@ -196,7 +196,11 @@ export function buildStandardSpecPanel( slope, judgedSlope ? `비우면 자동 — 지금 판정값 1:${judgedSlope}` : "비우면 자동으로 판정합니다.", ), - field("야면석 계수", coeff, "비우면 품셈 열을 씁니다."), + field( + "야면석 계수", + coeff, + "비우면 품셈 열을 씁니다. 계수 열을 바꿔도 돌 종류는 그대로(다른 칸)입니다.", + ), field("채움 강도 (MPa)", mpa, "비우면 210. 180 은 국가기준 하한입니다."), field("버림 콘크리트", blinding, "비우면 넣습니다(두께 100㎜) — 빼려면 「안 넣음」."), ); diff --git a/B08_Quantity/B08_Quantity_UI_StructureSheet_UnitPrice.ts b/B08_Quantity/B08_Quantity_UI_StructureSheet_UnitPrice.ts index 084e14f0..2c6faec6 100644 --- a/B08_Quantity/B08_Quantity_UI_StructureSheet_UnitPrice.ts +++ b/B08_Quantity/B08_Quantity_UI_StructureSheet_UnitPrice.ts @@ -123,7 +123,7 @@ function render(table: UnitPriceTable): HTMLElement[] { const head = el( "p", "b08-sheet__head", - `일위대가 ${table.code} · ${table.unit}당 ${total} (미리보기 — 내역 금액은 원가계산이 셈)`, + `일위대가 ${table.code} · ${table.unit}당 ${total} (미리보기 — 내역 금액은 원가계산이 셈 · 하위 일위대가는 5단까지 풀고 더 깊거나 돌면 막힘)`, ); if (table.unconfirmed) { head.append(el("span", "b08-unit__badge", `미확정 ${table.unconfirmed}건`)); diff --git a/B09_Estimation/B09_Estimation_CostSheet.py b/B09_Estimation/B09_Estimation_CostSheet.py index b5299552..f2837fcc 100644 --- a/B09_Estimation/B09_Estimation_CostSheet.py +++ b/B09_Estimation/B09_Estimation_CostSheet.py @@ -64,7 +64,8 @@ FIELD_HINTS: dict[str, str] = { "자동 = 요율 데이터 적용 하한(추정금액 1억 이상)일 때 적용 · STmate 의 토목·준설·건축·기타" " 구분은 현행 제비율(2026-04-13)에서 율이 같아(2.3%) 칸으로 두지 않음" ), - "overhead_class": "임도가 (주)공사·전문공사 어느 쪽인지 정한 규정 없음 — 기본 (주)공사", + # 10-A ② 문구 못박음(2026-09-14 사용자 확정 — SW 규칙). + "overhead_class": "임도가 어느 쪽인지 규정이 없어 (주)공사 기본 · 칸에서 바꿀 수 있음", "cut_basis": ( "기본 「총공사비 1,000원 미만 버림」 — 근거: 산림청고시 제2025-82호 「금액의 단위표준」" "(설계서의 총액 · 원 · 1,000 · 미만버림) · 실무 6건이 여섯 다 000 으로 끝남 ·" @@ -99,7 +100,8 @@ FIELD_HINTS: dict[str, str] = { "waste_placement": ( "법 문언(기본): 예정가격작성기준 제19조③18호 — 폐기물처리비는 경비라 일반관리비·이윤" " 밑수에 듦 · 실무 관행: 울진소광 원가계산서(이윤 뒤·총원가 안)·임목폐기물처리 실정보고" - "(공급가액 뒤)는 승률 밖 — 금액이 크게 갈려 설계자가 고름" + "(공급가액 뒤)는 승률 밖 — 금액이 크게 갈려 설계자가 고름 ·" + " 어느 자리든 법정경비 밑수(직접공사비·노무비)에는 안 넣음" ), } _GRADE_OPTIONS = [ diff --git a/B09_Estimation/B09_Estimation_UI_Tab_Bill.ts b/B09_Estimation/B09_Estimation_UI_Tab_Bill.ts index 56458849..e43a6656 100644 --- a/B09_Estimation/B09_Estimation_UI_Tab_Bill.ts +++ b/B09_Estimation/B09_Estimation_UI_Tab_Bill.ts @@ -246,6 +246,8 @@ function drawBill(ctx: B09TabContext, bill: BillDto, reload: () => void): void { "b09s-hint b09s-hint--warn", `${L("B09_Sheet_Missing")} ${bill.summary.missing.length}${L("B09_Sheet_Count")}`, ), + // 10-A ⑳ 까닭은 그대로 올림(2026-09-14 브레인 규칙) — 규칙을 목록 머리에 드러냄. + hint(L("B09_Sheet_Missing_Passed")), ); for (const item of bill.summary.missing) { details.append(hint(`${item.name} — ${item.reason}`)); diff --git a/B09_Estimation/B09_Estimation_WorkItems_AX.py b/B09_Estimation/B09_Estimation_WorkItems_AX.py index 43f77128..dd5f9167 100644 --- a/B09_Estimation/B09_Estimation_WorkItems_AX.py +++ b/B09_Estimation/B09_Estimation_WorkItems_AX.py @@ -20,9 +20,10 @@ WORK_ITEMS: dict[str, dict[str, Any]] = { "name": "모르타르 배합", "spec": "1:3", "unit": "㎥", + # 10-A ③ 문구 못박음(2026-09-14 사용자 확정 — SW 규칙) · 교차 참조 명시(지침 2장). "basis": ( - "건설공사 표준품셈(2026) [건축부문] 9-1-1 모르타르 배합(㎥당)" - " — 산림 품셈엔 배합 절이 없음 · 배합비 1:3 은 돌쌓기 시방(시멘트:잔골재 부피비) · 실무 울진 대흥(2024)·소광(2025)" + "산림품셈에 배합 절이 없어 건설품셈 [건축] 9-1-1 을 씀(교차 참조)" + " — 건설공사 표준품셈(2026) [건축부문] 9-1-1 모르타르 배합(㎥당) · 배합비 1:3 은 돌쌓기 시방(시멘트:잔골재 부피비) · 실무 울진 대흥(2024)·소광(2025)" " 「모르타르배합 1:3」 과 같은 짜임" ), # (노임 코드, 수량, 칸) — 원문 [주]② 「배합이 포함된 것이며, 비빔은 제외」. diff --git a/resources/tester/test_b08_library_name_tag.py b/resources/tester/test_b08_library_name_tag.py new file mode 100644 index 00000000..eb2be844 --- /dev/null +++ b/resources/tester/test_b08_library_name_tag.py @@ -0,0 +1,56 @@ +"""10-A ⑫ 라이브러리 항목 이름표 = 「종류 + 제원 요약」 자동 · 사용자가 고칠 수 있음 (2026-09-14 사용자 확정). + +코드는 난수라 이름이 흔들려도 안전 — 이름은 고르개에 보이는 글자일 뿐. +""" + +from __future__ import annotations + +import json +import sys +from pathlib import Path + +import pytest +from pydantic import ValidationError + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT)) + +import B08_Quantity.B08_Quantity_Engine_StructureLibrary as library_module # noqa: E402 +import B08_Quantity.B08_Quantity_Router_StructureSheet as router_module # noqa: E402 +from B08_Quantity.B08_Quantity_Engine_StructureTemplate import load_template # noqa: E402 + + +def _saved(folder: Path, code: str) -> dict: + return json.loads((folder / f"{code}.json").read_text(encoding="utf-8")) + + +def test_저장_때_적은_이름표가_실린다(tmp_path: Path) -> None: + template = load_template("masonry_wet") + code = library_module.save_personal( + tmp_path, template, None, None, "personal", name=" 돌쌓기(찰) H=2.5 1:0.3 봉화 " + ) + assert _saved(tmp_path, code)["name"] == "돌쌓기(찰) H=2.5 1:0.3 봉화" + assert template["name"] == load_template("masonry_wet")["name"] # 원본 안 건드림 + + +def test_이름표를_비우면_양식_이름_그대로(tmp_path: Path) -> None: + template = load_template("masonry_wet") + code = library_module.save_personal(tmp_path, template, None, None, "personal", name=" ") + assert _saved(tmp_path, code)["name"] == template["name"] + + +def test_요청은_이름표를_받고_길이를_막는다() -> None: + request = router_module.LibrarySaveRequest.model_validate({"sheet_key": "k", "name": "가"}) + assert request.name == "가" + with pytest.raises(ValidationError): + router_module.LibrarySaveRequest.model_validate({"sheet_key": "k", "name": "가" * 201}) + source = (ROOT / "B08_Quantity" / "B08_Quantity_Router_StructureSheet.py").read_text("utf-8") + assert "payload.name" in source + + +def test_저장_발행_창이_종류_제원_요약을_이름표로_제안한다() -> None: + ui = (ROOT / "B08_Quantity" / "B08_Quantity_UI_StructureSheet_Library.ts").read_text("utf-8") + assert ui.count("window.prompt(") == 2 # [내 라이브러리에 저장] · [발행] + assert "options.defaultName" in ui and "name: tag" in ui + sheet_ui = (ROOT / "B08_Quantity" / "B08_Quantity_UI_StructureSheet.ts").read_text("utf-8") + assert "defaultName: sheet.title" in sheet_ui diff --git a/resources/tester/test_b10a_rules_visible_b05_b09.py b/resources/tester/test_b10a_rules_visible_b05_b09.py new file mode 100644 index 00000000..62abd427 --- /dev/null +++ b/resources/tester/test_b10a_rules_visible_b05_b09.py @@ -0,0 +1,59 @@ +"""10-A — 우리가 정한 SW 규칙을 화면에 드러내기(B05·B09 몫) · 2026-09-14 사용자 확정 · PLAN 12장. + +번호는 PLAN 10장 「✅ 사용자 확정 — SW 규칙」 줄 차례. 문구 못박을 셋(①②③)은 글자 그대로 봄. +""" + +import json +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] + + +def _read(path: str) -> str: + return (ROOT / path).read_text(encoding="utf-8") + + +def test_11_일반관리비_문구_못박음() -> None: + from B09_Estimation.B09_Estimation_CostSheet import FIELD_HINTS + + assert FIELD_HINTS["overhead_class"] == ( + "임도가 어느 쪽인지 규정이 없어 (주)공사 기본 · 칸에서 바꿀 수 있음" + ) + + +def test_13_모르타르_배합_교차_참조_문구() -> None: + from B09_Estimation.B09_Estimation_WorkItems_AX import MORTAR_MIX, WORK_ITEMS + + assert WORK_ITEMS[MORTAR_MIX]["basis"].startswith( + "산림품셈에 배합 절이 없어 건설품셈 [건축] 9-1-1 을 씀(교차 참조)" + ) + + +def test_15_폐기물처리비는_법정경비_밑수에_안_넣음() -> None: + from B09_Estimation.B09_Estimation_CostSheet import FIELD_HINTS + + assert "법정경비 밑수(직접공사비·노무비)에는 안 넣음" in FIELD_HINTS["waste_placement"] + + +def test_17_옹벽_높이_기본값_문구와_칸_밑_근거() -> None: + types = json.loads(_read("B05_Profile/B05_Profile_Structure_Types.json")) + wall = next(t for t in types["types"] if t["type_id"] == "retaining_wall") + height = next(o for o in wall["options"] if o["key"] == "height_m") + assert height["default_basis"] == "기본값 · 소광리 도면 H=2.0 · 바꿀 수 있음" + # 칸 밑 근거 한 줄 — 등록부 폼이 `default_basis` 를 칸 밑에 그림 + assert "field(label, input, option.default_basis)" in _read( + "B05_Profile/B05_Profile_UI_Structures_Panel.ts" + ) + + +def test_19_시설_저장은_폼에_없는_칸을_그대로_둠() -> None: + assert "폼에 없는 칸(집계표·구조물도로 적은 값)은 그대로 둠" in _read( + "B05_Profile/B05_Profile_UI_Drainage_Facility.ts" + ) + + +def test_20_막힌_사유는_아래_단계_것을_그대로() -> None: + assert 'hint(L("B09_Sheet_Missing_Passed"))' in _read( + "B09_Estimation/B09_Estimation_UI_Tab_Bill.ts" + ) + assert "그대로 옮김 — 여기서 새로 짓지 않음" in _read("ui_template/ui_template_locale_b3.ts") diff --git a/resources/tester/test_b10a_rules_visible_b08.py b/resources/tester/test_b10a_rules_visible_b08.py new file mode 100644 index 00000000..01850fde --- /dev/null +++ b/resources/tester/test_b10a_rules_visible_b08.py @@ -0,0 +1,46 @@ +"""10-A — 우리가 정한 SW 규칙을 화면에 드러내기(B08 몫) · 2026-09-14 사용자 확정 · PLAN 12장. + +훑기(21건) 결과 안 보이거나 반만 보이던 자리만 채움 — 칸 밑 근거 한 줄 · 사유 문구 · 제안 회색. +번호는 PLAN 10장 「✅ 사용자 확정 — SW 규칙」 줄 차례(PLAN 4장 10-A 절에 목록). +""" + +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] +B08 = ROOT / "B08_Quantity" + + +def _read(name: str) -> str: + return (B08 / name).read_text(encoding="utf-8") + + +def test_3_뒷채움_폭_칸_신설은_B05_등록부_몫() -> None: + assert "칸을 새로 두는 것은 B05 등록부 몫" in _read( + "B08_Quantity_Engine_UnitQuantity_Masonry.py" + ) + + +def test_4_하위_일위대가_깊이_한도를_미리_보인다() -> None: + assert "하위 일위대가는 5단까지" in _read("B08_Quantity_UI_StructureSheet_UnitPrice.ts") + + +def test_5_계수_열과_돌_종류는_다른_축() -> None: + assert "돌 종류는 그대로" in _read("B08_Quantity_UI_StructureSheet_Spec.ts") + + +def test_9_고친_식은_양식과_프로젝트에_묶임() -> None: + assert "제원(높이 등)을 바꿔도 따라감" in _read("B08_Quantity_UI_StructureSheet_Formula.ts") + + +def test_16_양식_반올림은_m당_값에() -> None: + assert "반올림은 m당 값에 걸고" in _read("B08_Quantity_UI_StructureSheet.ts") + + +def test_22_제근_굴착기_크기_칸() -> None: + page = _read("B08_Quantity_UI_Page.ts") + side = _read("B08_Quantity_UI_Side_RootRemoval.ts") + assert "root_removal_excavator_m3: draft.root_removal_excavator_m3" in page + assert "root_removal_excavator_m3: (stored.root_removal_excavator_m3" in page + assert "appendRootRemovalFields(" in page + assert "choices.suggested" in side and "createButton(" in side + assert side.count("= suggested.value") == 2 # 단추 안에서만 넣음(비우면 「안 정함」) diff --git a/ui_template/ui_template_locale_b2.ts b/ui_template/ui_template_locale_b2.ts index c76794e1..d1dba944 100644 --- a/ui_template/ui_template_locale_b2.ts +++ b/ui_template/ui_template_locale_b2.ts @@ -693,6 +693,10 @@ export const ui_locales_b2 = { "⚠ 「육상」은 통상값이고 사용자 확정이 아닙니다(확정 3차 ④) — 품셈 9-13 의 18구분이 이 값으로 갈립니다", "⚠ “Dry” is a customary default, not a user decision — it selects one of the 18 sub-items", ], + B08_Quantity_Side_RootRemoval_Label: ["제근 굴착기 크기", "Root removal excavator size"], + B08_Quantity_RootRemoval_Unset: ["안 정함(금액 안 섬)", "Not set (no amount)"], + B08_Quantity_RootRemoval_Suggest: ["제안(비우면 안 정함):", "Suggested (blank = not set):"], + B08_Quantity_RootRemoval_FillSuggested: ["제안값 넣기", "Fill suggested"], B08_Quantity_Side_StandVolume_Label: ["임목축적 등급", "Stand volume class"], B08_Quantity_StandVolume_Unset: ["안 정함(줄이 막힘)", "Not set (row blocked)"], B08_Quantity_StandVolume_Low: ["소림(30~60㎥/㏊)", "Low (30–60 ㎥/ha)"], diff --git a/ui_template/ui_template_locale_b3.ts b/ui_template/ui_template_locale_b3.ts index b79d3809..696e7e1a 100644 --- a/ui_template/ui_template_locale_b3.ts +++ b/ui_template/ui_template_locale_b3.ts @@ -28,6 +28,10 @@ export const ui_locales_b3 = { B09_Sheet_Unconfirmed: ["미확정", "Unconfirmed"], B09_Sheet_Unpriced: ["금액에 안 들어감", "not in the amount"], B09_Sheet_Missing: ["금액을 못 세운 줄", "Rows without a price"], + B09_Sheet_Missing_Passed: [ + "사유는 아래 단계(수량·단가)가 낸 것을 그대로 옮김 — 여기서 새로 짓지 않음", + "Reasons are passed through from the quantity/price step as-is", + ], B09_Sheet_Reload: ["다시 불러오기", "Reload"], B09_Sheet_Level: ["보이는 레벨", "Show levels"], B09_Sheet_Level_All: ["모두", "All"],