diff --git a/docs/raw/요구_내역서/_분석_내역5-C_기초자료만들기_화면.md b/docs/raw/요구_내역서/_분석_내역5-C_기초자료만들기_화면.md new file mode 100644 index 000000000..6bacd3663 --- /dev/null +++ b/docs/raw/요구_내역서/_분석_내역5-C_기초자료만들기_화면.md @@ -0,0 +1,114 @@ +# 내역 5-C — 아이슬로 설계 기초자료 만들기 화면 실측 (sub7 · 10-31) + +- 대상 = M02 마스터 템플릿(구조물 조합 · 일위대가 사전조합 · 일위대가 실무자료) · 계정 = 시스템 관리자(umsangdon · `/api/auth/session` 200 · SYSTEM_ADMIN 확인 뒤 판정) + 일반 사용자(verify_accounts 「일반」 · USER · 회사 3 · 쿠키 따로 문맥). +- ⛔ 쓰기 없음 — [확인] · [저장] · [사전조합 생성] · [복사] 확인 · [파일 업로드] 직전에서 멈추거나 [닫기] · [취소]. 파일 올리기는 올리는 순간 서버에 저장되므로(`M02_Labor_Router.py:197-209` · `M02_Hopyo_Store.py:78-102`) 올리지 않고, 이미 올라와 있는 북부청 엑셀(호표 68)로 셈. +- ORCA 탭은 이번엔 열렸으나 10~15 초마다 페이지가 저절로 다시 읽혀(performance.timeOrigin 실측 · 홈 화면도 같음) 조작이 끊김 → 지난번처럼 자기 tmp 의 headless Chrome(같은 5182)으로 셈. ORCA 탭은 놓음(release). +- 사진 = `tmp/내역_화면5/Aislo_M02_*.png` 16 장. 3-C 에서 찍은 화면(M02 첫 화면 · 사전조합 문서 · 목록표 · B06 구조물 배치 · B08 구조물 조합 선택)은 다시 찍지 않음. +- 엑셀 견본 = `resources/knowledge/original/실무문서/` 의 실무 xlsx 넷(열 짜임만 읽음 · 올리지 않음). + +## ① 결론 다섯 줄 + +1. 엑셀 → 설계 기초자료 길은 네 단계 — 파일 통째 올리기 → 호표마다 맞춤 확인 → 「일위대가 사전조합」 생성 → 「구조물 조합」에 다시 담기. 붙여넣기(Ctrl+V)로 호표 하나 넣는 길과 숫자 그대로 넣는 줄이 없음(`M02_Labor_Router.py:325-328` 금액 · 품 칸 = 400). +2. 가장 큰 벽은 맞춤 — 엑셀 줄 415 중 서버 제안 234(56%). 나머지는 사람이 로직을 고르거나 버림. 제9호표는 줄 6 중 제안 2 · 자재 「큰돌」은 「없음」이라 체크도 못 함 → 만들어진 일위대가가 엑셀 금액과 다름. +3. 구조물 조합 하나 = 카드 셋(도면 · 일위대가 · 면적산출)을 따로 채움 — 일위대가만 줄 7개면 약 50 누름(추정). 수량산출을 안 넣은 줄은 합계에서 조용히 빠짐(돌기슭막이 예: 줄 합 1,910,689 원 → 반영 100,407 원 · 경고 없음). +4. 규격만 다른 조합(H=1.5 → 2.0)은 실무 엑셀에서 수량 4 칸 바꾸기(울진 간선 호표 17~19)인데, 아이슬로는 복사 → 이름 바꾸기 → 도면 · 면적 · 일위대가를 각각 고침(높이 한 칸이 셋으로 번지는 구조 없음). 복사본은 「원래 이름」 그대로라 같은 이름이 둘 생김. +5. 브레인 가설 근거: 실무 일위대가 줄 = 「단가산출 참조 × 수량」. 석축처럼 m 당 금액인 구조물은 설계가 높이로 조합을 고르고 길이를 곱하면 되므로 고정형(엑셀 숫자 그대로)으로도 금액은 이어짐(추정). 설계 값(관 길이 · 높이)이 넣을 값으로 들어가는 구조물만 계산형이 꼭 필요(추정 · 3-C ⑥-1 「관 길이 8 m(7.9)」). 지금은 고정형 줄 자체가 없음. + +## ② 새로 본 화면 생김새 + +### 구조물 조합 항목 (`Aislo_M02_구조물조합_항목.png`) + +- 위: 비교 행렬(종류 · 분류1~3 · 일위대가 합계). 행렬 칸을 누르면 아래 카드 셋이 그 항목으로 바뀜. +- 카드 셋 나란히 — 도면 정보(SD-) · 일위대가(SU-) · 면적산출(SA-). 카드마다 [?] [저장] [가져오기] [수정] [삭제]. 면적산출만 [알약] 더 있음. +- 도면 카드: CAD 미리보기 + 「DWG · DXF 파일을 끌어다 놓거나 [삽입 → DWG 불러오기]」. +- 일위대가 카드: 「단가산출 로직」 표(차례 · 구분 ▾ · 이름(GC000260 메쌓기…) · 별칭 · 수량산출 · 결과 단위 · 노무비 · 재료비 · 경비 · 합계 · 순서). 금액 칸 두 줄 = 위 단위당 · 아래 반영. 수량산출 「—」 줄은 아래 칸도 「—」. +- 면적산출 카드: 엑셀 꼴 시트(서식 · 붓 · 필터 · Σ · 데이터 ▾ · fx) + 아래 알약 줄(메쌓기 ㎡ · 큰돌 채집 개 …). 이 견본 시트에 「블라블라 · 이히리베」 같은 시험 글이 남아 있음. +- [?] 도움말: 「로직을 더하거나 순서를 바꿀 때는 [수정]」(`M02_MasterTemplete_CardHelp.ts:63`). + +### 일위대가 상세 (`Aislo_M02_일위대가상세.png` · `_원문띠.png`) + +- 머리: [돌아가기] · 「SU-000006 돌기슭막이 메쌓기 1.2m …」 · [도면] [면적산출] [?] · 오른쪽 [새로고침] [저장]. +- 단위(m) · 본뜬 곳(「system UA000008」) → 단가산출 로직 표 + [로직 추가] → 자재목록 + [자재 추가] → 다이어그램(넣을 값 → 일위대가 → 노무비 · 재료비 · 경비 칸). +- 오른쪽 접힌 「원문」 띠: 로직 줄을 고르면 품셈 원문(조항 번호 · 표 · [주] ①~⑪) + [원문 다운로드]. + +### 항목 추가 (`Aislo_M02_새항목창.png` · `_새로만들기창.png` · `_복사생성.png` · `_복사생성_확인.png`) + +- [새 항목] → 「새 항목이 들어갈 자리 — 맨 위(폴더 밖) · 나무에서 폴더를 고르면 그 안」 + [새로 만들기] [복사 생성]. +- [새로 만들기] → 「항목 추가」 창: 구조물 종류(있는 맨 위 폴더 이름 목록 · 새 이름도 됨) · 항목 이름 · [확인]. 단위 · 분류 · 규격은 안 물음(`M02_MasterTemplete_TreePanel.ts:483-492`). [확인] = 바로 서버에 씀(`:423-424`). +- [복사 생성] → 층 탭 넷(마스터 · 회사 · 개인 · 다른 사람) · 찾기 · 표(이름 · 폴더 · 단위 · 도번 · 일위대가번 · 면적번 · 고친 날). 금액 · 그림 미리보기 없음. 줄 누름 → 「… 복사해 「맨 위 (폴더 밖)」 에 새 항목으로 만듦 — 새 번호 · 원래 이름」 [취소] [확인]. + +### 일위대가 실무자료 (`Aislo_M02_실무자료.png` · `_실무자료_엑셀보기.png` · `_실무호표_확인창.png`) + +- 왼쪽 묶음: [파일 업로드] · 올린 파일 줄(「2026년_북부지방산림청_임도사업_설계내역서(공통단가).xls · 호표 68」) · 「별칭 장부」. +- 엑셀 보기 창: 머리 「시트 일위대가 · 호표 68 · 검산 맞음 · 제안 234 / 줄 415 · [삭제]」. 왼쪽 = 원본 엑셀 그대로(읽기 전용 · 아래 시트 탭 일위대가 · 단가산출). 오른쪽 = 「호표 · 품명 · 규격 찾기」 + 호표 목록(체크 · 제N호표 · [수정] · 이름 · 제안 n/m) + [저장]. 시트 고르기 단계 없음(시트 이름 고정 `M02_Hopyo_Excel.py:35-36`). +- 확인 창(호표 [수정]): 머리 「제9호표 큰돌흙막이(찰쌓기) · ㎡ · 엑셀 단가 148,382 · 금액 칸 위 = 엑셀 · 아래 = 우리(미리 셈) · 제안 2 / 줄 6 · 체크 2」 + [엑셀 보기] [맞음 · 별칭 일괄 체크 (2)] [사전조합 생성]. + - 로직 줄: 체크 · 차례 · 구분 + 판정 꼬리표(맞음 · 다른 점 · 후보) · 우리 로직(GF000227 큰돌쌓기 찰쌓기 + 배수 칸) · 별칭(엑셀 이름) · 수량산출 · 금액 넷 · 「품 비교」(「작업반장 0.083 → 0.083 ✓ · 굴착기+집게 0.48 → – ≠」) · [고르기] [원문 보기] [엑셀 보기] [옵션] · 「추가 ▾」(로직없음 · 사용안함). + - 자재 줄: 판정(맞음 · 없음 · 후보) · 엑셀 이름 · 규격 · 자재구분 · 별도계상/직접계산 · 출처 넷 · 「엑셀 / 우리 반영 금액」. 「없음」 줄은 체크 칸이 꺼짐. + +### 나누기 (`Aislo_M02_나무_우클릭.png` · `_회사로복사창.png` · `_회사층.png` · `_일반_첫화면.png` · `_일반_회사층.png`) + +- 관리자 나무 우클릭(항목): 이름 바꾸기 · 옮기기 · 지우기 · 복사(1개) · 마스터로 복사…(1개) · 회사로 복사…(1개). 「회사로 복사…」 → 「복사할 폴더 [맨 위 (폴더 밖)] [확인]」. +- 관리자 회사 층(회사 1): 비어 있음 — 아직 아무도 회사로 나누지 않음(실측). +- 일반 사용자: 층 단추 「마스터 · 읽기 | 회사 · 읽기 | 개인」. 회사 층 위 막대 「회사용은 복사해서 씀 — 고치려면 고른 열을 개인용으로 복사 [개인용 복사]」. 회사로 올리는 메뉴 없음(`M02_MasterTemplete_UI_Page.ts:84-93` 쓰기 권한 층만). + +## ③ K1~K6 누름 표 + +누름 = 마우스 누름 + 글 칸 입력 한 덩어리. 출발 = M02 화면이 열린 상태(대시보드 → M02 는 3-C T3 의 다섯 누름). 화면 이동 = 페이지 바뀜(창 겹은 괄호). + +| 일 | 차례(화면 · 누름) | 누름 | 화면 이동 | 고민 지점 | +| --- | --- | --- | --- | --- | +| K1 엑셀 호표 하나 등록 | 붙여넣기 길 없음 → 실무자료 펼침(1) → [파일 업로드](1) → 파일 창에서 고름(2 · 추정) → (올리는 순간 저장 · 엑셀 보기 저절로 열림) → 찾기 칸 입력(1) → 호표 [수정](1) → 제안 줄 2 는 체크돼 있음 · 나머지 줄마다 [고르기](1) → 찾기(1) → 고름(1) · [옵션] 넣을 값(1~2) → [사전조합 생성](1) | 약 13~19(제9호표 · 추정) | 0 (창 3겹: 파일 창 · 엑셀 보기 · 확인) | 엑셀에서 복사한 줄을 붙일 칸이 없음 · 호표 하나를 위해 파일 통째를 올림 · 올리면 바로 저장(되돌림 = [삭제]) · 판정 낱말(맞음 · 다른 점 · 후보 · 없음) · 품 비교 ≠ 를 어떻게 할지 · 「없음」 자재(큰돌)는 체크 불가 · 결과물은 「사전조합」이라 설계(B06)에서 안 보임 → K3 를 또 해야 함 | +| K2 시트 통째(호표 수십) | 실무자료 펼침(1) → 업로드(3) → 엑셀 보기 호표 체크 ×68(모두 고르기 없음) → [저장 (68)](1) → 확인 1 번(1) — 안 연 호표는 서버 제안 줄로 만듦(`M02_MasterTemplete_Hopyo_Sheet.ts:9-10` · `:416-423`) | 약 74 | 0 (창 2겹) | 체크 68 번 · 제안 줄만 들어가 줄 415 중 234(56%)만 사전조합에 남음 · 체크 줄 없는 호표는 건너뜀 · 하위 호표를 먼저 안 만들면 409 안내 · 「만듦 n / m」 알림 뒤 무엇이 빠졌는지는 호표마다 열어 봐야 앎 | +| K3 새 조합(석축 H=2.0) | [새 항목](1) → [새로 만들기](1) → 종류(1) · 이름(1) → [확인](1 · 서버 씀) → 일위대가 카드 [수정](1) → [로직 추가](1) → 찾기(1) → 고름(1) → 줄마다 [옵션] 넣을 값(2~3) · 수량산출 칸(1) · 수기 또는 면적 칸 고름(1~2) — 실무 호표 17 꼴 7 줄 반복 → [저장](1) → [돌아가기](1) → 면적 카드 칸 입력 · 알약 잇기(줄마다 2~3) → [저장](1) → 도면 [수정] → DWG 불러오기 또는 CAD 그리기 → [저장] | 일위대가만 약 50 · 면적까지 약 65 + 도면(추정) | 화면 2(목록 → 일위대가 상세 → 목록 · 도면 상세 별도) | 「석축」으로 찾으면 헐기 2 건뿐(쌓기는 「돌쌓기 찰쌓기」 이름) · 종류 칸이 자유 글이라 B06 구조물 종류와 맞출 기준이 안 보임 · 단위(m)와 로직 단위(㎡)를 수량산출로 맞춰야 함 · 수량산출 안 채운 줄은 합계에서 빠지는데 경고 없음 · 엑셀의 1.57 같은 「m 당 수량」을 어느 칸에 넣는지(수기 vs 면적 칸) 판단 | +| K4 규격만 다른 조합 | 나무 우클릭(1) → 「복사(1개)」(1) → 붙여넣기(1~2) 또는 [새 항목] → [복사 생성] → 층 → 줄 → [확인](5) → 이름 바꾸기(우클릭 · 메뉴 · 입력 · 확인 4) → 일위대가 [수정] → 줄마다 수량 고침(4 줄 × 2) → [저장] → 면적 칸 고침(2~4) → [저장] → 도면 치수 고침(CAD · n) → [저장] | 약 22~28 + 도면(추정) | 화면 2 | 복사본 이름이 원래 이름 그대로(같은 이름 둘) · 높이를 한 곳에서 바꾸는 칸이 없음 — 도면 · 면적 · 일위대가 각각(`M02_Labor_Doc.py:3-9` · 면적 칸에 이은 수량만 따라감 `:8`) · 실무 엑셀은 같은 줄에 수량만 1.57 → 2.09 → 2.61 ㎡(울진 간선 `일위대가표` 133~158 행) | +| K5 회사 사람과 나누기 | 관리자 · 마스터 계정: 나무 우클릭(1) → 「회사로 복사…」(1) → 폴더(1) → [확인](1). 일반 사용자: 메뉴 없음 → **막힘**(회사 관리자에게 부탁 · 화면 밖). 받는 쪽: 회사 층 → 열 고름(1) → [개인용 복사](1) → 고침 | 관리자 4 · 일반 막힘 | 0 | 일반 실무자는 자기 것을 회사로 올릴 길이 없음(`M02_Layer_Access.py:1-9`) · 회사 사본은 원본과 따로 — 고친 뒤 다시 올려야 · 사전조합 목록표 [복사]는 같은 층 안만(`M02_MasterTemplete_Library_List.ts:197-205`) · 별칭 장부는 회사로 못 올림 | +| K6 설계에서 고르고 수량 · 금액 보기 | B06 구조물 고름(1) → 부품 「구조물 조합 [미지정]」(1) → 층 단추(1) → 나무 펼침(2~3) → 열 고름(1) → [적용](1) → 구조물 [저장](1) → B08(1) 집계표 → B09(1) → 설계내역 탭(1) (3-C 사진 재사용 · `Aislo_B06_구조물배치` · `Aislo_B08_구조물조합선택`) | 약 11~12 | 2(B06 → B08 → B09) | B06 고르기 창은 구조물 조합만 — 사전조합은 B09 단가연결에서만 고름(`B09_Estimation_UI_Tabs_Bill_Link.ts:227-237`) · 사본은 구조물 [저장] 때 생김 · M02 원본을 고쳐도 공사 사본은 그대로 → [재적용](`M02_Item_Snapshot.py:1-20`) · 금액은 저장 뒤(3-C T6) | + +## ④ 엑셀을 들고 온 실무자가 처음 막히는 곳 다섯 + +1. **붙일 칸이 없음** — 엑셀에서 호표 줄을 복사해도 받는 칸이 없음. 로직 표 · 자재 표엔 붙여넣기 처리가 없고(M02 Labor 파일 paste 0 건), 엑셀 보기 창은 읽기 전용(`M02_MasterTemplete_Hopyo_Sheet.ts:337-338`). 파일 통째를 올려야 하고, 올리면 바로 저장 · 원본 파일은 안 남음(`M02_Hopyo_Store.py:1-5`). +2. **엑셀 숫자가 그대로 안 들어감** — 금액 · 품을 보내면 400(`M02_Labor_Router.py:325-328`). 우리 로직으로 다시 셈 → 제9호표 첫 줄 엑셀 134,304 / 우리 136,348, 품 비교 「굴착기+집게 0.48 → –」. 「우리에 없음」 줄 · 「없음」 자재는 빠짐. 실무자 눈엔 「내 단가가 바뀌었다」. +3. **이름이 셋 · 단계가 둘** — 실무자료(HP) → 사전조합(SC) → 구조물 조합(SU · SD · SA). 사전조합까지 만들어도 B06 에선 안 보이고, 구조물 조합에 [로직 추가] → 「일위조합」으로 다시 담아야 함. 화면엔 이 차례를 알려 주는 안내가 없음. +4. **빈 수량 줄이 조용히 빠짐** — 일위대가 카드 줄 7 개 중 수량산출 「—」 6 줄은 반영 금액 「—」. 합계 칸 위 1,910,689 · 아래 100,407 두 숫자가 함께 보여 어느 것이 이 구조물 값인지 헷갈림(`Aislo_M02_일위대가상세.png`). 「수량을 안 넣은 줄 6」 같은 알림 없음. +5. **규격 변형 · 찾기 낱말** — 높이 하나 바꾸려고 카드 셋을 따로 고침 · 복사본 이름이 같음. 실무 말 「석축」으로 찾으면 쌓기 로직이 안 나옴(「돌쌓기 찰쌓기」 · 「찰쌓기(굴착기+부착용 집게)」로 쳐야 나옴). 로직 고르기 목록은 GF · GC 번호와 「산림품셈 13장 구조물」 같은 장 이름이 앞섬. + +## ⑤ 지금 화면 중 살릴 것 + +| 화면 장치 | 어디 | 왜 | +| --- | --- | --- | +| 엑셀 보기 창 — 원본 엑셀 꼴 그대로 + 오른쪽 호표 목록 · 제안 n/m(다 이으면 녹색) | 실무자료 | 엑셀 손버릇과 같음 · 무엇이 남았는지 한눈 | +| 엑셀 두 꼴을 다 읽음 — EST 꼴 「일위대가」(거창 · 호표 14) · STmate 꼴 「일위대가표」+「단가산출근거」(영월 23 · 울진 34) | `M02_Hopyo_Excel.py:35-36` · `M02_Hopyo_Stmate.py:1-20` | 실무 xlsx 넷 모두 받는 시트가 있음(열 짜임 확인) | +| 확인 창 「품 비교」(엑셀 품 → 우리 품 ✓ / ≠) · 금액 위 = 엑셀 · 아래 = 우리 | 실무 호표 확인 | 어디가 다른지 숫자로 봄 — 고정형 갈래를 둘 때도 그대로 쓸 꼴 | +| 호표 체크 + [저장 (n)] 한 번 · [맞음 · 별칭 일괄 체크] | 엑셀 보기 · 확인 창 | 수십 개를 한 번에 만드는 뼈대가 이미 있음 | +| 원문 띠 — 품셈 책 꼴(조항 · 표 · [주]) + [원문 다운로드] | 일위대가 상세 오른쪽 | 근거를 책에서 안 찾음 | +| 카드 셋 한 화면 + 비교 행렬 | 구조물 조합 | 도면 · 일위대가 · 면적을 같이 봄 · 규격별 금액 비교 | +| [복사 생성] 목록(마스터 · 회사 · 개인 · 다른 사람) · 우클릭 「회사로 복사…」 | 새 항목 · 나무 | 변형 · 나누기 길의 자리는 이미 있음 — 이름 바꾸기 · 일반 사용자 권한만 모자람 | +| 회사 층 안내 막대 「회사용은 복사해서 씀 … [개인용 복사]」 | 일반 사용자 회사 층 | 읽기 전용 이유와 할 일을 한 줄로 알려 줌 | + +## ⑥ 품셈 보여 주는 꼴 (덧 · 내역 6) + +사진: `Aislo_M02_로직추가창.png` · `Aislo_M02_원문띠.png` · `Aislo_M02_실무호표_확인창.png`. + +| 물음 | 실측 | +| --- | --- | +| 품셈 책 꼴인가 · 코드 목록 꼴인가 | **고를 때는 코드 목록 꼴.** 「로직 고르기」 창 = 종류(전체 · 단가산출 · 일위조합 · 실무 호표) · 구분(산림품셈 · 건설품셈 공통/토목/건축/기계설비/유지관리 · A 횡단배수 ~ G 노면공 · 실무자료) · 상세구분 · 찾기 · 층 단추 → 줄 = 「단가산출 · 돌쌓기 찰쌓기(인력) · GF000221 · 산림품셈 13장 구조물 · 원/㎡」. 1,471~1,609 줄. **담은 뒤에는 책 꼴** — 원문 띠에 「산림품셈 13-2-3 큰돌 채집 (단위: 100개당)」 표(작업반장 2.6 인 · 굴착기 13.4 h …)와 [주] ①~⑪. | +| 고르기 전에 품 · 수량이 보이나 | **안 보임.** 줄을 누르면 바로 담기고 창이 닫힘(`M02_MasterTemplete_Labor_Pick.ts:161-164`) — 미리보기 · 원문 보기 없음. 단위만 보임(원/㎡ · 원/10㎡). 실무 호표 줄만 「제안 2/6」이 덧붙음. | +| 담은 뒤 품이 보이나 | 일위대가 표엔 품(인 · 시간)이 없음 — 이름 · 옵션 요약(「뒷길이 55cm이하 · 켜쌓기 90%」) · 금액 넷만. 품은 원문 띠(책) · 다이어그램(넣을 값 → 노무비 · 재료비 · 경비)으로 봄. 품이 숫자로 나란히 보이는 곳은 실무 호표 확인 창의 「품 비교」 칸 하나뿐. | +| 복사해 와서 고칠 수 있나 | **품은 못 고침.** 일위대가 줄에서 고치는 것 = 옵션(넣을 값) · 수량산출 · 별칭 · 순서 · 빼기. 로직(품) 고치기 · 복사는 M01 시스템 관리자만(1-C ⑥ 6-2). 엑셀에서 가져온 품도 우리 품으로 바뀜(≠ 는 버려짐). 「복사해 와서 품만 고치는」 길 없음. | +| 낱말 | 「석축」 → 헐기 2 건뿐 · 쌓기는 「돌쌓기 찰쌓기」 · 「큰돌쌓기 찰쌓기」 · 「찰쌓기(굴착기+부착용 집게)」 넷으로 흩어짐(산림 · 건설 품셈 둘) — 실무자는 둘 중 무엇을 골라야 할지 판단 필요. | + +## 브레인 가설 확인 거리 (계산형 · 고정형) + +- 실무 엑셀 일위대가 = 줄마다 「단가산출 N 참조 × 수량」(울진 간선 호표 17: 찰쌓기 52,995 원/㎡ × 1.57 ㎡ · 고임돌채집 24,816 × 0.24 …). 규격 변형 = 같은 줄에 수량만 다름. +- 아이슬로 설계가 조합에서 쓰는 것(추정): 단위당 금액(일위대가 합계) · 자재 수량(B08 자재) · 넣을 값(설계가 넘기는 관 길이 · 높이 · 측점 — 3-C ⑥-1). + - 설계가 「높이로 조합을 고르고 길이를 곱함」인 구조물(석축 · 돌기슭막이 · 측구) → 고정형(엑셀 숫자)으로도 금액 · 수량이 이어질 것으로 추정. + - 설계 값이 로직 안 품 · 수량을 바꾸는 구조물(관 길이 → 관 · 보호공 수량 등) → 계산형 필요(추정). +- 잃는 것(고정형): 노임 새 판 반영 · 품셈 개정 따라가기 · 넣을 값 옵션. 확인 창 「품 비교」로 고정 ↔ 계산 차이를 보여 줄 수 있음. +- 지금 코드엔 고정형 줄 자리가 없음 — 가까운 것은 개인 「신규 자재」(이름 · 규격 · 단위 · 값 → 재료비 단가 하나 · `M02_Material_Own.py:1-8`). + +## 남은 것 · 한계 + +- 파일 올리기 · [사전조합 생성] · 새 항목 [확인] · 복사 [확인]을 누르지 않아 K1~K4 뒤 단계는 코드 읽기 · 추정. 도면(CAD) 누름 수는 안 셈. +- K6 은 3-C 사진 · 코드로 셈(검증_sub7 은 구조물 조합이 아직 안 이어짐). +- 일반 사용자 화면은 M02 첫 화면 · 회사 층만 봄(브레인검증회사 · 비밀번호 출력 없음). +- ORCA 탭 저절로 다시 읽힘(10~15 초 · 원인 모름) — 지난번 「탭이 사라짐」과 같은 뿌리로 추정. diff --git a/docs/raw/요구_내역서/_분석_내역7_기준단가산출_대조.md b/docs/raw/요구_내역서/_분석_내역7_기준단가산출_대조.md new file mode 100644 index 000000000..cb9c43ea5 --- /dev/null +++ b/docs/raw/요구_내역서/_분석_내역7_기준단가산출_대조.md @@ -0,0 +1,151 @@ +# 내역 7 — 경쟁사 기준 단가산출 · 일위대가 자료의 생김새 · 아이슬로 M01 대조 (sub6 · 10-31) + +읽기 전용 조사. 경로 줄임: +- `ST/` = `sub_laptop_8/resources/knowledge/original/경쟁사 프로그램/STmate 분석/` +- `EST/` = `sub_laptop_8/resources/knowledge/original/경쟁사 프로그램/EST Plus 2/REA 분석/` +- `DS/` = `sub_laptop_8/resources/knowledge/original/원가계산/다산소프트/` (EST 2026 하반기 배포본 `2026-07-main.estx` 있음) +- `MD/` = `sub_laptop_6/resources/master_data/` +- 「main 직접 집계」 = 이번 조사에서 `2026-07-main.estx` 를 메모리에서만 풀어 센 값(파일 안 만듦 · 줄 번호 없음). + +## ① 결론 다섯 줄 + +1. 경쟁사 기준 자료 = **공급사가 반기마다 통째로 배포하는 「완성된 호표 묶음」** — EST 는 main 하나(산근 1,569 · 일위 1,468) + 공종별 설계참고철 13 개, STmate 는 조달청 표준일위대가를 다시 담은 다섯 파일(토목 · 건축 · 기계설비 · 전기 · 통신 · 암호화). +2. 산근 하나 = **숫자를 박은 글 문서** — 품셈 표 값 · q·K·f·E·Cm 을 숫자로 적고 노임 단가를 곱해 내려씀. 표를 부르는 구조 없음. 규격(관경 · 뒷길이 · 굴착기 크기)마다 호표를 **따로 늘어놓음**(EST 흄관 D250~2000 = 18 건 · 터파기 87 건). +3. 일위대가도 공급사가 줌(EST 1,468 · STmate 표준일위대가). 다만 실무 내역서의 몸통은 산근(실무 3 건 산근 61~74 · 일위 3~14) — 일위는 취수장 · 횡단개거 같은 현장 복합 구조물에만. 실무 일위대가는 「기준 복사 → 이름 · 규격 · 수량 고침」으로 생김. +4. 아이슬로 M01 = **조항 하나 = 로직 하나 · 규격은 입력 칸 · 품셈 표는 따로 두고 부름**(로직 1,469 중 1,399 가 `찾기(표…)`) — 계산 근거는 더 곧고 개정 대응도 쉬움. 대신 실무자가 늘 보던 「석축쌓기 | 메쌓기, T=35cm이하 | ㎡ · 80,663 원」 같은 **이름 · 규격 · 금액이 정해진 한 장**이 없음. +5. 바꿀 것 후보(제안만): 로직은 그대로 두고 **「대표 규격으로 펼친 보기 목록」**(품명 · 규격 · 단위 · 금액)과 **「계산서 꼴 보기」**(경쟁사 산근 글 모양으로 숫자를 채워 찍음)를 겉에 더함 — 숫자 박기로 되돌리지 않음. + +## ② 경쟁사 기준 자료 생김새 + +### 2-1 EST Plus 2 + +| 물음 | 생김새 | 근거 | +|---|---|---| +| 몇 개 | main.estx 하나: 산근 1,569 · 산근 줄 77,329 · 일위 1,468 · 일위 줄 10,506 · 기계 1,049 · 자재 23,987 · 노임 395 | `DS/ESTX_분석.md:26-27` · `DS/ESTX_테이블사전.md:11-12` | +| 덧 묶음 | 「2026_상반_설계참고철」 공종별 13 파일(가시설 · 건축 · 교량 · 단지 · 도로 · 산림토목 · 설비 · 용역비 · 전기 · 조경 · 측량 · 하천 · 해양) · 형식은 보통 프로젝트 .estx 와 같음 | `EST/01_전체구조/installation_inventory.json:153-231` | +| 설계참고철 크기(상한) | 산근/일위 — 산림토목 422/101 · 도로 308/259 · 하천 418/260 · 전기 0/9,917 · 설비 0/4,786 → 토목은 산근 위주, 설비 · 전기는 일위 위주 | `EST/06_정적교차검증/all_bundle_layout_candidates.json` (머리 크기로 계산 · 추정) | +| 묶는 법 | 장 · 조항 계층 없음 — **평평한 목록**. 차례만 공종 흐름(콘크리트깨기 → 터파기 81~167 → 석축쌓기 428~ → 흄관 655~) | main 직접 집계 | +| 이름 | 품명 · 규격 · 단위 세 칸. 예 `석축쌓기 \| 메쌓기, T=35cm이하 \| ㎡`. 코드 = 접두 글자 + 10 자리(산근 D · 일위 U · 자재 M · 노임 L · 기계 E) · 품셈 조항 번호는 코드에 안 들어감 | main 직접 집계 | +| 산근 안의 꼴 | 자유 글 문서 · 한 줄 = 한 레코드 · 중앙값 24 줄(최대 190). 글 안 숨은 표지로 품명 · 단위 · 연결 단가 · 앞 결과 참조 · 합계를 엔진이 읽음 | `EST/09_수식집계_변경저장_추적_20261029/ESTPlus2_산근LineData_수량연결_정적분석_20261029.md:18-29` · `EST/ESTPlus2_REA_최종보고서_20261030.md:66-68` | +| 변수 | `q = 0.7㎥ f = 1/1.24 = 0.81 K = 0.9` 꼴 — 글자는 사람 보라고 적은 이름, 실제로 넘어가는 것은 숫자(추정). 「전역변수」 메뉴는 있음 | 위 문서:47 · `EST/02_화면구성/captions.json:8896` | +| 숫자 박기 대 표 부르기 | **숫자 박기** — `3인 / 23㎡` · `(3.6+2.7+2.2)/3` · `Cm = 20sec`. 품셈 표를 부르는 길 없음. 「품셈자료 붙여넣기」 = 품셈책 글을 붙이는 기능(추정) | main 직접 집계 · `installation_inventory.json:429-555`(BookData .blb) | +| 규격 변형 | 규격마다 **따로 한 건씩** — 흄관(소켓관) D250~2000 18 건 · (수밀밴드) 18 건 · 터파기 87 건 | main 직접 집계 | +| 조항 출처 | 칸 없음 · **본문 주석 글**로 — `< 공통품셈 8-2-15 >` · `< 2025년 품셈 개정 >` · `2. 메쌓기 (26년 품셈개정)`. 1,569 중 973 건에 「품셈」 · 「n-n-n」 글 있음 | main 직접 집계 | +| 반기 배포 | main 공지 41 행: 노임(협회 2026.1.1) · 중기기초자료 2026.7.1 · 환율 · 유류(석유공사 26년 6월) · **「2026년 개정품셈 적용」과 개정 항목 목록**(건설기계 · 자재손율 · 돌쌓기 · 돌붙임 · 전석쌓기 …) · 시중물가 · 관급 기준일 · 표준시장단가 → **단가와 품셈 개정 둘 다 반영해 다시 배포** | main 직접 집계 · `DS/_meta.md:3,19-20` | + +### 2-2 STmate + +| 물음 | 생김새 | 근거 | +|---|---|---| +| 몇 개 | 기준자료 패키지(.STBSC) 28 개 · 그중 표준일위대가 = `ST_BSC\일위대가\2026\` 다섯 파일(토목 199,599 B 가 가장 큼) | `ST/20_분석/02_데이터셋과_파일형식.md:10,124` · `ST/30_원자료/system_inventory.csv:119-123` | +| 정체 | 안내 파일 「2026년 상반기 조달청 표준일위대가 / 발표일 2026.01.27 공사원가통합관리시스템(03.17 수정배포)」 — **조달청 자료를 STmate 꼴로 다시 담은 것** | `ST/30_원자료/system_structures.json:31434-31458` | +| 안 보이는 것 | 내용 **암호화** — 호표 수 · 차례 · 식 글 직접 확인 못 함. 산근 전용 패키지는 목록에 없음(일위대가 안에 같이 든 것으로 추정) | `ST/20_분석/29_암호화자료의_뼈대.md:81-102` · `system_inventory.csv:39-129` | +| 이름 | 명칭 · 규격 · 단위 세 칸. 코드 = 대가 B · 산근 D · 중기 X · 노무 L · 자재 M · 경비 S + 프로젝트 안 순번(`B00004 깬잡석찰쌓기`). 출력은 「산근 4호표」 「대가 1호표」 | `ST/20_분석/20_설정파일과_보고서_카탈로그.md:79-83` · `ST/30_원자료/stmate_code_name_list.csv:5,178` | +| 산근 안의 꼴 | 줄 글 식 — 자원 참조 `&X00006L&` · 품+단위 `0.31'Kg'` · 할증 `5'%'` · 중간 이름 `:=A01` · `Q=` 식 | `원가계산/STmate/SIxMemo_구조.md:28-52` · `ST/20_분석/23_COSTN_SMT_전수파싱.md:12-31` | +| 변수 | 이름(q · K · f · E · Cm · V1~ · t1~)을 적되 **값은 숫자**(`q=0.7 , K=0.9 , E=0.45-0.05= 0.40`). 사용자 등록 「중기공식」 — Q 식 10 꼴 · Cm 식 54 꼴 | `ST/20_분석/26_편집기와_사이클식.md:27-38` · `23_…:56-70` · `28_변환_수량산출_식언어.md:36-43` | +| 규격 변형 | **호표를 따로** — 기슭막이 H=2.0/2.5/3.0 = B00010/B00011/B00047 · 파형강관 D800/D1200 = D00020/D00039 · 구성은 같고 수만 다름 | `stmate_code_name_list.csv:11-12,48,178,195` · `ST/20_분석/31_일위대가_레시피_사전.md:89-96` | +| 조항 출처 | 일부만 · 두 꼴 — 산근 머리 글(`건설표준품셈 3-1-3(터파기),11-3(굴삭기) 적용`) · 일위대가 안 수량 0 줄(`건설표준품셈 \| 7-1-2(찰쌓기) \| \| 0.0`). 350 중 82 만 있음 | `31_…:112` · `ST/20_분석/32_특수산식과_현행품셈_대조.md:50` | +| 반기 배포 | 노임기계 반기마다(25 상 · 하 · 26 상 · 하) · 표준일위대가는 26 상반기 한 판. 품셈 개정이 들어가는지는 확인 못 함(조달청이 해마다 개정 반영하므로 따라 들어감 — 추정) | `02_…:130-140` · `31_…:18` | + +## ③ 일위대가 기준 자료 + +- **공급사가 일위대가까지 줌** — EST main 일위 1,468 건 · STmate 조달청 표준일위대가 다섯 파일(`ST/20_분석/21_정정과_트랙통합.md:116-124`). +- EST 는 같은 항목을 산근 · 일위 **두 벌로** 주기도 함 — 흄관(소켓관) D=600 = 산근 91,198 원 · 일위 91,050 원(`DS/ESTX_분석.md:27`). +- 일위대가의 꼴 = **식 없는 격자** — 품명 · 규격 · 단위 · 수량(고정 숫자) · 단가 · 연결(자재 · 노임 · 기계 · 산근 · 다른 일위). 수량 근거는 규격 칸에 글로(`석공 | 3인/27㎡ | 0.111`)(main 직접 집계). +- 실무에서 쓰는 비중: EST 실무 내역서 3 건 = 산근 61~74 · 일위 3~14 — **단가 몸통은 산근**, 일위는 현장 전용 복합 구조물(취수장 · 횡단개거 · 기슭막이)(main · 실무 estx 직접 집계). +- 실무 일위대가가 오는 길: 기준을 가져와 **이름 · 규격 · 수량을 고쳐 프로젝트 사본**으로 씀 — 실무 코드가 D0000000173 처럼 작은 번호(새로 매김 · 추정). 같은 파형강관도 main 은 일위 「파형강관부설및접합」, 실무는 산근 「파형강관 매설」. +- STmate 레시피 사전 350 조합의 출처 = **공급사 자료 아님** · 임도 · 사방 실무 설계서 출력 엑셀 6 개의 「일위대가표 · 중기사용료 · 단가산출근거」 시트에서 뽑은 526 호표 → 겹침 걷어 350(일위 132 · 산근 136 · 중기 82)(`ST/40_스크립트/recipe_extract.py:19,162-173` · `ST/30_원자료/일위대가_레시피_감사.txt:1-10` · `ST/20_분석/31_…:4,24-31`). 수는 그 설계서 당시 판 값 — 현행 원문과 맞음을 확인한 것 2 개뿐(`32_…:30-44`). +- 아이슬로도 같은 실무 호표를 「일위대가 사전조합 · 실무 일위대가」 36 개로 이미 옮겨 둠 — 로직만 담고 수량 · 비율은 프로젝트 몫(`MD/일위대가조합.json:4` · 예 UA000004 기슭막이 `:61`). + +## ④ 복사 · 고치기 꼴 (3-A · 3-B 와 겹치는 것은 줄만 가리킴) + +| 걸음 | EST | STmate | 아이슬로 | +|---|---|---|---| +| 기준 찾기 | 통합검색 — 일위 · 산근 · 기계 · 자재 체크 · 「Main Data에서 검색 / 사용자 지정 폴더」 · 여러 개 골라 [적용] · 「Main값우선」(`EST/02_화면구성/captions.json:11605,19179,25234`) | 단가목록 `wM_JabPum` — 코드 종류로 거름 · 「명칭,규격 / 산근내용」 검색(`ST/20_분석/34_화면구조_정리.md:56`) | M01 단가산출 로직 목록 · 장 · 소유 · 찾기로 거름(3-C ② M01) | +| 복사되는 곳 | 프로젝트 파일 안 사본 · 코드 새로 매김(추정) | 프로젝트 단가 원장(B · D 코드 행 + 산출 글)(`ST/20_분석/04_계산_데이터계보.md:50`) | 「본떠 만들기」 → 자체 로직(새 키) · 정본은 못 고침(`docs/raw/M01_쉬운단가/쉬운단가_검토_D_지난시도.md:24,40`) | +| 산근 고치기 | 워드 같은 글 편집 + 오른쪽 메뉴로 줄 성격 지정 — 3-A ② 「단가산출 편집기」(`_분석_내역3-A_EST_화면.md:60-69`) | 식 글 편집기 + 문법 오류 목록 — 3-B ② 3(`_분석_내역3-B_STmate_화면.md:43-52`) | 별칭 꼴 식 줄 고침 · 표 바꾸기 · 시험 계산(`M01_MasterData/M01_MasterData_UI_LogicLab_Copy.ts:3-8`) | +| 일위 고치기 | 격자 — 3-A ② 「일위대가」(`…3-A…:48-58`) | 격자 — 3-B ② 2(`…3-B…:32-41`) | M02 사전조합 화면(3-C ② M02) | +| 반기 갱신 | 「Main Data로 갱신 / 선택파일로 갱신」 · 「되돌릴 수 없음」 경고(`captions.json:9974,10016`) | `wM_Mrg_BSC` — 맞춤 열쇠(코드 · 명칭 · 규격 · 단위) 고르고 명칭 · 단가를 「내 것 유지 / 새 판으로」(`ST/20_분석/22_UI_폼_분석.md:116-124`) | 요소(노임 · 자재 · 기계) 판만 바뀜 → 로직은 그대로 새 값으로 다시 셈(`MD/_갱신절차.md` 로직 절) | + +- 경쟁사 공통점: 사용자가 고치는 대상 = **숫자와 글**(수량 · 이름 · 규격). 계산 틀을 고친다는 감각이 없음. +- 아이슬로 차이: 고치는 대상 = **식**(찾기 · 만약) — 1-C ⑥ 「고치기가 끝내 식 글자로 남음」과 같은 뿌리(`_분석_내역1-C_아이슬로현황.md:141-146`). + +## ⑤ 같은 항목 셋 대조 + +### 5-1 석축(돌쌓기) 메쌓기 + +| | EST 기준(main) | STmate(실무 호표) | 아이슬로 M01 | +|---|---|---|---| +| 이름 | 산근 D0000002783 `석축쌓기 \| 메쌓기, T=35cm이하 \| ㎡` · 80,663 원 | `돌쌓기 \| 메쌓기,L3=35cm이하 \| m2` | GF000220 「돌쌓기 메쌓기(장비)」 · 원/㎡ · 산림품셈 13-4-2 (`MD/로직_산림품셈_13장_구조물.json:329`) | +| 규격 늘어놓기 | 메 · 찰 × T=35/55/75 = 산근 6 건 | 뒷길이마다 따로 | 로직 하나 · 입력 칸 「뒷길이」 하나 | +| 몸통 | `석공 : 268,908 × 3인 / 23㎡ = 35,074.9` · `보통인부 : 172,068 × 1인 / 23㎡` · `굴착기 Q = 23㎡ / 8hr = 2.88 ㎡/hr` → `59,020 ÷ 2.88` | 근거줄 `7-1-1(메쌓기)` · 석공 0.1 · 보통인부 0.05 · 굴착기 0.39 시간 · 끝에 `켜쌓기적용 \| 전체금액의 % \| 90.0` | 석공 = `찾기(QF000431, 명칭='석공', 뒷길이=뒷길이).수량` · 보통인부 · 굴착기+집게 같은 꼴 · 잡석 = 입력 「잡석량」 | +| 품셈 값 | 3인/23㎡ 등 **글 속 숫자** | 0.1 · 0.05 **숫자** | 표 QF000431(뒷길이 35/55/75 × 석공 0.10/0.09/0.08 …)을 **부름**(`MD/소요량_산림품셈_13장_구조물.json:261`) | +| 조항 표기 | 본문 주석 「(26년 품셈개정)」 | 근거줄(수량 0) | 칸 「출처」 · 입력마다 출처(지역 · 보정작업 = 건설 공통 8-1-7) | +| 더 하는 것 | — | — | 지역 · 보정작업이 기계 경비까지 이어짐 · 원문 주(높이 3m 초과 증가율 · 13-4-1 주③)를 「중간」 식으로 둠(GF000219 `:286`) | + +근거: EST main 직접 집계 · STmate `ST/30_원자료/일위대가_레시피.json:8504-8590` · `stmate_code_name_list.csv:11,17`. + +### 5-2 파형강관(흄관) 부설 + +| | EST 기준(main) | STmate(실무 산근 · 일위) | 아이슬로 M01 | +|---|---|---|---| +| 이름 | 일위 U0000001121 `파형강관부설및접합 \| D=600m/m \| M` · 12,328 원 (흄관은 산근 D=250~2000 18 건) | 산근 D00020 `파형강관 부설 및 접합 \| D=800mm \| M` · 일위 `파형강관부설 \| D=800mm \| m` | GC000404 「파형강관 부설 및 접합」 · 원/본 · 건설품셈 토목 6-7-2 (`MD/로직_건설품셈_06장_관부설_및_접합공사.json:1559`) | +| 규격 늘어놓기 | D=250~1500 **11 건 따로** | D800 · D1000 · D1200 따로 | 로직 하나 · 「관경」 고르기 11 값 | +| 몸통 | 파형강관 1 본(단가 0 · 별도) · 배관공 0.17 · 보통인부 0.08 · 공구손료 2% · 크레인 5Ton 0.2 hr · `=> M당 적용금액 \| 소계÷6M \| 0.16667` | 근거줄 `6-7-2` · 파형강관 별도계상 · 배관공 0.04 · 보통인부 0.02 · 공구손료 2% · 크레인 0.0416 시간(이미 m 당으로 나눈 값) | 배관공 = `찾기(QC000699, 관경=관경).배관공_수도 * 요율` · 크레인 = 고른 크레인 로직 × (1 + 8m 직관 가산 %) · 덧줄 공구손료 = 인력품의 2% | +| 품셈 값 | 숫자 | 숫자 | 표 QC000699(관경 11 줄 × 배관공 · 보통인부 · 크레인)를 부름 · 주 ①~⑥ 원문째 보관(`MD/소요량_건설품셈_06장_관부설_및_접합공사.json:736`) | +| 단위 | m 당(본 ÷ 6 m 를 줄로 박음) | m 당 | **본 당** — m 당은 쓰는 쪽(조합 · 수량)에서 나눔 | +| 더 하는 것 | — | — | 시공조건 A · B · C 요율(6-1-1 8) · 크레인 규격 고르기(주④) · 8 m 직관 10% 가산(주③) · 지역 | + +근거: EST main 직접 집계 · STmate `ST/30_원자료/일위대가_레시피.json:5196,5224,10752-10870`. + +### 5-3 (구조물) 터파기 + +| | EST 기준(main) | STmate(실무 산근) | 아이슬로 M01 | +|---|---|---|---| +| 이름 | 산근 D0000000812 `터파기 \| 백호(0.7㎥)90%+인력10% \| ㎥` · 9,764 원 | 산근 D00023 `구조물터파기(육상토사0~1m) \| 기계90%+인력10%(굴삭기 0.7M3) \| M3` | GF000101 「구조물터파기 육상토사(0~1m)」 · 원/㎥ · 산림품셈 9-13-1 + 작업량 GF000100 (`MD/로직_산림품셈_09장_토공.json:1057,1074`) | +| 규격 늘어놓기 | 87 건(인력 × 토질 5 · 백호 0.12~1.0 × 비율 · 육상 · 용수 · 수중) | 조합마다 따로 | 조항(깊이 · 육상/용수 · 토사/암)마다 로직 — 산림 9-13 만 18 로직 + 작업량 6 | +| 몸통 | `q = 0.7㎥ f = 1/1.24 = 0.81 K = 0.9 E = 0.6 Cm = 20sec` → `Q = 55.11 ㎥/hr` → `노무비 59,020 ÷ 55.11 × 0.9` · `특별인부 226,122 × 1인 / ((3.6+2.7+2.2)/3) × 0.1` | `▣임도표준품셈 5-1-2 가.육상토사(0~1m) 적용` · `q=0.7 , K=0.9 , E=0.40 / f=1/1.175 / Cm=20` → `Q=38.56` · `경비 23,128 / Q * 0.9` · `보통인부 165,545 * A(0.2) * 0.1` | 작업량 = `반올림(3600 * q * K * f * E / Cm, 3)` · q · K · f · E · Cm 을 각각 표에서 찾기 · 보통인부 = 표 값 × 인력비율 · 굴착기 = 기계비율 ÷ 작업량 | +| 품셈 값 | 숫자(q · K · E · Cm 모두) | 숫자 · 옛 판 값(굴삭기 0.4㎥ 판과 어긋나 값 복사 금지 판정 · `ST/20_분석/33_임도품셈_판_대응표.md:30`) | 표 QF000270 · 계수 CF000127(인력 · 장비 비율)을 부름 | +| 계산 근거 보기 | **한 장에 식 · 숫자 · 결과가 다 보임** | 같음 | 흐름도 · 시험 계산으로 봄 — 한 장 계산서 꼴 없음 | + +근거: EST main 직접 집계 · `EST/09_…/linedata_sample_precision_check.json`(엔진 재현 72/72) · STmate `ST/30_원자료/특수_단가산출_21개.json`(호표 21) · `stmate_code_name_list.csv:181`. + +### 5-4 아이슬로가 더 하는 것 · 덜 하는 것 + +더 하는 것: +- 품셈 표 · 계수를 **따로 두고 부름**(로직 1,469 중 1,399 · 소요량 표 2,535) → 품셈 개정 때 표만 바꾸면 그 표를 부르는 로직이 다 바뀜. 경쟁사는 공급사가 호표 글을 다시 써서 다시 배포해야 함. +- 규격 · 조건을 **입력 칸**으로(로직 1,189 개가 입력 가짐) — 관경 · 뒷길이 · 시공조건 · 지역 · 보정작업. +- 원문 주(할증 · 가산 · 상한)를 식으로 담고 못 담은 것은 「비고」에 「미반영 · 설계자 판단」으로 적음(GF000101 비고 「관보호공 30% 가산 미반영」). +- 설계 값이 그대로 들어옴 — 잡석량 · 높이 · 관경이 B06 · B08 설계에서 넘어옴(경쟁사는 사람이 손으로 침 · 내역 7 원문 `사용자요구_내역서.md:37-39`). +- 흐름도 · 시험 계산 · 조항 출처 칸이 줄마다 있음. + +덜 하는 것: +- **이름 · 규격 · 금액이 정해진 기준 목록**이 없음 — 실무자가 찾는 「석축쌓기 | 메쌓기, T=35cm이하 | ㎡ · 80,663 원」 같은 줄이 목록에 안 보임(M01 목록 = 키 · 조항 · 로직 이름 — 금액 칸 없음). +- **한 장 계산서 꼴**이 없음 — 경쟁사는 산근 글 그대로가 제출 서류 「단가산출근거」. 아이슬로는 B09 에서 찍지만 M01 에서 기준을 볼 때는 식 · 흐름도. +- 일위대가 기준 묶음이 얇음 — EST 1,468 · 조달청 표준일위대가 대 아이슬로 사전조합 37(실무 36). +- 반기 「무엇이 바뀌었나」 공지 · 개정 항목 목록이 없음(EST main 공지 41 행 같은 것). +- 운반 · 채집 · 잔토처리 같은 실무 자주 쓰는 산근 일부가 「우리 로직에 없음」(`MD/일위대가조합.json` 비고들 · 예 `:58`). + +실무자 눈에 읽기 쉬운 쪽: +- **보기(검토 · 제출)** = 경쟁사 쪽이 쉬움 — 숫자 · 노임 단가 · 결과가 한 장에 줄줄이. 계산기로 따라 셀 수 있음. +- **고치기 · 규격 바꾸기** = 아이슬로가 덜 손 감 — 관경만 바꾸면 끝. 경쟁사는 다른 호표를 찾아 바꾸거나 숫자를 손으로 고침(STmate 실무 호표 82/350 만 출처 · 옛 판 값 섞임). +- **낱말** = 경쟁사 쪽이 익숙 — 「산근 · 일위 · 호표 · 품명 · 규격」. 아이슬로 M01 은 「로직 · 찾기 · 중간 · 덧줄 · QF · GF」(3-C ⑥-2 ★ 목록 `_분석_내역3-C_아이슬로_화면.md:126-137`). + +## ⑥ 바꿀 것 후보 (제안만 · 고치지 않음) + +| # | 무엇 | 경쟁사 꼴 | 아이슬로에 둘 꼴(후보) | 크기 | +|---|---|---|---|---| +| 1 | 이름 | 품명 \| 규격 \| 단위 세 칸 · 「석축쌓기 \| 메쌓기, T=35cm이하 \| ㎡」 | 로직 이름은 두고 보기 목록에 **품명 · 규격 · 단위 세 칸**을 더함 — 규격 = 고른 입력 값을 이어 씀(「뒷길이 35㎝ · 장비」) | 작음 | +| 2 | 묶음 | EST 평평한 목록(공종 흐름 차례) · STmate 공종 트리 추정 | 장 · 조항 차례는 두고 **공종 갈래**(토공 · 돌공 · 관 · 콘크리트 …) 거름을 더함 — 임도 · 사방 자주 쓰는 것 먼저 | 작음 | +| 3 | 규격 늘어놓기 | 규격마다 한 건(흄관 18 · 터파기 87) | 로직은 하나로 두고 **대표 규격 펼친 보기**(관경 11 줄 · 뒷길이 3 줄)를 자동으로 만들어 금액까지 보여 줌 — 실무자는 줄을 고르고, 속은 같은 로직 + 입력 값 | 중간 | +| 4 | 화면 꼴(계산서) | 산근 글 = `석공 : 268,908 × 3인 / 23㎡ = 35,074.9` | 로직 · 표 값으로 **경쟁사 산근 글 모양의 「계산서 보기」**를 찍음(식을 숫자로 풀어 노임 단가 × 품 = 금액 · 맨 위에 「산림품셈 13-4-2 · 2026 판」) — 읽기 전용 | 중간 | +| 5 | 숫자 박기 대 표 부르기 | 숫자 박기 | **표 부르기 그대로**(개정 대응 · 규격 바꾸기의 힘) · 보이는 쪽만 숫자 — 4 의 계산서 보기가 「박은 숫자」 노릇 | 바꾸지 않음 | +| 6 | 출처 표기 | 주석 글 · 근거줄(수량 0) | 이미 칸 있음 · 계산서 보기 맨 위 한 줄로 찍음 | 작음 | +| 7 | 일위대가 기준 | 공급사 일위 1,468 | 실무 사전조합 36 을 「대표 일위대가」 이름(품명 · 규격 · 단위 · 대표 수량)으로 보이게 · 조달청 표준일위대가 원천을 따로 받을지는 사용자 판단 | 중간~큼 | +| 8 | 반기 공지 | main 공지 41 행(개정 품셈 항목 목록) | 판 바뀔 때 「바뀐 요소 · 바뀐 표 · 영향 받은 로직 수」 한 장 | 중간 | +| 9 | 낱말 | 산근 · 일위 · 호표 | 화면 낱말만 「단가산출(산근)」 · 「일위대가」로 맞춤 · 「로직 · 찾기 · 덧줄」은 관리자 화면 속으로 | 작음 | + +남은 것 · 한계: +- STmate 공급 원본은 암호화 — 실례는 모두 실무 프로젝트 평문(공급사 원본과 같은지 모름). +- EST 설계참고철(산림토목 등) 13 파일은 이 노트북에 없음 — 크기 상한만. +- EST 수치는 이번에 main.estx 를 메모리에서 풀어 센 값 — 지식DB 에 줄 번호 근거로 남은 것 아님. diff --git a/docs/raw/요구_내역서/_종합_내역3_조합안.html b/docs/raw/요구_내역서/_종합_내역3_조합안.html new file mode 100644 index 000000000..d88849fb7 --- /dev/null +++ b/docs/raw/요구_내역서/_종합_내역3_조합안.html @@ -0,0 +1,183 @@ + + + + +내역서 화면 조합안 + + + + +

