From 9e8a1c1f63a605e72dab66506890f080e92181da Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sun, 4 Oct 2026 17:19:16 +0900 Subject: [PATCH 01/14] auto: 2026-10-04 17:19 (ESD_LAPTOP) --- .../raw/M02_구조물조합개편/사용자요구_1004.md | 50 ++++++++ docs/raw/M02_구조물조합개편/설계_정함.md | 105 +++++++++++++++++ ...가산출_흐름도_조합다이어그램_마스터요소.md | 108 ++++++++++++++++++ 3 files changed, 263 insertions(+) create mode 100644 docs/raw/M02_구조물조합개편/사용자요구_1004.md create mode 100644 docs/raw/M02_구조물조합개편/설계_정함.md create mode 100644 docs/raw/plans/2026-10-04_plan_단가산출_흐름도_조합다이어그램_마스터요소.md diff --git a/docs/raw/M02_구조물조합개편/사용자요구_1004.md b/docs/raw/M02_구조물조합개편/사용자요구_1004.md new file mode 100644 index 000000000..936caa071 --- /dev/null +++ b/docs/raw/M02_구조물조합개편/사용자요구_1004.md @@ -0,0 +1,50 @@ +# M02 구조물 조합 개편 — 사용자 요구 (2026-10-04 · 원문 그대로) + +1번(계획서 32장 이후 정리)은 브레인이 처리함(c8269a969). 7 · 8번은 원문에 없음. + +## 2. 일위대가 조합 → 구조물 조합 하위 + +M02페이지의 마스터 템플릿 페이지에서 일위대가 조합은 구조물 조합의 하위 컨테이너로 넣을 것.(구조물 도면과 면적산출 사이에 배치) +- 일위대가 조합의 좌측 패널의 기존 보기 방식인 트리구조는 필요없으며 구조물 도면 처럼 리스트형태로 보임 +- 추가로 구조물 도면, 일위대가, 면적산출의 최상위에는 전체항목을 넣어주고 우측 상세에는 리스트로 보여줌, M01 항목을 보면 양식화되어 있는 페이지 헤이아웃 있음 + +## 3. 이름 + +이동한 일위대가 조합은 일위대가로 명칭 변경 + +## 4. 구분선 + +구조물 조합 컨테이너 내부의 서브 컨테이너 항목과 트리구조 사이에 수평 구분선 추가 + +## 5. 사용자별 구조물 조합 읽기 / 쓰기 / 복사 권한 점검 및 개선 + +- 시스템 관리자 : 아이슬로 프로그램 관리자로 마스터를 제공하여 회사관리자나 일반 사용자가 사용하기 쉽게 한다. +- 시스템관리자가 대시보드의 컨테이너 제목의 우측의 회사명을 선택함에 따라 변경한 목적은 해당 회사의 정보를 접근하고나 AS을 목적으로 고객을 도와주기 위함임. 복사항목에는 마스터나 시스템 관리자 회사, 개인으로 들어갈 항목 뿐만 아니라 고객 회사의 특정 폴더로 복사할 수 있는 기능이 있어야함. 수정/생성권한 포함 +- 회사관리자 : 고객으로 본인회사의 양식을 만들거나 개인 프로젝트에 사용할 개인 마스터데이터 관리 +- 회사관리자는 개인버튼을 누를경우 최상위 트리에는 다른 개인들의 정보를 보고 복사하여 개인에게 복사하거나 수정, 생성도 가능해야함. +- 개인은 마스터와 회사는 읽기만, 다른 개인의 정보는 볼수 없음. 트리는 현재를 유지해야함. + +## 6. 구조물 조합 항목 변경 + +- 최상위 구조물 조합에 구조물 도면, 일위대가, 면적산출로만 표현(기존의 재료산출의 사용자가 추가하는 재료들과 값은 엑셀을 사용하지 않고 일위대가의 담은로직 처럼 하위에 별도의 컨테이너로 추가되는 자재항목을 넣어야함. 이때 M01자재목록에서 하나씩 가져오되 수량은 면적산출의 알약으로 값을 싱크하거나 별도의 모달창에서 셀을 지정하게 구현) +- 재료산출 항목은 삭제 +- 선택입력 항목은 삭제(구조물 조합에서는 각 옵션의 상세 구조물에 따라 미리 옵션을 다 지정할 생각임으로 그럴필요 없음) +- 면적산출의 미리보기에서 아래 알약 그룹 행을 하이드/쇼 되게 반영(기본값은 하이드), 단, 면적산출 상세에서는 알약 행 보이기가 기본값으로 +- 면적산출 상세 화면의 우측 패널에서 현재의 일위대가 조합 목록을 검색하는 기능이 아니라 현재 구조물도의 일위대가 담은 로직과 추가될 사용자 지정 재료를 보여줄것, +- 일위대가 상세화면에서는 미리보기(보기만/저장안함) 컨테이너는 각 단가산출 로직에 입력이 필요한 정보들임. 구조물별로 하나씩만들어져야함으로 구조물의 일위대가 항목에 옵션값이 저장되야함. 그리고 이값에 대한 지정은 담은 로직 컨테이너의 항목을 누르면 모달창이 나오고 사용자가 입력 및 변경할수 있게함. (하위에 지정된 항목과 값을 작은 글씨로 표현되게 반영) +- 담은 로직에는 기준 단가를 기준으로 넣었을때 나온 값과 수량산출값을 넣었을때 값에 대한 노무비, 재료비, 경비, 합계를 같이 보여줌. 이때 수량산출은 면적산출의 특정 셀값이나 사용자가 수기로 입력한 값이 될수 있음. 이렇게 되면 미리보기 계산 컨테이너 필요 없음. +- 일위대가의 상세 화면의 순서는 담은로직 > 자재목록 > 다이어그램 순서임. +- 면적산출 산출 상세의 우측 패널에 보여져야하는 결과는 결국 일위대가의 담은 로직컨테이너와 자재목록임. +- 추가된 자재목록에도 기준에 대한 단가와, 면적산출 엑셀의 특정 셀과 연결되었을때 나온 단가가 같이 나와야함. + +## 9. 오른쪽 클릭 + +프로그램 B와 M영역의 페이지들에 마우스 오른쪽 클릭을 할 경우 브라우저 옵션 안보이게 할것.(우리 프로그램의 옵션은 허용, 공통양식 사용) + +## 사용자 답 (10-4 · 일위대가 · 원문 그대로) + +일위대가의 경우 +1. 구조물 조합을 복사한 경우 신규 키를 생성하여 만듬. (값과 로직은 일단 같이 복사) +2. 신규 구조물 조합을 생성하는 경우 미리보기 페이지에는 새로만들기와 복사생성하기를 선택하게 하고 복사 생성인 경우에는 해당 리스트를 보여주고 선택하게 보여주는 값은 개인인 경우에는 개인을, 회사관리자인경우 개인, 시스템관리자인 경우 마스터를 기본값으로 보여주고 탭을 눌러 다른 항목에서 가져올수 있는 항목들을 보여준다. +3. 다시 말해 최종적으로는 구조물 조합에 일위대가, 도면, 면적산출이 연되는게 맞으나 특정항목만 다른 값을 복사하여 가져올수 있어야함. 저장이 된 데이터라도 변경가능해야함. +4. 현재 매칭이 안된 데이터는 나중에 삭제할예정이나 마스터 데이터를 수동 생성을 위해서 필요한 데이터들임. 차라리 실무문서라는 항목을 마스터에 별도로 만들어서 나중에 한번에 지우게 할수 있게 반영. diff --git a/docs/raw/M02_구조물조합개편/설계_정함.md b/docs/raw/M02_구조물조합개편/설계_정함.md new file mode 100644 index 000000000..229b350fd --- /dev/null +++ b/docs/raw/M02_구조물조합개편/설계_정함.md @@ -0,0 +1,105 @@ +# M02 구조물 조합 개편 — 정함 (브레인 · 10-4) + +근거 = `사용자요구_1004.md` · 조사 보고 A~H(각 창 `tmp/조사_39-0X.md`). 사용자가 바꿀 수 있음. + +## 1. 자료 틀 (B · sub7) + +- 일위대가 = 구조물 열마다 `labor/<열id>.json` · 번호 `SU-######`(도번 · 면적번과 같은 뒷자리 · 층마다 셈) · 자리 = 시스템 `resources/master_template/labor/` · 회사 · 개인 `…/templates/labor/` · 새 프로젝트 씨앗도 KINDS 로 함께. +- 문서 꼴: + +```json +{ + "종류": "일위대가", "판": 1, "열": "item1", "일위대가번": "SU-000001", "단위": "개소", + "본뜬": { "층": "system", "주인": null, "키": "UA000008" }, + "담은로직": [ + { "로직": "GF000219", "메모": "", "입력값": { "형식": "메쌓기" }, "수량": null }, + { "로직": "GF000137", "메모": "", "입력값": {}, "수량": { "손": 3 } } + ], + "자재": [ { "코드": "MT000008", "이름": "치즐", "규격": "", "단위": "EA", "수량": null } ], + "계산값": { "줄": [], "자재": [], "기준합": {}, "반영합": {} } +} +``` + +- 이름 = 구조물 열 이름(따로 안 둠) · 단위 = 자기 칸 · 같은 로직 두 번 금지 · 빈 일위대가 저장 됨 · 이름 겹침 검사 없음 · 입력값 = 그 로직 입력 이름만(모르는 이름 버림 · 빈 값 = 자동값). +- 수량 = `null`(면적산출 알약 연결 따름 · 로직 알약 「이름|단위」 · 자재 알약 「자재|MT키」 · 칸 연결 정본 = 면적산출 문서 `연결` 한 곳) | `{손: 수}`(손 입력 · 손이 칸보다 앞섬). +- 금액 = 기준(결과단위 그대로 · 원/10㎡ 면 10㎡ 값 · 단위 같이 표기) · 반영 = 기준 × 수량 ÷ 배 · 돈 아닌 로직은 반영 없음 · 「원/1,000본」 배 1000(`_PER` 고침) · 자재 기준 단가 = M01 `master_material.element` 기본값. +- 계산 자리 = 기준 ③ 서버(M01 엔진 · calc 길 · 화면은 최신 응답만) · 반영 곱 ① 짝(`M02_MasterTemplete_Labor_Calc.ts` ↔ `M02_Labor_Doc.py` · 두 파일 머리에 짝 · 거울 시험 · 면적 칸 값이 바뀌면 화면이 바로 곱함) · [저장] 때 서버가 `계산값` 을 다시 적음(브라우저 값 버림 · 목록 합계용) · 상세는 늘 새로 셈. +- 길 = 문서 `GET/PUT /api/m02/layers/{층}/templates/labor/{열}`(양식 길 · owner) · 계산 `POST /api/m02/layers/{층}/labor/{열}/calc` {doc?, 수량?: {키: 값}} → {줄[{로직, 분모, 배, 수량, 수량출처, 기준{노무비,재료비,경비,계}, 반영{…}, 까닭}], 자재[{코드, 기준단가, 수량, 수량출처, 반영, 까닭}], 기준합, 반영합} · 알약 `GET /api/m02/layers/{층}/labor/{열}/pills`(로직 물량 + 자재). +- 집계표 열 `일위대가` · `자재조합` 칸 지움(정본 = labor 파일) · SS 장부 줄 = `일위대가번` · `도번` · `면적번`. +- 옮기기(한 번 · `M02_Labor_Migrate.py`) = 층마다 열마다 labor 파일 · 열 `일위대가` 키 조합의 담은로직 · 단위 복사(`본뜬.키` = 옛 키 · 입력값 {} · 수량 null · 자재 []) · 여러 열이 쓰던 조합(UP10-000001)은 열마다 복사 · 키 없는 열 = 빈 일위대가 · 몇 번 돌려도 같음 · 시스템 층(git)은 바로 · storage 층은 압축 사본 뒤 브레인이 돌림. +- 실무문서 = 마스터 `resources/master_data/일위대가조합.json` 통째(37 줄 · 손대지 않음 · 옮긴 뒤 어느 열도 안 가리킴 · M01 일위대가 조합 화면도 그대로) · 회사 · 개인 옛 조합(UC · UP)은 파일 그대로 · 화면에서 뺌. +- 프로젝트 = 스냅샷 `일위대가.json` 은 labor 문서 + 담은 로직 줄 · `입력값.json`(모양 그대로) 첫 값 = labor 입력값 · `단가.json` 에 자재 MT 줄 · 계산은 반영 같은 함수. +- 은퇴(화면이 안 부른 뒤) = 일위대가 갈라 내기 · 키 적기(use-combo) · 조합 고치기 · 미리 보기 길(preview) · 일위대가 조합 나무 길. +- 로직 지우기 검사 = 모든 층 labor 담은로직 + 실무문서(32장 「로직 지우기 서버 검사」 줄 끝남). + +## 2. 좌측 패널 (A · sub5) + +- 맨 위 「일위대가 조합」 컨테이너 · 조합 나무 · 둘째 층 단추 벌 지움 · 구조물 조합 안 차례 = 층 단추 + 나무 → 수평선 → 구조물 도면 → 일위대가 → 면적산출. +- 하위마다 첫 줄 「전체 항목」(누르면 `onAll(kind)` → H 목록) · 일위대가 줄 = 열마다 「SU-… 이름」 · 누름 = `onOpen({kind: "labor", name: 열})`(C 상세). +- 이름 「일위대가」(하위 제목 · 상세 제목 · 대시보드 카드 글) · 해시 `open=일위대가` = 구조물 조합 펼침 + 일위대가 하위 펼침 + 전체 항목. +- 「실무문서」 = 구조물 조합 밖 별도 컨테이너(도면 양식 아래) · 마스터 층 · 시스템 관리자만 · 목록 + 상세 읽기만(새 파일 `M02_MasterTemplete_Practice.ts`) · [통째 지우기](물음 두 번 · 서버 = 시스템 관리자 · 쓰는 곳 0 확인 · 줄을 비움 · 버튼은 사용자가 누를 때만). + +## 3. 일위대가 상세 (C · sub2) + +- UI_Main 종류 `labor` 로 엶(양식 길 읽기 · [저장] · [다시 읽기] · 캐시 = 다른 양식과 같음) · 차례 = 머리 → 정보(단위 · 본뜬) → 담은 로직 → 자재목록 → 다이어그램 · 미리 보기 칸 지움. +- 담은 로직 줄 누름 = 입력 모달(그 로직 입력 전부 · 빈 칸 자동값 · 수량 = 이은 칸 · 값 보임 / 손 입력 / 「면적산출에서 잇기」 = 면적산출 상세로) · [확인] = 줄 값만(캐시) · 저장은 [저장]. +- 줄 아래 작은 글 「지역 서울 · 규격 25mm · 수량 B6 = 12.5㎡」 · 빈 입력 「빈 값 n」 붉게. +- 금액 칸 넷(노무비 · 재료비 · 경비 · 합계) · 칸마다 두 줄(위 기준 작게 · 아래 반영) · 맨 아래 합계 줄. +- 자재목록 = 코드 · 이름 · 규격 · 단위 · 수량 · 기준 단가 · 반영 금액 · [자재 추가](M01 자재 하나씩 · `M02_Material_Pick.ts`) · 줄 누름 = 수량 모달. +- 다이어그램 = 저장된 줄 값 + calc 답(수량 있으면 반영 · 없으면 기준 · 표기) · 알약 누름 = 입력 모달 · 자재목록 = 카드 하나 → 재료비 칸. +- 보기 갈래(고치기 없음) 부품을 냄 — D 오른쪽 패널 · H 카드가 씀. +- 입력 목록 · 값 받기 부품은 선택 입력에서 옮겨 다시 씀(`M02_Labor_Inputs.py` · `M02_Labor_Values.ts` · `M02_Labor_Values_Model.ts`) · 값 칸 규칙 한 벌(M01 넣을 값) · 로직 입력이 바뀌어 사라진 값은 알림. +- 37장 「미리 보기 값은 구조물 조합으로 안 넘김」 뒤집힘. + +## 4. 면적산출 (D · sub3) + +- 미리보기 카드 알약 줄 켬끔 단추 · 기본 꺼짐 · pref(계정) · 상세는 늘 보임(단추 없음). +- 상세 오른쪽 패널 = 그 열 일위대가의 담은 로직 + 자재목록(C 보기 갈래 · 기준 · 반영) · 조합 목록 · 찾기 · [이 열에 쓰기] · [조합 고치기] 지움. +- 패널 줄 누름 = 칸 지정 모달(`M02_MasterTemplete_AreaLink.ts` · [지금 고른 칸으로] · 주소 · [풀기]) → 면적 문서 `연결` · spreadsheet 손잡이 `setLink(키, 칸|null, 항목)`. +- 알약 = 로직 물량 알약 + 자재 알약(`자재|MT키` · 알약 모드 「자재」 다시 씀) · 선택 입력 알약 빠짐. +- 면적 금액 서버 계산은 1장 calc 하나(따로 안 만듦). + +## 5. 목록 · 고르기 · 카드 (H · sub1) + +- 목록 부품 하나 `M02_MasterTemplete_ItemList.ts`(M01 머리 · 쪽 · 표를 import · 옮기지 않음) — 「전체 항목」 · 복사 생성 · 항목 하나 복사가 같이 씀. +- M02 상세 제목 = M01 꼴(18px · 단추 오른쪽) 전부. +- 목록 칸: 도면 = 도번 · 이름 · 폴더 · 단위 · 고친 날 / 면적산출 = 면적번 · … / 일위대가 = 일위대가번 · 이름 · 폴더 · 단위 · 담은 로직 수 · 합계(`계산값` · 없으면 「—」) · 고친 날. +- 「새 항목」 → 상세 칸에 [새로 만들기] [복사 생성] · 복사 생성 = 같은 칸 탭 + 목록 → 지금 보는 층 · 고른 폴더(없으면 맨 위) · 원래 이름(겹치면 「(2)」) · 일위대가 새 번호. +- 카드 = 도면 · 일위대가 · 면적산출 셋(그 차례) · 빈 카드 = [새로 만들기] [복사 생성] · 찬 카드 = 「복사해 가져오기」(창 · 탭 + 목록 → 확인 → 덮어씀 · 번호 · 열 그대로) · 일위대가 카드 몸 = C 보기 갈래. +- 탭 = 마스터 · 회사 · 개인(나) · 다른 사람(드롭다운 · 맡은 회사일 때) · 실무문서(일위대가 고를 때 · 시스템 관리자) · 기본 = 시스템 관리자 마스터 · 그 밖 개인(회사 없으면 마스터) · 탭 목록 = E `readableSources`. +- 항목 하나 복사 서버 = `copy_from` 에 from_layer · from_owner(B 가 만듦) · 원본이 실무문서 조합이면 조합 → 일위대가 문서(입력값 빈 값). + +## 6. 권한 (E · sub4) + +- 「회사 C 를 맡음」 = 시스템 관리자 · 또는 C 의 회사 관리자 → C 회사 층 · C 모든 개인 층 = 보기 · 만들기 · 고치기 · 지우기 · 복사 대상. +- 일반 = 마스터 · 회사 읽기만 · 남 개인 안 보임 · 나무 그대로 · 프로젝트 apply · sources 도 같은 규칙. +- 사람 나무 = 맡은 회사면 켬 · 회사 사람 전부(나 맨 위 · 빈 사람 「비어 있음」) · 남의 층 첫 쓰기 = 소속 확인 뒤 뼈대. +- 「〇〇로 복사…」 = 쓸 수 있는 층 전부(마스터 · 볼 회사 · 내 개인 · 지금 사람) + 대상 폴더 고르기 창(`M02_MasterTemplete_TreePanel_CopyTo.ts`). +- 남의 층 쓰기 = `system_audit_logs` 한 줄(`M02_OTHER_LAYER_WRITE`). +- 실무문서 = 일위대가 조합 길 전부 시스템 관리자만(서버가 막음). +- 새 길(labor 문서 · calc · pills · 층 넘는 복사)도 `_root` · `_layer_dir` 한 벌을 거침. + +## 7. 지우기 (G · sub6) + +- G1 자재 조합(UM) · G2 재료산출(`material/` · SM · 카드 · 시트 · 옆 판 · 길 · 시험) 먼저 · G3 선택 입력(카드 · `inputs/` · 자리 · 알약 길)은 C 의 입력 모달이 선 뒤. +- `pickMaterial` · `MaterialItem` → `M02_Material_Pick.ts` 로 옮긴 뒤 지움 · 알약 모드 「자재」 · `LinkMaterial`(A00 spreadsheet) 남김 · `고른자재.json` 남김 · 프로젝트 `입력값.json` 남김. +- storage 층(회사 · 개인 · 프로젝트)의 `material/` · `inputs/` · `자재조합.json` 파일은 지우지 않음(읽기만 끊음 · 정리는 사용자) · git 안 시스템 층 · fixture 는 지움. +- 옛 SM 번호 버림 · 장부 `재료번` 칸은 다음 집계표 [저장] 때 빠짐. + +## 8. 파일 주인 (같은 파일 = 몫만 고침 · 받은 뒤 고침) + +| 파일 | 주인 | 다른 창 몫 | +| --- | --- | --- | +| `M02_MasterTemplete_Store.py` · `M02_Template_Layers.py` | sub7 | sub6 = material · inputs 줄 | +| `M02_MasterTemplete_Router_Items.py` | sub4(`_root`) | sub7 = copy 길 from_layer · sub6 = material · inputs 길 | +| `M02_MasterTemplete_Router_Combos.py` · `Combo_Store.py` | sub5(나무 길) | sub4 = 시스템 관리자 문 · sub2 = preview 지움 | +| `M02_MasterTemplete_UI_Side.ts` | sub5 | sub6 = material · matset 줄 | +| `M02_MasterTemplete_UI_Page.ts` | sub5 | sub4 = copyTo · sub1 = onAll 이음 | +| `M02_MasterTemplete_UI_Main.ts` | sub3(면적 길) | sub2 = labor 종류 · sub1 = 제목 꼴 · showView · sub6 = material 길 | +| `M02_MasterTemplete_ItemCards.ts` | sub1(카드 셋 · 빈 카드) | sub3 = 알약 켬끔 · sub6 = 재료 · 선택 입력 카드 | +| `M02_MasterTemplete_Structure.ts` | sub3(pillsFor) | sub1 = pickCopy · sub6 = inputs · material | +| `M02_MasterTemplete_TreePanel.ts`(670줄) | sub1(copyIn · 넘으면 `TreePanel_Clip.ts`) | sub5 = 일위대가 전용 칸 지움 · sub4 = 「〇〇로 복사…」 이음 | +| `M02_MasterTemplete_Labor_*.ts` | sub2 | sub5 = `Labor.ts` · `Labor_Tree.ts` 나무 지움 | +| `M02_MasterTemplete_Api_Fetch.ts` | 각자 자기 함수만 | — | +| `A00_Common/spreadsheet/spreadsheet_links.ts` · `spreadsheet.ts` | sub3 | — | +| 공용 메뉴 · 진입(`main.ts` · `cad_host.ts` · `ui_template_context_menu.*` · `ui_template_tree.*` · `spreadsheet_condition.ts` · `spreadsheet_menu.ts`) | sub8 | — | diff --git a/docs/raw/plans/2026-10-04_plan_단가산출_흐름도_조합다이어그램_마스터요소.md b/docs/raw/plans/2026-10-04_plan_단가산출_흐름도_조합다이어그램_마스터요소.md new file mode 100644 index 000000000..fa33578c6 --- /dev/null +++ b/docs/raw/plans/2026-10-04_plan_단가산출_흐름도_조합다이어그램_마스터요소.md @@ -0,0 +1,108 @@ +# 지난 계획 — 33~38장 (2026-10-01 ~ 10-02) + +단가산출 로직 점검(데스크탑 한시) · 쉬운 단가산출 · 로직 만들기 시험판(세 칸 판 · 시트 로직 · 다이어그램) · 흐름도 시험판 · 흐름도 본 프로그램 적용 · 일위대가 조합 다이어그램 · 마스터 요소 화면 손질. + +## 33. 단가산출 로직 점검 · 수정 (사용자 + 데스크탑 메인 · 보조 · 10-1 한시) + +- 사용자가 데스크탑 메인 · 보조와 직접 진행 · 브레인은 랩탑 sub1~8 로 사용자가 요청하는 다른 일을 맡음(10-1 18:00 무렵까지). +- 근거 = `docs/raw/M01_점검/종합/` · `docs/raw/M01_결정목록/` · `docs/raw/M01_입력목록/` · `docs/raw/M01_고침/`. + +## 34. 쉬운 단가산출 제어 (새 길 · 10-1 · 랩탑) + +- 사용자 확정(10-1 오전): 만든 쉬운 단가산출은 유지(지우지 않음) · 로직 정의 자체를 만드는 판은 제안만 · 허락 뒤 착수 +- 사용자 확정: 지금 단가산출은 그대로 · 「추가」 단추 옆 새 단추로 따로 들어감 · 코드는 `M01_MasterData/EasyCalc/` 한 곳(안 되면 폴더째 지움) · 초등학생도 추가 · 삭제 · 수정 · 시간이 걸려도 검토 먼저. +- 정함(브레인): 새 길은 지금 로직 형식을 그대로 읽고 씀(계산 엔진 안 바꿈) · 폴더 밖 참조는 단추 자리 한 곳만. +- [x] 34-1 검토 — 데이터 · 계산 지도(sub4) · 지금 화면 걸음 재기(sub6) · 실무 모양 + 갈래 비교(sub7) · 지난 시도(sub8) → `docs/raw/M01_쉬운단가/` · 지금 화면 새 로직 38 누름 · 5~6 창 +- [x] 34-2 설계 확정(브레인) — 문장 카드 · 품셈 기본은 「이걸 바탕으로 고치기」 · 쓰기는 `/logic/*` 만 · 1단계 숫자 수량 + 잠금 카드 → `docs/raw/M01_쉬운단가/설계.md` +- [x] 34-3a 단추 + 빈 창(sub1 · 「식으로 새로 만들기」 옆) 검증완료 +- [x] 34-3b 1단계 — Model(sub7) · Api(sub4) · Cards(sub2) · Pick(sub3) · Sheet(sub5) · 틀 · 잇기(sub1) — 설계.md 2 · 3절 · Sheet 검증완료 · Model 검증완료 · Api 검증완료 · Pick 검증완료 +- [x] 34-3c 2단계 — 표 보고 고르기(sub8) · 조건 고치기(sub7 · 조건 줄 93% 읽음) 검증완료 · 카드 단추 잇기(sub2) · 조건 고치기 검증완료 +- [x] 34-4 검증 — 대표 일 여섯 + 예외(빈 값 · 같은 이름 · 저장 안 하고 닫기 · 권한 없음 · 밝음 · 어두움) · 지우기 시험(폴더 밖 참조 줄 수) · 걸음(sub3 고침 뒤) ①21 ②8 ③2 ④4 ⑤2 ⑥3 = 40(목표 36 넘음 4 · ① 새 설계 값 자동값으로 채워 금액 곧장 보임) · 다른 일위대가 넣고 저장 됨 · 권한 없음(일반 사용자) = M01 화면 못 들어감 · /api/m01 403 · 폴더 밖 참조 두 줄 · 검증완료 + +## 35. 단가산출 로직 만들기 판 (제안 단계 · 착수 전) + +- 사용자 확정: 쉬운 단가산출처럼 따로 폴더 시험판 · 로직 몇 개 골라 만듦 · 페이지 보고 판단(마음에 안 들면 지움) · **착수는 허락 뒤** · 인터넷 조사 먼저 · 화면 용어는 단어로 다시 제안. +- 제안서 https://claude.ai/artifact/U4rkgpYWL4Em5VKBEVa4oY (세 칸: 원문 표 · 산출 과정 · 결과) +- [x] 35-0 인터넷 조사 — 견적 프로그램(sub6) · 노코드 규칙 도구(sub8) → `docs/raw/M01_쉬운단가/로직판_조사_*.md` · 제안서 2판에 반영 +- [x] 35-1 용어 단어 제안(제안서 2판 「용어 — 단어로」) → 사용자 확정 · 완료 처리(10-4): 시험판 지움(35-9)으로 접음 +- [x] 35-2 시험판 — 사용자 허락(10-1 · 갈래가 여럿이면 갈래마다 · 목적 = 코딩 없이 원문 보며 확인 · 수정 · 생성) · A 세 칸 판 `LogicBuilder/` · B 엑셀 시트형(우리 스프레드시트) · 먼저 조사: 엑셀 로직 지도(sub4) · A 설계 초안(sub7) → 브레인 A 대 B 판단 → 구현 · 완료 처리(10-4): 35-2a 로 함 +- [x] 35-2a A 세 칸 판(가운데 문장 / 격자 두 보기) — 정함 `docs/raw/M01_쉬운단가/로직판_A_정함.md` · Blocks(sub7) · 서버 · Api(sub4) · 원문 · 검사(sub8) · 문장(sub2) · 격자(sub5) · 단계 안내(sub3) · 틀 · 잇기 · 끝 검증(sub1) · LB 서버 검증완료 · Blocks 검증완료 · Grid 검증완료 · Steps 검증완료 · Source · Check 검증완료 · Wizard 검증완료 · 잇기 · 끝 검증완료(sub1) — ① 원문 대조 6/6 블록 읽힘 · 걸린 줄 · 출처 보임 ② GF000219 표 칸 0.15→0.20 = 79,280→96,759 원(예상과 같음) · 파일 안 바뀜 · 다시 열면 0.15 ③ 단계 안내 11 걸음 + 저장 → 현장 다시 열림 60,984 원(/calc 같음) → 삭제 2 걸음 ④ 빈틈 해당 없음 10 · 못 따진 표 조회 1 · 기대값 시험표 맞음/다름 · 화면 초안만 ⑤ 표 고침 없을 때 lb/calc = /calc 8/8 · 문장 ↔ 격자 같은 값 · 예외(빈 이름 · 저장 안 하고 닫기 · Ctrl+Z · Ctrl+S · 권한 없음 403 · 밝음 · 어두움) · 폴더 밖 참조 LogicLab.ts 두 줄 + main.py 등록 한 줄 +- [x] 35-2b 남은 것(사용자 판단 뒤) — 문장 / 격자 고르기 · 표 칸 고침 저장 여부 · 적용 품셈 판 · 용어 · 칸 역할 「결과」 바꾸기 · 단계 안내 고르기 창 수량 물음 · GX000001(메인 11:53 · 12:02) 주인 확인 · 보고 https://claude.ai/artifact/NqKfHaJULcQo6dSvnsxy1n · 완료 처리(10-4): 시험판 지움(35-9)으로 접음 +- [x] 35-3 점검(확인만) — 표끼리의 관계(로직) 자체를 초등학생이 만들고 · 바꾸고 · 지울 수 있나 · 품셈에 없는 관계 더하기 · 시나리오 여섯(sub6) — 결과: 있는 관계의 값만 고침(초등 중) · 관계 더하기 · 지우기 · 다시 잇기 · 새 입력값 · 새 표 · 로직끼리 바꾸기 안 됨 · 문턱만 바꾸면 계산 멈춤 + 엉뚱한 알림 · `docs/raw/M01_쉬운단가/로직판_관계제어_점검.md` +- [x] 35-4 다이어그램 제안(사용자 생각 10-1) — 상자 = 표 · 입력값 · 고정값 · 계산 · 항목 · 다른 로직 · 결과를 직접 다룸 · 선 = 관계를 만듦 · 품셈에 안 묶임(사용자 표 · 사용자 관계) · 관계 제어 = 다이어그램(사용자) · 위 가로 띠 + 아래 고른 상자 다루기 · 결과 · 관계 = 진짜 엑셀 수식(칸 주소 · 함수 · 사용자 확정) · 알약 = 입출력 칸 표시 · 새 종류 「시트 로직」(TS 엔진 한 벌 · 서버 Node) · 제안서 https://claude.ai/artifact/4iCAGPhE9maauGNTkXg5he · 허락 전 착수 금지 · 완료 처리(10-4): 35-5 · 35-7 로 함 +- [x] 35-5 시트 로직 시험판(사용자 허락 10-1 오후) — 위 가로 다이어그램 · 상자 = 시트 · 관계 = 엑셀 수식 · 입출력 = 알약 · 저장 = `M01_MasterData/SheetLogic/_data/`(git 밖 · 폴더째 지움) · 정함 `docs/raw/M01_쉬운단가/시트로직_정함.md` · Model · Eval(sub7) · 서버 · Api(sub4) · Diagram(sub8) · Panel(sub2) · Table(sub3) · Item · Result(sub5) · Import(sub6) · 틀 · 잇기 · 끝 검증(sub1) · SL 서버 검증완료 · Model 검증완료 · Diagram 검증완료 · Item · Result 검증완료 · Table 검증완료 · Import 검증완료 · 잇기 · 끝 검증완료(sub1) — 걸음: S1 ①②③ 각 3(79,280 · 73,085 손셈 같음 · 2.5m 문턱은 품셈 표 3m 부터라 2.5~3m 해당 없음) · S2 ① 3 ② 4(찰쌓기 표로) ③ 5 · S3 ① 약 19(경사 급 65,018) ② 9(내 표 붙여넣기 · 할증 10) ③ 7(잡재료 2% 17,551) · S4 선 끊기 2 · 상자 지우기 2(물음 한 번) · Ctrl+Z 되돌림 · S5 0(상자 · 선 · 값 한 장 · 고르면 이어진 선 진하게) · S6 항목 일위대가 11 · 다른 로직 상자 13(덤프트럭 16.2) · 새 로직(품셈 없이) 25 걸음 → 저장 서버 36,000 = 화면 → 다시 열림 → 지우기 2 · 서버 = 화면 4/4(36,000 · 17,550.9 · 10.12 · 다시 열기) · 가져오기 6/6 /calc 같음(0 · 60,984/79,280 · 2,064,816 · 670 · 16.2 · 57,516) · 예외(빈 이름 · 같은 상자 · 순환 · 빈 입력 물음 · 저장 안 하고 닫기 · 권한 없음 sl 5/5 403 · 밝음 · 어두움) · 세 칸 안쪽 세로 밀기 없음(6개 모든 상자) · 폴더 밖 참조 LogicLab.ts 2 + main.py 1 · _data 비움 · 남은 흠(다른 창): 가져온 표 상자 + 결과 칸 목록 예시값이 글자로 들어가 30 등에서 해당 없음(sub5) · 가져온 로직 화면 번호 · 자재 검색어 · 끝 검증완료(S1~S6 · 새 로직 · 서버 = 화면 4/4 · 가져오기 6/6) · 남은 흠: 결과 칸 예시값 선택이 글자 「30」 으로 들어가 표 찾기 해당 없음(sub5 · 사용자 지시 뒤) +- [x] 35-5a 사용자 요청 — 다이어그램 띠 · 고른 상자 시트 · 결과 높이를 안쪽 세로 스크롤이 없을 때까지(sub1) +- [x] 35-6 로직 문법 먼저(사용자 방향 10-1 14시 무렵 · 화면보다 로직) — 바탕 표(인력 378 · 재료 30,767 · 기계 654 · 소요량 2,535 · 계수 498 · 환율 · 요율 · 사용자 표 — 보기 · 만들기 · 비목은 표가 정함) · 단위 넷(값 · 표 · 정보 · 요소) + 묶음(세분화 · 묶기 · 분류 · 묶음째 잇기 · 겹 묶음 · 틀 다시 쓰기) · 흐름 셋(정보 만들기 · 잇기[그대로 · 바꿔서 · 갈라서 · 모아서 · 묶음째] · 내기) · 확인 도구 · 제안서 https://claude.ai/artifact/BFWvm1Q4fhCSZ4Rt34UgYY · 다음 = 문법 확정 → 종이 연습(절 10개) — 허락 전 착수 금지 · 완료 처리(10-4): 35-7 · 35-9 흐름도로 갈음 +- [x] 35-5b 단추 공용 양식(점검 끝 · 고침은 사용자 지시 뒤) — 날 단추 23(목록 줄 8 · 칩 · 역할 · 보기 바꿈 11 · ▾ 2 …) · sm 단추를 44px 로 덮기 3 · 큰 시작 56px 3 · `docs/raw/M01_쉬운단가/시험판_단추_점검.md` · 완료 처리(10-4): 시험판 지움(35-9)으로 접음 +- [x] 35-7 단가산출 다이어그램 시험판(사용자 기준 10-1 14시 무렵) — 입력 · 함수 · 출력 상자 · 자유 배치 · 함수 = 엑셀 · 출력 하나 · 출력 = 비용 종류 · 원가 · 폴더 `M02_MasterTemplete/CostFlow/`(_data · _sample) · 정함 `docs/raw/M01_쉬운단가/원가다이어그램_정함.md` · Model · Eval(sub7) · 서버(sub4) · Diagram(sub8) · 함수 판(sub2) · 입력 판(sub3) · 출력 · 원가(sub5) · 견본(sub6) · 단추 · 페이지 · 잇기 · 끝 검증(sub1) · Model 검증완료 · Output · Summary 검증완료 · Sample 검증완료 · FuncPanel 검증완료 · Diagram 검증완료 · CF 서버 검증완료 · InputPanel 검증완료(사용자 요소 만들기는 서버 Api 가 main 에 오면 확인) · 잇기 · 끝 검증완료(sub1) — ① 빈 판에서 손으로 메쌓기 꼴(변수 4 · 소요량 표 2 · 인력 2 · 함수 증가율 → 석공 / 보통인부 병렬 · 출력 노무비 2) 9 상자 · 약 138 손놀림 → 원가 148,471 = 견본(증가율 40 · 석공 97,882.512 · 보통인부 50,587.992 손셈 같음) ② 비용 종류 바꾸면 148,471 이 노무비 → 경비 → 재료비 로 옮겨감 · 「값」 이면 원가에서 빠짐(각 1 걸음) ③ 상자 끌어 옮김(1520,190 → 1675,306) · 저장 → 다시 열면 그대로 ④ 함수 출력 하나(다른 칸 고르면 옮겨감 · 알약 하나) · 순환 막힘 · 출력에서 나가는 선 없음 · 빈 받는 자리는 저장 때 물음 ⑤ 사용자 요소(시험 보조공 200,000) 5 걸음 · 마스터 그대로 · _data 에만 ⑥ 저장 때 서버 = 화면 2/2(148,470.504) ⑦ 단위 오른쪽 위(- → 원/㎡) · 색 바꾸기(Entry 판 머리) · 「다이어그램」 이름 · 다이어그램 726px · 안쪽 스크롤은 함수 격자(가상 200줄)만 · 밝음 · 어두움 · 권한 없음(cf 길 6/6 403 · 화면 못 들어감) · 견본 셋 = /calc · 손셈(148,471 · 3,000 · 15.552) · 폴더 밖 참조 LogicLab.ts 2 + main.py 1 · _data 비움 · 끝 검증완료(손으로 메쌓기 9 상자 = 견본 148,471 · 비용 종류 옮김 · 자리 저장 · 규칙 · 사용자 요소 · 서버 = 화면 · 견본 셋 손셈) · 함수 출력 기본 이름 겹침 고침(sub7 2be767d18) · 남은 것: M01 단가산출 단추 줄이 셋째 줄까지(시험판 단추 넷 · 사용자 결정) +- [x] 35-7a 브레인 실제 검증(10-1 16:45) 흠 여섯 고침 — ① 고칠 것 칸 지난 경고 · ⑥ 시험판 단추 「시험판 ▾」 로 묶기(sub1) · ② 알약 쉼표 · ③ 표 알약 글 · ⑤ 맞춤 확대 90% 아래 안 감(sub8) · ④ 입력 상자 공통 단위(sub3) · 다시 검증완료(16:55 · 고칠 것 칸 비어 있음 · 알약 쉼표 · 표 · 40줄 · 인력 원/일 · 확대 90% · 막대 한 줄 · 시험판 ▾ 넷 · 원가 148,471) + +## 35-8. 단가산출 다이어그램 2판 — 재료 선택 자리 · 선 규칙 (계획 · 허락 전) + +- 사용자 확정: 고르는 방식은 재료만(다른 그룹은 상자를 늘림) · 설계 때 입력은 오른쪽 아래 검증 칸 · 나중에 프로젝트에서 일위대가로 쓸 때 재료는 목록에서 고르거나 단일 값 · 표에 따라 출력을 가르는 것은 엑셀 수식 대신 선 규칙. +- 정함(브레인): 재료 요소 = 고정 · 선택 자리 · 조건 찾기 · 알약 이름 = 자리 이름 · 선 규칙 = 선 하나에 하나(표에서 찾기 · 조건 통과 · 바꾸기) · 규칙은 받는 칸 엑셀 수식으로 바뀌어 엔진 한 벌 · 선 위 규칙 글 늘 보임. +- [x] 35-8a 틀(Model) — 재료 요소 방식 · 범위(구분 · 상세구분 · 찾는 말) · 조건(규격 ← 알약) · 기본 품목 · 선 규칙 칸 · 규칙 → 받는 칸 수식 · 조건 선 겹침 · 안 맞음 검사 — node 시험(sub7) · 완료 처리(10-4): 35-9 에서 함 +- [x] 35-8b 서버 — 선택 자리 기본 품목 · 조건 찾기 단가(맞음 0 · 여럿 알림) · 「설계 때 정할 것」 목록(변수 + 재료 자리) 내보내기 — 시험(sub4) · 완료 처리(10-4): 35-9 에서 함 +- [x] 35-8c 입력 판 — 재료 방식 · 범위 · 조건 규칙 · 기본 품목 · 찾기 고침(쪽 넘기기 · 띄어쓰기 AND · 규격까지) — ORCA(sub3) · 완료 처리(10-4): 35-9 에서 함 +- [x] 35-8d 검증 칸 — 재료 선택 자리를 변수처럼(범위 안 목록 고르기 · 찾기) · 조건 자리는 잡힌 품목 표시 — ORCA(sub5) · 완료 처리(10-4): 35-9 에서 함 +- [x] 35-8e 선 규칙 — 선 위 규칙 글 · 선 누르면 아래 판에서 규칙 고르기(표 찾기: 조건 칸 ↔ 알약 · 꺼낼 칸 / 조건 통과: 알약 · 같음 · 다름 · 이상 · 이하 · 값 / 바꾸기: × · ÷ · +% · 단위 · 끝수) · 고른 상자 판에 들어오는 선 규칙 목록 — ORCA(sub8 · 판 sub2) · 완료 처리(10-4): 35-9 에서 함 +- [x] 35-8f 견본 · 끝 검증 — 견본 둘 더(지반에 따라 굴삭기 / 브레이커 가르기 · 흄관 관경 조건 찾기) · 사용자처럼: 함수 상자 없이 표 찾기 · 가르기 · 검증 칸에서 재료 고르기 · 방법 걸음 수(sub6 견본 · sub1 잇기 · 끝 검증) · 완료 처리(10-4): 35-9 에서 함 +- [x] 35-8g 다이어그램 마우스 — 판 옮기기 = 가운데 단추 끌기 · 고르기 = 왼쪽 누름 · 왼쪽 끌기로 글자가 선택되지 않음(다이어그램 영역 글자 선택 막음) · 상자 옮기기는 상자 머리 왼쪽 끌기 그대로 — ORCA(sub8) · 완료 처리(10-4): 35-9 에서 함 +- [x] 35-8h 「고른 상자」 → 「상세 내용」 · 상자뿐 아니라 알약 · 선을 골라도 내용이 바뀜(알약 = 값 · 단위 · 어디서 오나 · 어디로 가나 / 선 = 선 규칙 고르기) — ORCA(sub1 잇기 · 알약 · 선 판 sub2) · 완료 처리(10-4): 35-9 에서 함 +- [x] 35-8i 상세 내용 단추 · 칸 크기 — 함수 판 꾸밈이 공용 작은 단추를 44px 로 덮는 줄(`M02_CF_FuncPanel.css` `.m01-cfpanel .ui-btn …`)을 지우고 판 안 단추를 모두 공용 작은 크기(createButton size "sm")로 · 입력 · 고르기 칸도 공용 크기 — ORCA 수치(sub2 · sub3 · sub5 각자 판) · 완료 처리(10-4): 35-9 에서 함 +- [x] 35-8j 상세 내용 머리 — 왼쪽 「상세 내용」 · 오른쪽에 고른 상자 제목 · 알약을 골랐으면 그 오른쪽에 알약 이름(예 「높이 증가율 찾기 › 증가율」) · 상세 내용 안 칸 차례 정리(머리 · 상자 설정 · 본문 · 경고) — ORCA(sub1) · 완료 처리(10-4): 35-9 에서 함 +- [x] 35-8k 단위는 고르기 — 단위 칸을 목록 고르기로(치면 걸러져 보이고 고르기는 사용자 · 손으로 쓴 글은 안 받음) · 목록 = 단위 표준(35-8l) 단위 + 마스터 결과단위 + 금액 단위(원) · 「-」 = 없음 · 함수 · 출력 · 변수 · 사용자 요소 단위 모두 같은 고르기 — 공용 고르기 파일 `M02_CF_UnitPick.ts`(sub2) · 판마다 바꿔 끼움(sub2 · sub3 · sub5) · 완료 처리(10-4): 35-9 에서 함 +- [x] 35-8l 단위 표준 표 — 산림품셈 1-2-2 「설계서의 단위 및 소수의 표준」(원문 `resources/knowledge/original/원가계산/산림_표준품셈/본문/제01장_적용기준/1-02_설계_및_수량.md` · 종목 약 70 · 규격 단위 · 소수자리 · 단위수량 단위 · 소수자리 · 비고 · 주 ①~③)을 표로 — 소수자리 「-」 = 0 · 단위 「-」 = 없음 · 원문 흠은 사용자가 고침(시험판에서 보고 고치는 표 · 저장 `CostFlow/_data` · 처음 값 `CostFlow/_standard/`) — 시험(sub6) · 완료 처리(10-4): 35-9 에서 함 +- [x] 35-8m 금액 단위 표준 — 같은 절 「금액의 단위표준」(설계서 총액 1,000원 · 소계 · 금액란 · 일위대가표 계금 1원 · 일위대가표 금액란 0.1원 · 모두 미만 버림)을 같은 표 자리에 — 출력 상자(비용) 금액 기본 = 일위대가표 금액란 0.1원 미만 버림 · 원가 합계 기본 = 계금 1원 미만 버림 · 바꿀 수 있음(sub6 표 · sub5 출력 · 원가) · 완료 처리(10-4): 35-9 에서 함 +- [x] 35-8n 끝수 고르기 — 함수 상자 출력 · 출력 상자에 「끝수」(소수자리 · 반올림 / 올림 / 버림) · 단위를 고르면 단위 표준에서 소수자리를 제안(주 ① 반올림 · ② 항목 값이 우선) · 고른 끝수는 출력 칸 수식(ROUND 계열)으로 — 엔진 한 벌(sub7 틀 · sub2 함수 판 · sub5 출력 판) · 완료 처리(10-4): 35-9 에서 함 +- [x] 35-8o 로직끼리 잇기 · 흐름 따라가기 — 저장한 다른 로직을 입력 상자로 불러 그 출력 알약을 받음(일위대가 → 일위대가) · 알약 · 선을 고르면 값이 어디서 와서(출처 사슬) 어디로 가는지(가는 곳) 다이어그램에서 길을 칠하고 상세 내용에 줄로 · 값이 끊긴 곳(빈 받는 자리 · 통과 안 된 조건 · 표에 없는 조건) 표시 — 시험 · ORCA(sub7 틀 · sub8 다이어그램 · sub1 상세 내용) · 완료 처리(10-4): 35-9 에서 함 +- 차례(사용자 10-1 · 「데이터의 흐름과 값의 연결에 집중」): **1단계 = 흐름 · 연결** a 틀 · b 서버 · e 선 규칙 · o 로직끼리 · 흐름 따라가기 · h 알약 · 선 고르기 · c · d 재료 자리 · f 견본 · 끝 검증(흐름 기준 — 값이 어디서 와서 어떤 규칙을 지나 어디로 가는지) / **2단계 = 값의 뜻 · 보기** k 단위 고르기 · l 단위 표준 · m 금액 표준 · n 끝수 · j 머리 · g 마우스 · i 단추 + +## 35-9. 단가산출 흐름도 시험판 (사용자 지시 10-1 17시 무렵 · 브레인이 끝까지 · 35-8 전체) + +- 사용자 확정: 브레인이 전체 제어해 새 시험판 하나 · 단가산출 다이어그램(시험)만 남기고 옛 시험판 셋 삭제 · 새 폴더(필요 없으면 바로 지움) · 데이터 흐름 · 값 연결 먼저 · 자유도 높고 이해하기 쉬운 화면 · 끝까지 구현 · 검증. +- 정함 `docs/raw/M01_쉬운단가/흐름도_정함.md` · 폴더 `M02_MasterTemplete/DataFlow/` +- [x] 35-9 창 여덟 — 옮기기 · Model · Eval(sub7) · 옛 시험판 지우기 · 들어가는 길 · 상세 내용 머리 · 잇기 · 끝 검증(sub1) · 서버(sub4) · 다이어그램(sub8) · 상세 내용 판 · 단위 고르기(sub2) · 입력 판(sub3) · 출력 · 검증 칸(sub5) · 표준 표 · 견본(sub6) · 표준 · 견본 검증완료 · Model 검증완료 · 출력 · 검증 칸 검증완료 · Diagram 검증완료 · 입력 판 검증완료(서버 자재 찾기 · UnitPick 이 main 에 오면 다시 확인) · DF 서버 검증완료 · 잇기 · 끝 검증완료(sub1) — ① 빈 판에서 함수 상자 없이 네 흐름 73 걸음: 재료 조건 찾기(관경 → 흄관 자리 · 13 개 중 하나 · 125,870) 21 · 선 규칙 표에서 찾기(높이 증가율표 → 출력) 19 · 조건 통과 두 갈래(지반 = / ≠ 토사) 27 · 다른 로직 잇기(견본 지반 · 지반 알약 이음) 6 ② 알약 · 선 고름 → 온 길 · 가는 길 칠함 + 상세 내용 줄(지반 → 다른 로직 · 두 조건 선 · 흄관 ← 관경) ③ 끊긴 곳 넷 보임(빈 받는 자리 · 통과 안 됨 · 표에 맞는 줄 없음 · 맞는 재료 없음 — 알약 · 선 「!」 · 끊긴 곳 줄 누르면 그 자리 고름) ④ 검증 칸 관경 600 → 800 = Φ800 품목 215,530 · 지반 토사 → 암 = 내 두 갈래 뒤집힘 + 다른 로직 2,000 → 10,000 · 다른 로직 설계 값 바꿈 10,000 → 20,000 ⑤ 단위 고르기 「mm」 = 3 개로 걸러짐 · 손글 안 받음 · 끝수 80,000.37 = 기본 225,870 / 1원 올림 225,871 · 표준 표 고침 저장(3 → 2) → 처음 값으로 되돌림 ⑥ 가운데 끌기 = 판 옮김(−60, −30) · 왼쪽 끌기 = 판 그대로 · 글자 선택 막힘 · 단추 18px 모두 sm · 칸 30px ⑦ 저장 → 다시 열기 = 상자 9 · 선 6 · 자리 · 규칙 글 그대로 · 서버 다시 셈 = 화면 187,910 · 견본 다섯 사본 저장 서버 = 화면(148,470.504 · 2,000 · 84,680 · 3,216.6 · 값 15.552) · 밝음 · 어두움 · 로그인 없음 df 길 6/6 401(관리자 아닌 계정 403 은 계정 없어 못 함) · 폴더 밖 참조 LogicLab.ts 2 + main.py 1 · _data 비움 · 검증 중 고친 내 흠(047f3a279 · 28469de62): 재료 조건 찾기 · 선택 자리 값을 서버에 안 물음(흄관 견본 관경 600 → 400 = 211,700 → 116,600 · 1m 84,680 → 46,640 · 1000 = 235,120 · 선택 자리 Φ600 → Φ400 = 211,700 → 116,600) · 「상자 이름」 칸(입력 · 함수 · 같은 이름 · 빈 이름 막음 · 출력은 출력 판) · 머리 이름 · 단위 칸 작은 크기(30px) · 「새로 만들기」 단추 76px(칸 폭 710 아님) · 표준 표 모달 제목 한 번 · 남은 흠(sub2): 선 규칙 표에서 찾기 조건 칸에 범위 이름(「높이」)이 없어 범위 찾기를 화면에서 못 만듦(높이 5 → 줄 없음 · 4 만 맞음) · 브레인 실제 검증완료(흠 14 고침 · 흐름도 시험 81 · 견본 다섯 서버 = 화면 · 보고 JgfqRzLPYgqXG7vB2cV1Yj) · 검증 대기 + +## 36. 단가산출 흐름도 본 프로그램 적용 (사용자 지시 10-1 21:40 · 결정 반영 22시 · 착수 승인 22:20 · 밤새 진행 · 아침 보고) + +- 사용자 확정: 흐름도 = 최종 · 로직 목록에서 로직을 열면 지금 내용 그대로 + 오른쪽 흐름도 패널(같은 로직) · 「식으로 새로 만들기」 지우고 「신규 로직」(흐름도 새로 만들기) · 「본떠 만들기」 → 「로직 복사/생성」(신규 로직 오른쪽) · 시험판 단추 · 입구 지움 · DataFlow 폴더 유지. +- 근거(마스터 로직 57 파일 · 1,472 로직 · 입력 3,518 · 중간 식 3,168 · 호표 줄 5,265): 식 말 = 찾기(표 · 조건 · 칸) 8,152 · 만약 3,532 · 또는 185 · 최대/최소 173 · 올림/반올림/버림 141 · 로직() 41 → 흐름도 표에서 찾기 · 조건 · 함수 · 다른 로직 입력으로 옮길 수 있음. +- 사용자 결정 D1(그대로): 「품셈원문도 제대로 구현된게 아니기 때문에 내가 흐름도까지 만들어서 로직을 볼려는 거잖아. 그러니 당연히 둘다 같은 내용 … 원문과 대조하면서 검증할때 잘못된게 있으면 흐름도에서 수정할거야」 → 한 로직 · 두 보기: 왼쪽 지금 내용 = 보기 · 고치기는 흐름도(공용 로직 포함 · 시스템 관리자) · 흐름도로 저장한 로직은 흐름도가 정본 · 왼쪽은 흐름도에서 만든 같은 꼴 · 아직 저장 안 한 로직은 지금 내용에서 자동으로 그린 흐름도(값 대조로 같음). +- 사용자 결정 D2(그대로): 「흐름에 대한 로직은 costflow폴더에 있을테니 일단은 그냥 두고 쓸대없는 견본과 버튼만 삭제」 → CostFlow 폴더 그대로 · 시험판 단추 · 견본 목록 지움(흐름도는 DataFlow 폴더만으로 돌아감 · CostFlow 를 부르지 않음). +- 사용자 결정(함수 상자): 「함수박스를 기준으로 하나의 결과가 나가는게 맞는것같아. 여러개가 동시에 나가면 혼란만 가중」 → 함수 상자 하나 = 결과 알약 하나(그대로) · 식은 상자 겉에 넣지 않음(상자가 커짐) · 함수 상자 · 선을 고르면 상세 내용에 사칙연산 · 조건이 보이고 고치면 바로 따라감. +- 칸 연결 = M02 방식 그대로(칸을 고른 채 알약을 눌러 하나씩 연결 · 식은 칸 주소 · `spreadsheet_links.ts` 직접 확인) · 계산 엔진 · 이름 풀기 · 이름 규칙 없음 — 보기만 칸 주소를 연결된 알약 이름으로 바꿈. +- 사용자 결정(조건): 「조건문은 현재를 유지하는게 보기 편한것같고 대신 어떤값을 지정할지는 입력된 리스트에서 선택 … 조건 값을 내가 넣으면 오기 … 복수 조건문도 넣어주고」 · 「조건 만들기도 엑셀 기능 내부에서 존재해야해」 → 선 조건은 지금 웹앱 모양 · 조건 값은 고른 알약이 가질 수 있는 값 목록에서 고름(변수 고르기 · 표 칸 값 · 재료 규격 등 · 목록이 없는 수는 숫자 칸 + 범위 검사) · 조건 여러 줄(그리고 / 또는) · 함수 상자 엑셀 시트 안 「조건 만들기」(칸 고름 → 만약 · 아니면 만약 · 아니면 줄 → 그 칸에 IF 식 · 값은 같은 목록에서 고름). +- 안전: 화면 시험 저장은 마스터 사본 폴더에서만(서버가 환경 변수로 사본 폴더를 씀 · 실제 resources/master_data 는 안 바뀜 · git status 로 확인). +- 창: sub2 36-0(판 · 조건 만들기) · sub7 36-0b(조건 틀 · 값 목록) → 36-2 · sub6 36-1 → 36-7 · sub8 36-3 · sub4 36-4 서버 · 사본 폴더 · sub5 36-4 왼쪽 보기 · sub1 36-5 · 잇기 · 36-8 · sub3 36-6 +- 브레인 정함(조사 뒤 · 아침 보고에 알림): 옛 로직 「기계 · 직종 · 유가 자리」(EQ:{기계} · LB:{직종} · MO:경유:{지역} · 입력값으로 요소를 고름 · 71 로직)는 새 모드 없이 이미 있는 「조건 찾기」(이름 · 키 같음 · 변수 알약)를 인력 · 기계 · 유가 요소에도 씀 · 대조 입력 = check_master.시험입력 · 로직 셈을 부르는 곳(Store.calc · text · master_combo.run · M02_Item_Prices.calc · /logic/auto)은 한 곳에서 흐름 갈래. +- [x] 36-0 함수 상자 안 보기 · 칸 연결 조정(36-2 보다 먼저) — ① 상세 내용 「식 보기」: 결과 칸 식을 칸 주소 → 연결된 알약 이름으로(예: 1m 재료비 = 흄관 단가 ÷ 2.5) + 값 들어간 식(= 211,700 ÷ 2.5 = 84,680) · IF 는 만약 / 아니면 줄 · 지금 타는 갈래 표시 · 중간 칸 식도 차례대로 · 칸 식을 고치면 바로 다시 셈 → 다이어그램 · 원가 따라감 ② 「칸 연결 표」(칸 · 연결 알약 · 입력 / 결과 · 이어진 선 · 표에서 바로 바꿈 · 풀기) + 칸 색(입력 파랑 · 결과 초록 · 상자 색과 같음) · 결과 칸 하나 ③ 선을 고르면 그 선 규칙도 같은 「식 보기」 꼴(예: 굴착량 → 지반 = 토사 일 때만 → 굴삭기로 파기) ④ 엑셀 시트 안 「조건 만들기」 — 칸을 고르고 열면 만약 [알약] [비교] [값 고르기] → [알약 · 값] 줄 · 아니면 만약 더하기 · 아니면 → 그 칸에 IF 식(엑셀로 쓴 IF 도 같은 줄로 다시 열림 · 못 바꾸는 식은 식 그대로) · 검증완료(sub2 · 식 보기 · 칸 연결 표 · 조건 만들기 · 선 조건 여러 줄 · 시험 8) · 검증 대기 +- [x] 36-0b 선 조건 손질 — 조건 값 = 고른 알약의 값 목록에서 고름(Model 값 목록 함수 · 변수 고르기 · 표 칸 값 · 재료 규격 · 목록 없는 수는 숫자 칸) · 조건 여러 줄(그리고 / 또는) · 규칙 → 받는 칸 엑셀 식 · 서버 Compute 같은 뜻 · 선 위 이름표 · 겹침 · 안 맞음 검사 그대로 · 검증완료(sub7 · 조건 여러 줄 그리고/또는 · 값 목록 · 서버 = 화면 · 시험 30) · 검증 대기 +- [x] 36-1 조사 — 옛 식 문법 전수(서버 파서 · 요소 부르기 꼴 · 따옴표 글) · 변환 못 하는 꼴 목록 · /calc 를 부르는 곳 · 검증완료 +- [x] 36-2 변환 — 옛 로직(입력 · 중간 · 호표 · 덧줄 · 결과 · 끝수) → 흐름도(입력 상자 · 표 알약 + 표에서 찾기 · 함수 · 출력 상자) + 자동 배치 · 서버 파서로 · 옛 식에 나온 이름마다 알약을 칸에 연결하고 식은 칸 주소로(만약 → IF · 최대 → MAX · 찾기 → 표에서 찾기) · 함수 상자 하나 = 결과 하나 · 전수 대조(변환 흐름도 값 = 옛 계산 값 · 맞는 비율 · 안 맞는 까닭) · 검증완료(sub7 · M02_DF_Convert · 전수 1,472 시험입력 96.7% · 기본 입력 97.4% · 안 맞는 49 = 재료 검색어 자리 18 · 재료 빈 검색어 13 · 표 범위 규칙 경계 15 · 단가 빈 품목 3 · tmp/convert_report_36.json) · 검증 대기 · 마지막 대조: 시험입력 · 기본 입력 모두 1,472/1,472(100%) · 빈 단가 = 옛 셈 뜻(그 값 쓰는 식 빈 값 · 비목 합 0 · 끊긴 곳 「단가 없는 품목을 씀」) +- [x] 36-3 오른쪽 패널 — 로직 상세 오른쪽에 흐름도(다이어그램 · 상세 내용 · 원가 · 검증) · 너비 끌어 조절 · 접기 · 「크게 보기」(전체 화면) · 검증완료 +- [x] 36-4 저장 · 정본 — 로직 줄에 「흐름」 칸(화면 [저장] 때만 씀) · 흐름이 있으면 /calc 가 흐름도로 셈(서버 다시 셈) · 왼쪽 기본정보 · 단가산출 상세 · 텍스트 수식 · 시험 계산은 흐름도에서 만든 같은 꼴(보기) · 공용 로직도 흐름도로 고침(시스템 관리자) · 캐시 → [저장] 한 번 · [고친 것 버리기] 함께 · 왼쪽 보기 검증완료(sub5) · 서버 검증완료(sub4) +- [x] 36-5 단추 — 「식으로 새로 만들기」 지움 → 「신규 로직」(빈 흐름도 + 기본정보 · 저장 = 자체 GX 새 키) · 「본떠 만들기」 → 「로직 복사/생성」(흐름도도 복사 · 신규 로직 오른쪽) · 「시험판 ▾」 지움 · 잇기 검증완료(sub1 · accf8ecc4 · abbf61971 · 0d45014e3) — 「식으로 새로 만들기」 · 시험판 ▾ · 시험판 단추 둘 지움 · 단추 차례 ← 로직 목록 · 신규 로직 · 로직 복사/생성 · 로직 지우기 · 고친 것 버리기 · 저장 · 두 칸(LogicLab_Flow) · 오른쪽 패널(흐름 받기 · 고침 → drafts → [저장] 한 번 · 버리기 함께 · 흐름 로직이면 왼쪽 흐름 보기) · 신규 로직 기본정보 칸(LogicLab_Basic) → 저장 = 서버 added 키로 엶 +- [x] 36-6 다른 로직 입력 — 대상 = 마스터 로직 전체(찾기) · 흐름도 로직이면 흐름도 셈 · 아니면 옛 셈 · 목록 · 찾기 · 쪽 · 알약 이음 검증완료(서버 흐름 받기 · 값 셈 길이 main 에 오면 GF 로직 값 확인) +- [x] 36-7 정리 — 「시험판 ▾」 · 시험판 입구 · 흐름도 시작 화면 견본 목록 지움 · 시험이 쓰는 견본은 resources/tester 로 · 시험 저장 길 숨김 · DataFlow · CostFlow 폴더 그대로 · 검증완료 +- [x] 36-8 끝 검증 — 흐름 기준 · 옛 로직 열기 → 오른쪽 흐름도 값 = 왼쪽 계 · 신규 로직 → 저장 → 다시 열기 · 복사 · 권한 · 브레인 실제 검증 · 보고 · 끝 검증완료(sub1 · 사본 폴더 tmp/master_copy_36 + AISLO_MASTER_DIR · 네 포트) — ① 옛 로직 셋: GF000001(찾기) 오른쪽 34,413 = /calc 34,413.6 = 왼쪽 34,414(보임 끝수만 다름) · 변환 알림 한 줄 / GF000007(만약) 0 ≠ 597,285 · GF000288(로직()) 0 ≠ 1,234,760.96 — 아래 흠 ② GF000001 함수 식 ×1.5 → 오른쪽 · 왼쪽 51,620 따라감 → [저장](공용 GF · 사본) → 다시 열기 상자 6 · 변환 알림 없음 · /calc flow 51,620.4 = 화면 ③ 신규 로직 65 걸음(변수 셋 · 함수 · 조건 만들기 · 출력 둘 · 조건 선) → 저장 GX000004 → 다시 열기 상자 4 · 선 4 · 화면 100,000 = /calc 100,000 = 왼쪽 계(높이 2 = 0 · 지반 암 = 50,000) ④ 로직 복사/생성 2 걸음 → GX000005 상자 4 · 선 4 · 100,000 ⑤ 선 조건 두 줄 「지반 = 토사 그리고 높이 ≥ 3 일 때만」 · 값 고르기(토사|암) · 높이 2 → 0 · 또는 → 50,000 · 조건 만들기 =IF(A2>=3,A3,0) ⑥ 식 보기 「결과 1 = 금액 × 높이 ÷ 4」 · IF 만약 / 아니면 줄 · 칸 연결 표 ⑦ 크게 보기 1484×1076 · 거기서 고친 것 패널로 · 접기 40px · 펴기 · 너비 320 → 522 ⑧ 고친 것 버리기 5 → 4 · 캐시 비움 · 로그인 없음 flow · save · calc · logics 401 · 시험판 · 옛 단추 글 0 · resources/master_data 바뀜 0 · 흠(다른 창): 변환 숫자 입력 예시값이 비어 있음(시험입력 · 기본 입력을 안 넣음 — GF000288 나무높이 6 · 집재거리 100 · GF000007 정리량 50 …) · GF000007 「표 QF000033 → 할증」 표에 맞는 줄 없음(서버 477,828 = 할증 25% 빠짐 · 화면 0) · GF000288 다른 로직 GC000270 기계 자리 「EQ:{기계} … 자리」 못 풂(#N/A → 0) · 칸 #N/A 일 때 화면 셈 0 · 서버 셈 그대로(둘이 다름)(sub7) · 식 보기 값 줄이 선 규칙을 지난 값을 안 씀(「(#N/A × 0) = #VALUE!」 · 결과 알약은 34,413.6)(sub2) · 조건 통과 둘째 줄 값 목록 「number:0」 글 · 조건 만들기 만약 목록에 놓인 알약 없음(sub2) · 옛 로직 왼쪽 계 반올림 · 흐름 원가 계금 1원 버림(1원 차이) +- 36 마무리(10-2 02시 · 브레인 실제 검증완료): 전수 변환 대조 시험입력 1,469/1,472(99.8%) · 기본 1,466(99.6%) · 남은 3 = 마스터 빈 단가 · 시험 141 + 변환 시험 초록 · 사본 서버 저장 → 사본만 바뀜 · 다시 열기 흐름 정본 · GF000001 34,414 = 34,414 · GF000003 8.946 = 8.946 · 식 보기 값 줄 · 검증 대기 + +## 37. 일위대가 조합 다이어그램 (사용자 지시 10-2 09시 · 계획 · 착수) + +- 사용자 확정: 「복잡한 경우가 있으니 일위대가 조합도 다이어그램으로 표현 … 중요하게 생각하는 정보가 다르니 잘 구분」 · 조합은 계산하지 않고 로직을 뭉쳐 비용 구분별로 표기 · 입력은 로직별로 묶어 빈 값은 사용자가 넣음 · 출력은 로직마다 · 미리 보기 값은 구조물 조합으로 안 넘김 · 「흐름도는 별도 로직이 아님」(단가산출 로직을 쓰는 방법). +- 모양: 왼쪽 = 로직별 입력 묶음(받아야 할 값 · 빈 값 표시) → 가운데 = 로직 카드(키 · 이름 · 결과 단위 · 식 · 함수 안 보임) → 오른쪽 = 비용 구분 칸 셋(노무비 · 재료비 · 경비 · 로직마다 선으로 모임) + 값 로직은 「값」 칸 · 이름이 같은 입력(지역 · 자재지역 등)은 점선으로 이어 보이기만(검사 없음) · 로직 카드를 누르면 그 로직의 단가산출 로직 화면(흐름도)으로 · 계산 · 고치기 없음(묶음 고치기는 지금 표) · 흐름도와 모양 · 색을 달리함. +- 자리: 일위대가 조합 상세(M02_MasterTemplete_Labor_Edit)의 미리 보기와 같은 값 — 미리 보기 입력 칸에 넣은 값이 다이어그램에도 · 다이어그램 빈 입력 알약을 누르면 미리 보기 그 칸으로 · 미리 보기 결과(로직별 노무비 · 재료비 · 경비 · 계)가 카드 · 칸에. +- [x] 37-1 다이어그램 판 — M02_MasterTemplete_Labor_Diagram.ts(+css) 새 파일 · 고정 세 칸 배치(자동) · 판 옮기기 · 확대 · 한눈에 · 밝음 · 어두움 · 좁은 폭 +- [x] 37-2 잇기 — Labor_Edit 에 「다이어그램」 칸(접기) · 미리 보기와 값 한 벌 · 빈 입력 누름 → 미리 보기 칸 · 로직 카드 누름 → M01 단가산출 로직 그 로직 열기(setPendingLogicKey 꼴) +- [x] 37-3 검증 — 조합 셋(로직 2 · 9 · 14개)으로 입력 묶음 수 = 로직 입력 수 · 출력 = 미리 보기 줄 · 비용 칸 합 = 미리 보기 합 · 빈 값 표시 · 같은 이름 점선 · 로직 열기 · 브레인 실제 검증 · 보고 +- 창: sub2 37-1 · 37-2 · 37-3 + +## 38. 마스터 요소 화면 손질 (사용자 지시 10-2 09:40 · 착수) + +- 사용자 확정: ① 인력 · 재료 · 기계 · 소요량 · 계수 · 환율 · 요율 상세의 제목 · 단추 · 찾기 라벨 양식 = 단가산출 로직과 같게 ② 재료 컨테이너의 유가 · 전력을 별도 컨테이너로 ③ 단가산출 로직 「막힘만」 체크 — 의미 없으면 지움 ④ 단가산출 로직 컨테이너 「전체」 를 누르면 상세 목록 보기가 안 됨(다른 컨테이너는 됨). +- 브레인 정함: 「막힘만」 = 로직 검사에 걸린 로직만 거르기 · 지금 1,469 중 0 · 목록 줄에 「⛔ 막힘」 표시 · 까닭 말풍선은 이미 있음 → 체크만 지움(표시는 둠). +- [x] 38-1 양식 맞춤 — 일곱 상세 화면 머리(제목 · 단추 줄 · 찾기 칸 라벨)를 단가산출 로직 화면 꼴로 · 공용 단추 · 칸 크기(sub5) · 검증완료 +- [x] 38-2 유가 · 전력 컨테이너 — 왼쪽 패널에 재료 다음 「유가 · 전력」 컨테이너(재료_유가전력.json) · 재료 컨테이너에서는 뺌 · 고르기 · 찾기 · 상세 그대로(sub6) · 검증완료 +- [x] 38-3 「막힘만」 체크 지움 · 38-4 「전체」 목록 보기 고침(sub1) · 검증완료 — 원인: 로직 컨테이너 거름이 목록만 거르고 가운데 화면 전환(onLogic)을 안 부름 → Side 거름 run 에서 로직이면 onLogic · 「전체」 는 찾는 글도 비움 · 화면(5174): 로직 1,469 줄 → 인력 전체 → 로직 「전체」 = 로직 화면 1,469 줄(찾는 글 「굴착」 비움) → 산림품셈 330 → 03장 작업장 관리 10 → 로직 열기 → 「전체」 = 상세 닫힘 · 1,469 줄 · 막힘만 체크 0 · 화면 오류 0 +- 「고친 것 버리기」(사용자 물음 · 확인만): 저장 안 한 고친 것(화면 캐시)을 버리고 파일의 로직을 다시 엶 — 사본 저장은 없음 · 그대로 둠(사용자 결정 기다림) From 056ddd882261fb2edfa82a795afd503edee04120 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sun, 4 Oct 2026 17:19:18 +0900 Subject: [PATCH 02/14] auto: 2026-10-04 17:19 (ESD_LAPTOP) --- .../raw/M02_구조물조합개편/사용자요구_1004.md | 50 ++++++++ docs/raw/M02_구조물조합개편/설계_정함.md | 105 +++++++++++++++++ ...가산출_흐름도_조합다이어그램_마스터요소.md | 108 ++++++++++++++++++ 3 files changed, 263 insertions(+) create mode 100644 docs/raw/M02_구조물조합개편/사용자요구_1004.md create mode 100644 docs/raw/M02_구조물조합개편/설계_정함.md create mode 100644 docs/raw/plans/2026-10-04_plan_단가산출_흐름도_조합다이어그램_마스터요소.md diff --git a/docs/raw/M02_구조물조합개편/사용자요구_1004.md b/docs/raw/M02_구조물조합개편/사용자요구_1004.md new file mode 100644 index 000000000..936caa071 --- /dev/null +++ b/docs/raw/M02_구조물조합개편/사용자요구_1004.md @@ -0,0 +1,50 @@ +# M02 구조물 조합 개편 — 사용자 요구 (2026-10-04 · 원문 그대로) + +1번(계획서 32장 이후 정리)은 브레인이 처리함(c8269a969). 7 · 8번은 원문에 없음. + +## 2. 일위대가 조합 → 구조물 조합 하위 + +M02페이지의 마스터 템플릿 페이지에서 일위대가 조합은 구조물 조합의 하위 컨테이너로 넣을 것.(구조물 도면과 면적산출 사이에 배치) +- 일위대가 조합의 좌측 패널의 기존 보기 방식인 트리구조는 필요없으며 구조물 도면 처럼 리스트형태로 보임 +- 추가로 구조물 도면, 일위대가, 면적산출의 최상위에는 전체항목을 넣어주고 우측 상세에는 리스트로 보여줌, M01 항목을 보면 양식화되어 있는 페이지 헤이아웃 있음 + +## 3. 이름 + +이동한 일위대가 조합은 일위대가로 명칭 변경 + +## 4. 구분선 + +구조물 조합 컨테이너 내부의 서브 컨테이너 항목과 트리구조 사이에 수평 구분선 추가 + +## 5. 사용자별 구조물 조합 읽기 / 쓰기 / 복사 권한 점검 및 개선 + +- 시스템 관리자 : 아이슬로 프로그램 관리자로 마스터를 제공하여 회사관리자나 일반 사용자가 사용하기 쉽게 한다. +- 시스템관리자가 대시보드의 컨테이너 제목의 우측의 회사명을 선택함에 따라 변경한 목적은 해당 회사의 정보를 접근하고나 AS을 목적으로 고객을 도와주기 위함임. 복사항목에는 마스터나 시스템 관리자 회사, 개인으로 들어갈 항목 뿐만 아니라 고객 회사의 특정 폴더로 복사할 수 있는 기능이 있어야함. 수정/생성권한 포함 +- 회사관리자 : 고객으로 본인회사의 양식을 만들거나 개인 프로젝트에 사용할 개인 마스터데이터 관리 +- 회사관리자는 개인버튼을 누를경우 최상위 트리에는 다른 개인들의 정보를 보고 복사하여 개인에게 복사하거나 수정, 생성도 가능해야함. +- 개인은 마스터와 회사는 읽기만, 다른 개인의 정보는 볼수 없음. 트리는 현재를 유지해야함. + +## 6. 구조물 조합 항목 변경 + +- 최상위 구조물 조합에 구조물 도면, 일위대가, 면적산출로만 표현(기존의 재료산출의 사용자가 추가하는 재료들과 값은 엑셀을 사용하지 않고 일위대가의 담은로직 처럼 하위에 별도의 컨테이너로 추가되는 자재항목을 넣어야함. 이때 M01자재목록에서 하나씩 가져오되 수량은 면적산출의 알약으로 값을 싱크하거나 별도의 모달창에서 셀을 지정하게 구현) +- 재료산출 항목은 삭제 +- 선택입력 항목은 삭제(구조물 조합에서는 각 옵션의 상세 구조물에 따라 미리 옵션을 다 지정할 생각임으로 그럴필요 없음) +- 면적산출의 미리보기에서 아래 알약 그룹 행을 하이드/쇼 되게 반영(기본값은 하이드), 단, 면적산출 상세에서는 알약 행 보이기가 기본값으로 +- 면적산출 상세 화면의 우측 패널에서 현재의 일위대가 조합 목록을 검색하는 기능이 아니라 현재 구조물도의 일위대가 담은 로직과 추가될 사용자 지정 재료를 보여줄것, +- 일위대가 상세화면에서는 미리보기(보기만/저장안함) 컨테이너는 각 단가산출 로직에 입력이 필요한 정보들임. 구조물별로 하나씩만들어져야함으로 구조물의 일위대가 항목에 옵션값이 저장되야함. 그리고 이값에 대한 지정은 담은 로직 컨테이너의 항목을 누르면 모달창이 나오고 사용자가 입력 및 변경할수 있게함. (하위에 지정된 항목과 값을 작은 글씨로 표현되게 반영) +- 담은 로직에는 기준 단가를 기준으로 넣었을때 나온 값과 수량산출값을 넣었을때 값에 대한 노무비, 재료비, 경비, 합계를 같이 보여줌. 이때 수량산출은 면적산출의 특정 셀값이나 사용자가 수기로 입력한 값이 될수 있음. 이렇게 되면 미리보기 계산 컨테이너 필요 없음. +- 일위대가의 상세 화면의 순서는 담은로직 > 자재목록 > 다이어그램 순서임. +- 면적산출 산출 상세의 우측 패널에 보여져야하는 결과는 결국 일위대가의 담은 로직컨테이너와 자재목록임. +- 추가된 자재목록에도 기준에 대한 단가와, 면적산출 엑셀의 특정 셀과 연결되었을때 나온 단가가 같이 나와야함. + +## 9. 오른쪽 클릭 + +프로그램 B와 M영역의 페이지들에 마우스 오른쪽 클릭을 할 경우 브라우저 옵션 안보이게 할것.(우리 프로그램의 옵션은 허용, 공통양식 사용) + +## 사용자 답 (10-4 · 일위대가 · 원문 그대로) + +일위대가의 경우 +1. 구조물 조합을 복사한 경우 신규 키를 생성하여 만듬. (값과 로직은 일단 같이 복사) +2. 신규 구조물 조합을 생성하는 경우 미리보기 페이지에는 새로만들기와 복사생성하기를 선택하게 하고 복사 생성인 경우에는 해당 리스트를 보여주고 선택하게 보여주는 값은 개인인 경우에는 개인을, 회사관리자인경우 개인, 시스템관리자인 경우 마스터를 기본값으로 보여주고 탭을 눌러 다른 항목에서 가져올수 있는 항목들을 보여준다. +3. 다시 말해 최종적으로는 구조물 조합에 일위대가, 도면, 면적산출이 연되는게 맞으나 특정항목만 다른 값을 복사하여 가져올수 있어야함. 저장이 된 데이터라도 변경가능해야함. +4. 현재 매칭이 안된 데이터는 나중에 삭제할예정이나 마스터 데이터를 수동 생성을 위해서 필요한 데이터들임. 차라리 실무문서라는 항목을 마스터에 별도로 만들어서 나중에 한번에 지우게 할수 있게 반영. diff --git a/docs/raw/M02_구조물조합개편/설계_정함.md b/docs/raw/M02_구조물조합개편/설계_정함.md new file mode 100644 index 000000000..229b350fd --- /dev/null +++ b/docs/raw/M02_구조물조합개편/설계_정함.md @@ -0,0 +1,105 @@ +# M02 구조물 조합 개편 — 정함 (브레인 · 10-4) + +근거 = `사용자요구_1004.md` · 조사 보고 A~H(각 창 `tmp/조사_39-0X.md`). 사용자가 바꿀 수 있음. + +## 1. 자료 틀 (B · sub7) + +- 일위대가 = 구조물 열마다 `labor/<열id>.json` · 번호 `SU-######`(도번 · 면적번과 같은 뒷자리 · 층마다 셈) · 자리 = 시스템 `resources/master_template/labor/` · 회사 · 개인 `…/templates/labor/` · 새 프로젝트 씨앗도 KINDS 로 함께. +- 문서 꼴: + +```json +{ + "종류": "일위대가", "판": 1, "열": "item1", "일위대가번": "SU-000001", "단위": "개소", + "본뜬": { "층": "system", "주인": null, "키": "UA000008" }, + "담은로직": [ + { "로직": "GF000219", "메모": "", "입력값": { "형식": "메쌓기" }, "수량": null }, + { "로직": "GF000137", "메모": "", "입력값": {}, "수량": { "손": 3 } } + ], + "자재": [ { "코드": "MT000008", "이름": "치즐", "규격": "", "단위": "EA", "수량": null } ], + "계산값": { "줄": [], "자재": [], "기준합": {}, "반영합": {} } +} +``` + +- 이름 = 구조물 열 이름(따로 안 둠) · 단위 = 자기 칸 · 같은 로직 두 번 금지 · 빈 일위대가 저장 됨 · 이름 겹침 검사 없음 · 입력값 = 그 로직 입력 이름만(모르는 이름 버림 · 빈 값 = 자동값). +- 수량 = `null`(면적산출 알약 연결 따름 · 로직 알약 「이름|단위」 · 자재 알약 「자재|MT키」 · 칸 연결 정본 = 면적산출 문서 `연결` 한 곳) | `{손: 수}`(손 입력 · 손이 칸보다 앞섬). +- 금액 = 기준(결과단위 그대로 · 원/10㎡ 면 10㎡ 값 · 단위 같이 표기) · 반영 = 기준 × 수량 ÷ 배 · 돈 아닌 로직은 반영 없음 · 「원/1,000본」 배 1000(`_PER` 고침) · 자재 기준 단가 = M01 `master_material.element` 기본값. +- 계산 자리 = 기준 ③ 서버(M01 엔진 · calc 길 · 화면은 최신 응답만) · 반영 곱 ① 짝(`M02_MasterTemplete_Labor_Calc.ts` ↔ `M02_Labor_Doc.py` · 두 파일 머리에 짝 · 거울 시험 · 면적 칸 값이 바뀌면 화면이 바로 곱함) · [저장] 때 서버가 `계산값` 을 다시 적음(브라우저 값 버림 · 목록 합계용) · 상세는 늘 새로 셈. +- 길 = 문서 `GET/PUT /api/m02/layers/{층}/templates/labor/{열}`(양식 길 · owner) · 계산 `POST /api/m02/layers/{층}/labor/{열}/calc` {doc?, 수량?: {키: 값}} → {줄[{로직, 분모, 배, 수량, 수량출처, 기준{노무비,재료비,경비,계}, 반영{…}, 까닭}], 자재[{코드, 기준단가, 수량, 수량출처, 반영, 까닭}], 기준합, 반영합} · 알약 `GET /api/m02/layers/{층}/labor/{열}/pills`(로직 물량 + 자재). +- 집계표 열 `일위대가` · `자재조합` 칸 지움(정본 = labor 파일) · SS 장부 줄 = `일위대가번` · `도번` · `면적번`. +- 옮기기(한 번 · `M02_Labor_Migrate.py`) = 층마다 열마다 labor 파일 · 열 `일위대가` 키 조합의 담은로직 · 단위 복사(`본뜬.키` = 옛 키 · 입력값 {} · 수량 null · 자재 []) · 여러 열이 쓰던 조합(UP10-000001)은 열마다 복사 · 키 없는 열 = 빈 일위대가 · 몇 번 돌려도 같음 · 시스템 층(git)은 바로 · storage 층은 압축 사본 뒤 브레인이 돌림. +- 실무문서 = 마스터 `resources/master_data/일위대가조합.json` 통째(37 줄 · 손대지 않음 · 옮긴 뒤 어느 열도 안 가리킴 · M01 일위대가 조합 화면도 그대로) · 회사 · 개인 옛 조합(UC · UP)은 파일 그대로 · 화면에서 뺌. +- 프로젝트 = 스냅샷 `일위대가.json` 은 labor 문서 + 담은 로직 줄 · `입력값.json`(모양 그대로) 첫 값 = labor 입력값 · `단가.json` 에 자재 MT 줄 · 계산은 반영 같은 함수. +- 은퇴(화면이 안 부른 뒤) = 일위대가 갈라 내기 · 키 적기(use-combo) · 조합 고치기 · 미리 보기 길(preview) · 일위대가 조합 나무 길. +- 로직 지우기 검사 = 모든 층 labor 담은로직 + 실무문서(32장 「로직 지우기 서버 검사」 줄 끝남). + +## 2. 좌측 패널 (A · sub5) + +- 맨 위 「일위대가 조합」 컨테이너 · 조합 나무 · 둘째 층 단추 벌 지움 · 구조물 조합 안 차례 = 층 단추 + 나무 → 수평선 → 구조물 도면 → 일위대가 → 면적산출. +- 하위마다 첫 줄 「전체 항목」(누르면 `onAll(kind)` → H 목록) · 일위대가 줄 = 열마다 「SU-… 이름」 · 누름 = `onOpen({kind: "labor", name: 열})`(C 상세). +- 이름 「일위대가」(하위 제목 · 상세 제목 · 대시보드 카드 글) · 해시 `open=일위대가` = 구조물 조합 펼침 + 일위대가 하위 펼침 + 전체 항목. +- 「실무문서」 = 구조물 조합 밖 별도 컨테이너(도면 양식 아래) · 마스터 층 · 시스템 관리자만 · 목록 + 상세 읽기만(새 파일 `M02_MasterTemplete_Practice.ts`) · [통째 지우기](물음 두 번 · 서버 = 시스템 관리자 · 쓰는 곳 0 확인 · 줄을 비움 · 버튼은 사용자가 누를 때만). + +## 3. 일위대가 상세 (C · sub2) + +- UI_Main 종류 `labor` 로 엶(양식 길 읽기 · [저장] · [다시 읽기] · 캐시 = 다른 양식과 같음) · 차례 = 머리 → 정보(단위 · 본뜬) → 담은 로직 → 자재목록 → 다이어그램 · 미리 보기 칸 지움. +- 담은 로직 줄 누름 = 입력 모달(그 로직 입력 전부 · 빈 칸 자동값 · 수량 = 이은 칸 · 값 보임 / 손 입력 / 「면적산출에서 잇기」 = 면적산출 상세로) · [확인] = 줄 값만(캐시) · 저장은 [저장]. +- 줄 아래 작은 글 「지역 서울 · 규격 25mm · 수량 B6 = 12.5㎡」 · 빈 입력 「빈 값 n」 붉게. +- 금액 칸 넷(노무비 · 재료비 · 경비 · 합계) · 칸마다 두 줄(위 기준 작게 · 아래 반영) · 맨 아래 합계 줄. +- 자재목록 = 코드 · 이름 · 규격 · 단위 · 수량 · 기준 단가 · 반영 금액 · [자재 추가](M01 자재 하나씩 · `M02_Material_Pick.ts`) · 줄 누름 = 수량 모달. +- 다이어그램 = 저장된 줄 값 + calc 답(수량 있으면 반영 · 없으면 기준 · 표기) · 알약 누름 = 입력 모달 · 자재목록 = 카드 하나 → 재료비 칸. +- 보기 갈래(고치기 없음) 부품을 냄 — D 오른쪽 패널 · H 카드가 씀. +- 입력 목록 · 값 받기 부품은 선택 입력에서 옮겨 다시 씀(`M02_Labor_Inputs.py` · `M02_Labor_Values.ts` · `M02_Labor_Values_Model.ts`) · 값 칸 규칙 한 벌(M01 넣을 값) · 로직 입력이 바뀌어 사라진 값은 알림. +- 37장 「미리 보기 값은 구조물 조합으로 안 넘김」 뒤집힘. + +## 4. 면적산출 (D · sub3) + +- 미리보기 카드 알약 줄 켬끔 단추 · 기본 꺼짐 · pref(계정) · 상세는 늘 보임(단추 없음). +- 상세 오른쪽 패널 = 그 열 일위대가의 담은 로직 + 자재목록(C 보기 갈래 · 기준 · 반영) · 조합 목록 · 찾기 · [이 열에 쓰기] · [조합 고치기] 지움. +- 패널 줄 누름 = 칸 지정 모달(`M02_MasterTemplete_AreaLink.ts` · [지금 고른 칸으로] · 주소 · [풀기]) → 면적 문서 `연결` · spreadsheet 손잡이 `setLink(키, 칸|null, 항목)`. +- 알약 = 로직 물량 알약 + 자재 알약(`자재|MT키` · 알약 모드 「자재」 다시 씀) · 선택 입력 알약 빠짐. +- 면적 금액 서버 계산은 1장 calc 하나(따로 안 만듦). + +## 5. 목록 · 고르기 · 카드 (H · sub1) + +- 목록 부품 하나 `M02_MasterTemplete_ItemList.ts`(M01 머리 · 쪽 · 표를 import · 옮기지 않음) — 「전체 항목」 · 복사 생성 · 항목 하나 복사가 같이 씀. +- M02 상세 제목 = M01 꼴(18px · 단추 오른쪽) 전부. +- 목록 칸: 도면 = 도번 · 이름 · 폴더 · 단위 · 고친 날 / 면적산출 = 면적번 · … / 일위대가 = 일위대가번 · 이름 · 폴더 · 단위 · 담은 로직 수 · 합계(`계산값` · 없으면 「—」) · 고친 날. +- 「새 항목」 → 상세 칸에 [새로 만들기] [복사 생성] · 복사 생성 = 같은 칸 탭 + 목록 → 지금 보는 층 · 고른 폴더(없으면 맨 위) · 원래 이름(겹치면 「(2)」) · 일위대가 새 번호. +- 카드 = 도면 · 일위대가 · 면적산출 셋(그 차례) · 빈 카드 = [새로 만들기] [복사 생성] · 찬 카드 = 「복사해 가져오기」(창 · 탭 + 목록 → 확인 → 덮어씀 · 번호 · 열 그대로) · 일위대가 카드 몸 = C 보기 갈래. +- 탭 = 마스터 · 회사 · 개인(나) · 다른 사람(드롭다운 · 맡은 회사일 때) · 실무문서(일위대가 고를 때 · 시스템 관리자) · 기본 = 시스템 관리자 마스터 · 그 밖 개인(회사 없으면 마스터) · 탭 목록 = E `readableSources`. +- 항목 하나 복사 서버 = `copy_from` 에 from_layer · from_owner(B 가 만듦) · 원본이 실무문서 조합이면 조합 → 일위대가 문서(입력값 빈 값). + +## 6. 권한 (E · sub4) + +- 「회사 C 를 맡음」 = 시스템 관리자 · 또는 C 의 회사 관리자 → C 회사 층 · C 모든 개인 층 = 보기 · 만들기 · 고치기 · 지우기 · 복사 대상. +- 일반 = 마스터 · 회사 읽기만 · 남 개인 안 보임 · 나무 그대로 · 프로젝트 apply · sources 도 같은 규칙. +- 사람 나무 = 맡은 회사면 켬 · 회사 사람 전부(나 맨 위 · 빈 사람 「비어 있음」) · 남의 층 첫 쓰기 = 소속 확인 뒤 뼈대. +- 「〇〇로 복사…」 = 쓸 수 있는 층 전부(마스터 · 볼 회사 · 내 개인 · 지금 사람) + 대상 폴더 고르기 창(`M02_MasterTemplete_TreePanel_CopyTo.ts`). +- 남의 층 쓰기 = `system_audit_logs` 한 줄(`M02_OTHER_LAYER_WRITE`). +- 실무문서 = 일위대가 조합 길 전부 시스템 관리자만(서버가 막음). +- 새 길(labor 문서 · calc · pills · 층 넘는 복사)도 `_root` · `_layer_dir` 한 벌을 거침. + +## 7. 지우기 (G · sub6) + +- G1 자재 조합(UM) · G2 재료산출(`material/` · SM · 카드 · 시트 · 옆 판 · 길 · 시험) 먼저 · G3 선택 입력(카드 · `inputs/` · 자리 · 알약 길)은 C 의 입력 모달이 선 뒤. +- `pickMaterial` · `MaterialItem` → `M02_Material_Pick.ts` 로 옮긴 뒤 지움 · 알약 모드 「자재」 · `LinkMaterial`(A00 spreadsheet) 남김 · `고른자재.json` 남김 · 프로젝트 `입력값.json` 남김. +- storage 층(회사 · 개인 · 프로젝트)의 `material/` · `inputs/` · `자재조합.json` 파일은 지우지 않음(읽기만 끊음 · 정리는 사용자) · git 안 시스템 층 · fixture 는 지움. +- 옛 SM 번호 버림 · 장부 `재료번` 칸은 다음 집계표 [저장] 때 빠짐. + +## 8. 파일 주인 (같은 파일 = 몫만 고침 · 받은 뒤 고침) + +| 파일 | 주인 | 다른 창 몫 | +| --- | --- | --- | +| `M02_MasterTemplete_Store.py` · `M02_Template_Layers.py` | sub7 | sub6 = material · inputs 줄 | +| `M02_MasterTemplete_Router_Items.py` | sub4(`_root`) | sub7 = copy 길 from_layer · sub6 = material · inputs 길 | +| `M02_MasterTemplete_Router_Combos.py` · `Combo_Store.py` | sub5(나무 길) | sub4 = 시스템 관리자 문 · sub2 = preview 지움 | +| `M02_MasterTemplete_UI_Side.ts` | sub5 | sub6 = material · matset 줄 | +| `M02_MasterTemplete_UI_Page.ts` | sub5 | sub4 = copyTo · sub1 = onAll 이음 | +| `M02_MasterTemplete_UI_Main.ts` | sub3(면적 길) | sub2 = labor 종류 · sub1 = 제목 꼴 · showView · sub6 = material 길 | +| `M02_MasterTemplete_ItemCards.ts` | sub1(카드 셋 · 빈 카드) | sub3 = 알약 켬끔 · sub6 = 재료 · 선택 입력 카드 | +| `M02_MasterTemplete_Structure.ts` | sub3(pillsFor) | sub1 = pickCopy · sub6 = inputs · material | +| `M02_MasterTemplete_TreePanel.ts`(670줄) | sub1(copyIn · 넘으면 `TreePanel_Clip.ts`) | sub5 = 일위대가 전용 칸 지움 · sub4 = 「〇〇로 복사…」 이음 | +| `M02_MasterTemplete_Labor_*.ts` | sub2 | sub5 = `Labor.ts` · `Labor_Tree.ts` 나무 지움 | +| `M02_MasterTemplete_Api_Fetch.ts` | 각자 자기 함수만 | — | +| `A00_Common/spreadsheet/spreadsheet_links.ts` · `spreadsheet.ts` | sub3 | — | +| 공용 메뉴 · 진입(`main.ts` · `cad_host.ts` · `ui_template_context_menu.*` · `ui_template_tree.*` · `spreadsheet_condition.ts` · `spreadsheet_menu.ts`) | sub8 | — | diff --git a/docs/raw/plans/2026-10-04_plan_단가산출_흐름도_조합다이어그램_마스터요소.md b/docs/raw/plans/2026-10-04_plan_단가산출_흐름도_조합다이어그램_마스터요소.md new file mode 100644 index 000000000..fa33578c6 --- /dev/null +++ b/docs/raw/plans/2026-10-04_plan_단가산출_흐름도_조합다이어그램_마스터요소.md @@ -0,0 +1,108 @@ +# 지난 계획 — 33~38장 (2026-10-01 ~ 10-02) + +단가산출 로직 점검(데스크탑 한시) · 쉬운 단가산출 · 로직 만들기 시험판(세 칸 판 · 시트 로직 · 다이어그램) · 흐름도 시험판 · 흐름도 본 프로그램 적용 · 일위대가 조합 다이어그램 · 마스터 요소 화면 손질. + +## 33. 단가산출 로직 점검 · 수정 (사용자 + 데스크탑 메인 · 보조 · 10-1 한시) + +- 사용자가 데스크탑 메인 · 보조와 직접 진행 · 브레인은 랩탑 sub1~8 로 사용자가 요청하는 다른 일을 맡음(10-1 18:00 무렵까지). +- 근거 = `docs/raw/M01_점검/종합/` · `docs/raw/M01_결정목록/` · `docs/raw/M01_입력목록/` · `docs/raw/M01_고침/`. + +## 34. 쉬운 단가산출 제어 (새 길 · 10-1 · 랩탑) + +- 사용자 확정(10-1 오전): 만든 쉬운 단가산출은 유지(지우지 않음) · 로직 정의 자체를 만드는 판은 제안만 · 허락 뒤 착수 +- 사용자 확정: 지금 단가산출은 그대로 · 「추가」 단추 옆 새 단추로 따로 들어감 · 코드는 `M01_MasterData/EasyCalc/` 한 곳(안 되면 폴더째 지움) · 초등학생도 추가 · 삭제 · 수정 · 시간이 걸려도 검토 먼저. +- 정함(브레인): 새 길은 지금 로직 형식을 그대로 읽고 씀(계산 엔진 안 바꿈) · 폴더 밖 참조는 단추 자리 한 곳만. +- [x] 34-1 검토 — 데이터 · 계산 지도(sub4) · 지금 화면 걸음 재기(sub6) · 실무 모양 + 갈래 비교(sub7) · 지난 시도(sub8) → `docs/raw/M01_쉬운단가/` · 지금 화면 새 로직 38 누름 · 5~6 창 +- [x] 34-2 설계 확정(브레인) — 문장 카드 · 품셈 기본은 「이걸 바탕으로 고치기」 · 쓰기는 `/logic/*` 만 · 1단계 숫자 수량 + 잠금 카드 → `docs/raw/M01_쉬운단가/설계.md` +- [x] 34-3a 단추 + 빈 창(sub1 · 「식으로 새로 만들기」 옆) 검증완료 +- [x] 34-3b 1단계 — Model(sub7) · Api(sub4) · Cards(sub2) · Pick(sub3) · Sheet(sub5) · 틀 · 잇기(sub1) — 설계.md 2 · 3절 · Sheet 검증완료 · Model 검증완료 · Api 검증완료 · Pick 검증완료 +- [x] 34-3c 2단계 — 표 보고 고르기(sub8) · 조건 고치기(sub7 · 조건 줄 93% 읽음) 검증완료 · 카드 단추 잇기(sub2) · 조건 고치기 검증완료 +- [x] 34-4 검증 — 대표 일 여섯 + 예외(빈 값 · 같은 이름 · 저장 안 하고 닫기 · 권한 없음 · 밝음 · 어두움) · 지우기 시험(폴더 밖 참조 줄 수) · 걸음(sub3 고침 뒤) ①21 ②8 ③2 ④4 ⑤2 ⑥3 = 40(목표 36 넘음 4 · ① 새 설계 값 자동값으로 채워 금액 곧장 보임) · 다른 일위대가 넣고 저장 됨 · 권한 없음(일반 사용자) = M01 화면 못 들어감 · /api/m01 403 · 폴더 밖 참조 두 줄 · 검증완료 + +## 35. 단가산출 로직 만들기 판 (제안 단계 · 착수 전) + +- 사용자 확정: 쉬운 단가산출처럼 따로 폴더 시험판 · 로직 몇 개 골라 만듦 · 페이지 보고 판단(마음에 안 들면 지움) · **착수는 허락 뒤** · 인터넷 조사 먼저 · 화면 용어는 단어로 다시 제안. +- 제안서 https://claude.ai/artifact/U4rkgpYWL4Em5VKBEVa4oY (세 칸: 원문 표 · 산출 과정 · 결과) +- [x] 35-0 인터넷 조사 — 견적 프로그램(sub6) · 노코드 규칙 도구(sub8) → `docs/raw/M01_쉬운단가/로직판_조사_*.md` · 제안서 2판에 반영 +- [x] 35-1 용어 단어 제안(제안서 2판 「용어 — 단어로」) → 사용자 확정 · 완료 처리(10-4): 시험판 지움(35-9)으로 접음 +- [x] 35-2 시험판 — 사용자 허락(10-1 · 갈래가 여럿이면 갈래마다 · 목적 = 코딩 없이 원문 보며 확인 · 수정 · 생성) · A 세 칸 판 `LogicBuilder/` · B 엑셀 시트형(우리 스프레드시트) · 먼저 조사: 엑셀 로직 지도(sub4) · A 설계 초안(sub7) → 브레인 A 대 B 판단 → 구현 · 완료 처리(10-4): 35-2a 로 함 +- [x] 35-2a A 세 칸 판(가운데 문장 / 격자 두 보기) — 정함 `docs/raw/M01_쉬운단가/로직판_A_정함.md` · Blocks(sub7) · 서버 · Api(sub4) · 원문 · 검사(sub8) · 문장(sub2) · 격자(sub5) · 단계 안내(sub3) · 틀 · 잇기 · 끝 검증(sub1) · LB 서버 검증완료 · Blocks 검증완료 · Grid 검증완료 · Steps 검증완료 · Source · Check 검증완료 · Wizard 검증완료 · 잇기 · 끝 검증완료(sub1) — ① 원문 대조 6/6 블록 읽힘 · 걸린 줄 · 출처 보임 ② GF000219 표 칸 0.15→0.20 = 79,280→96,759 원(예상과 같음) · 파일 안 바뀜 · 다시 열면 0.15 ③ 단계 안내 11 걸음 + 저장 → 현장 다시 열림 60,984 원(/calc 같음) → 삭제 2 걸음 ④ 빈틈 해당 없음 10 · 못 따진 표 조회 1 · 기대값 시험표 맞음/다름 · 화면 초안만 ⑤ 표 고침 없을 때 lb/calc = /calc 8/8 · 문장 ↔ 격자 같은 값 · 예외(빈 이름 · 저장 안 하고 닫기 · Ctrl+Z · Ctrl+S · 권한 없음 403 · 밝음 · 어두움) · 폴더 밖 참조 LogicLab.ts 두 줄 + main.py 등록 한 줄 +- [x] 35-2b 남은 것(사용자 판단 뒤) — 문장 / 격자 고르기 · 표 칸 고침 저장 여부 · 적용 품셈 판 · 용어 · 칸 역할 「결과」 바꾸기 · 단계 안내 고르기 창 수량 물음 · GX000001(메인 11:53 · 12:02) 주인 확인 · 보고 https://claude.ai/artifact/NqKfHaJULcQo6dSvnsxy1n · 완료 처리(10-4): 시험판 지움(35-9)으로 접음 +- [x] 35-3 점검(확인만) — 표끼리의 관계(로직) 자체를 초등학생이 만들고 · 바꾸고 · 지울 수 있나 · 품셈에 없는 관계 더하기 · 시나리오 여섯(sub6) — 결과: 있는 관계의 값만 고침(초등 중) · 관계 더하기 · 지우기 · 다시 잇기 · 새 입력값 · 새 표 · 로직끼리 바꾸기 안 됨 · 문턱만 바꾸면 계산 멈춤 + 엉뚱한 알림 · `docs/raw/M01_쉬운단가/로직판_관계제어_점검.md` +- [x] 35-4 다이어그램 제안(사용자 생각 10-1) — 상자 = 표 · 입력값 · 고정값 · 계산 · 항목 · 다른 로직 · 결과를 직접 다룸 · 선 = 관계를 만듦 · 품셈에 안 묶임(사용자 표 · 사용자 관계) · 관계 제어 = 다이어그램(사용자) · 위 가로 띠 + 아래 고른 상자 다루기 · 결과 · 관계 = 진짜 엑셀 수식(칸 주소 · 함수 · 사용자 확정) · 알약 = 입출력 칸 표시 · 새 종류 「시트 로직」(TS 엔진 한 벌 · 서버 Node) · 제안서 https://claude.ai/artifact/4iCAGPhE9maauGNTkXg5he · 허락 전 착수 금지 · 완료 처리(10-4): 35-5 · 35-7 로 함 +- [x] 35-5 시트 로직 시험판(사용자 허락 10-1 오후) — 위 가로 다이어그램 · 상자 = 시트 · 관계 = 엑셀 수식 · 입출력 = 알약 · 저장 = `M01_MasterData/SheetLogic/_data/`(git 밖 · 폴더째 지움) · 정함 `docs/raw/M01_쉬운단가/시트로직_정함.md` · Model · Eval(sub7) · 서버 · Api(sub4) · Diagram(sub8) · Panel(sub2) · Table(sub3) · Item · Result(sub5) · Import(sub6) · 틀 · 잇기 · 끝 검증(sub1) · SL 서버 검증완료 · Model 검증완료 · Diagram 검증완료 · Item · Result 검증완료 · Table 검증완료 · Import 검증완료 · 잇기 · 끝 검증완료(sub1) — 걸음: S1 ①②③ 각 3(79,280 · 73,085 손셈 같음 · 2.5m 문턱은 품셈 표 3m 부터라 2.5~3m 해당 없음) · S2 ① 3 ② 4(찰쌓기 표로) ③ 5 · S3 ① 약 19(경사 급 65,018) ② 9(내 표 붙여넣기 · 할증 10) ③ 7(잡재료 2% 17,551) · S4 선 끊기 2 · 상자 지우기 2(물음 한 번) · Ctrl+Z 되돌림 · S5 0(상자 · 선 · 값 한 장 · 고르면 이어진 선 진하게) · S6 항목 일위대가 11 · 다른 로직 상자 13(덤프트럭 16.2) · 새 로직(품셈 없이) 25 걸음 → 저장 서버 36,000 = 화면 → 다시 열림 → 지우기 2 · 서버 = 화면 4/4(36,000 · 17,550.9 · 10.12 · 다시 열기) · 가져오기 6/6 /calc 같음(0 · 60,984/79,280 · 2,064,816 · 670 · 16.2 · 57,516) · 예외(빈 이름 · 같은 상자 · 순환 · 빈 입력 물음 · 저장 안 하고 닫기 · 권한 없음 sl 5/5 403 · 밝음 · 어두움) · 세 칸 안쪽 세로 밀기 없음(6개 모든 상자) · 폴더 밖 참조 LogicLab.ts 2 + main.py 1 · _data 비움 · 남은 흠(다른 창): 가져온 표 상자 + 결과 칸 목록 예시값이 글자로 들어가 30 등에서 해당 없음(sub5) · 가져온 로직 화면 번호 · 자재 검색어 · 끝 검증완료(S1~S6 · 새 로직 · 서버 = 화면 4/4 · 가져오기 6/6) · 남은 흠: 결과 칸 예시값 선택이 글자 「30」 으로 들어가 표 찾기 해당 없음(sub5 · 사용자 지시 뒤) +- [x] 35-5a 사용자 요청 — 다이어그램 띠 · 고른 상자 시트 · 결과 높이를 안쪽 세로 스크롤이 없을 때까지(sub1) +- [x] 35-6 로직 문법 먼저(사용자 방향 10-1 14시 무렵 · 화면보다 로직) — 바탕 표(인력 378 · 재료 30,767 · 기계 654 · 소요량 2,535 · 계수 498 · 환율 · 요율 · 사용자 표 — 보기 · 만들기 · 비목은 표가 정함) · 단위 넷(값 · 표 · 정보 · 요소) + 묶음(세분화 · 묶기 · 분류 · 묶음째 잇기 · 겹 묶음 · 틀 다시 쓰기) · 흐름 셋(정보 만들기 · 잇기[그대로 · 바꿔서 · 갈라서 · 모아서 · 묶음째] · 내기) · 확인 도구 · 제안서 https://claude.ai/artifact/BFWvm1Q4fhCSZ4Rt34UgYY · 다음 = 문법 확정 → 종이 연습(절 10개) — 허락 전 착수 금지 · 완료 처리(10-4): 35-7 · 35-9 흐름도로 갈음 +- [x] 35-5b 단추 공용 양식(점검 끝 · 고침은 사용자 지시 뒤) — 날 단추 23(목록 줄 8 · 칩 · 역할 · 보기 바꿈 11 · ▾ 2 …) · sm 단추를 44px 로 덮기 3 · 큰 시작 56px 3 · `docs/raw/M01_쉬운단가/시험판_단추_점검.md` · 완료 처리(10-4): 시험판 지움(35-9)으로 접음 +- [x] 35-7 단가산출 다이어그램 시험판(사용자 기준 10-1 14시 무렵) — 입력 · 함수 · 출력 상자 · 자유 배치 · 함수 = 엑셀 · 출력 하나 · 출력 = 비용 종류 · 원가 · 폴더 `M02_MasterTemplete/CostFlow/`(_data · _sample) · 정함 `docs/raw/M01_쉬운단가/원가다이어그램_정함.md` · Model · Eval(sub7) · 서버(sub4) · Diagram(sub8) · 함수 판(sub2) · 입력 판(sub3) · 출력 · 원가(sub5) · 견본(sub6) · 단추 · 페이지 · 잇기 · 끝 검증(sub1) · Model 검증완료 · Output · Summary 검증완료 · Sample 검증완료 · FuncPanel 검증완료 · Diagram 검증완료 · CF 서버 검증완료 · InputPanel 검증완료(사용자 요소 만들기는 서버 Api 가 main 에 오면 확인) · 잇기 · 끝 검증완료(sub1) — ① 빈 판에서 손으로 메쌓기 꼴(변수 4 · 소요량 표 2 · 인력 2 · 함수 증가율 → 석공 / 보통인부 병렬 · 출력 노무비 2) 9 상자 · 약 138 손놀림 → 원가 148,471 = 견본(증가율 40 · 석공 97,882.512 · 보통인부 50,587.992 손셈 같음) ② 비용 종류 바꾸면 148,471 이 노무비 → 경비 → 재료비 로 옮겨감 · 「값」 이면 원가에서 빠짐(각 1 걸음) ③ 상자 끌어 옮김(1520,190 → 1675,306) · 저장 → 다시 열면 그대로 ④ 함수 출력 하나(다른 칸 고르면 옮겨감 · 알약 하나) · 순환 막힘 · 출력에서 나가는 선 없음 · 빈 받는 자리는 저장 때 물음 ⑤ 사용자 요소(시험 보조공 200,000) 5 걸음 · 마스터 그대로 · _data 에만 ⑥ 저장 때 서버 = 화면 2/2(148,470.504) ⑦ 단위 오른쪽 위(- → 원/㎡) · 색 바꾸기(Entry 판 머리) · 「다이어그램」 이름 · 다이어그램 726px · 안쪽 스크롤은 함수 격자(가상 200줄)만 · 밝음 · 어두움 · 권한 없음(cf 길 6/6 403 · 화면 못 들어감) · 견본 셋 = /calc · 손셈(148,471 · 3,000 · 15.552) · 폴더 밖 참조 LogicLab.ts 2 + main.py 1 · _data 비움 · 끝 검증완료(손으로 메쌓기 9 상자 = 견본 148,471 · 비용 종류 옮김 · 자리 저장 · 규칙 · 사용자 요소 · 서버 = 화면 · 견본 셋 손셈) · 함수 출력 기본 이름 겹침 고침(sub7 2be767d18) · 남은 것: M01 단가산출 단추 줄이 셋째 줄까지(시험판 단추 넷 · 사용자 결정) +- [x] 35-7a 브레인 실제 검증(10-1 16:45) 흠 여섯 고침 — ① 고칠 것 칸 지난 경고 · ⑥ 시험판 단추 「시험판 ▾」 로 묶기(sub1) · ② 알약 쉼표 · ③ 표 알약 글 · ⑤ 맞춤 확대 90% 아래 안 감(sub8) · ④ 입력 상자 공통 단위(sub3) · 다시 검증완료(16:55 · 고칠 것 칸 비어 있음 · 알약 쉼표 · 표 · 40줄 · 인력 원/일 · 확대 90% · 막대 한 줄 · 시험판 ▾ 넷 · 원가 148,471) + +## 35-8. 단가산출 다이어그램 2판 — 재료 선택 자리 · 선 규칙 (계획 · 허락 전) + +- 사용자 확정: 고르는 방식은 재료만(다른 그룹은 상자를 늘림) · 설계 때 입력은 오른쪽 아래 검증 칸 · 나중에 프로젝트에서 일위대가로 쓸 때 재료는 목록에서 고르거나 단일 값 · 표에 따라 출력을 가르는 것은 엑셀 수식 대신 선 규칙. +- 정함(브레인): 재료 요소 = 고정 · 선택 자리 · 조건 찾기 · 알약 이름 = 자리 이름 · 선 규칙 = 선 하나에 하나(표에서 찾기 · 조건 통과 · 바꾸기) · 규칙은 받는 칸 엑셀 수식으로 바뀌어 엔진 한 벌 · 선 위 규칙 글 늘 보임. +- [x] 35-8a 틀(Model) — 재료 요소 방식 · 범위(구분 · 상세구분 · 찾는 말) · 조건(규격 ← 알약) · 기본 품목 · 선 규칙 칸 · 규칙 → 받는 칸 수식 · 조건 선 겹침 · 안 맞음 검사 — node 시험(sub7) · 완료 처리(10-4): 35-9 에서 함 +- [x] 35-8b 서버 — 선택 자리 기본 품목 · 조건 찾기 단가(맞음 0 · 여럿 알림) · 「설계 때 정할 것」 목록(변수 + 재료 자리) 내보내기 — 시험(sub4) · 완료 처리(10-4): 35-9 에서 함 +- [x] 35-8c 입력 판 — 재료 방식 · 범위 · 조건 규칙 · 기본 품목 · 찾기 고침(쪽 넘기기 · 띄어쓰기 AND · 규격까지) — ORCA(sub3) · 완료 처리(10-4): 35-9 에서 함 +- [x] 35-8d 검증 칸 — 재료 선택 자리를 변수처럼(범위 안 목록 고르기 · 찾기) · 조건 자리는 잡힌 품목 표시 — ORCA(sub5) · 완료 처리(10-4): 35-9 에서 함 +- [x] 35-8e 선 규칙 — 선 위 규칙 글 · 선 누르면 아래 판에서 규칙 고르기(표 찾기: 조건 칸 ↔ 알약 · 꺼낼 칸 / 조건 통과: 알약 · 같음 · 다름 · 이상 · 이하 · 값 / 바꾸기: × · ÷ · +% · 단위 · 끝수) · 고른 상자 판에 들어오는 선 규칙 목록 — ORCA(sub8 · 판 sub2) · 완료 처리(10-4): 35-9 에서 함 +- [x] 35-8f 견본 · 끝 검증 — 견본 둘 더(지반에 따라 굴삭기 / 브레이커 가르기 · 흄관 관경 조건 찾기) · 사용자처럼: 함수 상자 없이 표 찾기 · 가르기 · 검증 칸에서 재료 고르기 · 방법 걸음 수(sub6 견본 · sub1 잇기 · 끝 검증) · 완료 처리(10-4): 35-9 에서 함 +- [x] 35-8g 다이어그램 마우스 — 판 옮기기 = 가운데 단추 끌기 · 고르기 = 왼쪽 누름 · 왼쪽 끌기로 글자가 선택되지 않음(다이어그램 영역 글자 선택 막음) · 상자 옮기기는 상자 머리 왼쪽 끌기 그대로 — ORCA(sub8) · 완료 처리(10-4): 35-9 에서 함 +- [x] 35-8h 「고른 상자」 → 「상세 내용」 · 상자뿐 아니라 알약 · 선을 골라도 내용이 바뀜(알약 = 값 · 단위 · 어디서 오나 · 어디로 가나 / 선 = 선 규칙 고르기) — ORCA(sub1 잇기 · 알약 · 선 판 sub2) · 완료 처리(10-4): 35-9 에서 함 +- [x] 35-8i 상세 내용 단추 · 칸 크기 — 함수 판 꾸밈이 공용 작은 단추를 44px 로 덮는 줄(`M02_CF_FuncPanel.css` `.m01-cfpanel .ui-btn …`)을 지우고 판 안 단추를 모두 공용 작은 크기(createButton size "sm")로 · 입력 · 고르기 칸도 공용 크기 — ORCA 수치(sub2 · sub3 · sub5 각자 판) · 완료 처리(10-4): 35-9 에서 함 +- [x] 35-8j 상세 내용 머리 — 왼쪽 「상세 내용」 · 오른쪽에 고른 상자 제목 · 알약을 골랐으면 그 오른쪽에 알약 이름(예 「높이 증가율 찾기 › 증가율」) · 상세 내용 안 칸 차례 정리(머리 · 상자 설정 · 본문 · 경고) — ORCA(sub1) · 완료 처리(10-4): 35-9 에서 함 +- [x] 35-8k 단위는 고르기 — 단위 칸을 목록 고르기로(치면 걸러져 보이고 고르기는 사용자 · 손으로 쓴 글은 안 받음) · 목록 = 단위 표준(35-8l) 단위 + 마스터 결과단위 + 금액 단위(원) · 「-」 = 없음 · 함수 · 출력 · 변수 · 사용자 요소 단위 모두 같은 고르기 — 공용 고르기 파일 `M02_CF_UnitPick.ts`(sub2) · 판마다 바꿔 끼움(sub2 · sub3 · sub5) · 완료 처리(10-4): 35-9 에서 함 +- [x] 35-8l 단위 표준 표 — 산림품셈 1-2-2 「설계서의 단위 및 소수의 표준」(원문 `resources/knowledge/original/원가계산/산림_표준품셈/본문/제01장_적용기준/1-02_설계_및_수량.md` · 종목 약 70 · 규격 단위 · 소수자리 · 단위수량 단위 · 소수자리 · 비고 · 주 ①~③)을 표로 — 소수자리 「-」 = 0 · 단위 「-」 = 없음 · 원문 흠은 사용자가 고침(시험판에서 보고 고치는 표 · 저장 `CostFlow/_data` · 처음 값 `CostFlow/_standard/`) — 시험(sub6) · 완료 처리(10-4): 35-9 에서 함 +- [x] 35-8m 금액 단위 표준 — 같은 절 「금액의 단위표준」(설계서 총액 1,000원 · 소계 · 금액란 · 일위대가표 계금 1원 · 일위대가표 금액란 0.1원 · 모두 미만 버림)을 같은 표 자리에 — 출력 상자(비용) 금액 기본 = 일위대가표 금액란 0.1원 미만 버림 · 원가 합계 기본 = 계금 1원 미만 버림 · 바꿀 수 있음(sub6 표 · sub5 출력 · 원가) · 완료 처리(10-4): 35-9 에서 함 +- [x] 35-8n 끝수 고르기 — 함수 상자 출력 · 출력 상자에 「끝수」(소수자리 · 반올림 / 올림 / 버림) · 단위를 고르면 단위 표준에서 소수자리를 제안(주 ① 반올림 · ② 항목 값이 우선) · 고른 끝수는 출력 칸 수식(ROUND 계열)으로 — 엔진 한 벌(sub7 틀 · sub2 함수 판 · sub5 출력 판) · 완료 처리(10-4): 35-9 에서 함 +- [x] 35-8o 로직끼리 잇기 · 흐름 따라가기 — 저장한 다른 로직을 입력 상자로 불러 그 출력 알약을 받음(일위대가 → 일위대가) · 알약 · 선을 고르면 값이 어디서 와서(출처 사슬) 어디로 가는지(가는 곳) 다이어그램에서 길을 칠하고 상세 내용에 줄로 · 값이 끊긴 곳(빈 받는 자리 · 통과 안 된 조건 · 표에 없는 조건) 표시 — 시험 · ORCA(sub7 틀 · sub8 다이어그램 · sub1 상세 내용) · 완료 처리(10-4): 35-9 에서 함 +- 차례(사용자 10-1 · 「데이터의 흐름과 값의 연결에 집중」): **1단계 = 흐름 · 연결** a 틀 · b 서버 · e 선 규칙 · o 로직끼리 · 흐름 따라가기 · h 알약 · 선 고르기 · c · d 재료 자리 · f 견본 · 끝 검증(흐름 기준 — 값이 어디서 와서 어떤 규칙을 지나 어디로 가는지) / **2단계 = 값의 뜻 · 보기** k 단위 고르기 · l 단위 표준 · m 금액 표준 · n 끝수 · j 머리 · g 마우스 · i 단추 + +## 35-9. 단가산출 흐름도 시험판 (사용자 지시 10-1 17시 무렵 · 브레인이 끝까지 · 35-8 전체) + +- 사용자 확정: 브레인이 전체 제어해 새 시험판 하나 · 단가산출 다이어그램(시험)만 남기고 옛 시험판 셋 삭제 · 새 폴더(필요 없으면 바로 지움) · 데이터 흐름 · 값 연결 먼저 · 자유도 높고 이해하기 쉬운 화면 · 끝까지 구현 · 검증. +- 정함 `docs/raw/M01_쉬운단가/흐름도_정함.md` · 폴더 `M02_MasterTemplete/DataFlow/` +- [x] 35-9 창 여덟 — 옮기기 · Model · Eval(sub7) · 옛 시험판 지우기 · 들어가는 길 · 상세 내용 머리 · 잇기 · 끝 검증(sub1) · 서버(sub4) · 다이어그램(sub8) · 상세 내용 판 · 단위 고르기(sub2) · 입력 판(sub3) · 출력 · 검증 칸(sub5) · 표준 표 · 견본(sub6) · 표준 · 견본 검증완료 · Model 검증완료 · 출력 · 검증 칸 검증완료 · Diagram 검증완료 · 입력 판 검증완료(서버 자재 찾기 · UnitPick 이 main 에 오면 다시 확인) · DF 서버 검증완료 · 잇기 · 끝 검증완료(sub1) — ① 빈 판에서 함수 상자 없이 네 흐름 73 걸음: 재료 조건 찾기(관경 → 흄관 자리 · 13 개 중 하나 · 125,870) 21 · 선 규칙 표에서 찾기(높이 증가율표 → 출력) 19 · 조건 통과 두 갈래(지반 = / ≠ 토사) 27 · 다른 로직 잇기(견본 지반 · 지반 알약 이음) 6 ② 알약 · 선 고름 → 온 길 · 가는 길 칠함 + 상세 내용 줄(지반 → 다른 로직 · 두 조건 선 · 흄관 ← 관경) ③ 끊긴 곳 넷 보임(빈 받는 자리 · 통과 안 됨 · 표에 맞는 줄 없음 · 맞는 재료 없음 — 알약 · 선 「!」 · 끊긴 곳 줄 누르면 그 자리 고름) ④ 검증 칸 관경 600 → 800 = Φ800 품목 215,530 · 지반 토사 → 암 = 내 두 갈래 뒤집힘 + 다른 로직 2,000 → 10,000 · 다른 로직 설계 값 바꿈 10,000 → 20,000 ⑤ 단위 고르기 「mm」 = 3 개로 걸러짐 · 손글 안 받음 · 끝수 80,000.37 = 기본 225,870 / 1원 올림 225,871 · 표준 표 고침 저장(3 → 2) → 처음 값으로 되돌림 ⑥ 가운데 끌기 = 판 옮김(−60, −30) · 왼쪽 끌기 = 판 그대로 · 글자 선택 막힘 · 단추 18px 모두 sm · 칸 30px ⑦ 저장 → 다시 열기 = 상자 9 · 선 6 · 자리 · 규칙 글 그대로 · 서버 다시 셈 = 화면 187,910 · 견본 다섯 사본 저장 서버 = 화면(148,470.504 · 2,000 · 84,680 · 3,216.6 · 값 15.552) · 밝음 · 어두움 · 로그인 없음 df 길 6/6 401(관리자 아닌 계정 403 은 계정 없어 못 함) · 폴더 밖 참조 LogicLab.ts 2 + main.py 1 · _data 비움 · 검증 중 고친 내 흠(047f3a279 · 28469de62): 재료 조건 찾기 · 선택 자리 값을 서버에 안 물음(흄관 견본 관경 600 → 400 = 211,700 → 116,600 · 1m 84,680 → 46,640 · 1000 = 235,120 · 선택 자리 Φ600 → Φ400 = 211,700 → 116,600) · 「상자 이름」 칸(입력 · 함수 · 같은 이름 · 빈 이름 막음 · 출력은 출력 판) · 머리 이름 · 단위 칸 작은 크기(30px) · 「새로 만들기」 단추 76px(칸 폭 710 아님) · 표준 표 모달 제목 한 번 · 남은 흠(sub2): 선 규칙 표에서 찾기 조건 칸에 범위 이름(「높이」)이 없어 범위 찾기를 화면에서 못 만듦(높이 5 → 줄 없음 · 4 만 맞음) · 브레인 실제 검증완료(흠 14 고침 · 흐름도 시험 81 · 견본 다섯 서버 = 화면 · 보고 JgfqRzLPYgqXG7vB2cV1Yj) · 검증 대기 + +## 36. 단가산출 흐름도 본 프로그램 적용 (사용자 지시 10-1 21:40 · 결정 반영 22시 · 착수 승인 22:20 · 밤새 진행 · 아침 보고) + +- 사용자 확정: 흐름도 = 최종 · 로직 목록에서 로직을 열면 지금 내용 그대로 + 오른쪽 흐름도 패널(같은 로직) · 「식으로 새로 만들기」 지우고 「신규 로직」(흐름도 새로 만들기) · 「본떠 만들기」 → 「로직 복사/생성」(신규 로직 오른쪽) · 시험판 단추 · 입구 지움 · DataFlow 폴더 유지. +- 근거(마스터 로직 57 파일 · 1,472 로직 · 입력 3,518 · 중간 식 3,168 · 호표 줄 5,265): 식 말 = 찾기(표 · 조건 · 칸) 8,152 · 만약 3,532 · 또는 185 · 최대/최소 173 · 올림/반올림/버림 141 · 로직() 41 → 흐름도 표에서 찾기 · 조건 · 함수 · 다른 로직 입력으로 옮길 수 있음. +- 사용자 결정 D1(그대로): 「품셈원문도 제대로 구현된게 아니기 때문에 내가 흐름도까지 만들어서 로직을 볼려는 거잖아. 그러니 당연히 둘다 같은 내용 … 원문과 대조하면서 검증할때 잘못된게 있으면 흐름도에서 수정할거야」 → 한 로직 · 두 보기: 왼쪽 지금 내용 = 보기 · 고치기는 흐름도(공용 로직 포함 · 시스템 관리자) · 흐름도로 저장한 로직은 흐름도가 정본 · 왼쪽은 흐름도에서 만든 같은 꼴 · 아직 저장 안 한 로직은 지금 내용에서 자동으로 그린 흐름도(값 대조로 같음). +- 사용자 결정 D2(그대로): 「흐름에 대한 로직은 costflow폴더에 있을테니 일단은 그냥 두고 쓸대없는 견본과 버튼만 삭제」 → CostFlow 폴더 그대로 · 시험판 단추 · 견본 목록 지움(흐름도는 DataFlow 폴더만으로 돌아감 · CostFlow 를 부르지 않음). +- 사용자 결정(함수 상자): 「함수박스를 기준으로 하나의 결과가 나가는게 맞는것같아. 여러개가 동시에 나가면 혼란만 가중」 → 함수 상자 하나 = 결과 알약 하나(그대로) · 식은 상자 겉에 넣지 않음(상자가 커짐) · 함수 상자 · 선을 고르면 상세 내용에 사칙연산 · 조건이 보이고 고치면 바로 따라감. +- 칸 연결 = M02 방식 그대로(칸을 고른 채 알약을 눌러 하나씩 연결 · 식은 칸 주소 · `spreadsheet_links.ts` 직접 확인) · 계산 엔진 · 이름 풀기 · 이름 규칙 없음 — 보기만 칸 주소를 연결된 알약 이름으로 바꿈. +- 사용자 결정(조건): 「조건문은 현재를 유지하는게 보기 편한것같고 대신 어떤값을 지정할지는 입력된 리스트에서 선택 … 조건 값을 내가 넣으면 오기 … 복수 조건문도 넣어주고」 · 「조건 만들기도 엑셀 기능 내부에서 존재해야해」 → 선 조건은 지금 웹앱 모양 · 조건 값은 고른 알약이 가질 수 있는 값 목록에서 고름(변수 고르기 · 표 칸 값 · 재료 규격 등 · 목록이 없는 수는 숫자 칸 + 범위 검사) · 조건 여러 줄(그리고 / 또는) · 함수 상자 엑셀 시트 안 「조건 만들기」(칸 고름 → 만약 · 아니면 만약 · 아니면 줄 → 그 칸에 IF 식 · 값은 같은 목록에서 고름). +- 안전: 화면 시험 저장은 마스터 사본 폴더에서만(서버가 환경 변수로 사본 폴더를 씀 · 실제 resources/master_data 는 안 바뀜 · git status 로 확인). +- 창: sub2 36-0(판 · 조건 만들기) · sub7 36-0b(조건 틀 · 값 목록) → 36-2 · sub6 36-1 → 36-7 · sub8 36-3 · sub4 36-4 서버 · 사본 폴더 · sub5 36-4 왼쪽 보기 · sub1 36-5 · 잇기 · 36-8 · sub3 36-6 +- 브레인 정함(조사 뒤 · 아침 보고에 알림): 옛 로직 「기계 · 직종 · 유가 자리」(EQ:{기계} · LB:{직종} · MO:경유:{지역} · 입력값으로 요소를 고름 · 71 로직)는 새 모드 없이 이미 있는 「조건 찾기」(이름 · 키 같음 · 변수 알약)를 인력 · 기계 · 유가 요소에도 씀 · 대조 입력 = check_master.시험입력 · 로직 셈을 부르는 곳(Store.calc · text · master_combo.run · M02_Item_Prices.calc · /logic/auto)은 한 곳에서 흐름 갈래. +- [x] 36-0 함수 상자 안 보기 · 칸 연결 조정(36-2 보다 먼저) — ① 상세 내용 「식 보기」: 결과 칸 식을 칸 주소 → 연결된 알약 이름으로(예: 1m 재료비 = 흄관 단가 ÷ 2.5) + 값 들어간 식(= 211,700 ÷ 2.5 = 84,680) · IF 는 만약 / 아니면 줄 · 지금 타는 갈래 표시 · 중간 칸 식도 차례대로 · 칸 식을 고치면 바로 다시 셈 → 다이어그램 · 원가 따라감 ② 「칸 연결 표」(칸 · 연결 알약 · 입력 / 결과 · 이어진 선 · 표에서 바로 바꿈 · 풀기) + 칸 색(입력 파랑 · 결과 초록 · 상자 색과 같음) · 결과 칸 하나 ③ 선을 고르면 그 선 규칙도 같은 「식 보기」 꼴(예: 굴착량 → 지반 = 토사 일 때만 → 굴삭기로 파기) ④ 엑셀 시트 안 「조건 만들기」 — 칸을 고르고 열면 만약 [알약] [비교] [값 고르기] → [알약 · 값] 줄 · 아니면 만약 더하기 · 아니면 → 그 칸에 IF 식(엑셀로 쓴 IF 도 같은 줄로 다시 열림 · 못 바꾸는 식은 식 그대로) · 검증완료(sub2 · 식 보기 · 칸 연결 표 · 조건 만들기 · 선 조건 여러 줄 · 시험 8) · 검증 대기 +- [x] 36-0b 선 조건 손질 — 조건 값 = 고른 알약의 값 목록에서 고름(Model 값 목록 함수 · 변수 고르기 · 표 칸 값 · 재료 규격 · 목록 없는 수는 숫자 칸) · 조건 여러 줄(그리고 / 또는) · 규칙 → 받는 칸 엑셀 식 · 서버 Compute 같은 뜻 · 선 위 이름표 · 겹침 · 안 맞음 검사 그대로 · 검증완료(sub7 · 조건 여러 줄 그리고/또는 · 값 목록 · 서버 = 화면 · 시험 30) · 검증 대기 +- [x] 36-1 조사 — 옛 식 문법 전수(서버 파서 · 요소 부르기 꼴 · 따옴표 글) · 변환 못 하는 꼴 목록 · /calc 를 부르는 곳 · 검증완료 +- [x] 36-2 변환 — 옛 로직(입력 · 중간 · 호표 · 덧줄 · 결과 · 끝수) → 흐름도(입력 상자 · 표 알약 + 표에서 찾기 · 함수 · 출력 상자) + 자동 배치 · 서버 파서로 · 옛 식에 나온 이름마다 알약을 칸에 연결하고 식은 칸 주소로(만약 → IF · 최대 → MAX · 찾기 → 표에서 찾기) · 함수 상자 하나 = 결과 하나 · 전수 대조(변환 흐름도 값 = 옛 계산 값 · 맞는 비율 · 안 맞는 까닭) · 검증완료(sub7 · M02_DF_Convert · 전수 1,472 시험입력 96.7% · 기본 입력 97.4% · 안 맞는 49 = 재료 검색어 자리 18 · 재료 빈 검색어 13 · 표 범위 규칙 경계 15 · 단가 빈 품목 3 · tmp/convert_report_36.json) · 검증 대기 · 마지막 대조: 시험입력 · 기본 입력 모두 1,472/1,472(100%) · 빈 단가 = 옛 셈 뜻(그 값 쓰는 식 빈 값 · 비목 합 0 · 끊긴 곳 「단가 없는 품목을 씀」) +- [x] 36-3 오른쪽 패널 — 로직 상세 오른쪽에 흐름도(다이어그램 · 상세 내용 · 원가 · 검증) · 너비 끌어 조절 · 접기 · 「크게 보기」(전체 화면) · 검증완료 +- [x] 36-4 저장 · 정본 — 로직 줄에 「흐름」 칸(화면 [저장] 때만 씀) · 흐름이 있으면 /calc 가 흐름도로 셈(서버 다시 셈) · 왼쪽 기본정보 · 단가산출 상세 · 텍스트 수식 · 시험 계산은 흐름도에서 만든 같은 꼴(보기) · 공용 로직도 흐름도로 고침(시스템 관리자) · 캐시 → [저장] 한 번 · [고친 것 버리기] 함께 · 왼쪽 보기 검증완료(sub5) · 서버 검증완료(sub4) +- [x] 36-5 단추 — 「식으로 새로 만들기」 지움 → 「신규 로직」(빈 흐름도 + 기본정보 · 저장 = 자체 GX 새 키) · 「본떠 만들기」 → 「로직 복사/생성」(흐름도도 복사 · 신규 로직 오른쪽) · 「시험판 ▾」 지움 · 잇기 검증완료(sub1 · accf8ecc4 · abbf61971 · 0d45014e3) — 「식으로 새로 만들기」 · 시험판 ▾ · 시험판 단추 둘 지움 · 단추 차례 ← 로직 목록 · 신규 로직 · 로직 복사/생성 · 로직 지우기 · 고친 것 버리기 · 저장 · 두 칸(LogicLab_Flow) · 오른쪽 패널(흐름 받기 · 고침 → drafts → [저장] 한 번 · 버리기 함께 · 흐름 로직이면 왼쪽 흐름 보기) · 신규 로직 기본정보 칸(LogicLab_Basic) → 저장 = 서버 added 키로 엶 +- [x] 36-6 다른 로직 입력 — 대상 = 마스터 로직 전체(찾기) · 흐름도 로직이면 흐름도 셈 · 아니면 옛 셈 · 목록 · 찾기 · 쪽 · 알약 이음 검증완료(서버 흐름 받기 · 값 셈 길이 main 에 오면 GF 로직 값 확인) +- [x] 36-7 정리 — 「시험판 ▾」 · 시험판 입구 · 흐름도 시작 화면 견본 목록 지움 · 시험이 쓰는 견본은 resources/tester 로 · 시험 저장 길 숨김 · DataFlow · CostFlow 폴더 그대로 · 검증완료 +- [x] 36-8 끝 검증 — 흐름 기준 · 옛 로직 열기 → 오른쪽 흐름도 값 = 왼쪽 계 · 신규 로직 → 저장 → 다시 열기 · 복사 · 권한 · 브레인 실제 검증 · 보고 · 끝 검증완료(sub1 · 사본 폴더 tmp/master_copy_36 + AISLO_MASTER_DIR · 네 포트) — ① 옛 로직 셋: GF000001(찾기) 오른쪽 34,413 = /calc 34,413.6 = 왼쪽 34,414(보임 끝수만 다름) · 변환 알림 한 줄 / GF000007(만약) 0 ≠ 597,285 · GF000288(로직()) 0 ≠ 1,234,760.96 — 아래 흠 ② GF000001 함수 식 ×1.5 → 오른쪽 · 왼쪽 51,620 따라감 → [저장](공용 GF · 사본) → 다시 열기 상자 6 · 변환 알림 없음 · /calc flow 51,620.4 = 화면 ③ 신규 로직 65 걸음(변수 셋 · 함수 · 조건 만들기 · 출력 둘 · 조건 선) → 저장 GX000004 → 다시 열기 상자 4 · 선 4 · 화면 100,000 = /calc 100,000 = 왼쪽 계(높이 2 = 0 · 지반 암 = 50,000) ④ 로직 복사/생성 2 걸음 → GX000005 상자 4 · 선 4 · 100,000 ⑤ 선 조건 두 줄 「지반 = 토사 그리고 높이 ≥ 3 일 때만」 · 값 고르기(토사|암) · 높이 2 → 0 · 또는 → 50,000 · 조건 만들기 =IF(A2>=3,A3,0) ⑥ 식 보기 「결과 1 = 금액 × 높이 ÷ 4」 · IF 만약 / 아니면 줄 · 칸 연결 표 ⑦ 크게 보기 1484×1076 · 거기서 고친 것 패널로 · 접기 40px · 펴기 · 너비 320 → 522 ⑧ 고친 것 버리기 5 → 4 · 캐시 비움 · 로그인 없음 flow · save · calc · logics 401 · 시험판 · 옛 단추 글 0 · resources/master_data 바뀜 0 · 흠(다른 창): 변환 숫자 입력 예시값이 비어 있음(시험입력 · 기본 입력을 안 넣음 — GF000288 나무높이 6 · 집재거리 100 · GF000007 정리량 50 …) · GF000007 「표 QF000033 → 할증」 표에 맞는 줄 없음(서버 477,828 = 할증 25% 빠짐 · 화면 0) · GF000288 다른 로직 GC000270 기계 자리 「EQ:{기계} … 자리」 못 풂(#N/A → 0) · 칸 #N/A 일 때 화면 셈 0 · 서버 셈 그대로(둘이 다름)(sub7) · 식 보기 값 줄이 선 규칙을 지난 값을 안 씀(「(#N/A × 0) = #VALUE!」 · 결과 알약은 34,413.6)(sub2) · 조건 통과 둘째 줄 값 목록 「number:0」 글 · 조건 만들기 만약 목록에 놓인 알약 없음(sub2) · 옛 로직 왼쪽 계 반올림 · 흐름 원가 계금 1원 버림(1원 차이) +- 36 마무리(10-2 02시 · 브레인 실제 검증완료): 전수 변환 대조 시험입력 1,469/1,472(99.8%) · 기본 1,466(99.6%) · 남은 3 = 마스터 빈 단가 · 시험 141 + 변환 시험 초록 · 사본 서버 저장 → 사본만 바뀜 · 다시 열기 흐름 정본 · GF000001 34,414 = 34,414 · GF000003 8.946 = 8.946 · 식 보기 값 줄 · 검증 대기 + +## 37. 일위대가 조합 다이어그램 (사용자 지시 10-2 09시 · 계획 · 착수) + +- 사용자 확정: 「복잡한 경우가 있으니 일위대가 조합도 다이어그램으로 표현 … 중요하게 생각하는 정보가 다르니 잘 구분」 · 조합은 계산하지 않고 로직을 뭉쳐 비용 구분별로 표기 · 입력은 로직별로 묶어 빈 값은 사용자가 넣음 · 출력은 로직마다 · 미리 보기 값은 구조물 조합으로 안 넘김 · 「흐름도는 별도 로직이 아님」(단가산출 로직을 쓰는 방법). +- 모양: 왼쪽 = 로직별 입력 묶음(받아야 할 값 · 빈 값 표시) → 가운데 = 로직 카드(키 · 이름 · 결과 단위 · 식 · 함수 안 보임) → 오른쪽 = 비용 구분 칸 셋(노무비 · 재료비 · 경비 · 로직마다 선으로 모임) + 값 로직은 「값」 칸 · 이름이 같은 입력(지역 · 자재지역 등)은 점선으로 이어 보이기만(검사 없음) · 로직 카드를 누르면 그 로직의 단가산출 로직 화면(흐름도)으로 · 계산 · 고치기 없음(묶음 고치기는 지금 표) · 흐름도와 모양 · 색을 달리함. +- 자리: 일위대가 조합 상세(M02_MasterTemplete_Labor_Edit)의 미리 보기와 같은 값 — 미리 보기 입력 칸에 넣은 값이 다이어그램에도 · 다이어그램 빈 입력 알약을 누르면 미리 보기 그 칸으로 · 미리 보기 결과(로직별 노무비 · 재료비 · 경비 · 계)가 카드 · 칸에. +- [x] 37-1 다이어그램 판 — M02_MasterTemplete_Labor_Diagram.ts(+css) 새 파일 · 고정 세 칸 배치(자동) · 판 옮기기 · 확대 · 한눈에 · 밝음 · 어두움 · 좁은 폭 +- [x] 37-2 잇기 — Labor_Edit 에 「다이어그램」 칸(접기) · 미리 보기와 값 한 벌 · 빈 입력 누름 → 미리 보기 칸 · 로직 카드 누름 → M01 단가산출 로직 그 로직 열기(setPendingLogicKey 꼴) +- [x] 37-3 검증 — 조합 셋(로직 2 · 9 · 14개)으로 입력 묶음 수 = 로직 입력 수 · 출력 = 미리 보기 줄 · 비용 칸 합 = 미리 보기 합 · 빈 값 표시 · 같은 이름 점선 · 로직 열기 · 브레인 실제 검증 · 보고 +- 창: sub2 37-1 · 37-2 · 37-3 + +## 38. 마스터 요소 화면 손질 (사용자 지시 10-2 09:40 · 착수) + +- 사용자 확정: ① 인력 · 재료 · 기계 · 소요량 · 계수 · 환율 · 요율 상세의 제목 · 단추 · 찾기 라벨 양식 = 단가산출 로직과 같게 ② 재료 컨테이너의 유가 · 전력을 별도 컨테이너로 ③ 단가산출 로직 「막힘만」 체크 — 의미 없으면 지움 ④ 단가산출 로직 컨테이너 「전체」 를 누르면 상세 목록 보기가 안 됨(다른 컨테이너는 됨). +- 브레인 정함: 「막힘만」 = 로직 검사에 걸린 로직만 거르기 · 지금 1,469 중 0 · 목록 줄에 「⛔ 막힘」 표시 · 까닭 말풍선은 이미 있음 → 체크만 지움(표시는 둠). +- [x] 38-1 양식 맞춤 — 일곱 상세 화면 머리(제목 · 단추 줄 · 찾기 칸 라벨)를 단가산출 로직 화면 꼴로 · 공용 단추 · 칸 크기(sub5) · 검증완료 +- [x] 38-2 유가 · 전력 컨테이너 — 왼쪽 패널에 재료 다음 「유가 · 전력」 컨테이너(재료_유가전력.json) · 재료 컨테이너에서는 뺌 · 고르기 · 찾기 · 상세 그대로(sub6) · 검증완료 +- [x] 38-3 「막힘만」 체크 지움 · 38-4 「전체」 목록 보기 고침(sub1) · 검증완료 — 원인: 로직 컨테이너 거름이 목록만 거르고 가운데 화면 전환(onLogic)을 안 부름 → Side 거름 run 에서 로직이면 onLogic · 「전체」 는 찾는 글도 비움 · 화면(5174): 로직 1,469 줄 → 인력 전체 → 로직 「전체」 = 로직 화면 1,469 줄(찾는 글 「굴착」 비움) → 산림품셈 330 → 03장 작업장 관리 10 → 로직 열기 → 「전체」 = 상세 닫힘 · 1,469 줄 · 막힘만 체크 0 · 화면 오류 0 +- 「고친 것 버리기」(사용자 물음 · 확인만): 저장 안 한 고친 것(화면 캐시)을 버리고 파일의 로직을 다시 엶 — 사본 저장은 없음 · 그대로 둠(사용자 결정 기다림) From 6c28357fb74c4055da8334561735c966457fedd9 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sun, 4 Oct 2026 17:19:20 +0900 Subject: [PATCH 03/14] auto: 2026-10-04 17:19 (ESD_LAPTOP) --- .../raw/M02_구조물조합개편/사용자요구_1004.md | 50 ++++++++ docs/raw/M02_구조물조합개편/설계_정함.md | 105 +++++++++++++++++ ...가산출_흐름도_조합다이어그램_마스터요소.md | 108 ++++++++++++++++++ 3 files changed, 263 insertions(+) create mode 100644 docs/raw/M02_구조물조합개편/사용자요구_1004.md create mode 100644 docs/raw/M02_구조물조합개편/설계_정함.md create mode 100644 docs/raw/plans/2026-10-04_plan_단가산출_흐름도_조합다이어그램_마스터요소.md diff --git a/docs/raw/M02_구조물조합개편/사용자요구_1004.md b/docs/raw/M02_구조물조합개편/사용자요구_1004.md new file mode 100644 index 000000000..936caa071 --- /dev/null +++ b/docs/raw/M02_구조물조합개편/사용자요구_1004.md @@ -0,0 +1,50 @@ +# M02 구조물 조합 개편 — 사용자 요구 (2026-10-04 · 원문 그대로) + +1번(계획서 32장 이후 정리)은 브레인이 처리함(c8269a969). 7 · 8번은 원문에 없음. + +## 2. 일위대가 조합 → 구조물 조합 하위 + +M02페이지의 마스터 템플릿 페이지에서 일위대가 조합은 구조물 조합의 하위 컨테이너로 넣을 것.(구조물 도면과 면적산출 사이에 배치) +- 일위대가 조합의 좌측 패널의 기존 보기 방식인 트리구조는 필요없으며 구조물 도면 처럼 리스트형태로 보임 +- 추가로 구조물 도면, 일위대가, 면적산출의 최상위에는 전체항목을 넣어주고 우측 상세에는 리스트로 보여줌, M01 항목을 보면 양식화되어 있는 페이지 헤이아웃 있음 + +## 3. 이름 + +이동한 일위대가 조합은 일위대가로 명칭 변경 + +## 4. 구분선 + +구조물 조합 컨테이너 내부의 서브 컨테이너 항목과 트리구조 사이에 수평 구분선 추가 + +## 5. 사용자별 구조물 조합 읽기 / 쓰기 / 복사 권한 점검 및 개선 + +- 시스템 관리자 : 아이슬로 프로그램 관리자로 마스터를 제공하여 회사관리자나 일반 사용자가 사용하기 쉽게 한다. +- 시스템관리자가 대시보드의 컨테이너 제목의 우측의 회사명을 선택함에 따라 변경한 목적은 해당 회사의 정보를 접근하고나 AS을 목적으로 고객을 도와주기 위함임. 복사항목에는 마스터나 시스템 관리자 회사, 개인으로 들어갈 항목 뿐만 아니라 고객 회사의 특정 폴더로 복사할 수 있는 기능이 있어야함. 수정/생성권한 포함 +- 회사관리자 : 고객으로 본인회사의 양식을 만들거나 개인 프로젝트에 사용할 개인 마스터데이터 관리 +- 회사관리자는 개인버튼을 누를경우 최상위 트리에는 다른 개인들의 정보를 보고 복사하여 개인에게 복사하거나 수정, 생성도 가능해야함. +- 개인은 마스터와 회사는 읽기만, 다른 개인의 정보는 볼수 없음. 트리는 현재를 유지해야함. + +## 6. 구조물 조합 항목 변경 + +- 최상위 구조물 조합에 구조물 도면, 일위대가, 면적산출로만 표현(기존의 재료산출의 사용자가 추가하는 재료들과 값은 엑셀을 사용하지 않고 일위대가의 담은로직 처럼 하위에 별도의 컨테이너로 추가되는 자재항목을 넣어야함. 이때 M01자재목록에서 하나씩 가져오되 수량은 면적산출의 알약으로 값을 싱크하거나 별도의 모달창에서 셀을 지정하게 구현) +- 재료산출 항목은 삭제 +- 선택입력 항목은 삭제(구조물 조합에서는 각 옵션의 상세 구조물에 따라 미리 옵션을 다 지정할 생각임으로 그럴필요 없음) +- 면적산출의 미리보기에서 아래 알약 그룹 행을 하이드/쇼 되게 반영(기본값은 하이드), 단, 면적산출 상세에서는 알약 행 보이기가 기본값으로 +- 면적산출 상세 화면의 우측 패널에서 현재의 일위대가 조합 목록을 검색하는 기능이 아니라 현재 구조물도의 일위대가 담은 로직과 추가될 사용자 지정 재료를 보여줄것, +- 일위대가 상세화면에서는 미리보기(보기만/저장안함) 컨테이너는 각 단가산출 로직에 입력이 필요한 정보들임. 구조물별로 하나씩만들어져야함으로 구조물의 일위대가 항목에 옵션값이 저장되야함. 그리고 이값에 대한 지정은 담은 로직 컨테이너의 항목을 누르면 모달창이 나오고 사용자가 입력 및 변경할수 있게함. (하위에 지정된 항목과 값을 작은 글씨로 표현되게 반영) +- 담은 로직에는 기준 단가를 기준으로 넣었을때 나온 값과 수량산출값을 넣었을때 값에 대한 노무비, 재료비, 경비, 합계를 같이 보여줌. 이때 수량산출은 면적산출의 특정 셀값이나 사용자가 수기로 입력한 값이 될수 있음. 이렇게 되면 미리보기 계산 컨테이너 필요 없음. +- 일위대가의 상세 화면의 순서는 담은로직 > 자재목록 > 다이어그램 순서임. +- 면적산출 산출 상세의 우측 패널에 보여져야하는 결과는 결국 일위대가의 담은 로직컨테이너와 자재목록임. +- 추가된 자재목록에도 기준에 대한 단가와, 면적산출 엑셀의 특정 셀과 연결되었을때 나온 단가가 같이 나와야함. + +## 9. 오른쪽 클릭 + +프로그램 B와 M영역의 페이지들에 마우스 오른쪽 클릭을 할 경우 브라우저 옵션 안보이게 할것.(우리 프로그램의 옵션은 허용, 공통양식 사용) + +## 사용자 답 (10-4 · 일위대가 · 원문 그대로) + +일위대가의 경우 +1. 구조물 조합을 복사한 경우 신규 키를 생성하여 만듬. (값과 로직은 일단 같이 복사) +2. 신규 구조물 조합을 생성하는 경우 미리보기 페이지에는 새로만들기와 복사생성하기를 선택하게 하고 복사 생성인 경우에는 해당 리스트를 보여주고 선택하게 보여주는 값은 개인인 경우에는 개인을, 회사관리자인경우 개인, 시스템관리자인 경우 마스터를 기본값으로 보여주고 탭을 눌러 다른 항목에서 가져올수 있는 항목들을 보여준다. +3. 다시 말해 최종적으로는 구조물 조합에 일위대가, 도면, 면적산출이 연되는게 맞으나 특정항목만 다른 값을 복사하여 가져올수 있어야함. 저장이 된 데이터라도 변경가능해야함. +4. 현재 매칭이 안된 데이터는 나중에 삭제할예정이나 마스터 데이터를 수동 생성을 위해서 필요한 데이터들임. 차라리 실무문서라는 항목을 마스터에 별도로 만들어서 나중에 한번에 지우게 할수 있게 반영. diff --git a/docs/raw/M02_구조물조합개편/설계_정함.md b/docs/raw/M02_구조물조합개편/설계_정함.md new file mode 100644 index 000000000..229b350fd --- /dev/null +++ b/docs/raw/M02_구조물조합개편/설계_정함.md @@ -0,0 +1,105 @@ +# M02 구조물 조합 개편 — 정함 (브레인 · 10-4) + +근거 = `사용자요구_1004.md` · 조사 보고 A~H(각 창 `tmp/조사_39-0X.md`). 사용자가 바꿀 수 있음. + +## 1. 자료 틀 (B · sub7) + +- 일위대가 = 구조물 열마다 `labor/<열id>.json` · 번호 `SU-######`(도번 · 면적번과 같은 뒷자리 · 층마다 셈) · 자리 = 시스템 `resources/master_template/labor/` · 회사 · 개인 `…/templates/labor/` · 새 프로젝트 씨앗도 KINDS 로 함께. +- 문서 꼴: + +```json +{ + "종류": "일위대가", "판": 1, "열": "item1", "일위대가번": "SU-000001", "단위": "개소", + "본뜬": { "층": "system", "주인": null, "키": "UA000008" }, + "담은로직": [ + { "로직": "GF000219", "메모": "", "입력값": { "형식": "메쌓기" }, "수량": null }, + { "로직": "GF000137", "메모": "", "입력값": {}, "수량": { "손": 3 } } + ], + "자재": [ { "코드": "MT000008", "이름": "치즐", "규격": "", "단위": "EA", "수량": null } ], + "계산값": { "줄": [], "자재": [], "기준합": {}, "반영합": {} } +} +``` + +- 이름 = 구조물 열 이름(따로 안 둠) · 단위 = 자기 칸 · 같은 로직 두 번 금지 · 빈 일위대가 저장 됨 · 이름 겹침 검사 없음 · 입력값 = 그 로직 입력 이름만(모르는 이름 버림 · 빈 값 = 자동값). +- 수량 = `null`(면적산출 알약 연결 따름 · 로직 알약 「이름|단위」 · 자재 알약 「자재|MT키」 · 칸 연결 정본 = 면적산출 문서 `연결` 한 곳) | `{손: 수}`(손 입력 · 손이 칸보다 앞섬). +- 금액 = 기준(결과단위 그대로 · 원/10㎡ 면 10㎡ 값 · 단위 같이 표기) · 반영 = 기준 × 수량 ÷ 배 · 돈 아닌 로직은 반영 없음 · 「원/1,000본」 배 1000(`_PER` 고침) · 자재 기준 단가 = M01 `master_material.element` 기본값. +- 계산 자리 = 기준 ③ 서버(M01 엔진 · calc 길 · 화면은 최신 응답만) · 반영 곱 ① 짝(`M02_MasterTemplete_Labor_Calc.ts` ↔ `M02_Labor_Doc.py` · 두 파일 머리에 짝 · 거울 시험 · 면적 칸 값이 바뀌면 화면이 바로 곱함) · [저장] 때 서버가 `계산값` 을 다시 적음(브라우저 값 버림 · 목록 합계용) · 상세는 늘 새로 셈. +- 길 = 문서 `GET/PUT /api/m02/layers/{층}/templates/labor/{열}`(양식 길 · owner) · 계산 `POST /api/m02/layers/{층}/labor/{열}/calc` {doc?, 수량?: {키: 값}} → {줄[{로직, 분모, 배, 수량, 수량출처, 기준{노무비,재료비,경비,계}, 반영{…}, 까닭}], 자재[{코드, 기준단가, 수량, 수량출처, 반영, 까닭}], 기준합, 반영합} · 알약 `GET /api/m02/layers/{층}/labor/{열}/pills`(로직 물량 + 자재). +- 집계표 열 `일위대가` · `자재조합` 칸 지움(정본 = labor 파일) · SS 장부 줄 = `일위대가번` · `도번` · `면적번`. +- 옮기기(한 번 · `M02_Labor_Migrate.py`) = 층마다 열마다 labor 파일 · 열 `일위대가` 키 조합의 담은로직 · 단위 복사(`본뜬.키` = 옛 키 · 입력값 {} · 수량 null · 자재 []) · 여러 열이 쓰던 조합(UP10-000001)은 열마다 복사 · 키 없는 열 = 빈 일위대가 · 몇 번 돌려도 같음 · 시스템 층(git)은 바로 · storage 층은 압축 사본 뒤 브레인이 돌림. +- 실무문서 = 마스터 `resources/master_data/일위대가조합.json` 통째(37 줄 · 손대지 않음 · 옮긴 뒤 어느 열도 안 가리킴 · M01 일위대가 조합 화면도 그대로) · 회사 · 개인 옛 조합(UC · UP)은 파일 그대로 · 화면에서 뺌. +- 프로젝트 = 스냅샷 `일위대가.json` 은 labor 문서 + 담은 로직 줄 · `입력값.json`(모양 그대로) 첫 값 = labor 입력값 · `단가.json` 에 자재 MT 줄 · 계산은 반영 같은 함수. +- 은퇴(화면이 안 부른 뒤) = 일위대가 갈라 내기 · 키 적기(use-combo) · 조합 고치기 · 미리 보기 길(preview) · 일위대가 조합 나무 길. +- 로직 지우기 검사 = 모든 층 labor 담은로직 + 실무문서(32장 「로직 지우기 서버 검사」 줄 끝남). + +## 2. 좌측 패널 (A · sub5) + +- 맨 위 「일위대가 조합」 컨테이너 · 조합 나무 · 둘째 층 단추 벌 지움 · 구조물 조합 안 차례 = 층 단추 + 나무 → 수평선 → 구조물 도면 → 일위대가 → 면적산출. +- 하위마다 첫 줄 「전체 항목」(누르면 `onAll(kind)` → H 목록) · 일위대가 줄 = 열마다 「SU-… 이름」 · 누름 = `onOpen({kind: "labor", name: 열})`(C 상세). +- 이름 「일위대가」(하위 제목 · 상세 제목 · 대시보드 카드 글) · 해시 `open=일위대가` = 구조물 조합 펼침 + 일위대가 하위 펼침 + 전체 항목. +- 「실무문서」 = 구조물 조합 밖 별도 컨테이너(도면 양식 아래) · 마스터 층 · 시스템 관리자만 · 목록 + 상세 읽기만(새 파일 `M02_MasterTemplete_Practice.ts`) · [통째 지우기](물음 두 번 · 서버 = 시스템 관리자 · 쓰는 곳 0 확인 · 줄을 비움 · 버튼은 사용자가 누를 때만). + +## 3. 일위대가 상세 (C · sub2) + +- UI_Main 종류 `labor` 로 엶(양식 길 읽기 · [저장] · [다시 읽기] · 캐시 = 다른 양식과 같음) · 차례 = 머리 → 정보(단위 · 본뜬) → 담은 로직 → 자재목록 → 다이어그램 · 미리 보기 칸 지움. +- 담은 로직 줄 누름 = 입력 모달(그 로직 입력 전부 · 빈 칸 자동값 · 수량 = 이은 칸 · 값 보임 / 손 입력 / 「면적산출에서 잇기」 = 면적산출 상세로) · [확인] = 줄 값만(캐시) · 저장은 [저장]. +- 줄 아래 작은 글 「지역 서울 · 규격 25mm · 수량 B6 = 12.5㎡」 · 빈 입력 「빈 값 n」 붉게. +- 금액 칸 넷(노무비 · 재료비 · 경비 · 합계) · 칸마다 두 줄(위 기준 작게 · 아래 반영) · 맨 아래 합계 줄. +- 자재목록 = 코드 · 이름 · 규격 · 단위 · 수량 · 기준 단가 · 반영 금액 · [자재 추가](M01 자재 하나씩 · `M02_Material_Pick.ts`) · 줄 누름 = 수량 모달. +- 다이어그램 = 저장된 줄 값 + calc 답(수량 있으면 반영 · 없으면 기준 · 표기) · 알약 누름 = 입력 모달 · 자재목록 = 카드 하나 → 재료비 칸. +- 보기 갈래(고치기 없음) 부품을 냄 — D 오른쪽 패널 · H 카드가 씀. +- 입력 목록 · 값 받기 부품은 선택 입력에서 옮겨 다시 씀(`M02_Labor_Inputs.py` · `M02_Labor_Values.ts` · `M02_Labor_Values_Model.ts`) · 값 칸 규칙 한 벌(M01 넣을 값) · 로직 입력이 바뀌어 사라진 값은 알림. +- 37장 「미리 보기 값은 구조물 조합으로 안 넘김」 뒤집힘. + +## 4. 면적산출 (D · sub3) + +- 미리보기 카드 알약 줄 켬끔 단추 · 기본 꺼짐 · pref(계정) · 상세는 늘 보임(단추 없음). +- 상세 오른쪽 패널 = 그 열 일위대가의 담은 로직 + 자재목록(C 보기 갈래 · 기준 · 반영) · 조합 목록 · 찾기 · [이 열에 쓰기] · [조합 고치기] 지움. +- 패널 줄 누름 = 칸 지정 모달(`M02_MasterTemplete_AreaLink.ts` · [지금 고른 칸으로] · 주소 · [풀기]) → 면적 문서 `연결` · spreadsheet 손잡이 `setLink(키, 칸|null, 항목)`. +- 알약 = 로직 물량 알약 + 자재 알약(`자재|MT키` · 알약 모드 「자재」 다시 씀) · 선택 입력 알약 빠짐. +- 면적 금액 서버 계산은 1장 calc 하나(따로 안 만듦). + +## 5. 목록 · 고르기 · 카드 (H · sub1) + +- 목록 부품 하나 `M02_MasterTemplete_ItemList.ts`(M01 머리 · 쪽 · 표를 import · 옮기지 않음) — 「전체 항목」 · 복사 생성 · 항목 하나 복사가 같이 씀. +- M02 상세 제목 = M01 꼴(18px · 단추 오른쪽) 전부. +- 목록 칸: 도면 = 도번 · 이름 · 폴더 · 단위 · 고친 날 / 면적산출 = 면적번 · … / 일위대가 = 일위대가번 · 이름 · 폴더 · 단위 · 담은 로직 수 · 합계(`계산값` · 없으면 「—」) · 고친 날. +- 「새 항목」 → 상세 칸에 [새로 만들기] [복사 생성] · 복사 생성 = 같은 칸 탭 + 목록 → 지금 보는 층 · 고른 폴더(없으면 맨 위) · 원래 이름(겹치면 「(2)」) · 일위대가 새 번호. +- 카드 = 도면 · 일위대가 · 면적산출 셋(그 차례) · 빈 카드 = [새로 만들기] [복사 생성] · 찬 카드 = 「복사해 가져오기」(창 · 탭 + 목록 → 확인 → 덮어씀 · 번호 · 열 그대로) · 일위대가 카드 몸 = C 보기 갈래. +- 탭 = 마스터 · 회사 · 개인(나) · 다른 사람(드롭다운 · 맡은 회사일 때) · 실무문서(일위대가 고를 때 · 시스템 관리자) · 기본 = 시스템 관리자 마스터 · 그 밖 개인(회사 없으면 마스터) · 탭 목록 = E `readableSources`. +- 항목 하나 복사 서버 = `copy_from` 에 from_layer · from_owner(B 가 만듦) · 원본이 실무문서 조합이면 조합 → 일위대가 문서(입력값 빈 값). + +## 6. 권한 (E · sub4) + +- 「회사 C 를 맡음」 = 시스템 관리자 · 또는 C 의 회사 관리자 → C 회사 층 · C 모든 개인 층 = 보기 · 만들기 · 고치기 · 지우기 · 복사 대상. +- 일반 = 마스터 · 회사 읽기만 · 남 개인 안 보임 · 나무 그대로 · 프로젝트 apply · sources 도 같은 규칙. +- 사람 나무 = 맡은 회사면 켬 · 회사 사람 전부(나 맨 위 · 빈 사람 「비어 있음」) · 남의 층 첫 쓰기 = 소속 확인 뒤 뼈대. +- 「〇〇로 복사…」 = 쓸 수 있는 층 전부(마스터 · 볼 회사 · 내 개인 · 지금 사람) + 대상 폴더 고르기 창(`M02_MasterTemplete_TreePanel_CopyTo.ts`). +- 남의 층 쓰기 = `system_audit_logs` 한 줄(`M02_OTHER_LAYER_WRITE`). +- 실무문서 = 일위대가 조합 길 전부 시스템 관리자만(서버가 막음). +- 새 길(labor 문서 · calc · pills · 층 넘는 복사)도 `_root` · `_layer_dir` 한 벌을 거침. + +## 7. 지우기 (G · sub6) + +- G1 자재 조합(UM) · G2 재료산출(`material/` · SM · 카드 · 시트 · 옆 판 · 길 · 시험) 먼저 · G3 선택 입력(카드 · `inputs/` · 자리 · 알약 길)은 C 의 입력 모달이 선 뒤. +- `pickMaterial` · `MaterialItem` → `M02_Material_Pick.ts` 로 옮긴 뒤 지움 · 알약 모드 「자재」 · `LinkMaterial`(A00 spreadsheet) 남김 · `고른자재.json` 남김 · 프로젝트 `입력값.json` 남김. +- storage 층(회사 · 개인 · 프로젝트)의 `material/` · `inputs/` · `자재조합.json` 파일은 지우지 않음(읽기만 끊음 · 정리는 사용자) · git 안 시스템 층 · fixture 는 지움. +- 옛 SM 번호 버림 · 장부 `재료번` 칸은 다음 집계표 [저장] 때 빠짐. + +## 8. 파일 주인 (같은 파일 = 몫만 고침 · 받은 뒤 고침) + +| 파일 | 주인 | 다른 창 몫 | +| --- | --- | --- | +| `M02_MasterTemplete_Store.py` · `M02_Template_Layers.py` | sub7 | sub6 = material · inputs 줄 | +| `M02_MasterTemplete_Router_Items.py` | sub4(`_root`) | sub7 = copy 길 from_layer · sub6 = material · inputs 길 | +| `M02_MasterTemplete_Router_Combos.py` · `Combo_Store.py` | sub5(나무 길) | sub4 = 시스템 관리자 문 · sub2 = preview 지움 | +| `M02_MasterTemplete_UI_Side.ts` | sub5 | sub6 = material · matset 줄 | +| `M02_MasterTemplete_UI_Page.ts` | sub5 | sub4 = copyTo · sub1 = onAll 이음 | +| `M02_MasterTemplete_UI_Main.ts` | sub3(면적 길) | sub2 = labor 종류 · sub1 = 제목 꼴 · showView · sub6 = material 길 | +| `M02_MasterTemplete_ItemCards.ts` | sub1(카드 셋 · 빈 카드) | sub3 = 알약 켬끔 · sub6 = 재료 · 선택 입력 카드 | +| `M02_MasterTemplete_Structure.ts` | sub3(pillsFor) | sub1 = pickCopy · sub6 = inputs · material | +| `M02_MasterTemplete_TreePanel.ts`(670줄) | sub1(copyIn · 넘으면 `TreePanel_Clip.ts`) | sub5 = 일위대가 전용 칸 지움 · sub4 = 「〇〇로 복사…」 이음 | +| `M02_MasterTemplete_Labor_*.ts` | sub2 | sub5 = `Labor.ts` · `Labor_Tree.ts` 나무 지움 | +| `M02_MasterTemplete_Api_Fetch.ts` | 각자 자기 함수만 | — | +| `A00_Common/spreadsheet/spreadsheet_links.ts` · `spreadsheet.ts` | sub3 | — | +| 공용 메뉴 · 진입(`main.ts` · `cad_host.ts` · `ui_template_context_menu.*` · `ui_template_tree.*` · `spreadsheet_condition.ts` · `spreadsheet_menu.ts`) | sub8 | — | diff --git a/docs/raw/plans/2026-10-04_plan_단가산출_흐름도_조합다이어그램_마스터요소.md b/docs/raw/plans/2026-10-04_plan_단가산출_흐름도_조합다이어그램_마스터요소.md new file mode 100644 index 000000000..fa33578c6 --- /dev/null +++ b/docs/raw/plans/2026-10-04_plan_단가산출_흐름도_조합다이어그램_마스터요소.md @@ -0,0 +1,108 @@ +# 지난 계획 — 33~38장 (2026-10-01 ~ 10-02) + +단가산출 로직 점검(데스크탑 한시) · 쉬운 단가산출 · 로직 만들기 시험판(세 칸 판 · 시트 로직 · 다이어그램) · 흐름도 시험판 · 흐름도 본 프로그램 적용 · 일위대가 조합 다이어그램 · 마스터 요소 화면 손질. + +## 33. 단가산출 로직 점검 · 수정 (사용자 + 데스크탑 메인 · 보조 · 10-1 한시) + +- 사용자가 데스크탑 메인 · 보조와 직접 진행 · 브레인은 랩탑 sub1~8 로 사용자가 요청하는 다른 일을 맡음(10-1 18:00 무렵까지). +- 근거 = `docs/raw/M01_점검/종합/` · `docs/raw/M01_결정목록/` · `docs/raw/M01_입력목록/` · `docs/raw/M01_고침/`. + +## 34. 쉬운 단가산출 제어 (새 길 · 10-1 · 랩탑) + +- 사용자 확정(10-1 오전): 만든 쉬운 단가산출은 유지(지우지 않음) · 로직 정의 자체를 만드는 판은 제안만 · 허락 뒤 착수 +- 사용자 확정: 지금 단가산출은 그대로 · 「추가」 단추 옆 새 단추로 따로 들어감 · 코드는 `M01_MasterData/EasyCalc/` 한 곳(안 되면 폴더째 지움) · 초등학생도 추가 · 삭제 · 수정 · 시간이 걸려도 검토 먼저. +- 정함(브레인): 새 길은 지금 로직 형식을 그대로 읽고 씀(계산 엔진 안 바꿈) · 폴더 밖 참조는 단추 자리 한 곳만. +- [x] 34-1 검토 — 데이터 · 계산 지도(sub4) · 지금 화면 걸음 재기(sub6) · 실무 모양 + 갈래 비교(sub7) · 지난 시도(sub8) → `docs/raw/M01_쉬운단가/` · 지금 화면 새 로직 38 누름 · 5~6 창 +- [x] 34-2 설계 확정(브레인) — 문장 카드 · 품셈 기본은 「이걸 바탕으로 고치기」 · 쓰기는 `/logic/*` 만 · 1단계 숫자 수량 + 잠금 카드 → `docs/raw/M01_쉬운단가/설계.md` +- [x] 34-3a 단추 + 빈 창(sub1 · 「식으로 새로 만들기」 옆) 검증완료 +- [x] 34-3b 1단계 — Model(sub7) · Api(sub4) · Cards(sub2) · Pick(sub3) · Sheet(sub5) · 틀 · 잇기(sub1) — 설계.md 2 · 3절 · Sheet 검증완료 · Model 검증완료 · Api 검증완료 · Pick 검증완료 +- [x] 34-3c 2단계 — 표 보고 고르기(sub8) · 조건 고치기(sub7 · 조건 줄 93% 읽음) 검증완료 · 카드 단추 잇기(sub2) · 조건 고치기 검증완료 +- [x] 34-4 검증 — 대표 일 여섯 + 예외(빈 값 · 같은 이름 · 저장 안 하고 닫기 · 권한 없음 · 밝음 · 어두움) · 지우기 시험(폴더 밖 참조 줄 수) · 걸음(sub3 고침 뒤) ①21 ②8 ③2 ④4 ⑤2 ⑥3 = 40(목표 36 넘음 4 · ① 새 설계 값 자동값으로 채워 금액 곧장 보임) · 다른 일위대가 넣고 저장 됨 · 권한 없음(일반 사용자) = M01 화면 못 들어감 · /api/m01 403 · 폴더 밖 참조 두 줄 · 검증완료 + +## 35. 단가산출 로직 만들기 판 (제안 단계 · 착수 전) + +- 사용자 확정: 쉬운 단가산출처럼 따로 폴더 시험판 · 로직 몇 개 골라 만듦 · 페이지 보고 판단(마음에 안 들면 지움) · **착수는 허락 뒤** · 인터넷 조사 먼저 · 화면 용어는 단어로 다시 제안. +- 제안서 https://claude.ai/artifact/U4rkgpYWL4Em5VKBEVa4oY (세 칸: 원문 표 · 산출 과정 · 결과) +- [x] 35-0 인터넷 조사 — 견적 프로그램(sub6) · 노코드 규칙 도구(sub8) → `docs/raw/M01_쉬운단가/로직판_조사_*.md` · 제안서 2판에 반영 +- [x] 35-1 용어 단어 제안(제안서 2판 「용어 — 단어로」) → 사용자 확정 · 완료 처리(10-4): 시험판 지움(35-9)으로 접음 +- [x] 35-2 시험판 — 사용자 허락(10-1 · 갈래가 여럿이면 갈래마다 · 목적 = 코딩 없이 원문 보며 확인 · 수정 · 생성) · A 세 칸 판 `LogicBuilder/` · B 엑셀 시트형(우리 스프레드시트) · 먼저 조사: 엑셀 로직 지도(sub4) · A 설계 초안(sub7) → 브레인 A 대 B 판단 → 구현 · 완료 처리(10-4): 35-2a 로 함 +- [x] 35-2a A 세 칸 판(가운데 문장 / 격자 두 보기) — 정함 `docs/raw/M01_쉬운단가/로직판_A_정함.md` · Blocks(sub7) · 서버 · Api(sub4) · 원문 · 검사(sub8) · 문장(sub2) · 격자(sub5) · 단계 안내(sub3) · 틀 · 잇기 · 끝 검증(sub1) · LB 서버 검증완료 · Blocks 검증완료 · Grid 검증완료 · Steps 검증완료 · Source · Check 검증완료 · Wizard 검증완료 · 잇기 · 끝 검증완료(sub1) — ① 원문 대조 6/6 블록 읽힘 · 걸린 줄 · 출처 보임 ② GF000219 표 칸 0.15→0.20 = 79,280→96,759 원(예상과 같음) · 파일 안 바뀜 · 다시 열면 0.15 ③ 단계 안내 11 걸음 + 저장 → 현장 다시 열림 60,984 원(/calc 같음) → 삭제 2 걸음 ④ 빈틈 해당 없음 10 · 못 따진 표 조회 1 · 기대값 시험표 맞음/다름 · 화면 초안만 ⑤ 표 고침 없을 때 lb/calc = /calc 8/8 · 문장 ↔ 격자 같은 값 · 예외(빈 이름 · 저장 안 하고 닫기 · Ctrl+Z · Ctrl+S · 권한 없음 403 · 밝음 · 어두움) · 폴더 밖 참조 LogicLab.ts 두 줄 + main.py 등록 한 줄 +- [x] 35-2b 남은 것(사용자 판단 뒤) — 문장 / 격자 고르기 · 표 칸 고침 저장 여부 · 적용 품셈 판 · 용어 · 칸 역할 「결과」 바꾸기 · 단계 안내 고르기 창 수량 물음 · GX000001(메인 11:53 · 12:02) 주인 확인 · 보고 https://claude.ai/artifact/NqKfHaJULcQo6dSvnsxy1n · 완료 처리(10-4): 시험판 지움(35-9)으로 접음 +- [x] 35-3 점검(확인만) — 표끼리의 관계(로직) 자체를 초등학생이 만들고 · 바꾸고 · 지울 수 있나 · 품셈에 없는 관계 더하기 · 시나리오 여섯(sub6) — 결과: 있는 관계의 값만 고침(초등 중) · 관계 더하기 · 지우기 · 다시 잇기 · 새 입력값 · 새 표 · 로직끼리 바꾸기 안 됨 · 문턱만 바꾸면 계산 멈춤 + 엉뚱한 알림 · `docs/raw/M01_쉬운단가/로직판_관계제어_점검.md` +- [x] 35-4 다이어그램 제안(사용자 생각 10-1) — 상자 = 표 · 입력값 · 고정값 · 계산 · 항목 · 다른 로직 · 결과를 직접 다룸 · 선 = 관계를 만듦 · 품셈에 안 묶임(사용자 표 · 사용자 관계) · 관계 제어 = 다이어그램(사용자) · 위 가로 띠 + 아래 고른 상자 다루기 · 결과 · 관계 = 진짜 엑셀 수식(칸 주소 · 함수 · 사용자 확정) · 알약 = 입출력 칸 표시 · 새 종류 「시트 로직」(TS 엔진 한 벌 · 서버 Node) · 제안서 https://claude.ai/artifact/4iCAGPhE9maauGNTkXg5he · 허락 전 착수 금지 · 완료 처리(10-4): 35-5 · 35-7 로 함 +- [x] 35-5 시트 로직 시험판(사용자 허락 10-1 오후) — 위 가로 다이어그램 · 상자 = 시트 · 관계 = 엑셀 수식 · 입출력 = 알약 · 저장 = `M01_MasterData/SheetLogic/_data/`(git 밖 · 폴더째 지움) · 정함 `docs/raw/M01_쉬운단가/시트로직_정함.md` · Model · Eval(sub7) · 서버 · Api(sub4) · Diagram(sub8) · Panel(sub2) · Table(sub3) · Item · Result(sub5) · Import(sub6) · 틀 · 잇기 · 끝 검증(sub1) · SL 서버 검증완료 · Model 검증완료 · Diagram 검증완료 · Item · Result 검증완료 · Table 검증완료 · Import 검증완료 · 잇기 · 끝 검증완료(sub1) — 걸음: S1 ①②③ 각 3(79,280 · 73,085 손셈 같음 · 2.5m 문턱은 품셈 표 3m 부터라 2.5~3m 해당 없음) · S2 ① 3 ② 4(찰쌓기 표로) ③ 5 · S3 ① 약 19(경사 급 65,018) ② 9(내 표 붙여넣기 · 할증 10) ③ 7(잡재료 2% 17,551) · S4 선 끊기 2 · 상자 지우기 2(물음 한 번) · Ctrl+Z 되돌림 · S5 0(상자 · 선 · 값 한 장 · 고르면 이어진 선 진하게) · S6 항목 일위대가 11 · 다른 로직 상자 13(덤프트럭 16.2) · 새 로직(품셈 없이) 25 걸음 → 저장 서버 36,000 = 화면 → 다시 열림 → 지우기 2 · 서버 = 화면 4/4(36,000 · 17,550.9 · 10.12 · 다시 열기) · 가져오기 6/6 /calc 같음(0 · 60,984/79,280 · 2,064,816 · 670 · 16.2 · 57,516) · 예외(빈 이름 · 같은 상자 · 순환 · 빈 입력 물음 · 저장 안 하고 닫기 · 권한 없음 sl 5/5 403 · 밝음 · 어두움) · 세 칸 안쪽 세로 밀기 없음(6개 모든 상자) · 폴더 밖 참조 LogicLab.ts 2 + main.py 1 · _data 비움 · 남은 흠(다른 창): 가져온 표 상자 + 결과 칸 목록 예시값이 글자로 들어가 30 등에서 해당 없음(sub5) · 가져온 로직 화면 번호 · 자재 검색어 · 끝 검증완료(S1~S6 · 새 로직 · 서버 = 화면 4/4 · 가져오기 6/6) · 남은 흠: 결과 칸 예시값 선택이 글자 「30」 으로 들어가 표 찾기 해당 없음(sub5 · 사용자 지시 뒤) +- [x] 35-5a 사용자 요청 — 다이어그램 띠 · 고른 상자 시트 · 결과 높이를 안쪽 세로 스크롤이 없을 때까지(sub1) +- [x] 35-6 로직 문법 먼저(사용자 방향 10-1 14시 무렵 · 화면보다 로직) — 바탕 표(인력 378 · 재료 30,767 · 기계 654 · 소요량 2,535 · 계수 498 · 환율 · 요율 · 사용자 표 — 보기 · 만들기 · 비목은 표가 정함) · 단위 넷(값 · 표 · 정보 · 요소) + 묶음(세분화 · 묶기 · 분류 · 묶음째 잇기 · 겹 묶음 · 틀 다시 쓰기) · 흐름 셋(정보 만들기 · 잇기[그대로 · 바꿔서 · 갈라서 · 모아서 · 묶음째] · 내기) · 확인 도구 · 제안서 https://claude.ai/artifact/BFWvm1Q4fhCSZ4Rt34UgYY · 다음 = 문법 확정 → 종이 연습(절 10개) — 허락 전 착수 금지 · 완료 처리(10-4): 35-7 · 35-9 흐름도로 갈음 +- [x] 35-5b 단추 공용 양식(점검 끝 · 고침은 사용자 지시 뒤) — 날 단추 23(목록 줄 8 · 칩 · 역할 · 보기 바꿈 11 · ▾ 2 …) · sm 단추를 44px 로 덮기 3 · 큰 시작 56px 3 · `docs/raw/M01_쉬운단가/시험판_단추_점검.md` · 완료 처리(10-4): 시험판 지움(35-9)으로 접음 +- [x] 35-7 단가산출 다이어그램 시험판(사용자 기준 10-1 14시 무렵) — 입력 · 함수 · 출력 상자 · 자유 배치 · 함수 = 엑셀 · 출력 하나 · 출력 = 비용 종류 · 원가 · 폴더 `M02_MasterTemplete/CostFlow/`(_data · _sample) · 정함 `docs/raw/M01_쉬운단가/원가다이어그램_정함.md` · Model · Eval(sub7) · 서버(sub4) · Diagram(sub8) · 함수 판(sub2) · 입력 판(sub3) · 출력 · 원가(sub5) · 견본(sub6) · 단추 · 페이지 · 잇기 · 끝 검증(sub1) · Model 검증완료 · Output · Summary 검증완료 · Sample 검증완료 · FuncPanel 검증완료 · Diagram 검증완료 · CF 서버 검증완료 · InputPanel 검증완료(사용자 요소 만들기는 서버 Api 가 main 에 오면 확인) · 잇기 · 끝 검증완료(sub1) — ① 빈 판에서 손으로 메쌓기 꼴(변수 4 · 소요량 표 2 · 인력 2 · 함수 증가율 → 석공 / 보통인부 병렬 · 출력 노무비 2) 9 상자 · 약 138 손놀림 → 원가 148,471 = 견본(증가율 40 · 석공 97,882.512 · 보통인부 50,587.992 손셈 같음) ② 비용 종류 바꾸면 148,471 이 노무비 → 경비 → 재료비 로 옮겨감 · 「값」 이면 원가에서 빠짐(각 1 걸음) ③ 상자 끌어 옮김(1520,190 → 1675,306) · 저장 → 다시 열면 그대로 ④ 함수 출력 하나(다른 칸 고르면 옮겨감 · 알약 하나) · 순환 막힘 · 출력에서 나가는 선 없음 · 빈 받는 자리는 저장 때 물음 ⑤ 사용자 요소(시험 보조공 200,000) 5 걸음 · 마스터 그대로 · _data 에만 ⑥ 저장 때 서버 = 화면 2/2(148,470.504) ⑦ 단위 오른쪽 위(- → 원/㎡) · 색 바꾸기(Entry 판 머리) · 「다이어그램」 이름 · 다이어그램 726px · 안쪽 스크롤은 함수 격자(가상 200줄)만 · 밝음 · 어두움 · 권한 없음(cf 길 6/6 403 · 화면 못 들어감) · 견본 셋 = /calc · 손셈(148,471 · 3,000 · 15.552) · 폴더 밖 참조 LogicLab.ts 2 + main.py 1 · _data 비움 · 끝 검증완료(손으로 메쌓기 9 상자 = 견본 148,471 · 비용 종류 옮김 · 자리 저장 · 규칙 · 사용자 요소 · 서버 = 화면 · 견본 셋 손셈) · 함수 출력 기본 이름 겹침 고침(sub7 2be767d18) · 남은 것: M01 단가산출 단추 줄이 셋째 줄까지(시험판 단추 넷 · 사용자 결정) +- [x] 35-7a 브레인 실제 검증(10-1 16:45) 흠 여섯 고침 — ① 고칠 것 칸 지난 경고 · ⑥ 시험판 단추 「시험판 ▾」 로 묶기(sub1) · ② 알약 쉼표 · ③ 표 알약 글 · ⑤ 맞춤 확대 90% 아래 안 감(sub8) · ④ 입력 상자 공통 단위(sub3) · 다시 검증완료(16:55 · 고칠 것 칸 비어 있음 · 알약 쉼표 · 표 · 40줄 · 인력 원/일 · 확대 90% · 막대 한 줄 · 시험판 ▾ 넷 · 원가 148,471) + +## 35-8. 단가산출 다이어그램 2판 — 재료 선택 자리 · 선 규칙 (계획 · 허락 전) + +- 사용자 확정: 고르는 방식은 재료만(다른 그룹은 상자를 늘림) · 설계 때 입력은 오른쪽 아래 검증 칸 · 나중에 프로젝트에서 일위대가로 쓸 때 재료는 목록에서 고르거나 단일 값 · 표에 따라 출력을 가르는 것은 엑셀 수식 대신 선 규칙. +- 정함(브레인): 재료 요소 = 고정 · 선택 자리 · 조건 찾기 · 알약 이름 = 자리 이름 · 선 규칙 = 선 하나에 하나(표에서 찾기 · 조건 통과 · 바꾸기) · 규칙은 받는 칸 엑셀 수식으로 바뀌어 엔진 한 벌 · 선 위 규칙 글 늘 보임. +- [x] 35-8a 틀(Model) — 재료 요소 방식 · 범위(구분 · 상세구분 · 찾는 말) · 조건(규격 ← 알약) · 기본 품목 · 선 규칙 칸 · 규칙 → 받는 칸 수식 · 조건 선 겹침 · 안 맞음 검사 — node 시험(sub7) · 완료 처리(10-4): 35-9 에서 함 +- [x] 35-8b 서버 — 선택 자리 기본 품목 · 조건 찾기 단가(맞음 0 · 여럿 알림) · 「설계 때 정할 것」 목록(변수 + 재료 자리) 내보내기 — 시험(sub4) · 완료 처리(10-4): 35-9 에서 함 +- [x] 35-8c 입력 판 — 재료 방식 · 범위 · 조건 규칙 · 기본 품목 · 찾기 고침(쪽 넘기기 · 띄어쓰기 AND · 규격까지) — ORCA(sub3) · 완료 처리(10-4): 35-9 에서 함 +- [x] 35-8d 검증 칸 — 재료 선택 자리를 변수처럼(범위 안 목록 고르기 · 찾기) · 조건 자리는 잡힌 품목 표시 — ORCA(sub5) · 완료 처리(10-4): 35-9 에서 함 +- [x] 35-8e 선 규칙 — 선 위 규칙 글 · 선 누르면 아래 판에서 규칙 고르기(표 찾기: 조건 칸 ↔ 알약 · 꺼낼 칸 / 조건 통과: 알약 · 같음 · 다름 · 이상 · 이하 · 값 / 바꾸기: × · ÷ · +% · 단위 · 끝수) · 고른 상자 판에 들어오는 선 규칙 목록 — ORCA(sub8 · 판 sub2) · 완료 처리(10-4): 35-9 에서 함 +- [x] 35-8f 견본 · 끝 검증 — 견본 둘 더(지반에 따라 굴삭기 / 브레이커 가르기 · 흄관 관경 조건 찾기) · 사용자처럼: 함수 상자 없이 표 찾기 · 가르기 · 검증 칸에서 재료 고르기 · 방법 걸음 수(sub6 견본 · sub1 잇기 · 끝 검증) · 완료 처리(10-4): 35-9 에서 함 +- [x] 35-8g 다이어그램 마우스 — 판 옮기기 = 가운데 단추 끌기 · 고르기 = 왼쪽 누름 · 왼쪽 끌기로 글자가 선택되지 않음(다이어그램 영역 글자 선택 막음) · 상자 옮기기는 상자 머리 왼쪽 끌기 그대로 — ORCA(sub8) · 완료 처리(10-4): 35-9 에서 함 +- [x] 35-8h 「고른 상자」 → 「상세 내용」 · 상자뿐 아니라 알약 · 선을 골라도 내용이 바뀜(알약 = 값 · 단위 · 어디서 오나 · 어디로 가나 / 선 = 선 규칙 고르기) — ORCA(sub1 잇기 · 알약 · 선 판 sub2) · 완료 처리(10-4): 35-9 에서 함 +- [x] 35-8i 상세 내용 단추 · 칸 크기 — 함수 판 꾸밈이 공용 작은 단추를 44px 로 덮는 줄(`M02_CF_FuncPanel.css` `.m01-cfpanel .ui-btn …`)을 지우고 판 안 단추를 모두 공용 작은 크기(createButton size "sm")로 · 입력 · 고르기 칸도 공용 크기 — ORCA 수치(sub2 · sub3 · sub5 각자 판) · 완료 처리(10-4): 35-9 에서 함 +- [x] 35-8j 상세 내용 머리 — 왼쪽 「상세 내용」 · 오른쪽에 고른 상자 제목 · 알약을 골랐으면 그 오른쪽에 알약 이름(예 「높이 증가율 찾기 › 증가율」) · 상세 내용 안 칸 차례 정리(머리 · 상자 설정 · 본문 · 경고) — ORCA(sub1) · 완료 처리(10-4): 35-9 에서 함 +- [x] 35-8k 단위는 고르기 — 단위 칸을 목록 고르기로(치면 걸러져 보이고 고르기는 사용자 · 손으로 쓴 글은 안 받음) · 목록 = 단위 표준(35-8l) 단위 + 마스터 결과단위 + 금액 단위(원) · 「-」 = 없음 · 함수 · 출력 · 변수 · 사용자 요소 단위 모두 같은 고르기 — 공용 고르기 파일 `M02_CF_UnitPick.ts`(sub2) · 판마다 바꿔 끼움(sub2 · sub3 · sub5) · 완료 처리(10-4): 35-9 에서 함 +- [x] 35-8l 단위 표준 표 — 산림품셈 1-2-2 「설계서의 단위 및 소수의 표준」(원문 `resources/knowledge/original/원가계산/산림_표준품셈/본문/제01장_적용기준/1-02_설계_및_수량.md` · 종목 약 70 · 규격 단위 · 소수자리 · 단위수량 단위 · 소수자리 · 비고 · 주 ①~③)을 표로 — 소수자리 「-」 = 0 · 단위 「-」 = 없음 · 원문 흠은 사용자가 고침(시험판에서 보고 고치는 표 · 저장 `CostFlow/_data` · 처음 값 `CostFlow/_standard/`) — 시험(sub6) · 완료 처리(10-4): 35-9 에서 함 +- [x] 35-8m 금액 단위 표준 — 같은 절 「금액의 단위표준」(설계서 총액 1,000원 · 소계 · 금액란 · 일위대가표 계금 1원 · 일위대가표 금액란 0.1원 · 모두 미만 버림)을 같은 표 자리에 — 출력 상자(비용) 금액 기본 = 일위대가표 금액란 0.1원 미만 버림 · 원가 합계 기본 = 계금 1원 미만 버림 · 바꿀 수 있음(sub6 표 · sub5 출력 · 원가) · 완료 처리(10-4): 35-9 에서 함 +- [x] 35-8n 끝수 고르기 — 함수 상자 출력 · 출력 상자에 「끝수」(소수자리 · 반올림 / 올림 / 버림) · 단위를 고르면 단위 표준에서 소수자리를 제안(주 ① 반올림 · ② 항목 값이 우선) · 고른 끝수는 출력 칸 수식(ROUND 계열)으로 — 엔진 한 벌(sub7 틀 · sub2 함수 판 · sub5 출력 판) · 완료 처리(10-4): 35-9 에서 함 +- [x] 35-8o 로직끼리 잇기 · 흐름 따라가기 — 저장한 다른 로직을 입력 상자로 불러 그 출력 알약을 받음(일위대가 → 일위대가) · 알약 · 선을 고르면 값이 어디서 와서(출처 사슬) 어디로 가는지(가는 곳) 다이어그램에서 길을 칠하고 상세 내용에 줄로 · 값이 끊긴 곳(빈 받는 자리 · 통과 안 된 조건 · 표에 없는 조건) 표시 — 시험 · ORCA(sub7 틀 · sub8 다이어그램 · sub1 상세 내용) · 완료 처리(10-4): 35-9 에서 함 +- 차례(사용자 10-1 · 「데이터의 흐름과 값의 연결에 집중」): **1단계 = 흐름 · 연결** a 틀 · b 서버 · e 선 규칙 · o 로직끼리 · 흐름 따라가기 · h 알약 · 선 고르기 · c · d 재료 자리 · f 견본 · 끝 검증(흐름 기준 — 값이 어디서 와서 어떤 규칙을 지나 어디로 가는지) / **2단계 = 값의 뜻 · 보기** k 단위 고르기 · l 단위 표준 · m 금액 표준 · n 끝수 · j 머리 · g 마우스 · i 단추 + +## 35-9. 단가산출 흐름도 시험판 (사용자 지시 10-1 17시 무렵 · 브레인이 끝까지 · 35-8 전체) + +- 사용자 확정: 브레인이 전체 제어해 새 시험판 하나 · 단가산출 다이어그램(시험)만 남기고 옛 시험판 셋 삭제 · 새 폴더(필요 없으면 바로 지움) · 데이터 흐름 · 값 연결 먼저 · 자유도 높고 이해하기 쉬운 화면 · 끝까지 구현 · 검증. +- 정함 `docs/raw/M01_쉬운단가/흐름도_정함.md` · 폴더 `M02_MasterTemplete/DataFlow/` +- [x] 35-9 창 여덟 — 옮기기 · Model · Eval(sub7) · 옛 시험판 지우기 · 들어가는 길 · 상세 내용 머리 · 잇기 · 끝 검증(sub1) · 서버(sub4) · 다이어그램(sub8) · 상세 내용 판 · 단위 고르기(sub2) · 입력 판(sub3) · 출력 · 검증 칸(sub5) · 표준 표 · 견본(sub6) · 표준 · 견본 검증완료 · Model 검증완료 · 출력 · 검증 칸 검증완료 · Diagram 검증완료 · 입력 판 검증완료(서버 자재 찾기 · UnitPick 이 main 에 오면 다시 확인) · DF 서버 검증완료 · 잇기 · 끝 검증완료(sub1) — ① 빈 판에서 함수 상자 없이 네 흐름 73 걸음: 재료 조건 찾기(관경 → 흄관 자리 · 13 개 중 하나 · 125,870) 21 · 선 규칙 표에서 찾기(높이 증가율표 → 출력) 19 · 조건 통과 두 갈래(지반 = / ≠ 토사) 27 · 다른 로직 잇기(견본 지반 · 지반 알약 이음) 6 ② 알약 · 선 고름 → 온 길 · 가는 길 칠함 + 상세 내용 줄(지반 → 다른 로직 · 두 조건 선 · 흄관 ← 관경) ③ 끊긴 곳 넷 보임(빈 받는 자리 · 통과 안 됨 · 표에 맞는 줄 없음 · 맞는 재료 없음 — 알약 · 선 「!」 · 끊긴 곳 줄 누르면 그 자리 고름) ④ 검증 칸 관경 600 → 800 = Φ800 품목 215,530 · 지반 토사 → 암 = 내 두 갈래 뒤집힘 + 다른 로직 2,000 → 10,000 · 다른 로직 설계 값 바꿈 10,000 → 20,000 ⑤ 단위 고르기 「mm」 = 3 개로 걸러짐 · 손글 안 받음 · 끝수 80,000.37 = 기본 225,870 / 1원 올림 225,871 · 표준 표 고침 저장(3 → 2) → 처음 값으로 되돌림 ⑥ 가운데 끌기 = 판 옮김(−60, −30) · 왼쪽 끌기 = 판 그대로 · 글자 선택 막힘 · 단추 18px 모두 sm · 칸 30px ⑦ 저장 → 다시 열기 = 상자 9 · 선 6 · 자리 · 규칙 글 그대로 · 서버 다시 셈 = 화면 187,910 · 견본 다섯 사본 저장 서버 = 화면(148,470.504 · 2,000 · 84,680 · 3,216.6 · 값 15.552) · 밝음 · 어두움 · 로그인 없음 df 길 6/6 401(관리자 아닌 계정 403 은 계정 없어 못 함) · 폴더 밖 참조 LogicLab.ts 2 + main.py 1 · _data 비움 · 검증 중 고친 내 흠(047f3a279 · 28469de62): 재료 조건 찾기 · 선택 자리 값을 서버에 안 물음(흄관 견본 관경 600 → 400 = 211,700 → 116,600 · 1m 84,680 → 46,640 · 1000 = 235,120 · 선택 자리 Φ600 → Φ400 = 211,700 → 116,600) · 「상자 이름」 칸(입력 · 함수 · 같은 이름 · 빈 이름 막음 · 출력은 출력 판) · 머리 이름 · 단위 칸 작은 크기(30px) · 「새로 만들기」 단추 76px(칸 폭 710 아님) · 표준 표 모달 제목 한 번 · 남은 흠(sub2): 선 규칙 표에서 찾기 조건 칸에 범위 이름(「높이」)이 없어 범위 찾기를 화면에서 못 만듦(높이 5 → 줄 없음 · 4 만 맞음) · 브레인 실제 검증완료(흠 14 고침 · 흐름도 시험 81 · 견본 다섯 서버 = 화면 · 보고 JgfqRzLPYgqXG7vB2cV1Yj) · 검증 대기 + +## 36. 단가산출 흐름도 본 프로그램 적용 (사용자 지시 10-1 21:40 · 결정 반영 22시 · 착수 승인 22:20 · 밤새 진행 · 아침 보고) + +- 사용자 확정: 흐름도 = 최종 · 로직 목록에서 로직을 열면 지금 내용 그대로 + 오른쪽 흐름도 패널(같은 로직) · 「식으로 새로 만들기」 지우고 「신규 로직」(흐름도 새로 만들기) · 「본떠 만들기」 → 「로직 복사/생성」(신규 로직 오른쪽) · 시험판 단추 · 입구 지움 · DataFlow 폴더 유지. +- 근거(마스터 로직 57 파일 · 1,472 로직 · 입력 3,518 · 중간 식 3,168 · 호표 줄 5,265): 식 말 = 찾기(표 · 조건 · 칸) 8,152 · 만약 3,532 · 또는 185 · 최대/최소 173 · 올림/반올림/버림 141 · 로직() 41 → 흐름도 표에서 찾기 · 조건 · 함수 · 다른 로직 입력으로 옮길 수 있음. +- 사용자 결정 D1(그대로): 「품셈원문도 제대로 구현된게 아니기 때문에 내가 흐름도까지 만들어서 로직을 볼려는 거잖아. 그러니 당연히 둘다 같은 내용 … 원문과 대조하면서 검증할때 잘못된게 있으면 흐름도에서 수정할거야」 → 한 로직 · 두 보기: 왼쪽 지금 내용 = 보기 · 고치기는 흐름도(공용 로직 포함 · 시스템 관리자) · 흐름도로 저장한 로직은 흐름도가 정본 · 왼쪽은 흐름도에서 만든 같은 꼴 · 아직 저장 안 한 로직은 지금 내용에서 자동으로 그린 흐름도(값 대조로 같음). +- 사용자 결정 D2(그대로): 「흐름에 대한 로직은 costflow폴더에 있을테니 일단은 그냥 두고 쓸대없는 견본과 버튼만 삭제」 → CostFlow 폴더 그대로 · 시험판 단추 · 견본 목록 지움(흐름도는 DataFlow 폴더만으로 돌아감 · CostFlow 를 부르지 않음). +- 사용자 결정(함수 상자): 「함수박스를 기준으로 하나의 결과가 나가는게 맞는것같아. 여러개가 동시에 나가면 혼란만 가중」 → 함수 상자 하나 = 결과 알약 하나(그대로) · 식은 상자 겉에 넣지 않음(상자가 커짐) · 함수 상자 · 선을 고르면 상세 내용에 사칙연산 · 조건이 보이고 고치면 바로 따라감. +- 칸 연결 = M02 방식 그대로(칸을 고른 채 알약을 눌러 하나씩 연결 · 식은 칸 주소 · `spreadsheet_links.ts` 직접 확인) · 계산 엔진 · 이름 풀기 · 이름 규칙 없음 — 보기만 칸 주소를 연결된 알약 이름으로 바꿈. +- 사용자 결정(조건): 「조건문은 현재를 유지하는게 보기 편한것같고 대신 어떤값을 지정할지는 입력된 리스트에서 선택 … 조건 값을 내가 넣으면 오기 … 복수 조건문도 넣어주고」 · 「조건 만들기도 엑셀 기능 내부에서 존재해야해」 → 선 조건은 지금 웹앱 모양 · 조건 값은 고른 알약이 가질 수 있는 값 목록에서 고름(변수 고르기 · 표 칸 값 · 재료 규격 등 · 목록이 없는 수는 숫자 칸 + 범위 검사) · 조건 여러 줄(그리고 / 또는) · 함수 상자 엑셀 시트 안 「조건 만들기」(칸 고름 → 만약 · 아니면 만약 · 아니면 줄 → 그 칸에 IF 식 · 값은 같은 목록에서 고름). +- 안전: 화면 시험 저장은 마스터 사본 폴더에서만(서버가 환경 변수로 사본 폴더를 씀 · 실제 resources/master_data 는 안 바뀜 · git status 로 확인). +- 창: sub2 36-0(판 · 조건 만들기) · sub7 36-0b(조건 틀 · 값 목록) → 36-2 · sub6 36-1 → 36-7 · sub8 36-3 · sub4 36-4 서버 · 사본 폴더 · sub5 36-4 왼쪽 보기 · sub1 36-5 · 잇기 · 36-8 · sub3 36-6 +- 브레인 정함(조사 뒤 · 아침 보고에 알림): 옛 로직 「기계 · 직종 · 유가 자리」(EQ:{기계} · LB:{직종} · MO:경유:{지역} · 입력값으로 요소를 고름 · 71 로직)는 새 모드 없이 이미 있는 「조건 찾기」(이름 · 키 같음 · 변수 알약)를 인력 · 기계 · 유가 요소에도 씀 · 대조 입력 = check_master.시험입력 · 로직 셈을 부르는 곳(Store.calc · text · master_combo.run · M02_Item_Prices.calc · /logic/auto)은 한 곳에서 흐름 갈래. +- [x] 36-0 함수 상자 안 보기 · 칸 연결 조정(36-2 보다 먼저) — ① 상세 내용 「식 보기」: 결과 칸 식을 칸 주소 → 연결된 알약 이름으로(예: 1m 재료비 = 흄관 단가 ÷ 2.5) + 값 들어간 식(= 211,700 ÷ 2.5 = 84,680) · IF 는 만약 / 아니면 줄 · 지금 타는 갈래 표시 · 중간 칸 식도 차례대로 · 칸 식을 고치면 바로 다시 셈 → 다이어그램 · 원가 따라감 ② 「칸 연결 표」(칸 · 연결 알약 · 입력 / 결과 · 이어진 선 · 표에서 바로 바꿈 · 풀기) + 칸 색(입력 파랑 · 결과 초록 · 상자 색과 같음) · 결과 칸 하나 ③ 선을 고르면 그 선 규칙도 같은 「식 보기」 꼴(예: 굴착량 → 지반 = 토사 일 때만 → 굴삭기로 파기) ④ 엑셀 시트 안 「조건 만들기」 — 칸을 고르고 열면 만약 [알약] [비교] [값 고르기] → [알약 · 값] 줄 · 아니면 만약 더하기 · 아니면 → 그 칸에 IF 식(엑셀로 쓴 IF 도 같은 줄로 다시 열림 · 못 바꾸는 식은 식 그대로) · 검증완료(sub2 · 식 보기 · 칸 연결 표 · 조건 만들기 · 선 조건 여러 줄 · 시험 8) · 검증 대기 +- [x] 36-0b 선 조건 손질 — 조건 값 = 고른 알약의 값 목록에서 고름(Model 값 목록 함수 · 변수 고르기 · 표 칸 값 · 재료 규격 · 목록 없는 수는 숫자 칸) · 조건 여러 줄(그리고 / 또는) · 규칙 → 받는 칸 엑셀 식 · 서버 Compute 같은 뜻 · 선 위 이름표 · 겹침 · 안 맞음 검사 그대로 · 검증완료(sub7 · 조건 여러 줄 그리고/또는 · 값 목록 · 서버 = 화면 · 시험 30) · 검증 대기 +- [x] 36-1 조사 — 옛 식 문법 전수(서버 파서 · 요소 부르기 꼴 · 따옴표 글) · 변환 못 하는 꼴 목록 · /calc 를 부르는 곳 · 검증완료 +- [x] 36-2 변환 — 옛 로직(입력 · 중간 · 호표 · 덧줄 · 결과 · 끝수) → 흐름도(입력 상자 · 표 알약 + 표에서 찾기 · 함수 · 출력 상자) + 자동 배치 · 서버 파서로 · 옛 식에 나온 이름마다 알약을 칸에 연결하고 식은 칸 주소로(만약 → IF · 최대 → MAX · 찾기 → 표에서 찾기) · 함수 상자 하나 = 결과 하나 · 전수 대조(변환 흐름도 값 = 옛 계산 값 · 맞는 비율 · 안 맞는 까닭) · 검증완료(sub7 · M02_DF_Convert · 전수 1,472 시험입력 96.7% · 기본 입력 97.4% · 안 맞는 49 = 재료 검색어 자리 18 · 재료 빈 검색어 13 · 표 범위 규칙 경계 15 · 단가 빈 품목 3 · tmp/convert_report_36.json) · 검증 대기 · 마지막 대조: 시험입력 · 기본 입력 모두 1,472/1,472(100%) · 빈 단가 = 옛 셈 뜻(그 값 쓰는 식 빈 값 · 비목 합 0 · 끊긴 곳 「단가 없는 품목을 씀」) +- [x] 36-3 오른쪽 패널 — 로직 상세 오른쪽에 흐름도(다이어그램 · 상세 내용 · 원가 · 검증) · 너비 끌어 조절 · 접기 · 「크게 보기」(전체 화면) · 검증완료 +- [x] 36-4 저장 · 정본 — 로직 줄에 「흐름」 칸(화면 [저장] 때만 씀) · 흐름이 있으면 /calc 가 흐름도로 셈(서버 다시 셈) · 왼쪽 기본정보 · 단가산출 상세 · 텍스트 수식 · 시험 계산은 흐름도에서 만든 같은 꼴(보기) · 공용 로직도 흐름도로 고침(시스템 관리자) · 캐시 → [저장] 한 번 · [고친 것 버리기] 함께 · 왼쪽 보기 검증완료(sub5) · 서버 검증완료(sub4) +- [x] 36-5 단추 — 「식으로 새로 만들기」 지움 → 「신규 로직」(빈 흐름도 + 기본정보 · 저장 = 자체 GX 새 키) · 「본떠 만들기」 → 「로직 복사/생성」(흐름도도 복사 · 신규 로직 오른쪽) · 「시험판 ▾」 지움 · 잇기 검증완료(sub1 · accf8ecc4 · abbf61971 · 0d45014e3) — 「식으로 새로 만들기」 · 시험판 ▾ · 시험판 단추 둘 지움 · 단추 차례 ← 로직 목록 · 신규 로직 · 로직 복사/생성 · 로직 지우기 · 고친 것 버리기 · 저장 · 두 칸(LogicLab_Flow) · 오른쪽 패널(흐름 받기 · 고침 → drafts → [저장] 한 번 · 버리기 함께 · 흐름 로직이면 왼쪽 흐름 보기) · 신규 로직 기본정보 칸(LogicLab_Basic) → 저장 = 서버 added 키로 엶 +- [x] 36-6 다른 로직 입력 — 대상 = 마스터 로직 전체(찾기) · 흐름도 로직이면 흐름도 셈 · 아니면 옛 셈 · 목록 · 찾기 · 쪽 · 알약 이음 검증완료(서버 흐름 받기 · 값 셈 길이 main 에 오면 GF 로직 값 확인) +- [x] 36-7 정리 — 「시험판 ▾」 · 시험판 입구 · 흐름도 시작 화면 견본 목록 지움 · 시험이 쓰는 견본은 resources/tester 로 · 시험 저장 길 숨김 · DataFlow · CostFlow 폴더 그대로 · 검증완료 +- [x] 36-8 끝 검증 — 흐름 기준 · 옛 로직 열기 → 오른쪽 흐름도 값 = 왼쪽 계 · 신규 로직 → 저장 → 다시 열기 · 복사 · 권한 · 브레인 실제 검증 · 보고 · 끝 검증완료(sub1 · 사본 폴더 tmp/master_copy_36 + AISLO_MASTER_DIR · 네 포트) — ① 옛 로직 셋: GF000001(찾기) 오른쪽 34,413 = /calc 34,413.6 = 왼쪽 34,414(보임 끝수만 다름) · 변환 알림 한 줄 / GF000007(만약) 0 ≠ 597,285 · GF000288(로직()) 0 ≠ 1,234,760.96 — 아래 흠 ② GF000001 함수 식 ×1.5 → 오른쪽 · 왼쪽 51,620 따라감 → [저장](공용 GF · 사본) → 다시 열기 상자 6 · 변환 알림 없음 · /calc flow 51,620.4 = 화면 ③ 신규 로직 65 걸음(변수 셋 · 함수 · 조건 만들기 · 출력 둘 · 조건 선) → 저장 GX000004 → 다시 열기 상자 4 · 선 4 · 화면 100,000 = /calc 100,000 = 왼쪽 계(높이 2 = 0 · 지반 암 = 50,000) ④ 로직 복사/생성 2 걸음 → GX000005 상자 4 · 선 4 · 100,000 ⑤ 선 조건 두 줄 「지반 = 토사 그리고 높이 ≥ 3 일 때만」 · 값 고르기(토사|암) · 높이 2 → 0 · 또는 → 50,000 · 조건 만들기 =IF(A2>=3,A3,0) ⑥ 식 보기 「결과 1 = 금액 × 높이 ÷ 4」 · IF 만약 / 아니면 줄 · 칸 연결 표 ⑦ 크게 보기 1484×1076 · 거기서 고친 것 패널로 · 접기 40px · 펴기 · 너비 320 → 522 ⑧ 고친 것 버리기 5 → 4 · 캐시 비움 · 로그인 없음 flow · save · calc · logics 401 · 시험판 · 옛 단추 글 0 · resources/master_data 바뀜 0 · 흠(다른 창): 변환 숫자 입력 예시값이 비어 있음(시험입력 · 기본 입력을 안 넣음 — GF000288 나무높이 6 · 집재거리 100 · GF000007 정리량 50 …) · GF000007 「표 QF000033 → 할증」 표에 맞는 줄 없음(서버 477,828 = 할증 25% 빠짐 · 화면 0) · GF000288 다른 로직 GC000270 기계 자리 「EQ:{기계} … 자리」 못 풂(#N/A → 0) · 칸 #N/A 일 때 화면 셈 0 · 서버 셈 그대로(둘이 다름)(sub7) · 식 보기 값 줄이 선 규칙을 지난 값을 안 씀(「(#N/A × 0) = #VALUE!」 · 결과 알약은 34,413.6)(sub2) · 조건 통과 둘째 줄 값 목록 「number:0」 글 · 조건 만들기 만약 목록에 놓인 알약 없음(sub2) · 옛 로직 왼쪽 계 반올림 · 흐름 원가 계금 1원 버림(1원 차이) +- 36 마무리(10-2 02시 · 브레인 실제 검증완료): 전수 변환 대조 시험입력 1,469/1,472(99.8%) · 기본 1,466(99.6%) · 남은 3 = 마스터 빈 단가 · 시험 141 + 변환 시험 초록 · 사본 서버 저장 → 사본만 바뀜 · 다시 열기 흐름 정본 · GF000001 34,414 = 34,414 · GF000003 8.946 = 8.946 · 식 보기 값 줄 · 검증 대기 + +## 37. 일위대가 조합 다이어그램 (사용자 지시 10-2 09시 · 계획 · 착수) + +- 사용자 확정: 「복잡한 경우가 있으니 일위대가 조합도 다이어그램으로 표현 … 중요하게 생각하는 정보가 다르니 잘 구분」 · 조합은 계산하지 않고 로직을 뭉쳐 비용 구분별로 표기 · 입력은 로직별로 묶어 빈 값은 사용자가 넣음 · 출력은 로직마다 · 미리 보기 값은 구조물 조합으로 안 넘김 · 「흐름도는 별도 로직이 아님」(단가산출 로직을 쓰는 방법). +- 모양: 왼쪽 = 로직별 입력 묶음(받아야 할 값 · 빈 값 표시) → 가운데 = 로직 카드(키 · 이름 · 결과 단위 · 식 · 함수 안 보임) → 오른쪽 = 비용 구분 칸 셋(노무비 · 재료비 · 경비 · 로직마다 선으로 모임) + 값 로직은 「값」 칸 · 이름이 같은 입력(지역 · 자재지역 등)은 점선으로 이어 보이기만(검사 없음) · 로직 카드를 누르면 그 로직의 단가산출 로직 화면(흐름도)으로 · 계산 · 고치기 없음(묶음 고치기는 지금 표) · 흐름도와 모양 · 색을 달리함. +- 자리: 일위대가 조합 상세(M02_MasterTemplete_Labor_Edit)의 미리 보기와 같은 값 — 미리 보기 입력 칸에 넣은 값이 다이어그램에도 · 다이어그램 빈 입력 알약을 누르면 미리 보기 그 칸으로 · 미리 보기 결과(로직별 노무비 · 재료비 · 경비 · 계)가 카드 · 칸에. +- [x] 37-1 다이어그램 판 — M02_MasterTemplete_Labor_Diagram.ts(+css) 새 파일 · 고정 세 칸 배치(자동) · 판 옮기기 · 확대 · 한눈에 · 밝음 · 어두움 · 좁은 폭 +- [x] 37-2 잇기 — Labor_Edit 에 「다이어그램」 칸(접기) · 미리 보기와 값 한 벌 · 빈 입력 누름 → 미리 보기 칸 · 로직 카드 누름 → M01 단가산출 로직 그 로직 열기(setPendingLogicKey 꼴) +- [x] 37-3 검증 — 조합 셋(로직 2 · 9 · 14개)으로 입력 묶음 수 = 로직 입력 수 · 출력 = 미리 보기 줄 · 비용 칸 합 = 미리 보기 합 · 빈 값 표시 · 같은 이름 점선 · 로직 열기 · 브레인 실제 검증 · 보고 +- 창: sub2 37-1 · 37-2 · 37-3 + +## 38. 마스터 요소 화면 손질 (사용자 지시 10-2 09:40 · 착수) + +- 사용자 확정: ① 인력 · 재료 · 기계 · 소요량 · 계수 · 환율 · 요율 상세의 제목 · 단추 · 찾기 라벨 양식 = 단가산출 로직과 같게 ② 재료 컨테이너의 유가 · 전력을 별도 컨테이너로 ③ 단가산출 로직 「막힘만」 체크 — 의미 없으면 지움 ④ 단가산출 로직 컨테이너 「전체」 를 누르면 상세 목록 보기가 안 됨(다른 컨테이너는 됨). +- 브레인 정함: 「막힘만」 = 로직 검사에 걸린 로직만 거르기 · 지금 1,469 중 0 · 목록 줄에 「⛔ 막힘」 표시 · 까닭 말풍선은 이미 있음 → 체크만 지움(표시는 둠). +- [x] 38-1 양식 맞춤 — 일곱 상세 화면 머리(제목 · 단추 줄 · 찾기 칸 라벨)를 단가산출 로직 화면 꼴로 · 공용 단추 · 칸 크기(sub5) · 검증완료 +- [x] 38-2 유가 · 전력 컨테이너 — 왼쪽 패널에 재료 다음 「유가 · 전력」 컨테이너(재료_유가전력.json) · 재료 컨테이너에서는 뺌 · 고르기 · 찾기 · 상세 그대로(sub6) · 검증완료 +- [x] 38-3 「막힘만」 체크 지움 · 38-4 「전체」 목록 보기 고침(sub1) · 검증완료 — 원인: 로직 컨테이너 거름이 목록만 거르고 가운데 화면 전환(onLogic)을 안 부름 → Side 거름 run 에서 로직이면 onLogic · 「전체」 는 찾는 글도 비움 · 화면(5174): 로직 1,469 줄 → 인력 전체 → 로직 「전체」 = 로직 화면 1,469 줄(찾는 글 「굴착」 비움) → 산림품셈 330 → 03장 작업장 관리 10 → 로직 열기 → 「전체」 = 상세 닫힘 · 1,469 줄 · 막힘만 체크 0 · 화면 오류 0 +- 「고친 것 버리기」(사용자 물음 · 확인만): 저장 안 한 고친 것(화면 캐시)을 버리고 파일의 로직을 다시 엶 — 사본 저장은 없음 · 그대로 둠(사용자 결정 기다림) From 1871b6ffc2a06985f72c07081b8d1a4f507d5c73 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sun, 4 Oct 2026 17:20:47 +0900 Subject: [PATCH 04/14] =?UTF-8?q?feat(M02):=20=EB=A9=B4=EC=A0=81=EC=82=B0?= =?UTF-8?q?=EC=B6=9C=20=EC=B9=B4=EB=93=9C=20[=EC=95=8C=EC=95=BD]=20?= =?UTF-8?q?=EC=BC=AC=EB=81=94=20=E2=80=94=20=EC=95=84=EB=9E=98=20=EC=95=8C?= =?UTF-8?q?=EC=95=BD=20=EC=A4=84=20=EA=B8=B0=EB=B3=B8=20=EC=88=A8=EA=B9=80?= =?UTF-8?q?=20=C2=B7=20=EA=B3=84=EC=A0=95=20=EC=B7=A8=ED=96=A5=20=EA=B8=B0?= =?UTF-8?q?=EC=96=B5=20=C2=B7=20=EC=83=81=EC=84=B8=EB=8A=94=20=EB=8A=98=20?= =?UTF-8?q?=EB=B3=B4=EC=9E=84=20(PLAN=2039-4=20D1)?= 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_01W4uKaV4ZBhsJN4GHG7yRj4 --- A00_Common/b_page_state.ts | 2 ++ .../M02_MasterTemplete_ItemCards.ts | 18 ++++++++++++++++++ .../M02_MasterTemplete_Structure.css | 5 +++++ resources/tester/test_m02_card_sheet_edit.py | 14 ++++++++++++++ 4 files changed, 39 insertions(+) diff --git a/A00_Common/b_page_state.ts b/A00_Common/b_page_state.ts index cca13de9d..9f9e3ae48 100644 --- a/A00_Common/b_page_state.ts +++ b/A00_Common/b_page_state.ts @@ -62,6 +62,8 @@ export const STATE_REGISTRY = { /** M02 면적산출 · 재료산출 오른쪽 조합 패널 — 접힘 · 폭 */ "m02-combo-panel-collapsed": { bucket: "pref", scope: "global" }, "m02-combo-panel-width": { bucket: "pref", scope: "global" }, + /** M02 면적산출 카드 미리보기 아래 알약 줄 보임(기본 숨김 · 상세는 늘 보임) */ + "m02-card-pills": { bucket: "pref", scope: "global" }, /** B05 계획노선 편집 창 — 보던 화면(회전 · 가운데 · 배율). 프로젝트마다 따로 */ "routeedit-view": { bucket: "pref", scope: "project" }, /** B05 계획노선 편집 창 — 시점 · 종점 · 교각점 고정. [저장] 대상이 아니라 draft 에 안 둠 */ diff --git a/M02_MasterTemplete/M02_MasterTemplete_ItemCards.ts b/M02_MasterTemplete/M02_MasterTemplete_ItemCards.ts index fa329afec..3b3a24b3f 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_ItemCards.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_ItemCards.ts @@ -13,6 +13,7 @@ import { createButton, el, showConfirmDialog } from "@ui/ui_template_elements"; import { attachCollapsible } from "@ui/ui_template_collapsible"; +import { readStateRaw, writeStateRaw } from "../A00_Common/b_page_state"; import type { LinkOptions, SpreadsheetHandle, @@ -62,6 +63,8 @@ const TEXT = { readFailed: "{value} 파일을 못 읽음 — {why}", discard: "카드에 저장 안 한 고침이 있음 — 버리고 다른 항목을 열까요?", sheetHelp: "단축키 — 칸을 고르고 바로 침", + pills: "알약", + pillsTip: "아래 알약 줄 보임 · 숨김", }; /** 엑셀 카드 단축키 — 스프레드시트 미리보기(`spreadsheet_keys.ts`)가 받는 것만 */ @@ -221,6 +224,20 @@ export function createItemCards(opts: ItemCardsOptions): ItemCardsHandle { return card; }; const sheets = [sheetCard("area"), sheetCard("material")]; + // 면적산출 카드 [알약] — 아래 알약 줄 보임 · 숨김(기본 숨김 · 계정 취향 · 상세는 늘 보임) + const pills = createButton({ label: TEXT.pills, variant: "ghost", size: "sm" }); + pills.title = TEXT.pillsTip; + const paintPills = (shown: boolean): void => { + pills.setAttribute("aria-pressed", String(shown)); + sheets[0].host.classList.toggle("is-pills-off", !shown); + }; + pills.addEventListener("click", () => { + const shown = pills.getAttribute("aria-pressed") !== "true"; + writeStateRaw("m02-card-pills", String(shown)); + paintPills(shown); + }); + paintPills(readStateRaw("m02-card-pills") === "true"); + sheets[0].shell.tools.prepend(pills); const cards: Card[] = [info, ...sheets]; const defaults: Record = { structure: TEXT.edit, @@ -244,6 +261,7 @@ export function createItemCards(opts: ItemCardsOptions): ItemCardsHandle { c.saver.button.hidden = c.help.hidden = !can; c.edit.hidden = !opts.editable(); } + pills.hidden = !sheetEdit; }; const drop = (): void => { diff --git a/M02_MasterTemplete/M02_MasterTemplete_Structure.css b/M02_MasterTemplete/M02_MasterTemplete_Structure.css index b44d120e2..57eaacbce 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Structure.css +++ b/M02_MasterTemplete/M02_MasterTemplete_Structure.css @@ -247,3 +247,8 @@ .m02-struct__sheet-grid .ss-links__list { max-height: 8rem; } + +/* 면적산출 카드 [알약] 끔(기본) — 아래 알약 줄만 숨김 · 연결 · 칸 세모는 그대로 */ +.m02-struct__sheet.is-pills-off .ss-links { + display: none; +} diff --git a/resources/tester/test_m02_card_sheet_edit.py b/resources/tester/test_m02_card_sheet_edit.py index a4b3e62c6..6dbe46cf5 100644 --- a/resources/tester/test_m02_card_sheet_edit.py +++ b/resources/tester/test_m02_card_sheet_edit.py @@ -75,3 +75,17 @@ def test_저장_안_한_카드_고침_물음(): assert main.count("busy()") >= 3 # 떠나기 · 새로고침 막기 · 나무 고침(settle) assert main.index("await panel.saveCards();") < main.index("await panel.reload();") assert "keepColumn: (colId) => sheet?.selectColumn(colId)" in main + + +def test_면적산출_카드_알약_줄_켬끔_기본_숨김(): + cards = _text(M02 / "M02_MasterTemplete_ItemCards.ts") + assert 'paintPills(readStateRaw("m02-card-pills") === "true");' in cards # 없으면 숨김 + assert 'writeStateRaw("m02-card-pills", String(shown));' in cards + assert 'sheets[0].host.classList.toggle("is-pills-off", !shown);' in cards + assert "pills.hidden = !sheetEdit;" in cards # 고칠 수 있는 카드만(보기만은 알약 줄 없음) + css = _text(M02 / "M02_MasterTemplete_Structure.css") + assert re.search(r"\.m02-struct__sheet\.is-pills-off \.ss-links \{\s*display: none;", css) + state = _text(ROOT / "A00_Common" / "b_page_state.ts") + assert '"m02-card-pills": { bucket: "pref", scope: "global" },' in state + # 상세(전체 편집기)는 켬끔 없음 — 늘 보임 + assert "is-pills-off" not in _text(M02 / "M02_MasterTemplete_UI_Main.ts") From 8fde124df5ab88c22890733e5187742fd5a0e7b7 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sun, 4 Oct 2026 17:21:12 +0900 Subject: [PATCH 05/14] =?UTF-8?q?refactor(M02):=20=EC=9E=90=EC=9E=AC=20?= =?UTF-8?q?=EA=B3=A0=EB=A5=B4=EA=B8=B0=20=EC=B0=BD=20=EB=94=B0=EB=A1=9C=20?= =?UTF-8?q?=E2=80=94=20M02=5FMaterial=5FPick.ts(pickMaterial=20=C2=B7=20Ma?= =?UTF-8?q?terialItem)=20=C2=B7=20=EC=9D=BC=EC=9C=84=EB=8C=80=EA=B0=80=20?= =?UTF-8?q?=EC=9E=90=EC=9E=AC=EB=AA=A9=EB=A1=9D=EC=9D=B4=20=EB=8B=A4?= =?UTF-8?q?=EC=8B=9C=20=EC=94=80?= 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_01NPJGtfXo6GMbcpiboQC4uK --- .../M02_MasterTemplete_Materials.ts | 68 +--------------- .../M02_MasterTemplete_Matset.ts | 2 +- M02_MasterTemplete/M02_Material_Pick.ts | 77 +++++++++++++++++++ 3 files changed, 80 insertions(+), 67 deletions(-) create mode 100644 M02_MasterTemplete/M02_Material_Pick.ts diff --git a/M02_MasterTemplete/M02_MasterTemplete_Materials.ts b/M02_MasterTemplete/M02_MasterTemplete_Materials.ts index 809c92d72..93697e6fb 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Materials.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Materials.ts @@ -5,15 +5,7 @@ * (서버 `branchMaterialCombo`) · 원래 조합은 그대로. 빼는 자재가 칸에 이어져 있으면 확인 후 연결도 풂. * ========================================================================== */ -import { - createButton, - createInputField, - el, - showConfirmDialog, - showToast, -} from "@ui/ui_template_elements"; -import { openModal } from "@ui/ui_template_modal"; -import { searchMaterials } from "../M01_MasterData/M01_MasterData_Api_Fetch"; +import { createButton, el, showConfirmDialog, showToast } from "@ui/ui_template_elements"; import type { LinkItem } from "../A00_Common/spreadsheet/spreadsheet_links"; import type { CellLink } from "../A00_Common/spreadsheet/spreadsheet_types"; import { @@ -22,6 +14,7 @@ import { type MaterialCombo, type MaterialItem, } from "./M02_MasterTemplete_Api_Fetch"; +import { pickMaterial } from "./M02_Material_Pick"; import { askBranch, branchName, branchToast } from "./M02_MasterTemplete_Combo"; import { readColumnBase, reasonOf } from "./M02_MasterTemplete_Combo_Api"; @@ -51,12 +44,6 @@ const TEXT = { askAlone: "이 자재 조합은 모든 프로젝트가 같이 씀 — 고친 내용으로 새 자재 조합을 만들고 이 열만 새 조합을 가리킴 · 같은 내용이면 있는 조합을 씀", failed: "자재 조합을 못 고침 — {value}", - pickTitle: "자재 추가", - pickClose: "닫기", - search: "자재 이름 · 규격 · 코드", - searching: "찾는 중…", - noMatch: "맞는 자재 없음", - searchFailed: "자재 목록을 못 받음", }; const fill = (text: string, values: Record): string => @@ -122,57 +109,6 @@ export interface MaterialPanelOptions { onApplied?: () => void; } -/** 자재 고르기 창 — 이름 · 규격 · 코드 낱말로 찾아 하나 고름(좌측 자재 조합 상세도 씀) */ -export function pickMaterial(done: (item: MaterialItem) => void): void { - openModal({ - title: TEXT.pickTitle, - closeLabel: TEXT.pickClose, - dialogClass: "m02-modal m02-pick", - mount: (body, close) => { - const search = createInputField({ placeholder: TEXT.search, type: "search" }); - const list = el("div", { className: "m02-pick__list" }); - let seq = 0; - const paint = async (): Promise => { - const mine = ++seq; - list.replaceChildren(el("p", { className: "m02-struct__empty", text: TEXT.searching })); - try { - const hits = await searchMaterials(search.input.value.trim()); - if (mine !== seq) return; - list.replaceChildren( - ...(hits.length - ? hits.map((m) => { - const row = el("button", { - className: "m02-pick__item", - attrs: { type: "button", "data-material": m.코드 }, - children: [ - el("span", { className: "m02-pick__key", text: m.코드 }), - `${m.이름} ${m.규격} ${m.단위}`.replace(/\s+/g, " "), - ], - }); - row.addEventListener("click", () => { - done(m); - close(); - }); - return row; - }) - : [el("p", { className: "m02-struct__empty", text: TEXT.noMatch })]), - ); - } catch { - if (mine === seq) showToast(TEXT.searchFailed, "error"); - } - }; - let timer = 0; - search.input.addEventListener("input", () => { - window.clearTimeout(timer); - timer = window.setTimeout(() => void paint(), 250); - }); - body.append(search.root, list); - void paint(); - search.input.focus(); - }, - }); -} - /** 옆 판 — 이 열에 자재 조합이 없어도 판은 뜸(더하면 새로 만듦) */ export function buildMaterialPanel(opts: MaterialPanelOptions): MaterialPanel { let key = opts.state.key; diff --git a/M02_MasterTemplete/M02_MasterTemplete_Matset.ts b/M02_MasterTemplete/M02_MasterTemplete_Matset.ts index 931e58d67..03f6367a6 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Matset.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Matset.ts @@ -22,7 +22,7 @@ import { StaleError, type MaterialCombo, } from "./M02_MasterTemplete_Api_Fetch"; -import { pickMaterial } from "./M02_MasterTemplete_Materials"; +import { pickMaterial } from "./M02_Material_Pick"; import { tm } from "./M02_MasterTemplete_Matset_Text"; export interface MatsetSectionOptions { diff --git a/M02_MasterTemplete/M02_Material_Pick.ts b/M02_MasterTemplete/M02_Material_Pick.ts new file mode 100644 index 000000000..8b0e604b5 --- /dev/null +++ b/M02_MasterTemplete/M02_Material_Pick.ts @@ -0,0 +1,77 @@ +/* ============================================================================= + * M02_Material_Pick.ts + * M01 자재 하나 고르기 창 — 이름 · 규격 · 코드 낱말로 찾아 하나 고름(일위대가 자재목록 · 자재 추가가 씀). + * 모양(`m02-pick*`)은 `M02_MasterTemplete_Structure.css`. + * ========================================================================== */ + +import { createInputField, el, showToast } from "@ui/ui_template_elements"; +import { openModal } from "@ui/ui_template_modal"; +import { searchMaterials } from "../M01_MasterData/M01_MasterData_Api_Fetch"; + +/** 고른 자재 한 줄 — M01 자재(키 `MT`) */ +export interface MaterialItem { + 코드: string; + 이름: string | null; + 규격: string | null; + 단위: string | null; +} + +const TEXT = { + pickTitle: "자재 추가", + pickClose: "닫기", + search: "자재 이름 · 규격 · 코드", + searching: "찾는 중…", + noMatch: "맞는 자재 없음", + searchFailed: "자재 목록을 못 받음", +}; + +/** 자재 고르기 창 — 고르면 `done` 부르고 닫음 */ +export function pickMaterial(done: (item: MaterialItem) => void): void { + openModal({ + title: TEXT.pickTitle, + closeLabel: TEXT.pickClose, + dialogClass: "m02-modal m02-pick", + mount: (body, close) => { + const search = createInputField({ placeholder: TEXT.search, type: "search" }); + const list = el("div", { className: "m02-pick__list" }); + let seq = 0; + const paint = async (): Promise => { + const mine = ++seq; + list.replaceChildren(el("p", { className: "m02-struct__empty", text: TEXT.searching })); + try { + const hits = await searchMaterials(search.input.value.trim()); + if (mine !== seq) return; + list.replaceChildren( + ...(hits.length + ? hits.map((m) => { + const row = el("button", { + className: "m02-pick__item", + attrs: { type: "button", "data-material": m.코드 }, + children: [ + el("span", { className: "m02-pick__key", text: m.코드 }), + `${m.이름} ${m.규격} ${m.단위}`.replace(/\s+/g, " "), + ], + }); + row.addEventListener("click", () => { + done(m); + close(); + }); + return row; + }) + : [el("p", { className: "m02-struct__empty", text: TEXT.noMatch })]), + ); + } catch { + if (mine === seq) showToast(TEXT.searchFailed, "error"); + } + }; + let timer = 0; + search.input.addEventListener("input", () => { + window.clearTimeout(timer); + timer = window.setTimeout(() => void paint(), 250); + }); + body.append(search.root, list); + void paint(); + search.input.focus(); + }, + }); +} From ab84c763b79f65f093efcc684ab445cb5adb0b36 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sun, 4 Oct 2026 17:22:24 +0900 Subject: [PATCH 06/14] =?UTF-8?q?refactor(M02):=20=EC=A2=8C=EC=B8=A1=20?= =?UTF-8?q?=E3=80=8C=EC=9D=BC=EC=9C=84=EB=8C=80=EA=B0=80=20=EC=A1=B0?= =?UTF-8?q?=ED=95=A9=E3=80=8D=20=EC=BB=A8=ED=85=8C=EC=9D=B4=EB=84=88=20?= =?UTF-8?q?=C2=B7=20=EC=A1=B0=ED=95=A9=20=EB=82=98=EB=AC=B4=20=C2=B7=20?= =?UTF-8?q?=EB=91=98=EC=A7=B8=20=EC=B8=B5=20=EB=8B=A8=EC=B6=94=20=EB=B2=8C?= =?UTF-8?q?=20=EC=A7=80=EC=9B=80(39-2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Labor.ts · Labor_Tree.ts 지움 · 페이지 · 좌측 이음 · 해시 표시(mark) 지움 - 나무 부품 일위대가 전용 칸(clipKind · maxDepth · item) 지움 - 옛 나무 시험을 새 짜임 검사로 갈음 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01BVPp2o6Mh2FZJr72yW3ofZ --- .../M02_MasterTemplete_Labor.ts | 222 ------------------ .../M02_MasterTemplete_Labor_Tree.ts | 45 ---- .../M02_MasterTemplete_TreePanel.ts | 23 +- .../M02_MasterTemplete_UI_Page.ts | 36 +-- .../M02_MasterTemplete_UI_Side.ts | 24 +- .../M02_MasterTemplete_UI_Style.css | 8 - resources/tester/test_m02_labor_tree.py | 131 ++--------- resources/tester/test_m02_layer_screen.py | 13 +- 8 files changed, 39 insertions(+), 463 deletions(-) delete mode 100644 M02_MasterTemplete/M02_MasterTemplete_Labor.ts delete mode 100644 M02_MasterTemplete/M02_MasterTemplete_Labor_Tree.ts diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor.ts deleted file mode 100644 index 985d1a1f8..000000000 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor.ts +++ /dev/null @@ -1,222 +0,0 @@ -/* ============================================================================= - * M02_MasterTemplete_Labor.ts - * 좌측 「일위대가 조합」 컨테이너(구조물 조합 위 · PLAN 27-2b) — 「일위대가 라이브러리」 줄 아래 층 단추 - * (`TreePanel_Roots`) · 조합 나무(폴더 [구분, 상세구분] 깊이 2 · 구조물 라이브러리와 같은 `TreePanel` · - * 좌측 패널 안 · PLAN 29-2) · 줄 · 나무 항목을 누르면 메인 칸에 찾기 칸 + 조합 목록 · 상세(`Labor_Edit`). - * 나무 고침 = `POST /api/m02/layers/{층}/combos/tree` · 「조합 더하기」 = 그 폴더를 채운 빈 조합(저장해야 생김). - * ========================================================================== */ - -import { el, showConfirmDialog } from "@ui/ui_template_elements"; -import { ApiError } from "../M01_MasterData/M01_MasterData_UI_Logic_Api"; -import { StaleError } from "./M02_MasterTemplete_Api_Fetch"; -import { - editComboTree, - editComboTreeMany, - fetchComboList, - pasteCombos, - type ComboList, - type ComboTreeEdit, - type LaborScope, -} from "./M02_MasterTemplete_Labor_Api"; -import { createLaborEditor } from "./M02_MasterTemplete_Labor_Edit"; -import { noteOf, toView } from "./M02_MasterTemplete_Labor_Tree"; -import { tc } from "./M02_MasterTemplete_Labor_Text"; -import { createTreePanel } from "./M02_MasterTemplete_TreePanel"; - -/** 나무 부품이 아는 막힘으로 — 판 다름 = `StaleError("HTTP 409")` · 나머지는 서버 까닭 글 */ -function treeError(error: unknown): Error { - if (!(error instanceof ApiError)) - return error instanceof Error ? error : new Error(String(error)); - const d = error.detail as { errors?: string[]; stale?: unknown } | string | null; - const text = typeof d === "string" ? d : d?.errors?.join(" · ") || `HTTP ${String(error.status)}`; - return error.status === 409 ? new StaleError(text) : new Error(text); -} - -export interface LaborLibraryOptions { - scope: LaborScope; - canEdit: boolean; - /** 「〇〇로 복사」 — 이 사람이 쓸 수 있는 층(지금 보는 층 뺌 · 페이지가 구조물 나무와 같은 목록을 줌) */ - copyTo?: { layer: LaborScope["layer"]; owner: string | null; label: string }[]; - /** 나무 항목을 누름 — 메인 칸에 일위대가 화면을 먼저 띄움 */ - reveal: () => Promise; -} - -export interface LaborLibrary { - /** 메인 칸 — 찾기 칸 + 조합 목록 · 상세 */ - root: HTMLElement; - /** 좌측 패널 나무 */ - tree: HTMLElement; - /** 나무 맨 위 더하기 「+」(층 단추 옆) */ - add: HTMLElement; - /** 켜진 층 단추를 다시 누름 — 조합 목록으로(저장 안 한 고침은 물음 · PLAN 30-4) */ - back: () => Promise; - refresh: () => Promise; - /** 제목 줄 오른쪽 끝 도구 — 「새 조합」(읽기 층 = null · PLAN 30-5) */ - tools: HTMLElement | null; - /** 메인 칸 「저장 안 한 고침」 손잡이 */ - bind: (setDirty: (dirty: boolean) => void) => void; -} - -export function buildLaborLibrary(opt: LaborLibraryOptions): LaborLibrary { - const { scope } = opt; - let setDirty: (dirty: boolean) => void = () => {}; - let keys = new Set(); // 지금 목록의 조합 키 — 열린 조합이 지워졌나 - const took = (list: ComboList): ComboList => { - keys = new Set(list.줄.map((c) => c.키)); - editor.setList(list.줄); - return list; - }; - const editor = createLaborEditor({ - scope, - canEdit: opt.canEdit, - onDirty: (dirty) => setDirty(dirty), - onSaved: async (key) => { - await tree.refresh(); - tree.select(key); - }, - }); - const tree = createTreePanel({ - layer: scope.layer, - owner: scope.owner, - canEdit: opt.canEdit, - maxDepth: 2, - item: { add: tc("AddCombo"), name: tc("ComboName"), onAdd: (path) => editor.startNew(path) }, - api: { - fetch: async () => { - return toView(took(await fetchComboList(scope))); - }, - edit: async (_layer, edit, 판) => { - const body = { ...edit, 판 } as ComboTreeEdit; - const got = await editComboTree(body, scope).catch((error: unknown) => { - throw treeError(error); - }); - return toView(took(got), noteOf(edit, got.지운조합)); - }, - // 여러 개 옮기기 · 지우기(PLAN 30-3) — 고른 것마다 나무 고침 하나 · 서버가 한 번에 저장 - many: async (_layer, body, 판) => { - const ops = body.대상.map((ref) => ({ - op: body.op, - ...(ref.키 ? { 키: ref.키 } : { 경로: ref.경로 }), - ...(body.op === "move" ? { 새경로: body.새경로 ?? [] } : {}), - })); - const got = await editComboTreeMany(ops, 판, scope).catch((error: unknown) => { - throw treeError(error); - }); - return toView(took(got), { op: body.op, 지운항목: got.지운조합 }); - }, - // 붙여넣기 · 「〇〇로 복사」 — 대상 층(`layer` · `owner`)에 새 키 · 이 나무가 아니면 이 나무는 그대로 - paste: async (layer, owner, clip, dest) => { - const to = { layer, owner } as LaborScope; - const pick = { - 키: clip.refs.flatMap((ref) => (ref.키 ? [ref.키] : [])), - 폴더: clip.refs.flatMap((ref) => (ref.키 ? [] : [ref.경로])), - }; - const got = await fetchComboList(to) - .then((list) => pasteCombos(clip.from as LaborScope, pick, dest, list.판, to)) - .catch((error: unknown) => { - throw treeError(error); - }); - const here = layer === scope.layer && (owner ?? null) === (scope.owner ?? null); - return toView(took(here ? got : await fetchComboList(scope))); - }, - }, - clipKind: "일위대가", - copyTo: opt.copyTo, - onPick: async (item) => { - await opt.reveal(); - tree.select(item.key); - void editor.open(item.key); - }, - // 나무를 고치기 전 — 저장 안 한 조합 고침은 버릴지 물음(버리면 목록으로) - settle: async () => { - if (!editor.isDirty()) return "clean"; - if (!(await showConfirmDialog(tc("BackAsk")))) return false; - editor.showList(); - return "changed"; - }, - // 나무가 바뀜 — 열린 조합은 새 판으로 다시 엶(이름 · 폴더가 바뀌었을 수 있음) · 지워졌으면 목록 - onChanged: async () => { - const open = editor.current(); - if (!open) return; - if (keys.has(open)) await editor.open(open); - else editor.showList(); - }, - }); - // 찾기 칸 하나(PLAN 29-6) — 나무와 조합 목록을 같은 글 · 같은 규칙으로 거름 - tree.search - .querySelector("input") - ?.addEventListener("input", (event) => - editor.setQuery((event.target as HTMLInputElement).value), - ); - return { - root: el("div", { className: "m02-labor", children: [tree.search, editor.root] }), - tree: tree.root, - add: tree.add, - back: async () => { - await opt.reveal(); // 일위대가 화면이 아니면 먼저 띄움 - if (editor.current() === null) return; - if (editor.isDirty() && !(await showConfirmDialog(tc("BackAsk")))) return; - editor.showList(); - }, - refresh: async () => { - const stop = editor.loading(); // 조합 목록 칸에도 「불러오는 중」(나무 칸은 나무 부품이 · PLAN 30-6) - try { - await tree.refresh(); - } finally { - stop(); - } - }, - tools: editor.newButton, - bind: (fn) => (setDirty = fn), - }; -} - -export interface LaborSectionOptions { - /** 메인 칸에 화면을 띄움 — 고침 표시 손잡이를 돌려받음 */ - show: (label: string, node: HTMLElement, tools?: HTMLElement | null) => (dirty: boolean) => void; - /** 저장 안 한 고침을 버릴지 확인 — 취소면 false */ - confirmLeave: () => Promise; - /** 화면을 열었음 — 좌측 다른 고름 표시를 지움 */ - onOpen: () => void; - /** 라이브러리 — `reveal` = 메인 칸에 일위대가 화면을 띄움(나무 항목을 누를 때) */ - make: (reveal: () => Promise) => LaborLibrary; - /** 층 단추(PLAN 29-2) — `add` = 나무 맨 위 더하기 「+」를 단추 옆에 */ - bar?: (add: HTMLElement, back: () => void) => HTMLElement; - /** 나무를 감쌈 — 남의 회사 「개인」이면 사람 나무 안에(PLAN 29-6) */ - wrap?: (tree: HTMLElement) => HTMLElement; -} - -export interface LaborSection { - title: string; - body: HTMLElement; - open: () => Promise; - /** 다른 화면을 열었음 — 고름 표시만 지움 */ - clear: () => void; -} - -/** 컨테이너 몸 = 층 단추 + 나무(같은 이름 줄 단추 없음 · 컨테이너 제목을 펼치면 엶 · PLAN 29-6) */ -export function buildLaborSection(opt: LaborSectionOptions): LaborSection { - let opened = false; - const mark = (on: boolean): void => { - opened = on; - }; - - async function open(): Promise { - if (opened || !(await opt.confirmLeave())) return; - library.bind(opt.show(tc("Library"), library.root, library.tools)); - opt.onOpen(); - mark(true); - await library.refresh(); - } - // 나무는 좌측 패널에 늘 있음(PLAN 29-2) — 처음부터 만들고 읽음 - const library = opt.make(open); - void library.refresh(); - const body = el("div", { - className: "m02-side__items ui-sidebar-list", - children: [ - ...(opt.bar ? [opt.bar(library.add, () => void library.back())] : []), - opt.wrap ? opt.wrap(library.tree) : library.tree, - ], - }); - return { title: tc("Title"), body, open, clear: () => mark(false) }; -} diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_Tree.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_Tree.ts deleted file mode 100644 index d18017cae..000000000 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_Tree.ts +++ /dev/null @@ -1,45 +0,0 @@ -/* ============================================================================= - * M02_MasterTemplete_Labor_Tree.ts - * 일위대가 라이브러리 나무 자료(화면 없음) — 조합 목록 → 나무 부품(`TreePanel`) 모양 · 나무 고침 뒤 알림. - * 조합 폴더 = [구분] · [구분, 상세구분](깊이 2 · PLAN 27-2a) · 항목 = 조합(열 = 키). - * ========================================================================== */ - -import type { TreeEdit, TreeNote, TreeView } from "./M02_MasterTemplete_Api_Fetch"; -import type { ComboList, ComboSummary } from "./M02_MasterTemplete_Labor_Api"; - -/** 조합의 폴더 — [구분] · [구분, 상세구분] · 구분 없으면 맨 위 */ -export const pathOf = (c: ComboSummary): string[] => - c.구분 ? (c.상세구분 ? [c.구분, c.상세구분] : [c.구분]) : []; - -/** 조합 목록 → 나무 부품 모양(항목 = 조합 · 열 = 키) */ -export const toView = (list: ComboList, note?: TreeNote): TreeView => ({ - 판: list.판, - 폴더: list.폴더, - items: list.줄.map((c) => ({ - key: c.키, - 열: c.키, - 경로: [...pathOf(c), c.이름], - 이름: c.이름, - 단위: c.단위, - })), - ...(note ? { 알림: note } : {}), -}); - -/** 나무 고침 뒤 알림 — 새 · 바뀐 폴더를 펼치고 고친 조합은 고른 채 */ -export function noteOf(edit: TreeEdit, gone: string[]): TreeNote { - const at = edit.경로 ?? []; - const last = at[at.length - 1] ?? ""; - if (edit.키 && edit.op !== "delete") { - const where = edit.op === "move" ? (edit.새경로 ?? []) : at; - return { op: edit.op, 열: edit.키, 경로: [...where, edit.이름 ?? ""], 지운항목: gone }; - } - const path = - edit.op === "add-folder" - ? [...at, edit.이름 ?? ""] - : edit.op === "rename" - ? [...at.slice(0, -1), edit.이름 ?? ""] - : edit.op === "move" - ? [...(edit.새경로 ?? []), last] - : undefined; - return { op: edit.op, ...(path ? { 경로: path } : {}), 지운항목: gone }; -} diff --git a/M02_MasterTemplete/M02_MasterTemplete_TreePanel.ts b/M02_MasterTemplete/M02_MasterTemplete_TreePanel.ts index 426b52b8d..014e9c9b1 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_TreePanel.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_TreePanel.ts @@ -81,18 +81,12 @@ export interface TreePanelOptions { dest: string[], ) => Promise; }; - /** 복사해 둔 것의 나무 종류 — 같은 종류 나무끼리만 붙임(기본 「구조물」) */ - clipKind?: string; /** 항목 추가 창의 일위대가 고르기(PLAN 31-2 · 세 층 조합 · 찾기 AND) — 없으면 그 줄 없음 */ pickCombo?: () => Promise<{ key: string; label: string } | null>; /** 「〇〇로 복사」 — 고른 것을 그 층 맨 위에 바로(층을 안 바꿈 · `label` 의 {n} = 개수 · `api.paste` 로) */ copyTo?: { layer: Layer; owner: string | null; label: string }[]; /** 이 층을 고칠 수 있나 — 아니면 메뉴 · 키 없음 */ canEdit: boolean; - /** 폴더 깊이 한도 — 닿은 폴더엔 「폴더 더하기」 없음(일위대가 라이브러리 = 2) */ - maxDepth?: number; - /** 항목 부르는 말 · 「항목 더하기」 대신 부르는 쪽이 새 항목 화면을 엶(일위대가 조합) */ - item?: { add: string; name: string; onAdd?: (path: string[]) => void }; /** 항목을 누름 — 표에서 그 열을 고름 */ onPick: (item: TreeItem) => void; /** 고치기 전에 저장 안 한 표 고침을 처리 — 취소면 false · 저장했으면 "changed" */ @@ -123,7 +117,7 @@ const hint = (text: string): HTMLElement => el("p", { className: "m02-tree__hint export function createTreePanel(opt: TreePanelOptions): TreePanelHandle { const owner = opt.owner ?? null; const api = opt.api ?? { fetch: fetchTree, edit: editTree, many: editTreeMany, paste: pasteTree }; - const clipKind = opt.clipKind ?? "구조물"; + const clipKind = "구조물"; // 복사해 둔 것의 나무 종류 — 같은 종류 나무끼리만 붙임 let view: TreeView | null = null; let text = ""; let activeCol: string | null = null; @@ -213,13 +207,8 @@ export function createTreePanel(opt: TreePanelOptions): TreePanelHandle { return [ ...(target.kind === "folder" ? [ - ...(opt.maxDepth !== undefined && target.path.length >= opt.maxDepth - ? [] - : [{ label: L("M02_TreeAddFolder"), run: () => void addFolder(target.path) }]), - { - label: opt.item?.add ?? L("M02_TreeAddItem"), - run: () => void addItem(target.path), - }, + { label: L("M02_TreeAddFolder"), run: () => void addFolder(target.path) }, + { label: L("M02_TreeAddItem"), run: () => void addItem(target.path) }, ] : []), ...(isTop @@ -466,7 +455,6 @@ export function createTreePanel(opt: TreePanelOptions): TreePanelHandle { async function addItem(parent: string[]): Promise { if (!(await ready())) return; - if (opt.item?.onAdd) return opt.item.onAdd(parent); // 종류(맨 위 폴더 · 고른 폴더면 그 안) + 이름 + 일위대가 · 단위 안 받음(PLAN 31-2) const kinds = view ? buildFolders(view).folders.map((f) => f.path[0]) : []; const got = await askNewItem({ @@ -490,10 +478,7 @@ export function createTreePanel(opt: TreePanelOptions): TreePanelHandle { const got = await askText(L("M02_TreeRename"), [ { key: "name", - label: - target.kind === "item" - ? (opt.item?.name ?? L("M02_TreeItemName")) - : L("M02_TreeFolderName"), + label: target.kind === "item" ? L("M02_TreeItemName") : L("M02_TreeFolderName"), value: nameOf(target), }, ]); diff --git a/M02_MasterTemplete/M02_MasterTemplete_UI_Page.ts b/M02_MasterTemplete/M02_MasterTemplete_UI_Page.ts index 2be94a20d..a19bc482a 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_UI_Page.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_UI_Page.ts @@ -27,7 +27,6 @@ import { type OpenTarget, type Root, } from "./M02_MasterTemplete_Layer"; -import { buildLaborLibrary, buildLaborSection } from "./M02_MasterTemplete_Labor"; import { headRenameEdit, pickCombo, @@ -86,14 +85,6 @@ export async function renderM02MasterTemplate(root: HTMLElement): Promise main.settle(), onChanged: (colId) => main.openTable(layer, colId, true), }); - // 일위대가 화면이 떠 있나 — 층을 바꿔도 일위대가로 다시 엶 · - // 해시 `open` 도 맞춤(새로 고침 · 카드 진입과 같게) · 옵션 · 도면은 옛 규칙(구조물 조합으로) - let view: OpenTarget = entry.open; - const mark = (open: OpenTarget): void => { - view = open; - const query = new URLSearchParams(entryQuery({ ...entry, open })); - history.replaceState(history.state, "", `#/${ROUTES.M02_MASTER_TEMPLATE}?${query}`); - }; const go = async (next: Root, nextOwner: string | null, open: OpenTarget): Promise => { // 같은 주소 안 이동은 머리 가드가 안 물음 — 저장 안 한 고침은 여기서 if (!(await main.confirmLeave())) return; @@ -102,9 +93,9 @@ export async function renderM02MasterTemplate(root: HTMLElement): Promise void): HTMLElement => createLayerBar({ user, @@ -127,11 +118,7 @@ export async function renderM02MasterTemplate(root: HTMLElement): Promise void Promise.all([side.refresh(), tree.refresh()]), - () => { - side.paint(); - // 일위대가 화면에서 양식으로 옮김(일위대가 · 옵션 화면은 current 없음) - if (main.current() && view === "일위대가") mark("구조물조합"); - }, + () => side.paint(), { top: readOnlyBar?.root, search: tree.search, @@ -168,19 +155,6 @@ export async function renderM02MasterTemplate(root: HTMLElement): Promise void main.openTable(layer)), peopleTree("구조물조합", tree.root), ], - labor: ({ onOpen }) => - buildLaborSection({ - show: main.showView, - confirmLeave: main.confirmLeave, - onOpen: () => { - onOpen(); - mark("일위대가"); - }, - bar: (add, back) => layerBar("일위대가", add, back), - wrap: (panel) => peopleTree("일위대가", panel), - make: (reveal) => - buildLaborLibrary({ scope: { layer, owner }, canEdit: isAdmin, reveal, copyTo }), - }), }); const layout = el("div", { className: "ui-workflow-layout m02-master" }); @@ -200,8 +174,8 @@ export async function renderM02MasterTemplate(root: HTMLElement): Promise Promise; /** 메인 칸에 양식 밖 화면(자재 조합 상세)을 띄움 */ showView: (label: string, node: HTMLElement) => (dirty: boolean) => void; - /** 「일위대가 조합」 컨테이너(구조물 조합 위 · PLAN 27-2b) — 페이지가 만듦(뿌리 셋이 페이지 것) */ - labor?: (hooks: { onOpen: () => void }) => LaborSection; /** 「구조물 조합」 줄 아래 — 층 단추 · 고른 층의 구조물 라이브러리 나무(PLAN 29-2 · 페이지가 만듦) */ tree?: HTMLElement[]; } @@ -85,7 +82,7 @@ export interface SideHandle { refresh: () => Promise; /** 고름 표시를 지금 열린 것 한 줄로 — 편집 칸이 무엇을 열든(카드 [도면 수정] 등) 부름 */ paint: () => void; - /** 진입 해시 `open`(PLAN 27-1d) — 그 컨테이너를 펼침(옵션은 열기까지) · 시스템 층에만 있음 */ + /** 진입 해시 `open`(PLAN 27-1d) — 그 컨테이너를 펼침(옵션은 열기까지) · 옵션 · 도면은 시스템 층에만 */ reveal: (target: "옵션" | "도면" | "일위대가") => void; } @@ -136,7 +133,7 @@ export function buildSide(opt: SideOptions): SideHandle { confirmLeave: opt.confirmLeave, show: opt.showView, close: () => opt.onOpen(null), - onOpen: () => (options.clear(), labor?.clear(), paint()), + onOpen: () => (options.clear(), paint()), }); const matsetSection = section(matset.title, matset.body, ...matset.actions); // 펼칠 때마다 새로 받음 — 재료산출 옆 판이 갈라 낸 조합도 바로 보이게 @@ -148,9 +145,8 @@ export function buildSide(opt: SideOptions): SideHandle { isAdmin: canEdit(layer, opt.isAdmin), confirmLeave: opt.confirmLeave, show: opt.showView, - onOpen: () => (matset.clear(), labor?.clear(), paint()), + onOpen: () => (matset.clear(), paint()), }); - const labor = opt.labor?.({ onOpen: () => (matset.clear(), options.clear(), paint()) }); /** 컨테이너 제목을 펼치면 그 화면을 엶 — 같은 이름 줄 단추를 갈음(PLAN 29-6) */ const openOnExpand = (sec: HTMLElement, run: () => void): HTMLElement => { sec.querySelector(".ui-collapsible__title")?.addEventListener("click", () => { @@ -181,15 +177,9 @@ export function buildSide(opt: SideOptions): SideHandle { : null; const listHost = el("div", { className: "m02-side__lists ui-sidebar-panel", - children: [ - ...(labor ? [openOnExpand(section(labor.title, labor.body), () => void labor.open())] : []), - summary, - ...(extras ? [extras.옵션, extras.도면] : []), - ], + children: [summary, ...(extras ? [extras.옵션, extras.도면] : [])], }); attachCollapsible(listHost); - // 들어오면 「일위대가 조합」 · 「구조물 조합」 둘 다 펼친 채(PLAN 31-5 · 구조물 조합은 위에서 펼침) - labor?.body.closest("section")?.classList.remove("is-collapsed"); const root = el("div", { className: "m02-side ui-sidebar-panel", children: [listHost] }); @@ -263,7 +253,6 @@ export function buildSide(opt: SideOptions): SideHandle { paintButtons(); if (opt.getOpen()) matset.clear(); if (opt.getOpen()) options.clear(); - if (opt.getOpen()) labor?.clear(); } // 여는 것은 편집 칸(`onOpen`) — 편집 칸이 열 때마다 `paint` 를 부름(페이지가 이음) @@ -388,10 +377,7 @@ export function buildSide(opt: SideOptions): SideHandle { } const reveal = (target: "옵션" | "도면" | "일위대가"): void => { - if (target === "일위대가") { - labor?.body.closest("section")?.classList.remove("is-collapsed"); - return void labor?.open(); - } + if (target === "일위대가") return; // 구조물 조합은 처음부터 펼침 extras?.[target].classList.remove("is-collapsed"); if (target === "옵션") options.body.querySelector("button")?.click(); }; diff --git a/M02_MasterTemplete/M02_MasterTemplete_UI_Style.css b/M02_MasterTemplete/M02_MasterTemplete_UI_Style.css index 0e323ca8c..c4c51d1ca 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_UI_Style.css +++ b/M02_MasterTemplete/M02_MasterTemplete_UI_Style.css @@ -222,14 +222,6 @@ font-size: 11px; } -/* 나무는 좌측 패널(PLAN 29-2) — 일위대가 화면 = 찾기 칸 위 · 조합 목록 · 상세 아래 */ -.m02-labor { - display: flex; - flex-direction: column; - gap: var(--spacing-8); - min-width: 0; -} - .m02-main__head + .m02-tree__search { margin-bottom: var(--spacing-8); } diff --git a/resources/tester/test_m02_labor_tree.py b/resources/tester/test_m02_labor_tree.py index 9268309c8..755dac40b 100644 --- a/resources/tester/test_m02_labor_tree.py +++ b/resources/tester/test_m02_labor_tree.py @@ -1,124 +1,35 @@ -"""일위대가 라이브러리 나무 자료(PLAN 27-2b · `Labor_Tree.ts`) — Node 로 TS 를 돌려 판정. +"""좌측 패널 일위대가(PLAN 39-2) — 맨 위 「일위대가 조합」 컨테이너 · 조합 나무 · 둘째 층 단추 벌 지움 · +일위대가는 구조물 조합 하위(구조물 도면 → 일위대가 → 면적산출). 글 검사(화면은 ORCA). -못박는 것 - - 조합 → 나무 항목: 경로 = [구분, 상세구분, 이름](빈 칸 건너뜀) · 열 = 키. - - 나무 고침 알림: 새 · 바뀐 · 옮긴 폴더 경로 · 조합 고침은 그 조합(열)을 고른 채 · - 지우기는 고름 없음. +옛 일위대가 라이브러리 나무 시험(PLAN 27-2b · 30-3 · 30-5)은 나무를 지우며 이 검사로 갈음. """ -import json -import shutil -import subprocess from pathlib import Path -import pytest - ROOT = Path(__file__).resolve().parents[2] -SCRIPT = r""" -const fs = require("fs"); -const ts = require(process.argv[1] + "/config/node_modules/typescript"); -const file = "/M02_MasterTemplete/M02_MasterTemplete_Labor_Tree.ts"; -const src = fs.readFileSync(process.argv[1] + file, "utf8"); -const js = ts.transpileModule(src, { - compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 }, -}).outputText; -const box = { exports: {} }; -new Function("exports", "module", "require", js)(box.exports, box, require); -const m = box.exports; -const row = (키, 이름, 구분, 상세구분) => ({ 키, 이름, 구분, 상세구분, 단위: "m" }); -const list = { - 판: "v1", - 폴더: [["석축"], ["석축", "찰쌓기"], ["빈"]], - 줄: [row("UP1-000001", "가", "석축", "찰쌓기"), row("UP1-000002", "나", "석축", null), - row("UP1-000003", "다", null, "무시")], -}; -const view = m.toView(list); -const notes = [ - { op: "add-folder", 경로: ["석축"], 이름: "메쌓기" }, - { op: "rename", 경로: ["석축", "찰쌓기"], 이름: "찰" }, - { op: "move", 경로: ["석축", "찰쌓기"], 새경로: [] }, - { op: "move", 경로: ["석축"], 키: "UP1-000002", 새경로: ["빈"] }, - { op: "rename", 경로: ["석축"], 키: "UP1-000002", 이름: "나나" }, - { op: "delete", 경로: ["석축"], 키: "UP1-000002" }, - { op: "delete", 경로: ["빈"] }, -].map((e) => m.noteOf(e, e.op === "delete" ? ["UP1-000002"] : [])); -console.log(JSON.stringify({ view, notes })); -""" +M02 = ROOT / "M02_MasterTemplete" -@pytest.fixture(scope="module") -def got(): - if shutil.which("node") is None: - pytest.skip("node 가 없음") - proc = subprocess.run( - ["node", "-e", SCRIPT, str(ROOT)], capture_output=True, text=True, encoding="utf-8" - ) - assert proc.returncode == 0, proc.stderr - return json.loads(proc.stdout) +def _text(name: str) -> str: + return (M02 / name).read_text("utf-8") -def test_조합이_나무_항목으로(got): - items = got["view"]["items"] - assert [i["경로"] for i in items] == [["석축", "찰쌓기", "가"], ["석축", "나"], ["다"]] - assert [i["열"] for i in items] == ["UP1-000001", "UP1-000002", "UP1-000003"] - assert got["view"]["폴더"] == [["석축"], ["석축", "찰쌓기"], ["빈"]] - assert "알림" not in got["view"] +def test_일위대가_나무_파일과_이음이_없음(): + assert not (M02 / "M02_MasterTemplete_Labor.ts").exists() + assert not (M02 / "M02_MasterTemplete_Labor_Tree.ts").exists() + page = _text("M02_MasterTemplete_UI_Page.ts") + assert "buildLaborSection" not in page and 'layerBar("일위대가"' not in page + side = _text("M02_MasterTemplete_UI_Side.ts") + assert "LaborSection" not in side and "labor?." not in side -def test_나무_고침_알림(got): - add, rename, move, move_item, rename_item, delete_item, delete_folder = got["notes"] - assert add["경로"] == ["석축", "메쌓기"] and "열" not in add - assert rename["경로"] == ["석축", "찰"] - assert move["경로"] == ["찰쌓기"] - assert move_item["열"] == "UP1-000002" and move_item["경로"][:-1] == ["빈"] - assert rename_item["경로"] == ["석축", "나나"] - assert "열" not in delete_item and delete_item["지운항목"] == ["UP1-000002"] - assert "경로" not in delete_folder +def test_나무_부품에_일위대가_전용_칸_없음(): + panel = _text("M02_MasterTemplete_TreePanel.ts") + for gone in ("maxDepth", "opt.item", "opt.clipKind", "clipKind?:"): + assert gone not in panel, gone -def test_뿌리를_바꿔도_일위대가_화면_유지_해시도_맞춤(): - """브레인 검증 흠(27-2b) · 29-2 — 층 단추 두 벌(일위대가 라이브러리 · 구조물 조합 줄 아래) · - 일위대가 단추로 층을 바꾸면 일위대가 화면 그대로 · 좌측 「일위대가 라이브러리」를 열면 해시 - `open=일위대가`(다시 그리지 않고 주소만).""" - page = (ROOT / "M02_MasterTemplete/M02_MasterTemplete_UI_Page.ts").read_text("utf-8") - assert "go: (next, nextOwner) => void go(next, nextOwner, open)," in page # 두 벌 같은 길 - assert ( - 'bar: (add, back) => layerBar("일위대가", add, back),' in page - and 'layerBar("구조물조합", tree.add, () => void main.openTable(layer)),' in page - ) - labor = page.split("buildLaborSection({")[1].split("make:")[0] - assert 'mark("일위대가")' in labor # 여는 순간 해시 open - assert "history.replaceState" in page # hashchange 없이(화면 다시 안 그림) - assert 'if (main.current() && view === "일위대가") mark("구조물조합")' in page - - -def test_일위대가_나무에_여러_개_복사_붙여넣기_연결(): - """PLAN 30-3 — 공용 나무 부품(30-2) 콜백을 일위대가 서버 길에 이음 · - 붙임 종류는 따로(구조물과 안 섞임).""" - labor = (ROOT / "M02_MasterTemplete/M02_MasterTemplete_Labor.ts").read_text("utf-8") - assert 'clipKind: "일위대가"' in labor and "copyTo: opt.copyTo" in labor - assert "editComboTreeMany(ops, 판, scope)" in labor # 여러 개 옮기기 · 지우기 - assert "pasteCombos(clip.from as LaborScope, pick, dest, list.판, to)" in labor # 대상 층 - page = (ROOT / "M02_MasterTemplete/M02_MasterTemplete_UI_Page.ts").read_text("utf-8") - assert "canEdit: isAdmin, reveal, copyTo })" in page # 구조물 나무와 같은 「〇〇로 복사」 목록 - - -def test_새_조합_단추는_제목_줄_오른쪽_목록은_공용_표(): - """PLAN 30-5 — 「새 조합」 = 제목 줄 오른쪽 끝(읽기 층 없음) · 목록 = 공용 표(줄 선). - 머리 줄도 공용 표 모양.""" - edit = (ROOT / "M02_MasterTemplete/M02_MasterTemplete_Labor_Edit.ts").read_text("utf-8") - assert ( - "newButton: edit" in edit and 'className: "ui-general-block__table m02-labor__list"' in edit - ) - assert ( - 'tc("New"), variant: "filled", onClick: () => startNew([]) })]' not in edit - ) # 목록 안 옛 자리 - labor = (ROOT / "M02_MasterTemplete/M02_MasterTemplete_Labor.ts").read_text("utf-8") - assert 'library.bind(opt.show(tc("Library"), library.root, library.tools))' in labor - main = (ROOT / "M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts").read_text("utf-8") - assert ( - 'slot.replaceChildren(el("span", { className: "m02-main__tools", children: [tools] }))' - in main - ) - css = (ROOT / "M02_MasterTemplete/M02_MasterTemplete_UI_Style.css").read_text("utf-8") - assert ".m02-main__tools {\n display: inline-flex;\n margin-left: auto;" in css +def test_층_단추는_구조물_조합_한_벌(): + page = _text("M02_MasterTemplete_UI_Page.ts") + assert page.count('layerBar("') == 1 # 구조물 조합 줄 아래 하나 + assert 'layerBar("구조물조합", tree.add, () => void main.openTable(layer)),' in page diff --git a/resources/tester/test_m02_layer_screen.py b/resources/tester/test_m02_layer_screen.py index c7bb3f17f..299ec5e4b 100644 --- a/resources/tester/test_m02_layer_screen.py +++ b/resources/tester/test_m02_layer_screen.py @@ -136,7 +136,7 @@ def test_층_단추_셋_남의_회사_사람_나무(): assert 'const people = entry.root === "personal" && seesOthers(company, user);' in page assert '(await fetchOwners("personal", company).catch(() => []))[0]?.owner' in page assert 'peopleTree("구조물조합", tree.root)' in page - assert 'wrap: (panel) => peopleTree("일위대가", panel)' in page + assert 'peopleTree("일위대가"' not in page # 일위대가 사람 나무 벌은 39-2 에서 지움 panel = _text(M02 / "M02_MasterTemplete_TreePanel.ts") assert ( "api.fetch(opt.layer, owner)" in panel and "api.edit(opt.layer, body_, 판, owner)" in panel @@ -219,17 +219,13 @@ def test_읽기만_표는_단추_고치기_모두_막음(): def test_켜진_층_단추는_목록으로_복사_끌기(): - """30-4 — 켜진 층 단추를 다시 누르면 그 층 목록(구조물 = 집계표 · 일위대가 = 조합 목록 · - 저장 안 한 고침은 물음) · 30-2 「〇〇로 복사」 = 쓸 수 있는 자기 층(지금 층 뺌) · - 끌어다 옮기기는 쓸 수 있는 층만.""" + """30-4 — 켜진 층 단추를 다시 누르면 그 층 목록(구조물 = 집계표 · 일위대가 단추 벌은 39-2 에서 지움) · + 30-2 「〇〇로 복사」 = 쓸 수 있는 자기 층(지금 층 뺌) · 끌어다 옮기기는 쓸 수 있는 층만.""" roots = _text(M02 / "M02_MasterTemplete_TreePanel_Roots.ts") assert "if (!current) opt.go(root, owner);\n else opt.reselect?.();" in roots page = _text(M02 / "M02_MasterTemplete_UI_Page.ts") assert 'layerBar("구조물조합", tree.add, () => void main.openTable(layer))' in page - assert 'bar: (add, back) => layerBar("일위대가", add, back)' in page assert "if (!accessFor(to, null, user).canWrite || (to === layer && !owner)) return [];" in page - labor = _text(M02 / "M02_MasterTemplete_Labor.ts") - assert 'if (editor.isDirty() && !(await showConfirmDialog(tc("BackAsk")))) return;' in labor panel = _text(M02 / "M02_MasterTemplete_TreePanel.ts") assert "if (opt.canEdit) drag.arm(button, one.id);" in panel drag = _text(M02 / "M02_MasterTemplete_TreePanel_Drag.ts") @@ -259,10 +255,9 @@ def test_구조물_조합_제목_줄_새_항목(): def test_들어오면_두_컨테이너_펼침_끌기_뒤_강조_지움(): - """31-5 — 좌측 「일위대가 조합」 · 「구조물 조합」 둘 다 펼친 채 · 끌어 놓은 뒤 강조(나무 칸 + """31-5 — 좌측 「구조물 조합」 펼친 채(일위대가는 그 하위 · 39-2) · 끌어 놓은 뒤 강조(나무 칸 자신 = 맨 위 자리 포함)를 모두 지움 · 칸 밖으로 나가면 지움(칸 안 자식 위로는 그대로).""" side = _text(M02 / "M02_MasterTemplete_UI_Side.ts") - assert 'labor?.body.closest("section")?.classList.remove("is-collapsed");' in side assert 'summary.classList.remove("is-collapsed");' in side drag = _text(M02 / "M02_MasterTemplete_TreePanel_Drag.ts") mark = drag.split("const dropMark = ")[1].split("};")[0] From 8739148bba03d03b7bbc8287f5fc9fc28fa67cab Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sun, 4 Oct 2026 17:23:01 +0900 Subject: [PATCH 07/14] =?UTF-8?q?feat(spreadsheet):=20=EC=86=90=EC=9E=A1?= =?UTF-8?q?=EC=9D=B4=20setLink=20=E2=80=94=20=EC=95=8C=EC=95=BD=20?= =?UTF-8?q?=ED=82=A4=EB=A5=BC=20=EC=B9=B8=EC=97=90=20=EC=9E=87=EA=B8=B0=20?= =?UTF-8?q?=C2=B7=20=ED=92=80=EA=B8=B0=20=C2=B7=20=EC=95=8C=EC=95=BD=20?= =?UTF-8?q?=EB=88=84=EB=A6=84=EA=B3=BC=20=EA=B0=99=EC=9D=80=20=EA=B7=9C?= =?UTF-8?q?=EC=B9=99=20=C2=B7=20=EB=90=98=EB=8F=8C=EB=A6=AC=EA=B8=B0=20?= =?UTF-8?q?=EB=90=A8=20(PLAN=2039-4=20D4)?= 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_01W4uKaV4ZBhsJN4GHG7yRj4 --- A00_Common/spreadsheet/spreadsheet.ts | 13 +++ .../spreadsheet/spreadsheet_link_set.ts | 55 +++++++++++ .../spreadsheet/spreadsheet_link_set_entry.ts | 92 +++++++++++++++++++ resources/tester/test_spreadsheet_set_link.py | 68 ++++++++++++++ 4 files changed, 228 insertions(+) create mode 100644 A00_Common/spreadsheet/spreadsheet_link_set.ts create mode 100644 resources/tester/spreadsheet/spreadsheet_link_set_entry.ts create mode 100644 resources/tester/test_spreadsheet_set_link.py diff --git a/A00_Common/spreadsheet/spreadsheet.ts b/A00_Common/spreadsheet/spreadsheet.ts index 87ec2cc7b..d452f6b95 100644 --- a/A00_Common/spreadsheet/spreadsheet.ts +++ b/A00_Common/spreadsheet/spreadsheet.ts @@ -24,6 +24,7 @@ import { createHistory } from "./spreadsheet_history"; import { attachKeys } from "./spreadsheet_keys"; import type { ConditionOptions } from "./spreadsheet_condition"; import type { LinkOptions, Links } from "./spreadsheet_links"; +import { linkCommand, type LinkInfo } from "./spreadsheet_link_set"; import { attachMenu } from "./spreadsheet_menu"; import { attachMouse } from "./spreadsheet_mouse"; import { LAST_C, LAST_R, selectCell } from "./spreadsheet_selection"; @@ -48,6 +49,7 @@ import type { export type { Workbook, CalcValues, CalcValue, LinkMaterial } from "./spreadsheet_types"; export type { LinkItem, LinkOptions } from "./spreadsheet_links"; +export type { LinkInfo } from "./spreadsheet_link_set"; export interface SpreadsheetOptions { /** 읽기만 — 도구 모음 · 메뉴 없음 · 편집 · 붙여넣기 · 끌기 막음(고르기 · 복사는 됨) */ @@ -76,6 +78,9 @@ export interface SpreadsheetHandle { recalc(): CalcValues; /** 알약 키 → 연결 칸 값(빈 칸 0) */ getLinkedValues(): Record; + /** 알약 키를 지금 시트 `칸`(A1)에 잇기 · 풀기(null) — 알약 누름과 같은 규칙 · 되돌리기 됨 · + * 주소가 틀리거나 읽기만이면 false */ + setLink(key: string, cell: string | null, info?: LinkInfo): boolean; destroy(): void; } @@ -288,6 +293,14 @@ export function createSpreadsheet( } return out; }, + setLink(key, cell, info) { + if (readOnly) return false; + const command = linkCommand(ctx.book, ctx.sheet().id, key, cell, info); + if (command === false) return false; + if (command) dispatch(command); + loadLinks(); // 처음 연결이면 알약 막대 · 칸 세모를 불러옴 + return true; + }, destroy() { clearTimeout(toastTimer); detachKeys(); diff --git a/A00_Common/spreadsheet/spreadsheet_link_set.ts b/A00_Common/spreadsheet/spreadsheet_link_set.ts new file mode 100644 index 000000000..8f740992c --- /dev/null +++ b/A00_Common/spreadsheet/spreadsheet_link_set.ts @@ -0,0 +1,55 @@ +/* ============================================================================= + * spreadsheet_link_set.ts (주인 D) + * 알약 키를 칸에 잇기 · 풀기 — 밖(M02 칸 지정 모달)에서 부르는 길(손잡이 `setLink`). + * 알약 누름(`spreadsheet_links.ts` `link`)과 같은 규칙: 키 하나 = 칸 하나 · 칸 하나 = 키 하나 · + * 다른 시트에 있던 같은 키 연결은 빠짐. 바꿈은 `시트통째`(되돌리기 짝) · DOM 을 모름(시험이 Node 로 부름). + * ========================================================================== */ + +import { parseA1, toA1 } from "./spreadsheet_address"; +import type { CellLink, Command, LinkMaterial, Workbook } from "./spreadsheet_types"; + +/** 연결에 같이 적을 알약 정보 — 없으면 있던 연결 것을 그대로 */ +export interface LinkInfo { + 이름?: string; + 단위?: string; + 자재?: LinkMaterial; +} + +/** `칸`(A1 · null = 풀기)을 `sheetId` 시트에 이을 명령 — 바뀔 것 없으면 null · 칸 주소가 틀리면 false */ +export function linkCommand( + book: Workbook, + sheetId: string, + key: string, + cell: string | null, + info: LinkInfo = {}, +): Command | null | false { + const at = cell === null ? null : parseA1(cell.trim().toUpperCase()); + if (cell !== null && !at) return false; + const a1 = at ? toA1(at.r, at.c) : null; + const old = book.시트.flatMap((s) => s.연결 ?? []).find((l) => l.키 === key); + const home = book.시트.find((s) => s.id === sheetId); + if (a1 && !home) return false; + if (a1 && home?.연결?.some((l) => l.키 === key && l.칸 === a1)) return null; // 이미 그 칸 + const one: CellLink | null = a1 ? { 키: key, 칸: a1 } : null; + if (one) { + const 이름 = info.이름 ?? old?.이름; + const 단위 = info.단위 ?? old?.단위; + const 자재 = info.자재 ?? old?.자재; + if (이름) one.이름 = 이름; + if (단위) one.단위 = 단위; + if (자재) one.자재 = 자재; + } + const commands: Command[] = []; + for (const s of book.시트) { + const was = s.연결 ?? []; + const rest = was.filter((l) => l.키 !== key && !(s === home && a1 && l.칸 === a1)); + const next = s === home && one ? [...rest, one] : rest; + if (next.length === was.length && next.every((l, i) => l === was[i])) continue; + const copy = structuredClone(s); + if (next.length) copy.연결 = next; + else delete copy.연결; + commands.push({ 종류: "시트통째", 시트: copy }); + } + if (!commands.length) return null; + return commands.length === 1 ? commands[0] : { 종류: "묶음", 명령: commands }; +} diff --git a/resources/tester/spreadsheet/spreadsheet_link_set_entry.ts b/resources/tester/spreadsheet/spreadsheet_link_set_entry.ts new file mode 100644 index 000000000..ce15d5b73 --- /dev/null +++ b/resources/tester/spreadsheet/spreadsheet_link_set_entry.ts @@ -0,0 +1,92 @@ +/* setLink 순수 길(`spreadsheet_link_set.ts`) 시험 진입 — vite --ssr 로 번들해 Node 로 돌림. + * 결과 = JSON {pass, fail, failures} 한 줄(`test_spreadsheet_set_link.py` 가 읽음) */ +import { applyCommand } from "../../../A00_Common/spreadsheet/spreadsheet_commands"; +import { linkCommand } from "../../../A00_Common/spreadsheet/spreadsheet_link_set"; +import type { Command, Workbook } from "../../../A00_Common/spreadsheet/spreadsheet_types"; + +let pass = 0; +const failures: string[] = []; +const eq = (name: string, got: unknown, want: unknown): void => { + if (JSON.stringify(got) === JSON.stringify(want)) pass += 1; + else failures.push(`${name}: ${JSON.stringify(got)} ≠ ${JSON.stringify(want)}`); +}; + +const book = (): Workbook => + ({ + 종류: "통합문서", + 판: 1, + 열: "t", + 서식: [{}], + 시트: [ + { + id: "s1", + 이름: "면적산출", + 칸: { B1: { 값: 12.5 } }, + 연결: [{ 키: "깔기|㎡", 이름: "깔기", 단위: "㎡", 칸: "B6" }], + }, + { id: "s2", 이름: "둘", 칸: {}, 연결: [{ 키: "치기|㎥", 이름: "치기", 칸: "C3" }] }, + ], + 활성: "s1", + }) as unknown as Workbook; +const links = (b: Workbook) => b.시트.map((s) => s.연결 ?? null); +const run = (b: Workbook, c: Command | null | false) => { + if (!c) return null; + return applyCommand(b, c).undo; +}; + +// 새 키를 칸에 — 이름 · 단위 · 자재가 같이 들어감 · 소문자 · 빈칸 주소도 받음 +let b = book(); +const mt = { 코드: "MT000008", 이름: "치즐", 규격: "", 단위: "EA" }; +let undo = run( + b, + linkCommand(b, "s1", "자재|MT000008", " d2 ", { 이름: "치즐", 단위: "EA", 자재: mt }), +); +eq("새 연결", b.시트[0].연결?.[1], { + 키: "자재|MT000008", + 칸: "D2", + 이름: "치즐", + 단위: "EA", + 자재: mt, +}); +// 되돌리기 = 처음 그대로 +run(b, undo); +eq("되돌림", links(b), links(book())); + +// 있는 키를 다른 칸으로 — 옛 칸 빠지고 이름 · 단위는 있던 것 +b = book(); +run(b, linkCommand(b, "s1", "깔기|㎡", "B1")); +eq("옮김", b.시트[0].연결, [{ 키: "깔기|㎡", 칸: "B1", 이름: "깔기", 단위: "㎡" }]); + +// 같은 칸에 다른 키 — 그 칸 옛 키 빠짐(칸 하나 = 키 하나) +b = book(); +run(b, linkCommand(b, "s1", "새|m", "B6", { 이름: "새", 단위: "m" })); +eq("칸 하나 키 하나", b.시트[0].연결, [{ 키: "새|m", 칸: "B6", 이름: "새", 단위: "m" }]); + +// 다른 시트에 있던 키를 이 시트로 — 저쪽 연결 빠짐(연결 칸 없으면 `연결` 지움) · 되돌리기 한 번에 둘 다 +b = book(); +undo = run(b, linkCommand(b, "s1", "치기|㎥", "A1")); +eq("시트 넘어 옮김", links(b), [ + [ + { 키: "깔기|㎡", 이름: "깔기", 단위: "㎡", 칸: "B6" }, + { 키: "치기|㎥", 칸: "A1", 이름: "치기" }, + ], + null, +]); +run(b, undo); +eq("시트 넘어 되돌림", links(b), links(book())); + +// 풀기 — 그 키만 빠지고 마지막이면 `연결` 칸이 없어짐 +b = book(); +run(b, linkCommand(b, "s1", "깔기|㎡", null)); +eq("풀기", links(b), [null, [{ 키: "치기|㎥", 이름: "치기", 칸: "C3" }]]); + +// 바뀔 것 없음 = null · 틀린 주소 = false · 없는 시트 = false +b = book(); +eq("이미 그 칸", linkCommand(b, "s1", "깔기|㎡", "b6"), null); +eq("없는 키 풀기", linkCommand(b, "s1", "없음", null), null); +eq("틀린 주소", linkCommand(b, "s1", "깔기|㎡", "가1"), false); +eq("빈 주소", linkCommand(b, "s1", "깔기|㎡", ""), false); +eq("없는 시트", linkCommand(b, "s9", "깔기|㎡", "A1"), false); +eq("문서 그대로", links(b), links(book())); + +console.log(JSON.stringify({ pass, fail: failures.length, failures })); diff --git a/resources/tester/test_spreadsheet_set_link.py b/resources/tester/test_spreadsheet_set_link.py new file mode 100644 index 000000000..13e3ba179 --- /dev/null +++ b/resources/tester/test_spreadsheet_set_link.py @@ -0,0 +1,68 @@ +"""스프레드시트 손잡이 `setLink` — 알약 키를 칸에 잇기 · 풀기(PLAN 39-4 D4). + +순수 길(`spreadsheet_link_set.ts` · DOM 모름)을 vite `--ssr` 로 번들해 Node 로 돌림 — +새 연결 · 옮김 · 칸 하나 = 키 하나 · 시트 넘어 옮김 · 풀기 · 되돌리기 · 바뀔 것 없음 · 틀린 주소. +손잡이 몸(`spreadsheet.ts`)은 글로 확인 — 화면 조작은 ORCA. +""" + +from __future__ import annotations + +import json +import shutil +import subprocess +import tempfile +from pathlib import Path + +import pytest + +ROOT = Path(__file__).resolve().parents[2] +ENTRY = ROOT / "resources" / "tester" / "spreadsheet" / "spreadsheet_link_set_entry.ts" +VITE = ROOT / "config" / "node_modules" / "vite" / "bin" / "vite.js" + + +@pytest.mark.skipif(shutil.which("node") is None, reason="node 가 없음") +def test_setLink_순수_길(): + with tempfile.TemporaryDirectory(prefix="spreadsheet_link_set_") as outdir: + entry_rel = "../" + str(ENTRY.relative_to(ROOT)).replace("\\", "/") + build = subprocess.run( + [ + "node", + str(VITE), + "build", + "--configLoader", + "runner", + "--ssr", + entry_rel, + "--outDir", + outdir, + ], + cwd=str(ROOT), + capture_output=True, + text=True, + encoding="utf-8", + errors="replace", + timeout=120, + ) + assert build.returncode == 0, f"vite ssr 번들 실패:\n{build.stdout}\n{build.stderr}" + run = subprocess.run( + ["node", str(Path(outdir) / "spreadsheet_link_set_entry.js")], + capture_output=True, + text=True, + encoding="utf-8", + errors="replace", + timeout=60, + ) + assert run.stdout.strip(), run.stderr + result = json.loads(run.stdout) + assert result["fail"] == 0, result["failures"] + assert result["pass"] == 13 + + +def test_손잡이_setLink_몸(): + src = (ROOT / "A00_Common" / "spreadsheet" / "spreadsheet.ts").read_text(encoding="utf-8") + assert "setLink(key: string, cell: string | null, info?: LinkInfo): boolean;" in src + body = src.split(" setLink(key, cell, info) {")[1].split(" destroy() {")[0] + assert "if (readOnly) return false;" in body + assert "linkCommand(ctx.book, ctx.sheet().id, key, cell, info)" in body + assert "if (command) dispatch(command);" in body # 되돌리기 이력에 쌓임(setDoc 아님) + assert "loadLinks();" in body From 0b158636ccb6d8233aac4a914db2ae472e91f254 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sun, 4 Oct 2026 17:23:56 +0900 Subject: [PATCH 08/14] =?UTF-8?q?feat(M02):=20=EB=A9=B4=EC=A0=81=EC=82=B0?= =?UTF-8?q?=EC=B6=9C=20=EC=B9=B8=20=EC=A7=80=EC=A0=95=20=EB=AA=A8=EB=8B=AC?= =?UTF-8?q?=20AreaLink=20=E2=80=94=20=EC=A7=80=EA=B8=88=20=EA=B3=A0?= =?UTF-8?q?=EB=A5=B8=20=EC=B9=B8=EC=9C=BC=EB=A1=9C=20=C2=B7=20=EC=A3=BC?= =?UTF-8?q?=EC=86=8C=EB=A1=9C=20=EC=9E=87=EA=B8=B0=20=C2=B7=20=ED=92=80?= =?UTF-8?q?=EA=B8=B0=20=C2=B7=20=EC=A0=95=EB=B3=B8=EC=9D=80=20=EB=A9=B4?= =?UTF-8?q?=EC=A0=81=20=EB=AC=B8=EC=84=9C=20=EC=97=B0=EA=B2=B0(setLink)=20?= =?UTF-8?q?(PLAN=2039-4=20D5)?= 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_01W4uKaV4ZBhsJN4GHG7yRj4 --- .../M02_MasterTemplete_AreaLink.ts | 107 ++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 M02_MasterTemplete/M02_MasterTemplete_AreaLink.ts diff --git a/M02_MasterTemplete/M02_MasterTemplete_AreaLink.ts b/M02_MasterTemplete/M02_MasterTemplete_AreaLink.ts new file mode 100644 index 000000000..9bd76f293 --- /dev/null +++ b/M02_MasterTemplete/M02_MasterTemplete_AreaLink.ts @@ -0,0 +1,107 @@ +/* ============================================================================= + * M02_MasterTemplete_AreaLink.ts + * 면적산출 상세 — 칸 지정 모달(PLAN 39-4 D5). 오른쪽 패널의 담은 로직 · 자재 줄을 누르면 엶. + * 그 줄 알약 키(로직 물량 「이름|단위」 · 자재 「자재|MT키」)를 면적산출 칸에 잇기 · 옮기기 · 풀기. + * 정본 = 면적산출 문서 `연결` 한 곳 — 스프레드시트 손잡이 `setLink`(알약 누름과 같은 규칙 · 되돌리기 됨)라 + * 아래 알약 줄 · 칸 세모 · 패널 수량이 같이 따라감. 손 입력 수량은 일위대가 몫(여기 없음) · 저장은 [저장]. + * ========================================================================== */ + +import { createButton, createInputField, el } from "@ui/ui_template_elements"; +import { openModal } from "@ui/ui_template_modal"; +import type { LinkInfo, SpreadsheetHandle } from "../A00_Common/spreadsheet/spreadsheet"; + +const TEXT = { + title: "칸 지정 — {value}", + close: "닫기", + now: "지금 연결", + none: "연결 없음", + picked: "지금 고른 칸으로 ({value})", + address: "칸 주소", + hint: "예 B6", + link: "잇기", + unlink: "풀기", + bad: "칸 주소가 틀림 — 예 B6", + readOnly: "읽기만 — 고칠 수 없음", +}; + +const fill = (text: string, value: string): string => text.replace("{value}", value); + +export interface AreaLinkOptions { + /** 줄 이름(모달 제목) */ + label: string; + /** 알약 키 */ + key: string; + /** 연결에 같이 적을 이름 · 단위 · 자재 */ + info: LinkInfo; + sheet: SpreadsheetHandle; + /** 시트에서 지금 고른 칸(A1) */ + picked: () => string; + readOnly: boolean; +} + +/** 지금 연결 칸 · 값 글 — 「B6 = 12.5 ㎡」 · 없으면 null */ +function linkedText(opts: AreaLinkOptions): string | null { + const link = opts.sheet + .getDoc() + .시트.flatMap((s) => s.연결 ?? []) + .find((l) => l.키 === opts.key); + if (!link) return null; + const value = opts.sheet.getLinkedValues()[opts.key]; + return [link.칸, "=", String(value ?? ""), opts.info.단위 ?? ""].join(" ").trim(); +} + +export function openAreaLink(opts: AreaLinkOptions): void { + const { close } = openModal({ + title: fill(TEXT.title, opts.label), + closeLabel: TEXT.close, + dialogClass: "m02-modal m02-area-link", + mount: (body) => { + const now = el("p", { className: "m02-area-link__now" }); + const paint = (): void => { + const text = linkedText(opts); + now.textContent = `${TEXT.now} · ${text ?? TEXT.none}`; + unlink.disabled = opts.readOnly || !text; + }; + const done = (cell: string | null): boolean => { + if (!opts.sheet.setLink(opts.key, cell, opts.info)) return false; + close(); + return true; + }; + const here = opts.picked(); + const pick = createButton({ + label: fill(TEXT.picked, here), + variant: "filled", + size: "sm", + onClick: () => void done(here), + }); + const address = createInputField({ label: TEXT.address, placeholder: TEXT.hint }); + const link = createButton({ + label: TEXT.link, + variant: "ghost", + size: "sm", + onClick: () => { + if (!done(address.input.value)) address.setError(TEXT.bad); + }, + }); + const unlink = createButton({ + label: TEXT.unlink, + variant: "ghost", + size: "sm", + onClick: () => void done(null), + }); + address.input.addEventListener("input", () => address.setError(undefined)); + address.input.addEventListener("keydown", (ev) => ev.key === "Enter" && link.click()); + pick.disabled = link.disabled = address.input.disabled = opts.readOnly; + paint(); + body.append( + now, + ...(opts.readOnly + ? [el("p", { className: "m02-struct__empty", text: TEXT.readOnly })] + : []), + el("div", { className: "m02-area-link__row", children: [pick, unlink] }), + el("div", { className: "m02-area-link__row", children: [address.root, link] }), + ); + if (!opts.readOnly) address.input.focus(); + }, + }); +} From 18a063925351ff1e5b72deddf1b7ecaf3522859c Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sun, 4 Oct 2026 17:24:00 +0900 Subject: [PATCH 09/14] =?UTF-8?q?feat(M02):=20=EA=B5=AC=EC=A1=B0=EB=AC=BC?= =?UTF-8?q?=20=ED=95=AD=EB=AA=A9=20=EB=AA=A9=EB=A1=9D=20=EB=B6=80=ED=92=88?= =?UTF-8?q?=20=E2=80=94=20M01=20=EC=B0=BE=EA=B8=B0=20=EC=A4=84=20=C2=B7=20?= =?UTF-8?q?=EC=AA=BD=20=C2=B7=20=ED=91=9C=20=EA=BC=B4=20=C2=B7=20=EC=A2=85?= =?UTF-8?q?=EB=A5=98=EB=A7=88=EB=8B=A4=20=EC=B9=B8(=EB=8F=84=EB=A9=B4=20?= =?UTF-8?q?=C2=B7=20=EB=A9=B4=EC=A0=81=EC=82=B0=EC=B6=9C=20=C2=B7=20?= =?UTF-8?q?=EC=9D=BC=EC=9C=84=EB=8C=80=EA=B0=80=20=C2=B7=20=ED=95=AD?= =?UTF-8?q?=EB=AA=A9=20=ED=86=B5=EC=A7=B8)=20=C2=B7=20=EC=84=9C=EB=B2=84?= =?UTF-8?q?=20=EB=AA=A9=EB=A1=9D=20=EC=B9=B8=20=EC=88=98=EC=A0=95=EC=9D=BC?= =?UTF-8?q?=20=C2=B7=20labor(=EB=B2=88=ED=98=B8=20=C2=B7=20=EB=8B=B4?= =?UTF-8?q?=EC=9D=80=20=EB=A1=9C=EC=A7=81=20=EC=88=98=20=C2=B7=20=ED=95=A9?= =?UTF-8?q?=EA=B3=84)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 39-5 H1 · H6 — 전체 항목 · 복사 생성 · 항목 하나 복사가 같이 쓸 목록 하나. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016PSXsvyDxjNmufteN5bqmt --- .../M02_MasterTemplete_ItemList.ts | 182 ++++++++++++++++++ .../M02_MasterTemplete_Items.py | 40 +++- resources/tester/helper_m02_item_list.cjs | 65 +++++++ resources/tester/test_m02_item_list.py | 94 +++++++++ resources/tester/test_m02_layers_store.py | 8 +- 5 files changed, 387 insertions(+), 2 deletions(-) create mode 100644 M02_MasterTemplete/M02_MasterTemplete_ItemList.ts create mode 100644 resources/tester/helper_m02_item_list.cjs create mode 100644 resources/tester/test_m02_item_list.py diff --git a/M02_MasterTemplete/M02_MasterTemplete_ItemList.ts b/M02_MasterTemplete/M02_MasterTemplete_ItemList.ts new file mode 100644 index 000000000..363c48e5c --- /dev/null +++ b/M02_MasterTemplete/M02_MasterTemplete_ItemList.ts @@ -0,0 +1,182 @@ +/* ============================================================================= + * M02_MasterTemplete_ItemList.ts + * 구조물 항목 목록 하나(PLAN 39-5) — 「전체 항목」(도면 · 일위대가 · 면적산출) · 복사 생성 · 항목 하나 + * 복사가 같이 씀. 꼴 = M01 마스터 화면 — 찾기 줄(`buildHeadFilters`) · 쪽(`buildPager`) · 표 + * (`m01-master__grid`) 를 M01 파일에서 그대로 import(옮기지 않음). + * 자료 = 층 나무 항목(`GET /layers/{층}/tree` — 서버 `items` 의 `수정일` · `labor` 칸) · 칸은 종류마다. + * 찾기 = 보이는 칸 글 전부에서 띄어쓰기 AND(나무 찾기와 같은 `matchText`). + * ========================================================================== */ + +import { createInputField, el } from "@ui/ui_template_elements"; +import { buildPager } from "../M01_MasterData/M01_MasterData_UI_Cells"; +import { buildHeadFilters } from "../M01_MasterData/M01_MasterData_UI_Head"; +import "../M01_MasterData/M01_MasterData_UI_Style.css"; +import { fetchTree, type Layer, type TreeItem } from "./M02_MasterTemplete_Api_Fetch"; +import { folderOf, leafName, matchText } from "./M02_MasterTemplete_TreePanel_Model"; + +/** 목록 종류 — 하위 컨테이너 셋 + 항목 통째(복사 생성) */ +export type ListKind = "structure" | "area" | "labor" | "item"; + +/** 나무 항목 줄 + 목록 칸(서버 `M02_MasterTemplete_Items._list_extras`) */ +export interface ItemRow extends TreeItem { + 도번?: string | null; + 면적번?: string | null; + 수정일?: Partial>; + labor?: { 번호: string | null; 로직수: number; 합계: number | null } | null; +} + +export interface Col { + head: string; + get: (row: R) => string; +} + +const TEXT = { + search: "찾기", + none: "항목이 없음", + noMatch: "맞는 항목이 없음", + loading: "불러오는 중", +}; + +const SIZE = 30; +const DASH = "—"; +const day = (iso?: string): string => (iso ? iso.slice(0, 10) : ""); +const money = (value: number | null | undefined): string => + typeof value === "number" ? value.toLocaleString("ko-KR", { maximumFractionDigits: 0 }) : DASH; +/** 가장 늦게 고친 날 — 항목 통째 */ +const latest = (row: ItemRow): string => + day( + Object.values(row.수정일 ?? {}) + .sort() + .pop(), + ); + +const NAME: Col = { head: "이름", get: (r) => leafName(r) }; +const FOLDER: Col = { head: "폴더", get: (r) => folderOf(r).join(" / ") }; +const UNIT: Col = { head: "단위", get: (r) => r.단위 ?? "" }; +const changed = (part: "structure" | "area" | "labor"): Col => ({ + head: "고친 날", + get: (r) => day(r.수정일?.[part]), +}); + +/** 종류마다 칸(39-5 · 정함 5장) — 규격 · 층 칸 없음(한 목록 = 한 층) */ +export const ITEM_COLS: Record[]> = { + structure: [ + { head: "도번", get: (r) => r.도번 || DASH }, + NAME, + FOLDER, + UNIT, + changed("structure"), + ], + area: [{ head: "면적번", get: (r) => r.면적번 || DASH }, NAME, FOLDER, UNIT, changed("area")], + labor: [ + { head: "일위대가번", get: (r) => r.labor?.번호 || DASH }, + NAME, + FOLDER, + UNIT, + { head: "담은 로직 수", get: (r) => (r.labor ? String(r.labor.로직수) : DASH) }, + { head: "합계", get: (r) => money(r.labor?.합계) }, + changed("labor"), + ], + item: [ + NAME, + FOLDER, + UNIT, + { head: "도번", get: (r) => r.도번 || DASH }, + { head: "일위대가번", get: (r) => r.labor?.번호 || DASH }, + { head: "면적번", get: (r) => r.면적번 || DASH }, + { head: "고친 날", get: latest }, + ], +}; + +/** 찾기 · 쪽 — 보일 줄 · 맞춘 쪽 · 맞는 줄 수(화면 밖 · 시험이 부름) */ +export function pageOf( + rows: R[], + cols: Col[], + query: string, + page: number, + size = SIZE, +): { shown: R[]; page: number; total: number } { + const hits = rows.filter((r) => + matchText( + cols.map((c) => c.get(r)), + query, + ), + ); + const last = Math.max(1, Math.ceil(hits.length / size)); + const at = Math.min(Math.max(1, page), last); + return { shown: hits.slice((at - 1) * size, at * size), page: at, total: hits.length }; +} + +/** 한 층 항목 — `owner` = 남의 층(없으면 자기 층) */ +export const loadItems = async (layer: Layer, owner: string | null): Promise => + (await fetchTree(layer, owner)).items as ItemRow[]; + +export interface ItemListHandle { + root: HTMLElement; + /** 줄 · 칸 · 누를 일을 바꿔 그림(찾는 글은 그대로 · 쪽은 1) */ + show: (rows: R[], cols: Col[], onPick: (row: R) => void) => void; + /** 표 자리에 글 한 줄(불러오는 중 · 못 받음) */ + message: (text?: string) => void; +} + +export function buildItemList(): ItemListHandle { + let draw = (): void => {}; + let page = 1; + const searchField = createInputField({ type: "search", placeholder: TEXT.search }); + const search = searchField.input; + const grid = el("div", { className: "m01-master__grid-wrap" }); + let pager = buildPager(0, SIZE, 1, () => {}); + const note = (text: string): void => { + grid.replaceChildren(el("p", { className: "m01-master__empty", text })); + }; + + const show = (rows: R[], cols: Col[], onPick: (row: R) => void): void => { + page = 1; + draw = (): void => { + const got = pageOf(rows, cols, search.value, page); + page = got.page; + const next = buildPager(got.total, SIZE, page, (p) => { + page = p; + draw(); + }); + pager.replaceWith(next); + pager = next; + if (!got.shown.length) return note(rows.length ? TEXT.noMatch : TEXT.none); + const body = got.shown.map((row) => { + const tr = el("tr", { + className: "m01-master__table-row", + children: cols.map((c) => { + const text = c.get(row); + return el("td", { text, attrs: { title: text } }); + }), + }); + tr.addEventListener("click", () => onPick(row)); + return tr; + }); + grid.replaceChildren( + el("table", { + className: "m01-master__grid", + children: [ + el("thead", { + children: [el("tr", { children: cols.map((c) => el("th", { text: c.head })) })], + }), + el("tbody", { children: body }), + ], + }), + ); + }; + draw(); + }; + + search.addEventListener("input", () => { + page = 1; + draw(); + }); + const root = el("div", { + className: "m02-itemlist", + children: [buildHeadFilters([searchField.root, pager]), grid], + }); + // 처음 = 빈 표 자리(줄이 오면 `show`) + note(TEXT.loading); + return { root, show, message: (text = TEXT.loading) => note(text) }; +} diff --git a/M02_MasterTemplete/M02_MasterTemplete_Items.py b/M02_MasterTemplete/M02_MasterTemplete_Items.py index 90f0dacd7..385c46d28 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Items.py +++ b/M02_MasterTemplete/M02_MasterTemplete_Items.py @@ -9,6 +9,7 @@ from __future__ import annotations import json +from datetime import datetime from pathlib import Path from typing import Any @@ -85,10 +86,47 @@ def items(folder: Path) -> list[dict[str, Any]]: k: (head or r.get("열")) if r.get(k) else None for k in ("도번", "면적번", "재료번") }, } - out.append({"key": r.get("키"), **{k: r.get(k) for k in ITEM_FIELDS}, "names": names}) + out.append( + { + "key": r.get("키"), + **{k: r.get(k) for k in ITEM_FIELDS}, + "names": names, + **_list_extras(Path(folder), str(r.get("열"))), + } + ) return out +def _list_extras(folder: Path, col: str) -> dict[str, Any]: + """목록 칸(39-5) — `수정일` = 도면 · 면적산출 · 일위대가 파일 시각 · `labor` = 일위대가 문서의 + 번호 · 담은 로직 수 · 합계(`계산값.반영합.계` · 없으면 null) — 파일이 없으면 칸 없음.""" + stamps: dict[str, str] = {} + for kind in ("structure", "area", "labor"): + path = folder / kind / f"{col}.json" + if path.is_file(): + stamps[kind] = datetime.fromtimestamp(path.stat().st_mtime).isoformat( + timespec="seconds" + ) + labor = None + path = folder / "labor" / f"{col}.json" + if path.is_file(): + try: + doc = json.loads(path.read_bytes()) + except ValueError: + doc = {} + total = ((doc.get("계산값") or {}).get("반영합") or {}).get("계") + try: + total = None if total is None or isinstance(total, bool) else float(total) + except (TypeError, ValueError): + total = None # 수 아닌 값(옛 꼴)은 「—」 + labor = { + "번호": doc.get("일위대가번"), + "로직수": len(doc.get("담은로직") or []), + "합계": total, + } + return {"수정일": stamps, "labor": labor} + + def _fresh_id(want: str, used: set[str]) -> str: out, n = want, 2 while out in used: diff --git a/resources/tester/helper_m02_item_list.cjs b/resources/tester/helper_m02_item_list.cjs new file mode 100644 index 000000000..875074b62 --- /dev/null +++ b/resources/tester/helper_m02_item_list.cjs @@ -0,0 +1,65 @@ +/* M02 항목 목록(`M02_MasterTemplete_ItemList.ts`) 칸 · 찾기 · 쪽 단위검증 헬퍼 — 트랜스파일해 Node 로 돌리고 + * JSON 으로 냄. 화면 부품(ui_template · M01 머리 · css)은 빈 것으로 갈음 · 나무 자료(`TreePanel_Model`)는 진짜. */ +const fs = require("fs"); +const path = require("path"); +const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript")); + +const M02 = path.join(__dirname, "..", "..", "M02_MasterTemplete"); +const load = (name, stubs) => { + const js = ts.transpileModule(fs.readFileSync(path.join(M02, name), "utf8"), { + compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 }, + }).outputText; + const box = { exports: {} }; + const need = (id) => (id in stubs ? stubs[id] : {}); + new Function("exports", "module", "require", js)(box.exports, box, need); + return box.exports; +}; +const model = load("M02_MasterTemplete_TreePanel_Model.ts", {}); +const m = load("M02_MasterTemplete_ItemList.ts", { + "./M02_MasterTemplete_TreePanel_Model": model, +}); + +const row = (열, 경로, extra = {}) => ({ + key: `SS-${열}`, + 열, + 경로, + 이름: 경로.join(" "), + 단위: "m", + 도번: `SD-${열}`, + 면적번: `SA-${열}`, + 수정일: { structure: "2026-10-01T09:00:00", area: "2026-10-03T10:00:00" }, + labor: null, + ...extra, +}); +const rows = [ + row("c1", ["기슭막이", "메쌓기", "1.2m"], { + labor: { 번호: "SU-000001", 로직수: 3, 합계: 1234567.4 }, + 수정일: { structure: "2026-10-01T09:00:00", labor: "2026-10-04T11:00:00" }, + }), + row("c2", ["기슭막이", "찰쌓기"], { labor: { 번호: "SU-000002", 로직수: 0, 합계: null } }), + row("c3", ["옹벽"], { 도번: "", 수정일: {} }), +]; +const cells = (kind) => rows.map((r) => m.ITEM_COLS[kind].map((c) => c.get(r))); +const many = Array.from({ length: 65 }, (_, i) => row(`n${i}`, ["배수", `측구 ${i}`])); + +process.stdout.write( + JSON.stringify({ + heads: Object.fromEntries( + Object.entries(m.ITEM_COLS).map(([k, cols]) => [k, cols.map((c) => c.head)]), + ), + cells: { + structure: cells("structure"), + area: cells("area"), + labor: cells("labor"), + item: cells("item"), + }, + find: m.pageOf(rows, m.ITEM_COLS.structure, "기슭 찰", 1).shown.map((r) => r.열), + findLabor: m.pageOf(rows, m.ITEM_COLS.labor, "SU-000001", 1).shown.map((r) => r.열), + findNone: m.pageOf(rows, m.ITEM_COLS.structure, "없는말", 1), + page3: (({ page, total, shown }) => ({ page, total, n: shown.length }))( + m.pageOf(many, m.ITEM_COLS.structure, "", 3), + ), + pageOver: m.pageOf(many, m.ITEM_COLS.structure, "", 9).page, + pageEmpty: m.pageOf([], m.ITEM_COLS.structure, "", 2).page, + }), +); diff --git a/resources/tester/test_m02_item_list.py b/resources/tester/test_m02_item_list.py new file mode 100644 index 000000000..63b809bd7 --- /dev/null +++ b/resources/tester/test_m02_item_list.py @@ -0,0 +1,94 @@ +"""M02 항목 목록(PLAN 39-5) — 칸 · 찾기 · 쪽(TS · `helper_m02_item_list.cjs`) · +서버 목록 칸(임시 storage). + +① 종류마다 칸(도면 · 면적산출 · 일위대가 · 항목 통째) · 빈 번호 「—」 · 합계 없으면 「—」 · + 고친 날 = 날짜만 · 찾기 = 보이는 칸 AND · 쪽 30 줄 · 넘친 쪽은 끝 쪽. +② 서버 `items` — `수정일`(도면 · 면적산출 · 일위대가 파일) · `labor`(번호 · 담은 로직 수 · + 합계 = 계산값.반영합.계) · 일위대가 파일이 없으면 null · 깨진 파일 · 수 아닌 합계도 목록은 뜸. +""" + +from __future__ import annotations + +import json +import os +import subprocess + +import pytest + +from resources.tester import test_m02_tree as base +from resources.tester.test_m02_tree import Tree, _as + +world = base.world # 같은 임시 storage 판 +HERE = os.path.dirname(os.path.abspath(__file__)) + + +@pytest.fixture(scope="module") +def ts() -> dict: + proc = subprocess.run( + ["node", os.path.join(HERE, "helper_m02_item_list.cjs")], + capture_output=True, + text=True, + encoding="utf-8", + timeout=60, + ) + assert proc.returncode == 0, proc.stderr + return json.loads(proc.stdout) + + +def test_종류마다_칸(ts): + assert ts["heads"]["structure"] == ["도번", "이름", "폴더", "단위", "고친 날"] + assert ts["heads"]["area"] == ["면적번", "이름", "폴더", "단위", "고친 날"] + assert ts["heads"]["labor"] == [ + "일위대가번", + "이름", + "폴더", + "단위", + "담은 로직 수", + "합계", + "고친 날", + ] + assert "규격" not in sum(ts["heads"].values(), []) and "층" not in sum(ts["heads"].values(), []) + + +def test_칸_값_빈_번호_합계_날짜(ts): + s, labor = ts["cells"]["structure"], ts["cells"]["labor"] + assert s[0] == ["SD-c1", "1.2m", "기슭막이 / 메쌓기", "m", "2026-10-01"] + assert s[2][0] == "—" and s[2][2] == "" # 빈 도번 · 맨 위 항목(폴더 없음) + assert labor[0][3:] == ["m", "3", "1,234,567", "2026-10-04"] # 합계 원 단위 반올림 + assert labor[1][5] == "—" and labor[2][0] == labor[2][4] == "—" # 합계 없음 · 일위대가 없음 + assert ts["cells"]["item"][0][-1] == "2026-10-04" # 항목 통째 = 가장 늦게 고친 날 + + +def test_찾기_쪽(ts): + assert ts["find"] == ["c2"] # 낱말 둘 AND(폴더 · 이름 칸) + assert ts["findLabor"] == ["c1"] # 번호 칸도 찾음 + assert ts["findNone"] == {"shown": [], "page": 1, "total": 0} + assert ts["page3"] == {"page": 3, "total": 65, "n": 5} + assert ts["pageOver"] == 3 and ts["pageEmpty"] == 1 + + +def test_서버_목록_칸_수정일_labor(world): + t = Tree(_as(world), "personal") + t.do(op="add-item", 경로=["배수"], 이름="측구", 단위="m") + t.do(op="add-item", 경로=[], 이름="옹벽", 단위="m") + root = world["storage"] / "7" / "42" / "templates" + ditch, wall = t.item(["배수", "측구"]), t.item(["옹벽"]) + assert set(ditch["수정일"]) >= {"structure", "area"} + assert len(ditch["수정일"]["structure"]) == 19 # 초 단위 ISO + assert ditch["labor"] is None # 일위대가 파일 없음 + (root / "labor").mkdir(exist_ok=True) + doc = { + "일위대가번": "SU-000001", + "담은로직": [{"로직": "GF000219"}, {"로직": "GF000137"}], + "계산값": {"반영합": {"계": 12345.6}}, + } + (root / "labor" / f"{ditch['열']}.json").write_text(json.dumps(doc), encoding="utf-8") + (root / "labor" / f"{wall['열']}.json").write_text("{깨짐", encoding="utf-8") + rows = {r["열"]: r for r in t.client.get(t.url).json()["items"]} + assert rows[ditch["열"]]["labor"] == {"번호": "SU-000001", "로직수": 2, "합계": 12345.6} + assert "labor" in rows[ditch["열"]]["수정일"] + assert rows[wall["열"]]["labor"] == {"번호": None, "로직수": 0, "합계": None} + doc["계산값"] = {"반영합": {"계": "글"}} + (root / "labor" / f"{ditch['열']}.json").write_text(json.dumps(doc), encoding="utf-8") + again = {r["열"]: r for r in t.client.get(t.url).json()["items"]} + assert again[ditch["열"]]["labor"]["합계"] is None diff --git a/resources/tester/test_m02_layers_store.py b/resources/tester/test_m02_layers_store.py index 1be487e98..9398233f1 100644 --- a/resources/tester/test_m02_layers_store.py +++ b/resources/tester/test_m02_layers_store.py @@ -77,7 +77,13 @@ def test_시스템_층은_그대로_항목_목록은_SS_줄(world): rows = client.get("/api/m02/items").json() ss = store.read_combos()["줄"] assert [r["key"] for r in rows] == [r["키"] for r in ss] and rows[0]["key"] == "SS000001" - assert set(rows[0]) == {"key", "names", *items_router.items_store.ITEM_FIELDS} + assert set(rows[0]) == { + "key", + "names", + "수정일", # 목록 칸(39-5) + "labor", + *items_router.items_store.ITEM_FIELDS, + } assert set(rows[0]["names"]) == {"일위대가", "자재조합", "도번", "면적번", "재료번"} assert client.get("/api/m02/layers/system/structure-combos").json() == store.read_combos() # 시스템 층 쓰기는 SYSTEM_ADMIN 만 From 134a727c9cf6e98832496cc162f4faf3b87225c1 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sun, 4 Oct 2026 17:27:02 +0900 Subject: [PATCH 10/14] =?UTF-8?q?feat(M02):=20=EC=83=81=EC=84=B8=20?= =?UTF-8?q?=EC=A0=9C=EB=AA=A9=20M01=20=EB=A8=B8=EB=A6=AC=20=EA=BC=B4=20?= =?UTF-8?q?=E2=80=94=20=EC=A0=9C=EB=AA=A9=2018px=20=EC=99=BC=EC=AA=BD=20?= =?UTF-8?q?=C2=B7=20=E3=80=8C=EB=8F=8C=EC=95=84=EA=B0=80=EA=B8=B0=E3=80=8D?= =?UTF-8?q?=20=EB=8A=94=20=EC=A0=9C=EB=AA=A9=20=EC=95=9E=20=C2=B7=20?= =?UTF-8?q?=EB=8B=A8=EC=B6=94=20=EC=98=A4=EB=A5=B8=EC=AA=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 39-5 — 마스터 템플릿 상세 머리 전부(구조물 조합 · 도면 · 면적산출 · 일위대가 · 양식 밖 화면). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016PSXsvyDxjNmufteN5bqmt --- M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts | 3 ++- M02_MasterTemplete/M02_MasterTemplete_UI_Style.css | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts b/M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts index 480f213f2..522751dc9 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_UI_Main.ts @@ -152,7 +152,8 @@ export function createMain( ...(extras.top ? [extras.top] : []), el("div", { className: "m02-main__head", - children: [title, back, slot, reload, save, ...(extras.newItem ? [extras.newItem] : [])], + // M01 머리 꼴(39-5) — ← 는 제목 앞 · 제목 18px 왼쪽 · 단추는 오른쪽(slot 이 늘어나 밂) + children: [back, title, slot, reload, save, ...(extras.newItem ? [extras.newItem] : [])], }), ...(extras.search ? [extras.search] : []), notice, diff --git a/M02_MasterTemplete/M02_MasterTemplete_UI_Style.css b/M02_MasterTemplete/M02_MasterTemplete_UI_Style.css index 0e323ca8c..4b4fa823a 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_UI_Style.css +++ b/M02_MasterTemplete/M02_MasterTemplete_UI_Style.css @@ -42,7 +42,7 @@ overflow-wrap: anywhere; /* 번호 + 이름 — 잘리지 않게 줄바꿈 */ margin: 0; color: var(--color-text); - font-size: var(--text-body); + font-size: 18px; /* M01 머리 제목과 같음(39-5) */ } .m02-main__head .ui-btn { From 43a59a68932d1d80e4ae7779d2e9bf44c107ab0f Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sun, 4 Oct 2026 17:27:30 +0900 Subject: [PATCH 11/14] =?UTF-8?q?refactor(M02):=20=EC=9D=BC=EC=9C=84?= =?UTF-8?q?=EB=8C=80=EA=B0=80=20=EC=9E=85=EB=A0=A5=20=EB=AA=A9=EB=A1=9D=20?= =?UTF-8?q?=C2=B7=20=EB=84=A3=EC=9D=84=20=EA=B0=92=20=EC=B9=B8=20=EB=B6=80?= =?UTF-8?q?=ED=92=88=20=EC=98=AE=EA=B9=80(39-3=20C2)=20=E2=80=94=20?= =?UTF-8?q?=EC=84=A0=ED=83=9D=20=EC=9E=85=EB=A0=A5=EC=97=90=EC=84=9C=20M02?= =?UTF-8?q?=5FLabor=5FInputs.py=20=C2=B7=20M02=5FLabor=5FValues(=5FModel).?= =?UTF-8?q?ts=20=EB=A1=9C=20=C2=B7=20=EC=98=9B=20=EC=9E=90=EB=A6=AC?= =?UTF-8?q?=EB=8A=94=20=EB=8B=A4=EC=8B=9C=20=EB=82=B4=EB=B3=B4=EB=83=84=20?= =?UTF-8?q?=C2=B7=20=EC=9E=85=EB=A0=A5=20=EB=AA=A9=EB=A1=9D=20=EA=B8=B8=20?= =?UTF-8?q?logic-lib/inputs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 서버: inputs · combo_logics · _num 몸을 M02_Labor_Inputs.py 로(logic_inputs = 로직 키들 입력 + 자동값) · M02_MasterTemplete_Inputs.py 는 그것을 부름 - 길: GET /api/m02/logic-lib/inputs?keys= (로그인 · 일위대가 입력 모달용) - 화면: 값 카드(createInputsValueCard) · 값 함수 → M02_Labor_Values.ts · M02_Labor_Values_Model.ts · 값 칸 하나(createValueControl)를 떼어 값 카드와 입력 모달이 같이 씀 · CSS 도 옮김 - 옛 자리(M02_MasterTemplete_Inputs.ts · _Inputs_Model.ts)는 새 파일을 다시 내보냄 — B05 고르기 모달 그대로 - 시험: 새 test_m02_labor_values.py · 옮긴 자리 따라 test_item_picker_detail · test_m02_card_shell · helper_m02_inputs 고침 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01MgChuAcyBqUkhHMTXomQC7 --- M02_MasterTemplete/M02_Labor_Inputs.py | 94 ++++++ M02_MasterTemplete/M02_Labor_Values.css | 99 ++++++ M02_MasterTemplete/M02_Labor_Values.ts | 316 ++++++++++++++++++ M02_MasterTemplete/M02_Labor_Values_Model.ts | 116 +++++++ .../M02_MasterTemplete_Inputs.css | 98 ------ .../M02_MasterTemplete_Inputs.py | 65 +--- .../M02_MasterTemplete_Inputs.ts | 299 +---------------- .../M02_MasterTemplete_Inputs_Model.ts | 58 +--- .../M02_MasterTemplete_Router_Combos.py | 7 + resources/tester/helper_m02_inputs.cjs | 19 +- resources/tester/test_item_picker_detail.py | 9 +- resources/tester/test_m02_card_shell.py | 4 +- resources/tester/test_m02_labor_values.py | 101 ++++++ 13 files changed, 770 insertions(+), 515 deletions(-) create mode 100644 M02_MasterTemplete/M02_Labor_Inputs.py create mode 100644 M02_MasterTemplete/M02_Labor_Values.css create mode 100644 M02_MasterTemplete/M02_Labor_Values.ts create mode 100644 M02_MasterTemplete/M02_Labor_Values_Model.ts create mode 100644 resources/tester/test_m02_labor_values.py diff --git a/M02_MasterTemplete/M02_Labor_Inputs.py b/M02_MasterTemplete/M02_Labor_Inputs.py new file mode 100644 index 000000000..068c64b33 --- /dev/null +++ b/M02_MasterTemplete/M02_Labor_Inputs.py @@ -0,0 +1,94 @@ +"""M02 일위대가 입력 목록(39-3 · 옛 선택 입력 `M02_MasterTemplete_Inputs` 에서 옮김). + +담은 단가산출 로직마다 입력 전부 · 로직마다 갈라 보이고 이름이 같아도 합치지 않음 · 입력 목록은 +그 로직 줄의 `입력` 그대로(하위 로직으로 넘겨지는 입력은 맨 위 로직 입력으로만) · 자동값 = +`master_auto.values`(보기용 · 저장 안 함). 일위대가 입력 모달(`GET /api/m02/logic-lib/inputs`)과 +옛 선택 입력 길(`inputs`)이 같이 씀. M01 은 읽기만 · 권한은 부르는 쪽 몫. +""" + +from __future__ import annotations + +from decimal import Decimal +from typing import Any + +from M01_MasterData import M01_MasterData_Store as m01 +from M01_MasterData import M01_MasterData_Store_Combo as combo_store +from M02_MasterTemplete import M02_MasterTemplete_Combo_Store as combos + +_SPEC = ("이름", "설명", "단위", "고르기", "범위") + + +def _num(value: Any) -> Any: + """Decimal → JSON 수(M01 길과 같은 규칙) · 목록 · 묶음 안까지.""" + if isinstance(value, Decimal): + return int(value) if value == value.to_integral_value() else float(value) + if isinstance(value, list): + return [_num(v) for v in value] + if isinstance(value, dict): + return {k: _num(v) for k, v in value.items()} + return value + + +def _logics(codes: list[str]) -> tuple[list[dict], list[str]]: + """([로직 줄 …], 빠진 로직 키) — 키 차례 그대로.""" + whole = m01.loaded()[1] + found, missing = [], [] + for code in codes: + try: + found.append(whole.logic(code)) + except combo_store.cm.mf.FormulaError: + missing.append(code) + return found, missing + + +def combo_logics(ua: str | None) -> tuple[dict[str, Any] | None, list[dict], list[str]]: + """(조합 줄, [로직 줄 …], 빠진 로직 키) — 조합이 없거나 못 찾으면 (None, [], []).""" + if not ua: + return None, [], [] + row = combos.find(str(ua)) # 마스터 UA · 회사 UC · 개인 UP(27-2c) + if row is None: + return None, [], [] + found, missing = _logics(combo_store.mcb.logic_keys(row)) + return row, found, missing + + +def _listed(logics: list[dict]) -> list[dict[str, Any]]: + """로직 줄마다 `{로직, 이름, 결과단위, 입력: [{이름, 설명, 단위, 고르기?, 범위?, 자동값}]}`.""" + whole = m01.loaded()[1] + out = [] + for logic in logics: + auto = m01.mau.values(whole, logic) + specs = [] + for spec in logic.get("입력") or []: + one = {k: spec[k] for k in _SPEC if k in spec} + one.setdefault("설명", None) + one.setdefault("단위", None) + specs.append({**one, "자동값": auto.get(spec["이름"])}) + out.append( + { + "로직": logic.get("키"), + "이름": logic.get("이름"), + "결과단위": logic.get("결과단위"), + "입력": specs, + } + ) + return out + + +def logic_inputs(codes: list[str]) -> dict[str, Any]: + """일위대가 입력 모달 — 로직 키들의 입력 목록 `{로직: [...], 빠진로직}`(키 차례 · 겹침 한 번)""" + found, missing = _logics(list(dict.fromkeys(c for c in codes if c))) + return _num({"로직": _listed(found), "빠진로직": missing}) + + +def inputs_of(ua: str | None) -> dict[str, Any]: + """옛 선택 입력 목록 — `{일위대가, 이름, 로직: [...], 빠진로직}` · 일위대가 없으면 로직 없음.""" + row, logics, missing = combo_logics(ua) + return _num( + { + "일위대가": ua or None, + "이름": row.get("이름") if row else None, + "로직": _listed(logics), + "빠진로직": missing, + } + ) diff --git a/M02_MasterTemplete/M02_Labor_Values.css b/M02_MasterTemplete/M02_Labor_Values.css new file mode 100644 index 000000000..dca291790 --- /dev/null +++ b/M02_MasterTemplete/M02_Labor_Values.css @@ -0,0 +1,99 @@ +/* 넣을 값 칸(39-3 · 옛 선택 입력 CSS 에서 옮김) — 프로젝트 값 카드 · 일위대가 입력 모달이 같이 씀. 색은 테마 토큰. */ + +/* ── 프로젝트에서 값 받기(PLAN 25-2d) — 값 · 확인 표시 카드 ── */ +.m02-inputval__wrap { + display: flex; + flex-direction: column; + flex: 1 1 0; + min-height: var(--m02-struct-body-min, 16rem); + min-width: 0; + gap: var(--spacing-8); +} + +.m02-inputval { + display: flex; + flex-direction: column; + gap: var(--spacing-12); + overflow: auto; + min-height: 0; + min-width: 0; +} + +.m02-inputval__places, +.m02-inputval__missing { + margin: 0; + color: var(--color-text-secondary); + font-size: var(--text-caption); +} + +.m02-inputval__logic { + display: flex; + flex-direction: column; + gap: var(--spacing-4); + min-width: 0; +} + +.m02-inputval__logic-name { + margin: 0; + color: var(--color-text); + font-size: var(--text-caption); + overflow-wrap: anywhere; +} + +.m02-inputval__row { + display: grid; + grid-template-columns: minmax(6rem, 2fr) minmax(8rem, 3fr); + gap: var(--spacing-8); + align-items: center; + min-width: 0; +} + +.m02-inputval__name { + color: var(--color-text); + font-size: var(--text-caption); + overflow-wrap: anywhere; +} + +.m02-inputval__cell { + display: flex; + flex-wrap: wrap; + gap: var(--spacing-4) var(--spacing-8); + align-items: center; + min-width: 0; +} + +.m02-inputval__control { + flex: 1 1 6rem; + min-width: 0; + max-width: 24rem; +} + +/* 자동값 = 저장값이 아님 — 기울임 + 보조 글자색(글자 대비 4.5 이상 유지) */ +.m02-inputval__value.is-auto, +.m02-inputval__control.is-auto, +.m02-inputval__control::placeholder { + color: var(--color-text-secondary); + font-style: italic; + opacity: 1; +} + +.m02-inputval__state { + color: var(--color-text-secondary); + font-size: var(--text-caption); + white-space: nowrap; +} + +.m02-inputval__state.is-confirmed { + color: var(--color-text); + font-weight: 600; +} + +.m02-inputval__error { + flex: 1 0 100%; + color: var(--color-danger); + font-size: var(--text-caption); +} + +.m02-inputval__control[aria-invalid="true"] { + border-color: var(--color-danger); +} diff --git a/M02_MasterTemplete/M02_Labor_Values.ts b/M02_MasterTemplete/M02_Labor_Values.ts new file mode 100644 index 000000000..a7453b93a --- /dev/null +++ b/M02_MasterTemplete/M02_Labor_Values.ts @@ -0,0 +1,316 @@ +/* ============================================================================= + * M02_Labor_Values.ts + * 넣을 값 칸(39-3 · 옛 선택 입력 `M02_MasterTemplete_Inputs.ts` 에서 옮김) — 값 칸 규칙 한 벌: + * 고르기 = 목록 · 범위 = 숫자(밖이면 붉은 글) · 빈 칸 = 자동값(흐리게 · placeholder). + * · `createValueControl` — 칸 하나(일위대가 입력 모달 · 프로젝트 값 카드가 같이 씀) + * · `createInputsValueCard` — 프로젝트 고르기 모달 값 카드(옛 자리 `M02_MasterTemplete_Inputs.ts` 가 다시 내보냄) + * ========================================================================== */ + +import { createButton, el } from "@ui/ui_template_elements"; +import type { ItemInput, ItemInputs, ItemLogic, InputLinks } from "./M02_MasterTemplete_Api_Fetch"; +import { cardNote, cardShell } from "./M02_MasterTemplete_CardShell"; +import { inputKey, pickedGroups, seatsOf } from "./M02_MasterTemplete_Inputs_Model"; +import { choicesOf, isOutside, rangeOf, storedOf, type ValueDoc } from "./M02_Labor_Values_Model"; +import "./M02_Labor_Values.css"; + +const fill = (text: string, values: Record): string => + text.replace(/\{(\w+)\}/g, (_, key: string) => String(values[key] ?? "")); + +const CONTROL_TEXT = { auto: "자동값", outOfRange: "범위 {value} 밖" }; + +const autoValue = (input: ItemInput): string | number | null => { + const auto = input.자동값; + return typeof auto === "string" || typeof auto === "number" ? auto : null; +}; + +export interface ValueControl { + control: HTMLInputElement | HTMLSelectElement; + /** 범위 밖 글(빈 글이면 비움) */ + error: HTMLElement; + /** 지금 범위 밖인가 — 부르는 쪽이 셈(값 카드 「막힌 칸 수」) */ + wasBad: boolean; +} + +/** 값 칸 하나 — `value` = 저장 값(null = 자동값) · 바뀔 때마다 `onChange(저장 값, 범위 밖)` */ +export function createValueControl( + input: ItemInput, + value: string | number | null, + onChange: (value: string | number | null, bad: boolean) => void, +): ValueControl { + const auto = autoValue(input); + const range = rangeOf(input); + const choices = choicesOf(input); + const control = ( + choices.length + ? el("select", { className: "ui-select ui-select--sm m02-inputval__control" }) + : el("input", { + className: "ui-input m02-inputval__control", + attrs: { + type: range ? "number" : "text", + ...(range?.min !== undefined ? { min: String(range.min) } : {}), + ...(range?.max !== undefined ? { max: String(range.max) } : {}), + ...(range ? { step: "any" } : {}), + placeholder: auto === null ? "" : String(auto), + }, + }) + ) as HTMLInputElement | HTMLSelectElement; + if (control instanceof HTMLSelectElement) { + control.append( + el("option", { + attrs: { value: "" }, + text: auto === null ? "—" : `${CONTROL_TEXT.auto} ${String(auto)}`, + }), + ...choices.map((c) => el("option", { attrs: { value: c.값 }, text: c.이름 })), + ); + } + const empty = value === null || value === ""; + control.value = empty ? "" : String(value); + control.classList.toggle("is-auto", empty); + const error = el("span", { className: "m02-inputval__error" }); + const out: ValueControl = { control, error, wasBad: false }; + control.addEventListener(control instanceof HTMLSelectElement ? "change" : "input", () => { + const raw = control.value; + const bad = isOutside(input, raw); + control.setAttribute("aria-invalid", bad ? "true" : "false"); + error.textContent = bad + ? fill(CONTROL_TEXT.outOfRange, { value: `${range?.min ?? ""}~${range?.max ?? ""}` }) + : ""; + control.classList.toggle("is-auto", raw === ""); + onChange(storedOf(input, raw), bad); + }); + return out; +} + +/* ── 프로젝트 복사본에서 값 받기(PLAN 25-2d) — 고르기 모달 카드 ───────────────────────────── + * 같은 카드 틀 · 같은 입력 목록인데 여기서는 자리 고르기 대신 **값**을 받음: 자리가 「선택」 인 입력만 + * 로직마다 묶어, 고르기 = 목록 · 범위 = 숫자 · 자동값은 흐리게 · 「확인함」 표시. 자동값은 저장값이 아님 + * (확인 안 한 입력은 계산 때 「빠진 입력」). 값은 로직마다 따로 — 이름이 같아도 합치지 않음. + * mode "view" 보기만 + [선택 입력 수정] · mode "value" 값 받기(범위 밖은 저장 막음) */ + +export interface InputsValueData { + list: ItemInputs; + link: InputLinks | null; + values: ValueDoc; +} + +export interface InputsValueOptions { + mode: "view" | "value"; + /** mode "view" — [선택 입력 수정] */ + onEdit?: () => void; + /** mode "value" — 값이 바뀔 때마다 */ + onChange?: (values: ValueDoc) => void; +} + +export interface InputsValueHandle { + root: HTMLElement; + /** 자료를 바꿔 끼움 — `null` 은 못 읽은 경우(까닭 글 `note`) */ + set: (data: InputsValueData | null, note?: string, retry?: () => void) => void; + /** 목록을 받는 동안 0.3초 넘으면 「불러오는 중」(31-7) — 돌려받은 함수 = 끝 */ + loading: () => () => void; + /** 지금 값(확인한 것 · 값이 든 것만) */ + getValues: () => ValueDoc; + /** 범위 밖 값이 없나 */ + isValid: () => boolean; + /** [선택 입력 수정] 글 — 아직 안 가져온 항목이면 「프로젝트로 가져와 고침」 */ + setEditLabel: (label: string) => void; +} + +const VALUE_TEXT = { + title: "선택 입력", + missing: "빠진 로직 {value}", + edit: "선택 입력 수정", + none: "선택으로 이은 입력이 없음", + auto: "자동값", + unconfirmed: "확인 안 함", + confirmed: "✓ 확인함", + confirmBox: "확인", + summary: "선택 {value}", + places: "면적산출 알약 {area} · 재료산출 알약 {material}", + noList: "입력 목록을 못 읽음 — {value}", +}; + +type ValueEntry = ValueDoc[string][string]; + +export function createInputsValueCard(opts: InputsValueOptions): InputsValueHandle { + let data: InputsValueData | null = null; + let values: ValueDoc = {}; + let invalid = 0; + + const summary = el("span", { className: "m02-struct__no" }); + const edit = createButton({ label: VALUE_TEXT.edit, variant: "ghost", size: "sm" }); + edit.hidden = opts.mode !== "view"; + if (opts.onEdit) edit.addEventListener("click", opts.onEdit); + const places = el("p", { className: "m02-inputval__places" }); + const body = el("div", { className: "m02-struct__sheet m02-inputval" }); + // 공용 카드 틀(31-7) — 부제 = 확인 n/m · 제목 줄 오른쪽 = [선택 입력 수정] · 상태 글은 목록 칸에 + const shell = cardShell({ + title: VALUE_TEXT.title, + sub: [summary], + body: el("div", { className: "m02-inputval__wrap", children: [places, body] }), + stateHost: body, + }); + shell.tools.append(edit); + const root = shell.root; + + const groups = (): ReturnType => + data ? pickedGroups(data.list, data.link) : []; + const one = (logic: string, name: string): ValueEntry => + values[logic]?.[name] ?? { 값: null, 확인: false }; + const isEmpty = (value: string | number | null): boolean => value === null || value === ""; + const put = (logic: string, name: string, next: ValueEntry): void => { + (values[logic] ??= {})[name] = next; + }; + const autoOf = autoValue; + + const paintSummary = (): void => { + let total = 0; + let confirmed = 0; + for (const { logic, inputs } of groups()) + for (const input of inputs) { + total += 1; + if (one(logic.로직, input.이름).확인) confirmed += 1; + } + summary.textContent = data ? fill(VALUE_TEXT.summary, { value: `${confirmed}/${total}` }) : ""; + }; + + /** 값 칸 하나 — 자동값은 흐리게(기울임 · placeholder · 첫 줄) */ + const valueCell = (logic: ItemLogic, input: ItemInput): HTMLElement => { + const stored = one(logic.로직, input.이름); + const auto = autoOf(input); + const state = el("span", { className: "m02-inputval__state" }); + const paintState = (): void => { + const now = one(logic.로직, input.이름); + state.textContent = now.확인 ? VALUE_TEXT.confirmed : VALUE_TEXT.unconfirmed; + state.classList.toggle("is-confirmed", now.확인); + }; + if (opts.mode === "view") { + const text = isEmpty(stored.값) ? auto : stored.값; + const shown = el("span", { + className: `m02-inputval__value${isEmpty(stored.값) ? " is-auto" : ""}`, + text: text === null ? "—" : String(text), + attrs: isEmpty(stored.값) ? { title: VALUE_TEXT.auto } : {}, + }); + paintState(); + return el("div", { className: "m02-inputval__cell", children: [shown, state] }); + } + + const emit = (): void => { + paintState(); + paintSummary(); + opts.onChange?.(values); + }; + const range = rangeOf(input); + const field = createValueControl(input, stored.값, (value, bad) => { + if (bad !== field.wasBad) invalid += bad ? 1 : -1; + field.wasBad = bad; + // 값을 넣으면 확인 · 비우면 확인 풀림(자동값은 저장값이 아님) + put(logic.로직, input.이름, { 값: bad ? null : value, 확인: value !== null && !bad }); + box.checked = value !== null && !bad; + emit(); + }); + const { control, error } = field; + const box = el("input", { + attrs: { + type: "checkbox", + title: VALUE_TEXT.confirmBox, + "aria-label": VALUE_TEXT.confirmBox, + }, + }) as HTMLInputElement; + box.checked = stored.확인; + // 확인 상자 — 비운 채 켜면 자동값을 그대로 받아들임 · 끄면 확인만 풀림 + box.addEventListener("change", () => { + const now = one(logic.로직, input.이름); + if (box.checked && isEmpty(now.값) && auto !== null) { + control.value = String(auto); + control.classList.remove("is-auto"); + put(logic.로직, input.이름, { 값: range ? Number(auto) : auto, 확인: true }); + } else if (box.checked && isEmpty(now.값)) { + box.checked = false; // 받아들일 값이 없음 + } else { + put(logic.로직, input.이름, { ...now, 확인: box.checked }); + } + emit(); + }); + paintState(); + return el("div", { className: "m02-inputval__cell", children: [control, box, state, error] }); + }; + + const paint = (note?: string, retry?: () => void): void => { + invalid = 0; + paintSummary(); + if (!data) { + places.textContent = ""; + if (note && retry) return shell.fail(fill(VALUE_TEXT.noList, { value: note }), retry); + return shell.note(note ? fill(VALUE_TEXT.noList, { value: note }) : VALUE_TEXT.none); + } + const seats = [...seatsOf(data.link).values()]; + const area = seats.filter((seat) => seat === "면적").length; + const material = seats.filter((seat) => seat === "재료").length; + places.textContent = area + material ? fill(VALUE_TEXT.places, { area, material }) : ""; + const shown = groups(); + const missing = data.list.빠진로직.length + ? [ + el("p", { + className: "m02-inputval__missing", + text: fill(VALUE_TEXT.missing, { value: data.list.빠진로직.join(", ") }), + }), + ] + : []; + if (!shown.length) { + body.replaceChildren(cardNote(VALUE_TEXT.none), ...missing); + return; + } + body.replaceChildren( + ...shown.map(({ logic, inputs }) => + el("section", { + className: "m02-inputval__logic", + attrs: { "data-logic": logic.로직 }, + children: [ + el("h4", { + className: "m02-inputval__logic-name", + text: logic.이름 || logic.로직, + attrs: { title: `${logic.로직}${logic.결과단위 ? ` · ${logic.결과단위}` : ""}` }, + }), + ...inputs.map((input) => + el("div", { + className: "m02-inputval__row", + attrs: { "data-input": inputKey(logic.로직, input.이름) }, + children: [ + el("span", { + className: "m02-inputval__name", + text: input.단위 ? `${input.이름} (${input.단위})` : input.이름, + attrs: input.설명 ? { title: input.설명 } : {}, + }), + valueCell(logic, input), + ], + }), + ), + ], + }), + ), + ...missing, + ); + }; + + return { + root, + set: (next, note, retry) => { + data = next; + values = next ? structuredClone(next.values) : {}; + paint(note, retry); + }, + loading: () => shell.loading(), + getValues: () => { + // 확인한 것 · 값이 든 것만 — 입력 목록에서 없어진 입력은 그대로 둠(다음 가져오기가 정함) + const out: ValueDoc = {}; + for (const [logic, byName] of Object.entries(values)) + for (const [name, entry] of Object.entries(byName)) + if (entry.확인 || !isEmpty(entry.값)) (out[logic] ??= {})[name] = entry; + return out; + }, + isValid: () => invalid === 0, + setEditLabel: (label) => { + (edit.querySelector(".ui-btn__label") ?? edit).textContent = label; + }, + }; +} diff --git a/M02_MasterTemplete/M02_Labor_Values_Model.ts b/M02_MasterTemplete/M02_Labor_Values_Model.ts new file mode 100644 index 000000000..046ee7cc8 --- /dev/null +++ b/M02_MasterTemplete/M02_Labor_Values_Model.ts @@ -0,0 +1,116 @@ +/* ============================================================================= + * M02_Labor_Values_Model.ts + * 넣을 값 셈(화면 없음 · 39-3) — 옛 선택 입력 `M02_MasterTemplete_Inputs_Model.ts` 의 값 함수를 옮김. + * 일위대가 입력 모달 · 프로젝트 고르기 모달 값 카드가 같은 값 칸 규칙(고르기 = 목록 · 범위 = 숫자 · + * 빈 값 = 자동값)을 씀. 일위대가 입력값 = `{입력 이름: 값}`(빈 값은 안 둠 = 자동값 · 모르는 이름 버림). + * ========================================================================== */ + +import type { ItemInput } from "./M02_MasterTemplete_Api_Fetch"; + +const text = (value: unknown): string => + value === null || value === undefined + ? "" + : typeof value === "object" + ? JSON.stringify(value) + : String(value); + +/** 고르기 · 범위 표시 — 선택지 목록이면 「a / b / c」 · 범위(배열 둘 · {최소, 최대})면 「최소 ~ 최대」 */ +export function valueHint(input: ItemInput): string { + if (Array.isArray(input.고르기) && input.고르기.length) return input.고르기.map(text).join(" / "); + const range = input.범위; + if (Array.isArray(range) && range.length >= 2) return `${text(range[0])} ~ ${text(range[1])}`; + if (range && typeof range === "object") { + const r = range as Record; + const lo = r.최소 ?? r.min; + const hi = r.최대 ?? r.max; + if (lo !== undefined || hi !== undefined) return `${text(lo)} ~ ${text(hi)}`; + } + return text(range); +} + +/** 자동값 글 — 없으면 빈 글 */ +export const autoText = (input: ItemInput): string => text(input.자동값); + +/** `입력값.json` — `{로직키: {입력이름: {값, 확인}}}` (서버 검사 모양 · 값은 수 · 글 · 빈 값) */ +export type ValueDoc = Record< + string, + Record +>; + +const number = (value: unknown): number | undefined => + typeof value === "number" && Number.isFinite(value) ? value : undefined; + +/** 범위 모양 둘(`[최소, 최대]` · `{최소, 최대}`)을 하나로 — 범위가 아니면 null */ +export function rangeOf(input: ItemInput): { min?: number; max?: number } | null { + const raw = input.범위; + if (Array.isArray(raw)) return { min: number(raw[0]), max: number(raw[1]) }; + if (raw && typeof raw === "object") { + const r = raw as Record; + return { min: number(r.최소 ?? r.min), max: number(r.최대 ?? r.max) }; + } + return null; +} + +/** 고르기 목록 → 화면 값 · 이름 짝 — `raw` 는 원래 모양(숫자 목록이면 숫자로 저장) */ +export function choicesOf(input: ItemInput): { 값: string; 이름: string; raw: string | number }[] { + return (input.고르기 ?? []).map((one) => { + const object = + typeof one === "object" && one !== null ? (one as Record) : null; + const raw = (object ? (object.값 ?? object.이름 ?? "") : one) as string | number; + return { 값: String(raw), 이름: object ? String(object.이름 ?? raw) : String(one), raw }; + }); +} + +/** 범위 밖 글인가 — 빈 글 · 범위 없는 입력은 아님 */ +export function isOutside(input: ItemInput, raw: string): boolean { + const range = rangeOf(input); + if (!range || raw.trim() === "") return false; + const value = Number(raw); + return ( + !Number.isFinite(value) || + (range.min !== undefined && value < range.min) || + (range.max !== undefined && value > range.max) + ); +} + +/** 칸 글 → 저장 값 — 빈 글 = null(자동값) · 범위 = 수 · 고르기 = 원래 모양 · 그 밖 = 글 */ +export function storedOf(input: ItemInput, raw: string): string | number | null { + if (raw === "") return null; + if (rangeOf(input)) return Number(raw); + return choicesOf(input).find((c) => c.값 === raw)?.raw ?? raw; +} + +/* ── 일위대가 입력값(39-3) — 로직 한 줄의 `{입력 이름: 값}` ───────────────────────────── */ + +export type LaborValues = Record; + +/** 로직 입력 목록에 있는 이름 · 빈 값 아닌 것만 남김 + 사라진 이름(로직 입력이 바뀌어 버려질 값) */ +export function keepKnown( + inputs: ItemInput[], + values: LaborValues | null | undefined, +): { kept: LaborValues; lost: string[] } { + const names = new Set(inputs.map((i) => i.이름)); + const kept: LaborValues = {}; + const lost: string[] = []; + for (const [name, value] of Object.entries(values ?? {})) { + if (!names.has(name)) lost.push(name); + else if (value !== "" && value !== null && value !== undefined) kept[name] = value; + } + return { kept, lost }; +} + +/** 줄 아래 작은 글 — 「지역 서울 · 규격 25mm」(넣은 값만 · 입력 차례) + 빈 값 수(자동값도 없는 입력) */ +export function valueLine( + inputs: ItemInput[], + values: LaborValues | null | undefined, +): { text: string; empty: number } { + const parts: string[] = []; + let empty = 0; + for (const input of inputs) { + const value = values?.[input.이름]; + if (value !== undefined && value !== "") { + parts.push(`${input.이름} ${value}${input.단위 ? input.단위 : ""}`); + } else if (autoText(input) === "") empty += 1; + } + return { text: parts.join(" · "), empty }; +} diff --git a/M02_MasterTemplete/M02_MasterTemplete_Inputs.css b/M02_MasterTemplete/M02_MasterTemplete_Inputs.css index d90721bea..fff107c95 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Inputs.css +++ b/M02_MasterTemplete/M02_MasterTemplete_Inputs.css @@ -103,101 +103,3 @@ .m02-inputs__seat:disabled { opacity: 0.7; } - -/* ── 프로젝트에서 값 받기(PLAN 25-2d) — 값 · 확인 표시 카드 ── */ -.m02-inputval__wrap { - display: flex; - flex-direction: column; - flex: 1 1 0; - min-height: var(--m02-struct-body-min, 16rem); - min-width: 0; - gap: var(--spacing-8); -} - -.m02-inputval { - display: flex; - flex-direction: column; - gap: var(--spacing-12); - overflow: auto; - min-height: 0; - min-width: 0; -} - -.m02-inputval__places, -.m02-inputval__missing { - margin: 0; - color: var(--color-text-secondary); - font-size: var(--text-caption); -} - -.m02-inputval__logic { - display: flex; - flex-direction: column; - gap: var(--spacing-4); - min-width: 0; -} - -.m02-inputval__logic-name { - margin: 0; - color: var(--color-text); - font-size: var(--text-caption); - overflow-wrap: anywhere; -} - -.m02-inputval__row { - display: grid; - grid-template-columns: minmax(6rem, 2fr) minmax(8rem, 3fr); - gap: var(--spacing-8); - align-items: center; - min-width: 0; -} - -.m02-inputval__name { - color: var(--color-text); - font-size: var(--text-caption); - overflow-wrap: anywhere; -} - -.m02-inputval__cell { - display: flex; - flex-wrap: wrap; - gap: var(--spacing-4) var(--spacing-8); - align-items: center; - min-width: 0; -} - -.m02-inputval__control { - flex: 1 1 6rem; - min-width: 0; - max-width: 24rem; -} - -/* 자동값 = 저장값이 아님 — 기울임 + 보조 글자색(글자 대비 4.5 이상 유지) */ -.m02-inputval__value.is-auto, -.m02-inputval__control.is-auto, -.m02-inputval__control::placeholder { - color: var(--color-text-secondary); - font-style: italic; - opacity: 1; -} - -.m02-inputval__state { - color: var(--color-text-secondary); - font-size: var(--text-caption); - white-space: nowrap; -} - -.m02-inputval__state.is-confirmed { - color: var(--color-text); - font-weight: 600; -} - -.m02-inputval__error { - flex: 1 0 100%; - color: var(--color-danger); - font-size: var(--text-caption); -} - -.m02-inputval__control[aria-invalid="true"] { - border-color: var(--color-danger); -} diff --git a/M02_MasterTemplete/M02_MasterTemplete_Inputs.py b/M02_MasterTemplete/M02_MasterTemplete_Inputs.py index 91bc32d87..d6c130984 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Inputs.py +++ b/M02_MasterTemplete/M02_MasterTemplete_Inputs.py @@ -14,30 +14,18 @@ M01 은 읽기만(`M01_MasterData_Store` · `_Store_Combo` · `master_auto`) · from __future__ import annotations import json -from decimal import Decimal from pathlib import Path from typing import Any from common_util.common_util_json import atomic_write_json -from M01_MasterData import M01_MasterData_Store as m01 from M01_MasterData import M01_MasterData_Store_Combo as combo_store +from M02_MasterTemplete import M02_Labor_Inputs as labor_inputs from M02_MasterTemplete import M02_MasterTemplete_Combo_Store as combos from M02_MasterTemplete import M02_MasterTemplete_Store as store +from M02_MasterTemplete.M02_Labor_Inputs import _num, combo_logics PLACES = ("면적", "재료", "선택", None) PILL_HEAD = "입력" -_SPEC = ("이름", "설명", "단위", "고르기", "범위") - - -def _num(value: Any) -> Any: - """Decimal → JSON 수(M01 길과 같은 규칙) · 목록 · 묶음 안까지.""" - if isinstance(value, Decimal): - return int(value) if value == value.to_integral_value() else float(value) - if isinstance(value, list): - return [_num(v) for v in value] - if isinstance(value, dict): - return {k: _num(v) for k, v in value.items()} - return value def pill_key(logic: str, name: str) -> str: @@ -56,55 +44,10 @@ def _combo_row(ua: str) -> dict[str, Any] | None: return combos.find(ua) # 마스터 UA · 회사 UC · 개인 UP(27-2c) -def combo_logics(ua: str | None) -> tuple[dict[str, Any] | None, list[dict], list[str]]: - """(조합 줄, [로직 줄 …], 빠진 로직 키) — 조합이 없거나 못 찾으면 (None, [], []).""" - if not ua: - return None, [], [] - row = _combo_row(str(ua)) - if row is None: - return None, [], [] - whole = m01.loaded()[1] - found, missing = [], [] - for one in combo_store.mcb.items(row): - code = str(one.get("로직") or "") if isinstance(one, dict) else "" - try: - found.append(whole.logic(code)) - except combo_store.cm.mf.FormulaError: - missing.append(code) - return row, found, missing - - def inputs(key: str) -> dict[str, Any]: """25-1b 입력 목록 — `{일위대가, 이름, 로직: [{로직, 이름, 결과단위, 입력: [...]}], 빠진로직}`. - 일위대가 없는 항목은 로직 빈 목록.""" - ua = _item(key).get("일위대가") - row, logics, missing = combo_logics(ua) - whole = m01.loaded()[1] - out = [] - for logic in logics: - auto = m01.mau.values(whole, logic) - specs = [] - for spec in logic.get("입력") or []: - one = {k: spec[k] for k in _SPEC if k in spec} - one.setdefault("설명", None) - one.setdefault("단위", None) - specs.append({**one, "자동값": auto.get(spec["이름"])}) - out.append( - { - "로직": logic.get("키"), - "이름": logic.get("이름"), - "결과단위": logic.get("결과단위"), - "입력": specs, - } - ) - return _num( - { - "일위대가": ua or None, - "이름": row.get("이름") if row else None, - "로직": out, - "빠진로직": missing, - } - ) + 일위대가 없는 항목은 로직 빈 목록 · 몸은 `M02_Labor_Inputs`(일위대가 입력 모달과 한 벌).""" + return labor_inputs.inputs_of(_item(key).get("일위대가")) # ── 항목 연결 파일 ───────────────────────────────────── diff --git a/M02_MasterTemplete/M02_MasterTemplete_Inputs.ts b/M02_MasterTemplete/M02_MasterTemplete_Inputs.ts index 8b31ae48f..2b9b8a143 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Inputs.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Inputs.ts @@ -10,7 +10,7 @@ * · 면적 · 재료로 고른 입력은 그 시트 편집 화면 알약 줄에 뜸(`inputPills` → `pillsFor`). * ========================================================================== */ -import { createButton, el } from "@ui/ui_template_elements"; +import { el } from "@ui/ui_template_elements"; import type { LinkItem } from "../A00_Common/spreadsheet/spreadsheet_links"; import { fetchItemInputs, @@ -18,28 +18,21 @@ import { getActiveLayer, readInputLinks, saveInputLinks, - type InputLinks, type InputSeat, - type ItemInput, type ItemInputs, - type ItemLogic, type ItemRow, type Layer, } from "./M02_MasterTemplete_Api_Fetch"; import { autoText, - choicesOf, countOf, inputKey, linesOf, - pickedGroups, pillsOf, - rangeOf, seatsOf, SEAT_OPTIONS, valueHint, type SeatMap, - type ValueDoc, } from "./M02_MasterTemplete_Inputs_Model"; import { cardNote, cardShell } from "./M02_MasterTemplete_CardShell"; import "./M02_MasterTemplete_Inputs.css"; @@ -288,286 +281,10 @@ export async function inputPills(column: string, seat: "면적" | "재료"): Pro } } -/* ── 프로젝트 복사본에서 값 받기(PLAN 25-2d) — 고르기 모달 카드 ───────────────────────────── - * 같은 카드 틀 · 같은 입력 목록인데 여기서는 자리 고르기 대신 **값**을 받음: 자리가 「선택」 인 입력만 - * 로직마다 묶어, 고르기 = 목록 · 범위 = 숫자 · 자동값은 흐리게 · 「확인함」 표시. 자동값은 저장값이 아님 - * (확인 안 한 입력은 계산 때 「빠진 입력」). 값은 로직마다 따로 — 이름이 같아도 합치지 않음. - * mode "view" 보기만 + [선택 입력 수정] · mode "value" 값 받기(범위 밖은 저장 막음) */ - -export interface InputsValueData { - list: ItemInputs; - link: InputLinks | null; - values: ValueDoc; -} - -export interface InputsValueOptions { - mode: "view" | "value"; - /** mode "view" — [선택 입력 수정] */ - onEdit?: () => void; - /** mode "value" — 값이 바뀔 때마다 */ - onChange?: (values: ValueDoc) => void; -} - -export interface InputsValueHandle { - root: HTMLElement; - /** 자료를 바꿔 끼움 — `null` 은 못 읽은 경우(까닭 글 `note`) */ - set: (data: InputsValueData | null, note?: string, retry?: () => void) => void; - /** 목록을 받는 동안 0.3초 넘으면 「불러오는 중」(31-7) — 돌려받은 함수 = 끝 */ - loading: () => () => void; - /** 지금 값(확인한 것 · 값이 든 것만) */ - getValues: () => ValueDoc; - /** 범위 밖 값이 없나 */ - isValid: () => boolean; - /** [선택 입력 수정] 글 — 아직 안 가져온 항목이면 「프로젝트로 가져와 고침」 */ - setEditLabel: (label: string) => void; -} - -const VALUE_TEXT = { - edit: "선택 입력 수정", - none: "선택으로 이은 입력이 없음", - auto: "자동값", - unconfirmed: "확인 안 함", - confirmed: "✓ 확인함", - confirmBox: "확인", - outOfRange: "범위 {value} 밖", - summary: "선택 {value}", - places: "면적산출 알약 {area} · 재료산출 알약 {material}", - noList: "입력 목록을 못 읽음 — {value}", -}; - -type ValueEntry = ValueDoc[string][string]; - -export function createInputsValueCard(opts: InputsValueOptions): InputsValueHandle { - let data: InputsValueData | null = null; - let values: ValueDoc = {}; - let invalid = 0; - - const summary = el("span", { className: "m02-struct__no" }); - const edit = createButton({ label: VALUE_TEXT.edit, variant: "ghost", size: "sm" }); - edit.hidden = opts.mode !== "view"; - if (opts.onEdit) edit.addEventListener("click", opts.onEdit); - const places = el("p", { className: "m02-inputval__places" }); - const body = el("div", { className: "m02-struct__sheet m02-inputval" }); - // 공용 카드 틀(31-7) — 부제 = 확인 n/m · 제목 줄 오른쪽 = [선택 입력 수정] · 상태 글은 목록 칸에 - const shell = cardShell({ - title: TEXT.title, - sub: [summary], - body: el("div", { className: "m02-inputval__wrap", children: [places, body] }), - stateHost: body, - }); - shell.tools.append(edit); - const root = shell.root; - - const groups = (): ReturnType => - data ? pickedGroups(data.list, data.link) : []; - const one = (logic: string, name: string): ValueEntry => - values[logic]?.[name] ?? { 값: null, 확인: false }; - const isEmpty = (value: string | number | null): boolean => value === null || value === ""; - const put = (logic: string, name: string, next: ValueEntry): void => { - (values[logic] ??= {})[name] = next; - }; - const autoOf = (input: ItemInput): string | number | null => { - const auto = input.자동값; - return typeof auto === "string" || typeof auto === "number" ? auto : null; - }; - - const paintSummary = (): void => { - let total = 0; - let confirmed = 0; - for (const { logic, inputs } of groups()) - for (const input of inputs) { - total += 1; - if (one(logic.로직, input.이름).확인) confirmed += 1; - } - summary.textContent = data ? fill(VALUE_TEXT.summary, { value: `${confirmed}/${total}` }) : ""; - }; - - /** 값 칸 하나 — 자동값은 흐리게(기울임 · placeholder · 첫 줄) */ - const valueCell = (logic: ItemLogic, input: ItemInput): HTMLElement => { - const stored = one(logic.로직, input.이름); - const auto = autoOf(input); - const state = el("span", { className: "m02-inputval__state" }); - const paintState = (): void => { - const now = one(logic.로직, input.이름); - state.textContent = now.확인 ? VALUE_TEXT.confirmed : VALUE_TEXT.unconfirmed; - state.classList.toggle("is-confirmed", now.확인); - }; - if (opts.mode === "view") { - const text = isEmpty(stored.값) ? auto : stored.값; - const shown = el("span", { - className: `m02-inputval__value${isEmpty(stored.값) ? " is-auto" : ""}`, - text: text === null ? "—" : String(text), - attrs: isEmpty(stored.값) ? { title: VALUE_TEXT.auto } : {}, - }); - paintState(); - return el("div", { className: "m02-inputval__cell", children: [shown, state] }); - } - - const emit = (): void => { - paintState(); - paintSummary(); - opts.onChange?.(values); - }; - const range = rangeOf(input); - const choices = choicesOf(input); - const control = ( - choices.length - ? el("select", { className: "ui-select ui-select--sm m02-inputval__control" }) - : el("input", { - className: "ui-input m02-inputval__control", - attrs: { - type: range ? "number" : "text", - ...(range?.min !== undefined ? { min: String(range.min) } : {}), - ...(range?.max !== undefined ? { max: String(range.max) } : {}), - ...(range ? { step: "any" } : {}), - placeholder: auto === null ? "" : String(auto), - }, - }) - ) as HTMLInputElement | HTMLSelectElement; - const error = el("span", { className: "m02-inputval__error" }); - const box = el("input", { - attrs: { - type: "checkbox", - title: VALUE_TEXT.confirmBox, - "aria-label": VALUE_TEXT.confirmBox, - }, - }) as HTMLInputElement; - - if (control instanceof HTMLSelectElement) { - control.append( - el("option", { - attrs: { value: "" }, - text: auto === null ? "—" : `${VALUE_TEXT.auto} ${String(auto)}`, - }), - ...choices.map((c) => el("option", { attrs: { value: c.값 }, text: c.이름 })), - ); - } - control.value = isEmpty(stored.값) ? "" : String(stored.값); - control.classList.toggle("is-auto", isEmpty(stored.값)); - box.checked = stored.확인; - - const outside = (raw: string): boolean => { - if (!range || raw.trim() === "") return false; - const value = Number(raw); - return ( - !Number.isFinite(value) || - (range.min !== undefined && value < range.min) || - (range.max !== undefined && value > range.max) - ); - }; - let wasBad = false; - const changed = (): void => { - const raw = control.value; - const bad = outside(raw); - if (bad !== wasBad) invalid += bad ? 1 : -1; - wasBad = bad; - control.setAttribute("aria-invalid", bad ? "true" : "false"); - error.textContent = bad - ? fill(VALUE_TEXT.outOfRange, { value: `${range?.min ?? ""}~${range?.max ?? ""}` }) - : ""; - control.classList.toggle("is-auto", raw === ""); - const value: string | number | null = - raw === "" ? null : range ? Number(raw) : (choices.find((c) => c.값 === raw)?.raw ?? raw); - // 값을 넣으면 확인 · 비우면 확인 풀림(자동값은 저장값이 아님) - put(logic.로직, input.이름, { 값: bad ? null : value, 확인: raw !== "" && !bad }); - box.checked = raw !== "" && !bad; - emit(); - }; - control.addEventListener(control instanceof HTMLSelectElement ? "change" : "input", changed); - // 확인 상자 — 비운 채 켜면 자동값을 그대로 받아들임 · 끄면 확인만 풀림 - box.addEventListener("change", () => { - const now = one(logic.로직, input.이름); - if (box.checked && isEmpty(now.값) && auto !== null) { - control.value = String(auto); - control.classList.remove("is-auto"); - put(logic.로직, input.이름, { 값: range ? Number(auto) : auto, 확인: true }); - } else if (box.checked && isEmpty(now.값)) { - box.checked = false; // 받아들일 값이 없음 - } else { - put(logic.로직, input.이름, { ...now, 확인: box.checked }); - } - emit(); - }); - paintState(); - return el("div", { className: "m02-inputval__cell", children: [control, box, state, error] }); - }; - - const paint = (note?: string, retry?: () => void): void => { - invalid = 0; - paintSummary(); - if (!data) { - places.textContent = ""; - if (note && retry) return shell.fail(fill(VALUE_TEXT.noList, { value: note }), retry); - return shell.note(note ? fill(VALUE_TEXT.noList, { value: note }) : VALUE_TEXT.none); - } - const seats = [...seatsOf(data.link).values()]; - const area = seats.filter((seat) => seat === "면적").length; - const material = seats.filter((seat) => seat === "재료").length; - places.textContent = area + material ? fill(VALUE_TEXT.places, { area, material }) : ""; - const shown = groups(); - const missing = data.list.빠진로직.length - ? [ - el("p", { - className: "m02-inputval__missing", - text: fill(TEXT.missing, { value: data.list.빠진로직.join(", ") }), - }), - ] - : []; - if (!shown.length) { - body.replaceChildren(cardNote(VALUE_TEXT.none), ...missing); - return; - } - body.replaceChildren( - ...shown.map(({ logic, inputs }) => - el("section", { - className: "m02-inputval__logic", - attrs: { "data-logic": logic.로직 }, - children: [ - el("h4", { - className: "m02-inputval__logic-name", - text: logic.이름 || logic.로직, - attrs: { title: `${logic.로직}${logic.결과단위 ? ` · ${logic.결과단위}` : ""}` }, - }), - ...inputs.map((input) => - el("div", { - className: "m02-inputval__row", - attrs: { "data-input": inputKey(logic.로직, input.이름) }, - children: [ - el("span", { - className: "m02-inputval__name", - text: input.단위 ? `${input.이름} (${input.단위})` : input.이름, - attrs: input.설명 ? { title: input.설명 } : {}, - }), - valueCell(logic, input), - ], - }), - ), - ], - }), - ), - ...missing, - ); - }; - - return { - root, - set: (next, note, retry) => { - data = next; - values = next ? structuredClone(next.values) : {}; - paint(note, retry); - }, - loading: () => shell.loading(), - getValues: () => { - // 확인한 것 · 값이 든 것만 — 입력 목록에서 없어진 입력은 그대로 둠(다음 가져오기가 정함) - const out: ValueDoc = {}; - for (const [logic, byName] of Object.entries(values)) - for (const [name, entry] of Object.entries(byName)) - if (entry.확인 || !isEmpty(entry.값)) (out[logic] ??= {})[name] = entry; - return out; - }, - isValid: () => invalid === 0, - setEditLabel: (label) => { - (edit.querySelector(".ui-btn__label") ?? edit).textContent = label; - }, - }; -} +/* ── 프로젝트 복사본에서 값 받기(PLAN 25-2d) — 39-3 에 `M02_Labor_Values.ts` 로 옮김 · 옛 이름 그대로 내보냄 ── */ +export { + createInputsValueCard, + type InputsValueData, + type InputsValueHandle, + type InputsValueOptions, +} from "./M02_Labor_Values"; diff --git a/M02_MasterTemplete/M02_MasterTemplete_Inputs_Model.ts b/M02_MasterTemplete/M02_MasterTemplete_Inputs_Model.ts index a6ab719f2..bbdb8fc53 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Inputs_Model.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Inputs_Model.ts @@ -1,6 +1,7 @@ /* ============================================================================= * M02_MasterTemplete_Inputs_Model.ts - * 「선택 입력」 카드의 셈(화면 없음) — 입력 키 · 알약 키 · 자리 고름 · 줄 만들기 · 알약 목록 · 값 글. + * 「선택 입력」 카드의 셈(화면 없음) — 입력 키 · 알약 키 · 자리 고름 · 줄 만들기 · 알약 목록. + * 값 글 · 값 칸 규칙은 `M02_Labor_Values_Model.ts`(일위대가 입력 모달과 한 벌 · 여기서 다시 내보냄). * 약속 = PLAN 25-1b: 입력 키 `{로직키}|{입력이름}` · 알약 키 `입력|{로직키}|{입력이름}` · * 같은 이름의 입력도 로직마다 따로. 처음은 모두 「안 씀」 — 프로그램이 추측해 나누지 않음. * ========================================================================== */ @@ -78,62 +79,11 @@ export function pillsOf( return out; } -const text = (value: unknown): string => - value === null || value === undefined - ? "" - : typeof value === "object" - ? JSON.stringify(value) - : String(value); - -/** 고르기 · 범위 표시 — 선택지 목록이면 「a / b / c」 · 범위(배열 둘 · {최소, 최대})면 「최소 ~ 최대」 */ -export function valueHint(input: ItemInput): string { - if (Array.isArray(input.고르기) && input.고르기.length) return input.고르기.map(text).join(" / "); - const range = input.범위; - if (Array.isArray(range) && range.length >= 2) return `${text(range[0])} ~ ${text(range[1])}`; - if (range && typeof range === "object") { - const r = range as Record; - const lo = r.최소 ?? r.min; - const hi = r.최대 ?? r.max; - if (lo !== undefined || hi !== undefined) return `${text(lo)} ~ ${text(hi)}`; - } - return text(range); -} - -/** 자동값 글 — 없으면 빈 글 */ -export const autoText = (input: ItemInput): string => text(input.자동값); +/* ── 값 함수 — 일위대가 입력 모달과 한 벌(39-3 · `M02_Labor_Values_Model.ts` 로 옮김 · 옛 이름 그대로 내보냄) ── */ +export { autoText, choicesOf, rangeOf, valueHint, type ValueDoc } from "./M02_Labor_Values_Model"; /* ── 프로젝트에서 값 받기(PLAN 25-2d) — 자리 「선택」 인 입력만 로직마다 묶어 값 · 확인 표시 ── */ -/** `입력값.json` — `{로직키: {입력이름: {값, 확인}}}` (서버 검사 모양 · 값은 수 · 글 · 빈 값) */ -export type ValueDoc = Record< - string, - Record ->; - -const number = (value: unknown): number | undefined => - typeof value === "number" && Number.isFinite(value) ? value : undefined; - -/** 범위 모양 둘(`[최소, 최대]` · `{최소, 최대}`)을 하나로 — 범위가 아니면 null */ -export function rangeOf(input: ItemInput): { min?: number; max?: number } | null { - const raw = input.범위; - if (Array.isArray(raw)) return { min: number(raw[0]), max: number(raw[1]) }; - if (raw && typeof raw === "object") { - const r = raw as Record; - return { min: number(r.최소 ?? r.min), max: number(r.최대 ?? r.max) }; - } - return null; -} - -/** 고르기 목록 → 화면 값 · 이름 짝 — `raw` 는 원래 모양(숫자 목록이면 숫자로 저장) */ -export function choicesOf(input: ItemInput): { 값: string; 이름: string; raw: string | number }[] { - return (input.고르기 ?? []).map((one) => { - const object = - typeof one === "object" && one !== null ? (one as Record) : null; - const raw = (object ? (object.값 ?? object.이름 ?? "") : one) as string | number; - return { 값: String(raw), 이름: object ? String(object.이름 ?? raw) : String(one), raw }; - }); -} - /** 자리가 「선택」 인 입력만 로직 차례로 묶음 — 값 받기 카드가 보이는 입력 */ export function pickedGroups( list: ItemInputs, diff --git a/M02_MasterTemplete/M02_MasterTemplete_Router_Combos.py b/M02_MasterTemplete/M02_MasterTemplete_Router_Combos.py index 71232ea4b..0944f8d6d 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Router_Combos.py +++ b/M02_MasterTemplete/M02_MasterTemplete_Router_Combos.py @@ -18,6 +18,7 @@ from pydantic import BaseModel from common_util.common_util_auth import verify_session from M01_MasterData import M01_MasterData_Store as m01 +from M02_MasterTemplete import M02_Labor_Inputs as labor_inputs from M02_MasterTemplete import M02_Layer_Access as access from M02_MasterTemplete import M02_MasterTemplete_Combo_Copy as combo_copy from M02_MasterTemplete import M02_MasterTemplete_Combo_Store as combos @@ -230,3 +231,9 @@ def get_logic(key: str) -> dict: def get_logic_auto(key: str) -> dict: """견본 입력 — 미리 보기용 자동값.""" return _read(m01.auto, key) + + +@router.get("/logic-lib/inputs", dependencies=[Depends(verify_session)]) +def get_logic_inputs(keys: str = "") -> dict: + """일위대가 입력 모달 — 로직 키들(쉼표)의 입력 목록 + 자동값(`M02_Labor_Inputs`).""" + return _read(labor_inputs.logic_inputs, keys.split(",")) diff --git a/resources/tester/helper_m02_inputs.cjs b/resources/tester/helper_m02_inputs.cjs index 6f014a6a1..1e14e5c27 100644 --- a/resources/tester/helper_m02_inputs.cjs +++ b/resources/tester/helper_m02_inputs.cjs @@ -4,13 +4,18 @@ const fs = require("fs"); const path = require("path"); const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript")); -const file = path.join(__dirname, "..", "..", "M02_MasterTemplete", "M02_MasterTemplete_Inputs_Model.ts"); -const js = ts.transpileModule(fs.readFileSync(file, "utf8"), { - compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 }, -}).outputText; -const box = { exports: {} }; -new Function("exports", "module", "require", js)(box.exports, box, require); -const m = box.exports; +// 상대 경로 TS(값 함수 = `M02_Labor_Values_Model.ts` 다시 내보냄)는 이어 트랜스파일 +function loadTs(file) { + const js = ts.transpileModule(fs.readFileSync(file, "utf8"), { + compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 }, + }).outputText; + const box = { exports: {} }; + const local = (request) => + request.startsWith(".") ? loadTs(`${path.resolve(path.dirname(file), request)}.ts`) : require(request); + new Function("exports", "module", "require", js)(box.exports, box, local); + return box.exports; +} +const m = loadTs(path.join(__dirname, "..", "..", "M02_MasterTemplete", "M02_MasterTemplete_Inputs_Model.ts")); // 같은 이름(높이)의 입력이 두 로직에 있음 const list = { diff --git a/resources/tester/test_item_picker_detail.py b/resources/tester/test_item_picker_detail.py index f5a58dfe4..e0b785b12 100644 --- a/resources/tester/test_item_picker_detail.py +++ b/resources/tester/test_item_picker_detail.py @@ -137,11 +137,14 @@ def test_모달은_M02_와_같은_카드_한_벌을_씀(): def test_선택_입력_카드는_sub2_파일_한_벌(out): - """두 벌 금지 — 모달의 값 받기 카드는 `M02_MasterTemplete_Inputs.ts`(M02 카드와 같은 파일).""" + """두 벌 금지 — 모달의 값 받기 카드는 `M02_Labor_Values.ts`(39-3 · 일위대가 입력 모달과 같은 + 값 칸) · 옛 자리 `M02_MasterTemplete_Inputs.ts` 가 다시 내보냄.""" assert not (M02 / "M02_MasterTemplete_InputsCard.ts").exists() inputs = (M02 / "M02_MasterTemplete_Inputs.ts").read_text(encoding="utf-8") + values = (M02 / "M02_Labor_Values.ts").read_text(encoding="utf-8") assert "export function createInputsCard(" in inputs # M02 자리 고르기(link) - assert "export function createInputsValueCard(" in inputs # 모달 값 받기(view · value) + assert "export function createInputsValueCard(" in values # 모달 값 받기(view · value) + assert "createInputsValueCard," in inputs and '} from "./M02_Labor_Values";' in inputs for name in ("ui_template_item_picker_detail.ts", "ui_template_item_picker_edit.ts"): text = (UI / name).read_text(encoding="utf-8") assert "M02_MasterTemplete_Inputs" in text and 'InputsCard"' not in text, name @@ -183,6 +186,8 @@ def test_소스가_700줄을_넘지_않음(): M02 / "M02_MasterTemplete_ItemSource.ts", M02 / "M02_MasterTemplete_Inputs.ts", M02 / "M02_MasterTemplete_Inputs_Model.ts", + M02 / "M02_Labor_Values.ts", + M02 / "M02_Labor_Values_Model.ts", ): assert len(path.read_text(encoding="utf-8").splitlines()) <= 700, path.name diff --git a/resources/tester/test_m02_card_shell.py b/resources/tester/test_m02_card_shell.py index 8501c9945..f0070d654 100644 --- a/resources/tester/test_m02_card_shell.py +++ b/resources/tester/test_m02_card_shell.py @@ -44,8 +44,8 @@ def test_네_카드가_틀을_씀_수정_단추_한_모양(): assert 'variant: "filled"' not in cards # [수정] 은 모두 빈 단추(ghost) assert "c.saver.button.hidden = c.help.hidden = !can;" in cards # [저장] · ? 늘 보임 assert "DRAWING_KEYS" in cards and "SHEET_KEYS" in cards - inputs = _text("M02_MasterTemplete_Inputs.ts") - assert inputs.count("cardShell({") == 2 # 선택 입력 · 값 카드 + inputs = _text("M02_MasterTemplete_Inputs.ts") + _text("M02_Labor_Values.ts") + assert inputs.count("cardShell({") == 2 # 선택 입력 · 값 카드(39-3 에 `M02_Labor_Values.ts` 로) assert "shell.fail(" in inputs and "shell.loading()" in inputs assert "m02-struct__card ui-collapsible" not in inputs # 손으로 만든 겉 틀 없음 drawing = _text("M02_MasterTemplete_DrawingCard.ts") diff --git a/resources/tester/test_m02_labor_values.py b/resources/tester/test_m02_labor_values.py new file mode 100644 index 000000000..1757129cb --- /dev/null +++ b/resources/tester/test_m02_labor_values.py @@ -0,0 +1,101 @@ +"""일위대가 넣을 값(39-3) — 입력 목록 길 `GET /api/m02/logic-lib/inputs` · 값 셈(TS 모델). + +못박는 것 + - 입력 목록 = 키 차례 · 겹친 키 한 번 · 입력마다 자동값 · 없는 키는 「빠진로직」. + - 옛 선택 입력 길(`M02_MasterTemplete_Inputs.inputs`)과 몸 한 벌(`M02_Labor_Inputs`). + - 값 셈 = 칸 글 → 저장 값(빈 글 = 자동값 · 범위 = 수 · 고르기 = 원래 모양) · 범위 밖 · + 모르는 이름 버림(사라진 값 알림) · 줄 아래 작은 글 · 빈 값 수(자동값도 없는 입력만). +화면 TS 는 그 자리에서 트랜스파일해 node 로 돌림 · 정본은 읽기만. +""" + +from __future__ import annotations + +import json +import shutil +import subprocess +from pathlib import Path + +import pytest +from fastapi import FastAPI +from fastapi.testclient import TestClient + +from common_util.common_util_auth import verify_session +from M02_MasterTemplete import M02_Labor_Inputs as labor_inputs +from M02_MasterTemplete import M02_MasterTemplete_Router_Combos as combos_router + +ROOT = Path(__file__).resolve().parents[2] +A, B = "GF000222", "GF000221" # 찰쌓기 인력 · 장비 + + +@pytest.fixture() +def client() -> TestClient: + app = FastAPI() + app.include_router(combos_router.router) + app.dependency_overrides[verify_session] = lambda: {"user_id": 42, "role": "USER"} + return TestClient(app) + + +def test_입력_목록은_키_차례_겹침_한_번_자동값_빠진_로직(client: TestClient) -> None: + res = client.get("/api/m02/logic-lib/inputs", params={"keys": f"{B},{A},{B},GF999999,"}) + assert res.status_code == 200, res.text + got = res.json() + assert [x["로직"] for x in got["로직"]] == [B, A] + assert got["빠진로직"] == ["GF999999"] + one = got["로직"][1] + assert one["이름"] and one["결과단위"] + assert one["입력"] and all("자동값" in i and "이름" in i for i in one["입력"]) + # 옛 선택 입력 목록과 같은 몸 + assert got["로직"] == labor_inputs.logic_inputs([B, A])["로직"] + + +def test_빈_키는_빈_목록(client: TestClient) -> None: + got = client.get("/api/m02/logic-lib/inputs").json() + assert got == {"로직": [], "빠진로직": []} + + +SCRIPT = r""" +const fs = require("fs"); +const path = require("path"); +const ts = require(path.join(process.argv[1], "config", "node_modules", "typescript")); +const file = path.join(process.argv[1], "M02_MasterTemplete", "M02_Labor_Values_Model.ts"); +const js = ts.transpileModule(fs.readFileSync(file, "utf8"), { + compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 }, +}).outputText; +const box = { exports: {} }; +new Function("exports", "module", "require", js)(box.exports, box, require); +const m = box.exports; +const height = { 이름: "높이", 단위: "m", 범위: [0.5, 5], 자동값: 1.2 }; +const kind = { 이름: "종류", 고르기: ["막돌", "견치돌"] }; +const size = { 이름: "규격", 고르기: [25, 40] }; +const area = { 이름: "지역" }; +const inputs = [height, kind, size, area]; +console.log(JSON.stringify({ + stored: [ + m.storedOf(height, ""), m.storedOf(height, "2.5"), m.storedOf(kind, "막돌"), + m.storedOf(size, "40"), m.storedOf(area, "서울"), + ], + outside: [ + m.isOutside(height, "9"), m.isOutside(height, "0.5"), m.isOutside(height, ""), + m.isOutside(height, "abc"), m.isOutside(area, "9"), + ], + known: m.keepKnown(inputs, { 높이: 2, 종류: "", 없앤것: 3 }), + line: m.valueLine(inputs, { 높이: 2, 규격: 40 }), + lineEmpty: m.valueLine(inputs, {}), +})); +""" + + +def test_값_셈() -> None: + node = shutil.which("node") + if not node: + pytest.skip("node 없음") + out = subprocess.run( + [node, "-e", SCRIPT, str(ROOT)], capture_output=True, text=True, encoding="utf-8" + ) + assert out.returncode == 0, out.stderr + got = json.loads(out.stdout) + assert got["stored"] == [None, 2.5, "막돌", 40, "서울"] + assert got["outside"] == [True, False, False, True, False] + assert got["known"] == {"kept": {"높이": 2}, "lost": ["없앤것"]} + assert got["line"] == {"text": "높이 2m · 규격 40", "empty": 2} # 종류 · 지역 = 자동값 없음 + assert got["lineEmpty"] == {"text": "", "empty": 3} # 높이는 자동값이 있어 빈 값 아님 From 80a635d3b9c62017266ec9b6a78c06444089aaa2 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sun, 4 Oct 2026 17:30:41 +0900 Subject: [PATCH 12/14] =?UTF-8?q?feat(M02):=20=EC=A2=8C=EC=B8=A1=20?= =?UTF-8?q?=EA=B5=AC=EC=A1=B0=EB=AC=BC=20=EC=A1=B0=ED=95=A9=20=EC=88=98?= =?UTF-8?q?=ED=8F=89=EC=84=A0=20=C2=B7=20=ED=95=98=EC=9C=84=20=E3=80=8C?= =?UTF-8?q?=EC=9D=BC=EC=9C=84=EB=8C=80=EA=B0=80=E3=80=8D=20=C2=B7=20?= =?UTF-8?q?=E3=80=8C=EC=A0=84=EC=B2=B4=20=ED=95=AD=EB=AA=A9=E3=80=8D=20?= =?UTF-8?q?=EC=A4=84=20=C2=B7=20onAll=20=EC=86=90=EC=9E=A1=EC=9D=B4(39-2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 층 단추 · 나무 → 수평선 → 구조물 도면 → 일위대가 → 면적산출(재료산출 · 자재 조합은 sub6 몫으로 그대로) - 하위마다 맨 위 「전체 항목」 — 누르면 SideOptions.onAll(kind)(화면은 39-5) · 양식 · 다른 보기가 열리면 고름 지움 - 이름 「일위대가」(하위 제목 · 대시보드 카드 글) · 해시 open=일위대가 = 일위대가 하위 펼침 + 전체 항목 - 일위대가 열 줄은 SU 번호(39-1a) 뒤 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01BVPp2o6Mh2FZJr72yW3ofZ --- .../M02_MasterTemplete_UI_Side.ts | 83 +++++++++++++++---- .../M02_MasterTemplete_UI_Style.css | 7 ++ resources/tester/test_m02_labor_tree.py | 16 ++++ .../tester/test_m02_structure_options.py | 16 ++-- resources/tester/test_m02_structure_panel.py | 21 +++-- ui_template/ui_template_locale_m2.ts | 4 +- 6 files changed, 112 insertions(+), 35 deletions(-) diff --git a/M02_MasterTemplete/M02_MasterTemplete_UI_Side.ts b/M02_MasterTemplete/M02_MasterTemplete_UI_Side.ts index f2133c829..691426e65 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_UI_Side.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_UI_Side.ts @@ -1,7 +1,8 @@ /* ============================================================================= * M02_MasterTemplete_UI_Side.ts - * 마스터 템플릿 좌측 패널 — 「구조물 조합」(구조물집계표 줄 하나 + 하위 구조물 도면 · 면적산출 · - * 재료산출 · 자재 조합) · 「도면 양식」(새로 · 본떠 · 지우기). 서버 길은 계약 `6_계약.md`. + * 마스터 템플릿 좌측 패널 — 「구조물 조합」(층 단추 · 나무 → 수평선 → 하위 구조물 도면 · 일위대가 · + * 면적산출 · 재료산출 · 자재 조합 · 하위마다 맨 위 「전체 항목」) · 「도면 양식」(새로 · 본떠 · 지우기). + * 서버 길은 계약 `6_계약.md`. * 구조물 도면 · 면적산출 · 재료산출 줄은 집계표 구조물 열(꼴=수)마다 하나씩 — * 서버가 미리 만들어 둠(도번 SD · 면적번 SA · 재료번 SM + 같은 8자리) · * 줄 글은 번호가 맨 앞(「SD01010100 돌기슭막이 …」). 「자재 조합」 컨테이너는 `M02_MasterTemplete_Matset.ts`. @@ -46,13 +47,22 @@ const KINDS: { kind: Kind; label: () => string }[] = [ { kind: "table", label: () => L("M02_KindTable") }, { kind: "drawing", label: () => L("M02_KindDrawing") }, ]; -/** 구조물 열마다 한 줄씩인 목록 셋 — 종류 · 번호 칸 · 제목 */ -type ColumnKind = "structure" | "area" | "material"; -const COLUMN_KINDS: { kind: ColumnKind; key: keyof StructureNumbers; label: () => string }[] = [ - { kind: "structure", key: "도번", label: () => L("M02_KindStructure") }, - { kind: "area", key: "면적번", label: () => L("M02_KindArea") }, - { kind: "material", key: "재료번", label: () => L("M02_KindMaterial") }, +/** 구조물 열마다 한 줄씩인 하위 목록 — 종류 · 번호 칸(null = 열 줄 아직 없음) · 제목 · + * `all` = 맨 위 「전체 항목」 줄(PLAN 39-2) · 차례 = 구조물 도면 → 일위대가 → 면적산출 */ +type ColumnKind = "structure" | "labor" | "area" | "material"; +const COLUMN_KINDS: { + kind: ColumnKind; + key: keyof StructureNumbers | null; + label: () => string; + all: boolean; +}[] = [ + { kind: "structure", key: "도번", label: () => L("M02_KindStructure"), all: true }, + { kind: "labor", key: null, label: () => L("M02_KindLabor"), all: true }, + { kind: "area", key: "면적번", label: () => L("M02_KindArea"), all: true }, + { kind: "material", key: "재료번", label: () => L("M02_KindMaterial"), all: false }, ]; +/** 「전체 항목」 줄이 있는 하위 — 누르면 H 목록 화면(`onAll`) */ +export type AllKind = "structure" | "labor" | "area"; /** 새 도면 양식의 첫 문서 — 빈 웹캐드 문서 */ const STUB_DRAWING = { format: 6, @@ -75,6 +85,8 @@ export interface SideOptions { showView: (label: string, node: HTMLElement) => (dirty: boolean) => void; /** 「구조물 조합」 줄 아래 — 층 단추 · 고른 층의 구조물 라이브러리 나무(PLAN 29-2 · 페이지가 만듦) */ tree?: HTMLElement[]; + /** 하위 맨 위 「전체 항목」 줄 — 메인 칸에 그 하위 목록(화면 = `ItemList` · 페이지가 이음) */ + onAll?: (kind: AllKind) => void; } export interface SideHandle { @@ -102,6 +114,8 @@ export function buildSide(opt: SideOptions): SideHandle { let items: TemplateInfo[] = []; let columns: StructureColumn[] = []; let numbers: Record = {}; + /** 지금 연 「전체 항목」 — 양식 · 다른 보기 화면이 열리면 지움 */ + let allPicked: AllKind | null = null; let seq = 0; const section = (title: string, body: HTMLElement, ...more: HTMLElement[]): HTMLElement => @@ -133,7 +147,7 @@ export function buildSide(opt: SideOptions): SideHandle { confirmLeave: opt.confirmLeave, show: opt.showView, close: () => opt.onOpen(null), - onOpen: () => (options.clear(), paint()), + onOpen: () => (options.clear(), (allPicked = null), paint()), }); const matsetSection = section(matset.title, matset.body, ...matset.actions); // 펼칠 때마다 새로 받음 — 재료산출 옆 판이 갈라 낸 조합도 바로 보이게 @@ -145,7 +159,7 @@ export function buildSide(opt: SideOptions): SideHandle { isAdmin: canEdit(layer, opt.isAdmin), confirmLeave: opt.confirmLeave, show: opt.showView, - onOpen: () => (matset.clear(), paint()), + onOpen: () => (matset.clear(), (allPicked = null), paint()), }); /** 컨테이너 제목을 펼치면 그 화면을 엶 — 같은 이름 줄 단추를 갈음(PLAN 29-6) */ const openOnExpand = (sec: HTMLElement, run: () => void): HTMLElement => { @@ -154,12 +168,14 @@ export function buildSide(opt: SideOptions): SideHandle { }); return sec; }; - // 구조물 집계 양식 — 층 단추 · 나무 아래 하위 컨테이너 넷(들여 · 같이 접힘) · 처음부터 펼침(집계표가 열려 있음) · + const columnSections = COLUMN_KINDS.map((k, i) => section(k.label(), columnBodies[i])); + // 구조물 집계 양식 — 층 단추 · 나무 → 수평선 → 하위 컨테이너(들여 · 같이 접힘) · 처음부터 펼침(집계표가 열려 있음) · // 같은 이름 줄 단추 없음(제목이 그 이름 · 29-6) const summary = section( L("M02_KindTable"), el("div", { className: "m02-side__items ui-sidebar-list", children: opt.tree ?? [] }), - ...COLUMN_KINDS.map((k, i) => section(k.label(), columnBodies[i])), + el("hr", { className: "m02-side__rule" }), + ...columnSections, matsetSection, ); summary.classList.remove("is-collapsed"); @@ -219,9 +235,25 @@ export function buildSide(opt: SideOptions): SideHandle { } } - /** 구조물 도면 · 면적산출 · 재료산출 줄 — 구조물 열마다 하나 · 「번호 이름 변수값」(번호가 없으면 「없음」) */ - function paintRows(body: HTMLElement, kind: ColumnKind, key: keyof StructureNumbers): void { + /** 맨 위 「전체 항목」 줄(PLAN 39-2) — 누르면 그 하위 목록(`onAll`) */ + function allRow(kind: AllKind): HTMLElement { + const b = el("button", { + className: `m02-side__item m02-side__all ui-sidebar-row${allPicked === kind ? " is-active" : ""}`, + text: L("M02_ItemsAll"), + attrs: { type: "button", "data-all": kind }, + }); + b.addEventListener("click", () => void selectAll(kind)); + return b; + } + + /** 하위 줄 — 「전체 항목」 + 구조물 열마다 하나 · 「번호 이름 변수값」(번호가 없으면 「없음」) */ + function paintRows(body: HTMLElement, k: (typeof COLUMN_KINDS)[number]): void { + const { key } = k; + const head = k.all ? [allRow(k.kind as AllKind)] : []; + if (key === null) return body.replaceChildren(...head); + const kind = k.kind as Kind; // 번호 칸이 있는 하위만 — 일위대가 열 줄은 번호(SU)가 선 뒤 body.replaceChildren( + ...head, ...(columns.length ? columns.map((col) => { const num = numbers[col.id]?.[key] || L("M02_StructureNone"); @@ -236,8 +268,7 @@ export function buildSide(opt: SideOptions): SideHandle { : [el("p", { className: "m02-side__empty", text: L("M02_NoTemplates") })]), ); } - const paintColumns = (): void => - COLUMN_KINDS.forEach((k, i) => paintRows(columnBodies[i], k.kind, k.key)); + const paintColumns = (): void => COLUMN_KINDS.forEach((k, i) => paintRows(columnBodies[i], k)); /** 본떠 만들기 · 지우기 — 그 컨테이너에서 고른(열린) 양식이 있을 때만 켜짐 */ function paintButtons(): void { @@ -248,6 +279,7 @@ export function buildSide(opt: SideOptions): SideHandle { /** 고름 표시는 여기서만 — 목록 · 열 줄 · 자재 조합 줄을 함께 그려야 두 줄이 동시에 안 남음 * (양식이 열려 있으면 자재 조합 고름은 지움 · 자재 조합 상세는 열린 양식이 없음) */ function paint(): void { + if (opt.getOpen()) allPicked = null; paintList(); paintColumns(); paintButtons(); @@ -255,13 +287,22 @@ export function buildSide(opt: SideOptions): SideHandle { if (opt.getOpen()) options.clear(); } + async function selectAll(kind: AllKind): Promise { + if (!opt.onAll || allPicked === kind || !(await opt.confirmLeave())) return; + matset.clear(); + options.clear(); + allPicked = kind; + opt.onAll(kind); // 메인 칸 보기 화면 — 연 뒤 `paint` 가 이 줄을 고른 채 그림 + paint(); + } + // 여는 것은 편집 칸(`onOpen`) — 편집 칸이 열 때마다 `paint` 를 부름(페이지가 이음) async function select(info: TemplateInfo): Promise { if (!isOpen(info) && !(await opt.confirmLeave())) return; opt.onOpen({ layer, projectId: null, kind: info.종류, name: info.이름 }); } - async function selectColumn(col: StructureColumn, kind: ColumnKind): Promise { + async function selectColumn(col: StructureColumn, kind: Kind): Promise { if (!isPicked(kind, col.id) && !(await opt.confirmLeave())) return; // 제목은 편집 화면이 문서 번호를 앞에 붙임(「SD010101 돌기슭막이 …」) opt.onOpen({ layer, projectId: null, kind, name: col.id, label: columnHead(col) }); @@ -377,7 +418,13 @@ export function buildSide(opt: SideOptions): SideHandle { } const reveal = (target: "옵션" | "도면" | "일위대가"): void => { - if (target === "일위대가") return; // 구조물 조합은 처음부터 펼침 + // 일위대가 = 구조물 조합(처음부터 펼침) 안 일위대가 하위를 펼치고 「전체 항목」 + if (target === "일위대가") { + columnSections[COLUMN_KINDS.findIndex((k) => k.kind === "labor")].classList.remove( + "is-collapsed", + ); + return void selectAll("labor"); + } extras?.[target].classList.remove("is-collapsed"); if (target === "옵션") options.body.querySelector("button")?.click(); }; diff --git a/M02_MasterTemplete/M02_MasterTemplete_UI_Style.css b/M02_MasterTemplete/M02_MasterTemplete_UI_Style.css index c4c51d1ca..aa14b148e 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_UI_Style.css +++ b/M02_MasterTemplete/M02_MasterTemplete_UI_Style.css @@ -15,6 +15,13 @@ margin-left: var(--spacing-8); } +/* 구조물 조합 안 층 단추 · 나무와 하위 컨테이너 사이 수평선(PLAN 39-2) */ +.m02-side__rule { + margin: var(--spacing-8) 0; + border: 0; + border-top: 1px solid var(--color-border); +} + .m02-side__row { display: flex; flex-wrap: wrap; diff --git a/resources/tester/test_m02_labor_tree.py b/resources/tester/test_m02_labor_tree.py index 755dac40b..0776b560f 100644 --- a/resources/tester/test_m02_labor_tree.py +++ b/resources/tester/test_m02_labor_tree.py @@ -29,6 +29,22 @@ def test_나무_부품에_일위대가_전용_칸_없음(): assert gone not in panel, gone +def test_하위_차례_전체_항목_이름(): + side = _text("M02_MasterTemplete_UI_Side.ts") + kinds = side.split("const COLUMN_KINDS")[1].split("];")[0] + order = [kinds.index(f'kind: "{k}"') for k in ("structure", "labor", "area")] + assert order == sorted(order) # 구조물 도면 → 일위대가 → 면적산출 + assert 'label: () => L("M02_KindLabor"), all: true' in kinds + assert "onAll?: (kind: AllKind) => void;" in side # 화면은 ItemList(39-5) · 페이지가 이음 + assert "if (!opt.onAll || allPicked === kind || !(await opt.confirmLeave())) return;" in side + assert 'return void selectAll("labor");' in side # 해시 open=일위대가 + locale = (ROOT / "ui_template" / "ui_template_locale_m2.ts").read_text("utf-8") + assert 'M02_KindLabor: ["일위대가"' in locale and 'M02_ItemsAll: ["전체 항목"' in locale + assert 'B01_Dashboard_CardUnitCombo: ["일위대가",' in locale + css = _text("M02_MasterTemplete_UI_Style.css") + assert ".m02-side__rule {" in css and "border-top: 1px solid var(--color-border);" in css + + def test_층_단추는_구조물_조합_한_벌(): page = _text("M02_MasterTemplete_UI_Page.ts") assert page.count('layerBar("') == 1 # 구조물 조합 줄 아래 하나 diff --git a/resources/tester/test_m02_structure_options.py b/resources/tester/test_m02_structure_options.py index 6df8d180b..5d1a98e5e 100644 --- a/resources/tester/test_m02_structure_options.py +++ b/resources/tester/test_m02_structure_options.py @@ -120,17 +120,11 @@ def test_좌측_줄은_구조물_집계_양식_아래_한_줄_같은_한_곳에 assert side.index("옵션: section(options.title") < side.index( '도면: section(L("M02_KindDrawing")' ) - # 차례 = 일위대가 조합(27-2b) → 구조물 조합 → 옵션 → 도면 양식 - order = [ - "...(labor ? [openOnExpand(section(labor.title, labor.body), () => void labor.open())]" - " : []),", - " summary,", - " ...(extras ? [extras.옵션, extras.도면] : []),", - ] - assert "\n".join(order) in side - # 자재 조합을 열면 이 줄 · 일위대가 고름 지움 / 이 줄을 열면 자재 조합 · 일위대가 고름 지움 - assert "onOpen: () => (options.clear(), labor?.clear(), paint())" in side - assert "onOpen: () => (matset.clear(), labor?.clear(), paint())" in side + # 차례 = 구조물 조합(일위대가는 그 하위 · 39-2) → 옵션 → 도면 양식 + assert "children: [summary, ...(extras ? [extras.옵션, extras.도면" in side + # 자재 조합을 열면 이 줄 · 「전체 항목」 고름 지움 / 이 줄을 열면 자재 조합 · 「전체 항목」 고름 지움 + assert "onOpen: () => (options.clear(), (allPicked = null), paint())" in side + assert "onOpen: () => (matset.clear(), (allPicked = null), paint())" in side paint = side.split("function paint(): void {")[1].split("\n }\n")[0] assert "if (opt.getOpen()) options.clear();" in paint # 양식을 열면 이 줄 고름 지움 api = (ROOT / "M02_MasterTemplete/M02_MasterTemplete_Api_Fetch.ts").read_text("utf-8") diff --git a/resources/tester/test_m02_structure_panel.py b/resources/tester/test_m02_structure_panel.py index bb9b3341f..b10a492d8 100644 --- a/resources/tester/test_m02_structure_panel.py +++ b/resources/tester/test_m02_structure_panel.py @@ -154,9 +154,16 @@ def test_좌측_구조물_집계_양식_아래_하위_넷_새로_본떠_지우 assert "createSelectField" not in side # 새로 = 종류 고르기 없이 이름만 assert "cur?.종류 !== kind" in side # 본떠 · 지우기 = 그 컨테이너에서 고른 것이 있을 때만 assert "i.이름 === STRUCTURE_TABLE" in side # 표는 구조물집계표 한 줄만 - # 구조물 도면 · 면적산출 · 재료산출 · 자재 조합 = 구조물 집계 양식 컨테이너 안(같이 접힘) + # 구조물 도면 · 일위대가 · 면적산출 · 재료산출 · 자재 조합 = 구조물 집계 양식 컨테이너 안(같이 접힘) · + # 층 단추 · 나무와 하위 사이 수평선(39-2) summary = side.split("const summary = section(")[1].split(");")[0] - assert "COLUMN_KINDS.map" in summary and "matsetSection" in summary + assert ( + summary.index("opt.tree") + < summary.index("m02-side__rule") + < summary.index("...columnSections") + ) + assert "matsetSection" in summary + assert "const columnSections = COLUMN_KINDS.map(" in side assert 'M02_KindTable: ["구조물 조합"' in _text( # 화면 이름(26-7a) ROOT / "ui_template" / "ui_template_locale_m2.ts" ) @@ -184,7 +191,11 @@ def test_좌측_고름_표시는_열린_것_한_줄뿐(): """카드 [도면 수정] 등 무엇으로 열든 편집 칸이 좌측 `paint` 를 부름 — 목록 · 열 줄을 함께 그림.""" side = _text(SIDE) assert "picked" not in side # 따로 든 고름 상태 없음 — 편집 칸의 지금 열린 것만 봄 - assert re.search(r"function paint\(\): void \{\s*paintList\(\);\s*paintColumns\(\);", side) + assert re.search( + r"function paint\(\): void \{\s*if \(opt\.getOpen\(\)\) allPicked = null;\s*" + r"paintList\(\);\s*paintColumns\(\);", + side, + ) # 양식이 열리면 「전체 항목」 고름도 지움(39-2) for fn in ("async function select(", "async function selectColumn("): body = side.split(fn)[1].split("\n }\n")[0] assert "paint" not in body, fn # 여기서 한쪽만 그리면 두 줄이 남음 @@ -192,8 +203,8 @@ def test_좌측_고름_표시는_열린_것_한_줄뿐(): # 자재 조합 상세도 같은 한 곳 — 열면 양식 · 열 줄 고름 지움 · 양식을 열면 자재 조합 고름 지움 assert re.search(r"sel = null;\s*onOpened\?\.\(\);", _text(MAIN)) assert "if (opt.getOpen()) matset.clear();" in side.split("function paint(): void {")[1] - # 편집 칸이 열 때마다 좌측 `paint`(27-2b 뒤 = 일위대가 화면 해시 맞춤도 같은 자리) - assert re.search(r"\(\) => \{\s*side\.paint\(\);", _text(M02 / "M02_MasterTemplete_UI_Page.ts")) + # 편집 칸이 열 때마다 좌측 `paint`(일위대가 화면 해시 맞춤은 39-2 에서 지움) + assert "() => side.paint()," in _text(M02 / "M02_MasterTemplete_UI_Page.ts") def test_리본_접힘_모드에서_탭으로_편_것은_밖을_누르면_다시_접힘(): diff --git a/ui_template/ui_template_locale_m2.ts b/ui_template/ui_template_locale_m2.ts index eff4717b9..1058adaa6 100644 --- a/ui_template/ui_template_locale_m2.ts +++ b/ui_template/ui_template_locale_m2.ts @@ -4,7 +4,7 @@ * ========================================================================== */ export const ui_locales_m2 = { - B01_Dashboard_CardUnitCombo: ["일위대가 조합", "Unit cost combos"], + B01_Dashboard_CardUnitCombo: ["일위대가", "Unit costs"], B01_Dashboard_CardStructCombo: ["구조물 조합", "Structure combos"], B01_Dashboard_CardStructOption: ["구조물 옵션", "Structure options"], B01_Dashboard_CardDrawing: ["도면 양식", "Drawing templates"], @@ -172,4 +172,6 @@ export const ui_locales_m2 = { M02_TreeSaveGo: ["저장하고 계속", "Save and continue"], M02_TreeDiscardGo: ["버리고 계속", "Discard and continue"], M02_TreeDone: ["나무를 고침", "Tree updated"], + M02_KindLabor: ["일위대가", "Unit costs"], + M02_ItemsAll: ["전체 항목", "All items"], } as const; From eb132f13d0d86d8add08b58e0c90b9228a2d2567 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sun, 4 Oct 2026 17:31:11 +0900 Subject: [PATCH 13/14] =?UTF-8?q?feat(M02):=20=EA=B0=80=EC=A0=B8=EC=98=AC?= =?UTF-8?q?=20=EA=B3=B3=20=EA=B3=A0=EB=A5=B4=EA=B8=B0=20ItemPick=20?= =?UTF-8?q?=E2=80=94=20=ED=83=AD(=EB=A7=88=EC=8A=A4=ED=84=B0=20=C2=B7=20?= =?UTF-8?q?=ED=9A=8C=EC=82=AC=20=C2=B7=20=EA=B0=9C=EC=9D=B8=20=C2=B7=20?= =?UTF-8?q?=EB=8B=A4=EB=A5=B8=20=EC=82=AC=EB=9E=8C=20=C2=B7=20=EC=8B=A4?= =?UTF-8?q?=EB=AC=B4=EB=AC=B8=EC=84=9C)=20+=20=ED=95=AD=EB=AA=A9=20?= =?UTF-8?q?=EB=AA=A9=EB=A1=9D=20=C2=B7=20=EC=B9=B8=20=EC=95=88=20=C2=B7=20?= =?UTF-8?q?=EC=B0=BD=20=C2=B7=20=EC=A4=84=20=EB=88=84=EB=A6=84=20=ED=99=95?= =?UTF-8?q?=EC=9D=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 39-5 H3 — 탭 목록 · 기본 탭 = readableSources(39-6 E5) · 다른 사람 = 사람 고르기 하나 · 실무문서 = 마스터 일위대가 조합(일위대가 고를 때). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016PSXsvyDxjNmufteN5bqmt --- .../M02_MasterTemplete_ItemPick.css | 15 ++ .../M02_MasterTemplete_ItemPick.ts | 178 ++++++++++++++++++ 2 files changed, 193 insertions(+) create mode 100644 M02_MasterTemplete/M02_MasterTemplete_ItemPick.css create mode 100644 M02_MasterTemplete/M02_MasterTemplete_ItemPick.ts diff --git a/M02_MasterTemplete/M02_MasterTemplete_ItemPick.css b/M02_MasterTemplete/M02_MasterTemplete_ItemPick.css new file mode 100644 index 000000000..70f57c665 --- /dev/null +++ b/M02_MasterTemplete/M02_MasterTemplete_ItemPick.css @@ -0,0 +1,15 @@ +/* 가져올 곳 고르기(PLAN 39-5) — 탭 줄 + 목록 · 창이면 목록이 넓게 */ +.m02-itempick { + display: flex; + flex-direction: column; + gap: var(--spacing-12); + min-width: 0; +} + +.m02-itempick .m01-master__grid-wrap { + max-height: min(60vh, calc(100vh - 300px)); +} + +.ui-modal.m02-itempick__dialog { + width: min(960px, 94vw); +} diff --git a/M02_MasterTemplete/M02_MasterTemplete_ItemPick.ts b/M02_MasterTemplete/M02_MasterTemplete_ItemPick.ts new file mode 100644 index 000000000..dc1fbc122 --- /dev/null +++ b/M02_MasterTemplete/M02_MasterTemplete_ItemPick.ts @@ -0,0 +1,178 @@ +/* ============================================================================= + * M02_MasterTemplete_ItemPick.ts + * 가져올 곳 고르기(PLAN 39-5) — 탭(마스터 · 회사 · 개인 · 다른 사람 · 실무문서) + 항목 목록(`ItemList`). + * 복사 생성(상세 칸 안 · `buildPicker`) · 복사해 가져오기(창 · `pickFrom`)가 같이 씀. + * 탭 목록 · 기본 탭 = `readableSources`(권한 몫 · `M02_MasterTemplete_Layer.ts`) · 탭은 목록만 바꿈 + * (좌측 층 단추와 달리 화면을 다시 안 세움) · 모양은 층 단추(`m02-layers__btn`) 그대로. + * 다른 사람 = 사람 고르기 하나(`fetchOwners`) · 실무문서 = 마스터 일위대가 조합 줄(일위대가 고를 때만). + * ========================================================================== */ + +import { createSelectField, el, showConfirmDialog } from "@ui/ui_template_elements"; +import { openModal } from "@ui/ui_template_modal"; +import type { SessionUser } from "../A06_Login/A06_Login_Api_Fetch"; +import { fetchOwners, type Layer } from "./M02_MasterTemplete_Api_Fetch"; +import { + buildItemList, + ITEM_COLS, + loadItems, + type Col, + type ItemRow, + type ListKind, +} from "./M02_MasterTemplete_ItemList"; +import { fetchCombos, type ComboSummary } from "./M02_MasterTemplete_Labor_Api"; +import { readableSources, type Source } from "./M02_MasterTemplete_Layer"; +import { leafName } from "./M02_MasterTemplete_TreePanel_Model"; +import "./M02_MasterTemplete_TreePanel.css"; +import "./M02_MasterTemplete_ItemPick.css"; + +const TEXT = { + close: "닫기", + person: "사람", + noPeople: "다른 사람이 없음", + failed: "목록을 못 받음 — {value}", +}; + +/** 고른 곳 — `practice` = 실무문서(마스터 일위대가 조합) */ +export interface From { + layer: Layer; + owner: string | null; + label: string; + practice: boolean; +} + +export type Picked = { from: From; row: ItemRow } | { from: From; combo: ComboSummary }; + +/** 고른 것 이름 — 확인 글에 씀 */ +export const pickedName = (p: Picked): string => + "row" in p ? leafName(p.row) : `${p.combo.키} ${p.combo.이름}`; + +const PRACTICE_COLS: Col[] = [ + { head: "키", get: (c) => c.키 }, + { head: "이름", get: (c) => c.이름 }, + { head: "구분", get: (c) => c.구분 ?? "" }, + { head: "상세구분", get: (c) => c.상세구분 ?? "" }, + { head: "단위", get: (c) => c.단위 ?? "" }, + { head: "담은 로직 수", get: (c) => String(c.count) }, +]; + +const why = (error: unknown): string => (error instanceof Error ? error.message : String(error)); + +export interface PickerOptions { + user: SessionUser | null; + /** 볼 회사(`viewCompany`) */ + company: string | null; + /** 칸 종류 — 항목 통째(복사 생성) · 도면 · 면적산출 · 일위대가(실무문서 탭이 붙음) */ + kind: ListKind; + /** 고를 수 없는 줄(받는 열 자신 등) — 목록에서 뺌 */ + skip?: (from: From, row: ItemRow) => boolean; + onPick: (picked: Picked) => void; +} + +/** 탭 + 목록 — 부르는 쪽이 칸 안 · 창에 붙임 */ +export function buildPicker(opt: PickerOptions): { root: HTMLElement } { + const { tabs, first } = readableSources(opt.user, opt.company, { + practice: opt.kind === "labor", + }); + const list = buildItemList(); + const people = createSelectField({ options: [], compact: true, label: TEXT.person }); + people.root.hidden = true; + let owners: { owner: string; name: string }[] | null = null; + let seq = 0; + + const buttons = tabs.map((tab) => { + const b = el("button", { + className: "m02-layers__btn", + text: tab.label, + attrs: { type: "button", "data-source": tab.id }, + }); + b.addEventListener("click", () => void show(tab)); + return b; + }); + + async function show(tab: Source): Promise { + const mine = ++seq; + buttons.forEach((b) => b.classList.toggle("is-active", b.dataset.source === tab.id)); + people.root.hidden = tab.id !== "people"; + list.message(); + try { + if (tab.id === "practice") { + const combos = await fetchCombos(); + if (mine !== seq) return; + const from = { layer: tab.layer, owner: null, label: tab.label, practice: true }; + return list.show(combos, PRACTICE_COLS, (combo) => opt.onPick({ from, combo })); + } + let owner = tab.owner; + let label = tab.label; + if (tab.id === "people") { + owners ??= await fetchOwners("personal", opt.company); + if (mine !== seq) return; + if (!owners.length) return list.message(TEXT.noPeople); + if (!people.select.value) { + people.setOptions( + owners.map((o) => ({ value: o.owner, text: o.name })), + owners[0].owner, + ); + } + owner = people.select.value; + label = `${tab.label} · ${owners.find((o) => o.owner === owner)?.name ?? owner}`; + } + const rows = await loadItems(tab.layer, owner); + if (mine !== seq) return; + const from = { layer: tab.layer, owner, label, practice: false }; + list.show( + rows.filter((row) => !opt.skip?.(from, row)), + ITEM_COLS[opt.kind], + (row) => opt.onPick({ from, row }), + ); + } catch (error) { + if (mine === seq) list.message(TEXT.failed.replace("{value}", why(error))); + } + } + + people.select.addEventListener("change", () => { + const tab = tabs.find((t) => t.id === "people"); + if (tab) void show(tab); + }); + const start = tabs.find((t) => t.id === first) ?? tabs[0]; + if (start) void show(start); + return { + root: el("div", { + className: "m02-itempick", + children: [ + el("div", { className: "m02-layers__row", children: [...buttons, people.root] }), + list.root, + ], + }), + }; +} + +/** 창으로 고름 — 줄 누름 → `ask` 확인(있으면) → 고른 것 · 닫으면 null */ +export function pickFrom( + opt: Omit & { title: string; ask?: (picked: Picked) => string }, +): Promise { + return new Promise((resolve) => { + let done = false; + const finish = (value: Picked | null): void => { + if (!done) resolve(value); + done = true; + }; + openModal({ + title: opt.title, + closeLabel: TEXT.close, + dialogClass: "m02-modal m02-itempick__dialog", + beforeClose: () => (finish(null), true), + mount: (body, close) => { + const picker = buildPicker({ + ...opt, + onPick: (picked) => + void (async () => { + if (opt.ask && !(await showConfirmDialog(opt.ask(picked)))) return; + finish(picked); + close(); + })(), + }); + body.append(picker.root); + }, + }); + }); +} From af92f0553e7c58009e3b449e88745dc64763791c Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sun, 4 Oct 2026 17:34:10 +0900 Subject: [PATCH 14/14] =?UTF-8?q?feat(M02):=20=E3=80=8C=EC=A0=84=EC=B2=B4?= =?UTF-8?q?=20=ED=95=AD=EB=AA=A9=E3=80=8D=20=ED=99=94=EB=A9=B4=20=E2=80=94?= =?UTF-8?q?=20=EA=B5=AC=EC=A1=B0=EB=AC=BC=20=EB=8F=84=EB=A9=B4=20=C2=B7=20?= =?UTF-8?q?=EC=9D=BC=EC=9C=84=EB=8C=80=EA=B0=80=20=C2=B7=20=EB=A9=B4?= =?UTF-8?q?=EC=A0=81=EC=82=B0=EC=B6=9C=20=ED=95=98=EC=9C=84=20=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D=EC=9D=84=20=EC=83=81=EC=84=B8=20=EC=B9=B8=EC=97=90=20?= =?UTF-8?q?=C2=B7=20=EC=A4=84=20=EB=88=84=EB=A6=84=20=3D=20=EA=B7=B8=20?= =?UTF-8?q?=ED=95=AD=EB=AA=A9=20=EC=83=81=EC=84=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 39-5 H2 — 좌측 onAll(39-2) 이음 · 제목 「하위 · 전체 항목」 · 목록 = ItemList(M01 꼴). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016PSXsvyDxjNmufteN5bqmt --- .../M02_MasterTemplete_ItemList.ts | 17 ++++++++++++ .../M02_MasterTemplete_UI_Page.ts | 26 +++++++++++++++++-- 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/M02_MasterTemplete/M02_MasterTemplete_ItemList.ts b/M02_MasterTemplete/M02_MasterTemplete_ItemList.ts index 363c48e5c..4d015ca6b 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_ItemList.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_ItemList.ts @@ -35,6 +35,7 @@ const TEXT = { none: "항목이 없음", noMatch: "맞는 항목이 없음", loading: "불러오는 중", + failed: "목록을 못 받음", }; const SIZE = 30; @@ -111,6 +112,22 @@ export function pageOf( export const loadItems = async (layer: Layer, owner: string | null): Promise => (await fetchTree(layer, owner)).items as ItemRow[]; +/** 「전체 항목」 화면(39-5) — 한 층 · 한 하위 목록 · 줄 누름 = 그 항목 상세(좌측 열 줄 누름과 같음) */ +export function buildAllItems( + kind: ListKind, + layer: Layer, + owner: string | null, + onOpen: (row: ItemRow) => void, +): HTMLElement { + const list = buildItemList(); + loadItems(layer, owner).then( + (rows) => list.show(rows, ITEM_COLS[kind], onOpen), + (error: unknown) => + list.message(`${TEXT.failed} — ${error instanceof Error ? error.message : String(error)}`), + ); + return list.root; +} + export interface ItemListHandle { root: HTMLElement; /** 줄 · 칸 · 누를 일을 바꿔 그림(찾는 글은 그대로 · 쪽은 1) */ diff --git a/M02_MasterTemplete/M02_MasterTemplete_UI_Page.ts b/M02_MasterTemplete/M02_MasterTemplete_UI_Page.ts index a19bc482a..9b792a9ce 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_UI_Page.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_UI_Page.ts @@ -15,7 +15,7 @@ import { t as L } from "@ui/ui_template_locale"; import { createWorkflowOverlays } from "@ui/ui_template_overlay"; import { navigateTo, type MountedPage } from "../A00_Common/router"; import { fetchSessionUser } from "../A06_Login/A06_Login_Api_Fetch"; -import { fetchOwners, setActiveLayer } from "./M02_MasterTemplete_Api_Fetch"; +import { fetchOwners, setActiveLayer, type Kind } from "./M02_MasterTemplete_Api_Fetch"; import { accessFor, buildReadOnlyBar, @@ -41,9 +41,17 @@ import { seesOthers, } from "./M02_MasterTemplete_TreePanel_Roots"; import { canEdit, createMain, newItemButton, type Selection } from "./M02_MasterTemplete_UI_Main"; -import { buildSide } from "./M02_MasterTemplete_UI_Side"; +import { buildSide, type AllKind } from "./M02_MasterTemplete_UI_Side"; +import { buildAllItems } from "./M02_MasterTemplete_ItemList"; import "./M02_MasterTemplete_UI_Style.css"; +/** 「전체 항목」 화면 제목 앞마디 — 하위 이름 */ +const ALL_TITLE = { + structure: "M02_KindStructure", + labor: "M02_KindLabor", + area: "M02_KindArea", +} as const satisfies Record; + export async function renderM02MasterTemplate(root: HTMLElement): Promise { const user = await fetchSessionUser().catch(() => null); const entry = entryFromHash(); @@ -155,6 +163,20 @@ export async function renderM02MasterTemplate(root: HTMLElement): Promise void main.openTable(layer)), peopleTree("구조물조합", tree.root), ], + // 「전체 항목」(39-5) — 메인 칸에 그 하위 목록 · 줄 누름 = 그 항목 상세(좌측 열 줄과 같은 `open`) + onAll: (kind) => + void main.showView( + `${L(ALL_TITLE[kind])} · ${L("M02_ItemsAll")}`, + buildAllItems(kind, layer, owner, (row) => { + void main.open({ + layer, + projectId: null, + kind: kind as Kind, + name: row.열, + label: row.이름, + }); + }), + ), }); const layout = el("div", { className: "ui-workflow-layout m02-master" });