From a5b5dc1b509222764c8ab9f9793df51a8aba37aa Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sat, 31 Oct 2026 17:27:09 +0900 Subject: [PATCH 01/11] =?UTF-8?q?knowledge(=EC=9A=94=EA=B5=AC):=20?= =?UTF-8?q?=EC=9A=94=EA=B5=AC=20517=20=EC=A1=B0=EC=82=AC=20=EA=B8=B0?= =?UTF-8?q?=EB=A1=9D=20=E2=80=94=20=EC=A0=84=EA=B5=AD=20=EC=82=B0=EB=A6=BC?= =?UTF-8?q?=20=EC=A3=BC=EC=A0=9C=EB=8F=84=20=EB=8B=A4=EB=A5=B8=20=EC=B0=BD?= =?UTF-8?q?=EA=B5=AC=20=C2=B7=20=EC=83=81=EC=9A=A9=20=EC=A1=B0=EA=B1=B4=20?= =?UTF-8?q?=C2=B7=20=EA=B3=B5=EC=8B=9D=20=EC=9A=94=EC=B2=AD=20=EA=B8=B8?= 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_015uTVHHDv583hbLQAFeBmd9 --- .../_조사_517_전국_산림주제도_다른곳.md | 131 ++++++++++++++++++ 1 file changed, 131 insertions(+) create mode 100644 docs/raw/요구_1025/_조사_517_전국_산림주제도_다른곳.md diff --git a/docs/raw/요구_1025/_조사_517_전국_산림주제도_다른곳.md b/docs/raw/요구_1025/_조사_517_전국_산림주제도_다른곳.md new file mode 100644 index 000000000..8366c2c01 --- /dev/null +++ b/docs/raw/요구_1025/_조사_517_전국_산림주제도_다른곳.md @@ -0,0 +1,131 @@ +# 517 전국 산림 주제도 — 상용 가능한 다른 창구 조사 (2026-10-31 · 웹 공개 페이지만) + +비교 기준 = `docs/raw/요구_1025/_조사_513_산림청_주제도_API.md`(다시 조사 안 함). + +## 결론 + +- **「전국 + 상용 허용 문구 확실」 창구 = 세 주제도 모두 없음.** +- 가장 가까운 곳 = **산림빅데이터거래소(한국임업진흥원)** — 임상도 4차 1:25,000 전국 · 임상도 1:5,000(시도 · 시군구 · 도엽) · 임도망도 전국 상품 페이지에 「라이선스: Creative Commons Attribution」 · 이용료 0원(검색 색인 기준). 그러나 같은 페이지 「Copyright 2019. SEESUNIT CO., LTD. all rights reserved」 · 약관 「자체 사용 목적 외에 외부로 유출 … 구매회원 책임」 · 사이트가 이 조사 환경에서 접속 거부 → **상용 판단 확인 못 함(서면 확인 필요)**. +- 산사태위험지도 전국 파일 — FGIS 일반인 신청(2025년판 · 서약 적용) · 브이월드 = 화면 타일만 · 원본은 「공문 요청 후 방문 수령」 · 옛 국가공간정보포털 오픈마켓 SHP(2019)는 사이트 폐쇄. +- **새 상충** — 산림청 공공데이터 개방 페이지: 임상도 1:25,000 「공공기관 … 비영리공공사업이나 학술·연구목적으로만 제공」(data.go.kr 「제한 없음」과 반대). +- **조건 정정** — 공공누리 상업 허용 = **1 · 3 유형**(2유형 = 「비상업적 이용만 가능」 · 3유형 = 상업 가능 · 변경 금지). +- 견본뿐일 때 공식 길 = 목록별 오류신고 → 제공신청(10일 + 10일) → 거부 · 중단 시 60일 안 분쟁조정(위원회 30일 안 조정안). + +## 1. 임상도 (1:25,000 이상 · 전국) + +| 창구 | 범위 · 축척 · 형식 · 갱신 | 이용 조건 문구 | 로그인 · 신청 | 상용 판단 | 주소 | +|---|---|---|---|---|---| +| data.go.kr 3045619 (기준) | 전국 표기 · 공개 파일 = 제주 견본 · 1:25,000 · SHP · 2025-11-20 | 「이용허락범위 제한 없음」 | 받기 = FGIS 신청 | 문구 OK · 전국 파일 없음 | [D2](https://www.data.go.kr/data/3045619/fileData.do) | +| 산림청 공공데이터 개방 PBD0000059 | 5차 전국 · 견본 = 제주특별자치도 제주시 SHP | 「공공기관(중앙행정기관 및 소속기관, 지방자치단체, 정부투자기관, 학교 및 연구기관)의 비영리공공사업이나 학술·연구목적으로만 제공하고 있음」 · 공공누리 유형 표기 없음 | FGIS 무료신청(일반인 · 공공/연구) | **불가 문구** | [F7](https://www.forest.go.kr/kfsweb/kfi/kfs/trail/fTMap.do?pblicDataId=PBD0000059&tabs=1&mn=NKFS_06_08_02&subTitle=%EC%9E%84%EC%83%81%EB%8F%84) | +| data.go.kr 15093362 (1:5,000) | 전국 · SHP · 2026-10-06 | 「이용허락범위 제한 없음」 · 받는 길 = 「산림공간정보 다운로드 무료신청 > 일반인 신청 > 대축척 임상도 선택 … 신청 목적 입력 > shp 파일 다운로드」 | FGIS(서약) | FGIS 서약 적용 → 확인 못 함 | [D1](https://www.data.go.kr/data/15093362/fileData.do) | +| FGIS 자료유통현황 | 임상도 1:25,000 · 1:5,000 = 「일반 제공용」 · SHP · EPSG:5179 | 서약(업무 한정 · 유출 금지 — 513 조사) | 로그인 · 신청 | 상용 재배포와 충돌 | [F1](https://www.forest.go.kr/newkfsweb/kfi/kfs/fgis/selectAvailMapList.do?mn=KFS_02_04_02_02&orgId=fgis) | +| 브이월드 30136 (기준) · data.go.kr 연계 258662 | 1:5,000 전국 GDB | 브이월드 CC BY-NC-ND · 연계 페이지 이용허락범위 빈칸 | — | 불가 | [V1](https://www.vworld.kr/dtmk/dtmk_ntads_s002.do?svcCde=MK&dsId=30136) [D9](https://www.data.go.kr/data/258662/linkedData.do) | +| **산림빅데이터거래소 FRT001201** 임상도 4차(1:25000)_전국 | 전국 750도엽 · 2018 기준 · 4차(1996~2005) 수치화 · SHP zip 약 22 MB · EPSG:5179 · 「갱신없음」 | 「라이선스: Creative Commons Attribution」 · 「Copyright 2019. SEESUNIT CO., LTD. all rights reserved」 · 이용료 ₩0 · 판매처 ㈜시선아이티 | 회원가입 | CC BY 면 가능 · 저작권 표기 · 약관과 상충 → 확인 못 함 | [B4](https://www.bigdata-forest.kr/product/FRT001201) [D10](https://www.data.go.kr/data/95584/linkedData.do) | +| 산림빅데이터거래소 FRT001002~004 임상도(1:5000) 시도 · 시군구 · 도엽 | 1:5,000 | 「Creative Commons Attribution」 · ₩0 | 회원가입 | 같은 자료가 브이월드 CC BY-NC-ND → 상충 | [B5](https://www.bigdata-forest.kr/product/FRT001002) [B6](https://www.bigdata-forest.kr/product/FRT001004) | +| 산림빅데이터거래소 FRT001101 수종별 임상도 전국 | 1:5,000 기반 | 이용료 400,000원 | 회원가입 · 결제 | 확인 못 함 | [B7](https://www.bigdata-forest.kr/product/FRT001101) | +| 지자체 (data.go.kr) | 「임상도」 파일 검색 = 산림청 2건뿐 · 지자체 등록 0 | — | — | 해당 없음 | [검색](https://www.data.go.kr/tcs/dss/selectDataSetList.do?keyword=%EC%9E%84%EC%83%81%EB%8F%84&dType=FILE) | +| K-Geo(kgeop.go.kr) · egis · AI Hub | K-Geo = 토지 · 건물 정보 · 산림 주제도 언급 없음 · egis · AI Hub = 임상도 제공 문구 못 찾음 | — | — | 확인 못 함 | [K1](https://www.kgeop.go.kr/) | + +## 2. 임도망도 (전국 노선) + +| 창구 | 범위 · 축척 · 형식 · 갱신 | 이용 조건 문구 | 로그인 · 신청 | 상용 판단 | 주소 | +|---|---|---|---|---|---| +| data.go.kr 3045621 (기준) | 「전국 산림 내에 조성된 임도」 · 견본만 · SHP · 2025-11-20 · 산림디지털담당관실 042-481-4285 | 「이용허락범위 제한 없음」 | 기관자체 다운로드 | 문구 OK · 전국 파일 없음 | [D3](https://www.data.go.kr/data/3045621/fileData.do) | +| FGIS 자료유통현황 | 임도망도 1:5,000 = 「공공연구기관 제공용」 | — | 일반 신청 불가 | 불가 | [F1](https://www.forest.go.kr/newkfsweb/kfi/kfs/fgis/selectAvailMapList.do?mn=KFS_02_04_02_02&orgId=fgis) | +| 산림청 임도망도 현황 페이지 | 1:25,000 · 607도엽 · 1999 1차 구축 · 국유임도 중심 · 문의 목재산업과 042-481-4276 | 받는 길 · 조건 표기 없음 | — | 확인 못 함 | [F8](https://www.forest.go.kr/newkfsweb/html/HtmlPage.do?pg=%2Ffgis%2FUI_KFS_5002_020700.html&mn=KFS_02_04_03_04_04&orgId=fgis) | +| **산림빅데이터거래소 FRT002601** 임도망도(산길)_전국 | 전국 · 2019 기준 · 노선 · 시공년도 · 거리 · csv/zip 약 25.71 MB · SHP · EPSG:5179 | CC BY 표기(색인) · 이용료 ₩0 · 판매처 ㈜시선아이티 · data.go.kr 연계 95799 이용허락범위 빈칸 | 회원가입 | 확인 못 함(약관 상충) | [B2](https://www.bigdata-forest.kr/product/FRT002601) [D11](https://www.data.go.kr/data/95799/linkedData.do) | +| 브이월드 | 산림청 12건(임상도 · 토양도 · 경제림 · 맞춤형조림 · 백두대간 · 산림기능 · 등산로) — 임도 없음 · 「임도」 검색 0건 | — | — | 해당 없음 | [V4](https://www.vworld.kr/dtmk/dtmk_ntads_s001.do?svcCde=MK) | +| data.go.kr 임도 표(산림청 · 지자체 CSV) | 예: 거창군 15006041 = 개설년도 · 노선명 · 지번 · 연장 · 산림청 임도시설 현황 15094372 · 산림토목관리 노선정보 15120710 = 노선거리만 · **좌표 없음** | 「이용허락범위 제한 없음」 | 없음 | 가능하나 노선 도형 아님 | [D12](https://www.data.go.kr/data/15006041/fileData.do) [D13](https://www.data.go.kr/data/15120710/fileData.do) | +| data.go.kr 15071663 산림토목사업 대상지정보 | 「대상지 좌표」(점) 포함 표기 · 2026-09-03 | 「이용허락범위 제한 없음」 | 없음 | 가능 · 노선 아님 | [D14](https://www.data.go.kr/data/15071663/fileData.do) | +| 국토지리정보원 연속수치지형도 15059721 | 전국 임의 영역 · 1:5,000 · 1:25,000 · SHP/NGI · 국토정보플랫폼 | 「공공저작물 : 출처표시 (제 1유형)」 | 국토정보플랫폼 | 상용 가능 · **임도 표현 여부 확인 못 함** | [D15](https://www.data.go.kr/data/15059721/fileData.do) | + +## 3. 산사태위험지도 (전국) + +| 창구 | 범위 · 축척 · 형식 · 갱신 | 이용 조건 문구 | 로그인 · 신청 | 상용 판단 | 주소 | +|---|---|---|---|---|---| +| data.go.kr 15074817 (기준) | 전국 표기 · 10 m 격자 · XML · 견본 = 제주시 ZIP · 2026-09-03 · 산사태방지과 042-481-8846 | 「이용허락범위 제한 없음」 | — | 문구 OK · 전국 파일 없음 | [D7](https://www.data.go.kr/data/15074817/fileData.do) [F9](https://www.forest.go.kr/kfsweb/kfi/kfs/trail/sanSaTae.do?mn=NKFS_01_01&subTitle=%EC%82%B0%EC%82%AC%ED%83%9C%EC%9C%84%ED%97%98%EC%A7%80%EB%8F%84) | +| FGIS 일반인 신청 (산림청 답변 2026-01-14) | 구축 2013 · 2019 · 2022~2025 · 「현재 … 2025년 산사태위험지도만 제공이 가능」 · 래스터 제작(2023 답변) | 「map.forest.go.kr -> 산림공간정보 다운로드 -> 무료신청 -> 일반인 신청」 · 자료유통현황 표는 「공공연구기관 제공용」(상충) · 서약 적용 | 로그인 · 신청 | 서약 → 상용 불가 쪽 | [F10](https://www.forest.go.kr/newkfsweb/cop/bbs/selectBoardArticle.do?nttId=3215640&bbsId=BBSMSTR_1351&mn=KFS_02_04_07_02&orgId=fgis) [F11](https://www.forest.go.kr/newkfsweb/cop/bbs/selectBoardArticle.do?nttId=3182126&bbsId=BBSMSTR_1351&mn=KFS_02_04_07_02&orgId=fgis) | +| 정보공개청구(open.go.kr) | 과거판(2013~2024) — 「과거 산사태위험지도는 "정보공개청구"를 통하여 신청 가능」 | 받은 자료의 이용 조건 = 확인 못 함 | 청구 | 확인 못 함 | [F10](https://www.forest.go.kr/newkfsweb/cop/bbs/selectBoardArticle.do?nttId=3215640&bbsId=BBSMSTR_1351&mn=KFS_02_04_07_02&orgId=fgis) | +| 브이월드 DAT_0000000000000096 | 전국 · 2D · 3D **PNG 타일**(지도 보기) · 2024 구축 · 2025-02-05 갱신 · 원본 3.52 GB | 라이선스 표기 없음 · 「대외제공 가능여부: Y」 · 원본 「공문 요청 후 방문 수령」 | 공문 | 확인 못 함 | [V5](https://www.vworld.kr/dtmk/dtmk_ntads_s002.do?svcCde=DT&dsId=DAT_0000000000000096) | +| 옛 국가공간정보포털 오픈마켓 20210708ds00009 | 전국 · SHP(TB_FGDI_FS_FD100.zip) · 2019-05-01 | 「지정된 라이선스 없음」(검색 요약) | — | **사이트 폐쇄**(data.nsdi.go.kr DNS 없음 · 오픈마켓은 2023-12-31 브이월드 통합 — 나무위키) · 브이월드 산림청 목록에 없음 | [N1](http://data.nsdi.go.kr/dataset/20210708ds00009) | +| 생활안전지도 WMS (기준) | 전국 WMS | 공공누리 4유형 | 인증키 | 불가 | [S1](https://www.safemap.go.kr/opna/data/dataViewRenew.do?objtId=125) | +| 산림빅데이터거래소 FRT000301 산사태발생위험등급 | 전국 · 시도 · 시군구 · 1~5 등급 · API | data.go.kr 연계 81177 이용허락범위 빈칸 · 무료 | 회원가입 | 확인 못 함 | [D16](https://www.data.go.kr/data/81177/linkedData.do) | +| 재난안전데이터 공유플랫폼 (data.go.kr 15139681 지역산사태위험) | 지구명 · 법정동코드 · 위험등급 · 고시일자 · 예상면적 · JSON · **도형 없음**(지정 위험지구 목록) | 「이용허락범위 제한 없음」 | 인증키 | 가능 · 위험지도 아님 | [D17](https://www.data.go.kr/data/15139681/openapi.do) | +| 한국지질자원연구원 산사태 취약성도 (85304~85306) | 일부 지역(키워드 강릉 · 보은 · 봉화 · 서울 · 인제 · 평창 · 이천광주) · asc · grid · tiff | 이용허락범위 빈칸 | — | 전국 아님 | [D18](https://www.data.go.kr/data/85304/linkedData.do) | +| 지자체 | 산사태 취약지역 현황 CSV(충남 · 경기 · 인천 연수구 · 창녕) — 지정지역 목록 | 「제한 없음」 등 | — | 위험지도 아님 · 일부 | [검색](https://www.data.go.kr/tcs/dss/selectDataSetList.do?keyword=%EC%82%B0%EC%82%AC%ED%83%9C%EC%9C%84%ED%97%98%EC%A7%80%EB%8F%84) | + +## 4. 산림빅데이터거래소 — 이용 조건 쪽 근거 + +| 항목 | 문구 · 사실 | 출처 | +|---|---|---| +| 운영 | 한국임업진흥원 AI빅데이터실 · 02-6393-2714 · 「유로 또는 무료로 제공되는 데이터」 · 구매회원 = 「웹·모바일 서비스 개발이나 사업 전략」 활용 개인 · 기업 | [K2](https://www.kofpi.or.kr/service/bigdata_01.do) | +| 약관 | 「구매회원은 구매 상품에 대하여 사무국에 허락을 득하지 않고 자체 사용 목적 외에 외부로 유출하여 발생되는 손해에 대하여 모든 책임을 부담하여야 합니다.」(검색 요약 · 전문 미열람) | [B8](https://www.bigdata-forest.kr/tou) | +| 상품 표기 | 임상도 4차 전국 · 임상도 1:5,000 · 임도망도 전국 = 「Creative Commons Attribution」 · ₩0 · 판매처 ㈜시선아이티 · 「Copyright 2019. SEESUNIT CO., LTD. all rights reserved」(검색 색인) | B4 · B2 · B5 | +| 접속 | 이 조사 환경에서 `ECONNREFUSED`(https · http 모두) · 검색 색인에 2026-01-05 공지 있음 | [B9](https://www.bigdata-forest.kr/support/notice/20260105093536) | +| 참고(공식 아님) | 나무신문 칼럼(2021-05-04) 「거의 무료로 다운로드 받을 수 있어, 학술연구 및 사업적인 목적으로 다양하게 이용할 수 있는 장점」 | [W1](https://www.imwood.co.kr/news/articleView.html?idxno=25690) | + +## 5. 「제한 없음」인데 견본뿐일 때 — 공식 길 + +| 길 | 쓰는 때 | 절차 · 기한 | 주소 · 연락 | +|---|---|---|---| +| ① 목록별 오류신고 · 데이터 개선요청 | 등록 목록의 파일 누락 | 포털 약관 「제공기관은 … 회원이 신고한 공공데이터 등록 누락, 오류신고, 이용불편사항 등에 대해서 조치를 취해야 합니다」 · 포털 공통 기한 = 확인 못 함(기관 안내 예: 경찰청 · 해양환경공단 10일 · 법무부 · 중기부 14일) | 각 목록 페이지 · [P1](https://www.data.go.kr/ugs/selectPortalPolicyView.do) · 활용지원센터 1566-0025 | +| ①′ 산림청 데이터 품질오류 신고 게시판 | 같음 | 신고접수 → 담당자 지정 → 담당자 처리 → 결과 안내 · 기한 표기 없음 · 1쪽에 전국 파일 요청 글 없음 | [F12](https://www.forest.go.kr/kfsweb/cop/bbs/selectBoardList.do?bbsId=BBSMSTR_1662&mn=NKFS_06_08_04) | +| ② 공공데이터 제공신청(법 제27조) | 목록에 없는 데이터 · 전국 원본 요청 | 「제공여부 심의 > 공공데이터제공(10일)」 · 10일 안 결정 · 부득이하면 10일 연장 · 불가 시 사유를 신청인 · 행안부에 통보 | [P3](https://www.data.go.kr/tcs/dor/insertDataOfferReqstProcssView.do) [M2](https://www.mois.go.kr/frt/sub/a06/b02/openData_4/screen.do) | +| ③ 공공데이터 제공 분쟁조정(법 제29 · 31조) | 제공 거부 · 제공 중단 처분 | 처분일부터 60일 안 신청 → 사실조사 → 조정 전 합의 권고 → 위원회 30일 안 조정안(의결로 연장) → 「조정결정일부터 15일 이내 … 수락」 = 재판상 화해 · 「공공기관은 특별한 사정이 없으면 조정안을 수락」(행안부) · 사용 중 문제는 다루지 않음 | [P4](https://www.data.go.kr/tcs/dor/insertTrublMdatReqstProcssView.do) [P5](https://www.data.go.kr/odmc/intro/index.do) · odmc@nia.or.kr · 02-6191-2064 | +| ④ 정보공개청구(정보공개법 제11조) | 과거판 산사태위험지도 등 | 10일 안 결정 · 10일 연장 · 이의신청 30일 | [open.go.kr](https://www.open.go.kr) | + +- 법 근거 참고 — 공공데이터법 제3조 ④ 영리 이용 제한 금지(513 조사 L1) · 저작권법 제24조의2 ① 「국가 또는 지방자치단체가 업무상 작성하여 공표한 저작물 … 허락 없이 이용할 수 있다」(제외 4가지 · 영리 여부 문구는 문체부 페이지에 없음)([C1](https://www.mcst.go.kr/site/s_policy/copyright/question/question07.jsp)) · 공공누리 유형([C2](https://www.kogl.or.kr/info/license.do)). + +## 6. 결론 표 + +| 주제도 | 창구 | 범위 | 조건 | 상용 | 주소 | +|---|---|---|---|---|---| +| 임상도 | data.go.kr 3045619 | 견본(제주) | 제한 없음 | 문구 OK · 파일 없음 | [D2](https://www.data.go.kr/data/3045619/fileData.do) | +| 임상도 | 산림청 개방 페이지 | 전국 | 공공기관 비영리 · 학술 전용 | 불가 | [F7](https://www.forest.go.kr/kfsweb/kfi/kfs/trail/fTMap.do?pblicDataId=PBD0000059&tabs=1&mn=NKFS_06_08_02&subTitle=%EC%9E%84%EC%83%81%EB%8F%84) | +| 임상도 | FGIS 신청 | 전국 1:25,000 · 1:5,000 | 서약 | 불가 쪽 | [F1](https://www.forest.go.kr/newkfsweb/kfi/kfs/fgis/selectAvailMapList.do?mn=KFS_02_04_02_02&orgId=fgis) | +| 임상도 | 브이월드 30136 | 전국 1:5,000 | CC BY-NC-ND | 불가 | [V1](https://www.vworld.kr/dtmk/dtmk_ntads_s002.do?svcCde=MK&dsId=30136) | +| 임상도 | 산림빅데이터거래소 4차 1:25,000 · 1:5,000 | 전국 | CC BY 표기 · 약관 외부 유출 제한 | 확인 못 함 | [B4](https://www.bigdata-forest.kr/product/FRT001201) | +| 임도망도 | data.go.kr 3045621 | 견본 | 제한 없음 | 문구 OK · 파일 없음 | [D3](https://www.data.go.kr/data/3045621/fileData.do) | +| 임도망도 | FGIS | 1:5,000 | 공공 · 연구 전용 | 불가 | [F1](https://www.forest.go.kr/newkfsweb/kfi/kfs/fgis/selectAvailMapList.do?mn=KFS_02_04_02_02&orgId=fgis) | +| 임도망도 | 산림빅데이터거래소 FRT002601 | 전국(2019) | CC BY 표기 · 약관 제한 | 확인 못 함 | [B2](https://www.bigdata-forest.kr/product/FRT002601) | +| 임도망도 | 연속수치지형도 | 전국 | 공공누리 1유형 | 가능 · 임도 포함 확인 못 함 | [D15](https://www.data.go.kr/data/15059721/fileData.do) | +| 산사태 | data.go.kr 15074817 | 견본(제주) | 제한 없음 | 문구 OK · 파일 없음 | [D7](https://www.data.go.kr/data/15074817/fileData.do) | +| 산사태 | FGIS 일반인(2025년판) | 전국 | 서약 | 불가 쪽 | [F10](https://www.forest.go.kr/newkfsweb/cop/bbs/selectBoardArticle.do?nttId=3215640&bbsId=BBSMSTR_1351&mn=KFS_02_04_07_02&orgId=fgis) | +| 산사태 | 브이월드 지도 | 전국 타일 · 원본 방문 수령 | 표기 없음 | 확인 못 함 | [V5](https://www.vworld.kr/dtmk/dtmk_ntads_s002.do?svcCde=DT&dsId=DAT_0000000000000096) | +| 산사태 | 생활안전지도 WMS | 전국 | 공공누리 4 | 불가 | [S1](https://www.safemap.go.kr/opna/data/dataViewRenew.do?objtId=125) | +| 산사태 | 거래소 산사태발생위험등급 API | 전국 | 빈칸 | 확인 못 함 | [D16](https://www.data.go.kr/data/81177/linkedData.do) | + +권함 순서: + +1. **data.go.kr 목록별 오류신고(3045619 · 3045621 · 15074817)** — 「제한 없음 표기 · 전국 파일 없음 → 전국 파일 등록 또는 표기 정정」 요청 + 산림청 산림디지털담당관실(042-481-4285 · 4169)에 상용 탑재 서면 질의. 「제한 없음」 = 상용 근거 문구가 가장 또렷한 자리. +2. 답 없음 · 거부 → **제공신청**(전국 원본 · 10일 + 10일) → 거부 처분 → **분쟁조정**(60일 안). +3. 병행 — **산림빅데이터거래소 사무국**(한국임업진흥원 AI빅데이터실 02-6393-2714)에 임상도 4차 전국 · 임도망도 전국 상용 탑재 허락 서면 요청(CC BY 표기 근거 · 약관 「사무국 허락」 조항 근거). 국내 망에서 상품 페이지 · 약관 전문 직접 열람 선행. +4. 임도 보조 후보 — 연속수치지형도(1유형) 도로 레이어에 임도가 들어 있는지 확인. +5. 산사태 — 상용 가능한 전국 파일 창구 없음 · 1 · 2 결과 기다림. + +## 7. 확인 못 함 + +- 산림빅데이터거래소 상품 페이지 · 약관 전문 직접 열람(접속 거부) · CC BY 표기와 「all rights reserved」 · 「자체 사용 목적」 조항의 관계 · 임상도 1:5,000 전국판(FRT001001) 라이선스. +- data.go.kr 목록별 오류신고의 포털 공통 처리 기한. +- 연속수치지형도에 임도 레이어 · 코드 존재 여부. +- 브이월드 산사태 타일의 이용 조건 · 정보공개청구로 받은 자료의 이용 조건. +- egis · AI Hub · 경기 · 강원 · 경북 공간정보포털의 임상도 · 임도 · 산사태 자료(검색에 안 나옴). +- 옛 오픈마켓 산사태 SHP(2019)의 이관 여부. + +## 출처 (513 조사에 없는 것) + +- F7 https://www.forest.go.kr/kfsweb/kfi/kfs/trail/fTMap.do?pblicDataId=PBD0000059&tabs=1&mn=NKFS_06_08_02 — 산림청 공공데이터 개방 임상도 +- F8 https://www.forest.go.kr/newkfsweb/html/HtmlPage.do?pg=%2Ffgis%2FUI_KFS_5002_020700.html&mn=KFS_02_04_03_04_04&orgId=fgis — 임도망도 현황 +- F9 https://www.forest.go.kr/kfsweb/kfi/kfs/trail/sanSaTae.do?mn=NKFS_01_01 — 산사태위험지도 개방 견본 +- F10 https://www.forest.go.kr/newkfsweb/cop/bbs/selectBoardArticle.do?nttId=3215640&bbsId=BBSMSTR_1351&mn=KFS_02_04_07_02&orgId=fgis — 산사태위험지도 다운 문의 답변(2026-01-14) +- F11 https://www.forest.go.kr/newkfsweb/cop/bbs/selectBoardArticle.do?nttId=3182126&bbsId=BBSMSTR_1351&mn=KFS_02_04_07_02&orgId=fgis — 산사태위험지도 SHP 답변(2023-04-13) +- F12 https://www.forest.go.kr/kfsweb/cop/bbs/selectBoardList.do?bbsId=BBSMSTR_1662&mn=NKFS_06_08_04 — 산림청 데이터 품질오류 신고 +- D9~D18 data.go.kr 258662 · 95584 · 95799 · 15006041 · 15120710 · 15071663 · 15059721 · 81177 · 15139681 · 85304 +- V4 https://www.vworld.kr/dtmk/dtmk_ntads_s001.do?svcCde=MK — 브이월드 데이터 목록(산림청 12건) +- V5 https://www.vworld.kr/dtmk/dtmk_ntads_s002.do?svcCde=DT&dsId=DAT_0000000000000096 — 브이월드 산사태위험지도 +- N1 http://data.nsdi.go.kr/dataset/20210708ds00009 — 옛 오픈마켓(접속 불가 · 검색 색인만) +- B2 · B4~B9 https://www.bigdata-forest.kr/… — 산림빅데이터거래소(검색 색인만) +- K1 https://www.kgeop.go.kr/ · K2 https://www.kofpi.or.kr/service/bigdata_01.do +- P3 https://www.data.go.kr/tcs/dor/insertDataOfferReqstProcssView.do · P4 https://www.data.go.kr/tcs/dor/insertTrublMdatReqstProcssView.do · P5 https://www.data.go.kr/odmc/intro/index.do · M2 https://www.mois.go.kr/frt/sub/a06/b02/openData_4/screen.do +- C1 https://www.mcst.go.kr/site/s_policy/copyright/question/question07.jsp · C2 https://www.kogl.or.kr/info/license.do +- W1 https://www.imwood.co.kr/news/articleView.html?idxno=25690 From 1d71b66dea553a132f75458ce5fe587cd4516588 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sat, 31 Oct 2026 17:28:40 +0900 Subject: [PATCH 02/11] =?UTF-8?q?chore(=EC=84=A4=EC=A0=95):=20=EC=83=9D?= =?UTF-8?q?=ED=99=9C=EC=95=88=EC=A0=84=EC=A7=80=EB=8F=84=20=ED=82=A4=20.en?= =?UTF-8?q?v=20=EC=97=90=20=EB=84=A3=EC=9D=8C=20=C2=B7=20=EC=9A=94?= =?UTF-8?q?=EA=B5=AC=20520=20=C2=B7=20521=20=EC=9B=90=EB=AC=B8=20=EA=B8=B0?= =?UTF-8?q?=EB=A1=9D(=ED=82=A4=20=EA=B0=92=20=EB=B9=BC=EA=B3=A0)=20?= =?UTF-8?q?=E2=80=94=20=EC=98=A4=EB=A5=98=EC=8B=A0=EA=B3=A0=20=EC=B7=A8?= =?UTF-8?q?=EC=86=8C?= 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_015uTVHHDv583hbLQAFeBmd9 --- .env | 2 +- docs/raw/요구_1025/사용자요구_1025.md | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/.env b/.env index 4d9fecafd..2e3d4cb5c 100644 --- a/.env +++ b/.env @@ -89,5 +89,5 @@ AUTH_OTP_DISABLED=True # 대시보드 지식 챗봇(B01_Dashboard_Knowledge_Chat) NVIDIA 키 NVIDIA_API_KEY=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 # 노선분석 산사태위험지도(행안부 생활안전지도 WMS IF_0046 · D03_PlanAnalysis_Landslide) 키 — 비우면 겹 꺼짐 -SAFEMAP_API_KEY= +SAFEMAP_API_KEY=MFO8CY4O-MFO8-MFO8-MFO8-MFO8CY4O6X DATA_GO_KR_API_KEY=0j5WZHeNxSkdPVSLXUsx9kVikXEN%2BDc%2Byv68jz8IpKFffjbZkRiQrJO78y6rxPCTqhiSOW4lDxpid0twkSVQ9g%3D%3D diff --git a/docs/raw/요구_1025/사용자요구_1025.md b/docs/raw/요구_1025/사용자요구_1025.md index e0828c8d3..775ee4e45 100644 --- a/docs/raw/요구_1025/사용자요구_1025.md +++ b/docs/raw/요구_1025/사용자요구_1025.md @@ -1045,3 +1045,11 @@ CPU는 더늘리기는 힘들어보이고 GPU 사용을 위해 부품사용에 ## 요구 519 (10-31 · 사용자) 같은 데이터 인지 확인해보면 될것같은데. / (붙여넣음: 산사태위험지도 WMS — 재난안전 · 제공기관 산림청 · REST · WMS · 활용 신청 258 · 갱신주기 1년 · 수정일 2025-05-30 · 5등급 · 호출 safemap.go.kr/openapi2/IF_0046_WMS · 레이어 A2SM_SANSATAI · serviceKey · srs · bbox · format · width · height · transparent · 범례 lgdInfo?intId=IF_0046) 이거 확인해봐. 그리고 포털 활용신청은 한거야. 이전 채팅에 일반 인증키가 있잖아. + +## 요구 520 (10-31 · 사용자) + +(붙여넣음: 이름 · 이메일 · 발급키 — 값 적지 않음) 이게 행안부 오픈 api키야. + +## 요구 521 (10-31 · 사용자) + +오류 신고는 취소해. From 0e8270518b96d821822655475ef14b7124872a96 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sat, 31 Oct 2026 17:35:01 +0900 Subject: [PATCH 03/11] =?UTF-8?q?knowledge(=EC=9A=94=EA=B5=AC):=20?= =?UTF-8?q?=EC=9A=94=EA=B5=AC=20522=20=EC=9B=90=EB=AC=B8=20=EA=B8=B0?= =?UTF-8?q?=EB=A1=9D=20=E2=80=94=20=ED=99=94=EB=A9=B4=20=ED=99=95=EC=9D=B8?= =?UTF-8?q?=EC=9D=80=20=EB=82=98=EC=A4=91=EC=97=90=20=ED=95=9C=20=EB=B2=88?= =?UTF-8?q?=EC=97=90=20=C2=B7=20=EB=8B=A4=EC=9D=8C=20=EC=B0=BE=EC=9D=84=20?= =?UTF-8?q?=EC=9E=90=EB=A3=8C=20=EB=AC=BC=EC=9D=8C?= 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_015uTVHHDv583hbLQAFeBmd9 --- docs/raw/요구_1025/사용자요구_1025.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/raw/요구_1025/사용자요구_1025.md b/docs/raw/요구_1025/사용자요구_1025.md index 775ee4e45..7abb76e1b 100644 --- a/docs/raw/요구_1025/사용자요구_1025.md +++ b/docs/raw/요구_1025/사용자요구_1025.md @@ -1053,3 +1053,7 @@ CPU는 더늘리기는 힘들어보이고 GPU 사용을 위해 부품사용에 ## 요구 521 (10-31 · 사용자) 오류 신고는 취소해. + +## 요구 522 (10-31 · 사용자) + +일단 확인은 나중에 한번에 하고 다음으로 찾아야할 데이터는? From e0b90876ce75578ddccc36868b171a2af34b37bb Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sat, 31 Oct 2026 17:51:31 +0900 Subject: [PATCH 04/11] =?UTF-8?q?knowledge(=EC=9A=94=EA=B5=AC):=20?= =?UTF-8?q?=EC=9A=94=EA=B5=AC=20523=20=EC=9B=90=EB=AC=B8=20=EA=B8=B0?= =?UTF-8?q?=EB=A1=9D=20=E2=80=94=20=EB=B0=9B=EC=9D=80=20=EC=85=8B=20=3D=20?= =?UTF-8?q?=EC=98=A4=ED=94=88API=20=C2=B7=20=EC=9E=84=EB=8F=84=EB=A7=9D?= =?UTF-8?q?=EB=8F=84=20=EA=B2=BD=EB=B6=81=2047.zip=20=ED=99=95=EC=9D=B8?= 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_015uTVHHDv583hbLQAFeBmd9 --- docs/raw/요구_1025/사용자요구_1025.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/raw/요구_1025/사용자요구_1025.md b/docs/raw/요구_1025/사용자요구_1025.md index 7abb76e1b..37f061ef9 100644 --- a/docs/raw/요구_1025/사용자요구_1025.md +++ b/docs/raw/요구_1025/사용자요구_1025.md @@ -1057,3 +1057,7 @@ CPU는 더늘리기는 힘들어보이고 GPU 사용을 위해 부품사용에 ## 요구 522 (10-31 · 사용자) 일단 확인은 나중에 한번에 하고 다음으로 찾아야할 데이터는? + +## 요구 523 (10-31 · 사용자) + +지금 받은 3개는 결국 전부 오픈 API야 참고해. 다음으로 임도망도은 산림공간정보에서 받았는데 경상북도 지역만 일단 받았어. 확인해봐. 47.zip이야. From 481363e792d429df021c35812bcfd2b98519ac63 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sat, 31 Oct 2026 18:03:10 +0900 Subject: [PATCH 05/11] =?UTF-8?q?knowledge(=EC=9A=94=EA=B5=AC):=20?= =?UTF-8?q?=EC=9A=94=EA=B5=AC=20524=20=EC=9B=90=EB=AC=B8=20=EA=B8=B0?= =?UTF-8?q?=EB=A1=9D=20=E2=80=94=20=EA=B5=AD=EA=B0=80=EC=9E=84=EB=8F=84?= =?UTF-8?q?=EB=A7=8C=20=3D=20=EB=A7=9E=EC=9D=8C=20=C2=B7=20=EC=A7=80?= =?UTF-8?q?=EB=B0=A9=20=C2=B7=20=EB=AF=BC=EA=B0=84=EC=9E=84=EB=8F=84?= =?UTF-8?q?=EB=8A=94=20=EC=82=AC=EC=9A=A9=EC=9E=90=20=EC=9E=85=EB=A0=A5=20?= =?UTF-8?q?=C2=B7=20=EC=9E=84=EB=8F=84=EB=A7=9D=EB=8F=84=20=EC=A0=84?= =?UTF-8?q?=EC=B2=B4=20=EB=B0=9B=EC=9D=8C=20=ED=99=95=EC=9D=B8?= 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_015uTVHHDv583hbLQAFeBmd9 --- docs/raw/요구_1025/사용자요구_1025.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/raw/요구_1025/사용자요구_1025.md b/docs/raw/요구_1025/사용자요구_1025.md index 37f061ef9..2fd023fc0 100644 --- a/docs/raw/요구_1025/사용자요구_1025.md +++ b/docs/raw/요구_1025/사용자요구_1025.md @@ -1061,3 +1061,7 @@ CPU는 더늘리기는 힘들어보이고 GPU 사용을 위해 부품사용에 ## 요구 523 (10-31 · 사용자) 지금 받은 3개는 결국 전부 오픈 API야 참고해. 다음으로 임도망도은 산림공간정보에서 받았는데 경상북도 지역만 일단 받았어. 확인해봐. 47.zip이야. + +## 요구 524 (10-31 · 사용자) + +당연히 임도는 국유림에 진행되기 때문에 알수 없는게 맞아. 사유지나 지자체가 하는건 사용자가 직접 넣는게 맞는것같아. 다음으로 지금 전체를 다운받아서 넣었어. 확인해봐. From ab5e5c8277cc9b37539a802c4ba5c28a2faa7eb3 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sat, 31 Oct 2026 18:09:40 +0900 Subject: [PATCH 06/11] =?UTF-8?q?knowledge(=EC=9A=94=EA=B5=AC):=20?= =?UTF-8?q?=EC=9A=94=EA=B5=AC=20525=20=EC=9B=90=EB=AC=B8=20=EA=B8=B0?= =?UTF-8?q?=EB=A1=9D=20=E2=80=94=20=EC=9E=84=EC=83=81=EB=8F=84=20=EC=A0=9C?= =?UTF-8?q?=EC=99=B8=20=C2=B7=20=EB=8B=A4=EC=9D=8C=20=EB=B3=B4=ED=98=B8=20?= =?UTF-8?q?=C2=B7=20=EA=B7=9C=EC=A0=9C=20=EA=B5=AC=EC=97=AD=20=C2=B7=20?= =?UTF-8?q?=EC=83=9D=ED=83=9C=EC=9E=90=EC=97=B0=EB=8F=84=20=C2=B7=20?= =?UTF-8?q?=EC=A7=80=EC=A7=88=EB=8F=84?= 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_015uTVHHDv583hbLQAFeBmd9 --- docs/raw/요구_1025/사용자요구_1025.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/raw/요구_1025/사용자요구_1025.md b/docs/raw/요구_1025/사용자요구_1025.md index 2fd023fc0..5c66a1def 100644 --- a/docs/raw/요구_1025/사용자요구_1025.md +++ b/docs/raw/요구_1025/사용자요구_1025.md @@ -1065,3 +1065,7 @@ CPU는 더늘리기는 힘들어보이고 GPU 사용을 위해 부품사용에 ## 요구 524 (10-31 · 사용자) 당연히 임도는 국유림에 진행되기 때문에 알수 없는게 맞아. 사유지나 지자체가 하는건 사용자가 직접 넣는게 맞는것같아. 다음으로 지금 전체를 다운받아서 넣었어. 확인해봐. + +## 요구 525 (10-31 · 사용자) + +임상도는 일단안해도 될것같은데 어떻게 생각함? / 좋아 제외하자. 다음 확보데이터는 너가 말했던 1. 보호·규제 구역 묶음 2. 생태자연도 항목이야. 3. 지질도 (암상) 인데 가장 먼저 1번 보호 규제 구역 묶음인데 이중에 벌써 받은 정보가 있었을거 같은데? From 3c2c82c78e661feced15bcc7503ae6607f5b0141 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sat, 31 Oct 2026 18:19:09 +0900 Subject: [PATCH 07/11] =?UTF-8?q?fix(M02=20=EA=B8=B0=EC=A4=80=20=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D=C2=B7=EA=B3=84=EC=82=B0=EC=84=9C):=20=EB=81=9D?= =?UTF-8?q?=EC=88=98=20=3D=20B09=20=EC=82=B0=EA=B7=BC=20=EA=B7=9C=EC=B9=99?= =?UTF-8?q?(=EC=A4=84=EB=A7=88=EB=8B=A4=20=EB=B2=84=EB=A6=BC=20=C2=B7=20?= =?UTF-8?q?=EB=B9=84=EB=AA=A9=EB=A7=88=EB=8B=A4=20=EB=B2=84=EB=A6=BC)?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EB=A7=9E=EC=B6=A4=20=E2=80=94=20=EA=B3=84?= =?UTF-8?q?=EC=82=B0=EC=84=9C=205,654=20=EB=8C=80=20B09=205,651=20?= =?UTF-8?q?=EC=96=B4=EA=B8=8B=EB=82=A8=20(PLAN=20704-2=20=EB=94=B8?= =?UTF-8?q?=EB=A6=BC)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 새 `M02_Logic_Settle` — 엔진 구조 그대로 줄 ROUNDDOWN 1 · 비목 ROUNDDOWN 0 · 작업량 Q ROUND 2 · 하위 로직 같은 규칙 - 기준 목록 금액 · 계산서 줄 · 소계 · 계가 같은 값 · 목록 저장본 꼴 번호 올림 · 머리 글에 지식DB 단수처리 근거 - 시험: 거울 test_704_settle(B09 _money · block_totals) · 옛 목록 시험은 끝수 차 3 원 안으로 고침 Co-Authored-By: Claude Sonnet 5.5 --- M02_MasterTemplete/M02_Logic_Settle.py | 115 ++++++++++++++++++++ M02_MasterTemplete/M02_Logic_Standard.py | 17 ++- resources/tester/test_704_logic_standard.py | 5 +- resources/tester/test_704_settle.py | 75 +++++++++++++ 4 files changed, 205 insertions(+), 7 deletions(-) create mode 100644 M02_MasterTemplete/M02_Logic_Settle.py create mode 100644 resources/tester/test_704_settle.py diff --git a/M02_MasterTemplete/M02_Logic_Settle.py b/M02_MasterTemplete/M02_Logic_Settle.py new file mode 100644 index 000000000..8900471e7 --- /dev/null +++ b/M02_MasterTemplete/M02_Logic_Settle.py @@ -0,0 +1,115 @@ +"""M02 기준 목록 · 계산서 끝수(PLAN 704-2 딸림) — B09 산근 끝수 규칙으로 셈(③ 서버 단독 · +`B09_Estimation_Sangeun` 과 규칙 짝 · 거울 시험 `test_704_settle`). + +줄 금액 = ROUNDDOWN(단가 × 수량, 1) 비목마다 · 비목 총계 = ROUNDDOWN(Σ 줄, 0) · 계 = 셋 합 · +작업량 Q(중간 이름 Q · Q1 …) = ROUND(식, 2) 뒤 쓰임 · % 덧줄 밑수 = 그때까지 줄 합(줄 끝수 뒤). +근거 = 지식DB `technical_info/01_임도/05_원가정보/단수처리_규칙.md`(행 금액 절사 · 관측 후보 규칙). +M01 엔진(`master_formula.run`)의 구조 그대로 — 끝수 자리만 다름(엔진은 줄 끝수 없이 계에서 한 번). +하위 로직(기계경비 포함)은 같은 규칙으로 다시 셈 · 흐름 · 결과 로직은 엔진 값 그대로. +""" + +from __future__ import annotations + +import re +from decimal import ROUND_DOWN, ROUND_HALF_UP, Decimal +from typing import Any + +from M01_MasterData import M01_MasterData_Store as m01 + +mf = m01.cm.mf +_Q = re.compile(r"Q\d*") + + +def _down(value: Any, places: int) -> Any: + return None if value is None else mf._round(Decimal(value), places, ROUND_DOWN) + + +def _bundle(whole: Any, item: dict, env: dict, depth: int) -> dict: + """로직 호표 줄이 부르는 하위 로직 — 같은 규칙으로 다시 셈(엔진 `logic` 마디 그대로).""" + node = mf.parse(item["요소"]) + if node[0] != "logic": + return mf.evaluate(node, env, whole, depth) + given = {k: mf.evaluate(v, env, whole, depth) for k, v in node[2]} + got = settle(whole, mf.fill(node[1], env), given, depth + 1) + return got["결과"] if "결과" in got else {k: got[k] for k in mf.BUNDLE} + + +def settle(whole: Any, ref: str, given: dict, depth: int = 0) -> dict: + """`master_formula.run` 과 같은 꼴 `{줄, 노무비, 재료비, 경비, 계, 중간}` — 끝수만 B09 규칙.""" + row = whole.logic(ref) + if isinstance(row.get("흐름"), dict) or "결과" in row: + return mf.run(whole, ref, given, depth) + env = mf._inputs(row, given) + for step in row.get("중간", []): + try: + value = mf.evaluate(mf.parse(step["식"]), env, whole, depth) + except mf.EmptyInput as e: + env[step["이름"]] = mf.Blank(str(e)) + continue + if _Q.fullmatch(step["이름"]) and isinstance(value, Decimal): + value = value.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP) + env[step["이름"]] = value + lines: list[dict] = [] + for n, item in enumerate(row.get("호표", [])): + try: + qty = mf.evaluate(mf.parse(item["수량"]), env, whole, depth) + if item["종류"] == "로직": + bundle = _bundle(whole, item, env, depth) + price = bundle["계"] + split = ( + {k: qty * bundle[k] for k in mf.COST_ITEMS} + if "비목" not in item + else {item["비목"]: qty * price} + ) + else: + ref_ = ( + { + k: mf.fill(v, env) if isinstance(v, str) else v + for k, v in item["요소"].items() + } + if isinstance(item["요소"], dict) + else mf.fill(item["요소"], env) + ) + if isinstance(ref_, str) and qty != 0: + whole.get(ref_) + price = mf.mp.priced(whole, item, ref_, env, n if depth == 0 else None)[0] + if price is None and qty == 0: + price = Decimal(0) + split = {item["비목"]: None if price is None else qty * price} + except mf.EmptyInput as e: + lines.append(mf._blank_line(item, str(e))) + continue + lines.append(_line(item["이름"], split)) + sums = {k: sum((x["비목"].get(k) or 0 for x in lines), Decimal(0)) for k in mf.COST_ITEMS} + env["줄"] = {x["이름"]: x["금액"] for x in lines if x["이름"] and x["금액"] is not None} + for extra in row.get("덧줄", []): + env.update(sums) + try: + value = mf.evaluate(mf.parse(extra["식"]), env, whole, depth) + env[extra["이름"]] = value + except mf.FormulaError: + value = None + sums[extra["비목"]] += _down(value, 1) or 0 + lines.append(_line(extra["이름"], {extra["비목"]: value})) + if value is not None: + env["줄"][extra["이름"]] = lines[-1]["금액"] + sums = {k: mf.cut(row, _down(v, 0), "계") for k, v in sums.items()} + return {"줄": lines, **sums, "계": sum(sums.values()), "중간": mf._shown(env)} + + +def _line(name: str, split: dict) -> dict: + cut = {k: _down(v, 1) for k, v in split.items()} + whole_sum = None if all(v is None for v in cut.values()) else sum(v or 0 for v in cut.values()) + return {"이름": name, "비목": cut, "금액": whole_sum} + + +def apply(groups: list[dict], engine: dict, done: dict) -> None: + """계산서 묶음의 줄 금액 · 소계를 `settle` 값으로 바꿈 — 줄 차례는 엔진과 같음.""" + seq: dict[str, list] = {k: [] for k in mf.COST_ITEMS} + for eng, mine in zip(engine["줄"], done["줄"]): + for part, _ in m01.mtx._shares(eng): + seq[part].append(mine["비목"].get(part)) + for group in groups: + for line, amount in zip(group["줄"], seq.get(group["비목"], [])): + line["금액"] = amount + group["소계"] = done[group["비목"]] diff --git a/M02_MasterTemplete/M02_Logic_Standard.py b/M02_MasterTemplete/M02_Logic_Standard.py index 71eaac2cb..bcfd69de3 100644 --- a/M02_MasterTemplete/M02_Logic_Standard.py +++ b/M02_MasterTemplete/M02_Logic_Standard.py @@ -5,12 +5,13 @@ 위 끝) · 그 밖 연속 값 입력은 견본 자동값(`master_auto.values`) 한 줄 · 지역 = 자동값(전국평균). 펼침 = 바탕(자동값 · 규격 축은 첫 대표 값) 한 줄 + 규격 축마다 값 하나씩 바꾼 줄(곱하지 않음 · 바꾼 줄이 0 원이면 표에 없는 조합으로 보고 뺌). -금액 = M01 엔진(`master_formula.run`) 「계」 — M01 시험 계산과 같은 셈 · 최신 판 노임(마스터 판). +금액 = M01 엔진 식 + B09 산근 끝수(줄마다 버림 · `M02_Logic_Settle` · 근거 지식DB +`technical_info/01_임도/05_원가정보/단수처리_규칙.md`) · 최신 판 노임(마스터 판). 목록은 서버가 뒤에서 한 번 셈(프로세스 `WORKERS` 개 · 수 분) → 프로세스 메모리 + 파생 저장본 (storage `_derived/m02_logic_standard/`) · 열쇠 = 마스터 내용 지문 + `SHAPE` · 같으면 안 셈 · 셈 도중 목록 = 빈 줄 `준비: false`(화면은 로직 줄 그대로 · 금액 빈칸). 실무 낱말(`WORDS`) = 별칭 칸에 덧붙여 찾기(공용 `matchLogic`)가 「석축」 → 돌쌓기를 찾음. -계산서 = `master_text.lines`(줄 「석공 | 268,908 * 0.1 = 26,891」 · 비목 소계 · 계 = 엔진 값). +계산서 = `master_text.lines`(줄 「석공 | 268,908 * 0.1 = 26,891」 · 비목 소계 · 계) + 같은 끝수 값. M01 은 읽기만 · 권한은 부르는 쪽 몫(로그인). """ @@ -27,9 +28,10 @@ from typing import Any from common_util.common_util_derived_store import derived_key, read_derived, write_derived from M01_MasterData import M01_MasterData_Store as m01 from M02_MasterTemplete import M02_Labor_Inputs as labor_inputs +from M02_MasterTemplete import M02_Logic_Settle as settle from M02_MasterTemplete import M02_Template_Layers as layers -SHAPE = 2 # 줄 꼴이 바뀌면 올림 — 옛 저장본을 안 씀 +SHAPE = 3 # 줄 꼴이 바뀌면 올림 — 옛 저장본을 안 씀 KIND = "m02_logic_standard" REGION = "지역" # 지역 = 자동값 한 벌(정함 8) QUIET = ("해당 없음", "없음") # 규격 글에서 뺄 값 @@ -168,7 +170,7 @@ def _money(whole: Any, logic: dict, given: dict) -> Decimal | None: if not str(logic.get("결과단위") or "").startswith("원"): return None try: - got = m01.cm.mf.run(whole, str(logic["키"]), m01.cm.mf.fill_empty(logic, given)) + got = settle.settle(whole, str(logic["키"]), m01.cm.mf.fill_empty(logic, given)) except (m01.cm.mf.FormulaError, ArithmeticError, KeyError, TypeError, ValueError): return None return got.get("계", got.get("결과")) @@ -325,7 +327,12 @@ def sheet(key: str, change: dict) -> dict: except m01.cm.mf.FormulaError as e: raise m01.StoreError(404, str(e)) from e given, ax = _given(whole, logic, change) - got = m01.mtx.lines(files, key, m01.cm.mf.fill_empty(logic, given)) + filled = m01.cm.mf.fill_empty(logic, given) + got = m01.mtx.lines(files, key, filled) + done = settle.settle(whole, key, filled) + if "결과" not in done: # 돈 로직 — 줄마다 버림(B09 산근 규칙) + settle.apply(got.get("groups") or [], m01.cm.mf.run(whole, key, filled), done) + got["계"] = done["계"] groups = [ { "비목": g["비목"], diff --git a/resources/tester/test_704_logic_standard.py b/resources/tester/test_704_logic_standard.py index 2cd394958..97a5f2191 100644 --- a/resources/tester/test_704_logic_standard.py +++ b/resources/tester/test_704_logic_standard.py @@ -34,13 +34,14 @@ def _given(key: str, change: dict) -> dict: @pytest.mark.parametrize("key", THREE) -def test_줄_금액은_M01_시험_계산(key: str) -> None: +def test_줄_금액은_M01_시험_계산에_끝수만_버림(key: str) -> None: rows = _rows(key) assert rows for row in rows: got = m01.calc(key, _given(key, row["바꿈"])) assert got["ok"], got - assert row["금액"] == pytest.approx(float(got["sums"]["계"]), abs=0.01), row["규격"] + gap = float(got["sums"]["계"]) - row["금액"] # 줄 · 비목 버림(B09 산근 규칙) — 최대 3 원 + assert -0.01 <= gap < 3.2, row["규격"] def test_대표_규격_펼침() -> None: diff --git a/resources/tester/test_704_settle.py b/resources/tester/test_704_settle.py new file mode 100644 index 000000000..7e85217bd --- /dev/null +++ b/resources/tester/test_704_settle.py @@ -0,0 +1,75 @@ +"""704-2 딸림 — 기준 목록 · 계산서 끝수 = B09 산근 규칙(줄마다 ROUNDDOWN 1 · 비목마다 ROUNDDOWN 0). + +거울: 엔진 줄(수량 · 단가)에 B09 `_money` · `block_totals` 를 그대로 씌운 값 = `settle` 값. +""" + +import re +import sys +from decimal import Decimal +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] +if str(ROOT) not in sys.path: + sys.path.insert(0, str(ROOT)) + +from B09_Estimation import B09_Estimation_Sangeun as sg # noqa: E402 +from M01_MasterData import M01_MasterData_Store as m01 # noqa: E402 +from M02_MasterTemplete import M02_Logic_Settle as st # noqa: E402 +from M02_MasterTemplete import M02_Logic_Standard as ls # noqa: E402 + +mf = m01.cm.mf +PART_OF = {"노무비": "labor", "재료비": "material", "경비": "expense"} + + +def _plain(row: dict) -> bool: + """로직 줄 · 덧줄 · 작업량 Q 가 없는 돈 로직 — 엔진 줄만으로 B09 값을 낼 수 있음.""" + return ( + str(row.get("결과단위") or "").startswith("원") + and not row.get("덧줄") + and not row.get("끝수") + and not any(i.get("종류") == "로직" for i in row.get("호표") or []) + and not any(re.fullmatch(r"Q\d*", s["이름"]) for s in row.get("중간") or []) + ) + + +def test_줄마다_버림_비목마다_버림_B09와_같음(): + files, whole = m01.loaded() + checked = 0 + for _, data in m01._logic_files(files): + for row in data.get("줄") or []: + if not _plain(row) or checked >= 60: + continue + base, _ = ls.axes(whole, row) + given = mf.fill_empty(row, base) + got = mf.run(whole, row["키"], given) + lines = [] + for line in got["줄"]: + one = dict.fromkeys(PART_OF.values(), 0.0) + for part, money in line["비목"].items(): + one[PART_OF[part]] = sg._money(1.0, float(money)) if money is not None else None + lines.append(one) + if any(v is None for one in lines for v in one.values()): + continue + want = sg.block_totals(lines) + mine = st.settle(whole, row["키"], given) + for name, part in PART_OF.items(): + assert float(mine[name]) == want[part], (row["키"], name) + assert float(mine["계"]) == want["sum"], row["키"] + checked += 1 + assert checked >= 30 + + +def test_계산서_줄_소계_계가_맞물림(): + files, whole = m01.loaded() + key = "GF000089" + got = ls.sheet(key, {}) + assert got["묶음"], key + for group in got["묶음"]: + lines = [Decimal(str(x["금액"] or 0)) for x in group["줄"]] + assert all(v == v.quantize(Decimal("0.1")) for v in lines), group["비목"] + assert group["소계"] == int(group["소계"]) + assert group["소계"] <= sum(lines) < group["소계"] + 1 + Decimal("0.05"), group["비목"] + assert got["계"] == sum(g["소계"] for g in got["묶음"]) + assert got["계"] == ls._money( + whole, whole.logic(key), ls._given(whole, whole.logic(key), {})[0] + ) From 8c65a323ccc86a717cf0b6b0c032b73a556f9156 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sat, 31 Oct 2026 18:30:51 +0900 Subject: [PATCH 08/11] =?UTF-8?q?fix(=EB=B2=88=ED=98=B8=20=EB=85=B8?= =?UTF-8?q?=EC=B6=9C):=20=EB=B3=B5=EC=82=AC=20=EC=B0=BD=20=EA=B7=9C?= =?UTF-8?q?=EA=B2=A9=20=C2=B7=20=EC=9E=AC=EB=A3=8C=20=EC=B0=BD=EA=B3=A0=20?= =?UTF-8?q?=EA=B7=9C=EA=B2=A9=20=C2=B7=20B09=20=EC=A4=84=20=EB=B9=84?= =?UTF-8?q?=EA=B3=A0=20=C2=B7=20M02=20=EA=B2=A9=EC=9E=90=20=EB=A8=B8?= =?UTF-8?q?=EB=A6=AC=20=C2=B7=20=EB=B9=84=EA=B3=A0=20=3D=20=EC=82=AC?= =?UTF-8?q?=EB=9E=8C=20=EB=A7=90(=EC=B8=B5=20=EB=82=B1=EB=A7=90=20=C2=B7?= =?UTF-8?q?=20=EC=9D=B4=EB=A6=84)=20=C2=B7=20=EB=B2=88=ED=98=B8=EB=8A=94?= =?UTF-8?q?=20=ED=92=8D=EC=84=A0=EB=A7=8C=20(PLAN=20704=20=EB=94=B8?= =?UTF-8?q?=EB=A6=BC=20=E2=91=A4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 사전조합 대가 블록 규격 = 「내 자료집 · 회사 · 마스터」 · 키는 spec_raw(풍선) · 복사해 새 일위대가 창 규격은 빈 칸 - B09 설계내역서 구조물 줄 비고 = 「층 낱말 · 조합 이름」(옛 「personal|1/3|SP3-000015」) - M02 일위대가 격자 머리 = 번호꼴 일위대가번이면 이름 · 「일위대가」 · 참조 줄 비고 = 층 낱말 · 참조 창 제목 · 이미 담음 알림에서 키 뺌 - 내 자료집 저장 알림 · 같은 이름 물음에서 키 뺌 - 시험: test_704_3 격자 · test_701_1 비고 고침 Co-Authored-By: Claude Sonnet 5.5 --- B09_Estimation/B09_Estimation_Bill_Items.py | 18 ++++++++++++++++- B09_Estimation/B09_Estimation_Labor_Source.py | 4 +++- .../B09_Estimation_UI_Tabs_Bill_Fold_Menu.ts | 6 +++--- .../M02_MasterTemplete_Labor_Detail.ts | 6 +++--- .../M02_MasterTemplete_Labor_Grid.ts | 13 ++++++++++-- .../M02_MasterTemplete_Labor_RefEdit.ts | 2 +- .../M02_MasterTemplete_Labor_Text.ts | 2 ++ .../tester/test_701_1_b08_wall_summary.py | 2 +- resources/tester/test_704_3_hopyo_grid.py | 20 +++++++++++++++++-- 9 files changed, 59 insertions(+), 14 deletions(-) diff --git a/B09_Estimation/B09_Estimation_Bill_Items.py b/B09_Estimation/B09_Estimation_Bill_Items.py index f9b4fa725..3bf6feba6 100644 --- a/B09_Estimation/B09_Estimation_Bill_Items.py +++ b/B09_Estimation/B09_Estimation_Bill_Items.py @@ -16,6 +16,7 @@ from typing import Any from B08_Quantity.B08_Quantity_Haul import bill_key, haul_groups from B08_Quantity.B08_Quantity_Material_Calc import _structure_row +from M02_MasterTemplete.M02_Item_Source import item_name WHY_EARTH = "조합 밖 토공 산근 — 연결 없음" WHY_READY = "준비공 대가 — 연결 없음" @@ -47,6 +48,20 @@ EARTH: list[tuple[str, str, str, tuple[str, str, str | None] | None]] = [ #: 구조물 집계표 열 id 머리 → 공종(그 밖 = 2.2 배수공) · 커플링 = 자재라 뺌 STRUCTURE_SECTION = {"pipe:revet|": "2.1", "pipe:chute|": "2.3", "pipe:wall|": "2.3"} SKIP_COLUMNS = ("pipe:cp|",) +#: 층 이름 — 화면 낱말(번호 · 영문 층 이름은 화면에 안 냄) +LAYER_WORD = {"system": "마스터", "company": "회사", "personal": "내 자료집", "project": "이 공사"} + + +def combo_text(combo: str, names: dict[str, str]) -> str: + """조합 id 「{층}|{주인}|{키}」 → 「내 자료집 · 조합 이름」(이름을 못 읽으면 층 낱말만).""" + layer, _, rest = combo.partition("|") + owner, _, key = rest.partition("|") + if combo not in names: + try: + names[combo] = item_name({"layer": layer, "owner": owner or None, "key": key}) + except (LookupError, KeyError, ValueError, OSError): + names[combo] = "" + return " · ".join(t for t in (LAYER_WORD.get(layer, ""), names[combo]) if t) def _same(row: dict[str, Any], want: tuple[str, str, str | None]) -> bool: @@ -80,13 +95,14 @@ def structure_items(summary: dict[str, Any] | None) -> list[dict[str, Any]]: columns = (summary or {}).get("columns") or [] total = (summary or {}).get("total") or {} out = [] + names: dict[str, str] = {} for column in columns: cid = str(column.get("id") or "") if cid.startswith(SKIP_COLUMNS): continue section = next((s for head, s in STRUCTURE_SECTION.items() if cid.startswith(head)), "2.2") name, spec, unit = _structure_row(column) - combos = [c for c in column.get("combos") or [] if c] + combos = [combo_text(c, names) for c in column.get("combos") or [] if c] out.append( { "key": f"st:{cid}", diff --git a/B09_Estimation/B09_Estimation_Labor_Source.py b/B09_Estimation/B09_Estimation_Labor_Source.py index ba3d269db..7520f426d 100644 --- a/B09_Estimation/B09_Estimation_Labor_Source.py +++ b/B09_Estimation/B09_Estimation_Labor_Source.py @@ -33,6 +33,7 @@ from typing import Any from B09_Estimation import B09_Estimation_Bill_Library as library from B09_Estimation import B09_Estimation_Sangeun as sg from B09_Estimation import B09_Estimation_Sangeun_Text as st +from B09_Estimation.B09_Estimation_Bill_Items import LAYER_WORD from M02_MasterTemplete import M02_Item_Prices as item_prices from M02_MasterTemplete import M02_Labor_Doc as labor_doc from M02_MasterTemplete import M02_Labor_Fixed as fixed @@ -266,7 +267,8 @@ class Builder: { "kind": "사전조합", "name": doc.get("이름") or ref.get("키"), - "spec": str(ref.get("키") or ""), + "spec": LAYER_WORD.get(str(ref.get("층")), ""), # 번호(키)는 풍선 글로만 + "spec_raw": str(ref.get("키") or ""), "unit": doc.get("단위") or "", "cite": "", "origin": {"참조": refs.ref_id(ref)}, diff --git a/B09_Estimation/B09_Estimation_UI_Tabs_Bill_Fold_Menu.ts b/B09_Estimation/B09_Estimation_UI_Tabs_Bill_Fold_Menu.ts index 95b7fef78..c633edc0c 100644 --- a/B09_Estimation/B09_Estimation_UI_Tabs_Bill_Fold_Menu.ts +++ b/B09_Estimation/B09_Estimation_UI_Tabs_Bill_Fold_Menu.ts @@ -38,7 +38,7 @@ async function copyBlock(ctx: TabContext, raw: DataRow[], row: Row, block: Row): note: `「${block.name}」 을 이 공사 안 새 호표로 — 고친 칸까지 복사`, fields: [ { key: "name", label: "이름", value: `${block.name} 복사`, required: true }, - { key: "spec", label: "규격", value: block.spec || "" }, + { key: "spec", label: "규격", value: block.kind === "사전조합" ? "" : block.spec || "" }, ], okLabel: "만들기", closeLabel: "닫기", @@ -106,7 +106,7 @@ async function savePersonal(ctx: TabContext, block: Row): Promise { const tail = left ? ` · 안 담은 고친 칸 ${left}(품 ${skip.품} · 단가 ${skip.단가}) — 품은 고정 줄(PLAN 705) 뒤 담김` : ""; - showToast(`내 자료집에 저장 「${got.이름}」(${got.키})${tail}`, "success", 6000); + showToast(`내 자료집에 저장 「${got.이름}」${tail}`, "success", 6000); return; } if (status !== 409 || !same) { @@ -116,7 +116,7 @@ async function savePersonal(ctx: TabContext, block: Row): Promise { } const over = await askConfirm({ title: "같은 이름", - message: `내 자료집에 「${name.trim()}」(${same}) 이 있음 — 덮어쓰기 · 새 이름`, + message: `내 자료집에 「${name.trim()}」 이 있음 — 덮어쓰기 · 새 이름`, okLabel: "덮어쓰기", cancelLabel: "새 이름", }); diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts index 280c3b2d6..c7286f964 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_Detail.ts @@ -455,7 +455,7 @@ export function mountLaborDetail( if (!ref) return; void openRefEdit({ ref, - title: `${ref.키} ${lineOf(lineId({ 참조: ref }) ?? "")?.이름 ?? ""}`.trim(), + title: lineOf(lineId({ 참조: ref }) ?? "")?.이름 ?? "", readOnly: !edit || ref.층 !== opt.layer, closed: () => alive && recalc(), }); @@ -466,7 +466,7 @@ export function mountLaborDetail( const row = cur.담은로직[i]; const line = lineOf(lineId(row) ?? ""); openQtyFor( - `${row.참조?.키 ?? row.로직} ${(row.참조 ? line?.이름 : logics.get(row.로직)?.이름) ?? ""}`.trim(), + `${row.참조 ? "" : row.로직} ${(row.참조 ? line?.이름 : logics.get(row.로직)?.이름) ?? ""}`.trim(), row, line, line?.분모 ?? "", @@ -491,7 +491,7 @@ export function mountLaborDetail( pick: (ref: LaborRef, name: string, 별칭: string | null = null) => { const id = lineId({ 참조: ref }); if (cur.담은로직.some((r) => lineId(r) === id)) - return showToast(tc("Labor_AlreadyRef").replace("{value}", `${ref.키} ${name}`), "error"); + return showToast(tc("Labor_AlreadyRef").replace("{value}", name), "error"); const 수량 = lib ? { 손: 1 } : null; // 사전조합 줄 기본 1(51-6) cur.담은로직.push({ 로직: "", 참조: ref, 별칭, 메모: null, 입력값: {}, 수량 }); changed(); diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_Grid.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_Grid.ts index b4f6bd348..8c05e65c5 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_Grid.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_Grid.ts @@ -34,6 +34,13 @@ const money = (m: LaborCost | null | undefined, times = 1): HopyoMoney => } : {}; +/** 층 낱말 — 번호(키)는 화면 글에 안 냄 */ +const layerWord = (layer: string): string => + layer === "personal" ? tc("Grid_Mine") : tc(layer === "company" ? "Lib_Company" : "Lib_System"); + +/** 일위대가번 — 번호꼴(SU-000011 · SU01010101)은 사람 글이 아니라 뺌 */ +const humanNo = (no?: string): string => (no && !/^S[A-Z]\d?-?\d{6,8}$/.test(no) ? no : ""); + /** 문서 + calc 답 → 호표 하나 — calc 없으면(계산 중) 금액 빈 칸 · 빈 줄 0 */ export function laborBlock( doc: LaborDoc, @@ -68,7 +75,9 @@ export function laborBlock( unit: line?.분모 ?? (row.참조 ? "" : (logic?.결과단위 ?? "")), prices: money(line?.기준, line?.배 || 1), amounts: money(line?.반영), - note: line?.까닭 ?? (calc && qty === null ? tc("Grid_NoQty") : (row.참조?.키 ?? row.로직)), + note: + line?.까닭 ?? + (calc && qty === null ? tc("Grid_NoQty") : row.참조 ? layerWord(row.참조.층) : row.로직), tag: HOPYO_TAG.pum, }; }); @@ -96,7 +105,7 @@ export function laborBlock( const lines = [...logicLines, ...matLines]; const block: HopyoBlock = { key: doc.열, - name: doc.일위대가번 || doc.열, + name: humanNo(doc.일위대가번) || (doc as { 이름?: string }).이름 || tc("Grid_Head"), unit: doc.단위 ?? "", total: money(calc?.반영합), lines, diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_RefEdit.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_RefEdit.ts index bc08770ce..75916b241 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_RefEdit.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_RefEdit.ts @@ -15,7 +15,7 @@ import { tc } from "./M02_MasterTemplete_Labor_Text"; export interface RefEditOptions { ref: LaborRef; - /** 「SC-000001 돌붙임」 */ + /** 「돌붙임」 */ title: string; readOnly: boolean; closed: () => void; diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_Text.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_Text.ts index 0d8541919..d64dbdfc9 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_Text.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_Text.ts @@ -179,6 +179,8 @@ const TEXT = { Grid_Apart: ["별도계상 · 합계 밖", "Separate · not in total"], Grid_Linked: ["이 공사 단가에 이음", "Linked to project price"], Grid_Hand: ["손 단가(붙인 값)", "Hand price (pasted)"], + Grid_Head: ["일위대가", "Unit price"], + Grid_Mine: ["내 자료집", "My library"], } as const satisfies Record; export type ComboTextKey = keyof typeof TEXT; diff --git a/resources/tester/test_701_1_b08_wall_summary.py b/resources/tester/test_701_1_b08_wall_summary.py index c8723be4b..f651ca569 100644 --- a/resources/tester/test_701_1_b08_wall_summary.py +++ b/resources/tester/test_701_1_b08_wall_summary.py @@ -125,4 +125,4 @@ def test_B09_금액_길이_곱하기_대가(out): inputs = {"bill": {"items": structure_items(out)}} sheets = compute_estimation(inputs, {}, {"s-wall": price}.get) row = next(r for r in sheets["bill"]["rows"] if r["key"].endswith("#blk:w")) - assert (row["qty"], row["sum"], row["note"]) == (12.0, 1495128, "대가1 · system||SS000201") + assert (row["qty"], row["sum"], row["note"]) == (12.0, 1495128, "대가1 · 마스터") diff --git a/resources/tester/test_704_3_hopyo_grid.py b/resources/tester/test_704_3_hopyo_grid.py index 935a63bcd..96bc0d304 100644 --- a/resources/tester/test_704_3_hopyo_grid.py +++ b/resources/tester/test_704_3_hopyo_grid.py @@ -58,7 +58,7 @@ def test_머리_구성_합계_줄_꼬리표_빈_줄(): STUBS = r""" -const tc = (k) => ({ Grid_NoQty: "수량 없음", Grid_Apart: "별도계상 · 합계 밖", Labor_Layer: "켜쌓기 {n}%" })[k] ?? k; +const tc = (k) => ({ Grid_NoQty: "수량 없음", Grid_Apart: "별도계상 · 합계 밖", Labor_Layer: "켜쌓기 {n}%", Grid_Head: "일위대가", Grid_Mine: "내 자료집", Lib_Company: "회사", Lib_System: "마스터" })[k] ?? k; const valueLine = (inputs, values) => ({ text: Object.values(values ?? {}).join(" · "), empty: 0 }); """ @@ -108,7 +108,7 @@ def test_M02_문서_calc_를_격자_줄로(): "\nconsole.log(JSON.stringify({ b, foot: hopyoGrid([b]).rows.at(-1), wait: hopyoGrid([wait]).rows.at(-1) }));" ), STUBS) # fmt: skip b = out["b"] - assert b["name"] == "SU-000006" and b["unit"] == "m" and b["total"]["sum"] == 112559.55 + assert b["name"] == "일위대가" and b["unit"] == "m" and b["total"]["sum"] == 112559.55 one, two, mt1, mt2 = b["lines"] assert ( one["name"] == "돌쌓기 찰쌓기" @@ -141,3 +141,19 @@ def test_세_곳_같은_부품(): assert '"@ui/ui_template_hopyo_grid"' in m02 and '"@ui/ui_template_hopyo_grid"' in b09 kit = (ROOT / "ui_template/ui_template_ui_kit_parts.ts").read_text("utf-8") assert 'key: "hopyo-grid"' in kit # 부품 모음 견본 + + +def test_번호는_글에_안_냄_사람_글은_그대로(): + files = [GRID, "M02_MasterTemplete/M02_MasterTemplete_Labor_Calc.ts", + "M02_MasterTemplete/M02_MasterTemplete_Labor_Spec.ts", + "M02_MasterTemplete/M02_MasterTemplete_Labor_Grid.ts"] # fmt: skip + ref = {"로직": "", "별칭": None, "입력값": {}, "수량": {"손": 1}, + "참조": {"층": "personal", "주인": "1/3", "키": "SC-000002"}} # fmt: skip + named = {**DOC, "일위대가번": "제5호표", "담은로직": [ref]} + out = _node(files, ( + f"\nconst a = laborBlock({json.dumps(named, ensure_ascii=False)}, null, new Map());" + f"\nconst c = laborBlock({json.dumps({**named, '일위대가번': 'SP3-000015'}, ensure_ascii=False)}, null, new Map());" + "\nconsole.log(JSON.stringify({ a, c }));" + ), STUBS) # fmt: skip + assert out["a"]["name"] == "제5호표" and out["c"]["name"] == "일위대가" + assert out["a"]["lines"][0]["note"] == "내 자료집" From 5dc014e9eedd96d4204513706f3704933d8a5787 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sat, 31 Oct 2026 18:34:47 +0900 Subject: [PATCH 09/11] =?UTF-8?q?fix(M02=20=EA=B5=AC=EC=A1=B0=EB=AC=BC=20?= =?UTF-8?q?=EC=A1=B0=ED=95=A9):=20[=EA=B5=AC=EC=A1=B0=EB=AC=BC=20=EC=A1=B0?= =?UTF-8?q?=ED=95=A9=EC=9C=BC=EB=A1=9C]=20=EB=A7=8C=EB=93=A0=20=ED=95=AD?= =?UTF-8?q?=EB=AA=A9=EC=9D=B4=20=EC=97=B4=EB=A6=B0=20=ED=91=9C(=ED=96=89?= =?UTF-8?q?=EB=A0=AC)=EC=97=90=20=EB=B0=94=EB=A1=9C=20=EB=B3=B4=EC=9E=84?= =?UTF-8?q?=20=E2=80=94=20[=EC=83=88=EB=A1=9C=EA=B3=A0=EC=B9=A8]=20?= =?UTF-8?q?=EC=97=86=EC=9D=B4=20(PLAN=20704-4=20=EB=94=B8=EB=A6=BC=20?= =?UTF-8?q?=E2=91=A5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 만들기 알림이 오면 나무 · 옆 목록에 더해 구조물 조합 표가 열려 있으면 다시 읽음(저장 안 한 고침은 먼저 물음) Co-Authored-By: Claude Sonnet 5.5 --- M02_MasterTemplete/M02_MasterTemplete_UI_Page.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/M02_MasterTemplete/M02_MasterTemplete_UI_Page.ts b/M02_MasterTemplete/M02_MasterTemplete_UI_Page.ts index 69d869319..17cb6cbc6 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_UI_Page.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_UI_Page.ts @@ -157,11 +157,15 @@ export async function renderM02MasterTemplate(root: HTMLElement): Promise void go("personal", next, open), }) : panel; - // 704-4 [구조물 조합으로] 만든 항목 → 나무 · 옆 목록 다시(페이지가 내려가면 듣기 끝) + // 704-4 [구조물 조합으로] 만든 항목 → 나무 · 옆 목록 · 열린 표(행렬)를 다시(페이지가 내려가면 듣기 끝) + const afterItem = async (): Promise => { + await Promise.all([side.refresh(), tree.refresh()]); + const now = main.current(); + const shown = now?.kind === "table" && now.name === STRUCTURE_TABLE; + if (shown && (await main.settle()) !== false) await main.openTable(layer, null, true); + }; const onItem = (): void => - tree.root.isConnected - ? void Promise.all([side.refresh(), tree.refresh()]) - : document.removeEventListener(ITEM_BUILT, onItem); + tree.root.isConnected ? void afterItem() : document.removeEventListener(ITEM_BUILT, onItem); document.addEventListener(ITEM_BUILT, onItem); const main = createMain( isAdmin, From 63fd282771f7d06c4df0fa5b4f3215f03d803808 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sat, 31 Oct 2026 18:34:48 +0900 Subject: [PATCH 10/11] =?UTF-8?q?fix(M02=20=EC=98=AC=EB=A6=AC=EA=B8=B0=20?= =?UTF-8?q?=EC=9A=94=EC=B2=AD):=20=ED=9A=8C=EC=82=AC=20=EA=B4=80=EB=A6=AC?= =?UTF-8?q?=EC=9E=90=EB=8A=94=20=EC=96=B4=EB=8A=90=20=EC=B8=B5=EC=9D=84=20?= =?UTF-8?q?=EC=97=B4=EC=96=B4=EB=8F=84=20=E3=80=8C=EC=98=AC=EB=A6=AC?= =?UTF-8?q?=EA=B8=B0=20=EC=9A=94=EC=B2=AD=20n=E3=80=8D=20=EB=9D=A0?= =?UTF-8?q?=EA=B0=80=20=EB=B3=B4=EC=9E=84=20(PLAN=20706-1=20=EB=94=B8?= =?UTF-8?q?=EB=A6=BC=20=E2=91=A6)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 띠 조건에서 「회사 층일 때만」 뺌 — 회사를 고칠 수 있는 사람이면 개인 · 마스터 층에서도 보임 · 누르면 요청 목록 Co-Authored-By: Claude Sonnet 5.5 --- M02_MasterTemplete/M02_MasterTemplete_ShareRequest.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/M02_MasterTemplete/M02_MasterTemplete_ShareRequest.ts b/M02_MasterTemplete/M02_MasterTemplete_ShareRequest.ts index 086a735b3..133e4b0b7 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_ShareRequest.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_ShareRequest.ts @@ -3,7 +3,7 @@ * 회사에 올리기 요청 · 승인(PLAN 706-1) — 서버 `M02_Share_Request.py` 짝(장부 = 회사 층 파일 하나). * 일반 사용자 = 자기 개인 층 구조물 조합 · 사전조합 나무 오른쪽 클릭 「회사에 올리기 요청」 → 글(선택) · * 같은 것 요청이 남아 있으면 「요청 중」(흐림) · 결과는 다음에 M02 열 때 위 띠(닫으면 봄) - * 회사 관리자 = 회사 층 위 띠 「올리기 요청 n」 → 목록(요청자 · 항목 · 날짜 · 글) · [미리 보기] · + * 회사 관리자 = M02 위 띠 「올리기 요청 n」(어느 층이든) → 목록(요청자 · 항목 · 날짜 · 글) · [미리 보기] · * [승인](폴더 고름 = 「회사로 복사…」와 같은 길 · 원본 그대로) · [거절](사유 한 줄) * 관리자(회사 층을 고칠 수 있는 사람)는 「회사로 복사…」가 있어 요청 메뉴를 안 둠. * ========================================================================== */ @@ -50,7 +50,7 @@ export interface ShareOptions { user: SessionUser | null; /** 볼 회사(시스템 관리자는 고른 회사) */ company: string | null; - /** 지금 페이지 층 — 회사 층이면 관리자 띠 */ + /** 지금 페이지 층 — 개인 층이면 요청 메뉴 */ layer: Layer; owner: string | null; showView: (label: string, node: HTMLElement) => void; @@ -77,10 +77,7 @@ export function createShare(opt: ShareOptions): ShareHandle { // 요청할 사람 = 회사가 있고 회사 층을 못 고침(고칠 수 있으면 「회사로 복사…」) const asker = !!ownOwner("personal", opt.user) && !accessFor("company", companyOwner, opt.user).canWrite; - const boss = - opt.layer === "company" && - !!companyOwner && - accessFor("company", companyOwner, opt.user).canWrite; + const boss = !!companyOwner && accessFor("company", companyOwner, opt.user).canWrite; const q = opt.company ? `?company=${encodeURIComponent(opt.company)}` : ""; let rows: ShareRow[] = []; let listHost: HTMLElement | null = null; From 1cab899ecf1c2297177a08d92c75b284c0025614 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sat, 31 Oct 2026 19:02:20 +0900 Subject: [PATCH 11/11] =?UTF-8?q?test(=EB=B2=88=ED=98=B8=20=EB=85=B8?= =?UTF-8?q?=EC=B6=9C):=20=EC=82=AC=EC=A0=84=EC=A1=B0=ED=95=A9=20=EB=8C=80?= =?UTF-8?q?=EA=B0=80=20=EB=B8=94=EB=A1=9D=20=EA=B7=9C=EA=B2=A9=20=3D=20?= =?UTF-8?q?=EC=B8=B5=20=EB=82=B1=EB=A7=90=20=C2=B7=20=EB=B2=88=ED=98=B8?= =?UTF-8?q?=EB=8A=94=20spec=5Fraw=20=EC=8B=9C=ED=97=98=20(PLAN=20704=20?= =?UTF-8?q?=EB=94=B8=EB=A6=BC=20=E2=91=A4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 --- resources/tester/test_145_2_b09_labor_calc.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/resources/tester/test_145_2_b09_labor_calc.py b/resources/tester/test_145_2_b09_labor_calc.py index 883a453c8..20e07bddc 100644 --- a/resources/tester/test_145_2_b09_labor_calc.py +++ b/resources/tester/test_145_2_b09_labor_calc.py @@ -311,6 +311,10 @@ def test_견본_사본_대가_블록(project): assert pct["kind"] == "pct" and pct["base"] == "part:material" and pct["qty"] == 20 sc = blocks[b["lines"][3]["key"]] assert sc["kind"] == "사전조합" and b["lines"][3]["qty"] == 4 + assert (sc["spec"], sc["spec_raw"]) == ( + "마스터", + "SC-T", + ) # 규격 = 층 낱말 · 번호는 풍선(spec_raw) assert ( sc["lines"][0]["kind"] == "sangeun" and sc["lines"][0]["key"] == a["lines"][0]["key"]