내역서 화면 조합안 — 누름과 고민 줄이기

+
한시 브레인 sub8 · 10-31 · 근거 = EST · STmate 화면 설계도 분석 + 아이슬로 실측 (자세한 것: docs/raw/요구_내역서/_종합_내역3_조합안.md)
+ +
+ 한 줄 결론 — 아이슬로는 「설계에서 저절로 오는 것」(수량 · 구조물 묶음)이 경쟁사보다 앞섬. + 막히는 곳은 「고쳐 쓰기」(품 고치기 · 복사해 새로 · 지난 공사에서 가져오기 · 금액 바로 보기). + 경쟁사의 고쳐 쓰기 화면을 B09 한 화면에 붙이면, 막힌 일 셋이 3~6번 누름으로 줄어듦. +
+ +

1. 대표 일 여섯 — 몇 번 누르나

+ + + + + + + + +
일ESTSTmate아이슬로 지금조합안 목표
T1 새 공사 내역서 시작8~11약 30
지난 공사 복사 12
설계 단계를 지나면 수량이 차 있음 · 첫 화면이 빈 원가계산서첫 화면 = 설계내역서
T2 일위대가 품 고쳐 이 공사만5~77막힘 품 칸이 어디에도 없음3
T3 규격만 다른 일위대가 새로8~11약 1612~13
페이지 두 번 옮김 · 숨은 목록표
4
T4 지난 공사 일위대가 가져오기9~14약 14막힘 화면 길이 없음4
T5 노임 새 판 · 고친 값 지킴12~15
+ 내 값 다시 넣기
약 20
고친 것을 기억
2 + 구조물마다2 + 확인
T6 줄 추가 → 단가 → 금액 → 총액7~8약 11막힘 10번 눌러도 금액 안 보임 · B09 에 [저장] 없음6
+
경쟁사 = 실행이 안 되어 화면 설계도로 센 추정 · 아이슬로 = 검증_sub7 에서 실측 · 목표 = 조합안 기준 추정.
+ +

2. 무엇을 살리고 무엇을 빌리나

+
+
+

아이슬로 — 이미 앞선 것 (그대로)

+ +
+
+

경쟁사 — 빌려 올 화면 장치

+ +
+
+
+

경쟁사 약점 — 따라 하지 않음

+ +
+ +

3. 조합안 — B09 「내역서 작업대」 한 화면 (모형)

+
+
+ B09 원가계산 · 설계내역서 + 총액 412,350,000원 (빈 줄 3 — 아는 금액까지)   원가계산서 저장 +
+
+
+
데이터 점검
+
빈 줄 3 ▸
+
단가 새 판 [업데이트]
바뀌는 줄 12 · 고친 칸 2 지킴
+
공종 목록
+
1 토공사
2 배수공
3 구조물공
+
+
+
+
명칭 · 규격수량단위단가금액
+
절토 (토사)1,924㎥3,1206,002,880
+
석축 쌓기 H=2.0140㎡58,4108,177,400
+
측구 터파기140㎥4,870681,800
+
+
+
▾ 석축 쌓기 H=2.0 — 일위대가 구성 (줄을 누르면 펼침)
+
품명품 · 수량단가품셈 값
+
석공0.080 인288,0000.072 인 [품셈 값으로]
+
보통인부0.150 인171,000같음
+
굴착기 0.7㎥0.020 hr98,500같음
+
노란 칸 = 내가 고친 칸 · 이 공사에만 · 새 판이 와도 지킴
+
+
+
+
재료 창고
+
이 공사내 것회사품셈지난 공사
+
🔍 석축
+
석축 쌓기 H=1.5 · ○○임도 2025
+
석축 쌓기 H=2.0 · △△임도 2026
+
두 번 누르거나 끌어서 내역에 넣기
+
우클릭
항목 추가 · 줄 삭제
복사해 새 일위대가
내 자료집에 저장
어디 쓰였나 찾기
+
+
+
+
모형의 숫자는 보기용. 매일 하는 일 = B09 한 곳 · M02 = 내 자료집 정리 · M01 = 관리자 바탕(품셈 기본값). 구조물 고르기(B06 · B08)와 수량 자동은 그대로.
+ +

4. 단계 후보

+
    +
  1. 작은 것부터 (바로 효과) — 첫 화면을 설계내역서로 · 빈 줄 숫자 하나로 · 원가계산서는 아는 금액까지 · B09 [저장] · 연결하면 금액 바로 · 헷갈리는 단추 이름 · 낱말(★ 서른여 개) 정리 → T6 · 첫인상
  2. +
  3. 핵심 — 줄 펼쳐 셀에서 고치기(고친 칸 색 · 품셈 값 곁에 · 되돌리기) · 우클릭 「복사해 새 일위대가」 → T2 · T3
  4. +
  5. 재료 창고 — 지난 공사 · 내 것 · 회사 · 「내 자료집에 저장」(고친 숫자째) → T4
  6. +
  7. 단가 새 판 한 곳 — 바뀌는 줄 미리 보기 · 고친 칸 지킴 · 구조물 재적용과 합침 → T5
  8. +
  9. 뒤 — 단가산출 숫자 고치기 → 식 편집
  10. +
+ +

5. 남은 것

+ + +

6. 정해 줄 것

+
+
    +
  1. 조합안 방향(B09 작업대 한 화면에서 끝내기)이 맞는지
  2. +
  3. 다음 차례 — 1단계(작은 것) 계획부터 할지, 2단계(줄 펼쳐 고치기) 화면 시안을 먼저 볼지
  4. +
+
+ + + diff --git a/docs/raw/요구_내역서/_종합_내역5-7_기초자료_보여주는꼴.html b/docs/raw/요구_내역서/_종합_내역5-7_기초자료_보여주는꼴.html new file mode 100644 index 000000000..ce1550357 --- /dev/null +++ b/docs/raw/요구_내역서/_종합_내역5-7_기초자료_보여주는꼴.html @@ -0,0 +1,182 @@ + + + + +설계 기초자료 만들기 — 보여 주는 꼴 + + + + +

설계 기초자료 만들기 — 무엇을, 어떤 꼴로 보여 줘야 쉬운가

+
한시 브레인 sub8 · 10-31 · 내역 5 · 6 · 7 · 근거 = docs/raw/요구_내역서/_분석_내역5-A · 5-B · 5-C · 7 (사진 ref_내역5)
+ +
+ 먼저 구분 — 경쟁사는 내역서만 만들고 수량은 손으로 침. 아이슬로는 설계 · 수량 · 내역을 한 번에 다룸. + 수량 입력은 아이슬로가 덜어 줌. 대신 기초자료를 「우리 품셈 로직」 꼴로만 받게 해서 실무자에게 새 짐을 지움. + 그런데 조사해 보니 설계는 그 짐을 요구하지 않음. +
+ +

1. 찾은 것 다섯

+
+
①
설계가 일위대가에 바라는 것은 「1 단위 값(노무비 · 재료비 · 경비)」 하나뿐 — 배수관 · 석축 · 측구 · 세월교 · 토공 다섯 종류 모두 「사람이 규격을 고르고, 설계는 수량만 곱함」. 설계 값(높이 · 관경)이 품 계산에 들어가는 곳은 지금 하나도 없음. 5-B
+
②
실무 엑셀은 전부 「숫자 그대로」 꼴 — 8개 파일 모두 일위대가 수량은 손으로 친 값(식 0). 규격 변형(H=1.5 / 2.0 / 2.5)은 따로 만든 호표이고, 같은 줄에 수량만 다름. 5-A
+
③
경쟁사 「붙여넣으면 들어감」의 실체 — 구성 줄(이름 · 규격 · 수량 · 단위)을 그대로 넣고, 단가만 이름으로 이 공사 노임 · 자재에 다시 이음. 품 · 수량은 남음. 남의 엑셀 호표를 통째로 받는 길은 경쟁사에도 없음. 5-A
+
④
경쟁사 기준 자료 = 공급사가 「완성된 호표 묶음」을 반기마다 배포 — EST 단가산출 1,569 · 일위대가 1,468. 목록은 「품명 | 규격 | 단위 | 금액」 한 줄씩, 열면 숫자가 박힌 한 장 계산서. 아이슬로 M01 로직 1,469 는 품셈 표를 불러 쓰고 규격은 입력 칸이라 개정 · 규격 바꾸기에 더 강함. 대신 이름 · 규격 · 금액이 정해진 목록과 한 장 계산서 꼴이 없음. 7
+
⑤
아이슬로 지금 — 엑셀 → 기초자료가 네 단계(올리기 → 맞춤 → 사전조합 → 구조물 조합에 다시 담기). 맞춘 줄 56 %만 남고 엑셀 숫자는 우리 값으로 바뀜. 구조물 조합 하나에 약 65번 누름 + 도면. 일반 사용자는 회사로 나눌 길 없음. 품 · 수량 빈 줄은 경고 없이 합계에서 빠짐. 5-C
+
+ +

2. 기초자료 만들기 K1~K6 — 몇 번 누르나

+ + + + + + + + +
일ESTSTmate아이슬로 지금목표(추정)
K1 엑셀 호표 하나 → 내 자료약 13
셀 붙여넣기 + 자동연결
10 + 줄당 3~513~19 · 파일 통째 올림 · 숫자 바뀜 · 뒤에 K3 또 해야약 5
K2 시트 통째(호표 수십)약 36막힘 남의 엑셀약 74 · 56 %만 남음약 6
K3 새 구조물 조합(석축 H=2.0)경쟁사엔 「구조물 조합」이 없음 — 일위대가 하나 만들기는 EST 19 + 2n · STmate 20~25약 65 + 도면약 20 + 도면
K4 규격만 다른 것(H=1.5 → 2.0)8~11 + 2n14~1822~28 + 도면 · 같은 이름 둘약 6
K5 회사 사람과 나누기약 12 (파일)약 26 (폴더)관리자 4 · 일반 막힘3
K6 설계에서 고르고 금액 보기경쟁사엔 없음(수량 손)11~12그대로 (강점)
+ +

3. 보여 주는 꼴 — 「겉은 실무자가 보던 꼴, 속은 아이슬로」

+

경쟁사도 품셈을 미리 만들어 두고 사용자가 복사해 고침. 차이는 보이는 꼴. 실무자 눈에 익은 꼴은 「목록 한 줄 = 품명 · 규격 · 단위 · 금액」, 「계산서 한 장 = 숫자 × 숫자 = 금액」, 「엑셀 격자 = 호표 머리 + 구성 줄」 셋.

+ +

A. 기준 목록 — 로직 하나를 대표 규격으로 펼쳐 한 줄씩

+
+
기준 단가산출 · 공종: 토공 돌공 관 콘크리트🔍 석축 (「돌쌓기」도 찾음)
+
+ + + + + + +
품명규격단위금액출처
돌쌓기 메쌓기(장비)뒷길이 35cm 이하㎡58,512산림 13-4-2
돌쌓기 메쌓기(장비)뒷길이 55cm 이하㎡55,822산림 13-4-2
돌쌓기 메쌓기(장비)뒷길이 75cm 이하㎡53,134산림 13-4-2
돌쌓기 찰쌓기(장비)뒷길이 35cm 이하㎡101,530산림 13-4-3
+
속은 로직 하나 + 입력 값(뒷길이). GF · QF 같은 번호는 숨김. 숫자는 보기용.
+
+
+ +

B. 줄을 누르면 「계산서 보기」 — 경쟁사 단가산출 한 장 꼴 (담기 전에 미리 봄)

+
+
돌쌓기 메쌓기(장비) · 뒷길이 55cm 이하 · ㎡산림품셈 13-4-2 · 2026 상반기 노임   원문 보기 내 일위대가에 담기
+
석공 268,908 원 × 0.09 인 = 24,201.7 +보통인부 172,068 원 × 0.05 인 = 8,603.4 +굴착기+집게 59,020 원 × 0.39 hr = 23,017.8 +잡석 (설계 값) = 별도 +───────────────────────────────────────────── +노무비 32,805 · 경비 23,017 · 계 55,822 원/㎡
+
지금은 고르는 순간 바로 담기고 창이 닫혀 품을 미리 못 봄. 원문 띠(책 꼴)는 그대로 곁에.
+
+ +

C. 내 일위대가 = 엑셀 꼴 격자 하나 — 줄마다 「품셈」 · 「고정」 꼬리표

+
+
제17호표 돌기슭막이 H=2.0 · m · 내 것엑셀에서 붙여넣기 기준 목록에서 담기 저장
+
+ + + + + + + +
품명규격수량단위노무비재료비경비금액
품셈돌쌓기 찰쌓기뒷길이 35cm2.09㎡41,250—11,745110,759
품셈고임돌 채집현장0.31㎥24,816——7,693
고정야면석 채집 운반계곡 내0.45㎥18,300—6,12010,989
고정잡재료노무비의2%—2,043—2,043
1 m 당 합계 (빈 수량 줄 0 — 있으면 여기 빨간 글로 알림)131,484
+
아래 두 줄 = 엑셀에서 붙인 줄 · 숫자 그대로 · 단가는 이 공사 노임에 다시 이음.
+ 품셈 = 우리 로직이 품을 셈 · 노임 새 판 · 품셈 개정을 따라감 · 노란 칸 = 내가 고친 칸(곁에 품셈 값).   + 고정 = 붙인 숫자 그대로 · 노임 새 판 땐 단가만 바뀌고 품은 그대로(실무와 같음) · 원하면 [품셈으로 바꾸기]. +
+
+
+ +

D. 규격 변형 = 「규격 표」 — 같은 일위대가에 높이 열을 나란히

+
+
돌기슭막이 · m · 규격 표+ 열 (H=3.0)
+
+ + + + + + + +
품명H=1.5H=2.0H=2.5
돌쌓기 찰쌓기 (㎡)1.572.092.61
고임돌 채집 (㎥)0.240.310.39
야면석 채집 운반 (㎥)0.340.450.56
잡재료 (노무비의 %)222
1 m 당99,000131,484164,222
+
실무 엑셀의 「규격만 다른 호표 여러 개」를 한 표로. 설계(B06)는 높이로 열을 고름 — 지금 비교 행렬과 같은 자리. 도면 · 면적도 열마다.
+
+
+ +

E. 만드는 길 셋 — 모두 같은 격자(C)로 끝남

+
+
① 기준 목록에서 담기
A 에서 고름 → B 로 미리 봄 → 담기 · 품셈 줄
+
② 엑셀에서 붙여넣기
엑셀 Ctrl+C → 격자 Ctrl+V · 구성 줄 그대로 고정 · 단가는 이름으로 다시 이음(제안) · 못 이은 줄은 엑셀 단가 그대로 · 시트 통째도 같은 길
+
③ 있는 것 복사해 고치기
내 것 · 회사 · 지난 공사에서 복사 → 이름 · 수량 고침 · 규격 변형은 D 에 열 하나
+
+
↓ 만든 일위대가는 바로 구조물 조합의 일위대가 카드로 (사전조합 → 구조물 조합 두 단계를 하나로)
+ +

4. 지금 방침과 부딪히는 것

+
+ +
+ +

5. 덤으로 찾은 흠 (내역 영역)

+ + +

6. 정해 줄 것

+
+
    +
  1. 고정형 일위대가(엑셀 · 손 숫자 그대로, 단가만 다시 이음)를 받을지
  2. +
  3. 보여 주는 꼴 — 기준 목록(A) · 계산서 보기(B) · 엑셀 꼴 격자 + 꼬리표(C) · 규격 표(D) 방향이 맞는지
  4. +
  5. 다음 차례 — 실제 자료로 움직이는 화면 시안을 먼저 볼지, 계획서(PLAN 700번대 · 앞 조합안 B09 작업대와 묶어 단계 짜기)를 먼저 할지
  6. +
+
+ + + diff --git a/docs/raw/요구_내역서/ref_내역5/Aislo_M02_구조물조합_항목.png b/docs/raw/요구_내역서/ref_내역5/Aislo_M02_구조물조합_항목.png new file mode 100644 index 000000000..68b41293f Binary files /dev/null and b/docs/raw/요구_내역서/ref_내역5/Aislo_M02_구조물조합_항목.png differ diff --git a/docs/raw/요구_내역서/ref_내역5/Aislo_M02_나무_우클릭.png b/docs/raw/요구_내역서/ref_내역5/Aislo_M02_나무_우클릭.png new file mode 100644 index 000000000..8f75c523c Binary files /dev/null and b/docs/raw/요구_내역서/ref_내역5/Aislo_M02_나무_우클릭.png differ diff --git a/docs/raw/요구_내역서/ref_내역5/Aislo_M02_로직추가창.png b/docs/raw/요구_내역서/ref_내역5/Aislo_M02_로직추가창.png new file mode 100644 index 000000000..a16e7fe1f Binary files /dev/null and b/docs/raw/요구_내역서/ref_내역5/Aislo_M02_로직추가창.png differ diff --git a/docs/raw/요구_내역서/ref_내역5/Aislo_M02_복사생성.png b/docs/raw/요구_내역서/ref_내역5/Aislo_M02_복사생성.png new file mode 100644 index 000000000..f0b54804f Binary files /dev/null and b/docs/raw/요구_내역서/ref_내역5/Aislo_M02_복사생성.png differ diff --git a/docs/raw/요구_내역서/ref_내역5/Aislo_M02_복사생성_확인.png b/docs/raw/요구_내역서/ref_내역5/Aislo_M02_복사생성_확인.png new file mode 100644 index 000000000..0d934119f Binary files /dev/null and b/docs/raw/요구_내역서/ref_내역5/Aislo_M02_복사생성_확인.png differ diff --git a/docs/raw/요구_내역서/ref_내역5/Aislo_M02_새로만들기창.png b/docs/raw/요구_내역서/ref_내역5/Aislo_M02_새로만들기창.png new file mode 100644 index 000000000..6a1e6a854 Binary files /dev/null and b/docs/raw/요구_내역서/ref_내역5/Aislo_M02_새로만들기창.png differ diff --git a/docs/raw/요구_내역서/ref_내역5/Aislo_M02_새항목창.png b/docs/raw/요구_내역서/ref_내역5/Aislo_M02_새항목창.png new file mode 100644 index 000000000..c713fca6d Binary files /dev/null and b/docs/raw/요구_내역서/ref_내역5/Aislo_M02_새항목창.png differ diff --git a/docs/raw/요구_내역서/ref_내역5/Aislo_M02_실무자료.png b/docs/raw/요구_내역서/ref_내역5/Aislo_M02_실무자료.png new file mode 100644 index 000000000..ffcf33602 Binary files /dev/null and b/docs/raw/요구_내역서/ref_내역5/Aislo_M02_실무자료.png differ diff --git a/docs/raw/요구_내역서/ref_내역5/Aislo_M02_실무자료_엑셀보기.png b/docs/raw/요구_내역서/ref_내역5/Aislo_M02_실무자료_엑셀보기.png new file mode 100644 index 000000000..cd7006575 Binary files /dev/null and b/docs/raw/요구_내역서/ref_내역5/Aislo_M02_실무자료_엑셀보기.png differ diff --git a/docs/raw/요구_내역서/ref_내역5/Aislo_M02_실무호표_확인창.png b/docs/raw/요구_내역서/ref_내역5/Aislo_M02_실무호표_확인창.png new file mode 100644 index 000000000..72a9df57a Binary files /dev/null and b/docs/raw/요구_내역서/ref_내역5/Aislo_M02_실무호표_확인창.png differ diff --git a/docs/raw/요구_내역서/ref_내역5/Aislo_M02_원문띠.png b/docs/raw/요구_내역서/ref_내역5/Aislo_M02_원문띠.png new file mode 100644 index 000000000..56479c1c6 Binary files /dev/null and b/docs/raw/요구_내역서/ref_내역5/Aislo_M02_원문띠.png differ diff --git a/docs/raw/요구_내역서/ref_내역5/Aislo_M02_일반_첫화면.png b/docs/raw/요구_내역서/ref_내역5/Aislo_M02_일반_첫화면.png new file mode 100644 index 000000000..55849f3d7 Binary files /dev/null and b/docs/raw/요구_내역서/ref_내역5/Aislo_M02_일반_첫화면.png differ diff --git a/docs/raw/요구_내역서/ref_내역5/Aislo_M02_일반_회사층.png b/docs/raw/요구_내역서/ref_내역5/Aislo_M02_일반_회사층.png new file mode 100644 index 000000000..cb81931df Binary files /dev/null and b/docs/raw/요구_내역서/ref_내역5/Aislo_M02_일반_회사층.png differ diff --git a/docs/raw/요구_내역서/ref_내역5/Aislo_M02_일위대가상세.png b/docs/raw/요구_내역서/ref_내역5/Aislo_M02_일위대가상세.png new file mode 100644 index 000000000..324d199dd Binary files /dev/null and b/docs/raw/요구_내역서/ref_내역5/Aislo_M02_일위대가상세.png differ diff --git a/docs/raw/요구_내역서/ref_내역5/Aislo_M02_회사로복사창.png b/docs/raw/요구_내역서/ref_내역5/Aislo_M02_회사로복사창.png new file mode 100644 index 000000000..e12f65137 Binary files /dev/null and b/docs/raw/요구_내역서/ref_내역5/Aislo_M02_회사로복사창.png differ diff --git a/docs/raw/요구_내역서/ref_내역5/Aislo_M02_회사층.png b/docs/raw/요구_내역서/ref_내역5/Aislo_M02_회사층.png new file mode 100644 index 000000000..27de30aad Binary files /dev/null and b/docs/raw/요구_내역서/ref_내역5/Aislo_M02_회사층.png differ diff --git a/resources/tester/test_612_precompute.py b/resources/tester/test_612_precompute.py index 32b06e9d0..92cc16e2b 100644 --- a/resources/tester/test_612_precompute.py +++ b/resources/tester/test_612_precompute.py @@ -70,7 +70,7 @@ def fake_store(tmp_path, monkeypatch): calls: list[tuple[str, str]] = [] fail = {"36901002"} - def fake_run(step: str, no: str): + def fake_run(step: str, no: str, part=None): calls.append((step, no)) if no in fail: return step, no, {"ok": False, "error": "RuntimeError: 받기 실패"} @@ -85,8 +85,9 @@ def test_resume_skips_done_and_retries_failed(fake_store) -> None: sheets = ["36901001", "36901002", "36901003"] progress = tool.load_progress() tool.run(sheets, ["e07", "dem"], 1, progress, say=lambda _: None) - assert [s for s, _ in calls] == ["dem"] * 3 + ["e07"] * 3 # 차례 고정 - saved = json.loads((root / tool.PROGRESS_NAME).read_text(encoding="utf-8")) + # 차례 고정 · 지형 단계는 0차곡 이웃(3x3)까지 셈(618-1) + assert [s for s, n in calls if n in sheets] == ["dem"] * 3 + ["e07"] * 3 + saved = json.loads(tool.progress_path().read_text(encoding="utf-8")) assert saved["steps"]["dem"]["36901001"]["ok"] is True assert saved["steps"]["dem"]["36901002"]["ok"] is False diff --git a/resources/tester/test_618_precompute_split.py b/resources/tester/test_618_precompute_split.py new file mode 100644 index 000000000..b8cf989ab --- /dev/null +++ b/resources/tester/test_618_precompute_split.py @@ -0,0 +1,195 @@ +"""PLAN 618-1 — 전국 미리 계산 두 PC 나눔(`tools/precompute_terrain.py`) 지킴 시험. + +나눔 겹침 0 · 합치면 전체 · 이웃 일감 한 PC 만 · PC 별 기록 이어 받기 · CPU 상한(가짜 값) · +0차곡 남의 몫 이웃 없으면 미룸 · 지정 코어 인자 풀기 · +셈 함수는 가짜로 바꿔 서버 · 전국 파일 없이 돎. +""" + +from __future__ import annotations + +import json +import sys +from pathlib import Path + +import pytest + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT)) + +from B04_PreProcess.B04_PreProcess_Engine_MapSheet import neighbors_3x3 # noqa: E402 +from tools import precompute_terrain as tool # noqa: E402 + +SHEETS = [f"36901{i:03d}" for i in range(1, 101)] + [f"35810{i:03d}" for i in range(1, 101)] + + +def test_parse_part_and_cpus() -> None: + assert tool.parse_part("") is None + assert tool.parse_part("0-69/100") == (0, 69, 100) + assert tool.parse_part("70-99/100") == (70, 99, 100) + for bad in ("50-10/100", "0-100/100"): + with pytest.raises(ValueError): + tool.parse_part(bad) + assert tool.parse_cpus("12-19") == list(range(12, 20)) + assert tool.parse_cpus("1, 3-4") == [1, 3, 4] + assert tool.parse_cpus("") == [] + + +def test_split_no_overlap_and_union_is_all() -> None: + for spans in (("0-69/100", "70-99/100"), ("0-49/100", "50-99/100")): + a, b = (tool.parse_part(s) for s in spans) + mine = {no for no in SHEETS if tool.in_part(no, a)} + other = {no for no in SHEETS if tool.in_part(no, b)} + assert not mine & other and mine | other == set(SHEETS) + ja, jb = set(tool.dem_jobs(SHEETS, a)), set(tool.dem_jobs(SHEETS, b)) + assert not ja & jb and ja | jb == set(tool.dem_jobs(SHEETS, None)) + big = {no for no in SHEETS if tool.in_part(no, (0, 69, 100))} + assert 0.6 < len(big) / len(SHEETS) < 0.8 + assert tool.in_part("36901001@5186", (0, 99, 100)) + + +def test_dem_jobs_cover_neighbors_once() -> None: + jobs = tool.dem_jobs(["36901055"], None) + assert jobs[0] == "36901055" and set(jobs) == set(neighbors_3x3("36901055")) + # 띠 경계(128° · 5186 | 5187) 넘는 이웃 = 가운데 좌표계로 옮겨 담는 일감 + west = [no for no in tool.dem_jobs(["36809001"], None) if "@" in no] + assert west and all(j.endswith(f"@{tool.epsg_of('36809001')}") for j in west) + + +@pytest.fixture() +def fake_store(tmp_path, monkeypatch): + monkeypatch.setattr(tool.store, "TERRAIN_ROOT", tmp_path) + monkeypatch.setattr(tool.store, "_gpkg_stamp", lambda: "stamp-1") + monkeypatch.setattr(tool, "PC_NAME", "LAPTOP") + monkeypatch.setattr(tool, "CPU_SAMPLE_S", 0.0) + log: list = [] + + def fake_run(step: str, job: str, part=None): + log.append(("run", step, job)) + return step, job, {"ok": True, "s": 0.01, "fresh": True, "empty": False} + + monkeypatch.setattr(tool, "run_step", fake_run) + return tmp_path, log + + +def test_progress_per_pc_and_legacy_resume(fake_store, monkeypatch) -> None: + root, log = fake_store + legacy = {"gpkg": "stamp-1", "cell_m": 2.0, "steps": {"dem": {"36901001": {"ok": True}}}} + (root / tool.PROGRESS_NAME).write_text(json.dumps(legacy), encoding="utf-8") + tool.run(["36901001", "36901002"], ["e07"], 1, tool.load_progress(), say=lambda _: None) + assert (root / "_precompute_progress_LAPTOP.json").is_file() + assert json.loads((root / tool.PROGRESS_NAME).read_text(encoding="utf-8")) == legacy # 안 씀 + log.clear() + tool.run(["36901001", "36901002"], ["dem"], 1, tool.load_progress(), say=lambda _: None) + assert ("run", "dem", "36901001") not in log # 옛 기록 성공 = 이어 받음 + + monkeypatch.setattr(tool, "PC_NAME", "DESKTOP") + desk = tool.load_progress() + assert desk["steps"]["e07"] == {} # 다른 PC 기록은 run 에선 안 섞음 + desk["steps"]["e07"]["36901003"] = {"ok": True, "s": 1.0, "fresh": True} + tool.save_progress(desk) + merged = tool.load_progress(every_pc=True)["steps"]["e07"] + assert {"36901001", "36901002", "36901003"} <= set(merged) + + +def test_cpu_cap_holds_new_work(fake_store) -> None: + _, log = fake_store + values = iter([95, 90, 60, 80, 40]) + + def cpu() -> float: + value = next(values) + log.append(("cpu", value)) + return value + + tool.run( + ["36901001", "36901002"], + ["e07"], + 1, + tool.load_progress(), + cpu_cap=70, + cpu=cpu, + say=lambda _: None, + ) + assert [x[1] if x[0] == "cpu" else x[2] for x in log] == [ + 95, + 90, + 60, + "36901001", + 80, + 40, + "36901002", + ] + + +def test_zero_defers_when_foreign_neighbor_missing(tmp_path, monkeypatch) -> None: + monkeypatch.setattr(tool.store, "TERRAIN_ROOT", tmp_path) + center = "36901055" + part = (0, 49, 100) + foreign = [no for no in neighbors_3x3(center) if not tool.in_part(no, part)] + assert foreign # 고른 가운데는 남의 몫 이웃이 있음 + stored: set[str] = set() + monkeypatch.setattr( + tool.store, "_stored_ok", lambda no, e, c: {"k": 1} if no in stored else None + ) + ensured: list[str] = [] + monkeypatch.setattr(tool.store, "ensure_sheet_terrain", lambda no, e, c: ensured.append(no)) + _, _, info = tool.run_step("zero", center, part) + assert info["ok"] is False and info["wait"] and not ensured # 셈 안 함 + + stored.update(foreign) # 다른 PC 가 셈 · 동기 옴 + from D03_PlanAnalysis import D03_PlanAnalysis_ZeroOrder as zero + + monkeypatch.setattr(zero, "sheet_accumulation", lambda no, e, c: tmp_path / "acc.npz") + _, _, info = tool.run_step("zero", center, part) + assert info["ok"] is True and len(ensured) == 9 + + +def test_deferred_rounds_retry_until_done(fake_store, monkeypatch) -> None: + root, log = fake_store + tries: dict[str, int] = {} + + def fake_run(step: str, job: str, part=None): + tries[job] = tries.get(job, 0) + 1 + if job == "36901002" and tries[job] < 3: + return step, job, {"ok": False, "wait": True, "error": "이웃 지형 없음"} + return step, job, {"ok": True, "s": 0.01, "fresh": True, "empty": False} + + monkeypatch.setattr(tool, "run_step", fake_run) + monkeypatch.setattr(tool.time, "sleep", lambda s: None) + progress = tool.load_progress() + tool.run(["36901001", "36901002"], ["zero"], 1, progress, say=lambda _: None, defer_wait=1) + assert tries == {"36901001": 1, "36901002": 3} + out = tool.summary(["36901001", "36901002"], progress)["zero"] + assert out["ok"] == 2 and out["failed"] == {} and out["waiting"] == 0 + + +def test_clean_dead_only_own_part(tmp_path) -> None: + part = (0, 49, 100) + mine = next(no for no in SHEETS if tool.in_part(no, part)) + other = next(no for no in SHEETS if not tool.in_part(no, part)) + for no in (mine, other): + (tmp_path / no).mkdir() + (tmp_path / no / ".dem_5187_2m.lock").write_text("999999999 0", encoding="utf-8") + assert tool.clean_dead(tmp_path, part) == 1 + assert (tmp_path / other / ".dem_5187_2m.lock").exists() + + +def test_worker_init_pins_first_e_workers(monkeypatch, tmp_path) -> None: + import multiprocessing + + import psutil + + pinned: list[list[int]] = [] + + class FakeProc: + def nice(self, _): + pass + + def cpu_affinity(self, cpus): + pinned.append(cpus) + + monkeypatch.setattr(psutil, "Process", FakeProc) + monkeypatch.setattr(tool.store, "TERRAIN_ROOT", tool.store.TERRAIN_ROOT) # 끝나면 되돌림 + counter = multiprocessing.Value("i", 0) + for _ in range(5): + tool.init_worker(str(tmp_path), counter, [12, 13], 3) + assert pinned == [[12, 13]] * 3 and tool.store.TERRAIN_ROOT == tmp_path diff --git a/tools/precompute_terrain.py b/tools/precompute_terrain.py index 5dbfc3e6b..b5a78e4a3 100644 --- a/tools/precompute_terrain.py +++ b/tools/precompute_terrain.py @@ -6,16 +6,23 @@ --sheets 36901,36712089 도엽 번호 또는 앞자리(1:50,000 묶음) · 주면 높이 차 거름 안 씀 --box lon0,lat0,lon1,lat1 도엽 가운데가 이 경위도 상자 안 --min-relief 150 도엽 안 높이 차(m) 이상만(산지) · --limit N 앞 N 장만 - --workers 4 일꾼 수 · --status 진행 기록 요약만 · --rebuild-index 도엽 목록 다시 + --workers 4 일꾼 수 · --status 진행 기록 요약만(모든 PC) · --rebuild-index 도엽 목록 다시 + --part 0-69/100 두 PC 나눔(도엽 번호 crc32 % 100 이 0~69 인 것만 · PLAN 618) + --cpu-cap 70 전체 CPU % 가 넘으면 새 도엽 안 줌 · --e-cores 12-19 --e-workers 3 + 일꾼 셋만 그 논리 CPU 로 지정 · --root 창고 자리(시험용 임시 폴더) - 도엽 목록 = 전국 등고선 파일 꼭짓점이 닿는 도엽(0.025°) — `_precompute_sheets.json`(전국 파일 지문이 같으면 다시 안 셈) · 차례 = 등고선 길이 큰 것(산지)부터 · 같으면 높이 차. - 좌표계 = 도엽 가운데 경도의 TM 띠(`tm_epsg_for_lon` · 5185~5187) · 격자 2 m. - 셈 = 공용 함수만 부름(창고 지형 · D 0차곡 누적 · E07 도엽) — 저장 열쇠가 같으면 함수가 건너뜀 · 쓰기 = 함수 쪽 원자 쓰기(끊겨도 반쯤 쓴 파일 없음). -- 이어 하기 = `_precompute_progress.json`(단계 · 도엽마다 성공 · 시간) — 성공한 도엽은 - 다시 안 부름 · 실패는 다음에 다시 · 전국 파일이 바뀌면 기록을 새로 시작. -- 시작 때 죽은 셈의 잠금(주인 프로세스 없음) · 오래된 임시 파일을 치움 · +- 이어 하기 = PC 별 `_precompute_progress_<컴퓨터 이름>.json`(단계 · 도엽마다 성공 · 시간 · + 시놀로지가 두 PC 사이 옮김 → 한 파일은 한 PC 만 씀) · 옛 `_precompute_progress.json` 의 성공은 + 읽어서 이어 받음(안 씀) · 성공한 도엽은 다시 안 부름 · 실패는 다음에 다시 · 전국 파일이 바뀌면 + 기록을 새로 시작. +- 나눔 = 지형 단계가 목록 도엽 + 0차곡이 쓸 이웃(목록 밖 · 띠 넘음 포함)을 해시 몫대로 셈 · + 0차곡은 남의 몫 이웃 지형이 창고에 없으면 미뤘다 다시(다른 PC 셈 · 시놀로지 동기 기다림). +- 시작 때 자기 몫 도엽의 죽은 셈 잠금(주인 프로세스 없음) · 오래된 임시 파일을 치움 · 낮은 우선순위(BELOW_NORMAL). """ @@ -24,11 +31,15 @@ from __future__ import annotations import argparse import json import math +import multiprocessing import os +import platform import sqlite3 import sys +import threading import time -from concurrent.futures import ProcessPoolExecutor, as_completed +import zlib +from concurrent.futures import FIRST_COMPLETED, ProcessPoolExecutor, wait from pathlib import Path from typing import Any @@ -50,7 +61,10 @@ from common_util.common_util_json import atomic_write_json # noqa: E402 STEPS = ("dem", "zero", "e07") CELL_M = 2.0 -PROGRESS_NAME = "_precompute_progress.json" +PROGRESS_NAME = "_precompute_progress.json" # 옛 한 벌 기록(읽기만) +PC_NAME = platform.node() or "pc" +CPU_SAMPLE_S = 3.0 # 전체 CPU 평균 잼 · 상한 쓸 때 새 도엽 주는 간격 +DEFER_WAIT_S = 600.0 # 0차곡 이웃 지형 기다림 한 바퀴 INDEX_NAME = "_precompute_sheets.json" INDEX_CHUNK = 50_000 # 가닥 묶음(약 0.5 GB 읽기) INDEX_STRIDE = 8 # 꼭짓점 솎음(도엽 2 km 에 비해 넉넉) @@ -68,8 +82,77 @@ def below_normal() -> None: psutil.Process().nice(psutil.BELOW_NORMAL_PRIORITY_CLASS) -def epsg_of(sheet_no: str) -> int: - return tm_epsg_for_lon(sheet5k_center(sheet_no)[1]) +def epsg_of(job: str) -> int: + """도엽(또는 `도엽@좌표계` 일감)의 좌표계 — 기본 = 도엽 가운데 경도 띠.""" + no, _, epsg = job.partition("@") + return int(epsg) if epsg else tm_epsg_for_lon(sheet5k_center(no)[1]) + + +# ── 나눔 · CPU ─────────────────────────────────────────────────────────────── + +Part = tuple[int, int, int] + + +def parse_part(text: str) -> Part | None: + """`A-B/N` → (A, B, N) · 빈 글 = 나눔 없음.""" + if not text: + return None + span, _, n = text.partition("/") + a, _, b = span.partition("-") + part = (int(a), int(b or a), int(n or 100)) + if not 0 <= part[0] <= part[1] < part[2]: + raise ValueError(f"--part = A-B/N (0 ≤ A ≤ B < N): {text}") + return part + + +def in_part(job: str, part: Part | None) -> bool: + """도엽 번호 crc32 % N 이 A~B — PC 와 실행에 상관없이 같은 몫(겹침 0).""" + if part is None: + return True + value = zlib.crc32(job.partition("@")[0].encode()) % part[2] + return part[0] <= value <= part[1] + + +def parse_cpus(text: str) -> list[int]: + """`12-19` · `12,14,16-17` → 논리 CPU 번호.""" + cpus: list[int] = [] + for piece in filter(None, (p.strip() for p in text.split(","))): + a, _, b = piece.partition("-") + cpus.extend(range(int(a), int(b or a) + 1)) + return cpus + + +class CpuMeter: + """전체 CPU %(시스템 · 다른 일 포함) — 뒤 실이 `CPU_SAMPLE_S` 평균을 잼 · 부르면 마지막 값.""" + + def __init__(self) -> None: + import psutil + + self.value = psutil.cpu_percent(interval=CPU_SAMPLE_S) + + def loop() -> None: + while True: + self.value = psutil.cpu_percent(interval=CPU_SAMPLE_S) + + threading.Thread(target=loop, daemon=True).start() + + def __call__(self) -> float: + return self.value + + +def init_worker(root: str, counter: Any, e_cpus: list[int], e_workers: int) -> None: + """일꾼 시작 — 창고 자리 · 낮은 우선순위 · 앞선 `e_workers` 명은 `e_cpus` 에만(저전력 코어).""" + store.TERRAIN_ROOT = Path(root) + below_normal() + if counter is None: + return + with counter.get_lock(): + slot = counter.value + counter.value += 1 + if slot < e_workers: + import psutil + + psutil.Process().cpu_affinity(e_cpus) # ── 도엽 목록 ──────────────────────────────────────────────────────────────── @@ -203,9 +286,24 @@ def _mtime(path: Path) -> int | None: return None -def run_step(step: str, sheet_no: str) -> tuple[str, str, dict[str, Any]]: - """도엽 하나 · 단계 하나 — 공용 함수만 부름 · fresh = 이번에 새로 셈.""" - epsg = epsg_of(sheet_no) +def dem_jobs(sheets: list[str], part: Part | None) -> list[str]: + """지형 단계 일감 — 목록 도엽 뒤 0차곡이 쓸 3x3 이웃(그 가운데 도엽 좌표계) · 자기 몫만. + + 이웃이 가운데와 띠가 다르면 `도엽@좌표계`(옮겨 담기) — 목록 밖 이웃도 한 PC 만 셈. + """ + jobs: dict[str, None] = {no: None for no in sheets if in_part(no, part)} + for center in sheets: + epsg = epsg_of(center) + for no in neighbors_3x3(center): + if in_part(no, part): + jobs[no if epsg == epsg_of(no) else f"{no}@{epsg}"] = None + return list(jobs) + + +def run_step(step: str, job: str, part: Part | None = None) -> tuple[str, str, dict[str, Any]]: + """일감 하나 · 단계 하나 — 공용 함수만 부름 · fresh = 이번에 새로 셈 · wait = 이웃 기다림.""" + sheet_no = job.partition("@")[0] + epsg = epsg_of(job) started = time.monotonic() try: if step == "dem": @@ -214,7 +312,11 @@ def run_step(step: str, sheet_no: str) -> tuple[str, str, dict[str, Any]]: elif step == "zero": from D03_PlanAnalysis import D03_PlanAnalysis_ZeroOrder as zero - for no in neighbors_3x3(sheet_no): # 여유 200 m 이웃 지형(누적 열쇠가 나중에 안 바뀌게) + neighbors = neighbors_3x3(sheet_no) + for no in neighbors: # 남의 몫 이웃 지형이 아직 없음 = 미룸(누적 열쇠가 나중에 바뀜) + if not in_part(no, part) and store._stored_ok(no, epsg, CELL_M) is None: + return step, job, {"ok": False, "wait": True, "error": f"이웃 지형 없음 {no}"} + for no in neighbors: # 여유 200 m 이웃 지형(누적 열쇠가 나중에 안 바뀌게) store.ensure_sheet_terrain(no, epsg, CELL_M) target = zero._acc_names(sheet_no, epsg, CELL_M)[0] before = _mtime(target) @@ -231,30 +333,55 @@ def run_step(step: str, sheet_no: str) -> tuple[str, str, dict[str, Any]]: else: raise ValueError(f"모르는 단계: {step}") except Exception as exc: # noqa: BLE001 — 한 장이 죽어도 나머지는 감 · 기록에 까닭 - return step, sheet_no, {"ok": False, "error": f"{type(exc).__name__}: {exc}"[:300]} + return step, job, {"ok": False, "error": f"{type(exc).__name__}: {exc}"[:300]} seconds = round(time.monotonic() - started, 2) - return step, sheet_no, {"ok": True, "s": seconds, "fresh": fresh, "empty": empty} + return step, job, {"ok": True, "s": seconds, "fresh": fresh, "empty": empty} # ── 진행 기록 · 정리 ───────────────────────────────────────────────────────── -def load_progress() -> dict[str, Any]: - path = store.TERRAIN_ROOT / PROGRESS_NAME - stamp = store._gpkg_stamp() +def progress_path() -> Path: + return store.TERRAIN_ROOT / f"_precompute_progress_{PC_NAME}.json" + + +def progress_files() -> list[Path]: + """모든 PC 기록(옛 한 벌 · 시놀로지 충돌 사본 포함).""" + return sorted(store.TERRAIN_ROOT.glob("_precompute_progress*.json")) + + +def _read_progress(path: Path, stamp: str | None) -> dict[str, Any] | None: try: saved = json.loads(path.read_text(encoding="utf-8")) except (OSError, ValueError): - saved = None - if not saved or saved.get("gpkg") != stamp or saved.get("cell_m") != CELL_M: - saved = {"gpkg": stamp, "cell_m": CELL_M, "steps": {}} + return None + if not isinstance(saved, dict) or saved.get("gpkg") != stamp or saved.get("cell_m") != CELL_M: + return None + return saved + + +def load_progress(every_pc: bool = False) -> dict[str, Any]: + """이 PC 기록 + 옛 한 벌의 성공(every_pc = 모든 PC 기록의 성공) — 쓰기는 이 PC 파일만.""" + stamp = store._gpkg_stamp() + own_path = progress_path() + saved = _read_progress(own_path, stamp) or {"gpkg": stamp, "cell_m": CELL_M, "steps": {}} for step in STEPS: saved["steps"].setdefault(step, {}) + others = progress_files() if every_pc else [store.TERRAIN_ROOT / PROGRESS_NAME] + for path in others: + other = None if path == own_path else _read_progress(path, stamp) + for step, records in (other or {}).get("steps", {}).items(): + mine = saved["steps"].get(step) + if mine is None: + continue + for job, info in records.items(): + if info.get("ok") and not mine.get(job, {}).get("ok"): + mine[job] = info return saved def save_progress(progress: dict[str, Any]) -> None: - atomic_write_json(store.TERRAIN_ROOT / PROGRESS_NAME, progress) + atomic_write_json(progress_path(), progress) def _alive(pid: int) -> bool: @@ -263,15 +390,18 @@ def _alive(pid: int) -> bool: return psutil.pid_exists(pid) -def clean_dead(root: Path | None = None) -> int: - """주인이 죽은 잠금 · 오래된 임시 파일을 지움(끊긴 일괄 셈이 남긴 것) → 지운 수.""" +def clean_dead(root: Path | None = None, part: Part | None = None) -> int: + """자기 몫 도엽의 주인이 죽은 잠금 · 오래된 임시 파일을 지움(끊긴 일괄 셈이 남긴 것) → 지운 수. + + 남의 몫 도엽 = 다른 PC 잠금(이 PC 에선 주인 번호가 없어 보임)이라 안 건드림. + """ root = root or store.TERRAIN_ROOT removed = 0 if not root.is_dir(): return 0 now = time.time() for folder in root.iterdir(): - if not folder.is_dir(): + if not folder.is_dir() or not in_part(folder.name, part): continue for path in folder.glob(".*"): try: @@ -288,47 +418,102 @@ def clean_dead(root: Path | None = None) -> int: return removed +def _execute(step: str, todo: list[str], workers: int, record, opts: dict[str, Any]) -> None: + """일감 차례대로 일꾼에 줌 — 상한 쓰면 전체 CPU 가 넘는 동안 새 일 안 줌(도는 것은 그대로).""" + part, cap, cpu = opts["part"], opts["cpu_cap"], opts["cpu"] + + def over() -> bool: + return cap is not None and cpu() > cap + + if workers <= 1: + for job in todo: + while over(): + time.sleep(CPU_SAMPLE_S) + record(run_step(step, job, part)) + return + e_cpus, e_workers = opts["e_cpus"], opts["e_workers"] + counter = multiprocessing.Value("i", 0) if e_cpus and e_workers else None + initargs = (str(store.TERRAIN_ROOT), counter, e_cpus, e_workers) + with ProcessPoolExecutor(workers, initializer=init_worker, initargs=initargs) as pool: + queue = list(reversed(todo)) + running: set = set() + try: + while queue or running: + while queue and len(running) < workers and not over(): + running.add(pool.submit(run_step, step, queue.pop(), part)) + if cap is not None: + break # 상한 쓸 때 한 번에 하나 — 다음 잼을 보고 또 + if not running: + time.sleep(CPU_SAMPLE_S) + continue + timeout = CPU_SAMPLE_S if cap is not None and queue else None + finished, running = wait(running, timeout=timeout, return_when=FIRST_COMPLETED) + for future in finished: + record(future.result()) + except BaseException: + pool.shutdown(wait=False, cancel_futures=True) + raise + + def run( sheets: list[str], steps: list[str], workers: int, progress: dict[str, Any], say=print, + *, + part: Part | None = None, + cpu_cap: float | None = None, + cpu=None, + e_cpus: list[int] | None = None, + e_workers: int = 0, + defer_wait: float = 0.0, ) -> dict[str, Any]: - """단계 차례대로 · 단계 안은 일꾼 여럿 · 성공 기록된 도엽은 건너뜀 → 단계별 벽시계 초.""" + """단계 차례대로 · 단계 안은 일꾼 여럿 · 성공 기록된 일감은 건너뜀 → 단계별 벽시계 초. + + sheets = 나누기 전 목록(이웃 몫에 씀) · 미룬 0차곡 = defer_wait 초 뒤 다시(0 = 안 기다림). + """ + if cpu_cap is not None and cpu is None: + cpu = CpuMeter() + opts = {"part": part, "cpu_cap": cpu_cap, "cpu": cpu, "e_cpus": e_cpus or []} + opts["e_workers"] = e_workers walls: dict[str, float] = {} for step in [s for s in STEPS if s in steps]: done = progress["steps"][step] - todo = [no for no in sheets if not done.get(no, {}).get("ok")] - say(f"[{step}] {len(todo)} 장 (건너뜀 {len(sheets) - len(todo)})") + jobs = dem_jobs(sheets, part) if step == "dem" else [n for n in sheets if in_part(n, part)] + todo = [job for job in jobs if not done.get(job, {}).get("ok")] + say(f"[{step}] {len(todo)} 장 (건너뜀 {len(jobs) - len(todo)} · 몫 {len(jobs)})") started = time.monotonic() saved_at = started count = 0 + waiting: list[str] = [] def record(result: tuple[str, str, dict[str, Any]]) -> None: nonlocal count, saved_at - _, no, info = result - done[no] = info + _, job, info = result + done[job] = info count += 1 - state = f"{info['s']} s" if info["ok"] else f"실패 {info['error']}" - say(f"[{step}] {count}/{len(todo)} {no} {state}") + if info.get("wait"): + waiting.append(job) + state = ( + f"{info['s']} s" + if info["ok"] + else f"{'기다림' if info.get('wait') else '실패'} {info['error']}" + ) + say(f"[{step}] {count}/{len(todo)} {job} {state}") if time.monotonic() - saved_at > SAVE_EVERY_S: save_progress(progress) saved_at = time.monotonic() try: - if workers <= 1: - for no in todo: - record(run_step(step, no)) - else: - with ProcessPoolExecutor(workers, initializer=below_normal) as pool: - futures = [pool.submit(run_step, step, no) for no in todo] - try: - for future in as_completed(futures): - record(future.result()) - except BaseException: - pool.shutdown(wait=False, cancel_futures=True) - raise + while True: + _execute(step, todo, workers, record, opts) + if not waiting or defer_wait <= 0: + break + save_progress(progress) + say(f"[{step}] 이웃 지형 기다림 {len(waiting)} 장 · {defer_wait:g} s 뒤 다시") + time.sleep(defer_wait) + todo, waiting[:], count = list(waiting), [], 0 finally: save_progress(progress) walls[step] = round(time.monotonic() - started, 1) @@ -341,12 +526,14 @@ def summary(sheets: list[str], progress: dict[str, Any]) -> dict[str, Any]: for step in STEPS: done = progress["steps"][step] infos = [done[no] for no in sheets if no in done] - failed = {no: done[no]["error"] for no in sheets if done.get(no, {}).get("ok") is False} + failed = { + no: done[no]["error"] + for no in sheets + if done.get(no, {}).get("ok") is False and not done[no].get("wait") + } fresh = [i["s"] for i in infos if i.get("ok") and i.get("fresh")] - sizes = [] + sizes = [] # 창고 실물(다른 PC 가 셈 · 시놀로지로 옴 포함) for no in sheets: - if not done.get(no, {}).get("ok"): - continue stem = f"{epsg_of(no)}_{CELL_M:g}m" folder = store.TERRAIN_ROOT / no total = 0 @@ -358,6 +545,8 @@ def summary(sheets: list[str], progress: dict[str, Any]) -> dict[str, Any]: "ok": sum(1 for i in infos if i.get("ok")), "fresh": len(fresh), "failed": failed, + "waiting": sum(1 for no in sheets if done.get(no, {}).get("wait")), + "stored": len(sizes), "s_median": round(float(np.median(fresh)), 1) if fresh else None, "s_mean": round(float(np.mean(fresh)), 1) if fresh else None, "s_max": round(float(np.max(fresh)), 1) if fresh else None, @@ -377,7 +566,22 @@ def main(argv: list[str] | None = None) -> int: parser.add_argument("--workers", type=int, default=4) parser.add_argument("--status", action="store_true") parser.add_argument("--rebuild-index", action="store_true") + parser.add_argument("--part", default="", help="A-B/N — 도엽 번호 crc32 %% N 이 A~B 인 것만") + parser.add_argument("--cpu-cap", type=float, default=None, help="전체 CPU %% 상한") + parser.add_argument("--e-cores", default="", help="저전력 논리 CPU(예 12-19)") + parser.add_argument("--e-workers", type=int, default=0, help="그 CPU 로 지정할 일꾼 수") + parser.add_argument("--defer-wait", type=float, default=DEFER_WAIT_S) + parser.add_argument("--root", default="", help="창고 자리(시험용 임시 폴더)") args = parser.parse_args(argv) + try: + part = parse_part(args.part) + e_cpus = parse_cpus(args.e_cores) + except ValueError as exc: + parser.error(str(exc)) + if (args.e_workers and not e_cpus) or args.e_workers > args.workers: + parser.error("--e-workers 는 --e-cores 와 함께 · --workers 이하") + if args.root: + store.TERRAIN_ROOT = Path(args.root) steps = [s.strip() for s in args.steps.split(",") if s.strip()] unknown = [s for s in steps if s not in STEPS] if unknown: @@ -392,14 +596,39 @@ def main(argv: list[str] | None = None) -> int: sheets = pick_sheets( index, sheets=wanted or None, box=box, min_relief=args.min_relief, limit=args.limit or None ) - print(f"도엽 {len(sheets)} 장 (목록 {len(index)} 장) · 단계 {steps} · 일꾼 {args.workers}") - progress = load_progress() - if not args.status: - print(f"치운 잠금 · 임시 파일 {clean_dead()}") + print( + f"도엽 {len(sheets)} 장 (목록 {len(index)} 장) · 단계 {steps} · 일꾼 {args.workers}" + f" · 몫 {args.part or '전부'} · PC {PC_NAME} · 상한 {args.cpu_cap}" + f" · 지정 CPU {e_cpus} × {args.e_workers}" + ) + mine = [no for no in sheets if in_part(no, part)] + if args.status: + out = summary(mine, load_progress(every_pc=True)) + stamp = store._gpkg_stamp() + out["pc"] = {} + for path in progress_files(): + recs = (_read_progress(path, stamp) or {}).get("steps", {}) + out["pc"][path.name] = { + step: sum(1 for i in recs.get(step, {}).values() if i.get("ok")) for step in STEPS + } + else: + progress = load_progress() + print(f"치운 잠금 · 임시 파일 {clean_dead(part=part)}") started = time.monotonic() - walls = run(sheets, steps, args.workers, progress) + walls = run( + sheets, + steps, + args.workers, + progress, + part=part, + cpu_cap=args.cpu_cap, + e_cpus=e_cpus, + e_workers=args.e_workers, + defer_wait=args.defer_wait, + ) print(f"벽시계 {walls} · 전체 {time.monotonic() - started:.1f} s") - print(json.dumps(summary(sheets, progress), ensure_ascii=False, indent=1)) + out = summary(mine, progress) + print(json.dumps(out, ensure_ascii=False, indent=1)) return 0