From a4f5997c9127ace205bcfd191e03524f588570d3 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sat, 10 Oct 2026 14:12:03 +0900 Subject: [PATCH 01/81] =?UTF-8?q?sync:=20=EB=A1=9C=EC=BB=AC=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B8=20=ED=8C=8C=EC=9D=BC=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/raw/요구_1010_2/사용자요구_1010_2.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/raw/요구_1010_2/사용자요구_1010_2.md b/docs/raw/요구_1010_2/사용자요구_1010_2.md index 18c1b1be2..9cd35f849 100644 --- a/docs/raw/요구_1010_2/사용자요구_1010_2.md +++ b/docs/raw/요구_1010_2/사용자요구_1010_2.md @@ -66,3 +66,10 @@ 43. B06 횡단도 좌측 패널에서 세부 컨테이너의 정보를 변경하면 해당 구조물 형상을 잘 선택되고 있는데 해당 패널의 컨테이너 영역을 선택해도 변경되게 해줘. 44. 랩탑 메인 프로젝트에서 38+9 측점 확인 종단의 선을 봤을때 중간에 안보이는 가상의 측점이 존재하는것처럼 보임. 데이터로 확인. (사진: 종단 계획선(보라 점선)이 고른 측점 세로선 근처에서 꺾임) 45. 지금 횡단도의 보라색선인 계획선과 관련인데 여기서 선을 찍으면 직선선 성분을 기준으로 이동을 할수 있게 만들어져 있임. 여기서 이동의 제약이 있어야함. 마음대로 변경되고 있음. 보라색 선은 선택은 크게는 성토측, 절토측, 도로측이 있음. 성토측 보라색 선을 선택할경우에는 도로 중심을 기준으로 성토방향으로 만 동작해야함. 만약 배관의 길이 영향으로 갈수 있는 방향은 절토측은 고정이 되고 성토측 방향의 구조물+성토선 전체가 이동이 되어야함. 반대로도 동일함. 이때 노면, 노견의 선을 선택한다면 Y방향으로 이동이되고 이때 종단도도 같이 동작해야함. 지금도 그렇게 구성되어 있음. 전반적으로 잘 구현되어 있으나 다듬을 필요가 있음. +46. 소단에서 소단위 위치값은 필요없음. 이동은 어차피 아래 위밖에 안되는 상태일테고 추가 기본지점을 도로하단 1m 지점에 추가해주면 사용자가 위치를 결정하게 해줘. 이때 성토사면 그룹의 이동과 충돌나는걸 피하기위해 좌우 이동이 없는거야. 결과적으로는 사용자가 지정한 요소들의 위치가 중요한거지. +47. 추가로 보라색 선 성분을 선택할때 나오는 텍스트는 사면 길이 > 길이로 변경 다음에 경사비율, 수평, 높이로 표현해줘. +48. (46번 보충) 소단을 선택됨을 활성화하는 영역은 횡단도의 소단 수평선 성분을 하이라이트 하면되고 패널과 컨테이너를 활성화하면 됨. 성절토의 X 이동과 와 도로 Y 이동, 소단의 높이 위치 이동을 명확학헤 분리해서 제어되어야하며 상하좌우 이동시 다른 선 성분으로 이동되는 경우가 있으니 조심 +49. B06 횡단도 제일산단의 쓸대없는 정보는 지우고 나오는 항목 이름을 줄여서 표현하고 전부 열림을 하면 공간을 많이 차지하니 내용을 보여주는 영역을 공유하는게 어떤지 +50. 대시보드의 구조물옵션 정의가 제대로 연결안됨. +51. B08의 구조물 집계표에서 미연결 상태표시는 행의 좌측에 하나의 램프로 표기, 미지정 열을 필요없어 보임. +52. B08의 페이지들을 보면 합계가 나오는 행들을 보면 초기에는 행들이 추가되다가 특정 컨테이너 높이가 되면 합계 행을 고정하고 내부를 스크롤 방식으로 변경하고 있음. 이방법 자체는 좋은데 그렇다면 컨테이너의 높이제약을 현재 브라우져가 스크롤이 안되는 지점을 기준으로 적용하는게 조작 편의성이 좋아보임. 그럼 행들이 추가됨에 따라 밀리더라도 스크롤은 페이지스크롤은 없을테니 From 1cbeea6bfd3739fd499a82901178f25f90c88f52 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sat, 10 Oct 2026 16:11:53 +0900 Subject: [PATCH 02/81] =?UTF-8?q?chore(sync):=20=EB=A1=9C=EC=BB=AC=20?= =?UTF-8?q?=EB=AF=B8=EB=B0=98=EC=98=81=20=EC=88=98=EC=A0=95=EB=B6=84=20?= =?UTF-8?q?=EC=BB=A4=EB=B0=8B=20(=EC=9B=90=EA=B2=A9=EA=B3=BC=20=EB=8F=99?= =?UTF-8?q?=EC=9D=BC=20=EB=82=B4=EC=9A=A9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/raw/요구_1010_2/사용자요구_1010_2.md | 31 +++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/docs/raw/요구_1010_2/사용자요구_1010_2.md b/docs/raw/요구_1010_2/사용자요구_1010_2.md index 9cd35f849..c6bdce7b4 100644 --- a/docs/raw/요구_1010_2/사용자요구_1010_2.md +++ b/docs/raw/요구_1010_2/사용자요구_1010_2.md @@ -73,3 +73,34 @@ 50. 대시보드의 구조물옵션 정의가 제대로 연결안됨. 51. B08의 구조물 집계표에서 미연결 상태표시는 행의 좌측에 하나의 램프로 표기, 미지정 열을 필요없어 보임. 52. B08의 페이지들을 보면 합계가 나오는 행들을 보면 초기에는 행들이 추가되다가 특정 컨테이너 높이가 되면 합계 행을 고정하고 내부를 스크롤 방식으로 변경하고 있음. 이방법 자체는 좋은데 그렇다면 컨테이너의 높이제약을 현재 브라우져가 스크롤이 안되는 지점을 기준으로 적용하는게 조작 편의성이 좋아보임. 그럼 행들이 추가됨에 따라 밀리더라도 스크롤은 페이지스크롤은 없을테니 +53. B09의 박스보기 버튼과 가로 숫자 버튼, 그리고 그위의 계산들에 대한 안내는 같은행으로 변경 +54. 박스보기의 경우 지정된 카드 행들은 높이를 같게 변경 +55. 설계내역서의 공종 및 명칭의 글자들은 좌측 맞춤이고 좌측에 들여쓰기로 레벨이 표현되는게 맞아보임. 그리고 레벨 제어를 어떻게 하는지 모르겠음. +56. 박스보기에서 제목과 합계 정보열의 색은 다른색으로 지정하는게 테마고려하여 선택 +57. B08/B09의 테이블 양식이 에서 레벨이 필요한 정보들은 설계내역서의 레벨지정 양식을 따라가되 제어가 안되게 반영하면 일원화되지 않을까? 그리고 셀에 같은 값으로 표현되는 행들은 결국 병합인것같은데 맞지 않아? +58. 제비율표는 일력하는 공사원가계산서와 총괄설계내역서의 내용에서 요율을 원문 제비율표를 보고 누락이 된게 없는지 봐줘. (조사만) +59. 현재 B08/B09의 테이블 컨테이너의 양식을 보면 제일상단에 경고줄이 있어 이걸제일하단으로 옮겨줘. 이때 박스보기와 가로 카드수 정의는 보조탭 행에 우측맞춤으로 이동 +60. 다음으로 B09의 좌측패널의 원가 단가 컨테이너의 이름을 데이터 점검으로 이름을 변경하고 내부의 쓸대없는 설명은 삭제, 단가 갱신버튼의 제목을 업데이트, 업데이트가 필요한경우 버튼에 오버레이 마크 추가, 상세 페이지의 연결점검 탭은 데이터 점검 컨테이너 내부로 이동 +61. B09의 설계내역서에서 행들을 레벨이나 이동을 드래그나 키보드의 방향키로 제어가 되어야함. 구현하고 결국 내어쓰기와 들여쓰기는 결국 레벨이 되어야함. 상위 레벨로 이동이 되면 자연스럽게 하위항목의 합계가 세부항목이 되면 상위로 데이터를 전달하는 역할을 그리고 만약 기존 상위에 있던것들이 하위로 이동할경우에는 같이 이동되는 기능도 있어야함. 다음으로 일위대가와 단가산출의 정보를 연결할때 중요한건 마스터/회사/개인의 정보중 구조물조합, 일위대가사전조합, 현재처럼 벌써 들어와 있는 일위대가와 호표를 지정할 수도 있어야함. +62. B09의 좌측 패널의 컨테이너를 공용양식을 사용한건지 확인 (확인만) +63. B05/B06의 구조물배치 컨테이너에서 삭제버튼과 리셋버튼이 높이사이즈를 하위 컨테이너의 버튼 높이와 동일하게 구현하고 내부 컨테이너에서 삭제와 리셋버튼이 항상보이게 구현 +64. [사진] 현재 검증 서브1 프로젝트의 1+0 측점의 내용임. 이때 원래는 측구가 없어야하는 성토사면에 존재함. 이때 측구버튼과 일반L형 버튼이 없음. 숨기기 결과로 보임. 이런 현상의 원인이 궁금함. 한번 좌우를 바꾸면 재현안됨. 확실히 조사하여 반영할것. (사진: 1+0 · 양측 성토 · 성토사면 좌 18.61m · 우 0.26m · 좌|우 단추 중 「우」 켜짐 · 오른쪽 노견 바깥에 작은 측구 모양 · 측구 · 일반/L형 단추 안 보임) +65. B06의 상단의 성토 5m 이상, 포장 권장, 소단 검토에서 버튼의 형상을 사각 라운드로 변경하고 누를시 나오는 버튼들은 현재를 유지, 그리고 메인 페이지의 스크롤이 제일 상단에 있을경우 상단 버튼으로 내역을 열면 페이지가 아래로 밀림. 안밀리게 변경, 그리고 항목을 선택하면 이동하면서 해당 버튼셋은 접힘으로 그리고 만약 선택된 상태에서 다시 열면 하이라이트 기능, 다시 말해 사용자가 측점 횡단도를 누른상태에서 상단 3버튼을 누르면 하이라이트 되게 변경 +66. M01의 단가산출로직들의 항목에 신규열인 별칭 열이 필요함 설계자들이 찾을때 사용하는 명칭과 다른 경우가 있음. 이를 보완하기 위해 별칭열을 추가해줘. 내가 별칭을 있을수도 있고 없을 수도 있지만 값은 내가 넣을거야. 그리고 이때 프로그램 전역에서 단가산출 관련 검색은 별칭도 같이 검색이 되게 해야함. + +## 셋째 붙임 (10-10 오후) +67. B06 횡단도 상세 모달 — 셀에 지정된 면적과 길이값을 선택하게 하였는데 안잡히는 경우가 있어 개선이 필요함. 상세 모달창에서 선들이 교차되는 지점들을 기준으로 선들이 분할되어야함. 그리고 선택할때는 다중 선택을 하면 원지반도 가능해야함. 그럼 문제 없어보임. 그럼 면적에서도 폐쇄 영역을 지정하기 좋아보임. 추가로 선택영 지점의 면적과 선들의 길이의 값을 보는 기능이 있었으면 함. 이는 메인페이지의 기능이 있으니 확인, 잘 반영되었다면 메인페이지에도 구현한 생각임. +68. 간혹가다 B06의 상단에 누락된 구조물 측점이라고 측점추가 버튼이 나오는데 이유는? (조사) +69. 관보호공 유입 측 흙막이의 경우에 기본 높이를 배관 + 0.5m로 지정되어 있을것임. 이걸 0.7m로 변경 +70. 도수로/돌붙입의 구조물 형상을 잡고 이동하는 경우 토스트 메세지로 '기준측점을 옮겼습니다 ....' 나오는데 나는 도수로와 돌붙임의 이동에 대해 요청한건 없음. 왜그런지 분석하고 알려줘. (조사) +71. B07 (기반영요청사항) 횡단도의 격자에 배치를 옮기기 위해 선을 선택할때 원지반선이 안잡힘 +72. B07 (기반영요청사항) 추가로 레이어별로 측점의 원지반선과 설계선을 잡을 수 있는 레이어 자동 생성 +73. B07 횡단도의 붉은색 격사선을 하이드쇼할수 있는 버튼 기능은 좋은데 뒤에 검은 격자선이 하나더 있음. 확인 후 삭제 +74. B07 횡단도 표준 옵션의 버튼들이 양식화와 일원화 안되어 있음. 확인후 정리할 수 있는 양식으로 적용 +76. 다음으로 중요한 문제인데 특정 측점에 2개의 측점이 숨어 있는 경우가 있음. 엄상준_검증 프로젝트에서 13+3 지점에 대해 이유를 확인해줘. 원인파악을 제대로 할것 계속 반복적으로 동일한 문제가 발생됨 +77. (제안 · 의견 물음) 버튼으로 조작하여 종단을 만드는 기능은 사용자들이 어려워 하는것같아. 직선화버튼 우측에 자유지정 버튼을 선택하면 먼저 범위를 지정(2개의 세로선 · 영역 하이라이트) · 시작이나 종료 세로선 근처 클릭 = 연결된 상태로 시작 · 직선 > R > 직선 > R 형태로 클릭할때마다 연결. 어떻게 생각함? +78. 캐드 프로그램에서 되돌리기와 다시하기 기능이 없는거야? 아니면 구조물조합의 캐드의 내용에 국한된 문제인거야? +79. (77번 보충) 1-1 아마 사용자가 선택하는건 자유이지만 아마 세로측점선을 기준으로 하되 높이값을 받아야 되고 때 확정은 아니니 실시간으로 점선으로 보여주면 좋을것같아. +80. 횡단페이지에는 횡단배수와 일반측점의 종단 세로선택에 따라 좌측 구조물 배치 패널이 잘 작동하는데 B05에서는 잘안되는데 이유를 확인해봐. +81. B06 페이지의 제일 상단 경고 버튼들과 횡단도 컨테이너 공간이 여유가 조금 많은것같아. 줄여줘. +82. 신규기능으로 B05 ~B09 페이지에 마우스 우측 클릭 오래누르기와 z키 누르기로 마우스 커서를 따라가는 돋보기 기능이 있으면 해. 범위와 배율은 넣거 일단 지정해줘. From aa8f846446f6d3fdba28fd35437322081be09219 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sat, 10 Oct 2026 16:53:40 +0900 Subject: [PATCH 03/81] =?UTF-8?q?sync:=20=EC=9E=84=EC=8B=9C=20=EB=B0=98?= =?UTF-8?q?=EC=98=81(=EB=AA=A9=ED=91=9C=EC=99=80=20=EB=8F=99=EC=9D=BC=20?= =?UTF-8?q?=EB=82=B4=EC=9A=A9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/raw/요구_1010_2/사용자요구_1010_2.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/docs/raw/요구_1010_2/사용자요구_1010_2.md b/docs/raw/요구_1010_2/사용자요구_1010_2.md index cd0af97fd..2a930d328 100644 --- a/docs/raw/요구_1010_2/사용자요구_1010_2.md +++ b/docs/raw/요구_1010_2/사용자요구_1010_2.md @@ -106,3 +106,18 @@ 82. 신규기능으로 B05 ~B09 페이지에 마우스 우측 클릭 오래누르기와 z키 누르기로 마우스 커서를 따라가는 돋보기 기능이 있으면 해. 범위와 배율은 넣거 일단 지정해줘. 83. 돋보기 구현 완료, 돋보기가 활성화된 상태에서는 스크롤로 배율을 조절하게 단 화면 스크롤은 안되게 막기, 돋보기의 크기는 지금크기의 50% 증가 84. 돋보기의 기능은 M01, M02, B01 ~ B09까지로 적용해줘. +85. 오토캐드 영역은 돋보기 필요없음 + +## 넷째 붙임 (10-10 저녁) +86. M01 GF000156 의 모래단위중량, 자갈단위중량은 밀도값으로 보이는데 기본값이 있을 것으로 판단됨(or 현재 12-1-2의 원문을 조사하면 역산하여 밀도를 뽑을 수 있지 않음?). 원문 조사 +87. 엄상준의 SU-000007의 구조물 조합의 일위대가 중 기계비빔타설을 보면 시멘트, 모래, 자갈에 대한 원가들이 잡혀있으며 이때 단위가 잘못된걸로 판단됨. 확인, 추가로 하단의 자재 목록을 보면 별도계상이 나오게 되고 이때는 상단의 일위대가에 단가가 빠져야되는 상황으로 보임. 문제는 2가지임. 분리해서 검토. 일위대가의 단가산출 항목 중 상세내용의 내역을 자세히 검토하기 위해 자재목록으로 자동연결한 것 뿐이니 별도로 보면 안됨. +88. 기계비빔타설의 경우 원문참고가 12-1-2와 12-1-3 같이 되어야하는걸로 보이는데 하나만 보임. +89. (질문) 시멘트는 어떻게 연결되어 나온 정보임? +90. 구조물 조합의 자재에서 별도계상을 선택하면 선택된 자재품목을 사라져야함. 이건 나중에 본프로그램에서 별도로 계상할 생각임 +91. B05/B06 좌측 패널 — 구조물배치의 구조물 조합연결 모달창에서 M02의 구조물조합 페이지의 상단 테이블도 표현 +92. B06 횡단도 — 세월교와 배관 자리의 경우 배관정보를 텍스트로 표현해야되는데 이때 노폭과 노견의 길이정보를 보여주는 라벨 위로 이동해줘. +93. B06 — 관보호공의 위치제어의 경우 관 길이에 맞춰 이동하다보니 자유도가 떨어짐. 관 길이를 무시하고 0.1m 단위로 이동이 되게 반영, 단, 관길이 정보를 나타내는 라벨과 좌측 패널에서는 정수값으로 사사오입하여 작성하고 추가로 괄호를 추가하여 소수점 1째자리까지 표현, 이때 구조물 집계표로 정보를 전달할때는 사사오입한 정보를 전달 +94. 캐드 프로그램에서 스케일 기능은 잘되지만 추가로 오토캐드처럼 오리진을 지정하고 숫자로 스케일 값을 입력을 받게 추가 +95. 캐드 프로그램에서 줌아웃을 많이 하면 브라우져의 속도가 많이 떨어지던데 개선방향은? (질문) +96. 특히 M01 GF000156과 같은 단가산출이 있는지 조사하고 개선 +97. 돋보기 기능이 페이지의 스크롤에 상황에 따라 화면이 안바뀜 개선 From a870e9a3a46806a4cd074ce9bb1c88c6c4280f80 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sat, 10 Oct 2026 23:57:15 +0900 Subject: [PATCH 04/81] =?UTF-8?q?sync:=20=EC=9E=84=EC=8B=9C=20=EB=B0=98?= =?UTF-8?q?=EC=98=81(=EB=AA=A9=ED=91=9C=EC=99=80=20=EB=8F=99=EC=9D=BC=20?= =?UTF-8?q?=EB=82=B4=EC=9A=A9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/raw/plans/2026-10-10_plan_221-227.md | 5 ++ docs/raw/요구_1010_2/사용자요구_1010_2.md | 57 +++++++++++++++++++++++ 2 files changed, 62 insertions(+) diff --git a/docs/raw/plans/2026-10-10_plan_221-227.md b/docs/raw/plans/2026-10-10_plan_221-227.md index 0dc8f340a..27829b4a7 100644 --- a/docs/raw/plans/2026-10-10_plan_221-227.md +++ b/docs/raw/plans/2026-10-10_plan_221-227.md @@ -42,3 +42,8 @@ - 검증완료 — test_m02_card_shell · test_modal_top_and_add_word 14 passed · test_200_2 4 passed · typecheck 통과 · 코드 글만 고침(Bill_Level 안내 「더하기」→「추가」) - [x] 225-2 (sub1) test_79 관 보호공 기본 자리 d(유입) 0.6 → 0.4 로 바뀜 — 211(유입 흙막이 관 + 0.7)의 당연한 결과인지 · 흠인지 판정 · 결과면 기대값 · 흠이면 고침 — 시험 통과 - 검증완료 — 판정 = 당연한 결과(흠 아님): 벽 바닥은 관 invert 상한(노견 − 관경 − 토피 0.5)에 고정 · 벽이 0.2 높아져 윗면 노견 − 0.5 → − 0.3 · d = 올림(1.2 × (1.5 − 1.2) · 0.1) = 0.4 · 225-1 관 길이와 무관 · 기대값 d 0.4 · 윗면 노견 − 0.3 으로 고침(시험 글에 까닭) · test_79 · 211_1 · 225_1 27 passed · typecheck 0 + +## 228. 표 레벨 = 들여쓰기에 따라 레벨 · 글꼴(크기 · 굵기) 자동 (sub3) +정함(브레인) = 사용자 99 · 100번 · 200-2 에서 글자 크기를 한 벌로 맞춘 것은 브레인 판정 잘못(23번 원문 「들여쓰기와 폰트 · 사이즈로 구분」) → 되살림 · 공용 표 level 옵션(201)에서 레벨마다 글꼴 자동: Lv1 16px 굵게 · Lv2 15px 굵게 · Lv3 14px 보통 · Lv4 13px 보통 · Lv5 12px 보통 + 들여쓰기 1em/레벨(줄 높이 28px 안) · 설계내역서는 들여쓰기 · 내어쓰기 · 끌기 · 방향키로 부모가 바뀌면 그 자리에서 레벨 · 글꼴이 바로 바뀜(다시 그림 확인) · 보기 전용 표(총괄 · 원가계산서 · 일위대가표 등)도 같은 글꼴 규칙 · 조사 AE(외부 사례) 결과로 덧붙일 것(접기/펴기 등)은 따로 +- [x] 228-1 레벨 글꼴 자동 · 들여쓰기 = 레벨 바로 반영 — test_200_2 · 201_1 · 204_1 · 182 고침 · 새 시험 + - 검증완료 — test_228_1 새 6 + 200_2 · 201_1 · 204_1 · 182 · 184_3 · 185_1 = 37 통과 · typecheck 0 · 굵게 = 묶음(브레인 답 A) · 내어쓰기 g2 Lv4→3 · 자손 Lv5→4 · 들여쓰기로 자식 생긴 항목 = 굵게 · 커밋 833e4146a diff --git a/docs/raw/요구_1010_2/사용자요구_1010_2.md b/docs/raw/요구_1010_2/사용자요구_1010_2.md index 2a930d328..a2ae14ba1 100644 --- a/docs/raw/요구_1010_2/사용자요구_1010_2.md +++ b/docs/raw/요구_1010_2/사용자요구_1010_2.md @@ -121,3 +121,60 @@ 95. 캐드 프로그램에서 줌아웃을 많이 하면 브라우져의 속도가 많이 떨어지던데 개선방향은? (질문) 96. 특히 M01 GF000156과 같은 단가산출이 있는지 조사하고 개선 97. 돋보기 기능이 페이지의 스크롤에 상황에 따라 화면이 안바뀜 개선 +98. 왜 프로젝트의 B06의 좌측패널의 구조물 리스트에서 노면공의 구조물 범위에 들어온 측점들중 다른 구조물의 리스트를 선택했음에도 노면공의 횡단면도 전체가 선택되는거야. 일단 조사만 진행해줘. 코드 구현은 나중에 할래 +99. 테이블 양식에서 설계내역서의 레벨 기능이 제대로 구현안되어 있음. 외부에서 어떻게 해야하는지 확인해봐. (조사) +100. (99번 보충) 테이블에서 제대로 안된다는 건 결국 들여쓰기에따라 레벨이 지정이 되고 폰트들도 자동으로 변경이 되어야하는데 안되고 있잖아. 그냥 들여쓰기만 하는거 아니야? +101. B09의 설계내역서 탭은 지금 하위탭이 없음. 여기에 보조탭 공간을 활용하여 행의 들여쓰기, 내어쓰기, 위, 아래 버튼들을 넣어줘. +102. B05에서 종단 새창으로 띄우고 브라우져 최대화한경우 마우스 스크롤로 종단을 움직이는 기능이 작동이 안됨. 최대화를 안하면 작동됨. +103. 노면공 범위 관련은 1번 방향이 더 좋을것같아. 대신 하이라이트는 여러군데 연동되어 있어 좀더 폭넓게 조사해보고 작동해줘. +104. 3D의 측점의 노선을 선택할때 하이라이트되는데 이게 바로 꺼지는 상황이야. 일단 하이라이트가 계속 문제되는데 전체적인 조사가 필요해. 일단 조사부터 해줘. (조사) +105. 추가로 하이라이트에서 소수점 단위 구조물 측점들의 경우 1단위로 사사오입하여 측점을 지정하게 반영했던걸로 기억하는데 언제 했고 어떤식으로 했는지 알아보고 어떻게 개선할지 조사만 해봐. (조사) +106. (105번 보충) 일반측점이 있고 소수점 단위로 근처의 측점이 있다면 입력되는 기준의 측점을 값을 정수단위로 맞춰야하는게 아니냐는 거야. 선택이 안되는게 문제가 아니라 사용자가 소수점을 못넣게 하고 넣더라도 정수로 변경하여 맞출수 있는 기능이 필요하지 않을까해서 조사를 시킨거야. 조사만 해. +107. (질문) 노면공이 아닌데 범위값을 지정하는 구조물이 뭐가 있는거야? — 답: B 측구 · 산마루측구 · 소단측구 · 도수로·산비탈수로 · 절토사면 배수로 · 맹암거 / C 옹벽 · 돌쌓기(찰 · 메) · 흙막이 · 큰돌쌓기 · 소단 / E 대피소 · 정차·작업장 · 가드레일 / F 비탈면 녹화 +108. 셋다 너의 제안대로 해주고(104 구멍 둘 먼저 · 고름 한 칸 원천으로 단계별 묶음 · 구조물 기준 측점 정수 맞춤) 노면공은 아닌데 범위를 갖는 구조물은 예전의 자료인것같은데 지금 구현하는 구성을 다시 확인해봐. +109. 추가로 소단에 대한 측점 표기가 3D에서 측점 표현이 안되는것같은데 현재 구조물 리스트에서 내용과 현재 구조물군과 종류의 내용 중 누락된게 뭐가 있는지 조사해봐. (조사) +110. (질문) 지금 조사를 하고 있는 너의 별도 에이전트가 있어? +111. B09에서 설계 내역서의 내용중 공종, 규격, 수량 단위의 값이 텍스트입력을 바로 받게 되어 있어서 드래그가 힘들어. 더블클릭하여 입력을 할수 있게하면 드래그에 문제없을것같아. 그리고 해당 행의 하이라이트는 박스가 쳐지게 바꾸는게 좋을것같아. +112. B09의 테이블 행을 기준으로 항목을 레벨화하는게 어려운거야? 접히지 않는 트리라고 생각하면 쉽잖아. 지금 구조물조합의 마스터/회사/개인도 다 트리형태인거고 그럼 중간 폴더들은 당연히 값이 아니라 수식이들어와야하고 파일이되는 최하위 항목을 입력을 받게 하는데 이때 일위대가나 단가산출을 연결하게 하면되는거고 +113. 추가로 B08과 B09의 내용중에서 아래 설명때문에 페이지 스크롤이 동작해야하는 경우가 있어. 그냥 제작 설명을 제외하고 내용을 줄여주고 페이지에 스크롤이 동작하지 않게 해줘. + +## 다섯째 붙임 (10-10 저녁) + +114. 엑셀 프로그램에서 글 입력의 셀 맞춤 방식은 상하, 좌우 둘다 가운데 값을 기본값으로 변경 +115. 저장때 측점이 지워지는 구멍은 막을것. +116. 나머지는 일단 보류 (조사 AG 3D 표기 범위 · B06 꺼진 종류 · 표 레벨 번호 칸 · 되돌리기 · 로직 기본값 · 제비율 · 관급 등) +117. 앞으로 설명할때는 어느 페이지의 어떤 영역인지 말할것. 설명이 이해안됨. +118. B09의 총괄설계내역서는 내용중에 순공사비를 뽑기 위해 설계내역서의 레벨 2까지의 내용을 총괄설계내역서의 상단에 작성하는 내용이 빠져있음. 다시말해 레벨 2까지의 집계 내용을 상단에 정리해서 넣기만 하면 되고 해당항목을 선택하였을때 설계내역서를 이동하고 붉은색으로 하이라이트 하면 됨. +119. 설계내역서의 폴더구조는 좋은데 레벨에 따라 폴더가 되고 폴더가 되면 폰트를 같은 레벨의 다른 것들과 같은 폰트 사이즈와 색상을 선정하는게 어려운거야.? 너가 직접 확인하고 코드작업해줘. +120. B08의 에서 메인탭으로 거리이정표를 만들어주고 하위탭으로 거리이정표 하나더 추가 거리에 따라 별도로 추가 탭이 생성이 되면 됨. 생성되는 기준으로는 거리 이정표를 구현하여야함. (첨부 = 거리이정표 견본: 태원 · 주상기업 · 거창군청 · 병곡리 968-31 · 시점 · 중점 · 김천역 상자와 도로구분 km, 아래 표 = 구분(장비 거창군청→시점 20톤 · 8톤 / 혼합골재 골재원→임도중점 / 전석운반 골재원→임도중점 / 철근 김천역→임도중점) · 도로구분(2차포장 · 1차포장 · 비포장(1)) · 계(포장 · 비포장 · 합계) · 비고) 기본적으로 시점의 위치는 공사지의 시작 위치임. 중점은 사용자가 거리를 입력하는 위치 이두개는 하나의 컨테이너로 묶어서 공사지가 되고 공사지를 기준으로 특정 기준점이 있을수도 있고 없을 수도 있음. 이는 첨부사진의 병곡리가 됨. 특정기준점이나 공사지를 기준으로 거래업체를 다음 지도 API로 맵을 보여주고 길찾기를 하여 거리를 확보하고 이때 거리찾기한 항목을 각각의 하위탭으로 컨테이너 하나에 사진하나씩 배치해주면 됨. 여러개가 있을수 있으니 추가와 삭제 버튼은 있어야함. +121. B09의 설계내역 테이블을 확인했는데 제어가 어려웠던 이유는 레벨이동의 앞에 들여쓰기가 너무 좁아서 적당한 위치에 놓기 어려웠던것같아. 그러니 들여쓰기 폭을 조금 더 띄워주고 추가로 어디에 추가되는지 어디가 들어가는지가 불분명해. 조금더 잘보이게 표시해줘. +122. 추가로 B09의 좌측패널에서 데이터 점검 컨테이너와 업데이트 버튼 사이가 너무 협소해. 업데이트 좌측 여유만큼 상단에도 여유가 있었으면 함. +123. 다음으로 추가로 B08과 B09의 테이블의 내용중에 사용자가 입력가능한 셀영역을 셀색상을 바꾸는데 이것대신에 셀 외곽부에 테두리를 넣게 변경해줘. +124. 입력셀 테두리 색상선정할때 블랙과 화이트테마를 잘 고려해줘. +125. B09에서 총괄설계내역서의 내용을 선택하게 되면 설계내역서의 해당항목을 하이라이트 되는데 이때 하이라이트는 해당 항목의 하위 내용까지 같이 범위로 지정해줘야해. 다음으로 그냥 행선택과 하이라이트가 중복으로 표기되는데 하나만 표기되게 해줘. 하이라이트 되었더라도 다른 행을 선택하면 하이라이트가 취소되어야하고 이전에 선택된 행이 있더라도 총괄설계내역서에서 선택하면 선택행이 취소되고 하이라이트되어야함. +126. 다음으로 B08과B09의 테이블 내용에서 수기입력 방식은 더블클릭으로 변경해줘. 모든 값들은 쉽게 변경되면 안되거든. +120-답(조사 AH 물음 넷 답 · 원문). ① 경로 그림 = 「일단 브이월드에서 길찾기가 있는지 몰랐네? 있다면 브이월드도 좋아 벌써 API가 연결되어있거든.」 ② 도로 구분 = 「모두 사용자가 선택하게 해줘. 기점별로 어차피 드롭다운 메뉴를 지정하면 되지 않을까?」 ③ 표 구분 = 「구분에서 첫번째열은 항목: 수기입력, 2번째열은 업체이름: 수기입력, 세번째 열은 목적지임. 사용자는 다이어그램으로 표현된 컨테이너를 이용하여 추가수정할수 있게 하되 대신 방향은 기준점을 기준으로 지도상의 방위를 나타냄. 항목을 추가할때는 구분정보와 지도상의 위치값과 길찾기 값을 받아와야함.」 ④ 중점 = 「사용자가 입력할수 있게 해줘. 기본값은 도로의 중간지점으로 기본값으로 하고 사용자가 지정하게 하자.」 +127. 다음으로 공사원가계산서의 테이블에서 병합기능을 사용하였는데 행의 마우스 호버에 연동된 하이라이트 기능이 형태가 복잡해지고 있어. 개선해줘. +128. (B09 설계내역서 표 화면 붙임 · 입력 칸 보라 테두리) 테이블에 셀테두리를 추가했는데 구분에 전혀 도움이 되지 않고 있어. 일단 테두리는 원래대로 돌려주고 다음으로 새 공종추가, 아래 줄, 위에 줄 추가들은 하나로 항목 추가로 하자. 어차피 마우스 우클릭 후 해당 위치를 기준으로 그냥 하나를 넣어주면 되는거야. 어차피 위치에 따라 결정되니 없어도 문제 없어보여. 연결에서는 서고나 (마스터 / 회사/개인)이라는 이름을 삭제해줘 쓸데없어. (M01도 새산근도 동일함.) 구조물 조합, 일위대가 사전조합, 단가산출 이렇게만 표현해줘. 줄지우기는 줄 삭제로 변경 +129. 다음으로 마우스 우클릭 같은 패널에 항목을 추가하여 보여주는데 그냥 우측에 신규 페이지로 연결 5개 항목을 보여주면 됨. +130. (거리이정표 고침 · 원문) 1. 공사지의 + 버튼으로 나오는 모달창을 이용하여 항목, 업체이름 목적지를 받음. 방위항목은 지도상에 지정한 위치에 따라 자동배치되는 항목임. 하단에는 주소검색 기능을 구현과 등록완료시 지정된 맵을 보여주는 기능 구현. 프로젝트 등록에 관련 기능있음. 재활용, 시점을 컨테이너를 누르면 모달창으로 프로젝트 등록할 당시의 주소와 브이월드 주소와 하단에 맵을 보여줌. 2. 공사지의 (끊김) 3. 다이어그램의 영역은 팬과 줌인 동작이 되어야함. 동작과 관련해서는 공용 양식 있음. 확인 4. 보조탭으로 별도로 추가버튼 필요 없으며 탭의 내용은 거리 길찾기를 한 맵의 결과가 필요함. 구간, 표줄 이런건 필요없음. 만약 길찾기의 경우 안된다면 별도의 창에서 다음맵을 연결하여 길을 찾게하고 해당 브라우저를 확인시 캡처하게 하도록 변경 5. 컨테이너를 누를경우 모달창으로 관련정보를 보여줌. 페이지 이동 아님 +130-6. 거리와 도로 구분 정보는 다이어그램에서 바꿀수도 있게 변경 +131. 다음으로 구조물 집계표에서 해당측점의 행이 구조물조합들이 전부 연결이 된 상태라면 녹색램프 표현, +132. 거리이정표의 추가내용 - 테이블의 폭은 상단 다이어그램과 동일한 폭을 유지 - 다이어그램의 내부 내용에는 항목도 추가 - 좌측패널의 거리이정표 패널은 필요없음. 다이어그램에서 직접 수정 - 다이어그램 팬 동작 구현안됨. +133. 업체라는 말보다는 거래처라는 말이 맞아보임 +134. 다음을 별도 브라우저 탭이 아닌 내부 모달창 내부에서 실행 안됨? 그래야 캡처가 쉬울것같음. (브레인 확인: map.kakao.com 길찾기 링크가 iframe 안에서 열림 · 경로 · km 보임) +135. 추가로 다이어그램의 영역에서 스크롤이 존재함. 줌인아웃에 방해됨. 스크롤 안되게 해줘. +136. (길찾기 방식 결정) 일단 카카오로 가야할것같아. 카카오 디벨로퍼 페이지에 와있는데 제휴방법을 알려줘. +137. (경로 그림 방식) 가로 가는게 좋겠어 — 서버가 카카오 길찾기 경로 선을 브이월드 지도 위에 그려 자동 저장. 메일에도 거리와 경로데이터라고 해야하는거 아니야? +138. (조사 더 · 너무 보수적 아님?) 가 사안은 배제해. 데이터가 다른 맵위에 맞을리가 없어. → 나(카카오 지도 JS 위 경로 + [캡처])로 +139. 그럼 캡처는 백엔드에서 하고 그림만 가져오는건 어때? (브레인 시험: 서버 헤드리스 Edge + 카카오 지도 JS + 길 선 → 1200×800 PNG 2.4초 성공) +140. 테일스케일 IP는 (카카오 JavaScript 도메인에) 등록했어 참고해 +141. 다음으로 구조물 집계표에 구조물조합 연결이 완료된 항목은 녹색 램프를 넣어달라고 했는데 없어. (화면 붙임 · 노랑만 · 247-1 이 sub7 에 안 들어갔던 것 — 다시 보냄) +142. 다음으로 거리이정표의 다이어그램을 보여줄때 적당히 넓은 영역을 배경영역으로 지정되어야할것같아. 초기에는 당연히 공사지 컨테이너만 있을텐데 이러면 어떤 항목이 들어와도 다시 줌아웃을 해야하잖아. +143. 시점과 중점도 연결선이 존재해야하고 시점과 중점컨테이너에도 작은 글씨로 주소와 좌표값을 보여주는게 좋을것 같아. +144. 거래처 컨테이너의 경우 항목, 거래처, 주소를 보여줘. +145. 추가로 거리이정표에는 선들과 컨테이너들의 공간을 여유를 조금 두는게 좋겠어. 자꾸 침범을 하네. +146. 추가로 누락된 업무가 있는지 확인해봐. +147. (브레인 물음 답 · 원문) 1. 백엔드 재시작 2. 폴더만 기준이 되면 됨. 3. 반영 4. 테이블이 아닌 항목은 대상이 아님 5. 다음으로 공사지 +로 추가하는 항목에서 나오는 모달창의 크기를 조금 더 키우고 내부 내용을 정리할 생각임. 항목은 분류(기준위치, 거래처) 항목, 거래처 이름, 목적지를 2열배치 이때 분류에 따라 항목이 바뀜(다이어그램의 기준점+ 버튼은 삭제), 거래처 위치를 소재지로 변경하고 직접입력버튼은 삭제. 전부 주소찾기나 지도에서 찾아야함. 등록에 이제 다음지도로 대체 6. 메일은 내가 알아서 하겠음. 7. 데스크탑은 세션 확인하면 살아있음. 오르카 리스트 확인 8. 추가 거리이정표 항목 - 등록으로 보이는 모달창은 해당 컨테이너를 눌렀을 때 같이 보여주면 됨. - 길찾기는 연결선을 누르면 보이는 모달창에 추가 이때의 항목은 도로 구분과 거리를 항목값으로 받고 하위에 다음맵으로 경로를 보여주면 되는데 기준점이 발생되는 이유는 임도 특성상 도로가 없는 경우 한 지점을 지정하게 됨. 다시말해 기준점이 있다면 기준점과 거래처의 다음맵지도가 필요함. 이때 위치조절을 위해 위치이동은 되어야함. 시점과 기준점은 도로가 연결이 안되는 경우가 있어 수기 입력이 되어야할때도 있음. 시점과 중점은 항상 거리는 수기입력임. 이때 도로 구분의 경우 2차 포장이란 시점이 아니라 기준점이 있을때의 경우임. 다시말해 기준점을 거쳐 시점에 들어가는 경우에만 존재하는 열 항목임. 기준점이 없다면 그냥 포장이 됨. +148. 길찾기 그림이 없는 이유는 뭐야? (브레인 판정: 서버 안에서 헤드리스 브라우저를 못 띄움 — uvicorn 이 쓰는 Windows Selector 이벤트 루프는 하위 프로세스를 못 만들어 Playwright NotImplementedError · 시험은 서버 밖이라 통과) +149. 고침이라는 명칭을 수정으로 변경. 컨테이너 항목에서 + 줄이 있는 이유가 뭐야. 거래처 등록은 거래처에 관련 정보를 선에는 경로관련을 묶어서 표현하려고 하는거잖아. 그리고 기준위치에는 항목이 필요없어. 거쳐가야하는 지점이다보니 의미가 없어. 이번 부터는 너가 직접작업해. 거리이정표의 경우에는 폭을 브라우저에 맞춰서 폭 전체를 사용하게 하고 하단의 테이블도 동일하게 폭에 맞춰 열의 사이즈를 조절해줘. 그리고 현재 다이어그램이 줌아웃이 너무 멀어서 글자가 잘안보여 적당히 줌인해줘. From ed4df9e2c359b467307494810eba748b870b9544 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 12 Oct 2026 23:02:08 +0900 Subject: [PATCH 05/81] =?UTF-8?q?chore(=EB=8F=99=EA=B8=B0=ED=99=94):=20?= =?UTF-8?q?=EA=B3=B5=EC=9C=A0=20docs=20=EC=9A=94=EA=B5=AC=20=EC=9E=A5?= =?UTF-8?q?=EB=B6=80=20=EB=B0=9B=EA=B8=B0=20=E2=80=94=20=EC=9B=90=EA=B2=A9?= =?UTF-8?q?=EA=B3=BC=20=EA=B0=99=EC=9D=80=20=EB=82=B4=EC=9A=A9?= 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_01YZVseAexp5wNFNVfRtxjkg --- docs/raw/요구_1010_2/사용자요구_1010_2.md | 42 +++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/docs/raw/요구_1010_2/사용자요구_1010_2.md b/docs/raw/요구_1010_2/사용자요구_1010_2.md index a2ae14ba1..76ce578ab 100644 --- a/docs/raw/요구_1010_2/사용자요구_1010_2.md +++ b/docs/raw/요구_1010_2/사용자요구_1010_2.md @@ -178,3 +178,45 @@ 147. (브레인 물음 답 · 원문) 1. 백엔드 재시작 2. 폴더만 기준이 되면 됨. 3. 반영 4. 테이블이 아닌 항목은 대상이 아님 5. 다음으로 공사지 +로 추가하는 항목에서 나오는 모달창의 크기를 조금 더 키우고 내부 내용을 정리할 생각임. 항목은 분류(기준위치, 거래처) 항목, 거래처 이름, 목적지를 2열배치 이때 분류에 따라 항목이 바뀜(다이어그램의 기준점+ 버튼은 삭제), 거래처 위치를 소재지로 변경하고 직접입력버튼은 삭제. 전부 주소찾기나 지도에서 찾아야함. 등록에 이제 다음지도로 대체 6. 메일은 내가 알아서 하겠음. 7. 데스크탑은 세션 확인하면 살아있음. 오르카 리스트 확인 8. 추가 거리이정표 항목 - 등록으로 보이는 모달창은 해당 컨테이너를 눌렀을 때 같이 보여주면 됨. - 길찾기는 연결선을 누르면 보이는 모달창에 추가 이때의 항목은 도로 구분과 거리를 항목값으로 받고 하위에 다음맵으로 경로를 보여주면 되는데 기준점이 발생되는 이유는 임도 특성상 도로가 없는 경우 한 지점을 지정하게 됨. 다시말해 기준점이 있다면 기준점과 거래처의 다음맵지도가 필요함. 이때 위치조절을 위해 위치이동은 되어야함. 시점과 기준점은 도로가 연결이 안되는 경우가 있어 수기 입력이 되어야할때도 있음. 시점과 중점은 항상 거리는 수기입력임. 이때 도로 구분의 경우 2차 포장이란 시점이 아니라 기준점이 있을때의 경우임. 다시말해 기준점을 거쳐 시점에 들어가는 경우에만 존재하는 열 항목임. 기준점이 없다면 그냥 포장이 됨. 148. 길찾기 그림이 없는 이유는 뭐야? (브레인 판정: 서버 안에서 헤드리스 브라우저를 못 띄움 — uvicorn 이 쓰는 Windows Selector 이벤트 루프는 하위 프로세스를 못 만들어 Playwright NotImplementedError · 시험은 서버 밖이라 통과) 149. 고침이라는 명칭을 수정으로 변경. 컨테이너 항목에서 + 줄이 있는 이유가 뭐야. 거래처 등록은 거래처에 관련 정보를 선에는 경로관련을 묶어서 표현하려고 하는거잖아. 그리고 기준위치에는 항목이 필요없어. 거쳐가야하는 지점이다보니 의미가 없어. 이번 부터는 너가 직접작업해. 거리이정표의 경우에는 폭을 브라우저에 맞춰서 폭 전체를 사용하게 하고 하단의 테이블도 동일하게 폭에 맞춰 열의 사이즈를 조절해줘. 그리고 현재 다이어그램이 줌아웃이 너무 멀어서 글자가 잘안보여 적당히 줌인해줘. +150. 기준점과 선의 컨테이너 내부 카카오맵의 영역의 높이를 더 높여줘. 카카오맵의 높이는 어디서든 같게 해줘. +151. 그리고 선 성분의 연결정보는 컨테이너들을 드래그해서 연결하고 이정보를 가져오는건 어때? (제안 · 물음) +152. 그리고 포장과 비포장이다가 기준점이 생성되면 당연히 1차 포장, 2차 포장이 되어야할것같아. 그리고 그때 선의 연결여부에 따라 1차냐 2차냐가 되는거고 +153. 카카오맵에서 기준점이나 소재지를 지정하면 보기가 초기화되고 줌아웃 상태가 되고 있어. 변화가 없는게 편할것 같아. 공사지, 기준점, 거래처를 기준으로 색상을 조금 넣어주는 좋겠어. 이때 프로그램의 내부에 화이트와 블랙테마 양식이 있으니 참고해. 다음으로 요소들을 드래그로 위치를 변경하거나 정렬하는 기능이 있으면 좋겠어. +154. (선 연결을 드래그로 — 연결 점 ● 끌어 기준점 · 공사지에 놓기 · 설명 뒤) 동의 작업진행 +155. 컨테이너 요소는 총 4개야. 3개의 컨테이너와 하나의 선 타입이야. 시점과 중점은 자동생성으로 그냥두고 기준점과 거래처의 정보를 등록하고 선으로 연결하면 모달창으로 정보가 나오는데 이때 경로를 찾은 거리와 경로를 카카오맵으로 보여주면 되는거야. 이때 문제가 있을수 있으니 수기입력 기능으로 카카오맵 연결을 취소하게 하면 될것같고 기준점과 공사지의 우측 상단의 +는 삭제하고 자동정렬 버튼 우측에 요소 추가 버튼을 만들어서 대체하게 해줘. +156. 거리이정표탭의 하위 탭중 거래처 항목 탭의 경우 내부에 길찾기가 있는데 이항목은 이제 없어도 될것같은데 어떻게 생각함? 필요없는 로직으로 보임. +157. 1. 요소추가 버튼은 다이어그램 영역의 좌측 하단으로 이동하고 버튼사이즈는 메인 탭 제목과 같은 사이즈양식으로 변경, 자동정렬도 같이 이동 2. 하위 거래처 탭에서 보이는 정보중에 그림 올리기 버튼은 그림 변경으로 이름 변경, 그림 지우기 버튼은 필요 없으나 초기화 버튼으로 사용자가 올린그림을 지우고 원래연결되 그림으로 되돌리는 기능으로 변경. 이름은 초기화. 3. 하위 거래처 탭에서 내가 보기에는 이제 그림으로 표현할 필요는 없어보이는데? 거리 이정표에서 선을 선택할 때 보이는 길찾기 화면을 보여주는게 제어도 되고 좋을것같음. 단 사용자가 그림 변경하여 업로드하면 변경한 그림을 보여주고 초기화하면 다시 길찾기 화면을 보여주면 됨. 추가로 쓸데없는 설명은 삭제 그림 변경과 초기화 버튼은 거래처명이 작성된 라벨과 같은행에 우측맞춤하여 배치, 거래처명, 주소, 버튼의 글자는 크게 변경, 메인 탭 사이즈와 동일하게 +158. 자동정렬의 기준이 뭐임? 혹시 각각 요소들의 위치를 나타내는건가? (질문) +159. (원문) 1. 자동정렬 명칭을 방위배치로 변경(방위 배치 안보이고 테이블이 브라우져 폭을 다사용하게 반영 안됨) 2. 이제 연결작업을 할 생각임. 요소추가 버튼으로 생성할 수 있어야하지만 기본적으로 프로젝트의 사용하고 있는 구조물조합의 일위대가의 자재목록을 하나의 행으로 구성되어야함. - 초기에는 거래처와 주소가 지정되지 않은 컨테이너를 다이어그램에 배치해주고 사용자가 컨테이너의 모달로 접근하여 정보입력하게 됨. - 이후 선을 연결하여 정보 연결을 마무리함. 3. 이후 사용자는 단가산출 로직이나 마스터/회사/개인의 일위대가 사전조합을 연결한다. 이때 연결여부 표현을 위해 (구조물 집계표 측점 램프 그림) 항목 앞에 램프를 추가함. 4. 이 정보들은 모여서 B09의 운반공 하위의 항목으로 각 행들이 들어간다. 이때 B08에서 연결이 안되어 있더라도 설계내역서 행의 기능 중 연결 기능으로 단가산출이나 일위대가 사전조합을 연결할 수 있어야함. 5. 설계내역서/거리이정표의 마우스 우클릭의 기능 중 연결에서 여러정보들이 나오는데 이렇게 하지말고 모달창을 띄워서 보여주고 한번에 검색하거나 필터링하여 검색할수 있는 기능 부여. 이때 연결이라는 명칭보다는 단가연결 이라고 변경하여 표현 6. 연결은 중요한 문제니 너가 직접 조사 작업을 하여 계획서를 작성하고 작업을 이어서 해줘. +160. 추가로 단가연결을 할때는 구조물조합이나 일위대가 사전조합이라면 문제가 없겠지만 단가산출의 경우에는 연결할때 옵션값을 받아야하는 경우가 있을거야. 이때는 추가 모달창을 보여줘서 사용자가 선택할수 있게 반영해줘. 만약 내가 잘못알고 있는거라면 알아서 해주고 +161. (원문) 다음은 B09에 신규 메인탭인 공기를 넣어주고 하위에 공기집계와 공기산출 탭을 만들어줘. 이때 공기관련내용은 초안이니 B09 폴더 내부에 draft폴더를 만들어서 코드를 작업해줘. 나중에 마음에 안들면 그냥 draft폴더를 삭제할거야. 일단 참고문서는 resources\knowledge\original\실무문서\01.산불거창(거창북상병곡)\01.내역서\00.25년_산불_거창(거창,북상,병곡,산8외)내역+수량.xlsx 파일 중에 공기집계와 공기산출 탭을 참고하면 될거야. 여기서 중요한 점은 참고를 보고 내용을 넣는게 중요한게 아니라 로직을 분석하고 어떻게 계산하는지 확인하고 구현하는게 중요한거야. 다시말하지만 참고 항목이 있다고 테이블을 만드는게 아니야. 우리 프로그램에 맞춰 적용하는게 중요한거야. 그래서 길게 생각하고 작업해도 되니까. 꼼꼼히 분석해서 반영해줘. 이건 현재 작업내용을 이어서 진행하면 됨. 나 쉬러 갈테니까 작업완료하고 내일 아침에 내가 아티팩트를 만들어줘. 일단 이번 구현에 대해서는 방향성 결정이 필요한 내용이 있다면 너가 결정해서 아티팩트로 알려줘. +162. (원문 · 10-11) 1. 거리이정표 - 설계내역서 운반공 연결 - 거리이정표의 항목행 하나당 하나씩 설계내역서의 운반공 하위의 노용으로 들어가야함. - 거리이정표와 설계내역서의 연결 후 선택된 항목에 대해 거리값을 입력받아야하는 경우 연결되거나 될 거리이정표의 거리와 연결되어야 하며 이때 아마 비포장과 포장이 다를것같은데 이부분에 대한 계산 절차를 보여주는 모달창이 있어야함. 이때 모달창에는 옵션값과 계산 절차를 같은 화면에서 보여주며 단가산출을 다른 항목으로 수정할수 있어야함. 단가연결이 안된경우와 된경우를 같은 모달창을 사용하면 좋을것같아. 2. 거리이정표의 하위탭중 항목별 경로는 나타내는 하위탭에서 거래처/기준점/시점을 옮긴다면 주로를 옮겨서 업데이트 3. 거리이정표의 시점에서 나오는 모달창의 중점 측점은 km 단위로 변경, 하위의 설명은 삭제, 맵은 공통 카카오맵으로 변경 4. 거리이정표의 다이어그램에서 항목의 폰트를 거래처의 폰트와 동일하게 변경, 아래 주소가 길경우 자동으로 줄변경하고 컨테이너 세로 높이 증가, 방위 배치나 기본 맵연결에 따라 위치를 잡을때 거리에 따른 선길이가 지정되면 좋겠음. 단 너무 널리떠러지면 안됨. 선, 위의 텍스트 중 포장타입과 거리는 2행 구조로 변경, 거래처 및 기준점에 대한 모달창에서 제목의 폰트사이지가 내용보다 작음, 소재지-주소찯기.지도에서 고르기(...) 항목은 삭제, 기준점의 모달창인 경유 기준점의 이름이 2행구조로 됨. , 주소 및 맵 영역은 상단에 가로선을 추가하여 구분할것. 5. 기준점의 다이어그램 컨테이너에도 주소 추가 6. 거리이정표의 다이어그램에서 선연결의 드래그하는 원을 박스의 좌측 상단에 배치, 단 테두리를 넘어가면 안됨. 추가로 컨테이너의 내부에 작은 주소를 찾은 그림을 캡쳐하여 프로젝트 영구저장소에 넣어주고 컨테이너 내부에 배치 이때 그림을 다른 곳에서도 사용할 예정임. 잘 보관할 것. 7. 거리이정표의 하위 탭의 경로보기 기능 중 그림을 캡쳐하여 프로젝트 영구저장소에 넣어줄것. 프론트엔드 기능변경이 아님. 그림은 나중에 다른곳에 사용할 예정 8. 거리이정표의 다이어그램을 누렀을때나오는 제목에 테이블 제목 행의 정보가 들어오는데 삭제. 9. 거리이정표의 테이블이 컨테이너가 다이어그램의 영역과 동일하게 브라우져의 폭에 맞춰 변경이 되지만 테이블은 맞춤이 안됨. 혹시 테이블 양식과 충돌이 있다면 알려줄것. 10. 거리이정표의 기준점은 2개 이상일 수도 있을것같음. 이럴경우를 대비하여 선연결 기능을 기준점에도 추가 11. B09 총괄설계내역서와 설계내역서를 하위탭으로 이동하고 상위 탭으로 설계내역 탭을 생성 12. B09 단가연결 열의 정보가 텍스트로 구현되어있음. 랩프로 변경(램프양식은 B08의 구조물집계표에 있음), 선택 기능은 유지, 단가연결 열 제목은 단가 > 줄바꿈 > 연결 로 변경하고 테이블 열 폭을 줄일것. 13. B09와 테이블 행선택에서 페이지이동 기능이 구현된 행의 경우 붉은색 박스 하이라이트가 반영안된게 있음. 14. 하이라이트 기능과 연결하여 조심할 부분은 셀에 수기 입력이 가능한 영역을 셀테두리로 표기한게 없어졌음. 조심 테두리의 값을 조금 더 옅게 표현. 15. 자재단가대비표에 선택한 적용단가인 경우에 해당 셀에 색상을 변경했는데 이걸 테두리를 넣는걸로 대체. 이때 입력셀 테두리와 혼돈이 될수 있으니 녹색계열로 변경 16. 자재단가대비표에서 최저가 일지라도 적용단가를 변경해야할 경우가 있음. 마우스 우클릭에 항목을 적용단가 적용 항목 변경 17. 설계내역서의 비고 열에 빈 칸XX 정보는 삭제. 18. 총괄설계내역서/공사원가계산서의 제일하단의 총공사비는 하단고정으로 변경. 양식은 테이블 양식 확인 19. B06의 횡단도에서 상세버튼 모당창의 내용 중 나눠진 면적지정에 대한 기능 구현안됨. 기준은 현재 선들이 교차가 되면 선 요소를 분할하였음. (암경계선도 분할 필요) 이렇게 됨면 오픈된 영역을 제외하면 먼저 지정도 가능해야함. 이때 선택된 면적은 선을 지정하였을때 길이관련 정보(기능 누락됨)보여주는 정보들처럼 면적 정보들을 보여주면 됨.(이때 테이블을 선택하면 그래프에서 선이나 면적을 지정하게 될텐데 이때도 길이나 면적이 보여야함. ) 20. 횡단도 상세버튼 모달창 테이블의 항목들을 선택하면 테이블 하이라이트가 되는데 여기서 셀과 셀 제목을 지정해주는건 좋은데 중복되는 선때문에 보기가 안좋음. 21. 횡단도 상세버튼 모달창에서의 그래프 영역하단은 텍스트 입력창으로 별도의 정보를 B07로 전달되어야하는데 내가 원하는 방향은(그림_162-21_횡단배수_글.png — 배수관 원 표기 · NO.71+13 · 관보호공 · 관유입부보강 글) 구조물이 없는 경우에는 당연히 빈값이어야하고 구조물 중 횡단배수 구조물에 대해서만 미리 넣는 스크립트를 만들어주면 됨. 이때 이질감이 있을수 있으니 텍스트창이아니라 캐드창을 보여주는데 그냥 배경만 있는 캐드창이기만 하면됨. 그럼 다음B07에 테이블 상단에 배치하면 이질감이 없을것같아. 기본적으로 설계자들은 대부분 캐드 단축키를 알고 있으니 빈화면의 캐드화면이라도 문제 없어보임. 22. B05/B06 공통 좌측 패널의 구조물 조합 지정 기능에서 미지정 항목에 대해 노란새 색상이 좋기는 한데 눈에 너무 띄는것같음. 이건 구조물리스트의 램프와 같은 색상으로 가는게 좋겠어. 회색이 될것같네. 23. 이제까지는 너가 직접 코드작업을 요청했지만 이제 폭넓은 부분에 코드작업을 해야하니 너가 조사작업을 하고 계획서를 작성하고 브레인으로써 다른워크트리에게 작업을 지시해줘. 그리고 완료시 너가 작업이 누락된게 없는지 확인해줘. 코더는 자기가 작업한 코드만 검증을 하고 화면 검증은 작업완료시 내가 진행할께. 너도 필요하다고 판단되면 브라우져 검증진행해도 됨. +163. (원문 · 브레인 화면 검증 흠 다섯에 대한 답) 고칠 흠중 1. 소수점 제어 확인 2. 보류 3. 제안대로 4. 제안대로 5. 운반공의 하위에는 단가연결이 연결이 되던안되던 일단 이정표의 항목명으로 운반공에 하위에 개별로 들어와야함. +164. (원문 · 10-11) 1. 거리이정표 - 거래처의 주소를 입력할때 정해져 있다면 공사지나 시점의 마킹을 남기는게 좋음> - 거리이정표의 컨테이너 모달창에서 제일 상단의 제목이 거래처 - 거래처, 공사지 - 시점,종점 , 기준점 - 주소 로 표현됨. 에서 - 앞의 글자는 필요없음 - 단가연결에서 수기로 전체입력을 받는게 아님. 사용자는 미리만들어진 단가산출 로직이나 구조물 조합, 일위대가 사전조합에서 지정하게 될텐데 이때 지정한 항목이 필요한 입력정보를 보여주는데 여기서 이정표로 거리값을 확보했으니 입력에 자동으로 넣어달라는 말이잖아. 처음부터 값을 하나하나 지정해서 만든다는게 아니라. 그리고 어차피 너가만든 기준으로 만든 8-2-8 도 벌써 만들어져 있어. 입력값이 필요할 뿐이지. 2. B09 공기초안의 아티팩트는 봤어. 그런데 이상한건 너가 정한것들은 모두 사용자가 지정해야할 내용들 아니야? 그렇다면 B09의 좌측패널에 별도의 컨테이너를 만들어서 옵션을 선택할 수 있게 변경해줘. 3. B06 횡단도 상세 모달창의 내부의 캐드영역 구현은 잘되었음. 여기서 접었다 폈다하는 기능을 구현해주고 일반측점의 경우에는 접힌상태가 구조물 측점의 경우에는 펼쳐진 상태가 모달창이 열릴때 기본값으로 변경하고 캐드영역과 테이블 순서를 바꿔줘. 4. B06 횡단도 상세 모달창의 내부 횡단도에서의 기본 줌인값은 B06 좌측패널의 횡단반폭 보기설정값을 적용하게 해줘. 5. B06 횡단도 상세 모달창인 캐드화면에 기본 값을 배관만 넣어줬는데 다른 구조물도 넣어야하고 문구를 그냥 내가 넣어준 값을 복사하여 사용한것 같은데 여기서 구조물조합에 연결된 일위대가의 이름과 관련 정보(옵션 값)들을 넣어주면 됨. 그럼 사용자가 수정하여 사용함. (배관은 원형마크로 대체 되어야함. ) 6. B06 횡단도의 상세 모달창의 캐드화면에서 배관의 길이는 정수값을 사용해야함. 사사오입한 값을 사용해야함. 벌써 내부에 계산되어 있음. 7. B06 횡단도에 표기되니 관매설 정보를 알려주는 노폭 정보위의 회색 글씨의 크기는 노폭정보 폰트 크기와 동일하게 변경 +165. (원문 · 10-11) 1. B07의 횡단도에 측구를 표현할때 노란색으로 측구가 하나더있어보임. 확인후 삭제 2. B07의 배관이 구분이 어려움, 색상변경,(박스암거도) 3. B07 좌측 패널의 사이즈는 요소들의 버튼과 입력창의 양식은 B05/B06 좌측 패널의 구조물 배치의 공용양식 사용. 이때 B06의 다른 컨테이너도 포함. B05 이상의 페이지들의 좌측패널의 컨테이너 공용양식 사용확인, 이때 패널의 하단에 버튼셋들은 예외. 4. B05/B06 좌측패널의 구조물 리스트나 B06의 횡단도, 배수유역도의 번호 및 영역, 테이블열, 3D 측점 하이라이트 기능은 다연동되어 있는데 지금 배수유역과 테이블 열 하이라이트 기능을 잃은것같아. 이건 정말 조심히 개선해야하는데 어제 저녁쯤에 상당히 오랫동안 분석해서 나온결과야. 주요 문제점은 노면공의 범위 값에 의한 하이라이트 충돌문제였어. 제대로 확인한 후에 4번만 너가 직접 작업하고 브라우져 검증해줘. +166. (원문 · 10-11 · 브레인 보고 답) 1. CAD 미리넣는 기능에서 사용자가 어떻게 사용할지를 몰라서 일단은 좌측패널 옵션과 구조물조합이름을 다 넣어주면 내가 추가로 선정하겠음. 2. 상세 창 처음화면은 전체가 다안보여도 상관없어. 3. 표열 강조가 무슨의미인지 자기 측점이 없는게 있을수 있나 무슨 말인지 모름. 4. 깃 싱크해도됨. 5. B05/B06 좌측 패널의 구조물 리스트 상단에 정수 맞춤 버튼이 있는데 역할을 전방위 적으로 코드를 확인해줘. 내가 원하는 방향은 전처리과정에서 확보된 배관지점들을 결정할때 반올림하여 횡단배수들의 위치를 결정되어한다. 이후 사용자들이 조절을 하거나 다른 구조물 전체 중 어떤 걸 넣더라고 지정된 위치는 그냥 정수값을 받고 지정해야한다는 거야. 혹시 문제가 있는지 확인해줘. 그리고 만약 구조물 측점과 횡단도 측점이 같은 측점으로 뭉쳐진다면 상위인 구조물 측점이 존재하고 일반측점은 비활성화되어야해. 그리고 만약 구조물 측점을 옮기게 되면 비활성화된 일반측점은 활성화하고 구조물 측점을 이동하는거야. 이건 하이라이트 기능들과 많이 연동되어있어. 간혹 두가지 중첩된 상태로 보이는 상황이거든 정말 자세히 검토해줘. <<< 에게 왠지 3번 항목과 연관되지 않을까 생각됨. +167. (원문 · 10-11 · 조사 AK 답) 너가 뭔가 오해한것 같음. 1. 현재 측점을 가지는 구조물들을 유지해 필요하면 내가 나중에 추가할 수도 있음. 2. 프로그램에서 측점의 수치를 중 '측점 + 거리' or 누가거리에서 소수점을 사용하지 않겠다는 의미야. m 단위로 결정한다는 말이지. 3. 단추는 필요없어. 그냥 소수가 아닌 정수로만 제어하겠다는 의미야. 그러니 옛데이터도 그렇게 되어야겠지. 겹침 개선해. 지금 개발하면서 반복적으로 발생되고 있는 문제야. 이게 아마 하나씩 구조물 측점을 하나씩 추가하면서 발생된 문제같은데 근본적으로 문제를 해결해줘. 현재 전체적인 로직과 개발 방향은 잡힌상태야. 기능 확장과 동일 요소에 적용같은 방향만 있을것같은데 근본적인 문제해결을 위해 리팩토링이 필요하다면 말해. +168. (원문 · 10-11 · 272 뒤) 이상하네 B05와 B06은 하나의 페이지로 동작하게 만들어져 있어. 그렇다면 신규 추가나 이동에 대해 바로 반영이 되면 좋겠는데? 그리고 종단도에 다른 영역들의 선택에 따라 종단 그래프에서도 활성화가 잘되는데 이떄 해당지점으로 이동해주는 기능이 누락되었어. 이전 코드적용 가능한지 확인해봐. 조건을 측전 활성화 결과를 보고 해당지점으로 이동하게 하는게 좋을것같아. / 해당지점을 화면의 중간에 오면 더 좋고 +169. (원문 · 10-11) B06의 횡단도의 상세 모달창에서 모여주는 표위 cad에서 배수관의 경우 도형이 원과 하단 수직선으로 구성도 되는데 이때 하단 수직선은 필요없음. +170. (원문 · 10-12) 1. 검증대기는 진행 후 체크만 안할걸로 보임. 2. b08의 거리이정표 탭에서 연결 후 다시 접근하는 페이즈의 우측에 계산을 보여주고 있는데 단일 텍스트로 나열만 되어있어. 보기어려움. 폰트와 영역들을 나누고 결과를 분리해서 효과적으로 사용자가 볼 수 있게 개선 제안 +171. (원문 · 10-12 · 그림_171_운반단가창.png — 운반 단가 — 전석운반 · GF000284) 1. 거리이정표의 단가연결 편집에서 보여주는 우측 영역의 수식들이 여전히 분간이 안감. 컨테이너와 제목, 결과 값 작성이 필요한 경우 컨테이너 하단에 넣을 수 있게 변경했으면 함. 아마 여러 단가산출이 연결되어 있는 경우에는 내용을 컨테이너별로 분리하고 순차적으로 배치하는게 어떤지.. 2. 거리이정표의 단가연결 편집에서 보여주는 운반거리 입력 창의 높이만 다름. 앞/위의 항목과 동일하게 변경, 거리값을 가져온 경우 거리에서 라는 글자가 들어옮. 이때 글자는 거리에서의 글자를 자동계산으로 변경, 이후 사용자가 값을 변경할 경우 자동계산 버튼의 하이라이트를 끄고 다시 누른다면 자동으로 가져온 값을 넣게 변경 >>> 두개의 요청을 수정하기전에 조사부터 진행해줘. +172. (원문 · 10-12) 단가산출로직은 손대지말고 일단 프론트엔드 부터 집중해주고 . GF000284 이건 옵션이 없다는 말이야. 아니면 임의 지정했다는 말이야? +173. (원문 · 10-12) 지금 B08의 거리이정표의 단가연결에서 모달창의 좌측영역의 폭을 조금 줄여도 될것같아. 그리고 우측의 계산식 적용 방식 중 중간 계산에대한 보기 방식이 꽤나 마음에 들어 B09의 단가산출근거의 박스보기에도 적용해줘. +174. (원문 · 10-12) 1. 다음으로 다음으로 B09의 단가산출근거탭의 버튼 높이와 다른 보조탭의 높이가 달라. 이때 호표보기는 작게 표현되기도 하고 박스보기로 변경하면 조금 높아져. 지금 높아진 상태가 더 마음에 들어. 이걸 B08과 B09의 전체 영역에 보조탭 버튼에 적용되도록 양식화하고 변경해줘 +175. (원문 · 10-12) 1. 다음으로 B05/B06의 좌측패널의 구조물조합은 선택버튼으로 선택과 수정을 할수 있음. 선택 버튼은 삭제하고 미지정이나 선택된 구조물 조합 이름이 표기되는 라벨을 선택했을 선택 버튼 기능을 부여해줘. 해제버튼은 유지해줘. 2. 구조물 조합 선택 모달창의 이름을 구조물 조합 선택이라고 변경해줘. 그리고 모달창의 상단테이블의 항목을 선택하면 좌측하단의 트리구조에서도 선택 변경이 되어야하고 거기에 따라 우측의 3개 컨테이너 정보도 바뀌어야함. 그리고 사용자가 마스터, 횡사, 개인 버튼을 누르게 되면 상단 테이블이 해당항목으로 변경되어야하는데 지금은 그냥 유지되고 있는것같아. 확인해봐. 3. 2번 내용은 M02 구조물 조합에도 유사하게 반영되어 있는데 동일한 문제가 없는지 확인해봐. +176. (원문 · 10-12) B05/B06의 하단 종단패널은 공용으로 사용하고 있어. 여기에서 배수유역의 줌아웃 범위가 수치지형도의 등고선을 기준으로 하다보니 줌아웃이 너무 많이 되고 있어 노선을 기준으로 줌아웃을 하되 그래도 줌아웃할 경우가 있으니 적당히 범위를 지정해줘. +177. (원문 · 10-12) B05와 B06의 하단 종단패널에서 우측 상단의 배수유역도 버튼은 패널 내 우측 보조패널 버튼이 있어. 중복으로 보여 삭제해줘. 그 옆의 새창 버튼은 현재행에서 초기화 버튼 좌측으로 이동해줘. +178. (원문 · 10-12) 다음은 수정하지말고 확인만 진행해. B05와 B06의 선택 하이라이트 기능은 어제 엄청 힘들게 잡아둔상태야. 그러니 일단 코드 확인부터할거야. 지금 문제가 되는 부분은 종단도의 하단 알약버튼이 선택하면 하이라이트 되고 취소가 되어야하는데 이때 중요한 점은 구조물 중에는 측점이 없는 구조물 알약도 존재해. 그런데 선택을 하면 근처 측점을 하이라이트 하더라고 그리고 구조물 측점이 있는 위치라도 하이라이트 해제는 안되고 있어. 정밀 확인 해줘. 이건 너가 직접 코드확인해줘. +179. (원문 · 10-12) 1. 알약관련해서는 이제 너가 작업해줘. 대신 바로 확인해보고 롤백할거니까 기준점을 깃으로 잘지정해줘. 2. 추가로 이번에 수정한 새 창버튼의 이름을 창 분할로 변경하고 제자리로 버튼도 창 결합으로 변경해줘. +180. (원문 · 10-12) 1. 추가로 구조물이나 측점을 하이라이트가 된상태에서 대시보드나 프로젝트 외부 페이지를 접근하고 다시 프로젝트에 접근할때 측점이 하이라이트 된 상태로 복귀가 되는데 캐시문제는 아닌것같고 어디에 현재 영구저장소에 저장하고 이걸 보여주는 것 같은데 맞아? 코드수정하지 말고 확인만 해줘. 2. 다음은 횡단배수군 중 물넘이포장에 대해 종횡단 설계를 반영할거야. 종단에는 구조물측점이 있어야함. 물넘이포장 구조물측점의 경우 횡단도에서 측구가 존재할 수 없으며 노면의 경사는 입력된 정보를 기준으로 표고가 낮은 방향으로 기울어 져야함. 여기에 도수로/돌붙임이 추가되는 경우에는 다른 구조물과 공용으로 컨테이너를 보여주되 컨테이너 항목 하단에 추가로 길이 값을 받는 항목을 추가하고 도수로나 돌붙임의 길이값으로 가지고 있음. 이값은 설계내역이나 수량산출, 일위대가 등에 사용하지 않으며 단순히 횡단도의 도수로/돌붙임 그림을 그릴때만 활용한다. 도수로와 돌붙임의 길이값이 없는 경우에는 현재처럼 원지반까지만 하면 되지만 값을 넣는 경우에는 수치만큼의 길이의 도수로나 돌붙임의 형상을 횡단도에 넣어준다. 이번 수정에는 대부분 형상 로직들이 존재하며 재활용하여 로직을 구현하고 하이라이트와 구조물 측점 추가 로직은 공통로직일테니 항목에 물넘이포장을 연결만 하면 사용할 수 있을것으로 예상됨. +181. (원문 · 10-12) 그럼 만약에 대시보드로 갔다가 2번 프로젝트를 열고 다시 1번 프로젝트로 돌아오면 1번에 지정한 측점을 가지고 있는거야? 분석해봐. +182. (원문 · 10-12) 그리고 횡단배수군 구조물 종류 중에 구조물측점을 제외한 구조물은 뭐야? +183. (원문 · 10-12 · 그림_183_물넘이포장.png) 1. 다른프로젝트에 들어갈경우에는 전체 프로젝트 관련 캐시는 지우는게 맞는것같아. 다른 프로젝트들을 돌아다닌다면 캐시가 무거워진다는 말이잖아. 다시 돌아온다면 그건 사용자의 의견이니 당연히 다시 로딩하여 사용자에게 보여주는게 맞는것같아. 2. 도수로/돌붙임의 경우에 면적값은 조심해야하는게 길이값을 지운다면 면적값은 기본값으로 돌려줘야함. 다시 넣으면 폭과 길이로 면적값을 산출하면 됨. 그때는 원지반까지 연결하면 되는거고 이해한거지? 3. 맹암거의 횡단도 형상에 대해 설명하겠음. 높이값과 기울기값을 받게 되어있으며 측구는 존재하지 않음. 도로의 경사를 각도를 반영하여 지대가 낮은 방향으로 기울이고 맹암거의 높이만큼 도로를 옵셋하고 성토까지 연결. 절토측의 선을 연장하여 옵셋한 기울여진 선과 접함. 그후 그 안에는 돌모양을 넣는다. 4. 물넘이 포장의 경우 포장임으로 노면공처럼 도로의 포장형상을 추가함. 이때 노면공과는 다르게 노견까지 포함되어야함. 5. 종단도 그래프의 좌측 상단의 누가토량의 값을 나타내는 알약을 조금더 크게 적용해줘. 공용양식을 찾아봐. 대신 크기가 커지고 나면 아래로 알약위치가 조금 아래로 내려와야 상승 버튼이 안가려질거야. 6. 물넘이 포장의 옵션값 입력에서 월류 폭 입력하면 높이는 자동으로 계산되어질텐데 이건 세월교에 로직이 있을거야. 공용로직이야. 7. 물넘이 포장의 경우 [Image #5] 형상중 회색 점선의 경우 너가 그려준 포장과 같은 각도여야할것같고 성토가 있는 경우에는 도로와 같은각도로 포장형상이 늘어나야해. 단 짧아지는 경우에는 당연히 성토가 생겨야함. 8. 물넘이 포장의 경우 콘크리트면이 기준이 되어야하고 점선이 물넘이 월류높이라면 중심점 위로 표현이 맞음. 9. 물넘이 포장과 맹암거의 구조물 선택에 대해 고려하여 반영할것. 이것까지 반영하고 너가 직접 사용자처럼 브라우져 검증해봐. +184. (원문 · 10-12) 10번 구조물 배치에서 신규 구조물은 추가버튼을 누른 뒤에 추가되게 반영. 지금은 기준측점을 넣으면 생성하는것같음. +185. (원문 · 10-12) 다음으로 이번에 형상을 추가하는 맹암거와 물넘이포장의 경우에 조심해야할 부분은 기존에 보라색선들로 종단측점의 상하나 성토, 절토로 이동을 하고 있다는 점이야. 구조물 선택과 포장 선택에 대해 조심히 반영해야할거야. +186. (원문 · 10-12) 누가토량의 알약의 크기는 적당한데 테두리와 색상을 붉은계동으로 변경해줘. +187. (원문 · 10-12) 다음으로 캐드 프로그램의 기본기능인데 지금 텍스트를 더블클릭하면 브라우져의 창이 나오고 변경할 수 있게 되어있는데 그냥 그자리에서 프롬프트아 바를 보여줘서 바로 텍스트를 바로 입력하게 해줘. +188. (원문 · 10-12 · 그림_188_물넘이연장.png) 1. 도수로와 돌붙임 컨테이너의 삭제버튼 누락됨. 확인 2. 물넘이포장의 횡단도 구조물 선택 및 하이라이트에서 콘크리트 형상은 좌측패널의 경사각도 컨테이너가 활성화 되어야함.(도수로 돌붙임 컨테이너 아님) 2. 물넘이포장이 도로가 원지반에 닫지않는다면 성토부 방향으로 도로의 기울기만금 콘크리트와 노면선을 연장하여야함. (첨부참조) 3. 물넘이포장이 도수로/돌붙임은 성토부측(낮은 방향) 끝단부에 맞춰 그려져야함. 이때 길이의 숫자가 없다면 원지반에 닿으면 끝(성토부 사면이 생성되면 성토사선만큼만 ), 이때 양측절토가 되는 경우 당연히 원지반까지 연장하였으니 물넘이의 경우에는 당연히 닿았으니 안보여야 정상임. 다른 구조물들의 도수로/돌붙임도 동일, 길이를 입력하는 경우 길이만큼만 생성. 4. 앞으로 ㅗ화면검증은 오래걸리니 내가 하겠음. +189. (원문 · 10-12) 도수로/돌붙임 생성의 경우 성토 사면 선택이 잘 안됨. 잘고려하여 잘 선택되게 반영 +190. (원문 · 10-12) 1. 횡단도와 상세 버튼으로 접근하는 모달창에서의 선과 면적을 선택할때는 선택면적이 좁은 순으로 마우스커서로 잡게 하여야 됨. 화면상에 위에 배치되어야 선택이 용이해짐. 2. 횡단도의 상세버튼으로 접근하고 모달창의 횡단도는 초기에 사용자에게 보여줄때 중심점이 센터에 오게 그림을 배치 3. 물넘의 포장의 월류폭에 의해 인접 횡단도에 그림이 그려진다면 흙막이처럼 연결된 상태라는걸 알리기 위해 별도 표기 4. B09의 좌측 패널에서 업데이트 버튼으로 불러온 정보를 업데이트 하게 되는데 이때는 만약 복사해온 정보가 삭제되었다면 업데이트 항목에서 제외 5. 횡단도의 상세버튼 모달창에서 구조물은 선들을 나누는 기준이 될수 선이 될수 있지만 구조물들의 선을 나눌필요는 없음. 6. 나는 이제 쉬러갈거니 너가 다른워크트리들을 잘 제어해서 요청업무에 대해 잘 완료하고 깃 싱크할것. 7. 깃 싱크전 너가 브라우져로 사용자가 된것처럼 제어하고 검증한 결과를 아티팩트로 보여줄것. +191. (원문 · 10-12) 추가 사항 있음. 신규 횡단배수군들 중 배관, 박스암거, 세월교, 물넘이포장의 경우 배수유역을 재분석을 해야할텐데 추가버튼을 누르면 횡단도 생성하고 배수유역도은 몰래 업데이트 되게 했으면 함. 몰래가 힘들면 로딩도 무관함. From 5bc017b07e3cccd9529166bed41ec049ee6229b0 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Tue, 13 Oct 2026 09:02:57 +0900 Subject: [PATCH 06/81] =?UTF-8?q?chore(=EB=8F=99=EA=B8=B0=ED=99=94):=20?= =?UTF-8?q?=EA=B3=B5=EC=9C=A0=20docs=20=EC=9A=94=EA=B5=AC=20=EC=9E=A5?= =?UTF-8?q?=EB=B6=80=20=EB=B0=9B=EA=B8=B0=20=E2=80=94=20=EC=9B=90=EA=B2=A9?= =?UTF-8?q?=EA=B3=BC=20=EA=B0=99=EC=9D=80=20=EB=82=B4=EC=9A=A9?= 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_01YZVseAexp5wNFNVfRtxjkg --- docs/raw/요구_1010_2/사용자요구_1010_2.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/raw/요구_1010_2/사용자요구_1010_2.md b/docs/raw/요구_1010_2/사용자요구_1010_2.md index 76ce578ab..9b187f75a 100644 --- a/docs/raw/요구_1010_2/사용자요구_1010_2.md +++ b/docs/raw/요구_1010_2/사용자요구_1010_2.md @@ -220,3 +220,4 @@ 189. (원문 · 10-12) 도수로/돌붙임 생성의 경우 성토 사면 선택이 잘 안됨. 잘고려하여 잘 선택되게 반영 190. (원문 · 10-12) 1. 횡단도와 상세 버튼으로 접근하는 모달창에서의 선과 면적을 선택할때는 선택면적이 좁은 순으로 마우스커서로 잡게 하여야 됨. 화면상에 위에 배치되어야 선택이 용이해짐. 2. 횡단도의 상세버튼으로 접근하고 모달창의 횡단도는 초기에 사용자에게 보여줄때 중심점이 센터에 오게 그림을 배치 3. 물넘의 포장의 월류폭에 의해 인접 횡단도에 그림이 그려진다면 흙막이처럼 연결된 상태라는걸 알리기 위해 별도 표기 4. B09의 좌측 패널에서 업데이트 버튼으로 불러온 정보를 업데이트 하게 되는데 이때는 만약 복사해온 정보가 삭제되었다면 업데이트 항목에서 제외 5. 횡단도의 상세버튼 모달창에서 구조물은 선들을 나누는 기준이 될수 선이 될수 있지만 구조물들의 선을 나눌필요는 없음. 6. 나는 이제 쉬러갈거니 너가 다른워크트리들을 잘 제어해서 요청업무에 대해 잘 완료하고 깃 싱크할것. 7. 깃 싱크전 너가 브라우져로 사용자가 된것처럼 제어하고 검증한 결과를 아티팩트로 보여줄것. 191. (원문 · 10-12) 추가 사항 있음. 신규 횡단배수군들 중 배관, 박스암거, 세월교, 물넘이포장의 경우 배수유역을 재분석을 해야할텐데 추가버튼을 누르면 횡단도 생성하고 배수유역도은 몰래 업데이트 되게 했으면 함. 몰래가 힘들면 로딩도 무관함. +192. (원문 · 10-13) 1. 물넘이에는 배관이 없는게 맞음. 절토면의 물이내려오면 지대가 낮은 성토부로 콘크리트 노면을 이용하여 배수 2. 배수로의 면적은 수기입력이 맞은 실수임. 길이는 현상구현에만 사용. 면적계산에서는 제외 3. 인쇄미리보기 버튼 및 기능 추가. 초안프로그램 로직으로 draft폴더에 코드 생성. b09 좌측패널 하단에 버튼생성. 신규페이지로 이동하고 b07의 도면과 b08, b09의 수량산출, 원가계산영역으로 구분하여 미리보기함. 이때 구조물 조합에 묶여있는 도면은 도면측으로 전달하여 미리보기. 미리보기는 이전 페이지에서 생성한 결과물을 인쇄 전 결과물을 미리보기위한 페이지로 도면은 pdf로 수량산출과 원가계산은 pdf와 엑셀로 출력 및 다운로드됨. 이때 원가계산의 페이지순서는 실무문서의 3공구 내역서 참조. 순서만 참조 4. b09 행선택시 페이지 이동 후 하이라이트에서 하이라이트 부분만 모달창으로 보여주는 기능 구현. 이 기능은 좌측 패널의 하단에 하이퍼링크 버튼과 팝업 버튼을 스위치하며 선택가능 기본값은 팝업 버튼. 하이퍼링크 버튼 화성화 시 현재처럼 테이블 행을 선택하면 해당페이지로 이동. 팝업버튼 활성화시 테이블 행을 선택시 하이라이트 되는 행 전체와 열제목을 지정한 행주변에 보여줌. 이때 조심할 내용은 모달창의 내용도 선택하면 다시 연결된 모달창을 추가로 보여줘야함. 5. b09의 박스보기 및 호표보기의 섬택은 좌측 패널로 옮기고 공용 세팅화 함. 6. 랩탑 메인프로젝트는 너와 내가 같이 사용하는 프로젝트임. 모르는 구조물이 나오더라도 문제 없음. 7. 상세 검토 후 신규 계획서를 작성하여 수행할고 브라우져 검증을 이번 아티팩트를 만든것처럼 수행 From 5cdf4e840a07bd3317fcec9668e8f8194e4b8bc8 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Tue, 13 Oct 2026 17:30:44 +0900 Subject: [PATCH 07/81] =?UTF-8?q?chore(=EB=8F=99=EA=B8=B0=ED=99=94):=20?= =?UTF-8?q?=EA=B3=B5=EC=9C=A0=20docs=20=EA=B0=99=EC=9D=80=20=EB=82=B4?= =?UTF-8?q?=EC=9A=A9?= 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_0141RHfyDdi6VJY8Yiuch6DX --- docs/raw/요구_1010_2/사용자요구_1010_2.md | 7 ++ docs/raw/요구_1010_2/조사_AN_기기대응.md | 111 ++++++++++++++++++++++ 2 files changed, 118 insertions(+) create mode 100644 docs/raw/요구_1010_2/조사_AN_기기대응.md diff --git a/docs/raw/요구_1010_2/사용자요구_1010_2.md b/docs/raw/요구_1010_2/사용자요구_1010_2.md index 9b187f75a..2066d447b 100644 --- a/docs/raw/요구_1010_2/사용자요구_1010_2.md +++ b/docs/raw/요구_1010_2/사용자요구_1010_2.md @@ -221,3 +221,10 @@ 190. (원문 · 10-12) 1. 횡단도와 상세 버튼으로 접근하는 모달창에서의 선과 면적을 선택할때는 선택면적이 좁은 순으로 마우스커서로 잡게 하여야 됨. 화면상에 위에 배치되어야 선택이 용이해짐. 2. 횡단도의 상세버튼으로 접근하고 모달창의 횡단도는 초기에 사용자에게 보여줄때 중심점이 센터에 오게 그림을 배치 3. 물넘의 포장의 월류폭에 의해 인접 횡단도에 그림이 그려진다면 흙막이처럼 연결된 상태라는걸 알리기 위해 별도 표기 4. B09의 좌측 패널에서 업데이트 버튼으로 불러온 정보를 업데이트 하게 되는데 이때는 만약 복사해온 정보가 삭제되었다면 업데이트 항목에서 제외 5. 횡단도의 상세버튼 모달창에서 구조물은 선들을 나누는 기준이 될수 선이 될수 있지만 구조물들의 선을 나눌필요는 없음. 6. 나는 이제 쉬러갈거니 너가 다른워크트리들을 잘 제어해서 요청업무에 대해 잘 완료하고 깃 싱크할것. 7. 깃 싱크전 너가 브라우져로 사용자가 된것처럼 제어하고 검증한 결과를 아티팩트로 보여줄것. 191. (원문 · 10-12) 추가 사항 있음. 신규 횡단배수군들 중 배관, 박스암거, 세월교, 물넘이포장의 경우 배수유역을 재분석을 해야할텐데 추가버튼을 누르면 횡단도 생성하고 배수유역도은 몰래 업데이트 되게 했으면 함. 몰래가 힘들면 로딩도 무관함. 192. (원문 · 10-13) 1. 물넘이에는 배관이 없는게 맞음. 절토면의 물이내려오면 지대가 낮은 성토부로 콘크리트 노면을 이용하여 배수 2. 배수로의 면적은 수기입력이 맞은 실수임. 길이는 현상구현에만 사용. 면적계산에서는 제외 3. 인쇄미리보기 버튼 및 기능 추가. 초안프로그램 로직으로 draft폴더에 코드 생성. b09 좌측패널 하단에 버튼생성. 신규페이지로 이동하고 b07의 도면과 b08, b09의 수량산출, 원가계산영역으로 구분하여 미리보기함. 이때 구조물 조합에 묶여있는 도면은 도면측으로 전달하여 미리보기. 미리보기는 이전 페이지에서 생성한 결과물을 인쇄 전 결과물을 미리보기위한 페이지로 도면은 pdf로 수량산출과 원가계산은 pdf와 엑셀로 출력 및 다운로드됨. 이때 원가계산의 페이지순서는 실무문서의 3공구 내역서 참조. 순서만 참조 4. b09 행선택시 페이지 이동 후 하이라이트에서 하이라이트 부분만 모달창으로 보여주는 기능 구현. 이 기능은 좌측 패널의 하단에 하이퍼링크 버튼과 팝업 버튼을 스위치하며 선택가능 기본값은 팝업 버튼. 하이퍼링크 버튼 화성화 시 현재처럼 테이블 행을 선택하면 해당페이지로 이동. 팝업버튼 활성화시 테이블 행을 선택시 하이라이트 되는 행 전체와 열제목을 지정한 행주변에 보여줌. 이때 조심할 내용은 모달창의 내용도 선택하면 다시 연결된 모달창을 추가로 보여줘야함. 5. b09의 박스보기 및 호표보기의 섬택은 좌측 패널로 옮기고 공용 세팅화 함. 6. 랩탑 메인프로젝트는 너와 내가 같이 사용하는 프로젝트임. 모르는 구조물이 나오더라도 문제 없음. 7. 상세 검토 후 신규 계획서를 작성하여 수행할고 브라우져 검증을 이번 아티팩트를 만든것처럼 수행 +193. (원문 · 10-13) 다음 검토해야되는 내용은때 사용자가 태블릿과 모바일에서 접근할 때를 대비하는 방법이 있어야 할것같아. 지금은 고려가 안된상태라 문제가 없는데 태블릿과 모바일에서 접근해보면 문제가 되더라고 보통 개발할때 어떻게 하는지 알려줘. 이건에 대해서는 너와 많은대화를 해야할거같아. 코드수정과 계획서작성이 아니라 방향성검토에 집중해줘. 대화를 시작해보자. +194. (원문 · 10-13) 내가 원하는 방향은 어떠한 경우 어떠한 장소에도 업무를 했으면 해. 다시말하면 누구냐가 문제가 아니라 어떤기기로든 동일한 기능을 동작하였으면 해. 한다는거지. 지금 당장 잘안되는게 중요한게 아니라 냐가 느낀점은 모바일이나 테블릿으로 업무하기에는 레이아웃들이 부적합하다는거지. 아이패드냐 갤럭시캬는 중요하지않는건 결국 브라우져 앱인지라 뮨제로 보이지 않음. 기기변경에 대해 어떻게 인식을 할수 있는지와 동일한 기능을 어떻게 해야 사용자가 사용에 불편함이 없냐가 더 중요해. 혹시 인터넷에 조사를 해봐 나와 비슷한 생각을 하는 사람이 있을것같고 도움을 받을수 있는툴이 있을것같아 +195. (원문 · 10-13) 캐가 원하는 방향중에 별도로 특정기기들을 위해 개발하는건 유지보수가 어려워 피하고싶어. 혼재 로직을 최댛ㄴ 그대로 사용하되 특수한 경우에만 적용되게하는게 좋을것같아. 예를들어 나는 데스크탑을 보고 개발했는데 자동으로 다른 기기로 접근했을때도 반영되었으면 좋겠다는 말이비. +196. (원문 · 10-13) 서브 1검증중이야. 추가로 반영해줄 내용이 햄버거 매뉴로 접근할수 있다면 로그인 후 모습에서 제일상단의 항목들이 메뉴 안으로 들어왔으니 상단행을 없애도 될것같은데 그럼 더 넓은 화면을 사용할수 있잖아. 그리고 프로젝트 내부에서도 진행단계의 오버레이메뉴도 햄버거 메뉴로 넣으면 더 좋을것 같아 대싴 구분 가로 선이 있어야 됨. 러떤지 서브1에만 반영해봐 +197. (원문 · 10-13) 지금 검증중인데 상단메뉴행을 프로젝트 내부의 확면에서 햄버거메뉴로 대체하는게 생각보다 좋은것같아. 컴퓨터 브라우져에서도 동작하게 변경하는데 햄버거 메뉴를 좌측으로 이동해줘. 그런데 그렇게 되면 프로젝트 페이지나 대시보드의 제목에 의해 중복될테니까 햄버거 메뉴만큼 들여쓰기를 하면 좋을것같아. +198. (원문 · 10-13) 추가로 햄버거메뉴 폭을 조금 줄여도 될것같아. +199. (원문 · 10-13) 서브 1이 작업하는 동안 우리는 행동지침에 대해 수정하자. 오늘부터 검증 사용자들이 랩탑메인 워크트리로 실행중인 서버에 접속하여 검증중이야. 그래서 나는 랩탑 메인을 임시배포로 지정할거야. 문제가 되는 부분은 내가 깃 싱크해. 라고 요청할 경우인데 이때 브레인이 스크립트를 실행하여 워크트리의 전역을 맞추고 있어. 이걸 랩탑 메인은 제외하고 깃 싱크를 하는거야. 그리고 만약 검증 사용자가 작업하지 않는 시점에 내가 전역 깃 싱크해라고 하면 랩탑까지 포함하여 깃 싱크를 맞추게 하고 싶어. 그럴려면 스크립트와 행동지침을 변경해야할거야. 어디를 변경해야할지 확인해보고 행도지침 수정에는 이력을 남길필요없이 그냥 지침을 수정하면 됨. diff --git a/docs/raw/요구_1010_2/조사_AN_기기대응.md b/docs/raw/요구_1010_2/조사_AN_기기대응.md new file mode 100644 index 000000000..23043d182 --- /dev/null +++ b/docs/raw/요구_1010_2/조사_AN_기기대응.md @@ -0,0 +1,111 @@ +# 조사 AN — 어느 기기든 같은 기능 · 기기 대응 방향 (요구 193~195 · 조사만) + +대상 = 휴대폰 · 태블릿 · PC 어디서든 같은 기능. 사용자 확정 방향 = 기기별 따로 개발 없음 · 페이지는 데스크탑 보고 개발 → 공통 층이 다른 기기에 자동 반영 · 특수한 경우만 따로. 코드 · 계획서 없음 · 인터넷 조사 셋(사람 · 제품 / 기기 바뀜 알아채기 / 손가락 꼴 · 도구) + 코드 훑음(10-13). + +## 1. 한눈 결론 + +- 사용자 방향 = 업계 정석. W3C One Web(2008) · Apple · Microsoft · Google · NN/g 모두 「기능은 같게 · 배치 · 보이는 양 · 입력 방식만 바꿈」. +- 기기 이름으로 가르지 않음 — 아이패드 사파리는 Mac 으로, 고급 안드로이드 태블릿 크롬은 데스크탑으로 자기를 알림. 실행 중 세 가지를 계속 봄: 쓸 수 있는 폭 · 지금 쓰는 입력 · 되는 기능. +- 배치는 폭으로만 바꿈 · 입력(손가락 · 마우스)은 누를 자리 크기 · 동작 번역에만 씀(입력은 수시로 뒤바뀜 → 배치를 흔들면 누르는 도중 화면이 움직임). +- 공통 층 세 곳으로 거의 모든 페이지가 손대지 않고 맞춰짐: ① 공통 CSS(폭 구간 · 손가락이면 누를 자리 크게) ② 입력 번역 파일 하나(길게 누름 → 우클릭 · 두 손가락 → 휠 확대 · 두 번 톡 → 더블클릭 · 끌기) ③ 지도 · 3D 라이브러리에 이미 든 손가락 기능. +- 「데스크탑만 보고 개발」을 지키는 울타리 = 자동 점검(Playwright 기기 흉내로 모든 페이지를 작은 화면 · 터치로 열어 넘침 · 가려짐 · 못 누름 찾음) + 마지막 실기기(iOS 고유 흠). +- 특수한 경우 = CAD · 그림 정밀 고르기(꾹 누르면 십자선 · 돋보기) · 표 칸 편집 화면 자판 · 3D · 큰 지도 무게 · 붙여넣기. +- 피할 것 = 새 부품 체계 통째 도입(Ionic · Material Web · Fluent · Spectrum) · 모바일 전용 화면 · 기기 이름 갈래 · 확대 막기(user-scalable=no) · 멈춘 도구(Hammer.js · what-input · detect-it · Touch Punch). +- http 주소(Tailscale IP) = 「안전하지 않은 주소」 → 클립보드 읽기 · 카메라 · 위치 · 앱 설치(PWA) · Web Share 막힘 = 기기 아닌 주소 탓 기능 차이. 푸는 길 = Tailscale https 인증서(90일 갱신 · 기기 이름이 공개 인증서 기록에 남음). + +## 2. 같은 생각 — 사람 · 원칙 + +| 누구 | 요지 | 출처 | +|---|---|---| +| W3C One Web(2008) | 같은 정보 · 서비스를 기기와 상관없이 · 모양은 같지 않아도 됨 | https://www.w3.org/TR/mobile-bp/ | +| Karen McGrane(2012) | "You don't get to decide which platform or device your customers use to access your content: they do." | https://abookapart.com/products/content-strategy-for-mobile | +| Josh Clark(2012) | 「Mobile = Less」는 미신 · 사용자는 데스크탑에서 되는 일을 다 기대 · 어느 PC 든 터치일 수 있다고 보고 만듦 | https://bradfrost.com/blog/post/bdconf-josh-clark-presents-the-seven-deadly-myths-of-mobile/ | +| Ethan Marcotte(2010) | 기기마다 따로 말고 같은 경험의 여러 면 | https://alistapart.com/article/responsive-web-design/ | +| Jason Grigsby(2016) | 입력은 미리 못 알고 수시로 바뀜 → 어디서나 되는 기본 입력 위에 덧붙임 · 기본 크기는 터치 기준 | https://alistapart.com/article/adapting-to-input/ | +| NN/g(2016) | 기기 간 내용 · 기능 동등 = 좋은 목표 · 화면 요소까지 같을 필요 없음 | https://www.nngroup.com/articles/mobile-first-not-mobile-only/ | +| Patrick Lauke(2020) | pointer · hover 미디어 기능을 잘못 짐작하기 쉬운 점들 | https://css-tricks.com/interaction-media-features-and-their-potential-for-incorrect-assumptions/ | + +## 3. 플랫폼 지침 + +- Apple HIG — 크기가 바뀌어도 기능은 그대로 · 바꿔도 되는 것은 보이는 양뿐 · 배치는 기기 종류 · 방향이 아니라 크기 등급으로. https://developer.apple.com/design/human-interface-guidelines/layout +- iPadOS 26(2025-06) — 창 크기 자유 조절 · 여러 창 → 태블릿도 어떤 폭이든 됨. https://www.apple.com/newsroom/2025/06/ipados-26-introduces-powerful-new-features-that-push-ipad-even-further/ +- Google 창 크기 등급 — 폭 600 · 840 · 1200 · 1600 경계 · 실행 중 바뀜 · 「태블릿 판정용 아님」. https://developer.android.com/develop/adaptive-apps/guides/use-window-size-classes +- Google 표준 배치 — 목록-상세(좁으면 한 칸씩 + 뒤로) · 보조 칸(좁으면 아래 · 바텀시트) · 피드. https://developer.android.com/develop/adaptive-apps/guides/canonical-layouts +- Android 16 · 17 — 큰 화면에서 앱의 방향 고정 · 크기 조절 금지 무시 · Google Play 2027-08 의무. 큰 화면 앱에 마우스 · 키보드(우클릭 · hover · Ctrl+휠 · 단축키)도 요구. https://developer.android.com/about/versions/16/behavior-changes-16 · https://developer.android.com/docs/quality-guidelines/adaptive-app-quality/tier-2 +- Microsoft — 기능은 한 번 만들고 모든 화면 크기에서 돌게 · 경계는 물리 화면이 아니라 앱 창 크기. https://learn.microsoft.com/en-us/windows/apps/design/layout/responsive-design +- web.dev — 입력 방식을 화면 크기로 짐작하지 말고 pointer · hover 미디어 기능으로. https://web.dev/learn/design/interaction + +## 4. 제품 사례 + +- Onshape(CAD) — 「보기 전용이나 기능 일부만 주는 길을 거부」(2015) · 휴대폰에서도 설계 · 핀치 · 여러 손가락 제스처 · 꾹 누르면 십자선 정밀 고르기 · 로그인 8건 중 1건 모바일(2018). 한계: 모바일 브라우저 3D 가 약해 앱으로 감(2018) · 도면은 iOS 에서 이름 바꿈 · 지움 · 내보내기만 · 새 기능이 모바일에 몇 주 늦음. https://develop3d.com/develop3d-blog/onshape-cloud-3d-cad-system-now-available-on-android-phones-and-tablets/ · https://www.onshape.com/en/blog/under-the-hood-cad-for-mobile +- Shapr3D — Mac · iPad · Windows 같은 기능(회사 주장) · iPad 는 펜 · 터치, PC 는 마우스 · 단축키. https://www.shapr3d.com/mac-os +- Excel — 리본이 휴대폰에서 아래 명령 막대로 · 접고 펼침. 웹판은 매크로 불가 → 데스크탑 앱으로 넘김. https://support.microsoft.com/en-us/office/show-or-hide-the-ribbon-in-excel-for-mobile-devices-91175ba0-51fd-4256-93a1-69f5a23f27e6 +- Notion — 데스크탑 「/」 명령을 모바일 「+」 메뉴가 대신. https://www.notion.com/help/workspaces-on-mobile +- 물러난 쪽 — Figma 휴대폰 앱 = 디자인 편집 불가(보기 · 댓글) · AutoCAD 모바일 = 가벼운 편집 · Airtable = 일부 기능 없음. https://help.figma.com/hc/en-us/articles/1500007537281-Guide-to-the-Figma-mobile-app · https://support.airtable.com/docs/differences-between-the-desktop-and-mobile-versions +- 교훈 — 끝까지 지킨 쪽은 손가락 입력을 새로 설계(정밀 고르기 · 회전 잠금 · 펜). + +## 5. 기기 바뀜 알아채기 — 권장 모델 + +- 셸 모듈 하나가 `` 에 지금 상태를 계속 적음: 폭 구간 · 손가락 있음(any-pointer: coarse) · 마지막 입력(pointerType) · 보안 주소 · 화면 자판이 가린 높이. +- 듣는 것: matchMedia change(폭 · 입력 · 배율) · pointerdown · visualViewport resize · screen.orientation change → 바뀔 때만 이벤트 하나. +- 공통 CSS 가 그 표시로 누를 자리를 키우고 hover 때만 보이던 것을 늘 보이게 함 · 판 안 부품은 컨테이너 쿼리(2025-08 모든 브라우저) · 캔버스 · 지도 · 3D 는 자기 ResizeObserver. +- 기능은 쓰기 바로 전에 확인 · 없으면 단추 숨김. +- 함정: 첫 화면 깜빡임(첫 값은 index.html 머리에서) · 폭 경계가 CSS · TS 두 벌이면 어긋남(한 표 + 시험) · 창 폭 ≠ 판 폭(컨테이너 쿼리) · 감지는 틀릴 수 있음 → 사용자 「터치 모드」 스위치로 덮어씀. +- 화면 높이 `100vh` → `dvh` · 노치 → `env(safe-area-inset-*)` · `user-scalable=no` 쓰지 않음(캔버스 · 지도에만 `touch-action: none`). +- 서버 쪽 기기 힌트(Sec-CH-UA-Mobile) = 크롬만 · https 에서만 → 서버는 한 벌만 냄. +- 출처: https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent/pointerType · https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/At-rules/@media/any-pointer · https://web-platform-dx.github.io/web-features-explorer/features/container-queries/ · https://developer.mozilla.org/en-US/docs/Web/Security/Defenses/Secure_Contexts · https://tailscale.com/kb/1153/enabling-https + +## 6. 손가락 · 작은 화면 대응표 (공통 층에서 자동) + +| 데스크탑 | 손가락 · 작은 화면(같은 기능) | 공통 층 자리 | +|---|---|---| +| 위 메뉴 | 좁으면 ☰ 서랍(항목 많음) | 앱 껍데기 | +| 좌측 패널 320 | 접히는 서랍 · 아래에서 올라오는 판 | 공용 레이아웃 | +| 떠 있는 끌기 판 | 비모달 바텀시트(뒤 화면 계속 조작) | 공용 판 | +| 팝업 · 겹친 팝업 | 전체 화면 시트 · 둘째는 같은 시트 안 다음 쪽 + [뒤로] | 공용 모달 관리 | +| 우클릭 메뉴 | 길게 누름 + 늘 보이는 ⋯ 단추(같은 메뉴 정의) | 공용 메뉴 + 입력 번역 | +| 더블클릭 | 두 번 톡 + 고른 뒤 보이는 [편집] 단추 | 명령 등록 | +| 휠 확대 | 핀치 + [+][−] 단추(WCAG 2.5.1) | 공용 캔버스 틀 | +| 단축키 | 같은 명령을 단추 · 명령 목록으로도 | 명령 등록 한 곳 | +| 끌어 놓기 | 손잡이 + [이동] 단추(WCAG 2.5.7) | 공용 목록 | +| 넓은 표 | 머리줄 · 첫 열 고정 + 가로 밀기 · 카드 보기는 특수한 경우만 | 공용 표 | +| 숫자 칸 | 숫자 자판(inputmode decimal · numeric) | 공용 입력 | +| PDF · 엑셀 받기 | 서버 주소 + attachment(아이폰은 blob 이 탭에서 열림) | 공용 내려받기 | + +- 입력 번역 사실 확인: iOS 사파리는 길게 누름에 우클릭 신호를 안 냄(WebKit 213953 · 2026 미해결) · 안드로이드 크롬은 냄(둘 다 오면 하나만) · 터치스크린 핀치는 휠로 안 바뀜(노트북 터치패드 핀치만 Ctrl+휠) → 두 손가락 거리 비율로 같은 꼴 Ctrl+휠을 만들면 지금 확대 코드가 받음 · 지도 · 3D 는 빼고(자체 터치) · 대상에 `touch-action: none` · 두 번 톡 더블클릭은 대개 브라우저가 냄(안 올 때만 대신). +- 누를 자리: WCAG 2.5.8 최소 24px · Apple 44pt · Material 48dp → 손가락이면 공용 단추만 키움. +- CAD 정밀 고르기: 손가락 접촉 크기만큼 고름 반경 · 꾹 누르면 손가락 위 십자선(Onshape) · 돋보기 + 스냅(AutoCAD · ARES) · 펜은 마우스처럼 정밀. +- 출처: https://bugs.webkit.org/show_bug.cgi?id=213953 · https://danburzo.ro/dom-gestures/ · https://www.w3.org/WAI/WCAG22/Understanding/target-size-minimum.html · https://www.nngroup.com/articles/mobile-tables/ · https://bradfrost.github.io/this-is-responsive/patterns.html · https://www.onshape.com/en/resource-center/tech-tips/tech-tip-mastering-gestures-mobile + +## 7. 도구 (2026-10-13 판) + +- 공용 층만 · 페이지 그대로: 브라우저 기본(Pointer Events · touch-action · pointer/hover 미디어 · sticky · inputmode) · maplibre-gl 6.14(핀치 · 끌기 · 기울기 내장) · three.js 0.186 OrbitControls(한 손가락 회전 · 두 손가락 확대 · 이동) · long-press-event 2.5.2(길게 누름 · 지연 500ms 로). +- 공용 부품 속만: Web Awesome 3.14(Shoelace 후계 · 서랍) · Tabulator 6.6(열 우선순위 접기) — 이득 작음. +- 페이지마다 손질: AG Grid · @panzoom/panzoom · interact.js. +- 멈춤: Hammer.js · @use-gesture/vanilla · Touch Punch · what-input · detect-it. +- 새 체계 통째(피함): Ionic 9 · Material Web(관리 중단 예고) · Fluent UI · Spectrum. +- 시험: Playwright 기기 흉내(화면 폭 · 터치 · 이미 쓰는 도구 → resources/tester 자동 점검) · Chrome 기기 모드(근사) · Responsively(무료 · 여러 크기 한 화면) · Polypane(유료) · BrowserStack · TestMu AI(실기기 대여 · iOS 마지막 확인). +- 포장(나중): PWA(https 필요) · Capacitor(같은 웹 코드를 iOS · Android 앱으로 · 내려받기는 플러그인 갈래). + +## 8. 우리 코드 지금 (10-13 훑음) + +- 900 이하 위 메뉴 통째 숨김 · 대신할 단추 없음(`A00_Common/app_shell.ts`). +- 좌측 패널 320 고정 · 860 이하 줄임 · 560 이하 덮개 + 손잡이(`ui_template/ui_template_overlay.css`). +- 휠 확대 14 · 우클릭 24 · 더블클릭 11 파일 · 손가락 확대 0 · pointer 방식 41(손가락 누름 · 끌기 대체로 먹음) · setPointerCapture 19. +- 100vh 27 파일 · B07 CAD `user-scalable=no`. +- http 대비 이미 있음: randomUUID 대체(`A00_Common/random_uuid_polyfill.ts` · openwebcad) · 붙여넣기 메뉴 = 보안 주소에서만(`spreadsheet_menu_items.ts`). +- 지도 maplibre-gl · 3D three.js = 손가락 기능 내장(켜는 설정만 봄). + +## 9. 확인 못 함 + +- iOS 26.1 에서 `-webkit-touch-callout: none` 이 안 먹는다는 보고(답 없음) · WebKit 301558(window 처리기 dblclick) 실린 iOS 판. +- `touch-action: manipulation` 이 dblclick 에 주는 영향 · 핀치를 Ctrl+휠로 바꿔 주는 기성 라이브러리. +- Device Posture · Viewport Segments 의 https 요구 · iPadOS 26 창 크기 바꿀 때 사파리 화면 영역 세부. +- AutoCAD · Shapr3D 현행 앱 동작 · McGrane 「access the internet」 판 문구 · Clark 「mobile users want it all」 문구. + +## 10. 다음 대화 거리 + +- 가장 좁은 기준 폭(휴대폰 세로 약 390 까지 「불편 없이」 vs 휴대폰은 가로 기준). +- https 주소 전환(Tailscale 인증서 · 기기 이름 공개 기록). +- 「터치 모드」 스위치를 사용자에게 보일지(자동 감지 + 손으로 덮어씀). +- 실기기 확인 방법(사용자 기기 · 실기기 대여). From dd0650d8f29497b6e8245f2c672cd259eadd5da4 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Tue, 13 Oct 2026 17:50:16 +0900 Subject: [PATCH 08/81] =?UTF-8?q?chore(=EB=8F=99=EA=B8=B0=ED=99=94):=20?= =?UTF-8?q?=EA=B3=B5=EC=9C=A0=20docs=20=EA=B0=99=EC=9D=80=20=EB=82=B4?= =?UTF-8?q?=EC=9A=A9?= 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_0141RHfyDdi6VJY8Yiuch6DX --- docs/raw/요구_1010_2/사용자요구_1010_2.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/raw/요구_1010_2/사용자요구_1010_2.md b/docs/raw/요구_1010_2/사용자요구_1010_2.md index 2066d447b..bac44066c 100644 --- a/docs/raw/요구_1010_2/사용자요구_1010_2.md +++ b/docs/raw/요구_1010_2/사용자요구_1010_2.md @@ -228,3 +228,4 @@ 197. (원문 · 10-13) 지금 검증중인데 상단메뉴행을 프로젝트 내부의 확면에서 햄버거메뉴로 대체하는게 생각보다 좋은것같아. 컴퓨터 브라우져에서도 동작하게 변경하는데 햄버거 메뉴를 좌측으로 이동해줘. 그런데 그렇게 되면 프로젝트 페이지나 대시보드의 제목에 의해 중복될테니까 햄버거 메뉴만큼 들여쓰기를 하면 좋을것같아. 198. (원문 · 10-13) 추가로 햄버거메뉴 폭을 조금 줄여도 될것같아. 199. (원문 · 10-13) 서브 1이 작업하는 동안 우리는 행동지침에 대해 수정하자. 오늘부터 검증 사용자들이 랩탑메인 워크트리로 실행중인 서버에 접속하여 검증중이야. 그래서 나는 랩탑 메인을 임시배포로 지정할거야. 문제가 되는 부분은 내가 깃 싱크해. 라고 요청할 경우인데 이때 브레인이 스크립트를 실행하여 워크트리의 전역을 맞추고 있어. 이걸 랩탑 메인은 제외하고 깃 싱크를 하는거야. 그리고 만약 검증 사용자가 작업하지 않는 시점에 내가 전역 깃 싱크해라고 하면 랩탑까지 포함하여 깃 싱크를 맞추게 하고 싶어. 그럴려면 스크립트와 행동지침을 변경해야할거야. 어디를 변경해야할지 확인해보고 행도지침 수정에는 이력을 남길필요없이 그냥 지침을 수정하면 됨. +200. (원문 · 10-13) 1. B06 횡단도의 내부의 구조물 그림 수정 및 선택 하이라이트 개선 - 맹암거의 내부 형상에 의해 다른 면적 선택이 안됨. 우선순위 재확인하고 내부 형상이 돌 모양이 교차한 형상으로 변경 - 물넘이 포장과 배관이 인접하여 포장이 배수관 횡단도로 연동되어 넘어간경우 노면의 콘크리트를 선택하면 배관의 노면 콘크리트 선택까진는 좋음. 하지만 배관 형상까지 하이라이트 됨. - 노면공과 물넘이 포장의 경우 종단 측점 세로선, 알약, 좌측패널 횡단배수 측점과 영역을 선택하면 컨크리트 구조물까지 선택됨. 이건 이전에 선택이 어려울때 남은 잔재임. 구조물 형상선택에 따른 좌측 패널 하위컨테이너 활성화는 별개로 볼것. 다른 구조물들과 동일한 로직이 됨. (대신 수정이 복잡하게 엮여있으니 정밀히 분석 후 반영) 2. 깃처리 테스트라는 브랜치는 없음 각 워크트리에는 하나의 브랜치 존재. 일단 sub1까지 일단 전역 싱크하자. sub1이 더 수정했으니 포함하여 싱크 3. 측점이 존재하지 않는 구조물의 경우에는 하이라이트할 횡단도는 없으니 횡단 하이라이트 이동은 안되게(기 요청사항이었음. 로직 확인) From 75730c43f3c5d7d341619c797f7e8d3ac48ca311 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Tue, 13 Oct 2026 18:23:50 +0900 Subject: [PATCH 09/81] =?UTF-8?q?chore(=EB=8F=99=EA=B8=B0=ED=99=94):=20?= =?UTF-8?q?=EA=B3=B5=EC=9C=A0=20docs=20=EA=B0=99=EC=9D=80=20=EB=82=B4?= =?UTF-8?q?=EC=9A=A9?= 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_0141RHfyDdi6VJY8Yiuch6DX --- docs/raw/요구_1010_2/사용자요구_1010_2.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/raw/요구_1010_2/사용자요구_1010_2.md b/docs/raw/요구_1010_2/사용자요구_1010_2.md index bac44066c..f8c8f5a9a 100644 --- a/docs/raw/요구_1010_2/사용자요구_1010_2.md +++ b/docs/raw/요구_1010_2/사용자요구_1010_2.md @@ -229,3 +229,7 @@ 198. (원문 · 10-13) 추가로 햄버거메뉴 폭을 조금 줄여도 될것같아. 199. (원문 · 10-13) 서브 1이 작업하는 동안 우리는 행동지침에 대해 수정하자. 오늘부터 검증 사용자들이 랩탑메인 워크트리로 실행중인 서버에 접속하여 검증중이야. 그래서 나는 랩탑 메인을 임시배포로 지정할거야. 문제가 되는 부분은 내가 깃 싱크해. 라고 요청할 경우인데 이때 브레인이 스크립트를 실행하여 워크트리의 전역을 맞추고 있어. 이걸 랩탑 메인은 제외하고 깃 싱크를 하는거야. 그리고 만약 검증 사용자가 작업하지 않는 시점에 내가 전역 깃 싱크해라고 하면 랩탑까지 포함하여 깃 싱크를 맞추게 하고 싶어. 그럴려면 스크립트와 행동지침을 변경해야할거야. 어디를 변경해야할지 확인해보고 행도지침 수정에는 이력을 남길필요없이 그냥 지침을 수정하면 됨. 200. (원문 · 10-13) 1. B06 횡단도의 내부의 구조물 그림 수정 및 선택 하이라이트 개선 - 맹암거의 내부 형상에 의해 다른 면적 선택이 안됨. 우선순위 재확인하고 내부 형상이 돌 모양이 교차한 형상으로 변경 - 물넘이 포장과 배관이 인접하여 포장이 배수관 횡단도로 연동되어 넘어간경우 노면의 콘크리트를 선택하면 배관의 노면 콘크리트 선택까진는 좋음. 하지만 배관 형상까지 하이라이트 됨. - 노면공과 물넘이 포장의 경우 종단 측점 세로선, 알약, 좌측패널 횡단배수 측점과 영역을 선택하면 컨크리트 구조물까지 선택됨. 이건 이전에 선택이 어려울때 남은 잔재임. 구조물 형상선택에 따른 좌측 패널 하위컨테이너 활성화는 별개로 볼것. 다른 구조물들과 동일한 로직이 됨. (대신 수정이 복잡하게 엮여있으니 정밀히 분석 후 반영) 2. 깃처리 테스트라는 브랜치는 없음 각 워크트리에는 하나의 브랜치 존재. 일단 sub1까지 일단 전역 싱크하자. sub1이 더 수정했으니 포함하여 싱크 3. 측점이 존재하지 않는 구조물의 경우에는 하이라이트할 횡단도는 없으니 횡단 하이라이트 이동은 안되게(기 요청사항이었음. 로직 확인) +201. (원문 · 10-13) 4. B09의 팝업기능으로 연결정보를 보는 복수 모달창에서 esc키를 누르면 전체 모달창이 닫게하고 최종모달창을 기준으로 다른 곳을 누를 경우에도 닫히게 반영 5. 계획서는 내가 지웠음 +202. (원문 · 10-13) 6. B05/B06의 종단그래프 내부 누가토량의 알약과 폰트 색상을 테마에 맞추되 잘보이게 변경하고 알약을 그래프 영역 내부에서 이동이 되게 반영 +203. (원문 · 10-13) 7. b09 페이지의 탭의 상세페이지에서 안내문구 영역이 있음. 영역을 텍스트 3줄정도만 보이게 하고 이후는 스크롤로 처리, 테이블을 더 넓게 사용하도록 조절, 이때 제일 하단행 고정기능이나 행이 적을 경우 합계행이 끌려올라가야하니 조심. 이때 안내문구 영역도 끌려올라가야함. 안내문구 영역은 2줄로 바꿀수도 있으니 참고하여 코드 반영 +204. (원문 · 10-13) 8. B09의 좌측페널 데이터 점검 및 공기 옵션 컨티이너의 기본값은 접힘상태. 9. B07 좌측패널의 평면도, 종단도, 횡단도는 접힘상태가 기본값이며 B07 초기 접근시 보여주는 페이지는 표지로 변경 10. B06 좌측 패널의 횡단보기 설정 기본값 접힘. 11. B05 좌측 패널의 컨테이너 이름을 계획노선 컨테이너의 기본값은 접힘상태로 변경 12. exp_sub1_device 브랜치 삭제 From 1ab59256e357fbed17b7454d605fdd62a63c2173 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Tue, 13 Oct 2026 18:54:46 +0900 Subject: [PATCH 10/81] =?UTF-8?q?chore(=EB=8F=99=EA=B8=B0=ED=99=94):=20?= =?UTF-8?q?=EA=B3=B5=EC=9C=A0=20docs=20=EA=B0=99=EC=9D=80=20=EB=82=B4?= =?UTF-8?q?=EC=9A=A9?= 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_0141RHfyDdi6VJY8Yiuch6DX --- docs/raw/요구_1010_2/사용자요구_1010_2.md | 3 + .../요구_1010_2/조사_AO_구조물목록_빈목록.md | 72 +++++++++++++++++++ 2 files changed, 75 insertions(+) create mode 100644 docs/raw/요구_1010_2/조사_AO_구조물목록_빈목록.md diff --git a/docs/raw/요구_1010_2/사용자요구_1010_2.md b/docs/raw/요구_1010_2/사용자요구_1010_2.md index f8c8f5a9a..ec8abb2aa 100644 --- a/docs/raw/요구_1010_2/사용자요구_1010_2.md +++ b/docs/raw/요구_1010_2/사용자요구_1010_2.md @@ -233,3 +233,6 @@ 202. (원문 · 10-13) 6. B05/B06의 종단그래프 내부 누가토량의 알약과 폰트 색상을 테마에 맞추되 잘보이게 변경하고 알약을 그래프 영역 내부에서 이동이 되게 반영 203. (원문 · 10-13) 7. b09 페이지의 탭의 상세페이지에서 안내문구 영역이 있음. 영역을 텍스트 3줄정도만 보이게 하고 이후는 스크롤로 처리, 테이블을 더 넓게 사용하도록 조절, 이때 제일 하단행 고정기능이나 행이 적을 경우 합계행이 끌려올라가야하니 조심. 이때 안내문구 영역도 끌려올라가야함. 안내문구 영역은 2줄로 바꿀수도 있으니 참고하여 코드 반영 204. (원문 · 10-13) 8. B09의 좌측페널 데이터 점검 및 공기 옵션 컨티이너의 기본값은 접힘상태. 9. B07 좌측패널의 평면도, 종단도, 횡단도는 접힘상태가 기본값이며 B07 초기 접근시 보여주는 페이지는 표지로 변경 10. B06 좌측 패널의 횡단보기 설정 기본값 접힘. 11. B05 좌측 패널의 컨테이너 이름을 계획노선 컨테이너의 기본값은 접힘상태로 변경 12. exp_sub1_device 브랜치 삭제 +205. (원문 · 10-13) 다음은 B05와 B06의 좌측 공용 패널의 내용 중 하단의 구조물 리스트에서 구조물을 하나씩 삭제하면서 리프레시 해봤는데 하나만 남을때까지는 문제없어 보이는데 마지막을 구조물을 삭제한 직후 구조물 리스트가 초기화 되고 리프레시 하면 마지막 하나가 남아있는 상황이야. 확인해줘. 뭔가 저장에 대한 제어가 문제가 있는것같아. 주고 자동생성된 횡단배수 구조물로 봐서는 배수유역에 대한 자동계산이나 영구저장소 문제로 판단됨. +206. (원문 · 10-13) 1. 조사하는 동안 구조물리스트 제일 하단에 구조물 전체삭제 버튼을 넣어줘. 하나도 없으면 당연히 숨김처리되어야함. 2. 직전 깃 요청이 깃 싱크였어. 전역 깃 싱크였어? +207. (원문 · 10-13) 구조물 전체 삭제 버튼이 상단 구조물 버튼의 높이와 다름. 변경하고 다시 전역 깃 싱크 diff --git a/docs/raw/요구_1010_2/조사_AO_구조물목록_빈목록.md b/docs/raw/요구_1010_2/조사_AO_구조물목록_빈목록.md new file mode 100644 index 000000000..f5ca82d1a --- /dev/null +++ b/docs/raw/요구_1010_2/조사_AO_구조물목록_빈목록.md @@ -0,0 +1,72 @@ +# 312 분석 — B05 · B06 좌측 구조물 목록: 마지막 하나 지움이 새로고침 뒤 되살아남 + +sub5 · 조사만 · 코드 고치지 않음 · 2026-10-13 + +## 한 줄 결론 + +관(자동 생성 횡단배수 = `pipe_points.json`) 목록에서 **「빈 목록」이 세 군데에서 「값 없음」으로 읽힘** — 화면 초안 읽기 · 서버 계산 · 서버 저장. 그래서 마지막 관을 지우면 화면은 자동 배치로 다시 차고(「초기화」), 새로고침 · [저장] 뒤에는 저장돼 있던 마지막 하나가 되살아남. 손으로 놓은 구조물(`structures.json`)은 빈 목록을 바르게 다룸 — 흠 없음. + +## 재현 + +방법 — 검증_sub5(`bec7fe41-…`) storage **사본**(임시 폴더) + DB 읽기 전용 세션으로 서버 API 를 화면 차례대로 부름(기존 `test_b05_b06_save_roundtrip.py` 와 같은 틀). 화면(ORCA)은 띄우지 않음 — 화면 쪽은 코드 길을 따라 확인. + +시작 상태 — 저장된 관 11개(전부 자동 배치 `spacing`·`stream`) · 구조물 0개. + +| 단계 | 화면에서 하는 일 | 서버 요청 | 결과 | +|---|---|---|---| +| P1 | 하나씩 지우고 [저장] → 2개 | PUT pipe-points [2개] | 저장 2 · 파일 2 ✔ | +| P2 | 하나 더 지우고 [저장] → 1개 | PUT [1개] | 저장 1 · 파일 1 ✔ | +| P3 | **마지막 하나 지움** | POST detail-basins `[]` (화면 다시 나눔) | **응답 관 11개 = 자동 배치** → 목록 「초기화」 | +| P4 | 새로고침([저장] 안 함) | 초안 `[]` 를 「없음」으로 봄 → GET pipe-points | **1개(85.05) 되살아남** | +| P5 | [저장] | PUT `[]` | **저장 1 · 파일 1 그대로** (빈 목록 안 써짐) | +| P6 | (파일이 `points: []` 라면) | GET | **자동 배치 11개 · saved=false** | +| S1~S3 | 손으로 놓은 구조물 1개 → 지우고 [저장] → 새로고침 | PUT structures `[]` → GET | 0개 ✔ (정상) | + +화면 길별 결과(코드 추적): + +| 화면 | [저장] 안 함 · 새로고침 | [저장] 함 | +|---|---|---| +| B05 (배수유역도 · 좌측 목록) | 지운 직후 자동 11개로 참(P3) · 새로고침 → 저장분 1개(P4) | 관 초안 `[]` PUT → 서버가 저장분 1개 유지(P5) · 이어서 `side.savePipes()` 가 **화면 목록(자동 11개)을 PUT** → 자동 11개가 정본에 저장됨 | +| B06 (구조물 목록) | B06 목록은 `??` 라 빈 목록 유지 · 그러나 초안 쓰기 뒤 `side.reload()` → 배수유역도가 저장분 1개를 다시 읽어 B05 쪽 목록 · 종단 알약에 1개 | 관 초안 `[]` PUT → 1개 유지 · `side.savePipes()` 가 배수유역도 목록(1개) PUT → 1개 되살아남 | + +## 원인 — 「빈 목록 = 없음」 자리 + +1. **화면 초안 읽기** — `B05_Profile/B05_Profile_UI_Drainage_Panel.ts:424` `return pending?.length ? computeDetailBasins(...) : fetchDetailPipePoints(...)` — 초안 `[]` 를 「초안 없음」으로 보고 저장분을 읽음. 같은 초안을 B06 은 `readPendingPipes(projectId) ?? 저장분`(`B06_Section_UI_Page_Structures_Panel.ts:388`)으로 읽어 두 보기가 갈림. +2. **서버 관 목록 고르기** — `B04_PreProcess/B04_PreProcess_Router_Basins.py:373` `points = requested or stored or None` — 요청 `[]` 는 저장분으로, 저장분 `[]` 는 `None`(자동 배치)으로 넘어감. PUT 도 이 줄을 거쳐 **빈 목록이 절대 저장되지 않음**(P5). +3. **서버 계산** — 같은 파일 `:70` `chainages = [...] if points else None` · `common_util/common_util_drainage_detail.py:189` `if confirmed_chainages:` — 빈 확정 목록을 「기본 관 + 간격 보충(자동 배치)」으로 바꿈(P3 · P6). 사용자가 말한 「배수유역 자동 계산이 다시 돎」이 이 자리. +4. 덤 — `A00_Common/design/design_save.ts` 의 관 차례는 초안 내보내기 뒤 `side.savePipes()`(`design_shared.ts:301`)로 **배수유역도 화면 목록을 한 번 더 PUT** — 1~3 때문에 화면 목록이 자동 11개 · 저장분 1개로 바뀐 뒤라 그 값이 정본이 됨. + +정상인 자리(빈 목록을 바르게 다룸): 세션 캐시 `readState`(`[]` 그대로) · `flushPendingPipes`(`if (!pending)` — `[]` 도 보냄) · 구조물 PUT · GET(`structures.json` `[]` 저장 · 읽기) · B05 계획선 · 횡단 측점의 관 읽기(`B05_Profile_Engine_Sections.py:175` — `[]` 그대로). `initial_snapshot` · [초기화](DELETE pipe-points)는 이 흐름과 무관. + +곁 흠(같은 꼴 · 표시만) — `B06_Section/B06_Section_Section_Store.ts:81` `if (!drafts || !drafts.length)` — 구조물 초안 `[]` 를 「초안 없음」으로 봐 저장 전 횡단도에 저장분 벽(C군)이 남음. [저장] 뒤에는 정본이 `[]` 라 풀림. + +## 고칠 자리 제안 (한 벌 · ③ 서버 단독 + 화면 한 줄) + +뜻 정함 — **`None`(파일 없음 · 노선 바뀜 · 요청에 칸 없음) = 자동 배치 · `[]` = 사용자가 다 지움(관 0개)**. 자동 배치로 되돌리기는 [초기화](DELETE)만. + +1. 서버 `B04_PreProcess_Router_Basins.py` `_resolve` — 요청 본문에 `points` 칸이 있으면(빈 목록 포함) 그 값 · 없으면 `None`. `points = requested if requested is not None else stored`. 응답 `saved` 판정(`bool(stored) and not requested`)도 `is None` 으로. +2. 서버 같은 파일 `_build:70 · :74` — `if points is not None` 로. +3. 서버 `common_util_drainage_detail.py:189` `build_detail` — `if confirmed_chainages is not None:` (빈 목록이면 관 0 · 유역 0 — 아래 `if not pipes: return detail` 가 이미 받음). +4. 화면 `B05_Profile_UI_Drainage_Panel.ts:424` — `pending !== null` 로(B06 `??` 와 같은 규칙). +5. (선택) 곁 흠 `B06_Section_Section_Store.ts:81` — 「`[]` 도 초안 없음」 주석(2026-09-09)이 막던 결함(저장분 벽을 통째로 지움)을 다시 보고 `drafts === null` 로 바꿀지 판단 필요 — 그 결함의 까닭이 「빈 초안」이 아니라 다른 곳이었는지 먼저 확인. + +짝 파일 — 없음(관 목록 고르기는 서버 단독 · 화면은 한 줄). `load_pipe_points` 는 이미 `[]` 와 `None` 을 가름 — 고칠 것 없음. + +영향: +- B04 배수유역 편집(`B04_PreProcess_UI_Basins.ts:334` 다시 나눔 · `:479` 저장)도 관을 다 지우면 이제 0개로 남음(지금은 자동 배치로 다시 참) — 같은 뜻으로 맞춰짐. +- 관 0개 정본 → B05 계획선 정착 · 구조물 측점 · B06 암거 횡단 · B08 수량에서 관이 빠짐 — 사용자가 다 지운 뜻 그대로. +- 문서 줄 고침 — `computeDetailBasins` 머리 주석(`B04_PreProcess_Api_Fetch.ts:586~588`) · `post_detail_basins` 독스트링 「points 를 비우면 자동 배치」. + +시험 거리(`resources/tester/test_312_empty_pipes.py` 가칭 · 위 재현과 같은 사본 틀): +- PUT `[]` → 응답 0 · 파일 `points: []` · GET 0 · `saved=true`. +- POST detail-basins `[]` → 관 0 · 유역 0. +- 파일 없음 → GET 자동 배치(지금대로) · DELETE 뒤 자동 배치(지금대로). +- 노선 지문 바뀌고 좌표 없는 저장분 → 자동 배치(지금대로 · `None` 길). +- 화면 소스 — `loadSaved` 가 `pending !== null` 로 고르는지(기존 `test_298_add_reanalyze.py` 문자열 시험 그대로 통과). + +## 되돌림 확인 + +- 실제 프로젝트 파일 손 안 댐 — 재현은 임시 사본에서 · 끝나고 사본 지움. +- `pipe_points.json` sha1 `503f60d6…` · `structures.json` sha1 `a12ee824…` — 재현 앞뒤 같음. +- DB — 읽기 전용 세션(`tx_read_only=1`) · 하류 무효화 함수는 가짜로 갈음 · 쓰기 없음. +- 화면 탭 · 서버 재시작 없음 · git 작업 트리 깨끗(커밋 없음). From 837fe3d64a509257f9f35108db3516750fbb3ff4 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 15 Oct 2026 11:43:40 +0900 Subject: [PATCH 11/81] =?UTF-8?q?chore(=EC=9A=94=EA=B5=AC):=20224=20?= =?UTF-8?q?=EC=9B=90=EB=AC=B8=20=E2=80=94=20B05=C2=B7B06=20=EC=9C=A0?= =?UTF-8?q?=ED=86=A0=EA=B3=A1=EC=84=A0=C2=B7=EB=B0=B0=EC=88=98=EC=9C=A0?= =?UTF-8?q?=EC=97=AD=20=EC=B0=BD=EB=B6=84=EB=A6=AC=20=C2=B7=20=EA=B3=84?= =?UTF-8?q?=ED=9A=8D=EB=85=B8=EC=84=A0=20=EC=B8=A1=EC=A0=90=20=EC=8A=A4?= =?UTF-8?q?=EB=83=85=20=C2=B7=20=EC=B6=94=EC=B2=9C=20=EB=85=B8=EC=84=A0=20?= =?UTF-8?q?=EC=98=81=EA=B5=AC=EC=A0=80=EC=9E=A5=20=C2=B7=20=ED=81=B0=20R?= =?UTF-8?q?=20=EC=9A=B0=EC=84=A0=20=C2=B7=20M02=20=EC=99=B8=EB=B6=80=20?= =?UTF-8?q?=EC=9D=BC=EC=9C=84=EB=8C=80=EA=B0=80=20=EC=97=91=EC=85=80?= =?UTF-8?q?=EA=BC=B4=20=EB=AA=A8=EB=8B=AC=20=C2=B7=20=EA=B3=84=EC=82=B0?= =?UTF-8?q?=EC=8B=9D=C2=B7=EC=B6=9C=EC=B2=98=20=EB=A7=A4=EC=B9=AD=20=C2=B7?= =?UTF-8?q?=20CAD=20=EC=97=B0=EC=86=8D=20=EB=AA=85=EB=A0=B9=20=C2=B7=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=EB=AA=A8=EB=8B=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01C6BNBFXFndbqYxu6sZuknK --- docs/raw/요구_1010_2/사용자요구_1010_2.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/raw/요구_1010_2/사용자요구_1010_2.md b/docs/raw/요구_1010_2/사용자요구_1010_2.md index 1f52bbcf1..a3e1691fb 100644 --- a/docs/raw/요구_1010_2/사용자요구_1010_2.md +++ b/docs/raw/요구_1010_2/사용자요구_1010_2.md @@ -264,3 +264,13 @@ - 단위가 상이한 경우에는 우리의 단가산출 로직에 단위변환에 따른 결과 로직을 부여하여 맞출것 - 신규계획서를 작성이 필요하며 작성전 상세분석을 진행하여 코드의 영향을 분석을 진행. 6. 4번 전역 깃 싱크를 필히 한후 추가 검토 및 반영 업무 진행할 것. +224. (원문 · 10-15) 다름으로 이제 프로그램 개발 작업으로 돌아가자. +1. b05 b06의 공통 기능 중 하단 패널 분리가 있음. 여기에서 창분리를 추가로 하였으면 함. 종단 패널 전체에는 내부에 테이블, 유토곡선, 배수유역 이 보조 패널로 존재함. 이 중 유토곡선과 배수유역을 분리할 수 있게 반영. 버튼은 종단 그래프 좌측 상단의 버튼 양식 참고 +2. b05의 계획노선 편집에서 계획노선 제안이나 기본 노선 사용자 제어기능 중 호의 사이에 존재하는 측점은 호의 중심에 존재해야한다함. 일종의 스냅기능이 필요함. 계획노선 편잡창에 측점 스냅 버튼을 추가하여 활성화 비활성화 할수 있게 반영. +3. 계획노선의 추천기능은 현재 서버에서 1분여간 걔산하여 보여줌. 이기능은 시점과 종점이 변경되거나 옵션값이 변경되지 않는다면 변경되지 않음. 사용자 편의성을 위해 미리 영구저장소에 값을저장하여 보여주고 옵션이나 시종점이 바뀐경우에만 신규추가할것. 기존 기본값들은 항상보관. +4. 계획노선의 결국 초기값은 예정노선을 따라가는 커브임. 이때 추천 선택지애 따라 연산한 결과들을 보여주고 적용할텐데 이때 추천노선 전체의 r값은 최소 12m 가 아니라 클수록 평면 계획노선이 좋음. 무조건 좋다라기 보다는 결국 현재노선 로직에서 추가로직으로 정리하는 방향이 좋을 수도 있음. +5. m02의 외부문서의 일위대가를 가져온 기능에 초기에 모달창으로 해당정보를 엑셀에서 열은것처럼 모달창에서 보여주고 사용자가 호표를 선택하면 영역을 하이라이트 하거나 한번에 불러오는 기능이 있으면 좋겠음. +6. m02의 외부문서에사 일위대가를 가져올 경우 이름이 많이 다름. 다른 외부문서에서도 동일함. 그러니 이름을 보고 매칭하는 기능도 좋지만 연결된 단가산출 탭의 계산식과 출처들을 참조한다면 정합성을 높일수 있을것같음. +7. 내장 캐드프로그램에는 오토캐드 프로그램을 참고하여 만들었음. 기능 중 명령어 중에 연속 명령어로 제어할 수 있는 기능이 필요. 예를들어서 산 단축키 입력> 각도지정> 길이입력 등임. 연속적으로 키보드로 작동하는 기능임. 이건 인터넷 조사하여 오토캐드에서 어떤식으로 제어기능을 제공하는지 분석필요함. +8. 우리 프로그램에는 마우스로 팬, 줌인아웃, 선택 기능들이 사용자가 제어할 수 있는 기능이 있음. 일체반영을 위해 햄버거 메뉴 안에 설정버튼을 추가하고 별도모달창에는 탭으로 구분하여 마우스 제어방법이나 오토캐드의 단축키, 다른 곳에서의 단축키 설정, 나중에는 페이지들 기본 옵션값들을 사용자가 조절할 수 있는 기능을 부여하려고 함. (기본값은 지금은 아님. 내가 별도 요청) +9. 신규 계획서를 작성하기 전 현재의 코드나 외부정보를 먼저 정밀 분석하고 진행. 신규계획서를 작성 후에는 업무를 분배하여 진행하고 너는 최종 완료시점에 사용자처럼 브라우져 제어하고 보고서를 아티팩트로 공유. From b5b6264016f1d8b48c4d209b327fa95e86448983 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 15 Oct 2026 12:12:08 +0900 Subject: [PATCH 12/81] =?UTF-8?q?chore(=EC=A1=B0=EC=82=AC):=20=EC=9A=94?= =?UTF-8?q?=EA=B5=AC=20224=20=EC=A0=95=EB=B0=80=20=EB=B6=84=EC=84=9D=20?= =?UTF-8?q?=E2=80=94=20325=20=EC=9C=A0=ED=86=A0=EA=B3=A1=EC=84=A0=C2=B7?= =?UTF-8?q?=EB=B0=B0=EC=88=98=EC=9C=A0=EC=97=AD=20=EC=B0=BD=EB=B6=84?= =?UTF-8?q?=EB=A6=AC=20=C2=B7=20326=20=EC=B8=A1=EC=A0=90=20=EC=8A=A4?= =?UTF-8?q?=EB=83=85=20=C2=B7=20327-328=20=EC=B6=94=EC=B2=9C=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=C2=B7=ED=81=B0=20R=20=C2=B7=20329-330=20=EC=99=B8?= =?UTF-8?q?=EB=B6=80=20=EC=9D=BC=EC=9C=84=EB=8C=80=EA=B0=80=20=EB=B3=B4?= =?UTF-8?q?=EA=B8=B0=20=EC=B0=BD=C2=B7=EA=B7=BC=EA=B1=B0=20=EB=A7=9E?= =?UTF-8?q?=EC=B6=A4=20=C2=B7=20331=20CAD=20=EC=97=B0=EC=86=8D=20=EB=AA=85?= =?UTF-8?q?=EB=A0=B9(=EC=98=A4=ED=86=A0=EC=BA=90=EB=93=9C=20=EC=A1=B0?= =?UTF-8?q?=EC=82=AC)=20=C2=B7=20332=20=EC=84=A4=EC=A0=95=20=EB=AA=A8?= =?UTF-8?q?=EB=8B=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01C6BNBFXFndbqYxu6sZuknK --- ...2026-10-15_325_유토곡선_배수유역_창분리.md | 47 +++++++++++++ docs/raw/조사/2026-10-15_326_측점스냅.md | 35 ++++++++++ .../2026-10-15_327-328_추천노선_저장_큰R.md | 53 +++++++++++++++ ...15_329-330_외부일위대가_보기창_근거맞춤.md | 52 +++++++++++++++ docs/raw/조사/2026-10-15_331_CAD연속명령.md | 66 +++++++++++++++++++ docs/raw/조사/2026-10-15_332_설정모달.md | 64 ++++++++++++++++++ 6 files changed, 317 insertions(+) create mode 100644 docs/raw/조사/2026-10-15_325_유토곡선_배수유역_창분리.md create mode 100644 docs/raw/조사/2026-10-15_326_측점스냅.md create mode 100644 docs/raw/조사/2026-10-15_327-328_추천노선_저장_큰R.md create mode 100644 docs/raw/조사/2026-10-15_329-330_외부일위대가_보기창_근거맞춤.md create mode 100644 docs/raw/조사/2026-10-15_331_CAD연속명령.md create mode 100644 docs/raw/조사/2026-10-15_332_설정모달.md diff --git a/docs/raw/조사/2026-10-15_325_유토곡선_배수유역_창분리.md b/docs/raw/조사/2026-10-15_325_유토곡선_배수유역_창분리.md new file mode 100644 index 000000000..0a5ed5811 --- /dev/null +++ b/docs/raw/조사/2026-10-15_325_유토곡선_배수유역_창분리.md @@ -0,0 +1,47 @@ +# 325 유토곡선 · 배수유역 창분리 (요구 224-1) + +기준 = 837fe3d64. B05 · B06 종단도는 한 벌(`B05_Profile_UI_Profile_Panel.ts:30` · `B06_Section_UI_Section_View_Profile.ts:277` → `sharedProfile`) — 한 번 고치면 두 보기에 같이. + +## 지금 — 종단 [창 분할] + +- 단추: `A00_Common/design/design_shared.ts:153,158` 빈 button → `toolLead` · `:181` `attachProfilePopout`(`design_profile_popout.ts:25-29`) · 글 「창 분할」/「창 결합」 · 툴팁 `title` · 클래스 `ui-btn ui-btn--ghost b05-route-profile__popout`. +- 열기: `window.open("", "aislo-profile-popout", "popup,width=1400,height=720")`(`:52`) · 막히면 토스트(`:54`) · 스타일 · html 속성 · body 배경 열 때 한 번 베낌(`:61-70`) · 종단 뿌리 DOM 을 새 창 body 로 옮김(`:71-73`). +- 동기화: 따로 없음 — 코드 · 가게 · sessionStorage 는 메인 것 · 같은 함수가 다른 창 DOM 을 그림(BroadcastChannel · postMessage 안 씀) → 고름 · 커서 · 확대 · 스크롤 저절로 맞물림. +- 이벤트: 새 창 keydown/keyup/pointerdown/pointerup/pointercancel · blur 를 메인 document 로 다시 쏨(`:14,76-90`) · 새 창 ResizeObserver → `profile.redraw`(`:93-96`). +- 되돌림: 새 창 pagehide → `slot.append` + 다시 그림(`:31-38,97`) · 메인 pagehide · 페이지 떠남(`design_page.ts:263` → `profile.dispose`) → 새 창 닫고 되돌림(`:39-49`). +- 화면 틀 · 시계 = 요소가 놓인 창 것(`common_util_view_window.ts:11-18` · 230-1/2) · 창 자리 · 크기 기억 없음(매번 1400×720) · 테마는 열 때만 베낌 · CSS `design_page.css:43-56`. + +## 종단 안 보조 판 셋 + +| 판 | 만드는 곳 | 그리는 곳 | 접기 · 크기 | +|---|---|---|---| +| 표 | `design_profile_overlays.ts:61-63` · `_Profile_TableOverlay.ts` | `_Profile_Render.ts:173-199,409-411` | 손잡이 `table-open` · `table-height` | +| 유토곡선 | overlays `:48-59` → `B05_Profile_UI_Profile_MassHaul.ts:190`(손잡이 200-204 · 판 208-249) | `_Profile_Render.ts:415-458` → `MassHaul.draw :360-489` | `masshaul-open`(:220,264-279) · 높이 `masshaul-height`(:224-242) · 범례 `masshaul-visible`(:281-286) | +| 배수유역 | `design_profile.ts:181-199` `drainageSide`(`_Profile_SideTool.ts:11`) → `B05_Profile_UI_Drainage_Side.ts:26` → `_Drainage_Panel.ts:79` · 뼈대 `_Drainage_Chrome.ts:54-149` | 자체 캔버스 `_Drainage_Panel.ts:235-316` | `drainage-collapsed`(:556-567) · 폭 `drainage-width`(Chrome :139-147) | + +- 배치: 뿌리 = [「종단」 손잡이][높이 끌기][content](`design_profile.ts:207`) · content = [bodyWrap][배수 판](`:198-199`) · 유토 · 표 판은 bodyWrap 바닥에 absolute 로 겹침(`_Style_MassHaul.css:54-66,168-180`) · 높이 나눔 `_Profile_Heights.ts:66-179` · 유토는 종단 본문 크기 0 이면 건너뜀(`_Profile_Render.ts:125`). + +## 종단 그래프 좌측 상단 단추 줄 + +- DOM: `.b05-route-profile__balance`(28px · `_Style.css:533-547`) 맨 앞 `span.b05-route-profile__tools` 안 [창 분할] + `button.b05-route-profile__tool` 8개(초기화 ↶ ↷ 직선화 자유지정 쉬프트 ▲ ▼). +- 만드는 코드: `tools.render()`(`_Profile_Tools.ts:161-232` · 단추 함수 69-80) → `withToolLead`(`design_profile.ts:296-299`) → `renderBalanceBar`(`_Profile_Balance.ts:48-51`) · 그릴 때마다 새로. +- 양식: 글자 단추 · `title` 툴팁 · `--btn-bar-padding` · `--btn-bar-font-size`(`ui_template_theme.css:169-170` · 높이 16 · 글 12) · [창 분할] = ghost + `design_page.css:53-56`. + +## 뗄 때 손볼 곳 + +1. 높이 나눔 · 끌기가 「`overlay.hidden` 거짓 = 열림」만 봄(`_Profile_Heights.ts:70` · `_Profile_Drag.ts:146,172,194`) → 떼어도 메인이 자리 비움 · 높이 끌기 놓으면 새 창 높이가 저장 높이로 굳음 → 「열림 && 뿌리 안」. +2. 유토 손잡이가 메인에 남음 → 뗀 동안 숨김. +3. 「같이 줄이기」 인라인 높이(`_Profile_Heights.ts:147`)가 새 창에 남음 → 뗄 때 비움. +4. 유토 세로 범위가 메인 보이는 구간을 따름(`_Profile_Render.ts:441,453-458`) → 새 창이 넓으면 잘림 → 뗀 동안 `viewRange` 안 씀(`MassHaul.ts:441`). +5. 배수 지도 크기 관찰 = 메인 창 ResizeObserver(`_Drainage_Parts.ts:468`) → 놓인 창 것으로 바꿔 끼우는 `rehome`(직전 크기 지켜 배율 유지). +6. 방향키 ←→ 는 마지막 누름이 `.b05-route-profile` 안일 때만(`B06_Section_UI_Cross_Panel_Base.ts:92`) → 선택자에 두 판 클래스. +7. 종단 새 창 안에서 다시 떼면 메인 `window.open` 이 팝업 차단될 수 있음 → 단추가 놓인 창으로 엶. +8. 창 이름은 조각마다 달리(`aislo-masshaul-popout` · `aislo-drainage-popout`) — 같으면 앞 창 body 를 덮어 조각이 사라짐. +9. 한계: 메인 종단 패널 접으면 뗀 유토 멈춤 · 유토 창으로 메인을 다 가리면 메인 화면 틀이 멈춰 늦음 · 토스트 · 좌측 폼은 메인 창. + +700줄: `design_profile.ts` 700(시험 고정) · `design_view_profile.ts` 700 → 손대지 않음 · `_Drainage_Panel.ts` 700 → 한 줄 더하면 주석 합쳐 700. 다른 224 일감과 겹치는 파일 없음. + +## 시험 + +- 있음: `test_282_1_profile_toolbar.py`(+ helper · 가짜 require 가 `showToast` 만 → popout 이 `viewOf` 를 가져오면 helper 도 고치거나 `button.ownerDocument?.defaultView ?? window`) · `test_187_1_b05_head_button_size.py`(`.b05-route-profile__tools > .b05-route-profile__popout {` 블록 정규식 → 새 단추 규칙은 따로) · `test_230_1_popout_wheel.py` · `test_230_2_popout_frames.py`(`POPOUT_FILES` 메인 창 rAF/setTimeout 금지) · `test_219_1_arrow_owner.py` · `test_design_profile_single.py` · `test_b05_drainage_side.py`(배수 파일은 `UI_Profile_` · `B06_Section` import 금지 · ≤700) · `test_281_1_drainage_zoom_range.py`. +- 새로: `test_224_1a_drainage_rehome.py`(+ cjs) · `test_224_1b_masshaul_docked.py`(+ cjs) · `test_224_1c_subpanel_popout.py`(+ cjs · 단추 자리 · 글 · 클래스 · 창 이름 셋 다름 · 열림/닫힘/dispose 제자리 · 손잡이 숨김/보임). diff --git a/docs/raw/조사/2026-10-15_326_측점스냅.md b/docs/raw/조사/2026-10-15_326_측점스냅.md new file mode 100644 index 000000000..633ba2d57 --- /dev/null +++ b/docs/raw/조사/2026-10-15_326_측점스냅.md @@ -0,0 +1,35 @@ +# 326 계획노선 편집 — 측점 스냅 (요구 224-2) + +기준 = 837fe3d64. + +## 해석 + +- 「호의 사이」 = BC~EC 누가거리 구간(서버는 곡선표 BC · EC 를 노선에 내려 구함 `B05_Profile/B05_Profile_Engine_Sections_Core.py:241-275` · 5 m 밖 곡선 버림 `:221`). +- 「호의 중심」 = 원 중심(노선 밖)이 아니라 곡선 중점 MC(IP 에서 내각 2등분선으로 외할장 SL · 누가거리로 BC~EC 한가운데 · `resources/knowledge/original/산림과임업기술(임도)/2. 임도/5. 설계.md:191`). +- 근거: 곡선부 BC · MC · EC 보조측점(`…/4. 노선측량.md:144`) · 「측점간의 번호는 20m이내에서 조정」(`:143`) · 교재 예시 표(`5. 설계.md:182-191`). +- 후보: (가) 호 안 20 m 측점을 MC 로 옮김 — 채택 · (나) 편집 손잡이를 IP 대신 MC 에(「측점」 말과 안 맞음) · (다) 노선을 움직여 MC 를 격자에(곡선 수십 곳 동시 불가). +- 실제 어긋남(저장소 자료 셈): 1078 m · 곡선 26 · R 12~199 → 호 안 측점 14곳 · MC 와 최대 11.3 m(호 616.1~646.5 · MC 631.3 에 620 · 640 둘) · 1153 m · 곡선 50(R 12 · CL ≤ 5.6) → 호 안 11곳(2.7 m 안) · 측점 없는 호 39곳. 측점 = 노선 모양과 상관없는 20 m 격자라 어떤 조작이든 같음. + +## 지금 + +- 편집 창: `A00_Common/design/design_view_profile.ts:278-286` → `openRouteEditModal`(`B05_Profile/B05_Profile_UI_RouteEdit.ts:104-110`) · 단추 줄 `_RouteEdit_Chrome.ts:51-78`([노선 추천] | 분석 지형 · [1m 등고선] · [거리 재기] | 되돌리기 · 다시하기 · 초기화 · 취소 · 확인) · 켬끔 본보기 = [1m 등고선](`aria-pressed` + `is-active` `:63-65` · 배선 `_RouteEdit_Contour.ts:136-148`). +- 조작: 노드 끌기 `_RouteEdit_Pointer.ts:265-294` · 곡선 손잡이 BC · EC `:241-263` + `_RouteEdit_Curve.ts:89-109` · R · 곡선 길이 · 고정 `_RouteEdit_CurveBar.ts:61-116` · 두 번 = 노드 더함 · 오른쪽 = 지움(`RouteEdit.ts:494-512`) · 측점 눈금 누름 = 횡단 미리보기(`_Pointer.ts:186-201`) · 추천 [적용] `_RouteEdit_Initial.ts:132-160` → `RouteEdit.ts:428-434` · 기본 노선 `_ensure_planned_initial`(`B05_Profile_Router_Replan.py:339-379`) · [초기화] `_Initial.ts:164-197` · `Router_Replan.py:282-297`. +- 계산: 편집 중 TS 짝 `buildEditedPolyline`(`_Curve.ts:226-327`) · [확인] 서버 `replan`(`Router_Replan.py:580-669`) 전 체인. +- 곡선 자료: apex · radius_m · tangent_m · inner_angle_deg · start(BC) · end(EC)(서버 `common_util/common_util_route_polyline.py:104-141` · TS `_Curve.ts:32-42`) · 원 중심 셈만(`:130-152`) · MC 셈 코드 없음. +- 측점 정본 `generate_sections`: 20 m 격자 `_chainages`(`_Sections_Core.py:86-92,333`) · 관 · 구조물 0.005 m 안이면 격자에 얹음(`:338-357`) · 종류 bp/ep/irregular/regular(`:411-424`) · 곡선표는 확폭에만(`:368-385`) · BC · MC · EC 측점 안 심음 · 부르는 곳 둘 = `run_section_generation`(`B05_Profile_Engine_Sections.py:392-438`) · `sample_structure_sections`(`:513-551`) · 곡선표 읽기 `_load_design_curves`(`:71-96`). +- 편집 창 눈금: `drawStationTicks`(`B04_PreProcess_UI_MapOverlays.ts:225-260`) ← `_RouteEdit_Render.ts:461-482` · 같은 격자 따로 셈 = 누름 `_Input.ts:368-407` · 기울기 라벨 `_Slope.ts:54-60` · 종단 띠 `_ProfileStrip.ts:26-60` · 횡단 미리보기 서버 = 가장 가까운 격자(`Router_Terrain.py:172-184`). +- 측점 한 벌(272): `plantedChainages`(`common_util_structure_station.ts:235-242`) ↔ `resolve_extra_stations`(`Engine_Sections.py:250-275`) · `oneStationList`(`ts:254-274`) ↔ `_merge_irregular_into_longitudinal`(`Router_Confirm.py:295-343`) · `followMovedNodes`(`ts:276-298`) ↔ `_follow_moved_stations`(`Router_Confirm.py:204-292`) · 거울 시험 `test_272_1_station_list.py` · 정수 m = 구조물 · 관만 floor(x+0.5)(`common_util_structure_station.py:38-45` · `.ts:51-60`). + +## 구현 자리 (짝 ① · 조작 중 TS · [확인] 서버 정본) + +0. 쪼개기: `Router_Replan.py` 698줄 → 요청 모델 다섯(`:72-138`)을 `B05_Profile_Schema.py` 로(Replan 에서 다시 import · `replan.RouteInitialRequest` 그대로). +1. 짝 셈: `_Sections_Core.py` `snap_curve_stations(stations, spans)`(호 안 BC+0.005 < s < EC−0.005 측점 중 MC 에 가장 가까운 하나 → floor(MC+0.5) · 같은 m 하나로) · `generate_sections(..., station_snap=False)`(spans 셈을 앞으로 · 격자 붙임은 스냅 뒤 목록) ↔ `_RouteEdit_Frame.ts`(77줄 · Sections_Core 짝 · `snapCurveStations` · `curveSpans` · `stationsAlong` · import 없이). +2. 저장: 곡선표 `planned_route_curves.json` 에 `"station_snap"`([확인] 때만 · 작업본 · 스냅샷이 route 폴더째 복사 `common_util_initial_snapshot.py:39`) · 없으면 끔 · DB 옵션에 안 둠(`SectionGenerationOptions` 다시 짓는 곳 여섯) · `Router_Replan.py` 요청 칸 → `_write_planned_polyline`(`:300-336`) · GET `/route/plan` 돌려줌(`:558-577`) · 실패 되돌림(`:607-639`)에 `_curves.json` · `_nodes.csv` 도(기존 틈) · `Engine_Sections.py` `load_station_snap` → `run_section_generation` · `sample_structure_sections` 둘 다(한쪽만이면 [저장] 때 640 되살아남) · `Router_Terrain.py` 횡단 미리보기 `station_snap` + `design_curves=built.curves`. +3. 편집 창: `_RouteEdit_Chrome.ts` [측점 스냅](1m 등고선 양식 · [거리 재기] 옆) · `RouteEdit.ts`(635줄) 상태 = 메모리 모드(되돌리기 밖) · 열 때 GET 값 · `stations()` 한 곳 → 그리기 · 누름 · 기울기 · 횡단 요청 · [확인] `changed` + `station_snap` · 함께 `_RouteEdit_Render.ts` · `B04_PreProcess_UI_MapOverlays.ts`(`chainages` 선택 칸) · `_Input.ts`(`stationAtScreen` 선택 칸 · 라벨 판정 값으로) · `_Pointer.ts` · `_Slope.ts` · `_Apply.ts` · `B05_Profile_Api_Replan.ts` · 선택 칸은 뒤에(B04 지도 · 배수유역도 · `helper_b05_routeedit_pick.cjs` 그대로). + +영향: B05 종단 · 3D · B06 · B07(`Router_Support_Basin.py:280-290`) · B08 = 서버 측점 목록 따라감 · 옮긴 측점의 B06 사용자 입력은 누가거리 정확 맞춤 이월이라 안 넘어감(`B03_FileInput_Service_Chain.py:574-586`) · 추천 비교값 · 종단 띠 균형 종단 = 20 m 격자 그대로 · B04 지도 · 배수유역도 눈금 격자 그대로(`_Drainage_Panel.ts` 700줄) · `common_util_route_polyline.py`(745줄) 손대지 않음. + +## 시험 + +- 새로: `test_224_2_station_snap.py`(tsc 컴파일해 파이썬과 견줌 · 곡선 없음 · 측점 없는 호 · 하나 · 둘 · BC 위 · .5 올림 · 이웃 격자 같은 m · `generate_sections` 끔 = 지금과 같음 · 켬 = 640 없음 · 631 있음 · 631 에 관이면 `grid`). +- 있음: `test_74_route_confirm_initial` · `test_route_initial` · `test_64_1_profile_toggle` · `test_b05_routeedit_frame_mirror` · `test_b05_routeedit_pick` · `test_b05_routeedit_chrome_modal` · `test_b05_routeedit_cross_width` · `test_272_1_station_list` · `test_272_2_station_sync` · `test_238_1_save_keeps_structure_stations` · `test_273_2_preview_stations` · `test_b05_extra_stations_supplier` · `test_regeneration_extra_stations` · `test_b05_station_roundtrip` · `test_b06_curve_widening` · `test_75_profile_strip`. diff --git a/docs/raw/조사/2026-10-15_327-328_추천노선_저장_큰R.md b/docs/raw/조사/2026-10-15_327-328_추천노선_저장_큰R.md new file mode 100644 index 000000000..86771733b --- /dev/null +++ b/docs/raw/조사/2026-10-15_327-328_추천노선_저장_큰R.md @@ -0,0 +1,53 @@ +# 327 · 328 계획노선 추천 — 영구 저장 · 큰 R (요구 224-3 · 224-4) + +기준 = 837fe3d64. 경로는 저장소 뿌리 기준. + +## 지금 — 추천 계산 길 + +- 화면: 편집 창 [노선 추천] → 모달 `B05_Profile/B05_Profile_UI_RouteEdit_Initial.ts:132-162` · 알약 다섯(예상노선 추적 · 영선 · 토공 최소 · 기울기 최소 · 비중 · `_Recommend.ts:66-74`) · 열면 지금 갈래 한 번 셈 `:552-554` · 알약 = `run() :423-480` · 서버 갈래 여덟 중 safe · crossings · cost 숨김(`_Initial_Model.ts:439`) · [적용] = 편집값 + sessionStorage `routeedit-initial`(`_Initial.ts:151-159` · `_Initial_Model.ts:561-564`) · 서버 안 씀 · 편집 창 [확인] → `POST /route/replan`(`_RouteEdit_Apply.ts:86-101`) → 작업본 `planned_route.csv` · `_nodes.csv` · `_curves.json` + `B05_Profile/route/initial_choice.json`(갈래 이름) + initial_snapshot 다시 뜸(`B05_Profile_Router_Replan.py:607-663`). +- API: `POST /api/projects/{id}/route/initial`(`B05_Profile_Router_RouteInitial.py:27-126` · 화면 대기 5분 `B05_Profile_Api_RouteInitial.ts:15`) · 입력 = 예상노선 `B05_Profile/route/expected_route.csv`(없으면 initial_snapshot) `:59-61` · 최소 R `_plan_criteria :67` · 종단 상한 `:68-70` · 제약 칸(기울기 · 최소 R · 계류 이격 100 · 지반 고름 5 · 영선 목표) `:72-79` · 지형(확정 지표면 + 분석 지형) `:80-83` → 비용면 캐시 `B05_Profile_Engine_Solver.py:214-257` · 계류 `도엽_하천중심선.geojson` 소하천 이상(`Engine_RouteInitial.py:87-137`) · 관 지점 · mode · weights(50/25/25) · with_profile · design_speed. +- 차례(`generate_initial_route` `Engine_RouteInitial.py:212-350`): ① 예상노선 추적 = 전처리 초기값 규칙(0.4 초) ② 나머지 = 후보 묶음(입력 지문 `_pool_key :361-372` · 메모리 4벌 `_POOLS :108-110`) — 격자 탐색(예상노선 ±100 m 복도 · 4 m 칸 · 16방향 Dijkstra · `Search.py:236-301`) · 비용(이격 · 토공 · 기울기² · 영선 · 위험 · 계류 · 길이 · 꺾임 R·θ) · 후보(목표 7 × 상한 3단 + 쓸기 11 · `Pool.py:50-213`) · `_trace Pool.py:99-114` → `route_metrics Metrics.py:115-293` · 균형 종단(후보 전부 대충 → 고른 하나 바로 · ±60 m 횡단 · 계획고 ±6 m · B06 `compute_cross_design` · `Profile.py:66-402`). +- 1분 까닭: 같은 입력 첫 셈 35~80 초 · 같은 묶음 다른 갈래 3~5 초 · 묶음 짜기 1.9~2.7 초 · 대부분 = 후보 전부 균형 종단(63-4 때 1.8 → 18.3 초) · 메모리만이라 재시작 · 리로드 · 입력 바뀜마다 다시. +- 저장 없음: 남는 것 = 메모리 `_POOLS` · sessionStorage 한 벌 · `initial_choice.json`. 머리글 「파일 안 씀」 = `Engine_RouteInitial.py:6` · `Router_RouteInitial.py:33` · `Router_Replan.py:29` · `Profile.py:4-5`. + +## 327 저장 설계 + +- 키: 시점 · 종점만으론 부족(추천은 예상노선 전체 씀) · 기존 `_pool_key`(지형 · 예상노선 · 계류 · 상한 · 최소 R · 이격 · 고름 · 목표 기울기 내용 해시) + mode · weights · with_profile · design_speed · `ALGO_VERSION`(코드 판 · 선례 `B04_PreProcess_Router_Watershed.py:82-87,227-242` `RESPONSE_SCHEMA_VERSION`) · 관 지점은 뺌([확인] 때마다 다시 냄 · 덮개 수는 꺼낼 때 다시 셈 `Profile.py:432-451`). + +| 자리 | 판단 | +|---|---| +| `B05_Profile/route_recommend/<키>.json` | 씀 — initial_snapshot 대상(`common_util_initial_snapshot.py:38-43`) 밖이라 [초기화](`:154-159,260-274`)에 안 휩쓸림 · 자료 교체(`common_util_project_reset.py:23-54`) · 프로젝트 삭제 때 같이 지워짐 | +| `B05_Profile/route/` | 안 됨 — [초기화]가 되돌림 | +| initial_snapshot | 안 됨 — 쓰는 때 정해짐 · [확인]마다 통째 지움(`Router_RouteInitial.py:284` · `B03_FileInput_Service_Chain.py:449`) | +| DB 표 | 안 씀 — 마이그레이션 · 정리 목록까지 | + +- 층 = 「서버 계산 캐시」(데이터 3층 밖 · 선례 비용면 npz `Solver.py:169-257` · 코리도 사전 생성 · 배수유역 응답) · 사용자 편집값 아님 → 자동저장 금지와 안 부딪힘. +- 꼴: 키마다 파일 하나 `{version, key, pool, default, created_at, request, result}` · 있으면 셈 안 함 · 없으면 셈해 더함(덮어쓰기 없음) · `default` = 빈 제약 칸 + 50/25/25 + 설계 지형 · 이 표시는 안 지움 · 한 벌 10~30 KB. +- 무효화: 지형 · 예상노선 · 계류 · 설정 바뀜 = 다른 키(옛 파일 남김) · 코드 바뀜 = `ALGO_VERSION` 올림 · 자료 교체 = 폴더째. +- 미리 셈: 체인 끝(`Service_Chain.py:225-227` 예상노선 · `:343-351` 스냅샷 뒤)에서 예약만(`_schedule_background_task` `B03_FileInput_Router_Helpers.py:279-299` · 체인이 [초기화] 요청 안에서도 불림 `B05_Profile_Router_Lifecycle.py:246`) · 다섯 알약 ≈ CPU 1~1.5 분 · 진행 중 같은 키 = 기다림. + +## 328 큰 R + +- R 정하는 자리: `common_util/common_util_route_polyline.py:670-717`(접선 자리 = 앞뒤 직선 짧은 쪽 절반 `:673` · 기본 최소 `:682` · 못박은 R `:683-685` · R 맞춤 `_fit_radius_m :358-391,686-697` = 최소 이상에서 예정노선과 평균 거리 최소 · 단순화 갈래에서만 · 같은 방향 꺾임 묶기 `:449-576`) · 추천은 예상노선 추적만 맞춤 · 나머지 `_trace` = 편집 갈래 → R = 최소 → **추천 곡선 전부 12 m**(자리 모자라면 더 작게 + 위반 `:698-716`). +- 12 m 출처: 설계속도 20 · 특수지형 `config/config_system_design.py:521-525` ← `B05_Profile_Engine_Grade.py:142-151` ← `Router_Replan.py:188-226`. +- 제약: 이웃 접선 겹침(절반씩) · 탐색 경로 이탈(외할 E = R(sec(IA/2)−1)) · 종단경사 · 계류(맞춤 뒤 다시 잼 · 위반 늘면 안 됨) · 토공(R 크면 확폭 줄어 토공 줄음 `Profile.py:127-151`) · 곡선 길이 하한(법정 없음 · 표시 5 m `config_system_design.py:548-557`). +- 방안 A(1단 · 「현재 노선 로직」 재사용): `_trace` 에서 탐색 경로를 예정노선처럼 폴리라인화 `build_planned_polyline(densify_route(nodes, 2 m), min_radius_m)` → 같은 단순화 · 묶음 · R 맞춤 · 최소 R 판보다 위반 많으면 최소 R 판 · 응답 꼴 같아 화면 `flattenServerPlan`(`_RouteEdit_Edits.ts:232-272`) 그대로 · +5~12 초. +- 방안 B(2단 · 「클수록」): A 곡선마다 R 을 [맞춘 R, 접선 자리 ÷ tan(IA/2)] 안에서 탐색 경로와 최대 거리 ≤ 허용 이탈인 가장 큰 값 → 못박아(`simplify=False · radii`) 다시 짬(브라우저 짝 있음) · +1~3 초 · 허용 이탈 값은 사용자 확정. +- 방안 C(탐색 자체 큰 R): 층 수 ∝ R · 1.5~2.5배 느림 · 안 씀. +- 붙일 곳 `_trace` 한 곳 → 영선 · 토공 최소 · 기울기 최소 · 비중(+숨긴 셋) · 예상노선 추적은 그대로(`test_route_initial.py:102` · `Router_Replan.py:300-336` · 09-07 확정 「예정노선에 가장 가까운 R」). +- 손대지 않음: `common_util_route_polyline.py`(745줄 · TS 짝) · `Router_Replan.py`(698줄). 설명 정정: `B05_Profile_UI_RouteEdit_Recommend_Help.ts:32` 추적 「최소 R」 → 「맞춘 R」. + +## 지식DB 근거 (값은 안 정함) + +- 법정 최소곡선반지름(별표2 Ⅰ.2.다): 설계속도 40 → 60/40 · 30 → 30/20 · 20 → 15/12(일반/특수) · 중심선 기준 · 내각 155° 이상 곡선 생략 · 배향곡선 10 m — `resources/knowledge/original/법률/산림자원의 조성 및 관리에 관한 법률 시행규칙/별표/별표2_…(제5조제1항 관련).md:151-165`(산불진화임도 `:436-450`). +- 정리본 `resources/knowledge/technical_info/01_임도/02_상세설계/평면선형.md:17-72`(작업임도 규정 없음 · 확폭 · 45 m 이상 확폭 불요 · 곡선반지름 확보 우선). +- 「곡선반지름은 … 가급적 큰 것이 좋음」 — `original/임도기술교본/1. 교본의 개요/3. 용어해설.md:95` · 확보 우선 `3. 임도 계획과 설계/3. 실시설계.md:229,273`. +- 산식 · 표 `original/산림과임업기술(임도)/2. 임도/2. 임도구조.md:65-106`(R = ℓ²/4B · 20 m 목재 · 노폭 4 m → 25 m · R = V²/127(i+f) · 20 km/h 14~16 · 30 → 31~35 · 40 → 55~63) · 곡선부 종단 G − 80/R > S `:221-226` · K = R/I ≥ 3.0 `:235-248` · 예각 내각은 R 크게 못 둠 `:250`. +- 25 m 이하 = 25톤 대형차 곤란 · 구조개량 대상 — `original/행정규칙/임도설치 및 관리 등에 관한 규정/별표/별표3_임도구조 개량사업(제17조 관련).md:25,55,77`. +- 곡선 요소 식(TL · CL · SL · M · 접선 겹침) — `technical_info/01_임도/03_계산정보/곡선설치_계산.md:19-34`. + +## 시험 + +- 추천: `test_route_initial.py` · `test_63_4_route_profile.py` · `test_77_recommend_widening.py` · `test_78_recommend_mass.py` · 화면 `test_b05_routeedit_initial.py`(+ cjs) · `test_b05_routeedit_initial_roundtrip.py`(+ cjs). +- R: `test_route_polyline*.py`(curve_fit · edit · apex_roundtrip · idempotent · handle_drag) · `test_user_values_survive.py` · `test_plan_curve_limits.py` · `test_b05_curve_radius_floor.py` · `test_initial_snapshot.py`. +- 새로: `test_224_3_recommend_store.py`(같은 입력 같은 키 · 하나만 바뀌어도 다른 키 · 두 번째 셈 0회 · 덮어쓰기 없음 · 스냅샷 복원 뒤 남음) · `test_224_4_recommend_radius.py`(R ≥ 최소 · 하나 이상 큼 · 위반 ≤ 최소 R 판 · 추적 그대로 · (B) 이탈 ≤ 허용). diff --git a/docs/raw/조사/2026-10-15_329-330_외부일위대가_보기창_근거맞춤.md b/docs/raw/조사/2026-10-15_329-330_외부일위대가_보기창_근거맞춤.md new file mode 100644 index 000000000..7bc52d850 --- /dev/null +++ b/docs/raw/조사/2026-10-15_329-330_외부일위대가_보기창_근거맞춤.md @@ -0,0 +1,52 @@ +# 329 · 330 외부 일위대가 — 엑셀 보기 창 · 단가산출 근거 맞춤 (요구 224-5 · 224-6) + +기준 = 9edaec82e(324 끝). M02 = `M02_MasterTemplete/`. + +## 지금 + +- 올리기: `Hopyo_Side.ts:38-72` → Router `:201-216` → `_upload :174-179` → `Excel.read_book` → `Store.save :72-87` — 읽은 결과만 `실무자료/HP-######.json`(원본 엑셀 안 남김). +- 본 화면: `Hopyo_View.ts:132-186` 호표 표 → `:207-268` 줄 표(11열) · 모달 = `choose() :359-394`(후보 5 + 다른 로직 찾기) 하나. +- 불러오기 길 둘 · 둘 다 호표 하나씩: ① build `:414-425` → `POST hopyo-build`(Router `:291-302` → `Build.py:191-204`) ② [로직 추가] `Labor_Detail.ts:496-512` → `Hopyo_Pick.ts:18-58`. +- 서버가 읽는 것: `Excel.py:69-97` 「일위대가」·「일위대가총괄표」 값만(xls formatting_info 없음 · xlsx read_only) → 호표 `{번, 이름, 규격, 단위, 금액}` · 줄 `{차례, 이름, 규격, 수량, 단위, 단가, 금액, 비고, 참조, 갈래}` · 검산. 격자 · 병합 · 열 폭 · 서식 · 행 번호 · 단가산출 시트 없음. +- xlrd `formatting_info=True`(북부청 .xls 0.05 초): 병합 11 · 열 폭 15열 · 행 높이 · 숫자 형식 `#,##0` · 가로 맞춤 · 테두리 · 굵게/채움 없음. xlsx 병합 · 폭은 openpyxl 비 read_only(무거움) → xlsx 는 값만. +- 크기: 북부청 일위대가 560행×15열(≈129 KB) · 단가산출 4,345행×6열(≈192 KB) · 거창 xlsx 단가산출 146열. 호표 행 범위 깔끔(제3호표 = 20–24행 · 68개 안 겹침). + +## 329 엑셀 보기 창 — 부품 + +| 후보 | 근거 | 판단 | +|---|---|---| +| A00 산출근거 스프레드시트 `createSpreadsheet(host, book, {readOnly, tabs, onSelect})` | `A00_Common/spreadsheet/spreadsheet.ts:56-115` · Workbook `spreadsheet_types.ts:165-242`(병합 · 열 폭 · 행 높이 · 서식 · 틀고정 · 여러 시트) · M02 모달에서 읽기 전용 사용 중 `AreaCellPick.ts:49-118` | **씀** — 엑셀 모양 · 보이는 칸만 그림 · 늦게 불러옴 · 손잡이에 `select(범위)` 하나 더함(안쪽 `select :250-266` · `selectSpan` `spreadsheet_selection.ts:89` 있음) | +| B08·B09 자료 표 `ui_template_data_table.ts` | focusRow · rowClass | 병합 · 열 글자 없음 | +| 집계표 `createSheet` | 양식 문서용 | 안 맞음 | + +## 330 근거 맞춤 — 자료 + +- 우리: 로직 1,469(돈 1,438) · 칸 = 키 · 원문번호 · 구분(산림품셈 · 건설품셈 공통/토목/건축/기계설비/유지관리) · 출처(「건설품셈 공통 3-2-1」) · 호표 줄 `{인력/재료/기계/로직, 요소 LB…, 단위, 수량식, 비목}` · 조항 묶음 `M02_Labor_Inputs.py:68-85` sources() · 인력 LB 378 · 기계 EQ 654. +- 엑셀: 일위대가 줄 비고 「단산N참조」 → 단가산출 B열 머리 「N.이름[단위]」(북부청 268/268 · 사방 139/139 이어짐) · 블록 안 출처 글(「산림사업표준품셈 9-14…」 「[건설표준품셈(3-1-3)]」 · 오타 「건설표줌품셈」) · 직종 줄(「보통인부 : 172,068 × 0.1인」 · 글자 사이 빈칸) · 출처 있는 블록 북부청 90/95 · 사방 24/67 · 직종 있는 블록 69/95 · 48/67. +- 지금 점수(`Match.py:297-325`): 두 글자 겹침 + 이름 같음 1 + 단위 0.15 + 산림 0.05 + 자재 규격 0.5 · 거름 `:307` 때문에 이름이 아주 다르면 후보에도 못 듦 · `_targets :215-256` 에 조항 · 직종 없음 · 북부청(기억 없음) 단가산출 268 = 같음 63 · 후보 200 · 없음 5. + +| 단산 | 엑셀 이름 · 근거 | 지금 1순위 | 근거 1순위 | +|---|---|---|---| +| 93 | 구조물터파기(인력100%)(토사) · 건설 3-2-1 · 특별인부 | 구조물터파기 육상토사 GF000101 | GC000083 굴착(인력/토사) | +| 21·22·23 | 레미콘타설 · 건설 6-1-1 · 콘크리트공+보통인부 | 펌프카 타설 | GC000198 레디믹스트콘크리트 타설 | +| 64 | 비탈면보호공(평떼) · 건설 4-1-1 · 조경공+보통인부 | 비탈면 보양 | GC000143 잔디붙임 | +| 49 | 흙깍기(토사)(굴삭기1.0㎥) · 건설 3-2-3 | 없음(깍/깎) | GC000085 흙깎기(기계) 보통토사 | + +함정: ① 건설품셈 번호에 부문 없음(잔토처리 3-1-3 ↔ 우리 유지관리 철골재 철거) → 공통 · 토목 덤 · 직종 · 낱말 하나도 안 겹치면 버림 ② 품셈 판 다름(산림 5-23-3 ↔ 우리 5-22-3) → 조항 같음은 덤 점수만 · 자동 확정 안 씀 ③ 보통인부는 겹침에서 뺌 ④ 직종 없는 기계 로직은 부문 덤이 가름. + +## 위험 · 지킬 것 + +- 줄 수: Match 530 · View 485 · Router 312 · Excel 269 · Build 204 · Api 190 · Side 128 · Pick 58 · spreadsheet.ts 363 · `Labor_Detail.ts` 693(시험 고정 · 손대지 않음 · 덧줄은 Hopyo_Pick). +- 원본 읽기만: `resources/master_data/*.json` · `일위대가조합.json`(시험 master_hash). +- 길 충돌: `/hopyo/{key}/{no:int}`(Router `:228`) → 격자 길은 `/hopyo-grid/{key}`. +- `Match.py:464` 요약 `h[k]` → 옛 파일 KeyError → `.get`. +- 격자는 따로 파일 `실무자료/격자/<키>.json`(HP json 에 넣으면 목록 · 고르기가 통째로 읽음) · 지우기 때 같이. +- 이미 올린 파일 = 격자 · 근거 없음 → 다시 올려야 보임. +- `A00 spreadsheet.ts` 공용(고쳐도 됨) · 계약 `spreadsheet_types.ts` 안 고침. +- 기대값 바뀜: `test_m02_hopyo_match.py:106-108` 제3호표 구조물터파기 1순위 → 굴착(인력/토사)(의도). +- STmate 꼴(「일위대가표」·「단가산출근거」 · 실무 xlsx 6개)은 지금 「시트 없음」 400 — 이번 밖. + +## 시험 + +- 있음: `test_m02_hopyo_excel.py` · `_match.py` · `_build.py` · `_screen.py`(+ `helper_m02_hopyo_pick.cjs`) · `spreadsheet/test_spreadsheet_d_input.py`(+ `helper_spreadsheet_d.cjs`). +- 새로: `test_m02_hopyo_grid.py`(병합 11 · 15열 · B20 제3호표 · `#,##0` · 행 범위 68 안 겹침 · [20,24] · 200/404 · 지우면 격자도 · 역할 · master_hash) · `test_m02_hopyo_basis.py`(블록 북부청 95 · 사방 67 · 단산93 → GC000083 · 21 → GC000198 · 64 → GC000143 위 둘 · 49 → GC000085 · 잔토처리에 철골재 철거 없음 · 근거 없는 줄 그대로 · master_hash). diff --git a/docs/raw/조사/2026-10-15_331_CAD연속명령.md b/docs/raw/조사/2026-10-15_331_CAD연속명령.md new file mode 100644 index 000000000..4fff5cbdd --- /dev/null +++ b/docs/raw/조사/2026-10-15_331_CAD연속명령.md @@ -0,0 +1,66 @@ +# 331 내장 CAD 키보드 연속 명령 (요구 224-7) + +기준 = 837fe3d64. 경로 `B07_DesignDetail/openwebcad/` 아래 `src/…`. + +## 지금 — 우리 코드 + +- 이미 됨: `L␣` → 첫 점 → `@100<30⏎`(한 줄 극좌표) · 마우스 방향 + `100⏎`(직접 거리). +- 안 됨: `<30⏎` → `100⏎`(각도 고정 뒤 길이 · `<30` 읽는 곳 없음 · 버려짐) · 이동 · 복사 · 회전은 클릭만. +- 버그: 시퀀스 도구(폴리선 등) 첫 점에 숫자 · `@x,y` → 기준점 없어 오류 → 「Error in tool actor」 · 도구 멈춤(`sequence-tool.ts:183-189,284-285` · `get-point-from-event.ts:26-28,42-44` · `state.ts:309-317`). + +| 길 | 자리 | +|---|---| +| 키 | `src/inputController/input-controller.ts:92` keydown → `handleKeyStroke :340-448` · 글자 대문자로 `this.text` 에 쌓음 `:441-447` | +| 커서 옆 칸 | `draw :127-142` → `input-field-draw.ts:37-86`(안내 · 절대 x,y · 친 글 · 후보 6) | +| 명령줄 | `components/CommandLine.tsx:17-39`(↑↓ 이력 · Tab 완성 `:78-81`) · 기본 숨김 `components/Toolbar.tsx:23` | +| 단추 | `ribbon/Ribbon.tsx:172` · `components/QuickAccessBar.tsx:33` → `commands/run-command.ts:26-53` · 손가락 `input-touch.ts:122-140` | +| 이름 · 별칭 | `commands/commands.{draw,modify,annotate,layer,utility,insert,view,file}.ts` 153개(별칭 82) → `commands/registry.ts:27-57` · 해석 `:116-120` = 이름 → 별칭 → 접두사 첫 후보 | +| 상태 기계 | xstate 5.18.1 · 원본 기계 20(선 `tools/line-tool.ts:50-110`) + 우리 공장 `tools/factories/sequence-tool.ts:161-359`(약 100 명령) | +| 프롬프트 | `meta.instructions` → `state.ts:297-308` → 커서 칸 · `CommandLine.tsx:87` | + +`handleEnterKey :474-599` 차례: 빈 Enter(대기면 마지막 명령 반복 `:483-486`) → TEXT_INPUT 단계면 글 그대로 `:498-508` → 명령 이름 `:509` → 숫자 `:516` → `x,y :531` → `@x,y :548` → `@d`) · 키보드 좌표 · 거리 시험 0건. + +규모: src 228 파일 ≈27,800 줄 · 700 넘음 `App.css` 1,102 · 가까움 `screenCanvas.drawController.ts` 691 · `state.ts` 647 · `MeasurementEntity.ts` 622 · `input-controller.ts` 620. 원본 OpenWebCAD(MIT · 77ab85739 들여옴 · 원본과 연결 없음 → 원본 파일 고쳐도 됨). + +## 조각 (코드 조사 제안) + +1. 각도 고정 `<각도` → 길이 — 새 `src/inputController/input-angle-lock.ts`(고정 저장 · `lockPoint(base, p)` · 안내) · `input-patterns.ts` `ANGLE_LOCK_REGEXP` · `input-controller.ts` `:509` 앞 분기 · draw 안내 줄 · Esc 풂 · `get-point-from-event.ts` 고정 중 미리보기 · 클릭 · 숫자를 고정 방향으로 · 한 점 쓰면 풂 · `line-tool.ts:134` · `sequence-tool.ts:179` 미리보기 · `run-command.ts:40` 새 명령이면 풂 — 확인 = `resources/tester/cad/keyboard-chain.test.ts`(`0,0` · `<30` · `100` → (86.603, 50) · `<90` · `50` → (86.603, 100) · 고정 없이 `100` = 마우스 방향). +2. 시퀀스 첫 점 버그 — `sequence-tool.ts:284-285` NUMBER · RELATIVE 에 `!!lastPoint(context.values)` 조건. +3. 이동 · 복사 키보드 값 — 시작 ABSOLUTE · 끝 NUMBER · ABSOLUTE · RELATIVE · `getPointFromEvent` 로. +4. 회전 각도 숫자 — `rotate-tool.ts:156-174` NUMBER_INPUT · 음수 허용(`input-patterns.ts:4`). +5. (선택) 선 중 `C` · `U` · 6. 원본 안내문 한글화 + 「(길이 · <각도 · @x,y)」 · 7. 동적 입력 `100 Tab 30⏎`. + +## 오토캐드 방식 (인터넷 조사 · Autodesk 도움말 우선) + +- 네 겹: 명령줄(글자) → 프롬프트 대기(점 · 값 · 선택지) → 점 입력 문법(`x,y` · `@` · `<` · 숫자만) → 보조 모드(직교 · 극 추적 · 스냅). 프롬프트 하나가 클릭 · 좌표 · 거리 · 선택지 글자 · 스냅 이름을 다 받음. +- 명령줄: 이름 · 별칭(acad.pgp `A, *ARC` · 대소문자 무관) → Enter/Space · 자동완성(자주 쓴 순 · 중간 글자 · 지연 300ms) · ↑↓ 최근 입력 · 끝난 뒤 Enter/Space = 직전 명령 · Esc 취소 · 우클릭 = Enter/메뉴(시간 감지 짧게 Enter · 길게 메뉴) · 투명 명령 `'ZOOM`. +- 프롬프트: `Specify next point or [Close/Undo]:` · 대괄호 = 선택지 · 대문자 글자 = 입력 키 · `<값>` = 기본값(Enter 면 유지) · 선택지 클릭 · 동적 입력 ↓ 목록. +- 점 입력: 절대 `100,200` · 상대 `@30,0` · 극 `30<45` · 상대 극 `@30<45` · 각도 잠금 `<45` Enter → 커서로 쪽 → `30` Enter(다음 한 점만 · "Angle Override") · 직접 거리(커서 방향 + `30` Enter · 직교 · 극 추적과 같이) · 각도 = 동쪽 0° 반시계 + · 도분초 `45d30'15"` · 방위 `N45d30'E` · 소수점 `.` 만(`,` = 좌표 구분). +- 동적 입력(F12): 둘째 점부터 기본 상대 극(`@` 생략) · 절대는 `#` · 치수 입력 = 길이 · 각도 칸 · 값 넣고 Tab = 그 칸 잠금 + 다음 칸 · Enter 확정. +- 보조 모드: 극 추적 F10(증분 90 · 60 · 45 · 30 · 22.5 · 18 · 15 · 10 · 5° · 추가각) · 직교 F8(Shift 누르는 동안 반전) · 객체 스냅 F3 + 점 프롬프트에서 END · MID · CEN · INT · PER · TAN · NEA · QUA · NOD(다음 한 점만) · NON · 스냅 추적 F11 · 임시 덮어쓰기 Shift+키. +- LINE 「선 → 각도 → 길이」 키 차례: 동적 입력 켬 = `L` Enter → 첫 점 → Tab `45` Tab(각도 잠김) `30` Enter 또는 `30` Tab `45` Enter 또는 `30<45` Enter · 끔 = `L` Enter → 첫 점 → `<45` Enter → 커서로 쪽 → `30` Enter 또는 `@30<45` Enter · `U` Enter = 마지막 선 취소 · `C` Enter = 첫 점으로 닫고 끝 · Enter · Esc = 끝 · 새 LINE 첫 점에서 Enter = 마지막 끝점에서 이어 그림. +- 비슷한 프로그램: BricsCAD(같은 명령줄 · 동적 치수 길이 → Tab 잠금 → 각도 · F12 = Quad) · DraftSight(Quick Input F12 · Tab) · LibreCAD(`@100<90` · `@` 없으면 절대) · nanoCAD(`300` Tab `30` Enter) · Onshape(명령줄 없음 · 그린 뒤 치수 구속) · AutoCAD Web(명령줄 · 선택지 단추). +- 웹 함정: 브라우저 F키(F1 · F3 · F5 · F6 · F7 · F10 · F11 · F12 · 막을 수 있는 건 캔버스 초점 때 preventDefault · 실측 필요) · Ctrl+W · T · N 못 막음 · 한글 IME(key "Process" · keyCode 229 · `event.code` 우선 · 조합 중 Enter 무시) · y축 아래(내부 수학각 하나 · 화면에서 뒤집기) · 우클릭 · 가운데 누름 기본 동작 막기. +- 사용자 지정(설정 모달 후보): 별칭 편집(acad.pgp User Defined · 같은 별칭은 마지막이 이김) · 단축키(CUI) · 우클릭 모드 · 시간 · 휠 줌 방향(ZOOMWHEEL 0 = 앞으로 굴림 확대 · 1 = 반대) · 비율(ZOOMFACTOR 60) · 가운데 끌기 = 팬(MBUTTONPAN 1) · 동적 입력 기본(상대/절대 · 극/직교) · 극 추적 각 · 각 형식. +- 추천 사양: 1단계 = 명령줄 · 별칭 + 자동완성 · 프롬프트 `[닫기(C)/명령취소(U)] <기본값>` · 점 파서 한 벌(`x,y` · `@dx,dy` · `d 서랍 900) · 저장 안 한 고침 물음 `ui_template/ui_template_leave_dialog.ts:13` `askLeave`(예 `B08_Quantity_UI_Drawing.ts:210`). +- 탭 공용 부품 없음 · 공용 모양 `.ui-subtabs`(`ui_template_general_layout.css:58-70`) + `createButton` filled/ghost + `role=tab`(예 `B08_Quantity_UI_Milestone.ts:180-200` · `B09_Estimation_UI_TabBar.ts:30-60` · `M02_DF_InputPanel.ts:144-155`). + +## 마우스 현황 (당김 = 휠을 몸 쪽으로 굴리면 확대) + +| 화면 | 왼쪽 | 가운데 | 오른쪽 | 휠 | 근거 | +|---|---|---|---|---|---| +| B04 2D 지도 | 고름 · 관 끌기 | 팬 | 메뉴 | 줌 · 당김 | `B04_PreProcess_UI_MapViewer.ts:536-625` | +| B04 · B05 3D | 회전 · 고름 | 팬 | 배치 메뉴 | 줌 · 당김 | `B04_PreProcess_UI_Camera.ts:140-254` · `B05_Profile_UI_Viewer.ts:145,266` | +| B05 배수유역도 | 관 끌기 · 고름 | 팬 | 메뉴 | 줌 · 당김 | `B05_Profile_UI_Drainage_Interact.ts:60-119` | +| B05 노선 편집 지도 | 노드 끌기 · 두 번 = 더함 | 팬 | 노드 지움 | 줌 · 당김 | `B05_Profile_UI_RouteEdit_Input.ts:49-146` | +| 노선 편집 횡단 · B06 [상세] | 고름 | 팬 | — | 줌 · 당김 · 두 번 = 원배율 | `A00_Common/b_svg_zoom_pan.ts:162-264` | +| B05 · B06 종단 · 유토 · 측점표 | 가로 이동 | 가로 이동 | 메뉴 | 가로 스크롤 · 확대는 단추 | `B05_Profile_UI_Profile_Wheel.ts:82-177` | +| B06 횡단 카드 | 고름 | 팬 | 메뉴 | 목록 스크롤 | `B06_Section_UI_Cross_View_Zoom.ts:51-100` | +| CAD(B07 · B06 상세 · M02 도면) | 고름 · 점 | 팬 | Enter | 줌 · 당김 | `openwebcad/src/inputController/input-controller.ts:150-300` · `input-wheel.ts:20-23` | +| B08 거리이정표 | 빈 곳 끌기 = 팬 | 팬 | — | 줌 · 당김 | `B08_Quantity_UI_Milestone_Diagram.ts:157,463-474` | +| B08 · B09 표 | 고름 · 두 번 = 입력 | — | 메뉴 | 스크롤 | `ui_template/ui_template_data_table.ts:514-529` | +| M02 DF 그림 | 상자 끌기 · Space+왼쪽 = 팬 | 팬 | — | 줌 · 밀기 | `M02_DF_Diagram.ts:57-66,453-476` | +| M02 노무 그림 | Space+왼쪽 = 팬 | 팬 | — | Ctrl+휠 · 밀기 | `M02_MasterTemplete_Labor_Diagram.ts:283-332` | +| M02 시트 | 칸 고름 | — | 메뉴 | Ctrl+휠 · 밀기 | `A00_Common/spreadsheet/spreadsheet_grid.ts:270-284` | +| 공용 돋보기 | — | — | 0.4초 누름(Z 키도) | 배율 · 밀기 | `ui_template/ui_template_loupe.ts:5-93` | + +- 팬 = 가운데 단추로 통일 · 휠 방향 둘로 갈림 · 공용 판정 `A00_Common/touch_bridge.ts:35-36` `wheelZoomsIn` 은 셋만 씀(B04 지도 · 배수유역도 · 노선 편집 지도) · 나머지 직접 박음(`b_svg_zoom_pan.ts:217` · `B04_PreProcess_UI_Camera.ts:253` · `input-wheel.ts:23` · `M02_DF_Diagram.ts:457` · `Labor_Diagram.ts:316` · `ui_template_loupe.ts:91` · `spreadsheet_grid.ts:274`). +- 지금 사용자가 바꿀 수 있는 것: 터치 모드(localStorage `frd_touch_mode` · `device_context.ts:33,91-101`) · B05 3D 투영 · B09 행 누름(pref `b_page_state.ts:71,107`) — 마우스 단추 · 휠 방향 칸 없음. CAD 휠 지수 · 스냅 거리 상수(`openwebcad/src/App.consts.ts:52,150-160`) · 토글은 메모리(`state.ts:82,179-180`). maplibre 안 씀(평면 = 자체 캔버스). + +## 단축키 현황 + +| 자리 | 키 | 근거 | +|---|---|---| +| 공용 판정 | Ctrl+Z · Shift+Z · Y · S · A(한글 자판 자리) | `A00_Common/key_shortcut.ts:28-49` | +| 전역 | Esc = ☰ · 창 닫힘 · 고름 풂 · Z 누름 = 돋보기 | `app_shell.ts:173` · `ui_template_modal.ts:53` · `design/design_pick_escape.ts:15-29` · `ui_template_loupe.ts:66-72` | +| B05 종단 | Ctrl+Z · Ctrl+Shift+Z · 방향키 · 자유지정 Enter · Esc · Backspace | `B05_Profile_UI_Profile_Panel_Tools.ts:310-349` | +| B05 노선 편집 | Ctrl+Z · Y · Shift+Z | `B05_Profile_UI_RouteEdit_History.ts:157-165` | +| B06 | 방향키 · ▲▼ · Enter/Space · Esc | `B06_Section_UI_Cross_Panel_Base.ts:264-290` · `_Cross_Design_Pick.ts:545-562` | +| CAD | F3 · F7 · F8 · F10 · Ctrl+Z · Y · A · S(M02) · V · Delete · Esc · Enter · Space = Enter · 방향키 · 글자 = 명령 | `input-controller.ts:364-447` | +| CAD 별칭 | 명령 153 · 별칭 82 · `commands.*.ts` `aliases` | `openwebcad/src/commands/registry.ts:50-57,116-135` | +| B08 · B09 · 표 | Enter · F2 · 방향키 · Esc | `ui_template_data_table.ts:520-529` 외 | +| M02 | Ctrl+S · Z · C/V · Delete · Esc · Space = 팬 · 시트 엑셀 키 | `M02_MasterTemplete_CardShell.ts:79-80` · `spreadsheet_keys.ts` | + +정의 표 = CAD 별칭 하나(엑셀 키 표 `spreadsheet_shortcuts.ts:1-10` 은 대조용) · 충돌 관리 없음(입력 칸 건너뜀 · `defaultPrevented` 양보 · 창 떠 있으면 양보 · CAD 는 iframe · 별칭 겹치면 먼저 것 `registry.ts:53-56` · 이름 > 별칭 `:119`). + +## 저장 — 새 표 · API 필요 없음 + +- 표 `user_ui_prefs`(`db_management/018_user_ui_prefs.sql:17-25`) · `GET/PUT /api/dashboard/me/ui-prefs`(`B01_Dashboard_Router.py:122-133` · 값 문자열 · 통째 덮음 `Repository.py:554-573`). +- 브라우저 `pref` 통 = localStorage `aislo:pref:*`(`A00_Common/b_page_state.ts:47-112,285-287`) · 바뀌면 1.5초 모아 PUT(`:337-375`) · 로그인 첫 세션에 서버 값 얹음(`A06_Login_Api_Fetch.ts:64-69` → `b_page_state.ts:378-404`) · CAD iframe 같은 오리진 → localStorage 같이 씀(테마 선례 `openwebcad/src/theme.ts:43-52`). + +## 설계 후보 · 위험 + +- 읽기 층 새 `A00_Common/input_prefs.ts`(의존 없음 · CAD 도 import) — `INPUT_PREFS_KEY` · `DEFAULT_INPUT_PREFS`(= 지금 동작) · `inputPrefs()`(부를 때마다 읽음 · 깨짐/Node = 기본값) · `wheelIn(deltaY)` · 모달 [저장] 때만 `writeState("input-prefs", 값)`. +- 배선 자리: 휠 = `touch_bridge.ts:36`(셋 자동) · `b_svg_zoom_pan.ts:217` · `B04_PreProcess_UI_Camera.ts:253` · `input-wheel.ts:23` · (일체면) M02 둘 · 돋보기 · 시트 · CAD 별칭 = `registry.ts:116-131`(이름 > 내 별칭 > 기본) · CAD 오른쪽 = `input-controller.ts:155`. +- 위험: 700줄(`B04_PreProcess_UI_MapViewer.ts` 699 안 만짐 · `M02_DF_Diagram.ts` 692 한 줄 · `input-controller.ts` 620) · 공용 파일(`b_page_state.ts` · `app_shell.ts` · `touch_bridge.ts` · locale) 한 창이 맡음 · CAD `input-controller.ts` · `registry.ts` 는 331 과 겹침 → 331 뒤 · 휠 일체면 M02 그림 · 시트 · 돋보기 기본 방향 반대 · 계정 전환 시 앞 사람 값 남을 수 있음(기존 한계) · Node 시험(helper_164 · 266 등)이 `b_svg_zoom_pan` · `touch_bridge` 를 불러 돌림 → `input_prefs` 는 불러올 때 window 안 만짐 · CAD 명령 목록은 `commands.*.ts` import 시 xstate 딸려 옴 → 목록 JSON 한 벌 + 거울 시험. + +## 시험 + +- 있음: `test_device_drawer.py`(서랍 차례) · `test_device_audit.py` · `test_touch_bridge.py` · `test_cad_touch.py` · `test_prefs_persist_across_tabs.py` · `test_b01_ui_prefs_dead_project.py` · `test_session_keys_registered.py` · 휠 · 팬(`test_164_detail_zoom.py` · `test_266_detail_home.py` · `test_281_1_drainage_zoom_range.py` · `test_230_1_popout_wheel.py` · `test_244_6/8_*` · `test_b05_routeedit_cross_width.py` · `test_cross_axes_zoom.mjs` · `test_cross_pan_bounds.mjs` · `openwebcad/src/inputController/input-wheel.test.ts`) · 돋보기 · 키 · 모달(`test_218_1_loupe.py` · `test_220_1` · `test_221_1` · `cad/ctrl-shortcut-hangul.test.ts` · `cad/ctrl-shortcut-sheet.test.ts` · `test_modal_head_actions.py` · `test_cad_host_tabs.py`). +- 새로: `test_input_prefs.py`(+ cjs) · `cad/input-prefs-cad.test.ts` · `test_app_settings.py`. From 6d5e5f911ed1346e71243b88bb63fc4651ab698b Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 15 Oct 2026 12:22:37 +0900 Subject: [PATCH 13/81] =?UTF-8?q?feat(=EC=A2=85=EB=8B=A8):=20325-1=20?= =?UTF-8?q?=EB=B0=B0=EC=88=98=20=ED=8C=90=20=EC=B0=BD=EA=B5=AC=20=E2=80=94?= =?UTF-8?q?=20=EC=A7=80=EB=8F=84=20=ED=81=AC=EA=B8=B0=20=EA=B4=80=EC=B0=B0?= =?UTF-8?q?=EC=9D=84=20=EB=86=93=EC=9D=B8=20=EC=B0=BD=20=EA=B2=83=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EB=B0=94=EA=BF=94=20=EB=81=BC=EC=9A=B0=EB=8A=94=20?= =?UTF-8?q?rehome=20=C2=B7=20=EC=A7=81=EC=A0=84=20=ED=81=AC=EA=B8=B0=20?= =?UTF-8?q?=EC=9D=B4=EC=96=B4=EB=B0=9B=EC=95=84=20=EB=B0=B0=EC=9C=A8=20?= =?UTF-8?q?=EC=9C=A0=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01VmC34fsxxbweEG3RUjs3Mn --- B05_Profile/B05_Profile_UI_Drainage_Panel.ts | 4 +- .../B05_Profile_UI_Drainage_Panel_Types.ts | 2 + B05_Profile/B05_Profile_UI_Drainage_Parts.ts | 27 +++-- .../tester/helper_224_1a_drainage_rehome.cjs | 99 +++++++++++++++++++ .../tester/test_224_1a_drainage_rehome.py | 65 ++++++++++++ 5 files changed, 189 insertions(+), 8 deletions(-) create mode 100644 resources/tester/helper_224_1a_drainage_rehome.cjs create mode 100644 resources/tester/test_224_1a_drainage_rehome.py diff --git a/B05_Profile/B05_Profile_UI_Drainage_Panel.ts b/B05_Profile/B05_Profile_UI_Drainage_Panel.ts index d8d32dcd5..75a2d3ab5 100644 --- a/B05_Profile/B05_Profile_UI_Drainage_Panel.ts +++ b/B05_Profile/B05_Profile_UI_Drainage_Panel.ts @@ -536,8 +536,7 @@ export function createDrainagePanel(callbacks: DrainagePanelCallbacks = {}): Dra selectBasin: (index) => selectBasin(index), }); - // 패널을 늘리면 지도를 더 보여 줄 뿐, 배율은 그대로 둔다 — 늘릴 때마다 확대되면 - // 방금 보던 자리를 다시 찾아야 한다(2026-08-02 사용자 지시). + // 패널을 늘리면 지도를 더 보여 줄 뿐, 배율은 그대로 둔다(늘릴 때마다 확대되면 보던 자리를 잃음). const resizeObserver = observeViewportSize(viewport, { meta: () => meta, anchor: () => ({ width: 0, height: 0, scale, offsetX, offsetY }), @@ -696,5 +695,6 @@ export function createDrainagePanel(callbacks: DrainagePanelCallbacks = {}): Dra resizeObserver.disconnect(); widthResizer.dispose(); }, + rehome: () => resizeObserver.rehome(), }; } diff --git a/B05_Profile/B05_Profile_UI_Drainage_Panel_Types.ts b/B05_Profile/B05_Profile_UI_Drainage_Panel_Types.ts index 7ed1fe8b7..365d8a1ef 100644 --- a/B05_Profile/B05_Profile_UI_Drainage_Panel_Types.ts +++ b/B05_Profile/B05_Profile_UI_Drainage_Panel_Types.ts @@ -52,6 +52,8 @@ export interface DrainagePanel { /** 구간형 구조물이 놓인 자리 — 계획선 위에 띠로 얹는다(계획서 3-6). * 종단 레인의 띠와 같은 뜻이고, 목록이 바뀔 때마다 통째로 넣어 준다. */ setIntervalSpans: (spans: ReadonlyArray) => void; + /** 판이 다른 창(새 창 · 제자리)으로 옮겨 간 뒤 — 지도 크기 관찰을 그 창 것으로 바꿔 낌(325-1). */ + rehome: () => void; dispose: () => void; } diff --git a/B05_Profile/B05_Profile_UI_Drainage_Parts.ts b/B05_Profile/B05_Profile_UI_Drainage_Parts.ts index 868e450c5..f6e4539ba 100644 --- a/B05_Profile/B05_Profile_UI_Drainage_Parts.ts +++ b/B05_Profile/B05_Profile_UI_Drainage_Parts.ts @@ -23,6 +23,7 @@ import type { MapContextMenu, MapContextMenuItem } from "@ui/ui_template_context import type { DetailBasin } from "../B04_PreProcess/B04_PreProcess_Api_Fetch"; import type { PipeEditor, PipePoint } from "./B05_Profile_UI_Drainage_Pipes"; import { PIPE_MATCH_M } from "../A00_Common/design/design_selection"; +import { viewOf } from "../common_util/common_util_view_window"; function L(key: keyof typeof ui_locales): string { return ui_locales[key][currentLanguageIndex]; @@ -454,7 +455,8 @@ export function preserveViewOnResize( }; } -/** 뷰포트 크기 변화를 지켜보며 배율을 지킨다. 반환값은 정리용 `ResizeObserver`. */ +/** 뷰포트 크기 변화를 지켜보며 배율을 지킨다. `rehome` = 판이 다른 창으로 옮겨 간 뒤 그 창의 + * 관찰자로 바꿔 낌(직전 크기를 지켜 배율 유지 · 325-1) · `disconnect` = 정리. */ export function observeViewportSize( viewport: HTMLElement, ports: { @@ -463,9 +465,9 @@ export function observeViewportSize( apply: (next: { scale: number; offsetX: number; offsetY: number }) => void; redraw: () => void; }, -): ResizeObserver { +): { rehome: () => void; disconnect: () => void } { let last: { width: number; height: number } | null = null; - const observer = new ResizeObserver(() => { + const onResize = (): void => { const rect = viewport.getBoundingClientRect(); const width = Math.max(1, Math.floor(rect.width)); const height = Math.max(1, Math.floor(rect.height)); @@ -482,9 +484,22 @@ export function observeViewportSize( } last = { width, height }; ports.redraw(); - }); - observer.observe(viewport); - return observer; + }; + // 새 창 크기 변화는 그 창의 관찰자만 안다 — 판이 놓인 창의 것으로 단다. + const watch = (): ResizeObserver => { + const view = viewOf(viewport) as Window & typeof globalThis; + const next = new view.ResizeObserver(onResize); + next.observe(viewport); + return next; + }; + let observer = watch(); + return { + rehome: () => { + observer.disconnect(); + observer = watch(); + }, + disconnect: () => observer.disconnect(), + }; } /** 캔버스 버퍼를 뷰포트 크기·DPR에 맞춘다. 실제로 바뀔 때만 재할당한다(재할당 시 내용이 지워진다). */ diff --git a/resources/tester/helper_224_1a_drainage_rehome.cjs b/resources/tester/helper_224_1a_drainage_rehome.cjs new file mode 100644 index 000000000..1f3bbea85 --- /dev/null +++ b/resources/tester/helper_224_1a_drainage_rehome.cjs @@ -0,0 +1,99 @@ +/* 224-1a 배수 판 창구(rehome) — 진짜 `observeViewportSize`(B05_Profile_UI_Drainage_Parts.ts)를 + * 가짜 창 둘 위에서 돌려 JSON 으로 냄. + * · 처음 = 판이 놓인 창(메인)의 관찰자 + * · rehome = 옛 관찰자 끊고 지금 놓인 창(새 창)의 관찰자로 · 직전 크기 이어받아 배율 유지 + * · disconnect = 지금 관찰자 끊음 */ +const fs = require("fs"); +const path = require("path"); +const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript")); + +const ROOT = path.join(__dirname, "..", ".."); +const cache = new Map(); +const stub = new Proxy({}, { get: () => () => "" }); + +/** TS 한 장을 CommonJS 로 — 상대 경로는 진짜(공용 창 · 지도 셈만), 나머지는 빈 함수. */ +function loadTs(full) { + if (cache.has(full)) return cache.get(full); + const js = ts.transpileModule(fs.readFileSync(full, "utf8"), { + compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 }, + }).outputText; + const box = { exports: {} }; + cache.set(full, box.exports); + const localRequire = (request) => + /common_util_view_window|B04_PreProcess_UI_MapRender/.test(request) + ? loadTs(path.join(path.dirname(full), `${request}.ts`)) + : stub; + new Function("exports", "module", "require", js)(box.exports, box, localRequire); + return box.exports; +} + +/** 가짜 창 — 관찰자를 모아 두고 크기를 바꿔 울림. */ +function fakeWindow(name) { + const win = { name, observers: [] }; + win.ResizeObserver = class { + constructor(fn) { + this.fn = fn; + this.live = false; + win.observers.push(this); + } + observe() { + this.live = true; + } + disconnect() { + this.live = false; + } + }; + return win; +} +const main = fakeWindow("main"); +const popup = fakeWindow("popup"); +global.window = main; + +const parts = loadTs(path.join(ROOT, "B05_Profile", "B05_Profile_UI_Drainage_Parts.ts")); +const doc = { defaultView: main }; +let size = { width: 400, height: 300 }; +const viewport = { ownerDocument: doc, getBoundingClientRect: () => ({ ...size }) }; +const fire = (win) => win.observers.filter((o) => o.live).forEach((o) => o.fn()); + +let view = { scale: 2, offsetX: 10, offsetY: -5 }; +const applied = []; +let redraws = 0; +const META = { + x_min: 0, + x_max: 4000, + y_min: 0, + y_max: 3000, + width_meters: 4000, + height_meters: 3000, +}; +const watcher = parts.observeViewportSize(viewport, { + meta: () => META, + anchor: () => ({ width: 0, height: 0, ...view }), + apply: (next) => (applied.push(next), (view = next)), + redraw: () => (redraws += 1), +}); +const out = { firstOwner: main.observers.length === 1 && popup.observers.length === 0 }; +fire(main); // 처음 크기 기억 +out.firstApplied = applied.length; + +// 새 창으로 옮김 — 메인 관찰자 끊고 새 창 관찰자로 +doc.defaultView = popup; +watcher.rehome(); +out.mainLive = main.observers.some((o) => o.live); +out.popupLive = popup.observers.filter((o) => o.live).length; +size = { width: 800, height: 600 }; // 새 창에서 두 배로 커짐 +fire(popup); +out.afterGrow = { applied: applied.length, ...view }; +// 메인 창 관찰자가 울려도(이미 끊김) 아무 일 없음 +fire(main); +out.mainIgnored = applied.length; + +// 제자리 — 다시 메인 +doc.defaultView = main; +watcher.rehome(); +out.backPopupLive = popup.observers.some((o) => o.live); +out.backMainLive = main.observers.filter((o) => o.live).length; +watcher.disconnect(); +out.afterDispose = [...main.observers, ...popup.observers].some((o) => o.live); +out.redraws = redraws; +process.stdout.write(JSON.stringify(out)); diff --git a/resources/tester/test_224_1a_drainage_rehome.py b/resources/tester/test_224_1a_drainage_rehome.py new file mode 100644 index 000000000..c53de3f95 --- /dev/null +++ b/resources/tester/test_224_1a_drainage_rehome.py @@ -0,0 +1,65 @@ +# -*- coding: utf-8 -*- +"""224-1a(PLAN 325-1) 배수유역 판 창구 — 새 창으로 떼어도 지도 크기 관찰이 그 창을 따름. + +- 판 지도 크기 관찰(`observeViewportSize`)은 판이 놓인 창의 ResizeObserver 를 씀. +- `rehome()` = 옛 창 관찰자 끊고 지금 놓인 창 것으로 · 직전 크기를 이어받아 배율(화면 m 당 px) 유지. +- 판 창구(`DrainagePanel.rehome`)로 내보내고 패널 본체는 700줄 안. + +`helper_224_1a_drainage_rehome.cjs` 가 진짜 TS 를 가짜 창 둘 위에서 돌림. +""" + +import json +import os +import subprocess +from pathlib import Path + +import pytest + +HERE = os.path.dirname(os.path.abspath(__file__)) +B05 = Path(HERE).parents[1] / "B05_Profile" + + +@pytest.fixture(scope="module") +def r(): + proc = subprocess.run( + ["node", os.path.join(HERE, "helper_224_1a_drainage_rehome.cjs")], + capture_output=True, + text=True, + encoding="utf-8", + timeout=60, + ) + assert proc.returncode == 0, proc.stderr + return json.loads(proc.stdout) + + +def test_처음은_놓인_창_관찰자(r): + assert r["firstOwner"] is True + assert r["firstApplied"] == 0 # 처음 크기는 기억만 + + +def test_rehome_은_새_창_관찰자로(r): + assert r["mainLive"] is False + assert r["popupLive"] == 1 + + +def test_새_창에서_커져도_배율_유지(r): + """400 → 800 px · 배율 2 → 1 = 화면 m 당 px 그대로.""" + grow = r["afterGrow"] + assert grow["applied"] == 1 + assert grow["scale"] == pytest.approx(1) + assert r["mainIgnored"] == 1 # 끊긴 메인 관찰자는 안 울림 + + +def test_제자리와_정리(r): + assert r["backPopupLive"] is False + assert r["backMainLive"] == 1 + assert r["afterDispose"] is False + assert r["redraws"] == 2 + + +def test_판_창구와_700줄(): + panel = (B05 / "B05_Profile_UI_Drainage_Panel.ts").read_text(encoding="utf-8") + assert "rehome: () => resizeObserver.rehome()," in panel + assert len(panel.splitlines()) <= 700 + types = (B05 / "B05_Profile_UI_Drainage_Panel_Types.ts").read_text(encoding="utf-8") + assert "rehome: () => void;" in types From 7bae5ebb7d34231efdaf43a1abdfd6cf06179ca8 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 15 Oct 2026 12:25:45 +0900 Subject: [PATCH 14/81] =?UTF-8?q?feat(=EC=8A=A4=ED=94=84=EB=A0=88=EB=93=9C?= =?UTF-8?q?=EC=8B=9C=ED=8A=B8):=20329-2=20=EC=86=90=EC=9E=A1=EC=9D=B4=20se?= =?UTF-8?q?lect(=EB=B2=94=EC=9C=84,=20=EC=8B=9C=ED=8A=B8=3F)=20=E2=80=94?= =?UTF-8?q?=20=EB=B2=94=EC=9C=84=20=EA=B8=80=20=E2=86=92=20=EA=B3=A0?= =?UTF-8?q?=EB=A6=84(=EB=B3=91=ED=95=A9=20=EB=8A=98=EB=A6=BC=20=C2=B7=20?= =?UTF-8?q?=EC=97=AC=EB=9F=BF)=20=C2=B7=20=EA=B7=B8=20=EC=8B=9C=ED=8A=B8?= =?UTF-8?q?=20=ED=83=AD=EC=9C=BC=EB=A1=9C=20=C2=B7=20=EB=B3=B4=EC=9D=B4?= =?UTF-8?q?=EA=B2=8C=20=EA=B5=B4=EB=A6=BC=20=C2=B7=20=ED=8B=80=EB=A6=B0=20?= =?UTF-8?q?=EA=B8=80=20false?= 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_01XWh5wNNycMkbwMpACz1rte --- A00_Common/spreadsheet/spreadsheet.ts | 17 ++++++++++++++++- A00_Common/spreadsheet/spreadsheet_selection.ts | 14 ++++++++++++++ .../tester/spreadsheet/helper_spreadsheet_d.cjs | 5 +++++ .../spreadsheet/test_spreadsheet_d_input.py | 5 +++++ 4 files changed, 40 insertions(+), 1 deletion(-) diff --git a/A00_Common/spreadsheet/spreadsheet.ts b/A00_Common/spreadsheet/spreadsheet.ts index 27ac5da6a..6d7fefd78 100644 --- a/A00_Common/spreadsheet/spreadsheet.ts +++ b/A00_Common/spreadsheet/spreadsheet.ts @@ -28,7 +28,7 @@ import type { LinkOptions, Links } from "./spreadsheet_links"; import { linkCommand, type LinkInfo } from "./spreadsheet_link_set"; import { attachMenu } from "./spreadsheet_menu"; import { attachMouse } from "./spreadsheet_mouse"; -import { LAST_C, LAST_R, selectCell } from "./spreadsheet_selection"; +import { LAST_C, LAST_R, selectCell, selectRanges } from "./spreadsheet_selection"; import { createStatusBar } from "./spreadsheet_statusbar"; import { mountTabs } from "./spreadsheet_tabs"; import { mountToolbar } from "./spreadsheet_toolbar"; @@ -90,6 +90,9 @@ export interface SpreadsheetHandle { /** HTML 표를 지금 시트 고른 칸(왼위)부터 붙임(105-1 도면 → 면적) — 붙여넣기와 같은 길(병합 · 되돌리기 한 번) · * `check` = 덮을 자리의 값 칸 · 알약 칸 수 · 범위 → false 면 안 붙임 · 붙인 범위 A1 · 읽기만 · 못 하면 null */ pasteTable(html: string, check?: PasteCheck): Promise; + /** 범위 고름 · 보이게 굴림(329-2 엑셀 보기 창) — `시트` = id 나 이름(없으면 지금 시트 · 다르면 그 탭으로) · + * 범위 여럿 = 여러 고름 · 시트가 없거나 주소가 틀리면 false(고름 그대로) */ + select(범위: string | string[], 시트?: string): boolean; destroy(): void; } @@ -350,6 +353,18 @@ export function createSpreadsheet( return 이름; }, pasteTable: (html, check) => pasteTableAt(ctx, html, check), + select(range, sheetKey) { + const target = sheetKey + ? ctx.book.시트.find((s) => s.id === sheetKey || s.이름 === sheetKey) + : ctx.sheet(); + const next = target && selectRanges(target, typeof range === "string" ? [range] : range); + if (!target || !next) return false; + showSheet(target.id); + if (ctx.selection.시트 !== target.id) return false; // 편집기 유효성에 막힘 + select(next, true); + grid.reveal(next.활성.r, next.활성.c); // 범위가 판보다 크면 왼위가 보이게 + return true; + }, destroy() { clearTimeout(toastTimer); detachKeys(); diff --git a/A00_Common/spreadsheet/spreadsheet_selection.ts b/A00_Common/spreadsheet/spreadsheet_selection.ts index 46c0c3e8c..750318511 100644 --- a/A00_Common/spreadsheet/spreadsheet_selection.ts +++ b/A00_Common/spreadsheet/spreadsheet_selection.ts @@ -91,6 +91,20 @@ export function selectSpan(sheet: Sheet, anchor: CellAddress, to: CellAddress): return { 시트: sheet.id, 범위: [g], 활성: { ...anchor }, 기준: { ...anchor } }; } +/** A1 범위 글 여럿 → 고름(329-2 손잡이 `select`) — 병합만큼 늘림 · 활성 = 첫 범위 왼위 · 틀린 글 · 빈 목록 = null */ +export function selectRanges(sheet: Sheet, texts: string[]): Selection | null { + const list = merges(sheet); + const 범위: CellRange[] = []; + for (const t of texts) { + const g = parseRange(t); + if (!g) return null; + 범위.push(expand(g, list)); + } + if (!범위.length) return null; + const at = { r: 범위[0].r0, c: 범위[0].c0 }; + return { 시트: sheet.id, 범위, 활성: at, 기준: { ...at } }; +} + /** 행 전체(r0~r1) · 열 전체(c0~c1) · 전체 — 활성은 첫 보이는 칸 · 걸친 병합만큼 늘림(엑셀) */ export function selectRows(sheet: Sheet, a: number, b: number, active?: number): Selection { const r0 = Math.min(a, b); diff --git a/resources/tester/spreadsheet/helper_spreadsheet_d.cjs b/resources/tester/spreadsheet/helper_spreadsheet_d.cjs index e93264998..792c7c4a1 100644 --- a/resources/tester/spreadsheet/helper_spreadsheet_d.cjs +++ b/resources/tester/spreadsheet/helper_spreadsheet_d.cjs @@ -65,6 +65,11 @@ const out = { })(), cycleSingle: sel.cycle(s1, cell(3, 2), 1, 0), usedEnd: addr.toA1(sel.usedEnd(s1).r, sel.usedEnd(s1).c), + // 손잡이 select(범위) — 범위 글 → 고름(병합 늘림 · 여럿 · 틀린 글 null) + ranges: [["B20:O24"], ["D4"], ["B20:O24", "A1:A2"], ["가1"], []].map((t) => { + const s = sel.selectRanges(s1, t); + return s && [addr.toA1(s.활성.r, s.활성.c), ...s.범위.map(addr.rangeToA1)].join("|"); + }), // 입력 글 → 칸 textToCell: ["12", "1,234.5", "50%", "-1e3", "true", "'=12", "=", "=A1+1", "", "1,23", "abc"].map( (t) => ed.textToCell(t, { 서식: 4 }), diff --git a/resources/tester/spreadsheet/test_spreadsheet_d_input.py b/resources/tester/spreadsheet/test_spreadsheet_d_input.py index ebd4f575b..4f8252708 100644 --- a/resources/tester/spreadsheet/test_spreadsheet_d_input.py +++ b/resources/tester/spreadsheet/test_spreadsheet_d_input.py @@ -46,6 +46,11 @@ def test_cycle_in_selection(r): assert r["cycleSingle"] is None +def test_select_ranges(r): + # 엑셀 보기 창(329-2) 호표 영역 강조 — 활성 = 왼위 · 병합 C4:F4 · A1:H1 걸치면 늘림 · 틀린 글 · 빈 목록 null + assert r["ranges"] == ["B20|B20:O24", "C4|C4:F4", "B20|B20:O24|A1:H2", None, None] + + def test_text_to_cell(r): got = r["textToCell"] assert [c.get("값", c.get("식")) for c in got] == [ From 2d239f780e95b1a73bb35f7ded7a5d40c9879515 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 15 Oct 2026 12:26:31 +0900 Subject: [PATCH 15/81] =?UTF-8?q?feat(=EC=A2=85=EB=8B=A8):=20325-2=20?= =?UTF-8?q?=EC=9C=A0=ED=86=A0=20=ED=8C=90=20=EB=B6=99=EC=96=B4=20=EC=9E=88?= =?UTF-8?q?=EC=9D=8C=20=ED=8C=90=EC=A0=95=20=E2=80=94=20=EB=86=92=EC=9D=B4?= =?UTF-8?q?=20=EB=82=98=EB=88=94=20=C2=B7=20=EB=81=8C=EA=B8=B0=EB=8A=94=20?= =?UTF-8?q?=EC=97=B4=EB=A6=BC=20&&=20=EC=A2=85=EB=8B=A8=20=EB=BF=8C?= =?UTF-8?q?=EB=A6=AC=20=EC=95=88=EB=A7=8C=20=C2=B7=20=EB=97=80=20=EC=9C=A0?= =?UTF-8?q?=ED=86=A0=EB=8A=94=20=EB=85=B8=EC=84=A0=20=EC=A0=84=EC=B2=B4=20?= =?UTF-8?q?=EC=84=B8=EB=A1=9C=20=C2=B7=20=EB=B0=A9=ED=96=A5=ED=82=A4=20?= =?UTF-8?q?=ED=8C=90=EC=A0=95=EC=97=90=20=EB=97=80=20=EC=9C=A0=ED=86=A0=20?= =?UTF-8?q?=C2=B7=20=EB=B0=B0=EC=88=98=20=ED=8C=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01VmC34fsxxbweEG3RUjs3Mn --- B05_Profile/B05_Profile_UI_Profile_Drag.ts | 8 +- B05_Profile/B05_Profile_UI_Profile_Heights.ts | 3 +- .../B05_Profile_UI_Profile_MassHaul.ts | 7 +- .../B06_Section_UI_Cross_Panel_Base.ts | 5 +- resources/tester/helper_219_1_arrow_owner.cjs | 3 +- .../tester/helper_224_1b_masshaul_docked.cjs | 112 ++++++++++++++++++ .../tester/test_224_1b_masshaul_docked.py | 69 +++++++++++ 7 files changed, 200 insertions(+), 7 deletions(-) create mode 100644 resources/tester/helper_224_1b_masshaul_docked.cjs create mode 100644 resources/tester/test_224_1b_masshaul_docked.py diff --git a/B05_Profile/B05_Profile_UI_Profile_Drag.ts b/B05_Profile/B05_Profile_UI_Profile_Drag.ts index b5c22166a..4bb99cbf0 100644 --- a/B05_Profile/B05_Profile_UI_Profile_Drag.ts +++ b/B05_Profile/B05_Profile_UI_Profile_Drag.ts @@ -133,6 +133,8 @@ export function createProfileDrag(ctx: ProfileDragContext): ProfileDrag { }; // 위쪽 경계를 끌어 패널 높이를 조절한다. 늘어난 만큼은 그래프만 먹고 도면 테이블은 // 처음 잡힌 높이를 지킨다(사용자 지시) — 테이블 행이 늘었다 줄었다 하면 읽기 어려워서다. + // 새 창으로 뗀 유토 판은 자리를 안 차지함 — 「열림 && 뿌리 안」(325-2). + const massDocked = (): boolean => !massHaul.overlay.hidden && root.contains(massHaul.overlay); const heightResizer = createPanelResizer({ axis: "vertical", target: root, @@ -143,7 +145,7 @@ export function createProfileDrag(ctx: ProfileDragContext): ProfileDrag { // 셋 다 최소가 되면 여기서 더는 못 내려가게 막는다(뒤로 넘어가는 역전 차단). min: () => { const chrome = Math.max(0, root.offsetHeight - body.clientHeight); - const massMin = massHaul.overlay.hidden ? 0 : MASSHAUL_MIN_HEIGHT; + const massMin = massDocked() ? MASSHAUL_MIN_HEIGHT : 0; const tableMin = tableOverlay.isOpen() ? TABLE_OVERLAY_MIN_HEIGHT : 0; return Math.max( MIN_PANEL_HEIGHT, @@ -169,7 +171,7 @@ export function createProfileDrag(ctx: ProfileDragContext): ProfileDrag { let mainDragRef: { chart: number; mass: number; table: number } | null = null; heightResizer.root.addEventListener("pointerdown", () => { mainPanelDragging = true; - const massH = massHaul.overlay.hidden ? 0 : massHaul.overlay.offsetHeight; + const massH = massDocked() ? massHaul.overlay.offsetHeight : 0; const tableH = tableOverlay.isOpen() ? tableOverlay.overlay.offsetHeight : 0; mainDragRef = { chart: Math.max( @@ -191,7 +193,7 @@ export function createProfileDrag(ctx: ProfileDragContext): ProfileDrag { // 비례 연동 결과를 서브패널 저장 높이로 확정한다 — 이후 모든 redraw의 판정 // 기준이 이 값이 되어 멱등성이 유지된다(확정 없이는 손을 떼는 순간 예전 // 저장 높이로 튄다). 확정 후 인라인은 걷는다(변수가 같은 값을 담는다). - if (!massHaul.overlay.hidden) massHaul.commitHeight(massHaul.overlay.offsetHeight); + if (massDocked()) massHaul.commitHeight(massHaul.overlay.offsetHeight); if (tableOverlay.isOpen()) tableOverlay.commitHeight(tableOverlay.overlay.offsetHeight); massHaul.overlay.style.height = ""; tableOverlay.overlay.style.height = ""; diff --git a/B05_Profile/B05_Profile_UI_Profile_Heights.ts b/B05_Profile/B05_Profile_UI_Profile_Heights.ts index 3771fdf1e..fe6035427 100644 --- a/B05_Profile/B05_Profile_UI_Profile_Heights.ts +++ b/B05_Profile/B05_Profile_UI_Profile_Heights.ts @@ -67,7 +67,8 @@ export function createHeightCascade(deps: HeightCascadeDeps): HeightCascade { // 구조물 알약 레인이 그래프 아래 한 줄을 차지한다 — 그만큼 먼저 떼어 놓고 나눈다 // (2026-08-17 사용자 지시 6: 그래프가 밀려도 되지만 여유는 최소). const available = Math.max(120, deps.body.clientHeight - STRUCTURE_LANE_HEIGHT_PX); - const massOpen = !deps.massHaul.overlay.hidden; + // 새 창으로 뗀 유토 판은 자리를 안 차지함 — 「열림 && 뿌리 안」(325-2). + const massOpen = !deps.massHaul.overlay.hidden && deps.root.contains(deps.massHaul.overlay); const tableOpen = deps.tableOverlay.isOpen(); // 판정 기준은 항상 **저장 높이**(리사이저 변수·기본값)다 — 임시 축소가 반영된 화면 // 높이(offsetHeight)로 재면 "줄임 → 충분해 보임 → 풀림 → 부족 → 다시 줄임"이 diff --git a/B05_Profile/B05_Profile_UI_Profile_MassHaul.ts b/B05_Profile/B05_Profile_UI_Profile_MassHaul.ts index 0600b48e1..d219230b3 100644 --- a/B05_Profile/B05_Profile_UI_Profile_MassHaul.ts +++ b/B05_Profile/B05_Profile_UI_Profile_MassHaul.ts @@ -438,7 +438,12 @@ export function createRouteMassHaulPanel( window: undefined, hideStations: true, } - : { ...params.axis, window: windowState }; + : { + ...params.axis, + // 새 창으로 뗀 판은 넓어 메인 보이는 구간 세로면 잘림 — 노선 전체 세로(325-2). + viewRange: overlay.classList.contains("is-popout") ? undefined : params.axis.viewRange, + window: windowState, + }; let massAxis: { padLeft: number; ticks: Array<{ y: number; label: string }> } | null = null; const chart = createMassHaulChart( diff --git a/B06_Section/B06_Section_UI_Cross_Panel_Base.ts b/B06_Section/B06_Section_UI_Cross_Panel_Base.ts index 6833d8a20..53ff81b73 100644 --- a/B06_Section/B06_Section_UI_Cross_Panel_Base.ts +++ b/B06_Section/B06_Section_UI_Cross_Panel_Base.ts @@ -89,7 +89,10 @@ if (typeof document !== "undefined") /** 방향키가 종단도 몫인가 — 마지막으로 누른 자리가 종단도 안이면 참. */ export function arrowsBelongToProfile(): boolean { - return !!lastPointer?.closest(".b05-route-profile"); + // 새 창으로 뗀 유토 · 배수 판도 종단도 몫(325-2). + return !!lastPointer?.closest( + ".b05-route-profile, .b05-profile__masshaul-overlay, .b05-drainage", + ); } /** 조정창 없는 고름(관 · 도수로/돌붙임 띠)이 방향키를 삼키나 — 그 동안 방향키는 아무것도 안 옮김. diff --git a/resources/tester/helper_219_1_arrow_owner.cjs b/resources/tester/helper_219_1_arrow_owner.cjs index a1f70e5b9..5e95ef67b 100644 --- a/resources/tester/helper_219_1_arrow_owner.cjs +++ b/resources/tester/helper_219_1_arrow_owner.cjs @@ -102,7 +102,8 @@ tools.createPanelTools({ /** 누른 자리 — 종단도 안이면 closest 가 그 판을 줌(새 창 요소처럼 Element 가 아닌 것도). */ const spot = (inProfile) => ({ - closest: (sel) => (inProfile && sel === ".b05-route-profile" ? {} : null), + closest: (sel) => + inProfile && sel.split(",").some((s) => s.trim() === ".b05-route-profile") ? {} : null, }); const press = (target) => listeners.document.pointerdown?.forEach((fn) => fn({ target })); diff --git a/resources/tester/helper_224_1b_masshaul_docked.cjs b/resources/tester/helper_224_1b_masshaul_docked.cjs new file mode 100644 index 000000000..2c07b616a --- /dev/null +++ b/resources/tester/helper_224_1b_masshaul_docked.cjs @@ -0,0 +1,112 @@ +/* 224-1b 유토 판 「붙어 있음」 판정 — 진짜 높이 나눔(`createHeightCascade`)과 방향키 판정 + * (`arrowsBelongToProfile` 본문)을 가짜 DOM 위에서 돌려 JSON 으로 냄. + * · 열림 && 뿌리 안 = 자리 차지 · 열림 && 새 창(뿌리 밖) = 자리 0 · 인라인 높이 안 씀 + * · 방향키 = 마지막 누름이 종단 뿌리 · 뗀 유토 · 뗀 배수 판 안이면 종단도 몫 */ +const fs = require("fs"); +const path = require("path"); +const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript")); + +const ROOT = path.join(__dirname, "..", ".."); +const read = (...p) => fs.readFileSync(path.join(ROOT, ...p), "utf8"); +const transpile = (code) => + ts.transpileModule(code, { + compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 }, + }).outputText; + +// ---- 가짜 요소(부모 사슬 · contains · closest) ---- +function el(className = "") { + const node = { className, parent: null, hidden: false, clientHeight: 0, offsetHeight: 0 }; + node.style = { + height: "", + setProperty: (k, v) => (node.style[k] = v), + }; + node.append = (kid) => ((kid.parent = node), kid); + node.contains = (other) => { + for (let at = other; at; at = at.parent) if (at === node) return true; + return false; + }; + node.closest = (selector) => { + const wanted = selector.split(",").map((s) => s.trim().replace(/^\./, "")); + for (let at = node; at; at = at.parent) + if (wanted.some((c) => at.className.split(/\s+/).includes(c))) return at; + return null; + }; + return node; +} + +// ---- 진짜 높이 나눔 ---- +const heightsExports = {}; +new Function( + "exports", + "require", + "window", + transpile(read("B05_Profile", "B05_Profile_UI_Profile_Heights.ts")), +)( + heightsExports, + (id) => + id.includes("MassHaul") + ? { MASSHAUL_MIN_HEIGHT: 160 } + : id.includes("TableOverlay") + ? { TABLE_OVERLAY_MIN_HEIGHT: 120 } + : id.includes("Structures_Marks") + ? { STRUCTURE_LANE_HEIGHT_PX: 40 } + : { frameOf: () => 0 }, + { innerHeight: 900 }, +); + +function run(docked) { + const root = el("b05-route-profile"); + root.offsetHeight = 500; + const body = root.append(el()); + body.clientHeight = 400; + const overlay = el("b05-profile__masshaul-overlay"); + if (docked) body.append(overlay); + else el("popup-body").append(overlay); // 새 창 body + const cascade = heightsExports.createHeightCascade({ + root, + body, + massHaul: { overlay, desiredHeight: () => 300, syncHandle() {} }, + tableOverlay: { + overlay: el(), + isOpen: () => false, + desiredHeight: () => 0, + setBottomOffset() {}, + }, + isMainDragging: () => false, + isSubDragging: () => false, + mainDragRef: () => null, + isMainDragCooldown: () => true, // 자동 키우기 끔 — 나눔만 봄 + hasDetail: () => true, + }); + const { chartHeight } = cascade.apply(false); + return { chartHeight, inline: overlay.style.height }; +} + +// ---- 진짜 방향키 판정 본문 ---- +const base = read("B06_Section", "B06_Section_UI_Cross_Panel_Base.ts"); +const at = base.indexOf("export function arrowsBelongToProfile"); +const fnSrc = base.slice(at, base.indexOf("\n}", at) + 2).replace("export ", ""); +const arrows = new Function( + `let lastPointer; ${transpile(fnSrc)}; return (p) => ((lastPointer = p), arrowsBelongToProfile());`, +)(); +const profileRoot = el("b05-route-profile"); +const profileChild = profileRoot.append(el("b05-profile__chart")); +const massPopped = el("b05-profile__masshaul-overlay is-popout"); +const massChild = massPopped.append(el("b05-profile__masshaul-scroll")); +const drainPopped = el("b05-drainage is-popout"); +const drainChild = drainPopped.append(el("b05-drainage__canvas")); +const card = el("b06-cross-card"); + +process.stdout.write( + JSON.stringify({ + docked: run(true), + popped: run(false), + arrows: { + profile: arrows(profileChild), + mass: arrows(massChild), + drainage: arrows(drainChild), + card: arrows(card), + none: arrows(null), + }, + }), +); diff --git a/resources/tester/test_224_1b_masshaul_docked.py b/resources/tester/test_224_1b_masshaul_docked.py new file mode 100644 index 000000000..d537ad087 --- /dev/null +++ b/resources/tester/test_224_1b_masshaul_docked.py @@ -0,0 +1,69 @@ +# -*- coding: utf-8 -*- +"""224-1b(PLAN 325-2) 유토곡선 판 「붙어 있음」 판정 · 방향키. + +- 높이 나눔 · 끌기는 「열림 && 종단 뿌리 안」만 자리를 줌 — 새 창으로 뗀 유토 판은 메인 자리 0 · + 임시 축소 인라인 높이도 안 씀(뗀 창 높이가 굳지 않게). +- 뗀 유토 판(`is-popout`)은 메인 보이는 구간 세로 범위(viewRange)를 안 씀 — 노선 전체 세로. +- 방향키 ←→ 판정은 마지막 누름이 종단 뿌리 · 뗀 유토 · 뗀 배수 판 안이면 종단도 몫. + +`helper_224_1b_masshaul_docked.cjs` 가 진짜 TS 를 가짜 DOM 위에서 돌림. +""" + +import json +import os +import re +import subprocess +from pathlib import Path + +import pytest + +HERE = os.path.dirname(os.path.abspath(__file__)) +B05 = Path(HERE).parents[1] / "B05_Profile" + + +@pytest.fixture(scope="module") +def r(): + proc = subprocess.run( + ["node", os.path.join(HERE, "helper_224_1b_masshaul_docked.cjs")], + capture_output=True, + text=True, + encoding="utf-8", + timeout=60, + ) + assert proc.returncode == 0, proc.stderr + return json.loads(proc.stdout) + + +def test_붙은_유토는_자리를_차지(r): + """본문 400 − 알약 40 = 360 · 유토 300 은 예산 247 넘어 247 로 줄고 그래프 100.""" + assert r["docked"] == {"chartHeight": 100, "inline": "247px"} + + +def test_뗀_유토는_자리_0_인라인_없음(r): + assert r["popped"] == {"chartHeight": 347, "inline": ""} + + +def test_방향키_판정(r): + assert r["arrows"] == { + "profile": True, + "mass": True, + "drainage": True, + "card": False, + "none": False, + } + + +def test_끌기도_같은_판정(): + drag = (B05 / "B05_Profile_UI_Profile_Drag.ts").read_text(encoding="utf-8") + assert "massHaul.overlay.hidden" in drag # 판정 함수 한 곳에만 + assert drag.count("massHaul.overlay.hidden") == 1 + assert drag.count("massDocked()") == 3 + assert "root.contains(massHaul.overlay)" in drag + + +def test_뗀_유토는_노선_전체_세로(): + mass = (B05 / "B05_Profile_UI_Profile_MassHaul.ts").read_text(encoding="utf-8") + assert re.search( + r'viewRange: overlay\.classList\.contains\("is-popout"\) \? undefined : params\.axis\.viewRange', + mass, + ) From d33cf220f3d9973af776e0992335d105d7cd1c9d Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 15 Oct 2026 12:31:21 +0900 Subject: [PATCH 16/81] =?UTF-8?q?feat(=EC=A2=85=EB=8B=A8):=20325-3=20?= =?UTF-8?q?=EC=9C=A0=ED=86=A0=EA=B3=A1=EC=84=A0=20=C2=B7=20=EB=B0=B0?= =?UTF-8?q?=EC=88=98=EC=9C=A0=EC=97=AD=20[=EC=B0=BD=20=EB=B6=84=ED=95=A0]?= =?UTF-8?q?=20=E2=80=94=20=EB=AA=B8=ED=86=B5=20attachPopout=20=ED=95=9C=20?= =?UTF-8?q?=EB=B2=8C=20=C2=B7=20=EC=9C=A0=ED=86=A0=20=ED=8C=90=20=EC=A2=8C?= =?UTF-8?q?=EC=83=81=EB=8B=A8=20=C2=B7=20=EB=B0=B0=EC=88=98=20=EB=A8=B8?= =?UTF-8?q?=EB=A6=AC=EC=A4=84=20=EB=A7=A8=20=EC=95=9E=20=EB=8B=A8=EC=B6=94?= =?UTF-8?q?=20=C2=B7=20=EC=B0=BD=20=EC=9D=B4=EB=A6=84=20=EB=94=B0=EB=A1=9C?= =?UTF-8?q?=20=C2=B7=20=EB=8B=A8=EC=B6=94=EA=B0=80=20=EB=86=93=EC=9D=B8=20?= =?UTF-8?q?=EC=B0=BD=EC=97=90=EC=84=9C=20=EC=97=B6=20=C2=B7=20=EB=97=80=20?= =?UTF-8?q?=ED=8C=90=20=EC=B0=BD=20=EA=BD=89=20=EC=B1=84=EC=9B=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01VmC34fsxxbweEG3RUjs3Mn --- A00_Common/design/design_page.css | 29 +++ A00_Common/design/design_profile_popout.ts | 129 +++++++++-- A00_Common/design/design_shared.ts | 3 +- .../tester/helper_224_1c_subpanel_popout.cjs | 215 ++++++++++++++++++ .../tester/helper_282_1_profile_toolbar.cjs | 3 + .../tester/test_224_1c_subpanel_popout.py | 107 +++++++++ 6 files changed, 462 insertions(+), 24 deletions(-) create mode 100644 resources/tester/helper_224_1c_subpanel_popout.cjs create mode 100644 resources/tester/test_224_1c_subpanel_popout.py diff --git a/A00_Common/design/design_page.css b/A00_Common/design/design_page.css index 605935355..7ffb67950 100644 --- a/A00_Common/design/design_page.css +++ b/A00_Common/design/design_page.css @@ -55,6 +55,35 @@ font-size: var(--btn-bar-font-size); } +/* 유토곡선 · 배수유역 판 [창 분할](계획서 325-3) — 종단 [창 분할]과 같은 양식 · 유토는 판 좌상단에 띄움. */ +.b05-subpanel__popout { + flex: none; + padding: var(--btn-bar-padding); + font-size: var(--btn-bar-font-size); +} + +.b05-profile__masshaul-overlay > .b05-subpanel__popout { + position: absolute; + top: 4px; + left: var(--spacing-8); + z-index: 4; +} + +/* 새 창으로 뗀 유토곡선 · 배수유역 판 — 창 전체를 채움 · 높이 · 폭 끌기와 접기 손잡이는 감춤. */ +.b05-profile__masshaul-overlay.is-popout, +.b05-drainage.is-popout { + position: relative; + width: 100%; + max-width: none; + height: 100dvh; +} + +.b05-profile__masshaul-overlay.is-popout > .ui-resizer, +.b05-drainage.is-popout > .ui-resizer, +.b05-drainage.is-popout > .ui-workflow-overlay__handle { + display: none; +} + /* 횡단 조정(카드 조정창 사본 · 폼 칸에 옮겨 붙은 행)은 횡단 보기 몫 — 종단 보기에서는 한 벌 폼 안에서 가린다(패널 뿌리 클래스가 보기를 따라 바뀐다). */ .b05-route__panel .b06-adjust-dock, diff --git a/A00_Common/design/design_profile_popout.ts b/A00_Common/design/design_profile_popout.ts index ce69d30d5..304bdb07f 100644 --- a/A00_Common/design/design_profile_popout.ts +++ b/A00_Common/design/design_profile_popout.ts @@ -1,6 +1,7 @@ /* ============================================================================= * A00_Common/design/design_profile_popout.ts * 종단 하단 패널(종단도 · 표 · 유토곡선 · 배수유역도)을 **브라우저 새 창**으로 뺀다(계획서 54-35). + * 유토곡선 · 배수유역 판은 따로 또 뗀다(계획서 325-3) — 같은 몸통(`attachPopout`), 창 이름만 다름. * * 화면 조각(DOM)만 옮긴다 — 코드 · 자료 · 가게는 메인 창 그대로, 같은 함수가 그린다. * 창 전역에 걸린 듣기(방향키 · 포인터 놓기 · 메뉴 닫기)는 메인 창에만 있으므로 새 창에서 난 @@ -13,28 +14,39 @@ import type { DesignProfile } from "./design_profile"; /** 메인 창 문서 · 창에 걸린 듣기가 받아야 할 새 창 이벤트. */ const FORWARD_TYPES = ["keydown", "keyup", "pointerdown", "pointerup", "pointercancel"]; -/** 종단 도구줄 [초기화] 왼쪽 단추(`toolLead`)를 [창 분할] 단추로 꾸민다 — 두 보기가 같은 한 벌을 쓴다. */ -export function attachProfilePopout( - profile: DesignProfile, - slot: HTMLElement, - button: HTMLButtonElement, -): void { - const { root } = profile; +/** 요소가 놓인 창(문서에 안 붙었으면 메인 창). */ +const viewOfNode = (node: Node): Window => node.ownerDocument?.defaultView ?? window; + +/** 조각 하나를 새 창으로 떼는 단추 — 창 이름은 조각마다 달라야 함(같으면 앞 창 body 를 덮어 조각이 사라짐). */ +interface PopoutSpec { + piece: HTMLElement; + button: HTMLButtonElement; + name: string; + features: string; + docTitle: string; + /** 조각을 제자리로 돌림. */ + home: () => void; + /** 뗐다 · 붙였다(조각이 옮겨 간 뒤). */ + onMove?: (popped: boolean) => void; +} + +/** 조각을 새 창으로 떼고 붙이는 몸통 — 새 창은 단추가 놓인 창에서 엶(종단 새 창 안에서 다시 떼기). */ +function attachPopout(profile: DesignProfile, spec: PopoutSpec): void { + const { piece, button } = spec; let popup: Window | null = null; button.type = "button"; - button.className = "ui-btn ui-btn--ghost b05-route-profile__popout"; button.textContent = "창 분할"; - button.title = "종단 하단 패널을 별도 창으로 분할 · 다시 누르면 결합"; button.addEventListener("click", () => (popup && !popup.closed ? closePopup() : open())); const restore = (): void => { - if (!root.classList.contains("is-popout")) return; - root.classList.remove("is-popout"); + if (!piece.classList.contains("is-popout")) return; + piece.classList.remove("is-popout"); button.textContent = "창 분할"; - slot.append(root); + spec.home(); popup = null; - requestAnimationFrame(profile.redraw); + spec.onMove?.(false); + viewOfNode(piece).requestAnimationFrame(profile.redraw); }; const closePopup = (): void => { popup?.close(); @@ -49,28 +61,31 @@ export function attachProfilePopout( }; function open(): void { - const next = window.open("", "aislo-profile-popout", "popup,width=1400,height=720"); + const opener = viewOfNode(button); + const next = opener.open("", spec.name, spec.features); if (!next) { showToast("새 창이 막힘 — 주소창의 팝업 허용을 켠 뒤 다시 누름", "error"); return; } popup = next; + const source = opener.document; const doc = next.document; - doc.title = "종단 — Aislo"; - // 스타일 · 테마 — 메인 창 시트 · html 속성을 그대로 옮겨 같은 색 · 글꼴로 그린다. + doc.title = spec.docTitle; + // 스타일 · 테마 — 단추가 놓인 창의 시트 · html 속성을 그대로 옮겨 같은 색 · 글꼴로 그린다. doc.head.replaceChildren( - ...Array.from(document.querySelectorAll('link[rel="stylesheet"], style'), (node) => + ...Array.from(source.querySelectorAll('link[rel="stylesheet"], style'), (node) => doc.importNode(node, true), ), ); - Array.from(document.documentElement.attributes).forEach((attr) => + Array.from(source.documentElement.attributes).forEach((attr) => doc.documentElement.setAttribute(attr.name, attr.value), ); - doc.body.className = document.body.className; - doc.body.style.cssText = `margin:0;height:100dvh;overflow:hidden;background:${getComputedStyle(document.body).backgroundColor}`; - root.classList.add("is-popout"); + doc.body.className = source.body.className; + doc.body.style.cssText = `margin:0;height:100dvh;overflow:hidden;background:${opener.getComputedStyle(source.body).backgroundColor}`; + piece.classList.add("is-popout"); button.textContent = "창 결합"; - doc.body.replaceChildren(root); + doc.body.replaceChildren(piece); + spec.onMove?.(true); // 새 창 키 · 포인터를 메인 문서로 — 대상은 새 창 요소 그대로(입력칸 판정 · 메뉴 안 판정). FORWARD_TYPES.forEach((type) => @@ -93,7 +108,75 @@ export function attachProfilePopout( new (next as Window & typeof globalThis).ResizeObserver(() => { next.cancelAnimationFrame(pending); pending = next.requestAnimationFrame(profile.redraw); - }).observe(root); + }).observe(piece); next.addEventListener("pagehide", restore); } } + +/** 종단 도구줄 [초기화] 왼쪽 단추(`toolLead`)를 [창 분할] 단추로 꾸민다 — 두 보기가 같은 한 벌을 쓴다. */ +export function attachProfilePopout( + profile: DesignProfile, + slot: HTMLElement, + button: HTMLButtonElement, +): void { + button.className = "ui-btn ui-btn--ghost b05-route-profile__popout"; + button.title = "종단 하단 패널을 별도 창으로 분할 · 다시 누르면 결합"; + attachPopout(profile, { + piece: profile.root, + button, + name: "aislo-profile-popout", + features: "popup,width=1400,height=720", + docTitle: "종단 — Aislo", + home: () => slot.append(profile.root), + }); +} + +/** 유토곡선 · 배수유역 판에 [창 분할] 단추를 단다 — 유토 판 좌상단 · 배수 머리줄 맨 앞(계획서 325-3). */ +export function attachSubPanelPopouts(profile: DesignProfile): void { + const subButton = (title: string): HTMLButtonElement => { + const button = document.createElement("button"); + button.className = "ui-btn ui-btn--ghost b05-subpanel__popout"; + button.title = title; + return button; + }; + + const massOverlay = profile.root.querySelector(".b05-profile__masshaul-overlay"); + const massHandle = profile.root.querySelector(".b05-profile__masshaul-handle"); + if (massOverlay && massHandle) { + const button = subButton("유토곡선 창 분할"); + massOverlay.append(button); + attachPopout(profile, { + piece: massOverlay, + button, + name: "aislo-masshaul-popout", + features: "popup,width=1400,height=420", + docTitle: "유토곡선 — Aislo", + // 제자리 = 유토 손잡이 바로 앞(종단 본문 바닥 겹침 판). + home: () => massHandle.before(massOverlay), + onMove: (popped) => { + // 뗀 동안 메인 손잡이는 숨김 · 같이 줄이기 인라인 높이는 비움(새 창은 창 높이를 다 씀). + massHandle.hidden = popped; + massOverlay.style.height = ""; + }, + }); + } + + const drainage = profile.side; + const header = drainage?.root.querySelector(".b05-drainage__header"); + if (drainage && header) { + const button = subButton("배수유역 창 분할"); + header.prepend(button); + const content = drainage.root.parentElement; + attachPopout(profile, { + piece: drainage.root, + button, + name: "aislo-drainage-popout", + features: "popup,width=1000,height=720", + docTitle: "배수유역 — Aislo", + // 제자리 = 종단 content 끝(종단 본문 오른쪽). + home: () => content?.append(drainage.root), + // 지도 크기 관찰을 놓인 창 것으로(325-1). + onMove: () => drainage.rehome(), + }); + } +} diff --git a/A00_Common/design/design_shared.ts b/A00_Common/design/design_shared.ts index 9296eb899..3374528c1 100644 --- a/A00_Common/design/design_shared.ts +++ b/A00_Common/design/design_shared.ts @@ -24,7 +24,7 @@ import type { RouteProfilePanelCallbacks } from "../../B05_Profile/B05_Profile_U import { drainageSide } from "../../B05_Profile/B05_Profile_UI_Profile_SideTool"; import { createDesignPanel, type DesignPanel } from "./design_panel"; import { createDesignProfile, type DesignProfile } from "./design_profile"; -import { attachProfilePopout } from "./design_profile_popout"; +import { attachProfilePopout, attachSubPanelPopouts } from "./design_profile_popout"; import { saveDesign, type DesignSaveStep } from "./design_save"; import { resetDesign } from "./design_reset"; import type { DesignView } from "./design_dock"; @@ -179,6 +179,7 @@ export function sharedProfile(shared: DesignPageShared): DesignProfile { }); shared.profileSlot.append(profile.root); attachProfilePopout(profile, shared.profileSlot, popoutButton); + attachSubPanelPopouts(profile); shared.profile = profile; return profile; } diff --git a/resources/tester/helper_224_1c_subpanel_popout.cjs b/resources/tester/helper_224_1c_subpanel_popout.cjs new file mode 100644 index 000000000..c48685322 --- /dev/null +++ b/resources/tester/helper_224_1c_subpanel_popout.cjs @@ -0,0 +1,215 @@ +/* 224-1c 유토곡선 · 배수유역 [창 분할] — 진짜 `attachSubPanelPopouts` · `attachProfilePopout` + * (design_profile_popout.ts)를 가짜 DOM · 가짜 창 위에서 돌려 JSON 으로 냄. + * · 단추 자리 · 글 · 클래스 · 툴팁 · 창 이름 셋 다름 + * · 뗌 = 새 창 body · 유토 손잡이 숨김 · 인라인 높이 비움 · 배수 rehome + * · 붙임(단추 · 새 창 닫힘 · dispose) = 제자리(유토 손잡이 앞 · content 끝) + * · 종단 새 창 안의 단추 = 그 창에서 엶 */ +const fs = require("fs"); +const path = require("path"); +const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript")); + +const ROOT = path.join(__dirname, "..", ".."); +const read = (...p) => fs.readFileSync(path.join(ROOT, ...p), "utf8"); + +// ---- 가짜 DOM — 부모 사슬 끝이 어느 문서 body 냐로 ownerDocument 를 정함 ---- +let mainDoc = null; +function el(tag, className = "") { + const node = { + tagName: tag.toUpperCase(), + className, + textContent: "", + title: "", + type: "", + hidden: false, + children: [], + parent: null, + listeners: {}, + style: { height: "", cssText: "" }, + }; + node.classList = { + list: () => node.className.split(/\s+/).filter(Boolean), + contains: (c) => node.classList.list().includes(c), + add: (c) => + !node.classList.contains(c) && (node.className = [...node.classList.list(), c].join(" ")), + remove: (c) => + (node.className = node.classList + .list() + .filter((x) => x !== c) + .join(" ")), + }; + Object.defineProperty(node, "parentElement", { get: () => node.parent }); + Object.defineProperty(node, "ownerDocument", { + get: () => { + let top = node; + while (top.parent) top = top.parent; + return top.doc ?? mainDoc; + }, + }); + node.detach = () => { + if (node.parent) node.parent.children = node.parent.children.filter((c) => c !== node); + node.parent = null; + }; + node.append = (...kids) => + kids.forEach((k) => (k.detach(), (k.parent = node), node.children.push(k))); + node.prepend = (...kids) => + kids.reverse().forEach((k) => (k.detach(), (k.parent = node), node.children.unshift(k))); + node.replaceChildren = (...kids) => { + node.children.slice().forEach((c) => c.detach()); + node.append(...kids); + }; + node.before = (other) => { + const parent = node.parent; + other.detach(); + other.parent = parent; + parent.children.splice(parent.children.indexOf(node), 0, other); + }; + node.querySelector = (sel) => { + const cls = sel.replace(/^\./, ""); + for (const kid of node.children) { + if (kid.classList.contains(cls)) return kid; + const deep = kid.querySelector(sel); + if (deep) return deep; + } + return null; + }; + node.addEventListener = (type, fn) => (node.listeners[type] ??= []).push(fn); + node.click = () => (node.listeners.click ?? []).forEach((fn) => fn({})); + return node; +} +function fakeDoc(view) { + const doc = { title: "", defaultView: view }; + doc.head = el("head"); + doc.body = el("body"); + doc.body.doc = doc; + doc.documentElement = Object.assign(el("html"), { attributes: [], setAttribute() {} }); + doc.querySelectorAll = () => []; + doc.importNode = (n) => n; + doc.createElement = (tag) => el(tag); + return doc; +} +const opened = []; // [여는 창 이름, 새 창 이름] +function fakeWindow(label) { + const win = { label, listeners: {}, closed: false, frames: 0 }; + win.document = fakeDoc(win); + win.addEventListener = (t, fn) => (win.listeners[t] ??= []).push(fn); + win.removeEventListener = () => {}; + win.getComputedStyle = () => ({ backgroundColor: "" }); + win.requestAnimationFrame = () => (win.frames += 1); + win.cancelAnimationFrame = () => {}; + win.ResizeObserver = class { + observe() {} + }; + win.close = () => (win.closed = true); + win.fire = (t) => (win.listeners[t] ?? []).forEach((fn) => fn({})); + win.open = (_url, name) => { + const next = fakeWindow(name); + opened.push([label, name]); + return next; + }; + return win; +} +const main = fakeWindow("main"); +mainDoc = main.document; + +const js = ts.transpileModule(read("A00_Common", "design", "design_profile_popout.ts"), { + compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 }, +}).outputText; +const mod = {}; +new Function("exports", "require", "window", "document", js)( + mod, + () => ({ showToast() {} }), + main, + mainDoc, +); + +// ---- 종단 뿌리 = [content = [bodyWrap = [유토 판, 유토 손잡이], 배수 판 = [머리줄 = [제목]]]] ---- +const slot = el("div"); +mainDoc.body.append(slot); +const root = el("div", "b05-route-profile"); +slot.append(root); +const content = el("div", "b05-route-profile__content"); +const bodyWrap = el("div"); +const massOverlay = el("div", "b05-profile__masshaul-overlay"); +const massHandle = el("div", "b05-profile__masshaul-handle"); +bodyWrap.append(massOverlay, massHandle); +const drainageRoot = el("aside", "b05-drainage"); +const header = el("div", "b05-drainage__header"); +header.append(el("h3")); +drainageRoot.append(header); +content.append(bodyWrap, drainageRoot); +root.append(content); +let rehomes = 0; +let disposed = 0; +const profile = { + root, + redraw() {}, + dispose: () => (disposed += 1), + side: { root: drainageRoot, rehome: () => (rehomes += 1) }, +}; +const profileButton = el("button"); +mod.attachProfilePopout(profile, slot, profileButton); +mod.attachSubPanelPopouts(profile); + +const massButton = massOverlay.children.find((c) => c.tagName === "BUTTON"); +const drainButton = header.children[0]; +const btn = (b) => ({ text: b.textContent, title: b.title, className: b.className, type: b.type }); +const out = { mass: btn(massButton), drainage: btn(drainButton), profile: btn(profileButton) }; + +// 유토 뗌 +massOverlay.style.height = "123px"; +massButton.click(); +const massPopup = massOverlay.parent?.doc?.defaultView; +out.massOpened = { + inPopupBody: massOverlay.parent === massPopup?.document.body, + isPopout: massOverlay.classList.contains("is-popout"), + handleHidden: massHandle.hidden, + inline: massOverlay.style.height, + text: massButton.textContent, + docTitle: massPopup?.document.title, +}; +// 배수 뗌 +drainButton.click(); +const drainPopup = drainageRoot.parent?.doc?.defaultView; +out.drainOpened = { + inPopupBody: drainageRoot.parent === drainPopup?.document.body, + rehomes, + text: drainButton.textContent, +}; +// 유토 새 창 닫힘 → 제자리(손잡이 바로 앞) +massPopup.fire("pagehide"); +out.massClosed = { + order: bodyWrap.children.map((c) => c.className), + handleHidden: massHandle.hidden, + isPopout: massOverlay.classList.contains("is-popout"), + text: massButton.textContent, +}; +// 배수 단추 다시 누름 → 창 닫고 content 끝 +drainButton.click(); +out.drainClosed = { + popupClosed: drainPopup.closed, + last: content.children[content.children.length - 1] === drainageRoot, + rehomes, + text: drainButton.textContent, +}; + +// 종단 새 창 → 그 안에서 유토 뗌 = 종단 새 창이 엶 +profileButton.click(); +const profilePopup = root.parent?.doc?.defaultView; +massButton.click(); +const massPopup2 = massOverlay.parent?.doc?.defaultView; +out.nested = { profileInPopup: !!profilePopup && profilePopup !== main }; +// 페이지 떠남(dispose) → 셋 다 닫히고 제자리 +drainButton.click(); +const drainPopup2 = drainageRoot.parent?.doc?.defaultView; +profile.dispose(); +out.disposed = { + closed: [profilePopup.closed, massPopup2.closed, drainPopup2.closed], + profileInSlot: root.parent === slot, + massBeforeHandle: + bodyWrap.children.indexOf(massOverlay) === bodyWrap.children.indexOf(massHandle) - 1, + drainInContent: drainageRoot.parent === content, + handleHidden: massHandle.hidden, + original: disposed, +}; +out.opened = opened; +process.stdout.write(JSON.stringify(out)); diff --git a/resources/tester/helper_282_1_profile_toolbar.cjs b/resources/tester/helper_282_1_profile_toolbar.cjs index e24598ca9..1fe0671b6 100644 --- a/resources/tester/helper_282_1_profile_toolbar.cjs +++ b/resources/tester/helper_282_1_profile_toolbar.cjs @@ -84,6 +84,9 @@ const window = { addEventListener: (t, fn) => (window.listeners[t] ??= []).push(fn), removeEventListener: () => {}, open: () => popup, + document, + getComputedStyle: () => ({ backgroundColor: "" }), + requestAnimationFrame: (fn) => frames.push(fn), }; const popupListeners = {}; const popup = { diff --git a/resources/tester/test_224_1c_subpanel_popout.py b/resources/tester/test_224_1c_subpanel_popout.py new file mode 100644 index 000000000..df926a112 --- /dev/null +++ b/resources/tester/test_224_1c_subpanel_popout.py @@ -0,0 +1,107 @@ +# -*- coding: utf-8 -*- +"""224-1c(PLAN 325-3) 유토곡선 · 배수유역 [창 분할] — 종단 [창 분할]과 같은 양식 · 따로 새 창. + +- 단추 = 유토 판 좌상단 · 배수 머리줄 맨 앞 · 글 [창 분할]/[창 결합] · 툴팁에 판 이름. +- 창 이름 셋 다름(같으면 앞 창 body 를 덮어 조각이 사라짐). +- 뗌 = 새 창 body · 유토 손잡이 숨김 · 인라인 높이 비움 · 배수 지도 관찰 rehome. +- 붙임(단추 · 새 창 닫힘 · 페이지 떠남) = 제자리 — 유토 손잡이 바로 앞 · 배수 content 끝. +- 종단 새 창 안에서 다시 떼면 그 창이 새 창을 엶(메인 창 팝업 차단 피함). + +`helper_224_1c_subpanel_popout.cjs` 가 진짜 TS 를 가짜 DOM · 가짜 창 위에서 돌림. +""" + +import json +import os +import re +import subprocess +from pathlib import Path + +import pytest + +HERE = os.path.dirname(os.path.abspath(__file__)) +ROOT = Path(HERE).parents[1] +NAMES = ["aislo-profile-popout", "aislo-masshaul-popout", "aislo-drainage-popout"] + + +@pytest.fixture(scope="module") +def r(): + proc = subprocess.run( + ["node", os.path.join(HERE, "helper_224_1c_subpanel_popout.cjs")], + capture_output=True, + text=True, + encoding="utf-8", + timeout=60, + ) + assert proc.returncode == 0, proc.stderr + return json.loads(proc.stdout) + + +def test_단추_글_툴팁_클래스(r): + cls = "ui-btn ui-btn--ghost b05-subpanel__popout" + assert r["mass"] == { + "text": "창 분할", + "title": "유토곡선 창 분할", + "className": cls, + "type": "button", + } + assert r["drainage"] == { + "text": "창 분할", + "title": "배수유역 창 분할", + "className": cls, + "type": "button", + } + assert "b05-route-profile__popout" in r["profile"]["className"] + + +def test_유토_뗌(r): + assert r["massOpened"] == { + "inPopupBody": True, + "isPopout": True, + "handleHidden": True, + "inline": "", + "text": "창 결합", + "docTitle": "유토곡선 — Aislo", + } + + +def test_배수_뗌은_rehome(r): + assert r["drainOpened"] == {"inPopupBody": True, "rehomes": 1, "text": "창 결합"} + + +def test_새_창_닫힘과_단추로_제자리(r): + assert r["massClosed"] == { + "order": ["b05-profile__masshaul-overlay", "b05-profile__masshaul-handle"], + "handleHidden": False, + "isPopout": False, + "text": "창 분할", + } + assert r["drainClosed"] == {"popupClosed": True, "last": True, "rehomes": 2, "text": "창 분할"} + + +def test_창_이름_셋_다름과_여는_창(r): + names = [name for _, name in r["opened"]] + assert set(names) == set(NAMES) and len(set(NAMES)) == 3 + # 종단 새 창 안의 단추는 종단 새 창이 엶 + assert r["opened"][3] == ["aislo-profile-popout", "aislo-masshaul-popout"] + assert r["nested"]["profileInPopup"] is True + + +def test_페이지_떠나면_셋_다_닫고_제자리(r): + assert r["disposed"] == { + "closed": [True, True, True], + "profileInSlot": True, + "massBeforeHandle": True, + "drainInContent": True, + "handleHidden": False, + "original": 1, + } + + +def test_배선과_양식(): + shared = (ROOT / "A00_Common/design/design_shared.ts").read_text(encoding="utf-8") + assert "attachSubPanelPopouts(profile);" in shared + css = (ROOT / "A00_Common/design/design_page.css").read_text(encoding="utf-8") + block = re.search(r"\n\.b05-subpanel__popout \{([^}]*)\}", css).group(1) + assert "var(--btn-bar-padding)" in block and "var(--btn-bar-font-size)" in block + assert ".b05-profile__masshaul-overlay.is-popout," in css + assert ".b05-drainage.is-popout {" in css From 4c6658cf1c08dd761776890a5a42017172da17d2 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 15 Oct 2026 12:32:36 +0900 Subject: [PATCH 17/81] =?UTF-8?q?feat(B05):=20326-1=C2=B7326-2=20=EC=B8=A1?= =?UTF-8?q?=EC=A0=90=20=EC=8A=A4=EB=83=85=20=EC=84=9C=EB=B2=84=20=E2=80=94?= =?UTF-8?q?=20=EC=9A=94=EC=B2=AD=20=EB=AA=A8=EB=8D=B8=20Schema=20=EB=A1=9C?= =?UTF-8?q?=20=EC=AA=BC=EA=B0=AC=20=C2=B7=20=ED=98=B8=20=EC=95=88=20?= =?UTF-8?q?=EC=B8=A1=EC=A0=90=20=ED=95=98=EB=82=98=EB=A5=BC=20=EA=B3=A1?= =?UTF-8?q?=EC=84=A0=20=EC=A4=91=EC=A0=90=EC=9C=BC=EB=A1=9C=20=EC=98=AE?= =?UTF-8?q?=EA=B8=B0=EB=8A=94=20=EC=A7=9D=20=EC=85=88(snap=5Fcurve=5Fstati?= =?UTF-8?q?ons=20=E2=86=94=20snapCurveStations)=20=C2=B7=20[=ED=99=95?= =?UTF-8?q?=EC=9D=B8]=20=EB=95=8C=20=EA=B3=A1=EC=84=A0=ED=91=9C=20station?= =?UTF-8?q?=5Fsnap=20=EC=A0=80=EC=9E=A5=20=C2=B7=20GET=20=EB=8F=8C?= =?UTF-8?q?=EB=A0=A4=EC=A4=8C=20=C2=B7=20=EC=8B=A4=ED=8C=A8=20=EB=90=98?= =?UTF-8?q?=EB=8F=8C=EB=A6=BC=EC=97=90=20=EA=B3=A1=EC=84=A0=ED=91=9C=C2=B7?= =?UTF-8?q?=EB=85=B8=EB=93=9C=20=C2=B7=20=EC=B8=A1=EC=A0=90=20=EC=85=88=20?= =?UTF-8?q?=EB=91=98=20=C2=B7=20=ED=9A=A1=EB=8B=A8=20=EB=AF=B8=EB=A6=AC?= =?UTF-8?q?=EB=B3=B4=EA=B8=B0=20=EB=B0=98=EC=98=81?= 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_01TteK8mfYZetk5P3bEZkyVZ --- B05_Profile/B05_Profile_Engine_Sections.py | 25 +- .../B05_Profile_Engine_Sections_Core.py | 42 +- B05_Profile/B05_Profile_Router_Replan.py | 114 ++--- B05_Profile/B05_Profile_Router_Terrain.py | 11 +- B05_Profile/B05_Profile_Schema.py | 75 +++- B05_Profile/B05_Profile_UI_RouteEdit_Frame.ts | 99 +++++ resources/tester/test_224_2_station_snap.py | 388 ++++++++++++++++++ 7 files changed, 662 insertions(+), 92 deletions(-) create mode 100644 resources/tester/test_224_2_station_snap.py diff --git a/B05_Profile/B05_Profile_Engine_Sections.py b/B05_Profile/B05_Profile_Engine_Sections.py index b9e354689..5dc47a85a 100644 --- a/B05_Profile/B05_Profile_Engine_Sections.py +++ b/B05_Profile/B05_Profile_Engine_Sections.py @@ -79,21 +79,31 @@ def _load_design_curves(project_root: Path) -> list[dict[str, Any]]: 곡선표를 읽으면 있지도 않은 자리에 확폭이 붙는다. 곡선표가 없으면 빈 목록을 돌려주고, 받는 쪽이 옛 방식(측점별 실측 반경)으로 물러선다. """ + curves = _read_design_curve_table(project_root).get("curves") + return ( + [curve for curve in curves if isinstance(curve, dict)] if isinstance(curves, list) else [] + ) + + +def load_station_snap(project_root: Path) -> bool: + """[측점 스냅] 켬 — 곡선표 `"station_snap"`(요구 224-2 · [확인] 때 적힘). 칸 없으면 끔.""" + return _read_design_curve_table(project_root).get("station_snap") is True + + +def _read_design_curve_table(project_root: Path) -> dict[str, Any]: + """설계가 쓰는 곡선표 파일 통째 — 수정본이 있으면 그 곁 것 · 없거나 못 읽으면 빈 dict.""" route_path = planned_route_working_path(project_root) if not route_path.is_file(): route_path = planned_route_initial_path(project_root) target = route_path.with_name(f"{route_path.stem}_curves.json") if not target.is_file(): - return [] + return {} try: data = json.loads(target.read_text(encoding="utf-8")) except (OSError, json.JSONDecodeError): logger.warning("계획노선 곡선표를 읽지 못했습니다: %s", target) - return [] - curves = data.get("curves") if isinstance(data, dict) else None - return ( - [curve for curve in curves if isinstance(curve, dict)] if isinstance(curves, list) else [] - ) + return {} + return data if isinstance(data, dict) else {} # ── 횡단 이름 규칙(한 벌) — 파일 이름 · B07 도면 id · 찾기가 모두 이것을 씀 ──────────── @@ -435,6 +445,7 @@ def run_section_generation( # 곡선부 확폭의 정본 — 설계 곡선표(시·종점·반지름)를 그대로 쓴다. 없으면 빈 목록이라 # 종전의 측점별 실측 반경으로 물러선다. design_curves=_load_design_curves(project_root), + station_snap=load_station_snap(project_root), ) stage_root = project_root / _STAGE_SUBDIR @@ -548,6 +559,8 @@ def sample_structure_sections( crs=crs, # 비정규(구조물) 측점도 같은 곡선표를 봐야 규칙 측점과 확폭이 어긋나지 않는다. design_curves=_load_design_curves(project_root), + # 규칙 측점과 같은 스냅 — 한쪽만 켜면 [저장] 때 옮기기 전 격자(640)가 되살아남. + station_snap=load_station_snap(project_root), ) def structural(entry: dict[str, Any]) -> bool: diff --git a/B05_Profile/B05_Profile_Engine_Sections_Core.py b/B05_Profile/B05_Profile_Engine_Sections_Core.py index 2505e8535..22e567aaa 100644 --- a/B05_Profile/B05_Profile_Engine_Sections_Core.py +++ b/B05_Profile/B05_Profile_Engine_Sections_Core.py @@ -6,6 +6,9 @@ ⚠ 파이썬·TS 짝 — `_interpolate_xy`·`_tangent_at` ↔ `B05_Profile_UI_RouteEdit_Frame.ts` (`stationFrameAt`). 거울 시험 `resources/tester/test_b05_routeedit_frame_mirror.py`. +⚠ 파이썬·TS 짝 — 측점 스냅 `_chainages` · `_design_curve_spans` · `snap_curve_stations` ↔ +`B05_Profile_UI_RouteEdit_Frame.ts` `stationsAlong` · `curveSpans` · `snapCurveStations`. 거울 시험 +`resources/tester/test_224_2_station_snap.py`. """ import math @@ -275,6 +278,35 @@ def _design_curve_spans( return sorted(spans) +#: 호 끝(BC · EC)에서 이만큼 안쪽 측점만 「호 안」 — 관 · 구조물 겹침 판정과 같은 값(m). +CURVE_STATION_EDGE_M = 0.005 + + +def snap_curve_stations( + stations: np.ndarray | list[float], + spans: list[tuple[float, ...]], +) -> np.ndarray: + """측점 스냅(요구 224-2) — 호(BC~EC) 안 측점 중 곡선 중점 MC 에 가장 가까운 하나를 정수 MC + (floor(MC+0.5))로 옮김. 측점 없는 호는 그대로 · 옮긴 자리가 다른 측점과 같은 m 이면 하나로. + + `spans` = `_design_curve_spans` 꼴(앞 두 칸 = BC · EC 누가거리). + """ + values = [float(value) for value in stations] + for span in spans: + start, end = float(span[0]), float(span[1]) + middle = (start + end) / 2.0 + inside = [ + index + for index, value in enumerate(values) + if start + CURVE_STATION_EDGE_M < value < end - CURVE_STATION_EDGE_M + ] + if not inside: + continue + nearest = min(inside, key=lambda index: abs(values[index] - middle)) + values[nearest] = float(math.floor(middle + 0.5)) + return np.unique(np.asarray(values, dtype=np.float64)) + + def _design_curve_widenings( station_chainage: np.ndarray, spans: list[tuple[float, float, float, str]], @@ -319,8 +351,12 @@ def generate_sections( source_snapshot: dict[str, Any] | None = None, crs: str | None = None, design_curves: list[dict[str, Any]] | None = None, + station_snap: bool = False, ) -> dict[str, Any]: - """확정 경로로 CAD 인계 가능한 종단·횡단 원시 데이터를 생성한다.""" + """확정 경로로 CAD 인계 가능한 종단·횡단 원시 데이터를 생성한다. + + `station_snap` — 호 안 측점 하나를 곡선 중점으로 옮김(`snap_curve_stations` · 요구 224-2). + """ options = options or SectionGenerationOptions() options.validate() points, route_chainage = _clean_polyline(np.asarray(polyline, dtype=np.float64)) @@ -331,6 +367,9 @@ def generate_sections( long_z, long_valid = sampler.sample_xy(long_xy) station_chainage = _chainages(total, options.station_interval_m, options.include_endpoint) + spans = _design_curve_spans(points, route_chainage, design_curves or []) + if station_snap: + station_chainage = snap_curve_stations(station_chainage, spans) # 규칙(격자) 측점 chainage 집합 — 비정규와 겹칠 때 규칙 측점을 우선 판정하는 데 쓴다. regular_rounded = {round(float(value), 6) for value in station_chainage} # 비정규 측점(구조물)을 격자 측점 배열에 병합한다. 정렬·중복 제거만 하면 이후 접선·횡단 @@ -372,7 +411,6 @@ def generate_sections( # 다시 재면 같은 곡선 안에서도 확폭이 갈리고, 곡선이 측점 간격보다 짧으면 통째로 # 빠진다(실측: 설계 12m 곡선에 1.50m 이 붙고 곡선 밖 직선까지 흘러나갔다). # 곡선표가 없는 옛 프로젝트만 종전의 측점별 실측으로 물러선다. - spans = _design_curve_spans(points, route_chainage, design_curves or []) if spans: plan_radii, plan_outer_sides, plan_widenings = _design_curve_widenings( station_chainage, spans diff --git a/B05_Profile/B05_Profile_Router_Replan.py b/B05_Profile/B05_Profile_Router_Replan.py index 10e2f4559..67c62b68f 100644 --- a/B05_Profile/B05_Profile_Router_Replan.py +++ b/B05_Profile/B05_Profile_Router_Replan.py @@ -41,10 +41,17 @@ from uuid import UUID from fastapi import APIRouter from fastapi.responses import JSONResponse -from pydantic import AliasChoices, BaseModel, Field from B03_FileInput.B03_FileInput_Repository import get_project_storage_relative_path from B05_Profile.B05_Profile_Engine_RouteInitial import read_initial_choice, write_initial_choice +from B05_Profile.B05_Profile_Engine_Sections import load_station_snap +from B05_Profile.B05_Profile_Schema import ( # noqa: F401 — 부르는 곳이 `replan.` 으로 씀 + RouteInitialCriteria, + RouteInitialRequest, + RouteInitialWeights, + RouteReplanRequest, + RouteVertexInput, +) from common_util.common_util_initial_snapshot import design_route_csv_path from common_util.common_util_route_geometry import ( densify_route, @@ -69,74 +76,6 @@ router = APIRouter(prefix="/api/projects", tags=["B05 Route Design"]) _PROJECT_PATH_MISSING = {"status": "error", "message": "프로젝트 저장 경로를 찾을 수 없습니다."} -class RouteVertexInput(BaseModel): - """계획노선 꺾임점 하나 — 사업지 좌표계(m). - - 사용자 편집을 그대로 나른다(2026-09-07 사용자 지시) — - · **직선 삭제·추가** = 이 목록에서 점을 빼거나 더하는 것. - · **곡선 삭제·추가** = `curve` 를 끄고 켜는 것. - · **반지름 변경** = `radius_m` 을 주는 것. 안 주면 서버가 고른다. - """ - - x: float - y: float - curve: bool = True - """이 자리에 곡선을 둘지. 끄면 직선이 그대로 꺾인다.""" - radius_m: float | None = None - """못박을 곡선 반지름(m). 없으면 예정노선에 맞추거나 법정 하한을 쓴다.""" - - -class RouteInitialWeights(BaseModel): - """비중 갈래의 세 목표 비중 — 합 100(PLAN 20장 · 기본 50/25/25).""" - - expected: float = Field(50.0, ge=0, le=100) - earthwork: float = Field(25.0, ge=0, le=100) - grade: float = Field(25.0, ge=0, le=100) - - -class RouteInitialCriteria(BaseModel): - """공통 제약 겹침 값 — 화면 네 칸(PLAN 20장 확정). 비우면(None) 프로젝트 기본값을 쓴다. - - 기본값(2026-09-29 사용자 결정 — 지식DB 후보 표 그대로 확정) — 종단기울기 상한 · 최소 - 곡선반지름은 프로젝트 설계속도 · 지형에 따른 표값, 계류 이격 100 m, 기울기 잴 지반 5 m 고름. - """ - - max_grade_pct: float | None = Field(None, ge=0, le=100) - min_radius_m: float | None = Field(None, ge=0) - stream_offset_m: float | None = Field(None, ge=0) - grade_smooth_m: float | None = Field(None, ge=0) - # 영선 노선 목표 종단기울기(%) — 비우면 종단기울기 상한 · 상한보다 크면 상한(PLAN 23-5) - target_grade_pct: float | None = Field(None, ge=0, le=100) - - -class RouteInitialRequest(BaseModel): - """초기 계획노선 갈래 — follow(예상노선 추종) · gradeline(영선) · earthwork · gentle · - safe(안전 우선) · crossings(계류 횡단 최소) · cost(공사비 어림 최소) · weighted(PLAN 23-5).""" - - mode: Literal[ - "follow", "gradeline", "earthwork", "gentle", "safe", "crossings", "cost", "weighted" - ] = Field("follow", validation_alias=AliasChoices("logic", "mode")) - weights: RouteInitialWeights | None = None - criteria: RouteInitialCriteria | None = None - # 추천 점수에 균형 종단(PLAN 63-4)을 넣을지 — 끄면 원지반만(63-4 전 점수). 파일에 안 적힘. - with_profile: bool = True - # 분석 지형(PLAN 133-1) — None = 지금 설계 지형 · [확인] 때 설계 지형이 됨(133-2). - terrain: Literal["las", "sheet"] | None = None - - -class RouteReplanRequest(BaseModel): - """고친 계획노선. 정점은 시점 → 종점 순서(사업지 좌표계 m).""" - - vertices: list[RouteVertexInput] = Field(default_factory=list) - initial: RouteInitialRequest | None = None - """이 노드를 낳은 초기값(갈래 · 비중) — 성공하면 프로젝트에 적어 둔다(현재 초기값).""" - balanced_profile: bool = False - """「종단 반영」 켬으로 [적용]한 추천 그대로인가 — 켜면 서버가 균형 종단을 다시 셈해 - 1차 종단으로 쓴다(PLAN 74-4). 값은 받지 않는다 — 켬/끔 표시만.""" - terrain: Literal["las", "sheet"] | None = None - """편집 창에서 고른 분석 지형 = 설계 지형(PLAN 133-2). None 이면 지금 설계 지형 그대로.""" - - async def _project_paths(project_id: UUID) -> tuple[Path, str] | None: """(프로젝트 실경로, 저장소 상대경로). 없으면 None.""" pool = get_db_pool() @@ -305,9 +244,12 @@ def _write_planned_polyline( simplify: bool = True, curve_flags: list[bool] | None = None, radii: list[float | None] | None = None, + station_snap: bool | None = None, ) -> dict: """점 묶음을 폴리라인으로 바꿔 CSV 로 쓴다. 노드 요약을 돌려준다. + `station_snap` — [확인] 때만 곡선표에 적음(요구 224-2). None 이면 칸을 안 둠(끔). + **노드도 함께 남긴다** — 노드는 폴리라인에서 되뽑을 수 없다. 폴리라인에는 원호 위 점이 섞여 있어 다시 단순화하면 꺾임점이 조금씩 지워지고, 그것을 반복하면 [확인]을 누를 때마다 선이 깎인다(2026-09-07 실측: 정점 136 → 134 → 119). 낳은 값을 그대로 보관한다. @@ -321,13 +263,13 @@ def _write_planned_polyline( ) write_route_csv(path, [{"x": x, "y": y} for x, y in result.vertices]) write_route_csv(_nodes_path(path), [{"x": node.x, "y": node.y} for node in result.nodes]) - _curves_path(path).write_text( - json.dumps( - {"min_radius_m": round(radius_m, 3), "curves": [c.as_dict() for c in result.curves]}, - ensure_ascii=False, - ), - encoding="utf-8", - ) + table: dict[str, Any] = { + "min_radius_m": round(radius_m, 3), + "curves": [c.as_dict() for c in result.curves], + } + if station_snap is not None: + table["station_snap"] = bool(station_snap) + _curves_path(path).write_text(json.dumps(table, ensure_ascii=False), encoding="utf-8") return { "nodes": len(result.nodes), "curves": result.curve_count, @@ -572,6 +514,7 @@ async def read_route_plan(project_id: UUID) -> dict[str, Any] | JSONResponse: "curve_count": len(saved_curves) if saved_curves else outline.curve_count, "violation_count": outline.violation_count, "edited": bool(working), + "station_snap": await asyncio.to_thread(load_station_snap, project_root), "current_initial": await asyncio.to_thread(read_initial_choice, project_root), "initial_route": await asyncio.to_thread(_initial_shape, project_root, expected, radius_m), } @@ -604,11 +547,17 @@ async def replan_route( radii = [vertex.radius_m for vertex in request.vertices] # 체인이 끊기면 되돌릴 수 있게 이전 수정본을 손에 쥔다 — 실패했는데 노선만 바뀌면 # 종횡단과 어긋난 채 굳고, 다시 누를수록 어긋남이 쌓인다(2026-09-06 실측). + # 곡선표 · 노드도 함께 — 남기면 되돌린 노선에 새 곡선표(스냅 켬끔)가 붙음. working_path = planned_route_working_path(project_root) - previous = working_path.read_bytes() if working_path.is_file() else None + keep = (working_path, _curves_path(working_path), _nodes_path(working_path)) + previous = {path: path.read_bytes() if path.is_file() else None for path in keep} summary = await asyncio.to_thread( functools.partial( - _write_planned_polyline, simplify=False, curve_flags=curve_flags, radii=radii + _write_planned_polyline, + simplify=False, + curve_flags=curve_flags, + radii=radii, + station_snap=request.station_snap, ), working_path, nodes, @@ -633,10 +582,11 @@ async def replan_route( ) if "error" in result: # 노선을 되돌린다 — 실패했는데 새 노선만 남으면 화면·정본이 어긋난 채 굳는다. - if previous is None: - working_path.unlink(missing_ok=True) - else: - working_path.write_bytes(previous) + for path, saved in previous.items(): + if saved is None: + path.unlink(missing_ok=True) + else: + path.write_bytes(saved) logger.warning( "계획노선 갈아 끼우기 실패 — 노선을 되돌렸습니다: project_id=%s 사유=%s", project_id, diff --git a/B05_Profile/B05_Profile_Router_Terrain.py b/B05_Profile/B05_Profile_Router_Terrain.py index 559d64724..197578e4b 100644 --- a/B05_Profile/B05_Profile_Router_Terrain.py +++ b/B05_Profile/B05_Profile_Router_Terrain.py @@ -133,6 +133,8 @@ class CrossPreviewRequest(BaseModel): #: 횡단 반폭(m) — 화면이 적용해 둔 값. 비면 서버 기본값. cross_half_width_m: float | None = Field(None, gt=0) terrain: AnalysisTerrain | None = None + #: [측점 스냅] 켬(요구 224-2) — 편집 창 단추 값 그대로. + station_snap: bool = False def _cross_preview( @@ -177,7 +179,14 @@ def _cross_preview( else defaults.station_interval_m, cross_half_width_m=request.cross_half_width_m or defaults.cross_half_width_m, ) - result = generate_sections(built.vertices, sampler, options) + # 곡선표 = 방금 지은 노선의 곡선 — 스냅 자리 · 확폭이 [확인] 뒤 서버 정본과 같은 규칙. + result = generate_sections( + built.vertices, + sampler, + options, + design_curves=[curve.as_dict() for curve in built.curves], + station_snap=request.station_snap, + ) sections = result["cross_sections"] if not sections: return None diff --git a/B05_Profile/B05_Profile_Schema.py b/B05_Profile/B05_Profile_Schema.py index 0578c7334..5221c9d7d 100644 --- a/B05_Profile/B05_Profile_Schema.py +++ b/B05_Profile/B05_Profile_Schema.py @@ -2,7 +2,7 @@ from typing import Any, Literal -from pydantic import BaseModel, ConfigDict, Field, model_validator +from pydantic import AliasChoices, BaseModel, ConfigDict, Field, model_validator from common_util.common_util_surface_sampler import SurfaceFilter, SurfaceMethod from config.config_system import ( @@ -309,3 +309,76 @@ class RouteLatestResponse(BaseModel): route_points: list[dict[str, Any]] = Field(default_factory=list) surface_params: dict[str, Any] route_params: dict[str, Any] | None = None + + +# ── 계획노선 편집 요청(`B05_Profile_Router_Replan`) ───────────────────────────── + + +class RouteVertexInput(BaseModel): + """계획노선 꺾임점 하나 — 사업지 좌표계(m). + + 사용자 편집을 그대로 나른다(2026-09-07 사용자 지시) — + · **직선 삭제·추가** = 이 목록에서 점을 빼거나 더하는 것. + · **곡선 삭제·추가** = `curve` 를 끄고 켜는 것. + · **반지름 변경** = `radius_m` 을 주는 것. 안 주면 서버가 고른다. + """ + + x: float + y: float + curve: bool = True + """이 자리에 곡선을 둘지. 끄면 직선이 그대로 꺾인다.""" + radius_m: float | None = None + """못박을 곡선 반지름(m). 없으면 예정노선에 맞추거나 법정 하한을 쓴다.""" + + +class RouteInitialWeights(BaseModel): + """비중 갈래의 세 목표 비중 — 합 100(PLAN 20장 · 기본 50/25/25).""" + + expected: float = Field(50.0, ge=0, le=100) + earthwork: float = Field(25.0, ge=0, le=100) + grade: float = Field(25.0, ge=0, le=100) + + +class RouteInitialCriteria(BaseModel): + """공통 제약 겹침 값 — 화면 네 칸(PLAN 20장 확정). 비우면(None) 프로젝트 기본값을 쓴다. + + 기본값(2026-09-29 사용자 결정 — 지식DB 후보 표 그대로 확정) — 종단기울기 상한 · 최소 + 곡선반지름은 프로젝트 설계속도 · 지형에 따른 표값, 계류 이격 100 m, 기울기 잴 지반 5 m 고름. + """ + + max_grade_pct: float | None = Field(None, ge=0, le=100) + min_radius_m: float | None = Field(None, ge=0) + stream_offset_m: float | None = Field(None, ge=0) + grade_smooth_m: float | None = Field(None, ge=0) + # 영선 노선 목표 종단기울기(%) — 비우면 종단기울기 상한 · 상한보다 크면 상한(PLAN 23-5) + target_grade_pct: float | None = Field(None, ge=0, le=100) + + +class RouteInitialRequest(BaseModel): + """초기 계획노선 갈래 — follow(예상노선 추종) · gradeline(영선) · earthwork · gentle · + safe(안전 우선) · crossings(계류 횡단 최소) · cost(공사비 어림 최소) · weighted(PLAN 23-5).""" + + mode: Literal[ + "follow", "gradeline", "earthwork", "gentle", "safe", "crossings", "cost", "weighted" + ] = Field("follow", validation_alias=AliasChoices("logic", "mode")) + weights: RouteInitialWeights | None = None + criteria: RouteInitialCriteria | None = None + # 추천 점수에 균형 종단(PLAN 63-4)을 넣을지 — 끄면 원지반만(63-4 전 점수). 파일에 안 적힘. + with_profile: bool = True + # 분석 지형(PLAN 133-1) — None = 지금 설계 지형 · [확인] 때 설계 지형이 됨(133-2). + terrain: Literal["las", "sheet"] | None = None + + +class RouteReplanRequest(BaseModel): + """고친 계획노선. 정점은 시점 → 종점 순서(사업지 좌표계 m).""" + + vertices: list[RouteVertexInput] = Field(default_factory=list) + initial: RouteInitialRequest | None = None + """이 노드를 낳은 초기값(갈래 · 비중) — 성공하면 프로젝트에 적어 둔다(현재 초기값).""" + balanced_profile: bool = False + """「종단 반영」 켬으로 [적용]한 추천 그대로인가 — 켜면 서버가 균형 종단을 다시 셈해 + 1차 종단으로 쓴다(PLAN 74-4). 값은 받지 않는다 — 켬/끔 표시만.""" + terrain: Literal["las", "sheet"] | None = None + """편집 창에서 고른 분석 지형 = 설계 지형(PLAN 133-2). None 이면 지금 설계 지형 그대로.""" + station_snap: bool = False + """[측점 스냅] 켬(요구 224-2) — 곡선표 `"station_snap"` 으로 남아 측점 셈이 읽음.""" diff --git a/B05_Profile/B05_Profile_UI_RouteEdit_Frame.ts b/B05_Profile/B05_Profile_UI_RouteEdit_Frame.ts index f7e1f4b56..67bc9aeb8 100644 --- a/B05_Profile/B05_Profile_UI_RouteEdit_Frame.ts +++ b/B05_Profile/B05_Profile_UI_RouteEdit_Frame.ts @@ -5,6 +5,10 @@ * ⚠ 파이썬·TS 짝 — `B05_Profile_Engine_Sections_Core.py` 의 `_interpolate_xy`·`_tangent_at` * (`generate_sections` 가 측점 자리·좌측 축을 내는 식). 한쪽을 고치면 다른 쪽도 고치고 * 거울 시험 `resources/tester/test_b05_routeedit_frame_mirror.py` 를 돌림. + * + * ⚠ 파이썬·TS 짝 — 측점 스냅(요구 224-2) `stationsAlong` · `curveSpans` · `snapCurveStations` + * ↔ `B05_Profile_Engine_Sections_Core.py` `_chainages` · `_design_curve_spans` · + * `snap_curve_stations`. 거울 시험 `resources/tester/test_224_2_station_snap.py`. * ========================================================================== */ type Vertex = [number, number]; @@ -75,3 +79,98 @@ export function stationFrameAt( const tangent: Vertex = [(to[0] - from[0]) / length, (to[1] - from[1]) / length]; return { origin: interpolate(points, chainage, chainageM), left: [-tangent[1], tangent[0]] }; } + +/** 곡선표 시 · 종점이 노선에서 이보다 멀면 그 노선 곡선이 아님(m) — 파이썬 같은 이름. */ +export const DESIGN_CURVE_MATCH_TOLERANCE_M = 5.0; +/** 호 끝(BC · EC)에서 이만큼 안쪽 측점만 「호 안」(m) — 파이썬 같은 이름. */ +export const CURVE_STATION_EDGE_M = 0.005; + +/** 곡선표 한 줄에서 스냅이 읽는 칸 — 서버 곡선표 · `EditedCurve` 모두 이 꼴을 품음. */ +export interface CurveEnds { + start?: ReadonlyArray | null; + apex?: ReadonlyArray | null; + end?: ReadonlyArray | null; + radius_m?: number | null; +} + +/** 규칙 측점 누가거리 — 0 · 간격 · 2·간격 · … (+ 끝 자투리 `includeEndpoint`). `_chainages` 짝. */ +export function stationsAlong( + line: ReadonlyArray, + intervalM: number, + includeEndpoint: boolean, +): number[] { + const { chainage } = cleanPolyline(line); + const total = chainage.length ? chainage[chainage.length - 1] : 0; + const values: number[] = []; + if (intervalM > 0) + for (let i = 0; i * intervalM < total + 1e-9; i += 1) values.push(i * intervalM); + if (!values.length || Math.abs(values[0]) > 1e-9) values.unshift(0); + if (includeEndpoint && total - values[values.length - 1] > 1e-6) values.push(total); + return values; +} + +/** 점을 노선 위로 내려 [떨어진 거리, 누가거리] — `_project_chainage` 짝(같으면 앞 선분). */ +function projectChainage( + points: Vertex[], + chainage: number[], + xy: ReadonlyArray, +): [number, number] { + let bestGap = Infinity; + let bestAt = 0; + for (let index = 0; index < points.length - 1; index += 1) { + const [sx, sy] = points[index]; + const vx = points[index + 1][0] - sx; + const vy = points[index + 1][1] - sy; + const length2 = vx * vx + vy * vy; + const safe = length2 > 1e-12 ? length2 : 1; + const ratio = Math.min(1, Math.max(0, ((xy[0] - sx) * vx + (xy[1] - sy) * vy) / safe)); + const gap = Math.hypot(sx + vx * ratio - xy[0], sy + vy * ratio - xy[1]); + if (gap < bestGap) { + bestGap = gap; + bestAt = chainage[index] + (chainage[index + 1] - chainage[index]) * ratio; + } + } + return [bestGap, bestAt]; +} + +/** 곡선표 → [BC, EC] 누가거리(앞쪽부터) — 노선 밖 곡선 · 방향 못 재는 곡선은 버림. + * `_design_curve_spans` 짝(반지름 · 바깥쪽 칸은 스냅이 안 써 뺌). */ +export function curveSpans( + line: ReadonlyArray, + curves: ReadonlyArray, +): Array<[number, number]> { + const { points, chainage } = cleanPolyline(line); + if (points.length < 2) return []; + const spans: Array<[number, number]> = []; + for (const { start, apex, end, radius_m } of curves) { + if (!start?.length || !apex?.length || !end?.length || typeof radius_m !== "number") continue; + const [startGap, startAt] = projectChainage(points, chainage, start); + const [endGap, endAt] = projectChainage(points, chainage, end); + if (Math.max(startGap, endGap) > DESIGN_CURVE_MATCH_TOLERANCE_M) continue; + const cross = + (apex[0] - start[0]) * (end[1] - apex[1]) - (apex[1] - start[1]) * (end[0] - apex[0]); + if (Math.abs(cross) < 1e-12) continue; + spans.push([Math.min(startAt, endAt), Math.max(startAt, endAt)]); + } + return spans.sort((a, b) => a[0] - b[0] || a[1] - b[1]); +} + +/** 측점 스냅 — 호 안 측점 중 곡선 중점 MC 에 가장 가까운 하나를 floor(MC+0.5) 로 옮김 · + * 측점 없는 호는 그대로 · 같은 m 는 하나로(오름차순). `snap_curve_stations` 짝. */ +export function snapCurveStations( + stations: ReadonlyArray, + spans: ReadonlyArray, +): number[] { + const values = [...stations]; + for (const [start, end] of spans) { + const middle = (start + end) / 2; + let nearest = -1; + values.forEach((value, index) => { + if (value <= start + CURVE_STATION_EDGE_M || value >= end - CURVE_STATION_EDGE_M) return; + if (nearest < 0 || Math.abs(value - middle) < Math.abs(values[nearest] - middle)) + nearest = index; + }); + if (nearest >= 0) values[nearest] = Math.floor(middle + 0.5); + } + return [...new Set(values)].sort((a, b) => a - b); +} diff --git a/resources/tester/test_224_2_station_snap.py b/resources/tester/test_224_2_station_snap.py new file mode 100644 index 000000000..2e123b818 --- /dev/null +++ b/resources/tester/test_224_2_station_snap.py @@ -0,0 +1,388 @@ +"""요구 224-2 · PLAN 326 — 계획노선 편집 [측점 스냅]. + +짝: 파이썬 `B05_Profile_Engine_Sections_Core` (`_chainages` · `_design_curve_spans` · +`snap_curve_stations`) ↔ TS `B05_Profile_UI_RouteEdit_Frame.ts` (`stationsAlong` · +`curveSpans` · `snapCurveStations`). 켜면 호(BC~EC) 안 측점 중 곡선 중점 MC 에 가장 가까운 +하나를 floor(MC+0.5) 로 옮김 · 측점 없는 호는 안 심음 · 같은 m 하나로. +""" + +from __future__ import annotations + +import json +import math +import re +import subprocess +import sys +from pathlib import Path + +import numpy as np +import pytest + +ROOT = Path(__file__).resolve().parents[2] +if str(ROOT) not in sys.path: + sys.path.insert(0, str(ROOT)) + +from B05_Profile import B05_Profile_Engine_Sections_Core as core # noqa: E402 + +TS_FILE = ROOT / "B05_Profile" / "B05_Profile_UI_RouteEdit_Frame.ts" +TSC = ROOT / "config" / "node_modules" / "typescript" / "bin" / "tsc" + +GRID = [float(value) for value in range(0, 1001, 20)] + + +def _snap(spans: list[tuple[float, float]], stations: list[float] | None = None) -> list[float]: + return [float(v) for v in core.snap_curve_stations(stations or GRID, spans)] + + +# ── 파이썬 규칙 ──────────────────────────────────────────────────────────────── + + +def test_곡선_없음_그대로() -> None: + assert _snap([]) == GRID + + +def test_측점_없는_호는_안_심음() -> None: + assert _snap([(621.0, 639.0)]) == GRID + + +def test_호_안_하나를_정수_MC_로() -> None: + out = _snap([(612.0, 634.0)]) # MC 623 + assert 623.0 in out and 620.0 not in out and len(out) == len(GRID) + + +def test_호_안_둘이면_MC_에_가까운_하나만() -> None: + out = _snap([(616.1, 646.5)]) # MC 631.3 — 640(8.7) 이 620(11.3) 보다 가까움 + assert 631.0 in out and 640.0 not in out and 620.0 in out + + +def test_BC_위_측점은_호_밖() -> None: + assert _snap([(620.0, 630.0)]) == GRID + assert _snap([(610.0, 620.0)]) == GRID # EC 위도 밖 + + +def test_반_올림() -> None: + out = _snap([(611.0, 640.0)]) # MC 625.5 → 626 · 640 = EC 위라 밖 + assert 626.0 in out and 620.0 not in out and 640.0 in out + + +def test_같은_m_는_하나로() -> None: + # 20.3 이 MC 20.35 → 20 으로 옮겨 이웃 20 과 같은 m — 하나만 남음 + assert _snap([(20.1, 20.6)], [0.0, 20.0, 20.3, 40.0]) == [0.0, 20.0, 40.0] + + +# ── 파이썬 ↔ TS 거울 ───────────────────────────────────────────────────────── + + +def _arc(cx: float, cy: float, radius: float, start: float, end: float, count: int) -> list: + return [ + [cx + radius * math.cos(angle), cy + radius * math.sin(angle)] + for angle in np.linspace(start, end, count) + ] + + +def _bend_route() -> tuple[list, list]: + """직선 300 m → R 40 호 → 직선 — 곡선표 한 줄 + 노선 밖 곡선 · 방향 없는 곡선.""" + arc = _arc(300.0, 40.0, 40.0, -math.pi / 2, 0.3, 60) + line = [[0.0, 0.0], *arc, [arc[-1][0] - 200 * math.sin(0.3), arc[-1][1] + 200 * math.cos(0.3)]] + curves = [ + {"start": arc[0], "apex": [340.0, 0.0], "end": arc[-1], "radius_m": 40.0}, + {"start": [0.0, 500.0], "apex": [10.0, 510.0], "end": [20.0, 500.0], "radius_m": 30.0}, + {"start": [50.0, 0.0], "apex": [60.0, 0.0], "end": [70.0, 0.0], "radius_m": 30.0}, + {"start": [80.0, 0.0], "apex": [90.0, 1.0], "end": [100.0, 0.0], "radius_m": None}, + ] + return line, curves + + +CASES = [ + {"line": [[0, 0], [1000, 0]], "interval": 20, "end": True, "curves": []}, + { + "line": [[0, 0], [1000.3, 0]], + "interval": 20, + "end": True, + "curves": [{"start": [616.1, 0], "apex": [631.3, 3], "end": [646.5, 0], "radius_m": 40}], + }, + { + "line": [[0, 0], [500, 0], [500, 0], [700, 10]], + "interval": 20, + "end": False, + "curves": [ + {"start": [611, 0], "apex": [625.5, -2], "end": [640, 0], "radius_m": 25}, + {"start": [621, 0], "apex": [630, 2], "end": [639, 0], "radius_m": 25}, + {"start": [100, 0], "apex": [110, 2], "end": [112, 0], "radius_m": 25}, + ], + }, + {"line": _bend_route()[0], "interval": 20, "end": True, "curves": _bend_route()[1]}, + {"line": _bend_route()[0], "interval": 7.5, "end": True, "curves": _bend_route()[1]}, +] + +_RUNNER = """ +import { readFileSync, writeFileSync } from "node:fs"; +import * as frame from "./B05_Profile_UI_RouteEdit_Frame.js"; +const cases = JSON.parse(readFileSync(process.argv[2], "utf8")); +const out = { + tolerance: frame.DESIGN_CURVE_MATCH_TOLERANCE_M, + edge: frame.CURVE_STATION_EDGE_M, + rows: cases.map((item) => { + const grid = frame.stationsAlong(item.line, item.interval, item.end); + const spans = frame.curveSpans(item.line, item.curves); + return { grid, spans, snapped: frame.snapCurveStations(grid, spans) }; + }), +}; +writeFileSync(process.argv[3], JSON.stringify(out)); +""" + + +def _python_row(case: dict) -> dict: + points, route_chainage = core._clean_polyline(np.asarray(case["line"], dtype=np.float64)) + grid = core._chainages(float(route_chainage[-1]), float(case["interval"]), case["end"]) + spans = core._design_curve_spans(points, route_chainage, case["curves"]) + return { + "grid": [float(v) for v in grid], + "spans": [[span[0], span[1]] for span in spans], + "snapped": [float(v) for v in core.snap_curve_stations(grid, spans)], + } + + +def _ts_rows(tmp_path: Path) -> dict: + out = tmp_path / "js" + subprocess.run( # noqa: S603 — 고정 실행 파일 + [ + "node", + str(TSC), + str(TS_FILE), + "--outDir", + str(out), + "--module", + "esnext", + "--target", + "es2022", + "--moduleResolution", + "bundler", + "--ignoreConfig", + ], + cwd=str(ROOT), + check=True, + capture_output=True, + ) + for emitted in out.glob("*.js"): + emitted.write_text( + re.sub(r'(from\s+"\./[^"]+)"', r'\1.js"', emitted.read_text(encoding="utf-8")), + encoding="utf-8", + ) + (out / "runner.mjs").write_text(_RUNNER, encoding="utf-8") + payload = tmp_path / "input.json" + result = tmp_path / "output.json" + payload.write_text(json.dumps(CASES), encoding="utf-8") + subprocess.run( # noqa: S603 + ["node", str(out / "runner.mjs"), str(payload), str(result)], + cwd=str(ROOT), + check=True, + capture_output=True, + ) + return json.loads(result.read_text(encoding="utf-8")) + + +@pytest.mark.skipif(not TSC.is_file(), reason="프론트엔드 의존성 미설치") +def test_스냅_셈이_두_쪽에서_같다(tmp_path: Path) -> None: + actual = _ts_rows(tmp_path) + assert actual["tolerance"] == core.DESIGN_CURVE_MATCH_TOLERANCE_M + assert actual["edge"] == core.CURVE_STATION_EDGE_M + moved = 0 + for index, case in enumerate(CASES): + expected = _python_row(case) + row = actual["rows"][index] + assert row["grid"] == pytest.approx(expected["grid"], abs=1e-9), f"사례 {index} 격자" + assert len(row["spans"]) == len(expected["spans"]), f"사례 {index} 곡선 수" + for got, want in zip(row["spans"], expected["spans"], strict=True): + assert got == pytest.approx(want, abs=1e-9), f"사례 {index} 곡선" + assert row["snapped"] == pytest.approx(expected["snapped"], abs=1e-9), f"사례 {index} 스냅" + moved += row["snapped"] != row["grid"] + assert moved >= 3 # 옮김이 실제로 일어난 사례를 견줌 + + +def test_짝_머리글이_서로를_가리킨다() -> None: + text = TS_FILE.read_text(encoding="utf-8") + assert "B05_Profile_Engine_Sections_Core.py" in text and "snap_curve_stations" in text + assert "snapCurveStations" in (core.__doc__ or "") + + +# ── generate_sections ──────────────────────────────────────────────────────── + + +class _Flat: + def sample_xy(self, xy): + count = len(np.asarray(xy, dtype=np.float64)) + return np.full(count, 100.0), np.ones(count, dtype=bool) + + +_LINE = [[0.0, 0.0, 100.0], [1000.3, 0.0, 100.0]] +_CURVES = [{"start": [616.1, 0.0], "apex": [631.3, 3.0], "end": [646.5, 0.0], "radius_m": 40.0}] + + +def _stations(snap: bool | None, extras: tuple = ()) -> list[dict]: + kwargs = {} if snap is None else {"station_snap": snap} + result = core.generate_sections( + _LINE, + _Flat(), + core.SectionGenerationOptions(extra_stations=extras), + design_curves=_CURVES, + **kwargs, + ) + return result["longitudinal"]["stations"] + + +def test_끔은_지금과_같음() -> None: + assert _stations(False) == _stations(None) + assert 640.0 in [s["chainage_m"] for s in _stations(False)] + + +def test_켬은_640_없고_631_있음() -> None: + by_chainage = {s["chainage_m"]: s for s in _stations(True)} + assert 640.0 not in by_chainage and 631.0 in by_chainage + assert by_chainage[631.0]["kind"] == "regular" + assert by_chainage[631.0]["plan_radius_m"] == 40.0 # 호 안 = 곡선표 반경 + assert len(by_chainage) == len(_stations(False)) + + +def test_631_에_관이면_grid() -> None: + by_chainage = {s["chainage_m"]: s for s in _stations(True, ((631.0, "관"),))} + assert by_chainage[631.0]["kind"] == "irregular" + assert by_chainage[631.0]["grid"] is True + # 640 에 놓인 관은 스냅 뒤 격자가 아니라 따로 선 측점 + off_grid = {s["chainage_m"]: s for s in _stations(True, ((640.0, "관"),))} + assert off_grid[640.0]["kind"] == "irregular" and "grid" not in off_grid[640.0] + + +# ── 서버 저장 · 정본 (326-2) ───────────────────────────────────────────────── + +_NODES = [(0.0, 0.0), (300.0, 0.0), (300.0, 300.0)] + + +def _working(tmp_path: Path) -> Path: + from common_util.common_util_route_geometry import planned_route_working_path + + path = planned_route_working_path(tmp_path) + path.parent.mkdir(parents=True, exist_ok=True) + return path + + +def test_확인_때_곡선표에_적고_읽음(tmp_path: Path) -> None: + from B05_Profile import B05_Profile_Router_Replan as replan + from B05_Profile.B05_Profile_Engine_Sections import load_station_snap + + path = _working(tmp_path) + assert load_station_snap(tmp_path) is False # 파일 없음 + replan._write_planned_polyline(path, _NODES, 30.0, simplify=False) + assert "station_snap" not in json.loads(replan._curves_path(path).read_text("utf-8")) + assert load_station_snap(tmp_path) is False # 칸 없음 = 끔(옛 프로젝트) + replan._write_planned_polyline(path, _NODES, 30.0, simplify=False, station_snap=True) + assert load_station_snap(tmp_path) is True + replan._write_planned_polyline(path, _NODES, 30.0, simplify=False, station_snap=False) + assert load_station_snap(tmp_path) is False + + +def _patch_replan(monkeypatch, tmp_path: Path): + from B05_Profile import B05_Profile_Router_Replan as replan + + async def paths(_project_id): + return tmp_path, "stored" + + async def criteria(_project_id): + return 30.0, 0.0, 0.0 + + monkeypatch.setattr(replan, "_project_paths", paths) + monkeypatch.setattr(replan, "_plan_criteria", criteria) + return replan + + +def test_실패하면_곡선표_노드도_되돌림(tmp_path: Path, monkeypatch) -> None: + import asyncio + from uuid import uuid4 + + replan = _patch_replan(monkeypatch, tmp_path) + + async def broken(*_args, **_kwargs): + return {"error": "체인 끊김"} + + monkeypatch.setattr(replan, "_recompute", broken) + path = _working(tmp_path) + replan._write_planned_polyline(path, _NODES, 30.0, simplify=False) + files = (path, replan._curves_path(path), replan._nodes_path(path)) + before = {file: file.read_bytes() for file in files} + request = replan.RouteReplanRequest( + vertices=[{"x": 0, "y": 0}, {"x": 320, "y": 40}, {"x": 300, "y": 300}], + station_snap=True, + ) + response = asyncio.run(replan.replan_route(uuid4(), request)) + assert response.status_code == 409 + assert {file: file.read_bytes() for file in files} == before + + +def test_GET_계획노선이_스냅_값을_돌려줌(tmp_path: Path, monkeypatch) -> None: + import asyncio + from uuid import uuid4 + + replan = _patch_replan(monkeypatch, tmp_path) + path = _working(tmp_path) + replan._write_planned_polyline(path, _NODES, 30.0, simplify=False) + assert asyncio.run(replan.read_route_plan(uuid4()))["station_snap"] is False + replan._write_planned_polyline(path, _NODES, 30.0, simplify=False, station_snap=True) + assert asyncio.run(replan.read_route_plan(uuid4()))["station_snap"] is True + + +def test_측점_셈_둘_다_스냅을_읽음(tmp_path: Path, monkeypatch) -> None: + from B05_Profile import B05_Profile_Engine_Sections as sections + from B05_Profile import B05_Profile_Router_Replan as replan + + path = _working(tmp_path) + replan._write_planned_polyline(path, _NODES, 30.0, simplify=False) + replan._curves_path(path).write_text( + json.dumps({"curves": _CURVES, "station_snap": True}), encoding="utf-8" + ) + monkeypatch.setattr(sections, "_load_route_polyline", lambda *_args: _LINE) + monkeypatch.setattr(sections, "build_surface_sampler", lambda *_args, **_kw: _Flat()) + # [저장] 때 구조물 측점 셈 — 631 은 스냅한 격자 자리 + stations, _ = sections.sample_structure_sections( + tmp_path, "route.geojson", "f", "m", False, extra_stations=((631.0, "관"),) + ) + assert [s["chainage_m"] for s in stations] == [631.0] and stations[0]["grid"] is True + + seen: dict = {} + + def stop(*_args, **kwargs): + seen.update(kwargs) + raise RuntimeError("여기까지") + + monkeypatch.setattr(sections, "generate_sections", stop) + monkeypatch.setattr(sections, "_load_pipe_points", lambda *_args: []) + monkeypatch.setattr(sections, "resolve_extra_stations", lambda *_args: ()) + with pytest.raises(RuntimeError): + sections.run_section_generation(tmp_path, "route.geojson", "f", "m", False) + assert seen["station_snap"] is True + + +def test_횡단_미리보기도_스냅(tmp_path: Path, monkeypatch) -> None: + from B05_Profile import B05_Profile_Router_Terrain as terrain + from common_util.common_util_route_polyline import build_planned_polyline + + monkeypatch.setattr(terrain, "build_surface_sampler", lambda *_args, **_kw: _Flat()) + built = build_planned_polyline(_NODES, min_radius_m=40.0, simplify=False) + points, chain = core._clean_polyline(np.asarray(built.vertices, dtype=np.float64)) + spans = core._design_curve_spans(points, chain, [c.as_dict() for c in built.curves]) + assert len(spans) == 1 + target = math.floor((spans[0][0] + spans[0][1]) / 2 + 0.5) + assert target % 20 # 격자 밖 자리여야 견줄 뜻이 있음 + + def ask(snap: bool) -> float: + request = terrain.CrossPreviewRequest( + vertices=[{"x": x, "y": y} for x, y in _NODES], + chainage_m=target, + min_radius_m=40.0, + station_interval_m=20.0, + station_snap=snap, + ) + params = {"source_filter": "f", "method": "m", "smooth": False} + return terrain._cross_preview(tmp_path, params, request)["chainage_m"] + + assert ask(True) == target + assert ask(False) % 20 == 0 From 6ae8bb4f6ae19dedd72fc1861b623ede6d293177 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 15 Oct 2026 12:32:45 +0900 Subject: [PATCH 18/81] =?UTF-8?q?feat(B05=EC=B6=94=EC=B2=9C):=20328-1=20?= =?UTF-8?q?=EC=B6=94=EC=B2=9C=20=EB=85=B8=EC=84=A0=20R=20=EB=A7=9E?= =?UTF-8?q?=EC=B6=A4=20=E2=80=94=20=ED=83=90=EC=83=89=20=EA=B2=BD=EB=A1=9C?= =?UTF-8?q?=EB=A5=BC=20=EC=98=88=EC=A0=95=EB=85=B8=EC=84=A0=20=EA=B7=9C?= =?UTF-8?q?=EC=B9=99(=EB=8B=A8=EC=88=9C=ED=99=94=20=C2=B7=20=EB=AC=B6?= =?UTF-8?q?=EC=9D=8C=20=C2=B7=20R=20=EB=A7=9E=EC=B6=A4)=EC=9C=BC=EB=A1=9C?= =?UTF-8?q?=20=C2=B7=20=EC=B5=9C=EC=86=8C=20R=20=ED=8C=90=EB=B3=B4?= =?UTF-8?q?=EB=8B=A4=20=EC=9C=84=EB=B0=98=20=EB=A7=8E=EC=9C=BC=EB=A9=B4=20?= =?UTF-8?q?=EC=B5=9C=EC=86=8C=20R=20=ED=8C=90=20=C2=B7=20=EB=8F=84?= =?UTF-8?q?=EC=9B=80=EB=A7=90=20=EC=B6=94=EC=A0=81=20=E3=80=8C=EB=A7=9E?= =?UTF-8?q?=EC=B6=98=20=EA=B3=A1=EC=84=A0=EB=B0=98=EC=A7=80=EB=A6=84?= =?UTF-8?q?=E3=80=8D?= 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_01Se7ytqbohmBQnfYpwYFnYf --- .../B05_Profile_Engine_RouteInitial_Pool.py | 22 ++++++++++++++++--- ...B05_Profile_UI_RouteEdit_Recommend_Help.ts | 2 +- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/B05_Profile/B05_Profile_Engine_RouteInitial_Pool.py b/B05_Profile/B05_Profile_Engine_RouteInitial_Pool.py index f09392ef6..9a7571509 100644 --- a/B05_Profile/B05_Profile_Engine_RouteInitial_Pool.py +++ b/B05_Profile/B05_Profile_Engine_RouteInitial_Pool.py @@ -43,6 +43,7 @@ from B05_Profile.B05_Profile_Engine_RouteInitial_Search import ( NoRoute, RouteGraph, ) +from common_util.common_util_route_geometry import densify_route from common_util.common_util_route_polyline import build_planned_polyline ZERO = {"expected": 0.0, "earthwork": 0.0, "grade": 0.0} @@ -70,6 +71,8 @@ MIN_RANGE_SHARE = 0.05 SIMILAR_M = 10.0 # 비중 계산의 제약 몫(기울기 · 계류 각각) = (1 − 예상 몫) × 이 값 — 크기 맞춘 목표 한 몫과 같은 세기 CONSTRAINT_SHARE = 1.0 +# 탐색 경로를 예정노선처럼 폴리라인화하기 전 점 간격(m) — R 맞춤이 꺾임점 사이 점으로 잼 +TRACE_DENSIFY_M = 2.0 Measure = Callable[..., dict[str, Any]] Route = tuple[Any, dict[str, Any]] @@ -103,14 +106,27 @@ def _trace( traced: dict[tuple, Route], **kw: Any, ) -> Route: - # 노드마다 곡선(묶지 않음) — [확인] 재구성(`/route/replan`)과 같은 규칙 → 적용 뒤도 같은 선. + # 큰 R(PLAN 328) — 1단 판(탐색 경로를 예정노선 규칙으로 단순화 · 묶음 · R 맞춤)이 위반이 + # 최소 R 판(노드마다 곡선 · 옛 규칙)보다 많지 않으면 그 판. 둘 다 화면이 [확인] 때 같은 선으로 + # 다시 짜는 꼴. # 같은 노드 · 같은 잼이면 다시 재지 않음(쓸기는 이웃 비중이 같은 길을 자주 냄 · 묶음마다 따로). key = (tuple(nodes), tuple(sorted(kw.items()))) if key not in traced: - outline = build_planned_polyline( + floor = build_planned_polyline( nodes, min_radius_m=min_radius_m, simplify=False, curve_flags=[True] * len(nodes) ) - traced[key] = (outline, measure(outline, **kw)) + least = (floor, measure(floor, **kw)) + path = densify_route(nodes, TRACE_DENSIFY_M) + fitted = build_planned_polyline(path, min_radius_m=min_radius_m) + larger = (fitted,) + traced[key] = next( + ( + route + for route in ((outline, measure(outline, **kw)) for outline in larger) + if route[1]["over_limit"]["total"] <= least[1]["over_limit"]["total"] + ), + least, + ) return traced[key] diff --git a/B05_Profile/B05_Profile_UI_RouteEdit_Recommend_Help.ts b/B05_Profile/B05_Profile_UI_RouteEdit_Recommend_Help.ts index c3c713495..d088f80b7 100644 --- a/B05_Profile/B05_Profile_UI_RouteEdit_Recommend_Help.ts +++ b/B05_Profile/B05_Profile_UI_RouteEdit_Recommend_Help.ts @@ -29,7 +29,7 @@ const LOGIC_HELP: Record = { follow: { summary: "전처리 때 만든 예상노선(점선)을 그대로 따라가는 노선", how: [ - "새로 길을 찾지 않음 — 예상노선 꺾임점에 최소 곡선반지름으로 곡선만 넣음", + "새로 길을 찾지 않음 — 예상노선 꺾임점에 맞춘 곡선반지름(최소 이상 · 예상노선에 가장 가까운 값)으로 곡선만 넣음", "전처리 초기 계획노선과 같은 규칙이라 같은 선이 나옴", "종단기울기 · 계류 이격은 막지 않고, 넘은 곳만 「못 지킨 구간」에 적음", ], From 394fb7ba604e064e05566659571aa34c58f761d4 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 15 Oct 2026 12:32:56 +0900 Subject: [PATCH 19/81] =?UTF-8?q?feat(B05=EC=B6=94=EC=B2=9C):=20328-2=20?= =?UTF-8?q?=EC=B6=94=EC=B2=9C=20=EB=85=B8=EC=84=A0=20=ED=81=B0=20R=20?= =?UTF-8?q?=E2=80=94=20=EA=B3=A1=EC=84=A0=EB=A7=88=EB=8B=A4=20[=EB=A7=9E?= =?UTF-8?q?=EC=B6=98=20R,=20=EC=A0=91=EC=84=A0=20=EC=9E=90=EB=A6=AC=20?= =?UTF-8?q?=C3=B7=20tan(IA/2)]=20=EC=95=88=20=ED=83=90=EC=83=89=20?= =?UTF-8?q?=EA=B2=BD=EB=A1=9C=20=EC=9D=B4=ED=83=88=20=E2=89=A4=204=20m=20?= =?UTF-8?q?=EC=9D=B8=20=EA=B0=80=EC=9E=A5=20=ED=81=B0=20R(1=20m=20?= =?UTF-8?q?=EB=82=B4=EB=A6=BC)=20=C2=B7=20=EB=AA=BB=EB=B0=95=EC=95=84=20?= =?UTF-8?q?=EB=8B=A4=EC=8B=9C=20=EC=A7=AC(=ED=8E=B8=20=EB=85=B8=EB=93=9C?= =?UTF-8?q?=20=C2=B7=20[=ED=99=95=EC=9D=B8]=20=EC=9E=AC=EA=B5=AC=EC=84=B1?= =?UTF-8?q?=EA=B3=BC=20=EA=B0=99=EC=9D=80=20=EC=84=A0)=20=C2=B7=20?= =?UTF-8?q?=ED=97=88=EC=9A=A9=20=EC=9D=B4=ED=83=88=20=EC=83=81=EC=88=98=20?= =?UTF-8?q?=C2=B7=20test=5F224=5F4=5Frecommend=5Fradius?= 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_01Se7ytqbohmBQnfYpwYFnYf --- .../B05_Profile_Engine_RouteInitial_Pool.py | 9 +- .../B05_Profile_Engine_RouteInitial_Radius.py | 110 ++++++++++++ config/config_system_design.py | 2 + .../tester/test_224_4_recommend_radius.py | 156 ++++++++++++++++++ 4 files changed, 273 insertions(+), 4 deletions(-) create mode 100644 B05_Profile/B05_Profile_Engine_RouteInitial_Radius.py create mode 100644 resources/tester/test_224_4_recommend_radius.py diff --git a/B05_Profile/B05_Profile_Engine_RouteInitial_Pool.py b/B05_Profile/B05_Profile_Engine_RouteInitial_Pool.py index 9a7571509..1f7e2d3df 100644 --- a/B05_Profile/B05_Profile_Engine_RouteInitial_Pool.py +++ b/B05_Profile/B05_Profile_Engine_RouteInitial_Pool.py @@ -37,6 +37,7 @@ from B05_Profile.B05_Profile_Engine_RouteInitial_Metrics import ( COST_LENGTH_M, ) from B05_Profile.B05_Profile_Engine_RouteInitial_Profile import plain_metrics, profile_earth +from B05_Profile.B05_Profile_Engine_RouteInitial_Radius import widen from B05_Profile.B05_Profile_Engine_RouteInitial_Search import ( CROSSING_BAND, OBJECTIVES, @@ -106,9 +107,9 @@ def _trace( traced: dict[tuple, Route], **kw: Any, ) -> Route: - # 큰 R(PLAN 328) — 1단 판(탐색 경로를 예정노선 규칙으로 단순화 · 묶음 · R 맞춤)이 위반이 - # 최소 R 판(노드마다 곡선 · 옛 규칙)보다 많지 않으면 그 판. 둘 다 화면이 [확인] 때 같은 선으로 - # 다시 짜는 꼴. + # 큰 R(PLAN 328) — 2단 판(허용 이탈 안 가장 큰 R · `..._Radius.widen`) → 1단 판(탐색 경로를 + # 예정노선 규칙으로 단순화 · 묶음 · R 맞춤) 차례로, 위반이 최소 R 판(노드마다 곡선 · 옛 규칙) + # 보다 많지 않은 첫 판. 셋 다 화면이 [확인] 때 같은 선으로 다시 짜는 꼴. # 같은 노드 · 같은 잼이면 다시 재지 않음(쓸기는 이웃 비중이 같은 길을 자주 냄 · 묶음마다 따로). key = (tuple(nodes), tuple(sorted(kw.items()))) if key not in traced: @@ -118,7 +119,7 @@ def _trace( least = (floor, measure(floor, **kw)) path = densify_route(nodes, TRACE_DENSIFY_M) fitted = build_planned_polyline(path, min_radius_m=min_radius_m) - larger = (fitted,) + larger = (widen(fitted, path, min_radius_m), fitted) traced[key] = next( ( route diff --git a/B05_Profile/B05_Profile_Engine_RouteInitial_Radius.py b/B05_Profile/B05_Profile_Engine_RouteInitial_Radius.py new file mode 100644 index 000000000..f425eaa70 --- /dev/null +++ b/B05_Profile/B05_Profile_Engine_RouteInitial_Radius.py @@ -0,0 +1,110 @@ +"""추천 노선 큰 R — 2단(PLAN 328-2 · 요구 224-4 「R 은 클수록 좋음」). + +`B05_Profile_Engine_RouteInitial_Pool._trace` 가 부름. 예상노선 추적(전처리 초기값)은 안 거침. + +1단(`_trace`)이 예정노선 규칙(단순화 · 같은 방향 묶음 · R 맞춤)으로 짠 선을 받아, 곡선마다 R 을 +[맞춘 R, 접선 자리 ÷ tan(IA/2)] 안에서 탐색 경로 점과 가장 먼 거리 ≤ `ROUTE_RECOMMEND_MAX_GAP_M` +인 가장 큰 값(1 m 내림)으로 고른다. 맞는 값이 없으면 맞춘 R 그대로. + +고른 R 은 편집 갈래(`simplify=False` · `radii`)로 못박아 다시 짠다 — 화면이 곡선을 교각점 하나로 +펴서(`flattenServerPlan`) [확인]이 다시 짜는 것과 같은 입력이라 저장 뒤 R 이 응답 R 과 같다. +""" + +from __future__ import annotations + +import math + +import numpy as np +from shapely.geometry import LineString, Point + +from common_util.common_util_route_polyline import ( + PlannedPolyline, + _inner_angle_deg, + build_planned_polyline, +) +from config.config_system_design import ROUTE_RECOMMEND_MAX_GAP_M + +Xy = tuple[float, float] + + +def _flat(outline: PlannedPolyline) -> tuple[list[Xy], list[bool], list[float | None]]: + """곡선 묶음을 교각점 하나로 편 노드 · 곡선 켬 · R — 화면 `flattenServerPlan` 과 같은 꼴.""" + starts = {curve.node_first: curve for curve in outline.curves} + inside = {i for c in outline.curves for i in range(c.node_first + 1, c.node_last + 1)} + points: list[Xy] = [] + flags: list[bool] = [] + radii: list[float | None] = [] + for index, node in enumerate(outline.nodes): + if index in inside: + continue + curve = starts.get(index) + points.append(curve.apex if curve else (node.x, node.y)) + flags.append(curve is not None) + radii.append(round(curve.radius_m, 2) if curve else None) + return points, flags, radii + + +def _cross(u: np.ndarray, v: np.ndarray) -> np.ndarray: + return u[..., 0] * v[..., 1] - u[..., 1] * v[..., 0] + + +def _gap(samples: np.ndarray, before: Xy, apex: Xy, after: Xy, radii: np.ndarray) -> np.ndarray: + """R 마다 점들이 「직선 · 원호 · 직선」(앞뒤 직선은 절반까지)에서 가장 먼 거리(m).""" + a = np.asarray(apex, dtype=float) + p = samples - a # (S, 2) 교각점 기준 + units = [(np.asarray(end, dtype=float) - a) for end in (before, after)] + halves = [float(np.hypot(*u)) / 2 for u in units] + units = [u / (2 * h) for u, h in zip(units, halves)] + inner = math.radians(_inner_angle_deg(before, apex, after)) + tangent = radii[:, None] * math.tan((math.pi - inner) / 2) # (M, 1) + legs = [] + for unit, half in zip(units, halves): + along = p @ unit + side = np.abs(_cross(unit, p)) + legs.append(np.hypot(side, np.maximum(0.0, np.maximum(tangent - along, along - half)))) + bisector = (units[0] + units[1]) / np.hypot(*(units[0] + units[1])) + center = (radii / math.sin(inner / 2))[:, None, None] * bisector # (M, 1, 2) + starts = [tangent[..., None] * unit - center for unit in units] # 중심 → 접선점 + q = p[None] - center # (M, S, 2) 중심 → 점 + turn = _cross(starts[0], starts[1]) + within = (_cross(starts[0], q) * turn >= 0) & (_cross(q, starts[1]) * turn >= 0) + arc = np.where(within, np.abs(np.hypot(q[..., 0], q[..., 1]) - radii[:, None]), np.inf) + return np.minimum(np.minimum(legs[0], legs[1]), arc).max(axis=1) + + +def widen( + outline: PlannedPolyline, + path: list[Xy], + min_radius_m: float, + max_gap_m: float = ROUTE_RECOMMEND_MAX_GAP_M, +) -> PlannedPolyline: + """곡선마다 허용 이탈 안 가장 큰 R(1 m 내림)로 못박아 다시 짠 선. 곡선이 없으면 그대로.""" + if not outline.curves: + return outline + points, flags, radii = _flat(outline) + line = LineString(path) + coords = np.asarray(path, dtype=float) + along = np.concatenate([[0.0], np.cumsum(np.hypot(*np.diff(coords, axis=0).T))]) + for index in range(1, len(points) - 1): + if not flags[index]: + continue + before, apex, after = points[index - 1], points[index], points[index + 1] + half_tan = math.tan(math.radians(180.0 - _inner_angle_deg(before, apex, after)) / 2) + if half_tan <= 1e-9: + continue + room = min(math.dist(before, apex), math.dist(apex, after)) / 2 / half_tan + candidates = np.arange(math.floor(room), float(radii[index]), -1.0) # 큰 것부터 + # 이 곡선이 쓰는 자리 = 앞뒤 직선 가운데점 사이 — 그 사이 탐색 경로 점만 잼 + ends = [ + line.project(Point((apex[0] + end[0]) / 2, (apex[1] + end[1]) / 2)) + for end in (before, after) + ] + samples = coords[(along >= min(ends)) & (along <= max(ends))] + if not candidates.size or not len(samples): + continue + ok = candidates[_gap(samples, before, apex, after, candidates) <= max_gap_m] + if ok.size: + radii[index] = float(ok[0]) + return build_planned_polyline( + points, min_radius_m=min_radius_m, simplify=False, curve_flags=flags, radii=radii + ) diff --git a/config/config_system_design.py b/config/config_system_design.py index 13fb522ef..4e7c43d93 100644 --- a/config/config_system_design.py +++ b/config/config_system_design.py @@ -38,6 +38,8 @@ ROUTE_DIRECT_LINK_CELL_FACTOR = float(os.getenv("ROUTE_DIRECT_LINK_CELL_FACTOR", # 예정노선을 자동 체인에 넘기기 전, 직결 문턱의 이 비율까지 정점 간격을 좁힌다. # 문턱과 정확히 같게 두면 부동소수 오차 한 번에 탐색으로 넘어가 노선이 바뀐다. ROUTE_PLANNED_DENSIFY_SAFETY = float(os.getenv("ROUTE_PLANNED_DENSIFY_SAFETY", "0.9")) +# 추천 노선 큰 R — 곡선을 키울 때 탐색 경로에서 벗어나도 되는 가장 먼 거리(m) · 탐색 칸 하나 +ROUTE_RECOMMEND_MAX_GAP_M = float(os.getenv("ROUTE_RECOMMEND_MAX_GAP_M", "4.0")) # 임도 종류 — 현행 규칙(별표2)의 3종. `branch`(지선)는 규칙에서 폐지됐으나 기존 # 저장분이 남아 있어 값으로는 계속 받는다(화면 선택지에서는 뺀다, 2026-08-19). ROUTE_GRADE_CLASSES = ("trunk", "fire", "work", "branch") diff --git a/resources/tester/test_224_4_recommend_radius.py b/resources/tester/test_224_4_recommend_radius.py new file mode 100644 index 000000000..befa3dae8 --- /dev/null +++ b/resources/tester/test_224_4_recommend_radius.py @@ -0,0 +1,156 @@ +"""추천 노선 큰 R(PLAN 328 · 요구 224-4) — 1단 R 맞춤 · 2단 허용 이탈 안 가장 큰 R. + + ./venv/Scripts/python.exe -m pytest resources/tester/test_224_4_recommend_radius.py -q + +· 네 갈래(영선 · 토공 최소 · 기울기 최소 · 비중) R ≥ 최소(모자라면 위반) · 하나 이상 큼. +· 위반 ≤ 같은 노드로 짠 최소 R 판(노드마다 곡선 · 옛 규칙) — 묶음의 잰 노선 전부. +· 예상노선 추적은 전처리 초기값 그대로. +· 2단 = 탐색 경로 이탈 ≤ 허용(4 m) · 1 m 더 키우면 넘음 · [확인] 재구성 뒤 같은 R. +""" + +import math + +import numpy as np +import pytest +from shapely.geometry import LineString, Point + +import B05_Profile.B05_Profile_Engine_RouteInitial as engine +from B05_Profile.B05_Profile_Engine_RouteInitial import Terrain, generate_initial_route +from B05_Profile.B05_Profile_Engine_RouteInitial_Radius import _flat, widen +from common_util.common_util_route_polyline import build_planned_polyline +from config.config_system_design import ROUTE_RECOMMEND_MAX_GAP_M + +GRADE = 0.14 +RADIUS = 12.0 +SHOWN = ["gradeline", "earthwork", "gentle", "weighted"] + + +def _line() -> list[tuple[float, float]]: + return [(float(x), 200.0) for x in np.arange(50.0, 752.0, 4.0)] + + +@pytest.fixture(scope="module") +def runs() -> dict: + """가운데 언덕(40 m) — 곧은 예상노선이 꼭대기를 넘어 갈래마다 돌아감. 묶음 · 잼도 붙잡음.""" + x = np.arange(0.0, 802.0, 2.0) + y = np.arange(0.0, 402.0, 2.0) + xx, yy = np.meshgrid(x, y) + z = 40.0 * np.exp(-((xx - 400.0) ** 2 + (yy - 200.0) ** 2) / 90.0**2) + terrain = Terrain(x, y, z, np.ones(xx.shape, dtype=bool)) + caught: dict = {} + real = engine.build_pool + + def spy(graph, follow, max_grade, min_radius_m, measure): + caught["measure"] = measure + caught["pool"] = real(graph, follow, max_grade, min_radius_m, measure) + return caught["pool"] + + engine._POOLS.clear() + engine.build_pool = spy + try: + results = { + mode: generate_initial_route( + _line(), terrain, [], mode=mode, weights=None, max_grade=GRADE, min_radius_m=RADIUS + ) + for mode in [*SHOWN, "follow"] + } + finally: + engine.build_pool = real + engine._POOLS.clear() + return {"results": results, **caught} + + +def test_shown_branches_radius_at_least_min_and_some_larger(runs: dict) -> None: + radii = [] + for mode in SHOWN: + for curve in runs["results"][mode]["curves"]: + assert curve["radius_m"] >= RADIUS - 1e-6 or curve["violations"], mode + radii.append(curve["radius_m"]) + assert max(radii) > RADIUS + 1.0 + + +def test_violations_not_above_min_radius_version(runs: dict) -> None: + measure = runs["measure"] + for (nodes, kw), (_, metrics) in runs["pool"].traced.items(): + floor = build_planned_polyline( + list(nodes), min_radius_m=RADIUS, simplify=False, curve_flags=[True] * len(nodes) + ) + least = measure(floor, **dict(kw))["over_limit"]["total"] + assert metrics["over_limit"]["total"] <= least + + +def test_follow_unchanged(runs: dict) -> None: + initial = build_planned_polyline(_line(), min_radius_m=RADIUS) + result = runs["results"]["follow"] + assert result["planned"] == [[round(x, 4), round(y, 4)] for x, y in initial.vertices] + assert result["curves"] == [curve.as_dict() for curve in initial.curves] + + +def _bend(radius: float) -> list[tuple[float, float]]: + """직선 100 m → 반지름 `radius` 원호 90° → 직선 100 m 를 2 m 간격 점으로.""" + points = [(float(x), 0.0) for x in np.arange(-100.0, 0.0, 2.0)] + steps = int(radius * math.pi / 2 / 2.0) + points += [ + (radius * math.sin(a), radius - radius * math.cos(a)) + for a in np.linspace(0.0, math.pi / 2, steps) + ] + points += [(radius, radius + float(y)) for y in np.arange(2.0, 102.0, 2.0)] + return points + + +def _window_gap(path, outline, curve) -> float: + """곡선 앞뒤 직선 가운데점 사이 경로 점이 노선에서 가장 먼 거리.""" + line = LineString(path) + drawn = LineString(outline.vertices) + apex = curve.apex + nodes = [(node.x, node.y) for node in outline.nodes] + at = nodes.index(min(nodes, key=lambda n: math.dist(n, apex))) + ends = [ + line.project(Point((apex[0] + n[0]) / 2, (apex[1] + n[1]) / 2)) + for n in (nodes[at - 1], nodes[at + 1]) + ] + return max( + drawn.distance(Point(p)) for p in path if min(ends) <= line.project(Point(p)) <= max(ends) + ) + + +def test_widen_largest_radius_within_gap() -> None: + path = _bend(80.0) + fitted = build_planned_polyline(path, min_radius_m=RADIUS) + wide = widen(fitted, path, RADIUS) + assert len(wide.curves) == 1 + curve = wide.curves[0] + assert curve.radius_m >= fitted.curves[0].radius_m + assert curve.radius_m == math.floor(curve.radius_m) # 1 m 내림 + assert curve.radius_m > 40.0 + assert _window_gap(path, wide, curve) <= ROUTE_RECOMMEND_MAX_GAP_M + 1e-6 + # 1 m 더 키우면 허용 이탈을 넘음(가장 큰 값) — 셈 자(shapely)로 따로 잼 + points, flags, radii = _flat(wide) + seat = flags.index(True) + radii[seat] = curve.radius_m + 1.0 + bigger = build_planned_polyline( + points, min_radius_m=RADIUS, simplify=False, curve_flags=flags, radii=radii + ) + assert _window_gap(path, bigger, bigger.curves[0]) > ROUTE_RECOMMEND_MAX_GAP_M + + +def test_widen_survives_confirm_rebuild() -> None: + """화면이 펴서 보내는 꼴(교각점 · 곡선 켬 · R)로 [확인]이 다시 짜도 같은 R.""" + path = _bend(60.0) + [(60.0 + float(x), 160.0) for x in np.arange(2.0, 120.0, 2.0)] + wide = widen(build_planned_polyline(path, min_radius_m=RADIUS), path, RADIUS) + points, flags, radii = _flat(wide) + again = build_planned_polyline( + points, min_radius_m=RADIUS, simplify=False, curve_flags=flags, radii=radii + ) + assert [c.radius_m for c in again.curves] == [c.radius_m for c in wide.curves] + assert again.vertices == wide.vertices + + +def test_widen_keeps_fitted_when_gap_already_over() -> None: + """허용 이탈 0 이면 키울 R 이 없어 맞춘 R 그대로.""" + path = _bend(80.0) + fitted = build_planned_polyline(path, min_radius_m=RADIUS) + same = widen(fitted, path, RADIUS, max_gap_m=0.0) + assert [round(c.radius_m, 2) for c in same.curves] == [ + round(c.radius_m, 2) for c in fitted.curves + ] From 8e96880880daa243858fd1cb5bc05081b4076cd6 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 15 Oct 2026 12:33:28 +0900 Subject: [PATCH 20/81] =?UTF-8?q?feat(CAD):=20331-1=20=EA=B0=81=EB=8F=84?= =?UTF-8?q?=20=EA=B3=A0=EC=A0=95=20`<=EA=B0=81=EB=8F=84`=20=E2=86=92=20?= =?UTF-8?q?=EA=B8=B8=EC=9D=B4=20=C2=B7=20=EC=8B=9C=ED=80=80=EC=8A=A4=20?= =?UTF-8?q?=EC=B2=AB=20=EC=A0=90=20=EC=88=AB=EC=9E=90=20=EB=AC=B4=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 새 input-angle-lock — 다음 한 점만 고정 방향(동쪽 0° 반시계) · 커서 · 클릭은 고정 직선 위로 · 숫자는 그 방향 길이 · 한 점 · Esc · 새 명령이면 풂 · 커서 옆 안내 「각도 30° 고정 — 길이 입력」 - input-patterns — 숫자 음수 · .5 허용 · 각도 고정 꼴 · 점 글 해석 한 곳(parsePointText) - Enter 해석 차례 — 자유 글 단계만 원문 먼저 → 각도 고정 → 점 · 거리 → 단계 선택지 → 명령 이름 - 선 · 시퀀스 미리보기도 고정 방향 · 시퀀스 첫 점의 거리 · 상대 좌표는 무시(도구 멈춤 · 「Error in tool actor」 없앰) - 시험 resources/tester/cad/keyboard-chain.test.ts Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013QyNXY2TUKXCFMwAca2APG --- .../openwebcad/src/commands/run-command.ts | 2 + .../src/helpers/get-point-from-event.ts | 10 +- .../src/inputController/input-angle-lock.ts | 31 ++++ .../src/inputController/input-controller.ts | 155 ++++++------------ .../src/inputController/input-patterns.ts | 58 ++++++- .../src/tools/factories/sequence-tool.ts | 10 +- .../openwebcad/src/tools/line-tool.ts | 2 +- resources/tester/cad/keyboard-chain.test.ts | 150 +++++++++++++++++ 8 files changed, 299 insertions(+), 119 deletions(-) create mode 100644 B07_DesignDetail/openwebcad/src/inputController/input-angle-lock.ts create mode 100644 resources/tester/cad/keyboard-chain.test.ts diff --git a/B07_DesignDetail/openwebcad/src/commands/run-command.ts b/B07_DesignDetail/openwebcad/src/commands/run-command.ts index 3c905bb6b..0e405565e 100644 --- a/B07_DesignDetail/openwebcad/src/commands/run-command.ts +++ b/B07_DesignDetail/openwebcad/src/commands/run-command.ts @@ -2,6 +2,7 @@ import { toast } from 'react-toastify'; import { Actor } from 'xstate'; import { HtmlEvent } from '../App.types'; +import { setAngleLock } from '../inputController/input-angle-lock'; import { getSelectedEntities, isDrawingReadOnly, notifyWindow, setActiveToolActor } from '../state'; import type { CadCommand } from './command.types'; import { getCommandById, isViewOnlyCommand, resolveCommandInput } from './registry'; @@ -38,6 +39,7 @@ export function runCommand(command: CadCommand): string { } lastCommandId = command.id; + setAngleLock(null); // 앞 명령의 각도 고정은 새 명령에 넘기지 않음 if (command.machine) { setActiveToolActor(new Actor(command.machine)); diff --git a/B07_DesignDetail/openwebcad/src/helpers/get-point-from-event.ts b/B07_DesignDetail/openwebcad/src/helpers/get-point-from-event.ts index 76565a4e2..90a6693bf 100644 --- a/B07_DesignDetail/openwebcad/src/helpers/get-point-from-event.ts +++ b/B07_DesignDetail/openwebcad/src/helpers/get-point-from-event.ts @@ -1,4 +1,5 @@ import { type Point, Vector } from '@flatten-js/core'; +import { getAngleLock, pointAtLock, projectOnLock } from '../inputController/input-angle-lock.ts'; import { type AbsolutePointInputEvent, ActorEvent, @@ -12,21 +13,26 @@ import { /** * Various tools need to convert user input into a point * This function handles mouse click event, number events and in the future absoluteCoordinates and relativeCoordinate events + * 각도 고정(`<30`) 중이면 커서 · 클릭 · 숫자를 기준점에서 고정 방향으로. * @param startPoint * @param event */ export function getPointFromEvent(startPoint: Point | null, event: PointInputEvent): Point { + const locked = startPoint !== null && getAngleLock() !== null; if (event.type === ActorEvent.DRAW) { - return (event as DrawEvent).drawController.getWorldMouseLocation(); + const mouse = (event as DrawEvent).drawController.getWorldMouseLocation(); + return locked ? projectOnLock(startPoint, mouse) : mouse; } if (event.type === ActorEvent.MOUSE_CLICK) { - return (event as MouseClickEvent).worldMouseLocation; + const click = (event as MouseClickEvent).worldMouseLocation; + return locked ? projectOnLock(startPoint, click) : click; } if (event.type === ActorEvent.NUMBER_INPUT) { if (!startPoint) { throw new Error('Cannot get relative point by distance if no start point is provided'); } const distance = (event as NumberInputEvent).value; + if (locked) return pointAtLock(startPoint, distance); // Direction indicated by the startPoint and the mouse location const direction = new Vector( event.worldMouseLocation.x - startPoint.x, diff --git a/B07_DesignDetail/openwebcad/src/inputController/input-angle-lock.ts b/B07_DesignDetail/openwebcad/src/inputController/input-angle-lock.ts new file mode 100644 index 000000000..bee46aac9 --- /dev/null +++ b/B07_DesignDetail/openwebcad/src/inputController/input-angle-lock.ts @@ -0,0 +1,31 @@ +/** + * 각도 고정 `<각도` — 오토캐드 각도 재지정. 다음 한 점만 기준점에서 그 방향(동쪽 0° 반시계 +). + * 커서 · 클릭은 고정 방향 직선 위로 내리고 · 숫자는 그 방향 길이. 점 하나 쓰면 · Esc · 새 명령이면 풂. + */ +import { Point } from '@flatten-js/core'; +import { polarToPoint } from './input-patterns.ts'; + +let lockedDegrees: number | null = null; + +export const getAngleLock = (): number | null => lockedDegrees; + +export const setAngleLock = (degrees: number | null) => { + lockedDegrees = degrees; +}; + +/** 커서 옆 안내 줄 */ +export const angleLockHint = (): string | null => + lockedDegrees === null ? null : `각도 ${lockedDegrees}° 고정 — 길이 입력`; + +/** 기준점에서 고정 방향으로 거리만큼 간 점 */ +export function pointAtLock(base: Point, distance: number): Point { + const offset = polarToPoint(distance, lockedDegrees ?? 0); + return new Point(base.x + offset.x, base.y + offset.y); +} + +/** 점을 기준점 · 고정 방향 직선 위로 내림(커서 · 클릭) */ +export function projectOnLock(base: Point, point: Point): Point { + const unit = polarToPoint(1, lockedDegrees ?? 0); + const along = (point.x - base.x) * unit.x + (point.y - base.y) * unit.y; + return new Point(base.x + unit.x * along, base.y + unit.y * along); +} diff --git a/B07_DesignDetail/openwebcad/src/inputController/input-controller.ts b/B07_DesignDetail/openwebcad/src/inputController/input-controller.ts index b3a2e3e5b..768d6fec9 100644 --- a/B07_DesignDetail/openwebcad/src/inputController/input-controller.ts +++ b/B07_DesignDetail/openwebcad/src/inputController/input-controller.ts @@ -44,14 +44,7 @@ import { } from '../state.ts'; import { Tool } from '../tools.ts'; import { SelectState, selectToolStateMachine } from '../tools/select-tool.ts'; -import { - type AbsolutePointInputEvent, - ActorEvent, - type MouseClickEvent, - type NumberInputEvent, - type RelativePointInputEvent, - type TextInputEvent, -} from '../tools/tool.types.ts'; +import { ActorEvent, type MouseClickEvent, type TextInputEvent } from '../tools/tool.types.ts'; import { activeToolAcceptsText, activeToolWantsFreeText, @@ -63,14 +56,8 @@ import { drawInputField } from './input-field-draw.ts'; import { ctrlShortcut } from './input-shortcut.ts'; import { wheelZoom } from './input-wheel.ts'; import { attachTouch } from './input-touch.ts'; -import { - ABSOLUTE_POINT_REGEXP, - ABSOLUTE_POLAR_REGEXP, - NUMBER_REGEXP, - polarToPoint, - RELATIVE_POINT_REGEXP, - RELATIVE_POLAR_REGEXP, -} from './input-patterns.ts'; +import { ANGLE_LOCK_REGEXP, parsePointText, type PointTextEvent } from './input-patterns.ts'; +import { angleLockHint, setAngleLock } from './input-angle-lock.ts'; /** 이만큼 넘게 끌면 클릭이 아니라 드래그로 본다 (화면 픽셀) */ const DRAG_SELECT_MIN_PX = 4; @@ -135,6 +122,8 @@ export class InputController { const roundedY = round(drawController.getWorldMouseLocation().y, 2); texts.push(`${roundedX},${roundedY}`); } + const lockHint = angleLockHint(); + if (lockHint) texts.push(lockHint); // 입력 중인 문자로 시작하는 명령 후보. 예: C => CIRCLE (C), COPY (CO) texts.push(...this.getCommandSuggestions()); // 입력 글(조합 중인 한글도 함께) @@ -213,7 +202,7 @@ export class InputController { ); const worldMouseLocation = closestSnapPoint ? closestSnapPoint.point : worldMouseLocationTemp; - getActiveToolActor()?.send({ + this.sendPointEvent({ type: ActorEvent.MOUSE_CLICK, worldMouseLocation, screenMouseLocation: screenCanvasDrawController.worldToTarget(worldMouseLocation), @@ -448,6 +437,7 @@ export class InputController { } public handleEscapeKey() { + setAngleLock(null); if (getSelectedEntities().length > 0) { // Deselect entities setSelectedEntityIds([]); @@ -495,17 +485,20 @@ export class InputController { if (wasSelectTool && getActiveToolActor()?.getSnapshot()?.status === 'done') { setActiveToolActor(new Actor(selectToolStateMachine)); } - } else if (activeToolCanHandleTextInput) { - console.log('TEXT_INPUT: ', { - text: this.text, - activeTool: getActiveToolActor(), - }); - // Send the text to the active tool - getActiveToolActor()?.send({ - type: ActorEvent.TEXT_INPUT, - value: this.text, - } as TextInputEvent); + } else if (activeToolWantsFreeText()) { + // 글 단계(TEXT · MTEXT · 표 칸 …)는 친 그대로 — 숫자 · 명령 이름도 글 + this.sendText(); + } else if (ANGLE_LOCK_REGEXP.test(this.text)) { + // 각도 고정 `<30` — 다음 한 점만 그 방향 + setAngleLock(Number.parseFloat((ANGLE_LOCK_REGEXP.exec(this.text) as RegExpExecArray)[1])); this.text = ''; + } else if (this.pointEventFromText()) { + // 거리 `100` · `x,y` · `@x,y` · `@d + getSnapPointOnAngleGuide()?.point || + getSnapPoint()?.point || + getScreenCanvasDrawController().getWorldMouseLocation() + ); + } + + /** 점 하나를 도구에 보냄 — 각도 고정은 이 한 점으로 끝 */ + private sendPointEvent(event: PointTextEvent | MouseClickEvent) { + getActiveToolActor()?.send(event); + setAngleLock(null); + } + public handleUndo(evt: KeyboardEvent) { evt.preventDefault(); undo(); diff --git a/B07_DesignDetail/openwebcad/src/inputController/input-patterns.ts b/B07_DesignDetail/openwebcad/src/inputController/input-patterns.ts index d2483f459..0d92a49c9 100644 --- a/B07_DesignDetail/openwebcad/src/inputController/input-patterns.ts +++ b/B07_DesignDetail/openwebcad/src/inputController/input-patterns.ts @@ -1,17 +1,65 @@ -/** 명령 입력 해석 — 숫자 · 좌표 · 극좌표 꼴 */ +/** 명령 입력 해석 — 숫자 · 좌표 · 극좌표 · 각도 고정 꼴 */ import { Point } from '@flatten-js/core'; +import { + type AbsolutePointInputEvent, + ActorEvent, + type NumberInputEvent, + type RelativePointInputEvent, +} from '../tools/tool.types.ts'; -export const NUMBER_REGEXP = /^[0-9]+([.][0-9]+)?$/; -/** 부호 있는 실수 한 개 (좌표는 음수가 될 수 있다) */ -const SIGNED = '(-?[0-9]+(?:[.][0-9]+)?)'; +/** 부호 있는 실수 한 개 — `-5` · `.5` · `1.5` */ +const SIGNED = '(-?(?:[0-9]+(?:[.][0-9]+)?|[.][0-9]+))'; +export const NUMBER_REGEXP = new RegExp(`^${SIGNED}$`); export const ABSOLUTE_POINT_REGEXP = new RegExp(`^${SIGNED}[ ]*,[ ]*${SIGNED}$`); export const RELATIVE_POINT_REGEXP = new RegExp(`^@${SIGNED}[ ]*,[ ]*${SIGNED}$`); /** 극좌표 — 거리<각도(도). `@`가 붙으면 직전 점 기준 */ export const ABSOLUTE_POLAR_REGEXP = new RegExp(`^${SIGNED}[ ]*<[ ]*${SIGNED}$`); export const RELATIVE_POLAR_REGEXP = new RegExp(`^@${SIGNED}[ ]*<[ ]*${SIGNED}$`); +/** 각도 고정 — `<30` (다음 한 점만 그 방향) */ +export const ANGLE_LOCK_REGEXP = new RegExp(`^<[ ]*${SIGNED}$`); -/** 거리·각도(도)를 x·y 변위로 바꾼다 */ +/** 거리·각도(도)를 x·y 변위로 바꾼다 — 각도 = 동쪽 0° 반시계 + */ export function polarToPoint(distance: number, degrees: number): Point { const radians = (degrees * Math.PI) / 180; return new Point(distance * Math.cos(radians), distance * Math.sin(radians)); } + +export type PointTextEvent = NumberInputEvent | AbsolutePointInputEvent | RelativePointInputEvent; + +/** 친 글 → 점 · 거리 사건(숫자 · `x,y` · `@x,y` · `@d Point): PointTextEvent | null { + const num = (value: string) => Number.parseFloat(value); + let match = NUMBER_REGEXP.exec(text); + if (match) { + return { type: ActorEvent.NUMBER_INPUT, value: num(match[1]), worldMouseLocation: cursor() }; + } + match = ABSOLUTE_POINT_REGEXP.exec(text); + if (match) { + return { + type: ActorEvent.ABSOLUTE_POINT_INPUT, + value: new Point(num(match[1]), num(match[2])), + }; + } + match = RELATIVE_POINT_REGEXP.exec(text); + if (match) { + return { + type: ActorEvent.RELATIVE_POINT_INPUT, + value: new Point(num(match[1]), num(match[2])), + }; + } + match = RELATIVE_POLAR_REGEXP.exec(text); + if (match) { + return { + type: ActorEvent.RELATIVE_POINT_INPUT, + value: polarToPoint(num(match[1]), num(match[2])), + }; + } + match = ABSOLUTE_POLAR_REGEXP.exec(text); + if (match) { + return { + type: ActorEvent.ABSOLUTE_POINT_INPUT, + value: polarToPoint(num(match[1]), num(match[2])), + }; + } + return null; +} diff --git a/B07_DesignDetail/openwebcad/src/tools/factories/sequence-tool.ts b/B07_DesignDetail/openwebcad/src/tools/factories/sequence-tool.ts index c4c7859b6..78596b9e8 100644 --- a/B07_DesignDetail/openwebcad/src/tools/factories/sequence-tool.ts +++ b/B07_DesignDetail/openwebcad/src/tools/factories/sequence-tool.ts @@ -176,7 +176,9 @@ export function createSequenceTool(config: SequenceToolConfig) { const drawPreview = ({ context, event }: { context: SequenceContext; event: StateEvent }) => { if (!config.preview) return; - const ghosts = config.preview(makeInput(context.values, cursorPoint(event), context.picks)); + // 각도 고정(`<30`) 중이면 커서를 고정 방향 위로 + const cursor = getPointFromEvent(lastPoint(context.values), event as DrawEvent); + const ghosts = config.preview(makeInput(context.values, cursor, context.picks)); setGhostHelperEntities(ghosts); }; @@ -281,8 +283,10 @@ export function createSequenceTool(config: SequenceToolConfig) { on.DRAW = { actions: drawPreview }; on.MOUSE_CLICK = { actions: pushPoint, target }; on.ABSOLUTE_POINT_INPUT = { actions: pushPoint, target }; - on.RELATIVE_POINT_INPUT = { actions: pushPoint, target }; - on.NUMBER_INPUT = { actions: pushPoint, target }; + // 거리 · 상대 좌표는 기준점(앞 점)이 있을 때만 — 첫 점이면 무시(도구가 멈추지 않게) + const hasBase = ({ context }: { context: SequenceContext }) => !!lastPoint(context.values); + on.RELATIVE_POINT_INPUT = { guard: hasBase, actions: pushPoint, target }; + on.NUMBER_INPUT = { guard: hasBase, actions: pushPoint, target }; if (config.repeatLastStep && index === lastIndex) { on.ENTER = { target: COMMIT_STATE }; } diff --git a/B07_DesignDetail/openwebcad/src/tools/line-tool.ts b/B07_DesignDetail/openwebcad/src/tools/line-tool.ts index 42dc7dd3d..4ea1cb27a 100644 --- a/B07_DesignDetail/openwebcad/src/tools/line-tool.ts +++ b/B07_DesignDetail/openwebcad/src/tools/line-tool.ts @@ -131,7 +131,7 @@ export const lineToolStateMachine = createMachine( const activeLine = new LineEntity( getActiveLayerId(), context.startPoint as Point, - (event as DrawEvent).drawController.getWorldMouseLocation() + getPointFromEvent(context.startPoint, event as DrawEvent) ); activeLine.lineColor = getActiveLineColor(); activeLine.lineWidth = getActiveLineWidth(); diff --git a/resources/tester/cad/keyboard-chain.test.ts b/resources/tester/cad/keyboard-chain.test.ts new file mode 100644 index 000000000..be66f34d4 --- /dev/null +++ b/resources/tester/cad/keyboard-chain.test.ts @@ -0,0 +1,150 @@ +/** + * CAD 키보드 연속 명령(PLAN 331 · 요구 224-7) — 오토캐드 방식. + * 각도 고정 `<30` → 길이 · 시퀀스 첫 점 숫자 · 이동 · 복사 · 회전 키보드 값 · 선 중 C · U · 한 번 스냅 · 동적 입력 Tab. + * 시험 쪽 flatten-js 는 다른 사본(「Illegal Parameters」) — 점은 앱 코드로만 만들고 숫자로 판정. + * 실행: cd B07_DesignDetail/openwebcad && npx vitest run --dir ../../resources/tester/cad keyboard-chain + */ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +vi.mock("react-toastify", () => ({ + toast: Object.assign(vi.fn(), { error: vi.fn(), info: vi.fn(), warn: vi.fn(), success: vi.fn() }), +})); + +import { toast } from "react-toastify"; +import { InputController } from "../../../B07_DesignDetail/openwebcad/src/inputController/input-controller"; +import { getAngleLock } from "../../../B07_DesignDetail/openwebcad/src/inputController/input-angle-lock"; +import { + getActiveToolActor, + getEntities, + setEntities, + setSelectedEntityIds, +} from "../../../B07_DesignDetail/openwebcad/src/state"; +import { runCommandInput } from "../../../B07_DesignDetail/openwebcad/src/commands/run-command"; +import { initApplication } from "../../../B07_DesignDetail/openwebcad/test/helpers/init-application"; +import { CANVAS_HEIGHT } from "../../../B07_DesignDetail/openwebcad/test/helpers/tests.consts"; + +type Xy = { x: number; y: number }; +type Seg = { start: Xy; end: Xy }; + +const key = (controller: InputController, k: string) => + controller.handleKeyStroke({ + key: k, + code: "", + keyCode: 0, + isComposing: false, + shiftKey: false, + ctrlKey: false, + altKey: false, + metaKey: false, + repeat: false, + target: null, + preventDefault: vi.fn(), + stopPropagation: vi.fn(), + } as unknown as KeyboardEvent); + +/** 글자를 치고 Space(= Enter) */ +const type = (controller: InputController, text: string) => { + for (const ch of text) key(controller, ch); + key(controller, " "); +}; + +/** 세계 좌표 (x, y) 로 커서 이동 — 시험 캔버스는 축척 1 · y 위 + */ +const move = (controller: InputController, x: number, y: number) => + controller.handleMouseMove({ clientX: x, clientY: CANVAS_HEIGHT - y } as MouseEvent); + +const clickAt = (controller: InputController, x: number, y: number) => { + move(controller, x, y); + controller.handleMouseUp({ + button: 0, + clientX: x, + clientY: CANVAS_HEIGHT - y, + target: null, + preventDefault: () => {}, + stopPropagation: () => {}, + } as unknown as MouseEvent); +}; + +const lines = () => + getEntities() + .filter((entity) => entity.getType() === "Line") + .map((entity) => (entity as unknown as { segment: Seg }).segment); + +const expectXy = (point: Xy, x: number, y: number) => { + expect(point.x).toBeCloseTo(x, 3); + expect(point.y).toBeCloseTo(y, 3); +}; + +let controller: InputController; + +beforeEach(() => { + vi.mocked(toast.error).mockClear(); + controller = initApplication(); + setEntities([], true); + setSelectedEntityIds([]); +}); + +describe("331-1 각도 고정 → 길이", () => { + it("L · 0,0 · <30 · 100 → (86.603, 50) · <90 · 50 → (86.603, 100)", () => { + type(controller, "l"); + type(controller, "0,0"); + move(controller, 500, 500); // 커서 방향과 무관하게 고정 각도로 + type(controller, "<30"); + expect(getAngleLock()).toBe(30); + type(controller, "100"); + expect(getAngleLock()).toBeNull(); // 한 점만 + type(controller, "<90"); + type(controller, "50"); + const drawn = lines(); + expect(drawn).toHaveLength(2); + expectXy(drawn[0].start, 0, 0); + expectXy(drawn[0].end, 86.603, 50); + expectXy(drawn[1].end, 86.603, 100); + }); + + it("고정 없이 숫자 = 마우스 방향", () => { + type(controller, "l"); + type(controller, "0,0"); + move(controller, 0, 300); + type(controller, "100"); + expectXy(lines()[0].end, 0, 100); + }); + + it("고정 중 클릭 = 고정 방향 위로 내림 · Esc · 새 명령이면 풂", () => { + type(controller, "l"); + type(controller, "0,0"); + type(controller, "<0"); + clickAt(controller, 60, 25); // 가이드에서 먼 자리(스냅 안 붙게) + expectXy(lines()[0].end, 60, 0); + type(controller, "<45"); + key(controller, "Escape"); + expect(getAngleLock()).toBeNull(); + type(controller, "<45"); + runCommandInput("CIRCLE"); + expect(getAngleLock()).toBeNull(); + }); + + it("음수 · .5 숫자 · 음수 각도", () => { + type(controller, "l"); + type(controller, "0,0"); + type(controller, "<-90"); + type(controller, ".5"); + expectXy(lines()[0].end, 0, -0.5); + type(controller, "<0"); + type(controller, "-10"); + expectXy(lines()[1].end, -10, -0.5); + }); + + it("PLINE 첫 점에 숫자 · @x,y → 무시 · 도구 살아 있음 · 알림 0", () => { + type(controller, "pline"); + type(controller, "100"); + type(controller, "@10,0"); + const snapshot = getActiveToolActor()?.getSnapshot(); + expect(snapshot?.status).toBe("active"); + expect(snapshot?.value).toBe("STEP_0"); + expect(toast.error).not.toHaveBeenCalled(); + type(controller, "0,0"); + type(controller, "<0"); + type(controller, "100"); + expect(getActiveToolActor()?.getSnapshot()?.context.values).toHaveLength(2); + }); +}); From e36fc5091d04120dc0e57e070a8a4da01aa321b0 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 15 Oct 2026 12:33:32 +0900 Subject: [PATCH 21/81] =?UTF-8?q?feat(=EC=9D=BC=EC=9C=84=EB=8C=80=EA=B0=80?= =?UTF-8?q?=EC=8B=A4=EB=AC=B4):=20329-3=20=EC=97=91=EC=85=80=20=EB=B3=B4?= =?UTF-8?q?=EA=B8=B0=20=EC=B0=BD=20=E2=80=94=20=EC=9D=BD=EA=B8=B0=20?= =?UTF-8?q?=EC=A0=84=EC=9A=A9=20=EC=8B=9C=ED=8A=B8(=EC=9D=BC=EC=9C=84?= =?UTF-8?q?=EB=8C=80=EA=B0=80=20=C2=B7=20=EB=8B=A8=EA=B0=80=EC=82=B0?= =?UTF-8?q?=EC=B6=9C=20=ED=83=AD)=20+=20=ED=98=B8=ED=91=9C=20=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D(=EC=B2=B4=ED=81=AC=20=C2=B7=20=EC=B0=BE=EA=B8=B0=20?= =?UTF-8?q?=C2=B7=20=EB=A7=9E=EC=B6=A4)=20=C2=B7=20=EB=AA=A9=EB=A1=9D=20?= =?UTF-8?q?=EB=88=84=EB=A6=84=20=E2=86=92=20=EC=98=81=EC=97=AD=20=EA=B0=95?= =?UTF-8?q?=EC=A1=B0=20=C2=B7=20=EC=B9=B8=20=EB=88=84=EB=A6=84=20=E2=86=92?= =?UTF-8?q?=20=EA=B7=B8=20=ED=98=B8=ED=91=9C=20=C2=B7=20=EA=B3=A0=EB=A5=B8?= =?UTF-8?q?=20=ED=98=B8=ED=91=9C=20=ED=95=9C=20=EB=B2=88=EC=97=90=20?= =?UTF-8?q?=EC=82=AC=EC=A0=84=EC=A1=B0=ED=95=A9=20+=20=EA=B2=B0=EA=B3=BC?= =?UTF-8?q?=20=ED=91=9C=20=C2=B7=20=EC=97=AC=EB=8A=94=20=EC=9E=90=EB=A6=AC?= =?UTF-8?q?=20=EB=84=B7(=EC=98=AC=EB=A6=B0=20=EC=A7=81=ED=9B=84=20=C2=B7?= =?UTF-8?q?=20=EC=97=91=EC=85=80=EB=A1=9C=20=EB=B3=B4=EA=B8=B0=20=C2=B7=20?= =?UTF-8?q?=EC=97=91=EC=85=80=EC=97=90=EC=84=9C=20=EB=B3=B4=EA=B8=B0=20?= =?UTF-8?q?=C2=B7=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80=20=EB=8D=A7?= =?UTF-8?q?=EC=A4=84)=20=C2=B7=20=EB=8A=A6=EA=B2=8C=20=EB=B6=88=EB=9F=AC?= =?UTF-8?q?=EC=98=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01XWh5wNNycMkbwMpACz1rte --- .../M02_MasterTemplete_Hopyo.css | 78 +++++ .../M02_MasterTemplete_Hopyo_Api.ts | 21 ++ .../M02_MasterTemplete_Hopyo_Pick.ts | 40 ++- .../M02_MasterTemplete_Hopyo_Sheet.ts | 282 ++++++++++++++++++ .../M02_MasterTemplete_Hopyo_Side.ts | 5 +- .../M02_MasterTemplete_Hopyo_View.ts | 25 +- resources/tester/helper_m02_hopyo_pick.cjs | 18 +- resources/tester/test_m02_hopyo_screen.py | 47 ++- 8 files changed, 508 insertions(+), 8 deletions(-) create mode 100644 M02_MasterTemplete/M02_MasterTemplete_Hopyo_Sheet.ts diff --git a/M02_MasterTemplete/M02_MasterTemplete_Hopyo.css b/M02_MasterTemplete/M02_MasterTemplete_Hopyo.css index 7a00414cc..134403225 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Hopyo.css +++ b/M02_MasterTemplete/M02_MasterTemplete_Hopyo.css @@ -149,3 +149,81 @@ gap: var(--spacing-4); margin-bottom: var(--spacing-8); } + +/* 엑셀 보기 창(329) — 왼쪽 읽기 전용 시트 · 오른쪽 호표 목록 · 불러오기 결과 */ +.m02-hopyo-sheet__dialog > .ui-modal__body { + overflow: hidden; +} + +.m02-hopyo-sheet { + display: grid; + grid-template-columns: minmax(0, 1fr) 22rem; + gap: var(--spacing-8); + flex: 1; + min-height: 0; +} + +.m02-hopyo-sheet__grid { + display: flex; + flex-direction: column; + min-width: 0; + min-height: 0; + border: 1px solid var(--color-border); + border-radius: var(--radius-sm); + overflow: hidden; +} + +.m02-hopyo-sheet__side { + display: flex; + flex-direction: column; + gap: var(--spacing-8); + min-height: 0; +} + +.m02-hopyo-sheet__list { + flex: 1; + min-height: 0; + overflow: auto; + border: 1px solid var(--color-border); + border-radius: var(--radius-sm); +} + +.m02-hopyo-sheet__item { + display: flex; + align-items: center; + gap: var(--spacing-4); + padding: var(--spacing-4) var(--spacing-8); + border-left: 3px solid transparent; + cursor: pointer; +} + +.m02-hopyo-sheet__item:hover { + background: var(--color-surface); +} + +.m02-hopyo-sheet__item.is-current { + border-left-color: var(--color-accent); + background: var(--color-mist-violet); +} + +.m02-hopyo-sheet__no { + white-space: nowrap; + font-variant-numeric: tabular-nums; +} + +.m02-hopyo-sheet__name { + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.m02-hopyo-sheet__result { + max-height: 40%; + overflow: auto; +} + +.m02-hopyo-sheet__result:empty { + display: none; +} diff --git a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Api.ts b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Api.ts index e11e7dcec..c4ef47641 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Api.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Api.ts @@ -5,6 +5,7 @@ * ========================================================================== */ import { API_BASE_URL } from "@config/config_frontend"; +import type { Workbook } from "../A00_Common/spreadsheet/spreadsheet"; import { call, getActiveLayer, getActiveOwner, type Layer } from "./M02_MasterTemplete_Api_Fetch"; import type { LaborRef } from "./M02_MasterTemplete_Labor_Calc"; @@ -34,6 +35,22 @@ export interface HopyoBrief { 금액: Money; /** [들어감 수, 줄 수] */ 맞춤: [number, number]; + /** 일위대가 시트 행 [시작, 끝](엑셀 1부터 · 329 · 옛 파일 없음) */ + 행?: [number, number]; +} + +/** 엑셀 보기 창 목록 한 줄 — 호표 · 단가산출 블록 행 범위 */ +export interface HopyoSpan { + 번: number; + 이름: string; + 행: [number, number]; +} + +/** 엑셀 보기 창 격자(329) — 시트 「일위대가」 · 「단가산출」 */ +export interface HopyoGrid { + 책: Workbook; + 호표: HopyoSpan[]; + 단산: HopyoSpan[]; } export interface HopyoView extends HopyoFile { @@ -136,6 +153,10 @@ export const listHopyo = async (scope: HopyoScope): Promise => export const readHopyo = (scope: HopyoScope, key: string): Promise => call(at(scope, `/hopyo/${enc(key)}`)); +/** 격자 — 없으면 404 「격자 없음 — 다시 올리면 보임」(옛 파일) */ +export const readHopyoGrid = (scope: HopyoScope, key: string): Promise => + call(at(scope, `/hopyo-grid/${enc(key)}`)); + export const readHopyoOne = (scope: HopyoScope, key: string, no: number): Promise => call(at(scope, `/hopyo/${enc(key)}/${no}`)); diff --git a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Pick.ts b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Pick.ts index 04cdd4c89..c460ce6a3 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Pick.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Pick.ts @@ -2,7 +2,8 @@ * M02_MasterTemplete_Hopyo_Pick.ts * [로직 추가] 창의 「실무 호표」 줄(PLAN 324-4) — 일위대가 문서 층 · 위 층 실무자료의 호표마다 한 줄(맞춤 n/m) · * 고르면 서버가 그 호표를 문서 층 사전조합으로 만들고(`/hopyo-build` · 있던 것은 다시 씀) `lib.pick` 으로 - * 참조 줄(별칭 = 호표 품명) · 빠진 줄이 있으면 알림. 프로젝트 층엔 없음. + * 참조 줄(별칭 = 호표 품명) · 빠진 줄이 있으면 알림 · 파일마다 「엑셀로 보기 · 여러 호표」 줄(329 엑셀 보기 창 · + * 고른 호표마다 참조 줄). 프로젝트 층엔 없음. * ========================================================================== */ import { showToast } from "@ui/ui_template_elements"; @@ -25,7 +26,42 @@ export function withHopyo(lib: LibraryPick): LibraryPick { { value: "precombo", text: tc("Kind_Combo") }, { value: HOPYO_KIND, text: "실무 호표" }, ], - more: async () => (await listHopyoPicks(scope)).map((row) => hopyoExtra(row, lib)), + more: async () => { + const rows = await listHopyoPicks(scope); + const files = new Map(); + for (const row of rows) { + const key = `${row.층}|${row.주인 ?? ""}|${row.키}`; + files.set(key, [...(files.get(key) ?? []), row]); + } + return [ + ...rows.map((row) => hopyoExtra(row, lib)), + ...[...files].map(([key, list]) => sheetExtra(key, list[0], list.length, lib)), + ]; + }, + }; +} + +/** 파일 하나 — 엑셀 보기 창에서 여러 호표를 골라 한 번에(만드는 층 = 문서 층) */ +export function sheetExtra(key: string, row: HopyoPickRow, n: number, lib: LibraryPick): PickExtra { + return { + kind: HOPYO_KIND, + key: `${key}|엑셀`, + tag: "실무 호표", + name: "엑셀로 보기 · 여러 호표", + sub: `${row.파일} · ${LAYER_TEXT[row.층] ?? row.층}`, + unit: `호표 ${n}`, + onPick: () => + void import("./M02_MasterTemplete_Hopyo_Sheet").then((m) => + m.openHopyoSheet({ + scope: { layer: row.층, owner: row.주인 }, + key: row.키, + 파일: row.파일, + canWrite: true, + target: { layer: lib.layer, owner: lib.owner }, + onImport: (got, h) => + lib.pick(got.참조, got.이름, h.이름.slice(0, ALIAS_MAX).trim() || null), + }), + ), }; } diff --git a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Sheet.ts b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Sheet.ts new file mode 100644 index 000000000..e547de48e --- /dev/null +++ b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Sheet.ts @@ -0,0 +1,282 @@ +/* ============================================================================= + * M02_MasterTemplete_Hopyo_Sheet.ts + * 일위대가 실무자료 엑셀 보기 창(PLAN 329) — 큰 모달 · 왼쪽 = 올린 엑셀을 연 것처럼 읽기 전용 시트(A00 산출근거 + * 스프레드시트 · 탭 일위대가 · 단가산출) · 오른쪽 = 호표 목록(체크 · 찾기 · 맞춤 n/m) + [고른 n개 불러오기]. + * · 목록 누름 → 시트에서 그 호표 영역(B열 ~ 끝 열) 고름 · 칸 누름 → 그 행 호표를 목록에서 강조 · 영역 고름 + * · 불러오기 = 고른 호표마다 차례로 사전조합 만들기(`/hopyo-build`) → 결과 표(사전조합 키 · 빠진 줄 · 값 차이) · + * `onImport` 가 있으면 호표마다 알림([로직 추가] 참조 줄) + * · 격자 = 서버 `/hopyo-grid/{키}` · 옛 파일(격자 없음)은 시트 자리에 까닭 글 · 목록 · 불러오기는 됨 + * ⚠ 부르는 쪽은 `import()` 로 엶 · 스프레드시트도 열 때 불러옴(다른 화면 번들에 안 섞이게). + * ========================================================================== */ + +import { createButton, el, showToast } from "@ui/ui_template_elements"; +import { openModal } from "@ui/ui_template_modal"; +import type { Sheet } from "../A00_Common/spreadsheet/spreadsheet_types"; +import type { SpreadsheetHandle } from "../A00_Common/spreadsheet/spreadsheet"; +import { + buildHopyo, + readHopyo, + readHopyoGrid, + type HopyoBrief, + type HopyoBuilt, + type HopyoGrid, + type HopyoScope, + type HopyoSpan, +} from "./M02_MasterTemplete_Hopyo_Api"; +import { builtWord } from "./M02_MasterTemplete_Hopyo_View"; +import "./M02_MasterTemplete_Hopyo.css"; + +export interface HopyoSheetOptions { + /** 실무자료 파일 층 */ + scope: HopyoScope; + key: string; + 파일: string; + /** 만들 층 쓰기 권한 — 없으면 체크 · 불러오기 없음(보기만) */ + canWrite: boolean; + /** 사전조합을 만들 층(없으면 `scope` · [로직 추가] = 문서 층) */ + target?: HopyoScope; + /** 처음 고를 호표 번(호표 화면 [엑셀에서 보기]) */ + 호표?: number; + /** 처음 보일 단가산출 행(330-3 근거 [엑셀에서 보기]) */ + 단산행?: number | [number, number]; + /** 호표 하나를 만들었음 — [로직 추가] 참조 줄 */ + onImport?: (got: HopyoBuilt, hopyo: HopyoBrief) => void; +} + +const ILWI = "일위대가"; +const DANSAN = "단가산출"; + +const why = (error: unknown): string => (error instanceof Error ? error.message : String(error)); +const won = (v: number | null | undefined): string => + v === null || v === undefined ? "" : v.toLocaleString("ko-KR"); +const note = (text: string): HTMLElement => el("span", { className: "m02-hopyo__note", text }); + +/** 엑셀 행(1부터)이 든 호표 번 · 없으면 null */ +export const hopyoAtRow = (list: HopyoSpan[], row: number): number | null => + list.find((h) => h.행[0] <= row && row <= h.행[1])?.번 ?? null; + +/** 호표 영역 A1 — B열 ~ 시트 끝 열(북부청 = `B20:O24`) */ +export const spanText = (행: [number, number], last: string): string => `B${행[0]}:${last}${행[1]}`; + +/** 시트 끝 열 글자 — 칸 · 열 정보 중 가장 오른쪽 */ +export function lastColumn(sheet: Pick): string { + let best = "A"; + for (const k of [...Object.keys(sheet.칸), ...Object.keys(sheet.열 ?? {})]) { + const c = /^[A-Z]+/.exec(k)?.[0]; + if (c && (c.length > best.length || (c.length === best.length && c > best))) best = c; + } + return best; +} + +/** 단가산출 행 → 행 전체 범위 A1(`4:9`) */ +export const rowsText = (행: number | [number, number]): string => + typeof 행 === "number" ? `${행}:${행}` : `${행[0]}:${행[1]}`; + +export function openHopyoSheet(opt: HopyoSheetOptions): void { + let sheet: SpreadsheetHandle | null = null; + let closed = false; + let briefs: HopyoBrief[] = []; + let spans: HopyoSpan[] = []; + let ilwi = ""; // 일위대가 시트 id + let last = "O"; + let at = { 시트: "", 칸: "", 범위: "" }; // 시트 고름 + let current: number | null = opt.호표 ?? null; + const checked = new Set(); + const items = new Map(); + + const left = el("div", { + className: "m02-hopyo-sheet__grid", + children: [note("엑셀을 읽는 중…")], + }); + const search = el("input", { + className: "ui-input ui-input--sm", + attrs: { type: "search", placeholder: "호표 · 품명 · 규격 찾기" }, + }) as HTMLInputElement; + const list = el("div", { className: "m02-hopyo-sheet__list" }); + const count = note(""); + const go = createButton({ label: "고른 0개 불러오기", variant: "filled", size: "sm" }); + go.disabled = true; + const result = el("div", { className: "m02-hopyo-sheet__result" }); + const right = el("div", { + className: "m02-hopyo-sheet__side", + children: [ + search, + el("div", { className: "m02-hopyo__bar", children: [count, ...(opt.canWrite ? [go] : [])] }), + list, + result, + ], + }); + + openModal({ + title: `엑셀 보기 — ${opt.파일}`, + closeLabel: "닫기", + wide: true, + dialogClass: "m02-modal m02-hopyo-sheet__dialog", + beforeClose: async () => { + closed = true; + sheet?.destroy(); + return true; + }, + mount: (body) => + body.append(el("div", { className: "m02-hopyo-sheet", children: [left, right] })), + }); + + const paintGo = (): void => { + go.querySelector(".ui-btn__label")!.textContent = `고른 ${checked.size}개 불러오기`; + go.disabled = !checked.size; + }; + + /** 목록 강조 · 보이게(시트 고름은 그대로) */ + function mark(no: number | null): void { + items.get(current ?? NaN)?.classList.remove("is-current"); + current = no; + const row = items.get(no ?? NaN); + row?.classList.add("is-current"); + row?.scrollIntoView({ block: "nearest" }); + } + + /** 호표 하나 강조 — 목록 · 시트 영역(같은 범위면 다시 안 부름) */ + function focus(no: number): void { + mark(no); + const span = spans.find((h) => h.번 === no)?.행 ?? briefs.find((h) => h.번 === no)?.행; + if (!sheet || !span) return; + const text = spanText(span, last); + if (at.시트 !== ilwi || at.범위 !== text) sheet.select(text, ilwi); + } + + function paintList(): void { + const q = search.value.trim().toLowerCase(); + items.clear(); + const shown = briefs.filter( + (h) => !q || `제${h.번}호표 ${h.이름} ${h.규격}`.toLowerCase().includes(q), + ); + list.replaceChildren( + ...shown.map((h) => { + const box = el("input", { attrs: { type: "checkbox" } }) as HTMLInputElement; + box.checked = checked.has(h.번); + box.hidden = !opt.canWrite; + box.addEventListener("click", (e) => e.stopPropagation()); + box.addEventListener("change", () => { + if (box.checked) checked.add(h.번); + else checked.delete(h.번); + paintGo(); + }); + const row = el("div", { + className: `m02-hopyo-sheet__item${h.번 === current ? " is-current" : ""}`, + attrs: { "data-hopyo-no": String(h.번), title: `${h.이름} ${h.규격}`.trim() }, + children: [ + box, + el("span", { className: "m02-hopyo-sheet__no", text: `제${h.번}호표` }), + el("span", { className: "m02-hopyo-sheet__name", text: `${h.이름} ${h.규격}`.trim() }), + note(`${h.맞춤[0]}/${h.맞춤[1]}`), + ], + }); + row.addEventListener("click", () => focus(h.번)); + items.set(h.번, row); + return row; + }), + ); + count.textContent = q ? `${shown.length} / ${briefs.length}` : `호표 ${briefs.length}`; + } + search.addEventListener("input", paintList); + + async function mountSheet(grid: HopyoGrid): Promise { + const { createSpreadsheet } = await import("../A00_Common/spreadsheet/spreadsheet"); + if (closed) return; + const book = structuredClone(grid.책); + const first = book.시트.find((s) => s.이름 === ILWI) ?? book.시트[0]; + if (!first) return void left.replaceChildren(note("시트 없음")); + ilwi = first.id; + last = lastColumn(first); + book.활성 = first.id; + left.replaceChildren(); + sheet = createSpreadsheet(left, book, { + readOnly: true, + onSelect: (s) => { + at = s; + if (s.시트 !== ilwi) return; + const no = hopyoAtRow(spans, Number(/\d+/.exec(s.칸)?.[0])); + if (no !== null && no !== current) mark(no); + }, + }); + // 칸을 눌러 뗌 → 그 호표 영역 고름(끌기 · 방향키 도중엔 안 바꿈) + left.querySelector(".ss-grid-host")?.addEventListener("mouseup", (e) => { + if ((e as MouseEvent).button !== 0 || at.시트 !== ilwi) return; + const no = hopyoAtRow(spans, Number(/\d+/.exec(at.칸)?.[0])); + if (no !== null) focus(no); + }); + } + + async function load(): Promise { + try { + const [file, grid] = await Promise.all([ + readHopyo(opt.scope, opt.key), + readHopyoGrid(opt.scope, opt.key).catch((error: unknown) => new Error(why(error))), + ]); + if (closed) return; + briefs = file.호표; + spans = grid instanceof Error ? [] : grid.호표; + paintList(); + if (grid instanceof Error) return void left.replaceChildren(note(grid.message)); + await mountSheet(grid); + if (opt.단산행 !== undefined) sheet?.select(rowsText(opt.단산행), DANSAN); + else if (current !== null) focus(current); + } catch (error) { + if (!closed) left.replaceChildren(note(`못 읽음 — ${why(error)}`)); + } + } + + async function importPicked(): Promise { + const nos = [...checked].sort((a, b) => a - b); + const target = opt.target ?? opt.scope; + const rows: HTMLElement[] = []; + let ok = 0; + go.disabled = true; + for (const no of nos) { + const h = briefs.find((x) => x.번 === no); + if (!h || closed) continue; + const head = el("td", { text: `제${no}호표 ${h.이름}`.trim() }); + try { + const from = { 층: opt.scope.layer, 주인: opt.scope.owner, 키: opt.key, 번: no }; + const got = await buildHopyo(target, from); + ok += 1; + opt.onImport?.(got, h); + rows.push( + el("tr", { + attrs: { "data-built": got.참조.키 }, + children: [ + head, + el("td", { text: `${got.참조.키} · ${builtWord(got, false)}` }), + el("td", { text: String(got.빠진줄.length), className: "m02-hopyo__num" }), + el("td", { text: won(got.비교.차이), className: "m02-hopyo__num" }), + ], + }), + ); + } catch (error) { + rows.push(el("tr", { children: [head, el("td", { text: `실패 — ${why(error)}` })] })); + } + result.replaceChildren( + el("table", { + className: "ui-general-block__table m02-hopyo__table", + children: [ + el("thead", { + children: [ + el("tr", { + children: ["호표", "사전조합", "빠진 줄", "값 차이"].map((t) => + el("th", { text: t }), + ), + }), + ], + }), + el("tbody", { children: rows }), + ], + }), + ); + } + paintGo(); + showToast(`불러옴 ${ok} / ${nos.length}`, ok === nos.length ? "success" : "warning"); + } + go.addEventListener("click", () => void importPicked()); + + void load(); +} diff --git a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Side.ts b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Side.ts index 74ce15703..3bd583efb 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Side.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Side.ts @@ -2,7 +2,8 @@ * M02_MasterTemplete_Hopyo_Side.ts * 좌측 「일위대가 실무자료」 컨테이너(PLAN 324-4 · 일위대가 사전조합 아래) — 층 = 구조물 조합 층(화면 층) · * [엑셀 올리기](숨은 file 입력 .xls · .xlsx · 쓰기 권한 없으면 꺼짐) + 파일 줄 → 누르면 본 화면 - * (`M02_MasterTemplete_Hopyo_View`) · 고름 표시 = 본 화면이 메인 칸에 붙어 있는 동안만. + * (`M02_MasterTemplete_Hopyo_View`) · 고름 표시 = 본 화면이 메인 칸에 붙어 있는 동안만 · + * 올린 직후 엑셀 보기 창(329 · `M02_MasterTemplete_Hopyo_Sheet` 늦게 불러옴)을 자동으로 엶. * ========================================================================== */ import { createButton, el, showToast } from "@ui/ui_template_elements"; @@ -66,6 +67,8 @@ export function buildHopyoSection(opt: HopyoSectionOptions): HopyoSection { showToast(`올림 — 호표 ${got.호표수}${wrong}`, got.어긋남 ? "warning" : "success"); await refresh(); await show(got); + const sheet = await import("./M02_MasterTemplete_Hopyo_Sheet"); + sheet.openHopyoSheet({ scope, key: got.키, 파일: got.파일, canWrite: opt.canWrite }); }) .catch((error) => showToast(why(error), "error")) .finally(() => (upload.disabled = !opt.canWrite)); diff --git a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_View.ts b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_View.ts index 5601885cf..3aa5fe2c8 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_View.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_View.ts @@ -2,7 +2,8 @@ * M02_MasterTemplete_Hopyo_View.ts * 일위대가 실무자료 본 화면(PLAN 324-4) — 파일 하나: 파일 · 시트 · 호표 수 · 검산 · 맞춤 n/m · 호표 표 + 찾기 · * [지우기] / 호표 하나: 줄 표(갈래 · 품명 · 규격 · 수량 · 단위 · 엑셀 단가 · 맞춤 · 대상 · 입력값 · 환산 · - * [고르기] [확정] [빼기]) · [같음 모두 확정] · 빠진 줄 · [사전조합 만들기] → 값 비교 · 다시 씀이면 [새로 만들기]. + * [고르기] [확정] [빼기]) · [같음 모두 확정] · 빠진 줄 · [사전조합 만들기] → 값 비교 · 다시 씀이면 [새로 만들기] · + * [엑셀로 보기] · [엑셀에서 보기](329 엑셀 보기 창 · 늦게 불러옴 · 호표 화면은 그 호표 고른 채). * 맞춤 · 만들기는 서버(`M02_Hopyo_Match` · `M02_Hopyo_Build`) · 쓰기 단추는 층 쓰기 권한만. * ========================================================================== */ @@ -25,6 +26,7 @@ import { type HopyoTarget, type HopyoView, } from "./M02_MasterTemplete_Hopyo_Api"; +import type { HopyoSheetOptions } from "./M02_MasterTemplete_Hopyo_Sheet"; import { openLogicPicker } from "./M02_MasterTemplete_Labor_Pick"; import { pickMaterial } from "./M02_Material_Pick"; import "./M02_MasterTemplete_Hopyo.css"; @@ -52,6 +54,9 @@ const why = (error: unknown): string => (error instanceof Error ? error.message const won = (v: number | null | undefined): string => v === null || v === undefined ? "" : v.toLocaleString("ko-KR"); const fail = (error: unknown): void => showToast(`실패 — ${why(error)}`, "error"); +/** 엑셀 보기 창(329) — 열 때 불러옴 */ +const openSheet = (o: HopyoSheetOptions): void => + void import("./M02_MasterTemplete_Hopyo_Sheet").then((m) => m.openHopyoSheet(o), fail); const grid = (heads: string[], body: HTMLElement, cls = ""): HTMLElement => el("div", { @@ -176,6 +181,12 @@ export function buildHopyoView(key: string, opt: HopyoViewOptions): HTMLElement note(`호표 ${file.호표수}`), note(wrong.length ? `검산 어긋남 ${wrong.length}` : "검산 맞음"), note(`맞춤 ${file.맞춤[0]} / ${file.맞춤[1]}`), + createButton({ + label: "엑셀로 보기", + size: "sm", + onClick: () => + openSheet({ scope: opt.scope, key, 파일: file.파일, canWrite: opt.canWrite }), + }), ...(opt.canWrite ? [dropButton(file)] : []), ], }), @@ -240,6 +251,18 @@ export function buildHopyoView(key: string, opt: HopyoViewOptions): HTMLElement note(h.단위), note(`엑셀 단가 ${won(h.금액.계)}`), note(`맞춤 ${h.맞춤[0]} / ${h.맞춤[1]}`), + createButton({ + label: "엑셀에서 보기", + size: "sm", + onClick: () => + openSheet({ + scope: opt.scope, + key, + 파일: got.파일, + canWrite: opt.canWrite, + 호표: no, + }), + }), ...tools, ], }), diff --git a/resources/tester/helper_m02_hopyo_pick.cjs b/resources/tester/helper_m02_hopyo_pick.cjs index a061c282c..1a389f69f 100644 --- a/resources/tester/helper_m02_hopyo_pick.cjs +++ b/resources/tester/helper_m02_hopyo_pick.cjs @@ -1,6 +1,7 @@ /* M02 일위대가 실무자료(324-4) 헬퍼 — `M02_MasterTemplete_Hopyo_Pick.ts`(고르기 창 「실무 호표」 덧줄) · - * `M02_MasterTemplete_Hopyo_View.ts` 글 도우미를 트랜스파일해 Node 로 돌림 · fetch 대역이 부른 길 · 몸을 모음 · - * stdin = {rows, built} · stdout = {calls, kinds, rows, picked, project, view} JSON. */ + * `M02_MasterTemplete_Hopyo_View.ts` 글 도우미 · `M02_MasterTemplete_Hopyo_Sheet.ts` 엑셀 보기 창 도우미(329 · 행 → 호표)를 + * 트랜스파일해 Node 로 돌림 · fetch 대역이 부른 길 · 몸을 모음 · + * stdin = {rows, built} · stdout = {calls, kinds, rows, picked, project, view, sheet} JSON. */ const fs = require("fs"); const path = require("path"); const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript")); @@ -55,6 +56,13 @@ function loadTs(file) { await pick.pickHopyo(input.rows[0], lib); const project = pick.withHopyo({ ...lib, layer: "project" }); const view = loadTs(path.join(M02, "M02_MasterTemplete_Hopyo_View.ts")); + const sheet = loadTs(path.join(M02, "M02_MasterTemplete_Hopyo_Sheet.ts")); + const api = loadTs(path.join(M02, "M02_MasterTemplete_Hopyo_Api.ts")); + await api.readHopyoGrid({ layer: "company", owner: "7" }, "HP-000001"); + const spans = [ + { 번: 3, 이름: "임도표지석", 행: [20, 24] }, + { 번: 4, 이름: "배수관", 행: [25, 30] }, + ]; const out = { calls, kinds: wrapped.kindFilter.map((k) => k.value), @@ -80,6 +88,12 @@ function loadTs(file) { view.builtWord({ 다시씀: true }, true), ], }, + sheet: { + at: [19, 20, 22, 24, 25, 30, 31].map((row) => sheet.hopyoAtRow(spans, row)), + span: sheet.spanText([20, 24], sheet.lastColumn({ 칸: { A1: {}, N24: {} }, 열: { O: {} } })), + last: [sheet.lastColumn({ 칸: { Z9: {}, AA1: {} } }), sheet.lastColumn({ 칸: {} })], + rows: [sheet.rowsText(4345), sheet.rowsText([93, 101])], + }, }; process.stdout.write(JSON.stringify(out)); })().catch((error) => { diff --git a/resources/tester/test_m02_hopyo_screen.py b/resources/tester/test_m02_hopyo_screen.py index 46345114b..804e09399 100644 --- a/resources/tester/test_m02_hopyo_screen.py +++ b/resources/tester/test_m02_hopyo_screen.py @@ -3,6 +3,7 @@ - 고르기 창 덧줄(Node · fetch 대역): 이 층 · 위 층 호표 줄(맞춤 n/m) · 종류 고르기 · 고르면 문서 층에 만들기 길 → `lib.pick` 참조 줄(별칭 = 호표 품명) · 프로젝트 층엔 없음 · 본 화면 글 도우미 +- 엑셀 보기 창(329): 행 → 호표 · 호표 영역 A1 · 격자 길 · 여는 자리 넷 · 늦게 불러옴 - 소스: 좌측 판 자리(사전조합 아래 · 구조물 조합 층 · 쓰기 권한 없으면 올리기 꺼짐 · 숨은 file 입력) · Labor_Detail 줄 수 그대로 · 새 파일 이름 · 700 줄 · 색 = 공용 토큰 """ @@ -15,7 +16,7 @@ from pathlib import Path HERE = os.path.dirname(os.path.abspath(__file__)) M02 = Path(HERE).parents[1] / "M02_MasterTemplete" -NEW = ("Hopyo_Api", "Hopyo_Pick", "Hopyo_Side", "Hopyo_View") +NEW = ("Hopyo_Api", "Hopyo_Pick", "Hopyo_Side", "Hopyo_View", "Hopyo_Sheet") def _text(name: str) -> str: @@ -68,7 +69,15 @@ def test_고르기_창_실무_호표_줄과_고름(): "name": "임도표지석 910×600×150", "sub": "북부청.xls · 제3호표 · 마스터", "unit": "개소 · 맞춤 2/4", - } + }, + { + "kind": "hopyo", + "key": "system||HP-000001|엑셀", + "tag": "실무 호표", + "name": "엑셀로 보기 · 여러 호표", + "sub": "북부청.xls · 마스터", + "unit": "호표 1", + }, ] build = got["calls"][1] assert build["url"] == "/api/m02/layers/company/hopyo-build?owner=7" # 만드는 층 = 문서 층 @@ -145,3 +154,37 @@ def test_로직_추가_한_곳과_파일_규칙(): css = _text("M02_MasterTemplete_Hopyo.css") assert "var(--color-" in css and not re.search(r"#[0-9a-fA-F]{3,6}\b", css) assert '"./M02_MasterTemplete_Hopyo.css"' in _text("M02_MasterTemplete_Hopyo_View.ts") + + +def test_엑셀_보기_창_행과_호표(): + got = _run() + assert got["calls"][2] == { + "url": "/api/m02/layers/company/hopyo-grid/HP-000001?owner=7", + "method": "GET", + "body": None, + } + sheet = got["sheet"] + assert sheet["at"] == [None, 3, 3, 3, 4, 4, None] # 22행 → 제3호표 · 범위 밖 = 없음 + assert sheet["span"] == "B20:O24" # 제3호표 영역 = B열 ~ 끝 열(열 폭 정보 O) + assert sheet["last"] == ["AA", "A"] + assert sheet["rows"] == ["4345:4345", "93:101"] # 단가산출 행 전체 + + +def test_엑셀_보기_창_여는_자리와_늦게_불러옴(): + lazy = 'import("./M02_MasterTemplete_Hopyo_Sheet")' + side = _text("M02_MasterTemplete_Hopyo_Side.ts") + assert lazy in side.split("await show(got);")[1].split("})")[0] # 올린 직후 자동 + view = _text("M02_MasterTemplete_Hopyo_View.ts") + assert lazy in view and '"엑셀로 보기"' in view and '"엑셀에서 보기"' in view + assert "호표: no" in view # 호표 화면 = 그 호표 고른 채 + pick = _text("M02_MasterTemplete_Hopyo_Pick.ts") + assert lazy in pick and "lib.pick(got.참조" in pick + static = r'^import (?!type )[^(;]*"\./M02_MasterTemplete_Hopyo_Sheet"' + for text in (side, view, pick): + assert not re.search(static, text, re.M) # 정적 import 없음(타입만) + sheet = _text("M02_MasterTemplete_Hopyo_Sheet.ts") + assert 'import("../A00_Common/spreadsheet/spreadsheet")' in sheet + assert not re.search( + r'^import \{[^}]*\} from "\.\./A00_Common/spreadsheet/spreadsheet"', sheet, re.M + ) + assert "readOnly: true" in sheet and "wide: true" in sheet From f127c0179e75983629b94f179a08a288458a4a53 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 15 Oct 2026 12:34:58 +0900 Subject: [PATCH 22/81] =?UTF-8?q?feat(=EB=85=B8=EC=84=A0=EC=B6=94=EC=B2=9C?= =?UTF-8?q?):=20327-1=20=EC=B6=94=EC=B2=9C=20=EA=B2=B0=EA=B3=BC=20?= =?UTF-8?q?=EC=98=81=EA=B5=AC=20=EC=A0=80=EC=9E=A5=20=E2=80=94=20=EC=9E=85?= =?UTF-8?q?=EB=A0=A5=20=EC=A7=80=EB=AC=B8=20=ED=82=A4=20=C2=B7=20=EA=B0=99?= =?UTF-8?q?=EC=9D=80=20=EC=9E=85=EB=A0=A5=EC=9D=B4=EB=A9=B4=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=EB=B3=B8=20=C2=B7=20=EB=B0=94=EB=80=90=20=EC=9E=85?= =?UTF-8?q?=EB=A0=A5=EB=A7=8C=20=EC=85=88=ED=95=B4=20=EB=8D=94=ED=95=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 새 B05_Profile_Engine_RouteInitial_Store(ALGO_VERSION · request_fingerprint · 읽기 · 쓰기 · 묶음 잠금) · 자리 B05_Profile/route_recommend/<키>.json · 덮어쓰기 없음 · 상한 없음 - 엔드포인트 몸통 compute_initial(찾기 → 셈 · 더함 · 덮개 수 꺼낼 때 다시 셈 · stored 표시) · precompute_defaults(기본 다섯 알약) - resolve_inputs 로 기본값 풀이 한 곳 · 머리글 「파일 안 씀」 정정 - 시험 test_224_3_recommend_store Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_014utrjfoW831Vc5wJUd13kL --- .../B05_Profile_Engine_RouteInitial.py | 26 +- ...B05_Profile_Engine_RouteInitial_Profile.py | 5 +- .../B05_Profile_Engine_RouteInitial_Store.py | 128 ++++++++++ B05_Profile/B05_Profile_Router_Replan.py | 2 +- .../B05_Profile_Router_RouteInitial.py | 110 +++++++-- .../tester/test_224_3_recommend_store.py | 231 ++++++++++++++++++ 6 files changed, 470 insertions(+), 32 deletions(-) create mode 100644 B05_Profile/B05_Profile_Engine_RouteInitial_Store.py create mode 100644 resources/tester/test_224_3_recommend_store.py diff --git a/B05_Profile/B05_Profile_Engine_RouteInitial.py b/B05_Profile/B05_Profile_Engine_RouteInitial.py index ba7278c84..13a1e0fd5 100644 --- a/B05_Profile/B05_Profile_Engine_RouteInitial.py +++ b/B05_Profile/B05_Profile_Engine_RouteInitial.py @@ -3,8 +3,9 @@ 예상노선 추종 · 영선 노선 · 토공 최소 · 기울기 순한 · 안전 우선 · 계류 횡단 최소 · 공사비 어림 최소 · 비중. -계산 자리 = 서버 단독(③). 결과는 화면 캐시용 — 이 모듈은 파일을 쓰지 않는다. 적용은 화면이 -받은 노드로 기존 `POST /route/replan`([확인])을 부른다. +계산 자리 = 서버 단독(③). 이 모듈은 파일을 쓰지 않는다 — 결과는 라우터가 입력 지문 키로 +`B05_Profile/route_recommend/` 에 남긴다(`..._Store` · PLAN 327). 적용은 화면이 받은 노드로 +기존 `POST /route/replan`([확인])을 부른다. 갈래(`mode`): · follow — 예상노선 추적 = 전처리 초기값(`planned_route_initial.csv`)과 같은 규칙 · 같은 선. @@ -182,6 +183,20 @@ def write_initial_choice( return choice +def resolve_inputs( + max_grade: float, + stream_offset_m: float | None, + grade_smooth_m: float | None, + target_grade: float | None, +) -> tuple[float, float, float]: + """(계류 이격, 기울기 잴 지반 폭, 영선 목표 기울기) — 비운 값은 기본값 · 저장 키와 같은 풀이.""" + stream_offset = STREAM_OFFSET_M if stream_offset_m is None else float(stream_offset_m) + grade_smooth = GRADE_SMOOTH_M if grade_smooth_m is None else float(grade_smooth_m) + # 영선 노선 목표 기울기(비율) — 없으면 상한 · 상한 넘게 넣어도 상한(제약 안에서만) + goal_grade = max_grade if target_grade is None else min(float(target_grade), max_grade) + return stream_offset, grade_smooth, goal_grade + + def resolve_weights(mode: str, weights: dict[str, float] | None) -> dict[str, float]: """갈래의 비중(합 100). weighted 가 아니면 갈래가 정한 값.""" if mode != "weighted": @@ -240,10 +255,9 @@ def generate_initial_route( if len(expected) < 2: raise ValueError("예상노선 정점이 2개 미만입니다.") used = resolve_weights(mode, weights) - stream_offset = STREAM_OFFSET_M if stream_offset_m is None else float(stream_offset_m) - grade_smooth = GRADE_SMOOTH_M if grade_smooth_m is None else float(grade_smooth_m) - # 영선 노선 목표 기울기(비율) — 없으면 상한 · 상한 넘게 넣어도 상한(제약 안에서만) - goal_grade = max_grade if target_grade is None else min(float(target_grade), max_grade) + stream_offset, grade_smooth, goal_grade = resolve_inputs( + max_grade, stream_offset_m, grade_smooth_m, target_grade + ) def measure( outline: Any, *, bound: float | None, follow: bool = False, weighted: bool = False diff --git a/B05_Profile/B05_Profile_Engine_RouteInitial_Profile.py b/B05_Profile/B05_Profile_Engine_RouteInitial_Profile.py index 13ec86a8e..de8e9fc8f 100644 --- a/B05_Profile/B05_Profile_Engine_RouteInitial_Profile.py +++ b/B05_Profile/B05_Profile_Engine_RouteInitial_Profile.py @@ -1,8 +1,9 @@ """노선 추천 후보의 균형 종단(PLAN 63-4 · 63-2 안 A). 후보 노선 하나에 종단을 세워 절 · 성토를 **횡단 ㎥** 로 맞춘 계획선과 그 토량을 낸다. -편집 창 [노선 추천]의 점수 · 미리 보기에만 쓴다 — 파일 · 캐시 · initial_snapshot · 확정 종단을 -바꾸지 않는다. 확정 체인의 종단 규칙(1차 계획고 = 지반고 · 2026-09-02 사용자 확정)은 그대로다. +편집 창 [노선 추천]의 점수 · 미리 보기에만 쓴다 — 작업본 파일 · initial_snapshot · 확정 종단을 +바꾸지 않는다(추천 결과째 저장본에 남는 것은 라우터 몫 · PLAN 327). 확정 체인의 종단 규칙 +(1차 계획고 = 지반고 · 2026-09-02 사용자 확정)은 그대로다. 계산 자리 = 서버 단독(③) — 화면은 받은 측점 · 지반고 · 계획고를 그리기만 한다. diff --git a/B05_Profile/B05_Profile_Engine_RouteInitial_Store.py b/B05_Profile/B05_Profile_Engine_RouteInitial_Store.py new file mode 100644 index 000000000..5f4cd944b --- /dev/null +++ b/B05_Profile/B05_Profile_Engine_RouteInitial_Store.py @@ -0,0 +1,128 @@ +"""계획노선 추천 결과 영구 저장(PLAN 327 · 요구 224-3). + +같은 입력이면 같은 결과 — 셈한 추천을 입력 지문 키로 프로젝트 폴더에 남기고, 같은 입력이 오면 +셈하지 않고 꺼낸다. 바뀐 입력만 새로 셈해 더한다(덮어쓰기 없음 · 상한 없음). + +층 = 서버 계산 캐시(데이터 3층 밖 · 사용자 편집값 아님 — 자동저장 금지와 무관). 자리 +`B05_Profile/route_recommend/<키>.json` — initial_snapshot 대상(`B05_Profile/route`) 밖이라 +[초기화]에 안 휩쓸리고, 자료 교체(`purge_output_folders`) · 프로젝트 삭제 때 폴더째 지워진다. + +키 = 후보 묶음 지문(`_pool_key` — 지형 · 예상노선 · 계류 · 상한 · 최소 R · 이격 · 고름 · 목표 +기울기) + 갈래 · 비중 · 종단 반영 · 설계속도 · `ALGO_VERSION`. 관 지점은 뺀다 — 덮개 수는 꺼낼 때 +다시 센다. 추천 결과를 바꾸는 코드 고침은 `ALGO_VERSION` 을 올린다(옛 파일은 다른 키라 안 쓰임). +""" + +from __future__ import annotations + +import asyncio +import hashlib +import json +from contextlib import asynccontextmanager +from datetime import datetime +from pathlib import Path +from typing import Any, AsyncIterator + +from B05_Profile.B05_Profile_Engine_RouteInitial import ( + DEFAULT_WEIGHTS, + Terrain, + _pool_key, + resolve_inputs, + resolve_weights, +) +from common_util.common_util_json import atomic_write_json + +ALGO_VERSION = 1 +STORE_DIR = "B05_Profile/route_recommend" +# 업로드 체인 끝에 미리 셈할 기본 다섯 알약(화면 차례) · 추적은 묶음이 선 뒤라 `similar` 가 채워짐 +DEFAULT_MODES = ("gradeline", "earthwork", "gentle", "weighted", "follow") + +# 묶음 지문마다 잠금 — 진행 중 같은 묶음(같은 키 포함)은 기다렸다 꺼냄(두 번 셈 막음) +_LOCKS: dict[str, asyncio.Lock] = {} + + +def store_dir(project_root: Path) -> Path: + return Path(project_root) / STORE_DIR + + +def request_fingerprint( + expected: list[tuple[float, float]], + terrain: Terrain, + streams: list[list[tuple[float, float]]], + *, + mode: str, + weights: dict[str, float] | None, + max_grade: float, + min_radius_m: float, + stream_offset_m: float | None, + grade_smooth_m: float | None, + target_grade: float | None, + with_profile: bool, + design_speed: int, +) -> tuple[str, str]: + """(저장 키, 묶음 지문) — `generate_initial_route` 와 같은 기본값 풀이로 같은 입력 = 같은 키.""" + pool = _pool_key( + expected, + terrain, + streams, + max_grade, + min_radius_m, + *resolve_inputs(max_grade, stream_offset_m, grade_smooth_m, target_grade), + ).hex() + used = resolve_weights(mode, weights) + tail = (mode, sorted(used.items()), bool(with_profile), int(design_speed), ALGO_VERSION) + key = hashlib.blake2b((pool + repr(tail)).encode(), digest_size=16).hexdigest() + return key, pool + + +def is_default( + mode: str, weights: dict[str, float] | None, criteria: dict[str, Any] | None, terrain: Any +) -> bool: + """기본값 결과 — 빈 제약 · 50/25/25 · 설계 지형. 표시일 뿐 지우는 데 쓰지 않음.""" + empty = not any(value is not None for value in (criteria or {}).values()) + plain = mode != "weighted" or resolve_weights(mode, weights) == DEFAULT_WEIGHTS + return empty and plain and terrain is None + + +def read_stored(project_root: Path, key: str) -> dict[str, Any] | None: + """저장본 한 벌 — 없거나 못 읽거나 판이 다르면 None.""" + try: + data = json.loads((store_dir(project_root) / f"{key}.json").read_text(encoding="utf-8")) + except (OSError, ValueError): + return None + if not isinstance(data, dict) or data.get("version") != ALGO_VERSION: + return None + return data + + +def write_stored( + project_root: Path, + key: str, + pool: str, + request: dict[str, Any], + result: dict[str, Any], + default: bool, +) -> bool: + """새 키만 적는다 — 이미 있으면 그대로 두고 False(덮어쓰기 없음).""" + path = store_dir(project_root) / f"{key}.json" + if path.exists(): + return False + atomic_write_json( + path, + { + "version": ALGO_VERSION, + "key": key, + "pool": pool, + "default": default, + "created_at": datetime.now().isoformat(timespec="seconds"), + "request": request, + "result": result, + }, + ) + return True + + +@asynccontextmanager +async def pool_lock(pool: str) -> AsyncIterator[None]: + # ponytail: 잠금은 지우지 않음 — 입력 묶음 수만큼(작음) · 다중 프로세스면 파일 잠금으로 + async with _LOCKS.setdefault(pool, asyncio.Lock()): + yield diff --git a/B05_Profile/B05_Profile_Router_Replan.py b/B05_Profile/B05_Profile_Router_Replan.py index 10e2f4559..e3ae05f53 100644 --- a/B05_Profile/B05_Profile_Router_Replan.py +++ b/B05_Profile/B05_Profile_Router_Replan.py @@ -26,7 +26,7 @@ GET /api/projects/{id}/route/plan → 예상노선·계획노선 정점(사업지 좌표계) POST /api/projects/{id}/route/replan → 고친 계획노선으로 갈아 끼우고 재계산 POST /api/projects/{id}/route/replan/reset → 계획노선을 예상노선으로 되돌리고 재계산 - POST /api/projects/{id}/route/initial → 초기 계획노선 갈래 하나를 만들어 돌려줌(파일 안 씀) + POST /api/projects/{id}/route/initial → 초기 계획노선 갈래 하나를 만들어 돌려줌(추천 저장본만 씀) """ import asyncio diff --git a/B05_Profile/B05_Profile_Router_RouteInitial.py b/B05_Profile/B05_Profile_Router_RouteInitial.py index 88e90e6b1..5bb6f494e 100644 --- a/B05_Profile/B05_Profile_Router_RouteInitial.py +++ b/B05_Profile/B05_Profile_Router_RouteInitial.py @@ -30,14 +30,35 @@ async def initial_route( ) -> dict[str, Any] | JSONResponse: """초기 계획노선 갈래 하나를 만들어 비교값과 함께 돌려준다(PLAN 20장). - **파일을 쓰지 않는다** — 화면 캐시용. 적용은 받은 `nodes` 로 `POST /route/replan`([확인]). + 작업본은 쓰지 않는다 — 추천 저장본(`..._Store` · PLAN 327)만. 적용은 받은 `nodes` 로 + `POST /route/replan`([확인]). + """ + return await compute_initial(project_id, request) + + +async def compute_initial( + project_id: UUID, request: replan.RouteInitialRequest +) -> dict[str, Any] | JSONResponse: + """같은 입력이면 저장본을 꺼내고 없으면 셈해 더한다(PLAN 327) · 덮개 수는 늘 지금 관으로. + + 응답 `stored` = 저장본에서 꺼냈나. 진행 중인 같은 묶음은 기다렸다 꺼낸다(두 번 셈 막음). """ from B05_Profile.B05_Profile_Engine_Grade import legal_grade_limit_pct from B05_Profile.B05_Profile_Engine_RouteInitial import ( generate_initial_route, load_terrain_and_streams, ) - from B05_Profile.B05_Profile_Engine_RouteInitial_Profile import project_pipes + from B05_Profile.B05_Profile_Engine_RouteInitial_Profile import ( + cover_shortfall, + project_pipes, + ) + from B05_Profile.B05_Profile_Engine_RouteInitial_Store import ( + is_default, + pool_lock, + read_stored, + request_fingerprint, + write_stored, + ) from common_util.common_util_surface_confirmation import ( analysis_terrain_params, get_surface_confirmation_params, @@ -81,37 +102,62 @@ async def initial_route( async with pool.acquire() as connection: selection = await get_surface_confirmation_params(connection, str(project_id)) selection = analysis_terrain_params(selection, request.terrain) + inputs = { + "mode": request.mode, + "weights": weights, + "max_grade": max_grade, + "min_radius_m": radius_m, + "stream_offset_m": stream_offset_m, + "grade_smooth_m": grade_smooth_m, + "target_grade": None if target_pct is None else target_pct / 100.0, + "design_speed": design_speed or 20, + "with_profile": request.with_profile, + } started = time.perf_counter() try: terrain, streams = await asyncio.to_thread( load_terrain_and_streams, project_root, selection ) - pipes = ( - await asyncio.to_thread(project_pipes, project_root) if request.with_profile else None - ) - result = await asyncio.to_thread( - functools.partial( - generate_initial_route, - [(x, y) for x, y in expected], - terrain, - streams, - mode=request.mode, - weights=weights, - max_grade=max_grade, - min_radius_m=radius_m, - stream_offset_m=stream_offset_m, - grade_smooth_m=grade_smooth_m, - target_grade=None if target_pct is None else target_pct / 100.0, - design_speed=design_speed or 20, - pipes=pipes, - with_profile=request.with_profile, - ) + route = [(x, y) for x, y in expected] + key, pool_key = await asyncio.to_thread( + functools.partial(request_fingerprint, route, terrain, streams, **inputs) ) + async with pool_lock(pool_key): + stored = await asyncio.to_thread(read_stored, project_root, key) + if stored is not None: + result = stored["result"] + else: + # 관 지점은 키 밖 — 덮개 수 없이 셈해 남기고 아래에서 지금 관으로 센다. + result = await asyncio.to_thread( + functools.partial( + generate_initial_route, route, terrain, streams, pipes=None, **inputs + ) + ) + await asyncio.to_thread( + write_stored, + project_root, + key, + pool_key, + request.model_dump(), + result, + is_default( + request.mode, + weights, + criteria.model_dump() if criteria else None, + request.terrain, + ), + ) + profile = result["metrics"].get("profile") + if request.with_profile and profile and "error" not in profile: + pipes = await asyncio.to_thread(project_pipes, project_root) + if pipes: + profile.update(cover_shortfall(profile, result["planned"], pipes)) except (FileNotFoundError, ValueError) as error: return JSONResponse(status_code=409, content={"status": "error", "message": str(error)}) logger.info( - "초기 계획노선 %s: project_id=%s %.1fs 연장 %.0fm 이격 평균 %.1fm", + "초기 계획노선 %s(%s): project_id=%s %.1fs 연장 %.0fm 이격 평균 %.1fm", request.mode, + "저장본" if stored is not None else "셈", project_id, time.perf_counter() - started, result["metrics"]["length_m"], @@ -122,10 +168,28 @@ async def initial_route( "status": "success", "project_id": str(project_id), **result, + "stored": stored is not None, "current_initial": current, } +async def precompute_defaults(project_id: UUID) -> None: + """업로드 체인 끝 예약(PLAN 327-2) — 기본 다섯 알약을 미리 셈해 남김 · 있으면 꺼내기만.""" + from B05_Profile.B05_Profile_Engine_RouteInitial_Store import DEFAULT_MODES + + started = time.perf_counter() + for mode in DEFAULT_MODES: + # 화면 기본과 같은 몸통 — 빈 제약 · 50/25/25 · 설계 지형 · 추적만 종단 끔(`withProfileOf`) + request = replan.RouteInitialRequest(mode=mode, with_profile=mode != "follow") + outcome = await compute_initial(project_id, request) + if isinstance(outcome, JSONResponse): + logger.warning( + "추천 미리 셈 멈춤(%s %s): project_id=%s", mode, outcome.status_code, project_id + ) + return + logger.info("추천 미리 셈 끝: project_id=%s %.1fs", project_id, time.perf_counter() - started) + + def _set_base_pvi(path: Path, stations: list[float], plan: list[float]) -> int: """종단 정본의 1차 변화점(`base_pvi`) 계획고를 `stations` · `plan` 직선 보간으로 바꿔 쓴다.""" import json diff --git a/resources/tester/test_224_3_recommend_store.py b/resources/tester/test_224_3_recommend_store.py new file mode 100644 index 000000000..528f34605 --- /dev/null +++ b/resources/tester/test_224_3_recommend_store.py @@ -0,0 +1,231 @@ +"""계획노선 추천 결과 영구 저장(PLAN 327 · 요구 224-3) — `B05_Profile_Engine_RouteInitial_Store`. + + ./venv/Scripts/python.exe -m pytest resources/tester/test_224_3_recommend_store.py -q + +같은 입력 = 같은 키 · 하나만 바뀌어도 다른 키 · 두 번째 요청 셈 0회 · 덮어쓰기 없음 · +[초기화](스냅샷 복원) 뒤에도 남음 · 자료 교체 때 지워짐 · 덮개 수는 꺼낼 때 지금 관으로. +엔드포인트 몸통(`compute_initial`)은 DB · 지형 읽기만 갈아 끼우고 그대로 돌린다. +""" + +import asyncio +import json +import shutil +from contextlib import asynccontextmanager +from pathlib import Path +from uuid import uuid4 + +import numpy as np +import pytest + +from B05_Profile import B05_Profile_Router_Replan as replan +from B05_Profile import B05_Profile_Router_RouteInitial as router +from B05_Profile.B05_Profile_Engine_RouteInitial import STREAM_OFFSET_M, Terrain +from B05_Profile.B05_Profile_Engine_RouteInitial_Store import ( + DEFAULT_MODES, + request_fingerprint, + store_dir, + write_stored, +) +from common_util.common_util_initial_snapshot import ( + _FILE_TREES, + restore_snapshot_files, + snapshot_dir, +) +from common_util.common_util_project_reset import purge_output_folders + + +def _terrain(lift: float = 0.0) -> Terrain: + x = np.arange(0.0, 40.0, 2.0) + y = np.arange(0.0, 20.0, 2.0) + xx, yy = np.meshgrid(x, y) + return Terrain(x, y, 0.1 * xx + lift, np.ones(xx.shape, dtype=bool)) + + +EXPECTED = [(0.0, 10.0), (20.0, 10.0), (38.0, 10.0)] +STREAMS = [[(5.0, 0.0), (5.0, 18.0)]] +BASE = { + "mode": "weighted", + "weights": None, + "max_grade": 0.14, + "min_radius_m": 12.0, + "stream_offset_m": None, + "grade_smooth_m": None, + "target_grade": None, + "with_profile": True, + "design_speed": 20, +} + + +def _key(expected=EXPECTED, terrain=None, streams=STREAMS, **changes) -> str: + return request_fingerprint(expected, terrain or _terrain(), streams, **{**BASE, **changes})[0] + + +def test_same_input_same_key() -> None: + assert _key() == _key() + # 비운 값 = 기본값 · 비중 None = 50/25/25 — 화면이 채워 보내도 같은 키 + assert _key() == _key(stream_offset_m=STREAM_OFFSET_M) + assert _key() == _key(weights={"expected": 50.0, "earthwork": 25.0, "grade": 25.0}) + # 비중 갈래가 아니면 비중은 갈래가 정함 — 보낸 비중은 키에 안 듦 + assert _key(mode="earthwork") == _key(mode="earthwork", weights={"expected": 1.0}) + + +@pytest.mark.parametrize( + "change", + [ + {"mode": "earthwork"}, + {"weights": {"expected": 40.0, "earthwork": 30.0, "grade": 30.0}}, + {"max_grade": 0.12}, + {"min_radius_m": 15.0}, + {"stream_offset_m": 50.0}, + {"grade_smooth_m": 3.0}, + {"target_grade": 0.08}, + {"with_profile": False}, + {"design_speed": 30}, + {"expected": [(0.0, 10.0), (20.0, 11.0), (38.0, 10.0)]}, + {"terrain": _terrain(lift=0.5)}, + {"streams": []}, + ], + ids=lambda change: next(iter(change)), +) +def test_one_change_other_key(change: dict) -> None: + assert _key(**change) != _key() + + +def test_no_overwrite(tmp_path: Path) -> None: + assert write_stored(tmp_path, "k", "p", {}, {"n": 1}, True) + assert not write_stored(tmp_path, "k", "p", {}, {"n": 2}, False) + data = json.loads((store_dir(tmp_path) / "k.json").read_text(encoding="utf-8")) + assert data["result"] == {"n": 1} and data["default"] is True + assert set(data) == {"version", "key", "pool", "default", "created_at", "request", "result"} + + +def test_survives_reset_and_goes_with_data_swap(tmp_path: Path) -> None: + (tmp_path / "B05_Profile/route").mkdir(parents=True) + (tmp_path / "B05_Profile/route/initial_choice.json").write_text("{}", encoding="utf-8") + write_stored(tmp_path, "k", "p", {}, {}, True) + # 스냅샷 = 작업본 트리 사본(`save_initial_snapshot` 과 같은 자리) + for tree in _FILE_TREES: + source = tmp_path / tree + if source.is_dir(): + shutil.copytree(source, snapshot_dir(tmp_path) / tree.replace("/", "__")) + restore_snapshot_files(tmp_path) + assert (store_dir(tmp_path) / "k.json").is_file() + purge_output_folders(tmp_path) # 자료 교체 + assert not store_dir(tmp_path).exists() + + +class _Calls: + def __init__(self) -> None: + self.count = 0 + + +@pytest.fixture +def wired(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> _Calls: + """`compute_initial` 의 DB · 파일 읽기를 갈아 끼움 · 셈은 횟수만 세는 가짜.""" + import B05_Profile.B05_Profile_Engine_Grade as grade + import B05_Profile.B05_Profile_Engine_RouteInitial as engine + import B05_Profile.B05_Profile_Engine_RouteInitial_Profile as profile + import common_util.common_util_surface_confirmation as surface + + calls = _Calls() + + async def project_paths(_pid): + return tmp_path, "x" + + async def plan_criteria(_pid): + return 12.0, 12.0, 5.0 + + async def road_settings(_pid): + return "간선", 20, "일반" + + async def surface_params(_connection, _pid): + return {} + + class _Pool: + @asynccontextmanager + async def acquire(self): + yield None + + def generate(expected, terrain, streams, *, pipes, **_inputs): + assert pipes is None # 관은 키 밖 — 덮개 없이 셈해 남김 + calls.count += 1 + return { + "planned": [[x, y] for x, y in expected], + "metrics": { + "length_m": 38.0, + "offset_avg_m": 0.0, + "profile": { + "stations_m": [0.0, 38.0], + "plan_m": [1.0, 1.0], + "ground_m": [0.0, 0.0], + }, + }, + } + + monkeypatch.setattr(replan, "_project_paths", project_paths) + monkeypatch.setattr(replan, "_plan_criteria", plan_criteria) + monkeypatch.setattr(replan, "_road_settings", road_settings) + monkeypatch.setattr(replan, "_vertices_of", lambda _path: [list(p) for p in EXPECTED]) + monkeypatch.setattr(router, "get_db_pool", lambda: _Pool()) + monkeypatch.setattr(surface, "get_surface_confirmation_params", surface_params) + monkeypatch.setattr(surface, "analysis_terrain_params", lambda selection, _t: selection) + monkeypatch.setattr(grade, "legal_grade_limit_pct", lambda *_a: 14.0) + monkeypatch.setattr(engine, "load_terrain_and_streams", lambda *_a: (_terrain(), STREAMS)) + monkeypatch.setattr(engine, "generate_initial_route", generate) + # 덮개 여유 2 m 관 하나가 노선 위 — 계획고 − 지반고 1 m 라 모자람 1 + monkeypatch.setattr(profile, "project_pipes", lambda _root: [(19.0, 10.0, 2.0)]) + calls.root = tmp_path + return calls + + +def _ask(**fields) -> dict: + request = replan.RouteInitialRequest(**fields) + return asyncio.run(router.compute_initial(uuid4(), request)) + + +def test_second_request_computes_nothing(wired: _Calls) -> None: + first = _ask(mode="earthwork") + assert wired.count == 1 and first["stored"] is False + second = _ask(mode="earthwork") + assert wired.count == 1 and second["stored"] is True + assert second["planned"] == first["planned"] + # 덮개 수는 꺼낼 때마다 지금 관으로 · 저장본에는 없음 + assert second["metrics"]["profile"]["cover_short"] == 1 + files = list(store_dir(wired.root).glob("*.json")) + assert len(files) == 1 + saved = json.loads(files[0].read_text(encoding="utf-8")) + assert "cover_short" not in saved["result"]["metrics"]["profile"] + assert saved["default"] is True and saved["request"]["mode"] == "earthwork" + # 제약 하나 바꾸면 파일 하나 더 · 옛 파일 그대로 · 기본값 표시 꺼짐 + changed = _ask(mode="earthwork", criteria={"min_radius_m": 15.0}) + assert changed["stored"] is False and wired.count == 2 + assert len(list(store_dir(wired.root).glob("*.json"))) == 2 + assert json.loads(files[0].read_text(encoding="utf-8")) == saved + + +def test_same_pool_in_flight_waits(wired: _Calls) -> None: + async def both() -> list[dict]: + request = replan.RouteInitialRequest(mode="gentle") + project = uuid4() + return await asyncio.gather( + router.compute_initial(project, request), router.compute_initial(project, request) + ) + + results = asyncio.run(both()) + assert wired.count == 1 + assert sorted(result["stored"] for result in results) == [False, True] + + +def test_precompute_defaults_then_hits(wired: _Calls) -> None: + asyncio.run(router.precompute_defaults(uuid4())) + assert wired.count == len(DEFAULT_MODES) == 5 + saved = [ + json.loads(path.read_text(encoding="utf-8")) + for path in store_dir(wired.root).glob("*.json") + ] + assert sorted(item["request"]["mode"] for item in saved) == sorted(DEFAULT_MODES) + assert all(item["default"] for item in saved) + # 화면 첫 누름(비중은 화면이 50/25/25 를 채워 보냄 · 추적은 종단 끔) — 셈 없이 저장본 + hit = _ask(mode="weighted", weights={"expected": 50, "earthwork": 25, "grade": 25}) + follow = _ask(mode="follow", with_profile=False) + assert hit["stored"] and follow["stored"] and wired.count == 5 From b55bad1686db32b7f0139a4a8af32f0da1e307f2 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 15 Oct 2026 12:35:57 +0900 Subject: [PATCH 23/81] =?UTF-8?q?feat(B05):=20326-3=20=EA=B3=84=ED=9A=8D?= =?UTF-8?q?=EB=85=B8=EC=84=A0=20=ED=8E=B8=EC=A7=91=20=EC=B0=BD=20[?= =?UTF-8?q?=EC=B8=A1=EC=A0=90=20=EC=8A=A4=EB=83=85]=20=EB=8B=A8=EC=B6=94?= =?UTF-8?q?=20=E2=80=94=20=EC=BC=AC=EB=81=94=20=EB=B3=B4=EA=B8=B0=20?= =?UTF-8?q?=EB=AA=A8=EB=93=9C(=EB=90=98=EB=8F=8C=EB=A6=AC=EA=B8=B0=20?= =?UTF-8?q?=EB=B0=96)=20=C2=B7=20=EC=97=B4=20=EB=95=8C=20=EC=84=9C?= =?UTF-8?q?=EB=B2=84=20=EA=B0=92=20=C2=B7=20=EC=B8=A1=EC=A0=90=20=ED=95=9C?= =?UTF-8?q?=20=EA=B3=B3(stations)=EC=9C=BC=EB=A1=9C=20=EB=88=88=EA=B8=88?= =?UTF-8?q?=20=C2=B7=20=EB=88=84=EB=A6=84=20=C2=B7=20=EA=B8=B0=EC=9A=B8?= =?UTF-8?q?=EA=B8=B0=20=C2=B7=20=ED=9A=A1=EB=8B=A8=20=EB=AF=B8=EB=A6=AC?= =?UTF-8?q?=EB=B3=B4=EA=B8=B0=20=C2=B7=20[=ED=99=95=EC=9D=B8]=20=EB=95=8C?= =?UTF-8?q?=20station=5Fsnap=20=EC=8B=A4=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_01TteK8mfYZetk5P3bEZkyVZ --- .../B04_PreProcess_UI_MapOverlays.ts | 7 +++- B05_Profile/B05_Profile_Api_Replan.ts | 4 ++ B05_Profile/B05_Profile_UI_RouteEdit.ts | 38 ++++++++++++++++++- B05_Profile/B05_Profile_UI_RouteEdit_Apply.ts | 3 ++ .../B05_Profile_UI_RouteEdit_Chrome.ts | 7 ++++ B05_Profile/B05_Profile_UI_RouteEdit_Cross.ts | 1 + B05_Profile/B05_Profile_UI_RouteEdit_Input.ts | 10 ++++- .../B05_Profile_UI_RouteEdit_Pointer.ts | 3 ++ .../B05_Profile_UI_RouteEdit_Render.ts | 3 ++ B05_Profile/B05_Profile_UI_RouteEdit_Slope.ts | 22 +++++++++-- 10 files changed, 89 insertions(+), 9 deletions(-) diff --git a/B04_PreProcess/B04_PreProcess_UI_MapOverlays.ts b/B04_PreProcess/B04_PreProcess_UI_MapOverlays.ts index 73543179e..e46176d19 100644 --- a/B04_PreProcess/B04_PreProcess_UI_MapOverlays.ts +++ b/B04_PreProcess/B04_PreProcess_UI_MapOverlays.ts @@ -211,6 +211,8 @@ export interface StationTickOptions { /** 라벨을 그릴 때마다 그 글자 상자를 알린다 — 누름 판정이 쓴다. 그림 좌표(회전 전)이고, * 상자는 `uprightRad` 만큼 (x, y) 를 축으로 돌아 있다. 안 주면 아무 일도 안 한다. */ onLabel?: (box: StationLabelBox) => void; + /** 그릴 측점 누가거리(오름차순) — 안 주면 간격마다(B05 편집 창 측점 스냅 · 요구 224-2). */ + chainages?: ReadonlyArray; } /** 그려진 측점 라벨 글자 상자 — 가운데 (x, y) · 가로 반폭 `half` · 세로 반폭 8 · 돌린 각. */ @@ -253,7 +255,10 @@ export function drawStationTicks( // 이미 그린 라벨과 겹치는 자리는 비켜 놓는다. const drawn: Array<{ x: number; y: number; half: number }> = []; let cursor = 1; - for (let chainage = 0; chainage <= total; chainage += interval) { + const marks: number[] = []; + if (options.chainages) marks.push(...options.chainages.filter((value) => value <= total)); + else for (let chainage = 0; chainage <= total; chainage += interval) marks.push(chainage); + for (const chainage of marks) { const stationNo = Math.round(chainage / interval); if (stationNo % step !== 0) continue; while (cursor < cumulative.length - 1 && cumulative[cursor] < chainage) cursor += 1; diff --git a/B05_Profile/B05_Profile_Api_Replan.ts b/B05_Profile/B05_Profile_Api_Replan.ts index 3285d710d..97ac66fb1 100644 --- a/B05_Profile/B05_Profile_Api_Replan.ts +++ b/B05_Profile/B05_Profile_Api_Replan.ts @@ -100,6 +100,8 @@ export interface RoutePlanResponse { violation_count: number; /** 사용자가 고친 계획노선이 저장돼 있으면 true. */ edited: boolean; + /** [측점 스냅] 켬(요구 224-2) — [확인] 때 곡선표에 적은 값. 칸 없으면 끔. */ + station_snap?: boolean; /** 현재 초기값 — 서버가 [확인] 때 프로젝트에 적어 둔 초기노선 갈래(계획서 20). */ current_initial?: { logic: string; weights: Record | null }; /** 전처리가 만든 초기 노선(예상노선 추적)의 노드 · 곡선 — [초기화]가 재계산 없이 놓는다. */ @@ -220,6 +222,8 @@ export interface CrossPreviewRequest { station_interval_m: number; /** 횡단 반폭(m) — 없으면 서버 기본값. */ cross_half_width_m?: number; + /** [측점 스냅] 켬 — 호 안 측점 하나를 곡선 중점으로(요구 224-2). */ + station_snap?: boolean; } /** 한 측점 횡단을 묻는다. 종·횡단을 한 번 돌리므로 **한두 초** 걸린다(사용자 확정: 괜찮음). */ diff --git a/B05_Profile/B05_Profile_UI_RouteEdit.ts b/B05_Profile/B05_Profile_UI_RouteEdit.ts index a69bcf658..65d2759b3 100644 --- a/B05_Profile/B05_Profile_UI_RouteEdit.ts +++ b/B05_Profile/B05_Profile_UI_RouteEdit.ts @@ -59,6 +59,7 @@ import { viewPxPerMeter, } from "./B05_Profile_UI_RouteEdit_View"; import { createCurveBar } from "./B05_Profile_UI_RouteEdit_CurveBar"; +import { curveSpans, snapCurveStations, stationsAlong } from "./B05_Profile_UI_RouteEdit_Frame"; import { lockColumn, readRouteLocks, @@ -109,7 +110,7 @@ export async function openRouteEditModal( const stationIntervalM = options.stationIntervalM ?? 20; const chrome = createRouteEditChrome(); const { overlay, canvas, status, busy, measureBox, measureText, measureButton } = chrome; - const { minorContourButton, terrainSelect } = chrome; + const { minorContourButton, terrainSelect, stationSnapButton } = chrome; const context = canvas.getContext("2d")!; let expected: Vertex[] = []; @@ -138,6 +139,24 @@ export async function openRouteEditModal( let apexLock: boolean[] = []; /** 지금 고른 꺾임점 — 곡선 편집줄이 이 자리를 만진다. 없으면 -1. */ let picked = -1; + /** [측점 스냅](요구 224-2) — 보기 모드라 되돌리기 밖 · 열 때 서버 값 · [확인] 때 저장. */ + let stationSnap = false; + let savedStationSnap = false; + /** 편집 창 측점 한 곳 — 끔이면 null(간격마다) · 켬이면 호 안 하나를 곡선 중점으로. + * 그리기 · 누름 · 기울기가 이것을 봄 · 셈은 서버 짝(`_Frame`). */ + const stations = (): number[] | null => { + if (!stationSnap) return null; + const line = plannedLine.length ? plannedLine : planned; + return snapCurveStations( + stationsAlong(line, stationIntervalM, false), + curveSpans(line, curveInfo), + ); + }; + const setStationSnap = (on: boolean): void => { + stationSnap = on; + stationSnapButton.classList.toggle("is-active", on); + stationSnapButton.setAttribute("aria-pressed", String(on)); + }; /** 되돌리기 사진첩 — 노선을 읽은 뒤에 선다(그전에는 되돌릴 것이 없다). */ let history: RouteEditHistory | null = null; let meta: VWorldMeta | null = null; @@ -165,6 +184,7 @@ export async function openRouteEditModal( })), min_radius_m: minRadiusM || 12, station_interval_m: stationIntervalM, + station_snap: stationSnap, }), onShown: () => draw(), // 횡단이 뜨면 그 측점 반폭 선을 그린다. }); @@ -217,6 +237,11 @@ export async function openRouteEditModal( const slope = createSlopeLabels({ projectId, isClosed: () => closed, onChange: () => draw() }); /** 1m 간격 등고선 켬끔 — 기본 꺼짐(계획서 18 ②). */ const showMinorContours = bindMinorContourToggle(minorContourButton, () => draw()); + stationSnapButton.addEventListener("click", () => { + setStationSnap(!stationSnap); + void crossPreview.refresh(); // 떠 있는 횡단도 옮긴 측점 규칙으로. + draw(); + }); /** 보던 화면 기억(계획서 18 ③) — 자료를 읽고 화면을 되살린 **뒤부터** 적는다. */ let viewReady = false; const viewMemory = createViewMemory(projectId, () => @@ -307,6 +332,7 @@ export async function openRouteEditModal( toScreen, uprightRad: rotation.uprightRad(), dragging: pointer.dragging(), + chainages: stations(), }), measure: measure.marks(), crossWidth: crossPreview.watched(), @@ -319,6 +345,7 @@ export async function openRouteEditModal( locked: apexLock, picked, stationIntervalM, + stations: stations(), }); // 라벨은 **그린 뒤** 자리를 맞춘다 — 확대·이동·창 크기가 바뀌어도 고른 노드에 붙어 있게. syncCurveBar(); @@ -448,6 +475,7 @@ export async function openRouteEditModal( canvas, status, stationIntervalM, + stations, state: () => ({ planned, plannedLine, @@ -530,8 +558,12 @@ export async function openRouteEditModal( projectId, nodes: () => ({ planned, curveOn, curveRadius }), // 분석 지형만 바꿔도 [확인]이 설계 지형을 바꿔 다시 셈한다(PLAN 133-2). - changed: () => (history?.isDirty() ?? false) || pickedTerrain() !== designTerrain, + changed: () => + (history?.isDirty() ?? false) || + pickedTerrain() !== designTerrain || + stationSnap !== savedStationSnap, terrain: pickedTerrain, + stationSnap: () => stationSnap, initial, profileShown: strip.settle, close, @@ -554,6 +586,8 @@ export async function openRouteEditModal( minRadiusM = plan.min_radius_m ?? 0; limitRadiusM = plan.limit_radius_m ?? 0; limitArcM = plan.limit_curve_length_m ?? 0; + savedStationSnap = plan.station_snap === true; + setStationSnap(savedStationSnap); // 곡선 성분을 편집할 수 있는 꼴로 편다 — 셈은 `_Edits` 몫(까닭도 그쪽에 적었다). const flat = flattenServerPlan(nodes, plan.curves ?? []); ({ planned, curveOn, curveRadius, curveLock, curveArc } = flat); diff --git a/B05_Profile/B05_Profile_UI_RouteEdit_Apply.ts b/B05_Profile/B05_Profile_UI_RouteEdit_Apply.ts index ed8036b2a..05f05f7d2 100644 --- a/B05_Profile/B05_Profile_UI_RouteEdit_Apply.ts +++ b/B05_Profile/B05_Profile_UI_RouteEdit_Apply.ts @@ -27,6 +27,8 @@ export interface RouteApplyParams { changed: () => boolean; /** 설계 지형으로 보낼 분석 지형(PLAN 133-2) — 고르기가 안 보이면 null(지금 설계 지형 그대로). */ terrain: () => string | null; + /** [측점 스냅] 켬끔(요구 224-2) — 곡선표에 적혀 측점 셈이 읽음. */ + stationSnap: () => boolean; /** 초기노선 갈래 — [확인]에 지금 초기값을 싣고, 결과 캐시를 비운 뒤 다시 적는다. */ initial: { current: () => unknown; @@ -97,6 +99,7 @@ export function bindRouteApply(params: RouteApplyParams): void { // 띠 응답 전에 눌렀으면 기다린다 — 서버는 같은 노선 · 같은 셈으로 띠 계획선을 다시 낸다. balanced_profile: await params.profileShown(), terrain: params.terrain(), + station_snap: params.stationSnap(), }, ), ); diff --git a/B05_Profile/B05_Profile_UI_RouteEdit_Chrome.ts b/B05_Profile/B05_Profile_UI_RouteEdit_Chrome.ts index fb0c4208d..c6e66feee 100644 --- a/B05_Profile/B05_Profile_UI_RouteEdit_Chrome.ts +++ b/B05_Profile/B05_Profile_UI_RouteEdit_Chrome.ts @@ -36,6 +36,8 @@ export interface RouteEditChrome { measureText: HTMLElement; measureButton: HTMLButtonElement; minorContourButton: HTMLButtonElement; + /** [측점 스냅] 켬끔(요구 224-2) — `aria-pressed` · `is-active` 는 본체가 맞춤. */ + stationSnapButton: HTMLButtonElement; /** 분석 지형 고르기(PLAN 133-1) — 라이다 있는 프로젝트만 보임(감싼 label 의 hidden). */ terrainSelect: HTMLSelectElement; } @@ -65,6 +67,9 @@ export function createRouteEditChrome(): RouteEditChrome { title="1m 간격 등고선을 얇고 옅게 겹칩니다 (확대하면 보임)">1m 등고선 + @@ -136,6 +141,7 @@ export function createRouteEditChrome(): RouteEditChrome { '[data-act="contour-minor"]', )!; const terrainSelect = overlay.querySelector('[data-act="terrain"]')!; + const stationSnapButton = overlay.querySelector('[data-act="station-snap"]')!; return { overlay, @@ -146,6 +152,7 @@ export function createRouteEditChrome(): RouteEditChrome { measureText, measureButton, minorContourButton, + stationSnapButton, terrainSelect, }; } diff --git a/B05_Profile/B05_Profile_UI_RouteEdit_Cross.ts b/B05_Profile/B05_Profile_UI_RouteEdit_Cross.ts index 1c6a3648c..c0b491141 100644 --- a/B05_Profile/B05_Profile_UI_RouteEdit_Cross.ts +++ b/B05_Profile/B05_Profile_UI_RouteEdit_Cross.ts @@ -35,6 +35,7 @@ export interface CrossPreviewParams { vertices: Array<{ x: number; y: number; curve: boolean; radius_m: number | null }>; min_radius_m: number; station_interval_m: number; + station_snap?: boolean; }; /** 위 판에 횡단이 떴다 · 비었다 — 평면도의 반폭 선을 다시 그리라는 신호. */ onShown?: () => void; diff --git a/B05_Profile/B05_Profile_UI_RouteEdit_Input.ts b/B05_Profile/B05_Profile_UI_RouteEdit_Input.ts index fbef8b083..4eef74ec3 100644 --- a/B05_Profile/B05_Profile_UI_RouteEdit_Input.ts +++ b/B05_Profile/B05_Profile_UI_RouteEdit_Input.ts @@ -321,13 +321,15 @@ export function stationAtScreen( py: number, maxPx: number, labels: ReadonlyArray = [], + /** 측점 누가거리 — 안 주면 간격마다(측점 스냅 · 요구 224-2). */ + chainages: ReadonlyArray | null = null, ): number | null { // 라벨이 그려진 측점만 눈금도 보인다(`drawStationTicks` 가 성기게 솎는다) — 안 보이는 눈금이 // 호·등고선 누름을 가로채지 않게 한다. 라벨 정보가 없으면 전부 본다. const shown = labels.length ? new Set(labels.map((box) => Math.round(box.chainage / intervalM))) : null; - const tick = stationTickAtScreen(line, toScreen, intervalM, px, py, maxPx, shown); + const tick = stationTickAtScreen(line, toScreen, intervalM, px, py, maxPx, shown, chainages); // 눈금과 라벨이 겹치면 눈금이 먼저다 — 라벨은 눈금을 못 잡았을 때만 본다. return tick ?? stationLabelAtScreen(labels, px, py); } @@ -373,6 +375,7 @@ function stationTickAtScreen( py: number, maxPx: number, shown: ReadonlySet | null, + chainages: ReadonlyArray | null, ): number | null { if (line.length < 2 || !(intervalM > 0)) return null; const cumulative: number[] = [0]; @@ -386,7 +389,10 @@ function stationTickAtScreen( let best: number | null = null; let bestDistance = maxPx; let cursor = 1; - for (let chainage = 0; chainage <= total; chainage += intervalM) { + const marks: number[] = []; + if (chainages) marks.push(...chainages.filter((value) => value <= total)); + else for (let chainage = 0; chainage <= total; chainage += intervalM) marks.push(chainage); + for (const chainage of marks) { if (shown && !shown.has(Math.round(chainage / intervalM))) continue; while (cursor < cumulative.length - 1 && cumulative[cursor] < chainage) cursor += 1; const back = line[cursor - 1]; diff --git a/B05_Profile/B05_Profile_UI_RouteEdit_Pointer.ts b/B05_Profile/B05_Profile_UI_RouteEdit_Pointer.ts index 3763161ea..2da4c4f10 100644 --- a/B05_Profile/B05_Profile_UI_RouteEdit_Pointer.ts +++ b/B05_Profile/B05_Profile_UI_RouteEdit_Pointer.ts @@ -64,6 +64,8 @@ export interface RouteEditPointerParams { canvas: HTMLCanvasElement; status: HTMLElement; stationIntervalM: number; + /** 편집 창 측점 — 끔이면 null(간격마다 · 측점 스냅 요구 224-2). */ + stations: () => ReadonlyArray | null; state: () => RouteEditPointerState; setPicked: (index: number) => void; setPickedContour: (index: number) => void; @@ -194,6 +196,7 @@ export function bindRouteEditPointer(params: RouteEditPointerParams): RouteEditP py, pickPx(STATION_HIT_PX), stationLabelBoxes(canvas), + params.stations(), ); if (chainage === null) return false; void params.crossPreview.open(chainage); diff --git a/B05_Profile/B05_Profile_UI_RouteEdit_Render.ts b/B05_Profile/B05_Profile_UI_RouteEdit_Render.ts index 3a35c064e..90c6664ec 100644 --- a/B05_Profile/B05_Profile_UI_RouteEdit_Render.ts +++ b/B05_Profile/B05_Profile_UI_RouteEdit_Render.ts @@ -94,6 +94,8 @@ export interface RouteEditScene { picked: number; /** 규칙 측점 간격(m). */ stationIntervalM: number; + /** 그릴 측점 누가거리 — 끔이면 null(간격마다 · 측점 스냅 요구 224-2). */ + stations: ReadonlyArray | null; /** 구간 재기로 찍은 점(0~2개) — 노선 위 자리와 그 노선의 누가거리(계획서 0-9 ⑤). */ measure: ReadonlyArray; /** 오른쪽에 횡단이 떠 있는 측점과 좌·우 설계 반폭 — 없으면 null. */ @@ -478,6 +480,7 @@ function drawStationMarks( toScreen: (x, y) => scene.toScreen([x, y]), uprightRad: scene.uprightRad, onLabel: (box) => labels.push(box), + chainages: scene.stations ?? undefined, }, ); const total = polylineLengthM(line); diff --git a/B05_Profile/B05_Profile_UI_RouteEdit_Slope.ts b/B05_Profile/B05_Profile_UI_RouteEdit_Slope.ts index e35789f81..aa372ee5b 100644 --- a/B05_Profile/B05_Profile_UI_RouteEdit_Slope.ts +++ b/B05_Profile/B05_Profile_UI_RouteEdit_Slope.ts @@ -42,6 +42,8 @@ export interface SlopeDrawArgs { uprightRad: number; /** 노드·손잡이를 끄는 중 — 이때는 묻지 않는다. */ dragging: boolean; + /** 측점 누가거리(종점 뺌) — 끔이면 null(간격마다 · 측점 스냅 요구 224-2). */ + chainages?: ReadonlyArray | null; } export interface SlopeLabels { @@ -91,6 +93,8 @@ export function createSlopeLabels(params: SlopeLabelParams): SlopeLabels { /** 높이를 받아 둔 노선·간격과 그 결과. 노선이 바뀌면(새 배열) 버린다. */ let cachedLine: ReadonlyArray | null = null; let cachedInterval = 0; + /** 받아 둔 측점 목록 열쇠 — 스냅을 켜고 끄면 같은 노선이어도 다시 묻는다. */ + let cachedKey = ""; let chainages: number[] = []; let heights: Array = []; /** 측점 자리와 구간 가운데 자리 — 받아 올 때 한 번 셈해 둔다(그리기마다 다시 안 셈). */ @@ -100,19 +104,27 @@ export function createSlopeLabels(params: SlopeLabelParams): SlopeLabels { let asking: ReadonlyArray | null = null; let timer: ReturnType | null = null; - function schedule(line: ReadonlyArray, intervalM: number): void { + function schedule( + line: ReadonlyArray, + intervalM: number, + marks: ReadonlyArray | null, + ): void { if (asking === line) return; if (timer) clearTimeout(timer); asking = line; timer = setTimeout(() => { timer = null; - const wanted = stationChainages(lengthOf(line), intervalM); + const total = lengthOf(line); + const wanted = marks + ? [...marks.filter((value) => value < total - 1e-6), total] + : stationChainages(total, intervalM); const points = pointsAt(line, wanted); void fetchRouteElevations(params.projectId, points) .then((z) => { if (params.isClosed() || asking !== line) return; // 그 사이 노선이 또 바뀌었다. cachedLine = line; cachedInterval = intervalM; + cachedKey = marks?.join() ?? ""; chainages = wanted; heights = z; stationPoints = points; @@ -160,9 +172,11 @@ export function createSlopeLabels(params: SlopeLabelParams): SlopeLabels { const { line, intervalM } = args; if (line.length < 2 || !(intervalM > 0)) return 0; if (intervalM * args.pxPerMeter < SLOPE_MIN_GAP_PX) return 0; // 멀리 보면 숨긴다. - const fresh = cachedLine === line && cachedInterval === intervalM; + const marks = args.chainages ?? null; + const fresh = + cachedLine === line && cachedInterval === intervalM && cachedKey === (marks?.join() ?? ""); if (!fresh) { - if (!args.dragging) schedule(line, intervalM); + if (!args.dragging) schedule(line, intervalM, marks); return 0; // 낡은 높이로 그리면 엉뚱한 기울기가 적힌다 — 받아 올 때까지 비운다. } // 화면 안 판정은 **캔버스 변환 그대로**(회전·고해상도 배율 포함) 옮긴 자리로 한다 — From b1e8d6cb7f5f8a8d4734cd3fdfeddc026ada1d0f Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 15 Oct 2026 12:36:04 +0900 Subject: [PATCH 24/81] =?UTF-8?q?feat(=EC=84=A4=EC=A0=95):=20332-1=20?= =?UTF-8?q?=ED=9C=A0=20=ED=99=95=EB=8C=80=20=EB=B0=A9=ED=96=A5=20=EC=9D=BD?= =?UTF-8?q?=EA=B8=B0=20=EC=B8=B5=20=E2=80=94=20input=5Fprefs=20=C2=B7=20?= =?UTF-8?q?=EB=AA=A8=EB=93=A0=20=ED=9C=A0=20=ED=99=94=EB=A9=B4=20=ED=95=9C?= =?UTF-8?q?=20=EC=84=A4=EC=A0=95(=EA=B8=B0=EB=B3=B8=20=EB=8B=B9=EA=B9=80)?= =?UTF-8?q?=20=C2=B7=20=ED=95=80=EC=B9=98=EB=8A=94=20=EB=8A=98=20=EB=B2=8C?= =?UTF-8?q?=EB=A6=AC=EB=A9=B4=20=ED=99=95=EB=8C=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 새 A00_Common/input_prefs.ts(INPUT_PREFS_KEY · DEFAULT_INPUT_PREFS · inputPrefs · wheelIn · wheelEventIn · 의존 없음) - 등록표 input-prefs(pref · global) 한 줄 - touch_bridge wheelZoomsIn · b_svg_zoom_pan · B04 3D 카메라 · M02 DF 그림 · M02 노무 그림 · 돋보기 · 시트 배선 - M02 그림 · 시트 · 돋보기 기본 방향 = 당김(지금과 반대) - 시험: test_input_prefs 새로 · 돋보기 · 266 · 244-8 헬퍼가 읽기 층을 불러옴 · 돋보기 부호 당김 기본 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01QfHKQs7J3hQELKMWQMUJEy --- A00_Common/b_page_state.ts | 2 + A00_Common/b_svg_zoom_pan.ts | 6 +- A00_Common/input_prefs.ts | 64 ++++++++++ A00_Common/spreadsheet/spreadsheet_grid.ts | 4 +- A00_Common/touch_bridge.ts | 7 +- B04_PreProcess/B04_PreProcess_UI_Camera.ts | 5 +- M02_MasterTemplete/DataFlow/M02_DF_Diagram.ts | 3 +- .../M02_MasterTemplete_Labor_Diagram.ts | 3 +- resources/tester/helper_218_1_loupe.cjs | 32 ++--- resources/tester/helper_266_detail_home.cjs | 1 + resources/tester/helper_input_prefs.cjs | 63 ++++++++++ resources/tester/test_220_1_loupe_wheel.py | 6 +- .../test_244_8_b08_milestone_no_scroll.py | 5 +- resources/tester/test_input_prefs.py | 113 ++++++++++++++++++ ui_template/ui_template_loupe.ts | 5 +- 15 files changed, 288 insertions(+), 31 deletions(-) create mode 100644 A00_Common/input_prefs.ts create mode 100644 resources/tester/helper_input_prefs.cjs create mode 100644 resources/tester/test_input_prefs.py diff --git a/A00_Common/b_page_state.ts b/A00_Common/b_page_state.ts index b3745a035..2f14d7bab 100644 --- a/A00_Common/b_page_state.ts +++ b/A00_Common/b_page_state.ts @@ -103,6 +103,8 @@ export const STATE_REGISTRY = { "cross-columns": { bucket: "pref", scope: "global" }, /** B05 · B06 종단 누가토량 알약 자리(309-2) — 그래프 영역 안 비율 `{"x":0~1,"y":0~1}` · 없으면 처음 자리. */ "mass-haul-badge-pos": { bucket: "pref", scope: "global" }, + /** ☰ [설정] 마우스 · 단축키(332) — 읽기는 `input_prefs.ts`(같은 키 글자) · [저장] 때만 씀. */ + "input-prefs": { bucket: "pref", scope: "global" }, /** B05 3D 영역 투영(316-2) `perspective · ortho` · 없으면 원근. */ "b05-3d-projection": { bucket: "pref", scope: "global" }, "section-panel-height": { diff --git a/A00_Common/b_svg_zoom_pan.ts b/A00_Common/b_svg_zoom_pan.ts index 9590e576c..ea40f76d9 100644 --- a/A00_Common/b_svg_zoom_pan.ts +++ b/A00_Common/b_svg_zoom_pan.ts @@ -14,6 +14,8 @@ * 그리는 일은 안 한다 — 넘겨받은 `layer` 에 `transform` 만 적는다. * ========================================================================== */ +import { wheelEventIn } from "./input_prefs"; + /** 줌·팬 상태 — 다시 그릴 때 배율을 되살리는 데 쓴다(2026-08-22 사용자 ③). */ export interface ZoomPanState { scale: number; @@ -213,8 +215,8 @@ export function attachSvgZoomPan(options: SvgZoomPanOptions): ZoomPanHandle { const rect = svg.getBoundingClientRect(); const cx = ((event.clientX - rect.left) / Math.max(rect.width, 1)) * widthPx; const cy = ((event.clientY - rect.top) / Math.max(rect.height, 1)) * heightPx; - // 노선 편집 지도와 같은 방향 — **당기면 확대**(`deltaY > 0`). - zoomAt(event.deltaY > 0 ? WHEEL_IN : WHEEL_OUT, cx, cy); + // ☰ [설정] 휠 방향(332 · 기본 당기면 확대) — 그림 면이 손가락을 받으므로 핀치는 트랙패드뿐. + zoomAt(wheelEventIn(event) ? WHEEL_IN : WHEEL_OUT, cx, cy); }, { passive: false }, ); diff --git a/A00_Common/input_prefs.ts b/A00_Common/input_prefs.ts new file mode 100644 index 000000000..49fff86db --- /dev/null +++ b/A00_Common/input_prefs.ts @@ -0,0 +1,64 @@ +/* ============================================================================= + * input_prefs.ts + * 마우스 · 단축키 설정 읽기 층(PLAN 332) — ☰ [설정] 창이 [저장] 때 계정 pref 로 씀. + * + * - 값 = `aislo:pref:input-prefs`(등록표 `input-prefs`) JSON 한 벌 · 부를 때마다 읽음(새로고침 없이 바로 따름) + * - 없음 · 깨짐 · Node(저장소 없음) = 기본값(= 지금 동작) · 의존 없음 · 불러올 때 window 안 만짐(CAD 도 import) + * - 휠 확대 방향 하나로 모든 화면 일체 — 당김 = 휠을 몸 쪽으로(deltaY > 0) 굴리면 확대 + * - 핀치(트랙패드 · 손가락)는 설정과 상관없이 늘 벌리면 확대(deltaY < 0) + * ========================================================================== */ + +export type WheelZoom = "pull" | "push"; + +export interface InputPrefs { + /** 휠 확대 방향 — pull = 당기면 확대 · push = 밀면 확대 */ + wheelZoom: WheelZoom; + /** CAD 내 별칭 `{명령: [별칭]}`(대문자 · acad.pgp 식 — 명령 이름 > 내 별칭 > 기본 별칭) */ + cadAliases: Record; +} + +/** `stateKey("input-prefs")` 와 같은 키(시험이 맞춰 봄) — 등록표를 불러오면 window 가 딸려 와서 글자로 둠 */ +export const INPUT_PREFS_KEY = "aislo:pref:input-prefs"; + +export const DEFAULT_INPUT_PREFS: InputPrefs = { wheelZoom: "pull", cadAliases: {} }; + +const cleanAliases = (raw: unknown): Record => { + const out: Record = {}; + if (!raw || typeof raw !== "object") return out; + for (const [id, list] of Object.entries(raw as Record)) { + if (!Array.isArray(list)) continue; + const aliases = list + .filter((a): a is string => typeof a === "string") + .map((a) => a.toUpperCase()); + if (aliases.length) out[id.toUpperCase()] = aliases; + } + return out; +}; + +/** 지금 설정 — 부를 때마다 저장소에서 읽음 */ +export function inputPrefs(): InputPrefs { + try { + const raw = globalThis.localStorage?.getItem(INPUT_PREFS_KEY); + if (!raw) return { ...DEFAULT_INPUT_PREFS, cadAliases: {} }; + const value = JSON.parse(raw) as Partial | null; + return { + wheelZoom: value?.wheelZoom === "push" ? "push" : "pull", + cadAliases: cleanAliases(value?.cadAliases), + }; + } catch { + return { ...DEFAULT_INPUT_PREFS, cadAliases: {} }; + } +} + +/** 휠 한 번이 확대인가 — 설정 방향을 따름 */ +export const wheelIn = (deltaY: number): boolean => + inputPrefs().wheelZoom === "push" ? deltaY < 0 : deltaY > 0; + +/** 트랙패드 핀치 — 브라우저가 Ctrl+휠 · 잘게 쪼갠 값으로 냄 + * ponytail: |deltaY| < 50 로 마우스 Ctrl+휠(한 칸 ≥ 약 100)과 가름 · 잘게 도는 마우스는 핀치로 봄 */ +export const isTrackpadPinch = (e: Pick): boolean => + e.ctrlKey && Math.abs(e.deltaY) < 50; + +/** 휠 사건이 확대인가 — 핀치(`pinch` 손가락 벌림 · 트랙패드)는 늘 벌리면 확대 · 나머지는 설정 방향 */ +export const wheelEventIn = (e: Pick, pinch = false): boolean => + pinch || isTrackpadPinch(e) ? e.deltaY < 0 : wheelIn(e.deltaY); diff --git a/A00_Common/spreadsheet/spreadsheet_grid.ts b/A00_Common/spreadsheet/spreadsheet_grid.ts index da403b2d4..9e0b97db7 100644 --- a/A00_Common/spreadsheet/spreadsheet_grid.ts +++ b/A00_Common/spreadsheet/spreadsheet_grid.ts @@ -13,6 +13,7 @@ import "./spreadsheet_grid.css"; import { colIndex, toA1 } from "./spreadsheet_address"; +import { wheelZoomsIn } from "../touch_bridge"; import { buildColGeometry, buildOutlineRuns, @@ -271,7 +272,8 @@ export function createGrid(ctx: SpreadsheetContext): GridHandle { if (!ev.ctrlKey) return; ev.preventDefault(); const before = zoom; - const next = Math.min(ZOOM_MAX, Math.max(ZOOM_MIN, zoom * Math.exp(-ev.deltaY * 0.0015))); + const step = (wheelZoomsIn(ev) ? 1 : -1) * Math.abs(ev.deltaY) * 0.0015; // ☰ [설정] 방향(332) + const next = Math.min(ZOOM_MAX, Math.max(ZOOM_MIN, zoom * Math.exp(step))); if (next === before) return; zoom = next; colGeo = scaledGeometry(baseColGeo, zoom); diff --git a/A00_Common/touch_bridge.ts b/A00_Common/touch_bridge.ts index 6c83aa700..6fc8a810d 100644 --- a/A00_Common/touch_bridge.ts +++ b/A00_Common/touch_bridge.ts @@ -11,6 +11,8 @@ * 마우스 · 펜 이벤트는 건드리지 않음. * ========================================================================== */ +import { wheelEventIn } from "./input_prefs"; + const LONG_MS = 500; const SLOP_PX = 10; const DOUBLE_MS = 300; @@ -31,9 +33,8 @@ let contactPx = 0; /** 고름 반경(px) — 손가락으로 눌렀으면 접촉 크기만큼(최소 24px) · 마우스 · 펜은 그대로(기기대응 4-4) */ export const pickPx = (px: number): number => (contactPx ? Math.max(px, contactPx) : px); -/** 「당기면 확대」 휠 화면(B04 · B05 지도)의 확대 판정 — 손가락 벌림은 거꾸로라 뒤집음 */ -export const wheelZoomsIn = (e: WheelEvent): boolean => - isPinchWheel(e) ? e.deltaY < 0 : e.deltaY > 0; +/** 휠 화면(B04 · B05 지도 · M02 그림 · 시트)의 확대 판정 — ☰ [설정] 방향(332) · 손가락 벌림은 늘 벌리면 확대 */ +export const wheelZoomsIn = (e: WheelEvent): boolean => wheelEventIn(e, isPinchWheel(e)); const ownsTouch = (t: EventTarget | null): boolean => t instanceof Element && !!t.closest("[data-touch-own]"); diff --git a/B04_PreProcess/B04_PreProcess_UI_Camera.ts b/B04_PreProcess/B04_PreProcess_UI_Camera.ts index d5adca1d9..8bf42cc44 100644 --- a/B04_PreProcess/B04_PreProcess_UI_Camera.ts +++ b/B04_PreProcess/B04_PreProcess_UI_Camera.ts @@ -1,6 +1,7 @@ import * as THREE from "three"; import type { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js"; import type { SurfaceBounds } from "./B04_PreProcess_Api_Fetch"; +import { wheelEventIn } from "../A00_Common/input_prefs"; export const SURFACE_CAMERA_FOV = 50; const LIGHT_VIEWER_BACKGROUND = 0xf5f7f9; @@ -246,11 +247,11 @@ export function bindCursorPivotControls(options: CursorPivotOptions): () => void } /** 휠 줌 — 커서가 가리키는 지점을 축으로 삼아 그 점이 화면에 고정된 채 멀어지고 가까워진다. - * 휠을 위로 올리면 멀어진다(사용자 지시). */ + * 방향 = ☰ [설정](332 · 기본 당기면 가까워짐 · 위로 올리면 멀어짐). */ function onWheel(event: WheelEvent): void { if (!controls.enabled || options.blocked?.()) return; event.preventDefault(); - zoomAround(event, event.deltaY < 0 ? 1 / ZOOM_STEP : ZOOM_STEP); + zoomAround(event, wheelEventIn(event) ? ZOOM_STEP : 1 / ZOOM_STEP); } /** 그 자리를 축으로 멀어지고(factor > 1) 가까워짐 — 휠 · 두 손가락 벌림이 같이 씀 */ diff --git a/M02_MasterTemplete/DataFlow/M02_DF_Diagram.ts b/M02_MasterTemplete/DataFlow/M02_DF_Diagram.ts index 76d4b028c..0623a007e 100644 --- a/M02_MasterTemplete/DataFlow/M02_DF_Diagram.ts +++ b/M02_MasterTemplete/DataFlow/M02_DF_Diagram.ts @@ -7,6 +7,7 @@ * 그리기만(바꾸는 것은 부른 쪽이 Model 로) · 순수 부분은 `M02_DF_Diagram_Parts.ts`. * ========================================================================== */ +import { wheelZoomsIn } from "../../A00_Common/touch_bridge"; import { createButton, el, showConfirmDialog } from "@ui/ui_template_elements"; import type { CfBox, CfKind, CfLogic, CfPill, CfPort } from "./M02_DF_Model"; import { @@ -454,7 +455,7 @@ export function renderDiagram( "wheel", (ev) => { ev.preventDefault(); - setZoom(st.zoom * (ev.deltaY < 0 ? 1.1 : 1 / 1.1), ev.clientX, ev.clientY); + setZoom(st.zoom * (wheelZoomsIn(ev) ? 1.1 : 1 / 1.1), ev.clientX, ev.clientY); // ☰ [설정] 방향(332) }, { passive: false }, ); diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_Diagram.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_Diagram.ts index b966ba85c..1d653f7a2 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_Diagram.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_Diagram.ts @@ -7,6 +7,7 @@ * ========================================================================== */ import { currentLanguageIndex } from "@ui/ui_template_locale"; +import { wheelZoomsIn } from "../A00_Common/touch_bridge"; import { createButton, el } from "@ui/ui_template_elements"; import { formatMoney } from "../M01_MasterData/M01_MasterData_UI_Logic_Money"; import "./M02_MasterTemplete_Labor_Diagram.css"; @@ -313,7 +314,7 @@ function mountShell(host: HTMLElement): State { if (!e.ctrlKey) return; e.preventDefault(); const r = viewport.getBoundingClientRect(); - zoomAt(st, e.deltaY < 0 ? 1.1 : 1 / 1.1, e.clientX - r.left, e.clientY - r.top); + zoomAt(st, wheelZoomsIn(e) ? 1.1 : 1 / 1.1, e.clientX - r.left, e.clientY - r.top); // ☰ [설정] 방향(332) }, { passive: false }, ); diff --git a/resources/tester/helper_218_1_loupe.cjs b/resources/tester/helper_218_1_loupe.cjs index ac649dbf7..031fa6f4d 100644 --- a/resources/tester/helper_218_1_loupe.cjs +++ b/resources/tester/helper_218_1_loupe.cjs @@ -15,7 +15,9 @@ function load(file) { ? {} : p.endsWith("key_shortcut") // 공용 키 판정(214-2)도 그 자리에서 트랜스파일 ? load(path.join(ROOT, "A00_Common", "key_shortcut.ts")) - : require(p); + : p.endsWith("input_prefs") // 휠 방향(332 · Node = 기본 당김) + ? load(path.join(ROOT, "A00_Common", "input_prefs.ts")) + : require(p); new Function("exports", "module", "require", js)(box.exports, box, req); return box.exports; } @@ -100,30 +102,30 @@ r.consts = { r.listeners = win.calls.map((c) => [c.type, c.capture]); } { - // 220-1 휠 — 켜 있는 동안만 배율 · 스크롤 막음 · 켤 때마다 2배 + // 220-1 휠 — 켜 있는 동안만 배율 · 스크롤 막음 · 켤 때마다 2배 · 확대 = 당김(deltaY > 0 · 332 기본) const { win, view } = setup(); const wheel = (deltaY) => win.fire("wheel", { deltaY }); const zoomLog = () => view.log.filter((l) => l[0] === "zoom").map((l) => l[1]); - const offEv = wheel(-100); + const offEv = wheel(100); const offState = { prevented: offEv.prevented, stopped: offEv.stopped, log: view.log.length }; win.fire("keydown", { code: "KeyZ" }); - const up = wheel(-100); + const up = wheel(100); const upState = { prevented: up.prevented, stopped: up.stopped, zoom: zoomLog().at(-1) }; - wheel(100); - wheel(100); + wheel(-100); + wheel(-100); const down = zoomLog().at(-1); wheel(0); // 가로 굴림 — 배율 그대로 · 스크롤은 막음 const flat = zoomLog().at(-1); - for (let i = 0; i < 30; i++) wheel(-50); + for (let i = 0; i < 30; i++) wheel(50); const max = zoomLog().at(-1); - for (let i = 0; i < 30; i++) wheel(120); + for (let i = 0; i < 30; i++) wheel(-120); const min = zoomLog().at(-1); win.fire("keyup", { code: "KeyZ" }); const n = zoomLog().length; - const afterOff = wheel(-100); + const afterOff = wheel(100); const afterOffZoomed = zoomLog().length !== n; win.fire("keydown", { code: "KeyZ" }); - wheel(-100); + wheel(100); r.wheel = { off: offState, up: upState, @@ -134,17 +136,17 @@ r.consts = { afterOff: { prevented: afterOff.prevented, stopped: afterOff.stopped, zoomed: afterOffZoomed }, reopen: zoomLog().at(-1), // 다시 켜면 2배에서 한 칸 pure: [ - m.nextLoupeZoom(2, -1), m.nextLoupeZoom(2, 1), - m.nextLoupeZoom(6, -1), - m.nextLoupeZoom(1.5, 1), + m.nextLoupeZoom(2, -1), + m.nextLoupeZoom(6, 1), + m.nextLoupeZoom(1.5, -1), ], }; // 길게 누름으로 켠 동안도 같음 const b = setup(); b.win.fire("mousedown", { button: 2, buttons: 2 }); b.win.tick(400); - const press = b.win.fire("wheel", { deltaY: -1 }); + const press = b.win.fire("wheel", { deltaY: 1 }); r.wheel.press = { prevented: press.prevented, zoom: b.view.log.at(-1) }; } { @@ -298,7 +300,7 @@ r.origin = { // 켜진 채 CAD 위로(mouseover · mousemove 둘 다) — 숨김 · 휠 안 막음 · 나오면 배율 그대로 보임 const { win, view } = setup(); win.fire("keydown", { code: "KeyZ" }); - win.fire("wheel", { deltaY: -100 }); // 2.25 + win.fire("wheel", { deltaY: 100 }); // 2.25 win.fire("mouseover", { target: cad }); const hidden = last(view); const n = view.log.length; diff --git a/resources/tester/helper_266_detail_home.cjs b/resources/tester/helper_266_detail_home.cjs index a5dfdbdbf..9b02f78cb 100644 --- a/resources/tester/helper_266_detail_home.cjs +++ b/resources/tester/helper_266_detail_home.cjs @@ -28,6 +28,7 @@ function loadTs(full) { new Function("exports", "module", "require", js)(box.exports, box, localRequire); return box.exports; } +STUBS["./input_prefs"] = loadTs(path.join(ROOT, "A00_Common", "input_prefs.ts")); // 휠 방향(332 · Node = 기본 당김) const metrics = loadTs(path.join(B06, "B06_Section_UI_Cross_View_Metrics.ts")); const { attachSvgZoomPan } = loadTs(path.join(ROOT, "A00_Common", "b_svg_zoom_pan.ts")); diff --git a/resources/tester/helper_input_prefs.cjs b/resources/tester/helper_input_prefs.cjs new file mode 100644 index 000000000..860064bb5 --- /dev/null +++ b/resources/tester/helper_input_prefs.cjs @@ -0,0 +1,63 @@ +/* PLAN 332-1 단위검증 헬퍼 — 휠 방향 읽기 층(`A00_Common/input_prefs.ts`) · 손가락 다리(`touch_bridge.ts`) + * · 등록표 키(`b_page_state.ts`)를 그 자리에서 트랜스파일해 Node 로 돌림. 가짜 localStorage 로 설정을 바꿔 봄. + * 결과 = 표준출력 JSON 한 벌. 짝 = `test_input_prefs.py`. */ +const fs = require("fs"); +const path = require("path"); +const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript")); + +const ROOT = path.join(__dirname, "..", ".."); +const A00 = path.join(ROOT, "A00_Common"); +function load(file) { + const js = ts.transpileModule(fs.readFileSync(file, "utf8"), { + compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 }, + }).outputText; + const box = { exports: {} }; + const req = (p) => (p === "./input_prefs" ? prefs : {}); + new Function("exports", "module", "require", js)(box.exports, box, req); + return box.exports; +} + +const out = {}; +// ① Node(저장소 없음) — 불러올 때 window 를 안 만지고 기본값 +const prefs = load(path.join(A00, "input_prefs.ts")); +const bridge = load(path.join(A00, "touch_bridge.ts")); +const page = load(path.join(A00, "b_page_state.ts")); +const sample = () => ({ + prefs: prefs.inputPrefs(), + pull: prefs.wheelIn(100), + push: prefs.wheelIn(-100), + mouseCtrl: prefs.wheelEventIn({ ctrlKey: true, deltaY: 100 }), + padSpread: prefs.wheelEventIn({ ctrlKey: true, deltaY: -3.5 }), + padPinch: prefs.wheelEventIn({ ctrlKey: true, deltaY: 3.5 }), + fingerSpread: prefs.wheelEventIn({ ctrlKey: true, deltaY: -9.5 }, true), + bridgePull: bridge.wheelZoomsIn({ ctrlKey: false, deltaY: 100 }), +}); +out.node = { key: prefs.INPUT_PREFS_KEY, default: prefs.DEFAULT_INPUT_PREFS, ...sample() }; +out.registryKey = page.stateKey("input-prefs"); +out.registryEntry = page.STATE_REGISTRY["input-prefs"]; + +// ② 브라우저 저장소 흉내 — 값을 바꾸면 다음 부름부터 바로 따름 +const store = new Map(); +globalThis.localStorage = { + getItem: (k) => (store.has(k) ? store.get(k) : null), + setItem: (k, v) => store.set(k, String(v)), + removeItem: (k) => store.delete(k), +}; +const set = (v) => store.set(prefs.INPUT_PREFS_KEY, v); +set(JSON.stringify({ wheelZoom: "push", cadAliases: { line: ["ll", 3], ARC: [] } })); +out.push = sample(); +set("{깨짐"); +out.broken = sample(); +set(JSON.stringify({ wheelZoom: "sideways" })); +out.unknown = sample(); +store.delete(prefs.INPUT_PREFS_KEY); +out.empty = sample(); +// ③ 불러오는 동안 저장소가 던지는 브라우저(사생활 보호) — 기본값 +globalThis.localStorage = { + getItem: () => { + throw new Error("blocked"); + }, +}; +out.blocked = sample(); + +process.stdout.write(JSON.stringify(out)); diff --git a/resources/tester/test_220_1_loupe_wheel.py b/resources/tester/test_220_1_loupe_wheel.py index a1c5bda7d..e70b9c4da 100644 --- a/resources/tester/test_220_1_loupe_wheel.py +++ b/resources/tester/test_220_1_loupe_wheel.py @@ -34,10 +34,10 @@ def test_지름_360_배율_한도(r): assert (c["zoom"], c["zoomMin"], c["zoomMax"], c["zoomStep"]) == (2, 1.5, 6, 0.25) -def test_휠_위_크게_아래_작게_한도(r): +def test_휠_당김_크게_밀기_작게_한도(r): # 332 기본 = 당김 확대 w = r["wheel"] - assert w["up"]["zoom"] == 2.25 # 위로 한 칸 - assert w["down"] == 1.75 # 아래로 두 칸 + assert w["up"]["zoom"] == 2.25 # 당겨 한 칸 + assert w["down"] == 1.75 # 밀어 두 칸 assert w["flat"] == 1.75 # 가로 굴림은 그대로 assert w["max"] == 6 and w["min"] == 1.5 assert w["pure"] == [2.25, 1.75, 6, 1.5] diff --git a/resources/tester/test_244_8_b08_milestone_no_scroll.py b/resources/tester/test_244_8_b08_milestone_no_scroll.py index 17c980d2a..e3c7ecce2 100644 --- a/resources/tester/test_244_8_b08_milestone_no_scroll.py +++ b/resources/tester/test_244_8_b08_milestone_no_scroll.py @@ -20,7 +20,10 @@ def _read(rel): def _node(script): - src = IMPORTS.sub("", _read("A00_Common/b_svg_zoom_pan.ts")) + # 휠 방향 읽기 층(332) 을 앞에 붙임 — Node 엔 저장소가 없어 기본(당기면 확대) + src = _read("A00_Common/input_prefs.ts") + IMPORTS.sub( + "", _read("A00_Common/b_svg_zoom_pan.ts") + ) result = subprocess.run( ["node", "--experimental-strip-types", "--no-warnings", "--input-type=module-typescript", "-e", src + "\n" + script], diff --git a/resources/tester/test_input_prefs.py b/resources/tester/test_input_prefs.py new file mode 100644 index 000000000..c9c2cd0b7 --- /dev/null +++ b/resources/tester/test_input_prefs.py @@ -0,0 +1,113 @@ +"""PLAN 332-1 — 휠 확대 방향 한 설정(`A00_Common/input_prefs.ts`) · 모든 휠 화면이 그 판정을 거침. + +· 기본 = 당김(휠을 몸 쪽으로 = 확대) · 밀기 저장 뒤 뒤집힘 · 깨짐 · 모르는 값 · 막힌 저장소 · Node = 기본 +· 핀치(트랙패드 · 손가락)는 설정과 상관없이 늘 벌리면 확대 +· 키 = 등록표 `input-prefs`(pref · global) 와 같은 글자 +짝: `helper_input_prefs.cjs`(실제 TS 를 Node 로 돌림). +""" + +from __future__ import annotations + +import json +import os +import re +import subprocess + +import pytest + +HERE = os.path.dirname(__file__) +ROOT = os.path.join(HERE, "..", "..") + +pytestmark = pytest.mark.skipif( + not os.path.isdir(os.path.join(ROOT, "config", "node_modules")), + reason="프론트엔드 의존성 미설치", +) + + +def _read(rel: str) -> str: + with open(os.path.join(ROOT, rel), encoding="utf-8") as f: + return f.read() + + +@pytest.fixture(scope="module") +def r() -> dict: + proc = subprocess.run( # noqa: S603 — 고정 실행 파일 + ["node", os.path.join(HERE, "helper_input_prefs.cjs")], + capture_output=True, + text=True, + encoding="utf-8", + timeout=60, + ) + assert proc.returncode == 0, proc.stderr + return json.loads(proc.stdout) + + +PULL = {"pull": True, "push": False, "mouseCtrl": True, "bridgePull": True} +PINCH = {"padSpread": True, "padPinch": False, "fingerSpread": True} + + +def _pick(case: dict, keys: dict) -> dict: + return {k: case[k] for k in keys} + + +def test_Node_기본_당김(r): + n = r["node"] + assert n["default"] == {"wheelZoom": "pull", "cadAliases": {}} + assert n["prefs"] == n["default"] + assert _pick(n, PULL) == PULL and _pick(n, PINCH) == PINCH + + +def test_밀기_저장_뒤_뒤집힘_핀치는_그대로(r): + p = r["push"] + assert p["prefs"]["wheelZoom"] == "push" + assert _pick(p, PULL) == {"pull": False, "push": True, "mouseCtrl": False, "bridgePull": False} + assert _pick(p, PINCH) == PINCH # 벌리면 확대는 설정과 상관없음 + # 별칭 = 대문자 · 글자 아닌 것 · 빈 목록 버림 + assert p["prefs"]["cadAliases"] == {"LINE": ["LL"]} + + +@pytest.mark.parametrize("case", ["broken", "unknown", "empty", "blocked"]) +def test_깨짐_모르는_값_없음_막힘_기본(r, case): + c = r[case] + assert c["prefs"] == {"wheelZoom": "pull", "cadAliases": {}} + assert _pick(c, PULL) == PULL and _pick(c, PINCH) == PINCH + + +def test_등록표_키(r): + assert r["node"]["key"] == r["registryKey"] == "aislo:pref:input-prefs" + assert r["registryEntry"] == {"bucket": "pref", "scope": "global"} + + +def test_읽기_층은_의존_없음_불러올_때_window_안_만짐(): + src = _read("A00_Common/input_prefs.ts") + assert not re.search(r"^import ", src, re.M) + assert "window." not in src + + +# 휠 줌 자리 — 직접 박은 방향(`deltaY < 0 ?` · `deltaY > 0 ?` · `-ev.deltaY`)이 없고 공용 판정을 부름 +WHEEL_SITES = { + "A00_Common/touch_bridge.ts": "wheelEventIn(e, isPinchWheel(e))", + "A00_Common/b_svg_zoom_pan.ts": "wheelEventIn(event)", + "B04_PreProcess/B04_PreProcess_UI_Camera.ts": "wheelEventIn(event)", + "M02_MasterTemplete/DataFlow/M02_DF_Diagram.ts": "wheelZoomsIn(ev)", + "M02_MasterTemplete/M02_MasterTemplete_Labor_Diagram.ts": "wheelZoomsIn(e)", + "ui_template/ui_template_loupe.ts": "wheelIn(deltaY)", + "A00_Common/spreadsheet/spreadsheet_grid.ts": "wheelZoomsIn(ev)", + # wheelZoomsIn 을 거쳐 따라오는 셋 + "B04_PreProcess/B04_PreProcess_UI_MapViewer.ts": "wheelZoomsIn(event)", + "B05_Profile/B05_Profile_UI_Drainage_Interact.ts": "wheelZoomsIn(event)", + "B05_Profile/B05_Profile_UI_RouteEdit_Input.ts": "wheelZoomsIn(event)", +} + + +@pytest.mark.parametrize("rel", sorted(WHEEL_SITES)) +def test_휠_자리_공용_판정(rel): + src = _read(rel) + assert WHEEL_SITES[rel] in src + assert not re.search(r"deltaY [<>] 0 \?", src), rel + assert "-ev.deltaY" not in src and "Math.sign(-deltaY)" not in src + + +def test_700줄_안(): + for rel in WHEEL_SITES: + assert _read(rel).count("\n") <= 700, rel diff --git a/ui_template/ui_template_loupe.ts b/ui_template/ui_template_loupe.ts index 16479963a..68a0cfee4 100644 --- a/ui_template/ui_template_loupe.ts +++ b/ui_template/ui_template_loupe.ts @@ -14,6 +14,7 @@ import "./ui_template_loupe.css"; import { keyLetter } from "../A00_Common/key_shortcut"; +import { wheelIn } from "../A00_Common/input_prefs"; /** 원 지름(px) */ export const LOUPE_DIAMETER = 360; @@ -86,9 +87,9 @@ export function isCadTarget(target: unknown): boolean { return !!(target as Partial | null)?.closest?.(LOUPE_SKIP); } -/** 휠 한 번 뒤 배율 — 위로 굴림(deltaY < 0) = 크게 · 한도 안 */ +/** 휠 한 번 뒤 배율 — ☰ [설정] 방향(332 · 기본 당김 = deltaY > 0 = 크게) · 한도 안 */ export function nextLoupeZoom(zoom: number, deltaY: number): number { - const step = Math.sign(-deltaY) * LOUPE_ZOOM_STEP; + const step = deltaY ? (wheelIn(deltaY) ? 1 : -1) * LOUPE_ZOOM_STEP : 0; return Math.max(LOUPE_ZOOM_MIN, Math.min(LOUPE_ZOOM_MAX, zoom + step)); } From 32ddaa8064105f3f56458a368b85804d8083cbce Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 15 Oct 2026 12:36:19 +0900 Subject: [PATCH 25/81] =?UTF-8?q?feat(CAD):=20331-2=20=EC=9D=B4=EB=8F=99?= =?UTF-8?q?=20=C2=B7=20=EB=B3=B5=EC=82=AC=20=C2=B7=20=ED=9A=8C=EC=A0=84=20?= =?UTF-8?q?=ED=82=A4=EB=B3=B4=EB=93=9C=20=EA=B0=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 이동 · 복사 — 기준점 `x,y` · 둘째 점 거리 · `x,y` · `@x,y` · 각도 고정 · 미리보기도 같은 점 계산(getPointFromEvent) - 회전 — 기준점 `x,y` · 각도 숫자 바로(반시계 +) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013QyNXY2TUKXCFMwAca2APG --- .../openwebcad/src/tools/copy-tool.ts | 43 ++++++++++++---- .../openwebcad/src/tools/move-tool.ts | 39 +++++++++++---- .../openwebcad/src/tools/rotate-tool.ts | 49 ++++++++++++++++--- resources/tester/cad/keyboard-chain.test.ts | 49 +++++++++++++++++++ 4 files changed, 154 insertions(+), 26 deletions(-) diff --git a/B07_DesignDetail/openwebcad/src/tools/copy-tool.ts b/B07_DesignDetail/openwebcad/src/tools/copy-tool.ts index 73b6811fb..25b7d8542 100644 --- a/B07_DesignDetail/openwebcad/src/tools/copy-tool.ts +++ b/B07_DesignDetail/openwebcad/src/tools/copy-tool.ts @@ -10,7 +10,8 @@ import { setShouldDrawHelpers, } from '../state'; import { Tool } from '../tools'; -import type { DrawEvent, MouseClickEvent, StateEvent, ToolContext } from './tool.types'; +import type { DrawEvent, PointInputEvent, StateEvent, ToolContext } from './tool.types'; +import { getPointFromEvent } from '../helpers/get-point-from-event'; import { assign, createMachine, sendTo } from 'xstate'; import { selectToolStateMachine } from './select-tool'; import type { Entity } from '../entities/Entity'; @@ -147,6 +148,10 @@ export const copyToolStateMachine = createMachine( actions: [CopyAction.RECORD_START_POINT, CopyAction.COPY_SELECTION_BEFORE_COPY], target: CopyState.WAITING_FOR_END_COPY_POINT, }, + ABSOLUTE_POINT_INPUT: { + actions: [CopyAction.RECORD_START_POINT, CopyAction.COPY_SELECTION_BEFORE_COPY], + target: CopyState.WAITING_FOR_END_COPY_POINT, + }, ESC: { actions: CopyAction.DESELECT_ENTITIES, target: CopyState.INIT, @@ -166,6 +171,18 @@ export const copyToolStateMachine = createMachine( actions: [CopyAction.COPY_SELECTION], target: CopyState.WAITING_FOR_END_COPY_POINT, }, + NUMBER_INPUT: { + actions: [CopyAction.COPY_SELECTION], + target: CopyState.WAITING_FOR_END_COPY_POINT, + }, + ABSOLUTE_POINT_INPUT: { + actions: [CopyAction.COPY_SELECTION], + target: CopyState.WAITING_FOR_END_COPY_POINT, + }, + RELATIVE_POINT_INPUT: { + actions: [CopyAction.COPY_SELECTION], + target: CopyState.WAITING_FOR_END_COPY_POINT, + }, ESC: { actions: CopyAction.DESELECT_ENTITIES, target: CopyState.INIT, @@ -191,10 +208,9 @@ export const copyToolStateMachine = createMachine( setShouldDrawHelpers(true); }, [CopyAction.RECORD_START_POINT]: assign(({ event }) => { - setAngleGuideOriginPoint((event as MouseClickEvent).worldMouseLocation); - return { - startPoint: (event as MouseClickEvent).worldMouseLocation, - }; + const startPoint = getPointFromEvent(null, event as PointInputEvent); + setAngleGuideOriginPoint(startPoint); + return { startPoint }; }), [CopyAction.COPY_SELECTION_BEFORE_COPY]: assign(({ context }) => { const selectedEntities = getSelectedEntities(); @@ -208,7 +224,9 @@ export const copyToolStateMachine = createMachine( return { startPoint: context.startPoint, // Make a copy of the selected entities before copying them, so we can restore them when the user cancels the copy action - originalSelectedEntities: compact(selectedEntities.map((entity) => cloneKeepingLook(entity))), + originalSelectedEntities: compact( + selectedEntities.map((entity) => cloneKeepingLook(entity)) + ), copiedEntities: selectedEntities, }; }), @@ -217,11 +235,13 @@ export const copyToolStateMachine = createMachine( throw new Error('[COPY] Calling draw temp copy line without a start point'); } - const endPointTemp = (event as DrawEvent).drawController.getWorldMouseLocation(); + const endPointTemp = getPointFromEvent(context.startPoint, event as DrawEvent); // Copy the entities to the new location // Draw all selected entities according to translation vector, so the user gets visual feedback of where the entities will be copied; - const movedEntities = context.originalSelectedEntities.map((entity) => cloneKeepingLook(entity)); + const movedEntities = context.originalSelectedEntities.map((entity) => + cloneKeepingLook(entity) + ); moveEntities( movedEntities, endPointTemp.x - context.startPoint.x, @@ -245,8 +265,11 @@ export const copyToolStateMachine = createMachine( } // Copy the entities one final time - const currentEndPoint = (event as MouseClickEvent).worldMouseLocation; - const copiedEntities = context.originalSelectedEntities.map((entity) => cloneKeepingLook(entity)); + // 클릭 · 거리 · 절대 · 상대 좌표(키보드) + const currentEndPoint = getPointFromEvent(context.startPoint, event as PointInputEvent); + const copiedEntities = context.originalSelectedEntities.map((entity) => + cloneKeepingLook(entity) + ); moveEntities( copiedEntities, currentEndPoint.x - context.startPoint.x, diff --git a/B07_DesignDetail/openwebcad/src/tools/move-tool.ts b/B07_DesignDetail/openwebcad/src/tools/move-tool.ts index ba19c7073..a2fe3cbfd 100644 --- a/B07_DesignDetail/openwebcad/src/tools/move-tool.ts +++ b/B07_DesignDetail/openwebcad/src/tools/move-tool.ts @@ -11,7 +11,8 @@ import { setShouldDrawHelpers, } from '../state'; import { Tool } from '../tools'; -import type { DrawEvent, MouseClickEvent, StateEvent, ToolContext } from './tool.types'; +import type { DrawEvent, PointInputEvent, StateEvent, ToolContext } from './tool.types'; +import { getPointFromEvent } from '../helpers/get-point-from-event'; import { assign, createMachine, sendTo } from 'xstate'; import { selectToolStateMachine } from './select-tool'; import type { Entity } from '../entities/Entity'; @@ -148,6 +149,10 @@ export const moveToolStateMachine = createMachine( actions: [MoveAction.RECORD_START_POINT, MoveAction.COPY_SELECTION_BEFORE_MOVE], target: MoveState.WAITING_FOR_END_MOVE_POINT, }, + ABSOLUTE_POINT_INPUT: { + actions: [MoveAction.RECORD_START_POINT, MoveAction.COPY_SELECTION_BEFORE_MOVE], + target: MoveState.WAITING_FOR_END_MOVE_POINT, + }, ESC: { actions: MoveAction.DESELECT_ENTITIES, target: MoveState.INIT, @@ -167,6 +172,18 @@ export const moveToolStateMachine = createMachine( actions: [MoveAction.MOVE_SELECTION, MoveAction.DESELECT_ENTITIES], target: MoveState.WAITING_FOR_SELECTION, }, + NUMBER_INPUT: { + actions: [MoveAction.MOVE_SELECTION, MoveAction.DESELECT_ENTITIES], + target: MoveState.WAITING_FOR_SELECTION, + }, + ABSOLUTE_POINT_INPUT: { + actions: [MoveAction.MOVE_SELECTION, MoveAction.DESELECT_ENTITIES], + target: MoveState.WAITING_FOR_SELECTION, + }, + RELATIVE_POINT_INPUT: { + actions: [MoveAction.MOVE_SELECTION, MoveAction.DESELECT_ENTITIES], + target: MoveState.WAITING_FOR_SELECTION, + }, ESC: { actions: MoveAction.RESTORE_ORIGINAL_ENTITIES, target: MoveState.INIT, @@ -192,10 +209,9 @@ export const moveToolStateMachine = createMachine( setShouldDrawHelpers(true); }, [MoveAction.RECORD_START_POINT]: assign(({ event }) => { - setAngleGuideOriginPoint((event as MouseClickEvent).worldMouseLocation); - return { - startPoint: (event as MouseClickEvent).worldMouseLocation, - }; + const startPoint = getPointFromEvent(null, event as PointInputEvent); + setAngleGuideOriginPoint(startPoint); + return { startPoint }; }), [MoveAction.COPY_SELECTION_BEFORE_MOVE]: assign(({ context }) => { const selectedEntities = getSelectedEntities(); @@ -211,7 +227,9 @@ export const moveToolStateMachine = createMachine( return { startPoint: context.startPoint, // Make a copy of the selected entities before moving them, so we can restore them when the user cancels the move action - originalSelectedEntities: compact(selectedEntities.map((entity) => cloneKeepingLook(entity, true))), + originalSelectedEntities: compact( + selectedEntities.map((entity) => cloneKeepingLook(entity, true)) + ), movedEntities: selectedEntities, }; }), @@ -220,11 +238,13 @@ export const moveToolStateMachine = createMachine( throw new Error('[MOVE] Calling draw temp move line without a start point'); } - const endPointTemp = (event as DrawEvent).drawController.getWorldMouseLocation(); + const endPointTemp = getPointFromEvent(context.startPoint, event as DrawEvent); // Move the entities to the new location // Draw all selected entities according to translation vector, so the user gets visual feedback of where the entities will be moved; - const movedEntities = context.originalSelectedEntities.map((entity) => cloneKeepingLook(entity, true)); + const movedEntities = context.originalSelectedEntities.map((entity) => + cloneKeepingLook(entity, true) + ); moveEntities( movedEntities, endPointTemp.x - context.startPoint.x, @@ -248,7 +268,8 @@ export const moveToolStateMachine = createMachine( } // Move the entities one final time - const currentEndPoint = (event as MouseClickEvent).worldMouseLocation; + // 클릭 · 거리 · 절대 · 상대 좌표(키보드) + const currentEndPoint = getPointFromEvent(context.startPoint, event as PointInputEvent); moveEntities( context.originalSelectedEntities, currentEndPoint.x - context.startPoint.x, diff --git a/B07_DesignDetail/openwebcad/src/tools/rotate-tool.ts b/B07_DesignDetail/openwebcad/src/tools/rotate-tool.ts index 46888d7a5..c2930e017 100644 --- a/B07_DesignDetail/openwebcad/src/tools/rotate-tool.ts +++ b/B07_DesignDetail/openwebcad/src/tools/rotate-tool.ts @@ -10,7 +10,15 @@ import { setShouldDrawHelpers, } from '../state'; import { Tool } from '../tools'; -import type { DrawEvent, MouseClickEvent, StateEvent, ToolContext } from './tool.types'; +import type { + DrawEvent, + MouseClickEvent, + NumberInputEvent, + PointInputEvent, + StateEvent, + ToolContext, +} from './tool.types'; +import { getPointFromEvent } from '../helpers/get-point-from-event'; import { assign, createMachine, sendTo } from 'xstate'; import { selectToolStateMachine } from './select-tool'; import type { Entity } from '../entities/Entity'; @@ -41,6 +49,7 @@ export enum RotateAction { COPY_SELECTION_BEFORE_ROTATE = 'COPY_SELECTION_BEFORE_ROTATE', DRAW_TEMP_ROTATE_ENTITIES = 'DRAW_TEMP_ROTATE_ENTITIES', ROTATE_SELECTION = 'ROTATE_SELECTION', + ROTATE_BY_ANGLE = 'ROTATE_BY_ANGLE', DESELECT_ENTITIES = 'DESELECT_ENTITIES', RESTORE_ORIGINAL_ENTITIES = 'RESTORE_ORIGINAL_ENTITIES', } @@ -147,6 +156,10 @@ export const rotateToolStateMachine = createMachine( actions: [RotateAction.RECORD_ROTATION_ORIGIN], target: RotateState.WAITING_FOR_ANGLE_START_POINT, }, + ABSOLUTE_POINT_INPUT: { + actions: [RotateAction.RECORD_ROTATION_ORIGIN], + target: RotateState.WAITING_FOR_ANGLE_START_POINT, + }, ESC: { actions: RotateAction.DESELECT_ENTITIES, target: RotateState.INIT, @@ -166,6 +179,15 @@ export const rotateToolStateMachine = createMachine( ], target: RotateState.WAITING_FOR_ANGLE_END_POINT, }, + NUMBER_INPUT: { + // 각도(도) 바로 — 반시계 + + actions: [ + RotateAction.COPY_SELECTION_BEFORE_ROTATE, + RotateAction.ROTATE_BY_ANGLE, + RotateAction.DESELECT_ENTITIES, + ], + target: RotateState.WAITING_FOR_SELECTION, + }, ESC: { actions: RotateAction.RESTORE_ORIGINAL_ENTITIES, target: RotateState.INIT, @@ -204,11 +226,9 @@ export const rotateToolStateMachine = createMachine( setShouldDrawHelpers(true); }, [RotateAction.RECORD_ROTATION_ORIGIN]: assign(({ context, event }): RotateContext => { - setAngleGuideOriginPoint((event as MouseClickEvent).worldMouseLocation); - return { - ...context, - rotationOrigin: (event as MouseClickEvent).worldMouseLocation, - }; + const rotationOrigin = getPointFromEvent(null, event as PointInputEvent); + setAngleGuideOriginPoint(rotationOrigin); + return { ...context, rotationOrigin }; }), [RotateAction.RECORD_ROTATION_ANGLE_START_POINT]: assign( ({ context, event }): RotateContext => { @@ -232,7 +252,9 @@ export const rotateToolStateMachine = createMachine( return { ...context, // Make a copy of the selected entities before rotating them, so we can restore them when the user cancels the rotate action - originalSelectedEntities: compact(selectedEntities.map((entity) => cloneKeepingLook(entity, true))), + originalSelectedEntities: compact( + selectedEntities.map((entity) => cloneKeepingLook(entity, true)) + ), }; }), [RotateAction.DRAW_TEMP_ROTATE_ENTITIES]: ({ context, event }) => { @@ -279,6 +301,19 @@ export const rotateToolStateMachine = createMachine( setGhostHelperEntities([]); setSelectedEntityIds([]); }, + [RotateAction.ROTATE_BY_ANGLE]: ({ context, event }) => { + if (!context.rotationOrigin) { + throw new Error('[ROTATE] Calling rotate by angle without a rotation origin'); + } + const radians = ((event as NumberInputEvent).value * Math.PI) / 180; + const rotatedEntities = compact( + context.originalSelectedEntities.map((entity) => cloneKeepingLook(entity, true)) + ); + for (const entity of rotatedEntities) entity.rotate(context.rotationOrigin, radians); + addEntities(rotatedEntities, true); + setGhostHelperEntities([]); + setSelectedEntityIds([]); + }, [RotateAction.DESELECT_ENTITIES]: assign(({ context }): RotateContext => { setGhostHelperEntities([]); setSelectedEntityIds([]); diff --git a/resources/tester/cad/keyboard-chain.test.ts b/resources/tester/cad/keyboard-chain.test.ts index be66f34d4..af5d1a5d6 100644 --- a/resources/tester/cad/keyboard-chain.test.ts +++ b/resources/tester/cad/keyboard-chain.test.ts @@ -148,3 +148,52 @@ describe("331-1 각도 고정 → 길이", () => { expect(getActiveToolActor()?.getSnapshot()?.context.values).toHaveLength(2); }); }); + +/** 선 (x1,y1)-(x2,y2) 하나를 키보드로 그리고 고름 */ +const drawAndSelect = (x1: number, y1: number, x2: number, y2: number) => { + type(controller, "l"); + type(controller, `${x1},${y1}`); + type(controller, `${x2},${y2}`); + key(controller, "Escape"); + key(controller, "Escape"); + setSelectedEntityIds(getEntities().map((entity) => entity.id)); +}; + +describe("331-2 이동 · 복사 · 회전 키보드 값", () => { + it("MOVE · 0,0 · @10,0 → 오른쪽 10", () => { + drawAndSelect(0, 0, 20, 0); + type(controller, "move"); + type(controller, "0,0"); + type(controller, "@10,0"); + const drawn = lines(); + expect(drawn).toHaveLength(1); + expectXy(drawn[0].start, 10, 0); + expectXy(drawn[0].end, 30, 0); + }); + + it("COPY · 0,0 · <0 · 5 → 원본 + 오른쪽 5", () => { + drawAndSelect(0, 0, 20, 0); + type(controller, "copy"); + type(controller, "0,0"); + type(controller, "<0"); + type(controller, "5"); + const starts = lines() + .map((segment) => segment.start.x) + .sort((a, b) => a - b); + expect(starts).toHaveLength(2); + expect(starts[0]).toBeCloseTo(0, 3); + expect(starts[1]).toBeCloseTo(5, 3); + }); + + it("ROTATE · 0,0 · 90 → 반시계 90°", () => { + drawAndSelect(10, 0, 20, 0); + type(controller, "rotate"); + type(controller, "0,0"); + type(controller, "90"); + const drawn = lines(); + expect(drawn).toHaveLength(1); + expectXy(drawn[0].start, 0, 10); + expectXy(drawn[0].end, 0, 20); + expect(toast.error).not.toHaveBeenCalled(); + }); +}); From be86e3ee2c97c25d2b434cec532b43e50ede1f7b Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 15 Oct 2026 12:37:17 +0900 Subject: [PATCH 26/81] =?UTF-8?q?feat(=EB=85=B8=EC=84=A0=EC=B6=94=EC=B2=9C?= =?UTF-8?q?):=20327-2=20=EB=AF=B8=EB=A6=AC=20=EC=85=88=20=C2=B7=20?= =?UTF-8?q?=ED=91=9C=EC=8B=9C=20=E2=80=94=20=EC=97=85=EB=A1=9C=EB=93=9C=20?= =?UTF-8?q?=EC=B2=B4=EC=9D=B8=20=EC=8A=A4=EB=83=85=EC=83=B7=20=EB=92=A4=20?= =?UTF-8?q?=EA=B8=B0=EB=B3=B8=20=EB=8B=A4=EC=84=AF=20=EC=95=8C=EC=95=BD=20?= =?UTF-8?q?=EC=98=88=EC=95=BD=20=C2=B7=20=EC=B6=94=EC=B2=9C=20=EC=B0=BD=20?= =?UTF-8?q?=EC=83=81=ED=83=9C=20=EC=A4=84=20=E3=80=8C=EC=A0=80=EC=9E=A5?= =?UTF-8?q?=EB=90=9C=20=EA=B2=B0=EA=B3=BC=E3=80=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - B03 자동 설계 체인: 초기값 스냅샷 뒤 precompute_defaults 예약(기다리지 않음 · 실패는 비치명적) - 추천 창: 서버 stored 를 받아 미리 보기 줄에 「(저장된 결과)」 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_014utrjfoW831Vc5wJUd13kL --- B03_FileInput/B03_FileInput_Service_Chain.py | 12 ++++++++++++ .../B05_Profile_UI_RouteEdit_Initial_Model.ts | 3 +++ B05_Profile/B05_Profile_UI_RouteEdit_Recommend.ts | 4 +++- 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/B03_FileInput/B03_FileInput_Service_Chain.py b/B03_FileInput/B03_FileInput_Service_Chain.py index 703c27d4e..90791b126 100644 --- a/B03_FileInput/B03_FileInput_Service_Chain.py +++ b/B03_FileInput/B03_FileInput_Service_Chain.py @@ -350,6 +350,18 @@ async def run_auto_design_chain( logger.exception("초기값 스냅샷 실패: project_id=%s", project_id) mark_design_failed(project_root, f"초기값 스냅샷 저장 실패: {exc}") + # 노선 추천 기본 다섯 알약 미리 셈(PLAN 327-2) — 예약만 · 체인이 [초기화] 요청 안에서도 + # 불리므로 기다리지 않는다. 같은 입력이면 저장본이 있어 셈 없이 끝난다. + try: + from B03_FileInput.B03_FileInput_Router_Helpers import _schedule_background_task + from B05_Profile.B05_Profile_Router_RouteInitial import precompute_defaults + + _schedule_background_task( + precompute_defaults(project_id), task_name=f"추천 노선 미리 셈 {project_id}" + ) + except Exception: # noqa: BLE001 — 미리 셈이 체인을 막지는 않는다 + logger.exception("추천 노선 미리 셈 예약 실패: project_id=%s", project_id) + return { "route_id": route_id, "length_m": float(solve_result.total_length_m or 0.0), diff --git a/B05_Profile/B05_Profile_UI_RouteEdit_Initial_Model.ts b/B05_Profile/B05_Profile_UI_RouteEdit_Initial_Model.ts index 6e63790ff..7199fa9b8 100644 --- a/B05_Profile/B05_Profile_UI_RouteEdit_Initial_Model.ts +++ b/B05_Profile/B05_Profile_UI_RouteEdit_Initial_Model.ts @@ -189,6 +189,8 @@ export interface InitialChoice { similar?: SimilarRoute[] | null; /** 제약 안에서 갈 수 있는 길의 띠 폭 어림(m) — 서버 `band_width_m`. */ bandWidthM?: number | null; + /** 서버 추천 저장본에서 꺼낸 결과인가(계획서 327) — 서버 `stored`. */ + stored?: boolean; /** 균형 종단(계획서 63-4) — 모달 미리 보기에만. 캐시에는 안 적는다(`writeInitialChoice`). */ balance?: BalanceProfile | null; } @@ -336,6 +338,7 @@ export function normalizeRouteInitial( terrainBoundPct: body.relaxed === true ? num(body.terrain_bound_pct) : null, similar: parseSimilar(body.similar), bandWidthM: num(body.band_width_m), + stored: body.stored === true, balance: parseBalance(m.profile), criteria: body.criteria ? { diff --git a/B05_Profile/B05_Profile_UI_RouteEdit_Recommend.ts b/B05_Profile/B05_Profile_UI_RouteEdit_Recommend.ts index 6d8a4f854..d42d94aef 100644 --- a/B05_Profile/B05_Profile_UI_RouteEdit_Recommend.ts +++ b/B05_Profile/B05_Profile_UI_RouteEdit_Recommend.ts @@ -462,7 +462,9 @@ export function openRecommendModal(params: RecommendParams): void { preview = choice; if (choice.balance) seen.set(label, choice.balance); else seen.delete(label); // 종단 없는 갈래(예상노선 추적) - stateEl.textContent = `미리 보기: ${choiceLabel(logic, sentWeights)} — [적용]을 눌러야 편집 창에 놓입니다.`; + stateEl.textContent = `미리 보기: ${choiceLabel(logic, sentWeights)}${ + choice.stored ? " (저장된 결과)" : "" + } — [적용]을 눌러야 편집 창에 놓입니다.`; } catch (error) { if (closed || mine !== seq) return; stateEl.textContent = "셈하지 못했습니다."; From 27c62ce22094358319bdf3e89e8e780af85430d4 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 15 Oct 2026 12:37:18 +0900 Subject: [PATCH 27/81] =?UTF-8?q?feat(=EC=9D=BC=EC=9C=84=EB=8C=80=EA=B0=80?= =?UTF-8?q?=EC=8B=A4=EB=AC=B4):=20329-1=20=C2=B7=20330-1=20=C2=B7=20330-2?= =?UTF-8?q?=20=EC=84=9C=EB=B2=84=20=E2=80=94=20=EC=97=91=EC=85=80=EA=BC=B4?= =?UTF-8?q?=20=EA=B2=A9=EC=9E=90=20=C2=B7=20=ED=96=89=20=EB=B2=94=EC=9C=84?= =?UTF-8?q?=20=C2=B7=20=EB=8B=A8=EA=B0=80=EC=82=B0=EC=B6=9C=20=EA=B7=BC?= =?UTF-8?q?=EA=B1=B0=20=EB=A7=9E=EC=B6=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 329-1 새 M02_Hopyo_Grid(xls formatting_info 값 · 병합 · 열 폭 · 행 높이 · 서식표 · 틀고정 / xlsx 값만) · read_book 호표 · 줄 「행」 · Store 격자 저장 · 읽기 · 지움(실무자료/격자/<키>.json) · GET hopyo-grid/{key}(옛 파일 404) · Match 요약 .get - 330-1 새 M02_Hopyo_Basis.read — 단산N 블록 이름 · 단위 · 행 · 출처(책 · 절) · 직종(우리 인력 이름 · 옛이름) · 금액 · 단산 줄 「근거」 - 330-2 Basis.ours · candidates(조항 1.0 · 앞 마디 0.5 · 건설 공통 · 토목 0.1 · 직종 0.5 · 단위 0.15 · 까닭) · match_line 이름 후보와 로직 키로 합침 · 상태 후보 그대로 - 시험 test_m02_hopyo_grid · test_m02_hopyo_basis 새로 · test_m02_hopyo_match 제3호표 구조물터파기 1순위 → 굴착(인력/토사) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Ji4nuyLVHcxrXWHTdb5nRD --- M02_MasterTemplete/M02_Hopyo_Basis.py | 231 +++++++++++++++++++++++ M02_MasterTemplete/M02_Hopyo_Excel.py | 26 ++- M02_MasterTemplete/M02_Hopyo_Grid.py | 211 +++++++++++++++++++++ M02_MasterTemplete/M02_Hopyo_Match.py | 19 +- M02_MasterTemplete/M02_Hopyo_Store.py | 30 ++- M02_MasterTemplete/M02_Labor_Router.py | 19 +- resources/tester/test_m02_hopyo_basis.py | 108 +++++++++++ resources/tester/test_m02_hopyo_grid.py | 120 ++++++++++++ resources/tester/test_m02_hopyo_match.py | 2 +- 9 files changed, 748 insertions(+), 18 deletions(-) create mode 100644 M02_MasterTemplete/M02_Hopyo_Basis.py create mode 100644 M02_MasterTemplete/M02_Hopyo_Grid.py create mode 100644 resources/tester/test_m02_hopyo_basis.py create mode 100644 resources/tester/test_m02_hopyo_grid.py diff --git a/M02_MasterTemplete/M02_Hopyo_Basis.py b/M02_MasterTemplete/M02_Hopyo_Basis.py new file mode 100644 index 000000000..e4fcf4458 --- /dev/null +++ b/M02_MasterTemplete/M02_Hopyo_Basis.py @@ -0,0 +1,231 @@ +"""M02 일위대가 실무자료 단가산출 근거(PLAN 330) — 엑셀 「단가산출」 블록 → 출처 · 직종 · +우리 돈 로직과 견준 근거 후보. + +읽기(`read`) = B열(머리 「산출근거」) 머리 줄 「N.이름[단위]」(앞 빈칸 없음)부터 +다음 머리 앞까지 한 블록 · +출처 = 「…품셈」(오타 「품줌」) 뒤 20 글자 안 절 번호 — 앞 글에 산림 · 임도 = 산림품셈 · +그 밖 = 건설품셈 · +직종 = 「이름 : 수」 줄의 빈칸 뺀 이름이 우리 인력(LB) 이름 · 옛이름일 때만(지금 이름으로). +견줌(`candidates`) = 같은 책 조항 같음 1.0 · 앞 마디 같음 0.5(조항이 안 맞으면 후보 아님) · +건설 공통 · 토목 덤 0.1 · 직종 겹침(보통인부 뺌) 0.5 · 단위 0.15 · 직종도 뜻 낱말도 안 겹치면 버림 · +같은 점수는 글자 두 개 묶음(괄호 글 포함)이 가름. 자동 확정 없음 — 맞춤 차례는 `M02_Hopyo_Match`. +M01 · 원본 로직은 읽기만. +""" + +from __future__ import annotations + +import re +from functools import lru_cache +from typing import Any + +from M01_MasterData import M01_MasterData_Store as m01 +from M02_MasterTemplete import M02_Hopyo_Match as hopyo_match # 서로 부름 — 부를 때만 씀 +from M02_MasterTemplete import M02_Labor_Inputs as labor_inputs + +SHEET = "단가산출" +COMMON = "보통인부" +BONUS_PARTS = ("공통", "토목") +_HEAD = re.compile(r"^(\d+)\.\s*(.*?)\s*\[([^\[\]]*)\]\s*$") +_BOOK = re.compile(r"품[셈줌]") +_TRADE = re.compile(r"^[\s*·\-]*([가-힣][가-힣\s]*?)\s*:\s*[\d,]") +_TYPOS = (("깍기", "깎기"),) +_MONEY = {"합계": "계", "노무비": "노무비", "재료비": "재료비", "경비": "경비"} + + +def _flat(value: Any) -> str: + return re.sub(r"\s+", "", str(value or "")) + + +# ── 엑셀 블록 읽기 ───────────────────────────────────────────────────── +@lru_cache(maxsize=2) +def _trade_names(whole_id: int) -> dict[str, str]: + """{빈칸 뺀 이름 · 옛이름: 지금 이름} — 우리 인력(LB).""" + out: dict[str, str] = {} + for row in m01.loaded()[1].numbers.get("LB", {}).values(): + name = str(row.get("이름") or "") + out[_flat(name)] = name + for old in row.get("옛이름") or []: + out.setdefault(_flat(old), name) + return out + + +def trade_names() -> dict[str, str]: + return _trade_names(id(m01.loaded()[1])) + + +def sources(text: str) -> list[list[str]]: + """글 → [[책, 절 번호]] — 「건설공사표준품셈 3-2-1」 · 「산림사업표준품셈(9-14)」.""" + out = [] + for hit in _BOOK.finditer(text): + num = labor_inputs._NUMBER.search(text, hit.end()) + if not num or num.start() - hit.end() > 20: + continue + before = text[max(0, hit.start() - 12) : hit.start()] + book = "산림품셈" if "산림" in before or "임도" in before else "건설품셈" + if [book, num.group()] not in out: + out.append([book, num.group()]) + return out + + +def _columns(rows: list[list[Any]]) -> tuple[int, int, dict[str, int]]: + """(자료 첫 줄, 산출근거 열, {계 · 노무비 · 재료비 · 경비: 열}) — 머리 없으면 (0, 1, {}).""" + for at, row in enumerate(rows[:10]): + flat = [_flat(v) for v in row] + if "산출근거" in flat: + money = {key: flat.index(head) for head, key in _MONEY.items() if head in flat} + return at + 1, flat.index("산출근거"), money + return 0, 1, {} + + +def read(rows: list[list[Any]] | None) -> dict[int, dict[str, Any]]: + """단가산출 시트 줄 → `{N: {번, 이름, 단위, 행: [시작, 끝], 출처, 직종, 금액}}` + (행 = 엑셀 1부터).""" + if not rows: + return {} + start, col, money = _columns(rows) + names = trade_names() + blocks: dict[int, dict[str, Any]] = {} + block: dict[str, Any] | None = None + for at, row in enumerate(rows[start:], start + 1): + text = row[col] if col < len(row) else None + if not isinstance(text, str) or not text.strip(): + continue + head = None if text[:1].isspace() else _HEAD.match(text) + if head: + block = blocks.setdefault( + int(head.group(1)), + { + "번": int(head.group(1)), + "이름": head.group(2), + "단위": head.group(3).strip(), + "행": [at, at], + "출처": [], + "직종": [], + "금액": { + key: v + for key, c in money.items() + for v in [row[c] if c < len(row) else None] + if isinstance(v, (int, float)) and not isinstance(v, bool) + }, + }, + ) + continue + if block is None: + continue + block["행"][1] = at + block["출처"] += [one for one in sources(text) if one not in block["출처"]] + trade = _TRADE.match(text) + name = names.get(_flat(trade.group(1))) if trade else None + if name and name not in block["직종"]: + block["직종"].append(name) + return blocks + + +# ── 우리 쪽 조항 · 직종 ──────────────────────────────────────────────── +@lru_cache(maxsize=2) +def _ours(whole_id: int) -> dict[str, dict[str, Any]]: + """{로직 키: {책, 부문, 구분, 조항: {절 번호}, 직종: {이름}}} — 조항 = `sources()` 묶음 · + 직종 = 호표 인력(LB) 요소.""" + files, whole = m01.loaded() + lb = {str(row.get("키")): str(row.get("이름")) for row in whole.numbers.get("LB", {}).values()} + out = {} + for data in files.values(): + if data.get("그룹") != "로직": + continue + for row in data.get("줄") or []: + kind = str(row.get("구분") or "") + book, _, part = kind.partition(" ") + trades = { + lb[str(h.get("요소"))] + for h in row.get("호표") or [] + if isinstance(h, dict) and str(h.get("요소")) in lb + } + clauses = set(labor_inputs._NUMBER.findall(str(labor_inputs.sources(row) or ""))) + out[str(row.get("키"))] = { + "책": book, + "부문": part, + "구분": kind, + "조항": clauses, + "직종": trades, + } + return out + + +def ours() -> dict[str, dict[str, Any]]: + return _ours(id(m01.loaded()[1])) + + +def _head(number: str) -> str: + return number.rsplit("-", 1)[0] + + +def _clause(theirs: list[list[str]], our: dict[str, Any]) -> tuple[float, list[str], str]: + """(점수, 맞은 우리 조항 글, 꼬리표) — 같은 책만.""" + nums = [n for book, n in theirs if book == our["책"]] + same = sorted(n for n in nums if n in our["조항"]) + if same: + return 1.0, [f"{our['구분']} {n}" for n in same], "조항 같음" + heads = {_head(n) for n in nums} + near = sorted(n for n in our["조항"] if _head(n) in heads) + if near: + return 0.5, [f"{our['구분']} {n}" for n in near], "앞 마디 같음" + return 0.0, [], "" + + +def _text(line: dict[str, Any]) -> str: + text = f"{line.get('이름') or ''} {line.get('규격') or ''}" + for old, new in _TYPOS: + text = text.replace(old, new) + return text + + +def candidates(line: dict[str, Any], got: dict[str, Any]) -> list[dict[str, Any]]: + """근거 후보 — 줄 `근거.출처` 가 없으면 빈 것 · `{로직, 이름, 단위, 점수, 근거}` 점수 차례.""" + basis = line.get("근거") or {} + theirs = [list(one) for one in basis.get("출처") or []] + if not theirs: + return [] + trades = set(basis.get("직종") or []) - {COMMON} + text = _text(line) + mine = hopyo_match.words(text) # 괄호 글 뺌 — 「잔토처리(인력)」 ↔ 「철골재 철거(인력)」 + grams = hopyo_match._grams(hopyo_match.mm.plain(text)) # 괄호 글 포함 — 같은 점수 가름 + table = ours() + scored = [] + for one in got["로직"]: + our = table.get(one["로직"]) + if not our: + continue + score, clauses, tag = _clause(theirs, our) + if not score: + continue + shared = sorted(trades & our["직종"]) + if not shared and not any(a in b or b in a for a in mine for b in one["words"]): + continue + tags = [tag] + if our["책"] == "건설품셈" and our["부문"] in BONUS_PARTS: + score += 0.1 + tags.append(f"건설 {our['부문']}") + if shared: + score += 0.5 + tags.append("직종 " + " · ".join(shared)) + unit = hopyo_match.fits(line.get("단위"), one["단위"]) + if unit: + score += 0.15 + tags.append("단위 맞음") + dice = 2 * len(grams & one["grams"]) / (len(grams) + len(one["grams"])) + why = {"조항": clauses, "직종": shared, "단위": unit, "점수": round(score, 3)} + scored.append( + (score, dice, {**hopyo_match._public(one), "근거": {**why, "까닭": " · ".join(tags)}}) + ) + scored.sort(key=lambda x: (-x[0], -x[1])) + return [one for _, _, one in scored] + + +def merge(basis: list[dict[str, Any]], named: list[dict[str, Any]]) -> list[dict[str, Any]]: + """근거 후보 + 이름 후보 → 로직 키로 합침(점수 = 이름 점수 + 근거 점수) · 점수 차례.""" + if not basis: + return named + by_name = {c.get("로직"): c["점수"] for c in named} + out = [{**c, "점수": round(c["근거"]["점수"] + by_name.pop(c["로직"], 0), 3)} for c in basis] + out += [c for c in named if c.get("로직") in by_name] + return sorted(out, key=lambda c: -c["점수"]) diff --git a/M02_MasterTemplete/M02_Hopyo_Excel.py b/M02_MasterTemplete/M02_Hopyo_Excel.py index 38d11a81e..9995de2b4 100644 --- a/M02_MasterTemplete/M02_Hopyo_Excel.py +++ b/M02_MasterTemplete/M02_Hopyo_Excel.py @@ -11,6 +11,9 @@ 검산 = 줄 금액 floor(단가 × 수량 × 10) / 10 · 합계 단가 = 셋 합 · % 줄 = floor(바탕 × 율 / 100 × 10) / 10 · 호표 비목 = floor(줄 금액 합) · 총괄표 합계 — 어긋남은 글 목록(막지 않음). +행(329-1) = 호표 [제N호표 줄, 마지막 구성 줄] · 구성 줄 한 줄(엑셀 1부터) · +근거(330-1) = 단산N참조 줄에 「단가산출」 블록 `{출처, 직종, 행}`(`M02_Hopyo_Basis.read`) · +`단산` = 블록마다 `{번, 이름, 단위, 행}`. """ from __future__ import annotations @@ -20,6 +23,8 @@ import re from decimal import ROUND_FLOOR, Decimal, InvalidOperation from typing import Any +from M02_MasterTemplete import M02_Hopyo_Basis as hopyo_basis + SHEET = "일위대가" SUMMARY = "일위대가총괄표" COSTS = ("노무비", "재료비", "경비") @@ -67,8 +72,8 @@ def _out(value: Decimal | None) -> int | float | None: # ── 파일 → 시트 표(줄마다 값 목록) ──────────────────────────────────────── def _sheets(name: str, data: bytes) -> dict[str, list[list[Any]]]: - """시트 이름 → 줄 목록 — 일위대가 · 총괄표만 읽음.""" - wanted = (SHEET, SUMMARY) + """시트 이름 → 줄 목록 — 일위대가 · 총괄표 · 단가산출만 읽음.""" + wanted = (SHEET, SUMMARY, hopyo_basis.SHEET) if not data: raise ExcelError("빈 파일") try: @@ -212,7 +217,7 @@ def read_book(name: str, data: bytes) -> dict[str, Any]: start, cols = _columns(rows, ("합계", *COSTS)) hopyos: list[dict[str, Any]] = [] raw: list[list[dict[str, Any]]] = [] - for row in rows[start:]: + for at, row in enumerate(rows[start:], start + 1): head = _flat(_cell(row, cols.get("호표"))) name_ = _text(_cell(row, cols.get("품명"))) hit = _HOPYO.fullmatch(head) @@ -224,6 +229,7 @@ def read_book(name: str, data: bytes) -> dict[str, Any]: "규격": _text(_cell(row, cols.get("규격"))), "단위": _text(_cell(row, cols.get("단위"))), "금액": _money(row, cols, 1), + "행": [at, at], } ) raw.append([]) @@ -244,9 +250,15 @@ def read_book(name: str, data: bytes) -> dict[str, Any]: "참조": {"종류": ref.group(1), "번": int(ref.group(2))} if ref else None, } line["갈래"] = kind_of(line) + line["행"] = hopyos[-1]["행"][1] = at raw[-1].append(line) if not hopyos: raise ExcelError(f"{SHEET} 시트에 호표(제N호표)가 없음") + blocks = hopyo_basis.read(sheets.get(hopyo_basis.SHEET)) + for line in (one for lines in raw for one in lines): + ref = line["참조"] or {} + if ref.get("종류") == "단산" and (block := blocks.get(ref["번"])): + line["근거"] = {k: block[k] for k in ("출처", "직종", "행")} wrong: list[str] = [] totals = _summary(sheets.get(SUMMARY)) for hopyo, lines in zip(hopyos, raw, strict=True): @@ -258,7 +270,13 @@ def read_book(name: str, data: bytes) -> dict[str, Any]: {**_json(hopyo), "줄": [_json(line) for line in lines]} for hopyo, lines in zip(hopyos, raw, strict=True) ] - return {"시트": SHEET, "호표": out, "검산": {"어긋남": wrong, "총괄표": len(totals)}} + basis = [{k: b[k] for k in ("번", "이름", "단위", "행")} for b in blocks.values()] + return { + "시트": SHEET, + "호표": out, + "단산": basis, + "검산": {"어긋남": wrong, "총괄표": len(totals)}, + } def _json(value: Any) -> Any: diff --git a/M02_MasterTemplete/M02_Hopyo_Grid.py b/M02_MasterTemplete/M02_Hopyo_Grid.py new file mode 100644 index 000000000..39af897fa --- /dev/null +++ b/M02_MasterTemplete/M02_Hopyo_Grid.py @@ -0,0 +1,211 @@ +"""M02 일위대가 실무자료 엑셀꼴 격자(PLAN 329-1) — 올린 엑셀 「일위대가」 · 「단가산출」 시트 → +A00 산출근거 스프레드시트 Workbook(`A00_Common/spreadsheet/spreadsheet_types.ts` 꼴 · +읽기 전용 보기). + +xls = xlrd `formatting_info` — 값 · 병합 · 열 폭(÷256 글자) · +행 높이(÷20 pt · 시트 기본과 다른 것만) · 서식 표(글꼴 · 굵게 · 맞춤 · 줄바꿈 · 테두리 · 채움 · +숫자 형식) · 틀고정(파일 값 · 없으면 머리 4행). +xlsx = 값만 · 넓은 시트는 머리 글(앞 4 줄) 마지막 열까지. +결과 `{책, 호표: [{번, 이름, 행}], 단산: [{번, 이름, 행}]}` — 행 = 엑셀 1부터 [시작, 끝]. +""" + +from __future__ import annotations + +import io +from typing import Any + +from openpyxl.utils import get_column_letter + +from M02_MasterTemplete import M02_Hopyo_Basis as hopyo_basis +from M02_MasterTemplete import M02_Hopyo_Excel as hopyo_excel + +SHEETS = (hopyo_excel.SHEET, hopyo_basis.SHEET) +HEAD_ROWS = 4 +_HALIGN = {1: "left", 2: "center", 3: "right", 6: "centerContinuous", 7: "distributed"} +_VALIGN = {0: "top", 1: "center"} +_LINES = {1: "thin", 2: "medium", 3: "dashed", 4: "dotted", 5: "thick", 6: "double", 7: "hair"} +_SIDES = (("위", "top"), ("아래", "bottom"), ("왼", "left"), ("오른", "right")) + + +class _Styles: + """서식 표 — 같은 서식은 한 번만 · [0] = 기본 `{}`.""" + + def __init__(self) -> None: + self.table: list[dict[str, Any]] = [{}] + self._index: dict[str, int] = {"{}": 0} + + def add(self, style: dict[str, Any]) -> int: + key = repr(sorted(style.items())) + if not style: + return 0 + if key not in self._index: + self._index[key] = len(self.table) + self.table.append(style) + return self._index[key] + + +def _rgb(book: Any, index: int) -> str | None: + got = book.colour_map.get(index) + return "#{:02X}{:02X}{:02X}".format(*got) if got else None + + +def _xf_style(book: Any, xf: Any, base: Any) -> dict[str, Any]: + font = book.font_list[xf.font_index] + out: dict[str, Any] = {} + if font.name != base.name: + out["글꼴"] = font.name + if font.height != base.height: + out["크기"] = font.height / 20 + for key, on in ( + ("굵게", font.bold), + ("기울임", font.italic), + ("밑줄", font.underline_type), + ("취소선", font.struck_out), + ): + if on: + out[key] = True + color = _rgb(book, font.colour_index) + if color and color != "#000000": + out["글자색"] = color + if xf.background.fill_pattern == 1 and (fill := _rgb(book, xf.background.pattern_colour_index)): + out["채움"] = fill + align = xf.alignment + if align.hor_align in _HALIGN: + out["가로"] = _HALIGN[align.hor_align] + if align.vert_align in _VALIGN: + out["세로"] = _VALIGN[align.vert_align] + if align.text_wrapped: + out["줄바꿈"] = True + sides = {} + for name, side in _SIDES: + line = getattr(xf.border, f"{side}_line_style") + if line: + one = {"선": _LINES.get(line, "dashed")} + color = _rgb(book, getattr(xf.border, f"{side}_colour_index")) + if color and color != "#000000": + one["색"] = color + sides[name] = one + if sides: + out["테두리"] = sides + fmt = book.format_map.get(xf.format_key) + if fmt and fmt.format_str and fmt.format_str != "General": + out["형식"] = fmt.format_str + return out + + +def _value(cell: Any) -> Any: + import xlrd + + if cell.ctype == xlrd.XL_CELL_TEXT: + return cell.value or None + if cell.ctype in (xlrd.XL_CELL_NUMBER, xlrd.XL_CELL_DATE): + return int(cell.value) if cell.value == int(cell.value) else cell.value + if cell.ctype == xlrd.XL_CELL_BOOLEAN: + return bool(cell.value) + if cell.ctype == xlrd.XL_CELL_ERROR: + return xlrd.error_text_from_code.get(cell.value, "#N/A") + return None + + +def _xls_sheet(book: Any, sh: Any, styles: _Styles, sid: str) -> dict[str, Any]: + base = book.font_list[0] + by_xf: dict[int, int] = {} + cells: dict[str, dict[str, Any]] = {} + for r in range(sh.nrows): + for c in range(sh.ncols): + cell = sh.cell(r, c) + if cell.xf_index not in by_xf: + by_xf[cell.xf_index] = styles.add( + _xf_style(book, book.xf_list[cell.xf_index], base) + ) + one: dict[str, Any] = {} + if (value := _value(cell)) is not None: + one["값"] = value + if by_xf[cell.xf_index]: + one["서식"] = by_xf[cell.xf_index] + if one: + cells[f"{get_column_letter(c + 1)}{r + 1}"] = one + merged = [ + f"{get_column_letter(c0 + 1)}{r0 + 1}:{get_column_letter(c1)}{r1}" + for r0, r1, c0, c1 in sh.merged_cells + ] + cols = { + get_column_letter(c + 1): { + "폭": round(info.width / 256, 2), + **({"숨김": True} if info.hidden else {}), + } + for c, info in sh.colinfo_map.items() + if c < sh.ncols + } + rows = { + str(r + 1): {"높이": info.height / 20, **({"숨김": True} if info.hidden else {})} + for r, info in sh.rowinfo_map.items() + if r < sh.nrows and (info.height != sh.default_row_height or info.hidden) + } + frozen = sh.panes_are_frozen and (sh.horz_split_pos or sh.vert_split_pos) + freeze = {"행": sh.horz_split_pos, "열": sh.vert_split_pos} if frozen else {"행": HEAD_ROWS} + out = { + "id": sid, + "이름": sh.name.strip(), + "칸": cells, + "병합": merged, + "열": cols, + "행": rows, + "틀고정": {k: v for k, v in freeze.items() if v}, + } + if sh.default_row_height: + out["기본"] = {"행높이": sh.default_row_height / 20} + return out + + +def _xls(data: bytes) -> dict[str, Any]: + import xlrd + + book = xlrd.open_workbook(file_contents=data, formatting_info=True, logfile=io.StringIO()) + styles = _Styles() + found = {sh.name.strip(): sh for sh in book.sheets()} + sheets = [ + _xls_sheet(book, found[name], styles, f"s{at}") + for at, name in enumerate((n for n in SHEETS if n in found), 1) + ] + font = book.font_list[0] + return { + "기본": {"글꼴": font.name, "크기": font.height / 20}, + "서식": styles.table, + "시트": sheets, + } + + +def _xlsx(name: str, data: bytes) -> dict[str, Any]: + # ponytail: xlsx 는 값만(병합 · 폭 · 서식은 openpyxl 비 read_only 로 무거움) — 필요하면 그때 + found = hopyo_excel._sheets(name, data) + sheets = [] + for at, title in enumerate((n for n in SHEETS if n in found), 1): + rows = found[title] + width = max( + (c + 1 for row in rows[:HEAD_ROWS] for c, v in enumerate(row) if v not in (None, "")), + default=0, + ) + cells = { + f"{get_column_letter(c + 1)}{r + 1}": { + "값": v if isinstance(v, (int, float, bool)) else str(v) + } + for r, row in enumerate(rows) + for c, v in enumerate(row[:width]) + if v not in (None, "") + } + sheets.append({"id": f"s{at}", "이름": title, "칸": cells, "틀고정": {"행": HEAD_ROWS}}) + return {"서식": [{}], "시트": sheets} + + +def read(name: str, data: bytes, parsed: dict[str, Any]) -> dict[str, Any]: + """엑셀 → `{책: Workbook, 호표, 단산}` — `parsed` = `M02_Hopyo_Excel.read_book` 결과 + (행 범위).""" + xlsx = data[:4] == b"PK\x03\x04" or name.lower().endswith(".xlsx") + book = _xlsx(name, data) if xlsx else _xls(data) + keep = ("번", "이름", "행") + return { + "책": {"종류": "통합문서", "판": 1, "열": "", **book, "활성": "s1"}, + "호표": [{k: h.get(k) for k in keep} for h in parsed.get("호표") or []], + "단산": [{k: b.get(k) for k in keep} for b in parsed.get("단산") or []], + } diff --git a/M02_MasterTemplete/M02_Hopyo_Match.py b/M02_MasterTemplete/M02_Hopyo_Match.py index dc894c8a4..c9ccbc557 100644 --- a/M02_MasterTemplete/M02_Hopyo_Match.py +++ b/M02_MasterTemplete/M02_Hopyo_Match.py @@ -4,6 +4,8 @@ 씨앗(일위대가조합.json 메모 · 읽기만) ③ 정리 이름 같음(괄호 · 기계 낱말) ④ 후보 5(글자 두 개 묶음 점수 + 단위 맞음 + 산림품셈 덤 · 뜻 낱말이 하나도 안 겹치면 뺌 — 「잔토처리」 ↔ 「소금처리」) — ++ 근거 후보(330-2 · `M02_Hopyo_Basis` · 단산 줄 출처 · 직종 · 이름 거름 안 거침 · +로직 키로 합쳐 점수 더함) · ③ ④ 는 사용자 확정(층 기억) 뒤에만 들어감. 줄 갈래: 단가산출 · 기계 · 기타 = 로직 / 자재 · 별도계상 = 자재(M01 자재품목) / 호표 = 그 호표 / % = 안 들어감(로직 덧줄이 셈). @@ -27,6 +29,7 @@ from typing import Any from M01_MasterData import M01_MasterData_Store as m01 from M01_MasterData import M01_MasterData_Store_Combo as combo_store +from M02_MasterTemplete import M02_Hopyo_Basis as hopyo_basis from M02_MasterTemplete import M02_Hopyo_Store as hopyo_store from M02_MasterTemplete import M02_Labor_Inputs as labor_inputs from M02_MasterTemplete import M02_Labor_Ref as refs @@ -295,9 +298,10 @@ def _exact(line: dict[str, Any], pool: str, got: dict[str, Any]) -> list[dict[st def candidates( - line: dict[str, Any], pool: str, got: dict[str, Any] | None = None, limit: int = 5 + line: dict[str, Any], pool: str, got: dict[str, Any] | None = None, limit: int | None = 5 ) -> list[dict[str, Any]]: - """③ 정리 이름 같음 먼저 · ④ 글자 두 개 묶음 점수 + 단위 맞음 + 산림품셈 덤 — 위 `limit`.""" + """③ 정리 이름 같음 먼저 · ④ 글자 두 개 묶음 점수 + 단위 맞음 + 산림품셈 덤 — 위 `limit` + (None = 모두).""" me = tidy(line.get("이름")) grams, unit = _grams(me), line.get("단위") text = mm.plain_spec(f"{line.get('이름')}{line.get('규격')}") @@ -402,11 +406,9 @@ def match_line( return out top = [{**_public(x), "점수": 9} for x in found] same = [_public(x) for x in found] - rest = [ - c - for c in candidates(line, pool, got) - if {k: v for k, v in c.items() if k != "점수"} not in same - ] + basis = hopyo_basis.candidates(line, got) if pool == "로직" else [] + named = candidates(line, pool, got, None if basis else 5) + rest = [c for c in hopyo_basis.merge(basis, named) if _public(c) not in same] out["후보"] = (top + rest)[:5] return {**out, "상태": "후보" if out["후보"] else "없음"} @@ -461,7 +463,8 @@ def view(folder: Path, key: str, no: int | None = None) -> dict[str, Any]: if no is None: got = targets() hopyos = [match_hopyo(h, memo, units, False, got) for h in doc.get("호표") or []] - brief = [{k: h[k] for k in ("번", "이름", "규격", "단위", "금액", "맞춤")} for h in hopyos] + keep = ("번", "이름", "규격", "단위", "금액", "맞춤") + brief = [{k: h.get(k) for k in keep} for h in hopyos] ready = [sum(h["맞춤"][0] for h in hopyos), sum(h["맞춤"][1] for h in hopyos)] return {**hopyo_store.brief(doc), "검산": doc.get("검산"), "호표": brief, "맞춤": ready} hopyo = next((h for h in doc.get("호표") or [] if int(h["번"]) == int(no)), None) diff --git a/M02_MasterTemplete/M02_Hopyo_Store.py b/M02_MasterTemplete/M02_Hopyo_Store.py index c632a409d..b82ad7b51 100644 --- a/M02_MasterTemplete/M02_Hopyo_Store.py +++ b/M02_MasterTemplete/M02_Hopyo_Store.py @@ -1,5 +1,5 @@ """M02 일위대가 실무자료 저장(PLAN 324-1) — 층 폴더 `실무자료/<키>.json` · -맞춤 기억 `실무자료/맞춤.json`. +맞춤 기억 `실무자료/맞춤.json` · 엑셀꼴 격자 `실무자료/격자/<키>.json`(329-1 · 파일과 같이 지움). 파일 = 읽은 결과만(원본 엑셀 안 둠) `{키, 파일, 올린때, 올린이, 시트, 호표, 검산}` · 키 `HP-######` (층마다 다음 번호) · 맞춤 기억 = `{줄: {줄 열쇠: {로직?|자재?|빼기?, 입력값, 환산, 확정}}}`. @@ -20,6 +20,8 @@ from M02_MasterTemplete import M02_MasterTemplete_Store as store DIRNAME = "실무자료" MEMORY = "맞춤" +GRID = "격자" +NO_GRID = "격자 없음 — 다시 올리면 보임" _KEY = re.compile(r"^HP-(\d{6})$") # ponytail: 프로세스 안 잠금 — 서버를 여럿 띄우면 파일 잠금으로 _LOCK = threading.Lock() @@ -42,6 +44,10 @@ def _load(path: Path) -> Any: return None +def _grid_path(folder: str | Path, key: str) -> Path: + return _dir(folder) / GRID / _path(folder, key).name + + def brief(doc: dict[str, Any]) -> dict[str, Any]: """목록 한 줄 — `{키, 파일, 올린때, 올린이, 시트, 호표수, 어긋남}`.""" keep = ("키", "파일", "올린때", "올린이", "시트") @@ -69,8 +75,14 @@ def read(folder: str | Path, key: str) -> dict[str, Any]: return doc -def save(folder: str | Path, name: str, parsed: dict[str, Any], who: Any) -> dict[str, Any]: - """읽은 결과 → 새 키로 씀 → 목록 한 줄.""" +def save( + folder: str | Path, + name: str, + parsed: dict[str, Any], + who: Any, + grid: dict[str, Any] | None = None, +) -> dict[str, Any]: + """읽은 결과(+ 격자) → 새 키로 씀 → 목록 한 줄.""" with _LOCK: have = [ int(m.group(1)) for p in _dir(folder).glob("HP-*.json") if (m := _KEY.match(p.stem)) @@ -83,16 +95,28 @@ def save(folder: str | Path, name: str, parsed: dict[str, Any], who: Any) -> dic "올린이": who, **parsed, } + if grid is not None: + book = {**grid["책"], "열": key} + atomic_write_json(_grid_path(folder, key), {**grid, "책": book}) atomic_write_json(_path(folder, key), doc) return brief(doc) +def grid(folder: str | Path, key: str) -> dict[str, Any]: + """엑셀꼴 격자 `{책, 호표, 단산}` — 없으면(옛 파일) 404.""" + doc = _load(_grid_path(folder, key)) + if not isinstance(doc, dict): + raise store.StoreError(404, NO_GRID) + return doc + + def delete(folder: str | Path, key: str) -> None: path = _path(folder, key) with _LOCK: if not path.is_file(): raise store.StoreError(404, f"없는 실무자료 「{key}」") path.unlink() + _grid_path(folder, key).unlink(missing_ok=True) # ── 맞춤 기억 ───────────────────────────────────────────────────────── diff --git a/M02_MasterTemplete/M02_Labor_Router.py b/M02_MasterTemplete/M02_Labor_Router.py index 1f028a4b1..55739aa3a 100644 --- a/M02_MasterTemplete/M02_Labor_Router.py +++ b/M02_MasterTemplete/M02_Labor_Router.py @@ -20,6 +20,7 @@ from pydantic import BaseModel from common_util.common_util_auth import verify_session from M02_MasterTemplete import M02_Hopyo_Build as hopyo_build from M02_MasterTemplete import M02_Hopyo_Excel as hopyo_excel +from M02_MasterTemplete import M02_Hopyo_Grid as hopyo_grid from M02_MasterTemplete import M02_Hopyo_Match as hopyo_match from M02_MasterTemplete import M02_Hopyo_Store as hopyo_store from M02_MasterTemplete import M02_Labor_Doc as labor @@ -176,7 +177,11 @@ def _upload(name: str, data: bytes, who: dict[str, Any]) -> dict[str, Any]: parsed = hopyo_excel.read_book(name, data) except hopyo_excel.ExcelError as error: raise store.StoreError(400, str(error)) from error - return hopyo_store.save(store.root(), name, parsed, who) + try: + grid = hopyo_grid.read(name, data, parsed) + except Exception: # 격자는 보기용 덤 — 못 만들어도 올리기는 됨(보기 창 404) + grid = None + return hopyo_store.save(store.root(), name, parsed, who, grid) @router.get("/layers/{layer}/hopyo") @@ -205,7 +210,7 @@ def hopyo_upload( owner: str | None = None, session: Session = Depends(verify_session), ) -> dict: - """엑셀(xls · xlsx) 올리기 — 「일위대가」 시트를 읽어 결과만 저장 · 20 MB · + """엑셀(xls · xlsx) 올리기 — 「일위대가」 시트를 읽어 결과 · 격자 저장 · 20 MB · 엉터리 · 시트 없음 400.""" folder = _root(session, layer, owner, write=True) data = file.file.read(HOPYO_MAX_BYTES + 1) @@ -225,6 +230,16 @@ def hopyo_read( return _call(folder, hopyo_match.view, folder, key) +@router.get("/layers/{layer}/hopyo-grid/{key}") +def hopyo_grid_read( + layer: Layer, key: str, owner: str | None = None, session: Session = Depends(verify_session) +) -> dict: + """엑셀꼴 격자(329-1) `{책: Workbook, 호표: [{번, 이름, 행}], 단산: [{번, 이름, 행}]}` · + 옛 파일 404.""" + folder = _root(session, layer, owner) + return _call(folder, hopyo_store.grid, folder, key) + + @router.get("/layers/{layer}/hopyo/{key}/{no}") def hopyo_one( layer: Layer, diff --git a/resources/tester/test_m02_hopyo_basis.py b/resources/tester/test_m02_hopyo_basis.py new file mode 100644 index 000000000..526bfd4cb --- /dev/null +++ b/resources/tester/test_m02_hopyo_basis.py @@ -0,0 +1,108 @@ +"""일위대가 실무자료 단가산출 근거 맞춤(PLAN 330 · `M02_Hopyo_Basis` · 맞춤 후보 `근거`). + +블록 북부청 95 · 사방 67 · 단산93 근거 [건설 3-2-1] · [특별인부] · +후보 93 → GC000083 · 21 → GC000198 · 64 → GC000143 위 둘 · 49 → GC000085 · +잔토처리에 철골재 철거 없음 · 근거 없는 줄 그대로 · 상태 「후보」(자동 확정 없음) · +master_data 그대로. +""" + +import pytest + +from M02_MasterTemplete import M02_Hopyo_Basis as hb +from M02_MasterTemplete import M02_Hopyo_Excel as hx +from M02_MasterTemplete import M02_Hopyo_Match as hm +from resources.tester import test_m02_hopyo_excel as base +from resources.tester.test_m02_hopyo_excel import ( + NORTH, + SABANG, + URL, + _post, + master_hash, +) + +client = base.client # 공용 고정물(임시 층) + + +@pytest.fixture(scope="module") +def north() -> dict: + return hx.read_book(NORTH.name, NORTH.read_bytes()) + + +def _line(book: dict, no: int) -> dict: + """단산 no 를 가리키는 첫 줄.""" + return next(x for h in book["호표"] for x in h["줄"] if x["참조"] == {"종류": "단산", "번": no}) + + +def test_블록_수_와_근거(north): + assert len(north["단산"]) == 95 + assert [b["번"] for b in north["단산"]] == list(range(1, 96)) + sabang = hx.read_book(SABANG.name, SABANG.read_bytes()) + assert len(sabang["단산"]) == 67 + assert _line(north, 93)["근거"] == { + "출처": [["건설품셈", "3-2-1"]], + "직종": ["특별인부"], + "행": [4229, 4244], + } + assert _line(north, 21)["근거"]["직종"] == ["콘크리트공", "보통인부"] # 「보 통 인부」 빈칸 뺌 + assert _line(north, 64)["근거"]["출처"][0] == ["건설품셈", "4-1-1"] + lines = [x for h in north["호표"] for x in h["줄"] if x["갈래"] == "단가산출"] + assert all("근거" in x for x in lines) # 268 줄 모두 블록 있음 + + +def test_출처_글(): + assert hb.sources("[건설공사표준품셈 3-2-1 인력굴착]") == [["건설품셈", "3-2-1"]] + assert hb.sources("건설표줌품셈(3-1-3) 자갈") == [["건설품셈", "3-1-3"]] + assert hb.sources("[2025년 산림사업표준품셈 9-14]") == [["산림품셈", "9-14"]] + assert hb.sources("흙운반 [임도표준품셈 4-2]") == [["산림품셈", "4-2"]] + assert hb.sources("[건설표준품셈 준용]") == [] + + +def _top(book: dict, no: int) -> list[dict]: + got = hm.match_line(_line(book, no), [], hm.units_of(book)) + assert got["상태"] == "후보" and got["들어감"] is False # 자동 확정 없음 + return got["후보"] + + +def test_근거_후보(north): + before = master_hash() + dig = _top(north, 93) + assert dig[0]["로직"] == "GC000083" + assert dig[0]["근거"] == { + "조항": ["건설품셈 공통 3-2-1"], + "직종": ["특별인부"], + "단위": True, + "점수": 1.75, + "까닭": "조항 같음 · 건설 공통 · 직종 특별인부 · 단위 맞음", + } + assert _top(north, 21)[0]["로직"] == "GC000198" + assert "GC000143" in [c["로직"] for c in _top(north, 64)[:2]] + assert _top(north, 49)[0]["로직"] == "GC000085" + soil = next(x for x in north["호표"][2]["줄"] if x["이름"].startswith("잔토처리")) + names = [c["이름"] for c in hm.match_line(soil, [], {})["후보"]] + assert not any("철골재" in n for n in names) + assert master_hash() == before + + +def test_근거_없는_줄_그대로(north): + bare = [ + x + for h in north["호표"] + for x in h["줄"] + if x["갈래"] == "단가산출" and not x["근거"]["출처"] + ] + assert bare + for line in bare: + plain = {k: v for k, v in line.items() if k != "근거"} + assert hm.match_line(line, [], {}) == hm.match_line(plain, [], {}) + + +def test_길_응답_근거_칸(client): + before = master_hash() + assert _post(client).status_code == 200 + res = client.get(f"{URL.format('personal')}/HP-000001/3") + lines = {x["이름"]: x for x in res.json()["호표"]["줄"]} + dig = lines["구조물터파기(인력100%)"] + assert dig["근거"]["출처"] == [["건설품셈", "3-2-1"]] + assert dig["맞춤"]["후보"][0]["근거"]["까닭"].startswith("조항 같음") + assert "근거" not in lines["임도표지석(받침,글새김포함)"] # 자재 줄 + assert master_hash() == before diff --git a/resources/tester/test_m02_hopyo_grid.py b/resources/tester/test_m02_hopyo_grid.py new file mode 100644 index 000000000..16ba2b303 --- /dev/null +++ b/resources/tester/test_m02_hopyo_grid.py @@ -0,0 +1,120 @@ +"""일위대가 실무자료 엑셀꼴 격자(PLAN 329-1 · `M02_Hopyo_Grid` · `GET hopyo-grid/{키}`). + +북부청 임도 xls — 병합 11 · 15열 · B20 「제3호표」 · `#,##0` · 호표 68 행 범위 안 겹침 · +제3호표 [20, 24] · 올리면 격자도 · 200 / 옛 파일 404 · 지우면 격자도 · 역할 · xlsx 값만 · +master_data 그대로. +""" + +import pytest + +from M02_MasterTemplete import M02_Hopyo_Excel as hx +from M02_MasterTemplete import M02_Hopyo_Grid as hg +from M02_MasterTemplete import M02_Hopyo_Store as hs +from M02_MasterTemplete import M02_Template_Layers as layers +from resources.tester import test_m02_hopyo_excel as base +from resources.tester.test_m02_hopyo_excel import ( + NORTH, + URL, + _post, + _xlsx, + master_hash, +) + +client = base.client # 공용 고정물(임시 층) +GRID = "/api/m02/layers/{}/hopyo-grid/{}" + + +@pytest.fixture(scope="module") +def north() -> dict: + data = NORTH.read_bytes() + return hg.read(NORTH.name, data, hx.read_book(NORTH.name, data)) + + +def _sheet(grid: dict, name: str) -> dict: + return next(s for s in grid["책"]["시트"] if s["이름"] == name) + + +def test_격자_모양(north): + book = north["책"] + assert (book["종류"], book["판"], book["활성"]) == ("통합문서", 1, "s1") + assert [s["이름"] for s in book["시트"]] == ["일위대가", "단가산출"] + sheet = _sheet(north, "일위대가") + assert len(sheet["병합"]) == 11 and "B1:O2" in sheet["병합"] + assert len(sheet["열"]) == 15 and sheet["열"]["C"]["폭"] == round(7208 / 256, 2) + assert sheet["틀고정"] == {"행": 4} + assert sheet["칸"]["B20"]["값"] == "제3호표" + assert sheet["칸"]["H5"]["값"] == 1040052 + styles = book["서식"] + assert styles[0] == {} and any(s.get("형식") == "#,##0" for s in styles) + assert any(s.get("테두리") for s in styles) + assert _sheet(north, "단가산출")["칸"]["B4"]["값"] == "1.유용토운반(무대)[㎥]" + + +def test_호표_단산_행_범위(north): + rows = [h["행"] for h in north["호표"]] + assert len(rows) == 68 + assert all(a[1] < b[0] for a, b in zip(sorted(rows), sorted(rows)[1:], strict=False)) + assert next(h for h in north["호표"] if h["번"] == 3) == { + "번": 3, + "이름": "임도표지석", + "행": [20, 24], + } + assert len(north["단산"]) == 95 + assert next(b for b in north["단산"] if b["번"] == 93)["행"] == [4229, 4244] + + +def test_줄_행(north): + book = hx.read_book(NORTH.name, NORTH.read_bytes()) + three = next(h for h in book["호표"] if h["번"] == 3) + assert three["행"] == [20, 24] and [x["행"] for x in three["줄"]] == [21, 22, 23, 24] + + +def test_올리기_격자_200_404_지우기(client): + before = master_hash() + assert _post(client).status_code == 200 + res = client.get(GRID.format("personal", "HP-000001")) + assert res.status_code == 200, res.text + got = res.json() + assert got["책"]["열"] == "HP-000001" and len(got["호표"]) == 68 + folder = layers.personal_dir(7, 42) + path = folder / hs.DIRNAME / hs.GRID / "HP-000001.json" + assert path.is_file() + assert [r["키"] for r in client.get(URL.format("personal")).json()["줄"]] == ["HP-000001"] + path.unlink() # 옛 파일(324) = 격자 없음 + old = client.get(GRID.format("personal", "HP-000001")) + assert old.status_code == 404 and old.json()["detail"] == "격자 없음 — 다시 올리면 보임" + assert _post(client).status_code == 200 + assert client.get(GRID.format("personal", "HP-000002")).status_code == 200 + assert client.delete(URL.format("personal") + "/HP-000002").status_code == 200 + assert not (folder / hs.DIRNAME / hs.GRID / "HP-000002.json").exists() + assert client.get(GRID.format("personal", "HP-000002")).status_code == 404 + assert client.get(GRID.format("personal", "../x")).status_code == 404 + assert client.get(URL.format("personal") + "/HP-000001/3").status_code == 200 # 길 안 부딪힘 + assert master_hash() == before + + +@pytest.mark.parametrize( + ("role", "layer", "owner", "want"), + [ + ({"role": "USER"}, "personal", None, 200), + ({"role": "USER"}, "personal", "7/43", 403), + ({"role": "USER"}, "company", None, 200), + ({"role": "USER"}, "system", None, 200), + ], +) +def test_역할_읽기(client, role, layer, owner, want): + client.who.update({"role": "SYSTEM_ADMIN"}) + assert _post(client, layer if want == 200 else "personal").status_code == 200 + client.who.update(role) + params = {"owner": owner} if owner else {} + assert client.get(GRID.format(layer, "HP-000001"), params=params).status_code == want + + +def test_xlsx_값만(): + data = _xlsx() + grid = hg.read("a.xlsx", data, hx.read_book("a.xlsx", data)) + sheet = grid["책"]["시트"][0] + assert sheet["이름"] == "일위대가" and sheet["틀고정"] == {"행": 4} + assert sheet["칸"]["B5"] == {"값": "제 1 호표"} and sheet["칸"]["E6"] == {"값": 0.5} + assert "K6" not in sheet["칸"] # 머리 글(앞 4 줄) 마지막 열 J 까지 + assert grid["호표"] == [{"번": 1, "이름": "표지석", "행": [5, 6]}] and grid["단산"] == [] diff --git a/resources/tester/test_m02_hopyo_match.py b/resources/tester/test_m02_hopyo_match.py index 0f1224240..69f8cd397 100644 --- a/resources/tester/test_m02_hopyo_match.py +++ b/resources/tester/test_m02_hopyo_match.py @@ -105,7 +105,7 @@ def test_이름_같음_후보_호표_퍼센트(client): assert all(c["규격"] == "25-21-120" for c in remicon["후보"][:3]) # 규격 같은 자재 먼저 dig = _hopyo(client, 3)["구조물터파기(인력100%)"] assert dig["상태"] == "후보" and len(dig["후보"]) == 5 - assert dig["후보"][0]["이름"].startswith("구조물터파기") + assert dig["후보"][0]["이름"] == "굴착(인력/토사)" # 330-2 근거(건설 3-2-1 · 특별인부) 1순위 ref = _hopyo(client, 45)["고화매트재료비"] assert ref["상태"] == "호표" and ref["대상"] == {"호표": 46, "단위": "㎡"} and ref["들어감"] pct = _hopyo(client, 46)["잡재료비"] From f044b8d0b47e91fe0c05cc62782a7728c82589dc Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 15 Oct 2026 12:38:41 +0900 Subject: [PATCH 28/81] =?UTF-8?q?feat(=EC=9D=BC=EC=9C=84=EB=8C=80=EA=B0=80?= =?UTF-8?q?=EC=8B=A4=EB=AC=B4):=20330-3=20=EA=B3=A0=EB=A5=B4=EA=B8=B0=20?= =?UTF-8?q?=EC=B0=BD=20=EA=B7=BC=EA=B1=B0=20=E2=80=94=20=ED=9B=84=EB=B3=B4?= =?UTF-8?q?=20=EA=B7=BC=EA=B1=B0=20=EA=BC=AC=EB=A6=AC=ED=91=9C(=EC=A1=B0?= =?UTF-8?q?=ED=95=AD=20=C2=B7=20=EC=A7=81=EC=A2=85=20=C2=B7=20=EB=8B=A8?= =?UTF-8?q?=EC=9C=84)=20=C2=B7=20=EC=97=91=EC=85=80=20=EA=B7=BC=EA=B1=B0?= =?UTF-8?q?=20=EC=A4=84(=EC=B6=9C=EC=B2=98=20=C2=B7=20=EC=A7=81=EC=A2=85?= =?UTF-8?q?=20=C2=B7=20=EB=8B=A8=EA=B0=80=EC=82=B0=EC=B6=9C=20=ED=96=89)?= =?UTF-8?q?=20+=20[=EC=97=91=EC=85=80=EC=97=90=EC=84=9C=20=EB=B3=B4?= =?UTF-8?q?=EA=B8=B0]=20=C2=B7=20[=EA=B7=BC=EA=B1=B0=20=EB=A7=9E=EC=9D=8C?= =?UTF-8?q?=20=EB=AA=A8=EB=91=90=20=ED=99=95=EC=A0=95](=EC=85=8B=20?= =?UTF-8?q?=EB=AA=A8=EB=91=90=20=EB=A7=9E=EB=8A=94=20=ED=9B=84=EB=B3=B4?= =?UTF-8?q?=EB=A7=8C)=20=C2=B7=20=EC=98=9B=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EB=8B=A8=EC=B6=94=20=EC=97=86=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_01XWh5wNNycMkbwMpACz1rte --- .../M02_MasterTemplete_Hopyo.css | 21 +++ .../M02_MasterTemplete_Hopyo_Api.ts | 20 +++ .../M02_MasterTemplete_Hopyo_View.ts | 120 ++++++++++++++++-- resources/tester/helper_m02_hopyo_pick.cjs | 28 ++++ resources/tester/test_m02_hopyo_screen.py | 21 +++ 5 files changed, 201 insertions(+), 9 deletions(-) diff --git a/M02_MasterTemplete/M02_MasterTemplete_Hopyo.css b/M02_MasterTemplete/M02_MasterTemplete_Hopyo.css index 134403225..8e771bad7 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Hopyo.css +++ b/M02_MasterTemplete/M02_MasterTemplete_Hopyo.css @@ -227,3 +227,24 @@ .m02-hopyo-sheet__result:empty { display: none; } + +/* 고르기 창 근거(330) — 후보 꼬리표 · 엑셀 근거 줄 */ +.m02-hopyo__basis { + margin-left: var(--spacing-8); + padding: 0 var(--spacing-4); + border: 1px solid var(--color-border); + border-radius: var(--radius-sm); + color: var(--color-text-secondary); + font-size: var(--text-caption); + white-space: nowrap; +} + +.m02-hopyo__basis.is-ok { + border-color: var(--color-success); + color: var(--color-success); +} + +.m02-hopyo__source { + padding-bottom: var(--spacing-8); + border-bottom: 1px solid var(--color-border); +} diff --git a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Api.ts b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Api.ts index c4ef47641..12312fba2 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Api.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Api.ts @@ -59,6 +59,22 @@ export interface HopyoView extends HopyoFile { 맞춤: [number, number]; } +/** 후보가 단가산출 근거와 맞는 것(330-2) — 맞은 조항 글 · 겹친 직종 · 단위 같음 · 점수 · 까닭 꼬리표 */ +export interface HopyoBasisHit { + 조항: string[]; + 직종: string[]; + 단위: boolean; + 점수: number; + 까닭: string; +} + +/** 단산 줄 근거(330-1) — 단가산출 블록의 출처([책, 번호]) · 직종 · 엑셀 행 [시작, 끝] */ +export interface HopyoLineBasis { + 출처: [string, string][]; + 직종: string[]; + 행: [number, number]; +} + /** 맞춤 대상 — 로직 · 자재 · 다른 호표 */ export interface HopyoTarget { 로직?: string; @@ -68,6 +84,8 @@ export interface HopyoTarget { 규격?: string; 단위?: string | null; 점수?: number; + /** 근거 맞춤(330 · 옛 파일 · 근거 없는 줄은 없음) */ + 근거?: HopyoBasisHit; } export interface HopyoMatch { @@ -92,6 +110,8 @@ export interface HopyoLine { 금액: Money; 비고: string; 맞춤: HopyoMatch; + /** 단가산출 근거(330 · 「단산N참조」 줄만) */ + 근거?: HopyoLineBasis; } export interface HopyoOne extends HopyoFile { diff --git a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_View.ts b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_View.ts index 3aa5fe2c8..63a5eef75 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_View.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_View.ts @@ -3,7 +3,8 @@ * 일위대가 실무자료 본 화면(PLAN 324-4) — 파일 하나: 파일 · 시트 · 호표 수 · 검산 · 맞춤 n/m · 호표 표 + 찾기 · * [지우기] / 호표 하나: 줄 표(갈래 · 품명 · 규격 · 수량 · 단위 · 엑셀 단가 · 맞춤 · 대상 · 입력값 · 환산 · * [고르기] [확정] [빼기]) · [같음 모두 확정] · 빠진 줄 · [사전조합 만들기] → 값 비교 · 다시 씀이면 [새로 만들기] · - * [엑셀로 보기] · [엑셀에서 보기](329 엑셀 보기 창 · 늦게 불러옴 · 호표 화면은 그 호표 고른 채). + * [엑셀로 보기] · [엑셀에서 보기](329 엑셀 보기 창 · 늦게 불러옴 · 호표 화면은 그 호표 고른 채) · + * 고르기 창 후보 근거 꼬리표 · 엑셀 근거 줄 + [엑셀에서 보기](단가산출 그 행) · [근거 맞음 모두 확정](330). * 맞춤 · 만들기는 서버(`M02_Hopyo_Match` · `M02_Hopyo_Build`) · 쓰기 단추는 층 쓰기 권한만. * ========================================================================== */ @@ -17,9 +18,11 @@ import { readHopyo, readHopyoOne, saveHopyoMemory, + type HopyoBasisHit, type HopyoBuilt, type HopyoGuess, type HopyoLine, + type HopyoLineBasis, type HopyoMemory, type HopyoOne, type HopyoScope, @@ -114,6 +117,42 @@ export function memoryOf( return null; } +/** 근거 맞음 — 조항 · 직종 · 단위 모두 맞음(330 [근거 맞음 모두 확정]) */ +export const basisOk = (b: HopyoBasisHit | undefined): boolean => + !!b && b.조항.length > 0 && b.직종.length > 0 && b.단위; + +/** 후보 근거 꼬리표 — 「조항 건설 공통 3-2-1 · 직종 특별인부 · 단위 같음」 · 근거 없으면 "" */ +export const basisTag = (b: HopyoBasisHit | undefined): string => + b + ? [ + b.조항.length ? `조항 ${b.조항.join(", ")}` : "", + b.직종.length ? `직종 ${b.직종.join(", ")}` : "", + b.단위 ? "단위 같음" : "", + ] + .filter(Boolean) + .join(" · ") + : ""; + +/** 엑셀 근거 줄 — 「출처 건설표준품셈 3-2-1 · 직종 특별인부 · 단가산출 93–101행」 · 없으면 "" */ +export const sourceText = (b: HopyoLineBasis | undefined): string => + b + ? [ + b.출처.length ? `출처 ${b.출처.map((x) => x.join(" ").trim()).join(", ")}` : "", + b.직종.length ? `직종 ${b.직종.join(", ")}` : "", + `단가산출 ${b.행[0] === b.행[1] ? b.행[0] : `${b.행[0]}–${b.행[1]}`}행`, + ] + .filter(Boolean) + .join(" · ") + : ""; + +/** [근거 맞음 모두 확정] 대상 — 「후보」 줄의 지금 대상이 근거 맞으면 그것 · 아니면 근거 맞는 첫 후보 · 없으면 null */ +export function basisPick(line: HopyoLine): HopyoTarget | null { + const m = line.맞춤; + if (m.상태 !== "후보") return null; + if (m.대상 && basisOk(m.대상.근거)) return m.대상; + return (m.후보 ?? []).find((c) => basisOk(c.근거)) ?? null; +} + export function buildHopyoView(key: string, opt: HopyoViewOptions): HTMLElement { const view = el("div", { className: "m02-hopyo", attrs: { "data-hopyo-view": key } }); let nav = 0; // 늦게 온 응답 버림 @@ -236,9 +275,19 @@ export function buildHopyoView(key: string, opt: HopyoViewOptions): HTMLElement const all = createButton({ label: `같음 모두 확정 (${same.length})`, size: "sm" }); all.disabled = !same.length; all.addEventListener("click", () => void confirmAll(no, same)); + tools.push(all); + // 근거(330) — 옛 파일 · 근거 없는 호표면 단추 없음 + if (h.줄.some((x) => x.근거 || x.맞춤.후보?.some((c) => c.근거))) { + const fit = h.줄.filter((x) => basisPick(x)); + const ok = createButton({ label: `근거 맞음 모두 확정 (${fit.length})`, size: "sm" }); + ok.title = "조항 · 직종 · 단위가 모두 맞는 후보만"; + ok.disabled = !fit.length; + ok.addEventListener("click", () => void confirmBasis(no, fit)); + tools.push(ok); + } const make = createButton({ label: "사전조합 만들기", variant: "filled", size: "sm" }); make.addEventListener("click", () => void build(no, false)); - tools.push(all, make); + tools.push(make); } const missing = h.줄.filter((x) => !x.맞춤.들어감); view.replaceChildren( @@ -330,7 +379,7 @@ export function buildHopyoView(key: string, opt: HopyoViewOptions): HTMLElement variant: "ghost", size: "sm", onClick: () => - choose(line, (t) => + choose(line, got.파일, (t) => guessHopyo(opt.scope, key, got.호표.번, line.차례, t).then( (g) => (picked.set(line.차례, g), repaint()), fail, @@ -378,8 +427,8 @@ export function buildHopyoView(key: string, opt: HopyoViewOptions): HTMLElement }); } - /** 대상 고르기 — 후보 5 + 다른 로직 · 자재 찾기 */ - function choose(line: HopyoLine, done: (t: HopyoTarget) => void): void { + /** 대상 고르기 — 엑셀 근거 줄 · 후보 5(근거 꼬리표) + 다른 로직 · 자재 찾기 */ + function choose(line: HopyoLine, 파일: string, done: (t: HopyoTarget) => void): void { const material = MATERIAL.has(line.갈래); openModal({ title: `고르기 — ${line.이름} ${line.규격}`.trim(), @@ -390,14 +439,48 @@ export function buildHopyoView(key: string, opt: HopyoViewOptions): HTMLElement close(); done(t); }; - const list = (line.맞춤.후보 ?? []).map((c) => - createButton({ + const list = (line.맞춤.후보 ?? []).map((c) => { + const button = createButton({ label: `${targetText(c)} · ${c.로직 ?? c.자재 ?? ""}`, variant: "ghost", size: "sm", onClick: () => pick(c), - }), - ); + }); + const tag = basisTag(c.근거); + if (tag && c.근거) + button.append( + el("span", { + className: `m02-hopyo__basis${basisOk(c.근거) ? " is-ok" : ""}`, + text: tag, + attrs: { title: `${c.근거.까닭} · 점수 ${c.근거.점수}` }, + }), + ); + return button; + }); + const basis = line.근거; + const source = basis + ? [ + el("div", { + className: "m02-hopyo__bar m02-hopyo__source", + children: [ + note(`엑셀 근거 — ${sourceText(basis)}`), + createButton({ + label: "엑셀에서 보기", + variant: "ghost", + size: "sm", + onClick: () => + openSheet({ + scope: opt.scope, + key, + 파일, + canWrite: opt.canWrite, + 단산행: basis.행, + }), + }), + ], + }), + ] + : []; const more = createButton({ label: material ? "다른 자재 찾기…" : "다른 로직 찾기…", size: "sm", @@ -408,6 +491,7 @@ export function buildHopyoView(key: string, opt: HopyoViewOptions): HTMLElement }, }); body.append( + ...source, el("p", { className: "m02-hopyo__note", text: list.length ? "후보" : "가까운 것 없음" }), el("div", { className: "m02-hopyo__cands", children: list }), more, @@ -434,6 +518,24 @@ export function buildHopyoView(key: string, opt: HopyoViewOptions): HTMLElement await remember(no, body); } + /** 근거 맞는 후보로 확정 — 지금 대상이 아니면 입력값 · 환산을 서버에서 다시 읽음 */ + async function confirmBasis(no: number, lines: HopyoLine[]): Promise { + const body: Record = {}; + for (const x of lines) { + const c = basisPick(x); + const m = x.맞춤; + if (!c) continue; + const g = + c === m.대상 + ? { 대상: c, 입력값: m.입력값, 환산: m.환산 } + : await guessHopyo(opt.scope, key, no, x.차례, c).catch(() => null); + const one = g && memoryOf(g.대상, g.입력값, g.환산?.배 ?? null); + if (one) body[m.열쇠] = one; + } + if (!Object.keys(body).length) return showToast("확정할 줄 없음 — 환산을 못 읽음", "warning"); + await remember(no, body); + } + async function build(no: number, fresh: boolean): Promise { try { const from = { 층: opt.scope.layer, 주인: opt.scope.owner, 키: key, 번: no }; diff --git a/resources/tester/helper_m02_hopyo_pick.cjs b/resources/tester/helper_m02_hopyo_pick.cjs index 1a389f69f..78c8b60a9 100644 --- a/resources/tester/helper_m02_hopyo_pick.cjs +++ b/resources/tester/helper_m02_hopyo_pick.cjs @@ -88,6 +88,34 @@ function loadTs(file) { view.builtWord({ 다시씀: true }, true), ], }, + basis: (() => { + const hit = { + 조항: ["건설 공통 3-2-1"], + 직종: ["특별인부"], + 단위: true, + 점수: 1.65, + 까닭: "조항", + }; + const half = { ...hit, 직종: [] }; + const line = (상태, 대상, 후보) => ({ 맞춤: { 상태, 대상, 후보 } }); + const a = { 로직: "GF000101", 근거: half }; + const b = { 로직: "GC000083", 근거: hit }; + return { + ok: [hit, half, { ...hit, 단위: false }, undefined].map(view.basisOk), + tag: [hit, half, undefined].map(view.basisTag), + source: [ + view.sourceText({ 출처: [["건설표준품셈", "3-2-1"]], 직종: ["특별인부"], 행: [93, 101] }), + view.sourceText({ 출처: [], 직종: [], 행: [7, 7] }), + view.sourceText(undefined), + ], + pick: [ + line("후보", a, [a, b]), + line("후보", b, [b]), + line("같음", b, [b]), + line("후보", a, [a]), + ].map((x) => view.basisPick(x)?.로직 ?? null), + }; + })(), sheet: { at: [19, 20, 22, 24, 25, 30, 31].map((row) => sheet.hopyoAtRow(spans, row)), span: sheet.spanText([20, 24], sheet.lastColumn({ 칸: { A1: {}, N24: {} }, 열: { O: {} } })), diff --git a/resources/tester/test_m02_hopyo_screen.py b/resources/tester/test_m02_hopyo_screen.py index 804e09399..c844f2d7d 100644 --- a/resources/tester/test_m02_hopyo_screen.py +++ b/resources/tester/test_m02_hopyo_screen.py @@ -3,6 +3,7 @@ - 고르기 창 덧줄(Node · fetch 대역): 이 층 · 위 층 호표 줄(맞춤 n/m) · 종류 고르기 · 고르면 문서 층에 만들기 길 → `lib.pick` 참조 줄(별칭 = 호표 품명) · 프로젝트 층엔 없음 · 본 화면 글 도우미 +- 고르기 창 근거(330-3): 꼬리표 · 엑셀 근거 줄 · [근거 맞음 모두 확정] 대상 - 엑셀 보기 창(329): 행 → 호표 · 호표 영역 A1 · 격자 길 · 여는 자리 넷 · 늦게 불러옴 - 소스: 좌측 판 자리(사전조합 아래 · 구조물 조합 층 · 쓰기 권한 없으면 올리기 꺼짐 · 숨은 file 입력) · Labor_Detail 줄 수 그대로 · 새 파일 이름 · 700 줄 · 색 = 공용 토큰 @@ -188,3 +189,23 @@ def test_엑셀_보기_창_여는_자리와_늦게_불러옴(): r'^import \{[^}]*\} from "\.\./A00_Common/spreadsheet/spreadsheet"', sheet, re.M ) assert "readOnly: true" in sheet and "wide: true" in sheet + + +def test_고르기_창_근거_꼬리표와_근거_맞음(): + basis = _run()["basis"] + assert basis["ok"] == [True, False, False, False] # 조항 · 직종 · 단위 모두 + assert basis["tag"] == [ + "조항 건설 공통 3-2-1 · 직종 특별인부 · 단위 같음", + "조항 건설 공통 3-2-1 · 단위 같음", + "", + ] + assert basis["source"] == [ + "출처 건설표준품셈 3-2-1 · 직종 특별인부 · 단가산출 93–101행", + "단가산출 7행", + "", + ] + # 지금 대상이 근거 안 맞으면 근거 맞는 후보 · 「후보」 줄만 · 근거 맞는 것 없으면 없음 + assert basis["pick"] == ["GC000083", "GC000083", None, None] + view = _text("M02_MasterTemplete_Hopyo_View.ts") + assert "단산행: basis.행" in view and "근거 맞음 모두 확정" in view + assert "x.근거 || x.맞춤.후보?.some((c) => c.근거)" in view # 옛 파일 = 단추 없음 From ca11ee14560b5d536c840d3414089a19640ab8e0 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 15 Oct 2026 12:41:16 +0900 Subject: [PATCH 29/81] =?UTF-8?q?feat(CAD):=20331-3=20=EC=84=A0=20?= =?UTF-8?q?=EC=A4=91=20=EB=8B=AB=EA=B8=B0(C)=20=C2=B7=20=EB=AA=85=EB=A0=B9?= =?UTF-8?q?=EC=B7=A8=EC=86=8C(U)=20=C2=B7=20=ED=95=9C=20=EB=B2=88=20?= =?UTF-8?q?=EC=8A=A4=EB=83=85=20=EC=9D=B4=EB=A6=84=20=C2=B7=20=ED=95=9C?= =?UTF-8?q?=EA=B8=80=20=EC=95=88=EB=82=B4=EB=AC=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 선 — 이어 그리기 첫 점 · 구간 기억 · C = 둘째 선부터 첫 점으로 닫고 끝 · U = 마지막 한 구간만 취소 - 점 단계 글 — 선택지 글자(meta.options)만 도구로 · 나머지는 명령 이름 그대로 · 한글 받개는 점 단계에서 안 잡음 - 새 input-snap-override — END · MID · CEN · INT · PER · NEA · NON 다음 한 점만 · F3 꺼져도 · 맞는 점 없으면 클릭 버림 · Esc 풂 · 커서 옆 안내 - 원본 도구 안내문 한글(선 · 원 · 사각형 · 이동 · 복사 · 회전 · 배열 · 정렬 · 지우개 · 그림 · 폴리선 합치기 · 선택) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013QyNXY2TUKXCFMwAca2APG --- .../src/inputController/ime-sink.ts | 25 +++++- .../src/inputController/input-controller.ts | 43 +++++++--- .../inputController/input-snap-override.ts | 80 ++++++++++++++++++ .../src/tools/align-tool.helpers.ts | 2 +- .../openwebcad/src/tools/array-tool.ts | 14 ++-- .../openwebcad/src/tools/circle-tool.ts | 4 +- .../openwebcad/src/tools/copy-tool.ts | 10 +-- .../openwebcad/src/tools/eraser-tool.ts | 4 +- .../openwebcad/src/tools/image-import-tool.ts | 12 +-- .../openwebcad/src/tools/line-tool.ts | 61 +++++++++++++- .../openwebcad/src/tools/move-tool.ts | 10 +-- .../openwebcad/src/tools/pedit-tool.ts | 2 +- .../openwebcad/src/tools/rectangle-tool.ts | 8 +- .../openwebcad/src/tools/rotate-tool.ts | 14 ++-- .../openwebcad/src/tools/select-tool.ts | 8 +- resources/tester/cad/keyboard-chain.test.ts | 82 +++++++++++++++++++ 16 files changed, 318 insertions(+), 61 deletions(-) create mode 100644 B07_DesignDetail/openwebcad/src/inputController/input-snap-override.ts diff --git a/B07_DesignDetail/openwebcad/src/inputController/ime-sink.ts b/B07_DesignDetail/openwebcad/src/inputController/ime-sink.ts index 327220356..ae60c87a3 100644 --- a/B07_DesignDetail/openwebcad/src/inputController/ime-sink.ts +++ b/B07_DesignDetail/openwebcad/src/inputController/ime-sink.ts @@ -9,12 +9,33 @@ import { getActiveToolActor } from '../state.ts'; export const isImeKey = (evt: KeyboardEvent): boolean => evt.isComposing || evt.keyCode === 229 || evt.key === 'Process'; +/** 활성 도구의 지금 단계 설정(평평한 단계만) */ +const activeStateConfig = () => { + const snapshot = getActiveToolActor()?.getSnapshot(); + return snapshot?.machine?.states?.[snapshot?.value]?.config; +}; + /** 활성 도구의 지금 단계가 TEXT_INPUT 을 받는가 */ export function activeToolAcceptsText(): boolean { - const snapshot = getActiveToolActor()?.getSnapshot(); - return !!snapshot?.machine?.states?.[snapshot?.value]?.config?.on?.TEXT_INPUT; + return !!activeStateConfig()?.on?.TEXT_INPUT; } +/** 지금 단계가 점(`x,y`)을 받는가 — 한 번 스냅 이름 · 선택지 판정 */ +export function activeToolWantsPoint(): boolean { + return !!activeStateConfig()?.on?.ABSOLUTE_POINT_INPUT; +} + +/** 지금 단계가 이 글을 받는가 — 점 단계는 선택지 글자(meta.options · 선 C · U)만 · 나머지는 명령 이름으로 */ +export function activeToolTakesText(text: string): boolean { + if (!activeToolAcceptsText()) return false; + if (!activeToolWantsPoint()) return true; + const options = (activeStateConfig()?.meta as { options?: string[] } | undefined)?.options; + return !!options?.includes(text); +} + +/** 한글 받개에 초점을 줄 단계 — 글은 받되 점 단계 선택지(영문 C · U)는 아님 */ +export const activeToolWantsIme = (): boolean => activeToolAcceptsText() && !activeToolWantsPoint(); + /** 지금 단계가 자유 글(TEXT · MTEXT · 표 칸 · 이름 …)을 받는가 — Space 가 띄어쓰기(아니면 Enter) */ export function activeToolWantsFreeText(): boolean { const meta = getActiveToolActor()?.getSnapshot()?.getMeta?.() as diff --git a/B07_DesignDetail/openwebcad/src/inputController/input-controller.ts b/B07_DesignDetail/openwebcad/src/inputController/input-controller.ts index 768d6fec9..5c257cfc6 100644 --- a/B07_DesignDetail/openwebcad/src/inputController/input-controller.ts +++ b/B07_DesignDetail/openwebcad/src/inputController/input-controller.ts @@ -17,6 +17,7 @@ import { getClosestSnapPointWithinRadius } from '../helpers/get-closest-snap-poi import { pickRadius } from '../helpers/pick-radius.ts'; import { getActiveToolActor, + getAngleGuideOriginPoint, getAngleStep, getCanvas, getPickableEntities, @@ -46,8 +47,10 @@ import { Tool } from '../tools.ts'; import { SelectState, selectToolStateMachine } from '../tools/select-tool.ts'; import { ActorEvent, type MouseClickEvent, type TextInputEvent } from '../tools/tool.types.ts'; import { - activeToolAcceptsText, + activeToolTakesText, activeToolWantsFreeText, + activeToolWantsIme, + activeToolWantsPoint, ImeSink, isImeKey, isPlainSpace, @@ -58,6 +61,13 @@ import { wheelZoom } from './input-wheel.ts'; import { attachTouch } from './input-touch.ts'; import { ANGLE_LOCK_REGEXP, parsePointText, type PointTextEvent } from './input-patterns.ts'; import { angleLockHint, setAngleLock } from './input-angle-lock.ts'; +import { + getSnapOverride, + isSnapName, + setSnapOverride, + snapOverrideHint, + snapOverridePoint, +} from './input-snap-override.ts'; /** 이만큼 넘게 끌면 클릭이 아니라 드래그로 본다 (화면 픽셀) */ const DRAG_SELECT_MIN_PX = 4; @@ -112,7 +122,7 @@ export class InputController { } public draw(drawController: ScreenCanvasDrawController) { - this.ime.sync(activeToolAcceptsText()); + this.ime.sync(activeToolWantsIme()); const toolInstruction = getLastStateInstructions(); const texts: string[] = []; if (toolInstruction) { @@ -122,8 +132,7 @@ export class InputController { const roundedY = round(drawController.getWorldMouseLocation().y, 2); texts.push(`${roundedX},${roundedY}`); } - const lockHint = angleLockHint(); - if (lockHint) texts.push(lockHint); + texts.push(...compact([angleLockHint(), snapOverrideHint()])); // 입력 중인 문자로 시작하는 명령 후보. 예: C => CIRCLE (C), COPY (CO) texts.push(...this.getCommandSuggestions()); // 입력 글(조합 중인 한글도 함께) @@ -173,7 +182,7 @@ export class InputController { return; } this.sendMouseClick(evt); - this.ime.sync(activeToolAcceptsText()); // 클릭으로 문자 단계에 들면 바로 한글 받개로 + this.ime.sync(activeToolWantsIme()); // 클릭으로 문자 단계에 들면 바로 한글 받개로 } } @@ -200,7 +209,12 @@ export class InputController { const worldMouseLocationTemp = screenCanvasDrawController.targetToWorld( this.getCanvasPoint(evt) ); - const worldMouseLocation = closestSnapPoint ? closestSnapPoint.point : worldMouseLocationTemp; + let worldMouseLocation = closestSnapPoint ? closestSnapPoint.point : worldMouseLocationTemp; + if (getSnapOverride()) { + const snapped = snapOverridePoint(worldMouseLocationTemp, getAngleGuideOriginPoint()); + if (!snapped) return; // 맞는 점 없음 — 다시 누르기를 기다림 + worldMouseLocation = snapped; + } this.sendPointEvent({ type: ActorEvent.MOUSE_CLICK, @@ -408,12 +422,12 @@ export class InputController { } else if (evt.key === 'Enter') { // User wants to submit the input or submit the action this.handleEnterKey(true); - this.ime.sync(activeToolAcceptsText()); + this.ime.sync(activeToolWantsIme()); } else if (evt.key === ' ' && !activeToolWantsFreeText()) { // Space = Enter(오토캐드) · 글 단계는 아래 띄어쓰기 · 수정키 · 길게 눌러 반복은 무시 if (isPlainSpace(evt)) { this.handleEnterKey(true); - this.ime.sync(activeToolAcceptsText()); + this.ime.sync(activeToolWantsIme()); } } else if (evt.key === 'ArrowDown') { // Move the screen down @@ -438,6 +452,7 @@ export class InputController { public handleEscapeKey() { setAngleLock(null); + setSnapOverride(null); if (getSelectedEntities().length > 0) { // Deselect entities setSelectedEntityIds([]); @@ -463,7 +478,6 @@ export class InputController { /** @param repeatIfIdle 키보드 Enter · Space — 빈 입력 + 선택 도구 대기면 마지막 명령 반복(우클릭은 확정만) */ public handleEnterKey(repeatIfIdle = false) { // submit the text as input to the active tool and clear the input field - const activeToolCanHandleTextInput = activeToolAcceptsText(); if (this.text === '') { console.log('ENTER: ', { @@ -496,8 +510,12 @@ export class InputController { // 거리 `100` · `x,y` · `@x,y` · `@d> = { + END: SnapPointType.LineEndPoint, + MID: SnapPointType.LineMidPoint, + CEN: SnapPointType.CircleCenter, +}; + +let override: SnapName | null = null; + +export const getSnapOverride = (): SnapName | null => override; +export const setSnapOverride = (name: SnapName | null) => { + override = name; +}; +export const isSnapName = (text: string): text is SnapName => text in SNAP_LABELS; + +/** 커서 옆 안내 줄 */ +export const snapOverrideHint = (): string | null => + override ? `${SNAP_LABELS[override]}(${override}) 한 번 — 점 지정` : null; + +const closest = (points: Point[], mouse: Point, radius: number): Point | null => { + let best: Point | null = null; + let bestDistance = radius; + for (const point of points) { + const distance = Math.hypot(point.x - mouse.x, point.y - mouse.y); + if (distance <= bestDistance) { + best = point; + bestDistance = distance; + } + } + return best; +}; + +/** 한 번 스냅이 고른 점 · 맞는 점 없으면 null. base = 앞 점(PER 기준) */ +export function snapOverridePoint(mouse: Point, base: Point | null): Point | null { + if (override === 'NON') return mouse; + // ponytail: 일반 스냅(15px)의 두 배 둘레 — 오토캐드 조준 상자 크기 설정이 필요하면 설정 모달로 + const radius = (SNAP_POINT_DISTANCE * 2) / getScreenCanvasDrawController().getScreenScale(); + const entities = queryEntitiesNearPoint(mouse.x, mouse.y, radius).filter( + (entity) => !getLayerById(entity.layerId)?.isLocked + ); + const type = override ? SNAP_TYPES[override] : undefined; + if (type) { + const points = entities.flatMap((entity) => + entity + .getSnapPoints() + .filter((snap) => snap.type === type) + .map((snap) => snap.point) + ); + return closest(points, mouse, radius); + } + if (override === 'INT') return closest(getIntersectionPoints(entities), mouse, radius); + const near = findClosestEntity(mouse, entities); + if (!near.entity || near.distance > radius) return null; + if (override === 'NEA') return near.segment.start; + // PER — 앞 점에서 커서 아래 객체로 내린 발(선분이면 수선의 발) + if (!base) return null; + return near.entity.distanceTo(base)?.[1].start ?? null; +} diff --git a/B07_DesignDetail/openwebcad/src/tools/align-tool.helpers.ts b/B07_DesignDetail/openwebcad/src/tools/align-tool.helpers.ts index 0f1c2c495..fac184b20 100644 --- a/B07_DesignDetail/openwebcad/src/tools/align-tool.helpers.ts +++ b/B07_DesignDetail/openwebcad/src/tools/align-tool.helpers.ts @@ -57,7 +57,7 @@ export function GET_ALIGN_TOOL_STATE(type: Tool): MachineContext { [AlignState.WAITING_FOR_SELECTION]: { description: 'Select what you want to align', meta: { - instructions: 'Select what you want to align, then ENTER', + instructions: '정렬할 객체 선택 뒤 Enter', }, invoke: { id: 'selectToolInsideTheAlignTool', diff --git a/B07_DesignDetail/openwebcad/src/tools/array-tool.ts b/B07_DesignDetail/openwebcad/src/tools/array-tool.ts index 2fd83edff..be817e7d2 100644 --- a/B07_DesignDetail/openwebcad/src/tools/array-tool.ts +++ b/B07_DesignDetail/openwebcad/src/tools/array-tool.ts @@ -91,7 +91,7 @@ export const arrayToolStateMachine = createMachine( [ArrayState.WAITING_FOR_SELECTION]: { description: 'Select what you want to copy', meta: { - instructions: 'Select what you want to copy, then ENTER', + instructions: '배열할 객체 선택 뒤 Enter', }, invoke: { id: 'selectToolInsideTheCopyTool', @@ -145,7 +145,7 @@ export const arrayToolStateMachine = createMachine( ], }, [ArrayState.ASK_COPY_MODE]: { - meta: { instructions: 'Choose copy mode: Linear (L) or Radial (R), ENTER for Linear' }, + meta: { instructions: '배열 방식 [직선(L)/원형(R)] <직선>:' }, on: { TEXT_INPUT: { actions: assign(({ event }) => { @@ -168,7 +168,7 @@ export const arrayToolStateMachine = createMachine( }, }, [ArrayState.ASK_NUMBER_OF_COPIES]: { - meta: { instructions: 'Enter number of copies' }, + meta: { instructions: '복사 개수 입력:' }, on: { NUMBER_INPUT: [ { @@ -196,7 +196,7 @@ export const arrayToolStateMachine = createMachine( }, }, [ArrayState.LINEAR_WAITING_FOR_START_POINT]: { - meta: { instructions: 'Click to define the start of the distance vector' }, + meta: { instructions: '간격 시작점 지정:' }, on: { ABSOLUTE_POINT_INPUT: { actions: assign(({ event }) => ({ @@ -216,7 +216,7 @@ export const arrayToolStateMachine = createMachine( }, }, [ArrayState.LINEAR_WAITING_FOR_END_POINT]: { - meta: { instructions: 'Click to define the end of the distance vector' }, + meta: { instructions: '간격 끝점 지정:' }, on: { RELATIVE_POINT_INPUT: { actions: assign(({ event, context }) => ({ @@ -245,7 +245,7 @@ export const arrayToolStateMachine = createMachine( }, }, [ArrayState.RADIAL_WAITING_FOR_PIVOT_POINT]: { - meta: { instructions: 'Click to set pivot point' }, + meta: { instructions: '회전 중심점 지정:' }, on: { ABSOLUTE_POINT_INPUT: { actions: assign(({ event }) => ({ @@ -265,7 +265,7 @@ export const arrayToolStateMachine = createMachine( }, }, [ArrayState.RADIAL_WAITING_FOR_ANGLE]: { - meta: { instructions: 'Enter angle per step (in degrees)' }, + meta: { instructions: '한 칸 각도(도) 입력:' }, on: { NUMBER_INPUT: { target: ArrayState.EXECUTE_COPY, diff --git a/B07_DesignDetail/openwebcad/src/tools/circle-tool.ts b/B07_DesignDetail/openwebcad/src/tools/circle-tool.ts index cbd81db8f..8f992f6c9 100644 --- a/B07_DesignDetail/openwebcad/src/tools/circle-tool.ts +++ b/B07_DesignDetail/openwebcad/src/tools/circle-tool.ts @@ -60,7 +60,7 @@ export const circleToolStateMachine = createMachine( [CircleState.WAITING_FOR_CENTER_POINT]: { description: 'Select the center point of the circle tool', meta: { - instructions: 'Select the center point of the circle', + instructions: '중심점 지정 (x,y):', }, on: { MOUSE_CLICK: { @@ -79,7 +79,7 @@ export const circleToolStateMachine = createMachine( [CircleState.WAITING_FOR_POINT_ON_CIRCLE]: { description: 'Select a point on the circle', meta: { - instructions: 'Select the point on the circle', + instructions: '원 위의 점 지정 또는 반지름 입력:', }, on: { DRAW: { diff --git a/B07_DesignDetail/openwebcad/src/tools/copy-tool.ts b/B07_DesignDetail/openwebcad/src/tools/copy-tool.ts index 25b7d8542..2a24a8503 100644 --- a/B07_DesignDetail/openwebcad/src/tools/copy-tool.ts +++ b/B07_DesignDetail/openwebcad/src/tools/copy-tool.ts @@ -97,7 +97,7 @@ export const copyToolStateMachine = createMachine( [CopyState.WAITING_FOR_SELECTION]: { description: 'Select what you want to copy', meta: { - instructions: 'Select what you want to copy, then ENTER', + instructions: '복사할 객체 선택 뒤 Enter', }, invoke: { id: 'selectToolInsideTheCopyTool', @@ -136,9 +136,9 @@ export const copyToolStateMachine = createMachine( }, }, [CopyState.WAITING_FOR_START_COPY_POINT]: { - description: 'Select the start of the copy line', + description: '기준점 지정 (x,y):', meta: { - instructions: 'Select the start of the copy line', + instructions: '기준점 지정 (x,y):', }, always: { actions: CopyAction.ENABLE_HELPERS, @@ -159,9 +159,9 @@ export const copyToolStateMachine = createMachine( }, }, [CopyState.WAITING_FOR_END_COPY_POINT]: { - description: 'Select the end of the copy line', + description: '둘째 점 지정 (길이 · <각도 · @x,y):', meta: { - instructions: 'Select the end of the copy line', + instructions: '둘째 점 지정 (길이 · <각도 · @x,y):', }, on: { DRAW: { diff --git a/B07_DesignDetail/openwebcad/src/tools/eraser-tool.ts b/B07_DesignDetail/openwebcad/src/tools/eraser-tool.ts index 5aedd0898..933a54d2e 100644 --- a/B07_DesignDetail/openwebcad/src/tools/eraser-tool.ts +++ b/B07_DesignDetail/openwebcad/src/tools/eraser-tool.ts @@ -60,9 +60,9 @@ export const eraserToolStateMachine = createMachine( }, }, [EraserState.WAITING_FOR_FIRST_CLICK]: { - description: 'Select a line segment to delete', + description: '지울 선 선택', meta: { - instructions: 'Select a line segment to delete', + instructions: '지울 선 선택', }, on: { // TODO implement a DRAW action to draw the segment that will be erased in dotted line diff --git a/B07_DesignDetail/openwebcad/src/tools/image-import-tool.ts b/B07_DesignDetail/openwebcad/src/tools/image-import-tool.ts index 81f9b313b..ab3070427 100644 --- a/B07_DesignDetail/openwebcad/src/tools/image-import-tool.ts +++ b/B07_DesignDetail/openwebcad/src/tools/image-import-tool.ts @@ -70,9 +70,9 @@ export const imageImportToolStateMachine = createMachine( }, }, [ImageImportState.WAIT_FOR_IMAGE_DATA]: { - description: 'Select an image file to import', + description: '가져올 그림 파일 선택', meta: { - instructions: 'Select an image file to import', + instructions: '가져올 그림 파일 선택', }, on: { [ActorEvent.FILE_SELECTED]: { @@ -85,9 +85,9 @@ export const imageImportToolStateMachine = createMachine( }, }, [ImageImportState.WAITING_FOR_START_POINT]: { - description: 'Select the start point of the imageImport', + description: '그림 첫 모서리 지정:', meta: { - instructions: 'Select the start point of the imageImport', + instructions: '그림 첫 모서리 지정:', }, on: { MOUSE_CLICK: { @@ -101,9 +101,9 @@ export const imageImportToolStateMachine = createMachine( }, }, [ImageImportState.WAITING_FOR_END_POINT]: { - description: 'Select the end point of the imageImport', + description: '그림 맞은 모서리 지정:', meta: { - instructions: 'Select the end point of the imageImport', + instructions: '그림 맞은 모서리 지정:', }, on: { DRAW: { diff --git a/B07_DesignDetail/openwebcad/src/tools/line-tool.ts b/B07_DesignDetail/openwebcad/src/tools/line-tool.ts index 4ea1cb27a..2f02d7f85 100644 --- a/B07_DesignDetail/openwebcad/src/tools/line-tool.ts +++ b/B07_DesignDetail/openwebcad/src/tools/line-tool.ts @@ -2,6 +2,7 @@ import type { Point } from '@flatten-js/core'; import { LineEntity } from '../entities/LineEntity'; import { addEntities, + deleteEntities, getActiveLayerId, getActiveLineColor, getActiveLineDash, @@ -14,12 +15,22 @@ import { } from '../state'; import { Tool } from '../tools'; import { Actor, assign, createMachine } from 'xstate'; -import type { DrawEvent, PointInputEvent, StateEvent, ToolContext } from './tool.types'; +import type { + DrawEvent, + PointInputEvent, + StateEvent, + TextInputEvent, + ToolContext, +} from './tool.types'; import { selectToolStateMachine } from './select-tool.ts'; import { getPointFromEvent } from '../helpers/get-point-from-event.ts'; export interface LineContext extends ToolContext { startPoint: Point | null; + /** 이번 이어 그리기의 첫 점 — 닫기(C) */ + firstPoint: Point | null; + /** 이번 이어 그리기에서 그린 구간 — 명령취소(U) */ + segments: LineEntity[]; } export enum LineState { @@ -33,6 +44,8 @@ export enum LineAction { RECORD_START_POINT = 'RECORD_START_POINT', DRAW_TEMP_LINE = 'DRAW_TEMP_LINE', DRAW_FINAL_LINE = 'DRAW_FINAL_LINE', + CLOSE_LINE = 'CLOSE_LINE', + UNDO_SEGMENT = 'UNDO_SEGMENT', SWITCH_TO_SELECT_TOOL = 'SWITCH_TO_SELECT_TOOL', } @@ -44,6 +57,8 @@ export const lineToolStateMachine = createMachine( }, context: { startPoint: null, + firstPoint: null, + segments: [], type: Tool.LINE, }, initial: LineState.INIT, @@ -58,7 +73,7 @@ export const lineToolStateMachine = createMachine( [LineState.WAITING_FOR_START_POINT]: { description: 'Select the start point of the line', meta: { - instructions: 'Select the start point of the line', + instructions: '첫 점 지정 (x,y):', }, on: { MOUSE_CLICK: { @@ -77,7 +92,8 @@ export const lineToolStateMachine = createMachine( [LineState.WAITING_FOR_END_POINT]: { description: 'Select the end point of the line', meta: { - instructions: 'Select the end point of the line', + instructions: '다음 점 지정 또는 [닫기(C)/명령취소(U)]:', + options: ['C', 'U'], }, on: { DRAW: { @@ -99,6 +115,21 @@ export const lineToolStateMachine = createMachine( actions: LineAction.DRAW_FINAL_LINE, target: LineState.WAITING_FOR_END_POINT, }, + TEXT_INPUT: [ + { + // 닫기 — 둘째 선부터 · 첫 점으로 잇고 끝 + guard: ({ context, event }) => + (event as TextInputEvent).value === 'C' && context.segments.length >= 2, + actions: LineAction.CLOSE_LINE, + target: LineState.INIT, + }, + { + // 명령취소 — 마지막 구간만 + guard: ({ context, event }) => + (event as TextInputEvent).value === 'U' && context.segments.length > 0, + actions: LineAction.UNDO_SEGMENT, + }, + ], ESC: { target: LineState.INIT, }, @@ -118,6 +149,8 @@ export const lineToolStateMachine = createMachine( setAngleGuideOriginPoint(null); return { startPoint: null, + firstPoint: null, + segments: [], }; }), [LineAction.RECORD_START_POINT]: assign(({ event }) => { @@ -125,6 +158,8 @@ export const lineToolStateMachine = createMachine( setAngleGuideOriginPoint(startPoint); return { startPoint, + firstPoint: startPoint, + segments: [], }; }), [LineAction.DRAW_TEMP_LINE]: ({ context, event }) => { @@ -159,8 +194,28 @@ export const lineToolStateMachine = createMachine( setAngleGuideOriginPoint(endPoint); return { startPoint: endPoint, + segments: [...context.segments, activeLine], }; }), + [LineAction.CLOSE_LINE]: ({ context }) => { + const closing = new LineEntity( + getActiveLayerId(), + context.startPoint as Point, + context.firstPoint as Point + ); + closing.lineColor = getActiveLineColor(); + closing.lineWidth = getActiveLineWidth(); + closing.lineDash = getActiveLineDash(); + addEntities([closing], true); + }, + [LineAction.UNDO_SEGMENT]: assign(({ context }) => { + const last = context.segments[context.segments.length - 1]; + deleteEntities([last], true); + const startPoint = last.getStartPoint(); + setGhostHelperEntities([new LineEntity(getActiveLayerId(), startPoint, startPoint)]); + setAngleGuideOriginPoint(startPoint); + return { startPoint, segments: context.segments.slice(0, -1) }; + }), [LineAction.SWITCH_TO_SELECT_TOOL]: () => { setActiveToolActor(new Actor(selectToolStateMachine)); }, diff --git a/B07_DesignDetail/openwebcad/src/tools/move-tool.ts b/B07_DesignDetail/openwebcad/src/tools/move-tool.ts index a2fe3cbfd..3517c1ce4 100644 --- a/B07_DesignDetail/openwebcad/src/tools/move-tool.ts +++ b/B07_DesignDetail/openwebcad/src/tools/move-tool.ts @@ -98,7 +98,7 @@ export const moveToolStateMachine = createMachine( [MoveState.WAITING_FOR_SELECTION]: { description: 'Select what you want to move', meta: { - instructions: 'Select what you want to move, then ENTER', + instructions: '이동할 객체 선택 뒤 Enter', }, invoke: { id: 'selectToolInsideTheMoveTool', @@ -137,9 +137,9 @@ export const moveToolStateMachine = createMachine( }, }, [MoveState.WAITING_FOR_START_MOVE_POINT]: { - description: 'Select the start of the move line', + description: '기준점 지정 (x,y):', meta: { - instructions: 'Select the start of the move line', + instructions: '기준점 지정 (x,y):', }, always: { actions: MoveAction.ENABLE_HELPERS, @@ -160,9 +160,9 @@ export const moveToolStateMachine = createMachine( }, }, [MoveState.WAITING_FOR_END_MOVE_POINT]: { - description: 'Select the end of the move line', + description: '둘째 점 지정 (길이 · <각도 · @x,y):', meta: { - instructions: 'Select the end of the move line', + instructions: '둘째 점 지정 (길이 · <각도 · @x,y):', }, on: { DRAW: { diff --git a/B07_DesignDetail/openwebcad/src/tools/pedit-tool.ts b/B07_DesignDetail/openwebcad/src/tools/pedit-tool.ts index d00c3874f..83c7f330b 100644 --- a/B07_DesignDetail/openwebcad/src/tools/pedit-tool.ts +++ b/B07_DesignDetail/openwebcad/src/tools/pedit-tool.ts @@ -75,7 +75,7 @@ export const peditToolStateMachine = createMachine( [PeditState.WAITING_FOR_SELECTION]: { description: 'Select the entities that you want to combine into a polyline', meta: { - instructions: 'Select what you want to combine into a polyline, then ENTER', + instructions: '폴리선으로 합칠 객체 선택 뒤 Enter', }, invoke: { id: 'selectToolInsideThePeditTool', diff --git a/B07_DesignDetail/openwebcad/src/tools/rectangle-tool.ts b/B07_DesignDetail/openwebcad/src/tools/rectangle-tool.ts index aa375f5d6..afee826d8 100644 --- a/B07_DesignDetail/openwebcad/src/tools/rectangle-tool.ts +++ b/B07_DesignDetail/openwebcad/src/tools/rectangle-tool.ts @@ -53,9 +53,9 @@ export const rectangleToolStateMachine = createMachine( }, }, [RectangleState.WAITING_FOR_START_POINT]: { - description: 'Select the start point of the rectangle', + description: '첫 모서리 지정 (x,y):', meta: { - instructions: 'Select the start point of the rectangle', + instructions: '첫 모서리 지정 (x,y):', }, on: { MOUSE_CLICK: { @@ -69,9 +69,9 @@ export const rectangleToolStateMachine = createMachine( }, }, [RectangleState.WAITING_FOR_END_POINT]: { - description: 'Select the end point of the rectangle', + description: '맞은 모서리 지정 (@x,y):', meta: { - instructions: 'Select the end point of the rectangle', + instructions: '맞은 모서리 지정 (@x,y):', }, on: { DRAW: { diff --git a/B07_DesignDetail/openwebcad/src/tools/rotate-tool.ts b/B07_DesignDetail/openwebcad/src/tools/rotate-tool.ts index c2930e017..619c7e63e 100644 --- a/B07_DesignDetail/openwebcad/src/tools/rotate-tool.ts +++ b/B07_DesignDetail/openwebcad/src/tools/rotate-tool.ts @@ -105,7 +105,7 @@ export const rotateToolStateMachine = createMachine( [RotateState.WAITING_FOR_SELECTION]: { description: 'Select what you want to rotate', meta: { - instructions: 'Select what you want to rotate, then ENTER', + instructions: '회전할 객체 선택 뒤 Enter', }, invoke: { id: 'selectToolInsideTheRotateTool', @@ -144,9 +144,9 @@ export const rotateToolStateMachine = createMachine( }, }, [RotateState.WAITING_FOR_ROTATION_ORIGIN]: { - description: 'Select the origin of the rotate operation', + description: '기준점 지정 (x,y):', meta: { - instructions: 'Select the origin of the rotate operation', + instructions: '기준점 지정 (x,y):', }, always: { actions: RotateAction.ENABLE_HELPERS, @@ -167,9 +167,9 @@ export const rotateToolStateMachine = createMachine( }, }, [RotateState.WAITING_FOR_ANGLE_START_POINT]: { - description: 'Select the end of the base rotate line', + description: '회전 각도 입력 또는 기준선 끝점 지정:', meta: { - instructions: 'Select the end of the base rotate line', + instructions: '회전 각도 입력 또는 기준선 끝점 지정:', }, on: { MOUSE_CLICK: { @@ -195,9 +195,9 @@ export const rotateToolStateMachine = createMachine( }, }, [RotateState.WAITING_FOR_ANGLE_END_POINT]: { - description: 'Select the end of the rotate line', + description: '회전 방향 점 지정:', meta: { - instructions: 'Select the end of the rotate line', + instructions: '회전 방향 점 지정:', }, on: { DRAW: { diff --git a/B07_DesignDetail/openwebcad/src/tools/select-tool.ts b/B07_DesignDetail/openwebcad/src/tools/select-tool.ts index 55a339266..62aa98e41 100644 --- a/B07_DesignDetail/openwebcad/src/tools/select-tool.ts +++ b/B07_DesignDetail/openwebcad/src/tools/select-tool.ts @@ -57,7 +57,7 @@ export const selectToolStateMachine = createMachine( [SelectState.WAITING_FOR_FIRST_SELECT_POINT]: { description: 'Select a line or select the first point of a selection rectangle', meta: { - instructions: 'Select a line or start drawing a selection rectangle', + instructions: '객체 선택 또는 선택 상자 시작', }, on: { MOUSE_CLICK: { @@ -79,7 +79,7 @@ export const selectToolStateMachine = createMachine( [SelectState.CHECK_SELECTION]: { description: 'Checking to select one line or start drawing a selection rectangle', meta: { - instructions: 'Select one line or start drawing a selection rectangle', + instructions: '객체 하나 선택 또는 선택 상자 시작', }, always: [ { @@ -95,9 +95,9 @@ export const selectToolStateMachine = createMachine( ], }, [SelectState.WAITING_FOR_SECOND_SELECT_POINT]: { - description: 'Select the second point of a selection rectangle', + description: '선택 상자 맞은 모서리 지정', meta: { - instructions: 'Select the second point of a selection rectangle', + instructions: '선택 상자 맞은 모서리 지정', }, on: { DRAW: { diff --git a/resources/tester/cad/keyboard-chain.test.ts b/resources/tester/cad/keyboard-chain.test.ts index af5d1a5d6..a10e808fc 100644 --- a/resources/tester/cad/keyboard-chain.test.ts +++ b/resources/tester/cad/keyboard-chain.test.ts @@ -13,9 +13,11 @@ vi.mock("react-toastify", () => ({ import { toast } from "react-toastify"; import { InputController } from "../../../B07_DesignDetail/openwebcad/src/inputController/input-controller"; import { getAngleLock } from "../../../B07_DesignDetail/openwebcad/src/inputController/input-angle-lock"; +import { getSnapOverride } from "../../../B07_DesignDetail/openwebcad/src/inputController/input-snap-override"; import { getActiveToolActor, getEntities, + getLastStateInstructions, setEntities, setSelectedEntityIds, } from "../../../B07_DesignDetail/openwebcad/src/state"; @@ -197,3 +199,83 @@ describe("331-2 이동 · 복사 · 회전 키보드 값", () => { expect(toast.error).not.toHaveBeenCalled(); }); }); + +describe("331-3 선 중 C · U · 한 번 스냅 · 한글 안내", () => { + const drawOpen = () => { + type(controller, "l"); + type(controller, "0,0"); + type(controller, "10,0"); + type(controller, "10,10"); + }; + + it("C = 첫 점으로 닫고 끝 · 안내 「[닫기(C)/명령취소(U)]」", () => { + drawOpen(); + expect(getLastStateInstructions()).toBe("다음 점 지정 또는 [닫기(C)/명령취소(U)]:"); + type(controller, "c"); + const drawn = lines(); + expect(drawn).toHaveLength(3); + expectXy(drawn[2].start, 10, 10); + expectXy(drawn[2].end, 0, 0); + expect(getActiveToolActor()?.getSnapshot()?.value).toBe("WAITING_FOR_START_POINT"); + expect(getLastStateInstructions()).toBe("첫 점 지정 (x,y):"); + }); + + it("C 는 둘째 선부터 — 한 구간이면 무시", () => { + type(controller, "l"); + type(controller, "0,0"); + type(controller, "10,0"); + type(controller, "c"); + expect(lines()).toHaveLength(1); + expect(getActiveToolActor()?.getSnapshot()?.value).toBe("WAITING_FOR_END_POINT"); + }); + + it("U = 마지막 한 구간만 취소 · 그 시작점에서 이어 그림", () => { + drawOpen(); + type(controller, "u"); + expect(lines()).toHaveLength(1); + type(controller, "20,0"); + const drawn = lines(); + expect(drawn).toHaveLength(2); + expectXy(drawn[1].start, 10, 0); + expectXy(drawn[1].end, 20, 0); + }); + + it("선 중 다른 명령 이름은 그대로 명령", () => { + drawOpen(); + type(controller, "circle"); + expect(getActiveToolActor()?.getSnapshot()?.context.type).toBe("CIRCLE"); + }); + + it("END 한 번 — 둘레 안 끝점으로 · 한 점 뒤 풂", () => { + type(controller, "l"); + type(controller, "0,0"); + type(controller, "100,0"); + key(controller, "Escape"); + type(controller, "end"); + expect(getSnapOverride()).toBe("END"); + clickAt(controller, 80, 20); // 끝점 (100, 0) 에서 28 — 일반 스냅(15)은 못 닿음 + expect(getSnapOverride()).toBeNull(); + type(controller, "@0,10"); + const drawn = lines(); + expect(drawn).toHaveLength(2); + expectXy(drawn[1].start, 100, 0); + expectXy(drawn[1].end, 100, 10); + }); + + it("END 둘레에 끝점 없으면 클릭 버림 · Esc 로 풂", () => { + type(controller, "l"); + type(controller, "end"); + clickAt(controller, 500, 500); + expect(getActiveToolActor()?.getSnapshot()?.value).toBe("WAITING_FOR_START_POINT"); + expect(getSnapOverride()).toBe("END"); + key(controller, "Escape"); + expect(getSnapOverride()).toBeNull(); + }); + + it("점 단계가 아니면 END 는 스냅 아님", () => { + key(controller, "Escape"); + key(controller, "Escape"); + type(controller, "end"); + expect(getSnapOverride()).toBeNull(); + }); +}); From 309be22ce0f84c5f5b21afabf9065c11ac83dade Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 15 Oct 2026 12:44:23 +0900 Subject: [PATCH 30/81] =?UTF-8?q?feat(CAD):=20331-4=20=EB=8F=99=EC=A0=81?= =?UTF-8?q?=20=EC=9E=85=EB=A0=A5=20=E2=80=94=20=EC=BB=A4=EC=84=9C=20?= =?UTF-8?q?=EC=98=86=20=EA=B8=B8=EC=9D=B4=20=C2=B7=20=EA=B0=81=EB=8F=84=20?= =?UTF-8?q?=EB=91=90=20=EC=B9=B8=20=C2=B7=20Tab=20=EC=9E=A0=EA=B8=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 기준점 있는 점 단계에서만 Tab 가로챔(그 밖은 브라우저 초점 이동) · 숫자 넣고 Tab = 그 칸 잠금 + 다음 칸 · 빈 Tab = 칸만 바꿈 - 각도 칸 잠금 = 각도 고정 · 길이 잠근 채 Enter = 그 길이 · `100` Tab `30` Enter → (86.603, 50) - 커서 옆 줄 `▸[길이 100 🔒] [각도 30_]` — 잠금 🔒 · 지금 칸 ▸ · 나머지는 기준점→커서 실측 - 한 점 · Esc · 새 명령이면 각도 고정 · 길이 잠금 모두 풂(releaseOnePoint) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013QyNXY2TUKXCFMwAca2APG --- .../openwebcad/src/commands/run-command.ts | 4 +- .../src/inputController/input-angle-lock.ts | 21 +++++ .../src/inputController/input-controller.ts | 87 +++++++++++++++---- .../src/inputController/input-field-draw.ts | 24 +++++ resources/tester/cad/keyboard-chain.test.ts | 58 +++++++++++++ 5 files changed, 177 insertions(+), 17 deletions(-) diff --git a/B07_DesignDetail/openwebcad/src/commands/run-command.ts b/B07_DesignDetail/openwebcad/src/commands/run-command.ts index 0e405565e..f8dae7a06 100644 --- a/B07_DesignDetail/openwebcad/src/commands/run-command.ts +++ b/B07_DesignDetail/openwebcad/src/commands/run-command.ts @@ -2,7 +2,7 @@ import { toast } from 'react-toastify'; import { Actor } from 'xstate'; import { HtmlEvent } from '../App.types'; -import { setAngleLock } from '../inputController/input-angle-lock'; +import { releaseOnePoint } from '../inputController/input-angle-lock'; import { getSelectedEntities, isDrawingReadOnly, notifyWindow, setActiveToolActor } from '../state'; import type { CadCommand } from './command.types'; import { getCommandById, isViewOnlyCommand, resolveCommandInput } from './registry'; @@ -39,7 +39,7 @@ export function runCommand(command: CadCommand): string { } lastCommandId = command.id; - setAngleLock(null); // 앞 명령의 각도 고정은 새 명령에 넘기지 않음 + releaseOnePoint(); // 앞 명령의 각도 고정 · 동적 입력 잠금은 새 명령에 넘기지 않음 if (command.machine) { setActiveToolActor(new Actor(command.machine)); diff --git a/B07_DesignDetail/openwebcad/src/inputController/input-angle-lock.ts b/B07_DesignDetail/openwebcad/src/inputController/input-angle-lock.ts index bee46aac9..509ea4a4c 100644 --- a/B07_DesignDetail/openwebcad/src/inputController/input-angle-lock.ts +++ b/B07_DesignDetail/openwebcad/src/inputController/input-angle-lock.ts @@ -1,11 +1,15 @@ /** * 각도 고정 `<각도` — 오토캐드 각도 재지정. 다음 한 점만 기준점에서 그 방향(동쪽 0° 반시계 +). * 커서 · 클릭은 고정 방향 직선 위로 내리고 · 숫자는 그 방향 길이. 점 하나 쓰면 · Esc · 새 명령이면 풂. + * 동적 입력(커서 옆 길이 · 각도 두 칸) — 값 넣고 Tab = 그 칸 잠금 + 다음 칸 · 각도 칸 잠금 = 각도 고정. */ import { Point } from '@flatten-js/core'; import { polarToPoint } from './input-patterns.ts'; let lockedDegrees: number | null = null; +/** 동적 입력 — 지금 칸 · 잠근 길이 */ +let dynamicField: 'length' | 'angle' = 'length'; +let lockedLength: number | null = null; export const getAngleLock = (): number | null => lockedDegrees; @@ -13,6 +17,23 @@ export const setAngleLock = (degrees: number | null) => { lockedDegrees = degrees; }; +export const getDynamicField = () => dynamicField; +export const getLockedLength = () => lockedLength; + +/** 한 점을 쓴 뒤 · Esc · 새 명령 — 각도 고정 · 동적 입력 잠금을 모두 풂 */ +export function releaseOnePoint() { + lockedDegrees = null; + lockedLength = null; + dynamicField = 'length'; +} + +/** Tab — 친 숫자를 지금 칸에 잠그고 다음 칸으로(빈 Tab 은 칸만 바꿈) · 숫자 아니면 그대로 둠 */ +export function tabDynamicInput(value: number | null) { + if (value !== null && dynamicField === 'length') lockedLength = value; + if (value !== null && dynamicField === 'angle') lockedDegrees = value; + dynamicField = dynamicField === 'length' ? 'angle' : 'length'; +} + /** 커서 옆 안내 줄 */ export const angleLockHint = (): string | null => lockedDegrees === null ? null : `각도 ${lockedDegrees}° 고정 — 길이 입력`; diff --git a/B07_DesignDetail/openwebcad/src/inputController/input-controller.ts b/B07_DesignDetail/openwebcad/src/inputController/input-controller.ts index 5c257cfc6..896c616d1 100644 --- a/B07_DesignDetail/openwebcad/src/inputController/input-controller.ts +++ b/B07_DesignDetail/openwebcad/src/inputController/input-controller.ts @@ -55,12 +55,25 @@ import { isImeKey, isPlainSpace, } from './ime-sink.ts'; -import { drawInputField } from './input-field-draw.ts'; +import { drawInputField, dynamicInputLine } from './input-field-draw.ts'; import { ctrlShortcut } from './input-shortcut.ts'; import { wheelZoom } from './input-wheel.ts'; import { attachTouch } from './input-touch.ts'; -import { ANGLE_LOCK_REGEXP, parsePointText, type PointTextEvent } from './input-patterns.ts'; -import { angleLockHint, setAngleLock } from './input-angle-lock.ts'; +import { + ANGLE_LOCK_REGEXP, + NUMBER_REGEXP, + parsePointText, + type PointTextEvent, +} from './input-patterns.ts'; +import { + angleLockHint, + getAngleLock, + getDynamicField, + getLockedLength, + releaseOnePoint, + setAngleLock, + tabDynamicInput, +} from './input-angle-lock.ts'; import { getSnapOverride, isSnapName, @@ -132,6 +145,19 @@ export class InputController { const roundedY = round(drawController.getWorldMouseLocation().y, 2); texts.push(`${roundedX},${roundedY}`); } + const base = getAngleGuideOriginPoint(); + if (base && this.dynamicInputApplies()) { + texts.push( + dynamicInputLine( + base, + drawController.getWorldMouseLocation(), + getDynamicField(), + this.text, + getLockedLength(), + getAngleLock() + ) + ); + } texts.push(...compact([angleLockHint(), snapOverrideHint()])); // 입력 중인 문자로 시작하는 명령 후보. 예: C => CIRCLE (C), COPY (CO) texts.push(...this.getCommandSuggestions()); @@ -385,7 +411,13 @@ export class InputController { return; } if (evt.key === 'Tab') { - // Tab => move keyboard focus + // 점 단계(기준점 있음) = 동적 입력 칸 넘김 · 그 밖 = 브라우저 초점 이동 + if (!this.dynamicInputApplies()) return; + evt.preventDefault(); + const isNumber = NUMBER_REGEXP.test(this.text); + if (this.text && !isNumber) return; // 숫자 아닌 글(@x,y 등)은 그대로 둠 + tabDynamicInput(isNumber ? Number.parseFloat(this.text) : null); + this.text = ''; return; } if (evt.ctrlKey && evt.code === 'KeyV') { @@ -451,7 +483,7 @@ export class InputController { } public handleEscapeKey() { - setAngleLock(null); + releaseOnePoint(); setSnapOverride(null); if (getSelectedEntities().length > 0) { // Deselect entities @@ -479,7 +511,11 @@ export class InputController { public handleEnterKey(repeatIfIdle = false) { // submit the text as input to the active tool and clear the input field - if (this.text === '') { + const lockedLength = getLockedLength(); + if (this.text === '' && lockedLength !== null && this.dynamicInputApplies()) { + // 동적 입력 길이를 잠근 채 Enter — 그 길이(방향 = 각도 고정 · 아니면 커서) + this.sendPointEvent(this.distanceEvent(lockedLength)); + } else if (this.text === '') { console.log('ENTER: ', { text: this.text, activeTool: getActiveToolActor(), @@ -502,6 +538,15 @@ export class InputController { } else if (activeToolWantsFreeText()) { // 글 단계(TEXT · MTEXT · 표 칸 …)는 친 그대로 — 숫자 · 명령 이름도 글 this.sendText(); + } else if ( + getDynamicField() === 'angle' && + NUMBER_REGEXP.test(this.text) && + this.dynamicInputApplies() + ) { + // 동적 입력 각도 칸 — 각도 고정 · 길이를 잠갔으면 바로 그 점 + setAngleLock(Number.parseFloat(this.text)); + this.text = ''; + if (lockedLength !== null) this.sendPointEvent(this.distanceEvent(lockedLength)); } else if (ANGLE_LOCK_REGEXP.test(this.text)) { // 각도 고정 `<30` — 다음 한 점만 그 방향 setAngleLock(Number.parseFloat((ANGLE_LOCK_REGEXP.exec(this.text) as RegExpExecArray)[1])); @@ -538,21 +583,33 @@ export class InputController { this.text = ''; } - /** 친 글을 점 · 거리 사건으로 — 거리의 방향 = 스냅 · 가이드 · 커서 */ - private pointEventFromText(): PointTextEvent | null { - return parsePointText( - this.text, - () => - getSnapPointOnAngleGuide()?.point || - getSnapPoint()?.point || - getScreenCanvasDrawController().getWorldMouseLocation() + /** 거리의 방향 = 스냅 · 가이드 · 커서 */ + private distanceCursor() { + return ( + getSnapPointOnAngleGuide()?.point || + getSnapPoint()?.point || + getScreenCanvasDrawController().getWorldMouseLocation() ); } + private distanceEvent(value: number): PointTextEvent { + return { type: ActorEvent.NUMBER_INPUT, value, worldMouseLocation: this.distanceCursor() }; + } + + /** 친 글을 점 · 거리 사건으로 */ + private pointEventFromText(): PointTextEvent | null { + return parsePointText(this.text, () => this.distanceCursor()); + } + + /** 동적 입력(길이 · 각도 칸 · Tab) — 기준점이 있는 점 단계에서만 */ + private dynamicInputApplies(): boolean { + return activeToolWantsPoint() && !!getAngleGuideOriginPoint(); + } + /** 점 하나를 도구에 보냄 — 각도 고정 · 한 번 스냅은 이 한 점으로 끝 */ private sendPointEvent(event: PointTextEvent | MouseClickEvent) { getActiveToolActor()?.send(event); - setAngleLock(null); + releaseOnePoint(); setSnapOverride(null); } diff --git a/B07_DesignDetail/openwebcad/src/inputController/input-field-draw.ts b/B07_DesignDetail/openwebcad/src/inputController/input-field-draw.ts index ab2d70848..051f42ac2 100644 --- a/B07_DesignDetail/openwebcad/src/inputController/input-field-draw.ts +++ b/B07_DesignDetail/openwebcad/src/inputController/input-field-draw.ts @@ -84,3 +84,27 @@ export function drawInputField( ); }); } + +/** + * 동적 입력 두 칸 — `[길이 100 🔒] ▸[각도 30_]`. 잠근 칸은 🔒 · 지금 칸은 ▸ + 친 글 · + * 나머지는 기준점→커서 실측(각도 = 동쪽 0° 반시계 +). + */ +export function dynamicInputLine( + base: Point, + mouse: Point, + field: 'length' | 'angle', + typing: string, + lockedLength: number | null, + lockedDegrees: number | null +): string { + const live = { + length: Math.hypot(mouse.x - base.x, mouse.y - base.y), + angle: ((Math.atan2(mouse.y - base.y, mouse.x - base.x) * 180) / Math.PI + 360) % 360, + }; + const cell = (name: 'length' | 'angle', label: string, locked: number | null) => { + const active = field === name; + const value = active && typing ? `${typing}_` : (locked ?? Number(live[name].toFixed(2))); + return `${active ? '▸' : ' '}[${label} ${value}${locked !== null ? ' 🔒' : ''}]`; + }; + return `${cell('length', '길이', lockedLength)} ${cell('angle', '각도', lockedDegrees)}`; +} diff --git a/resources/tester/cad/keyboard-chain.test.ts b/resources/tester/cad/keyboard-chain.test.ts index a10e808fc..4f0004e26 100644 --- a/resources/tester/cad/keyboard-chain.test.ts +++ b/resources/tester/cad/keyboard-chain.test.ts @@ -279,3 +279,61 @@ describe("331-3 선 중 C · U · 한 번 스냅 · 한글 안내", () => { expect(getSnapOverride()).toBeNull(); }); }); + +describe("331-4 동적 입력 길이 Tab 각도", () => { + const tab = () => key(controller, "Tab"); + const enter = () => key(controller, "Enter"); + const typed = (text: string) => { + for (const ch of text) key(controller, ch); + }; + + it("100 Tab 30 ⏎ → (86.603, 50)", () => { + type(controller, "l"); + type(controller, "0,0"); + move(controller, 0, 300); + typed("100"); + tab(); + typed("30"); + enter(); + const drawn = lines(); + expect(drawn).toHaveLength(1); + expectXy(drawn[0].end, 86.603, 50); + expect(getAngleLock()).toBeNull(); + }); + + it("빈 Tab → 각도 칸 · 30 Tab(잠금) · 100 ⏎ → (86.603, 50)", () => { + type(controller, "l"); + type(controller, "0,0"); + move(controller, 0, 300); + tab(); + typed("30"); + tab(); + expect(getAngleLock()).toBe(30); + typed("100"); + enter(); + expectXy(lines()[0].end, 86.603, 50); + }); + + it("길이만 잠그고 빈 ⏎ = 커서 방향 그 길이", () => { + type(controller, "l"); + type(controller, "0,0"); + move(controller, 0, 300); + typed("40"); + tab(); + enter(); + expectXy(lines()[0].end, 0, 40); + }); + + it("기준점 없는 단계 · 숫자 아닌 글이면 Tab 은 손대지 않음", () => { + type(controller, "l"); + typed("12"); + tab(); + expect((controller as unknown as { text: string }).text).toBe("12"); + key(controller, "Escape"); // 친 글만 지움 + type(controller, "0,0"); + typed("@5,0"); + tab(); + enter(); + expectXy(lines()[0].end, 5, 0); + }); +}); From aaecfa34380a09d5da28bd23799b947d6bdc40a0 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 15 Oct 2026 12:45:03 +0900 Subject: [PATCH 31/81] =?UTF-8?q?feat(=EC=84=A4=EC=A0=95):=20332-2=20?= =?UTF-8?q?=E2=98=B0=20[=EC=84=A4=EC=A0=95]=20=EC=B0=BD=20=E2=80=94=20?= =?UTF-8?q?=EB=A7=88=EC=9A=B0=EC=8A=A4=20=C2=B7=20CAD=20=EB=8B=A8=EC=B6=95?= =?UTF-8?q?=ED=82=A4=20=C2=B7=20=EB=8B=A4=EB=A5=B8=20=EA=B3=B3=20=EB=8B=A8?= =?UTF-8?q?=EC=B6=95=ED=82=A4=20=ED=83=AD=20=C2=B7=20[=EC=A0=80=EC=9E=A5]?= =?UTF-8?q?=20=EB=95=8C=EB=A7=8C=20=EA=B3=84=EC=A0=95=20pref=20=C2=B7=20?= =?UTF-8?q?=EB=8B=AB=EC=9D=84=20=EB=95=8C=20=EB=AC=BC=EC=9D=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - app_shell ☰ 설정 묶음에 [설정] 단추(서랍 닫고 창은 처음 누를 때 불러옴) - 새 app_settings.ts(틀 · 탭 셋 · [저장] · askLeave) · app_settings_cad.ts(명령 표 · 내 별칭 · 검사) · app_settings.css - 새 openwebcad/src/commands/command-catalog.json(명령 153 · 한글 이름 · 기본 별칭) + 거울 시험 - locale 문구(ui_template_locale_common) - 시험: test_app_settings · cad/command-catalog 새로 · test_device_audit · test_touch_bridge 화면 주소 환경값(AISLO_UI_BASE · AISLO_UI_PROJECT) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01QfHKQs7J3hQELKMWQMUJEy --- A00_Common/app_settings.css | 53 +++++ A00_Common/app_settings.ts | 159 +++++++++++++++ A00_Common/app_settings_cad.ts | 118 +++++++++++ A00_Common/app_shell.ts | 16 +- .../src/commands/command-catalog.json | 155 +++++++++++++++ resources/tester/cad/command-catalog.test.ts | 28 +++ resources/tester/helper_app_settings.cjs | 36 ++++ resources/tester/test_app_settings.py | 187 ++++++++++++++++++ resources/tester/test_device_audit.py | 6 +- resources/tester/test_touch_bridge.py | 3 +- ui_template/ui_template_locale_common.ts | 68 +++++++ 11 files changed, 824 insertions(+), 5 deletions(-) create mode 100644 A00_Common/app_settings.css create mode 100644 A00_Common/app_settings.ts create mode 100644 A00_Common/app_settings_cad.ts create mode 100644 B07_DesignDetail/openwebcad/src/commands/command-catalog.json create mode 100644 resources/tester/cad/command-catalog.test.ts create mode 100644 resources/tester/helper_app_settings.cjs create mode 100644 resources/tester/test_app_settings.py diff --git a/A00_Common/app_settings.css b/A00_Common/app_settings.css new file mode 100644 index 000000000..4ac71bdcf --- /dev/null +++ b/A00_Common/app_settings.css @@ -0,0 +1,53 @@ +/* ☰ [설정] 창(PLAN 332-2) — 탭 줄 · 휠 방향 고름 · 표 */ +.app-settings__tabs { + display: flex; + flex-wrap: wrap; + gap: var(--spacing-8); + padding-bottom: var(--spacing-8); + border-bottom: 1px solid var(--color-border); +} + +.app-settings__radios { + display: flex; + flex-wrap: wrap; + gap: var(--spacing-16); +} + +.app-settings__radios > label { + display: inline-flex; + align-items: center; + gap: var(--spacing-8); + min-height: 32px; + cursor: pointer; +} + +.app-settings__table { + width: 100%; + border-collapse: collapse; +} + +.app-settings__table th, +.app-settings__table td { + padding: var(--spacing-4) var(--spacing-8); + border-bottom: 1px solid var(--color-border); + text-align: left; + vertical-align: middle; +} + +.app-settings__table thead th { + position: sticky; + top: 0; + background: var(--color-surface-raised); +} + +.app-settings__table input[aria-invalid="true"] { + border-color: var(--color-danger); +} + +.app-settings__error { + color: var(--color-danger); +} + +.app-settings__error:empty { + display: none; +} diff --git a/A00_Common/app_settings.ts b/A00_Common/app_settings.ts new file mode 100644 index 000000000..d5ab19607 --- /dev/null +++ b/A00_Common/app_settings.ts @@ -0,0 +1,159 @@ +/* ============================================================================= + * app_settings.ts + * ☰ [설정] 창(PLAN 332-2 · 요구 224-8) — 탭 셋: ① 마우스 ② CAD 단축키 ③ 다른 곳 단축키(읽기 전용). + * + * - 고침은 창 안 사본에만 · [저장] 때만 계정 pref(`input-prefs`)로 한 번 씀 — 읽기는 `input_prefs.ts` + * - 저장 안 한 고침이 있으면 닫을 때 물음(askLeave) · 잘못 있는 별칭은 저장 안 함 + * - 로그인 전엔 이 브라우저에만 남음(계정 올리기는 `b_page_state` 가 로그인 뒤에) + * ========================================================================== */ + +import { createButton, el, showToast } from "@ui/ui_template_elements"; +import { askLeave } from "@ui/ui_template_leave_dialog"; +import { t as L } from "@ui/ui_template_locale"; +import { openModal } from "@ui/ui_template_modal"; +import { writeState } from "./b_page_state"; +import { inputPrefs, type InputPrefs, type WheelZoom } from "./input_prefs"; +import { buildCadAliasTab, checkCadAliases } from "./app_settings_cad"; +import "./app_settings.css"; + +type LocaleKey = Parameters[0]; +type TabId = "mouse" | "cad" | "keys"; + +const TABS: [TabId, LocaleKey][] = [ + ["mouse", "App_Settings_Tab_Mouse"], + ["cad", "App_Settings_Tab_Cad"], + ["keys", "App_Settings_Tab_Keys"], +]; + +const WHEEL: [WheelZoom, LocaleKey][] = [ + ["pull", "App_Settings_Wheel_Pull"], + ["push", "App_Settings_Wheel_Push"], +]; + +const MOUSE_RULES: LocaleKey[] = [ + "App_Settings_Rule_Pan", + "App_Settings_Rule_Pick", + "App_Settings_Rule_Right", + "App_Settings_Rule_Pinch", +]; + +/** ③ 안내표 줄 — [자리, 키] */ +const KEY_ROWS: [LocaleKey, LocaleKey][] = [ + ["App_Settings_Keys_All", "App_Settings_Keys_All_Keys"], + ["App_Settings_Keys_Profile", "App_Settings_Keys_Profile_Keys"], + ["App_Settings_Keys_RouteEdit", "App_Settings_Keys_RouteEdit_Keys"], + ["App_Settings_Keys_Section", "App_Settings_Keys_Section_Keys"], + ["App_Settings_Keys_Cad", "App_Settings_Keys_Cad_Keys"], + ["App_Settings_Keys_Table", "App_Settings_Keys_Table_Keys"], + ["App_Settings_Keys_M02", "App_Settings_Keys_M02_Keys"], +]; + +/** 비교 · 저장 꼴 — 별칭 명령 차례를 맞춤 */ +const normalize = (p: InputPrefs): InputPrefs => ({ + wheelZoom: p.wheelZoom, + cadAliases: Object.fromEntries(Object.entries(p.cadAliases).sort(([a], [b]) => (a < b ? -1 : 1))), +}); +const snapshot = (p: InputPrefs): string => JSON.stringify(normalize(p)); + +function mouseTab(draft: InputPrefs): HTMLElement { + const group = el("div", { + className: "app-settings__radios", + attrs: { role: "radiogroup", "aria-label": L("App_Settings_Wheel") }, + }); + for (const [mode, key] of WHEEL) { + const input = el("input", { + attrs: { type: "radio", name: "app-settings-wheel", value: mode }, + }); + input.checked = draft.wheelZoom === mode; + input.addEventListener("change", () => (draft.wheelZoom = mode)); + group.append(el("label", { children: [input, L(key)] })); + } + return el("div", { + children: [ + el("h4", { text: L("App_Settings_Wheel") }), + group, + el("h4", { text: L("App_Settings_Rules") }), + el("ul", { children: MOUSE_RULES.map((key) => el("li", { text: L(key) })) }), + ], + }); +} + +function keysTab(): HTMLElement { + const rows = KEY_ROWS.map(([place, keys]) => + el("tr", { children: [el("th", { text: L(place) }), el("td", { text: L(keys) })] }), + ); + return el("table", { + className: "app-settings__table", + children: [el("tbody", { children: rows })], + }); +} + +/** ☰ [설정] — 창을 열고 지금 설정을 사본으로 고침 */ +export function openAppSettings(): void { + const draft = normalize(inputPrefs()); + let saved = snapshot(draft); + const cad = buildCadAliasTab(draft.cadAliases); + const panels: Record = { + mouse: mouseTab(draft), + cad: cad.root, + keys: keysTab(), + }; + const tabs = el("div", { + className: "app-settings__tabs ui-subtabs", + attrs: { role: "tablist" }, + }); + const show = (id: TabId): void => { + tabs.replaceChildren( + ...TABS.map(([tab, key]) => { + const button = createButton({ + label: L(key), + variant: tab === id ? "filled" : "ghost", + size: "sm", + onClick: () => show(tab), + }); + button.setAttribute("role", "tab"); + button.setAttribute("aria-selected", String(tab === id)); + button.dataset.settingsTab = tab; + return button; + }), + ); + for (const [tab, panel] of Object.entries(panels)) panel.hidden = tab !== id; + }; + + /** [저장] — 잘못 있는 별칭이면 CAD 탭을 보이고 안 씀 */ + const save = (): boolean => { + const errors = checkCadAliases(draft.cadAliases); + cad.mark(errors); + if (Object.keys(errors).length) { + show("cad"); + showToast(L("App_Settings_Cad_Invalid"), "error"); + return false; + } + if (snapshot(draft) !== saved) writeState("input-prefs", normalize(draft)); + saved = snapshot(draft); + showToast(L("App_Settings_Saved"), "success"); + return true; + }; + + const saveButton = createButton({ + label: L("Common_Btn_Save"), + variant: "filled", + size: "sm", + onClick: () => void save(), + }); + openModal({ + title: L("App_Settings"), + closeLabel: L("Common_Btn_Close"), + dialogClass: "app-settings", + actions: [saveButton], + beforeClose: async () => { + if (snapshot(draft) === saved) return true; + const choice = await askLeave(L("App_Settings_Leave")); + return choice === "discard" || (choice === "save" && save()); + }, + mount: (body) => { + show("mouse"); + body.append(tabs, ...Object.values(panels)); + }, + }); +} diff --git a/A00_Common/app_settings_cad.ts b/A00_Common/app_settings_cad.ts new file mode 100644 index 000000000..217623f03 --- /dev/null +++ b/A00_Common/app_settings_cad.ts @@ -0,0 +1,118 @@ +/* ============================================================================= + * app_settings_cad.ts + * ☰ [설정] ② CAD 단축키 탭(PLAN 332-2) — 명령 표(명령 · 한글 이름 · 기본 별칭 · 내 별칭) · 검사. + * + * - 명령 목록 = `openwebcad/src/commands/command-catalog.json`(registry 거울 · commands.*.ts 는 xstate 가 딸려 와서 JSON 한 벌) + * - acad.pgp 식 덮어쓰기 — CAD 는 명령 이름 > 내 별칭 > 기본 별칭 순서로 찾음(배선은 332-3) + * - 검사 = 영문 · 숫자만 · 내 별칭끼리 겹침 금지 · 명령 이름과 같은 별칭 금지 + * ========================================================================== */ + +import catalog from "../B07_DesignDetail/openwebcad/src/commands/command-catalog.json"; +import { el } from "@ui/ui_template_elements"; +import { t as L } from "@ui/ui_template_locale"; + +export interface CadCommandRow { + id: string; + label: string; + aliases: string[]; +} + +export const CAD_COMMANDS = catalog as CadCommandRow[]; + +export type AliasError = "chars" | "dup" | "name"; + +const ERROR_TEXT: Record[0]> = { + chars: "App_Settings_Cad_ErrChars", + dup: "App_Settings_Cad_ErrDup", + name: "App_Settings_Cad_ErrName", +}; + +/** 칸 글 → 별칭 목록(쉼표 · 빈칸으로 가름 · 대문자) */ +export const parseAliases = (text: string): string[] => + text + .toUpperCase() + .split(/[\s,]+/) + .filter(Boolean); + +/** 내 별칭 검사 — `{명령: 첫 잘못}` · 비면 통과 */ +export function checkCadAliases( + aliases: Record, + commands: CadCommandRow[] = CAD_COMMANDS, +): Record { + const ids = new Set(commands.map((c) => c.id)); + const owner = new Map(); + const errors: Record = {}; + for (const [id, list] of Object.entries(aliases)) { + for (const alias of list) { + const first = owner.get(alias); + const error: AliasError | null = !/^[A-Z0-9]+$/.test(alias) + ? "chars" + : ids.has(alias) + ? "name" + : first !== undefined + ? "dup" + : null; + if (!error) owner.set(alias, id); + else { + errors[id] ??= error; + if (first !== undefined) errors[first] ??= "dup"; + } + } + } + return errors; +} + +/** 탭 몸 — `aliases` 를 칸 입력대로 고쳐 씀 · `mark` 로 잘못 칸 표시 */ +export function buildCadAliasTab(aliases: Record): { + root: HTMLElement; + mark: (errors: Record) => void; +} { + const message = el("p", { className: "app-settings__error", attrs: { role: "alert" } }); + const inputs = new Map(); + const rows = CAD_COMMANDS.map((command) => { + const input = el("input", { + className: "ui-input ui-input--sm", + attrs: { type: "text", "aria-label": `${command.id} ${L("App_Settings_Cad_Mine")}` }, + }); + input.value = (aliases[command.id] ?? []).join(", "); + input.dataset.cadCommand = command.id; + input.addEventListener("input", () => { + const list = parseAliases(input.value); + if (list.length) aliases[command.id] = list; + else delete aliases[command.id]; + }); + inputs.set(command.id, input); + const cells = [command.id, command.label, command.aliases.join(", ")].map((text) => + el("td", { text }), + ); + return el("tr", { children: [...cells, el("td", { children: [input] })] }); + }); + const head = el("tr", { + children: ( + [ + "App_Settings_Cad_Command", + "App_Settings_Cad_Label", + "App_Settings_Cad_Default", + "App_Settings_Cad_Mine", + ] as const + ).map((key) => el("th", { text: L(key) })), + }); + const table = el("table", { + className: "app-settings__table", + children: [el("thead", { children: [head] }), el("tbody", { children: rows })], + }); + const mark = (errors: Record): void => { + for (const [id, input] of inputs) { + const error = errors[id]; + if (error) input.setAttribute("aria-invalid", "true"); + else input.removeAttribute("aria-invalid"); + input.title = error ? L(ERROR_TEXT[error]) : ""; + } + const kinds = [...new Set(Object.values(errors))]; + message.textContent = kinds.map((k) => L(ERROR_TEXT[k])).join(" · "); + }; + const root = el("div", { + children: [el("p", { text: L("App_Settings_Cad_Help") }), message, table], + }); + return { root, mark }; +} diff --git a/A00_Common/app_shell.ts b/A00_Common/app_shell.ts index 13aec256f..8f257690f 100644 --- a/A00_Common/app_shell.ts +++ b/A00_Common/app_shell.ts @@ -171,10 +171,14 @@ function buildDrawer(header: HTMLElement): HTMLElement[] { burger.addEventListener("click", () => setOpen(!header.classList.contains("is-drawer-open"))); scrim.addEventListener("click", () => setOpen(false)); header.addEventListener("keydown", (e) => e.key === "Escape" && setOpen(false)); // ☰ 에 포커스가 있어도 닫힘 - // 로고 · 진행 단계 · 메뉴 · 로그인 항목을 누르면 닫힘(언어 · 테마 · 스위치는 그대로 둠) + // 로고 · 진행 단계 · 메뉴 · 로그인 · [설정] 을 누르면 닫힘(언어 · 테마 · 스위치는 그대로 둠) drawer.addEventListener("click", (e) => { const hit = (e.target as Element).closest("button"); - if (hit?.closest(".app-drawer__brand, .app-drawer__steps, .app-drawer__nav, .app-drawer__auth")) + if ( + hit?.closest( + ".app-drawer__brand, .app-drawer__steps, .app-drawer__nav, .app-drawer__auth, .app-drawer__settings", + ) + ) setOpen(false); }); @@ -199,6 +203,14 @@ function buildDrawer(header: HTMLElement): HTMLElement[] { onClick: onApp_Theme_Toggle_Click, }), ); + // ☰ [설정](332) — 마우스 · 단축키 창 · 처음 누를 때 불러옴 + const settingsButton = createButton({ + label: L("App_Settings"), + variant: "pill", + onClick: () => void import("./app_settings").then((m) => m.openAppSettings()), + }); + settingsButton.classList.add("app-drawer__settings"); + prefs.append(settingsButton); const auth = document.createElement("div"); auth.className = "app-drawer__row app-drawer__auth"; void fillAuthActions(auth); diff --git a/B07_DesignDetail/openwebcad/src/commands/command-catalog.json b/B07_DesignDetail/openwebcad/src/commands/command-catalog.json new file mode 100644 index 000000000..b430c9d19 --- /dev/null +++ b/B07_DesignDetail/openwebcad/src/commands/command-catalog.json @@ -0,0 +1,155 @@ +[ + {"id": "LINE", "label": "선", "aliases": ["L"]}, + {"id": "PLINE", "label": "폴리선", "aliases": ["PL"]}, + {"id": "CIRCLE", "label": "원", "aliases": ["C"]}, + {"id": "ARC", "label": "호", "aliases": ["A"]}, + {"id": "RECTANG", "label": "직사각형", "aliases": ["REC"]}, + {"id": "POLYGON", "label": "다각형", "aliases": ["POL"]}, + {"id": "ELLIPSE", "label": "타원", "aliases": ["EL"]}, + {"id": "SPLINE", "label": "스플라인", "aliases": ["SPL"]}, + {"id": "MLINE", "label": "다중선", "aliases": ["ML"]}, + {"id": "MLSTYLE", "label": "다중선 스타일", "aliases": []}, + {"id": "XLINE", "label": "구성선", "aliases": ["XL"]}, + {"id": "RAY", "label": "광선", "aliases": []}, + {"id": "POINT", "label": "점", "aliases": ["PO"]}, + {"id": "DONUT", "label": "도넛", "aliases": ["DO"]}, + {"id": "DIVIDE", "label": "등분", "aliases": ["DIV"]}, + {"id": "MEASURE", "label": "길이분할", "aliases": ["ME"]}, + {"id": "HATCH", "label": "해치", "aliases": ["H", "BH"]}, + {"id": "GRADIENT", "label": "그라데이션", "aliases": ["GD"]}, + {"id": "BOUNDARY", "label": "경계", "aliases": ["BO"]}, + {"id": "REGION", "label": "영역", "aliases": ["REG"]}, + {"id": "WIPEOUT", "label": "와이프아웃", "aliases": []}, + {"id": "MOVE", "label": "이동", "aliases": ["M"]}, + {"id": "COPY", "label": "복사", "aliases": ["CO", "CP"]}, + {"id": "ROTATE", "label": "회전", "aliases": ["RO"]}, + {"id": "SCALE", "label": "축척", "aliases": ["SC"]}, + {"id": "ALIGN", "label": "정렬", "aliases": ["AL"]}, + {"id": "MIRROR", "label": "대칭", "aliases": ["MI"]}, + {"id": "OFFSET", "label": "간격띄우기", "aliases": ["O"]}, + {"id": "ARRAY", "label": "배열", "aliases": ["AR"]}, + {"id": "TRIM", "label": "자르기", "aliases": ["TR"]}, + {"id": "EXTEND", "label": "연장", "aliases": ["EX"]}, + {"id": "STRETCH", "label": "신축", "aliases": ["S"]}, + {"id": "FILLET", "label": "모깎기", "aliases": ["F"]}, + {"id": "CHAMFER", "label": "모따기", "aliases": ["CHA"]}, + {"id": "BLEND", "label": "곡선 혼합", "aliases": []}, + {"id": "BREAK", "label": "끊기", "aliases": ["BR"]}, + {"id": "BREAKATPOINT", "label": "점에서 끊기", "aliases": []}, + {"id": "JOIN", "label": "결합", "aliases": ["J"]}, + {"id": "EXPLODE", "label": "분해", "aliases": ["X"]}, + {"id": "ERASE", "label": "지우기", "aliases": ["E"]}, + {"id": "LENGTHEN", "label": "길이조정", "aliases": ["LEN"]}, + {"id": "PEDIT", "label": "폴리선 편집", "aliases": ["PE"]}, + {"id": "HATCHEDIT", "label": "해치 편집", "aliases": ["HE"]}, + {"id": "DRAWORDER", "label": "맨 앞으로", "aliases": ["DR"]}, + {"id": "DRAWORDERBACK", "label": "맨 뒤로", "aliases": []}, + {"id": "MATCHPROP", "label": "특성 일치", "aliases": ["MA"]}, + {"id": "OVERKILL", "label": "중복 객체 삭제", "aliases": []}, + {"id": "REVERSE", "label": "방향 반전", "aliases": []}, + {"id": "SETBYLAYER", "label": "ByLayer로", "aliases": []}, + {"id": "ALIGNLEFT", "label": "왼쪽", "aliases": []}, + {"id": "ALIGNCENTERH", "label": "가로 중앙", "aliases": []}, + {"id": "ALIGNRIGHT", "label": "오른쪽", "aliases": []}, + {"id": "ALIGNTOP", "label": "위", "aliases": []}, + {"id": "ALIGNCENTERV", "label": "세로 중앙", "aliases": []}, + {"id": "ALIGNBOTTOM", "label": "아래", "aliases": []}, + {"id": "MTEXT", "label": "여러 줄 문자", "aliases": ["T", "MT"]}, + {"id": "TEXT", "label": "단일 행 문자", "aliases": ["DT"]}, + {"id": "STYLE", "label": "문자 스타일", "aliases": ["ST"]}, + {"id": "TEXTEDIT", "label": "문자 편집", "aliases": ["TEDIT"]}, + {"id": "FIND", "label": "찾기·대치", "aliases": []}, + {"id": "DIM", "label": "치수", "aliases": []}, + {"id": "DIMLINEAR", "label": "선형 치수", "aliases": ["DLI"]}, + {"id": "DIMALIGNED", "label": "정렬 치수", "aliases": ["DAL"]}, + {"id": "DIMANGULAR", "label": "각도 치수", "aliases": ["DAN"]}, + {"id": "DIMARC", "label": "호 길이 치수", "aliases": ["DAR"]}, + {"id": "DIMRADIUS", "label": "반지름 치수", "aliases": ["DRA"]}, + {"id": "DIMDIAMETER", "label": "지름 치수", "aliases": ["DDI"]}, + {"id": "DIMORDINATE", "label": "세로좌표 치수", "aliases": ["DOR"]}, + {"id": "DIMBASELINE", "label": "기준선 치수", "aliases": ["DBA"]}, + {"id": "DIMCONTINUE", "label": "연속 치수", "aliases": ["DCO"]}, + {"id": "QDIM", "label": "빠른 치수", "aliases": []}, + {"id": "CENTERMARK", "label": "중심 표식", "aliases": []}, + {"id": "CENTERLINE", "label": "중심선", "aliases": []}, + {"id": "DIMSTYLE", "label": "치수 스타일", "aliases": ["D"]}, + {"id": "DIMUPDATE", "label": "치수 업데이트", "aliases": []}, + {"id": "DIMSPACE", "label": "치수 간격", "aliases": []}, + {"id": "MLEADER", "label": "다중 지시선", "aliases": ["MLD"]}, + {"id": "MLEADERSTYLE", "label": "지시선 스타일", "aliases": ["MLS"]}, + {"id": "MLEADERALIGN", "label": "지시선 정렬", "aliases": []}, + {"id": "TABLE", "label": "테이블", "aliases": ["TB"]}, + {"id": "TABLESTYLE", "label": "테이블 스타일", "aliases": ["TS"]}, + {"id": "TABLEEDIT", "label": "칸 편집", "aliases": []}, + {"id": "TABLEROW", "label": "행 넣기·지우기", "aliases": []}, + {"id": "TABLECOL", "label": "열 넣기·지우기", "aliases": []}, + {"id": "TABLEMERGE", "label": "칸 병합", "aliases": []}, + {"id": "TABLEUNMERGE", "label": "병합 해제", "aliases": []}, + {"id": "PLACETABLE", "label": "표 놓기", "aliases": []}, + {"id": "REVCLOUD", "label": "구름형 리비전", "aliases": []}, + {"id": "ANNOSCALE", "label": "주석 축척", "aliases": []}, + {"id": "LAYER", "label": "도면층 특성", "aliases": ["LA"]}, + {"id": "LAYCURSET", "label": "현재 도면층 설정", "aliases": []}, + {"id": "LAYOFF", "label": "도면층 끄기", "aliases": []}, + {"id": "LAYON", "label": "모두 켜기", "aliases": []}, + {"id": "LAYFRZ", "label": "동결", "aliases": []}, + {"id": "LAYTHW", "label": "동결 해제", "aliases": []}, + {"id": "LAYLCK", "label": "잠금", "aliases": []}, + {"id": "LAYULK", "label": "잠금 해제", "aliases": []}, + {"id": "LAYISO", "label": "분리", "aliases": []}, + {"id": "LAYUNISO", "label": "분리 해제", "aliases": []}, + {"id": "LAYERP", "label": "이전 상태", "aliases": []}, + {"id": "LAYERSTATE", "label": "도면층 상태 저장", "aliases": ["LAS"]}, + {"id": "LAYERSTATERESTORE", "label": "도면층 상태 복원", "aliases": []}, + {"id": "LAYMCH", "label": "도면층 일치", "aliases": []}, + {"id": "LAYCUR", "label": "현재 도면층으로", "aliases": []}, + {"id": "LAYMRG", "label": "도면층 병합", "aliases": []}, + {"id": "LAYDEL", "label": "도면층 삭제", "aliases": []}, + {"id": "LAYWALK", "label": "도면층 탐색", "aliases": []}, + {"id": "COLOR", "label": "색상", "aliases": ["COL"]}, + {"id": "LINETYPE", "label": "선종류", "aliases": ["LT"]}, + {"id": "LWEIGHT", "label": "선가중치", "aliases": ["LW"]}, + {"id": "PROPERTIES", "label": "특성", "aliases": ["CH", "MO", "PR"]}, + {"id": "QUICKPROPERTIES", "label": "빠른 특성", "aliases": ["QP"]}, + {"id": "TRANSPARENCY", "label": "투명도", "aliases": []}, + {"id": "GROUP", "label": "그룹", "aliases": ["G"]}, + {"id": "UNGROUP", "label": "그룹 해제", "aliases": []}, + {"id": "DIST", "label": "거리", "aliases": ["DI"]}, + {"id": "RADIUS", "label": "반지름", "aliases": []}, + {"id": "ANGLE", "label": "각도", "aliases": []}, + {"id": "AREA", "label": "면적", "aliases": ["AA"]}, + {"id": "MEASUREGEOM", "label": "빠른 측정", "aliases": ["MEA"]}, + {"id": "ID", "label": "점 좌표", "aliases": []}, + {"id": "LIST", "label": "리스트", "aliases": ["LI"]}, + {"id": "QSELECT", "label": "빠른 선택", "aliases": []}, + {"id": "QUICKCALC", "label": "계산기", "aliases": ["QC"]}, + {"id": "SELECTSIMILAR", "label": "유사 선택", "aliases": []}, + {"id": "ISOLATEOBJECTS", "label": "객체 분리", "aliases": []}, + {"id": "HIDEOBJECTS", "label": "객체 숨기기", "aliases": []}, + {"id": "UNISOLATEOBJECTS", "label": "객체 분리 종료", "aliases": ["UNHIDE", "UNISOLATE"]}, + {"id": "COPYCLIP", "label": "복사", "aliases": []}, + {"id": "CUTCLIP", "label": "잘라내기", "aliases": []}, + {"id": "PASTECLIP", "label": "붙여넣기", "aliases": []}, + {"id": "COPYBASE", "label": "기준점 복사", "aliases": []}, + {"id": "PASTEORIG", "label": "원래 좌표로 붙여넣기", "aliases": []}, + {"id": "PASTEBLOCK", "label": "그룹으로 붙여넣기", "aliases": []}, + {"id": "IMAGEATTACH", "label": "이미지 부착", "aliases": ["IAT"]}, + {"id": "BLOCK", "label": "블록 작성", "aliases": ["B"]}, + {"id": "INSERT", "label": "블록 삽입", "aliases": ["I"]}, + {"id": "SELECT", "label": "선택", "aliases": ["SE"]}, + {"id": "ZOOM", "label": "범위 줌", "aliases": ["Z"]}, + {"id": "ZOOMIN", "label": "확대", "aliases": []}, + {"id": "ZOOMOUT", "label": "축소", "aliases": []}, + {"id": "REGEN", "label": "재생성", "aliases": ["RE"]}, + {"id": "PAN", "label": "초점이동", "aliases": ["P"]}, + {"id": "QSAVE", "label": "저장", "aliases": ["SAVE"]}, + {"id": "RECOVER", "label": "백업 복구", "aliases": []}, + {"id": "EXPORT", "label": "JSON 내보내기", "aliases": ["EXP"]}, + {"id": "EXPORTDXF", "label": "DXF 내보내기", "aliases": []}, + {"id": "EXPORTDWG", "label": "DWG 내보내기", "aliases": []}, + {"id": "EXPORTSVG", "label": "SVG 내보내기", "aliases": []}, + {"id": "EXPORTPNG", "label": "PNG 내보내기", "aliases": []}, + {"id": "TOAREA", "label": "면적산출로 보내기", "aliases": []}, + {"id": "UNDO", "label": "실행 취소", "aliases": ["U"]}, + {"id": "REDO", "label": "다시 실행", "aliases": []} +] diff --git a/resources/tester/cad/command-catalog.test.ts b/resources/tester/cad/command-catalog.test.ts new file mode 100644 index 000000000..69da00b2a --- /dev/null +++ b/resources/tester/cad/command-catalog.test.ts @@ -0,0 +1,28 @@ +/** + * CAD 명령 목록 거울(PLAN 332-2) — `commands/command-catalog.json` 이 registry(`commands.*.ts`)와 같은지. + * ☰ [설정] CAD 단축키 탭은 xstate 가 딸려 오지 않게 JSON 만 읽음 — 명령을 더하거나 별칭을 바꾸면 JSON 도 같이. + * 실행: cd B07_DesignDetail/openwebcad && npx vitest run --dir ../../resources/tester/cad command-catalog + */ +import { describe, expect, it } from "vitest"; + +import catalog from "../../../B07_DesignDetail/openwebcad/src/commands/command-catalog.json"; +import { getAllCommands } from "../../../B07_DesignDetail/openwebcad/src/commands/registry"; + +describe("command-catalog.json", () => { + it("registry 와 같은 명령 · 한글 이름 · 기본 별칭 · 차례", () => { + const live = getAllCommands().map((c) => ({ + id: c.id, + label: c.label, + aliases: c.aliases ?? [], + })); + expect(catalog).toEqual(live); + }); + + it("기본 별칭 = 영문 · 숫자 · 안 겹침 · 명령 이름과 다름", () => { + const ids = new Set(catalog.map((c) => c.id)); + const aliases = catalog.flatMap((c) => c.aliases); + expect(aliases.every((a) => /^[A-Z0-9]+$/.test(a))).toBe(true); + expect(new Set(aliases).size).toBe(aliases.length); + expect(aliases.filter((a) => ids.has(a))).toEqual([]); + }); +}); diff --git a/resources/tester/helper_app_settings.cjs b/resources/tester/helper_app_settings.cjs new file mode 100644 index 000000000..7c2fb6357 --- /dev/null +++ b/resources/tester/helper_app_settings.cjs @@ -0,0 +1,36 @@ +/* PLAN 332-2 단위검증 헬퍼 — ☰ [설정] CAD 별칭 검사(`A00_Common/app_settings_cad.ts`)를 Node 로 돌림. + * 결과 = 표준출력 JSON 한 벌. 짝 = `test_app_settings.py`. */ +const fs = require("fs"); +const path = require("path"); +const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript")); + +const ROOT = path.join(__dirname, "..", ".."); +const file = path.join(ROOT, "A00_Common", "app_settings_cad.ts"); +const js = ts.transpileModule(fs.readFileSync(file, "utf8"), { + compilerOptions: { + module: ts.ModuleKind.CommonJS, + target: ts.ScriptTarget.ES2020, + esModuleInterop: true, + }, +}).outputText; +const box = { exports: {} }; +const req = (p) => + p.endsWith(".json") + ? require(path.join(ROOT, "A00_Common", p)) + : p === "@ui/ui_template_locale" + ? { t: (k) => k } + : {}; +new Function("exports", "module", "require", js)(box.exports, box, req); +const m = box.exports; + +const check = (aliases) => m.checkCadAliases(aliases); +process.stdout.write( + JSON.stringify({ + commands: m.CAD_COMMANDS.length, + parse: [m.parseAliases(" ll, Cc x1 "), m.parseAliases(""), m.parseAliases(",, ")], + ok: check({ LINE: ["LL"], CIRCLE: ["CC", "C2"], COPY: ["C"] }), + chars: check({ LINE: ["L!"], CIRCLE: ["원"] }), + dup: check({ LINE: ["LL"], CIRCLE: ["LL"], ARC: ["AA", "AA"] }), + name: check({ CIRCLE: ["LINE"] }), + }), +); diff --git a/resources/tester/test_app_settings.py b/resources/tester/test_app_settings.py new file mode 100644 index 000000000..f78cab044 --- /dev/null +++ b/resources/tester/test_app_settings.py @@ -0,0 +1,187 @@ +"""PLAN 332-2 — ☰ [설정] 창(`A00_Common/app_settings.ts` · `app_settings_cad.ts` · `app_shell.ts`). + +① CAD 별칭 검사(Node) — 영문 · 숫자만 · 겹침 금지 · 명령 이름 금지 · 칸 글 나눔 +② 실제 브라우저(sub2 화면 서버 5177 · 로그인 없음 = 이 브라우저만 · 새 창이라 남는 것 없음) + ☰ → [설정] = 서랍 닫히고 창 · 탭 셋 전환 · 고쳐도 [저장] 전엔 안 씀 · 닫을 때 물음(취소 = 머묾 · + 버림 = 안 씀 · 저장 = 한 번) · [저장] 한 번 = 한 번 씀 · 저장 뒤 읽기 층 방향 뒤집힘 · 잘못 별칭은 안 씀 +""" + +from __future__ import annotations + +import json +import os +import subprocess +import urllib.request + +import pytest + +HERE = os.path.dirname(__file__) +ROOT = os.path.join(HERE, "..", "..") +BASE = "http://localhost:5177" # sub2 화면 서버(OWNERS 자기 줄) +KEY = "aislo:pref:input-prefs" + +# 앱 스크립트보다 먼저 — 설정 키에 쓰는 횟수를 셈 +COUNT_WRITES = f""" +window.__writes = []; +const set = Storage.prototype.setItem; +Storage.prototype.setItem = function (k, v) {{ + if (k === {json.dumps(KEY)}) window.__writes.push(v); + return set.call(this, k, v); +}}; +""" +# 앱이 불러온 그 읽기 층 사본(vite 가 ?t= 를 붙였을 수 있음) +WHEEL_IN = ( + "async (d) => (await import(performance.getEntriesByType('resource')" + ".map((e) => e.name).find((n) => n.includes('/input_prefs.ts')))).wheelIn(d)" +) + + +@pytest.fixture(scope="module") +def node() -> dict: + proc = subprocess.run( # noqa: S603 — 고정 실행 파일 + ["node", os.path.join(HERE, "helper_app_settings.cjs")], + capture_output=True, + text=True, + encoding="utf-8", + timeout=60, + ) + assert proc.returncode == 0, proc.stderr + return json.loads(proc.stdout) + + +def test_별칭_검사(node): + assert node["commands"] == 153 + assert node["parse"] == [["LL", "CC", "X1"], [], []] + assert node["ok"] == {} # 남의 기본 별칭(C = CIRCLE)을 내 것으로 덮는 건 됨(acad.pgp 식) + assert node["chars"] == {"LINE": "chars", "CIRCLE": "chars"} + assert node["dup"] == {"LINE": "dup", "CIRCLE": "dup", "ARC": "dup"} + assert node["name"] == {"CIRCLE": "name"} + + +def _up() -> bool: + try: + urllib.request.urlopen(BASE, timeout=3) + return True + except Exception: + return False + + +def _open_settings(page): + page.click(".app-burger") + page.click(".app-drawer__settings") + page.wait_for_selector(".app-settings") + assert not page.evaluate( + "document.querySelector('.app-header, header')?.classList.contains('is-drawer-open')" + ) + + +def _writes(page) -> list: + return page.evaluate("window.__writes") + + +def _visible_panel_has(page, selector: str) -> bool: + return page.evaluate( + "(s) => [...document.querySelectorAll('.app-settings .ui-modal__body > div:not([role]), " + ".app-settings .ui-modal__body > table')].some((p) => !p.hidden && p.querySelector(s))", + selector, + ) + + +def _save(page): + page.click(".app-settings .ui-modal__actions button") + + +def _close(page): + page.click(".app-settings .ui-modal__top > button") + + +@pytest.mark.skipif(not _up(), reason="sub2 화면 서버(5177) 꺼짐") +def test_설정_창_탭_저장_물음(): + from playwright.sync_api import sync_playwright + + with sync_playwright() as p: + browser = p.chromium.launch(channel="msedge", headless=True) + ctx = browser.new_context( + viewport={"width": 390, "height": 844}, has_touch=True, is_mobile=True + ) + ctx.add_init_script(COUNT_WRITES) + page = ctx.new_page() + page.goto(f"{BASE}/#/a01-home") + page.wait_for_selector(".app-burger") + assert page.evaluate(WHEEL_IN, 100) is True # 기본 = 당김 + + # ☰ → [설정] · 탭 셋 · 첫 탭 = 마우스 + _open_settings(page) + tabs = page.evaluate( + "[...document.querySelectorAll('.app-settings [role=tab]')]" + ".map((b) => [b.dataset.settingsTab, b.getAttribute('aria-selected')])" + ) + assert tabs == [["mouse", "true"], ["cad", "false"], ["keys", "false"]] + assert _visible_panel_has(page, "input[type=radio]") + page.click("[data-settings-tab=cad]") + assert _visible_panel_has(page, "input[data-cad-command]") + assert not _visible_panel_has(page, "input[type=radio]") + assert page.locator(".app-settings input[data-cad-command]").count() == 153 + page.click("[data-settings-tab=keys]") + assert ( + page.evaluate( + "document.querySelector('[data-settings-tab=keys]').getAttribute('aria-selected')" + ) + == "true" + ) + page.click("[data-settings-tab=mouse]") + + # 밀기 고름 — [저장] 전엔 안 씀 · 닫으면 물음 · 취소 = 머묾 + page.check("input[name=app-settings-wheel][value=push]") + assert _writes(page) == [] + page.keyboard.press("Escape") + page.wait_for_selector(".ui-confirm") + page.click(".ui-confirm button >> nth=0") # 취소 + assert page.locator(".app-settings").count() == 1 and _writes(page) == [] + + # [저장] = 한 번 씀 · 읽기 층 방향 뒤집힘 · 닫을 때 안 물음 + _save(page) + assert len(_writes(page)) == 1 + assert json.loads(_writes(page)[0]) == {"wheelZoom": "push", "cadAliases": {}} + assert page.evaluate(WHEEL_IN, 100) is False and page.evaluate(WHEEL_IN, -100) is True + _save(page) # 고친 것 없음 — 또 안 씀 + assert len(_writes(page)) == 1 + _close(page) + assert page.locator(".app-settings").count() == 0 + assert page.locator(".ui-confirm").count() == 0 + + # CAD 별칭 — 잘못이면 안 씀 · 칸 표시 + _open_settings(page) + assert page.is_checked("input[name=app-settings-wheel][value=push]") # 저장값으로 엶 + page.click("[data-settings-tab=cad]") + line = "input[data-cad-command=LINE]" + circle = "input[data-cad-command=CIRCLE]" + for line_v, circle_v in (("L!", ""), ("LL", "ll"), ("", "LINE")): + page.fill(line, line_v) + page.fill(circle, circle_v) + _save(page) + assert page.inner_text(".app-settings__error").strip(), (line_v, circle_v) + assert page.locator(".app-settings input[aria-invalid=true]").count() >= 1 + assert len(_writes(page)) == 1 + page.fill(line, "ll") + page.fill(circle, "") + _save(page) + assert page.inner_text(".app-settings__error").strip() == "" + assert len(_writes(page)) == 2 + assert json.loads(_writes(page)[1]) == {"wheelZoom": "push", "cadAliases": {"LINE": ["LL"]}} + + # 닫을 때 물음 — 버림 = 안 씀 · 저장 = 한 번 더 + page.click("[data-settings-tab=mouse]") + page.check("input[name=app-settings-wheel][value=pull]") + _close(page) + page.click(".ui-confirm button >> nth=1") # 버리고 넘어감 + assert page.locator(".app-settings").count() == 0 and len(_writes(page)) == 2 + assert page.evaluate(WHEEL_IN, 100) is False + _open_settings(page) + page.check("input[name=app-settings-wheel][value=pull]") + _close(page) + page.click(".ui-confirm button >> nth=2") # 저장 후 넘어감 + assert page.locator(".app-settings").count() == 0 and len(_writes(page)) == 3 + assert json.loads(_writes(page)[2])["wheelZoom"] == "pull" + assert page.evaluate(WHEEL_IN, 100) is True + browser.close() diff --git a/resources/tester/test_device_audit.py b/resources/tester/test_device_audit.py index dae54241d..aaf6d0404 100644 --- a/resources/tester/test_device_audit.py +++ b/resources/tester/test_device_audit.py @@ -12,14 +12,16 @@ sub1 서버(5174)에서 모든 페이지를 네 크기로 열어 문제 수를 """ import json +import os import sys from pathlib import Path import pytest ROOT = Path(__file__).resolve().parents[2] -BASE = "http://localhost:5174" -PROJECT_NAME = "검증_sub1" +# 다른 창 서버로 돌릴 때 — AISLO_UI_BASE · AISLO_UI_PROJECT(OWNERS 자기 줄) +BASE = os.environ.get("AISLO_UI_BASE", "http://localhost:5174") +PROJECT_NAME = os.environ.get("AISLO_UI_PROJECT", "검증_sub1") PAGES = [ "a01-home", "a02-prog-detail", diff --git a/resources/tester/test_touch_bridge.py b/resources/tester/test_touch_bridge.py index a56c75620..d6c6b4f88 100644 --- a/resources/tester/test_touch_bridge.py +++ b/resources/tester/test_touch_bridge.py @@ -8,11 +8,12 @@ sub1 화면 서버(5174) 첫 화면을 터치 기기로 열고 시험용 상자 """ import json +import os import urllib.request import pytest -BASE = "http://localhost:5174" +BASE = os.environ.get("AISLO_UI_BASE", "http://localhost:5174") # 다른 창 서버 = 환경값 def _up() -> bool: diff --git a/ui_template/ui_template_locale_common.ts b/ui_template/ui_template_locale_common.ts index ed38a2035..f78b3f6c8 100644 --- a/ui_template/ui_template_locale_common.ts +++ b/ui_template/ui_template_locale_common.ts @@ -109,6 +109,74 @@ export const ui_locales_common = { "그림 위를 길게 누르면 메뉴 · 두 손가락으로 확대", "Long-press the drawing for the menu · pinch to zoom", ], + /* ☰ [설정] 창(332) — 마우스 · CAD 단축키 · 다른 곳 단축키 */ + App_Settings: ["설정", "Settings"], + App_Settings_Tab_Mouse: ["마우스", "Mouse"], + App_Settings_Tab_Cad: ["CAD 단축키", "CAD shortcuts"], + App_Settings_Tab_Keys: ["다른 곳 단축키", "Other shortcuts"], + App_Settings_Wheel: ["휠 확대 방향(모든 화면)", "Wheel zoom-in direction (all views)"], + App_Settings_Wheel_Pull: ["당기면 확대(휠을 몸 쪽으로)", "Pull to zoom in (wheel toward you)"], + App_Settings_Wheel_Push: ["밀면 확대(휠을 앞으로)", "Push to zoom in (wheel away)"], + App_Settings_Rules: ["마우스 단추", "Mouse buttons"], + App_Settings_Rule_Pan: ["화면 옮기기 = 가운데 단추 끌기", "Pan = drag with the middle button"], + App_Settings_Rule_Pick: ["고르기 = 왼쪽 단추", "Select = left button"], + App_Settings_Rule_Right: [ + "오른쪽 단추 = 메뉴 · CAD 에서는 Enter", + "Right button = menu · Enter in CAD", + ], + App_Settings_Rule_Pinch: [ + "트랙패드 · 손가락 벌림 = 늘 확대(휠 방향과 상관없음)", + "Trackpad · finger spread = always zoom in (whatever the wheel direction)", + ], + App_Settings_Cad_Help: [ + "CAD 는 명령 이름 > 내 별칭 > 기본 별칭 차례로 찾음 · 영문 · 숫자만 · 여러 개는 쉼표로", + "CAD looks up command name > my alias > default alias · letters and digits only · comma for several", + ], + App_Settings_Cad_Command: ["명령", "Command"], + App_Settings_Cad_Label: ["이름", "Name"], + App_Settings_Cad_Default: ["기본 별칭", "Default alias"], + App_Settings_Cad_Mine: ["내 별칭", "My alias"], + App_Settings_Cad_ErrChars: ["영문 · 숫자만 씀", "Letters and digits only"], + App_Settings_Cad_ErrDup: ["같은 별칭이 두 번 있음", "Same alias used twice"], + App_Settings_Cad_ErrName: ["명령 이름과 같은 별칭은 못 씀", "Alias cannot be a command name"], + App_Settings_Cad_Invalid: ["별칭에 잘못이 있어 저장 안 함", "Not saved — fix the aliases"], + App_Settings_Saved: ["설정 저장함", "Settings saved"], + App_Settings_Leave: ["저장 안 한 설정이 있음", "You have unsaved settings"], + App_Settings_Keys_All: ["모든 화면", "All views"], + App_Settings_Keys_All_Keys: [ + "Esc = ☰ · 창 닫기 · 고름 풀기 · Z 누른 채 또는 오른쪽 0.4초 누름 = 돋보기(휠 = 배율)", + "Esc = ☰ · close window · clear selection · hold Z or right button 0.4 s = loupe (wheel = zoom)", + ], + App_Settings_Keys_Profile: ["B05 종단", "B05 profile"], + App_Settings_Keys_Profile_Keys: [ + "Ctrl+Z 되돌림 · Ctrl+Shift+Z 다시 · 방향키 = 옮김 · 자유지정 Enter = 끝 · Esc · Backspace", + "Ctrl+Z undo · Ctrl+Shift+Z redo · arrows = move · free mode Enter = finish · Esc · Backspace", + ], + App_Settings_Keys_RouteEdit: ["B05 노선 편집", "B05 route edit"], + App_Settings_Keys_RouteEdit_Keys: [ + "Ctrl+Z 되돌림 · Ctrl+Y · Ctrl+Shift+Z 다시", + "Ctrl+Z undo · Ctrl+Y · Ctrl+Shift+Z redo", + ], + App_Settings_Keys_Section: ["B06 횡단", "B06 cross section"], + App_Settings_Keys_Section_Keys: [ + "방향키 · ▲▼ = 측점 옮김 · Enter · Space = 고름 · Esc", + "Arrows · ▲▼ = move station · Enter · Space = pick · Esc", + ], + App_Settings_Keys_Cad: ["CAD", "CAD"], + App_Settings_Keys_Cad_Keys: [ + "F3 스냅 · F7 그리드 · F8 직교 · F10 45° · Ctrl+Z · Ctrl+Y · Ctrl+A · Ctrl+S(M02) · Delete · Esc · Enter · Space = Enter · 방향키 = 화면 옮김 · 글자 = 명령", + "F3 snap · F7 grid · F8 ortho · F10 45° · Ctrl+Z · Ctrl+Y · Ctrl+A · Ctrl+S (M02) · Delete · Esc · Enter · Space = Enter · arrows = pan · letters = command", + ], + App_Settings_Keys_Table: ["B08 · B09 표", "B08 · B09 tables"], + App_Settings_Keys_Table_Keys: [ + "Enter · F2 = 입력 · 방향키 = 칸 옮김 · Esc = 그만", + "Enter · F2 = edit · arrows = move cell · Esc = cancel", + ], + App_Settings_Keys_M02: ["M02 양식", "M02 templates"], + App_Settings_Keys_M02_Keys: [ + "Ctrl+S 저장 · Ctrl+C · Ctrl+V · Delete · Esc · Space+끌기 = 화면 옮김 · Ctrl+휠 = 배율 · 시트 = 엑셀 키", + "Ctrl+S save · Ctrl+C · Ctrl+V · Delete · Esc · Space+drag = pan · Ctrl+wheel = zoom · sheet = Excel keys", + ], App_Footer_Copyright: [ "© 2026 임도설계 및 견적 자동화. All rights reserved.", "© 2026 Forest Road Design & Estimation. All rights reserved.", From 34659db663e16df46d1cf5e987b888e9cefa0e0b Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 15 Oct 2026 12:47:42 +0900 Subject: [PATCH 32/81] =?UTF-8?q?feat(CAD):=20332-3=20=EC=84=A4=EC=A0=95?= =?UTF-8?q?=20=EB=B0=B0=EC=84=A0=20=E2=80=94=20=ED=9C=A0=20=ED=99=95?= =?UTF-8?q?=EB=8C=80=20=EB=B0=A9=ED=96=A5=20=C2=B7=20=EB=82=B4=20=EB=B3=84?= =?UTF-8?q?=EC=B9=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 휠 — A00_Common/input_prefs 방향(기본 당김 = 지금과 같음 · 밀기 저장 뒤 뒤집힘) · 트랙패드 핀치는 늘 벌리면 확대(옛 CAD 는 벌리면 축소) - 별칭 — 명령 이름 > 내 별칭(cadAliases) > 기본 별칭 · 자동완성 후보 맨 앞 · 부를 때마다 읽어 저장 즉시 따름 - tsconfig include 에 input_prefs.ts · 시험 resources/tester/cad/input-prefs-cad.test.ts Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013QyNXY2TUKXCFMwAca2APG --- .../openwebcad/src/commands/registry.ts | 21 +++- .../src/inputController/input-wheel.test.ts | 4 +- .../src/inputController/input-wheel.ts | 11 ++- B07_DesignDetail/openwebcad/tsconfig.app.json | 2 +- resources/tester/cad/input-prefs-cad.test.ts | 97 +++++++++++++++++++ 5 files changed, 126 insertions(+), 9 deletions(-) create mode 100644 resources/tester/cad/input-prefs-cad.test.ts diff --git a/B07_DesignDetail/openwebcad/src/commands/registry.ts b/B07_DesignDetail/openwebcad/src/commands/registry.ts index f6974dcb2..581c2716b 100644 --- a/B07_DesignDetail/openwebcad/src/commands/registry.ts +++ b/B07_DesignDetail/openwebcad/src/commands/registry.ts @@ -23,6 +23,7 @@ import { } from './commands.utility'; import { ALIGN_COMMANDS, MODIFY_COMMANDS } from './commands.modify'; import { VIEW_COMMANDS } from './commands.view'; +import { inputPrefs } from '../../../../A00_Common/input_prefs'; const ALL_COMMANDS: CadCommand[] = [ ...DRAW_COMMANDS, @@ -112,11 +113,24 @@ export const TOOL_STATE_MACHINES = Object.fromEntries( ]) ) as Record; -/** 명령행 입력 해석 — 정확한 이름·별칭이 먼저, 없으면 접두사 후보의 첫 항목 */ +/** 내 별칭(☰ [설정] · `cadAliases`) 이 가리키는 명령들 — 부를 때마다 읽어 저장 즉시 따름 */ +function userAliasCommands(match: (alias: string) => boolean): CadCommand[] { + return Object.entries(inputPrefs().cadAliases).flatMap(([id, aliases]) => { + const command = COMMANDS_BY_ID.get(id); + return command && aliases.some(match) ? [command] : []; + }); +} + +/** 명령행 입력 해석 — 이름 > 내 별칭 > 기본 별칭(acad.pgp 식), 없으면 접두사 후보의 첫 항목 */ export function resolveCommandInput(input: string): CadCommand | undefined { const text = input.trim().toUpperCase(); if (!text) return undefined; - return COMMANDS_BY_ID.get(text) ?? COMMANDS_BY_ALIAS.get(text) ?? matchCommandPrefixes(text)[0]; + return ( + COMMANDS_BY_ID.get(text) ?? + userAliasCommands((alias) => alias === text)[0] ?? + COMMANDS_BY_ALIAS.get(text) ?? + matchCommandPrefixes(text)[0] + ); } /** 접두사가 같은 명령 후보 (커서 옆 자동완성 목록에 쓴다) */ @@ -127,7 +141,8 @@ export function matchCommandPrefixes(input: string): CadCommand[] { (command.aliases ?? []).some((alias) => alias.startsWith(text)) ); const byId = ALL_COMMANDS.filter((command) => command.id.startsWith(text)); - return [...new Set([...byAlias, ...byId])]; + const byUser = userAliasCommands((alias) => alias.startsWith(text)); + return [...new Set([...byUser, ...byAlias, ...byId])]; } /** 명령행 자동완성에 보여줄 문자열 (예: "LINE (L) 선") */ diff --git a/B07_DesignDetail/openwebcad/src/inputController/input-wheel.test.ts b/B07_DesignDetail/openwebcad/src/inputController/input-wheel.test.ts index 3bc6389ce..299400f90 100644 --- a/B07_DesignDetail/openwebcad/src/inputController/input-wheel.test.ts +++ b/B07_DesignDetail/openwebcad/src/inputController/input-wheel.test.ts @@ -22,8 +22,8 @@ describe('CAD 휠 = 늘 줌(153-3)', () => { expect(wheel(-1, 2)).toEqual({ delta: WHEEL_PAGE_PX, exponent: WHEEL_ZOOM_EXPONENT }); }); - it('ctrl+휠(핀치)은 핀치 지수', () => { - expect(wheel(-7, 0, true)).toEqual({ delta: 7, exponent: PINCH_ZOOM_EXPONENT }); + it('ctrl+휠(핀치)은 핀치 지수 · 벌리면(deltaY < 0) 확대(332)', () => { + expect(wheel(-7, 0, true)).toEqual({ delta: -7, exponent: PINCH_ZOOM_EXPONENT }); }); it('델타 0 은 안 움직임', () => { diff --git a/B07_DesignDetail/openwebcad/src/inputController/input-wheel.ts b/B07_DesignDetail/openwebcad/src/inputController/input-wheel.ts index f6ea49e8b..db4242522 100644 --- a/B07_DesignDetail/openwebcad/src/inputController/input-wheel.ts +++ b/B07_DesignDetail/openwebcad/src/inputController/input-wheel.ts @@ -4,6 +4,7 @@ import { WHEEL_PAGE_PX, WHEEL_ZOOM_EXPONENT, } from '../App.consts.ts'; +import { wheelEventIn } from '../../../../A00_Common/input_prefs'; /** * 휠 한 번 → 화면 확대(`zoomScreen` 인자). 휠은 늘 줌 · 커서 자리 고정(153-3) — 마우스 · 트랙패드를 @@ -17,8 +18,12 @@ export function wheelZoom( const unitPx = evt.deltaMode === 1 ? WHEEL_LINE_PX : evt.deltaMode === 2 ? WHEEL_PAGE_PX : 1; const deltaY = evt.deltaY * unitPx; if (deltaY === 0) return null; - // Aislo: wheel direction is inverted on purpose - pulling the wheel zooms in, pushing - // zooms out, matching the B04/B05 maps and the B06 cross sections (2026-08-02). + // 방향 = ☰ [설정] 휠 확대 방향(기본 당김 = 몸 쪽으로 굴리면 확대 · 모든 화면 일체) · + // 트랙패드 핀치는 늘 벌리면 확대. zoomScreen 은 음수 = 확대. // 핀치(ctrl+휠)는 델타가 잘아 지수만 크게. - return { delta: -deltaY, exponent: evt.ctrlKey ? PINCH_ZOOM_EXPONENT : WHEEL_ZOOM_EXPONENT }; + const size = Math.abs(deltaY); + return { + delta: wheelEventIn({ ctrlKey: evt.ctrlKey, deltaY }) ? -size : size, + exponent: evt.ctrlKey ? PINCH_ZOOM_EXPONENT : WHEEL_ZOOM_EXPONENT, + }; } diff --git a/B07_DesignDetail/openwebcad/tsconfig.app.json b/B07_DesignDetail/openwebcad/tsconfig.app.json index 5ce3e3e8e..87ee7f72c 100644 --- a/B07_DesignDetail/openwebcad/tsconfig.app.json +++ b/B07_DesignDetail/openwebcad/tsconfig.app.json @@ -25,6 +25,6 @@ "noUnusedParameters": true, "noFallthroughCasesInSwitch": true }, - "include": ["src", "../../A00_Common/key_shortcut.ts"], + "include": ["src", "../../A00_Common/key_shortcut.ts", "../../A00_Common/input_prefs.ts"], "exclude": ["node_modules"] } diff --git a/resources/tester/cad/input-prefs-cad.test.ts b/resources/tester/cad/input-prefs-cad.test.ts new file mode 100644 index 000000000..18731237f --- /dev/null +++ b/resources/tester/cad/input-prefs-cad.test.ts @@ -0,0 +1,97 @@ +/** + * CAD 설정 배선(PLAN 332-3) — ☰ [설정] 휠 확대 방향 · 내 별칭(`A00_Common/input_prefs.ts`). + * 휠 = 기본 당김(지금과 같음) · 밀기 저장 뒤 뒤집힘 · 핀치는 늘 벌리면 확대. + * 별칭 = 명령 이름 > 내 별칭 > 기본 별칭 · 저장 즉시 따름. + * 실행: cd B07_DesignDetail/openwebcad && npx vitest run --dir ../../resources/tester/cad input-prefs-cad + */ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { INPUT_PREFS_KEY } from "../../../A00_Common/input_prefs"; +import { wheelZoom } from "../../../B07_DesignDetail/openwebcad/src/inputController/input-wheel"; +import { + matchCommandPrefixes, + resolveCommandInput, +} from "../../../B07_DesignDetail/openwebcad/src/commands/registry"; +import { getActiveToolActor } from "../../../B07_DesignDetail/openwebcad/src/state"; +import { initApplication } from "../../../B07_DesignDetail/openwebcad/test/helpers/init-application"; + +const store = new Map(); + +const savePrefs = (value: unknown) => store.set(INPUT_PREFS_KEY, JSON.stringify(value)); + +beforeEach(() => { + store.clear(); + vi.stubGlobal("localStorage", { + getItem: (key: string) => store.get(key) ?? null, + setItem: (key: string, value: string) => store.set(key, value), + removeItem: (key: string) => store.delete(key), + }); +}); + +afterEach(() => { + vi.unstubAllGlobals(); +}); + +const wheel = (deltaY: number, ctrlKey = false) => + wheelZoom({ deltaY, deltaMode: 0, ctrlKey })?.delta; + +describe("휠 확대 방향", () => { + it("저장 없음 = 당김(몸 쪽 deltaY > 0 → 확대 = 음수 delta) — 지금과 같음", () => { + expect(wheel(100)).toBe(-100); + expect(wheel(-100)).toBe(100); + }); + + it("밀기 저장 → 뒤집힘 · 깨진 값 = 당김", () => { + savePrefs({ wheelZoom: "push" }); + expect(wheel(100)).toBe(100); + expect(wheel(-100)).toBe(-100); + store.set(INPUT_PREFS_KEY, "{깨짐"); + expect(wheel(100)).toBe(-100); + }); + + it("트랙패드 핀치(ctrl · 잔 델타)는 설정과 상관없이 벌리면(deltaY < 0) 확대", () => { + expect(wheel(-7, true)).toBe(-7); + expect(wheel(7, true)).toBe(7); + savePrefs({ wheelZoom: "push" }); + expect(wheel(-7, true)).toBe(-7); + // 마우스 Ctrl+휠(큰 델타)은 설정 방향 + expect(wheel(100, true)).toBe(100); + }); +}); + +describe("내 별칭", () => { + it("기본 별칭 그대로 · 내 별칭이 기본 별칭을 이김 · 명령 이름이 내 별칭을 이김", () => { + expect(resolveCommandInput("L")?.id).toBe("LINE"); + savePrefs({ cadAliases: { CIRCLE: ["L"], ARC: ["LINE"] } }); + expect(resolveCommandInput("l")?.id).toBe("CIRCLE"); + expect(resolveCommandInput("LINE")?.id).toBe("LINE"); + }); + + it("새 별칭 · 자동완성 후보 맨 앞 · 없는 명령 이름은 무시", () => { + savePrefs({ cadAliases: { RECTANG: ["QQ"], NOPE: ["QZ"] } }); + expect(resolveCommandInput("QQ")?.id).toBe("RECTANG"); + expect(matchCommandPrefixes("Q")[0]?.id).toBe("RECTANG"); + expect(resolveCommandInput("QZ")?.id).not.toBe("NOPE"); + }); + + it("CAD 키 입력 — 「qq␣」 = 사각형 명령", () => { + savePrefs({ cadAliases: { RECTANG: ["QQ"] } }); + const controller = initApplication(); + for (const k of ["q", "q", " "]) { + controller.handleKeyStroke({ + key: k, + code: "", + keyCode: 0, + isComposing: false, + shiftKey: false, + ctrlKey: false, + altKey: false, + metaKey: false, + repeat: false, + target: null, + preventDefault: vi.fn(), + stopPropagation: vi.fn(), + } as unknown as KeyboardEvent); + } + expect(getActiveToolActor()?.getSnapshot()?.context.type).toBe("RECTANGLE"); + }); +}); From 510b36eea2fe7cab21f431fa2265aa1d167f985f Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 15 Oct 2026 12:50:31 +0900 Subject: [PATCH 33/81] =?UTF-8?q?fix(=EC=9D=BC=EC=9C=84=EB=8C=80=EA=B0=80?= =?UTF-8?q?=EC=8B=A4=EB=AC=B4):=20330=20=EB=8D=A7=EA=B3=A0=EC=B9=A8=20?= =?UTF-8?q?=E2=80=94=20=ED=9B=84=EB=B3=B4=20=EB=8B=A4=EC=84=AF=EC=97=90=20?= =?UTF-8?q?=EC=9D=B4=EB=A6=84=201=EC=88=9C=EC=9C=84=20=EC=9E=90=EB=A6=AC?= =?UTF-8?q?=20=C2=B7=20=EB=82=B1=EB=A7=90=20=EC=95=88=20=EA=B2=B9=EC=B9=98?= =?UTF-8?q?=EB=8A=94=20=EA=B7=BC=EA=B1=B0=20=ED=9B=84=EB=B3=B4=20=EB=92=A4?= =?UTF-8?q?=EB=A1=9C=20=C2=B7=20=EC=9C=84=20=EC=B8=B5=20=EA=B2=A9=EC=9E=90?= =?UTF-8?q?=20=EC=9D=BD=EA=B8=B0=20=EC=8B=9C=ED=97=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Basis.merge 같은 점수는 이름 겹침 큰 쪽 · 이름 · 낱말(괄호 글 포함 · 인력 · 기계 · 장비 뺌) 안 겹치는 근거 후보는 근거 맞음(조항 · 직종 · 단위)이 아니면 겹치는 근거 후보 뒤로 - Basis.pick 다섯 안에 이름 1순위 하나 늘 남김 · Match match_line 이 씀 - 시험 basis(제3호표 구조물터파기 다섯 = 굴착(인력/토사) 1위 · 육상토사 안 · 터널굴착 밖 · pick) · match(이름 1순위 자리) · grid(고르기 줄 층 · 주인 그대로 회사 · 시스템 격자 읽기) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Ji4nuyLVHcxrXWHTdb5nRD --- M02_MasterTemplete/M02_Hopyo_Basis.py | 51 ++++++++++++++++++++---- M02_MasterTemplete/M02_Hopyo_Match.py | 3 +- resources/tester/test_m02_hopyo_basis.py | 11 +++++ resources/tester/test_m02_hopyo_grid.py | 13 ++++++ resources/tester/test_m02_hopyo_match.py | 1 + 5 files changed, 70 insertions(+), 9 deletions(-) diff --git a/M02_MasterTemplete/M02_Hopyo_Basis.py b/M02_MasterTemplete/M02_Hopyo_Basis.py index e4fcf4458..7d615ee34 100644 --- a/M02_MasterTemplete/M02_Hopyo_Basis.py +++ b/M02_MasterTemplete/M02_Hopyo_Basis.py @@ -8,7 +8,10 @@ 직종 = 「이름 : 수」 줄의 빈칸 뺀 이름이 우리 인력(LB) 이름 · 옛이름일 때만(지금 이름으로). 견줌(`candidates`) = 같은 책 조항 같음 1.0 · 앞 마디 같음 0.5(조항이 안 맞으면 후보 아님) · 건설 공통 · 토목 덤 0.1 · 직종 겹침(보통인부 뺌) 0.5 · 단위 0.15 · 직종도 뜻 낱말도 안 겹치면 버림 · -같은 점수는 글자 두 개 묶음(괄호 글 포함)이 가름. 자동 확정 없음 — 맞춤 차례는 `M02_Hopyo_Match`. +같은 점수는 글자 두 개 묶음(괄호 글 포함)이 가름 · 이름 · 낱말(괄호 글 포함 · 인력 · 기계 뺌)이 +하나도 안 겹치는 근거 후보는(근거 맞음 = 조항 · 직종 · 단위 다 맞으면 빼고) 겹치는 근거 후보 뒤로 · +다섯 안에 이름 1순위 하나는 늘 남김(`pick`). +자동 확정 없음 — 맞춤 차례는 `M02_Hopyo_Match`. M01 · 원본 로직은 읽기만. """ @@ -29,6 +32,7 @@ _HEAD = re.compile(r"^(\d+)\.\s*(.*?)\s*\[([^\[\]]*)\]\s*$") _BOOK = re.compile(r"품[셈줌]") _TRADE = re.compile(r"^[\s*·\-]*([가-힣][가-힣\s]*?)\s*:\s*[\d,]") _TYPOS = (("깍기", "깎기"),) +_PLAIN_WORDS = frozenset({"인력", "기계", "장비"}) # 방법 낱말 — 「터널굴착 1발파당 인력」 _MONEY = {"합계": "계", "노무비": "노무비", "재료비": "재료비", "경비": "경비"} @@ -179,8 +183,14 @@ def _text(line: dict[str, Any]) -> str: return text +def _loose_words(text: str) -> frozenset[str]: + """괄호 글까지 뜻 낱말 — 방법 낱말 뺌.""" + return hopyo_match.words(text.replace("(", " ").replace(")", " ")) - _PLAIN_WORDS + + def candidates(line: dict[str, Any], got: dict[str, Any]) -> list[dict[str, Any]]: - """근거 후보 — 줄 `근거.출처` 가 없으면 빈 것 · `{로직, 이름, 단위, 점수, 근거}` 점수 차례.""" + """근거 후보 — 줄 `근거.출처` 가 없으면 빈 것 · `{로직, 이름, 단위, 근거, _겹침, _낱말}` + 점수 차례(`_` 칸은 `merge` 가 쓰고 뺌).""" basis = line.get("근거") or {} theirs = [list(one) for one in basis.get("출처") or []] if not theirs: @@ -189,6 +199,7 @@ def candidates(line: dict[str, Any], got: dict[str, Any]) -> list[dict[str, Any] text = _text(line) mine = hopyo_match.words(text) # 괄호 글 뺌 — 「잔토처리(인력)」 ↔ 「철골재 철거(인력)」 grams = hopyo_match._grams(hopyo_match.mm.plain(text)) # 괄호 글 포함 — 같은 점수 가름 + loose = _loose_words(text) table = ours() scored = [] for one in got["로직"]: @@ -214,18 +225,42 @@ def candidates(line: dict[str, Any], got: dict[str, Any]) -> list[dict[str, Any] tags.append("단위 맞음") dice = 2 * len(grams & one["grams"]) / (len(grams) + len(one["grams"])) why = {"조항": clauses, "직종": shared, "단위": unit, "점수": round(score, 3)} - scored.append( - (score, dice, {**hopyo_match._public(one), "근거": {**why, "까닭": " · ".join(tags)}}) - ) + theirs_loose = _loose_words(one["이름"]) + overlap = any(a in b or b in a for a in loose for b in theirs_loose) + full = tag == "조항 같음" and unit and (bool(shared) or not trades) # 근거 맞음 + why["까닭"] = " · ".join(tags) + extra = {"_겹침": dice, "_낱말": overlap or full} + scored.append((score, dice, {**hopyo_match._public(one), "근거": why, **extra})) scored.sort(key=lambda x: (-x[0], -x[1])) return [one for _, _, one in scored] def merge(basis: list[dict[str, Any]], named: list[dict[str, Any]]) -> list[dict[str, Any]]: - """근거 후보 + 이름 후보 → 로직 키로 합침(점수 = 이름 점수 + 근거 점수) · 점수 차례.""" + """근거 후보 + 이름 후보 → 로직 키로 합침(점수 = 이름 점수 + 근거 점수) · 점수 차례 · + 같으면 이름 겹침 큰 쪽 · 이름 · 낱말 안 겹치는 근거 후보는 겹치는 근거 후보 뒤로.""" if not basis: return named by_name = {c.get("로직"): c["점수"] for c in named} - out = [{**c, "점수": round(c["근거"]["점수"] + by_name.pop(c["로직"], 0), 3)} for c in basis] + out = [] + for c in basis: + named_too = c["로직"] in by_name + score = round(c["근거"]["점수"] + by_name.pop(c["로직"], 0), 3) + out.append({**c, "점수": score, "_낱말": c["_낱말"] or named_too}) out += [c for c in named if c.get("로직") in by_name] - return sorted(out, key=lambda c: -c["점수"]) + out.sort(key=lambda c: (-c["점수"], -c.get("_겹침", 0))) + loose = [c for c in out if c.get("_낱말") is False] + if loose and any(c.get("_낱말") for c in out): + out = [c for c in out if c.get("_낱말") is not False] + last = max(at for at, c in enumerate(out) if c.get("_낱말")) + out[last + 1 : last + 1] = loose + return [{k: v for k, v in c.items() if not k.startswith("_")} for c in out] + + +def pick(ordered: list[dict[str, Any]], first: Any, room: int) -> list[dict[str, Any]]: + """후보 `room` 개 — 이름 1순위(`first` 로직 키) 하나는 늘 남기고 나머지는 차례대로.""" + if room <= 0: + return [] + if first is None or all(c.get("로직") != first for c in ordered): + return ordered[:room] + others = [c for c in ordered if c.get("로직") != first][: room - 1] + return [c for c in ordered if c.get("로직") == first or any(c is o for o in others)] diff --git a/M02_MasterTemplete/M02_Hopyo_Match.py b/M02_MasterTemplete/M02_Hopyo_Match.py index c9ccbc557..c45f24f13 100644 --- a/M02_MasterTemplete/M02_Hopyo_Match.py +++ b/M02_MasterTemplete/M02_Hopyo_Match.py @@ -409,7 +409,8 @@ def match_line( basis = hopyo_basis.candidates(line, got) if pool == "로직" else [] named = candidates(line, pool, got, None if basis else 5) rest = [c for c in hopyo_basis.merge(basis, named) if _public(c) not in same] - out["후보"] = (top + rest)[:5] + first = next((c.get("로직") for c in named if _public(c) not in same), None) + out["후보"] = top[:5] + hopyo_basis.pick(rest, first if basis else None, 5 - len(top)) return {**out, "상태": "후보" if out["후보"] else "없음"} diff --git a/resources/tester/test_m02_hopyo_basis.py b/resources/tester/test_m02_hopyo_basis.py index 526bfd4cb..94d21bf83 100644 --- a/resources/tester/test_m02_hopyo_basis.py +++ b/resources/tester/test_m02_hopyo_basis.py @@ -74,6 +74,9 @@ def test_근거_후보(north): "점수": 1.75, "까닭": "조항 같음 · 건설 공통 · 직종 특별인부 · 단위 맞음", } + codes = [c["로직"] for c in dig] + assert "GF000101" in codes and "GC000346" not in codes # 이름 1순위 남김 · 낱말 안 겹침 뒤로 + assert all("_겹침" not in c and "_낱말" not in c for c in dig) assert _top(north, 21)[0]["로직"] == "GC000198" assert "GC000143" in [c["로직"] for c in _top(north, 64)[:2]] assert _top(north, 49)[0]["로직"] == "GC000085" @@ -83,6 +86,14 @@ def test_근거_후보(north): assert master_hash() == before +def test_이름_1순위_자리(): + ordered = [{"로직": k} for k in "ABCDEFG"] + assert [c["로직"] for c in hb.pick(ordered, "G", 5)] == ["A", "B", "C", "D", "G"] + assert [c["로직"] for c in hb.pick(ordered, "B", 5)] == ["A", "B", "C", "D", "E"] + assert [c["로직"] for c in hb.pick(ordered, None, 2)] == ["A", "B"] + assert hb.pick(ordered, "G", 0) == [] + + def test_근거_없는_줄_그대로(north): bare = [ x diff --git a/resources/tester/test_m02_hopyo_grid.py b/resources/tester/test_m02_hopyo_grid.py index 16ba2b303..58c6cac90 100644 --- a/resources/tester/test_m02_hopyo_grid.py +++ b/resources/tester/test_m02_hopyo_grid.py @@ -110,6 +110,19 @@ def test_역할_읽기(client, role, layer, owner, want): assert client.get(GRID.format(layer, "HP-000001"), params=params).status_code == want +@pytest.mark.parametrize("layer", ["company", "system"]) +def test_위_층_격자_읽기(client, layer): + """아래 층 사용자가 고르기 줄(층 · 주인) 그대로 위 층 파일 격자를 읽음.""" + client.who.update({"role": "SYSTEM_ADMIN"}) + assert _post(client, layer).status_code == 200 + client.who.update({"role": "USER"}) + rows = client.get("/api/m02/layers/personal/hopyo-picks").json()["줄"] + row = next(r for r in rows if r["층"] == layer) + params = {"owner": row["주인"]} if row["주인"] else {} + res = client.get(GRID.format(row["층"], row["키"]), params=params) + assert res.status_code == 200 and len(res.json()["호표"]) == 68 + + def test_xlsx_값만(): data = _xlsx() grid = hg.read("a.xlsx", data, hx.read_book("a.xlsx", data)) diff --git a/resources/tester/test_m02_hopyo_match.py b/resources/tester/test_m02_hopyo_match.py index 69f8cd397..1e8743fca 100644 --- a/resources/tester/test_m02_hopyo_match.py +++ b/resources/tester/test_m02_hopyo_match.py @@ -106,6 +106,7 @@ def test_이름_같음_후보_호표_퍼센트(client): dig = _hopyo(client, 3)["구조물터파기(인력100%)"] assert dig["상태"] == "후보" and len(dig["후보"]) == 5 assert dig["후보"][0]["이름"] == "굴착(인력/토사)" # 330-2 근거(건설 3-2-1 · 특별인부) 1순위 + assert any(c["이름"].startswith("구조물터파기") for c in dig["후보"]) # 이름 1순위 자리 ref = _hopyo(client, 45)["고화매트재료비"] assert ref["상태"] == "호표" and ref["대상"] == {"호표": 46, "단위": "㎡"} and ref["들어감"] pct = _hopyo(client, 46)["잡재료비"] From 20b2deeddd376ba04c3103001a693f8b5981a1ac Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 15 Oct 2026 12:50:43 +0900 Subject: [PATCH 34/81] =?UTF-8?q?fix(B05=EC=B6=94=EC=B2=9C):=20328-2=20?= =?UTF-8?q?=ED=81=B0=20R=20=E2=80=94=20=EB=8B=A8=EC=88=9C=ED=99=94?= =?UTF-8?q?=EA=B0=80=20=EA=B8=B0=EC=9A=B8=EA=B8=B0=20=EC=9C=84=EB=B0=98?= =?UTF-8?q?=EC=9D=84=20=EB=8A=98=EB=A6=AC=EB=8A=94=20=EA=B8=89=ED=95=9C=20?= =?UTF-8?q?=EC=A7=80=ED=98=95(=EA=B2=80=EC=A6=9D=5Fsub8)=EC=97=90=EC=84=9C?= =?UTF-8?q?=202=EB=8B=A8=EC=9D=84=20=EC=B5=9C=EC=86=8C=20R=20=ED=8C=90=20?= =?UTF-8?q?=EB=85=B8=EB=93=9C=EC=97=90=20=EB=B0=94=EB=A1=9C=20=EA=B1=B4=20?= =?UTF-8?q?=ED=8C=90=EC=9D=84=201=EB=8B=A8=EB=B3=B4=EB=8B=A4=20=EB=A8=BC?= =?UTF-8?q?=EC=A0=80=20=C2=B7=20=EC=8B=A4=EC=B8=A1=20=EB=84=A4=20=EA=B0=88?= =?UTF-8?q?=EB=9E=98=20R=2012=20=E2=86=92=2018/29/147?= 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_01Se7ytqbohmBQnfYpwYFnYf --- B05_Profile/B05_Profile_Engine_RouteInitial_Pool.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/B05_Profile/B05_Profile_Engine_RouteInitial_Pool.py b/B05_Profile/B05_Profile_Engine_RouteInitial_Pool.py index 1f7e2d3df..999230c66 100644 --- a/B05_Profile/B05_Profile_Engine_RouteInitial_Pool.py +++ b/B05_Profile/B05_Profile_Engine_RouteInitial_Pool.py @@ -107,9 +107,10 @@ def _trace( traced: dict[tuple, Route], **kw: Any, ) -> Route: - # 큰 R(PLAN 328) — 2단 판(허용 이탈 안 가장 큰 R · `..._Radius.widen`) → 1단 판(탐색 경로를 - # 예정노선 규칙으로 단순화 · 묶음 · R 맞춤) 차례로, 위반이 최소 R 판(노드마다 곡선 · 옛 규칙) - # 보다 많지 않은 첫 판. 셋 다 화면이 [확인] 때 같은 선으로 다시 짜는 꼴. + # 큰 R(PLAN 328) — 2단 판(허용 이탈 안 가장 큰 R · `..._Radius.widen`) → 2단을 최소 R 판 + # 노드에 바로 건 판(단순화가 기울기 위반을 늘리는 급한 지형 · 검증_sub8 실측) → 1단 판(탐색 + # 경로를 예정노선 규칙으로 단순화 · 묶음 · R 맞춤) 차례로, 위반이 최소 R 판(노드마다 곡선 · + # 옛 규칙)보다 많지 않은 첫 판. 모두 화면이 [확인] 때 같은 선으로 다시 짜는 꼴. # 같은 노드 · 같은 잼이면 다시 재지 않음(쓸기는 이웃 비중이 같은 길을 자주 냄 · 묶음마다 따로). key = (tuple(nodes), tuple(sorted(kw.items()))) if key not in traced: @@ -119,7 +120,7 @@ def _trace( least = (floor, measure(floor, **kw)) path = densify_route(nodes, TRACE_DENSIFY_M) fitted = build_planned_polyline(path, min_radius_m=min_radius_m) - larger = (widen(fitted, path, min_radius_m), fitted) + larger = (widen(fitted, path, min_radius_m), widen(floor, path, min_radius_m), fitted) traced[key] = next( ( route From c4a09c1ca228fd37ed3e09ab9d9293853146e4c4 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 15 Oct 2026 12:52:08 +0900 Subject: [PATCH 35/81] =?UTF-8?q?fix(B05=EC=B6=94=EC=B2=9C):=20328=20?= =?UTF-8?q?=EC=B6=94=EC=B2=9C=20=EC=A0=80=EC=9E=A5=EB=B3=B8=20ALGO=5FVERSI?= =?UTF-8?q?ON=201=20=E2=86=92=202=20=E2=80=94=20=ED=81=B0=20R=20=EB=A1=9C?= =?UTF-8?q?=20=EC=85=88=EC=9D=B4=20=EB=B0=94=EB=80=8C=EC=96=B4=20=EC=98=9B?= =?UTF-8?q?=20=EC=A0=80=EC=9E=A5=EB=B3=B8(R=2012)=20=EB=AC=B4=ED=9A=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Se7ytqbohmBQnfYpwYFnYf --- B05_Profile/B05_Profile_Engine_RouteInitial_Store.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/B05_Profile/B05_Profile_Engine_RouteInitial_Store.py b/B05_Profile/B05_Profile_Engine_RouteInitial_Store.py index 5f4cd944b..976917ab1 100644 --- a/B05_Profile/B05_Profile_Engine_RouteInitial_Store.py +++ b/B05_Profile/B05_Profile_Engine_RouteInitial_Store.py @@ -31,7 +31,7 @@ from B05_Profile.B05_Profile_Engine_RouteInitial import ( ) from common_util.common_util_json import atomic_write_json -ALGO_VERSION = 1 +ALGO_VERSION = 2 STORE_DIR = "B05_Profile/route_recommend" # 업로드 체인 끝에 미리 셈할 기본 다섯 알약(화면 차례) · 추적은 묶음이 선 뒤라 `similar` 가 채워짐 DEFAULT_MODES = ("gradeline", "earthwork", "gentle", "weighted", "follow") From a8d7832c8b7bc38a74af37bd4727fe6ce2c20430 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 15 Oct 2026 12:52:42 +0900 Subject: [PATCH 36/81] =?UTF-8?q?test(=EC=9D=BC=EC=9C=84=EB=8C=80=EA=B0=80?= =?UTF-8?q?=EC=8B=A4=EB=AC=B4):=20329=20=C2=B7=20330=20=EC=9D=B4=EC=9D=8C?= =?UTF-8?q?=EB=A7=A4=20=E2=80=94=20=EC=8B=A4=EC=A0=9C=20=EA=B2=A9=EC=9E=90?= =?UTF-8?q?=20=C2=B7=20=EB=A7=9E=EC=B6=A4=20=EC=9D=91=EB=8B=B5=EC=9D=84=20?= =?UTF-8?q?=ED=99=94=EB=A9=B4=20=EB=8F=84=EC=9A=B0=EB=AF=B8(Hopyo=5FSheet?= =?UTF-8?q?=20=C2=B7=20Hopyo=5FView)=EC=97=90=20=EB=A8=B9=EC=9D=B4?= =?UTF-8?q?=EB=8A=94=20=EC=8B=9C=ED=97=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - helper_m02_hopyo_seam.cjs · test_m02_hopyo_seam — 행 → 호표(25 빈 줄) · 영역 B20:O24 · 단산93 행 · 엑셀 근거 줄 · 근거 꼬리표 · 근거 맞음 대상(GC000083 · GC000143) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Ji4nuyLVHcxrXWHTdb5nRD --- resources/tester/helper_m02_hopyo_seam.cjs | 55 ++++++++++++++++++++++ resources/tester/test_m02_hopyo_seam.py | 47 ++++++++++++++++++ 2 files changed, 102 insertions(+) create mode 100644 resources/tester/helper_m02_hopyo_seam.cjs create mode 100644 resources/tester/test_m02_hopyo_seam.py diff --git a/resources/tester/helper_m02_hopyo_seam.cjs b/resources/tester/helper_m02_hopyo_seam.cjs new file mode 100644 index 000000000..faa75042a --- /dev/null +++ b/resources/tester/helper_m02_hopyo_seam.cjs @@ -0,0 +1,55 @@ +/* M02 일위대가 실무자료 이음매(329 · 330) 헬퍼 — 실제 서버 응답(격자 · 호표 맞춤)을 + * `M02_MasterTemplete_Hopyo_Sheet.ts`(행 → 호표 · 영역 A1) · `M02_MasterTemplete_Hopyo_View.ts` + * (근거 꼬리표 · 엑셀 근거 줄 · 근거 맞음 대상)에 먹임 · + * stdin = {grid, lines} · stdout = {at, span, rows, lines: [{이름, source, tag, ok, pick}]} JSON. */ +const fs = require("fs"); +const path = require("path"); +const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript")); + +const ROOT = path.join(__dirname, "..", ".."); +const STUBS = { "@config/config_frontend": { API_BASE_URL: "/api" } }; +const STUB_FILES = ["M02_MasterTemplete_Labor_Pick", "M02_Material_Pick"]; +const stub = new Proxy({}, { get: (_t, k) => (k === "currentLanguageIndex" ? () => 0 : () => {}) }); +const cache = new Map(); +function loadTs(file) { + const full = path.resolve(file); + if (cache.has(full)) return cache.get(full); + const js = ts.transpileModule(fs.readFileSync(full, "utf8"), { + compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 }, + }).outputText; + const box = { exports: {} }; + cache.set(full, box.exports); + const localRequire = (request) => { + if (STUBS[request]) return STUBS[request]; + if (STUB_FILES.some((name) => request.endsWith(name))) return stub; + if (request.startsWith(".") && !request.endsWith(".css")) + return loadTs(`${path.resolve(path.dirname(full), request)}.ts`); + return stub; + }; + new Function("exports", "module", "require", js)(box.exports, box, localRequire); + return box.exports; +} + +const input = JSON.parse(fs.readFileSync(0, "utf8")); +const M02 = path.join(ROOT, "M02_MasterTemplete"); +const sheet = loadTs(path.join(M02, "M02_MasterTemplete_Hopyo_Sheet.ts")); +const view = loadTs(path.join(M02, "M02_MasterTemplete_Hopyo_View.ts")); +const { grid } = input; +const ilwi = grid.책.시트.find((s) => s.이름 === "일위대가"); +const three = grid.호표.find((h) => h.번 === 3); +const out = { + at: [19, 20, 22, 24, 25, 26].map((row) => sheet.hopyoAtRow(grid.호표, row)), + span: sheet.spanText(three.행, sheet.lastColumn(ilwi)), + rows: sheet.rowsText(grid.단산.find((b) => b.번 === 93).행), + lines: input.lines.map((line) => { + const top = (line.맞춤.후보 ?? [])[0]; + return { + 이름: line.이름, + source: view.sourceText(line.근거), + tag: view.basisTag(top?.근거), + ok: view.basisOk(top?.근거), + pick: view.basisPick(line)?.로직 ?? null, + }; + }), +}; +process.stdout.write(JSON.stringify(out)); diff --git a/resources/tester/test_m02_hopyo_seam.py b/resources/tester/test_m02_hopyo_seam.py new file mode 100644 index 000000000..0fd197766 --- /dev/null +++ b/resources/tester/test_m02_hopyo_seam.py @@ -0,0 +1,47 @@ +"""일위대가 실무자료 이음매(PLAN 329 · 330) — 실제 서버 응답(북부청 격자 · 호표 맞춤)을 +화면 도우미(`helper_m02_hopyo_seam.cjs` → Hopyo_Sheet · Hopyo_View)에 먹임. + +행 22 → 제3호표 · 25 빈 줄 · 26 → 제4호표 · 영역 B20:O24 · 단산93 행 4229:4244 · +엑셀 근거 줄 · 근거 꼬리표 · 근거 맞음 대상. +""" + +import json +import os +import subprocess + +from resources.tester import test_m02_hopyo_excel as base +from resources.tester.test_m02_hopyo_excel import URL, _post + +HERE = os.path.dirname(os.path.abspath(__file__)) +client = base.client # 공용 고정물(임시 층) + + +def _node(payload: dict) -> dict: + proc = subprocess.run( + ["node", os.path.join(HERE, "helper_m02_hopyo_seam.cjs")], + input=json.dumps(payload, ensure_ascii=False), + capture_output=True, + text=True, + encoding="utf-8", + check=False, + ) + assert proc.returncode == 0, proc.stderr + return json.loads(proc.stdout) + + +def test_실제_응답_화면_도우미(client): + assert _post(client).status_code == 200 + grid = client.get("/api/m02/layers/personal/hopyo-grid/HP-000001").json() + lines = [] + for no in (3, 12, 38, 58): + res = client.get(f"{URL.format('personal')}/HP-000001/{no}").json() + lines += [x for x in res["호표"]["줄"] if x.get("근거")] + got = _node({"grid": grid, "lines": lines}) + assert got["at"] == [None, 3, 3, 3, None, 4] # 25행 = 호표 사이 빈 줄 + assert got["span"] == "B20:O24" and got["rows"] == "4229:4244" + by = {x["이름"]: x for x in got["lines"]} + dig = by["구조물터파기(인력100%)"] + assert dig["source"] == "출처 건설품셈 3-2-1 · 직종 특별인부 · 단가산출 4229–4244행" + assert dig["tag"] == "조항 건설품셈 공통 3-2-1 · 직종 특별인부 · 단위 같음" + assert dig["ok"] is True and dig["pick"] == "GC000083" + assert by["비탈면 보호공"]["pick"] == "GC000143" From 1a029141f8a8ea7e9374f49bb3f3fa97c4f55bc6 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 15 Oct 2026 12:57:25 +0900 Subject: [PATCH 37/81] =?UTF-8?q?fix(=EC=9D=BC=EC=9C=84=EB=8C=80=EA=B0=80?= =?UTF-8?q?=EC=8B=A4=EB=AC=B4):=20330=20=EB=8D=A7=EA=B3=A0=EC=B9=A8=202=20?= =?UTF-8?q?=E2=80=94=20=ED=99=94=EB=A9=B4=20=EA=B7=BC=EA=B1=B0=20=EB=A7=9E?= =?UTF-8?q?=EC=9D=8C=20=ED=8C=90=EC=A0=95=EC=9D=84=20=EC=A1=B0=ED=95=AD=20?= =?UTF-8?q?=EA=B0=99=EC=9D=8C=20=C2=B7=20=EB=8B=A8=EC=9C=84=20=C2=B7=20?= =?UTF-8?q?=EC=A7=81=EC=A2=85(=EC=A4=84=20=EC=A7=81=EC=A2=85=EC=9D=B4=20?= =?UTF-8?q?=EB=B3=B4=ED=86=B5=EC=9D=B8=EB=B6=80=EB=BF=90=20=C2=B7=20?= =?UTF-8?q?=EC=97=86=EC=9C=BC=EB=A9=B4=20=EC=95=88=20=EB=B4=84)=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Hopyo_View basisOk(후보 근거, 줄 근거) — 앞 마디 같음은 근거 맞음 아님 · 엑셀 직종 없는 줄(기계)도 조항 같음 + 단위면 맞음 · basisPick · 고르기 창 꼬리표가 줄 근거를 넘김 - 시험 screen(앞 마디 · 보통인부뿐 · 직종 없음) · seam(북부청 실제 응답 1순위 근거 맞음 123 = 서버 기준 · 앞 마디 0) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Ji4nuyLVHcxrXWHTdb5nRD --- .../M02_MasterTemplete_Hopyo_View.ts | 16 ++++++++++------ resources/tester/helper_m02_hopyo_pick.cjs | 12 +++++++++--- resources/tester/helper_m02_hopyo_seam.cjs | 12 ++++++++++-- resources/tester/test_m02_hopyo_screen.py | 3 ++- resources/tester/test_m02_hopyo_seam.py | 12 ++++++++++-- 5 files changed, 41 insertions(+), 14 deletions(-) diff --git a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_View.ts b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_View.ts index 63a5eef75..4af98e977 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_View.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_View.ts @@ -117,9 +117,13 @@ export function memoryOf( return null; } -/** 근거 맞음 — 조항 · 직종 · 단위 모두 맞음(330 [근거 맞음 모두 확정]) */ -export const basisOk = (b: HopyoBasisHit | undefined): boolean => - !!b && b.조항.length > 0 && b.직종.length > 0 && b.단위; +/** 근거 맞음(330 [근거 맞음 모두 확정]) — 조항 같음(앞 마디 아님) · 단위 맞음 · 직종 겹침 + * (줄 근거 직종이 보통인부뿐이거나 없으면 — 기계 등 — 직종은 안 봄) */ +export const basisOk = (b: HopyoBasisHit | undefined, line?: HopyoLineBasis): boolean => + !!b && + b.까닭.startsWith("조항 같음") && + b.단위 && + (b.직종.length > 0 || !(line?.직종 ?? []).some((x) => x !== "보통인부")); /** 후보 근거 꼬리표 — 「조항 건설 공통 3-2-1 · 직종 특별인부 · 단위 같음」 · 근거 없으면 "" */ export const basisTag = (b: HopyoBasisHit | undefined): string => @@ -149,8 +153,8 @@ export const sourceText = (b: HopyoLineBasis | undefined): string => export function basisPick(line: HopyoLine): HopyoTarget | null { const m = line.맞춤; if (m.상태 !== "후보") return null; - if (m.대상 && basisOk(m.대상.근거)) return m.대상; - return (m.후보 ?? []).find((c) => basisOk(c.근거)) ?? null; + if (m.대상 && basisOk(m.대상.근거, line.근거)) return m.대상; + return (m.후보 ?? []).find((c) => basisOk(c.근거, line.근거)) ?? null; } export function buildHopyoView(key: string, opt: HopyoViewOptions): HTMLElement { @@ -450,7 +454,7 @@ export function buildHopyoView(key: string, opt: HopyoViewOptions): HTMLElement if (tag && c.근거) button.append( el("span", { - className: `m02-hopyo__basis${basisOk(c.근거) ? " is-ok" : ""}`, + className: `m02-hopyo__basis${basisOk(c.근거, line.근거) ? " is-ok" : ""}`, text: tag, attrs: { title: `${c.근거.까닭} · 점수 ${c.근거.점수}` }, }), diff --git a/resources/tester/helper_m02_hopyo_pick.cjs b/resources/tester/helper_m02_hopyo_pick.cjs index 78c8b60a9..fb5daaa03 100644 --- a/resources/tester/helper_m02_hopyo_pick.cjs +++ b/resources/tester/helper_m02_hopyo_pick.cjs @@ -94,14 +94,20 @@ function loadTs(file) { 직종: ["특별인부"], 단위: true, 점수: 1.65, - 까닭: "조항", + 까닭: "조항 같음 · 직종 특별인부 · 단위 맞음", }; const half = { ...hit, 직종: [] }; - const line = (상태, 대상, 후보) => ({ 맞춤: { 상태, 대상, 후보 } }); + const trade = { 출처: [["건설품셈", "3-2-1"]], 직종: ["특별인부", "보통인부"], 행: [1, 1] }; + const plain = { ...trade, 직종: ["보통인부"] }; + const line = (상태, 대상, 후보) => ({ 근거: trade, 맞춤: { 상태, 대상, 후보 } }); const a = { 로직: "GF000101", 근거: half }; const b = { 로직: "GC000083", 근거: hit }; return { - ok: [hit, half, { ...hit, 단위: false }, undefined].map(view.basisOk), + ok: [hit, half, { ...hit, 단위: false }, { ...hit, 까닭: "앞 마디 같음" }, undefined].map( + (b) => view.basisOk(b, trade), + ), + // 엑셀 직종이 보통인부뿐 · 없음(기계) = 직종 안 봄 + plain: [view.basisOk(half, plain), view.basisOk(half, { ...trade, 직종: [] })], tag: [hit, half, undefined].map(view.basisTag), source: [ view.sourceText({ 출처: [["건설표준품셈", "3-2-1"]], 직종: ["특별인부"], 행: [93, 101] }), diff --git a/resources/tester/helper_m02_hopyo_seam.cjs b/resources/tester/helper_m02_hopyo_seam.cjs index faa75042a..55616cebd 100644 --- a/resources/tester/helper_m02_hopyo_seam.cjs +++ b/resources/tester/helper_m02_hopyo_seam.cjs @@ -1,7 +1,8 @@ /* M02 일위대가 실무자료 이음매(329 · 330) 헬퍼 — 실제 서버 응답(격자 · 호표 맞춤)을 * `M02_MasterTemplete_Hopyo_Sheet.ts`(행 → 호표 · 영역 A1) · `M02_MasterTemplete_Hopyo_View.ts` * (근거 꼬리표 · 엑셀 근거 줄 · 근거 맞음 대상)에 먹임 · - * stdin = {grid, lines} · stdout = {at, span, rows, lines: [{이름, source, tag, ok, pick}]} JSON. */ + * stdin = {grid, lines, all} · stdout = {at, span, rows, lines: [{이름, source, tag, ok, pick}], + * count: {top, pick, half}} JSON — count = 「후보」 줄 1순위 근거 맞음 · 근거 맞음 대상 · 그 가운데 앞 마디. */ const fs = require("fs"); const path = require("path"); const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript")); @@ -47,9 +48,16 @@ const out = { 이름: line.이름, source: view.sourceText(line.근거), tag: view.basisTag(top?.근거), - ok: view.basisOk(top?.근거), + ok: view.basisOk(top?.근거, line.근거), pick: view.basisPick(line)?.로직 ?? null, }; }), }; +const open = (input.all ?? []).filter((line) => line.맞춤.상태 === "후보"); +const picks = open.map((line) => view.basisPick(line)).filter(Boolean); +out.count = { + top: open.filter((line) => view.basisOk((line.맞춤.후보 ?? [])[0]?.근거, line.근거)).length, + pick: picks.length, + half: picks.filter((c) => !c.근거.까닭.startsWith("조항 같음")).length, +}; process.stdout.write(JSON.stringify(out)); diff --git a/resources/tester/test_m02_hopyo_screen.py b/resources/tester/test_m02_hopyo_screen.py index c844f2d7d..3eef9079e 100644 --- a/resources/tester/test_m02_hopyo_screen.py +++ b/resources/tester/test_m02_hopyo_screen.py @@ -193,7 +193,8 @@ def test_엑셀_보기_창_여는_자리와_늦게_불러옴(): def test_고르기_창_근거_꼬리표와_근거_맞음(): basis = _run()["basis"] - assert basis["ok"] == [True, False, False, False] # 조항 · 직종 · 단위 모두 + assert basis["ok"] == [True, False, False, False, False] # 조항 같음 · 직종 · 단위 모두 + assert basis["plain"] == [True, True] # 줄 직종이 보통인부뿐 · 없음 = 직종 안 봄 assert basis["tag"] == [ "조항 건설 공통 3-2-1 · 직종 특별인부 · 단위 같음", "조항 건설 공통 3-2-1 · 단위 같음", diff --git a/resources/tester/test_m02_hopyo_seam.py b/resources/tester/test_m02_hopyo_seam.py index 0fd197766..54ac54462 100644 --- a/resources/tester/test_m02_hopyo_seam.py +++ b/resources/tester/test_m02_hopyo_seam.py @@ -2,7 +2,8 @@ 화면 도우미(`helper_m02_hopyo_seam.cjs` → Hopyo_Sheet · Hopyo_View)에 먹임. 행 22 → 제3호표 · 25 빈 줄 · 26 → 제4호표 · 영역 B20:O24 · 단산93 행 4229:4244 · -엑셀 근거 줄 · 근거 꼬리표 · 근거 맞음 대상. +엑셀 근거 줄 · 근거 꼬리표 · 근거 맞음 대상 · 북부청 「후보」 줄 1순위 근거 맞음 = 서버 기준 123 · +앞 마디 같음 0(330 덧고침 2). """ import json @@ -36,7 +37,13 @@ def test_실제_응답_화면_도우미(client): for no in (3, 12, 38, 58): res = client.get(f"{URL.format('personal')}/HP-000001/{no}").json() lines += [x for x in res["호표"]["줄"] if x.get("근거")] - got = _node({"grid": grid, "lines": lines}) + every = [] + for one in grid["호표"]: + res = client.get(f"{URL.format('personal')}/HP-000001/{one['번']}").json() + every += [x for x in res["호표"]["줄"] if x["갈래"] == "단가산출"] + got = _node({"grid": grid, "lines": lines, "all": every}) + assert got["count"]["top"] == 123 and got["count"]["half"] == 0 + assert got["count"]["pick"] >= 123 assert got["at"] == [None, 3, 3, 3, None, 4] # 25행 = 호표 사이 빈 줄 assert got["span"] == "B20:O24" and got["rows"] == "4229:4244" by = {x["이름"]: x for x in got["lines"]} @@ -45,3 +52,4 @@ def test_실제_응답_화면_도우미(client): assert dig["tag"] == "조항 건설품셈 공통 3-2-1 · 직종 특별인부 · 단위 같음" assert dig["ok"] is True and dig["pick"] == "GC000083" assert by["비탈면 보호공"]["pick"] == "GC000143" + assert by["흙깍기(토사)"]["ok"] is True # 엑셀 직종 없는 줄(기계) = 조항 같음 + 단위 From 31e6adf7de613c390a4e90daa5a0cb90fcf10616 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 15 Oct 2026 15:12:15 +0900 Subject: [PATCH 38/81] =?UTF-8?q?chore(=EC=9A=94=EA=B5=AC):=20225=20?= =?UTF-8?q?=EB=8C=80=EC=8B=9C=EB=B3=B4=EB=93=9C=20=C2=B7=20=EC=8B=9C?= =?UTF-8?q?=EC=8A=A4=ED=85=9C=20=EC=84=A4=EC=A0=95=20=E2=98=B0=20=EB=B0=94?= =?UTF-8?q?=EB=A1=9C=EA=B0=80=EA=B8=B0=20=C2=B7=20CAD=20=EC=9E=85=EB=A0=A5?= =?UTF-8?q?=20=EB=B3=B4=EA=B0=95=20=C2=B7=20=EC=99=B8=EB=B6=80=20=EC=9D=BC?= =?UTF-8?q?=EC=9C=84=EB=8C=80=EA=B0=80=20=EC=9D=B4=EB=A6=84=20=EB=A7=9E?= =?UTF-8?q?=EC=B6=A4=20=EA=B2=80=ED=86=A0=20=EC=9B=90=EB=AC=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_01C6BNBFXFndbqYxu6sZuknK --- docs/raw/요구_1010_2/사용자요구_1010_2.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/raw/요구_1010_2/사용자요구_1010_2.md b/docs/raw/요구_1010_2/사용자요구_1010_2.md index a3e1691fb..4af9ac3fb 100644 --- a/docs/raw/요구_1010_2/사용자요구_1010_2.md +++ b/docs/raw/요구_1010_2/사용자요구_1010_2.md @@ -274,3 +274,9 @@ 7. 내장 캐드프로그램에는 오토캐드 프로그램을 참고하여 만들었음. 기능 중 명령어 중에 연속 명령어로 제어할 수 있는 기능이 필요. 예를들어서 산 단축키 입력> 각도지정> 길이입력 등임. 연속적으로 키보드로 작동하는 기능임. 이건 인터넷 조사하여 오토캐드에서 어떤식으로 제어기능을 제공하는지 분석필요함. 8. 우리 프로그램에는 마우스로 팬, 줌인아웃, 선택 기능들이 사용자가 제어할 수 있는 기능이 있음. 일체반영을 위해 햄버거 메뉴 안에 설정버튼을 추가하고 별도모달창에는 탭으로 구분하여 마우스 제어방법이나 오토캐드의 단축키, 다른 곳에서의 단축키 설정, 나중에는 페이지들 기본 옵션값들을 사용자가 조절할 수 있는 기능을 부여하려고 함. (기본값은 지금은 아님. 내가 별도 요청) 9. 신규 계획서를 작성하기 전 현재의 코드나 외부정보를 먼저 정밀 분석하고 진행. 신규계획서를 작성 후에는 업무를 분배하여 진행하고 너는 최종 완료시점에 사용자처럼 브라우져 제어하고 보고서를 아티팩트로 공유. +225. (원문 · 10-15) 추가수정 내용 +1. 대시보드의 햄버거 메뉴에는 좌측에 프로젝트 내부의 진행단계처럼 바로가기 버튼을 추가하고 페이지 스크롤 추가 +2. 시스템 설정컨테이너로 접근하는 4개의 페이지에는 햄버거메뉴에도 진행단계 처럼 바로 이동하는 버튼 추가 +3.오토캐드의 프로그램이 추가 보강 입력방법이 없는지 확인. +4. 외부에서 일위대가 가져오기 기능에서 이름이 달라 매칭이 안되는 문제를 개선 방법이 있는지 확인. +5. 3번과 4번은 코드 요청이 아니라 상세검토 요청임. 코드 작업은 검토 결과 보고 판단할 예정 From 422c449eb9360ec0c94456b23db3f6fce9c46e7e Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 15 Oct 2026 15:36:42 +0900 Subject: [PATCH 39/81] =?UTF-8?q?chore(=EC=A1=B0=EC=82=AC):=20334=20?= =?UTF-8?q?=E2=98=B0=20=EB=B0=94=EB=A1=9C=EA=B0=80=EA=B8=B0=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=A1=B0=EC=82=AC=20=E2=80=94=20=EB=8C=80=EC=8B=9C?= =?UTF-8?q?=EB=B3=B4=EB=93=9C=20=EA=B5=AC=EC=97=AD=20=C2=B7=20=EC=8B=9C?= =?UTF-8?q?=EC=8A=A4=ED=85=9C=20=EC=84=A4=EC=A0=95=204=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01C6BNBFXFndbqYxu6sZuknK --- .../2026-10-15_334_햄버거메뉴_바로가기.md | 230 ++++++++++++++++++ 1 file changed, 230 insertions(+) create mode 100644 docs/raw/조사/2026-10-15_334_햄버거메뉴_바로가기.md diff --git a/docs/raw/조사/2026-10-15_334_햄버거메뉴_바로가기.md b/docs/raw/조사/2026-10-15_334_햄버거메뉴_바로가기.md new file mode 100644 index 000000000..f4b075e43 --- /dev/null +++ b/docs/raw/조사/2026-10-15_334_햄버거메뉴_바로가기.md @@ -0,0 +1,230 @@ +# 요구 225-1 · 225-2 ☰ 바로가기 — 코드 조사 (PLAN 334) + +- 기준: `main_laptop_1` 31e6adf7d · 읽기만 · 근거 = 파일:줄 · 화면 판정은 코드로만(브라우저 · 서버 안 씀). +- 원문(요구 225 · 10-15): ① 대시보드 ☰ 좌측에 진행단계처럼 바로가기 + 페이지 스크롤 ② 시스템 설정 컨테이너로 가는 4 페이지 ☰ 에도 진행단계처럼 바로 이동 단추. + +## 0. 결론 요약 + +1. ☰ = 공용 한 벌(`A00_Common/app_shell.ts` `buildDrawer` 149-222) · 대시보드 ☰ 와 프로젝트 ☰ 는 같은 코드 · 다른 점은 「진행 단계」 칸을 페이지가 채우느냐(`setDrawerSteps`)뿐 · 서랍은 한 열(왼쪽 · 오른쪽 두 열 없음). +2. 진행단계 = `createStepBar`(`ui_template/ui_template_workflow_layout.ts` 57-164) · 🏠 대시보드 + 7단계 · ☰ 는 진행 판에 그린 **그 요소**를 열 때 빌려 감. +3. 시스템 설정 카드 넷(시스템 관리자) = M01 마스터 데이터 + M02 한 페이지의 `open=구조물조합 · 옵션 · 도면` · 그 밖 역할은 구조물 조합 하나 · 지금 그 페이지들 ☰ 의 진행 단계 칸은 비어 숨음. +4. 추천 해석: 대시보드 ☰ 진행 단계 자리에 「대시보드 컨테이너 바로가기」(역할별 화면에 있는 컨테이너 그대로 · 화면 차례) · 누르면 서랍 닫고 그 컨테이너로 굴림 · 본문 구조는 그대로. +5. 페이지 스크롤: 대시보드 본문 · ☰ 서랍 모두 지금도 굴러감(막는 코드 없음) → 「페이지 스크롤」 = 바로가기 누름 → 그 컨테이너로 굴림 + ☰ 자리 비킴(scroll-margin-top). +6. M01 · M02 ☰ = `createStepBar` 를 인자만 바꿔 재사용(🏠 + 카드 넷 · 지금 칸 강조) · 서랍이 「집 없는 요소」를 받게 `app_shell.ts` 두 줄. +7. 함정 셋: `createStepBar` 「둘째 칸 = 시스템 관리자 전용」이 칸 번호로 박힘 · M02 → M02 이동은 머리 가드가 안 물음 · B01 CSS 이미 789줄. +8. 최소안: 기반 2줄 + 몫 A(대시보드) 2 파일 · 몫 B(설정 4 페이지) 새 파일 1 + 4 파일 · 시험 1 고침 + 새 시험 1 · 한 창이 차례로가 나음. + +## 1. ☰ 메뉴 코드 + +### 한 벌 · 공용 + +- 그리기 = `A00_Common/app_shell.ts` `buildDrawer()` 149-222 — 모든 쪽(A · B · M) 같은 함수 · 페이지마다 따로 없음. +- 셸은 주소가 바뀔 때마다 새로 그림 — `A00_Common/main.ts` 67-80(`renderShell` → `renderCurrentRoute`) · `app_shell.ts` 323-337 · 325 에서 진행 단계 맡김(`drawerSteps`) 비움 → 새 페이지가 다시 맡김. B05↔B06 은 보기만 바꿔 셸을 안 다시 그림(`main.ts` 72-75). +- 대시보드 ☰ vs 프로젝트 ☰ = 같은 코드의 같은 갈래 · 다른 점은 「진행 단계」 칸뿐 — 프로젝트 쪽(B04~B09)은 `createWorkflowLayout` 이 `setDrawerSteps(progressContent)`(`ui_template_workflow_layout.ts` 202-203) · 대시보드(일반 레이아웃 · `B01_Dashboard/B01_Dashboard_UI_Page.ts` 315-320)는 안 맡겨 칸이 비고 CSS 로 숨음(`app_shell.ts` 486). +- B·M 쪽 표시: `html.is-bm-route` = 주소가 b·m + 숫자(`main.ts` 68-71 · `ui_template/ui_template_native_menu.ts` 10) → 위 줄 걷음(`app_shell.ts` 504-512) · ☰ 만 왼쪽 위에 띄움(513-525) · 서랍은 왼쪽에서 열림(526-531) · 토큰 `A00_Common/device_context.css` 89-98(`--app-header-h` 0 · `--app-burger-w` 36 · `--app-burger-inset` 52). + +### 칸 구성(한 열 · 위→아래 · `app_shell.ts` 185-220) + +| 차례 | 칸(class) | 내용 | 근거 | +| --- | --- | --- | --- | +| 1 | `.app-drawer__brand` | 로고 → A01 홈(대시보드 아님) | 102-117(115) · 186-188 | +| 2 | `.app-drawer__steps` | 진행 단계(맡긴 쪽만 · 비면 숨김) | 119-125 · 162-170 · 486 | +| 3 | `.app-drawer__nav` | 공개 메뉴 다섯(프로그램 · 회사소개 · 소식 · 교육 · 기술지원 → A 쪽) | 127-143 · 189-191 | +| 4 | 묶음(`.app-drawer__group`) | [EN/한] [☾/☀] [설정](332 · 처음 누를 때 `app_settings` 불러옴) | 192-213 | +| | `.app-drawer__auth` | [내 이름 → 대시보드] [로그아웃] | 214-216 · 262-311(273) | +| | `.app-drawer__touch` | 터치 모드 자동 · 켬 · 끔 | 224-259 · 217-220 | + +- 시험 `test_device_drawer.py` 76-81 이 이 차례(brand · steps · nav · settings)를 잼. + +### 여닫기 + +- ☰ 누름 = 토글(171) · 덮개 누름 = 닫힘(172) · Esc = 닫힘(173 · ☰ 초점에서도). +- 로고 · 진행 단계 · 메뉴 · 로그인 · [설정] 단추를 누르면 닫힘 · 언어 · 테마 · 터치는 열린 채(174-183). +- 진행 단계 빌림: 열 때 서랍으로 옮기고 닫을 때 제자리(진행 판)로(164-170) · 맡길 때 부모(집)가 없으면 받지 않음(124 `node?.parentElement ? … : null`). + +### 크기 · CSS(`SHELL_CSS` 344-537 · 주입 539-545) + +- 서랍: fixed · 위 = `--app-header-h`(B·M 0) · 아래 0 · 폭 `min(320px, 100vw)` · 안쪽 16 · 칸 사이 16 · **`overflow-y: auto`**(451-465) · 묶음 사이 선(482-485). +- 덮개: fixed · 반투명 보라(445-450). +- ☰: 36×36(손가락 높이 44) · 왼쪽 위 8,8 · 그림자(513-525). +- 머리 z = `z-dropdown + 1` = 201(504-509) → 좌측 패널 판(z 200 · `ui_template_overlay.css` 8) 위. +- 프로젝트 쪽 떠 있는 진행 판은 B·M 에서 숨김(`ui_template_overlay.css` 544-547) → B·M 의 진행 단계는 ☰ 안에만. +- 최신 모양 = 332-2 [설정] 단추 포함(sub2 aaecfa343 · HEAD 에 합쳐짐 · `A00_Common/app_settings.ts` 91~). + +## 2. 진행단계 바로가기 코드 + +- 그리는 함수: `createStepBar(steps, activeStep, options)` `ui_template_workflow_layout.ts` 57-164 · 세로형 `is-vertical`(65) · 프로젝트 쪽은 `createWorkflowLayout` 이 부름(184-192 · `homeButton: true` · `WORKFLOW_STEP_ICONS`). + +| 원천 | 값 | 근거 | +| --- | --- | --- | +| 표시 이름 | 파일입력 · 전처리 · 종단설계 · 횡단설계 · 상세설계 · 수량산출 · 원가계산 | `A00_Common/b_page_scaffold.ts` 139-149 · `ui_template_locale_b1.ts` 231 · `ui_template_locale_common.ts` 82-87 | +| 길 | b03-file-input · b04-preprocess · b05-profile · b06-section · b07-design-detail · b08-quantity · b09-estimation | `A00_Common/b_workflow_nav.ts` 24-32 | +| 아이콘 | 📁 🗺️ 🛣️ 📐 ∑ 🏗️ 📄 | `ui_template_workflow_layout.ts` 55 | +| 맨 위 | 🏠 대시보드 → b01-account(단계 아님 · 늘 켜짐 · 아래 선) | 66-83 · `ui_template_locale_common.ts` 54 · `ui_template_workflow_layout.css` 65-71 | + +- 지금 단계 강조: `is-active`(88 · css 83-86) · B05↔B06 은 `setActiveStep`(217-218). +- 잠금: 단계 상태 NOT_STARTED · STALE 면 막힘(지금 단계는 열림 · 103-117) · 개발(DEV)은 막힌 단계도 열고 표시(113 · 148-153) · 상태 테두리 색(css 93-108) · B07 은 운영에서 도면 다 확정 전 수량 · 원가 막음(`B07_DesignDetail_UI_Page.ts` 600-606). +- 역할: 둘째 칸(index 1 = 전처리)만 시스템 관리자 전용 — `fetchDashboardMe` 로 역할 보고 끔 · 풍선 「시스템 관리자 전용 단계입니다.」(119-129 · 9-14). ⚠ **칸 번호**로 박힘(길 아님) — 다른 목록에 쓰면 회사 관리자 · 일반에서 둘째 칸이 꺼짐. +- 누를 수 있는 조건: 켜짐 + `routes[index]` 있음(155-159) — 길 배열을 안 주면 누름이 안 걸림. +- 프로젝트 없이: 못 씀 — 단계 화면은 기억한 프로젝트로 엶(`router.ts` 107-116 · 223-233 「프로젝트 없음」 안내) · 회사 없으면(시스템 관리자 빼고) 대시보드로(`router.ts` 206-221) · 누름 = `goToWorkflowStage(projectId, 길)`(`b_workflow_nav.ts` 96-111) · projectId 없으면 아무것도 안 함(`b_page_scaffold.ts` 128-130 · `B07_DesignDetail_UI_Page.ts` 601 · `design_page.ts` 174-175 토스트). +- 대시보드 프로젝트 표의 단계 막대도 같은 함수(compact · `B01_Dashboard_UI_Projects.ts` 56-67). +- ⚠ 덧 발견(이번 밖): B03(파일입력)은 일반 레이아웃 + 오버레이를 직접 조립하고 `setDrawerSteps` 를 안 부름(`B03_FileInput_UI_Page.ts` 641-675) · B·M 에선 떠 있는 판도 숨음(`ui_template_overlay.css` 544-547) → **B03 ☰ 에 진행 단계 없음**(10-13 ☰ 이관 3fc50044e · fbe0f982d 가 B03 을 안 건드림) · 한 줄 고침 후보. + +## 3. 시스템 설정 컨테이너 · 4 페이지 + +- 컨테이너: `buildSystemSettingsSection` `B01_Dashboard_UI_Page.ts` 222-232(모든 역할 · 한 줄 통째 · 제목 눌러 접힘 · 시스템 관리자는 회사 알약) · 카드 = `buildSystemSettingsPanel` 165-200 · 카드 꼴 `settingCard` 149-163. +- 자리: 시스템 관리자 = 리소스 현황 아래(246) · 회사 관리자 · 일반 = 맨 위(281 · 294). + +| 카드(시스템 관리자 · 178-188) | 이동 | 파일 | +| --- | --- | --- | +| 마스터 데이터 | `#/m01-master-data` | `M01_MasterData/M01_MasterData_UI_Page.ts` | +| 구조물 조합 | `#/m02-master-template?open=구조물조합&root=personal`(회사 있음) · `master`(없음) `[&company=고른 회사]` | `M02_MasterTemplete/M02_MasterTemplete_UI_Page.ts` | +| 구조물 옵션 | `…?open=옵션&root=master[&company=]` | 같은 M02 | +| 도면 양식 | `…?open=도면&root=master[&company=]` | 같은 M02 | + +- 이름 `ui_template_locale_m1.ts` 7 · `ui_template_locale_m2.ts` 7-9 · 설명 `ui_template_locale_b3.ts` 142-147 · 회사 인자 170-171. +- 그 밖 역할(190-198): 구조물 조합 카드 하나 — 회사 있으면 personal(177) · 없으면 회사 관리자 · 회사 마스터 = company · 일반 = personal. +- 「4 페이지」 = 실제 길은 둘 — M02 셋은 한 페이지의 진입 인자 `open` 만 다름(`M02_MasterTemplete_Layer.ts` 19 · 39-52) → 좌측 패널 그 컨테이너 펼침(`M02_MasterTemplete_UI_Page.ts` 240 · `M02_MasterTemplete_UI_Side.ts` 342-347) · 옵션 · 도면 컨테이너는 마스터 층에만(`UI_Side` 154-160). +- 역할: M01 = 시스템 관리자만(아니면 토스트 + 대시보드로 · M01 page 48-56) · M02 = 로그인만 · 고치기는 층 권한(`Layer` 99-113). +- 지금 그 페이지들 ☰: 진행 단계 칸 빔(숨음) — M01 · M02 는 `createWorkflowOverlays` 를 진행 판 없이 부름(M01 page 263-268 · M02 page 222-227 · `ui_template_overlay.ts` 270-282) · `setDrawerSteps` 없음 → ☰ = 로고(A01 홈) · 공개 메뉴 다섯 · [EN][☾][설정] · [이름 → 대시보드][로그아웃] · 터치 모드. 대시보드로 가는 길 = 이름 단추 하나뿐 · 설정 페이지끼리 오가는 단추 없음. + +## 4. 대시보드 ☰ 「좌측」 후보 + +대시보드 컨테이너 — 모두 `section()` = `.b01-dashboard__section` · 제목 h3 `.ui-card__title`(`ui_template_general_blocks.ts` 9-40) · `grid` 의 직속 자식: + +| 역할 | 컨테이너(화면 차례) | 근거 | +| --- | --- | --- | +| 시스템 관리자(10) | 리소스 현황 · 시스템 설정 · 프로젝트 · 임시 보관함 · 사용자 관리 · 가입 요청 · 회사 관리 · 기본정보 · 보안 · 시스템 로그 | 241-277 · 305-312 | +| 회사 관리자(8) | 시스템 설정 · 프로젝트 · 임시 보관함 · 팀원 관리 · 가입 요청 · 회사 정보 · 기본정보 · 보안 | 278-291 · 305-308 | +| 일반(6) | 시스템 설정 · 프로젝트 · 임시 보관함 · 회사 정보 · 기본정보 · 보안 | 292-308 | + +- 챗봇 단추 = 오른쪽 아래 떠 있음 · 컨테이너 아님(314 · B01 CSS 525). +- 대시보드에서 다른 화면 가는 곳: 시스템 설정 카드(M01 · M02) · 프로젝트 「+」(B02 · 249 · 283 · 296) · 프로젝트 표 단계 막대(B03~B09 · `Projects.ts` 56-67). + +**추천 해석(하나)** — ☰ 서랍 「진행 단계」 자리(로고 바로 아래 · 프로젝트 ☰ 와 같은 자리 · 같은 세로 단추 꼴)에 **대시보드 컨테이너 바로가기** · 그 역할 화면의 컨테이너 그대로 · 화면 차례 그대로 · 누르면 서랍 닫히고 그 컨테이너로 부드럽게 굴림. +- 「좌측에」 = 왼쪽에서 열리는 ☰ 서랍(`app_shell.ts` 526-531) · 「페이지 스크롤」 = 누르면 굴러감. +- 10-8 롤백 둘(해시 탭 2a2c4d0f6 → 1849a11b3 · 좌측 패널에서 고른 컨테이너만 보이기 d69686a0a → c72585016 · `docs/raw/plans/2026-10-08_plan_99_…` 11)은 컨테이너를 감춰 본문 구조를 바꿈 → 이 안은 본문 그대로 · ☰ 안 단추 + 굴림만. + +다른 해석(한 줄씩): +- B 다른 화면 가기(시스템 설정 4 · 새 프로젝트) — 본문 카드 · 「+」 와 겹침 · 「페이지 스크롤」 이 설명 안 됨. +- C 최근 프로젝트 · 그 단계로 — 프로젝트 고르기가 먼저 필요 · 표에 단계 막대가 이미 있음. +- D 「좌측에」 = 서랍 안 왼쪽 열(두 열) — 서랍 폭 320 이라 좁음 · 프로젝트 ☰ 와 꼴이 달라짐. +- E 「페이지 스크롤」 = 지금 안 굴러가서 — 5장 판정상 지금도 굴러감. + +## 5. 페이지 스크롤 판정 + +대시보드 본문 — 막는 코드 없음 → 문서가 굴러감: +- html · body = 여백만(`ui_template_theme.css` 471-475) · `A00_Common/index.html` 꾸밈 없음 · 출구 `.app-outlet` = 최소 높이만(`app_shell.ts` 396-401) · `.ui-general-layout` = `min-height: 100dvh`(`ui_template_general_layout.css` 1-5). +- overflow hidden · 고정 높이 · 100vh 잠금 없음 — 전역 html · body · #app · .app-outlet 규칙 검색 결과 위 셋뿐(B07 CAD `App.css` 의 html,body 는 iframe 별도 문서) · JS 로 body overflow 잠그는 곳 0. +- 휠 가로채기: 돋보기는 원이 보일 때만(`ui_template_loupe.ts` 210-220) · `touch_bridge` 는 마우스 · 휠 안 건드림. +- 안쪽 굴림만 따로: 표 4줄 뒤(`B01_Dashboard_UI_Common.ts` 15 · `ui_template_general_blocks.ts` 53~) · 챗봇 창(B01 CSS 559-570 · 610-618). + +☰ 서랍 — `overflow-y: auto` + 위 · 아래 고정(`app_shell.ts` 451-465) → 넘치면 서랍만 굴러감 · 시스템 관리자 바로가기 10개면 서랍 높이 약 900px 넘음 → 이 굴림이 일함 · 서랍 끝에서 뒤 페이지로 이어 굴림(overscroll) 막음은 없음. + +판정: 「페이지 스크롤 추가」 = 바로가기 누름 → 그 컨테이너로 굴림(`scrollIntoView` · smooth · block start). + +고칠 곳: +- 굴림 = `B01_Dashboard_UI_Page.ts`(바로가기 누름). +- ☰ 자리 비킴 = 맨 위로 굴린 컨테이너 제목이 왼쪽 위 ☰(52px)에 깔림(폭 1248 아래 · 카드 왼쪽 끝 24) → `html.is-bm-route .ui-general-block__section { scroll-margin-top: var(--app-burger-inset) }` 한 규칙 · 자리 = `ui_template_general_layout.css`(119줄 · 같은 ☰ 들여쓰기 규칙 115-119 곁) · B01 CSS(789줄 · 이미 700 넘음)는 손대지 않음. +- (선택) `app_shell.ts` `.app-drawer` 에 `overscroll-behavior: contain` 한 줄. + +## 6. 시스템 설정 4 페이지 ☰ + +- 꼴: 프로젝트 ☰ 와 같음 — 🏠 대시보드 + 카드 넷(마스터 데이터 · 구조물 조합 · 구조물 옵션 · 도면 양식) · 지금 칸 강조 · 누르면 그 페이지(M02 셋은 `open` 인자) · 그 밖 역할은 🏠 + 구조물 조합. +- 재사용: 됨 — `createStepBar(카드 이름들, 지금 칸, { orientation: "vertical", icons, routes: 카드 길들, homeButton: true, onStepClick })` 인자만. stages 없으면 모두 켜짐(114-115) · `homeButton` = 🏠 대시보드(68-83) · 둘째 칸 규칙은 시스템 관리자 넷엔 통과 · 다른 역할은 한 칸이라 안 걸림(대시보드 목록엔 걸림 → 8장 기반 a). +- 막는 것 둘: + 1. 서랍이 집 없는 요소를 안 받음(`app_shell.ts` 124) — M01 · M02 · B01 은 진행 판이 없음 → 두 줄 고침(집 없음 = 서랍이 늘 가짐 · 닫을 때 안 옮김 · 121-124 · 167-169). 대안(진행 판에 넣어 숨긴 채 집으로) = 떠 있는 판 · 끌기 · 저장 키가 헛되이 생김 → 안 권함. + 2. M02 → M02(다른 `open`) 이동은 머리 가드가 안 물음(`main.ts` 85 · 같은 길 = 그냥 다시 그림) → 저장 안 한 M02 고침이 말없이 사라짐. M02 자체 층 단추는 먼저 `main.confirmLeave()`(M02 page 99-106 · `M02_MasterTemplete_UI_Main.ts` 239-240 · 시험 `test_m02_layer_screen.py` 183) → 바로가기도 같은 길이면 `takeLeaveGuard()`(`router.ts` 101-105) 먼저 물음 · 다른 길(M02 → M01 · 🏠)은 `main.ts` 가 물음 · M01 은 초안이 sessionStorage 라 가드 없음(M01 page 7-8). +- 역할 규칙 원천은 한 곳으로: 지금 카드 규칙(165-200 · 177 comboRoot 가 까다로움)을 순수 목록 함수로 옮겨 카드 · ☰ 가 같이 씀(두 벌 금지). +- 지금 칸: M01 = 마스터 데이터 · M02 = `entry.open`(구조물조합 · 옵션 · 도면 · 「일위대가조합」은 강조 없음) · 페이지 안 좌측 패널에서 다른 컨테이너를 펼쳐도 강조는 안 따라감(진입 인자 기준 · 알려 둘 한계). +- company: 대시보드는 시스템 관리자 고른 회사를 해시에 실음(170-171) · M02 는 해시 없으면 sessionStorage `aislo.adminCompany` → 자기 회사(`Layer` 69-80) → M01 에서는 안 실어도 같음 · M02 에서는 지금 `entry.company` 를 이어 실음. + +## 7. 시험 · 줄 수 · 겹침 + +시험(`resources/tester`): + +| 파일 | 보는 것 | 서버 | +| --- | --- | --- | +| `test_device_drawer.py` | ☰ 위 줄 걷기 · 서랍 차례 · 진행 단계 빌림 · 단계 누름 이동 · Esc(B07 · A01) | 5174(sub1) · 꺼지면 건너뜀 | +| `test_app_settings.py` + `helper_app_settings.cjs` | ☰ [설정] 창 탭 · 저장 · 물음 | Node + 5177(sub2) | +| `test_b01_settings_cards.py` | 시스템 설정 카드 역할별(제목 · 설명 · 해시) — `settingCard` · `buildSystemSettingsPanel` 소스를 잘라 Node 로 돌림(26-62) · `buildPage` 차례 문자열(99-141) | Node | +| `test_197_1_dashboard_option_card.py` | 카드 해시 셋(위 `_cards` 씀) · M02 진입 · reveal | Node | +| `test_device_audit.py` | 모든 쪽 네 크기 점검(☰ 포함) | 5174 | +| `test_design_page_single.py` | workflow_layout 의 `setActiveStep` · `setTitle` 문자열(96-97) | 없음 | +| `test_m02_layer_screen.py` · `_combo_screen` · `_combo_table` · `_labor_tree` · `_precombo_scope` · `_structure_panel` | M02 page 소스 문자열 | 없음 | + +- 대시보드 바로가기 · 설정 4 페이지 ☰ 시험은 아직 없음. + +줄 수(700 제한): + +| 파일 | 지금 | 예상 | +| --- | --- | --- | +| `A00_Common/app_shell.ts` | 545 | +1~2 | +| `ui_template/ui_template_workflow_layout.ts` | 220 | 한 줄 바꿈 | +| `ui_template/ui_template_general_layout.css` | 119 | +3 | +| `B01_Dashboard/B01_Dashboard_UI_Page.ts` | 329 | +15~20(카드 함수는 줄어듦) | +| `B01_Dashboard/B01_Dashboard_UI_Style.css` | **789(이미 초과)** | 손대지 않음 | +| `M01_MasterData/M01_MasterData_UI_Page.ts` | 280 | +2 | +| `M02_MasterTemplete/M02_MasterTemplete_UI_Page.ts` | 244 | +2 | +| 새 `A00_Common/m_settings_nav.ts` | — | ~50 | + +겹침: +- 원격 가지 전부(origin/sub_laptop_1~8 · main_desktop_1 · sub_desktop_1 · CODEX · 안티그래비티 · dev)에 HEAD 대비 위 파일의 안 합친 커밋 0 · 랩탑 보조 여덟 폴더 미커밋 0(읽기 diff) · 데스크탑 미커밋은 못 봄. +- PLAN 열린 장 중 이 파일을 쓰는 장 없음 · 332(☰ [설정] · sub2)는 끝 · 합침. +- 주의: `app_shell.ts` · `ui_template_workflow_layout.ts` = 모든 B · M 쪽 공용(B04~B09 진행 단계) · B01 = CODEX 챗봇 영역(`Knowledge_Chat` · CSS 520~ · PLAN 「CODEX」 장 열린 줄 「추가 커밋은 제 워크트리에만」) — 이번엔 `B01_Dashboard_UI_Page.ts` 만 · 챗봇 파일 안 건드림. +- 225-3 · 225-4 는 검토만(코드 없음) → 겹침 없음. + +## 8. 구현 최소안 + +기반(몫 A 가 먼저 · 둘 다 씀): +- (a) `ui_template_workflow_layout.ts` 119 — `index === 1` → `options?.routes?.[index] === ROUTES.B04_PREPROCESS`(전처리 시스템 관리자 전용 = 길 기준 · 지금 부르는 곳 B01 표 · B03 · B04 · B05/06 · B07 · B08 · B09 · 초안 모두 `WORKFLOW_STEP_ROUTES` 라 그대로 · `ROUTES` 이미 import 2). +- (b) `app_shell.ts` 121-124 · 167-169 — 집 없는 요소 받음(`home` null → 닫을 때 안 옮김). + +몫 A — 대시보드(225-1): +- `B01_Dashboard_UI_Page.ts` — `buildPage` 에서 grid 채운 뒤 새 함수 하나: grid 자식 컨테이너마다 제목(`.ui-card__title`) → `createStepBar(제목들, -1, { orientation: "vertical", icons, routes: 제목들, onStepClick: i → 컨테이너.scrollIntoView({ behavior: "smooth", block: "start" }) })` → `setDrawerSteps(…)`. 역할 규칙을 다시 안 씀(화면 차례를 읽음) · `homeButton` 없음(지금이 대시보드). +- `ui_template_general_layout.css` — scroll-margin-top 한 규칙(5장). +- 늦게 끝난 그림 막기: 맡기기 전 `currentRoute() === ROUTES.B01_ACCOUNT` 확인 한 줄(B01 은 자료를 기다린 뒤 그림 · 그 사이 떠나면 새 쪽 서랍에 대시보드 줄이 들어감 · `setDrawerSteps` 는 차례 검사 없음). + +몫 B — 시스템 설정 4 페이지(225-2): +- 새 `A00_Common/m_settings_nav.ts`(`b_workflow_nav.ts` 짝 이름 · B01 폴더도 가능하나 M → B01 의존이 생김): + - `settingLinks(user, companyId)` = B01 165-200 규칙을 옮긴 순수 목록(key · 이름 · 설명 · 길 · 인자) · M01 은 인자 `undefined`(`{}` 면 주소 끝에 「?」 · 시험 기대값 어긋남). + - `mountSettingsDrawer(user, activeKey, companyId)` = `createStepBar(…, { homeButton: true, … })` → `setDrawerSteps` · 누름 = 같은 길이면 `takeLeaveGuard()` 먼저 → `navigateTo(길, 인자)`. +- `B01_Dashboard_UI_Page.ts` 165-200 — `settingLinks` 로 카드만 그림(규칙 한 벌). +- M01 page 56 뒤 한 줄 — `mountSettingsDrawer(user, "m01", null)`. +- M02 page 236 뒤 한 줄 — `mountSettingsDrawer(user, entry.open, company)`. +- `test_b01_settings_cards.py` `_cards` — 옮긴 함수 소스(`export` 떼고)를 같이 실음 · 기대값 그대로(동작 안 바뀜 증명) · 지금 `_fn` 정규식은 `^function` 만 잡음. + +역할별 표시: +- 대시보드 ☰: 그 역할 화면의 컨테이너 그대로(10 · 8 · 6). +- 설정 ☰: 시스템 관리자 = 🏠 + 넷 · 회사 관리자 · 일반 = 🏠 + 구조물 조합(M02 만 · M01 은 못 들어옴). +- 새 locale 키 없음(카드 이름 · 컨테이너 제목 · 「대시보드」 다 있음). + +확인: +- 시험(고친 대상만): `test_b01_settings_cards` · `test_197_1_dashboard_option_card` · `test_device_drawer`(B07 그대로) · `test_design_page_single` · `test_m02_layer_screen` + 새 `test_225_drawer_shortcuts.py` + - Node: `settingLinks` 역할 셋 · 지금 칸 · 전처리 규칙이 길 기준(대시보드 둘째 칸이 회사 관리자 · 일반에서 켜짐). + - 브라우저(자기 vite 포트 · `test_device_audit` 의 `AISLO_UI_BASE` 꼴 · 꺼지면 건너뜀): 대시보드 ☰ 이름 = 컨테이너 제목 차례 · 「보안」 누름 → 서랍 닫힘 · 보안 위 끝 ≈ 52 · M01 ☰ = 대시보드 · 마스터 데이터 · 구조물 조합 · 구조물 옵션 · 도면 양식 · 강조 = 마스터 데이터 · 「도면 양식」 → 해시 `open=도면` · M02 고친 채 「구조물 옵션」 → 물음 창. 로그인 계정이 시스템 관리자인지 먼저 확인. +- `npm run typecheck` · prettier(고친 TS · CSS) · `ruff format`(새 시험). +- 화면 확인 거리(사용자): 역할 셋 대시보드 ☰ 바로가기 · 굴림 · 휴대폰 폭 · 설정 4 페이지 ☰ 강조 · 이동 · M02 저장 안 한 채 이동 물음. + +두 몫 파일 경계: + +| 몫 | 파일 | +| --- | --- | +| A 대시보드 | 기반 (a) · (b) · `B01_Dashboard_UI_Page.ts`(`buildPage` 쪽) · `ui_template_general_layout.css` · 새 시험 앞부분 | +| B 설정 4 페이지 | 새 `m_settings_nav.ts` · `B01_Dashboard_UI_Page.ts`(`buildSystemSettingsPanel` 쪽) · M01 page · M02 page · `test_b01_settings_cards.py` · 새 시험 뒷부분 | + +- 겹침 = `B01_Dashboard_UI_Page.ts`(서로 다른 함수) · 기반 (a)(b) → 권함: 한 창이 A → B 차례로(전체 100줄 안팎) · 나누면 A 합친 뒤 B. + +## 9. 정할 것(사용자) + +- 아이콘 쓸지 · 어떤 것 — 제안: 대시보드 = 📊 리소스 · ⚙️ 시스템 설정 · 🗂️ 프로젝트 · 📦 임시 보관함 · 👥 사용자/팀원 · 📨 가입 요청 · 🏢 회사 · 👤 기본정보 · 🔒 보안 · 📜 로그 / 설정 = 🗄️ 마스터 데이터 · 🧱 구조물 조합 · 🎛️ 구조물 옵션 · 📑 도면 양식(제목 글 ↔ 아이콘 짝은 같은 그림 때 `L()` 로 맞춤). +- 대시보드 ☰ 에 설정 4 페이지도 바로 넣을지(이 안 = 「시스템 설정」 한 줄 → 그 컨테이너로 굴림). +- 굴린 뒤 접혀 있던 시스템 설정 컨테이너를 펼칠지. +- (나중) 굴리는 동안 지금 컨테이너 강조 따라가기. + +## 10. 덧 발견(이번 밖) + +- B03 ☰ 진행 단계 없음(2장 끝). +- `B01_Dashboard_UI_Style.css` 789줄 — 700 초과. +- `.app-header.is-drawer-open` z-overlay(`app_shell.ts` 444)는 B·M 규칙(504-509 · 더 구체적)에 눌려 B·M 에선 안 먹음 — 지금은 201 로 좌측 패널(200) 위라 탈 없음. +- `setDrawerSteps` 에 차례 검사 없음 — 늦게 끝난 옛 페이지가 새 쪽 서랍에 맡길 수 있음(B 쪽도 같음 · 빠른 이동 때만). From c583dec29b1ac48e13f0d875c53e7e0190ab0b27 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 15 Oct 2026 15:47:14 +0900 Subject: [PATCH 40/81] =?UTF-8?q?feat(=EB=8C=80=EC=8B=9C=EB=B3=B4=EB=93=9C?= =?UTF-8?q?):=20334-1=20=E2=98=B0=20=EA=B5=AC=EC=97=AD=20=EB=B0=94?= =?UTF-8?q?=EB=A1=9C=EA=B0=80=EA=B8=B0=20=E2=80=94=20=EB=88=84=EB=A6=84=20?= =?UTF-8?q?=3D=20=EC=84=9C=EB=9E=8D=20=EB=8B=AB=ED=9E=98=20+=20=EA=B7=B8?= =?UTF-8?q?=20=EA=B5=AC=EC=97=AD=EC=9C=BC=EB=A1=9C=20=EA=B5=B4=EB=A6=BC=20?= =?UTF-8?q?=C2=B7=20=EC=A0=84=EC=B2=98=EB=A6=AC=20=EC=9E=A0=EA=B8=88=20?= =?UTF-8?q?=EA=B8=B8=20=EA=B8=B0=EC=A4=80=20=C2=B7=20=EC=84=9C=EB=9E=8D?= =?UTF-8?q?=EC=9D=B4=20=EC=A7=91=20=EC=97=86=EB=8A=94=20=EC=9A=94=EC=86=8C?= =?UTF-8?q?=20=EB=B0=9B=EC=9D=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ui_template_workflow_layout: 시스템 관리자 전용 판정 = 칸 번호 대신 전처리 길 - app_shell: setDrawerSteps 가 부모 없는 요소도 받음(닫을 때 안 옮김) - B01 buildPage: 그린 구역 그대로 · 화면 차례 · 아이콘 구역 키 짝 · 떠난 뒤엔 안 맡김 - general_layout.css: B·M 구역 scroll-margin-top = ☰ 자리 - 새 시험 test_225_drawer_shortcuts(앞) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01QfHKQs7J3hQELKMWQMUJEy --- A00_Common/app_shell.ts | 8 +- B01_Dashboard/B01_Dashboard_UI_Page.ts | 37 +++- resources/tester/test_225_drawer_shortcuts.py | 159 ++++++++++++++++++ ui_template/ui_template_general_layout.css | 5 + ui_template/ui_template_workflow_layout.ts | 2 +- 5 files changed, 206 insertions(+), 5 deletions(-) create mode 100644 resources/tester/test_225_drawer_shortcuts.py diff --git a/A00_Common/app_shell.ts b/A00_Common/app_shell.ts index 8f257690f..0d95b131d 100644 --- a/A00_Common/app_shell.ts +++ b/A00_Common/app_shell.ts @@ -118,10 +118,11 @@ function buildBrand(): HTMLButtonElement { /* ☰ 서랍 진행 단계 묶음(6-2) — 프로젝트 화면이 진행 판에 그린 단계 목록 **그 요소**를 받아 둠. * 서랍을 열 때 서랍으로 옮기고 닫을 때 제자리(진행 판)로 — 그리기 · 누름 코드는 한 벌. */ -let drawerSteps: { node: HTMLElement; home: HTMLElement } | null = null; +let drawerSteps: { node: HTMLElement; home: HTMLElement | null } | null = null; +/** 집(진행 판) 없는 요소 = 서랍이 늘 가짐 · 닫을 때 안 옮김(B01 · M01 · M02 바로가기) */ export function setDrawerSteps(node: HTMLElement | null): void { - drawerSteps = node?.parentElement ? { node, home: node.parentElement } : null; + drawerSteps = node ? { node, home: node.parentElement } : null; } const NAV_ITEMS: [keyof typeof ui_locales, string][] = [ @@ -166,7 +167,8 @@ function buildDrawer(header: HTMLElement): HTMLElement[] { burger.setAttribute("aria-expanded", String(open)); if (!drawerSteps) return; if (open) steps.append(drawerSteps.node); - else if (steps.contains(drawerSteps.node)) drawerSteps.home.append(drawerSteps.node); + else if (drawerSteps.home && steps.contains(drawerSteps.node)) + drawerSteps.home.append(drawerSteps.node); }; burger.addEventListener("click", () => setOpen(!header.classList.contains("is-drawer-open"))); scrim.addEventListener("click", () => setOpen(false)); diff --git a/B01_Dashboard/B01_Dashboard_UI_Page.ts b/B01_Dashboard/B01_Dashboard_UI_Page.ts index 4278b4777..a30713238 100644 --- a/B01_Dashboard/B01_Dashboard_UI_Page.ts +++ b/B01_Dashboard/B01_Dashboard_UI_Page.ts @@ -9,7 +9,9 @@ import { } from "@ui/ui_template_elements"; import { section } from "@ui/ui_template_general_blocks"; import { createGeneralLayout } from "@ui/ui_template_general_layout"; -import { navigateTo } from "../A00_Common/router"; +import { createStepBar } from "@ui/ui_template_workflow_layout"; +import { setDrawerSteps } from "../A00_Common/app_shell"; +import { currentRoute, navigateTo } from "../A00_Common/router"; import { fetchAllCompanies, fetchAllUsers, @@ -231,6 +233,38 @@ function buildSystemSettingsSection(state: DashboardState): HTMLElement { return card; } +/** ☰ 구역 바로가기 아이콘(334) — 구역 제목 키로 짝(EN 글에서도 같음) */ +const SECTION_ICONS: [Parameters[0], string][] = [ + ["B01_Dashboard_Resources", "📊"], + ["B01_Dashboard_SystemSettings", "⚙️"], + ["B01_Dashboard_Projects", "🗂️"], + ["B01_Temp_Section", "📦"], + ["B01_Dashboard_Users", "👥"], + ["B01_Dashboard_Members", "👥"], + ["B01_Dashboard_JoinRequests", "📨"], + ["B01_Dashboard_Companies", "🏢"], + ["B01_Dashboard_Company", "🏢"], + ["B01_Dashboard_Profile", "👤"], + ["B01_Account_Section_Security", "🔒"], + ["B01_Dashboard_AuditLogs", "📜"], +]; + +/** ☰ 구역 바로가기(334) — 그린 구역 그대로 · 화면 차례 · 누름 = 서랍 닫힘 + 그 구역으로 굴림 */ +function mountSectionShortcuts(grid: HTMLElement): void { + // 자료 기다리는 사이 다른 쪽으로 떠났으면 맡기지 않음 + if (currentRoute() !== ROUTES.B01_ACCOUNT) return; + const sections = [...grid.children] as HTMLElement[]; + const titles = sections.map((s) => s.querySelector(".ui-card__title")?.textContent ?? ""); + const icons = titles.map((title) => SECTION_ICONS.find(([key]) => L(key) === title)?.[1] ?? ""); + const bar = createStepBar(titles, -1, { + orientation: "vertical", + icons, + routes: titles, + onStepClick: (index) => sections[index].scrollIntoView({ behavior: "smooth", block: "start" }), + }); + setDrawerSteps(bar); +} + function buildPage(state: DashboardState): HTMLElement { // 제목·여백은 공용 템플릿을 따른다 (2026-09-06 사용자 지시) — B02 등 다른 화면과 같은 모양. const page = document.createElement("div"); @@ -311,6 +345,7 @@ function buildPage(state: DashboardState): HTMLElement { grid.append(section(L("B01_Dashboard_AuditLogs"), auditLogTable(state.auditLogs), true)); } page.append(grid); + mountSectionShortcuts(grid); page.append(buildKnowledgeChat()); const layout = createGeneralLayout({ pageClass: "b01-dashboard", diff --git a/resources/tester/test_225_drawer_shortcuts.py b/resources/tester/test_225_drawer_shortcuts.py new file mode 100644 index 000000000..d4cb2d4b9 --- /dev/null +++ b/resources/tester/test_225_drawer_shortcuts.py @@ -0,0 +1,159 @@ +"""PLAN 334 — ☰ 바로가기(요구 225-1 · 225-2). + +① Node — 진행 단계 「시스템 관리자 전용」 = 전처리 길 기준(칸 번호 아님) · 대시보드 둘째 칸은 안 꺼짐 +② 실제 브라우저(sub2 화면 서버 5177 · 꺼지면 건너뜀) — 대시보드 ☰ 이름 = 구역 제목 차례(역할 셋) · + 「보안」 누름 → 서랍 닫힘 · 보안 위 끝 ≈ ☰ 자리(52) +""" + +from __future__ import annotations + +import json +import os +import re +import subprocess +import urllib.request +from pathlib import Path + +import pytest + +ROOT = Path(__file__).resolve().parents[2] +BASE = os.environ.get("AISLO_UI_BASE", "http://localhost:5177") # sub2 화면 서버(OWNERS 자기 줄) +LAYOUT = ROOT / "ui_template" / "ui_template_workflow_layout.ts" +STEPS = ["파일입력", "전처리", "종단설계"] +WF_ROUTES = ["b03-file-input", "b04-preprocess", "b05-profile"] + + +def _fn(src: str, name: str) -> str: + found = re.search(rf"^(?:export )?function {name}\([\s\S]*?^}}", src, re.M) + assert found, f"{name} 를 못 찾음" + return found.group(0).removeprefix("export ") + + +def _node(script: str, *args: str): + result = subprocess.run( + ["node", "--experimental-strip-types", "--no-warnings", + "--input-type=module-typescript", "-e", script, *args], + capture_output=True, text=True, encoding="utf-8", timeout=60, + ) # fmt: skip + assert result.returncode == 0, result.stderr + return json.loads(result.stdout) + + +FAKE_DOM = ( + "const mk = (): any => { const cls = new Set(); return {" + " children: [] as any[], textContent: '', title: '', disabled: false, on: null as any," + " set className(v: string) { cls.clear(); v.split(' ').filter(Boolean).forEach((c) => cls.add(c)); }," + " get className() { return [...cls].join(' '); }," + " classList: { add: (...c: string[]) => c.forEach((x) => cls.add(x))," + " remove: (c: string) => cls.delete(c), contains: (c: string) => cls.has(c)," + " toggle: (c: string, on: boolean) => (on ? cls.add(c) : cls.delete(c)) }," + " setAttribute() {}, append(...c: any[]) { this.children.push(...c); }," + " addEventListener(_: string, f: any) { this.on = f; } }; };\n" + "const document = { createElement: () => mk() };\n" +) + + +def _step_bar(role: str, routes: list[str]) -> list[bool]: + """역할 · 길 목록으로 그린 단계 단추의 꺼짐 — 역할 물음이 끝난 뒤""" + src = _fn(LAYOUT.read_text(encoding="utf-8"), "createStepBar") + script = ( + FAKE_DOM + + "const ROUTES = { B01_ACCOUNT: 'b01-account', B04_PREPROCESS: 'b04-preprocess' };\n" + "const t = (k: string) => k;\nconst navigateTo = () => {};\n" + "const getDashboardRole = async () => process.argv[1];\n" + + src.replace("import.meta.env.DEV", "false") + + "\nconst bar = createStepBar(JSON.parse(process.argv[2]), -1," + " { routes: JSON.parse(process.argv[3]) });" + "\nawait new Promise((r) => setTimeout(r, 0));" + "\nconsole.log(JSON.stringify(bar.children.map((b: any) => b.disabled)));" + ) + return _node(script, role, json.dumps(STEPS), json.dumps(routes)) + + +def test_전처리_잠금은_길_기준(): + # 프로젝트 진행 단계 — 그대로(전처리만 시스템 관리자 전용) + assert _step_bar("ADMIN", WF_ROUTES) == [False, True, False] + assert _step_bar("USER", WF_ROUTES) == [False, True, False] + assert _step_bar("SYSTEM_ADMIN", WF_ROUTES) == [False, False, False] + # 대시보드 구역 바로가기 — 둘째 칸이 회사 관리자 · 일반에서 안 꺼짐 + titles = ["시스템 설정", "프로젝트", "임시 보관함"] + assert _step_bar("ADMIN", titles) == [False, False, False] + assert _step_bar("USER", titles) == [False, False, False] + + +# ── 실제 브라우저 ───────────────────────────────────────────── + + +def _up() -> bool: + try: + urllib.request.urlopen(BASE, timeout=3) + return True + except Exception: + return False + + +def _cred(who: str) -> dict: + if who == "시스템 관리자": + return json.loads((ROOT / "tmp" / "aislo_cred.json").read_text(encoding="utf-8")) + accounts = json.loads((ROOT / "tmp" / "verify_accounts.json").read_text(encoding="utf-8")) + return {k: accounts[who][k] for k in ("email", "password")} + + +def _page(browser, who: str, route: str, size=(1280, 700)): + ctx = browser.new_context(viewport={"width": size[0], "height": size[1]}) + r = ctx.request.post(f"{BASE}/api/auth/login/request", data=_cred(who), timeout=60000) + assert r.ok, f"로그인 실패 {r.status}" # 비밀번호는 안 찍음 + page = ctx.new_page() + page.goto(f"{BASE}/#/{route}", wait_until="domcontentloaded") + return page + + +DRAWER_LABELS = ( + "[...document.querySelectorAll('.app-drawer__steps .ui-workflow-layout__step-label')]" + ".map((e) => e.textContent.trim())" +) +DRAWER_DISABLED = ( + "[...document.querySelectorAll('.app-drawer__steps .ui-workflow-layout__step')]" + ".map((e) => e.disabled)" +) +SECTION_TITLES = ( + "[...document.querySelector('.b01-dashboard__grid').children]" + ".map((s) => s.querySelector('.ui-card__title').textContent.trim())" +) +DRAWER_OPEN = "!!document.querySelector('.is-drawer-open')" + + +def _open_drawer(page): + page.click(".app-burger") + page.wait_for_selector(".app-drawer__steps .ui-workflow-layout__step") + + +@pytest.mark.skipif(not _up(), reason="sub2 화면 서버(5177) 꺼짐") +@pytest.mark.parametrize("who, count", [("시스템 관리자", 10), ("회사관리자", 8), ("일반", 6)]) +def test_대시보드_구역_바로가기(who, count): + from playwright.sync_api import sync_playwright + + with sync_playwright() as p: + browser = p.chromium.launch(channel="msedge", headless=True) + page = _page(browser, who, "b01-account") + page.wait_for_selector(".b01-dashboard__grid", timeout=60000) + page.wait_for_load_state("networkidle") + titles = page.evaluate(SECTION_TITLES) + assert len(titles) == count, titles + _open_drawer(page) + assert page.evaluate(DRAWER_LABELS) == titles + assert page.evaluate(DRAWER_DISABLED) == [False] * count # 둘째 칸 안 꺼짐 + if who == "시스템 관리자": + page.click(".app-drawer__steps .ui-workflow-layout__step:has-text('보안')") + assert not page.evaluate(DRAWER_OPEN) + top = ( + "[...document.querySelector('.b01-dashboard__grid').children]" + ".find((s) => s.querySelector('.ui-card__title').textContent.trim() === '보안')" + ".getBoundingClientRect().top" + ) + try: # 부드러운 굴림이 멎을 때까지 + page.wait_for_function(f"() => Math.abs({top} - 52) <= 2", timeout=5000) + except Exception: + pass + assert abs(page.evaluate(top) - 52) <= 2, page.evaluate(top) + browser.close() diff --git a/ui_template/ui_template_general_layout.css b/ui_template/ui_template_general_layout.css index f5533249e..1cdc97764 100644 --- a/ui_template/ui_template_general_layout.css +++ b/ui_template/ui_template_general_layout.css @@ -117,3 +117,8 @@ html.is-bm-route .ui-general-layout__header { padding-left: max(0px, calc(var(--app-burger-inset) + var(--spacing-8) - (100vw - 100%) / 2)); } + +/* ☰ 바로가기(334) — 맨 위로 굴린 구역 제목이 ☰ 자리에 깔리지 않게 비킴 */ +html.is-bm-route .ui-general-block__section { + scroll-margin-top: var(--app-burger-inset); +} diff --git a/ui_template/ui_template_workflow_layout.ts b/ui_template/ui_template_workflow_layout.ts index 50b68bd70..bcee03cd3 100644 --- a/ui_template/ui_template_workflow_layout.ts +++ b/ui_template/ui_template_workflow_layout.ts @@ -116,7 +116,7 @@ export function createStepBar( button.classList.toggle("is-enabled", isEnabled); button.disabled = !isEnabled; - if (index === 1) { + if (options?.routes?.[index] === ROUTES.B04_PREPROCESS) { void getDashboardRole() .then((role) => { if (role !== "SYSTEM_ADMIN") { From 26fcebeb46c6a793d7c6f18d00d69a3b8a809548 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 15 Oct 2026 15:53:07 +0900 Subject: [PATCH 41/81] =?UTF-8?q?feat(=EC=84=A4=EC=A0=95):=20334-2=20?= =?UTF-8?q?=E2=98=B0=20=EC=8B=9C=EC=8A=A4=ED=85=9C=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?4=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EB=B0=94=EB=A1=9C=EA=B0=80?= =?UTF-8?q?=EA=B8=B0=20=E2=80=94=20=F0=9F=8F=A0=20=EB=8C=80=EC=8B=9C?= =?UTF-8?q?=EB=B3=B4=EB=93=9C=20+=20=EC=B9=B4=EB=93=9C=20=C2=B7=20?= =?UTF-8?q?=EC=A7=80=EA=B8=88=20=EC=B9=B8=20=EA=B0=95=EC=A1=B0=20=C2=B7=20?= =?UTF-8?q?M02=20=E2=86=92=20M02=20=EC=9D=B4=EB=8F=99=EC=9D=80=20=EB=96=A0?= =?UTF-8?q?=EB=82=98=EA=B8=B0=20=EB=AC=BC=EC=9D=8C=20=EB=A8=BC=EC=A0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 새 A00_Common/m_settings_nav: settingLinks = 대시보드 카드 역할 규칙 한 벌 · mountSettingsDrawer = ☰ 진행 단계 자리 - B01 시스템 설정 카드가 settingLinks 로만 그림(화면 그대로) - M01 · M02 page 한 줄씩(M02 = 진입 open · company 이어 실음) - test_b01_settings_cards 가 옮긴 함수 소스를 같이 실음(기대값 그대로) · test_225 뒷부분 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01QfHKQs7J3hQELKMWQMUJEy --- A00_Common/m_settings_nav.ts | 98 +++++++++++++++ B01_Dashboard/B01_Dashboard_UI_Page.ts | 38 ++---- M01_MasterData/M01_MasterData_UI_Page.ts | 2 + .../M02_MasterTemplete_UI_Page.ts | 2 + resources/tester/test_225_drawer_shortcuts.py | 115 +++++++++++++++++- resources/tester/test_b01_settings_cards.py | 7 +- 6 files changed, 228 insertions(+), 34 deletions(-) create mode 100644 A00_Common/m_settings_nav.ts diff --git a/A00_Common/m_settings_nav.ts b/A00_Common/m_settings_nav.ts new file mode 100644 index 000000000..432177a8e --- /dev/null +++ b/A00_Common/m_settings_nav.ts @@ -0,0 +1,98 @@ +/* ============================================================================= + * m_settings_nav.ts + * 시스템 설정 바로가기(334 · 요구 225-2) — 대시보드 「시스템 설정」 카드 · M01 · M02 ☰ 가 같이 쓰는 + * 역할 규칙 한 벌(`settingLinks`) · ☰ 진행 단계 자리에 🏠 대시보드 + 카드(`mountSettingsDrawer`). + * ========================================================================== */ + +import { ROUTES, type RoutePath } from "@config/config_frontend"; +import { currentLanguageIndex, ui_locales } from "@ui/ui_template_locale"; +import { createStepBar } from "@ui/ui_template_workflow_layout"; +import { setDrawerSteps } from "./app_shell"; +import { currentRoute, navigateTo, takeLeaveGuard } from "./router"; + +interface SettingUser { + role?: string; + is_master?: boolean; + company_id?: number | null; +} + +/** key = M01 은 「m01」 · M02 는 진입 인자 `open` */ +export interface SettingLink { + key: string; + title: string; + desc: string; + route: RoutePath; + query?: Record; +} + +function L(key: keyof typeof ui_locales): string { + return ui_locales[key][currentLanguageIndex]; +} + +const ICONS: Record = { m01: "🗄️", 구조물조합: "🧱", 옵션: "🎛️", 도면: "📑" }; + +/** 시스템 설정 — 역할로 카드 숨김 · M02 카드는 `?open=&root=` 로 진입(27-1d) */ +export function settingLinks(user: SettingUser, companyId: number | string | null): SettingLink[] { + // 시스템 관리자는 고른 회사를 해시에 실음(29-1) + const company: Record = + user.role === "SYSTEM_ADMIN" && companyId ? { company: String(companyId) } : {}; + const m02 = (title: string, open: string, root: string, desc: string): SettingLink => ({ + key: open, + title, + desc, + route: ROUTES.M02_MASTER_TEMPLATE, + query: { open, root, ...company }, + }); + // 구조물 조합 기본 층 = 개인(123-1 · 누구든) · 개인 층 없음(회사 없음) = 아래 역할 규칙 + const comboRoot = (fallback: string): string => (user.company_id ? "personal" : fallback); + if (user.role === "SYSTEM_ADMIN") { + const desc = L("B01_Dashboard_PermMasterWrite"); + return [ + { + key: "m01", + title: L("B01_Dashboard_MasterElements"), + desc: L("B01_Dashboard_PermSourceRead"), + route: ROUTES.M01_MASTER_DATA, + }, + m02(L("B01_Dashboard_CardStructCombo"), "구조물조합", comboRoot("master"), desc), + m02(L("B01_Dashboard_CardStructOption"), "옵션", "master", desc), + m02(L("B01_Dashboard_CardDrawing"), "도면", "master", desc), + ]; + } + // 회사 수정 = 회사 관리자 · 회사 마스터(M02 accessFor 와 같은 규칙) + const companyAdmin = user.role === "ADMIN" || user.is_master; + const root = companyAdmin ? "company" : "personal"; + const desc = [ + L("B01_Dashboard_PermMasterRead"), + L(companyAdmin ? "B01_Dashboard_PermCompanyWrite" : "B01_Dashboard_PermCompanyRead"), + L("B01_Dashboard_PermPersonalWrite"), + ].join(" · "); + return [m02(L("B01_Dashboard_CardStructCombo"), "구조물조합", comboRoot(root), desc)]; +} + +/** ☰ 진행 단계 자리 — 🏠 대시보드 + 카드 · 지금 칸 강조 · 같은 길(M02 → M02)은 머리 가드가 안 물어 여기서 먼저 */ +export function mountSettingsDrawer( + user: SettingUser, + activeKey: string, + companyId: number | string | null, +): void { + const links = settingLinks(user, companyId); + const bar = createStepBar( + links.map((link) => link.title), + links.findIndex((link) => link.key === activeKey), + { + orientation: "vertical", + icons: links.map((link) => ICONS[link.key] ?? ""), + routes: links.map((link) => link.route), + homeButton: true, + onStepClick: (index) => void go(links[index]), + }, + ); + setDrawerSteps(bar); +} + +async function go(link: SettingLink): Promise { + const guard = link.route === currentRoute() ? takeLeaveGuard() : null; + if (guard && !(await guard())) return; + navigateTo(link.route, link.query); +} diff --git a/B01_Dashboard/B01_Dashboard_UI_Page.ts b/B01_Dashboard/B01_Dashboard_UI_Page.ts index a30713238..82515ecfc 100644 --- a/B01_Dashboard/B01_Dashboard_UI_Page.ts +++ b/B01_Dashboard/B01_Dashboard_UI_Page.ts @@ -11,6 +11,7 @@ import { section } from "@ui/ui_template_general_blocks"; import { createGeneralLayout } from "@ui/ui_template_general_layout"; import { createStepBar } from "@ui/ui_template_workflow_layout"; import { setDrawerSteps } from "../A00_Common/app_shell"; +import { settingLinks } from "../A00_Common/m_settings_nav"; import { currentRoute, navigateTo } from "../A00_Common/router"; import { fetchAllCompanies, @@ -164,40 +165,15 @@ function settingCard(title: string, desc: string, go: () => void): HTMLElement { return card; } -/** 시스템 설정 — 역할로 카드 숨김 · M02 카드는 `?open=&root=` 로 진입(27-1d) */ +/** 시스템 설정 — 카드 규칙은 `settingLinks` 한 벌(☰ 바로가기와 같이 씀 · 334) */ function buildSystemSettingsPanel(user: DashboardUser, companyId: number | null): HTMLElement { const wrap = document.createElement("div"); wrap.className = "b01-dashboard__setting-grid"; - // 시스템 관리자는 고른 회사를 해시에 실음(29-1) - const company: Record = - user.role === "SYSTEM_ADMIN" && companyId ? { company: String(companyId) } : {}; - const m02 = (title: string, open: string, root: string, desc: string): HTMLElement => - settingCard(title, desc, () => - navigateTo(ROUTES.M02_MASTER_TEMPLATE, { open, root, ...company }), - ); - // 구조물 조합 기본 층 = 개인(123-1 · 누구든) · 개인 층 없음(회사 없음) = 아래 역할 규칙 - const comboRoot = (fallback: string): string => (user.company_id ? "personal" : fallback); - if (user.role === "SYSTEM_ADMIN") { - const desc = L("B01_Dashboard_PermMasterWrite"); - wrap.append( - settingCard(L("B01_Dashboard_MasterElements"), L("B01_Dashboard_PermSourceRead"), () => - navigateTo(ROUTES.M01_MASTER_DATA), - ), - m02(L("B01_Dashboard_CardStructCombo"), "구조물조합", comboRoot("master"), desc), - m02(L("B01_Dashboard_CardStructOption"), "옵션", "master", desc), - m02(L("B01_Dashboard_CardDrawing"), "도면", "master", desc), - ); - return wrap; - } - // 회사 수정 = 회사 관리자 · 회사 마스터(M02 accessFor 와 같은 규칙) - const companyAdmin = user.role === "ADMIN" || user.is_master; - const root = companyAdmin ? "company" : "personal"; - const desc = [ - L("B01_Dashboard_PermMasterRead"), - L(companyAdmin ? "B01_Dashboard_PermCompanyWrite" : "B01_Dashboard_PermCompanyRead"), - L("B01_Dashboard_PermPersonalWrite"), - ].join(" · "); - wrap.append(m02(L("B01_Dashboard_CardStructCombo"), "구조물조합", comboRoot(root), desc)); + wrap.append( + ...settingLinks(user, companyId).map((link) => + settingCard(link.title, link.desc, () => navigateTo(link.route, link.query)), + ), + ); return wrap; } diff --git a/M01_MasterData/M01_MasterData_UI_Page.ts b/M01_MasterData/M01_MasterData_UI_Page.ts index 26525e3d2..c43c90a4a 100644 --- a/M01_MasterData/M01_MasterData_UI_Page.ts +++ b/M01_MasterData/M01_MasterData_UI_Page.ts @@ -19,6 +19,7 @@ import { import { t as L } from "@ui/ui_template_locale"; import { createWorkflowOverlays } from "@ui/ui_template_overlay"; import { ROUTES } from "@config/config_frontend"; +import { mountSettingsDrawer } from "../A00_Common/m_settings_nav"; import { navigateTo } from "../A00_Common/router"; import { fetchSessionUser } from "../A06_Login/A06_Login_Api_Fetch"; import { saveFiles, type FileInfo } from "./M01_MasterData_Api_Fetch"; @@ -54,6 +55,7 @@ export async function renderM01MasterData(root: HTMLElement): Promise { } root.innerHTML = ""; root.append(buildPage()); + mountSettingsDrawer(user, "m01", null); // ☰ 시스템 설정 바로가기(334) } function buildPage(): HTMLElement { diff --git a/M02_MasterTemplete/M02_MasterTemplete_UI_Page.ts b/M02_MasterTemplete/M02_MasterTemplete_UI_Page.ts index 95f647195..c44e253e8 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_UI_Page.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_UI_Page.ts @@ -13,6 +13,7 @@ import { ROUTES } from "@config/config_frontend"; import { el } from "@ui/ui_template_elements"; import { t as L } from "@ui/ui_template_locale"; import { createWorkflowOverlays } from "@ui/ui_template_overlay"; +import { mountSettingsDrawer } from "../A00_Common/m_settings_nav"; import { navigateTo, type MountedPage } from "../A00_Common/router"; import { fetchSessionUser } from "../A06_Login/A06_Login_Api_Fetch"; import { fetchOwners, setActiveLayer } from "./M02_MasterTemplete_Api_Fetch"; @@ -238,6 +239,7 @@ export async function renderM02MasterTemplate(root: HTMLElement): Promise false, show: () => {}, dispose: () => setActiveLayer("system") }; diff --git a/resources/tester/test_225_drawer_shortcuts.py b/resources/tester/test_225_drawer_shortcuts.py index d4cb2d4b9..c7fdd1550 100644 --- a/resources/tester/test_225_drawer_shortcuts.py +++ b/resources/tester/test_225_drawer_shortcuts.py @@ -1,8 +1,10 @@ """PLAN 334 — ☰ 바로가기(요구 225-1 · 225-2). ① Node — 진행 단계 「시스템 관리자 전용」 = 전처리 길 기준(칸 번호 아님) · 대시보드 둘째 칸은 안 꺼짐 + settingLinks 역할 셋 · 설정 ☰ 지금 칸(진입 인자) ② 실제 브라우저(sub2 화면 서버 5177 · 꺼지면 건너뜀) — 대시보드 ☰ 이름 = 구역 제목 차례(역할 셋) · - 「보안」 누름 → 서랍 닫힘 · 보안 위 끝 ≈ ☰ 자리(52) + 「보안」 누름 → 서랍 닫힘 · 보안 위 끝 ≈ ☰ 자리(52) · M01 ☰ 다섯 · 강조 · 「도면 양식」 → open=도면 · + M02 → M02 이동은 떠나기 물음 먼저(가드 자리에 물음 창을 심어 확인 · 취소 = 머묾) """ from __future__ import annotations @@ -19,6 +21,7 @@ import pytest ROOT = Path(__file__).resolve().parents[2] BASE = os.environ.get("AISLO_UI_BASE", "http://localhost:5177") # sub2 화면 서버(OWNERS 자기 줄) LAYOUT = ROOT / "ui_template" / "ui_template_workflow_layout.ts" +NAV = ROOT / "A00_Common" / "m_settings_nav.ts" STEPS = ["파일입력", "전처리", "종단설계"] WF_ROUTES = ["b03-file-input", "b04-preprocess", "b05-profile"] @@ -81,6 +84,58 @@ def test_전처리_잠금은_길_기준(): assert _step_bar("USER", titles) == [False, False, False] +def _settings(user: dict, active: str, company) -> dict: + """settingLinks 목록 + ☰ 에 맡긴 단계 막대(이름 · 지금 칸 · 아이콘 · 🏠)""" + src = NAV.read_text(encoding="utf-8") + icons = re.search(r"^const ICONS.*$", src, re.M).group(0) + script = ( + "const ROUTES = { M01_MASTER_DATA: 'm01-master-data'," + " M02_MASTER_TEMPLATE: 'm02-master-template' };\n" + "const L = (k: string) => k;\nlet bar: any = null;\n" + "const createStepBar = (titles: string[], active: number, o: any) =>" + " ({ titles, active, icons: o.icons, home: o.homeButton, routes: o.routes });\n" + "const setDrawerSteps = (b: any) => { bar = b; };\nconst go = () => {};\n" + + icons + + "\n" + + _fn(src, "settingLinks") + + "\n" + + _fn(src, "mountSettingsDrawer") + + "\nconst [user, active, company] = JSON.parse(process.argv[1]);" + "\nmountSettingsDrawer(user, active, company);" + "\nconsole.log(JSON.stringify({ links: settingLinks(user, company).map((l) =>" + " [l.key, l.route, l.query ?? null]), bar }));" + ) + return _node(script, json.dumps([user, active, company])) + + +M02 = "m02-master-template" + + +def test_설정_바로가기_역할_셋_지금_칸(): + admin = _settings({"role": "SYSTEM_ADMIN", "company_id": 1}, "옵션", "3") + assert admin["links"] == [ + ["m01", "m01-master-data", None], + ["구조물조합", M02, {"open": "구조물조합", "root": "personal", "company": "3"}], + ["옵션", M02, {"open": "옵션", "root": "master", "company": "3"}], + ["도면", M02, {"open": "도면", "root": "master", "company": "3"}], + ] + bar = admin["bar"] + assert bar["active"] == 2 and bar["home"] is True + assert bar["icons"] == ["🗄️", "🧱", "🎛️", "📑"] + assert _settings({"role": "SYSTEM_ADMIN"}, "m01", None)["bar"]["active"] == 0 + # 일위대가조합 = 강조 없음 + assert _settings({"role": "SYSTEM_ADMIN"}, "일위대가조합", None)["bar"]["active"] == -1 + # 그 밖 역할 = 🏠 + 구조물 조합 하나(회사 인자 안 실음) + company = _settings({"role": "ADMIN", "company_id": 1}, "구조물조합", "3") + assert company["links"] == [["구조물조합", M02, {"open": "구조물조합", "root": "personal"}]] + assert company["bar"]["active"] == 0 + assert _settings({"role": "USER", "is_master": True}, "구조물조합", None)["links"][0][2] == { + "open": "구조물조합", + "root": "company", + } + assert _settings({"role": "USER"}, "구조물조합", None)["links"][0][2]["root"] == "personal" + + # ── 실제 브라우저 ───────────────────────────────────────────── @@ -157,3 +212,61 @@ def test_대시보드_구역_바로가기(who, count): pass assert abs(page.evaluate(top) - 52) <= 2, page.evaluate(top) browser.close() + + +ACTIVE = "document.querySelector('.app-drawer__steps .is-active .ui-workflow-layout__step-label')?.textContent" +HASH = "decodeURIComponent(location.hash)" + + +def _drawer_click(page, label: str): + _open_drawer(page) + page.click(f".app-drawer__steps .ui-workflow-layout__step:has-text('{label}')") + + +@pytest.mark.skipif(not _up(), reason="sub2 화면 서버(5177) 꺼짐") +def test_설정_4_페이지_바로가기(): + from playwright.sync_api import sync_playwright + + with sync_playwright() as p: + browser = p.chromium.launch(channel="msedge", headless=True) + page = _page(browser, "시스템 관리자", "m01-master-data") + page.wait_for_selector(".m01-master", timeout=60000) + _open_drawer(page) + assert page.evaluate(DRAWER_LABELS) == [ + "대시보드", + "마스터 데이터", + "구조물 조합", + "구조물 옵션", + "도면 양식", + ] + assert page.evaluate(ACTIVE) == "마스터 데이터" + page.click(".app-drawer__steps .ui-workflow-layout__step:has-text('도면 양식')") + page.wait_for_function("decodeURIComponent(location.hash).includes('open=도면')") + assert M02 in page.evaluate(HASH) + page.wait_for_selector(".m02-master", timeout=60000) + page.wait_for_load_state("networkidle") + _open_drawer(page) + assert page.evaluate(ACTIVE) == "도면 양식" + page.click(".app-burger") # 닫음 + + # M02 → M02 — 저장 안 한 고침 대신 가드 자리에 앱의 물음 창을 심음 + page.evaluate( + """async () => { + const url = (p) => performance.getEntriesByType('resource') + .map((e) => e.name).find((n) => n.includes(p)); + const router = await import(url('/router.ts')); + const ui = await import(url('/ui_template_elements.ts')); + router.setLeaveGuard(() => ui.showConfirmDialog('저장 안 한 고침')); + }""" + ) + before = page.evaluate(HASH) + _drawer_click(page, "구조물 옵션") + page.wait_for_selector(".ui-confirm") + page.click(".ui-confirm__actions button >> nth=0") # 취소 = 머묾 + page.wait_for_timeout(300) + assert page.evaluate(HASH) == before + _drawer_click(page, "구조물 옵션") + page.wait_for_selector(".ui-confirm") + page.click(".ui-confirm__actions button >> nth=1") # 버림 = 이동 + page.wait_for_function("decodeURIComponent(location.hash).includes('open=옵션')") + browser.close() diff --git a/resources/tester/test_b01_settings_cards.py b/resources/tester/test_b01_settings_cards.py index 004a0013d..7798ddfd2 100644 --- a/resources/tester/test_b01_settings_cards.py +++ b/resources/tester/test_b01_settings_cards.py @@ -17,6 +17,7 @@ import pytest ROOT = Path(__file__).resolve().parents[2] PAGE = ROOT / "B01_Dashboard" / "B01_Dashboard_UI_Page.ts" +NAV = ROOT / "A00_Common" / "m_settings_nav.ts" # 카드 규칙 한 벌(334) def _text(path: Path) -> str: @@ -24,9 +25,9 @@ def _text(path: Path) -> str: def _fn(src: str, name: str) -> str: - found = re.search(rf"^function {name}\([\s\S]*?^}}", src, re.M) + found = re.search(rf"^(?:export )?function {name}\([\s\S]*?^}}", src, re.M) assert found, f"{name} 를 못 찾음" - return found.group(0) + return found.group(0).removeprefix("export ") def _cards(user: dict, company_id: int | None = None) -> list: @@ -46,6 +47,8 @@ def _cards(user: dict, company_id: int | None = None) -> list: "const document = { createElement: () => mk() };\n" + _fn(src, "settingCard") + "\n" + + _fn(_text(NAV), "settingLinks") + + "\n" + _fn(src, "buildSystemSettingsPanel") + "\nconst user = JSON.parse(process.argv[1]);" + "\nconst panel = buildSystemSettingsPanel(user, JSON.parse(process.argv[2]));" From 04021d1f73b69b4a1f991ae9bd148bc845bb5b9d Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 15 Oct 2026 16:13:03 +0900 Subject: [PATCH 42/81] =?UTF-8?q?chore(=EC=A1=B0=EC=82=AC):=20335=20CAD=20?= =?UTF-8?q?=EC=9E=85=EB=A0=A5=20=EB=B3=B4=EA=B0=95=20=EC=83=81=EC=84=B8=20?= =?UTF-8?q?=EA=B2=80=ED=86=A0=20=E2=80=94=20=EC=98=A4=ED=86=A0=EC=BA=90?= =?UTF-8?q?=EB=93=9C=2071=20=EA=B0=80=EC=A7=80=20=EB=8C=80=20=EC=9A=B0?= =?UTF-8?q?=EB=A6=AC=20=C2=B7=20=ED=9D=A0=2012=20=C2=B7=20=EB=AC=B6?= =?UTF-8?q?=EC=9D=8C=20=EC=9D=BC=EA=B3=B1=20=C2=B7=20=EB=B6=80=EB=A1=9D=20?= =?UTF-8?q?=EB=91=98?= 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_01C6BNBFXFndbqYxu6sZuknK --- .../조사/2026-10-15_335_CAD입력보강_검토.md | 297 +++++++++++++++ .../조사/2026-10-15_335_부록1_오토캐드입력.md | 354 ++++++++++++++++++ .../조사/2026-10-15_335_부록2_우리CAD현황.md | 261 +++++++++++++ 3 files changed, 912 insertions(+) create mode 100644 docs/raw/조사/2026-10-15_335_CAD입력보강_검토.md create mode 100644 docs/raw/조사/2026-10-15_335_부록1_오토캐드입력.md create mode 100644 docs/raw/조사/2026-10-15_335_부록2_우리CAD현황.md diff --git a/docs/raw/조사/2026-10-15_335_CAD입력보강_검토.md b/docs/raw/조사/2026-10-15_335_CAD입력보강_검토.md new file mode 100644 index 000000000..a24b5d503 --- /dev/null +++ b/docs/raw/조사/2026-10-15_335_CAD입력보강_검토.md @@ -0,0 +1,297 @@ +# CAD 입력 보강 — 상세 검토 (요구 225-3 · PLAN 335) + +- 기준 = `main_laptop_1` 422c449eb · 코드 읽기만(실행 · 빌드 · 화면 실측 안 함) · 코드 고침 없음. +- 재료 = B1(오토캐드 쪽 · 쓸모 높음 32 · 중간 39 = 71 줄) · B2(우리 현황) · 331(연속 명령) · 332(설정 창) 조사. +- B1 = 같은 폴더 `2026-10-15_335_부록1_오토캐드입력.md`(출처 [S..] 목록) · B2 = `2026-10-15_335_부록2_우리CAD현황.md`. +- 경로 줄임 — 파일 이름만 = `B07_DesignDetail/openwebcad/src/` 아래 · `A00/` = `A00_Common/` · 페이지 파일은 저장소 뿌리부터. +- 기호 — ● 있음 · ◐ 일부 · ○ 없음 · ⚠ 흠 · (추정) = 코드 흐름 판단 · 화면 실측 필요. +- 크기 — 작음 = 반나절 안 · 중간 = 하루~이틀 · 큼 = 사흘 넘음(시험 포함 · 화면 확인 별도 · 코드 읽기 어림). + +## 0. 결론 요약 + +1. 오토캐드 입력 방법 중 쓸모 높음 · 중간 71 가지를 우리 CAD 와 맞대 봄 — 있음 2 · 일부 28 · 없음 41(331 에서 넣은 좌표 꼴 · 각도 고정 · Tab 칸 · 한 번 스냅 등은 이번 표 밖). +2. 보강보다 먼저 고칠 흠 12 — 급한 셋: ① Ctrl+C · Ctrl+X 같은 Ctrl 조합이 글자로 쌓여 Enter · Space · 우클릭 때 원 · 분해 · 지우기 명령이 돎 ② 확정 도면 · 미리보기가 그립 · Delete · 특성 칸으로 고쳐짐(저장은 안 되나 DXF 내보내기로 새어 나감) ③ 그립을 끌지 않고 누르기만 해도 점이 누른 자리로 밀림(실측 필요). +3. 흠은 거의 다 작음 — 한 창이 몰아 하면 사흘~엿새. +4. 보강은 일곱 묶음 — ① 좌표 · 값 ② 스냅 · 추적 ③ 고르기 ④ 그립 ⑤ 편집 손버릇 ⑥ 명령줄 ⑦ 자동화. +5. 먼저 셋 — ⓪ 흠 막기 → ③ 고르기 손버릇(상자로 더하기 · 명령 중 누르는 대로 더함 · 앞 선택 P) → ② 극 추적 각 고르기 · 비탈 비율 각(1:1.5 꼴) · Shift 직교. +6. 그다음 ④ 그립(누르고 · 옮기고 · 누르기 · 그립 중 숫자 · 치수 글자 비키기) · ① FROM · 두 점 가운데 · 식 그대로(3.2*1.5). +7. ⑥ 명령줄 · ⑦ 자동화는 임도 도면 손질에 쓸모가 낮아 뒤로. +8. B06 상세 · M02 카드에는 리본 · 상태막대 · 명령행이 없음 → 새 기능은 키 · 우클릭 메뉴 · ☰ 설정으로도 넣어야 세 곳 모두 씀. +9. 정할 것 14 — 빼기 키(Ctrl 대 Shift) · 우클릭(Enter 대 메뉴) · 극 추적 각 목록 · 직교 강제 · 그립 방식 등(5장). +10. 근거 = 코드 읽기 — 흠 중 그립 어긋남 · 안 보이는 것 고름 · 가이드 거리 · 명령행 한글은 화면 실측 필요. + +## 1. 흠 — 보강 전에 고칠 것 + +### 1-1. B2 주장 다시 확인 + +| B2 주장 | 다시 본 결과 | 근거 | +|---|---|---| +| Ctrl+C · Ctrl+X 가 입력 글로 쌓여 Enter 때 원 · 분해 | 맞음 · 더 넓음 — Ctrl · Alt 와 함께 누른 글자 전부(Z · Y · S · A 빼고)가 글로 쌓임 · Enter 뿐 아니라 Space · 우클릭도 제출 · 분해는 고른 것이 있으면 Enter 한 번 더에 실제로 나뉨 | `input-controller.ts:374,423-428,476-482` · `A00/key_shortcut.ts:31-49` · `commands.draw.ts:53` · `commands.modify.ts:202,211` · `tools/modify/structure-tools.ts:46-68` · `sequence-tool.ts:321-337` | +| 보기 전용(readOnly)에서 그립 · Delete · 특성 칸이 안 막힘 | 맞음 · 덧 — Ctrl+그립(폴리선 꼭짓점 지움) · 도면층 관리 층째 지움도 안 막힘 · 그립이 보기 전용에서도 그려짐 · 미리보기(viewOnly) Ctrl+A → Delete 맞음 | `select-tool.helpers.ts:55-72` · `grip-edit-tool.ts:96-105` · `select-tool.ts:73-76,167-171` · `input-controller.ts:438-450` · `PropertiesEditor.tsx:12-17` · `RibbonWidgets.tsx:39-47` · `LayerManager.tsx:41-60` · `helpers/draw.ts:56` · `state.ts:340-350` | +| 저장까지 가는지 미확인(B2 9-2) | 확인 — B07 = 저장 길 없음([수정]이 서버 원본을 다시 실음) · M02 = 부모 [저장]이 막음 · 단 B07 DXF · DWG 내보내기는 CAD 화면 그대로 보냄 → 확정본과 다른 파일이 나갈 수 있음 · 탭에 「미저장」 | `B07_DesignDetail/B07_DesignDetail_UI_Page.ts:122-126,476-489,505-510` · `M02_MasterTemplete/M02_MasterTemplete_Drawing.ts:231-233` | +| 극 추적 45° 고정 | 맞음 · 덧 — 직교와 한 값(둘 중 하나) · F3 끄면 같이 꺼짐 · 가이드 잡는 거리 = 도면 단위 15(화면 px 아님) | `input-controller.ts:277-280,403-412` · `state.ts:82` · `StatusBar.tsx:22-23,44-51` · `get-draw-guides.ts:36-49` · `App.consts.ts:57` | +| 고르기 상자 더하기 안 됨 | 맞음 · Ctrl+상자 = 「상자 안의 안 고른 것만」 남음도 맞음 · Shift+상자는 주석 TODO | `select-tool.helpers.ts:114-169` · `select-tool.ts:159-164` | +| 우클릭 = Enter 만 | 맞음 · 덧 — 쳐 둔 글도 제출(Ctrl+C 로 쌓인 「C」 → 우클릭 = 원) · iframe 전체 기본 메뉴 막음 | `input-controller.ts:116-119,174-180,511-575` · `ui_template/cad_host/cad_host.ts:149` | +| 그립 = 누른 채 끌어 놓기만(B2 추정) | 확인(추정 풂) · 덧 — 안 끌고 떼도 그 자리에 적용(H3) | `input-controller.ts:195-211,311-330` · `select-tool.helpers.ts:56-71` · `grip-edit-tool.ts:64-112` | +| 손가락으로 그립 못 옮김(B2 추정) | 확인 · 덧 — 톡 = 그립 근처 누른 자리로 점이 밀림(옮김 아닌 어긋남) | `input-touch.ts:46-50,106-112` · `pick-radius.ts:17-22` | +| 겹친 것 돌려 고르기 「Shift+Space」 | 고침 — Shift+Space 처리 없음(Shift 붙은 Space 는 아무 일 없음) · 같은 자리 다시 누르기만 | `ime-sink.ts:47-51` · `input-controller.ts:458-463` · `select-tool.helpers.ts:34-49` | +| 명령행 칸 한글 자판이면 명령 못 찾음(B2 추정) | 그대로 추정 — 일반 입력 칸 · 자모 → 영문 바꿈 없음 | `CommandLine.tsx:51-57` | +| 끝점 스냅 「폴리선 선 구간」 | 작게 고침 — 폴리선은 토막마다 맡김 → 호 토막도 끝점 · 중심 냄 | `PolyLineEntity.ts:114-116` · `ArcEntity.ts:132-148` | + +### 1-2. 흠 목록 + +| # | 무엇이 틀림(화면 말) | 근거 | 어디서 | 고침(한 줄) | 크기 | 급함 | +|---|---|---|---|---|---|---| +| H1 ⚠ | Ctrl · Alt+글자가 명령 글자로 쌓임 — Ctrl+C → 「C」 → Enter · Space · 우클릭 = 원 명령(선 그리는 중이면 닫기) · Ctrl+X → 「X」 → 분해(고른 것 있으면 Enter 한 번 더에 분해) · Ctrl+E → 지우기 · Ctrl+F → 모깎기 · Ctrl+L → 선 · Ctrl+1 → 「1」 → 점 단계면 거리 1 인 점이 찍힘 | 글자 받는 줄이 수정키를 안 봄 `input-controller.ts:476-482` · Ctrl 이름표 = Z · Y · S · A 넷 `A00/key_shortcut.ts:31-49` · Ctrl+V 만 따로 `input-controller.ts:423-426` | 세 곳 모두 · B06 상세는 고친 것이 곧바로 세션 초안 | 그 줄에 Ctrl · Alt · ⌘ 거름 + Ctrl+C/X = COPYCLIP · CUTCLIP | 작음 | 높음 | +| H2 ⚠ | 보기 전용인데 고쳐짐 — 확정 도면 · 권한 없는 양식에서 그립 끌기(+Ctrl+그립 = 꼭짓점 지움) · Delete · 손가락 🗑 · 특성 칸 · 리본 특성 · 도면층 관리(층 옮김 · 층째 지움)가 먹음 · 미리보기는 Ctrl+A → Delete · 저장은 안 되나 B07 DXF · DWG 내보내기에 그대로 실림 | 명령 길만 막음 `run-command.ts:28-34` · 직접 고치는 길에 문 없음 `state.ts:340-350` · 1-1 표 둘째 · 셋째 줄 | B07 확정본 · M02 권한 없음 · B07 평면도 관리 · M02 카드 보기 · 면적산출 패널 | 고치는 길 넷(그립 · Delete · 특성 · 도면층)에 문 한 줄씩 또는 공용 「고칠 수 있나」 판정 하나 · 보기 전용이면 그립 안 그림 | 작음~중간 | 높음 | +| H3 ⚠ | 그립을 누르기만 해도 점이 밀림(추정) — 그립을 누르면 그립 도구로 바뀌며 스냅이 지워짐 → 마우스를 안 움직이고 떼면 스냅 없이 「뗀 자리」로 적용 → 그립 중심에서 벗어난 만큼(마우스 최대 10px · 손가락 24px 넘게) 끝점 · 선 전체(가운데 그립) · 원 중심 · 글 기준점이 밀리고 「고침」 표시 | `state.ts:386-391` · `grip-edit-tool.ts:85-105` · `input-controller.ts:195-211,228-251,277-280` · 손가락 톡은 늘 이 길 `input-touch.ts:46-50` | 세 곳 모두 · 손가락 더 큼 | 안 끌고 떼면 적용하지 않음(오토캐드 = 그립이 「뜨거워짐」 · ④ 첫걸음) | 작음 | 높음 | +| H4 | 상자 고르기 — Ctrl+상자 = 상자 안의 「안 고른 것만」 남고 나머지 고름은 버려짐(더하기도 빼기도 아님) · Shift+상자 = 새로 고름 · 상자는 늘 앞 고름을 버림 | `select-tool.helpers.ts:118,131-168` · `select-tool.ts:159-164` | 세 곳 모두 | 상자 결과를 앞 고름에 더함 · 빼기 키 상자 = 뺌(정할 것 1) | 작음 | 중간 | +| H5 | 명령 중 고르기가 누를 때마다 바뀜 — 이동 · 복사 · 지우기 등 「객체 선택」 단계에서 하나씩 누르면 앞 것이 빠짐 → 오토캐드 손버릇으로 셋 누르고 Enter 하면 하나만 옮겨짐 · 지워짐 · 끌기 상자 안 됨(두 번 누름) · 스침 강조 없음 | `select-tool.helpers.ts:80-82` · `input-controller.ts:283,325-327` · `move-tool.ts:98-138` | 세 곳 모두 | 명령 안 고르기에선 누름 = 더함(정할 것 2) | 작음 | 중간 | +| H6 | 안 보이는 것이 골라지고 스냅됨(실측 필요) — 꺼진 층 · 동결 층 · 숨긴 객체(HIDEOBJECTS · ISOLATE)를 그리기에서만 뺌 → Ctrl+A → Delete 면 안 보이는 것까지 지워짐 · 빈 곳에 스냅 표지 · 그립 · 동결은 「선택에서도 뺀다」고 적혀 있으나 안 뺌 | 그리기만 뺌 `helpers/draw-functions.ts:32-37` · 고르기 · 스냅은 잠금 층만 뺌 `state.ts:220-221` · `select-tool.helpers.ts:123-131` · `calculate-angle-guides-and-snap-points.ts:29-33` · 적힌 뜻 `App.types.ts:68` | 세 곳 모두(층 끄기 · 숨기기 쓴 때) | 집을 수 있는 목록 · 상자 · 스냅 거름에 「보이는가」 한 줄 | 작음 | 중간 | +| H7 | 폴리선 등 점 찍는 명령 중 C · U — 선(LINE)만 C(닫기) · U(한 토막 취소)를 받음 · 폴리선 · 호 · 치수 등에서 `C` = 원 명령으로 바뀌며 그리던 폴리선이 버려짐 · `U` = 앞서 끝낸 편집 하나를 되돌림(명령은 그대로) | 점 단계에 글 받기 없음 `sequence-tool.ts:282-293` · 명령 이름으로 넘어감 `input-controller.ts:562-571` · `run-command.ts:44-49` · UNDO 별칭 U `commands.file.ts:100-107` · 폴리선 `tools/draw/basic-draw-tools.ts:21-36` | 세 곳 모두 | 폴리선에 C · U 선택지(선과 같은 틀 meta.options) · 점 단계에서 C · U 는 명령 이름으로 안 봄 | 작음 | 중간 | +| H8 | 직교가 직교가 아님 — ① F3(객체 스냅) 끄면 직교 · 극 추적 가이드도 계산 안 함(상태막대 「직교」는 켜진 채 · 손가락 ⌖ 도 같음) ② 직교는 축 15px 안일 때만 붙음 → 직교 켜고 거리 숫자를 쳐도 커서가 축에서 벗어나 있으면 비스듬한 선 | ① `input-controller.ts:277-280` · `state.ts:481-490` ② `input-controller.ts:587-593` · `get-draw-guides.ts:62-88` | 세 곳 모두 | 가이드 계산을 F3 와 뗌 · 직교(90)일 땐 커서를 늘 축으로 내림 | 작음 | 중간 | +| H9 | 가이드 잡는 거리가 줌 따라 바뀜(추정) — 직교 · 극 추적 가이드는 「도면 단위 15」 안에서 잡음 · 점 스냅 · 고르기는 화면 px 로 나눠 씀 → 크게 줄인 평면도에선 가이드가 거의 안 잡히고 크게 키운 횡단에선 늘 보임 | `App.consts.ts:57` · `get-draw-guides.ts:42` 대 `calculate-angle-guides-and-snap-points.ts:28` · `pick-radius.ts:17-22` | 세 곳 모두 | 15 를 화면 배율로 나눔 한 줄 | 작음 | 낮음~중간 | +| H10 | 그리드(F7)가 눈금이 아님 — 화면에 박힌 24px 무늬 · 팬 · 줌 · 도면 단위와 무관 → 눈금으로 오해 | `drawControllers/screenCanvas.drawController.ts:338-352` | 상태막대 있는 곳 · F7 | 그리드 뺌(작음) 또는 도면 단위 그리드 + 격자 스냅(중간 · 정할 것 10) | 작음 | 낮음 | +| H11 | 명령행 ↑ 기록에 알림 줄이 섞임 — 「ERASE: 확정 도면 — 실행하지 않음」 같은 줄도 기록 → ↑ 로 부르면 「ERASE:」 꼴 → 실행 안 됨 | `run-command.ts:31-38` · `CommandLine.tsx:38` | 명령행 있는 곳(B07 · M02 양식) | 기록에 명령 이름만 | 작음 | 낮음 | +| H12 | 명령행 칸에 한글 자판으로 치면 「ㅣ」 꼴 → 명령 못 찾음(추정) · 캔버스에 바로 치기는 괜찮음 | `CommandLine.tsx:51-57` · 캔버스 쪽 `ime-sink.ts:1-5` | 명령행 있는 곳 | 두벌식 자모 → 영문 자리 바꿈 표로 찾기(⑥ D13 과 같음) | 작음 | 낮음 | + +### 1-3. 고치는 차례 + +1. H1 · H2 · H3 — 도면을 몰래 바꾸는 것부터(B06 상세는 고친 것이 곧바로 세션 초안 `B06_Section/B06_Section_UI_Cross_Detail_Cad.ts:317-330`). +2. H5 · H4 · H7 — 오토캐드 손버릇이 사고로 이어지는 것. +3. H6 · H8 — 보이는 것과 실제가 다른 것. +4. H9~H12 — 나머지. +- 시험 — 고르기 · 그립 누르기 흐름 · F키 · 보기 전용 새는 길 시험이 0건(B2 10장) → 흠 고칠 때 `resources/tester/cad/` 에 시험 틀부터. + +## 2. 묶음 추천 + +### 2-1. 묶음 표 + +| 묶음 | 무엇이 생기나(예) | 담는 줄(3장 번호) | 크기 | 앞몫(먼저 할 몫) | 임도 쓸모 | +|---|---|---|---|---|---| +| ⓪ 흠 막기 | 확정 도면이 안 고쳐짐 · Ctrl+C 가 복사로 · 그립을 눌러도 안 밀림 · 상자로 더해 고르기 — 예: 확정 횡단에서 Delete 를 눌러도 그대로 | 1장 H1~H12 · 6-4 · D6(C · U) | 작음 11 · 작음~중간 1 → 어림 3~6일(한 줄짜리 많음) | 전부 | 바닥 | +| ① 좌표 · 값 | 기준점에서 띄운 점 · 두 점 가운데 · 식 그대로 · 한 점 X + 다른 점 Y — 예: `FROM` → 옹벽 끝 → `@0.5,0` | 1-1 · 1-4 · 1-6~1-9 · 1-11 · D5 · D6(A) · D7(1-5 는 맨 뒤) | 작음 7 · 중간 4 → 어림 10일 | FROM · 두 점 가운데 · 식 · `@` · QC 값 넣기 · 선 이어 그리기 → 3일 | 상세 · 횡단 높음 | +| ② 스냅 · 추적 | 극 추적 각 고르기 · 비탈 비율 각 · Shift 직교 · 글자 기준점 스냅 · 연장선 · 평행 · Tab 후보 — 예: `<1:1.5` Enter → 비탈 방향 고정 → `3.2` Enter | 1-10 · 2-1~2-7 · D1~D4 · D12 | 작음 11 · 중간 6 → 어림 15일 | D1 · D2 · D3 · 2-1 · 글자 기준점(INS) · D12 → 3일 | 횡단 높음 | +| ③ 고르기 | 상자로 더하기 · 빼기 · 명령 중 누르는 대로 더함 · 앞 선택 P · 울타리 F · 올가미 — 예: 회전 명령에서 `P` → 방금 옮긴 것 | 3-7 · 4-1~4-14 · D14 | 작음 11 · 중간 4 → 어림 12일 | 4-1 · 4-5~4-8 · 4-14 · 3-7 · D14 → 4일 · 다음 울타리 | 전부 높음 | +| ④ 그립 | 누르고 · 옮기고 · 누르기 · 그립 중 숫자 · Space 로 옮기기 · 돌리기 · 치수 글자 비키기 — 예: 선 끝 그립 누르고 `2.5` Enter | 5-1~5-9 · 8-1 | 작음 4 · 중간 6 · 큼 2 → 어림 19일 | 핫 그립 · 그립 숫자 · 끝 늘이기 → 3일 · 다음 치수 글자 그립 | 치수 · 글자 높음 | +| ⑤ 편집 손버릇 | 우클릭 메뉴 · 휠 두 번 = 전체 · 두 번 눌러 치수 · 표 고치기 · 빠른 속성에 길이 · 글 높이 · 특성 맞추기 여러 개 · 간격띄우기 미리보기 — 예: 고른 채 우클릭 → [복사] | 3-6 · 6-1 · 6-2 · 6-4~6-9 · 8-2 · 8-3 · 9-1 · 9-3 · D15 | 작음 6 · 중간 8 · 큼 1 → 어림 19일 | 우클릭 메뉴 · 휠 두 번 · 두 번 눌러 고치기 · 특성 맞추기 여러 · 간격띄우기 미리보기 → 4~5일 | 중간~높음 | +| ⑥ 명령줄 · 안내 | 한글 자판 그대로 명령 · 한글 이름으로 찾기 · 선택지 단추 · ↑ 로 좌표도 다시 · 창 확대 — 예: 명령행에 「ㅣ」 → 선 | 1-2 · 1-3 · 3-1~3-4 · 6-3 · 9-2 · D8~D11 · D13 | 작음 7 · 중간 5 → 어림 11일 | D13 · 3-2 · D11 · 1-3 → 2일 | 중간 | +| ⑦ 자동화 | 글 여러 줄 붙여 차례 실행 · 내 단추 · 블록에 층 · 축척 미리 — 예: 표기 다섯 줄을 한 번에 | 3-5 · 7-1~7-3 · 8-4~8-8 | 중간 5 · 큼 3 → 어림 20일 | 7-3 블록 기본값만 | 낮음 | + +### 2-2. 차례 추천 + +- 먼저 셋 + 1. ⓪ 흠 막기 — 도면을 몰래 바꾸는 길을 막고 다른 묶음의 바닥(보기 전용 문 · Ctrl 거름 · 그립 대기 · 고르기 더함)을 깖. + 2. ③ 고르기 앞몫 — 모든 편집의 시작 · 작음 위주 · 오토캐드 손버릇과 가장 자주 부딪히는 곳. + 3. ② 스냅 앞몫 — 횡단 비탈을 키보드로(요구 224-7 「각도 → 길이」의 연장) · 작음 위주. +- 그다음 — ④ 그립 앞몫(3일) → ① 좌표 앞몫(3일) → ⑤ 앞몫(4~5일) — 앞몫 합 어림 20~23일. +- 나머지 몫 · ⑥ · ⑦ — 앞몫을 써 본 뒤 고름. + +### 2-3. 묶음끼리 먼저 할 관계 + +- ⓪ → 전부 — 새 고치기 길(그립 모드 · Ctrl+화살표 · 빠른 속성 칸 · 우클릭 메뉴)은 모두 H2 의 보기 전용 문을 지나야 함 · H1 Ctrl 거름은 ⑤ Ctrl 조합의 바닥 · H3 그립 대기는 ④ 첫걸음 · H4 · H5 는 ③ 첫걸음. +- ⑤ 우클릭 메뉴 틀(D15) → ② Shift+우클릭 스냅 메뉴(2-5) · ③ 겹친 것 목록(4-10) · ④ 그립 올림 메뉴(5-5) · ⑥ 선택지 단추(D8) — 메뉴 틀 하나를 같이 씀 · 정할 것 3 먼저. +- ④ 핫 그립(5-1) → 그립 숫자(5-9) · 모드 돌림(5-2) · B · C(5-3) · 손가락 그립. +- ③ 앞 선택 기억(4-6) → ⑤ 우클릭 [앞 선택] · ⑦ 스크립트. +- ① 마지막 점(1-1) → D5 선 이어 그리기. +- ② 극 추적 각(D1) · 켬 상태 기억(D12) → ☰ 설정 CAD 탭(332 · 이미 있음). +- ⓪ H7 ↔ 정할 것 7(점 단계에서 명령 이름 거절). +- ⑦ → ⑥(명령줄 해석 · 선택지 · 기본값이 안정된 뒤). + +## 3. 차이표(전체) + +- 줄 = B1 의 쓸모 높음 · 중간 71 줄(B1 차례 그대로) + 3-10 덧(331 에서 남은 빈 곳 · 임도 맞춤). +- 위험 = 깨짐(다른 기능) · 한글(자판) · 터치 중 걸리는 것만. + +### 3-1. 좌표 입력(B1 1장) + +| # | 오토캐드(쓸모) | 우리 · 근거 | 빠진 점 | 넣는다면(어디 · 어떻게) | 크기 | 위험 | 임도 쓸모 | 묶음 | +|---|---|---|---|---|---|---|---|---| +| 1-1 | 마지막 점 `@`(중간) | ○ — `@` 홀로 = 어느 꼴에도 안 맞음 `input-patterns.ts:30-65` · 도구마다 자기 앞 점 | 명령 사이 마지막 점 · ID 로 찍은 점 이어 쓰기 | 새 `inputController/input-last-point.ts`(점 보낼 때 적음 `input-controller.ts:610-614`) + `@` 홀로 = 그 점 | 작음 | 낮음 | 중간 · 상세 · 치수 | ① | +| 1-2 | 포인터 칸 Tab(`x` Tab `y`)(중간) | ◐ — Tab 칸 = 길이 · 각도뿐(331-4 `input-controller.ts:413-422`) · 첫 점엔 칸 없음 · 켬/끔 없음 | 첫 점 X · Y 칸 · 켬/끔 | `input-angle-lock.ts` 칸에 X · Y 갈래 · 그림 `input-field-draw.ts` | 중간 | 깨짐: Tab 이 브라우저 초점 이동을 뺏는 범위 늘어남 | 낮음 | ⑥ | +| 1-3 | 동적 프롬프트 · 추가 팁(중간) | ◐ — 커서 곁 상자 = 안내 · 좌표 · 길이/각도 · 후보 6 `input-field-draw.ts:37-110` · 그립 팁 · 끄기 없음 | 단계마다 쓸 수 있는 키 · 끄기 | `input-field-draw.ts` 한 줄 더 · 켬/끔 = ☰ 설정 | 작음 | 낮음 | 중간 · 오토캐드 모르는 사용자 | ⑥ | +| 1-4 | 식 입력 `'CAL`(높음) | ○ — 숫자 꼴만 `input-patterns.ts:10-12` · QUICKCALC 는 결과를 알림에만 `tools/utility/inquiry-tools.ts:120-137` | 거리 · 좌표 칸에 `3.2*1.5` | 이미 있는 QC 계산(숫자 · 사칙 · 괄호만 거른 뒤 계산)을 함수로 빼서 `parsePointText` 숫자 자리에서 씀 | 작음 | 깨짐: `-` 음수 · `,` 좌표 가름과 겹침 → 식 안 쉼표 금지 | 높음 · 횡단(높이 × 비율 길이) | ① | +| 1-5 | CAL 스냅 함수 `(end+end)/2`(중간) | ○ | 식 안에서 물건 점 집기 | 식 해석 + 점 집기 단계 | 큼 | 중간 | 낮음(두 점 가운데로 대부분 대신) | ① 맨 뒤 | +| 1-6 | QuickCalc 값 바로 넣기(중간) | ◐ — 계산은 됨 · 입력에 못 씀 `inquiry-tools.ts:120-137` | 결과를 다음 입력으로 | QC 결과를 커서 곁 입력 글에 넣음 | 작음 | 낮음 | 중간 | ① | +| 1-7 | 좌표 필터 `.x` `.y`(높음) | ○ — 해석 없음 | 한 점 X + 다른 점 Y | 새 `inputController/input-point-modifiers.ts`(FROM · 두 점 가운데와 같은 자리) · `.X` → 다음 점 X 만 · 「(Y 필요)」 → 다음 점 Y · Enter 분기는 한 줄만 `input-controller.ts`(635줄) | 중간 | 깨짐: `.5` 소수 꼴과 겹침 → `.X` · `.Y` 글자만 | 높음 · 횡단(같은 계획고 · 같은 측점 X) | ① | +| 1-8 | FROM(높음) | ○ — 해석 없음 | 기준점에서 `@dx,dy` 띄운 점 | 같은 새 파일 · 다음 점 = 임시 기준 · 이어 오는 `@` · `@d` · 투명 `'ZOOM` | 331 조사에 있음 | — | — | — | +| 명령줄 | 자동 고침(AutoCorrect) | 같은 오타를 정한 횟수만큼 고치면 기억 → 다음부터 바른 명령 제안 · 목록 파일 autoCorrectUserDB.pgp 에 `BOLCK, *BLOCK` | 오타 다시 치기 줄임 | 중간 | [S25] [S26] | +| 명령줄 | 비슷한 말(동의어) | acadSynonymsGlobalDB.pgp 에 `SYMBOL, *BLOCK` → SYMBOL 치면 BLOCK 제안 · 관리 탭 [별칭 편집 › 동의어 목록 편집] | 다른 말로도 명령 찾기 — 우리 CAD 라면 한글 낱말 짝으로 응용 가능 | 중간 | [S25] | +| 명령줄 | 이름 있는 것 찾기(내용 찾기) | 명령줄에 블록 · 해치 · 레이어 · 글자 스타일 · 치수 스타일 이름, 또는 `layer` · `hatch` · `block` 뒤 Tab → 목록에서 고름 → 레이어 · 스타일 = 현재로 · 블록 = 삽입 · 해치 = 칠할 곳 클릭 | 레이어 바꾸기 · 블록 넣기를 이름 몇 글자로 | 높음 | [S27] [S26] | +| 명령줄 | 시스템 변수 함께 · 목록 지연 | 제안 목록에 시스템 변수 넣기 · 따로 보이기 · 목록 나오는 시간(ms) | — | 낮음 | [S26] | +| 명령줄 | 반복 Ctrl+J · Ctrl+M · 취소 Ctrl+[ · Ctrl+\ | 마지막 명령 다시 · 현재 명령 취소(Esc 와 같음) | — | 낮음 | [S30] | +| 명령줄 | F2 기록 · Ctrl+9 명령줄 | F2 = 지난 명령 · 프롬프트 펼쳐 봄 · Ctrl+9 = 명령줄 창 보이기/숨기기 | 지나간 입력 확인 | 낮음 | [S22] [S30] | +| 투명 명령 | 보충 | 331 조사에 있음 + 보충: 쓰는 중 프롬프트 앞 `>>` · 물건 고르기 · 만들기 · 끝내기 명령은 안 됨 · 대화상자로 바꾼 값은 원래 명령 끝난 뒤 적용 · 예 `'grid` | — | — | [S24] | +| 반복 | MULTIPLE | `MULTIPLE` Enter → 명령 이름 → 취소할 때까지 그 명령 되풀이(옛 문서 = Ctrl+C · 지금 판 취소 키 = Esc) · 명령 이름만 반복(값은 매번) | 한 번 끝나는 명령(CIRCLE · INSERT · FILLET)을 연달아 | 중간 | [S67] (옛 문서 · 제3자) [S52] | +| 접두사 | `-` 명령줄판 | `-LAYER` 처럼 앞에 `-` → 대화상자 대신 명령줄 질문 | 스크립트 · 매크로에서 값 차례로 | 중간 | [S28] | +| 접두사 | `_` · `.` · `'` · `+` | `_LINE` = 지역판에서 영문 원래 이름 · `.LINE` = 재정의 · UNDEFINE 무시하고 원래 명령 · `'` = 투명 · `+DSETTINGS` = 대화상자 특정 탭(문서 밖 기능) | 언어판에 상관없는 매크로 · 탭 바로 | 낮음 | [S52] [S68] | +| 접두사 | `~` 파일 대화상자 | FILEDIA 0 일 때 첫 프롬프트에 `~` → 파일 대화상자 | — | 낮음 | [S28] | +| 미리보기 | 명령 미리보기 | TRIM · EXTEND · FILLET · CHAMFER · OFFSET · BREAK · LENGTHEN · MATCHPROP 중 커서를 물건에 대면 결과 임시 표시 · 안 되면 금지 배지 · 기본 켜짐(COMMANDPREVIEW) | 자르기 · 늘리기 · 모깎기 · 간격띄우기 결과를 누르기 전에 확인 | 높음 | [S29] [S33] | +| 미리보기 | 고르기 · 속성 미리보기 | 커서 아래 물건 강조(SELECTIONPREVIEW · 명령 중/없을 때 따로) · 리본 · 속성 창 목록 위에 올리면 바뀐 모양(PROPERTYPREVIEW) | 무엇이 골라질지 · 색 · 레이어 바꾸기 전 확인 | 중간 | [S33] | + +## 4. 고르기 + +| 분류 | 이름 | 쓰는 법 | 무엇에 좋나 | 쓸모 | 출처 | +|---|---|---|---|---|---| +| 기본 | 창 · 걸침 | 빈 곳 두 점: 왼쪽 → 오른쪽 = 완전히 안에 든 것만 · 오른쪽 → 왼쪽 = 걸친 것까지 · `BOX` 도 같은 규칙 | 모든 편집의 기본 | 높음 | [S32] [S31] | +| 올가미 | 누르고 끌기 | 누른 채 끌면 자유 모양 · 오른쪽으로 = 창 · 왼쪽으로 = 걸침 · 끄는 중 Space = 창 → 걸침 → 울타리 돌림 · PICKAUTO 로 끔 | 굽은 노선 따라 필요한 것만 둘러 고르기 | 높음 | [S32] [S33] | +| 다각형 | `WP` · `CP` | 고르기 단계에서 `WP` / `CP` → 점 여러 개로 다각형(선이 서로 엇갈리면 안 됨) | 비스듬한 영역 고르기 | 중간 | [S31] [S32] | +| 울타리 | `F` | `F` → 꺾은 선 → 그 선에 닿은 것 모두 | 여러 횡단에 걸친 지반선만 줄로 고르기 | 높음 | [S31] [S32] | +| 모두 | `ALL` · Ctrl+A | 얼림 · 잠금 레이어 빼고 모두 | — | 중간 | [S31] [S30] | +| 앞 선택 | `P` · Ctrl+Shift+L | 바로 앞 선택 묶음 다시 | 옮긴 것을 이어서 회전 · 복사 | 높음 | [S31] [S30] | +| 마지막 | `L` | 마지막에 만든(보이는) 물건 | 방금 그린 것 바로 | 중간 | [S31] | +| 빼기 · 더하기 | `R` · `A` · Shift+클릭 | `R` = 빼기 방식 · `A` = 더하기 방식 · 아무 때나 Shift+클릭(Shift+걸침창) = 빼기 | 크게 고른 뒤 몇 개 빼기 | 높음 | [S31] [S32] | +| 그 밖 | Undo `U` · SIngle `SI` · AUto `AU` · Multiple `M` · Group `G` | `U` = 마지막 더한 것 취소 · `SI` = 한 번 고르면 끝 · `AU` = 물건 위면 그것 · 빈 곳이면 창 · `M` = 강조 없이 여러 점 · `G` = 그룹 이름으로 · 전체 목록은 `?` | — | 낮음 | [S31] | +| 방식 | 더하기 방식(PICKADD) | 2(기본) = 계속 더함 · Shift 로 뺌 · 0 = 새로 고르면 바꿔치기 · Shift 로 더함 | 사용자 습관에 맞춤 | 낮음 | [S33] [S32] | +| 방식 | 암시 창(PICKAUTO) | 빈 곳 클릭 = 창 시작 · 물건 위에서 눌러 끌기 허용 여부 · 빈 곳 창으로 선택 비우기 | — | 중간 | [S33] | +| 겹침 | 선택 순환 | Ctrl+W 로 켜면 겹친 곳에 배지 → 누르면 목록에서 고름 · 꺼져 있어도 Shift+Space 로 임시 | 겹친 지반선 · 계획선 · 치수선 중 하나 | 높음 | [S34] [S30] | +| 조건 | 빠른 고르기(QSELECT) | 범위(도면 전체/현재 선택) · 물건 종류 · 속성 · 연산자(= ≠ > < 와일드카드 · 모두) · 값 · 포함/제외 · 현재 선택에 더하기 | 「레이어 X 의 글자」 「색 Y 의 선」만 | 중간 | [S35] | +| 조건 | 비슷한 것 고르기(SELECTSIMILAR) | 견본 고른 뒤 명령 → 같은 레이어 · 같은 종류 모두 · Settings 로 맞출 속성(색 · 블록 이름 등) 더함 | 같은 블록 · 같은 레이어 선 한꺼번에 | 높음 | [S36] | +| 묶음 | 그룹 | 그룹 고르기 켜면 하나만 눌러도 묶음 전체 · Ctrl+Shift+A = 그룹 고르기 켬/끔 · Ctrl+H = PICKSTYLE · 그룹에 손잡이 하나 · 테두리 표시 · 연관 해치는 경계 같이 | 측점 표시(글자 + 선) 한 덩어리 | 중간 | [S33] [S31] [S30] | +| 차례 | 고른 뒤 명령(PICKFIRST) 대 명령 뒤 고르기 | 켜면 먼저 고르고 명령 · 꺼도 명령 뒤 고르기는 됨 · 빠른 속성 · 두 번 눌러 고치기는 켜져야 | 고른 채 바로 지우기 · 옮기기 | 높음 | [S33] [S46] | + +## 5. 그립 편집 + +| 분류 | 이름 | 쓰는 법 | 무엇에 좋나 | 쓸모 | 출처 | +|---|---|---|---|---|---| +| 기본 | 핫 그립 · 여러 핫 그립 | 그립 클릭 = 핫 그립(기본 늘리기) · Shift 누른 채 여러 그립 → 함께 늘림(그 사이 모양 그대로) | 선 끝 여러 개 동시 이동 | 높음 | [S37] | +| 모드 | Space/Enter 돌림 · 우클릭 | 핫 그립 뒤 Space(또는 Enter)마다 늘리기 → 옮기기 → 돌리기 → 크기 → 대칭 · 우클릭 메뉴로 바로 고르기 | 명령 안 치고 이동 · 회전 · 대칭 | 높음 | [S37] | +| 옵션 | `B` 기준점 · `C` 복사 | 모드 중 `B` = 기준점 다시 · `C` = 복사하며 여러 개(그립 끌 때까지) | 원본 두고 여러 개 | 높음 | [S37] [S38] | +| 복사 | Ctrl 간격 · 각 맞춤 | 첫 복사 뒤 Ctrl 누른 채 찍으면 같은 간격(돌리기 모드에선 같은 각) | 등간격 표시 · 블록 복사 | 중간 | [S38] [S37] | +| 다기능 | 올림 메뉴 · Ctrl 로 항목 돌림 | 그립 위에 멈추면 물건별 메뉴 · 그립 누른 뒤 Ctrl = 메뉴 항목 차례로 · 옵션 [동적 그립 메뉴] · [Ctrl 돌림 허용] | 그 자리에서 맞는 편집 | 높음 | [S37] [S33] | +| 폴리선 | 정점 추가 · 제거 · 끝 늘이기 · 호로 · 선으로 | 정점 그립 [Add Vertex] · [Remove Vertex] · 끝 그립 [Extend Vertex](2023 · 방향 안 뒤집힘) · 중간 그립 [Convert to Arc] · [Convert to Line] | 노선 · 지반선 폴리선 손질 | 높음 | [S39] [S57] | +| 치수 글자 | 글자 자리 메뉴 | 글자 그립 메뉴: 늘리기 · 치수선과 함께 · 글자만 · 지시선과 · 치수선 위 · 세로 가운데 · 글자 자리 되돌림 | 겹친 치수 글자 정리 | 높음 | [S40] | +| 치수 화살표 | 이어 치수 · 기준 치수 · 화살표 뒤집기 | 화살표 그립에서 그 끝부터 연속 · 기준선 치수 만들기 · 화살표 뒤집기(AutoCAD 2012~) | 횡단 폭 치수 이어 만들기 | 높음 | [S69] (제3자) | +| 치수 입력 | 그립 치수 칸(DYNDIVIS · DYNDIGRIP) | 그립으로 늘릴 때 보일 칸: 길이 변화 1칸 / 길이 변화 + 결과 길이 2칸 / 결과 치수 · 길이 변화 · 절대 각 · 각 변화 · 호 반지름 중 골라 동시 · Tab 으로 칸 옮겨 숫자 | 「늘린 길이」 또는 「최종 길이」를 숫자로 | 높음 | [S12] | +| 설정 | 그립 크기 · 블록 안 그립 · 팁 · 개수 제한 | GRIPSIZE · GRIPBLOCK · GRIPTIPS · GRIPOBJLIMIT(기본 100개 넘게 고르면 그립 숨김) | 많이 고를 때 느려짐 막기 | 낮음 | [S33] | + +## 6. 마우스 · 키보드 · 터치 + +| 분류 | 이름 | 쓰는 법 | 무엇에 좋나 | 쓸모 | 출처 | +|---|---|---|---|---|---| +| 마우스 | 휠 굴려 확대 · 휠 눌러 끌어 이동 · ZOOMWHEEL · ZOOMFACTOR · MBUTTONPAN | 331 조사에 있음 | — | — | — | +| 마우스 | 휠 두 번 = 전체 보기 | 휠 단추 두 번 클릭 = 범위 확대 | 길 잃었을 때 한 번에 전체 | 높음 | [S41] | +| 마우스 | Ctrl+휠 끌기 · 휠 클릭 | Ctrl + 휠 눌러 끌기 = 조이스틱 이동 · MBUTTONPAN 0 이면 휠 클릭 = 스냅 메뉴 | — | 낮음 | [S41] | +| 키보드 | Ctrl+화살표 미세 이동 | 고른 물건을 직교로 조금씩 · 스냅 꺼짐 = 화면 2픽셀 · 스냅 켜짐 = 스냅 간격 | 글자 · 기호 위치 눈대중 조정 | 중간 | [S42] | +| 기능 키 | F1~F12 | F1 도움말 · F2 명령 기록 · F4 3D 스냅 · F5 등각 평면 · F6 동적 UCS · F7 그리드 · F9 스냅 · F8 과 F10 은 서로 끔(F3 · F8 · F10 · F11 · F12 는 331 조사에 있음) | 켬/끔 한 손가락 | 중간 | [S22] | +| 키보드 | Ctrl 조합(편집) | Ctrl+C/X/V · Ctrl+Z/Y · Ctrl+A 모두 · Ctrl+Shift+L 앞 선택 · Ctrl+J/M 반복 · Ctrl+1 속성 창 · Ctrl+Shift+P 빠른 속성 · Ctrl+8 계산기 · Ctrl+3 도구 팔레트 · Ctrl+2 디자인센터 · Ctrl+0 화면 정리 · Ctrl+9 명령줄 | — | 중간 | [S30] | +| 키보드 | Ctrl 조합(제도 켬/끔) | Ctrl+B 스냅 · Ctrl+G 그리드 · Ctrl+L 직교 · Ctrl+U 극 추적 · Ctrl+F 상시 스냅 · Ctrl+W 선택 순환 · Ctrl+Shift+A 그룹 · Ctrl+H PICKSTYLE · Ctrl+I 좌표 표시 · Ctrl+Shift+I 구속 추론 | F 키 대신 | 낮음 | [S30] | +| 클립보드 | 기준점 복사 · 블록으로 붙여넣기 · 원래 좌표로 붙여넣기 · 기준점 잘라내기 | Ctrl+Shift+C = 기준점 찍고 복사(붙일 때 그 점으로) · Ctrl+Shift+V = 붙인 것을 블록 하나로 · PASTEORIG = 다른 도면 같은 좌표에 · CUTBASE(2023) · PASTESPEC = 형식 골라 붙이기 | 다른 도면 · 다른 횡단으로 정확히 옮기기 | 높음 | [S44] [S30] [S57] | +| 끌어 놓기 | 왼쪽 끌기 · Ctrl 끌기 · 오른쪽 끌기 메뉴 | 고른 것을 왼쪽으로 끌면 옮김(도면 사이 · 다른 앱도) · Ctrl = 복사 · 오른쪽으로 끌면 놓을 때 [여기로 옮기기 · 여기로 복사 · 블록으로 붙여넣기 · 취소] · 스냅 무시 | 대충 위치 옮기기 | 낮음 | [S43] [S42] | +| 고치기 | 두 번 눌러 고치기 | 물건 두 번 클릭 → 종류별 편집기 또는 빠른 속성 · CUI [Double Click Actions] 에서 물건 종류(DXF 이름)마다 명령 하나 지정 | 글자 · 치수 · 블록 속성 바로 고치기 | 높음 | [S45] | +| 고치기 | 빠른 속성 | 고르면 커서 옆 작은 속성 창(QPMODE · 모든 물건/지정 종류) · 커서 따라/고정 자리 · 접힘 행 수 · PICKFIRST 켜져야 · Ctrl+Shift+P | 레이어 · 색 · 글자 높이 그 자리에서 | 높음 | [S46] [S30] | +| 고치기 | 속성 맞추기(MATCHPROP) | 원본 하나 → 대상 여러 개(창 고르기도) · `S` 설정으로 옮길 속성(레이어 · 색 · 선 종류 · 축척 + 같은 종류끼리 치수 스타일 · 글자 스타일) · 설정은 다음에도 유지 · 미리보기 | 레이어 · 스타일 한 번에 통일 | 높음 | [S47] | +| 고치기 | 속성 창 Ctrl+1 | 속성 창 켬/끔 · 숫자 칸에서 계산기 | — | 중간 | [S30] [S04] | +| 터치 | 데스크톱 터치(2022) | 명령 없을 때 한 · 두 손가락 끌기 = 이동 · 두 손가락 오므리기 = 확대 · 탭 = 고르기 · 한 손가락 끌기 = 창 · 걸침 · 한 손가락 두 번 탭 = 명령 끝 · 선택 해제 · 점 단계 탭 = 점 · 명령 중 두 손가락 이동 · 확대 · 손가락을 물건 위로 끌어 원하는 스냅이 보이면 뗌 | 태블릿 PC 현장 수정 | 낮음 | [S48] | +| 터치 | 모바일 앱 돋보기 · 숫자판 | 스냅 점 근처를 누른 채 있으면 손가락 위에 작은 확대창(줌 루페) → 손가락을 살짝 흔들어 원하는 점에 붙인 뒤 뗌 · 그린 뒤 길이 · 각도 수치를 눌러 숫자판으로 정확한 값 · 고른 뒤에만 편집 도구가 나옴 | 손가락에 가린 점 정확히 찍기 | 낮음 | [S49] (AU 강의 자료) | +| 터치 | 웹 앱 | 331 조사에 있음(명령줄 · 선택지 단추 · 약한 근거) · 웹 앱 전용 터치 도움말은 이번에도 못 엶 | — | — | — | + +## 7. 묶음 · 자동화 + +| 분류 | 이름 | 쓰는 법 | 무엇에 좋나 | 쓸모 | 출처 | +|---|---|---|---|---|---| +| 스크립트 | `.scr` · SCRIPT | 글자 파일 한 줄 = 명령줄 입력 · 빈칸 · 줄바꿈 = Enter(빈칸 든 글은 큰따옴표) · `-LAYER` 같은 명령줄판 · `;` 주석 · 끝에 빈 줄 · SCRIPT 실행 · 도면 창에 파일 끌어 놓기 · 시작 단추 `/b` · RSCRIPT 반복 · DELAY 멈춤 · Backspace 로 멈추고 RESUME | 같은 설정 · 그리기를 여러 도면에 | 중간 | [S50] | +| 동작 기록기 | ACTRECORD · ACTSTOP | 명령 · 입력값 녹화 → 이름 붙여 저장(.actm · ACTRECPATH) → 다시 실행 · 사용자 입력 요청(ACTUSERINPUT) · 메시지(ACTUSERMESSAGE) 끼워 넣기 · 대화상자 안 변경은 기록 안 됨 | 코딩 없이 반복 작업 묶기 | 낮음 | [S51] | +| 매크로 | 단추 · 메뉴 명령 문자열 | `^C^C` = 진행 중 명령 취소 · `;` = Enter · 빈칸 = Space · `\` = 사용자 입력 기다림 · `_` = 영문 이름 · `.` = 원래 명령 · `'` = 투명 · `*` = 반복 · `^]`(2023) = 고른 것 없으면 고르기 묻기 · 예 `^C^C_.text \.4 0 DRAFT Inc;;;Main St.;;;City, State;` | 자주 쓰는 순서를 단추 하나로 | 중간 | [S52] | +| 매크로 제안 | My Insights · Macro Advisor(2023) | 자주 쓰는 명령 순서(예 COPY → ROTATE)를 보고 상태 막대 알림 → 명령 매크로 창 [Insights] 에서 [Try] → 저장 · 문자열은 CUI 에서 고침 | 내 습관에서 매크로 자동 제안 | 낮음 | [S56] [S57] | +| AutoLISP | (참고) | `defun` + `c:이름` 으로 새 명령 · 내장 명령 실행 · 입력 장치 입력 받기 · 물건 읽고 고침 · 대화상자(DCL) | 맞춤 명령 개발 | 낮음 | [S53] | +| 도구 팔레트 | Ctrl+3 | 새 팔레트 · 디자인센터에서 도면 · 블록 · 해치 끌어 넣기 · 구분선 · 글 · 도구 속성으로 레이어 · 색 · 선 종류 강제 + 블록 축척 · 회전 · 해치 각 미리 정함 · 없는 레이어는 넣을 때 자동 생성 | 표준 블록(배수관 · 옹벽 등)을 정해진 레이어 · 축척으로 한 번에 | 중간 | [S54] [S55] [S30] | +| 사용자 지정 | 별칭 편집 · 단축키(CUI) | 331 조사에 있음 | — | — | — | + +## 8. 2023~2027 새 입력 보조 + +| 분류 | 이름 | 쓰는 법 | 무엇에 좋나 | 쓸모 | 출처 | +|---|---|---|---|---|---| +| 2023 | 폴리선 끝 정점 늘이기 그립 | 끝 그립 [Extend Vertex] — 폴리선 방향 안 뒤집고 끝에 정점 더함 | 노선 · 지반선 끝 이어 그리기 | 높음 | [S57] [S39] | +| 2023 | CUTBASE | 기준점 찍고 잘라내기(클립보드) | 다른 도면으로 정확히 옮기기 | 중간 | [S57] | +| 2023 | MLEADER 기존 글 고르기 | 새 지시선에 기존 MTEXT 를 골라 붙임 | 이미 쓴 설명 글에 지시선 | 중간 | [S57] | +| 2023 | Markup Import · Markup Assist | PDF · PNG · JPG 수정 지시를 트레이스로 겹침(MARKUPIMPORT) → AI 가 글자 · 지시선 · 수정 구름을 찾아 MTEXT · MLEADER · REVCLOUD 로 넣기(MARKUPASSIST) · 2025 Autodesk Docs 의 PDF 마크업 연동 · 2026 줄 긋기 · 경계 표시도 찾음 | 검토 의견을 다시 타이핑 않고 넣기 | 낮음 | [S62] [S57] [S76] [S64] | +| 2023 | Count · Trace | 영역 안 블록 · 물건 개수 세기 · 도면을 안 바꾸고 위에 검토용 겹 그리기 | — | 낮음 | [S57] | +| 2023.1~ | Autodesk Assistant | AI 도우미(2023.1 영 · 독 · 일 → 2024.1 AutoCAD · LT) · 자연어로 명령 · 도움 찾기 · 2027 MCP 기반 기술 미리보기(그리는 내용 문맥 이해) | 명령 이름 몰라도 질문으로 | 낮음 | [S57] [S58] [S65] [S66] | +| 2024 | Smart Blocks: Placement | Blocks 팔레트에서 넣을 때 같은 블록이 앞서 놓인 자리와 비슷한 곳 제안(위치 · 회전 · 축척 맞춤) · 클릭 = 받기 · Ctrl = 다른 제안 · Shift+W 또는 Shift+[ = 잠시 끔 · `AUTOPLACEMENT` 1 = 켬 | 반복 블록을 비슷한 자리에 빠르게 | 중간 | [S60] [S61] [S58] | +| 2024 | Smart Blocks: Replacement | BREPLACE → 고른 블록 참조를 비슷한 블록 제안 팔레트에서 골라 바꿈 · 축척 · 회전 · 속성값 유지 · `-BREPLACE` | 블록 교체 | 중간 | [S59] [S58] | +| 2024 | Activity Insights · COPYFROMTRACE | 도면에 누가 무엇을 했는지 이력 창 · 트레이스에서 도면으로 복사 | — | 낮음 | [S58] | +| 2025 | Smart Blocks: Search and Convert | BCONVERT → 모양 고르면 도면 안 같은 모양 모두 강조 → 전부/골라서 새 블록 · 기존 블록으로 바꿈 | 낱선으로 그린 반복 기호를 블록화 | 중간 | [S76] [S63] | +| 2025~2026 | Smart Blocks: Object Detection → Detect and Convert | AI 가 블록으로 바꿀 만한 반복 물건을 스스로 찾아 팔레트에 묶음 → 검토 막대로 확인 · 2025 기술 미리보기(평면도 건축 물건에 잘 맞음) · 2026 감지 개선 | — | 낮음 | [S76] [S63] [S64] | +| 2025 | 해치 경계 없이 · 따라 그리기 | 원 · 사각형 · 점 찍은 폴리선으로 영역 주면 경계 물건 없이 해치 · 경계 따라 그리며 폭 지정 | 띠 모양 해치(법면 표시 등) | 중간 | [S76] [S63] | +| 2026 | Centerline · Centermark 레이어 지정 | 중심선 · 중심 표시 명령 안에서 레이어 지정 | — | 낮음 | [S64] | +| 2027 | Geometry Cleanup | 고른 물건의 틈 · 넘침 · 모자람 · 각도 어긋남을 찾아 고침 제안 | 끊긴 지반선 · 노선 정리 | 중간 | [S65] [S66] | +| 2027 | Checkout | 여러 사람이 물건 · 영역을 예약해 같은 DWG 동시 편집 → 트레이스로 제출 | — | 낮음 | [S66] | +| 찾지 못함 | 명령줄 · 좌표 입력 자체 개선 | 연 What's New 쪽(2023 · 2024)에 자동 완성 · 좌표 입력 자체의 새 기능 없음 · 2025~2027 은 업계 소개 글 기준 | — | — | [S57] [S58] | + +## 9. 호환 CAD 가 더한 입력 보조(참고) + +| 분류 | 이름 | 쓰는 법 | 무엇에 좋나 | 쓸모 | 출처 | +|---|---|---|---|---|---| +| BricsCAD | Quad 커서 메뉴 | 물건 위에 커서 → 그 종류에 최근 쓴 명령 단추 · 펼치면 최근 6개 + 탭 묶음 · 겹친 물건은 Tab 으로 돌림 · Rollover Tips = 커서 아래 물건 속성 · 길게 우클릭 = 그리기 명령 Quad · F12 켬/끔 | 명령줄 없이 커서 옆에서 바로 | 중간 | [S70] (331 이름만) | +| BricsCAD | Manipulator | 길게 눌러 고르면 손잡이 위젯 → 축 = 한 방향 이동 · 호 = 회전 · 평면 사각 = 평면 이동 · 화살촉 = 대칭 · Ctrl = 복사 | 그립 모드 돌림 없이 바로 | 낮음 | [S71] | +| BricsCAD | Hotkey Assistant | 일부 명령 중 화면 아래 가운데에 쓸 수 있는 단축키 안내 · HOTKEYASSISTANT 1(기본) | 숨은 키 배우기 — 우리 CAD 안내줄에 응용 가능 | 중간 | [S72] | +| BricsCAD | 계산기 패널 스냅 함수 | `(end+mid)/2` · `cur` · `dist(cur,end)` 결과를 명령줄로 붙임 — 오토캐드 CAL 과 같은 꼴 | — | 낮음 | [S73] | +| ZWCAD | Smart Mouse(마우스 제스처) | 우클릭 끌기 방향(↑ · ← · ↓→)이나 글자 모양(C · L · 8 등)으로 명령 실행 · SMARTMOUSE 켬 · SMARTMOUSECONFIG 에서 제스처마다 명령 지정 · 비슷한 모양 동시 사용 비권장 | 키보드 없이 자주 쓰는 명령 | 중간 | [S74] | +| ZWCAD | Smart Voice · Smart Select | 물건 · 점 · 영역에 음성 메모(1~30초 · VOICEMAN 으로 글로 바꿈 · 출력 안 됨) · SMARTSEL = 종류 · 색 · 레이어 · 선 종류로 걸러 고르기(QSELECT 비슷) | 현장 의견 남기기 | 낮음 | [S74] | +| GstarCAD | 음성 주석(2025) | Voice Manager 로 도면에 음성 메모 녹음 · 재생 · 관리 — 그 밖 2025 입력 기능(올가미 · WP/CP · DIM 미리보기 · 구속)은 오토캐드에 이미 있음 | — | 낮음 | [S75] | + +## 확인 못함 · 뺀 것 + +- `*` 앞붙이 세계 좌표(WCS) 입력 — 연 공식 쪽(좌표 입력 개요 · 동적 입력)에 없음 → 뺌. +- `<<` · `<<<` 각도 덮어쓰기 — 연 쪽에서 확인 안 됨 → 뺌. +- AutoCAD 웹 앱 전용 터치 · 입력 도움말 · Autodesk 블로그(autodesk.com/blogs) · 기능 소개 쪽 — 접속 막힘(403) → 웹 앱은 331 의 약한 근거만. +- 2027.1 새 Lengthen 옵션 · 속성 편집기 Ctrl+A 등 — 원문(Autodesk 블로그) 403 → 뺌. +- 2023~2027 「명령줄 자동 완성 개선」 — 연 What's New 쪽에 해당 항목 없음. + +## 출처 목록 + +- [S01] 좌표 필터 개요(About Combining Coordinate Values) — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-LT/files/GUID-A3F6F0F9-A085-4176-972C-8D11068E7355.htm +- [S02] FROM (Command Modifier) — https://help.autodesk.com/cloudhelp/2018/ENU/AutoCAD-LT/files/GUID-0EB8FEA1-4C14-40C6-8254-DB6693DB481C.htm +- [S03] TRACKING (Command Modifier) — https://help.autodesk.com/cloudhelp/2018/ENU/AutoCAD-LT/files/GUID-C689F165-7C9E-4F09-96C3-BCA91DC4297C.htm +- [S04] About Accessing the QuickCalc Calculator — https://help.autodesk.com/cloudhelp/2018/ENU/AutoCAD-LT/files/GUID-577F070C-7E5B-4A03-90BD-DDFB3B3C6AAB.htm +- [S05] Use Snap Modes in Arithmetic Expressions (CAL) — https://help.autodesk.com/cloudhelp/2018/ENU/AutoCAD-LT/files/GUID-A75D5C9A-7F72-4404-8BDF-676490B17FBE.htm +- [S06] Syntax and Functions Reference (CAL · QUICKCALC) — https://help.autodesk.com/cloudhelp/2024/ENU/AutoCAD-LT/files/GUID-B93BD7BF-B69D-40D4-968E-1EB60F90D93E.htm +- [S07] About Unit Format Conventions — https://help.autodesk.com/cloudhelp/2018/ENU/AutoCAD-LT/files/GUID-FD3A36E1-FC7F-43CB-B7B5-1806FAAC12B2.htm +- [S08] -UNITS (Command) — https://help.autodesk.com/cloudhelp/2018/ENU/AutoCAD-LT/files/GUID-D396FBFE-6171-4A89-9E68-6CB082EBE0E1.htm +- [S09] ID (Command) — https://help.autodesk.com/cloudhelp/2024/ENU/AutoCAD-LT-MAC/files/GUID-F258AC00-5E9F-4B6B-A670-33F7708E3FB6.htm +- [S10] Dynamic Input Tab (Drafting Settings) — https://help.autodesk.com/cloudhelp/2021/ENU/AutoCAD-LT/files/GUID-38EC86CF-D96A-455F-A5DE-2CDA23C28FC4.htm +- [S11] Coordinate Entry with Dynamic Input (Did You Know) — https://help.autodesk.com/cloudhelp/2024/ENU/AutoCAD-DidYouKnow/files/GUID-683349C0-E5C2-4E16-8846-5523E71172A9.htm +- [S12] Dimension Input Settings Dialog Box — https://help.autodesk.com/cloudhelp/2021/ENU/AutoCAD-LT/files/GUID-65DF3E4D-ED53-4EBB-A6C4-2963595CE896.htm +- [S13] About Coordinate Entry — https://help.autodesk.com/cloudhelp/2027/ENU/AutoCAD-LT/files/GUID-0A0135DB-3216-482B-81DD-74E6DB8CA3E3.htm +- [S14] To Specify a Precise Point Location — https://help.autodesk.com/cloudhelp/2027/ENU/AutoCAD-LT/files/GUID-392167BC-8032-44D9-B4A9-DF4AC00DF5C4.htm +- [S15] OSNAP (Command) — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-LT/files/GUID-CF5780AD-D1AB-4526-9608-83D7952749E7.htm +- [S16] Object Snaps (Command Modifier) — https://help.autodesk.com/cloudhelp/2018/ENU/AutoCAD-LT/files/GUID-76B81C1A-373E-4BCD-975A-789FB36C89FE.htm +- [S17] Object Snap Tracking (Did You Know) — https://help.autodesk.com/cloudhelp/2022/ENU/AutoCAD-DidYouKnow/files/GUID-6FB309A8-2696-4383-8277-950E9E2756A8.htm +- [S18] About Tracking Points Automatically Using Object Snaps — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-LT/files/GUID-665DC37F-8C3E-414A-9369-72A13C0BE07A.htm +- [S19] Drafting Tab (Options) — https://help.autodesk.com/cloudhelp/2024/ENU/AutoCAD-LT/files/GUID-33A0F8E8-9981-4111-AD73-128B20D7975C.htm +- [S20] Polar Tracking Tab (Drafting Settings) — https://help.autodesk.com/cloudhelp/2021/ENU/AutoCAD-LT/files/GUID-D7CBB7B0-9140-4C53-88EF-08EAA09FA9D7.htm +- [S21] Snap and Grid Tab (Drafting Settings) — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-LT/files/GUID-66D637C9-6C47-420C-ADD1-83B64C73217A.htm +- [S22] Function Key Reference — https://help.autodesk.com/cloudhelp/2018/ENU/AutoCAD-LT/files/GUID-ACAA0279-047D-458E-889F-60BBFDD40489.htm +- [S23] About Inferring Geometric Constraints — https://help.autodesk.com/cloudhelp/2019/ENU/AutoCAD-LT/files/GUID-3CEDC74D-742F-465D-BD83-DD2B4E536CF7.htm +- [S24] About Entering Commands in the Command Window — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-LT/files/GUID-BB0C3E79-66AF-4557-9140-D31B4CF3C9CF.htm +- [S25] To Work With the Command Line Suggestion List — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-LT/files/GUID-74478DCE-92D4-49B1-93E9-DCF09D57FA0D.htm +- [S26] Input Search Options Dialog Box — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-LT/files/GUID-3179393F-958F-42EA-8A92-E9975A1D160B.htm +- [S27] About Searching for Named Objects — https://help.autodesk.com/cloudhelp/2018/ENU/AutoCAD-LT/files/GUID-C5D1D7EC-8BEC-4760-9993-FD2C809456BB.htm +- [S28] About Switching Between Dialog Boxes and the Command Line — https://help.autodesk.com/cloudhelp/2018/ENU/AutoCAD-LT/files/GUID-A02E1B47-AED7-4308-A6A3-94368852C2D8.htm +- [S29] Command Preview (Did You Know) — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-LT-DidYouKnow/files/GUID-2EBF913D-2FDC-4D66-A397-068899D492DC.htm +- [S30] 기본 단축키 표(Customization) — https://help.autodesk.com/cloudhelp/2025/ENU/AutoCAD-Customization/files/GUID-B6A9C126-230D-47F5-8973-464BC80E3736.htm +- [S31] SELECT (Command) — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-LT/files/GUID-0DD5DA73-9DC5-4424-8FED-7BBE3BE52A4D.htm +- [S32] Window, Fence, Lasso, and More (Did You Know) — https://help.autodesk.com/cloudhelp/2022/ENU/AutoCAD-DidYouKnow/files/GUID-D0D5C0C3-F092-448A-8E81-D38F27094639.htm +- [S33] Selection Tab (Options) — https://help.autodesk.com/cloudhelp/2023/ENU/AutoCAD-LT/files/GUID-DD2A36B6-1196-4205-9A07-32056292F0B5.htm +- [S34] Work with Stacked Objects (Did You Know) — https://help.autodesk.com/cloudhelp/2022/ENU/AutoCAD-DidYouKnow/files/GUID-D6B80776-EB76-4B70-BAC3-1E3778B910DA.htm +- [S35] Quick Select Dialog Box — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-LT/files/GUID-84E1116E-DEAE-4A0B-9364-F61DACF5C300.htm +- [S36] SELECTSIMILAR (Command) — https://help.autodesk.com/cloudhelp/2018/ENU/AutoCAD-LT/files/GUID-C160A9C9-1287-4111-8D27-05AFBAA7C29F.htm +- [S37] Grips (Did You Know) — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-LT-DidYouKnow/files/GUID-BBEA1F71-EB16-4D49-80D9-970A6909F508.htm +- [S38] About Making Multiple Copies With Grips — https://help.autodesk.com/cloudhelp/2021/ENU/AutoCAD-LT/files/GUID-BCD1FCDB-67A2-45B0-9679-063505733210.htm +- [S39] To Modify Polylines — https://help.autodesk.com/cloudhelp/2027/ENU/AutoCAD-LT/files/GUID-6242DF04-BD00-4A5D-891C-B131184D0D36.htm +- [S40] About Modifying Dimension Text — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-LT/files/GUID-C48417B4-5D9E-455D-96B2-C33A1AAAE690.htm +- [S41] About Pointing Devices — https://help.autodesk.com/cloudhelp/2018/ENU/AutoCAD-LT/files/GUID-B1CDEE41-F8BF-48A4-A895-273DBB295378.htm +- [S42] About Moving Objects — https://help.autodesk.com/cloudhelp/2018/ENU/AutoCAD-LT/files/GUID-11AB8C73-D35A-45DF-A7AE-4D03313DDACA.htm +- [S43] About Copying Objects — https://help.autodesk.com/cloudhelp/2021/ENU/AutoCAD-LT/files/GUID-0BFF5C5E-B1B4-4996-9AA4-57396FD6F6F2.htm +- [S44] Accessing Data from the Clipboard (Did You Know) — https://help.autodesk.com/cloudhelp/2022/ENU/AutoCAD-DidYouKnow/files/GUID-39A97950-6A89-4A01-A713-255600556C76.htm +- [S45] To Customize Double-Click Actions — https://help.autodesk.com/cloudhelp/2018/ENU/AutoCAD-LT/files/GUID-1F2689C9-9125-4596-AD52-73FC12716FFC.htm +- [S46] Quick Properties Tab (Drafting Settings) — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-LT/files/GUID-5D1EA6CC-2F6A-42D2-B49F-6F1B2634E32D.htm +- [S47] Matching Properties (Did You Know) — https://help.autodesk.com/cloudhelp/2024/ENU/AutoCAD-DidYouKnow/files/GUID-B40824DE-CE30-4664-8958-82DB21205AC7.htm +- [S48] Touch Enhancements (What's New 2022) — https://help.autodesk.com/cloudhelp/2022/ENU/AutoCAD-WhatsNew/files/GUID-D5F8C831-6152-485C-8AAD-970D3ADF47BE.htm +- [S49] AU 강의 자료 AS323988 「Road Dogs: Mobile Apps for Designers and Drafters」(Jim LaPier · PDF) — https://static.au-uw2-stg.autodesk.com/Class_Handout_AS323988_Jim_LaPier.pdf +- [S50] Scripts (Did You Know) — https://help.autodesk.com/cloudhelp/2024/ENU/AutoCAD-DidYouKnow/files/GUID-BE44AE86-7638-48C9-BE5B-C1DF8E4C8808.htm +- [S51] Action Recorder (Customization) — https://help.autodesk.com/cloudhelp/2015/ENU/AutoCAD-Customization/files/GUID-FEAD3614-CD33-4B60-BC00-4CBC98D8CBCB.htm +- [S52] About Special Control Characters in Command Macros — https://help.autodesk.com/cloudhelp/2025/ENU/AutoCAD-Customization/files/GUID-DDDB6E26-75E1-4643-8C6A-BEAEBA83A424.htm +- [S53] AutoLISP Developer's Guide — https://help.autodesk.com/cloudhelp/2025/ENU/AutoCAD-LT-AutoLISP/files/GUID-265AADB3-FB89-4D34-AA9D-6ADF70FF7D4B.htm +- [S54] To Work With Creating and Modifying Tool Palettes — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-LT/files/GUID-7946B6B8-654A-4708-BEBE-F4142E555DF0.htm +- [S55] About Setting Tool Properties — https://help.autodesk.com/cloudhelp/2021/ENU/AutoCAD-LT/files/GUID-021B36D2-5009-43FE-A78C-E4BE8EED0960.htm +- [S56] About Macro Insights and Command Macros — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-F14ADDEB-A704-4793-BD39-0DA970165A2E.htm +- [S57] What's New in AutoCAD 2023(2026 도움말 안) — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-WhatsNew/files/GUID-C6EC4B39-2588-43A2-99EB-A97EFDCEE8C6.htm +- [S58] What's New in AutoCAD 2024(2025 도움말 안) — https://help.autodesk.com/cloudhelp/2025/ENU/AutoCAD-WhatsNew/files/GUID-3890D5F7-04CB-4F3B-97FF-7A9577D41852.htm +- [S59] Smart Blocks: Replacement (What's New) — https://help.autodesk.com/cloudhelp/2025/ENU/AutoCAD-WhatsNew/files/GUID-4F8E11D6-917A-43F3-B596-C8F296D32519.htm +- [S60] About Block Insertion With Placement Suggestions — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-LT/files/GUID-BB2E7F62-C6D1-4D96-B78A-9810B9749CBC.htm +- [S61] To Insert a Block Using Automatic Placement(Mac 판 · Ctrl 대신 Cmd) — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-LT-MAC/files/GUID-F873E4E7-FD92-442F-A9E9-E0484505D311.htm +- [S62] Markup Import and Markup Assist (What's New) — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-WhatsNew/files/GUID-64CFD65E-ABD0-49A8-9218-D4E2D22BC070.htm +- [S63] Hagerman 「What's New in AutoCAD 2025」(제3자) — https://blog.hagerman.com/discover-whats-new-in-autocad-2025 +- [S64] Hagerman 「What's New in AutoCAD 2026」(제3자) — https://blog.hagerman.com/whats-new-in-autocad-2026 +- [S65] Hagerman 「What's New in AutoCAD 2027」(제3자) — https://blog.hagerman.com/draw-smarter-deliver-faster-whats-new-in-autocad-2027 +- [S66] Architosh 「Autodesk 2027 adds strong new features」(제3자) — https://architosh.com/2026/04/autodesk-2027-adds-strong-new-features/ +- [S67] Fourmilab · John Walker 옛 AutoCAD 문서 MULTIPLE(제3자) — https://fourmilab.ch/autofile/www/subsectionstar2_52_0_4.html +- [S68] draftsperson.net 명령 접두사(제3자) — https://blog.draftsperson.net/what-prefixes-are-available-for-commands-autocad-tip +- [S69] Dummies 「Tips for Editing Dimensions in AutoCAD」(제3자) — https://www.dummies.com/article/technology/software/design-software/autocad/tips-editing-dimensions-autocad-222017 +- [S70] BricsCAD The Quad cursor menu — https://help.bricsys.com/en-us/document/bricscad/getting-started-with-bricscad/exploring-the-interface/the-quad-cursor-menu +- [S71] BricsCAD Using the Manipulator — https://help.bricsys.com/en-us/document/bricscad/getting-started-with-bricscad/working-in-bricscad/using-the-manipulator +- [S72] BricsCAD HOTKEYASSISTANT — https://help.bricsys.com/en-us/document/system-variable-reference/h/hotkeyassistant-system-variable +- [S73] BricsCAD Calculator panel — https://help.bricsys.com/en-us/document/panels/calculator-panel +- [S74] ZWCAD Smart Features — https://www.zwsoft.com/user-guide/zwcad-smart-features +- [S75] Digital Engineering 24/7 「GstarCAD 2025 is released」(제3자) — https://www.digitalengineering247.com/article/gstarcad-2025-is-released +- [S76] Digital Engineering 24/7 「What's New in AutoCAD 2025」(제3자) — https://www.digitalengineering247.com/article/whats-new-in-autocad-2025/design +- [S77] CADTutor 포럼 M2P · MTP(제3자) — https://www.cadtutor.net/forum/topic/51390-object-snap-mid-between-2-points-m2p-mtp + +[S01]: https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-LT/files/GUID-A3F6F0F9-A085-4176-972C-8D11068E7355.htm +[S02]: https://help.autodesk.com/cloudhelp/2018/ENU/AutoCAD-LT/files/GUID-0EB8FEA1-4C14-40C6-8254-DB6693DB481C.htm +[S03]: https://help.autodesk.com/cloudhelp/2018/ENU/AutoCAD-LT/files/GUID-C689F165-7C9E-4F09-96C3-BCA91DC4297C.htm +[S04]: https://help.autodesk.com/cloudhelp/2018/ENU/AutoCAD-LT/files/GUID-577F070C-7E5B-4A03-90BD-DDFB3B3C6AAB.htm +[S05]: https://help.autodesk.com/cloudhelp/2018/ENU/AutoCAD-LT/files/GUID-A75D5C9A-7F72-4404-8BDF-676490B17FBE.htm +[S06]: https://help.autodesk.com/cloudhelp/2024/ENU/AutoCAD-LT/files/GUID-B93BD7BF-B69D-40D4-968E-1EB60F90D93E.htm +[S07]: https://help.autodesk.com/cloudhelp/2018/ENU/AutoCAD-LT/files/GUID-FD3A36E1-FC7F-43CB-B7B5-1806FAAC12B2.htm +[S08]: https://help.autodesk.com/cloudhelp/2018/ENU/AutoCAD-LT/files/GUID-D396FBFE-6171-4A89-9E68-6CB082EBE0E1.htm +[S09]: https://help.autodesk.com/cloudhelp/2024/ENU/AutoCAD-LT-MAC/files/GUID-F258AC00-5E9F-4B6B-A670-33F7708E3FB6.htm +[S10]: https://help.autodesk.com/cloudhelp/2021/ENU/AutoCAD-LT/files/GUID-38EC86CF-D96A-455F-A5DE-2CDA23C28FC4.htm +[S11]: https://help.autodesk.com/cloudhelp/2024/ENU/AutoCAD-DidYouKnow/files/GUID-683349C0-E5C2-4E16-8846-5523E71172A9.htm +[S12]: https://help.autodesk.com/cloudhelp/2021/ENU/AutoCAD-LT/files/GUID-65DF3E4D-ED53-4EBB-A6C4-2963595CE896.htm +[S13]: https://help.autodesk.com/cloudhelp/2027/ENU/AutoCAD-LT/files/GUID-0A0135DB-3216-482B-81DD-74E6DB8CA3E3.htm +[S14]: https://help.autodesk.com/cloudhelp/2027/ENU/AutoCAD-LT/files/GUID-392167BC-8032-44D9-B4A9-DF4AC00DF5C4.htm +[S15]: https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-LT/files/GUID-CF5780AD-D1AB-4526-9608-83D7952749E7.htm +[S16]: https://help.autodesk.com/cloudhelp/2018/ENU/AutoCAD-LT/files/GUID-76B81C1A-373E-4BCD-975A-789FB36C89FE.htm +[S17]: https://help.autodesk.com/cloudhelp/2022/ENU/AutoCAD-DidYouKnow/files/GUID-6FB309A8-2696-4383-8277-950E9E2756A8.htm +[S18]: https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-LT/files/GUID-665DC37F-8C3E-414A-9369-72A13C0BE07A.htm +[S19]: https://help.autodesk.com/cloudhelp/2024/ENU/AutoCAD-LT/files/GUID-33A0F8E8-9981-4111-AD73-128B20D7975C.htm +[S20]: https://help.autodesk.com/cloudhelp/2021/ENU/AutoCAD-LT/files/GUID-D7CBB7B0-9140-4C53-88EF-08EAA09FA9D7.htm +[S21]: https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-LT/files/GUID-66D637C9-6C47-420C-ADD1-83B64C73217A.htm +[S22]: https://help.autodesk.com/cloudhelp/2018/ENU/AutoCAD-LT/files/GUID-ACAA0279-047D-458E-889F-60BBFDD40489.htm +[S23]: https://help.autodesk.com/cloudhelp/2019/ENU/AutoCAD-LT/files/GUID-3CEDC74D-742F-465D-BD83-DD2B4E536CF7.htm +[S24]: https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-LT/files/GUID-BB0C3E79-66AF-4557-9140-D31B4CF3C9CF.htm +[S25]: https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-LT/files/GUID-74478DCE-92D4-49B1-93E9-DCF09D57FA0D.htm +[S26]: https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-LT/files/GUID-3179393F-958F-42EA-8A92-E9975A1D160B.htm +[S27]: https://help.autodesk.com/cloudhelp/2018/ENU/AutoCAD-LT/files/GUID-C5D1D7EC-8BEC-4760-9993-FD2C809456BB.htm +[S28]: https://help.autodesk.com/cloudhelp/2018/ENU/AutoCAD-LT/files/GUID-A02E1B47-AED7-4308-A6A3-94368852C2D8.htm +[S29]: https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-LT-DidYouKnow/files/GUID-2EBF913D-2FDC-4D66-A397-068899D492DC.htm +[S30]: https://help.autodesk.com/cloudhelp/2025/ENU/AutoCAD-Customization/files/GUID-B6A9C126-230D-47F5-8973-464BC80E3736.htm +[S31]: https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-LT/files/GUID-0DD5DA73-9DC5-4424-8FED-7BBE3BE52A4D.htm +[S32]: https://help.autodesk.com/cloudhelp/2022/ENU/AutoCAD-DidYouKnow/files/GUID-D0D5C0C3-F092-448A-8E81-D38F27094639.htm +[S33]: https://help.autodesk.com/cloudhelp/2023/ENU/AutoCAD-LT/files/GUID-DD2A36B6-1196-4205-9A07-32056292F0B5.htm +[S34]: https://help.autodesk.com/cloudhelp/2022/ENU/AutoCAD-DidYouKnow/files/GUID-D6B80776-EB76-4B70-BAC3-1E3778B910DA.htm +[S35]: https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-LT/files/GUID-84E1116E-DEAE-4A0B-9364-F61DACF5C300.htm +[S36]: https://help.autodesk.com/cloudhelp/2018/ENU/AutoCAD-LT/files/GUID-C160A9C9-1287-4111-8D27-05AFBAA7C29F.htm +[S37]: https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-LT-DidYouKnow/files/GUID-BBEA1F71-EB16-4D49-80D9-970A6909F508.htm +[S38]: https://help.autodesk.com/cloudhelp/2021/ENU/AutoCAD-LT/files/GUID-BCD1FCDB-67A2-45B0-9679-063505733210.htm +[S39]: https://help.autodesk.com/cloudhelp/2027/ENU/AutoCAD-LT/files/GUID-6242DF04-BD00-4A5D-891C-B131184D0D36.htm +[S40]: https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-LT/files/GUID-C48417B4-5D9E-455D-96B2-C33A1AAAE690.htm +[S41]: https://help.autodesk.com/cloudhelp/2018/ENU/AutoCAD-LT/files/GUID-B1CDEE41-F8BF-48A4-A895-273DBB295378.htm +[S42]: https://help.autodesk.com/cloudhelp/2018/ENU/AutoCAD-LT/files/GUID-11AB8C73-D35A-45DF-A7AE-4D03313DDACA.htm +[S43]: https://help.autodesk.com/cloudhelp/2021/ENU/AutoCAD-LT/files/GUID-0BFF5C5E-B1B4-4996-9AA4-57396FD6F6F2.htm +[S44]: https://help.autodesk.com/cloudhelp/2022/ENU/AutoCAD-DidYouKnow/files/GUID-39A97950-6A89-4A01-A713-255600556C76.htm +[S45]: https://help.autodesk.com/cloudhelp/2018/ENU/AutoCAD-LT/files/GUID-1F2689C9-9125-4596-AD52-73FC12716FFC.htm +[S46]: https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-LT/files/GUID-5D1EA6CC-2F6A-42D2-B49F-6F1B2634E32D.htm +[S47]: https://help.autodesk.com/cloudhelp/2024/ENU/AutoCAD-DidYouKnow/files/GUID-B40824DE-CE30-4664-8958-82DB21205AC7.htm +[S48]: https://help.autodesk.com/cloudhelp/2022/ENU/AutoCAD-WhatsNew/files/GUID-D5F8C831-6152-485C-8AAD-970D3ADF47BE.htm +[S49]: https://static.au-uw2-stg.autodesk.com/Class_Handout_AS323988_Jim_LaPier.pdf +[S50]: https://help.autodesk.com/cloudhelp/2024/ENU/AutoCAD-DidYouKnow/files/GUID-BE44AE86-7638-48C9-BE5B-C1DF8E4C8808.htm +[S51]: https://help.autodesk.com/cloudhelp/2015/ENU/AutoCAD-Customization/files/GUID-FEAD3614-CD33-4B60-BC00-4CBC98D8CBCB.htm +[S52]: https://help.autodesk.com/cloudhelp/2025/ENU/AutoCAD-Customization/files/GUID-DDDB6E26-75E1-4643-8C6A-BEAEBA83A424.htm +[S53]: https://help.autodesk.com/cloudhelp/2025/ENU/AutoCAD-LT-AutoLISP/files/GUID-265AADB3-FB89-4D34-AA9D-6ADF70FF7D4B.htm +[S54]: https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-LT/files/GUID-7946B6B8-654A-4708-BEBE-F4142E555DF0.htm +[S55]: https://help.autodesk.com/cloudhelp/2021/ENU/AutoCAD-LT/files/GUID-021B36D2-5009-43FE-A78C-E4BE8EED0960.htm +[S56]: https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-F14ADDEB-A704-4793-BD39-0DA970165A2E.htm +[S57]: https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-WhatsNew/files/GUID-C6EC4B39-2588-43A2-99EB-A97EFDCEE8C6.htm +[S58]: https://help.autodesk.com/cloudhelp/2025/ENU/AutoCAD-WhatsNew/files/GUID-3890D5F7-04CB-4F3B-97FF-7A9577D41852.htm +[S59]: https://help.autodesk.com/cloudhelp/2025/ENU/AutoCAD-WhatsNew/files/GUID-4F8E11D6-917A-43F3-B596-C8F296D32519.htm +[S60]: https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-LT/files/GUID-BB2E7F62-C6D1-4D96-B78A-9810B9749CBC.htm +[S61]: https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-LT-MAC/files/GUID-F873E4E7-FD92-442F-A9E9-E0484505D311.htm +[S62]: https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-WhatsNew/files/GUID-64CFD65E-ABD0-49A8-9218-D4E2D22BC070.htm +[S63]: https://blog.hagerman.com/discover-whats-new-in-autocad-2025 +[S64]: https://blog.hagerman.com/whats-new-in-autocad-2026 +[S65]: https://blog.hagerman.com/draw-smarter-deliver-faster-whats-new-in-autocad-2027 +[S66]: https://architosh.com/2026/04/autodesk-2027-adds-strong-new-features/ +[S67]: https://fourmilab.ch/autofile/www/subsectionstar2_52_0_4.html +[S68]: https://blog.draftsperson.net/what-prefixes-are-available-for-commands-autocad-tip +[S69]: https://www.dummies.com/article/technology/software/design-software/autocad/tips-editing-dimensions-autocad-222017 +[S70]: https://help.bricsys.com/en-us/document/bricscad/getting-started-with-bricscad/exploring-the-interface/the-quad-cursor-menu +[S71]: https://help.bricsys.com/en-us/document/bricscad/getting-started-with-bricscad/working-in-bricscad/using-the-manipulator +[S72]: https://help.bricsys.com/en-us/document/system-variable-reference/h/hotkeyassistant-system-variable +[S73]: https://help.bricsys.com/en-us/document/panels/calculator-panel +[S74]: https://www.zwsoft.com/user-guide/zwcad-smart-features +[S75]: https://www.digitalengineering247.com/article/gstarcad-2025-is-released +[S76]: https://www.digitalengineering247.com/article/whats-new-in-autocad-2025/design +[S77]: https://www.cadtutor.net/forum/topic/51390-object-snap-mid-between-2-points-m2p-mtp diff --git a/docs/raw/조사/2026-10-15_335_부록2_우리CAD현황.md b/docs/raw/조사/2026-10-15_335_부록2_우리CAD현황.md new file mode 100644 index 000000000..5addf9666 --- /dev/null +++ b/docs/raw/조사/2026-10-15_335_부록2_우리CAD현황.md @@ -0,0 +1,261 @@ +# B2 우리 CAD 입력 방법 현황 (요구 225 · 10-15) + +- 기준 = `main_laptop_1` 31e6adf7d(331 · 332 들어간 뒤) · 코드 읽기만(실행 · 빌드 · 화면 실측 안 함). +- 경로 줄임 — `src/` = `B07_DesignDetail/openwebcad/src/` · `cad/` = `resources/tester/cad/` · `py/` = `resources/tester/` · `A00/` = `A00_Common/` · 줄만 적은 근거(`:123`) = 바로 앞 파일. +- 기호 — ● 있음 · ◐ 일부 · ○ 없음 · (추정) = 코드 흐름 읽기로 낸 판단 · 실측 필요 · ⚠ = 흠으로 보이는 것. + +## 0. 우리에게 없는 것 · 약한 것 (오토캐드 일반 기능 기준 짐작) + +1. 좌표 꼴 — `@`(마지막 점) · `#`(절대) · 도분초 · 방위각 · 프롬프트 안 식 계산 · 단위 붙은 숫자 없음 · 첫 점에 `@` · 거리 숫자는 조용히 버림. +2. 동적 입력 — 첫 점 X · Y 칸 없음 · 켜고 끄기(F12) 없음 · 캔버스에 그린 글 상자라 칸을 눌러 고칠 수 없음. +3. FROM · 두 점 가운데(M2P) · 임시 추적점(TT) 없음. +4. 객체 스냅 — 접점 · 연장 · 평행 · 삽입점 · 겉보기 교차 없음 · 호 중간점 · 호 사분점 없음 · 종류별 켜고 끄기 없음 · 한 번 스냅에 QUA · NOD 없음 · Shift+우클릭 스냅 메뉴 없음. +5. 극 추적 = 45° 고정(각 증분 · 추가 각 못 고름) · 직교 = 90° 가이드 가까이일 때만 붙음(강제 아님 · Shift 임시 직교 없음) · F3 끄면 직교 · 극 · 스냅 추적 가이드도 같이 꺼짐. +6. 그리드 = 화면 고정 24px 무늬(도면 단위 아님 · 팬 · 줌 안 따름) · 격자 스냅(F9) 없음 · F11 = 브라우저 전체 화면(스냅 추적 아님). +7. 고르기 — 올가미 · 울타리 · 앞 선택(P) · 마지막(L) · `ALL` 글자 없음 · 상자는 늘 새로 고름(더하기 안 됨) · 명령 안 고르기는 누를 때마다 바뀜(Shift 로 더함) · 끌기 상자 안 됨(두 번 누름) · 빼기 = Ctrl(오토캐드 = Shift). +8. 그립 — 누른 채 끌어 놓기만(추정) → 그립 중 키보드 값 사실상 못 넣음 · 거리 숫자 안 받음 · 이동 · 회전 · 축척 · 대칭 그립 모드 · 복사 · 여러 그립 없음 · 호 · 해치 · 치수 · 타원 · 스플라인 그립 없음 · 손가락으로 그립 못 옮김(추정). +9. ⚠ Ctrl+C · Ctrl+X 연결 없음 — 글자 `C` · `X` 가 입력 칸에 쌓임(Enter 면 원 · 분해 명령) · Ctrl+Shift+C · Ctrl+Shift+V · Ctrl+1 없음 · 클립보드 = 앱 안 메모리. +10. 우클릭 메뉴 없음(우클릭 = Enter · 반복 안 함) · 가운데 두 번 = 전체 보기 없음 · 화살표 = 화면 이동(객체 미세 이동 없음). +11. 명령 — 투명 명령 `'` 없음 · ZOOM = 범위만(창 · 이전 · 배율 없음) · PAN = 안내 글만 · 명령 중 다른 명령 이름 = 지금 명령 버리고 새 명령 · 제도 설정 명령(OSNAP · DSETTINGS · UNITS) 없음. +12. 선택지 — LINE(C · U) · ARRAY(L · R) 둘뿐 · 눌러 고르는 선택지 없음 · PLINE 닫기 · 취소 · 호 없음 · LINE 첫 점 Enter 이어 그리기 없음 · SCALE 기준점 · ROTATE 방향점 키보드 안 받음 · 참조(R) · 복사(C) 없음. +13. 자동 완성 = 앞글자만(중간 글자 · 자주 쓴 순 없음) · 앞글자만 쳐도 첫 후보 실행 · ↑ 기록 = 아래 명령행(기본 숨김)에서만 · 명령 이름만 기억 · 새로고침에 사라짐 · 명령행 칸은 한글 자판이면 명령 못 찾음(추정). +14. 속성 칸 = 색 · 선가중치 · 선종류 · 층 · 투명도만(좌표 · 길이 · 반지름 · 글 높이 · 글 내용 못 고침) · 두 번 눌러 고치기 = 글자만. +15. 스크립트 · 매크로 · 동작 기록 없음 · 바꿀 수 있는 것 = 휠 방향 · 별칭뿐(F키 · Ctrl · 우클릭 · 스냅 거리 · 극 각 못 바꿈) · 스냅 · 직교 · 극 켬 상태 저장 안 됨. +16. ⚠ 확정 · 권한 없음(readOnly)에서 그립 · Delete · 특성 칸 · 리본 특성 · 도면층 관리가 막히지 않음 · 미리보기(viewOnly)도 Ctrl+A → Delete 가 먹음(코드 읽기 · 실측 필요). +17. B06 상세 · M02 카드 고치기 = 상태막대 · 명령행 없음 → 직교 · 극 · 스냅 · 그리드 = F키만 · 스냅 추적 켜고 끄기 · ↑ 기록 못 씀. + +## 1. 좌표 입력 꼴 + +| 항목 | 상태 | 어떻게 | 근거 | 시험 | +|---|---|---|---|---| +| 절대 `x,y` | ● | 점 단계 어디서나 · 쉼표 둘레 빈칸 · 음수 · `.5` 허용 | `src/inputController/input-patterns.ts:10-13,36-42` · `src/inputController/input-controller.ts:554-557` | `cad/keyboard-chain.test.ts:88-137` | +| 상대 `@dx,dy` | ◐ | 앞 점 있는 단계만 · 첫 점이면 조용히 버림(도구 안 멈춤) | `input-patterns.ts:14,43-49` · `src/helpers/get-point-from-event.ts:47-53` · `src/tools/factories/sequence-tool.ts:286-289` | `keyboard-chain.test.ts:139-175` | +| 극좌표 `@d 객체 스냅점 > 커서 · LINE · 시퀀스 도구(둘째 점부터) · MOVE · COPY 끝점 · RECTANG(대각 길이) · CIRCLE(반지름) · 그립은 안 받음 | `input-controller.ts:587-597` · `get-point-from-event.ts:30-43` · `src/tools/grip-edit-tool.ts:64-80` | `keyboard-chain.test.ts:106-113,176-189` | +| 마지막 점 `@` 홀로 | ○ | 어느 꼴에도 안 맞아 글로 버려짐 · 명령 사이 「마지막 점」 기억 없음(도구마다 자기 앞 점) | `input-patterns.ts:30-65` · `input-controller.ts:572-574` | 없음 | +| `#` 절대(동적 입력 꼴) | ○ | `x,y` 가 늘 절대 · `#` 해석 없음 | `input-patterns.ts:12-19` | 없음 | +| Tab 칸 입력 | ● (331-4) | 앞 점 있는 점 단계 — 숫자 + Tab = 지금 칸 잠금 + 다음 칸 · 빈 Tab = 칸만 바꿈 · 각도 칸 잠금 = 각도 고정 · 길이 잠근 채 빈 Enter = 그 길이 · Shift+Tab 도 같은 쪽 · 그 밖 단계 Tab = 브라우저 초점 이동 | `input-controller.ts:413-422,514-517,541-549` · `input-angle-lock.ts:30-35` | `keyboard-chain.test.ts:283-330` | +| 커서 곁 동적 입력 칸 | ◐ | 캔버스에 그린 글 상자(진짜 입력 칸 아님) — 친 글 · 안내문 · 커서 절대 좌표 · `[길이 ▸ 🔒] [각도]` 두 칸(앞 점→커서 실측) · 각도 고정 · 한 번 스냅 안내 · 명령 후보 6 · 첫 점 X · Y 칸 없음 · 끄기 없음 · 미리보기엔 안 그림 | `input-controller.ts:137-166` · `src/inputController/input-field-draw.ts:37-110` · `src/helpers/draw.ts:75-78` | `keyboard-chain.test.ts:283-330` | +| 식 계산 `1+2*3` | ○ | 점 · 숫자 단계 해석 없음 · 따로 QUICKCALC(QC) = 결과를 알림에만(입력에 못 씀) | `input-patterns.ts:11` · `src/tools/utility/inquiry-tools.ts:120-140` | 없음 | +| 단위(`mm` · `m` · 피트-인치) | ○ | 숫자만 · 도면 단위 그대로 · 소수점 `.` 만(`,` = 좌표 가름) · `1e3` · 분수 안 됨 | `input-patterns.ts:10-11` | 없음 | +| 도분초 · 방위각 · 라디안 | ○ | 십진 도만 | `input-patterns.ts:15-25` | 없음 | + +### 1-1. 도구별 키보드 값 받는 자리 + +| 도구 | 첫 값 | 다음 값 | 근거 | +|---|---|---|---| +| LINE | 클릭 · `x,y` | 클릭 · 숫자 · `x,y` · `@` · `<` · `C` · `U` · Enter = 끊고 새 첫 점 | `src/tools/line-tool.ts:73-139` | +| 시퀀스 공장(PLINE · ARC · 치수 · DIST 등 약 100) | 클릭 · `x,y`(숫자 · `@` 버림) | 점 = 전부 · 숫자 단계 = 숫자 · 기본값 Enter · 글 단계 = 자유 글 · 객체 단계 = 클릭만 | `src/tools/factories/sequence-tool.ts:276-337` | +| CIRCLE | 중심 클릭 · `x,y` | 반지름 숫자 · `x,y` · `@` | `src/tools/circle-tool.ts:63-100` | +| RECTANG | 클릭 · `x,y` | 숫자(커서 쪽 대각 길이) · `x,y` · `@` | `src/tools/rectangle-tool.ts:58-92` | +| MOVE · COPY | 기준점 클릭 · `x,y` | 클릭 · 숫자 · `x,y` · `@` · `<`(COPY 는 여러 번) | `src/tools/move-tool.ts:139-191` · `src/tools/copy-tool.ts:161-191` | +| ROTATE | 기준점 클릭 · `x,y` | 각도 숫자(반시계 +) 또는 클릭 두 번(기준선 · 방향 — 방향점 키보드 안 받음) | `src/tools/rotate-tool.ts:146-215` | +| SCALE | 기준점 **클릭만** | 배율 숫자 또는 클릭 두 번 | `src/tools/scale-tool.ts:151-215` | +| ARRAY | — | `L` · `R`(Enter = 직선) · 개수 · 간격 점(`x,y` · `@`) · 한 칸 각도 | `src/tools/array-tool.ts:147-280` | +| 그립 | (누름 = 집기) | 클릭 · `x,y` · `@` · 숫자 안 받음 | `src/tools/grip-edit-tool.ts:64-80` | +| 블록 놓기 | 클릭 · `x,y` | — | `src/tools/insert-block-tool.ts:72-84` | + +## 2. 제도 보조 + +| 항목 | 상태 | 어떻게 | 근거 | 시험 | +|---|---|---|---|---| +| 직교 F8 | ◐ | 90° 간격 가이드 켬/끔 · 가이드 가까이(약 15px) 올 때만 붙음(강제 아님) · Shift 임시 직교 없음 · 상태막대 「직교」 | `input-controller.ts:403-407` · `src/helpers/get-draw-guides.ts:36-49` · `src/components/StatusBar.tsx:36-43` | 없음 | +| 극좌표 추적 F10 | ◐ | 45° 고정 · 각 증분 · 추가 각 못 고름 · F8 과 한 값(`angleStep`)이라 둘 중 하나 · 기본 45 켜짐 | `input-controller.ts:408-412` · `src/state.ts:82` · `StatusBar.tsx:44-51` · `src/helpers/get-angle-guide-lines.ts:7-30` | 없음 | +| 객체 스냅 상시 F3 | ◐ | 켬/끔 하나(종류별 고르기 없음 · 종류표 꼴만 있고 안 씀) · 둘레 15px · 끄면 직교 · 극 · 스냅 추적 가이드도 꺼짐 · 상태막대 「객체 스냅」 · 손가락 ⌖ | `input-controller.ts:278-280,393-397` · `src/state.ts:481-490` · `src/App.consts.ts:52` · `src/App.types.ts:21` | 없음 | +| 객체 스냅 한 번 | ◐ (331-3) | 점 단계에서 `END` `MID` `CEN` `INT` `PER` `NEA` `NON` + Enter → 다음 한 점만 · 둘레 30px · 맞는 점 없으면 클릭 버림 · F3 꺼져도 먹음 · `QUA` `TAN` `NOD` 없음 · Shift+우클릭 메뉴 없음 | `src/inputController/input-snap-override.ts:1-80` · `input-controller.ts:239-243,558-561` | `keyboard-chain.test.ts:249-281` | +| FROM · 두 점 가운데(M2P) · 임시 추적점(TT) | ○ | 해석 · 도구 없음 | (src 검색 0건) | 없음 | +| 객체 스냅 추적 | ◐ | 스냅점에 1초 머물면 표시(최대 3점) · 그 점에서 같은 각 간격 가이드 · 가이드끼리 · 객체와 교차점 스냅 · 상태막대 「스냅 추적」만(F11 아님) · 각 간격 0(직교 · 극 끔)이면 안 뻗음 | `src/helpers/calculate-angle-guides-and-snap-points.ts:36-58` · `src/helpers/track-hovered-snap-points.ts:9-70` · `App.consts.ts:77,83` · `StatusBar.tsx:52-59` | 없음 | +| 그리드 F7 | ◐ | 화면 고정 24px 무늬만 — 도면 단위 아님 · 팬 · 줌 안 따름 | `input-controller.ts:398-402` · `src/drawControllers/screenCanvas.drawController.ts:339-354` | `src/helpers/zoom-out-perf.test.ts:183-188` | +| 스냅(F9 · 격자 스냅) | ○ | 없음 | — | 없음 | +| 바꿀 수 있는 수치 | ○ | 스냅 15px · 한 번 30px · 집기 10px · 머묾 1초 · 그리드 24px 모두 상수 | `App.consts.ts:52-83` · `input-snap-override.ts:58-59` | — | + +### 2-1. 객체 스냅 종류 + +| 종류 | 상시 | 한 번 | 만드는 객체 · 근거 | +|---|---|---|---| +| 끝점 END | ● | ● | 선 · 호 끝 · 사각형 · 그림 모서리 · 해치 꼭짓점 · 표 격자 · 폴리선 선 구간 — `src/entities/LineEntity.ts:104-118` · `ArcEntity.ts:132-146` · `RectangleEntity.ts:109-145` · `HatchEntity.ts:183-185` · `TableEntity.ts:293-298` · `PolyLineEntity.ts:114-116` | +| 중간점 MID | ◐ | ● | 선 · 사각형 · 그림 변(호 중간 없음) — 같은 곳 | +| 중심 CEN | ● | ● | 원 · 호 — `CircleEntity.ts:98-123` · `ArcEntity.ts:132-137` | +| 사분점 QUA | ◐ | ○ | 원만(호 없음) — `CircleEntity.ts:106-122` | +| 교차 INT | ● | ● | 커서 둘레 객체끼리 + 가이드와 — `src/helpers/get-draw-guides.ts:51-88` | +| 노드 NOD | ● | ○ | 점 객체 — `PointEntity.ts:87-94` | +| 수직 PER | ○ | ● | 앞 점에서 내린 발 — `input-snap-override.ts:77-79` | +| 근처점 NEA | ○ | ● | `input-snap-override.ts:74-76` | +| 접점 TAN | ○ | ○ | 이름만(`App.types.ts:11`) · 만드는 곳 0 | +| 연장 · 평행 · 삽입점 · 겉보기 교차 · 기하 중심 | ○ | ○ | 글자 · 치수 · 화살표는 스냅점 0(`TextEntity.ts:191-193` · `MeasurementEntity.ts:483-485`) | + +### 2-2. 기능키 + +| 키 | 우리 | 근거 | +|---|---|---| +| F1 · F2 · F4 · F6 · F9 | 아무 일 없음(키 삼킴) | `input-controller.ts:427-428` | +| F3 | 객체 스냅 켬/끔 | `:393-397` | +| F5 | 브라우저 새로고침 | `:382-385` | +| F7 | 그리드 무늬 | `:398-402` | +| F8 | 직교(90°) | `:403-407` | +| F10 | 극 추적(45°) | `:408-412` | +| F11 | 브라우저 전체 화면(스냅 추적 아님) | `:386-391` | +| F12 | 개발자 도구(동적 입력 끄기 아님) | `:378-381` | + +## 3. 명령 입력 + +| 항목 | 상태 | 어떻게 | 근거 | 시험 | +|---|---|---|---|---| +| 캔버스에 바로 치기 | ● | 아무 글자 → 대문자로 쌓임(글 단계는 친 그대로) · Backspace 한 자 지움 · 커서 곁 상자에 보임 | `input-controller.ts:441-444,476-482` | `src/inputController/ime-sink.test.ts:58-140` | +| 아래 명령행 | ◐ | 기본 숨김 · 상태막대 「명령행」 단추로 켬 · 최근 3줄 · 안내문 · 후보 단추 8 · 빈 Enter = 확정 · 반복 | `src/components/Toolbar.tsx:23,70` · `src/components/CommandLine.tsx:8-108` · `StatusBar.tsx:68-75` | 없음 | +| 자동 완성 | ◐ | 앞글자 일치만(내 별칭 → 기본 별칭 → 이름 차례) · 커서 곁 6 · 명령행 8(눌러 실행) · 명령행 Tab = 첫 후보 이름 채움 · 중간 글자 · 자주 쓴 순 없음 | `src/commands/registry.ts:137-150` · `input-controller.ts:503-508` · `CommandLine.tsx:15,78-81,89-105` | `cad/input-prefs-cad.test.ts:69-75` | +| 이름 해석 차례 | ● | 이름 > 내 별칭 > 기본 별칭 > 앞글자 첫 후보(앞글자만 쳐도 실행) · 대소문자 무관 | `registry.ts:124-134` | `input-prefs-cad.test.ts:61-80` | +| 별칭 표 원천 | ● | `src/commands/commands.{draw,modify,annotate,layer,utility,insert,view,file}.ts` 의 `aliases` → `registry.ts:51-58`(겹치면 먼저 것) · 거울 `src/commands/command-catalog.json`(153 명령 · 별칭 88 · 별칭 있는 명령 82) | 같은 곳 | `cad/command-catalog.test.ts:11-30` | +| 내 별칭 | ● (332) | ☰ [설정] ② CAD 단축키 표 · 영문 · 숫자만 · 내 별칭끼리 · 명령 이름과 겹침 금지 · [저장] 때 계정 pref(`aislo:pref:input-prefs` `cadAliases`) · 부를 때마다 읽어 바로 따름 | `A00/app_settings_cad.ts:38-118` · `A00/input_prefs.ts:13-51` · `registry.ts:116-122` | `input-prefs-cad.test.ts:61-80` · `py/test_app_settings.py` · `py/test_input_prefs.py` | +| 마지막 명령 반복 | ◐ | 빈 Enter · Space = 선택 도구 대기일 때 직전 명령 · 우클릭 = 확정만(반복 안 함) · 명령행 빈 Enter 도 반복 | `input-controller.ts:518-526` · `run-command.ts:66-72` · `CommandLine.tsx:20-24` | `ime-sink.test.ts:115-140` | +| 입력 기록 ↑↓ | ◐ | 아래 명령행 칸에서만 · 실행한 명령 이름만(좌표 · 값 아님) · 200줄 메모리(새로고침에 사라짐) · 캔버스 ↑↓ = 화면 이동 | `run-command.ts:10-24` · `CommandLine.tsx:31-39,70-77` | 없음 | +| 투명 명령 | ◐ | 즉시형 명령(ZOOM · ZOOMIN · ZOOMOUT · REGEN · LAYER 창 등)은 지금 도구를 안 끊음 · 휠 · 가운데 팬 · 화살표는 언제나 · `'ZOOM` 꼴 없음 · 각도 고정 · 동적 잠금은 풀림 | `run-command.ts:41-54` · `input-controller.ts:565-571` | 없음 | +| 명령 중 다른 명령 이름 | ● (오토캐드와 다름) | 도구형 명령 이름 = 지금 명령 버리고 새 명령(오토캐드 = 점 아님으로 거절) | `run-command.ts:44-50` | `keyboard-chain.test.ts:243-248` | +| 옵션 키워드 | ◐ | 글자 + Enter — LINE `[닫기(C)/명령취소(U)]` · ARRAY `[직선(L)/원형(R)] <직선>` 둘뿐 · 눌러 고르는 선택지 없음 · 점 단계는 `meta.options` 글자만 도구로 · 나머지는 명령 이름으로 | `line-tool.ts:92-132` · `array-tool.ts:147-169` · `src/inputController/ime-sink.ts:28-34` · `input-controller.ts:562-564` | `keyboard-chain.test.ts:203-248` | +| 기본값 `<…>` | ◐ | 숫자 · 글 단계 `defaultValue` 만 빈 Enter 로 채택 | `sequence-tool.ts:302-310` | 없음 | +| 안내문 | ◐ | 우리 도구 · 331 손본 도구 = 한글 · 대괄호 선택지 표기는 LINE · ARRAY 만 | `line-tool.ts:76,95` · `array-tool.ts:148` | `keyboard-chain.test.ts:211-222` | +| Space = Enter | ● | 글 단계(TEXT · MTEXT · 표 칸 등)는 띄어쓰기 · Shift/Ctrl+Space · 길게 눌러 반복 무시 | `input-controller.ts:458-463` · `ime-sink.ts:39-51` | `ime-sink.test.ts:58-113` | +| Esc 차례 | ● | 각도 고정 · 한 번 스냅 풂 → 고름 풂 → 글 없으면 도구 취소 · 있으면 글 지움 | `input-controller.ts:485-500` | `keyboard-chain.test.ts:114-127,265-274` | +| 한글 자판 | ◐ | 명령 단계 = 숨은 IME 받개 초점 풀어 영문 글자로 받음 · 글 단계 = 숨은 칸에 초점 · 조합 글 커서 곁에 보임 · 조합 중 키는 단축키로 안 먹음 · Ctrl 단축키 = 자판 자리(`code`) · 명령행 칸은 일반 입력 칸이라 한글 자판이면 「ㅣ」 꼴로 들어가 명령 못 찾음(추정) · 내 별칭에 한글 못 씀 | `ime-sink.ts:1-103` · `input-controller.ts:374-376` · `A00/key_shortcut.ts:11-49` · `CommandLine.tsx:51-57` · `A00/app_settings_cad.ts:48` | `ime-sink.test.ts:11-56` · `cad/ctrl-shortcut-hangul.test.ts` | + +## 4. 고르기 + +| 항목 | 상태 | 어떻게 | 근거 | +|---|---|---|---| +| 한 번 누름 | ● | 집기 반경 10px(손가락 = 접촉 크기 · 최소 24px) · 기존 고름을 **바꿈** · 그룹은 통째 · 잠금층 못 고름 | `src/tools/select-tool.helpers.ts:74-82` · `src/helpers/pick-radius.ts:17-22` · `src/state.ts:365-376` | +| 창(왼→오 · 파랑 · 다 든 것) · 걸침(오→왼 · 초록 · 걸친 것) | ● | 시작점 기준 방향으로 가름 | `select-tool.helpers.ts:114-169,185-200` | +| 끌기 상자 | ◐ | 선택 도구일 때 = 누른 채 끌기(4px 넘음) 또는 두 번 누름 · **명령 안 고르기(MOVE · ERASE 등) = 두 번 누름만**(누름은 놓을 때 한 점) | `input-controller.ts:195-209,311-330` | +| 올가미 · 울타리(F) | ○ | — | — | +| 전체 | ◐ | Ctrl+A(잠금층 뺌) · 고르기 중 `ALL` 글자 없음 | `input-controller.ts:438-440` | +| 앞 선택(P) · 마지막(L) | ○ | 고르기 중 `P` = PAN 안내 · `L` = LINE 실행(지금 명령 버림) | `input-controller.ts:565-571` · `registry.ts:125-134` | +| 더하기 | ◐ | Shift+누름 · Ctrl+누름 · **상자는 늘 새로 고름**(기존 고름 버림) · Shift+상자 미구현 | `select-tool.helpers.ts:83-99,114-118,168` | +| 빼기 | ◐ | Ctrl+누름 = 넣고 빼기(오토캐드 = Shift+누름) · ⚠ Ctrl+상자 = 상자 안 안 고른 것만 남김 | `select-tool.helpers.ts:83-93,131-164` | +| 겹친 것 돌려 고르기 | ● | 같은 자리 다시 누르면 다음 후보(고름 바뀜) · Shift+Space · 후보 목록 창 없음 | `select-tool.helpers.ts:34-49` | +| 비슷한 것 | ◐ | SELECTSIMILAR(종류 · 색 · 층 같은 것) · QSELECT(종류 이름 글로) | `src/tools/utility/selection-tools.ts:9-48` | +| 고른 뒤 명령 | ◐ | MOVE · COPY · ROTATE · SCALE · ARRAY · PEDIT = 고르기 건너뜀 · 시퀀스 「고르기」 단계(ERASE · GROUP · AREA 등) = Enter 한 번 더 · BLOCK · TOAREA = 먼저 골라야 · Delete 키 = 바로 지움 | `move-tool.ts:81-97` · `sequence-tool.ts:321-337` · `run-command.ts:35-39` · `src/tools/modify/structure-tools.ts:71-81` | +| 명령 뒤 고르기 | ◐ | 속에 선택 도구 · 누를 때마다 바뀜(Shift 로 더함) · Enter · Space · 우클릭으로 끝 | `move-tool.ts:98-138` · `sequence-tool.ts:321-337` | +| 마우스 스침 강조 | ◐ | 선택 도구 활성일 때만(명령 안 고르기 땐 없음) | `input-controller.ts:283-293` · `src/main.tsx:89-111` | + +- 고르기 시험 = 없음(그룹 · 잠금층만 다른 시험이 간접으로 봄). + +## 5. 그립 + +| 항목 | 상태 | 어떻게 | 근거 | 시험 | +|---|---|---|---|---| +| 그립 종류 | ◐ | 선 = 끝 2 · 가운데(옮김) · 사각형 = 모서리 4(맞은편 고정) · 원 = 중심(옮김) · 사분 4(반지름) · 폴리선(선 구간만) = 꼭짓점 · 구간 가운데 · 표 = 왼쪽 위(옮김) · 열 · 행 경계 · 칸 있는 글 · 그림 = 모서리 4(크기) · 가운데(옮김) · 글 · 점 = 기준점 · 호 · 해치 · 치수 · 타원 · 스플라인 · 호 섞인 폴리선 = 없음 | `src/helpers/grips.ts:1-8,74-148` | `cad/grips.test.ts:25-80` | +| 조작 | ◐ | 고른 뒤 그립 위 누름 = 그립 도구로 바뀜 → 놓는 자리에 적용 → 곧 선택 도구 = **누른 채 끌어 놓기만**(클릭 두 번 방식 아님 · 주석 「집어서 놓는 방식」과 다름 · 추정) · 끄는 동안 스냅 · 가이드 · 미리보기 | `select-tool.helpers.ts:55-72` · `input-controller.ts:195-211,311-330` · `grip-edit-tool.ts:1-4,26-30,64-112` | 없음(흐름 시험 0) | +| 다기능 | ◐ | 폴리선 구간 가운데 끌기 = 꼭짓점 더함 · Ctrl+꼭짓점 누름 = 꼭짓점 뺌 · 선 · 원 가운데 = 옮김 · Space 로 이동 · 회전 · 축척 · 대칭 바꾸기 · 복사 · 여러 그립 · 기준점 · 올림 메뉴 없음 | `grips.ts:151-241` · `select-tool.helpers.ts:58-67` | `grips.test.ts:55-75` | +| 그립 중 숫자 | ◐ | 도구는 `x,y` · `@` · `<` 받음 · 거리 숫자 안 받음 · 놓는 순간 끝나 누른 채 쳐야 함(사실상 못 씀 · 추정) | `grip-edit-tool.ts:64-80` | 없음 | +| 손가락 | ○ (추정) | 톡 = 같은 자리 누름 + 놓음이라 그립이 바로 끝남 · 끌기 = 화면 이동 | `src/inputController/input-touch.ts:46-50,85-111` | 없음 | +| 칸 모서리 끌기(따로) | ● | 고른 칸 있는 글 · 그림 모서리 9px 안 = 진짜 끌기로 크기 · 확정 도면 막음 | `src/integration/box-resize-drag.ts:14-26,67` | 없음 | + +## 6. 마우스 · 키보드 · 터치 · 펜 + +| 항목 | 상태 | 어떻게 | 근거 | 시험 | +|---|---|---|---|---| +| 휠 | ● | 커서 자리 고정 줌 · 방향 = ☰ [설정] 당김 · 밀기(모든 화면 한 값) · 줄 · 쪽 단위 환산 · 한 칸 10% | `src/inputController/input-wheel.ts:14-29` · `A00/input_prefs.ts:54-64` · `App.consts.ts:150-160` | `src/inputController/input-wheel.test.ts:13-33` · `input-prefs-cad.test.ts:37-59` | +| 트랙패드 핀치 | ● | Ctrl+휠 잔 델타 = 늘 벌리면 확대 · 브라우저 확대 막음 | `input-controller.ts:111-113,125-133` · `A00/input_prefs.ts:59-64` | `input-wheel.test.ts:25-28` | +| 가운데 단추 끌기 | ● | 팬 · 자동 스크롤 · 붙여넣기 막음 | `input-controller.ts:120-124,264-272,312-316` | 없음 | +| 가운데 두 번 = 전체 보기 | ○ | 전체 보기 = `Z` · 우하단 ⛶ 뿐 | `src/commands/commands.view.ts:42-51` · `src/components/ViewControls.tsx:28-35` | — | +| 왼쪽 두 번 누름 | ◐ | 글자 위 = 그 자리 입력 칸(Enter 적용 · 여러 줄 Ctrl+Enter · Esc 취소 · 수량표 글 제외) · 미리보기 = 전체 맞춤 · 선 · 폴리선 등 = 아무 일 없음 | `src/integration/aislo-drawing-bridge.ts:114-118` · `src/integration/text-inplace-edit.ts:1-5,210-247` | `cad/text-inplace-edit.test.ts:124-220` | +| 우클릭 | ◐ | = Enter(확정) · 메뉴 없음 · 길게 누름 메뉴 없음 | `input-controller.ts:116-119,175-180` | `ime-sink.test.ts:115-140` | +| 화살표 | ◐ | 화면 이동(Shift = 크게) · 고른 객체 미세 이동 없음 | `input-controller.ts:346-367,464-475` | 없음 | +| Ctrl+Z · Ctrl+Y · Ctrl+Shift+Z | ● | 되돌리기 · 다시(한글 자판도 자리로) | `input-controller.ts:429-434,616-634` · `A00/key_shortcut.ts:31-49` | `cad/ctrl-shortcut-hangul.test.ts:38-116` | +| Ctrl+A | ● | 잠금층 뺀 전부 고름 | `input-controller.ts:438-440` | `ctrl-shortcut-hangul.test.ts:46-50` | +| Ctrl+S | ◐ | M02(부모 [저장])만 · B07 · B06 = 아무 일 없음(브라우저 저장 창도 막음) | `input-controller.ts:435-437` | 같은 곳 | +| Ctrl+V | ● | 시스템 클립보드 글이 DXF = 서버가 도형으로 바꿔 원좌표 붙임 · 아니면 앱 안 클립보드 PASTECLIP(놓을 자리 지정) | `input-controller.ts:423-426` · `src/integration/dxf-clip-paste.ts:76-87` | `cad/dxf-clip.test.ts` · `py/test_b07_dxf_clip.py` | +| Ctrl+C · Ctrl+X | ○ ⚠ | 연결 없음 — 글자 `C` · `X` 가 입력 칸에 쌓임(Enter 면 CIRCLE · EXPLODE · 선 중이면 닫기) · 복사 = COPYCLIP 명령 · 리본만 | `input-controller.ts:423-428,476-482` · `A00/key_shortcut.ts:37-48` | 없음 | +| Ctrl+Shift+C · Ctrl+Shift+V · Ctrl+1 | ○ | 명령(COPYBASE · PASTEBLOCK · PROPERTIES)은 있고 키 없음 · 클립보드 = 앱 안 메모리(창끼리 · 새로고침 안 넘음) | `src/commands/commands.utility.ts:225-270` · `src/helpers/cad-clipboard.ts:1-34` | 없음 | +| Delete · Backspace | ● | Delete = 선택 도구에서 고른 것 지움 · Backspace = 친 글 한 자 | `input-controller.ts:441-450` · `src/tools/select-tool.ts:73-76,167-171` | 없음 | +| 속성 칸(특성 팔레트) | ◐ | 색 · 선가중치 · 선종류 · 층 · 투명도만 고침 · 길이 · 크기 · 시작점 = 읽기만 · 좌표 · 반지름 · 글 높이 · 글 내용 칸 없음 · 여러 개면 첫 객체 값 | `src/components/PropertiesEditor.tsx:12-147` · `src/components/QuickProperties.tsx:7-21` | 없음 | +| 리본 특성 · 도면층 관리 | ● | 고른 것에 바로 색 · 선 · 층 옮김 · 층째 지움 | `src/components/RibbonWidgets.tsx:39-122` · `src/components/LayerManager.tsx:41-60` | 없음 | +| 특성 맞추기 | ● | MATCHPROP(MA) 명령 | `src/commands/commands.modify.ts:262-270` | 없음 | +| 파일 끌어 놓기 | ● | DWG · DXF → 서버 변환 → 원좌표 붙임 · 고른 상태 | `dxf-clip-paste.ts:88-105` | `py/test_b07_dxf_clip.py` | +| 손가락 | ● | 톡 = 누름(스냅) · 한 손가락 끌기 = 팬 · 두 손가락 = 벌려 확대 + 같이 팬 · 길게(0.45초) = 손가락 위 70px 십자선으로 맞춰 떼면 누름 · 고름 반경 = 접촉 크기(최소 24px) · 오른쪽 단추 6(Esc · Enter · 지움 · 되돌리기 · 다시 · 스냅 F3) — 손가락 기기에서만 | `input-touch.ts:7-144` · `src/inputController/input-touch.css:1-29` | `py/test_cad_touch.py` | +| 펜 | ◐ | 마우스와 같음(손가락 처리에서 빠짐) · 필압 · 옆 단추 전용 처리 없음 | `input-touch.ts:56-60` | 없음 | +| 키 초점 | ● | 포인터가 CAD 위로 오면 iframe 초점(부모 입력 칸에서 치는 중이면 안 뺏음) · CAD 안 입력 칸에서는 CAD 키 안 먹음 | `ui_template/cad_host/cad_host.ts:152-156` · `input-controller.ts:370-373` | `py/test_221_1_loupe_cad.py` | + +## 7. 명령 목록 — 153 · 별칭 88(별칭 있는 명령 82) + +| 갈래 | 수 | 명령(별칭) | 근거 | +|---|---|---|---| +| 그리기 | 21 | LINE(L) · PLINE(PL) · CIRCLE(C) · ARC(A) · RECTANG(REC) · POLYGON(POL) · ELLIPSE(EL) · SPLINE(SPL) · MLINE(ML) · MLSTYLE · XLINE(XL) · RAY · POINT(PO) · DONUT(DO) · DIVIDE(DIV) · MEASURE(ME) · HATCH(H · BH) · GRADIENT(GD) · BOUNDARY(BO) · REGION(REG) · WIPEOUT | `src/commands/commands.draw.ts:31-218` | +| 고치기 | 28 | MOVE(M) · COPY(CO · CP) · ROTATE(RO) · SCALE(SC) · ALIGN(AL) · MIRROR(MI) · OFFSET(O) · ARRAY(AR) · TRIM(TR) · EXTEND(EX) · STRETCH(S) · FILLET(F) · CHAMFER(CHA) · BLEND · BREAK(BR) · BREAKATPOINT · JOIN(J) · EXPLODE(X) · ERASE(E) · LENGTHEN(LEN) · PEDIT(PE) · HATCHEDIT(HE) · DRAWORDER(DR) · DRAWORDERBACK · MATCHPROP(MA) · OVERKILL · REVERSE · SETBYLAYER | `src/commands/commands.modify.ts:47-295` | +| 맞춤 | 6 | ALIGNLEFT · ALIGNCENTERH · ALIGNRIGHT · ALIGNTOP · ALIGNCENTERV · ALIGNBOTTOM | `commands.modify.ts:297-340` | +| 글자 | 5 | MTEXT(T · MT) · TEXT(DT) · STYLE(ST · 안내만) · TEXTEDIT(TEDIT) · FIND | `src/commands/commands.annotate.ts:50-98` | +| 치수 | 16 | DIM · DIMLINEAR(DLI) · DIMALIGNED(DAL) · DIMANGULAR(DAN) · DIMARC(DAR) · DIMRADIUS(DRA) · DIMDIAMETER(DDI) · DIMORDINATE(DOR) · DIMBASELINE(DBA) · DIMCONTINUE(DCO) · QDIM · CENTERMARK · CENTERLINE · DIMSTYLE(D) · DIMUPDATE · DIMSPACE | `commands.annotate.ts:100-238` | +| 지시선 · 표 · 표식 | 13 | MLEADER(MLD) · MLEADERSTYLE(MLS) · MLEADERALIGN · TABLE(TB) · TABLESTYLE(TS) · TABLEEDIT · TABLEROW · TABLECOL · TABLEMERGE · TABLEUNMERGE · PLACETABLE · REVCLOUD · ANNOSCALE | `commands.annotate.ts:240-355` | +| 도면층 | 18 | LAYER(LA) · LAYCURSET · LAYOFF · LAYON · LAYFRZ · LAYTHW · LAYLCK · LAYULK · LAYISO · LAYUNISO · LAYERP · LAYERSTATE(LAS) · LAYERSTATERESTORE · LAYMCH · LAYCUR · LAYMRG · LAYDEL · LAYWALK | `src/commands/commands.layer.ts:25-166` | +| 특성 · 그룹 | 8 | COLOR(COL) · LINETYPE(LT) · LWEIGHT(LW) · PROPERTIES(CH · MO · PR) · QUICKPROPERTIES(QP) · TRANSPARENCY · GROUP(G) · UNGROUP | `src/commands/commands.utility.ts:40-112` | +| 측정 · 조회 · 고르기 | 13 | DIST(DI) · RADIUS · ANGLE · AREA(AA) · MEASUREGEOM(MEA) · ID · LIST(LI) · QSELECT · QUICKCALC(QC) · SELECTSIMILAR · ISOLATEOBJECTS · HIDEOBJECTS · UNISOLATEOBJECTS(UNHIDE · UNISOLATE) | `commands.utility.ts:114-223` | +| 클립보드 | 6 | COPYCLIP · CUTCLIP · PASTECLIP · COPYBASE · PASTEORIG · PASTEBLOCK | `commands.utility.ts:225-270` | +| 블록 · 삽입 | 3 | IMAGEATTACH(IAT) · BLOCK(B · 이름 = 브라우저 prompt) · INSERT(I · 블록 창 열기) | `src/commands/commands.insert.ts:13-61` | +| 보기 | 6 | SELECT(SE) · ZOOM(Z · 범위만) · ZOOMIN · ZOOMOUT · REGEN(RE) · PAN(P · 안내 글만) | `src/commands/commands.view.ts:31-87` | +| 파일 · 되돌리기 | 10 | QSAVE(SAVE) · RECOVER · EXPORT(EXP) · EXPORTDXF · EXPORTDWG · EXPORTSVG · EXPORTPNG · TOAREA · UNDO(U) · REDO | `src/commands/commands.file.ts:13-118` | + +- 없는 제도 설정 명령 — OSNAP · DSETTINGS · UNITS · SNAP · GRID · ORTHO 이름 없음 · `SE` = 우리 SELECT(오토캐드 acad.pgp 는 DSETTINGS 로 앎 · 대조 필요). + +## 8. 묶음 · 자동화 + +| 항목 | 상태 | 어떻게 | 근거 | +|---|---|---|---| +| 스크립트(.scr) · 매크로 · 동작 기록 | ○ | 없음 | (src 검색 0건) | +| 명령 기록 | ◐ | 실행 명령 200줄 메모리 · 명령행에 최근 3줄 · 저장 · 내보내기 · 다시 돌리기 없음 | `run-command.ts:10-24` · `CommandLine.tsx:43-48` | +| 마지막 명령 반복 | ● | 3장 참고 | `run-command.ts:66-72` | +| 부모 화면 → CAD 메시지 | ● (사람 입력 아님) | 도면 싣기 · 저장 요청 · 표 놓기 · 표 칸 갈기 · 작도 영역 · 가이드 보이기 | `src/integration/aislo-drawing-bridge.ts:36-49,145-217` · `ui_template/cad_host/cad_host.ts:17-39` | +| 시험용 재생 | ◐ (시험만) | 크롬 기록기 JSON 재생 도우미 — 앱 기능 아님 | `B07_DesignDetail/openwebcad/test/helpers/replay-recording.ts` | +| 상태 읽기 창구 | ◐ (검증용) | `window.__aisloCad` 읽기만 | `src/helpers/debug-hook.ts:1-63` | +| 설정 저장 | ◐ | 휠 방향 · 내 별칭만 계정 pref · 스냅 · 직교 · 극 · 그리드 · 명령행 켬은 메모리(새로 열면 기본) | `A00/input_prefs.ts:13-51` · `src/state.ts:82,179-182` · `Toolbar.tsx:23` | + +## 9. 세 곳 차이 · 보기 전용 + +### 9-1. 띄우는 곳별 + +| 자리 | 띄우는 곳 | 겉칸 | 편집 | 입력 차이 | +|---|---|---|---|---| +| B07 상세 설계 | `B07_DesignDetail/B07_DesignDetail_UI_Page.ts:117,357` | 전부(제목 · 리본 · 좌측 패널 · 상태막대 · 명령행 켬 단추 · 우하단 줌) | 됨 · 확정 도면 = readOnly | 표준 · Ctrl+S 없음 · 도각 편집 때 자리표 단추 · 가이드 끌기 | +| B07 평면도 관리 미리보기 | `B07_DesignDetail/B07_DesignDetail_UI_PlanManage.ts:67,184-190` | 캔버스만(bare) | 안 됨(readOnly + viewOnly) | 왼쪽 끌기 = 팬 · 두 번 = 맞춤 · 휠 · 고르기 · 스냅 · 커서 상자 없음 | +| B06 횡단 [상세] 아래 「표 위 CAD」 | `B06_Section/B06_Section_UI_Cross_Detail_Cad.ts:317-346` | 캔버스만(bare) — 리본 · 상태막대 · 명령행 · 좌측 패널 · 우하단 줌 없음 | 됨 · 고칠 때마다 세션 초안에 씀 | 명령 = 커서 곁 글 치기 · 손가락 단추만 · 직교 · 극 · 스냅 · 그리드 = F키만 · 스냅 추적 켜고 끄기 · ↑ 기록 못 씀 · 되돌리기 = Ctrl+Z 만 | +| M02 도면 양식 편집 | `M02_MasterTemplete/M02_MasterTemplete_Drawing.ts:243,270-291` | 리본 접힘(탭 줄만) · 우하단 ↶ ↷ | 권한 따라 readOnly · 미리보기면 viewOnly | Ctrl+S · 💾 = 부모 [저장] · 작도 영역 가이드 끌기 · 원점 십자 · 자리표 단추 | +| M02 구조물 카드 | `M02_MasterTemplete/M02_MasterTemplete_DrawingCard.ts:42-45,68-95` | 보기 = 제목 · 리본 숨김 · 고치기 = 제목 · 리본 · 좌측 패널 · 상태막대 · 빠른 막대 숨김 | 보기 = viewOnly · 고치기 = 됨 | 고치기 = 상태막대 없어 명령행 · 스냅 추적 · 그리드 단추 못 씀(F키 · 글 치기만) | +| M02 면적산출 도면 패널 | `M02_MasterTemplete/M02_MasterTemplete_AreaDrawing.ts:31-47,155-167` | 카드 고치기와 같은 숨김 | readOnly(고르기는 됨 — TOAREA 용) | 누름 · 끌기로 고르기 → 보내기 | + +- 공통 — 같은 앱 한 벌(`/b07-cad`) · 갈래는 싣기 곁값(`readOnly` · `viewOnly` · `bare` · `hostSave` · `ribbonCollapsed` · `panelsCollapsed` · `originMarker` · `guide`)뿐 — `ui_template/cad_host/cad_host.ts:83-104` · `src/integration/aislo-drawing-bridge.ts:183-197`. +- 실을 때마다 SELECT 로 돌림(앞 도면 도구 내림) — `aislo-drawing-bridge.ts:197`. +- 부모 돋보기(Z 키)는 CAD 위에서 안 켬 — `py/test_221_1_loupe_cad.py`. + +### 9-2. 보기 전용에서 막히는 입력 + +| 입력 | readOnly(확정 · 권한 없음) | viewOnly(미리보기) | 근거 | +|---|---|---|---| +| 명령(리본 · 글 · 별칭) | 보는 명령만(보기 · 조회 · 도면층 보이기 · 내보내기 · COPYCLIP · COPYBASE · TOAREA) · 나머지 알림 | 같음 | `src/commands/registry.ts:60-98` · `run-command.ts:30-34` | +| 고르기(누름 · 상자) | 됨 | 안 됨(왼쪽 = 팬) | `input-controller.ts:191-194,319-324` | +| 스냅 · 스침 강조 · 커서 상자 | 됨 | 안 됨 | `input-controller.ts:275` · `draw.ts:75-78` · `main.tsx:89` | +| 휠 · 가운데 팬 · 화살표 | 됨 | 됨 | — | +| 글자 두 번 누름 | 막힘(알림) | 전체 맞춤 | `text-inplace-edit.ts:210-218` | +| 붙여넣기 · 파일 놓기 | 막힘 | 막힘 | `dxf-clip-paste.ts:70-74` · `run-command.ts:30-34` | +| 칸 모서리 끌기 | 막힘 | 막힘(못 고름) | `box-resize-drag.ts:67` | +| 우하단 ↶ ↷ 단추 | 숨김 | 숨김 | `ViewControls.tsx:14` | +| ⚠ 그립 끌기 | **안 막힘**(명령 길을 안 거침) | 못 고름 | `select-tool.helpers.ts:55-72` · `grip-edit-tool.ts:26-30` | +| ⚠ Delete · 손가락 🗑 | **안 막힘** | **Ctrl+A → Delete 로 안 막힘** | `input-controller.ts:438-450` · `select-tool.ts:73-76,167-171` | +| ⚠ 특성 칸 · 리본 특성 · 도면층 관리(옮김 · 층 지움) | **안 막힘** | 고른 것 없음 · 층 지움은 패널 보이면 됨(추정) | `PropertiesEditor.tsx:12-17` · `RibbonWidgets.tsx:39-47` · `LayerManager.tsx:41-60` | +| Ctrl+Z · Ctrl+Y | 안 막힘(위 새는 고침만 되돌림) | 같음 | `input-controller.ts:429-434` | + +- ⚠ 줄 = 코드 읽기 · 실측 필요 · 저장까지 가는지는 부모 [저장] 길 미확인 · 읽기 전용 판정 시험 = `src/state.test.ts:113-123`. + +## 10. 시험 파일 모음 + +| 시험 | 보는 것 | +|---|---| +| `cad/keyboard-chain.test.ts` | 331-1~4(각도 고정 · 시퀀스 첫 점 · MOVE · COPY · ROTATE 값 · LINE C · U · 한 번 스냅 · 동적 입력 Tab) | +| `src/inputController/ime-sink.test.ts` | 한글 IME · Space = Enter · 빈 Enter 반복 · 우클릭 반복 안 함 | +| `src/inputController/input-wheel.test.ts` · `cad/input-prefs-cad.test.ts` · `py/test_input_prefs.py` | 휠 줌 · 방향 설정 · 핀치 · 내 별칭 | +| `cad/command-catalog.test.ts` · `py/test_app_settings.py` | 명령 표 거울 · 별칭 검사 · 설정 창 | +| `cad/ctrl-shortcut-hangul.test.ts` | Ctrl 단축키 자판 자리 | +| `cad/grips.test.ts` | 그립 도우미(누르기 흐름 아님) | +| `cad/text-inplace-edit.test.ts` | 글자 두 번 누름 입력 칸 | +| `cad/dxf-clip.test.ts` · `py/test_b07_dxf_clip.py` | DXF 붙여넣기 · 끌어 놓기 | +| `py/test_cad_touch.py` | 손가락 단추 · 핀치 · 끌기 | +| `py/test_221_1_loupe_cad.py` · `py/test_cad_host_tabs.py` | 부모 쪽 돋보기 · 탭 | +| (없음) | 고르기 · F키 토글 · 그립 누르기 흐름 · 명령행 ↑ 기록 · 읽기 전용 새는 길 | From 5bf5e0c46eebea62b55483726b8b211ebbc313aa Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 15 Oct 2026 16:24:23 +0900 Subject: [PATCH 43/81] =?UTF-8?q?chore(=EC=A1=B0=EC=82=AC):=20336=20?= =?UTF-8?q?=EC=99=B8=EB=B6=80=20=EC=9D=BC=EC=9C=84=EB=8C=80=EA=B0=80=20?= =?UTF-8?q?=EC=9D=B4=EB=A6=84=20=EB=A7=9E=EC=B6=A4=20=EA=B0=9C=EC=84=A0=20?= =?UTF-8?q?=EC=83=81=EC=84=B8=20=EA=B2=80=ED=86=A0=20=E2=80=94=20=EC=8B=A4?= =?UTF-8?q?=EC=B8=A1=20=EC=84=B8=20=ED=8C=8C=EC=9D=BC=20=C2=B7=20=EB=AC=B6?= =?UTF-8?q?=EC=9D=8C=20=EB=84=B7=20=C2=B7=20=EC=A0=95=ED=95=A0=20=EA=B2=83?= =?UTF-8?q?=20=C2=B7=20=EB=B6=80=EB=A1=9D=20=EB=91=98?= 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_01C6BNBFXFndbqYxu6sZuknK --- docs/raw/조사/2026-10-15_336_부록1_실측.md | 299 ++++++++ .../raw/조사/2026-10-15_336_부록2_바깥방법.md | 696 ++++++++++++++++++ ...26-10-15_336_외부일위대가_이름맞춤_검토.md | 323 ++++++++ 3 files changed, 1318 insertions(+) create mode 100644 docs/raw/조사/2026-10-15_336_부록1_실측.md create mode 100644 docs/raw/조사/2026-10-15_336_부록2_바깥방법.md create mode 100644 docs/raw/조사/2026-10-15_336_외부일위대가_이름맞춤_검토.md diff --git a/docs/raw/조사/2026-10-15_336_부록1_실측.md b/docs/raw/조사/2026-10-15_336_부록1_실측.md new file mode 100644 index 000000000..fb17791b7 --- /dev/null +++ b/docs/raw/조사/2026-10-15_336_부록1_실측.md @@ -0,0 +1,299 @@ +# C1 외부 일위대가 이름 맞춤 실측 (요구 225 · 검토) + +M02 「외부 일위대가 가져오기」 맞춤을 실제 엑셀로 재고 실패 까닭 · 개선 방법 효과를 가늠한 기록. 코드 안 고침(읽기 · 스크래치패드 흉내만). +기준 = main_laptop_1 31e6adf7d · 우리 돈 로직 1,438(산림 300 · 건설 1,138) · 기억 없음(층 기억 빈 것)으로 잼. + +## 1. 결론 요약 + +1. 잰 자료 = 북부청 임도 · 사방(지금 올라가는 꼴) + 동부청 영월 2024(STmate 꼴 · 지금은 「일위대가 시트 없음」 400 → 스크래치패드에서 따로 읽어 같은 맞춤 함수에 넣음). 로직 줄 고유 136(줄 504) 전수 정답 표시 · 정답 있음 126 · 우리에 없음 10(7.4%). +2. 이름만(330 앞) 1순위 엄격 34.1% · 느슨 60.3% · 다섯 안 57.1 / 87.3% — 이름 문제 실재(같은 뜻 다른 말 · 산림/건설 책 갈래 · 레미콘 같은 줄임말이 주 까닭). +3. 지금(330 근거 맞춤 뒤) 1순위 71.4 / 81.7% · 다섯 안 86.5 / 94.4% — 엑셀 단가산출 조항 · 직종이 이름 문제 대부분을 덮음(+37pt). 줄 무게로는 1순위 느슨 78.1% · 다섯 안 느슨 97.9%. +4. 지금 남은 1순위 실패 36 줄(고유)의 주 까닭 = 책 갈래 11(「같음」 자동 대상이 엑셀 출처와 다른 책 10 · 이름 같음 덤이 근거를 이김 1) · 같은 뜻 다른 말 11(고임돌 · 조약돌 · 소운반 · 철골설치 · 측구수로관) · 우리 로직이 더 잘게 5(철근 = 가공 + 조립) · 그 밖 5(같은 이름 접힘 · 이름 같음 덤) · 장비 줄 2 · 복합 1 · 괄호 1. +5. 이름 쪽 손질(다듬기 · n-그램 · 낱말 차례 · 단위 거름망)은 효과 없거나 해로움. 사전(임도에서 16쌍)은 사방 +7.0 · 영월 −8.9(느슨 1위) → 한 파일로 만든 사전은 다른 꼴에 안 통함. +6. 효과 큰 것 = 근거 쪽 작은 고침 — 조항 윗마디(6-2 ⊃ 6-2-2) · 같은 이름 접힘 고침 · 조항 오타 → 1순위 77.8 / 88.1% · 여기에 「같음도 근거 검사(판 지킴)」 더하면 85.7 / 88.1% · 다섯 안 94.4 / 94.4%. +7. 금액(우리 값 ÷ 엑셀 단가)은 순위 점수로 넣으면 엄격 −4.7pt(복합 줄 · 입력 기본값) · 확인 표시로는 ±5% 안 1순위 21 중 20 맞음(95%). +8. 기억 — 지금 열쇠(품명|규격|단위)로는 같은 회사 다른 파일 재사용 고유 4/48 · 둘째 열쇠(품명|단위)면 15/48(모두 맞음) · 씨앗(사용자 실무 이름 80)이 영월 1순위 엄격 44.1 → 67.6. +9. 단추 정밀 — [근거 맞음 모두 확정] 고른 55 줄(고유) 모두 맞음(엄격 54) · [같음 모두 확정] 25 중 엄격 20(다른 책 5 · 느슨은 25). +10. 「다른 외부문서도 같음」의 실제 걸림돌 = 저장소 실무 내역 xlsx 6 개를 열어 보니 5 개가 STmate 꼴(일위대가표 · 단가산출근거 · D/M/L/X 코드) → 지금은 올리기부터 400 · 이름 맞춤 앞 단계 문제. + +## 2. 지표 표 + +고유 줄 = 같은 (품명 · 규격 · 단위 · 단산 번호) 한 번 셈(확정 한 번이면 같은 열쇠 줄 모두 기억으로 들어감). 칸 = 엄격 / 느슨(%). 엄격 = 엑셀 조항 · 품과 같은 우리 로직 · 느슨 = 같은 일의 다른 책 로직 · 복합의 곁 몫까지. 정답 있는 줄만 분모 · 「우리에 없음」은 따로. +\* 영월 = STmate 꼴을 스크래치패드에서 읽은 가정(지금 화면은 400). 「지금」 · 「추천 결합」은 기억 없음 · 씨앗 켬. + +| 파일 | 고유 줄(줄) | 정답 있음 | 우리에 없음(줄) | 이름만 1위 | 이름만 5안 | 지금 1위 | 지금 5안 | 추천 결합 1위 | 추천 결합 5안 | +|---|---|---|---|---|---|---|---|---|---| +| 북부청 임도 | 52(271) | 49 | 3(6) · 5.8% | 26.5 / 57.1 | 55.1 / 89.8 | 71.4 / 85.7 | 83.7 / 98.0 | 91.8 / 91.8 | 98.0 / 98.0 | +| 북부청 사방 | 48(142) | 43 | 5(22) · 10.4% | 34.9 / 62.8 | 55.8 / 88.4 | 74.4 / 83.7 | 88.4 / 95.3 | 86.0 / 88.4 | 95.3 / 95.3 | +| 동부청 영월* | 36(91) | 34 | 2(5) · 5.6% | 44.1 / 61.8 | 61.8 / 82.4 | 67.6 / 73.5 | 88.2 / 88.2 | 76.5 / 82.4 | 88.2 / 88.2 | +| 세 파일 | 136(504) | 126 | 10(33) · 7.4% | 34.1 / 60.3 | 57.1 / 87.3 | 71.4 / 81.7 | 86.5 / 94.4 | 85.7 / 88.1 | 94.4 / 94.4 | + +- 추천 결합 = ⑥ 조항 윗마디 + ⑬ 같은 이름 접힘 고침 + 조항 오타(13-114 → 13-11-4) + ⑪ 같음도 근거 검사(판 지킴). +- 우리에 없음 10 줄 중 지금 엉뚱 후보가 보이는 줄 5(고화매트 → 식생매트 · 잔토정리 → 타설후 정리 · 철강재운반 → 덤프 자재 운반 · 철골공 → 철골 공장용접 · 영월 잔토처리 → 터널굴착 1발파당 인력). +- 줄 무게(같은 줄 되풀이 셈) — 이름만 1위 느슨 56.1 · 지금 78.1 · 추천 결합 85.6 · 다섯 안 느슨 90.0 · 97.9 · 97.9. +- 앞서 잰 「1순위 근거 맞음 123/201」은 근거 꼬리표 기준(정답 대조 아님) — 이번 고유 줄 지금 [근거 맞음] 고름 임도 26/41 · 사방 22/40 과 같은 결. +- 자재 · 별도계상 줄(북부청 임도 142 · 사방 56)은 정답 표시 밖 — 상태만: 임도 자재 같음 12 · 후보 50 · 없음 16 / 별도계상 후보 53 · 없음 11. + +## 3. 실패 까닭 갈래 표 + +1순위 엄격 실패 줄(고유)마다 주 갈래 하나 + 곁 갈래(겹쳐 셈). 칸 = 주 갈래 고유(줄) · 곁 갈래 수. 이름만 = 330 앞 · 씨앗 끔(실패 83 · 줄 345) / 지금 = 330 뒤(실패 36 · 줄 162). + +| 갈래 | 이름만 실패 | 지금 실패 | 예 | +|---|---|---|---| +| 같은 뜻 다른 말 | 25(88) · 곁 9 | 11(46) · 곁 0 | 휠라설치 → 신축이음 채움재 설치 · 구조물터파기(인력100%) → 굴착(인력/토사) · 고임돌 · 조약돌 채집 → 모래·자갈·약돌 채집(1위 궤도 자갈채집) | +| 줄임말 | 9(51) · 곁 2 | 0(0) · 곁 0 | 레미콘타설(장비사용) → 레디믹스트콘크리트 타설(1위 펌프카 타설) · 배수관부설및접합(P.V.C) → PVC관 고무링 접합 및 부설 · B/H 90%(백호 = 굴착기) | +| 낱말 차례 | 0(0) · 곁 10 | 0(0) · 곁 0 | 철근가공조립(현장) ↔ 철근 현장가공 및 조립 · 큰돌찰붙임 ↔ 큰돌붙이기 찰붙이기 · (곁 갈래만 · 주 까닭 아님) | +| 괄호 속 규격 | 4(23) · 곁 3 | 1(18) · 곁 0 | 큰돌쌓기(찰쌓기) → 1위 메쌓기(괄호 「찰쌓기」 버림) · 구조물터파기(토사) → 1위 터파기(기계) 암 · 구조물터파기(암) → 1위 육상토사 | +| 비율 표기(인력100%·9:1) | 0(0) · 곁 1 | 0(0) · 곁 0 | 구조물터파기(인력100%) · 고임돌채집및운반(9:1) · (곁 갈래만 — 비율 글은 지금도 낱말로 안 셈) | +| 단위 표기(hr·공/㎥·개소) | 0(0) · 곁 7 | 0(0) · 곁 5 | 대형브레이카 hr ↔ 시간 · 강관동바리 공/㎥ ↔ 공㎥ · 도로반사경 개소 ↔ 본(환산 「입력」) | +| 장비 · 노임 이름 줄 | 2(2) · 곁 0 | 2(2) · 곁 0 | 콘크리트 펌프차 41m [hr] → 기계 시간당 기계경비(기계 입력) · 크레인(타이어) 10톤 [시간] · 철골공 · 특별인부 [인] = 노임(우리에 없음) | +| 책 갈래(산림↔건설) | 29(122) · 곁 3 | 11(53) · 곁 0 | 합판거푸집 → 같음 산림 12-4(엑셀은 건설 6-3-1) · 강관비계 · 강관동바리 → 같음 산림(엑셀 건설 2-7-1 · 2-6-1) · 큰돌메붙임 → 1위 건설 메붙임(굴착기+집게)(엑셀 산림 13-7-1) | +| 우리 로직이 더 잘게/굵게 | 6(33) · 곁 0 | 5(27) · 곁 1 | 철근가공조립(현장) = 우리 현장가공 + 현장조립 둘 · 잔토처리(인력) = 우리 굴착(인력) 입력 「잔토처리」에 묶임 · 원형돌망태 조립설치 · 채움 = 우리 원형 돌망태 하나 | +| 엑셀 복합 호표 | 1(1) · 곁 21 | 1(1) · 곁 7 | 레미콘타설 + 굴착기 시간 · 고임돌 = 채집 + 상차 + 운반 · 도장(철재면2회) = 바탕 + 녹막이 + 유성 셋 | +| 근거 조항 비어 있음 · 엉뚱 | 0(0) · 곁 6 | 0(0) · 곁 6 | 철골설치(출처 없음) · 조약돌채집운반(9:1)(출처 없음) · 고임돌 「1-10」 = 단위중량 인용(작업 조항 아님) | +| 품셈 판 차이(번호 바뀜) | 1(6) · 곁 3 | 0(0) · 곁 9 | 철근 「건설 6-2」 = 옛 한 조항 → 지금 6-2-2 · 6-2-3 · 야면석채집 「건설 8-2 (2016 삭제)」 · 영월 「건설 3-1-3 · 9-3 · 1-9-3 · 임도품셈 5-5」 옛 번호 | +| 그 밖(오타 · 비슷한 이름 함정 · 접힘) | 6(19) · 곁 14 | 5(15) · 곁 3 | 흙깍기(오타 깍/깎) → 후보 없음 · 원형돌망태 → 1위 타원형(원형 ⊂ 타원형) · 유로폼 설치,해체 → 같은 이름 산림 · 건설 둘이 이름 후보에서 하나로 접힘 | + +- 이름만 실패의 65%(54/83)가 같은 뜻 다른 말 · 책 갈래 — 330 근거가 이 둘을 대부분 풂(책 갈래 29 → 11 · 같은 뜻 25 → 11). +- 지금 책 갈래 11 = 「같음」(이름 같음 · 씨앗) 자동 대상이 엑셀 출처와 다른 책 10(산림 로직 ↔ 엑셀 건설 조항 9 · 반대 1 = 돌망태형 옹벽) + 이름 같음 덤(+1)이 근거 점수를 이김 1(영월 합판거푸집(4회)) — 같음에도 근거 검사를 하면 10 이 풀림(⑪). +- 지금 같은 뜻 다른 말 11 = 근거로도 못 품 — 엑셀 출처가 비었거나(사방 철골설치 · 조약돌 9:1) · 단위중량 인용(고임돌 「1-10」) · 옛 판 번호(영월 9-1 · 9-3) · 우리에 그 책 조항 없음(건설 1-2-7 인력운반). + +## 4. 방법별 효과 표 + +세 파일 정답 있는 126 줄(고유) · 칸 = 1위 엄격 / 느슨 · 5안 엄격 / 느슨(%). 「이름만에」 = 330 앞 맞춤에 얹음 · 「지금에」 = 330 맞춤에 얹음. 흉내는 스크래치패드에서 함수를 바꿔 끼워 같은 줄을 다시 돌림. + +| 방법 | 1위 | 5안 | 들 일 | 위험 · 비고 | +|---|---|---|---|---| +| 기준 ① 이름만(330 앞 · 씨앗 끔) | 34.1 / 60.3 | 57.1 / 87.3 | — | — | +| 기준 ② 지금(330 뒤) | 71.4 / 81.7 | 86.5 / 94.4 | — | — | +| ① 이름 다듬기(괄호 낱말 살림 · 수 · 단위 · 규격 조각 뺌) — 이름만에 | 30.2 / 53.2 | 59.5 / 88.1 | 작음 | 괄호 속 규격이 엉뚱 후보를 끌어옴 · 느슨 1위 −7.1 | +| ① 이름 다듬기 — 지금에 | 70.6 / 81.7 | 86.5 / 95.2 | 작음 | 영월 −2.9 · 사방 터파기(토사)만 나아짐 | +| ② 같은 뜻 말 사전(임도 파일에서 16쌍) — 이름만에 | 34.1 / 65.1 | 59.5 / 87.3 | 작음(사전) · 키우기 큼 | 사방(처음 봄) 느슨 1위 62.8→69.8 · 영월(다른 꼴) 61.8→52.9 | +| ② 같은 뜻 말 사전 — 지금에 | 64.3 / 81.0 | 86.5 / 94.4 | 작음 | 레미콘 → 산림 GF000155 로 넘어감(이름 같음 덤 + 산림 덤) · 엄격 −7.1 | +| ③ 낱말 차례 무시(뜻 낱말 겹침 덤) — 이름만에 | 37.3 / 59.5 | 65.9 / 81.7 | 작음 | 다섯 안 느슨 −5.6 · 한국어 붙여 쓴 이름이라 효과 적음 | +| ④ 글자 1·2·3그램 — 이름만에 | 34.1 / 59.5 | 57.1 / 87.3 | 작음 | 변화 거의 없음 | +| ④ difflib(가장 긴 같은 조각) — 이름만에 | 35.7 / 54.0 | 73.0 / 86.5 | 작음 | 다섯 안 엄격 ↑ · 느슨 ↓ · 없음 줄 엉뚱 후보 4→10 | +| ⑤ 단위 거름망(단위 안 맞으면 뺌) — 지금에 | 63.5 / 73.8 | 78.6 / 86.5 | 작음 | 해로움 — 맞는 짝 단위가 달라(개소↔M · 개↔㎡ · 조↔㎡ · hr↔시간) 1위 엄격 −7.9 | +| ⑥ 근거 조항 윗마디(엑셀 6-2 ⊃ 우리 6-2-2) — 지금에 | 75.4 / 85.7 | 86.5 / 94.4 | 작음(_clause 몇 줄) | 낮음 — 윗마디 = 0.9 · 직종 · 낱말 거름 그대로 | +| ⑦ 직종 · 장비 구성 겹침(조항 없이도) — 지금에 | 71.4 / 82.5 | 85.7 / 94.4 | 중간 | 효과 작음 — 직종은 이미 근거 점수에 듦 | +| ⑧ 금액(우리 값 ÷ 엑셀 단가 ±15% 덤) — 지금에 | 66.7 / 77.8 | 86.5 / 94.4 | 중간 | 해로움 — 복합 줄 · 입력 기본값 탓 · 확인 표시로는 ±5% 안 1순위 21 중 20 맞음 | +| ⑪ 같음도 근거 검사(판 지킴 없이) — 지금에 | 77.0 / 80.2 | 93.7 / 94.4 | 중간 | 영월 −5.8 — 옛 판 1-9-3 이 지금 낙석방지책으로 걸림 | +| ⑪ 같음도 근거 검사(판 지킴: 옛 판 글이면 안 내림 · 직종/뜻 낱말 요구) | 78.6 / 81.7 | 93.7 / 94.4 | 중간 | 낮음 — 같음 → 후보로 바뀌어 사람 확정 한 번 더 | +| ⑫ 블록 글 낱말(우리 이름 낱말이 엑셀 산출 글에 나오면 덤) | 69.8 / 84.1 | 86.5 / 95.2 | 중간 | 섞임 — 느슨 ↑ 엄격 ↓(블록에 단위중량 · 자재 이름이 섞임) | +| ⑬ 같은 이름 접힘 고침(산림·건설 같은 이름을 하나로 접던 것) | 73.8 / 84.1 | 86.5 / 94.4 | 작음(중복 빼기 열쇠) | 다섯 안에 같은 이름 둘 → 화면에 책 표시 필요 | +| ⑭ 부문(공통 · 토목 글로 부문 가름) | 75.4 / 85.7 | 86.5 / 94.4 | 작음 | 이 표본에선 변화 0 | +| 책 맞춤 덤(엑셀 출처 책 = 우리 책 +0.2) | 73.8 / 82.5 | 86.5 / 94.4 | 작음 | 섞임 — 영월 +11.8 · 임도 −2.0(건설 삭제 조항 · 산림에만 있는 일) | +| 결합 ⑥ + ⑬ + 조항 오타 | 77.8 / 88.1 | 86.5 / 94.4 | 작음 | 낮음 | +| 결합 ⑥ + ⑬ + 조항 오타 + ⑪(판 지킴) ← 추천 | 85.7 / 88.1 | 94.4 / 94.4 | 작음~중간 | 낮음 — 같음 11 줄이 후보로 바뀜(사람 확정 한 번 더) | +| 결합 + ① 다듬기 | 85.7 / 88.9 | 94.4 / 95.2 | 중간 | 영월 기초잡석 · 큰돌 채집 둘 뒤집힘 | +| ⑨ 맞춤 기억 재사용(임도 정답 → 사방) | 지금 열쇠 4/48 · 품명\|단위 15/48(맞음 15) · 정리이름\|단위 24/48(맞음 21) | 사방 → 임도 4/52 · 18/52(18) · 23/52(22) | 중간 | 정리이름 열쇠는 괄호 낱말을 버려 엉뚱 3(구조물터파기(토사) ↔ (인력100%)) → 품명\|단위가 알맞음 | +| ⑨' 씨앗(사용자 실무 이름 80 = 기억의 한 꼴) | 영월 1위 엄격 44.1 → 67.6 · 느슨 55.9 → 73.5 | 영월 79.4 → 88.2 | — | 임도 · 사방은 씨앗 「면고르기 · 문양거푸집 → 산림」 탓에 엄격 −4.1 · −2.3 | +| 단위 별칭(hr=시간 · 공/㎥=공㎥ · 개소=개=본) | 순위 변화 거의 없음 | — | 작음 | 정답 짝 환산 「입력」 20 → 14 줄 | +| 장비 줄 길(갈래 기계 · 단위 시간 → 「기계 시간당 기계경비」 + 기계 입력) | 흉내 안 함 · 남은 실패 2 줄(펌프차 · 크레인) 풂 예상 | — | 작음 | 낮음 | +| 임베딩(한국어 문장 벡터) 재순위 | 기대 — 같은 뜻 다른 말 일부(터파기/굴착 · 레미콘/레디믹스트) | — | 중간~큼(새 패키지 · 모델 · 미리 셈) | 휠라 · 고임돌 · 야면석 같은 현장말은 모를 가능성 · 산림/건설 같은 이름은 벡터도 같아 책 가름 못 함 → 근거와 함께만 | +| 큰 언어 모델 재순위(후보 20 + 엑셀 산출 글 + 우리 호표 · 출처) | 기대 — 남은 실패(복합 · 판 차이 · 같은 뜻)의 대부분 | — | 중간(길 · 열쇠) · 비용 줄당 수 원 | 외부 전송(실무 자료) · 매번 다른 답 · 없는 키 지어냄 → 후보 목록 안에서만 고르게 · 근거 실패 줄(약 15%)에만 · 사람 확정 그대로 | + +## 5. 추천 차례 — 먼저 할 것 셋 + +1. **근거를 이름보다 앞세우는 서버 고침 넷**(들 일 작음~중간 · 위험 낮음) — `M02_Hopyo_Basis._clause` 윗마디(엑셀 6-2 → 우리 6-2-2 · 6-2-3) · `M02_Hopyo_Match.candidates` 접힘 열쇠에 로직 키 · 조항 오타(13-114) · 같음일 때도 근거 검사(엑셀 글이 옛 판 「24년 · 임도품셈 · 사방품셈」이면 안 내림 · 근거 후보가 직종 또는 뜻 낱말을 나눌 때만) → 1순위 엄격 71.4 → 85.7% · 다섯 안 엄격 86.5 → 94.4%. 덤으로 장비 줄 길(기계 갈래 → 기계 시간당 기계경비 + 기계 입력)과 단위 별칭(hr · 공/㎥ · 개소). +2. **고르기 창 손질**(중간 · 부록 A) — 후보마다 책 · 조항 · 우리 값 ÷ 엑셀 단가(±5% 표시) · 엑셀 산출 글의 직종 · 품과 우리 호표를 나란히 · 같음 줄에도 근거 후보 · [같음 모두 확정]에서 근거 어긋남 빼기 · [다른 로직 찾기]에 엑셀 조항 미리 넣기. 다섯 안 정답이 94%라 남은 일은 「사람이 빨리 고르기」. +3. **기억 넓히기**(중간) — 확정 때 둘째 열쇠(품명|단위)를 「기억 후보」로 · 확정한 엑셀 이름을 층 별칭으로 남겨 이름 후보 · 찾기에 씀(손 사전 대신 사용자 확정이 사전이 됨) → 같은 회사 다음 파일 4/48 → 15/48 · 씨앗 효과(영월 +23.5pt)와 같은 결. + +- 별도로 알아 둘 것 — 다른 외부문서 대부분(STmate 꼴)은 지금 올리기부터 400. 「다른 문서에서도 이름이 다름」을 풀려면 STmate 읽기(일위대가표 · 단가산출근거 · D/L/X 코드로 줄 ↔ 블록 · 노임 · 장비 바로 이음)가 먼저 — 코드가 있어 이름 · 블록 잇기는 북부청 꼴보다 쉬움. +- 손 사전 · 금액 순위 점수 · 단위 거름망 · n-그램은 하지 않음(위 표에서 해로움 · 효과 없음). + +## 부록 A. 화면 사용 흐름 — 사람이 고르기 어렵게 만드는 것(추천 2 의 근거) + +- 후보 단추 글 = 「이름 [단위] · 키」 뿐(View.ts:446-452 · targetText :86-91) — 이름 후보에는 책 · 조항이 안 보여 같은 이름 산림 · 건설(유로폼 설치 및 해체 GF000210 · GC000214)을 구별 못 함. +- 점수 · 까닭은 마우스 올림(title)에만(View.ts:459) · 초록 「근거 맞음」은 조항 같음 + 단위 + 직종 셋 다일 때만(basisOk :122-126) — 윗마디 조항(6-2)이면 늘 회색. +- 「같음」 줄은 서버가 후보를 안 만듦(Match.py:395-403) → 고르기 창에 「가까운 것 없음」만(View.ts:499) · 다른 책으로 바꾸려면 [다른 로직 찾기…]로 이름을 다시 쳐야 함. +- [같음 모두 확정](View.ts:276-282)은 근거를 안 봄 — 이번 표본 25 중 5 가 엑셀과 다른 책. +- 엑셀 근거는 한 줄 요약(출처 · 직종 · 행 · View.ts:464-487) — 계산식(품 · 수량 · 장비)과 우리 로직 호표를 나란히 못 봄 · [엑셀에서 보기]로 창을 하나 더 엶. +- 후보마다 우리 값(엑셀 단위로 환산) ÷ 엑셀 단가가 없음 — ±5% 안이면 95% 맞는 신호인데 안 씀. +- [다른 로직 찾기…](View.ts:488-496)는 고르기 창을 닫고 빈 찾기 창을 엶 — 찾기 칸은 원문번호(조항)로도 걸러지지만(ui_template_logic_find.ts:44-53) 엑셀 조항을 미리 넣어 주지 않음 · 안내 없음. +- 표 「대상」 칸(View.ts:424)도 키 · 책 없이 이름만 — 같은 이름 둘이면 무엇을 골랐는지 안 보임. +- 확정 열쇠 = 품명|규격|단위 정확히 같을 때만(Match.py:68-76) · M01 로직 별칭(208-1)은 로직 1,438 중 0 — 확정한 엑셀 이름이 다음 파일 · 찾기 창에 안 쓰임. + +## 부록 B. 맞춤 흐름(파일:줄) + +- 올리기 — 화면 `M02_MasterTemplete_Hopyo_Side.ts` → `Hopyo_Api.ts:219-231 uploadHopyo` → `M02_Labor_Router.py:206 POST /layers/{층}/hopyo` → `_upload :174-184` → `M02_Hopyo_Excel.read_book :211-279`(`_sheets :74-102` 「일위대가 · 일위대가총괄표 · 단가산출」 세 시트만 · 없으면 400 `NO_SHEET :32` · 호표 · 줄 · `kind_of :132-149` 갈래 · 단산N참조 줄에 `Basis.read :84-125` 블록 근거 `{출처, 직종, 행}` :257-261) → `Grid.read` → `Store.save :78`(맞춤은 저장 안 함 · 열 때마다 셈). +- 호표 열기 — `Hopyo_View.ts drawOne :261-344` → `Api readHopyoOne :180-181` → `Router :243-252 GET …/hopyo/{key}/{no}` → `Match.view :460-474` → `match_hopyo :440-453` → `match_line :367-414`. +- `match_line` 차례 — % · 빼기 · 일위N(호표) → 층 기억 `recall :348-350`(열쇠 `line_key :68-76` = 품명|규격|단위) → 이름 같음 · 씨앗 `_exact :286-297`(`_seeds :266-278` 일위대가조합.json 메모) → 하나면 「같음」 끝(:395-403 · 후보 안 만듦) → 아니면 근거 후보 `Basis.candidates :191-235`(`_clause :166-176` 조항 같음 1.0 · 앞 마디 0.5 · 건설 공통/토목 0.1 · 직종 겹침 0.5 · 단위 0.15 · 직종도 뜻 낱말도 안 겹치면 버림 :213-214) + 이름 후보 `Match.candidates :300-329`(정리 이름 `tidy :79-84` 괄호 글 버림 · 글자 두 개 묶음 dice · 정리 이름 같음 +1 · 단위 0.15 · 산림 0.05 · 자재 규격 0.5 · 뜻 낱말 거름 `near :103-105` · (이름 · 규격 · 단위) 같으면 하나로 접음 :320-325) → `Basis.merge :238-256`(로직 키로 점수 합 · 낱말 안 겹친 근거 후보 뒤로) → `Basis.pick :259-266`(이름 1순위 한 자리) → 후보 5. +- 출처 읽기 — `Basis.sources :60-71`(「…품셈」 뒤 20 글자 안 절 번호 · 앞 글에 산림 · 임도 = 산림품셈 · 그 밖 건설품셈 — 부문 · 연도 버림) · 우리 쪽 조항 `M02_Labor_Inputs.sources :68-85` · `_NUMBER :20`. +- 고르기 · 확정 — `View choose :435-505` → `guessHopyo`(Api :183-193) → `Router :263-281` → `Match.guess :417-428`(입력값 · 환산) → [확정] `saveHopyoMemory` → `Router :283-304` → `Match.confirm :495-526`(층 기억). 단추 [같음 모두 확정] `View :276-282` · [근거 맞음 모두 확정] `View :284-291`(basisOk :122-126 · basisPick :153-158). + +## 부록 C. 실제 자료 + +- `resources/knowledge/original/실무문서/2026_실무일위대가 모음/` — `2026년_북부지방산림청_임도사업_설계내역서(공통단가).xls`(호표 68 · 줄 415 · 로직 줄 고유 52) · `2026년_북부지방산림청_사방사업(계류+댐)_설계내역서.xls`(호표 27 · 줄 201 · 고유 48) — 시험 `resources/tester/test_m02_hopyo_excel.py:26-30` 이 씀. 같은 폴더 `2016_상반_…사방사업표준품셈(안).xls` · `2026_상반_…수목진료.xls` = 일위대가 시트 없음(400). +- `resources/knowledge/original/실무문서/05. 동부청 영월/내역서/2024년 임도시설사업2차(산불진화_기번6)-영월.xlsx` — STmate 꼴(일위대가표 211행 · 단가산출근거 5,604행) · D 코드 줄 고유 36(줄 91). 같은 꼴 = 간선임도 기번3 · 울진 소광 · 계류보전 영덕 · 봉화 현동 내역 xlsx(시트 이름 확인) · 거창은 또 다른 꼴. +- 우리 로직 = `resources/master_data/로직_*.json`(M01 `loaded()` 읽기만) · 씨앗 = `resources/master_data/일위대가조합.json` 메모 80. +- tmp 안 실무 엑셀 없음. + +## 부록 D. 정답 표시 근거(고유 줄 136 · 전수) + +칸 「지금」 = 지금 맞춤 상태 · 1순위 키 · 표시(✓ 엄격 맞음 · △ 허용 맞음 · 5안 = 다섯 안에만 · ✗). N = 북부청 임도 · S = 사방 · Y = 영월. 「짐작」 = 블록에 출처가 없거나 우리 로직이 일부만 덮어 사람 판단이 섞임. 영월 「씨앗」 줄은 사용자 실무 이름 → 로직(일위대가조합.json)을 정답으로 봄. + +| id | 엑셀 품명 · 규격 [단위] ×줄 | 정답(엄격) · 허용 | 근거 | 지금 | +|---|---|---|---|---| +| N000 | 레미콘타설(장비사용) · 철근구조물-소량 타설 [㎥] ×13 | GC000198 · 허용 GF000155 | 건설 6-1-1 콘크리트공 3 · 보통인부 1 ÷ 일 시공량(+소량 ×2) + 굴착기 시간 — GC000198 6-1-1(타설방법 · 소량감률 · 굴착기 입력) | 후보 GC000198 ✓ | +| N001 | 합판거푸집 · 4회 [㎡] ×11 | GC000211 · 허용 GF000160 | 건설 6-3-1 형틀목공 5 · 보통인부 2 ÷ 일 시공량 = GC000211 6-3-1 · 이름 같음 GF000160 은 산림 12-4 | 같음 GF000160 △(허용) | +| N002 | 기초잡석채집,운반,다짐 · 발파암활용,덤프15톤,L=500m [㎥] ×18 | GC000110 · GF000168 · 짐작 | 채집 · 적사 · 운반 + 기초지정 건설 3-4-8 = GC000110(부분) · 산림 12-12(옛 번호) → 지금 12-25 기초잡석 GF000168 | 후보 GC000110 ✓ | +| N003 | 철근가공조립(현장) · Type-Ⅰ(소량 시공) [Ton] ×12 | GC000206 · GC000207 · 허용 GF000159 | 건설 6-2 Type 가공(철근공 0.67 · 보통 0.22) + 조립 — 우리 6-2-2 가공 · 6-2-3 조립 둘(복합) · 산림 12-3 GF000159 같은 일 · 이름 1위 GF000187 은 산림 12-18 복잡 고정 4.2인(다른 품) | 후보 GF000187 5안 | +| N004 | 야면석채집 및 운반(계곡내) · 굴삭기0.6㎥,35cm,4.5톤,L=500m [㎡] ×1 | GF000214 · 짐작 | 인력 10% 보통인부 0.17인/㎡(= 산림 13-2-4 뒷길이 35㎝) + 굴착기 채집 + 운반 — GF000214 은 인력 채집 몫(부분) · 건설 8-2 는 2016 삭제 | 후보 GF000214 ✓ | +| N005 | 고임돌 채집 및 운반 · 굴삭기0.6㎥(90%),4.5톤,L=500m [㎥] ×1 | GF000211 · 허용 GF000212 · 짐작 | 채집(기계 90 · 인력 10% 보통인부 0.3인/㎥ = 산림 13-2-1 막자갈) + 상차 + 운반 — GF000211 은 인력 채집 몫만 · 출처 1-10 은 단위중량 인용 · 1위 GC000363 은 궤도 자갈(토목 4-6-1) | 후보 GC000363 5안 | +| N006 | 야면석소운반 · L=35cm [㎡] ×3 | GF000280 · 허용 GF000279 · 짐작 | 건설 1-2-7 인력운반 공식(575㎏/㎡ ÷ 25㎏ ÷ 42회) — 우리 건설 1-2-7 없음 · 산림 10-6-2 인력운반 돌(지게) 같은 일(㎡ ↔ ㎥) | 후보 GF000214 ✗ | +| N007 | 야면석찰붙임 · L3=35cm적용,장비사용 [㎡] ×7 | GF000225 · 허용 GC000263 | 산림 13-5-2 석공 0.11 · 보통 0.04 + 굴착기 집게 = GF000225 돌붙임 찰붙임(장비) | 후보 GF000225 ✓ | +| N008 | 구조물터파기(인력100%) · 토사 [㎥] ×4 | GC000083 · 허용 GF000101 | 건설 3-2-1 특별인부 (3.6+2.7+2.2)/3 = GC000083 굴착(인력/토사) | 후보 GC000083 ✓ | +| N009 | 구조물되메우기(인력100%) [㎥] ×4 | GF000113 | 산림 9-14 인력품 보통인부 0.1인 = GF000113 되메우기(9-14-1) | 후보 GF000113 ✓ | +| N010 | 잔토처리(인력100%) [㎥] ×4 | 우리에 없음 · 부분 GC000083 | 보통인부 0.2인/㎥ = 건설 3-2-1 비고 잔토처리 — 우리 단독 로직 없음(GC000083 입력 「잔토처리」로 굴착과 묶임) | 없음 — 없음 줄 · 후보 없음 | +| N011 | 야면석채집및운반(계곡내) · 굴삭기0.6㎥,35cm,4.5톤,L=500m [㎡] ×7 | GF000214 · 짐작 | 인력 10% 보통인부 0.17인/㎡(= 산림 13-2-4 뒷길이 35㎝) + 굴착기 채집 + 운반 — GF000214 은 인력 채집 몫(부분) · 건설 8-2 는 2016 삭제 | 후보 GF000214 ✓ | +| N012 | 고임돌채집및운반 · 굴삭기0.6㎥(90%),4.5톤,L=500m [㎥] ×17 | GF000211 · 허용 GF000212 · 짐작 | 채집(기계 90 · 인력 10% 보통인부 0.3인/㎥ = 산림 13-2-1 막자갈) + 상차 + 운반 — GF000211 은 인력 채집 몫만 · 출처 1-10 은 단위중량 인용 · 1위 GC000363 은 궤도 자갈(토목 4-6-1) | 후보 GC000363 5안 | +| N013 | 레미콘타설(장비사용) · 무근구조물 [㎥] ×13 | GC000198 · 허용 GF000155 | 건설 6-1-1 콘크리트공 3 · 보통인부 1 ÷ 일 시공량(+소량 ×2) + 굴착기 시간 — GC000198 6-1-1(타설방법 · 소량감률 · 굴착기 입력) | 후보 GC000198 ✓ | +| N014 | 유로폼 · 보통,0~7m [㎡] ×13 | GC000214 · 허용 GF000210 | 건설 6-3-3 형틀목공 4 · 보통 1 ÷ 35~40 = GC000214 | 같음 GC000214 ✓ | +| N015 | 문양거푸집 [㎡] ×4 | GC000215 · 허용 GF000161 | 건설 6-3-4 형틀목공 0.07 · 보통 0.03 = GC000215 · 이름 같음 GF000161 은 산림 12-5 | 같음 GF000161 △(허용) | +| N016 | P.V.C파이프설치 · Φ50mm [M] ×11 | GF000192 | 산림 12-22 재료비 + 설치비 5% = GF000192 P.V.C 파이프 설치(개소 ↔ M) | 후보 GF000192 ✓ | +| N017 | 다웰바설치 · Φ32×1000 [EA] ×4 | GC000226 · 허용 GF000201 | 건설 6-3-10 다웰바 형틀목공 0.043 = GC000226 · 산림 12-30-1 GF000201(D32 · L1000) 같은 일 | 후보 GC000226 ✓ | +| N018 | 휠라설치 · T=15mm,1000×1500mm [㎡] ×4 | GC000227 | 건설 6-3-10 휠라(채움재) 형틀목공 0.029 = GC000227 신축이음 채움재 설치 | 후보 GC000227 ✓ | +| N019 | 철근가공조립(현장) · 연결철근,Type-Ⅰ×30%적용 [Ton] ×4 | GC000206 · GC000207 · 허용 GF000159 | 건설 6-2 Type 가공(철근공 0.67 · 보통 0.22) + 조립 — 우리 6-2-2 가공 · 6-2-3 조립 둘(복합) · 산림 12-3 GF000159 같은 일 · 이름 1위 GF000187 은 산림 12-18 복잡 고정 4.2인(다른 품) | 후보 GF000187 5안 | +| N020 | 큰돌채집운반 · 공사중 발생석 활용 50%적용 [㎡] ×4 | GF000213 | 산림 13-2-3 작업반장 0.026인/개 × 4개/㎡ × 75% × 50 · 40% + 굴착기 = GF000213 큰돌 채집(기계)(개 ↔ ㎡) | 후보 GF000213 ✓ | +| N021 | 큰돌쌓기(메쌓기) · 직경40cm이상~60cm미만 [㎡] ×2 | GF000226 · 허용 GC000260 | 산림 13-6-1 = GF000226 큰돌쌓기 메쌓기 | 후보 GF000226 ✓ | +| N022 | 뒷채움돌채집운반뒷채움(발파) · 굴삭기0.6㎥(90%),4.5톤,L=500m [㎥] ×11 | GC000106 · 허용 GF000218 · 짐작 | 채집(9:1) · 운반 + 뒷채움 및 다짐 건설 3-4-4(소형장비) = GC000106(부분) | 후보 GC000106 ✓ | +| N023 | 큰돌쌓기(찰쌓기)_배수공 · 직경40cm이상~60cm미만 [㎡] ×2 | GF000227 | 산림 13-6-2 = GF000227 큰돌쌓기 찰쌓기 | 후보 GF000227 ✓ | +| N024 | 레미콘타설(인력운반) · 무근구조물-소량 타설 [㎥] ×3 | GC000198 · 허용 GF000155 | 건설 6-1-1 콘크리트공 3 · 보통인부 1 ÷ 일 시공량(+소량 ×2) + 굴착기 시간 — GC000198 6-1-1(타설방법 · 소량감률 · 굴착기 입력) | 후보 GC000198 ✓ | +| N025 | 합판거푸집 · 6회 [㎡] ×12 | GC000211 · 허용 GF000160 | 건설 6-3-1 형틀목공 5 · 보통인부 2 ÷ 일 시공량 = GC000211 6-3-1 · 이름 같음 GF000160 은 산림 12-4 | 같음 GF000160 △(허용) | +| N026 | 도로반사경설치 · D=1000(STS) [개소] ×1 | GC000310 | 건설 1-8-3 = GC000310 도로반사경 설치(본 ↔ 개소) | 같음 GC000310 ✓ | +| N027 | 야면석메붙임 · L3 =35cm내외,장비사용 [㎡] ×1 | GF000224 · 허용 GC000262 | 산림 13-5-2 석공 0.13 · 보통 0.04 = GF000224 돌붙임 메붙임(장비) | 후보 GF000224 ✓ | +| N028 | 큰돌메붙임 · 직경40cm이상~60cm미만 [㎡] ×2 | GF000229 · 허용 GC000262 | 산림 13-7-1 = GF000229 큰돌붙이기 메붙이기 | 후보 GF000229 ✓ | +| N029 | 큰돌찰붙임 · 직경40cm이상~60cm미만 [㎡] ×4 | GF000230 · 허용 GC000263 | 산림 13-7-2 = GF000230 큰돌붙이기 찰붙이기 | 후보 GF000230 ✓ | +| N030 | 레미콘타설(장비사용) · 소형구조물 [㎥] ×2 | GC000198 · 허용 GF000155 | 건설 6-1-1 콘크리트공 3 · 보통인부 1 ÷ 일 시공량(+소량 ×2) + 굴착기 시간 — GC000198 6-1-1(타설방법 · 소량감률 · 굴착기 입력) | 후보 GC000198 ✓ | +| N031 | 야면석찰쌓기 · L3=35cm적용,장비사용 [㎡] ×2 | GF000222 · 허용 GC000261 | 산림 13-4-5 석공 0.09 · 보통 0.05 = GF000222 돌쌓기 찰쌓기(장비) | 후보 GF000222 ✓ | +| N032 | 면고르기 [㎡] ×4 | GC000111 · 허용 GF000124 | 건설 3-5-1 절토면 고르기 굴착기 1.0 = GC000111 · 씨앗 같음 GF000124 은 산림 9-19-1 | 같음 GF000124 △(허용) | +| N033 | 대형브레이카 · +굴착기(0.7㎥) [hr] ×3 | GC000271 | 기계 hr 대형브레이카 + 굴착기 0.7 = GC000271 | 후보 GC000271 ✓ | +| N034 | 와이어메시바닥깔기 · #6100×100 [㎡] ×1 | GC000665 | 건설 8-1-3 = GC000665 | 같음 GC000665 ✓ | +| N035 | 원형돌망태 설치 및 돌채움 · #8,45×45 cm [㎡] ×6 | GF000328 | 산림 13-11-1 조립 0.08 · 돌채움 0.17 = GF000328 원형 돌망태 | 후보 GF000328 ✓ | +| N036 | 조약돌채집및운반 · 굴삭기0.6㎥(90%),4.5톤,L=500m [㎥] ×5 | GF000211 · 짐작 | 채집 인력 10%(보통인부 0.3 · 0.6인/㎥ = 산림 13-2-1 막자갈 · 조약돌) + 상차 + 운반 — GF000211(부분) · 1위 GC000363 궤도 자갈 | 후보 GC000363 5안 | +| N037 | 돌망태형 옹벽설치 · 사각돌망태 [㎡] ×4 | GF000235 · 허용 GC000342 | 산림 13-114(= 13-11-4 오타) 사각돌망태 = GF000235 · 이름 같음 GC000342 은 건설 토목 2-2-3 | 같음 GC000342 △(허용) | +| N038 | 콘크리트포장(인력) · T=20cm, 종단15%미만 [㎥] ×4 | GC000297 · 허용 GF000162 | 건설 1-6-2 포장공 4 · 보통 2 ÷ 100㎥(15% 이상 ×1.2) = GC000297 콘크리트 표층 인력포설 · 산림 12-6 GF000162(종단경사 입력) 같은 일 | 후보 GC000297 ✓ | +| N039 | 비탈면 보호공 · 평떼 [㎡] ×6 | GC000143 · 허용 GF000032 | 건설 4-1-1 조경공 1 · 보통 4 ÷ 150 + 떼꽂이(산림 5-23-3) = GC000143 잔디붙임(주) | 후보 GC000143 ✓ | +| N040 | 콘크리트포장(인력) · T=20cm,종단15%이상 [㎥] ×3 | GC000297 · 허용 GF000162 | 건설 1-6-2 포장공 4 · 보통 2 ÷ 100㎥(15% 이상 ×1.2) = GC000297 콘크리트 표층 인력포설 · 산림 12-6 GF000162(종단경사 입력) 같은 일 | 후보 GC000297 ✓ | +| N041 | 고화매트 설치 [㎡] ×1 | 우리에 없음 · 부분 GF000124 · GF000126 | 고화매트120 설치(건설품셈 p.266 · 우리 없음) + 토사면 고르기(산림 9-19-1) + 크레인 · 살수 | 후보 GC000343 없음 줄 · 엉뚱 후보 보임 | +| N042 | 큰돌쌓기(찰쌓기) · 직경40cm이상~60cm미만 [㎡] ×1 | GF000227 | 산림 13-6-2 = GF000227 | 후보 GF000227 ✓ | +| N043 | 레미콘타설(장비사용) · 철근구조물 [㎥] ×7 | GC000198 · 허용 GF000155 | 건설 6-1-1 콘크리트공 3 · 보통인부 1 ÷ 일 시공량(+소량 ×2) + 굴착기 시간 — GC000198 6-1-1(타설방법 · 소량감률 · 굴착기 입력) | 후보 GC000198 ✓ | +| N044 | 강관동바리 · 암거용-1개월사용 [공/㎥] ×7 | GC000015 · 허용 GF000189 | 건설 2-6-1 형틀목공 3 · 보통 1 ÷ 50공㎥ = GC000015 강관 동바리(토목) · 이름 같음 GF000189 은 산림 12-20 | 같음 GF000189 △(허용) | +| N045 | 철근가공조립(현장) · Type-ll [Ton] ×7 | GC000206 · GC000207 · 허용 GF000159 | 건설 6-2 Type 가공(철근공 0.67 · 보통 0.22) + 조립 — 우리 6-2-2 가공 · 6-2-3 조립 둘(복합) · 산림 12-3 GF000159 같은 일 · 이름 1위 GF000187 은 산림 12-18 복잡 고정 4.2인(다른 품) | 후보 GF000187 5안 | +| N046 | 강관비계 · 10m이하,3개월사용 [㎡] ×6 | GC000022 · 허용 GF000188 | 건설 2-7-1 비계공 3 · 보통 1 ÷ 55㎡ = GC000022 강관비계(쌍줄) · 이름 같음 GF000188 은 산림 12-19(공㎥) | 같음 GF000188 △(허용) | +| N047 | 흙깍기(토사) · 굴삭기1.0㎥,보통토사 [㎥] ×1 | GC000085 · 허용 GF000089 | 건설 3-2-3 굴착기 1.0 310㎥/일 = GC000085 흙깎기(기계) 보통토사 | 후보 GC000085 ✓ | +| N048 | 잔토정리 · 굴삭기0.6㎥ [㎥] ×1 | 우리에 없음 · 부분 GC000267 · 짐작 | 굴착기 0.6 사이클 공식만(출처 없음) — 우리 「잔토정리」 로직 없음 | 후보 GC000357 없음 줄 · 엉뚱 후보 보임 | +| N049 | 잡철물 제작설치 · 규격철물 설치(일반철재) [Ton] ×1 | GC000676 | 건설(건축) 8-3-1 = GC000676 잡철물 제작 및 설치 | 후보 GC000676 ✓ | +| N050 | 도장(철재면2회) · 방청1,유성2 [㎡] ×1 | GC000705 · GC000714 · GC000712 | 건설(건축) 11-1-3 바탕 + 11-2-6 녹막이 + 11-2-4 유성 — 우리 셋(복합) | 후보 GC000705 ✓ | +| N051 | 임도안전표지판설치 · 삼각,900mm [개소] ×1 | GC000308 | 건설 1-8-1 지주 · 표지판 = GC000308 교통 안전표지판 설치 | 후보 GC000308 ✓ | +| S052 | 레미콘타설(장비사용) · 철근구조물 [㎥] ×1 | GC000198 · 허용 GF000155 | 건설 6-1-1 콘크리트공 3 · 보통인부 1 ÷ 일 시공량(+소량 ×2) + 굴착기 시간 — GC000198 6-1-1(타설방법 · 소량감률 · 굴착기 입력) | 후보 GC000198 ✓ | +| S053 | 레미콘타설(장비사용) · 무근구조물 [㎥] ×4 | GC000198 · 허용 GF000155 | 건설 6-1-1 콘크리트공 3 · 보통인부 1 ÷ 일 시공량(+소량 ×2) + 굴착기 시간 — GC000198 6-1-1(타설방법 · 소량감률 · 굴착기 입력) | 후보 GC000198 ✓ | +| S054 | 유로폼(Euro Form) · 6회, 보통 [㎡] ×2 | GC000214 · 허용 GF000210 | 건설 6-3-3 형틀목공 4 · 보통 1 ÷ 35~40 = GC000214 | 후보 GC000214 ✓ | +| S055 | 철근가공조립(현장) · Type-Ⅱ [Ton] ×1 | GC000206 · GC000207 · 허용 GF000159 | 건설 6-2 Type 가공(철근공 0.67 · 보통 0.22) + 조립 — 우리 6-2-2 가공 · 6-2-3 조립 둘(복합) · 산림 12-3 GF000159 같은 일 · 이름 1위 GF000187 은 산림 12-18 복잡 고정 4.2인(다른 품) | 후보 GF000187 5안 | +| S056 | 강관비계 · (10m이하) [㎡] ×2 | GC000022 · 허용 GF000188 | 건설 2-7-1 비계공 3 · 보통 1 ÷ 55㎡ = GC000022 강관비계(쌍줄) · 이름 같음 GF000188 은 산림 12-19(공㎥) | 같음 GF000188 △(허용) | +| S057 | 강관동바리 · (2.5m이하) [공/㎥] ×1 | GC000015 · 허용 GF000189 | 건설 2-6-1 형틀목공 3 · 보통 1 ÷ 50공㎥ = GC000015 강관 동바리(토목) · 이름 같음 GF000189 은 산림 12-20 | 같음 GF000189 △(허용) | +| S058 | 유로폼(Euro Form) · 6회, 간단 [㎡] ×1 | GC000214 · 허용 GF000210 | 건설 6-3-3 형틀목공 4 · 보통 1 ÷ 35~40 = GC000214 | 후보 GC000214 ✓ | +| S059 | 철근가공조립(현장) · Type-Ⅰ(소량) [Ton] ×3 | GC000206 · GC000207 · 허용 GF000159 | 건설 6-2 Type 가공(철근공 0.67 · 보통 0.22) + 조립 — 우리 6-2-2 가공 · 6-2-3 조립 둘(복합) · 산림 12-3 GF000159 같은 일 · 이름 1위 GF000187 은 산림 12-18 복잡 고정 4.2인(다른 품) | 후보 GF000187 5안 | +| S060 | 철강재운반(할증포함) · (구역화물) [Ton] ×1 | 우리에 없음 · 부분 GF000283 · 짐작 | 구역화물자동차 운임(왕복 150㎞ + 60㎞) — 우리 화물 운임 로직 없음(덤프 운반 GF000283 은 다른 셈) | 후보 GF000283 없음 줄 · 엉뚱 후보 보임 | +| S061 | 철골설치 [Ton] ×1 | GC000563 · 허용 GC000561 · 짐작 | 크레인 10Ton 1hr/Ton 철골세우기 기계만(출처 없음) = GC000563 철골세우기 장비 손료(ton당) | 후보 GC000571 ✗ | +| S062 | 철골가공조립(공장) · (대형) [Ton] ×1 | GC000559 · 허용 GC000560 | 건설(건축) 1-1 철골공 2.48 × 0.71(89%) + 1-1-3 용접 — GC000559 주 · GC000560 곁 | 후보 GC000560 △(허용) | +| S063 | 잡철물제작 [Ton] ×2 | GC000676 | 건설 8-3-1 = GC000676 | 후보 GC000676 ✓ | +| S064 | 합판거푸집(6회) [㎡] ×7 | GC000211 · 허용 GF000160 | 건설 6-3-1 형틀목공 5 · 보통인부 2 ÷ 일 시공량 = GC000211 6-3-1 · 이름 같음 GF000160 은 산림 12-4 | 후보 GC000211 ✓ | +| S065 | 와이어메시 바닥깔기 [㎡] ×2 | GC000665 | 건설 8-1-3 = GC000665 | 같음 GC000665 ✓ | +| S066 | 구조물터파기(토사) [㎥] ×18 | GC000089 · 허용 GC000088 | 인력 10% + 기계 90% 혼합토사 건설 3-2-4 = GC000089 터파기(기계) 혼합토사 | 후보 GC000090 5안 | +| S067 | 구조물되메우기 및 다짐 [㎥] ×6 | GC000109 | 건설 3-4-7 대형장비 290㎥/일 = GC000109 | 후보 GC000109 ✓ | +| S068 | 잔토처리 [㎥] ×18 | 우리에 없음 · 짐작 | 굴착기 0.8 사이클 기계 100%(출처 없음) — 우리 잔토처리 로직 없음 | 없음 — 없음 줄 · 후보 없음 | +| S069 | 레미콘타설(장비사용) · 무근구조물-소량타설 [㎥] ×7 | GC000198 · 허용 GF000155 | 건설 6-1-1 콘크리트공 3 · 보통인부 1 ÷ 일 시공량(+소량 ×2) + 굴착기 시간 — GC000198 6-1-1(타설방법 · 소량감률 · 굴착기 입력) | 후보 GC000198 ✓ | +| S070 | 합판거푸집(4회) [㎡] ×1 | GC000211 · 허용 GF000160 | 건설 6-3-1 형틀목공 5 · 보통인부 2 ÷ 일 시공량 = GC000211 6-3-1 · 이름 같음 GF000160 은 산림 12-4 | 후보 GC000211 ✓ | +| S071 | 기초잡석다짐 및 채집운반 · (9:1)(0.6㎥+4.5Ton) [㎥] ×1 | GC000110 · GF000168 · 짐작 | 채집 · 적사 · 운반 + 기초지정 건설 3-4-8 = GC000110(부분) · 산림 12-12(옛 번호) → 지금 12-25 기초잡석 GF000168 | 후보 GC000110 ✓ | +| S072 | 배수관부설 · (D=1000mm) [M] ×1 | GC000404 · 허용 GF000176 · GF000179 | 건설 6-7-2 파형강관 배관공(수도) · 보통 ÷ 6 = GC000404 파형강관 부설 및 접합(본 ↔ M) · 산림 12-11-3 GF000176 | 후보 GC000404 ✓ | +| S073 | 철골공 [인] ×1 | 우리에 없음 | 노임 한 줄(인 단위) — 로직 아님(인력 단가) | 후보 GC000560 없음 줄 · 엉뚱 후보 보임 | +| S074 | 특별인부 [인] ×1 | 우리에 없음 | 노임 한 줄(인 단위) — 로직 아님(인력 단가) | 없음 — 없음 줄 · 후보 없음 | +| S075 | 레미콘타설(인력운반) · 무근구조물-소량타설 [㎥] ×1 | GC000198 · 허용 GF000155 | 건설 6-1-1 콘크리트공 3 · 보통인부 1 ÷ 일 시공량(+소량 ×2) + 굴착기 시간 — GC000198 6-1-1(타설방법 · 소량감률 · 굴착기 입력) | 후보 GC000198 ✓ | +| S076 | 구조물터파기 · 인력 [㎥] ×1 | GC000083 · 허용 GF000101 | 건설 3-2-1 특별인부 = GC000083 | 후보 GC000083 ✓ | +| S077 | 구조물되메우기 · 인력 [㎥] ×1 | GF000113 · 짐작 | 보통인부 0.1인/㎥(출처 없음) = 산림 9-14-1 GF000113(임도 파일 단산12 와 같은 값) | 후보 GF000113 ✓ | +| S078 | 잔토처리 · 인력 [㎥] ×1 | 우리에 없음 · 부분 GC000083 | 보통인부 0.2인/㎥ = 건설 3-2-1 비고 잔토처리 — 우리 단독 로직 없음(GC000083 입력 「잔토처리」로 굴착과 묶임) | 없음 — 없음 줄 · 후보 없음 | +| S079 | 원형돌망태조립설치 · 산림사업품셈 [조] ×1 | GF000328 | 산림 13-11-1 조립설치 = GF000328(시공 입력 · 조 ↔ ㎡) | 후보 GF000328 ✓ | +| S080 | 원형돌망태채움 · 산림사업품셈 [조] ×1 | GF000328 | 산림 13-11-1 돌채움 = GF000328 | 후보 GF000328 ✓ | +| S081 | 조약돌채집운반 · (9:1)(0.6㎥+4.5Ton) [㎥] ×1 | GF000211 · 짐작 | 채집 인력 10%(보통인부 0.3 · 0.6인/㎥ = 산림 13-2-1 막자갈 · 조약돌) + 상차 + 운반 — GF000211(부분) · 1위 GC000363 궤도 자갈 | 후보 GF000212 5안 | +| S082 | 구조물터파기(암) [㎥] ×2 | GC000090 | 건설 3-2-4 암 = GC000090 | 후보 GC000090 ✓ | +| S083 | 배수관부설 · (D=500mm) [M] ×1 | GC000404 · 허용 GF000176 · GF000179 | 건설 6-7-2 파형강관 배관공(수도) · 보통 ÷ 6 = GC000404 파형강관 부설 및 접합(본 ↔ M) · 산림 12-11-3 GF000176 | 후보 GC000404 ✓ | +| S084 | 레미콘타설(무근펌프카)(130㎥) · 41m(80-95㎥/hr) [㎥] ×1 | GC000201 · 허용 GF000182 | 건설 6-1-4 펌프차 편성 = GC000201 | 후보 GC000201 ✓ | +| S085 | 콘크리트 펌프차 · 41m,80~95㎥/hr [hr] ×1 | GC000268 · GC000269 · GC000275 · 짐작 | 기계 hr 콘크리트펌프차 41m — 우리 「기계 시간당 기계경비」(기계 입력) | 후보 GC000201 ✗ | +| S086 | 문양거푸집 [㎡] ×1 | GC000215 · 허용 GF000161 | 건설 6-3-4 형틀목공 0.07 · 보통 0.03 = GC000215 · 이름 같음 GF000161 은 산림 12-5 | 같음 GF000161 △(허용) | +| S087 | 지수판 설치 · 산림사업품셈 [M] ×1 | GF000196 | 산림 12-27-1 = GF000196 지수판 설치 | 같음 GF000196 ✓ | +| S088 | P.V.C 파이프 설치 · 산림사업품셈 [M] ×1 | GF000192 | 산림 12-22 = GF000192(개소 ↔ M) | 후보 GF000192 ✓ | +| S089 | 떼붙임(평떼) · 산림사업품셈 [㎡] ×2 | GF000025 · 허용 GF000032 | 산림 5-12 보통인부 0.06 + 떼 소운반 = GF000025 떼붙임(재배잔디) | 후보 GF000025 ✓ | +| S090 | 큰돌메쌓기 · 40cm-60cm,(산림사업품셈) [㎡] ×1 | GF000226 · 허용 GC000260 | 산림 13-6-1 | 후보 GF000226 ✓ | +| S091 | 고임돌채집및운반 · (9:1)(0.6㎥+4.5Ton) [㎥] ×9 | GF000211 · 허용 GF000212 · 짐작 | 채집(기계 90 · 인력 10% 보통인부 0.3인/㎥ = 산림 13-2-1 막자갈) + 상차 + 운반 — GF000211 은 인력 채집 몫만 · 출처 1-10 은 단위중량 인용 · 1위 GC000363 은 궤도 자갈(토목 4-6-1) | 후보 GC000363 5안 | +| S092 | 뒷채움잡석채집운반다짐 · (9:1)(0.6㎥+4.5Ton) [㎥] ×4 | GC000106 · 허용 GF000218 · 짐작 | 채집(9:1) · 운반 + 뒷채움 및 다짐 건설 3-4-4(소형장비) = GC000106(부분) | 후보 GC000106 ✓ | +| S093 | 큰돌찰쌓기 · 40cm-60cm,(산림사업품셈) [㎡] ×10 | GF000227 · 허용 GC000261 | 산림 13-6-2 | 후보 GF000227 ✓ | +| S094 | 큰돌채집운반 · 산림사업품셈 [㎡] ×5 | GF000213 | 산림 13-2-3 작업반장 0.026인/개 × 4개/㎡ × 75% × 50 · 40% + 굴착기 = GF000213 큰돌 채집(기계)(개 ↔ ㎡) | 후보 GF000213 ✓ | +| S095 | 뒷채움잡석구입운반 [㎥] ×1 | GF000135 · GF000284 · 짐작 | 적사(굴착기 0.8) + 덤프 15t 운반 18.7㎞(출처 없음) = 산림 10-3-2 골재운반 잡석 · 10-12-1 덤프운반 | 후보 GF000135 ✓ | +| S096 | 뒷채움잡석뒷채움 [㎥] ×2 | GC000106 | 건설 3-4-4 소형장비 = GC000106 | 후보 GC000106 ✓ | +| S097 | 뒷채움잡석구입운반 · (창봉 기번3)(2차) [㎥] ×1 | GF000135 · GF000284 · 짐작 | 적사(굴착기 0.8) + 덤프 15t 운반 18.7㎞(출처 없음) = 산림 10-3-2 골재운반 잡석 · 10-12-1 덤프운반 | 후보 GF000135 ✓ | +| S098 | 큰돌메붙임 · 40cm-60cm,(산림사업품셈) [㎡] ×2 | GF000229 · 허용 GC000262 | 산림 13-7-1 | 후보 GF000229 ✓ | +| S099 | 큰돌찰붙임 · 40cm-60cm,(산림사업품셈) [㎡] ×7 | GF000230 · 허용 GC000263 | 산림 13-7-2 | 후보 GF000230 ✓ | +| Y000 | 무근콘크리트타설 · 인력운반타설 [M3] ×3 | GC000198 · 허용 GF000155 | 건설 6-1-1 인력운반 23㎥/일 · 씨앗 같음 | 같음 GC000198 ✓ | +| Y001 | 합판거푸집(6회) · 간단 [M2] ×6 | GC000211 · 허용 GF000160 | 건설 6-3-1 · 씨앗 같음 | 같음 GC000211 ✓ | +| Y002 | 구조물터파기(인력) · 경질토사 [M3] ×3 | GF000101 · 허용 GC000083 · 짐작 | 옛 건설 3-1-3 인력 터파기 보통인부 0.26 · 씨앗 = GF000101 | 같음 GF000101 ✓ | +| Y003 | 구조물되메우기 · (인력) [M3] ×4 | GF000113 | 보통인부 0.1 · 씨앗 같음 | 같음 GF000113 ✓ | +| Y004 | 잔토처리 · 인력100% [M3] ×4 | 우리에 없음 | 잔토처리 인력 0.2인 — 단독 로직 없음 | 후보 GC000346 없음 줄 · 엉뚱 후보 보임 | +| Y005 | 크레인(타이어) · 10톤 [시간] ×1 | GC000268 · GC000269 · GC000275 · 짐작 | 기계 시간 크레인(타이어) 10톤 — 기계 시간당 기계경비(기계 입력) | 후보 GF000144 ✗ | +| Y006 | 구조물터파기 · 토사(B/H 90%+인력10%) [M3] ×1 | GF000101 · 허용 GC000089 · 짐작 | 임도품셈 5-1-2(옛) 기계 90 + 인력 10 · 씨앗 = GF000101 | 같음 GF000101 ✓ | +| Y007 | 잔토처리 · 토사(B/H 100%) [M3] ×1 | 우리에 없음 | 잔토처리 기계 100%(백호 0.7) — 로직 없음 | 없음 — 없음 줄 · 후보 없음 | +| Y008 | 콘크리트타설 · 임도품셈 5-5 [M3] ×6 | GC000198 · 허용 GF000155 · GF000166 · 짐작 | 임도품셈 5-5(옛) 콘크리트공 0.15 · 보통 0.27 · 씨앗 = GC000198 | 같음 GC000198 ✓ | +| Y009 | 유로폼 설치,해체(복잡) · 수직고7.0m 이하 [M2] ×5 | GC000214 · 허용 GF000210 | 건설 6-3-3 유로폼(복잡) — GC000214 · 이름 후보에서 산림 GF000210 과 한 줄로 접혀 근거 점수만 받음 | 후보 GC000223 5안 | +| Y010 | 철근가공조립(현장) · TYPE Ⅰ [Ton] ×6 | GC000206 · GC000207 · 허용 GF000159 | 건설 6-2-3 · 6-2-2 가공 + 조립 | 후보 GF000187 5안 | +| Y011 | 기초잡석채집및운반부설 · 4.5톤(L=500) [M3] ×11 | GF000168 · 허용 GC000110 · 짐작 | 채집(9:1) · 운반 · 부설 — 산림 12-25 기초잡석(짐작) | 후보 GF000168 ✓ | +| Y012 | 임도안전표지판설치 · 삼각,D=900 [개소] ×1 | GC000308 | 옛 1-9-3 표지판 · 씨앗 같음 | 같음 GC000308 ✓ | +| Y013 | 야면석 채집 운반 · (25x25x35㎝)장비 [M2] ×4 | GF000214 · 짐작 | 기계 8 : 인력 2 채집 + 운반 — 인력 채집 몫(부분) | 후보 GF000214 ✓ | +| Y014 | 야면석찰붙임 · 뒷길이 35Cm [M2] ×3 | GC000263 · 허용 GF000225 | 건설 7-2-2 석공 0.05 + 굴착기 집게 = GC000263 | 후보 GC000263 ✓ | +| Y015 | 고임돌채집운반 · 4.5톤덤프(L=500) [M3] ×5 | GF000211 · 허용 GF000212 · 짐작 | 채집(9:1) + 덤프 — 인력 채집 몫(짐작) | 후보 GF000212 △(허용) | +| Y016 | 잡석운반및다짐 [M3] ×2 | GF000218 | 씨앗 같음(잡석운반및다짐) | 같음 GF000218 ✓ | +| Y017 | 면고르기 [M2] ×2 | GF000124 · 허용 GC000111 | 보통인부 0.009 + 굴착기 0.7 · 씨앗 같음 | 같음 GF000124 ✓ | +| Y018 | 구조물터파기 · 인력(경질토사) [M3] ×1 | GF000101 · 허용 GC000083 · 짐작 | 씨앗 같음 | 같음 GF000101 ✓ | +| Y019 | 도로반사경설치 · D=1000(STS) [개소] ×1 | GC000310 | 씨앗 같음 | 같음 GC000310 ✓ | +| Y020 | 철근콘크리트타설 · 장비사용타설 [M3] ×2 | GC000198 · 허용 GF000155 | 건설 6-1-1 장비사용 55㎥/일 + 굴삭기 — GC000198(1위 펌프차 · 토목 6-1-1 소켓관이 부문 없이 조항 같음) | 후보 GC000201 5안 | +| Y021 | 유로폼 설치,해체(보통) · 수직고7.0m 이하 [M2] ×1 | GC000214 · 허용 GF000210 | 건설 6-3-3 유로폼 — 이름 같은 산림 GF000210 과 접힘(이름 후보 중복 빼기) | 후보 GC000223 5안 | +| Y022 | 지수판 설치(PVC 용접) · 200×5t [m] ×1 | GC000224 | 씨앗 같음 | 같음 GC000224 ✓ | +| Y023 | 배수관부설및접합(P.V.C) · Ø 50 MM - VG2 일반관 [m] ×1 | GC000393 | 토목 6-4-2 · 씨앗 같음 | 같음 GC000393 ✓ | +| Y024 | 야면석소운반(L=35CM) · (L=20m) [M2] ×2 | GF000280 · 허용 GF000279 · 짐작 | 옛 건설 9-1 인력운반(지게) 575㎏/㎡ — 산림 10-6-2(짐작) | 후보 GF000214 ✗ | +| Y025 | 콘크리트소운반 · (L=20m) [M3] ×1 | GF000279 · GF000281 · 짐작 | 옛 건설 9-1 인력운반 2,300㎏/㎥ — 산림 10-6 인력운반(짐작) | 후보 GF000162 ✗ | +| Y026 | 야면석메붙임 · 뒷길이 35Cm [M2] ×1 | GF000224 · GC000262 | 건설 7-2-1 석공 0.13 = GC000262 · 씨앗 = GF000224 | 같음 GF000224 ✓ | +| Y027 | 측구수로관부설 · 500∼700kg미만 [m] ×1 | GC000251 | 공통 6-7-1 플륨관 특별 · 보통 + 트럭크레인 = GC000251 플륨관 설치(1위 PC관 = 토목 6-7-1) | 후보 GC000403 ✗ | +| Y028 | 콘크리트타설 · 장비사용타설(소형) [M3] ×1 | GC000198 · 허용 GF000155 | 6-1-1 장비사용(소형) — GC000198 · 1위 GF000166 은 산림 12-37 다른 품 | 후보 GF000166 5안 | +| Y029 | 합판거푸집(4회) · 보통 [M2] ×1 | GC000211 · 허용 GF000160 | 건설 6-3-1 4회 보통 | 후보 GF000160 △(허용) | +| Y030 | 혼합골재부설 · 영월-산불 [M3] ×1 | GF000136 · 짐작 | 골재장 → 현장 덤프 운반(옛 9-7) = 산림 10-3-3(짐작) | 후보 GF000136 ✓ | +| Y031 | 콘크리트포장(인력) · T=20㎝ [M3] ×1 | GF000162 · 허용 GC000297 | 임도품셈 7-1-1(옛) · 씨앗 같음 | 같음 GF000162 ✓ | +| Y032 | 콘크리트포장절단공 · 임도 [M] ×1 | GF000163 | 임도품셈 7-1-2(옛) · 씨앗 같음 | 같음 GF000163 ✓ | +| Y033 | 뒷채움돌채집운반부설 · 4.5톤 덤프트럭(L=500m) [M3] ×2 | GF000218 | 씨앗 같음 | 같음 GF000218 ✓ | +| Y034 | 큰돌메쌓기 · 뒷길이 0.6~0.8m [M2] ×2 | GF000226 | 사방품셈 P52 · 씨앗 같음 | 같음 GF000226 ✓ | +| Y035 | 큰돌채집운반(뒷길이 0.6~0.8m) · 장비(80%) + 인력(20%) [M2] ×2 | GF000213 | 큰돌 채집 장비 80 + 인력 20 | 후보 GF000213 ✓ | + +## 부록 E. 스크립트 · 결과 파일 + +폴더 = 스크래치패드 `req225/C1/`. 모두 읽기만(서버 · DB · storage · 저장소 안 씀 · 함수 바꿔 끼움은 같은 프로세스 안). + +- `s01_extract.py` — 두 xls 를 `M02_Hopyo_Excel.read_book` 으로 읽어 줄마다 `match_line`(기억 없음) → `out_lines.json`(616 줄 · 후보 · 점수 · 단산 블록 글) · 우리 로직 표 `out_ours.json`. +- `s02_view.py` · `s03_find.py` — 고유 로직 줄 보기(`out_items.json`) · 우리 로직 찾기. +- `labels.py` · `labels_y.py` — 정답 표시(부록 C 원천) · `cats.py` — 실패 갈래. +- `s04_eval.py` · `s04b_diff.py` — 지표 · ①②④⑥ 흉내 · 줄별 바뀜. +- `s05_more.py` · `s05b_diff.py` — 책 맞춤 · ⑤⑦⑧⑪ · 금액 진단(`out_money.json`). +- `s06_memory.py` — ⑨ 기억 재사용 · 점수 문턱. +- `s09_stmate.py` — 영월 STmate 읽기(흉내) → `out_stmate.json`. +- `s10_all.py` · `s10b_diff.py` — 세 파일 지표 · ⑫⑬⑭ · 씨앗 끔 → `out_all.json`. +- `s11_buttons.py` — 단추 정밀 · `s12_units.py` — 단위 별칭 · `s13_money_flag.py` — 금액 확인 표시 정밀. +- `s14_fails_all.py` · `s15_cats.py` — 실패 목록 · 갈래 표(`out_fails_all.json` · `out_cats.json`). +- `s16_guard.py` — 작은 고침 묶음 · ⑪ 판 지킴 → `out_guard.json` · `s18_name_all.py` — 이름 쪽 방법 세 파일 → `out_name_all.json`. +- `s17_report.py` — 이 글. +- 실행 = 저장소 뿌리 `venv/Scripts/python.exe -B <스크립트>`(스크래치패드 폴더에서 · 각 120 초 안). + +## 부록 F. 정의 · 한계 + +- 엄격 = 엑셀 블록 출처 조항 · 직종 · 품과 같은 우리 로직(복합이면 주된 몫 · 여럿이면 어느 하나). 느슨 = 엄격 + 같은 일 다른 책 · 복합의 곁 몫. 「우리에 없음」은 분모에서 빼고 따로 셈. +- 짐작 표시 31 줄(출처 없음 · 부분 덮음) — 고임돌 · 조약돌 = 산림 13-2-1 인력 채집 몫 · 소운반 = 산림 10-6 인력운반 · 철골설치 = 철골세우기 장비 손료 등은 사람 판단. +- 사전(②)은 임도 파일에서만 뽑아 사방 · 영월을 처음 보는 셈으로 잼 — 같은 파일 점수는 부풀어 있음. +- 금액(⑧)은 자동값 + 규격에서 읽은 입력값으로 우리 로직을 서버 엔진 그대로 셈(`M02_Labor_Doc.run_line`) · 엑셀 노임(2026)과 우리 노임 차이 몇 % 섞임. +- 영월은 지금 화면이 못 올리는 꼴 — 지표는 「읽기를 붙였다고 가정」 · 노임 코드 → 직종 이름이 덜 이어져 근거 직종이 거의 빔. diff --git a/docs/raw/조사/2026-10-15_336_부록2_바깥방법.md b/docs/raw/조사/2026-10-15_336_부록2_바깥방법.md new file mode 100644 index 000000000..17718a0a7 --- /dev/null +++ b/docs/raw/조사/2026-10-15_336_부록2_바깥방법.md @@ -0,0 +1,696 @@ +# C2 · 일위대가 이름 맞춤 — 다른 곳은 어떻게 푸나 (상세 검토 · 코드 고침 없음) + +조사일 2026-10-15 · 요구 225(10-15) 「외부에서 일위대가 가져오기 기능에서 이름이 달라 매칭이 안되는 문제 개선 방법」 · 저장소 읽기만 · 웹 출처 = 실제로 연 페이지만 · 확인 못 한 것 = 「확인 못 함」 · 우리 자료 실측은 다른 조사자 몫(여기 수치는 바깥 자료 · 코드 읽기 · 예시 계산). +근거 꼴 = 저장소 `경로:줄`(지식DB 는 `resources/knowledge/` 아래 경로 — `original/` · `technical_info/` 로 시작 · 코드 · 마스터 자료는 저장소 뿌리 기준) · 실무 엑셀 `시트!R행(C열)` · 웹 = URL. + +## 0. 한눈 결론 + +1. **이름 자동 맞춤으로 푼 곳은 없음.** 시중 적산 프로그램은 자원 줄(노임 · 자재 · 중기 · 경비)은 코드로, 일위대가 · 단가산출 머리는 사람이 검색해 고름 — 다산 공지 「일위대가, 단가산출은 '코드일괄변경'이 적용되지 않으므로 … 검색후 [코드변경]」(2-1). STmate 도 정확 일치 열쇠를 5 단으로 내려갈 뿐 퍼지 · 학습 없음(2-3). 조달청 의뢰 연구(2020)는 「표준 공사코드와 내역항목의 일치 … 방법론은 구축되어 있지 않은 상태」(2-2). 국내 논문(2020)은 글자 유사도 1순위 정답률 50 % 아래 → 「후보 몇 개 + 전문가 고름」 을 권함(3-3). 우리 「후보 + 사용자 고름 + 층 기억」 은 이 흐름과 같음. +2. **코드로 잇기는 보조까지.** 실무 엑셀 6 개(임도 4 · 사방 2)에 국가 표준 코드 칸 0(1-0). 실무 STC 에서도 국가 호환코드가 일위대가 머리 0/216 · 단가산출 0/371(2-3). 쓸 수 있는 「코드」 = 단가산출 글 속 품셈 조항 번호뿐 — 판 · 부문이 빠지고 서식이 제각각. 번호는 판마다 바뀜(산림 판마다 전부 · 건설 해마다 끼워 넣기로 밀림 · 4-다). 지금 근거 길(조항 대조)에 판 짐작 · 부문 넣기 · 옛 번호 바꿈표가 붙어야 힘을 냄. +3. **예시 짝은 원문이 직접 이어 줌.** 건설 3-2-1 [주]① 「깊이 1m이하의 인력에 의한 구조물 터파기 또는 흙깎기 등에 적용」(`original/원가계산/건설공사_표준품셈/본문/01_공통부문/제03장_토공사/3-02_굴착.md:30`) · 실무 출처 칸 「[건설공사표준품셈 3-2-1 인력굴착]」(`original/실무문서/2026_실무일위대가 모음/2026년_북부지방산림청_임도사업_설계내역서(공통단가).xls` 단가산출!R4229C2 「93.구조물터파기(인력100%)(토사)[㎥]」 · R4231C2) · 조달청 세부공종코드 목록에 옛 「인력터파기」(ABB111100000)와 새 「굴착(인력/토사)」(ABB11100010S)이 같은 코드 묶음(1-4). +4. **코드 읽기로 본 벽(실측 아님).** 지금 맞춤은 괄호 글을 지우고 뜻 낱말이 하나도 안 겹치면 후보에서 뺌 → 「구조물터파기(인력100%) 토사」 ↔ 「굴착(인력/토사)」 은 이름 길에서 빠지고, 근거 길도 줄 직종이 보통인부뿐이면 같은 낱말 조건에 걸림(3-0). 같은 뜻 말 한 줄(터파기 ↔ 굴착)이면 넘음. 예시 계산에서 글자 기법은 모두 오답(이름 비슷한 기계 터파기)을 더 높게 봄(3-1). +5. **가장 값싼 길 = 이미 있는 그릇.** M01 로직 줄에 「별칭」 칸(화면 · 층 · 쉼표로 여럿)이 이미 있고 M01 찾기는 씀. 마스터 1,469 줄은 모두 비어 있고 M02 맞춤은 안 읽음(3-0). 별칭을 맞춤에 읽히고, 씨앗 메모 170 · 품셈 [주] 다리 문구 · 조달청 옛/새 이름 · 실무 짝 20 · 사용자 확정 짝으로 채우는 길. +6. **바로 쓸 만함(새 의존성 없음)**: 별칭 · 같은 뜻 말/쪼개기 표 · 표기 고르기(깎/깍 · ㎥/m3 · 굴삭기/굴착기/백호우 · 브레이카) · 괄호 글을 칸(방법 · 토질 · 규격)으로 · 칸 어긋남 규칙 · 구성 비교(직종 · 장비 · 자재 · 「보통인부뿐」) · 확정 짝 → 별칭 후보 · 정확 일치 열쇠 단계 내리기 · 글자 2~3 그램 TF-IDF(5). +7. **조건부**: 조항 번호 바꿈표(산림 14 건 씨앗 · 건설 2025 → 2026 한 단계) · 조달청 세부공종코드 CSV(33,491 행 · 무료 · 사전 재료로는 바로) · C3R XML · STC 직접 읽기 · 조달 8 자리 자재 번호 · 금액 대조 · Jaro-Winkler/자모 · 형태소 분석 · 문장 임베딩(수백 MB) · 능동 학습(5). +8. **안 맞음**: 표준시장단가 · 건설정보분류체계 · 물가지 코드 · 일위대가 머리 국가 코드 · STmate/다산 내부 코드(프로젝트 순번) · 암 이름을 같은 뜻 말로 묶기 · 큰 언어 모델(오프라인 · 외부 전송) · 신경망 분류 학습(자료 적음)(5). + +--- + +## 1. 국내 표준 코드 · 분류 — 이름 대신 코드로 이을 수 있나 + +### 1-0. 한눈 요약 + +| # | 대상 | 코드 꼴 | 실무 엑셀(임도 · 사방 6개)에 있나 | 이름 대신 코드로 잇기 | 판정 | +|---|---|---|---|---|---| +| 1 | 건설공사 표준품셈 | 부문 → 장 → 절 `N-N` → 항목 `N-N-N` · 해마다 개정 | 있음 — 단가산출 글 속 자유 서식(판 · 부문 빠짐) | 번호만으로는 못 함(판 · 부문이 있어야 유일) | 조건부 | +| 2 | 표준시장단가 | `AA310.01010` (영문 2 + 숫자 3 + 점 + 숫자 5) · 반기 | 없음(0건) | 임도 · 사방 일위대가엔 해당 없음 | 안 맞음 | +| 3 | 건설정보분류체계(공종분류) | 고시(2009 적용기준) · 자릿수 확인 못 함 | 없음 | 못 함 — 실무에서 안 씀 | 안 맞음 | +| 4 | 조달청 공사원가호환규정(C3R) · 표준공사코드 | 세부공종코드 12자(첫 글자 = 공사구분) · 33,491행 공개 CSV | 없음(0건) — STmate 산출 xlsx 숨은 칸엔 프로그램 자체 코드뿐 | 엑셀 입력엔 못 함 · XML 입력이면 열쇠 칸 있음 | 조건부(사전 재료로는 바로 쓸 만함) | +| 5 | 산림사업 표준품셈 | 산림청고시 제2025-82호(2026-01-01 시행) · 장-절 `5-12` | 책 이름만(「임도품셈」「산림사업품셈」) · 조항 번호 거의 없음 | 못 함 — 판이 통째로 바뀜 | 조건부(항목 제목 = 기준말로는 바로 쓸 만함) | +| 6 | 한국물가정보 · 물가자료 | KPI 분류 CATE_CD 4 · 8자리 보임 · 품목 코드 비공개(유료) | 없음 — 「쪽 번호」만 | 못 함 | 안 맞음 | +| 7 | 조달청 물품목록(물품분류 8자리 · 식별번호 8자리) · 나라장터 가격 | 물품식별번호 8자리 | 일부 — 조달청 가격 쓴 자재 줄에 8자리 번호 | 그 자재 줄만 가능 | 조건부 | + +핵심: 실무 엑셀 6개(임도 4 · 사방 2)에 **국가 표준 코드 칸은 하나도 없음**. 들어 있는 것은 ① 단가산출 글 속 품셈 조항 번호(서식 제각각) ② 조달청 가격 자재 줄의 8자리 식별번호 ③ STmate 가 산출한 xlsx 셀 안 숨은 프로그램 자체 코드뿐임. 따라서 「코드로 잇기」는 엑셀 입력에서는 보조 열쇠(후보 거르기)까지이고, 대신 공개 목록(조달청 세부공종코드 · 품셈 항목 제목)은 **같은 뜻 말 · 정규 표기 사전의 재료**로 쓸 만함. + +### 1-실측. 실무 엑셀에 코드가 들어 있나 + +훑은 파일(저장소 `original/실무문서/` 아래, 스크립트 = scratchpad `req225/p1_scan_codes.py`): + +| 파일 | 품셈 조항 `N-N-N` | 표준시장단가 · 조달청 세부공종코드 꼴 | 「코드」 머리칸 | 8자리 숫자 | +|---|---:|---:|---:|---:| +| `2026_실무일위대가 모음/2026년_북부지방산림청_임도사업_설계내역서(공통단가).xls` | 92 | 0 | 0 | 14(자재단가 시트) | +| `2026_실무일위대가 모음/2026년_북부지방산림청_사방사업(계류+댐)_설계내역서.xls` | 61(레미콘 규격 25-24-12 섞임) | 0 | 0 | 4(품명 괄호) | +| `2024년 간선임도(기번3-울진.대흥)(공통)/1. 설계원본/01 내역자료/2024년 간선임도사업(기번3).xlsx` (STmate 산출) | 39 | 0 | 9(안내문 · 숨은 메타) | — | +| `20250527 2025년 계류보전사업(…)(변경)/01.내역자료/2025년 계류보전사업(…)(변경).xlsx` (STmate 산출) | 17 | 0 | 7(안내문 · 숨은 메타) | — | +| `01.산불거창(거창북상병곡)/01.내역서/00.25년_산불_거창(…)내역+수량.xlsx` | — | 0 | — | — | +| `05. 동부청 영월/내역서/2024년 임도시설사업2차(산불진화_기번6)-영월.xlsx` | — | 0 | — | — | + +관찰 예: +- 품셈 근거가 글 속에 제각각 — `단가산출!R242C2` 「[건설표준품셈(8-2-8)] 덤프트럭 적용」 · `R277C2` 「되메우기및 다짐 [건설공사표준품셈 표준품셈3-4-7]대형장비」 · `R582C2` 「건설공사표준품셈 2-2-3 콘테이너형 가설건축물」 · `R10C2` 「[건설표준품셈 준용]」(번호 없음) — 2026 북부청 임도. +- 같은 문서 안 옛 번호 · 새 번호 공존 — 2025 계류보전 `단가산출근거!R7C2` 「건설표준품셈 9-3 굴삭기 적용」 ↔ `R180C2` 「건설표준품셈 8-2-3(굴삭기) , 8-2-8(덤프트럭) 적용」. 2026판에서 8-2-3 = 「굴착기」(`original/원가계산/건설공사_표준품셈/본문/01_공통부문/제08장_건설기계/8-02-03_굴착기.md` 16줄), 9-3 = 토목부문 「지형 및 토지측량」(`…/02_토목부문/제09장_측량/9-03_지형_및_토지측량.md` 3줄) → 부문 · 판 없이 번호만 대조하면 엉뚱한 항목에 붙음. 이름도 「굴삭기」 ↔ 「굴착기」. +- 일위대가표 첫 칸에 조항 번호 — STmate 산출 xlsx `일위대가표!R7C2` 「6-1-4(모르타르)」 · `R44C2` 「7-1-2(찰쌓기)」 · `R87C1` 「임도표준품셈」(2024 간선임도). +- 산림 품셈은 책 이름만 — 2026 북부청 임도 `내역서!R81C4` 「임도품셈, 발생석활용 50%적용」(규격 칸) · 사방 `내역서!R22C4` 「산림사업품셈」. +- 호표 번호는 문서 안 차례일 뿐 — `일위대가!R5C2` 「제1호표」. +- STmate 산출 xlsx 셀 안 숨은 메타 — `중기목록표!R27C10` 「`COD|X00102 … EQC|03060201050`」 · `노무비목록표!R14C7` 「`COD|L00033 … BQC|01000000007`」(2024 간선임도). M · L · S · X · W + 5자리 = 프로그램 자체 코드(국가 코드 아님 · 필드 뜻은 `original/원가계산/STmate/STC_필드사전_핵심.md` 57 · 61 · 90 · 91줄 — EQCODE「중기 코드」, BQCODE「단가 적용월」 ⚠). + +### 1-1. 건설공사 표준품셈 (국토교통부 공고 · 한국건설기술연구원 관리) + +- 무엇: 「건설공사의 일반ㆍ보편적 공종에서 단위작업 당 투입되는 인력, 장비 등을 수치화한 자료」(뉴스서울 2026 공고 기사). +- 코드 체계: 부문(공통 · 토목 · 건축 · 기계설비 · 유지관리) → 장(부문마다 제1장부터) → 절 `N-N` → 항목 `N-N-N`. 2026판 = 5부문 · 45장(공통 8 · 토목 9 · 건축 11 · 기계설비 13 · 유지관리 4)(`original/원가계산/건설공사_표준품셈/_meta.md` 19줄). 항목 제목 끝에 개정 연도 꼬리표 — 「3-2-1 굴착(인력/토사)('08, '20, 25년 보완)」(`…/제03장_토공사/3-02_굴착.md` 18줄). +- 갱신 주기: 해마다 12월 말 다음 해 판 공고 · 1월 1일 적용(`_meta.md` 6줄). 2026판 = 「총 1459개 항목 중 349개 항목(공통 254, 토목 28, 건축 30, 설비 24, 유지관리 13)을 개정」(조선비즈 2025-12-22). 항목 수가 해마다 크게 다름 — 2019 2,317개(산업일보 2019-01-02) · 2025판 1,438개(뉴스서울 2024-12-26) · 2026판 1,459개. +- 번호가 바뀌나: 바뀜. + - 2019 개편 — 「토목·건축·기계설비 부문에 중복 분류돼 있던 토공사, 철근콘크리트공사 등의 주요공종 단일화를 통해 … 표준품셈 체계개편을 추진」(산업일보 2019-01-02) → 같은 일이 부문 · 장을 옮김. + - 2026판 토공사 절 사이에 '25년 신설 항목이 끼어 있음 — 3-2-3 흙깎기(기계) · 3-2-4 터파기(기계) · 3-4-1 흙쌓기 · 3-4-3 흙 다지기 · 3-4-6 · 3-4-7 되메우기 및 다짐(`3-02_굴착.md` 126줄 · `3-04_쌓기.md` 152줄). 2026판 본문 항목 제목 1,095개 중 「'25년 신설」 27개(곧은 · 굽은 따옴표 모두 · 6-1 에서 다시 셈). 끼어든 자리 뒤 항목은 옛 판과 번호가 다를 수 있음 — 옛 판 원문 대조는 확인 못 함. + - 실무 실측 — 위 「9-3 굴삭기」 ↔ 「8-2-3 굴착기」. + - 신구 번호 대응표: 공개처에서는 못 찾음. 저장소 `2026년_건설공사_표준품셈_개정사항.pdf` 의 현행 ↔ 개정 대조에서 2025 → 2026 한 단계 번호 바뀜은 뽑힘(4-다-2 · 6-1). +- 공식 공개처: 「국토교통부 누리집 또는 한국건설기술연구원 공사비원가관리센터 누리집에서 열람 가능」(조선비즈 2025-12-22). CODIL(codil.or.kr)은 인증서 오류로 이번에 열지 못함(저장소 `_meta.md` 5줄도 같은 문제 기록). +- 실무 엑셀에 있나: 있음 — 단가산출(근거) 글 속, 일위대가표 첫 칸. 다만 판(연도) · 부문을 거의 안 적고, 괄호 · 띄어쓰기 · 책 이름(「건설표준품셈」「건설공사표준품셈」「건설공사건설표준품셈」)이 제각각. +- 이름 대신 코드로 잇기: 번호 하나로는 못 함 — 「판 + 부문 + 번호」가 있어야 유일. 실무 글엔 판 · 부문이 빠지는 일이 흔해 확정 열쇠로는 약함. 쓸 길 = ① 문서 작성 연도로 판을 짐작해 후보를 같은 절로 좁힘 ② 번호 → 그 판의 항목 제목으로 바꿔 이름 맞춤의 기준말로 씀. +- 우리에 맞나: 지금 쓰는 「단가산출 출처 조항 대조」가 바로 이것 — 판 짐작 · 부문 짐작 · 옛 번호 대응이 붙어야 힘을 냄. 우리 이름 「굴착(인력/토사)」은 2026판 3-2-1 제목 그대로임. +- 판정: **조건부** — 후보 거르기 열쇠로는 쓸 만함, 확정 열쇠로는 판 · 부문 짐작과 옛 번호 대응표(직접 만들어야 함)가 붙어야 함. + +### 1-2. 표준시장단가 (국토교통부 공고 · 한국건설기술연구원) + +- 무엇: 「이미 수행한 공사의 시장거래가격을 토대로 산정된 표준단가(100억원 이상 공사에 활용)」(뉴스서울 2026 공고 기사). 2025 적용분 「표준시장단가 1,832개 중 414개 항목(토목 236, 건축 101, 기계설비 77)」 현장조사 반영(뉴스서울 2024-12-26). +- 코드 체계: 영문 2자 + 숫자 3자리 + 점 + 숫자 5자리 — `AA310.01010` · `AA323.10500` · `LH340.02000` · `FB002.01000` · `EE001.20000`(2024 상반기 신구대비표 PDF 사본에서 뽑음). 반기 공고. 신구대비표에 같은 코드로 규격 · 단가가 바뀐 줄 다수(뽑은 글에서 양쪽 코드가 같은 줄 257줄 — 조판 탓에 코드 자체 변경 여부는 판정 못 함). +- 품셈과 잇는 표: 공개 자료에서 확인 못 함. +- 실무 엑셀에 있나: 없음(6개 파일 0건). +- 이름 대신 코드로 잇기: 임도 · 사방 일위대가는 품셈 기반이라 해당 없음. +- 덤 관찰: 조달청 세부공종코드가 이 코드를 그대로 품음 — 「공사구분 글자 + 표준시장단가 코드(점 뺌) + 0」. 대조 7건 모두 맞음: `AA323.10500` → `AAA323105000`(건축) · `CAA323105000`(토목) 「시스템동바리」 · `AA310.02010` → `CAA310020100` 「시스템비계」 · `LH340.02000` → `CLH340020000` 「가드휀스」 · `EE001.20000` → `CEE001200000` 「철근가공조립/현장가공」 · `AC210.10000` → `AAC210100000` 「임대료/타워크레인」 · `FB002.01000` → `CFB002010000`. (호환규정 XSD 의 실적공사비코드 꼴 `[ACEMT]+…` 와도 맞음 — 4번.) +- 우리에 맞나: 임도 · 사방 공사는 보통 100억 미만이라 대상 밖(개별 공사 규모는 따로 확인). +- 판정: **안 맞음**. + +### 1-3. 건설정보분류체계(공종분류 등) + +- 무엇: 건설 정보를 분류 축별로 나눈 국가 분류체계. 2015 논문 참고문헌에 「Construction Information Classification Application Standard, Ministry of Land, Transport and Maritime Affairs, 2009.」(한국산학기술학회논문지 2015). 고시 원문 · 번호 · 공종 코드 자릿수는 확인 못 함(법제처 한글주소 오류 페이지). 시설물 · 공간 · 부위 · 공종 · 자원 축 구성은 검색 요약에만 보였고 연 페이지에선 확인 못 함. +- 실무 활용: 「건설정보 분류체계가 고시된 후 수년이 경과되었으나, 아직까지 현업에서의 활용은 극히 저조한 상태」(한국정보처리학회 2012). 「이에 대한 활용비중이 낮으며」(한국산학기술학회논문지 2014). 1998년에도 건설기술연구원 표준분류체계와 건설교통부 실적공사비 분류체계가 「서로 상이」하다고 지적(건설산업연구원 1998). +- 실무 엑셀에 있나: 없음 — 공종분류 코드 칸이 어느 파일에도 없음. +- 이름 대신 코드로 잇기: 못 함. +- 판정: **안 맞음** — 실무 문서에 코드가 안 실리고, 분류 자체가 개념 수준. + +### 1-4. 조달청 공사원가호환규정(C3R) · 표준공사코드 + +- 무엇: 「공사원가호환규정은 … XML … 문서로 작성」 · 「공사원가호환 데이터 파일의 작성자는 반드시 XML스키마를 통해 공사원가호환규정의 유효성을 검토하여 유효한 파일을 작성」 · 호환규정검증시스템 = 「설계내역 전산파일이 조달청 공사원가호환규정에 따라 적합하게 작성되었는지 검증하는 서비스」 · 「조달청 공사원가호환규정 및 코드에 따라 작성된XML파일만 등록이 가능」(서울시 「XML 안내」 수정일 2026-04-03). +- XSD 칸(저장소 사본 = V1.0.5 · 2022-11-28, `original/경쟁사 프로그램/STmate 분석/30_원자료/조달청_공사원가호환규정.xsd`): + - 판 이력 — 「표준시장단가 추가 … 1.0.3 (2015-07-14)」 · 「내역공종코드형식 max length 변경(16->20) (2017-04-19)」(4~11줄). + - 일위대가 — `일위대가코드` = 세부공종코드형식(12자 · 영숫자)(708 · 1244~1249줄) · 품명 · 규격 · 단위 · 일위대가상세(728줄). + - 일위대가상세 — `자원구분`(자재 · 노무 · 경비항목 · 일위대가 · 중기단가산출 · 기계경비 · 실적공사비 · 시장시공가격 · 손료 · 표준시장단가)(642~657줄) · `자원코드`(17자)(658 · 1279~1283줄) · `노임산출근거` → `공량품셈근거`(자유 글)(632~638 · 701줄). + - 세부공종 — `세부공종코드`(17자)(947 · 1224~1228줄) · `WBS코드1` · `WBS코드2`(1057~1058줄). + - 실적공사비코드 — 12자 · `[ACEMT]+[0-9A-Z]*`(621 · 1237~1242줄). 자재코드 · 노무코드 16자(1258~1269줄). 노무코드 주석 「`[L]{1}[A-Z0-9]+` 표준노무코드 적용시」(1109줄). + - 저장소 정리: `…/20_분석/30_조달청_호환규정_XSD.md` 28줄 「규정 코드는 반기마다 갱신」 · `30_원자료/호환규정코드_안내.txt` 1~3줄 「호환규정코드 2026년 9월 1일 조달청 발표분 반영 … https://npccs.g2b.go.kr 발표」. npccs 사이트는 이번에 본문이 비어 열람 못 함(스크립트로 그리는 화면). +- 표준공사코드: 「공사원가 산정업무의 효율성과 가격의 객관성을 확보하기 위해 '표준공사코드' 체계를 운영 중」 · 「시설자재, 노무비, 기계경비, 세부공종 등에 대해 수요기관이 공사내역 작성 시 활용할 수 있도록 표준공사코드를 제공」(공공데이터포털 15151273 메타). 파일 「조달청_공사원가_세부공종코드_20250911」 — 칸 = 공사구분 · 세부공종코드(최대 16) · 품명 · 규격 · 단위 · 33,491행 · 등록 2025-11-05 · 「수시 (1회성 데이터)」(공공데이터포털 파일 페이지). 무료 · 로그인 없이 내려받음. +- 내려받아 훑은 결과(scratchpad `req225/p1dl_datagokr/seb.csv` · CP949 · 2.3MB): + - 공사구분 6 — 건축 17,551 · 토목 6,771 · 설비 5,556 · 통신 1,794 · 전기 1,150 · 문화재수리 669. + - 코드 12자 33,475행 · 첫 글자 = 공사구분(A 건축 · C 토목 · M 설비 · T 통신 · E 전기 · R 문화재) · 고유 품명 6,071 · 품명+규격 같은 쌍이 2번 넘게 실린 것 1,296(건축 · 토목 겹침 등). + - 끝 글자 S 코드 10,043 — 품명이 품셈 항목 꼴: `ABB11100010S` 「굴착(인력/토사) / 보통토사, 깊이 1m 이하」 · `CDE10010000S` 「굴착(인력/토사) / 보통토사, H=1m 이하」 · `CRB11000035S` 「돌쌓기 / 메쌓기 / 뒷길이 35cm」 · `CDA21000000S` 「벌개제근 / 벌목」. S 의 공식 뜻은 확인 못 함 · 코드에 품셈 조항 번호가 담기지는 않음(관찰). + - 옛 이름 · 새 이름이 이웃 코드로 함께 실림 — `ABB111100000` 「인력터파기 / 보통토사,0∼1m」 ↔ `ABB11100010S` 「굴착(인력/토사) / 보통토사, 깊이 1m 이하」(앞 6자 `ABB111` 같음) · `CDA210100100` 「벌목 / 5m미만」 ↔ `CDA21000000S` 「벌개제근 / 벌목 / H=5.0m 미만」 · `CRB101035000` 「돌쌓기/메쌓기」 ↔ `CRB11000035S` 「돌쌓기 / 메쌓기」. 코드 앞부분이 같은 묶음 = 같은 뜻 말 후보(관찰 3건 — 전체 규칙인지는 확인 못 함). + - 임도 · 사방 고유 공종 없음 — 품명에 「임도」「사방」「골막이」「기슭막이」「바닥막이」「떼붙임」「씨뿌리기」 0건. 일반 토목은 있음 — 돌붙임 20 · 메쌓기 7 · 찰쌓기 7 · 파형강관 85 · 측구 53 · 벌개 6 · 굴착 72 · 터파기 211. 「구조물터파기」 0건 · 「인력터파기」 36건. + - 예: 「조달청은 정부 전기공사의 2,300여 주요 공종」 일위대가표를 코드화해 공개한 적 있음(산업일보 2012-07-27) — 일위대가 코드화 선례. +- 실무 엑셀에 있나: 없음 — 세부공종코드 · 표준시장단가 꼴 0건(6개 파일). STmate 산출 xlsx 숨은 칸 코드는 프로그램 자체 코드. +- 이름 대신 코드로 잇기: 엑셀 입력엔 못 함. C3R XML 을 받는다면 `일위대가코드` · `자원코드` · `공량품셈근거` 칸이 이름보다 단단한 열쇠 — 다만 실제 납품 XML 에 표준공사코드가 실리는지는 확인 못 함(저장소에 C3R XML 견본 없음). +- 우리에 맞나: + - 사전 재료로 맞음 — 무료 · 오프라인 CSV 한 벌 · 품명 / 규격 / 단위 정규 표기 + 옛 · 새 이름 묶음 · 품셈 꼴 이름(S 코드). 예: 엑셀 「인력터파기」류 ↔ 우리 「굴착(인력/토사)」를 같은 코드 묶음으로 잇는 근거 후보. + - 빈틈 — 산림 고유 공종이 없음 · 임도 · 사방 실무 이름(「구조물터파기(인력100%)」 등)은 그대로 실려 있지 않음. +- 판정: **조건부** — 「같은 뜻 말 · 정규 표기 사전 재료」로는 바로 쓸 만함(내려받아 서버에 두면 됨), 「코드로 잇기」는 C3R XML 입력을 받을 때만. + +### 1-5. 산림사업 표준품셈 (산림청) + +- 무엇 · 판: 산림청고시 제2025-82호 「산림사업 표준품셈」 · 제정 · 발령 2025-11-26 · 시행 2026-01-01 · 현행(법제처 행정규칙 XML). 목적 = 「「산림기술 진흥 및 관리에 관한 법률」제15조 … 에 따른 산림사업의 실시설계 시 적용할 산림사업 표준품셈을 규정」. 재검토기한 = 「매 3년이 되는 시점」. 부칙 경과조치 = 「「임도설치 및 관리 등에 관한 규정」 및 기타 관계 법령에 따라 … 계약이 체결된 경우는 종전의 규정에 따르며」 → 2025년까지 실무 엑셀의 「임도품셈」은 다른 문서(종전 규정)를 가리킴. +- 앞선 판: 「산림청, '산림사업 표준품셈' 책자 첫 발간」(데일리안 2018-05-17) — 「제1편 기술부문에 설계와 감리」 · 「제2편에는 기능부문으로 산림경영·산림보호·산림공학」 · 「산림공학분야는 사방·임도·숲길로 편성」 · 「산림청 누리집(통합자료실 전자북)에서 누구나 내려 받을 수 있다」. +- 코드 체계: 2026 고시 별표 = 14장 · 번호 꼴 장-절(예: 「5-12 떼붙임(재배잔디)」 · 「5-18 씨뿌리기(줄)」)(저장소 `original/원가계산/산림_표준품셈/_meta.md` 7 · 8 · 16줄 · `본문/` 폴더 이름). 2018 책자는 사업별 편(산림공학 → 사방 · 임도 · 숲길), 2026 고시는 공종별 장(제9장 토공 · 제13장 구조물 …) → 판 사이 번호 연속성 없음. 연도별 대응은 저장소 쪽(다른 포크)에서 확인. +- 실무 엑셀에 있나: 책 이름만 — 「임도품셈」「산림사업품셈」「임도표준품셈」, 조항 번호 거의 없음(위 실측). +- 이름 대신 코드로 잇기: 못 함. +- 우리에 맞나: 우리 일위대가 로직의 1차 근거라 「항목 제목 = 기준말」로 최우선 재료. 옛 「임도 품셈」 ↔ 2026 고시 대응이 있어야 2025년 이전 실무 엑셀과 이음. +- 판정: **조건부** — 항목 제목을 기준말 사전으로 쓰는 것은 바로 쓸 만함, 번호로 잇기는 판 대응표가 붙어야 함. + +### 1-6. 한국물가정보(KPI) · 물가자료(한국물가협회) 자재 코드 + +- 한국물가정보: 첫 화면 분류 링크에 `CATE_CD=1010` 공통자재 · `1015` 토목자재 · `1018` 조경자재 · `1020` 건축자재 …(4자리) · `CATE_CD=10100615`(8자리) 같은 계층 코드가 보임(kpi.or.kr). 데이터 연동 = 「한국물가정보 데이터제공서비스(KPI-Data Provide Service) — 물가정보 데이터를 고객사의 시스템과 연동하여 … 제공하는 서비스」 · 메뉴 이름 「DPS(DB판매)」(유료). 품목 단위 코드의 공개 여부 · 자릿수는 확인 못 함. +- 한국물가협회(물가자료): 공식 페이지를 찾지 못함 — 코드 체계 확인 못 함. +- 실무 엑셀에 있나: 없음 — 2026 북부청 임도 `자재단가!R3` 머리 = 품명 · 규격 · 단위 · 가격정보(단가 · page) · 물가자료(단가 · page) · 유통물가(단가 · page) · 물가정보(단가 · page) · 거래가격. 물가지는 쪽 번호로만 가리킴(예 `R29` 다이아몬드톱날 — 물가자료 1414쪽 · 물가정보 1080쪽). 다산 ESTX 도 같은 6출처 슬롯(`original/원가계산/다산소프트/ESTX_데이터관계.md` 62줄). +- 이름 대신 코드로 잇기: 못 함 — 쪽 번호는 월호마다 바뀌고 코드는 유료 · 비공개. +- 판정: **안 맞음**. + +### 1-7. 조달청 물품목록정보 · 나라장터 시설자재 가격 + +- 코드 체계: 물품분류번호 = 8자리 숫자 · 4단계 계층 · 세부품명번호 = 최대 10자리 · 물품식별번호 = 최대 8자리(공공데이터포털 15131726 칸 정의). 분류 바탕 = UNSPSC — 「2003년 … UNSPSC … 를 정부물품 분류체계로 도입」 · 2024-10 「21년만에 전면 개편」 · 물품분류(품명) 9,900개 · 품목 등록정보 512만개(보안뉴스 2024-10-14). +- 가격: 나라장터 가격정보현황서비스 API(`apis.data.go.kr/1230000/ao/PriceInfoService`) — 저장소 사본 27,485행 · 칸 `prdctClsfcNo`(8자리) · `prdctIdntNo`(8자리) · `krnPrdctNm` · `unit` · `prce`(`original/원가계산/자재단가/_meta.md` 11 · 17줄 · CSV 1행). 철근 · 레미콘 · 아스콘 없음(같은 파일 17줄). +- 실무 엑셀에 있나: 일부 — 2026 북부청 임도 `자재단가!R30C7` 파형강관 D=800 → 「21280926」 · `R31C7` 파형강관 이음관 → 「21280982」 · `R61C7` 임도안내판 → 「25872137」(가격정보 page 칸) · 사방 `내역서!R188C3` 「계도사인(25872124)」 · `R189C3` 「국가지점번호판(25872138)」(품명 괄호). 꼴은 물품식별번호(8자리)와 같음 — 이 번호들의 실제 식별번호 대조는 확인 못 함(저장소 2026-08-14 CSV 엔 없음). +- 이름 대신 코드로 잇기: 8자리 번호가 보이는 자재 줄만 가능. 물가지 출처 자재 · 노무 · 장비 줄엔 해당 없음. 일위대가 호표 이름 맞춤 자체엔 직접 도움 없음. +- 판정: **조건부** — 자재 줄에서 8자리 번호가 보이면 「번호 먼저, 이름은 그다음」. + +### 1-곁. 곁 메모(다른 절에 쓸 것) + +- 저장소 venv 확인(2026-10-15): `fitz`(PyMuPDF) · `pypdf` · `openpyxl` · `xlrd` 있음, `rapidfuzz` · `kiwipiepy` · `scikit-learn` 없음. +- 이름 차이 실례(사전 거리): 「굴삭기」↔「굴착기」 · 「뒷채움」(실무)↔「뒤채움」(2026판 3-4-4) · 「인력터파기」(조달청 옛 코드)↔「굴착(인력/토사)」(2026판 3-2-1 · 조달청 S 코드 · 우리 이름). + +### 1-출처. 실제로 연 페이지 + +- 산업일보 2019-01-02 「올해 표준시장단가 3.39%↑, 1일부터 적용」 — https://kidd.co.kr/news/206547 +- 조선비즈(다음) 2025-12-22 「국토부, 2026년 적용 건설공사 표준품셈·표준시장단가 공고」 — https://v.daum.net/v/20251222110205258 +- 뉴스서울 「국토교통부, 2026년 적용 건설공사 표준품셈·표준시장단가 공고」 — https://newsseoul.co.kr/news/view/1065583819330007 +- 뉴스서울 2024-12-26 「국토교통부, 2025년 적용 표준시장단가·표준품셈 공고」 — https://www.newsseoul.co.kr/news/view/1065586820132417 +- 2024 상반기 표준시장단가 신구대비표 PDF(블로그 파일 저장소 사본 · 비공식 게시처) — https://files-scs.pstatic.net/2024/01/02/W86QynSheM/3._[공고자료]_2024년도_상반기_건설공사_표준시장단가_신구대비표.pdf +- 공공데이터포털 「조달청_공사원가_세부공종코드」 메타 — https://www.data.go.kr/biz/dcat/metadata/15151273.do +- 공공데이터포털 같은 파일 페이지 — https://www.data.go.kr/data/15151273/fileData.do · 내려받기 https://www.data.go.kr/cmm/cmm/fileDownload.do?atchFileId=FILE_000000003514458&fileDetailSn=1&insertDataPrcus=N +- 서울시 「XML 안내」(C3R) — https://news.seoul.go.kr/gov/archives/202400 +- 산업일보 2012-07-27 「전기공사 설계 내역서 표준화 한다」 — https://kidd.co.kr/news/157668 +- 건설산업연구원 1998 「건설정보 분류 체계의 표준화」 — https://www.cerik.re.kr/report/issue/117 +- 한국정보처리학회 2012 「국내외 건설정보 분류체계 개발 동향에 관한 연구」 — https://koreascience.kr/article/CFKO201221868477879.page +- 한국산학기술학회논문지 2014 「건설정보 분류체계의 BIM 도입을 위한 개선방안」 — https://koreascience.kr/article/JAKO201433150757488.do +- 한국산학기술학회논문지 2015 「건설정보 분류체계의 BIM 수용을 위한 확장목록 개발」 — https://koreascience.kr/article/JAKO201525550548312.do +- 데일리안 2018-05-17 「산림청, '산림사업 표준품셈' 책자 첫 발간」 — https://m.dailian.co.kr/news/view/713952 +- 법제처 행정규칙 「산림사업 표준품셈」 XML — https://www.law.go.kr/DRF/lawService.do?OC=test&target=admrul&ID=2100000268530&type=XML +- 한국물가정보 첫 화면 — https://www.kpi.or.kr · KPI-DPS 안내 — https://www.kpi.or.kr/www/cost/api_service_01.asp?CFG_CD=dps01 +- 공공데이터포털 「조달청_디지털서비스몰 계약상품정보」(물품분류번호 · 세부품명번호 · 물품식별번호 칸 정의) — https://www.data.go.kr/data/15131726/fileData.do +- 보안뉴스 2024-10-14 「조달청, 정부 물품목록제 21년만에 전면 개편」 — http://www.boannews.com/news/articleView.html?idxno=133514 + +열지 못한 곳(출처로 안 씀): 국토교통부 정책자료 페이지(되돌림 반복) · CODIL(인증서 오류) · npccs.g2b.go.kr(본문 빔) · 법제처 한글주소 「건설정보분류체계적용기준」(오류 페이지) · 신한옥 분류체계 PDF(한글 글자층 없음). + +--- + +## 2. 국내 견적 · 적산 프로그램 — 외부 내역 · 일위대가 이름을 어떻게 잇나 + +### 2-0. 요약 + +- 시중 프로그램은 「이름 자동 맞춤」을 공개 설명에 내세우지 않음. 관찰된 관행 = **자원(노임·자재·중기·경비)은 코드로 · 일위대가·단가산출 머리는 사람 검색·선택으로** 잇기. +- 다산 EST: 조달청 표준공사코드 적용 때 노임·자재·표준(실적)단가·기계경비는 [코드일괄변경] 자동, **일위대가·단가산출은 일괄변경 불가** → 공종별 검색 뒤 [코드변경], 없으면 분류 골라 [사용자정의코드]. +- STmate: 기준자료 갱신 매칭 = **정확 일치 키 5단 내리기**(호환규정코드 → 시설코드 → 명칭·규격·단위 → 호환+명+규+단 → 명칭·단위). 퍼지 점수·학습 흔적 없음. 엑셀 재등록은 STmate가 만든 엑셀(전체계산식)만. +- 이 조사 실측(실무 STC 7건): STmate 코드(B00001·L00002 등)는 **프로젝트 안 순번** — 같은 코드가 프로젝트마다 다른 항목. 국가 호환코드(JDCODE) 채움 = 노임 86/97 · 중기 117/176 · 경비 123/166 · 자재 13/318 · **일위대가 0/216 · 단가산출 0/371**. +- 조달청 의뢰 연구(2020): C3R 검증은 코드 사용 여부·자료 구조만 봄 · 「표준 공사코드와 내역항목의 일치 및 수정에 대한 방법론은 구축되어 있지 않은 상태」 · 설계사가 과거 내역서 품명·규격·단위·코드를 복사 · 텍스트 마이닝·딥러닝 자동 맞춤은 장기 과제로만 제안. + +### 2-1. 웹 — 프로그램별 공개 설명 + +| 프로그램(회사) | 무엇(공개 설명 원문) | 어떻게 잇나 | 우리에 맞나 | 출처 | +|---|---|---|---|---| +| STmate (경영정보(주)) | 설계내역 특징 「명칭 및 코드를 간단히 검색」 · 견적내역 「조달청 등의 전산내역입찰서를 작성」 · 계약실행 「전산내역입찰서를 계약내역서로 이용」 · FAQ 「STmate에서 작성된 엑셀파일(전체계산식)을 STmate자료로 다시 등록」 · FAQ 「공사목록표에서 공사원가 호환규정 포맷(XML,조달청)으로 변환」 · 사용설명서는 정회원 전용(확인 못 함) | 코드·이름 검색(사람) · 자기 출력 엑셀 왕복 · 조달청 XML(코드 기반) | 타사 엑셀 이름 맞춤 기능 아님 — 자기 엑셀 왕복용 | W1 · W2 · W3 | +| EST+4 · Total EST+4 (다산소프트) | 「엑셀변환을 지원합니다. (단가산출까지 수식이 연결되어 변환되고, 엑셀로 변환후에는 편집없이 바로 출력이 가능합니다.)」(내보내기) · 「조달청포맷파일(JDL)로 변환을 지원합니다.」 · 품셈검색 · 엑셀 불러오기 설명 없음 | 엑셀→EST = 고객지원 「착공내역서요청」·「기술문의」 게시판에 엑셀 내역서를 붙여 요청 → 「접수(11/12)→완료(11/12)」 꼴 회신. 프로그램 기능인지 사람 대행인지 확인 못 함 | 사람 대행형 — 안 맞음 | W4 · W5 · W6 · W7 · W8 · W9 · W10 | +| 다산 EST 표준공사코드 적용(공지 2012·2022) | 「설계서에 입력된 모든 자료에 표준공사코드를 반드시 적용하여야함」 · 1개라도 누락되면 XML 변환 안 됨 · 메뉴 [도우미→조달청 표준공사코드 적용] | 노임·자재·표준(실적)단가·기계경비 = [코드일괄변경] 자동 · 자동 안 된 것은 가장 유사한 항목 골라 [사용자정의코드] · **일위대가·단가산출 = 「'코드일괄변경'이 적용되지 않으므로」 공종별로 「단산,일위 코드」에서 검색 뒤 [코드변경]** · 검색 안 되면 「공종분류」 탭 분류 선택 뒤 [사용자정의코드] | 일위대가 머리는 사람 확인이라는 우리 처지와 같은 결론 — 관행 근거로 씀 | W11 · W12 · W13 | +| SGS (나스) | 「엑셀(Excel)로 작업된 내역을 SGS내역자료에 맞게 아주 간단히 읽어 들일 수 있는 기능을 업계 최초로 내장」 · 대상 「자재, 노임, 견적.기타, 표준시장단가, 건설기계, 단가산출, 일위대가, 예산내역서, 원가계산서」 · 「선택 영역(열, 행)만 읽어오기, 전 영역(열, 행) 읽어오기」 · 「외부자료 변환기능」 · 「코드 또는 공종으로 해당자원들 … 자동입력」 · 「코드 또한 사용자정의 방식」 · 조달청 XML 읽기/쓰기 | 열·행 지정 읽기 + 코드·공종 기준 자원 입력. 이름 맞춤 방식 설명 없음(확인 못 함) | 열 지정 읽기 층만 같음 · 맞춤 참고 못 됨 | W14 · W15 | +| Easy 2.0 건설 (건설연구원) | 「엑셀불러오기 - 엑셀로 작성한 내역서를 프로그램으로 불러옵니다.」 · C3R 적합 인증 2017-10-19 · 「2026년 6월 1일 현재 재인증 준비중」 | 맞춤 방식 설명 없음(확인 못 함) | 근거 부족 | W16 · W17 | +| 에니윈2000 | 「이 일위대가는 에니윈2000 을 사용하면 자동연결되어 견적됩니다.」 · 자재+일위대가 12만여 개 · 삼사단가 | 같은 회사 자료↔같은 회사 프로그램 사이 연결(맞춤 문제 자체 없음) | 외부 문서 맞춤 아님 — 안 맞음 | W18 | +| 콘엑스 (AI 적산, 웹) | 페이지 구조화 자료 featureList 「AI 기반 일위대가 자동 검증」 · 「표준품셈 실시간 대조」 · 설명 「일위대가를 자동 검증합니다」 | 방법·근거·성능 비공개(확인 못 함) | 오프라인·근거 설명 요구에 맞는지 판단 불가 | W19 | +| RevitQ (명진소프트컨설팅, 2013 기사) | 「수량/산출 코드와 Revit 모델링 데이터 간의 코드 매핑(mapping) 작업」 · 「코드 매핑 작업이 완료된 … 파일을 사용자 라이브러리(Library)로 등록」 · 다른 프로젝트에서 재사용 | 사람이 한 매핑을 라이브러리로 저장·재사용(BIM 쪽) | 「확정 매핑 기억」 발상만 참고 | W20 | + +### 2-2. 웹 — 조달청 생태계 규칙(프로그램이 따르는 열쇠) + +| 시점 | 내용 | 출처 | +|---|---|---| +| 2010-07-01 | 조달청 계약 요청 시설공사 = 호환규정 표준코드 적용 내역서 제출 · 다산 2010-08-02 인증 | W13 | +| 2012-01-01 | 호환규정·코드 준수 XML 제출 · 코드 1개라도 누락이면 XML 변환 안 됨 · C3R 검증 2013-08 나라장터 이관 | W12 | +| 2017-07-01 | 표준 코드 의무 — 「노무비, 표준시장단가, 시장시공가격 -> 매칭율 100%」 · 건축 일위대가 850개 「매칭율 80% 이상」 | W21 | +| 2025-05 · 2025-09-24 | 「올해 5월 가격정보와 산출근거를 포함하여 보다 내실 있는 코드체계로 개편」 · C3R 인증 원가계산 프로그램사 11곳 간담회(이름 미기재) | W22 | + +한국조달연구원 「공사코드 표준화를 통한 유형별 공사비 산정방안 연구」 최종보고서(2020-11, 조달청 제출) — W23 + +| 쪽 | 내용 | +|---|---| +| p.16 | 국도설계실무요령 대공종 ↔ 표준공사코드 대분류 1:1 대응 어려움 · 「대부분 적산프로그램에 의해 산출된 과거 내역서 항목을 그대로 불러와 내역서를 작성하는 문제」 | +| p.17 | 국도31호선 양구-원통 설계내역서 C3R 검증 → 표준공사코드 사용률 27.2% · 표준시장단가·시장시공가격·노무단가는 대부분 준수, 세부공종·자재 코드는 미준수 | +| p.18~19 | 도로공사 44건 세부공종 구성: 중기단가산출 54.79% · 일위대가 4.77% · 자재 6.86% 등 = 표준코드 미사용(임의코드) | +| p.20 | 조달청 자원코드 = UNSPSC 기반 8자리 + 식별번호 8자리 = 16자리 | +| p.27~28 | 업체 임의코드는 「1번의 코드 수집을 통해 자동 변환이 가능」(코드↔코드 대응표) · 「코드가 없이 품목과 규격만 존재하는 경우에는 프로젝트 마다 인위적인 표준공사코드를 부여」 → 텍스트 마이닝 접목 필요 | +| p.29 | 미준수 3갈래 — ①표준코드 없음 ②있는데 안 씀 ③「공사코드 품명을 잘못 연결한 경우」 | +| p.30 | 설계사가 과거 내역서 품명·규격·단위·공사코드를 그대로 복사 = 준수율 저조 주원인 · C3R 은 사용 여부와 자료 구조만 검증 · 「표준 공사코드와 내역항목의 일치 및 수정에 대한 방법론은 구축되어 있지 않은 상태」 | +| p.31 · p.35 | 장기 = 텍스트 마이닝·딥러닝 알고리즘 · 「인공지능 기반 공사코드 자동 매칭 시스템」 | + +⇒ 국가 차원에서도 「이름 → 코드」 자동 맞춤은 2020년 기준 미해결 과제. 그 뒤 진척은 확인 못 함. + +### 2-3. 저장소 — STmate · 다산이 무엇으로 식별하고 무엇으로 잇나 + +경로 머리 생략: `K/` = `resources/knowledge/original/` · `ST/` = `K/경쟁사 프로그램/STmate 분석/` + +#### 2-3.1 STmate + +| 항목 | 내용 | 근거(파일:줄) | +|---|---|---| +| 식별 칸 | `CODE`(C6) + `NAME` + `SIZE` + `UNIT` · 연결 칸 `JDCODE`(C17 호환규정코드) · `EQCODE`(시설) · `BQCODE` 등 | `ST/20_분석/02_데이터셋과_파일형식.md:44-46` · `K/원가계산/STmate/STC_필드사전_핵심.md:88-91, 110` | +| 코드 종류 | 노무·자재·경비·**대가(=일위대가 `B`)**·중기·**산근(=단가산출 `D`)**·기타·실적 | `ST/20_분석/20_설정파일과_보고서_카탈로그.md:79-83` | +| 프로젝트 코드는 순번 (이 조사 실측) | 실무 STC 7건 `COSTN.DBF`: 같은 코드가 프로젝트마다 다른 이름 — B 158 중 30 · D 273 중 50 · L 37 중 23 · M 228 중 43 · S 92 중 31 · X 96 중 35 (예 `L00002` = 보통인부 / 철근공 / 형틀목공) | 측정 `scratchpad/req225/p2_codecheck.py` → `p2_codecheck.txt` | +| 국가 호환코드 채움 (이 조사 실측) | `JDCODE` 채움 = L 86/97 · X 117/176 · S 123/166 · M 13/318 · **B 0/216 · D 0/371** · W 0/17. 예 `L00002 형틀목공 → L001010101000007` · `M00012 휘발유 → 1510150620282203`(앞 8자리 15101506 = UNSPSC 휘발유, 조달연 p.20 의 16자리 구조와 맞음) · `X00002 굴삭기(무한궤도) 0.7㎥ → 00000201007000000` | 측정 `scratchpad/req225/p2_jdcheck.py` → `p2_jdcheck.txt` | +| 출력 엑셀에 찍히는 코드 | 인쇄메뉴 `PM_Prt` 의 코드·비고 칸 선택지 = 「코드번호 인쇄, 번호(호표) 인쇄, 분류번호 인쇄, 시설코드 인쇄, 호환규정코드(, 적용기준일, 수공분류코드)」 → 엑셀에 호환코드가 보이는지는 설계자 인쇄 설정에 달림 | `ST/30_원자료/ui_form_catalog.txt:5-10` | +| 기준자료 갱신 매칭 `wM_Mrg_BSC` | 「매칭 기준(5단계) 호환규정코드 / 시설코드 / 명칭·규격·단위 / 호환+명+규+단 / 명칭·단위 / 설정안함」 · 콤보 5개(`cBx_Sel1~5`)로 우선순위 지정 · 명칭·단가·조달코드는 복사처/복사원 적용 선택 → **정확 일치 키를 차례로 내림** | `ST/20_분석/22_UI_폼_분석.md:116-124` · `ST/20_분석/34_화면구조_정리.md:62` · `ST/30_원자료/ui_form_catalog.txt:140-145` | +| 호환규정코드 찾기 `wM_JDCode` | 기준 「명칭 / 명칭·규격 / 분류번호 / 시설코드 / 선택안함」 · 검색 「명칭·규격·단위 / 호환규정코드 / 군급목록번호 / 중기품셈코드 / 시설코드번호」 → **사람 검색·선택** | `ST/20_분석/34_화면구조_정리.md:61` · `ST/30_원자료/ui_form_catalog.txt:116-118` | +| 단가 목록 검색 `wM_JabPum` | 검색 「명칭,규격 / **산근내용**」 → 구성 글로도 찾음 | `ST/20_분석/34_화면구조_정리.md:56` | +| 자원 사전 | `NAME` 과 `ST_NAME`(표준명칭) 별도 칸 · `SIZE` 110자 · 자재 명칭 1,761종 : 코드 57,580건(규격이 난관) · `ST_NAME` 은 경비·중기만 채움 | `ST/20_분석/29_암호화자료의_뼈대.md:21-26, 69, 76, 79` | +| 엑셀 왕복 | `CONV_XLS_2_ST`(엑셀→STmate 역등록) · `XLS_2_BDREC` · 행을 무슨 키로 잇는지는 확인 못 함(사용설명서 정회원 전용) | `ST/20_분석/24_모듈구조_전수.md:61-69` · W3 | +| 숨김 `〓 INITIAL 〓` 시트 | 계산 설정 스냅샷(공사정보 33 · 초기값 52 · 인쇄정보 75) · 행↔코드 매핑 규칙은 미착수 | `ST/20_분석/18_누락_계산로직_점검.md:81-101` · `K/원가계산/STmate/XLSX_INITIAL_구조.md:32` | +| 조달청 XML | `Conv_Jodal_ST8` ↔ `Conv_ST8_XML` 양방향 · XSD V1.0.5 · 내역공종코드 16→20자(2017-04-19) | `ST/20_분석/28_변환_수량산출_식언어.md:72-88` · `ST/20_분석/30_조달청_호환규정_XSD.md:19-30` | +| 일위대가 조합 350개 | 「건설표준품셈」 근거가 23조합에 글로 남음(`▣ 토공의 비탈규준틀[개소]2-6-1` 꼴) · 자원 이름 잇기에 `stmate_code_name_list.csv` 를 쓰자는 제안 | `ST/20_분석/31_일위대가_레시피_사전.md:112, 141` — ⚠ 그 CSV 의 코드 칸은 프로젝트 순번(위 실측)이라 열쇠로 못 씀, 이름·규격·단위 칸만 씀 | + +#### 2-3.2 다산 EST (2026-07 주자료 ESTX) + +| 항목 | 내용 | 근거(파일:줄) | +|---|---|---| +| 2층 구조 | 머리(HeadTitle 일위대가 1,468 · CostTitle 단가산출 1,569 · MachTitle 기계경비 1,049) ↔ 구성(*Main) 의 `TitleCode` 전수 일치 | `K/원가계산/다산소프트/ESTX_데이터관계.md:5-10` | +| 구성행이 원천을 잇는 열쇠 | 구성행 `Code` + `LinkIndex`(5 자재 · 6 노임 · 103 일위대가 · 104 단가산출 · 105 기계경비) = 코드 참조 | `K/원가계산/다산소프트/ESTX_데이터관계.md:16-38` | +| 식별 | `Code`(접두 U 일위대가 · D 단가산출 · E 기계경비 · M 자재 · L 노임 + 10자리) + `Name` + `Size` + `Unite` | `K/원가계산/다산소프트/ESTX_값사전_견적내역.md:88-91, 449-452` · `ESTX_값사전_자재노무기계.md:14-17, 182-185` | +| 이름은 규격으로 갈림 | 일위대가 머리 1,468건에 `Name` 347종 · `Size` 748종 | `ESTX_값사전_견적내역.md:449-451` | +| 표준시장단가 코드 | `Code` 대표값 `AA210.90110` 꼴 | `ESTX_값사전_자재노무기계.md:251` | +| 품셈 조항·조달 코드 칸 | 확인된 필드 범위에 전용 칸 없음(비고 `Bigo` 글만) | `ESTX_값사전_견적내역.md:440-500` | +| 엑셀 관련 | `ExcelSheetIndex` 칸 있으나 뜻 확인 불가 · `ExcelMullangList` 0건 | `ESTX_값사전_견적내역.md:397` · `ESTX_테이블사전.md:41` | +| 코드의 판 사이 안정성 | 확인 못 함(비교 스냅샷에서 `Code` 다른 행 존재 — CostMain 895) | `ESTX_데이터관계.md:69` | + +### 2-4. 관찰된 패턴 + +1. **같은 원천이면 코드로 자동** — 에니윈(자사 자료) · 다산 자원 [코드일괄변경] · STmate 갱신 1순위 호환규정코드. +2. **정확 일치 키 단계 내리기** — STmate 5단(코드 → 명+규+단 → 명+단). 퍼지·학습 없음. +3. **일위대가·단가산출 머리 = 국가 코드 거의 없음 → 사람 검색·선택 + 사용자정의코드** — 다산 공지 · 실측 B·D 의 JDCODE 0 · 조달연 p.18~19. +4. **타사 엑셀은 「읽기」(열·행 지정)까지만 공개** — 이름 맞춤 방식 공개 자료 없음(나스 · Easy). 다산은 변환 요청 게시판. +5. **이름 → 코드 자동 맞춤(텍스트 마이닝·AI)** = 조달연 장기 제안 단계 · 시중 AI 제품은 방법 비공개. +6. **확정 매핑 재사용** — RevitQ 사용자 라이브러리 · 조달연 「1번의 코드 수집」 대응표. + +### 2-5. 판정 — 우리 M02 에 가져올 것 + +| 방법(출처 패턴) | 판정 | 까닭 | +|---|---|---| +| 사람 검색·선택 + 사용자정의 표시(다산·STmate 공통) | 바로 쓸 만함 | 시중 표준 관행 · 「사용자가 고르는 도구」 원칙과 같음 · 새 의존성 없음 | +| 확정 매핑 기억(외부 이름+규격+단위 → 우리 일위대가) 재사용 (RevitQ 라이브러리 · 조달연 대응표) | 바로 쓸 만함 | 같은 설계사·같은 실무 양식이 되풀이되는 임도·사방에 맞음 · 표 하나로 됨 | +| 정확 일치 키 단계 내리기(정규화 뒤 명+규+단 → 명+단) | 바로 쓸 만함 | 계산 싸고 설명 쉬움 — 1차 거름으로 · 이름 차이 자체는 못 풂 | +| 구성 내용으로 찾기(STmate 「산근내용」 검색 · 다산 구성행 코드 참조) | 바로 쓸 만함 | 지금 단가산출 탭 대조와 같은 방향 — 구성 자원(노임 직종·장비)이 머리 이름보다 안정 | +| 자원 줄을 국가 호환코드(JDCODE)로 잇기 | 조건부 | 노임·중기·경비는 STC 에 코드가 대부분 있음 · 엑셀에는 인쇄 설정에서 「호환규정코드」를 골랐을 때만 보임 — 실무 엑셀 포함 여부 실측은 다른 조사 몫 | +| STC 파일을 직접 받아 읽기 | 조건부 | 엑셀보다 열쇠가 많음(`COSTN` 의 JDCODE·NAME·SIZE·UNIT 평문 + `COSTN.SMT` 산근 전개 평문 — `ST/20_분석/02_데이터셋과_파일형식.md:50-52`) · 발주처가 STC 를 줄 때만 · STC import 는 이미 필수 요구(`K/원가계산/STmate/_meta.md:22`) | +| AI·텍스트 마이닝 자동 맞춤(조달연 장기안 · 콘엑스) | 조건부 | 공개된 방법·성능 근거 없음 · 오프라인·무거운 의존성 부담 — 후보 순위 보조로만 | +| 일위대가 머리를 국가 코드로 잇기 | 안 맞음 | 실무 STC 일위대가 0/216 · 단가산출 0/371 · 조달연도 일위대가·중기단가산출은 임의코드로 봄 | +| STmate 프로젝트 코드(B00001 등)로 잇기 | 안 맞음 | 프로젝트 안 순번 — 같은 코드가 다른 항목(실측) | +| 엑셀 → 프로그램 변환 대행(다산 게시판) | 안 맞음 | 사람 서비스 — 기능 아님 | + +### 2-6. 확인 못 함 + +- STmate `CONV_XLS_2_ST` 가 엑셀 행을 무슨 키로 잇는지 (사용설명서 정회원 전용) +- 다산 엑셀 → EST 변환이 프로그램 기능인지 사람 대행인지 +- 나스 SGS · Easy 2.0 엑셀 읽기 뒤 이름 맞춤 방식 +- 콘엑스 AI 검증 방법·성능 +- 조달청 2025-05 개편 코드에 토목 일위대가·단가산출 코드가 생겼는지 (항목 1 몫) +- 다산 내부 코드(U·D·E·M·L+10자리)의 판 사이 안정성 + +### 2-출처 + +#### 웹 (실제로 연 페이지 · W1~W3·W18·W19 는 curl 로 받음) + +- W1 http://www.stma.co.kr/new_program/?pIdx=program2 +- W2 http://www.stma.co.kr/cust/?pIdx=cust&B_Name=center&b_dir=FAQ_bbs&b_url=list&category=cust2 +- W3 http://www.stma.co.kr/cust/?pIdx=cust&B_Name=center&b_dir=bbs&b_url=list&category=cust1 +- W4 https://www.dasansoft.com/products/?mname=products01&sub=2&pageN=1 +- W5 https://www.dasansoft.com/products/?mname=products02&sub=2&pageN=2 +- W6 https://www.dasansoft.com/products/?mname=products03&sub=2&pageN=3 +- W7 https://dasansoft.com/support/?file=dd_view&grp=1000&id=2121&mname=support05&mode=view&no=1866&offset=450&page=31&pageN=3&sh_category_id=&sub=0&table=statement +- W8 https://www.dasansoft.com/support/index.html?mname=support05&file=dd_list&table=statement&oid=535&sh_keyword=&sh_kind=&sh_category_id=&sub=0&pageN=3&page=112 +- W9 https://dasansoft.com/support/?file=dd_view&grp=12587&id=21360&mname=support03&mode=view&no=18887&offset=4035&page=270&pageN=1&sub=0&table=techqna +- W10 https://dasansoft.com/support/?file=dd_view&grp=17116&id=30408&mname=support03&mode=view&no=27338&offset=855&page=58&pageN=&sh_category_id=&sub=&table=techqna +- W11 https://www.dasansoft.com/customer/?mname=customer01&file=dd_view&table=notice&mode=view&id=4335&grp=4321&sub=3&pageN=1 +- W12 https://dasansoft.com/customer/?file=dd_view&grp=4078&id=4078&mname=customer01&mode=view&no=201&offset=0&page=1&pageN=&sh_category_id=&sub=3&table=notice +- W13 https://www.dasansoft.com/customer/?mname=customer01&file=dd_view&table=notice&mode=view&id=4045&grp=4045&sub=3&pageN=1 +- W14 https://nas.co.kr/product/features +- W15 https://nas.co.kr/nas/product/sgs10.php +- W16 https://www.04info.co.kr/products/1489130729/ +- W17 https://www.04info.co.kr/customer/01/v/330 +- W18 http://www.ew2002.com/office/i_ilwi.htm +- W19 https://ddusul.com/ +- W20 https://www.cadgraphics.co.kr/newsview.php?pages=review&sub=review01&subcate=24&catecode=24&num=4777 +- W21 https://www.glotechsoft.com/noticeView?F_NUM=5936 +- W22 https://www.pps.go.kr/kor/bbs/view.do;jsessionid=glV5dbwk1zSkvZI-PCz7tW9wZBCrSqUlHorpQtAVbR_j7QyrAvaj!286171551?key=00634&bbsSn=2509240009 +- W23 https://www.kip.re.kr/src/uploads/20221121/%E2%98%85(%ED%99%95%EC%A0%95)+%EA%B3%B5%EC%82%AC%EC%BD%94%EB%93%9C+%ED%91%9C%EC%A4%80%ED%99%94%EB%A5%BC+%ED%86%B5%ED%95%9C+%EC%9C%A0%ED%98%95%EB%B3%84+%EA%B3%B5%EC%82%AC%EB%B9%84+%EC%B5%9C%EC%A2%85%EB%B3%B4%EA%B3%A0%EC%84%9C+1216.pdf (pdftotext 로 본문 추출 · 쪽 번호 = 인쇄 쪽) + +#### 저장소 (읽기만) + +- `resources/knowledge/original/경쟁사 프로그램/STmate 분석/20_분석/` 00 · 02 · 18 · 20 · 22 · 24 · 28 · 29 · 30 · 31 · 34 +- `resources/knowledge/original/경쟁사 프로그램/STmate 분석/30_원자료/ui_form_catalog.txt` · `stmate_code_name_list.csv` +- `resources/knowledge/original/원가계산/STmate/` `_meta.md` · `STC_필드사전_핵심.md` · `XLSX_INITIAL_구조.md` +- `resources/knowledge/original/원가계산/다산소프트/` `_meta.md` · `ESTX_분석.md` · `ESTX_테이블사전.md` · `ESTX_데이터관계.md` · `ESTX_값사전_견적내역.md` · `ESTX_값사전_자재노무기계.md` +- 실측 대상: `resources/knowledge/original/실무문서/**/*.STC` 7건 (`COSTN.DBF`) + +#### 이 조사의 측정 (스크래치패드) + +- `req225/p2_codecheck.py` → `p2_codecheck.txt` (STmate 코드의 프로젝트 사이 이름 충돌) +- `req225/p2_jdcheck.py` → `p2_jdcheck.txt` (`COSTN.JDCODE` 종류별 채움) + +--- + +## 3. 일반 맞춤 기법 — 한국어 짧은 공종 이름 기준 + +### 3-0. 바탕 — 지금 맞춤이 무엇을 하나(코드 읽기 · 실측 아님) + +- 차례 = ① 층 기억(개인 → 회사 → 마스터 · 「품명 | 규격 | 단위」 정규화 열쇠가 똑같을 때만) ② 이름 같음 · 씨앗(`일위대가조합.json` 메모 170 줄) ③ 정리 이름 같음 ④ 후보 5 = 글자 두 개 묶음 Dice + 단위 덤 0.15 + 산림 덤 0.05 + 규격 덤 0.5 · 근거 후보(단산 출처 조항 · 직종 · 단위) 합침 — `M02_MasterTemplete/M02_Hopyo_Match.py:1-17`, `M02_Hopyo_Basis.py:1-15`. +- 정리 이름이 **괄호 글을 지움** — `M02_Hopyo_Match.py:79-84`. 우리 이름 「굴착(인력/토사)」 → 「굴착」 만 남음. +- 이름 후보 거름 = 글자 두 개 묶음이 하나도 안 겹치거나 뜻 낱말(괄호 밖 한글 덩어리)이 하나도 안 겹치면 뺌 — `M02_Hopyo_Match.py:311`. +- 근거 후보 거름 = 줄 직종에서 보통인부를 빼고 겹침이 없고 뜻 낱말도 안 겹치면 뺌 — `M02_Hopyo_Basis.py:198-213`. +- 읽은 결과(실측 아님): 「구조물터파기(인력100%) 토사」 ↔ 「굴착(인력/토사)」 은 글자 묶음 · 뜻 낱말이 하나도 안 겹쳐 **이름 길로는 후보에 못 듦**. 근거 길도 같은 벽 — 실무 출처 칸이 「[건설공사표준품셈 3-2-1 인력굴착]」 으로 우리 3-2-1 과 조항이 같아도(4-나-3 #1), 줄 직종이 보통인부뿐이면 직종 겹침이 0 이 되고(`M02_Hopyo_Basis.py:198`) 뜻 낱말(「구조물터파기」 ↔ 「굴착」)도 안 겹쳐 빠지는 길(`:213`). 같은 뜻 말 한 줄(터파기 ↔ 굴착)이면 이 거름을 넘음. 실제 비율은 다른 조사자 실측 몫. +- **이미 있는 그릇**: M01 로직 줄에 「별칭」(설계자가 로직을 찾는 다른 이름 · 쉼표로 여럿 · 208-1) · 「옛이름」 칸이 있음. M01 찾기는 씀 — `M01_MasterData/M01_MasterData_Store_Find.py:1-10`(키 · 원문번호 · 이름 · 옛이름 · 별칭). M02 맞춤은 안 씀 — `M02_Hopyo_Match.py:16` 「M01 은 읽기만(로직 별칭 안 씀)」. 마스터 로직 1,469 줄 셈 = 별칭 0 · 옛이름 0 · 원문번호(품셈 절 번호) 1,469 줄 모두(`resources/master_data/로직_*.json`). 템플릿 층에는 별칭을 쓴 자리 있음(예 `storage/1/10/templates/labor/item1.json:23` 「고임도채집」). + +### 3-1. 예시 계산(쌍 6 개 · 실측 아님 · 표준 라이브러리만) + +스크립트 = 스크래치패드 `req225/tools/demo_sim.py`. 우리 이름은 `resources/master_data/로직_*.json`(로직 1,469 줄) · 정답 짝은 `resources/master_data/일위대가조합.json` 씨앗 메모 기준. + +| 엑셀 이름 | 우리 이름 | 판 | 2자 Dice(괄호 뺌 · 지금 방식 근사) | 2자 Dice(괄호 둠) | Jaro-Winkler | difflib | 자모 2자 Dice | 같은 뜻 말 + 낱말 묶음 Jaccard | +|---|---|---|---|---|---|---|---|---| +| 구조물터파기(인력100%) 토사 | 굴착(인력/토사) | 정답(건설 3장 인력 굴착) | 0.00 | 0.24 | 0.00 | 0.00 | 0.19 | 0.75 | +| 구조물터파기(인력100%) 토사 | 구조물터파기 육상토사(0~1m) | 오답(산림 9장 · 굴착기) | 0.75 | 0.50 | 0.96 | 0.89 | 0.81 | 0.75 | +| 구조물 터파기(토사) 굴삭기 0.2m3 | 터파기(기계) 보통토사 | 정답(씨앗 GC000088) | 0.22 | 0.27 | 0.55 | 0.30 | 0.29 | 0.40 | +| 레미콘타설(장비) 무근 | 레디믹스트콘크리트 타설 | 정답(씨앗 GC000198) | 0.12 | 0.11 | 0.54 | 0.44 | 0.42 | 0.50 | +| 콘크리트믹서사용 0.45 m3 | 기계비빔타설 | 정답(씨앗 GF000156) | 0.00 | 0.00 | 0.00 | 0.00 | 0.06 | 0.00 | +| 모르터바르기 1:3 | 모르타르 배합(시멘트 · 모래) | 정답(씨앗 GC000677) | 0.17 | 0.12 | 0.70 | 0.43 | 0.43 | 0.33 | + +읽는 법: +- 1 · 2 줄 = 글자 기법(Dice · Jaro-Winkler · difflib · 자모) **모두 오답 쪽을 더 높게 봄**. 이름이 비슷한 다른 일(기계 터파기)이 이김. 같은 뜻 말 사전을 써도 낱말 묶음만으로는 동점(0.75 = 0.75). 가르는 것 = 방법 칸(인력 ↔ 기계)이 어긋나면 크게 깎는 규칙, 또는 구성(보통인부만 ↔ 굴착기) 대조. +- 5 줄 = 모든 글자 기법 0. 「믹서 ↔ 기계비빔」 같은 뜻 말 줄이나 구성 대조(콘크리트믹서)만 잡음. +- 6 줄 = 「모르터 ↔ 모르타르」 같은 표기 흔들림은 자모 분해 · 같은 뜻 말이 일부 메움. +- 결론 = 글자 유사도 기법을 바꾸는 것만으로는 근본 해결 안 됨. **같은 뜻 말 + 칸 쪼개기 + 구성 대조** 가 핵심. + +### 3-2. 기법별 + +| 기법 | 무엇 · 어떻게 | 한국어 짧은 공종 이름에서 | 설치 부담 | 판정 | 출처 | +|---|---|---|---|---|---| +| 정규화(괄호 · 규격 · 단위 · 숫자) | NFKC(전각 → 반각 · ㎥ → m3 · Ⅰ → I) · 옛 표기(굴삭기 → 굴착기) · 단위 표기 통일 · 「인력100%」 「기계90%+인력10%」 → 방법 칸 · 「Ø 50 MM」 「D50」 → D=50 mm · 괄호 글은 **지우지 말고 칸으로 옮김** | 이름 흔들림의 가장 큰 몫(띄어쓰기 · 괄호 · 규격 섞임)을 값싸게 줄임. 괄호를 지우면 「인력/토사」 같은 결정적 정보가 사라짐 | 없음(표준 `unicodedata` · `re`) | 바로 쓸 만함 | Python unicodedata 문서 https://docs.python.org/3.12/library/unicodedata.html · 공종명 혼재 실태 = 정상원 · 정기창(2020) 한국건설관리학회 논문집 21(6) 125~134 https://www.koreascience.or.kr/article/JAKO202034352378495.pdf (Table 1 · 1.1절) | +| 순서 무관 열쇠(fingerprint) | 낱말을 정렬 · 중복 제거해 붙인 열쇠로 같음 판정 — 「토사 구조물터파기」 = 「구조물터파기 토사」 | 칸 쪼개기 뒤에 쓰면 「이름 같음」 단계가 넓어짐. 오탐이 가장 적은 방식 | 없음 | 바로 쓸 만함(칸 쪼개기와 함께) | OpenRefine clustering https://openrefine.org/docs/technical-reference/clustering-in-depth | +| 같은 뜻 말 사전(시소러스) | 세 종류 줄 — 같은 말(레미콘 = 레디믹스트콘크리트 · 모르터 = 모르타르) · 쪼개기(구조물터파기 → 구조물 + 터파기) · 덮는 말(보통토사 ⊂ 토사). 검색엔진 한국어 분석기도 같은 방식(사용자 사전에 「복합어 → 쪼갠 낱말」 줄) | 예시 1 · 5 줄처럼 글자가 하나도 안 겹치는 짝을 잇는 유일한 값싼 길. 사전 품이 드는 대신 근거가 사람 눈에 보임 | 없음 — 그릇이 이미 있음(M01 로직 「별칭」 칸 · 화면에서 고침 · 층별). 맞춤이 별칭을 읽게만 하면 됨 · 쪼개기 · 덮는 말 줄은 작은 표 하나 더 | 바로 쓸 만함(핵심) | Elasticsearch nori user_dictionary · decompound_mode https://www.elastic.co/docs/reference/elasticsearch/plugins/analysis-nori-tokenizer · 씨앗 = `resources/master_data/일위대가조합.json`(메모 170 줄) · `M02_Hopyo_Match.py:45-51`(MACH_WORDS 5 짝). 재료 후보 = 품셈 [주] 다리 문구(4-나-1) · 조달청 세부공종코드 옛/새 이름 묶음(1-4) · 실무 짝 20(4-나-3) · 국토교통부_건설용어사전 CSV 6,666 줄(칸 = 용어ID · 용어명 · 용어영문명 · 용어설명 · 초성값 — 동의어 칸 없음 · 해마다 갱신 · 정의문에서 손으로 뽑아야 함) https://www.data.go.kr/data/15128802/fileData.do · https://www.data.go.kr/biz/dcat/metadata/15128802.do | +| 한국어 형태소 분석 | kiwipiepy = Apache-2.0 · Windows x86-64 바퀴(cp39-abi3 → 3.12 됨 · 3.7 MB) · 모델 꾸러미 `kiwipiepy_model` 자동 설치 · 사용자 사전 `add_user_word` · 오타 `typos` · 띄어쓰기 `space()`. mecab-ko = python-mecab-ko 1.3.7(BSD-3) 에 cp312 win_amd64 바퀴(653,484 바이트) 있음 · 사전 꾸러미 `python-mecab-ko-dic` 를 함께 받음. KoNLPy 의 Mecab 은 Windows 미지원 · KoNLPy 는 JDK + JPype 필요 | 공종 이름은 짧고 전문 복합어(구조물터파기 · 깬잡석찰쌓기)라 일반 사전으로는 잘못 쪼갤 위험. 분석기가 띄어쓰기에 크게 기댐(위 논문 5.1절). 어차피 도메인 사전이 있어야 함 | 새 의존성(kiwipiepy + 모델) · 시험 못 함(설치 금지) | 조건부 — 도메인 사전으로 모자랄 때만, 고른다면 kiwipiepy | https://pypi.org/project/kiwipiepy/ · https://github.com/bab2min/kiwipiepy · https://github.com/jonghwanhyeon/python-mecab-ko · https://pypi.org/pypi/python-mecab-ko/1.3.7/json · https://konlpy.org/en/latest/install/ | +| 퍼지 맞춤(rapidfuzz · Jaro-Winkler · difflib) | rapidfuzz 3.14.6 = MIT · 의존성 없음 · Windows 바퀴 · Python ≥ 3.11 · `ratio` `partial_ratio` `token_sort_ratio` `token_set_ratio` `WRatio` · 3.0 부터 기본 전처리 없음. 표준 `difflib.SequenceMatcher`(Ratcliff/Obershelp) · `get_close_matches` 는 설치 없음 | 국내 공종명 비교 연구(타일 공사 684 개)에서 Jaro-Winkler 가 5 기법 중 가장 나음 — 그래도 1순위 정답률 33.7~49.7 %(N-gram 2 = 28.5~37.7 %). 결론 = 「완전 자동화 힘듦 · 전문가에게 몇 개 보여 주는 추천 시스템」. token_* 은 빈칸 기준이라 붙여 쓴 한국어에선 칸 쪼개기 뒤에야 쓸모. 예시 1 · 2 줄처럼 오답을 더 높게 보기도 함 | rapidfuzz 작음 · difflib 은 표준 · Jaro-Winkler 는 25 줄 | 조건부 — 보조 점수 · 근본 해결 아님 | https://pypi.org/project/RapidFuzz/ · https://docs.python.org/3.12/library/difflib.html · 정상원 · 정기창(2020) 위 PDF Table 5 · 7 · 6.2절 | +| 자모 분해 | NFD 정규화 = 한글 음절을 자모로 풂(표준 `unicodedata.normalize("NFD", …)`) → 자모 n-그램 · 편집 거리 | 「모르터 ↔ 모르타르」 「깍기 ↔ 깎기」 같은 표기 흔들림에 강함(예시 6 줄 0.17 → 0.43). 뜻이 다른 말은 못 이음 | 없음 | 조건부 — 보조 점수 | Unicode FAQ(Korean) https://www.unicode.org/faq/korean.html 「Text that is normalized as NFD will contain only Jamo」 | +| 글자 n-그램 TF-IDF | 글자 2~3 그램에 IDF 무게 → 흔한 묶음(콘크·크리·설치·운반)은 가볍게, 드문 묶음은 무겁게. 지금 Dice 는 모든 묶음을 같은 무게로 봄 | 우리 로직 1,469 개 · 엑셀 수천 줄이면 표준 라이브러리로 충분. 같은 뜻 말 문제는 못 풂 | 없음(scikit-learn 미설치 · `Counter` 로 20 줄 · numpy/scipy 는 설치돼 있음) | 바로 쓸 만함(지금 Dice 대체 · 작은 이득) | scikit-learn TfidfVectorizer `analyzer='char_wb'` 설명 https://scikit-learn.org/stable/modules/generated/sklearn.feature_extraction.text.TfidfVectorizer.html | +| 문장 임베딩 | multilingual-e5-small = MIT · 12 층 · 384 차원 · 한국어 포함 · 「query: / passage: 」 머리말 필수 · model.safetensors 470,641,600 바이트 · ONNX qint8 118,346,824 바이트. ko-sroberta-multitask = 768 차원 · KorSTS 코사인 스피어만 85.60 · model.safetensors 442,499,016 바이트 · sentence-transformers(PyTorch) 필요. ONNX 길 = onnxruntime 1.31.0(cp312 바퀴 14.9 MB) + 토크나이저 | 「아스팔트 부수기 ↔ 아스콘 깨기」 같은 뜻 거리를 잡을 가능성. 그러나 짧은 전문 용어(터파기 ↔ 굴착 · 리핑암)를 일반 말뭉치 모델이 아는지 미확인 — 시험 먼저. 우리 로직 이름은 한 번 미리 계산해 두면 됨(CPU 가능) | 큼(수백 MB 모델 파일 + 새 실행 꾸러미) · 오프라인은 모델 파일 복사로 가능 | 조건부 — 위 기법 뒤 남은 실패가 「뜻은 같고 글자 다름」 일 때만 · 실측으로 이득 확인 뒤 | https://huggingface.co/intfloat/multilingual-e5-small/raw/main/README.md · https://huggingface.co/api/models/intfloat/multilingual-e5-small/tree/main · https://huggingface.co/api/models/intfloat/multilingual-e5-small/tree/main/onnx · https://huggingface.co/jhgan/ko-sroberta-multitask · https://sbert.net/docs/installation.html · https://pypi.org/project/onnxruntime/ | +| 큰 언어 모델 재순위 | 후보 5~10 개를 주고 고르게 하고 까닭(조항 · 직종 · 방법)을 한 줄로 받음. Claude Haiku 5.5 = 입력 $0.10 · 출력 $0.50 / 백만 토큰(프롬프트 10 만 이하) · 캐시 적중 $0.01 · 배치 50 % 할인. 어림(가정 · 생각 토큰 별도): 줄 2,000 × (입력 400 + 출력 60 토큰) ≈ 입력 0.8 M · 출력 0.12 M → 약 $0.14 / 파일 — 돈은 걸림돌 아님 | 뜻 맞춤 · 설명은 가장 잘할 가능성. 그러나 인터넷 필요(사내 오프라인 서버에서 못 씀) · 발주처 자료가 밖으로 나감 · 같은 입력에 답이 흔들릴 수 있음. 로컬 모델(예 EXAONE 3.5 2.4B = 매개변수 2.4B · 32K 문맥 · GGUF 양자화본 있음 · 크기 확인 못 함)은 라이선스가 「EXAONE AI Model License Agreement 1.1 - NC」(비상업) | 클라우드 = 설치 작음 · 망 필요 / 로컬 = 큼 · 라이선스 걸림 | 안 맞음(지금 조건) — 온라인 · 외부 전송이 허용되면 「설명 붙임 보조」 로 조건부 | https://platform.claude.com/docs/en/about-claude/pricing.md · https://huggingface.co/LGAI-EXAONE/EXAONE-3.5-2.4B-Instruct | +| 구조 쪼개기(칸별 맞춤) | 이름을 칸으로 나눔 — 대상(구조물 · 측구 · 노체) · 작업(굴착 · 되메우기 · 쌓기 · 붙이기 · 타설) · 방법(인력 · 기계 · 비율) · 재료/토질(토사 · 혼합토사 · 리핑암 · 발파암 · 깬잡석 · 레미콘) · 규격(D · H · T · L3 · 0.7㎥) · 조건(육상/용수 · 깊이 0~1 m · 소형/간단/보통). 칸마다 같음 · 덮음 · 어긋남을 매기고 더함. 결정 칸(방법 · 토질)이 어긋나면 크게 깎음. 무게는 처음엔 손으로, 확정 짝이 쌓이면 칸별 m/u 셈(Fellegi–Sunter: 칸 무게 = log₂(m/u), 칸 무게를 더함) | 국내 연구도 공종명을 〈대상〉+〈작업〉 두 칸으로 정의함(Table 3). 예시 1 · 2 줄을 가르는 길. 화면에 「어느 칸이 맞고 어긋났나」 를 그대로 보일 수 있음(사용자가 고르는 도구에 맞음) | 없음(규칙 + 사전) | 바로 쓸 만함(핵심) | Splink Fellegi–Sunter https://moj-analytical-services.github.io/splink/topic_guides/theory/fellegi_sunter.html · 정상원 · 정기창(2020) 위 PDF Table 3 · 5.1절 · `M02_Hopyo_Match.py:57-66 · 115-180`(이름 붙은 수 D= H= … 이미 읽음) | +| 구성 비교(노무 · 재료 · 장비 줄 묶음) | 엑셀 호표 구성 줄의 직종 · 장비 · 자재 묶음 ↔ 우리 로직 호표 요소 묶음 겹침(Jaccard). 「보통인부만」 = 인력 작업의 증거 · 굴착기 있음 = 기계 작업의 증거 | 이름과 무관한 증거라 예시 1 · 2 · 5 줄을 가름. 지금 근거 길은 단산 직종을 쓰되 보통인부를 뺌(`M02_Hopyo_Basis.py:198`) — 장비 · 자재 · 「보통인부뿐」 신호는 아직 안 씀 | 없음(엑셀 · 우리 로직 둘 다 이미 읽음) | 바로 쓸 만함 | `M02_Hopyo_Basis.py:128-156`(우리 직종 = 호표 LB 요소) | +| 금액 대조 | 엑셀 호표 단가(재료비 · 노무비 · 경비)를 같은 해 단가로 셈한 우리 후보 단가와 견줌 — 비율이 가까우면 덤 · 멀면 표시. 절대 금액보다 노무비 몫(노무비 ÷ 계) 이 해 · 지역 차이에 덜 흔들림 | 동점 가르기에 쓸모. 실무 엑셀은 옛 판 값 · 할증 · 지역 단가가 섞여 단독 판정 불가 | 없음(우리 셈 이미 있음) | 조건부 — 동점 가르기 · 경고 표시만 | 씨앗 메모 「(실무 수는 옛 판 값)」 꼬리표 — `M02_Hopyo_Match.py:52` | +| 사용자 확정 학습 | ① 맞춤 기억 — 이미 있음(층 기억 개인 → 회사 → 마스터 · 열쇠 = 품명 · 규격 · 단위 똑같음). ② 확정 짝에서 낱말 차이를 뽑아 「같은 뜻 말 후보」 로 쌓고 관리자가 승인 → 사전으로 → 똑같지 않은 다음 줄에도 번짐. ③ 능동 학습 — 가장 불확실한 줄부터 사용자에게 보여 주고, 확정마다 무게 다시 셈(dedupe 방식). ④ 확정 짝으로 칸 무게 셈(m/u) | ① 은 똑같은 줄만 기억 → 이름이 조금만 달라도 못 씀. ② 가 일반화의 핵심. ③ 은 사용자 품을 줄임 | 없음 | ①② 바로 쓸 만함 · ③④ 조건부(확정 짝이 쌓인 뒤) | `M02_Hopyo_Match.py:68-77 · 333-352`(기억 열쇠 · 층 차례) · dedupe 능동 학습 https://docs.dedupe.io/en/latest/how-it-works/Matching-records.html · OpenRefine reconciliation(후보 + 점수 + 사람이 고름) https://openrefine.org/docs/manual/reconciling | + +### 3-3. 비슷한 남의 사례(문헌) + +| 사례 | 무엇 | 우리와 | 출처 | +|---|---|---|---| +| 정상원 · 정기창(2020) 「문자열 유사도 알고리즘을 이용한 공종명 인식의 자연어처리 연구」 | 나라장터 입찰 내역서의 타일 공종명 684 개를 2020 표준품셈 기준 정식 공종명에 맞춤. Hamming · N-gram(2 · 3) · Levenshtein · Jaro-Winkler 비교 → Jaro-Winkler 최고(1순위 정답률 33.7~49.7 %). 정답률이 낮아 「추천 시스템(후보 몇 개 + 전문가 고름)」 과 「전문가 작업 환경에서 DB 가 자라게」 를 권함 | 우리 「후보 + 사용자 고름 + 층 기억」 구조가 이 결론과 같은 방향. 글자 기법만으로는 한계라는 근거 | https://www.koreascience.or.kr/article/JAKO202034352378495.pdf | +| Lee · Lee · Chi · Oh(2023) 국도 내역서 공종 분류 코드 자동 부여 | 국도 BOQ 세부 공종 설명을 토큰으로 나눠 낱말 겹침으로 가장 비슷한 공종 분류 코드를 붙임 · 정확도 91.12 % 넘음(초록 기준) | 「표준 코드 표 + 낱말 겹침」 이 국내 공공 내역에서 통한 사례. 원문은 열람 못 함(ASCE 403) — 초록 수치는 검색 요약으로만 확인 | https://cm.snu.ac.kr/publication/automatic-classification-of-construction-work-codes-in-bill-of-quantities-of-national-roadway-based-on-text-analysis/ · https://api.semanticscholar.org/graph/v1/paper/DOI:10.1061/JCEMD4.COENG-12730?fields=title,abstract,year,authors,venue | +| Deza 외(2022) 영국 BOQ → ICMS 32 분류 | 24 개 사업 5 만여 줄 · 신경망 분류 · 평균 F1 90 % 넘음 | 학습 자료가 5 만 줄 규모일 때 이야기 — 우리는 자료 적음 · 안 맞음 | https://arxiv.org/abs/2211.07705 | +| Maurya 외(2020) 송장 품목 줄 맞춤 · 사용자 피드백 온라인 학습 | 담당자의 맞음/아님 · 순위 피드백으로 유사도를 계속 학습. 「739mL ↔ 0.739L」 같은 단위 · 약어 문제를 지적 | 우리 확정 학습(②~④)과 같은 발상 · 단위 정규화 필요성 근거 | https://arxiv.org/abs/2001.00288 | + +--- + +## 4. 저장소 지식DB — 맞춤 사전 재료 + +근거 표기 = `resources/knowledge/` 아래 상대경로:줄. 줄임 표: + +| 줄임 | 경로 | +|---|---| +| 건설/ | original/원가계산/건설공사_표준품셈/본문/ | +| 산림/ | original/원가계산/산림_표준품셈/본문/ | +| TI/ | technical_info/01_임도/ | +| ST/ | original/경쟁사 프로그램/STmate 분석/ | +| [북부임도] | original/실무문서/2026_실무일위대가 모음/2026년_북부지방산림청_임도사업_설계내역서(공통단가).xls — 「시트!R행」 | +| [북부사방] | 같은 폴더 2026년_북부지방산림청_사방사업(계류+댐)_설계내역서.xls | +| [사방안16] | 같은 폴더 2016_상반_산림청_사방사업표준품셈(안).xls | +| [경남1]·[경남2] | original/원가계산/자재단가/경남교육청_일위대가/일위대가_1.xlsx · 일위대가_2.xlsx | +| [레시피] | ST/30_원자료/일위대가_레시피.json (줄) | +| [개정26] | original/원가계산/건설공사_표준품셈/2026년_건설공사_표준품셈_개정사항.pdf (인쇄쪽) | + +- xls·xlsx 는 읽기 전용 파이썬(-I) 한 번으로 이름 칸(앞 8열 글자)만 훑음 — 스크립트·출력 = 스크래치패드 `req225/scan_names.py` · `req225/scan_out_u8.txt`. 단가산출 탭의 「번호.이름[단위]」 줄은 B열(C2). +- [개정26] 은 pdftotext 로 글자만 뽑음 — `req225/rev2026.txt`. md 판 없음(original/원가계산/건설공사_표준품셈/_meta.md:23). + +--- + +### 4-가. 품셈 항목 이름 목록 — 뽑을 자리 · 수 · 형식 + +| 원천 | 자리 | 수(머리말 셈) | 머리말 꼴 | +|---|---|---|---| +| 건설공사 표준품셈 2026 | 건설/ 5부문 45장 320 절 파일 (original/원가계산/건설공사_표준품셈/_meta.md:19) | 절(##) 222 · 항(###) **1,095** · 목(####) 458 — 공통 항 321·목 282 / 토목 항 235·목 76 / 건축 162 / 기계설비 242 / 유지관리 135 | `### 3-2-1 굴착(인력/토사)('08, '20, 25년 보완)` (건설/01_공통부문/제03장_토공사/3-02_굴착.md:18) — 번호 · 이름 · 개정 표지 | +| 건설 8장 기계 손료표 | 건설/01_공통부문/제08장_건설기계/8-03-0*_*.md | 기종 머리 156 | `#### (0201) 굴착기(무한궤도)` (8-03-01_[00]토공기계.md:74) — 4자리 기종 코드 + 이름 | +| 산림사업 표준품셈 2026 | 산림/ 14장 203 절 파일 (original/원가계산/산림_표준품셈/_meta.md:16) | 절(##) 189 · 항(###) 274 · 목(####) 78 — 토공 9장 22/50/16 · 철근콘크리트 12장 38/28/5 · 구조물 13장 16/34/12 | `## 9-13. 구조물터파기` · `### 9-13-1. 육상토사(0~1m)` (산림/제09장_토공/9-13_구조물터파기.md:16 · :21) — 번호 뒤 마침표 | +| 산림 별표 md | original/원가계산/산림_표준품셈/별표/별표0_산림사업 표준품셈.md | 내용 없음(안내문뿐 · 같은 파일:5) | 쓸 것 없음 | +| 산림 현행_20260101.md | original/원가계산/산림_표준품셈/현행_20260101.md | 고시 본문 3조뿐(:6-16) | 항목 이름 없음 | + +- 머리말 규칙이 정해져 있음 — 「`#` 장 · `##` 절 · `###` 항 · `####` 목, 번호와 이름은 PDF 글자 그대로」(original/원가계산/_품셈md_작성규칙.md:49) · 개정 표지 남김(:48). 두 품셈 모두 PDF 1:1 대조 「확정」(건설 _meta.md:20 · 산림 _meta.md:16). +- 기계로 뽑기 = 쉬움. 정규식 한 줄(번호 `\d+(-\d+)+\.?` + 나머지)로 번호·이름이 갈림. 손볼 것: + 1. 산림 하위 머리는 한정어만인 것이 많음 — 6자 이하 머리: `인력` 3 · `토사` 3 · `기계` 2 · `암절취` 2 · `발파암` 2 · `집토` 2 … 예 `### 9-3-1. 인력`(산림/제09장_토공/9-03_토사깍기.md:18) → 윗머리와 이어 「토사깍기 > 인력」으로 만들어야 이름이 됨. + 2. 건설 절 번호는 부문마다 1장부터 다시 시작 — 열쇠에 부문을 넣어야 함 (TI/04_수량분석정보/건설품셈_활용.md:54). + 3. 개정 표지 괄호는 이름에서 뗌 — 따옴표가 `'` · `‘` 두 꼴 (건설/01_공통부문/제04장_조경공사/4-01_잔디_및_초화류.md:36). + 4. 산림 목(####) 일부에 식이 붙음 — `#### 3. 집토: Q=3600×q×k×f×E/㎝= ㎥/시간` (산림/제09장_토공/9-08_철근콘크리트_깨기.md:43). + 5. 산림 원문 목차 번호 중복 오기 2건 — 본문 머리 번호를 씀 (TI/04_수량분석정보/건설품셈_활용.md:93-100). +- 표 안 「구분」 열(특별인부 · 보통인부 · 굴착기 0.7㎥ …)이 구성(노무·장비) 비교 재료로 함께 뽑힘 — 예 건설/01_공통부문/제03장_토공사/3-02_굴착.md:25. +- 범위 밖 한 줄: 지식DB 문서가 가리키는 `resources/data_work_item_link/…json`(TI/04_수량분석정보/건설품셈_활용.md:53) · `resources/data_work_item_master/…json` 은 지금 저장소에서 찾지 못함 — `resources/master_data/계수_*품셈_*.json` 이 뒤를 이은 것으로 보이나 지식DB 밖이라 열지 않음(확인 못 함). + +--- + +### 4-나. 용어 정의 · 같은 뜻 말 + +#### 4-나-1. 원문이 직접 잇는 말 (다리 문구) + +| 다리 | 근거 | +|---|---| +| 굴착(인력/토사) = 깊이 1m 이하 인력 「구조물 터파기 또는 흙깎기」에 적용 | 건설/01_공통부문/제03장_토공사/3-02_굴착.md:30 | +| 「잔토처리」 = 굴착 품 비고의 별도 계상 항(㎥당 보통인부 0.2인) | 같은 파일:26 | +| 인력굴착 — 깊이 20㎝ 이상 = 「터파기」, 그 밖 = 「절취」 | 산림/제09장_토공/9-01_굴착.md:22 | +| 구조물터파기 기본 = 「인력 10%+기계 90%」 → 실무 이름의 「(인력100%)」「(기계100%)」「기계 90% + 인력 10%」 표기의 뿌리 | 산림/제09장_토공/9-13_구조물터파기.md:18 | +| 한 절 안에서 장비 이름 둘 — 머리 「굴착기 0.7㎥」 · 표 「유압식백호우(무한궤도,0.7㎥)」 | 같은 파일:18 · :28 | +| 표준시방서 제목이 같은 뜻 말을 괄호로 병기 — 「땅깎기(절토)」「흙쌓기(성토)」「되메우기 및 뒤채움」「사토 및 잔토처리」(KCS 11 20 10 · 20 · 25 · 30) | original/표준시방서/건설공사 표준시방서 (건설기준 코드 KDS_KCS)/_index.md:42-43 (같은 폴더 `첨부/[압축] 개정전문/KCS 11 00 00 지반 공사/` 파일 이름) | +| 「리핑암 = 풍화암 / 발파암 = 연·경암」(KDS 분류) · 품셈 분류(보통토사~극경암)와 두 체계 | TI/03_계산정보/토량계산.md:90 · :108 | +| 「암절취 (리핑암 — 암파쇄·집토)」 | TI/05_원가정보/원가_출력변수_사전.md:66 · TI/04_수량분석정보/토공_수량.md:19 | +| 「사토장 관리」(품셈 공종명) ≠ 「유용토운반작업장」(시행규칙 시설명) — 용어 다름 | TI/04_수량분석정보/토공_수량.md:54 | +| 「지장목 제거(벌개제근)·뿌리다듬기·제근」을 한 공종으로 묶음 | TI/05_원가정보/원가_출력변수_사전.md:55 | +| 이름이 비슷하다는 이유만으로 자동 연결 금지 — 산림 총칙 7호(유사 공종은 산림 품셈 우선) | TI/04_수량분석정보/건설품셈_활용.md:50 · 산림/제01장_적용기준/1-01_일반사항.md:34 | +| 이름이 같아도 다른 것 — 「표면 마무리」 산림 12-2 ㎡당 vs 건설 6-1-3 100㎡당(100배) · 「토량환산」 같은 이름 토질 행이 다른 계수 · 「돌쌓기」 표 축 다름 | TI/04_수량분석정보/건설품셈_활용.md:89 · :87 · :85 | + +#### 4-나-2. 원천별 표기 갈래 (낱말 수 · 부분 문자열 셈) + +원천 경로 — 건설 품셈 = 건설/ · 산림 품셈 = 산림/ · 임도교본 = original/임도기술교본 · 사방교본 = original/사방기술교본 · KCS 지반 = original/표준시방서/건설공사 표준시방서 (건설기준 코드 KDS_KCS)/첨부/[압축] 개정전문/KCS 11 00 00 지반 공사. 「깎기」는 땅깎기·흙깎기를 포함 · 「굴착」은 굴착기를 뺀 셈. + +| 낱말 | 건설 품셈 | 산림 품셈 | 임도교본 | 사방교본 | KCS 지반 | +|---|---:|---:|---:|---:|---:| +| 굴착(기 뺌) | 146 | 17 | 16 | 22 | 383 | +| 터파기 | 74 | 22 | 131 | 15 | 187 | +| 절토 | 20 | 22 | 279 | 5 | 12 | +| 절취 | 7 | 49 | 48 | 10 | 10 | +| 땅깎기 | 0 | 1 | 43 | 44 | 100 | +| 흙깎기 | 4 | 1 | 20 | 9 | 3 | +| 깍기(맞춤법 어긋남) | 0 | 4 | 21 | 2 | 2 | +| 되메우기 / 되메움 | 42 / 0 | 14 / 0 | 57 / 1 | 23 / 0 | 173 / 6 | +| 뒤채움 / 뒷채움 | 15 / 0 | 34 / 15 | 41 / 44 | 3 / 8 | 173 / 18 | +| 잔토처리 / 잔토정리 | 16 / 0 | 0 / 1 | 12 / 0 | 1 / 0 | 9 / 0 | +| 사토 | 4 | 7 | 74 | 2 | 28 | +| 굴착기 / 굴삭기 / 백호우·백호 | 260 / 7 / 0 | 148 / 15 / 39 | 5 / 8 / 3 | 3 / 4 / 0 | 9 / 0 / 1 | + +- 원천마다 주 낱말이 다름 — 건설·KCS = 굴착·터파기 · 교본 = 절토·땅깎기 · 산림 = 절취·깍기. 같은 원천 안에서도 뒤채움/뒷채움 · 굴착기/굴삭기/백호우가 섞임. + +#### 4-나-3. 같은 일 · 다른 이름 짝 (20) + +판정 칸 = 「같은 일」(출처 인용 번호나 원문 다리로 이어짐) · 「같은 갈래」(같은 일인지 사용자 확인 필요). + +| # | 이름들 | 근거 | 판정 | +|---|---|---|---| +| 1 | 실무 「구조물터파기(인력100%)(토사)」 — 출처 칸 「[건설공사표준품셈 3-2-1 인력굴착]」 ↔ 우리 「굴착(인력/토사)」 = 건설 3-2-1 | [북부임도] 단가산출!R4229 · R4231 · 건설/01_공통부문/제03장_토공사/3-02_굴착.md:18 | 같은 일 — 겹치는 낱말은 인력·토사뿐, 출처 번호로 이어짐 | +| 2 | 「인력굴착(토사) / 보통토사」 + 비고 「H=1m 이하(구조물터파기, 흙깍기)」 | [경남1] 공종별내역서!R845 · 일위대가목록!R1405 | 같은 일 — 어순 뒤집힘 · 비고가 다리 | +| 3 | 사방 「구조물터파기(인력)」 ↔ 임도 「구조물터파기(인력100%)」 — 한 발주처 두 문서 | [북부사방] 단가산출!R316 · [북부임도] 일위대가!R22 | 같은 일 | +| 4 | 「구조물터파기(암)」 [2025년 표준품셈3-2-4] ↔ 「구조물터파기(암절취)」 [건설공사표준품셈3-2-4] ↔ 건설 「터파기(기계)」 3-2-4 ↔ 산림 「구조물터파기 > 육상 암절취(0~1m)」 9-13-7 | [북부사방] 단가산출!R336 · R338 · [북부임도] 단가산출!R4039 · R4041 · 3-02_굴착.md:126 · 산림/제09장_토공/9-13_구조물터파기.md:92 | 같은 이름이 건설·산림 두 출처로 갈림 | +| 5 | 「흙깍기(토사)」 — 출처 「흙깎기-보통토사 [건설공사 표준품셈3-2-3]」 ↔ 「토사절취(하상절취)」 — 출처 「토사절취-혼합토사 [2025년 표준품셈3-2-3]」 ↔ 건설 「흙깎기(기계)」 3-2-3 ↔ 산림 「토사깍기 > 기계」 9-3-2 | [북부임도] 단가산출!R2346 · R2348 · [북부사방] 단가산출!R2757 · R2759 · 3-02_굴착.md:54 · 산림/제09장_토공/9-03_토사깍기.md:33 | 같은 일(토질 축만 다름) | +| 6 | 「흙깎기(연암)(굴삭기1.0㎥+대형브레이커)」 — 산출 첫 줄 「암절취 : 굴착기(무한궤도)(1.0+브레이카)」 ↔ 산림 「암절취」 9-4 ↔ TI 「암절취(리핑암)」 | [북부임도] 단가산출!R2370 · R2376 · 산림/제09장_토공/9-04_암절취.md:16 · TI/04_수량분석정보/토공_수량.md:19 | 같은 갈래 — 암 분류 경계가 체계마다 다름 | +| 7 | 「잔토정리(굴삭기0.6㎥)」 ↔ 「잔토처리(기계100%) - 굴삭기 0.8㎥」 ↔ 「구조물잔토처리 굴착기0.6m3」 | [북부임도] 단가산출!R2573 · [북부사방] 단가산출!R1663 · R1665 · [레시피]:342 | 같은 일 | +| 8 | 「구조물되메우기및다짐(대형장비,굴삭기0.6㎥)」 — 「[건설공사표준품셈 표준품셈3-4-7]대형장비」 ↔ 「구조물되메우기 및 다짐」 ↔ 「되메우기 및 다짐」 ↔ 건설 3-4-7 ↔ 산림 9-14 | [북부임도] 단가산출!R2294 · R277 · [북부사방] 단가산출!R263 · [경남2] 일위대가목록!R1721 · 건설/01_공통부문/제03장_토공사/3-04_쌓기.md:152 · 산림/제09장_토공/9-14_되메우기_및_다짐.md:16 | 같은 일 | +| 9 | 「양질사토운반」 ↔ 「사토운반(L=0.3㎞)」 ↔ 「사토처리」 · 「사토운반 L=409m」 | [북부임도] 내역서!R36 · [북부사방] 내역서!R12 · [레시피]:3160 · :3096 | 같은 갈래 — 거리 규격이 이름에 섞임 | +| 10 | 「레미콘타설(장비사용)」 — 출처 「건설표준품셈, 6-1-1 콘크리트 타설」 ↔ 건설 「레디믹스트콘크리트 타설」 6-1-1 ↔ 「레디믹스트콘크리트 장비사용 타설」 ↔ 「레미콘타설(장비) 무근」 · 「레미콘타설 무근(장비)」 ↔ 산림 12-1-1 | [북부임도] 일위대가!R7 · 단가산출!R1101 · 건설/01_공통부문/제06장_철근콘크리트공사/6-01_콘크리트.md:23 · [경남1] 일위대가목록!R407 · [레시피]:2448 · :2374 · 산림/제12장_철근콘크리트/12-01_콘크리트타설.md:18 | 같은 일 — 줄임말 · 괄호 자리 바뀜 · 출처 칸 이름도 본문 머리와 다름 | +| 11 | 「레미콘타설(인력운반)(무근구조물-소량타설)」 — 「2024년 품셈개정, 6-1-1콘크리트 타설」 ↔ 「레디믹스트콘크리트 인력운반 타설」 | [북부사방] 단가산출!R903 · R905 · [경남1] 일위대가목록!R405 | 같은 일 | +| 12 | 「합판거푸집(4회)」 — 「건설공사표준품셈, 6-3-1 합판거푸집」 ↔ 건설 「합판거푸집 설치 및 해체」 6-3-1 ↔ 「합판거푸집 설치 및 해체 보통 4회, 수직고 7m까지」 ↔ 산림 「합판거푸집」 12-4 | [북부임도] 단가산출!R1173 · R1175 · 건설/01_공통부문/제06장_철근콘크리트공사/6-03_거푸집.md:18 · [경남1] 일위대가!R1876 · 산림/제12장_철근콘크리트/12-04_합판거푸집.md:16 | 같은 일 — 사용 횟수·높이가 이름에 섞임 | +| 13 | 「문양거푸집」 [건설표준품셈6-3-4] ↔ 건설 「문양거푸집(판넬) 설치 및 해체」 6-3-4 ↔ 「문양거푸집 (판넬)」 ↔ 「문양 거푸집」 | [북부임도] 단가산출!R893 · R895 · 6-03_거푸집.md:208 · [경남1] 일위대가목록!R1425 · [레시피]:5787 | 같은 일 — 띄어쓰기 | +| 14 | 「큰돌메쌓기(40cm-60cm,(산림사업품셈))」 — 산출 「큰돌쌓기(메쌓기) (2026년 산림사업품셈 13-6-1)」 ↔ 「큰돌쌓기(메쌓기)(직경40cm이상~60cm미만)」 ↔ 산림 「큰돌쌓기 > 메쌓기」 13-6-1 | [북부사방] 단가산출!R2531 · R2533 · [북부임도] 단가산출!R1605 · 산림/제13장_구조물/13-06_큰돌쌓기.md:18 | 같은 일 — 어순 · 규격 표기 다름 | +| 15 | 「큰돌찰쌓기」 ↔ 「큰돌쌓기(찰쌓기)」 [산림사업표준품셈 13-6-2] ↔ 구조물 이름이 앞에 붙은 「큰돌흙막이(찰쌓기)」 · 「큰돌기슭막이(찰쌓기)」 | [북부사방] 단가산출!R2619 · R2621 · 일위대가!R129 · [북부임도] 단가산출!R1561 · R1563 · 일위대가!R73 · 13-06_큰돌쌓기.md:44 | 같은 공법 — 흙막이·기슭막이 호표는 공법 위 묶음이라 1:1 아님 | +| 16 | 「돌쌓기 / 메쌓기 / 깬돌」 ↔ 「깬잡석메쌓기」 · 「메쌓기(켜쌓기)」 ↔ 건설 「메쌓기」 7-1-1 ↔ 산림 「돌쌓기 > 메쌓기(인력)」 13-4-1 | [경남2] 일위대가목록!R3900 · [레시피]:6873 · :6699 · 건설/01_공통부문/제07장_돌공사/7-01_돌쌓기.md:18 · 산림/제13장_구조물/13-04_돌쌓기.md:18 | 같은 갈래 — 건설·산림 표 축 다름(TI/04_수량분석정보/건설품셈_활용.md:85) | +| 17 | 「떼붙임(평떼)(산림사업품셈)」 — 「떼붙임 [2026년 산림사업표준품셈(5-12)]」 ↔ 「비탈면보호공(평떼)」 — 「[건설표준품셈(4-1-1)]」 ↔ 건설 「잔디붙임」 4-1-1 ↔ 산림 「떼붙임(재배잔디)」 5-12 · 「평떼 > 평떼 붙임」 5-22-2 | [북부사방] 단가산출!R830 · R836 · [북부임도] 단가산출!R2899 · R2901 · 건설/01_공통부문/제04장_조경공사/4-01_잔디_및_초화류.md:18 · 산림/제05장_식재_및_녹화/5-12_떼붙임(재배잔디).md:16 · 5-22_평떼.md:22 | 같은 일 이름 — 출처가 산림·건설 두 갈래 | +| 18 | 「떼붙임(줄떼)」 ↔ 「떼심기(줄떼(평탄지용))」 ↔ 산림 「떼심기」 5-13 · 「줄떼 > 떼붙임」 5-23-2 | [사방안16] 단가산출!R6983 · R7060 · 산림/제05장_식재_및_녹화/5-13_떼심기.md:16 · 5-23_줄떼.md:22 | 같은 갈래 — 붙임·심기는 다른 품일 수 있음 | +| 19 | 산림 「씨앗뿜어붙이기 > 뿜어붙이기-기계/일반」 5-24-1 ↔ 건설 「초류종자 살포(기계살포)」 4-1-3 ↔ 건설 목 「식생기반제 뿜어붙이기」(3-7-4 안) ↔ 「식생기반제 뿜어붙이기(20m 미만)」 | 산림/제05장_식재_및_녹화/5-24_씨앗뿜어붙이기.md:18 · 4-01_잔디_및_초화류.md:53 · 건설/01_공통부문/제03장_토공사/3-07_비탈면_보호공.md:119 · [경남2] 일위대가목록!R1761 | 같은 갈래 — 재료·두께가 달라 같은 일 아닐 수 있음 | +| 20 | 「(☞ 터파기(기계))」 → 「기계터파기/보통토사/5M이하(육상)」 ↔ 건설 「터파기(기계)」 3-2-4 | [경남2] 일위대가목록!R1630 · R1631 · 3-02_굴착.md:126 | 같은 일 — 어순 뒤집힘 · 육상/용수 축 덧붙음 | + +#### 4-나-4. 철자 · 띄어쓰기 · 장비 · 직종 · 시설 이름 변이 + +- 깎기 ↔ 깍기 — 한 문서 안에서도 섞임: [북부임도] 단가산출!R2346 「흙깍기(토사)」 vs R2348 「흙깎기-보통토사」 · [경남2] 일위대가목록!R1595 「(☞ 흙깍기(기계))」 vs R1596 「흙깎기/보통토사」 · 산림 절 이름 자체가 「토사깍기」(산림/제09장_토공/9-03_토사깍기.md:16). +- 띄어쓰기 — 「구조물 되메우기」 ↔ 「구조물되메우기」([레시피]:5845 · :5817) · 「구조물 터파기(토사)」 ↔ 「구조물터파기(토사)」(:5839 · :5811). +- 오타·줄임 — 「브렐이커」([북부임도] 단가산출!R4039) ↔ 「브레이커」(R2370) ↔ 「브레이카」(R2376) ↔ 「브레카」([레시피]:3788). +- 단위 표기 — 「㎥」 ↔ 「m3」 ↔ 「M3」 ([레시피]:246 「굴삭기 0.7M3」 · :190 「0.2m3」). +- 장비 — 굴착기 ↔ 굴삭기 ↔ 유압식백호우 ↔ B/H ↔ 백호 (산림/제09장_토공/9-13_구조물터파기.md:18 · :28 · [레시피]:370 「대형브레카+B/H0.7㎥」 · 나-2 표) · 불도저 ↔ 불도우저 ↔ 도자 ([북부임도] 단가산출!R2374 · original/실무문서/_종합비교/03_토공_경사시공.md:37). +- 직종 — 목도 → 인력운반공 · 건설기계운전기사 → 건설기계운전사 · 운전사(운반차) → 화물차운전사 (original/원가계산/노임단가_건설업_대한건설협회/2. 임금적용요령.md:71-73) · 특수비계공 → 비계공 · 갱부 → 특별인부 (original/원가계산/노임단가_직종대응_근거.md:65 · :67) · STmate 직종 사전 106종에 건설기계운전기사·건설기계운전사·중기운전사가 함께 있음 (ST/20_분석/28_변환_수량산출_식언어.md:90-100). +- 공종 이름 — 실무 「지장목제거」 ↔ 「벌개제근」 ↔ 「제근」 (original/실무문서/05. 동부청 영월/_분석보고서.md:18 · TI/05_원가정보/원가_출력변수_사전.md:55 · 건설/01_공통부문/제03장_토공사/3-09_벌개제근.md:16 · 산림/제09장_토공/9-21_제근.md:16) · 실무 「토사절취」(울진 2공구) ↔ 「절토」(3공구) ↔ 품셈 「토사깍기」 (original/실무문서/2024년 간선임도(기번3-울진.대흥)(2공구)/_분석보고서.md:18 · (3공구)/_분석보고서.md:17). +- 시설 이름 — 「관보호공」 ↔ 실무 「동물이동통로 돌집수정」 (original/실무문서/_종합비교/04_임도구조물_원단위.md:76 · original/실무문서/2024년 간선임도(기번3-울진.대흥)(공통)/_분석보고서.md:68). +- 암 이름 — 「연암」(울진 1·3공구) ↔ 「브레카암」(2공구) ↔ 「암절취」(산림) ↔ 「리핑암」(TI) (original/실무문서/_종합비교/03_토공_경사시공.md:17-19 · TI/03_계산정보/토량계산.md:90). **같은 뜻 말로 묶지 말 것** — KDS 는 연암을 발파암 쪽에 둠(토량계산.md:90), 실무는 연암을 브레이커 암으로 씀([북부임도] 단가산출!R2370). + +--- + +### 4-다. 해마다 조항 번호가 바뀐 이력 + +#### 4-다-1. 산림(임도·사방) 품셈 — 판마다 번호 전부 바뀜 + +- 판 흐름: 2017 임도표준품셈 → 2018 통합 산림사업 표준품셈 → 2024 개정판 → 2026 산림청고시 제2025-82호(제정 · 2026-01-01 시행) (ST/20_분석/33_임도품셈_판_대응표.md:11 · original/원가계산/산림_표준품셈/현행_20260101.md:3). 2026 고시 전에는 임도 품이 「임도설치 및 관리 등에 관한 규정」 등 여러 지침 안에 있었음(현행_20260101.md:21 부칙 경과조치). +- 대응표 = STmate 레시피가 인용한 14건만: 확정 13 · 미확인 1 (ST/20_분석/33_임도품셈_판_대응표.md:13-17 · ST/30_원자료/임도품셈_판_대응표.csv). + +| 공종 | 2017 | 2018 | 2024 | 2026 현행 | 근거 | +|---|---|---|---|---|---| +| 층따기 | 3-7 | 4.25 | 8-19 | 9-18 | 임도품셈_판_대응표.csv:13 | +| 제근 | 2-3 | 4.20.2 | 8-22 | 9-21 | csv:11 | +| 토사깎기(굴삭기) | 3-3-1 | 4.3.2 | 8-2 | 9-3-2 「기계」 | csv:14 | +| 임도성토면 다짐 | 3-6 | 4.24 | 8-18 | 9-17-1 「비탈면 다짐」(이름도 바뀜) | csv:7 | +| 구조물터파기 육상 암 0~1m | 5-1-2 사 | 4.27.2 | 8-12 사 | 9-13-7 | csv:3 | +| 측구터파기 암절취 | 5-1-1 나 | 4.27.1 나 | 8-11 나 | 9-12-2 | csv:12 | +| 쇄석·혼합석 부설 | 7-2(선행) | 4.39 | 10-4 | 11-4 | csv:15 | +| 선떼붙이기공 | 선행 사방품셈 4-2-2 | 4.16.2 | 4-16 | 5-16 / 5-16-1 단끊기 / 5-16-2 선떼붙이기 (일대다) | csv:5 | +| 암절취 | 3-3-2 | 4.3.3 | 8-3 | 9-4 / 9-4-1 암파쇄 / 9-4-2 집토 (일대다) | csv:10 | +| 노면다짐 | 7-3 | 4.40 | 목차에 없음 | 미확인(연결 금지) | csv:4 | + +- 번호 꼴도 바뀜 — 2018 은 점 표기(4.3.2), 2024·2026 은 줄표(8-2 · 9-3-2). +- 사용 경계 — 일대다는 대표 하위 절로 자동 치환하지 않음 · 절 대응 확정 ≠ 값 복사 허용 (33_임도품셈_판_대응표.md:26-30) · 「번호만 같은 다른 절을 자동 연결하지 않는다」(ST/20_분석/32_특수산식과_현행품셈_대조.md:51). +- 실무 인용의 실태 — STmate 레시피 350 중 「근거 미기재」 107 · 「근거문구 있으나 현행 위치 미확인(과거 번호 개편·복수 절)」 69 · 「명칭으로 후보 절만」 161 · 「현행 절 확인·수동 대조」 11 · 「원문 직접 일치」 2 (32_특수산식과_현행품셈_대조.md:30-37). 같은 문서 :50 은 「근거문구 보유 82개」라 적어 표와 셈 기준이 다름 — 확인 필요. +- 2017·2018·2024 판 원문 PDF 는 저장소 안에서 찾지 못함(33_임도품셈_판_대응표.md:34-36 이 이름만 적음) — 확인 못 함. 2016 사방사업표준품셈(안) xls 는 있음([사방안16]). + +#### 4-다-2. 건설 품셈 — 해마다(12월 말) 새 판 · 끼워 넣기로 뒤 번호가 밀림 + +- 갱신 주기 = 연간, 매년 12월 말 익년도판 (original/원가계산/건설공사_표준품셈/_meta.md:6). +- [개정26] 「현행 ↔ 개정(안)」 대조표에서 번호가 바뀐 줄(같은 줄에 두 번호가 찍힌 것만 셈 — 최소치): + +| 현행(2025) | 2026 | 인쇄쪽 | 2026 본문 확인 | +|---|---|---|---| +| 공통 2-7-7 비계용 브라켓 설치 및 해체 | 2-7-9 (2-7-3 · 2-7-5 신설로 밀림) | 16 | 건설/01_공통부문/제02장_가설공사/2-07_비계.md:190 | +| 공통 2-7-2 시스템비계 설치 및 해체 | 2-7-2 「…(인력)」 + 새 2-7-3 「시스템비계 설치 및 해체」 — **옛 이름이 다른 항이 됨** | 14 · 15 | 2-07_비계.md:39 · :62 | +| 공통 5-1-3 H-Beam 철거 | 5-1-4 H-Beam 해체 (번호·이름) | 32 | 건설/01_공통부문/제05장_기초공사/5-01_흙막이_및_물막이.md:81 | +| 공통 5-1-4 흙막이판 설치・철거 | 5-1-6 흙막이판 설치・해체 | 33 | 같은 파일:125 | +| 공통 5-1-5 어스앵커 공법 | 5-1-7 | 34 | 같은 파일:149 | +| 건축 10-4-1 알루미늄 프레임 설치 | 10-1-8 커튼월 프레임 설치 | 124 | 건설/03_건축부문/제10장_창호_및_유리공사/10-01_창호.md:144 | +| 건축 10-4-2 외벽 패널 설치 | 8-2-6 금속패널 설치 (장 이동) | 125 | 건설/03_건축부문/제08장_금속공사/8-02_시설물.md:125 | +| 건축 10-4-3 코킹 | 8-2-7 금속패널 코킹 | 125 | 같은 파일:146 | +| 유지관리 2-1-12 차선도색 | 2-1-15 | 149 | 건설/05_유지관리부문/제02장_토목/2-01_도로포장공사.md:467 | + +- [개정26] 구분 칸 어림 — 보완 119 · 신설 31 · 적정검토 23 줄 (pdftotext 줄머리 셈). +- 2026 본문 항(###) 1,095 중 개정 표지 있는 항 837 · 「'24년 신설」 18 · 「'25년 신설」 27 · 「'26년 신설」 26 (머리말 셈). +- 토공 2025 개편 — 3-2-3 흙깎기(기계) · 3-2-4 터파기(기계) · 3-4-1 흙쌓기 · 3-4-3 흙 다지기 · 3-4-6/3-4-7 되메우기 및 다짐 전부 「'25년 신설」 (3-02_굴착.md:54 · :126 · 3-04_쌓기.md:18 · :71 · :132 · :152). 2024 이전 토공 번호는 옛 판이 저장소에 없어 확인 못 함. +- 실무 출처 칸의 판 표기도 제각각 — 「[2025년 표준품셈3-2-4]」([북부사방] 단가산출!R338) · 「2024년 품셈개정, 6-1-1」(R905) · 「[2026년 건설표준품셈6-3-4]」(R1121) · 「[건설표준품셈6-3-4]」(연도 없음 · [북부임도] 단가산출!R895) · 「[임도표준품셈의 발파암 기준 적용]」(옛 품셈 이름 · R1522) · 「< 공통품셈 8-」(다산 · original/원가계산/다산소프트/ESTX_값사전_견적내역.md:37). +- 삭제 표지 — 경남교육청 이름 뒤 「23-1/4 삭제」 · 「24-1/4삭제」 ([경남1] 일위대가!R1681 · R1956). +- 노임 직종 코드도 판마다 바뀜 — 145→117(2010) → 123(2018) → 127(2020-09) → 132(2025-01) · 코드 3001~3018 은 같은 번호로 뜻이 바뀜(문화재목공 → 한식목공) (TI/05_원가정보/원가_기초데이터_원천.md:148 · :154). +- 조달청 호환규정 — 내역공종코드 16 → 20자(2017-04-19) · 규정 코드 반기마다 갱신 (ST/20_분석/30_조달청_호환규정_XSD.md:30 · :28). + +#### 4-다-3. 대응표를 맞춤 열쇠로 쓸 수 있나 + +- 산림 판 대응표 — 14건뿐(STmate 인용분) → 「옛 번호 → 현행 번호」 바꿈표의 씨앗 수준. 출처 칸이 옛 번호를 인용한 실무 엑셀에만 쓸모. 전 항목으로 넓히려면 2017·2018·2024 판 원문이 필요(저장소에 없음). +- 건설 개정사항 — [개정26] 에서 「현행 ↔ 개정」 줄을 뽑으면 한 해 치 바꿈표가 됨. 2026 판만 있어 2025 → 2026 한 단계만 가능. 해마다 받아 쌓아야 함. +- 열쇠 꼴 — 「품셈 갈래(건설·산림) + 부문 + 판 연도 + 번호」 넷이 있어야 이어짐 (TI/04_수량분석정보/건설품셈_활용.md:54 · :131). 실무 출처 칸은 판 연도·부문을 자주 빼먹음(위 실무 표기 예). + +--- + +### 4-라. technical_info 등 — 이름 · 조항 대응 · 같은 뜻 말을 다룬 문서 + +| 문서 | 담긴 것 | 쓰임 | +|---|---|---| +| TI/04_수량분석정보/건설품셈_활용.md | 두 품셈 연결 원칙(:50 이름만으로 자동 연결 금지) · 부문별 번호 재시작(:54) · 잇지 못하는 갈래 표(:83-89) · 산림 목차 번호 오기(:93-100) · 품셈 자료 요건 = 공종 코드 + 출처(산림/건설 + 장·절 + 판 연도)(:131) | 맞춤 규칙 근거 | +| TI/04_수량분석정보/토공_수량.md | 토공 공종 ↔ 산림 절 대응(:16-31) · 구조물터파기 3축 18구분(:33-41) · 용어 차이 해석(:54) | 토공 이름 축 사전 — 단 근거 줄(L…)이 지운 옛 md 를 가리킴(:7 · technical_info/_index.md:8) | +| TI/05_원가정보/원가_출력변수_사전.md | 우리 공종 79 ↔ 산림 절 (B 절 :47~) | 사전의 「우리 쪽」 이름 목록 | +| TI/05_원가정보/산림_건설_미명시7건_대응매핑.md | 산림이 「건설품셈 준용」만 적은 7건의 건설 절 대응 (:12-) | 두 품셈 사이 다리 | +| TI/05_원가정보/연결고리_건설공종_6유형_분류.md | 연결 64건 · 고유 건설 절 42 (:7 · :13-20) | 참고 — 「공통 3-6-1 터파기」(:20 · :68)가 2026 본문 3-6-1 「성토면 고르기」(건설/01_공통부문/제03장_토공사/3-06_성토부대공.md:18)와 어긋남 · 판정 안 함 · 사용자 확인 필요 | +| TI/05_원가정보/원가_기초데이터_원천.md:144-157 | 직종·자재·기계 판 교체 때 「번호가 같아도 뜻이 바뀜」 경고 | 코드 열쇠의 한계 근거 | +| TI/03_계산정보/토량계산.md:89-90 · :108 | 토질·암 분류 두 체계(품셈 vs KDS) | 암 이름은 같은 뜻 말 아님 근거 | +| original/원가계산/노임단가_직종대응_근거.md | 품셈 옛 직종명 → 현행 직종 대응표(:50-69 등) | 직종 사전 그대로 | +| original/원가계산/노임단가_건설업_대한건설협회/2. 임금적용요령.md:66-76 · :78 · :86 · :93 | 2010 직종 명칭변경표 · 2018 · 2020 · 2025 신설직종 | 직종 사전 | +| original/원가계산/노임단가_제조업_중소기업중앙회/직종코드_변천_대조표.md | 제조 직종 코드 변천(151줄) | 이번에 자세히 안 읽음 | +| ST/20_분석/31_일위대가_레시피_사전.md + [레시피] | 실무 6건 일위대가·단가산출·중기 조합 350(이름 · 규격 · 단위 · Q식 · 품셈근거 · 구성) (:48-56 · :118-134) | 「같은 일 · 다른 이름 · 같은 구성」 학습·시험 자료 | +| ST/30_원자료/stmate_code_name_list.csv | 자원 코드 894 ↔ 이름·규격·단위 | 자원 이름 사전 후보 | +| ST/20_분석/22_UI_폼_분석.md:116-124 | STmate 기준자료 갱신 매칭 키 5단계 — 호환규정코드 / 시설코드 / 명칭·규격·단위 / 호환+명+규+단 / 명칭·단위 (퍼지 없음) | 경쟁사도 「코드 → 이름·규격·단위 일치」 순으로 이음 | +| ST/20_분석/30_조달청_호환규정_XSD.md + ST/30_원자료/조달청_공사원가호환규정.xsd | 세부공종명·규격·단위 · 노임산출근거 안 「공량품셈근거」 칸(xsd:637) · 실적공사비코드 `[ACEMT]+[0-9A-Z]*`(xsd:1241) · 일위대가·자재·노무 코드는 영숫자 12~16자 형식만 정함(xsd:1244-1271) | XML 납품본이면 품셈 근거 칸이 있음 · 값 목록(국가 공통 코드 여부)은 확인 못 함 | +| original/원가계산/다산소프트/ESTX_분석.md:26-27 | 다산 ESTX 안 단가산출 제목 1,569 · 일위대가 제목 1,468 | 이름 목록 md 없음 · 원본 .estx 를 풀어야 함(이번에 안 엶 · 확인 못 함) | +| technical_info/02_사방댐/05_원가정보/01_원가_특이사항.md | 사방 원가 특이사항(47줄) — 이름·조항 대응 없음 | 해당 없음 | +| 같은 뜻 말 전용 문서 | 없음 — 「동의어·같은 뜻·별칭·유의어·alias」 검색 결과는 사방 대상지 「별칭」 한 줄뿐(technical_info/02_사방댐/01_대상지_타당성평가/03_대상지조사_시공대상지.md:19) | — | + +--- + +### 4-마. 판정 — 맞춤 사전 재료로 + +| 재료 | 판정 | 한 줄 이유 | +|---|---|---| +| 건설·산림 품셈 본문 머리말(항 이름 1,095 + 산림 274 등) | 바로 쓸 만함 | 규칙 있는 md 머리말 · 정규식으로 뽑힘 · 윗머리 잇기·부문 붙이기만 | +| 건설 8장 기종 코드 머리 156 | 바로 쓸 만함 | 장비 줄 구성 비교의 열쇠(코드 + 이름) | +| 직종 대응표(직종대응_근거 · 2010 명칭변경표) | 바로 쓸 만함 | 원문 근거 있는 옛 이름 → 새 이름 짝 | +| 장비 이름 변이(굴착기·굴삭기·백호우·B/H · 브레이커·브레이카 · 불도저·도자) | 바로 쓸 만함 | 짧은 손사전 · 원문·실무 근거 있음 | +| 철자·띄어쓰기·단위 표기 고르기(깎/깍 · 뒤/뒷채움 · ㎥/m3/M3 · 띄어쓰기) | 바로 쓸 만함 | 규칙 몇 줄 | +| 품셈 [주]·비고 속 다리 문구(3-2-1 주① 등) · KCS 제목 병기 | 조건부 | 손으로 골라 담아야 함 · 수십 건 규모 | +| 실무 출처 칸 인용 문자열(「[건설공사표준품셈 3-2-1 인력굴착]」 꼴) | 조건부 | 번호는 강한 열쇠이나 판 연도·부문 표기가 제각각 · 옛 번호·옛 품셈 이름 섞임 · STmate 레시피 350 중 107 은 근거 없음 | +| 실무 이름 짝(북부청 2건 · 경남 2건 · STmate 레시피 350) | 조건부 | 같은 일·다른 이름 학습·시험 자료로 좋음 · 사용자 확정 뒤에만 사전에 넣음 | +| 산림 판 대응표(14건) | 조건부 | 씨앗 수준 · 전 항목 아님 · 옛 판 원문 없음 | +| 건설 개정사항 PDF 현행↔개정 줄 | 조건부 | md 없음 · 2026 판만 · 해마다 받아 쌓아야 바꿈표가 됨 | +| 조달청 호환규정 XSD 코드·품셈근거 칸 | 조건부 | XML 납품본에만 해당 · 이번 실무 엑셀 5건에선 공종 코드 칸을 못 봄(경남만 자체 10자리 공종 코드가 이름 칸 앞에 붙음 — [경남1] 공종별집계표!R50-R60) · 엑셀 열 전체는 안 봄(확인 못 함) | +| 다산 ESTX 일위대가·단가산출 제목 | 조건부 · 확인 못 함 | 이름 목록이 md 로 풀려 있지 않음 | +| 토질·암 분류 이름(연암·리핑암·브레카암·발파암) | 안 맞음 | 체계마다 경계가 달라 같은 뜻 말로 묶으면 틀림 — 「축」으로 따로 다룸 | +| 같은 뜻 말 전용 문서 | 안 맞음 | 지식DB 에 없음 | + +덧붙임(범위 밖 한 줄): 저장소 venv 에 xlrd · openpyxl · pandas 있음 · rapidfuzz · kiwipiepy · scikit-learn 없음(import 확인만). Git Bash 에 pdftotext 있음. + +--- + +## 5. 판정 표 — 우리 조건 기준 + +조건 = 오프라인 사내 서버 · Python 3.12 · FastAPI · 무거운 새 의존성 싫음 · 사용자가 결과를 고르는 도구 · 자동 확정 없음. +판정 = 바로 쓸 만함 / 조건부 / 안 맞음 · 품 = 작음 · 중간 · 큼(어림) · 근거 절 = 이 문서 절 번호. + +| # | 갈래 | 방법 | 판정 | 까닭(한 줄) | 품 · 새 의존성 | 근거 절 | +|---|---|---|---|---|---|---| +| 1 | 사전 | M01 로직 「별칭」 을 맞춤이 읽게 함 + 채움 | 바로 쓸 만함 | 그릇 · 화면 · 층이 이미 있음 · 마스터 1,469 줄 모두 빔 · M02 맞춤은 안 읽음 | 작음 · 없음 | 3-0 | +| 2 | 사전 | 같은 뜻 말 · 쪼개기(구조물터파기 → 구조물 + 터파기) · 덮는 말(보통토사 ⊂ 토사) 표 — 재료 = 품셈 [주] 다리 문구 · 조달청 옛/새 이름 · 실무 짝 20 · 씨앗 메모 170 | 바로 쓸 만함 | 글자가 하나도 안 겹치는 짝을 잇는 유일한 값싼 길 · 근거가 눈에 보임 | 작음 · 없음 | 3-2 · 4-나 · 1-4 | +| 3 | 정규화 | 표기 고르기 — 깎/깍 · 뒤/뒷채움 · ㎥/m3/M3 · 굴삭기/굴착기/백호우/B/H · 브레이커/브레이카/브렐이커 · 불도저/불도우저/도자 · 띄어쓰기 | 바로 쓸 만함 | 한 문서 안에서도 섞임 · 규칙 몇 줄 | 작음 · 없음 | 4-나-4 · 3-2 | +| 4 | 정규화 | 괄호 글을 지우지 않고 칸(방법 · 토질 · 규격 · 비율)으로 옮김 | 바로 쓸 만함 | 지금은 괄호를 지워 「인력/토사」 를 잃음 | 작음 · 없음 | 3-0 · 3-2 | +| 5 | 칸 맞춤 | 칸별 같음 · 어긋남 매김 · 방법 · 토질이 어긋나면 크게 깎음 · 화면에 칸별 까닭 | 바로 쓸 만함 | 이름 비슷한 다른 일(기계 터파기)을 가름 · 국내 연구도 〈대상〉+〈작업〉 구조 | 중간 · 없음 | 3-1 · 3-2 | +| 6 | 증거 | 구성 비교 — 직종 · 장비 · 자재 묶음 · 「보통인부뿐」 신호 · 직종 옛/새 이름 표 · 건설 8장 기종 코드(156) | 바로 쓸 만함 | 이름과 무관한 증거 · 시중 STmate 「산근내용」 검색과 같은 방향 · 엑셀 · 우리 로직 둘 다 이미 읽음 | 중간 · 없음 | 2-3 · 3-2 · 4-라 | +| 7 | 학습 | 확정 짝 → 별칭 후보 쌓기 → 관리자 승인 → 사전 | 바로 쓸 만함 | 지금 기억은 품명 · 규격 · 단위가 똑같을 때만 · 일반화가 빠짐 | 작음 · 없음 | 3-2 | +| 8 | 단계 | 정확 일치 열쇠 단계 내리기 — 정규화 · 칸 쪼개기 뒤 순서 무관 열쇠 → 명+규+단 → 명+단 | 바로 쓸 만함 | STmate 갱신 매칭과 같은 방식 · 오탐 적음 · 설명 쉬움 | 작음 · 없음 | 2-3 · 3-2 | +| 9 | 점수 | 글자 2~3 그램 TF-IDF(흔한 묶음 가볍게) | 바로 쓸 만함(작은 이득) | 지금 Dice 대체 · 표준 라이브러리로 됨 · 같은 뜻 말 문제는 못 풂 | 작음 · 없음 | 3-2 | +| 10 | 사람 | 후보 + 사람 고름 + 층 기억(개인 → 회사 → 마스터) | 이미 있음 — 유지 | 시중 프로그램 · 조달연 · 국내 논문 모두 이 길 | — | 2 · 3-3 | +| 11 | 코드 | 단가산출 출처 품셈 조항 번호(지금 근거 길) + 판 짐작 + 부문 넣기 + 옛 번호 바꿈표 | 조건부 | 번호는 강한 열쇠 · 실무 글은 판 · 부문을 자주 뺌 · 판마다 번호 바뀜 · 바꿈표 재료는 산림 14 건 · 건설 2025 → 2026 한 단계뿐 | 중간 · 없음 | 1-1 · 1-5 · 4-다 | +| 12 | 코드 | 조달청 세부공종코드 CSV(33,491 행 · 무료 · 오프라인)를 사전 재료로 | 조건부(사전 재료로는 바로 쓸 만함) | 옛/새 이름 묶음 · 품셈 꼴 이름(S 코드) · 임도 · 사방 고유 공종 없음 | 작음 · 없음(CSV 한 벌) | 1-4 | +| 13 | 코드 | C3R XML 입력(일위대가코드 · 자원코드 · 공량품셈근거 칸) | 조건부 | XML 을 받을 때만 · 실제 납품 XML 에 표준공사코드가 실리는지 확인 못 함 | 중간 · 없음 | 1-4 | +| 14 | 코드 | 자원 줄 국가 호환코드(JDCODE) · 조달 8 자리 물품식별번호 | 조건부 | STC 엔 노임 · 중기 · 경비 대부분 있음 · 엑셀엔 인쇄 설정에 달림 · 조달 가격 자재 줄만 번호 보임 | 작음 · 없음 | 1-7 · 2-3 | +| 15 | 코드 | STC 파일 직접 읽기 | 조건부 | 엑셀보다 열쇠가 많음 · 발주처가 STC 를 줄 때만 | 중간 · 없음 | 2-5 | +| 16 | 증거 | 금액 대조(노무비 몫 · 같은 해 단가) | 조건부 | 옛 판 값 · 할증 · 지역 단가가 섞임 — 동점 가르기 · 경고만 | 중간 · 없음 | 3-2 | +| 17 | 점수 | Jaro-Winkler · rapidfuzz · 자모 분해 | 조건부 | 보조 점수 · 국내 연구 1순위 정답률 50 % 아래 · 예시에서 오답을 더 높게 봄 | 작음 · rapidfuzz 만 새 꾸러미(작음) | 3-1 · 3-2 | +| 18 | 점수 | 형태소 분석(kiwipiepy · mecab-ko) | 조건부 | 둘 다 Windows · 3.12 바퀴 있음 · 짧은 전문 복합어는 도메인 사전이 먼저 | 중간 · 새 꾸러미 + 모델 | 3-2 | +| 19 | 학습 | 불확실한 줄 먼저 보이기 · 칸 무게 학습(m/u) | 조건부 | 확정 짝이 쌓인 뒤 | 중간 · 없음 | 3-2 | +| 20 | 사전 | 국토교통부_건설용어사전 CSV(6,666 용어) | 조건부 | 동의어 칸 없음 · 정의문에서 손으로 뽑아야 함 | 중간 · 없음 | 3-2 | +| 21 | 점수 | 문장 임베딩(multilingual-e5-small · ko-sroberta) | 조건부(보류) | 수백 MB · 전문 용어 이해 미확인 · 1~9 뒤 남은 실패를 보고 시험 | 큼 · 새 실행 꾸러미 + 모델 | 3-2 | +| 22 | 코드 | 표준시장단가 코드 | 안 맞음 | 100억 원 이상 공사용 · 실무 엑셀 0 건 | — | 1-2 | +| 23 | 코드 | 건설정보분류체계(공종분류) | 안 맞음 | 실무 활용 「극히 저조」 · 실무 엑셀 0 건 | — | 1-3 | +| 24 | 코드 | 물가지(한국물가정보 · 물가자료) 자재 코드 | 안 맞음 | 엑셀엔 쪽 번호뿐 · 품목 코드는 유료 · 비공개 | — | 1-6 | +| 25 | 코드 | 일위대가 · 단가산출 머리를 국가 코드로 잇기 | 안 맞음 | 실무 STC 일위대가 0/216 · 단가산출 0/371 | — | 2-3 | +| 26 | 코드 | STmate · 다산 내부 코드 | 안 맞음 | 프로젝트 안 순번 — 같은 코드가 프로젝트마다 다른 항목 | — | 2-3 | +| 27 | 사전 | 암 이름(연암 · 리핑암 · 브레카암 · 발파암)을 같은 뜻 말로 묶기 | 안 맞음 | 체계마다 경계가 다름 — 따로 「축」 으로 다룸 | — | 4-나-4 | +| 28 | 점수 | 큰 언어 모델 재순위 | 안 맞음(지금 조건) | 인터넷 · 자료 외부 전송 필요 · 로컬 모델은 큼 · 라이선스 걸림 · 돈은 싸서 걸림돌 아님 | — | 3-2 | +| 29 | 점수 | 신경망 분류 학습(BERT 미세조정 등) | 안 맞음 | 5 만 줄 규모 학습 자료가 전제 · 우리는 적음 | — | 3-3 | + +### 5-1. 묶어 보면 — 차례 제안(정할 것은 사용자) + +| 단계 | 묶음 | 기대 | +|---|---|---| +| 1 | 1 · 2 · 3 · 4 · 7 (별칭 · 같은 뜻 말 · 표기 고르기 · 괄호 칸 · 확정 짝 → 별칭) | 「글자가 안 겹쳐 후보에도 못 듦」 을 없앰 — 새 의존성 없음 | +| 2 | 5 · 6 · 8 (칸 어긋남 · 구성 비교 · 열쇠 단계) | 「이름 비슷한 다른 일이 이김」 을 가름 · 화면에 칸별 까닭 | +| 3 | 9 · 11 · 12 (TF-IDF · 조항 바꿈표 · 조달청 CSV) | 순위 다듬기 · 옛 판 엑셀 대응 | +| 4 | 실측 뒤 남은 실패를 보고 16 ~ 21 중 고름 | 무거운 것은 효과가 보일 때만 | + +--- + +## 6. 조사끼리 어긋난 셈 · 확인 못 한 것 + +### 6-1. 어긋난 셈(이 정리에서 다시 봄) + +| 무엇 | 1 절 | 4 절 | 다시 본 결과 | +|---|---|---|---| +| 건설 2026 본문 항(###) 중 「'25년 신설」 수 | 처음 셈 22 | 27 | **27** — 곧은 따옴표 22 + 굽은 따옴표(‘) 5(4-1-2 · 4-4-5 · 6-1-7 · 2-1-12 · 2-1-14). 처음 셈은 굽은 따옴표를 빠뜨림 — 1 절 본문은 27 로 고쳐 둠. 처음 셈의 「'25 꼬리표 135」 는 꼬리표 꼴(「25년 보완」 따옴표 없음 등)이 섞여 다시 세지 않음 — 뺌 | +| `2026년_건설공사_표준품셈_개정사항.pdf` 읽힘 | 처음엔 「글자층이 깨져 읽지 못함」 | pdftotext 로 본문 뽑아 번호 바뀜 9 건 대조 | **본문은 읽힘** — 뽑은 글(스크래치패드 `req225/rev2026.txt`)에서 「2-7-7 비계용 브라켓 … → 2-7-9」 「5-1-2 H-Beam 설치 → 5-1-2 H-Beam 설치(띠장)」 줄이 그대로 보임 · 표지 글자만 겹침. 1 절 본문은 「공개처에서는 못 찾음 · 2025 → 2026 한 단계는 이 PDF 에서 뽑힘(4-다-2)」 으로 고쳐 둠 | +| STmate 레시피 근거 셈 | — | 「근거 미기재 107」 vs 같은 문서 「근거문구 보유 82」 | 셈 기준이 다름 — 확인 필요(4-다-1) | +| 「공통 3-6-1 터파기」(TI 연결고리 문서) | — | 2026 본문 3-6-1 = 「성토면 고르기」 | 판정 안 함 · 사용자 확인 필요(4-라) | + +### 6-2. 확인 못 함(모음) + +- STmate `CONV_XLS_2_ST` 가 엑셀 행을 무슨 열쇠로 잇는지(사용설명서 정회원 전용) · 다산 엑셀 → EST 변환이 기능인지 사람 대행인지 · 나스 SGS · Easy 2.0 엑셀 읽기 뒤 이름 맞춤 방식 · 콘엑스 AI 검증 방법 · 성능(2-6). +- 조달청 2025-05 개편 코드에 토목 일위대가 · 단가산출 코드가 생겼는지(npccs.g2b.go.kr 본문 빔) · 실제 납품 C3R XML 에 표준공사코드가 실리는지(저장소에 XML 견본 없음)(1-4). +- 건설 품셈 2025 이전 판 번호 대응(옛 판 원문 · 공개 대응표 못 찾음) · 산림 2017 · 2018 · 2024 판 원문(저장소에 없음)(1-1 · 4-다). +- 건설정보분류체계 고시 원문 · 공종 코드 자릿수(법제처 오류 페이지)(1-3) · 물가지 품목 코드 체계(유료)(1-6) · 실무 엑셀 8 자리 번호의 실제 물품식별번호 대조(1-7). +- Lee 외(2023) 국도 내역서 공종 코드 논문 원문(ASCE 403 — 초록 수치 91.12 % 는 검색 요약으로만)(3-3). +- 형태소 분석기 · 임베딩 모델이 「구조물터파기」 「깬잡석찰쌓기」 같은 이름을 어떻게 다루는지(설치 금지로 시험 못 함) · EXAONE GGUF 크기(3-2). +- 다산 내부 코드의 판 사이 안정성 · ESTX 일위대가 · 단가산출 제목 목록(.estx 안 엶)(2-3 · 4-라). + +## 부록 · 이 조사의 스크립트 · 받은 자료(스크래치패드 `req225/` · 저장소 밖) + +| 파일 | 무엇 | +|---|---| +| `tools/demo_sim.py` | 3-1 예시 계산(표준 라이브러리만) | +| `paper_kjcem2020.txt` | 정상원 · 정기창(2020) 논문 글자(3-3) | +| `p1_scan_codes.py` · `p1_seb_stats.py` · `p1dl_datagokr/seb.csv` | 실무 엑셀 코드 훑기 · 조달청 세부공종코드 33,491 행(1 절) | +| `p2_codecheck.py` → `.txt` · `p2_jdcheck.py` → `.txt` | STmate 코드 충돌 · JDCODE 채움 실측(2 절) | +| `scan_names.py` → `scan_out_u8.txt` · `rev2026.txt` | 실무 엑셀 이름 훑기 · 2026 개정사항 PDF 글자(4 절) | diff --git a/docs/raw/조사/2026-10-15_336_외부일위대가_이름맞춤_검토.md b/docs/raw/조사/2026-10-15_336_외부일위대가_이름맞춤_검토.md new file mode 100644 index 000000000..c218a20b9 --- /dev/null +++ b/docs/raw/조사/2026-10-15_336_외부일위대가_이름맞춤_검토.md @@ -0,0 +1,323 @@ +# 외부 일위대가 이름 맞춤 — 상세 검토 (요구 225-4 · PLAN 336) + +M02 「외부 일위대가 가져오기」에서 이름이 달라 못 맞추는 문제의 개선 방법 검토. 코드 고침 없음 — 어떤 개선을 코드로 만들지 고르는 판단 자료. +재료 = C1 실측(우리 엑셀 3 파일 · 정답 표시 136 줄) · C2 바깥 조사(시중 프로그램 · 표준 코드 · 기법 · 지식DB) · 324 · 329-330 조사 · C3 덧잼(C1 함수로 몇 가지 더 잼). +C1 = 같은 폴더 `2026-10-15_336_부록1_실측.md` · C2 = `2026-10-15_336_부록2_바깥방법.md` · 실험 스크립트(C1 · C3 · `req225/` 경로)는 브레인 스크래치패드 임시 — 수치는 이 문서 · 부록에 남김. +기준 = main_laptop_1 04021d1f7(맞춤 코드는 C1 기준 31e6adf7d 와 같음 — 그 뒤 바뀐 것은 메뉴 화면 두 파일뿐) · 수치는 모두 「기억 없음」(확정한 것 없는 첫 올리기) 상태. + +## 0. 결론 요약 + +1. 이름이 달라 못 맞추던 문제는 330(엑셀 단가산출의 품셈 조항 · 직종으로 맞춤)으로 이미 크게 줄었음 — 세 파일 126 줄 기준 1순위 정답 41% → 71% · 후보 다섯 안 정답 60% → 87%(엄격 기준). +2. 남은 1순위 실패 36 줄 = 산림 ↔ 건설 책 갈림 11 · 현장말(고임돌 · 소운반 · 철골설치) 11 · 우리 로직이 더 잘게 5 · 그 밖 9 — 줄임말 · 띄어쓰기 · 낱말 차례처럼 글자 꼴만 다른 실패는 0. +3. 이름 글자를 더 다듬는 방법(같은 뜻 사전 · 글자 비교 바꾸기 · 괄호 살리기 · 단위 거르기)은 우리 자료로 재 보니 효과 0 이거나 오히려 떨어짐. +4. 가장 싸고 큰 개선 = 근거 쪽 서버 고침 넷 → 1순위 71% → 86% · 다섯 안 87% → 94% · 「같음」으로 엑셀과 다른 책이 들어가던 10 줄 → 0. +5. 손으로 만든 같은 뜻 사전은 안 맞음 — 회사(발주처)마다 같은 일에 쓰는 책이 달라 한 파일로 만든 사전이 다른 꼴 문서에서 −9 점 · 지금 맞춤에 얹으면 −7 점. +6. 사전 대신 「사용자 확정이 회사 층에 쌓이는 맞춤 기억」을 넓힘 — 열쇠를 품명 · 단위로 넓히면 같은 발주처 다음 파일에서 바로 1순위로 뜨는 엑셀 줄 5% → 33%(모두 맞음) · 다른 발주처 기억은 62~71% 만 맞아 「후보 꼬리표」로만 · 원본 로직 · M01 별칭은 안 건드림. +7. 다섯 안 정답이 94% 라 그다음은 「사람이 빨리 · 바르게 고르기」 — 후보마다 책 · 조항 · 직종 · 금액 비(±5% 안이면 95% 맞음)를 보이는 고르기 창 손질. +8. 「다른 문서에서도」의 실제 걸림돌 = 지식DB 실무 내역 xlsx 9 개 중 6 개(STmate 꼴)가 올리기부터 막힘 · 2 개는 문서 보안(DRM) 잠김 — 이름 맞춤 앞 단계. +9. 큰 언어 모델 · 임베딩은 오프라인 사내 서버 · 자료 외부 유출 금지 조건에 안 맞음 — 보류. +10. 추천 차례 = ① 근거 고침 → ② 고르기 창 → ③ 기억 넓히기 · ④ STmate 읽기는 「다른 문서」를 언제 쓸지에 따라 ①과 나란히. + +--- + +## 1. 지금 실력 + +말 풀이 — 1순위 = 고르기 창 맨 위 후보(또는 「같음」 대상) · 다섯 안 = 후보 다섯 안에 정답 있음 · 엄격 = 엑셀 단가산출이 적은 품셈 조항 · 품과 같은 우리 로직 · 느슨 = 같은 일의 다른 책(산림 ↔ 건설) 로직이나 복합 줄의 곁 몫까지 맞음 · 고유 줄 = 품명 · 규격 · 단위 · 단산 번호가 같은 줄은 한 번 셈(한 번 확정하면 같은 줄 모두 들어감) · 같음 = 이름이 똑같거나 씨앗과 같아 사람이 안 골라도 들어가는 줄 · 씨앗 = 일위대가조합.json 메모(실무 이름 80 → 로직 · 읽기만). + +### 1-1. 330 앞 대 지금 (칸 = 엄격 / 느슨 %) + +| 파일 | 고유 줄(엑셀 줄) | 정답 있음 | 우리에 없음 | 330 앞 1순위 | 330 앞 다섯 안 | 지금 1순위 | 지금 다섯 안 | +|---|---|---|---|---|---|---|---| +| 북부청 임도 | 52(271) | 49 | 3 · 5.8% | 28.6 / 59.2 | 53.1 / 89.8 | 71.4 / 85.7 | 83.7 / 98.0 | +| 북부청 사방 | 48(142) | 43 | 5 · 10.4% | 34.9 / 62.8 | 53.5 / 88.4 | 74.4 / 83.7 | 88.4 / 95.3 | +| 동부청 영월* | 36(91) | 34 | 2 · 5.6% | 67.6 / 79.4 | 76.5 / 88.2 | 67.6 / 73.5 | 88.2 / 88.2 | +| **세 파일** | 136(504) | 126 | 10 · 7.4% | **41.3 / 65.9** | **59.5 / 88.9** | **71.4 / 81.7** | **86.5 / 94.4** | +| 엑셀 줄 무게(되풀이 셈 · 느슨) | 504 | 471 | 33 · 6.5% | 59.2 | 90.7 | 78.1 | 97.9 | + +- \* 영월 = STmate 꼴 — 지금 화면은 올리기부터 400. C1 이 스크래치패드에서 따로 읽어 같은 맞춤 함수에 넣은 가정 값. +- 330 앞 = 씨앗 켬(실제 프로그램 상태). C1 결론 표는 씨앗 끈 값(34.1)을 「이름만」으로 둬 영월의 330 효과가 부풀어 보임 — 실제 영월의 330 효과 = 1순위 엄격 0 · 다섯 안 +11.7(3-2 절). +- 읽는 법 — 북부청 두 파일은 330 으로 1순위가 두 배 넘게 오름. 영월은 근거 직종을 거의 못 읽어(STmate 흉내 한계 · 5 절) 330 효과가 다섯 안에만 남. + +### 1-2. 씨앗 몫 — 처음 보는 파일로 치면 (1순위 엄격 / 느슨 %) + +| 파일 | 330 앞 씨앗 끔 | 330 앞 씨앗 켬 | 지금 씨앗 끔 | 지금 씨앗 켬 | 묶음 ① 뒤 씨앗 끔 | 묶음 ① 뒤 씨앗 켬 | +|---|---|---|---|---|---|---| +| 북부청 임도 | 26.5 / 57.1 | 28.6 / 59.2 | 75.5 / 85.7 | 71.4 / 85.7 | 91.8 / 91.8 | 91.8 / 91.8 | +| 북부청 사방 | 34.9 / 62.8 | 34.9 / 62.8 | 76.7 / 83.7 | 74.4 / 83.7 | 86.0 / 88.4 | 86.0 / 88.4 | +| 동부청 영월 | 44.1 / 61.8 | 67.6 / 79.4 | 44.1 / 55.9 | 67.6 / 73.5 | 55.9 / 67.6 | 76.5 / 82.4 | +| 세 파일 | 34.1 / 60.3 | 41.3 / 65.9 | 67.5 / 77.0 | 71.4 / 81.7 | 80.2 / 84.1 | 85.7 / 88.1 | + +- 씨앗(일위대가조합.json 조합 37 · 실무 이름 80)을 만든 실무 호표에 영월 호표 20 개가 들어 있음(봉화 29 · 울진대흥 19 · 영월 20 · 울진소광 11 · 거창 7 · 계류보전 1) → 영월의 씨앗 효과(+20~24)는 「이미 본 파일」 효과. 처음 보는 STmate 파일 실력 = 영월 씨앗 끔 값. +- 지금은 씨앗이 북부청 두 파일에 해로움(−4.1 · −2.3) — 씨앗이 문양거푸집 2 줄 · 면고르기 1 줄을 산림 쪽으로 「같음」 처리하는데 북부청 엑셀은 건설 조항. 묶음 ①(「같음」도 근거 검사)을 넣으면 이 해가 0 이 됨. + +### 1-3. 「같음」 줄 — 사람이 안 골라도 들어가는 줄 + +- 지금 「같음」 33 줄(고유) 중 10 줄이 엑셀 출처와 다른 책(모두 같은 일 · 느슨으론 맞음) — 합판거푸집 · 문양거푸집 · 강관비계 · 강관동바리 · 면고르기 · 돌망태형 옹벽. +- 환산이 정해진 25 줄은 [사전조합 만들기] 때 확정 없이 들어감 — 그중 5 줄이 다른 책. 나머지 다른 책 5 줄은 환산을 넣으면 들어감. +- 묶음 ① 뒤 「같음」 22 줄 · 다른 책 0 — 11 줄(다른 책 10 + 두 책 모두 정답인 야면석메붙임 1)이 「후보」로 바뀌어 사람이 고름 · 그 11 줄 모두 「엑셀 출처 책 로직 1순위 · 같은 이름 다른 책 2순위」로 보임. + +### 1-4. 단추 정밀 + +- [근거 맞음 모두 확정] — 후보 줄 98 중 55 줄을 한 번에 · 55 줄 모두 느슨 맞음(엄격 54) — 믿을 만한 단추. +- [같음 모두 확정] — 25 줄 중 엄격 20(다른 책 5) — 근거를 안 봄. + +### 1-5. 우리에 없음 10 줄(고유 · 엑셀 33 줄) + +- 잔토처리 · 잔토정리 6 — 우리 로직엔 단독으로 없음(「굴착(인력/토사)」의 잔토처리 입력으로만 있음). +- 노임 줄 2 — 철골공 · 특별인부 [인] — 로직이 아닌 노임 단가 · 사전조합엔 노임 줄 그릇이 없음. +- 고화매트 설치 1 · 철강재 화물 운임 1. +- 이 중 5 줄은 엉뚱한 후보가 1순위로 보임(고화매트 → 식생매트 · 잔토정리 → 타설 후 정리 · 철골공 → 철골 공장용접 등). + +--- + +## 2. 남은 실패 까닭 + +1순위 엄격 실패 줄(고유) · 칸 = 고유 줄(엑셀 줄). 「묶음 ① 뒤」 = 3 절 1~4 고침을 얹은 흉내. + +| 갈래 | 지금 실패 | 묶음 ① 뒤 | 쉬운 예 | 풀 길 | +|---|---|---|---|---| +| 책 갈래(산림 ↔ 건설) | 11(53) | 1(1) | 합판거푸집(4회) — 엑셀은 건설 6-3-1 인데 이름이 같은 산림 12-4 가 「같음」 | 「같음」도 근거 검사(4) | +| 같은 뜻 다른 말 · 현장말 | 11(46) | 11(46) | 고임돌채집및운반 → 1위 「자갈채집 및 운반」(철도 궤도 자갈) · 정답 「모래·자갈·약돌 채집(인력)」 | 근거로도 못 품(출처 없음 · 단위중량 인용 · 옛 판) → 한 번 확정 뒤 기억(③) | +| 우리 로직이 더 잘게 | 5(27) | 0 | 철근가공조립(현장) = 우리 「철근 현장가공」 + 「철근 현장조립」 둘 · 엑셀 출처는 윗 조항 「6-2」 | 조항 윗마디(1) | +| 그 밖(접힘 · 이름 함정) | 5(15) | 2(7) | 유로폼 설치,해체 — 산림 · 건설 같은 이름 둘이 후보에서 하나로 접혀 건설 것이 사라짐 · 「철근가공조립(현장)」이 괄호를 지우면 우리 「철근가공조립(복잡)」과 이름이 같아짐 | 접힘 고침(2) · 남은 2 = 영월 — 근거 직종을 못 읽어 「괄호 지운 이름 같음」 덤이 이김 → 괄호 다른 덤 빼기(2-1)로 1 더 풂 | +| 괄호 속 규격 | 1(18) | 1(18) | 구조물터파기(토사) → 1위 「터파기(기계) 암」(괄호 「토사」를 지워 토질을 못 봄) | 토질 칸 어긋남 깎기(7) — 느슨까지 | +| 장비 · 노임 이름 줄 | 2(2) | 2(2) | 콘크리트 펌프차 41m [hr] → 1위 「콘크리트 펌프차 타설」(㎥) · 가야 할 곳 = 「기계 시간당 기계경비」 | 장비 시간 줄 길(5) | +| 엑셀 복합 호표 | 1(1) | 1(1) | 철골가공조립(공장) = 우리 「공장가공조립」 + 「공장용접」 | 사람이 고름(느슨 1위 맞음) | +| **합계** | **36(162)** | **18(75)** | | | + +- 다섯 안에도 정답이 없는 줄(엄격) — 지금 17 → 묶음 ① 뒤 7(야면석소운반 2 · 콘크리트소운반 · 철골설치 · 측구수로관부설 · 펌프차 · 크레인) · 장비 길(5)로 2 더 풀면 5. 이 5 줄은 [다른 로직 찾기]로 사람이 찾아야 함. +- 묶음 ① 뒤 남은 현장말 11 줄 중 6 줄이 고임돌 · 조약돌 채집(엑셀은 「기계 90% + 인력 10%」를 한 줄로 · 우리 정답은 인력 채집 몫) — 같은 품명이 세 파일에 되풀이되므로 기억(③)이 가장 잘 맞는 갈래. +- 330 앞(씨앗 끔) 실패 83 줄의 65% 가 같은 뜻 다른 말 · 책 갈래였고 330 이 이 둘을 대부분 풂(책 갈래 29 → 11 · 같은 뜻 25 → 11 · 줄임말 9 → 0). + +--- + +## 3. 방법 표 + +### 3-1. 방법마다 (기대 효과 기준 = 지금 1순위 71.4 / 81.7 · 다섯 안 86.5 / 94.4 · 세 파일 고유 126 줄) + +판정 = 먼저 / 다음 / 보류 / 안 맞음 · 묶음 번호(①~④)는 4 절. + +**가. 근거 쪽(서버)** + +| # | 방법 | 무엇이 좋아지나 | 기대 효과 | 들 일 | 위험 | 판정 | +|---|---|---|---|---|---|---| +| 1 | 조항 윗마디 | 엑셀이 윗 조항(「6-2」)만 적어도 우리 「6-2-2 · 6-2-3」을 같은 조항으로 봄 — 철근 가공조립 | 75.4 / 85.7 (+4.0 / +4.0) | 작음 | 낮음 | 먼저 ① | +| 2 | 같은 이름 접힘 고침 | 산림 · 건설에 이름이 똑같은 로직 둘이 후보에서 하나로 접혀 사라지던 것 → 둘 다 남음 — 유로폼 · 레미콘 | 73.8 / 84.1 (+2.4 / +2.4) · 영월 +8.9 | 작음 | 같은 이름 둘이 나란히 → 책 표시 필요(27) | 먼저 ① | +| 2-1 | 괄호 다른 「같은 이름」 덤 빼기 | 괄호를 지운 이름만 같고 괄호 글이 다르면(「철근가공조립(현장)」 ↔ 「철근가공조립(복잡)」) 이름 같음 덤(+1)을 안 줌 | 묶음 ① 위 86.5 / 88.9 (+0.8 / +0.8) · 영월 철근 1 줄 풂 · 해 0 | 작음 | 낮음 | 먼저 ① 덤 | +| 3 | 조항 오타 바로잡기 | 「13-114」 → 「13-11-4」 꼴 — 돌망태형 옹벽 | 1~4 묶음 안에서만 셈 | 작음 | 낮음 | 먼저 ① | +| 4 | 「같음」도 근거 검사(판 지킴) | 이름이 같거나 씨앗으로 「같음」이 된 줄도 엑셀 출처 조항이 다른 책이면 「후보」로 내려 사람이 고름 · 엑셀 글이 옛 판(「24년」 · 「임도품셈」 · 「사방품셈」)이면 그대로 | 78.6 / 81.7 (+7.2 / 0) · 다섯 안 93.7 · 같음 다른 책 10 → 0 | 중간 | 같음 11 줄이 후보로 → 확정 한 번 더 | 먼저 ① | +| 1~4 | 묶음 | | **85.7 / 88.1 (+14.3 / +6.4) · 다섯 안 94.4 / 94.4** · 씨앗의 해 사라짐 | 작음~중간 | 낮음 | 먼저 ① | +| 5 | 장비 시간 줄 길 | [hr] · [시간] 장비 줄 → 「기계 시간당 기계경비」 + 기종 입력 — 펌프차 · 크레인 | 2 줄 풂 어림(+1.6) · 흉내 안 함 | 작음 | 낮음 | 먼저 ① 덤 | +| 6 | 노임 줄 표시 | [인] 노임 줄(철골공 · 특별인부)을 「노임 · 그릇 없음」으로 → 엉뚱 후보 안 보임 | 엉뚱 후보 5 → 4 어림 | 작음 | 낮음 | 먼저 ① 덤 | +| 7 | 토질 칸 어긋남 깎기(토사 ↔ 암) | 이름 괄호 「토사」와 후보 「암」이 어긋나면 뒤로 — 구조물터파기(토사) | 엄격 0 · 느슨 +0.8(한 줄 · 엑셀 18 번 되풀이) | 작음 | 낮음 | 다음 ① 덤 | +| 8 | 방법 칸 어긋남 깎기(인력 ↔ 기계) | C2 제안 | −2.4 / −1.6 — 「굴삭기 + 인력 10%」 섞인 채집 줄 3 뒤집힘 | 작음 | 높음 | 안 맞음 | +| 9 | 부문 가름(공통 · 토목 글) | 같은 번호 다른 부문 가름 | 변화 0 | 작음 | — | 보류 | +| 10 | 블록 글 낱말 덤 | 우리 이름 낱말이 엑셀 산출 글에 나오면 덤 | 69.8 / 84.1 (−1.6 / +2.4) | 중간 | 단위중량 · 자재 이름 섞임 | 보류 | +| 11 | 직종 · 장비 구성 점수 | 직종 · 장비 묶음 겹침을 점수로 | 71.4 / 82.5 (0 / +0.8) | 중간 | 직종은 이미 근거 점수에 듦 | 보류(나란히 보기 28 로만) | +| 12 | 책 맞춤 덤 | 엑셀 출처 책 = 우리 책이면 덤 | 73.8 / 82.5 · 영월 +11.8 · 임도 −2.0 | 작음 | 섞임 · 4 가 더 곱게 함 | 보류 | +| 13 | 옛 조항 번호 바꿈표 | 옛 판 번호를 지금 번호로 | 잼 못 함 · 재료 = 산림 14 건 · 건설 한 해 치뿐 | 중간 | 번호만 같은 다른 절에 붙음 | 보류 | + +**나. 이름 쪽** + +| # | 방법 | 무엇이 좋아지나 | 기대 효과 | 들 일 | 위험 | 판정 | +|---|---|---|---|---|---|---| +| 14 | 손 사전(같은 뜻 말 16 쌍) 순위 덤 | 레미콘 = 레디믹스트콘크리트 · 터파기 = 굴착 등 | 지금 위 64.3 / 81.0 (−7.1 / −0.7) · 묶음 ① 위 −0.8 / −1.6 · 다른 꼴(영월) 330 앞 위 −8.8 / −8.9 | 작음(사전) · 키우기 큼 | 회사마다 책이 달라 틀린 쪽을 1위로 | 안 맞음 | +| 15 | 표기 고르기 — 후보 넓히기에만 | 깎/깍 · 뒷/뒤채움 · ㎥/M3 · 브레이카/브레이커 — 근거 없는 줄에서 글자 꼴이 달라 후보에도 못 드는 것 | 잼 못 함 · 근거 있는 줄은 이미 근거가 덮음(흙깍기 맞음) | 작음 | 낮음(덤 안 줄 때) | 보류 | +| 16 | 이름 다듬기(괄호 낱말 살림 · 수 · 단위 뺌) | 괄호 속 「인력/토사」 살림 | 지금 위 70.6 / 81.7 (−0.8 / 0) · 330 앞 위 −3.9 / −7.1 | 작음 | 괄호 속 규격이 엉뚱 후보를 끌어옴 | 안 맞음 | +| 17 | 낱말 차례 무시 | 「큰돌찰붙임 ↔ 큰돌붙이기 찰붙이기」 | 330 앞 위 1순위 +3.2 · 다섯 안 느슨 −5.6 | 작음 | 엉뚱 후보 | 안 맞음 | +| 18 | 글자 비교 바꾸기 | 1·2·3 글자 묶음 · difflib · Jaro-Winkler · TF-IDF | 글자 묶음 변화 0 · difflib 다섯 안 엄격 +15.9 이나 우리에 없음 줄 엉뚱 후보 4 → 10 · Jaro-Winkler · TF-IDF 잼 못 함 | 작음 | 엉뚱 후보 늘어남 | 안 맞음 | +| 19 | 단위 거름망(단위 다르면 뺌) | | 63.5 / 73.8 (−7.9 / −7.9) | 작음 | 맞는 짝 단위가 다름(개소 ↔ M · 조 ↔ ㎡) | 안 맞음 | +| 20 | 단위 별칭(hr = 시간 · 공/㎥ = 공㎥ · 개소 = 개 = 본) | 환산 칸 손 덜기 | 순위 변화 0 · 환산 「입력」 20 → 14 줄 | 작음 | 낮음 | 다음 ① 덤 | +| 21 | 형태소 분석(kiwipiepy) | 붙여 쓴 이름 쪼개기 | 잼 못 함(설치 안 함) | 중간 · 새 꾸러미 | 짧은 전문 복합어 잘못 쪼갬 | 보류 | + +**다. 기억 · 사전 (사용자 확정 · 마스터 밖)** + +| # | 방법 | 무엇이 좋아지나 | 기대 효과 | 들 일 | 위험 | 판정 | +|---|---|---|---|---|---|---| +| 22 | 기억 둘째 열쇠(품명 · 단위) → 「기억 후보」 | 규격만 다른 같은 품명이 다음 파일에서 바로 1순위 | 같은 발주처: 고유 4/48 → 15/48 · 엑셀 줄 5% → 33%(모두 맞음) · 다른 발주처: 북부청 → 영월 13 줄 중 엄격 8 · 느슨 12 / 영월 → 북부청 17 줄 중 엄격 12 · 느슨 17 | 중간 | 다른 회사 기억 = 다른 책 → 후보로만 · 근거 검사 | 다음 ③ | +| 23 | 기억 셋째 열쇠(정리 이름 · 단위) | 더 넓게 | 24/48 중 21 맞음 · 3 틀림(괄호 「토사」 · 「인력100%」를 지워 섞임) | 작음 | 틀린 짝이 1순위 | 안 맞음 | +| 24 | 확정한 엑셀 이름 → 층 기억 별칭 | 현장말(고임돌 · 소운반 · 철골설치)을 한 번 확정하면 다음 파일 이름 후보 · 찾기 창에 걸림 | 잼 못 함 · 같은 꼴인 씨앗 = 같은 출처 파일(영월) +20.6 · 다른 발주처엔 4 와 함께면 0 | 중간 | 회사마다 다름 → 회사 층에 | 다음 ③ | +| 25 | M01 로직 별칭을 맞춤이 읽기만 | 사용자가 M01 화면에서 채운 별칭을 이름 후보로 | 지금 별칭 1,438 중 0 → 효과 0(채운 만큼) | 작음 | 낮음 | 보류 | +| 26 | 손 사전을 마스터에(별칭 칸 자동 채움) | C2 제안 | 14 와 같음 · 원본 로직을 코드로 고치게 됨 | — | 높음 | 안 맞음 | + +**라. 고르기 창 (사람이 빨리 · 바르게)** + +| # | 방법 | 무엇이 좋아지나 | 기대 효과 | 들 일 | 위험 | 판정 | +|---|---|---|---|---|---|---| +| 27 | 후보마다 책 · 조항 · 직종 늘 보이기 | 같은 이름 산림 · 건설 구별 · 마우스 안 올려도 까닭 보임 | 순위 그대로 · 다섯 안 94% 를 빨리 고름 | 중간 | 낮음 | 먼저 ② | +| 28 | 엑셀 산출 글 ↔ 우리 호표 나란히 | 직종 · 품 · 장비를 눈으로 맞춤 | 잼 못 함 | 중간 | 낮음 | 먼저 ② | +| 29 | 금액 비 초록 표시(우리 값 ÷ 엑셀 단가) | 맞는 짝을 한눈에 | ±5% 안 21 줄 중 20 맞음 · 덮는 줄 16% · ±15% 밖 69 줄 중 51 도 맞음 → 빨강 표시 없음 | 중간(서버가 후보 값 셈) | 낮음(초록만) | 다음 ② | +| 30 | 금액 순위 점수 | | 66.7 / 77.8 (−4.7 / −3.9) | 중간 | 복합 줄 · 입력 기본값 | 안 맞음 | +| 31 | [같음 모두 확정]에서 근거 어긋남 빼기 · 같음 줄에도 근거 후보 | 4 의 화면 쪽 | 25 중 다른 책 5 가 빠짐 | 작음 | 낮음 | 먼저 ② | +| 32 | [다른 로직 찾기]에 엑셀 조항 · 이름 미리 넣기 | 다섯 안 밖 줄 찾기 손 덜기 | 잼 못 함 | 작음 | 낮음 | 먼저 ② | +| 33 | 낮은 점수 후보 숨김(문턱) | 엉뚱 후보 숨김 | 문턱 0.6 → 없음 줄 3 숨김 · 맞는 1순위 3 잃음 | 작음 | 맞는 것도 숨음 | 안 맞음 | + +**마. 무거운 길 · 코드** + +| # | 방법 | 무엇이 좋아지나 | 기대 효과 | 들 일 | 위험 | 판정 | +|---|---|---|---|---|---|---| +| 34 | 문장 임베딩 재순위 | 뜻 거리 | 잼 못 함 · 수백 MB 모델 · 산림/건설 같은 이름은 벡터도 같아 못 가름 | 큼 | 현장말 모를 수 있음 | 보류 | +| 35 | 큰 언어 모델(클라우드) 재순위 | 남은 현장말 · 복합 줄 | 잼 못 함 · 사내 오프라인 서버에서 못 씀 | 중간 | 발주처 자료 밖으로 · 답 흔들림 | 안 맞음(지금 조건) | +| 36 | 로컬 큰 언어 모델 | 같음 | 한국어 모델(EXAONE) 비상업 라이선스 · 설치 큼 | 큼 | 라이선스 | 안 맞음 | +| 37 | 국가 표준 코드로 잇기(조달청 세부공종 · 표준시장단가) | | 실무 엑셀 코드 칸 0 · STmate 자료 일위대가 국가 코드 0/216 | — | — | 안 맞음 | +| 38 | 조달청 세부공종코드 CSV 를 사전 재료로 | 옛/새 이름 묶음 | 잼 못 함 · 임도 · 사방 고유 공종 0 · 사전 자체가 근거 위에서 이득 없음 | 작음 | — | 보류 | + +**바. 읽기 (다른 꼴)** + +| # | 방법 | 무엇이 좋아지나 | 기대 효과 | 들 일 | 위험 | 판정 | +|---|---|---|---|---|---|---| +| 39 | STmate 꼴 읽기 | 막히던 STmate 내역 6 개가 올라감 | 영월 흉내(처음 보는 셈) 지금 44.1 / 55.9 → 묶음 ① 위 55.9 / 67.6 · 다섯 안 82.4 · 노무 목록표로 직종을 이으면 더 오를 여지(잼 못 함) | 중간 | 판마다 시트 꼴 조금씩 다름 | 먼저 ④(별도 축) | +| 40 | 거창 꼴(줄 = 노임 · 자재 직접) | 호표 머리 이름을 로직 하나에 맞춤 | 잼 못 함 · 지금도 올라감(호표 14 · 검산 맞음) | 중간~큼 | — | 보류 | +| 41 | 문서 보안(DRM) 파일 안내 | 「엑셀을 읽지 못함」 대신 「문서 보안 걸린 파일 — 풀린 사본을 올림」 | — | 작음 | 낮음 | 다음 ④ | + +### 3-2. C2(바깥 조사)와 C1 · C3(우리 실측)이 어긋나는 곳 — 실측 우선 + +| 주제 | C2 말 | 우리 실측 | 정리 — 어떻게 쓰면 되나 | +|---|---|---|---| +| 같은 뜻 말 사전 | 「바로 쓸 만함(핵심)」 · 재료 = 품셈 다리 문구 · 조달청 옛/새 이름 · 실무 짝 20 · 씨앗 · M01 별칭 채움 | 임도 파일로 만든 16 쌍 — 330 앞 위 사방 +7.0 · 영월 −8.9(느슨) · 지금 위 −7.1(엄격) · 묶음 ① 위 −0.8. 해의 대부분 = 「레미콘 = 레디믹스트콘크리트」 한 짝(9 줄) — 이름이 같아진 산림 · 건설 둘 중 산림이 1위로(같은 이름 접힘과 겹침). 판단이 든 짝(터파기 → 굴착 등 10 쌍)은 지금 위 이득 0 | 순위에 덤 주는 손 사전은 두지 않음. 「어느 로직을 쓰나」는 회사마다 다름(북부청 구조물터파기(인력) = 건설 3-2-1 · 영월 · 씨앗 = 산림 9-13-1) → 사용자 확정이 회사 층 기억에 쌓이는 맞춤 사전(③). 표기 고르기는 근거 없는 줄의 후보 넓히기에만(15) | +| M01 로직 별칭 칸 | 「가장 값싼 길 — 그릇 있음 · 채움」 | 별칭 1,438 중 0 · 원본 로직은 코드로 안 고침 | 3-3 두 갈래 — 추천은 M02 맞춤 사전(마스터 밖) | +| 괄호 글을 칸으로(토질 · 방법) | 「바로 쓸 만함」 · 「인력/토사」를 잃음 | 괄호 낱말 살림 = 지금 위 −0.8 · 330 앞 위 −3.9 / −7.1. 토질 칸만 = 0 / +0.8 · 방법 칸까지 = −2.4(9:1 섞인 채집 줄 3 뒤집힘). C2 예시 「구조물터파기(인력100%) ↔ 굴착(인력/토사)」는 330 근거로 이미 풀림(맞음 2 줄) | 토질 칸만 덤(7) · 방법 칸은 안 씀(8) | +| 구성 비교(직종 · 장비 · 보통인부뿐) | 「바로 쓸 만함」 | 0 / +0.8 · 직종은 이미 근거 점수에 듦 | 점수는 안 둠 · 고르기 창 나란히 보기(28)로 | +| 글자 비교(TF-IDF · Jaro-Winkler) | 「바로 쓸 만함(작은 이득)」 · 「조건부(보조)」 | 글자 묶음 바꾸기 0 · difflib 은 엉뚱 후보 4 → 10 | 하지 않음 | +| 품셈 조항 번호 | 「조건부 — 판 짐작 · 부문 · 옛 번호 바꿈표가 붙어야」 | 근거 쪽 작은 고침이 가장 큰 이득(+14.3) · 부문 가름 0 · 옛 판은 「같음 안 내림」(판 지킴)으로 충분 | 근거 작은 고침 넷을 먼저(①) · 바꿈표 보류 | +| 금액 대조 | 「조건부 — 동점 가르기 · 경고」 | 순위 점수 −4.7 · ±5% 안 21 줄 중 20 맞음 · 멀어도 69 줄 중 51 맞음 | 「가까우면 초록」만 · 경고(빨강)는 안 둠 | +| 열쇠 단계 내리기 | 「바로 쓸 만함」(STmate 5 단) | 품명 · 단위 = 같은 발주처 15/15 · 18/18 맞음 · 다른 발주처 엄격 62~71% · 정리 이름 · 단위 = 3 틀림 | 품명 · 단위까지만 · 「기억 후보」로(22) | +| 큰 언어 모델 | 「안 맞음(지금 조건)」 | C1 「기대 큼 · 근거 실패 줄(약 15%)에만」 | 오프라인 · 유출 금지 기준 보류 — 외부 전송 허용은 사용자가 정함 | +| 씨앗 | 사전 재료(메모 170) | 영월 +23.5 이나 북부청 −4.1 · −2.3 · 영월은 씨앗 출처 파일 | 마스터 층 기억의 한 꼴로 그대로 · 4 와 함께면 해 없음 | +| C1 지표의 「이름만」 | — | C1 은 씨앗 끈 값(34.1)을 330 앞으로 둠 → 영월 330 효과 +23.5 로 보임 | 이 검토서는 씨앗 켬(41.3)을 330 앞으로 · 영월 330 효과 = 1순위 0 · 다섯 안 +11.7 | + +### 3-3. 「별칭」 두 갈래 — 꼭 가를 것 + +| | (가) M01 로직 별칭 칸 | (나) M02 맞춤 사전(= 층 기억 넓히기) | +|---|---|---| +| 자리 | 마스터 정본 로직 줄 안의 칸 | M02 층 기억 파일(층마다 · 마스터 로직 밖) | +| 누가 채우나 | 사용자가 M01 화면 로직 목록 「별칭」 칸에 쳐서 [저장] — 코드가 자동으로 채우지 않음 | 사용자가 M02 고르기 창에서 [확정]할 때 쌓임 | +| 지금 | 1,438 중 0 · M02 맞춤은 안 읽음 | 있음 — 품명 · 규격 · 단위가 똑같을 때만 씀 | +| 회사별 | 정본 하나(모든 회사 같음) | 개인 → 회사 → 마스터 차례로 봄 — 회사마다 갈림 | +| 원본 지킴 | 사용자 화면 작업이라 원칙에 맞음 · 코드 자동 채움은 금지 | 원본 로직 · 일위대가조합.json 안 건드림 | +| 판정 | 보류(사용자가 채우기 시작하면 「읽기만」 붙임 · 25) | 추천(③ · 22 · 24) | + +--- + +## 4. 묶음 추천 · 차례 + +| 묶음 | 1순위 엄격 / 느슨 | 다섯 안 엄격 | 들 일 | 차례 | +|---|---|---|---|---| +| 지금 | 71.4 / 81.7 | 86.5 | — | — | +| ① 근거 쪽 서버 고침 | 85.7 / 88.1 (덤 2-1 · 5 더해 어림 88.1 / 90.5) | 94.4 (어림 96.0) | 작음~중간 | 1 | +| ② 고르기 창 손질 | 순위 그대로 | 그대로 | 중간 | 2(①과 같은 때가 좋음) | +| ③ 기억 넓히기(회사별 맞춤 사전) | 같은 발주처 다음 파일 바로 1순위 엑셀 줄 5% → 33% | — | 중간 | 3 | +| ④ STmate 꼴 읽기 | 영월(처음 보는 셈) ① 위 55.9 / 67.6 | 82.4 | 중간 | ①과 나란히 또는 4 | + +### ① 근거 쪽 서버 고침 — 방법 1 · 2 · 3 · 4 (+ 덤 2-1 · 5 · 6 · 7 · 20) + +- 화면에서 달라지는 것: 호표 줄 표의 1순위가 엑셀 단가산출 출처 조항과 같은 우리 로직으로 · 「같음」이던 11 줄(엑셀과 다른 책 10 포함)이 「후보」로 바뀌어 엑셀 책 로직 1순위 · 같은 이름 다른 책 2순위(확정 한 번 더) · 철근 가공조립이 「철근 현장가공 · 현장조립」 쪽으로 · 펌프차 · 크레인 시간 줄이 「기계 시간당 기계경비」로 · 철골공 · 특별인부 줄은 「노임 · 그릇 없음」. +- 수치: 1순위 71.4 / 81.7 → 85.7 / 88.1(덤 2-1 · 5 더해 어림 88.1 / 90.5) · 다섯 안 86.5 → 94.4(어림 96.0) · 파일별 1순위 엄격 임도 71.4 → 91.8 · 사방 74.4 → 86.0 · 영월 67.6 → 76.5 · 엑셀 줄 무게 1순위 느슨 78.1 → 85.6 · 「같음」 다른 책 10 → 0 · 씨앗을 꺼도 북부청 값 그대로(씨앗의 해 사라짐). +- 들 일: 작음~중간 — 서버 맞춤 두 파일의 몇 함수 · 시험 기대값 고침 · 화면은 안 바뀜. 다만 2 로 같은 이름 둘이 다섯 안에 함께 보이므로 ②의 책 표시와 같이 내는 것이 좋음. +- 차례: 1 — 가장 싸고 가장 큼 · STmate 를 읽게 되면 그 파일에도 같은 함수로 효과. + +### ② 고르기 창 손질 — 방법 27 · 28 · 31 · 32 (+ 29) + +- 화면에서 달라지는 것: 후보 단추에 「건설 공통 6-3-1 · 형틀목공 · 단위 맞음」 같은 줄이 늘 보임(지금은 「이름 [단위] · 키」 뿐 · 까닭은 마우스 올림에만) · 엑셀 산출 글(직종 · 품 · 장비)과 후보 우리 호표를 위아래로 · 금액 비 ±5% 안이면 초록 · [같음 모두 확정]이 근거 어긋남 줄을 빼고 셈 · [다른 로직 찾기…]가 엑셀 조항 · 이름을 미리 넣고 엶(지금은 빈 찾기 창). +- 수치: 순위는 그대로 — 남은 일은 다섯 안 94% 정답을 빨리 고르기 · [근거 맞음 모두 확정]은 지금도 55 줄 모두 맞음 · 금액 초록 = 덮는 줄 16% · 맞음 95%. +- 들 일: 중간 — 고르기 창이 든 화면 파일이 614 줄(한도 700)이라 고르기 창을 새 파일로 나눔 · 서버가 후보마다 책 · 조항 · 직종 · 금액 비를 보냄(금액은 우리 로직을 엑셀 단위로 셈). +- 차례: 2. + +### ③ 기억 넓히기 = 회사별 맞춤 사전 — 방법 22 · 24 + +- 화면에서 달라지는 것: 다음 파일을 올리면 전에 확정한 품명과 같은 줄이 규격이 달라도 「기억 후보」 꼬리표로 1순위(자동 확정 아님 · 사람이 [확정]) · 확정한 엑셀 이름이 [다른 로직 찾기] 찾기 칸에서도 걸림 · 다른 회사 기억이 엑셀 근거와 어긋나면 꼬리표에 「근거 다름」. +- 수치: 같은 발주처 다음 파일 바로 1순위 — 임도 → 사방 고유 4/48 → 15/48 · 엑셀 줄 5% → 33% · 사방 → 임도 엑셀 줄 10% → 44% · 모두 맞음. 다른 발주처(북부청 ↔ 영월) 기억은 엄격 62~71% → 후보로만. 묶음 ① 뒤 남은 현장말 11 줄(고임돌 · 조약돌 6 · 소운반 3 · 철골설치 · 측구수로관)은 한 번 확정하면 같은 품명 다음 파일부터 바로. +- 저장 자리: M02 층 기억 파일(개인 → 회사 → 마스터 차례 이미 있음) · 원본 로직 · 일위대가조합.json · M01 별칭 안 건드림. +- 들 일: 중간 — 기억 꼴에 둘째 열쇠 · 엑셀 이름 덧칸 · 「기억 후보」 꼬리표 · 시험. +- 차례: 3 — 쓸수록 좋아지는 길 · ①의 근거 검사와 함께여야 다른 회사 기억이 틀린 책을 끌어오지 않음. + +### ④ STmate 꼴 읽기 — 방법 39 · 41 + +- 화면에서 달라지는 것: 지금 「일위대가 시트 없음」으로 막히는 STmate 내역 xlsx 가 올라가 같은 호표 표 · 고르기 창으로 보임 · 문서 보안 잠긴 파일은 까닭 글. +- 수치: 지식DB 실무 내역 xlsx 9 개 중 올라가는 것 1 → 7 · 영월 흉내(처음 보는 셈) 묶음 ① 위 1순위 55.9 · 다섯 안 82.4 · 같은 회사 기억이 쌓이면 영월 씨앗처럼 76.5 근처(이미 본 셈). +- 들 일: 중간 — 서버 새 읽기 파일 하나(일위대가표 · 단가산출근거 · 노무 목록표 · 줄 코드 D · L · M · X · B 이음) · 6 파일 시험 · 엑셀 보기 창은 값만(서식 없음). +- 차례: 「다른 문서」를 곧 쓸 거면 ①과 나란히(서로 다른 파일이라 안 겹침) · 아니면 4. + +--- + +## 5. 걸림돌 — STmate 꼴 · 문서 보안 + +지식DB 실무 문서의 엑셀 꼴(시트 이름 · 파일 머리로 가름): + +| 꼴 | 파일 | 지금 | +|---|---|---| +| 북부청 꼴(일위대가 + 단가산출 · 「단산N참조」) | 북부청 임도 · 사방 xls 2 | 올라감 · 근거 맞춤 됨 | +| STmate 꼴(일위대가표 · 단가산출근거 · 줄 코드) | 봉화 현동 · 동부청 영월 · 울진 간선 기번3(2) · 영덕 계류보전 · 울진 소광 — 6(사업 5) | 400 「일위대가 시트 없음」 | +| 거창 꼴(일위대가 시트 · 줄 = 노임 · 자재 직접) | 거창 1 | 올라감(호표 14 · 줄 68 · 검산 맞음) · 줄이 「레미콘 · 콘크리트공 · 보통인부 · 공구손료 %」 꼴이라 줄 맞춤으로는 로직을 못 찾음 | +| 문서 보안(DRM) 잠김 | 장수 · 진안 「내역+수량」 2 | 400 「엑셀 파일을 읽지 못함」 — 파일 머리가 「DRMONE」 · 어떤 길로도 못 읽음 | +| 일위대가 시트 없음(단가산출만) | 2016 사방품셈(안) · 수목진료 xls 2 | 400 | + +- C1 은 「6 개 중 5 개」로 셈(사업 단위) — 파일로는 STmate 6 · 장수 · 진안 2 개(보안 잠김)는 C1 셈에 빠져 있었음. +- 판단: 「다른 문서에서도 이름이 다름」을 풀려면 STmate 읽기가 이름 맞춤보다 앞 — 지금은 맞춤까지 가지도 못함. 다만 ①은 읽기와 상관없이 북부청 꼴에서 바로 효과이고 STmate 에도 같은 맞춤 함수가 쓰이므로, ①을 먼저(작음) 하거나 ④와 나란히 함. +- STmate 가 오히려 쉬운 점: 줄마다 코드(D 단가산출 · L 노임 · M 자재 · X 기계 · B 다른 호표 · S 별도계상)가 있어 줄 ↔ 단가산출 블록 · 노임 · 장비를 이름 없이 바로 이음 · 같은 D 코드 = 같은 줄(한 번 확정이면 모두) · 노임 · 장비 줄을 코드로 바로 가름(L → 「노임」 표시 · X → 장비 시간 길). +- 어려운 점: 단가산출근거 블록 안 노임 줄은 코드(「L00002L」)와 식만 있고 직종 이름이 없음 → 노무 목록표 시트로 이어야 근거 직종이 채워짐(C1 흉내는 이를 안 해 영월 근거 직종이 거의 빔 — 영월 330 효과가 작은 까닭) · 2024 문서들은 옛 판 번호를 인용(「건설표준품셈 3-1-3」 · 「임도품셈 5-5」) → 근거 힘이 약함 · 판마다 덧시트가 조금씩 다름(일위대가 단가산출 금액 집계표 · 일식경비 목록표 등). +- 들 일: 중간. STmate 자료 파일(STC)을 직접 읽는 길은 발주처가 STC 를 줄 때만 — 보류. +- 문서 보안 파일: 사용자가 보안을 푼 사본으로 저장해 올려야 함 · 화면 글만 고침(작음). + +--- + +## 6. 사용자가 정할 것 + +1. **같은 일 다른 책(산림 ↔ 건설)** — 엑셀 출처 책(남의 문서 그대로)을 1순위로 할지 · 산림 품셈 총칙 1-1 제7호(「타 부문과 유사한 공종의 품은 본 품셈을 우선하여 적용」)대로 산림을 1순위로 할지. 예: 북부청은 합판거푸집을 건설 6-3-1 로 셈(산림 12-4 있음). 갈리는 것 = 방법 4 의 판정 · 정답 기준 · 금액. 기본안 = 엑셀 출처 책 1순위 · 같은 이름 다른 책은 둘째 후보로 늘 보임. +2. **「같음」 자동 들어감** — 지금 환산이 정해진 「같음」은 확정 없이 사전조합에 들어감(① 뒤 22 줄 · 다른 책 0). 그대로 둘지 · [같음 모두 확정] 한 번을 거치게 할지. +3. **금액 비 표시 기준** — ±5%(맞음 95% · 덮는 줄 16%) / ±15%(맞음 89% · 덮는 줄 29%). 「멀다」 빨강 표시는 두지 않음(멀어도 74% 맞음). +4. **회사 공유 사전** — 확정이 쌓이는 층: 개인만 / 회사 층(지금은 회사 관리자만 회사 층에 씀) / 개인 확정을 관리자가 회사로 올리는 단추. 마스터 층 기억과 씨앗(일위대가조합.json 메모)을 「모든 회사 공통」으로 계속 둘지. +5. **둘째 열쇠 기억** — 「바로 들어감」 / 「1순위 후보 + 꼬리표」. 기본안 = 후보로만(같은 발주처 100% · 다른 발주처 62~71%). +6. **잔토처리 · 잔토정리** — 「우리에 없음」 10 줄 중 6 줄. M01 에 단독 로직을 둘지(사용자 마스터 작업) · 「굴착(인력/토사)」의 잔토처리 입력으로 맞출지. +7. **노임 줄([인])** — 사전조합에 노임 줄 그릇이 없음. 「노임 · 그릇 없음」 표시만(작음) / 노임 줄 꼴 새로(큼 · 일위대가 문서 꼴이 바뀜). +8. **섞인 줄(기계 90% + 인력 10% 채집 · 터파기)** — 우리 인력 로직 하나(인력 몫)를 정답으로 볼지 · 기계 몫 로직까지 둘로 쪼개 넣을지(정답 기준 · 사전조합 꼴에 걸림). +9. **차례** — STmate 읽기를 ①과 나란히(다른 문서 곧 씀) / ① → ② → ③ 뒤로. +10. **외부 AI** — 발주처 자료를 클라우드 언어 모델로 보내도 되는지(지금 판단 = 안 됨 · 보류). +11. **문서 보안 파일** — 풀린 사본으로 올리라는 안내 글로 충분한지. + +--- + +## 7. 부록 + +### A. 정답 표시 기준 + +- 자료 = 북부청 임도(xls · 호표 68 · 줄 415 · 로직 줄 고유 52) · 북부청 사방(xls · 호표 27 · 줄 201 · 고유 48) · 동부청 영월(STmate xlsx · 고유 36 · 지금 화면은 400 → 스크래치패드에서 읽어 같은 맞춤 함수에 넣음). +- 대상 = 단가산출 · 기계 · 기타 갈래 줄(로직 줄). 자재 · 별도계상 · % 줄은 정답 표시 밖. +- 정답 = 엑셀 단가산출 블록의 출처 조항 · 직종 · 품을 우리 로직 호표와 대조해 붙임(C1 부록 D · 136 줄 전수). 엄격 = 같은 조항 · 품 · 느슨 = 같은 일 다른 책 · 복합의 곁 몫. 짐작 31 줄(출처 없음 · 일부만 덮음)은 사람 판단 섞임. 영월 씨앗 줄은 씨앗 짝을 정답으로 봄(영월 씨앗 효과가 큰 까닭 하나). +- 「우리에 없음」은 분모에서 빼고 따로 셈. 기억 없음 · 씨앗 켬이 기본. +- 한계: 표본 3 파일 · 그중 둘이 같은 발주처 · 영월은 읽기 흉내(직종 이음 빠짐) · 금액은 엑셀 노임(2026)과 우리 노임 차이가 몇 % 섞임. + +### B. 실험 경로 (스크래치패드 `req225/` · 저장소 안 고침) + +- C1 — `C1/` 스크립트 s01~s18 · 결과 `out_*.json`(C1 부록 E). 이번에 다시 돌려 수치 확인: `s06_memory.py`(기억 재사용) · `s11_buttons.py`(단추 정밀) · `s13_money_flag.py`(금액 표시). +- C3 덧잼 — `C3/s20_c3_extra.py`(C1 함수를 불러 같은 프로세스 안에서 바꿔 끼움): + - `a` 사전이 바꾼 줄 · 표기 사전(6 쌍) / 판단 사전(10 쌍) 나눠 얹기 + - `b` 묶음 ① 씨앗 끔 · 묶음 ① + 사전 · 「같음」 줄 셈 + - `c` 묶음 ① 뒤 남은 실패(C1 갈래 표시) + - `d` 칸 어긋남 깎기 · `d soil` = 토질 칸만 + - `e` 다른 발주처 기억 재사용(열쇠 꼴별) + - `f` 괄호 다른 「같은 이름」 덤 빼기(씨앗 켬 · 끔) +- 실행 = 저장소 `venv/Scripts/python.exe -B s20_c3_extra.py `(C3 폴더에서 · 각 20 초 안). +- 시트 꼴 확인 = 읽기 전용 한 번(시트 이름 · 파일 머리 바이트 · 거창은 지금 읽기 함수로 읽어 봄). + +### C. 바깥 출처 대표 (C2 에서) + +- 다산 EST 공지 — 일위대가 · 단가산출은 「'코드일괄변경'이 적용되지 않으므로」 검색 뒤 [코드변경](사람이 고름) — https://www.dasansoft.com/customer/?mname=customer01&file=dd_view&table=notice&mode=view&id=4335&grp=4321&sub=3&pageN=1 +- 한국조달연구원(2020) 공사코드 표준화 연구 — 「표준 공사코드와 내역항목의 일치 및 수정에 대한 방법론은 구축되어 있지 않은 상태」 — https://www.kip.re.kr/src/uploads/20221121/%E2%98%85(%ED%99%95%EC%A0%95)+%EA%B3%B5%EC%82%AC%EC%BD%94%EB%93%9C+%ED%91%9C%EC%A4%80%ED%99%94%EB%A5%BC+%ED%86%B5%ED%95%9C+%EC%9C%A0%ED%98%95%EB%B3%84+%EA%B3%B5%EC%82%AC%EB%B9%84+%EC%B5%9C%EC%A2%85%EB%B3%B4%EA%B3%A0%EC%84%9C+1216.pdf +- 정상원 · 정기창(2020) 공종명 인식 — 글자 유사도 1순위 정답률 33.7~49.7% → 「후보 몇 개 + 전문가 고름」 권함 — https://www.koreascience.or.kr/article/JAKO202034352378495.pdf +- STmate 기준자료 갱신 매칭 = 정확 일치 열쇠 5 단 · 퍼지 없음 — 저장소 `resources/knowledge/original/경쟁사 프로그램/STmate 분석/20_분석/22_UI_폼_분석.md:116-124` +- OpenRefine 맞추기(후보 + 점수 + 사람이 고름) — https://openrefine.org/docs/manual/reconciling +- 산림 품셈 총칙 1-1 제7호 — `resources/knowledge/original/원가계산/산림_표준품셈/본문/제01장_적용기준/1-01_일반사항.md` · 이름만으로 자동 연결 금지 — `resources/knowledge/technical_info/01_임도/04_수량분석정보/건설품셈_활용.md:50` + +### D. 코드 자리 (다음 코딩 창용) + +| 고칠 것 | 자리 | +|---|---| +| 조항 윗마디(1) | `M02_MasterTemplete/M02_Hopyo_Basis.py` `_clause` :166-176 | +| 조항 오타(3) | `M02_Hopyo_Basis.py` `sources` :60-71 | +| 같은 이름 접힘(2) · 괄호 다른 덤(2-1) | `M02_MasterTemplete/M02_Hopyo_Match.py` `candidates` :320-325(접힘 열쇠 = 이름 · 규격 · 단위) · 이름 같음 덤 :314 · 정리 이름 `tidy` :79-84 | +| 「같음」 근거 검사(4) · 장비 줄(5) · 노임 줄(6) | `M02_Hopyo_Match.py` `match_line` :395-403 · 갈래 = `M02_Hopyo_Excel.py` `kind_of` | +| 기억 열쇠(22 · 24) | `M02_Hopyo_Match.py` `line_key` :68-76 · `recall` :348-350 · `confirm` :495-526 · 파일 `M02_Hopyo_Store.py` `memory` · `remember`(`실무자료/맞춤.json`) | +| 고르기 창(27~32) | `M02_MasterTemplete/M02_MasterTemplete_Hopyo_View.ts` `choose` :435-505(파일 614 줄) | +| 엑셀 읽기(39) | `M02_Hopyo_Excel.py` `_sheets` :74-102 | +| 시험 | `resources/tester/test_m02_hopyo_match.py` · `test_m02_hopyo_basis.py` · `test_m02_hopyo_excel.py` | From b7e72786309690620dd91820fc709b0ce2f43edb Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 15 Oct 2026 17:33:08 +0900 Subject: [PATCH 44/81] =?UTF-8?q?chore(=EC=9A=94=EA=B5=AC):=20226=20?= =?UTF-8?q?=E2=80=94=20CAD=20=EC=9E=85=EB=A0=A5=20=EB=B3=B4=EA=B0=95=20?= =?UTF-8?q?=EB=B0=94=EA=B9=A5=20=EC=A1=B0=EC=82=AC=20=EB=84=93=ED=9E=98(?= =?UTF-8?q?=EC=BD=94=EB=8D=B1=EC=8A=A4=20=C2=B7=20=EC=95=88=ED=8B=B0?= =?UTF-8?q?=EA=B7=B8=EB=9E=98=EB=B9=84=ED=8B=B0=20=C2=B7=20=EB=B8=8C?= =?UTF-8?q?=EB=A0=88=EC=9D=B8)=20=C2=B7=20=EC=99=B8=EB=B6=80=20=EC=9D=BC?= =?UTF-8?q?=EC=9C=84=EB=8C=80=EA=B0=80=20=EB=A1=9C=EC=A7=81=20=EC=A1=B0?= =?UTF-8?q?=ED=95=A9=20=EB=A7=9E=EC=B6=A4=20=EC=9E=AC=EC=A1=B0=EC=82=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01C6BNBFXFndbqYxu6sZuknK --- docs/raw/요구_1010_2/사용자요구_1010_2.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/raw/요구_1010_2/사용자요구_1010_2.md b/docs/raw/요구_1010_2/사용자요구_1010_2.md index 4af9ac3fb..d0ab3dd40 100644 --- a/docs/raw/요구_1010_2/사용자요구_1010_2.md +++ b/docs/raw/요구_1010_2/사용자요구_1010_2.md @@ -280,3 +280,5 @@ 3.오토캐드의 프로그램이 추가 보강 입력방법이 없는지 확인. 4. 외부에서 일위대가 가져오기 기능에서 이름이 달라 매칭이 안되는 문제를 개선 방법이 있는지 확인. 5. 3번과 4번은 코드 요청이 아니라 상세검토 요청임. 코드 작업은 검토 결과 보고 판단할 예정 +226. (원문 · 10-15) 3번 캐드항목에 대해 코덱스와 안티그래비티에게도 인터넷을 찾아보라고 하고 너도 추가로 더 찾아볼것. +4번 산림과 건설이 유일한 품셈정보이나 사용자들이 본인들의 편의대로 변경해서임. 우리 서버의 단가산출 로직이 정상이지만 우리서버를 바꾸더라도 같은 문제가 발생됨. 나는 우리서버측 단가산출 로직을 변경할 생각은 없음. 일위대가는 어차피 단가산출 로직의 조합이거나 일위대가일 수 밖에 없음. 그리고 중요한건 값이아니라 어떤 로직의 조합인지와 로직이 중요. 경우에 따라서는 한번 한 것들은 기억하는것도 좋음. 다시한번 정밀검토하여 정밀도를 높일 수 있는 방안 확보할 것. 3번/4번 재조사 From 42e4aa70ca78545fce5f2721bd248c995849373a Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 15 Oct 2026 18:38:06 +0900 Subject: [PATCH 45/81] =?UTF-8?q?chore(=EC=A1=B0=EC=82=AC):=20335=20CAD=20?= =?UTF-8?q?=EC=9E=85=EB=A0=A5=20=EB=B3=B4=EA=B0=95=20=EC=9E=AC=EC=A1=B0?= =?UTF-8?q?=EC=82=AC=20=E2=80=94=20=EC=BD=94=EB=8D=B1=EC=8A=A4=20=C2=B7=20?= =?UTF-8?q?=EC=95=88=ED=8B=B0=EA=B7=B8=EB=9E=98=EB=B9=84=ED=8B=B0=20=C2=B7?= =?UTF-8?q?=20=EB=B8=8C=EB=A0=88=EC=9D=B8=20=EB=B0=94=EA=B9=A5=20=EC=A1=B0?= =?UTF-8?q?=EC=82=AC=2053=20=EC=A4=84=20=C2=B7=20=EC=83=88=20=ED=9D=A0=20H?= =?UTF-8?q?13=20=C2=B7=20H14=20=C2=B7=20=EB=AC=B6=EC=9D=8C=20=E2=91=A7~?= =?UTF-8?q?=E2=91=A9=20=C2=B7=20=EC=A0=95=ED=95=A0=20=EA=B2=83=2015~23?= 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_01C6BNBFXFndbqYxu6sZuknK --- .../조사/2026-10-15_335_부록3_바깥조사원문.md | 519 ++++++++++++++++++ .../raw/조사/2026-10-15_335_재조사_CAD입력.md | 257 +++++++++ 2 files changed, 776 insertions(+) create mode 100644 docs/raw/조사/2026-10-15_335_부록3_바깥조사원문.md create mode 100644 docs/raw/조사/2026-10-15_335_재조사_CAD입력.md diff --git a/docs/raw/조사/2026-10-15_335_부록3_바깥조사원문.md b/docs/raw/조사/2026-10-15_335_부록3_바깥조사원문.md new file mode 100644 index 000000000..61f27adb5 --- /dev/null +++ b/docs/raw/조사/2026-10-15_335_부록3_바깥조사원문.md @@ -0,0 +1,519 @@ +# 부록3 — 바깥 조사 원문 (요구 226-3 · PLAN 335-3) + +- 본문 = `2026-10-15_335_재조사_CAD입력.md` · 아래 = 재료 여섯 원문(내용 손대지 않음 · 차례 = 코덱스 1차 → 코덱스 2차 → 안티그래비티 → 코덱스 3차 출처 대조 → B4 토목측량 → B5 웹 · 터치 · AI). +- 안티그래비티 출처는 코덱스 3차 대조 판정을 따름(본문 5-1). + + +--- + +## 원문 1 — `codex_1.md` · 코덱스 1차 — 해외 CAD(BricsCAD · ZWCAD · Onshape · Fusion · SOLIDWORKS · DraftSight · Rhino · SketchUp) + +# 코덱스 1차 (GPT-6-Luna medium · ctx_a0f7d41d525f) + +쓸모 상: BricsCAD Quad는 객체 위에 문맥 명령을 띄워 도구 선택·클릭을 줄이며, 우클릭/Tab으로 실행 또는 겹친 객체 순환을 지원함. BricsCAD Manipulator는 선택 객체를 축·평면·회전 핸들로 바로 이동/복사/회전하고 거리·각도를 입력하는 편집 방식임. Rhino SmartTrack은 임시 기준선·점으로 기존 형상 사이의 정렬/위치 추론을 제공함. Onshape 스케치 중 치수 입력은 형상을 그리면서 값을 바로 구속하고 Alt+방향키로 복수 치수 칸을 이동함. SketchUp 추론 잠금과 Fusion 우클릭 방사형 메뉴는 기존 입력과 대체로 같음. + +| 입력 방법 | 프로그램 | 입력 방식 | 임도 도면 손질 쓸모 | 출처 | +|---|---|---|---|---| +| Quad 문맥 커서 메뉴 | BricsCAD | 객체에 커서 올려 최근/객체별 명령 노출, 우클릭 실행, Tab으로 겹친 객체 순환 | 상 — 객체 종류별 편집 명령을 메뉴 탐색 없이 호출 | [공식 도움말](https://help.bricsys.com/document/_guides--BCAD_getting_started--GD_quadcursormenu/V23/EN_US?id=165079146598) | +| Manipulator 핸들 조작 | BricsCAD | 선택 후 축·평면·회전 핸들 드래그, Ctrl로 복사, 거리·각도 수치 입력 | 상 — 구조물 상세도 요소 이동·복사·회전과 수치 조정에 직접적 | [공식 도움말](https://help.bricsys.com/en-us/document/bricscad/modifying-entities/the-manipulator?version=V26) | +| 적응형 스냅 눈금자 | BricsCAD | 확대 수준에 따라 스냅 간격 자동 변화, 눈금자에서 정밀 간격 선택 | 중 — 상세도 확대/축소 때 스냅 밀도를 자동 맞춤; 기존 그리드 스냅의 자동 조절형 | [공식 도움말](https://help.bricsys.com/en-us/document/bricscad/modifying-entities/the-manipulator?version=V26) | +| Smart Mouse 제스처 | ZWCAD | 마우스 제스처에 명령/옵션을 지정하고 추가 제스처 등록 | 중 — 반복 편집 명령을 짧은 마우스 동작으로 실행; SolidWorks·DraftSight에도 같은 방식 | [제품 기술자료](https://confluence.zwcad.com/pages/viewpage.action?pageId=110934086) | +| Smart Voice 음성 메모 | ZWCAD | 점·영역·객체·발언자 위치에 음성 메모를 붙이고 아이콘 눌러 재생 | 하 — 입력 명령이 아니라 도면 검토사항/현장 설명 기록에 활용 | [제품 기술자료](https://confluence.zwcad.com/pages/viewpage.action?pageId=321161799) | +| 스케치 중 치수 구속 입력 | Onshape | 스케치 요소 작성 중 숫자/식을 입력, Alt+방향키로 여러 치수 입력칸 이동; 음수로 방향 전환 가능한 치수 있음 | 상 — 종단·횡단의 정형 단면이나 구조물 치수 스케치에 즉시 수치 지정 | [공식 도움말](https://cad.onshape.com/help/Content/Sketch/sketch_basics.htm) | +| 치수값 직접 편집/구동 전환 | Onshape | 치수 더블클릭 후 값 입력; 우클릭으로 구동/참조 치수 전환 | 중 — 제약 조건을 유지하는 수치 수정 가능; 웹 CAD 치수 편집에 참고 | [공식 도움말](https://cad.onshape.com/help/Content/Sketch/dimension.htm) | +| 스케치 S 단축 툴바 | Onshape | 활성 스케치에서 S를 눌러 사용자 지정 툴바 호출 | 중 — 선·호·치수 등 자주 쓰는 도구를 커서 근처에서 선택 | [공식 도움말](https://cad.onshape.com/help/Content/Sketch/sketch_basics.htm) | +| 치수 입력으로 사각형 생성 | Fusion | 첫 점 후 길이·폭 입력하고 다음 점 클릭; Alt+방향키는 기존 조사 동작과 같음 | 중 — 구조물 상세도 박스·단면 요소를 정확한 크기로 작성 | [공식 도움말](https://help.autodesk.com/cloudhelp/ENU/Fusion-Sketch/files/SKT-CREATE-RECTANGLES.htm) | +| 우클릭 방사형 Marking 메뉴 | Fusion | 캔버스 우클릭 후 방향으로 드래그해 부채꼴 명령 선택 | 중 — 이동·삭제·스케치 등 반복 명령을 포인터 근처에서 호출 | [공식 도움말](https://help.autodesk.com/cloudhelp/ENU/Fusion-GetStarted/files/GUID-6514ABC1-CB75-4F0B-AB0E-316FAD36BA93.htm) | +| 마우스 제스처 가이드 사용자화 | SOLIDWORKS | 우측 드래그 후 2·3·4·8·12방향 명령/매크로 가이드에서 선택 | 중 — 도면·스케치의 반복 명령 호출 단축; ZWCAD Smart Mouse와 같은 방식 | [공식 도움말](https://help.solidworks.com/2025/english/SolidWorks/sldworks/c_mouse_sestures.htm) | +| 마우스 제스처 | DraftSight | 우측 드래그 방향으로 명령 호출, 4/8방향 및 명령 재지정 | 중 — ZWCAD·SOLIDWORKS와 같은 제스처 입력 | [공식 도움말](https://help.solidworks.com/2023/english/DraftSight/additional/mg_Customizing_Mouse_Gestures.htm?id=1cec871ec38740e89f6da03d9c13cb83) | +| SmartTrack 임시 기준선·점 | Rhino | 점/객체에 커서를 두어 임시 추적점·선 생성, 교차 위치를 추론해 점 지정 | 상 — 평면·종단·횡단에서 기존 선형/구조물 기준 정렬점 지정에 유용 | [공식 도움말](https://docs.mcneel.com/rhino/8/help/en-us/user_interface/rhino_window.htm) | +| Gumball 직접 조작 및 Alt 복사 | Rhino | 객체 선택 후 이동·회전·축척 핸들 드래그; 드래그 중 Alt로 복사 | 중 — 상세도 요소의 직관적 변형; BricsCAD Manipulator와 같은 계열 | [공식 도움말](https://docs.mcneel.com/rhino/8/help/en-us/user_interface/keyboard%20modifiers.htm) | +| 기존 방식과 같음: 추론 잠금/축 잠금 및 치수 숫자 입력 | SketchUp | 이동 중 Shift 또는 방향키로 축 잠금, 숫자·좌표 입력 | 이미 조사한 항목과 같음 — 신규로 세지 않음 | [공식 도움말](https://help.sketchup.com/en/sketchup/moving-entities-around) | + +확인 범위는 사용자가 열거한 제품 중심의 공식 문서 검색이며, 각 기능이 웹 CAD에 구현 가능하거나 해당 라이선스에서 사용 가능하다는 뜻은 아님. ZWCAD Smart Voice는 음성 인식 명령이 아니라 도면에 붙이는 녹음 메모로 확인됨; Figma류 라벨 드래그 수치 조절, Shapr3D 펜/터치, GstarCAD·nanoCAD·QCAD·LibreCAD의 신규 입력 기능은 이번 자료에서 공식 근거를 확인하지 못해 제외함. + + +--- + +## 원문 2 — `codex_2.md` · 코덱스 2차 — 해외 CAD(Vectorworks · Archicad · ARES · GstarCAD · nanoCAD · QCAD · LibreCAD · FreeCAD · Shapr3D) + +# 코덱스 2차 (GPT-6-Luna medium · ctx_a9087afe50e2) + +쓸모 상: Vectorworks Snap Loupe는 Z 키로 커서 주변을 임시 확대해 복잡한 교차점에서 스냅 위치를 고르는 방식으로, 상세도 손질에 상급 후보임. Archicad Magic Wand는 기존 선·폴리라인의 연결 경계나 닫힌 영역을 따라 새 다각형을 생성하는 방식으로, 해치·영역 윤곽 작성에 상급 후보임. ARES A3는 자연어 한 문장으로 객체 선택과 이동·회전·해치 등 수정을 함께 요청하고, Kudo Professional은 음성 입력도 지원함. Archicad Tracker의 좌표값 증감 입력은 편집값 조정에, ARES QuickModify 반복 변환은 다수 복사·배치에 중간 후보임. Vectorworks Data Bar, GstarCAD·nanoCAD 동적 입력, QCAD 좌표 필터, ARES SmartSelect 등은 이미 조사한 입력·필터 개념과 같거나 가까운 항목임. + +| 입력 방법 | 판정 | 프로그램 | 어떻게 입력하나 | 임도 도면 손질 쓸모 | 출처 | +|---|---|---|---|---|---| +| Snap Loupe 커서 확대 스냅 | 신규 | Vectorworks | 스냅할 곳에 커서를 두고 Z, 필요하면 다시 Z를 눌러 임시 확대 후 목표 스냅점 클릭 | 상 — 촘촘한 상세도 교차·끝점에서 오선택 감소 | [공식 도움말](https://app-help.vectorworks.net/2027/eng/VW2027_Guide/Start/Streamline_drawing_interactions.htm) | +| Magic Wand 경계 추적 생성 | 신규 | Archicad | 새 요소 도구를 고른 뒤 Space를 누른 상태에서 기존 선/경계 또는 닫힌 영역 클릭 | 상 — 기존 선형으로 해치/폴리곤 영역이나 경계 형상 빠르게 생성 | [공식 도움말](https://help.graphisoft.com/AC/28/INT/_AC28_Help/030_Interaction/030_Interaction-53.htm) | +| A3 자연어 선택·수정 | 신규 | ARES Commander / Kudo | 자연어 한 프롬프트로 도면 요소를 선택하고 이동·회전·반전·삭제·해치·신축 등 수행; Commander 음성 미지원, Kudo Professional 음성 입력 지원 | 상 — 명령어를 외우지 않고 반복 객체 편집·해치 적용; 결과 확인 절차 필요 | [공식 제품 안내](https://www.graebert.com/cad-software/ares-commander/new-features/) | +| Smart Options Display 커서 문맥 도구 | 같음: Quad/방사형 메뉴 | Vectorworks | 설정한 키/동작으로 커서 옆에 즐겨 쓰는 도구·모드·스냅 옵션 표시 | 중 — 툴바 왕복 감소; BricsCAD Quad·Fusion Marking 메뉴와 유사 | [공식 도움말](https://app-help.vectorworks.net/2027/eng/VW2027_Guide/Start/Streamline_drawing_interactions.htm) | +| Floating Data Bar 숫자 입력 | 같음: 동적 입력 | Vectorworks | 커서 옆 Data Bar에 거리·각도 등 표시; Tab 또는 숫자 입력으로 칸 활성화·고정 | 중 — 위치 고정형 숫자 입력이며 기존 DYN의 커서 숫자칸과 동일 계열 | [공식 설정 도움말](https://app-help.vectorworks.net/2025/eng/VW2025_Guide/Basic1/Data_bar_options.htm) | +| Tracker 좌표값 증감 | 보강점 | Archicad | Tracker에서 X3+ / Y-4-처럼 입력해 현재 좌표값에 증감; /로 XY와 거리·각도 전환 | 중 — 단면·상세 편집 중 기준 위치에서 일정량 이동 시 편리; 좌표 입력 자체는 기존과 같음 | [공식 도움말](https://help.graphisoft.com/AC/26/INT/_AC26_Help/030_Interaction/030_Interaction-41.htm) | +| QuickModify 반복 변환 | 보강점 | ARES Commander | 기준점과 객체 선택 후 Copy/Move/Rotate/Scale을 한 명령 안에서 반복하고 Exit | 중 — 동일 부재·기호를 연속 배치하는 반복 편집에 유용; 조작 자체는 Manipulator와 유사 | [공식 도움말](https://onlinehelp.graebert.com/EN/ARES_Content/25/additional/hlpid_edit_quickmodify.htm) | +| SmartSelect 속성 필터 | 같음: 빠른 선택/거르개 | ARES Commander | 객체 종류·속성·연산자·값을 지정해 도면 전체/현재 선택에서 대상 집합 생성 | 중 — 레이어·유형별 일괄 수정에 적합하나 기존 Quick Select/필터와 같은 기능 | [공식 도움말](https://onlinehelp.graebert.com/EN/ARES_Content/25/html/hlpid_query_qselect.htm) | +| 입력 방식별 펜·터치 동시 사용 | 낮음 | Shapr3D | 호환 기기에서 펜·터치 제스처 사용, 키보드 핫키 또는 명령 검색과 병용 | 하 — 태블릿 외근/현장 검토에는 유용 가능; 정밀 2D 도면 입력 직접성 낮음 | [공식 도움말](https://support.shapr3d.com/hc/en-us/articles/7873906073884-Keyboard-shortcuts-gestures-and-hotkeys) | +| 치수 라벨 드래그로 숫자 변경 | 확인 못 함 | Figma 류 | 요청된 동작의 공식 도움말/제품 문서 근거를 이번 조사에서 확인 못 함 | 판정 보류 — CAD 치수에 같은 조작을 적용할 근거 미확인 | 공식 근거 미확인 | +| 동적 입력 분리 포인터/치수칸 | 같음: 동적 입력·Tab 이동 | GstarCAD 2024+ | 커서 옆 분리 필드에 좌표·거리·각도 표시, Tab으로 칸 이동·값 편집; 프롬프트 옵션 목록 표시 | 중 — 이미 조사한 동적 입력과 같고 분리형 UI와 옵션 목록이 보강점 | [공식 도움말](https://www.gstarcad.com/help/GstarCAD_2024_zh-CN/detail/GstarCAD2024NEW/Dynamicdimensioninput.html) | +| Dynamic Input 커서 프롬프트 | 같음: 동적 입력 | nanoCAD | 커서 옆에 명령 프롬프트·좌표·치수 필드를 띄우고 Tab·화살표로 항목 선택 | 중 — 기존 동적 입력과 같음 | [공식 도움말](https://nanocad.com/learning/online-help/nanocad-platform/dynamic-input/) | +| QCAD 좌표 추출 스냅 | 같음: 점 거르개(.X/.Y) | QCAD | X/Y from Points에서 한 점의 X와 다른 점의 Y를 순서대로 잠금 | 중 — 기존 점 거르개와 같음 | [공식 설명서](https://www.qcad.org/doc/qcad/latest/reference/en/qcad_reference_manual_en.html) | +| LibreCAD 특수 좌표 입력 | 확인 범위에서 신규 없음 | LibreCAD | 공식 설명서의 명령줄 절대·상대·극좌표 입력은 기존 조사 항목과 같음 | 신규 판정 없음 | [공식 설명서](https://docs.librecad.org/en/latest/ref/fundamentals.html) | +| FreeCAD Sketcher / SolveSpace 구속 치수 | 같음: Onshape 치수 구속 | FreeCAD / SolveSpace | 스케치 구속·치수 라벨 편집으로 기하를 구동하거나 참조값으로 표시 | 신규 판정 없음 — Onshape의 구동/참조 치수와 같은 계열 | [FreeCAD 도움말](https://reqrefusion.github.io/FreeCAD-Documentation-html/wiki/Sketcher_Dimension.html), [SolveSpace 설명서](https://solvespace.readthedocs.io/en/latest/constraints/) | + +progeCAD·TurboCAD·CorelCAD의 독립적인 새 입력 방식과 Figma 숫자 라벨 스크러빙은 이번 검색에서 신뢰할 공식 근거를 찾지 못해 확인 못 함으로 남김. 파일 변경·코드 작업 없음. + + +--- + +## 원문 3 — `agy_1.md` · 안티그래비티 — 국내 실무 리습 · 국산 CAD · 국내 측량(28 가지) + +# 안티그래비티 (agy -p · effort high) + +### 「쓸모 상」 핵심 입력 방법 요약 (웹 CAD 임도 도면 손질 관점) + +- **측점(Station) 및 편측 오프셋(Offset) 직교 입력**: 임도 중심선을 따라 20m 정측점 및 플러스 측점을 일괄 타각(`DSTA`)하고, 선형 기준 이격거리(`STA 3+15, L 4.0`)로 집수정·배수관·소단·옹벽 등 구조물 위치를 직접 스냅하는 방식은 평면도 손질의 핵심 기반임. +- **토공 사면 경사비(1:n) 구속 및 소단(Berm) 단차 전개**: 산지 지형 특성상 절토·성토 비탈면 작도가 빈번하므로, 사면 경사비(1:1.5, 1:1.2 등) 각도 구속 스냅과 5m 높이별 소단 자동 전개(`DSLO`), 법면 빗금 기호(`TALUS`) 일괄 생성이 필수적임. +- **종단 계획 구배율(%) 직접 입력 및 표고(EL) 자동 계산**: 산지관리법상 임도 최대 종단경사 기준(13~18% 이내) 준수를 위한 구배율(%) 직결 고정 입력과, 기준 BM 대비 Y축 변위를 자동 환산하는 표고 주기(`EL`) 입력이 종단도 손질에 직결됨. +- **한글 자판 상태 단축키 자동 보정 (IME 가상 매핑)**: 한글 주석·측점 작성 후 영문 전환 없이 `ㅣ(Line)`, `ㅡ(Move)`, `채(Copy)`를 쳐도 즉시 명령이 실행되는 한영 자동 보정은 브라우저 웹 CAD 환경에서 실무자의 조작 피로도를 획기적으로 낮춤. +- **도면 텍스트 사칙연산(`CTC`) 및 연속 연장 합산(`TLEN`)**: 선형 및 종단 계획 변경 시 도면 내 지반고·계획고 수치를 일괄 가감승제하고, 측구·난간·암거의 총 연장 수량을 다중 선택으로 도면에 자동 기입하는 기능이 수량 검토 작업 속도를 결정함. +- **현장 측량 데이터(XYZ/PNT) 및 엑셀 표 양방향 연동**: GNSS/광파기 야장(점번호, X, Y, Z) 원클릭 도면 전개와 토적표·수량표의 캐드-엑셀 간 상호 복사(`CTE`/`ETC`)가 웹 CAD의 현장 실무 호환성을 좌우함. + +--- + +### 국내 실무 CAD 입력 방법 조사 결과 표 + +| 번호 | 이름 | 어디서 (프로그램·리습) | 어떻게 입력하나 (입력 시퀀스) | 임도 도면 손질 쓸모 (상·중·하 + 까닭) | 출처 URL | +| :---: | :--- | :--- | :--- | :--- | :--- | +| 1 | **선형 측점 일괄 표기 (DSTA)** | 드림플러스 / ZDream | 폴리선 선택 → 시작 측점(예: 0+00) 및 간격(예: 20m) 입력 → 엔터 시 직각 틱 마크 및 NO. 텍스트 일괄 생성 | **상** — 임도 중심선에 20m 간격 정측점 및 주요 변화점 일괄 배치 필수임 | [dreamcad.net](https://dreamcad.net) | +| 2 | **측점·오프셋 조회 주기 (STT/GST)** | 드림플러스 / 토목 리습 | 중심선 클릭 → 도면상 임의 지점 클릭 → 측점 및 좌우 이격거리(`STA 2+14.5, L 3.2m`) 지시선 텍스트 자동 삽입 | **상** — 배수관, 암거, 집수정 등 부속 구조물 위치를 평면도에 즉시 주기함 | [dreamcad.net](https://dreamcad.net) | +| 3 | **계획 사면 자동 작도 (DSLO)** | 드림플러스 / ZDream | 노견 기준선 클릭 → 경사비(예: 1:1.5) 및 소단 조건 입력 → 자연지반선 클릭 시 법면선 자동 교차 작도 | **상** — 임도 횡단면 절토·성토 사면 및 5m 소단 작도의 핵심 기능임 | [dreamcad.net](https://dreamcad.net) | +| 4 | **법면 빗금 기호 작도 (TALUS)** | 토목 LISP (`talus.lsp`) | `TALUS` 입력 → 사면 어깨선(Top)과 발선(Toe) 연속 클릭 → 빗금 간격 입력 시 긴선·짧은선 기호 자동 생성 | **상** — 임도 평면도 상에서 절·성토 비탈면 범위를 나타내는 표준 제도 기호임 | [gocad.co.kr](https://www.gocad.co.kr) | +| 5 | **경사비(1:n) 및 구배 표기 (SLP)** | 토목 LISP (`slope.lsp`) | `SLP` 입력 → 경사선 위 두 점 클릭 → 경사비(1:1.5) 또는 구배(%) 선택 시 직각삼각형 기호와 수치 자동 기입 | **상** — 횡단 비탈면 경사 및 종단 노면 종단구배 표기에 직접 활용됨 | [gocad.co.kr](https://www.gocad.co.kr) | +| 6 | **표고 기호 및 높이차 계산 (EL)** | 토목/건축 LISP (`el.lsp`) | `EL` 입력 → 기준점 클릭 및 기준 표고 입력 → 대상점 클릭 시 Y축 변위를 가산하여 역삼각 기호와 `EL=수치` 자동 삽입 | **상** — 종·횡단면도에서 임도 계획고·지반고 손질 시 필수 계산 입력임 | [gocad.co.kr](https://www.gocad.co.kr) | +| 7 | **다중 선분 총 연장 합산 (TLEN)** | 범용 LISP (`tlen.lsp`) | `TLEN` 입력 → 선·호·폴리선 다중 선택 → 엔터 시 총 연장 출력 및 도면 클릭 지점에 합산 텍스트 기입 | **상** — 측구수로관, 가드레일, 낙석방지망 등 임도 시설물 총 연장 산출에 직결됨 | [lee-mac.com](https://www.lee-mac.com/totallength.html) | +| 8 | **면적 텍스트 자동 기입 (AR/DLA)** | 토목 LISP (`ar.lsp`) | `AR` 입력 → 닫힌 구역 내부점 클릭 → m² 단위 및 자릿수 지정 시 클릭 위치에 산출 면적 텍스트 자동 작성 | **상** — 횡단면 토공(절토·성토) 단면적 산출 및 용지 편입 면적 확인에 필수임 | [dreamcad.net](https://dreamcad.net) | +| 9 | **숫자·문자 자동 증분 넘버링 (TCOUNT)** | AutoCAD Express / 국산 CAD | `TCOUNT` 입력 → 텍스트 다중 선택 → 정렬 축/선택순 지정 → 시작값·증분값(예: 1, 1) 입력 시 일괄 치환 | **중** — 단면 번호, 측점 번호, 집수정 일련번호 순차 부여 작업에 유용함 | [help.autodesk.com](https://help.autodesk.com) | +| 10 | **도면 텍스트 숫자 사칙연산 (CTC)** | 드림플러스 / ZDream | `CTC` 입력 → 연산자(+ - * /) 및 상수 입력 → 도면 내 숫자 텍스트들을 드래그 선택 시 표기 수치 일괄 연산 갱신 | **상** — 계획고 변경이나 표고차 일괄 가감 시 텍스트 재입력 없이 즉시 갱신함 | [dreamcad.net](https://dreamcad.net) | +| 11 | **캐드 표 ↔ 엑셀 양방향 변환 (ETC/CTE)** | 드림플러스 / ZDream | `ETC` 입력 후 캐드 표(선·문자) 드래그 시 엑셀 시트 자동 생성 (반대로 엑셀 복사 후 `CTE` 입력 시 캐드 표 생성) | **상** — 토적표, 수량집계표, 측점좌표표를 엑셀과 연동하여 손질할 때 필수적임 | [dreamcad.net](https://dreamcad.net) | +| 12 | **숫자 키 원터치 레이어 토글 (11/22/33)** | 국내 실무 단축 LISP | 객체 선택 후 `11`(선택 레이어만 격리), `22`(전체 켜기), `33`(선택 레이어 끄기) 입력 | **상** — 등고선, 도로중심선, 측점문자 등 복잡한 토목 도면층을 초고속으로 제어함 | [gocad.co.kr](https://www.gocad.co.kr) | +| 13 | **TM 국가좌표 지시선 주기 (XY)** | 드림플러스 / 토목 LISP | `XY` 입력 → 대상 위치 클릭 → 지시선 끝점 클릭 시 `X(N): ..., Y(E): ...` 형식으로 국가 TM 좌표 자동 표기 | **중** — 임도 교각점(IP), 시·종점, 주요 구조물 설치 위치의 현장 측량 좌표 표기에 사용됨 | [dreamcad.net](https://dreamcad.net) | +| 14 | **지형도 교차 종단 지반고 추출 (GRP)** | 드림플러스 / ZDream | 수치지형도 3D 등고선 선택 → 평면 중심선 선택 → 종단 원점 클릭 시 종단면도 지반선 및 밴드 일괄 자동 작도 | **상** — 수치지형도로부터 임도 종단면도를 초기 생성하고 손질하는 핵심 도구임 | [dreamcad.net](https://dreamcad.net) | +| 15 | **평면 선형 기반 횡단 지반선 추출 (CFM)** | 드림플러스 / ZDream | 횡단 폭(좌우 15m) 및 측점 지정 → 3D 지형도 선택 시 측점별 횡단 지반선 및 그리드 일괄 작도 | **상** — 임도 횡단면도 토공 단면을 추출하고 구조물을 얹는 기본 바탕임 | [dreamcad.net](https://dreamcad.net) | +| 16 | **한글 자판 단축키 자동 보정 (IME 변환)** | 캐디안 / 드림플러스 (HCMD) | 한글 입력 상태에서 `ㅣ`(L), `ㅡ`(M), `ㅊ`(C), `채`(CO) 입력 시 영문 전환 없이 해당 명령 즉시 실행 | **상** — 한글 측점·주석 작성 후 명령 입력 시 한/영 전환 오입력을 완전히 방지함 | [cadian.com](https://www.cadian.com) | +| 17 | **한글 자연어 명령어 직접 입력** | 캐디안 (CADian) | 명령줄에 `선`, `원`, `복사`, `이동`, `지우기` 등 한국어 단어 입력 후 엔터 시 해당 캐드 명령 실행 | **중** — 단축키에 미숙한 산림 토목 현장 담당자 및 초심자에게 직관성을 제공함 | [cadian.com](https://www.cadian.com) | +| 18 | **마우스 제스처 명령 입력 (SmartMouse)** | ZWCAD (지더블유캐드) | 우클릭 드래그로 화면에 궤적(←: Undo, →: Redo, ㄴ: Erase, 대각선: Zoom Extents 등)을 그리면 명령 실행 | **중** — 뷰 이동, 명령 취소 등 빈번한 단순 보조 입력을 마우스 한 손으로 신속 처리함 | [zwsoft.co.kr](https://www.zwsoft.co.kr) | +| 19 | **도면 음성 메모 입력 (SmartVoice)** | ZWCAD (지더블유캐드) | 도면 객체 클릭 후 마이크 녹음 → 도면에 확성기 심볼 생성 → 심볼 클릭 시 현장 수정 지시 음성 재생 | **중** — 웹 환경에서 설계 검토자 간 구조물 수정 위치 및 지시 사항 전달에 유용함 | [zwsoft.co.kr](https://www.zwsoft.co.kr) | +| 20 | **증분 복사 입력 (Incremental Copy)** | 지스타캐드 / ZWCAD | 숫자가 포함된 문자 복사 시 증분 모드 적용 → 마우스 클릭 시마다 숫자 부분이 1씩 자동 증가하여 부착 | **상** — 측점 번호(NO.0, NO.1..), 배수관 일련번호를 연속 복사 배치할 때 극도로 효율적임 | [gstarcad.co.kr](https://www.gstarcad.co.kr) | +| 21 | **비대칭 축척(H/V) 종단 그리드 생성** | 지스타캐드 Magic / 드림플러스 | 수평 축척(1:1000)과 수직 축척(1:100)을 분리 입력하여 왜곡 비율이 적용된 종단 그리드 및 도각 자동 삽입 | **상** — 고저차를 과장 표현해야 하는 국내 토목·임도 종단면도의 표준 규격 작도에 필수임 | [gstarcad.co.kr](https://www.gstarcad.co.kr) | +| 22 | **측점 + 편측 오프셋 좌표 스냅 입력** | 국내 도로설계 S/W (INFRACAD 등) | 명령줄에 `STA 3+15.5, L 4.0` 입력 → 기준 선형의 측점 3+15.5 위치에서 좌측 4.0m 수직 지점에 좌표 스냅 | **상** — 임도 중심선 기준으로 측구, 소단, 비탈면 경계점을 도면에 직접 작도할 때 유용함 | 확인 못 함 | +| 23 | **방위각(도분초) + 거리 입력** | 토목 측량 CAD 패키지 | 방위각 `125.3020`(125°30'20")과 거리 `45.2m` 입력 → 기준점에서 북 기준 방위각 방향으로 직선 분절 작도 | **상** — 산림 경계, 필지 경계선, 노선 교각점(IP) 측량 야장 데이터를 직접 전개할 때 쓰임 | [gocad.co.kr](https://www.gocad.co.kr) | +| 24 | **사면 경사비 구속 입력 (Slope Lock)** | 토목 횡단 설계 도구 | 경사선 작도 중 `1:1.5` 또는 `1:1.2` 입력 → 마우스 커서 이동 각도가 해당 사면 경사선 상으로 고정 구속됨 | **상** — 수식 계산 없이 커서 드래그만으로 지반선 교점까지 절·성토 비탈면선을 원클릭 작도함 | 확인 못 함 | +| 25 | **종단 계획 구배율(%) 직접 입력** | 도로·임도 종단 설계 패키지 | 종단 변곡점에서 `i = 5.5%` 입력 → 수평거리 대비 높이 각도가 자동 고정되어 다음 계획선까지 연속 작도 | **상** — 산지관리법 임도 최대 종단경사(13~18%) 규정 준수 여부를 즉각 반영하여 작도함 | 확인 못 함 | +| 26 | **종단 곡선 제원(VCL / R) 입력** | 도로·임도 종단 설계 패키지 | 종단 변곡점에서 곡선장 `VCL=40` 또는 반경 `R=300` 입력 → 두 경사선 사이에 접선 포물선 호 자동 생성 | **상** — 종단 계획선에서 볼록·오목 종단곡선을 오토캐드 원호 명령 없이 정밀 작도함 | 확인 못 함 | +| 27 | **측량 좌표(CSV/TXT) 일괄 전개 (PNT)** | 드림플러스 / ZDream (CIM) | `점번호, N, E, Z, 코드` 포맷 파일 드래그 앤 드롭 → 점 심볼, 점번호, 표고 텍스트를 레이어별 일괄 전개 | **상** — GNSS/토털스테이션 현장 측량 데이터를 웹 CAD 캔버스에 즉시 로드하는 기본 입력임 | [dreamcad.net](https://dreamcad.net) | +| 28 | **소단(Berm) 계단식 단차 자동 전개** | 토목 사면 모듈 | 비탈면 높이 5m 도달 시 소단폭 1.0m 및 배수 역경사(4%) 입력 → 지반선 접점까지 계단식 단차 연속 생성 | **상** — 깊은 절토 사면에서 암반 안정성 확보를 위한 계단식 소단 상세도 손질에 필수적임 | [dreamcad.net](https://dreamcad.net) | + + +--- + +## 원문 4 — `codex_3.md` · 코덱스 3차 — 안티그래비티 28 가지 출처 대조 + +# 코덱스 3차 — 안티그래비티 결과 출처 대조 (ctx_c76434261edc) + +확인 2 · 일부 13 · 못 찾음 13 + +| 번호 | 판정 | 대조 결과 · 바른 내용 | 바른 출처 URL | +|---:|---|---|---| +| 1 | 확인 | DSTA의 선형 측점 표기와 간격 지정, 선형 직각 표기 확인. 세부 설정은 프로그램 창 도움말 기준임 | [드림플러스 DSTA 안내](https://dreamcad.net/bbs/board.php?bo_table=qna&wr_id=16553) | +| 2 | 일부 | STT는 선택 측점 표기, GST는 측점 조회로 확인. L/R 오프셋 지시선 자동 삽입 순서는 확인 못 함 | [STT 안내](https://www.dreamcad.net/bbs/board.php?bo_table=qna&wr_id=22368), [DSTA/GST 안내](https://www.dreamcad.net/bbs/board.php?bo_table=qna&wr_id=12839) | +| 3 | 못 찾음 | DSLO 및 제시한 사면·소단 입력 절차를 출처에서 확인 못 함 | 드림플러스 일반 홈페이지뿐, 기능 페이지 미제시 | +| 4 | 못 찾음 | 고캐드 홈 주소만으로 TALUS LISP와 입력 순서 확인 불가 | 기능을 설명하는 출처 못 찾음 | +| 5 | 못 찾음 | SLP 명령과 선택 순서를 설명하는 출처 못 찾음 | 기능을 설명하는 출처 못 찾음 | +| 6 | 일부 | 제시된 일반 EL 명령의 동작은 확인 못 함. 드림플러스에는 FE 상대 EL, GEL 두 점 사이 EL 등 서로 다른 명령이 있음 | [FE/GEL 안내](https://dreamcad.net/bbs/board.php?bo_table=qna&wr_id=13168) | +| 7 | 일부 | Lee Mac TLEN은 선·호·폴리선 등을 선택해 총 연장을 계산하지만 결과는 명령줄 출력임. 도면 클릭 위치에 합산 문자를 쓰는 기능은 아님 | [Lee Mac Total Length](https://lee-mac.com/totallengthandarea.html) | +| 8 | 못 찾음 | AR/DLA 및 내부점 클릭 후 면적 문자 작성 순서 확인 못 함 | 드림플러스 일반 홈페이지뿐, 기능 페이지 미제시 | +| 9 | 일부 | AutoCAD Express TCOUNT의 객체 선택, X/Y/선택순 정렬, 시작·증분 및 덮어쓰기/접두/접미 순서 확인. 국산 CAD 호환은 이 출처로 확인 못 함 | [Autodesk TCOUNT 한국어 도움말](https://help.autodesk.com/cloudhelp/2025/KOR/AutoCAD-Core/files/GUID-4B92219C-DA94-4D71-B308-D3818D3C6B8F.htm) | +| 10 | 일부 | CTC 문자 사칙연산은 확인 못 함. 드림플러스 공지에는 CALC의 문자 기능으로 사칙연산하는 내용 확인; 명령명 수정 필요 | [드림플러스 업데이트 공지](https://www.dreamcad.net/bbs/board.php?bo_table=notice&wr_id=80) | +| 11 | 일부 | ETC는 엑셀 표→캐드, CTE는 캐드 표→엑셀임. 제시 표의 방향이 뒤바뀜. 업데이트 기능은 양쪽 확인, CAD 선·문자 선택 등 제시 순서 일부 확인 | [드림플러스 공식 Q&A](https://dreamcad.net/bbs/board.php?bo_table=qna&wr_id=30570) | +| 12 | 일부 | 고캐드 예시는 11/22/33에 레이어 1/2/3의 켜기·현재 설정·다른 층 끄기를 사용자 LISP로 정의한 사례임. 선택 객체 레이어 격리/전체 켜기/끄기라는 보편 단축키는 아님 | [고캐드 LISP 코드 사례](https://www.gocad.co.kr/%EC%9E%90%EB%A3%8C%EC%8B%A4/?act=procFileDownload&file_srl=2095880&mod=document&module=file&pageid=453&q=%EC%9E%90%EB%A3%8C%EC%8B%A4%2F&sid=3bd9beccd8c7e82101307b2ffc710d66&uid=4770) | +| 13 | 일부 | DreamPlus XY 좌표 쓰기와 좌표축 설정 확인. X(N)/Y(E) 형식 및 클릭 후 지시선 종점 순서는 출처에서 확인 못 함 | [XY 사용 안내](https://dreamcad.net/bbs/board.php?bo_table=qna&wr_id=15273) | +| 14 | 일부 | GRP는 Z값 지형의 선형을 선택해 종단을 추출함. 원점 클릭·밴드 자동 작성 등 제시한 상세 시퀀스는 해당 도움말에서 확인 못 함 | [드림플러스 GRP 설명](https://www.dreamcad.net/bbs/board.php?bo_table=product&wr_id=3) | +| 15 | 일부 | CFM은 Z값 지형에서 횡단 추출. 측점 간격 추출, 기존 횡단선 선택, Excel 측점 목록, 좌우 편측 폭 등 모드 확인; 단순히 측점·폭 입력 후 3D 지형을 고르는 순서로 한정 불가 | [드림플러스 CFM 설명](https://www.dreamcad.net/bbs/board.php?bo_table=product&wr_id=4) | +| 16 | 일부 | HCMD는 한글 상태에서 명령을 입력해 ‘알 수 없는 명령’이 되면 영문으로 바꿔 재실행하는 드림플러스 기능임. 자모/한글 글쇠를 영문 키에 가상 매핑하는 기능은 아님; CADian 근거도 못 찾음 | [HCMD 공식 답변](https://dreamcad.net/bbs/board.php?bo_table=qna&page=197&wr_id=17336) | +| 17 | 못 찾음 | CADian 한글 자연어 명령어 입력 근거를 공식 문서에서 확인 못 함 | CADian 제품 소개는 해당 기능 설명 없음 | +| 18 | 일부 | SmartMouse 우클릭 드래그와 방향/패턴 제스처, 명령 사용자 지정 확인. ← Undo/→ Redo/ㄴ Erase/대각 Zoom Extents 고정 배정은 확인 못 함 | [ZWCAD 공식 안내](https://www.zwsoft.com/user-guide/zwcad-smart-features) | +| 19 | 확인 | SmartVoice는 점·객체·영역에 녹음 메모 연결, 녹음 버튼 누르고 있는 동안 녹음, 놓으면 재생 버튼 표시. ‘확성기 심볼’은 정확한 UI 설명이 아님 | [ZWCAD 공식 안내](https://www.zwsoft.com/user-guide/zwcad-smart-features) | +| 20 | 일부 | GstarCAD Architecture 설명서에 문자 숫자/문자 부분 선택→기준점→배치점마다 증분 복사 확인. GstarCAD Architecture 근거이며 ZWCAD 일반 기능이라는 근거는 없음 | [GstarCAD Architecture 안내 PDF](https://cdn-sg-gw.gstarcad.net/gstarsoft_pdf/GstarCAD-Architecture-2021-User-Guide.pdf) | +| 21 | 못 찾음 | 제시된 GstarCAD 도메인에서 비대칭 축척 종단 그리드·도각 자동 삽입을 설명하는 근거 못 찾음 | 기능 출처 미제시 | +| 22 | 못 찾음 | INFRACAD 등 제품명·명령 도움말·직접 출처가 없어 측점+편측 오프셋 입력 확인 못 함 | 출처 URL 미제시 | +| 23 | 못 찾음 | 고캐드 홈 주소에서 방위각·거리 입력 명령과 순서 확인 못 함 | 기능을 설명하는 출처 못 찾음 | +| 24 | 못 찾음 | Slope Lock 명령이나 입력 절차 근거 없음. 1:n 등 도메인 숫자 — 지식DB 확인 몫 | 출처 URL 미제시 | +| 25 | 못 찾음 | 구배율 고정 입력·계획선 연속 작도 근거 없음. 임도 최대 경사 숫자 — 지식DB 확인 몫 | 출처 URL 미제시 | +| 26 | 못 찾음 | VCL/R 입력으로 포물선 종단곡선 자동 작성하는 근거 없음. 곡선 제원 숫자 — 지식DB 확인 몫 | 출처 URL 미제시 | +| 27 | 일부 | CIM은 Excel 좌표를 CAD 점·선 또는 3D 폴리선 등으로 가져올 수 있음. 파일 드래그앤드롭, 점번호·코드 포함 5열 포맷, 레이어별 전개 순서는 출처에서 확인 못 함 | [드림플러스 CIM 사용 예](https://dreamcad.net/bbs/board.php?bo_table=qna&wr_id=14564) | +| 28 | 못 찾음 | 소단폭·배수 역경사 등을 입력해 자동 전개하는 사면 모듈 출처 확인 못 함 | DreamPlus 일반 홈페이지뿐, 기능 페이지 미제시 | + +도메인 숫자 13~18% 등은 판정하지 않았으며, 해당 항목에 지식DB 확인 몫으로만 표시함. + + +--- + +## 원문 5 — `B4_토목측량입력.md` · B4 — 토목 · 도로 · 측량 전용 입력 + +# B4 — 토목 · 도로 · 측량 전용 CAD 입력 조사 (요구 226-3) + +- 기준 = `main_laptop_1` b7e727863 · 인터넷 공식 문서 먼저(제3자 · 포럼은 표기) · 우리 코드는 grep · 짧은 읽기만 · 코드 고침 없음. +- 겹침 뺌 = 335(오토캐드 71 · 우리 현황) · 코덱스 1 · 2차(BricsCAD · Rhino · Onshape 등) — 같은 것은 「같음 + 번호」 한 마디만. +- 기호 — 우리: 있음 · 일부 · 없음 · 쓸모: 상 · 중 · 하(임도 도면 손질 기준 · 짐작 포함) · [R..] = 맨 끝 출처. + +## 0. 결론 + +1. 토목 CAD 입력의 바탕 = 도면 좌표 ↔ 실값(측점 · 오프셋 · 표고 · 경사 %)을 아는 「뷰 · 노선 객체」 — 사용자는 종이 mm 가 아니라 실값을 침(Civil 3D 투명 명령 · Civil AccuDraw · TBC COGO 칸 모두 같은 틀). +2. 우리 CAD = 종이 mm 도형 + 제목 글(「2+0.0」 · 「STA.0+050.000」)만 받음 → 아래 「상」 거의 전부 지금은 불가 · 엔진이 이미 계산하는 장 · 블록별 변환(축척 · 원점 · 기준 표고 · 시작 측점 · 노선 표지)을 실음 메시지에 덧붙이면 길이 열림(3장). +3. 상 · 없음(평면) — 측점 + 오프셋 점(P1 `'SO` · P2 Civil AccuDraw · P3 TBC 선 따라 거리 + 오프셋 = 노선 객체 없이 폴리선만으로 됨) · 측점 · 오프셋 조회(P6) · 측점 · 오프셋 글씨(T1) · 측점 꼴 `No.n+d` 입력(P7 · 20 m 번호 꼴 = Trimble 「Station index」). +4. 상 · 없음(종단) — 측점 · 표고 점(L1 `'PSE` · 우리 종단 가로 1/1000 · 세로 1/200 = 세로 5 배 → 손 계산 없앰) · 경사 % + 측점 · 길이 선(L2 `'PGS` · `'PGL`) · 측점 · 표고 / 두 점 경사 글씨(T3). +5. 상 · 없음(횡단) — 오프셋 · 표고 표 입력 + 앞 점에서 경사 · 비율 → 표고(C1 Carlson) · 커서 자리 실값 읽기(C3) · 오프셋 · 표고 / 두 점 경사 글씨(T2) · 실거리 단위 붙여 치기 「3m」 → 축척대로 30 mm(C7 TBC). +6. 조사 중 찾은 빈틈 — 우리 치수 글 = 종이 거리 그대로 → 횡단 1/100 에서 3 m 를 재면 「30」 · 평면 1/1200 은 「2.5」(T4 · 오토캐드 DIMLFAC 류 없음) — 축척 정보 하나로 C7 과 함께 풂. +7. 정할 것 — 오프셋 부호: 우리 엔진 = +좌 / −우 · Carlson · GEOPAK · TBC = 좌 − · 우 + → 입력 칸 부호는 사용자 협의로. +8. MicroStation AccuDraw 는 대부분 기존 조사와 같음(동적 입력 · 직교 · FROM · 각도 잠금) — 새것 = 나침반을 고른 요소 방향으로 돌림(M4) · 잠긴 값에 Enter 또 = 그만큼 더함(P2) · 가 스냅 뒤 확정(M5) — 모두 중. +9. 설계 객체 표 편집(PVI 표 · 끌기 고정 꼴 · 정렬 표)은 설계 정본 몫(B05 · B06) — CAD 손질엔 하(L6). +10. 국내 임도 전용 설계 프로그램 = 확인 못 함 · 국내 유틸 KTOOL(엑셀 ↔ 캐드 종단) 하나 확인 — 우리 B05 → B07 자동과 같은 일(K1). + +## 1. 표 + +### 1-1. 평면 · 측점 + +| # | 이름 | 프로그램 | 어떻게 입력하나 | 쓸모 | 우리 | 쓰일 곳 | 출처 | +|---|---|---|---|---|---|---|---| +| P1 | 측점 · 오프셋 점 `'SO` | Civil 3D | 점 묻는 단계에서 `'SO` → 노선 고름 → 측점 → 오프셋(치거나 찍기) → 점 · 측점 · 오프셋 칸 자물쇠 = 같은 값 되풀이 · 노선 바꿔 이어 감 · Esc 끝 · 측점은 `+` 없이 숫자(포럼) | 상 — 구조물 · 시설 기호를 「No.12+5 좌 3 m」 자리에 바로 | 없음 | B07 평면 | [R1][R2][R3] | +| P2 | 측점 · 오프셋 / Δ측점 · 오프셋 칸 | OpenRoads(Civil AccuDraw) | `O` → 기준 요소 고름 → 커서 따라 측점 · 오프셋 칸이 움직임 → 칸에 치고 Enter = 잠금 · 잠긴 칸에 Enter 또 = 그 값만큼 더함 · End = 풂 · Shift+End = 모두 풂 · Tab 칸 넘김 · ←→ 칸 선택지 · 칸 꼴 = 거리 · 방향 / XY / DX DY / 측점 · 오프셋 / Δ측점 · 오프셋(고쳐 더할 수 있음) | 상 — 한 칸 잠그고 다른 칸만 바꿔 되풀이 · 20 m 간격 되풀이 | 없음 | B07 평면 | [R5][R6][R8] | +| P3 | 선 따라 거리 + 오프셋(Offset Line Snap) | Trimble Business Center | 좌표 칸에서 그림 우클릭 → Offset → 선 · 호 고름 → 측점 → 오프셋 · 측점 0 = 고른 자리에 가까운 끝 · + = 오른쪽 · 기운 선이면 Z 보간 | 상 — 노선 객체 없이 그냥 폴리선에 됨 = 우리 평면(노선 = 폴리선 한 줄)에 바로 맞음 · 축척만 있으면 됨 | 없음 | B07 평면 · 구조물 | [R9] | +| P4 | Chainage Offset 점(+ 끝 너머 Extended) | 12d Model | CAD → Point → Chainage Offset → 선 고름 → 측점 → 오프셋 · 다른 도구 고르기 전까지 이어 찍음 · Extended = 선 끝 너머 연장 | 중 — P3 과 같음 · 끝 너머 연장만 덧 | 없음 | B07 평면 | [R12] | +| P5 | 곡선 좌표 추적(Curvilinear · 체인) | GEOPAK | 좌표 꼴 = Curvilinear + 체인 고름 → 커서 = 측점 · 오프셋으로 읽힘 · 반올림 값 쳐서 자물쇠 · 왼쪽 = − | 중 — P2 와 같은 계열(옛판) | 없음 | B07 평면 | [R13] | +| P6 | 측점 · 오프셋 조회 | 12d(Measure String from point) · Carlson(Station-Offset from Coords) · TBC(Station from Line Snap · Station at Point Snap) | 점 찍음 → 노선 기준 측점 · 수직 오프셋 보고 · TBC 는 측점 칸을 그림 찍기로 채움(종단에서도) | 상 — 손질한 기호 · 글 자리를 측점으로 대조 | 일부 — `ID` = 종이 mm 좌표만 | B07 평면 · 종단 | [R12][R16][R10] | +| P7 | 측점 꼴 「번호 + 나머지」 | Trimble Access · Civil 3D | 표시 꼴 = 1000.0 · 10+00.0 · 1+000.0 · Station index(간격 20 → 42.0 m = 2+02.0) · Civil 3D 입력은 `+` 없이 숫자 | 상 — 우리 도면 글 = `No.n+d`(20 m) · 제목 「2+0.0」 · 정보 「STA.0+050.000」 → 입력도 `No.12+5` · `12+5` · `245` 다 받아야 | 없음 — 입력 없음 · 글만 `B07_DesignDetail_Engine_Cad.py:329-330` | B07 평면 · 종단 · 횡단 | [R17][R3] | +| P8 | 점 · 측점으로 이동 `'ZTP` | Civil 3D | 점 이름 · 번호 · 번호 범위 침 → 그리로 확대 | 중 — 우리 판 = 측점 쳐서 긴 평면 · 종단의 그 자리로 | 없음 — ZOOM = 범위만(335 D11) | B07 평면 · 종단 | [R1] | +| P9 | 간격 + 오프셋 점 배치(Locate Template Points) | Carlson | 중심선 따라 찍은 점 · 점 번호 · 측점 + 오프셋 침 · 측점 간격 + 오프셋 되풀이 · 자리 오프셋 · 표고 오프셋 따로 | 중 — 측점 눈금 · 표지 되풀이 | 일부 — MEASURE(길이분할 · 선 위 간격 점) 있음 · 오프셋 · 실거리 m 없음 | B07 평면 | [R19] | +| P10 | 임시 기준선 두 점 → 측점 · 오프셋 점 여러 개 | TBC(Create Points at Station Offsets) | 시작 · 끝 점으로 임시 기준선 → 점마다 측점 · 오프셋 고침 · 파란 화살표 = 쓰는 X · Y 축 | 중 — 비스듬한 구조물 축 기준 띄우기(사용자 좌표계 계열) | 없음 — UCS 없음 | 구조물 · B07 평면 | [R14] | +| P11 | 측점 추적선(Station Tracker) | Civil 3D | 평면 커서 → 종단 · 횡단 창에 같은 측점 임시선 | 하 — 우리 CAD 는 도면 한 장씩 · 페이지 몫 | 없음 | B07 페이지 | [R20] | + +### 1-2. 종단 + +| # | 이름 | 프로그램 | 어떻게 입력하나 | 쓸모 | 우리 | 쓰일 곳 | 출처 | +|---|---|---|---|---|---|---|---| +| L1 | 측점 · 표고 점 `'PSE` (+ `'STAE` · `'SSE` · `'SPE`) | Civil 3D | 점 묻는 단계에서 `'PSE` → 프로파일 뷰 고름 → 측점 → 표고(치거나 찍기) · 뷰가 가로 · 세로 축척 · 과장을 앎 · `'STAE` = 측점을 평면 노선에서 찍기 · `'SSE` = 표고를 지형면에서 · `'SPE` = 측량점에서 | 상 — 우리 종단 = 가로 1/1000 · 세로 1/200(세로 5 배) → 종이 mm 손 계산 없앰 | 없음 | B07 종단 | [R1][R21][R22] | +| L2 | 경사 + 측점 `'PGS` · 경사 + 길이 `'PGL` | Civil 3D | 앞 점 하나 있어야 · `'PGS` → 뷰 → 경사(치거나 두 점 찍기) → 측점(치거나 찍기) → 다음 경사 · 측점 되풀이 · Esc 끝 | 상 — 종단 경사(%)로 계획선 · 보조선 긋기(5 배 과장 안 따져도) | 없음 — 각 = 종이 각(십진 도) | B07 종단 | [R1][R23] | +| L3 | Profile Toolset 띠 | Civil 3D | 띠 칸 = 측점 · 표고 · 경사 · 길이 · 첫 점 = 측점 + 표고 · 다음 = 측점 + 표고나 경사 / 길이 + 경사 / 표고 + 경사 · 점마다 칸 비움 · 평면 · 종단 두 창 | 중 — L1 · L2 를 한 묶음 칸으로(우리 커서 곁 칸에 맞춤) | 없음 | B07 종단 | [R4] | +| L4 | 종단 Civil AccuDraw | OpenRoads | 종단 도구에 따라 칸 = 측점 · Z · ΔZ · 종단 오프셋 · 경사 · 측점 칸은 단축키 안 받음(포럼) | 중 — L3 과 같은 계열 | 없음 | B07 종단 | [R7] | +| L5 | 고저 편집 표(Elevation Editor) | Civil 3D(피처 라인 · 그레이딩) | 표 줄 = 측점 · 표고 · 앞 경사 · 뒤 경사 · 앞 경사 고침 = 다음 점 표고 바뀜 · 뒤 경사 고침 = 이 점 표고 바뀜 · 여러 줄 골라 경사 한 번 · 처음 · 끝 고정 평탄화 · 경사 꺾임만 보기 | 중 — 종단 계획선 · 횡단 지반선 폴리선을 실값 표로 손질 | 없음 — 특성 칸 길이 = 읽기만 | B07 종단 · 횡단 | [R24] | +| L6 | PVI 표 + 끌기 고정 꼴 | Carlson(Road Profile Editor) · Civil 3D(Profile Grid View · Insert PVIs-Tabular) | 표(측점 · 표고 · 종곡선 길이 · K)와 그림이 맞물림 · PVI 끌기 = Free / Hold Slope In / Hold Slope Out / Hold Station / Hold Elevation · 끄는 동안 종곡선 길이 · K 고정 · 해상도 스냅(측점 · 경사 반올림) · 세로 과장 목록 | 하 — 종단 정본 = B05 · CAD 손질은 설계에 안 돌아감 | 없음(B05 몫) | B05(참고) | [R25][R26] | + +### 1-3. 횡단 + +| # | 이름 | 프로그램 | 어떻게 입력하나 | 쓸모 | 우리 | 쓰일 곳 | 출처 | +|---|---|---|---|---|---|---|---| +| C1 | 오프셋 · 표고 표 입력 | Carlson(Input-Edit Section File) | 측점마다 표 = 오프셋 · 표고 · 설명 · 왼쪽 = − · 앞 점에서 경사나 비율 치면 표고 계산 · 표고 칸 식(`1926.18-1.25`) · Check Offset = 오프셋 치면 표고 · 정렬 · 줄 더함 · 뺌 · F2/F3 앞뒤 측점 | 상 — 횡단 지반선 · 계획선을 m 값으로 · 1:1.5 비탈 끝 표고 자동 | 없음 — 그립 = 종이 mm | B07 횡단 · B06 상세(참고) | [R27] | +| C2 | 끌기 고정 꼴(Hold Slope · Offset · Elevation) | Carlson | 점 끌 때 Free / Hold Slope(비탈 따라) / Hold Offset(표고만) / Hold Elevation(오프셋만) | 중 — 비탈 기울기 그대로 끝만 늘림 · 높이만 맞춤(연장 · 극 추적으로도 됨) | 일부 — 그립 끌기 있음 · 축 고정은 335 D3(Shift 직교) 제안뿐 · 비탈 따라 고정 없음 | B07 횡단 · 구조물 | [R27] | +| C3 | 커서 자리 실값 읽기 | Carlson(오프셋 · 표고 · 경사) · GEOPAK(평면 측점 · 오프셋 · 종단 측점 · 표고) · OpenRoads(`O` 뒤 커서 따라 측점 · 오프셋) | 커서만 움직이면 실값 표시 | 상 — 모든 손질에 바로 도움 | 없음 — 커서 곁 = 종이 mm 절대 좌표만(335 부록2) | B07 횡단 · 종단 · 평면 | [R27][R13][R8] | +| C4 | 비탈 꼴 바꿈(% · 비율 · 각) | Civil 3D · Trimble Access | Civil 3D 비탈 칸 = 숫자 3 → 3:1 · `25%` → 4:1 로 바꿔 보임(제3자) · Trimble 표시 꼴 = 각 · % · Rise:Run · Run:Rise | 중 — 335 D2(`<1:1.5`)와 같음 · 덧 = 한 칸이 % · 1:n · n:1 · 각 다 받기 + 꼴 설정 | 없음(D2 제안) | B07 횡단 · 종단 | [R30][R17] | +| C5 | 실거리 단위 붙여 치기 · 칸 식 · 점 ID 표고 | TBC(COGO 칸) | `3m` = 프로젝트 단위로 바꿈 · 칸 안 `+ - * / ^` · 표고 칸에 `"12"` = 점 12 의 표고 · 좌표 칸엔 따옴표 없이 점 ID | 상 — 우리 도면 = 종이 mm → `3m` 치면 축척대로(횡단 30 mm · 평면 2.5 mm) | 없음 — 단위 붙은 숫자 없음(335 부록2) · 식은 335 1-4 | B07 횡단 · 평면 · 구조물 | [R11] | + +### 1-4. 글씨 · 치수 + +| # | 이름 | 프로그램 | 어떻게 입력하나 | 쓸모 | 우리 | 쓰일 곳 | 출처 | +|---|---|---|---|---|---|---|---| +| T1 | 측점 · 오프셋 글씨 | Civil 3D(Station Offset · Fixed Point 라벨 · AddAlignOffLbl · AddAlignOffXYLbl) | 노선 고름 → 점 찍음 → 노선 이름 · 측점 · 오프셋 · 좌표 글 · 좌우 따라 글 돌림은 스타일 식 | 상 — 「No.5+12.0 좌 3.0」 글을 계산 없이 | 없음 | B07 평면 | [R18] | +| T2 | 횡단 글씨 — 오프셋 · 표고 / 두 점 경사 | Civil 3D(Section View Labels) | 섹션 뷰 고름 → Offset Elevation = 점 찍으면 오프셋 · 표고 글 · Grade = 두 점 → 경사 글(꼴 = 스타일) · 낱개 글은 측점 바뀌어도 안 따라감 | 상 — 「EL.123.45」 · 「1:1.5」 글을 계산 없이 | 없음 | B07 횡단 | [R28] | +| T3 | 종단 글씨 — 측점 · 표고 / 깊이 · 경사 | Civil 3D(Profile View Labels) | 프로파일 뷰 → Station Elevation = 점 → 측점 · 표고 글 · Depth = 두 점 → 깊이 · 경사 글 · 글은 뷰에 붙음(선이 바뀌어도 안 따라감) | 상 — 종단 손질의 표고 · 경사 글 | 없음 | B07 종단 | [R29] | +| T4 | 치수 측정 축척(DIMLFAC) | AutoCAD(치수 스타일 Primary Units) | 측정값 × 계수 = 치수 글(각도 · 공차 빼고) | 상 — 우리 치수 글 = 종이 거리 그대로 → 횡단 1/100 에서 3 m = 「30」 · 평면 1/1200 = 「2.5」 · 종단 세로 1/200 = 「15」 | 없음 — `entities/MeasurementEntity.ts:304,419` 반올림만 · `commands/dim-settings.ts:13-33` 소수 자리 · 주석 축척뿐 | B07 횡단 · 평면 · 종단 · 구조물 | [R31] | +| T5 | 값 글자 눌러 고치기(text manipulators) | OpenRoads · ConceptStation | 고른 노선의 반지름 · 길이 · 경사 글자를 눌러 새 값 + Enter → 도형이 규칙대로 따라 바뀜 | 하 — 우리 도면 = 선 · 글 따로(규칙 객체 없음) · 큼 · 코덱스 Onshape 치수 구동과 같은 계열 | 없음 | B07 평면 · 종단 | [R35] | + +### 1-5. 측량 좌표 · 각 · 값 집기 + +| # | 이름 | 프로그램 | 어떻게 입력하나 | 쓸모 | 우리 | 쓰일 곳 | 출처 | +|---|---|---|---|---|---|---|---| +| S1 | 북 · 동 좌표 `'NE` · 격자 `'GN` · 위경도 `'LL` | Civil 3D | 북(Northing) · 동(Easting) 값으로 점 · 격자는 축척 계수 반영 | 중 — 국내 측량 X = 북 · Y = 동(CAD X · Y 와 거꾸로) → 「북 · 동」 칸 이름이 뒤바뀜 막음 · 기준점 · 구조물 TM 자리 | 없음 — 평면 = 장 최소점 뺀 종이 mm | B07 평면 | [R1] | +| S2 | 방위 · 방위각 · 각 · 편각 + 거리 `'BD` `'ZD` `'AD` `'DD` | Civil 3D | 앞 점에서 방위(N45°E 꼴) · 방위각(북 0 시계) · 기준선에서 각 · 앞 선 편각 + 거리 | 중 — 편각 = 노선 IP 교각 손버릇 · 방위 꼴 자체는 331 · 335 부록1 과 같음 | 일부 — 극좌표 = 동 0° 반시계 십진 도만 | B07 평면 | [R1] | +| S3 | 사이드샷 `'SS` · 점 번호 · 이름 · 점 `'PN` `'PA` `'PO` · 거르개 `.p` `.g` `.n` | Civil 3D | 측량 점 번호 · 이름 쳐서 그 좌표 · 기준점 + 후시 방향 → 각 · 거리 | 하 — 우리 도면엔 측량 점 객체 없음 | 없음 | — | [R1] | +| S4 | 객체 길이 · 반지름 집기 `AmL` `AmR` | Civil 3D | 길이 · 반지름 묻는 단계에서 객체 고르면 그 값 | 중 — 같은 길이 · 반지름 되풀이 | 없음 | 구조물 · B07 평면 | [R1] | +| S5 | 곡선 계산기 `'CCALC` | Civil 3D | 곡선 요소 둘 → 나머지(반지름 · 길이 · 교각 · 접선장 등) | 하 — IP 표 대조 정도 | 없음 | B07 평면(IP 표) | [R1] | + +### 1-6. MicroStation AccuDraw · 그 밖 + +| # | 이름 | 프로그램 | 어떻게 입력하나 | 쓸모 | 우리 | 쓰일 곳 | 출처 | +|---|---|---|---|---|---|---|---| +| M1 | 나침반 · 칸 잠금 | MicroStation AccuDraw | 앞 점에 나침반 · 숫자 치면 커서 쪽 칸으로 · `M` 직교 ↔ 극 · `X` `Y` `D` `A` 칸 잠금 · `P` · `M` 점 키인 창 | 중 — 같음(동적 입력 · 직접 거리 · 각도 잠금 = 331 · 335) | 일부 | — | [R32] | +| M2 | Enter 스마트 잠금 | MicroStation AccuDraw | 축 가까우면 그 축 0 잠금 · 극이면 0 · 90 · −90 · 180 잠금 · 아니면 앞 거리 잠금 | 중 — 축 잠금은 335 D3 와 같음 · 「앞 거리 잠금」 덧 | 없음 | 구조물 · B07 횡단 | [R32] | +| M3 | 원점 옮김 `O` | MicroStation AccuDraw | 명령 중 커서 자리로 나침반 원점 | 하 — 같음(335 1-8 FROM) | 없음 | — | [R32] | +| M4 | 나침반 돌림 `RE` · `RQ` · `RA` | MicroStation AccuDraw | `RE` = 고른 요소 방향 · `RQ` = 잠시 · `RA` = 영구(ACS) · `RX` `RY` `RZ` = 90° · `B` = 기본으로 | 중 — 노선 · 비탈 방향 기준 앞 · 옆 거리 · 사용자 좌표계 계열 · 「요소 하나 찍어 맞춤」이 덧 | 없음 — UCS 없음 | B07 평면 · 구조물 | [R32] | +| M5 | 가 스냅(Tentative) | MicroStation | 가 스냅 단추 → 강조 + 좌표 → 데이터 점 = 확정 · 다시 누르면 다음 후보 · Reset = 놓음 | 중 — 겹친 곳 확인 뒤 확정 · 펜 · 터치 정밀(B5 몫과 겹침) | 일부 — 335 2-4(Tab 후보 돌기) 제안과 같은 계열 | B07 횡단 · 구조물 | [R33] | +| M6 | 색인 잠금 `LI` | OpenRoads(Civil AccuDraw) | 켜면 방향 = 나침반 눈금 · 거리 = 앞 거리 · Civil AccuDraw 에선 Enter 가 스마트 잠금 아님 | 하 — 같음(극 추적) | 일부 | — | [R6] | +| K1 | KTOOL 「도로 종단면도 그리기」 | 국내 유틸(카페 배포) | 엑셀 A = 누가거리 · B = 지반고 · 「종단계획 엑셀로」 → 계획선 폴리선 · 시점 계획고 · 종단 시점 찍음 → 절점 누가거리 · 계획고가 엑셀로 · 종곡선 길이 손 입력 · 축척 · 측점 꼴 · 기준 GL · 자리수 → 「종단 그리기」 → 삽입점 | 중 — 표 ↔ 도면 오가기 = 우리 B05 → B07 이 이미 자동 · 「도면 → 표 역추출」만 덧 | 일부 | B07 종단 | [R34] | +| K2 | 국내 임도 전용 설계 프로그램 | — | 확인 못 함 | — | — | — | — | + +## 2. 쓰일 곳별 묶음(상 · 없음만) + +| 쓰일 곳 | 상 · 없음 항목 | 꼭 필요한 문맥 | +|---|---|---| +| B07 평면 | P1 · P2 · P3 · P6 · P7 · T1 · C5 · T4 | 축척(1/1200 · 고를 수 있음) · 노선 폴리선 표지 · 그 장 시작 측점 | +| B07 종단 | L1 · L2 · T3 · P7 · T4 · C3 | 가로 · 세로 축척 둘 · 기준선 표고 · 그 장 시작 측점 | +| B07 횡단 | C1 · C3 · T2 · C5 · T4 | 축척 · 블록마다 도로 중심 · 기준 표고 | +| 구조물 · B06 상세 · M02 양식 | C5 · T4(축척 있는 구조물 도면만) | 축척 하나 · B06 상세 · M02 = 표 · 양식이라 노선 문맥 없음 → 나머지 하 | + +## 3. 측점 문맥 판정 + +판정 한 줄 — 지금은 「측점 + 오프셋」 입력 불가: CAD 는 종이 mm 도형과 제목 글만 받고 축척 · 원점 · 기준 표고 · 노선 표지가 없음 · 엔진이 이미 장 · 블록마다 변환을 계산하므로 이를 실음 메시지에 실으면 가능(크기 중간 · 어림). + +근거: + +- B07 도면 = 서버가 B05 · B06 산출물(노선)로 만듦 — `B07_DesignDetail/B07_DesignDetail_Engine_Cad.py:1-14`(좌표 규약: 종단 x = 측점 m · 횡단 x = 오프셋 m(+좌/−우) · y = 표고 m). +- 도면 좌표 = 종이 mm · 축척 `config/config_system.py:155-194` — 횡단 1/100(1 m = 10 mm) · 종단 가로 1/1000 · 세로 1/200(1 m = 1 · 5 mm) · 평면 1/1200(1 m = 0.833 mm · 고를 수 있음 500~6000). +- 횡단 블록 = 종이 (오프셋 × 10 + ox, (표고 − dy) × 10 + oy) · 블록마다 ox(도로 중심) · dy(그 단면 높이 가운데) · oy(쌓은 자리) 다름 — `B07_DesignDetail_Engine_Cad.py:488-560`. +- 종단 = x = 측점 × 1.0 · y = (표고 − 기준선) × 5 · 기준선 = 최저 표고 아래 5 m 단위 내림 — `B07_DesignDetail_Engine_Cad_Long.py:53-55,597-610`. +- 평면 = (사업지 좌표 − 장 콘텐츠 최소점) × 0.833 · 노선 = 폴리선 한 줄 · 측점 눈금 + `No.n+d` 글 — `B07_DesignDetail_Engine_Cad_Plan.py:15,45,334-373` · `B07_DesignDetail_Engine_Cad.py:329-330`. +- CAD 가 받는 문맥 = `DesignMeta`(kind · drawingId · title · info = 글 · confirmed · quantityTable · hasPrev/Next) — `openwebcad/src/App.types.ts:42-57` · 실음 메시지에 축척 · 원점 칸 없음 — `openwebcad/src/integration/aislo-drawing-bridge.ts:51-77`. +- `openwebcad/src` grep — station · chainage 0 건 · 측점 = 위 타입 주석 · 수량 패널뿐 · 노선 0 건 · UCS 0 건 · 엔티티 id = uuid5(결정적) → 측점 뜻을 못 읽음. +- 있는 것 — `ID`(점 좌표 · 종이 mm) `commands/commands.utility.ts:159-164` · MEASURE(길이분할) · DIVIDE(등분) `commands/commands.draw.ts:157-170` · DIMORDINATE. + +열리는 차례(어림): + +1. 축척만(장마다 mm/m · 종단은 가로 · 세로 둘) → C5 `3m` · T4 치수 축척 · P3 선 따라 거리(상대) — 작음~중간. +2. + 블록 · 장 원점 · 기준 표고 → L1 · L2 · C1 · C3 · T2 · T3 — 중간. +3. + 노선 표지(평면 노선 폴리선 · 그 장 시작 측점) · 측점 꼴 해석(`No.n+d` · `n+d` · m) → P1 · P2 · P6 · T1 — 중간. +- 내보내기(DXF · DWG)는 종이 mm 그대로 — 문맥은 화면 입력 · 표시에만 씀(정본 안 바뀜). + +정할 것: + +- 오프셋 부호 — 엔진 = +좌/−우(`B05_Profile/B05_Profile_Engine_Sections_Core.py:562` 「positive: left」) · Carlson · GEOPAK = 왼쪽 − · TBC = 오른쪽 + → 입력 칸 부호를 사용자 협의로. +- 종이 좌우 — 엔진 식 그대로면 +좌(왼쪽 오프셋)가 종이 오른쪽에 그려짐(추정 · 화면 실측 필요). +- 측점 꼴 — `No.n+d`(20 m) · `n+d` · `STA.0+050.000` 셋이 섞여 보임 → 입력은 셋 다 받고 표시는 한 꼴로. + +## 4. 확인 못 함 + +- 국내 임도 전용 · 국내 도로 설계 상용 프로그램(국내 쓰는 CARD/1 등)의 입력 방법 — 검색 한계(미국 지역 검색)로 확인 못 함 · 국내 리습 · 국산 CAD 는 안티그래비티 몫. +- Civil 3D 측점 입력 꼴 `1+50` 받는지 — 공식 문서 없음 · 포럼은 `+` 없이 숫자. +- Civil 3D 섹션 뷰 전용 투명 명령(오프셋 · 표고 쳐서 점) — 2024 표에 없음 → 없는 것으로 봄(라벨만 확인). +- Civil 3D `'NE` 의 묻는 차례(북 먼저) — 이름 차례로만 봄 · 화면 확인 못 함. +- OpenRoads 횡단용 Civil AccuDraw 칸(오프셋 · 표고) — 공식 본문 확인 못 함(종단 칸은 포럼). +- 12d 「pick ops」 · 높이 스냅 — 공식 문서 확인 못 함 · 12d 위키 본문 403 → 검색 요약 근거. +- Civil 3D 비탈 칸 꼴(3 = 3:1 · 25% → 4:1) — 제3자 글 근거. +- 쓸모 = 도면 종류별 짐작 · B07 에서 실제 무엇을 자주 고치는지 자료 없음(335 와 같음). + +## 5. 출처 + +- [R1] Civil 3D Transparent Command Reference — https://help.autodesk.com/cloudhelp/2024/ENG/Civil3D-UserGuide/files/GUID-21B960A1-8F33-4BBA-BA05-8A1DB5CBD4C7.htm +- [R2] Civil 3D Station Offset 투명 명령 — https://help.autodesk.com/cloudhelp/2023/ENU/Civil3D-UserGuide/files/GUID-0B7110B8-9941-44FE-A916-B4DA701E2EBA.htm +- [R3] Autodesk 포럼 Point to alignment with station — https://forums.autodesk.com/t5/civil-3d-forum/point-to-alignment-with-station/m-p/12927097 +- [R4] Civil 3D Profile Toolset — https://help.autodesk.com/cloudhelp/2023/ENU/Civil3D-UserGuide/files/GUID-9A3AE605-6EA6-4F1E-B429-198E97CC1CCD.htm +- [R5] OpenRoads Toolbox Civil AccuDraw(칸 꼴) — https://docs.bentley.com/LiveContent/web/OpenRoads%20Designer-v2024.0.2/Help/en/topics/405971/toolbox_civil_accudraw.html +- [R6] Civil AccuDraw Shortcut Key-ins — https://docs.bentley.com/LiveContent/web/OpenRail%20Designer-v2025/Help/en/topics/405971/shortcuts.html +- [R7] Bentley 포럼 Civil AccuDraw 종단 칸 — https://communities.bentley.com/products/road___site_design/f/geopak-inroads-mx-openroads-forum/203565/ir-ss4-10-civil-accudraw-shortcuts-non-functioning-in-profile-views +- [R8] NDDOT OpenRoads Fundamentals for Reviewers — https://www.dot.nd.gov/manuals/design/caddmanual/OpenRoads%20Fundamentals%20for%20Reviewers.pdf · CDOT 교육 — https://www.codot.gov/business/designsupport/cadd/ord/openroads-designer-10-10-for-roadway-designers-self-paced-training/creating-editing-centerline-geometry/creati-1.pdf +- [R9] TBC Offset Line Snap — https://help.fieldsystems.trimble.com/tbc/1255.htm +- [R10] TBC Station from Line Snap — https://help.fieldsystems.trimble.com/tbc/3842.htm · Station at Point Snap — https://help.fieldsystems.trimble.com/tbc/1418.htm +- [R11] TBC COGO Expressions, Units, and Entry Formats — https://help.fieldsystems.trimble.com/tbc/1847.htm +- [R12] 12d 위키 Chainage Offset — https://12dwiki.com.au/tag/chainage-and-offset/ · Measure chainage and offset — https://12dwiki.com.au/measure-chainage-and-offset +- [R13] TDOT GEOPAK Road Manual 2017(Tracking · Graphical COGO · 검색 결과 주소) — https://www.tdot.tn.gov/PublicDocuments/%5CDesignDivision%5Cassistant_engineer_design%5Cdesign%5CTraining%5CGeopak%20Road%20Manual%202017%5CProposed%20Roadway%20Tracking.pdf +- [R14] TBC Create Points at Station Offsets — https://help.fieldsystems.trimble.com/tbc/27540.htm +- [R16] Carlson Station Offset from Coords — https://files.carlsonsw.com/mirror/manuals/Carlson_2022/source/CGSurvey/CGCogo/Station_Offset/Station-Offset_From_Coords/Station-Offset_From_Coords.html +- [R17] Trimble Access Job units(측점 · 경사 꼴) — https://help.fieldsystems.trimble.com/trimble-access/2025.10/en/job-units.htm +- [R18] Civil 3D Alignment Command Reference — https://help.autodesk.com/cloudhelp/2027/ENU/Civil3D-UserGuide/files/GUID-7A01C84A-0476-4F7F-9C71-7D8208020D47.htm · VDCI(제3자) — https://vdci.edu/learn/civil-3d/working-with-alignment-labels-in-civil-3d-station-offset-fixed-point-and-station-offset +- [R19] Carlson Locate Template Points — https://files.carlsonsw.com/mirror/manuals/Carlson_2022/source/Site_Road_Design/Roads/Locate_Template_Points/Locate_Template_Points.htm +- [R20] Civil 3D Station Tracker — https://help.autodesk.com/cloudhelp/2023/ENU/Civil3D-UserGuide/files/GUID-DB691511-62AD-4FD0-BE0A-37EEE1C023BA.htm +- [R21] Civil 3D `'STAE` — https://help.autodesk.com/cloudhelp/2023/ENU/Civil3D-UserGuide/files/GUID-86AD3A0B-F3D2-47CA-8B67-A4EC4C320C5E.htm +- [R22] NRCS HowTo Profile Creation C3D(`'PSE` 흐름) — https://www.nrcs.usda.gov/sites/default/files/2022-10/HowTo-%20Profile%20Creation%20C3D.pdf +- [R23] Civil 3D `'PGS` — https://help.autodesk.com/cloudhelp/2023/ENU/Civil3D-UserGuide/files/GUID-DCEE5557-CEFF-432F-9A10-78859AC9AE6D.htm +- [R24] Civil 3D Grading Elevation Editor — https://help.autodesk.com/cloudhelp/2023/ENU/Civil3D-UserGuide/files/GUID-CD59A8C3-6A59-4655-9CDB-75ECA6C0D31B.htm +- [R25] Carlson Road Profile Editor — https://files.carlsonsw.com/mirror/manuals/Carlson_2022/source/Site_Road_Design/Profiles/Input-Edit_Road_Profile/InputEditRoadProfile.html +- [R26] Civil 3D To Work With Profile PVIs — https://help.autodesk.com/cloudhelp/2023/ENU/Civil3D-UserGuide/files/GUID-E0F43873-38D5-4C1B-85E2-E32D09B9F879.htm +- [R27] Carlson Input-Edit Section File — https://files.carlsonsw.com/mirror/manuals/Carlson_2022/source/Site_Road_Design/Sections/Input-Edit_Section_File/InputEdit_Section_File.htm +- [R28] Civil 3D To Add Section View Labels — https://help.autodesk.com/cloudhelp/2023/ENU/Civil3D-UserGuide/files/GUID-FB83A5B0-7D18-4250-B11D-8540DB4F4161.htm +- [R29] Civil 3D To Work With Adding Profile View Labels — https://help.autodesk.com/cloudhelp/2023/ENU/Civil3D-UserGuide/files/GUID-E2C1DAA4-B3DC-4996-84D4-688D3E47F3F2.htm +- [R30] IMAGINiT(제3자) Slope vs Grade · 서브어셈블리 값 꼴 — https://resources.imaginit.com/civil-3d/civil-3d-slope-vs-grade-format · https://resources.imaginit.com/civil-3d/civil3d-tips-and-tricks-switching-format-for-subassembly-values +- [R31] AutoCAD Primary Units Tab(DIMLFAC) — https://help.autodesk.com/cloudhelp/2023/ENG/AutoCAD-Core/files/GUID-2D1177AB-3130-4522-A631-AA28493529DC.htm +- [R32] MicroStation Complete List of AccuDraw Shortcut Key-ins — https://docs.bentley.com/LiveContent/web/MicroStation%20Help-v26/en/GUID-F954C772-A9B3-79A5-8441-AEC7EA77DCB0.html · AccuDraw's Shortcut Key-ins — https://docs.bentley.com/LiveContent/web/MicroStation%20Help-v27/en/GUID-7855BF35-47CA-7489-8D1E-AA12CFF8DC2D.html +- [R33] MicroStation 가 스냅 — https://prd-aws-docs.bentley.com/LiveContent/web/MicroStation%20Help-v28/en/GUID-67C2437B-8695-A8DC-5277-981A506A28B2.html · Bentley 포럼 — https://communities.bentley.com/products/microstation/f/microstation-forum/143160/how-do-i-prevent-sticking-of-tentative-snap +- [R34] KTOOL 도로 종단면도 그리기(다음 카페) — https://m.cafe.daum.net/gildosa/EBSk/297 +- [R35] OpenRoads ConceptStation Editing Roads — https://docs.bentley.com/LiveContent/web/OpenRoads%20ConceptStation%20Help-v16/en/GUID-D1A928C9-FC4F-452E-A9AF-9C651E5E7FD5.html · CDOT Horizontal Geometry — https://www.codot.gov/business/designsupport/cadd/ord/edit-horiz-geometry + + +--- + +## 원문 6 — `B5_웹터치AI근거.md` · B5 — 웹 · 터치 · 펜 · AI · 효율 근거 + +# B5 웹 · 터치 · 펜 · AI 입력 + 효율 근거 (요구 226-3 · 10-15) + +- 범위 = 웹 2D 그리기 도구 · 터치 · 펜 · 태블릿 CAD · AI · 자연어 입력 · 효율 근거(논문). 코드 고침 없음 · 웹 조사 + 우리 코드 읽기(실측 없음). +- 우리 상태 기준 = `docs/raw/조사/2026-10-15_335_부록2_우리CAD현황.md` · 「(추정)」 = 코드 읽기 판단 · 화면 실측 필요. +- 겹쳐 뺀 것 — 335(오토캐드 71) · 부록1 · 코덱스 1 · 2차에 이미 있는 것: 올가미 · 식 입력 · 극 추적 각 고르기 · 우클릭 메뉴(AutoCAD Web 의 우클릭 스냅 메뉴 · 머리 설정 극 추적 각도 같은 것) · Ctrl 끌기 복제 · 꼴 맞추기(Excalidraw Ctrl+Alt+C/V = MATCHPROP) · 도구 잠금 Q(= 명령 되풀이) · Quad · 표식 메뉴 · 마우스 몸짓 · Snap Loupe · ARES A3 설명. Space+끌기 팬(Figma · Excalidraw)은 우리 Space = Enter 라 안 맞아 뺌. +- 출처 = 표 안 [W..] · [E..] → 맨 아래 목록. + +## 0. 결론 + +1. 「누구든 · 어느 기기든 같은 기능」에서 가장 큰 빈칸 = 자판 없는 기기에서 숫자 · 명령을 넣는 길. 손가락 단추 6(Esc · Enter · 지움 · ↶ · ↷ · 스냅)뿐이라 태블릿 · 휴대폰은 거리 · 좌표 · 반지름 · 글 높이를 못 넣음(커서 곁 칸 = 캔버스 그림이라 못 누름) · B06 상세 · M02 카드는 리본 · 명령행도 없어 손가락 기기에선 명령까지 못 부름(추정 · 코드 읽기). +2. 그 빈칸을 메우는 셋 — ① 화면 숫자판(값 눌러 고치기 · AutoCAD 모바일 · DWG FastView) ② 명령 찾기 창(Figma Ctrl+K · FigJam 최근 셋 · Excalidraw Ctrl+/ — 단추로도 열림) ③ 고른 것 위 동작 막대(Miro). 셋 다 마우스 · 손가락 · 펜에서 같은 판이라 기기별 개발이 아님. +3. 웹 도구에서 가져올 작은 손버릇 — 고른 것 맞춤 보기(Shift+2) · 끌다가 가장자리 자동 이동(tldraw) · 여러 개 고른 값 「섞임」 + 식 한꺼번에(Figma `Mixed+100`) · 같은 거리 되풀이 복제(Alt 끌기 → Ctrl+D) · 누르는 동안 스냅 끔(draw.io Alt). +4. 펜 — 펜을 한 번 쓰면 손가락은 화면 이동만(펜 모드 · tldraw · Excalidraw · Morpholio) · iPad 는 펜이 닿는 동안 손가락 이벤트를 아예 안 보냄 → 「펜 = 그리기 · 손가락 = 다루기」가 근거 있는 기본값. 옆 단추 = 우클릭(Enter) · 지우개 끝 = 지움은 웹 표준만으로 됨. +5. 손가락 정밀 — 우리 「길게 눌러 손가락 위 70px 십자선」 = 오프셋 커서 꼴. 논문상 작은 표적 오류를 81% → 4~14% 로 줄이는 갈래라 방향은 맞음. 다음 걸음 = 가린 곳을 보여 주는 확대 창(callout) · 아래 70px 띠는 못 닿음(추정). +6. AI — 상용 CAD AI(Autodesk Assistant · Markup Assist · Smart Blocks 감지 · ARES A3 · Onshape AI Advisor)는 모두 클라우드 → 발주처 자료 유출 금지 조건에서 그대로 못 씀. 사내에서 되는 길 = ⓐ 규칙 기반 한글 명령 찾기(모델 없음) ⓑ 내 명령 순서를 세어 다음 명령 후보(NX · Solid Edge 꼴 · 모델 없음) ⓒ 사내 서버 작은 모델(0.5~8B · Qwen3 Apache 2.0 · HyperCLOVA X SEED 상업 무료) + 명령 목록 도구 부르기(시험 필요). 브라우저 안 AI(크롬 내장)는 안드로이드 · iOS 가 빠져 원칙 어긋남 · 음성은 크롬 기본이 서버 인식이라 불가. +7. 효율 근거 — 명령은 소수만 씀(9할이 90 개 미만 · 가장 많은 무리 31~40 개 · 최다 ERASE · ERASE→MOVE→COPY) · 숙련자도 빠른 길로 잘 안 넘어감(CAD 효율 전략이면 약 40% 단축 · 단축키 드묾) · 생각 한 번(1.35초) = 키 예닐곱 번 · 끌기 지연 25ms 넘으면 성능 떨어짐. +8. 근거로 본 차례 — ⓪ 335 흠 막기 → ① 기기 무관 셋(숫자판 · 명령 찾기 · 동작 막대) → ② 335 고르기 · 극 추적 묶음 → ③ 펜 모드 · 확대 창 · 두 손가락 되돌리기 → ④ 고른 것 맞춤 · 가장자리 이동 · 섞인 값 식 · 같은 거리 복제 → ⑤ 한글 명령 찾기 · 다음 명령 후보 → ⑥ 사내 작은 모델(시험). + +## 1. 웹 2D 그리기 도구 입력 + +| # | 이름 | 어디 | 어떻게 입력하나 | 임도 쓸모 | 우리 | 출처 | +|---|---|---|---|---|---|---| +| 1-1 | 명령 찾기 창(팔레트) | Figma Actions(Ctrl+K · 툴바 단추) · FigJam 빠른 실행(Ctrl+/ · 열면 최근 셋 · ↑↓ + Enter 되풀이 · 「Create new …」) · Excalidraw(Ctrl+/) | 단추나 키로 열고 낱말 침 → 명령 목록 → 누름 | 상 — 명령행 없는 B06 상세 · M02 카드 · 태블릿에서도 153 명령 모두 닿음 · 최근 셋 = 손가락 되풀이 | 일부 — B07 명령행 후보 8(기본 숨김) · 커서 곁 후보 6 · 앞글자만 · B06 · M02 없음 | [W1][W2][W3] | +| 1-2 | 고른 것 위 동작 막대 | Miro(고르면 물건 위 가로 막대 · Shift 누르면 숨김 · … 더 보기) · AutoCAD 모바일(고른 뒤에만 편집 도구 · 부록1) | 고르면 곁에 옮김 · 복사 · 지움 · 특성 단추 | 상 — 리본 없는 곳 · 손가락에서 고치기 명령 바로 · 손가락 그립 대신 「옮김」 | 없음 — 리본 · 리본 특성만 | [W4] | +| 1-3 | 여러 개 고른 값 「섞임」 + 식 | Figma(위치 · 크기 · 회전 칸 · `+10` · `(x/2)+6` · 섞인 칸에 `Mixed+100` = 고른 것마다 제 값에 더함) | 섞인 칸에 식 → 고른 것마다 적용 | 상 — 측점 · 표기 글 높이 일괄 · 여러 글 자리 일괄 | 없음 — 특성 칸 = 색 · 선 · 층만 · 여러 개면 첫 객체 값 · 335 6-7(빠른 속성 숫자 칸)에 「섞임 + 상대식」만 덧 | [W5] | +| 1-4 | 같은 거리 되풀이 복제 | Figma(Alt 끌어 복제 → Ctrl+D 마다 같은 거리로 하나 더) · Excalidraw(Ctrl+D · Alt 끌기) · draw.io(Ctrl 끌기 · Ctrl+D) | 한 번 옮겨 복제한 거리 기억 → 키 한 번에 하나 | 중 — 배수관 · 표지 · 표기 같은 간격 · ARRAY 보다 가벼움 | 일부 — COPY 여러 번 · ARRAY | [W6][W7][W3][W8] | +| 1-5 | 미세 이동 두 걸음 + 걸음 값 설정 | Figma(화살표 1 · Shift 10 · 환경설정에서 바꿈) · draw.io(Shift+화살표 = 격자 한 칸) | 고른 것 있으면 화살표 = 옮김 | 중 — 글 · 기호 자리 | 없음 — 화살표 = 화면 이동 · 335 6-2 에 「큰 걸음 · 값 설정」만 덧 | [W5][W8] | +| 1-6 | 누르는 동안 스냅 끔 | draw.io(Alt 누른 채 옮기기 · 크기 = 격자 · 가이드 무시) | 키를 누르는 동안만 | 중 — 촘촘한 횡단 상세에서 엉뚱한 스냅 피함 | 일부 — F3 켬끔 · 한 번 NON · 손가락 ⌖ | [W8] | +| 1-7 | 고른 것 맞춤 보기 | Excalidraw(Shift+1 전체 · Shift+2 고른 것) | 키 하나 · 손가락은 단추 | 중 — 긴 노선 평면에서 고른 구조물로 바로 | 일부 — 전체(Z · ⛶)만 | [W3] | +| 1-8 | 끌다가 가장자리 자동 이동 | tldraw(가장자리 8px 안에 0.2초 머물면 화면이 따라 감 · 0.2초에 걸쳐 빨라짐 · 손가락은 띠 12px 더 넓음 · 옮기기 · 상자 고르기 · 크기 바꾸기에만) | 끌기 중 화면 끝으로 | 중 — 작은 화면 · 긴 노선 · 그립 끌기 · 상자 고르기 | 없음 | [W9] | +| 1-9 | 거리 빨간 선(명령 없이) | Figma(하나 고르고 Alt 누른 채 다른 물건 위 = 가로 · 세로 거리) | 키 누르고 스침 | 중 — 표기와 선 띄움 확인 · 손가락은 스침이 없어 기기 무관 아님 | 없음 — DIST 명령 | [W10] | +| 1-10 | 값 칸 끌어 고치기(스크럽) | Figma(칸 이름 끌기 · 속도 2 · 1 · ½ · ¼) | 칸 이름 좌우로 끔 | 하 — CAD 값은 정확한 숫자가 기본 | 없음 | [W5] | +| 1-11 | 손가락이면 고름 크기 키움 | tldraw(`isCoarsePointer` — 예 48px 대 마우스 32px) | 기기 자동 판별 | 중 | 있음 — 접촉 크기 · 최소 24px · 단추 44px · `any-pointer: coarse` | [W11] | +| 1-12 | 검토 표시층(마크업) | Autodesk Viewer · Docs(화살표 · 말풍선 · 구름 · 손그림 · 글 · 길이 · 각 · 넓이 · 축척 맞춤) | 도면은 그대로 · 위에 표시만 | 중 — 확정 도면에 검토 의견(정본 안 건드림) · 저장 자리 따로 필요 | 없음 — 확정 = 보기 전용 · REVCLOUD 는 편집 명령 | [W12] | +| 1-13 | 웹 앱 · 모바일 앱 나눔(참고) | AutoCAD Web 강의 자료(AU · 강의자 권함) — 큰 iPad = 웹 앱 · 휴대폰 = 모바일 앱 | — | 참고 — 우리는 한 벌(사용자 확정) → 휴대폰 너비 칸 배치를 따로 봐야 함 | — | [W13] | + +## 2. 터치 · 펜 · 태블릿 입력(기기 구분 없이 같은 기능 중심) + +| # | 이름 | 어디 | 어떻게 입력하나 | 임도 쓸모 | 우리 | 출처 | +|---|---|---|---|---|---|---| +| 2-1 | 화면 숫자판 · 값 눌러 고치기 | AutoCAD 모바일(두 점 뒤 길이 · 각 숫자를 누르면 아래 숫자판 · 펜으론 숫자판이 안 떴다는 사용자 글) · DWG FastView(좌표 값 바꿔 점 옮김 · 절대 · 상대 · 극) · 웹 표준 `inputmode="decimal"`(진짜 입력 칸이면 기기 숫자 자판) | 값을 누름 → 숫자판 → Enter | 상 — 자판 없는 기기에서 거리 · 좌표 · 반지름 · 글 높이를 넣는 길 · 원칙의 핵심 빈칸 | 없음 — 손가락 단추 6 뿐(`input-touch.ts:126-133`) · B07 은 아래 명령행 칸(기본 숨김)을 켜면 기기 자판(추정) · B06 상세 · M02 카드 없음 | [W14][W15][W16][W17] | +| 2-2 | 펜 모드(펜이면 손가락은 이동만) | tldraw(`isPenMode` — Apple Pencil 같은 펜을 쓰면 켜짐 · 켜진 동안 손가락 무시) · Excalidraw(펜 첫 감지 때 켜짐 · 손가락 대부분 막음 · 두 손가락 팬만 · 핀치 확대 끔 · 펜 이벤트를 화면 주사율로 줄임) · Morpholio(Pencil Only = 손바닥 거부 · 한 손가락 팬) · Concepts(손가락 동작 따로 정함) | 펜 첫 사용 때 자동 · 끄기 단추 | 상 — 태블릿 + 펜에서 손바닥 톡이 점 · 고름으로 들어가는 것 막음 | 없음 — 펜 = 마우스 길 · 손가락 톡 = 늘 누름 | [W11][W18][W19][W20][W21] | +| 2-3 | 가린 곳 확대 창(callout) | Shift 기법(논문 · 손가락 22mm 위 원 26mm · 작은 표적만 바로 · 큰 표적은 약 1.2초 뒤) · DWG FastView 돋보기 · CAD Pockets 돋보기 · AutoCAD 모바일 돋보기(부록1) | 누르면 손가락 위에 확대 창 → 끌어 맞추고 뗌 | 중~상 — 횡단 상세 끝점 · 교차가 촘촘 | 일부 — 길게(0.45초) = 위 70px 십자선(오프셋 커서) · 확대 창 없음 · 아래 70px 띠 못 닿음(추정) · 부모 돋보기(Z)는 CAD 를 비킴 | [W22][W15][W23] | +| 2-4 | 두 손가락 톡 = 되돌리기 · 세 손가락 = 다시 | Procreate(두 손가락 누른 채 = 연속 되돌리기) · Concepts(기본값 · 동작 바꿀 수 있음) · Morpholio | 화면 아무 데나 톡 | 중 — 단추까지 손 안 옮김 · 핀치와 가르기 = 움직임 없음 + 짧게 | 일부 — 단추 ↶ ↷ 만 · 몸짓 없음 | [W24][W20][W21] | +| 2-5 | 몸짓마다 단추 대안 | Procreate Single Touch Gestures Companion(여러 손가락 몸짓을 한 손가락으로) | 몸짓 = 덤 · 같은 일을 단추로 | 상(원칙) — 기기 무관 · 한 손 · 장갑 낀 현장 | 일부 — 손가락 단추 6 | [W24] | +| 2-6 | 길게 누름 메뉴 | iOS Safari 는 길게 눌러도 `contextmenu` 를 안 보냄 → 자체 타이머 필요 | — | 중 — 우리 길게 누름 = 정밀 고르기라 겹침 → 메뉴는 동작 막대(1-2)로 | 없음 | [W25] | +| 2-7 | 펜 옆 단추 · 지우개 끝 | 웹 표준 Pointer Events(옆 단추 = button 2 · 지우개 = button 5 · buttons 32) | 옆 단추 누른 채 톡 · 펜 뒤집어 문지름 | 중 — 펜만 들고 확정 · 지움 | 일부(추정) — 펜이 마우스 길이라 옆 단추 = 우클릭 = Enter 일 것 · 지우개 처리 없음 | [W26] | +| 2-8 | 펜 띄워 미리보기(hover) | Safari 가 Apple Pencil 띄움을 hover 이벤트로 보냄(WebKit 답변) · 윈도우 펜 | 화면 닿기 전 스냅 표시 | 중 | 있음(추정) — 펜 = 마우스 길 → 스냅 표시 | [W27] | +| 2-9 | 그리다 멈추면 반듯한 도형 | Procreate QuickShape(끝에서 머물면 선 · 도형 · 다른 손가락 톡 = 반듯이) · Miro 스마트 그리기 | 손그림 끝에서 머묾 | 하 — CAD 는 명령으로 정확히 · Miro 오변환 불만 | 없음 | [W24][W28] | +| 2-10 | 문질러 지우기 | Miro(스마트 도형 절반 넘게 문지르면 지움) | 문지름 | 하~중 — 오지움 위험 | 없음 — 🗑 단추 = Delete | [W28] | + +- 펜 + 손가락 동시 몸짓은 iPad 에서 못 씀(펜이 닿는 동안 손가락 이벤트가 안 옴 [W18]) → 「손가락 = 이동 · 펜 = 그리기」 나눔만 현실적 · 근거 = Pen + Touch 논문 「펜은 쓰고 손가락은 다룸」[W29]. +- iOS 는 사용자 누름 안에서 준 초점에만 자판을 띄움 [W17] → 숫자판은 앱 안 단추판이 기기 차이가 적음(기기 자판은 기기마다 꼴 · 높이가 다름). + +## 3. AI · 자연어 입력 + +| # | 이름 | 어디 | 무엇을 넣으면 무엇이 되나 | 임도 쓸모 | 우리 | 사내 · 유출 금지 판정 | 출처 | +|---|---|---|---|---|---|---|---| +| 3-1 | Autodesk Assistant | AutoCAD 2026(물음 답) → 2027(표준 파일 대조 · 자연어로 고르기 · 도면 조회 = 층 목록 · 블록 쓰임 · 프롬프트 모음 · 대화 기록 · MCP) → 2027.1(고른 것 알아봄 · Count · Query 넓힘 · 맞춤 팁) | 채팅 글(음성 = 업체 글) → 답 · 고름 · 보고 | 중 — 「층 위반 찾기」 · 「덮어쓴 치수 찾기」 같은 검사 | 없음 | ✕ 클라우드 · Autodesk 계정 개인정보 설정 · 일부 기술 미리보기 → 그대로 불가. 「자연어로 고르기 · 조회」는 규칙 거르개(층 · 종류 · 글 내용 · 덮어쓴 치수)로 대신 가능 | [W30][W31][W32] | +| 3-2 | Markup Import · Assist | AutoCAD 2023~2026 | PDF · 그림의 붉은 표시 → 글 · 지시선 · 구름 | 하 | 없음 | ✕ 클라우드 처리(도움말 명시) · LT · 웹 · 모바일 없음 | [W33] | +| 3-3 | Smart Blocks 감지 · 바꾸기 | AutoCAD 2025~2026.1.1 | 반복 모양 → 블록 후보 | 하 | 없음 | ✕ 도면 기하를 Autodesk 기계학습 서비스로 보냄 | [W34] | +| 3-4 | BricsCAD Assist | V26.2 | 채팅 → 도움말 · 방법 안내(명령 실행 안 함) | 하 | 없음 | △ 클라우드 여부 확인 못 함 · 같은 일 = 사내 도움말 글 찾기(모델 없이) | [W35] | +| 3-5 | ARES A3 | ARES 2024~2027 | 자연어 → 고르기 · 만들기 · 고치기(회전 · 대칭 · 축척 · 해치 · 신축 · 지움 · 블록) · Kudo 음성 | 중 | 없음 | ✕ OpenAI 기술 · 클라우드 · 하루 대화 수 제한(코덱스 2차와 겹침 · 판정만) | [W36] | +| 3-6 | Onshape AI Advisor | 2025 | 물음 → 자체 도움말 · 강좌 · 블로그 근거 답 + 번호 출처 | 하 | 없음 | ✕ 클라우드 · 기업판 기본 꺼짐 · 정부판 없음 · 「근거 단 도움말」 생각만 사내 글 찾기로 | [W37] | +| 3-7 | 다음 명령 예측 | Siemens NX(2019 · 하는 일 보고 다음 명령을 작은 판에) · Solid Edge 2021(커서 곁) | 입력 없음 — 쓰는 순서 학습 | 중 — 임도 손질은 순서가 잦음(간격띄우기 → 자르기 등 · 짐작) | 일부 — 빈 Enter 마지막 명령 · 자주 쓴 순 없음(335 D9) | ○ 내 명령 기록에서 「앞 → 다음」 짝 세기만으로 가능 · 모델 없음 · 계정 pref | [W38][W39] | +| 3-8 | 크롬 내장 AI | Gemini Nano · Prompt API | 브라우저 안 작은 모델 | — | 없음 | ✕ 윈도우 · 맥 · 리눅스 · 크롬북 Plus 만(안드로이드 · iOS 안 됨) · 디스크 22GB · GPU 4GB 넘게 또는 RAM 16GB → 기기 무관 원칙 어긋남 | [W40] | +| 3-9 | 음성 명령 | 브라우저 Web Speech API | 말 → 글 → 명령 | 하 | 없음 | ✕ 크롬 기본 = 서버 인식(유출) · 기기 안 처리(`processLocally`)는 실험 · 엣지 Canary · Dev 만 한국어 로컬 | [W41][W42] | +| 3-10 | 사내 서버 작은 모델 + 도구 부르기 | Ollama 도구 부르기(Qwen3 · Llama 3.1 등) · Qwen3 0.6~8B(Apache 2.0 · 119 언어에 한국어) · HyperCLOVA X SEED 0.5 · 1.5 · 3B(상업 무료 · 한국어 강조) · FunctionGemma 270M(도구 부르기 전용 · 미세조정 전제) | 「고른 글 높이 2.5 로」 → 모델이 명령 이름 + 값(JSON) → 화면이 미리보기 → 사람이 [확인] | 중 — 명령 이름 모르는 초보 · 일괄 고치기 | 없음 | ○ 사내 서버 안에서 끝남 → 유출 없음 · 단 한국어 정확도 · 서버 GPU · 라이선스 원문 · 잘못 고침 막는 확인 단계 시험 필요 | [W43][W44][W45][W46] | +| 3-11 | 규칙 기반 한글 명령 찾기 | 우리 덧 — AutoCAD 동의어 · 자동 고침(부록1 3장)과 같은 계열 | 「간격띄우기」 · 초성 「ㄱㄱㄸ」 · 한글 자판 그대로 친 「ㅐㄹㄹㄴㄷㅅ」 → OFFSET | 상 — 한글 화면 · 초보 · 명령 찾기 창(1-1)과 한 벌 | 없음 — 내 별칭 영문 · 숫자만 · 335 D13(명령행 한글 자판) | ○ 모델 · 네트워크 없음 · 표 한 벌 | (판단) | + +## 4. 효율 근거 → 우리 차례에 주는 뜻 + +1. 명령은 소수만 씀 — AutoCAD CIP 1.6만 명 · 6개월 · 4천만 건: 9할이 90 개 미만 · 4천 명 표본에서 가장 많은 무리 31~40 개 · 명령 사이 95% 가 1분 안 [E1] · 가장 많이 쓴 명령 ERASE · 가장 많은 순서 ERASE→MOVE→COPY(Autodesk Research CIP · 2008 블로그 인용) [E2] · CIP 누적 6천만 건 [E3]. + → 우리 153 명령 중 지움 · 옮김 · 복사 · 간격띄우기 · 자르기 · 글 고치기 같은 30~40 개를 마우스 · 손가락 · 펜 어디서나 두 번 누름 안에 닿게(동작 막대 1-2 · 명령 찾기 1-1)가 먼저 · 나머지는 찾기 창이면 충분 · 「최근 셋」(FigJam)은 ERASE→MOVE→COPY 같은 짧은 되풀이에 맞음. +2. 숙련자도 빠른 길로 잘 안 넘어감 — CAD 숙련자가 묶어 고르기 · 되풀이 대신 손 제도 같은 순서를 씀 · 효율 전략이면 한 사용자 모형에서 실행 시간 약 40% 단축(둘째 도형 42%) [E4] · Word 숙련자 251 명 대부분 단축키 드묾 · 단축키가 가장 빠름 [E5] · 키를 누르는 동안 단축키를 단추 위에 겹쳐 보이면 더 쓰고 더 빨라짐(ExposeHK) [E6] · 메뉴에 소리 알림 · 메뉴 막기로 단축키 학습 빨라짐(2차 요약) [E19] · 표식 메뉴는 메뉴보다 약 3.5 배(현장 2 명) [E7] · 빠른 길은 있어도 안 쓰이는 게 흔함(개관) [E8]. + → 기능을 더하기보다 「있는 빠른 길을 보이게」가 싸고 큼 — 명령 찾기 창에 별칭 같이 보임 · 리본 풍선은 지금 기본 별칭 첫째만(`ribbon/Ribbon.tsx:34`) → 내 별칭도 · 335 고르기 묶음(상자로 더하기 · 앞 선택 P = 묶어 고르기 전략)이 앞 차례인 근거. +3. 키 수준 모형(KLM) 표준값 — 키 0.2초(숙련 0.08 ~ 낯선 자판 1.2) · 가리키기 1.1초 · 손 옮김 0.4초 · 생각 1.35초 [E9]. + → 「뭘 누르지?」 생각 한 번 = 키 예닐곱 번 → 단계마다 쓸 값 · 선택지를 커서 곁에 보이는 안내(335 9-2 · D8)가 키 줄이기보다 큼 · 손가락 기기는 숫자 하나마다 단추판을 가리켜야 → 숫자판은 숫자 · 소수점 · 지움 · Enter · `@` · `<` 를 한 판에(판 오가기 = 가리키기 추가). +4. 손가락 정밀 — 맨손가락 작은 표적(2.6mm) 오류 최대 81% · 오프셋 · Shift 4~14% · 오프셋 커서는 손가락으로 될 큰 표적에서 1.57 배 느림 · 필요할 때만 확대 창(Shift)이 오프셋보다 28~32% 빠름 [E10] · 손가락 표적 9.2~9.6mm [E11] · 웹 접근성 최소 24×24 CSS px [E12] · 받침(안정) 있으면 손가락도 4px 표적까지 마우스와 차이 없음 [E13]. + → 우리 「길게 눌러 오프셋」은 맞는 갈래 · 큰 표적 톡 = 바로(지금대로) · 다음 걸음 = 확대 창(2-3) · 손가락 단추 44px · 고름 최소 24px 은 기준 안. +5. 지연 — 끌기 지연 25ms 넘으면 성능 떨어짐 · 손가락 화면에서 알아채는 차이 끌기 11ms · 톡 69ms [E14] · 75ms 부터 움직임 시간 16%↑ · 225ms 64%↑ · 오류 214%↑ [E15] · 화면 이동 · 확대 지연은 앱을 못 쓰게 하나 새 결과 1초는 견딤 · 500ms 더하면 탐색 줄어듦 [E16] · 웹 INP 좋음 200ms 이하(RAIL 100ms) [E17] · 「두 번 톡」을 가르려 기다리면 한 번 톡이 150~500ms 늦어짐(iOS 250 · 윈도우 500 · iOS WebKit 350ms) [E14] · 펜 잉크 지연은 50ms 안팎에서 알아챔(비잉크 2~7ms) [E18]. + → 미리보기 · 스냅 · 팬 · 그립 끌기는 브라우저 안에서 한 화면 틀(16ms) 안(지금 원칙 그대로) · 서버 계산은 [저장] · [확정] 때만 · 손가락 「두 번 톡」 몸짓은 넣지 않음(점 찍기 톡이 늦어짐) · 두 손가락 톡(2-4)은 두 번째 손가락 판별만이라 기다림 짧음 · 펜 그리기 매끄럽게(`getCoalescedEvents` · `getPredictedEvents` · 크롬 `desynchronized`) [E20]는 점 찍는 CAD 엔 낮은 차례. +6. 펜 + 손가락 나눔 — 펜은 쓰고 손가락은 다룸 [W29] · iPad 는 펜이 닿으면 손가락 이벤트를 안 보냄 [W18] · Excalidraw 는 손바닥 오작동 때문에 펜 모드에서 핀치를 껐음 [W19]. + → 펜 모드(2-2)는 근거 있는 기본값 · 켜는 조건 = 펜 첫 사용(자동) · 끄는 단추는 손가락 단추줄에. +7. 웹 CAD 전용 지연 · KLM 측정 · 터치 KLM(TLM) 값 = 못 찾음 → 위 일반 HCI 근거로 대신. + +근거로 본 보강 차례(335 차례에 끼움): + +- ⓪ 335 흠 12(Ctrl+C 글자 쌓임 · 확정 도면 새는 고침 · 그립 밀림) — 335 그대로. +- ① 기기 무관 셋 — 화면 숫자판 · 명령 찾기 창 · 동작 막대(B07 · B06 · M02 세 곳 · 마우스에도 같은 판) — 사용자 확정 원칙 · 위 1 · 2 · 3. +- ② 335 고르기 · 극 추적 · Shift 직교 — 위 2(묶어 고르기 전략 = 약 40%). +- ③ 펜 모드 · 확대 창 · 두 손가락 되돌리기 · 몸짓마다 단추 — 위 4 · 6. +- ④ 고른 것 맞춤 · 가장자리 이동 · 섞인 값 식 · 같은 거리 복제 · Alt 스냅 끔 · 큰 걸음 미세 이동 — 위 1(고치기 명령 비중). +- ⑤ 한글 명령 찾기 · 다음 명령 후보 · 리본 풍선에 내 별칭 — 위 2 · 3. +- ⑥ 사내 작은 모델 + 도구 부르기 — 시험만 · 3-10. + +## 5. 확인 못 함 + +- AutoCAD Web 터치 도움말 · Autodesk 블로그 · Autodesk Assistant 소개 쪽 · 터치 몸짓 글 = 403 · 503 → 웹 앱 터치는 2019 강의 자료 · 검색 요약만 · Assistant 의 자료 처리 · 오프라인 여부 원문 못 봄(계정 개인정보 설정 언급만). +- Figma Ctrl+D 되풀이 = 공식 글은 「Alt 끌어 복제 뒤 Ctrl+D」 꼴만 확인 · 회전 섞이면 어긋난다는 건 사용자 글 · Figma 거리 빨간 선 · 액션 메뉴의 최근 항목은 글에 없음(FigJam 만 최근 셋). +- Excalidraw 지우개 끌기 동작 · 화살표 미세 이동 값 = 공식 근거 못 찾음 · Miro 스마트 그리기 쪽은 403 → 검색 요약. +- Morpholio 기능 = 사용자 올린 안내서(2차) · DWG FastView · CAD Pockets = 앱 장터 · 목록 글. +- BricsCAD Assist · Onshape AI Advisor 클라우드 여부 · 자료 처리 = 문서에 없음. +- 사내 모델 — 한국어 명령 해석 정확도 · FunctionGemma 라이선스 · 한국어 · EXAONE 4.0 상업 조건 · HyperCLOVA X SEED 「제한적 예외」 내용 = 원문 · 시험 필요. +- 우리 쪽 추정 — 펜 옆 단추 = Enter · 펜 띄움 스냅 표시 · 아래 70px 띠 · B07 명령행 칸 기기 자판 · iPad 글(TEXT) 단계 한글 받개 초점(`ime-sink.ts:93-101` 이 누름 안에서 불리는지) = 화면 실측 필요. +- 숫자 크기 — 표식 메뉴 3.5 배 = 현장 2 명 · CAD 40% = 한 사용자 모형 · ERASE 최다 = 2008 블로그의 2차 인용 · 펜 잉크 50ms = 검색 요약 → 방향만 참고. +- 웹 CAD 전용 지연 연구 · CAD 전용 KLM 측정 · TLM 연산자 값 = 못 찾음. + +## 출처 + +- [W1] Figma 액션 메뉴 — https://help.figma.com/hc/en-us/articles/23570416033943-Use-the-actions-menu-in-Figma-Design +- [W2] FigJam 빠른 실행(화면 낭독기 글 안) — https://help.figma.com/hc/en-us/articles/14477051168791-Use-FigJam-with-a-screen-reader +- [W3] Excalidraw 단축키(도움말 창 원본) — https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/components/HelpDialog.tsx · 시작 안내 — https://plus.excalidraw.com/how-to-start +- [W4] Miro 도구 막대 · 고른 것 메뉴 — https://help.miro.com/hc/en-us/articles/360017730553-Toolbars +- [W5] Figma 위치 · 크기(미세 이동 · 식 · Mixed · 스크럽) — https://help.figma.com/hc/en-us/articles/360039956914-Adjust-alignment-dimensions-rotation-and-position +- [W6] Figma 복사 · 붙여넣기(Alt 끌기 + Ctrl+D) — https://help.figma.com/hc/en-us/articles/4409078832791-Copy-and-paste-objects +- [W7] Figma 스마트 선택(간격 손잡이) — https://help.figma.com/hc/en-us/articles/360040450233-Arrange-layers-with-Smart-selection +- [W8] draw.io 수정키 · 맞춤 도구 — https://www.drawio.com/docs/reference/shortcuts/modifier-shortcuts-in-diagrams/ · https://www.drawio.com/docs/manual/editor/alignment-tools/ +- [W9] tldraw 가장자리 이동 — https://tldraw.dev/sdk-features/edge-scrolling +- [W10] Figma 거리 재기 — https://help.figma.com/hc/en-us/articles/360039956974-Measure-distances-between-layers +- [W11] tldraw 인스턴스 상태(isPenMode · isCoarsePointer · 도구 잠금) — https://tldraw.dev/sdk-features/instance-state +- [W12] Autodesk Viewer 도구(마크업 · 재기) — https://knowledge.autodesk.com/search-result/caas/CloudHelp/cloudhelp/ENU/ADSKVIEWER-Help/files/ADSKVIEWER-Help-AutodeskViewerTools-html-html.html +- [W13] AU 강의 자료 CS317864(AutoCAD Web · 태블릿) — https://static.au-uw2-prd.autodesk.com/Class_Handout_CS317864_Shaun_Bryant.pdf +- [W14] AU 강의 자료 AS323988(모바일 숫자판 · 돋보기) — https://static.au-uw2-stg.autodesk.com/Class_Handout_AS323988_Jim_LaPier.pdf · 펜 숫자판 글 — https://forums.autodesk.com/t5/autocad-on-mobile-forum/length-and-keypad-issue/m-p/9427781 +- [W15] DWG FastView(앱 장터) — https://apps.apple.com/us/app/-/id456933691 +- [W16] MDN inputmode — https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Global_attributes/inputmode +- [W17] WebKit 195884(누름 안 초점만 자판) — https://bugs.webkit.org/show_bug.cgi?id=195884 +- [W18] tldraw 블로그 A touchy subject — https://tldraw.dev/blog/a-touchy-subject +- [W19] Excalidraw 3년(펜 모드) — https://plus.excalidraw.com/blog/year-three +- [W20] Concepts 설명서 — https://concepts.app/en/manual +- [W21] Morpholio Trace 안내서(2차) — https://www.scribd.com/document/562575563/TracePro-HowTo +- [W22] Vogel · Baudisch, Shift(CHI 2007) — https://projects.csail.mit.edu/hci-reading/papers/chi2007-vogel.pdf +- [W23] CAD Pockets(목록) — https://www.cesdb.com/cad-pockets.html +- [W24] Procreate 몸짓 — https://help.procreate.com/procreate/handbook/interface-gestures/gestures +- [W25] WebKit 213953(길게 눌러 contextmenu 없음) — https://bugs.webkit.org/show_bug.cgi?id=213953 +- [W26] MDN Pointer events(펜 단추 값) — https://developer.mozilla.org/en-US/docs/Web/API/Pointer_events +- [W27] WebKit 296943(Pencil hover) — https://bugs.webkit.org/show_bug.cgi?id=296943 +- [W28] Miro 스마트 그리기 — https://help.miro.com/hc/articles/360017572014 +- [W29] Hinckley 외, Pen + Touch = New Tools(UIST 2010) — https://www.microsoft.com/en-us/research/publication/pen-touch-new-tools/ +- [W30] AutoCAD 2027 새 기능 — Autodesk Assistant — https://help.autodesk.com/cloudhelp/2027/ENU/AutoCAD-WhatsNew/files/GUID-99E2D8F7-D4D9-4B3A-9E57-7B73D6BB12AD.htm +- [W31] Autodesk Assistant 소개(403 · 검색 요약) — https://www.autodesk.com/solutions/autodesk-ai/autodesk-assistant +- [W32] AutoCAD 2027.1 블로그(403 · 검색 요약) — https://www.autodesk.com/blogs/autocad/autocad-2027-1/ +- [W33] Markup Import · Assist 개요 · 자료 처리 — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Platform/files/GUID-0CD6B2CE-EAF0-478B-B39B-F4821F9DC91D.htm · https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Platform/files/GUID-46693B8E-FDC8-41F2-8E11-C96B718D815D.htm +- [W34] Smart Blocks 물체 감지 — https://help.autodesk.com/cloudhelp/2025/ENU/AutoCAD-WhatsNew/files/GUID-25BD5FB7-119A-42A8-B1C1-62BB812A3F4F.htm +- [W35] BricsCAD V26 — https://bricscad.octave.com/bricscad/v26 +- [W36] ARES A3 — https://www.graebert.com/blog/general-news/introducing-ares-ai-assist-a3-your-personal-cad-assistant/ · ARES 2027 — https://architosh.com/2026/04/graebert-releases-ares-2027-ai-push-and-forma-integration/ +- [W37] Onshape AI Advisor — https://cad.onshape.com/help/Content/ai_advisor.htm +- [W38] Siemens NX 명령 예측 — https://develop3d.com/develop3d-blog/siemens-adds-machine-learning-prediction-productivity-nx/ +- [W39] Solid Edge 2021 적응 화면 — https://www.engineering.com/solid-edge-2021-adds-subdivision-modeling-an-ai-ui-and-more/ +- [W40] 크롬 내장 AI 시작 — https://developer.chrome.com/docs/ai/get-started +- [W41] MDN Web Speech · processLocally — https://developer.mozilla.org/docs/Web/API/Web_Speech_API/Using_the_Web_Speech_API · https://developer.mozilla.org/docs/Web/API/SpeechRecognition/processLocally +- [W42] 엣지 로컬 음성 인식 — https://learn.microsoft.com/en-us/microsoft-edge/web-platform/speech-recognition-api +- [W43] Ollama 도구 부르기 — https://ollama.com/blog/tool-support · https://ollama.com/blog/streaming-tool +- [W44] Qwen3 — https://qwenlm.github.io/blog/qwen3/ +- [W45] HyperCLOVA X SEED — https://clova.ai/en/tech-blog/sowing-the-seeds-in-the-ai-ecosystem-introducing-hyperclova-x-seed-a-commercial-open-source-ai +- [W46] FunctionGemma — https://ollama.com/library/functiongemma +- [E1] Li 외, 명령 추천(TOCHI 2011 · CIP 통계) — https://www.tovigrossman.com/papers/2011%20TOCHI%20cc.pdf +- [E2] ERASE 최다 명령(2008 · Autodesk Research 인용) — https://budweiser.cadstudio.cz/2008/09/erase-most-frequent-autocad-command.html +- [E3] Autodesk Research 명령 쓰임 그림 — https://www.research.autodesk.com/projects/command-usage-arc-diagrams/ +- [E4] Bhavnani · John, From Sufficient to Efficient Usage(CHI 97) — https://chi1997.acm.org/proceedings/paper/skb.html +- [E5] Lane 외, Hidden Costs of GUIs(2005 · 서지) — https://dblp.dagstuhl.de/pid/65/2289.html +- [E6] Malacria 외, ExposeHK(CHI 2013) — https://hal.archives-ouvertes.fr/hal-01894253 +- [E7] Kurtenbach · Buxton, 표식 메뉴(CHI 94) — https://www.research.autodesk.com/publications/user-learning-and-performance-with-marking-menus/ +- [E8] Cockburn 외, 초보 → 숙련 넘어가기(2014) — https://doi.org/10.1145/2659796 +- [E9] KLM 연산자 값 — https://syntagm.co.uk/design/klmcalc.shtml · https://cs.auckland.ac.nz/courses/compsci345s1c/lectures/2006/Lecture%2019%20Cognitive%20models%202.pdf +- [E10] = [W22] +- [E11] Parhi 외, 엄지 표적 크기(MobileHCI 2006) — https://dx.doi.org/10.1145/1152215.1152260 +- [E12] WCAG 2.5.8 표적 크기 최소 — https://www.w3.org/WAI/WCAG22/Understanding/target-size-minimum.html +- [E13] Sears · Shneiderman, 정밀 터치 대 마우스(1991) — https://www.cs.umd.edu/users/ben/papers/Sears1991High.pdf +- [E14] Nishida 외, 한 번 톡 지연(2024 · Jota 2013 · Deber 2015 인용) — https://arxiv.org/pdf/2408.02525 +- [E15] MacKenzie · Ware, 지연과 성능(INTERCHI 93) — https://www.yorku.ca/mack/CHI93b.html +- [E16] Liu · Heer, 지연과 탐색(InfoVis 2014) — https://idl.uw.edu/papers/latency · 후속 요약 — https://arxiv.org/pdf/2507.19690 +- [E17] web.dev INP — https://web.dev/articles/inp +- [E18] Annett 외, 잉크 지연(GI 2014 · 검색 요약) — https://webdocs.cs.ualberta.ca/~wfb/publications/C-2014-GI-Latency.pdf +- [E19] Grossman 외, 단축키 학습(CHI 2007) — https://www.dgp.toronto.edu/~ravin/papers/chi2007_learninghotkeys.pdf +- [E20] MDN getPredictedEvents · 크롬 desynchronized — https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent/getPredictedEvents · https://developer.chrome.com/blog/desynchronized + diff --git a/docs/raw/조사/2026-10-15_335_재조사_CAD입력.md b/docs/raw/조사/2026-10-15_335_재조사_CAD입력.md new file mode 100644 index 000000000..adb47a706 --- /dev/null +++ b/docs/raw/조사/2026-10-15_335_재조사_CAD입력.md @@ -0,0 +1,257 @@ +# CAD 입력 보강 — 재조사 (요구 226-3 · PLAN 335-3) + +재료 = 코덱스 1 · 2차(해외 CAD 16 종) · 안티그래비티(국내 리습 · 국산 CAD · 측량 28 가지) + 코덱스 3차 출처 대조 · B4 토목측량 입력 · B5 웹 · 터치 · 펜 · AI · 효율 근거 · 앞 검토 335(본문 · 부록1 · 부록2) · 코드 읽기(`main_laptop_1` b7e727863 · 실행 · 화면 실측 없음) · 원문 = 같은 폴더 `2026-10-15_335_부록3_바깥조사원문.md`. + +- 기호 — 쓸모 상 · 중 · 하(임도 도면 손질 기준 · 짐작 포함) · 우리 있음 · 일부 · 없음 · 크기 작(반나절) · 중(하루~이틀) · 큼(사흘 넘음 · 335 와 같은 자) · 근거 공식(공식 문서 · 표준 · 논문) · 대조(코덱스 3차가 출처 확인 · 「일부」는 바른 내용으로 고쳐 씀) · 약함(출처 못 찾음 · 제3자 · 앱 장터 · 카페). +- 줄 이름 — 335 번호(1-4 · D2 · H1 등) · B4 번호(P · L · C · T · S · M · K) · B5 번호(1-1 · 2-1 · 3-1 · E1 · W29) · 코덱스 X1(1차) · X2(2차) · 안티그래비티 A1~A28. +- 경로 줄임 — `src/` = `B07_DesignDetail/openwebcad/src/` · 별표2 = `resources/knowledge/original/법률/산림자원의 조성 및 관리에 관한 법률 시행규칙/별표/별표2_산림관리기반시설의 설계 및 시설기준(제5조제1항 관련).md` · 그 밖 페이지 파일은 저장소 뿌리부터. + +## 0. 결론 요약 + +1. 바깥 조사 셋에서 앞 71 가지에 없던 입력 방법 53 줄을 새로 추림(겹침은 한 줄로 합침) — 쓸모 상 20 · 중 29 · 하 3 · 뺌 1 · 우리 없음 38 · 일부 15. +2. 가장 큰 새 갈래 = 「실제 값으로 넣기」 — 우리 CAD 는 종이 위 mm 만 앎 → 「No.12+5 좌 3 m」 · 「표고 123.45」 · 「경사 8 %」를 칠 수 없고 1/100 · 1/200 · 1/1200 을 손으로 바꿔 쳐야 함. +3. 새 흠 둘(코드로 확인) — H13 사람이 넣는 치수 · 거리 · 면적 숫자가 종이 길이로 나옴(횡단 3 m → 「30」 · 평면 3 m → 「2.5」 · 면적 100 배) · 그 숫자를 고칠 칸도 없음 / H14 B07 횡단도는 도로 왼쪽을 종이 오른쪽에 그림 → B06 화면과 좌우가 거울(어느 쪽이 맞는지 사용자 확인). +4. 좋은 소식 — 서버가 횡단 칸마다 축척 · 원점 · 기준 높이를 이미 만들어 페이지에 보냄 → CAD 안으로만 넘기면 횡단부터 실제 값 입력이 작게 열림. +5. 둘째 갈래 = 「어느 기기든 같은 입력」 — 태블릿 · 휴대폰엔 숫자 · 명령을 넣을 길이 없음 → 화면 숫자판 · 명령 찾기 창 · 고른 것 곁 단추 막대로 마우스 · 손가락 · 펜 모두 같은 판. +6. 셋째 갈래 = 「임도 표기 도구」 — 측점 · 표고 · 경사 글 · 길이 합 · 숫자 한꺼번에 셈 · 번호 매김 · 번호 늘려 복사(국내 리습에 흔함 · 출처는 반쯤만 확인). +7. 안티그래비티 28 가지 = 출처 확인 2 · 일부 14 · 못 찾음 12 → 못 찾은 것은 「근거 약함」으로만 둠 · 재료 속 도메인 숫자(종단 13~18 % · 소단 5 m 등)는 버리고 지식DB 값으로 바꿈. +8. AI — 상용 CAD AI 는 모두 바깥 서버라 자료 유출 금지 조건에서 못 씀 · 사내에서 되는 길 = 한글 명령 찾기(모델 없음) · 다음 명령 후보 · 사내 작은 모델(시험만). +9. 묶음 셋 더함 — ⑧ 실제 값 입력 · ⑨ 기기 무관 입력 · ⑩ 임도 표기 도구 · 새 차례 = ⓪ 흠(+H13) → ⑨ 앞몫 → ③ 고르기 → ⑧ 횡단 앞몫 → ② 스냅 → 나머지(앞 다섯 어림 21~24 일). +10. 정할 것 아홉 더함(15~23) — 먼저 = 횡단 좌우(17) · 오프셋 부호(15) · 측점 꼴(16) · 치수 실값 범위(18). + +## 1. 새로 찾은 것 표 + +- 335 의 71 줄 · D1~D15 와 같은 것은 줄로 안 셈(1-6 묶음 줄) · 일부만 겹치면 「같음 계열: …」 + 덧만 적음 · 재료끼리 겹침 = 한 줄에 출처를 모두 적음. +- 크기 = 문맥(⑧ 축척 · 원점 · 노선)을 넘긴 뒤 기준 · 코드 읽기 어림. + +### 1-1. 실제 값 입력 — 측점 · 오프셋 · 표고 · 경사 · 단위 (⑧) + +| # | 이름 | 어디 | 어떻게 입력하나 | 쓸모 | 우리 | 크기 | 묶음 | 근거 | +|---|---|---|---|---|---|---|---|---| +| N01 | 측점 + 오프셋으로 점 | Civil 3D `'SO` · OpenRoads Civil AccuDraw · TBC Offset Line Snap · 12d · GEOPAK(B4 P1~P5) · 같음: A22(INFRACAD 「STA 3+15.5, L 4.0」) | 점 묻는 단계에서 노선(폴리선) 고름 → 측점 → 오프셋(치거나 찍기) · 칸 잠금 = 같은 값 되풀이 · 잠긴 칸에 Enter 또 = 그만큼 더함 · TBC 는 노선 객체 없이 폴리선만으로 됨 | 상 — 구조물 · 기호를 「No.12+5 좌 3 m」 자리에 바로 | 없음 | 중 | ⑧ 평면 | 공식 · A22 약함 | +| N02 | 측점 · 오프셋 조회 | 12d · Carlson · TBC(B4 P6) · 드림플러스 GST(A2) | 점 찍음 → 노선 기준 측점 · 수직 오프셋 보고 | 상 — 손질한 기호 · 글 자리를 측점으로 대조 | 일부 — `ID` = 종이 mm | 작 | ⑧ | 공식 · 대조 | +| N03 | 측점 꼴 「번호 + 나머지」로 치기 | Trimble Access Station index · Civil 3D(`+` 없이 숫자)(B4 P7) | `No.12+5` · `12+5` · `245`(m) 다 받음 · 표시는 한 꼴 | 상 — 지식DB 꼴 「No.11+13」(`resources/knowledge/original/산림과임업기술(임도)/2. 임도/5. 설계.md:157`) · 측점 간격 20 m(별표2 `:25`) | 없음 — 글만(평면 `No.n+d` · 제목 「2+0.0」 · 정보 「STA.0+050.000」) | 작 | ⑧ | 공식 | +| N04 | 측점으로 화면 이동 | Civil 3D `'ZTP`(B4 P8) | 측점 쳐서 그 자리로 확대 | 중 — 긴 평면 · 종단 | 없음 — ZOOM = 범위만 | 작 | ⑧ | 공식 | +| N05 | 간격 + 오프셋 되풀이 점 · 두 점 임시 기준선 | Carlson Locate Template Points · TBC Create Points at Station Offsets(B4 P9 · P10) | 측점 간격 + 오프셋 되풀이 · 두 점으로 임시 기준선 → 측점 · 오프셋 | 중 — 표지 되풀이 · 비스듬한 구조물 축 | 일부 — MEASURE · DIVIDE = 선 위 간격 점만 | 중 | ⑧ 뒤 | 공식 | +| N06 | 측점 · 표고로 점(종단) | Civil 3D `'PSE` · `'STAE`(B4 L1) · Profile Toolset 띠(L3) · OpenRoads 종단 칸(L4) | 뷰 고름 → 측점 → 표고(치거나 찍기) · 뷰가 가로 · 세로 축척을 앎 | 상 — 종단 세로 5 배(가로 1/1000 · 세로 1/200) 손 환산 없앰 | 없음 | 중 | ⑧ 종단 | 공식 | +| N07 | 경사 % + 측점 · 길이로 선(종단) | Civil 3D `'PGS` · `'PGL`(B4 L2) · 같음: A25(구배율 입력) | 앞 점 → 경사 → 측점(또는 길이) 되풀이 · Esc 끝 | 상 — 계획선 · 보조선을 경사 % 로(기준 = 5-2) | 없음 — 각 = 종이 각 | 중 | ⑧ 종단 | 공식 · A25 약함 | +| N08 | 오프셋 · 표고로 점 · 표 입력(횡단) | Carlson Input-Edit Section File(B4 C1) | 오프셋 · 표고 침(왼쪽 부호 = 정할 것 15) · 앞 점에서 경사 · 비율 치면 표고 계산 · 칸 식 | 상 — 지반선 · 계획선 · 비탈 끝을 m 값으로 | 없음 — 그립 = 종이 mm | 중 | ⑧ 횡단 | 공식 | +| N09 | 고저 편집 표(점마다 측점 · 표고 · 앞뒤 경사) | Civil 3D Elevation Editor(B4 L5) | 표 줄 고침 → 선이 따라 바뀜 · 여러 줄 경사 한 번 | 중 | 없음 — 특성 칸 길이 = 읽기만 | 큼 | ⑧ 뒤 | 공식 | +| N10 | 커서 자리 실값 읽기 | Carlson · GEOPAK · OpenRoads(B4 C3) | 커서만 움직이면 오프셋 · 표고 / 측점 · 표고 / 측점 · 오프셋 | 상 — 모든 손질에 바로 | 없음 — 커서 곁 = 종이 mm 좌표 | 작 | ⑧ | 공식 | +| N11 | 실거리 단위 붙여 치기 `3m` | TBC COGO 칸(B4 C5) | `3m` → 축척대로(횡단 30 mm · 평면 2.5 mm) · 칸 안 사칙 · 점 표고 집기 | 상 | 없음 — 단위 붙은 숫자 없음 · 식 = 335 1-4 | 작 | ⑧ | 공식 | +| N12 | 북 · 동 좌표 · 방위각 · 편각 + 거리 | Civil 3D `'NE` `'BD` `'ZD` `'DD`(B4 S1 · S2) · 같음: A23(방위각 도분초) | 북 · 동 값으로 점(국내 X = 북 · Y = 동 → CAD X · Y 와 거꾸로) · 편각 = IP 교각 손버릇 | 중 — 기준점 · 구조물 TM 자리 · 교각 부호 = 지식DB 좌측각 (+)(`…/2. 임도/4. 노선측량.md:322`) | 일부 — 극좌표 = 동 0° 반시계 십진 도만 · 평면 원점 없음 | 중 | ⑧ 평면 뒤 | 공식 · A23 약함 | +| N13 | 치수 측정 축척(DIMLFAC) | AutoCAD 치수 스타일(B4 T4) | 측정값 × 계수 = 치수 글 | 상 — H13 을 바로 풂 | 없음(H13) | 중 | ⓪ · ⑧ | 공식 | + +### 1-2. 스냅 · 값 · 끌기 손버릇 덧 (① ② ④ ⑤) + +| # | 이름 | 어디 | 어떻게 입력하나 | 쓸모 | 우리 | 크기 | 묶음 | 근거 | +|---|---|---|---|---|---|---|---|---| +| N14 | 비탈 꼴 바꿔 받기(% · 1:n · n:1 · 각) | Civil 3D 비탈 칸(제3자) · Trimble Access 꼴(B4 C4) · 같음 계열: 335 D2 `<1:1.5` · A24(Slope Lock) | 한 칸이 `25%` · `1:1.5` · `1.5:1` · 각 모두 받음 · 표시 꼴 설정 | 중 — 비탈 기준 = 5-2 | 없음 — D2 제안뿐 | 작 | ②(D2 와 한 함수 · ⑧ 공용) | 공식 일부 · 제3자 | +| N15 | 끌기 고정 꼴(비탈 따라 · 높이만 · 오프셋만) | Carlson(B4 C2) | 점 끌 때 Hold Slope · Elevation · Offset | 중 — 비탈 기울기 그대로 끝만 늘림 | 일부 — 그립 끌기 · 축 고정 = 335 D3 제안 | 중 | ④ | 공식 | +| N16 | 객체 길이 · 반지름 집기 | Civil 3D `AmL` · `AmR`(B4 S4) | 길이 묻는 단계에서 객체 고르면 그 값 | 중 — 같은 길이 · 반지름 되풀이 | 없음 | 작 | ① | 공식 | +| N17 | 칸 값 증감 `X3+` | Archicad Tracker(X2) · 같음 계열: Figma `+10`(B5 1-3) · 335 1-4 식 | 지금 좌표 · 값에 더하고 뺌 · `/` 로 XY ↔ 거리 · 각 | 중 | 없음 | 작 | ① | 공식 | +| N18 | 나침반을 고른 요소 방향으로 · Enter 스마트 잠금 · 가 스냅 | MicroStation AccuDraw `RE` · Enter · Tentative(B4 M2 · M4 · M5) | 요소 하나 찍어 축 맞춤 · 축 가까우면 0 잠금 · 가 스냅 뒤 확정 | 중 — 노선 · 비탈 방향 기준 앞 · 옆 거리 | 없음 — UCS 없음 · 가 스냅 = 335 2-4 계열 | 중 | ② 뒤 | 공식 | +| N19 | 확대 따라 바뀌는 스냅 눈금 | BricsCAD 적응형 스냅 눈금자(X1) | 확대 수준 따라 격자 · 스냅 간격 자동 · 눈금자에서 고름 | 중 | 없음 — 그리드 = 화면 무늬(H10) | 중 | ②(정할 것 10) | 공식 | +| N20 | 손잡이 위젯(축 · 회전 손잡이) | BricsCAD Manipulator · Rhino Gumball(X1) · 같음: 부록1 9장(낮음 판정) | 고르면 축 · 회전 손잡이 · 끌며 거리 · 각 숫자 · Ctrl / Alt = 복사 | 중 | 없음 | 큼 | ④ 맨 뒤 | 공식 | +| N21 | 같은 거리 되풀이 복제 · 한 명령 안 반복 변환 | Figma Alt 끌기 → Ctrl+D · draw.io(B5 1-4) · ARES QuickModify(X2) · 같음 계열: 335 5-4 | 한 번 옮겨 복제한 거리 기억 → 키 한 번에 하나 더 | 중 — 배수관 · 표지 같은 간격 | 일부 — COPY 여러 번 · ARRAY | 작 | ⑤ | 공식 | +| N22 | 미세 이동 두 걸음 · 걸음 값 | Figma · draw.io(B5 1-5) · 같음 계열: 335 6-2 | 고른 것 있으면 화살표 1 · Shift 10 | 중 — 글 · 기호 자리 | 없음 — 화살표 = 화면 이동 | 작 | ⑤ | 공식 | +| N23 | 누르는 동안 스냅 끔 | draw.io Alt(B5 1-6) | 키 누르는 동안만 | 중 — 촘촘한 횡단 상세 | 일부 — F3 · 한 번 NON | 작 | ② | 공식 | +| N24 | 고른 것 맞춤 보기 | Excalidraw Shift+2(B5 1-7) | 키 · 단추 하나 | 중 — 긴 평면에서 고른 구조물로 | 일부 — 전체만(Z · ⛶) | 작 | ⑤ | 공식 | +| N25 | 여러 개 고른 값 「섞임」 + 식 | Figma `Mixed+100`(B5 1-3) · 같음 계열: 335 6-7 | 섞인 칸에 식 → 고른 것마다 제 값에 적용 | 상 — 글 높이 · 자리 일괄 | 없음 — 특성 칸 = 색 · 선 · 층 · 여러 개면 첫 값 | 중 | ⑤(6-7 덧) | 공식 | +| N26 | 명령 없이 거리 표시 | Figma Alt 스침(B5 1-9) | 고른 뒤 Alt + 다른 것 위 = 가로 · 세로 거리 | 중 — 손가락은 못 씀 | 없음 — DIST 명령 | 작 | ⑤ 뒤 | 공식 | + +### 1-3. 어느 기기든 같은 입력 — 숫자판 · 찾기 · 막대 · 펜 · 터치 (⑨) + +| # | 이름 | 어디 | 어떻게 입력하나 | 쓸모 | 우리 | 크기 | 묶음 | 근거 | +|---|---|---|---|---|---|---|---|---| +| N27 | 화면 숫자판 · 값 눌러 고치기 | AutoCAD 모바일 · DWG FastView · 웹 `inputmode`(B5 2-1) · 같음: 부록1 6장 숫자판(낮음 → 기기 무관 원칙으로 올림) | 값 누름 → 숫자판 → Enter | 상 — 자판 없는 기기에서 거리 · 좌표 · 반지름 · 글 높이 | 없음 — 손가락 단추 6(`src/inputController/input-touch.ts:126-133`) | 중 | ⑨ | 공식 · 앱 장터 | +| N28 | 명령 찾기 창 | Figma Ctrl+K · FigJam(최근 셋) · Excalidraw Ctrl+/(B5 1-1) | 단추 · 키 → 낱말 → 목록 → 누름 · 별칭 같이 보임 | 상 — 명령행 없는 B06 상세 · M02 카드 · 태블릿에서도 153 명령 | 일부 — 명령행 후보 8 · 커서 곁 6 · 앞글자만 · B06 · M02 없음 | 중 | ⑨ | 공식 | +| N29 | 고른 것 곁 단추 막대 | Miro · AutoCAD 모바일(B5 1-2) · 같음 계열: BricsCAD Quad(335 9-1) · Onshape `S` 툴바 · Vectorworks Smart Options · Fusion 표식 메뉴(X1 · X2) | 고르면 곁에 옮김 · 복사 · 지움 · 특성 단추 | 상 — 리본 없는 곳 · 손가락 | 없음 | 중 | ⑨(⑤ D15 와 한 틀) | 공식 | +| N30 | 펜 모드(펜이면 손가락 = 화면 이동만) | tldraw · Excalidraw · Morpholio · Concepts(B5 2-2) · Shapr3D(X2) | 펜 첫 사용 때 자동 · 끄기 단추 | 상 — 손바닥 톡이 점 · 고름으로 들어가는 것 막음 | 없음 — 펜 = 마우스 길 · 손가락 톡 = 늘 누름 | 작 | ⑨ | 공식 | +| N31 | 확대 창(손가락 가린 곳 · 마우스도) | Shift 기법 논문 · DWG FastView · AutoCAD 모바일 돋보기(B5 2-3) · Vectorworks Snap Loupe `Z`(X2) · 같음: 부록1 6장 돋보기(낮음 → 올림) | 손가락 길게 → 손가락 위 확대 창 / 마우스 `Z` → 커서 둘레 잠깐 확대 → 스냅점 누름 | 상 — 횡단 상세 끝점 · 교차 촘촘 | 일부 — 길게 = 위 70px 십자선 · 확대 창 없음 | 중 | ⑨ | 공식 · 앱 장터 | +| N32 | 두 손가락 톡 = 되돌리기 · 세 손가락 = 다시 · 몸짓마다 단추 | Procreate · Concepts · Morpholio(B5 2-4 · 2-5) | 화면 아무 데나 톡 · 같은 일을 단추로도 | 중 | 일부 — 단추 ↶ ↷ | 작 | ⑨ | 공식 | +| N33 | 펜 옆 단추 · 지우개 끝 | 웹 Pointer Events(B5 2-7) | 옆 단추 누른 채 톡 = 확정 · 뒤집어 문지름 = 지움 | 중 | 일부(추정) — 옆 단추 = 우클릭 = Enter · 지우개 처리 없음 | 작 | ⑨ | 공식 | +| N34 | 끌다가 가장자리 자동 이동 | tldraw(B5 1-8) | 끌기 중 화면 끝에 머물면 화면이 따라 감 | 중 — 작은 화면 · 긴 노선 | 없음 | 중 | ⑨ | 공식 | +| N35 | 한글로 명령 찾기(이름 · 초성 · 한글 자판 그대로) | 우리 덧(B5 3-11) · 드림플러스 HCMD(A16 — 모르는 명령이면 영문 자판으로 바꿔 다시) · 같음 계열: 335 3-2 · D13 · H12 · A17(CADian · 못 찾음) | 「간격띄우기」 · 「ㄱㄱㄸ」 · 「ㅐㄹㄹㄴㄷㅅ」 → OFFSET | 상 — 한글 화면 · 초보 · N28 과 한 벌 | 없음 — 명령 단계 = 영문 · 내 별칭 영문 · 숫자 | 작 | ⑨(⑥ 3-2 · D13 당김) | 대조 · 판단 | + +### 1-4. 임도 표기 · 재기 도구 (⑩) + +- 서버가 이미 자동으로 그리는 것(평면 측점 눈금 · 번호 · 비탈 눈금 · 횡단 수량 표 · 종단 측점 표)은 손질 때만 씀. + +| # | 이름 | 어디 | 어떻게 입력하나 | 쓸모 | 우리 | 크기 | 묶음 | 근거 | +|---|---|---|---|---|---|---|---|---| +| N36 | 측점 · 오프셋 글 | Civil 3D Station Offset 라벨(B4 T1) · 드림플러스 STT 선택 측점 표기(A2 · 좌우 오프셋 지시선 순서는 못 찾음) | 노선 고름 → 점 → 「No.5+12.0 좌 3.0」 | 상 | 없음 | 중 | ⑩(⑧ 평면 뒤) | 공식 · 대조 | +| N37 | 횡단 글 — 오프셋 · 표고 / 두 점 경사 | Civil 3D Section View Labels(B4 T2) · 같음: A5(SLP · 못 찾음) | 점 → 「EL 123.45」 · 두 점 → 「1:1.5」 | 상 | 없음 | 중 | ⑩(⑧ 횡단 뒤) | 공식 · A5 약함 | +| N38 | 종단 글 — 측점 · 표고 / 깊이 · 경사 | Civil 3D Profile View Labels(B4 T3) · 드림플러스 FE(기준 대비 상대 EL) · GEL(두 점 사이 EL)(A6 · 재료의 `EL` 한 명령은 못 찾음) | 점 → 측점 · 표고 글 · 두 점 → 경사 글 | 상 | 없음 | 중 | ⑩(⑧ 종단 뒤) | 공식 · 대조 | +| N39 | 측점 눈금 · 번호 일괄 표기 | 드림플러스 DSTA(A1) | 폴리선 → 시작 측점 · 간격 → 직각 눈금 + 번호 글 | 중 — 노선 손질 뒤 다시 매길 때만 | 일부 — 서버가 평면에 자동 · CAD 도구 없음 · MEASURE = 점만 | 중 | ⑩(⑧ 평면 뒤) | 대조(확인) | +| N40 | 골라 길이 합 | Lee Mac TLEN(A7 — 결과 = 명령줄 · 도면 글 아님) | 선 · 호 · 폴리선 여럿 고름 → 총 연장 | 중 — 측구 · 난간 연장 대조 | 없음 — LIST = 낱개 길이 · 종이 단위(`src/tools/utility/inquiry-tools.ts:114`) | 작 | ⑩ | 대조 | +| N41 | 닫힌 영역 안 한 번 누름 → 경계 · 해치 · 면적 | Archicad Magic Wand(X2 · Space + 누름) · A8(면적 글 · 못 찾음) | 영역 안 누름 → 경계 다각형 → 해치 · 면적 글 | 상 — 횡단 절 · 성토 면 해치 · 면적 | 없음 — HATCH · BOUNDARY = 경계 객체를 골라야(`src/tools/draw/fill-tools.ts:36,81`) · AREA = 알림만 · 종이 단위 | 중 | ⑩ | 공식 · A8 약함 | +| N42 | 숫자 글 한꺼번에 셈 | 드림플러스 CALC 문자 사칙연산(A10 — 재료의 `CTC` 이름은 틀림) | 숫자 글들 고름 → `+0.5` → 글 숫자 일괄 갱신 | 중 — 계획고 · 표고 일괄 가감 | 없음 | 작 | ⑩ | 대조 | +| N43 | 번호 일괄 매김 | AutoCAD Express TCOUNT(A9 · 국산 CAD 호환은 못 찾음) | 글 여럿 고름 → 정렬(X · Y · 고른 순) → 시작 · 증분 → 덮어쓰기 · 앞 · 뒤 | 중 — 집수정 · 단면 번호 | 없음 | 중 | ⑩ | 공식 | +| N44 | 번호 늘려 복사(증분 복사) | GstarCAD Architecture(A20 · ZWCAD 일반 기능 근거 없음) | 숫자 든 글 복사 → 놓을 때마다 숫자 +1 | 중 — 배수관 · 측점 번호 | 없음 — COPY = 글 그대로 | 중 | ⑩ | 대조 | +| N45 | 표 ↔ 엑셀 | 드림플러스 ETC(엑셀 → 캐드) · CTE(캐드 → 엑셀)(A11 · 재료는 방향이 거꾸로) · KTOOL 엑셀 ↔ 종단(B4 K1 · 카페) | 엑셀 칸 복사 → 캐드 표 · 캐드 표 고름 → 엑셀 | 중 — 수량 · 좌표 표 손질 | 없음 — 붙여넣기 = DXF 글 · 앱 클립보드만(`src/integration/dxf-clip-paste.ts:79-84`) | 중 + 중 | ⑩ | 대조 · 약함 | +| N46 | 좌표 글(TM 북 · 동) | 드림플러스 XY(A13 · `X(N) Y(E)` 꼴은 못 찾음) | 점 → 지시선 끝 → 좌표 글 | 중 — IP · 시종점 · 구조물 좌표 | 일부 — DIMORDINATE = 종이 좌표(H13) | 작 | ⑩(⑧ 평면 뒤) | 대조 | +| N47 | 숫자 키 층 켬끔(11 · 22 · 33) | 국내 사용자 LISP(A12 — 보편 단축키 아님) | `11` = 층 하나만 · `22` = 모두 켬 · `33` = 끔 | 하 — B07 도면 층이 적음 | 일부 — LAYISO · LAYON · LAYOFF 명령 있음 · 내 별칭에 숫자 이름 가능(`A00_Common/app_settings_cad.ts:48`) | 작 | ⑥(안내만) | 대조 | + +### 1-5. AI · 그 밖 (⑥ ⑦ · 뺌) + +| # | 이름 | 어디 | 어떻게 입력하나 | 쓸모 | 우리 | 크기 | 묶음 | 근거 | +|---|---|---|---|---|---|---|---|---| +| N48 | 다음 명령 후보 | Siemens NX · Solid Edge(B5 3-7) | 내 명령 「앞 → 다음」 짝을 세어 커서 곁에 | 중 — 간격띄우기 → 자르기 같은 순서 | 일부 — 빈 Enter = 직전 명령 | 작~중 | ⑥(D9 와 같이) | 제3자 기사 | +| N49 | 자연어 → 명령(사내 작은 모델 + 도구 부르기) | Ollama 도구 부르기 · Qwen3 · HyperCLOVA X SEED · FunctionGemma(B5 3-10) | 「고른 글 높이 2.5 로」 → 명령 이름 + 값 → 미리보기 → [확인] | 중 | 없음 | 큼(시험) | ⑦ 시험만 | 공식(모델 문서) · 한국어 정확도 미확인 | +| N50 | 좌표 표 → 점 · 선 | 드림플러스 CIM(A27 — 끌어 놓기 · 5 열 꼴은 못 찾음) | 엑셀 좌표 붙임 → 점 · 선 · 3D 폴리선 | 하 — B07 도면은 서버 자동 | 없음 | 중 | ⑦ | 대조 | +| N51 | 검토 표시층(마크업) | Autodesk Viewer · Docs(B5 1-12) | 도면은 그대로 · 위에 화살표 · 구름 · 글 · 재기 | 중 — 확정 도면에 의견(정본 안 건드림) | 없음 — 확정 = 보기 전용 · REVCLOUD 는 편집 | 큼 | ⑦(저장 자리 정해야) | 공식 | +| N52 | 클라우드 AI · 음성 · 브라우저 안 AI | Autodesk Assistant · Markup Assist · Smart Blocks 감지 · BricsCAD Assist · ARES A3(X2) · Onshape AI Advisor · 크롬 내장 AI · Web Speech(B5 3-1~3-6 · 3-8 · 3-9) | 채팅 · 음성 → 명령 | — | 없음 | — | 뺌 — 도면이 바깥 서버로 감(유출 금지) · 안드로이드 · iOS 빠짐(기기 무관 어긋남) | 공식 | +| N53 | 음성 메모 | ZWCAD Smart Voice(A19 · X1) · GstarCAD | 점 · 객체에 녹음 붙임 | 하 | 없음 | 중 | 뺌 | 대조(확인) · 공식 | + +### 1-6. 같음 · 이미 함 · 낮음 (줄로 안 셈) + +- 같음(335 에 있음) — SketchUp 축 잠금(X1) · Vectorworks Data Bar · GstarCAD · nanoCAD 동적 입력(X2 = 331-4 Tab 칸) · Onshape · Fusion 그리면서 치수 칸(음수 = 반대 방향은 우리도 됨 `src/helpers/get-point-from-event.ts:42`) · Fusion 사각형 치수 · QCAD X/Y(1-7) · ARES SmartSelect(4-11) · Rhino SmartTrack(1-10 · 2-7) · ZWCAD · SOLIDWORKS · DraftSight 몸짓 · A18(9-3) · MicroStation M1 · M3 · M6 · LibreCAD · FreeCAD · SolveSpace(새것 없음). +- 서버 · 설계 쪽이 이미 함 — A14 GRP 종단 지반선 · A15 CFM 횡단 지반선(B05 단면 뽑기) · A21 가로 · 세로 다른 축척 종단 틀(`B07_DesignDetail/B07_DesignDetail_Engine_Cad_Long.py:53-55`) · A3 DSLO 비탈 · A28 소단(B06 설계) · A26 종단곡선(B05) · A4 TALUS 법면 기호(평면 비탈 눈금 = `B07_DesignDetail/B07_DesignDetail_Engine_Cad_PlanSlope.py:1` 자동 · 횡단 띠 해치 = 335 8-7) · B4 K1 표 → 종단(B05 → B07 자동). +- 낮음 · 설계 정본 몫 — PVI 표 · 끌기 고정(B4 L6 · B05 몫) · 값 글자 눌러 규칙대로 바꿈(B4 T5 · Onshape 치수 구동 X1 — 규칙 객체 없음 · 큼) · 측점 추적선(P11) · 사이드샷 · 점 번호(S3) · 곡선 계산기(S5) · 값 칸 끌기(B5 1-10) · 길게 누름 메뉴(2-6 → N29 로) · 펜 띄움 미리보기(2-8 · 있음 추정) · 그리다 멈추면 반듯한 도형 · 문질러 지움(2-9 · 2-10) · 손가락 고름 크기(1-11 · 있음). + +## 2. 새 흠 · 확인 거리 + +### 2-1. 흠(H13~) + +| # | 무엇이 틀림(화면 말) | 근거 | 어디서 | 고침(한 줄) | 크기 | 급함 | 판정 | +|---|---|---|---|---|---|---|---| +| H13 ⚠ | 사람이 CAD 로 넣는 치수 · 재기 숫자 = 종이 길이 — 횡단(1/100) 3 m → 「30」 · 면적 2 m² → 「200」 · 평면(1/1200) 3 m → 「2.5」 · 종단 세로(1/200) 높이 1 m → 「5」 · 표준도(1/50) 1 m → 「20」(측구 확대도 1/10 → 「100」) · 종단 각도 치수 = 세로 5 배 과장 각 · 치수 글 덮어쓰기 칸이 없어 숫자를 고칠 길도 없음 · 서버가 만든 수치는 글(Text)이라 맞음 | 선형 치수 = 두 점 종이 거리 반올림 `src/entities/MeasurementEntity.ts:304,419` · 반지름 · 지름 · 호 길이 `src/tools/annotate/annotation.helpers.ts:43-48` · 좌표 치수 = 종이 X · Y `src/tools/annotate/dimension-radial-tools.ts:146-163` · DIST · AREA · LIST `src/tools/utility/inquiry-tools.ts:36,88,114` · 치수 설정 = 글 높이 · 화살표 · 자리수 · 주석 축척(크기만) `src/commands/dim-settings.ts:11-36` · 치수 객체 칸 = 세 점 · 색 · 선 · 층(글 칸 없음) · 서버 도면 형 = Line · PolyLine · Circle · Hatch · Table · Text · Image(치수 객체 없음) · 축척 상수 `config/config_system.py:155-194` | B07 횡단 · 종단 · 평면 · 표준도 · 유토 · 유역 · DXF · DWG 내보내기(그 숫자 그대로 · 추정) | 장 · 칸마다 측정 계수(m ÷ 종이 mm) 받아 치수 · 재기 값에 곱함 · 종단은 가로 · 세로 따로(기운 치수 · 각도는 막거나 경고) · 한 장 두 축척은 칸별(정할 것 18) | 중 | 중~높음 | 코드로 확인됨(값 만드는 줄에 축척 곱 없음) · 화면 숫자는 실측 안 함 | +| H14 ⚠ | 횡단 좌우 — B07 횡단도는 도로 왼쪽(+ 오프셋)을 종이 오른쪽에 그림 · B06 횡단 화면은 왼쪽을 화면 왼쪽에 그림 → 같은 측점이 두 화면에서 좌우 거울 · 진행 방향을 보고 그리는 관례라면 B07 쪽이 뒤집힌 셈 | + = 왼쪽(법선 = 접선 반시계 90°) `B05_Profile/B05_Profile_Engine_Sections_Core.py:406,562` · B07 종이 x = 오프셋 × 10 + ox(부호 그대로) `B07_DesignDetail/B07_DesignDetail_Engine_Cad.py:13,557-558` · B07 겹침 그림 같은 식 `B07_DesignDetail/B07_DesignDetail_Cross_Overlay.ts:603` · B06 화면 x = 왼쪽 끝 + (최대 오프셋 − 오프셋) `B06_Section/B06_Section_UI_Cross_Axes.ts:60,73` · 지식DB 좌우 방향 글 없음(「좌측하단에서 상단방향」 = 장 안 배치 차례 · 별표2 `:69`) · 시험이 좌우를 고정하지 않음 | B07 횡단 · 횡단 장 · 겹침 구조물 · 내보내기 · ⑧ 오프셋 입력 | 관례 확인 뒤 — B07 을 B06 처럼 뒤집거나(엔진 · 겹침 · 테두리 · 표 자리 · 시험) 지금대로 두고 까닭을 적음 | 중(뒤집을 때 · 추정) | 높음 후보(관례 확인 먼저) | 식은 코드로 확인됨 · 화면 좌우 · 관례는 화면 확인 + 사용자 확인 필요 | +| H15 | 손가락 정밀 고르기(길게 누름)는 늘 손가락 위 70px 자리를 누름 → 캔버스 아래 70px 띠는 정밀 모드로 못 짚음(추정) · 위 70px 안에서 길게 누르면 십자선이 캔버스 밖 | `src/inputController/input-touch.ts:18,71,95,111` | 손가락 기기 세 곳 | 가장자리에선 띄우는 방향을 바꿈 · 또는 확대 창(N31)으로 갈음 | 작 | 낮음 | 식은 코드로 확인됨 · 못 짚는지는 화면 확인 필요 | + +### 2-2. 확인 거리(흠 아님 · ⑧ 바닥) + +- K1 — 서버가 횡단 칸마다 변환값(`cross_placements` = 측점 · ox · oy · dy · mm_per_m · 테두리)을 이미 냄 `B07_DesignDetail/B07_DesignDetail_Engine_Cad.py:619-638` · 페이지만 씀 `B07_DesignDetail/B07_DesignDetail_UI_Cad_Structures.ts:28-30` · CAD 엔 안 감(`src/App.types.ts:41-57` 축척 · 원점 칸 없음 · `src` 의 station · chainage · 측점 = 주석 · 수량 패널뿐) → 횡단은 넘기기만 하면 됨(작). +- K2 — 종단 · 평면은 변환값을 따로 안 냄 — 축척 = 상수(종단 가로 1/1000 · 세로 1/200 · 평면 1/1200 · 고를 수 있음 500~6000 `config/config_system.py:155-191`) · 종단 기준선 표고 · 평면 원점(장 콘텐츠 최소점) · 그 장 시작 측점 · 노선 폴리선 표지는 서버가 덧내야 함(B4 3장) → 중. +- K3 — 한 장 두 축척 — 표준 횡단도 본 그림 1/50 + 측구 확대도 1/10 `B07_DesignDetail/B07_DesignDetail_Engine_Cad_Standard.py:10,43-44` · 종단 가로 · 세로 → 장 하나에 계수 하나로는 H13 을 못 풂. +- K4 — 계산 자리(CLAUDE 5장) — 변환값 = 서버가 냄 · 입력 바꿈 · 표시는 브라우저가 즉시(왕복 없음) · 도면 좌표 · 내보내기는 종이 mm 그대로(정본 안 바뀜). + +## 3. 묶음 갱신 · 차례 + +### 3-1. 묶음 표(덧 · 새) + +| 묶음 | 무엇이 생기나(예) | 담는 줄 | 기대 효과 | 크기(어림) | 앞몫 | +|---|---|---|---|---|---| +| ⓪ 흠 막기(덧) | 앞 H1~H12 + 치수 · 재기가 m 로 · 횡단 좌우 판정 뒤 손봄 | H13 · H14 · H15 · N13 | 틀린 숫자가 도면 · 내보내기에 안 남음 | 앞 3~6 일 + H13 1.5 일(+ H14 뒤집으면 1.5 일) | H13 = 축척 값 넘기기와 한 몸 | +| ① 좌표 · 값(덧) | 객체 길이 집기 · 칸 증감 `X3+` | N16 · N17 | 같은 길이 되풀이 | +1 일 | — | +| ② 스냅 · 추적(덧) | 비탈 꼴 네 가지 받기 · 누르는 동안 스냅 끔 · 요소 방향 나침반 · 확대 따라 눈금 | N14 · N18 · N19 · N23 | 횡단 비탈을 키보드로 | +4 일 | N14 · N23 → 1 일(D2 와 같이) | +| ④ 그립(덧) | 비탈 따라 끌기 · 손잡이 위젯 | N15 · N20 | 비탈 끝만 늘림 | +5.5 일 | — | +| ⑤ 편집 손버릇(덧) | 같은 거리 복제 · 큰 걸음 미세 이동 · 고른 것 맞춤 · 섞인 값 식 · 거리 표시 | N21 · N22 · N24 · N25 · N26 | 같은 간격 · 일괄 고침 | +3.5 일 | N21 · N24 → 1 일 | +| ⑥ 명령줄(덧) | 다음 명령 후보 · 숫자 별칭 안내 | N47 · N48 | 순서 되풀이 | +1~2 일 | — | +| ⑦ 자동화 · 그 밖(덧) | 좌표 표 → 점 · 검토 표시층 · 사내 모델 시험 | N49 · N50 · N51 | — | +9.5 일(시험 따로) | 사내 모델 = 정할 것 21 | +| ⑧ 실제 값 입력(새) | 축척 · 원점 · 노선 정보 받기(K1 · K2) → 「No.12+5 좌 3」 · 「오프셋 −2.5 · 표고 123.4」 · 「경사 8 % · 측점 No.15」 · 비탈 비율(② D2 · N14 공용) · `3m` · 커서 곁 실값 · 측점으로 이동 | N01~N12 | 1/100 · 1/200 · 1/1200 손 환산 · 계산기 없앰 | 어림 14 일(+ 고저 편집 표 4 일) | 횡단 = 원점 · 기준 높이 넘김 · N10 · N11 · N08 → 3 일 | +| ⑨ 기기 무관 입력(새) | 숫자판 · 명령 찾기 창(한글) · 고른 것 곁 막대 · 펜 모드 · 확대 창 · 두 손가락 되돌리기 · 펜 단추 · 가장자리 이동 | N27~N35 | 태블릿 · 휴대폰 · 펜에서도 같은 일 · 마우스도 같은 판 | 어림 9.5 일 | N27 · N28 · N35 · N29 → 5 일 | +| ⑩ 임도 표기 도구(새) | 측점 · 표고 · 경사 글 · 측점 눈금 · 길이 합 · 안쪽 눌러 면적 · 숫자 일괄 셈 · 번호 매김 · 번호 늘려 복사 · 표 ↔ 엑셀 · 좌표 글 | N36~N46 | 표기 · 수량 손질을 계산 · 다시 치기 없이 | 어림 15 일 | 문맥 없이 되는 N40 · N42 · N43 · N44 → 4 일 | + +- 크기 셈 = 작 0.5 · 중 1.5 · 큼 4 일(335 와 같음) · ⑧ 에 문맥 넘기기(횡단 0.5 · 종단 · 평면 서버 덧 1.5) 포함. + +### 3-2. 먼저 할 관계(덧) + +- ⓪ H13 → ⑧(축척 값 넘기기를 같이 깖) · ⑩ 글 도구(숫자가 m 여야). +- 정할 것 17(횡단 좌우) · 15(오프셋 부호) → ⑧ 횡단 · ⑩ N37 — 그림 좌우가 바뀌면 입력 부호 · 커서 실값 · 글이 다 따라 바뀜. +- ⑧ 횡단(변환값 있음) → ⑧ 종단(서버가 가로 · 세로 축척 · 기준선 표고 · 시작 측점 덧) → ⑧ 평면(노선 표지 · 시작 측점 · 측점 꼴 해석 N03). +- ⑧ → ⑩ N36~N39 · N46. +- ⑨ 숫자판(N27) → ⑧ 실값 칸 · ④ 그립 숫자(5-9) · ① 값 — 손가락 기기에서 숫자를 넣는 바닥. +- ⑨ 명령 찾기 창(N28 · N35) = ⑥ 3-2 · D9 · D13 과 한 찾기 함수 → ⑥ 앞몫 일부를 ⑨ 로 당김. +- ⑨ 막대(N29) ↔ ⑤ 우클릭 메뉴(D15) = 한 메뉴 틀(정할 것 3 · 23). +- ⓪ H2(보기 전용 문) → ⑨ 막대 · ⑩ 일괄 셈 · 번호 매김 · 번호 늘려 복사(새 고치기 길 모두 문을 지남). +- ② D2(1:n 각) = N14(비탈 꼴) = ⑧ N08(앞 점에서 비율 → 표고) — 비율 해석 함수 하나. +- ⑨ 펜 모드(N30) → 확대 창(N31) · 펜 단추(N33). + +### 3-3. 새 차례 추천(B5 효율 근거로) + +| 차례 | 묶음 · 몫 | 크기(어림) | 까닭(근거) | +|---|---|---|---| +| 1 | ⓪ 흠 막기 + H13(+ H14 판정) | 4.5~7.5 일 | 도면을 몰래 바꾸거나 틀린 숫자를 남기는 것부터 | +| 2 | ⑨ 앞몫 — 숫자판 · 명령 찾기 창(한글) · 고른 것 곁 막대 | 5 일 | 쓰는 명령은 소수(가장 많은 무리 31~40 개 · 최다 ERASE→MOVE→COPY [B5 E1 · E2]) → 어느 기기에서나 두 번 누름 안에 · 있는 빠른 길을 보이게 [E5 · E6] · 기기 무관 원칙 | +| 3 | ③ 고르기 앞몫(335) | 4 일 | 묶어 고르기 · 되풀이 전략이면 실행 시간 약 40 % 줄음 [E4] | +| 4 | ⑧ 횡단 앞몫 — 원점 · 기준 높이 넘김 · 커서 실값 · `3m` · 오프셋 · 표고 점 | 3 일 | 「뭘 치지?」 생각 한 번(1.35 초) = 키 예닐곱 번 [E9] → 1/100 손 환산이 손질마다 그 생각 · 서버 변환값이 있어 작음 · 정할 것 15 · 16 · 17 먼저 | +| 5 | ② 스냅 앞몫(335) + N14 · N23 | 4 일 | 횡단 비탈을 키보드로 · ⑧ 비율 해석과 한 함수 | +| 6 | ⑨ 펜 모드 · 확대 창 · 두 손가락 되돌리기 · 펜 단추 | 3 일 | 작은 표적 손가락 오류 최대 81 % → 확대 · 오프셋 4~14 % [E10] · 펜은 쓰고 손가락은 다룸 [W29] | +| 7 | ④ · ① · ⑤ 앞몫(335) | 10~11 일 | 335 차례 그대로 | +| 8 | ⑩ 앞몫(길이 합 · 숫자 일괄 셈 · 번호 매김 · 번호 늘려 복사) | 4 일 | 문맥 없이 됨 · 수량 · 표기 손질 | +| 9 | ⑧ 종단 · 평면 → ⑩ 글 도구(측점 · 표고 · 경사 · 좌표 글) | 어림 15 일 | 서버 덧(K2) 뒤 | +| 10 | ⑥ · ⑦ · 나머지 몫 | — | 앞몫을 써 본 뒤 · 사내 모델은 시험만(정할 것 21) | + +- 앞 다섯 합 = 어림 21~24 일 · 미리보기 · 스냅 · 끌기는 브라우저 안 한 화면 틀(16ms) 안 · 서버 계산은 [저장] · [확정] 때만 [E14 · E15]. +- 335 차례와 다른 점 — ⑨ 를 ③ 앞으로(기기 무관 원칙 · 효율 근거) · ⑧ 횡단 앞몫을 ② 앞에 끼움 · ⑩ 은 ⑤ 뒤. + +## 4. 정할 것 갱신(335 의 1~14 뒤) + +| # | 무엇 | 지금 | 바깥 · 근거 | 갈래 | 걸리는 묶음 | +|---|---|---|---|---|---| +| 15 | 오프셋 부호(입력 칸) | 엔진 = +좌 / −우(`B05_Profile/B05_Profile_Engine_Sections_Core.py:562`) | Carlson · GEOPAK = 왼쪽 − · TBC = 오른쪽 + · 지식DB 교각 = 좌측각 (+)(`…/2. 임도/4. 노선측량.md:322`) | (가) 엔진 그대로 +좌 (나) 바깥식 좌 − (다) 부호 대신 「좌 3」 · 「우 3」 · L · R 글자 | ⑧ · ⑩ | +| 16 | 측점 입력 · 표시 꼴 | 평면 글 `No.n+d`(20 m) · 제목 「2+0.0」 · 정보 「STA.0+050.000」 셋 섞임 | 지식DB 「No.11+13」 · 「STA NO. 3」(`…/5. 설계.md:157,244`) · 측점 간격 20 m(별표2 `:25`) · Trimble Station index · Civil 3D `+` 없이 숫자 | 입력 = `No.12+5` · `12+5` · `245`(m) 다 받을지 · `2+015` 를 km 꼴(2 015 m)로 볼지 번호 꼴(No.2 + 15 m = 55 m)로 볼지 · 표시 한 꼴 | ⑧ · ⑩ | +| 17 | 횡단 좌우 방향 | B07 = 왼쪽(+)을 종이 오른쪽 · B06 = 화면 왼쪽(H14) | 관례 = 사용자 확인(지식DB 글 없음) | (가) B07 을 B06 처럼 뒤집음 (나) 지금 그대로 + 까닭을 도면 · 문서에 적음 | ⓪ H14 · ⑧ | +| 18 | 치수 · 재기 실값 범위 | 종이 값 그대로(H13) | AutoCAD DIMLFAC = 치수 스타일마다 계수 하나 | 새 치수만 / DIST · AREA · LIST · ID 도 · 종단 가로 · 세로 따로(기운 치수 · 각도는 막음 · 종이 값 표시) · 한 장 두 축척(표준도 1/50 + 1/10)은 칸별 · 치수 글 덮어쓰기 칸(335 6-6 · 6-7)도 같이 넣을지 | ⓪ H13 · ⑧ | +| 19 | 펜 모드 기본 | 펜 = 마우스 길 · 손가락 톡 = 늘 누름 | tldraw · Excalidraw = 펜 첫 사용 때 자동 켬 | (가) 자동 켬 + 끄기 단추 (나) ☰ 설정에서만 (다) 안 넣음 | ⑨ | +| 20 | 한글 명령 찾기 범위(정할 것 11 덧) | 명령 단계 = 영문 글자 · 명령행 한글 못 찾음(추정) | 드림플러스 HCMD = 모르는 명령이면 영문 자판으로 바꿔 다시 | 한글 이름 · 초성 · 자판 그대로 중 어디까지 · 찾기 창만 vs 캔버스 · 명령행도 · 자판 바꿔 바로 실행 vs 후보만 보임 · 내 별칭 한글 허용 | ⑨ · ⑥ | +| 21 | 사내 작은 AI 모델 시험 | 없음 | 상용 CAD AI = 바깥 서버(못 씀) | (가) 시험 안 함 (나) 사내 서버에 작은 모델 시험(한국어 정확도 · GPU · 라이선스 원문 · [확인] 단계) | ⑦ | +| 22 | 화면 숫자판 꼴 · 띄울 때 | 없음 · 손가락 단추 6 | AutoCAD 모바일 = 값 누르면 숫자판 · 웹 `inputmode` = 기기 자판(기기마다 꼴 · 높이 다름) | (가) 앱 안 단추판 (나) 기기 자판 (다) 둘 다 · 손가락 기기만 / 늘 / 단추로 · 판의 키(숫자 · 소수점 · `-` · `@` · `<` · `:` · `m` · 지움 · Enter) | ⑨ | +| 23 | 고른 것 곁 막대 ↔ 우클릭 메뉴(정할 것 3 덧) | 둘 다 없음 | Miro 막대 · BricsCAD Quad · Fusion 표식 메뉴 | 한 틀 · 같은 명령 목록(고른 채 = 막대 · 우클릭 = 메뉴) · 막대에 둘 명령(지움 · 옮김 · 복사 · 간격띄우기 · 자르기 · 특성 등 — 근거 E1 · E2) | ⑨ · ⑤ | + +- 먼저 정할 것(새) = 17 · 15 · 16 · 18(⓪ H13 · ⑧ 앞몫) → 22 · 23 · 19 · 20(⑨ 앞몫) → 21(시험 여부). + +## 5. 근거 약함 · 확인 못 함 · 출처 + +### 5-1. 근거 약함 — 안티그래비티 「못 찾음」 12(일반 아이디어로만) + +| A | 이름(재료) | 판정 · 어디로 | +|---|---|---| +| A3 | DSLO 사면 자동 작도 | 기능 쪽 못 찾음 · 비탈은 B06 설계가 그림 → 손질은 N08 · N14 | +| A4 | TALUS 법면 빗금 | 기능 쪽 못 찾음 · 평면 비탈 눈금은 서버 자동 · 횡단 띠 해치 = 335 8-7 | +| A5 | SLP 경사 기호 | 못 찾음 → 공식 근거 있는 N37 · N38 로 | +| A8 | AR · DLA 면적 글 | 못 찾음 → N41(Archicad 공식)로 | +| A17 | CADian 한글 자연어 명령 | 못 찾음 → N35(규칙 기반 한글 찾기)로 | +| A21 | 가로 · 세로 다른 축척 종단 틀 | 못 찾음 · 우리 서버 자동 | +| A22 | INFRACAD 측점 + 오프셋 | 제품 · 출처 없음 → N01(Civil 3D · OpenRoads · TBC 공식) | +| A23 | 방위각 + 거리 | 못 찾음 → N12(Civil 3D 공식) | +| A24 | Slope Lock | 못 찾음 → N14 · 335 D2 | +| A25 | 구배율 입력 | 못 찾음 → N07(Civil 3D 공식) | +| A26 | VCL · R 종단곡선 | 못 찾음 · 종단곡선은 B05 설계 몫 | +| A28 | 소단 자동 전개 | 못 찾음 · 소단은 B06 설계 몫 | + +- 코덱스 3차 머리 줄 = 「확인 2 · 일부 13 · 못 찾음 13」 · 표 줄을 세면 확인 2(A1 · A19) · 일부 14 · 못 찾음 12 → 줄별 판정을 따름. +- 「일부」를 바른 내용으로 고친 것 — A6(일반 EL 명령 없음 · FE · GEL 있음) · A7(결과 = 명령줄) · A10(CTC → CALC 문자 기능) · A11(ETC = 엑셀 → 캐드 · CTE = 캐드 → 엑셀) · A12(사용자 LISP 사례) · A13(`X(N) Y(E)` 꼴 못 찾음) · A16(HCMD = 모르는 명령이면 영문으로 다시 · 자모 가상 매핑 아님 · CADian 근거 없음) · A18(몸짓마다 명령 고정 배정 못 찾음) · A20(GstarCAD Architecture 근거) · A2 · A9 · A14 · A15 · A27(세부 순서 못 찾음). + +### 5-2. 재료 속 도메인 숫자 → 지식DB 로 바꿈 + +| 재료 숫자 | 지식DB | 인용 | +|---|---|---| +| 「산지관리법상 임도 최대 종단경사 13~18 %」(안티그래비티) | 법이 다름 · 간선임도 종단기울기(순기울기) = 설계속도 40 · 30 · 20 km/h 에 일반지형 7 · 8 · 9 % 이하 · 특수지형 10 · 12 · 14 % 이하 · 노면포장 때만 18 % 범위 조정 · 작업임도 = 최대 20 % 범위 | 별표2 `:168-177` · `:665` | +| 「5 m 높이마다 소단 · 소단폭 1.0 m · 배수 역경사 4 %」 | 붕괴 우려 지역 = 사면길이 2~3 m 마다 폭 50~100 cm 소단 · 토사 절토는 높이에 따라 소단 · 성토사면 길이 5 m 이내(넘으면 옹벽 · 석축) · 역경사 4 % = 지식DB 확인 몫 | 별표2 `:274` · `:258` · `:267` | +| 「1:1.5 · 1:1.2」 비탈 | 절토 경암 1:0.3~0.8 · 연암 1:0.5~1.2 · 토사 1:0.8~1.5 · 성토 1:1.2~2.0 | 별표2 `:258` · `:265` | +| 「VCL=40 · R=300」 | 종단곡선 반경 · 길이 = 40 km/h 450 · 40 m 이상 · 30 km/h 250 · 30 · 20 km/h 100 · 20 · 비포장 대수차 5 % 이하면 안 씀 · 포물선 가능 | 별표2 `:187-197` | +| 「20 m 정측점」 | 측점 간격 20 m · 필요 지점 보조말뚝 | 별표2 `:25` | +| 그 밖(「횡단 폭 좌우 15 m」 등) | 입력 기능 예시일 뿐 · 쓰지 않음 | — | + +- 이 숫자들은 근거 · 후보일 뿐 — 극 추적 비율 목록 등 기본값은 정할 것 4 대로 사용자 협의. + +### 5-3. 확인 못 함 + +- 화면 실측 안 함 — H13 화면 숫자 · H14 화면 좌우 · H15 아래 띠 · B5 의 우리 쪽 추정(펜 옆 단추 = Enter · 펜 띄움 스냅 표시 · B07 명령행 칸 기기 자판 · iPad 한글 받개 초점). +- B07 손질에서 실제로 무엇을 자주 고치는지 자료 없음 → 쓸모 = 도면 종류별 짐작(335 와 같음). +- 국내 임도 전용 · 국내 도로 설계 상용 프로그램 입력 = 못 찾음(B4) · 국산 CAD 국내 문서는 대부분 홈페이지 주소뿐(안티그래비티). +- 코덱스 — progeCAD · TurboCAD · CorelCAD 근거 못 찾음 · Figma 값 칸 끌기는 코덱스 「확인 못 함」 · B5 가 Figma 도움말로 확인(B5 판정 따름). +- B4 · B5 의 「확인 못 함」 그대로(부록 원문 5 · 6) · B5 숫자 크기(표식 메뉴 3.5 배 = 2 명 · CAD 40 % = 한 사용자 모형)는 방향만. +- H13 의 내보내기 숫자 · H14 를 뒤집을 때 크기 · 모든 크기 = 코드 읽기 어림(시험 포함 · 화면 확인 별도). + +### 5-4. 출처 + +- 원문 · 전체 출처 목록 = `2026-10-15_335_부록3_바깥조사원문.md`(B4 [R1~R35] · B5 [W1~W46] [E1~E20] · 코덱스 표 링크 · 코덱스 3차 바른 URL). +- 이 글에서 B4 · B5 밖에서 쓴 출처: + - Vectorworks Snap Loupe · Smart Options — https://app-help.vectorworks.net/2027/eng/VW2027_Guide/Start/Streamline_drawing_interactions.htm + - Archicad Magic Wand — https://help.graphisoft.com/AC/28/INT/_AC28_Help/030_Interaction/030_Interaction-53.htm · Tracker 증감 — https://help.graphisoft.com/AC/26/INT/_AC26_Help/030_Interaction/030_Interaction-41.htm + - ARES QuickModify — https://onlinehelp.graebert.com/EN/ARES_Content/25/additional/hlpid_edit_quickmodify.htm + - BricsCAD Manipulator · 적응형 스냅 — https://help.bricsys.com/en-us/document/bricscad/modifying-entities/the-manipulator?version=V26 + - Rhino Gumball — https://docs.mcneel.com/rhino/8/help/en-us/user_interface/keyboard%20modifiers.htm + - ZWCAD Smart Voice — https://www.zwsoft.com/user-guide/zwcad-smart-features + - 그 밖 코덱스 줄(Quad · Onshape · Shapr3D · ARES A3 · SmartTrack 등) = 부록 원문 1 · 2 표 링크 + - 드림플러스 DSTA — https://dreamcad.net/bbs/board.php?bo_table=qna&wr_id=16553 · STT — https://www.dreamcad.net/bbs/board.php?bo_table=qna&wr_id=22368 · DSTA · GST — https://www.dreamcad.net/bbs/board.php?bo_table=qna&wr_id=12839 · FE · GEL — https://dreamcad.net/bbs/board.php?bo_table=qna&wr_id=13168 · CALC 문자 — https://www.dreamcad.net/bbs/board.php?bo_table=notice&wr_id=80 · ETC · CTE — https://dreamcad.net/bbs/board.php?bo_table=qna&wr_id=30570 · XY — https://dreamcad.net/bbs/board.php?bo_table=qna&wr_id=15273 · CIM — https://dreamcad.net/bbs/board.php?bo_table=qna&wr_id=14564 · HCMD — https://dreamcad.net/bbs/board.php?bo_table=qna&page=197&wr_id=17336 + - Lee Mac Total Length — https://lee-mac.com/totallengthandarea.html + - AutoCAD TCOUNT(한국어) — https://help.autodesk.com/cloudhelp/2025/KOR/AutoCAD-Core/files/GUID-4B92219C-DA94-4D71-B308-D3818D3C6B8F.htm + - GstarCAD Architecture 증분 복사 — https://cdn-sg-gw.gstarcad.net/gstarsoft_pdf/GstarCAD-Architecture-2021-User-Guide.pdf + - 고캐드 11 · 22 · 33 LISP 사례 = 코덱스 3차 12 줄 링크(부록 원문 4) +- 지식DB — 별표2(5-2 · `:25,69,168-177,187-197,258,265,267,274,665`) · `resources/knowledge/original/산림과임업기술(임도)/2. 임도/5. 설계.md:157,238,244` · `…/2. 임도/4. 노선측량.md:322`. +- 코드(읽기만) — 2 장 표 · K1~K3 의 줄. From 546db1ab1108a0128cf7c2e2661b486264a12af0 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 15 Oct 2026 19:22:57 +0900 Subject: [PATCH 46/81] =?UTF-8?q?chore(=EC=A1=B0=EC=82=AC):=20336=20?= =?UTF-8?q?=EC=99=B8=EB=B6=80=20=EC=9D=BC=EC=9C=84=EB=8C=80=EA=B0=80=20?= =?UTF-8?q?=EB=A7=9E=EC=B6=A4=20=EC=9E=AC=EA=B2=80=ED=86=A0=20=E2=80=94=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=A1=B0=ED=95=A9=20=EA=B8=B0=EC=A4=80=20?= =?UTF-8?q?=C2=B7=20=EC=9B=90=EB=B3=B8=20=EB=A1=9C=EC=A7=81=20=EC=95=88=20?= =?UTF-8?q?=EA=B3=A0=EC=B9=A8=20=C2=B7=201=EC=88=9C=EC=9C=84=2071=20?= =?UTF-8?q?=E2=86=92=2093%=20=C2=B7=20=EC=A1=B0=ED=95=A9=20=EB=A7=9E?= =?UTF-8?q?=EC=9D=8C=2010=20=E2=86=92=2076%=20=C2=B7=20=EB=AC=B6=EC=9D=8C?= =?UTF-8?q?=20=EC=97=AC=EC=84=AF=20=C2=B7=20=EC=A0=95=ED=95=A0=20=EA=B2=83?= =?UTF-8?q?=20=EB=8B=A4=EC=8B=9C=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01C6BNBFXFndbqYxu6sZuknK --- .../조사/2026-10-15_336_부록3_재실측원문.md | 806 ++++++++++++++++++ .../2026-10-15_336_재검토_로직조합맞춤.md | 313 +++++++ 2 files changed, 1119 insertions(+) create mode 100644 docs/raw/조사/2026-10-15_336_부록3_재실측원문.md create mode 100644 docs/raw/조사/2026-10-15_336_재검토_로직조합맞춤.md diff --git a/docs/raw/조사/2026-10-15_336_부록3_재실측원문.md b/docs/raw/조사/2026-10-15_336_부록3_재실측원문.md new file mode 100644 index 000000000..27c29da7e --- /dev/null +++ b/docs/raw/조사/2026-10-15_336_부록3_재실측원문.md @@ -0,0 +1,806 @@ +# 부록3 — 재실측 원문 (요구 226-4 · PLAN 336-3) + +본문 = 같은 폴더 `2026-10-15_336_재검토_로직조합맞춤.md` · 아래 세 글은 C4 · C5 · C6 결과 글을 손대지 않고 차례대로 이어 붙임(머리만 닮). + + +--- + + +**부록 A. C4 — 로직 지문 맞춤 실측 원문** (원문 `C4_로직지문.md`) + +# C4 로직 지문 맞춤 실측 (요구 226-4) + +바깥 단가산출 블록의 「로직 속」(직종 · 품 · 장비 · 자재 · 할증 · 단위 · 조건 · 조항)을 뽑아 우리 돈 로직 1,438 의 같은 꼴 지문과 견준 맞춤 흉내. 금액(원 · 단가 · 금액 비)은 순위 · 판정에 안 씀. 저장소 안 고침(읽기 · 스크래치패드 흉내만). +기준 = main_laptop_1 b7e727863 · 정답 · 셈 = 225 C1 · C3 와 같음(북부청 임도 · 사방 · 동부청 영월 · 고유 136 줄 · 정답 있음 126 · 기억 없음 · 씨앗 켬이 기본). 지금 · 묶음 ① 수치는 C3 스크립트를 다시 돌려 같음 확인(71.4 / 81.7 · 85.7 / 88.1). + +## 0. 결론 + +1. **로직 속만으로 맞춤이 됨** — 지문만(조항 · 이름은 덤) 1순위 엄격 81.7% · 다섯 안 93.7%(지금 71.4 · 86.5). 북부청 두 파일(92 줄)만 보면 지문만 91.3% 로 묶음 ①(89.1%)보다 높음. 영월은 씨앗(사용자가 전에 확정한 실무 이름) 줄이 많아 55.9%(묶음 ① 씨앗 끔과 같음). +2. **추천 = 「같음은 그대로 · 나머지는 지문」** — 묶음 ① 이 「같음」으로 낸 22 줄(씨앗 · 이름 같고 조항 검사 통과)은 그대로 두고, 나머지는 지문 점수에 묶음 ① 차례 덤을 더해 줄 세움 → 1순위 88.9 / 90.5 · 다섯 안 96.8(묶음 ① 85.7 / 88.1 · 94.4). 처음 보는 파일(씨앗 끔) 85.7 / 87.3 · 96.8(묶음 ① 씨앗 끔 80.2 / 84.1 · 92.9). 미리 정한 단순 섞기(β=2)만 해도 87.3 / 89.7 · 96.0. +3. **「딱 맞음」 신호 — 금액 없이 확실함 표시** — 1위 로직의 직종 묶음이 같고 품이 모두 3% 안 · 2위와 점수 차 큼 = 고유 57 줄(엑셀 줄 220 / 504 = 44%) · 정답 있는 56 줄 모두 맞음(100%). C3 금액 ±5% 표시(21 줄 중 20)보다 넓고 정확 → 초록 표시 · [모두 확정] 후보. +4. **로직 조합이 보임** — 블록을 절(1. 현장가공 · 2. 조립)과 작은 절(가. 굴착기 · 나. 인력 10%)로 갈라 따로 맞추면 복합 정답 8 줄 중 7 이 조합 그대로(철근가공조립 = 현장가공 + 현장조립 6/6 · 도장 = 바탕 + 녹막이 + 유성) · 섞음 줄의 인력 몫이 따로 붙음(고임돌 9:1 → 모래·자갈·약돌 채집). 새 지표 「조합 맞음」 지금 76.2 → 묶음 ① 86.5 → 추천 88.9. +5. **사용자 말 맞음 — 작성자가 편의대로 바꿈** — 정답 있는 126 줄 중 이름 다름 116(92%) · 이름만 다르고 조항 · 품 같음 40 · 묶음(우리 로직에 없는 장비 · 일 더함) 40 · 계수(품) 바꿈 32 · 섞음(기계 % + 인력 %) 18 · 다른 책 조항 18 · 단위 바꿈 12 · 출처 없음 11 · 옛 판 번호 5 · 할증 · 횟수 붙임 4 · 직종 바꿈 3 · 쪼갬 2. 직종 묶음은 거의 안 바꿈(3 줄) → 직종 · 품 꼴이 이름 · 조항보다 단단한 지문. +6. **책 갈래 11 줄은 작성자 잘못이 아님** — 품 숫자가 모두 엑셀이 인용한 책과 같음(건설 7 · 산림 1 · 두 책 같은 품 2 · 장비만 건설 1). 틀린 쪽은 우리 「이름 같음」 자동 대상. 반대로 「조항은 건설 · 숫자는 산림」은 9 줄(고임돌 · 조약돌 4 · 야면석 채집 3 · 되메우기 1 · 야면석메붙임 1) → 조항은 한 신호로만. +7. **「로직 X · 계수 바뀜」 판정이 됨** — 지문 1위가 맞은 103 줄 중 품 같음 56 · 조합 같음 4 · 계수 바뀜 23 · 섞음 · 할증 붙임 3 · 직종 다름 3 · 단위 달라 크기 못 견줌 8 · 그 밖 6. 26 줄은 「로직은 맞고 다른 점만」 글로 보임 — 예 「GC000083 굴착(인력/토사) · 계수 바뀜 특별인부 0.370 → 0.354」. +8. **덤 — 입력값 추천** — 품이 같게 나오는 입력 조합 = 엑셀이 쓴 조건. 품 같은 65 줄 중 지금 규격 읽기로 같은 품 48 · 지문 변형으로만 맞는 17(합판 「유형=보통」은 규격엔 없고 블록 글 숫자에만). +9. **남는 것** — 지문 · 묶음 ① 둘 다 틀린 11 줄(고임돌 · 조약돌 6 · 소운반 3 · 측구수로관 · 철골 공장) = 기억(C6) 몫 · 보통인부 하나뿐 줄은 품 숫자로 못 가름 · 「우리에 없음」 줄에도 늘 후보가 뜸(문턱 무익). +10. **원본 로직 안 고침** — 우리 쪽은 M01 엔진으로 읽어 셈만. 고칠 곳 = M02 맞춤(새 `M02_Hopyo_Finger.py` + `Basis.read` · `Match.match_line` · `guess` 몇 줄). 우리 지문 미리 셈 ≈ 2 분 → 서버 뒤에서 · 마스터가 바뀔 때만. + +--- + +## 1. 지문 만드는 법 + +### 1-1. 바깥(엑셀) 지문 — 고유 136 줄 + +| 칸 | 뽑는 법 | 있는 줄 | +|---|---|---| +| 직종 · 품 | 「직종 : 단가 × 식 = 금액」에서 단가를 떼고 남은 식을 셈(273,540 × 3인 ÷ 55㎥ × 2 → 0.109 인/㎥) · 변수(P · Q) 든 식은 바꿔 쓴 식(= 215,907×4×0.026…)으로 · 금액 ÷ 단가 역산은 0 줄 | 118 | +| 밑품(율 뺀 품) | 「× 10%」 · 「0.75(품할증)」 · 「× 2회」 · 이름 · 절 머리의 9:1 · (90%)와 같은 맨 인자를 율로 가름 — 0.3인/㎥ × 10% → 밑품 0.3 | 율 든 직종 · 장비 | +| 장비 | 절 머리 · 줄의 장비 낱말(굴착기 · 덤프 · 크레인 · 브레이커 · 펌프차 · 집게 …) + 규격 수(0.6㎥ · 4.5톤) + 시간(「노무비 : 단가 ÷ Q × 90%」에서 단가 뗀 식) | 72 | +| 자재 | 「이름 단가 × 수량」 · 「: 별도계상」 | 50 | +| 율 · 계수 | (노무비의 2%) 꼴 56 · q · K · f · E · Cm · L · V · T 42 | — | +| 조건 낱말 | 토질 · 메/찰 · 원형/사각 · 무근/철근 · 난이 · 규모 · 방법 · N회 · N개월 · Type | 98 | +| 조항 | C1 `Basis.sources` 그대로 · 옛 책 이름(임도품셈 · 사방품셈) · 2020 이하 글은 「옛판」 | 119 | +| 절 | 1. · 가. · 1) · ① 마다 직종 · 장비를 갈라 둠 | 둘 이상 72 | + +- 영월(STmate) = 코드 칸(L · X · M · S) + QTY 칸이 곧 품 · 시간 · 수량 → 노무비 · 중기 · 재료비 · 경비 목록표로 이름을 이음. 직종 든 줄이 C1 흉내 5 → 33(36 중). +- 못 뽑는 줄 = 노임 줄 2(철골공 · 특별인부 [인]). +- 쉬운 예 — 합판거푸집(4회) N001: 형틀목공 5인 ÷ 45 = 0.111 · 보통인부 0.044 · 합판 1.03㎡ × 38% · 각재 0.038㎥ × 38% · 소모자재(주자재비의 9%) · 공구손료(노무비의 1%) · 조건 「4회 · 보통」 · 조항 건설 6-3-1. + +### 1-2. 우리 로직 지문 — 1,438 + +| 칸 | 만드는 법 | +|---|---| +| 정적 | 호표 인력(LB) → 직종 · 「로직(기계경비 …)」 부름 → 장비 갈래 · 규격(EQ) · 재료 이름 · 덧줄 %(식 「* N / 100」) · 조항(`Basis.ours`) · 이름 · 단위 · 이름과 고르기 글의 조건 낱말 | +| 동적 | M01 엔진(`mf.run` — `Labor_Doc.run_line` 과 같은 길)으로 입력 조합마다 단위당 품(인) · 장비 시간 · 재료 수량(결과단위 「원/100㎡」면 ÷100) | +| 입력 조합 | 자동값 + 고르기 입력(지역 · 조종원직종 · 보정작업 · 기종 목록 뺌) — 곱이 64 이하면 모두 · 넘으면 하나씩 바꿈 | +| 섞음 | 인력비율 · 기계비율 중간값 든 로직 23(산림 9 장 터파기 · 되메우기 …)은 「섞기 전 품」 변형 덧붙임 | +| 기계경비 로직 | 고를 수 있는 기종의 갈래 · 규격 = 지문(조종원 줄 뺌) | + +| 덮는 범위 | 수 | +|---|---| +| 엔진으로 셈 | 1,407 / 1,438 (97.8%) | +| 품 든 지문 | 1,328 — 품 모름 110(기본값으로 못 돎 31 · 품 입력이 비어 0 인 79, 예 떼붙임 「품」 칸) | +| 변형(입력 조합) | 엔진 6,767(맞춤용 6,773 = 섞기 전 · 정적 더함) · 변형 2 개 이상인 로직 775 | +| 인력 · 장비 · 재료 든 로직 | 1,347 · 512 · 275 | +| 셈 시간 | 지문 한 번 ≈ 110 초(노트북) · 맞춤 136 줄 ≈ 7 초 | + +- 쉬운 예 — GC000211 합판거푸집(건설 6-3-1) 「유형=보통」 = 형틀목공 0.112 · 보통인부 0.045(엑셀과 1% 차) · GF000160 합판거푸집(산림 12-4)은 가장 닮은 조합도 0.104 · 0.057 → 숫자로 건설. +- 곁 발견 — GC000211 자동값 「높이감률 = 1」이라 시공량이 1% 깎여 나옴(엑셀 0.111 ↔ 우리 0.112). 맞춤엔 지장 없음(3% 안) · 원본 확인은 사용자 몫. + +### 1-3. 닮음 셈 — 금액 안 씀 · 무게는 미리 정함 + +점수 = 3.0 인력 + 2.0 장비 + 1.0 자재 + 0.3 율 + 0.5 단위 + 0.6 조건 + 1.5 조항 + 0.6 이름 (로직마다 가장 닮은 변형으로) + +- 인력 = 0.35 직종 겹침(보통인부 반 무게) + 0.25 꼴(코사인 · 품 · 밑품 중 큰 쪽) + 0.40 크기(직종마다 3% 안 = 1 · 멀수록 0) — 단위가 다르면 크기 덤 없음 · 단위 별칭(개소 = 개 = 본 · hr = 시간 · 공/㎥ = 공㎥). +- 장비 = 갈래 겹침 0.6 + 규격 0.2 + 시간 0.2 · 우리에만 있는 갈래는 깎음. +- 자재 = 엑셀 자재 낱말이 우리 재료에 있는 비율 · 율 = 같은 % 하나라도. +- 조건 = 엑셀 조건 낱말이 우리 이름 · 고르기 글에 있는 비율 − 이름 축 어긋남(토질 · 메찰 · 형만 — C3 #8 대로 인력/기계는 안 깎음). +- 조항 = C1 윗마디 셈(같음 1 · 윗마디 0.9 · 앞 마디 0.5) · 옛판이면 반 · 이름 = 정리 이름 글자 두 개 묶음 닮음(약한 덤). +- 엑셀엔 없는데 우리에만 인력 · 장비 → −0.6 · −0.4. +- 딱 맞음 = 1위의 직종 묶음 같고 품 모두 3% 안(보통인부 하나뿐 줄 뺌) 또는 장비 갈래 · 시간 맞음 · 2위와 0.3 넘는 차. +- 섞기 = 지문 점수 + β × 묶음 ① 차례 덤(1 · 0.6 · 0.45 · 0.35 · 0.25) · 조합 = 절 · 작은 절마다 직종 · 장비만 떼어 같은 셈 → 절 1위 묶음. + +--- + +## 2. 수치 표 + +### 2-1. 지표 — 세 파일 고유 126 줄(칸 = 엄격 / 느슨 %) + +| 방법 | 1순위 | 다섯 안 | 임도 1위 | 사방 1위 | 영월 1위 | 엑셀 줄 무게 1위 느슨 | 우리에 없음 줄 후보 | +|---|---|---|---|---|---|---|---| +| 지금 | 71.4 / 81.7 | 86.5 / 94.4 | 71.4 | 74.4 | 67.6 | 78.1 | 5 | +| 지금(씨앗 끔) | 67.5 / 77.0 | 86.5 / 93.7 | 75.5 | 76.7 | 44.1 | 75.8 | 5 | +| 묶음 ① | 85.7 / 88.1 | 94.4 / 94.4 | 91.8 | 86.0 | 76.5 | 85.6 | 5 | +| 묶음 ①(씨앗 끔) | 80.2 / 84.1 | 92.9 / 93.7 | 91.8 | 86.0 | 55.9 | 83.9 | 5 | +| **지문만** | **81.7 / 82.5** | **93.7 / 93.7** | 89.8 | **93.0** | 55.9 | 83.0 | 10 | +| 지문 − 조항 | 65.9 / 77.8 | 88.1 / 89.7 | 79.6 | 65.1 | 47.1 | 77.9 | 10 | +| 지문 − 이름 | 74.6 / 75.4 | 86.5 / 87.3 | 85.7 | 81.4 | 50.0 | 75.4 | 10 | +| 속만(조항 · 이름 다 뺌) | 53.2 / 65.1 | 80.2 / 82.5 | 67.3 | 48.8 | 38.2 | 65.8 | 10 | +| 지문 − 품 크기(갈래만) | 78.6 / 79.4 | 90.5 / 90.5 | 89.8 | 88.4 | 50.0 | 79.4 | 10 | +| 지문 + 묶음 ①(β=1) | 88.1 / 89.7 | 96.8 / 96.8 | 91.8 | 86.0 | 85.3 | 89.4 | 10 | +| 지문 + 묶음 ①(β=2 · 미리 정함) | 87.3 / 89.7 | 96.0 / 96.0 | 91.8 | 86.0 | 82.4 | 89.4 | 10 | +| 같음은 묶음 ① · 나머지 지문만 | 88.1 / 88.9 | 93.7 / 93.7 | 89.8 | 93.0 | 79.4 | 87.3 | 10 | +| **추천: 같음은 묶음 ① · 나머지 지문 + 묶음 ①(β=1)** | **88.9 / 90.5** | **96.8 / 96.8** | 91.8 | 86.0 | **88.2** | **90.7** | 10 | +| 추천(씨앗 끔) | 85.7 / 87.3 | 96.8 / 96.8 | 91.8 | 86.0 | 76.5 | 88.5 | 10 | +| 지문 + 묶음 ①(씨앗 끔 β=2) | 84.9 / 87.3 | 96.0 / 96.0 | 91.8 | 86.0 | 73.5 | 88.5 | 10 | + +- 북부청 두 파일(92 줄) 1순위 엄격 — 지금 72.8 · 묶음 ① 89.1 · 지문만 91.3 · 추천 89.1. 사방(처음 보는 꼴 많음)은 지문만이 가장 좋음(93.0 · 다섯 안 100). +- 신호 몫 — 조항을 빼면 −15.8 · 이름을 빼면 −7.1 · 품 크기를 빼면 −3.1 · 속만(조항 · 이름 다 뺌)으로도 1순위 53% · 다섯 안 80%. +- 지문만과 묶음 ① 둘 중 하나라도 맞음 115 / 126(91.3%) · 둘 다 틀림 11 → 추천이 둘의 장점을 대부분 모음(112 / 126). +- 주의 — β 와 「같음은 그대로」 규칙은 표본을 보고 고름(한 줄 = 0.8pt) · 미리 정한 β=2 가 보수 값. + +### 2-2. 지금 실패 36 줄 — 갈래별 1순위 엄격으로 풀린 줄 + +| 갈래(C3) | 지금 실패 | 묶음 ① | 지문만 | 추천 | +|---|---|---|---|---| +| 책 갈래(산림 ↔ 건설) | 11 | 10 | 11 | 11 | +| 같은 뜻 다른 말 · 현장말 | 11 | 0 | 1(철골설치) | 1 | +| 우리 로직이 더 잘게 | 5 | 5 | 5 | 5 | +| 그 밖(접힘 · 이름 함정) | 5 | 3 | 5 | 5 | +| 장비 · 노임 이름 줄 | 2 | 0 | 2(펌프차 · 크레인 → 기계경비) | 1 | +| 괄호 속 규격 | 1 | 0 | 1(터파기 토사 9:1) | 0 | +| 엑셀 복합 호표 | 1 | 0 | 0 | 0 | +| **합(36 중)** | — | **18** | **25** | **23** | +| 지금 맞던 줄이 새로 틀림 | — | 0 | 12(영월 11 — 씨앗 8 · 짐작 정답 2 · 큰돌채집 1 · 도장 1) | 1(영월 큰돌채집) | + +- 현장말 중 고임돌 · 조약돌 6 줄 — 추천에서 1 줄(영월 Y015) 풂 · 나머지 5 줄은 정답(인력 몫 GF000211)이 2~4 위 · 1위는 숫자가 더 닮은 9:1 터파기 로직(4 절). + +### 2-3. 로직 조합 — 절마다 맞춤 + +| 엑셀 줄 | 절 1위 묶음(지문) | 정답 조합 | 판정 | +|---|---|---|---| +| 철근가공조립(현장) ×6 | 현장가공 GC000206(0.67 · 0.22) + 현장조립 GC000207(3.53 · 1.18) | 같음 | 6/6 맞음 | +| 도장(철재면 2회) | 바탕 GC000705 + 녹막이 GC000714 + 유성 GC000712 | 같음 | 맞음(줄 1위는 「걸레받이용 페인트칠」) | +| 철골가공조립(공장) | 엉뚱(GC000904) + 공장용접 GC000560 | GC000559 + GC000560 | 틀림 — 엑셀 「철골공」 ↔ 우리 「철골 가공조립공」 | +| 비탈면 보호공(평떼) | 건설 잔디붙임 GC000143 + 산림 줄떼 떼꽂이 GF000035 | (정답표 GC000143 주) | 두 책 로직을 섞어 묶은 꼴 | +| 고임돌채집및운반(사방 9:1) | 굴착기 채집 → 터파기(기계) GC000088 · 인력 10% → GF000211 · 적사 → GC000088 · 운반 → 기계경비 | 정답 GF000211 = 인력 몫 | 몫 맞음 | +| 야면석 채집 운반(영월 8:2) | 인력 20% → GF000214(뒷길이 35 · 0.17) + 굴착기 · 덤프 | 정답 GF000214 | 몫 맞음 | + +- 「조합 맞음」 지표(복합 8 줄 = 절 1위 묶음 ⊇ 정답 조합 · 나머지 = 1순위 엄격) — 지금 76.2 · 묶음 ① 86.5 · 지문만 82.5 · 추천 88.9. +- 절 둘 이상인 정답 줄 71 중 정답 로직이 절 1위 묶음 안에 있음 59. +- 장비만 있는 절(상차 · 운반)은 굴착기 · 덤프 든 우리 로직이 많아(512) 엉뚱하게 붙기 쉬움 — 「운반 = 덤프운반 GF000284」 정도만 믿을 만함(C5 몫). + +### 2-4. 딱 맞음 + +| | 줄 | +|---|---| +| 딱 맞음 고유 | 57 (엑셀 줄 220 / 504 = 44%) | +| 정답 있음 → 1위 맞음 | 56 / 56 | +| 우리에 없음 줄 | 1 — 영월 잔토처리(B/H 100%) = 우리 토사깍기(기계 · 굴착기 0.7)와 숫자 같음(닫힌 세계로 보면 그 로직) | +| 그 57 중 지금 1위 맞음 | 43 · 묶음 ① 55 | +| 뺀 꼴 | 보통인부 하나뿐 줄(우연 같음 — 콘크리트소운반 → 모르타르 배합) | + +--- + +## 3. 「편의대로 바꿈」 꼴 — 사용자 말 확인 + +### 3-1. 꼴 표 — 정답 있는 126 줄(한 줄에 여럿 · 정답 로직의 가장 닮은 변형과 견줌) + +| 꼴 | 줄 | 뜻 · 예 | +|---|---|---| +| 이름 다름 | 116 | 거의 전부 — 「레미콘타설(장비사용)」 ↔ 「레디믹스트콘크리트 타설」 | +| 이름만 다름 | 40 | 조항 같음 · 품 3% 안 · 섞음 · 묶음 없음 — 합판거푸집 4회 = GC000211 「유형=보통」 | +| 인용 조항만 다름 | 3 (+ 출처 없음 · 품 같음 6) | 품 같은데 조항이 다른 책 · 번호 — 영월 되메우기 「건설 3-1-3」 = 산림 9-14 섞기 전 0.1 | +| 계수(품) 바꿈 | 32 | 같은 직종 · 품이 3% 넘게 다름 — 세 토질 평균 · 옛 판 값 · 두 작업을 한 품으로 | +| 묶음 | 40 | 우리 정답 로직에 없는 장비 · 일을 더함(채집 + 상차 + 운반) · 복합 8 | +| 쪼갬 | 2 | 우리 한 로직을 두 줄로 — 원형돌망태 조립설치(S079) + 채움(S080) = GF000328(기계 시공) · 단위도 ㎡ → 조(0.45㎡) | +| 섞음(기계 % + 인력 %) | 18 | 9:1 · 8:2 · (인력100%) — 고임돌 인력 0.3 × 10% | +| 할증 · 횟수 · 보정 붙임 | 4 | 품할증 0.75 · 보정계수 1.36 · × 2회 | +| 다른 책 조항 | 18 | 고임돌 「건설 1-10」(단위중량 인용) · 야면석 「건설 8-2」(2016 삭제) | +| 옛 판 · 없는 번호 | 5 | 「임도품셈 5-5」 · 「사방품셈 P52」 | +| 출처 없음 | 11 | 철골설치 · 사방 9:1 채집 | +| 단위 바꿈 | 12 | 개소 ↔ 본 · ㎡ ↔ 개 · 조 · ㎡ ↔ ㎥ | +| 직종 바꿈 | 3 | 다웰바(철근공 더함) · 펌프카(편성 다름) · 터파기 9:1(특별인부 더함) | + +- 정답 로직 대 엑셀 품 판정(126) — 같음 58 · 조합 같음 5 · 섞음 · 할증 붙임(밑품 같음) 9 · 계수 바뀜 29 · 조합 계수 바뀜 3 · 단위 달라 못 견줌 12 · 직종 다름 3 · 장비 · 기계경비 4 · 그 밖 3. +- 읽는 법 — 이름은 거의 다 바꾸고(92%) 조항도 넷에 하나꼴로 다른데, 직종 묶음을 바꾼 줄은 3 뿐 · 품을 그대로 둔 줄이 절반 넘음(같음 · 밑품 같음 72) → 「어떤 로직인가」는 직종 · 품 꼴이 가장 잘 말함. + +### 3-2. 책 갈래 11 줄 — 품 숫자는 어느 책 + +| 줄 | 엑셀 인용 | 엑셀 품 | 건설 로직 | 산림 로직 | 숫자 | +|---|---|---|---|---|---| +| 합판거푸집 N001 · N025 · Y029 | 건설 6-3-1 | 형틀 0.111 · 보통 0.044 | GC000211 0.112 · 0.045 | GF000160 0.104 · 0.057(가장 닮은 조합) | 건설 | +| 문양거푸집 N015 · S086 | 건설 6-3-4 | 0.07 · 0.03 | GC000215 같음 | GF000161 같음 | 두 책 같은 품 | +| 강관동바리 N044 · S057 | 건설 2-6-1 | 형틀 0.06 · 보통 0.02 | GC000015 같음 | GF000189 0.07 · 0.05(㎥) | 건설 | +| 강관비계 N046 · S056 | 건설 2-7-1 | 비계 0.055 · 보통 0.018 | GC000022 같음 | GF000188 비계 0.1(공㎥) | 건설 | +| 돌망태형 옹벽 N037 | 산림 13-114(= 13-11-4) | 석공 0.072 · 특별 0.053 · 보통 0.013 | GC000342 0.19 · 0.134 · 0.117 | GF000235 같음(단위 ㎡ ↔ ㎥ 표기만 다름) | 산림 | +| 면고르기 N032 | 건설 3-5-1 | 굴착기만 | GC000111 장비만(시간 다름) | GF000124 보통인부 + 장비 | 구성 건설 | + +- 11 줄 모두 숫자 = 엑셀 인용 책 → 작성자는 책을 바르게 적음 · 틀린 쪽은 우리 「이름 같음」 자동 대상(묶음 ① 같음 근거 검사 · 지문 둘 다 11 줄 풂). +- 정답 · 허용에 두 책이 다 있는 64 줄로 넓히면 — 숫자 = 인용 책 31 · 두 책 같은 품 8 · 어느 책과도 3% 넘게 다름 23 · 인용 건설 · 숫자 산림 1(영월 야면석메붙임 「건설 7-2-1」 · 품 = 산림 13-5-2) · 출처 없음 · 숫자 산림 1. +- 「조항은 건설 · 숫자는 산림」 모두 9 줄 = 고임돌 · 조약돌 4(「건설 1-10」 단위중량 인용 · 밑품 0.3 · 0.6 = 산림 13-2-1 막자갈 · 조약돌) · 야면석 채집 3(「건설 8-2」 2016 삭제 · 밑품 0.17 = 산림 13-2-4) · 되메우기 1 · 야면석메붙임 1. + +### 3-3. 「로직 X · 계수 바뀜」 판정 — 되는가 · 몇 줄 + +- 지문 1위가 맞은 103 줄 — 품 같음 56 · 조합 같음 4 · 계수 바뀜 21 + 조합 2 · 섞음 · 할증 붙임 3 · 직종 다름 3 · 단위 달라 크기 못 견줌 8 · 기계경비 · 자재 · 품 모름 6. +- 「로직은 맞추고 다른 점만」 글로 보일 줄 = 26(계수 23 · 섞음 3) · 직종 다름 3 은 「직종 다름(철근공 더함)」 글로 · 단위 다른 8 은 꼴(비율)만 견줌. + +| 엑셀 줄 | 판정 글 | 까닭(엑셀 블록) | +|---|---|---| +| 구조물터파기(인력100%) 토사 | GC000083 굴착(인력/토사) · 계수 바뀜 특별인부 0.370 → 0.354 | 보통 · 경질 · 자갈섞인 토사 평균 | +| 임도안전표지판설치 | GC000308 교통 안전표지판 설치 · 계수 바뀜 특별인부 0.167 → 0.258 · 보통인부 0.083 → 0.129 | 지주 · 표지판 두 작업을 한 품으로 | +| 야면석찰붙임(영월) | GC000263 찰붙임(굴착기+집게) · 계수 바뀜 석공 0.050 → 0.081 · 보통인부 0.020 → 0.027 | 옛 판 값 | +| 구조물되메우기(인력100%) | GF000113 되메우기 · 섞음 비율 바뀜(인력 10% → 100%) · 품 같음 0.1 | 산림 9-14 섞음 비율 무시 | +| 고임돌채집및운반(9:1) | GF000211 모래·자갈·약돌 채집 · 섞음 10%(밑품 0.3 = 막자갈) + 굴착기 · 덤프 묶음 | 채집 + 상차 + 운반 한 줄 | +| 철근가공조립(현장) TypeⅠ | GC000206 + GC000207 · 조합 같음 | 건설 6-2 윗 조항 하나로 | + +- 엑셀 조건값 — 품 같은 65 줄 중 지금 규격 읽기(`spec_inputs`)로 같은 품 48 · 지문 변형으로만 같은 품 17 → 지문 1위 변형을 입력값 추천으로 쓰면 17 줄 입력이 맞아짐(예 합판 「사용횟수 4회」만 읽고 「유형=보통」은 놓침). + +--- + +## 4. 남는 실패 · 한계 + +| 남는 1순위 실패 | 지문만 23 | 추천 14 | 풀 길 | +|---|---|---|---| +| 고임돌 · 조약돌(9:1 · 정답 = 인력 몫) | 6 | 5(정답 2~4 위) | 숫자로는 「인력 10% + 굴착기 90%」 꼴의 터파기 로직(GF000103)이 더 닮음 → 절 조합(2-3)으로 보이거나 기억(C6) | +| 소운반(옛 9-1 인력운반 식) | 3 | 3 | 단위 ㎡ ↔ ㎥ · 식이 다름 → 기억 | +| 측구수로관 · 철골 공장 · 철골설치 | 2 | 3 | 직종 이름 다름(철골공 ↔ 철골 가공조립공) · 플륨관 이름 | +| 펌프차 · 터파기(토사) | 0 | 2 | 지문은 맞히나 섞기에서 잃음 — 「딱 맞음」이 아닌 줄 | +| 영월 씨앗 · 짐작 정답 줄(사용자 확정 · 짐작이 숫자와 다름) | 10 | 0 | 「같음은 그대로」 · 섞기로 풂 — 예 콘크리트타설 Y008: 숫자는 산림 GF000166(0.15 · 0.27 정확) · 씨앗은 건설 GC000198 | +| 영월 큰돌채집운반(직종 · 단위 바꿈 — 엑셀 보통인부 ↔ 우리 작업반장 · ㎡ ↔ 개) | 1 | 1 | 기억 | +| 도장(복합) | 1 | 0 | 조합으로는 맞음 | + +- 보통인부 하나뿐 줄(되메우기 · 잔토처리 · 떼붙임 …) — 같은 품 로직이 수십 → 품 숫자로 못 가름 · 이름 · 조항 · 기억이 가름(딱 맞음에서도 뺌). +- 우리에 없음 10 줄 — 지문은 늘 가장 닮은 것을 내어 후보 뜸 5 → 10 · 1위 점수 문턱 2.5 = 없음 4 숨김 · 맞은 4 잃음(C3 #33 과 같은 결 · 무익). 다만 영월 잔토처리(B/H 100%)는 숫자가 토사깍기(기계)와 같아 「닫힌 세계」로는 그 로직(C5 와 겹침). +- 품 모름 로직 110 — 직종 이름 · 조항 · 이름으로만 셈(예 GF000192 P.V.C 파이프 · GF000025 떼붙임 「품」 빈 칸). +- 같은 품 로직 둘 — 영월 포장절단 GF000262 · GF000163 처럼 우리 쪽에 숫자가 같은 로직이 있으면 숫자로 못 가름. +- 표본 3 파일(둘이 같은 발주처) · 정답표 일부 짐작(31) · 영월 씨앗 줄은 사용자 선택이 정답 · 무게 · β · 규칙은 표본을 보고 다듬음 → 다른 발주처 파일로 다시 잴 것. +- 북부청 꼴 식 풀기는 글 꼴(「직종 : 단가 × 식 = 금액」)에 기댐 — 거창 꼴(줄 = 노임 · 자재)은 줄 자체가 지문이라 더 쉬움(안 잼). + +--- + +## 5. 코드로 옮길 때 자리 + +| 할 일 | 자리 | 비고 · 흉내 | +|---|---|---| +| 엑셀 지문 뽑기(직종 품 · 밑품 · 장비 · 자재 · 율 · 계수 · 조건 · 절) | 새 `M02_MasterTemplete/M02_Hopyo_Finger.py` `excel(...)` — `M02_Hopyo_Basis.read`(:84-125)가 블록을 돌 때 함께 불러 블록에 「지문」 칸 | Basis 266 줄 → 새 파일(700 줄 지킴) · `s1_excel_fp.north` | +| STmate 지문 | C3 ④ STmate 읽기 안(코드 · QTY · 목록표) | `s1_excel_fp.yeongwol` · C6 와 겹침 | +| 우리 지문 미리 셈 | `M02_Hopyo_Finger.ours()` — `m01.loaded()` + `m01.cm.mf.run`(입력 조합) · 마스터 자국이 바뀔 때만 · 처음 ≈ 2 분 → `M01_MasterData_Store_Cache.warm` 처럼 뒤에서 · 메모리(원하면 tmp 캐시) | `s2_ours_fp.py` | +| 닮음 셈 · 딱 맞음 · 판정 글 | `M02_Hopyo_Finger.score · sure · verdict` | `s3_match.score_pair · sure` · `s4_changes.compare` | +| 후보에 합치기 | `M02_Hopyo_Match.match_line`(:404-414) — 근거 · 이름 후보(`hopyo_basis.merge` 뒤)에 지문 점수를 더해 다시 줄 세움 · 지문 상위도 후보에 넣음 · 같음(:395-403)은 그대로(추천 규칙 · 묶음 ① ⑪ 과 함께) | 금액 안 씀 | +| 입력값 추천 | `M02_Hopyo_Match.guess`(:417-428) — 지문 1위 변형 입력을 `spec_inputs` 빈 칸에 | 17 줄 | +| 조합(절마다) | `M02_Hopyo_Finger.sections` | C5 와 겹침 — 한 벌로 | +| 화면 | 고르기 창 후보 글에 「딱 맞음」 · 「계수 바뀜 …」 · 절 조합 | C3 ② 고르기 창 손질과 같이 | +| 시험 | `resources/tester/test_m02_hopyo_finger.py` — 식 풀기 · 섞음 가름 · N001 → GC000211 · 철근 조합 · 딱 맞음 | | + +- 원본 로직 안 고침 확인 — 우리 쪽은 `m01.loaded()` 읽기 · 엔진 셈(`run_line` 과 같은 읽기 길)만 · `resources/master_data` · `일위대가조합.json` 쓰기 없음 · 이번 흉내도 저장소 파일 안 고침. +- 겹침 표시 — C5(조합 풀이 · 닫힌 세계): 절마다 조합(`s4_changes.combination`) · 영월 잔토처리 「없음 → 토사깍기」. C6(기억 · 시험 파일): STmate 읽기 · 딱 맞음 = 자동 확정 · 기억 후보 재료. 함수는 각자 만들었으므로 코딩 때 한 벌로 모음. + +## 부록. 스크립트 · 자료 (스크래치패드 `req226/C4/` · 각 120 초 안 · 저장소 `venv` 파이썬) + +- `fp_common.py` — 식 풀기(`parse_rhs`) · 장비 갈래 · 조건 낱말 · 닮음 셈. +- `s1_excel_fp.py` → `excel_fp.json`(엑셀 지문 136) · `s2_ours_fp.py <시작> <끝>` · `merge` → `ours_fp.json`(우리 지문 1,438 · 세 번에 나눠 ≈ 110 초). +- `s3_match.py` → `out_match.json`(지표 · 줄마다 1~5 위 · 채널 · 변형 · 딱 맞음 · 추천) · `s4_changes.py` → `out_changes.json`(조합 · 바꿈 꼴 · 판정 · 책 갈래 · 두 책). +- `s5_summary.py` 표 뽑기 · `s6_specinputs.py` 규격 읽기 대 지문 변형 · `p0*.py` 들여다보기. +- 정답 · 셈 · 지금 · 묶음 ① = C1 · C3 함수(`labels` · `labels_y` · `cats` · `s10_all` · `s20_c3_extra`)를 같은 프로세스에서 불러 씀. + +--- + + +**부록 B. C5 — 조합 풀이 · 닫힌 세계 실측 원문** (원문 `C5_조합풀이.md`) + +# 요구 226-4 C5 — 조합 풀이 · 닫힌 세계 실측 + +바깥 일위대가(호표)를 「우리 로직 · 우리 일위대가 · 단가 줄의 조합」으로 푸는 길을 실측함. 코드 · 저장소 고침 없음 — 실험만. +기준 = main_laptop_1 b7e727863 · 정답 · 셈 = 앞 조사(225-4 C1 · C3)와 같음(고유 줄 · 엄격/느슨 · 1순위 · 다섯 안 · 씨앗 켬 · 기억 없음). +「묶음 ①」 = C3 추천 결합(조항 윗마디 · 같은 이름 접힘 고침 · 조항 오타 · 같음도 근거 검사) 흉내 · 「(가)」 = 이 조사의 호표 묶음 조건 · 「조합 풀이」 = 블록 몫 벡터를 우리 로직 벡터로 풂. +스크립트 · 중간 자료 = 스크래치패드 `req226/C5/`(7 절). + +## 0. 결론 + +1. 바깥 호표 나무 — 8 파일 · 호표 335 · 줄 1,388 을 세 꼴(북부청 · 거창 · STmate)로 다 세움. 직접 줄 호표(노임 · 장비 줄이 호표에 바로) 59 · 호표 참조 줄 90 · 참조 깊이 최대 3. +2. (가) 호표 묶음 조건만으로는 줄 1순위가 작게 오름 — 묶음 ① 위 +1.6(85.7 → 87.3 · 엄격). 「한 호표 안 같은 책」은 오히려 −1.6(북부청은 한 호표 안에서 콘크리트 · 거푸집 = 건설 · 되메우기 · 돌쌓기 = 산림을 섞음) · 「다른 발주처 조합과 통째 같음」은 −0.8(그 발주처 책을 끌고 옴 · 근거 지킴을 달면 0). +3. 조합 풀이(몫 벡터 · 금액 안 씀)를 더하면 줄 1순위 87.3 → 91.3(엄격) · 93.7(느슨) · 엑셀 줄 무게 느슨 88.5 → 95.5. 쉬운 예 — 「고임돌 채집 및 운반(굴삭기 90% · 인력 10%)」이 「자갈채집 및 운반(궤도)」 대신 인력 10% 몫 벡터(보통인부 0.03 = 0.3 × 10%)로 「모래·자갈·약돌 채집(인력)」을 찾음. +4. 새 지표 「조합 맞음」(호표 통째 · 덮을 수 있는 83 호표 · 엄격) — 지금 9.6% → 묶음 ① 27.7% → +(가) 26.5% → + 조합 풀이 72.3%. 칸(호표 안 로직) 맞음 59.1 → 77.7 → 91.2%. 줄 1순위 몇 점 차이가 호표 통째에선 세 배 가까이 — 철근 가공 + 조립 · 섞인 채집 줄이 여러 호표에 되풀이되기 때문. +5. (나) 직접 줄 호표는 노임 · 장비 벡터가 이름보다 훨씬 맞음 — 판정 34 호표(STmate 인용 29 · 거창 사람 판정 5 · 짐작) 엄격 이름만 9 · 이름 + 인용 11 · 벡터 + 인용 32(느슨 27 · 29 · 33). 이름은 「같은 일」은 찾되 책을 틀림 — 직종 · 장비(굴착기 + 집게) · 품이 책 · 갈래를 가름. +6. 4 번 물음 — 우리 로직이 더 잘게(철근 가공 + 조립) 고유 6 중 4 풂(엑셀 33 줄 중 23) · 엑셀 복합 4 중 1 풂(도장 셋 풂 · 철골 가공조립 + 용접은 직종 이름 어긋남으로 못 풂) · 섞인 줄(기계 90% + 인력 10%) 11 중 인력 몫 11 · 기계 몫까지 9 · 줄 1순위 바로잡음 5. +7. 다른 꼴(거창 · STmate) 24 호표를 37 조합의 「실무 호표 — 봉화 B00005」 출처 표시(사람 판정)와 견주면 조합 풀이가 통째 14 → 17 · 조합 로직 찾음 79 → 90%(씨앗 켬 = 이미 본 파일). 씨앗 끔(처음 보는 셈)은 3 → 5 · 44 → 57% — 처음 보는 파일은 기억(C6) · 읽기가 먼저. +8. 원본 로직 · 일위대가조합.json · M01 은 읽기만 — 우리 로직 벡터는 M01 엔진(`mf.run`)을 부르기만 해서 냄. 옮길 곳 = M02 맞춤 쪽 새 파일 셋(몫 벡터 · 우리 벡터 캐시 · 조합 풀이) + 읽기 작은 고침 둘 + 사전조합 만들기 「한 줄 = 담은로직 여럿」(정할 것 8). +9. 한계 — 규칙 문턱을 같은 정답 표본을 보며 정함(과적합 위험 · 새 파일 재측정 필요) · 직종 이름 어긋남 · 숫자 입력(몫 비율 · 칠 횟수) 갈래 안 돌림 · 장비만 든 몫은 「기계 시간 로직」 규칙으로만 · 우리에 없음 줄에 엉뚱 로직. + +--- + +## 1. 나무 세우기 + +호표마다 줄 → 갈래. 단산 = 단가산출(근거) 블록 참조 · 호표 = 다른 호표 참조 · 노임 · 장비 = 직접 줄 · 근거글 = STmate 인용 줄(「건설표준품셈 7-2-2(찰붙임)」). + +| 파일 | 꼴 | 호표 | 줄 | 단산 | 호표 참조 | 노임 | 장비 | 자재 | % | 별도계상 | 근거글 | 직접 줄 호표 | 참조만 호표 | 단산 블록 | 몫 벡터 단산 줄 | 깊이 | +|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---| +| 북부청 임도 | 북부청 xls | 68 | 415 | 268 | 1 | 0 | 3 | 78 | 1 | 64 | 0 | 3 | 64 | 95 | 257/268 | 2 | +| 북부청 사방 | 북부청 xls | 27 | 201 | 139 | 2 | 2 | 1 | 38 | 1 | 18 | 0 | 2 | 25 | 67 | 135/139 | 2 | +| 거창 | 거창 xlsx | 14 | 68 | 21 | 12 | 10 | 3 | 13 | 6 | 3 | 0 | 5 | 9 | 61 | 21/21 | 3 | +| 봉화 | STmate | 45 | 216 | 103 | 22 | 30 | 7 | 33 | 7 | 7 | 7 | 15 | 30 | 65 | 67/103 | 3 | +| 동부청 영월 | STmate | 23 | 138 | 90 | 3 | 3 | 1 | 23 | 0 | 18 | 0 | 2 | 21 | 69 | 55/90 | 3 | +| 울진 대흥 | STmate | 34 | 184 | 69 | 28 | 29 | 11 | 15 | 3 | 16 | 13 | 16 | 18 | 47 | 52/69 | 2 | +| 영덕 계류 | STmate | 9 | 37 | 2 | 3 | 14 | 5 | 4 | 3 | 0 | 6 | 7 | 2 | 15 | 1/2 | 2 | +| 울진 소광 | STmate | 25 | 129 | 43 | 19 | 17 | 5 | 15 | 9 | 9 | 12 | 9 | 16 | 40 | 38/43 | 3 | +| **합** | | **335** | **1,388** | **735** | **90** | **105** | **36** | **219** | **30** | **135** | **38** | **59** | **185** | **459** | **626/735** | | + +- 직접 줄 호표 = 노임 · 장비 줄이 하나라도 있음 · 참조만 = 단산 · 호표 참조 · 자재 · % · 별도계상뿐. +- 깊이 3 쉬운 예 — 거창 제5호표 BOX(암거) → 일위1 레미콘타설(철근) · 일위3 합판거푸집 → 일위4 합판거푸집 1회재료비(자재만). 일위1 · 3 은 노임 줄이 바로 적힌 직접 줄 호표. + +꼴마다 읽기: + +| 꼴 | 읽은 길 | 줄 갈래 | 블록 몫 벡터 | +|---|---|---|---| +| 북부청 xls | 지금 읽기 함수(`M02_Hopyo_Excel.read_book`) 그대로 | 지금 `kind_of` + 단위 「인」 · 우리 직종 이름 = 노임(사방 철골공 · 특별인부 2) | B 열 블록 글 → 맨 윗 번호 절(1. 2.) · 가. 나. 몫 · 품 = 식에서 첫 수(노임 · 장비 단가)를 1 로 두고 나머지만 셈(「273,540 × 3인 ÷ 55㎥ × 2」 → 0.109 · 「노무비 : 59,020 ÷ 23.1 × 90%」 → 장비 0.039 시간) · 식 514 풂 · 못 푼 32 는 노임 줄만 금액 ÷ 노임으로 품 되찾음(순위 · 판정엔 금액 안 씀) | +| 거창 xlsx | 지금 읽기로 호표 14 · 줄 68 읽힘 | 노임 줄이 지금은 「기타」로 로직 맞춤에 들어감(엉뚱 후보) | 단가산출 글이 한 행 수십 칸에 흩어짐 → 지금 `Basis.read`(B 열만)는 출처 0/21 · 직종 0/21 · 행 칸을 이어 붙이면 출처 17/21 · 직종 21/21 · 몫 벡터 21/21 | +| STmate xlsx 5 | 흉내 읽기(C1 `s09_stmate` 넓힘) — 일위대가표 + 단가산출근거 | 줄 코드 = 14 열 「D00096_1」(봉화 · 대흥은 12 열이 비고 · 「대가 N」 「산근 N」 글) · B 호표 참조 · D 단산 · L 노임 · M 자재(「별산」 · 「별도계상」 = 별도계상) · X 장비 · S · W 별도계상 · PR % · 빈 코드 = 근거글 · T · C = 계 줄(뺌) | 단산 블록 머리 = 6 열 「BDCOD\|D00002」 · 자원 줄 = 0 열 「L00002L」 또는 6 열 식 「&L00007L&」 + 7 열 QTY + 23 열 이름 → 노임 · 장비 수가 식 없이 바로 나옴 | + +--- + +## 2. 닫힌 세계 풀이 — 줄 + +### 2-1. (가) 호표 묶음 조건 — 지금 후보 다섯(지금 함수 그대로)에서 다시 고름 + +칸 = 1순위 엄격 / 느슨 · 다섯 안 엄격 / 느슨(고유 126 줄). 조건이 다섯 안에서만 다시 고르므로 다섯 안 값은 그대로. + +| 조건 | 무엇 | 지금 위 | 묶음 ① 위 | 쉬운 예 | +|---|---|---|---|---| +| 바탕 | | 71.4 / 81.7 · 86.5 / 94.4 | 85.7 / 88.1 · 94.4 / 94.4 | | +| 같은 책(줄 근거 먼저 · 없으면 호표 다수) | 1순위 책이 다르면 다섯 안 같은 일 다른 책으로 | 80.2 / 82.5 (+8.8) | 84.9 / 87.3 (−0.8) | 지금 위 오름은 묶음 ①의 「같음도 근거 검사」와 같은 몫 | +| 같은 책(호표 다수만 · 근거 없는 줄) | 한 호표 안 줄끼리 같은 책 | 69.8 / 80.2 (−1.6) | 84.1 / 86.5 (−1.6) | 구조물되메우기(근거 없음 · 정답 산림 9-14) → 호표 다수 건설 「되메우기 및 다짐(소형장비)」로 틀어짐 | +| 같은 조건(토질) | 줄 · 호표 이름 「토사 ↔ 암」 어긋난 후보 뒤로 | 71.4 / 82.5 (0 / +0.8) | 85.7 / 88.9 (0 / +0.8) | 구조물터파기(토사) → 「터파기(기계) 암」 대신 보통토사 | +| 같은 이름 다수(한 문서) | 정리 이름 같은 줄(규격만 다름)은 같은 로직 | 73.0 / 82.5 (+1.6) | 87.3 / 88.9 (+1.6) | 영월 콘크리트타설 · 합판거푸집(4회) 2 줄이 문서 다수 답으로 | +| 우리 조합 통째 같음(37) | 호표 로직 줄 수 = 조합 로직 수 · 줄마다 다섯 안에 조합 로직 하나 | 70.6 / 81.0 (−0.8) | 84.9 / 88.1 (−0.8) | 북부청 임도반사경 = 「도로반사경 설치」 조합(영월 출처) → 구조물터파기(인력100%)가 건설 3-2-1 → 산림 9-13-1 로 틀어짐 | +| 우리 조합 안에 다 듦 | 호표 줄(2 이상)이 모두 한 조합 안 | 70.6 / 82.5 | 82.5 / 87.3 (−3.2) | 큰돌찰쌓기 → 메쌓기로 틀어짐 | +| 통째 + 근거 지킴 | 1순위가 줄 근거 조항과 맞으면 안 바꿈 | 71.4 / 81.7 (0) | 85.7 / 88.1 (0) | | +| 안에 다 듦 + 근거 지킴 | 〃 | 73.8 / 84.1 (+2.4) | 85.7 / 88.1 (0) | 철근가공조립(현장) → 「철근 현장가공」(묶음 ①은 조항 윗마디로 이미 풂) | +| **(가) 좋은 것**(토질 · 이름 다수 · 안에 다 듦 근거 지킴) | | **75.4 / 85.7 (+4.0)** | **87.3 / 89.7 (+1.6)** | | +| 씨앗 끔 | | 67.5 / 77.0 → 69.0 / 79.4 | 80.2 / 84.1 → 81.0 / 84.9 | | + +- 「아래에서 위로」 — 정답 세 파일의 호표 참조 줄은 6 뿐이고 로직 줄이 아님 → 줄 지표엔 안 듦 · 조합 지표(3 절)에서 참조 호표 로직을 위 호표에 더함. 거창 · STmate 넷(영월 뺌)은 참조 줄 84 라 조합 지표에서 큰 몫(3-2). +- 「같은 일 몫」 — 정답 세 파일엔 몫이 줄로 나뉜 호표가 없음(북부청은 기계 90% + 인력 10% 를 한 블록에 씀) → 줄 안 몫 풀이(2-2)로 셈. +- 뜻 — 북부청 호표는 일마다 책을 골라 섞음(콘크리트 · 거푸집 · 철근 = 건설 · 되메우기 · 돌쌓기 · 채집 = 산림). 호표 단위 「같은 책」은 맞지 않는 가정 · 다른 발주처가 만든 조합을 통째로 옮기면 그 발주처의 책 선택이 따라옴(C3 「다른 발주처 기억 62~71%」와 같은 뜻). + +### 2-2. 조합 풀이 — 블록 몫 벡터를 우리 로직 벡터로 + +방법: +- 우리 로직 단위당 벡터 = M01 엔진 `mf.run(whole, 로직, 입력)` 줄의 단위 「인」(직종 · 품) · 「시간」(장비 · 시간) — 자동값 한 벌 + 고르기 입력 갈래(지역 뺌 · 곱 48 넘으면 하나씩 바꿈) · 돈 로직 1,438 · 엔진 13,854 번(85 초씩 다섯 번) · 캐시 `ourvec_var.json`. +- 맞음 = 차원(직종 · 장비 종류)마다 작은/큰 가중 평균(보통인부 0.5 · 장비는 있음 0.5 + 시간 0.5 · 크기 다름 × 0.8 · 굴착기 ↔ 굴착기 + 집게 × 0.8) · 몫 x ∈ {0.1 … 0.9}(10% · 90% 나눔) × 0.95 · 몫 표시(「인 력 (10%)」)와 같은 x 면 덤. +- 순위 = 맞음 + 앞선 믿음(지금 합친 후보 순위 0.3 · 줄 근거 조항 같음(윗마디 포함) 0.3 · 같은 책 0.1) · 후보 풀 = 지금 합친 후보 15 + 드문 직종 · 장비 색인(40 개 이하) + 절 제목 이름 후보 8. +- 고름 = 1순위 + 1순위가 못 덮는 절 · 몫의 로직(절 맞음 ≥ 0.75 · 몫 ≥ 0.6) · 장비만 든 절 · 몫 = 「기계 시간당 기계경비」(닫힌 세계 규칙 · 기종 입력) · 섞인 줄(몫 표시 + 노임만 든 몫 + 장비만 든 몫)에서 1순위가 줄 벡터를 35% 못 덮으면 1순위를 인력 몫 로직으로. + +| 단계 | 1순위 엄격 / 느슨 | 다섯 안 | 엑셀 줄 무게 1순위 느슨 | 파일별 1순위 엄격(임도 · 사방 · 영월) | +|---|---|---|---|---| +| 지금 | 71.4 / 81.7 | 86.5 / 94.4 | 78.1 | 71.4 · 74.4 · 67.6 | +| 묶음 ① | 85.7 / 88.1 | 94.4 / 94.4 | 85.6 | 91.8 · 86.0 · 76.5 | +| + (가) | 87.3 / 89.7 | 94.4 / 94.4 | 88.5 | 91.8 · 86.0 · 82.4 | +| **+ 조합 풀이(섞인 줄 1순위 바꿈)** | **91.3 / 93.7** | 94.4 / 94.4 | **95.5** | **98.0 · 90.7 · 82.4** | +| 씨앗 끔: (가) → + 조합 풀이 | 81.0 / 84.9 → 84.9 / 88.9 | 92.9 / 93.7 | | | + +- 바로잡은 5 줄(엑셀 33 줄) = 고임돌 채집 및 운반 셋 · 조약돌 채집 둘 — 「자갈채집 및 운반(궤도)」 · 「막돌 채집」 → 「모래·자갈·약돌 채집(인력)」(x = 0.1 · 맞음 0.95). +- 뺌 규칙을 모든 줄에 걸면 「구조물되메우기(인력100%)」가 틀어짐(엑셀이 인력 몫을 100% 로 바꿈 · 우리 자동값은 10%) → 섞인 줄에만 씀. + +### 2-3. 4 번 물음 — 조합 풀이로 몇 개 푸나 + +| 갈래 | 고유(엑셀 줄) | 풂 | 쉬운 예 | 못 푼 까닭 | +|---|---|---|---|---| +| 우리 로직이 더 잘게 | 6(33) | 4(23) — {철근 현장가공, 철근 현장조립} 둘 다 | 「철근가공조립(현장)」 블록 1. 현장가공(철근공 0.67 · 보통 0.22 = 6-2-2 Type-Ⅰ 맞음 0.99) + 2. 조립 → 6-2-3 | 임도 1 = 현장가공 몫 수가 달라 「공장가공」으로 · 영월 1 = 블록 몫 벡터 못 읽음 | +| 엑셀 복합 호표 · 복합 줄 | 4(4) | 1 — 도장(바탕 + 녹막이 + 유성 셋) | 「도장(철재면2회)」 = 11-1-3 + 11-2-6 + 11-2-4 | 철골가공조립(공장) = 엑셀 「철골공」 ↔ 우리 「철골 가공조립공」 직종 이름 어긋남(용접 몫만 맞음) · 잡석 구입 운반 2 = 운반 블록 하나를 골재운반이 다 덮음(덤프운반 몫 안 나뉨) | +| 섞인 줄(기계 90% + 인력 10%) | 11(75) | 인력 몫 11 · 기계 몫(기계 시간 로직)까지 9 · 1순위 바로잡음 5 | 고임돌 · 조약돌 · 야면석 채집 · 뒷채움 · 기초잡석 · 구조물터파기(토사) | 영월 섞인 줄(구조물터파기 B/H 90% · 야면석 · 고임돌 채집운반)은 STmate 블록에서 인력 · 기계 몫이 한 절로 읽혀 판별 못 함 | + +- 홑 정답 줄 105 중 조합 풀이가 로직을 둘 넘게 고른 줄 5 — 엉뚱 덧 2(다웰바 + 쉬즈관 · 되메우기 + 훈증더미) · 기계 시간 몫 덧 3(기초잡석 운반 · 잡석운반 · 포장절단). + +--- + +## 3. 새 지표 「조합 맞음」(호표 단위) + +### 3-1. 정답 세 파일(북부청 둘 · 영월) — 115 호표 · 덮을 수 있음 83 + +정답 = 앞 정답(labels · labels_y)을 호표로 모음 — 줄마다 칸 하나(정답 여럿 = 그중 하나) · 철근 가공 + 조립 · 도장 셋 · 잡석 운반 둘 = 칸 여럿(모두) · 정답 없는 줄(잔토처리 등) = 「없음」 칸(채울 수 없음 → 덮을 수 없는 호표 32) · 호표 참조 = 참조 호표 칸을 더함(아래에서 위로). +통째 = 칸이 다 차고 남는 로직 없음 · 칸 = 찬 칸 비율 · 고름 = 고른 로직 중 맞은 비율. T1 = 앞 정답 그대로(섞인 줄 = 인력 몫 하나) · T2 = 정할 것 8 다른 안(섞인 줄에 기계 몫 칸 · 철골 = 가공조립 + 용접 두 칸). + +| 우리 | T1 통째 엄격 / 느슨(덮을 수 있음 83) | T1 칸 | T1 통째(전체 115) | T1 파일별 통째(임도 · 사방 · 영월) | T2 통째 | T2 칸 | +|---|---|---|---|---|---|---| +| 지금(줄 1순위 모음) | 9.6 / 30.1 | 59.1 / 73.2 | 7.0 | 4/60 · 0/6 · 4/17 | 9.6 / 26.5 | 52.7 / 65.9 | +| 묶음 ① | 27.7 / 31.3 | 77.7 / 79.5 | 20.0 | 19/60 · 0/6 · 4/17 | 24.1 / 27.7 | 70.3 / 72.1 | +| + (가) | 26.5 / 28.9 | 77.6 / 79.0 | 19.1 | 19/60 · 0/6 · 3/17 | 22.9 / 25.3 | 70.1 / 71.6 | +| + 조합 풀이 · 기계 몫 넣음 · 뺌 | 21.7 / 24.1 | 91.2 / 92.6 | 15.7 | 15/60 · 2/6 · 1/17 | **51.8 / 54.2** | **91.6 / 93.0** | +| **+ 조합 풀이 · 기계 몫 안 넣음 · 뺌** | **72.3 / 74.7** | **91.2 / 92.6** | **52.2** | **53/60 · 4/6 · 3/17** | 42.2 / 44.6 | 80.5 / 82.0 | +| 씨앗 끔: (가) → + 조합 풀이(T1) | 24.1 → 69.9 | 72.9 → 86.5 | | | | | + +- 나눠 셈(T1 · 통째 · 83 호표) — (가) 22 → 덧(철근 조립 · 도장 몫 등)만 38 → 섞인 줄 1순위 바꿈만 40 → 둘 다 60. +- 줄 1순위 +4 점이 호표 통째 +46 점 — 같은 엑셀 줄이 여러 호표에 되풀이(고임돌채집및운반 1 줄이 임도 호표 17 개 · 철근가공조립 1 줄이 11 개). +- 남는 실패 호표(T1 · 23) — 영월 기초잡석채집 · 철근(블록 벡터 없음) · 고임돌채집운반(영월) · 야면석 · 콘크리트 소운반(현장말 · 우리에 없음) · 잡석 구입 운반 · 크레인 · 측구수로관. + +### 3-2. 다른 꼴(거창 · STmate) — 37 조합 출처 표시를 사람 판정(짐작)으로 + +일위대가조합.json 37 조합 비고 「실무 호표 — 봉화 B00005 · 울진대흥 B01224」를 호표 정답으로 씀. 출처 하나뿐인 조합 24 호표 = 통째 견줌 · 출처 여럿 63 = 고른 것 맞음만(조합 = 여러 호표 합). 「사람이 뺀 줄」(비고의 「우리 로직에 없음」 · 「이름이 달라 뺌」) 줄은 우리 쪽에서도 뺌. +우리 — 지금 = 줄마다 지금 1순위(노임 · 장비 줄도 지금처럼 로직 줄로) / 조합 풀이 = 단산 줄 묶음 ① + 조합 풀이 · 직접 줄 = 벡터 풀이(4 절) · 호표 참조 = 아래에서 위로. + +| | 씨앗 켬(이미 본 파일) 통째 · 찾음 · 고름 | 씨앗 끔(처음 보는 셈) 통째 · 찾음 · 고름 | 출처 여럿 63 고름(켬 · 끔) | +|---|---|---|---| +| 지금 | 14/24 · 79.3% · 81.7% | 3/24 · 44.2% · 46.4% | 59.2% · 38.2% | +| **조합 풀이** | **17/24 · 90.2% · 92.8%** | **5/24 · 56.5% · 58.0%** | 59.2% · 45.0% | + +- 쉬운 예 — 거창 제5호표 BOX(암거): 지금 = {굴착(인력/토사) · 철근 현장가공 · 유로폼 · 콘크리트 포장(인력시공) · 강관비계}(노임 줄 「콘크리트공 · 보통인부」를 로직 이름으로 맞춤) · 조합 풀이 = {레디믹스트콘크리트 타설 · 합판거푸집 · 유로폼 · 철근 현장가공 · 철근 현장조립 · 강관 동바리 · 강관비계(쌍줄)} → 조합 UA000014 일곱 중 여섯 같음(합판거푸집 책만 다름) · 씨앗 꺼도 같음. 날개벽(제7 · 8호표)은 통째 같음. +- 어긋남 다수 = 같은 일 다른 책 — 울진소광 파형강관 · 문양거푸집 · 돌조공: 엑셀 인용은 건설(「건설품셈 토목 6-7-2」 · 「※ 건설품셈 6-3-4」 · 「건설품셈 7-1-1」) · 우리 풀이도 건설 · 조합(사람)은 산림 → 책 정책(앞 검토 정할 것 1)에 따라 판정이 갈림. +- 씨앗 켬 · 끔 차이(통째 14 → 3)가 조합 풀이 효과(+2~3)보다 큼 — 다른 꼴에서는 기억(이미 확정한 조합)이 가장 큰 몫. + +--- + +## 4. 직접 줄 풀이 (나) + +직접 줄 호표 59(북부청 5 · 거창 5 · STmate 49). E = 호표의 노임 · 장비 줄 벡터(참조 줄 뺌). 견줌: +- 이름만 = 호표 머리(이름 · 규격 · 단위)를 지금 맞춤 함수에 줄로 넣음(앞 검토 방법 40 흉내). +- 이름 + 인용 = STmate 근거글(「건설표준품셈 7-2-2(찰붙임)」)을 근거 출처로 함께. +- 벡터 풀이 = 2-2 엔진(통째 하나 맞음 ≥ 0.85 · 아니면 몫) · 맞음이 낮으면 음 아닌 최소제곱 짝(둘) 참고. +- 판정 정답 = 인용 정답(인용 조항이 같고 직종이 겹치는 우리 로직 · 29 · 짐작 — 옛 판 번호 섞임) + 거창 사람 판정 5(레미콘타설 셋 = 6-1-1 · 합판거푸집 = 6-3-1 · 강관동바리 = 2-6-1 · 조합 UA000014 와 맞음). + +| 길 | 엄격(34) | 느슨(같은 일 다른 책 · 34) | +|---|---|---| +| 이름만 | 9 | 27 | +| 이름 + 인용 | 11 | 29 | +| 벡터(인용 없이) | 15 | 31 | +| **벡터 + 인용** | **32** | **33** | + +- 쉬운 예 — 영덕 「돌붙임 메붙임 L3=35cm」(석공 0.13 · 보통인부 0.04 · 굴삭기 + 집게 0.25 시간 · 인용 건설 7-2-1): 이름만 = 「돌붙임(인력)」 · 벡터 + 인용 = 「메붙임(굴착기 + 부착용 집게)」(맞음 0.91). 거창 제2호표 레미콘타설(무근): 이름만 = 「펌프카 타설」 · 벡터 = 「레디믹스트콘크리트 타설」(콘크리트공 0.06 · 보통 0.02 · 굴착기 0.09). +- 정답 없는 25 호표 사람 판정(짐작) — 맞음 16(기계 시간 로직 넷 · 앵커볼트 · 찰쌓기 · 메쌓기 · 포장 인력포설 · 잡철물 · 와이어메시 · 파형강관 · 관목 굴취 · 식재 · 모르타르 · 우리에 없음 둘을 빈 답) · 틀림 6(녹막이 1회 + 조합 2회 · 유성페인트 3회 둘 = 곱 · 합 못 풂 · 표면마무리 못 찾음 · 글자새김 → 찰쌓기(인력) · 규준틀 → 마루틀 = 우리에 없음인데 엉뚱) · 모름 3. +- 음 아닌 최소제곱 짝은 이름 후보 풀에서 엉뚱한 짝(철부도장 → 강관 접합부 도장 + 강교 도장 · 규준틀 → 마루틀 + 건축물보양 × 11.5) — 풀을 직종 · 장비 색인과 인용으로 좁히지 않으면 못 씀. 집합 덮기(절 · 몫마다 하나 · 맞음 문턱)가 낫음. + +--- + +## 5. 남는 실패 · 한계 + +| 갈래 | 예 | 풀 길(제안 · 코드 안 고침) | +|---|---|---| +| 현장말 · 우리에 없음 | 야면석소운반 · 콘크리트소운반 · 잔토처리 · 글자새김 · 녹생토 | 조합 풀이로 못 품 → 「우리에 없음」 빈 답 + 기억(C6) · 맞음 문턱 아래 거름 | +| 직종 이름 어긋남 | 엑셀 「철골공」 ↔ 우리 「철골 가공조립공」 · 거창 철근 블록 「철근공」만 읽힘 | LB 옛이름 쪽 짝 표(마스터 밖 맞춤 사전) — 원본 LB 안 고침 | +| 숫자 입력 갈래 안 돌림 | 「인력 100%」로 바꾼 되메우기 · 칠 3회 · 소량 철근(3.53 인) | 고르기 갈래만 돌림 → 몫 비율 · 횟수 · 운반거리 같은 숫자 입력은 몫 x 에 2 · 3 · 10 배까지 넣어 봄(위험 셈 필요) | +| 장비만 든 몫 | 상차 · 적사 · 운반 · 펌프차 시간 | 일 로직(덤프운반 등)인지 기계 시간 로직인지 벡터로 못 가름 → 지금은 기계 시간 로직 규칙 · 덤프운반(적재 + 운반) 같은 굵은 로직은 절 이어 붙여 셈 필요 | +| 같은 일 다른 책 | 산림 · 건설 벡터가 거의 같음(돌붙임 · 합판거푸집) | 벡터만으론 책 가름 약함(인용 없이 엄격 15/34) → 인용 · 줄 근거 · 책 정책 | +| 영월(STmate) 블록 | 몫 벡터 55/90 · 인력 · 기계 몫이 한 절 | STmate 절 표시(「1. 인 력 (10 %)」) 위치 맞춤 · 직종은 노무비목록표로 | +| 과적합 | 문턱(0.6 · 0.75 · 0.85 · 35%) · 뺌 규칙을 정답 표본을 보며 정함 | 새 파일(다른 발주처)로 다시 잼 — 다른 꼴 24 호표에선 씨앗 끔 통째 +2 · 찾음 +12 점으로 작게 오름 | +| 정답 판정 | 인용 정답 = 작성자 인용(옛 판 번호) · 조합 정답 = 사람이 뺀 줄 · 책 선택 | 「짐작」 표시 · 사용자 판정 표본 늘림 | +| 실행 시간 | 우리 벡터 캐시 13,854 번 엔진(약 7 분) | 마스터 판(sha) 열쇠 디스크 캐시 · 또는 후보 로직만 그때그때 | + +--- + +## 6. 코드로 옮길 때 자리 — 원본 로직 안 고침 확인 + +| 할 것 | 자리 | 원본 로직 · 마스터 | +|---|---|---| +| 노임 줄 갈래(단위 「인」 + 우리 직종 이름) · 장비 줄은 지금대로 | `M02_MasterTemplete/M02_Hopyo_Excel.py` `kind_of` :132-149 | 안 고침 | +| 거창 꼴 단가산출 블록 = 행 칸 이어 붙여 읽음 | `M02_Hopyo_Basis.py` `read` :84-125(지금 B 열 한 칸) | 안 고침 | +| STmate 읽기(일위대가표 · 단가산출근거 · 줄 코드 14 열 · 자원 줄) | 새 파일(예: `M02_Hopyo_Stmate.py`) · `M02_Hopyo_Excel._sheets` :74-102 시트 이름 | 안 고침 | +| 블록 → 절 · 몫 · 품 벡터(식 풀이) | 새 파일(예: `M02_Hopyo_Parts.py` ← c01 `sections` · `qty_of` · `sections_stm`) | 안 고침 | +| 우리 로직 단위당 벡터 + 갈래 캐시 | 새 파일(예: `M02_Hopyo_Vec.py` ← c02 · c03) — `M01_MasterData_Store` 의 `mf.run` · `mau.values` 부르기만 | 엔진 읽기만 | +| 조합 풀이(1순위 + 못 덮는 몫 · 섞인 줄 1순위 바꿈 · 직접 줄 호표 풀이) | 새 파일(예: `M02_Hopyo_Combine.py` ← c05) · `M02_Hopyo_Match.match_line` :367-414 결과에 「조합」 칸 · `match_hopyo` :440-453 에 직접 줄 풀이 · `view` :460-474 에 같은 이름 다수(문서 한 번) | 안 고침 | +| (가) 토질 · 같은 이름 다수 · 조합 안 근거 지킴 | `M02_Hopyo_Match.view` · `match_hopyo`(문서 · 호표를 함께 보는 곳) — Match 534 줄이라 새 파일로 | 일위대가조합.json 읽기만 | +| 사전조합 만들기 — 한 줄 = 담은로직 여럿(철근 가공 + 조립 같은 수량 · 섞인 줄 인력 몫 × 0.1 · 기계 몫 = 기계 시간 로직 × 시간) | `M02_Hopyo_Build.py` `Builder.build` :110-175(지금 한 줄 = 로직 하나 · 같은 로직 둘째 감쌈) | 정할 것 8 · 사전조합 꼴 | +| 조합 기억(호표 · 줄 열쇠 → 로직 모음 · 몫) | `M02_Hopyo_Match.confirm` :495-526 · `M02_Hopyo_Store.remember` | C6 와 겹침 | +| 시험 | `resources/tester/test_m02_hopyo_match.py` · `_basis` · `_excel` + 새 시험(몫 벡터 · 조합 풀이 거울) | | + +- 원본 확인 — 실험은 `resources/master_data`(로직 · 계수 · 인력 · 일위대가조합.json)와 M01 를 읽기만 함(`m01.loaded()` · `mf.run` 부르기 · 파일 쓰기 없음) · 37 조합은 통째 비교 · 출처 정답으로만 씀 · M02 마스터 사전조합은 견본 1(SC-000001)뿐이라 「우리 일위대가」 = 37 조합. +- 계산 자리(CLAUDE.md 5 장) — 조합 풀이는 서버 단독(③) · 브라우저는 안 셈. + +--- + +## 7. 방법 · 실행 경로 · 겹침 + +- 스크립트(`req226/C5/` · 저장소 `venv/Scripts/python.exe -B` · 차례) — `c01_trees.py`(나무 · 몫 벡터 → `trees.json`) → `c02_ourvec.py`(자동값 벡터 → `ourvec_auto.json`) → `c02b_varcache.py 0 1438 85` 다섯 번(갈래 벡터 → `ourvec_var.json`) → `c04_closed.py`((가) → `closed.json`) → `c06_lines.py`(정답 줄 조합 · 4 번 → `lines_solved.json`) → `c08_drop.py`(섞인 줄 1순위 바꿈 → `final.json`) → `c07_hopyo.py`(조합 맞음 → `hopyo_metric.json`) → `c09_direct.py`((나) → `direct.json`) → `c10_others.py [끔]`(다른 꼴 · 37 조합 → `others.json` · `others_off.json`). 엔진 부품 = `c03_vec.py`(맞음) · `c05_solve.py`(조합 풀이). 진단 = `t_ablate`(덧 · 뺌 나눠 셈) · `t_off`(씨앗 끔) · `t_rows`(엑셀 줄 무게) · `t_stats`(읽기 셈) · `t_fail`(실패 호표) · `t_fallback`(식 풀이 셈). +- 앞 조사 부품을 그대로 부름 — C1 `s04_eval` · `s10_all`(정답 · 셈 · run) · C3 `s16_guard.typo_guard`(묶음 ①) · `clause_prefix` · `candidates_nofold`. +- 겹침 — C4(로직 속 지문): 몫 벡터(직종 · 품 · 장비) · 우리 로직 벡터 · 맞음 점수가 같은 재료 → 여기서는 조합(여럿 · 몫 · 호표)으로만 씀 · 줄 하나 지문 순위는 C4 결과와 맞대어 하나로. C6(기억 · 시험 파일 넓히기): STmate · 거창 읽기 흉내(c01) · 씨앗 켬/끔 차이(= 이미 본 조합 기억 효과) · 조합 기억 칸이 겹침. + +--- + + +**부록 C. C6 — 기억 · 시험 파일 넓히기 실측 원문** (원문 `C6_기억실측.md`) + +# C6 — 기억 · 시험 파일 넓히기 실측 (요구 226-4) + +조사 · 실험만 — 저장소 안 고침(끝에 `git status` 깨끗 · HEAD b7e727863). 스크립트 · 중간 자료 = 스크래치패드 `req226/C6/`. +셈은 336 과 같음(고유 줄 · 엄격 / 느슨 · 1순위 · 다섯 안 · 씨앗 켬 · 끔 · 기억 없음) + 새 지표 「로직 조합 맞음」. + +## 0. 결론 + +1. 시험 자료를 3 파일 → 8 파일로 넓힘 — STmate 꼴 5 사업(봉화 · 영월 · 울진대흥 · 영덕계류 · 울진소광) + 거창. 고유 로직 줄 136 → 415(호표에 쓰인 줄 231) · 엑셀 로직 줄 504 → 783. +2. STmate 는 한 읽기 함수로 두 판(25 열 · 45~47 열)을 다 읽음 — 열쇠 칸(12열 자원 코드 · 7열 품 · 23열 이름)이 같음. L 코드 번호는 파일마다 다름(봉화 L00004 = 석공 · 영월 L00002 = 보통인부) → 직종 이름은 파일마다 23열 · 노무비목록표에서 이음(두 곳 어긋남 0 · 23열이 빈 1 줄은 목록표로 채움). +3. 자동 정답(엄격 — 인용 조항 같음 + 직종 · 품 ±3% 가 우리 로직 하나와만 맞음) = 58 줄. 사람 정답과 겹친 38 줄 모두 같음(100%) · 손 확인 20 줄 모두 맞음. 다만 줄 수가 적고(단가산출 줄의 15%) 레미콘 · 유로폼 두 로직이 절반 — 「쉬운 줄」 정답. +4. 지금 맞춤 실력(336 같은 정답 126 줄) = 1순위 71.4 · 다섯 안 86.5 그대로 재현. 영월 직종을 노무 목록으로 이으면 영월 1순위 67.6 → 73.5(씨앗 끔 44.1 → 52.9) — 336 이 「잼 못 함」으로 둔 몫. +5. 처음 보는 STmate · 거창 줄(씨앗 짝 43 줄 · 씨앗 끔) = 1순위 41.9 · 다섯 안 65.1 — 북부청(75%)보다 크게 낮음. 거창은 단가산출 글이 여러 칸에 흩어져 지금 읽기로는 근거 0 → 1순위 0~11%. +6. 기억 열쇠 넷 중 넓게 닿는 것 = (b) 품명 · 단위(앞 파일 모두 기억 시 줄 33%) · 틀림 없는 것 = (c) 로직 속 지문(단위 · 직종:품 · 장비 · 조항 — 서로 다른 줄 49 중 49 맞음 · 다른 발주처 30/30). (a) 지금 열쇠는 8% 만 닿음 · (d) 호표 조합 지문은 2% — 쓸모 없음. +7. 해로움은 「다른 발주처 + 엑셀 인용 조항과 기억 답 조항이 다름(근거 다름)」에 몰림 — (b) 9/20 만 맞음 · (c2) 6/15. 같은 발주처는 열쇠 a · b · c 모두 100%. 해로움 예 = 산림 ↔ 건설 책 갈림(구조물터파기 · 면고르기 · 콘크리트포장 · 야면석붙임 · 지수판). +8. 「경우에 따라서는」 = 근거 판정으로 가름: (a · c) 근거 맞음/인용 없음 → 바로 씀 · (b · c2) → 1순위 후보 · 근거 다름 → 2순위 후보. 앞 파일 모두 기억 흉내에서 1순위 72.1 → 78.6 · 해로움 0 · 바로 들어간 32 줄 모두 맞음(발주처 표시까지 쓰면 80.0 · 39 줄 모두 맞음). 지금 코드(a 만 바로) = 72.1 그대로 · 「a · b 모두 바로」 = 76.4 이나 해로움 6. +9. 새 지표 「로직 조합 맞음」(호표 하나의 로직 줄이 모두 1순위 맞음) — 사람 정답 세 파일 호표 113 중 15(13%) → 기억 가름 33(29%). 자주 되풀이되는 줄 하나가 틀리면 호표 여럿이 함께 틀림 → 기억 효과가 줄 지표보다 큼. +10. 설계 안 = 지금 `맞춤.json`(열쇠 a 하나 · 덮어쓰기)에 둘째 · 셋째 열쇠(b · c) · 출처 파일 · 엑셀 조항 · 로직 판을 덧붙이고, recall 을 「열쇠 차례 a → c → b → c2 + 근거 판정」으로 — 원본 로직 · 일위대가조합.json · M01 은 안 건드림. + +--- + +## 1. 넓힌 자료 + +### 1-1. 파일 표 + +| 파일(발주처 · 해 · 꼴) | 호표 | 줄 | 로직 줄(엑셀) | 고유 로직 줄(호표 쓰임/모두*) | 단가산출 블록(모두/호표 쓰임) | 출처 있음 · 직종 있음(고유) | 호표 = 로직** | 자동 정답 엄격/준엄격 | 씨앗 짝 줄 | +|---|---|---|---|---|---|---|---|---|---| +| 북부청 임도(북부 · 2026 · 엑셀꼴) | 68 | 415 | 271 | 52 / 52 | 단산 블록(C1) | 50 · 47 | — | 19 / 31 | — | +| 북부청 사방(북부 · 2026 · 엑셀꼴) | 27 | 201 | 142 | 48 / 48 | 〃 | 36 · 38 | — | 14 / 25 | — | +| 동부청 영월(동부 · 2024 · STmate 47열) | 23 | 138 | 91 | 36 / 70 | 69 / 35 | 64 · 47 | 2 | 6 / 19 | (사람 정답) | +| 봉화 현동(남부 · 2024 · STmate 25열) | 45 | 203 | 110 | 33 / 67 | 65 / 31 | 44 · 31 | 15 | 7 / 10 | 19 | +| 울진대흥 기번3(남부 · 2024 · STmate 45열) | 34 | 171 | 80 | 18 / 50 | 46 / 14 | 31 · 10 | 16 | 3 / 4 | 9 | +| 영덕 계류보전(남부 · 2025 · STmate 45열) | 9 | 30 | 7 | 3 / 16 | 15 / 2 | 13 · 2 | 7 | 1 / 1 | 0 | +| 울진소광 기번8(남부 · 2025 · STmate 25열) | 25 | 108 | 48 | 20 / 43 | 40 / 17 | 20 · 17 | 9 | 7 / 9 | 6 | +| 거창(남부 추정 · 2025 · 엑셀꼴 흩어진 칸) | 14 | 68 | 34 | 21 / 69 | 61 / 13 | 10 · 36(지금 읽기 0 · 0) | 5 | 1 / 3 | 9 | +| **합** | 245 | 1,334 | 783 | 231 / 415 | | | 54 | 58 / 102 | 43 | + +- \* 모두 = 호표에 안 쓰이고 내역서가 바로 부르는 단가산출 블록까지(단가산출근거목록표로 이름 · 규격 · 단위). 기억은 호표 고르기 창 확정에서만 쌓이므로 기억 흉내는 「호표 쓰임」만 씀. +- \*\* 호표 = 로직 = 노임 줄(L 코드 · 「인」)을 호표에 바로 적은 호표(봉화 「깬잡석찰쌓기」 = 석공 0.08 + 보통인부 0.04 + 굴착기 0.3hr). 줄 맞춤으로는 못 맞추고 호표 머리를 로직 하나에 맞춰야 함(336 방법 40) — C5(조합 풀이)와 겹침. +- 줄 갈래(STmate 코드): D 단가산출 · X 기계 · L 노임 · M 자재 · B 다른 호표 · S 별도계상 · W 일식. 로직 줄 = D · X(엑셀꼴은 단가산출 · 기계 · 기타). + +### 1-2. 못 읽은 파일 · 따로 셈 + +| 파일 | 까닭 | +|---|---| +| 장수 · 진안 「내역+수량」 xlsx 2 | 문서 보안 잠김(파일 머리 「DRMONE」) — 어떤 길로도 못 읽음 | +| 봉화 오솔길 xlsx 1 | 문서 보안 잠김 · 내역 아님(야장 입력) | +| 2016 사방품셈(안) · 수목진료 xls 2 | 일위대가 시트 없음(336) | +| 울진대흥 「신설사업(착공)」 xlsx | 읽힘 — 일위대가 줄 126 가지가 「기번3(설계)」와 모두 같음 → 한 파일로 셈(블록 글은 판 꼴만 다름) | +| 울진대흥 1 · 2 · 3 공구 폴더 | 수량산출만(내역 없음) | + +### 1-3. 읽기에서 알게 된 것 + +- STmate 는 줄 코드로 줄 ↔ 블록 ↔ 노임 · 장비가 이름 없이 이어짐 — 같은 D 코드 = 같은 줄. 블록 안 자원 줄 7열 = 단위당 품(「0.26 * 165,545」의 0.26 · 「3 / (63/2)」의 0.0952)이라 품을 글 풀이 없이 바로 얻음. +- 블록의 30%(영월) ~ 87%(영덕계류)는 기계만(직종 없음 — 봉화 46% · 울진대흥 74% · 울진소광 55%) — 토공 · 채집 · 운반 블록. 직종 근거가 없어 조항만으로 맞춤. +- 거창 단가산출 시트는 글 한 줄이 열 수십 칸에 한 덩이씩 흩어짐 → 지금 읽기(B열만)로는 블록 61 모두 빈 글 · 출처 · 직종 0. 칸을 이어 붙이면 직종 36 줄 · 출처 10 줄(거창은 조항을 거의 안 적음 — 51 줄 출처 없음). + +--- + +## 2. 자동 정답(엄격) + +### 2-1. 규칙 + +- 대상 = 단가산출 줄(블록 있음). 블록 인용 조항(지금 `hb.sources`) 중 하나가 우리 돈 로직 조항과 같은 책 · 같은 번호 → 후보. +- 후보 로직을 입력 조합마다 엔진으로 돌림(고르기 모두 · 범위 두 끝 · 지역 · 기계 고르기는 견본값 · 600 조합 안) → 「인」 줄 직종 묶음 · 품. +- 엑셀 블록 직종 묶음과 같고 직종마다 품이 ±3% 안인 조합이 있는 로직이 **하나뿐이면** 자동 정답 · 아니면 「정답 없음」. +- 준엄격(덧) = 품 값은 안 보고 직종 묶음만 같은 로직이 하나. +- 쉬운 예: 봉화 「레미콘타설(장비) 무근,소형」 블록 = 「건설 6-1-1 · 콘크리트공 3 / (63/2) · 보통인부 1 / (63/2)」 → 우리 GC000198 을 「장비사용 · 무근 · 소량감률 50%」로 돌린 품 0.0952 · 0.0317 과 같음 → 자동 정답 GC000198. + +### 2-2. 파일별 수 · 정답 없음 까닭(단가산출 줄 · 고유 모두) + +| 파일 | 단가산출 줄 | 자동 엄격 | 준엄격 | 출처 없음 | 직종 · 품 없음 | 조항 같은 로직 없음 | 조항 같으나 품 다름 | 둘 이상 | +|---|---|---|---|---|---|---|---|---| +| 북부청 임도 | 51 | 19 | 31 | 1 | 4 | 8 | 19 | 0 | +| 북부청 사방 | 45 | 14 | 25 | 9 | 2 | 2 | 18 | 0 | +| 영월 | 69 | 6 | 19 | 5 | 21 | 11 | 26 | 0 | +| 봉화 | 65 | 7 | 10 | 21 | 21 | 10 | 6 | 0 | +| 울진대흥 | 46 | 3 | 4 | 15 | 24 | 1 | 3 | 0 | +| 영덕계류 | 15 | 1 | 1 | 2 | 12 | 0 | 0 | 0 | +| 울진소광 | 40 | 7 | 9 | 20 | 6 | 4 | 3 | 0 | +| 거창 | 61 | 1 | 3 | 51 | 0 | 2 | 7 | 0 | +| **합** | 392 | **58** | **102** | 124 | 90 | 38 | 82 | 0 | + +- 「조항 같으나 품 다름」 82 = 2024 문서의 옛 판 품 · 소량 할증 · 9:1 섞음 · 채집 + 운반 묶음 — 준엄격이 이 중 44 를 더 받음. + +### 2-3. 맞대 봄 + +| 맞대 본 것 | 엄격 | 준엄격 | +|---|---|---| +| 사람 정답(C1 · 북부청 + 영월)과 겹친 줄 | 38 중 38 같음(100%) | 70 중 68 같음 — 다른 2 = 영월 「기초잡석 · 뒷채움돌 채집운반부설」(사람 정답도 짐작) | +| 씨앗 짝과 겹친 줄 | 18 중 16 같음 — 다른 2 = 「문양거푸집」(씨앗은 산림 12-5 · 엑셀 인용 · 사람 정답은 건설 6-3-4 = 자동과 같음) | 28 중 23 | +| 손 확인 표본(사람 정답 없는 새 줄 20 · 블록 글 · 로직 품 대조) | **20 중 20 맞음** | — | + +- 손 확인 20 줄 = 레미콘 6-1-1(6) · 유로폼 6-3-3(7) · 합판거푸집 6-3-1 · 파형강관 6-7-2 · 뒤채움 3-4-4 · 막자갈채우기 = 뒤채움 대형장비 3-4-5 · 포장 인력포설 1-6-2 · 평떼 = 잔디붙임 4-1-1 · 수축줄눈 = 포장줄눈 절단 1-6-6. 「막자갈채우기 → 뒤채움 및 다짐(대형장비)」처럼 이름은 다르나 로직이 같은 줄도 맞힘. +- 한계: 엄격 58 줄이 로직 24 가지뿐 · GC000198(17) · GC000214(12)가 절반. 조항 · 품이 깨끗한 「쉬운 줄」 정답이라 이 줄의 맞춤 실력은 위 끝(3 절). + +--- + +## 3. 지금 실력(넓힌 자료 · 기억 없음) + +읽기 갈래(STmate · 거창만 바꿈 — STmate 는 지금 400 이라 읽는다고 친 값): 이름만 = 근거 없음 · 출처만 = 336 영월 흉내(직종 빠짐) · 출처직종 = 노무 목록으로 직종 이음(거창은 흩어진 칸 붙임). + +### 3-1. 같은 정답 · 같은 셈(336 의 126 줄) — 칸 = 1순위 엄격 / 느슨 · 다섯 안 엄격 / 느슨 + +| 줄 | 읽기 | 씨앗 켬 | 씨앗 끔 | +|---|---|---|---| +| 세 파일 126 | 출처만(336 그대로) | **71.4 / 81.7 · 86.5 / 94.4** | 67.5 / 77.0 · 86.5 / 93.7 | +| 세 파일 126 | 출처직종 | 73.0 / 82.5 · 86.5 / 94.4 | 69.8 / 77.8 · 86.5 / 93.7 | +| 영월 34 | 출처만 → 출처직종 | 67.6 → 73.5 · 88.2 그대로 | 44.1 → 52.9 · 79.4 그대로 | + +- 336 수치를 그대로 재현함(같은 정답 · 같은 셈 확인). 직종을 이으면 영월 1순위 +5.9(씨앗 끔 +8.8) — 근거 점수 「직종 겹침 0.5」가 살아남. + +### 3-2. 새 줄 + +| 정답 셋 | 줄 | 이름만 켬 / 끔 | 출처만 켬 / 끔 | 출처직종 켬 / 끔 | 다섯 안(출처직종 켬) | +|---|---|---|---|---|---| +| 자동 엄격(사람 정답 없는 새 줄) | 20 | 60.0 / 35.0 | 75.0 / 75.0 | **90.0 / 90.0** | 90.0 | +| 준엄격 덧 | 32 | 46.9 / 28.1 | 62.5 / 59.4 | 71.9 / 71.9 | 71.9 | +| 씨앗 짝(씨앗 끔만 — 처음 보는 셈) | 43 | — / 32.6 | — / 41.9 | — / 41.9 | 65.1(끔) | + +- 자동 엄격에서 틀린 2 줄 = 「같음」이 다른 책을 들임(파형강관 → 산림 GF000176 · 합판거푸집 3회 → 산림 GF000160 · 엑셀은 건설) — 336 묶음 ① 방법 4 대상. 근거가 깨끗한 줄은 근거만 읽히면 거의 다 맞음. +- 씨앗 짝 43 줄(봉화 19 · 울진대흥 9 · 울진소광 6 · 거창 9) = 실무자가 이름을 바꾼 어려운 줄 — 씨앗 끔 1순위 봉화 42.1 · 울진대흥 66.7 · 울진소광 50.0 · 거창 11.1(근거 없음). 영월 씨앗 끔(52.9)과 비슷 → 「처음 보는 STmate 파일 실력 ≈ 40~55%」. + +### 3-3. 새 지표 — 로직 조합 맞음(호표의 판정 줄이 모두 1순위 맞음) + +| 바탕 | 사람 정답 세 파일 호표(113) | 넓힌 8 파일 호표(154) | +|---|---|---| +| 출처직종 · 씨앗 켬 · 기억 없음 | 15(13%) — 임도 5/65 · 사방 1/26 · 영월 9/22 | 55(36%) | +| 〃 + 기억(근거 가름 · 4-6) | 33(29%) — 임도 17 · 사방 5 · 영월 11 | 73(47%) | +| 〃 + 기억(근거 가름 + 발주처) | 35(31%) | 75(49%) | + +- 북부청 임도는 줄 1순위 71% 인데 호표 조합은 8% — 「레미콘타설(장비) 소량」 · 「합판거푸집 4회」처럼 호표 10~13 곳에 되풀이되는 줄 하나가 틀리면 그 호표가 모두 틀림. 기억이 그런 줄을 한 번에 고쳐 조합 지표가 두 배 넘게 오름. +- STmate 호표는 자동 정답(쉬운 줄)만 판정이라 높게 나옴 — 사람 정답 세 파일 칸이 바른 비교. + +--- + +## 4. 기억 흉내 — 열쇠별 수치 · 해로움 + +### 4-1. 흉내 방법 + +- 「앞 파일에서 사람이 확정했다」 = 앞 파일 줄 정답(사람 · 자동 엄격) + 씨앗 짝(씨앗을 만든 파일 줄 = 사람이 고른 것)을 기억에 넣음. 판정은 사람 · 자동 엄격 정답만(씨앗은 판정에 안 씀). +- 바탕 = 지금 맞춤 · 출처직종 · **씨앗 끔**(씨앗을 기억 쪽으로 옮겨 셈 — 씨앗 출처 파일 오염 없음). 기억이 닿으면 그 답을 1순위로. +- 열쇠: **a** 품명|규격|단위(지금 `line_key`) · **b** 품명|단위 · **c** 로직 속 지문 = 단위 | 직종:품(유효 3자리) | 장비(굴착기 · 덤프트럭 …) | 조항 — 금액 안 씀(엑셀꼴 품은 「금액 ÷ 단가」로 되찾기만) · **c2** = 단위 | 직종:품만 · **d** 호표 조합 지문 = 그 호표 로직 줄 지문 묶음 + 자리. c · c2 는 C4(로직 속 지문)와 겹침 · d 는 C5(조합 풀이)와 겹침. +- 쉬운 예(c): 봉화 「레미콘타설(장비) 철근」과 영월 「철근콘크리트타설 장비사용타설」은 이름이 다르나 지문이 같음(m3 | 콘크리트공 0.0545 · 보통인부 0.0182 | 굴착기 | 건설 6-1-1) → 봉화에서 확정한 GC000198 이 영월에서 1순위(지금은 「콘크리트 펌프차 타설」이 1위). + +### 4-2. 장면별(호표 쓰임 줄 · 칸 = 기억 닿음 / 그중 맞음 · 1순위 전 → 뒤 · 해로움) + +| 장면 | 판정 줄 | a | b | c | c2 | d | +|---|---|---|---|---|---|---| +| 북부청 임도 → 사방(같은 발주처 · 해) | 43 | 4/4 · 77→77 · 0 | 15/15 · 77→**88** · 0 | 15/15 · 77→81 · 0 | 20/20 · 77→88 · 0 | 0 | +| 북부청 사방 → 임도 | 49 | 4/4 · 76→76 · 0 | 18/18 · 76→**90** · 0 | 16/16 · 76→84 · 0 | 23/23 · 76→94 · 0 | 0 | +| 울진대흥 ↔ 봉화(남부 · 2024) | 10 | 4/4 | 4/4 | 7/7 | 7/7 | 2/2 | +| 남부 2024 → 남부 2025(해 바뀜) | 7 | 2/2 | 3/3 | 4/4 | 4/4 | 1/1 | +| 북부청 → 영월(다른 발주처) | 34 | 2/1 · 53→53 | 13/8 · 53→**44** · **4** | 4/4 · 53→59 · 0 | 11/8 · 53→56 · 2 | 0 | +| 영월 → 북부청(다른 발주처) | 92 | 2/1 · 76→75 · 1 | 17/11 · 76→75 · **6** | 8/8 · 76→78 · 0 | 17/11 · 76→76 · 2 | 0 | +| 남부 2024 · 2025 → 북부청(다른 발주처 · 해) | 92 | 2/2 | 9/8 · 76→83 · 0 | 13/13 · 76→78 · 0 | 14/13 · 76→77 · 1 | 0 | + +- 남부 STmate 파일끼리는 판정 줄(자동 정답)이 적어 맞음만 봄 — 모두 맞음. + +### 4-3. 차례 누적(울진대흥 → 봉화 → 영월 → 울진소광 → 영덕계류 → 거창 → 북부청 임도 → 사방 · 앞 파일 모두 기억) + +| 열쇠 | 닿음(앞 줄 모두 확정이라 치면 · 고유 213) | 엑셀 줄 무게 | 판정 140 줄 중 닿음 · 맞음 | 1순위 전 → 뒤 | 다섯 안 전 → 뒤 | 도움 · 해로움 · 충돌 | +|---|---|---|---|---|---|---| +| a(지금) | 17(8%) | 9% | 12 · 11 | 72.1 → 72.1 | 87.1 → 87.9 | 1 · 1 · 0 | +| b | 71(33%) | 46%(a→b) | 42 · 35 | 72.1 → 76.4 | 87.1 → 91.4 | 12 · 6 · 1 | +| c | 43(20%) | 26%(a→c) | 33 · 33 | 72.1 → 76.4 | 87.1 → 88.6 | 6 · 0 · 0 | +| c2 | 54(25%) | — | 48 · 42 | 72.1 → 77.1 | 87.1 → 90.7 | 10 · 3 · 4 | +| d | 4(2%) | — | 2 · 2 | 그대로 | 그대로 | 0 | +| a → c → b → c2 사슬 | 96(45%) | 61% | 4-6 표 | | | | + +- 충돌 때 「최근」 대 「다수」 — 차이 없음(충돌 열쇠가 b 1 · c2 4 뿐). +- 열쇠 a 가 거의 안 닿는 까닭 = 규격 칸이 파일마다 다름(「굴삭기 0.2m3」 ↔ 「굴착기0.6m3」 · 「4회」 ↔ 「(4회)」). + +### 4-4. 조건별 맞음(짝 모두 + 차례 누적 · 서로 다른 판정 줄 · 답으로 셈) + +| 조건 | a | b | c | c2 | +|---|---|---|---|---| +| 모두 | 19/21(90%) | 60/73(82%) | **49/49(100%)** | 68/78(87%) | +| 같은 발주처 | 14/14 | 42/42 | 42/42 | 54/54 | +| 다른 발주처 | 5/7 | 28/41(68%) | 30/30 | 39/49(80%) | +| 근거 맞음(기억 답 조항 = 엑셀 인용 조항) | 18/18 | 46/47(98%) | 45/45 | 53/53 | +| 인용 없음 | 0/1 | 3/4 | — | 2/3 | +| 근거 다름 | 1/2 | 11/22(50%) | 4/4 | 13/22(59%) | +| 다른 발주처 · 근거 다름 | 1/2 | **9/20(45%)** | — | **6/15(40%)** | +| 다른 발주처 · 근거 맞음 | 4/4 | 17/18(94%) | 30/30 | 32/32 | +| 사람 정답으로만 판정 | 12/14 | 50/63 | 36/36 | 55/65 | +| 같은 열쇠 다른 답(충돌) | 0 | 1 열쇠 · 0/1 | 0 | 8 번 중 1 맞음(12%) | + +- c 의 100% 는 사람 정답 판정(36/36)에서도 같음 — 자동 정답 판정 줄은 지문과 정답이 같은 재료(조항 · 품)라 맴돎이 있어 사람 판정 칸을 근거로 봄. +- 한 파일 안 같은 열쇠 정답이 갈린 묶음: b 17 중 0 · c 6 중 1(영월 「기초잡석 · 뒷채움돌 채집운반부설」 — 블록 내용은 같고 사람 정답이 짐작으로 갈림). + +### 4-5. 해로움 예(다른 발주처 · 근거 다름) + +| 기억(앞 파일) | 다음 파일 줄 | 기억 답 → 정답 | +|---|---|---| +| 영월 「구조물터파기 토사(B/H 90%+인력10%)」(임도품셈 5-1-2 · 산림) | 북부청 사방 「구조물터파기 인력」(건설 3-2-1) | 산림 GF000101 → 건설 GC000083(열쇠 b) | +| 영월 「면고르기」(인용 없음 · 산림) | 북부청 임도 「면고르기」(건설 3-5-1) | 산림 GF000124 → 건설 GC000111(**열쇠 a — 지금 코드도 바로 넣음**) | +| 영월 「콘크리트포장(인력) T=20㎝」(산림 7-1-1) | 북부청 임도 「콘크리트포장(인력) T=20cm」(건설 1-6-2) | 산림 GF000162 → 건설 GC000297(b) | +| 북부청 사방 「지수판 설치」(산림 12-27-1) | 영월 「지수판 설치(PVC 용접)」(건설 6-3-9) | 직종 · 품이 같아 c2 가 섞음 → c(조항 포함)는 가름 | + +- 모두 「같은 일 다른 책」 — 발주처마다 쓰는 책이 다름. 엑셀이 인용한 조항과 기억 답의 조항을 맞대면(근거 판정) 모두 걸러짐. + +### 4-6. 가름 규칙 흉내(차례 누적 · 판정 140 줄 · 바탕 씨앗 끔 1순위 72.1 · 다섯 안 87.1) + +| 규칙 | 1순위 | 다섯 안 | 바로 들어감(맞음) | 1순위 후보(맞음) | 2순위 후보(맞음) | 도움 · 해로움 | +|---|---|---|---|---|---|---| +| 지금(a 만 · 바로) | 72.1 | 87.9 | 12(11) | — | — | 1 · 1 | +| a · b 바로(336 방법 22 를 바로 씀으로) | 76.4 | 91.4 | 42(35) | — | — | 12 · 6 | +| 모든 열쇠 바로 | 77.1 | 92.1 | 63(51) | — | — | 15 · 8 | +| **근거 가름** | **78.6** | 92.1 | 32(32) | 12(11) | 19(8) | 9 · **0** | +| 근거 가름 + 발주처 표시 | 80.0 | 92.1 | 39(39) | 7(6) | 17(6) | 11 · 0 | + +- 근거 가름 = 열쇠 차례 a → c → b → c2 · 첫 닿음만 씀: 근거 다름 → 2순위 후보 · a · c 이면서 근거 맞음/인용 없음 → 바로 씀 · b · c2 이면서 근거 맞음/인용 없음 → 1순위 후보(확정 한 번). + 발주처 = 같은 발주처 기억이면 a · b · c 를 근거와 상관없이 바로(같은 발주처 100% 라서). + +### 4-7. 호표 = 로직 54 개(노임을 바로 적은 호표) — 다른 파일과 닿음 + +| 열쇠 | 닿음 | +|---|---| +| 호표 이름 · 단위 | 54 중 11(20%) | +| 호표 지문(단위 · 노임 직종:수량 · 장비) | 54 중 23(43%) | + +- 정답이 없어 맞음은 못 잼. 이름을 바꿔도 지문은 같아 두 배 닿음 — 호표 머리 맞춤(336 방법 40 · C5)을 만들면 같은 지문 기억이 그대로 쓰임. + +--- + +## 5. 기억 설계 안 + +### 5-1. 한눈에 + +| 지금(`M02_Hopyo_Match` · `M02_Hopyo_Store`) | 안 | +|---|---| +| 열쇠 a 하나(품명 · 규격 · 단위) | 열쇠 a · b · c(+ c2) — 한 번 확정에 셋을 함께 담음 | +| 닿으면 바로 씀(상태 「기억」 · 사전조합에 바로 들어감) | 근거 판정으로 바로 씀 / 1순위 후보 / 2순위 후보 가름(5-3) | +| 같은 열쇠 다른 답 = 덮어씀(마지막만) | 답마다 출처 파일 · 횟수 · 엑셀 조항을 남기고 근거로 고름(5-5) | +| 출처 · 판 안 남김 | 출처 파일 · 확정 때 · 사람 · 엑셀 인용 조항 · 로직 원문번호 · 판 · 입력 이름 | +| 층 차례 = 같은 층 → 개인 → 회사 → 마스터(있음) | 그대로 + 층 올림 기준(5-4) | + +### 5-2. 한 기억에 담을 것(`맞춤.json` 「줄」 값 덧칸) + +`{로직|자재, 입력값, 환산, 확정}`(지금) + `열쇠2`(b) · `지문`(c) · `지문2`(c2) · `파일`(실무자료 이름) · `발주처`(올릴 때 고르는 칸 — 없으면 빈 값) · `엑셀조항`([[책, 번호]]) · `판`(로직 원문번호 · 마스터 판 · 입력 이름 목록) · `답들`(같은 열쇠에 확정된 다른 답 · 횟수). 읽을 때 b · c 찾기 표를 한 번 만듦(파일은 지금처럼 a 열쇠 한 벌). + +### 5-3. 경우에 따라 — 바로 씀 / 후보로만 (수치로 가름) + +| 경우 | 맞음(4-4) | 처리 | 화면 | +|---|---|---|---| +| a 또는 c 닿음 + 근거 맞음 · 인용 없음 | a 18/18 · c 45/45 | **바로 씀**(지금 「기억」 그대로 · 들어감) | 「기억 · 같은 줄」 / 「기억 · 같은 로직」 꼬리표 | +| b 또는 c2 닿음 + 근거 맞음 | b 46/47 · c2 53/53 | **1순위 후보**(확정 한 번) | 후보 단추 「기억 · 같은 품명 — 봉화 파일」 | +| b · c2 + 인용 없음 | 3/4 · 2/3 | 1순위 후보 | 「기억 · 근거 없음」 | +| 같은 발주처 기억(발주처 칸이 같을 때) | a · b · c 98/98 | 바로 씀(근거 다름이어도) | 「기억 · 같은 발주처」 | +| 어느 열쇠든 근거 다름 · 다른 발주처 | b 9/20 · c2 6/15 | **2순위 후보**(1순위는 지금 맞춤) | 「기억 · 근거 다름(산림 ↔ 건설)」 | +| 같은 열쇠 답 둘 넘음(충돌) | c2 1/8 | 바로 안 씀 · 답마다 후보 + 횟수 | 「기억 둘 — A 3번 · B 1번」 | +| d 호표 조합 | 닿음 2% | 안 만듦 | — | + +- 근거 판정 = 엑셀 줄 인용 조항(`근거.출처`)과 기억 답 로직 조항(`hb.ours()`)을 같은 책 · 같은 번호로 맞대 봄 — 330 근거 코드에 이미 있는 재료. +- 흉내 결과: 1순위 72.1 → 78.6 · 해로움 0(4-6). 지금 코드도 열쇠 a 를 다른 발주처 파일에 바로 넣어 틀린 예가 있음(면고르기) → a 에도 근거 판정을 붙임. + +### 5-4. 층 + +- 개인 층 = 내가 확정한 것(기본 자리 · 지금 그대로). 회사 층 = 회사 관리자 확정 + 개인 기억 올림. +- 올림 기준(안): 같은 답이 서로 다른 파일 둘 넘게 확정되고 충돌 없음 → 「회사로 올림」 단추에 표시(관리자가 누름) — 두 파일 넘게 같은 답 b 9/10 · c2 19/19. 자동 올림은 하지 않음(회사마다 쓰는 책이 다름). +- 마스터 층 = 씨앗(일위대가조합.json 메모 · 읽기만) + 마스터 관리자 확정. 이름 열쇠(a · b) 마스터 기억은 다른 발주처에서 68% 라 늘 근거 판정을 거침(씨앗의 북부청 해 −4.1 · −2.3 도 같은 까닭 — 336 묶음 ① 4 와 같은 고침). c 열쇠는 다른 발주처 30/30 → 마스터에 둬도 됨. +- 층이 갈려 답이 다르면 아래 층(개인)이 이김 · 「회사 기억과 다름」 꼬리표. + +### 5-5. 같은 열쇠 다른 답 + +- 덮어쓰지 않고 `답들`에 쌓음(출처 파일 · 횟수 · 엑셀 조항). +- 부를 때: 답들 중 근거 맞는 답이 하나면 그 답(5-3 처리) · 둘 넘거나 없으면 바로 안 씀 → 후보로 모두 보임. 예: 「구조물터파기|㎥」 = 북부청 파일 GC000083(건설 3-2-1) · 영월 파일 GF000101(산림) → 건설 3-2-1 을 인용한 줄엔 GC000083 · 임도품셈을 인용한 줄엔 GF000101. + +### 5-6. 「기억에서 옴」 표시 + +- 줄 상태 칸: 지금 「기억」 하나 → 「기억 · 같은 줄 / 같은 품명 / 같은 로직」 + 출처 파일 이름(마우스 올리면 확정 때 · 사람 · 층). +- 후보 단추: 기억에서 온 후보에 「기억」 표 + 근거 판정 글(「근거 맞음 · 건설 6-1-1」 / 「근거 다름 — 엑셀 산림 13-5-2」). +- [같음 모두 확정] · [근거 맞음 모두 확정] 에 「기억 후보 모두 확정」을 따로 두지 않음 — 바로 씀 줄은 이미 들어가고 나머지는 사람이 봄. + +### 5-7. 잊기 + +- 줄마다 [기억 지움](지금 confirm 에 null — 있음) + 층별 기억 목록(파일 · 로직 · 날짜로 거름 · 여럿 지움) — 작은 새 창. +- 바로 쓴 기억을 사람이 다른 답으로 고치면: 덮어쓰지 않고 `답들`에 「고쳐짐」 횟수 → 그 열쇠는 다음부터 바로 안 씀(후보로만). +- 실무자료 파일을 지워도 기억은 남김(지금 그대로) · 출처에 「지운 파일」. +- 시간으로 저절로 잊기는 안 둠(자료에 근거 없음). + +### 5-8. 우리 로직 판이 바뀔 때 + +- 확정 때 로직 원문번호 · 마스터 판(건설 「2026」 · 산림 「2026-01-01」) · 입력 이름 목록을 함께 담음. +- 부를 때 비교: 로직 키 없어짐 → 기억 버림 + 「기억한 로직 없어짐」 알림 · 원문번호 · 판 · 입력 이름이 바뀜 → 바로 씀을 1순위 후보로 내림 + 「로직 판 바뀜 — 입력값 다시」 · 없어진 입력은 입력값에서 뺌. +- c 지문은 바깥 엑셀 내용이라 판이 바뀌어도 열쇠는 그대로 · 답만 다시 봄. 원본 로직 · 일위대가조합.json 은 손대지 않음. + +### 5-9. 들 일 · 차례 + +| 차례 | 할 것 | 크기 | +|---|---|---| +| 1 | recall 에 근거 판정 · 열쇠 b(품명 · 단위) · 1순위/2순위 후보 갈래 · 충돌 쌓기 — 맞춤 서버 두 파일 + 시험 | 작음~중간 | +| 2 | 블록 품 뽑기(엑셀꼴 = 「금액 ÷ 단가」 · STmate = 7열) + 열쇠 c · c2 | 중간 — STmate 읽기(336 묶음 ④)와 같이 | +| 3 | 화면 꼬리표 · 기억 목록(잊기) · 올림 단추 · 발주처 칸 | 중간 | + +- 336 묶음 ①(「같음」도 근거 검사)과 같은 재료(조항 판정)를 씀 → 같이 하면 씨앗 · 기억 · 같음 세 길이 한 판정 함수를 거침. + +--- + +## 6. 한계 + +- 판정 정답이 사람 정답(북부청 · 영월 126 줄)에 몰림 — 새 STmate 파일은 자동 엄격 20 줄 · 씨앗 짝 43 줄뿐이라 새 파일 수치는 n 이 작음(표에 n 적음). +- 자동 정답은 「조항 · 품이 깨끗한 쉬운 줄」만 · 로직 24 가지 · 레미콘 · 유로폼이 절반 — 어려운 줄(이름 바뀜 · 옛 판 · 섞인 채집)은 정답 없음으로 남음. +- 씨앗을 「앞 파일 사람 확정」으로 씀 — 씨앗은 브레인이 만든 짝이고 「옛 판 값」 짝이 섞임. +- 발주처: 봉화 · 거창은 관할로 남부청 짐작 · 같은 발주처라도 설계사가 다를 수 있음 · 해 안의 파일 차례는 짐작. +- c 지문은 품이 유효 3 자리까지 같아야 함 → 품셈 개정(2024 → 2026)으로 품이 바뀐 줄은 해가 바뀌면 안 닿음(같은 발주처 · 다른 해 c 닿음 7%). +- 엑셀꼴에서 품이 안 잡힌 줄(기계만 블록 · 「금액 ÷ 단가」 꼴 아닌 글 포함): 북부청 임도 6 · 사방 11 · 거창 33 → 이 줄은 열쇠 c · c2 가 없음. +- 기억이 1순위로 들어간 뒤 사람이 고르는 손은 안 잼(고르기 창 일). + +## 7. 코드 자리 · 확인 · 실험 경로 + +| 할 것 | 자리 | +|---|---| +| 열쇠 a(지금) · b · c 함수 | `M02_MasterTemplete/M02_Hopyo_Match.py` `line_key` :68-76 | +| 기억 부르기 · 가름 | 같은 파일 `recall` :348-350 · `match_line` :379-394(상태 「기억」 · 들어감) | +| 확정 저장 · 덧칸 | 같은 파일 `confirm` :495-526 · `M02_Hopyo_Store.py` `memory` · `remember` :123-140(`실무자료/맞춤.json`) | +| 근거 판정 재료 | `M02_Hopyo_Basis.py` `ours` :158 · `_clause` :166-176 · 블록 품은 `read` :84-125 직종 줄 고리에 덧 | +| 층 차례 | `M02_Hopyo_Match.py` `chain` :333-341 · 확정 길 = `M02_Labor_Router.py` `hopyo_memory` :283-292 | +| 화면 꼬리표 | `M02_MasterTemplete_Hopyo_View.ts` `STATE` :45-53 · `M02_MasterTemplete_Hopyo_Api.ts` :93 | +| STmate 읽기 · 거창 흩어진 칸 | `M02_Hopyo_Excel.py` `_sheets` :74-102 · `M02_Hopyo_Basis.py` `read`(B열만 읽음) | +| 시험 | `resources/tester/test_m02_hopyo_match.py` · `test_m02_hopyo_basis.py` | + +- 원본 로직 안 고침 확인: 우리 로직 품은 엔진 `mf.run`(같은 프로세스 · 읽기)으로 셈 · 씨앗 끔은 `hm._seeds` 를 같은 프로세스에서 바꿔 끼움 · `resources/master_data` · `일위대가조합.json` · 실무문서는 읽기만 · 끝에 저장소 `git status` 변경 0. +- 겹침: c · c2 지문 · 우리 로직 품 벡터(`s04_auto.vectors`) = C4(로직 속 지문)와 겹침 · d 호표 조합 · 호표 = 로직 54 = C5(조합 풀이)와 겹침 — 함수는 따로 만듦. +- 실험 경로(스크래치패드 `req226/C6/` · 저장소 `venv/Scripts/python.exe -B`): + +| 스크립트 | 하는 일 · 결과 | +|---|---| +| `s02_read.py` | STmate 6 · 거창 읽기 → `data_<파일>.json` | +| `s03_items.py` | 고유 줄 · 블록 속(출처 · 직종 · 품 · 장비) → `items.json` · `hopyo_units.json` | +| `s04_auto.py [loose]` | 자동 정답 → `auto.json` · `auto_loose.json` · 로직 품 벡터 `vec_cache.json` | +| `s05_handcheck.py` | 손 확인 표본 20 줄 보기 | +| `s06_match.py` · `s07_score.py` | 지금 맞춤 6 갈래 → `match.json` · 실력 표 `score.json` | +| `s08_memory.py [모두] [준엄격]` | 기억 흉내(장면 · 누적 · 조건) → `memory.json` | +| `s09_examples.py` · `s13_distinct.py` | 도움 · 해로움 예 · 서로 다른 줄 셈 | +| `s10_policy.py` · `s11_extra.py` · `s12_combo.py` | 가름 규칙 · 엑셀 줄 무게 · 로직 조합 맞음 | diff --git a/docs/raw/조사/2026-10-15_336_재검토_로직조합맞춤.md b/docs/raw/조사/2026-10-15_336_재검토_로직조합맞춤.md new file mode 100644 index 000000000..8fe5cc94a --- /dev/null +++ b/docs/raw/조사/2026-10-15_336_재검토_로직조합맞춤.md @@ -0,0 +1,313 @@ +# 외부 일위대가 맞춤 — 재검토 · 로직 조합 기준 (요구 226-4 · PLAN 336-3) + +재료 = C4 로직 지문 · C5 조합 풀이 · C6 기억 실측(스크래치패드 `req226/` · 원문 = 같은 폴더 `2026-10-15_336_부록3_재실측원문.md`) + 336 검토(`docs/raw/조사/2026-10-15_336_외부일위대가_이름맞춤_검토.md`) + C7 덧잼(세 실험 함수를 한 프로세스에 불러 겹쳐 잼 · `req226/C7/`) · 기준 main_laptop_1 42e4aa70c(맞춤 · M01 · master_data 는 C4~C6 기준 b7e727863 과 같음) · 저장소 고침 없음 · 원본 로직 · 일위대가조합.json 읽기만. + +## 0. 결론 요약 + +1. 사용자 말이 맞음 — 실무자는 이름 · 인용 조항은 편의대로 거의 다 바꾸지만 직종 묶음 · 품(로직 속)은 거의 그대로 둠 → 이름 · 책이 아니라 「어떤 로직(의 조합)인가」로 맞춰야 함. +2. 원본 로직은 그대로 두고 맞춤 쪽만 묶음을 쌓으면 — 1순위 71.4% → 92.9% · 다섯 안 86.5% → 96.8% · 호표 통째 조합 맞음 9.6% → 75.9%. +3. 몫이 큰 것 — 근거 고침(1순위 85.7%) · 조합 풀이(조합 맞음 30.1% → 75.9%) · 로직 지문(다섯 안 96.8% · 처음 보는 셈 1순위 80.2% → 85.7%). +4. 지문과 조합 풀이를 그냥 겹치면 조합 맞음이 49.4% 로 깎임 — 「섞인 줄(기계 90% + 인력 10%)은 몫마다 먼저 풂」 규칙을 넣어야 둘이 더해짐. +5. 기억은 맞춤이 약할 때 큰 도움(지금 위 1순위 71.4% → 80.2%) · 다 쌓은 뒤엔 같은 줄을 이미 맞혀 1순위 그대로 — 대신 사람 확인 없이 바로 들어가는 줄이 생김(틀림 없음). +6. 「딱 맞음」(직종 · 품이 그대로 같음) 표시는 가린 파일에서도 거의 다 맞음 → 금액 비 대신 초록 표시 · 한꺼번에 확정 후보. +7. 넘침 확인 — 임도 하나로만 손잡이를 정해도 사방 1순위 90.7% 그대로 · 영월 88.2% → 85.3% · 새 파일 같거나 오름 → 부풀린 값 아님 · 다만 무게를 한 파일로 정하면 크게 흔들림. +8. 처음 보는 STmate · 거창 파일은 아직 약함 — 1순위 41.9% → 쌓아서 65.1~72.1% · 읽기 넓힘이 있어야 지문 · 기억이 돎. +9. 추천 차례 — ① 근거 고침 → ② 로직 지문 → ③ 조합 풀이 → ④ 조건부 기억 · ⑤ 읽기 넓힘은 새 문서를 곧 쓰면 ①과 나란히 · ⑥ 고르기 창은 ②부터 함께. +10. 정할 것 — 앞 11 중 넷은 이번 말로 풀림 · 새로 넷(사전조합 「한 줄 = 담은로직 여럿」 · 딱 맞음 한꺼번에 확정 · 우리에 없음 줄 · 발주처 칸). + +--- + +## 1. 사용자 말 확인 — 실무자가 편의대로 바꾼 꼴 + +### 1-1. 꼴 표 — 정답 있는 126 줄(한 줄에 여럿 · 정답 로직의 가장 닮은 입력 조합과 견줌 · C4) + +| 꼴 | 줄 | 쉬운 예 | +|---|---|---| +| 이름 다름 | 116 (92%) | 「레미콘타설(장비사용)」 ↔ 우리 「레디믹스트콘크리트 타설」 | +| 이름만 다름(조항 · 품 같음) | 40 | 「합판거푸집(4회)」 = GC000211 「유형=보통」 그대로 | +| 묶음(우리 로직에 없는 장비 · 일을 더함) | 40 | 채집 + 상차 + 운반을 한 줄로 | +| 계수(품) 바꿈 | 32 | 토질 셋 평균 · 옛 판 값 · 두 작업을 한 품으로 | +| 섞음(기계 % + 인력 %) | 18 | 고임돌 9 : 1 — 인력 0.3 × 10% | +| 다른 책 조항 인용 | 18 | 고임돌 「건설 1-10」(단위중량 표를 인용) | +| 단위 바꿈 | 12 | 개소 ↔ 본 · ㎡ ↔ 개 | +| 출처 없음 | 11 | 철골설치 · 사방 9 : 1 채집 | +| 옛 판 · 없는 번호 | 5 | 「임도품셈 5-5」 · 「사방품셈 P52」 | +| 할증 · 횟수 붙임 | 4 | 품할증 0.75 · × 2회 | +| 직종 바꿈 | 3 | 다웰바(철근공 더함) | +| 쪼갬 | 2 | 원형돌망태 조립 + 채움 두 줄 = 우리 로직 하나 | + +- 품 판정 — 품 그대로 72 줄(같음 58 · 조합 같음 5 · 섞음 · 할증만 붙고 밑품 같음 9) · 계수 바뀜 32 · 단위 달라 못 견줌 12 · 그 밖 10. +- 읽는 법 — 이름은 거의 다 · 조항은 넷에 하나꼴로 바꾸지만 직종 묶음을 바꾼 줄은 3 뿐 · 품은 절반 넘게 그대로 → 「어떤 로직인가」는 직종 · 품 꼴이 가장 잘 말함. 우리 서버 이름을 바꿔도 다음 문서 작성자가 또 바꾸므로 같은 문제(사용자 말 그대로). + +### 1-2. 같은 일 다른 책 — 숫자는 인용한 책 + +- 지금 실패 중 책 갈래 11 줄 — 품 숫자가 모두 엑셀이 인용한 책과 같음(건설 7 · 산림 1 · 두 책 같은 품 2 · 장비만 건설 1) → 작성자는 책을 바르게 씀 · 틀린 쪽은 우리 「이름 같음」 자동 연결. +- 반대로 「조항은 건설 · 숫자는 산림」 9 줄(고임돌 · 조약돌 4 · 야면석 채집 3 · 되메우기 · 야면석메붙임) → 조항은 한 신호일 뿐 · 숫자(로직 속)가 정답 책을 가름. +- 쉬운 예 — 합판거푸집 N001(건설 6-3-1 인용 · 형틀목공 0.111 · 보통인부 0.044) = 건설 GC000211(0.112 · 0.045) · 산림 GF000160 은 가장 닮은 조합도 0.104 · 0.057. + +### 1-3. 닫힌 세계 — 바깥 줄은 셋 중 하나 + +- 8 파일 · 호표 335 · 줄 1,388 을 모두 갈래로 나눔(C5) — 단가산출 735 · 다른 호표 참조 90 · 노임 105 · 장비 36 · 자재 219 · % 30 · 별도계상 135 · 근거글 38 → 「우리 로직(조합) · 우리 일위대가 · 단가 · % 줄」 밖으로 안 나감. +- 정답 세 파일 「우리에 없음」 = 136 중 10 — 잔토처리 · 잔토정리 6(우리 굴착 로직의 입력으로만 있음) · 노임 줄 2 · 고화매트 · 철강재 운임. + +### 1-4. 「로직 X · 계수 바뀜」으로 보임 + +지문 1위가 맞은 103 줄 — 품 같음 56 · 조합 같음 4 · 계수 바뀜 23 · 섞음 · 할증 3 · 직종 다름 3 · 단위 달라 8 · 그 밖 6. + +| 엑셀 줄 | 보일 글 | 까닭(엑셀 블록) | +|---|---|---| +| 구조물터파기(인력100%) 토사 | GC000083 굴착(인력/토사) · 계수 바뀜 특별인부 0.370 → 0.354 | 토사 셋 평균 | +| 임도안전표지판설치 | GC000308 교통 안전표지판 설치 · 특별인부 0.167 → 0.258 | 지주 · 표지판 두 작업을 한 품 | +| 구조물되메우기(인력100%) | GF000113 되메우기 · 섞음 비율 10% → 100% · 품 같음 0.1 | 산림 9-14 섞음 비율 무시 | +| 철근가공조립(현장) | GC000206 현장가공 + GC000207 현장조립 · 조합 같음 | 건설 6-2 윗 조항 하나로 | + +--- + +## 2. 누적 실력 · 가림 시험 + +같은 정답 · 같은 셈 = 336 · C4~C6 와 같음(고유 줄 · 정답 있는 126 줄 · 엄격 / 느슨 · 1순위 · 다섯 안). 조합 맞음 = C5 T1(정답 세 파일 호표 중 덮을 수 있는 83 · 호표 안 로직 칸이 다 차고 남는 로직 없음 · 엄격) · 칸 = 찬 칸 비율. + +### 2-1. 누적 표 — 씨앗 켬(지금 셈) + +| 단계 | 1순위 엄격 / 느슨 | 다섯 안 엄격 / 느슨 | 1순위 임도 · 사방 · 영월 | 조합 맞음(83) | 칸 | +|---|---|---|---|---|---| +| 지금 | 71.4 / 81.7 | 86.5 / 94.4 | 71.4 · 74.4 · 67.6 | 9.6 (8) | 59.1 | +| ① 근거 고침 | 85.7 / 88.1 | 94.4 / 94.4 | 91.8 · 86.0 · 76.5 | 27.7 (23) | 77.7 | +| ② + 로직 지문 | 88.9 / 90.5 | 96.8 / 96.8 | 91.8 · 86.0 · 88.2 | 30.1 (25) | 81.5 | +| ③ + 조합 풀이(섞인 줄 몫 먼저) | **92.9 / 94.4** | **96.8 / 96.8** | 98.0 · 90.7 · 88.2 | **75.9 (63)** | 95.1 | +| ④ + 조건부 기억(근거 가름) | 92.9 / 94.4 | 96.8 / 96.8 | 98.0 · 90.7 · 88.2 | 75.9 (63) | 95.1 | +| ⑤ + 읽기 넓힘(영월 직종 이음) | 92.1 / 93.7 | 96.8 / 96.8 | 98.0 · 90.7 · 85.3 | 75.9 (63) | 95.1 | +| (참고) C5 길만(① + (가) + 조합 풀이) | 91.3 / 93.7 | 94.4 / 94.4 | 98.0 · 90.7 · 82.4 | 72.3 (60) | 91.2 | +| (참고) ② 위에 조합 풀이를 그냥 겹침 | 89.7 / 91.3 | 96.8 / 96.8 | 91.8 · 88.4 · 88.2 | 49.4 (41) | 85.5 | + +- ① = 336 묶음 ①(조항 윗마디 · 같은 이름 접힘 고침 · 조항 오타 · 「같음」도 근거 검사) — 원본 로직이 아니라 **맞춤 코드**(M02 맞춤 서버 파일) 고침. +- ② = C4 추천(「같음」은 ① 그대로 · 나머지 = 지문 점수 + ① 차례 덤 β=1). ③ = 섞인 줄 11 은 C5 길(① + (가) 1순위 → 몫 풀이 → 인력 몫으로 바꿈) · 나머지 줄은 ② 1순위 + 못 덮는 절 · 몫의 덧 로직. +- ④ = 앞 파일(C6 차례 — 울진대흥 → 봉화 → 영월 → 울진소광 → 영덕 → 거창 → 북부청 임도 → 사방)을 사람이 다 확정했다고 침 · 열쇠 a → c → b → c2 · 근거 맞음 a · c 바로 / b · c2 1순위 후보 / 근거 다름 2순위. +- ⑤ = 126 줄에선 영월 근거 직종만 바뀜 · ②의 영월 지문이 이미 STmate 직종을 읽어 씀 → 맨 위에선 −1 줄(Y012 임도안전표지판 — 옛 판 번호 「1-9-3」이 지금 판의 낙석방지책 로직을 끌어옴). ⑤의 값어치는 새 파일(2-4). +- 엑셀 줄 무게(되풀이 셈 · 1순위 느슨) — 78.1 → 85.6 → 90.7 → 97.7 → 97.7 → 97.5. + +### 2-2. 씨앗 끔(처음 보는 셈 · 씨앗 = 마스터 층 기억으로 옮김) + +| 단계 | 1순위 엄격 / 느슨 | 다섯 안 엄격 | 1순위 영월 | 조합 맞음(83) | +|---|---|---|---|---| +| 지금 | 67.5 / 77.0 | 86.5 | 44.1 | 9.6 (8) | +| ① 근거 고침 | 80.2 / 84.1 | 92.9 | 55.9 | 24.1 (20) | +| ② + 로직 지문 | 85.7 / 87.3 | 96.8 | 76.5 | 26.5 (22) | +| ③ + 조합 풀이 | **89.7 / 91.3** | **96.8** | 76.5 | **72.3 (60)** | +| ④ + 조건부 기억 · ⑤ + 읽기 넓힘 | 89.7 / 91.3 | 96.8 | 76.5 | 72.3 (60) | +| (참고) C5 길만 | 84.9 / 88.9 | 92.9 | 58.8 | 69.9 (58) | +| (참고) ② 위에 조합 풀이를 그냥 겹침 | 86.5 / 88.1 | 96.8 | 76.5 | 45.8 (38) | + +- 임도 · 사방은 씨앗 켬과 같음(98.0 · 90.7) — 씨앗 몫은 영월(씨앗을 만든 파일)뿐. + +### 2-3. 겹침 — 그냥 더해지지 않음 + +**지문 × 조합 풀이** +- 지문 1순위에 조합 풀이를 그냥 얹으면 조합 맞음 49.4%(C5 길만 72.3% 보다 낮음). +- 까닭 = 섞인 줄 「고임돌 · 조약돌 채집 및 운반(굴삭기 90% + 인력 10%)」 — 지문이 줄 통째 숫자가 닮은 「구조물터파기(9 : 1)」를 1위로 냄 · 그 로직이 줄 벡터를 다 덮어 「인력 몫으로 바꿈」이 안 걸림 · 고임돌 줄 하나가 임도 호표 17 곳 · 사방 9 곳 · 조약돌 줄이 5 곳에 되풀이 → 호표 통째가 무더기로 틀림. +- 고침 = 섞인 줄(몫 표시 + 노임만 든 몫 + 장비만 든 몫 · 글 꼴로 가름 · 정답 안 봄)은 몫마다 먼저 풂 · 나머지는 지문 → 지문이 푼 영월 3 줄(크레인 · 기초잡석 · 고임돌)과 조합이 푼 고임돌 · 조약돌 5 줄이 함께 맞음. + +**기억 × 나머지** — 기억을 얹는 자리마다(1순위 엄격 · 도움 / 해로움 줄) + +| 얹는 자리 | 씨앗 켬 | 씨앗 끔 | +|---|---|---| +| 지금 위 | 71.4 → 80.2 (11 / 0) | 67.5 → 76.2 (11 / 0) | +| ① 위 | 85.7 → 87.3 (2 / 0) | 80.2 → 82.5 (3 / 0) | +| ② 위 | 88.9 → 89.7 (1 / 0) | 85.7 → 86.5 (1 / 0) | +| ③ 위(맨 위) | 92.9 → 92.9 (0 / 0) | 89.7 → 89.7 (0 / 0) | + +- 기억이 고치는 줄 = ① · ② · ③이 고치는 줄과 같음 → 맨 위에선 1순위 0. +- 맨 위 기억의 몫 = 사람 손 — 바로 씀 24 줄 모두 맞음 · 그중 20 줄(엑셀 93 줄)은 「후보」라 사람이 골라야 했던 줄(씨앗 끔 22 줄 · 112 줄) · 1순위 후보 12 중 11 은 쌓은 맞춤 1위와 같음(확인 신호) · 해로움 0. + +### 2-4. 새 파일 — STmate 5 · 거창(씨앗 끔 · 처음 보는 셈) + +| 단계 | 자동 정답 20(조항 · 품 깨끗한 쉬운 줄) 1순위 · 다섯 안 | 씨앗 짝 43(실무자가 이름 바꾼 줄) 1순위 · 다섯 안 | +|---|---|---| +| 지금(336 영월 흉내 읽기) | 75.0 · 75.0 | 41.9 · 65.1 | +| ① 근거 고침 | 80.0 · 80.0 | 53.5 · 69.8 | +| ⑤ + 읽기 넓힘(STmate 직종 이음 · 거창 행 칸 이음) | 95.0 · 95.0 | 55.8 · 72.1 | +| ② + 로직 지문(「같음」 그대로) | 95.0 · 95.0 | 58.1 · 76.7 | +| ② + 로직 지문(「딱 맞음」이 「같음」을 이김) | 100.0 · 100.0 | 62.8 · 81.4 | +| ④ + 조건부 기억(근거 가름) | 100.0 · 100.0 | **65.1 · 83.7** | +| ④ + 같은 발주처 기억은 바로 | 100.0 · 100.0 | **72.1 · 88.4** | + +- 새 파일은 읽기가 앞 — 지문(직종 · 품)이 STmate 노무 목록 · 7열 품 · 거창 흩어진 칸에서 나오므로 ⑤ 없이는 ②가 못 돎. +- 「딱 맞음이 같음을 이김」 = 「같음」 줄이라도 지문이 딱 맞음(2-6)이고 다른 로직을 가리키면 지문 → 126 줄은 바뀐 줄 0 · 새 파일만 오름. +- ③ 조합 풀이는 못 잼(이 파일 블록 몫 벡터가 줄 id 로 안 이어짐) — C5 다른 꼴 24 호표 씨앗 끔 통째 3 → 5 · 찾음 44.2 → 56.5%. +- 지문 = C6 items 로 만든 가벼운 지문(자재 · 장비 시간 · 섞음 밑품 없음) — 126 줄에서 C4 지문 88.9 대 가벼움 88.1(영월 −1 줄) → 거의 같은 셈. +- 자동 정답은 지문과 같은 재료(조항 · 품)로 만든 정답이라 지문 쪽에 맴돎 · 씨앗 짝이 바른 가림 무리. + +### 2-5. 가림 시험 — 문턱 · 무게를 북부청 임도로만 정함 + +손잡이 = C4 무게 다섯(인력 2 · 3 · 4 · 장비 1 · 2 · 3 · 조항 0.75 · 1.5 · 3 · 이름 0.3 · 0.6 · 1.2 · 조건 0.3 · 0.6 · 1.2) × β(0 · 0.5 · 1 · 2 · 3) × 「같음 그대로」(예 · 아니오) × C5 뺌 문턱(0.2 · 0.35 · 0.5) × C6 기억 규칙 5 = 36,450 설정 · 임도 49 줄 1순위 엄격(동점이면 지문 다섯 안)으로만 고름 · 1순위 엄격. + +| 설정 | 임도(맞춘 곳 · 49) | 사방(가림 · 43) | 영월(가림 · 34) | 새 파일 자동(가림 · 20) | 새 파일 씨앗 짝(가림 · 43) | +|---|---|---|---|---|---| +| 지금 설정(세 파일 보고 고름 · β=1) | 98.0 | 90.7 | 88.2 | 95.0 | 62.8 | +| 미리 정한 값(β=2) | 98.0 | 90.7 | 85.3 | 95.0 | 65.1 | +| 임도만 보고 고름 — 가운데(범위) | 98.0 | 90.7 (86.0~95.3) | 85.3 (55.9~88.2) | 95.0 (95.0~100) | 67.4 (58.1~81.4) | +| **떨어지는 폭(가운데 − 지금 설정)** | — | **0** | **−2.9 (한 줄)** | **0** | **+4.6** | + +- 임도가 거의 꽉 참(49 중 48) → 최고 동점 7,368 설정 · 임도 하나로는 무게를 못 정함 · 범위 끝(영월 55.9)처럼 한 파일로 정한 무게는 다른 파일에서 크게 흔들림. +- 손잡이마다(나머지는 지금 설정 · 임도 / 사방 / 영월 / 자동 / 씨앗 짝): + - β — 0: 95.9 / 95.3 / 79.4 / 95.0 / 60.5 · 1: 98.0 / 90.7 / 88.2 / 95.0 / 62.8 · 2: 98.0 / 90.7 / 85.3 / 95.0 / 65.1 → β=1 은 영월 꼭대기(표본을 보고 고른 값). + - 「같음 그대로」 — 아니오: 98.0 / 90.7 / 85.3 / 100 / 69.8 → 영월 +2.9 는 씨앗 덕(영월 = 씨앗 만든 파일) · 새 파일엔 해 → 「딱 맞음이 같음을 이김」으로 바꾸면 126 줄 그대로 · 새 파일 자동 100 · 씨앗 짝 62.8. + - C5 뺌 문턱 — 셋 모두 같음(튼튼). + - C6 기억 규칙 — 임도만 보고도 「근거 가름」(또는 + 발주처)이 뽑힘 · 다른 규칙은 맨 위에서 해로움(임도 — 지금 코드 「a 바로」 95.9 · 「a · b 바로」 87.8 · 「모든 열쇠 바로」 81.6) → 지금 코드도 다른 발주처 기억 1 줄(면고르기 · 산림 ↔ 건설)을 틀리게 넣음. +- 「섞인 줄 몫 먼저」 규칙은 결과를 보고 넣음 — 임도 줄(고임돌 · 조약돌 N005 · N012 · N036)만 보고 넣었다고 쳐도 사방(가림)에서 S081 · S091 풂 · 해 0. +- 판정 — 넘침 폭은 작음(사방 0 · 영월 −1 줄 · 새 파일 0~+5) · 무게는 C4 미리 정한 값 그대로 두고 β · 「같음」 규칙은 여러 파일로 정함. + +### 2-6. 「딱 맞음」 — 가림에서도 + +딱 맞음 = 1위 로직의 직종 묶음이 같고 품이 모두 3% 안(보통인부 하나뿐 줄 뺌) 또는 장비 갈래 · 시간 맞음 · 2위와 점수 차 큼(C4). + +| 무리 | 딱 맞음 · 맞음 | +|---|---| +| 북부청 임도 | 27 · 27 | +| 북부청 사방 | 19 · 19 | +| 동부청 영월 | 10 · 10 (+ 우리에 없음 1 = 잔토처리 B/H 100% · 숫자로는 토사깍기(기계)) | +| 새 파일 씨앗 짝(가벼운 지문 · 가림) | 15 · 14 — 틀린 1 = 구조물되메우기(지문 건설 ↔ 씨앗 산림 · 같은 일 다른 책) | +| 새 파일 자동 정답 | 18 · 18(맴돎 무리) | + +- 126 줄 = 고유 57 · 엑셀 줄 220 / 504(44%) → [딱 맞음 모두 확정] 한 번이면 엑셀 줄 절반 가까이. + +### 2-7. 어떻게 냈나 + +- C1 정답 · 셈(`s10_all`) · C3 묶음 ①(`combo`) · C4 지문(`s3_match.rank` · `fuse`) · C5 (가)(`c04_closed`) · 조합 풀이(`c05_solve.combine` · `c08` 뺌 규칙) · 조합 맞음(`c07_hopyo.judge`) · C6 기억(`s08_memory.build` · `s10_policy.first_hit` · `tier`)을 한 프로세스에서 불러 단계마다 앞 결과를 다음 단계에 넣음 · 단계 앞 값(71.4 · 85.7 · 88.9 · C5 91.3 · 72.3 · C6 75.0 / 90.0 / 41.9)을 그대로 재현해 맞음 확인. +- 가림 시험 = C4 점수를 채널 · 입력 조합별로 미리 셈(무게만 바꿔 끼움 · 지금 무게로 C4 순위와 136 줄 모두 같음 확인). +- 명령마다 120 초 안(가장 긴 것 36 초) · 어림 없음 — 모든 수치는 실측. + +--- + +## 3. 묶음 · 차례 + +사용자 뜻 = 원본 로직 안 고침 · 금액이 아니라 로직 조합 · 한 번 한 것 기억. 모든 묶음은 M02 맞춤 쪽(읽기 · 찾기 · 고르기 · 기억)만 고침 · 우리 로직은 M01 엔진으로 읽어 셈만. + +### 3-1. 묶음 표 + +기대 효과 = 앞 묶음 위에 얹은 실측(2 절) · 「126」 = 씨앗 켬 1순위 · 다섯 안 · 조합 맞음 · 「새 파일」 = 씨앗 짝 1순위. + +| 묶음 | 무엇 | 기대 효과 | 일 크기 | 먼저 할 것 | 차례 | +|---|---|---|---|---|---| +| ① 근거 고침 | 조항 윗마디 · 같은 이름 접힘 고침 · 조항 오타 · 「같음」도 근거 검사 + 덤(장비 시간 줄 · 노임 줄 표시 — 흉내 밖 · 펌프차 · 크레인 2 줄 어림) | 126: 71.4 → 85.7 · 86.5 → 94.4 · 9.6 → 27.7 / 새 파일: 41.9 → 53.5 | 작음~중간 | — | 1 | +| ② 로직 지문 | 엑셀 블록 직종 · 품 · 장비 · 자재 · 할증 · 조건 · 조항 대 우리 로직 1,438 엔진 지문 · 딱 맞음 · 판정 글 | 126: → 88.9 · 96.8 · 30.1 / 새 파일: → 62.8(⑤ 뒤) | 중간~큼 | ①(① 차례가 덤) · 새 파일은 ⑤ | 2 | +| ③ 조합 풀이 | 블록을 절 · 몫으로 갈라 우리 로직 조합으로 풂 · 섞인 줄 몫 먼저 · 1순위가 못 덮는 절은 덧 로직 · 직접 줄 호표 = 호표 통째 로직 하나 | 126: → 92.9 · 96.8 · 75.9 / 새 파일: 못 잼 | 중간 | ②(우리 벡터 한 벌) · 담기는 정할 것 N1 | 3 | +| ④ 조건부 기억 | 열쇠 a(품명 · 규격 · 단위) · b(품명 · 단위) · c(로직 지문 = 단위 · 직종 품 · 장비 · 조항) · 근거 판정으로 바로 / 1순위 후보 / 2순위 · 같은 열쇠 다른 답 쌓기 | 126: 1순위 그대로 · 사람 확인 20 줄 덜음 · 해 0 / 새 파일: → 65.1(같은 발주처 72.1) | 작음~중간 | 근거 판정 = ① · c 열쇠 = ② | 근거 판정 · b 는 ①과 함께 · c 는 ② 뒤 | +| ⑤ 읽기 넓힘 | STmate 꼴 읽기(줄 코드 · 노무 목록으로 직종 · 7열 품) · 거창 단가산출 행 칸 이어 붙임 | 126: ±1 줄 / 새 파일: STmate 6 파일이 올라감(지금 400) · 자동 80 → 95 · 씨앗 짝 53.5 → 55.8 · ② ④의 바탕 | 중간 | — | 새 문서를 곧 쓰면 ①과 나란히 · 아니면 5 | +| ⑥ 고르기 창 | 딱 맞음 초록 + [딱 맞음 모두 확정] · 「로직 X · 계수 바뀜」 · 「기억에서 옴」 · 조합(절마다 로직) 보임 · 금액 비는 순위 · 판정 · 표시에서 뺌 | 순위 그대로 — 다섯 안 96.8% 를 사람이 빨리 · 바르게 고름 | 중간 | ②③④가 낸 칸 | ②부터 함께 | + +### 3-2. 묶음마다 + +**① 근거 고침 — 맞춤 코드 고침(원본 로직 아님)** +- 화면 — 1순위가 엑셀 인용 조항 · 직종과 같은 우리 로직으로 · 이름만 같은 다른 책이 「같음」으로 들어가던 10 줄이 「후보」로 · 철근 가공조립이 「현장가공 · 현장조립」 쪽으로. +- 자리 — `M02_Hopyo_Basis._clause`(:166-176 윗마디) · `sources`(:60-71 오타) · `M02_Hopyo_Match.candidates`(:300-331 접힘 열쇠) · `match_line`(:395-403 같음 근거 검사) · `M02_Hopyo_Excel.kind_of`(:132-150 노임 · 장비 시간 줄). +- 원본 — 우리 로직 · 계수 · 일위대가조합.json · M01 쓰기 없음. + +**② 로직 지문** +- 화면 — 후보 차례가 「숫자가 닮은 로직」 쪽으로 · 딱 맞음 줄 초록 · 맞은 로직에 「계수 바뀜 특별인부 0.370 → 0.354」 같은 글 · 입력값 추천(엑셀 조건 — 합판 「유형=보통」 등 17 줄). +- 새 파일 — `M02_Hopyo_Finger.py`(엑셀 블록 → 지문: 식 풀기 · 밑품 · 장비 · 자재 · 율 · 조건 · 절 · 몫 — C4 `s1_excel_fp` · `fp_common` + C5 `c01` 절 · 몫 한 벌) · `M02_Hopyo_Ours.py`(우리 로직 지문 · 단위당 벡터 — 입력 조합마다 엔진 셈 · 마스터 판 열쇠 캐시 · 처음 ≈ 2 분 → 뒤에서 데움) · `M02_Hopyo_Fit.py`(닮음 점수 · 딱 맞음 · 판정 글 · ① 차례 덤 · 「딱 맞음이 같음을 이김」). +- 고리 — `Basis.read`(:84-125 블록 읽을 때 지문 칸) · `Match.match_line`(:404-414 후보 다시 줄 세움) · `Match.guess`(:417-429 입력값). +- 원본 — `M01_MasterData_Store.loaded`(:65) · `M01_MasterData_Store_Flow.run`(:170 — `M02_Labor_Doc.run_line` :518 과 같은 읽기 길) 부르기만 · 데움 본보기 `M01_MasterData_Store_Cache.warm`(:257). + +**③ 조합 풀이** +- 화면 — 호표 줄마다 「1. 현장가공 → 철근 현장가공 · 2. 조립 → 철근 현장조립」 · 「인력 10% → 모래·자갈·약돌 채집(인력) · 굴삭기 90% → 기계 시간당 기계경비」처럼 절 · 몫마다 로직이 보임. +- 자리 — `M02_Hopyo_Fit.py` 에 조합 풀이(C5 `c05` combine · cover · is_mixed · 길면 `M02_Hopyo_Combine.py` 로 나눔) · 우리 벡터는 ②의 `Ours` 한 벌 · `Match.match_line` 결과에 「조합」 칸 · `Match.match_hopyo`(:440-455 직접 줄 호표 — 벡터 + 인용 32 / 34) · `M02_Hopyo_Build.Builder.build`(:110-175 — 정할 것 N1 정한 뒤). +- 원본 — 37 조합 · 일위대가조합.json 은 견줌에만(읽기). + +**④ 조건부 기억(열쇠 = 로직 지문 · 품명 · 단위)** +- 화면 — 줄 상태 「기억 · 같은 로직 — 봉화 파일」 · 후보 단추 「기억 · 근거 다름(산림 ↔ 건설)」 · 같은 열쇠 다른 답은 답마다 후보 + 횟수. +- 자리 — `Match.line_key`(:68-76 열쇠 b · c · c2) · `Match.recall`(:348-352 근거 판정 갈래) · `Match.confirm`(:495-527) + `M02_Hopyo_Store.remember`(:130-140 덧칸: 열쇠2 · 지문 · 파일 · 발주처 · 엑셀 조항 · 로직 판 · 답들) · 층 차례 `Match.chain`(:333) 그대로 · 확정 길 `M02_Labor_Router.hopyo_memory`(:284). +- 원본 — 기억은 `실무자료/맞춤.json` 층 파일 · 마스터 로직 밖. + +**⑤ 읽기 넓힘(STmate · 거창)** +- 화면 — 지금 「일위대가 시트 없음」으로 막히는 STmate 내역이 올라가 같은 호표 표 · 고르기 창으로 · 거창 단가산출 근거가 보임. +- 자리 — 새 `M02_Hopyo_Stmate.py`(일위대가표 · 단가산출근거 · 노무비목록표 · 줄 코드 D · L · M · X · B · 7열 품) · `Excel._sheets`(:74-102 STmate 꼴이면 부름) · `Basis.read`(:84-125 거창 행 칸 이어 붙임). + +**⑥ 고르기 창** +- 화면 — 후보 단추에 책 · 조항 · 직종 · 딱 맞음 초록 · 판정 글 · 조합 · 기억 출처가 늘 보임 · [딱 맞음 모두 확정] · 금액 비 표시 없음. +- 자리 — 새 `M02_MasterTemplete_Hopyo_Choose.ts`(`View.ts` 614 줄 · `choose` :435-505 를 옮김 · 700 줄 지킴) · `STATE`(:45-53 꼬리표) · `M02_MasterTemplete_Hopyo_Api.ts`(:93 `HopyoMatch` 칸). +- 계산 자리 — 맞춤 · 지문 · 조합은 서버 단독(CLAUDE.md 5장 ③) · 화면은 보이기만. + +### 3-3. 추천 차례 + +1. ① 근거 고침 + ④의 근거 판정 · b 열쇠(지금 코드 a 열쇠의 해 막기) — 작고 가장 큼. +2. ② 로직 지문 + ⑥ 딱 맞음 · 판정 글 보이기. +3. ③ 조합 풀이 + ⑥ 조합 보이기(사전조합 담기는 N1 정한 뒤). +4. ④ 나머지(c 열쇠 · 출처 · 로직 판 · 잊기 · 층 올림) + ⑥ 기억 꼬리표. +5. ⑤ 읽기 넓힘 — 새 문서(STmate · 거창)를 곧 쓰면 1 과 나란히(파일이 안 겹침). + +--- + +## 4. 정할 것 — 336 정할 것 11 다시 정리 + +### 4-1. 풀림(이번 말로) + +| 앞 번호 | 물음 | 풀린 답 | +|---|---|---| +| 1 | 같은 일 다른 책(산림 ↔ 건설) | 엑셀이 쓴 숫자(품)와 같은 책 로직이 정답 · 두 책 숫자가 같으면 인용 책 · 인용과 숫자가 어긋나면 숫자 | +| 3 | 금액 비 표시 기준 | 뺌 — 순위 · 판정 · 표시에 금액 안 씀 · 대신 「딱 맞음」 | +| 5 | 둘째 열쇠 기억 바로 / 후보 | 「경우에 따라」 = 근거 판정 — a · c 근거 맞음 바로 · b · c2 1순위 후보 · 근거 다름 2순위(임도만 보고도 같은 규칙이 뽑힘) | +| 8 | 섞인 줄(9 : 1) 인력 몫 하나 / 둘 | 방향 = 로직의 조합으로 담음(인력 몫 로직 × 비율 + 기계 시간 로직) · 담는 꼴은 N1 | + +### 4-2. 남음 + +- 2 「같음」 자동 들어감 — 「같음」이라도 딱 맞음이 다른 로직이면 후보로 내림(126 줄 해 0) · 나머지 「같음」은 그대로 자동 / 한 번 누름 — N2 와 함께 정함. +- 4 회사 공유 층 — 개인 → 회사(관리자 올림 단추 · 두 파일 넘게 같은 답이면 표시) → 마스터(씨앗 · c 열쇠만) 안대로 할지. +- 7 노임 줄 — 노임을 바로 적은 호표는 호표 통째를 로직 하나로(③) · 따로 남는 노임 줄만 「노임 단가」 표시 · 노임 줄 꼴을 새로 만들지. +- 9 차례 — 3-3 안 · ⑤를 ①과 나란히 할지. +- 10 외부 AI — 그대로 보류. 11 문서 보안 파일 — 안내 글 그대로. +- 6 잔토처리 → N3 로 합침. + +### 4-3. 새 + +- **N1 사전조합 「한 줄 = 담은로직 여럿」** — 철근 가공 + 조립(같은 수량) · 섞인 줄 = 인력 몫 로직 × 0.1 + 기계 시간 로직 × 시간 · 지금 사전조합은 한 줄 = 로직 하나 → M02 만들기(`Build`) 꼴을 바꿀지(일위대가조합.json · 원본 로직 안 고침). +- **N2 「딱 맞음」 한꺼번에 확정** — [딱 맞음 모두 확정] 한 번 / 확정 없이 자동 · 가림에서도 거의 다 맞음(2-6). +- **N3 「우리에 없음」 줄** — 원본 안 고침 → 사용자가 M01 로직 화면 [복사해서 만들기](자체 로직 GX · 정본 안 건드림)로 만들지 · 「없음」으로 둘지 · 숫자가 같은 로직이 있으면 「숫자 같음」 꼬리표로만 보일지(잔토처리 6 · 고화매트 · 철강재 운임 · 노임 줄 2 · 영월 잔토처리 B/H 100% = 숫자로는 토사깍기(기계)). +- **N4 발주처 칸** — 올릴 때 발주처를 고르면 같은 발주처 기억은 근거와 상관없이 바로(같은 발주처 맞음 100% · 새 파일 씨앗 짝 65.1 → 72.1) · 올리기 화면에 칸을 둘지. + +--- + +## 5. 한계 · 남는 실패 + +### 5-1. 맨 위 남는 1순위 실패 — 씨앗 켬 9 · 씨앗 끔 13 + +| 줄 | 다섯 안 | 까닭 | 풀 길 | +|---|---|---|---| +| 야면석소운반 ×2 · 콘크리트소운반 | 밖 | 현장말 · 옛 9-1 인력운반 식 · 단위 다름 | [다른 로직 찾기] 한 번 → 기억(같은 품명 다음 파일) | +| 철골설치 · 철골가공조립(공장) | 2위 | 엑셀 「철골공」 ↔ 우리 「철골 가공조립공」 | M02 맞춤 쪽 직종 이름 짝(LB 안 고침) | +| 구조물터파기(토사) — 엑셀 18 줄 | 2위(느슨 맞음) | 보통 ↔ 혼합토사 세분 | 토질 세분 조건 · 기억 | +| 콘크리트 펌프차 | 3위 | 장비 시간 줄 | ① 덤(장비 시간 줄 → 기계 시간당 기계경비 · 이번 흉내 밖) | +| 측구수로관부설 | 5위 | 플륨관 이름 | 기억 | +| 큰돌채집운반(영월) | 밖 | 직종 · 단위 바꿈(보통인부 ↔ 작업반장 · ㎡ ↔ 개) — 지문이 깸(①은 맞음) | 기억 | +| (씨앗 끔에서 더) 콘크리트타설 · 임도안전표지판 · 잡석운반및다짐 · 콘크리트포장절단 | 2~5위 | 씨앗(사람 확정) 몫 · 옛 판 번호 | 씨앗 = 마스터 층 기억 | + +### 5-2. 한계 + +- 지문 × 조합 겹침 규칙(섞인 줄 몫 먼저)은 결과를 보고 넣음 — 사방에서 맞음 2 · 해 0 이나 새 파일 섞인 줄로는 못 잼. +- 표본 — 사람 정답 126 줄(둘이 같은 발주처) · 새 파일 51 줄(자동 정답은 지문과 같은 재료 · 씨앗 짝은 사람 짝 · 일부 짐작) · 임도가 거의 꽉 차 무게를 한 파일로 못 정함. +- 안 돌린 손잡이 — C5 조합 풀이 안 문턱(0.6 · 0.75 · 0.85) · 몫 비율 후보 · C6 열쇠 차례 · 딱 맞음 문턱(3% · 점수 차 0.3). +- 새 파일 = 가벼운 지문 · 조합 풀이 못 잼 · 기억 흉내 = 앞 파일을 사람이 정답대로 다 확정했다고 침(파일 차례 · 봉화 · 거창 발주처 짐작) · 씨앗 짝 정답과 기억이 같은 씨앗 출처 → 새 파일 기억 효과는 위 끝. +- 「우리에 없음」 10 줄에도 지문은 늘 후보를 냄(10 / 10) · 점수 문턱으로 숨기기는 무익(C3 · C4) → N3. +- 사람이 고르는 손(고르기 창 빠르기)은 안 잼 · 장수 · 진안 2 파일은 문서 보안 잠김. + +--- + +## 6. 코드 자리 + +| 할 것 | 자리 | 묶음 | 원본 | +|---|---|---|---| +| 조항 윗마디 · 오타 | `M02_MasterTemplete/M02_Hopyo_Basis.py` `_clause` :166-176 · `sources` :60-71 | ① | 안 고침 | +| 같은 이름 접힘 · 「같음」 근거 검사 | `M02_Hopyo_Match.py` `candidates` :300-331 · `match_line` :395-403 | ① | 안 고침 | +| 노임 · 장비 시간 줄 | `M02_Hopyo_Excel.py` `kind_of` :132-150 | ① | 안 고침 | +| 엑셀 블록 → 지문 · 절 · 몫 | 새 `M02_Hopyo_Finger.py` ← `Basis.read` :84-125 에서 부름 | ②③ | 안 고침 | +| 우리 로직 지문 · 벡터 캐시(뒤에서 데움) | 새 `M02_Hopyo_Ours.py` — `M01_MasterData_Store.loaded` :65 · `M01_MasterData_Store_Flow.run` :170 부르기만 | ②③ | 읽기만 | +| 닮음 · 딱 맞음 · 판정 글 · 조합 풀이 | 새 `M02_Hopyo_Fit.py`(길면 `M02_Hopyo_Combine.py` 로 나눔) | ②③ | — | +| 후보 다시 줄 세움 · 입력값 · 직접 줄 호표 | `M02_Hopyo_Match.py` `match_line` :404-414 · `guess` :417-429 · `match_hopyo` :440-455 | ②③ | — | +| 기억 열쇠 · 근거 판정 · 덧칸 | `M02_Hopyo_Match.py` `line_key` :68-76 · `recall` :348-352 · `confirm` :495-527 · `M02_Hopyo_Store.py` `remember` :130-140 | ④ | 마스터 밖 | +| STmate · 거창 읽기 | 새 `M02_Hopyo_Stmate.py` · `Excel._sheets` :74-102 · `Basis.read` :84-125 | ⑤ | 안 고침 | +| 사전조합 「한 줄 = 담은로직 여럿」 | `M02_Hopyo_Build.py` `Builder.build` :110-175 | ③(N1) | 일위대가조합.json 안 고침 | +| 고르기 창 | 새 `M02_MasterTemplete_Hopyo_Choose.ts` ← `View.ts` `choose` :435-505 · `STATE` :45-53 · `Hopyo_Api.ts` :93 | ⑥ | — | +| 시험 | `resources/tester/test_m02_hopyo_finger.py`(식 풀기 · N001 → GC000211 · 딱 맞음) · `_fit`(철근 조합 · 고임돌 몫 · 섞인 줄 몫 먼저) · `_match`(기억 근거 판정) · `_stmate` | 모두 | — | + +- 원본 로직 안 고침 확인 — 이번 실측도 `resources/master_data` · `일위대가조합.json` · M01 을 읽기만(엔진 부르기 · 같은 프로세스 바꿔 끼움) · 저장소 쓰기 없음. +- C7 실험(스크래치패드 `req226/C7/` · 저장소 `venv` 파이썬 `-B`) — `k0_lib.py`(C1 · C3 · C4 · C5 · C6 함수 묶음 · 단계) · `k1_stack.py 켬|끔`(누적 표 → `stack_*.json`) · `k2_detail.py`(기억 자리별 · 남는 실패) · `k3_vec.py`(지문 채널 미리 셈 → `vec_*.npz`) · `k4_wide.py`(새 파일 정답 · ① → `wide.json`) · `k5_hold.py 준비|그물`(가림 시험 → `hold_*.json`) · `k6_sum.py`(가림 정리) · `k7_wide_stack.py`(새 파일 누적 · 가벼움 눈금 · 딱 맞음) · `k8_sure_same.py`(딱 맞음이 같음을 이김). From eba998b58580f8ba3b79625f8f297af9308402f2 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 15 Oct 2026 19:56:06 +0900 Subject: [PATCH 47/81] =?UTF-8?q?chore(=EC=9A=94=EA=B5=AC):=20227=20?= =?UTF-8?q?=E2=80=94=20CAD=20=EC=9E=85=EB=A0=A5=20=EC=A1=B0=EC=82=AC=20?= =?UTF-8?q?=EB=B2=94=EC=9C=84=20=EB=B0=94=EB=A1=9C=EC=9E=A1=EC=9D=8C(?= =?UTF-8?q?=EC=98=A4=ED=86=A0=EC=BA=90=EB=93=9C=20=EB=A7=88=EC=9A=B0?= =?UTF-8?q?=EC=8A=A4=20=C2=B7=20=ED=82=A4=EB=B3=B4=EB=93=9C=20=EC=88=AB?= =?UTF-8?q?=EC=9E=90=20=C2=B7=20=ED=98=95=EC=83=81=20=EC=9E=85=EB=A0=A5)?= =?UTF-8?q?=20=C2=B7=20=EC=99=B8=EB=B6=80=20=EC=9D=BC=EC=9C=84=EB=8C=80?= =?UTF-8?q?=EA=B0=80=20=EB=AA=A9=EC=A0=81=20=EB=8B=A4=EC=8B=9C=20=EA=B3=A0?= =?UTF-8?q?=EB=AF=BC(=EB=B3=84=EC=B9=AD=20=C2=B7=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=EC=9E=90=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_01C6BNBFXFndbqYxu6sZuknK --- docs/raw/요구_1010_2/사용자요구_1010_2.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/raw/요구_1010_2/사용자요구_1010_2.md b/docs/raw/요구_1010_2/사용자요구_1010_2.md index d0ab3dd40..c81ce0bf8 100644 --- a/docs/raw/요구_1010_2/사용자요구_1010_2.md +++ b/docs/raw/요구_1010_2/사용자요구_1010_2.md @@ -282,3 +282,10 @@ 5. 3번과 4번은 코드 요청이 아니라 상세검토 요청임. 코드 작업은 검토 결과 보고 판단할 예정 226. (원문 · 10-15) 3번 캐드항목에 대해 코덱스와 안티그래비티에게도 인터넷을 찾아보라고 하고 너도 추가로 더 찾아볼것. 4번 산림과 건설이 유일한 품셈정보이나 사용자들이 본인들의 편의대로 변경해서임. 우리 서버의 단가산출 로직이 정상이지만 우리서버를 바꾸더라도 같은 문제가 발생됨. 나는 우리서버측 단가산출 로직을 변경할 생각은 없음. 일위대가는 어차피 단가산출 로직의 조합이거나 일위대가일 수 밖에 없음. 그리고 중요한건 값이아니라 어떤 로직의 조합인지와 로직이 중요. 경우에 따라서는 한번 한 것들은 기억하는것도 좋음. 다시한번 정밀검토하여 정밀도를 높일 수 있는 방안 확보할 것. 3번/4번 재조사 +227. (원문 · 10-15) 3번 다시 조사 시작. 잘못조사함. 왜 값자기 cad를 말함. 내가 요청한건 오토캐드에서 마우스와 키보드로 숫자나 형상을 그리기 위해 단축키와 키보드 조작에 대한 기능을 조사를 요청한것임. 내장 CAD 프로그램을 보강하기 위해 조하를 요청한 것. 다시 조사하되 코덱스와 안티그래비티 워크트리의 각각 코덱스와 안트그래비티 에이전트 세션을 실행해뒀으니 세션확인 후 할것. 여기서 코덱스 워크트리에 코덱스 에이전트에 요청해야함. 안티그래비티도 동일하니 다시 조사 요청. +4번 현재 우리의 근거를 고칠일은 없음. 우리가 기준일 확율이 높음. 우리는 품셈을 확인해서 구현한 로직임. 하지만 만약 품셈원문을 조사해서 문제가 있다면 우리 로직도 변경가능. +- 딱 맞음이라도 저장하겠다는 의미는 아님 사용자에게 보여주고 확인을 받아야함. 맞다는 표현만하고 확인은 사용자가 +- 우리에게 없는 단가산출 로직이라면 넣는게 맞지만 품셈 원문을 보고 판단해야함. 대체로 이름이 바뀌었을 확율이 높음. 바로 넣지말고 나에게 확인 받을것. +- 발주처 구분은 의미 없음. 관리는 사용자 몫 +- 다시말하지만 산출근거는 변경가능하나 원문이 맞는 경우에만 변경이 되어야함. +- 현재 우리가 확인하고 구현해야하는 로직은 사용자들이 가지고 있는 문서들을 불러와서 우리프로그램의 단가산출과 매칭하고 자동이 안될경우는 사용자가 선택하게 하되 별칭으로 남겨 일위대가항목을 쉽게 반영하기 위함. << 이방식에 대한 로직구현인지 다시 고민할 것. 구현은 3번과 동시 상세계획서 작성과 코드작업이 진행할 예정이니 천천히 목적을 고민해 볼것. From 9c0ef7f827a53b1021f5707b2881266f5982a74c Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 15 Oct 2026 20:02:18 +0900 Subject: [PATCH 48/81] =?UTF-8?q?chore(=EC=9A=94=EA=B5=AC):=20228=20?= =?UTF-8?q?=E2=80=94=20=EC=97=91=EC=85=80=20=EC=9D=BC=EC=9C=84=EB=8C=80?= =?UTF-8?q?=EA=B0=80=20=3D=20=EC=9A=B0=EB=A6=AC=20=EB=A1=9C=EC=A7=81?= =?UTF-8?q?=EC=97=90=20=EC=98=B5=EC=85=98=EC=9D=84=20=EB=84=A3=EA=B3=A0=20?= =?UTF-8?q?=EB=82=98=EC=98=A8=20=EA=B0=92=EC=9D=BC=20=ED=99=95=EB=A5=A0=20?= =?UTF-8?q?=EB=86=92=EC=9D=8C=20=C2=B7=20=EC=BD=94=EB=8D=B1=EC=8A=A4=20?= =?UTF-8?q?=EC=84=B8=EC=85=98=20=EB=8B=A4=EC=8B=9C=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_01C6BNBFXFndbqYxu6sZuknK --- docs/raw/요구_1010_2/사용자요구_1010_2.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/raw/요구_1010_2/사용자요구_1010_2.md b/docs/raw/요구_1010_2/사용자요구_1010_2.md index c81ce0bf8..42a7088a9 100644 --- a/docs/raw/요구_1010_2/사용자요구_1010_2.md +++ b/docs/raw/요구_1010_2/사용자요구_1010_2.md @@ -289,3 +289,5 @@ - 발주처 구분은 의미 없음. 관리는 사용자 몫 - 다시말하지만 산출근거는 변경가능하나 원문이 맞는 경우에만 변경이 되어야함. - 현재 우리가 확인하고 구현해야하는 로직은 사용자들이 가지고 있는 문서들을 불러와서 우리프로그램의 단가산출과 매칭하고 자동이 안될경우는 사용자가 선택하게 하되 별칭으로 남겨 일위대가항목을 쉽게 반영하기 위함. << 이방식에 대한 로직구현인지 다시 고민할 것. 구현은 3번과 동시 상세계획서 작성과 코드작업이 진행할 예정이니 천천히 목적을 고민해 볼것. +228. (원문 · 10-15) 참고로 내가 지정해준 엑셀 일위대가에는 우리 로직의 옵션을 지정 후 나온값일 확율이 높음. 그래서 내가 값보다는 로직을 보라고 한것임. +다음 코덱스는 내가 실행해둠 오르카 리스트로 재확인 From 3656ebcdf01155f7788464088484673ab42829f5 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 15 Oct 2026 21:02:08 +0900 Subject: [PATCH 49/81] =?UTF-8?q?chore(=EC=A1=B0=EC=82=AC):=20335=20?= =?UTF-8?q?=EB=8B=A4=EC=8B=9C=20=EC=A1=B0=EC=82=AC=20=E2=80=94=20=EC=98=A4?= =?UTF-8?q?=ED=86=A0=EC=BA=90=EB=93=9C=20=EB=A7=88=EC=9A=B0=EC=8A=A4=20?= =?UTF-8?q?=C2=B7=20=ED=82=A4=EB=B3=B4=EB=93=9C=20=EC=A1=B0=EC=9E=91=20146?= =?UTF-8?q?=20=EB=8C=80=20=EB=82=B4=EC=9E=A5=20CAD(=EC=9E=88=EC=9D=8C=2026?= =?UTF-8?q?=20=C2=B7=20=EC=9D=BC=EB=B6=80=2069=20=C2=B7=20=EC=97=86?= =?UTF-8?q?=EC=9D=8C=2051)=20=C2=B7=20=ED=9D=A0=2030=20=C2=B7=20=EB=AC=B6?= =?UTF-8?q?=EC=9D=8C=20=EC=9D=BC=EA=B3=B1=20=C2=B7=20=EC=A0=95=ED=95=A0=20?= =?UTF-8?q?=EA=B2=83=2024?= 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_01C6BNBFXFndbqYxu6sZuknK --- ...10-15_335_다시조사_오토캐드마우스키보드.md | 337 +++++ ...0-15_335_부록4_오토캐드마우스키보드원문.md | 1122 +++++++++++++++++ 2 files changed, 1459 insertions(+) create mode 100644 docs/raw/조사/2026-10-15_335_다시조사_오토캐드마우스키보드.md create mode 100644 docs/raw/조사/2026-10-15_335_부록4_오토캐드마우스키보드원문.md diff --git a/docs/raw/조사/2026-10-15_335_다시조사_오토캐드마우스키보드.md b/docs/raw/조사/2026-10-15_335_다시조사_오토캐드마우스키보드.md new file mode 100644 index 000000000..0032920e4 --- /dev/null +++ b/docs/raw/조사/2026-10-15_335_다시조사_오토캐드마우스키보드.md @@ -0,0 +1,337 @@ +# 내장 CAD 보강 — 오토캐드 마우스 · 키보드 조작 대조 (요구 227-3 · PLAN 335-4) + +- 재료 = 코덱스 `codex_227.md`(공식 5 절) · 안티그래비티 `agy_227.md`(국내 실무 33 줄) · K1(브레인 공식 한 벌 217 줄) · K2(우리 CAD 코드 읽기 · `main_laptop_1` 9c0ef7f82) · 앞 검토 335(흠 H1~H12 · 묶음 ⓪~⑦ · 정할 것 1~14) — 원문 = 같은 폴더 `2026-10-15_335_부록4_오토캐드마우스키보드원문.md`. +- 우리 = ● 있음 · ◐ 일부(— 뒤 = 다른 점) · ○ 없음 / 쓸모 = 내장 CAD 보강 쓸모 상 · 중 · 하 / 실무 = 안티그래비티 표시(꼭 = 꼭 있어야 함 · 자주 = 자주 씀 · 헷갈림 = 초보가 헷갈림) / 크기 = 작(반나절) · 중(하루~이틀) · 큼(사흘 넘음) / 근거 = 공식(오토데스크 도움말) · 실무(국내 글 · 공식과 맞음) · 약함(2차 출처 · 출처 쪽에 없음). +- 코드 읽기만 · 화면 실측 없음 · 파일 줄 = `B07_DesignDetail/openwebcad/src/` 아래 · DI = 동적 입력. + +## 0. 결론 + +1. 오토캐드에서 마우스 · 키보드로 숫자를 넣고 형상을 그리고 고치는 조작을 세 조사에서 모아 겹친 것을 하나로 합침 — 146 가지(명령 31 포함) · 우리 CAD 에 있음 26 · 일부 69 · 없음 51 · 일부 · 없음 120 의 보강 쓸모 = 상 40 · 중 51 · 하 29. +2. 이미 오토캐드처럼 되는 것 — 좌표 꼴(`x,y` · `@` · `<각`) · 거리 바로 치기 · Tab 길이/각도 · Space 반복 · 창/걸침 고르기 · 휠 줌 · 가운데 팬 · F3 · F8 · Ctrl+Z · 별칭 대부분. +3. 크게 빠진 것 — 명령마다 옵션 글자(31 명령 중 LINE · ARRAY 둘만 → 사각형 `D` · 원 `D` 지름 · 폴리선 `C` 닫기 없음) · 앞에 쓴 값 기억(간격띄우기 거리 · 모깎기 반지름) · Shift+우클릭 스냅 메뉴 · Shift 누르는 동안 직교 · 그립 누르고-옮기고-누르기 · FROM · 두 점 가운데 · 식. +4. 실무자가 꼽은 33 가지(안티그래비티) 중 우리에 다 있음 9 · 일부 13 · 없음 11 — 없는 것 = `#` · F12 · 휠 두 번 · 고르기 `P` · `L` · Shift+우클릭 · 자르기/연장 Shift · 사각형 `D` · 원 `D` · 그립 Space. +5. 오토캐드 손버릇이 사고로 번지는 흠 30(앞 12 + 새 18) — 급한 일곱: Ctrl+C → 원(H1) · 확정 도면이 고쳐짐(H2) · 이동 도중 딴 명령이면 고른 것이 사라짐(N1) · 고르는 중 그립 누르면 명령이 바뀜(N7) · 그립 누르기만 해도 밀림(H3) · 명령행 소문자 `c` → 원(N6 · 확인됨) · `R` 만 쳐도 사각형 시작(앞글자 실행 · 확인됨). +6. 보강 묶음 일곱 — 추천 차례 ⓪ 흠 막기 → ② 명령 중 키 → ⑤ 명령 옵션 → ① 숫자 넣기 → ③ 마우스 → ④ 단축키 → ⑥ 켬끔 기억 · 앞몫 합 어림 28 일(한 창). +7. 정할 것 24(앞 14 + 새 10) — 먼저: 앞글자 실행 없앨지 · 별칭을 오토캐드와 맞출지 · 소문자 · Esc 한 번 뜻 · 점 묻는 중 명령 이름 거절. +8. 오토캐드와 거꾸로인 기본값 둘 — 휠(우리 = 당기면 확대 · 오토캐드 = 밀면 확대) · 배열 `R`(우리 = 원형 · 오토캐드 = 사각). +9. 범위 밖 — 다른 CAD · 리습 · 스크립트 · 토목 전용(비탈 1:n) · 손가락 · AI · 3D · H13 · H14(따로 남은 흠). + +## 1. 대조표 + +### ① 숫자 넣기(좌표 · 거리 · 각도 · 동적 입력 · 식) + +| # | 무엇 | 오토캐드 키 · 마우스 | 누르는 차례 | 우리 | 쓸모 | 실무 | 크기 | 근거 | +|---|---|---|---|---|---|---|---|---| +| 1-1 | 절대 좌표 | `x,y` | 점 물음 → `3,4` Enter | ● 늘 원점 기준 | — | — | — | 공식 | +| 1-2 | 상대 좌표 | `@dx,dy` | 앞 점 → `@5,0` Enter | ● 그 명령 안 앞 점 기준(첫 점 = 1-10) | — | 꼭 · 헷갈림 | — | 공식 · 실무 | +| 1-3 | 절대 극 | `d 스냅 > 커서 | — | 꼭 · 자주 | — | 공식 · 실무 | +| 1-7 | 각 잠금 | `` 물음 → Enter = 직전 값 | ◐ — 빈 Enter ● · 꺾쇠 값 = 상수(간격 늘 1 · 모깎기 늘 0) | 상 | — | 작~중 | 공식 | +| 2-4 | Space 예외 | Space | 글 넣는 중 = 빈칸 | ● | — | — | — | 공식 | +| 2-5 | 명령 취소 | Esc · Ctrl+[ | 아무 때 | ◐ — 도구마다 다름: 이동 · 복사 · 회전 · 축척 · 배열 안 끝남(N2) · 사각형 첫 점 · 자르기 처리 없음(N3) · 폴리선 다 버림(N4) | 상 | 꼭 · 자주 | 중 | 공식 · 실무 | +| 2-6 | 고른 것 풂 | Esc | 명령 없이 고른 채 | ● | — | 꼭 · 자주 | — | 공식 · 실무 | +| 2-7 | 옵션 글자 | 대문자 글자 + Enter | `[Close/Undo]` → `C` Enter | ◐ — LINE C · U · ARRAY L · R 뿐(31 중 둘) · 그 밖 점 단계 글자 = 다른 명령(H7) | 상 | — | 중(틀) | 공식 | +| 2-8 | 대소문자 | 소문자도 됨 | `c` = `C` | ◐ — 캔버스 ● · 명령행 칸 소문자 = 다른 명령(N6 확인됨) | 상 | — | 작 | 공식 | +| 2-9 | 옵션 · 기본값 표기 | `[...]` · `<...>` | 물음마다 적힘 | ◐ — `[...]` = LINE · ARRAY 만 · `<...>` = 숫자 물음 몇 곳 | 중 | — | 작 | 공식 | +| 2-10 | 옵션 누르기 · 목록 | 옵션 글자 클릭 · DI ↓ | ↓ → ↑↓ → Enter | ○ | 중 | — | 중 | 공식 | +| 2-11 | 명령 이력 | ↑ ↓ | 명령 없을 때 ↑ → Enter | ◐ — 명령행 칸만(기본 숨김) · 캔버스 ↑↓ = 화면 이동 · 알림 줄 섞임(H11) | 중 | — | 작 | 공식 | +| 2-12 | 값 이력 | ↑ ↓ | 명령 중 그 물음의 최근 값 20 | ○ | 중 | — | 중 | 공식 | +| 2-13 | 최근 값 · 명령 메뉴 | 우클릭 → Recent Input · 명령줄 우클릭 | — | ○ | 하 | — | 작 | 공식 | +| 2-14 | 자동 완성 | 글자 → 후보 · Tab 돌림 | `CI` → 후보 고름 → Enter | ◐ — 후보 6(캔버스) · 8(명령행) · Tab = 첫 후보 채움(명령행) · ⚠ 앞글자 Enter = 첫 후보 실행(N18) | 중 | — | 작 | 공식 | +| 2-15 | 점 묻는 중 명령 이름 | — | 명령 이름은 「명령:」 물음에서만 알아봄 | ○ 지금 명령 버리고 새 명령(N1 · H7 의 뿌리) | 상 | — | 작 | 공식(간접) | +| 2-16 | 투명 명령 | `'ZOOM` · `'CAL` · `'DIST` | 물음 중 `'` + 이름 | ◐ — 즉시형 33(ZOOM · LAYER 등)은 `'` 없이 됨 · `'` 꼴 버림 · 숫자 물음은 삼킴 | 하 | — | 작 | 공식 | +| 2-17 | 명령 중 `U` · `C` | `U` 취소 · `C` 닫기 | LINE · PLINE 다음 점 → `U` | ◐ — LINE ● · 그 밖 `U` = UNDO · `C` = 원(H7) | 상 | — | (⑤) | 공식 | +| 2-18 | 되돌림 · 다시 | Ctrl+Z · Ctrl+Y | 명령 없을 때 | ● (명령 중 = 되돌림 + 명령 취소 · 오토캐드 쪽 확인 못 함) | — | — | — | 공식 | +| 2-19 | 지우기 | Delete | 고른 뒤 | ● (보기 전용도 지워짐 H2) | — | — | — | 공식 | +| 2-20 | 글자 지움 | Backspace | 친 글 | ● 한 자씩 | — | — | — | 공식 | +| 2-21 | 되풀이 명령 | `MULTIPLE` → 명령 | Esc 까지 | ◐ — 점만 받는 그리기는 늘 되풀이 · MULTIPLE ○ | 하 | — | 작 | 공식 | + +### ③ 마우스(단추 · 휠 · 우클릭 · 두 번 누름 · 고르기 · 그립 · 끌어 놓기) + +| # | 무엇 | 오토캐드 키 · 마우스 | 누르는 차례 | 우리 | 쓸모 | 실무 | 크기 | 근거 | +|---|---|---|---|---|---|---|---|---| +| 3-1 | 왼 단추 | 클릭 | 점 · 물체 · 메뉴 | ● (명령 중엔 놓을 때 점) | — | — | — | 공식 | +| 3-2 | 우클릭 뜻 | 우클릭(SHORTCUTMENU 11) | 쉴 때 = 반복/메뉴 · 고른 채 = 편집 메뉴 · 명령 중 = Enter/옵션 메뉴 | ◐ — 늘 Enter(반복 안 함 · 친 글 제출) · 메뉴 ○ | 중 | 자주(우클릭 = Enter 로 바꿔 씀) | 중 | 공식 · 실무 | +| 3-3 | 시간차 우클릭 | 짧게 · 길게 | 짧게 = Enter · 길게(250 ms) = 메뉴 | ○ | 중 | — | 작 | 공식 | +| 3-4 | 한 번 스냅 메뉴 | Shift+우클릭 | 점 물음 → 끝점 · From · M2P … | ○ | 상 | 꼭 · 자주 | 작(메뉴 틀 뒤) | 공식 · 실무 | +| 3-5 | 휠 굴림 | 앞 · 뒤 | 밂(앞) = 확대 · 커서 자리 | ◐ — 커서 자리 ● · 기본 거꾸로(당기면 확대 · ☰ 설정에서 바꿈) · 한 칸 약 10% 고정 | 중 | 꼭 · 자주 | 작 | 공식 · 실무 | +| 3-6 | 휠 두 번 | 가운데 두 번 | 범위 맞춤 | ○ (`Z` · ⛶ 로) | 상 | 꼭 · 자주 | 작 | 공식 · 실무 | +| 3-7 | 휠 끌기 | 가운데 누르고 끌기 | 화면 이동 | ● | — | 꼭 · 자주 | — | 공식 · 실무 | +| 3-8 | Ctrl+휠 끌기 · 휠 클릭 메뉴 | Ctrl+가운데 끌기 · MBUTTONPAN 0 | 조이스틱 이동 · 스냅 메뉴 | ○ (Ctrl+휠 굴림 = 빠른 줌) | 하 | — | 작 | 공식 | +| 3-9 | 눌러 고르기 | 물체 위 클릭 | 고르기 상자 PICKBOX 3 | ● 10px | — | — | — | 공식 | +| 3-10 | 창 · 걸침 | 빈 곳 왼→오 · 오→왼 | 클릭-클릭 또는 끌기 | ◐ — 쉴 때 ● · 명령 안 고르기 = 두 번 누름만(끌기 ○ · H5) | 상 | 꼭 · 헷갈림 | 작 | 공식 · 실무 | +| 3-11 | 더해 고르기 | 누름 = 늘 더함(PICKADD 2) | 하나씩 누름 → Enter | ◐ — 누름 = 바꿈(앞 것 빠짐 · 명령 중도 · H5) · Shift = 더함 | 상 | — | 작 | 공식 | +| 3-12 | 빼기 | Shift+클릭 · Shift+창 | 고른 채 Shift+누름 | ◐ — Ctrl+누름 = 넣고 빼기 · Shift = 더함 · 상자 = 늘 새로(H4) | 상 | 꼭 · 자주 | 작 | 공식 · 실무 | +| 3-13 | 올가미 | 빈 곳 누르고 끌기 · 끄는 중 Space = 창 → 걸침 → 울타리 | — | ○ 끌기 = 늘 사각 | 중 | — | 중 | 공식 | +| 3-14 | 앞 고름 · 마지막 · 모두 | `P` · `L` · `ALL` | 고르기 물음 → `P` Enter | ○ `P` = 화면 이동 안내 · `L` = LINE 실행 · `ALL` 없음(Ctrl+A ●) | 상 | 자주 | 작 | 공식 · 실무 | +| 3-15 | 울타리 · 다각형 고르기 | `F` · `WP` · `CP` · `R` · `A` · `?` | 고르기 물음 → `F` → 꺾은 선 | ○ | 중 | — | 중 | 공식 | +| 3-16 | 고르기 끝 | Enter · Space · 우클릭 | 고르기 물음 | ● | — | — | — | 공식 | +| 3-17 | 겹친 것 | Ctrl+W · Shift+Space · 목록 | 겹친 곳 → 목록 | ◐ — 같은 자리 다시 누름 = 다음 · 목록 ○ · Ctrl+W = 브라우저 탭 닫기 | 중 | — | 중 | 공식 | +| 3-18 | 스침 강조 | 커서 올림 | 쉴 때 + 고르기 물음(SELECTIONPREVIEW 3) | ◐ — 쉴 때만 · 명령 안 고르기 ○(H5) | 중 | — | 작 | 공식 | +| 3-19 | 두 번 눌러 고치기 | 두 번 클릭 | 글 · 치수 = 글 고침 · 폴리선 = PEDIT 등(DBLCLKEDIT) | ◐ — 글자만 · 치수 · 표 · 폴리선 ○ · 그리기 중 = 점 둘(N16) | 상 | — | 중 | 공식 | +| 3-20 | 뜨거운 그립 | 그립 클릭 → 옮김 → 클릭 | 고른 뒤 그립 클릭(빨강) → 점 | ◐ — 누른 채 끌어 놓기만 · 안 끌고 떼도 적용(H3) | 상 | — | 중 | 공식 | +| 3-21 | 그립 중 숫자 | 거리 · `x,y` · `@` · 각 | 뜨거운 그립 → `2.5` Enter | ◐ — `x,y` · `@` 는 누른 채만 · 거리 · 길이+Tab 무시(N8) | 상 | — | 작(3-20 뒤) | 공식 | +| 3-22 | 그립 모드 돌림 | Enter · Space | 늘리기 → 옮기기 → 돌리기 → 크기 → 대칭 | ○ | 중 | 자주 · 헷갈림 | 중 | 공식 · 실무 | +| 3-23 | 그립 복사 · 기준점 | `C` · `B` · Ctrl 누른 채 | 그립 모드 중 `C` | ○ | 중 | — | 작 | 공식 | +| 3-24 | 여러 그립 | Shift+그립 클릭 | 사이 모양 그대로 늘림 | ○ | 중 | — | 중 | 공식 | +| 3-25 | 다기능 그립 메뉴 | 그립 위 멈춤 · Ctrl 돌림 | 꼭짓점 더함 · 뺌 · 호로 | ◐ — 폴리선 가운데 끌기 = 꼭짓점 더함 · Ctrl+꼭짓점 = 뺌 · 메뉴 ○ | 중 | — | 중 | 공식 | +| 3-26 | 그립 우클릭 메뉴 | 그립 뒤 우클릭 | 모드 · 옵션 | ○ | 하 | — | 작 | 공식 | +| 3-27 | 통째 옮기는 그립 | 글 · 블록 · 선 가운데 · 원 중심 | 끌기 | ● | — | — | — | 공식 | +| 3-28 | 끌어 옮기기 · 복사 | 고른 것 끌기 · Ctrl 끌기 · 우클릭 끌기 | Move Here · Copy Here · Paste as Block | ○ | 중 | — | 중 | 공식 | +| 3-29 | 조금씩 옮기기 | Ctrl+화살표 | 고른 채 | ○ (화살표 = 화면 이동) | 중 | — | 작 | 공식 | +| 3-30 | 그립 제한 · 크기 | GRIPOBJLIMIT 100 · GRIPSIZE | — | ◐ — 늘 그림(8px · 제한 없음) · 보기 전용도 그림(H2) | 하 | — | 작 | 공식 | + +### ④ 단축키(Ctrl · Shift · Alt · F 키 · 임시 우선 키 · 별칭) + +| # | 무엇 | 오토캐드 키 · 마우스 | 누르는 차례 | 우리 | 쓸모 | 실무 | 크기 | 근거 | +|---|---|---|---|---|---|---|---|---| +| 4-1 | 도움말 · 기록 | F1 · F2 | 명령 중 F1 · F2 = 명령 기록 펼침 | ○ 삼킴 | 하 | — | 작 | 공식 | +| 4-2 | 객체 스냅 켬끔 | F3 · Ctrl+F | — | ◐ — F3 ● · 끄면 직교 · 극 · 추적도 꺼짐(H8) · Ctrl+F = 글자 → 모깎기(H1) | 상 | 꼭 · 자주 | 작 | 공식 · 실무 | +| 4-3 | 3D 스냅 · 등각 · 동적 UCS | F4 · F5 · F6 · Ctrl+E · Ctrl+D | — | ○ (2D · F5 = 브라우저 새로고침) | 하 | — | — | 공식 | +| 4-4 | 그리드 | F7 · Ctrl+G | — | ◐ — 화면 무늬(H10) · Ctrl+G = 글자 → 그룹(H1) | 하 | — | 작 | 공식 | +| 4-5 | 직교 | F8 · Ctrl+L | — | ◐ — F8 ● · 축 15 안에서만 붙음(강제 아님 H8) · Ctrl+L = 선(H1) | 상 | 꼭 · 자주 | 작 | 공식 · 실무 | +| 4-6 | 격자 스냅 | F9 · Ctrl+B | — | ○ | 하 | — | 중 | 공식 | +| 4-7 | 극 추적 | F10 · Ctrl+U | 직교와 둘 중 하나 | ◐ — F10 ● · 45° 고정(각 고르기 ○) · 직교와 둘 중 하나 = 같음 | 상 | 자주 | 작 | 공식 · 실무 | +| 4-8 | 스냅 추적 | F11 | — | ◐ — 상태막대 단추만 · F11 = 브라우저 전체 화면 | 중 | 자주 · 헷갈림 | 작 | 공식 · 실무 | +| 4-9 | 동적 입력 | F12 · F12 누른 채 = 잠시 끔 | — | ○ 늘 켬 · F12 = 개발자 도구 | 하 | 자주 · 헷갈림 | 작 | 공식 · 실무 | +| 4-10 | 되돌림 · 다시 · 모두 | Ctrl+Z · Ctrl+Y · Ctrl+A | — | ● (Ctrl+A = 안 보이는 것까지 H6) | — | — | — | 공식 | +| 4-11 | 복사 · 잘라냄 · 붙임 | Ctrl+C · Ctrl+X · Ctrl+V | — | ◐ — Ctrl+V ● · Ctrl+C · X = 글자로 쌓여 원 · 분해(H1) | 상 | — | 작 | 공식 | +| 4-12 | 기준점 복사 · 블록 붙임 | Ctrl+Shift+C · Ctrl+Shift+V | 기준점 → 고름 / 넣을 점 | ◐ — 명령(COPYBASE · PASTEBLOCK)만 · 키 ○ | 중 | 꼭 · 자주 | 작 | 공식 · 실무 | +| 4-13 | 앞 고름 · 반복 | Ctrl+Shift+L · Ctrl+J · Ctrl+M | — | ○ | 하 | — | 작 | 공식 | +| 4-14 | 특성 · 계산기 · 명령줄 | Ctrl+1 · Ctrl+8 · Ctrl+9 · Ctrl+0 | — | ◐ — PROPERTIES · QC 명령만 · Ctrl+1 = 글자 `1`(H1) · 명령행 = 단추만 | 중 | — | 작 | 공식 | +| 4-15 | 그 밖 Ctrl 켬끔 | Ctrl+W · H · I · Shift+I · Shift+A | 선택 순환 · 그룹 고르기 · 좌표 표시 등 | ○ (Ctrl+W = 브라우저 탭 닫기) | 하 | — | — | 공식 | +| 4-16 | 파일 · 창 | Ctrl+N · O · S · P · Q · Tab · PgUp/PgDn | — | ◐ — Ctrl+S = M02 만 · 나머지 브라우저 몫 | 하 | — | — | 공식 | +| 4-17 | Alt | Alt(리본 키팁) · Alt+F4 · Alt+F8 | — | ○ Alt+글자 = 글자로 쌓임(H1) | 하 | — | 작 | 공식 | +| 4-18 | 임시 직교 | Shift 누르는 동안 | 점 물음 중 | ○ | 상 | — | 작 | 공식 | +| 4-19 | 임시 스냅 하나만 | Shift+E · P(끝) · M · V(중간) · C · ,(중심) | 누르는 동안 | ○ | 중 | — | 작 | 공식 | +| 4-20 | 임시 켬끔 | Shift+A · '(스냅) · D · L(모두 끔) · S · ;(강제) · X · .(극) · Q · ](추적) | 누르는 동안 | ○ | 중 | — | 작 | 공식 | +| 4-21 | 임시 키 그 밖 | Shift+W · [(배치 제안) · Shift+Z · /(동적 UCS) | — | ○ | 하 | — | — | 공식 | +| 4-22 | 별칭 | acad.pgp(L · PL · C · REC · M · CO · O · TR …) | 별칭 → Enter/Space | ◐ — 88 개 · 대부분 같음 · 다른 것 = 아래 표 | 상 | — | 작 | 공식 · 약함 | +| 4-23 | 별칭 고치기 | acad.pgp 사용자 칸 · ALIASEDIT · REINIT | 같은 별칭 = 뒤 줄이 이김 | ● ☰ 설정 ② 내 별칭(기본 별칭 덮기 됨 · 한글 못 씀) | — | 자주(C → COPY 등) | — | 공식 · 실무 | + +별칭이 오토캐드와 다른 것(근거 = 약함 — 공식 별칭 표 403 · 옛 판 acad.pgp 사본[2차]과 코덱스 공식 일부로 맞댐): + +| 친 글 | 오토캐드 acad.pgp | 우리 | 판정 | +|---|---|---|---| +| `R` | REDRAW(화면 다시 그림) | RECTANG(앞글자) | 다름 — 손버릇 `R` = 사각형 시작 | +| `RA` · `W` | REDRAWALL · WBLOCK | RAY · WIPEOUT(앞글자) | 다름 | +| `LE` | QLEADER(지시선) | LENGTHEN(앞글자) | 다름 — 지시선 대신 길이 바꾸기 | +| `UN` · `TO` · `TA` | UNITS · TOOLBAR · TEXTALIGN(옛 판 TABLET) | UNISOLATEOBJECTS · TOAREA · TABLE(앞글자) | 다름 | +| `HI` · `IN` · `EXT` | HIDE · INTERSECT · EXTRUDE(3D) | HIDEOBJECTS · INSERT · EXTEND(앞글자) | 다름 — `EXT` 는 점 물음 중 스냅 이름이기도(N14) | +| `SE` | DSETTINGS | SELECT | 다름 | +| `Q` · `LAY` · `DD` | 별칭 없음 | QUICKPROPERTIES · LAYER · DIMDIAMETER(앞글자) | 우리만 | +| `OS` · `DS` · `SN` | OSNAP · DSETTINGS · SNAP(설정 창) | 버림 | 설정 창 없음 | +| `V` · `PU` · `LTS` · `OP` · `ED` | VIEW · PURGE · LTSCALE · OPTIONS · TEXTEDIT | 버림 | 명령 없음(`ED` 는 `TEDIT` 로) | +| `P` · `Z` | PAN · ZOOM(옵션 여럿) | PAN 안내만 · ZOOM 범위만 | 일부 | +| `U` · `SAVE` | U(한 단계) · SAVE(이름 물음) | UNDO · QSAVE | 거의 같음 | + +- 같음 — 그리기 · 고치기 · 치수 · 재기 별칭 거의 전부(L PL C A REC POL EL SPL XL ML PO DO DIV ME H BH GD BO REG · M CO CP RO SC AL MI O AR TR EX S F CHA BR J X E LEN PE HE DR MA · T MT DT ST TEDIT DLI DAL DAN DAR DRA DDI DOR DBA DCO D MLD MLS TB TS · LA LAS COL LT LW CH MO PR QP G · DI AA MEA LI QC · B I RE EXP IAT). + +### ⑤ 그리기 · 고치기 명령마다 옵션(우리 31 명령) + +- 칸 뜻 — 셋째 = 오토캐드 옵션 글자 · 넷째 = 빈 Enter 뜻 · 앞 값 기억 · 다섯째 = 우리와 빠진 것. + +| # | 명령(별칭) | 오토캐드 옵션 글자 | 빈 Enter · 기본값 기억 | 우리 — 빠진 것 | 쓸모 | 실무 | 크기 | 근거 | +|---|---|---|---|---|---|---|---|---| +| 5-1 | LINE(L) | `C` 닫기 · `U` 취소 | 첫 점 Enter = 앞 선 · 호 끝에서 이음 | ◐ — C · U ● · 첫 점 Enter 이음 ○ · 명령행 `c` = 원(N6) | 중 | — | 작 | 공식 | +| 5-2 | PLINE(PL) | `A` 호 · `C` · `H` · `L` 길이 · `U` · `W` 폭 · 호 안 `CE` `D` `R` `S` | 시작 Enter = 이음 · 폭 기억(PLINEWID) | ○ 옵션 0 · `C` = 원으로 바뀌며 그리던 것 버림 · `U` = UNDO(H7) · Esc = 다 버림(N4) | 상 | — | 작(C · U) + 중 | 공식 | +| 5-3 | RECTANG(REC) | 첫 구석 `C` `E` `F` `T` `W` · 맞은 구석 `A` 넓이 · `D` 치수 · `R` 회전 | — | ○ `@x,y` · 대각 거리만 · D(길이 → 너비) ○ · 첫 구석 Esc ○(N3) | 상 | 꼭 · 자주(D) | 작(D) + 중 | 공식 · 실무 | +| 5-4 | CIRCLE(C) | `3P` `2P` `T`(Ttr) · 반지름 또는 `D` 지름 | 반지름 기억(CIRCLERAD) | ○ 중심 + 반지름만 · D ○ · 기억 ○ | 상 | 꼭 · 헷갈림(D) | 작(D) + 중 | 공식 · 실무 | +| 5-5 | ARC(A) | 시작 `C` · 둘째 `C` `E` · 끝 `A` `L` `D` `R` | 시작 Enter = 앞 선 끝 접선 호 · Ctrl = 시계 | ○ 세 점만 | 중 | — | 중 | 공식 | +| 5-6 | POLYGON(POL) | 변 수 · `E` 변 · `I` / `C` 내접 · 외접 | 변 수 기억(POLYSIDES) | ◐ — 면 수 `<6>` 상수 · 내접만 · E ○ | 하 | — | 작 | 공식 | +| 5-7 | ELLIPSE(EL) | 축 끝(기본) · `C` 중심 · `A` 호 · `R` 회전 | — | ◐ — 중심 방식만 · 셋째 숫자 ≠ 단축(N10) · 폴리선 근사 | 하 | — | 작~중 | 공식 | +| 5-8 | SPLINE(SPL) | `M` `K` `D` `O` · 다음 점 `U` `C` `L` | Enter = 끝 | ○ 점만 · Esc 다 버림(N4) | 하 | — | 중 | 공식 | +| 5-9 | XLINE(XL) | `H` `V` `A` `B` `O` | — | ○ 두 점만 | 중 | — | 작~중 | 공식 | +| 5-10 | OFFSET(O) | `T` 통과 · `E` 지움 · `L` 층 · 하위 `M` `U` `E`xit | 빈 Enter = 직전 거리(OFFSETDIST) · 물체 되풀이 | ◐ — 거리 `<1>` 상수 · 한 번 뒤 끝(되풀이 ○) · T · E · L · M · U ○ | 상 | 꼭 · 자주(되풀이) | 작~중 | 공식 · 실무 | +| 5-11 | FILLET(F) | `U` `P` `R` `T` `M` · Shift+둘째 = 0 | 물체 먼저 · 반지름 기억(FILLETRAD) | ◐ — 반지름 먼저 `<0>` 상수 · P · T · M · U · Shift ○ | 상 | 자주(Shift 0) | 중 | 공식 · 실무 | +| 5-12 | CHAMFER(CHA) | `U` `P` `D` `A` `T` `E` `M` · Shift 0 | 거리 기억(CHAMFERA~D) | ◐ — 거리 둘 먼저 `<1>` · 각 · 방식 · P · T · M ○ | 중 | — | 중 | 공식 | +| 5-13 | TRIM(TR) | 빠른(기본) = 바로 누름 · 끌기 경로 · Shift = 늘이기 · `T` `C` `O` `P` `R` `U` | 표준 빈 Enter = 모두 자르는 선 | ◐ — 누를 때마다 가까운 교차 사이 지움 · 멀리 눌러도 잘림(N9) · Esc ○(N3) · Shift · 끌기 · 옵션 ○ | 상 | 자주 · 헷갈림(Shift) | 중 | 공식 · 실무 | +| 5-14 | EXTEND(EX) | 빠른 · Shift = 자르기 · `B` `C` `F` `P` `E` `U` | 되풀이 · 빈 Enter = 끝 | ◐ — 경계 하나 · 선 하나 뒤 끝 · Shift ○ | 상 | 자주 · 헷갈림(Shift) | 작~중 | 공식 · 실무 | +| 5-15 | MOVE(M) | 기준점 `D` | 둘째 빈 Enter = 첫 점이 변위 | ◐ — D · 빈 Enter 변위 ○ · Esc 안 끝남(N2) · 도중 딴 명령 = 고른 것 사라짐(N1) | 상 | — | 작 | 공식 | +| 5-16 | COPY(CO · CP) | `D` · `O` · `M` · 둘째 `A` 배열 · `E` · `U` | 되풀이(COPYMODE 0) · 빈 Enter = 끝 | ◐ — 되풀이 ● · Enter 로 못 끝냄(Esc 만) · D · O · A · U ○ | 상 | — | 작 | 공식 | +| 5-17 | ROTATE(RO) | 각 · `C` 복사 · `R` 참조(`@` → 점 → 새 각) | — | ◐ — 각 숫자 · 기준선 누름 · 방향점 키보드 ○ · C · R ○ · N1 · N2 | 중 | — | 중 | 공식 | +| 5-18 | SCALE(SC) | 배율 · `C` · `R` 참조(길이 → 새 길이) | — | ◐ — 배율 · 기준선 누름 · C ○ · R 숫자 ○ · 기준점 누름만 · N1 · N2 | 중 | — | 중 | 공식 | +| 5-19 | STRETCH(S) | 걸침 창 · CP → 기준점 `D` | 둘째 빈 Enter = 변위 | ◐ — 선 = 기준점 가까운 끝만 · 그 밖 통째 옮김 · 걸침 안 꼭짓점만 옮기기 ○ | 상 | 꼭 · 헷갈림 | 중~큼 | 공식 · 실무 | +| 5-20 | MIRROR(MI) | `Y` · `N` | 원본 지울까 빈 Enter = 둠 | ◐ — 늘 둠 · Y/N 물음 ○ | 중 | — | 작 | 공식 | +| 5-21 | ARRAY(AR) | `R` 사각 · `PA` 경로 · `PO` 원형 · 행 · 열 · 사이각 · 채우기 각 | 빈 Enter = 사각(ARRAYTYPE) | ◐ — `L` 직선 · `R` 원형(⚠ 오토캐드 `R` = 사각) · 사각 · 경로 · 채우기 각 ○ · Esc 일부 ○(N3) · N17 | 중 | — | 중~큼 | 공식 | +| 5-22 | BREAK(BR) | 고른 점 = 첫 점 · `F` · 둘째 `@` = 그 자리 끊음 | — | ◐ — 첫 점 따로 누름 · F · `@` ○ | 중 | — | 작 | 공식 | +| 5-23 | JOIN(J) | 여러 개 한 번 · 호 → `L` 원 | — | ◐ — 끝점 맞닿은 것만 | 하 | — | 작~중 | 공식 | +| 5-24 | LENGTHEN(LEN) | `DE` · `P` · `T` · `DY` · 클릭 = 길이 보기 | 되풀이 | ◐ — 증분만 `<10>` · 선만 · 한 번 뒤 끝 | 중 | — | 작~중 | 공식 | +| 5-25 | DIVIDE(DIV) | 수 · `B` 블록 | — | ◐ — 수만 · B ○ | 하 | — | 중 | 공식 | +| 5-26 | MEASURE(ME) | 길이 · `B` 블록 | 고른 자리 가까운 끝부터 | ◐ — 길이만 `<10>` · B ○ | 중 | — | 중 | 공식 | +| 5-27 | DIST(DI) | `M` 여러 점 | 결과 = 거리 · 각 · ΔX · ΔY | ◐ — 두 점 · 알림 · M ○ | 하 | — | 작 | 공식 | +| 5-28 | TEXT(DT) | `J` 맞춤 · `S` 스타일 → 높이 → 각 | 높이 기억(TEXTSIZE) · Enter = 다음 줄 · 시작 Enter = 앞 글 아래 | ◐ — 시작점 → 한 줄 · 높이 · 각 안 물음 · J · S · 줄 이어 ○ | 중 | — | 중 | 공식 | +| 5-29 | MTEXT(T · MT) | 맞은 구석 `H` `J` `L` `R` `S` `W` `C` · 편집기 Ctrl+Enter · Esc | — | ◐ — 첫 코너 → 한 칸 글(`\P` 줄바꿈) · 맞은 코너 · 옵션 · 편집기 ○ | 중 | — | 중~큼 | 공식 | +| 5-30 | DIM 류 | DIM 한 명령 여럿 · 자리 `M` `T` `A` `H` `V` `R` · `Q` | 첫 점 Enter = 물체 고름 | ◐ — 점만 · 물체 고름 · M · T · A · H · V · R ○ · 가로/세로 자동 ● | 상 | — | 중 | 공식 | +| 5-31 | HATCH(H · BH) | 내부 점 · 물체 고름 · 무늬 · 축척 · 각 · `-HATCH` 옵션 | — | ◐ — 경계 물체 고름만 · 무늬 · 간격 · 각 고정 | 중 | — | 중 | 공식 | + +- 빈 Enter 뜻 갈림 — 오토캐드 = LINE · PLINE · ARC 첫 점 이음 · MOVE · COPY · STRETCH 둘째 점 = 변위 · OFFSET = 직전 거리 · MIRROR = 원본 둠 · COPY = 끝 / 우리 = MIRROR 만 같음. + +### ⑥ 켬끔 · 설정(시스템 변수 중 우리에 필요한 것) + +| # | 무엇 | 오토캐드 키 · 마우스 | 누르는 차례 | 우리 | 쓸모 | 실무 | 크기 | 근거 | +|---|---|---|---|---|---|---|---|---| +| 6-1 | 상시 스냅 종류 | OSMODE · `-OSNAP` | 기본 끝 · 중심 · 교차 · 연장 | ◐ — 종류 고르기 ○ · 끝 · 중간 · 중심 · 사분 · 교차 · 노드 늘 켬 | 중 | — | 작~중 | 공식 | +| 6-2 | 스냅 · 집기 크기 | APERTURE 10 · PICKBOX 3 | — | ◐ — 스냅 15px · 집기 10px 고정 | 하 | — | 작 | 공식 | +| 6-3 | 스냅 표지 · 자석 | AUTOSNAP 63 | — | ● | — | — | — | 공식 | +| 6-4 | 극 추적 각 | POLARANG 90 · 45 · 30 · 22.5 · 15 · 10 · 5 · 추가각 | — | ◐ — 45 고정 | 상 | — | 작 | 공식 | +| 6-5 | 극 각 기준 · Shift 추적점 | POLARMODE | 마지막 선분 기준 | ○ | 중 | — | 작 | 공식 | +| 6-6 | 임시 키 전체 켬끔 | TEMPOVERRIDES 1 | — | ○ (임시 키 없음) | 하 | — | 작 | 공식 | +| 6-7 | DI 꼴 | DYNMODE · DYNPICOORDS(둘째 점 상대) · DYNPIFORMAT(극) | — | ◐ — 늘 켬 · 칸 = 상대 극 · 친 `x,y` = 절대 | 중 | — | 작 | 공식 | +| 6-8 | 커서 곁 물음 | DYNPROMPT 1 | — | ● (끄기 ○) | — | — | — | 공식 | +| 6-9 | 먼저 고르고 명령 | PICKFIRST 1 | 고른 채 명령 | ◐ — 이동 · 복사 · 회전 · 축척 · 배열 바로 · 지우기 · 대칭 · 신축 Enter 한 번 더 · 선 · 원 · 사각형은 고름 풂 | 상 | — | 작 | 공식 | +| 6-10 | 결과 미리보기 | COMMANDPREVIEW 1 | TRIM · OFFSET · FILLET 결과 미리 | ○ | 상 | — | 중 | 공식 | +| 6-11 | 휠 · 가운데 단추 | ZOOMWHEEL 0(밂 = 확대) · ZOOMFACTOR 60 · MBUTTONPAN 1 | — | ◐ — 방향 ☰ ● · 기본 거꾸로 · 배율 칸 ○ | 중 | — | 작 | 공식 | +| 6-12 | 각 단위 · 0° · 돌림 | AUNITS 0 · ANGBASE 0 · ANGDIR 0 | 십진 도 · 동쪽 · 반시계 | ● 같음(바꿀 칸 없음) | — | — | — | 공식 | +| 6-13 | 명령 기본값 기억 | OFFSETDIST · FILLETRAD · CHAMFERA~D · CIRCLERAD · POLYSIDES · PLINEWID · TEXTSIZE | 도면 · 세션에 남음 | ○ 상수 | 상 | — | 작~중 | 공식 | +| 6-14 | 명령 방식 기본 | COPYMODE · TRIMEXTENDMODE · EDGEMODE · MIRRTEXT · ARRAYTYPE · TRIMMODE | — | ○ 고정 | 하 | — | 작 | 공식 | +| 6-15 | 켬 상태 기억 | ORTHOMODE · OSMODE · POLARANG 등 도면 · 사용자 설정에 남음 | — | ○ 메모리(새로 열면 기본) | 중 | — | 작 | 공식 | + +## 2. 흠(급함 차례 · 앞 H1~H12 모두 지금도 남음 · K2 9c0ef7f82 다시 확인) + +- 확인 = 코드(K2 코드 읽기) · 확인됨(브레인 + K3 코드 확인) · 화면 재현 필요(코드 흐름 추정 · 화면에서 다시 해 봐야 함). + +| 차례 | # | 무엇이 틀림(화면 말) → 오토캐드는 | 확인 | 급함 | 크기 | 묶음 | +|---|---|---|---|---|---|---| +| 1 | H1 | Ctrl · Alt+글자가 명령 글자로 쌓임 — Ctrl+C 뒤 Enter · 우클릭 = 원 · Ctrl+X = 분해 · Ctrl+1 = 거리 1 점 → 오토캐드 Ctrl+C = 복사 | 코드 | 높음 | 작 | ⓪ | +| 2 | H2 | 확정 도면 · 미리보기가 그립 · Delete · 특성 칸 · 도면층으로 고쳐짐(DXF 내보내기에 실림) | 코드 | 높음 | 작~중 | ⓪ | +| 3 | N1 | 이동 · 회전 · 축척 도중 딴 명령(글 · 별칭 · 리본) → 고른 물체가 도면에서 빠진 채 남음 → 오토캐드는 점 묻는 중 명령 이름을 안 받음 | 화면 재현 필요 | 높음 | 작~중 | ⓪ | +| 4 | N7 | 명령 안 「객체 선택」 중 고른 물체 그립 10px 안 누름 → 명령이 그립 편집으로 바뀌어 사라짐 | 화면 재현 필요 | 높음 | 작 | ⓪ | +| 5 | H3 | 그립을 누르기만 해도 점이 누른 자리로 밀림 → 오토캐드 = 누르면 그립이 뜨거워질 뿐 | 화면 재현 필요 | 높음 | 작 | ⓪ | +| 6 | N6 | 명령행 칸 소문자 — 선 중 `c` → 원 · `u` → UNDO · 배열 `r` → 직선 · `end` 버림 → 오토캐드 = 대소문자 무관 | 확인됨 — `ime-sink.ts:29-33` 대문자 선택지와 그대로 맞댐 · `input-controller.ts:168-170` 원문 그대로 넘김 | 높음 | 작 | ② | +| 7 | N18(새) | 앞글자만 쳐도 첫 후보 실행 — `R` → 사각형 · `W` → 가림막 · `LE` → 길이 바꾸기 · `EXT` → 연장 · `UN` → 숨김 풂 → 오토캐드 `R` = 다시 그림 · 앞글자는 후보 목록(Enter 동작은 공식 글 없음) | 확인됨 — `commands/registry.ts:125-132` 이름 > 내 별칭 > 기본 별칭 > `matchCommandPrefixes(text)[0]` | 높음 | 작 | ⓪ | +| 8 | H7 | 점 찍는 명령 중 `C` · `U` — 선만 받음 · 폴리선에서 `C` → 원으로 바뀌며 그리던 것 버림 · `U` → 앞 편집 되돌림 | 코드 | 중간 | 작 | ② | +| 9 | N4 | 폴리선 · 스플라인 그리던 중 Esc · Ctrl+Z → 그린 데까지 다 버림 | 코드 | 중간 | 작 | ② | +| 10 | N2 | 이동 · 복사 · 회전 · 축척 · 배열이 Esc 로 안 끝나고 고르기 단계로 돌아가 남음 | 코드 | 중간 | 작 | ② | +| 11 | N3 | Esc 처리 없음 — 사각형 첫 모서리 · 자르기 · 배열 방식 · 개수 · 원형 각도 | 코드 | 중간 | 작 | ② | +| 12 | N9 | 자르기가 누른 자리에서 멀어도 가장 가까운 물체를 자름 · 사각형 자르면 변 넷이 지금 층 선으로 | 화면 재현 필요 | 중간 | 작 | ⓪ | +| 13 | H5 | 명령 중 고르기가 누를 때마다 바뀜 → 셋 누르고 Enter 하면 하나만 옮겨짐 · 끌기 상자 안 됨 | 코드 | 중간 | 작 | ③ | +| 14 | H4 | Ctrl+상자 = 안 고른 것만 남음 · Shift+상자 = 새로 · 상자 = 늘 앞 고름 버림 | 코드 | 중간 | 작 | ③ | +| 15 | N5 | 숫자 칸 `3.2*1.5` → 3.2 · `2m` → 2 로 조용히 먹음 → 오토캐드 = 숫자 아니면 다시 물음 | 코드 | 중간 | 작 | ⓪ | +| 16 | N14 | 스냅 이름 `EXT` · `INS` → 연장 · 넣기 명령이 돌아 지금 명령 버림 · `TAN` `QUA` `NOD` `APP` `PAR` `NONE` 조용히 버림 | 코드 | 중간 | 작 | ⓪ | +| 17 | H8 | F3 끄면 직교 · 극 추적도 꺼짐 · 직교가 축 15 안에서만 붙음(강제 아님) | 코드 | 중간 | 작 | ⓪ | +| 18 | H6 | 꺼진 층 · 숨긴 물체가 골라지고 스냅됨 · Ctrl+A → Delete 면 같이 지워짐 | 화면 재현 필요 | 중간 | 작 | ⓪ | +| 19 | N8 | 그립 끄는 중 [길이][각도] 칸이 뜨나 길이 + Tab · Enter · 거리 숫자 무시 | 코드 | 낮음 | 작 | ③ | +| 20 | H9 | 직교 · 극 가이드 잡는 거리 = 도면 단위 15(줌 따라 달라짐) | 화면 재현 필요 | 낮음 | 작 | ⓪ | +| 21 | H11 | 명령행 ↑ 기록에 알림 줄(`ERASE: 확정 도면 …`) 섞임 | 코드 | 낮음 | 작 | ② | +| 22 | H12 | 명령행 칸 한글 자판 → 자모 그대로 → 명령 못 찾음 | 화면 재현 필요 | 낮음 | 작 | ④ | +| 23 | N11 | 되돌리기 두 길 — `U` = 명령 안 끊음 · Ctrl+Z = 명령도 취소 · ↶ 단추 뒤 빈 Enter = UNDO 되풀이 | 코드 | 낮음 | 작 | ② | +| 24 | N16 | 그리기 중 두 번 누름 = 점 둘(선 = 0 길이) + 글자 위면 글 칸도 뜸 | 화면 재현 필요 | 낮음 | 작 | ③ | +| 25 | N10 | 타원 셋째(단축) 숫자 ≠ 단축 반지름 | 코드 | 낮음 | 작 | ⑤ | +| 26 | N17 | 배열 직선 「간격 시작점」에 `x,y` → 복사 0 · 원형 각도 0 → 복사 0 | 코드 | 낮음 | 작 | ⑤ | +| 27 | N12 | 캔버스 `10, 20` → 빈칸 = Enter 라 `10,` 먼저 제출 → 오토캐드도 Space = Enter(같음 · 안내만) | 코드 | 낮음 | 작 | ① | +| 28 | N15 | 커서가 앞 점 위일 때 거리 숫자 → 방향 0 → 오류 알림 | 화면 재현 필요 | 낮음 | 작 | ① | +| 29 | N13 | 맥 ⌘V → 붙여넣기 대신 `V` 글자 | 화면 재현 필요 | 낮음 | 작 | ④ | +| 30 | H10 | 그리드(F7) = 화면에 박힌 24px 무늬(눈금 아님) | 코드 | 낮음 | 작 | ⑥ | + +- 먼저 막을 일곱(1~7) — 도면을 몰래 바꾸거나 엉뚱한 명령이 도는 것 · B06 상세는 고친 것이 곧바로 세션 초안이라 더 급함(335 그대로). +- 시험 — 고르기 · 그립 누르기 · F키 · 보기 전용 새는 길 · 대소문자 · 앞글자 실행 시험이 0 → 흠 고칠 때 `resources/tester/cad/` 에 시험 틀부터. + +## 3. 보강 묶음 · 차례 + +| 묶음 | 화면에서 무엇이 바뀌나(예) | 담는 줄 | 크기(앞몫 / 전체 · 날) | 먼저 할 관계 | +|---|---|---|---|---| +| ⓪ 흠 막기 | Ctrl+C 가 원을 안 그림(복사로) · 확정 도면이 그대로 · 이동 중 딴 명령을 쳐도 물체가 안 사라짐 · `R` 이 사각형을 안 시작 · 그립 눌러도 안 밀림 | 2장 ⓪ 표시 12(H1 · H2 · H3 · N1 · N7 · N18 · N9 · N5 · N14 · H8 · H6 · H9) | 4(1~7 중 N6 뺀 여섯) / 7 | 전부의 바닥 · N18 은 정할 것 15 뒤 | +| ② 명령 중 키 | 모든 명령 물음에 `[옵션]` 글자 · 소문자도 됨 · 간격 거리 · 모깎기 반지름을 다음에 꺾쇠로 · Esc 한 번 = 명령 끝 · 점 묻는 중 명령 이름은 「점 아님」 · ↑ = 앞에 친 값 | 2-2~2-17 · 2-21 · 6-13 · N6 · H7 · N2~N4 · H11 · N11 | 4.5(옵션 틀 · 대소문자 · 기본값 기억 · Esc 통일) / 11.5 | ⓪ 뒤 · ⑤ 의 틀 | +| ⑤ 명령 옵션 채우기 | 사각형 `D` 로 가로 · 세로 · 원 `D` 지름 · 간격띄우기 되풀이 · 모깎기 Shift = 0 · 자르기 Shift = 늘이기 · 폴리선 `C` 닫기 · 이동 둘째 점 Enter = 변위 | 5-1~5-31 · N10 · N17 | 6.5(5-2 C · U · 5-3 D · 5-4 D · 5-10 · 5-11 · 5-13 · 5-14 · 5-15 · 5-16) / 40 안팎 | ② 옵션 틀 뒤(점 물음에서 `D` 를 옵션으로 받아야 함) | +| ① 숫자 넣기 | `@` 하나 = 마지막 점 · 선 첫 점 Enter = 이어 그림 · `#0,0` · `FROM` → 기준점 → `@0.5,0` · `M2P` · `3.2*1.5` · `30d15'` | 1-10~1-25 · N12 · N15 | 3(1-10 · 1-13 · 1-15 · 1-17 · 1-18 · 1-19) / 12.5 | ② 와 따로 됨 · FROM · M2P 를 Shift+우클릭 메뉴에도 두려면 ③ 메뉴 틀 | +| ③ 마우스 | 하나씩 눌러 더하고 Shift 로 빼기 · 명령 중 끌기 상자 · `P` 앞 고름 · 우클릭 메뉴 · Shift+우클릭 스냅 메뉴 · 휠 두 번 = 전체 · 그립 누르고-옮기고-누르기 · 그립에 숫자 | 3-2~3-30 · H4 · H5 · N8 · N16 | 7(3-2 · 3-4 · 3-6 · 3-10~3-12 · 3-14 · 3-20 · 3-21) / 23.5 | ⓪ H3 뒤 그립 · 메뉴 틀(3-2) → Shift+우클릭(3-4) · 그립 메뉴 | +| ④ 단축키 · 임시 키 · 별칭 | Ctrl+Shift+C · V · Ctrl+1 이 오토캐드처럼 · Shift 누르는 동안 직교 · Shift+E 누른 동안 끝점만 · 별칭 표 손질 | 4-1~4-23 · 별칭 표 · H12 · N13 | 1.5(4-12 · 4-14 · 4-18 · 4-22) / 7.5 | ⓪ H1(Ctrl 거름 · Ctrl+C/X) 뒤 · 정할 것 16 · 24 | +| ⑥ 켬끔 기억 | 스냅 · 직교 · 극 각을 다음에 열어도 그대로 · 극 각 30 · 15 고르기 · 고른 채 명령 바로 · 스냅 종류 끄기 | 6-1~6-12 · 6-14 · 6-15 · H10(6-13 = ②) | 1.5(6-4 · 6-9 · 6-15) / 7.5 | ☰ 설정 CAD 탭(332 · 이미 있음) 위 | + +- 추천 차례 — ⓪ → ② 앞몫 → ⑤ 앞몫 → ① 앞몫 → ③ 앞몫 → ④ 앞몫 → ⑥ 앞몫(앞몫 합 어림 28 일) → 나머지 몫은 써 본 뒤 고름. 까닭 — 이번 요구의 중심 = 키보드로 숫자 · 형상 넣기 → 옵션 틀 · 명령 옵션 · 숫자 꼴을 마우스보다 앞에 둠. +- 둘 곳 — B06 상세 · M02 카드는 리본 · 상태막대 · 명령행이 없음 → 새 기능은 키 · 글 명령 · 우클릭 메뉴 · ☰ 설정으로도(세 곳이 한 앱 · 한 번 고치면 같이 바뀜). +- 크기 셈 = 작 0.5 · 작~중 1 · 중 1.5 · 중~큼 2.5 · 큼 4 일(코드 읽기 어림 · 시험 포함 · 화면 확인 별도). +- 335 묶음과의 관계 — 335 ① 좌표 → ① · ② 스냅 → ①(한 번 스냅 · TT) · ④(임시 키) · ⑥(극 각 · 스냅 종류) · ③ 고르기 · ④ 그립 · ⑤ 우클릭 · 휠 두 번 · 두 번 누름 → ③ · ⑤ Ctrl 조합 → ⓪ · ④ · ⑥ 명령줄 → ② · ④ · ⑦ 자동화 · D2 비탈 1:n · D14 손가락 → 범위 밖. + +## 4. 정할 것 + +### 4-1. 335 에서 이번 범위에 남는 것(1~14) + +| # | 무엇 | 지금 | 오토캐드 | 갈래(이번 덧) | 걸리는 묶음 | +|---|---|---|---|---|---| +| 1 | 빼기 · 더하기 키 | Shift = 더함 · Ctrl = 넣고 빼기 · 상자 = 새로 | 누름 = 늘 더함 · Shift = 빼기 | (가) 오토캐드식 (나) 지금 + 상자도 더함 (다) ☰ 설정 — 덧: 실무 「Shift 빼기 = 꼭 · 자주」 | ③ | +| 2 | 명령 중 누름 | 바꿈 | 더함 | 1 과 같이 | ③ | +| 3 | 우클릭 | 늘 Enter(반복 안 함) | 기본 = 메뉴 · 설정으로 Enter · 짧게/길게 | (가) 지금 + 끝난 뒤 우클릭 = 반복 + Shift+우클릭 메뉴 (나) 쉴 때 · 고른 채 = 메뉴 (다) 짧게 Enter · 길게 메뉴 — 덧: 실무는 우클릭 = Enter 로 바꿔 씀(출처 확인) | ③ | +| 4 | 극 추적 각 | 45 고정 | 증분 목록 + 추가각 | 목록 · 기본값(비탈 1:n 추가각은 범위 밖) | ⑥ | +| 5 | 직교 | 축 15 안에서만 | 강제 | (가) 강제 (나) 지금 | ⓪ H8 | +| 6 | 그립 방식 | 끌어 놓기(안 끌어도 적용) | 누르고 · 옮기고 · 누르기 | (가) 둘 다(끌면 지금 · 안 끌면 대기) (나) 오토캐드식만 | ⓪ H3 · ③ | +| 7 | 점 묻는 중 명령 이름 | 지금 명령 버리고 새 명령 | 명령 이름은 「명령:」 물음에서만 | (가) 점 물음에선 「점 · 옵션 아님」 (나) 지금 — 덧: 공식(간접) 근거로 (가) 쪽 | ② · ⓪ N1 | +| 8 | Ctrl+C · X 클립보드 범위 | 한 화면 메모리 | 시스템 클립보드 | (가) 한 화면 (나) CAD 화면끼리 (다) 오토캐드와 DXF 글 | ⓪ H1 · ④ | +| 9 | Tab | DI 칸 넘김 | 칸 넘김 + 스냅 후보 돌기 | 글이 비고 스냅 표지 있으면 후보 돌기 / 안 넣음 | ① | +| 10 | 그리드 F7 · F9 | 화면 무늬 | 도면 단위 그리드 · 격자 스냅 | (가) 뺌 (나) 도면 단위 + 격자 스냅 | ⑥ · ④ | +| 11 | 한글 | 명령행 칸 자모 그대로 | — | 자모 → 영문 자리 바꿈 · 한글 명령 이름 찾기 | ④ | +| 12 | 켬 상태 기억 | 메모리 | 도면 · 사용자 설정 | ☰ 설정 기본값으로 남길지 · 무엇을 | ⑥ | +| 13 | 보기 전용 범위 | 명령만 막음 | — | 고르기 · 재기 · 내보내기만 · 그립 안 그림 · Delete · 특성 · 층 막음 | ⓪ H2 | +| 14 | 브라우저가 쥔 키 | F1 · F5 · F11 · F12 · Ctrl+W/N/T = 브라우저 | 도움말 · 스냅 추적 · DI · 선택 순환 | 실측 뒤 · 안 되면 다른 키(예: 스냅 추적 = 상태막대 + 다른 키) — 덧: F1 · F12 · Ctrl+W 까지 넓힘 | ④ | + +### 4-2. 새로 생긴 것 + +| # | 무엇 | 지금 | 오토캐드 | 갈래 | 걸리는 묶음 | +|---|---|---|---|---|---| +| 15 | 앞글자 실행 | `R` → 사각형 등 첫 후보 실행(N18) | 별칭 · 이름이 딱 맞을 때 실행 · 앞글자 = 후보 목록 · Tab 돌림 | (가) 없앰 — 후보만 보이고 고름 (나) 지금 (다) 후보가 하나일 때만 | ⓪ | +| 16 | 기본 별칭을 acad.pgp 와 맞춤 | `SE` = SELECT · `OS` 등 없음 · 앞글자로 엉뚱 | `R` · `RA` · `W` · `LE` · `UN` · `SE` … | (가) 맞춤(우리에 없는 명령은 비워 둠) (나) 지금 (다) ☰ 설정 「오토캐드식 별칭」 켬 | ④ | +| 17 | 대소문자 | 캔버스 = 대문자로 · 명령행 칸 = 원문 | 무관 | (가) 명령행도 대문자로 판정(글 물음만 원문) — 추천 (나) 지금 | ② | +| 18 | 둘째 점 `x,y` 뜻 | 늘 절대 | DI 켬 = 상대 · `#` = 절대 / 명령줄 = 절대 | (가) 지금 + `#` 받기 (나) DI 꼴(상대) + `#` (다) ☰ 설정 | ① · ⑥ | +| 19 | 식 | 버림 · 숫자 칸은 앞 숫자만 | `'CAL` 뒤에만 | (가) 숫자 칸에 바로 `3.2*1.5` (나) `'CAL` 뒤 (다) 둘 다 | ① | +| 20 | Esc 한 번 뜻 | 도구마다 다름 | 명령 끝(단계별 동작 공식 확인 못 함) | (가) 명령 끝 · 끝낸 토막 · 사본은 남김 (나) 한 단계 뒤로 (다) 지금 | ② | +| 21 | 기본값 기억 범위 | 상수 | 도면 · 세션에 남음 | (가) 세션 (나) 도면에 저장 (다) 계정(☰) | ② · ⑥ | +| 22 | 휠 기본 방향 | 당기면 확대(☰ 에서 바꿈) | 밀면 확대 | (가) 기본을 오토캐드식으로 (나) 지금 — 사용자 손버릇 | ③ · ⑥ | +| 23 | 옵션 글자를 오토캐드 글자로 | 배열 `L` 직선 · `R` 원형 | `R` 사각 · `PO` 원형 · `PA` 경로 | (가) 오토캐드 글자 (나) 지금 + 안내 | ⑤ | +| 24 | 임시 우선 키(Shift+글자) | 없음 · Shift+글자 = 대문자 | 누르는 동안 스냅 · 직교 | (가) 친 글이 빌 때만 임시 키 (나) Shift 직교만 (다) 안 넣음 | ④ | + +- 먼저 정할 것 — 15 · 16 · 17 · 7 · 20(⓪ · ② 앞몫) → 23 · 21(⑤ 앞몫) → 18 · 19(① 앞몫) → 1 · 2 · 3 · 6(③ 앞몫) → 나머지. + +## 5. 범위 밖 · 근거 약함 · 확인 못 함 + +### 5-1. 범위 밖(한 줄씩만) + +- 다른 CAD(BricsCAD Quad · ZWCAD 몸짓 · 335 9-1~9-3) · 리습 · 스크립트 · 매크로 · 도구 팔레트(335 ⑦) · AI. +- 토목 전용 — 비탈 비율 각 `<1:1.5`(335 D2) · 임도 표기 도구(226 재조사 ⑩). +- 손가락 · 펜(H15 · D14 · 226 재조사 ⑨) · 3D(F4 · F6 · Ctrl+Shift+휠 · 동적 UCS). +- 226 재조사의 H13(치수 · 재기 숫자 = 종이 길이) · H14(횡단 좌우 거울) = 범위 밖 · 따로 남은 흠(재조사 문서 그대로). + +### 5-2. 근거 약함 + +- 안티그래비티 33 줄 중 30 줄 = 공식 재료와 맞음 → 그대로 · 공식 밖 세부 셋은 출처를 열어 봄: + - 17(PGP 왼손 배치) — 출처(eyehole-tech.com/autocad-acad-pgp/)는 우클릭 = Enter 설정 · `C` → COPY · `CC` → CIRCLE · `Q` → QSAVE 고쳐 쓰기만 말함 · 「Q · W · E · A · S · D 왼손 배치」 · REINIT 은 그 쪽에 없음(REINIT 자체는 공식) → 왼손 배치 = 약함. + - 10 · 11(F10 · F11) — 출처(cad2012.tistory.com 추적 글)에 「F8 과 서로 배타」 · 「1초 머묾」 없음 → 세부 약함(둘 다 우리 동작과는 같음). + - 27(사각형 D) — 출처(yjk923.tistory.com STRETCH 글)에 사각형 D 없음 · 내용은 공식(RECTANG)과 맞음 → 출처만 틀림. +- 별칭 다른 것 표 — 공식 별칭 표(autodesk.com/shortcuts/autocad) 403 → 옛 판 acad.pgp 사본(stress-free.co.nz · AutoCAD 2002 무렵 · 2차)으로 맞댐 · `ED` = DDEDIT → 새 판 TEXTEDIT · `TA` = TABLET → 새 판 TEXTALIGN 은 기억. +- 2-15(점 묻는 중 명령 이름 거절) — 공식 AutoLISP 참조 `command` 쪽 「AutoCAD recognizes command names only when it issues a Command prompt」 = 간접 근거 · 거절 문구(「Point or option keyword required」)는 포럼 · 2차. + +### 5-3. 확인 못 함 + +- 오토캐드 쪽 — 앞글자만 친 뒤 Enter 가 첫 후보를 돌리는지(공식 명령 창 쪽은 「Enter · Space = 시작」 · Tab 돌림만 적음) · 명령 중 Ctrl+Z · Y · Ctrl+우클릭 · `<<` 각 · 그립 모드 프롬프트 원문 · 단계별 Esc(MTEXT 편집기만 확인) · 2026 판 명령 프롬프트 원문(K1 = 2015~2016 원문으로 대신). +- 우리 쪽 — 화면 재현 필요 줄(H3 · H6 · H9 · H12 · N1 · N7 · N9 · N13 · N15 · N16) · 1-26 친 좌표가 스냅을 이기는지(추정) · iframe 안에서 F1 · F5 · F11 · F12 · Ctrl+W/T/N 이 브라우저로 가는지. +- 크기 · 날 = 코드 읽기 어림 · 쓸모 = 내장 CAD 보강 판단(임도 도면에서 무엇을 자주 고치는지 자료 없음 → 사용자 확인). +- 출처 — 공식 = K1 출처 목록(`2026-10-15_335_부록4_오토캐드마우스키보드원문.md` K1 절 · 출처 URL 절) · 코덱스 표 안 URL / 이번에 연 쪽 = help.autodesk.com AutoLISP `command`(GUID-1C989B35 · 2023) · 명령 창 입력(GUID-BB0C3E79 · 2026 LT) · eyehole-tech.com/autocad-acad-pgp/ · cad2012.tistory.com(추적 글) · yjk923.tistory.com(STRETCH 글) · stress-free.co.nz acad_pgp_aliases_06.html. diff --git a/docs/raw/조사/2026-10-15_335_부록4_오토캐드마우스키보드원문.md b/docs/raw/조사/2026-10-15_335_부록4_오토캐드마우스키보드원문.md new file mode 100644 index 000000000..0d2f0af1f --- /dev/null +++ b/docs/raw/조사/2026-10-15_335_부록4_오토캐드마우스키보드원문.md @@ -0,0 +1,1122 @@ +# 부록4 — 오토캐드 마우스 · 키보드 조사 원문 (요구 227-3 · PLAN 335-4) + +- 차례 = 1 코덱스 · 2 안티그래비티 · 3 K1(+ 출처 URL) · 4 K2 — 원문 그대로 이어 붙임(머리만 더함 · 내용 손대지 않음) · 본문 = `2026-10-15_335_다시조사_오토캐드마우스키보드.md`. + +--- + +## 부록 1. 코덱스 — `codex_227.md` + +# AutoCAD 마우스·키보드·숫자 입력 조작 조사 + +- 범위: Autodesk 공식 AutoCAD 도움말에서 확인한 명령줄 중심 조작임. 제품 버전·Windows/Mac·사용자 CUI/PGP 설정에 따라 키와 프롬프트가 달라질 수 있으므로 표기된 기본 사례의 범위를 넘겨 보편 동작으로 단정하지 않음. +- 표 읽기: `무엇`은 입력/조작, `차례·프롬프트`는 대표 진행, `결과·주의`는 동작 의미임. 프롬프트는 현지화·설정에 따라 차이가 있어 공식 영문 프롬프트의 핵심 옵션을 보존함. + +## 1. 단축키·기능키·임시 우선 키·명령 별칭 + +| 무엇(조작·키) | 어떻게(누르는 차례·프롬프트) | 결과·주의 | Autodesk 공식 출처 | +|---|---|---|---| +| 명령줄·팔레트 표시 | Ctrl+0, Ctrl+1, Ctrl+2, Ctrl+3, Ctrl+4, Ctrl+6, Ctrl+7, Ctrl+8, Ctrl+9 | 화면 정리, Properties, DesignCenter, Tool Palettes, Sheet Set, DBConnect, Markup Set, QuickCalc, Command Line 표시/전환임 | [AutoCAD shortcuts](https://www.autodesk.com/shortcuts/autocad) | +| 복사·클립보드·실행 취소 | Ctrl+C/X/V, Ctrl+Shift+C(기준점 복사), Ctrl+Shift+V(블록으로 붙여넣기), Ctrl+Z/Y | 복사·잘라내기·붙여넣기·기준점 복사·블록 붙여넣기·Undo/Redo임 | [AutoCAD shortcuts](https://www.autodesk.com/shortcuts/autocad) | +| 파일·창 조작 | Ctrl+N/O/S/P, Ctrl+Tab·Ctrl+Shift+Tab, Ctrl+PageUp·PageDown, Ctrl+Q | 새로 만들기·열기·저장·플롯·열린 도면 전환·도면 탭 전환·종료임 | [AutoCAD shortcuts](https://www.autodesk.com/shortcuts/autocad) | +| 보기·상태 토글 | Ctrl+D(동적 UCS 상태), Ctrl+E(등각 평면 순환), Ctrl+F(객체 스냅 전환), Ctrl+G(그리드), Ctrl+H(선택 스타일), Ctrl+I(좌표), Ctrl+Shift+H(팔레트 숨김), Ctrl+Shift+I(추론 구속조건) | 표시/보조 기능 전환임. 사용자 키맵·제품 변형에 따라 다를 수 있음 | [AutoCAD shortcuts](https://www.autodesk.com/shortcuts/autocad) | +| 기능키 F1–F4 | F1 도움말, F2 문자 화면, F3 객체 스냅, F4 3D 객체 스냅 | 도움말·명령 기록/문자 화면·OSNAP·3D OSNAP 토글임 | [AutoCAD shortcuts](https://www.autodesk.com/shortcuts/autocad) | +| 기능키 F5–F8 | F5 등각 평면, F6 동적 UCS, F7 그리드, F8 직교 모드 | 등각 평면 순환·동적 UCS·그리드·직교 제한 토글임 | [AutoCAD shortcuts](https://www.autodesk.com/shortcuts/autocad) | +| 기능키 F9–F12 | F9 스냅 모드, F10 극좌표 추적, F11 객체 스냅 추적, F12 동적 입력 | 상태바 제도 보조 토글임 | [AutoCAD shortcuts](https://www.autodesk.com/shortcuts/autocad) | +| 임시 우선 키 기본 동작 | 점 지정 중 Shift를 누른 채 유지하면 ORTHO 임시 전환, Shift+' OSNAP 전환, Shift+. 극좌표 추적, Shift+/ UCSDETECT 전환 | 키를 누르는 동안/키 다운·업 매크로의 임시 보조 동작임. CUIx와 키보드 배열에 따라 사용자화 가능함 | [Temporary Override Keys](https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Customization/files/GUID-B4AC255C-DF3E-44BB-9235-6E91D53CBA90.htm), [CUI temporary overrides](https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Customization/files/GUID-EEC8DC54-1B73-4856-8CC8-228AECDDFE3C.htm) | +| 임시 OSNAP 키 | Shift+A(스냅 추적), Shift+C(중심), Shift+E(끝점), Shift+M(중간점), Shift+P(끝점), Shift+V(중간점) | 한 점 지정용 스냅 재정의/추적임. 같은 기능에 복수 키가 배정된 항목 및 중복은 기본 CUI 참조표 기준임 | [Temporary Override Keys](https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Customization/files/GUID-B4AC255C-DF3E-44BB-9235-6E91D53CBA90.htm) | +| 임시 스냅·추적 제어 키 | Shift+D·Shift+L(모든 스냅/추적 억제), Shift+Q(OSNAP 추적 토글), Shift+S(OSNAP 강제), Shift+X(극좌표 전환), Shift+Z(UCSDETECT) | 키 재정의의 기본 매핑임. 의미·가용성은 CUIx에서 바뀔 수 있음 | [Temporary Override Keys](https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Customization/files/GUID-B4AC255C-DF3E-44BB-9235-6E91D53CBA90.htm) | +| 그 밖의 임시 키 | Shift+, 중심 재정의, Shift+] 객체 스냅 추적, Shift+[ 배치 제안, Shift+; OSNAP enforcement, Shift+W 배치 제안, Shift+’ OSNAP, Shift+. Polar, Shift+/ UCSDETECT | 일부는 기능·키보드 레이아웃 의존임. 표의 사용자 지정 가능성 전제 필요함 | [Temporary Override Keys](https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Customization/files/GUID-B4AC255C-DF3E-44BB-9235-6E91D53CBA90.htm) | +| 키보드 한 글자 별칭·원 그리기 | 명령줄 `A`=ARC, `C`=CIRCLE, `L`=LINE, `PL`=PLINE, `REC`=RECTANG, `POL`=POLYGON 입력 후 Enter | 별칭은 명령 실행의 짧은 이름이며 기본 PGP 및 사용자 설정에 따라 달라짐 | [AutoCAD shortcuts](https://www.autodesk.com/shortcuts/autocad), [Create command aliases](https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Customization/files/GUID-7BFFBEE9-63F8-4C2B-A352-A8DCE1E8B4FC.htm) | +| 편집 별칭 | `CO`=COPY, `M`=MOVE, `MI`=MIRROR, `O`=OFFSET, `RO`=ROTATE, `SC`=SCALE, `F`=FILLET, `CHA`=CHAMFER, `TR`=TRIM, `EX`=EXTEND, `J`=JOIN | 명령줄에서 별칭→Enter→선택/점 입력 순임. 설치별 PGP가 최종 매핑임 | [AutoCAD shortcuts](https://www.autodesk.com/shortcuts/autocad), [PGP aliases](https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Customization/files/GUID-7BFFBEE9-63F8-4C2B-A352-A8DCE1E8B4FC.htm) | +| 치수·측정 별칭 | `D`=DIMSTYLE, `DAN`=DIMANGULAR, `DAR`=DIMARC, `DBA`=DIMBASELINE, `DOR`=DIMORDINATE, `DRA`=DIMRADIUS, `DT`=TEXT, `DS`=DSETTINGS, `ME`=MEASURE, `MEA`=MEASUREGEOM | DIM은 단일 통합 명령, 별도 DIM* 명령·스타일 명령 구별 필요함 | [AutoCAD shortcuts](https://www.autodesk.com/shortcuts/autocad), [PGP aliases](https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Customization/files/GUID-7BFFBEE9-63F8-4C2B-A352-A8DCE1E8B4FC.htm) | +| 해치·문자 별칭 | `H`=HATCH, `MT`=MTEXT, `T`/`DT`=TEXT 계열(설치 PGP 대조 필요) | 별칭 문서는 편집 가능함. 명령 별칭과 명령 이름을 혼동하지 않아야 함 | [AutoCAD shortcuts](https://www.autodesk.com/shortcuts/autocad), [PGP aliases](https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Customization/files/GUID-7BFFBEE9-63F8-4C2B-A352-A8DCE1E8B4FC.htm) | +| 별칭 사전·사용자화 | `acad.pgp`의 별칭 항목을 읽고 명령줄에서 별칭 입력 후 Enter; 별칭 추가/수정 후 파일 재로드 | PGP는 별칭→명령 대응 텍스트 파일이며 사용자화/후속 정의 우선 여부를 설치본에서 확인해야 함 | [Create command aliases](https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Customization/files/GUID-7BFFBEE9-63F8-4C2B-A352-A8DCE1E8B4FC.htm), [Edit command aliases](https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-LT-Customization/files/GUID-D4CACED6-DFBA-43C3-BC42-8D980AB3AE75.htm) | +| 기타 키 | Alt 조합 전체 및 Shift+A/E/X/Q/S/D/L/P/M/C/V를 전부 포괄하는 고정 공통표 | 확인한 Autodesk 키보드/임시 재정의 표에 일치하지 않는 Alt 조합·일부 사용자 키의 전역 고정 의미는 확인 못 함. 현재 CUIx·사용자 단축키에 따라 달라지는 영역임 | [AutoCAD shortcuts](https://www.autodesk.com/shortcuts/autocad), [Temporary Override Keys](https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Customization/files/GUID-B4AC255C-DF3E-44BB-9235-6E91D53CBA90.htm) | + +## 2. 숫자·좌표·동적 입력 + +| 무엇(입력 꼴) | 어떻게(누르는 차례·프롬프트) | 결과·주의 | Autodesk 공식 출처 | +|---|---|---|---| +| 절대 직교 좌표 | 점 프롬프트에서 `X,Y` 또는 `X,Y,Z` 입력 | 현재 UCS 원점 기준 절대 좌표 입력임. DYN 켜짐 상태에선 첫 점 기본 절대 처리임 | [Coordinate entry](https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-0A0135DB-3216-482B-81DD-74E6DB8CA3E3.htm), [Dynamic Input](https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-DidYouKnow/files/GUID-683349C0-E5C2-4E16-8846-5523E71172A9.htm) | +| 상대 직교 좌표 | `@dx,dy` 또는 `@dx,dy,dz` 입력 | 직전 지정점에서 증분 이동함. 예 `@5,3`은 X 5, Y 3 이동임 | [Coordinate entry](https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-0A0135DB-3216-482B-81DD-74E6DB8CA3E3.htm), [Draw lines](https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-57CDDB6C-B12B-46CE-B9C5-22EFC17258FF.htm) | +| 절대 극좌표 | `거리<각도` 입력 | 현재 UCS 원점/기준각 해석에 따른 극좌표 점 지정임. 양의 각도 방향·0 방향은 UNITS 설정 영향임 | [Coordinate entry](https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-0A0135DB-3216-482B-81DD-74E6DB8CA3E3.htm), [UNITS](https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-MAC-Core/files/GUID-75419F91-18B8-47EE-9272-3196ACC95977.htm) | +| 상대 극좌표 | `@거리<각도` 입력 | 직전점 기준 길이·각도 이동임. 각도 단위는 현재 도면 단위 형식임 | [Coordinate entry](https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-0A0135DB-3216-482B-81DD-74E6DB8CA3E3.htm), [Dynamic Input](https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-DidYouKnow/files/GUID-683349C0-E5C2-4E16-8846-5523E71172A9.htm) | +| 직접 거리 입력 | 점 프롬프트에서 커서를 원하는 방향으로 두고 숫자 거리 입력 | 직교/극좌표 추적 정렬선 방향으로 거리만 입력해 점 지정함. 방향이 먼저 필요함 | [Coordinate entry](https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-0A0135DB-3216-482B-81DD-74E6DB8CA3E3.htm), [AutoTrack](https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-LT/files/GUID-665DC37F-8C3E-414A-9369-72A13C0BE07A.htm) | +| 각도 잠금 | 극좌표 추적 각도 정렬선에서 커서 이동 후 `<각도` 입력/각도 고정, 이어 거리 입력 | 지정 각도 방향을 고정한 채 길이로 점을 정함. 극좌표 추적 증분·설정 영향임 | [Foundations precision](https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-OnBoarding/files/ACD_FOUNDATIONS_MAIN6.html), [Coordinate entry](https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-0A0135DB-3216-482B-81DD-74E6DB8CA3E3.htm) | +| 동적 입력 포인터 칸·치수 칸 | F12 켜기 → 점 프롬프트에서 커서 옆 좌표/치수 칸에 값 입력 | 포인터 근처에서 프롬프트·좌표·치수를 입력함. DYN 설정이 포인터 입력/치수 입력 표시를 제어함 | [Dynamic Input](https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-DidYouKnow/files/GUID-683349C0-E5C2-4E16-8846-5523E71172A9.htm), [DSETTINGS](https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-MAC-Core/files/GUID-06D81B23-B171-4F33-920B-4609E22DD9E5.htm) | +| 동적 입력 Tab 칸 이동·잠금 | 좌표/치수 한 칸 입력 후 Tab으로 다음 칸 이동 및 입력값 잠금, Enter로 점 확정 | X/Y 또는 거리/각 칸을 차례로 채움. 일부 명령·첫 점은 칸 수와 상대/절대 기본이 다름 | [Dynamic Input](https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-DidYouKnow/files/GUID-683349C0-E5C2-4E16-8846-5523E71172A9.htm) | +| 동적 입력 절대/상대/극 모드 | 포인터 입력 칸에서 `#X,Y` 절대, `@X,Y` 상대, 극좌표 거리·각 입력; 아래 화살표로 모드 선택 | 좌표 모드 선택 메뉴에 Absolute/Relative/Polar 등이 표시됨. DYN 상태에 따라 기본 모드가 바뀜 | [Dynamic Input](https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-DidYouKnow/files/GUID-683349C0-E5C2-4E16-8846-5523E71172A9.htm), [Draw lines](https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-57CDDB6C-B12B-46CE-B9C5-22EFC17258FF.htm) | +| 점 좌표 필터 .X·.Y | 점 프롬프트에서 `.X` 또는 `.Y` 입력 → 기준 객체/점 지정 → 나머지 좌표 점 지정 | 여러 점에서 X/Y/Z 성분을 추출해 복합점을 구성함. `.XY`, `.XZ`, `.YZ`도 공식 지원임 | [Coordinate filters](https://help.autodesk.com/cloudhelp/2022/ENU/AutoCAD-Core/files/GUID-46A720EC-712F-44E3-95F7-ABBCC08F8B85.htm) | +| FROM 기준점 | 점 프롬프트에서 Shift+우클릭 객체 스냅 메뉴 또는 입력 `FROM`/`_FROM` → 기준점 → `@dx,dy` | 기준점에서 상대 오프셋 점을 지정하는 OSNAP 재정의 계열임. 키보드 별칭·번역 프롬프트는 버전별 도움말 확인 필요함 | [Object snaps](https://help.autodesk.com/cloudhelp/2027/ENU/AutoCAD-MAC-Core/files/GUID-0EB8FEA1-4C14-40C6-8254-DB6693DB481C.htm), [Object Snap menu / M2P example](https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-OnBoarding/files/acd_foundations_main2/ACD_FOUNDATIONS_MAIN2B.html) | +| M2P 중간 두 점 | 점 프롬프트에서 Shift+우클릭 → Mid Between 2 Points → 첫 점 → 둘째 점 | 두 지정점의 중간점을 산출함. 명령줄 별칭은 `MTP` 또는 `M2P`로 공식 안내됨 | [MTP/M2P command modifier](https://help.autodesk.com/cloudhelp/2022/ENU/AutoCAD-Core/files/GUID-04C9F013-46A3-430E-A085-BF254EA6B1D6.htm), [M2P workflow](https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-OnBoarding/files/acd_foundations_main2/ACD_FOUNDATIONS_MAIN2B.html) | +| TT 임시 추적점 | 점 프롬프트에서 `tt` → 임시 기준점 지정 → 커서를 정렬 경로로 이동 | 지정한 임시 점에 + 표식과 AutoTrack 정렬 경로를 만들어 다음 점 위치를 잡음 | [Tracking points](https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-LT/files/GUID-665DC37F-8C3E-414A-9369-72A13C0BE07A.htm) | +| 투명 CAL 산술·기하식 | 명령의 점/수치 프롬프트에서 `'CAL` → `>> Expression:`에 `1/2`, `(mid+cen)/2`, `dist(...)` 등 → Enter | 현재 명령을 중단하지 않고 실수·점·벡터 표현식을 계산해 입력값으로 반환함 | [Command prompt calculator](https://help.autodesk.com/cloudhelp/2021/ENU/AutoCAD-Core/files/GUID-B29F2EF7-12C8-423C-91F5-3852E6AF1514.htm), [CAL](https://help.autodesk.com/cloudhelp/2022/ENU/AutoCAD-LT-MAC/files/GUID-335A5FC6-7D8F-47CA-B479-26655CDEA1AD.htm) | +| 소수·음수·단위 | 거리/좌표 칸에 십진수·음수 입력; 선형 형식은 UNITS에서 Decimal/Scientific/Engineering/Architectural/Fractional 선택 | 수치 입력 정밀도와 표시 형식은 구분됨. 음수는 좌표/이동 방향 성분으로 해석됨 | [Coordinate entry](https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-0A0135DB-3216-482B-81DD-74E6DB8CA3E3.htm), [UNITS](https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-MAC-Core/files/GUID-75419F91-18B8-47EE-9272-3196ACC95977.htm) | +| 각도 형식 | `45`, `5d10'20"`, `124.6r`, `14g`; 방위는 예 `N45d20'6"E` | 도·도분초·라디안(r)·그라드(g)·측량 방위 입력 가능함. UNITS의 각도 형식·0도 방향·시계/반시계 설정 영향임 | [Format angles](https://help.autodesk.com/cloudhelp/2015/ENU/AutoCAD-MAC-Core/files/GUID-0C6DAEB6-B0BD-45C4-A597-3AFE4E6AAEF1.htm), [Unit conventions](https://help.autodesk.com/view/ACD/2026/ENU/?caas=caas%2Fdocumentation%2FCIV3D%2F2014%2FfilesACD%2FGUID-FD3A36E1-FC7F-43CB-B7B5-1806FAAC12B2-htm.html) | + +## 3. 명령 중 키 입력 + +| 무엇(조작·키) | 어떻게(누르는 차례·프롬프트) | 결과·주의 | Autodesk 공식 출처 | +|---|---|---|---| +| Enter·Space | 명령 이름/별칭 입력 후 Enter 또는 Space; 값 입력 후에도 Enter/Space | 명령 시작, 현재 입력값 확정, 옵션 기본값 선택, 종료 후 직전 명령 반복에 사용됨. 프롬프트별 의미가 우선임 | [Entering commands](https://help.autodesk.com/cloudhelp/2022/ENU/AutoCAD-Core/files/GUID-945C8F74-064C-4EA6-99E1-232E6003CE27.htm) | +| Esc | 진행 중 한 번 Esc; 선택/그립 편집 중 필요 시 반복 | 현재 명령 또는 선택을 취소함. 취소 깊이는 열린 하위 상태에 따라 추가 Esc 필요 가능함 | [Entering commands](https://help.autodesk.com/cloudhelp/2022/ENU/AutoCAD-Core/files/GUID-945C8F74-064C-4EA6-99E1-232E6003CE27.htm) | +| Backspace | 명령줄 입력 필드에서 문자 입력 중 Backspace | 현재 편집 문자열에서 커서 앞 글자 삭제 동작은 표준 입력 동작이나, Autodesk AutoCAD 도움말에서 독립된 CAD 명령 규칙으로 확인 못 함 | [Command line entry](https://help.autodesk.com/cloudhelp/2016/ENU/AutoCAD-LT-MAC/files/GUID-3C87A58F-1980-451D-A9D3-32327FA63D81.htm) | +| 위·아래 화살표 | Command prompt 입력란에 포커스 → ↑/↓ | 최근 입력 명령을 오르내리며 재호출함. 실행 중 옵션 순환과 혼동 금지; 현재 프롬프트 옵션은 별도 선택임 | [Entering commands](https://help.autodesk.com/cloudhelp/2022/ENU/AutoCAD-Core/files/GUID-945C8F74-064C-4EA6-99E1-232E6003CE27.htm) | +| 옵션 글자 | 프롬프트의 대괄호 목록에서 굵게/대문자로 강조된 글자 입력 후 Enter | 옵션 선택은 대소문자 구분 없이 허용됨. 예 `CIRCLE`의 `[3P/2P/Ttr]`에서 `2p` 입력임 | [Entering commands](https://help.autodesk.com/cloudhelp/2022/ENU/AutoCAD-Core/files/GUID-945C8F74-064C-4EA6-99E1-232E6003CE27.htm) | +| `[ ]` 옵션·`< >` 기본값 | 예 `Specify radius or [Diameter] :`에서 `D`+Enter 또는 값 입력, 기본은 Enter | 대괄호는 선택 가능한 옵션, 꺾쇠는 현재 기본값임. Enter만 누르면 꺾쇠 기본값 채택임 | [Entering commands](https://help.autodesk.com/cloudhelp/2022/ENU/AutoCAD-Core/files/GUID-945C8F74-064C-4EA6-99E1-232E6003CE27.htm) | +| 종료 후 Enter 반복 | 직전 명령 종료 후 Enter 또는 Space, 또는 우클릭(환경 설정 영향) | 마지막 명령 재실행임. 명령 실행 도중 Enter는 보통 입력 확정/옵션 기본값임 | [Entering commands](https://help.autodesk.com/cloudhelp/2022/ENU/AutoCAD-Core/files/GUID-945C8F74-064C-4EA6-99E1-232E6003CE27.htm) | +| 명령 중 U·C | LINE 다음점 프롬프트에서 `U` Undo, 닫기 가능한 점에서 `C` Close; PLINE에서도 해당 옵션 | 보편 명령 키가 아니라 현재 명령의 `[Undo/Close]` 프롬프트 옵션임. 옵션이 없으면 동작 보장 안 됨 | [Start commands tutorial](https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-OnBoarding/files/ACD_FOUNDATIONS_MAIN4.html), [LINE](https://help.autodesk.com/cloudhelp/2025/ENG/AutoCAD-Core/files/GUID-E8C1190C-A26C-484C-ADDD-DDF81666F69F.htm), [PLINE](https://help.autodesk.com/cloudhelp/2027/ENG/AutoCAD-Core/files/GUID-11883C70-6435-4F80-8FB4-F6E933B8FD94.htm) | +| 투명 명령 `'` | 다른 명령의 점/숫자 입력 프롬프트에서 `'ZOOM`, `'CAL` 등 작은따옴표+명령 | 현재 명령을 일시 정지하고 지원되는 명령 실행 후 돌아옴. 입력값을 요구하는 일반 명령만 투명 사용 가능하며 명령별 제약 존재함 | [Command entry](https://help.autodesk.com/cloudhelp/2027/ENU/AutoCAD-Core/files/GUID-BB0C3E79-66AF-4557-9140-D31B4CF3C9CF.htm), [CAL](https://help.autodesk.com/cloudhelp/2022/ENU/AutoCAD-LT-MAC/files/GUID-335A5FC6-7D8F-47CA-B479-26655CDEA1AD.htm) | +| 명령 중 스냅 이름 | 점 프롬프트에서 Shift+우클릭 메뉴 선택 또는 `_END`, `_MID` 등 일회성 OSNAP 재정의 | 다음 한 점만 끝점/중간점 등으로 스냅함. 활성 영구 OSNAP 설정과 별도임; 명령줄 이름은 영문·언더바 형식에 따라 달라질 수 있음 | [Object snaps](https://help.autodesk.com/cloudhelp/2027/ENU/AutoCAD-MAC-Core/files/GUID-0EB8FEA1-4C14-40C6-8254-DB6693DB481C.htm), [Shift-right-click M2P example](https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-OnBoarding/files/acd_foundations_main2/ACD_FOUNDATIONS_MAIN2B.html) | + +## 4. 마우스·선택·그립 + +| 무엇(조작·키) | 어떻게(누르는 차례·프롬프트) | 결과·주의 | Autodesk 공식 출처 | +|---|---|---|---| +| 왼쪽 버튼 | 점 프롬프트에서 클릭; 객체 위 클릭; 드래그 선택 | 점 위치 지정·객체 선택·메뉴/편집 도구 선택의 기본 버튼임 | [Pointing devices](https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-LT-Customization/files/GUID-B1CDEE41-F8BF-48A4-A895-273DBB295378.htm) | +| 오른쪽 버튼 | 도면 영역에서 짧게 클릭 또는 길게 누름; `SHORTCUTMENU`·옵션에 따른 시간 임계 설정 | 설정에 따라 Enter/최근 명령/상황 메뉴임. 명령 중 기본값/Enter 대용 가능하며 환경설정에 따라 달라짐 | [Right-click customization](https://help.autodesk.com/cloudhelp/2020/ENU/AutoCAD-Core/files/GUID-7C6AE349-DDF0-40DF-9D1E-FA9702BE88BB.htm), [Pointing devices](https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-LT-Customization/files/GUID-B1CDEE41-F8BF-48A4-A895-273DBB295378.htm) | +| 가운데 버튼·휠 클릭 | 휠을 누른 채 드래그; 또는 `MBUTTONPAN=0`이면 클릭 | 기본 `MBUTTONPAN=1`에서 팬, 0이면 객체 스냅 메뉴 접근 동작임 | [Pointing devices](https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-LT-Customization/files/GUID-B1CDEE41-F8BF-48A4-A895-273DBB295378.htm) | +| 휠 확대·축소 | 휠 앞/뒤로 굴림 | 커서 위치 기준 줌 인/아웃; `ZOOMWHEEL`로 방향 반전 가능함 | [Pointing devices](https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-LT-Customization/files/GUID-B1CDEE41-F8BF-48A4-A895-273DBB295378.htm), [Navigation](https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-OnBoarding/files/ACD_FOUNDATIONS_MAIN3.html) | +| 휠 두 번 클릭·끌기 | 휠 더블클릭; 휠 누른 채 마우스 이동 | 전체 도면 범위 Zoom Extents; 휠 드래그 Pan임 | [Navigation](https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-OnBoarding/files/ACD_FOUNDATIONS_MAIN3.html) | +| Ctrl+휠 끌기 | Ctrl 누른 상태에서 휠 버튼 드래그 | `MBUTTONPAN=1`이면 조이스틱 팬; 0이면 카메라 회전 동작으로 설명됨. LT·제품별 차이는 공식 표에 따름 | [Pointing devices](https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-LT-Customization/files/GUID-B1CDEE41-F8BF-48A4-A895-273DBB295378.htm) | +| Ctrl+Shift+휠 끌기 | Ctrl+Shift 누르고 휠 버튼 드래그 | 3D 자유 궤도 회전; AutoCAD LT에는 미지원으로 안내됨 | [Pointing devices](https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-LT-Customization/files/GUID-B1CDEE41-F8BF-48A4-A895-273DBB295378.htm) | +| 우클릭 시간차 | 짧게/길게 도면 우클릭 | 짧은 클릭은 Enter, 긴 클릭은 바로가기 메뉴 설정 가능함. 모드별 기본/편집/명령 동작 구분임 | [Right-click customization](https://help.autodesk.com/cloudhelp/2020/ENU/AutoCAD-Core/files/GUID-7C6AE349-DDF0-40DF-9D1E-FA9702BE88BB.htm) | +| Shift+우클릭 | 점 프롬프트에서 Shift 누르고 오른쪽 클릭 → OSNAP 메뉴 | 다음 한 점의 객체 스냅 재정의; Mid Between 2 Points 같은 항목 접근임 | [Shift-right-click M2P example](https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-OnBoarding/files/acd_foundations_main2/ACD_FOUNDATIONS_MAIN2B.html), [Object snaps](https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-MAC-Core/files/GUID-8F5E5431-9EFB-414E-BC6D-2C65EFB2DAC3.htm) | +| Ctrl+우클릭 | 일반 점 입력 중 Ctrl+우클릭 전용 동작 | 현재 AutoCAD 도움말에서 보편 전용 조작으로 확인 못 함. 휠 클릭/Shift+우클릭 메뉴와 혼동하지 않음 | [Pointing devices](https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-LT-Customization/files/GUID-B1CDEE41-F8BF-48A4-A895-273DBB295378.htm) | +| 객체 더블클릭 편집 | 해당 객체를 두 번 클릭 | 객체 유형별 편집 명령 호출: TEXT/DDEDIT, MTEXT/MTEDIT, 치수/TEXTEDIT, 폴리선/PEDIT, 블록/BEDIT 또는 Properties 등; `DBLCLKEDIT` 설정 영향임 | [Double-click actions](https://help.autodesk.com/view/ACD/2026/ENU/?caas=caas%2Fdocumentation%2FACDLT%2F2014%2FENU%2Ffiles%2FGUID-0181E010-6BF2-4F59-8B9B-C64E10E127BA-htm.html) | +| 왼→오 창 선택 | 빈 곳에서 왼쪽→오른쪽으로 창 드래그 | 창 안에 완전히 포함된 객체 선택임 | [Selection](https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-LT-MAC/files/GUID-0E99D7EB-4EE9-4D6A-94AE-970A91ADE517.htm) | +| 오른→왼 걸침 선택 | 빈 곳에서 오른쪽→왼쪽으로 창 드래그 | 창 안에 있거나 경계를 가로지르는 객체 선택임 | [Selection](https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-LT-MAC/files/GUID-0E99D7EB-4EE9-4D6A-94AE-970A91ADE517.htm) | +| 올가미·경로 선택 | 선택 시작 후 누른 채 자유곡선으로 끌고 놓기; 스페이스로 선택 모드 순환 가능한 환경 | Lasso/window/crossing 등 선택 형태 순환은 설정·AutoCAD 버전에 영향 받음 | [Selection](https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-LT-MAC/files/GUID-0E99D7EB-4EE9-4D6A-94AE-970A91ADE517.htm), [PICKAUTO](https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-7BAAA374-8409-4296-B520-C5355941C836.htm) | +| Shift+클릭 빼기 | 선택 집합에 객체가 추가된 뒤 Shift 누르고 선택/창 지정 | 기존 선택에서 객체 제거임. 버전별 선택 설정/`PICKADD` 영향 가능함 | [Selection](https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-LT-MAC/files/GUID-0E99D7EB-4EE9-4D6A-94AE-970A91ADE517.htm) | +| 겹친 객체 순환 선택 | 겹친 객체 위 hover → 선택 배지/목록; Ctrl+W로 Selection Cycling 토글; 일부 Mac 도움말은 Shift+Space 순환 | 후보 겹침 목록에서 목표 객체 선택임. `SELECTIONCYCLING` 및 플랫폼에 따라 조작 차이 있음 | [Selection Cycling](https://help.autodesk.com/cloudhelp/2024/ENU/AutoCAD-Core/files/GUID-08FA6975-49A7-4029-A3F3-ABDA3B2A7122.htm), [Selection](https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-LT-MAC/files/GUID-F401A2DC-0724-464E-8D0D-52B7CF02AF5C.htm) | +| 그립 표시·뜨거운 그립 | 명령을 시작하지 않은 상태에서 객체 선택 → 그립 클릭 | 선택 그립은 기본 Stretch; 커서로 점을 지정하거나 좌표/스냅 입력 가능함 | [Edit with grips](https://help.autodesk.com/cloudhelp/2019/ENU/AutoCAD-Core/files/GUID-484774B4-8F67-4E19-8647-83A7D314782B.htm) | +| 그립 모드 순환 | 활성 그립에서 Enter 또는 Space 반복 | Stretch→Move→Rotate→Scale→Mirror 순환; 그립 우클릭 메뉴에서 직접 모드/옵션 선택 가능함 | [Edit with grips](https://help.autodesk.com/cloudhelp/2019/ENU/AutoCAD-Core/files/GUID-484774B4-8F67-4E19-8647-83A7D314782B.htm), [Grip workflow](https://help.autodesk.com/cloudhelp/2022/ENU/AutoCAD-DidYouKnow/files/GUID-BBEA1F71-EB16-4D49-80D9-970A6909F508.htm) | +| 그립 복사 | 그립 모드 중 `C` 입력 또는 Ctrl을 누른 채 재배치점 클릭 | 현재 그립 모드로 원본을 유지한 복사본 생성; Ctrl은 클릭으로 위치 확정할 때까지 유지하는 방식임 | [Grip copies](https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-35984B1B-C6C3-4618-95D1-F3D08D47D5F6.htm) | +| 여러 그립 선택 | Shift 누른 채 복수 그립 클릭 → Shift 놓고 하나를 끌기 | 선택 그립 사이의 형상을 유지하며 함께 스트레치함 | [Editing with grips](https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-01DE459C-21A7-4E92-A1D6-E2C36CD89F0C.htm) | +| 다기능 그립 메뉴·Ctrl 순환 | 다기능 그립 위 hover해 메뉴 표시; 설정되면 그립 선택 후 Ctrl로 옵션 순환 | 객체별 삽입/정점/호/해치 등 추가 조작 메뉴임. 모든 그립에 메뉴가 있는 것은 아니며 `GRIPMULTIFUNCTIONAL` 설정 영향임 | [Editing with grips](https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-01DE459C-21A7-4E92-A1D6-E2C36CD89F0C.htm), [GRIPMULTIFUNCTIONAL](https://help.autodesk.com/view/ACD/2026/ENU/?caas=caas%2Fdocumentation%2FCIV3D%2F2014%2FfilesACD%2FGUID-A88576CB-7CDD-4DC4-819C-EA0F892E72CB-htm.html) | +| 그립 중 숫자 | 그립 활성 상태에서 좌표/거리/각도 입력; DYN 켜짐 시 포인터 입력 | 그립 명령의 점·거리·각도 입력을 수치로 제한함. 원/타원 사분점 그립의 반경 수치는 중심에서 잰 거리임 | [Editing with grips](https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-01DE459C-21A7-4E92-A1D6-E2C36CD89F0C.htm) | +| 선택 객체 끌어 이동·복사 | 선택 후 객체 위에서 왼쪽 드래그; 복사는 Ctrl 누른 채 드래그; 오른쪽 드래그 후 메뉴 선택 | 이동 또는 복사; 우클릭 드래그 메뉴에 Move Here/Copy Here/Paste as Block/Cancel. 일반 드래그는 스냅 설정을 무시할 수 있음 | [Copy by dragging](https://help.autodesk.com/cloudhelp/2025/ENG/AutoCAD-Core/files/GUID-0BFF5C5E-B1B4-4996-9AA4-57396FD6F6F2.htm), [Move by dragging](https://help.autodesk.com/view/ACD/2026/ENU/?caas=caas%2Fdocumentation%2FACDLT%2F2014%2FENU%2Ffiles%2FGUID-11AB8C73-D35A-45DF-A7AE-4D03313DDACA-htm.html) | +| 선택 객체 미세 이동 | 선택 상태에서 Ctrl+방향키 | 스냅 꺼짐은 화면 픽셀 단위, 켜짐은 스냅 간격 기반으로 직교 방향 이동임 | [Move objects](https://help.autodesk.com/view/ACD/2026/ENU/?caas=caas%2Fdocumentation%2FACDLT%2F2014%2FENU%2Ffiles%2FGUID-11AB8C73-D35A-45DF-A7AE-4D03313DDACA-htm.html) | + +## 5. 그리기·수정·측정·문자·치수 명령 프롬프트 + +| 명령·무엇 | 어떻게(대표 프롬프트·키 입력 순서) | 결과·주의 | Autodesk 공식 출처 | +|---|---|---|---| +| LINE | `Specify first point:` 점 → `Specify next point or [Undo]:` 점/좌표 또는 U → C(닫기) 또는 Enter | 연속 선분 생성, 닫기는 시작점 연결; 명령 프롬프트 예시 기준임 | [LINE](https://help.autodesk.com/cloudhelp/2025/ENG/AutoCAD-Core/files/GUID-E8C1190C-A26C-484C-ADDD-DDF81666F69F.htm) | +| PLINE | `Specify start point:` → `Specify next point or [Arc/Halfwidth/Length/Undo/Width]:` 점/옵션 → Close 또는 Enter | 연결 세그먼트를 단일 폴리선으로 생성; Arc/Width 등 모드 전환 있음 | [PLINE](https://help.autodesk.com/cloudhelp/2027/ENG/AutoCAD-Core/files/GUID-11883C70-6435-4F80-8FB4-F6E933B8FD94.htm) | +| RECTANG | 첫 코너 `[Chamfer/Elevation/Fillet/Thickness/Width]` → 반대 코너 `[Area/Dimensions/Rotation]` | 닫힌 직사각형 폴리선 생성; 치수 옵션은 폭·높이 입력 | [RECTANG](https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-188B2DDA-6CD8-4D37-BF26-E6CF27C34C75.htm) | +| CIRCLE | 중심 `[3P/2P/Ttr]` → 반지름 또는 Diameter 옵션; 또는 3점/2점/접선·접선·반지름 선택 | 원 생성; Enter는 기본 중심 방식/반지름 값 수락 | [Circle workflow](https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-OnBoarding/files/ACD_FOUNDATIONS_MAIN5.html), [Commands](https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-OnBoarding/files/ACD_FOUNDATIONS_MAIN4.html) | +| ARC | 시작점/중심점/끝점·각도·현 길이·반지름 등 옵션 선택 후 점/수치 입력 | 지정 순서 조합으로 호 생성; 직전 선/호 뒤 시작점 프롬프트 Enter는 접선 연속 호, Ctrl 드래그는 시계방향 호의 공식 워크플로가 있음 | [ARC](https://help.autodesk.com/cloudhelp/2022/ENU/AutoCAD-Core/files/GUID-30ECFD30-A1D6-4D60-9DD1-B487603F6772.htm), [Arc methods](https://help.autodesk.com/cloudhelp/2025/ENU/AutoCAD-Core/files/GUID-C78378FD-4704-4D0A-9D86-82FCBB118097.htm) | +| POLYGON | 변 개수 `` → 중심점 또는 `[Edge]` → 내접/외접 반지름 또는 두 모서리 끝점 | 정다각형 생성; Edge는 한 변 양 끝 지정임 | [POLYGON](https://help.autodesk.com/cloudhelp/2016/ENU/AutoCAD-MAC-Core/files/GUID-E5CD464D-C0DC-4464-BFDF-50C4ABEC8B91.htm) | +| ELLIPSE | 축 끝점 지정 → 다른 축 거리 또는 `[Rotation]`; `[Arc/Center/Isocircle]` 옵션 | 타원·타원 호 생성; 등각 원은 등각 평면 모드에서 옵션 사용임 | [ELLIPSE](https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-107CB04F-AD1F-4D2F-8EC9-9D8E2E4F0D38.htm) | +| SPLINE | 시작점 `[Method/Degree/Object]` 또는 `[Method/Knots/Object]` → 다음점 반복 → `[Close/Undo]` | FIT 또는 CV 방식 곡선 생성; 프롬프트 구성은 선택 방식·버전별 차이임 | [SPLINE](https://help.autodesk.com/cloudhelp/2015/ENU/AutoCAD-MAC-Core/files/GUID-5E7D51E2-1595-4E0C-85F8-2D7CBD166A08.htm) | +| XLINE | 점 지정 또는 `[Hor/Ver/Ang/Bisect/Offset]` → 통과점/각도/거리 입력 | 양방향 무한 구성선 생성임 | [XLINE](https://help.autodesk.com/cloudhelp/2016/ENU/AutoCAD-MAC-Core/files/GUID-40650DCE-E8CA-483C-8E25-7FA9AB6992C1.htm) | +| OFFSET | 거리 또는 `[Through/Erase/Layer]` → 객체 선택 → 방향 점 `[Exit/Multiple/Undo]` | 평행 오프셋/동심 복제 생성; Through는 지정점을 통과함 | [OFFSET](https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-OnBoarding/files/acd_foundations_main2/ACD_FOUNDATIONS_MAIN2B.html) | +| FILLET | 첫 객체 `[Undo/Polyline/Radius/Trim/Multiple]` → 반지름 설정 가능 → 둘째 객체 또는 Shift 모서리 | 접선 호로 모서리 연결, 필요 시 잘라냄; 반지름 0은 날카로운 모서리임 | [FILLET](https://help.autodesk.com/cloudhelp/2016/ENU/AutoCAD-MAC-Core/files/GUID-C5530CBC-0402-439E-842D-AF5795B903C2.htm) | +| CHAMFER | 첫 선 `[Undo/Polyline/Distance/Angle/Trim/mEthod/Multiple/eXpression]` → 둘째 선 | 두 거리 또는 거리+각도로 모따기; 선택 순서에 따라 첫/둘째 거리 대응임 | [CHAMFER](https://help.autodesk.com/cloudhelp/2016/ENU/AutoCAD-MAC-Core/files/GUID-B1DCF991-90A7-4DB0-96FC-BDA3FB76337C.htm) | +| TRIM | Quick 모드는 절단 경계 자동 사용; Standard는 절단 모서리 선택→Enter→자를 객체 선택; `[Crossing/Fence/Mode/Project/Edge/Erase/Undo]` | 경계와 교차하는 부분 제거; Shift 선택은 EXTEND 동작으로 전환 가능함 | [TRIM](https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-B1A185EF-07C6-4C53-A76F-05ADE11F5C32.htm) | +| EXTEND | 경계 객체 선택→Enter→연장할 객체 선택; `[Fence/Crossing/Project/Edge/Undo]`; Shift 선택은 Trim | 경계까지 연장; Quick/Standard 및 선택 흐름에 따라 프롬프트 달라짐 | [EXTEND](https://help.autodesk.com/cloudhelp/2020/ENU/AutoCAD-Core/files/GUID-89DD7B0F-F4F1-410D-9A3A-5847CA5F8744.htm) | +| MOVE | 객체 선택→Enter→기준점 `[Displacement]`→두 번째 점 `` | 기준점과 목적점 벡터만큼 이동; 변위 좌표 직접 입력 가능함 | [MOVE](https://help.autodesk.com/cloudhelp/2015/ENU/AutoCAD-LT-MAC/files/GUID-47CE7325-84C0-4414-80A3-29DC98392709.htm) | +| COPY | 객체 선택→기준점 `[Displacement/mOde/Multiple]`→두 번째 점 `[Array]`; 연속 복사 후 Enter | 좌표/변위 복사·다중 복사·배열 복사; 프롬프트 기본값은 현재 설정임 | [COPY](https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-1CF9287F-06E8-4D03-8377-2E130862FE02.htm) | +| ROTATE | 객체 선택→기준점→회전각 `[Copy/Reference]` 입력 | 지정 기준점 중심 회전; Copy는 원본 보존, Reference는 기존 각 기준 변환임 | [ROTATE](https://help.autodesk.com/cloudhelp/2025/ENG/AutoCAD-Core/files/GUID-1C265537-FBAC-48D5-B448-B72E777071E5.htm) | +| SCALE | 객체 선택→기준점→축척계수 `[Copy/Reference]` | 기준점 기준 배율 변경; Reference는 기존 길이를 새 길이에 맞춤임 | [SCALE](https://help.autodesk.com/cloudhelp/2015/ENU/AutoCAD-LT-MAC/files/GUID-D4E17E51-5000-4AB6-8D6A-6D2AB4863C75.htm) | +| STRETCH | 교차 창/걸침 선택으로 꼭짓점 포함 객체 선택→기준점 `[Displacement]`→두 번째 점 | 걸친 꼭짓점만 늘리고 완전 포함 객체는 이동함. 창 선택 방향이 결과를 좌우함 | [STRETCH workflow](https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-OnBoarding/files/ACD_FOUNDATIONS_MAIN7.html) | +| MIRROR | 객체 선택→Enter→대칭축 첫 점→둘째 점→원본 삭제 `[Yes/No]` | 지정 축 대칭 복제, 원본 유지 여부 선택임 | [MIRROR](https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-595277C8-9B87-4CFB-A3AF-769537A22F3D.htm) | +| ARRAY | 객체 선택→`Rectangular/Path/Polar` 또는 배열 유형→행·열 간격/경로/중심·항목 수·각도 입력 | 연관 배열 생성; `-ARRAY` 명령줄형과 ARRAY 대화형 프롬프트가 다름 | [-ARRAY](https://help.autodesk.com/cloudhelp/2027/ENU/AutoCAD-Core/files/GUID-8336B4CD-5375-4290-BD08-7D9E022741F6.htm), [ARRAY](https://help.autodesk.com/cloudhelp/ENU/AutoCAD-Web-Help/files/List-Commands/ARRAY_COMMAND.html) | +| BREAK | 객체 선택 또는 첫 절단점 지정→둘째 절단점; `F`로 첫 점 재지정 가능 | 두 점 사이 제거; 같은 점을 두 번 지정하면 분할만 수행 가능함 | [BREAK](https://help.autodesk.com/view/ACD/2026/ENU/?caas=caas%2Fdocumentation%2FACDLT%2F2014%2FENU%2Ffiles%2FGUID-36A1CDE0-3871-4B25-AC98-93235FA83863-htm.html) | +| JOIN | 원본 객체 선택→추가 객체 선택→Enter | 호환되는 접촉/연속 객체를 하나로 결합; 유형·공면성 등 조건 영향임 | [JOIN](https://help.autodesk.com/cloudhelp/2020/ENU/AutoCAD-Core/files/GUID-4D2A28C3-3E7F-4830-BE6A-7C9907C95488.htm) | +| LENGTHEN | `[DElta/Percent/Total/DYnamic]` 선택→객체 선택 또는 값 입력 | 선/호 길이 또는 끝 각도 변경; 양수 연장·음수 단축임 | [LENGTHEN](https://help.autodesk.com/cloudhelp/2016/ENU/AutoCAD-LT-MAC/files/GUID-76C74C62-DBAA-41E7-8422-F0EE7E769638.htm) | +| DIVIDE | 객체 선택→등분할 세그먼트 수 입력 또는 `[Block]` 선택 | 객체를 실제 분할하지 않고 등간격 점/블록 위치를 표시함 | [DIVIDE intervals](https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-6157F6AB-2D6A-4991-987C-00AD46290CE8.htm) | +| MEASURE | 객체 선택→세그먼트 길이 입력 `[Block]` | 지정 간격으로 점/블록 배치; 객체 자체 분할은 아님 | [MEASURE](https://help.autodesk.com/cloudhelp/2016/ENU/AutoCAD-MAC-Core/files/GUID-212933E8-BC53-4872-A3DC-32C48DE1B2D0.htm) | +| DIST | 첫 점→둘째 점 또는 ``→점 반복 | 두 점 거리·각도·좌표 차 측정; 다중점은 누적 측정 흐름임 | [DIST](https://help.autodesk.com/cloudhelp/2017/ENU/AutoCAD-LT-MAC/files/GUID-A72EADC7-EF79-463C-B4F1-862C6139B004.htm) | +| TEXT | 시작점 `[Justify/Style]`→문자 높이(스타일 조건부)→회전각→문자 입력 | 한 줄 문자 생성; Enter로 후속 줄/반복 여부는 현재 TEXT 프롬프트 흐름에 따름 | [TEXT](https://help.autodesk.com/view/ACD/2026/ENU/?caas=caas%2Fdocumentation%2FACDLT%2F2014%2FENU%2Ffiles%2FGUID-D1C664DD-63D9-467E-8EC1-2F5A1777A924-htm.html) | +| MTEXT | 첫 구석→반대 구석→내부 문자 편집기에 입력·서식→닫기 | 지정 폭 경계의 여러 줄 문자 생성; `-MTEXT` 명령줄형 옵션과 편집기형 구별임 | [MTEXT](https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-E6BCE05D-B9E3-4875-BBBC-29134EA6FD51.htm) | +| DIM·선형 | DIM에서 두 점/선택 객체→치수선 위치; 또는 DIMLINEAR 점 2개→위치 `[Mtext/Text/Angle/Horizontal/Vertical/Rotated]` | 수평·수직·회전 선형치수 생성; DIM은 객체 유형 자동 판별 통합 명령임 | [DIM](https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-45C1A271-9650-4927-858F-B3BDB19B3E6C.htm), [DIMLINEAR](https://help.autodesk.com/cloudhelp/2025/ENU/AutoCAD-Core/files/GUID-E908848F-4621-43D2-B377-FD3FBC2B8848.htm) | +| DIM·정렬 | DIMALIGNED에서 첫/둘째 보조선 원점→치수선 위치 `[Mtext/Text/Angle]` | 측정 방향에 정렬된 선형 치수 생성임 | [DIMALIGNED](https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-MAC-Core/files/GUID-D7C05A85-8985-4576-B22C-5A795DD5B179.htm) | +| DIM·각도 | DIMANGULAR에서 두 선/호/원 또는 세 점 선택→각도 치수선 위치→필요 시 `[Mtext/Text/Angle/Quadrant]` | 두 선 또는 세 점 사이 각도 치수 생성; 프롬프트는 선택 대상에 따라 다름 | [DIMANGULAR](https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-6AB63ECB-BBCB-4D0F-B7BE-1674349A1FF9.htm) | +| DIM·반지름 | DIMRADIUS에서 호/원 선택→치수선 위치→필요 시 `[Mtext/Text/Angle]` | 반지름 기호 포함 치수 생성; 치수 스타일이 외형 결정임 | [DIMRADIUS](https://help.autodesk.com/cloudhelp/2023/ENU/AutoCAD-Core/files/GUID-99EED401-D7DF-4CF3-95ED-BBD107A13855.htm) | +| HATCH | 내부점 지정 또는 객체 선택; `-HATCH` 프롬프트 `[Properties/Select objects/draW boundary/remove Boundaries/Advanced/DRaw order/Origin/ANnotative/hatch COlor/LAyer/Transparency]` | 닫힌 영역 해치/채우기 생성; 경계 탐지·섬·연관 여부 설정에 따라 입력 흐름이 변함 | [HATCH](https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-27C104F2-B687-4025-B50B-A58E37329832.htm), [-HATCH prompts](https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-LT-MAC/files/GUID-410ECEBF-7CC2-4000-A45E-18F1F6BEE423.htm) | + +### 확인 범위 메모 + +- “빠짐없이”의 한계: Autodesk 도움말은 모든 제품 릴리즈·플랫폼·CUI 사용자화·제3자 명령을 합친 단일 고정 목록을 제공하지 않음. 따라서 이 문서는 요청된 항목 전부를 표에 넣고, 도움말에서 직접 확인되지 않은 Ctrl+우클릭·Backspace 전용 규칙·모든 Alt 조합·일부 임시 키 및 M2P 키보드 약어는 확인 못 함으로 표시함. +- 임시 키는 키보드 배열·CUIx macro·사용자 설정에 따라 차이가 있으며, 위 키 목록은 Autodesk의 기본 Temporary Override Keys 참조표 기준임. +- 명령 프롬프트는 제품 버전과 입력 방식(명령줄형, 동적 입력, 대화상자형)에 따라 일부 달라짐. 실제 구현 전 목표 제품 버전에서 프롬프트를 다시 대조할 근거 목록임. + + +--- + +## 부록 2. 안티그래비티 — `agy_227.md` + +| 번호 | 조작 구분 | 조작 명칭 | 키 및 입력 | 누르는 차례 및 조작 순서 | 동작 결과 | 실무 구분 | 출처 URL | +|---|---|---|---|---|---|---|---| +| 1 | 명령/반복 | 직전 명령 반복 실행 | Space 또는 Enter | 명령 종료 상태에서 Space(또는 Enter) 1회 입력 | 직전에 완료된 명령어가 즉시 재호출됨 | 「꼭 있어야 함」 · 「자주 씀」 | https://thecarpenter.tistory.com/58 | +| 2 | 명령/취소 | 명령 취소 및 선택 해제 | ESC | 작업 중 ESC 키 1~2회 연속 입력 | 진행 중인 명령이 중단되고 활성화된 그립 및 선택 세트가 완전히 해제됨 | 「꼭 있어야 함」 · 「자주 씀」 | https://firelectric.tistory.com/30 | +| 3 | 수치 작도 | 직접 거리 입력 (Direct Distance) | 마우스 방향 지정 + 숫자 + Space | 시작점 클릭 ➔ 마우스로 방향 유도 ➔ 거리 수치 입력 ➔ Space | 마우스 커서 방향으로 입력한 길이만큼의 선분/객체가 즉시 작도됨 | 「꼭 있어야 함」 · 「자주 씀」 | https://civilw.tistory.com/619 | +| 4 | 수치 작도 | 상대 좌표 수치 입력 | @ + X증분 + , + Y증분 + Space | 시작점 지정 ➔ @ 입력 ➔ X변위 ➔ , ➔ Y변위 ➔ Space | 직전 기준점으로부터 지정된 상대 변위(ΔX, ΔY) 위치에 점이 지정됨 | 「꼭 있어야 함」 · 「초보가 헷갈림」 | https://kimsungjin.tistory.com/288 | +| 5 | 수치 작도 | 상대 극좌표 수치 입력 | @ + 거리 + < + 각도 + Space | 시작점 지정 ➔ @ 입력 ➔ 거리 수치 ➔ < ➔ 각도 ➔ Space | 직전 점 기준으로 지정된 거리 및 반시계 방향 각도 위치에 점이 지정됨 | 「자주 씀」 · 「초보가 헷갈림」 | https://kimsungjin.tistory.com/288 | +| 6 | 수치 작도 | 동적 입력 치수/각도 전환 | Tab | 동적 입력(F12) 상태에서 거리 수치 입력 ➔ Tab ➔ 각도 입력 ➔ Enter | 길이 입력란에서 각도 입력란으로 전환되며 입력된 거리가 잠금 고정됨 | 「꼭 있어야 함」 · 「자주 씀」 | https://civilw.tistory.com/619 | +| 7 | 수치 작도 | 동적 입력 시 절대 좌표 강제 | # + X좌표 + , + Y좌표 | 동적 입력 켜진 상태에서 # 입력 ➔ X좌표 ➔ , ➔ Y좌표 ➔ Space | 상대좌표 자동 변환을 건너뛰고 도면 원점(0,0) 기준 절대 위치가 지정됨 | 「초보가 헷갈림」 | https://www.autodesk.com/kr/support/technical/article/caas/sfdcarticles/sfdcarticles/KOR/Using-absolute-coordinates-in-Dynamic-Input-mode.html | +| 8 | 기능키 토글 | 직교 모드 (ORTHO) 토글 | F8 | 제도 또는 편집 중 F8 1회 입력 | 커서 이동 및 선 작도 방향이 수평(0/180°) 및 수직(90/270°)으로 제한 또는 해제됨 | 「꼭 있어야 함」 · 「자주 씀」 | https://thecarpenter.tistory.com/58 | +| 9 | 기능키 토글 | 객체 스냅 (OSNAP) 토글 | F3 | 작도 또는 편집 중 F3 1회 입력 | 끝점·중간점·중심점·교차점 등에 커서가 자석처럼 흡착되는 기능이 켜지거나 꺼짐 | 「꼭 있어야 함」 · 「자주 씀」 | https://thecarpenter.tistory.com/58 | +| 10 | 기능키 토글 | 극좌표 추적 (Polar Tracking) | F10 | F10 1회 입력 (F8 직교와 상호 배타 작동) | 설정된 증분 각도(예: 30°, 45°) 근처에서 가상 유도 점선이 표시됨 | 「자주 씀」 | https://cad2012.tistory.com/entry/AutoCAD-명령어-정리-Tracking추적-사용하기 | +| 11 | 기능키 토글 | 객체 스냅 추적 (O-TRACK) | F11 | F11 켬 ➔ 스냅점 위에 클릭 없이 커서 1초 안착 후 이동 | 해당 스냅점으로부터 수평/수직 가상 정렬선이 뻗어나와 교차 지점을 쉽게 잡음 | 「자주 씀」 · 「초보가 헷갈림」 | https://cad2012.tistory.com/entry/AutoCAD-명령어-정리-Tracking추적-사용하기 | +| 12 | 기능키 토글 | 동적 입력 (Dynamic Input) | F12 | F12 1회 입력 | 마우스 커서 위치에 치수·각도 입력 상자 및 명령 옵션 툴팁이 직접 표시됨 | 「자주 씀」 · 「초보가 헷갈림」 | https://civilw.tistory.com/619 | +| 13 | 뷰포트 마우스 | 커서 중심 줌 인/아웃 | 마우스 휠 굴리기 | 확대/축소할 위치에 커서를 올리고 휠을 앞/뒤로 회전 | 마우스 커서가 놓인 지점을 축으로 도면 뷰포트가 실시간 확대·축소됨 | 「꼭 있어야 함」 · 「자주 씀」 | https://www.autodesk.com/kr/support/technical/article/caas/sfdcarticles/sfdcarticles/KOR/Zoom-does-not-work-correctly-using-the-mouse-wheel-in-AutoCAD.html | +| 14 | 뷰포트 마우스 | 화면 초점이동 (Pan) | 마우스 휠 누른 채 드래그 | 휠 버튼을 꾹 누른 상태에서 마우스 이동 후 버튼에서 손 뗌 | 현재 표시 배율을 유지한 채 도면 뷰포트 전체가 손바닥 툴처럼 끌려 이동함 | 「꼭 있어야 함」 · 「자주 씀」 | https://www.autodesk.com/kr/support/technical/article/caas/sfdcarticles/sfdcarticles/KOR/Wheel-button-on-mouse-does-not-perform-zoom-functions.html | +| 15 | 뷰포트 마우스 | 도면 전체 맞춤 (Zoom Extents) | 마우스 휠 더블클릭 | 마우스 휠 버튼을 빠르게 연속 2회 클릭 | 도면 내 모든 객체가 화면에 꽉 차게 들어오도록 줌 배율이 자동 정렬됨 | 「꼭 있어야 함」 · 「자주 씀」 | https://letter.ednc.com/2023/autocad-팁-보기-및-객체/ | +| 16 | 작업 환경 | 마우스 우클릭 엔터 사용자화 | 마우스 우클릭 | OP(옵션)에서 우클릭을 Enter로 커스텀 ➔ 명령 입력 후 우클릭 | 스페이스바를 누르지 않고 오른손 마우스 클릭만으로 명령 확정 및 직전 명령이 재실행됨 | 「자주 씀」 | https://eyehole-tech.com/autocad-acad-pgp/ | +| 17 | 작업 환경 | acad.pgp 왼손 단축키 재배치 | PGP 편집 + REINIT | acad.pgp에서 복사·선·이동 등을 Q,W,E,A,S,D,C로 변경 ➔ REINIT 실행 | 오른손 마우스 이동을 고정한 채 왼손 단축키와 스페이스바로 고속 제도함 | 「자주 씀」 | https://eyehole-tech.com/autocad-acad-pgp/ | +| 18 | 객체 선택 | 윈도우 선택 (Window) | 좌클릭 ➔ 우측 드래그 ➔ 좌클릭 | 빈 공간 클릭 ➔ 커서를 오른쪽으로 이동(파란 상자) ➔ 클릭 | 파란색 사각형 영역 내부에 100% 온전히 포함된 객체만 선택됨 | 「꼭 있어야 함」 · 「초보가 헷갈림」 | https://maya3d.tistory.com/119 | +| 19 | 객체 선택 | 걸침 선택 (Crossing) | 좌클릭 ➔ 좌측 드래그 ➔ 좌클릭 | 빈 공간 클릭 ➔ 커서를 왼쪽으로 이동(초록 상자) ➔ 클릭 | 초록색 사각형 영역 내부에 포함되거나 경계선에 조금이라도 걸친 객체가 모두 선택됨 | 「꼭 있어야 함」 · 「초보가 헷갈림」 | https://maya3d.tistory.com/119 | +| 20 | 객체 선택 | 선택 세트에서 객체 제외 (Deselect) | Shift + 클릭 또는 영역 드래그 | 여러 객체 선택 상태에서 Shift 키를 누른 채 특정 객체 클릭/드래그 | 전체 선택 세트는 유지되고 클릭하거나 지정한 특정 객체만 선택에서 제거됨 | 「꼭 있어야 함」 · 「자주 씀」 | https://1duri1.tistory.com/186 | +| 21 | 객체 선택 | 이전 선택 객체 재선택 (Previous) | P + Space | 편집 명령(M, CO 등)의 객체 선택 프롬프트에서 P 입력 ➔ Space | 바로 이전 작업에서 선택했었던 객체 집합이 그대로 다시 일괄 선택됨 | 「자주 씀」 | https://ng-insight.com/entry/이전-객체-선택-하기-오토캐드 | +| 22 | 객체 선택 | 마지막 생성 객체 선택 (Last) | L + Space | 편집 명령어 실행 후 객체 선택 프롬프트에서 L 입력 ➔ Space | 도면상에서 가장 마지막으로 생성된 1개의 객체가 즉시 선택됨 | 「자주 씀」 | https://officedocworks.com/오토캐드auto-cad-마지막에-생성-객체-선택하기/ | +| 23 | 정밀 스냅 | 일시적 단발 객체 스냅 호출 | Shift + 마우스 우클릭 | 명령 수행 중 점 지정 단계에서 Shift + 우클릭 ➔ 팝업 메뉴에서 스냅 선택 | 전역 F3 설정과 무관하게 지정한 1회에 한해 해당 스냅(중점·접선 등)이 강제 적용됨 | 「꼭 있어야 함」 · 「자주 씀」 | https://pbangaze.tistory.com/96 | +| 24 | 드로잉/편집 | 자르기 중 연장 전환 | Shift + 클릭 | TRIM(TR) 명령 실행 중 Shift 키를 누른 상태에서 선 클릭 | 자르기 기능 대신 마우스가 가리킨 경계선까지 선이 자동으로 연장됨 | 「자주 씀」 · 「초보가 헷갈림」 | https://officedocworks.com/자르기trim-연장하기extend-객체-선택이-안됨/ | +| 25 | 드로잉/편집 | 연장 중 자르기 전환 | Shift + 클릭 | EXTEND(EX) 명령 실행 중 Shift 키를 누른 상태에서 선 클릭 | 연장 기능 대신 교차하는 경계선을 기준으로 선이 즉시 잘려나감 | 「자주 씀」 · 「초보가 헷갈림」 | https://officedocworks.com/자르기trim-연장하기extend-객체-선택이-안됨/ | +| 26 | 드로잉/편집 | 모깎기 반지름 0 직각 모서리 정리 | Shift + 클릭 (또는 F ➔ R ➔ 0) | FILLET(F) 실행 후 Shift 누른 채 두 선 클릭 (또는 R값 0 지정 후 클릭) | 모깎기 곡률 없이 두 선이 교차점까지 자동 연장/절단되어 직각 모서리로 정리됨 | 「자주 씀」 | https://renderlog.tistory.com/15 | +| 27 | 형상 작도 | 사각형 치수(D) 지정 작도 | REC ➔ 구석점 ➔ D ➔ 가로 ➔ 세로 | REC ➔ 첫 구석점 클릭 ➔ D 입력 ➔ 가로수치 ➔ 세로수치 ➔ 방향 클릭 | 가로·세로 수치가 정확히 지정된 닫힌 직사각형 폴리선이 생성됨 | 「꼭 있어야 함」 · 「자주 씀」 | https://yjk923.tistory.com/entry/캐드-stretch-사용할-때-주의할-점-2가지 | +| 28 | 형상 작도 | 원 지름(D) 수치 작도 | C ➔ 중심점 ➔ D ➔ 지름수치 | C ➔ 중심점 클릭 ➔ D 입력 ➔ 지름 수치 입력 ➔ Space | 반지름 암산 환산 없이 지정한 지름 크기를 가진 정확한 원이 작도됨 | 「꼭 있어야 함」 · 「초보가 헷갈림」 | https://jangpro1253.tistory.com/85 | +| 29 | 형상 작도 | 간격띄우기(Offset) 연속 작업 | O ➔ 간격거리 ➔ 객체클릭 ➔ 방향클릭 | O 입력 ➔ 거리 수치 입력 ➔ 원본 객체 클릭 ➔ 띄울 방향 클릭 ➔ 다음 객체 반복 | 입력한 거리만큼 정확하게 평행 이격된 동일 형상의 복사본이 생성됨 | 「꼭 있어야 함」 · 「자주 씀」 | https://pyyny200.tistory.com/33 | +| 30 | 편집/변형 | 신축(Stretch) 꼭짓점 부분 변형 | S ➔ 걸침 선택 ➔ 기준점 ➔ 수치 입력 | S ➔ 우에서 좌로 드래그하여 변형할 정점만 걸침 선택 ➔ 기준점 클릭 ➔ 이동 거리 입력 | 선택된 정점만 이동하며 연결된 선분들의 길이가 늘어나거나 줄어듦 | 「꼭 있어야 함」 · 「초보가 헷갈림」 | https://yjk923.tistory.com/entry/캐드-stretch-사용할-때-주의할-점-2가지 | +| 31 | 그립 편집 | 그립 클릭 후 Space 모드 순환 | 객체 선택 ➔ 그립 클릭 ➔ Space 연타 | 객체 선택 ➔ 정점 파란 그립 클릭(빨갛게 전환) ➔ Space 누를 때마다 모드 순환 | 신축(STRETCH)➔이동(MOVE)➔회전(ROTATE)➔축척(SCALE)➔대칭(MIRROR) 순으로 전환됨 | 「자주 씀」 · 「초보가 헷갈림」 | https://cad2012.tistory.com/entry/AutoCAD-명령어-정리-Grips를-사용한-편집 | +| 32 | 클립보드 | 기준점 포함 복사 (COPYBASE) | Ctrl + Shift + C | Ctrl + Shift + C ➔ 기준점 클릭 ➔ 복사할 객체 선택 ➔ Space | 지정한 기준점 정보가 포함된 채 클립보드에 객체들이 복사 저장됨 | 「꼭 있어야 함」 · 「자주 씀」 | https://pilyo.tistory.com/40 | +| 33 | 클립보드 | 블록으로 즉시 붙여넣기 (PASTEBLOCK) | Ctrl + Shift + V | 복사된 상태에서 Ctrl + Shift + V ➔ 붙여넣을 삽입 위치 클릭 | 별도의 BLOCK 정의 없이 클립보드 내용물이 하나의 블록으로 묶여 배치됨 | 「꼭 있어야 함」 · 「자주 씀」 | https://pilyo.tistory.com/40 | + +--- + +## 부록 3. K1 — `K1_오토캐드_마우스키보드.md` + +# K1 오토캐드 마우스 · 키보드 조작 — 공식 문서 한 벌 (요구 227-3) + +- 범위 = AutoCAD 데스크톱 마우스 · 키보드 조작만(숫자 넣기 · 그리기 · 고치기). 다른 CAD · 리습 · 토목 · 터치 · AI 뺌. +- 출처 = help.autodesk.com 2025~2027 우선(없으면 옛 판 · 연도 표시) · 실제로 연 쪽만 번호([K..] · [E1-..] · [E2-..]). +- 「새」 = 부록1(`2026-10-15_335_부록1_오토캐드입력.md` · 그 안의 331)에 없음 · 보강 = 이름은 있고 키 · 프롬프트 · 기본값을 새로 밝힘 · 빈칸 = 이미 있음. +- 프롬프트 · 옵션 = 영어 원문 · DI = 동적 입력 · 부록1 표로 충분한 `'CAL` · QuickCalc · 끌어 놓기 · Ctrl+화살표 · 고르기 옵션(WP · CP · F)은 줄 안 둠. + +## 결론 + +1. 점 묻는 프롬프트 하나가 클릭 · `x,y` · `@dx,dy` · `d`. +5. 오른 단추 = 세 상황(명령 없음 · 고른 것만 · 명령 중)마다 Enter/반복 또는 메뉴 · 시간차 250 ms(켤 때) · Shift+오른 = 한 번 스냅 메뉴 · 휠 = 한 칸 60% · 두 번 = 범위 · 누르고 끌기 = 이동. +6. 고르기 = 빈 곳 클릭 두 번 또는 끌기 · 왼→오 창 · 오→왼 걸침 · 끄는 중 Space 로 방식 돌림 · Shift = 빼기 · Enter/Space/오른 = 끝 · Esc = 해제 · Delete = 지움 · 두 번 클릭 = 물체별 편집 명령. +7. 그립 = 클릭 → 늘리기 · Enter/Space 로 옮기기 → 돌리기 → 크기 → 대칭 · `C` 복사 · `B` 기준점 · Ctrl 누른 채 = 복사 · 멈추면 메뉴(Ctrl 로 돌림) · 글 · 블록 · 원 중심 그립은 통째 옮김. +8. 임시 우선 키 26개(Shift 직교 · Shift+E 끝점 · M 중간 · C 중심 · A 스냅 · X 극 추적 · Q 스냅 추적 · D 모두 끔 · S 강제 · 오른손 짝 키) — 누르는 동안만. +9. 명령 40여 개 프롬프트 · 옵션 글자 = 표 E · 빈 Enter 뜻이 명령마다 다름(LINE · PLINE · ARC 첫 점 = 마지막 끝에서 이어 · MOVE · COPY · STRETCH 둘째 점 = 첫 점 값이 변위 · OFFSET = 현재 거리 · MIRROR = 원본 둠 · TRIM 표준 = 모두 자르는 선) · Shift = FILLET · CHAMFER 0 · TRIM↔EXTEND. +10. 확인 못 함 = Ctrl+Z/Y 명령 중 동작 · Ctrl+오른 단추 · `<<` · `<<<` · 그립 모드 프롬프트 원문 · 별칭 일부 · 2026 판 명령 프롬프트 원문(2015~2016 Mac 판 원문으로 대신). + +## A. 숫자 넣기 + +| 무엇 | 키/마우스 | 누르는 차례(프롬프트) | 결과 | 새 | 출처 | +|---|---|---|---|---|---| +| 절대 직교 | `x,y` | 점 프롬프트 → `3,4` Enter | UCS 원점 기준 점 · 쉼표 = X · Y 구분 · 명령줄에선 `#` 안 씀 | | [K05] | +| 상대 직교 | `@dx,dy` | `#-2,1` → `@5,0` → `@0,3` → `@-5,-3` | 마지막 점 기준(공식 삼각형 예) | | [K05] | +| 절대 극 | `d:` `@dx,dy` 또는 직접 거리 | 기준점에서 띄운 점 · MOVE · COPY 끌기 중엔 못 씀 · 절대 좌표 치면 FROM 취소 | 보강 | [K41] | +| 두 점 가운데 | `M2P` · `MTP` | `First point of mid:` → `Second point of mid:` | 두 점 가운데 · 상시 · 한 번 스냅과 같이 씀 | 보강 | [K46] | +| 연속 추적 | `TK` · `TRACK` · `TRACKING` | `First tracking point:` → `Next point (Press ENTER to end tracking):` 되풀이 → Enter | 꺾어 가며 찾은 점 · 추적 중 직교 켜짐 · Shift+오른 메뉴에도 | 보강 | [K44] | +| 좌표 거르개 | `.x` `.y` `.xy` `.xz` `.yz` | `.x` → `of` 스냅으로 점 → `(need YZ)` 다른 점 | X 는 첫 점 · Y · Z 는 둘째 점 · 점 묻는 아무 프롬프트(쪽에 `.z` 는 없음) | 보강 | [K42] | +| 한 번 스냅 | `END` `MID` `CEN` `GCE` `NOD` `QUA` `INT` `EXT` `INS` `PER` `TAN` `NEA` `APP` `PAR` `NON` | 점 프롬프트 → 이름 Enter → 점 | 다음 한 점만 · 줄임 = 대문자 글자 | 보강 | [K45] [K43] | +| 상시 스냅 글로 | `-OSNAP` | `END,MID,INT` Enter · `NONE` · `OFF` = 지움 | 쉼표로 여러 개 · `OSNAP` 은 대화상자 | 보강 | [K45] | +| 각 단위 · 기준 | 변수 | AUNITS **0** 십진도(1 도분초 · 2 그라드 · 3 라디안 · 4 측량) · ANGBASE **0** · ANGDIR **0** 반시계(1 시계) | 친 각을 이 기준으로 읽음 · 모두 도면에 저장 | 보강 | [K56] [K14] [K15] | + +## B. 명령 중 키 + +| 무엇 | 키/마우스 | 누르는 차례(프롬프트) | 결과 | 새 | 출처 | +|---|---|---|---|---|---| +| 시작 · 확정 | Enter · Space · 오른 단추 | 이름 · 별칭 → Enter/Space · 값 → Enter | 명령 시작 · 값 넘김 · 도움말은 이 Enter 를 따로 안 적음 | | [K36] [K16] | +| 기본값 받기 | 빈 Enter | `... <4>:` → Enter | `< >` 안 현재값 유지 | | [K16] [K36] | +| 직전 명령 반복 | Enter · Space · 오른 단추 | 명령 끝난 뒤 | 직전 명령 다시(오른 단추는 설정 따라) | | [K36] [K37] | +| Space 예외 | Space | TEXT 글 넣기 중 | Space = 빈칸 글자 · TEXT 는 Enter 로만 끝 · Enter 여러 번 필요한 명령 있음 | 보강 | [K38] | +| 취소 | Esc · Ctrl+[ · Ctrl+\ | 아무 때 | 명령 취소 · 빈 곳 눌러 창 시작한 뒤 Esc = 그 고르기 취소 | | [K36] [K50] [K20] | +| 고른 것 해제 | Esc | 명령 없이 고른 상태 | 모두 해제 | 새 | [K49] | +| 명령 이력 | ↑ · ↓ | 명령 없을 때 | 이 세션 명령 차례로 → Enter = 다시 시작 | | [K36] [K40] | +| 값 이력 | ↑ · ↓ | 명령 중 프롬프트 | 그 프롬프트의 최근 값(기본 20개 · CMDINPUTHISTORYMAX) | 새 | [K40] | +| 최근 값 메뉴 | 오른 단추 → [Recent Input] | 그림 영역 | 이 명령 · 이 세션 최근 값(INPUTHISTORYMODE 2 · 4) | 새 | [K36] [K33] | +| 최근 명령 메뉴 | 명령줄 오른 단추 → [Recent Commands] | 명령줄 위 | 고른 명령 다시 시작 | 새 | [K40] | +| 최근 점 표지 | — | 점 프롬프트 | 최근에 친 점 자리에 표지(INPUTHISTORYMODE 8) | 새 | [K33] | +| 명령줄 Tab · 후보 | Tab · 화살표 → Enter/Space | 글자 몇 개 → Tab 되풀이 / 후보 목록(지연 300 ms) | Tab = 그 글자로 시작하는 명령 · 시스템 변수 차례로 · 후보 고르면 시작 | 새 | [K16] [K50] [K40] | +| 옵션 고름 | 대문자 글자 + Enter · 클릭 | `[Close/Undo]` → `C` Enter | 대소문자 무관 · 옵션 글자 눌러도 됨 | | [K16] [K36] | +| 옵션 목록(DI) | ↓ | 프롬프트 옆 ↓ 표시 | 목록 → ↑↓ → Enter | | [K03] | +| 투명 명령 | `'` | 아무 프롬프트 → `'ZOOM` | 끝나면 원래 명령 이어감 | | [K16] | +| 도움말 | F1 | 명령 중 · 항목 위 커서 | 그 명령 · 항목 도움말 | 새 | [K37] [K40] | +| 고르기 끝 | Enter · Space · 오른 단추 | `Select objects:` | 고르기 마침 → 다음 단계 | 새 | [K49] | +| 고르기 옵션 | `L` · `P` · `ALL` · `?` | `Select objects:` | 마지막 물체 · 앞 고름 · 모두 · 고르기 방법 목록 | | [K57] | +| 선분 취소 | `U` | LINE · PLINE 다음 점 프롬프트 | 마지막 선분 취소 · 되풀이 = 거꾸로 | | [K39] [K50] | +| 되돌림 | Ctrl+Z · Ctrl+Y | 명령 없을 때 | 마지막 동작 되돌림 · 앞 되돌림 취소 · 명령 중 동작 = 확인 못 함 | 보강 | [K20] | +| 지우기 | Delete | 고른 뒤(명령 없음) | 지움 | 새 | [K49] | +| 반복 | `MULTIPLE` | `MULTIPLE` → 명령 이름 | Esc 까지 되풀이 · 값은 매번 | | [K36] | + +## C. 마우스 + +| 무엇 | 키/마우스 | 누르는 차례 | 결과 | 새 | 출처 | +|---|---|---|---|---|---| +| 왼 단추 | 클릭 | — | 점 찍기 · 물체 고르기 · 메뉴 · 단추 · 칸 | 새 | [K23] | +| 오른 단추 | 클릭 | — | 상황 따라 명령 끝(Enter) · 메뉴 · 스냅 메뉴 · SHORTCUTMENU 로 바꿈 | 보강 | [K23] | +| 오른 · 기본 모드 | 클릭 | 명령 없음 · 고른 것 없음 | [Repeat Last Command] = 직전 명령 반복 / [Shortcut Menu] = 기본 메뉴 | 새 | [K17] [K25] | +| 오른 · 편집 모드 | 클릭 | 고른 것 있음 · 명령 없음 | 반복 / 편집 메뉴(물체 종류별 항목 붙음) | 새 | [K17] [K26] | +| 오른 · 명령 모드 | 클릭 | 명령 중 | [Enter] / [메뉴 늘] / [옵션 있을 때만 메뉴](없으면 Enter) · 메뉴에 그 명령 대괄호 옵션 들어감 | 새 | [K17] [K25] [K26] | +| 시간차 오른 단추 | 짧게 · 길게 | 오른 단추 짧게 / 오래 | 짧게 = Enter · 길게(기본 250 ms · 100~10000) = 메뉴 · SHORTCUTMENU 가 15 넘어야 | 보강 | [K25] [K58] | +| 메뉴 모두 끔 | 설정 | [Shortcut Menus in Drawing Area] 끔 | 오른 단추 = Enter | 새 | [K13] | +| 스냅 메뉴 | Shift+오른 | 점 프롬프트 | 스냅 종류 · 추적 · From · M2P · 다음 한 점만 | | [K26] [K43] | +| 그립 메뉴 | 오른 단추 | 그립 누른 뒤 | 그립 모드 · 옵션 메뉴 | | [K26] [K29] | +| Ctrl+오른 단추 | — | — | 공식 쪽에 없음 | 확인 못 함 | — | +| 휠 굴림 | 앞 · 뒤 | 아무 때(명령 중도) | 앞 = 확대 · 뒤 = 축소 · 한 칸 = ZOOMFACTOR **60**(3~100) · ZOOMWHEEL 1 = 반대 | 보강 | [K23] [K22] [K59] | +| 휠 두 번 | 두 번 누름 | — | 범위 확대 | | [K23] [K22] | +| 휠 누르고 끌기 | 끌기 | MBUTTONPAN **1** | 화면 이동 | | [K23] [K60] | +| Ctrl+휠 끌기 | 끌기 | MBUTTONPAN 1 | 조이스틱 이동 · Ctrl+Shift+휠 끌기 = 자유 궤도(3D · AutoCAD 만) | 보강 | [K23] | +| 휠 클릭 | 클릭 | MBUTTONPAN 0 | 스냅 메뉴(CUI 정의 동작) | | [K23] [K60] | +| 단추 다시 지정 | CUI | — | 단추 1(고르기) 빼고 · Shift · Ctrl · Ctrl+Shift 조합별 | 새 | [K23] | +| 클릭 고르기 | 클릭 | 물체 위 | 고르기 상자(PICKBOX **3** · 0 이면 고르기 미리보기 없음) 안 물체 | 보강 | [K49] [K61] | +| 창 두 방식 | 클릭-클릭 · 누르고 끌기 | 빈 곳 | PICKDRAG **2** = 둘 다 · 0 = 두 번 클릭 · 1 = 끌기(올가미) | 새 | [K34] | +| 창 · 걸침 | 끄는 방향 | 빈 곳 → 오른쪽 / 왼쪽 | 왼→오 = 안에 든 것 · 오→왼 = 걸친 것 | | [K49] | +| 올가미 | 누르고 끌기 | 빈 곳 | 창 · 걸침 올가미 · 끄는 중 Space = 창 → 걸침 → 울타리 · 울타리 옵션 중 끌기 = 자유 울타리 | | [K35] [K18] | +| 물체 위 끌기 | 누르고 끌기 | 명령 전 · 물체 위 | PICKAUTO 2 비트 = 물체 위에서도 창 시작 · 기본 **5** = 1(빈 곳 클릭 창) + 4(빈 곳 끌기 올가미) | 보강 | [K35] | +| 빼기 · 더하기 | Shift+클릭 · Shift+창 | 고른 상태 | PICKADD **2** = 고를 때마다 더함 · Shift = 뺌 · 0 · 3 = 바꿔치기(Shift = 더함 · 3 은 빈손 Shift 에도 앞 고름 유지) | 보강 | [K62] [K49] | +| 겹친 물체 | Ctrl+W · Shift+Space | 겹친 곳 | SELECTIONCYCLING **0**(2026 참조) · 1 = 배지 · 2 = 배지 + 목록 창 · 음수 = 끈 채 저장 | 보강 | [K30] | +| 두 번 누름 | 두 번 클릭 | 물체 위 | DBLCLKEDIT **1** · 폴리선 PEDIT · 글 · 치수 · 속성 정의 TEXTEDIT · 여러 줄 글 MTEDIT · 속성 블록 EATTEDIT · 속성 ATTIPEDIT · 블록 PROPERTIES 또는 -BEDIT(BLOCKEDITLOCK) · 스플라인 SPLINEDIT · 외부 참조 REFEDIT · 그림 IMAGEADJUST · 뷰포트 · OLE 지정 불가 | 보강 | [K47] [K63] | +| 그립 고름 | 클릭 | 고른 물체의 그립 | 뜨거운 그립 · 기본 늘리기 | | [K49] [K28] | +| 여러 그립 | Shift+클릭 | 그립 고르기 전 Shift | 여러 뜨거운 그립 · 그 사이 모양 그대로 늘림 | | [K27] [K29] | +| 그립 모드 돌림 | Enter · Space | 뜨거운 그립 뒤 | 늘리기 → 옮기기 → 돌리기 → 크기 → 대칭 → 늘리기 · 명령줄에 지금 모드 | | [K28] [K29] | +| 그립 복사 · 기준점 | `C`+Space · `B`+Space | 아무 그립 모드 | 복사 여러 개 · 기준점 새로 | | [K29] | +| 그립 Ctrl | Ctrl 누른 채 | 그립 옮겨 클릭할 때까지 | 복사 · 끌며 Ctrl = 더 복사 | 보강 | [K28] [K29] | +| 다기능 그립 | 커서 멈춤 · Ctrl | 그립 위 멈춤 → 메뉴 / 그립 누른 뒤 Ctrl | 물체별 메뉴 · Ctrl 로 항목 돌림 · Esc 로 빠짐 | | [K28] [K29] | +| 옮겨지는 그립 | 그립 끌기 | 글 · 블록 · 선 가운데 · 원 중심 · 점 그립 | 늘리기 대신 통째 옮김 · 원 · 타원 사분점 그립 거리 = 중심부터 | 새 | [K27] | +| 그립 안 보임 | — | 잠긴 레이어 · 처음 고른 수 > GRIPOBJLIMIT **100** | 그립 없음(뒤에 더 고르면 제한 안 걸림) | 보강 | [K27] | +| 겹친 그립 | 그립 | 여러 물체가 같은 자리 그립 | 그립 모드만 · 물체별 옵션 없음 | 새 | [K27] | +| 그립 프롬프트 | — | `** STRETCH **` 등 | 공식 원문 못 찾음 · 공식 확인 옵션 = `C` 복사 · `B` 기준점 | 확인 못 함 | [K29] | + +## D. 단축키 + +| 무엇 | 키 | 누르는 차례 | 결과 | 새 | 출처 | +|---|---|---|---|---|---| +| F 키 | F1~F12 | 아무 때 | F1 도움말 · F2 명령 기록 펼침(떠 있을 때) · 글 창(붙어 있을 때) · F3 OSNAP · F4 3DOSNAP(LT = TABMODE) · F5 ISOPLANE · F6 UCSDETECT · F7 GRIDMODE · F8 ORTHOMODE · F9 SNAPMODE · F10 극 추적 · F11 스냅 추적 · F12 DI | | [K20] | +| 임시 · 직교 | Shift 누르는 동안 | 점 프롬프트 | 직교 반전 · 떼면 원래 | | [K19] [K21] | +| 임시 · 스냅 켬끔 | Shift+A · Shift+' | 누르는 동안 | OSNAP 반전 | 새 | [K19] | +| 임시 · 끝점 | Shift+E · Shift+P | 누르는 동안 | 끝점 스냅만 | 보강 | [K19] | +| 임시 · 중간점 | Shift+M · Shift+V | 누르는 동안 | 중간점 스냅만 | 새 | [K19] | +| 임시 · 중심 | Shift+C · Shift+, | 누르는 동안 | 중심 스냅만 | 새 | [K19] | +| 임시 · 모두 끔 | Shift+D · Shift+L | 누르는 동안 | 스냅 · 추적 모두 끔 | 새 | [K19] | +| 임시 · 스냅 강제 | Shift+S · Shift+; | 누르는 동안 | 객체 스냅 강제(Enforcement) | 새 | [K19] | +| 임시 · 극 추적 | Shift+X · Shift+. | 누르는 동안 | 극 추적 반전 | 새 | [K19] | +| 임시 · 스냅 추적 | Shift+Q · Shift+] | 누르는 동안 | 스냅 추적 반전 | 새 | [K19] | +| 임시 · 배치 제안 | Shift+W · Shift+[ | 누르는 동안 | 블록 배치 제안 반전 | | [K19] | +| 임시 · 동적 UCS | Shift+Z · Shift+/ | AutoCAD 만 | UCSDETECT 반전 | 새 | [K19] | +| 임시 키 동작 | 누름 · 뗌 | CUI 키마다 누를 때 매크로 · 뗄 때 매크로 | 뗄 때 칸이 비면 변수 원래대로 · 표는 영문 US 자판 기준 · TEMPOVERRIDES **1**(0 = 모두 끔) | 새 | [K21] [K64] | +| F 키도 임시 표에 | F3 · F6 · F8~F12 | — | 임시 우선 키 표에도 실림 · 누르고 있을 때 동작 차이는 확인 못 함 | 새 | [K19] | +| Ctrl · 제도 켬끔 | Ctrl+B · G · L · U · F · W · E · D · I · Shift+I · Shift+A · H | 아무 때 | 스냅 · 그리드 · 직교 · 극 추적 · 상시 스냅 · 선택 순환 · 등각 평면 · 동적 UCS · 좌표 표시 · 구속 추론 · 그룹 · PICKSTYLE | | [K20] | +| Ctrl · 편집 | Ctrl+A · C · X · V · Z · Y · Shift+C · Shift+V · Shift+L · J · M | 아무 때 | 모두 고름 · 복사 · 잘라냄 · 붙임 · 되돌림 · 되살림 · 기준점 복사 · 블록으로 붙임 · 앞 고름 · 직전 명령 반복(J · M) | | [K20] | +| Ctrl · 창 · 파일 | Ctrl+0~9 · N · O · S · P · Q · R · F2 · F4 · Tab · PgUp · PgDn 등 | 아무 때 | 화면 정리 · 팔레트 · 새 · 열기 · 저장 · 출력 · 끝 · 뷰포트 돌림 · 글 창 · 도면 닫기 · 파일 탭 · 배치 넘김 · Shift+F1~F5 = 3D 부분 고르기(그리기 입력 밖) | 보강 | [K20] | +| Alt | Alt · Alt+F · Alt+H · Alt+F4 · Alt+F8 · Alt+F11 | 아무 때 | 리본 키팁 · 앱 메뉴 · 홈 탭 · 앱 닫기 · 매크로 · VBA | 새 | [K55] [K20] | +| 별칭 · 그리기 | `L` `PL` `REC` `C` `A` `EL` `H` · `BH` `T` · `MT` | 명령줄 → Enter/Space | LINE · PLINE · RECTANG · CIRCLE · ARC · ELLIPSE · HATCH · MTEXT | 보강 | [K48] [K65] | +| 별칭 · 고치기 | `E` `M` `CO` · `CP` `RO` `SC` `MI` `O` `F` `CHA` `TR` `EX` `AR` `BR` `J` `LEN` | 명령줄 → Enter/Space | ERASE · MOVE · COPY · ROTATE · SCALE · MIRROR · OFFSET · FILLET · CHAMFER · TRIM · EXTEND · ARRAY · BREAK · JOIN · LENGTHEN | 보강 | [K65] | +| 별칭 · 치수 · 재기 | `DLI` `DAL` `DAN` `DRA` `DDI` `DIV` `ME` `AA` `Z` `LA` | 명령줄 → Enter/Space | DIMLINEAR · DIMALIGNED · DIMANGULAR · DIMRADIUS · DIMDIAMETER · DIVIDE · MEASURE · AREA · ZOOM · LAYER | 보강 | [K65] [K48] | +| 별칭 못 굳힘 | `POL` `SPL` `XL` `S` `DI` `DT` `DCO` `DBA` `MEA` `PE` `X` `MA` | — | POLYGON · SPLINE · XLINE · STRETCH · DIST · TEXT · DIMCONTINUE · DIMBASELINE · MEASUREGEOM · PEDIT · EXPLODE · MATCHPROP 로 흔히 알려짐 · 공식 표 쪽 403 | 확인 못 함 | — | +| 별칭 파일 | acad.pgp | `A, *ARC` · `;` 주석 · 맨 끝 사용자 칸 · ALIASEDIT · REINIT | 같은 별칭은 마지막 줄이 이김 | | [K48] | + +## E. 형상 그리기 · 고치기 명령 + +프롬프트 원문 = 같은 GUID 2015 · 2016 Mac 판(2026 판엔 괄호 줄 거의 없음 · COPY 만 2026) · 옵션 · 동작 = 2026 · 키 = 대괄호 안 대문자(`mOde` → O) · 원문 없는 옵션 = 이름만. + +### E1. 그리기 · 글자 · 해치 · 재기 + +| 무엇 | 키/마우스 | 누르는 차례(공식 프롬프트) | 결과 | 새 | 출처 | +|---|---|---|---|---|---| +| LINE 첫 점 | 점 · Enter | `Specify first point:` | Enter = 마지막 선 · 폴리선 · 호 끝에서 시작(호면 접선 · 길이만 넣어도 됨) | 보강 | [E1-01] [E1-02] | +| LINE 다음 점 | 점 · `C` · `U` | `Specify next point or [Close/Undo]:` | C = 첫 점으로 닫고 끝(선분 둘 이상) · U = 마지막 선분 지움(되풀이 = 거꾸로) · 동적 입력 켬 = 상대 기본 | | [E1-01] [E1-02] | +| PLINE 시작 | 점 · Enter | `Specify start point:` → `Current line-width is ` | Enter = 마지막 폴리선 · 선 · 호 끝에서 시작 · 첫 점 + 표지 | 새 | [E1-03] [E1-04] | +| PLINE 선 | `A` `C` `H` `L` `U` `W` · Enter | `Specify next point or [Arc/Close/Halfwidth/Length/Undo/Width]:` | A = 호로 · C = 닫고 끝 · L → `Specify length of line:` 앞 선분 각(호면 접선) · U = 마지막 선분 지움 · Enter = 끝 | 새 | [E1-03] [E1-04] [E1-05] | +| PLINE 폭 | `W` · `H` | `Specify starting width :` → `Specify ending width :` | 끝 폭 Enter = 같은 폭 · 끝 폭이 뒤 선분 폭 | 새 | [E1-04] [E1-05] | +| PLINE 호 | `A` `CE` `CL` `D` `H` `L` `R` `S` `U` `W` | `Specify endpoint of arc or [Angle/CEnter/CLose/Direction/Halfwidth/Line/Radius/Second pt/Undo/Width]:` | 점 = 접선 호 · L = 선으로 · CE 두 글자(CEN 스냅과 구별) · 각 + 반시계 − 시계 · D 단계 Ctrl = 시계 | 새 | [E1-03] [E1-04] | +| PLINE 호 하위 | A · CE · R · S · D | `Specify included angle:` → `[Center/Radius]` · `Specify center point of arc:` → `[Angle/Length]` · `Specify radius of arc:` → `[Angle]` · `Specify direction of chord for arc :` · `Specify second point on arc:` · `Specify the tangent direction from the start point of arc:` | 현 방향 Enter = 꺾쇠 값 | 새 | [E1-04] | +| RECTANG 첫 구석 | `C` `E` `F` `T` `W` | `Specify first corner point or [Chamfer/Elevation/Fillet/Thickness/Width]:` | 모따기 · Z 높이 · 모깎기 · 두께 · 폭(E · T 는 다음 기본값) | 새 | [E1-06] | +| RECTANG 맞은 구석 | 점 · `D` · `A`→`L`/`W` · `R`→`P` | Area · Dimensions · Rotation(괄호 원문 못 찾음) | D = 길이 → 너비 → 쪽 점 · A = 넓이 → 길이나 너비 → 나머지 자동 · R = 각 또는 두 점 | 새 | [E1-06] [E1-07] | +| CIRCLE | `3P` `2P` `T` | `Specify center point for circle or [3P/2P/Ttr (tan tan radius)]:` | 중심 → 반지름 또는 지름(값 · 점) · Ttr = 두 접 물체 + 반지름 · Tan Tan Tan = 2026 본문만 | 새 | [E1-08] [E1-09] | +| ARC 시작 | 점 · `C` · Enter | `Specify start point of arc or [Center]:` | Enter = 마지막 선 · 호 · 폴리선 끝 접선 호 → 끝점만 · 기본 반시계 · 끌 때 Ctrl = 시계 | 새 | [E1-10] [E1-11] | +| ARC 둘째 점 | `C` · `E` | `Specify second point of arc or [Center/End]:` → `Specify end point of arc:` | 세 점 호 | 새 | [E1-11] | +| ARC 중심 · 끝 | `A` `L` · `A` `D` `R` | `Specify end point of arc or [Angle/chord Length]:` · `Specify center point of arc or [Angle/Direction/Radius]:` → `Specify included angle:` · `Specify length of chord:` · `Specify tangent direction for the start point of arc:` · `Specify radius of arc:` | 각 − = 시계(마우스 반시계로도) · 현 + 작은 호 − 큰 호 · 반지름 − = 큰 호 | 새 | [E1-10] [E1-11] | +| POLYGON | 숫자 · `E` · `I` `C` | `Enter number of sides :` → `Specify center of polygon or [Edge]:` → `Enter an option [Inscribed in circle/Circumscribed about circle] :` → `Specify radius of circle:` | 3~1024 · Enter = 꺾쇠 값 · 반지름 점 = 크기 + 회전 · 숫자 = 아래 변이 스냅 각 · E = 변 두 끝점 | 새 | [E1-12] [E1-13] [E1-05] | +| ELLIPSE | `A` `C` `I` | `Specify axis endpoint of ellipse or [Arc/Center/Isocircle]:` | 축 두 끝 → 다른 축 반 길이 또는 Rotation(0 = 원) · C = 중심부터 · A = 타원호(시작각 · 끝각 · Parameter · Included angle) · I = 등각 스냅 때 등각 원 | 새 | [E1-14] [E1-15] | +| SPLINE 첫 점 | `M` `K` `D` `O` | `Specify first point or [Method/Knots/Object]:`(맞춤점) · `[Method/Degree/Object]`(CV) — Mac 원문은 둘이 뒤바뀌어 적힘 | M = Fit · CV · K = Chord · Square root · Uniform · D = 차수 1~10 · O = 폴리선 → 스플라인 | 새 | [E1-16] [E1-17] | +| SPLINE 다음 점 | 점 · Enter · Undo · Close | 괄호 원문 못 찾음 | Enter = 끝 · Undo = 마지막 점 · Close = 닫음(주기) · Start · End Tangency · Tolerance 0 = 점 통과 | 새 | [E1-16] | +| XLINE | `H` `V` `A` `B` `O` | `Specify a point or [Hor/Ver/Ang/Bisect/Offset]:` | 점 = 두 점 통과 · H · V · A(또는 Reference) · B = 각 이등분 · O = 거리 또는 Through | 새 | [E1-18] [E1-19] | +| TEXT 시작 | `J` `S` · Enter | `Specify start point of text or [Justify/Style]:` | 점 → 높이(고정 높이 스타일이면 생략) → 회전각 → 글 · Enter(직전도 TEXT) = 높이 · 각 생략 · 앞 글 아래 | 새 | [E1-20] [E1-21] [E1-22] | +| TEXT 맞춤 | `J` → 이름 | Left · Center · Right · Align · Middle · Fit · TL TC TR ML MC MR BL BC BR | 시작 단계에 바로도 됨 · Align = 두 점에 맞춰 높이 · Fit = 두 점 + 높이로 폭 · `?` = 스타일 목록 | 새 | [E1-20] | +| TEXT 글 칸 | Enter · 클릭 · Tab · Alt+클릭 | 글 넣는 중 | Enter = 아래 새 줄 · 빈 줄 Enter = 끝 · 딴 곳 클릭 = 새 묶음 · Tab/Shift+Tab = 묶음 사이 · Alt+클릭 = 고침 · 우클릭 = 메뉴 | 새 | [E1-20] [E1-22] | +| MTEXT 틀 | `H` `J` `L` `R` `S` `W` `C` | `Specify first corner:` → `Specify opposite corner or [Height/Justify/Line spacing/Rotation/Style/Width/Columns]:` | 두 구석 = 틀 → 편집기 · 옵션 고름 또는 `-MTEXT` = 명령줄로 · 옵션 뒤 같은 줄 다시 | 새 | [E1-23] [E1-24] [E1-25] | +| MTEXT 옵션 값 | J · L · W · C | `Enter justification [TL/TC/TR/ML/MC/MR/BL/BC/BR] :` · L = At least · Distance · Exactly · Spacing factor(`2x`) · C = Static · Dynamic · No columns | W 0 = 줄바꿈 없음 · 높이 바꾸면 TEXTSIZE 바뀜 | 새 | [E1-24] [E1-25] | +| MTEXT 편집기 | Ctrl+Enter · Esc · 밖 클릭 | 편집기 안 | Ctrl+Enter · 밖 클릭 = 저장 끝 · Esc = 저장 없이 끝 | 새 | [E1-22] | +| HATCH | 점 · 옵션 | Pick internal point · Select objects · Draw · Undo · Settings(괄호 원문 못 찾음) | 점 = 둘러싼 경계 · Draw = 점 경계(PLINE 옵션 · Close · Enter 끝 · Area/Path · Rectangle · Circle) · Undo = 마지막 해치 · Settings = 대화상자 | 새 | [E1-26] | +| -HATCH | `P` `S` `W` `B` `A` `DR` `O` `AN` `CO` `LA` `T` | `Specify internal point or [Properties/Select objects/draW boundary/remove Boundaries/Advanced/DRaw order/Origin/ANnotative/hatch COlor/LAyer/Transparency]:` | P = 무늬 · `?` · Solid · `u` · W = 점 경계 · A = 섬 · 틈 허용 · 연관 · 점 단계 우클릭 = 메뉴 · 틈 = 빨간 원 · `.` = 현재값 | 새 | [E1-27] [E1-28] | +| DIST | `'DIST` · `M` | `Specify first point:` → `Specify second point or :` | 거리 · 각 · X · Y · Z 변화(현재 단위) · Z 없으면 현재 높이 · M = 여러 점 누계(호 · 길이 옵션) | 새 | [E1-29] [E1-30] | +| MEASUREGEOM | `D` `R` `A` `AR` `V` · Quick · Mode | `Enter an option [Distance/Radius/Angle/ARea/Volume] :`(2015 · 뒤에 Quick · Mode 더함) | Quick = 커서 따라 값 · 닫힌 곳 클릭 = 넓이 · Shift+클릭 = 더함 · 뺌 · Angle = 호 · 원 · 선 · Vertex · Area = 점 · Add · Subtract · Mode = Quick 늘 기본 또는 마지막 옵션 | 새 | [E1-31] [E1-32] | +| DIVIDE | 숫자 · `B` | `Select object to divide:` → `Enter number of segments or [Block]:` | 2~32,767 · 점 = 구간 − 1 · B = 블록 이름 → 정렬 Yes/No → 수 | 새 | [E1-33] [E1-34] | +| MEASURE | 길이 · `B` | `Select object to measure:` → `Specify length of segment or [Block]:` | 고른 자리 가까운 끝부터 · 닫힌 폴리선 = 첫 정점 · 원 = 스냅 각부터 · 결과 = 앞 선택(P) · NODE 스냅 | 새 | [E1-35] [E1-36] | + +### E2. 고치기 · 치수 + +| 무엇 | 키/마우스 | 누르는 차례(공식 프롬프트) | 결과 | 새 | 출처 | +|---|---|---|---|---|---| +| OFFSET · 거리 | 숫자 · 두 점 · 빈 Enter | `Specify Offset Distance or [Through/Erase/Layer] :` → 물체 → 쪽 점 | 빈 Enter = 현재값 · 되풀이 · Enter 로 끝 | 새 | [E2-01][E2-02] | +| OFFSET · 옵션 | T · E · L | T → 물체 → 지날 점 · E = 원본 지움 · L = 현재/원본 레이어 · 하위 Exit · Multiple · Undo(키 원문 없음) | Multiple = 같은 거리 되풀이 · Undo = 앞 띄우기 취소 | 새 | [E2-01][E2-03] | +| FILLET | 클릭 두 번 · Shift+둘째 | `Select first object or [Undo/Polyline/Radius/Trim/Multiple]:` → `Select second object or shift-select to apply corner:` | 접하는 호(고른 점 쪽) · Shift = 반지름 0 → 뾰족 모서리 · 원은 안 자름 | 새 | [E2-04][E2-05] | +| FILLET · 옵션 | U · P · R · T · M | R = 반지름(0 = 만나게) · T = Trim/No Trim · P → `Select 2D polyline:` 꼭짓점마다 · M = 여러 쌍 · U = 앞 동작 취소 | M 없으면 둘째 뒤 끝 · 짧은 선분 그대로 | 새 | [E2-04][E2-05] | +| CHAMFER | 클릭 두 번 · Shift+둘째 | `Select first line or [Undo/Polyline/Distance/Angle/Trim/mEthod/Multiple/eXpression]:` → 둘째 선 | 거리 · 각은 고른 차례대로 · Shift = 0 → 뾰족 모서리 | 새 | [E2-06][E2-07] | +| CHAMFER · 옵션 | U · P · D · A · T · E · M · X | D = 두 거리(둘 다 0 = 만나게) · A = 거리 + 각 · E = 기본 방식 · X = 식(2026 은 3D 쪽만) | 나머지 FILLET 과 같음 | 새 | [E2-06][E2-07] | +| TRIM · 빠른(2021~ 기본) | 클릭 · 끌기 · 빈 곳 두 점 · Shift | 명령 → 바로 자를 부분 · 옵션 Cutting edges · Crossing · Mode · Project · Erase | 모두가 자르는 선 · 끌기 = 자유 경로 · 두 점 = 울타리 · 못 자르면 지움 · Shift = 늘이기 | 새 | [E2-08] | +| TRIM · 표준 | 고르기 Enter · 빈 Enter | `Select cutting edges ...` `Select objects or :` → 둘째 점 → `Specify dimension line location or [Mtext/Text/Angle/Horizontal/Vertical/Rotated]:` | 빈 Enter = 물체(원은 남북 사분점 쪽이면 수평) · 자리 클릭 = 끝 · T = 명령줄 글(`<>` = 잰 값) · M = 편집기 · A = 글자 각 | 새 | [E2-42] | +| DIMALIGNED | 두 점 · 빈 Enter · M · T · A | 같은 첫 줄 → `Specify dimension line location or [Mtext/Text/Angle]:` | 두 점에 나란한 치수 · 자리 클릭 = 끝 | 새 | [E2-43] | +| DIMANGULAR | 클릭 · 빈 Enter · M · T · A · Q | `Select arc, circle, line, or :` → `Specify dimension arc line location or [Mtext/Text/Angle/Quadrant]:` | 호 = 중심 꼭짓점 · 두 선 = 사이각(180° 미만) · 빈 Enter = 세 점 · Q = 사분면 고정 · 2026 = 폴리선 선분도 | 새 | [E2-44][E2-45] | +| DIMRADIUS · DIMDIAMETER | 클릭 · M · T · A | `Select arc or circle:` → `Specify dimension line location or [Mtext/Text/Angle]:` | 폴리선 호 · 모깎기 호 포함 · 호 밖이면 호 보조선 자동 | 새 | [E2-46][E2-47] | + +## F. 입력 동작을 바꾸는 시스템 변수 + +| 무엇 | 키/마우스 | 값(기본 굵게) | 결과 | 새 | 출처 | +|---|---|---|---|---|---| +| OSMODE | F3 · Ctrl+F | **4133** = END 1 + CEN 4 + INT 32 + EXT 4096 · 비트 MID 2 · NOD 8 · QUA 16 · INS 64 · PER 128 · TAN 256 · NEA 512 · GCE 1024 · APP 2048 · PAR 8192 · 16384 = 상시 스냅 잠시 끔 | 상시 스냅 종류 | 보강 | [K31] | +| OSNAPCOORD | 옵션 [Priority for Coordinate Data Entry] | **2** 키보드 우선(스크립트 빼고) · 0 스냅 우선 · 1 늘 키보드 | 좌표 친 때 스냅 무시 여부 | 새 | [K13] | +| OSOPTIONS | — | **7**(LT 5) = 해치 무시 1 + 동적 UCS 음수 Z 무시 2(LT 없음) + 치수 보조선 끝 무시 4 | 스냅 거름 | 보강 | [K53] | +| APERTURE | — | **10**(1~50) | 스냅 조준 상자 크기(고르기 상자 아님) | 보강 | [K66] | +| AUTOSNAP | — | **63** = 표지 1 · 툴팁 2 · 자석 4 · 극 추적 8 · 스냅 추적 16 · 추적 툴팁 32 | 0 이면 표지 · 자석 · 추적까지 끔 | 새 | [K32] | +| ORTHOMODE | F8 · Ctrl+L · Shift | **0**(도면에 저장) | 1 = 커서 가로 · 세로만 | 보강 | [K67] | +| POLARANG | F10 · Ctrl+U | **90** · 값 90 · 45 · 30 · 22.5 · 18 · 15 · 10 · 5 | 극 추적 증분 | 보강 | [K68] | +| POLARMODE | — | **0** · 비트 1 = 마지막 선분 기준 각 · 2 = 스냅 추적에 극 각 · 4 = 추가각 씀 · 8 = Shift 로 추적점 얻기 | 극 추적 · 스냅 추적 방식 | 보강 | [K52] | +| TEMPOVERRIDES | — | **1** · 0 = 끔 | 임시 우선 키 전체 켬/끔 | 새 | [K64] | +| DYNMODE | F12 | **3** = 포인터 1 + 치수 2 · 0 = 모두 끔(프롬프트 포함) · 음수 = 끈 채 저장 | DI 켬/끔 | 보강 | [K11] [K10] | +| DYNPICOORDS | `#` · `@` | **0** 상대 · 1 절대 | 다음 점 포인터 꼴 | 보강 | [K11] [K12] | +| DYNPIFORMAT | `,` · `<` | **0** 극 · 1 직교 | 다음 점 포인터 꼴 | 새 | [K11] [K12] | +| DYNPIVIS | — | **1** · 칠 때만 / 점 물을 때 / 늘(값 번호 대응은 확인 못 함) | 포인터 칸 보일 때 | 새 | [K11] [K12] | +| DYNPROMPT · DYNINFOTIPS · DYNTOOLTIPS | — | **1** · **1** · **1** | 커서 옆 프롬프트 · 그립 Shift/Ctrl 팁 · 툴팁 모양 적용 범위 | 보강 | [K11] | +| DYNDIVIS · DYNDIGRIP | — | **1** · **31** | 그립 늘릴 때 치수 칸 수 · 종류 | | [K11] | +| INPUTHISTORYMODE | ↑↓ · 오른 단추 | **15** = 1 명령줄 · DI 이력 + 2 이 명령 메뉴 + 4 세션 메뉴 + 8 최근 점 표지 | 최근 입력 보이는 곳 | 새 | [K33] | +| CMDINPUTHISTORYMAX · INPUTSEARCHDELAY | — | **20** · **300** ms | 프롬프트당 기억 값 수 · 자동 완성 목록 지연 | 보강 | [K40] | +| PICKFIRST | — | **1** · 0 = 명령 뒤 고르기만 | 먼저 고르고 명령 | | [K69] | +| PICKADD | Shift | **2**(사용자 설정에 저장) · 0 · 1 · 3 | 고를 때 더함/바꿈 · C 표 참조 | 보강 | [K62] | +| PICKAUTO | — | **5** · 비트 1 · 2 · 4 | 암시 창 · 물체 위 끌기 · 올가미 | 보강 | [K35] | +| PICKDRAG | — | **2** · 0 · 1 | 창 두 점 / 끌기 / 둘 다 | 새 | [K34] | +| PICKBOX | — | **3** | 고르기 상자 높이 · 0 이면 고르기 미리보기 없음 | 새 | [K61] | +| PICKSTYLE | Ctrl+H | **1** · 0 없음 · 2 연관 해치 · 3 둘 다 | 그룹 · 연관 해치 같이 고르기 | 보강 | [K70] | +| SELECTIONCYCLING | Ctrl+W | **0**(2026 참조 · DYK 표는 -2 로 엇갈림) · 1 · 2 | 겹친 물체 배지 · 목록 | 보강 | [K30] | +| SELECTIONPREVIEW · COMMANDPREVIEW | — | **3**(1 명령 없음 + 2 고르기 프롬프트) · **1** | 커서 아래 강조 · 결과 미리보기 · COMMANDPREVIEW 켜면 SELECTIONPREVIEW 무시 | 보강 | [K71] [K72] | +| GRIPS 등 | — | GRIPS **2** · GRIPSIZE **5** · GRIPOBJLIMIT **100** · GRIPMULTIFUNCTIONAL **3** · GRIPBLOCK **0** · GRIPTIPS **1** | 그립 보임 · 크기 · 개수 제한 · 다기능 메뉴 접근 | 보강 | [K29] | +| ZOOMFACTOR · ZOOMWHEEL · MBUTTONPAN | 휠 | **60**(3~100) · **0** · **1** | 휠 한 칸 배율 · 방향 · 휠 끌기 이동 | | [K59] [K22] [K60] | +| SHORTCUTMENU | 오른 단추 | **11** = 1 기본 + 2 편집 + 8 옵션 있을 때 명령 메뉴 · 4 = 명령 메뉴 늘 · 16 = 길게 눌러 메뉴 · 0 = 모두 끔 | 오른 단추 뜻 | 보강 | [K24] | +| SHORTCUTMENUDURATION | 오른 단추 | **250** ms(100~10000) | 길게 누름 기준 · SHORTCUTMENU 15 넘을 때만 | 보강 | [K58] | +| DBLCLKEDIT | 두 번 클릭 | **1** · 0 · On/Off 도 받음 | 두 번 눌러 고치기 | 보강 | [K63] | +| LASTPOINT | `@` | **0,0,0** · 저장 안 됨 | 마지막 점 | | [K51] | +| ANGBASE · ANGDIR · AUNITS | — | **0** · **0** 반시계 · **0** 십진도 | 각 입력 해석 | 보강 | [K14] [K15] [K56] | +| UNITMODE | — | **0** 보고 꼴(빈칸 구분) · 1 입력 꼴 | 분수 · 피트인치 · 측량 각 표시 꼴 · 값 넣을 때 빈칸 못 씀 | 새 | [K54] | +| CIRCLERAD · POLYSIDES · PLINEWID | — | **0**(기본 없음) · **4**(3~1024) · **0** | 원 반지름 · 변 수 · 폴리선 폭 기본 | 새 | [E1-37] [E1-38] [E1-39] | +| SPLMETHOD · SPLKNOTS · SPLDEGREE | — | **0** 맞춤점(1 CV) · **0** Chord(1 Square root · 2 Uniform) · **3**(변수 쪽 1~5 · 명령 쪽 1~10) | 스플라인 기본 | 새 | [E1-40] [E1-41] [E1-42] | +| PELLIPSE · LASTANGLE | — | **0** 참 타원(1 폴리선) · **0**(읽기 전용) | 타원 꼴 · 마지막 호 · 선 끝 접선 각 = 이어 그리기 기준 | 새 | [E1-43] [E1-44] | +| TEXTED · TEXTSIZE · MTEXTCOLUMN | — | **2**(0 편집기 · 1 명령줄 · 2 = 0 + 글 되풀이) · **0.2** 인치 / 2.5 미터 · **2**(0 없음 · 1 동적 자동 · 2 동적 수동) | 글 고치기 꼴 · 글 높이 · 단 | 새 | [E1-45] [E1-46] [E1-47] | +| HPDLGMODE · PDMODE | — | **2**(0 대화상자 없음 · 1 늘 · 2 리본 켜짐이면 없음) · **0**(1 안 보임) | HATCH 대화상자 · DIVIDE · MEASURE 점 모양 | 새 | [E1-48] [E1-49] | +| OFFSETDIST · OFFSETGAPTYPE | — | **-1**(< 0 = Through 기본 · 저장 안 됨) · **0** 연장(1 모깎기 · 2 모따기) | 띄우기 거리 기본 · 폴리선 모서리 틈 | 새 | [E2-48] [E2-49] | +| FILLETRAD · FILLETPOLYARC · TRIMMODE | — | **0**(도면) · **1** · **1**(0 = 안 자름) | 모깎기 반지름 · 호 선분도 · 모깎기 · 모따기 때 자름 | 새 | [E2-50] [E2-51] [E2-52] | +| CHAMMODE · CHAMFERA~D | — | **0** 두 거리(1 길이 + 각) · **0** | 모따기 방식 · 거리1 · 거리2 · 길이 · 각 | 새 | [E2-53] [E2-54~57] | +| TRIMEXTENDMODE · TRIMEDGES | — | **1** 빠른(0 표준) · **1** | TRIM · EXTEND 방식 · 빠른 방식 해치 = 가장자리만 | 새 | [E2-58] [E2-59] | +| EDGEMODE · PROJMODE | — | **0**(1 = 경계 연장선까지) · **1**(0 3D · 1 UCS · 2 화면) | 자르기 · 늘이기 경계 · 투영 | 새 | [E2-60] [E2-61] | +| COPYMODE · MIRRTEXT · DELOBJ | — | **0** 되풀이(1 한 번) · **0**(1 글자 뒤집음) · **3** | COPY 되풀이 · 대칭 글자 · 원본 남김/지움 | 새 | [E2-62] [E2-63] [E2-64] | +| ARRAYASSOCIATIVITY · ARRAYTYPE | — | **1** · **0** 사각(1 경로 · 2 원형) | 배열 한 덩어리 · 기본 배열 | 새 | [E2-65] [E2-66] | +| DIMLAYER · DIMCONTINUEMODE · DIMDLI | — | **use current** · **1** · **3.75**(미터) | 새 치수 레이어 · 이어/기준 치수가 앞 치수 스타일 · 레이어 따름 · 기준선 줄 간격 | 새 | [E2-67] [E2-68] [E2-69] | + +## 확인 못 함 · 엇갈림 + +- Ctrl+Z · Ctrl+Y 를 명령 중에 누를 때 동작 — 공식 쪽에 없음(명령 안 되돌림은 각 명령 `U` 옵션만 확인). +- Ctrl+오른 단추 — 공식 쪽은 Shift+오른 단추만 적음. +- `<<` · `<<<` 각 덮어쓰기 — 공식 쪽에서 못 찾음. +- 그립 모드 프롬프트 원문(`** STRETCH **` 줄 · `U` · `X` 옵션) — 공식 쪽은 모드 돌림 · `C` · `B` 만. +- SELECTIONCYCLING 기본값 엇갈림 — 2026 변수 쪽 0 · 2026 DYK 표 -2. +- 별칭 — autodesk.com 단축키 안내서 · 블로그 별칭 표 403 · D 표 별칭은 검색 엔진 요약으로만 봄(DYK 쪽 직접 확인 = `C` `E` `LA` `A`) · `POL` `SPL` `XL` `S` `DI` `DT` `DCO` `DBA` `MEA` `PE` `X` `MA` 못 굳힘. +- 명령 프롬프트 원문 — 2026 명령 도움말엔 괄호 줄이 거의 없음 → 같은 GUID 2015 · 2016 Mac 판 원문(COPY 만 2026 원문) · Windows 2026 화면 글자와 같은지 실물 확인 못 함 · SPLINE 첫 줄 Knots · Degree 뒤바뀜 · SPLDEGREE 범위 1~5 대 1~10 엇갈림. +- 하위 프롬프트 원문 못 찾음 — E 표에 「원문 못 찾음 · 원문 없음 · 키 원문 없음」 으로 적은 곳(CIRCLE 반지름 · RECTANG 맞은 구석 · SPLINE 다음 점 · HATCH · TRIM · EXTEND 빠른 · DIM · BREAKATPOINT · 참조 하위 · 2026 배열 키 등). +- 단계마다 Esc 동작 — MTEXT 편집기(저장 없이 끝)만 확인. + +## 출처 + +주소 = `https://help.autodesk.com/cloudhelp/20{판}/ENU/{책}/files/GUID-{GUID}.htm` · 책 C AutoCAD-Core · L AutoCAD-LT · LM AutoCAD-LT-MAC · MC AutoCAD-MAC-Core · CU AutoCAD-Customization · LCU AutoCAD-LT-Customization · DYK AutoCAD-DidYouKnow · LDYK AutoCAD-LT-DidYouKnow · GS AutoCAD-GettingStarted · WN AutoCAD-WhatsNew · 전체 주소 = `req227/K1/K1_출처URL.md` · 모두 실제로 연 쪽(K65 만 403). + +- [K01] Coordinate Entry · 27 L · 0A0135DB-3216-482B-81DD-74E6DB8CA3E3 +- [K02] To Draw Lines · 27 L · 57CDDB6C-B12B-46CE-B9C5-22EFC17258FF +- [K03] Dynamic Input Tooltips · 27 L · 3EBD4C17-F0A5-49FA-B131-4AABE2E727DB +- [K04] 2D Polar Coordinates · 18 L · 58C1D97C-A9B3-4C3C-AC1B-95BE3DF2EDB9 +- [K05] 2D Cartesian Coordinates · 27 L · F64F8008-E1C0-49CC-A268-A6B8C6E9B566 +- [K06] Direct Distance Entry · 27 L · BF4A06D8-2D66-427D-9460-B62A479B22B4 +- [K07] Pointer Input 좌표 넣기 · 27 L · A265C30C-CFD8-405D-BEE2-257B379475E2 +- [K08] DI 상대 · 절대 · WCS · 27 L · AE225073-2ECD-41CC-9B8A-61743E8CFBA9 +- [K09] Pointer Input 오타 고침 · 27 L · EF6F0331-A432-4851-8F12-451A3815BAB1 +- [K10] DI 명령 · 변수 · 27 L · 0AA6E640-039A-4CAE-A161-64A5574E8AAB +- [K11] DI 좌표 입력(DYK) · 24 DYK · 683349C0-E5C2-4E16-8846-5523E71172A9 +- [K12] Pointer Input Settings · 23 C · F18D7834-44D2-4301-A5BC-768594D6C30F +- [K13] User Preferences 탭 · 26 L · 2E780292-7D34-4A34-8CCC-82363D4F9092 +- [K14] ANGBASE · 25 L · B7CAB5F3-16BC-4E06-97BC-AAAEC052727E +- [K15] ANGDIR · 25 L · A432574D-35B6-4D9E-8D8F-4259F2066234 +- [K16] Command Window 입력 · 26 L · BB0C3E79-66AF-4557-9140-D31B4CF3C9CF +- [K17] Right-Click Customization · 27 L · 7C6AE349-DDF0-40DF-9D1E-FA9702BE88BB +- [K18] Selection 탭 · 26 L · DD2A36B6-1196-4205-9A07-32056292F0B5 +- [K19] 임시 우선 키 표 · 25 LCU · B4AC255C-DF3E-44BB-9235-6E91D53CBA90 +- [K20] 기본 단축키 표 · 25 CU · B6A9C126-230D-47F5-8973-464BC80E3736 +- [K21] 임시 우선 키 개요 · 26 CU · 0DE1073F-6635-4579-8CF4-A46FF13FB29F +- [K22] 줌 · 이동(DYK) · 23 DYK · 10D3C9A9-22E8-4047-AED1-5E96C6C28866 +- [K23] Pointing Devices · 26 CU · B1CDEE41-F8BF-48A4-A895-273DBB295378 +- [K25] 우클릭 동작 바꾸기 · 26 C · 2BD818D6-BFA6-4560-A754-17483D5F33C5 +- [K26] Shortcut Menus · 19 L · 8AE6163C-BBB0-4862-96F1-EB49923E317D +- [K27] Editing with Grips · 26 C · 01DE459C-21A7-4E92-A1D6-E2C36CD89F0C +- [K28] 그립으로 고치기 · 26 C · 484774B4-8F67-4E19-8647-83A7D314782B +- [K29] Just Grip It(DYK) · 26 LDYK · BBEA1F71-EB16-4D49-80D9-970A6909F508 +- [K36] Entering Commands · 26 C · 945C8F74-064C-4EA6-99E1-232E6003CE27 +- [K37] Basics · 26 GS · 5B6347C1-B458-4336-AB2A-C16AF161B755 +- [K38] Command Macro Strings · 23 CU · D991386C-FBAA-4094-9FCB-AADD98ACD3EF +- [K39] Correcting Mistakes · 26 C · 58926D42-665C-4F68-AC5D-B29E0A2A746F +- [K40] Command Window(DYK) · 26 LDYK · 363A3BFA-CAF2-469E-9F35-0BF64139811C +- [K42] Coordinate Filters · 26 C · 46A720EC-712F-44E3-95F7-ABBCC08F8B85 +- [K43] Object Snaps · 26 C · 76B81C1A-373E-4BCD-975A-789FB36C89FE +- [K45] OSNAP · 26 L · CF5780AD-D1AB-4526-9608-83D7952749E7 +- [K47] Double Click Actions · 24 CU · 0181E010-6BF2-4F59-8B9B-C64E10E127BA +- [K48] 별칭 · 단축키(DYK) · 25 DYK · D2B4BF16-B1F7-4EF2-89AF-88ACE85FAD73 +- [K49] 온보딩 7 · https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-OnBoarding/files/ACD_FOUNDATIONS_MAIN7.html +- [K50] 온보딩 4 · https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-OnBoarding/files/ACD_FOUNDATIONS_MAIN4.html +- [K55] 리본 키팁 · 25 C · 9404C7E3-7A99-428E-A185-9DBD3CCEE4CF +- [K57] 지우기 · 26 C · 6B6AC9D6-7F6B-4A2C-B772-04CD817F4D97 +- [K65] 블로그 별칭 표(403 · 검색 요약만) · https://www.autodesk.com/blogs/autocad/autocad-command-aliases/ +- 26 C 묶음 · [K24] SHORTCUTMENU 27079CDD-85E0-4883-A961-82C8FC5C7F46 · [K30] SELECTIONCYCLING 08FA6975-49A7-4029-A3F3-ABDA3B2A7122 · [K31] OSMODE DD9B3216-A533-4D47-95D8-7585F738FD75 · [K32] AUTOSNAP BE7947EB-2A08-4406-A169-4C5E125B1F4D +- 26 C 묶음 · [K33] INPUTHISTORYMODE 1906269A-8E27-4E0C-A61A-69A2C2CD8733 · [K34] PICKDRAG 46CBF4EF-E210-447D-AE6E-793249551640 · [K35] PICKAUTO 7BAAA374-8409-4296-B520-C5355941C836 · [K41] FROM 0EB8FEA1-4C14-40C6-8254-DB6693DB481C +- 26 C 묶음 · [K44] TRACKING C689F165-7C9E-4F09-96C3-BCA91DC4297C · [K46] MTP 04C9F013-46A3-430E-A085-BF254EA6B1D6 · [K51] LASTPOINT 4EE5F6EA-B8D1-4DD0-8D5D-B7FA2AD3A3D0 · [K52] POLARMODE D91628CC-9975-4DBF-8D02-10B23A6F3ED5 +- 26 C 묶음 · [K53] OSOPTIONS 0D05BECE-0DC3-454D-999A-208C6DBC3C3E · [K54] UNITMODE C52134E8-10EB-4AE7-A0C0-8F798C68F823 · [K56] AUNITS C7C0F6A5-7982-43DB-97F9-5B9B0044E9FA · [K58] SHORTCUTMENUDURATION 1F0CD64B-A9EC-437A-9EC3-E475D7A83DBA +- 26 C 묶음 · [K59] ZOOMFACTOR 6A77AD55-6035-42FF-8FB1-FB0D8EFE1278 · [K60] MBUTTONPAN 07122CB7-EC98-4D5F-B3BE-16EA6FB54CED · [K61] PICKBOX 363698CF-C3DC-4770-81EF-CB09D86B3D3A · [K62] PICKADD 47C2A568-30EE-4F07-916F-884CDE25CBCA +- 26 C 묶음 · [K63] DBLCLKEDIT 0A1763AF-D682-4776-99BA-E64BC2B0D286 · [K64] TEMPOVERRIDES 77C5E101-5641-4630-AB50-6D97A6FD8D24 · [K66] APERTURE 2AA8CF8E-267F-4BD6-80CD-0E7C48B8A9F1 · [K67] ORTHOMODE CF142B68-675B-452F-B3A8-7831DDB71BD0 +- 26 C 묶음 · [K68] POLARANG 0CF67F9E-F953-43D6-9227-0D56E0E693ED · [K69] PICKFIRST 258E01D5-83E8-40EB-9662-78C626A60844 · [K70] PICKSTYLE 0F1D2321-E474-48AA-9179-98F17CCCA239 · [K71] SELECTIONPREVIEW 77B3B59F-95E5-4F8A-85E2-D9411D712AF0 +- 26 C 묶음 · [K72] COMMANDPREVIEW 2B245436-029A-4E14-92C2-0ED4F4F1362E +- [E1-01 · 02] LINE · 26 C · 15 LM · E8C1190C-A26C-484C-ADDD-DDF81666F69F +- [E1-03 · 04] PLINE · 26 C · 15 LM · 11883C70-6435-4F80-8FB4-F6E933B8FD94 +- [E1-05] To Draw Polylines · 26 C · ACC319C8-34EA-4D9D-981E-B69B7B187400 +- [E1-06] RECTANG · 26 C · 188B2DDA-6CD8-4D37-BF26-E6CF27C34C75 +- [E1-07] To Draw a Rectangle · 26 C · 377C261F-EEF1-4438-A80D-6760CAE37B81 +- [E1-08 · 09] CIRCLE · 26 C · 15 LM · C60B6D5D-AAEB-420F-917F-6E6B47E92F48 +- [E1-10 · 11] ARC · 26 C · 15 LM · 30ECFD30-A1D6-4D60-9DD1-B487603F6772 +- [E1-12 · 13] POLYGON · 26 C · 16 LM · E5CD464D-C0DC-4464-BFDF-50C4ABEC8B91 +- [E1-14 · 15] ELLIPSE · 26 C · 15 LM · 45F9C588-2BA9-414D-8AFD-FB6B448BF273 +- [E1-16 · 17] SPLINE · 26 C · 15 LM · 5E7D51E2-1595-4E0C-85F8-2D7CBD166A08 +- [E1-18 · 19] XLINE · 26 C · 15 LM · 40650DCE-E8CA-483C-8E25-7FA9AB6992C1 +- [E1-20] TEXT · 26 C · D1C664DD-63D9-467E-8EC1-2F5A1777A924 +- [E1-21] TEXT(Mac 2026 쪽) · 26 MC · C2B46B5D-6C17-4DD3-9AA1-ADB924CAEBFF +- [E1-22] 글 편집기 쪽(2025) · 25 C · 8739509C-15A7-4582-BF7C-3D83ED2A23B5 +- [E1-23] MTEXT · 26 C · E6BCE05D-B9E3-4875-BBBC-29134EA6FD51 +- [E1-24 · 25] MTEXT 옵션 쪽 · 26 C · 15 LM · F94BE932-DA31-437E-9610-27F46ACD5711 +- [E1-26] HATCH · 26 C · 27C104F2-B687-4025-B50B-A58E37329832 +- [E1-27 · 28] -HATCH · 26 C · 26 MC · 410ECEBF-7CC2-4000-A45E-18F1F6BEE423 +- [E1-29 · 30] DIST · 26 C · 15 LM · A72EADC7-EF79-463C-B4F1-862C6139B004 +- [E1-31 · 32] MEASUREGEOM · 26 C · 15 LM · 5D5B0EE1-DD90-47AE-8A55-642FBFF5E4E4 +- [E1-33 · 34] DIVIDE · 26 C · 15 LM · 3427CA9F-831C-4B53-A903-49EB6378335C +- [E1-35 · 36] MEASURE · 26 C · 15 LM · 212933E8-BC53-4872-A3DC-32C48DE1B2D0 +- 26 C 묶음 · [E1-37] CIRCLERAD 4C17F0AB-E726-4242-8060-0577C8F657BF · [E1-38] POLYSIDES D848B99F-62E9-4605-BA87-18606BE36F0C · [E1-39] PLINEWID 35EEB892-15C9-442F-847D-2F4DC52E9690 · [E1-40] SPLMETHOD 300EB1EF-E101-421C-A8B8-6D91E2873787 +- 26 C 묶음 · [E1-41] SPLKNOTS 134FD0A1-4396-4748-9FF4-C82B7BC24E28 · [E1-42] SPLDEGREE 40B00336-C1EB-47A4-8773-FFB22B8C931C · [E1-43] PELLIPSE A1C64346-BBFC-4FE9-B346-BCD121BEFA0A · [E1-44] LASTANGLE FBD5ACF2-BEB9-4159-8F76-AE464DE2DF6F +- 26 C 묶음 · [E1-45] TEXTED CFBB7AF6-884C-4CC8-93E3-A0CE29B4B1C3 · [E1-46] TEXTSIZE DD548317-95BA-4AEA-B54D-0D917F10D3C1 · [E1-47] MTEXTCOLUMN 188C7F13-7046-436E-9F4E-6E05984456E8 · [E1-48] HPDLGMODE 8C19FED5-6024-40A8-AEBD-2AA1C57240B3 +- 26 C 묶음 · [E1-49] PDMODE 82F9BB52-D026-4D6A-ABA6-BF29641F459B +- [E2-01 · 02] OFFSET · 26 C · 16 MC · C0E4246D-C420-42BD-A6FC-8B1852EFD005 +- [E2-03] OFFSET 하기 쪽 · 26 C · F6B26611-5D88-41DD-9BC2-DFB739528BD8 +- [E2-04] FILLET · 26 C · 64F8B700-23B3-4BD6-8C03-66121AA13E8F +- [E2-05] FILLET(2016 Mac) · 16 MC · C5530CBC-0402-439E-842D-AF5795B903C2 +- [E2-06 · 07] CHAMFER · 26 C · 16 MC · B1DCF991-90A7-4DB0-96FC-BDA3FB76337C +- [E2-08 · 09] TRIM · 26 C · 16 MC · B1A185EF-07C6-4C53-A76F-05ADE11F5C32 +- [E2-10 · 11] EXTEND · 26 C · 16 MC · 89DD7B0F-F4F1-410D-9A3A-5847CA5F8744 +- [E2-13 · 14] MOVE · 26 C · 16 MC · 47CE7325-84C0-4414-80A3-29DC98392709 +- [E2-15] COPY · 26 C · 1CF9287F-06E8-4D03-8377-2E130862FE02 +- [E2-16 · 17] ROTATE · 26 C · 16 MC · 1C265537-FBAC-48D5-B448-B72E777071E5 +- [E2-18] 참조로 돌리기 · 크기(DYK) · 26 LDYK · D988658E-0A92-4353-B614-6C30CF6281A2 +- [E2-19 · 20] SCALE · 26 C · 16 MC · D4E17E51-5000-4AB6-8D6A-6D2AB4863C75 +- [E2-21 · 22] STRETCH · 26 C · 16 MC · F000A502-D39E-4D31-A8E2-4A626473FB72 +- [E2-23 · 24] MIRROR · 26 C · 16 MC · 595277C8-9B87-4CFB-A3AF-769537A22F3D +- [E2-25 · 26] ARRAY · 26 C · 16 MC · E23F6125-E5E9-4645-9615-23717902C33B +- [E2-27 · 28] ARRAYRECT · 26 C · 16 MC · BB3DA888-0C3A-4C68-A3ED-E0E528781205 +- [E2-29 · 30] ARRAYPOLAR · 26 C · 16 MC · A6E74297-2CB3-4B1C-A07B-69CD08630052 +- [E2-31 · 32] ARRAYPATH · 26 C · 16 MC · D36C46CD-4E17-4A16-A387-C0B158EA5A9E +- [E2-33 · 34] BREAK · 26 C · 16 MC · 36A1CDE0-3871-4B25-AC98-93235FA83863 +- [E2-35] BREAKATPOINT · 26 C · E0439DE0-B2C3-4233-BB4D-5A574A00694B +- [E2-36] What's New 2021(BREAKATPOINT) · 21 WN · 9D9173FD-BD4D-4D93-A649-3DAEC9C2A3AC +- [E2-37] JOIN · 26 C · 4D2A28C3-3E7F-4830-BE6A-7C9907C95488 +- [E2-38] JOIN(DYK) · 26 LDYK · 29D46150-7550-420A-B151-73BE44146665 +- [E2-39 · 40] LENGTHEN · 26 C · 16 MC · 76C74C62-DBAA-41E7-8422-F0EE7E769638 +- [E2-41] DIM · 26 C · 45C1A271-9650-4927-858F-B3BDB19B3E6C +- [E2-42] DIMLINEAR · 16 MC · E908848F-4621-43D2-B377-FD3FBC2B8848 +- [E2-43] DIMALIGNED · 16 MC · D7C05A85-8985-4576-B22C-5A795DD5B179 +- [E2-44 · 45] DIMANGULAR · 26 C · 16 MC · 6AB63ECB-BBCB-4D0F-B7BE-1674349A1FF9 +- [E2-46] DIMRADIUS · 26 C · 99EED401-D7DF-4CF3-95ED-BBD107A13855 +- [E2-47] DIMDIAMETER · 26 C · 2E64616D-5C96-46A6-BDA2-9782F9036BE3 +- 26 C 묶음 · [E2-48] OFFSETDIST 4296AA53-76EF-4BDA-8B73-1AF08EA0B523 · [E2-49] OFFSETGAPTYPE EB2A74E1-E6C8-4690-9658-7F731AB7FD84 · [E2-50] FILLETRAD BA2DD5FF-B663-48DB-95A8-6DEAFCEDDBF5 · [E2-51] FILLETPOLYARC DD802826-BEE4-4B85-9B76-E9A3FBD02E24 +- 26 C 묶음 · [E2-52] TRIMMODE 3D639DCE-5A01-4C07-899D-0595F8DDEBF7 · [E2-53] CHAMMODE 0FFBF218-FED2-485D-A332-19F406228123 · [E2-54] CHAMFERA E2E34687-6BFF-481A-9732-885651AA44CF · [E2-55] CHAMFERB 53D438C7-3179-4971-BC39-E0F878FD18CF +- 26 C 묶음 · [E2-56] CHAMFERC FFFD13C0-3C7F-4865-B783-46E3BC652AC9 · [E2-57] CHAMFERD 19EB0506-005C-491A-B2E6-AF12890A3786 · [E2-58] TRIMEXTENDMODE 52B3803E-FADC-4D7B-9BF2-A176C7FB7535 · [E2-59] TRIMEDGES 6C0E9377-9DA8-4681-A880-FCCB1EF96E14 +- 26 C 묶음 · [E2-60] EDGEMODE ED7D2A22-0C65-49C3-A58E-D10A3BECBE57 · [E2-61] PROJMODE 8735A062-2688-44A7-809A-9883761533F9 · [E2-62] COPYMODE BB3CED8E-4FA7-492A-8229-77A9021F67D7 · [E2-63] MIRRTEXT D55D2D18-B26E-4525-B5E4-03A2A07FB5F6 +- 26 C 묶음 · [E2-64] DELOBJ CB64587F-611E-441E-AB07-14B415BF535F · [E2-65] ARRAYASSOCIATIVITY 298A0544-E6C3-496F-9FA1-12AEC2FF5647 · [E2-66] ARRAYTYPE ED05306A-0D24-4FE7-9B50-1207BCA2B4B8 · [E2-67] DIMLAYER 88E56602-EADC-4C68-93B4-93E7623BEB6B +- 26 C 묶음 · [E2-68] DIMCONTINUEMODE 2424CA1C-B005-449B-A61A-C4F2D38C4E66 · [E2-69] DIMDLI D2D1AF3D-004B-4C47-BABE-22B7327EE128 + +### 부록 3 덧. K1 출처 URL — `K1/K1_출처URL.md` + +# K1 출처 전체 주소 + +- [K01] Coordinate Entry — https://help.autodesk.com/cloudhelp/2027/ENU/AutoCAD-LT/files/GUID-0A0135DB-3216-482B-81DD-74E6DB8CA3E3.htm +- [K02] To Draw Lines — https://help.autodesk.com/cloudhelp/2027/ENU/AutoCAD-LT/files/GUID-57CDDB6C-B12B-46CE-B9C5-22EFC17258FF.htm +- [K03] Dynamic Input Tooltips — https://help.autodesk.com/cloudhelp/2027/ENU/AutoCAD-LT/files/GUID-3EBD4C17-F0A5-49FA-B131-4AABE2E727DB.htm +- [K04] 2D Polar Coordinates — https://help.autodesk.com/cloudhelp/2018/ENU/AutoCAD-LT/files/GUID-58C1D97C-A9B3-4C3C-AC1B-95BE3DF2EDB9.htm +- [K05] 2D Cartesian Coordinates — https://help.autodesk.com/cloudhelp/2027/ENU/AutoCAD-LT/files/GUID-F64F8008-E1C0-49CC-A268-A6B8C6E9B566.htm +- [K06] Direct Distance Entry — https://help.autodesk.com/cloudhelp/2027/ENU/AutoCAD-LT/files/GUID-BF4A06D8-2D66-427D-9460-B62A479B22B4.htm +- [K07] Pointer Input 좌표 넣기 — https://help.autodesk.com/cloudhelp/2027/ENU/AutoCAD-LT/files/GUID-A265C30C-CFD8-405D-BEE2-257B379475E2.htm +- [K08] DI 상대 · 절대 · WCS — https://help.autodesk.com/cloudhelp/2027/ENU/AutoCAD-LT/files/GUID-AE225073-2ECD-41CC-9B8A-61743E8CFBA9.htm +- [K09] Pointer Input 오타 고침 — https://help.autodesk.com/cloudhelp/2027/ENU/AutoCAD-LT/files/GUID-EF6F0331-A432-4851-8F12-451A3815BAB1.htm +- [K10] DI 명령 · 변수 — https://help.autodesk.com/cloudhelp/2027/ENU/AutoCAD-LT/files/GUID-0AA6E640-039A-4CAE-A161-64A5574E8AAB.htm +- [K11] DI 좌표 입력(DYK) — https://help.autodesk.com/cloudhelp/2024/ENU/AutoCAD-DidYouKnow/files/GUID-683349C0-E5C2-4E16-8846-5523E71172A9.htm +- [K12] Pointer Input Settings — https://help.autodesk.com/cloudhelp/2023/ENU/AutoCAD-Core/files/GUID-F18D7834-44D2-4301-A5BC-768594D6C30F.htm +- [K13] User Preferences 탭 — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-LT/files/GUID-2E780292-7D34-4A34-8CCC-82363D4F9092.htm +- [K14] ANGBASE — https://help.autodesk.com/cloudhelp/2025/ENU/AutoCAD-LT/files/GUID-B7CAB5F3-16BC-4E06-97BC-AAAEC052727E.htm +- [K15] ANGDIR — https://help.autodesk.com/cloudhelp/2025/ENU/AutoCAD-LT/files/GUID-A432574D-35B6-4D9E-8D8F-4259F2066234.htm +- [K16] Command Window 입력 — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-LT/files/GUID-BB0C3E79-66AF-4557-9140-D31B4CF3C9CF.htm +- [K17] Right-Click Customization — https://help.autodesk.com/cloudhelp/2027/ENU/AutoCAD-LT/files/GUID-7C6AE349-DDF0-40DF-9D1E-FA9702BE88BB.htm +- [K18] Selection 탭 — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-LT/files/GUID-DD2A36B6-1196-4205-9A07-32056292F0B5.htm +- [K19] 임시 우선 키 표 — https://help.autodesk.com/cloudhelp/2025/ENU/AutoCAD-LT-Customization/files/GUID-B4AC255C-DF3E-44BB-9235-6E91D53CBA90.htm +- [K20] 기본 단축키 표 — https://help.autodesk.com/cloudhelp/2025/ENU/AutoCAD-Customization/files/GUID-B6A9C126-230D-47F5-8973-464BC80E3736.htm +- [K21] 임시 우선 키 개요 — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Customization/files/GUID-0DE1073F-6635-4579-8CF4-A46FF13FB29F.htm +- [K22] 줌 · 이동(DYK) — https://help.autodesk.com/cloudhelp/2023/ENU/AutoCAD-DidYouKnow/files/GUID-10D3C9A9-22E8-4047-AED1-5E96C6C28866.htm +- [K23] Pointing Devices — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Customization/files/GUID-B1CDEE41-F8BF-48A4-A895-273DBB295378.htm +- [K24] SHORTCUTMENU — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-27079CDD-85E0-4883-A961-82C8FC5C7F46.htm +- [K25] 우클릭 동작 바꾸기 — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-2BD818D6-BFA6-4560-A754-17483D5F33C5.htm +- [K26] Shortcut Menus — https://help.autodesk.com/cloudhelp/2019/ENU/AutoCAD-LT/files/GUID-8AE6163C-BBB0-4862-96F1-EB49923E317D.htm +- [K27] Editing with Grips — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-01DE459C-21A7-4E92-A1D6-E2C36CD89F0C.htm +- [K28] 그립으로 고치기 — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-484774B4-8F67-4E19-8647-83A7D314782B.htm +- [K29] Just Grip It(DYK) — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-LT-DidYouKnow/files/GUID-BBEA1F71-EB16-4D49-80D9-970A6909F508.htm +- [K30] SELECTIONCYCLING — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-08FA6975-49A7-4029-A3F3-ABDA3B2A7122.htm +- [K31] OSMODE — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-DD9B3216-A533-4D47-95D8-7585F738FD75.htm +- [K32] AUTOSNAP — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-BE7947EB-2A08-4406-A169-4C5E125B1F4D.htm +- [K33] INPUTHISTORYMODE — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-1906269A-8E27-4E0C-A61A-69A2C2CD8733.htm +- [K34] PICKDRAG — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-46CBF4EF-E210-447D-AE6E-793249551640.htm +- [K35] PICKAUTO — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-7BAAA374-8409-4296-B520-C5355941C836.htm +- [K36] Entering Commands — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-945C8F74-064C-4EA6-99E1-232E6003CE27.htm +- [K37] Basics — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-GettingStarted/files/GUID-5B6347C1-B458-4336-AB2A-C16AF161B755.htm +- [K38] Command Macro Strings — https://help.autodesk.com/cloudhelp/2023/ENU/AutoCAD-Customization/files/GUID-D991386C-FBAA-4094-9FCB-AADD98ACD3EF.htm +- [K39] Correcting Mistakes — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-58926D42-665C-4F68-AC5D-B29E0A2A746F.htm +- [K40] Command Window(DYK) — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-LT-DidYouKnow/files/GUID-363A3BFA-CAF2-469E-9F35-0BF64139811C.htm +- [K41] FROM — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-0EB8FEA1-4C14-40C6-8254-DB6693DB481C.htm +- [K42] Coordinate Filters — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-46A720EC-712F-44E3-95F7-ABBCC08F8B85.htm +- [K43] Object Snaps — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-76B81C1A-373E-4BCD-975A-789FB36C89FE.htm +- [K44] TRACKING — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-C689F165-7C9E-4F09-96C3-BCA91DC4297C.htm +- [K45] OSNAP — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-LT/files/GUID-CF5780AD-D1AB-4526-9608-83D7952749E7.htm +- [K46] MTP — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-04C9F013-46A3-430E-A085-BF254EA6B1D6.htm +- [K47] Double Click Actions — https://help.autodesk.com/cloudhelp/2024/ENU/AutoCAD-Customization/files/GUID-0181E010-6BF2-4F59-8B9B-C64E10E127BA.htm +- [K48] 별칭 · 단축키(DYK) — https://help.autodesk.com/cloudhelp/2025/ENU/AutoCAD-DidYouKnow/files/GUID-D2B4BF16-B1F7-4EF2-89AF-88ACE85FAD73.htm +- [K49] 온보딩 7 — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-OnBoarding/files/ACD_FOUNDATIONS_MAIN7.html +- [K50] 온보딩 4 — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-OnBoarding/files/ACD_FOUNDATIONS_MAIN4.html +- [K51] LASTPOINT — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-4EE5F6EA-B8D1-4DD0-8D5D-B7FA2AD3A3D0.htm +- [K52] POLARMODE — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-D91628CC-9975-4DBF-8D02-10B23A6F3ED5.htm +- [K53] OSOPTIONS — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-0D05BECE-0DC3-454D-999A-208C6DBC3C3E.htm +- [K54] UNITMODE — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-C52134E8-10EB-4AE7-A0C0-8F798C68F823.htm +- [K55] 리본 키팁 — https://help.autodesk.com/cloudhelp/2025/ENU/AutoCAD-Core/files/GUID-9404C7E3-7A99-428E-A185-9DBD3CCEE4CF.htm +- [K56] AUNITS — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-C7C0F6A5-7982-43DB-97F9-5B9B0044E9FA.htm +- [K57] 지우기 — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-6B6AC9D6-7F6B-4A2C-B772-04CD817F4D97.htm +- [K58] SHORTCUTMENUDURATION — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-1F0CD64B-A9EC-437A-9EC3-E475D7A83DBA.htm +- [K59] ZOOMFACTOR — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-6A77AD55-6035-42FF-8FB1-FB0D8EFE1278.htm +- [K60] MBUTTONPAN — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-07122CB7-EC98-4D5F-B3BE-16EA6FB54CED.htm +- [K61] PICKBOX — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-363698CF-C3DC-4770-81EF-CB09D86B3D3A.htm +- [K62] PICKADD — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-47C2A568-30EE-4F07-916F-884CDE25CBCA.htm +- [K63] DBLCLKEDIT — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-0A1763AF-D682-4776-99BA-E64BC2B0D286.htm +- [K64] TEMPOVERRIDES — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-77C5E101-5641-4630-AB50-6D97A6FD8D24.htm +- [K65] 블로그 별칭 표(403 · 검색 요약만) — https://www.autodesk.com/blogs/autocad/autocad-command-aliases/ +- [K66] APERTURE — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-2AA8CF8E-267F-4BD6-80CD-0E7C48B8A9F1.htm +- [K67] ORTHOMODE — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-CF142B68-675B-452F-B3A8-7831DDB71BD0.htm +- [K68] POLARANG — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-0CF67F9E-F953-43D6-9227-0D56E0E693ED.htm +- [K69] PICKFIRST — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-258E01D5-83E8-40EB-9662-78C626A60844.htm +- [K70] PICKSTYLE — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-0F1D2321-E474-48AA-9179-98F17CCCA239.htm +- [K71] SELECTIONPREVIEW — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-77B3B59F-95E5-4F8A-85E2-D9411D712AF0.htm +- [K72] COMMANDPREVIEW — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-2B245436-029A-4E14-92C2-0ED4F4F1362E.htm +- [E1-01] LINE — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-E8C1190C-A26C-484C-ADDD-DDF81666F69F.htm +- [E1-02] — https://help.autodesk.com/cloudhelp/2015/ENU/AutoCAD-LT-MAC/files/GUID-E8C1190C-A26C-484C-ADDD-DDF81666F69F.htm +- [E1-03] PLINE — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-11883C70-6435-4F80-8FB4-F6E933B8FD94.htm +- [E1-04] — https://help.autodesk.com/cloudhelp/2015/ENU/AutoCAD-LT-MAC/files/GUID-11883C70-6435-4F80-8FB4-F6E933B8FD94.htm +- [E1-05] To Draw Polylines — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-ACC319C8-34EA-4D9D-981E-B69B7B187400.htm +- [E1-06] RECTANG — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-188B2DDA-6CD8-4D37-BF26-E6CF27C34C75.htm +- [E1-07] To Draw a Rectangle — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-377C261F-EEF1-4438-A80D-6760CAE37B81.htm +- [E1-08] CIRCLE — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-C60B6D5D-AAEB-420F-917F-6E6B47E92F48.htm +- [E1-09] — https://help.autodesk.com/cloudhelp/2015/ENU/AutoCAD-LT-MAC/files/GUID-C60B6D5D-AAEB-420F-917F-6E6B47E92F48.htm +- [E1-10] ARC — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-30ECFD30-A1D6-4D60-9DD1-B487603F6772.htm +- [E1-11] — https://help.autodesk.com/cloudhelp/2015/ENU/AutoCAD-LT-MAC/files/GUID-30ECFD30-A1D6-4D60-9DD1-B487603F6772.htm +- [E1-12] POLYGON — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-E5CD464D-C0DC-4464-BFDF-50C4ABEC8B91.htm +- [E1-13] — https://help.autodesk.com/cloudhelp/2016/ENU/AutoCAD-LT-MAC/files/GUID-E5CD464D-C0DC-4464-BFDF-50C4ABEC8B91.htm +- [E1-14] ELLIPSE — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-45F9C588-2BA9-414D-8AFD-FB6B448BF273.htm +- [E1-15] — https://help.autodesk.com/cloudhelp/2015/ENU/AutoCAD-LT-MAC/files/GUID-45F9C588-2BA9-414D-8AFD-FB6B448BF273.htm +- [E1-16] SPLINE — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-5E7D51E2-1595-4E0C-85F8-2D7CBD166A08.htm +- [E1-17] — https://help.autodesk.com/cloudhelp/2015/ENU/AutoCAD-LT-MAC/files/GUID-5E7D51E2-1595-4E0C-85F8-2D7CBD166A08.htm +- [E1-18] XLINE — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-40650DCE-E8CA-483C-8E25-7FA9AB6992C1.htm +- [E1-19] — https://help.autodesk.com/cloudhelp/2015/ENU/AutoCAD-LT-MAC/files/GUID-40650DCE-E8CA-483C-8E25-7FA9AB6992C1.htm +- [E1-20] TEXT — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-D1C664DD-63D9-467E-8EC1-2F5A1777A924.htm +- [E1-21] TEXT(Mac 2026 쪽) — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-MAC-Core/files/GUID-C2B46B5D-6C17-4DD3-9AA1-ADB924CAEBFF.htm +- [E1-22] 글 편집기 쪽(2025) — https://help.autodesk.com/cloudhelp/2025/ENU/AutoCAD-Core/files/GUID-8739509C-15A7-4582-BF7C-3D83ED2A23B5.htm +- [E1-23] MTEXT — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-E6BCE05D-B9E3-4875-BBBC-29134EA6FD51.htm +- [E1-24] MTEXT 옵션 쪽 — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-F94BE932-DA31-437E-9610-27F46ACD5711.htm +- [E1-25] — https://help.autodesk.com/cloudhelp/2015/ENU/AutoCAD-LT-MAC/files/GUID-F94BE932-DA31-437E-9610-27F46ACD5711.htm +- [E1-26] HATCH — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-27C104F2-B687-4025-B50B-A58E37329832.htm +- [E1-27] -HATCH — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-410ECEBF-7CC2-4000-A45E-18F1F6BEE423.htm +- [E1-28] — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-MAC-Core/files/GUID-410ECEBF-7CC2-4000-A45E-18F1F6BEE423.htm +- [E1-29] DIST — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-A72EADC7-EF79-463C-B4F1-862C6139B004.htm +- [E1-30] — https://help.autodesk.com/cloudhelp/2015/ENU/AutoCAD-LT-MAC/files/GUID-A72EADC7-EF79-463C-B4F1-862C6139B004.htm +- [E1-31] MEASUREGEOM — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-5D5B0EE1-DD90-47AE-8A55-642FBFF5E4E4.htm +- [E1-32] — https://help.autodesk.com/cloudhelp/2015/ENU/AutoCAD-LT-MAC/files/GUID-5D5B0EE1-DD90-47AE-8A55-642FBFF5E4E4.htm +- [E1-33] DIVIDE — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-3427CA9F-831C-4B53-A903-49EB6378335C.htm +- [E1-34] — https://help.autodesk.com/cloudhelp/2015/ENU/AutoCAD-LT-MAC/files/GUID-3427CA9F-831C-4B53-A903-49EB6378335C.htm +- [E1-35] MEASURE — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-212933E8-BC53-4872-A3DC-32C48DE1B2D0.htm +- [E1-36] — https://help.autodesk.com/cloudhelp/2015/ENU/AutoCAD-LT-MAC/files/GUID-212933E8-BC53-4872-A3DC-32C48DE1B2D0.htm +- [E1-37] CIRCLERAD — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-4C17F0AB-E726-4242-8060-0577C8F657BF.htm +- [E1-38] POLYSIDES — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-D848B99F-62E9-4605-BA87-18606BE36F0C.htm +- [E1-39] PLINEWID — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-35EEB892-15C9-442F-847D-2F4DC52E9690.htm +- [E1-40] SPLMETHOD — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-300EB1EF-E101-421C-A8B8-6D91E2873787.htm +- [E1-41] SPLKNOTS — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-134FD0A1-4396-4748-9FF4-C82B7BC24E28.htm +- [E1-42] SPLDEGREE — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-40B00336-C1EB-47A4-8773-FFB22B8C931C.htm +- [E1-43] PELLIPSE — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-A1C64346-BBFC-4FE9-B346-BCD121BEFA0A.htm +- [E1-44] LASTANGLE — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-FBD5ACF2-BEB9-4159-8F76-AE464DE2DF6F.htm +- [E1-45] TEXTED — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-CFBB7AF6-884C-4CC8-93E3-A0CE29B4B1C3.htm +- [E1-46] TEXTSIZE — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-DD548317-95BA-4AEA-B54D-0D917F10D3C1.htm +- [E1-47] MTEXTCOLUMN — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-188C7F13-7046-436E-9F4E-6E05984456E8.htm +- [E1-48] HPDLGMODE — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-8C19FED5-6024-40A8-AEBD-2AA1C57240B3.htm +- [E1-49] PDMODE — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-82F9BB52-D026-4D6A-ABA6-BF29641F459B.htm +- [E2-01] OFFSET — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-C0E4246D-C420-42BD-A6FC-8B1852EFD005.htm +- [E2-02] — https://help.autodesk.com/cloudhelp/2016/ENU/AutoCAD-MAC-Core/files/GUID-C0E4246D-C420-42BD-A6FC-8B1852EFD005.htm +- [E2-03] OFFSET 하기 쪽 — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-F6B26611-5D88-41DD-9BC2-DFB739528BD8.htm +- [E2-04] FILLET — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-64F8B700-23B3-4BD6-8C03-66121AA13E8F.htm +- [E2-05] FILLET(2016 Mac) — https://help.autodesk.com/cloudhelp/2016/ENU/AutoCAD-MAC-Core/files/GUID-C5530CBC-0402-439E-842D-AF5795B903C2.htm +- [E2-06] CHAMFER — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-B1DCF991-90A7-4DB0-96FC-BDA3FB76337C.htm +- [E2-07] — https://help.autodesk.com/cloudhelp/2016/ENU/AutoCAD-MAC-Core/files/GUID-B1DCF991-90A7-4DB0-96FC-BDA3FB76337C.htm +- [E2-08] TRIM — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-B1A185EF-07C6-4C53-A76F-05ADE11F5C32.htm +- [E2-09] — https://help.autodesk.com/cloudhelp/2016/ENU/AutoCAD-MAC-Core/files/GUID-B1A185EF-07C6-4C53-A76F-05ADE11F5C32.htm +- [E2-10] EXTEND — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-89DD7B0F-F4F1-410D-9A3A-5847CA5F8744.htm +- [E2-11] — https://help.autodesk.com/cloudhelp/2016/ENU/AutoCAD-MAC-Core/files/GUID-89DD7B0F-F4F1-410D-9A3A-5847CA5F8744.htm +- [E2-13] MOVE — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-47CE7325-84C0-4414-80A3-29DC98392709.htm +- [E2-14] — https://help.autodesk.com/cloudhelp/2016/ENU/AutoCAD-MAC-Core/files/GUID-47CE7325-84C0-4414-80A3-29DC98392709.htm +- [E2-15] COPY — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-1CF9287F-06E8-4D03-8377-2E130862FE02.htm +- [E2-16] ROTATE — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-1C265537-FBAC-48D5-B448-B72E777071E5.htm +- [E2-17] — https://help.autodesk.com/cloudhelp/2016/ENU/AutoCAD-MAC-Core/files/GUID-1C265537-FBAC-48D5-B448-B72E777071E5.htm +- [E2-18] 참조로 돌리기 · 크기(DYK) — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-LT-DidYouKnow/files/GUID-D988658E-0A92-4353-B614-6C30CF6281A2.htm +- [E2-19] SCALE — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-D4E17E51-5000-4AB6-8D6A-6D2AB4863C75.htm +- [E2-20] — https://help.autodesk.com/cloudhelp/2016/ENU/AutoCAD-MAC-Core/files/GUID-D4E17E51-5000-4AB6-8D6A-6D2AB4863C75.htm +- [E2-21] STRETCH — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-F000A502-D39E-4D31-A8E2-4A626473FB72.htm +- [E2-22] — https://help.autodesk.com/cloudhelp/2016/ENU/AutoCAD-MAC-Core/files/GUID-F000A502-D39E-4D31-A8E2-4A626473FB72.htm +- [E2-23] MIRROR — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-595277C8-9B87-4CFB-A3AF-769537A22F3D.htm +- [E2-24] — https://help.autodesk.com/cloudhelp/2016/ENU/AutoCAD-MAC-Core/files/GUID-595277C8-9B87-4CFB-A3AF-769537A22F3D.htm +- [E2-25] ARRAY — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-E23F6125-E5E9-4645-9615-23717902C33B.htm +- [E2-26] — https://help.autodesk.com/cloudhelp/2016/ENU/AutoCAD-MAC-Core/files/GUID-E23F6125-E5E9-4645-9615-23717902C33B.htm +- [E2-27] ARRAYRECT — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-BB3DA888-0C3A-4C68-A3ED-E0E528781205.htm +- [E2-28] — https://help.autodesk.com/cloudhelp/2016/ENU/AutoCAD-MAC-Core/files/GUID-BB3DA888-0C3A-4C68-A3ED-E0E528781205.htm +- [E2-29] ARRAYPOLAR — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-A6E74297-2CB3-4B1C-A07B-69CD08630052.htm +- [E2-30] — https://help.autodesk.com/cloudhelp/2016/ENU/AutoCAD-MAC-Core/files/GUID-A6E74297-2CB3-4B1C-A07B-69CD08630052.htm +- [E2-31] ARRAYPATH — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-D36C46CD-4E17-4A16-A387-C0B158EA5A9E.htm +- [E2-32] — https://help.autodesk.com/cloudhelp/2016/ENU/AutoCAD-MAC-Core/files/GUID-D36C46CD-4E17-4A16-A387-C0B158EA5A9E.htm +- [E2-33] BREAK — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-36A1CDE0-3871-4B25-AC98-93235FA83863.htm +- [E2-34] — https://help.autodesk.com/cloudhelp/2016/ENU/AutoCAD-MAC-Core/files/GUID-36A1CDE0-3871-4B25-AC98-93235FA83863.htm +- [E2-35] BREAKATPOINT — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-E0439DE0-B2C3-4233-BB4D-5A574A00694B.htm +- [E2-36] What's New 2021(BREAKATPOINT) — https://help.autodesk.com/cloudhelp/2021/ENU/AutoCAD-WhatsNew/files/GUID-9D9173FD-BD4D-4D93-A649-3DAEC9C2A3AC.htm +- [E2-37] JOIN — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-4D2A28C3-3E7F-4830-BE6A-7C9907C95488.htm +- [E2-38] JOIN(DYK) — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-LT-DidYouKnow/files/GUID-29D46150-7550-420A-B151-73BE44146665.htm +- [E2-39] LENGTHEN — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-76C74C62-DBAA-41E7-8422-F0EE7E769638.htm +- [E2-40] — https://help.autodesk.com/cloudhelp/2016/ENU/AutoCAD-MAC-Core/files/GUID-76C74C62-DBAA-41E7-8422-F0EE7E769638.htm +- [E2-41] DIM — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-45C1A271-9650-4927-858F-B3BDB19B3E6C.htm +- [E2-42] DIMLINEAR — https://help.autodesk.com/cloudhelp/2016/ENU/AutoCAD-MAC-Core/files/GUID-E908848F-4621-43D2-B377-FD3FBC2B8848.htm +- [E2-43] DIMALIGNED — https://help.autodesk.com/cloudhelp/2016/ENU/AutoCAD-MAC-Core/files/GUID-D7C05A85-8985-4576-B22C-5A795DD5B179.htm +- [E2-44] DIMANGULAR — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-6AB63ECB-BBCB-4D0F-B7BE-1674349A1FF9.htm +- [E2-45] — https://help.autodesk.com/cloudhelp/2016/ENU/AutoCAD-MAC-Core/files/GUID-6AB63ECB-BBCB-4D0F-B7BE-1674349A1FF9.htm +- [E2-46] DIMRADIUS — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-99EED401-D7DF-4CF3-95ED-BBD107A13855.htm +- [E2-47] DIMDIAMETER — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-2E64616D-5C96-46A6-BDA2-9782F9036BE3.htm +- [E2-48] OFFSETDIST — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-4296AA53-76EF-4BDA-8B73-1AF08EA0B523.htm +- [E2-49] OFFSETGAPTYPE — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-EB2A74E1-E6C8-4690-9658-7F731AB7FD84.htm +- [E2-50] FILLETRAD — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-BA2DD5FF-B663-48DB-95A8-6DEAFCEDDBF5.htm +- [E2-51] FILLETPOLYARC — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-DD802826-BEE4-4B85-9B76-E9A3FBD02E24.htm +- [E2-52] TRIMMODE — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-3D639DCE-5A01-4C07-899D-0595F8DDEBF7.htm +- [E2-53] CHAMMODE — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-0FFBF218-FED2-485D-A332-19F406228123.htm +- [E2-54] CHAMFERA — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-E2E34687-6BFF-481A-9732-885651AA44CF.htm +- [E2-55] CHAMFERB — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-53D438C7-3179-4971-BC39-E0F878FD18CF.htm +- [E2-56] CHAMFERC — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-FFFD13C0-3C7F-4865-B783-46E3BC652AC9.htm +- [E2-57] CHAMFERD — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-19EB0506-005C-491A-B2E6-AF12890A3786.htm +- [E2-58] TRIMEXTENDMODE — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-52B3803E-FADC-4D7B-9BF2-A176C7FB7535.htm +- [E2-59] TRIMEDGES — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-6C0E9377-9DA8-4681-A880-FCCB1EF96E14.htm +- [E2-60] EDGEMODE — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-ED7D2A22-0C65-49C3-A58E-D10A3BECBE57.htm +- [E2-61] PROJMODE — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-8735A062-2688-44A7-809A-9883761533F9.htm +- [E2-62] COPYMODE — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-BB3CED8E-4FA7-492A-8229-77A9021F67D7.htm +- [E2-63] MIRRTEXT — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-D55D2D18-B26E-4525-B5E4-03A2A07FB5F6.htm +- [E2-64] DELOBJ — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-CB64587F-611E-441E-AB07-14B415BF535F.htm +- [E2-65] ARRAYASSOCIATIVITY — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-298A0544-E6C3-496F-9FA1-12AEC2FF5647.htm +- [E2-66] ARRAYTYPE — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-ED05306A-0D24-4FE7-9B50-1207BCA2B4B8.htm +- [E2-67] DIMLAYER — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-88E56602-EADC-4C68-93B4-93E7623BEB6B.htm +- [E2-68] DIMCONTINUEMODE — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-2424CA1C-B005-449B-A61A-C4F2D38C4E66.htm +- [E2-69] DIMDLI — https://help.autodesk.com/cloudhelp/2026/ENU/AutoCAD-Core/files/GUID-D2D1AF3D-004B-4C47-BABE-22B7327EE128.htm + +--- + +## 부록 4. K2 — `K2_우리CAD_키마우스.md` + +# K2 우리 내장 CAD — 키 · 마우스 조작 현황 (요구 227-3) + +- 기준 = `main_laptop_1` HEAD 9c0ef7f82 · 코드 읽기만(실행 · 빌드 · 화면 실측 안 함) · 저장소 고침 없음. +- 앞 현황 B2(`docs/raw/조사/2026-10-15_335_부록2_우리CAD현황.md` · 31e6adf7d) 뒤로 CAD 쪽(`openwebcad/src` · `A00_Common/key_shortcut.ts` · `input_prefs.ts` · `app_settings*.ts` · `ui_template/cad_host`) 바뀐 줄 0 — 334(☰ 바로가기)는 `A00_Common/app_shell.ts` · `m_settings_nav.ts` 만 고침 → B2 줄 번호 그대로 맞음 · 이번에 다시 읽어 확인 · B2 와 다르게 본 곳 = 「B2 고침」. +- 경로 줄임 — `src/` = `B07_DesignDetail/openwebcad/src/` · `ic/` = `src/inputController/` · `tl/` = `src/tools/` · `A00/` = `A00_Common/` · `:123` = 바로 앞 파일. +- 기호 — ● 있음 · ◐ 일부 · ○ 없음 · ⚠ 흠 · (추정) = 코드 흐름 판단 · 실측 필요. +- 낱말 — 점 단계 = 지금 단계가 `ABSOLUTE_POINT_INPUT` 을 받는 때(`ic/ime-sink.ts:24-26`) · 숫자 · 글 · 객체 · 고르기 단계 = 시퀀스 공장 단계 종류(`tl/factories/sequence-tool.ts:38`) · 쉬는 때 = 선택 도구가 첫 점 대기. + +## 0. 결론 + +1. 숫자 꼴은 `거리` · `x,y` · `@x,y` · `@거리<각` · `거리<각`(원점 기준) · `<각`(각도 고정) · Tab 두 칸(길이 · 각도) — 식 · 단위 · `@` 홀로 · `#` · 도분초 · 첫 점 X/Y 칸 없음. 숫자 단계는 `3.2*1.5` → 3.2 로 조용히 먹음(⚠ N5). +2. 먹는 키 = Enter · Space(글 단계 빼고 Enter) · 우클릭(Enter · 되풀이 없음) · Esc · Backspace · Delete · 화살표(늘 화면 이동) · Tab(동적 입력) · Ctrl+Z/Y/Shift+Z/A/S/V · F3/F7/F8/F10 이 전부 — 나머지 Ctrl · Alt 조합은 글자로 쌓임(H1 그대로). +3. 마우스 = 왼쪽(고름 · 점) · 가운데 끌기(팬) · 휠(커서 자리 줌 · 방향 ☰) · 우클릭(Enter) · 두 번 누름(글자 칸만) · Shift(더함) · Ctrl(넣고 빼기) — 우클릭 메뉴 · 올가미 · 울타리 · 끌어 옮기기 · 그립 모드 없음. +4. 명령 153 · 기본 별칭 88 · ☰ 내 별칭 — 앞글자만 쳐도 첫 후보 실행(`R` → RECTANG · `W` → WIPEOUT · `Q` → QUICKPROPERTIES · `EXT` → EXTEND). +5. 선택지 글자는 LINE(C · U) · ARRAY(L · R) 뿐 · 나머지는 묻는 차례 고정 · `<기본값>` 은 상수(직전 값 기억 없음). +6. Esc · Enter 가 도구마다 다름 — MOVE · COPY · ROTATE · SCALE · ARRAY 는 Esc 로 안 끝남 · RECTANG 첫 점 · TRIM 은 Esc 처리 없음 · PLINE 은 Esc 면 그린 것 전부 버림. +7. 켬끔 = 객체 스냅(F3 · 켬) · 직교(F8 · 끔) · 극 45°(F10 · 켬) · 스냅 추적(단추만 · 켬) · 그리드(F7 · 끔) · 명령행(단추 · 숨김) · 동적 입력(늘 켬) — 모두 메모리 · ☰ 설정 = 휠 방향 · 내 별칭 둘뿐. +8. 앞 흠 H1~H12 — 열둘 모두 지금도 있음. +9. 새로 본 흠 17 — 급한 셋: N1 MOVE · ROTATE · SCALE 도중 다른 명령으로 넘어가면 고른 객체가 도면에서 빠짐 · N7 명령 안 고르기 중 고른 것 그립 근처 누름 = 명령이 그립 편집으로 바뀜 · N6 명령행 칸 소문자 `c` = 선 닫기 대신 원. +10. B06 상세 · M02 카드는 명령행 · 상태막대가 없어 키 · 별칭이 사실상 유일한 길 → 보강은 키 · ☰ 설정 쪽으로 넣어야 세 곳이 다 씀. + +## A. 숫자 넣기 + +### A-1. 받는 꼴 + +| 꼴 | 예 | 받는 때 | 뜻 | 근거 | +|---|---|---|---|---| +| 거리 숫자 | `100` · `-5` · `.5` | 점 단계 + 앞 점 있을 때 | 앞 점에서 방향(가이드 스냅점 > 객체 스냅점 > 커서)으로 그 길이 · 음수 = 반대쪽 · 각도 고정 중 = 고정 방향 | `ic/input-patterns.ts:11-12,32-35` · `ic/input-controller.ts:587-597` · `src/helpers/get-point-from-event.ts:30-43` | +| 절대 `x,y` | `10,20` · `-3.5,.5` | 점 단계 어디서나 | 늘 절대(원점 기준) | `input-patterns.ts:13,36-42` · `get-point-from-event.ts:44-46` | +| 상대 `@dx,dy` | `@10,0` | 앞 점 있을 때 | 그 도구의 앞 점 기준(명령 사이 「마지막 점」 기억 없음) | `input-patterns.ts:14,43-49` · `get-point-from-event.ts:47-53` | +| 상대 극 `@d` 있을 때만 | `sequence-tool.ts:193-201,293-304` · `input-controller.ts:562-564` | +| 글 단계 값 | 아무 글 | 글 단계(TEXT · MTEXT · 지시선 · 찾기 등) | 친 그대로(대소문자 · 빈칸 · 숫자 · 명령 이름도 글) | `input-controller.ts:538-540` · `ime-sink.ts:40-45` | +| 한 번 스냅 이름 | `END` `MID` `CEN` `INT` `PER` `NEA` `NON` | 점 단계만 | 다음 한 점만 · 둘레 30px · 맞는 점 없으면 누름 버림 · F3 꺼져도 먹음 | `ic/input-snap-override.ts:13-21,56-80` · `input-controller.ts:239-243,558-561` | + +### A-2. 받지 못하는 꼴 + +| 꼴 | 지금 | 근거 | +|---|---|---| +| `@` 홀로(마지막 점) · `@d`(각 없음) · `x,y,z` | 명령 찾기로 넘어가 맞는 것 없음 → 도구에 글로 감 → 버림 | `input-patterns.ts:30-65` · `input-controller.ts:565-574` | +| `#x,y` | 버림(`x,y` 가 늘 절대) | `input-patterns.ts:13` | +| 식 `1+2*3` | 점 단계 = 버림 · 숫자 단계 = 앞 숫자만(⚠ N5) · QUICKCALC(QC) 결과는 알림에만(다음 입력에 못 씀) | `tl/utility/inquiry-tools.ts:120-140` | +| 단위 `3m` · `300mm` · 피트-인치 | 점 단계 = 버림 · 숫자 단계 = 앞 숫자만 | 같은 곳 | +| 도분초 `30d15'` · 방위 `N45E` · 라디안 `1.2r` · `30°` | 버림(십진 도만) | `input-patterns.ts:15-19` | +| `1.`(끝 점) · `1,5`(쉼표 소수) | `1.` = 버림 · `1,5` = 점 (1,5) | `input-patterns.ts:11-13` | +| 빈칸 낀 `10, 20` | 정규식은 받으나 캔버스는 Space = Enter 라 `10,` 이 먼저 제출돼 버려짐(⚠ N12) · 명령행 칸은 Shift+Space 로만 빈칸 | `input-controller.ts:458-463` · `src/components/CommandLine.tsx:58-65` | +| 첫 점에 거리 · `@` | 시퀀스 = 조용히 버림(도구 살아 있음) · LINE · CIRCLE · RECTANG · MOVE/COPY/ROTATE 기준점 = 받는 칸 없어 버림 | `sequence-tool.ts:286-289` · `tl/line-tool.ts:79-90` · `tl/move-tool.ts:147-160` | +| 첫 점 X · Y 칸 · 동적 입력 `#`/`@` 바꿈 | 없음 — 커서 곁에 커서 좌표 `x,y` 글만 | `input-controller.ts:139-147` | +| FROM · M2P · TT · `'CAL` · `'ZOOM` | 없음 — 치면 버림(`'` 꼴 해석 없음) | src 검색 0 | + +### A-3. 단계 종류별 받는 것 + +| 단계 | 누름 | `x,y` | `@` 꼴 | 거리 숫자 | 글 | 빈 Enter | 근거 | +|---|---|---|---|---|---|---|---| +| 점 | ● 스냅 반영 | ● | 앞 점 있을 때 | 앞 점 있을 때 | ○(명령 이름으로 넘어감) | 점 모으는 명령 끝 단계만 확정 · 그 밖 ○ | `sequence-tool.ts:282-292` | +| 숫자 | ○ | ○ | ○ | ● | `parseFloat` · 명령 이름도 삼킴 | `<기본값>` 있을 때 | `:293-304` | +| 글 | ○ | 글로 | 글로 | 글로 | ● | `<기본값>` 있을 때 | `:305-310` | +| 객체 | 객체 위 누름만(10px · 잠금층 뺌 · 겹친 것 돌림 없음 · 스냅 없음) | ○ | ○ | ○ | ○(명령 이름으로) | ○ | `:150-159,311-319,344` | +| 고르기 | 선택 도구 속(누를 때마다 바꿈 · Shift 더함 · Ctrl 넣고 빼기 · 상자 = 두 번 누름) | ○ | ○ | ○ | ○(명령 이름으로) | 고르기 끝 | `:321-337` | + +### A-4. 두 입력 자리 차이 + +| 항목 | 커서 곁(캔버스에 바로 치기) | 아래 명령행 칸 | 근거 | +|---|---|---|---| +| 보임 | 캔버스에 그린 글 상자 — 친 글 · 안내문 · 커서 좌표 · 동적 두 칸 · 고정 · 스냅 안내 · 명령 후보 6 · 눌러 고칠 수 없음 · 미리보기엔 안 그림 | 기본 숨김(상태막대 「명령행」) · 최근 3줄 · 안내문 · 후보 단추 8 | `input-controller.ts:137-166,503-508` · `src/helpers/draw.ts:75-78` · `CommandLine.tsx:13-15,43-48,89-105` · `src/components/Toolbar.tsx:23` | +| 대소문자 | 명령 단계 = 대문자로 · 글 단계 = 그대로 | 그대로 넘김 → 선 중 `c` = CIRCLE · 배열 `r` = 직선 · `end` 버림(⚠ N6) | `input-controller.ts:168-172,481` | +| 빈칸 | Space = Enter(글 단계 빼고) · Shift+Space = 아무 일 없음 | Space = 제출 · Shift+Space = 빈칸 | `ime-sink.ts:48-51` · `CommandLine.tsx:58-65` | +| 한글 자판 | 명령 단계 = 영문 자리 글자로 받음 · 글 단계 = 한글 조합 | 자모가 그대로 들어가 명령 못 찾음(H12) | `ime-sink.ts:1-5,92-102` · `CommandLine.tsx:51-57` | +| ↑↓ · Tab · F키 · Ctrl+Z | ↑↓ = 화면 이동 · Tab = 동적 입력 · F키 · Ctrl+Z = CAD | ↑↓ = 기록(명령 이름만) · Tab = 첫 후보 이름 채움 · F키 · Ctrl+Z 등 CAD 키 안 먹음(입력 칸 안 키는 CAD 가 건너뜀) | `input-controller.ts:370-373` · `CommandLine.tsx:31-39,66-81` | + +## B. 명령 중 키 + +### B-1. 키 × 상황 + +| 키 | 쉬는 때 | 점 받는 중 | 숫자 · 글 단계 | 근거 | +|---|---|---|---|---| +| Enter(빈 입력) | 직전 명령 되풀이 · 즉시형(ZOOM · ↶ 단추의 UNDO 포함)도 직전 명령이 됨 · 도면 실은 직후 직전 = SELECT | 도구에 ENTER(도구별 B-2) · 길이 잠근 채면 그 길이 점 | `<기본값>` 채택 · 없으면 아무 일 없음 | `input-controller.ts:511-537` · `run-command.ts:41,66-72` · `src/integration/aislo-drawing-bridge.ts:197` | +| Enter(글 있음) | 명령 이름 · 별칭 · 앞글자 → 실행 · 숫자 · 좌표 = 선택 도구가 안 받아 버림 | 해석 차례: 동적 각도 칸 → `` | — | — | `<6>` · `<1>` 등 빈 Enter · 우클릭 · Space 로 채택 · 값은 상수(직전 값 기억 없음) | `sequence-tool.ts:302-310` | +| `U` | UNDO 명령(되돌림 · 도구 안 끊음) | LINE = 한 구간 취소 · 그 밖 점 단계 = UNDO 가 돎(그리던 명령은 그대로)(H7) | 숫자 = 무시 · 글 = 글자 U | `src/commands/commands.file.ts:100-108` | +| `C` | CIRCLE | LINE = 닫기(둘째 선부터 · 한 구간이면 무시) · 그 밖 점 단계 = CIRCLE 로 바뀌며 지금 명령 버림(H7) | 숫자 = 무시 · 글 = 글자 C | `line-tool.ts:118-126` · `run-command.ts:44-50` | +| 다른 명령 이름(도구형) | 실행 | 지금 명령 버리고 새 명령(오토캐드 = 「점 아님」 거절) · ⚠ MOVE · ROTATE · SCALE 도중이면 고른 객체가 빠짐(N1) | 숫자 단계 = 삼킴 · 글 단계 = 글 | `run-command.ts:44-50` · `resources/tester/cad/keyboard-chain.test.ts:243-248` | +| 투명 명령 | — | 즉시형 33(ZOOM · Z · ZOOMIN · ZOOMOUT · REGEN · PAN(안내만) · LAYER · PROPERTIES · LIST · UNDO · REDO · COPYCLIP · PASTEORIG · INSERT · QSAVE · EXPORT* 등)은 지금 도구를 안 끊음 · `'ZOOM` 꼴 ○ · 휠 · 가운데 팬 · 화살표는 언제나 | 숫자 단계 = 삼켜서 못 씀 | `run-command.ts:44-54` · `src/commands/command.types.ts:22-26` | +| 스냅 이름 | 버림 | 점 단계 = 한 번 스냅 · 누름만 받는 점(SCALE 기준점 · ROTATE 방향점 등) = 안 먹음 · `EXT` `INS` = 그 명령이 돎 · `TAN` `QUA` `NOD` `APP` `PAR` `NONE` = 버림(N14) | 버림 | `input-controller.ts:558-561` · `input-snap-override.ts:13-21` | +| Ctrl+Z · Ctrl+Y | 되돌림 · 다시 + 고름 풂 | 되돌림 + 도구에 ESC — LINE 연속 끊김 · PLINE 그리던 것 버림 · MOVE 등 원래 자리로(N1 덧) | 되돌림 + ESC(친 글은 남음) · 한글 조합 중에도 먹음 | `:429-434,616-634` · `:374-376` | +| Ctrl+A · Ctrl+V | 전부 고름 · 붙여넣기 | 같음(명령 안 끊고 고름 · PASTECLIP 은 도구형이라 명령 바뀜) | 같음 | `:423-426,438-440` | + +### B-2. 도구별 빈 Enter · Esc + +| 도구 | 빈 Enter · 우클릭 | Esc | 끝난 뒤 | 근거 | +|---|---|---|---|---| +| LINE | 첫 점 = 아무 일 없음(앞 선 끝에서 잇기 ○) · 다음 점 = 끊고 새 첫 점 | 첫 점 = 선택 도구로 · 다음 점 = 끊고 새 첫 점 | LINE 그대로 | `tl/line-tool.ts:73-139` | +| CIRCLE | 아무 일 없음 | 중심 = 선택 도구 · 반지름 = 처음부터 | 원 하나 뒤 다시 중심 | `tl/circle-tool.ts:60-108` | +| RECTANG | 아무 일 없음 | 첫 모서리 = 처리 없음(안 끝남 ⚠ N3) · 맞은 모서리 = 처음부터 | 하나 뒤 다시 | `tl/rectangle-tool.ts:55-100` | +| PLINE · SPLINE · MLINE · WIPEOUT · REVCLOUD | 끝 단계 = 확정 | 값 없으면 선택 도구 · 있으면 그린 것 전부 버리고 처음부터(⚠ N4) | 같은 명령 다시 | `sequence-tool.ts:252-261,290-292` | +| 그 밖 시퀀스(ARC · POLYGON · ELLIPSE · XLINE · 치수 · DIST · OFFSET · FILLET · BREAK · ERASE …) | 점 · 객체 단계 = 아무 일 없음 · 숫자 · 글 = 기본값 · 고르기 = 끝 | 값 없으면 선택 도구 · 있으면 처음부터 | 점만 받는 명령 = 다시 · 숫자 · 글 · 객체 · 고르기 섞이면 선택 도구 | `sequence-tool.ts:231-261` | +| MOVE · COPY · ROTATE · SCALE · ARRAY | 고르기 단계 = 고르기 끝 · 그 밖 아무 일 없음(COPY 둘째 점 Enter 로 못 끝냄) | 안 끝남 — 고르기 단계로 돌아가 명령이 남음 · 기준점 뒤 Esc = 원래 자리로(⚠ N2) | 한 번 하고 다시 「…객체 선택 뒤 Enter」 | `tl/move-tool.ts:98-192` · `tl/copy-tool.ts:97-191` · `tl/rotate-tool.ts:105-215` · `tl/scale-tool.ts:107-215` · `tl/array-tool.ts:91-281` | +| TRIM | 처리 없음 | 처리 없음(안 끝남 ⚠ N3) | 누를 때마다 자름 | `tl/eraser-tool.ts:62-74` | +| 그립 편집 | 처리 없음 | 선택 도구로(옮김 취소) | 한 번 놓으면 선택 도구 | `tl/grip-edit-tool.ts:64-80` | +| 선택 도구 상자 둘째 점 | 처리 없음 | 상자 취소 | — | `tl/select-tool.ts:97-114` | + +## C. 마우스 + +| 동작 | 쉬는 때(선택 도구) | 명령 중 | 미리보기(viewOnly) | 근거 | +|---|---|---|---|---| +| 왼쪽 누름 | 누르는 순간 첫 점 — 고른 것 그립 위 = 그립 편집 · 객체 위(10px · 손가락 접촉 크기 ≥24px) = 고름 바꿈 · 빈 곳 = 상자 시작 | 누를 땐 안 보내고 놓을 때 한 점(스냅 · 가이드 반영) | 팬 시작 | `input-controller.ts:195-212,228-252,311-330` · `select-tool.helpers.ts:51-112` · `src/helpers/pick-radius.ts:17-22` | +| 왼쪽 끌기 | 4px 넘게 끌면 놓는 자리로 상자 닫음 · 안 끌면 두 번째 누름으로 닫음 | 놓은 자리 한 번 누름(끌어 그리기 · 상자 ○) | 팬 | `input-controller.ts:85-86,196-209` | +| 두 번 누름 | 글자 위 = 그 자리 글 칸(Enter 적용 · 여러 줄 Ctrl+Enter · Esc 취소 · 칸 밖 누름 · 초점 잃음 = 적용) · 수량표 글 = 알림 · 그 밖 = 아무 일 없음 | 점 둘로 들어감(LINE = 0 길이 선 · 추정) + 글자 위면 글 칸도 뜸(⚠ N16) | 전체 맞춤 | `src/integration/text-inplace-edit.ts:96-104,186-197,210-247` · `aislo-drawing-bridge.ts:113-118` | +| Shift+누름 | 더함(고른 것 다시 = 그대로 · 빼기 ○) | 고르기 단계만 · 그리기 = 무시(임시 직교 ○) | — | `select-tool.helpers.ts:94-99` | +| Ctrl+누름 | 넣고 빼기 · 폴리선 꼭짓점 그립 = 꼭짓점 지움 | 고르기 단계만 | — | `:58-67,83-93` | +| Alt+누름 | 무시 | 무시 | — | `input-controller.ts:245-251` | +| 상자 방향 | 왼→오 = 창(파랑 · 다 든 것) · 오→왼 = 걸침(초록 · 걸친 것) · 시작점 x 로 가름 | 같음(두 번 누름만) | ○ | `select-tool.helpers.ts:185-200` · `src/App.consts.ts:133-134` | +| 상자 + 키 | 늘 새로 고름 · Ctrl+상자 = 「안 고른 것만」 남음(H4) · Shift+상자 = 주석 TODO | 같음 | — | `select-tool.helpers.ts:114-169` | +| 올가미 · 울타리 · WP · CP · ALL · P · L | ○(끌기는 늘 사각형) | ○ — 고르기 중 `P` = PAN 안내 · `L` = LINE 실행 | — | `registry.ts:124-146` | +| 겹친 것 | 같은 자리(반경 안) 다시 누름 = 다음 후보로 바꿈 · 목록 창 ○ | 객체 단계 = 가장 가까운 것만 | — | `select-tool.helpers.ts:34-49` · `sequence-tool.ts:150-159` | +| 스침 강조 | 선택 도구일 때만 | ○(H5) | ○ | `input-controller.ts:283-293` · `src/main.tsx:89-111` | +| 그립 | 고른 객체에 늘 그림(8px) · 누름 = 그립 편집 → 끌어 놓은 자리 적용 · 안 끌고 떼도 뗀 자리 적용(H3) · 끄는 동안 스냅 · 가이드 · 동적 두 칸 · `x,y` · `@` 는 누른 채 쳐야 함 · 거리 숫자 · 길이 잠금 Enter 무시(N8) · Space 모드 돌림 · Ctrl 복사 · Shift 여러 그립 · 그립 메뉴 ○ · 호 · 해치 · 치수 · 타원 · 스플라인 · 호 섞인 폴리선 그립 ○ | 고르기 단계에서 고른 것 그립 10px 안 누름 = 명령이 그립 편집으로 바뀜(⚠ N7) | 못 고름 | `select-tool.helpers.ts:55-72` · `tl/grip-edit-tool.ts:26-112` · `src/helpers/grips.ts:74-265` · `draw.ts:56` | +| 칸 모서리 끌기 | 고른 하나가 칸 있는 글 · 그림이면 모서리 9px = 진짜 끌기로 크기(도구 무관 · 확정 도면 막음) | 같음 | ○ | `src/integration/box-resize-drag.ts:25,59-118` | +| 작도 영역 가이드 끌기 | 도각 편집 · M02 양식(editable)만 변 · 모서리 9px | 같음 | — | `src/integration/guide-area.ts:149-188` | +| 가운데 끌기 | 팬(자동 스크롤 · 붙여넣기 막음) | 같음 | 같음 | `input-controller.ts:120-124,188-190,264-272,312-316` | +| 가운데 두 번 | ○(전체 보기 = `Z` · 우하단 ⛶) | ○ | ○ | `src/commands/commands.view.ts:41-51` | +| 휠 | 커서 자리 줌 · 한 칸 약 10% · 방향 ☰ [설정](기본 「당기면 확대」= 몸 쪽으로 굴리면 확대) | 같음(명령 안 끊음) | 같음 | `ic/input-wheel.ts:14-29` · `A00/input_prefs.ts:23,53-55` · `App.consts.ts:150` | +| Ctrl+휠 | 마우스 = 같은 방향 · 한 칸 약 35%(지수 0.003) · 트랙패드 핀치(\|Δ\|<50) = 늘 벌리면 확대 | 같음 | 같음 | `input-wheel.ts:26-27` · `A00/input_prefs.ts:59-64` · `App.consts.ts:156` | +| 우클릭 | = Enter(되풀이 없음) · 메뉴 · 길게 누름 메뉴 · Shift+우클릭 스냅 메뉴 ○ · iframe 기본 메뉴 막음 | 같음 | 같음 | `input-controller.ts:116-119,174-180` · `ui_template/cad_host/cad_host.ts:147-150` | +| 뒤 · 앞 단추 | 처리 없음 | 같음 | 같음 | `input-controller.ts:317` | +| 파일 끌어 놓기 | DWG · DXF → 서버 변환 → 원좌표 붙임 · 확정 도면 막음 | 같음 | 막음 | `src/integration/dxf-clip-paste.ts:88-105` | +| 고른 것 끌어 옮기기 · 블록 끌어 넣기 | ○(블록 = 패널 누름 → 놓을 점 누름 · `x,y`) | — | — | `src/components/BlockLibraryPanel.tsx:37-50` · `tl/insert-block-tool.ts:74-84` | +| 손가락 | 톡 = 누름 · 한 손가락 끌기 = 팬 · 두 손가락 = 핀치 + 팬 · 0.45초 길게 = 위 70px 십자선 정밀 · 오른쪽 단추 6(Esc · Enter · 지움 · ↶ · ↷ · F3 · 손가락 기기만) | 같음 · 톡 = 그립 편집 즉시 적용(H3 길) | 같음 | `ic/input-touch.ts:7-144` · `ic/input-touch.css:3-29` | + +## D. 단축키 + +### D-1. 키 조합 · 기능키 + +| 키 | 하는 일 | 근거 | +|---|---|---| +| Ctrl+Z | 되돌림 + 고름 풂 + 도구 ESC | `input-controller.ts:429-431,616-624` | +| Ctrl+Y · Ctrl+Shift+Z | 다시 + 같은 처리 | `:432-434,626-634` · `A00/key_shortcut.ts:38-41` | +| Ctrl+A | 잠금층 뺀 전부 고름(숨긴 것 · 꺼진 층 포함 H6) | `:438-440` | +| Ctrl+S | M02 양식(hostSave)만 부모 [저장] · B07 · B06 = 아무 일 없음(브라우저 저장 창 막음) | `:435-437` | +| Ctrl+V | 시스템 클립보드 글이 DXF = 서버 변환 붙임 · 아니면 앱 안 클립보드 있으면 PASTECLIP | `:423-426` · `dxf-clip-paste.ts:76-87` | +| Ctrl+C · Ctrl+X · 그 밖 Ctrl+글자 · 숫자 | 글자로 쌓임(H1) — Ctrl+C → `C` → Enter = CIRCLE · Ctrl+X = EXPLODE · Ctrl+E = ERASE · Ctrl+F = FILLET · Ctrl+P = PAN 안내 · Ctrl+1 = `1` · Ctrl+- / + / 0 = 글자(브라우저 확대도 막힘) · COPYCLIP 등은 명령 · 리본만 | `:427-428,476-482` | +| Alt+글자 | 글자로 쌓임(H1) | `:476-482` | +| ⌘(맥) | Z · Y · S · A = Ctrl 과 같음 · ⌘V = 붙여넣기 안 감(추정 N13) | `A00/key_shortcut.ts:19-25,36` · `input-controller.ts:423` | +| Shift+화살표 | 크게 이동(20 → 100 × 배율) | `:346-367` | +| Shift+Space · Ctrl+Space | 아무 일 없음(B2 「Shift+Space 겹친 것 돌림」 없음 — 검토 문서가 이미 고침) | `ime-sink.ts:48-51` | +| Shift+Tab | Tab 과 같음 | `:413` | +| Ctrl+Enter · Shift+Enter | Enter 와 같음(수정키 안 봄 · 길게 누르면 되풀이도 막지 않음) | `:454-457` | +| F1 · F2 · F4 · F6 · F9 | 아무 일 없음(키 삼킴 · 브라우저 도움말도 막힘) | `:427-428` | +| F3 | 객체 스냅 켬/끔 — 끄면 직교 · 극 · 추적 가이드도 꺼짐(H8) | `:393-397` | +| F5 | 브라우저 새로고침(막지 않음 · 안 저장한 것은 자동 백업 복구 안내로만) | `:382-385` · `src/helpers/autosave.ts:1-4` | +| F7 | 그리드(화면 24px 무늬 H10) | `:398-402` | +| F8 | 직교 90° — 극 켜져 있으면 극을 끄고 직교 | `:403-407` | +| F10 | 극 45° — 직교 켜져 있으면 직교를 끄고 극 | `:408-412` | +| F11 | 브라우저 전체 화면(스냅 추적 아님) | `:386-391` | +| F12 | 개발자 도구(동적 입력 끄기 아님) | `:378-381` | +| Home · End · PgUp · PgDn · Insert | 아무 일 없음(키 삼킴) | `:427-428,476` | +| 숫자판 | 숫자 · `.` · `-` · `+` · Enter 그대로 | `:476-482` | + +- 임시 우선 키 · 수정키 조합 — Shift 임시 직교 · Shift+우클릭 스냅 메뉴 · Shift+글자 임시 스냅 · Ctrl 그립 복사 · Shift 여러 그립 · 임시 추적점 모두 ○(`holdingShift` · `holdingCtrl` 은 선택 도구만 읽음 `select-tool.helpers.ts:58,80-99`). + +### D-2. 기본 별칭 88 → 명령(`src/commands/command-catalog.json` = registry 거울 · 시험 `resources/tester/cad/command-catalog.test.ts:11-27`) + +| 갈래 | 별칭 = 명령 | +|---|---| +| 그리기 19 | L=LINE · PL=PLINE · C=CIRCLE · A=ARC · REC=RECTANG · POL=POLYGON · EL=ELLIPSE · SPL=SPLINE · ML=MLINE · XL=XLINE · PO=POINT · DO=DONUT · DIV=DIVIDE · ME=MEASURE · H · BH=HATCH · GD=GRADIENT · BO=BOUNDARY · REG=REGION | +| 고치기 23 | M=MOVE · CO · CP=COPY · RO=ROTATE · SC=SCALE · AL=ALIGN · MI=MIRROR · O=OFFSET · AR=ARRAY · TR=TRIM · EX=EXTEND · S=STRETCH · F=FILLET · CHA=CHAMFER · BR=BREAK · J=JOIN · X=EXPLODE · E=ERASE · LEN=LENGTHEN · PE=PEDIT · HE=HATCHEDIT · DR=DRAWORDER · MA=MATCHPROP | +| 글자 · 치수 · 지시선 · 표 19 | T · MT=MTEXT · DT=TEXT · ST=STYLE · TEDIT=TEXTEDIT · DLI=DIMLINEAR · DAL=DIMALIGNED · DAN=DIMANGULAR · DAR=DIMARC · DRA=DIMRADIUS · DDI=DIMDIAMETER · DOR=DIMORDINATE · DBA=DIMBASELINE · DCO=DIMCONTINUE · D=DIMSTYLE · MLD=MLEADER · MLS=MLEADERSTYLE · TB=TABLE · TS=TABLESTYLE | +| 층 · 특성 · 그룹 10 | LA=LAYER · LAS=LAYERSTATE · COL=COLOR · LT=LINETYPE · LW=LWEIGHT · CH · MO · PR=PROPERTIES · QP=QUICKPROPERTIES · G=GROUP | +| 조회 · 고르기 7 | DI=DIST · AA=AREA · MEA=MEASUREGEOM · LI=LIST · QC=QUICKCALC · UNHIDE · UNISOLATE=UNISOLATEOBJECTS | +| 삽입 · 보기 · 파일 10 | IAT=IMAGEATTACH · B=BLOCK · I=INSERT · SE=SELECT · Z=ZOOM · RE=REGEN · P=PAN · SAVE=QSAVE · EXP=EXPORT · U=UNDO | +| 별칭 없는 명령 71 | MLSTYLE · RAY · WIPEOUT · BLEND · BREAKATPOINT · DRAWORDERBACK · OVERKILL · REVERSE · SETBYLAYER · ALIGN×6 · FIND · DIM · QDIM · CENTERMARK · CENTERLINE · DIMUPDATE · DIMSPACE · MLEADERALIGN · TABLE×6 · REVCLOUD · ANNOSCALE · LAY×16 · TRANSPARENCY · UNGROUP · RADIUS · ANGLE · ID · QSELECT · SELECTSIMILAR · ISOLATEOBJECTS · HIDEOBJECTS · 클립보드 6 · ZOOMIN · ZOOMOUT · RECOVER · EXPORT×4 · TOAREA · REDO | + +- 해석 차례 — 이름 > 내 별칭 > 기본 별칭 > 앞글자 첫 후보(내 별칭 → 기본 별칭 → 이름 · 명령 표 차례) · 대소문자 무관 — `src/commands/registry.ts:124-146`. +- 한 글자 결과 — A=ARC · B=BLOCK · C=CIRCLE · D=DIMSTYLE · E=ERASE · F=FILLET · G=GROUP · H=HATCH · I=INSERT · J=JOIN · K=없음 · L=LINE · M=MOVE · N=없음 · O=OFFSET · P=PAN(안내만) · Q=QUICKPROPERTIES(앞글자) · R=RECTANG(앞글자) · S=STRETCH · T=MTEXT · U=UNDO · V=없음 · W=WIPEOUT(앞글자) · X=EXPLODE · Y=없음 · Z=ZOOM(범위만). +- 앞글자로 엉뚱하게 도는 예 — `LAY`→LAYER · `UN`→UNISOLATEOBJECTS · `LE`→LENGTHEN · `DD`→DIMDIAMETER · `TO`→TOAREA · `TA`→TABLE · `HI`→HIDEOBJECTS · `RA`→RAY · `IN`→INSERT · `EXT`→EXTEND. +- 없는 이름(치면 버림) — `OS` · `OSNAP` · `DS` · `UNITS` · `SN` · `GRID` · `ORTHO` · `PURGE` · `LTS` · `WBLOCK` · `CL` · `ED` · `LEADER` · `CAL` 등. + +### D-3. ☰ [설정]에서 바꿀 수 있는 것 + +| 탭 | 무엇 | 근거 | +|---|---|---| +| ① 마우스 | 휠 확대 방향(당김 · 밂 · 모든 화면 한 값) · 단추 규칙 안내 4줄(읽기만) | `A00/app_settings.ts:22-79` · `A00/input_prefs.ts:11-55` | +| ② CAD 단축키 | 153 명령마다 내 별칭 — 영문 · 숫자 · 쉼표/빈칸으로 여럿 · 대문자로 · 내 별칭끼리 겹침 금지 · 명령 이름과 같음 금지 · 기본 별칭 덮기는 됨(내 별칭이 먼저) · [저장] 때 계정 pref `aislo:pref:input-prefs` · CAD 는 부를 때마다 읽어 바로 따름 | `A00/app_settings_cad.ts:31-63,66-118` · `registry.ts:116-122` | +| ③ 다른 곳 단축키 | 읽기만 안내표 — CAD 줄 「F3 스냅 · F7 그리드 · F8 직교 · F10 45° · Ctrl+Z · Ctrl+Y · Ctrl+A · Ctrl+S(M02) · Delete · Esc · Enter · Space = Enter · 방향키 = 화면 옮김 · 글자 = 명령」(Ctrl+V · Tab · 스냅 이름 · Shift/Ctrl+누름 · 우클릭 = Enter 빠짐) | `A00/app_settings.ts:41-49,81-89` · `ui_template/ui_template_locale_common.ts:165-169` | +| 못 바꿈 | F키 · Ctrl 조합 · 우클릭 · 스냅 15px · 한 번 스냅 30px · 집기 10px · 머묾 1초 · 극 각 · 켬끔 기본값 · 그립 크기 · 휠 배율 | `App.consts.ts:52-83,150-156` · `input-snap-override.ts:58-59` | + +### D-4. 한글 자판 + +| 자리 | 지금 | 근거 | +|---|---|---| +| 캔버스 명령 단계 | 숨은 IME 받개 초점을 풀어 영문 자리 글자로 받음(윈도 크롬 시험) | `ime-sink.ts:1-5,92-102` · `ic/ime-sink.test.ts:11-56` | +| 캔버스 글 단계 | 받개에 초점 → 한글 조합 · 조합 중 글 커서 곁에 보임 · 조합 중 키(229 · Process)는 단축키로 안 먹음 | `ime-sink.ts:9-10,53-91` · `input-controller.ts:165,374-376` | +| Ctrl 단축키 | 자판 자리(`code`)로 판정 → 한글 자판 · 조합 중에도 먹음 | `A00/key_shortcut.ts:11-17` · `resources/tester/cad/ctrl-shortcut-hangul.test.ts` | +| 명령행 칸 | 자모 그대로(H12) | `CommandLine.tsx:51-57` | +| 두 번 눌러 연 글 칸 | 일반 칸 — 한글 됨 · 조합 중 Enter = 조합 끝만 | `text-inplace-edit.ts:96-104` | +| 내 별칭 | 한글 못 씀 | `app_settings_cad.ts:48` | + +## E. 그리기 · 고치기 명령별 + +- 프롬프트 = 우리 글 그대로(→ = 다음 단계) · 「빠진 것」 = 오토캐드 기본 옵션 기억 기준 짐작(정확한 대조는 오토캐드 쪽 목록과 맞댐). +- 점 단계 꼴 = A-1(누름 · `x,y` · 앞 점 있으면 거리 · `@` · `.」 → 「다각형의 중심…」 → 「꼭짓점을 지정하십시오 (중심에서의 반지름).」 | 없음 | 면 수 · 꼭짓점 거리 = 중심~꼭짓점(내접 방식만) | 하나 뒤 선택 도구 · 결과 = 폴리선 | 모서리(E) · 내접/외접(I/C) 고르기 | `:62-80` | +| ELLIPSE(EL) | 「타원의 중심…」 → 「장축의 끝점…」 → 「단축 거리를 지정하십시오.」 | 없음 | 점 단계 꼴 · 셋째 숫자 = 장축 끝에서 간 점이라 단축 ≠ 친 값(N10) | 하나 뒤 다시 · 결과 = 폴리선 근사 | 축 끝 방식 · 호(A) · 회전(R) | `:82-105` | +| SPLINE(SPL) | 「스플라인의 첫 점…」 → 「다음 점… ENTER로 종료합니다.」 | 없음 | 점 단계 꼴 | Enter 확정 · Esc 버림 · 결과 = 폴리선 근사 | 방식(M) · 닫기(C) · 공차(L) · 시작 · 끝 접선 | `:107-122` | +| XLINE(XL) | 「구성선이 지날 점…」 → 「방향을 지정하십시오.」 | 없음 | 점 단계 꼴(둘째 거리 = 커서 방향과 같음) | 한 줄 뒤 처음부터(같은 지날 점에 여럿 ○) · 결과 = 화면 대각 20배 선 | 수평(H) · 수직(V) · 각도(A) · 이등분(B) · 간격(O) | `tl/draw/construction-tools.ts:16-52` | +| OFFSET(O) | 「간격띄우기 거리를 입력하십시오 <1>.」 → 「간격띄우기할 객체를 선택하십시오.」 → 「간격을 띄울 방향의 점을 지정하십시오.」 | 없음 | 거리 = 숫자(빈 Enter = 늘 1) | 한 번 뒤 선택 도구(같은 거리 계속 ○) | 통과점(T) · 지우기(E) · 층(L) · 여러 개(M) · 취소(U) · 거리 기억 | `tl/modify/transform-tools.ts:70-85` | +| FILLET(F) | 「모깎기 반지름을 입력하십시오 <0>.」 → 「첫 번째 객체를 남길 쪽에서 선택하십시오.」 → 「두 번째 객체…」 | 없음 | 반지름(빈 Enter = 늘 0) | 한 번 뒤 선택 도구 | 폴리선(P) · 자르기(T) · 여러 개(M) · 취소(U) · Shift 누름 = 0 · 반지름 기억 · 객체 먼저 차례 | `tl/modify/corner-tools.ts:19-36` | +| CHAMFER(CHA) | 「첫 번째 모따기 거리를 입력하십시오 <1>.」 → 「두 번째 … <1>.」 → 객체 둘 | 없음 | 거리 둘 | 한 번 뒤 선택 도구 | 각도(A) · 방식(E) · 폴리선(P) · 자르기(T) · 여러 개(M) | `:38-56` | +| TRIM(TR) | 「지울 선 선택」 | 없음 | 없음(누름만) | 누를 때마다 가까운 교차점 사이 지움 · 선 · 원 · 호 · 사각형만(폴리선 ○) · Esc 없음 · 집기 반경 없음(N9) | 절단 모서리(T) · 걸침(C) · 울타리(F) · 모드(O) · 지우기(R) · 취소(U) · Shift = 연장 | `tl/eraser-tool.ts:62-161` | +| EXTEND(EX) | 「경계로 쓸 객체를 선택하십시오.」 → 「연장할 선을 선택하십시오.」 | 없음 | 없음 | 경계 하나 · 선 하나 뒤 선택 도구 | 경계 여럿 · 계속 누르기 · 걸침 · 울타리 · Shift = 자르기 | `tl/modify/structure-tools.ts:9-27` | +| MOVE(M) | (고른 것 없으면)「이동할 객체 선택 뒤 Enter」 → 「기준점 지정 (x,y):」 → 「둘째 점 지정 (길이 · <각도 · @x,y):」 | 없음 | 기준 = 누름 · `x,y` · 둘째 = 누름 · 거리 · `x,y` · `@` · `:」 → 「복사 개수 입력:」 → 직선「간격 시작점 지정:」→「간격 끝점 지정:」 · 원형「회전 중심점 지정:」→「한 칸 각도(도) 입력:」 | L · R(R 밖 글 = 직선) | 개수(원본 뺀 수) · 간격 시작 = 누름(`x,y` 면 복사 0 ⚠ N17) · 끝 = 누름 · `x,y` · `@`(거리 숫자 ○) · 각도 숫자 | 한 번 뒤 고르기 단계 · 방식 · 개수 Esc = 고름만 풂 · 원형 각도 Esc 없음 | 사각(행 · 열) · 경로 · 극 채우기 각 · 연관 배열 | `tl/array-tool.ts:91-361` | +| BREAK(BR) | 「끊을 객체를 선택하십시오.」 → 「첫 번째 끊기 점…」 → 「두 번째 끊기 점…」 | 없음 | 첫 = 누름 · `x,y` · 둘째 = 거리 · `@` 도 | 선택 도구 | 고른 점 = 첫 점 · 첫 점(F) | `corner-tools.ts:75-88` | +| JOIN(J) | 「결합할 객체를 선택한 뒤 ENTER.」 | 없음 | — | 끝점 맞닿은 것만 · 선택 도구 | — | `structure-tools.ts:29-44` | +| LENGTHEN(LEN) | 「길이를 바꿀 선을 늘릴 쪽 끝 근처에서 선택하십시오.」 → 「증분 길이를 입력하십시오 (음수는 단축) <10>.」 | 없음 | 증분 | 선만 · 선택 도구 | 퍼센트(P) · 전체(T) · 동적(DY) · 계속 누르기 | `transform-tools.ts:128-148` | +| DIVIDE(DIV) | 「등분할 객체를 선택하십시오.」 → 「세그먼트 수를 입력하십시오 <4>.」 | 없음 | 수 | 점 객체 · 선택 도구 | 블록(B) | `tl/draw/divide-tools.ts:13-28` | +| MEASURE(ME) | 「길이로 분할할 객체를 선택하십시오.」 → 「세그먼트 길이를 입력하십시오 <10>.」 | 없음 | 길이 | 같음 | 블록(B) | `:30-45` | +| DIST(DI) | 「거리를 잴 첫 점…」 → 「두 번째 점…」 | 없음 | 점 단계 꼴 | 알림(거리 · X/Y 증분 · 각도) · DIST 다시 | 여러 점(M) · 결과를 다음 입력에 씀 | `tl/utility/inquiry-tools.ts:25-41` | +| TEXT(DT) | 「문자의 시작점을 지정하십시오.」 → 「문자를 입력하십시오.」 | 없음 | 시작점 | 한 줄 · 선택 도구 · 높이 · 각 = 지금 글 꼴 | 자리맞춤(J) · 글꼴(S) · 높이 · 회전 묻기 · 줄 이어 치기 | `tl/annotate/text-tools.ts:40-51` | +| MTEXT(T · MT) | 「여러 줄 문자의 첫 코너…」 → 「문자를 입력하십시오 (줄바꿈은 \P).」 | 없음 | 첫 코너 | 한 칸 글에 `\P` 줄바꿈 · 선택 도구 | 맞은 코너 · 높이 · 자리맞춤 · 줄 간격 · 회전 · 폭 · 편집기 | `:53-64` | +| DIM 류 | DIMLINEAR · DIMALIGNED 「첫 번째 치수보조선 원점…」→「두 번째…」→「치수선 위치…」 · DIMBASELINE · DIMCONTINUE 「다음 치수보조선 원점…」(직전 치수 기준) · DIMRADIUS · DIMDIAMETER · DIMARC 객체 →「치수 문자 위치…」 · DIMANGULAR 꼭짓점 · 변 위 점 둘 · DIMORDINATE 피처 · 지시선 끝 · DIM 객체 → 치수선 위치 · QDIM 고르기 → 치수선 위치 | 없음 | 점 단계 꼴 | 점만 = 다시 · 객체 섞임 = 선택 도구 · DIMLINEAR 가로/세로 = 놓는 쪽으로 자동 | 객체 고르기(첫 점 Enter) · 글 바꿈(M · T) · 글 각(A) · 가로(H) · 세로(V) · 회전(R) | `tl/annotate/dimension-tools.ts:24-160` · `tl/annotate/dimension-radial-tools.ts:54-160` | +| HATCH(H · BH) | 「해치를 채울 경계 객체를 선택한 뒤 ENTER를 누르십시오.」 | 없음 | — | 고른 객체로 닫힌 경계 · 무늬 · 간격 · 각 = 고정(무늬 · 자동 · 45° · 바꾸는 길 없음) · 선택 도구 | 내부 점 누르기 · 무늬 · 축척 · 각 고르기 · 연관 | `tl/draw/fill-tools.ts:16-55` · `src/commands/draw-settings.ts:9-32` | + +## F. 켬끔 · 설정 + +| 항목 | 기본 | 켜고 끄기 · 내용 | 저장 | 근거 | +|---|---|---|---|---| +| 객체 스냅(상시) | 켬 | F3 · 상태막대 「객체 스냅」(제목에 F3 안 적힘) · 손가락 ⌖ · 종류별 끄기 ○ | 메모리 | `src/state.ts:179,481-490` · `StatusBar.tsx:28-35` | +| 스냅 종류 | 끝점 · 중간점(선 · 사각형 · 그림 변) · 중심(원 · 호) · 사분점(원만) · 교차 · 노드 · 접점 ○ | — | — | B2 2-1 그대로(`src/entities/*` getSnapPoints · 바뀐 줄 0) | +| 직교 | 끔 | F8 · 「직교」 — 90° 가이드 · 도면 단위 15 안일 때만 붙음(H8 · H9) | 메모리 | `state.ts:82` · `src/helpers/get-draw-guides.ts:37-48` | +| 극 추적 | 켬 · 45° | F10 · 「극좌표 추적」 · 증분 · 추가 각 ○ · 직교와 한 값(둘 중 하나) | 메모리 | `state.ts:82` · `src/helpers/get-angle-guide-lines.ts:7-30` | +| 객체 스냅 추적 | 켬 | 상태막대 「스냅 추적」만(키 ○) · 1초 머묾 표시 · 최대 3 · 직교 · 극 둘 다 끄면 안 뻗음 · F3 끄면 같이 꺼짐 | 메모리 | `state.ts:182` · `StatusBar.tsx:52-59` · `src/helpers/calculate-angle-guides-and-snap-points.ts:36-58` | +| 그리드 | 끔 | F7 · 「그리드」 — 화면 24px 무늬 · 격자 스냅 ○ | 메모리 | `state.ts:180` · `src/drawControllers/screenCanvas.drawController.ts:339-353` | +| 동적 입력 | 늘 켬 | 끄기 ○ · 앞 점 있는 점 단계만 [길이][각도] | — | `input-controller.ts:148-160,605-607` | +| 명령행 | 숨김 | 상태막대 「명령행」 | 메모리 | `Toolbar.tsx:23` · `StatusBar.tsx:68-75` | +| 고르기 방식 | 누름 = 바꿈 · Shift 더함 · Ctrl 넣고 빼기 · 상자 = 끌기 또는 두 번 누름(명령 안 = 두 번만) · 먼저 고르고 명령 = MOVE · COPY · ROTATE · SCALE · ARRAY · PEDIT 바로 / 시퀀스 고르기 단계 = Enter 한 번 더 · LINE · CIRCLE · RECTANG 은 켜면서 고름 풂 · 그립 늘 켬 | 설정 ○ | — | `select-tool.helpers.ts:51-112` · `line-tool.ts:145-155` | +| 휠 방향 | 당기면 확대 | ☰ [설정] ① | 계정 pref | `A00/input_prefs.ts:23,39-55` | +| 내 별칭 | 없음 | ☰ [설정] ② | 계정 pref | `A00/app_settings_cad.ts:31-118` | +| 수치 | 스냅 15px · 한 번 30px · 집기 10px · 머묾 1초 · 표시 3 · 그립 8px · 칸 모서리 9px | 못 바꿈 | — | `App.consts.ts:52,57,63,70,77,83` · `box-resize-drag.ts:25` | + +### F-2. 띄우는 곳별 키 · 마우스 차이 + +| 자리 | 싣기 곁값 | 차이 | 근거 | +|---|---|---|---| +| B07 상세 설계 | 없음(겉칸 전부) | 표준 · Ctrl+S 아무 일 없음 · 확정 도면 = 명령만 막힘(그립 · Delete · 특성은 안 막힘 H2) | `B07_DesignDetail/B07_DesignDetail_UI_Page.ts:117,357,537` | +| B07 평면도 관리 미리보기 | readOnly · viewOnly · bare | 왼쪽 끌기 = 팬 · 두 번 = 맞춤 · 휠 · 가운데 · 화살표 됨 · 고르기 · 스냅 · 커서 상자 ○ · Ctrl+A → Delete 는 먹음(H2) | `B07_DesignDetail/B07_DesignDetail_UI_PlanManage.ts:178-190` | +| B06 횡단 [상세] 표 위 CAD | bare | 리본 · 상태막대 · 명령행 · 우하단 줌 · ↶↷ 없음 → 명령 = 글 치기 · 켬끔 = F키만 · 스냅 추적 켜고 끄기 ○ · ↑ 기록 ○ · 되돌리기 = Ctrl+Z · `U` · Ctrl+S 삼킴 · 고칠 때마다 세션 초안에 씀 | `B06_Section/B06_Section_UI_Cross_Detail_Cad.ts:317-346` · `Toolbar.tsx:236` | +| M02 도면 양식 | hostSave · ribbonCollapsed(+readOnly · viewOnly) | Ctrl+S · 💾 = 부모 [저장] · 우하단 ↶ ↷(UNDO · REDO 명령 → Enter 되풀이 대상이 됨) · 작도 영역 가이드 끌기 | `M02_MasterTemplete/M02_MasterTemplete_Drawing.ts:231-233,270-291` · `ViewControls.tsx:14-27` | +| M02 구조물 카드 | 보기 = readOnly · viewOnly / 고치기 = 상태막대 · 리본 · 패널 CSS 숨김 | 고치기 = 명령행 · 스냅 추적 · 그리드 단추 없음(F키 · 글 치기만) | `M02_MasterTemplete/M02_MasterTemplete_DrawingCard.ts:41-45,78-93` | +| M02 면적산출 도면 패널 | readOnly | 고르기(누름 · 끌기)만 → TOAREA | `M02_MasterTemplete/M02_MasterTemplete_AreaDrawing.ts:155-160` | +| 공통(부모 틀) | — | 포인터가 CAD 위로 오면 iframe 키 초점(부모 입력 칸 치는 중이면 안 뺏음) · 부모 돋보기(Z · 우클릭 길게)는 CAD 위에서 안 켬 · 부모 페이지에 CAD 용 키 처리 없음 | `ui_template/cad_host/cad_host.ts:147-156` · `resources/tester/test_221_1_loupe_cad.py` | + +## G. 흠 + +### G-1. 앞 흠 H1~H12 — 지금 + +| # | 지금 | 근거(지금 줄) | +|---|---|---| +| H1 Ctrl · Alt+글자 → 명령 글자 | 있음 | `input-controller.ts:476-482`(수정키 안 봄) · `A00/key_shortcut.ts:31-49`(Z · Y · S · A 만) | +| H2 보기 전용인데 고쳐짐 | 있음 — 막는 곳은 명령 · 붙여넣기 · 끌어 놓기 · 칸 모서리 · 글 두 번 누름 · ↶↷ 단추뿐 · 그립 · Delete · 특성 칸 · 리본 특성 · 도면층 관리에 문 없음 | `select-tool.helpers.ts:55-72` · `input-controller.ts:438-450` · `src/components/PropertiesEditor.tsx` · `RibbonWidgets.tsx` · `LayerManager.tsx`(readOnly 판정 부르는 곳 = `run-command.ts:30` · `dxf-clip-paste.ts:71` · `box-resize-drag.ts:67` · `text-inplace-edit.ts:215` · `ViewControls.tsx:14` · `DropHint.tsx:7` · `autosave.ts:93` · `aislo-table-bridge.ts:62`) | +| H3 그립 누르기만 해도 밀림 | 있음(추정) — 그립 편집 시작에 스냅 지움 → 안 움직이고 떼면 뗀 자리 | `src/state.ts:386-391` · `tl/grip-edit-tool.ts:85-105` · `input-controller.ts:195-212,228-238` | +| H4 상자 + Ctrl/Shift | 있음 | `select-tool.helpers.ts:114-169` | +| H5 명령 중 고르기 = 바꿈 · 끌기 상자 ○ · 스침 ○ | 있음 | `select-tool.helpers.ts:80-82` · `input-controller.ts:283,327` | +| H6 안 보이는 것 고름 · 스냅 | 있음 — 보임 판정은 그리기에서만 | `src/helpers/draw-functions.ts:32-37` · `state.ts:220-221` · `calculate-angle-guides-and-snap-points.ts:29-33` | +| H7 PLINE 등 C · U | 있음 | `basic-draw-tools.ts:21-36` · `sequence-tool.ts:282-292` · `commands.file.ts:100-107` | +| H8 F3 끄면 직교 · 극도 꺼짐 · 직교 강제 아님 | 있음 | `input-controller.ts:277-280` · `get-draw-guides.ts:37-48` | +| H9 가이드 거리 = 도면 단위 15 | 있음 | `get-draw-guides.ts:42` · `App.consts.ts:57` | +| H10 그리드 = 화면 무늬 | 있음 | `screenCanvas.drawController.ts:339-353` | +| H11 ↑ 기록에 알림 줄 | 있음 — `ERASE: 확정 도면 — 실행하지 않음` → ↑ = `ERASE:` | `run-command.ts:18-24,30-38` · `CommandLine.tsx:38` | +| H12 명령행 한글 자모 | 있음 — 자모 → 영문 바꿈 표 없음(검색 0) | `CommandLine.tsx:51-57` | + +### G-2. 새로 본 흠(H 밖 · 코드 읽기) + +| # | 무엇 | 근거 | 급함 | +|---|---|---|---| +| N1 ⚠ | MOVE 기준점 · ROTATE · SCALE 기준선 뒤에 다른 명령(글 · 별칭 · 리본)으로 넘어가면 고른 객체가 도면에서 빠진 채 남음 — 옮기는 동안 원본을 되돌리기 기록 없이 목록에서 빼 두고 되돌리는 길은 그 단계 Esc 하나 · 같은 단계 Ctrl+Z = 되돌림 뒤 ESC 가 원본을 또 더해 같은 id 두 벌(추정) | `tl/move-tool.ts:216-226` · `tl/rotate-tool.ts:247` · `tl/scale-tool.ts:247` · `state.ts:288-296` · `run-command.ts:44-50` · `input-controller.ts:616-624` | 높음 | +| N2 | MOVE · COPY · ROTATE · SCALE · ARRAY 는 Esc 로 안 끝나고 한 번 뒤에도 고르기 단계로 돌아가 남음 — 다른 명령 · 리본 [선택]으로만 나감 | `move-tool.ts:122-124,172-174` · `copy-tool.ts:121-123` · `rotate-tool.ts:129-131` · `scale-tool.ts:134-136` · `array-tool.ts:113-115` | 중간 | +| N3 | Esc 처리 없는 단계 — RECTANG 첫 모서리 · TRIM 전부 · ARRAY 원형 각도 · ARRAY 방식 · 개수(고름만 풀고 그 자리) | `rectangle-tool.ts:55-69` · `eraser-tool.ts:62-74` · `array-tool.ts:165-167,193-195,267-277` | 중간 | +| N4 | PLINE · SPLINE · MLINE · WIPEOUT · REVCLOUD 그리던 중 Esc · Ctrl+Z = 그린 데까지 전부 버림 | `sequence-tool.ts:252-261,278` · `input-controller.ts:616-624` | 중간 | +| N5 | 숫자 단계(OFFSET · FILLET · CHAMFER · POLYGON · LENGTHEN · DIVIDE · MEASURE · DONUT 등)에 식 · 단위 = 앞 숫자만 조용히 먹음(`3.2*1.5` → 3.2) | `sequence-tool.ts:193-201,296-301` · `input-controller.ts:562-564` | 중간 | +| N6 | 명령행 칸 소문자 — 선 중 `c` = CIRCLE · `u` = UNDO · 배열 `r` = 직선 · `end` 등 스냅 이름 버림(캔버스 치기는 대문자로 바꿔 괜찮음) | `input-controller.ts:168-172,558-571` · `ime-sink.ts:29-34` · `line-tool.ts:118-131` · `array-tool.ts:150-157` | 중간 | +| N7 ⚠ | 명령 안 「객체 선택」 단계에서 이미 고른 객체의 그립(끝 · 가운데 · 꼭짓점) 10px 안을 누르면 그 명령이 그립 편집으로 바뀌어 사라지고 다음 누름이 그 점을 옮김(추정) | `sequence-tool.ts:321-337` · `move-tool.ts:103-121` · `select-tool.helpers.ts:55-72` · `grip-edit-tool.ts:26-30` · `draw.ts:56` | 높음 | +| N8 | 그립 편집 중 커서 곁 [길이][각도] 칸이 뜨나 길이 + Tab · Enter · 거리 숫자는 무시 | `grip-edit-tool.ts:64-80` · `input-controller.ts:514-517,605-607` | 낮음 | +| N9 | TRIM 은 누른 자리에서 멀어도 도면에서 가장 가까운 객체를 자름(집기 반경 없음) · 사각형을 자르면 변 넷이 지금 층 선으로 바뀜(추정) | `eraser-tool.ts:94-100,125-139` | 중간 | +| N10 | ELLIPSE 셋째(단축) 단계 숫자 = 장축 끝점에서 그만큼 간 점 → 단축 반지름이 친 값과 다름 | `basic-draw-tools.ts:99-104` · `sequence-tool.ts:136-141,185-190` | 낮음 | +| N11 | 되돌리기 두 길 — `U`(UNDO 별칭 · ↶ 단추) = 도구 안 끊고 되돌림만 · Ctrl+Z = 되돌림 + 도구 ESC + 고름 풂 · ↶ 단추 뒤 빈 Enter = UNDO 되풀이 | `commands.file.ts:100-108` · `input-controller.ts:616-624` · `run-command.ts:41` | 낮음 | +| N12 | 캔버스 치기에서 빈칸 = Enter → `10, 20` 은 `10,` 이 먼저 제출돼 버려짐 | `input-controller.ts:458-463` | 낮음 | +| N13 | 맥 ⌘V 는 붙여넣기로 안 가고 `V` 글자로 쌓임(추정) | `input-controller.ts:423-428,476-482` | 낮음 | +| N14 | 오토캐드 스냅 이름 중 없는 것 — `EXT` · `INS` = EXTEND · INSERT 가 돌아 지금 명령 버림 · `TAN` `QUA` `NOD` `APP` `PAR` `FROM` `NONE` = 조용히 버림 | `input-snap-override.ts:13-21` · `registry.ts:125-146` | 중간 | +| N15 | 커서가 앞 점과 같은 자리일 때 거리 숫자 = 방향 0 → 도구 오류 알림(추정) | `get-point-from-event.ts:37-42` · `state.ts:309-317` | 낮음 | +| N16 | 그리기 명령 중 두 번 누름 = 점 둘(LINE = 0 길이 선 · 추정) + 글자 위면 글 칸도 뜸 | `input-controller.ts:195-212` · `line-tool.ts:176-199` · `aislo-drawing-bridge.ts:113-118` | 낮음 | +| N17 | ARRAY 직선 「간격 시작점」에 `x,y` 를 치면 끝점 없이 바로 실행돼 복사 0 · 고름만 풀림 · 원형 각도 0 도 복사 0 | `array-tool.ts:201-206,324-360` | 낮음 | + +## 확인 못 함 + +- 화면 실측 0 — (추정) 줄 전부(H3 밀림 크기 · N1 객체 빠짐 · N7 그립 가로챔 · N9 · N13 · N15 · N16)는 화면에서 재현해 봐야 함. +- 한글 자판 캔버스 명령 단계 — 윈도 크롬 시험으로만 확인 · 맥 · 다른 브라우저에서 `key` 가 자모로 오는지 모름. +- F5 · F11 · F12 · Ctrl+W/T/N · 마우스 뒤 · 앞 단추가 iframe 안에서 브라우저에 어떻게 먹는지(부모 페이지를 떠나는지) 모름. +- Shift+휠 · 가로 휠 — deltaY 0 이면 줌 안 함까지만 확인 · 브라우저별 값 모름. +- xstate 액터를 이벤트 처리 도중 갈아 끼울 때(N7 · 그립 편집 시작) 오류 없이 넘어가는지 모름. +- E 표 「빠진 것」 = 오토캐드 기본 옵션 기억 기준 짐작 — 정확한 대조는 오토캐드 쪽 목록(다른 에이전트 · 코덱스 · 안티그래비티)과 맞대야 함. +- M02 카드 · 면적산출 패널의 숨김 CSS 가 상태막대 · 명령행을 어디까지 가리는지 = B2 9-1 옮김 · 화면 미확인. + From 43f3fa923ba7365e559b1ef42827194ee2baa913 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 15 Oct 2026 21:28:33 +0900 Subject: [PATCH 50/81] =?UTF-8?q?chore(=EC=A1=B0=EC=82=AC):=20336=20?= =?UTF-8?q?=EC=99=B8=EB=B6=80=20=EC=9D=BC=EC=9C=84=EB=8C=80=EA=B0=80=20?= =?UTF-8?q?=EB=AA=A9=EC=A0=81=20=EB=8B=A4=EC=8B=9C=20=EC=84=B8=EC=9B=80=20?= =?UTF-8?q?=E2=80=94=20=EC=9A=B0=EB=A6=AC=20=EC=82=AC=EC=A0=84=EC=A1=B0?= =?UTF-8?q?=ED=95=A9(=EB=A1=9C=EC=A7=81=20+=20=EC=98=B5=EC=85=98=20+=20?= =?UTF-8?q?=EC=88=98=EB=9F=89)=EC=9C=BC=EB=A1=9C=20=EC=98=AE=EA=B9=80=20?= =?UTF-8?q?=C2=B7=20=ED=99=95=EC=9D=B8=ED=95=9C=20=EC=A4=84=EB=A7=8C=20?= =?UTF-8?q?=C2=B7=20=EB=B3=84=EC=B9=AD=20=EC=9E=A5=EB=B6=80=20=C2=B7=20?= =?UTF-8?q?=EC=98=B5=EC=85=98=20=EA=B1=B0=EA=BE=B8=EB=A1=9C=20=EC=B0=BE?= =?UTF-8?q?=EA=B8=B0=20=EC=8B=A4=EC=B8=A1=20=C2=B7=20=EB=AC=B6=EC=9D=8C=20?= =?UTF-8?q?A~F=20=C2=B7=20=EA=B8=B0=EB=B3=B8=EC=95=88=20=EB=8B=A4=EC=84=AF?= 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_01C6BNBFXFndbqYxu6sZuknK --- .../2026-10-15_336_목적다시_외부일위대가.md | 323 +++++++++++ ...6-10-15_336_부록4_목적흐름_옵션실측원문.md | 517 ++++++++++++++++++ 2 files changed, 840 insertions(+) create mode 100644 docs/raw/조사/2026-10-15_336_목적다시_외부일위대가.md create mode 100644 docs/raw/조사/2026-10-15_336_부록4_목적흐름_옵션실측원문.md diff --git a/docs/raw/조사/2026-10-15_336_목적다시_외부일위대가.md b/docs/raw/조사/2026-10-15_336_목적다시_외부일위대가.md new file mode 100644 index 000000000..76856fc33 --- /dev/null +++ b/docs/raw/조사/2026-10-15_336_목적다시_외부일위대가.md @@ -0,0 +1,323 @@ +# 외부 일위대가 가져오기 — 목적 다시 세움 (요구 227-4 · 228 · PLAN 336-4) + +재료 = K4(`2026-10-15_336_부록4_목적흐름_옵션실측원문.md` 1 절)(지금 흐름 · 별칭 자리 · 설계 안) · K5(`2026-10-15_336_부록4_목적흐름_옵션실측원문.md` 2 절)(로직 + 옵션 거꾸로 찾기 실측) · 336 재검토 `docs/raw/조사/2026-10-15_336_재검토_로직조합맞춤.md` · 요구 장부 223-5 · 224-5~6 · 225~228 원문(`docs/raw/요구_1010_2/사용자요구_1010_2.md`) · 브레인 정함. 수치 · 파일:줄은 재료 그대로(새로 안 잼) · 재료끼리 · 틀과 어긋나는 곳 = 「⚑」 · 파일 자리는 따로 적지 않으면 `M02_MasterTemplete/` 아래 · 줄 = K4 기준 9c0ef7f82(M02 맞춤 코드는 42e4aa70c · 지금 HEAD 3656ebcdf 와 같음) · 저장소 고침 없음. + +## 0. 결론 + +1. 목적 = 남이 만든 실무 엑셀의 일위대가를 「우리 로직 + 옵션 + 수량」으로 된 우리 일위대가(사전조합)로 쉽게 옮기기 · 값은 늘 우리 로직이 셈. +2. 한 번 고른 짝(엑셀 이름 → 우리 로직 + 옵션)은 별칭 장부에 남아 다음 문서에서 바로 제안됨 · 맞춤 정밀도는 사람이 고를 줄을 줄이는 수단일 뿐(자동 저장 아님). +3. 사용자 말(228)이 수치로 맞음 — 엑셀의 서로 다른 줄 177 중 102(58%)는 우리 로직에 옵션만 고르면 엑셀 품이 그대로 나옴 · 36(20%)은 엑셀이 셈을 덧붙인 것 · 우리 로직이 품셈 원문과 다른 줄은 0. +4. 그래서 판정은 금액이 아니라 「어느 로직 + 어느 옵션」 — 로직은 직종 · 품 · 장비 · 조항으로 고르고, 옵션은 고른 로직에서 거꾸로 찾음 · 엑셀이 바꾼 품은 「다른 점」으로 보이기만. +5. 지금 코드에 뼈대는 있음(올리기 → 맞춤 → 고르기 → 기억 → 사전조합) · 가장 큰 어긋남 = 「같음」 · 「기억」 줄과 [로직 추가] · 엑셀 보기 창 길이 사용자 확인 없이 들어감. +6. 새 규칙 = 맞춤은 제안만 · 들어가는 것은 사용자가 확인한 줄만 — 「맞음」 · 「별칭」 줄은 체크된 채 보이고 호표마다 [확인하고 사전조합 만들기] 한 번 · 그때 별칭 장부에 씀. +7. 「우리에 없음」은 프로그램이 안 정함 — 사용자가 엑셀 블록 · 품셈 원문을 보고 판단(대개 이름 바뀐 우리 로직) · 새 로직은 사용자 확인 뒤 M01 에서. +8. 우리 로직 · 근거가 기준 — 가져오기에서 안 고침 · 원문과 다르다는 근거가 나오면 사용자 확인 뒤 따로 고침(지금까지 0 건). +9. 차례 = A 확인 규칙 + 확인 창 → B 인용 조항 맞대기 손질 → C 별칭 장부 + 관리 화면 → D 로직 지문 + 옵션 거꾸로 찾기 → E 한 줄 여러 로직 그릇 → F STmate · 거창 읽기(때는 사용자). +10. 물을 것 다섯은 기본안으로 정해 7 장에 적음 — 바꾸려면 말씀. + +--- + +## 1. 목적 + +### 1-1. 뿌리 — 223-5 (10-14 원문) + +- 북부청 공통단가 xls 일위대가 탭 = 「실무자들이 주로 사용하는 일위대가의 전체를 모와둔 자료」 · 「사용자들이 구조물조합을 구성하기위해 반복적으로 검색과 옵션을 선택해야하는 어려움」을 덜려는 것. +- 「구조물조합의 일위대가의 로직추가할 경우 해당 일위대가 실무자료에 있는 호표항목을 불러올수 있는 기능」. +- 「매칭이 되는 경우에는 엑셀의 단가산출 이름을 일위대가의 별칭으로」 · 「옵션값은 규격열에 명기」 · 「단위가 상이한 경우 … 단위변환」. +- 「다른 사람들이 가지고 있는 실무문서의 내용을 가져와 우리의 일위대가로 만들고 싶음」. +- 덧(224-5 · 224-6) — 엑셀처럼 보이는 창에서 호표를 골라 한 번에 불러오기 · 이름만이 아니라 단가산출 탭의 계산식 · 출처를 참조해 맞춤. + +### 1-2. 이번 말 (225-4 → 228) + +- 225-4 — 이름이 달라 매칭이 안 되는 문제 → 맞춤 정밀도. +- 226-4 — 우리 단가산출 로직은 정상 · 바꿀 생각 없음 · 「중요한건 값이아니라 어떤 로직의 조합인지와 로직」 · 「한번 한 것들은 기억」 → 판정 = 로직(조합) · 기억. +- 227-4 — 우리가 기준 · 원문이 맞을 때만 고침 · 「딱 맞음이라도 … 확인은 사용자가」 · 우리에 없는 로직은 원문을 보고 「나에게 확인」 · 「발주처 구분은 의미 없음 · 관리는 사용자 몫」 · 「자동이 안될경우는 사용자가 선택하게 하되 별칭으로 남겨 일위대가항목을 쉽게 반영」 → 확인 · 별칭 · 우리에 없음 · 관리. +- 228 — 엑셀 일위대가 = 「우리 로직의 옵션을 지정 후 나온값일 확율」 · 「값보다는 로직」 → 판정 = 로직 + 옵션. + +### 1-3. 목적 한 줄 (브레인 정함) + +사용자가 가진 실무 문서의 일위대가를 우리 일위대가(사전조합 = 우리 로직 + 옵션 + 수량)로 쉽게 옮김 · 값은 우리 로직이 셈 · 한 번 고른 짝(엑셀 이름 → 우리 로직 + 옵션)은 별칭으로 남아 다음 문서에서 바로 제안됨 · 맞춤 정밀도는 사람이 고를 줄을 줄이는 수단(자동 저장 아님). + +### 1-4. 지킬 성질 다섯 + +| | 성질 | 근거 말 | +|---|---|---| +| ㉠ | 로직 + 옵션이 맞음 — 금액은 판정 밖 | 226 · 228 | +| ㉡ | 들어가는 것은 모두 사용자가 확인 — 「맞음」은 표시만 | 227 | +| ㉢ | 익숙한 이름이 남음 — 엑셀 이름이 줄 별칭으로 | 223 · 227 | +| ㉣ | 한 번 고른 짝은 다음에 쉽게 — 별칭 장부 | 226 · 227 | +| ㉤ | 우리 로직 · 근거가 기준 — 가져오기에서 안 고침 | 226 · 227 | + +### 1-5. 쉬운 예 — 엑셀 한 줄이 옮겨지는 길 (D 까지 된 뒤 모습) + +1. 엑셀 줄 「레미콘타설(장비사용) · 철근구조물-소량 타설」. +2. 맞춤 → 우리 GC000198(레디믹스트콘크리트 타설) + 옵션 「장비사용타설 · 철근구조물 · 소량감률 50」 → 우리 품 = 엑셀 품(콘크리트공 0.1091 · 보통인부 0.0364 · 굴착기 0.29 hr) → 「맞음」 체크된 채 보임. +3. 사용자가 보고 [확인하고 사전조합 만들기] → 사전조합 줄 {GC000198 · 옵션 · 수량 · 별칭 = 엑셀 이름} + 별칭 장부 한 줄. +4. 다음 문서에 같은 이름 줄 → 「별칭」 체크된 채 → 확인 한 번. + +- 지금 코드는 규격 칸만 읽어 「소량」을 놓침 → 옵션이 빠져 반값으로 셈(K5 1-5). + +### 1-6. 일의 무게 + +- 사람 손이 드는 곳 = 고유 줄 — 정답 세 파일 엑셀 504 줄 = 고유 136(336) · 같은 줄은 한 번 고르면 같은 파일 · 다음 파일에서 별칭으로 뜸. +- 지금도 후보 다섯 안에 정답 86.5% → 남은 몫은 「1순위 정밀도」보다 「빠르고 바른 확인 + 고른 것 다시 씀」. + +--- + +## 2. 지금 흐름 대 목적 + +### 2-1. 지금 흐름 (K4 1-1 줄임) + +올리기(좌측 「일위대가 실무자료」) → 엑셀 보기 창 → 호표 표(맞춤 n/m) → 줄 맞춤(% 줄 · 빼기 · 다른 호표 · 기억 · 같음 · 후보 · 없음) → 고르기 창(후보 5 · 근거 꼬리표 · [엑셀에서 보기] · [다른 로직 찾기]) → 확정(누를 때마다 바로 서버 씀 · `{층}/실무자료/맞춤.json`) → [사전조합 만들기](`{층}/precombo/SC-*.json` · 줄 = 로직 · 입력값 · 별칭 = 엑셀 품명 · 수량) → 구조물 일위대가 · B09 · B08. + +- 값은 이미 우리 엔진이 셈(`M02_Labor_Ref.Expand`) · 엑셀 금액은 안 받아 적음 — 「로직 + 옵션으로 남겨 우리가 셈」 꼴은 이미 있음. +- 위 층(회사 · 마스터) 파일 줄은 일반 사용자가 자기 층으로 확인할 길 없음 — [로직 추가] · 엑셀 보기 창으로 「자동 줄만」 만들기뿐. + +### 2-2. 단계 표 — 있는 것 · 빈 곳 · 어긋남 (K4 3-2) + +| 목적 단계 | 있는 것 | 빈 곳 | 어긋남 | +|---|---|---|---| +| 문서 불러오기 | 북부청 꼴 xls · xlsx · 블록 출처 · 직종 · 엑셀 보기 창 · 층 저장 | STmate 꼴(실무 내역 9 중 6 이 올리기부터 막힘) · 문서 보안 2 · 거창 줄 꼴 · 블록 품 · 조건 글을 안 남김 | — | +| 맞춤 | 이름 같음 · 씨앗 · 인용 조항 · 직종 · 규격 → 입력값 · 환산 | 품 견주기 없음(「어느 로직 + 어느 옵션」을 숫자로 못 가름) · 한 줄 = 로직 여럿 못 담음 | 이름만 같으면 「같음」 — 고유 33 중 다른 책 10(336) | +| 「맞음」 표시 · 확인 | 상태 · 근거 꼬리표 · 줄 [확정] · 모두 확정 단추 둘 | 넣음 체크 · 「확인 대기」 | 「같음」 · 「기억」 · 「호표」 줄이 확인 없이 들어감(환산 정해진 「같음」 25 중 다른 책 5) · [같음 모두 확정]은 안 눌러도 같음이 들어가 뜻 없음 · [로직 추가] · 엑셀 보기 창은 줄을 안 보이고 만듦 | +| 사용자가 고름 | 고르기 창 · 후보 5 · [엑셀에서 보기] · [다른 로직 찾기] | 옵션 칸 · 우리 품 ↔ 엑셀 품 · 원문 보기 · 찾기에 조항 · 낱말 미리 넣기 · 위 층 파일을 내 층에서 고르기 | — | +| 별칭으로 남김 | 줄 별칭 = 엑셀 이름 · 기억 = 로직 + 입력값 + 환산 | 보기 · 고치기 · 지우기 화면 · 원래 글 · 누가 · 파일 · 열쇠가 좁음(규격 글이 조금만 달라도 안 걸림) | 기억이 다음 파일에서 확인 없이 들어감(위 층 · 남이 확정한 것까지) | +| 일위대가에 반영 | 호표 → 사전조합 · [로직 추가] 실무 호표 · B09 · B08 단가연결 | [로직 더하기] 찾기에서 익숙한 이름으로 로직 + 옵션 한 번에 | 확인 안 한 줄이 섞인 사전조합이 서고에 남아 여러 구조물 · B09 가 같이 씀 | +| 우리에 없음 | 「없음」 상태 · 빠진 줄 표 | 원문 보기 · 「우리에 없음」 표시 · 까닭 | 엉뚱한 후보가 1순위로 보임 | +| 우리 로직이 원문과 다름 | 원문 보기 부품(M01 `M01_MasterData_UI_LogicLab_Source.ts` · M02 길 `/m02/logic-lib/source`) | 맞춤 화면에서 안 씀 | 없음(맞춤은 우리 로직을 안 고침) | +| 관리 · 발주처 | 층(개인 · 회사 · 마스터) | 관리 화면 | 앞 안(336 재검토 N4 발주처 칸 · ④ 같은 발주처 기억은 바로)이 227 과 어긋남 → 뺌 | + +### 2-3. 「별칭」 자리 — 셋 + 씨앗 + +| 자리 | 담는 것 | 이번 쓰임 | +|---|---|---| +| (가) M01 로직 별칭 | `resources/master_data/로직_*.json` `별칭` 칸 · 이름만 · 찾기용(요구 66) · 시스템 관리자 손 · 지금 1,469 중 0 | 엮지 않음(사람이 원할 때 손으로) | +| (나) M02 맞춤 기억 | `{층}/실무자료/맞춤.json` · 품명 \| 규격 \| 단위 → 로직 \| 자재 \| 빼기 + 입력값 + 환산 + 때 · 보는 화면 없음 | **별칭 장부로 키움(C)** | +| (다) 일위대가 줄 별칭 | 사전조합 · 일위대가 문서 줄 `별칭`(≤ 40 자 · `M02_Labor_Doc.py:4 · 57`) = 만들 때 엑셀 품명 | 지금처럼(됨) | +| ⚑ 씨앗 | 일위대가조합.json 메모(실무 이름 80 → 로직 · 읽기만 · `M02_Hopyo_Match._exact :286-297`) — 지금 「같음」으로 확인 없이 들어감 | K4 세 자리 밖 넷째 이름 짝 · 원본 그대로 두고 씨앗 줄도 숫자 판정대로 보이고 확인(씨앗 짝 31 줄 중 옵션으로 맞는 줄 6 · K5 0-10) | + +- 셋은 서로 안 이어짐 — 맞춤은 (가)를 안 읽음(`M02_Hopyo_Match.py:16`) · (나) 확정이 (가) · (다)로 안 감 · 사전조합에서 고친 별칭 · 옵션 · 수량이 (나)로 안 돌아감. + +--- + +## 3. 로직 + 옵션 — 228 수치 (K5) + +### 3-1. 사용자 말 맞음 — 고유 줄 177 갈래 + +대상 = 사람 정답 126(북부청 임도 49 · 사방 43 · 영월 34) + 새 파일 51 · 엑셀 줄 무게 617 · 정답 로직의 입력 칸(옵션) 조합을 M01 엔진으로 모두 돌려 엑셀 품(직종 품 · 장비 시간 · 자재 수량)과 견줌 · 금액 안 씀. + +| 갈래 | 줄 | 뜻 · 예 | +|---|---|---| +| 옵션만 고르면 엑셀 품 그대로 | **102 (58%)** · 줄 무게 57% | 레미콘 「소량」 → 소량감률 50 | +| 우리 로직에 엑셀이 셈을 덧붙임 | **36 (20%)** | 섞음(인력 10% 곱함 · 비율 바꿈) 12 · 옵션 둘 합 · 평균 5 · 할증 · 횟수 4 · 단위 환산(÷ 6 m/본) 4 · 묶음 5 · 직종 바꿈 · 쪼갬 5 · 자재만 1 | +| 숫자 자체가 다름 | **33 (19%)** | 옛 판 값 15 · 다른 책 숫자 15 · 다른 로직 숫자 3 | +| 장비만 줄 · 품 없는 로직 | 4 · 2 | 브레이커 시간 · P.V.C 파이프(재료비 + 설치비 5%) | + +- 앞 둘 합 = 78%(줄 무게 82%) — 엑셀 일위대가는 거의 「우리 로직 + 옵션(+ 엑셀 쪽 덧셈)」. +- ⚑ 102 = 허용 「R(0.5% + 엑셀 반올림 폭) 또는 3%」 기준 · 3-3 의 69 · 25 · 81 은 R 기준(R 만이면 맞음 94 = 53%) — 같은 실측의 다른 허용 칸. + +### 3-2. 우리 = 원문 — 원문과 다른 우리 로직 0 + +- 숫자가 다른 줄의 14 조항을 품셈 원문(`resources/knowledge/original/원가계산/`)과 대조 → 13 조항은 우리 = 지금 원문 · 1 조항(건설 6-7-2 파형강관)은 둘 다 원문(엑셀은 본당 → m 당 ÷ 6 환산만). +- 엑셀이 다른 까닭 — 원문에 '24~'26 보완 표시가 있는 9 조항 = 엑셀이 보완 전 옛 값(6-2-3 철근 조립 · 7-2-2 찰붙임 · 2-6-1 동바리 · 2-7-1 비계 · 3-5-1 절토면 고르기 · 6-1-1 레미콘 · 6-7-1 플륨관 · 4-1-1 잔디 · 산림 13-3-2 기초다짐) · 산림 경사 할증 × 1.2 를 건설 포장(1-6-2)에 붙임 · 다웰바(6-3-10) 철근공 더함 · 문양거푸집(6-3-4) 자재 × 1/0.72 · 기계비빔(6-1-2) 엑셀 숫자만 다름. +- 예 — 찰붙임 석공: 원문('26 보완) 0.10 = 우리 · 엑셀 0.05. +- 대조 못 함 — 「다른 책 숫자」 15 줄의 옛 건설 조항(3-1-3 · 9-1 · 9-3 · 9-7 — 지금 판에 그 번호 없음) · 장비만 4 줄. +- 곁 관찰(고침 제안 아님) — GC000211 · GC000214 「높이감률」 자동(견본)값 1 → 품 1% 줄어듦 · 옵션 찾기에선 0 을 골라야 맞음. + +### 3-3. 옵션이 하나로 정해지나 (R 기준 · 177) + +| 판정 | 줄 | 갈리는 것 | +|---|---|---| +| 하나로 정해짐 | 69 | — | +| 여럿(같은 품) | 25 | 이름만 다른 옵션 11(철근 「토목 / 건축」 등) · 엑셀이 안 적은 몫 11(합판 측압 · 폼타이 9 · 펌프차 · 크레인 기종 2) · 1% 차 옵션 3 | +| 없음 · 못 셈 | 81 · 2 | 3-1 의 덧셈 · 숫자 다름 · 장비만 줄(+ 3% 허용이면 맞는 8) | + +- 품에 아예 안 닿는 칸(물빼기파이프 · 모래 · 자갈상태 · 강도 · 슬럼프 — 15 줄)은 엑셀 자재 이름 · 규격 글로 가림. +- 「여럿」은 사용자가 옵션 칸에서 고름 · 프로그램은 「측압 · 폼타이규격 못 가름」 꼬리표만. + +### 3-4. 옵션 찾기의 값어치 = 입력값 채우기 + +맞은 94 줄(R) = 기본값 그대로 36 · 지금 규격 읽기(`M02_Hopyo_Match.spec_inputs :145-178`)로 더 12 · **옵션 찾기로만 46**(레미콘 「소량」 → 소량감률 50 · 합판 「4회」 → 유형 보통). + +- ⚑ 223-5 「옵션값은 규격열에 명기」는 절반만 맞음 — 46 줄은 조건이 단가산출 블록 글에만 있음 → 블록 글 · 품을 읽어야 옵션이 채워짐. + +### 3-5. 로직 고르기 ≠ 옵션 찾기 (사람 126 · 1순위 엄격) + +| 방법 | 1순위 | 다섯 안 | +|---|---|---| +| 지금 코드 | 71.4 | 86.5 | +| 품만(로직 + 옵션 · 딱 맞음 먼저) | 60.3 | 76.2 | +| 로직 지문(직종 · 품 · 장비 · 조항 · 이름 덤) | 81.7 | 93.7 | +| 지문 + 딱 맞음 먼저 | 81.0 | 93.7 | +| 지문 + 인용 조항 맞대기 손질(B) | 88.9 | 96.8 | +| + 한 줄 여러 로직 자동 풀기(E 뒤 반) | 92.9 | 96.8 | + +- 품만으론 로직을 못 고름 — 「없음」 줄에선 품이 닮은 엉뚱한 로직이 1위 · 보통인부 하나뿐 줄은 같은 품 로직이 수십. +- 그래서 로직은 지문(+ B)으로 줄 세우고 → 그 로직에서 옵션을 거꾸로 찾음 → 1위 로직이 맞은 103 줄 중 옵션까지 엑셀 품 그대로 56(앞 실험의 64 조합으론 37) · 새 파일 38 중 19(8). + +### 3-6. 화면 표시 기준과 수치 (K5 4) + +| 표시 | 조건 | 사람 126: 줄 · 틀림 | 새 파일 51: 줄(엑셀 줄) · 틀림 | +|---|---|---|---| +| 맞음 | 1위 (로직 + 옵션) 품 = 엑셀(R) · 엑셀 몫을 다 덮음 · 숫자 하나뿐 줄 아님 | 58(로직 하나 46 + 고름 12) · **1** | 21(64) · 2 | +| 다른 점 | 1위 로직 직종 묶음 = 엑셀 · 엑셀 인용 조항 = 그 로직 조항 · 품만 다름 | 25 · **1** | 11(32) · 1 | +| 후보 | 그 밖(거의 맞음 1 포함) | 43 · 22 | 19(50) · 11 | + +- 틀린 5 줄 — 맞음 S084 펌프카(숫자 = 산림 GF000182 · 정답표 = 건설) · C020 합판 6회(갱폼과 우연히 같음) · 고름 C017 잡철물 · 다른 점 Y025 콘크리트소운반(옛 9-1 식) · U002 콘크리트비빔 → 「맞음」 틀림 3 중 2 는 정답표 · 짝 쪽이 숫자와 어긋난 줄(다시 볼 거리). +- 다른 점 조건 — 직종만 같음이면 39 줄 · 틀림 16 → 인용 조항까지 같음으로 25 · 1. +- ⚑ 앞 문서 「딱 맞음」(C4 · 직종 · 품 3% · 점수 차 · 장비 몫 안 따짐) ≠ 이 문서 「맞음」(옵션까지 · 엑셀 몫 다 덮음) — 둘 다 45 · K5 만 13(옵션을 찾아야 맞는 줄) · C4 만 11(토질 평균 · 옛 판 · 섞음 밑품 등) → 이 문서는 K5 「맞음」을 씀. +- 씨앗 짝 31 줄은 옵션으로 맞는 줄 6 뿐(엑셀 숫자가 다른 책 · 옛 판) → 「맞음」 대신 「다른 점 · 후보」로 보이는 것이 맞음. + +### 3-7. 쉬운 예 + +| 엑셀 줄 | 갈래 | 화면에 보일 것 | +|---|---|---| +| N000 레미콘 소량 | 옵션만 | 「맞음 — GC000198 · 장비사용타설 · 철근구조물 · 소량감률 50」 | +| N001 합판 4회 | 옵션만(여럿 · 같은 품) | 「맞음 — GC000211 · 유형 보통 · 4회」 + 「측압 · 폼타이규격 못 가름」 | +| 구조물터파기(인력100%) 토사 | 엑셀 덧셈(옵션 셋 평균) | 「다른 점 — GC000083 · 특별인부 0.370 → 0.354(토질 보통 · 경질 · 고사점토 및 자갈섞인 토사 평균)」 → 토질은 사용자가 고름 · 우리 품으로 셈 | +| 고임돌 | 엑셀 덧셈(섞음) | 「GF000211 막자갈 0.3 × 인력 10% = 0.03」 꼬리표 | +| Y014 찰붙임 | 숫자 다름(옛 판) | 우리 = 원문('26 보완) 석공 0.10 ↔ 엑셀 0.05 · 우리 품으로 셈 | + +--- + +## 4. 화면 흐름 + +### 4-1. 줄 상태 + +| 상태 | 뜻 | 처음 체크 | 사용자가 할 일 | +|---|---|---|---| +| 맞음(초록) | D 전 = 이름 같음 또는 근거 맞음(인용 조항 · 직종 · 단위 · B 뒤 바르게) / D 뒤 = 1위 (로직 + 옵션) 품이 엑셀과 같음(3-6) | 체크(확인 대기) | 보고 확인 | +| 별칭(파랑) | 전에 확인한 짝 · 출처 꼬리표(층 · 때 · 처음 파일) | 체크 — 「규격 다름」 · 「근거 다름」 · 숫자 판정이 다른 로직이면 체크 안 함 | 보고 확인 · 옵션 확인 | +| 다른 점 | D 뒤 · 로직은 거의 확실(25 줄 중 틀림 1) · 품만 다름 + 까닭 꼬리표 | 안 함 | 옵션 고르고 체크 | +| 후보 | 그 밖 · 후보 다섯 | 안 함 | [고르기] | +| 뺌 | 사용자가 뺌 · 까닭 「우리에 없음」 · 「안 씀」 | — | — | +| 갈래 그대로 | % 줄(우리 덧줄이 셈) · 다른 호표(그 호표 확인이 먼저) · 노임 줄(그릇 없음) | — | — | + +- 틀 = 「맞음 · 별칭은 체크된 채」 · 「다른 점」 체크 안 함 · 「별칭인데 숫자 판정이 다른 로직이면 체크 안 함」은 그 틀에 맞춘 이 문서의 안(K4 5-2 · K5 0-10 을 이음). +- 「맞음」 뜻이 D 전후로 바뀜 — D 전엔 이름 · 근거(조항 · 직종 · 단위)뿐 · D 뒤에 품 · 옵션까지. +- 꼬리표 예 — 「품 다름 특별인부 0.370 → 0.354(엑셀이 바꿈)」 · 「옵션 — 규격에서 / 블록에서 / 별칭에서 / 자동값」 · 「근거 건설 3-2-1 · 특별인부」 · 「별칭 — 회사 층 · 2026-10-15 · 북부청 임도」. +- 금액 비는 안 보임 · 만든 뒤 값 비교는 「참고」 한 줄(판정 · 상태에 안 씀). + +### 4-2. 확인 — 맞춤은 제안 · 넣는 것은 사용자 + +- 한 줄 — 체크 · [고르기](후보 · [다른 로직 찾기]는 엑셀 조항 번호 · 낱말을 미리 넣음 — M01 찾기는 원문번호로도 걸림) · [옵션](로직 입력 창 다시 씀) · [원문 보기] · [엑셀에서 보기] · [뺌](까닭). +- 호표 하나 — [맞음 · 별칭 모두 체크] → **[확인하고 사전조합 만들기] 한 번** = 체크한 줄만 넣음 · 그때 별칭 장부에 씀 · 체크 안 한 줄 = 빠진 줄(까닭 「확인 안 함」). +- 여러 호표 — 엑셀 보기 창 [고른 n개 확인하고 만들기] = 호표마다 넣을 줄 · 빠질 줄을 펼쳐 보인 뒤 한 번 누름. +- [로직 추가] 「실무 호표」 — 만들 층에 확인한 사전조합이 있으면 그것을 참조 · 없으면 같은 확인 창을 모달로 → [확인하고 만들기] → 참조 줄. +- 다른 호표(일위N) 줄 — 그 호표가 먼저 확인돼 있어야 들어감. +- 누가 — 만들 층 쓰기 권한(개인 = 본인 · 회사 = 회사 관리자 · 마스터 = 시스템 관리자) · 파일 층은 읽기만 있으면 됨 → 위 층 파일도 내 층으로 확인 · 만들기. +- 없앰 — 줄 [확정]의 즉시 서버 쓰기 · [같음 모두 확정] · [근거 맞음 모두 확정] → 고름 · 체크 · 옵션은 화면 캐시. +- 서버는 받은 짝(로직 키 · 입력값 이름 · 배)을 다시 검사하고 셈 · 브라우저가 보낸 금액 · 품을 받아 적지 않음. +- 사전조합이 곧 「이 호표 확인함」 기록 — 확인 저장소를 따로 안 둠 · 이미 만든 실무자료 사전조합은 그대로(다시 만들 때 확인 창 · 지금 자료 = 개인 층 둘 · 실무자료 셋 · 맞춤.json 2 줄). + +### 4-3. 별칭 장부 — 관리는 사용자 몫 + +- 바탕 = 지금 맞춤 기억(`{층}/실무자료/맞춤.json` · `M02_Hopyo_Store.py:123-140`)을 키움. +- 한 줄 = 엑셀 품명(원래 글) · 규격 · 단위 → 짝 [{우리 로직 \| 자재, 옵션, 배}](여럿 가능) 또는 「우리에 없음」 · 「안 씀」 + 확인한 이 · 때 · 처음 파일. +- 열쇠 = 지금 그대로(품명 \| 규격 \| 단위 정규화) · 찾을 때만 품명 \| 단위로 넓힘(「별칭 · 규격 다름」 · 체크 안 함) · 품 꼴 열쇠는 D 뒤. +- 쓰는 때 = [확인하고 사전조합 만들기] · 장부 화면 [저장] 때만 · 확인한 줄은 모두 씀(이름 같음이라도 — 옵션 · 배를 남기려고) · 같은 열쇠는 덮음. +- 층 = 확인한 층에 씀 · 읽는 차례 개인 → 회사 → 마스터(지금 기억과 같음). +- 화면 = M02 좌측 「일위대가 실무자료」 판 안 [별칭 장부] → 본 칸 표(엑셀 품명 · 규격 · 단위 | 우리 로직 · 옵션 · 배 | 확인한 이 · 때 · 처음 파일 | 층 — 위 층은 읽기만) · 찾기 · 갈래 거름(짝 · 우리에 없음 · 안 씀) · [고치기](로직 다시 고르기 · 옵션 · 배) · [지우기] · 위 층 줄 [내 층으로 고쳐 씀] · 고친 것은 캐시 → [저장] 때만. +- 안 둠 — 발주처 칸 · 자동 올림 · 자동 합침 · 쓰인 횟수로 자동 고름 · 「처음 파일」은 보기용 글(판정에 안 씀). +- 기대 — 336 재검토 ④ 맨 위 몫 = 사람 손: 바로 쓸 수 있던 기억 24 줄 모두 맞음 · 그중 20 줄(엑셀 93 줄)은 「후보」라 사람이 골라야 했던 줄 → 이제 「별칭」 체크된 채 확인 한 번(바로 넣지 않음). + +### 4-4. 우리에 없음 — 프로그램이 안 정함 + +- 가까운 후보는 늘 나옴(「우리에 없음」 10 줄 중 10 줄에 후보 · 336) · 점수 문턱으로 숨기기는 무익 → 사용자 판단. +- 확인 창 도구 — [엑셀에서 보기](그 단가산출 블록) · [원문 보기](엑셀 인용 조항 → 품셈 본문 절) · [다른 로직 찾기](조항 번호 · 낱말 미리 넣음). +- 판단 셋 — ① 이름 바뀐 우리 로직(대개 — 227) → 고름 → 별칭 ② 정말 없음 → [뺌 · 우리에 없음] → 장부 「우리에 없음」 줄(다음 파일에서도 그 표시) ③ 넣어야 할 로직 → 사용자 확인 뒤 시스템 관리자가 원문을 보고 M01 [복사해서 만들기](자체 로직 GX · 정본 안 건드림) → 다음 확인에서 고름. +- 가져오기 흐름에선 로직을 만들지 않음 · 개발 · AI 도 같음(원문에서 이름 바뀐 같은 일을 먼저 찾아 보고 → 사용자 확인 뒤만 M01). +- 예(판단은 사용자) — 잔토처리 · 잔토정리(정답 세 파일 「우리에 없음」 10 중 6): 원문 건설 3-2-1 굴착 비고 「현장 내에서 소운반하여 깔고 고르는 잔토처리는 ㎥당 보통인부 0.2인을 별도 계상」(`resources/knowledge/original/원가계산/건설공사_표준품셈/본문/01_공통부문/제03장_토공사/3-02_굴착.md:26`) = 굴착의 비고 · 우리는 GC000083 입력 「잔토처리」로만 둠 → 단독 줄로 쓰려면 새 로직이 필요할 수 있음 = 사용자 판단 거리 · 고화매트 설치 = 원가계산 원문에서 낱말 못 찾음. + +### 4-5. 우리 로직이 원문과 다를 때 + +- 기본 = 우리가 기준 — 엑셀 품이 다르면 「다른 점」 꼬리표만 · 우리 품으로 셈 · 가까운 옵션은 사용자가 고름. +- 우리가 틀렸다고 보일 때만 — [원문 보기]로 원문 절(엑셀 인용 조항 · 우리 로직 출처 둘 다) → 사용자가 「우리가 틀림」 확인 → 시스템 관리자가 M01 에서 따로 고침(가져오기 흐름 밖 · PLAN 에 원문 근거 인용) → 확인 창이 다시 셈. +- 맞춤 화면에 「우리 로직 고치기」 단추 없음 · 지금까지 0 건(3-2). + +### 4-6. 자료 · 계산 자리 (CLAUDE.md 5장) + +- 초기값 = 올린 엑셀 읽은 결과(HP 파일 · D 뒤 블록 품 줄 · 지문 포함 · 올릴 때 한 번 · 안 바꿈) · 작업본 = 사전조합 · 별칭 장부([확인하고 사전조합 만들기] · 장부 [저장] 때만) · 캐시 = 고름 · 체크 · 옵션(조작 즉시 · sessionStorage). +- 계산 = 서버 단독(③) — 맞춤 · 지문 · 옵션 찾기 · 만들기 · 화면은 보이기만 · 옵션을 바꾸면 칸은 바로 · 품은 서버가 뒤에서(최신 응답만). +- 자동 저장(디바운스 PUT) 없음 · 화면 파일 `M02_MasterTemplete_Hopyo_View.ts` 614 줄(한도 700) → 확인 창 · 장부 화면은 새 파일. + +--- + +## 5. 묶음 · 차례 + +### 5-1. 차례 표 + +| 묶음 | 무엇 | 크기 | 먼저 할 것 | 앞 묶음 짝(336 재검토) | 기대(사람 126 · 씨앗 켬 · 1순위 엄격) | +|---|---|---|---|---|---| +| **A** 확인 규칙 + 확인 창 | 들어감 = 확인한 줄만 · 넣음 체크 · 옵션 칸 · 품 비교 · 원문 보기 · 미리 넣은 찾기 · [확인하고 사전조합 만들기] · [로직 추가] · 엑셀 보기 창 길도 같은 확인 | 중간 | — | (새) 확인 규칙 · ⑥ 고르기 창 → 확인 창 · N2 → 체크까지 | 순위 그대로 · 확인 없이 들어가던 줄 0 | +| **B** 인용 조항 맞대기 손질 | 조항 윗마디 · 오타 · 같은 이름 접힘 · 「같음」도 근거 검사(표시에만) | 작음 ⚑ | — | ① 근거 고침(이름만 바꿈) | 71.4 → 85.7 · 다섯 안 86.5 → 94.4 · 「같음」 다른 책 10 → 0 · 새 파일 씨앗 짝 41.9 → 53.5 | +| **C** 별칭 장부 + 관리 화면 | 맞춤 기억 키움 · 덧칸 · 느슨한 찾기 · 「로직 여럿」 꼴 · 장부 화면 | 중간 | A(확인 때 씀) | ④ 조건부 기억(바로 들어감 · 발주처 뺌) · N4 뺌 | B 위 85.7 → 87.3 · 고를 줄 20(엑셀 93) → 별칭 체크 | +| **D** 로직 지문 + 옵션 거꾸로 찾기 | 블록 품 줄을 HP 파일에 남김부터 · 우리 로직 지문 캐시 · 맞음 / 다른 점 / 후보 · 옵션 찾기로 입력값 | 중간~큼 | B(조항 차례가 지문의 덤) · 새 파일은 F | ② 로직 지문 + K5 | → 88.9(C 까지 89.7) · 다섯 안 → 96.8 · 옵션까지 56/103 · 입력값 46 줄 더 | +| **E** 한 줄 여러 로직 그릇 | 확인 창에서 로직 둘 넘게 · 만들기 줄 여럿(같은 엑셀 이름) · 장부 값 목록 · 자동 풀기는 뒤 | 작음~중간(짐작) | C(장부 꼴) | ③ 조합 풀이의 앞 반 · N1 | 그릇만 = 순위 그대로 / 뒤 자동 풀기 = 92.9 · 조합 맞음 30.1 → 75.9 | +| **F** STmate · 거창 읽기 | STmate 꼴(줄 코드 · 노무 목록 · 7열 품) · 거창 행 칸 이음 | 중간 | — | ⑤ 읽기 넓힘 | STmate 6 파일 올라감 · 새 파일 자동 정답 80 → 95 · 씨앗 짝 53.5 → 55.8 | + +- 씨앗 끔(처음 보는 셈) — 지금 67.5 → B 80.2 → D 85.7 → E 자동 풀기 89.7. +- N3 우리에 없음 = 4-4(사용자 판단 · 풀림). +- 336 재검토 정밀도 표는 그대로 씀 — 값어치는 「사람이 고를 줄을 줄임」이지 「자동으로 넣음」이 아님. + +### 5-2. 먼저 할 관계 + +- A 가 뼈대 — 「확인한 줄만 들어감」이 다른 묶음의 전제(B · D 가 표시를 바꿔도 들어감은 늘 사용자). +- ⚑ A 의 「품 비교」 — 숫자 판정(같음 · 다름 꼬리표)은 D 첫 걸음(블록 품 줄을 HP 에 남김 · 식 풀기) 뒤에야 됨 → A 에선 우리 품(고른 로직 + 옵션 셈) 표와 엑셀 블록 칸(격자)을 나란히 보임 · 꼬리표는 D. 아니면 D 첫 걸음을 A 로 당김 — 상세 계획 때 정함. +- B 는 D 앞 — 지문만 81.7 · B 를 얹어 88.9. +- ⚑ B 크기 — 336 재검토는 「작음~중간」(덤 = 노임 · 장비 시간 줄 갈래 · `M02_Hopyo_Excel.kind_of :132-150`) · 틀 「작음」은 덤 뺀 크기. +- ⚑ C 의 장부 꼴은 처음부터 「로직 여럿」 목록 — E 에서 꼴을 다시 안 바꿈. +- ⚑ D 를 새 파일(STmate · 거창)에 쓰려면 F 가 먼저 — 지문이 STmate 노무 목록 · 7열 품 · 거창 흩어진 칸에서 나옴(336 재검토 2-4) · 틀의 차례(F 끝)는 북부청 꼴 기준 · 다른 문서를 곧 쓰면 F 를 A 와 나란히(파일 안 겹침). +- D 셈 무게 — 우리 로직 지문 처음 ≈ 2 분(뒤에서 데움) · 옵션 찾기 줄당 15 로직 · 0.1~26 초(처음 · 캐시 뒤 다시 안 셈) → 올릴 때 서버가 한 번 셈해 초기값으로 남김 · 화면 전체 잠금 없음. +- E 자동 풀기 — 지문 × 조합 풀이를 그냥 겹치면 조합 맞음 49.4% 로 깎임 → 「섞인 줄(기계 90% + 인력 10%)은 몫마다 먼저 풂」 규칙과 함께(336 재검토 2-3). + +### 5-3. 묶음마다 코드 자리 (재료 그대로) + +| 묶음 | 자리 | +|---|---| +| A | 새 확인 창 TS(본 칸 · 모달 둘 다) ← `M02_MasterTemplete_Hopyo_View.ts` `lineRow :346-432` · `choose :435-505` · 단추 `:276-294 · 516-554` · `STATE :45-53` / `_Hopyo_Sheet.ts:229-278` · `_Hopyo_Pick.ts:82-94` · `_Hopyo_Api.ts:93` / 옵션 창 `Labor_Input.ts:53` `openLaborInput` · 원문 `M02_MasterTemplete_Router_Combos.py:117` / 서버 `M02_Labor_Router.py:283-292`(즉시 확정 길) · `:306-317`(만들기가 확인한 짝을 받음) · `M02_Hopyo_Match.match_line :395-403`(들어감 → 체크 제안) · `M02_Hopyo_Build.Builder.build :110-175` | +| B | `M02_Hopyo_Basis._clause :166-176` · `sources :60-71` · `M02_Hopyo_Match.candidates :300-329` · `match_line :395-403` | +| C | `M02_Hopyo_Match.line_key :68-76` · `recall :348-350` · `chain :333-341` · `confirm :495-526` · `M02_Hopyo_Store.remember :130-140` · 새 장부 화면 TS · 장부 보기 · 저장 길 | +| D | 새 `M02_Hopyo_Finger.py`(블록 → 지문) · `M02_Hopyo_Ours.py`(우리 지문 · 캐시) · `M02_Hopyo_Fit.py`(닮음 · 맞음 · 다른 점 · 옵션 찾기) ← `M02_Hopyo_Basis.read :84-125` · HP 근거 칸 `M02_Hopyo_Excel.py:257-261` · `M02_Hopyo_Match.match_line :404-414` · `guess :417-428` / 엔진은 부르기만 `M01_MasterData_Store.loaded :65` · `M01_MasterData_Store_Flow.run :170` · 데움 `M01_MasterData_Store_Cache.warm :257` / 옵션 공간 본보기 = 스크래치패드 `req227/K5/k5_lib.py` | +| E | `M02_Hopyo_Build.Builder.build :110-175` · `_combo_lines :202-223`(사전조합 그릇은 이미 여럿 담음) · 확인 창 · B09 산근(`B09_Estimation/B09_Estimation_Labor_Source.py:163-245`) | +| F | 새 `M02_Hopyo_Stmate.py` · `M02_Hopyo_Excel._sheets :74-102` · `M02_Hopyo_Basis.read :84-125` | +| 시험 | `resources/tester/test_m02_hopyo_*`(336 재검토 6 장 안 — 지문 · 맞춤 · 조합 · STmate) + 확인 규칙 · 별칭 장부 | + +- ⚑ 줄 번호 — K4 와 336 재검토의 끝 줄이 1~2 줄 다르게 적힘(`candidates :300-329 / 331` · `recall :348-350 / 352` · `confirm :495-526 / 527`) · 코드는 같음 → 여기선 K4 줄. + +--- + +## 6. 하지 말 것 + +1. 확인 없이 사전조합 · 별칭 장부에 넣음(맞음 · 별칭 · 근거 맞음 · 이름 같음 · 씨앗이라도). +2. 자동 저장 — 줄을 고르거나 체크할 때마다 서버 씀 · 디바운스. +3. 가져오기 흐름에서 우리 로직 · 근거 · `resources/master_data` · 일위대가조합.json · M01 을 고침. +4. 우리에 없는 로직을 자동으로 만듦 · M01 별칭 칸을 코드가 채움. +5. 엑셀 금액 · 단가를 순위 · 상태 · 판정에 씀 · 우리 값으로 받아 적음. +6. 엑셀 품(계수)을 손 품 · 손 수량으로 넣어 우리 로직을 흉내 냄(= 몰래 새 로직) — 엑셀이 바꾼 품은 「다른 점」으로 보이기만. +7. 브라우저가 보낸 금액 · 품을 서버가 받아 적음 — 서버가 받은 짝을 다시 검사하고 셈. +8. 발주처 칸 · 발주처별 기억 · 별칭 자동 승격(개인 → 회사 → 마스터) · 자동 합침 · 쓰인 횟수로 자동 고름. +9. 점수 문턱으로 「우리에 없음」을 정함 · 엉뚱한 후보를 몰래 숨김. +10. 「근거 고침」이라는 이름 — 우리 근거를 고치는 일로 읽힘 → 「인용 조항 맞대기 손질」. + +--- + +## 7. 기본안으로 정한 것 다섯 — 바꾸려면 말씀 + +1. **별칭 자리** — 별칭 장부(M02 · 층마다 · 엑셀 이름 · 규격 · 단위 → 우리 로직 + 옵션 + 배) + 일위대가 줄 별칭(엑셀 이름이 줄에 붙음 · 지금 됨). M01 로직 별칭 칸(찾기용 · 이름만)은 엮지 않음 — 원하면 M01 에서 손으로. 고른 엑셀 이름을 M01 로직 별칭에도 남기길 원하면 말씀. +2. **별칭에서 온 줄의 확인 정도** — 다음 파일에서 「별칭」 체크된 채 보이고 호표마다 [확인하고 사전조합 만들기] 한 번(줄마다 다시 누름 아님) · 내 층 별칭이라도 확인 없이 들어가지 않음. +3. **여러 호표 한꺼번에** — 엑셀 보기 창에서 고른 호표들의 넣을 줄 목록을 펼쳐 보인 뒤 한 번 누름 = 사용자 확인으로 침. +4. **별칭 장부 층** — 확인한 층에 쌓임(개인 = 본인 · 회사 = 회사 관리자 · 마스터 = 시스템 관리자) · 읽는 차례 개인 → 회사 → 마스터 · 자동 올림 없음 · 회사로 나누려면 회사 관리자가 회사 층에서 확인(따로 [회사로 올리기] 단추는 원하면 C 에 덧붙임). +5. **한 엑셀 줄 = 우리 로직 여럿**(철근 가공 + 조립 · 기계 90% + 인력 10%) — 사전조합에 같은 엑셀 이름 줄 둘 넘게(B09 산근도 둘) · 별칭 장부도 「로직 여럿」 · 자동으로 풀어 주기는 뒤. + +--- + +## 8. 한계 + +- 수치는 모두 앞 실측(K5 · 336 재검토) 그대로 · 새로 안 잼 · 화면 흐름 · 묶음 크기는 설계 안(짐작) · 사람이 고르는 손 빠르기는 안 잼. +- 정답 표본 — 사람 126(짐작 31 포함 · 두 파일이 같은 발주처) · 새 파일 51(자동 정답 20 은 지문과 같은 재료라 맴돎 · 씨앗 짝은 숫자 검증 없음) → 새 파일 수치는 n 작음. +- 옵션 찾기 — 엑셀 글에 칸 이름 · 단위가 붙은 수만 후보 · 합판거푸집은 엑셀 조건 낱말로 좁힘 · 1 단계 15 로직 밖 정답은 놓침(사람 9 · 새 8) · 「옵션 둘 합 · 평균」은 그 낱말이 있을 때만 셈. +- 원문 대조 — 14 조항뿐 · 「다른 책 숫자」 15 줄의 옛 조항 · 장비만 4 줄은 못 함 → 「우리가 원문과 다름 0」은 이 범위 안의 말. +- 「맞음」도 틀림이 있음 — 사람 58 줄 중 1 · 새 파일 21 줄 중 2 → 체크된 채 보이는 줄도 마지막 거름은 사용자 눈. +- 새 파일(STmate · 거창)은 F 없이는 지문 · 조합 못 잼 · 장수 · 진안 2 파일은 문서 보안 잠김. +- 노임 줄 · 장비 시간 줄 그릇은 336 정할 것 7 그대로(이 문서 밖). diff --git a/docs/raw/조사/2026-10-15_336_부록4_목적흐름_옵션실측원문.md b/docs/raw/조사/2026-10-15_336_부록4_목적흐름_옵션실측원문.md new file mode 100644 index 000000000..36a48a24d --- /dev/null +++ b/docs/raw/조사/2026-10-15_336_부록4_목적흐름_옵션실측원문.md @@ -0,0 +1,517 @@ +# 부록4 — 목적 · 흐름 조사와 로직 + 옵션 실측 원문 (요구 227-4 · 228 · PLAN 336-4) + +- 본문 = `2026-10-15_336_목적다시_외부일위대가.md` · 아래 = K4 · K5 결과 글 원문(머리만 더함 · 내용 손대지 않음). + +--- + +## 1 절 — K4 목적 · 흐름 + +# K4 — 외부 일위대가 가져오기 · 목적 대 지금 흐름 (요구 227-4 · 228 · PLAN 336-4) + +재료 = 저장소 main_laptop_1 9c0ef7f82 코드 읽기(M02 `M02_Hopyo_*` · 화면 `M02_MasterTemplete_Hopyo_*` · `M02_Labor_*` · M01 별칭 · B09 쓰임) + 324 · 329-330 · 336 · 336-3 조사 문서 + 요구 장부 66 · 223-5 · 225~228 원문. 저장소 고침 없음 · 옵션 거꾸로 찾기 수치는 K5 몫. +파일 자리 = 따로 적지 않으면 `M02_MasterTemplete/` 아래 · 줄 번호 = 9c0ef7f82. + +## 0. 결론 + +1. 목적의 뿌리(223-5 원문) = 실무자가 구조물 조합을 만들 때 로직을 되풀이해 찾고 옵션을 고르는 수고를 덜기 → 남의 일위대가를 「우리 로직 + 옵션 + 수량」으로 된 우리 일위대가(사전조합)로 만들어 두고 익숙한 엑셀 이름(별칭)으로 다시 씀. 맞춤 정밀도는 수단일 뿐임. +2. 지금 코드에 뼈대는 있음 — 올리기 → 맞춤(이름 · 인용 조항 · 직종) → 고르기 → 기억 → 사전조합(로직 + 입력값 + 별칭 = 엑셀 이름 + 수량) → 구조물 일위대가 · B09 · B08 · 값은 우리 엔진이 다시 셈(엑셀 금액 안 받아 적음). +3. 가장 큰 어긋남 = 확인 없이 들어감 — 「같음」 · 「기억」 줄은 안 눌러도 사전조합에 들어가고, [로직 추가] 「실무 호표」 · 엑셀 보기 창 [불러오기]는 줄을 보이지도 않고 바로 만듦. +4. 옵션은 남기긴 함(기억 · 사전조합 줄 입력값) — 그러나 규격 글에서 읽은 것뿐 · 화면에 옵션 고칠 칸 없음 · 못 읽은 칸은 자동값으로 조용히 들어감 · 엑셀 단가산출 블록의 품 · 조건 글은 파일에 안 남김. +5. 「별칭」이 세 자리로 갈려 서로 안 이어짐 — (가) M01 로직 별칭(찾기용 · 시스템 관리자 · 이름만 · 지금 0 개) (나) M02 맞춤 기억(층마다 · 로직 + 옵션 · 보는 화면 없음) (다) 일위대가 줄 별칭(엑셀 이름이 줄에 붙음 · 지금 됨). +6. 추천 = (나)를 「별칭 장부」로 키워 바탕(로직 + 옵션 · 층마다 · 보기 · 고치기 · 지우기 화면) · (다)는 지금처럼 만들 때 채움 · (가)는 엮지 않음(사용자가 원하면 손으로). +7. 확인 = 호표마다 [확인하고 사전조합 만들기] 한 번 — 맞음 · 별칭 줄은 체크된 채 보이고 후보는 사용자가 고름 · 확인한 줄만 들어가고 그때 별칭 장부에 남음 · 자동 저장 없음. +8. 「우리에 없음」은 프로그램이 못 가름(가까운 후보는 늘 나옴 · 336) → 사용자 판단 — 엑셀 블록 · 원문을 보고 대개 이름 바뀐 우리 로직을 고름 · 정말 없으면 뺌 + 까닭 · 새 로직은 시스템 관리자가 M01 에서(맞춤은 로직을 만들거나 고치지 않음). +9. 앞 묶음은 「그릇 먼저 · 자동은 뒤」로 다시 가름 — 꼭 필요 = 확인 규칙(새) · 확인 창(⑥ 고침) · 별칭 장부(④ 고침) · 인용 조항 맞대기(① 이름 바꿈) · 품 견주기로 옵션 찾기(② · 범위는 K5) · 한 줄 여러 로직 그릇(③의 앞 반) / 뒤 = 여러 로직 자동 풀기(③의 뒤 반) / 때는 사용자 = STmate 읽기(⑤ · 다른 문서를 쓰면 꼭) / 뺌 = 발주처 칸 · 기억 바로 들어감. +10. 물을 것 다섯 — 별칭 자리 · 별칭에서 온 줄 확인 정도 · 여러 호표 한꺼번에 확인 · 별칭 장부 층 · 한 엑셀 줄 = 로직 여럿. + +--- + +## 1. 지금 흐름 + +### 1-1. 단계 + +| 단계 | 화면 → 길 → 서버 | 남는 것 | 들어감 · 확인 | +|---|---|---|---| +| ① 올리기 | 좌측 「일위대가 실무자료」 [엑셀 올리기] `Hopyo_Side.ts:137-169` → `POST /layers/{층}/hopyo` `M02_Labor_Router.py:206-221`(층 쓰기 · 20 MB) → `_upload :175-184` → `M02_Hopyo_Excel.read_book :211-279` + 단가산출 블록 `M02_Hopyo_Basis.read :84-125` → 격자 → `M02_Hopyo_Store.save :78-102` | `{층}/실무자료/HP-######.json` = 키 · 파일 · 올린때 · 올린이 · 호표[번 · 이름 · 규격 · 단위 · 금액 · 행 · 줄[차례 · 이름 · 규격 · 수량 · 단위 · 단가 · 금액 · 비고 · 참조 · 갈래 · 근거{출처 · 직종 · 행}]] · 단산[번 · 이름 · 단위 · 행] · 검산 + `격자/<키>.json`(보기용 칸 값). 안 남김 = 원본 엑셀 · 블록의 품 · 장비 · 조건 글 · 식 | 올린 뒤 본 화면 + 엑셀 보기 창 자동 | +| ② 엑셀 보기 | `Hopyo_Sheet.ts:75-282` 읽기 전용 시트(일위대가 · 단가산출) + 호표 목록(체크 · 맞춤 n/m) ← `GET hopyo-grid` `Router :233-240` | — | [고른 n개 불러오기] `:229-278` = 호표마다 바로 만들기 | +| ③ 호표 표 | `Hopyo_View.ts:180-240` ← `GET hopyo/{키}` `Router :224-230` ← `M02_Hopyo_Match.view :460-470` | — | 호표마다 맞춤 n/m = 들어갈 줄 수(같음 · 기억 · 호표 포함) | +| ④ 줄 맞춤 | 호표 누름 `Hopyo_View.ts:261-344` ← `Router :243-253` ← `match_hopyo :440-453` ← `match_line :367-414` 차례: % 줄 = 안 들어감 · 기억 빼기 = 뺌 · 일위N = 그 호표 · 기억(`recall :348-350` · 층 차례 `chain :333-341`) = 「기억」 · 이름 같음 · 씨앗(`_exact :286-297`) 하나 = 「같음」(입력값 = 규격 글 `spec_inputs :145-178` · 환산 `factor :182-204`) · 그 밖 = 「후보」(이름 `candidates :300-329` + 근거 `Basis.candidates :191-235` · `merge :238-256`) / 「없음」 | — | 기억 · 같음 · 호표 = 환산만 정해지면 들어감 | +| ⑤ 줄 칸 | `lineRow :346-432` = 갈래 · 품명 · 규격 · 수량 · 단위 · 엑셀 단가 · 상태 · 대상 · 입력값(글만) · 환산(칸) · [고르기] [확정] [빼기] | — | 입력값 고칠 칸 없음 · 「⚠ 못 읽음」 글만 | +| ⑥ 고르기 창 | `choose :435-505` = 엑셀 근거 줄 + [엑셀에서 보기](단가산출 그 행) · 후보 5(근거 꼬리표 · 근거 맞음 초록 `basisOk :122-126`) · [다른 로직 찾기…](빈 찾기 창) → `POST …/guess` `Router :263-274` ← `Match.guess :417-428`(규격 → 입력값 · 환산 · 저장 안 함) | 화면 `picked` 묶음(페이지 떠나면 사라짐) | 「고름」 표시 | +| ⑦ 확정 | 줄 [확정] `:393-402` · [같음 모두 확정] `:276-282 · 516-523` · [근거 맞음 모두 확정] `:283-291 · 526-541` · [빼기] · [되살림] → `POST hopyo-memory` `Router :283-292`(층 쓰기) → `Match.confirm :495-526`(돈 로직 · 우리 자재 · 환산 > 0) → `Store.remember :130-140` | `{층}/실무자료/맞춤.json` `{열쇠: {로직 \| 자재 \| 빼기, 입력값, 환산, 확정(때)}}` · 열쇠 = 품명 \| 규격 \| 단위 정규화(`line_key :68-76`) | 누를 때마다 바로 서버 씀 | +| ⑧ 만들기 | [사전조합 만들기] `:292-294 · 543-554` → `POST hopyo-build` `Router :306-317`(만들 층 쓰기 · 파일 층 읽기) → `M02_Hopyo_Build.Builder.build :110-175` → `_save :58-69` → `M02_Labor_Ref.create` · `_combo_lines :202-223` | `{층}/precombo/SC-######.json` = 이름(호표 이름 + 규격) · 단위 · 구분 「실무자료」 · 상세구분(파일) · 본뜬(HP 키 제N호표) · 담은로직[{로직, 입력값, 별칭 = 엑셀 줄 품명 40 자, 메모 「실무 0.14㎥ × 1 · 규격」, 수량{손 = 엑셀 수량 × 환산}} · 같은 로직 둘째 = 한 줄 사전조합으로 감쌈 · 일위N = 참조] · 자재[코드 · 수량 · 별도계상] | 들어감 줄만 · 같은 본뜬 = 다시 씀 · 빠진 줄 + 값 비교(`compare :181-188` 엑셀 금액 ↔ 우리 셈) | +| ⑨ [로직 추가] 실무 호표 | 구조물 일위대가 [로직 추가] `Labor_Detail.ts:495-512` → `withHopyo` `Hopyo_Pick.ts:19-42` ← `GET hopyo-picks` `Router :196-203` ← `Match.picks :477-491`(이 층 · 위 층 파일 호표마다 n/m) → 고름 `pickHopyo :82-94` = 바로 만들기(문서 층) → `lib.pick` `Labor_Detail.ts:487-494` | 일위대가 문서 담은로직에 참조 줄 {참조, 별칭 = 호표 품명, 수량{손 1}} → 문서 [저장] 때 남음 | 줄을 안 보이고 만듦 · 빠진 줄은 알림만 | + +### 1-2. 누가 무엇 (`M02_Layer_Access.py:3-9`) + +| 할 일 | 마스터 층 | 회사 층 C | 개인 층 C/U | +|---|---|---|---| +| 목록 · 보기 · 맞춤 보기 · 엑셀 보기 | 로그인 누구나 | C 사람 · 시스템 관리자 | 본인 · C 관리자 | +| 올리기 · 지우기 · 확정(기억) · 사전조합 만들기 | 시스템 관리자 | C 관리자 · 시스템 관리자 | 본인 · C 관리자 | +| 기억 읽는 차례 | 마스터 | 회사 → 마스터 | 개인 → 회사 → 마스터 | +| M01 로직 별칭 고침 | 시스템 관리자만(M01 길 전부 `main.py:665`) | — | — | + +- 좌측 판 = 화면 층 파일만 · 위 층 파일 줄은 그 층 화면에서 읽기만(확정 단추 없음) → 일반 사용자는 회사 · 마스터 파일 줄을 자기 층으로 확인할 길 없음 — [로직 추가] · 엑셀 보기 창으로 「자동 줄만」 만들기뿐. +- 지금 실제 자료(storage · 읽기만) = 개인 층 둘에 실무자료 셋(북부청 임도 · 사방) · 맞춤.json 하나에 2 줄 → 자료 꼴을 바꿔도 옮길 것 거의 없음. + +### 1-3. 그 뒤 쓰임 + +- 서고 사전조합(좌측 「일위대가 사전조합」 · 구분 실무자료) — 참조 [수정] = 사전조합 상세(`Labor_RefEdit.ts` · 일위대가 상세와 같은 부품) 줄마다 [옵션] · 별칭 · 수량 고침 → 쓰는 모든 구조물에 따라감 · 고친 것은 기억으로 안 돌아감. +- 구조물 조합 일위대가 문서 참조 줄 → 프로젝트 사본 → B09 설계내역서(`B09_Estimation/B09_Estimation_Labor_Source.py:163-245`) — 사전조합 = 겹 대가(이름 = 엑셀 호표 이름 + 규격) · 안 로직 줄 = 산근(이름 = 우리 로직 이름 · 줄 별칭은 단가승격 줄만 `:198`) · 산근도 같은 로직 + 입력값으로 셈(`:195`). +- B09 · B08 단가연결(`B09_Estimation_UI_Tabs_Bill_Link.ts:219-235`) 종류 「일위대가 사전조합」. +- 실무자료 지우기(`Router :320-327`) = HP · 격자만 · 만든 사전조합 · 기억은 그대로. + +--- + +## 2. 별칭 자리 + +### 2-1. 세 자리 (브레인이 말한 둘 + 셋째) + +| | (가) M01 로직 별칭 | (나) M02 맞춤 기억 | (다) 일위대가 줄 별칭 | +|---|---|---|---| +| 자료 | `resources/master_data/로직_*.json` 로직 줄 `별칭` 칸(글 · 쉼표로 여럿) | `{층}/실무자료/맞춤.json` `{줄: {열쇠: 값}}`(`M02_Hopyo_Store.py:123-140`) | 사전조합 · 일위대가 문서 담은로직 줄 `별칭`(≤ 40 자 · `M02_Labor_Doc.py:4 · 57`) | +| 열쇠 → 값 | 로직 → 다른 이름들 | 품명 \| 규격 \| 단위(정규화 · 원래 글 안 남음) → {로직 \| 자재 \| 빼기, 입력값, 환산, 확정 때} | 줄 → 보일 이름 | +| 생긴 까닭 | 요구 66 — 「설계자들이 찾을 때 쓰는 명칭이 다름」 · 「값은 내가 넣을거야」 · 전역 찾기에 별칭 | 324 — 확정한 맞춤을 다음에 자동 | 223-5 — 「매칭되면 엑셀 단가산출 이름을 일위대가 별칭으로」 | +| 화면 | M01 로직 목록 「별칭」 열(이름 옆 · 고치면 캐시 → [저장] · `M01_MasterData_UI_Logic_List.ts:45 · 131` · `M01_MasterData_UI_LogicLab.ts:432-440`) | 없음 — 줄 표에서 그 줄 상태 · 대상 · 입력값으로만 · 고치기 = 다시 확정(덮음) · 지우기 = 빼기 되살림뿐 · 위 층 기억은 덮기만 | 일위대가 표 · 사전조합 상세 「별칭」 칸(`Labor_Rows.ts:215-231` · 고침 가능) | +| 누가 고침 | 시스템 관리자만 | 그 층 쓰기(1-2) | 그 문서 층 쓰기 | +| 누가 채움 | 사람 손만(코드 자동 채움 없음) | 확정 단추 | 만들기(`Build.py:129 · 149` 엑셀 줄 품명) · [로직 추가] 참조 줄(`Hopyo_Pick.ts:90` 호표 품명) | +| 읽는 곳 | 서버 찾기 `M01_MasterData_Store_Find.py`(키 · 원문번호 · 이름 · 옛이름 · 별칭) · 공용 `ui_template/ui_template_logic_find.ts` → M01 목록 · 요소 찾기 · M02 [로직 더하기](`/logic-lib/logics` `M02_MasterTemplete_Router_Combos.py:83`) · 흐름도 · B09 · B08 단가연결 | `match_line :391-394`(「기억」 · 들어감) · `picks`(n/m) · `Builder` | M02 일위대가 표 · 사전조합 상세 · B09(단가승격 줄만) | +| 층 | 하나(모든 회사 같음) | 개인 → 회사 → 마스터 | 문서 층 | +| 옵션 | 못 담음 | 입력값 담음(로직 자재 고름 · 켜쌓기율 없음) | 줄에 입력값 따로 있음 | +| 지금 자료 | 1,469 중 0 | 개인 층 하나 2 줄 | 만든 사전조합마다 | + +### 2-2. 이어짐 — 셋 다 안 이어짐 + +- 맞춤은 (가)를 안 읽음 — `M02_Hopyo_Match.py:16` 「로직 별칭 안 씀」 · `_targets :218-259` 이름만. +- (나) 확정이 (가) · (다)에 안 감 — (다)는 만들 때 엑셀 품명을 그때 씀(기억과 따로). +- (다) · 옵션 · 수량을 사전조합 상세에서 고쳐도 (나)로 안 돌아감. +- 같은 「별칭」 말이 세 자리 → 사용자 말 「별칭으로 남겨」가 어느 자리인지 갈라야 함(7 장 물음 1). + +--- + +## 3. 목적 대 지금 + +### 3-1. 목적 다시 세움 — 거꾸로 + +- 뿌리(223-5 원문): 「사용자들이 구조물조합을 구성하기위해 반복적으로 검색과 옵션을 선택해야하는 어려움」 · 「다른 사람들이 가지고 있는 실무문서의 내용을 가져와 우리의 일위대가로 만들고 싶음」. +- 만들 것 = 엑셀 호표 하나 → 우리 일위대가(사전조합) 하나 = 우리 로직 + 옵션 + 수량의 조합 · 줄에 엑셀 이름(별칭) · 한 번 고른 짝은 다음에 바로(226 · 227). +- 지킬 성질 다섯 — ㉠ 로직 + 옵션이 맞음(228 · 금액 아님) ㉡ 들어가는 것은 모두 사용자가 확인(227) ㉢ 익숙한 이름이 남음(223 · 227) ㉣ 한 번 고른 짝은 다음에 쉽게(226 · 227) ㉤ 우리 로직 · 근거가 기준(226 · 227). +- 사용자 손이 드는 곳 = 고유 줄 수(엑셀 504 줄 = 고유 136 · 336) — 같은 줄은 한 번 고르면 같은 파일 · 다음 파일에서 별칭으로 뜸 → 일의 무게는 「1순위 정밀도」보다 「빠르고 바른 확인 + 고른 것 다시 씀」 쪽. 다섯 안 정답이 이미 87%(336)라 남은 몫은 고르는 손. + +### 3-2. 단계 표 + +| 목적 단계 | 있는 것 | 빈 곳 | 어긋남 | +|---|---|---|---| +| 문서 불러오기 | 북부청 꼴 xls · xlsx · 단가산출 블록 출처 · 직종 · 엑셀 보기 창 · 층 저장 | STmate 꼴 6 · 문서 보안 2 · 거창 줄 꼴(실무 내역 9 중 6 못 올림 · 336) · 블록 품 · 조건 글 안 남김 | — | +| 단가산출과 맞춤 | 이름 같음 · 씨앗 · 정리 이름 · 글자 묶음 · 인용 조항 · 직종 · 규격 → 입력값 · 환산 | 품(계수) 견주기 없음 → 「어느 로직 + 어느 옵션」을 숫자로 못 가름 · 한 줄 = 로직 여럿 못 담음 · 노임 줄 · 장비 시간 줄 그릇 | 이름만 같으면 「같음」(다른 책 10 줄 · 336) | +| 「맞음」 표시 · 사용자 확인 | 상태 · 근거 꼬리표 · 줄 [확정] · 모두 확정 단추 둘 | 「딱 맞음」 표시 · 「확인 대기」 · 넣음 체크 | 「같음」 · 「기억」 · 「호표」 줄이 확인 없이 들어감 · [같음 모두 확정]을 안 눌러도 같음은 들어감(단추가 뜻 없음) · [로직 추가] · 엑셀 보기 창은 줄을 안 보이고 만듦 | +| 안 되면 사용자가 고름 | 고르기 창 · 후보 5 · 근거 꼬리표 · 엑셀 근거 줄 · [엑셀에서 보기] · [다른 로직 찾기] | 옵션 칸 · 우리 품 ↔ 엑셀 품 · 원문 보기 · [다른 로직 찾기]에 조항 · 이름 미리 넣기 · 위 층 파일 줄을 내 층에서 고르기 | — | +| 별칭으로 남김 | (다) 줄 별칭 = 엑셀 이름 · (나) 기억 = 로직 + 입력값 + 환산 | 별칭 보기 · 고치기 · 지우기 화면 · 원래 글 · 누가 · 파일 · 열쇠가 좁음(규격 글이 조금만 달라도 안 걸림) · 사전조합 고친 것이 안 돌아감 | 기억이 다음 파일에서 확인 없이 들어감(위 층 · 남이 확정한 것까지) · 「별칭」 세 자리 안 이어짐 | +| 일위대가 항목 쉽게 반영 | 호표 → 사전조합 · [로직 추가] 실무 호표 · 여러 호표 한 번 · B09 · B08 단가연결 · 우리 엔진이 다시 셈 | [로직 더하기] 찾기에서 익숙한 이름으로 로직 + 옵션 한 번에(줄 하나) · 위 층에 이미 확인한 같은 호표 사전조합 다시 쓰기 | 확인 안 한 줄이 섞인 사전조합이 서고에 남아 여러 구조물 · B09 가 같이 씀 | +| 우리에 없음 | 「없음」 상태 · 빠진 줄 표 | 원문 보기 · 「우리에 없음」 표시 · 까닭 · 목록 | 엉뚱한 후보가 1순위로 보임(5 줄 · 336) — 점수로는 못 가름 | +| 우리 로직이 원문과 다름 | 원문 보기 부품(`M01_MasterData/M01_MasterData_UI_LogicLab_Source.ts` · M02 길 `/m02/logic-lib/source` 로그인만 · `Router_Combos.py:117`) | 맞춤 화면에서 안 씀 · 「품 다름」 표시 | 없음(맞춤은 우리 로직을 안 고침) | +| 관리 · 발주처 | 층(개인 · 회사 · 마스터) | 관리 화면 | 지금 코드엔 발주처 없음 · 336-3 안(N4 발주처 칸 · 같은 발주처 기억 바로)이 사용자 말과 어긋남 → 뺌 | + +### 3-3. 브레인 예 다섯 판정 + +| 예 | 판정 | 근거 | +|---|---|---| +| 「같음」이 확인 없이 들어감 | 맞음(어긋남) | `match_line :395-403` 들어감 = 환산이 정해지면 · `Builder.build :117-125` 들어감 줄만 · 336 실측 25 줄 중 다른 책 5 | +| 기억이 바로 들어감 | 맞음(어긋남) | `:391-394` · 위 층(회사 · 마스터) 기억 · 다른 파일 확정도 그대로 들어감 · 336-3 「다른 발주처 기억 1 줄을 틀리게 넣음」 | +| 옵션을 안 남김 | 반만 맞음 | 남기긴 함(기억 · 사전조합 줄 입력값) · 그러나 규격 글로 읽은 것만 · 고칠 칸 없음 · 못 읽은 칸 = 자동값 · 블록 조건 안 읽음 · 사전조합에서 고친 옵션이 기억으로 안 감 | +| 「근거 고침」 이름 오해 | 맞음 | 앞 문서 ① = 맞춤 코드가 엑셀 인용 조항을 읽고 맞대는 부분 손질(`Basis._clause` · `sources` · `Match.candidates` · `match_line`) · 우리 근거(M01 로직 · 출처)는 안 고침 → 「인용 조항 맞대기 손질」로 | +| 발주처 | 코드엔 없음 · 안이 어긋남 | 336-3 N4 · ④ 「같은 발주처 기억은 바로」 → 「구분 의미 없음」으로 뺌 | + +--- + +## 4. 엑셀 값 = 우리 로직 + 옵션 (228) + +### 4-1. 지금 되는 것 + +- 사전조합 줄 = {로직, 입력값, 수량{손}} → 값은 우리 엔진(`M02_Labor_Ref.Expand`)이 셈 · 엑셀 금액 · 단가는 안 들어감(메모엔 수량 글만) → 「로직 + 옵션으로 남겨 우리가 다시 셈」은 이미 이 꼴. +- 사전조합 줄 그릇은 입력값 · 켜쌓기율 · 고름(로직 자재)까지 담음(`_combo_lines :208-214`) — 가져오기는 입력값만 채움. +- 기억도 입력값을 담음 → 같은 열쇠 줄은 다음 파일에서도 같은 옵션. +- B09 산근도 같은 로직 + 입력값으로 셈 → 가져온 옵션이 내역서까지 감. + +### 4-2. 못 담는 것 · 필요한 것 + +| 못 담는 것 | 지금 | 필요한 것 | +|---|---|---| +| 옵션의 출처 | 규격 글 + 품명 괄호(`spec_inputs`)뿐 · 블록 조건(토질 · 유형 …) · 품 안 봄 | 확인 창 옵션 칸(`Labor_Input.ts:53` `openLaborInput` 다시 씀) · 옵션 출처 꼬리표(규격 · 블록 · 별칭 · 자동값) · 품으로 옵션 거꾸로 찾기(K5) | +| 엑셀 블록의 품 · 조건 | HP 파일에 출처 · 직종 · 행만(`Excel.py:257-261`) | 블록 줄(직종 · 품 · 장비 시간 · 율 · 조건 글)을 HP 파일에 남김 — 격자 파일엔 칸 값으로 있음 | +| 「맞음」의 숫자 판정 | 없음(이름 · 조항 · 직종 · 단위뿐) | 우리 로직을 그 옵션으로 셈한 품 ↔ 엑셀 품 → 「품 같음(딱 맞음)」 · 「품 다름(엑셀이 바꿈)」 | +| 한 엑셀 줄 = 로직 여럿 | 기억 · 만들기 = 한 줄 로직 하나 | 별칭 값 = [로직 + 옵션 + 배] 목록 · 만들기가 줄 여럿(같은 엑셀 이름) — 사전조합 그릇은 이미 됨 | +| 섞음 비율 · 할증 · 횟수 | 환산(수량 배)으로만 · 수량에 녹아 메모 글로 | 우리 로직 입력이면 옵션 · 아니면 배 — 확인 창에 둘을 갈라 보임 | +| 노임 줄 · 장비 시간 줄 | 노임 줄 그릇 없음 · 장비 줄은 맞춤 못 함 | 장비 = 기계경비 로직 + 기계 옵션(그릇 있음) · 노임 = 336 정할 것 7 그대로 | +| 엑셀이 바꾼 품 | 안 담음(맞음 — 우리 기준) | 표시만 · 우리 품으로 셈 | + +- 예 — 구조물터파기(인력100%) 토사: 엑셀 특별인부 0.354 ↔ 우리 굴착(인력/토사) GC000083 0.370 — 336 이 「토사 셋 평균」으로 읽은 줄 · 원문 3-2-1 시공량은 토질마다 따로(보통 3.6 · 경질 2.7 · 고사점토 2.2 ㎥/인) → 어느 옵션도 딱 안 맞음 = 「품 다름(엑셀이 바꿈)」 · 사용자가 토질 고름 · 우리 품으로 셈. + +--- + +## 5. 설계 안 (코드 아님) + +### 5-1. 원칙 세 줄 + +- 가져오기 = 「엑셀 줄 → 우리 로직 + 옵션」 짝 찾기 · 값은 우리 셈 · 금액은 판정 밖. +- 맞춤은 제안만 · 들어가는 것은 사용자가 확인한 줄만 · 확인하면 별칭으로 남음. +- 우리 로직 · 근거 · 원본은 가져오기 흐름에서 안 고침 · 새 로직 · 고침은 원문 근거 + 사용자 확인 뒤 M01 에서. + +### 5-2. 줄 상태 (쉬운 말 · 색) + +| 상태 | 뜻 | 처음 체크 | 사용자가 할 일 | +|---|---|---|---| +| 맞음(초록) | ② 뒤 = 우리 로직 + 옵션의 품 · 직종 · 조항이 엑셀과 같음(딱 맞음) · ② 전 = 근거 맞음(조항 · 직종 · 단위) · ① 뒤 이름 같음 | 체크(확인 대기) | 보고 확인 | +| 별칭(파랑) | 전에 확인한 짝 · 「규격 다름」 · 「근거 다름」이면 체크 안 함 | 체크(확인 대기) | 보고 확인 · 옵션 확인 | +| 후보 | 가까운 것 여럿 | 안 함 | [고르기] | +| 확인함 | 이 화면에서 넣기로 함(캐시) | — | [확인하고 만들기] | +| 뺌 | 사용자가 뺌 · 까닭 「우리에 없음」 · 「안 씀」 | — | — | +| 갈래 그대로 | % 줄(우리 덧줄이 셈) · 다른 호표(그 호표 확인이 먼저) · 노임 줄(그릇 없음) | — | — | + +- 꼬리표: 「품 다름 특별인부 0.370 → 0.354(엑셀이 바꿈)」 · 「옵션 — 규격에서 / 블록에서 / 별칭에서 / 자동값」 · 「근거 건설 3-2-1 · 특별인부」 · 「별칭 — 회사 층 · 2026-10-15 · 북부청 임도」. +- 금액 비는 안 보임 · 만든 뒤 값 비교는 「참고」 한 줄(판정 · 상태에 안 씀). + +### 5-3. 사용자 확인 + +- 한 줄: 체크 · [고르기](후보 · [다른 로직 찾기]는 엑셀 조항 번호 · 낱말을 미리 넣음 — M01 찾기는 원문번호로도 걸림) · [옵션](로직 입력 창) · [뺌](까닭). +- 호표 하나: [맞음 · 별칭 모두 체크] → [확인하고 사전조합 만들기] 한 번 = 체크한 줄만 넣음 · 그때 별칭 장부에 씀 · 체크 안 한 줄 = 빠진 줄(까닭 「확인 안 함」). 줄 [확정]의 즉시 서버 쓰기는 없앰(고름 · 체크 · 옵션 = 화면 캐시). +- 여럿 한꺼번에: 엑셀 보기 창 [고른 n개 확인하고 만들기] = 호표마다 넣을 줄(맞음 · 별칭) · 빠질 줄(후보)을 펼쳐 보인 뒤 한 번 누름 — 물음 3. +- 누가 누름: 만들 층에 쓰기 권한 있는 사람(개인 = 본인 · 회사 = 회사 관리자 · 마스터 = 시스템 관리자) · 파일 층은 읽기만 있으면 됨 → 위 층 파일도 내 층으로 확인 · 만들기. +- [로직 추가] 「실무 호표」: 만들 층에 확인한 사전조합이 있으면 그것을 참조 · 없으면 같은 확인 창을 모달로 → [확인하고 만들기] → 참조 줄. +- 일위N(다른 호표) 줄: 그 호표가 먼저 확인돼 있어야 들어감. +- 서버는 받은 짝(로직 키 · 입력값 이름 · 환산)을 다시 검사하고 셈 · 브라우저 값(금액)을 받아 적지 않음(CLAUDE.md 5장). +- 사전조합이 곧 「이 호표 확인함」 기록 — 새 확인 저장소를 따로 안 둠. + +### 5-4. 별칭 저장 자리 — 안 셋 + +| | 안 1 = (가) M01 로직 별칭에 엑셀 이름 | 안 2 = (나) 맞춤 기억 → 별칭 장부(추천) | 안 3 = 섞음(안 2 + 관리자가 골라 M01 로 올림) | +|---|---|---|---| +| 담는 것 | 이름만 | 엑셀 이름 · 규격 · 단위 → 로직(여럿) + 옵션 + 환산 + 확인한 이 · 때 · 파일 | 안 2 + M01 에 이름 | +| 누가 · 층 | 시스템 관리자 · 층 하나 | 그 층 쓰기 · 개인 → 회사 → 마스터 | 올림 = 시스템 관리자 | +| 228 옵션 | 못 담음 | 담음 | 담음 | +| 관리 | M01 목록 칸(있음) | 새 장부 화면 | 둘 | +| 찾기 | M01 · M02 · B09 찾기에 바로 | 맞춤 · (있으면 좋음) [로직 더하기] 찾기 | 둘 | +| 원본 | 마스터 로직 파일에 씀(셈엔 안 닿음) | 안 건드림 | 올릴 때만 씀 | +| 판정 | 옵션 못 담고 일반 사용자가 못 씀 → 바탕으로 안 맞음 | 목적 다섯 성질 모두 · 지금 코드를 키움 | 사용자가 원할 때 덧붙임 | + +- 장부 한 줄 = {엑셀 품명(원래 글) · 규격 · 단위 · 짝 [{로직 \| 자재, 입력값, 배}] 또는 「우리에 없음」 · 「안 씀」 · 확인한 이 · 때 · 처음 파일}. +- 열쇠 = 지금 그대로(품명 \| 규격 \| 단위) · 찾을 때만 품명 \| 단위로 넓힘(「별칭 · 규격 다름」 · 체크 안 함) · 품 꼴 열쇠(336 c)는 ② 뒤. +- 확인한 줄은 모두 장부에 씀(이름이 같아도 — 옵션 · 환산을 남기려고) · 같은 열쇠는 덮음. +- (다) 줄 별칭 = 만들 때 엑셀 품명 그대로(지금처럼). +- 맞춤이 (가) M01 별칭도 이름으로 읽음 = 있으면 좋음(읽기만 · 지금 0 개라 효과 0 · 사용자가 채우면 바로 걸림). + +### 5-5. 별칭 장부 화면 — 관리는 사용자 몫 + +- 자리: M02 좌측 「일위대가 실무자료」 판 안 [별칭 장부] 한 줄 → 본 칸 표. +- 열: 엑셀 품명 · 규격 · 단위 | 우리 로직(이름 · 키) · 옵션 · 배 | 확인한 이 · 때 · 처음 파일 | 층(내 층 · 회사 · 마스터 — 위 층은 읽기만). +- 할 일: 찾기 · 갈래 거름(짝 · 우리에 없음 · 안 씀) · [고치기](로직 다시 고르기 · 옵션 · 배) · [지우기] · 위 층 줄 [내 층으로 고쳐 씀] · 고친 것은 캐시 → [저장] 때만 씀. +- 하지 않음: 발주처 칸 · 자동 올림(개인 → 회사 · 회사 → 마스터) · 자동 합침 · 쓰인 횟수로 자동 고름. +- 회사 공유 = 회사 관리자 [회사로 올리기] 단추(물음 4). + +### 5-6. 우리에 없음 — 원문 확인 → 사용자 확인 → 사용자가 M01 에서 + +- 프로그램은 「우리에 없음」을 스스로 정하지 않음 — 가까운 후보는 늘 나오고 점수 문턱으로 숨기기는 무익(336 · 10 / 10 줄에 후보) → 사용자 판단. +- 확인 창에서: [엑셀에서 보기](그 단가산출 블록 칸) · [원문 보기](엑셀 인용 조항 → 품셈 본문 절 · `/m02/logic-lib/source`) · [다른 로직 찾기](조항 번호 · 낱말 미리 넣음). +- 판단 셋: ① 이름 바뀐 우리 로직(대개 — 사용자 말) → 고름 → 별칭 ② 정말 없음 → [뺌 · 우리에 없음] → 장부 「우리에 없음」 줄(다음 파일에서도 그 표시 · 빠진 줄에 엑셀 금액) ③ 넣어야 할 로직 → 시스템 관리자가 원문 확인 뒤 M01 [복사해서 만들기](자체 로직 GX · 정본 안 건드림)로 만듦 → 다음 확인에서 고름. +- 개발 · AI 쪽도 같음: 「우리에 없음」 후보는 먼저 원문(`resources/knowledge/original/원가계산/`)에서 이름 바뀐 같은 일을 찾아 보고 → 사용자 확인 뒤에만 M01 에 넣음. +- 예(판단은 사용자): 잔토처리 · 잔토정리(336 우리에 없음 6 줄) — 원문 건설 3-2-1 굴착 비고 「현장 내에서 소운반하여 깔고 고르는 잔토처리는 ㎥당 보통인부 0.2인을 별도 계상」(`resources/knowledge/original/원가계산/건설공사_표준품셈/본문/01_공통부문/제03장_토공사/3-02_굴착.md:26`) = 단독 절이 아니라 굴착의 비고 · 우리는 GC000083 굴착(인력/토사) 입력 「잔토처리」로만 둠 → 엑셀 단독 줄로 쓰려면 새 로직이 필요할 수 있음 = 사용자 판단 거리. 고화매트 설치 = 원가계산 원문에서 낱말 못 찾음. + +### 5-7. 우리 로직이 원문과 다를 때 — 원문 근거 → 사용자 확인 → 고침 + +- 기본 = 우리가 기준 — 엑셀 품이 다르면 「품 다름(엑셀이 바꿈)」 꼬리표만 · 우리 품으로 셈 · 가까운 옵션은 사용자가 고름(4-2 예). +- 우리가 틀렸다고 보일 때만: [원문 보기]로 원문 절(엑셀 인용 조항 · 우리 로직 출처 둘 다) → 사용자가 「우리가 틀림」 확인 → 시스템 관리자가 M01 에서 고침(가져오기 흐름 밖 · 따로 일 · PLAN 에 원문 근거 인용) → 확인 창이 다시 셈. +- 맞춤 화면에 「우리 로직 고치기」 단추 안 둠. +- 개발 · AI 쪽: 엑셀과 다르다는 것만으로 master_data 를 고치지 않음 · 원문 파일 · 절 · 값을 대조해 보고 → 사용자 확인 뒤. +- 지금까지 실측 — 계수 다름 32 줄(336-3)의 까닭은 엑셀 쪽(평균 · 옛 판 · 두 작업 한 품)으로 읽힘 · 우리 쪽 원문 대조는 아직 안 함. + +### 5-8. 자료 · 계산 자리 + +- 3층(CLAUDE.md 5장) — 초기값 = 올린 엑셀 읽은 결과(HP 파일 · 안 바꿈) · 작업본 = 사전조합 · 별칭 장부([확인하고 만들기] · 장부 [저장] 때만) · 캐시 = 고름 · 체크 · 옵션. +- 계산 = 서버 단독(③) — 맞춤 · 품 견주기 · 옵션 찾기 · 만들기 · 화면은 보이기만. +- 화면 파일: `Hopyo_View.ts` 614 줄(한도 700) → 확인 창은 새 파일(본 칸 · 모달 둘 다 씀) · 장부 화면도 새 파일. + +### 5-9. 차례 + +1. 확인 규칙 + 확인 창(넣음 체크 · 옵션 칸 · 원문 보기 · 미리 넣은 찾기) + [로직 추가] · 엑셀 보기 창 길 — 목적의 뼈대 · 중간. +2. ① 인용 조항 맞대기 손질 — 작음 · 「맞음」 표시가 바르게(다른 책 「같음」 10 줄 → 후보). +3. 별칭 장부(덧칸 · 느슨한 찾기 · 「로직 여럿」 꼴 · 장부 화면) — 중간. +4. ② 품 견주기 + 옵션 거꾸로 — K5 결과로 범위 · 블록 품 줄을 HP 에 남기는 것부터 · 중간~큼. +5. ③ 한 줄 여러 로직 자동 풀기 — ② 뒤. +6. ⑤ STmate · 거창 읽기 — 다른 문서를 곧 쓰면 1 과 나란히(파일 안 겹침). +7. 있으면 좋음 — [로직 더하기] 찾기에 별칭 장부 이름(익숙한 이름 → 로직 + 옵션 + 별칭 한 번에 · 223-5 「되풀이 검색 · 옵션 고르기」에 바로 닿음) · 맞춤이 M01 별칭 읽기 · 위 층 확인한 사전조합 다시 쓰기. + +### 5-10. 하지 말 것 + +1. 확인 없이 사전조합 · 별칭 장부에 넣음(맞음 · 별칭 · 근거 맞음 · 이름 같음이라도). +2. 가져오기 흐름에서 우리 로직 · 근거 · `resources/master_data` · 일위대가조합.json · M01 을 고침. +3. 우리에 없는 로직을 자동으로 만듦 · M01 별칭 칸을 코드가 채움. +4. 엑셀 금액 · 단가를 순위 · 상태 · 판정에 씀 · 우리 값으로 받아 적음. +5. 엑셀 품(계수)을 손 품 · 손 수량으로 넣어 우리 로직을 흉내 냄(= 몰래 새 로직). +6. 발주처 칸 · 발주처별 기억 · 별칭 자동 승격 · 자동 합침. +7. 점수 문턱으로 「우리에 없음」을 정함 · 엉뚱한 후보를 몰래 숨김. +8. 자동 저장(줄 고를 때마다 서버 씀 · 디바운스). + +--- + +## 6. 앞 묶음 다시 가름 — 「그릇 먼저 · 자동은 뒤」 + +| 앞 이름 | 새 이름(오해 없는 말) | 목적에 비춰 | 가름 | 고쳐 쓸 점 · 차례 | +|---|---|---|---|---| +| ① 근거 고침 | 인용 조항 맞대기 손질 | 「맞음」 표시가 바르려면 필요 · 우리 근거는 안 고침(맞춤 코드만) | 꼭 필요 | 「같음」 근거 검사는 상태 표시에만(들어감은 확인 규칙이 막음) · 2 | +| ② 로직 지문 | 품 견주기 · 옵션 거꾸로 찾기 | 228 「어느 로직 + 어느 옵션」을 숫자로 가르는 길 · 딱 맞음 · 품 다름 표시의 바탕 | 꼭 필요(범위 · 무게는 K5) | 목표를 1순위 올림 → 옵션 찾기 · 맞음 표시로 · 블록 품 줄을 HP 에 남김부터 · 4 | +| ③ 조합 풀이 | 한 줄 여러 로직 | 226 「어떤 로직의 조합」 · 고임돌 · 조약돌 줄이 호표 31 곳에 되풀이(336-3) | 그릇 = 꼭 필요(별칭 값 목록 · 확인 창에서 로직 둘 · 만들기 줄 여럿) · 자동 풀기 = 있으면 좋음 | 그릇은 3(장부와 같이) · 자동은 5 | +| ④ 조건부 기억 | 별칭 장부 | 「별칭으로 남김」 · 「한 번 한 것 기억」 | 꼭 필요 · 고쳐서 | 바로 들어감 뺌(모두 확인 대기) · 발주처 뺌 · 열쇠 a + 품명 · 단위 찾기 · 품 꼴 열쇠는 ② 뒤 · 근거 다름 = 꼬리표 · 덧칸 · 장부 화면 · 3 | +| ⑤ 읽기 넓힘 | STmate · 거창 꼴 읽기 | 「사용자들이 가진 문서」 — 실무 내역 9 중 6 이 지금 못 올림 | 꼭 필요(다른 문서를 쓸 때) · 때는 사용자 | 그대로 · 6(또는 1 과 나란히) | +| ⑥ 고르기 창 | 확인 창 | 「확인은 사용자가」의 자리 | 꼭 필요 · 고쳐서 | 고르기 → 확인 중심(상태 · 넣음 체크 · 옵션 칸 · 품 비교 · 원문 보기 · 미리 넣은 찾기 · [확인하고 만들기]) · 모달로도 · 금액 비 뺌 · 1 | +| (새) — | 확인 규칙 | 227 「맞다는 표현만 · 확인은 사용자」 | 꼭 필요 | 들어감 = 확인한 줄만 · [로직 추가] · 엑셀 보기 창 길도 · 1 | +| N2 딱 맞음 한꺼번에 확정 | 맞음 · 별칭 모두 체크 | 체크까지 · 만들기는 사용자 누름 | 고쳐서 | 물음 3 | +| N3 우리에 없음 | 우리에 없음 — 사용자 판단 | 227 「원문 보고 · 나에게 확인」 | 풀림 | 5-6 | +| N4 발주처 칸 · 같은 발주처 기억 바로 | — | 227 「발주처 구분 의미 없음」 | 뺌 | — | +| N1 사전조합 한 줄 = 담은로직 여럿 | 한 줄 여러 로직 그릇 | ③과 같음 | 물음 5 | — | + +- 앞 문서의 정밀도 표(1순위 92.9% 등)는 그대로 쓸 수 있음 — 다만 값어치는 「사람이 고를 줄 수를 줄임」이지 「자동으로 넣음」이 아님. + +--- + +## 7. 사용자에게 물을 것 (꼭 필요한 것만) + +1. **「별칭」 자리** — 추천 = 별칭 장부(M02 · 층마다 · 엑셀 이름 → 우리 로직 + 옵션) 바탕 + 일위대가 줄 별칭(엑셀 이름이 줄에 붙음 · 지금 됨) · M01 로직 별칭 칸(찾기용 · 이름만 · 「값은 내가 넣음」)은 가져오기와 안 엮음. 이 뜻이 맞는지 · 아니면 고른 엑셀 이름을 M01 로직 별칭에도 남기길 원하는지. +2. **별칭에서 온 줄의 확인 정도** — 다음 파일에서 「별칭 · 확인 대기(체크됨)」로 보이고 호표마다 [확인하고 만들기] 한 번이면 되는지(줄마다 다시 누름 아님) · 아니면 내 층 별칭은 확인 없이 들어가도 되는지. +3. **여러 호표 한꺼번에 확인** — 엑셀 보기 창에서 고른 호표들의 넣을 줄(맞음 · 별칭) 목록을 펼쳐 보이고 한 번 눌러 만드는 것을 「사용자 확인」으로 쳐도 되는지. +4. **별칭 장부 층** — 각자 자기 층(개인)에 쌓이고 회사 공유는 회사 관리자가 올림 · 마스터는 시스템 관리자 — 「관리는 사용자 몫」이 이 뜻인지. +5. **한 엑셀 줄 = 우리 로직 여럿**(철근 가공 + 조립 · 기계 90% + 인력 10%) — 사전조합에 같은 엑셀 이름 줄 둘로 넣고(B09 산근도 둘) 별칭 장부도 「로직 여럿」을 담는 것 괜찮은지. + + +--- + +## 2 절 — K5 로직 + 옵션 거꾸로 찾기 + +# K5 「로직 + 옵션」 거꾸로 찾기 실측 (요구 227-4 · 사용자 말 228) + +사용자 말 228 「엑셀 일위대가 = 우리 로직에 옵션을 넣고 나온 값일 확률 높음」을 수치로 잼. 정답 로직이 알려진 엑셀 줄마다 그 로직의 입력 칸(옵션) 조합을 M01 엔진으로 모두 돌려 엑셀 품(직종 · 장비 시간 · 자재 수량)과 같은 조합이 있는지 봄. 금액 안 씀 · 저장소 안 고침(엔진 셈 · 원문 읽기만) · 설계는 K4 몫. + +## 0. 결론 + +1. **사용자 말 맞음** — 고유 줄 177 중 우리 로직 + 옵션이 엑셀 품을 그대로 냄 102 줄(58% · 엑셀 줄 무게 57%) · 우리 로직에 엑셀이 셈을 덧붙임(인력 10% 곱함 · 할증 · ÷6 m 환산 · 옵션 둘 합 · 묶음) 36 줄(20%) → 합 78%(줄 무게 82%). +2. 나머지 33 줄(19%)은 숫자 자체가 다름 — 옛 판 값 15 · 다른 책 숫자 15 · 다른 로직 숫자 3. 장비만 줄 4 · 품 없는 로직 2. +3. **우리 로직이 원문과 다른 줄은 못 찾음** — 숫자가 다른 줄의 14 조항을 품셈 원문과 대조 → 14 모두 우리 로직 = 지금 원문(엑셀 = '24~'26 보완 전 옛 값 · 다른 책 할증 · 환산 · 묶음). +4. 맞은 줄의 옵션 — 하나로 정해짐 69 · 여럿 25(이름만 다른 옵션 11 · 엑셀이 안 적은 몫(폼타이 · 기계 규격) 11 · 1% 차 옵션 3). +5. **옵션 찾기의 값어치 = 입력값 채우기** — 맞은 94 줄(0.5% + 반올림) 중 46 줄은 기본값도 지금 M02 규격 읽기도 못 맞추고 옵션 찾기로만 맞음(예 레미콘 「소량」 → 소량감률 50 · 합판 「4회」 → 유형 보통). +6. **로직 고르기엔 약함** — 품만으로 (로직 + 옵션)을 줄 세우면 1순위 60.3%(C4 지문 81.7%) · 지문에 「딱 맞음 먼저」를 얹어도 81.0% → 로직은 지문(+ 묶음 ①)으로, 옵션은 고른 로직에서 거꾸로 찾음. +7. 그렇게 하면 1순위 로직이 맞은 103 줄 중 56 줄이 옵션까지 엑셀 품 그대로(C4 변형만으론 37). +8. **「맞음」 표시** = 1위 (로직 + 옵션) 품이 엑셀과 같고 엑셀 몫을 다 덮음 → 사람 정답 126 중 58 줄 · 틀림 1(숫자는 산림 펌프카 · 정답표는 건설) · 새 파일 51 중 21 줄 · 틀림 2. +9. **「다른 점 보임」** = 직종 같음 + 엑셀 인용 조항 = 그 로직 조항 → 25 줄 · 틀림 1(직종만 보면 39 줄 · 틀림 16). 그 밖 = 「후보」. +10. 씨앗 짝(사람이 전에 고른 짝) 31 줄은 옵션으로 맞는 줄 6 뿐 — 엑셀 숫자가 다른 책 · 옛 판 → 「맞음」 대신 「다른 점 · 후보」로 보이는 것이 맞음 · 확인은 사용자. + +--- + +## 1. 옵션 거꾸로 찾기 표 + +### 1-1. 어떻게 셈 + +| 칸 | 내용 | +|---|---| +| 대상 | 고유 줄 177 = 사람 정답 126(북부청 임도 49 · 사방 43 · 영월 34 — C1 labels) + 새 파일 51(자동 정답 20 · 씨앗 짝 43 · 겹침 12 — C6 · C7). 엑셀 줄 무게 617 | +| 엑셀 품 | 126 = C4 엑셀 지문(식 풀기 · 밑품 · 장비 시간 · 자재) · 새 파일 = C6 STmate 블록 자원(L · X · M · S) · 거창 = C4 식 풀기 | +| 옵션 공간 | 고르기 모두 · 기계 고르기(엑셀 규격 수로 좁힘) · 범위 = 두 끝 + 칸 이름 · 단위가 붙은 엑셀 수 · 표 조건 수 = 표 줄 값 · 그 밖 수 = 자동값 + 엑셀 계수(q · f · E · Cm) + 이름 · 단위 붙은 수 · % 칸 = 0 더함 | +| 뺀 칸 | 지역 · 보정작업 · 조종원직종 — 1,438 로직 모두 값을 바꿔도 품이 안 바뀜(실측 3,756 셈) | +| 조합 | 곱 1,500 안 = 모두 돌림(167 / 177 줄) · 넘은 것은 합판거푸집 GC000211 하나(10 줄 — 엑셀 조건 낱말로 좁힘 · 그래도 넘으면 하나씩 + 둘씩) | +| 맞음 | 직종 묶음 같음 + 직종마다 품 · 같은 갈래 장비 시간 · 이름 겹치는 자재 수량이 허용 안 | +| 허용 | 0.5% · R(0.5% + 엑셀이 소수 둘째~넷째 자리로 적은 값의 반올림 폭 · 값의 4% 안) · 3% | +| 판정 | 하나만 = 맞는 조합의 품 바꾸는 칸 값이 하나 · 여럿(같은 품) = 엑셀 몫은 같은데 칸 값이 갈림 · 여럿(다른 품) = 허용 안에 다른 품 둘 넘게 · 없음 · 못 셈 | + +### 1-2. 줄 수(고유 줄 · 하나만 · 여럿(같은 품) · 여럿(다른 품) · 없음 · 못 셈) + +| 무리 | n | 0.5% | R(반올림 허용) | 3% | +|---|---|---|---|---| +| 사람 · 북부청 임도 | 49 | 19 · 6 · 1 · 22 · 1 | 22 · 6 · 1 · 19 · 1 | 20 · 4 · 5 · 19 · 1 | +| 사람 · 북부청 사방 | 43 | 13 · 7 · 1 · 21 · 1 | 17 · 7 · 1 · 17 · 1 | 15 · 4 · 7 · 16 · 1 | +| 사람 · 동부청 영월 | 34 | 10 · 5 · 0 · 19 · 0 | 9 · 5 · 1 · 19 · 0 | 8 · 4 · 5 · 17 · 0 | +| **사람 126** | 126 | 42 · 18 · 2 · 62 · 2 | **48 · 18 · 3 · 55 · 2** | 43 · 12 · 17 · 52 · 2 | +| 새 · 자동 정답 20 | 20 | 18 · 1 · 0 · 1 · 0 | 18 · 1 · 0 · 1 · 0 | 11 · 0 · 8 · 1 · 0 | +| 새 · 씨앗 짝만 31 | 31 | 3 · 3 · 0 · 25 · 0 | 3 · 3 · 0 · 25 · 0 | 3 · 3 · 3 · 22 · 0 | +| **새 파일 51** | 51 | 21 · 4 · 0 · 26 · 0 | **21 · 4 · 0 · 26 · 0** | 14 · 3 · 11 · 23 · 0 | +| **모두 177** | 177 | 63 · 22 · 2 · 88 · 2 | **69 · 22 · 3 · 81 · 2** | 57 · 15 · 28 · 75 · 2 | + +| 맞음 합(하나만 + 여럿) | 0.5% | R | 3% | R 또는 3% | +|---|---|---|---|---| +| 사람 126 | 62 (49%) | 69 (55%) | 72 (57%) | 74 (59%) | +| 새 파일 51 | 25 (49%) | 25 (49%) | 28 (55%) | 28 (55%) | +| 모두 177 | 87 (49%) | 94 (53%) | 100 (56%) | 102 (58%) | +| 엑셀 줄 무게 617 | 294 (48%) | 330 (53%) | 346 (56%) | 354 (57%) | + +- 자동 정답 20 은 정답을 「조항 같음 + 품 3% 안 로직 하나」로 만든 무리라 맞음이 높게 나옴(맴돎) · 바른 가림 무리는 사람 126 과 씨앗 짝. +- 3% 의 「여럿(다른 품)」 28 중 21 = 합판 · 유로폼 「높이감률」 0 ↔ 1 (자동(견본)값 1 이 품을 1% 줄임 · 0.5% 에선 0 이 맞음). +- 못 셈 2 = P.V.C 파이프 설치(GF000192 — 재료비 + 설치비 5% · 품 칸 없는 로직). + +### 1-3. 「여럿」 — 품으로 못 가르는 옵션 + +| 갈래(R) | 줄 | 갈리는 칸 · 예 | +|---|---|---| +| 이름만 다른 옵션(모든 몫 같음) | 11 | 철근 조립 「구분 토목 / 건축」(Type-Ⅰ 둘 다 6 · 2 · 3.4 ton) 2 · 레미콘 인력운반의 「굴착기」 칸 3 · 포장 A-Type 20㎝ = B-Type 40㎝(둘 다 100㎥) 2 · 잡철물 「단순설치」 3 · 철골설치 1 | +| 엑셀이 안 적은 몫이 갈림 | 11 | 합판 「측압 · 폼타이규격 · 폼타이」(엑셀에 폼타이 자재 없음) 9 · 펌프차 · 크레인 「기계」(41m · 10톤 기종 둘 넘게) 2 | +| 1% 차 옵션이 허용 안 | 3 | 동바리 「높이 · 설치간격」 2 · 유로폼 「높이감률」 0 ↔ 1 1 | + +- 갈리는 칸 셈(R · 줄) — 측압 9 · 폼타이규격 9 · 굴착기 4 · 단순설치 3 · 폼타이 3 · 구분 2 · 호 2 · 높이 2 · 설치간격 2 · 기계 2. +- 품에 아예 안 닿는 칸(어떤 자리에서도 품이 안 바뀜)이 든 정답 로직 줄 15 — 물빼기파이프 5 · 모래상태 · 자갈상태 5 · 강도 · 슬럼프 · 굵은골재 2. 자재 규격 · 단가 쪽 칸 → 엑셀 자재 이름 · 규격 글로 가려야 함(품으론 못 가림). + +### 1-4. 입력값 — 기본값 · 지금 M02 규격 읽기 · 옵션 찾기(맞은 줄) + +| 맞은 줄 | 기본값 그대로 맞음 | 지금 규격 읽기로 더 맞음 | 옵션 찾기로만 맞음 | +|---|---|---|---| +| 0.5% 87 | 34 | 8 | 45 | +| R 94 | 36 | 12 | **46** | + +- 지금 규격 읽기 = `M02_Hopyo_Match.spec_inputs`(규격 칸 · 이름 괄호 글) — 「소량」 · 「유형」처럼 블록 글에만 있는 조건을 못 읽음. + +### 1-5. 쉬운 예 + +| 엑셀 줄 | 정답 로직 | 판정(R) | 찾은 옵션 | 우리 품 = 엑셀 | +|---|---|---|---|---| +| N000 레미콘타설(장비사용) 「철근구조물-소량 타설」 | GC000198 | 하나만 | 장비사용타설 · 철근구조물 · **소량감률 50** | 콘크리트공 0.1091 · 보통인부 0.0364 · 굴착기 0.29 hr — 지금 읽기는 소량감률을 놓쳐 반값 | +| N007 야면석찰붙임 「L3=35cm」 | GF000225 | 하나만 | 뒷길이 35 | 석공 0.11 · 보통인부 0.04 · 굴착기+집게 0.22 (강도 · 슬럼프는 품에 안 닿음) | +| N001 합판거푸집 4회 | GC000211 | 여럿(같은 품) | 유형 보통 · 4회 · 높이감률 0 | 형틀목공 0.1111 · 보통인부 0.0444 · 합판 0.3914 · 각재 0.0144 — 측압 · 폼타이규격은 못 가름 | +| N003 철근가공조립 Type-Ⅰ(소량) | GC000206 + GC000207 | 여럿(같은 품) | Type-Ⅰ · Ⅰ-1 · 소량감률 50 | 철근공 4.196 · 보통인부 1.399 (구분 토목 · 건축 같은 품) | +| N013 레미콘타설 무근 소량 | GC000198 | 0.5% 없음 · R 하나만 | 장비사용 · 무근 · 소량감률 50 | 굴착기 0.127 hr ↔ 엑셀 0.13(반올림) | +| S085 콘크리트 펌프차 41m | GC000268 | 여럿(같은 품) | 기계 = 41m 펌프차 | 1 hr — 41m 기종 둘 넘게 | + +--- + +## 2. 「없음」 나눔 · 원문 대조 거리 + +### 2-1. 0.5% 에서만 없음 — 15 줄(R 또는 3% 맞음) + +| 까닭 | 줄 | 예 | +|---|---|---| +| 엑셀 반올림 | 7 | 레미콘 굴착기 시간 두 자리(0.13 ↔ 0.127 · 0.15 ↔ 0.1455) 6 · 뒷채움 품 0.009 ↔ 0.00909 1 | +| 옛 판 값이 3% 안 | 5 | 철근 가공조립 — 원문('24 보완) 조립 Ⅰ-1 3.4 ton = 우리 2.431 · 엑셀 3.5 ton 값 2.381 | +| 계수 어림 | 3 | 덤프 골재운반 — 엑셀 운반거리 · 계수가 조금 다름(0.339 ↔ 0.333) | + +### 2-2. R · 3% 모두 없음 — 73 줄 + +| 까닭(첫째) | 줄 | 쉬운 예 | 사용자 말 갈래 | +|---|---|---|---| +| 옛 판 값 | 15 (원문 확인 12) | 찰붙임 Y014 석공 0.05 ↔ 원문('26 보완) 0.10 · 동바리 C016 0.05 ↔ 0.06 | 옛 판 | +| 다른 책 숫자(짝 · 정답은 다른 책 로직) | 15 | 영월 구조물터파기(인력) 0.26 = 옛 건설 3-1-3 · 정답(짐작)은 산림 GF000101 · 믹서 0.185 hr 건설 ↔ 산림 0.148 | 옛 판 · 책 | +| 섞음 — 엑셀이 인력 % 를 곱함(+ 기계 채집 · 운반 묶음) | 9 | 고임돌 0.03 = 우리 GF000211 「막자갈」 0.3 × 10% · 조약돌 0.06 = 「조약돌 150mm」 0.6 × 10% | 섞음 | +| 섞음 비율을 엑셀이 바꿈(우리는 인력 10% 고정) | 3 | 구조물되메우기(인력 100%) 0.1 = 우리 GF000113 섞기 전 품 | 섞음 | +| 옵션 둘 넘게 섞음(합 · 평균) | 5 | 임도안전표지판 = GC000308 작업 「지주」 + 「표지판」 합(0.5% 안) 3 · 구조물터파기 = GC000083 토질 셋(보통 · 경질 · 자갈섞인)의 일 시공량 평균 2 | 계수 바꿈(옵션 섞음) | +| 할증 · 횟수 붙임 | 4 | 콘크리트포장 × 1.2 = 산림 12-6 ⑧ 종단경사 할증을 건설 로직에 · 도장 유성 2회 · 큰돌 품할증 0.75 | 할증 | +| 단위 환산 | 4 | 파형강관 원문 본당 ÷ 6 m/본(관경 1000 · 500 · 800) · PVC관 ÷ 4 | 계수 바꿈(환산) | +| 묶음(다른 일을 한 줄에) | 5 | 기초잡석 = 채집 9:1 + 운반 + 다짐 · 평떼 + 떼꽂이(산림 5-23-3) | 묶음 | +| 직종 바꿈 · 쪼갬 | 5 | 다웰바 + 철근공(철근 가공 6-2) · 철골공 ↔ 철골 가공조립공 · 원형돌망태 한 로직을 두 줄로 | 계수 바꿈 | +| 장비만 줄 — 시간 · 갈래 다름 | 4 | 터파기(암) 브레이커 0.333 ↔ 0.356 · 다지기 굴착기 ↔ 우리 콤팩터 · 야면석 기계 채집 ↔ 짝은 인력 채집 | 계수 바꿈 · 짝 | +| 다른 로직 숫자와 같음 | 3 | 펌프카 S084 = 산림 GF000182 그대로 · 콘크리트타설 Y008 = 산림 GF000166 · 포장 B-type U015 = 건설 GC000297 B-Type 20㎝ × 두께 0.2 | 정답 · 짝 다시 볼 거리 | +| 자재만 다름 | 1 | 문양거푸집 품 같음 · 자재 × 1/0.72(원문엔 없음) | 계수 바꿈 | +| **우리 로직이 원문과 다름** | **0** | — | — | + +- 「반올림」 15 · 「할증」 4 · 「섞음」 12 · 「옛 판」 15(+ 3% 안 5) · 「사용자가 계수 바꿈」(옵션 섞음 · 환산 · 묶음 · 직종 · 자재 · 장비) 24 · 「다른 책 · 다른 로직 숫자」 18 · 「우리 로직이 원문과 다름」 0. +- 까닭은 차례 규칙(허용 로직 숫자 → 밑품 · 섞음 → 섞기 전 → 옵션 합 · 평균 → 한 배수(÷ · × · 율로 설명될 때) → 다른 책 → 직종 → 장비 · 자재 → 옛 판 → 묶음) + 손 확인 25 줄 바로잡음(원문 대조 14 조항 · C4 가 본 쪼갬 · 묶음 · 할증 줄 — `k5_s4_sum12.CHECKED`). +- 「옵션 합 · 평균」은 엑셀 글에 그 옵션 낱말이 있을 때만 셈 — 낱말 없이 숫자만 맞는 6 건(거리 50 · 100 평균 등)은 우연으로 버림 · 안내표지판 C018 은 숫자가 N051 과 같아 넣음. + +### 2-3. 원문 대조 거리 — 14 조항(품셈 원문 `resources/knowledge/original/원가계산/`) + +| 조항 | 지금 원문 | 우리 로직 | 엑셀 | 원문과 같은 쪽 | +|---|---|---|---|---| +| 건설 6-2-3 철근 현장조립('24 보완) Ⅰ-1 6 · 2 ÷ 3.4 ton | 2.431 · 0.810(가공 포함) | 같음 | 2.381 · 0.794(3.5 ton) | 우리 | +| 건설 7-2-2 찰붙임('26 보완) 35㎝ 이하 30㎡ | 석공 0.10 · 보통 0.033 · 굴착기 0.267 | 같음 | 0.05 · 0.02 · 0.12 | 우리 | +| 건설 2-6-1 강관동바리('26 보완) 2.5m 이하 50공㎥ | 0.06 · 0.02 | 같음 | 0.05 · 0.017 / 0.054 · 0.021 | 우리 | +| 건설 2-7-1 강관비계('26 보완) 10m 이하 55㎡ | 0.0545 · 0.018(보호망 없으면 +8%) | 같음 | 0.05 · 0.02 | 우리 | +| 건설 3-5-1 절토면 고르기('25 보완) 연질토 250㎡ | 굴착기 0.032 | 같음 | 0.0276(290㎡) | 우리 | +| 건설 6-1-1 레미콘('24 보완) 장비사용 63 · 55㎥ | 0.0476 · 0.0545 | 같음 | 0.09 · 0.07 | 우리 | +| 건설 6-7-1 플륨관('25 보완) | 특별 · 보통 + 굴착기 | 같음 | 크레인 편성 | 우리 | +| 건설 4-1-1 잔디붙임('24 보완) 평떼 150㎡ | 조경 0.0067 · 보통 0.0267 | 같음 | 보통 0.0291 | 우리 | +| 건설 6-1-2 기계비빔 무근 | 0.15 · 0.46 | 같음(GF000156 도 같은 숫자) | 0.03 · 0.31 | 우리 | +| 산림 13-3-2 기초다짐(2025) 75mm 미만 | 보통 0.019 · 굴착기 0.076 | 같음 | 0.018 · 0.07 | 우리 | +| 건설 1-6-2 포장 인력 A-Type 20㎝ 100㎥ | 0.04 · 0.02 · 경사 할증 없음 | 같음 | × 1.2(산림 12-6 ⑧ 할증) | 우리 | +| 건설 6-7-2 파형강관 본당 | 0.24 · 0.12 · 크레인 0.25(Φ800) | 같음(본당) | m 당 ÷ 6 | 둘 다(환산만) | +| 건설 6-3-10 다웰바 | 0.043 · 0.009 | 같음 | + 철근공 | 우리 | +| 건설 6-3-4 문양거푸집 | 0.07 · 0.03 · 자재 1회 사용 | 같음 | 자재 × 1/0.72 | 우리 | + +- 원문 대조 거리(우리 로직이 원문과 다를 수 있는 줄) = **0 건**. 숫자가 다른 엑셀은 2024 · 2025 파일의 '24~'26 보완 전 옛 값 · 다른 책 할증 · 환산 · 묶음. +- 대조 못 한 것 — 「다른 책 숫자」 15 줄의 옛 건설 조항(3-1-3 · 9-1 · 9-3 · 9-7 — 지금 판에 그 번호 없음) · 장비만 줄 4. +- 곁 관찰(고침 제안 아님) — GC000211 · GC000214 「높이감률」 자동(견본)값 1 → 품 1% 줄어듦(C4 도 봄). 옵션 찾기에선 0 을 골라야 0.5% 맞음. + +--- + +## 3. 맞춤 방법 수치 — 정답을 모를 때 (로직 + 옵션) 줄 세우기 + +### 3-1. 다섯 갈래(1순위 엄격 / 느슨 · 다섯 안 엄격 / 느슨 · 1순위 옵션까지(R) · 옵션 하나로) + +| 방법 | 사람 126 | 새 파일 51 | 씨앗 짝 43 | 자동 정답 20 | +|---|---|---|---|---| +| C4 지문만(로직만 · 조항 · 이름 덤) — 재현 126/126 | 81.7 / 82.5 · 93.7 / 93.7 · 44.4 · 29.4 | 74.5 · 84.3 · 37.3 · 31.4 | 69.8 · 81.4 · 34.9 | 100 · 100 · 80.0 | +| 1 단계만(C4 변형 · 품 채널만) | 52.4 / 64.3 · 77.8 / 81.7 · 38.1 · 25.4 | 52.9 · 78.4 · 29.4 | 48.8 · 76.7 · 25.6 | 70.0 · 95.0 · 60.0 | +| 품만(로직 + 옵션 · 딱 맞음 먼저) | 60.3 / 65.1 · 76.2 / 77.0 · 44.4 · 29.4 | 58.8 · 76.5 · 37.3 | 55.8 · 72.1 · 34.9 | 90.0 · 100 · 80.0 | +| 지문 + 딱 맞음(R) 먼저 | 81.0 / 82.5 · 93.7 / 93.7 · 45.2 · 30.2 | 72.5 · 84.3 · 37.3 | 67.4 · 81.4 · 34.9 | 100 · 100 · 80.0 | +| (참고) C4 추천 = 지문 + 묶음 ①(β=1 · 같음 그대로) | 88.9 / 90.5 · 96.8 | — | — | — | + +- 1 단계 = C4 지문 변형(6,767)으로 1,438 로직을 품 채널(인력 3 · 장비 2 · 자재 1 · 단위 0.5)만 셈 → 위 15 로직. 2 단계 = 그 15 로직만 K5 옵션 공간(엑셀 조건으로 좁힘 · 곱 400 안 모두)을 엔진으로 돌려 0.5% · R · 3% 딱 맞음과 거리. 1 단계 15 안에 정답이 든 줄 사람 117/126 · 새 43/51. +- 「1순위 옵션까지」 = 1위 로직이 정답이고 그 로직의 찾은 옵션이 엑셀 품을 그대로 냄(R · 엑셀 몫을 다 덮음) · 「옵션 하나로」 = 그 옵션이 하나로 정해짐. + +### 3-2. 읽는 법 + +- **품만으론 로직을 못 고름** — 60.3%(C4 지문 81.7%). 「없음」 줄(41%)에선 품이 닮은 엉뚱한 로직이 1위 · 보통인부 하나뿐 줄은 같은 품 로직이 수십 · 조항 · 이름 신호가 빠짐. +- **딱 맞음을 지문 앞에 얹어도 로직 1순위는 그대로**(81.7 → 81.0 · 새 74.5 → 72.5) — 우연 맞음 1~2 줄(형틀목공 0.1 · 보통인부 0.02 = 갱폼)이 지문 1위를 밀어냄 · 지문의 품 크기 채널이 이미 가장 닮은 변형을 봄. +- **옵션 찾기는 「옵션까지」를 올림** — 1순위 로직이 맞은 줄(사람 103 · 새 38)에서 엑셀 품을 그대로 내는 입력 조합: C4 변형(64 조합 · 범위 기본값) 37 · 8 → K5 옵션 찾기 **56 · 19**. +- 쓰는 길 = 로직은 지문(+ 묶음 ① · 조합 풀이)으로 줄 세움 → 사용자가 고르거나 1위 → 그 로직의 옵션을 거꾸로 찾아 입력값을 채움 · 「맞음 / 다른 점 / 후보」 꼬리표. +- 셈 크기 — 2 단계는 줄당 15 로직 · 0.1~26 초(처음) · 같은 로직 · 조합은 캐시로 다시 안 셈(177 줄 엔진 셈 24,656 번). + +--- + +## 4. 화면 「맞음」 표시 기준 안 — 갈래마다 줄 수 · 틀린 적 + +### 4-1. 기준 + +| 표시 | 조건 | 화면 글 예 | +|---|---|---| +| **맞음** | 1위 (로직 + 옵션) 품 = 엑셀(R) · 엑셀 몫을 다 덮음(엑셀만 · 우리만 장비 없음 · 자재 다름 없음) · 숫자 하나뿐 줄 아님(직종 하나 · 장비 시간 하나 — 기계 1 시간 줄은 됨) | 「맞음 — GC000198 장비사용 · 철근 · 소량감률 50」 | +| 맞음 — 고름 | 위와 같은데 딱 맞는 로직이 둘 넘음 | 「맞음 둘 — GC000268 · GC000269(기계 41m 펌프차)」 | +| **다른 점 보임** | 1위 로직의 직종 묶음 = 엑셀 · 엑셀 인용 조항 = 그 로직 조항(윗마디 포함) · 품만 다름 | 「GC000083 · 계수 다름 — 토질 셋 평균 0.370 → 0.354」 · 「인력 10% 곱함」 · 「÷ 6 m/본 환산」 | +| **후보** | 그 밖 | 후보 다섯 | + +- 순위 = 지문 + 딱 맞음(R) 먼저(3-1 넷째 줄) · 「맞음」도 저장 · 확정 없이 표시만(227-4). + +### 4-2. 수치(1위 기준 · 틀림 = 1위가 정답 로직 아님) + +| 표시 | 사람 126: 줄 · 틀림 · 옵션 하나로 | 새 파일 51: 줄(엑셀 줄) · 틀림 · 옵션 하나로 | +|---|---|---| +| 맞음(로직 하나) | 46 · **1** · 33 | 15 (43) · **1** · 11 | +| 맞음 — 고름 | 12 · 0 · 5 | 6 (21) · 1 · 5 | +| 거의 맞음(3% 안 · R 아님) | 1 · 0 | 0 | +| 다른 점 보임 | 25 · **1** | 11 (32) · 1 | +| 후보 | 42 · 22 | 19 (50) · 11 | + +- 틀린 표시 5 줄 — 맞음: S084 펌프카(숫자 = 산림 GF000182 그대로 · 정답표 = 건설 GC000201 인용 조항) · C020 합판 6회(형틀목공 0.1 · 보통인부 0.02 가 갱폼 GC000223 과 우연히 같음 · 엑셀 2017 판) · 고름: C017 잡철물(숫자 = GC000676 · 씨앗 = GC000821) · 다른 점: Y025 콘크리트소운반(옛 9-1 식) · U002 콘크리트비빔(건설 현장비빔 GC000199 1위 · 씨앗 = 산림 GF000156). +- 「맞음」이 틀린 3 줄 중 2 는 정답표 · 짝 쪽이 숫자와 다른 줄(다시 볼 거리) · 우연 1. +- 다른 점 조건 견줌(사람 126) — 직종만 같음 39 줄 · 틀림 16(41%) · 직종 + 지문 점수 차 0.3 → 23 · 3 · **직종 + 인용 조항 같음 → 25 · 1(채택)**. +- C4 「딱 맞음」(직종 · 품 3% · 점수 차 · 126) 과 견줌 — C4 56 · K5 맞음 58 · 둘 다 45 · K5 만 13(옵션을 찾아야 맞는 줄 — 레미콘 「소량」 5 · 펌프차 · 크레인 · 철골세우기 기계 3 · 산림 펌프카 S084 …) · C4 만 11(C4 딱 맞음은 품 차 5% 안팎 · 장비 몫을 안 따짐 — 토질 평균 N008 · S076 · 옛 판 N032 · 밑품(섞음) N004 · N011 · 엑셀이 안 적은 장비 · 자재가 우리에 있는 줄 · 보통인부 하나뿐 줄). + +--- + +## 5. 한계 · 어떻게 쟀나 + +### 5-1. 한계 + +- 정답 — 사람 126(짐작 31 포함) · 자동 정답 20(엑셀 조항 · 품으로 만든 정답이라 맴돎) · 씨앗 43(사람 짝 · 숫자 검증 없음) → 새 파일 수치는 n 작음. +- 옵션 공간 — 수 칸은 엑셀 글에 칸 이름 · 단위가 붙은 수만 후보(아무 수로나 맞추지 않음) → 엑셀이 다른 낱말로 적은 수는 놓칠 수 있음 · 곱이 넘친 합판거푸집 GC000211(10 줄)은 엑셀 조건 낱말로 좁힘. +- 맞음은 직종 · 장비 시간 · 자재 수량 — 자재는 이름 낱말이 겹칠 때만 · 단가 · 금액 안 씀 · 반올림 허용(R)은 엑셀 값 자릿수로 짐작. +- 「없음」 까닭은 규칙 + 원문 확인 14 조항(손) · 「다른 책 숫자」 15 줄의 옛 조항은 지금 원문에 없어 숫자 대조 못 함. +- 2 단계는 1 단계 15 안 로직만(놓친 줄 사람 9 · 새 8) · 복합 정답(철근 · 도장)은 한 로직 1위로는 「옵션까지」가 안 됨. +- 「옵션 둘 합 · 평균」은 엑셀 글 낱말로만 거름 — 낱말 없는 진짜 합 · 평균은 놓칠 수 있음. + +### 5-2. 어떻게 쟀나 — 스크래치패드 `req227/K5/` · 저장소 venv `python -B` · 명령마다 120 초 안 + +| 스크립트 | 하는 일 → 결과 | +|---|---| +| `p00_inputs.py` · `p01_region.py` | 정답 로직 입력 칸 꼴 · 지역 · 보정작업 · 조종원직종이 품을 안 바꿈 확인(1,438 로직) | +| `k5_lib.py` | 옵션 공간(`space` · `plan` — 곱 CAP 안 모두 · 안 닿는 칸 · 같은 표 줄 값 묶음) · 엔진 셈 캐시(`run` · `run_cache.pkl` — C4 `s2_ours_fp.run_one` = M01 `mf.run`) · 맞대기(`compare` · `strong` · `csig` · `gap`) | +| `k5_s1_targets.py` | 대상 177 줄 엑셀 지문 → `targets.json` | +| `k5_s2_answer.py <시작> <끝>` · `merge` | 할 일 1 · 2 재료 — 정답 로직(복합은 몫 합 · 허용 로직도)마다 옵션 조합 · 0.5% · R · 3% 판정 · 지금 규격 읽기 · 없음 풀이(섞기 전 · 한 배수 · 옵션 합 · 평균) → `answer.json` | +| `k5_s4_sum12.py` | 표 1 · 여럿 나눔 · 입력값 겹침 · 반올림 · 없음 나눔 · 원문 확인 표 `CHECKED` → `sum12.json` | +| `k5_s5_rank.py` · `merge` | 1 단계(C4 지문 변형 · 품 채널) · 2 단계(15 로직 옵션 찾기) → `rank.json` | +| `k5_s6_sum34.py` · `p02_display.py` | 할 일 3 다섯 갈래 · C4 재현 · 할 일 4 표시 · 틀린 줄 · C4 딱 맞음 견줌 · 다른 점 조건 견줌 → `sum34.json` | + +- 재료 — C4 `excel_fp.json` · `s3_match`(지문 · `score_pair` · `ex_prep`) · `out_match.json` · C1 `labels` · `labels_y` · `s10_all` · C6 `items.json` · `data_*.json` · C7 `wide.json`. +- 저장소 쓰기 · git 쓰기 없음 · `resources/master_data` · 일위대가조합.json · 실무문서 · 품셈 원문은 읽기만. + From 8a4598c0511983c2dbd2bcec2392cdb33da81280 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 15 Oct 2026 22:39:05 +0900 Subject: [PATCH 51/81] =?UTF-8?q?chore(=EC=9A=94=EA=B5=AC):=20229=20?= =?UTF-8?q?=E2=80=94=20CAD=20=EB=AA=85=EB=A0=B9=20=C2=B7=20=EC=98=B5?= =?UTF-8?q?=EC=85=98=20=EC=A1=B0=EC=9E=91=EC=9D=84=20=EC=98=A4=ED=86=A0?= =?UTF-8?q?=EC=BA=90=EB=93=9C=EC=99=80=20=EA=B0=99=EA=B2=8C=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20=C2=B7=20=EC=99=B8=EB=B6=80=20=EC=9D=BC=EC=9C=84?= =?UTF-8?q?=EB=8C=80=EA=B0=80=20=3D=20=EC=97=B0=EA=B2=B0=EB=90=9C=20?= =?UTF-8?q?=EB=8B=A8=EA=B0=80=EC=82=B0=EC=B6=9C=EB=A1=9C=20=EC=9A=B0?= =?UTF-8?q?=EB=A6=AC=20=EB=A1=9C=EC=A7=81=20=EB=A7=9E=EC=B6=A4=20=E2=86=92?= =?UTF-8?q?=20=EC=9D=BC=EC=9C=84=EB=8C=80=EA=B0=80=20=EA=B5=AC=EC=84=B1=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20=C2=B7=20=EB=81=9D=EC=97=90=20=EB=B8=8C?= =?UTF-8?q?=EB=A0=88=EC=9D=B8=20=EB=B8=8C=EB=9D=BC=EC=9A=B0=EC=A0=80=20?= =?UTF-8?q?=EA=B2=80=EC=A6=9D=20+=20=EC=95=84=ED=8B=B0=ED=8C=A9=ED=8A=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_01C6BNBFXFndbqYxu6sZuknK --- docs/raw/요구_1010_2/사용자요구_1010_2.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/raw/요구_1010_2/사용자요구_1010_2.md b/docs/raw/요구_1010_2/사용자요구_1010_2.md index 42a7088a9..361c05920 100644 --- a/docs/raw/요구_1010_2/사용자요구_1010_2.md +++ b/docs/raw/요구_1010_2/사용자요구_1010_2.md @@ -291,3 +291,6 @@ - 현재 우리가 확인하고 구현해야하는 로직은 사용자들이 가지고 있는 문서들을 불러와서 우리프로그램의 단가산출과 매칭하고 자동이 안될경우는 사용자가 선택하게 하되 별칭으로 남겨 일위대가항목을 쉽게 반영하기 위함. << 이방식에 대한 로직구현인지 다시 고민할 것. 구현은 3번과 동시 상세계획서 작성과 코드작업이 진행할 예정이니 천천히 목적을 고민해 볼것. 228. (원문 · 10-15) 참고로 내가 지정해준 엑셀 일위대가에는 우리 로직의 옵션을 지정 후 나온값일 확율이 높음. 그래서 내가 값보다는 로직을 보라고 한것임. 다음 코덱스는 내가 실행해둠 오르카 리스트로 재확인 +229. (원문 · 10-15) 3번은 결국 명령어 입력후 연계된 명령어 입력으로 형상을 그리는 제어에 대한 내용이야. 대부분 찾은것 같은데 중요한 점은 오토캐드와 조작순서와 조작법이 같아야 사용자들이 사용하기 쉽다는걸 너가 이해해야해. 횔 확대는 벌서 들어간 내용인것같은데 확인해보고. 만약 충돌이나는 부분이 있으면 인터넷에 오토캐드를 조사해서 동일하게 반영해주면 됨. +4번은 너가 말한 목적이 애매모호함. 내가 원하는 방향은 대부분 우리 단가산출로직은 거의 품셈원문으로 기준으로 로직을 만들어뒀어. 너가 만들어야하는 로직은 엑셀로 들어오는 하나의 일위대가의 단가산출 리스트, 계산식을 보고 분석하여 매칭되는 우리 프로그램의 단가산출 로직을 찾아서 매칭하고 하나의 일위대가로 구성하는거야. 그런데 여기서 이름이 다르니 당연히 엑셀의 일위대가에 하이퍼링크로 연결된 단가산출 로직을 보면 우리의 프로그램의 단가산출 로직 매칭할수 있어. 어차피 엑셀도 품셈원문을 보고 만들었거든. 그러니 매칭하는 로직을 구현해달라는거였어. 내가 설명한 내용이 맞다면 계획서를 작성해서 진행하고 안 맞는 부분은 다시 확인해서 계획서 반영해. 중요한 점은 너가 분석하는건 의미가 없어. 결국 언제든 엑셀의 이름은 다를수 있으니 잘 매칭하는 로직 구현이 중요해. +일단 상당히 많은 검토를 했으니 조심해서 꼼꼼히 목표를 달설하게 계획서를 작성하고 코드작업을 진행해줘. 완료 후에는 너가 브라우져를 제어해서 검증해보고 아티팩트로 작성해서 결과를 보여줘. 나는 이제 쉬러 갈거니까 너가 브레인으로써 진행해. From 7a8f33452098e382987341160f4ec7823a614039 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 15 Oct 2026 23:08:43 +0900 Subject: [PATCH 52/81] =?UTF-8?q?chore(=EC=A1=B0=EC=82=AC):=20338=20?= =?UTF-8?q?=EC=99=B8=EB=B6=80=20=EC=9D=BC=EC=9C=84=EB=8C=80=EA=B0=80=20?= =?UTF-8?q?=EB=A7=9E=EC=B6=A4=20=EC=84=A4=EA=B3=84=20=E2=80=94=20=EC=97=B0?= =?UTF-8?q?=EA=B2=B0=EB=90=9C=20=EB=8B=A8=EA=B0=80=EC=82=B0=EC=B6=9C=20?= =?UTF-8?q?=EB=B8=94=EB=A1=9D=20=E2=86=92=20=EC=9A=B0=EB=A6=AC=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20+=20=EC=98=B5=EC=85=98=20=E2=86=92=20=EC=9D=BC?= =?UTF-8?q?=EC=9C=84=EB=8C=80=EA=B0=80=20=EA=B5=AC=EC=84=B1=20=C2=B7=20?= =?UTF-8?q?=EC=B0=BD=20=EC=85=8B=20=C2=B7=20=EC=8B=9C=ED=97=98=20=C2=B7=20?= =?UTF-8?q?=EA=B8=B0=EC=A4=80=20=EC=88=98=EC=B9=98=20+=20=EC=8B=A4?= =?UTF-8?q?=ED=97=98=20=EC=9E=90=EB=A3=8C=20=EC=82=AC=EB=B3=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_01C6BNBFXFndbqYxu6sZuknK --- .../조사/2026-10-15_338_실험자료/C1/cats.py | 67 + .../조사/2026-10-15_338_실험자료/C1/labels.py | 461 + .../2026-10-15_338_실험자료/C1/labels_y.py | 43 + .../2026-10-15_338_실험자료/C1/out_items.json | 13532 +++++++++ .../2026-10-15_338_실험자료/C1/s01_extract.py | 105 + .../2026-10-15_338_실험자료/C1/s02_view.py | 63 + .../2026-10-15_338_실험자료/C1/s03_find.py | 18 + .../2026-10-15_338_실험자료/C1/s04_eval.py | 257 + .../2026-10-15_338_실험자료/C1/s04b_diff.py | 30 + .../2026-10-15_338_실험자료/C1/s05_more.py | 330 + .../2026-10-15_338_실험자료/C1/s05b_diff.py | 17 + .../2026-10-15_338_실험자료/C1/s06_memory.py | 103 + .../2026-10-15_338_실험자료/C1/s07_fail.py | 33 + .../2026-10-15_338_실험자료/C1/s09_stmate.py | 124 + .../2026-10-15_338_실험자료/C1/s10_all.py | 201 + .../2026-10-15_338_실험자료/C1/s10b_diff.py | 29 + .../2026-10-15_338_실험자료/C1/s11_buttons.py | 27 + .../2026-10-15_338_실험자료/C1/s12_units.py | 26 + .../C1/s13_money_flag.py | 20 + .../C1/s14_fails_all.py | 26 + .../2026-10-15_338_실험자료/C1/s15_cats.py | 19 + .../2026-10-15_338_실험자료/C1/s16_guard.py | 57 + .../2026-10-15_338_실험자료/C1/s17_report.py | 265 + .../C1/s18_name_all.py | 25 + .../2026-10-15_338_실험자료/C4/fp_common.py | 256 + .../2026-10-15_338_실험자료/C4/p00_peek.py | 10 + .../2026-10-15_338_실험자료/C4/p01_run.py | 17 + .../2026-10-15_338_실험자료/C4/p02_survey.py | 33 + .../2026-10-15_338_실험자료/C4/p03_yw_peek.py | 17 + .../2026-10-15_338_실험자료/C4/p05_why.py | 16 + .../2026-10-15_338_실험자료/C4/s1_excel_fp.py | 339 + .../2026-10-15_338_실험자료/C4/s2_ours_fp.py | 253 + .../2026-10-15_338_실험자료/C4/s3_match.py | 355 + .../2026-10-15_338_실험자료/C4/s4_changes.py | 330 + .../2026-10-15_338_실험자료/C4/s5_summary.py | 24 + .../C4/s6_specinputs.py | 41 + .../2026-10-15_338_실험자료/C5/c01_trees.py | 489 + .../2026-10-15_338_실험자료/C5/c02_ourvec.py | 102 + .../C5/c02b_varcache.py | 21 + .../2026-10-15_338_실험자료/C5/c03_vec.py | 107 + .../2026-10-15_338_실험자료/C5/c04_closed.py | 304 + .../2026-10-15_338_실험자료/C5/c05_solve.py | 254 + .../2026-10-15_338_실험자료/C5/c06_lines.py | 109 + .../2026-10-15_338_실험자료/C5/c07_hopyo.py | 170 + .../2026-10-15_338_실험자료/C5/c08_drop.py | 41 + .../2026-10-15_338_실험자료/C5/c09_direct.py | 183 + .../2026-10-15_338_실험자료/C5/c10_others.py | 206 + .../2026-10-15_338_실험자료/C5/t_ablate.py | 19 + .../조사/2026-10-15_338_실험자료/C5/t_fail.py | 26 + .../2026-10-15_338_실험자료/C5/t_fallback.py | 15 + .../조사/2026-10-15_338_실험자료/C5/t_off.py | 33 + .../조사/2026-10-15_338_실험자료/C5/t_prof.py | 18 + .../조사/2026-10-15_338_실험자료/C5/t_rows.py | 7 + .../2026-10-15_338_실험자료/C5/t_stats.py | 18 + .../조사/2026-10-15_338_실험자료/C5/t_vec.py | 23 + .../2026-10-15_338_실험자료/C5/x00_sheets.py | 26 + .../2026-10-15_338_실험자료/C5/x01_peek.py | 14 + .../2026-10-15_338_실험자료/C5/x02_codes.py | 21 + .../2026-10-15_338_실험자료/C6/s00_sheets.py | 20 + .../2026-10-15_338_실험자료/C6/s01_peek.py | 24 + .../2026-10-15_338_실험자료/C6/s02_read.py | 210 + .../2026-10-15_338_실험자료/C6/s03_items.py | 202 + .../2026-10-15_338_실험자료/C6/s04_auto.py | 202 + .../C6/s05_handcheck.py | 26 + .../2026-10-15_338_실험자료/C6/s06_match.py | 69 + .../2026-10-15_338_실험자료/C6/s07_score.py | 96 + .../2026-10-15_338_실험자료/C6/s08_memory.py | 290 + .../C6/s09_examples.py | 26 + .../2026-10-15_338_실험자료/C6/s10_policy.py | 87 + .../2026-10-15_338_실험자료/C6/s11_extra.py | 28 + .../2026-10-15_338_실험자료/C6/s12_combo.py | 55 + .../C6/s13_distinct.py | 23 + .../조사/2026-10-15_338_실험자료/C7/k0_lib.py | 205 + .../2026-10-15_338_실험자료/C7/k1_stack.py | 82 + .../2026-10-15_338_실험자료/C7/k2_detail.py | 31 + .../조사/2026-10-15_338_실험자료/C7/k3_vec.py | 139 + .../2026-10-15_338_실험자료/C7/k4_wide.py | 68 + .../2026-10-15_338_실험자료/C7/k5_hold.py | 115 + .../조사/2026-10-15_338_실험자료/C7/k6_sum.py | 55 + .../C7/k7_wide_stack.py | 100 + .../C7/k8_sure_same.py | 78 + .../조사/2026-10-15_338_실험자료/K5/k5_lib.py | 485 + .../K5/k5_s1_targets.py | 123 + .../K5/k5_s2_answer.py | 302 + .../2026-10-15_338_실험자료/K5/k5_s4_sum12.py | 236 + .../2026-10-15_338_실험자료/K5/k5_s5_rank.py | 121 + .../2026-10-15_338_실험자료/K5/k5_s6_sum34.py | 191 + .../2026-10-15_338_실험자료/K5/p00_inputs.py | 77 + .../2026-10-15_338_실험자료/K5/p01_region.py | 39 + .../2026-10-15_338_실험자료/K5/p02_display.py | 59 + .../2026-10-15_338_실험자료/K5/targets.json | 22638 ++++++++++++++++ .../조사/2026-10-15_338_실험자료/README.md | 11 + .../조사/2026-10-15_338_일위대가맞춤_설계.md | 566 + 93 files changed, 46854 insertions(+) create mode 100644 docs/raw/조사/2026-10-15_338_실험자료/C1/cats.py create mode 100644 docs/raw/조사/2026-10-15_338_실험자료/C1/labels.py create mode 100644 docs/raw/조사/2026-10-15_338_실험자료/C1/labels_y.py create mode 100644 docs/raw/조사/2026-10-15_338_실험자료/C1/out_items.json create mode 100644 docs/raw/조사/2026-10-15_338_실험자료/C1/s01_extract.py create mode 100644 docs/raw/조사/2026-10-15_338_실험자료/C1/s02_view.py create mode 100644 docs/raw/조사/2026-10-15_338_실험자료/C1/s03_find.py create mode 100644 docs/raw/조사/2026-10-15_338_실험자료/C1/s04_eval.py create mode 100644 docs/raw/조사/2026-10-15_338_실험자료/C1/s04b_diff.py create mode 100644 docs/raw/조사/2026-10-15_338_실험자료/C1/s05_more.py create mode 100644 docs/raw/조사/2026-10-15_338_실험자료/C1/s05b_diff.py create mode 100644 docs/raw/조사/2026-10-15_338_실험자료/C1/s06_memory.py create mode 100644 docs/raw/조사/2026-10-15_338_실험자료/C1/s07_fail.py create mode 100644 docs/raw/조사/2026-10-15_338_실험자료/C1/s09_stmate.py create mode 100644 docs/raw/조사/2026-10-15_338_실험자료/C1/s10_all.py create mode 100644 docs/raw/조사/2026-10-15_338_실험자료/C1/s10b_diff.py create mode 100644 docs/raw/조사/2026-10-15_338_실험자료/C1/s11_buttons.py create mode 100644 docs/raw/조사/2026-10-15_338_실험자료/C1/s12_units.py create mode 100644 docs/raw/조사/2026-10-15_338_실험자료/C1/s13_money_flag.py create mode 100644 docs/raw/조사/2026-10-15_338_실험자료/C1/s14_fails_all.py create mode 100644 docs/raw/조사/2026-10-15_338_실험자료/C1/s15_cats.py create mode 100644 docs/raw/조사/2026-10-15_338_실험자료/C1/s16_guard.py create mode 100644 docs/raw/조사/2026-10-15_338_실험자료/C1/s17_report.py create mode 100644 docs/raw/조사/2026-10-15_338_실험자료/C1/s18_name_all.py create mode 100644 docs/raw/조사/2026-10-15_338_실험자료/C4/fp_common.py create mode 100644 docs/raw/조사/2026-10-15_338_실험자료/C4/p00_peek.py create mode 100644 docs/raw/조사/2026-10-15_338_실험자료/C4/p01_run.py create mode 100644 docs/raw/조사/2026-10-15_338_실험자료/C4/p02_survey.py create mode 100644 docs/raw/조사/2026-10-15_338_실험자료/C4/p03_yw_peek.py create mode 100644 docs/raw/조사/2026-10-15_338_실험자료/C4/p05_why.py create mode 100644 docs/raw/조사/2026-10-15_338_실험자료/C4/s1_excel_fp.py create mode 100644 docs/raw/조사/2026-10-15_338_실험자료/C4/s2_ours_fp.py create mode 100644 docs/raw/조사/2026-10-15_338_실험자료/C4/s3_match.py create mode 100644 docs/raw/조사/2026-10-15_338_실험자료/C4/s4_changes.py create mode 100644 docs/raw/조사/2026-10-15_338_실험자료/C4/s5_summary.py create mode 100644 docs/raw/조사/2026-10-15_338_실험자료/C4/s6_specinputs.py create mode 100644 docs/raw/조사/2026-10-15_338_실험자료/C5/c01_trees.py create mode 100644 docs/raw/조사/2026-10-15_338_실험자료/C5/c02_ourvec.py create mode 100644 docs/raw/조사/2026-10-15_338_실험자료/C5/c02b_varcache.py create mode 100644 docs/raw/조사/2026-10-15_338_실험자료/C5/c03_vec.py create mode 100644 docs/raw/조사/2026-10-15_338_실험자료/C5/c04_closed.py create mode 100644 docs/raw/조사/2026-10-15_338_실험자료/C5/c05_solve.py create mode 100644 docs/raw/조사/2026-10-15_338_실험자료/C5/c06_lines.py create mode 100644 docs/raw/조사/2026-10-15_338_실험자료/C5/c07_hopyo.py create mode 100644 docs/raw/조사/2026-10-15_338_실험자료/C5/c08_drop.py create mode 100644 docs/raw/조사/2026-10-15_338_실험자료/C5/c09_direct.py create mode 100644 docs/raw/조사/2026-10-15_338_실험자료/C5/c10_others.py create mode 100644 docs/raw/조사/2026-10-15_338_실험자료/C5/t_ablate.py create mode 100644 docs/raw/조사/2026-10-15_338_실험자료/C5/t_fail.py create mode 100644 docs/raw/조사/2026-10-15_338_실험자료/C5/t_fallback.py create mode 100644 docs/raw/조사/2026-10-15_338_실험자료/C5/t_off.py create mode 100644 docs/raw/조사/2026-10-15_338_실험자료/C5/t_prof.py create mode 100644 docs/raw/조사/2026-10-15_338_실험자료/C5/t_rows.py create mode 100644 docs/raw/조사/2026-10-15_338_실험자료/C5/t_stats.py create mode 100644 docs/raw/조사/2026-10-15_338_실험자료/C5/t_vec.py create mode 100644 docs/raw/조사/2026-10-15_338_실험자료/C5/x00_sheets.py create mode 100644 docs/raw/조사/2026-10-15_338_실험자료/C5/x01_peek.py create mode 100644 docs/raw/조사/2026-10-15_338_실험자료/C5/x02_codes.py create mode 100644 docs/raw/조사/2026-10-15_338_실험자료/C6/s00_sheets.py create mode 100644 docs/raw/조사/2026-10-15_338_실험자료/C6/s01_peek.py create mode 100644 docs/raw/조사/2026-10-15_338_실험자료/C6/s02_read.py create mode 100644 docs/raw/조사/2026-10-15_338_실험자료/C6/s03_items.py create mode 100644 docs/raw/조사/2026-10-15_338_실험자료/C6/s04_auto.py create mode 100644 docs/raw/조사/2026-10-15_338_실험자료/C6/s05_handcheck.py create mode 100644 docs/raw/조사/2026-10-15_338_실험자료/C6/s06_match.py create mode 100644 docs/raw/조사/2026-10-15_338_실험자료/C6/s07_score.py create mode 100644 docs/raw/조사/2026-10-15_338_실험자료/C6/s08_memory.py create mode 100644 docs/raw/조사/2026-10-15_338_실험자료/C6/s09_examples.py create mode 100644 docs/raw/조사/2026-10-15_338_실험자료/C6/s10_policy.py create mode 100644 docs/raw/조사/2026-10-15_338_실험자료/C6/s11_extra.py create mode 100644 docs/raw/조사/2026-10-15_338_실험자료/C6/s12_combo.py create mode 100644 docs/raw/조사/2026-10-15_338_실험자료/C6/s13_distinct.py create mode 100644 docs/raw/조사/2026-10-15_338_실험자료/C7/k0_lib.py create mode 100644 docs/raw/조사/2026-10-15_338_실험자료/C7/k1_stack.py create mode 100644 docs/raw/조사/2026-10-15_338_실험자료/C7/k2_detail.py create mode 100644 docs/raw/조사/2026-10-15_338_실험자료/C7/k3_vec.py create mode 100644 docs/raw/조사/2026-10-15_338_실험자료/C7/k4_wide.py create mode 100644 docs/raw/조사/2026-10-15_338_실험자료/C7/k5_hold.py create mode 100644 docs/raw/조사/2026-10-15_338_실험자료/C7/k6_sum.py create mode 100644 docs/raw/조사/2026-10-15_338_실험자료/C7/k7_wide_stack.py create mode 100644 docs/raw/조사/2026-10-15_338_실험자료/C7/k8_sure_same.py create mode 100644 docs/raw/조사/2026-10-15_338_실험자료/K5/k5_lib.py create mode 100644 docs/raw/조사/2026-10-15_338_실험자료/K5/k5_s1_targets.py create mode 100644 docs/raw/조사/2026-10-15_338_실험자료/K5/k5_s2_answer.py create mode 100644 docs/raw/조사/2026-10-15_338_실험자료/K5/k5_s4_sum12.py create mode 100644 docs/raw/조사/2026-10-15_338_실험자료/K5/k5_s5_rank.py create mode 100644 docs/raw/조사/2026-10-15_338_실험자료/K5/k5_s6_sum34.py create mode 100644 docs/raw/조사/2026-10-15_338_실험자료/K5/p00_inputs.py create mode 100644 docs/raw/조사/2026-10-15_338_실험자료/K5/p01_region.py create mode 100644 docs/raw/조사/2026-10-15_338_실험자료/K5/p02_display.py create mode 100644 docs/raw/조사/2026-10-15_338_실험자료/K5/targets.json create mode 100644 docs/raw/조사/2026-10-15_338_실험자료/README.md create mode 100644 docs/raw/조사/2026-10-15_338_일위대가맞춤_설계.md diff --git a/docs/raw/조사/2026-10-15_338_실험자료/C1/cats.py b/docs/raw/조사/2026-10-15_338_실험자료/C1/cats.py new file mode 100644 index 000000000..98ea384e4 --- /dev/null +++ b/docs/raw/조사/2026-10-15_338_실험자료/C1/cats.py @@ -0,0 +1,67 @@ +"""실패 까닭 갈래 — 1순위(엄격) 실패 줄마다 주 갈래 하나(첫 칸) + 곁 갈래. +이름만(씨앗 끔) 실패 · 지금 실패를 따로 봄(같은 줄이라도 까닭이 다르면 둘째 표에서 고침).""" + +A = "같은 뜻 다른 말" +B = "줄임말" +C = "낱말 차례" +D = "괄호 속 규격" +E = "비율 표기(인력100%·9:1)" +F = "단위 표기(hr·공/㎥·개소)" +G = "장비 · 노임 이름 줄" +H = "책 갈래(산림↔건설)" +I = "우리 로직이 더 잘게/굵게" +J = "엑셀 복합 호표" +K = "근거 조항 비어 있음 · 엉뚱" +LL = "품셈 판 차이(번호 바뀜)" +M = "그 밖(오타 · 비슷한 이름 함정 · 접힘)" +ORDER = [A, B, C, D, E, F, G, H, I, J, K, LL, M] + +NAME = { # 이름만(씨앗 끔) 실패 까닭 + "N000": [B, J, H], "N013": [B, J], "N024": [B, J], "N030": [B, J], "N043": [B, J], + "S052": [B, J], "S053": [B, J], "S069": [B, J], "S075": [B, J], + "N001": [H], "N025": [H], "N014": [H], "N015": [H], "N032": [H], "N044": [H, F], "N046": [H, F], + "S056": [H], "S057": [H, F], "S058": [H, D], "S054": [H, D], "S064": [H], "S070": [H], "S086": [H], + "N007": [H, A], "N027": [H, A], "N028": [H, A, C], "N029": [H, A, C], "N031": [H, A], + "S098": [H, A, C], "S099": [H, A, C], "N038": [H, A], "N040": [H, A], "N037": [H, M], + "S084": [H, B], "Y001": [H], "Y009": [H], "Y021": [H], "Y029": [H], + "N003": [I, C, M], "N019": [I, C, M], "N045": [I, C, M], "S055": [I, C, M], "S059": [I, C, M], + "Y010": [I, C, M], + "N005": [A, J, K, M], "N012": [A, J, K, M], "N036": [A, J, K, M], "S081": [A, J, K], "S091": [A, J, K], + "Y015": [A, J], + "N006": [A, LL], "Y024": [A, LL], "Y025": [A, LL], + "N008": [A, E], "S076": [A, H], "N018": [A], "N022": [A, J], "S092": [A, J], "S096": [A], + "N039": [A, J], "S061": [A, K], "S072": [A, M, F], "S083": [A, M, F], "Y016": [A, J], + "Y017": [A, M], "Y023": [A, B], "Y027": [A, M], "Y032": [A], "Y033": [A, J], + "N023": [D], "N042": [D], "S066": [D, J], "S082": [D], "N050": [J, D], + "N035": [M], "N047": [M], "S067": [M], "Y000": [M], "Y020": [M], "Y028": [M], + "Y008": [LL, H], + "S085": [G, F], "Y005": [G, F], +} + +NOW = { # 지금(330 뒤) 실패 까닭 + "N001": [H], "N025": [H], "N015": [H], "S086": [H], "N032": [H], "N044": [H, F], "S057": [H, F], + "N046": [H, F], "S056": [H], "N037": [H, M], "Y029": [H, M], + "N003": [I, LL], "N019": [I, LL], "N045": [I, LL], "S055": [I, LL], "S059": [I, LL], + "Y010": [M, I], + "N005": [A, K, J], "N012": [A, K, J], "N036": [A, K, J], "S081": [A, K, J], "S091": [A, K, J], + "Y015": [A, J], + "N006": [A, LL], "Y024": [A, LL], "Y025": [A, LL], "S061": [A, K], "Y027": [A, M], + "S062": [J, LL], "S066": [D, J], "S085": [G, F], "Y005": [G, F], + "Y009": [M], "Y021": [M], "Y020": [M], "Y028": [M], +} + +NOTE = { # 표 예시 글(갈래별 셋) + A: ["휠라설치 → 신축이음 채움재 설치", "구조물터파기(인력100%) → 굴착(인력/토사)", "고임돌 · 조약돌 채집 → 모래·자갈·약돌 채집(1위 궤도 자갈채집)"], + B: ["레미콘타설(장비사용) → 레디믹스트콘크리트 타설(1위 펌프카 타설)", "배수관부설및접합(P.V.C) → PVC관 고무링 접합 및 부설", "B/H 90%(백호 = 굴착기)"], + C: ["철근가공조립(현장) ↔ 철근 현장가공 및 조립", "큰돌찰붙임 ↔ 큰돌붙이기 찰붙이기", "(곁 갈래만 · 주 까닭 아님)"], + D: ["큰돌쌓기(찰쌓기) → 1위 메쌓기(괄호 「찰쌓기」 버림)", "구조물터파기(토사) → 1위 터파기(기계) 암", "구조물터파기(암) → 1위 육상토사"], + E: ["구조물터파기(인력100%)", "고임돌채집및운반(9:1)", "(곁 갈래만 — 비율 글은 지금도 낱말로 안 셈)"], + F: ["대형브레이카 hr ↔ 시간", "강관동바리 공/㎥ ↔ 공㎥", "도로반사경 개소 ↔ 본(환산 「입력」)"], + G: ["콘크리트 펌프차 41m [hr] → 기계 시간당 기계경비(기계 입력)", "크레인(타이어) 10톤 [시간]", "철골공 · 특별인부 [인] = 노임(우리에 없음)"], + H: ["합판거푸집 → 같음 산림 12-4(엑셀은 건설 6-3-1)", "강관비계 · 강관동바리 → 같음 산림(엑셀 건설 2-7-1 · 2-6-1)", "큰돌메붙임 → 1위 건설 메붙임(굴착기+집게)(엑셀 산림 13-7-1)"], + I: ["철근가공조립(현장) = 우리 현장가공 + 현장조립 둘", "잔토처리(인력) = 우리 굴착(인력) 입력 「잔토처리」에 묶임", "원형돌망태 조립설치 · 채움 = 우리 원형 돌망태 하나"], + J: ["레미콘타설 + 굴착기 시간", "고임돌 = 채집 + 상차 + 운반", "도장(철재면2회) = 바탕 + 녹막이 + 유성 셋"], + K: ["철골설치(출처 없음)", "조약돌채집운반(9:1)(출처 없음)", "고임돌 「1-10」 = 단위중량 인용(작업 조항 아님)"], + LL: ["철근 「건설 6-2」 = 옛 한 조항 → 지금 6-2-2 · 6-2-3", "야면석채집 「건설 8-2 (2016 삭제)」", "영월 「건설 3-1-3 · 9-3 · 1-9-3 · 임도품셈 5-5」 옛 번호"], + M: ["흙깍기(오타 깍/깎) → 후보 없음", "원형돌망태 → 1위 타원형(원형 ⊂ 타원형)", "유로폼 설치,해체 → 같은 이름 산림 · 건설 둘이 이름 후보에서 하나로 접힘"], +} diff --git a/docs/raw/조사/2026-10-15_338_실험자료/C1/labels.py b/docs/raw/조사/2026-10-15_338_실험자료/C1/labels.py new file mode 100644 index 000000000..651149fcf --- /dev/null +++ b/docs/raw/조사/2026-10-15_338_실험자료/C1/labels.py @@ -0,0 +1,461 @@ +"""정답 표시 — 고유 로직 줄 100(북부청 임도 N000~N051 · 사방 S052~S099). + +정답 = 엑셀 단가산출 블록의 출처 조항 · 직종 · 품 · 계산식이 같은 우리 로직(복합이면 주된 몫 · 여럿 가능). +허용 = 같은 일의 다른 책(산림 ↔ 건설) 로직 · 복합의 곁 몫(느슨 지표에서만 맞음). +부분 = 「우리에 없음」 줄의 가까운 조각(지표에 안 셈 · 참고). +짐작 = 블록에 출처가 없거나 우리 로직이 일부만 덮어 사람 판단이 섞임. +갈래 = 이름이 엇나간 까닭(실패 갈래 표에 씀). +""" + +CONCRETE = { + "정답": ["GC000198"], + "허용": ["GF000155"], + "근거": "건설 6-1-1 콘크리트공 3 · 보통인부 1 ÷ 일 시공량(+소량 ×2) + 굴착기 시간 — GC000198 6-1-1(타설방법 · 소량감률 · 굴착기 입력)", + "갈래": ["같은 뜻 다른 말", "엑셀 복합"], +} +REBAR = { + "정답": ["GC000206", "GC000207"], + "허용": ["GF000159"], + "근거": "건설 6-2 Type 가공(철근공 0.67 · 보통 0.22) + 조립 — 우리 6-2-2 가공 · 6-2-3 조립 둘(복합) · 산림 12-3 GF000159 같은 일 · 이름 1위 GF000187 은 산림 12-18 복잡 고정 4.2인(다른 품)", + "갈래": ["우리 로직이 더 잘게", "엑셀 조항이 윗마디", "비슷한 이름 엉뚱"], +} +PLY = { + "정답": ["GC000211"], + "허용": ["GF000160"], + "근거": "건설 6-3-1 형틀목공 5 · 보통인부 2 ÷ 일 시공량 = GC000211 6-3-1 · 이름 같음 GF000160 은 산림 12-4", + "갈래": ["같은 이름 다른 책"], +} +FORM_PATTERN = { + "정답": ["GC000215"], + "허용": ["GF000161"], + "근거": "건설 6-3-4 형틀목공 0.07 · 보통 0.03 = GC000215 · 이름 같음 GF000161 은 산림 12-5", + "갈래": ["같은 이름 다른 책"], +} +EURO = { + "정답": ["GC000214"], + "허용": ["GF000210"], + "근거": "건설 6-3-3 형틀목공 4 · 보통 1 ÷ 35~40 = GC000214", + "갈래": [], +} +WEDGE = { + "정답": ["GF000211"], + "허용": ["GF000212"], + "근거": "채집(기계 90 · 인력 10% 보통인부 0.3인/㎥ = 산림 13-2-1 막자갈) + 상차 + 운반 — GF000211 은 인력 채집 몫만 · 출처 1-10 은 단위중량 인용 · 1위 GC000363 은 궤도 자갈(토목 4-6-1)", + "짐작": True, + "갈래": ["같은 뜻 다른 말", "엑셀 복합", "근거 조항 엉뚱", "비슷한 이름 엉뚱"], +} +PEBBLE = { + **WEDGE, + "허용": [], + "근거": "채집 인력 10%(보통인부 0.3 · 0.6인/㎥ = 산림 13-2-1 막자갈 · 조약돌) + 상차 + 운반 — GF000211(부분) · 1위 GC000363 궤도 자갈", +} +FIELDSTONE = { + "정답": ["GF000214"], + "허용": [], + "근거": "인력 10% 보통인부 0.17인/㎡(= 산림 13-2-4 뒷길이 35㎝) + 굴착기 채집 + 운반 — GF000214 은 인력 채집 몫(부분) · 건설 8-2 는 2016 삭제", + "짐작": True, + "갈래": ["엑셀 복합", "품셈 판 번호 바뀜"], +} +SCAFFOLD = { + "정답": ["GC000022"], + "허용": ["GF000188"], + "근거": "건설 2-7-1 비계공 3 · 보통 1 ÷ 55㎡ = GC000022 강관비계(쌍줄) · 이름 같음 GF000188 은 산림 12-19(공㎥)", + "갈래": ["같은 이름 다른 책", "단위 다름"], +} +SHORING = { + "정답": ["GC000015"], + "허용": ["GF000189"], + "근거": "건설 2-6-1 형틀목공 3 · 보통 1 ÷ 50공㎥ = GC000015 강관 동바리(토목) · 이름 같음 GF000189 은 산림 12-20", + "갈래": ["같은 이름 다른 책"], +} +PAVE = { + "정답": ["GC000297"], + "허용": ["GF000162"], + "근거": "건설 1-6-2 포장공 4 · 보통 2 ÷ 100㎥(15% 이상 ×1.2) = GC000297 콘크리트 표층 인력포설 · 산림 12-6 GF000162(종단경사 입력) 같은 일", + "갈래": ["같은 뜻 다른 말"], +} +SOIL_LEFT_HAND = { + "정답": [], + "허용": [], + "부분": ["GC000083"], + "근거": "보통인부 0.2인/㎥ = 건설 3-2-1 비고 잔토처리 — 우리 단독 로직 없음(GC000083 입력 「잔토처리」로 굴착과 묶임)", + "갈래": ["우리 로직이 더 굵게"], +} +LABOR_ONLY = { + "정답": [], + "허용": [], + "근거": "노임 한 줄(인 단위) — 로직 아님(인력 단가)", + "갈래": ["노임 줄"], +} +BIG_STONE = { + "정답": ["GF000213"], + "허용": [], + "근거": "산림 13-2-3 작업반장 0.026인/개 × 4개/㎡ × 75% × 50 · 40% + 굴착기 = GF000213 큰돌 채집(기계)(개 ↔ ㎡)", + "갈래": ["단위 다름"], +} +BACKFILL_STONE = { + "정답": ["GC000106"], + "허용": ["GF000218"], + "근거": "채집(9:1) · 운반 + 뒷채움 및 다짐 건설 3-4-4(소형장비) = GC000106(부분)", + "짐작": True, + "갈래": ["엑셀 복합", "같은 뜻 다른 말"], +} +BASE_STONE = { + "정답": ["GC000110", "GF000168"], + "허용": [], + "근거": "채집 · 적사 · 운반 + 기초지정 건설 3-4-8 = GC000110(부분) · 산림 12-12(옛 번호) → 지금 12-25 기초잡석 GF000168", + "짐작": True, + "갈래": ["엑셀 복합", "품셈 판 번호 바뀜"], +} +DRAIN = { + "정답": ["GC000404"], + "허용": ["GF000176", "GF000179"], + "근거": "건설 6-7-2 파형강관 배관공(수도) · 보통 ÷ 6 = GC000404 파형강관 부설 및 접합(본 ↔ M) · 산림 12-11-3 GF000176", + "갈래": ["같은 뜻 다른 말", "단위 다름"], +} +DUMP = { + "정답": ["GF000135", "GF000284"], + "허용": [], + "근거": "적사(굴착기 0.8) + 덤프 15t 운반 18.7㎞(출처 없음) = 산림 10-3-2 골재운반 잡석 · 10-12-1 덤프운반", + "짐작": True, + "갈래": ["근거 조항 비어 있음"], +} + +L = { + "N000": CONCRETE, + "N001": PLY, + "N002": BASE_STONE, + "N003": REBAR, + "N004": FIELDSTONE, + "N005": WEDGE, + "N006": { + "정답": ["GF000280"], + "허용": ["GF000279"], + "근거": "건설 1-2-7 인력운반 공식(575㎏/㎡ ÷ 25㎏ ÷ 42회) — 우리 건설 1-2-7 없음 · 산림 10-6-2 인력운반 돌(지게) 같은 일(㎡ ↔ ㎥)", + "짐작": True, + "갈래": ["같은 뜻 다른 말", "우리에 그 책 조항 없음"], + }, + "N007": { + "정답": ["GF000225"], + "허용": ["GC000263"], + "근거": "산림 13-5-2 석공 0.11 · 보통 0.04 + 굴착기 집게 = GF000225 돌붙임 찰붙임(장비)", + "갈래": ["같은 뜻 다른 말"], + }, + "N008": { + "정답": ["GC000083"], + "허용": ["GF000101"], + "근거": "건설 3-2-1 특별인부 (3.6+2.7+2.2)/3 = GC000083 굴착(인력/토사)", + "갈래": ["같은 뜻 다른 말"], + }, + "N009": { + "정답": ["GF000113"], + "허용": [], + "근거": "산림 9-14 인력품 보통인부 0.1인 = GF000113 되메우기(9-14-1)", + "갈래": ["엑셀 조항이 윗마디"], + }, + "N010": SOIL_LEFT_HAND, + "N011": FIELDSTONE, + "N012": WEDGE, + "N013": CONCRETE, + "N014": EURO, + "N015": FORM_PATTERN, + "N016": { + "정답": ["GF000192"], + "허용": [], + "근거": "산림 12-22 재료비 + 설치비 5% = GF000192 P.V.C 파이프 설치(개소 ↔ M)", + "갈래": ["단위 다름"], + }, + "N017": { + "정답": ["GC000226"], + "허용": ["GF000201"], + "근거": "건설 6-3-10 다웰바 형틀목공 0.043 = GC000226 · 산림 12-30-1 GF000201(D32 · L1000) 같은 일", + "갈래": [], + }, + "N018": { + "정답": ["GC000227"], + "허용": [], + "근거": "건설 6-3-10 휠라(채움재) 형틀목공 0.029 = GC000227 신축이음 채움재 설치", + "갈래": ["같은 뜻 다른 말"], + }, + "N019": REBAR, + "N020": BIG_STONE, + "N021": { + "정답": ["GF000226"], + "허용": ["GC000260"], + "근거": "산림 13-6-1 = GF000226 큰돌쌓기 메쌓기", + "갈래": [], + }, + "N022": BACKFILL_STONE, + "N023": { + "정답": ["GF000227"], + "허용": [], + "근거": "산림 13-6-2 = GF000227 큰돌쌓기 찰쌓기", + "갈래": [], + }, + "N024": CONCRETE, + "N025": PLY, + "N026": { + "정답": ["GC000310"], + "허용": [], + "근거": "건설 1-8-3 = GC000310 도로반사경 설치(본 ↔ 개소)", + "갈래": ["단위 다름"], + }, + "N027": { + "정답": ["GF000224"], + "허용": ["GC000262"], + "근거": "산림 13-5-2 석공 0.13 · 보통 0.04 = GF000224 돌붙임 메붙임(장비)", + "갈래": ["같은 뜻 다른 말"], + }, + "N028": { + "정답": ["GF000229"], + "허용": ["GC000262"], + "근거": "산림 13-7-1 = GF000229 큰돌붙이기 메붙이기", + "갈래": ["같은 뜻 다른 말"], + }, + "N029": { + "정답": ["GF000230"], + "허용": ["GC000263"], + "근거": "산림 13-7-2 = GF000230 큰돌붙이기 찰붙이기", + "갈래": ["같은 뜻 다른 말"], + }, + "N030": CONCRETE, + "N031": { + "정답": ["GF000222"], + "허용": ["GC000261"], + "근거": "산림 13-4-5 석공 0.09 · 보통 0.05 = GF000222 돌쌓기 찰쌓기(장비)", + "갈래": ["같은 뜻 다른 말"], + }, + "N032": { + "정답": ["GC000111"], + "허용": ["GF000124"], + "근거": "건설 3-5-1 절토면 고르기 굴착기 1.0 = GC000111 · 씨앗 같음 GF000124 은 산림 9-19-1", + "갈래": ["같은 이름 다른 책"], + }, + "N033": { + "정답": ["GC000271"], + "허용": [], + "근거": "기계 hr 대형브레이카 + 굴착기 0.7 = GC000271", + "갈래": ["장비 이름"], + }, + "N034": {"정답": ["GC000665"], "허용": [], "근거": "건설 8-1-3 = GC000665", "갈래": []}, + "N035": { + "정답": ["GF000328"], + "허용": [], + "근거": "산림 13-11-1 조립 0.08 · 돌채움 0.17 = GF000328 원형 돌망태", + "갈래": [], + }, + "N036": PEBBLE, + "N037": { + "정답": ["GF000235"], + "허용": ["GC000342"], + "근거": "산림 13-114(= 13-11-4 오타) 사각돌망태 = GF000235 · 이름 같음 GC000342 은 건설 토목 2-2-3", + "갈래": ["같은 이름 다른 책", "근거 조항 엉뚱"], + }, + "N038": PAVE, + "N039": { + "정답": ["GC000143"], + "허용": ["GF000032"], + "근거": "건설 4-1-1 조경공 1 · 보통 4 ÷ 150 + 떼꽂이(산림 5-23-3) = GC000143 잔디붙임(주)", + "갈래": ["같은 뜻 다른 말", "엑셀 복합"], + }, + "N040": PAVE, + "N041": { + "정답": [], + "허용": [], + "부분": ["GF000124", "GF000126"], + "근거": "고화매트120 설치(건설품셈 p.266 · 우리 없음) + 토사면 고르기(산림 9-19-1) + 크레인 · 살수", + "갈래": ["우리에 없음(새 공종)"], + }, + "N042": { + "정답": ["GF000227"], + "허용": [], + "근거": "산림 13-6-2 = GF000227", + "갈래": [], + }, + "N043": CONCRETE, + "N044": SHORING, + "N045": REBAR, + "N046": SCAFFOLD, + "N047": { + "정답": ["GC000085"], + "허용": ["GF000089"], + "근거": "건설 3-2-3 굴착기 1.0 310㎥/일 = GC000085 흙깎기(기계) 보통토사", + "갈래": ["오타"], + }, + "N048": { + "정답": [], + "허용": [], + "부분": ["GC000267"], + "근거": "굴착기 0.6 사이클 공식만(출처 없음) — 우리 「잔토정리」 로직 없음", + "짐작": True, + "갈래": ["우리에 없음(새 공종)"], + }, + "N049": { + "정답": ["GC000676"], + "허용": [], + "근거": "건설(건축) 8-3-1 = GC000676 잡철물 제작 및 설치", + "갈래": [], + }, + "N050": { + "정답": ["GC000705", "GC000714", "GC000712"], + "허용": [], + "근거": "건설(건축) 11-1-3 바탕 + 11-2-6 녹막이 + 11-2-4 유성 — 우리 셋(복합)", + "갈래": ["엑셀 복합"], + }, + "N051": { + "정답": ["GC000308"], + "허용": [], + "근거": "건설 1-8-1 지주 · 표지판 = GC000308 교통 안전표지판 설치", + "갈래": ["같은 뜻 다른 말"], + }, + # ── 사방 ── + "S052": CONCRETE, + "S053": CONCRETE, + "S054": {**EURO, "갈래": ["괄호 속 영문"]}, + "S055": REBAR, + "S056": SCAFFOLD, + "S057": SHORING, + "S058": EURO, + "S059": REBAR, + "S060": { + "정답": [], + "허용": [], + "부분": ["GF000283"], + "근거": "구역화물자동차 운임(왕복 150㎞ + 60㎞) — 우리 화물 운임 로직 없음(덤프 운반 GF000283 은 다른 셈)", + "짐작": True, + "갈래": ["우리에 없음(새 공종)"], + }, + "S061": { + "정답": ["GC000563"], + "허용": ["GC000561"], + "근거": "크레인 10Ton 1hr/Ton 철골세우기 기계만(출처 없음) = GC000563 철골세우기 장비 손료(ton당)", + "짐작": True, + "갈래": ["근거 조항 비어 있음", "같은 뜻 다른 말"], + }, + "S062": { + "정답": ["GC000559"], + "허용": ["GC000560"], + "근거": "건설(건축) 1-1 철골공 2.48 × 0.71(89%) + 1-1-3 용접 — GC000559 주 · GC000560 곁", + "갈래": ["엑셀 복합", "엑셀 조항이 윗마디"], + }, + "S063": { + "정답": ["GC000676"], + "허용": [], + "근거": "건설 8-3-1 = GC000676", + "갈래": ["같은 뜻 다른 말"], + }, + "S064": PLY, + "S065": {"정답": ["GC000665"], "허용": [], "근거": "건설 8-1-3 = GC000665", "갈래": []}, + "S066": { + "정답": ["GC000089"], + "허용": ["GC000088"], + "근거": "인력 10% + 기계 90% 혼합토사 건설 3-2-4 = GC000089 터파기(기계) 혼합토사", + "갈래": ["괄호 속 규격", "엑셀 복합"], + }, + "S067": { + "정답": ["GC000109"], + "허용": [], + "근거": "건설 3-4-7 대형장비 290㎥/일 = GC000109", + "갈래": [], + }, + "S068": { + "정답": [], + "허용": [], + "근거": "굴착기 0.8 사이클 기계 100%(출처 없음) — 우리 잔토처리 로직 없음", + "짐작": True, + "갈래": ["우리에 없음(새 공종)"], + }, + "S069": CONCRETE, + "S070": PLY, + "S071": BASE_STONE, + "S072": DRAIN, + "S073": LABOR_ONLY, + "S074": LABOR_ONLY, + "S075": CONCRETE, + "S076": { + "정답": ["GC000083"], + "허용": ["GF000101"], + "근거": "건설 3-2-1 특별인부 = GC000083", + "갈래": ["같은 뜻 다른 말"], + }, + "S077": { + "정답": ["GF000113"], + "허용": [], + "근거": "보통인부 0.1인/㎥(출처 없음) = 산림 9-14-1 GF000113(임도 파일 단산12 와 같은 값)", + "짐작": True, + "갈래": ["근거 조항 비어 있음"], + }, + "S078": SOIL_LEFT_HAND, + "S079": { + "정답": ["GF000328"], + "허용": [], + "근거": "산림 13-11-1 조립설치 = GF000328(시공 입력 · 조 ↔ ㎡)", + "갈래": ["단위 다름", "우리 로직이 더 굵게"], + }, + "S080": { + "정답": ["GF000328"], + "허용": [], + "근거": "산림 13-11-1 돌채움 = GF000328", + "갈래": ["단위 다름", "우리 로직이 더 굵게"], + }, + "S081": PEBBLE, + "S082": {"정답": ["GC000090"], "허용": [], "근거": "건설 3-2-4 암 = GC000090", "갈래": []}, + "S083": DRAIN, + "S084": { + "정답": ["GC000201"], + "허용": ["GF000182"], + "근거": "건설 6-1-4 펌프차 편성 = GC000201", + "갈래": [], + }, + "S085": { + "정답": ["GC000268", "GC000269", "GC000275"], + "허용": [], + "근거": "기계 hr 콘크리트펌프차 41m — 우리 「기계 시간당 기계경비」(기계 입력)", + "짐작": True, + "갈래": ["장비 이름"], + }, + "S086": FORM_PATTERN, + "S087": { + "정답": ["GF000196"], + "허용": [], + "근거": "산림 12-27-1 = GF000196 지수판 설치", + "갈래": [], + }, + "S088": { + "정답": ["GF000192"], + "허용": [], + "근거": "산림 12-22 = GF000192(개소 ↔ M)", + "갈래": ["단위 다름"], + }, + "S089": { + "정답": ["GF000025"], + "허용": ["GF000032"], + "근거": "산림 5-12 보통인부 0.06 + 떼 소운반 = GF000025 떼붙임(재배잔디)", + "갈래": [], + }, + "S090": {"정답": ["GF000226"], "허용": ["GC000260"], "근거": "산림 13-6-1", "갈래": []}, + "S091": WEDGE, + "S092": BACKFILL_STONE, + "S093": {"정답": ["GF000227"], "허용": ["GC000261"], "근거": "산림 13-6-2", "갈래": []}, + "S094": BIG_STONE, + "S095": DUMP, + "S096": { + "정답": ["GC000106"], + "허용": [], + "근거": "건설 3-4-4 소형장비 = GC000106", + "갈래": ["같은 뜻 다른 말"], + }, + "S097": DUMP, + "S098": { + "정답": ["GF000229"], + "허용": ["GC000262"], + "근거": "산림 13-7-1", + "갈래": ["같은 뜻 다른 말"], + }, + "S099": { + "정답": ["GF000230"], + "허용": ["GC000263"], + "근거": "산림 13-7-2", + "갈래": ["같은 뜻 다른 말"], + }, +} diff --git a/docs/raw/조사/2026-10-15_338_실험자료/C1/labels_y.py b/docs/raw/조사/2026-10-15_338_실험자료/C1/labels_y.py new file mode 100644 index 000000000..cfb7ba814 --- /dev/null +++ b/docs/raw/조사/2026-10-15_338_실험자료/C1/labels_y.py @@ -0,0 +1,43 @@ +"""셋째 파일(동부청 영월 2024 · STmate 꼴) 정답 — 씨앗(일위대가조합.json 메모 = 사용자 실무 이름 → 로직)이 +있는 줄은 사용자 선택을 정답으로 봄(「씨앗」 표시).""" + +CON = {"정답": ["GC000198"], "허용": ["GF000155"], "근거": "6-1-1 콘크리트공 3 · 보통 ÷ 일 시공량", "갈래": ["같은 뜻 다른 말"]} +EURO = {"정답": ["GC000214"], "허용": ["GF000210"], "근거": "건설 6-3-3 유로폼 — 이름 같은 산림 GF000210 과 접힘(이름 후보 중복 빼기)", "갈래": ["같은 이름 다른 책"]} +LY = { + "Y000": {**CON, "근거": "건설 6-1-1 인력운반 23㎥/일 · 씨앗 같음"}, + "Y001": {"정답": ["GC000211"], "허용": ["GF000160"], "근거": "건설 6-3-1 · 씨앗 같음", "갈래": []}, + "Y002": {"정답": ["GF000101"], "허용": ["GC000083"], "근거": "옛 건설 3-1-3 인력 터파기 보통인부 0.26 · 씨앗 = GF000101", "짐작": True, "갈래": ["품셈 판 번호 바뀜"]}, + "Y003": {"정답": ["GF000113"], "허용": [], "근거": "보통인부 0.1 · 씨앗 같음", "갈래": ["품셈 판 번호 바뀜"]}, + "Y004": {"정답": [], "허용": [], "근거": "잔토처리 인력 0.2인 — 단독 로직 없음", "갈래": ["우리 로직이 더 굵게"]}, + "Y005": {"정답": ["GC000268", "GC000269", "GC000275"], "허용": [], "근거": "기계 시간 크레인(타이어) 10톤 — 기계 시간당 기계경비(기계 입력)", "짐작": True, "갈래": ["장비 이름"]}, + "Y006": {"정답": ["GF000101"], "허용": ["GC000089"], "근거": "임도품셈 5-1-2(옛) 기계 90 + 인력 10 · 씨앗 = GF000101", "짐작": True, "갈래": ["품셈 판 번호 바뀜"]}, + "Y007": {"정답": [], "허용": [], "근거": "잔토처리 기계 100%(백호 0.7) — 로직 없음", "갈래": ["우리에 없음(새 공종)"]}, + "Y008": {"정답": ["GC000198"], "허용": ["GF000155", "GF000166"], "근거": "임도품셈 5-5(옛) 콘크리트공 0.15 · 보통 0.27 · 씨앗 = GC000198", "짐작": True, "갈래": ["품셈 판 번호 바뀜"]}, + "Y009": {**EURO, "근거": "건설 6-3-3 유로폼(복잡) — GC000214 · 이름 후보에서 산림 GF000210 과 한 줄로 접혀 근거 점수만 받음"}, + "Y010": {"정답": ["GC000206", "GC000207"], "허용": ["GF000159"], "근거": "건설 6-2-3 · 6-2-2 가공 + 조립", "갈래": ["우리 로직이 더 잘게", "비슷한 이름 엉뚱"]}, + "Y011": {"정답": ["GF000168"], "허용": ["GC000110"], "근거": "채집(9:1) · 운반 · 부설 — 산림 12-25 기초잡석(짐작)", "짐작": True, "갈래": ["엑셀 복합", "품셈 판 번호 바뀜"]}, + "Y012": {"정답": ["GC000308"], "허용": [], "근거": "옛 1-9-3 표지판 · 씨앗 같음", "갈래": []}, + "Y013": {"정답": ["GF000214"], "허용": [], "근거": "기계 8 : 인력 2 채집 + 운반 — 인력 채집 몫(부분)", "짐작": True, "갈래": ["엑셀 복합"]}, + "Y014": {"정답": ["GC000263"], "허용": ["GF000225"], "근거": "건설 7-2-2 석공 0.05 + 굴착기 집게 = GC000263", "갈래": ["같은 뜻 다른 말"]}, + "Y015": {"정답": ["GF000211"], "허용": ["GF000212"], "근거": "채집(9:1) + 덤프 — 인력 채집 몫(짐작)", "짐작": True, "갈래": ["같은 뜻 다른 말", "엑셀 복합"]}, + "Y016": {"정답": ["GF000218"], "허용": [], "근거": "씨앗 같음(잡석운반및다짐)", "갈래": []}, + "Y017": {"정답": ["GF000124"], "허용": ["GC000111"], "근거": "보통인부 0.009 + 굴착기 0.7 · 씨앗 같음", "갈래": []}, + "Y018": {"정답": ["GF000101"], "허용": ["GC000083"], "근거": "씨앗 같음", "짐작": True, "갈래": []}, + "Y019": {"정답": ["GC000310"], "허용": [], "근거": "씨앗 같음", "갈래": []}, + "Y020": {**CON, "근거": "건설 6-1-1 장비사용 55㎥/일 + 굴삭기 — GC000198(1위 펌프차 · 토목 6-1-1 소켓관이 부문 없이 조항 같음)", "갈래": ["같은 뜻 다른 말", "부문 없는 조항"]}, + "Y021": EURO, + "Y022": {"정답": ["GC000224"], "허용": [], "근거": "씨앗 같음", "갈래": []}, + "Y023": {"정답": ["GC000393"], "허용": [], "근거": "토목 6-4-2 · 씨앗 같음", "갈래": []}, + "Y024": {"정답": ["GF000280"], "허용": ["GF000279"], "근거": "옛 건설 9-1 인력운반(지게) 575㎏/㎡ — 산림 10-6-2(짐작)", "짐작": True, "갈래": ["같은 뜻 다른 말", "품셈 판 번호 바뀜"]}, + "Y025": {"정답": ["GF000279", "GF000281"], "허용": [], "근거": "옛 건설 9-1 인력운반 2,300㎏/㎥ — 산림 10-6 인력운반(짐작)", "짐작": True, "갈래": ["같은 뜻 다른 말", "품셈 판 번호 바뀜"]}, + "Y026": {"정답": ["GF000224", "GC000262"], "허용": [], "근거": "건설 7-2-1 석공 0.13 = GC000262 · 씨앗 = GF000224", "갈래": []}, + "Y027": {"정답": ["GC000251"], "허용": [], "근거": "공통 6-7-1 플륨관 특별 · 보통 + 트럭크레인 = GC000251 플륨관 설치(1위 PC관 = 토목 6-7-1)", "갈래": ["같은 뜻 다른 말", "부문 없는 조항"]}, + "Y028": {**CON, "근거": "6-1-1 장비사용(소형) — GC000198 · 1위 GF000166 은 산림 12-37 다른 품", "갈래": ["비슷한 이름 엉뚱"]}, + "Y029": {"정답": ["GC000211"], "허용": ["GF000160"], "근거": "건설 6-3-1 4회 보통", "갈래": ["같은 이름 다른 책"]}, + "Y030": {"정답": ["GF000136"], "허용": [], "근거": "골재장 → 현장 덤프 운반(옛 9-7) = 산림 10-3-3(짐작)", "짐작": True, "갈래": []}, + "Y031": {"정답": ["GF000162"], "허용": ["GC000297"], "근거": "임도품셈 7-1-1(옛) · 씨앗 같음", "갈래": []}, + "Y032": {"정답": ["GF000163"], "허용": [], "근거": "임도품셈 7-1-2(옛) · 씨앗 같음", "갈래": []}, + "Y033": {"정답": ["GF000218"], "허용": [], "근거": "씨앗 같음", "갈래": []}, + "Y034": {"정답": ["GF000226"], "허용": [], "근거": "사방품셈 P52 · 씨앗 같음", "갈래": []}, + "Y035": {"정답": ["GF000213"], "허용": [], "근거": "큰돌 채집 장비 80 + 인력 20", "갈래": ["단위 다름"]}, +} diff --git a/docs/raw/조사/2026-10-15_338_실험자료/C1/out_items.json b/docs/raw/조사/2026-10-15_338_실험자료/C1/out_items.json new file mode 100644 index 000000000..da6b3d672 --- /dev/null +++ b/docs/raw/조사/2026-10-15_338_실험자료/C1/out_items.json @@ -0,0 +1,13532 @@ +[ + { + "파일": "북부청임도", + "호표": 1, + "호표이름": "배수관날개벽(유입구)", + "호표규격": "D=1,000mm,일반형", + "호표단위": "개소", + "차례": 1, + "이름": "레미콘타설(장비사용)", + "규격": "철근구조물-소량 타설", + "수량": 2.5, + "단위": "㎥", + "단가": { + "계": 67538, + "노무비": 53213, + "재료비": 6461, + "경비": 7864 + }, + "갈래": "단가산출", + "참조": { + "종류": "단산", + "번": 46 + }, + "근거": { + "출처": [ + [ + "건설품셈", + "6-1-1" + ] + ], + "직종": [ + "콘크리트공", + "보통인부" + ], + "행": [ + 2197, + 2229 + ] + }, + "맞춤": { + "열쇠": "레미콘타설(장비사용)|철근구조물-소량타설|m3", + "대상": null, + "입력값": {}, + "못읽음": [], + "상태": "후보", + "환산": null, + "들어감": false, + "후보": [ + { + "로직": "GC000198", + "이름": "레디믹스트콘크리트 타설", + "단위": "㎥", + "근거": { + "조항": [ + "건설품셈 공통 6-1-1" + ], + "직종": [ + "콘크리트공" + ], + "단위": true, + "점수": 1.75, + "까닭": "조항 같음 · 건설 공통 · 직종 콘크리트공 · 단위 맞음" + }, + "점수": 1.75 + }, + { + "로직": "GC000201", + "이름": "콘크리트 펌프차 타설", + "단위": "㎥", + "근거": { + "조항": [ + "건설품셈 공통 6-1-4" + ], + "직종": [ + "콘크리트공" + ], + "단위": true, + "점수": 1.25, + "까닭": "앞 마디 같음 · 건설 공통 · 직종 콘크리트공 · 단위 맞음" + }, + "점수": 1.567 + }, + { + "로직": "GC000199", + "이름": "현장비빔타설", + "단위": "㎥", + "근거": { + "조항": [ + "건설품셈 공통 6-1-2" + ], + "직종": [ + "콘크리트공" + ], + "단위": true, + "점수": 1.25, + "까닭": "앞 마디 같음 · 건설 공통 · 직종 콘크리트공 · 단위 맞음" + }, + "점수": 1.25 + }, + { + "로직": "GF000182", + "이름": "펌프카 타설(편성 · 일일 시공량)", + "단위": "㎥", + "점수": 0.45 + }, + { + "로직": "GF000166", + "이름": "콘크리트 타설(무근진동기 제외)", + "단위": "㎥", + "점수": 0.422 + } + ] + }, + "단산머리": { + "이름": "레미콘타설(장비사용) (철근구조물-소량 타설)", + "단위": "㎥" + }, + "단산글": [ + "46.레미콘타설(장비사용) (철근구조물-소량 타설) [㎥]", + " [건설표준품셈 6-1-1]", + " 1. 레미콘 : 별도계상", + " 2. 타설비", + " 콘크리트공 : 273,540 × 3인 ÷ 55㎥ × 2 = 29,840.7", + " 보통인부 : 172,068 × 1인 ÷ 55㎥ × 2 = 6,257", + " 인부소계 : 36,097.7", + " 공구손료 및 경장비(콘크리트 진동기등)의 기계경비(노무비의 2%)", + " 36,097.7 × 2% = 721.9", + " 3. 굴착기(타이어형)(0.6㎥)", + " Q = 1일 × 8hr ÷ 55㎥ × 2 = 0.29 hr/㎥", + " 노무비 : 59,020 × 0.29 = 17,115.8", + " 재료비 : 19,792 × 0.29 = 5,739.6", + " 경 비 : 27,120 × 0.29 = 7,864.8", + " 소 계 : 30,720.2", + " 4. 합 계", + " 노무비 : 36,097.7 + 17,115.8 = 53,213", + " 재료비 : 721.9 + 5,739.6 = 6,461", + " 경 비 : 7,864", + " 계 : 67,538" + ], + "줄수": 13, + "호표들": [ + 1, + 4, + 5, + 6, + 11, + 16, + 17, + 24, + 25, + 55, + 62, + 63, + 68 + ], + "id": "N000" + }, + { + "파일": "북부청임도", + "호표": 1, + "호표이름": "배수관날개벽(유입구)", + "호표규격": "D=1,000mm,일반형", + "호표단위": "개소", + "차례": 2, + "이름": "합판거푸집", + "규격": "4회", + "수량": 14.32, + "단위": "㎡", + "단가": { + "계": 53221, + "노무비": 38291, + "재료비": 14930, + "경비": null + }, + "갈래": "단가산출", + "참조": { + "종류": "단산", + "번": 24 + }, + "근거": { + "출처": [ + [ + "건설품셈", + "6-3-1" + ] + ], + "직종": [ + "형틀목공", + "보통인부" + ], + "행": [ + 1173, + 1210 + ] + }, + "맞춤": { + "열쇠": "합판거푸집|4회|m2", + "대상": { + "로직": "GF000160", + "이름": "합판거푸집", + "단위": "㎡" + }, + "입력값": { + "횟수": "4회사용시" + }, + "못읽음": [], + "상태": "같음", + "환산": { + "배": 1, + "갈래": "같음" + }, + "들어감": true + }, + "단산머리": { + "이름": "합판거푸집(4회)", + "단위": "㎡" + }, + "단산글": [ + "24.합판거푸집(4회)[㎡]", + " 건설공사표준품셈, 6-3-1 합판거푸집", + " ※ 사용횟수 4회 : 보통", + " 1. 재료비", + " 내수합판1급(12mm1220×2440) 10,984 × 1.03㎡ × 38% = 4,299.1", + " 각재(거푸집용,외송) (3.6×3.0×3.0) 626,543 × 0.038㎥ × 38% = 9,047.2", + " 주자재소계 : 13,346.3", + " 소모자재(박리재등)(주자재비의 9%)", + " 13,346.3 × 9% = 1,201.1", + " 2. 인건비", + " Q = 45㎡/일", + " 형틀목공 : 275,790 × 5인 ÷ 45 = 30,643.3", + " 보통인부 : 172,068 × 2인 ÷ 45 = 7,648", + " 인부소계 : 38,291.3", + " 공구손료 및 경장비 기계경비(노무비의 1%)", + " 38,291.3 × 1% = 382.9", + " 3. 합 계", + " 노무비 : 38,291", + " 재료비 : 13,346.3 + 1,201.1 + 382.9 = 14,930", + " 경 비 :", + " 계 : 53,221" + ], + "줄수": 11, + "호표들": [ + 1, + 4, + 5, + 6, + 11, + 16, + 17, + 24, + 62, + 63, + 68 + ], + "id": "N001" + }, + { + "파일": "북부청임도", + "호표": 1, + "호표이름": "배수관날개벽(유입구)", + "호표규격": "D=1,000mm,일반형", + "호표단위": "개소", + "차례": 3, + "이름": "기초잡석채집,운반,다짐", + "규격": "발파암활용,덤프15톤,L=500m", + "수량": 0.85, + "단위": "㎥", + "단가": { + "계": 26956, + "노무비": 17877, + "재료비": 4509, + "경비": 4570 + }, + "갈래": "단가산출", + "참조": { + "종류": "단산", + "번": 33 + }, + "근거": { + "출처": [ + [ + "산림품셈", + "12-12" + ], + [ + "건설품셈", + "3-4-8" + ] + ], + "직종": [ + "특별인부", + "보통인부" + ], + "행": [ + 1649, + 1758 + ] + }, + "맞춤": { + "열쇠": "기초잡석채집,운반,다짐|발파암활용,덤프15톤,l=500m|m3", + "대상": null, + "입력값": {}, + "못읽음": [], + "상태": "후보", + "환산": null, + "들어감": false, + "후보": [ + { + "로직": "GC000110", + "이름": "기초지정", + "단위": "㎥", + "근거": { + "조항": [ + "건설품셈 공통 3-4-8" + ], + "직종": [ + "특별인부" + ], + "단위": true, + "점수": 1.75, + "까닭": "조항 같음 · 건설 공통 · 직종 특별인부 · 단위 맞음" + }, + "점수": 1.75 + }, + { + "로직": "GC000106", + "이름": "뒤채움 및 다짐(소형장비)", + "단위": "㎥", + "근거": { + "조항": [ + "건설품셈 공통 3-4-4" + ], + "직종": [ + "특별인부" + ], + "단위": true, + "점수": 1.25, + "까닭": "앞 마디 같음 · 건설 공통 · 직종 특별인부 · 단위 맞음" + }, + "점수": 1.525 + }, + { + "로직": "GC000107", + "이름": "뒤채움 및 다짐(대형장비)", + "단위": "㎥", + "근거": { + "조항": [ + "건설품셈 공통 3-4-5" + ], + "직종": [ + "특별인부" + ], + "단위": true, + "점수": 1.25, + "까닭": "앞 마디 같음 · 건설 공통 · 직종 특별인부 · 단위 맞음" + }, + "점수": 1.525 + }, + { + "로직": "GC000108", + "이름": "되메우기 및 다짐(소형장비)", + "단위": "㎥", + "근거": { + "조항": [ + "건설품셈 공통 3-4-6" + ], + "직종": [ + "특별인부" + ], + "단위": true, + "점수": 1.25, + "까닭": "앞 마디 같음 · 건설 공통 · 직종 특별인부 · 단위 맞음" + }, + "점수": 1.518 + }, + { + "로직": "GF000168", + "이름": "기초잡석(소할 · 적사 · 부설 및 다짐)", + "단위": "㎥", + "근거": { + "조항": [ + "산림품셈 12-25" + ], + "직종": [], + "단위": true, + "점수": 0.65, + "까닭": "앞 마디 같음 · 단위 맞음" + }, + "점수": 1.279 + } + ] + }, + "단산머리": { + "이름": "기초잡석채집,운반,다짐(발파암활용,덤프15톤,L=500m)", + "단위": "㎥" + }, + "단산글": [ + "33.기초잡석채집,운반,다짐(발파암활용,덤프15톤,L=500m)[㎥]", + " 기초잡석 채집, 운반, 다짐 [산림사업표준품셈 12-12준용]", + " 작업조건 : 발파후 파쇄암중에 뒷채움돌 채집(㎥)", + " 일정량 채취후 상차 및 운반(L=500m)", + " 1. 기초잡석 발파암 채집", + " 굴착기(무한궤도)(0.7㎥)", + " q = 0.7㎥ K = 0.55 f = 1", + " E = 0.35 Cm = 18sec(90˚)", + " 3,600 × q × K × f × E 3,600 × 0.7 × 0.55 × 1 × 0.35", + " Q = ────────────── = ───────────────── = 26.95 ㎥/hr", + " Cm 18", + " 노무비 : 59,020 ÷ 26.95 × 0.9 = 1,970.9", + " 재료비 : 19,473 ÷ 26.95 × 0.9 = 650.3", + " 경 비 : 24,554 ÷ 26.95 × 0.9 = 819.9", + " 소 계 : 3,441.1", + " 2. 적 사", + " 굴착기(무한궤도)(0.7㎥)", + " q = 0.7㎥ K = 0.55 f = 1", + " E = 0.35 Cm = 18sec(90˚)", + " 3,600 × q × K × f × E 3,600 × 0.7 × 0.55 × 1 × 0.35", + " Q = ────────────── = ───────────────── = 26.95 ㎥/hr", + " Cm 18", + " 노무비 : 59,020 ÷ 26.95 = 2,189.9", + " 재료비 : 19,473 ÷ 26.95 = 722.5", + " 경 비 : 24,554 ÷ 26.95 = 911", + " 소 계 : 3,823.4", + " 3. 운 반 : 덤프트럭(15Ton)", + " L = 0.5km V1 = 5km/hr V2 = 6km/hr", + " T = 15.0ton rt = 1.75ton/㎥ E = 0.9", + " q = T / rt = 15 / 1.75 = 8.57 ㎥", + " n = q / (0.7 × K) = 8.57 / (0.7 × 0.55) = 22.26 회", + " t1 = 18 × n / (60×0.35) = 18 × 22.26 / (60×0.35) = 19.08", + " L L 0.5 0.5", + " t2 = (── + ──) × 60 = (── + ──) × 60 = 11 분(왕복)", + " V1 V2 5 6", + " t3 = 1.1분(적하) t4 = 0.9분(대기)", + " Cm = t1 + t2 + t3 + t4 = 19.08 + 11 + 1.1 + 0.9 = 32.08 분", + " 60 × q × f × E 60 × 8.57 × 1 × 0.9", + " Q = ─────────── = ──────────── = 14.43 ㎥/hr", + " Cm 32.08", + " 노무비 : 59,020 ÷ 14.43 = 4,090", + " 재료비 : 30,192 ÷ 14.43 = 2,092.3", + " 경 비 : 20,659 ÷ 14.43 = 1,431.6", + " 소 계 : 7,613.9", + " 4. 기초지정[2025년 표준품셈3-4-8]", + " 가. 굴착기(무한궤도)(0.2㎥)", + " Q = 90÷8 = 11.25㎥/hr", + " 노무비 : 59,020 ÷ 11.25 = 5,246.2", + " 재료비 : 8,324 ÷ 11.25 = 739.9", + " 경 비 : 13,873 ÷ 11.25 = 1,233.1", + " 소 계 : 7,219.2", + " 나. 진동로울러(핸드가이드식)(0.7Ton)", + " 노무비 : 36,248 ÷ 0.096 = 3,418.3", + " 재료비 : 3,420 ÷ 11.25 = 304", + " 경 비 : 1,967 ÷ 11.25 = 174.8", + " 소 계 : 3,897.1", + " 다. 인 부", + " 특별인부 : 226,122 × 0.011인 = 2,487.3", + " 보통인부 : 172,068 × 0.011인 = 1,892.7", + " 인부소계 : 4,380", + " 마. 소 계", + " 노무비 : 5,246.2 + 4,380 = 9,626.2", + " 재료비 : 739.9 + 304 = 1,043.9", + " 경 비 : 1,233.1 + 174.8 = 1,407.9", + " 계 : 12,078", + " 5. 합 계", + " 노무비 : 1,970.9 + 2,189.9 + 4,090 + 9,626.2 = 17,877", + " 재료비 : 650.3 + 722.5 + 2,092.3 + 1,043.9 = 4,509", + " 경 비 : 819.9 + 911 + 1,431.6 + 1,407.9 = 4,570", + " 계 : 26,956" + ], + "줄수": 18, + "호표들": [ + 1, + 4, + 5, + 6, + 11, + 16, + 17, + 27, + 38, + 39, + 40, + 41, + 42, + 43, + 44, + 64, + 67, + 68 + ], + "id": "N002" + }, + { + "파일": "북부청임도", + "호표": 1, + "호표이름": "배수관날개벽(유입구)", + "호표규격": "D=1,000mm,일반형", + "호표단위": "개소", + "차례": 5, + "이름": "철근가공조립(현장)", + "규격": "Type-Ⅰ(소량 시공)", + "수량": 0.059, + "단위": "Ton", + "단가": { + "계": 1421293, + "노무비": 1367280, + "재료비": 54013, + "경비": null + }, + "갈래": "단가산출", + "참조": { + "종류": "단산", + "번": 47 + }, + "근거": { + "출처": [ + [ + "건설품셈", + "6-2" + ] + ], + "직종": [ + "철근공", + "보통인부" + ], + "행": [ + 2232, + 2291 + ] + }, + "맞춤": { + "열쇠": "철근가공조립(현장)|type-i(소량시공)|톤", + "대상": null, + "입력값": {}, + "못읽음": [], + "상태": "후보", + "환산": null, + "들어감": false, + "후보": [ + { + "로직": "GF000187", + "이름": "철근가공조립(복잡)", + "단위": "ton", + "점수": 2.2 + }, + { + "로직": "GF000207", + "이름": "암거이음받침 철근가공조립(보통)", + "단위": "ton", + "점수": 0.825 + }, + { + "로직": "GF000159", + "이름": "철근 현장가공 및 조립", + "단위": "ton", + "점수": 0.662 + }, + { + "로직": "GC000206", + "이름": "철근 현장가공", + "단위": "ton", + "점수": 0.55 + }, + { + "로직": "GC000207", + "이름": "철근 현장조립", + "단위": "ton", + "점수": 0.55 + } + ] + }, + "단산머리": { + "이름": "철근가공조립(현장)(Type-Ⅰ(소량 시공))", + "단위": "Ton" + }, + "단산글": [ + "47.철근가공조립(현장)(Type-Ⅰ(소량 시공))[Ton]", + " 철근 현장가공 및 조립(Type-Ⅰ) [건설표준품셈6-2]", + " 1. 현장가공", + " 가. 재료비", + " 철근 : 별도계산", + " 나. 인건비", + " 철 근 공 : 268,187 × 0.67인 = 179,685.2", + " 보통인부 : 172,068 × 0.22인 = 37,854.9", + " 인부소계 : 217,540.1", + " 기계경비(인력품의 9%)", + " 217,540.1 × 9% = 19,578.6", + " 다. 소 계", + " 노무비 : 217,540.1", + " 재료비 : 19,578.6", + " 경 비 :", + " 계 : 237,118.7", + " 2. 조 립", + " 가. 재료비", + " 어닐링철선(결속선)(#200.9mm200.4m/kg) 1,760 × 6.5kg = 11,440", + " 나. 인건비", + " 철근공 : 268,187 × 3.53인 = 946,700.1", + " 보통인부 : 172,068 × 1.18인 = 203,040.2", + " 인부소계 : 1,149,740.3", + " 기구손료 (가공노력품의 2%)", + " 1,149,740.3 × 0.02 = 22,994.8", + " 다. 소 계", + " 노무비 : 1,149,740.3", + " 재료비 : 11,440 + 22,994.8 = 34,434.8", + " 경 비 :", + " 계 : 1,184,175.1", + " 3. 합 계", + " 노무비 : 217,540.1 + 1,149,740.3 = 1,367,280", + " 재료비 : 19,578.6 + 34,434.8 = 54,013", + " 경 비 :", + " 계 : 1,421,293" + ], + "줄수": 12, + "호표들": [ + 1, + 4, + 5, + 6, + 11, + 16, + 17, + 24, + 55, + 62, + 65, + 68 + ], + "id": "N003" + }, + { + "파일": "북부청임도", + "호표": 2, + "호표이름": "돌수로(넓은호형)", + "호표규격": "B=2.3,H=0.4,야면석,찰붙임", + "호표단위": "M", + "차례": 1, + "이름": "야면석채집 및 운반(계곡내)", + "규격": "굴삭기0.6㎥,35cm,4.5톤,L=500m", + "수량": 3, + "단위": "㎡", + "단가": { + "계": 8160, + "노무비": 6255, + "재료비": 879, + "경비": 1026 + }, + "갈래": "단가산출", + "참조": { + "종류": "단산", + "번": 6 + }, + "근거": { + "출처": [ + [ + "건설품셈", + "8-2" + ] + ], + "직종": [ + "보통인부" + ], + "행": [ + 360, + 445 + ] + }, + "맞춤": { + "열쇠": "야면석채집및운반(계곡내)|굴삭기0.6m3,35cm,4.5톤,l=500m|m2", + "대상": null, + "입력값": {}, + "못읽음": [], + "상태": "후보", + "환산": null, + "들어감": false, + "후보": [ + { + "로직": "GF000214", + "이름": "야면석 채집(인력)", + "단위": "㎡ · ㎥", + "점수": 0.927 + }, + { + "로직": "GC000363", + "이름": "자갈채집 및 운반", + "단위": "㎥", + "점수": 0.615 + }, + { + "로직": "GC000267", + "이름": "굴착기(무한궤도) 시간당 기계경비", + "단위": "시간", + "근거": { + "조항": [ + "건설품셈 공통 8-4" + ], + "직종": [], + "단위": false, + "점수": 0.6, + "까닭": "앞 마디 같음 · 건설 공통" + }, + "점수": 0.6 + }, + { + "로직": "GC000270", + "이름": "굴착기(무한궤도)+부착용 집게 시간당 기계경비", + "단위": "시간", + "근거": { + "조항": [ + "건설품셈 공통 8-4" + ], + "직종": [], + "단위": false, + "점수": 0.6, + "까닭": "앞 마디 같음 · 건설 공통" + }, + "점수": 0.6 + }, + { + "로직": "GC000271", + "이름": "굴착기(무한궤도)+대형브레이커 시간당 기계경비", + "단위": "시간", + "근거": { + "조항": [ + "건설품셈 공통 8-4" + ], + "직종": [], + "단위": false, + "점수": 0.6, + "까닭": "앞 마디 같음 · 건설 공통" + }, + "점수": 0.6 + } + ] + }, + "단산머리": { + "이름": "야면석채집및운반(계곡내)(굴삭기0.6㎥,35cm,4.5톤,L=500m)", + "단위": "㎡" + }, + "단산글": [ + "6.야면석채집및운반(계곡내)(굴삭기0.6㎥,35cm,4.5톤,L=500m)[㎡]", + " 야면석 채집 및 운반", + " 작업조건 계곡에 산재하여 매몰되어 있는 곳에서 굴삭기 0.6㎥이용 채집", + " 1. 야면석 채집", + " 가. 인력(10%)", + " 보통인부 : 172,068 × 0.17인 × 10% = 2,925.1", + " [건설표준품셈(8-2, 2016년 삭제)] L3=35cm, 인/㎡의 10%로 적용", + " 나. 기계채집 : 굴착기(타이어형)(0.6㎥)", + " q = 0.6㎥ K = 0.55 f = 1", + " E = 0.35 Cm = 18sec(90˚)", + " 3,600 × q × K × f × E 3,600 × 0.6 × 0.55 × 1 × 0.35", + " Q = ────────────── = ───────────────── = 23.1 ㎥/hr", + " Cm 18", + " 노무비 : 59,020 ÷ 23.1 × 575 / 1750 × 90% = 755.5", + " 재료비 : 19,792 ÷ 23.1 × 575 / 1750 × 90% = 253.3", + " 경 비 : 27,120 ÷ 23.1 × 575 / 1750 × 90% = 347.1", + " 소 계 : 1,355.9", + " 다. 상 차 : 굴착기(타이어형)(0.6㎥)", + " qs = 0.6㎥ K = 0.55 f = 1", + " Es = 0.35 Cm = 20sec(135˚)", + " 3,600 × qs × K × f × Es 3,600 × 0.6 × 0.55 × 1 × 0.35", + " Q = ────────────── = ───────────────── = 20.79 ㎥/hr", + " Cm 20", + " 노무비 : 59,020 ÷ 20.79 × 575 / 1750 = 932.7", + " 재료비 : 19,792 ÷ 20.79 × 575 / 1750 = 312.7", + " 경 비 : 27,120 ÷ 20.79 × 575 / 1750 = 428.6", + " 소 계 : 1,674", + " 라. 소 계", + " 노무비 : 2,925.1 + 755.5 + 932.7 = 4,613.3", + " 재료비 : 253.3 + 312.7 = 566", + " 경 비 : 347.1 + 428.6 = 775.7", + " 계 : 5,955", + " 3. 운 반 : 덤프트럭(4.5Ton)", + " L = 0.5km V1 = 10km/hr V2 = 15km/hr", + " T = 4.5ton rt = 1.75ton/㎥ E = 0.9", + " q = T / rt = 4.5 / 1.75 = 2.57 ㎥", + " n = q / (qs × K) = 2.57 / (0.6 × 0.55) = 7.79 회", + " t1 = Cm × n / (60 × Es) = 20 × 7.79 / (60 × 0.35) = 7.42분", + " L L 0.5 0.5", + " t2 = (── + ──) × 60 = (── + ──) × 60 = 5분(왕복)", + " V1 V2 10 15", + " t3 = 1.1분 t4 = 0.42분", + " Cm = t1 + t2 + t3 + t4 = 7.42 + 5 + 1.1 + 0.42 = 13.94 분", + " 60 × q × f × E 60 × 2.57 × 1 × 0.9", + " Q = ─────────── = ──────────── = 9.96 ㎥/hr", + " Cm 13.94", + " 노무비 : 49,778 ÷ 9.96 × 575 / 1750 = 1,642.1", + " 재료비 : 9,494 ÷ 9.96 × 575 / 1750 = 313.1", + " 경 비 : 7,613 ÷ 9.96 × 575 / 1750 = 251.1", + " 소 계 : 2,206.3", + " 4. 합 계", + " 노무비 : 4,613.3 + 1,642.1 = 6,255", + " 재료비 : 566 + 313.1 = 879", + " 경 비 : 775.7 + 251.1 = 1,026", + " 계 : 8,160" + ], + "줄수": 1, + "호표들": [ + 2 + ], + "id": "N004" + }, + { + "파일": "북부청임도", + "호표": 2, + "호표이름": "돌수로(넓은호형)", + "호표규격": "B=2.3,H=0.4,야면석,찰붙임", + "호표단위": "M", + "차례": 2, + "이름": "고임돌 채집 및 운반", + "규격": "굴삭기0.6㎥(90%),4.5톤,L=500m", + "수량": 0.27, + "단위": "㎥", + "단가": { + "계": 17512, + "노무비": 12999, + "재료비": 2078, + "경비": 2435 + }, + "갈래": "단가산출", + "참조": { + "종류": "단산", + "번": 3 + }, + "근거": { + "출처": [ + [ + "건설품셈", + "1-10" + ] + ], + "직종": [ + "보통인부" + ], + "행": [ + 133, + 218 + ] + }, + "맞춤": { + "열쇠": "고임돌채집및운반|굴삭기0.6m3(90%),4.5톤,l=500m|m3", + "대상": null, + "입력값": {}, + "못읽음": [], + "상태": "후보", + "환산": null, + "들어감": false, + "후보": [ + { + "로직": "GC000363", + "이름": "자갈채집 및 운반", + "단위": "㎥", + "점수": 0.765 + }, + { + "로직": "GF000212", + "이름": "막돌 채집", + "단위": "㎡ · ㎥", + "점수": 0.6 + }, + { + "로직": "GC000418", + "이름": "사석 적재 및 운반", + "단위": "㎥", + "점수": 0.458 + }, + { + "로직": "GF000211", + "이름": "모래·자갈·약돌 채집(인력)", + "단위": "㎥", + "점수": 0.45 + }, + { + "로직": "GF000213", + "이름": "큰돌 채집(기계)", + "단위": "개", + "점수": 0.45 + } + ] + }, + "단산머리": { + "이름": "고임돌채집및운반(굴삭기0.6㎥(90%),4.5톤,L=500m)", + "단위": "㎥" + }, + "단산글": [ + "3.고임돌채집및운반(굴삭기0.6㎥(90%),4.5톤,L=500m)[㎥]", + " 고임돌 채집 및 운반(㎥) [건설표준품셈 준용]", + " 작업조건 : 계곡에 산재한 고임돌 채집", + " 1. 채 집", + " 가. 굴착기(타이어형)(0.6㎥)", + " q = 0.6㎥ K = 0.55 f = 1", + " E = 0.35 Cm = 18sec(90˚)", + " 3,600 × q × K × f × E 3,600 × 0.6 × 0.55 × 1 × 0.35", + " Q = ────────────── = ───────────────── = 23.1 ㎥/hr", + " Cm 18", + " 노무비 : 59,020 ÷ 23.1 × 90% = 2,300", + " 재료비 : 19,792 ÷ 23.1 × 90% = 771.1", + " 경 비 : 27,120 ÷ 23.1 × 90% = 1,056.6", + " 소 계 : 4,127.7", + " 나. 인 력 (10%)", + " 보통인부 : 172,068 × 0.3인/㎥ × 10% = 5,162", + " 다. 소 계", + " 노무비 : 2,300 + 5,162 = 7,462", + " 재료비 : 771.1", + " 경 비 : 1,056.6", + " 계 : 9,289.7", + " 2. 상 차 : 굴착기(타이어형)(0.6㎥)", + " qs = 0.6㎥ K = 0.7 f = 1", + " Es = 0.45 Cm = 20sec(135˚)", + " 3,600 × qs × K × f × Es 3,600 × 0.6 × 0.7 × 1 × 0.45", + " Q = ────────────── = ───────────────── = 34.02 ㎥/hr", + " Cm 20", + " 노무비 : 59,020 ÷ 34.02 = 1,734.8", + " 재료비 : 19,792 ÷ 34.02 = 581.7", + " 경 비 : 27,120 ÷ 34.02 = 797.1", + " 소 계 : 3,113.6", + " 3. 운 반 : 덤프트럭(4.5Ton)", + " L = 0.5km V1 = 10km/hr V2 = 15km/hr", + " T = 4.5ton rt = 1.75ton/㎥ E = 0.9", + " q = T/rt = 4.5/1.75 = 2.57 ㎥", + " 건설표줌품셈(1-10) 자갈(습기)의 평균값 1.75 ton/㎥ 적용", + " n = q / (qs × K) = 2.57 / (0.6 × 0.7) = 6.12 회", + " t1 = 18 × n / (60 × Es) = 18 × 6.12 / (60 × 0.45) = 4.08 분", + " L L 0.5 0.5", + " t2 = (── + ──) × 60 = (── + ──) × 60 = 5분(왕복)", + " V1 V2 10 15", + " t3 = 1.1분 t4 = 0.42분", + " Cm = t1 + t2 + t3 + t4 = 4.08 + 5 + 1.1 + 0.42 = 10.6 분", + " 60 × q × f × E 60 × 2.57 × 1 × 0.9", + " Q = ─────────── = ──────────── = 13.09 ㎥/hr", + " Cm 10.6", + " 노무비 : 49,778 ÷ 13.09 = 3,802.7", + " 재료비 : 9,494 ÷ 13.09 = 725.2", + " 경 비 : 7,613 ÷ 13.09 = 581.5", + " 소 계 : 5,109.4", + " 4. 합 계", + " 노무비 : 7,462 + 1,734.8 + 3,802.7 = 12,999", + " 재료비 : 771.1 + 581.7 + 725.2 = 2,078", + " 경 비 : 1,056.6 + 797.1 + 581.5 = 2,435", + " 계 : 17,512" + ], + "줄수": 1, + "호표들": [ + 2 + ], + "id": "N005" + }, + { + "파일": "북부청임도", + "호표": 2, + "호표이름": "돌수로(넓은호형)", + "호표규격": "B=2.3,H=0.4,야면석,찰붙임", + "호표단위": "M", + "차례": 3, + "이름": "야면석소운반", + "규격": "L=35cm", + "수량": 3, + "단위": "㎡", + "단가": { + "계": 94227, + "노무비": 94227, + "재료비": null, + "경비": null + }, + "갈래": "단가산출", + "참조": { + "종류": "단산", + "번": 10 + }, + "근거": { + "출처": [ + [ + "건설품셈", + "1-2-7" + ] + ], + "직종": [ + "보통인부" + ], + "행": [ + 558, + 575 + ] + }, + "맞춤": { + "열쇠": "야면석소운반|l=35cm|m2", + "대상": null, + "입력값": {}, + "못읽음": [], + "상태": "후보", + "환산": null, + "들어감": false, + "후보": [ + { + "로직": "GF000214", + "이름": "야면석 채집(인력)", + "단위": "㎡ · ㎥", + "점수": 0.644 + }, + { + "로직": "GF000048", + "이름": "묘목 소운반", + "단위": "본", + "점수": 0.494 + }, + { + "로직": "GF000077", + "이름": "방제 소운반", + "단위": "㎥", + "점수": 0.494 + }, + { + "로직": "GF000149", + "이름": "불도저 운반", + "단위": "㎥", + "점수": 0.272 + }, + { + "로직": "GF000283", + "이름": "덤프트럭 자재 운반(1회 표준 운반량)", + "단위": "ton", + "점수": 0.217 + } + ] + }, + "단산머리": { + "이름": "야면석소운반(L=35cm)", + "단위": "㎡" + }, + "단산글": [ + "10.야면석소운반(L=35cm)[㎡]", + " 1. 야면석(L3=35cm내외) 소운반", + " 건설공사표준품셈 1-2-7", + " L = 6m {경사도 = 35°(α=8)} 운반회수 : n = 42회/일", + " P = 1 ㎡ × 575㎏/㎡ × 1회 ÷ 25㎏ × 1일 ÷ n = 1 × 575 × 1 ÷ 25 × 1 ÷ 42 = 0.547619", + " 보통인부 : 172,068 × P = 172,068 × 0.547619 = 94,227.7", + " 2. 합 계", + " 노무비 : 94,227", + " 재료비 :", + " 경 비 :", + " 계 : 94,227" + ], + "줄수": 3, + "호표들": [ + 2, + 13, + 47 + ], + "id": "N006" + }, + { + "파일": "북부청임도", + "호표": 2, + "호표이름": "돌수로(넓은호형)", + "호표규격": "B=2.3,H=0.4,야면석,찰붙임", + "호표단위": "M", + "차례": 4, + "이름": "야면석찰붙임", + "규격": "L3=35cm적용,장비사용", + "수량": 3, + "단위": "㎡", + "단가": { + "계": 60759, + "노무비": 49447, + "재료비": 4073, + "경비": 7239 + }, + "갈래": "단가산출", + "참조": { + "종류": "단산", + "번": 39 + }, + "근거": { + "출처": [ + [ + "산림품셈", + "13-5-2" + ] + ], + "직종": [ + "석공", + "보통인부" + ], + "행": [ + 1948, + 1974 + ] + }, + "맞춤": { + "열쇠": "야면석찰붙임|l3=35cm적용,장비사용|m2", + "대상": null, + "입력값": {}, + "못읽음": [], + "상태": "후보", + "환산": null, + "들어감": false, + "후보": [ + { + "로직": "GF000225", + "이름": "돌붙임 찰붙임(장비)", + "단위": "㎡", + "근거": { + "조항": [ + "산림품셈 13-5-2" + ], + "직종": [ + "석공" + ], + "단위": true, + "점수": 1.65, + "까닭": "조항 같음 · 직종 석공 · 단위 맞음" + }, + "점수": 2.294 + }, + { + "로직": "GF000224", + "이름": "돌붙임 메붙임(장비)", + "단위": "㎡", + "근거": { + "조항": [ + "산림품셈 13-5-2" + ], + "직종": [ + "석공" + ], + "단위": true, + "점수": 1.65, + "까닭": "조항 같음 · 직종 석공 · 단위 맞음" + }, + "점수": 1.65 + }, + { + "로직": "GF000223", + "이름": "돌붙임(인력)", + "단위": "㎡", + "근거": { + "조항": [ + "산림품셈 13-5-1" + ], + "직종": [ + "석공" + ], + "단위": true, + "점수": 1.15, + "까닭": "앞 마디 같음 · 직종 석공 · 단위 맞음" + }, + "점수": 1.15 + }, + { + "로직": "GC000263", + "이름": "찰붙임(굴착기+부착용 집게)", + "단위": "㎡", + "점수": 0.721 + }, + { + "로직": "GF000214", + "이름": "야면석 채집(인력)", + "단위": "㎡ · ㎥", + "점수": 0.644 + } + ] + }, + "단산머리": { + "이름": "야면석찰붙임(L3=35cm적용,장비사용)", + "단위": "㎡" + }, + "단산글": [ + "39.야면석찰붙임(L3=35cm적용,장비사용)[㎡]", + " [산림사업표준품셈(13-5-2)]", + " 1. 인력", + " 석 공 : 268,908 × 0.11 = 29,580", + " 보통인부 : 172,068 × 0.04 = 6,882.7", + " 인부소계 : 36,462.7", + " 2. 굴착기(타이어형)(0.6㎥+부착용집게)", + " Q = 0.22hr", + " 노무비 : 59,020 × 0.22 = 12,984.4", + " 재료비 : 18,515 × 0.22 = 4,073.3", + " 경 비 : 32,906 × 0.22 = 7,239.3", + " 소 계 : 24,297", + " 3. 합 계", + " 노무비 : 36,462.7 + 12,984.4 = 49,447", + " 재료비 : 4,073", + " 경 비 : 7,239", + " 계 : 60,759" + ], + "줄수": 7, + "호표들": [ + 2, + 4, + 5, + 17, + 24, + 25, + 47 + ], + "id": "N007" + }, + { + "파일": "북부청임도", + "호표": 3, + "호표이름": "임도표지석", + "호표규격": "910×600×150", + "호표단위": "개소", + "차례": 1, + "이름": "구조물터파기(인력100%)", + "규격": "토사", + "수량": 0.14, + "단위": "㎥", + "단가": { + "계": 80084, + "노무비": 80084, + "재료비": null, + "경비": null + }, + "갈래": "단가산출", + "참조": { + "종류": "단산", + "번": 93 + }, + "근거": { + "출처": [ + [ + "건설품셈", + "3-2-1" + ] + ], + "직종": [ + "특별인부" + ], + "행": [ + 4229, + 4244 + ] + }, + "맞춤": { + "열쇠": "구조물터파기(인력100%)|토사|m3", + "대상": null, + "입력값": {}, + "못읽음": [], + "상태": "후보", + "환산": null, + "들어감": false, + "후보": [ + { + "로직": "GC000083", + "이름": "굴착(인력/토사)", + "단위": "㎥", + "근거": { + "조항": [ + "건설품셈 공통 3-2-1" + ], + "직종": [ + "특별인부" + ], + "단위": true, + "점수": 1.75, + "까닭": "조항 같음 · 건설 공통 · 직종 특별인부 · 단위 맞음" + }, + "점수": 1.75 + }, + { + "로직": "GC000090", + "이름": "터파기(기계) 암", + "단위": "㎥", + "근거": { + "조항": [ + "건설품셈 공통 3-2-4" + ], + "직종": [], + "단위": true, + "점수": 0.75, + "까닭": "앞 마디 같음 · 건설 공통 · 단위 맞음" + }, + "점수": 1.4 + }, + { + "로직": "GC000088", + "이름": "터파기(기계) 보통토사", + "단위": "㎥", + "근거": { + "조항": [ + "건설품셈 공통 3-2-4" + ], + "직종": [], + "단위": true, + "점수": 0.75, + "까닭": "앞 마디 같음 · 건설 공통 · 단위 맞음" + }, + "점수": 1.264 + }, + { + "로직": "GC000089", + "이름": "터파기(기계) 혼합토사", + "단위": "㎥", + "근거": { + "조항": [ + "건설품셈 공통 3-2-4" + ], + "직종": [], + "단위": true, + "점수": 0.75, + "까닭": "앞 마디 같음 · 건설 공통 · 단위 맞음" + }, + "점수": 1.264 + }, + { + "로직": "GF000101", + "이름": "구조물터파기 육상토사(0~1m)", + "단위": "㎥", + "점수": 0.914 + } + ] + }, + "단산머리": { + "이름": "구조물터파기(인력100%)(토사)", + "단위": "㎥" + }, + "단산글": [ + "93.구조물터파기(인력100%)(토사)[㎥]", + " [건설공사표준품셈 3-2-1 인력굴착]", + " (보통토사+경질토사+자갈섞인 토사)/3 적용", + " 1. 인 력 (100 %) 적용", + " 특별인부 : 226,122 × (3.6+2.7+2.2)/3 ÷ 8 = 80,084.8", + " 2. 합 계", + " 노무비 : 80,084", + " 재료비 :", + " 경 비 :", + " 계 : 80,084" + ], + "줄수": 4, + "호표들": [ + 3, + 12, + 65, + 66 + ], + "id": "N008" + }, + { + "파일": "북부청임도", + "호표": 3, + "호표이름": "임도표지석", + "호표규격": "910×600×150", + "호표단위": "개소", + "차례": 2, + "이름": "구조물되메우기(인력100%)", + "규격": "", + "수량": 0.05, + "단위": "㎥", + "단가": { + "계": 17206, + "노무비": 17206, + "재료비": null, + "경비": null + }, + "갈래": "단가산출", + "참조": { + "종류": "단산", + "번": 12 + }, + "근거": { + "출처": [ + [ + "산림품셈", + "9-14" + ] + ], + "직종": [ + "보통인부" + ], + "행": [ + 626, + 641 + ] + }, + "맞춤": { + "열쇠": "구조물되메우기(인력100%)||m3", + "대상": null, + "입력값": {}, + "못읽음": [], + "상태": "후보", + "환산": null, + "들어감": false, + "후보": [ + { + "로직": "GF000113", + "이름": "되메우기", + "단위": "㎥", + "점수": 0.867 + }, + { + "로직": "GF000194", + "이름": "되메우기(노상재)", + "단위": "㎡", + "점수": 0.717 + }, + { + "로직": "GC000108", + "이름": "되메우기 및 다짐(소형장비)", + "단위": "㎥", + "점수": 0.65 + }, + { + "로직": "GC000109", + "이름": "되메우기 및 다짐(대형장비)", + "단위": "㎥", + "점수": 0.65 + }, + { + "로직": "GC000451", + "이름": "지하수 폐공 되메우기", + "단위": "m", + "점수": 0.429 + } + ] + }, + "단산머리": { + "이름": "구조물되메우기(인력100%)", + "단위": "㎥" + }, + "단산글": [ + "12.구조물되메우기(인력100%)[㎥]", + " 산림사업표준품셈 9-14중 인력품만 반영", + " 소규모이므로 협소공간 작업으로 인력100%적용", + " 1. 인 력", + " 보통인부 : 172,068 × 0.1인 = 17,206.8", + " 2. 합 계", + " 노무비 : 17,206", + " 재료비 :", + " 경 비 :", + " 계 : 17,206" + ], + "줄수": 4, + "호표들": [ + 3, + 12, + 65, + 66 + ], + "id": "N009" + }, + { + "파일": "북부청임도", + "호표": 3, + "호표이름": "임도표지석", + "호표규격": "910×600×150", + "호표단위": "개소", + "차례": 3, + "이름": "잔토처리(인력100%)", + "규격": "", + "수량": 0.09, + "단위": "㎥", + "단가": { + "계": 34413, + "노무비": 34413, + "재료비": null, + "경비": null + }, + "갈래": "단가산출", + "참조": { + "종류": "단산", + "번": 13 + }, + "근거": { + "출처": [ + [ + "건설품셈", + "3-1-3" + ] + ], + "직종": [ + "보통인부" + ], + "행": [ + 644, + 659 + ] + }, + "맞춤": { + "열쇠": "잔토처리(인력100%)||m3", + "대상": null, + "입력값": {}, + "못읽음": [], + "상태": "없음", + "환산": null, + "들어감": false, + "후보": [] + }, + "단산머리": { + "이름": "잔토처리(인력100%)", + "단위": "㎥" + }, + "단산글": [ + "13.잔토처리(인력100%)[㎥]", + " 잔토처리(인력100%)", + " [건설표준품셈(3-1-3)]", + " 1. 인 력", + " 보통인부 : 172,068 × 0.2 = 34,413.6", + " 2. 합 계", + " 노무비 : 34,413", + " 재료비 :", + " 경 비 :", + " 계 : 34,413" + ], + "줄수": 4, + "호표들": [ + 3, + 12, + 65, + 66 + ], + "id": "N010" + }, + { + "파일": "북부청임도", + "호표": 4, + "호표이름": "배수관날개벽(유입구)", + "호표규격": "D=800mm,ㄷ형", + "호표단위": "개소", + "차례": 4, + "이름": "야면석채집및운반(계곡내)", + "규격": "굴삭기0.6㎥,35cm,4.5톤,L=500m", + "수량": 3.32, + "단위": "㎡", + "단가": { + "계": 8160, + "노무비": 6255, + "재료비": 879, + "경비": 1026 + }, + "갈래": "단가산출", + "참조": { + "종류": "단산", + "번": 6 + }, + "근거": { + "출처": [ + [ + "건설품셈", + "8-2" + ] + ], + "직종": [ + "보통인부" + ], + "행": [ + 360, + 445 + ] + }, + "맞춤": { + "열쇠": "야면석채집및운반(계곡내)|굴삭기0.6m3,35cm,4.5톤,l=500m|m2", + "대상": null, + "입력값": {}, + "못읽음": [], + "상태": "후보", + "환산": null, + "들어감": false, + "후보": [ + { + "로직": "GF000214", + "이름": "야면석 채집(인력)", + "단위": "㎡ · ㎥", + "점수": 0.927 + }, + { + "로직": "GC000363", + "이름": "자갈채집 및 운반", + "단위": "㎥", + "점수": 0.615 + }, + { + "로직": "GC000267", + "이름": "굴착기(무한궤도) 시간당 기계경비", + "단위": "시간", + "근거": { + "조항": [ + "건설품셈 공통 8-4" + ], + "직종": [], + "단위": false, + "점수": 0.6, + "까닭": "앞 마디 같음 · 건설 공통" + }, + "점수": 0.6 + }, + { + "로직": "GC000270", + "이름": "굴착기(무한궤도)+부착용 집게 시간당 기계경비", + "단위": "시간", + "근거": { + "조항": [ + "건설품셈 공통 8-4" + ], + "직종": [], + "단위": false, + "점수": 0.6, + "까닭": "앞 마디 같음 · 건설 공통" + }, + "점수": 0.6 + }, + { + "로직": "GC000271", + "이름": "굴착기(무한궤도)+대형브레이커 시간당 기계경비", + "단위": "시간", + "근거": { + "조항": [ + "건설품셈 공통 8-4" + ], + "직종": [], + "단위": false, + "점수": 0.6, + "까닭": "앞 마디 같음 · 건설 공통" + }, + "점수": 0.6 + } + ] + }, + "단산머리": { + "이름": "야면석채집및운반(계곡내)(굴삭기0.6㎥,35cm,4.5톤,L=500m)", + "단위": "㎡" + }, + "단산글": [ + "6.야면석채집및운반(계곡내)(굴삭기0.6㎥,35cm,4.5톤,L=500m)[㎡]", + " 야면석 채집 및 운반", + " 작업조건 계곡에 산재하여 매몰되어 있는 곳에서 굴삭기 0.6㎥이용 채집", + " 1. 야면석 채집", + " 가. 인력(10%)", + " 보통인부 : 172,068 × 0.17인 × 10% = 2,925.1", + " [건설표준품셈(8-2, 2016년 삭제)] L3=35cm, 인/㎡의 10%로 적용", + " 나. 기계채집 : 굴착기(타이어형)(0.6㎥)", + " q = 0.6㎥ K = 0.55 f = 1", + " E = 0.35 Cm = 18sec(90˚)", + " 3,600 × q × K × f × E 3,600 × 0.6 × 0.55 × 1 × 0.35", + " Q = ────────────── = ───────────────── = 23.1 ㎥/hr", + " Cm 18", + " 노무비 : 59,020 ÷ 23.1 × 575 / 1750 × 90% = 755.5", + " 재료비 : 19,792 ÷ 23.1 × 575 / 1750 × 90% = 253.3", + " 경 비 : 27,120 ÷ 23.1 × 575 / 1750 × 90% = 347.1", + " 소 계 : 1,355.9", + " 다. 상 차 : 굴착기(타이어형)(0.6㎥)", + " qs = 0.6㎥ K = 0.55 f = 1", + " Es = 0.35 Cm = 20sec(135˚)", + " 3,600 × qs × K × f × Es 3,600 × 0.6 × 0.55 × 1 × 0.35", + " Q = ────────────── = ───────────────── = 20.79 ㎥/hr", + " Cm 20", + " 노무비 : 59,020 ÷ 20.79 × 575 / 1750 = 932.7", + " 재료비 : 19,792 ÷ 20.79 × 575 / 1750 = 312.7", + " 경 비 : 27,120 ÷ 20.79 × 575 / 1750 = 428.6", + " 소 계 : 1,674", + " 라. 소 계", + " 노무비 : 2,925.1 + 755.5 + 932.7 = 4,613.3", + " 재료비 : 253.3 + 312.7 = 566", + " 경 비 : 347.1 + 428.6 = 775.7", + " 계 : 5,955", + " 3. 운 반 : 덤프트럭(4.5Ton)", + " L = 0.5km V1 = 10km/hr V2 = 15km/hr", + " T = 4.5ton rt = 1.75ton/㎥ E = 0.9", + " q = T / rt = 4.5 / 1.75 = 2.57 ㎥", + " n = q / (qs × K) = 2.57 / (0.6 × 0.55) = 7.79 회", + " t1 = Cm × n / (60 × Es) = 20 × 7.79 / (60 × 0.35) = 7.42분", + " L L 0.5 0.5", + " t2 = (── + ──) × 60 = (── + ──) × 60 = 5분(왕복)", + " V1 V2 10 15", + " t3 = 1.1분 t4 = 0.42분", + " Cm = t1 + t2 + t3 + t4 = 7.42 + 5 + 1.1 + 0.42 = 13.94 분", + " 60 × q × f × E 60 × 2.57 × 1 × 0.9", + " Q = ─────────── = ──────────── = 9.96 ㎥/hr", + " Cm 13.94", + " 노무비 : 49,778 ÷ 9.96 × 575 / 1750 = 1,642.1", + " 재료비 : 9,494 ÷ 9.96 × 575 / 1750 = 313.1", + " 경 비 : 7,613 ÷ 9.96 × 575 / 1750 = 251.1", + " 소 계 : 2,206.3", + " 4. 합 계", + " 노무비 : 4,613.3 + 1,642.1 = 6,255", + " 재료비 : 566 + 313.1 = 879", + " 경 비 : 775.7 + 251.1 = 1,026", + " 계 : 8,160" + ], + "줄수": 7, + "호표들": [ + 4, + 5, + 13, + 17, + 24, + 25, + 47 + ], + "id": "N011" + }, + { + "파일": "북부청임도", + "호표": 4, + "호표이름": "배수관날개벽(유입구)", + "호표규격": "D=800mm,ㄷ형", + "호표단위": "개소", + "차례": 6, + "이름": "고임돌채집및운반", + "규격": "굴삭기0.6㎥(90%),4.5톤,L=500m", + "수량": 0.3, + "단위": "㎥", + "단가": { + "계": 17512, + "노무비": 12999, + "재료비": 2078, + "경비": 2435 + }, + "갈래": "단가산출", + "참조": { + "종류": "단산", + "번": 3 + }, + "근거": { + "출처": [ + [ + "건설품셈", + "1-10" + ] + ], + "직종": [ + "보통인부" + ], + "행": [ + 133, + 218 + ] + }, + "맞춤": { + "열쇠": "고임돌채집및운반|굴삭기0.6m3(90%),4.5톤,l=500m|m3", + "대상": null, + "입력값": {}, + "못읽음": [], + "상태": "후보", + "환산": null, + "들어감": false, + "후보": [ + { + "로직": "GC000363", + "이름": "자갈채집 및 운반", + "단위": "㎥", + "점수": 0.765 + }, + { + "로직": "GF000212", + "이름": "막돌 채집", + "단위": "㎡ · ㎥", + "점수": 0.6 + }, + { + "로직": "GC000418", + "이름": "사석 적재 및 운반", + "단위": "㎥", + "점수": 0.458 + }, + { + "로직": "GF000211", + "이름": "모래·자갈·약돌 채집(인력)", + "단위": "㎥", + "점수": 0.45 + }, + { + "로직": "GF000213", + "이름": "큰돌 채집(기계)", + "단위": "개", + "점수": 0.45 + } + ] + }, + "단산머리": { + "이름": "고임돌채집및운반(굴삭기0.6㎥(90%),4.5톤,L=500m)", + "단위": "㎥" + }, + "단산글": [ + "3.고임돌채집및운반(굴삭기0.6㎥(90%),4.5톤,L=500m)[㎥]", + " 고임돌 채집 및 운반(㎥) [건설표준품셈 준용]", + " 작업조건 : 계곡에 산재한 고임돌 채집", + " 1. 채 집", + " 가. 굴착기(타이어형)(0.6㎥)", + " q = 0.6㎥ K = 0.55 f = 1", + " E = 0.35 Cm = 18sec(90˚)", + " 3,600 × q × K × f × E 3,600 × 0.6 × 0.55 × 1 × 0.35", + " Q = ────────────── = ───────────────── = 23.1 ㎥/hr", + " Cm 18", + " 노무비 : 59,020 ÷ 23.1 × 90% = 2,300", + " 재료비 : 19,792 ÷ 23.1 × 90% = 771.1", + " 경 비 : 27,120 ÷ 23.1 × 90% = 1,056.6", + " 소 계 : 4,127.7", + " 나. 인 력 (10%)", + " 보통인부 : 172,068 × 0.3인/㎥ × 10% = 5,162", + " 다. 소 계", + " 노무비 : 2,300 + 5,162 = 7,462", + " 재료비 : 771.1", + " 경 비 : 1,056.6", + " 계 : 9,289.7", + " 2. 상 차 : 굴착기(타이어형)(0.6㎥)", + " qs = 0.6㎥ K = 0.7 f = 1", + " Es = 0.45 Cm = 20sec(135˚)", + " 3,600 × qs × K × f × Es 3,600 × 0.6 × 0.7 × 1 × 0.45", + " Q = ────────────── = ───────────────── = 34.02 ㎥/hr", + " Cm 20", + " 노무비 : 59,020 ÷ 34.02 = 1,734.8", + " 재료비 : 19,792 ÷ 34.02 = 581.7", + " 경 비 : 27,120 ÷ 34.02 = 797.1", + " 소 계 : 3,113.6", + " 3. 운 반 : 덤프트럭(4.5Ton)", + " L = 0.5km V1 = 10km/hr V2 = 15km/hr", + " T = 4.5ton rt = 1.75ton/㎥ E = 0.9", + " q = T/rt = 4.5/1.75 = 2.57 ㎥", + " 건설표줌품셈(1-10) 자갈(습기)의 평균값 1.75 ton/㎥ 적용", + " n = q / (qs × K) = 2.57 / (0.6 × 0.7) = 6.12 회", + " t1 = 18 × n / (60 × Es) = 18 × 6.12 / (60 × 0.45) = 4.08 분", + " L L 0.5 0.5", + " t2 = (── + ──) × 60 = (── + ──) × 60 = 5분(왕복)", + " V1 V2 10 15", + " t3 = 1.1분 t4 = 0.42분", + " Cm = t1 + t2 + t3 + t4 = 4.08 + 5 + 1.1 + 0.42 = 10.6 분", + " 60 × q × f × E 60 × 2.57 × 1 × 0.9", + " Q = ─────────── = ──────────── = 13.09 ㎥/hr", + " Cm 10.6", + " 노무비 : 49,778 ÷ 13.09 = 3,802.7", + " 재료비 : 9,494 ÷ 13.09 = 725.2", + " 경 비 : 7,613 ÷ 13.09 = 581.5", + " 소 계 : 5,109.4", + " 4. 합 계", + " 노무비 : 7,462 + 1,734.8 + 3,802.7 = 12,999", + " 재료비 : 771.1 + 581.7 + 725.2 = 2,078", + " 경 비 : 1,056.6 + 797.1 + 581.5 = 2,435", + " 계 : 17,512" + ], + "줄수": 17, + "호표들": [ + 4, + 5, + 8, + 9, + 10, + 13, + 14, + 15, + 17, + 20, + 24, + 25, + 47, + 48, + 49, + 50, + 51 + ], + "id": "N012" + }, + { + "파일": "북부청임도", + "호표": 7, + "호표이름": "L형측구옹벽", + "호표규격": "H:1.2m", + "호표단위": "M", + "차례": 1, + "이름": "레미콘타설(장비사용)", + "규격": "무근구조물", + "수량": 0.68, + "단위": "㎥", + "단가": { + "계": 29842, + "노무비": 23429, + "재료비": 2888, + "경비": 3525 + }, + "갈래": "단가산출", + "참조": { + "종류": "단산", + "번": 22 + }, + "근거": { + "출처": [ + [ + "건설품셈", + "6-1-1" + ] + ], + "직종": [ + "콘크리트공", + "보통인부" + ], + "행": [ + 1099, + 1133 + ] + }, + "맞춤": { + "열쇠": "레미콘타설(장비사용)|무근구조물|m3", + "대상": null, + "입력값": {}, + "못읽음": [], + "상태": "후보", + "환산": null, + "들어감": false, + "후보": [ + { + "로직": "GC000198", + "이름": "레디믹스트콘크리트 타설", + "단위": "㎥", + "근거": { + "조항": [ + "건설품셈 공통 6-1-1" + ], + "직종": [ + "콘크리트공" + ], + "단위": true, + "점수": 1.75, + "까닭": "조항 같음 · 건설 공통 · 직종 콘크리트공 · 단위 맞음" + }, + "점수": 1.75 + }, + { + "로직": "GC000201", + "이름": "콘크리트 펌프차 타설", + "단위": "㎥", + "근거": { + "조항": [ + "건설품셈 공통 6-1-4" + ], + "직종": [ + "콘크리트공" + ], + "단위": true, + "점수": 1.25, + "까닭": "앞 마디 같음 · 건설 공통 · 직종 콘크리트공 · 단위 맞음" + }, + "점수": 1.567 + }, + { + "로직": "GC000199", + "이름": "현장비빔타설", + "단위": "㎥", + "근거": { + "조항": [ + "건설품셈 공통 6-1-2" + ], + "직종": [ + "콘크리트공" + ], + "단위": true, + "점수": 1.25, + "까닭": "앞 마디 같음 · 건설 공통 · 직종 콘크리트공 · 단위 맞음" + }, + "점수": 1.25 + }, + { + "로직": "GF000182", + "이름": "펌프카 타설(편성 · 일일 시공량)", + "단위": "㎥", + "점수": 0.45 + }, + { + "로직": "GF000166", + "이름": "콘크리트 타설(무근진동기 제외)", + "단위": "㎥", + "점수": 0.422 + } + ] + }, + "단산머리": { + "이름": "레미콘타설(장비사용) (무근구조물)", + "단위": "㎥" + }, + "단산글": [ + "22.레미콘타설(장비사용) (무근구조물) [㎥]", + " 건설표준품셈, 6-1-1 콘크리트 타설", + " 1. 레미콘 : 별도계상", + " 2. 타설비", + " A = 63㎥/일", + " 콘크리트공 : 273,540 × 3인 ÷ 63 = 13,025.7", + " 보 통 인부 : 172,068 × 1인 ÷ 63 = 2,731.2", + " 인 부 소계 : 15,756.9", + " 공구손료 및 경장비(콘크리트 진동기등)의 기계경비(노무비의 2%)", + " 15,756.9 × 2% = 315.1", + " 3. 굴착기(타이어형)(0.6㎥)", + " Q = 8hr ÷ A = 8 ÷ 63 = 0.13hr/㎥", + " 노무비 : 59,020 × 0.13 = 7,672.6", + " 재료비 : 19,792 × 0.13 = 2,572.9", + " 경 비 : 27,120 × 0.13 = 3,525.6", + " 소 계 : 13,771.1", + " 4. 합 계", + " 노무비 : 15,756.9 + 7,672.6 = 23,429", + " 재료비 : 315.1 + 2,572.9 = 2,888", + " 경 비 : 3,525", + " 계 : 29,842" + ], + "줄수": 13, + "호표들": [ + 7, + 21, + 22, + 23, + 27, + 49, + 52, + 53, + 54, + 56, + 57, + 59, + 60 + ], + "id": "N013" + }, + { + "파일": "북부청임도", + "호표": 7, + "호표이름": "L형측구옹벽", + "호표규격": "H:1.2m", + "호표단위": "M", + "차례": 2, + "이름": "유로폼", + "규격": "보통,0~7m", + "수량": 2.97, + "단위": "㎡", + "단가": { + "계": 41724, + "노무비": 36435, + "재료비": 5289, + "경비": null + }, + "갈래": "단가산출", + "참조": { + "종류": "단산", + "번": 26 + }, + "근거": { + "출처": [ + [ + "건설품셈", + "6-3-3" + ] + ], + "직종": [ + "형틀목공", + "보통인부" + ], + "행": [ + 1253, + 1284 + ] + }, + "맞춤": { + "열쇠": "유로폼|보통,0~7m|m2", + "대상": { + "로직": "GC000214", + "이름": "유로폼 설치 및 해체", + "단위": "㎡" + }, + "입력값": { + "유형": "보통" + }, + "못읽음": [], + "상태": "같음", + "환산": { + "배": 1, + "갈래": "같음" + }, + "들어감": true + }, + "단산머리": { + "이름": "유로폼(보통,0~7m)", + "단위": "㎡" + }, + "단산글": [ + "26.유로폼(보통,0~7m) [㎡]", + " 건설공사표준품셈 6-3-3", + " 1. 재료비", + " 유로폼(패널) (600×1200) 28,500 × 0.095매 = 2,707.5", + " 2. 잡재료비 (주재료비의 55%)", + " (2,707.5) × 55% = 1,489.1", + " 3. 설치비", + " Q = 35㎡/일", + " 형틀목공 : 275,790 × 4인 ÷ 35 = 31,518.8", + " 보통인부 : 172,068 × 1인 ÷ 35 = 4,916.2", + " 인부소계 : 36,435", + " 4. 공구손료 및 경장비 기계경비 (노무비의 3%)", + " 36,435 × 3% = 1,093", + " 5. 합 계", + " 노무비 : 36,435", + " 재료비 : 2,707.5 + 1,489.1 + 1,093 = 5,289", + " 경 비 :", + " 계 : 41,724" + ], + "줄수": 13, + "호표들": [ + 7, + 21, + 22, + 23, + 27, + 52, + 53, + 54, + 55, + 56, + 57, + 59, + 60 + ], + "id": "N014" + }, + { + "파일": "북부청임도", + "호표": 7, + "호표이름": "L형측구옹벽", + "호표규격": "H:1.2m", + "호표단위": "M", + "차례": 4, + "이름": "문양거푸집", + "규격": "", + "수량": 1.05, + "단위": "㎡", + "단가": { + "계": 30369, + "노무비": 24467, + "재료비": 5902, + "경비": null + }, + "갈래": "단가산출", + "참조": { + "종류": "단산", + "번": 18 + }, + "근거": { + "출처": [ + [ + "건설품셈", + "6-3-4" + ] + ], + "직종": [ + "형틀목공", + "보통인부" + ], + "행": [ + 893, + 918 + ] + }, + "맞춤": { + "열쇠": "문양거푸집||m2", + "대상": { + "로직": "GF000161", + "이름": "문양거푸집 설치 및 해체(0~7m)", + "단위": "㎡" + }, + "입력값": {}, + "못읽음": [], + "상태": "같음", + "환산": { + "배": 1, + "갈래": "같음" + }, + "들어감": true + }, + "단산머리": { + "이름": "문양거푸집", + "단위": "㎡" + }, + "단산글": [ + "18.문양거푸집[㎡]", + " 문양거푸집 [건설표준품셈6-3-4]", + " 1. 재료비", + " 문양거푸집(ABS코팅폼)(600×1200×20~30mm) 4,166 × 1㎡ × (1/0.72)= 5,786.1", + " 잡재료비(재료비의 2%)", + " 5,786.1 × 2% = 116", + " 2. 문양거푸집설치 및 해체", + " 형틀목공 : 275,790 × 0.07인 = 19,305.3", + " 보통인부 : 172,068 × 0.03인 = 5,162", + " 인부소계 : 24,467.3", + " 3. 합 계", + " 노무비 : 24,467", + " 재료비 : 5,786.1 + 116 = 5,902", + " 경 비 :", + " 계 : 30,369" + ], + "줄수": 4, + "호표들": [ + 7, + 21, + 22, + 23 + ], + "id": "N015" + }, + { + "파일": "북부청임도", + "호표": 7, + "호표이름": "L형측구옹벽", + "호표규격": "H:1.2m", + "호표단위": "M", + "차례": 5, + "이름": "P.V.C파이프설치", + "규격": "Φ50mm", + "수량": 0.25, + "단위": "M", + "단가": { + "계": 4504, + "노무비": null, + "재료비": 4290, + "경비": 214 + }, + "갈래": "단가산출", + "참조": { + "종류": "단산", + "번": 37 + }, + "근거": { + "출처": [ + [ + "산림품셈", + "12-22" + ] + ], + "직종": [], + "행": [ + 1861, + 1878 + ] + }, + "맞춤": { + "열쇠": "p.v.c파이프설치|ø50mm|m", + "대상": null, + "입력값": {}, + "못읽음": [], + "상태": "후보", + "환산": null, + "들어감": false, + "후보": [ + { + "로직": "GF000192", + "이름": "P.V.C 파이프 설치(50㎜)", + "단위": "개소", + "근거": { + "조항": [ + "산림품셈 12-22" + ], + "직종": [], + "단위": false, + "점수": 1.0, + "까닭": "조항 같음" + }, + "점수": 3.05 + }, + { + "로직": "GF000204", + "이름": "전선관 설치(P.V.C ø54㎜)", + "단위": "m", + "근거": { + "조항": [ + "산림품셈 12-32" + ], + "직종": [], + "단위": true, + "점수": 0.65, + "까닭": "앞 마디 같음 · 단위 맞음" + }, + "점수": 1.004 + }, + { + "로직": "GF000200", + "이름": "스페이셔 설치(몰탈 블록)", + "단위": "개소", + "근거": { + "조항": [ + "산림품셈 12-29" + ], + "직종": [], + "단위": false, + "점수": 0.5, + "까닭": "앞 마디 같음" + }, + "점수": 0.693 + }, + { + "로직": "GF000203", + "이름": "물끊기 홈(NOTCH) 설치", + "단위": "개소", + "근거": { + "조항": [ + "산림품셈 12-31" + ], + "직종": [], + "단위": false, + "점수": 0.5, + "까닭": "앞 마디 같음" + }, + "점수": 0.693 + }, + { + "로직": "GF000161", + "이름": "문양거푸집 설치 및 해체(0~7m)", + "단위": "㎡", + "근거": { + "조항": [ + "산림품셈 12-5" + ], + "직종": [], + "단위": false, + "점수": 0.5, + "까닭": "앞 마디 같음" + }, + "점수": 0.661 + } + ] + }, + "단산머리": { + "이름": "P.V.C파이프설치(Φ50mm)", + "단위": "M" + }, + "단산글": [ + "37.P.V.C파이프설치(Φ50mm)[M]", + " [산림사업표준품셈 12-22]", + " 1. 재료비(VG1,Φ50mm)", + " P.V.CPIPE(VG1(50mm) 4,290 × 1m = 4,290", + " 설치비(재료비의 5%)", + " 4,290 × 5% = 214.5", + " 2. 합 계", + " 노무비 :", + " 재료비 : 4,290", + " 경 비 : 214", + " 계 : 4,504" + ], + "줄수": 11, + "호표들": [ + 7, + 21, + 22, + 23, + 52, + 53, + 54, + 56, + 57, + 59, + 60 + ], + "id": "N016" + }, + { + "파일": "북부청임도", + "호표": 7, + "호표이름": "L형측구옹벽", + "호표규격": "H:1.2m", + "호표단위": "M", + "차례": 6, + "이름": "다웰바설치", + "규격": "Φ32×1000", + "수량": 0.5, + "단위": "EA", + "단가": { + "계": 35013, + "노무비": 21925, + "재료비": 13088, + "경비": null + }, + "갈래": "단가산출", + "참조": { + "종류": "단산", + "번": 38 + }, + "근거": { + "출처": [ + [ + "건설품셈", + "6-3-10" + ], + [ + "건설품셈", + "6-2-1" + ] + ], + "직종": [ + "형틀목공", + "보통인부", + "철근공" + ], + "행": [ + 1881, + 1945 + ] + }, + "맞춤": { + "열쇠": "다웰바설치|ø32x1000|개", + "대상": null, + "입력값": {}, + "못읽음": [], + "상태": "후보", + "환산": null, + "들어감": false, + "후보": [ + { + "로직": "GC000226", + "이름": "신축이음 다웰바 설치", + "단위": "ea", + "근거": { + "조항": [ + "건설품셈 공통 6-3-10" + ], + "직종": [ + "형틀목공" + ], + "단위": true, + "점수": 1.75, + "까닭": "조항 같음 · 건설 공통 · 직종 형틀목공 · 단위 맞음" + }, + "점수": 2.567 + }, + { + "로직": "GC000227", + "이름": "신축이음 채움재 설치", + "단위": "㎡", + "근거": { + "조항": [ + "건설품셈 공통 6-3-10" + ], + "직종": [ + "형틀목공" + ], + "단위": false, + "점수": 1.6, + "까닭": "조항 같음 · 건설 공통 · 직종 형틀목공" + }, + "점수": 1.767 + }, + { + "로직": "GC000778", + "이름": "세대용 적산열량계 설치", + "단위": "개", + "근거": { + "조항": [ + "건설품셈 기계설비 6-2-1" + ], + "직종": [], + "단위": true, + "점수": 1.15, + "까닭": "조항 같음 · 단위 맞음" + }, + "점수": 1.454 + }, + { + "로직": "GC000223", + "이름": "갱폼 설치 및 해체", + "단위": "㎡", + "근거": { + "조항": [ + "건설품셈 공통 6-3-8" + ], + "직종": [ + "형틀목공" + ], + "단위": false, + "점수": 1.1, + "까닭": "앞 마디 같음 · 건설 공통 · 직종 형틀목공" + }, + "점수": 1.3 + }, + { + "로직": "GC000211", + "이름": "합판거푸집 설치 및 해체", + "단위": "㎡", + "근거": { + "조항": [ + "건설품셈 공통 6-3-1" + ], + "직종": [ + "형틀목공" + ], + "단위": false, + "점수": 1.1, + "까닭": "앞 마디 같음 · 건설 공통 · 직종 형틀목공" + }, + "점수": 1.254 + } + ] + }, + "단산머리": { + "이름": "다웰바설치(Φ32×1000)", + "단위": "EA" + }, + "단산글": [ + "38.다웰바설치(Φ32×1000)[EA]", + " 다웰바 설치-신축 이음부 [건설공사표준품셈6-3-10]", + " 1. 재료비", + " 다웰바(DOWELBAR)(Ø32×1,000) 10,500 × 1EA = 10,500", + " P.V.CPIPE(VG1(35mm) 2,252 × 1m = 2,252", + " 자재소계 : 12,752", + " 2. 인건비", + " 형틀목공 : 275,790 × 0.043인 = 11,858.9", + " 보통인부 : 172,068 × 0.009인 = 1,548.6", + " 인부소계 : 13,407.5", + " 3. 철근가공조립 간단 [건설표준품셈(6-2-1)]", + " 가. 현장가공", + " 1) 철 근 : 6.230kg (다웰바Ø32mm 이형철근 중량 산정)", + " 2) 인 부", + " 철 근 공 : 268,187 × 0.67인 × 0.00623 = 1,119.4", + " 보통인부 : 172,068 × 0.22인 × 0.00623 = 235.8", + " 인부소계 : 1,355.2", + " 기구손료(노무비의 9%)", + " 1,355.2 × 9% = 121.9", + " 나. 조 립", + " 어닐링철선(결속선)(#200.9mm200.4m/kg) 1,760 × 6.5kg × 0.00623 = 71.2", + " 철 근 공 : 268,187 × 3.53인 × 0.00623 = 5,897.9", + " 보통인부 : 172,068 × 1.18인 × 0.00623 = 1,264.9", + " 인부소계 :7,162.8", + " 다. 기구손료", + " 7,162.8 × 2% = 143.2", + " 다. 소 계", + " 노무비 : 1,355.2 + 7,162.8 = 8,518", + " 재료비 : 121.9 + 71.2 + 143.2 = 336.3", + " 경 비 :", + " 계 : 8,854.3", + " 4. 합 계", + " 노무비 : 13,407.5 + 8,518 = 21,925", + " 재료비 : 12,752 + 336.3 = 13,088", + " 경 비 :", + " 계 : 35,013" + ], + "줄수": 4, + "호표들": [ + 7, + 21, + 22, + 23 + ], + "id": "N017" + }, + { + "파일": "북부청임도", + "호표": 7, + "호표이름": "L형측구옹벽", + "호표규격": "H:1.2m", + "호표단위": "M", + "차례": 7, + "이름": "휠라설치", + "규격": "T=15mm,1000×1500mm", + "수량": 0.07, + "단위": "㎡", + "단가": { + "계": 20030, + "노무비": 9030, + "재료비": 11000, + "경비": null + }, + "갈래": "단가산출", + "참조": { + "종류": "단산", + "번": 41 + }, + "근거": { + "출처": [ + [ + "건설품셈", + "6-3-10" + ] + ], + "직종": [ + "형틀목공", + "보통인부" + ], + "행": [ + 2005, + 2026 + ] + }, + "맞춤": { + "열쇠": "휠라설치|t=15mm,1000x1500mm|m2", + "대상": null, + "입력값": {}, + "못읽음": [], + "상태": "후보", + "환산": null, + "들어감": false, + "후보": [ + { + "로직": "GC000227", + "이름": "신축이음 채움재 설치", + "단위": "㎡", + "근거": { + "조항": [ + "건설품셈 공통 6-3-10" + ], + "직종": [ + "형틀목공" + ], + "단위": true, + "점수": 1.75, + "까닭": "조항 같음 · 건설 공통 · 직종 형틀목공 · 단위 맞음" + }, + "점수": 2.082 + }, + { + "로직": "GC000226", + "이름": "신축이음 다웰바 설치", + "단위": "ea", + "근거": { + "조항": [ + "건설품셈 공통 6-3-10" + ], + "직종": [ + "형틀목공" + ], + "단위": false, + "점수": 1.6, + "까닭": "조항 같음 · 건설 공통 · 직종 형틀목공" + }, + "점수": 1.782 + }, + { + "로직": "GC000223", + "이름": "갱폼 설치 및 해체", + "단위": "㎡", + "근거": { + "조항": [ + "건설품셈 공통 6-3-8" + ], + "직종": [ + "형틀목공" + ], + "단위": true, + "점수": 1.25, + "까닭": "앞 마디 같음 · 건설 공통 · 직종 형틀목공 · 단위 맞음" + }, + "점수": 1.622 + }, + { + "로직": "GC000211", + "이름": "합판거푸집 설치 및 해체", + "단위": "㎡", + "근거": { + "조항": [ + "건설품셈 공통 6-3-1" + ], + "직종": [ + "형틀목공" + ], + "단위": true, + "점수": 1.25, + "까닭": "앞 마디 같음 · 건설 공통 · 직종 형틀목공 · 단위 맞음" + }, + "점수": 1.567 + }, + { + "로직": "GC000118", + "이름": "부착망 설치", + "단위": "㎡", + "점수": 0.436 + } + ] + }, + "단산머리": { + "이름": "휠라설치(T=15mm, 1000×1500mm)", + "단위": "㎡" + }, + "단산글": [ + "41.휠라설치(T=15mm, 1000×1500mm)[㎡]", + " [건설공사표준품셈(6-3-10)]", + " 1. 재료비", + " 휠라(T=15mm,1000×1000mm) 10,000 × 1.1 = 11,000", + " 2. 설치비", + " 형틀목공 : 275,790 × 0.029인 = 7,997.9", + " 보통인부 : 172,068 × 0.006인 = 1,032.4", + " 인부소계 : 9,030.3", + " 3. 합 계", + " 노무비 : 9,030", + " 재료비 : 11,000", + " 경 비 :", + " 계 : 20,030" + ], + "줄수": 4, + "호표들": [ + 7, + 21, + 22, + 23 + ], + "id": "N018" + }, + { + "파일": "북부청임도", + "호표": 7, + "호표이름": "L형측구옹벽", + "호표규격": "H:1.2m", + "호표단위": "M", + "차례": 8, + "이름": "철근가공조립(현장)", + "규격": "연결철근,Type-Ⅰ×30%적용", + "수량": 0.003, + "단위": "Ton", + "단가": { + "계": 246761, + "노무비": 237455, + "재료비": 9306, + "경비": null + }, + "갈래": "단가산출", + "참조": { + "종류": "단산", + "번": 29 + }, + "근거": { + "출처": [ + [ + "건설품셈", + "6-2" + ], + [ + "산림품셈", + "12-9-1" + ] + ], + "직종": [ + "철근공", + "보통인부" + ], + "행": [ + 1412, + 1458 + ] + }, + "맞춤": { + "열쇠": "철근가공조립(현장)|연결철근,type-ix30%적용|톤", + "대상": null, + "입력값": {}, + "못읽음": [], + "상태": "후보", + "환산": null, + "들어감": false, + "후보": [ + { + "로직": "GF000187", + "이름": "철근가공조립(복잡)", + "단위": "ton", + "점수": 2.2 + }, + { + "로직": "GF000207", + "이름": "암거이음받침 철근가공조립(보통)", + "단위": "ton", + "점수": 0.825 + }, + { + "로직": "GF000159", + "이름": "철근 현장가공 및 조립", + "단위": "ton", + "점수": 0.662 + }, + { + "로직": "GC000206", + "이름": "철근 현장가공", + "단위": "ton", + "점수": 0.55 + }, + { + "로직": "GC000207", + "이름": "철근 현장조립", + "단위": "ton", + "점수": 0.55 + } + ] + }, + "단산머리": { + "이름": "철근가공조립(현장)(연결철근,Type-Ⅰ×30%적용)", + "단위": "Ton" + }, + "단산글": [ + "29.철근가공조립(현장)(연결철근,Type-Ⅰ×30%적용)[Ton]", + " 철근현장가공및조립(Type-Ⅰ,1-1)의 [건설공사표준품셈6-2참조]", + " 연결철근은 30%적용[산림사업표준품셈12-9-1]", + " 1. 현장가공", + " 가. 철 근 : 별도계상", + " 나. 인건비", + " 철 근 공 : 268,187 × 3인 ÷ 4.5Ton × 0.3 = 53,637.4", + " 보통인부 : 172,068 × 1인 ÷ 4.5Ton × 0.3 = 11,472", + " 인부소계 : 65,109.4", + " 다. 기계경비(인력품의 9%)", + " 65,109.4 × 0.09 = 5,859.8", + " 라. 소 계", + " 노무비 : 65,109.4", + " 재료비 : 5,859.8", + " 경 비 :", + " 계 : 70,969.2", + " 2. 현장조립 인건비", + " 철 근 공 : 268,187 × 6인 ÷ 3.4Ton × 0.3 = 141,981.3", + " 보통인부 : 172,068 × 2인 ÷ 3.4Ton × 0.3 = 30,364.9", + " 인부소계 : 172,346.2", + " 기구손료(가공노력품의 2%)", + " 172,346.2 × 0.02 = 3,446.9", + " 3. 합 계", + " 노무비 : 65,109.4 + 172,346.2 = 237,455", + " 재료비 : 5,859.8 + 3,446.9 = 9,306", + " 경 비 :", + " 계 : 246,761" + ], + "줄수": 4, + "호표들": [ + 7, + 21, + 22, + 23 + ], + "id": "N019" + }, + { + "파일": "북부청임도", + "호표": 8, + "호표이름": "큰돌흙막이(메쌓기)", + "호표규격": "임도품셈, 발생석활용 50%적용", + "호표단위": "㎡", + "차례": 0, + "이름": "큰돌채집운반", + "규격": "공사중 발생석 활용 50%적용", + "수량": 1, + "단위": "㎡", + "단가": { + "계": 56018, + "노무비": 33404, + "재료비": 8180, + "경비": 14434 + }, + "갈래": "단가산출", + "참조": { + "종류": "단산", + "번": 30 + }, + "근거": { + "출처": [ + [ + "산림품셈", + "13-2-3" + ] + ], + "직종": [ + "작업반장" + ], + "행": [ + 1461, + 1558 + ] + }, + "맞춤": { + "열쇠": "큰돌채집운반|공사중발생석활용50%적용|m2", + "대상": null, + "입력값": {}, + "못읽음": [], + "상태": "후보", + "환산": null, + "들어감": false, + "후보": [ + { + "로직": "GF000213", + "이름": "큰돌 채집(기계)", + "단위": "개", + "근거": { + "조항": [ + "산림품셈 13-2-3" + ], + "직종": [ + "작업반장" + ], + "단위": false, + "점수": 1.5, + "까닭": "조항 같음 · 직종 작업반장" + }, + "점수": 2.3 + }, + { + "로직": "GF000212", + "이름": "막돌 채집", + "단위": "㎡ · ㎥", + "근거": { + "조항": [ + "산림품셈 13-2-2" + ], + "직종": [], + "단위": true, + "점수": 0.65, + "까닭": "앞 마디 같음 · 단위 맞음" + }, + "점수": 1.35 + }, + { + "로직": "GF000214", + "이름": "야면석 채집(인력)", + "단위": "㎡ · ㎥", + "근거": { + "조항": [ + "산림품셈 13-2-4" + ], + "직종": [], + "단위": true, + "점수": 0.65, + "까닭": "앞 마디 같음 · 단위 맞음" + }, + "점수": 1.072 + }, + { + "로직": "GF000211", + "이름": "모래·자갈·약돌 채집(인력)", + "단위": "㎥", + "근거": { + "조항": [ + "산림품셈 13-2-1" + ], + "직종": [], + "단위": false, + "점수": 0.5, + "까닭": "앞 마디 같음" + }, + "점수": 0.836 + }, + { + "로직": "GC000363", + "이름": "자갈채집 및 운반", + "단위": "㎥", + "점수": 0.364 + } + ] + }, + "단산머리": { + "이름": "큰돌채집운반(공사중 발생석 활용 50%적용)", + "단위": "㎡" + }, + "단산글": [ + "30.큰돌채집운반(공사중 발생석 활용 50%적용)[㎡]", + " 큰돌의 채집 및 운반 [임도표준품셈 13-2-3]", + " 큰돌 뒷길이 60~75㎝내외(뒷길이 67.5cm적용)", + " 공사시 발생하는 큰돌에 대해서는 채집품의 50% 적용", + " 큰돌운반거리는 500m로 가정하여 적용", + " Ta = 1 × 0.675 × 0.77(공극률제외) = 0.52㎥/㎡", + " 1. 큰돌채집", + " P = 4개/㎡ (40cm이상~60cm미만이므로 평균 50cm 적용)", + " 가. 인 력", + " 작업반장 : 215,907×P×0.026인/EA×0.75(품할증)×0.5 = 215,907×4×0.026×0.75×0.5 = 8,420.3", + " 나. 기 계 : 굴착기(타이어형)(0.6㎥)", + " 노무비 : 59,020×P×0.134×0.75(품할증)×0.5 = 59,020×4×0.134×0.75×0.5 = 11,863", + " 재료비 : 19,792×P×0.134×0.75(품할증)×0.5 = 19,792×4×0.134×0.75×0.5 = 3,978.1", + " 경 비 : 27,120×P×0.134×0.75(품할증)×0.5 = 27,120×4×0.134×0.75×0.5 = 5,451.1", + " 소 계 : 21,292.2", + " 다. 소 계", + " 노무비 : 8,420.3 + 11,863 = 20,283.3", + " 재료비 : 3,978.1", + " 경 비 : 5,451.1", + " 계 : 29,712.5", + " 2. 큰돌상차", + " 가. 굴착기(타이어형)(0.6㎥)", + " 노무비 : 59,020×P×0.134×0.75(품할증)×0.5 = 59,020×4×0.134×0.75×0.5 = 11,863", + " 재료비 : 19,792×P×0.134×0.75(품할증)×0.5 = 19,792×4×0.134×0.75×0.5 = 3,978.1", + " 경 비 : 27,120×P×0.134×0.75(품할증)×0.5 = 27,120×4×0.134×0.75×0.5 = 5,451.1", + " 소 계 : 21,292.2", + " 나. 부착용집게(0.7~0.8㎥)", + " 노무비 : 0×P×0.134×0.75(품할증)×0.5 = 0×4×0.134×0.75×0.5 = 0", + " 재료비 : 0×P×0.134×0.75(품할증)×0.5 = 0×4×0.134×0.75×0.5 = 0", + " 경 비 : 5,786×P×0.134×0.75(품할증)×0.5 = 5,786×4×0.134×0.75×0.5 = 1,162.9", + " 소 계 : 1,162.9", + " 다. 소 계", + " 노무비 : 11,863", + " 재료비 : 3,978.1", + " 경 비 : 5,451.1 + 1,162.9 = 6,614", + " 계 : 22,455.1", + " 3. 제잡비(노무비의 6%)", + " (8,420.3 + 11,863 + 11,863) × 6% = 1,928.7", + " 4. 운 반 [임도표준품셈의 발파암 기준 적용]", + " 덤프트럭(15Ton)", + " L = 0.5km V1 = 5km/hr V2 = 6km/hr", + " T = 15.0ton rt = 2.4ton/㎥ E = 0.9 k = 0.55 Es = 0.35", + " q = T/rt × 1.35 = 15/2.4 × 1.35 = 8.44 ㎥ f = 1", + " n = q / (k×0.6) = 8.44 / (0.55×0.6) = 25.58 회", + " t1 = 20 × n / (60 × Es) = 20 × 25.58 / (60 × 0.35) = 24.36 분", + " L L 0.5 0.5", + " t2 = (── + ──) × 60 = (── + ──) × 60 = 11 분(왕복)", + " V1 V2 5 6", + " t3 = 1.1분(적하) t4 = 0.9분(대기)", + " Cm = t1 + t2 + t3 + t4 = 24.36 + 11 + 1.1 + 0.9 = 37.36분", + " 60 × q × f × E 60 × 8.44 × 1 × 0.9", + " Q = ─────────── = ──────────── = 12.2 ㎥/hr", + " Cm 37.36", + " 노무비 : 59,020÷12.2×Ta×0.5 = 59,020÷12.2×0.52×0.5 = 1,257.8", + " 재료비 : 30,192÷12.2×((Cm-t1)/Cm)×Ta×0.5 = 30,192÷12.2×((37.36-24.36)/37.36)×0.52×0.5 = 223.8", + " 경 비 : 20,659÷12.2×Ta×0.5 = 20,659÷12.2×0.52×0.5 = 440.2", + " 소 계 : 1,921.8", + " 5. 합 계", + " 노무비 : 20,283.3 + 11,863 + 1,257.8 = 33,404", + " 재료비 : 3,978.1 + 3,978.1 + 223.8 = 8,180", + " 경 비 : 5,451.1 + 6,614 + 1,928.7 + 440.2 = 14,434", + " 계 : 56,018" + ], + "줄수": 4, + "호표들": [ + 8, + 14, + 15, + 20 + ], + "id": "N020" + }, + { + "파일": "북부청임도", + "호표": 8, + "호표이름": "큰돌흙막이(메쌓기)", + "호표규격": "임도품셈, 발생석활용 50%적용", + "호표단위": "㎡", + "차례": 1, + "이름": "큰돌쌓기(메쌓기)", + "규격": "직경40cm이상~60cm미만", + "수량": 1, + "단위": "㎡", + "단가": { + "계": 104789, + "노무비": 82815, + "재료비": 9377, + "경비": 12597 + }, + "갈래": "단가산출", + "참조": { + "종류": "단산", + "번": 32 + }, + "근거": { + "출처": [ + [ + "산림품셈", + "13-6-1" + ] + ], + "직종": [ + "작업반장", + "특별인부", + "보통인부" + ], + "행": [ + 1605, + 1646 + ] + }, + "맞춤": { + "열쇠": "큰돌쌓기(메쌓기)|직경40cm이상~60cm미만|m2", + "대상": null, + "입력값": {}, + "못읽음": [], + "상태": "후보", + "환산": null, + "들어감": false, + "후보": [ + { + "로직": "GF000226", + "이름": "큰돌쌓기 메쌓기", + "단위": "㎡", + "근거": { + "조항": [ + "산림품셈 13-6-1" + ], + "직종": [ + "작업반장", + "특별인부" + ], + "단위": true, + "점수": 1.65, + "까닭": "조항 같음 · 직종 작업반장 · 특별인부 · 단위 맞음" + }, + "점수": 2.6 + }, + { + "로직": "GF000227", + "이름": "큰돌쌓기 찰쌓기", + "단위": "㎡", + "근거": { + "조항": [ + "산림품셈 13-6-2" + ], + "직종": [ + "작업반장", + "특별인부" + ], + "단위": true, + "점수": 1.15, + "까닭": "앞 마디 같음 · 직종 작업반장 · 특별인부 · 단위 맞음" + }, + "점수": 2.1 + }, + { + "로직": "GF000228", + "이름": "친환경 큰돌쌓기", + "단위": "㎡", + "근거": { + "조항": [ + "산림품셈 13-6-3" + ], + "직종": [ + "작업반장", + "특별인부" + ], + "단위": true, + "점수": 1.15, + "까닭": "앞 마디 같음 · 직종 작업반장 · 특별인부 · 단위 맞음" + }, + "점수": 2.017 + }, + { + "로직": "GC000575", + "이름": "벽돌 쌓기(시멘트벽돌)", + "단위": "㎡", + "점수": 0.817 + }, + { + "로직": "GF000219", + "이름": "돌쌓기 메쌓기(인력)", + "단위": "㎡", + "점수": 0.771 + } + ] + }, + "단산머리": { + "이름": "큰돌쌓기(메쌓기)(직경40cm이상~60cm미만)", + "단위": "㎡" + }, + "단산글": [ + "32.큰돌쌓기(메쌓기)(직경40cm이상~60cm미만)[㎡]", + " [산림사업표준품셈 13-6-1]", + " 1. 큰돌쌓기(찰쌓기)", + " 가. 노무비", + " 작업반장 : 215,907 × 0.83 ÷ 10㎡ = 17,920.2", + " 특별인부 : 226,122 × 1.04 ÷ 10㎡ = 23,516.6", + " 보통인부 : 172,068 × 1.04 ÷ 10㎡ = 17,895", + " 인부소계 : 59,331.8", + " 나. 굴착기(무한궤도)(0.8㎥+부착용집게)", + " Q = 3.84 ÷ 10㎡ = 0.384", + " 노무비 : 59,020 × 0.384 = 22,663.6", + " 재료비 : 24,421 × 0.384 = 9,377.6", + " 경 비 : 32,805 × 0.384 = 12,597.1", + " 소 계 : 44,638.3", + " 라. 잡재료비", + " (59,331.8 + 22,663.6) × 1% = 819.9", + " 마. 소 계", + " 노무비 : 59,331.8 + 22,663.6 + 819.9 = 82,815.3", + " 재료비 : 9,377.6", + " 경 비 : 12,597.1", + " 계 : 104,790", + " 2. 합 계", + " 노무비 : 82,815", + " 재료비 : 9,377", + " 경 비 : 12,597", + " 계 : 104,789" + ], + "줄수": 2, + "호표들": [ + 8, + 10 + ], + "id": "N021" + }, + { + "파일": "북부청임도", + "호표": 8, + "호표이름": "큰돌흙막이(메쌓기)", + "호표규격": "임도품셈, 발생석활용 50%적용", + "호표단위": "㎡", + "차례": 2, + "이름": "뒷채움돌채집운반뒷채움(발파)", + "규격": "굴삭기0.6㎥(90%),4.5톤,L=500m", + "수량": 0.3, + "단위": "㎥", + "단가": { + "계": 29530, + "노무비": 22465, + "재료비": 3235, + "경비": 3830 + }, + "갈래": "단가산출", + "참조": { + "종류": "단산", + "번": 2 + }, + "근거": { + "출처": [ + [ + "건설품셈", + "1-10" + ], + [ + "건설품셈", + "3-4-4" + ] + ], + "직종": [ + "보통인부", + "특별인부" + ], + "행": [ + 8, + 130 + ] + }, + "맞춤": { + "열쇠": "뒷채움돌채집운반뒷채움(발파)|굴삭기0.6m3(90%),4.5톤,l=500m|m3", + "대상": null, + "입력값": {}, + "못읽음": [], + "상태": "후보", + "환산": null, + "들어감": false, + "후보": [ + { + "로직": "GC000106", + "이름": "뒤채움 및 다짐(소형장비)", + "단위": "㎥", + "근거": { + "조항": [ + "건설품셈 공통 3-4-4" + ], + "직종": [ + "특별인부" + ], + "단위": true, + "점수": 1.75, + "까닭": "조항 같음 · 건설 공통 · 직종 특별인부 · 단위 맞음" + }, + "점수": 1.75 + }, + { + "로직": "GC000107", + "이름": "뒤채움 및 다짐(대형장비)", + "단위": "㎥", + "근거": { + "조항": [ + "건설품셈 공통 3-4-5" + ], + "직종": [ + "특별인부" + ], + "단위": true, + "점수": 1.25, + "까닭": "앞 마디 같음 · 건설 공통 · 직종 특별인부 · 단위 맞음" + }, + "점수": 1.25 + }, + { + "로직": "GC000102", + "이름": "흙쌓기(다짐)", + "단위": "㎥", + "근거": { + "조항": [ + "건설품셈 공통 3-4-1" + ], + "직종": [ + "특별인부" + ], + "단위": true, + "점수": 1.25, + "까닭": "앞 마디 같음 · 건설 공통 · 직종 특별인부 · 단위 맞음" + }, + "점수": 1.25 + }, + { + "로직": "GC000104", + "이름": "암쌓기", + "단위": "㎥", + "근거": { + "조항": [ + "건설품셈 공통 3-4-2" + ], + "직종": [ + "특별인부" + ], + "단위": true, + "점수": 1.25, + "까닭": "앞 마디 같음 · 건설 공통 · 직종 특별인부 · 단위 맞음" + }, + "점수": 1.25 + }, + { + "로직": "GF000212", + "이름": "막돌 채집", + "단위": "㎡ · ㎥", + "점수": 0.564 + } + ] + }, + "단산머리": { + "이름": "뒷채움돌채집운반뒷채움(발파)(굴삭기0.6㎥(90%),4.5톤,L=500m)", + "단위": "㎥" + }, + "단산글": [ + "2.뒷채움돌채집운반뒷채움(발파)(굴삭기0.6㎥(90%),4.5톤,L=500m)[㎥]", + " 뒷채움돌 채집, 운반, 뒷채움 및 다짐 [건설표준품셈 준용]", + " 작업조건 : 발파후 파쇄암중에 뒷채움돌 채집(㎥)", + " 일정량 채취후 상차 및 운반(L=500m)", + " 1. 채 집", + " 가. 굴착기(타이어형)(0.6㎥)", + " q = 0.6㎥ K = 0.55 f = 1", + " E = 0.35 Cm = 18sec(90˚)", + " 3,600 × q × K × f × E 3,600 × 0.6 × 0.55 × 1 × 0.35", + " Q = ────────────── = ───────────────── = 23.1 ㎥/hr", + " Cm 18", + " 노무비 : 59,020 ÷ 23.1 × 90% = 2,299.4", + " 재료비 : 19,792 ÷ 23.1 × 90% = 771.1", + " 경 비 : 27,120 ÷ 23.1 × 90% = 1,056.6", + " 소 계 : 4,127.1", + " 나. 인 력 (10%)", + " 보통인부 : 172,068 × 0.3인/㎥ × 10% = 5,162", + " 다. 소 계", + " 노무비 : 2,299.4 + 5,162 = 7,461.4", + " 재료비 : 771.1", + " 경 비 : 1,056.6", + " 계 : 9,289.1", + " 2. 상 차 : 굴착기(타이어형)(0.6㎥)", + " qs = 0.6㎥ K = 0.7 f = 1", + " Es = 0.45 Cm = 20sec(135˚)", + " 3,600 × qs × K × f × Es 3,600 × 0.6 × 0.7 × 1 × 0.45", + " Q = ────────────── = ───────────────── = 34.02 ㎥/hr", + " Cm 20", + " 노무비 : 59,020 ÷ 34.02 = 1,734.8", + " 재료비 : 19,792 ÷ 34.02 = 581.7", + " 경 비 : 27,120 ÷ 34.02 = 797.1", + " 소 계 : 3,113.6", + " 3. 운 반 : 덤프트럭(4.5Ton)", + " L = 0.5km V1 = 7km/hr V2 = 8km/hr", + " T = 4.5ton rt = 1.75ton/㎥ E = 0.9", + " q = T/rt = 4.5/1.75 = 2.57 ㎥ K = 0.55", + " 건설표줌품셈(1-10) 자갈(습기)의 평균값 1.75 ton/㎥ 적용", + " n = q / (qs × K) = 2.57 / (0.6 × 0.55) = 7.79 회", + " t1 = 18 × n / (60 × Es) = 18 × 7.79 / (60 × 0.45) = 5.19 분", + " L L 0.5 0.5", + " t2 = (── + ──) × 60 = (── + ──) × 60 = 8.04분(왕복)", + " V1 V2 7 8", + " t3 = 1.1분 t4 = 0.7분", + " Cm = t1 + t2 + t3 + t4 = 5.19 + 8.04 + 1.1 + 0.7 = 15.03 분", + " 60 × q × f × E 60 × 2.57 × 1 × 0.9", + " Q = ─────────── = ──────────── = 9.23 ㎥/hr", + " Cm 15.03", + " 노무비 : 49,778 ÷ 9.23 = 5,393", + " 재료비 : 9,494 ÷ 9.23 = 1,028.6", + " 경 비 : 7,613 ÷ 9.23 = 824.8", + " 소 계 : 7,246.4", + " 4. 뒷채움 및 다짐 [건설공사건설표준품셈 3-4-4] 소형장비", + " 가. 굴착기(무한궤도)(0.2㎥)", + " 1일 시공량 : Qd = 110㎥/일", + " Q = Qd ÷ 8hr = 110 ÷ 8 = 13.75㎥/hr", + " 노무비 : 59,020 ÷ 13.75 = 4,292.3", + " 재료비 : 8,324 ÷ 13.75 = 605.3", + " 경 비 : 13,873 ÷ 13.75 = 1,008.9", + " 소 계 : 5,906.5", + " 다. 진동로울러(핸드가이드식)(0.7Ton)", + " 노무비 : 36,248 ÷ 13.75 = 2,636.2", + " 재료비 : 3,420 ÷ 13.75 = 248.7", + " 경 비 : 1,967 ÷ 13.75 = 143", + " 소 계 : 391.7", + " 라. 인 력", + " 보통인부 : 172,068 × 0.009인 = 1,548.6", + " 특별인부 : 226,122 × 0.009인 = 2,035", + " 인부소계 :3,583.6", + " 마. 소 계", + " 노무비 : 4,292.3 + 3,583.6 = 7,875.9", + " 재료비 : 605.3 + 248.7 = 854", + " 경 비 : 1,008.9 + 143 = 1,151.9", + " 계 : 9,881.8", + " 4. 합 계", + " 노무비 : 7,461.4 + 1,734.8 + 5,393 + 7,875.9 = 22,465", + " 재료비 : 771.1 + 581.7 + 1,028.6 + 854 = 3,235", + " 경 비 : 1,056.6 + 797.1 + 824.8 + 1,151.9 = 3,830", + " 계 : 29,530" + ], + "줄수": 11, + "호표들": [ + 8, + 9, + 10, + 20, + 25, + 26, + 34, + 35, + 36, + 37, + 48 + ], + "id": "N022" + }, + { + "파일": "북부청임도", + "호표": 9, + "호표이름": "큰돌흙막이(찰쌓기)", + "호표규격": "임도품셈,큰돌구입", + "호표단위": "㎡", + "차례": 2, + "이름": "큰돌쌓기(찰쌓기)_배수공", + "규격": "직경40cm이상~60cm미만", + "수량": 1, + "단위": "㎡", + "단가": { + "계": 134304, + "노무비": 106836, + "재료비": 11722, + "경비": 15746 + }, + "갈래": "단가산출", + "참조": { + "종류": "단산", + "번": 31 + }, + "근거": { + "출처": [ + [ + "산림품셈", + "13-6-2" + ] + ], + "직종": [ + "작업반장", + "특별인부", + "보통인부" + ], + "행": [ + 1561, + 1602 + ] + }, + "맞춤": { + "열쇠": "큰돌쌓기(찰쌓기)_배수공|직경40cm이상~60cm미만|m2", + "대상": null, + "입력값": {}, + "못읽음": [], + "상태": "후보", + "환산": null, + "들어감": false, + "후보": [ + { + "로직": "GF000227", + "이름": "큰돌쌓기 찰쌓기", + "단위": "㎡", + "근거": { + "조항": [ + "산림품셈 13-6-2" + ], + "직종": [ + "작업반장", + "특별인부" + ], + "단위": true, + "점수": 1.65, + "까닭": "조항 같음 · 직종 작업반장 · 특별인부 · 단위 맞음" + }, + "점수": 2.35 + }, + { + "로직": "GF000226", + "이름": "큰돌쌓기 메쌓기", + "단위": "㎡", + "근거": { + "조항": [ + "산림품셈 13-6-1" + ], + "직종": [ + "작업반장", + "특별인부" + ], + "단위": true, + "점수": 1.15, + "까닭": "앞 마디 같음 · 직종 작업반장 · 특별인부 · 단위 맞음" + }, + "점수": 1.85 + }, + { + "로직": "GF000228", + "이름": "친환경 큰돌쌓기", + "단위": "㎡", + "근거": { + "조항": [ + "산림품셈 13-6-3" + ], + "직종": [ + "작업반장", + "특별인부" + ], + "단위": true, + "점수": 1.15, + "까닭": "앞 마디 같음 · 직종 작업반장 · 특별인부 · 단위 맞음" + }, + "점수": 1.812 + }, + { + "로직": "GF000219", + "이름": "돌쌓기 메쌓기(인력)", + "단위": "㎡", + "점수": 0.564 + }, + { + "로직": "GF000220", + "이름": "돌쌓기 메쌓기(장비)", + "단위": "㎡", + "점수": 0.564 + } + ] + }, + "단산머리": { + "이름": "큰돌쌓기(찰쌓기)_배수공(직경40cm이상~60cm미만)", + "단위": "㎡" + }, + "단산글": [ + "31.큰돌쌓기(찰쌓기)_배수공(직경40cm이상~60cm미만)[㎡]", + " 큰돌쌓기(찰쌓기) [산림사업표준품셈 13-6-2]", + " 1. 큰돌쌓기(찰쌓기)", + " 가. 노무비", + " 작업반장 : 215,907 × 0.83 ÷ 10㎡ = 17,920.2", + " 특별인부 : 226,122 × 1.30 ÷ 10㎡ = 29,396", + " 보통인부 : 172,068 × 1.30 ÷ 10㎡ = 22,369", + " 인부소계 : 69,685.2", + " 나. 굴착기(무한궤도)(0.8㎥+부착용집게)", + " Q = 4.8 ÷ 10㎡ = 0.48", + " 노무비 : 59,020 × 0.48 = 28,329.6", + " 재료비 : 24,421 × 0.48 = 11,722", + " 경 비 : 32,805 × 0.48 = 15,746.4", + " 소 계 : 55,798", + " 다. 제잡비 비율(물빼기 파이프 설치시)", + " (69,685.2 + 28,329.6) × 9% = 8,821.3", + " 라. 소 계", + " 노무비 : 69,685.2 + 28,329.6 + 8,821.3 = 106,836.1", + " 재료비 : 11,722", + " 경 비 : 15,746.4", + " 계 : 134,304.5", + " 2. 합 계", + " 노무비 : 106,836", + " 재료비 : 11,722", + " 경 비 : 15,746", + " 계 : 134,304" + ], + "줄수": 2, + "호표들": [ + 9, + 20 + ], + "id": "N023" + }, + { + "파일": "북부청임도", + "호표": 12, + "호표이름": "임도반사경", + "호표규격": "D=1000mm(STS)", + "호표단위": "개소", + "차례": 0, + "이름": "레미콘타설(인력운반)", + "규격": "무근구조물-소량 타설", + "수량": 0.05, + "단위": "㎥", + "단가": { + "계": 118569, + "노무비": 116245, + "재료비": 2324, + "경비": null + }, + "갈래": "단가산출", + "참조": { + "종류": "단산", + "번": 21 + }, + "근거": { + "출처": [ + [ + "건설품셈", + "6-1-1" + ] + ], + "직종": [ + "콘크리트공", + "보통인부" + ], + "행": [ + 1071, + 1096 + ] + }, + "맞춤": { + "열쇠": "레미콘타설(인력운반)|무근구조물-소량타설|m3", + "대상": null, + "입력값": {}, + "못읽음": [], + "상태": "후보", + "환산": null, + "들어감": false, + "후보": [ + { + "로직": "GC000198", + "이름": "레디믹스트콘크리트 타설", + "단위": "㎥", + "근거": { + "조항": [ + "건설품셈 공통 6-1-1" + ], + "직종": [ + "콘크리트공" + ], + "단위": true, + "점수": 1.75, + "까닭": "조항 같음 · 건설 공통 · 직종 콘크리트공 · 단위 맞음" + }, + "점수": 1.75 + }, + { + "로직": "GC000201", + "이름": "콘크리트 펌프차 타설", + "단위": "㎥", + "근거": { + "조항": [ + "건설품셈 공통 6-1-4" + ], + "직종": [ + "콘크리트공" + ], + "단위": true, + "점수": 1.25, + "까닭": "앞 마디 같음 · 건설 공통 · 직종 콘크리트공 · 단위 맞음" + }, + "점수": 1.567 + }, + { + "로직": "GC000199", + "이름": "현장비빔타설", + "단위": "㎥", + "근거": { + "조항": [ + "건설품셈 공통 6-1-2" + ], + "직종": [ + "콘크리트공" + ], + "단위": true, + "점수": 1.25, + "까닭": "앞 마디 같음 · 건설 공통 · 직종 콘크리트공 · 단위 맞음" + }, + "점수": 1.25 + }, + { + "로직": "GF000182", + "이름": "펌프카 타설(편성 · 일일 시공량)", + "단위": "㎥", + "점수": 0.45 + }, + { + "로직": "GF000166", + "이름": "콘크리트 타설(무근진동기 제외)", + "단위": "㎥", + "점수": 0.422 + } + ] + }, + "단산머리": { + "이름": "레미콘타설(인력운반) (무근구조물-소량 타설)", + "단위": "㎥" + }, + "단산글": [ + "21.레미콘타설(인력운반) (무근구조물-소량 타설) [㎥]", + " 건설표준품셈 6-1-1", + " 1. 레미콘 : 별도계상", + " 2. 타설비", + " A = 23㎥/일", + " 콘크리트공 : 273,540 × 3인 / 23 ×2 = 71,358.2", + " 보 통 인부 : 172,068 × 3인 / 23 ×2 = 44,887.3", + " 인 부 소계 : 116,245.5", + " 공구손료 및 경장비(콘크리트 진동기등)의 기계경비(노무비의 2%)", + " 116,245.5 × 2% = 2,324.9", + " 3. 합 계", + " 노무비 : 116,245", + " 재료비 : 2,324", + " 경 비 :", + " 계 : 118,569" + ], + "줄수": 3, + "호표들": [ + 12, + 65, + 66 + ], + "id": "N024" + }, + { + "파일": "북부청임도", + "호표": 12, + "호표이름": "임도반사경", + "호표규격": "D=1000mm(STS)", + "호표단위": "개소", + "차례": 1, + "이름": "합판거푸집", + "규격": "6회", + "수량": 0.6, + "단위": "㎡", + "단가": { + "계": 47553, + "노무비": 34461, + "재료비": 13092, + "경비": null + }, + "갈래": "단가산출", + "참조": { + "종류": "단산", + "번": 25 + }, + "근거": { + "출처": [ + [ + "건설품셈", + "6-3-1" + ] + ], + "직종": [ + "형틀목공", + "보통인부" + ], + "행": [ + 1213, + 1250 + ] + }, + "맞춤": { + "열쇠": "합판거푸집|6회|m2", + "대상": { + "로직": "GF000160", + "이름": "합판거푸집", + "단위": "㎡" + }, + "입력값": { + "횟수": "6회사용시" + }, + "못읽음": [], + "상태": "같음", + "환산": { + "배": 1, + "갈래": "같음" + }, + "들어감": true + }, + "단산머리": { + "이름": "합판거푸집(6회)", + "단위": "㎡" + }, + "단산글": [ + "25.합판거푸집(6회)[㎡]", + " 건설공사표준품셈, 6-3-1 합판거푸집", + " ※ 사용횟수 6회 : 간단", + " 1. 재료비", + " 내수합판1급(12mm1220×2440) 10,984 × 1.03㎡ × 32.7% = 3,699.5", + " 각재(거푸집용,외송) (3.6×3.0×3.0) 626,543 × 0.038㎥ × 32.7% = 7,785.4", + " 주자재소계 : 11,484.9", + " 소모자재(박리재등)(주자재비의 11%)", + " 11,484.9 × 11% = 1,263.3", + " 2. 인건비", + " Q = 50㎡/일", + " 형틀목공 : 275,790 × 5인 ÷ 50 = 27,579", + " 보통인부 : 172,068 × 2인 ÷ 50 = 6,882.7", + " 인부소계 : 34,461.7", + " 공구손료 및 경장비 기계경비(노무비의 1%)", + " 34,461.7 × 1% = 344.6", + " 3. 합 계", + " 노무비 : 34,461", + " 재료비 : 11,484.9 + 1,263.3 + 344.6 = 13,092", + " 경 비 :", + " 계 : 47,553" + ], + "줄수": 12, + "호표들": [ + 12, + 18, + 19, + 38, + 39, + 40, + 41, + 42, + 43, + 44, + 65, + 66 + ], + "id": "N025" + }, + { + "파일": "북부청임도", + "호표": 12, + "호표이름": "임도반사경", + "호표규격": "D=1000mm(STS)", + "호표단위": "개소", + "차례": 2, + "이름": "도로반사경설치", + "규격": "D=1000(STS)", + "수량": 1, + "단위": "개소", + "단가": { + "계": 322533, + "노무비": 99547, + "재료비": 220000, + "경비": 2986 + }, + "갈래": "단가산출", + "참조": { + "종류": "단산", + "번": 42 + }, + "근거": { + "출처": [ + [ + "건설품셈", + "1-8-3" + ] + ], + "직종": [ + "특별인부", + "보통인부" + ], + "행": [ + 2029, + 2054 + ] + }, + "맞춤": { + "열쇠": "도로반사경설치|d=1000(sts)|개소", + "대상": { + "로직": "GC000310", + "이름": "도로반사경 설치", + "단위": "본" + }, + "입력값": {}, + "못읽음": [], + "상태": "같음", + "환산": { + "배": null, + "갈래": "입력" + }, + "들어감": false + }, + "단산머리": { + "이름": "도로반사경설치(D=1000(STS))", + "단위": "개소" + }, + "단산글": [ + "42.도로반사경설치(D=1000(STS))[개소]", + " [건설공사표준품셈 1-8-3]", + " 1. 재료비", + " 도로반사경(지주포함)(D=1000mm(STS) 220,000 × 1EA = 220,000", + " 2. 인건비", + " 특별인부 : 226,122 ÷ 1인 ÷ 4본/일 = 56,530.5", + " 보통인부 : 172,068 ÷ 1인 ÷ 4본/일 = 43,017", + " 인부소계 : 99,547.5", + " 공구손료 및 경장비", + " 99,547.5 × 3% = 2,986.4", + " 3. 합 계", + " 노무비 : 99,547", + " 재료비 : 220,000", + " 경 비 : 2,986", + " 계 : 322,533" + ], + "줄수": 1, + "호표들": [ + 12 + ], + "id": "N026" + }, + { + "파일": "북부청임도", + "호표": 13, + "호표이름": "돌수로(넓은호형)", + "호표규격": "B=2.6,야면석,메붙임", + "호표단위": "M", + "차례": 2, + "이름": "야면석메붙임", + "규격": "L3 =35cm내외,장비사용", + "수량": 2.6, + "단위": "㎡", + "단가": { + "계": 69449, + "노무비": 56595, + "재료비": 4628, + "경비": 8226 + }, + "갈래": "단가산출", + "참조": { + "종류": "단산", + "번": 88 + }, + "근거": { + "출처": [ + [ + "산림품셈", + "13-5-2" + ] + ], + "직종": [ + "석공", + "보통인부" + ], + "행": [ + 4008, + 4036 + ] + }, + "맞춤": { + "열쇠": "야면석메붙임|l3=35cm내외,장비사용|m2", + "대상": null, + "입력값": {}, + "못읽음": [], + "상태": "후보", + "환산": null, + "들어감": false, + "후보": [ + { + "로직": "GF000224", + "이름": "돌붙임 메붙임(장비)", + "단위": "㎡", + "근거": { + "조항": [ + "산림품셈 13-5-2" + ], + "직종": [ + "석공" + ], + "단위": true, + "점수": 1.65, + "까닭": "조항 같음 · 직종 석공 · 단위 맞음" + }, + "점수": 2.294 + }, + { + "로직": "GF000225", + "이름": "돌붙임 찰붙임(장비)", + "단위": "㎡", + "근거": { + "조항": [ + "산림품셈 13-5-2" + ], + "직종": [ + "석공" + ], + "단위": true, + "점수": 1.65, + "까닭": "조항 같음 · 직종 석공 · 단위 맞음" + }, + "점수": 1.65 + }, + { + "로직": "GF000223", + "이름": "돌붙임(인력)", + "단위": "㎡", + "근거": { + "조항": [ + "산림품셈 13-5-1" + ], + "직종": [ + "석공" + ], + "단위": true, + "점수": 1.15, + "까닭": "앞 마디 같음 · 직종 석공 · 단위 맞음" + }, + "점수": 1.15 + }, + { + "로직": "GC000262", + "이름": "메붙임(굴착기+부착용 집게)", + "단위": "㎡", + "점수": 0.721 + }, + { + "로직": "GF000214", + "이름": "야면석 채집(인력)", + "단위": "㎡ · ㎥", + "점수": 0.644 + } + ] + }, + "단산머리": { + "이름": "야면석메붙임(L3 =35cm내외,장비사용)", + "단위": "㎡" + }, + "단산글": [ + "88.야면석메붙임(L3 =35cm내외,장비사용)[㎡]", + " 야면석 메붙임 [산림사업표준품셈 13-5-2]", + " 야면석 찰붙임 [뒷길이35cm적용]", + " 1. 인력", + " 석 공 : 268,908 × 0.13 = 34,958", + " 보통인부 : 172,068 × 0.04 = 6,882.7", + " 인부소계 : 41,840.7", + " 2. 굴착기(타이어형)(0.6㎥+부착용집게)", + " Q = 0.25hr", + " 노무비 : 59,020 × 0.25 = 14,755", + " 재료비 : 18,515 × 0.25 = 4,628.7", + " 경 비 : 32,906 × 0.25 = 8,226.5", + " 소 계 : 27,610.2", + " 3. 합 계", + " 노무비 : 41,840.7 + 14,755 = 56,595", + " 재료비 : 4,628", + " 경 비 : 8,226", + " 계 : 69,449" + ], + "줄수": 1, + "호표들": [ + 13 + ], + "id": "N027" + }, + { + "파일": "북부청임도", + "호표": 14, + "호표이름": "큰돌메붙임", + "호표규격": "큰돌채집", + "호표단위": "㎡", + "차례": 1, + "이름": "큰돌메붙임", + "규격": "직경40cm이상~60cm미만", + "수량": 1, + "단위": "㎡", + "단가": { + "계": 68359, + "노무비": 54084, + "재료비": 5861, + "경비": 8414 + }, + "갈래": "단가산출", + "참조": { + "종류": "단산", + "번": 92 + }, + "근거": { + "출처": [ + [ + "산림품셈", + "13-7-1" + ] + ], + "직종": [ + "작업반장", + "특별인부", + "보통인부" + ], + "행": [ + 4184, + 4225 + ] + }, + "맞춤": { + "열쇠": "큰돌메붙임|직경40cm이상~60cm미만|m2", + "대상": null, + "입력값": {}, + "못읽음": [], + "상태": "후보", + "환산": null, + "들어감": false, + "후보": [ + { + "로직": "GF000229", + "이름": "큰돌붙이기 메붙이기", + "단위": "㎡", + "근거": { + "조항": [ + "산림품셈 13-7-1" + ], + "직종": [ + "작업반장", + "특별인부" + ], + "단위": true, + "점수": 1.65, + "까닭": "조항 같음 · 직종 작업반장 · 특별인부 · 단위 맞음" + }, + "점수": 1.65 + }, + { + "로직": "GF000230", + "이름": "큰돌붙이기 찰붙이기", + "단위": "㎡", + "근거": { + "조항": [ + "산림품셈 13-7-2" + ], + "직종": [ + "작업반장", + "특별인부" + ], + "단위": true, + "점수": 1.15, + "까닭": "앞 마디 같음 · 직종 작업반장 · 특별인부 · 단위 맞음" + }, + "점수": 1.15 + }, + { + "로직": "GC000262", + "이름": "메붙임(굴착기+부착용 집게)", + "단위": "㎡", + "점수": 0.817 + }, + { + "로직": "GF000224", + "이름": "돌붙임 메붙임(장비)", + "단위": "㎡", + "점수": 0.7 + }, + { + "로직": "GF000032", + "이름": "평떼 붙임", + "단위": "㎡", + "점수": 0.486 + } + ] + }, + "단산머리": { + "이름": "큰돌메붙임(직경40cm이상~60cm미만)", + "단위": "㎡" + }, + "단산글": [ + "92.큰돌메붙임(직경40cm이상~60cm미만)[㎡]", + " 큰돌메붙임 [산림사업표준품셈 13-7-1]", + " 1. 큰돌메붙이기, 뒷길이 0.60cm기준", + " 가. 노무비", + " 작업반장 : 215,907 × 0.58 ÷ 10㎡ = 12,522.6", + " 특별인부 : 226,122 × 0.58 ÷ 10㎡ = 13,115", + " 보통인부 : 172,068 × 0.83 ÷ 10㎡ = 14,281.6", + " 인부소계 : 39,919.2", + " 나. 굴착기(무한궤도)(0.8㎥+부착용집게)", + " Q = 2.4 ÷ 10㎡= 0.24", + " 노무비 : 59,020 × 0.24 = 14,164.8", + " 재료비 : 24,421 × 0.24 = 5,861", + " 경 비 : 32,805 × 0.24 = 7,873.2", + " 소 계 : 27,899", + " 다. 제잡비 비율", + " (39,919.2 + 14,164.8) × 1% = 540.8", + " 라. 소 계", + " 노무비 : 39,919.2 + 14,164.8 = 54,084", + " 재료비 : 5,861", + " 경 비 : 7,873.2 + 540.8 = 8,414", + " 계 : 68,359", + " 2. 합 계", + " 노무비 : 54,084", + " 재료비 : 5,861", + " 경 비 : 8,414", + " 계 : 68,359" + ], + "줄수": 2, + "호표들": [ + 14, + 50 + ], + "id": "N028" + }, + { + "파일": "북부청임도", + "호표": 15, + "호표이름": "큰돌찰붙임", + "호표규격": "큰돌채집,기초무", + "호표단위": "㎡", + "차례": 2, + "이름": "큰돌찰붙임", + "규격": "직경40cm이상~60cm미만", + "수량": 1, + "단위": "㎡", + "단가": { + "계": 93974, + "노무비": 72570, + "재료비": 8205, + "경비": 13199 + }, + "갈래": "단가산출", + "참조": { + "종류": "단산", + "번": 91 + }, + "근거": { + "출처": [ + [ + "산림품셈", + "13-7-2" + ] + ], + "직종": [ + "작업반장", + "특별인부", + "보통인부" + ], + "행": [ + 4140, + 4181 + ] + }, + "맞춤": { + "열쇠": "큰돌찰붙임|직경40cm이상~60cm미만|m2", + "대상": null, + "입력값": {}, + "못읽음": [], + "상태": "후보", + "환산": null, + "들어감": false, + "후보": [ + { + "로직": "GF000230", + "이름": "큰돌붙이기 찰붙이기", + "단위": "㎡", + "근거": { + "조항": [ + "산림품셈 13-7-2" + ], + "직종": [ + "작업반장", + "특별인부" + ], + "단위": true, + "점수": 1.65, + "까닭": "조항 같음 · 직종 작업반장 · 특별인부 · 단위 맞음" + }, + "점수": 1.65 + }, + { + "로직": "GF000229", + "이름": "큰돌붙이기 메붙이기", + "단위": "㎡", + "근거": { + "조항": [ + "산림품셈 13-7-1" + ], + "직종": [ + "작업반장", + "특별인부" + ], + "단위": true, + "점수": 1.15, + "까닭": "앞 마디 같음 · 직종 작업반장 · 특별인부 · 단위 맞음" + }, + "점수": 1.15 + }, + { + "로직": "GC000263", + "이름": "찰붙임(굴착기+부착용 집게)", + "단위": "㎡", + "점수": 0.817 + }, + { + "로직": "GF000225", + "이름": "돌붙임 찰붙임(장비)", + "단위": "㎡", + "점수": 0.7 + }, + { + "로직": "GF000032", + "이름": "평떼 붙임", + "단위": "㎡", + "점수": 0.486 + } + ] + }, + "단산머리": { + "이름": "큰돌찰붙임(직경40cm이상~60cm미만)", + "단위": "㎡" + }, + "단산글": [ + "91.큰돌찰붙임(직경40cm이상~60cm미만)[㎡]", + " 큰돌찰붙임 [산림사업표준품셈 13-7-2]", + " 1. 큰돌쌓기(찰쌓기)", + " 가. 노무비", + " 작업반장 : 215,907 × 0.58 ÷ 10㎡ = 12,522.6", + " 특별인부 : 226,122 × 1.01 ÷ 10㎡ = 22,838.3", + " 보통인부 : 172,068 × 1.01 ÷ 10㎡ = 17,378.8", + " 인부소계 : 52,739.7", + " 나. 굴착기(무한궤도)(0.8㎥+부착용집게)", + " Q = 3.36 ÷ 10㎡= 0.336", + " 노무비 : 59,020 × 0.336 = 19,830.7", + " 재료비 : 24,421 × 0.336 = 8,205.4", + " 경 비 : 32,805 × 0.336 = 11,022.4", + " 소 계 : 39,058.5", + " 라. 제잡비 비율", + " (52,739.7 + 19,830.7) × 3% = 2,177.1", + " 마. 소 계", + " 노무비 : 52,739.7 + 19,830.7 = 72,570.4", + " 재료비 : 8,205.4", + " 경 비 : 11,022.4 + 2,177.1 = 13,199.5", + " 계 : 93,975.3", + " 2. 합 계", + " 노무비 : 72,570", + " 재료비 : 8,205", + " 경 비 : 13,199", + " 계 : 93,974" + ], + "줄수": 4, + "호표들": [ + 15, + 48, + 49, + 51 + ], + "id": "N029" + }, + { + "파일": "북부청임도", + "호표": 18, + "호표이름": "큰돌흙막이(찰쌓기) 기초", + "호표규격": "", + "호표단위": "M", + "차례": 1, + "이름": "레미콘타설(장비사용)", + "규격": "소형구조물", + "수량": 0.4, + "단위": "㎥", + "단가": { + "계": 58626, + "노무비": 46268, + "재료비": 5578, + "경비": 6780 + }, + "갈래": "단가산출", + "참조": { + "종류": "단산", + "번": 57 + }, + "근거": { + "출처": [ + [ + "건설품셈", + "6-1-1" + ] + ], + "직종": [ + "콘크리트공", + "보통인부" + ], + "행": [ + 2598, + 2632 + ] + }, + "맞춤": { + "열쇠": "레미콘타설(장비사용)|소형구조물|m3", + "대상": null, + "입력값": {}, + "못읽음": [], + "상태": "후보", + "환산": null, + "들어감": false, + "후보": [ + { + "로직": "GC000198", + "이름": "레디믹스트콘크리트 타설", + "단위": "㎥", + "근거": { + "조항": [ + "건설품셈 공통 6-1-1" + ], + "직종": [ + "콘크리트공" + ], + "단위": true, + "점수": 1.75, + "까닭": "조항 같음 · 건설 공통 · 직종 콘크리트공 · 단위 맞음" + }, + "점수": 1.75 + }, + { + "로직": "GC000201", + "이름": "콘크리트 펌프차 타설", + "단위": "㎥", + "근거": { + "조항": [ + "건설품셈 공통 6-1-4" + ], + "직종": [ + "콘크리트공" + ], + "단위": true, + "점수": 1.25, + "까닭": "앞 마디 같음 · 건설 공통 · 직종 콘크리트공 · 단위 맞음" + }, + "점수": 1.567 + }, + { + "로직": "GC000199", + "이름": "현장비빔타설", + "단위": "㎥", + "근거": { + "조항": [ + "건설품셈 공통 6-1-2" + ], + "직종": [ + "콘크리트공" + ], + "단위": true, + "점수": 1.25, + "까닭": "앞 마디 같음 · 건설 공통 · 직종 콘크리트공 · 단위 맞음" + }, + "점수": 1.25 + }, + { + "로직": "GF000182", + "이름": "펌프카 타설(편성 · 일일 시공량)", + "단위": "㎥", + "점수": 0.45 + }, + { + "로직": "GF000166", + "이름": "콘크리트 타설(무근진동기 제외)", + "단위": "㎥", + "점수": 0.422 + } + ] + }, + "단산머리": { + "이름": "레미콘타설(장비사용)(소형구조물)", + "단위": "㎥" + }, + "단산글": [ + "57.레미콘타설(장비사용)(소형구조물)[㎥]", + " 건설공사표준품셈 6-1-1", + " 1. 레미콘 : 별도계상", + " 2. 타설비", + " A = 63인", + " 콘크리트공 : 273,540 × 3인 ÷ 63 × 2 = 26,051.4", + " 보통인부 : 172,068 × 1인 ÷ 63 × 2 = 5,462.4", + " 인부소계 : 31,513.8", + " 공구손료 및 경장비(콘크리트 진동기등)의 기계경비(노무비의 2%)", + " 31,513.8 × 2% = 630.2", + " 3. 굴착기(타이어형)(0.6㎥)", + " Q = 1 × 8hr ÷ A × 2 = 1 × 8 ÷ 63 × 2 = 0.25hr/㎥", + " 노무비 : 59,020 × 0.25 = 14,755", + " 재료비 : 19,792 × 0.25 = 4,948", + " 경 비 : 27,120 × 0.25 = 6,780", + " 소 계 : 26,483", + " 4. 합 계", + " 노무비 : 31,513.8 + 14,755 = 46,268", + " 재료비 : 630.2 + 4,948 = 5,578", + " 경 비 : 6,780", + " 계 : 58,626" + ], + "줄수": 2, + "호표들": [ + 18, + 19 + ], + "id": "N030" + }, + { + "파일": "북부청임도", + "호표": 25, + "호표이름": "측구흙막이", + "호표규격": "야면석,L=약35cm", + "호표단위": "개소", + "차례": 6, + "이름": "야면석찰쌓기", + "규격": "L3=35cm적용,장비사용", + "수량": 1.85, + "단위": "㎡", + "단가": { + "계": 67040, + "노무비": 51101, + "재료비": 5739, + "경비": 10200 + }, + "갈래": "단가산출", + "참조": { + "종류": "단산", + "번": 71 + }, + "근거": { + "출처": [ + [ + "산림품셈", + "13-4-5" + ] + ], + "직종": [ + "석공", + "보통인부" + ], + "행": [ + 3202, + 3228 + ] + }, + "맞춤": { + "열쇠": "야면석찰쌓기|l3=35cm적용,장비사용|m2", + "대상": null, + "입력값": {}, + "못읽음": [], + "상태": "후보", + "환산": null, + "들어감": false, + "후보": [ + { + "로직": "GF000222", + "이름": "돌쌓기 찰쌓기(장비)", + "단위": "㎡", + "근거": { + "조항": [ + "산림품셈 13-4-5" + ], + "직종": [ + "석공" + ], + "단위": true, + "점수": 1.65, + "까닭": "조항 같음 · 직종 석공 · 단위 맞음" + }, + "점수": 2.294 + }, + { + "로직": "GF000221", + "이름": "돌쌓기 찰쌓기(인력)", + "단위": "㎡", + "근거": { + "조항": [ + "산림품셈 13-4-4" + ], + "직종": [ + "석공" + ], + "단위": true, + "점수": 1.15, + "까닭": "앞 마디 같음 · 직종 석공 · 단위 맞음" + }, + "점수": 1.794 + }, + { + "로직": "GF000219", + "이름": "돌쌓기 메쌓기(인력)", + "단위": "㎡", + "근거": { + "조항": [ + "산림품셈 13-4-1" + ], + "직종": [ + "석공" + ], + "단위": true, + "점수": 1.15, + "까닭": "앞 마디 같음 · 직종 석공 · 단위 맞음" + }, + "점수": 1.15 + }, + { + "로직": "GF000220", + "이름": "돌쌓기 메쌓기(장비)", + "단위": "㎡", + "근거": { + "조항": [ + "산림품셈 13-4-2" + ], + "직종": [ + "석공" + ], + "단위": true, + "점수": 1.15, + "까닭": "앞 마디 같음 · 직종 석공 · 단위 맞음" + }, + "점수": 1.15 + }, + { + "로직": "GC000261", + "이름": "찰쌓기(굴착기+부착용 집게)", + "단위": "㎡", + "점수": 0.721 + } + ] + }, + "단산머리": { + "이름": "야면석찰쌓기(L3=35cm적용,장비사용)", + "단위": "㎡" + }, + "단산글": [ + "71.야면석찰쌓기(L3=35cm적용,장비사용)[㎡]", + " 산림사업표준품셈 13-4-5", + " 1. 인력", + " 석 공 : 268,908 × 0.09 = 24,201.7", + " 보통인부 : 172,068 × 0.05 = 8,603.4", + " 인부소계 :32,805.1", + " 2. 굴착기(타이어형)(0.6㎥+부착용집게)", + " Q = 0.31hr", + " 노무비 : 59,020 × 0.31 = 18,296.2", + " 재료비 : 18,515 × 0.31 = 5,739.6", + " 경 비 : 32,906 × 0.31 = 10,200.8", + " 소 계 : 34,236.6", + " 3. 합 계", + " 노무비 : 32,805.1 + 18,296.2 = 51,101", + " 재료비 : 5,739", + " 경 비 : 10,200", + " 계 : 67,040" + ], + "줄수": 2, + "호표들": [ + 25, + 47 + ], + "id": "N031" + }, + { + "파일": "북부청임도", + "호표": 26, + "호표이름": "측구보호용 잡석채움", + "호표규격": "75mm내외 잡석", + "호표단위": "M", + "차례": 1, + "이름": "면고르기", + "규격": "", + "수량": 1.24, + "단위": "㎡", + "단가": { + "계": 3348, + "노무비": 1628, + "재료비": 903, + "경비": 817 + }, + "갈래": "단가산출", + "참조": { + "종류": "단산", + "번": 43 + }, + "근거": { + "출처": [ + [ + "건설품셈", + "3-5-1" + ] + ], + "직종": [], + "행": [ + 2057, + 2085 + ] + }, + "맞춤": { + "열쇠": "면고르기||m2", + "대상": { + "로직": "GF000124", + "이름": "토사면 절토면 고르기", + "단위": "㎡" + }, + "입력값": {}, + "못읽음": [], + "상태": "같음", + "환산": { + "배": 1, + "갈래": "같음" + }, + "들어감": true + }, + "단산머리": { + "이름": "면고르기", + "단위": "㎡" + }, + "단산글": [ + "43.면고르기[㎡]", + " 절토면고르기[2025년 건설표준품셈 3-5-1 절토면 고르기]", + " 연질토 및 불순자갈 토질 기준", + " 1일 시공량 모래,사질토,점토,점질토 390㎥/일", + " 연질토,불순자갈 250㎥/일", + " 호박돌 섞인 고결토, 경질토 230㎥/일", + " 평균치 적용", + " 1. 굴착기(무한궤도)(1.0㎥)", + " Q = (390+250+230)/3 / 8 = 36.25 ㎥/hr", + " 노무비 : 59,020 ÷ 36.25 = 1,628.1", + " 재료비 : 32,735 ÷ 36.25 = 903", + " 경 비 : 29,621 ÷ 36.25 = 817.1", + " 소 계 : 3,348.2", + " 2. 합 계", + " 노무비 : 1,628", + " 재료비 : 903", + " 경 비 : 817", + " 계 : 3,348" + ], + "줄수": 4, + "호표들": [ + 26, + 58, + 64, + 67 + ], + "id": "N032" + }, + { + "파일": "북부청임도", + "호표": 26, + "호표이름": "측구보호용 잡석채움", + "호표규격": "75mm내외 잡석", + "호표단위": "M", + "차례": 2, + "이름": "대형브레이카", + "규격": "+굴착기(0.7㎥)", + "수량": 0.0133333, + "단위": "hr", + "단가": { + "계": 113501, + "노무비": 59020, + "재료비": 18515, + "경비": 35966 + }, + "갈래": "기계", + "참조": null, + "근거": null, + "맞춤": { + "열쇠": "대형브레이카|+굴착기(0.7m3)|hr", + "대상": null, + "입력값": {}, + "못읽음": [], + "상태": "후보", + "환산": null, + "들어감": false, + "후보": [ + { + "로직": "GC000271", + "이름": "굴착기(무한궤도)+대형브레이커 시간당 기계경비", + "단위": "시간", + "점수": 0.476 + } + ] + }, + "줄수": 3, + "호표들": [ + 26, + 64, + 67 + ], + "id": "N033" + }, + { + "파일": "북부청임도", + "호표": 27, + "호표이름": "물넘이포장", + "호표규격": "H=1.0m,B=4.0m", + "호표단위": "M", + "차례": 2, + "이름": "와이어메시바닥깔기", + "규격": "#6100×100", + "수량": 5.9, + "단위": "㎡", + "단가": { + "계": 6374, + "노무비": 1356, + "재료비": 5018, + "경비": null + }, + "갈래": "단가산출", + "참조": { + "종류": "단산", + "번": 60 + }, + "근거": { + "출처": [ + [ + "건설품셈", + "8-1-3" + ] + ], + "직종": [ + "특별인부" + ], + "행": [ + 2717, + 2738 + ] + }, + "맞춤": { + "열쇠": "와이어메시바닥깔기|#6100x100|m2", + "대상": { + "로직": "GC000665", + "이름": "와이어메시 바닥깔기", + "단위": "㎡" + }, + "입력값": {}, + "못읽음": [], + "상태": "같음", + "환산": { + "배": 1, + "갈래": "같음" + }, + "들어감": true + }, + "단산머리": { + "이름": "와이어메시바닥깔기(#6100×100)", + "단위": "㎡" + }, + "단산글": [ + "60.와이어메시바닥깔기(#6100×100)[㎡]", + " [건설표준품셈(8-1-3)]", + " 1. 재료비", + " 와이어메쉬(#6,100×100) 4,640 × 1.05 = 4,872", + " 2. 잡재료비(주재료비의 3%)", + " 4,872 × 3% = 146.1", + " 3. 인건비", + " 특별인부 : 226,122 × 0.006 = 1,356.7", + " 4. 합 계", + " 노무비 : 1,356", + " 재료비 : 4,872 + 146.1 = 5,018", + " 경 비 :", + " 계 : 6,374" + ], + "줄수": 1, + "호표들": [ + 27 + ], + "id": "N034" + }, + { + "파일": "북부청임도", + "호표": 28, + "호표이름": "돌망태흙막이", + "호표규격": "H:1.23m 조약돌 구입", + "호표단위": "M", + "차례": 3, + "이름": "원형돌망태 설치 및 돌채움", + "규격": "#8,45×45 cm", + "수량": 3, + "단위": "㎡", + "단가": { + "계": 43016, + "노무비": 43016, + "재료비": null, + "경비": null + }, + "갈래": "단가산출", + "참조": { + "종류": "단산", + "번": 62 + }, + "근거": { + "출처": [ + [ + "산림품셈", + "13-11-1" + ] + ], + "직종": [ + "보통인부" + ], + "행": [ + 2829, + 2852 + ] + }, + "맞춤": { + "열쇠": "원형돌망태설치및돌채움|#8,45x45cm|m2", + "대상": null, + "입력값": {}, + "못읽음": [], + "상태": "후보", + "환산": null, + "들어감": false, + "후보": [ + { + "로직": "GF000328", + "이름": "원형 돌망태", + "단위": "㎡", + "근거": { + "조항": [ + "산림품셈 13-11-1" + ], + "직종": [], + "단위": true, + "점수": 1.15, + "까닭": "조항 같음 · 단위 맞음" + }, + "점수": 1.921 + }, + { + "로직": "GF000329", + "이름": "타원형 돌망태", + "단위": "㎡", + "근거": { + "조항": [ + "산림품셈 13-11-2" + ], + "직종": [], + "단위": true, + "점수": 0.65, + "까닭": "앞 마디 같음 · 단위 맞음" + }, + "점수": 1.383 + }, + { + "로직": "GF000234", + "이름": "매트리스형 돌망태", + "단위": "㎡", + "근거": { + "조항": [ + "산림품셈 13-11-3" + ], + "직종": [], + "단위": true, + "점수": 0.65, + "까닭": "앞 마디 같음 · 단위 맞음" + }, + "점수": 1.203 + }, + { + "로직": "GF000235", + "이름": "사각형 돌망태", + "단위": "㎥", + "근거": { + "조항": [ + "산림품셈 13-11-4" + ], + "직종": [], + "단위": false, + "점수": 0.5, + "까닭": "앞 마디 같음" + }, + "점수": 0.95 + }, + { + "로직": "GC000340", + "이름": "타원형 돌망태 설치", + "단위": "㎡", + "점수": 0.856 + } + ] + }, + "단산머리": { + "이름": "원형돌망태설치및돌채움(#8,45×45cm)", + "단위": "㎡" + }, + "단산글": [ + "62.원형돌망태설치및돌채움(#8,45×45cm)[㎡]", + " [산림사업표준품셈 13-11-1]", + " 1. 재료비", + " 원형돌망태(몸통) #8 45㎝×45㎝ : - 별산 -", + " 원형돌망태(뚜껑) #8 45㎝×45㎝ : - 별산 -", + " 2. 조립 설치", + " 보통인부 : 172,068 × 0.08인 = 13,765.4", + " 3. 돌채움", + " 보통인부 : 172,068 × 0.17인 = 29,251.5", + " 4. 합 계", + " 노무비 : 13,765.4 + 29,251.5 = 43,016", + " 재료비 :", + " 경 비 :", + " 계 : 43,016" + ], + "줄수": 6, + "호표들": [ + 28, + 29, + 30, + 31, + 32, + 33 + ], + "id": "N035" + }, + { + "파일": "북부청임도", + "호표": 31, + "호표이름": "돌망태흙막이", + "호표규격": "H:0.84m 조약돌 채집", + "호표단위": "M", + "차례": 0, + "이름": "조약돌채집및운반", + "규격": "굴삭기0.6㎥(90%),4.5톤,L=500m", + "수량": 0.58, + "단위": "㎥", + "단가": { + "계": 17726, + "노무비": 13159, + "재료비": 2108, + "경비": 2459 + }, + "갈래": "단가산출", + "참조": { + "종류": "단산", + "번": 61 + }, + "근거": { + "출처": [ + [ + "건설품셈", + "1-2-3" + ] + ], + "직종": [ + "보통인부" + ], + "행": [ + 2741, + 2826 + ] + }, + "맞춤": { + "열쇠": "조약돌채집및운반|굴삭기0.6m3(90%),4.5톤,l=500m|m3", + "대상": null, + "입력값": {}, + "못읽음": [], + "상태": "후보", + "환산": null, + "들어감": false, + "후보": [ + { + "로직": "GC000363", + "이름": "자갈채집 및 운반", + "단위": "㎥", + "점수": 0.765 + }, + { + "로직": "GF000212", + "이름": "막돌 채집", + "단위": "㎡ · ㎥", + "점수": 0.6 + }, + { + "로직": "GF000211", + "이름": "모래·자갈·약돌 채집(인력)", + "단위": "㎥", + "점수": 0.575 + }, + { + "로직": "GC000418", + "이름": "사석 적재 및 운반", + "단위": "㎥", + "점수": 0.458 + }, + { + "로직": "GF000213", + "이름": "큰돌 채집(기계)", + "단위": "개", + "점수": 0.45 + } + ] + }, + "단산머리": { + "이름": "조약돌채집및운반(굴삭기0.6㎥(90%),4.5톤,L=500m)", + "단위": "㎥" + }, + "단산글": [ + "61.조약돌채집및운반(굴삭기0.6㎥(90%),4.5톤,L=500m)[㎥]", + " 조약돌 채집 및 운반(㎥) [건설표준품셈 준용]", + " 작업조건 : 계곡에 산재한 조약돌(Φ100~300mm) 채집", + " 1. 채 집", + " 가. 굴착기(타이어형)(0.6㎥)", + " q = 0.6㎥ K = 0.55 f = 1", + " E = 0.35 Cm = 18sec(90˚)", + " 3,600 × q × K × f × E 3,600 × 0.6 × 0.55 × 1 × 0.35", + " Q = ────────────── = ───────────────── = 23.1 ㎥/hr", + " Cm 18", + " 노무비 : 59,020 ÷ 23.1 × 90% = 2,299.4", + " 재료비 : 19,792 ÷ 23.1 × 90% = 771.1", + " 경 비 : 27,120 ÷ 23.1 × 90% = 1,056.6", + " 소 계 : 4,127.1", + " 나. 인 력(10%)", + " 보통인부 : 172,068 × 0.3인/㎥ × 10% = 5,162", + " 다. 소 계", + " 노무비 : 2,299.4 + 5,162 = 7,461.4", + " 재료비 : 771.1", + " 경 비 : 1,056.6", + " 계 : 9,289.1", + " 2. 상 차 : 굴착기(타이어형)(0.6㎥)", + " qs = 0.6㎥ K = 0.7 f = 1", + " Es = 0.45 Cm = 20sec(135˚)", + " 3,600 × qs × K × f × Es 3,600 × 0.6 × 0.7 × 1 × 0.45", + " Q = ────────────── = ───────────────── = 34.02 ㎥/hr", + " Cm 20", + " 노무비 : 59,020 ÷ 34.02 = 1,734.8", + " 재료비 : 19,792 ÷ 34.02 = 581.7", + " 경 비 : 27,120 ÷ 34.02 = 797.1", + " 소 계 : 3,113.6", + " 3. 운 반 : 덤프트럭(4.5Ton)", + " L = 0.5km V1 = 10km/hr V2 = 15km/hr", + " T = 4.5ton rt = 1.75ton/㎥ E = 0.9", + " q = T / rt = 4.5 / 1.75 = 2.57 ㎥", + " 건설표줌품셈(1-2-3) 자갈(습기)의 평균값 1.75 ton/㎥ 적용", + " n = q / (qs × K) = 2.57 / (0.6 × 0.7) = 6.12 회", + " t1 = 20 × n / (60 × Es) = 20 × 6.12 / (60 × 0.45) = 4.53분", + " L L 0.5 0.5", + " t2 = (── + ──) × 60 = (── + ──) × 60 = 5분(왕복)", + " V1 V2 10 15", + " t3 = 1.1분 t4 = 0.42분", + " Cm = t1 + t2 + t3 + t4 = 4.53 + 5 + 1.1 + 0.42 = 11.05 분", + " 60 × q × f × E 60 × 2.57 × 1 × 0.9", + " Q = ─────────── = ──────────── = 12.56 ㎥/hr", + " Cm 11.05", + " 노무비 : 49,778 ÷ 12.56 = 3,963.2", + " 재료비 : 9,494 ÷ 12.56 = 755.8", + " 경 비 : 7,613 ÷ 12.56 = 606.1", + " 소 계 : 5,325.1", + " 4. 합 계", + " 노무비 : 7,461.4 + 1,734.8 + 3,963.2 = 13,159", + " 재료비 : 771.1 + 581.7 + 755.8 = 2,108", + " 경 비 : 1,056.6 + 797.1 + 606.1 = 2,459", + " 계 : 17,726" + ], + "줄수": 5, + "호표들": [ + 31, + 32, + 33, + 35, + 37 + ], + "id": "N036" + }, + { + "파일": "북부청임도", + "호표": 34, + "호표이름": "사각돌망태옹벽", + "호표규격": "H:2.0m,조약돌 구입", + "호표단위": "M", + "차례": 3, + "이름": "돌망태형 옹벽설치", + "규격": "사각돌망태", + "수량": 2.5, + "단위": "㎡", + "단가": { + "계": 43988, + "노무비": 39543, + "재료비": 1966, + "경비": 2479 + }, + "갈래": "단가산출", + "참조": { + "종류": "단산", + "번": 63 + }, + "근거": { + "출처": [ + [ + "산림품셈", + "13-114" + ] + ], + "직종": [ + "석공", + "특별인부", + "보통인부" + ], + "행": [ + 2855, + 2896 + ] + }, + "맞춤": { + "열쇠": "돌망태형옹벽설치|사각돌망태|m2", + "대상": { + "로직": "GC000342", + "이름": "돌망태형옹벽 설치", + "단위": "㎥" + }, + "입력값": {}, + "못읽음": [], + "상태": "같음", + "환산": { + "배": null, + "갈래": "입력" + }, + "들어감": false + }, + "단산머리": { + "이름": "돌망태형 옹벽설치(사각돌망태)", + "단위": "㎡" + }, + "단산글": [ + "63.돌망태형 옹벽설치(사각돌망태)[㎡]", + " [산림사업표준품셈 13-114]", + " 1. 재료비 : 사각돌망태(Φ2.7mm 아연도금1.0×1.0×1.0m)", + " 철망태 : - 별도계상 -", + " 2. 설치비", + " 가. 굴착기(무한궤도)(0.7㎥)", + " Q = 0.101", + " 노무비 : 59,020 × 0.101 = 5,961", + " 재료비 : 19,473 × 0.101 = 1,966.7", + " 경 비 : 24,554 × 0.101 = 2,479.9", + " 소 계 : 10,407.6", + " 나. 인 력", + " 석 공 : 268,908 × 0.072인 = 19,361.3", + " 특별인부 : 226,122 × 0.053인 = 11,984.4", + " 보통인부 : 172,068 × 0.013인 = 2,236.8", + " 인부소계 : 33,582.5", + " 다. 소 계", + " 노무비 : 5,961 + 33,582.5 = 39,543.5", + " 재료비 : 1,966.7", + " 경 비 : 2,479.9", + " 계 : 43,990.1", + " 3. 합 계", + " 노무비 : 39,543", + " 재료비 : 1,966", + " 경 비 : 2,479", + " 계 : 43,988" + ], + "줄수": 4, + "호표들": [ + 34, + 35, + 36, + 37 + ], + "id": "N037" + }, + { + "파일": "북부청임도", + "호표": 38, + "호표이름": "콘크리트포장(간선임도)", + "호표규격": "T=20cm,B=3.5m,종단15%미만", + "호표단위": "M", + "차례": 1, + "이름": "콘크리트포장(인력)", + "규격": "T=20cm, 종단15%미만", + "수량": 0.7, + "단위": "㎥", + "단가": { + "계": 14804, + "노무비": 14373, + "재료비": null, + "경비": 431 + }, + "갈래": "단가산출", + "참조": { + "종류": "단산", + "번": 65 + }, + "근거": { + "출처": [ + [ + "건설품셈", + "1-6-2" + ] + ], + "직종": [ + "포장공", + "보통인부" + ], + "행": [ + 2931, + 2958 + ] + }, + "맞춤": { + "열쇠": "콘크리트포장(인력)|t=20cm,종단15%미만|m3", + "대상": null, + "입력값": {}, + "못읽음": [], + "상태": "후보", + "환산": null, + "들어감": false, + "후보": [ + { + "로직": "GC000297", + "이름": "콘크리트 표층 인력포설", + "단위": "㎥", + "근거": { + "조항": [ + "건설품셈 토목 1-6-2" + ], + "직종": [ + "포장공" + ], + "단위": true, + "점수": 1.75, + "까닭": "조항 같음 · 건설 토목 · 직종 포장공 · 단위 맞음" + }, + "점수": 2.329 + }, + { + "로직": "GF000162", + "이름": "콘크리트 포장(인력시공)", + "단위": "㎥", + "점수": 2.2 + }, + { + "로직": "GC000296", + "이름": "린 콘크리트 기층 포설", + "단위": "㎥", + "근거": { + "조항": [ + "건설품셈 토목 1-6-1" + ], + "직종": [ + "포장공" + ], + "단위": true, + "점수": 1.25, + "까닭": "앞 마디 같음 · 건설 토목 · 직종 포장공 · 단위 맞음" + }, + "점수": 1.862 + }, + { + "로직": "GC000298", + "이름": "콘크리트 표층 기계포설(소형장비)", + "단위": "㎥", + "근거": { + "조항": [ + "건설품셈 토목 1-6-3" + ], + "직종": [ + "포장공" + ], + "단위": true, + "점수": 1.25, + "까닭": "앞 마디 같음 · 건설 토목 · 직종 포장공 · 단위 맞음" + }, + "점수": 1.829 + }, + { + "로직": "GC000299", + "이름": "콘크리트 표층 기계포설(대형장비)", + "단위": "㎥", + "근거": { + "조항": [ + "건설품셈 토목 1-6-4" + ], + "직종": [ + "포장공" + ], + "단위": true, + "점수": 1.25, + "까닭": "앞 마디 같음 · 건설 토목 · 직종 포장공 · 단위 맞음" + }, + "점수": 1.829 + } + ] + }, + "단산머리": { + "이름": "콘크리트포장(인력)(T=20cm, 종단15%미만)", + "단위": "㎥" + }, + "단산글": [ + "65.콘크리트포장(인력)(T=20cm, 종단15%미만)[㎥]", + " [건설공사표준품셈 1-6-2]", + " ※ 콘크리트 포장두께 : T = 0.2m", + " 1일 시공량 : Qd = 100㎥/일", + " 작업면적 : Q = 1 / T = 1 / 0.2 = 5㎡/㎥", + " 1. 배치인원", + " 포 장 공 : 273,308 × 4.0인 ÷ Qd = 273,308 × 4 ÷ 100 = 10,932.3", + " 보통인부 : 172,068 × 2.0인 ÷ Qd = 172,068 × 2 ÷ 100 = 3,441.3", + " 인부소계 : 14,373.6", + " 2. 공구손료(노무비의 3%)", + " 14,373.6 × 3% = 431.2", + " 3. 합 계", + " 노무비 : 14,373", + " 재료비 :", + " 경 비 : 431", + " 계 : 14,804" + ], + "줄수": 4, + "호표들": [ + 38, + 40, + 42, + 43 + ], + "id": "N038" + }, + { + "파일": "북부청임도", + "호표": 38, + "호표이름": "콘크리트포장(간선임도)", + "호표규격": "T=20cm,B=3.5m,종단15%미만", + "호표단위": "M", + "차례": 4, + "이름": "비탈면 보호공", + "규격": "평떼", + "수량": 0.18, + "단위": "㎡", + "단가": { + "계": 18255, + "노무비": 11466, + "재료비": 6789, + "경비": null + }, + "갈래": "단가산출", + "참조": { + "종류": "단산", + "번": 64 + }, + "근거": { + "출처": [ + [ + "건설품셈", + "4-1-1" + ], + [ + "산림품셈", + "5-23-3" + ] + ], + "직종": [ + "조경공", + "보통인부" + ], + "행": [ + 2899, + 2928 + ] + }, + "맞춤": { + "열쇠": "비탈면보호공|평떼|m2", + "대상": null, + "입력값": {}, + "못읽음": [], + "상태": "후보", + "환산": null, + "들어감": false, + "후보": [ + { + "로직": "GC000143", + "이름": "잔디붙임", + "단위": "㎡", + "근거": { + "조항": [ + "건설품셈 공통 4-1-1" + ], + "직종": [ + "조경공" + ], + "단위": true, + "점수": 1.75, + "까닭": "조항 같음 · 건설 공통 · 직종 조경공 · 단위 맞음" + }, + "점수": 1.75 + }, + { + "로직": "GC000144", + "이름": "판형잔디붙임", + "단위": "㎡", + "근거": { + "조항": [ + "건설품셈 공통 4-1-2" + ], + "직종": [ + "조경공" + ], + "단위": true, + "점수": 1.25, + "까닭": "앞 마디 같음 · 건설 공통 · 직종 조경공 · 단위 맞음" + }, + "점수": 1.25 + }, + { + "로직": "GC000145", + "이름": "초류종자 살포(기계살포)", + "단위": "㎡", + "근거": { + "조항": [ + "건설품셈 공통 4-1-3" + ], + "직종": [ + "조경공" + ], + "단위": true, + "점수": 1.25, + "까닭": "앞 마디 같음 · 건설 공통 · 직종 조경공 · 단위 맞음" + }, + "점수": 1.25 + }, + { + "로직": "GC000147", + "이름": "거적덮기", + "단위": "㎡", + "근거": { + "조항": [ + "건설품셈 공통 4-1-5" + ], + "직종": [ + "조경공" + ], + "단위": true, + "점수": 1.25, + "까닭": "앞 마디 같음 · 건설 공통 · 직종 조경공 · 단위 맞음" + }, + "점수": 1.25 + }, + { + "로직": "GC000066", + "이름": "비탈면 보양", + "단위": "㎡", + "점수": 0.817 + } + ] + }, + "단산머리": { + "이름": "비탈면보호공(평떼)", + "단위": "㎡" + }, + "단산글": [ + "64.비탈면보호공(평떼)[㎡]", + " 비탈면보호공-평떼 [건설표준품셈(4-1-1)]", + " 1일 시공량 Q = 150 ㎡/일", + " 1. 떼구입비(0.18×0.18×0.01)", + " 잔디(0.18×0.18×0.01) 200 × 30.86매/㎡ × 1.1 = 6,789.2", + " 2. 떼붙임", + " 조 경 공 : 235,204 ×1/150 = 1,568", + " 보통인부 : 172,068 × 4/150 = 4,588.4", + " 인부소계 : 6,156.4", + " 3. 떼꽂이 제작 및 설치[1일 1인 1000개 제작]", + " [산림사업표준품셈 5-23-3]", + " 보통인부 : 172,068 × 1인 × 30.86 / 1000 = 5,310", + " 4. 합 계", + " 노무비 : 6,156.4 + 5,310 = 11,466", + " 재료비 : 6,789", + " 경 비 :", + " 계 : 18,255" + ], + "줄수": 6, + "호표들": [ + 38, + 39, + 40, + 41, + 43, + 44 + ], + "id": "N039" + }, + { + "파일": "북부청임도", + "호표": 39, + "호표이름": "콘크리트포장(간선임도)", + "호표규격": "T=20cm,B=3.5m,종단15%이상", + "호표단위": "M", + "차례": 1, + "이름": "콘크리트포장(인력)", + "규격": "T=20cm,종단15%이상", + "수량": 0.7, + "단위": "㎥", + "단가": { + "계": 17765, + "노무비": 17248, + "재료비": null, + "경비": 517 + }, + "갈래": "단가산출", + "참조": { + "종류": "단산", + "번": 66 + }, + "근거": { + "출처": [ + [ + "건설품셈", + "1-6-2" + ] + ], + "직종": [ + "포장공", + "보통인부" + ], + "행": [ + 2961, + 2988 + ] + }, + "맞춤": { + "열쇠": "콘크리트포장(인력)|t=20cm,종단15%이상|m3", + "대상": null, + "입력값": {}, + "못읽음": [], + "상태": "후보", + "환산": null, + "들어감": false, + "후보": [ + { + "로직": "GC000297", + "이름": "콘크리트 표층 인력포설", + "단위": "㎥", + "근거": { + "조항": [ + "건설품셈 토목 1-6-2" + ], + "직종": [ + "포장공" + ], + "단위": true, + "점수": 1.75, + "까닭": "조항 같음 · 건설 토목 · 직종 포장공 · 단위 맞음" + }, + "점수": 2.329 + }, + { + "로직": "GF000162", + "이름": "콘크리트 포장(인력시공)", + "단위": "㎥", + "점수": 2.2 + }, + { + "로직": "GC000296", + "이름": "린 콘크리트 기층 포설", + "단위": "㎥", + "근거": { + "조항": [ + "건설품셈 토목 1-6-1" + ], + "직종": [ + "포장공" + ], + "단위": true, + "점수": 1.25, + "까닭": "앞 마디 같음 · 건설 토목 · 직종 포장공 · 단위 맞음" + }, + "점수": 1.862 + }, + { + "로직": "GC000298", + "이름": "콘크리트 표층 기계포설(소형장비)", + "단위": "㎥", + "근거": { + "조항": [ + "건설품셈 토목 1-6-3" + ], + "직종": [ + "포장공" + ], + "단위": true, + "점수": 1.25, + "까닭": "앞 마디 같음 · 건설 토목 · 직종 포장공 · 단위 맞음" + }, + "점수": 1.829 + }, + { + "로직": "GC000299", + "이름": "콘크리트 표층 기계포설(대형장비)", + "단위": "㎥", + "근거": { + "조항": [ + "건설품셈 토목 1-6-4" + ], + "직종": [ + "포장공" + ], + "단위": true, + "점수": 1.25, + "까닭": "앞 마디 같음 · 건설 토목 · 직종 포장공 · 단위 맞음" + }, + "점수": 1.829 + } + ] + }, + "단산머리": { + "이름": "콘크리트포장(인력)(T=20cm,종단15%이상)", + "단위": "㎥" + }, + "단산글": [ + "66.콘크리트포장(인력)(T=20cm,종단15%이상)[㎥]", + " 콘크리트포장(인력) [건설공사표준품셈1-6-2]", + " ※ 콘크리트 포장두께 : T = 0.2m", + " 1일 시공량 : Qd = 100㎥/일", + " 작업면적 : Q = 1 / T = 1 / 0.2 = 5㎡/㎥", + " 1. 배치인원", + " 포 장 공 : 273,308 × 4.0인 ÷ Qd × 1.2 = 273,308 × 4 ÷ 100 × 1.2 = 13,118.7", + " 보통인부 : 172,068 × 2.0인 ÷ Qd × 1.2 = 172,068 × 2 ÷ 100 × 1.2 = 4,129.6", + " 인부소계 : 17,248.3", + " 2. 공구손료(노무비의 3%)", + " 17,248.3 × 3% = 517.4", + " 3. 합 계", + " 노무비 : 17,248", + " 재료비 :", + " 경 비 : 517", + " 계 : 17,765" + ], + "줄수": 3, + "호표들": [ + 39, + 41, + 44 + ], + "id": "N040" + }, + { + "파일": "북부청임도", + "호표": 45, + "호표이름": "고화매트수로", + "호표규격": "B=2.3m,H=0.4m이상", + "호표단위": "㎡", + "차례": 1, + "이름": "고화매트 설치", + "규격": "", + "수량": 2.3, + "단위": "㎡", + "단가": { + "계": 27360, + "노무비": 24208, + "재료비": 1506, + "경비": 1646 + }, + "갈래": "단가산출", + "참조": { + "종류": "단산", + "번": 70 + }, + "근거": { + "출처": [ + [ + "산림품셈", + "9-19-1" + ] + ], + "직종": [ + "보통인부", + "작업반장", + "특별인부" + ], + "행": [ + 3134, + 3199 + ] + }, + "맞춤": { + "열쇠": "고화매트설치||m2", + "대상": null, + "입력값": {}, + "못읽음": [], + "상태": "후보", + "환산": null, + "들어감": false, + "후보": [ + { + "로직": "GC000343", + "이름": "식생매트 설치", + "단위": "㎡", + "점수": 0.75 + }, + { + "로직": "GF000241", + "이름": "식생매트 설치", + "단위": "㎥", + "점수": 0.65 + }, + { + "로직": "GC000604", + "이름": "카페트 설치", + "단위": "㎡", + "점수": 0.594 + }, + { + "로직": "GC000744", + "이름": "PVC덕트 설치", + "단위": "㎡", + "점수": 0.514 + }, + { + "로직": "GC000916", + "이름": "덕트 설치(공정별)", + "단위": "식", + "점수": 0.5 + } + ] + }, + "단산머리": { + "이름": "고화매트설치", + "단위": "㎡" + }, + "단산글": [ + "70.고화매트설치[㎡]", + " 1. 토사면 고르기[산림사업표준품셈 9-19-1 토사면 고르기)", + " 가. 굴착기(무한궤도)(0.7㎥)", + " Q = (0.15+0.21+0.24)/3 = 0.2 (토사토질 3개소 평균)", + " 노무비 : 59,020 × 0.2 ÷ 10 = 1,180.4", + " 재료비 : 19,473 × 0.2 ÷ 10 = 389.4", + " 경 비 : 24,554 × 0.2 ÷ 10 = 491", + " 소 계 : 2,060.8", + " 나. 인건비", + " R = (0.05+0.09+0.1)/3 = 0.08 (토사토질 3개소 평균)", + " 보통인부 : 172,068 × R ÷ 10 = 172,068 × 0.08 ÷ 10 = 1,376.5", + " 다. 소 계", + " 노무비 : 1,180.4 + 1,376.5 = 2,556.9", + " 재료비 : 389.4", + " 경 비 : 491", + " 계 : 3,437.3", + " 2. 고화매트120 설치[2026년 대한건설협회 건설공사표준품셈 p.266]", + " 작업반장 : 215,907 × 0.01 = 2,159", + " 특별인부 : 226,122 × 0.03 = 6,783.6", + " 보통인부 : 172,068 × 0.04 = 6,882.7", + " 인부소계 : 15,825.3", + " 3. 트럭탑재형크레인(5Ton)", + " Q = 8hr", + " 노무비 : 49,778 × 8 ÷ 100 = 3,982.2", + " 재료비 : 8,421 × 8 ÷ 100 = 673.6", + " 경 비 : 11,117 × 8 ÷ 100 = 889.3", + " 소 계 : 5,545.1", + " 4. 표면살수", + " 보통인부 : 172,068 × 0.003 = 516.2", + " 5. 물탱크(5500ℓ)", + " ㅇ 1일 시공량 : Qd = 300㎡/일", + " ㅇ 1시간 작업량 : Q = Qd ÷ 8hr = 300 ÷ 8 = 37.5 ㎡/hr", + " 노무비 : 49,778 ÷ 37.5 = 1,327.4", + " 재료비 : 16,635 ÷ 37.5 = 443.6", + " 경 비 : 9,986 ÷ 37.5 = 266.2", + " 소 계 : 2,037.2", + " 6. 합 계", + " 노무비 : 2,556.9 + 15,825.3 + 3,982.2 + 516.2 + 1,327.4 = 24,208", + " 재료비 : 389.4 + 673.6 + 443.6 = 1,506", + " 경 비 : 491 + 889.3 + 266.2 = 1,646", + " 계 : 27,360" + ], + "줄수": 1, + "호표들": [ + 45 + ], + "id": "N041" + }, + { + "파일": "북부청임도", + "호표": 48, + "호표이름": "돌수로(제형)", + "호표규격": "B=1.5m,H=1.2m,큰돌", + "호표단위": "M", + "차례": 2, + "이름": "큰돌쌓기(찰쌓기)", + "규격": "직경40cm이상~60cm미만", + "수량": 2.5, + "단위": "㎡", + "단가": { + "계": 128422, + "노무비": 100954, + "재료비": 11722, + "경비": 15746 + }, + "갈래": "단가산출", + "참조": { + "종류": "단산", + "번": 90 + }, + "근거": { + "출처": [ + [ + "산림품셈", + "13-6-2" + ] + ], + "직종": [ + "작업반장", + "특별인부", + "보통인부" + ], + "행": [ + 4096, + 4137 + ] + }, + "맞춤": { + "열쇠": "큰돌쌓기(찰쌓기)|직경40cm이상~60cm미만|m2", + "대상": null, + "입력값": {}, + "못읽음": [], + "상태": "후보", + "환산": null, + "들어감": false, + "후보": [ + { + "로직": "GF000227", + "이름": "큰돌쌓기 찰쌓기", + "단위": "㎡", + "근거": { + "조항": [ + "산림품셈 13-6-2" + ], + "직종": [ + "작업반장", + "특별인부" + ], + "단위": true, + "점수": 1.65, + "까닭": "조항 같음 · 직종 작업반장 · 특별인부 · 단위 맞음" + }, + "점수": 2.6 + }, + { + "로직": "GF000226", + "이름": "큰돌쌓기 메쌓기", + "단위": "㎡", + "근거": { + "조항": [ + "산림품셈 13-6-1" + ], + "직종": [ + "작업반장", + "특별인부" + ], + "단위": true, + "점수": 1.15, + "까닭": "앞 마디 같음 · 직종 작업반장 · 특별인부 · 단위 맞음" + }, + "점수": 2.1 + }, + { + "로직": "GF000228", + "이름": "친환경 큰돌쌓기", + "단위": "㎡", + "근거": { + "조항": [ + "산림품셈 13-6-3" + ], + "직종": [ + "작업반장", + "특별인부" + ], + "단위": true, + "점수": 1.15, + "까닭": "앞 마디 같음 · 직종 작업반장 · 특별인부 · 단위 맞음" + }, + "점수": 2.017 + }, + { + "로직": "GC000575", + "이름": "벽돌 쌓기(시멘트벽돌)", + "단위": "㎡", + "점수": 0.817 + }, + { + "로직": "GF000219", + "이름": "돌쌓기 메쌓기(인력)", + "단위": "㎡", + "점수": 0.771 + } + ] + }, + "단산머리": { + "이름": "큰돌쌓기(찰쌓기)(직경40cm이상~60cm미만)", + "단위": "㎡" + }, + "단산글": [ + "90.큰돌쌓기(찰쌓기)(직경40cm이상~60cm미만)[㎡]", + " 큰돌쌓기(찰쌓기) [산림사업표준품셈 13-6-2]", + " 1. 큰돌쌓기(찰쌓기)", + " 가. 노무비", + " 작업반장 : 215,907 × 0.83 ÷ 10㎡ = 17,920.2", + " 특별인부 : 226,122 × 1.30 ÷ 10㎡ = 29,395.8", + " 보통인부 : 172,068 × 1.30 ÷ 10㎡ = 22,368.8", + " 인부소계 : 69,684.8", + " 나. 굴착기(무한궤도)(0.8㎥+부착용집게)", + " Q = 4.8 ÷ 10㎡ = 0.48", + " 노무비 : 59,020 × 0.48 = 28,329.6", + " 재료비 : 24,421 × 0.48 = 11,722", + " 경 비 : 32,805 × 0.48 = 15,746.4", + " 소 계 : 55,798", + " 라. 제잡비 비율", + " (69,684.8 + 28,329.6) × 3% = 2,940.4", + " 마. 소 계", + " 노무비 : 69,684.8 + 28,329.6 + 2,940.4 = 100,954.8", + " 재료비 : 11,722", + " 경 비 : 15,746.4", + " 계 : 128,423.2", + " 2. 합 계", + " 노무비 : 100,954", + " 재료비 : 11,722", + " 경 비 : 15,746", + " 계 : 128,422" + ], + "줄수": 1, + "호표들": [ + 48 + ], + "id": "N042" + }, + { + "파일": "북부청임도", + "호표": 52, + "호표이름": "수로BOX 구체", + "호표규격": "1.5m×1.5m×1련", + "호표단위": "M", + "차례": 2, + "이름": "레미콘타설(장비사용)", + "규격": "철근구조물", + "수량": 1.57, + "단위": "㎥", + "단가": { + "계": 34299, + "노무비": 26902, + "재료비": 3329, + "경비": 4068 + }, + "갈래": "단가산출", + "참조": { + "종류": "단산", + "번": 23 + }, + "근거": { + "출처": [ + [ + "건설품셈", + "6-1-1" + ] + ], + "직종": [ + "콘크리트공", + "보통인부" + ], + "행": [ + 1136, + 1170 + ] + }, + "맞춤": { + "열쇠": "레미콘타설(장비사용)|철근구조물|m3", + "대상": null, + "입력값": {}, + "못읽음": [], + "상태": "후보", + "환산": null, + "들어감": false, + "후보": [ + { + "로직": "GC000198", + "이름": "레디믹스트콘크리트 타설", + "단위": "㎥", + "근거": { + "조항": [ + "건설품셈 공통 6-1-1" + ], + "직종": [ + "콘크리트공" + ], + "단위": true, + "점수": 1.75, + "까닭": "조항 같음 · 건설 공통 · 직종 콘크리트공 · 단위 맞음" + }, + "점수": 1.75 + }, + { + "로직": "GC000201", + "이름": "콘크리트 펌프차 타설", + "단위": "㎥", + "근거": { + "조항": [ + "건설품셈 공통 6-1-4" + ], + "직종": [ + "콘크리트공" + ], + "단위": true, + "점수": 1.25, + "까닭": "앞 마디 같음 · 건설 공통 · 직종 콘크리트공 · 단위 맞음" + }, + "점수": 1.567 + }, + { + "로직": "GC000199", + "이름": "현장비빔타설", + "단위": "㎥", + "근거": { + "조항": [ + "건설품셈 공통 6-1-2" + ], + "직종": [ + "콘크리트공" + ], + "단위": true, + "점수": 1.25, + "까닭": "앞 마디 같음 · 건설 공통 · 직종 콘크리트공 · 단위 맞음" + }, + "점수": 1.25 + }, + { + "로직": "GF000182", + "이름": "펌프카 타설(편성 · 일일 시공량)", + "단위": "㎥", + "점수": 0.45 + }, + { + "로직": "GF000166", + "이름": "콘크리트 타설(무근진동기 제외)", + "단위": "㎥", + "점수": 0.422 + } + ] + }, + "단산머리": { + "이름": "레미콘타설(장비사용) (철근구조물)", + "단위": "㎥" + }, + "단산글": [ + "23.레미콘타설(장비사용) (철근구조물) [㎥]", + " 건설표준품셈, 6-1-1 콘크리트 타설", + " 1. 레미콘 : 별도계상", + " 2. 타설비", + " A = 55㎥/일", + " 콘크리트공 : 273,540 × 3인 ÷ 55 = 14,920.3", + " 보통인부 : 172,068 × 1인 ÷ 55 = 3,129", + " 인부소계 : 18,049.3", + " 공구손료 및 경장비(콘크리트 진동기등)의 기계경비(노무비의 2%)", + " 18,049.3 × 2% = 360.9", + " 3. 굴착기(타이어형)(0.6㎥)", + " Q = 8hr ÷ A = 8 ÷ 55 = 0.15hr/㎥", + " 노무비 : 59,020 × 0.15 = 8,853", + " 재료비 : 19,792 × 0.15 = 2,968.8", + " 경 비 : 27,120 × 0.15 = 4,068", + " 소 계 : 15,889.8", + " 4. 합 계", + " 노무비 : 18,049.3 + 8,853 = 26,902", + " 재료비 : 360.9 + 2,968.8 = 3,329", + " 경 비 : 4,068", + " 계 : 34,299" + ], + "줄수": 7, + "호표들": [ + 52, + 53, + 54, + 56, + 57, + 59, + 60 + ], + "id": "N043" + }, + { + "파일": "북부청임도", + "호표": 52, + "호표이름": "수로BOX 구체", + "호표규격": "1.5m×1.5m×1련", + "호표단위": "M", + "차례": 5, + "이름": "강관동바리", + "규격": "암거용-1개월사용", + "수량": 1.8, + "단위": "공/㎥", + "단가": { + "계": 20379, + "노무비": 19988, + "재료비": 391, + "경비": null + }, + "갈래": "단가산출", + "참조": { + "종류": "단산", + "번": 73 + }, + "근거": { + "출처": [ + [ + "건설품셈", + "2-2-4" + ], + [ + "건설품셈", + "2-6-1" + ] + ], + "직종": [ + "형틀목공", + "보통인부" + ], + "행": [ + 3293, + 3322 + ] + }, + "맞춤": { + "열쇠": "강관동바리|암거용-1개월사용|공/m3", + "대상": { + "로직": "GF000189", + "이름": "강관동바리", + "단위": "㎥" + }, + "입력값": {}, + "못읽음": [], + "상태": "같음", + "환산": { + "배": null, + "갈래": "입력" + }, + "들어감": false + }, + "단산머리": { + "이름": "강관동바리(암거용-1개월사용)", + "단위": "공/㎥" + }, + "단산글": [ + "73.강관동바리(암거용-1개월사용)[공/㎥]", + " 강간동바리 [건설공사표준품셈 2-2-4 구조물동바리 손율]", + " [건설공사표준품셈 2-6-1 강관동바리 설치및 해체]", + " ※ 암거구조물(1개월사용 : 재료비의 4%, 인건비의 100%)", + " 1. 재료비", + " 강관써포트(V42600-4000mm14.3kg) 24,500 × 0.38 × 4% = 372.4", + " 잡재료비(재료비의 5%)", + " 372.4 × 5% = 18.6", + " 2. 노무비", + " 형틀목공 : 275,790 × 3인 ÷ 50공㎥ = 16,547.4", + " 보통인부 : 172,068 × 1인 ÷ 50공㎥ = 3,441.3", + " 인부소계 : 19,988.7", + " 3. 합 계", + " 노무비 : 19,988", + " 재료비 : 372.4 + 18.6 = 391", + " 경 비 :", + " 계 : 20,379" + ], + "줄수": 7, + "호표들": [ + 52, + 53, + 54, + 56, + 57, + 59, + 60 + ], + "id": "N044" + }, + { + "파일": "북부청임도", + "호표": 52, + "호표이름": "수로BOX 구체", + "호표규격": "1.5m×1.5m×1련", + "호표단위": "M", + "차례": 7, + "이름": "철근가공조립(현장)", + "규격": "Type-ll", + "수량": 0.197, + "단위": "Ton", + "단가": { + "계": 940570, + "노무비": 891568, + "재료비": 49002, + "경비": null + }, + "갈래": "단가산출", + "참조": { + "종류": "단산", + "번": 72 + }, + "근거": { + "출처": [ + [ + "건설품셈", + "6-2" + ] + ], + "직종": [ + "철근공", + "보통인부" + ], + "행": [ + 3231, + 3290 + ] + }, + "맞춤": { + "열쇠": "철근가공조립(현장)|type-ll|톤", + "대상": null, + "입력값": {}, + "못읽음": [], + "상태": "후보", + "환산": null, + "들어감": false, + "후보": [ + { + "로직": "GF000187", + "이름": "철근가공조립(복잡)", + "단위": "ton", + "점수": 2.2 + }, + { + "로직": "GF000207", + "이름": "암거이음받침 철근가공조립(보통)", + "단위": "ton", + "점수": 0.825 + }, + { + "로직": "GF000159", + "이름": "철근 현장가공 및 조립", + "단위": "ton", + "점수": 0.662 + }, + { + "로직": "GC000206", + "이름": "철근 현장가공", + "단위": "ton", + "점수": 0.55 + }, + { + "로직": "GC000207", + "이름": "철근 현장조립", + "단위": "ton", + "점수": 0.55 + } + ] + }, + "단산머리": { + "이름": "철근가공조립(현장)(Type-ll)", + "단위": "Ton" + }, + "단산글": [ + "72.철근가공조립(현장)(Type-ll)[Ton]", + " 철근 현장가공 및 조립(Type-Ⅱ) [건설표준품셈6-2]", + " 1. 현장가공", + " 가. 재료비", + " 철근 : 별도계산", + " 나. 인건비", + " 철 근 공 : 268,187 × 0.75인 = 201,140.2", + " 보통인부 : 172,068 × 0.25인 = 43,017", + " 인부소계 : 244,157.2", + " 기계경비(인력품의 9%)", + " 244,157.2 × 9% = 21,974.1", + " 다. 소 계", + " 노무비 : 244,157.2", + " 재료비 : 21,974.1", + " 경 비 :", + " 계 : 266,131.3", + " 2. 조 립", + " 가. 재료비", + " 어닐링철선(결속선)(#200.9mm200.4m/kg) 1,760 × 8kg = 14,080", + " 나. 인건비", + " 철 근 공 : 268,187 × 1.92인 = 514,919", + " 보통인부 : 172,068 × 0.77인 = 132,492.3", + " 인부소계 : 647,411.3", + " 기구손료 (가공노력품의 2%)", + " 647,411.3 × 0.02 = 12,948.2", + " 다. 소 계", + " 노무비 : 647,411.3", + " 재료비 : 14,080 + 12,948.2 = 27,028.2", + " 경 비 :", + " 계 : 674,439.5", + " 3. 합 계", + " 노무비 : 244,157.2 + 647,411.3 = 891,568", + " 재료비 : 21,974.1 + 27,028.2 = 49,002", + " 경 비 :", + " 계 : 940,570" + ], + "줄수": 7, + "호표들": [ + 52, + 53, + 54, + 56, + 57, + 59, + 60 + ], + "id": "N045" + }, + { + "파일": "북부청임도", + "호표": 53, + "호표이름": "수로BOX 구체", + "호표규격": "4.0m×2.0m×1련", + "호표단위": "M", + "차례": 6, + "이름": "강관비계", + "규격": "10m이하,3개월사용", + "수량": 4.8, + "단위": "㎡", + "단가": { + "계": 20887, + "노무비": 18506, + "재료비": 2381, + "경비": null + }, + "갈래": "단가산출", + "참조": { + "종류": "단산", + "번": 74 + }, + "근거": { + "출처": [ + [ + "건설품셈", + "2-7-1" + ], + [ + "건설품셈", + "2-2-5" + ] + ], + "직종": [ + "비계공", + "보통인부" + ], + "행": [ + 3325, + 3362 + ] + }, + "맞춤": { + "열쇠": "강관비계|10m이하,3개월사용|m2", + "대상": { + "로직": "GF000188", + "이름": "강관비계", + "단위": "공㎥" + }, + "입력값": {}, + "못읽음": [], + "상태": "같음", + "환산": { + "배": null, + "갈래": "입력" + }, + "들어감": false + }, + "단산머리": { + "이름": "강관비계(10m이하,3개월사용)", + "단위": "㎡" + }, + "단산글": [ + "74.강관비계(10m이하,3개월사용)[㎡]", + " 강관비계10m이하 3개월사용 [건설공사표준품셈 2-7-1 강관비계설치및해체]", + " [건설공사표준품셈 2-2-5 구조물 비계 손율]", + " 1. 재료비 (3개월손료 적용)", + " 강관비계 (4000×48.6mm×2.3mm) 3,950 × 3.99m × 7% = 1,103.2", + " 이음철물 2,200 × 0.5EA × 12% = 132", + " 조임철물 (직교자재) 2,700 × 2.08EA × 12% = 673.9", + " 이음철물 2,200 × 0.04EA = 88", + " 받침철물 4,000 × 0.04EA × 9% = 14.4", + " 자재소계 : 2,011.5", + " 2. 인건비", + " 비 계 공 : 281,939 × 3인 ÷ 55㎡ = 15,378.4", + " 보통인부 : 172,068 × 1인 ÷ 55㎡ = 3,128.5", + " 인부소계 : 18,506.9", + " 3. 공구손료(노무비의 2%)", + " 18,506.9 × 2% = 370.1", + " 4. 합 계", + " 노무비 : 18,506", + " 재료비 : 2,011.5 + 370.1 = 2,381", + " 경 비 :", + " 계 : 20,887" + ], + "줄수": 6, + "호표들": [ + 53, + 54, + 56, + 57, + 59, + 60 + ], + "id": "N046" + }, + { + "파일": "북부청임도", + "호표": 58, + "호표이름": "야생동물이동로", + "호표규격": "B=1.0m", + "호표단위": "M", + "차례": 0, + "이름": "흙깍기(토사)", + "규격": "굴삭기1.0㎥,보통토사", + "수량": 5, + "단위": "㎥", + "단가": { + "계": 3131, + "노무비": 1523, + "재료비": 844, + "경비": 764 + }, + "갈래": "단가산출", + "참조": { + "종류": "단산", + "번": 49 + }, + "근거": { + "출처": [ + [ + "건설품셈", + "3-2-3" + ] + ], + "직종": [], + "행": [ + 2346, + 2366 + ] + }, + "맞춤": { + "열쇠": "흙깍기(토사)|굴삭기1.0m3,보통토사|m3", + "대상": null, + "입력값": {}, + "못읽음": [], + "상태": "후보", + "환산": null, + "들어감": false, + "후보": [ + { + "로직": "GC000085", + "이름": "흙깎기(기계) 보통토사", + "단위": "㎥", + "근거": { + "조항": [ + "건설품셈 공통 3-2-3" + ], + "직종": [], + "단위": true, + "점수": 1.25, + "까닭": "조항 같음 · 건설 공통 · 단위 맞음" + }, + "점수": 1.25 + }, + { + "로직": "GC000086", + "이름": "흙깎기(기계) 혼합토사", + "단위": "㎥", + "근거": { + "조항": [ + "건설품셈 공통 3-2-3" + ], + "직종": [], + "단위": true, + "점수": 1.25, + "까닭": "조항 같음 · 건설 공통 · 단위 맞음" + }, + "점수": 1.25 + }, + { + "로직": "GC000087", + "이름": "흙깎기(기계) 암", + "단위": "㎥", + "근거": { + "조항": [ + "건설품셈 공통 3-2-3" + ], + "직종": [], + "단위": true, + "점수": 1.25, + "까닭": "조항 같음 · 건설 공통 · 단위 맞음" + }, + "점수": 1.25 + }, + { + "로직": "GC000088", + "이름": "터파기(기계) 보통토사", + "단위": "㎥", + "근거": { + "조항": [ + "건설품셈 공통 3-2-4" + ], + "직종": [], + "단위": true, + "점수": 0.75, + "까닭": "앞 마디 같음 · 건설 공통 · 단위 맞음" + }, + "점수": 0.75 + }, + { + "로직": "GC000083", + "이름": "굴착(인력/토사)", + "단위": "㎥", + "근거": { + "조항": [ + "건설품셈 공통 3-2-1" + ], + "직종": [], + "단위": true, + "점수": 0.75, + "까닭": "앞 마디 같음 · 건설 공통 · 단위 맞음" + }, + "점수": 0.75 + } + ] + }, + "단산머리": { + "이름": "흙깍기(토사)(굴삭기1.0㎥,보통토사)", + "단위": "㎥" + }, + "단산글": [ + "49.흙깍기(토사)(굴삭기1.0㎥,보통토사)[㎥]", + " 흙깎기-보통토사 [건설공사 표준품셈3-2-3]", + " 시공량은 소규모 적용(10,000㎥이상이나 불도우저 적용 불가능하고 작업공간이 협소)", + " 1. 굴착기(무한궤도)(1.0㎥)", + " Q = 310㎥ ÷ 8 = 38.75㎥/hr", + " 노무비 : 59,020 ÷ 38.75 = 1,523", + " 재료비 : 32,735 ÷ 38.75 = 844.7", + " 경 비 : 29,621 ÷ 38.75 = 764.4", + " 소 계 : 3,132.1", + " 2. 합 계", + " 노무비 : 1,523", + " 재료비 : 844", + " 경 비 : 764", + " 계 : 3,131" + ], + "줄수": 1, + "호표들": [ + 58 + ], + "id": "N047" + }, + { + "파일": "북부청임도", + "호표": 58, + "호표이름": "야생동물이동로", + "호표규격": "B=1.0m", + "호표단위": "M", + "차례": 1, + "이름": "잔토정리", + "규격": "굴삭기0.6㎥", + "수량": 5, + "단위": "㎥", + "단가": { + "계": 1958, + "노무비": 1091, + "재료비": 366, + "경비": 501 + }, + "갈래": "단가산출", + "참조": { + "종류": "단산", + "번": 56 + }, + "근거": { + "출처": [], + "직종": [], + "행": [ + 2573, + 2595 + ] + }, + "맞춤": { + "열쇠": "잔토정리|굴삭기0.6m3|m3", + "대상": null, + "입력값": {}, + "못읽음": [], + "상태": "후보", + "환산": null, + "들어감": false, + "후보": [ + { + "로직": "GC000357", + "이름": "타설후 정리", + "단위": "m", + "점수": 0.286 + }, + { + "로직": "GC000682", + "이름": "콘크리트면 정리(연마)", + "단위": "㎡", + "점수": 0.222 + }, + { + "로직": "GC001084", + "이름": "도상갱환 타설 후 정리", + "단위": "m", + "점수": 0.182 + } + ] + }, + "단산머리": { + "이름": "잔토정리(굴삭기0.6㎥)", + "단위": "㎥" + }, + "단산글": [ + "56.잔토정리(굴삭기0.6㎥)[㎥]", + " 1. 굴착기(타이어형) (0.6㎥)", + " q = 0.6㎥ f = 1 / 1.3 = 0.77 K = 0.9", + " E = 0.65 Cm = 18sec (90˚)", + " 3,600×q×K×f×E 3,600×0.6×0.9×0.77×0.65", + " Q = ────────── = ──────────────= 54.05 ㎥/hr", + " Cm 18", + " 노무비 : 59,020 ÷ 54.05 = 1,091.9", + " 재료비 : 19,792 ÷ 54.05 = 366.1", + " 경 비 : 27,120 ÷ 54.05 = 501.7", + " 소 계 : 1,959.7", + " 2. 합 계", + " 노무비 : 1,091", + " 재료비 : 366", + " 경 비 : 501", + " 계 : 1,958" + ], + "줄수": 1, + "호표들": [ + 58 + ], + "id": "N048" + }, + { + "파일": "북부청임도", + "호표": 65, + "호표이름": "임도차단기", + "호표규격": "H=1.2m, B=5.6m", + "호표단위": "개소", + "차례": 15, + "이름": "잡철물 제작설치", + "규격": "규격철물 설치(일반철재)", + "수량": 0.10459, + "단위": "Ton", + "단가": { + "계": 3316858, + "노무비": 3071165, + "재료비": 245693, + "경비": null + }, + "갈래": "단가산출", + "참조": { + "종류": "단산", + "번": 82 + }, + "근거": { + "출처": [ + [ + "건설품셈", + "8-3-1" + ] + ], + "직종": [ + "철공", + "용접공", + "특별인부", + "보통인부" + ], + "행": [ + 3770, + 3799 + ] + }, + "맞춤": { + "열쇠": "잡철물제작설치|규격철물설치(일반철재)|톤", + "대상": null, + "입력값": {}, + "못읽음": [], + "상태": "후보", + "환산": null, + "들어감": false, + "후보": [ + { + "로직": "GC000676", + "이름": "잡철물 제작 및 설치", + "단위": "ton", + "근거": { + "조항": [ + "건설품셈 건축 8-3-1" + ], + "직종": [ + "용접공", + "철공", + "특별인부" + ], + "단위": true, + "점수": 1.65, + "까닭": "조항 같음 · 직종 용접공 · 철공 · 특별인부 · 단위 맞음" + }, + "점수": 2.569 + }, + { + "로직": "GC000807", + "이름": "강판제 · 패키지형수관식 보일러 설치", + "단위": "ton", + "근거": { + "조항": [ + "건설품셈 기계설비 8-3-1" + ], + "직종": [ + "특별인부" + ], + "단위": true, + "점수": 1.65, + "까닭": "조항 같음 · 직종 특별인부 · 단위 맞음" + }, + "점수": 1.895 + }, + { + "로직": "GC000806", + "이름": "주철제 보일러 설치", + "단위": "절", + "근거": { + "조항": [ + "건설품셈 기계설비 8-3-1" + ], + "직종": [ + "특별인부" + ], + "단위": false, + "점수": 1.5, + "까닭": "조항 같음 · 직종 특별인부" + }, + "점수": 1.654 + }, + { + "로직": "GC000810", + "이름": "온수보일러 설치", + "단위": "대", + "근거": { + "조항": [ + "건설품셈 기계설비 8-3-4" + ], + "직종": [ + "특별인부" + ], + "단위": false, + "점수": 1.0, + "까닭": "앞 마디 같음 · 직종 특별인부" + }, + "점수": 1.167 + }, + { + "로직": "GC000812", + "이름": "방열기 설치", + "단위": "조", + "근거": { + "조항": [ + "건설품셈 기계설비 8-3-6" + ], + "직종": [], + "단위": false, + "점수": 0.5, + "까닭": "앞 마디 같음" + }, + "점수": 0.7 + } + ] + }, + "단산머리": { + "이름": "잡철물 제작설치(규격철물 설치(일반철재))", + "단위": "Ton" + }, + "단산글": [ + "82.잡철물 제작설치(규격철물 설치(일반철재))[Ton]", + " < 건설공사표준품셈(건축) 8-3-1 >", + " 1. 노무비", + " 철 공 : 239,808 × 7.05인 = 1,690,646.4", + " 용 접 공 : 282,536 × 2.57인 = 726,117.5", + " 특별인부 : 226,122 × 1.92인 = 434,154.2", + " 보통인부 : 172,068 × 1.28인 = 220,247", + " 인부소계 : 3,071,165.1", + " 2. 공구손료 및 경장비의 기계경비(노무비의 5%)", + " 3,071,165.1 × 5% = 153,558.2", + " 3. 잡재료비(노무비의 3%)", + " 3,071,165.1 × 3% = 92,134.9", + " 4. 합 계", + " 노무비 : 3,071,165", + " 재료비 : 153,558.2 + 92,134.9 = 245,693", + " 경 비 :", + " 계 : 3,316,858" + ], + "줄수": 1, + "호표들": [ + 65 + ], + "id": "N049" + }, + { + "파일": "북부청임도", + "호표": 65, + "호표이름": "임도차단기", + "호표규격": "H=1.2m, B=5.6m", + "호표단위": "개소", + "차례": 17, + "이름": "도장(철재면2회)", + "규격": "방청1,유성2", + "수량": 6.22, + "단위": "㎡", + "단가": { + "계": 23083, + "노무비": 18108, + "재료비": 4975, + "경비": null + }, + "갈래": "단가산출", + "참조": { + "종류": "단산", + "번": 95 + }, + "근거": { + "출처": [ + [ + "건설품셈", + "11-1-3" + ], + [ + "건설품셈", + "11-2-6" + ], + [ + "건설품셈", + "11-2-4" + ] + ], + "직종": [ + "도장공", + "보통인부" + ], + "행": [ + 4284, + 4343 + ] + }, + "맞춤": { + "열쇠": "도장(철재면2회)|방청1,유성2|m2", + "대상": null, + "입력값": {}, + "못읽음": [], + "상태": "후보", + "환산": null, + "들어감": false, + "후보": [ + { + "로직": "GC000705", + "이름": "철재면 바탕만들기", + "단위": "㎡", + "근거": { + "조항": [ + "건설품셈 건축 11-1-3" + ], + "직종": [ + "도장공" + ], + "단위": true, + "점수": 1.65, + "까닭": "조항 같음 · 직종 도장공 · 단위 맞음" + }, + "점수": 1.65 + }, + { + "로직": "GC000712", + "이름": "유성페인트 붓칠(1회)", + "단위": "㎡", + "근거": { + "조항": [ + "건설품셈 건축 11-2-4" + ], + "직종": [ + "도장공" + ], + "단위": true, + "점수": 1.65, + "까닭": "조항 같음 · 직종 도장공 · 단위 맞음" + }, + "점수": 1.65 + }, + { + "로직": "GC000714", + "이름": "녹막이 페인트칠(붓 1회)", + "단위": "㎡", + "근거": { + "조항": [ + "건설품셈 건축 11-2-6" + ], + "직종": [ + "도장공" + ], + "단위": true, + "점수": 1.65, + "까닭": "조항 같음 · 직종 도장공 · 단위 맞음" + }, + "점수": 1.65 + }, + { + "로직": "GC000707", + "이름": "도장 후 퍼티 및 연마", + "단위": "㎡", + "근거": { + "조항": [ + "건설품셈 건축 11-1-1", + "건설품셈 건축 11-1-5" + ], + "직종": [ + "도장공" + ], + "단위": true, + "점수": 1.15, + "까닭": "앞 마디 같음 · 직종 도장공 · 단위 맞음" + }, + "점수": 1.55 + }, + { + "로직": "GC000378", + "이름": "강교 도장(1회)", + "단위": "㎡", + "점수": 0.65 + } + ] + }, + "단산머리": { + "이름": "도장(철재면2회)(방청1,유성2)", + "단위": "㎡" + }, + "단산글": [ + "95.도장(철재면2회)(방청1,유성2)[㎡]", + " 1. 철재면 바탕만들기 [건설공사표준품셈(건축) 11-1-3]", + " 도 장 공 : 263,017 × 0.006인 = 1,578.1", + " 보통인부 : 172,068 × 0.001인 = 172", + " 인부소계 : 1,750.1", + " 2. 공구손료(인력품의 3%)", + " 1,750.1 × 3% = 52.5", + " 3. 녹막이 페인트 칠 [건설공사표준품셈(건축) 11-2-6]", + " 도 장 공 : 263,017 × 0.015인 × 1회 = 3,945.2", + " 보통인부 : 172,068 × 0.003인 × 1회 = 516.2", + " 인부소계 : 4,461.4", + " 4. 공구손료(인력품의 2%)", + " 4,461.4 × 2% = 89.2", + " 5. 재료비", + " 광명단조합페인트(1종2류(철재) 8,400 × 0.161 × 2회 = 2,704.8", + " 신나(KSM60602종(조합페인트류) 3,477 × 0.008ℓ × 2회 = 55.6", + " 자재소계 : 2,760.4", + " 6. 유성페인트 붓칠 [건설공사표준품셈(건축) 11-2-4]", + " 도 장 공 : 263,017 × 0.02인 × 2회 = 10,520.6", + " 보통인부 : 172,068 × 0.004인× 2회 = 1,376.5", + " 인부소계 :11,897.1", + " 7. 공구손료(인력품의 2%)", + " 11,897.1 × 2% = 237.9", + " 8. 재료비", + " 조합페인트(KSM60202급,백색) 5,361 × 0.166인 × 2회 = 1,779.8", + " 신나(KSM60602종(조합페인트류) 3,477 × 0.008ℓ × 2회 = 55.6", + " 소계 : 1,835.4", + " 9. 합 계", + " 노무비 : 1,750.1 + 4,461.4 + 11,897.1 = 18,108", + " 재료비 : 52.5 + 89.2 + 2,760.4 + 237.9 + 1,835.4 = 4,975", + " 경 비 :", + " 계 : 23,083" + ], + "줄수": 1, + "호표들": [ + 65 + ], + "id": "N050" + }, + { + "파일": "북부청임도", + "호표": 66, + "호표이름": "임도안전표지판", + "호표규격": "삼각,D=900mm", + "호표단위": "개소", + "차례": 2, + "이름": "임도안전표지판설치", + "규격": "삼각,900mm", + "수량": 1, + "단위": "개소", + "단가": { + "계": 288119, + "노무비": 113754, + "재료비": 164642, + "경비": 9723 + }, + "갈래": "단가산출", + "참조": { + "종류": "단산", + "번": 81 + }, + "근거": { + "출처": [ + [ + "건설품셈", + "1-8-1" + ] + ], + "직종": [ + "특별인부", + "보통인부" + ], + "행": [ + 3725, + 3767 + ] + }, + "맞춤": { + "열쇠": "임도안전표지판설치|삼각,900mm|개소", + "대상": null, + "입력값": {}, + "못읽음": [], + "상태": "후보", + "환산": null, + "들어감": false, + "후보": [ + { + "로직": "GC000308", + "이름": "교통 안전표지판 설치", + "단위": "개소", + "근거": { + "조항": [ + "건설품셈 토목 1-8-1" + ], + "직종": [ + "특별인부" + ], + "단위": true, + "점수": 1.75, + "까닭": "조항 같음 · 건설 토목 · 직종 특별인부 · 단위 맞음" + }, + "점수": 2.65 + }, + { + "로직": "GC000309", + "이름": "도로 표지판 설치", + "단위": "개소", + "근거": { + "조항": [ + "건설품셈 토목 1-8-2" + ], + "직종": [ + "특별인부" + ], + "단위": true, + "점수": 1.25, + "까닭": "앞 마디 같음 · 건설 토목 · 직종 특별인부 · 단위 맞음" + }, + "점수": 1.971 + }, + { + "로직": "GC000315", + "이름": "도로표지병 설치", + "단위": "개소", + "근거": { + "조항": [ + "건설품셈 토목 1-8-4" + ], + "직종": [ + "특별인부" + ], + "단위": true, + "점수": 1.25, + "까닭": "앞 마디 같음 · 건설 토목 · 직종 특별인부 · 단위 맞음" + }, + "점수": 1.686 + }, + { + "로직": "GC000311", + "이름": "시선유도표지 설치", + "단위": "개소", + "근거": { + "조항": [ + "건설품셈 토목 1-8-5" + ], + "직종": [ + "특별인부" + ], + "단위": true, + "점수": 1.25, + "까닭": "앞 마디 같음 · 건설 토목 · 직종 특별인부 · 단위 맞음" + }, + "점수": 1.667 + }, + { + "로직": "GC000312", + "이름": "볼라드 설치", + "단위": "개소", + "근거": { + "조항": [ + "건설품셈 토목 1-8-6" + ], + "직종": [ + "특별인부" + ], + "단위": true, + "점수": 1.25, + "까닭": "앞 마디 같음 · 건설 토목 · 직종 특별인부 · 단위 맞음" + }, + "점수": 1.567 + } + ] + }, + "단산머리": { + "이름": "임도안전표지판설치(삼각,900mm)", + "단위": "개소" + }, + "단산글": [ + "81.임도안전표지판설치(삼각,900mm)[개소]", + " [건설공사표준품셈 1-8-1]", + " 1. 재 료 비", + " 삼각표지판(삼각,900mm) 84,000 × 1EA = 84,000", + " 표지판지주(D=76.3mm×3.0m) 75,000 × 1식 = 75,000", + " 자재소계 :159,000", + " 2. 인 건 비", + " 특별인부 : 226,122 × 2인 ÷ 12본/일 = 37,687 (지주)", + " 특별인부 : 226,122 × 2인 ÷ 22본/일 = 20,556.5 (표지판)", + " 보통인부 : 172,068 × 1인 ÷ 12본/일 = 14,339 (지주)", + " 보통인부 : 172,068 × 1인 ÷ 22본/일 = 7,821.2 (표지판)", + " 인부소계 : 80,403.7", + " 3. 장비 : 트럭탑재형크레인(5Ton)", + " Q = 8hr / 12개소 = 0.67", + " 노무비 : 49,778 × 0.67 = 33,351.2", + " 재료비 : 8,421 × 0.67 = 5,642", + " 경 비 : 11,117 × 0.67 = 7,448.3", + " 소 계 : 46,441.5", + " 4. 공구손료 및 경장비", + " (80,403.7 + 33,351.2) × 2% = 2,275", + " 5. 합 계", + " 노무비 : 80,403.7 + 33,351.2 = 113,754", + " 재료비 : 159,000 + 5,642 = 164,642", + " 경 비 : 7,448.3 + 2,275 = 9,723", + " 계 : 288,119" + ], + "줄수": 1, + "호표들": [ + 66 + ], + "id": "N051" + }, + { + "파일": "북부청사방", + "호표": 1, + "호표이름": "BOX 구체", + "호표규격": "4.0m×2.0m×1@", + "호표단위": "M", + "차례": 0, + "이름": "레미콘타설(장비사용)", + "규격": "철근구조물", + "수량": 5.86, + "단위": "㎥", + "단가": { + "계": 34298, + "노무비": 26901, + "재료비": 3329, + "경비": 4068 + }, + "갈래": "단가산출", + "참조": { + "종류": "단산", + "번": 22 + }, + "근거": { + "출처": [ + [ + "건설품셈", + "6-1-1" + ] + ], + "직종": [ + "콘크리트공", + "보통인부" + ], + "행": [ + 1005, + 1039 + ] + }, + "맞춤": { + "열쇠": "레미콘타설(장비사용)|철근구조물|m3", + "대상": null, + "입력값": {}, + "못읽음": [], + "상태": "후보", + "환산": null, + "들어감": false, + "후보": [ + { + "로직": "GC000198", + "이름": "레디믹스트콘크리트 타설", + "단위": "㎥", + "근거": { + "조항": [ + "건설품셈 공통 6-1-1" + ], + "직종": [ + "콘크리트공" + ], + "단위": true, + "점수": 1.75, + "까닭": "조항 같음 · 건설 공통 · 직종 콘크리트공 · 단위 맞음" + }, + "점수": 1.75 + }, + { + "로직": "GC000201", + "이름": "콘크리트 펌프차 타설", + "단위": "㎥", + "근거": { + "조항": [ + "건설품셈 공통 6-1-4" + ], + "직종": [ + "콘크리트공" + ], + "단위": true, + "점수": 1.25, + "까닭": "앞 마디 같음 · 건설 공통 · 직종 콘크리트공 · 단위 맞음" + }, + "점수": 1.567 + }, + { + "로직": "GC000199", + "이름": "현장비빔타설", + "단위": "㎥", + "근거": { + "조항": [ + "건설품셈 공통 6-1-2" + ], + "직종": [ + "콘크리트공" + ], + "단위": true, + "점수": 1.25, + "까닭": "앞 마디 같음 · 건설 공통 · 직종 콘크리트공 · 단위 맞음" + }, + "점수": 1.25 + }, + { + "로직": "GF000182", + "이름": "펌프카 타설(편성 · 일일 시공량)", + "단위": "㎥", + "점수": 0.45 + }, + { + "로직": "GF000166", + "이름": "콘크리트 타설(무근진동기 제외)", + "단위": "㎥", + "점수": 0.422 + } + ] + }, + "단산머리": { + "이름": "레미콘타설(장비사용)(철근구조물)", + "단위": "㎥" + }, + "단산글": [ + "22.레미콘타설(장비사용)(철근구조물)[㎥]", + " 2024년 품셈개정, 6-1-1콘크리트 타설", + " ○. 레미콘타설(장비사용타설-철근구조물(소량타설)", + " 1. 레미콘 : 별도계상", + " 2. 타설비", + " 콘크리트공 : 273,540 × 3 ÷ 55인 = 14,920.3", + " 보통인부 : 172,068 × 1 ÷ 55인 = 3,128.5", + " 인부소계 : 18,048.8", + " 3. 공구손료 및 경장비(콘크리트 진동기등)의 기계경비(노무비의 2%)", + " 기계손료 : 18,048.8 × 2% = 360.9", + " 4. 굴착기(타이어형)(0.6㎥)[hr]", + " Q = 8 / 55 = 0.15hr/㎥", + " 노무비 : 59,020 × 0.15 = 8,853", + " 재료비 : 19,792 × 0.15 = 2,968.8", + " 경 비 : 27,120 × 0.15 = 4,068", + " 소 계 : 15,889.8", + " 5. 합 계", + " 노무비 : 18,048.8 + 8,853 = 26,901", + " 재료비 : 360.9 + 2,968.8 = 3,329", + " 경 비 : 4,068", + " 계 : 34,298" + ], + "줄수": 1, + "호표들": [ + 1 + ], + "id": "S052" + }, + { + "파일": "북부청사방", + "호표": 1, + "호표이름": "BOX 구체", + "호표규격": "4.0m×2.0m×1@", + "호표단위": "M", + "차례": 1, + "이름": "레미콘타설(장비사용)", + "규격": "무근구조물", + "수량": 0.5, + "단위": "㎥", + "단가": { + "계": 29842, + "노무비": 23429, + "재료비": 2888, + "경비": 3525 + }, + "갈래": "단가산출", + "참조": { + "종류": "단산", + "번": 20 + }, + "근거": { + "출처": [ + [ + "건설품셈", + "6-1-1" + ] + ], + "직종": [ + "콘크리트공", + "보통인부" + ], + "행": [ + 931, + 965 + ] + }, + "맞춤": { + "열쇠": "레미콘타설(장비사용)|무근구조물|m3", + "대상": null, + "입력값": {}, + "못읽음": [], + "상태": "후보", + "환산": null, + "들어감": false, + "후보": [ + { + "로직": "GC000198", + "이름": "레디믹스트콘크리트 타설", + "단위": "㎥", + "근거": { + "조항": [ + "건설품셈 공통 6-1-1" + ], + "직종": [ + "콘크리트공" + ], + "단위": true, + "점수": 1.75, + "까닭": "조항 같음 · 건설 공통 · 직종 콘크리트공 · 단위 맞음" + }, + "점수": 1.75 + }, + { + "로직": "GC000201", + "이름": "콘크리트 펌프차 타설", + "단위": "㎥", + "근거": { + "조항": [ + "건설품셈 공통 6-1-4" + ], + "직종": [ + "콘크리트공" + ], + "단위": true, + "점수": 1.25, + "까닭": "앞 마디 같음 · 건설 공통 · 직종 콘크리트공 · 단위 맞음" + }, + "점수": 1.567 + }, + { + "로직": "GC000199", + "이름": "현장비빔타설", + "단위": "㎥", + "근거": { + "조항": [ + "건설품셈 공통 6-1-2" + ], + "직종": [ + "콘크리트공" + ], + "단위": true, + "점수": 1.25, + "까닭": "앞 마디 같음 · 건설 공통 · 직종 콘크리트공 · 단위 맞음" + }, + "점수": 1.25 + }, + { + "로직": "GF000182", + "이름": "펌프카 타설(편성 · 일일 시공량)", + "단위": "㎥", + "점수": 0.45 + }, + { + "로직": "GF000166", + "이름": "콘크리트 타설(무근진동기 제외)", + "단위": "㎥", + "점수": 0.422 + } + ] + }, + "단산머리": { + "이름": "레미콘타설(장비사용)(무근구조물)", + "단위": "㎥" + }, + "단산글": [ + "20.레미콘타설(장비사용)(무근구조물)[㎥]", + " 2024년 품셈개정, 6-1-1콘크리트 타설", + " ○. 레미콘타설(장비사용타설-철근구조물)", + " 1. 레미콘 : 별도계상", + " 2. 타설비", + " 콘크리트공 : 273,540 × 3 ÷ 63 = 13,025.7", + " 보통인부 : 172,068 × 1 ÷ 63 = 2,731.2", + " 인부소계 : 15,756.9", + " 3. 공구손료 및 경장비(콘크리트 진동기등)의 기계경비(노무비의 2%)", + " 15,756.9 × 2% = 315.1", + " 4. 굴착기(타이어형)(0.6㎥)[hr]", + " Q = 1 × 8hr / 63 = 0.13hr/㎥", + " 노무비 : 59,020 × 0.13 = 7,672.6", + " 재료비 : 19,792 × 0.13 = 2,572.9", + " 경 비 : 27,120 × 0.13 = 3,525.6", + " 소 계 : 13,771.1", + " 5. 합 계", + " 노무비 : 15,756.9 + 7,672.6 = 23,429", + " 재료비 : 315.1 + 2,572.9 = 2,888", + " 경 비 : 3,525", + " 계 : 29,842" + ], + "줄수": 4, + "호표들": [ + 1, + 2, + 4, + 18 + ], + "id": "S053" + }, + { + "파일": "북부청사방", + "호표": 1, + "호표이름": "BOX 구체", + "호표규격": "4.0m×2.0m×1@", + "호표단위": "M", + "차례": 2, + "이름": "유로폼(Euro Form)", + "규격": "6회, 보통", + "수량": 13.78, + "단위": "㎡", + "단가": { + "계": 41724, + "노무비": 36435, + "재료비": 5289, + "경비": null + }, + "갈래": "단가산출", + "참조": { + "종류": "단산", + "번": 38 + }, + "근거": { + "출처": [ + [ + "건설품셈", + "6-3-3" + ] + ], + "직종": [ + "형틀목공", + "보통인부" + ], + "행": [ + 1631, + 1660 + ] + }, + "맞춤": { + "열쇠": "유로폼(euroform)|6회,보통|m2", + "대상": null, + "입력값": {}, + "못읽음": [], + "상태": "후보", + "환산": null, + "들어감": false, + "후보": [ + { + "로직": "GC000214", + "이름": "유로폼 설치 및 해체", + "단위": "㎡", + "근거": { + "조항": [ + "건설품셈 공통 6-3-3" + ], + "직종": [ + "형틀목공" + ], + "단위": true, + "점수": 1.75, + "까닭": "조항 같음 · 건설 공통 · 직종 형틀목공 · 단위 맞음" + }, + "점수": 1.75 + }, + { + "로직": "GC000211", + "이름": "합판거푸집 설치 및 해체", + "단위": "㎡", + "근거": { + "조항": [ + "건설품셈 공통 6-3-1" + ], + "직종": [ + "형틀목공" + ], + "단위": true, + "점수": 1.25, + "까닭": "앞 마디 같음 · 건설 공통 · 직종 형틀목공 · 단위 맞음" + }, + "점수": 1.25 + }, + { + "로직": "GC000212", + "이름": "강재거푸집 인력 설치 및 해체", + "단위": "㎡", + "근거": { + "조항": [ + "건설품셈 공통 6-3-2" + ], + "직종": [ + "형틀목공" + ], + "단위": true, + "점수": 1.25, + "까닭": "앞 마디 같음 · 건설 공통 · 직종 형틀목공 · 단위 맞음" + }, + "점수": 1.25 + }, + { + "로직": "GC000213", + "이름": "강재거푸집 장비조합 설치 및 해체", + "단위": "㎡", + "근거": { + "조항": [ + "건설품셈 공통 6-3-2" + ], + "직종": [ + "형틀목공" + ], + "단위": true, + "점수": 1.25, + "까닭": "앞 마디 같음 · 건설 공통 · 직종 형틀목공 · 단위 맞음" + }, + "점수": 1.25 + }, + { + "로직": "GF000210", + "이름": "유로폼 설치 및 해체", + "단위": "㎡", + "점수": 0.644 + } + ] + }, + "단산머리": { + "이름": "유로폼(Euro Form)(6회, 보통)", + "단위": "㎡" + }, + "단산글": [ + "38.유로폼(Euro Form)(6회, 보통)[㎡]", + " 2024년 품셈개정, 6-3-3 유 로 폼", + " 1. 재료비", + " 유로폼(패널)(600×1200) 28,500 × 0.095매 = 2,707.5", + " 2. 잡재료비(주자재비의 55%)", + " 소모자재 : 2,707.5 × 55% = 1,489.1", + " 3. 설치비", + " 형틀목공 : 275,790 × 4 ÷ 35인 = 31,518.8", + " 보통인부 : 172,068 × 1 ÷ 35인 = 4,916.2", + " 인부소계 : 36,435", + " 4. 공구손료 및 경장비 기계경비(노무비의 3%)", + " 36,435 × 3% = 1,093", + " 5. 합 계", + " 노무비 : 36,435", + " 재료비 : 2,707.5 + 1,489.1 + 1,093 = 5,289", + " 경 비 :", + " 계 : 41,724" + ], + "줄수": 2, + "호표들": [ + 1, + 11 + ], + "id": "S054" + }, + { + "파일": "북부청사방", + "호표": 1, + "호표이름": "BOX 구체", + "호표규격": "4.0m×2.0m×1@", + "호표단위": "M", + "차례": 3, + "이름": "철근가공조립(현장)", + "규격": "Type-Ⅱ", + "수량": 0.887, + "단위": "Ton", + "단가": { + "계": 940570, + "노무비": 891568, + "재료비": 49002, + "경비": null + }, + "갈래": "단가산출", + "참조": { + "종류": "단산", + "번": 53 + }, + "근거": { + "출처": [ + [ + "건설품셈", + "6-2" + ] + ], + "직종": [ + "철근공", + "보통인부" + ], + "행": [ + 2268, + 2323 + ] + }, + "맞춤": { + "열쇠": "철근가공조립(현장)|type-ii|톤", + "대상": null, + "입력값": {}, + "못읽음": [], + "상태": "후보", + "환산": null, + "들어감": false, + "후보": [ + { + "로직": "GF000187", + "이름": "철근가공조립(복잡)", + "단위": "ton", + "점수": 2.2 + }, + { + "로직": "GF000207", + "이름": "암거이음받침 철근가공조립(보통)", + "단위": "ton", + "점수": 0.825 + }, + { + "로직": "GF000159", + "이름": "철근 현장가공 및 조립", + "단위": "ton", + "점수": 0.662 + }, + { + "로직": "GC000206", + "이름": "철근 현장가공", + "단위": "ton", + "점수": 0.55 + }, + { + "로직": "GC000207", + "이름": "철근 현장조립", + "단위": "ton", + "점수": 0.55 + } + ] + }, + "단산머리": { + "이름": "철근가공조립(현장)(Type-Ⅱ)", + "단위": "Ton" + }, + "단산글": [ + "53.철근가공조립(현장)(Type-Ⅱ)[Ton]", + " 2024년 철근 현장가공 및 조립(Type-Ⅱ) [건설표준품셈6-2]", + " 1. 현장가공", + " 가. 철 근 : 1Ton - 별 산 -", + " 철 근 공 : 268,187 × 0.75인 = 201,140.2", + " 보통인부 : 172,068 × 0.25인 = 43,017", + " 인부소계 : 244,157.2", + " 나. 기계경비(인력품의 9%)", + " 244,157.2 × 9% = 21,974.1", + " 다. 소 계", + " 노무비 : 244,157.2", + " 재료비 : 21,974.1", + " 경 비 :", + " 계 : 266,131.3", + " 2. 현장조립", + " 가. 재료비", + " 어닐링철선(결속선)(#200.9mm200.4m/kg) 1,760 × 8kg = 14,080", + " 나. 인 력", + " 철 근 공 : 268,187 × 1.92인 = 514,919", + " 보통인부 : 172,068 × 0.77인 = 132,492.3", + " 인부소계 : 647,411.3", + " 다. 기구손료(인력품의 2%)", + " 647,411.3 × 2% = 12,948.2", + " 라. 소 계", + " 노무비 : 647,411.3", + " 재료비 : 14,080 + 12,948.2 = 27,028.2", + " 경 비 :", + " 계 : 674,439.5", + " 3. 합 계", + " 노무비 : 244,157.2 + 647,411.3 = 891,568", + " 재료비 : 21,974.1 + 27,028.2 = 49,002", + " 경 비 :", + " 계 : 940,570" + ], + "줄수": 1, + "호표들": [ + 1 + ], + "id": "S055" + }, + { + "파일": "북부청사방", + "호표": 1, + "호표이름": "BOX 구체", + "호표규격": "4.0m×2.0m×1@", + "호표단위": "M", + "차례": 4, + "이름": "강관비계", + "규격": "(10m이하)", + "수량": 4.7, + "단위": "㎡", + "단가": { + "계": 20750, + "노무비": 18506, + "재료비": 2244, + "경비": null + }, + "갈래": "단가산출", + "참조": { + "종류": "단산", + "번": 4 + }, + "근거": { + "출처": [ + [ + "건설품셈", + "2-7-1" + ] + ], + "직종": [ + "비계공", + "보통인부" + ], + "행": [ + 102, + 137 + ] + }, + "맞춤": { + "열쇠": "강관비계|(10m이하)|m2", + "대상": { + "로직": "GF000188", + "이름": "강관비계", + "단위": "공㎥" + }, + "입력값": {}, + "못읽음": [], + "상태": "같음", + "환산": { + "배": null, + "갈래": "입력" + }, + "들어감": false + }, + "단산머리": { + "이름": "강관비계((10m이하))", + "단위": "㎡" + }, + "단산글": [ + "4.강관비계((10m이하))[㎡]", + " [2026년 표준품셈 2-7-1]", + " 1. 재료비", + " 강관비계(48.6mm×2.3mm×6m)[M] 3,950 × 3.99m × 0.06 = 945.6", + " 이음철물 2,200 × 0.5EA × 0.12 = 132", + " 조임철물(직교자재) 2,700 × 2.08EA × 0.12 = 673.9", + " 받침철물 4,000 × 0.04EA × 0.09 = 14.4", + " 철물(직교자재) 2,700 × 0.04EA = 108", + " 자재소계 : 1,873.9", + " 2. 설치 및 해체", + " 비 계 공 : 281,939 × 3 / 55인 = 15,378.4", + " 보통인부 : 172,068 × 1 / 55인 = 3,128.5", + " 인부소계 : 18,506.9", + " 공구손료 및 경장비(콘크리트 진동기등)의 기계경비 (인건비의 2%)", + " 18,506.9 × 2% = 370.1", + " 3. 합 계", + " 노무비 : 18,506", + " 재료비 : 1,873.9 + 370.1 = 2,244", + " 경 비 :", + " 계 : 20,750" + ], + "줄수": 2, + "호표들": [ + 1, + 11 + ], + "id": "S056" + }, + { + "파일": "북부청사방", + "호표": 1, + "호표이름": "BOX 구체", + "호표규격": "4.0m×2.0m×1@", + "호표단위": "M", + "차례": 5, + "이름": "강관동바리", + "규격": "(2.5m이하)", + "수량": 7.82, + "단위": "공/㎥", + "단가": { + "계": 20379, + "노무비": 19988, + "재료비": 391, + "경비": null + }, + "갈래": "단가산출", + "참조": { + "종류": "단산", + "번": 3 + }, + "근거": { + "출처": [ + [ + "건설품셈", + "2-6-1" + ] + ], + "직종": [ + "형틀목공", + "보통인부" + ], + "행": [ + 72, + 99 + ] + }, + "맞춤": { + "열쇠": "강관동바리|(2.5m이하)|공/m3", + "대상": { + "로직": "GF000189", + "이름": "강관동바리", + "단위": "㎥" + }, + "입력값": {}, + "못읽음": [], + "상태": "같음", + "환산": { + "배": null, + "갈래": "입력" + }, + "들어감": false + }, + "단산머리": { + "이름": "강관동바리((2.5m이하))", + "단위": "공/㎥" + }, + "단산글": [ + "3.강관동바리((2.5m이하))[공/㎥]", + " * 강관동바리 [2026년 표준품셈(2-6-1)(2.5mm이하)]", + " ※ 암거구조물(1개월사용 : 재료비의 4%, 인건비의 100%)", + " 1. 재료비", + " 강관써포트(V42600-4000mm14.3kg) 24,500 × 0.38 × 0.04 = 372.4", + " 2. 인 력", + " 형틀목공 : 275,790 × 3 / 50인 = 16,547.4", + " 보통인부 : 172,068 × 1 / 50인 = 3,441.3", + " 인부소계 : 19,988.7", + " 3. 잡재료 및 소모재료 (주재료의 5%)", + " (372.4) × 5% = 18.6", + " 4. 합 계", + " 노무비 : 19,988", + " 재료비 : 372.4 + 18.6 = 391", + " 경 비 :", + " 계 : 20,379" + ], + "줄수": 1, + "호표들": [ + 1 + ], + "id": "S057" + }, + { + "파일": "북부청사방", + "호표": 2, + "호표이름": "BOX 난간", + "호표규격": "0.2×0.2", + "호표단위": "M", + "차례": 2, + "이름": "유로폼(Euro Form)", + "규격": "6회, 간단", + "수량": 0.8, + "단위": "㎡", + "단가": { + "계": 36329, + "노무비": 31880, + "재료비": 4449, + "경비": null + }, + "갈래": "단가산출", + "참조": { + "종류": "단산", + "번": 37 + }, + "근거": { + "출처": [ + [ + "건설품셈", + "6-3-3" + ] + ], + "직종": [ + "형틀목공", + "보통인부" + ], + "행": [ + 1599, + 1628 + ] + }, + "맞춤": { + "열쇠": "유로폼(euroform)|6회,간단|m2", + "대상": null, + "입력값": {}, + "못읽음": [], + "상태": "후보", + "환산": null, + "들어감": false, + "후보": [ + { + "로직": "GC000214", + "이름": "유로폼 설치 및 해체", + "단위": "㎡", + "근거": { + "조항": [ + "건설품셈 공통 6-3-3" + ], + "직종": [ + "형틀목공" + ], + "단위": true, + "점수": 1.75, + "까닭": "조항 같음 · 건설 공통 · 직종 형틀목공 · 단위 맞음" + }, + "점수": 1.75 + }, + { + "로직": "GC000211", + "이름": "합판거푸집 설치 및 해체", + "단위": "㎡", + "근거": { + "조항": [ + "건설품셈 공통 6-3-1" + ], + "직종": [ + "형틀목공" + ], + "단위": true, + "점수": 1.25, + "까닭": "앞 마디 같음 · 건설 공통 · 직종 형틀목공 · 단위 맞음" + }, + "점수": 1.25 + }, + { + "로직": "GC000212", + "이름": "강재거푸집 인력 설치 및 해체", + "단위": "㎡", + "근거": { + "조항": [ + "건설품셈 공통 6-3-2" + ], + "직종": [ + "형틀목공" + ], + "단위": true, + "점수": 1.25, + "까닭": "앞 마디 같음 · 건설 공통 · 직종 형틀목공 · 단위 맞음" + }, + "점수": 1.25 + }, + { + "로직": "GC000213", + "이름": "강재거푸집 장비조합 설치 및 해체", + "단위": "㎡", + "근거": { + "조항": [ + "건설품셈 공통 6-3-2" + ], + "직종": [ + "형틀목공" + ], + "단위": true, + "점수": 1.25, + "까닭": "앞 마디 같음 · 건설 공통 · 직종 형틀목공 · 단위 맞음" + }, + "점수": 1.25 + }, + { + "로직": "GF000210", + "이름": "유로폼 설치 및 해체", + "단위": "㎡", + "점수": 0.644 + } + ] + }, + "단산머리": { + "이름": "유로폼(Euro Form)(6회, 간단)", + "단위": "㎡" + }, + "단산글": [ + "37.유로폼(Euro Form)(6회, 간단)[㎡]", + " 2024년 품셈개정, 6-3-3 유로폼", + " 1. 재료비", + " 유로폼(패널)(600×1200) 28,500 × 0.095매 = 2,707.5", + " 2. 잡재료비(주자재비의 29%)", + " 소모자재 : 2,707.5 × 29% = 785.1", + " 3. 설치비", + " 형틀목공 : 275,790 × 4 ÷ 40인 = 27,579", + " 보통인부 : 172,068 × 1 ÷ 40인 = 4,301.7", + " 인부소계 : 31,880.7", + " 4. 공구손료 및 경장비 기계경비(노무비의 3%)", + " 31,880.7 × 3% = 956.4", + " 5. 합 계", + " 노무비 : 31,880", + " 재료비 : 2,707.5 + 785.1 + 956.4 = 4,449", + " 경 비 :", + " 계 : 36,329" + ], + "줄수": 1, + "호표들": [ + 2 + ], + "id": "S058" + }, + { + "파일": "북부청사방", + "호표": 2, + "호표이름": "BOX 난간", + "호표규격": "0.2×0.2", + "호표단위": "M", + "차례": 3, + "이름": "철근가공조립(현장)", + "규격": "Type-Ⅰ(소량)", + "수량": 0.005, + "단위": "Ton", + "단가": { + "계": 1421293, + "노무비": 1367280, + "재료비": 54013, + "경비": null + }, + "갈래": "단가산출", + "참조": { + "종류": "단산", + "번": 52 + }, + "근거": { + "출처": [ + [ + "건설품셈", + "6-2" + ] + ], + "직종": [ + "철근공", + "보통인부" + ], + "행": [ + 2210, + 2265 + ] + }, + "맞춤": { + "열쇠": "철근가공조립(현장)|type-i(소량)|톤", + "대상": null, + "입력값": {}, + "못읽음": [], + "상태": "후보", + "환산": null, + "들어감": false, + "후보": [ + { + "로직": "GF000187", + "이름": "철근가공조립(복잡)", + "단위": "ton", + "점수": 2.2 + }, + { + "로직": "GF000207", + "이름": "암거이음받침 철근가공조립(보통)", + "단위": "ton", + "점수": 0.825 + }, + { + "로직": "GF000159", + "이름": "철근 현장가공 및 조립", + "단위": "ton", + "점수": 0.662 + }, + { + "로직": "GC000206", + "이름": "철근 현장가공", + "단위": "ton", + "점수": 0.55 + }, + { + "로직": "GC000207", + "이름": "철근 현장조립", + "단위": "ton", + "점수": 0.55 + } + ] + }, + "단산머리": { + "이름": "철근가공조립(현장)(Type-Ⅰ(소량))", + "단위": "Ton" + }, + "단산글": [ + "52.철근가공조립(현장)(Type-Ⅰ(소량))[Ton]", + " 2024년 철근 현장가공 및 조립(Type-Ⅰ) [건설표준품셈6-2]", + " 1. 현장가공", + " 가. 철 근 : 1Ton - 별 산 -", + " 철 근 공 : 268,187 × 0.67인 = 179,685.2", + " 보통인부 : 172,068 × 0.22인 = 37,854.9", + " 인부소계 : 217,540.1", + " 나. 기계경비(인력품의 9%)", + " 217,540.1 × 9% = 19,578.6", + " 다. 소 계", + " 노무비 : 217,540.1", + " 재료비 : 19,578.6", + " 경 비 :", + " 계 : 237,118.7", + " 2. 조 립", + " 가. 재료비", + " 어닐링철선(결속선)(#200.9mm200.4m/kg) 1,760 × 6.5kg = 11,440", + " 나. 인 력", + " 철 근 공 : 268,187 × 3.53인 = 946,700.1", + " 보통인부 : 172,068 × 1.18인 = 203,040.2", + " 인부소계 : 1,149,740.3", + " 다. 기구손료(인력품의 2%)", + " 1,149,740.3 × 2% = 22,994.8", + " 라. 소 계", + " 노무비 : 1,149,740.3", + " 재료비 : 11,440 + 22,994.8 = 34,434.8", + " 경 비 :", + " 계 : 1,184,175.1", + " 3. 합 계", + " 노무비 : 217,540.1 + 1,149,740.3 = 1,367,280", + " 재료비 : 19,578.6 + 34,434.8 = 54,013", + " 경 비 :", + " 계 : 1,421,293" + ], + "줄수": 3, + "호표들": [ + 2, + 5, + 11 + ], + "id": "S059" + }, + { + "파일": "북부청사방", + "호표": 3, + "호표이름": "GRID철강재", + "호표규격": "", + "호표단위": "식", + "차례": 15, + "이름": "철강재운반(할증포함)", + "규격": "(구역화물)", + "수량": 2.431, + "단위": "Ton", + "단가": { + "계": 278090, + "노무비": null, + "재료비": null, + "경비": 278090 + }, + "갈래": "단가산출", + "참조": { + "종류": "단산", + "번": 49 + }, + "근거": { + "출처": [], + "직종": [], + "행": [ + 2105, + 2120 + ] + }, + "맞춤": { + "열쇠": "철강재운반(할증포함)|(구역화물)|톤", + "대상": null, + "입력값": {}, + "못읽음": [], + "상태": "후보", + "환산": null, + "들어감": false, + "후보": [ + { + "로직": "GF000283", + "이름": "덤프트럭 자재 운반(1회 표준 운반량)", + "단위": "ton", + "점수": 0.564 + }, + { + "로직": "GF000149", + "이름": "불도저 운반", + "단위": "㎥", + "점수": 0.3 + }, + { + "로직": "GC000363", + "이름": "자갈채집 및 운반", + "단위": "㎥", + "점수": 0.2 + }, + { + "로직": "GC000418", + "이름": "사석 적재 및 운반", + "단위": "㎥", + "점수": 0.2 + } + ] + }, + "단산머리": { + "이름": "철강재운반(할증포함)(구역화물)", + "단위": "Ton" + }, + "단산글": [ + "49.철강재운반(할증포함)(구역화물)[Ton]", + " 1. GRID철강재운반 (춘천-도금공장(안산)) (L = 150km)", + " 구역화물자동차(4.5Ton)(80km이내) 105,520 ÷ 1.1 × 2(왕복) = 191,854.5", + " 2. GRID철강재운반(춘천-현장) (L = 60km)", + " 구역화물자동차(4.5Ton)(60km이내) 94,860 ÷ 1.1 × 1 = 86,236.3", + " 3. 합 계", + " 노무비 :", + " 재료비 :", + " 경 비 : 191,854.5 + 86,236.3 = 278,090", + " 계 : 278,090" + ], + "줄수": 1, + "호표들": [ + 3 + ], + "id": "S060" + }, + { + "파일": "북부청사방", + "호표": 3, + "호표이름": "GRID철강재", + "호표규격": "", + "호표단위": "식", + "차례": 16, + "이름": "철골설치", + "규격": "", + "수량": 0.338, + "단위": "Ton", + "단가": { + "계": 97632, + "노무비": 59020, + "재료비": 7268, + "경비": 31344 + }, + "갈래": "단가산출", + "참조": { + "종류": "단산", + "번": 51 + }, + "근거": { + "출처": [], + "직종": [], + "행": [ + 2185, + 2207 + ] + }, + "맞춤": { + "열쇠": "철골설치||톤", + "대상": null, + "입력값": {}, + "못읽음": [], + "상태": "후보", + "환산": null, + "들어감": false, + "후보": [ + { + "로직": "GC000571", + "이름": "부대철골 설치(중도리 · 띠장 · 캐노피)", + "단위": "ton", + "점수": 0.9 + }, + { + "로직": "GC000931", + "이름": "수차 설치(Kaplan · Francis · Propeller)", + "단위": "ton", + "점수": 0.483 + }, + { + "로직": "GC000935", + "이름": "수문 설치(Tainter Gate)", + "단위": "ton", + "점수": 0.483 + }, + { + "로직": "GC000936", + "이름": "수문 설치(Roller Gate)", + "단위": "ton", + "점수": 0.483 + }, + { + "로직": "GC000560", + "이름": "철골 공장용접(기본용접공수)", + "단위": "t", + "점수": 0.4 + } + ] + }, + "단산머리": { + "이름": "철골설치", + "단위": "Ton" + }, + "단산글": [ + "51.철골설치[Ton]", + " * 철골설치 (철골세우기의 작업능력 : 기계-크레인10Ton)", + " 1. 기 계 (1일 처리능력 8Ton적용)", + " (1일 = 8시간, 8시간/8Ton = 1hr/Ton)", + " 크레인(타이어)(10Ton)", + " Q = 8Ton ÷ 8hr = 1Ton", + " 노무비 : 59,020 ÷ 1 = 59,020", + " 재료비 : 7,268 ÷ 1 = 7,268", + " 경 비 : 31,344 ÷ 1 = 31,344", + " 소 계 : 97,632", + " 2. 합 계", + " 노무비 : 59,020", + " 재료비 : 7,268", + " 경 비 : 31,344", + " 계 : 97,632" + ], + "줄수": 1, + "호표들": [ + 3 + ], + "id": "S061" + }, + { + "파일": "북부청사방", + "호표": 3, + "호표이름": "GRID철강재", + "호표규격": "", + "호표단위": "식", + "차례": 17, + "이름": "철골가공조립(공장)", + "규격": "(대형)", + "수량": 0.338, + "단위": "Ton", + "단가": { + "계": 869447, + "노무비": 506560, + "재료비": 58951, + "경비": 303936 + }, + "갈래": "단가산출", + "참조": { + "종류": "단산", + "번": 50 + }, + "근거": { + "출처": [ + [ + "건설품셈", + "1-1" + ], + [ + "건설품셈", + "1-1-3" + ] + ], + "직종": [ + "철골공", + "용접공" + ], + "행": [ + 2123, + 2182 + ] + }, + "맞춤": { + "열쇠": "철골가공조립(공장)|(대형)|톤", + "대상": null, + "입력값": {}, + "못읽음": [], + "상태": "후보", + "환산": null, + "들어감": false, + "후보": [ + { + "로직": "GC000560", + "이름": "철골 공장용접(기본용접공수)", + "단위": "t", + "근거": { + "조항": [ + "건설품셈 건축 1-1-3" + ], + "직종": [ + "용접공" + ], + "단위": true, + "점수": 1.65, + "까닭": "조항 같음 · 직종 용접공 · 단위 맞음" + }, + "점수": 2.0 + }, + { + "로직": "GC000559", + "이름": "철골 공장가공조립(기본철골공수)", + "단위": "t", + "근거": { + "조항": [ + "건설품셈 건축 1-1-1", + "건설품셈 건축 1-1-2" + ], + "직종": [], + "단위": true, + "점수": 0.65, + "까닭": "앞 마디 같음 · 단위 맞음" + }, + "점수": 1.467 + }, + { + "로직": "GC000995", + "이름": "비탈면 보강재 조립", + "단위": "ton", + "근거": { + "조항": [ + "건설품셈 유지관리 1-1-1" + ], + "직종": [], + "단위": true, + "점수": 0.65, + "까닭": "앞 마디 같음 · 단위 맞음" + }, + "점수": 0.967 + }, + { + "로직": "GC000722", + "이름": "강관 용접접합", + "단위": "개소", + "근거": { + "조항": [ + "건설품셈 기계설비 1-1-1" + ], + "직종": [ + "용접공" + ], + "단위": false, + "점수": 1.0, + "까닭": "앞 마디 같음 · 직종 용접공" + }, + "점수": 1.0 + }, + { + "로직": "GF000159", + "이름": "철근 현장가공 및 조립", + "단위": "ton", + "점수": 0.508 + } + ] + }, + "단산머리": { + "이름": "철골가공조립(공장)(대형)", + "단위": "Ton" + }, + "단산글": [ + "50.철골가공조립(공장)(대형)[Ton]", + " 1. 철골 가공 조립(품셈 1-1 철골 가공 조립(공장 생산))", + " (H형강 부재 제작 71% 적용, 60Ton 미만, 표준 라멘구조 적용)", + " 가. 인 력", + " 철골공 : 252,641 × 2.48 × 0.71 = 444,850.2", + " 제경비로 적용", + " 444,850.2 × 60% = 266,910.1", + " 나. 소요 부자재", + " 강재 사용량 : Q = 0.95", + " 산소(6000ℓ)(99%,120kg/㎠)[ℓ] 3.3 × 2333 × 0.95 = 7,313.9", + " 아세칠렌(AC98%,용접용) 30,000 × 1.7 × 0.95 = 48,450", + " 서비스볼트(고장력볼트)(M20×60)[본] 1,195 × 1 × 0.95 = 1,135.2", + " 보조강재(열연강판)(T=9-12) 1,080 × 2 × 0.95 = 2,052", + " 자재소계 : 58,951.1", + " 다. 소 계", + " 노무비 : 444,850.2", + " 재료비 : 58,951.1", + " 경 비 : 266,910.1", + " 계 : 770,711.4", + " 2. 용접(품셈 1-1-3 용접(공장))", + " (용접길이 20m미만, 총 강재량 30톤 미만, H형강 부재 제작 73% 적용)", + " 가. 인 력", + " 용접공 : 282,536 × 0.22 × 1.36(보정계수) × 0.73 = 61,710.3", + " 제경비로 적용(노무비의 60%)", + " 61,710.3 × 60% = 37,026.1", + " 나. 소 계", + " 노무비 : 61,710.3", + " 재료비 :", + " 경 비 : 37,026.1", + " 계 : 98,736.4", + " 3. 합 계", + " 노무비 : 444,850.2 + 61,710.3 = 506,560", + " 재료비 : 58,951", + " 경 비 : 266,910.1 + 37,026.1 = 303,936", + " 계 : 869,447" + ], + "줄수": 1, + "호표들": [ + 3 + ], + "id": "S062" + }, + { + "파일": "북부청사방", + "호표": 3, + "호표이름": "GRID철강재", + "호표규격": "", + "호표단위": "식", + "차례": 18, + "이름": "잡철물제작", + "규격": "", + "수량": 2.093, + "단위": "Ton", + "단가": { + "계": 5754774, + "노무비": 5328496, + "재료비": 159854, + "경비": 266424 + }, + "갈래": "단가산출", + "참조": { + "종류": "단산", + "번": 42 + }, + "근거": { + "출처": [ + [ + "건설품셈", + "8-3-1" + ] + ], + "직종": [ + "철공", + "보통인부", + "용접공", + "특별인부" + ], + "행": [ + 1759, + 1788 + ] + }, + "맞춤": { + "열쇠": "잡철물제작||톤", + "대상": null, + "입력값": {}, + "못읽음": [], + "상태": "후보", + "환산": null, + "들어감": false, + "후보": [ + { + "로직": "GC000676", + "이름": "잡철물 제작 및 설치", + "단위": "ton", + "근거": { + "조항": [ + "건설품셈 건축 8-3-1" + ], + "직종": [ + "용접공", + "철공", + "특별인부" + ], + "단위": true, + "점수": 1.65, + "까닭": "조항 같음 · 직종 용접공 · 철공 · 특별인부 · 단위 맞음" + }, + "점수": 2.527 + }, + { + "로직": "GC000807", + "이름": "강판제 · 패키지형수관식 보일러 설치", + "단위": "ton", + "근거": { + "조항": [ + "건설품셈 기계설비 8-3-1" + ], + "직종": [ + "특별인부" + ], + "단위": true, + "점수": 1.65, + "까닭": "조항 같음 · 직종 특별인부 · 단위 맞음" + }, + "점수": 1.65 + }, + { + "로직": "GC000440", + "이름": "굴절법 탄성파 탐사", + "단위": "km", + "근거": { + "조항": [ + "건설품셈 토목 8-3-1" + ], + "직종": [ + "특별인부" + ], + "단위": false, + "점수": 1.6, + "까닭": "조항 같음 · 건설 토목 · 직종 특별인부" + }, + "점수": 1.6 + }, + { + "로직": "GC000806", + "이름": "주철제 보일러 설치", + "단위": "절", + "근거": { + "조항": [ + "건설품셈 기계설비 8-3-1" + ], + "직종": [ + "특별인부" + ], + "단위": false, + "점수": 1.5, + "까닭": "조항 같음 · 직종 특별인부" + }, + "점수": 1.5 + }, + { + "로직": "GC000441", + "이름": "2차원 전기비저항탐사", + "단위": "km", + "근거": { + "조항": [ + "건설품셈 토목 8-3-2" + ], + "직종": [ + "특별인부" + ], + "단위": false, + "점수": 1.1, + "까닭": "앞 마디 같음 · 건설 토목 · 직종 특별인부" + }, + "점수": 1.1 + } + ] + }, + "단산머리": { + "이름": "잡철물제작", + "단위": "Ton" + }, + "단산글": [ + "42.잡철물제작[Ton]", + " * 각종 잡철물 제작설치 (품셈 8-3-1)", + " 1. 인건비", + " 철 공 : 239,808 × 12.38인 = 2,968,823", + " 보통인부 : 172,068 × 2.25인 = 387,153", + " 용 접 공 : 282,536 × 3.38인 = 954,971.6", + " 특별인부 : 226,122 × 4.50인 = 1,017,549", + " 인부소계 : 5,328,496.6", + " 2. 공구손료 및 경장비 기계경비(인력품의 5% 적용)", + " 5,328,496.6 × 5% = 266,424.8", + " 3. 잡재료비(인력품의 3% 적용)", + " 5,328,496.6 × 3% = 159,854.8", + " 4. 합 계", + " 노무비 : 5,328,496", + " 재료비 : 159,854", + " 경 비 : 266,424", + " 계 : 5,754,774" + ], + "줄수": 2, + "호표들": [ + 3, + 8 + ], + "id": "S063" + }, + { + "파일": "북부청사방", + "호표": 4, + "호표이름": "물넘이포장(레)", + "호표규격": "B=3.5,L=20", + "호표단위": "개소", + "차례": 2, + "이름": "합판거푸집(6회)", + "규격": "", + "수량": 8, + "단위": "㎡", + "단가": { + "계": 47045, + "노무비": 34461, + "재료비": 12584, + "경비": null + }, + "갈래": "단가산출", + "참조": { + "종류": "단산", + "번": 67 + }, + "근거": { + "출처": [ + [ + "건설품셈", + "6-3-1" + ] + ], + "직종": [ + "형틀목공", + "보통인부" + ], + "행": [ + 2877, + 2912 + ] + }, + "맞춤": { + "열쇠": "합판거푸집(6회)||m2", + "대상": null, + "입력값": {}, + "못읽음": [], + "상태": "후보", + "환산": null, + "들어감": false, + "후보": [ + { + "로직": "GC000211", + "이름": "합판거푸집 설치 및 해체", + "단위": "㎡", + "근거": { + "조항": [ + "건설품셈 공통 6-3-1" + ], + "직종": [ + "형틀목공" + ], + "단위": true, + "점수": 1.75, + "까닭": "조항 같음 · 건설 공통 · 직종 형틀목공 · 단위 맞음" + }, + "점수": 2.515 + }, + { + "로직": "GF000160", + "이름": "합판거푸집", + "단위": "㎡", + "점수": 2.2 + }, + { + "로직": "GC000216", + "이름": "합성수지(P.E) 원형 맨홀 거푸집 설치 및 해체", + "단위": "개소", + "근거": { + "조항": [ + "건설품셈 공통 6-3-5" + ], + "직종": [], + "단위": false, + "점수": 0.6, + "까닭": "앞 마디 같음 · 건설 공통" + }, + "점수": 0.811 + }, + { + "로직": "GC000215", + "이름": "문양거푸집(판넬) 설치 및 해체", + "단위": "㎡", + "근거": { + "조항": [ + "건설품셈 공통 6-3-4" + ], + "직종": [ + "형틀목공" + ], + "단위": true, + "점수": 1.25, + "까닭": "앞 마디 같음 · 건설 공통 · 직종 형틀목공 · 단위 맞음" + }, + "점수": 1.25 + }, + { + "로직": "GC000212", + "이름": "강재거푸집 인력 설치 및 해체", + "단위": "㎡", + "근거": { + "조항": [ + "건설품셈 공통 6-3-2" + ], + "직종": [ + "형틀목공" + ], + "단위": true, + "점수": 1.25, + "까닭": "앞 마디 같음 · 건설 공통 · 직종 형틀목공 · 단위 맞음" + }, + "점수": 1.25 + } + ] + }, + "단산머리": { + "이름": "합판거푸집(6회)", + "단위": "㎡" + }, + "단산글": [ + "67.합판거푸집(6회)[㎡]", + " 2024년 품셈개정, 6-3-1 합판거푸집", + " ※ 사용횟수 6회 : 간단", + " 1. 재료비(1회의 38%)", + " 내수합판1급(12m/m1220×2440)[㎡] 11,421 × 1.03㎡ × 32.7% = 3,846.7", + " 각재(외송)(3.6×3.0×3.0)[㎥] 577,844 × 0.038㎥ × 32.7% = 7,180.2", + " 자재소계 : 11,026.9", + " 소모자재(박리재 등)(주자재비의 11%)", + " 11,026.9 × 11% = 1,212.9", + " 2. 인건비", + " 형틀목공 : 275,790 × 5 ÷ 50인 = 27,579", + " 보통인부 : 172,068 × 2 ÷ 50인 = 6,882.7", + " 인부소계 : 34,461.7", + " 공구손료 및 경장비 기계경비(노무비의 1%)", + " 34,461.7 × 1% = 344.6", + " 3. 합 계", + " 노무비 : 34,461", + " 재료비 : 11,026.9 + 1,212.9 + 344.6 = 12,584", + " 경 비 :", + " 계 : 47,045" + ], + "줄수": 7, + "호표들": [ + 4, + 18, + 21, + 22, + 23, + 24, + 25 + ], + "id": "S064" + }, + { + "파일": "북부청사방", + "호표": 4, + "호표이름": "물넘이포장(레)", + "호표규격": "B=3.5,L=20", + "호표단위": "개소", + "차례": 3, + "이름": "와이어메시 바닥깔기", + "규격": "", + "수량": 68, + "단위": "㎡", + "단가": { + "계": 6249, + "노무비": 1356, + "재료비": 4893, + "경비": null + }, + "갈래": "단가산출", + "참조": { + "종류": "단산", + "번": 33 + }, + "근거": { + "출처": [ + [ + "건설품셈", + "8-1-3" + ] + ], + "직종": [ + "특별인부" + ], + "행": [ + 1465, + 1486 + ] + }, + "맞춤": { + "열쇠": "와이어메시바닥깔기||m2", + "대상": { + "로직": "GC000665", + "이름": "와이어메시 바닥깔기", + "단위": "㎡" + }, + "입력값": {}, + "못읽음": [], + "상태": "같음", + "환산": { + "배": 1, + "갈래": "같음" + }, + "들어감": true + }, + "단산머리": { + "이름": "와이어메시 바닥깔기", + "단위": "㎡" + }, + "단산글": [ + "33.와이어메시 바닥깔기[㎡]", + " * 와이어메시 바닥깔기 [건설표준품셈(8-1-3)]", + " 1. 재료비", + " 와이어매쉬(#6100×100)[㎡] 4,525 × 1.05 = 4,751.2", + " 잡재료비(주재료비의 3%)", + " 4,751.2 × 3% = 142.5", + " 2. 인건비", + " 특별인부 : 226,122 × 0.006 = 1,356.7", + " 3. 합 계", + " 노무비 : 1,356", + " 재료비 : 4,751.2 + 142.5 = 4,893", + " 경 비 :", + " 계 : 6,249" + ], + "줄수": 2, + "호표들": [ + 4, + 11 + ], + "id": "S065" + }, + { + "파일": "북부청사방", + "호표": 4, + "호표이름": "물넘이포장(레)", + "호표규격": "B=3.5,L=20", + "호표단위": "개소", + "차례": 5, + "이름": "구조물터파기(토사)", + "규격": "", + "수량": 15.6, + "단위": "㎥", + "단가": { + "계": 12364, + "노무비": 10011, + "재료비": 950, + "경비": 1403 + }, + "갈래": "단가산출", + "참조": { + "종류": "단산", + "번": 11 + }, + "근거": { + "출처": [ + [ + "건설품셈", + "3-2-4" + ] + ], + "직종": [ + "특별인부" + ], + "행": [ + 397, + 438 + ] + }, + "맞춤": { + "열쇠": "구조물터파기(토사)||m3", + "대상": null, + "입력값": {}, + "못읽음": [], + "상태": "후보", + "환산": null, + "들어감": false, + "후보": [ + { + "로직": "GC000090", + "이름": "터파기(기계) 암", + "단위": "㎥", + "근거": { + "조항": [ + "건설품셈 공통 3-2-4" + ], + "직종": [], + "단위": true, + "점수": 1.25, + "까닭": "조항 같음 · 건설 공통 · 단위 맞음" + }, + "점수": 1.9 + }, + { + "로직": "GC000088", + "이름": "터파기(기계) 보통토사", + "단위": "㎥", + "근거": { + "조항": [ + "건설품셈 공통 3-2-4" + ], + "직종": [], + "단위": true, + "점수": 1.25, + "까닭": "조항 같음 · 건설 공통 · 단위 맞음" + }, + "점수": 1.764 + }, + { + "로직": "GC000089", + "이름": "터파기(기계) 혼합토사", + "단위": "㎥", + "근거": { + "조항": [ + "건설품셈 공통 3-2-4" + ], + "직종": [], + "단위": true, + "점수": 1.25, + "까닭": "조항 같음 · 건설 공통 · 단위 맞음" + }, + "점수": 1.764 + }, + { + "로직": "GC000083", + "이름": "굴착(인력/토사)", + "단위": "㎥", + "근거": { + "조항": [ + "건설품셈 공통 3-2-1" + ], + "직종": [ + "특별인부" + ], + "단위": true, + "점수": 1.25, + "까닭": "앞 마디 같음 · 건설 공통 · 직종 특별인부 · 단위 맞음" + }, + "점수": 1.25 + }, + { + "로직": "GF000101", + "이름": "구조물터파기 육상토사(0~1m)", + "단위": "㎥", + "점수": 0.914 + } + ] + }, + "단산머리": { + "이름": "구조물터파기(토사)", + "단위": "㎥" + }, + "단산글": [ + "11.구조물터파기(토사)[㎥]", + " 1. 인 력 (10%) 적용", + " 보통토사 + 경질토사 적용", + " 1일 시공량 : Q = (3.6 + 2.7) / 2 = 3.15㎥/일", + " 특별인부 : 226,122 / 3.15 × 0.1 = 7,178.4", + " 2. 기 계(90%) - 혼합토사 [2025년 표준품셈3-2-4]", + " 가. 구조물터파기 : 굴착기(타이어형)(0.6㎥)[hr]", + " * TypeⅢ(작업공간 협소) 반영", + " 1일 시공량 : Qd = 150㎥/일 (혼합토사 기준)", + " Q = Qd / 8 = 150 / 8 = 18.75㎥/hr", + " 노무비 : 59,020 ÷ 18.75 × 0.9 = 2,832.9", + " 재료비 : 19,792 ÷ 18.75 × 0.9 = 950", + " 경 비 : 27,120 ÷ 18.75 × 0.9 = 1,301.7", + " 소 계 : 5,084.6", + " 나. 리퍼경비(기계경비의 2%)", + " (2,832.9 + 950 + 1,301.7) × 2% = 101.6", + " 다. 소 계", + " 노무비 : 2,832.9", + " 재료비 : 950", + " 경 비 : 1,301.7 + 101.6 = 1,403.3", + " 계 : 5,186.2", + " 3. 합 계", + " 노무비 : 7,178.4 + 2,832.9 = 10,011", + " 재료비 : 950", + " 경 비 : 1,403", + " 계 : 12,364" + ], + "줄수": 18, + "호표들": [ + 4, + 5, + 6, + 9, + 10, + 11, + 12, + 16, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27 + ], + "id": "S066" + }, + { + "파일": "북부청사방", + "호표": 4, + "호표이름": "물넘이포장(레)", + "호표규격": "B=3.5,L=20", + "호표단위": "개소", + "차례": 6, + "이름": "구조물되메우기 및 다짐", + "규격": "", + "수량": 1.6, + "단위": "㎥", + "단가": { + "계": 8395, + "노무비": 5316, + "재료비": 1506, + "경비": 1573 + }, + "갈래": "단가산출", + "참조": { + "종류": "단산", + "번": 8 + }, + "근거": { + "출처": [ + [ + "건설품셈", + "3-4-7" + ] + ], + "직종": [ + "보통인부", + "특별인부" + ], + "행": [ + 263, + 313 + ] + }, + "맞춤": { + "열쇠": "구조물되메우기및다짐||m3", + "대상": null, + "입력값": {}, + "못읽음": [], + "상태": "후보", + "환산": null, + "들어감": false, + "후보": [ + { + "로직": "GC000109", + "이름": "되메우기 및 다짐(대형장비)", + "단위": "㎥", + "근거": { + "조항": [ + "건설품셈 공통 3-4-7" + ], + "직종": [ + "특별인부" + ], + "단위": true, + "점수": 1.75, + "까닭": "조항 같음 · 건설 공통 · 직종 특별인부 · 단위 맞음" + }, + "점수": 2.7 + }, + { + "로직": "GC000108", + "이름": "되메우기 및 다짐(소형장비)", + "단위": "㎥", + "근거": { + "조항": [ + "건설품셈 공통 3-4-6" + ], + "직종": [ + "특별인부" + ], + "단위": true, + "점수": 1.25, + "까닭": "앞 마디 같음 · 건설 공통 · 직종 특별인부 · 단위 맞음" + }, + "점수": 2.2 + }, + { + "로직": "GC000106", + "이름": "뒤채움 및 다짐(소형장비)", + "단위": "㎥", + "근거": { + "조항": [ + "건설품셈 공통 3-4-4" + ], + "직종": [ + "특별인부" + ], + "단위": true, + "점수": 1.25, + "까닭": "앞 마디 같음 · 건설 공통 · 직종 특별인부 · 단위 맞음" + }, + "점수": 1.686 + }, + { + "로직": "GC000107", + "이름": "뒤채움 및 다짐(대형장비)", + "단위": "㎥", + "근거": { + "조항": [ + "건설품셈 공통 3-4-5" + ], + "직종": [ + "특별인부" + ], + "단위": true, + "점수": 1.25, + "까닭": "앞 마디 같음 · 건설 공통 · 직종 특별인부 · 단위 맞음" + }, + "점수": 1.686 + }, + { + "로직": "GC000102", + "이름": "흙쌓기(다짐)", + "단위": "㎥", + "근거": { + "조항": [ + "건설품셈 공통 3-4-1" + ], + "직종": [ + "특별인부" + ], + "단위": true, + "점수": 1.25, + "까닭": "앞 마디 같음 · 건설 공통 · 직종 특별인부 · 단위 맞음" + }, + "점수": 1.25 + } + ] + }, + "단산머리": { + "이름": "구조물되메우기 및 다짐", + "단위": "㎥" + }, + "단산글": [ + "8.구조물되메우기 및 다짐[㎥]", + " [2025년 건설표준품셈3-4-7]", + " 1일 시공량 : Qd = 290㎥/일 (대형장비 기준)", + " Q = Qd / 8 = 290 / 8 = 36.25㎥/hr", + " 1. 인건비", + " P = 1인 ÷ Qd = 1 ÷ 290 = 0.00345인", + " 보통인부 : 172,068 × P = 172,068 × 0.00345 = 593.6", + " 특별인부 : 226,122 × P = 226,122 × 0.00345 = 780.1", + " 인부소계 : 1,373.7", + " 2. 기 계 : 굴착기(무한궤도)(0.6㎥)", + " 노무비 : 59,020 ÷ 36.25 = 1,628.1", + " 재료비 : 17,122 ÷ 36.25 = 472.3", + " 경 비 : 23,423 ÷ 36.25 = 646.1", + " 소 계 : 2,746.5", + " 3. 다 짐 : 진동로울러(자주식)(10Ton)", + " 노무비 : 59,020 ÷ 36.25 = 1,628.1", + " 재료비 : 25,758 ÷ 36.25 = 710.5", + " 경 비 : 26,662 ÷ 36.25 = 735.5", + " 소 계 : 3,074.1", + " 4. 다 짐 : 진동로울러(핸드가이드식)(0.7Ton)", + " 재료비 : 3,420 ÷ 36.25 = 94.3", + " 경 비 : 1,967 ÷ 36.25 = 54.2", + " 소 계 : 148.5", + " 5. 살 수 : 물탱크(5500ℓ)", + " 노무비 : 49,778 ÷ 36.25 × 0.5 = 686.5", + " 재료비 : 16,635 ÷ 36.25 × 0.5 = 229.4", + " 경 비 : 9,986 ÷ 36.25 × 0.5 = 137.7", + " 소 계 : 1,053.6", + " 6. 합 계", + " 노무비 : 1,373.7 + 1,628.1 + 1,628.1 + 686.5 = 5,316", + " 재료비 : 472.3 + 710.5 + 94.3 + 229.4 = 1,506", + " 경 비 : 646.1 + 735.5 + 54.2 + 137.7 = 1,573", + " 계 : 8,395" + ], + "줄수": 6, + "호표들": [ + 4, + 5, + 6, + 10, + 11, + 18 + ], + "id": "S067" + }, + { + "파일": "북부청사방", + "호표": 4, + "호표이름": "물넘이포장(레)", + "호표규격": "B=3.5,L=20", + "호표단위": "개소", + "차례": 7, + "이름": "잔토처리", + "규격": "", + "수량": 14, + "단위": "㎥", + "단가": { + "계": 2213, + "노무비": 1169, + "재료비": 509, + "경비": 535 + }, + "갈래": "단가산출", + "참조": { + "종류": "단산", + "번": 39 + }, + "근거": { + "출처": [], + "직종": [], + "행": [ + 1663, + 1689 + ] + }, + "맞춤": { + "열쇠": "잔토처리||m3", + "대상": null, + "입력값": {}, + "못읽음": [], + "상태": "없음", + "환산": null, + "들어감": false, + "후보": [] + }, + "단산머리": { + "이름": "잔토처리", + "단위": "㎥" + }, + "단산글": [ + "39.잔토처리[㎥]", + " 잔토처리(기계100%) - 굴삭기 0.8㎥", + " 1. 기 계 (100%)", + " 굴착기(무한궤도)(0.8㎥)[hr]", + " q = 0.8 f = 1 / 1.3 = 0.77 K = 0.7", + " Cm = 20sec (135˚) E = 0.65", + " 3600×q×K×f×E 3,600×0.8×0.7×0.77×0.65", + " Q = ────────── = ────────────── = 50.45㎥/hr", + " Cm 20", + " 노무비 : 59,020 ÷ 50.45 = 1,169.8", + " 재료비 : 25,684 ÷ 50.45 = 509", + " 경 비 : 27,018 ÷ 50.45 = 535.5", + " 소 계 : 2,214.3", + " 2. 합 계", + " 노무비 : 1,169", + " 재료비 : 509", + " 경 비 : 535", + " 계 : 2,213" + ], + "줄수": 18, + "호표들": [ + 4, + 5, + 6, + 9, + 10, + 11, + 12, + 16, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27 + ], + "id": "S068" + }, + { + "파일": "북부청사방", + "호표": 5, + "호표이름": "배수관날개벽", + "호표규격": "Φ1000mm,지수벽무", + "호표단위": "개소", + "차례": 0, + "이름": "레미콘타설(장비사용)", + "규격": "무근구조물-소량타설", + "수량": 2.5, + "단위": "㎥", + "단가": { + "계": 58626, + "노무비": 46268, + "재료비": 5578, + "경비": 6780 + }, + "갈래": "단가산출", + "참조": { + "종류": "단산", + "번": 21 + }, + "근거": { + "출처": [ + [ + "건설품셈", + "6-1-1" + ] + ], + "직종": [ + "콘크리트공", + "보통인부" + ], + "행": [ + 968, + 1002 + ] + }, + "맞춤": { + "열쇠": "레미콘타설(장비사용)|무근구조물-소량타설|m3", + "대상": null, + "입력값": {}, + "못읽음": [], + "상태": "후보", + "환산": null, + "들어감": false, + "후보": [ + { + "로직": "GC000198", + "이름": "레디믹스트콘크리트 타설", + "단위": "㎥", + "근거": { + "조항": [ + "건설품셈 공통 6-1-1" + ], + "직종": [ + "콘크리트공" + ], + "단위": true, + "점수": 1.75, + "까닭": "조항 같음 · 건설 공통 · 직종 콘크리트공 · 단위 맞음" + }, + "점수": 1.75 + }, + { + "로직": "GC000201", + "이름": "콘크리트 펌프차 타설", + "단위": "㎥", + "근거": { + "조항": [ + "건설품셈 공통 6-1-4" + ], + "직종": [ + "콘크리트공" + ], + "단위": true, + "점수": 1.25, + "까닭": "앞 마디 같음 · 건설 공통 · 직종 콘크리트공 · 단위 맞음" + }, + "점수": 1.567 + }, + { + "로직": "GC000199", + "이름": "현장비빔타설", + "단위": "㎥", + "근거": { + "조항": [ + "건설품셈 공통 6-1-2" + ], + "직종": [ + "콘크리트공" + ], + "단위": true, + "점수": 1.25, + "까닭": "앞 마디 같음 · 건설 공통 · 직종 콘크리트공 · 단위 맞음" + }, + "점수": 1.25 + }, + { + "로직": "GF000182", + "이름": "펌프카 타설(편성 · 일일 시공량)", + "단위": "㎥", + "점수": 0.45 + }, + { + "로직": "GF000166", + "이름": "콘크리트 타설(무근진동기 제외)", + "단위": "㎥", + "점수": 0.422 + } + ] + }, + "단산머리": { + "이름": "레미콘타설(장비사용)(무근구조물-소량타설)", + "단위": "㎥" + }, + "단산글": [ + "21.레미콘타설(장비사용)(무근구조물-소량타설)[㎥]", + " 2024년 품셈개정, 6-1-1콘크리트 타설", + " ○. 레미콘타설(장비사용타설-무근(소량)구조물)", + " 1. 레미콘 : 별도계상", + " 2. 타설비", + " 콘크리트공 : 273,540 × 3 ÷ 63 × 2인 = 26,051.4", + " 보통인부 : 172,068 × 1 ÷ 63 × 2인 = 5,462.4", + " 인부소계 : 31,513.8", + " 3. 공구손료 및 경장비(콘크리트 진동기등)의 기계경비(노무비의 2%)", + " 31,513.8 × 2% = 630.2", + " 4. 굴착기(타이어형)(0.6㎥)[hr]", + " Q = 1 × 8 / 63 × 2 = 0.25hr/㎥", + " 노무비 : 59,020 × 0.25 = 14,755", + " 재료비 : 19,792 × 0.25 = 4,948", + " 경 비 : 27,120 × 0.25 = 6,780", + " 소 계 : 26,483", + " 5. 합 계", + " 노무비 : 31,513.8 + 14,755 = 46,268", + " 재료비 : 630.2 + 4,948 = 5,578", + " 경 비 : 6,780", + " 계 : 58,626" + ], + "줄수": 7, + "호표들": [ + 5, + 10, + 21, + 22, + 23, + 24, + 25 + ], + "id": "S069" + }, + { + "파일": "북부청사방", + "호표": 5, + "호표이름": "배수관날개벽", + "호표규격": "Φ1000mm,지수벽무", + "호표단위": "개소", + "차례": 1, + "이름": "합판거푸집(4회)", + "규격": "", + "수량": 14.31, + "단위": "㎡", + "단가": { + "계": 52640, + "노무비": 38290, + "재료비": 14350, + "경비": null + }, + "갈래": "단가산출", + "참조": { + "종류": "단산", + "번": 66 + }, + "근거": { + "출처": [ + [ + "건설품셈", + "6-3-1" + ] + ], + "직종": [ + "형틀목공", + "보통인부" + ], + "행": [ + 2839, + 2874 + ] + }, + "맞춤": { + "열쇠": "합판거푸집(4회)||m2", + "대상": null, + "입력값": {}, + "못읽음": [], + "상태": "후보", + "환산": null, + "들어감": false, + "후보": [ + { + "로직": "GC000211", + "이름": "합판거푸집 설치 및 해체", + "단위": "㎡", + "근거": { + "조항": [ + "건설품셈 공통 6-3-1" + ], + "직종": [ + "형틀목공" + ], + "단위": true, + "점수": 1.75, + "까닭": "조항 같음 · 건설 공통 · 직종 형틀목공 · 단위 맞음" + }, + "점수": 2.515 + }, + { + "로직": "GF000160", + "이름": "합판거푸집", + "단위": "㎡", + "점수": 2.2 + }, + { + "로직": "GC000216", + "이름": "합성수지(P.E) 원형 맨홀 거푸집 설치 및 해체", + "단위": "개소", + "근거": { + "조항": [ + "건설품셈 공통 6-3-5" + ], + "직종": [], + "단위": false, + "점수": 0.6, + "까닭": "앞 마디 같음 · 건설 공통" + }, + "점수": 0.811 + }, + { + "로직": "GC000215", + "이름": "문양거푸집(판넬) 설치 및 해체", + "단위": "㎡", + "근거": { + "조항": [ + "건설품셈 공통 6-3-4" + ], + "직종": [ + "형틀목공" + ], + "단위": true, + "점수": 1.25, + "까닭": "앞 마디 같음 · 건설 공통 · 직종 형틀목공 · 단위 맞음" + }, + "점수": 1.25 + }, + { + "로직": "GC000212", + "이름": "강재거푸집 인력 설치 및 해체", + "단위": "㎡", + "근거": { + "조항": [ + "건설품셈 공통 6-3-2" + ], + "직종": [ + "형틀목공" + ], + "단위": true, + "점수": 1.25, + "까닭": "앞 마디 같음 · 건설 공통 · 직종 형틀목공 · 단위 맞음" + }, + "점수": 1.25 + } + ] + }, + "단산머리": { + "이름": "합판거푸집(4회)", + "단위": "㎡" + }, + "단산글": [ + "66.합판거푸집(4회)[㎡]", + " 2024년 품셈개정, 6-3-1 합판거푸집", + " ※ 사용횟수 4회 : 보통", + " 1. 재료비(1회의 38%)", + " 내수합판1급(12m/m1220×2440)[㎡] 11,421 × 1.03㎡ × 38% = 4,470.1", + " 각재(외송)(3.6×3.0×3.0)[㎥] 577,844 × 0.038㎥ × 38% = 8,344", + " 자재소계 : 12,814.1", + " 소모자재(박리재 등)(주자재비의 9%)", + " 12,814.1 × 9% = 1,153.2", + " 2. 인건비", + " 형틀목공 : 275,790 × 5 ÷ 45인 = 30,643.3", + " 보통인부 : 172,068 × 2 ÷ 45인 = 7,647.4", + " 인부소계 : 38,290.7", + " 공구손료 및 경장비 기계경비(노무비의 1%)", + " 38,290.7 × 1% = 382.9", + " 3. 합 계", + " 노무비 : 38,290", + " 재료비 : 12,814.1 + 1,153.2 + 382.9 = 14,350", + " 경 비 :", + " 계 : 52,640" + ], + "줄수": 1, + "호표들": [ + 5 + ], + "id": "S070" + }, + { + "파일": "북부청사방", + "호표": 5, + "호표이름": "배수관날개벽", + "호표규격": "Φ1000mm,지수벽무", + "호표단위": "개소", + "차례": 4, + "이름": "기초잡석다짐 및 채집운반", + "규격": "(9:1)(0.6㎥+4.5Ton)", + "수량": 0.85, + "단위": "㎥", + "단가": { + "계": 30971, + "노무비": 23486, + "재료비": 3358, + "경비": 4127 + }, + "갈래": "단가산출", + "참조": { + "종류": "단산", + "번": 12 + }, + "근거": { + "출처": [ + [ + "건설품셈", + "3-4-8" + ] + ], + "직종": [ + "보통인부", + "특별인부" + ], + "행": [ + 441, + 558 + ] + }, + "맞춤": { + "열쇠": "기초잡석다짐및채집운반|(9:1)(0.6m3+4.5ton)|m3", + "대상": null, + "입력값": {}, + "못읽음": [], + "상태": "후보", + "환산": null, + "들어감": false, + "후보": [ + { + "로직": "GC000110", + "이름": "기초지정", + "단위": "㎥", + "근거": { + "조항": [ + "건설품셈 공통 3-4-8" + ], + "직종": [ + "특별인부" + ], + "단위": true, + "점수": 1.75, + "까닭": "조항 같음 · 건설 공통 · 직종 특별인부 · 단위 맞음" + }, + "점수": 1.75 + }, + { + "로직": "GC000106", + "이름": "뒤채움 및 다짐(소형장비)", + "단위": "㎥", + "근거": { + "조항": [ + "건설품셈 공통 3-4-4" + ], + "직종": [ + "특별인부" + ], + "단위": true, + "점수": 1.25, + "까닭": "앞 마디 같음 · 건설 공통 · 직종 특별인부 · 단위 맞음" + }, + "점수": 1.533 + }, + { + "로직": "GC000107", + "이름": "뒤채움 및 다짐(대형장비)", + "단위": "㎥", + "근거": { + "조항": [ + "건설품셈 공통 3-4-5" + ], + "직종": [ + "특별인부" + ], + "단위": true, + "점수": 1.25, + "까닭": "앞 마디 같음 · 건설 공통 · 직종 특별인부 · 단위 맞음" + }, + "점수": 1.533 + }, + { + "로직": "GC000108", + "이름": "되메우기 및 다짐(소형장비)", + "단위": "㎥", + "근거": { + "조항": [ + "건설품셈 공통 3-4-6" + ], + "직종": [ + "특별인부" + ], + "단위": true, + "점수": 1.25, + "까닭": "앞 마디 같음 · 건설 공통 · 직종 특별인부 · 단위 맞음" + }, + "점수": 1.525 + }, + { + "로직": "GF000168", + "이름": "기초잡석(소할 · 적사 · 부설 및 다짐)", + "단위": "㎥", + "점수": 0.662 + } + ] + }, + "단산머리": { + "이름": "기초잡석다짐 및 채집운반((9:1)(0.6㎥+4.5Ton))", + "단위": "㎥" + }, + "단산글": [ + "12.기초잡석다짐 및 채집운반((9:1)(0.6㎥+4.5Ton))[㎥]", + " 1. 채 집 (기계 : 인력 = 9 : 1)", + " 가. 굴착기(타이어형)(0.6㎥)[hr] (90%)", + " q = 0.6 f = 1 / 1.125 = 0.89 k = 0.55", + " Cm = 18sec(90˚) E = 0.35", + " 3600×q×k×f×E 3,600×0.6×0.55×0.89×0.35", + " Q = ────────── = ─────────────── = 20.56㎥/hr", + " Cm 18", + " 노무비 : 59,020 ÷ 20.56 × 0.9 = 2,583.5", + " 재료비 : 19,792 ÷ 20.56 × 0.9 = 866.3", + " 경 비 : 27,120 ÷ 20.56 × 0.9 = 1,187.1", + " 소 계 : 4,636.9", + " 나. 인 력 (10%)", + " 보통인부 : 172,068 × 0.3 × 0.1 = 5,162", + " 다. 소 계", + " 노무비 : 2,583.5 + 5,162 = 7,745.5", + " 재료비 : 866.3", + " 경 비 : 1,187.1", + " 계 : 9,798.9", + " 2. 적 사", + " 굴착기(타이어형)(0.6㎥)[hr]", + " q = 0.6 f = 1 / 1.125 = 0.89 k = 0.7", + " Cm = 20sec(135˚) E = 0.45", + " 3600×q×k×f×E 3,600×0.6×0.7×0.89×0.45", + " Q = ────────── = ────────────── =30.28㎥/hr", + " Cm 20", + " 노무비 : 59,020 ÷ 30.28 = 1,949.1", + " 재료비 : 19,792 ÷ 30.28 = 653.6", + " 경 비 : 27,120 ÷ 30.28 = 895.6", + " 소 계 : 3,498.3", + " 3. 운 반", + " 덤프트럭(4.5Ton)", + " L = 0.5km", + " E = 0.9 f = 1 / 1.125 = 0.89", + " q1 = (4.5 / 1.75) × 1.125 = 2.89", + " n = q1 / (0.6 × k) = 2.89 / (0.6 × 0.7) = 6.88회", + " t1 = 20 × n / (60 × 0.45) = 20 × 6.88 / (60 × 0.45) = 5.1분", + " t2 = (L / 10 + L / 15) × 60 = (0.5 / 10 + 0.5 / 15) × 60 = 5분", + " t3 = 1.1분 t4 = 0.42분", + " Cm = t1 + t2 + t3 + t4 = 5.1 + 5 + 1.1 + 0.42 = 11.62분", + " 60×q1×f×E 60×2.89×0.89×0.9", + " Q = ──────── = ────────── = 11.95㎥/hr", + " Cm 11.62", + " 노무비 : 49,778 ÷ 11.95 = 4,165.5", + " 재료비 : 9,494 ÷ 11.95 = 794.4", + " 경 비 : 7,613 ÷ 11.95 = 637", + " 소 계 : 5,596.9", + " 4. 기초지정 [2025년 표준품셈3-4-8]", + " 가. 굴착기(무한궤도)(0.2㎥)[hr]", + " 1일 시공량 : Qd = 90㎥/일", + " Q = Qd / 8 = 90 / 8 = 11.25㎥/hr", + " 노무비 : 59,020 ÷ 11.25 = 5,246.2", + " 재료비 : 8,324 ÷ 11.25 = 739.9", + " 경 비 : 13,873 ÷ 11.25 = 1,233.1", + " 소 계 : 7,219.2", + " 나. 진동로울러(핸드가이드식)(0.7Ton)", + " 재료비 : 3,420 ÷ 11.25 = 304", + " 경 비 : 1,967 ÷ 11.25 = 174.8", + " 소 계 : 478.8", + " 다. 인 력", + " P = 1인 / Qd = 1 / 90 = 0.011인", + " 특별인부 : 226,122 × P = 226,122 × 0.011 = 2,487.3", + " 보통인부 : 172,068 × P = 172,068 × 0.011 = 1,892.7", + " 인부소계 : 4,380", + " 마. 소 계", + " 노무비 : 5,246.2 + 4,380 = 9,626.2", + " 재료비 : 739.9 + 304 = 1,043.9", + " 경 비 : 1,233.1 + 174.8 = 1,407.9", + " 계 : 12,078", + " 5. 합 계", + " 노무비 : 7,745.5 + 1,949.1 + 4,165.5 + 9,626.2 = 23,486", + " 재료비 : 866.3 + 653.6 + 794.4 + 1,043.9 = 3,358", + " 경 비 : 1,187.1 + 895.6 + 637 + 1,407.9 = 4,127", + " 계 : 30,971" + ], + "줄수": 1, + "호표들": [ + 5 + ], + "id": "S071" + }, + { + "파일": "북부청사방", + "호표": 6, + "호표이름": "배수관부설", + "호표규격": "1000M/M×1연", + "호표단위": "M", + "차례": 0, + "이름": "배수관부설", + "규격": "(D=1000mm)", + "수량": 1, + "단위": "M", + "단가": { + "계": 22429, + "노무비": 21081, + "재료비": 421, + "경비": 927 + }, + "갈래": "단가산출", + "참조": { + "종류": "단산", + "번": 26 + }, + "근거": { + "출처": [ + [ + "건설품셈", + "6-7-2" + ] + ], + "직종": [ + "보통인부" + ], + "행": [ + 1147, + 1183 + ] + }, + "맞춤": { + "열쇠": "배수관부설|(d=1000mm)|m", + "대상": null, + "입력값": {}, + "못읽음": [], + "상태": "후보", + "환산": null, + "들어감": false, + "후보": [ + { + "로직": "GC000404", + "이름": "파형강관 부설 및 접합", + "단위": "본", + "근거": { + "조항": [ + "건설품셈 토목 6-7-2" + ], + "직종": [], + "단위": false, + "점수": 1.1, + "까닭": "조항 같음 · 건설 토목" + }, + "점수": 1.433 + }, + { + "로직": "GC000403", + "이름": "PC관 부설 및 접합", + "단위": "본", + "근거": { + "조항": [ + "건설품셈 토목 6-7-1" + ], + "직종": [], + "단위": false, + "점수": 0.6, + "까닭": "앞 마디 같음 · 건설 토목" + }, + "점수": 0.964 + }, + { + "로직": "GC000405", + "이름": "유리섬유복합관 부설 및 접합", + "단위": "본", + "근거": { + "조항": [ + "건설품셈 토목 6-7-3" + ], + "직종": [], + "단위": false, + "점수": 0.6, + "까닭": "앞 마디 같음 · 건설 토목" + }, + "점수": 0.867 + }, + { + "로직": "GC000406", + "이름": "내충격PVC수도관 부설 및 접합", + "단위": "본", + "근거": { + "조항": [ + "건설품셈 토목 6-7-4" + ], + "직종": [], + "단위": false, + "점수": 0.6, + "까닭": "앞 마디 같음 · 건설 토목" + }, + "점수": 0.835 + }, + { + "로직": "GF000174", + "이름": "흄관 부설", + "단위": "m", + "점수": 0.771 + } + ] + }, + "단산머리": { + "이름": "배수관부설(D=1000mm)", + "단위": "M" + }, + "단산글": [ + "26.배수관부설(D=1000mm)[M]", + " * 배수관부설 [건설표준품셈(6-7-2)]", + " 파형강관, D = 1000mm 별도계상", + " 1. 재료비", + " 파형강관(D = 1000mm) : 1m", + " 2. 인건비", + " 배관공(수도) : 262,580 × 0.32인 ÷ 6 = 14,004.2", + " 보 통 인 부 : 172,068 × 0.16인 ÷ 6 = 4,588.4", + " 인부소계 : 18,592.6", + " 3. 공구손료 및 잡재료비(인력품의 2%)", + " 18,592.6 × 2% = 371.8", + " 4. 트럭탑재형크레인(5Ton)", + " Q = 0.3hr / 6 = 0.05", + " 노무비 : 49,778 × 0.05 = 2,488.9", + " 재료비 : 8,421 × 0.05 = 421", + " 경 비 : 11,117 × 0.05 = 555.8", + " 소 계 : 3,465.7", + " 5. 합 계", + " 노무비 : 18,592.6 + 2,488.9 = 21,081", + " 재료비 : 421", + " 경 비 : 371.8 + 555.8 = 927", + " 계 : 22,429" + ], + "줄수": 1, + "호표들": [ + 6 + ], + "id": "S072" + }, + { + "파일": "북부청사방", + "호표": 7, + "호표이름": "앵커볼트설치", + "호표규격": "", + "호표단위": "개", + "차례": 0, + "이름": "철골공", + "규격": "", + "수량": 0.12, + "단위": "인", + "단가": { + "계": 252641, + "노무비": 252641, + "재료비": null, + "경비": null + }, + "갈래": "기타", + "참조": null, + "근거": null, + "맞춤": { + "열쇠": "철골공||인", + "대상": null, + "입력값": {}, + "못읽음": [], + "상태": "후보", + "환산": null, + "들어감": false, + "후보": [ + { + "로직": "GC000560", + "이름": "철골 공장용접(기본용접공수)", + "단위": "t", + "점수": 0.571 + }, + { + "로직": "GC000559", + "이름": "철골 공장가공조립(기본철골공수)", + "단위": "t", + "점수": 0.444 + }, + { + "로직": "GC000565", + "이름": "철골 현장용접(CO2 반자동)", + "단위": "m", + "점수": 0.286 + }, + { + "로직": "GC000561", + "이름": "철골 현장 세우기", + "단위": "ton", + "점수": 0.25 + }, + { + "로직": "GC000566", + "이름": "철골 앵커볼트 설치", + "단위": "개", + "점수": 0.222 + } + ] + }, + "줄수": 1, + "호표들": [ + 7 + ], + "id": "S073" + }, + { + "파일": "북부청사방", + "호표": 7, + "호표이름": "앵커볼트설치", + "호표규격": "", + "호표단위": "개", + "차례": 1, + "이름": "특별인부", + "규격": "", + "수량": 0.05, + "단위": "인", + "단가": { + "계": 226122, + "노무비": 226122, + "재료비": null, + "경비": null + }, + "갈래": "기타", + "참조": null, + "근거": null, + "맞춤": { + "열쇠": "특별인부||인", + "대상": null, + "입력값": {}, + "못읽음": [], + "상태": "없음", + "환산": null, + "들어감": false, + "후보": [] + }, + "줄수": 1, + "호표들": [ + 7 + ], + "id": "S074" + }, + { + "파일": "북부청사방", + "호표": 8, + "호표이름": "원주목울타리", + "호표규격": "1500×1500", + "호표단위": "경간", + "차례": 1, + "이름": "레미콘타설(인력운반)", + "규격": "무근구조물-소량타설", + "수량": 0.05, + "단위": "㎥", + "단가": { + "계": 118569, + "노무비": 116245, + "재료비": 2324, + "경비": null + }, + "갈래": "단가산출", + "참조": { + "종류": "단산", + "번": 19 + }, + "근거": { + "출처": [ + [ + "건설품셈", + "6-1-1" + ] + ], + "직종": [ + "콘크리트공", + "보통인부" + ], + "행": [ + 903, + 928 + ] + }, + "맞춤": { + "열쇠": "레미콘타설(인력운반)|무근구조물-소량타설|m3", + "대상": null, + "입력값": {}, + "못읽음": [], + "상태": "후보", + "환산": null, + "들어감": false, + "후보": [ + { + "로직": "GC000198", + "이름": "레디믹스트콘크리트 타설", + "단위": "㎥", + "근거": { + "조항": [ + "건설품셈 공통 6-1-1" + ], + "직종": [ + "콘크리트공" + ], + "단위": true, + "점수": 1.75, + "까닭": "조항 같음 · 건설 공통 · 직종 콘크리트공 · 단위 맞음" + }, + "점수": 1.75 + }, + { + "로직": "GC000201", + "이름": "콘크리트 펌프차 타설", + "단위": "㎥", + "근거": { + "조항": [ + "건설품셈 공통 6-1-4" + ], + "직종": [ + "콘크리트공" + ], + "단위": true, + "점수": 1.25, + "까닭": "앞 마디 같음 · 건설 공통 · 직종 콘크리트공 · 단위 맞음" + }, + "점수": 1.567 + }, + { + "로직": "GC000199", + "이름": "현장비빔타설", + "단위": "㎥", + "근거": { + "조항": [ + "건설품셈 공통 6-1-2" + ], + "직종": [ + "콘크리트공" + ], + "단위": true, + "점수": 1.25, + "까닭": "앞 마디 같음 · 건설 공통 · 직종 콘크리트공 · 단위 맞음" + }, + "점수": 1.25 + }, + { + "로직": "GF000182", + "이름": "펌프카 타설(편성 · 일일 시공량)", + "단위": "㎥", + "점수": 0.45 + }, + { + "로직": "GF000166", + "이름": "콘크리트 타설(무근진동기 제외)", + "단위": "㎥", + "점수": 0.422 + } + ] + }, + "단산머리": { + "이름": "레미콘타설(인력운반)(무근구조물-소량타설)", + "단위": "㎥" + }, + "단산글": [ + "19.레미콘타설(인력운반)(무근구조물-소량타설)[㎥]", + " 2024년 품셈개정, 6-1-1콘크리트 타설", + " ○. 레미콘타설(장비사용타설-무근구조물(소량타설)", + " 1. 레미콘 : 별도계상", + " 2. 타설비", + " 콘크리트공 : 273,540 × 3 ÷ 23 × 2인 = 71,358.2", + " 보 통 인부 : 172,068 × 3 ÷ 23 × 2인 = 44,887.3", + " 인부소계 : 116,245.5", + " 3. 공구손료 및 경장비(콘크리트 진동기등)의 기계경비(노무비의 2%)", + " 116,245.5 × 2% = 2,324.9", + " 4. 합 계", + " 노무비 : 116,245", + " 재료비 : 2,324", + " 경 비 :", + " 계 : 118,569" + ], + "줄수": 1, + "호표들": [ + 8 + ], + "id": "S075" + }, + { + "파일": "북부청사방", + "호표": 8, + "호표이름": "원주목울타리", + "호표규격": "1500×1500", + "호표단위": "경간", + "차례": 6, + "이름": "구조물터파기", + "규격": "인력", + "수량": 0.1, + "단위": "㎥", + "단가": { + "계": 79901, + "노무비": 79901, + "재료비": null, + "경비": null + }, + "갈래": "단가산출", + "참조": { + "종류": "단산", + "번": 9 + }, + "근거": { + "출처": [ + [ + "건설품셈", + "3-2-1" + ] + ], + "직종": [ + "특별인부" + ], + "행": [ + 316, + 333 + ] + }, + "맞춤": { + "열쇠": "구조물터파기|인력|m3", + "대상": null, + "입력값": {}, + "못읽음": [], + "상태": "후보", + "환산": null, + "들어감": false, + "후보": [ + { + "로직": "GC000083", + "이름": "굴착(인력/토사)", + "단위": "㎥", + "근거": { + "조항": [ + "건설품셈 공통 3-2-1" + ], + "직종": [ + "특별인부" + ], + "단위": true, + "점수": 1.75, + "까닭": "조항 같음 · 건설 공통 · 직종 특별인부 · 단위 맞음" + }, + "점수": 1.75 + }, + { + "로직": "GC000090", + "이름": "터파기(기계) 암", + "단위": "㎥", + "근거": { + "조항": [ + "건설품셈 공통 3-2-4" + ], + "직종": [], + "단위": true, + "점수": 0.75, + "까닭": "앞 마디 같음 · 건설 공통 · 단위 맞음" + }, + "점수": 1.4 + }, + { + "로직": "GC000088", + "이름": "터파기(기계) 보통토사", + "단위": "㎥", + "근거": { + "조항": [ + "건설품셈 공통 3-2-4" + ], + "직종": [], + "단위": true, + "점수": 0.75, + "까닭": "앞 마디 같음 · 건설 공통 · 단위 맞음" + }, + "점수": 1.264 + }, + { + "로직": "GC000089", + "이름": "터파기(기계) 혼합토사", + "단위": "㎥", + "근거": { + "조항": [ + "건설품셈 공통 3-2-4" + ], + "직종": [], + "단위": true, + "점수": 0.75, + "까닭": "앞 마디 같음 · 건설 공통 · 단위 맞음" + }, + "점수": 1.264 + }, + { + "로직": "GF000101", + "이름": "구조물터파기 육상토사(0~1m)", + "단위": "㎥", + "점수": 0.914 + } + ] + }, + "단산머리": { + "이름": "구조물터파기(인력)", + "단위": "㎥" + }, + "단산글": [ + "9.구조물터파기(인력)[㎥]", + " [2025년 표준품셈 3-2-1]", + " 보통토사 + 경질토사 + 고사점토 및 자갈섞인토사 적용", + " 1. 인 부", + " 1일 시공량 : Q = (3.6 + 2.7 + 2.2) / 3 = 2.83㎥/일", + " 특별인부 : 226,122 / 2.83 = 79,901.7", + " 2. 합 계", + " 노무비 : 79,901", + " 재료비 :", + " 경 비 :", + " 계 : 79,901" + ], + "줄수": 1, + "호표들": [ + 8 + ], + "id": "S076" + }, + { + "파일": "북부청사방", + "호표": 8, + "호표이름": "원주목울타리", + "호표규격": "1500×1500", + "호표단위": "경간", + "차례": 7, + "이름": "구조물되메우기", + "규격": "인력", + "수량": 0.05, + "단위": "㎥", + "단가": { + "계": 17206, + "노무비": 17206, + "재료비": null, + "경비": null + }, + "갈래": "단가산출", + "참조": { + "종류": "단산", + "번": 7 + }, + "근거": { + "출처": [], + "직종": [ + "보통인부" + ], + "행": [ + 249, + 260 + ] + }, + "맞춤": { + "열쇠": "구조물되메우기|인력|m3", + "대상": null, + "입력값": {}, + "못읽음": [], + "상태": "후보", + "환산": null, + "들어감": false, + "후보": [ + { + "로직": "GF000113", + "이름": "되메우기", + "단위": "㎥", + "점수": 0.867 + }, + { + "로직": "GF000194", + "이름": "되메우기(노상재)", + "단위": "㎡", + "점수": 0.717 + }, + { + "로직": "GC000108", + "이름": "되메우기 및 다짐(소형장비)", + "단위": "㎥", + "점수": 0.65 + }, + { + "로직": "GC000109", + "이름": "되메우기 및 다짐(대형장비)", + "단위": "㎥", + "점수": 0.65 + }, + { + "로직": "GC000451", + "이름": "지하수 폐공 되메우기", + "단위": "m", + "점수": 0.429 + } + ] + }, + "단산머리": { + "이름": "구조물되메우기(인력)", + "단위": "㎥" + }, + "단산글": [ + "7.구조물되메우기(인력)[㎥]", + " 1. 인 부", + " 보통인부 : 172,068 × 0.1 = 17,206.8", + " 2. 합 계", + " 노무비 : 17,206", + " 재료비 :", + " 경 비 :", + " 계 : 17,206" + ], + "줄수": 1, + "호표들": [ + 8 + ], + "id": "S077" + }, + { + "파일": "북부청사방", + "호표": 8, + "호표이름": "원주목울타리", + "호표규격": "1500×1500", + "호표단위": "경간", + "차례": 8, + "이름": "잔토처리", + "규격": "인력", + "수량": 0.05, + "단위": "㎥", + "단가": { + "계": 34413, + "노무비": 34413, + "재료비": null, + "경비": null + }, + "갈래": "단가산출", + "참조": { + "종류": "단산", + "번": 40 + }, + "근거": { + "출처": [], + "직종": [ + "보통인부" + ], + "행": [ + 1692, + 1705 + ] + }, + "맞춤": { + "열쇠": "잔토처리|인력|m3", + "대상": null, + "입력값": {}, + "못읽음": [], + "상태": "없음", + "환산": null, + "들어감": false, + "후보": [] + }, + "단산머리": { + "이름": "잔토처리(인력)", + "단위": "㎥" + }, + "단산글": [ + "40.잔토처리(인력)[㎥]", + " * 현장내(소운반 깔고고르기포함)", + " 1. 인 력", + " 보통인부 : 172,068 × 0.2인 = 34,413.6원", + " 2. 합 계", + " 노무비 : 34,413", + " 재료비 :", + " 경 비 :", + " 계 : 34,413" + ], + "줄수": 1, + "호표들": [ + 8 + ], + "id": "S078" + }, + { + "파일": "북부청사방", + "호표": 9, + "호표이름": "원형돌망태골막이", + "호표규격": "", + "호표단위": "개소", + "차례": 0, + "이름": "원형돌망태조립설치", + "규격": "산림사업품셈", + "수량": 54, + "단위": "조", + "단가": { + "계": 5386, + "노무비": 5386, + "재료비": null, + "경비": null + }, + "갈래": "단가산출", + "참조": { + "종류": "단산", + "번": 35 + }, + "근거": { + "출처": [ + [ + "산림품셈", + "13-11-1" + ] + ], + "직종": [ + "보통인부", + "특별인부" + ], + "행": [ + 1543, + 1562 + ] + }, + "맞춤": { + "열쇠": "원형돌망태조립설치|산림사업품셈|조", + "대상": null, + "입력값": {}, + "못읽음": [], + "상태": "후보", + "환산": null, + "들어감": false, + "후보": [ + { + "로직": "GF000328", + "이름": "원형 돌망태", + "단위": "㎡", + "근거": { + "조항": [ + "산림품셈 13-11-1" + ], + "직종": [ + "특별인부" + ], + "단위": false, + "점수": 1.5, + "까닭": "조항 같음 · 직종 특별인부" + }, + "점수": 2.217 + }, + { + "로직": "GF000329", + "이름": "타원형 돌망태", + "단위": "㎡", + "근거": { + "조항": [ + "산림품셈 13-11-2" + ], + "직종": [ + "특별인부" + ], + "단위": false, + "점수": 1.0, + "까닭": "앞 마디 같음 · 직종 특별인부" + }, + "점수": 1.665 + }, + { + "로직": "GF000235", + "이름": "사각형 돌망태", + "단위": "㎥", + "근거": { + "조항": [ + "산림품셈 13-11-4" + ], + "직종": [ + "특별인부" + ], + "단위": false, + "점수": 1.0, + "까닭": "앞 마디 같음 · 직종 특별인부" + }, + "점수": 1.512 + }, + { + "로직": "GF000234", + "이름": "매트리스형 돌망태", + "단위": "㎡", + "근거": { + "조항": [ + "산림품셈 13-11-3" + ], + "직종": [ + "특별인부" + ], + "단위": false, + "점수": 1.0, + "까닭": "앞 마디 같음 · 직종 특별인부" + }, + "점수": 1.45 + }, + { + "로직": "GC000340", + "이름": "타원형 돌망태 설치", + "단위": "㎡", + "점수": 0.667 + } + ] + }, + "단산머리": { + "이름": "원형돌망태조립설치(산림사업품셈)", + "단위": "조" + }, + "단산글": [ + "35.원형돌망태조립설치(산림사업품셈)[조]", + " 1. 돌망태 조립설치", + " [2026년 산림사업표준품셈 13-11-1]", + " * 원형돌망태 조립설치", + " 보통인부 : 172,068 × 0.45m × 1m × 1조 × 0.017인/㎡ = 1,316.3", + " 특별인부 : 226,122 × 0.45m × 1m × 0.04인/㎡ = 4,070.1", + " 인부소계 : 5,386.4", + " 2. 합 계", + " 노무비 : 5,386", + " 재료비 :", + " 경 비 :", + " 계 : 5,386" + ], + "줄수": 1, + "호표들": [ + 9 + ], + "id": "S079" + }, + { + "파일": "북부청사방", + "호표": 9, + "호표이름": "원형돌망태골막이", + "호표규격": "", + "호표단위": "개소", + "차례": 1, + "이름": "원형돌망태채움", + "규격": "산림사업품셈", + "수량": 54, + "단위": "조", + "단가": { + "계": 6719, + "노무비": 5879, + "재료비": 441, + "경비": 399 + }, + "갈래": "단가산출", + "참조": { + "종류": "단산", + "번": 36 + }, + "근거": { + "출처": [ + [ + "산림품셈", + "13-11-1" + ] + ], + "직종": [ + "석공" + ], + "행": [ + 1565, + 1596 + ] + }, + "맞춤": { + "열쇠": "원형돌망태채움|산림사업품셈|조", + "대상": null, + "입력값": {}, + "못읽음": [], + "상태": "후보", + "환산": null, + "들어감": false, + "후보": [ + { + "로직": "GF000328", + "이름": "원형 돌망태", + "단위": "㎡", + "근거": { + "조항": [ + "산림품셈 13-11-1" + ], + "직종": [ + "석공" + ], + "단위": false, + "점수": 1.5, + "까닭": "조항 같음 · 직종 석공" + }, + "점수": 2.35 + }, + { + "로직": "GF000329", + "이름": "타원형 돌망태", + "단위": "㎡", + "근거": { + "조항": [ + "산림품셈 13-11-2" + ], + "직종": [ + "석공" + ], + "단위": false, + "점수": 1.0, + "까닭": "앞 마디 같음 · 직종 석공" + }, + "점수": 1.777 + }, + { + "로직": "GF000235", + "이름": "사각형 돌망태", + "단위": "㎥", + "근거": { + "조항": [ + "산림품셈 13-11-4" + ], + "직종": [ + "석공" + ], + "단위": false, + "점수": 1.0, + "까닭": "앞 마디 같음 · 직종 석공" + }, + "점수": 1.595 + }, + { + "로직": "GF000234", + "이름": "매트리스형 돌망태", + "단위": "㎡", + "근거": { + "조항": [ + "산림품셈 13-11-3" + ], + "직종": [], + "단위": false, + "점수": 0.5, + "까닭": "앞 마디 같음" + }, + "점수": 1.012 + }, + { + "로직": "GC000340", + "이름": "타원형 돌망태 설치", + "단위": "㎡", + "점수": 0.615 + } + ] + }, + "단산머리": { + "이름": "원형돌망태채움(산림사업품셈)", + "단위": "조" + }, + "단산글": [ + "36.원형돌망태채움(산림사업품셈)[조]", + " 1. 돌망태 돌채움", + " [2026년 산림사업표준품셈 13-11-1]", + " * 원형돌망태 채움", + " 가. 인 력", + " 석공 : 268,908 × 0.45m × 1m × 1조 × 0.042인/㎡ = 5,082.3", + " 나. 굴착기(무한궤도)(1.0㎥)[hr]", + " 노무비 : 59,020 × 0.03 × 0.45m × 1m × 1조 = 796.7", + " 재료비 : 32,735 × 0.03 × 0.45m × 1m × 1조 = 441.9", + " 경 비 : 29,621 × 0.03 × 0.45m × 1m × 1조 = 399.8", + " 소 계 : 1,638.4", + " 다. 소 계", + " 노무비 : 5,082.3 + 796.7 = 5,879", + " 재료비 : 441.9", + " 경 비 : 399.8", + " 계 : 6,720.7", + " 2. 합 계", + " 노무비 : 5,879", + " 재료비 : 441", + " 경 비 : 399", + " 계 : 6,719" + ], + "줄수": 1, + "호표들": [ + 9 + ], + "id": "S080" + }, + { + "파일": "북부청사방", + "호표": 9, + "호표이름": "원형돌망태골막이", + "호표규격": "", + "호표단위": "개소", + "차례": 2, + "이름": "조약돌채집운반", + "규격": "(9:1)(0.6㎥+4.5Ton)", + "수량": 7.04, + "단위": "㎥", + "단가": { + "계": 24055, + "노무비": 19022, + "재료비": 2314, + "경비": 2719 + }, + "갈래": "단가산출", + "참조": { + "종류": "단산", + "번": 43 + }, + "근거": { + "출처": [], + "직종": [ + "보통인부" + ], + "행": [ + 1791, + 1874 + ] + }, + "맞춤": { + "열쇠": "조약돌채집운반|(9:1)(0.6m3+4.5ton)|m3", + "대상": null, + "입력값": {}, + "못읽음": [], + "상태": "후보", + "환산": null, + "들어감": false, + "후보": [ + { + "로직": "GF000212", + "이름": "막돌 채집", + "단위": "㎡ · ㎥", + "점수": 0.644 + }, + { + "로직": "GF000211", + "이름": "모래·자갈·약돌 채집(인력)", + "단위": "㎥", + "점수": 0.6 + }, + { + "로직": "GF000213", + "이름": "큰돌 채집(기계)", + "단위": "개", + "점수": 0.494 + }, + { + "로직": "GC000363", + "이름": "자갈채집 및 운반", + "단위": "㎥", + "점수": 0.483 + }, + { + "로직": "GF000149", + "이름": "불도저 운반", + "단위": "㎥", + "점수": 0.4 + } + ] + }, + "단산머리": { + "이름": "조약돌채집운반((9:1)(0.6㎥+4.5Ton))", + "단위": "㎥" + }, + "단산글": [ + "43.조약돌채집운반((9:1)(0.6㎥+4.5Ton))[㎥]", + " 1. 채 집", + " 가. 굴착기(타이어형)(0.6㎥)[hr] : 90%", + " q = 0.6 f = 1 / 1.125 = 0.89 k = 0.55", + " Cm = 18sec(90˚) E = 0.35", + " 3600×q×k×f×E 3,600×0.6×0.55×0.89×0.35", + " Q = ────────── = ─────────────── = 20.56 ㎥/hr", + " Cm 18", + " 노무비 : 59,020 ÷ 20.56 × 0.9 = 2,583.5", + " 재료비 : 19,792 ÷ 20.56 × 0.9 = 866.3", + " 경 비 : 27,120 ÷ 20.56 × 0.9 = 1,187.1", + " 소 계 : 4,636.9", + " 나. 인 력 (10%)", + " 보통인부 : 172,068 × 0.6 × 0.1 = 10,324", + " 다. 소 계", + " 노무비 : 2,583.5 + 10,324 = 12,907.5", + " 재료비 : 866.3", + " 경 비 : 1,187.1", + " 계 : 14,960.9", + " 2. 적 사", + " 굴착기(타이어형)(0.6㎥)[hr]", + " q = 0.6 f = 1 / 1.125 = 0.89 k = 0.7", + " Cm = 20sec(135˚) E = 0.45", + " 3600×q×k×f×E 3,600×0.6×0.7×0.89×0.45", + " Q = ────────── = ────────────── = 30.28 ㎥/hr", + " Cm 20", + " 노무비 : 59,020 ÷ 30.28 = 1,949.1", + " 재료비 : 19,792 ÷ 30.28 = 653.6", + " 경 비 : 27,120 ÷ 30.28 = 895.6", + " 소 계 : 3,498.3", + " 3. 운 반", + " 덤프트럭(4.5Ton)", + " L = 0.5km", + " E = 0.9 f = 1 / 1.125 = 0.89", + " q1 = (4.5 / 1.75) × 1.125 = 2.89", + " n = q1 / (0.6 × k) = 2.89 / (0.6 × 0.7) = 6.88 회", + " t1 = 20 × n / (60 × 0.45) = 20 × 6.88 / (60 × 0.45) = 5.1분", + " L L 0.5 0.5", + " t2 = (──+──)×60 = (──+──)×60 = 5분", + " 10 15 10 15", + " t3 = 1.1분 t4 = 0.42분", + " Cm = t1 + t2 + t3 + t4 = 5.1 + 5 + 1.1 + 0.42 = 11.62 분", + " 60×q1×f×E 60×2.89×0.89×0.9", + " Q = ──────── = ────────── = 11.95㎥/hr", + " Cm 11.62", + " 노무비 : 49,778 ÷ 11.95 = 4,165.5", + " 재료비 : 9,494 ÷ 11.95 = 794.4", + " 경 비 : 7,613 ÷ 11.95 = 637", + " 소 계 : 5,596.9", + " 5. 합 계", + " 노무비 : 12,907.5 + 1,949.1 + 4,165.5 = 19,022", + " 재료비 : 866.3 + 653.6 + 794.4 = 2,314", + " 경 비 : 1,187.1 + 895.6 + 637 = 2,719", + " 계 : 24,055" + ], + "줄수": 1, + "호표들": [ + 9 + ], + "id": "S081" + }, + { + "파일": "북부청사방", + "호표": 10, + "호표이름": "유수배수관부설", + "호표규격": "500M/M×1연(물돌림)", + "호표단위": "M", + "차례": 1, + "이름": "구조물터파기(암)", + "규격": "", + "수량": 0.188, + "단위": "㎥", + "단가": { + "계": 53343, + "노무비": 22081, + "재료비": 11710, + "경비": 19552 + }, + "갈래": "단가산출", + "참조": { + "종류": "단산", + "번": 10 + }, + "근거": { + "출처": [ + [ + "건설품셈", + "3-2-4" + ], + [ + "건설품셈", + "8-2-3" + ], + [ + "건설품셈", + "1-2-3" + ] + ], + "직종": [], + "행": [ + 336, + 394 + ] + }, + "맞춤": { + "열쇠": "구조물터파기(암)||m3", + "대상": null, + "입력값": {}, + "못읽음": [], + "상태": "후보", + "환산": null, + "들어감": false, + "후보": [ + { + "로직": "GC000090", + "이름": "터파기(기계) 암", + "단위": "㎥", + "근거": { + "조항": [ + "건설품셈 공통 3-2-4" + ], + "직종": [], + "단위": true, + "점수": 1.25, + "까닭": "조항 같음 · 건설 공통 · 단위 맞음" + }, + "점수": 1.9 + }, + { + "로직": "GC000088", + "이름": "터파기(기계) 보통토사", + "단위": "㎥", + "근거": { + "조항": [ + "건설품셈 공통 3-2-4" + ], + "직종": [], + "단위": true, + "점수": 1.25, + "까닭": "조항 같음 · 건설 공통 · 단위 맞음" + }, + "점수": 1.764 + }, + { + "로직": "GC000089", + "이름": "터파기(기계) 혼합토사", + "단위": "㎥", + "근거": { + "조항": [ + "건설품셈 공통 3-2-4" + ], + "직종": [], + "단위": true, + "점수": 1.25, + "까닭": "조항 같음 · 건설 공통 · 단위 맞음" + }, + "점수": 1.764 + }, + { + "로직": "GF000101", + "이름": "구조물터파기 육상토사(0~1m)", + "단위": "㎥", + "점수": 0.914 + }, + { + "로직": "GF000103", + "이름": "구조물터파기 육상토사(1~2m)", + "단위": "㎥", + "점수": 0.914 + } + ] + }, + "단산머리": { + "이름": "구조물터파기(암)", + "단위": "㎥" + }, + "단산글": [ + "10.구조물터파기(암)[㎥]", + " * 구조물터파기 - 풍화암+연암+보통암+경암 [2025년 표준품셈3-2-4]", + " * TypeⅡ(연속작업 곤란) 반영", + " 1일 시공량 : Qd = (35+28+19+14)÷4 = 24㎥/일 (풍화암+연암+보통암+경암 기준)", + " Q = Qd ÷ 8hr = 24 ÷ 8 = 3㎥/hr", + " 1. 기계경비(굴착기 1.0㎥+대형 브레이커)", + " 가. 암절취 : 굴착기(무한궤도)(1.0+브레이카)", + " 노무비 : 59,020 ÷ 3 = 19,673.3", + " 재료비 : 31,125 ÷ 3 = 10,375", + " 경 비 : 48,560 ÷ 3 = 16,186.6", + " 소 계 : 46,234.9", + " 나. 치즐 손료(기계경비의 (2+5+7)/2%)", + " 소모자재", + " (19,673.3 + 10,375 + 16,186.6) × 4.667% = 2,157.7", + " 다. 소 계", + " 노무비 : 19,673.3", + " 재료비 : 10,375", + " 경 비 : 16,186.6 + 2,157.7 = 18,344.3", + " 계 : 48,392.6", + " 2. 집 토 : 굴착기(무한궤도)(1.0㎥)", + " [2025년 표준품셈8-2-3] 파쇄암", + " * 체적환산계수 [2025년 표준품셈1-2-3] 풍화암+연암+보통암+경암 반영", + " q = 1 E = (0.45 + 0.35) / 2 = 0.4 K = 0.55", + " f = 1 / ((1.325+1.4+1.625+1.85) / 4) = 0.65", + " Cm = 21sec (135˚)", + " 3600×q×K×f×E 3,600×1×0.55×0.65×0.4", + " Q = ────────── = ────────────── = 24.51㎥/hr", + " Cm 21", + " 노무비 : 59,020 ÷ 24.51 = 2,407.9", + " 재료비 : 32,735 ÷ 24.51 = 1,335.5", + " 경 비 : 29,621 ÷ 24.51 = 1,208.5", + " 소 계 : 4,951.9", + " 3. 합 계", + " 노무비 : 19,673.3 + 2,407.9 = 22,081", + " 재료비 : 10,375 + 1,335.5 = 11,710", + " 경 비 : 18,344.3 + 1,208.5 = 19,552", + " 계 : 53,343" + ], + "줄수": 2, + "호표들": [ + 10, + 11 + ], + "id": "S082" + }, + { + "파일": "북부청사방", + "호표": 10, + "호표이름": "유수배수관부설", + "호표규격": "500M/M×1연(물돌림)", + "호표단위": "M", + "차례": 4, + "이름": "배수관부설", + "규격": "(D=500mm)", + "수량": 1, + "단위": "M", + "단가": { + "계": 9928, + "노무비": 9189, + "재료비": 252, + "경비": 487 + }, + "갈래": "단가산출", + "참조": { + "종류": "단산", + "번": 27 + }, + "근거": { + "출처": [ + [ + "건설품셈", + "6-7-2" + ] + ], + "직종": [ + "보통인부" + ], + "행": [ + 1186, + 1222 + ] + }, + "맞춤": { + "열쇠": "배수관부설|(d=500mm)|m", + "대상": null, + "입력값": {}, + "못읽음": [], + "상태": "후보", + "환산": null, + "들어감": false, + "후보": [ + { + "로직": "GC000404", + "이름": "파형강관 부설 및 접합", + "단위": "본", + "근거": { + "조항": [ + "건설품셈 토목 6-7-2" + ], + "직종": [], + "단위": false, + "점수": 1.1, + "까닭": "조항 같음 · 건설 토목" + }, + "점수": 1.433 + }, + { + "로직": "GC000403", + "이름": "PC관 부설 및 접합", + "단위": "본", + "근거": { + "조항": [ + "건설품셈 토목 6-7-1" + ], + "직종": [], + "단위": false, + "점수": 0.6, + "까닭": "앞 마디 같음 · 건설 토목" + }, + "점수": 0.964 + }, + { + "로직": "GC000405", + "이름": "유리섬유복합관 부설 및 접합", + "단위": "본", + "근거": { + "조항": [ + "건설품셈 토목 6-7-3" + ], + "직종": [], + "단위": false, + "점수": 0.6, + "까닭": "앞 마디 같음 · 건설 토목" + }, + "점수": 0.867 + }, + { + "로직": "GC000406", + "이름": "내충격PVC수도관 부설 및 접합", + "단위": "본", + "근거": { + "조항": [ + "건설품셈 토목 6-7-4" + ], + "직종": [], + "단위": false, + "점수": 0.6, + "까닭": "앞 마디 같음 · 건설 토목" + }, + "점수": 0.835 + }, + { + "로직": "GF000174", + "이름": "흄관 부설", + "단위": "m", + "점수": 0.771 + } + ] + }, + "단산머리": { + "이름": "배수관부설(D=500mm)", + "단위": "M" + }, + "단산글": [ + "27.배수관부설(D=500mm)[M]", + " * 배수관부설 [건설표준품셈(6-7-2)]", + " 파형강관, D = 500mm", + " 1. 재료비(별도계상)", + " 파형강관(D=500mm) : 1m", + " 2. 인건비", + " 배관공(수도) : 262,580 × 0.13인 ÷ 6 = 5,689.2", + " 보 통 인 부 : 172,068 × 0.07인 ÷ 6 = 2,007.4", + " 인부소계 : 7,696.6", + " 3. 공구손료 및 잡재료비(인력품의 2%)", + " 7,696.6 × 2% = 153.9", + " 4. 트럭탑재형크레인(5Ton)", + " Q = 0.18hr / 6 = 0.03", + " 노무비 : 49,778 × 0.03 = 1,493.3", + " 재료비 : 8,421 × 0.03 = 252.6", + " 경 비 : 11,117 × 0.03 = 333.5", + " 소 계 : 2,079.4", + " 5. 합 계", + " 노무비 : 7,696.6 + 1,493.3 = 9,189", + " 재료비 : 252", + " 경 비 : 153.9 + 333.5 = 487", + " 계 : 9,928" + ], + "줄수": 1, + "호표들": [ + 10 + ], + "id": "S083" + }, + { + "파일": "북부청사방", + "호표": 11, + "호표이름": "콘크리트골막이", + "호표규격": "", + "호표단위": "개소", + "차례": 1, + "이름": "레미콘타설(무근펌프카)(130㎥)", + "규격": "41m(80-95㎥/hr)", + "수량": 234.32, + "단위": "㎥", + "단가": { + "계": 23710, + "노무비": 14757, + "재료비": 3219, + "경비": 5734 + }, + "갈래": "단가산출", + "참조": { + "종류": "단산", + "번": 18 + }, + "근거": { + "출처": [ + [ + "건설품셈", + "6-1-4" + ] + ], + "직종": [ + "콘크리트공", + "특별인부", + "보통인부" + ], + "행": [ + 858, + 900 + ] + }, + "맞춤": { + "열쇠": "레미콘타설(무근펌프카)(130m3)|41m(80-95m3/hr)|m3", + "대상": null, + "입력값": {}, + "못읽음": [], + "상태": "후보", + "환산": null, + "들어감": false, + "후보": [ + { + "로직": "GC000201", + "이름": "콘크리트 펌프차 타설", + "단위": "㎥", + "근거": { + "조항": [ + "건설품셈 공통 6-1-4" + ], + "직종": [ + "콘크리트공", + "특별인부" + ], + "단위": true, + "점수": 1.75, + "까닭": "조항 같음 · 건설 공통 · 직종 콘크리트공 · 특별인부 · 단위 맞음" + }, + "점수": 2.067 + }, + { + "로직": "GC000198", + "이름": "레디믹스트콘크리트 타설", + "단위": "㎥", + "근거": { + "조항": [ + "건설품셈 공통 6-1-1" + ], + "직종": [ + "콘크리트공" + ], + "단위": true, + "점수": 1.25, + "까닭": "앞 마디 같음 · 건설 공통 · 직종 콘크리트공 · 단위 맞음" + }, + "점수": 1.25 + }, + { + "로직": "GC000199", + "이름": "현장비빔타설", + "단위": "㎥", + "근거": { + "조항": [ + "건설품셈 공통 6-1-2" + ], + "직종": [ + "콘크리트공" + ], + "단위": true, + "점수": 1.25, + "까닭": "앞 마디 같음 · 건설 공통 · 직종 콘크리트공 · 단위 맞음" + }, + "점수": 1.25 + }, + { + "로직": "GC000204", + "이름": "콘크리트 치핑", + "단위": "㎡", + "근거": { + "조항": [ + "건설품셈 공통 6-1-6" + ], + "직종": [ + "특별인부" + ], + "단위": false, + "점수": 1.1, + "까닭": "앞 마디 같음 · 건설 공통 · 직종 특별인부" + }, + "점수": 1.1 + }, + { + "로직": "GF000182", + "이름": "펌프카 타설(편성 · 일일 시공량)", + "단위": "㎥", + "점수": 0.45 + } + ] + }, + "단산머리": { + "이름": "레미콘타설(무근펌프카)(130㎥)(41m(80-95㎥/hr))", + "단위": "㎥" + }, + "단산글": [ + "18.레미콘타설(무근펌프카)(130㎥)(41m(80-95㎥/hr))[㎥]", + " <2024년건설표준품셈 공통6-1-4>", + " 1. 레미콘 : 1㎥ - 별 산 -", + " 2. 콘크리트펌프차(41m,80~95㎥/hr)", + " q1 = 130㎥ : 1회 타설량", + " f1 = 1 : TYPE-2", + " f2 = 1 : TYPE-2", + " a1 = q1 × f1 × f2 = 130 × 1 × 1 = 130 : {1회 타설량}", + " Q = a1 / 8hr = 130 / 8 = 16.25㎥/hr : {시간당 시공량}", + " 노무비 : 59,020 ÷ 16.25 = 3,632", + " 재료비 : 43,282 ÷ 16.25 = 2,663.5", + " 경 비 : 93,192 ÷ 16.25 = 5,734.8", + " 소 계 : 12,030.3", + " 3. 편성인력", + " 콘크리트공 : 273,540 × 0.0231인 = 6,318.7", + " 특 별 인부 : 226,122 × 0.0154인 = 3,482.2", + " 보 통 인부 : 172,068 × 0.0077인 = 1,324.9", + " 인부소계 : 11,125.8", + " 4. 공구손료 및 경장비(콘크리트 진동기등)의 기계경비와 잡재료비(인력품의 5%)", + " 11,125.8 × 5% = 556.2", + " 5. 합 계", + " 노무비 : 3,632 + 11,125.8 = 14,757", + " 재료비 : 2,663.5 + 556.2 = 3,219", + " 경 비 : 5,734", + " 계 : 23,710" + ], + "줄수": 1, + "호표들": [ + 11 + ], + "id": "S084" + }, + { + "파일": "북부청사방", + "호표": 11, + "호표이름": "콘크리트골막이", + "호표규격": "", + "호표단위": "개소", + "차례": 2, + "이름": "콘크리트 펌프차", + "규격": "41m,80~95㎥/hr", + "수량": 9.38, + "단위": "hr", + "단가": { + "계": 195494, + "노무비": 59020, + "재료비": 43282, + "경비": 93192 + }, + "갈래": "기계", + "참조": null, + "근거": null, + "맞춤": { + "열쇠": "콘크리트펌프차|41m,80~95m3/hr|hr", + "대상": null, + "입력값": {}, + "못읽음": [], + "상태": "후보", + "환산": null, + "들어감": false, + "후보": [ + { + "로직": "GC000201", + "이름": "콘크리트 펌프차 타설", + "단위": "㎥", + "점수": 0.857 + }, + { + "로직": "GF000185", + "이름": "구체콘크리트(철근) 펌프카 타설", + "단위": "㎥", + "점수": 0.675 + }, + { + "로직": "GF000186", + "이름": "버림콘크리트(무근) 펌프카 타설", + "단위": "㎥", + "점수": 0.675 + }, + { + "로직": "GF000162", + "이름": "콘크리트 포장(인력시공)", + "단위": "㎥", + "점수": 0.595 + }, + { + "로직": "GF000166", + "이름": "콘크리트 타설(무근진동기 제외)", + "단위": "㎥", + "점수": 0.595 + } + ] + }, + "줄수": 1, + "호표들": [ + 11 + ], + "id": "S085" + }, + { + "파일": "북부청사방", + "호표": 11, + "호표이름": "콘크리트골막이", + "호표규격": "", + "호표단위": "개소", + "차례": 4, + "이름": "문양거푸집", + "규격": "", + "수량": 63.06, + "단위": "㎡", + "단가": { + "계": 28716, + "노무비": 24467, + "재료비": 4249, + "경비": null + }, + "갈래": "단가산출", + "참조": { + "종류": "단산", + "번": 25 + }, + "근거": { + "출처": [ + [ + "건설품셈", + "6-3-4" + ] + ], + "직종": [ + "형틀목공", + "보통인부" + ], + "행": [ + 1119, + 1144 + ] + }, + "맞춤": { + "열쇠": "문양거푸집||m2", + "대상": { + "로직": "GF000161", + "이름": "문양거푸집 설치 및 해체(0~7m)", + "단위": "㎡" + }, + "입력값": {}, + "못읽음": [], + "상태": "같음", + "환산": { + "배": 1, + "갈래": "같음" + }, + "들어감": true + }, + "단산머리": { + "이름": "문양거푸집", + "단위": "㎡" + }, + "단산글": [ + "25.문양거푸집[㎡]", + " * 문양거푸집 [2026년 건설표준품셈6-3-4]", + " 1. 재료비", + " 문양거푸집(ABS코팅품)(600×1200)[㎡] 4,166 × 1㎡ = 4,166", + " 2. 노무비", + " 형틀목공 : 275,790 × 0.07인 = 19,305.3", + " 보통인부 : 172,068 × 0.03인 = 5,162", + " 인부소계 : 24,467.3", + " 3. 잡재료 및 소모재료(주재료의 2%)", + " (4,166) × 2% = 83.3", + " 4. 합 계", + " 노무비 : 24,467", + " 재료비 : 4,166 + 83.3 = 4,249", + " 경 비 :", + " 계 : 28,716" + ], + "줄수": 1, + "호표들": [ + 11 + ], + "id": "S086" + }, + { + "파일": "북부청사방", + "호표": 11, + "호표이름": "콘크리트골막이", + "호표규격": "", + "호표단위": "개소", + "차례": 6, + "이름": "지수판 설치", + "규격": "산림사업품셈", + "수량": 51.7, + "단위": "M", + "단가": { + "계": 60906, + "노무비": 54104, + "재료비": 5179, + "경비": 1623 + }, + "갈래": "단가산출", + "참조": { + "종류": "단산", + "번": 46 + }, + "근거": { + "출처": [ + [ + "산림품셈", + "12-27-1" + ] + ], + "직종": [ + "특별인부", + "보통인부" + ], + "행": [ + 1943, + 1974 + ] + }, + "맞춤": { + "열쇠": "지수판설치|산림사업품셈|m", + "대상": { + "로직": "GF000196", + "이름": "지수판 설치", + "단위": "m" + }, + "입력값": {}, + "못읽음": [], + "상태": "같음", + "환산": { + "배": 1, + "갈래": "같음" + }, + "들어감": true + }, + "단산머리": { + "이름": "지수판 설치(산림사업품셈)", + "단위": "M" + }, + "단산글": [ + "46.지수판 설치(산림사업품셈)[M]", + " * 지수판 설치 [2026년 산림사업표준품셈 12-27-1]", + " 1. 재료비 (할증4%)", + " 지수판(T5×200)[M] 4,500 × 1.04m = 4,680", + " 용접봉(연강용)(3.2m/m) 4,100 × 0.042kg = 172.2", + " 어닐링철선(결속선)(#64.0mm10.1m/kg) 1,560 × 0.21kg = 327.6", + " 자재소계 : 5,179.8", + " 2. 인건비", + " 특별인부 : 226,122 × 0.151 = 34,144.4", + " 보통인부 : 172,068 × 0.116 = 19,959.8", + " 인부소계 : 54,104.2", + " 3. 공구손료(인건비의 3%)", + " 54,104.2 × 3% = 1,623.1", + " 4. 합 계", + " 노무비 : 54,104", + " 재료비 : 5,179", + " 경 비 : 1,623", + " 계 : 60,906" + ], + "줄수": 1, + "호표들": [ + 11 + ], + "id": "S087" + }, + { + "파일": "북부청사방", + "호표": 11, + "호표이름": "콘크리트골막이", + "호표규격": "", + "호표단위": "개소", + "차례": 7, + "이름": "P.V.C 파이프 설치", + "규격": "산림사업품셈", + "수량": 10, + "단위": "M", + "단가": { + "계": 4504, + "노무비": null, + "재료비": 4290, + "경비": 214 + }, + "갈래": "단가산출", + "참조": { + "종류": "단산", + "번": 1 + }, + "근거": { + "출처": [ + [ + "산림품셈", + "12-22" + ] + ], + "직종": [], + "행": [ + 4, + 21 + ] + }, + "맞춤": { + "열쇠": "p.v.c파이프설치|산림사업품셈|m", + "대상": null, + "입력값": {}, + "못읽음": [], + "상태": "후보", + "환산": null, + "들어감": false, + "후보": [ + { + "로직": "GF000192", + "이름": "P.V.C 파이프 설치(50㎜)", + "단위": "개소", + "근거": { + "조항": [ + "산림품셈 12-22" + ], + "직종": [], + "단위": false, + "점수": 1.0, + "까닭": "조항 같음" + }, + "점수": 3.05 + }, + { + "로직": "GF000204", + "이름": "전선관 설치(P.V.C ø54㎜)", + "단위": "m", + "근거": { + "조항": [ + "산림품셈 12-32" + ], + "직종": [], + "단위": true, + "점수": 0.65, + "까닭": "앞 마디 같음 · 단위 맞음" + }, + "점수": 1.004 + }, + { + "로직": "GF000191", + "이름": "부직포설치", + "단위": "㎡", + "근거": { + "조항": [ + "산림품셈 12-23" + ], + "직종": [], + "단위": false, + "점수": 0.5, + "까닭": "앞 마디 같음" + }, + "점수": 0.704 + }, + { + "로직": "GF000200", + "이름": "스페이셔 설치(몰탈 블록)", + "단위": "개소", + "근거": { + "조항": [ + "산림품셈 12-29" + ], + "직종": [], + "단위": false, + "점수": 0.5, + "까닭": "앞 마디 같음" + }, + "점수": 0.693 + }, + { + "로직": "GF000203", + "이름": "물끊기 홈(NOTCH) 설치", + "단위": "개소", + "근거": { + "조항": [ + "산림품셈 12-31" + ], + "직종": [], + "단위": false, + "점수": 0.5, + "까닭": "앞 마디 같음" + }, + "점수": 0.693 + } + ] + }, + "단산머리": { + "이름": "P.V.C 파이프 설치(산림사업품셈)", + "단위": "M" + }, + "단산글": [ + "1.P.V.C 파이프 설치(산림사업품셈)[M]", + " * P.V.C 파이프 설치 [2026년 산림사업표준품셈 12-22]", + " 1. 재료비", + " PVCPIPE(Φ50M/M,VG1) 4,290 × 1m = 4,290", + " 2. 설치비(재료비의 5%)", + " 4,290 × 5% = 214.5", + " 3. 합 계", + " 노무비 :", + " 재료비 : 4,290", + " 경 비 : 214", + " 계 : 4,504" + ], + "줄수": 1, + "호표들": [ + 11 + ], + "id": "S088" + }, + { + "파일": "북부청사방", + "호표": 11, + "호표이름": "콘크리트골막이", + "호표규격": "", + "호표단위": "개소", + "차례": 8, + "이름": "떼붙임(평떼)", + "규격": "산림사업품셈", + "수량": 97.2, + "단위": "㎡", + "단가": { + "계": 12595, + "노무비": 12595, + "재료비": null, + "경비": null + }, + "갈래": "단가산출", + "참조": { + "종류": "단산", + "번": 17 + }, + "근거": { + "출처": [ + [ + "산림품셈", + "5-12" + ] + ], + "직종": [ + "보통인부" + ], + "행": [ + 830, + 855 + ] + }, + "맞춤": { + "열쇠": "떼붙임(평떼)|산림사업품셈|m2", + "대상": null, + "입력값": {}, + "못읽음": [], + "상태": "후보", + "환산": null, + "들어감": false, + "후보": [ + { + "로직": "GF000025", + "이름": "떼붙임(재배잔디)", + "단위": "㎡", + "근거": { + "조항": [ + "산림품셈 5-12" + ], + "직종": [], + "단위": true, + "점수": 1.15, + "까닭": "조항 같음 · 단위 맞음" + }, + "점수": 3.35 + }, + { + "로직": "GF000032", + "이름": "평떼 붙임", + "단위": "㎡", + "점수": 1.0 + }, + { + "로직": "GF000033", + "이름": "줄떼 떼붙임", + "단위": "㎡", + "점수": 0.867 + }, + { + "로직": "GC000681", + "이름": "라스 붙임(메탈라스 · 유리섬유메쉬)", + "단위": "㎡", + "점수": 0.55 + }, + { + "로직": "GC000635", + "이름": "방수 보강포 붙임(1층)", + "단위": "㎡", + "점수": 0.4 + } + ] + }, + "단산머리": { + "이름": "떼붙임(평떼)(산림사업품셈)", + "단위": "㎡" + }, + "단산글": [ + "17.떼붙임(평떼)(산림사업품셈)[㎡]", + " 1. 떼 값 : 현장도착도", + " 별도계상", + " 2. 떼붙임 [2026년 산림사업표준품셈(5-12)]", + " 보통인부 : 172,068 × 0.06 = 10,324 \\/㎡", + " 3. 떼소운반", + " L = 20m 경사도 : 15° 운반회수 : 100회/일", + " 보통인부 : P", + " P = 1.0㎡ ×33kg/㎡ ×1회 / 25kg ×1일 / 100회 = 0.0132", + " 보통인부 : 172,068 × P = 172,068 × 0.0132 = 2,271.2", + " 4. 합 계", + " 노무비 : 10,324 + 2,271.2 = 12,595", + " 재료비 :", + " 경 비 :", + " 계 : 12,595" + ], + "줄수": 2, + "호표들": [ + 11, + 18 + ], + "id": "S089" + }, + { + "파일": "북부청사방", + "호표": 12, + "호표이름": "큰돌기슭막이(메쌓기)", + "호표규격": "", + "호표단위": "㎡", + "차례": 1, + "이름": "큰돌메쌓기", + "규격": "40cm-60cm,(산림사업품셈)", + "수량": 1, + "단위": "㎡", + "단가": { + "계": 104789, + "노무비": 82815, + "재료비": 9377, + "경비": 12597 + }, + "갈래": "단가산출", + "참조": { + "종류": "단산", + "번": 60 + }, + "근거": { + "출처": [ + [ + "산림품셈", + "13-6-1" + ] + ], + "직종": [ + "작업반장", + "특별인부", + "보통인부" + ], + "행": [ + 2531, + 2572 + ] + }, + "맞춤": { + "열쇠": "큰돌메쌓기|40cm-60cm,(산림사업품셈)|m2", + "대상": null, + "입력값": {}, + "못읽음": [], + "상태": "후보", + "환산": null, + "들어감": false, + "후보": [ + { + "로직": "GF000226", + "이름": "큰돌쌓기 메쌓기", + "단위": "㎡", + "근거": { + "조항": [ + "산림품셈 13-6-1" + ], + "직종": [ + "작업반장", + "특별인부" + ], + "단위": true, + "점수": 1.65, + "까닭": "조항 같음 · 직종 작업반장 · 특별인부 · 단위 맞음" + }, + "점수": 2.517 + }, + { + "로직": "GF000227", + "이름": "큰돌쌓기 찰쌓기", + "단위": "㎡", + "근거": { + "조항": [ + "산림품셈 13-6-2" + ], + "직종": [ + "작업반장", + "특별인부" + ], + "단위": true, + "점수": 1.15, + "까닭": "앞 마디 같음 · 직종 작업반장 · 특별인부 · 단위 맞음" + }, + "점수": 1.15 + }, + { + "로직": "GF000228", + "이름": "친환경 큰돌쌓기", + "단위": "㎡", + "근거": { + "조항": [ + "산림품셈 13-6-3" + ], + "직종": [ + "작업반장", + "특별인부" + ], + "단위": true, + "점수": 1.15, + "까닭": "앞 마디 같음 · 직종 작업반장 · 특별인부 · 단위 맞음" + }, + "점수": 1.15 + }, + { + "로직": "GC000260", + "이름": "메쌓기(굴착기+부착용 집게)", + "단위": "㎡", + "점수": 0.817 + }, + { + "로직": "GF000219", + "이름": "돌쌓기 메쌓기(인력)", + "단위": "㎡", + "점수": 0.7 + } + ] + }, + "단산머리": { + "이름": "큰돌메쌓기(40cm-60cm,(산림사업품셈))", + "단위": "㎡" + }, + "단산글": [ + "60.큰돌메쌓기(40cm-60cm,(산림사업품셈))[㎡]", + " 1. 큰돌쌓기(메쌓기) (2026년 산림사업품셈 13-6-1)", + " 가. 노무비", + " 작업반장 : 215,907 × 0.83 ÷ 10 = 17,920.2", + " 특별인부 : 226,122 × 1.04 ÷ 10 = 23,516.6", + " 보통인부 : 172,068 × 1.04 ÷ 10 = 17,895", + " 인부소계 : 59,331.8", + " 나. 기계경비", + " 굴착기(무한궤도)(0.8㎥+부착용집게)[hr]", + " T = 0.384", + " 노무비 : 59,020 × T = 59,020 × 0.384 = 22,663.6", + " 재료비 : 24,421 × T = 24,421 × 0.384 = 9,377.6", + " 경 비 : 32,805 × T = 32,805 × 0.384 = 12,597.1", + " 소 계 : 44,638.3", + " 다. 제잡비(품의 1%)", + " (17,920.2 + 23,516.6 + 17,895 + 22,663.6) × 1% = 819.9", + " 라. 소 계", + " 노무비 : 59,331.8 + 22,663.6 + 819.9 = 82,815.3", + " 재료비 : 9,377.6", + " 경 비 : 12,597.1", + " 계 : 104,790", + " 2. 합 계", + " 노무비 : 82,815", + " 재료비 : 9,377", + " 경 비 : 12,597", + " 계 : 104,789" + ], + "줄수": 1, + "호표들": [ + 12 + ], + "id": "S090" + }, + { + "파일": "북부청사방", + "호표": 12, + "호표이름": "큰돌기슭막이(메쌓기)", + "호표규격": "", + "호표단위": "㎡", + "차례": 2, + "이름": "고임돌채집및운반", + "규격": "(9:1)(0.6㎥+4.5Ton)", + "수량": 0.15, + "단위": "㎥", + "단가": { + "계": 18893, + "노무비": 13860, + "재료비": 2314, + "경비": 2719 + }, + "갈래": "단가산출", + "참조": { + "종류": "단산", + "번": 5 + }, + "근거": { + "출처": [], + "직종": [ + "보통인부" + ], + "행": [ + 140, + 221 + ] + }, + "맞춤": { + "열쇠": "고임돌채집및운반|(9:1)(0.6m3+4.5ton)|m3", + "대상": null, + "입력값": {}, + "못읽음": [], + "상태": "후보", + "환산": null, + "들어감": false, + "후보": [ + { + "로직": "GC000363", + "이름": "자갈채집 및 운반", + "단위": "㎥", + "점수": 0.765 + }, + { + "로직": "GF000212", + "이름": "막돌 채집", + "단위": "㎡ · ㎥", + "점수": 0.6 + }, + { + "로직": "GC000418", + "이름": "사석 적재 및 운반", + "단위": "㎥", + "점수": 0.458 + }, + { + "로직": "GF000211", + "이름": "모래·자갈·약돌 채집(인력)", + "단위": "㎥", + "점수": 0.45 + }, + { + "로직": "GF000213", + "이름": "큰돌 채집(기계)", + "단위": "개", + "점수": 0.45 + } + ] + }, + "단산머리": { + "이름": "고임돌채집및운반((9:1)(0.6㎥+4.5Ton))", + "단위": "㎥" + }, + "단산글": [ + "5.고임돌채집및운반((9:1)(0.6㎥+4.5Ton))[㎥]", + " 1. 채 집 (기계 : 인력 = 9 : 1)", + " 가. 굴착기(타이어형)(0.6㎥)[hr] : 90%", + " q = 0.6 f = 1 / 1.125 = 0.89 k = 0.55", + " Cm = 18sec(90˚) E = 0.35", + " 3600×q×k×f×E 3,600×0.6×0.55×0.89×0.35", + " Q = ────────── = ─────────────── = 20.56㎥/hr", + " Cm 18", + " 노무비 : 59,020 ÷ 20.56 × 0.9 = 2,583.5", + " 재료비 : 19,792 ÷ 20.56 × 0.9 = 866.3", + " 경 비 : 27,120 ÷ 20.56 × 0.9 = 1,187.1", + " 소 계 : 4,636.9", + " 나. 인 력 : 10%", + " 보통인부 : 172,068 × 0.3 × 0.1 = 5,162", + " 다. 소 계", + " 노무비 : 2,583.5 + 5,162 = 7,745.5", + " 재료비 : 866.3", + " 경 비 : 1,187.1", + " 계 : 9,798.9", + " 2. 적 사", + " 굴착기(타이어형)(0.6㎥)[hr]", + " q = 0.6 f = 1 / 1.125 = 0.89 k = 0.7", + " Cm = 20sec(135˚) E = 0.45", + " 3600×q×k×f×E 3,600×0.6×0.7×0.89×0.45", + " Q = ────────── = ────────────── = 30.28㎥/hr", + " Cm 20", + " 노무비 : 59,020 ÷ 30.28 = 1,949.1", + " 재료비 : 19,792 ÷ 30.28 = 653.6", + " 경 비 : 27,120 ÷ 30.28 = 895.6", + " 소 계 : 3,498.3", + " 3. 운 반", + " 덤프트럭(4.5Ton)", + " L = 0.5km", + " E = 0.9 f = 1 / 1.125 = 0.89", + " q1 = (4.5 / 1.75) × 1.125 = 2.89", + " n = q1 / (0.6 × k) = 2.89 / (0.6 × 0.7) = 6.88회", + " t1 = 20 × n / (60 × 0.45) = 20 × 6.88 / (60 × 0.45) = 5.1분", + " t2 = (L / 10 + L / 15) × 60 = (0.5 / 10 + 0.5 / 15) × 60 = 5분", + " t3 = 1.1분 t4 = 0.42분", + " Cm = t1 + t2 + t3 + t4 = 5.1 + 5 + 1.1 + 0.42 = 11.62분", + " 60×q1×f×E 60×2.89×0.89×0.9", + " Q = ──────── = ────────── = 11.95㎥/hr", + " Cm 11.62", + " 노무비 : 49,778 ÷ 11.95 = 4,165.5", + " 재료비 : 9,494 ÷ 11.95 = 794.4", + " 경 비 : 7,613 ÷ 11.95 = 637", + " 소 계 : 5,596.9", + " 4. 합 계", + " 노무비 : 7,745.5 + 1,949.1 + 4,165.5 = 13,860", + " 재료비 : 866.3 + 653.6 + 794.4 = 2,314", + " 경 비 : 1,187.1 + 895.6 + 637 = 2,719", + " 계 : 18,893" + ], + "줄수": 9, + "호표들": [ + 12, + 13, + 14, + 15, + 17, + 19, + 20, + 26, + 27 + ], + "id": "S091" + }, + { + "파일": "북부청사방", + "호표": 12, + "호표이름": "큰돌기슭막이(메쌓기)", + "호표규격": "", + "호표단위": "㎡", + "차례": 3, + "이름": "뒷채움잡석채집운반다짐", + "규격": "(9:1)(0.6㎥+4.5Ton)", + "수량": 0.3, + "단위": "㎥", + "단가": { + "계": 28775, + "노무비": 21736, + "재료비": 3168, + "경비": 3871 + }, + "갈래": "단가산출", + "참조": { + "종류": "단산", + "번": 16 + }, + "근거": { + "출처": [ + [ + "건설품셈", + "3-4-4" + ] + ], + "직종": [ + "보통인부", + "특별인부" + ], + "행": [ + 706, + 827 + ] + }, + "맞춤": { + "열쇠": "뒷채움잡석채집운반다짐|(9:1)(0.6m3+4.5ton)|m3", + "대상": null, + "입력값": {}, + "못읽음": [], + "상태": "후보", + "환산": null, + "들어감": false, + "후보": [ + { + "로직": "GC000106", + "이름": "뒤채움 및 다짐(소형장비)", + "단위": "㎥", + "근거": { + "조항": [ + "건설품셈 공통 3-4-4" + ], + "직종": [ + "특별인부" + ], + "단위": true, + "점수": 1.75, + "까닭": "조항 같음 · 건설 공통 · 직종 특별인부 · 단위 맞음" + }, + "점수": 2.167 + }, + { + "로직": "GC000107", + "이름": "뒤채움 및 다짐(대형장비)", + "단위": "㎥", + "근거": { + "조항": [ + "건설품셈 공통 3-4-5" + ], + "직종": [ + "특별인부" + ], + "단위": true, + "점수": 1.25, + "까닭": "앞 마디 같음 · 건설 공통 · 직종 특별인부 · 단위 맞음" + }, + "점수": 1.667 + }, + { + "로직": "GC000108", + "이름": "되메우기 및 다짐(소형장비)", + "단위": "㎥", + "근거": { + "조항": [ + "건설품셈 공통 3-4-6" + ], + "직종": [ + "특별인부" + ], + "단위": true, + "점수": 1.25, + "까닭": "앞 마디 같음 · 건설 공통 · 직종 특별인부 · 단위 맞음" + }, + "점수": 1.525 + }, + { + "로직": "GC000109", + "이름": "되메우기 및 다짐(대형장비)", + "단위": "㎥", + "근거": { + "조항": [ + "건설품셈 공통 3-4-7" + ], + "직종": [ + "특별인부" + ], + "단위": true, + "점수": 1.25, + "까닭": "앞 마디 같음 · 건설 공통 · 직종 특별인부 · 단위 맞음" + }, + "점수": 1.525 + }, + { + "로직": "GF000218", + "이름": "기초다짐 및 뒷채움(기계)", + "단위": "㎥", + "점수": 0.553 + } + ] + }, + "단산머리": { + "이름": "뒷채움잡석채집운반다짐((9:1)(0.6㎥+4.5Ton))", + "단위": "㎥" + }, + "단산글": [ + "16.뒷채움잡석채집운반다짐((9:1)(0.6㎥+4.5Ton))[㎥]", + " * 구조물뒷채움잡석(막자갈) 기계 90%, 인력 10%", + " 1. 채 집", + " 가. 굴착기(타이어형)(0.6㎥)[hr] (90%)", + " q = 0.6 f = 1 / 1.125 = 0.89 k = 0.55", + " Cm = 18sec(90˚) E = 0.35", + " 3600×q×k×f×E 3,600×0.6×0.55×0.89×0.35", + " Q = ────────── = ─────────────── = 20.56㎥/hr", + " Cm 18", + " 노무비 : 59,020 ÷ 20.56 × 0.9 = 2,583.5", + " 재료비 : 19,792 ÷ 20.56 × 0.9 = 866.3", + " 경 비 : 27,120 ÷ 20.56 × 0.9 = 1,187.1", + " 소 계 : 4,636.9", + " 나. 인 력 (10%)", + " 보통인부 : 172,068 × 0.3 × 0.1 = 5,162", + " 다. 소 계", + " 노무비 : 2,583.5 + 5,162 = 7,745.5", + " 재료비 : 866.3", + " 경 비 : 1,187.1", + " 계 : 9,798.9", + " 2. 적 사", + " 굴착기(타이어형)(0.6㎥)[hr]", + " q = 0.6 f = 1 / 1.125 = 0.89 k = 0.7", + " Cm = 20sec(135˚) E = 0.45", + " 3600×q×k×f×E 3,600×0.6×0.7×0.89×0.45", + " Q = ────────── = ────────────── = 30.28㎥/hr", + " Cm 20", + " 노무비 : 59,020 ÷ 30.28 = 1,949.1", + " 재료비 : 19,792 ÷ 30.28 = 653.6", + " 경 비 : 27,120 ÷ 30.28 = 895.6", + " 소 계 : 3,498.3", + " 3. 운 반", + " 덤프트럭(4.5Ton)", + " L = 0.5km", + " E = 0.9 f = 1 / 1.125 = 0.89", + " q1 = (4.5 / 1.75) × 1.125 = 2.89", + " n = q1 / (0.6 × k) = 2.89 / (0.6 × 0.7) = 6.88회", + " t1 = 20 × n / (60 × 0.45) = 20 × 6.88 / (60 × 0.45) = 5.1분", + " L L 0.5 0.5", + " t2 = (──+──)×60 = (──+──)×60 = 5분", + " 10 15 10 15", + " t3 = 1.1분 t4 = 0.42분", + " Cm = t1 + t2 + t3 + t4 = 5.1 + 5 + 1.1 + 0.42 = 11.62분", + " 60×q1×f×E 60×2.89×0.89×0.9", + " Q1 = ──────── = ────────── = 11.95㎥/hr", + " Cm 11.62", + " 노무비 : 49,778 ÷ Q1 = 49,778 ÷ 11.95 = 4,165.5", + " 재료비 : 9,494 ÷ Q1 = 9,494 ÷ 11.95 = 794.4", + " 경 비 : 7,613 ÷ Q1 = 7,613 ÷ 11.95 = 637", + " 소 계 : 5,596.9", + " 4. 뒤채움 및 다짐 [2025년 건설표준품셈3-4-4(소형장비)]", + " 가. 굴착기(무한궤도)(0.2㎥)[hr]", + " 1일 시공량 : Qd = 110㎥/일", + " Q = Qd / 8 = 110 / 8 = 13.75㎥/hr", + " 노무비 : 59,020 ÷ 13.75 = 4,292.3", + " 재료비 : 8,324 ÷ 13.75 = 605.3", + " 경 비 : 13,873 ÷ 13.75 = 1,008.9", + " 소 계 : 5,906.5", + " 나. 진동로울러(핸드가이드식)(0.7Ton)", + " 재료비 : 3,420 ÷ 13.75 = 248.7", + " 경 비 : 1,967 ÷ 13.75 = 143", + " 소 계 : 391.7", + " 다. 인 력", + " P = 1인 / Qd = 1 / 110 = 0.009인", + " 특별인부 : 226,122 × P = 226,122 × 0.009 = 2,035", + " 보통인부 : 172,068 × P = 172,068 × 0.009 = 1,548.6", + " 인부소계 : 3,583.6", + " 라. 소 계", + " 노무비 : 4,292.3 + 3,583.6 = 7,875.9", + " 재료비 : 605.3 + 248.7 = 854", + " 경 비 : 1,008.9 + 143 = 1,151.9", + " 계 : 9,881.8", + " 5. 합 계", + " 노무비 : 7,745.5 + 1,949.1 + 4,165.5 + 7,875.9 = 21,736", + " 재료비 : 866.3 + 653.6 + 794.4 + 854 = 3,168", + " 경 비 : 1,187.1 + 895.6 + 637 + 1,151.9 = 3,871", + " 계 : 28,775" + ], + "줄수": 4, + "호표들": [ + 12, + 13, + 15, + 16 + ], + "id": "S092" + }, + { + "파일": "북부청사방", + "호표": 13, + "호표이름": "큰돌기슭막이(찰쌓기)", + "호표규격": "", + "호표단위": "㎡", + "차례": 1, + "이름": "큰돌찰쌓기", + "규격": "40cm-60cm,(산림사업품셈)", + "수량": 1, + "단위": "㎡", + "단가": { + "계": 134303, + "노무비": 106835, + "재료비": 11722, + "경비": 15746 + }, + "갈래": "단가산출", + "참조": { + "종류": "단산", + "번": 62 + }, + "근거": { + "출처": [ + [ + "산림품셈", + "13-6-2" + ] + ], + "직종": [ + "작업반장", + "특별인부", + "보통인부" + ], + "행": [ + 2619, + 2660 + ] + }, + "맞춤": { + "열쇠": "큰돌찰쌓기|40cm-60cm,(산림사업품셈)|m2", + "대상": null, + "입력값": {}, + "못읽음": [], + "상태": "후보", + "환산": null, + "들어감": false, + "후보": [ + { + "로직": "GF000227", + "이름": "큰돌쌓기 찰쌓기", + "단위": "㎡", + "근거": { + "조항": [ + "산림품셈 13-6-2" + ], + "직종": [ + "작업반장", + "특별인부" + ], + "단위": true, + "점수": 1.65, + "까닭": "조항 같음 · 직종 작업반장 · 특별인부 · 단위 맞음" + }, + "점수": 2.517 + }, + { + "로직": "GF000226", + "이름": "큰돌쌓기 메쌓기", + "단위": "㎡", + "근거": { + "조항": [ + "산림품셈 13-6-1" + ], + "직종": [ + "작업반장", + "특별인부" + ], + "단위": true, + "점수": 1.15, + "까닭": "앞 마디 같음 · 직종 작업반장 · 특별인부 · 단위 맞음" + }, + "점수": 1.15 + }, + { + "로직": "GF000228", + "이름": "친환경 큰돌쌓기", + "단위": "㎡", + "근거": { + "조항": [ + "산림품셈 13-6-3" + ], + "직종": [ + "작업반장", + "특별인부" + ], + "단위": true, + "점수": 1.15, + "까닭": "앞 마디 같음 · 직종 작업반장 · 특별인부 · 단위 맞음" + }, + "점수": 1.15 + }, + { + "로직": "GC000261", + "이름": "찰쌓기(굴착기+부착용 집게)", + "단위": "㎡", + "점수": 0.817 + }, + { + "로직": "GF000221", + "이름": "돌쌓기 찰쌓기(인력)", + "단위": "㎡", + "점수": 0.7 + } + ] + }, + "단산머리": { + "이름": "큰돌찰쌓기(40cm-60cm,(산림사업품셈))", + "단위": "㎡" + }, + "단산글": [ + "62.큰돌찰쌓기(40cm-60cm,(산림사업품셈))[㎡]", + " 1. 큰돌쌓기(찰쌓기) (2026년 산림사업 품셈 13-6-2)", + " 가. 노무비", + " 작업반장 : 215,907 × 0.83 ÷ 10 = 17,920.2", + " 특별인부 : 226,122 × 1.30 ÷ 10 = 29,395.8", + " 보통인부 : 172,068 × 1.30 ÷ 10 = 22,368.8", + " 인부소계 : 69,684.8", + " 나. 기계경비", + " 굴착기(무한궤도)(0.8㎥+부착용집게)[hr]", + " T = 0.48", + " 노무비 : 59,020 × T = 59,020 × 0.48 = 28,329.6", + " 재료비 : 24,421 × T = 24,421 × 0.48 = 11,722", + " 경 비 : 32,805 × T = 32,805 × 0.48 = 15,746.4", + " 소 계 : 55,798", + " 다. 제잡비(노무비의 9%)", + " (17,920.2 + 29,395.8 + 22,368.8 + 28,329.6) × 9% = 8,821.2", + " 라. 소 계", + " 노무비 : 69,684.8 + 28,329.6 + 8,821.2 = 106,835.6", + " 재료비 : 11,722", + " 경 비 : 15,746.4", + " 계 : 134,304", + " 2. 합 계", + " 노무비 : 106,835", + " 재료비 : 11,722", + " 경 비 : 15,746", + " 계 : 134,303" + ], + "줄수": 10, + "호표들": [ + 13, + 14, + 15, + 16, + 17, + 21, + 22, + 23, + 24, + 25 + ], + "id": "S093" + }, + { + "파일": "북부청사방", + "호표": 14, + "호표이름": "큰돌기슭막이(찰쌓기)", + "호표규격": "(뒷채움잡석구입)", + "호표단위": "㎡", + "차례": 1, + "이름": "큰돌채집운반", + "규격": "산림사업품셈", + "수량": 0.5, + "단위": "㎡", + "단가": { + "계": 44220, + "노무비": 26291, + "재료비": 6498, + "경비": 11431 + }, + "갈래": "단가산출", + "참조": { + "종류": "단산", + "번": 63 + }, + "근거": { + "출처": [ + [ + "산림품셈", + "13-2-3" + ] + ], + "직종": [ + "작업반장" + ], + "행": [ + 2663, + 2754 + ] + }, + "맞춤": { + "열쇠": "큰돌채집운반|산림사업품셈|m2", + "대상": null, + "입력값": {}, + "못읽음": [], + "상태": "후보", + "환산": null, + "들어감": false, + "후보": [ + { + "로직": "GF000213", + "이름": "큰돌 채집(기계)", + "단위": "개", + "근거": { + "조항": [ + "산림품셈 13-2-3" + ], + "직종": [ + "작업반장" + ], + "단위": false, + "점수": 1.5, + "까닭": "조항 같음 · 직종 작업반장" + }, + "점수": 2.3 + }, + { + "로직": "GF000212", + "이름": "막돌 채집", + "단위": "㎡ · ㎥", + "근거": { + "조항": [ + "산림품셈 13-2-2" + ], + "직종": [], + "단위": true, + "점수": 0.65, + "까닭": "앞 마디 같음 · 단위 맞음" + }, + "점수": 1.35 + }, + { + "로직": "GF000214", + "이름": "야면석 채집(인력)", + "단위": "㎡ · ㎥", + "근거": { + "조항": [ + "산림품셈 13-2-4" + ], + "직종": [], + "단위": true, + "점수": 0.65, + "까닭": "앞 마디 같음 · 단위 맞음" + }, + "점수": 1.072 + }, + { + "로직": "GF000211", + "이름": "모래·자갈·약돌 채집(인력)", + "단위": "㎥", + "근거": { + "조항": [ + "산림품셈 13-2-1" + ], + "직종": [], + "단위": false, + "점수": 0.5, + "까닭": "앞 마디 같음" + }, + "점수": 0.836 + }, + { + "로직": "GC000363", + "이름": "자갈채집 및 운반", + "단위": "㎥", + "점수": 0.364 + } + ] + }, + "단산머리": { + "이름": "큰돌채집운반(산림사업품셈)", + "단위": "㎡" + }, + "단산글": [ + "63.큰돌채집운반(산림사업품셈)[㎡]", + " ※ 큰돌뒷길이 70cm내외", + " [2026년 산림사업 품셈 13-2-3]", + " T = 1.0 × 0.675 × 0.77 (공극률제외) = 0.52㎥/㎡", + " 1. 큰돌채집 : 채집운반은 채집돌의 40% 적용", + " P = 4개/㎡", + " 가. 인 력", + " 작업반장 : 215,907×P×0.026인/개×0.75(품할증)×0.4 = 215,907×4×0.026×0.75×0.4 = 6,736.2", + " 나. 굴착기(타이어형)(0.6㎥)[hr]", + " 노무비 : 59,020×P×0.134×0.75(품할증)×0.4 = 59,020×4×0.134×0.75×0.4 = 9,490.4", + " 재료비 : 19,792×P×0.134×0.75(품할증)×0.4 = 19,792×4×0.134×0.75×0.4 = 3,182.5", + " 경 비 : 27,120×P×0.134×0.75(품할증)×0.4 = 27,120×4×0.134×0.75×0.4 = 4,360.8", + " 소 계 : 17,033.7", + " 다. 소 계", + " 노무비 : 6,736.2 + 9,490.4 = 16,226.6", + " 재료비 : 3,182.5", + " 경 비 : 4,360.8", + " 계 : 23,769.9", + " 2. 큰돌상차", + " 가. 굴착기(타이어형)(0.6㎥)[hr]", + " 노무비 : 59,020×P×0.134×0.75(품할증)×0.4 = 59,020×4×0.134×0.75×0.4 = 9,490.4", + " 재료비 : 19,792×P×0.134×0.75(품할증)×0.4 = 19,792×4×0.134×0.75×0.4 = 3,182.5", + " 경 비 : 27,120×P×0.134×0.75(품할증)×0.4 = 27,120×4×0.134×0.75×0.4 = 4,360.8", + " 소 계 : 17,033.7", + " 나. 부착용집게(0.7~0.8㎥)", + " 노무비 : 0×P×0.134×0.75(품할증)×0.4 = 0×4×0.134×0.75×0.4 = 0", + " 재료비 : 0×P×0.134×0.75(품할증)×0.4 = 0×4×0.134×0.75×0.4 = 0", + " 경 비 : 5,786×P×0.134×0.75(품할증)×0.4 = 5,786×4×0.134×0.75×0.4 = 930.3", + " 소 계 : 930.3", + " 다. 소 계", + " 노무비 : 9,490.4", + " 재료비 : 3,182.5", + " 경 비 : 4,360.8 + 930.3 = 5,291.1", + " 계 : 17,964", + " 3. 제잡비(노무비의 6%)", + " (6,736.2 + 9,490.4 + 9,490.4 + 0) × 6% = 1,543", + " 4. 운 반", + " 덤프트럭(15Ton(발파)", + " L = 0.5km k = 0.55 E = 0.9 F = 1", + " q1 = (15 / 2.65) × 1 = 5.66", + " n = q1 / (k × 0.8) = 5.66 / (0.55 × 0.8) = 12.86회", + " t1 = 20 × n / (60 × k) = 20 × 12.86 / (60 × 0.55) = 7.79분", + " L L 0.5 0.5", + " t2 = (──+──)×60 = (──+──)×60 = 5분", + " 10 15 10 15", + " t3 = 1.1분 t4 = 0.42분", + " Cm = t1 + t2 + t3 + t4 = 7.79 + 5 + 1.1 + 0.42 = 14.31", + " 60×q1×F×E 60×5.66×1×0.9", + " Q = ──────── = ───────── = 21.36 ㎥/hr", + " Cm 14.31", + " 노무비 : 59,020÷21.36×T×0.4 = 59,020÷21.36×0.52×0.4 = 574.7", + " 재료비 : 30,192÷21.36×((Cm-t1)÷Cm)×T×0.4= 30,192÷21.36×((14.31-7.79)÷14.31)×0.52×0.4= 133.9", + " 경 비 : 24,285÷21.36×T×0.4 = 24,285÷21.36×0.52×0.4 = 236.4", + " 소 계 : 945", + " 5. 합 계", + " 노무비 : 16,226.6 + 9,490.4 + 574.7 = 26,291", + " 재료비 : 3,182.5 + 3,182.5 + 133.9 = 6,498", + " 경 비 : 4,360.8 + 5,291.1 + 1,543 + 236.4 = 11,431", + " 계 : 44,220" + ], + "줄수": 5, + "호표들": [ + 14, + 16, + 20, + 22, + 25 + ], + "id": "S094" + }, + { + "파일": "북부청사방", + "호표": 14, + "호표이름": "큰돌기슭막이(찰쌓기)", + "호표규격": "(뒷채움잡석구입)", + "호표단위": "㎡", + "차례": 5, + "이름": "뒷채움잡석구입운반", + "규격": "", + "수량": 0.3, + "단위": "㎥", + "단가": { + "계": 36300, + "노무비": null, + "재료비": null, + "경비": 36300 + }, + "갈래": "단가산출", + "참조": { + "종류": "단산", + "번": 13 + }, + "근거": { + "출처": [], + "직종": [], + "행": [ + 561, + 611 + ] + }, + "맞춤": { + "열쇠": "뒷채움잡석구입운반||m3", + "대상": null, + "입력값": {}, + "못읽음": [], + "상태": "후보", + "환산": null, + "들어감": false, + "후보": [ + { + "로직": "GF000135", + "이름": "골재운반 잡석(덤프 15ton)", + "단위": "㎥", + "점수": 0.508 + }, + { + "로직": "GF000218", + "이름": "기초다짐 및 뒷채움(기계)", + "단위": "㎥", + "점수": 0.467 + }, + { + "로직": "GF000193", + "이름": "뒷채움", + "단위": "㎡", + "점수": 0.45 + }, + { + "로직": "GF000149", + "이름": "불도저 운반", + "단위": "㎥", + "점수": 0.367 + }, + { + "로직": "GC000363", + "이름": "자갈채집 및 운반", + "단위": "㎥", + "점수": 0.293 + } + ] + }, + "단산머리": { + "이름": "뒷채움잡석구입운반", + "단위": "㎥" + }, + "단산글": [ + "13.뒷채움잡석구입운반[㎥]", + " 1. 적 사 : 굴착기 0.8㎥", + " q = 0.8 f = 1 / 1.125 = 0.89 K = 0.7", + " Cm = 20sec E = 0.45", + " 3600×q×K×f×E 3,600×0.8×0.7×0.89×0.45", + " Q = ────────── = ────────────── = 40.37㎥/hr", + " Cm 20", + " 2. 운 반 - 홍천골재장", + " 덤프트럭(15Ton)", + " L1 = 18.7km L2 = 2.3km", + " V11 = 30km V12 = 35km V21 = 10km V22 = 15km", + " k = 0.55 E = 0.9 F = 1", + " q1 = (15 / 1.7) × 1.125 = 9.93", + " n = q1 / (0.8 × k) = 9.93 / (0.8 × 0.55) = 22.57 회", + " t1 = 20 × n / (60 × 0.45) = 20 × 22.57 / (60 × 0.45) = 16.72 분", + " L1 L2 L1 L2 18.7 2.3 18.7 2.3", + " t2 = (──+──+──+──)×60 = (───+──+───+──)×60 =162.74분", + " V11 V12 V21 V22 30 35 10 15", + " t3 = 1.1분 t4 = 0.42분 t5 = 0.5분", + " Cm = t1 + t2 + t3 + t4 + t5 = 16.72 + 162.74 + 1.1 + 0.42 + 0.5 = 181.48", + " 60×q1×F×E 60×9.93×1×0.9", + " Q1 = ──────── = ───────── = 2.95㎥/hr", + " Cm 181.48", + " 노무비 : 59,020 ÷ Q1 = 59,020 ÷ 2.95 = 20,006.7", + " 재료비 : 30,192÷Q1×(Cm-t1)÷Cm = 30,192÷2.95×(181.48-16.72)÷181.48 = 9,291.6", + " 경 비 : 20,659 ÷ Q1 = 20,659 ÷ 2.95 = 7,003", + " 소 계 : 36,301.3", + " 3. 합 계", + " 노무비 : 20,006", + " 재료비 : 9,291", + " 경 비 : 7,003", + " 계 : 36,300" + ], + "줄수": 1, + "호표들": [ + 14 + ], + "id": "S095" + }, + { + "파일": "북부청사방", + "호표": 14, + "호표이름": "큰돌기슭막이(찰쌓기)", + "호표규격": "(뒷채움잡석구입)", + "호표단위": "㎡", + "차례": 6, + "이름": "뒷채움잡석뒷채움", + "규격": "", + "수량": 0.3, + "단위": "㎥", + "단가": { + "계": 9880, + "노무비": 7875, + "재료비": 854, + "경비": 1151 + }, + "갈래": "단가산출", + "참조": { + "종류": "단산", + "번": 15 + }, + "근거": { + "출처": [ + [ + "건설품셈", + "3-4-4" + ] + ], + "직종": [ + "특별인부", + "보통인부" + ], + "행": [ + 667, + 703 + ] + }, + "맞춤": { + "열쇠": "뒷채움잡석뒷채움||m3", + "대상": null, + "입력값": {}, + "못읽음": [], + "상태": "후보", + "환산": null, + "들어감": false, + "후보": [ + { + "로직": "GC000106", + "이름": "뒤채움 및 다짐(소형장비)", + "단위": "㎥", + "근거": { + "조항": [ + "건설품셈 공통 3-4-4" + ], + "직종": [ + "특별인부" + ], + "단위": true, + "점수": 1.75, + "까닭": "조항 같음 · 건설 공통 · 직종 특별인부 · 단위 맞음" + }, + "점수": 1.75 + }, + { + "로직": "GC000107", + "이름": "뒤채움 및 다짐(대형장비)", + "단위": "㎥", + "근거": { + "조항": [ + "건설품셈 공통 3-4-5" + ], + "직종": [ + "특별인부" + ], + "단위": true, + "점수": 1.25, + "까닭": "앞 마디 같음 · 건설 공통 · 직종 특별인부 · 단위 맞음" + }, + "점수": 1.25 + }, + { + "로직": "GC000102", + "이름": "흙쌓기(다짐)", + "단위": "㎥", + "근거": { + "조항": [ + "건설품셈 공통 3-4-1" + ], + "직종": [ + "특별인부" + ], + "단위": true, + "점수": 1.25, + "까닭": "앞 마디 같음 · 건설 공통 · 직종 특별인부 · 단위 맞음" + }, + "점수": 1.25 + }, + { + "로직": "GC000104", + "이름": "암쌓기", + "단위": "㎥", + "근거": { + "조항": [ + "건설품셈 공통 3-4-2" + ], + "직종": [ + "특별인부" + ], + "단위": true, + "점수": 1.25, + "까닭": "앞 마디 같음 · 건설 공통 · 직종 특별인부 · 단위 맞음" + }, + "점수": 1.25 + }, + { + "로직": "GF000193", + "이름": "뒷채움", + "단위": "㎡", + "점수": 0.621 + } + ] + }, + "단산머리": { + "이름": "뒷채움잡석뒷채움", + "단위": "㎥" + }, + "단산글": [ + "15.뒷채움잡석뒷채움[㎥]", + " 1. 구조물뒷채움잡석 뒤채움 및 다짐 [2025년 건설표준품셈3-4-4(소형장비)]", + " 가. 기 계 : 굴착기(무한궤도)(0.2㎥)", + " 1일 시공량 : Qd = 110㎥/일", + " Q = Qd / 8 = 110 / 8 = 13.75㎥/hr", + " 노무비 : 59,020 ÷ 13.75 = 4,292.3", + " 재료비 : 8,324 ÷ 13.75 = 605.3", + " 경 비 : 13,873 ÷ 13.75 = 1,008.9", + " 소 계 : 5,906.5", + " 나. 기 계 : 진동로울러(핸드가이드식)(0.7Ton)", + " 재료비 : 3,420 ÷ 13.75 = 248.7", + " 경 비 : 1,967 ÷ 13.75 = 143", + " 소 계 : 391.7", + " 다. 인건비", + " P = 1 / Qd = 1 / 110 = 0.009인", + " 특별인부 : 226,122 × P = 226,122 × 0.009 = 2,035", + " 보통인부 : 172,068 × P = 172,068 × 0.009 = 1,548.6", + " 인부소계 : 3,583.6", + " 라. 합 계", + " 노무비 : 4,292.3 + 3,583.6 = 7,875", + " 재료비 : 605.3 + 248.7 = 854", + " 경 비 : 1,008.9 + 143 = 1,151", + " 계 : 9,880" + ], + "줄수": 2, + "호표들": [ + 14, + 17 + ], + "id": "S096" + }, + { + "파일": "북부청사방", + "호표": 17, + "호표이름": "큰돌기슭막이(찰쌓기)", + "호표규격": "뒷채움자갈구입", + "호표단위": "㎡", + "차례": 4, + "이름": "뒷채움잡석구입운반", + "규격": "(창봉 기번3)(2차)", + "수량": 0.3, + "단위": "㎥", + "단가": { + "계": 35198, + "노무비": null, + "재료비": null, + "경비": 35198 + }, + "갈래": "단가산출", + "참조": { + "종류": "단산", + "번": 14 + }, + "근거": { + "출처": [], + "직종": [], + "행": [ + 614, + 664 + ] + }, + "맞춤": { + "열쇠": "뒷채움잡석구입운반|(창봉기번3)(2차)|m3", + "대상": null, + "입력값": {}, + "못읽음": [], + "상태": "후보", + "환산": null, + "들어감": false, + "후보": [ + { + "로직": "GF000135", + "이름": "골재운반 잡석(덤프 15ton)", + "단위": "㎥", + "점수": 0.508 + }, + { + "로직": "GF000218", + "이름": "기초다짐 및 뒷채움(기계)", + "단위": "㎥", + "점수": 0.467 + }, + { + "로직": "GF000193", + "이름": "뒷채움", + "단위": "㎡", + "점수": 0.45 + }, + { + "로직": "GF000149", + "이름": "불도저 운반", + "단위": "㎥", + "점수": 0.367 + }, + { + "로직": "GC000363", + "이름": "자갈채집 및 운반", + "단위": "㎥", + "점수": 0.293 + } + ] + }, + "단산머리": { + "이름": "뒷채움잡석구입운반((창봉 기번3)(2차))", + "단위": "㎥" + }, + "단산글": [ + "14.뒷채움잡석구입운반((창봉 기번3)(2차))[㎥]", + " 1. 적 사 : BACK-HOE 0.8㎥", + " q = 0.8 f = 1 / 1.125 = 0.89 K = 0.7", + " Cm = 20sec E = 0.45", + " 3600×q×K×f×E 3,600×0.8×0.7×0.89×0.45", + " Q = ────────── = ────────────── = 40.37 ㎥/hr", + " Cm 20", + " 2. 운 반 - 홍천골재장", + " 덤프트럭(15Ton)", + " L1 = 18.7km L2 = 1.4km", + " V11 = 30km V12 = 35km V21 = 10km V22 = 15km", + " k = 0.55 E = 0.9 F = 1", + " q1 = (15 / 1.7) × 1.125 = 9.93", + " n = q1 / (0.8 × k) = 9.93 / (0.8 × 0.55) = 22.57 회", + " t1 = 20 × n / (60 × 0.45) = 20 × 22.57 / (60 × 0.45) = 16.72 분", + " L1 L2 L1 L2 18.7 1.4 18.7 1.4", + " t2 = (──+──+──+──)×60 = (───+──+───+──)×60 =157.6분", + " V11 V12 V21 V22 30 35 10 15", + " t3 = 1.1분 t4 = 0.42분 t5 = 0.5분", + " Cm = t1 + t2 + t3 + t4 + t5 = 16.72 + 157.6 + 1.1 + 0.42 + 0.5 = 176.34", + " 60×q1×F×E 60×9.93×1×0.9", + " Q1 = ──────── = ───────── = 3.04 ㎥/hr", + " Cm 176.34", + " 노무비 : 59,020 ÷ Q1 = 59,020 ÷ 3.04 = 19,414.4", + " 재료비 : 30,192÷Q1×(Cm-t1)÷Cm = 30,192÷3.04×(176.34-16.72)÷176.34 = 8,989.8", + " 경 비 : 20,659 ÷ Q1 = 20,659 ÷ 3.04 = 6,795.7", + " 소 계 : 35,199.9", + " 3. 합 계", + " 노무비 : 19,414", + " 재료비 : 8,989", + " 경 비 : 6,795", + " 계 : 35,198" + ], + "줄수": 1, + "호표들": [ + 17 + ], + "id": "S097" + }, + { + "파일": "북부청사방", + "호표": 19, + "호표이름": "큰돌메붙임", + "호표규격": "(구입)", + "호표단위": "㎡", + "차례": 1, + "이름": "큰돌메붙임", + "규격": "40cm-60cm,(산림사업품셈)", + "수량": 0.8, + "단위": "㎡", + "단가": { + "계": 68660, + "노무비": 54624, + "재료비": 6164, + "경비": 7872 + }, + "갈래": "단가산출", + "참조": { + "종류": "단산", + "번": 59 + }, + "근거": { + "출처": [ + [ + "산림품셈", + "13-7-1" + ] + ], + "직종": [ + "작업반장", + "특별인부", + "보통인부" + ], + "행": [ + 2480, + 2528 + ] + }, + "맞춤": { + "열쇠": "큰돌메붙임|40cm-60cm,(산림사업품셈)|m2", + "대상": null, + "입력값": {}, + "못읽음": [], + "상태": "후보", + "환산": null, + "들어감": false, + "후보": [ + { + "로직": "GF000229", + "이름": "큰돌붙이기 메붙이기", + "단위": "㎡", + "근거": { + "조항": [ + "산림품셈 13-7-1" + ], + "직종": [ + "작업반장", + "특별인부" + ], + "단위": true, + "점수": 1.65, + "까닭": "조항 같음 · 직종 작업반장 · 특별인부 · 단위 맞음" + }, + "점수": 1.65 + }, + { + "로직": "GF000230", + "이름": "큰돌붙이기 찰붙이기", + "단위": "㎡", + "근거": { + "조항": [ + "산림품셈 13-7-2" + ], + "직종": [ + "작업반장", + "특별인부" + ], + "단위": true, + "점수": 1.15, + "까닭": "앞 마디 같음 · 직종 작업반장 · 특별인부 · 단위 맞음" + }, + "점수": 1.15 + }, + { + "로직": "GC000262", + "이름": "메붙임(굴착기+부착용 집게)", + "단위": "㎡", + "점수": 0.817 + }, + { + "로직": "GF000224", + "이름": "돌붙임 메붙임(장비)", + "단위": "㎡", + "점수": 0.7 + }, + { + "로직": "GF000032", + "이름": "평떼 붙임", + "단위": "㎡", + "점수": 0.486 + } + ] + }, + "단산머리": { + "이름": "큰돌메붙임(40cm-60cm,(산림사업품셈))", + "단위": "㎡" + }, + "단산글": [ + "59.큰돌메붙임(40cm-60cm,(산림사업품셈))[㎡]", + " 1. 큰돌메붙임 (2026년 산림사업 품셈 13-7-1)", + " 가. 노무비", + " 작업반장 : 215,907 × 0.058 = 12,522.6", + " 특별인부 : 226,122 × 0.058 = 13,115", + " 보통인부 : 172,068 × 0.083 = 14,281.6", + " 인부소계 : 39,919.2", + " 나. 기계경비", + " 굴착기(무한궤도)(0.8㎥)[hr]", + " T = 0.24", + " 노무비 : 59,020 × T = 59,020 × 0.24 = 14,164.8", + " 재료비 : 25,684 × T = 25,684 × 0.24 = 6,164.1", + " 경 비 : 27,018 × T = 27,018 × 0.24 = 6,484.3", + " 소 계 : 26,813.2", + " 부착용집게(0.7~0.8㎥)", + " 노무비 : 0 × T = 0 × 0.24 = 0", + " 재료비 : 0 × T = 0 × 0.24 = 0", + " 경 비 : 5,786 × T = 5,786 × 0.24 = 1,388.6", + " 소 계 : 1,388.6", + " 다. 제잡비(품의 1%)", + " (12,522.6 + 13,115 + 14,281.6 + 14,164.8) × 1% = 540.8", + " 라. 소 계", + " 노무비 : 39,919.2 + 14,164.8 + 540.8 = 54,624.8", + " 재료비 : 6,164.1", + " 경 비 : 6,484.3 + 1,388.6 = 7,872.9", + " 계 : 68,661.8", + " 2. 합 계", + " 노무비 : 54,624", + " 재료비 : 6,164", + " 경 비 : 7,872", + " 계 : 68,660" + ], + "줄수": 2, + "호표들": [ + 19, + 20 + ], + "id": "S098" + }, + { + "파일": "북부청사방", + "호표": 21, + "호표이름": "큰돌바닥막이(H=0.5)", + "호표규격": "", + "호표단위": "M", + "차례": 2, + "이름": "큰돌찰붙임", + "규격": "40cm-60cm,(산림사업품셈)", + "수량": 3, + "단위": "㎡", + "단가": { + "계": 93974, + "노무비": 74747, + "재료비": 8205, + "경비": 11022 + }, + "갈래": "단가산출", + "참조": { + "종류": "단산", + "번": 61 + }, + "근거": { + "출처": [ + [ + "산림품셈", + "13-7-2" + ] + ], + "직종": [ + "작업반장", + "특별인부", + "보통인부" + ], + "행": [ + 2575, + 2616 + ] + }, + "맞춤": { + "열쇠": "큰돌찰붙임|40cm-60cm,(산림사업품셈)|m2", + "대상": null, + "입력값": {}, + "못읽음": [], + "상태": "후보", + "환산": null, + "들어감": false, + "후보": [ + { + "로직": "GF000230", + "이름": "큰돌붙이기 찰붙이기", + "단위": "㎡", + "근거": { + "조항": [ + "산림품셈 13-7-2" + ], + "직종": [ + "작업반장", + "특별인부" + ], + "단위": true, + "점수": 1.65, + "까닭": "조항 같음 · 직종 작업반장 · 특별인부 · 단위 맞음" + }, + "점수": 1.65 + }, + { + "로직": "GF000229", + "이름": "큰돌붙이기 메붙이기", + "단위": "㎡", + "근거": { + "조항": [ + "산림품셈 13-7-1" + ], + "직종": [ + "작업반장", + "특별인부" + ], + "단위": true, + "점수": 1.15, + "까닭": "앞 마디 같음 · 직종 작업반장 · 특별인부 · 단위 맞음" + }, + "점수": 1.15 + }, + { + "로직": "GC000263", + "이름": "찰붙임(굴착기+부착용 집게)", + "단위": "㎡", + "점수": 0.817 + }, + { + "로직": "GF000225", + "이름": "돌붙임 찰붙임(장비)", + "단위": "㎡", + "점수": 0.7 + }, + { + "로직": "GF000032", + "이름": "평떼 붙임", + "단위": "㎡", + "점수": 0.486 + } + ] + }, + "단산머리": { + "이름": "큰돌찰붙임(40cm-60cm,(산림사업품셈))", + "단위": "㎡" + }, + "단산글": [ + "61.큰돌찰붙임(40cm-60cm,(산림사업품셈))[㎡]", + " 1. 큰돌찰붙임 (2026년 산림사업 품셈 13-7-2)", + " 가. 노무비", + " 작업반장 : 215,907 × 0.058 = 12,522.6", + " 특별인부 : 226,122 × 0.101 = 22,838.3", + " 보통인부 : 172,068 × 0.101 = 17,378.8", + " 인부소계 : 52,739.7", + " 나. 기계경비", + " 굴착기(무한궤도)(0.8㎥+부착용집게)[hr]", + " T = 0.336", + " 노무비 : 59,020 × T = 59,020 × 0.336 = 19,830.7", + " 재료비 : 24,421 × T = 24,421 × 0.336 = 8,205.4", + " 경 비 : 32,805 × T = 32,805 × 0.336 = 11,022.4", + " 소 계 : 39,058.5", + " 다. 제잡비(품의 3%)", + " (12,522.6 + 22,838.3 + 17,378.8 + 19,830.7) × 3% = 2,177.1", + " 라. 소 계", + " 노무비 : 52,739.7 + 19,830.7 + 2,177.1 = 74,747.5", + " 재료비 : 8,205.4", + " 경 비 : 11,022.4", + " 계 : 93,975.3", + " 2. 합 계", + " 노무비 : 74,747", + " 재료비 : 8,205", + " 경 비 : 11,022", + " 계 : 93,974" + ], + "줄수": 7, + "호표들": [ + 21, + 22, + 23, + 24, + 25, + 26, + 27 + ], + "id": "S099" + } +] \ No newline at end of file diff --git a/docs/raw/조사/2026-10-15_338_실험자료/C1/s01_extract.py b/docs/raw/조사/2026-10-15_338_실험자료/C1/s01_extract.py new file mode 100644 index 000000000..405977a5a --- /dev/null +++ b/docs/raw/조사/2026-10-15_338_실험자료/C1/s01_extract.py @@ -0,0 +1,105 @@ +"""req225 C1 — 실무 엑셀 일위대가 줄마다 지금 맞춤 결과(기억 없음) + 단가산출 블록 글 뽑기. + +읽기만: 엑셀 · master_data · 우리 로직. 서버 · DB · storage 안 씀. +출력 = 같은 폴더 out_lines.json · out_ours.json +""" + +import json +import sys +from pathlib import Path + +ROOT = Path(r"C:\Program_coding\임도설계 및 견적자동화 프로그램 개발") +sys.path.insert(0, str(ROOT)) +HERE = Path(__file__).resolve().parent + +from M01_MasterData import M01_MasterData_Store as m01 # noqa: E402 +from M02_MasterTemplete import M02_Hopyo_Basis as hb # noqa: E402 +from M02_MasterTemplete import M02_Hopyo_Excel as hx # noqa: E402 +from M02_MasterTemplete import M02_Hopyo_Match as hm # noqa: E402 +from M02_MasterTemplete import M02_Labor_Inputs as li # noqa: E402 + +SAMPLES = ROOT / "resources/knowledge/original/실무문서/2026_실무일위대가 모음" +FILES = { + "북부청임도": SAMPLES / "2026년_북부지방산림청_임도사업_설계내역서(공통단가).xls", + "북부청사방": SAMPLES / "2026년_북부지방산림청_사방사업(계류+댐)_설계내역서.xls", +} + + +def block_text(rows, span): + start, col, _ = hb._columns(rows) + out = [] + for at in range(span[0], span[1] + 1): + row = rows[at - 1] + text = row[col] if col < len(row) else "" + if isinstance(text, str) and text.strip(): + out.append(text.rstrip()) + return out + + +def main(): + got = hm.targets() + lines_out = [] + for tag, path in FILES.items(): + data = path.read_bytes() + book = hx.read_book(path.name, data) + sheets = hx._sheets(path.name, data) + calc = sheets.get(hb.SHEET) or [] + heads = {b["번"]: b for b in book["단산"]} + units = hm.units_of(book) + for h in book["호표"]: + for line in h["줄"]: + m = hm.match_line(line, [], units, True, got) + ref = line.get("참조") or {} + rec = { + "파일": tag, + "호표": h["번"], + "호표이름": h["이름"], + "호표규격": h["규격"], + "호표단위": h["단위"], + "차례": line["차례"], + "이름": line["이름"], + "규격": line["규격"], + "수량": line["수량"], + "단위": line["단위"], + "단가": line["단가"], + "갈래": line["갈래"], + "참조": ref or None, + "근거": line.get("근거"), + "맞춤": m, + } + if ref.get("종류") == "단산" and ref["번"] in heads: + b = heads[ref["번"]] + rec["단산머리"] = {"이름": b["이름"], "단위": b["단위"]} + rec["단산글"] = block_text(calc, b["행"]) + lines_out.append(rec) + (HERE / "out_lines.json").write_text( + json.dumps(lines_out, ensure_ascii=False, indent=1), encoding="utf-8" + ) + # 우리 돈 로직 목록(읽기만) + table = hb.ours() + whole = m01.loaded()[1] + ours = [] + for one in got["로직"]: + row = whole.logic(one["로직"]) + o = table.get(one["로직"], {}) + ours.append( + { + "로직": one["로직"], + "이름": one["이름"], + "단위": one["단위"], + "구분": o.get("구분"), + "조항": sorted(o.get("조항") or []), + "직종": sorted(o.get("직종") or []), + "출처": str(li.sources(row) or ""), + "원문번호": row.get("원문번호"), + "입력": [str(s.get("이름")) for s in row.get("입력") or []], + } + ) + (HERE / "out_ours.json").write_text( + json.dumps(ours, ensure_ascii=False, indent=1), encoding="utf-8" + ) + print("lines", len(lines_out), "ours", len(ours), "mats", len(got["자재"])) + + +if __name__ == "__main__": + main() diff --git a/docs/raw/조사/2026-10-15_338_실험자료/C1/s02_view.py b/docs/raw/조사/2026-10-15_338_실험자료/C1/s02_view.py new file mode 100644 index 000000000..9f77cef1a --- /dev/null +++ b/docs/raw/조사/2026-10-15_338_실험자료/C1/s02_view.py @@ -0,0 +1,63 @@ +"""고유 로직 줄(단가산출 · 기계 · 기타)마다 엑셀 근거 · 블록 글 · 지금 후보 다섯 — 사람 판정용 보기. + +python s02_view.py <파일 태그> <시작> <끝> → 고유 줄 번호 범위만 찍음 · out_items.json 저장 +""" + +import json +import sys +from pathlib import Path + +HERE = Path(__file__).resolve().parent +LOGIC = ("단가산출", "기계", "기타") + + +def items(): + lines = json.loads((HERE / "out_lines.json").read_text(encoding="utf-8")) + out, seen = [], {} + for x in lines: + if x["갈래"] not in LOGIC: + continue + ref = (x.get("참조") or {}).get("번") + key = (x["파일"], x["이름"], x["규격"], x["단위"], ref) + if key in seen: + seen[key]["줄수"] += 1 + seen[key]["호표들"].append(x["호표"]) + continue + rec = {**x, "줄수": 1, "호표들": [x["호표"]]} + seen[key] = rec + out.append(rec) + for i, r in enumerate(out): + r["id"] = f"{'N' if r['파일'] == '북부청임도' else 'S'}{i:03d}" + return out + + +def show(r): + m = r["맞춤"] + b = r.get("근거") or {} + print(f"## {r['id']} [{r['파일']}] 줄{r['줄수']} 호표{sorted(set(r['호표들']))[:6]}") + print(f" 엑셀: {r['이름']} | 규격 {r['규격']} | 단위 {r['단위']} | 갈래 {r['갈래']} | 단가 {r['단가']}") + if r.get("단산머리"): + print(f" 단산{r['참조']['번']}: {r['단산머리']} 출처 {b.get('출처')} 직종 {b.get('직종')}") + for t in r.get("단산글", [])[1:16]: + t = " ".join(t.split()) + if t.startswith(("2. 합", "노무비 :", "재료비 :", "경 비 :", "계 :")): + continue + print(f" · {t[:110]}") + tgt = m.get("대상") + print(f" 상태 {m['상태']} 대상 {tgt}") + for c in m.get("후보") or []: + why = (c.get("근거") or {}).get("까닭", "") + print(f" - {c.get('로직') or c.get('자재')} {c['이름']} [{c['단위']}] {c['점수']} {why}") + + +if __name__ == "__main__": + allx = items() + (HERE / "out_items.json").write_text( + json.dumps(allx, ensure_ascii=False, indent=1), encoding="utf-8" + ) + tag = sys.argv[1] if len(sys.argv) > 1 else "" + lo, hi = (int(sys.argv[2]), int(sys.argv[3])) if len(sys.argv) > 3 else (0, 10**6) + pick = [r for r in allx if r["파일"].endswith(tag) or not tag] + print("총", len(allx), "고른 파일", len(pick)) + for r in pick[lo:hi]: + show(r) diff --git a/docs/raw/조사/2026-10-15_338_실험자료/C1/s03_find.py b/docs/raw/조사/2026-10-15_338_실험자료/C1/s03_find.py new file mode 100644 index 000000000..a30b2127a --- /dev/null +++ b/docs/raw/조사/2026-10-15_338_실험자료/C1/s03_find.py @@ -0,0 +1,18 @@ +"""우리 돈 로직 찾기 — python s03_find.py <정규식> [<정규식> …] (이름 · 출처 · 키 · 조항에서).""" + +import json +import re +import sys +from pathlib import Path + +HERE = Path(__file__).resolve().parent +ours = json.loads((HERE / "out_ours.json").read_text(encoding="utf-8")) +for pat in sys.argv[1:]: + rx = re.compile(pat) + print(f"== /{pat}/") + for o in ours: + hay = f"{o['로직']} {o['이름']} {o['출처']} {' '.join(o['조항'])}" + if rx.search(hay): + print( + f" {o['로직']} {o['이름']} [{o['단위']}] {o['구분']} | {o['출처'][:60]} | 직종 {','.join(o['직종'])} | 입력 {','.join(o['입력'])[:70]}" + ) diff --git a/docs/raw/조사/2026-10-15_338_실험자료/C1/s04_eval.py b/docs/raw/조사/2026-10-15_338_실험자료/C1/s04_eval.py new file mode 100644 index 000000000..954270aa0 --- /dev/null +++ b/docs/raw/조사/2026-10-15_338_실험자료/C1/s04_eval.py @@ -0,0 +1,257 @@ +"""req225 C1 — 정답 표시 대비 지표 · 개선 방법 흉내(같은 프로세스 안 함수 바꿔 끼움 · 저장소 안 고침). + +python s04_eval.py → 지표 표(지금 · 이름만 · 방법별) 찍고 out_eval.json 저장 +""" + +import contextlib +import json +import re +import sys +import unicodedata +from pathlib import Path + +ROOT = Path(r"C:\Program_coding\임도설계 및 견적자동화 프로그램 개발") +sys.path.insert(0, str(ROOT)) +HERE = Path(__file__).resolve().parent +sys.path.insert(0, str(HERE)) + +from M02_MasterTemplete import M02_Hopyo_Basis as hb # noqa: E402 +from M02_MasterTemplete import M02_Hopyo_Match as hm # noqa: E402 + +from labels import L # noqa: E402 + +ITEMS = json.loads((HERE / "out_items.json").read_text(encoding="utf-8")) +BY_ID = {r["id"]: r for r in ITEMS} +FILES = ("북부청임도", "북부청사방") + + +# ── 지표 ───────────────────────────────────────────────────────────── +def score(results: dict, ids=None) -> dict: + """results = {id: {상태, top: [키 …]}} → 지표(고유 줄 · 줄 수 무게).""" + ids = ids or list(BY_ID) + out = {} + for weight in ("고유", "줄"): + w = (lambda i: 1) if weight == "고유" else (lambda i: BY_ID[i]["줄수"]) + have = [i for i in ids if L[i]["정답"]] + none = [i for i in ids if not L[i]["정답"]] + tot = sum(w(i) for i in have) + + def rate(pred): + return round(100 * sum(w(i) for i in have if pred(i)) / tot, 1) if tot else 0 + + def strict(i): + return set(L[i]["정답"]) + + def loose(i): + return set(L[i]["정답"]) | set(L[i]["허용"]) + + top = {i: results[i]["top"] for i in ids} + out[weight] = { + "있음": sum(w(i) for i in have), + "없음": sum(w(i) for i in none), + "1위 엄격": rate(lambda i: top[i][:1] and top[i][0] in strict(i)), + "1위 느슨": rate(lambda i: top[i][:1] and top[i][0] in loose(i)), + "5안 엄격": rate(lambda i: bool(set(top[i][:5]) & strict(i))), + "5안 느슨": rate(lambda i: bool(set(top[i][:5]) & loose(i))), + "없음인데 후보": sum(w(i) for i in none if top[i]), + } + return out + + +def table(name: str, results: dict) -> dict: + row = {"전체": score(results)} + for f in FILES: + row[f] = score(results, [r["id"] for r in ITEMS if r["파일"] == f]) + return {"이름": name, **row} + + +def show(row: dict) -> None: + def cell(s): + u = s["고유"] + return f"1위 {u['1위 엄격']:>5}/{u['1위 느슨']:>5} 5안 {u['5안 엄격']:>5}/{u['5안 느슨']:>5} 없음후보 {u['없음인데 후보']}" + + print(f"{row['이름'][:22]:<22} | 전체 {cell(row['전체'])} | 임도 {cell(row['북부청임도'])} | 사방 {cell(row['북부청사방'])}") + + +# ── 돌리기 ─────────────────────────────────────────────────────────── +def stored() -> dict: + out = {} + for r in ITEMS: + m = r["맞춤"] + if m["상태"] == "같음": + top = [m["대상"].get("로직")] + else: + top = [c.get("로직") for c in m.get("후보") or []] + out[r["id"]] = {"상태": m["상태"], "top": [k for k in top if k]} + return out + + +def line_of(r: dict) -> dict: + keep = ("이름", "규격", "수량", "단위", "단가", "갈래", "참조", "근거") + return {k: r.get(k) for k in keep} + + +def run(fn=None) -> dict: + """지금 파이프라인(hm.match_line) — 바꿔 끼운 함수가 있으면 그것으로.""" + hm._targets.cache_clear() + hb._ours.cache_clear() + got = hm.targets() + out = {} + for r in ITEMS: + line = line_of(r) + m = (fn or default_line)(line, got, r) + out[r["id"]] = m + return out + + +def default_line(line, got, r): + m = hm.match_line(line, [], {}, True, got) + if m["상태"] == "같음": + top = [m["대상"].get("로직")] + else: + top = [c.get("로직") for c in m.get("후보") or []] + return {"상태": m["상태"], "top": [k for k in top if k]} + + +def name_only(line, got, r): + """330 앞(이름만) — 같음 · 씨앗 + 글자 두 개 묶음 후보 다섯.""" + found = hm._exact(line, "로직", got) + if len(found) == 1: + return {"상태": "같음", "top": [found[0]["로직"]]} + top = [x["로직"] for x in found] + [c["로직"] for c in hm.candidates(line, "로직", got, 5)] + return {"상태": "후보" if top else "없음", "top": list(dict.fromkeys(top))[:5]} + + +@contextlib.contextmanager +def patched(obj, **funcs): + old = {k: getattr(obj, k) for k in funcs} + for k, v in funcs.items(): + setattr(obj, k, v) + try: + yield + finally: + for k, v in old.items(): + setattr(obj, k, v) + hm._targets.cache_clear() + hb._ours.cache_clear() + + +# ── 방법 ① 이름 다듬기 — 괄호 글 살림 · 수 · 단위 · 규격 조각 뺌 ───────────── +_SPECISH = re.compile( + r"[a-z]\d?\s*[=:]\s*[\d.,]+\s*(mm|cm|m|km)?" # L=500m · D=1000mm + r"|[\d.,]+\s*(㎥|㎡|m3|m2|mm|cm|km|m|톤|ton|t|%|회|kg|인|본|개)?" # 0.6㎥ · 4.5톤 · 100% + r"|[×x*~∼:/+]", + re.I, +) +_FILLER = ("적용", "사용", "포함", "이상", "미만", "이하", "내외", "산림사업품셈", "산림사업", "품셈") + + +def tidy_keep(name) -> str: + text = unicodedata.normalize("NFKC", str(name or "")).lower() + for old, new in hm.MACH_WORDS: + text = text.replace(old, new) + text = _SPECISH.sub(" ", text) + for w in _FILLER: + text = text.replace(w, " ") + text = re.sub(r"[()\[\]{}<>,._\-·]", " ", text) + return hm.mm.plain(text) + + +# ── 방법 ② 같은 뜻 말 사전(임도 파일에서만 뽑음 · 사방은 처음 보는 셈) ───────── +SYN_NORTH = [ + ("레미콘", "레디믹스트콘크리트"), + ("깍기", "깎기"), + ("뒷채움", "뒤채움"), + ("붙이기", "붙임"), + ("휠라", "채움재"), + ("메쉬", "메시"), + ("야면석", "돌"), + ("소운반", "인력운반"), + ("면고르기", "절토면고르기"), + ("구조물터파기", "굴착"), + ("터파기", "굴착"), + ("비탈면보호공", "잔디붙임"), + ("평떼", "잔디붙임"), + ("콘크리트포장", "콘크리트표층포설"), + ("고임돌", "약돌"), + ("조약돌", "약돌"), +] + + +def with_syn(base, pairs): + def tidy_syn(name): + text = str(name or "") + flat = re.sub(r"\s+", "", unicodedata.normalize("NFKC", text)) + for old, new in pairs: + flat = flat.replace(old, new) + return base(flat) + + return tidy_syn + + +# ── 방법 ④ 글자 n-그램 ───────────────────────────────────────────────── +def grams_123(text: str) -> set: + out = set(text) + out |= {text[i : i + 2] for i in range(len(text) - 1)} + out |= {text[i : i + 3] for i in range(len(text) - 2)} + return out or {text} + + +def grams_3(text: str) -> set: + return {text[i : i + 3] for i in range(len(text) - 2)} or {text} + + +# ── 방법 ⑥ 근거 조항 — 엑셀이 윗마디(6-2 ⊃ 6-2-2) · 오타 13-114 → 13-11-4 ──────────── +def clause_prefix(theirs, our): + nums = [n for book, n in theirs if book == our["책"]] + same = sorted(n for n in nums if n in our["조항"]) + if same: + return 1.0, [f"{our['구분']} {n}" for n in same], "조항 같음" + kids = sorted(c for c in our["조항"] for n in nums if c.startswith(n + "-")) + if kids: + return 0.9, [f"{our['구분']} {n}" for n in kids], "조항 같음" # 윗마디 = 같음 취급 + heads = {hb._head(n) for n in nums} + near = sorted(n for n in our["조항"] if hb._head(n) in heads) + if near: + return 0.5, [f"{our['구분']} {n}" for n in near], "앞 마디 같음" + return 0.0, [], "" + + +_TYPO_CLAUSE = re.compile(r"^(\d+)-(\d)(\d)(\d)$") # 13-114 → 13-11-4 + + +def fix_typo(theirs): + out = [] + for book, n in theirs: + m = _TYPO_CLAUSE.match(n) + out.append([book, f"{m.group(1)}-{m.group(2)}{m.group(3)}-{m.group(4)}" if m else n]) + return out + + +def basis_prefix_line(line, got, r): + b = dict(line.get("근거") or {}) + if b.get("출처"): + b["출처"] = fix_typo(b["출처"]) + line = {**line, "근거": b} + return default_line(line, got, r) + + +if __name__ == "__main__": + rows = [] + rows.append(table("저장된 지금", stored())) + rows.append(table("지금 다시 돌림", run())) + rows.append(table("이름만(330 앞)", run(name_only))) + with patched(hm, tidy=tidy_keep): + rows.append(table("①이름 다듬기", run())) + with patched(hm, tidy=with_syn(hm.tidy, SYN_NORTH)): + rows.append(table("②사전(임도에서 뽑음)", run())) + with patched(hm, _grams=grams_123): + rows.append(table("④1·2·3그램", run())) + with patched(hm, _grams=grams_3): + rows.append(table("④3그램", run())) + with patched(hb, _clause=clause_prefix): + rows.append(table("⑥조항 윗마디", run())) + rows.append(table("⑥+오타 조항", run(basis_prefix_line))) + for row in rows: + show(row) + (HERE / "out_eval.json").write_text(json.dumps(rows, ensure_ascii=False, indent=1), "utf-8") diff --git a/docs/raw/조사/2026-10-15_338_실험자료/C1/s04b_diff.py b/docs/raw/조사/2026-10-15_338_실험자료/C1/s04b_diff.py new file mode 100644 index 000000000..d3c0f0827 --- /dev/null +++ b/docs/raw/조사/2026-10-15_338_실험자료/C1/s04b_diff.py @@ -0,0 +1,30 @@ +"""두 결과의 1위 · 5안 바뀐 줄 — python s04b_diff.py (s04_eval 함수 씀).""" +from s04_eval import * # noqa: F401,F403 + + +def mark(i, top): + s, l = set(L[i]["정답"]), set(L[i]["정답"]) | set(L[i]["허용"]) + t1 = top[0] if top else None + return ("O" if t1 in s else "o" if t1 in l else "x") + ("5" if set(top[:5]) & l else "-") + + +def diff(a, b, title): + print("==", title) + for i in BY_ID: + if not L[i]["정답"]: + continue + ma, mb = mark(i, a[i]["top"]), mark(i, b[i]["top"]) + if ma != mb: + r = BY_ID[i] + print(f" {i} {r['이름'][:18]:<18} {ma}->{mb} {a[i]['top'][:2]} -> {b[i]['top'][:2]} 정답{L[i]['정답']}") + + +if __name__ == "__main__": + base = run() + with patched(hm, tidy=with_syn(hm.tidy, SYN_NORTH)): + diff(base, run(), "② 사전") + with patched(hm, tidy=tidy_keep): + diff(base, run(), "① 다듬기") + with patched(hb, _clause=clause_prefix): + diff(base, run(), "⑥ 윗마디") + diff(run(name_only), base, "이름만 → 지금") diff --git a/docs/raw/조사/2026-10-15_338_실험자료/C1/s05_more.py b/docs/raw/조사/2026-10-15_338_실험자료/C1/s05_more.py new file mode 100644 index 000000000..d359543d1 --- /dev/null +++ b/docs/raw/조사/2026-10-15_338_실험자료/C1/s05_more.py @@ -0,0 +1,330 @@ +"""req225 C1 — 방법 더: 이름만 쪽 방법(①②③④) · 책 맞춤 · 단위 거름망 · 직종 · 장비 겹침 · 금액 · +같음 근거 검사 · 결합. 저장소 안 고침(같은 프로세스 안 바꿔 끼움 · 읽기만). + +python s05_more.py money → 정답 로직 값 ↔ 엑셀 단가 비(진단) · out_money.json +python s05_more.py → 방법별 지표 · out_more.json +""" + +import difflib +import json +import re +import sys +import unicodedata +from functools import lru_cache + +from s04_eval import ( # noqa: F401 + BY_ID, + HERE, + ITEMS, + L, + SYN_NORTH, + clause_prefix, + default_line, + grams_123, + hb, + hm, + name_only, + patched, + run, + show, + table, + tidy_keep, + with_syn, +) + +from M01_MasterData import M01_MasterData_Store as m01 # noqa: E402 +from M02_MasterTemplete import M02_Labor_Doc as ldoc # noqa: E402 + +WHOLE = m01.loaded()[1] +_PER = re.compile(r"원\s*/\s*(\d[\d,]*\.?\d*|\.\d+)?\s*([^\s·(\d,.][^\s·(]*)") + + +# ── 이름만 쪽 방법 ──────────────────────────────────────────────────── +def name_words(line, got, r): + """③ 낱말 차례 무시 — 괄호 글 포함 뜻 낱말 겹침(부분 글 포함)을 점수에 더함.""" + base = name_only(line, got, r) + if base["상태"] == "같음": + return base + mine = hb._loose_words(f"{line.get('이름')} {line.get('규격')}") + scored = [] + for one in got["로직"]: + theirs = hb._loose_words(one["이름"]) + hit = sum(1 for a in mine if any(a in b or b in a for b in theirs)) + if not hit: + continue + g1, g2 = hm._grams(hm.tidy(line.get("이름"))), one["grams"] + dice = 2 * len(g1 & g2) / (len(g1) + len(g2)) + s = dice + 0.5 * hit / max(len(mine), 1) + (0.15 if hm.fits(line.get("단위"), one["단위"]) else 0) + scored.append((s, one["로직"])) + scored.sort(key=lambda x: -x[0]) + top = list(dict.fromkeys(k for _, k in scored))[:5] + return {"상태": "후보" if top else "없음", "top": top} + + +def name_difflib(line, got, r): + """④' difflib 비(가장 긴 같은 조각) — 정리 이름끼리.""" + base = name_only(line, got, r) + if base["상태"] == "같음": + return base + me = hm.tidy(line.get("이름")) + scored = [] + for one in got["로직"]: + s = difflib.SequenceMatcher(None, me, one["tidy"]).ratio() + s += 0.15 if hm.fits(line.get("단위"), one["단위"]) else 0 + scored.append((s, one["로직"])) + scored.sort(key=lambda x: -x[0]) + return {"상태": "후보", "top": [k for _, k in scored[:5]]} + + +# ── 책 맞춤 — 엑셀 출처 책(산림 · 건설)과 같은 책 덤(지금 = 산림이면 늘 0.05) ────────── +def book_of(line): + src = ((line.get("근거") or {}).get("출처") or []) + return src[0][0] if src else None + + +def candidates_book(line, pool, got=None, limit=5): + me = hm.tidy(line.get("이름")) + grams, unit = hm._grams(me), line.get("단위") + text = hm.mm.plain_spec(f"{line.get('이름')}{line.get('규격')}") + want = book_of(line) + scored, seen = [], set() + mine = hm.words(line.get("이름")) + for one in (got or hm.targets())[pool]: + if not grams & one["grams"] or not hm.near(mine, one["words"]): + continue + s = 2 * len(grams & one["grams"]) / (len(grams) + len(one["grams"])) + s += 1 if one["tidy"] == me else 0 + s += 0.15 if hm.fits(unit, one["단위"]) else 0 + if want: + s += 0.2 if ("산림품셈" if one.get("산림") else "건설품셈") == want else 0 + else: + s += 0.05 if one.get("산림") else 0 + s += 0.5 if one.get("spec") and one["spec"] in text else 0 + scored.append((s, one)) + scored.sort(key=lambda x: -x[0]) + out = [] + for s, one in scored: + label = (one["이름"], one.get("규격"), one["단위"]) + if label in seen: + continue + seen.add(label) + out.append({**hm._public(one), "점수": round(s, 3)}) + if len(out) == limit: + break + return out + + +# ── ⑤ 단위 거름망 ───────────────────────────────────────────────────── +def unit_filter_line(line, got, r): + m = default_line(line, got, r) + if m["상태"] == "같음": + return m + units = {x["로직"]: x["단위"] for x in got["로직"]} + keep = [k for k in m["top"] if hm.fits(line.get("단위"), units.get(k))] + return {"상태": "후보" if keep else "없음", "top": keep} + + +# ── ⑦ 직종 · 장비 구성 겹침(조항 없이도) ─────────────────────────────────── +MACH = { + "굴착기": ("굴착기", "굴삭기", "백호", "back-hoe", "backhoe"), + "불도저": ("불도저", "불도우저"), + "덤프": ("덤프",), + "크레인": ("크레인",), + "롤러": ("롤러", "로울러"), + "브레이커": ("브레이커", "브레이카"), + "펌프차": ("펌프차", "펌프카"), + "집게": ("집게",), + "콤팩터": ("콤팩터", "컴팩터"), + "살수": ("살수차", "물탱크"), + "화물차": ("화물자동차", "화물차"), +} +_EQ = re.compile(r"\d{4}-\d{4}") + + +def mach_types(text: str) -> set: + t = unicodedata.normalize("NFKC", text).lower() + return {k for k, ws in MACH.items() if any(w in t for w in ws)} + + +@lru_cache(maxsize=4096) +def our_mach(code: str) -> frozenset: + row = WHOLE.logic(code) + eq = WHOLE.numbers.get("EQ", {}) + bits = [] + for h in row.get("호표") or []: + el = str(h.get("요소") or "") + bits.append(el) + bits += [f"{eq[c].get('상세구분')} {eq[c].get('이름')}" for c in _EQ.findall(el) if c in eq] + for s in row.get("입력") or []: + for v in s.get("고르기") or []: + if isinstance(v, str) and v in eq: + bits.append(f"{eq[v].get('상세구분')} {eq[v].get('이름')}") + bits.append(str(s.get("이름") or "")) + return frozenset(mach_types(" ".join(bits))) + + +def excel_mach(r) -> set: + return mach_types(" ".join(r.get("단산글") or []) + " " + str(r.get("규격") or "")) + + +def compose_line(line, got, r, pool_n=40): + """지금 합친 차례(다섯 자르기 전) + 직종 · 장비 겹침 덤 — 다시 다섯.""" + m = hm.match_line(line, [], {}, True, got) + if m["상태"] == "같음": + return {"상태": "같음", "top": [m["대상"]["로직"]]} + basis = hb.candidates(line, got) + named = hm.candidates(line, "로직", got, None) + ordered = hb.merge(basis, named)[:pool_n] + trades = set((line.get("근거") or {}).get("직종") or []) - {"보통인부"} + em = excel_mach(r) + ours = hb.ours() + rescored = [] + for at, c in enumerate(ordered): + k = c.get("로직") + s = c["점수"] + ot = ours.get(k, {}).get("직종", set()) - {"보통인부"} + if trades: + s += 0.4 * len(trades & ot) / len(trades) + om = our_mach(k) + if em or om: + s += 0.3 * len(em & om) / max(len(em | om), 1) + rescored.append((s, -at, k)) + rescored.sort(reverse=True) + top = [k for _, _, k in rescored] + first = next((c.get("로직") for c in named), None) + five = top[:5] if first in top[:5] or first is None else top[:4] + [first] + return {"상태": "후보" if five else "없음", "top": five} + + +# ── ⑧ 금액 — 후보 로직 값(자동값 + 규격 입력) ↔ 엑셀 단가 ──────────────────── +@lru_cache(maxsize=8192) +def price(code: str, given_json: str) -> tuple: + got = ldoc.run_line(WHOLE, code, json.loads(given_json)) + base = got.get("기준") + if not base: + return (None, got.get("까닭")) + row = WHOLE.logic(code) + per = _PER.findall(str(row.get("결과단위") or "")) + times = float(per[0][0].replace(",", "")) if per and per[0][0] else 1.0 + return (float(base["계"]) / times, None) + + +def money_ratio(line, code, unit_of) -> float | None: + try: + specs = hm._logic_specs(code) + values, _ = hm.spec_inputs(line, specs) + except Exception: # noqa: BLE001 — 진단용 + values = {} + p, _why = price(code, json.dumps(values, ensure_ascii=False, sort_keys=True, default=str)) + excel = (line.get("단가") or {}).get("계") + if p is None or not excel: + return None + conv = hm.factor(line.get("단위"), unit_of.get(code), line.get("규격")) + if conv["배"] is None: + return None + return p * float(conv["배"]) / float(excel) + + +def money_line(line, got, r, pool_n=15): + m = hm.match_line(line, [], {}, True, got) + if m["상태"] == "같음": + return {"상태": "같음", "top": [m["대상"]["로직"]]} + basis = hb.candidates(line, got) + named = hm.candidates(line, "로직", got, None) + ordered = hb.merge(basis, named)[:pool_n] + unit_of = {x["로직"]: x["단위"] for x in got["로직"]} + rescored = [] + for at, c in enumerate(ordered): + k = c.get("로직") + s = c["점수"] + q = money_ratio(line, k, unit_of) + if q is not None and q > 0: + gap = abs(q - 1) + s += 0.6 if gap <= 0.15 else 0.3 if gap <= 0.35 else 0 + rescored.append((s, -at, k)) + rescored.sort(reverse=True) + top = [k for _, _, k in rescored] + first = next((c.get("로직") for c in named), None) + five = top[:5] if first in top[:5] or first is None else top[:4] + [first] + return {"상태": "후보" if five else "없음", "top": five} + + +# ── ⑪ 같음도 근거 검사 — 이름 같음이 엑셀 출처 책 · 조항과 어긋나면 근거 후보를 앞에 ─────── +def same_check_line(line, got, r): + m = hm.match_line(line, [], {}, True, got) + if m["상태"] != "같음": + return default_line(line, got, r) + tgt = m["대상"]["로직"] + theirs = (line.get("근거") or {}).get("출처") or [] + if not theirs: + return {"상태": "같음", "top": [tgt]} + our = hb.ours().get(tgt) + sc, _, _ = hb._clause(theirs, our) if our else (0, None, None) + if sc >= 0.9: + return {"상태": "같음", "top": [tgt]} + basis = [c for c in hb.candidates(line, got) if c["근거"]["까닭"].startswith("조항 같음")] + if not basis: + return {"상태": "같음", "top": [tgt]} + top = list(dict.fromkeys([basis[0]["로직"], tgt] + [c["로직"] for c in basis[1:]]))[:5] + return {"상태": "후보", "top": top} + + +def diag_money(): + got = hm.targets() + unit_of = {x["로직"]: x["단위"] for x in got["로직"]} + rows = [] + for r in ITEMS: + lab = L[r["id"]] + if not lab["정답"]: + continue + line = {k: r.get(k) for k in ("이름", "규격", "단위", "단가", "갈래", "참조", "근거")} + right = {k: money_ratio(line, k, unit_of) for k in lab["정답"]} + top = (r["맞춤"].get("후보") or [{}])[0].get("로직") or (r["맞춤"].get("대상") or {}).get("로직") + rows.append( + { + "id": r["id"], + "이름": r["이름"], + "엑셀": (r.get("단가") or {}).get("계"), + "정답 비": right, + "1위": top, + "1위 비": money_ratio(line, top, unit_of) if top and top not in lab["정답"] else None, + } + ) + (HERE / "out_money.json").write_text(json.dumps(rows, ensure_ascii=False, indent=1), "utf-8") + best = [min((abs(v - 1) for v in x["정답 비"].values() if v), default=None) for x in rows] + have = [b for b in best if b is not None] + print("정답 값 셈 됨", len(have), "/", len(rows)) + for cut in (0.05, 0.15, 0.35, 1.0): + print(f" |비-1| ≤ {cut}: {sum(1 for b in have if b <= cut)}") + for x in rows: + print(f" {x['id']} {x['이름'][:16]:<16} 엑셀 {x['엑셀']} 정답비 { {k: round(v, 2) if v else v for k, v in x['정답 비'].items()} } 1위 {x['1위']} {round(x['1위 비'], 2) if x['1위 비'] else ''}") + + +if __name__ == "__main__": + if sys.argv[1:] == ["money"]: + diag_money() + sys.exit() + rows = [table("지금", run()), table("이름만", run(name_only))] + with patched(hm, tidy=tidy_keep): + rows.append(table("이름만+①다듬기", run(name_only))) + with patched(hm, tidy=with_syn(hm.tidy, SYN_NORTH)): + rows.append(table("이름만+②사전", run(name_only))) + with patched(hm, tidy=with_syn(tidy_keep, SYN_NORTH)): + rows.append(table("이름만+①②", run(name_only))) + rows.append(table("이름만+③낱말", run(name_words))) + with patched(hm, _grams=grams_123): + rows.append(table("이름만+④123그램", run(name_only))) + rows.append(table("이름만+④difflib", run(name_difflib))) + with patched(hm, candidates=candidates_book): + rows.append(table("책 맞춤", run())) + rows.append(table("⑤단위 거름망", run(unit_filter_line))) + rows.append(table("⑦직종·장비 겹침", run(compose_line))) + rows.append(table("⑧금액", run(money_line))) + rows.append(table("⑪같음 근거 검사", run(same_check_line))) + with patched(hb, _clause=clause_prefix), patched(hm, tidy=tidy_keep, candidates=candidates_book): + rows.append(table("결합 ⑥①책", run())) + rows.append(table("결합 ⑥①책⑪", run(same_check_line))) + rows.append(table("결합 ⑥①책⑪⑧", run(lambda a, b, c: money_line(a, b, c) if hm.match_line(a, [], {}, True, b)["상태"] != "같음" else same_check_line(a, b, c)))) + for row in rows: + show(row) + (HERE / "out_more.json").write_text(json.dumps(rows, ensure_ascii=False, indent=1), "utf-8") diff --git a/docs/raw/조사/2026-10-15_338_실험자료/C1/s05b_diff.py b/docs/raw/조사/2026-10-15_338_실험자료/C1/s05b_diff.py new file mode 100644 index 000000000..3f1845ee9 --- /dev/null +++ b/docs/raw/조사/2026-10-15_338_실험자료/C1/s05b_diff.py @@ -0,0 +1,17 @@ +from s04b_diff import diff +from s05_more import * # noqa: F401,F403 + +if __name__ == "__main__": + base = run() + with patched(hm, candidates=candidates_book): + diff(base, run(), "책 맞춤") + with patched(hb, _clause=clause_prefix), patched(hm, tidy=tidy_keep, candidates=candidates_book): + diff(base, run(), "결합 ⑥①책") + with patched(hb, _clause=clause_prefix): + p6 = run() + with patched(hb, _clause=clause_prefix), patched(hm, tidy=tidy_keep): + diff(p6, run(), "⑥ → ⑥①") + diff(base, run(compose_line), "⑦") + diff(base, run(money_line), "⑧") + diff(base, run(same_check_line), "⑪") + diff(base, run(unit_filter_line), "⑤") diff --git a/docs/raw/조사/2026-10-15_338_실험자료/C1/s06_memory.py b/docs/raw/조사/2026-10-15_338_실험자료/C1/s06_memory.py new file mode 100644 index 000000000..d31f7cf4a --- /dev/null +++ b/docs/raw/조사/2026-10-15_338_실험자료/C1/s06_memory.py @@ -0,0 +1,103 @@ +"""⑨ 맞춤 기억 재사용 — 한 파일 정답을 기억으로 넣고 다른 파일에 몇 줄이 바로 들어가는지(열쇠 꼴별). ++ 낮은 확신 표시(점수 문턱) 흉내 · ⑥①⑪ 결합(책 빼고) · 사전 덤(글자 묶음에만).""" + +import json +import re +import unicodedata + +from s04_eval import ( # noqa: F401 + BY_ID, + HERE, + ITEMS, + L, + SYN_NORTH, + clause_prefix, + hb, + hm, + patched, + run, + show, + table, + tidy_keep, + with_syn, +) +from s05_more import same_check_line + +KEYS = { + "지금(품명|규격|단위)": lambda r: hm.line_key(r), + "품명|단위": lambda r: f"{hm.mm.plain(r['이름'])}|{hm.mm.plain_unit(r['단위'])}", + "정리이름|단위": lambda r: f"{hm.tidy(r['이름'])}|{hm.mm.plain_unit(r['단위'])}", + "다듬은이름+규격낱말|단위": lambda r: f"{tidy_keep(str(r['이름']) + ' ' + str(r['규격']))}|{hm.mm.plain_unit(r['단위'])}", +} + + +def reuse(src: str, dst: str) -> None: + for name, key in KEYS.items(): + memo = {} + for r in ITEMS: + if r["파일"] == src and L[r["id"]]["정답"]: + memo.setdefault(key(r), L[r["id"]]["정답"][0]) + hit = ok = lines = ok_lines = 0 + for r in ITEMS: + if r["파일"] != dst: + continue + k = memo.get(key(r)) + if k is None: + continue + hit += 1 + lines += r["줄수"] + good = k in set(L[r["id"]]["정답"]) | set(L[r["id"]]["허용"]) + ok += good + ok_lines += r["줄수"] if good else 0 + n = sum(1 for r in ITEMS if r["파일"] == dst) + nl = sum(r["줄수"] for r in ITEMS if r["파일"] == dst) + print(f" {src}→{dst} {name:<22} 고유 {hit}/{n} 맞음 {ok} · 줄 {lines}/{nl} 맞음 {ok_lines}") + + +def threshold() -> None: + """1순위 점수 문턱 — 없음 줄 엉뚱 후보 숨김 대 정답 1순위 잃음.""" + rows = [] + for r in ITEMS: + m = r["맞춤"] + if m["상태"] != "후보": + continue + top = (m.get("후보") or [{}])[0] + lab = L[r["id"]] + good = top.get("로직") in set(lab["정답"]) | set(lab["허용"]) + rows.append((top.get("점수", 0), bool(lab["정답"]), good, r["id"])) + for cut in (0.5, 0.6, 0.8, 1.0): + hidden_none = sum(1 for s, have, g, _ in rows if s < cut and not have) + lost = sum(1 for s, have, g, _ in rows if s < cut and have and g) + wrong_hidden = sum(1 for s, have, g, _ in rows if s < cut and have and not g) + print(f" 문턱 {cut}: 없음 줄 숨김 {hidden_none} · 맞는 1순위 숨김 {lost} · 틀린 1순위 숨김 {wrong_hidden}") + print(" 없음 줄 1순위 점수", sorted((round(s, 3), i) for s, have, g, i in rows if not have)) + + +def grams_syn(pairs): + base = hm._grams + + def g(text: str) -> set: + flat = unicodedata.normalize("NFKC", str(text)) + for old, new in pairs: + flat = flat.replace(old, new) + return base(text) | base(flat) + + return g + + +if __name__ == "__main__": + print("⑨ 기억 재사용(정답 첫 로직을 기억으로)") + reuse("북부청임도", "북부청사방") + reuse("북부청사방", "북부청임도") + print("낮은 확신 문턱(지금 후보 줄)") + threshold() + rows = [table("지금", run())] + with patched(hb, _clause=clause_prefix), patched(hm, tidy=tidy_keep): + rows.append(table("결합 ⑥①⑪", run(same_check_line))) + with patched(hm, _grams=grams_syn(SYN_NORTH)): + rows.append(table("②사전(글자 묶음만)", run())) + with patched(hb, _clause=clause_prefix), patched(hm, tidy=tidy_keep, _grams=grams_syn(SYN_NORTH)): + rows.append(table("결합 ⑥①⑪②", run(same_check_line))) + for row in rows: + show(row) + (HERE / "out_memory.json").write_text(json.dumps(rows, ensure_ascii=False, indent=1), "utf-8") diff --git a/docs/raw/조사/2026-10-15_338_실험자료/C1/s07_fail.py b/docs/raw/조사/2026-10-15_338_실험자료/C1/s07_fail.py new file mode 100644 index 000000000..f0d7d4c98 --- /dev/null +++ b/docs/raw/조사/2026-10-15_338_실험자료/C1/s07_fail.py @@ -0,0 +1,33 @@ +"""실패 줄 목록 — 이름만 · 지금 · 결합 ⑥①⑪ 의 1순위 엄격 실패(+ 5안 느슨 실패) · 갈래 꼬리표.""" +import json +from s04_eval import BY_ID, HERE, ITEMS, L, clause_prefix, hb, hm, name_only, patched, run, tidy_keep +from s05_more import same_check_line + +NAMES = {} + + +def fails(res, title): + print("==", title) + out = [] + for i, r in BY_ID.items(): + lab = L[i] + top = res[i]["top"] + s, l = set(lab["정답"]), set(lab["정답"]) | set(lab["허용"]) + if not s: + continue + t1 = top[0] if top else None + if t1 in s: + continue + kind = "느슨맞음" if t1 in l else ("5안" if set(top[:5]) & l else "5밖") + out.append({"id": i, "줄수": r["줄수"], "이름": r["이름"], "1위": t1, "형": kind, "갈래": lab["갈래"]}) + print(f" {i} 줄{r['줄수']:>2} {r['이름'][:20]:<20} 1위 {t1} {kind:<5} {lab['갈래']}") + return out + + +if __name__ == "__main__": + got = {} + got["이름만"] = fails(run(name_only), "이름만(330 앞)") + got["지금"] = fails(run(), "지금") + with patched(hb, _clause=clause_prefix), patched(hm, tidy=tidy_keep): + got["결합"] = fails(run(same_check_line), "결합 ⑥①⑪") + (HERE / "out_fail.json").write_text(json.dumps(got, ensure_ascii=False, indent=1), "utf-8") diff --git a/docs/raw/조사/2026-10-15_338_실험자료/C1/s09_stmate.py b/docs/raw/조사/2026-10-15_338_실험자료/C1/s09_stmate.py new file mode 100644 index 000000000..b2bf2606c --- /dev/null +++ b/docs/raw/조사/2026-10-15_338_실험자료/C1/s09_stmate.py @@ -0,0 +1,124 @@ +"""셋째 파일(다른 꼴 · STmate · 동부청 영월 2024) — 지금 읽기는 「일위대가 시트 없음」 400 이라 +여기서 따로 읽어(일위대가표 D 코드 줄 · X 기계 줄 + 단가산출근거 블록) 같은 맞춤 함수에 넣어 봄. + +python s09_stmate.py view → 고유 줄 · 블록 글 · 지금 후보(판정용) · out_stmate.json +""" + +import json +import re +import sys +import warnings +from pathlib import Path + +import openpyxl + +from s04_eval import HERE, hb, hm, name_only # noqa: F401 + +warnings.filterwarnings("ignore") +ROOT = Path(r"C:\Program_coding\임도설계 및 견적자동화 프로그램 개발") +FILE = ROOT / "resources/knowledge/original/실무문서/05. 동부청 영월/내역서/2024년 임도시설사업2차(산불진화_기번6)-영월.xlsx" +_HOPYO = re.compile(r"^\s*제\s*(\d+)\s*호표") +_CODE = re.compile(r"^([DXLMB])\d{5}$") + + +def _num(v): + try: + return float(str(v).replace(",", "")) + except (TypeError, ValueError): + return None + + +def read(): + book = openpyxl.load_workbook(FILE, read_only=True, data_only=True) + rows = [list(r) for r in book["일위대가표"].iter_rows(values_only=True)] + calc = [list(r) for r in book["단가산출근거"].iter_rows(values_only=True)] + book.close() + lines, labor, no = [], {}, None + for r in rows: + head = str(r[0] or "") + hit = _HOPYO.match(head) + if hit: + no = int(hit.group(1)) + continue + code = str(r[12] or "").strip() if len(r) > 12 else "" + m = _CODE.match(code) + if not m or not r[0]: + continue + if m.group(1) == "L": + labor[code] = str(r[0]).strip() + if m.group(1) in "DX": + lines.append( + { + "호표": no, + "이름": str(r[0]).strip(), + "규격": str(r[1] or "").strip(), + "단위": str(r[3] or "").strip(), + "단가": {"계": _num(r[4]), "노무비": _num(r[6]), "재료비": _num(r[8]), "경비": _num(r[10])}, + "갈래": "단가산출" if m.group(1) == "D" else "기계", + "코드": code, + } + ) + blocks, cur = {}, None + for at, r in enumerate(calc, 1): + first = str(r[0] or "").strip() + second = str(r[1] or "").strip() if len(r) > 1 else "" + if _CODE.match(second) and second.startswith("D") and len(r) > 2 and _num(r[2]) is not None: + cur = blocks.setdefault(second, {"글": [], "행": [at, at], "직종코드": []}) + continue + if cur is None: + continue + cur["행"][1] = at + if second: + cur["글"].append(second) + lc = re.match(r"^(L\d{5})L$", first) + if lc: + cur["직종코드"].append(lc.group(1)) + names = hb.trade_names() + seen, out = {}, [] + for x in lines: + key = (x["이름"], x["규격"], x["단위"], x["코드"]) + if key in seen: + seen[key]["줄수"] += 1 + continue + b = blocks.get(x["코드"]) + if b: + text = " ".join(b["글"]) + trades = [] + for c in b["직종코드"]: + nm = names.get(re.sub(r"\s+", "", labor.get(c, ""))) + if nm and nm not in trades: + trades.append(nm) + x["근거"] = {"출처": hb.sources(text), "직종": trades, "행": b["행"]} + x["단산글"] = b["글"][:40] + x["줄수"] = 1 + x["id"] = f"Y{len(out):03d}" + x["파일"] = "동부청영월" + seen[key] = x + out.append(x) + return out + + +if __name__ == "__main__": + items = read() + got = hm.targets() + for x in items: + line = {k: x.get(k) for k in ("이름", "규격", "단위", "단가", "갈래", "근거")} + m = hm.match_line(line, [], {}, True, got) + x["맞춤"] = m + x["이름만"] = name_only(line, got, x) + (HERE / "out_stmate.json").write_text(json.dumps(items, ensure_ascii=False, indent=1), "utf-8") + print("고유", len(items), "줄", sum(x["줄수"] for x in items)) + if sys.argv[1:] == ["view"]: + for x in items: + b = x.get("근거") or {} + print(f"## {x['id']} 줄{x['줄수']} {x['이름']} | {x['규격']} | {x['단위']} | {x['갈래']} | 단가 {x['단가']['계']}") + print(f" 출처 {b.get('출처')} 직종 {b.get('직종')}") + for t in (x.get("단산글") or [])[1:9]: + print(f" · {' '.join(t.split())[:100]}") + m = x["맞춤"] + if m["상태"] == "같음": + print(f" 같음 {m['대상']}") + for c in m.get("후보") or []: + why = (c.get("근거") or {}).get("까닭", "") + print(f" - {c.get('로직')} {c['이름']} [{c['단위']}] {c['점수']} {why}") + print(f" 이름만 {x['이름만']['top'][:3]}") diff --git a/docs/raw/조사/2026-10-15_338_실험자료/C1/s10_all.py b/docs/raw/조사/2026-10-15_338_실험자료/C1/s10_all.py new file mode 100644 index 000000000..e7eefa3f1 --- /dev/null +++ b/docs/raw/조사/2026-10-15_338_실험자료/C1/s10_all.py @@ -0,0 +1,201 @@ +"""req225 C1 — 세 파일(북부청 임도 · 사방 · 동부청 영월 STmate) 함께 지표 + 방법 더 +(⑫ 블록 글 낱말 · ⑬ 같은 이름 접힘 고침 · ⑭ 부문 · 씨앗 끔). 저장소 안 고침. + +python s10_all.py → 표 · out_all.json +""" + +import json +import re + +import s04_eval as E +from s04_eval import clause_prefix, fix_typo, hb, hm, name_only, patched, tidy_keep +from s05_more import compose_line, money_line, same_check_line + +from labels import L +from labels_y import LY + +Y = json.loads((E.HERE / "out_stmate.json").read_text(encoding="utf-8")) +ALL = E.ITEMS + Y +LAB = {**L, **LY} +BY = {r["id"]: r for r in ALL} +GROUPS = { + "북부청임도": [r["id"] for r in ALL if r["파일"] == "북부청임도"], + "북부청사방": [r["id"] for r in ALL if r["파일"] == "북부청사방"], + "동부청영월": [r["id"] for r in ALL if r["파일"] == "동부청영월"], +} + + +def score(results, ids): + have = [i for i in ids if LAB[i]["정답"]] + none = [i for i in ids if not LAB[i]["정답"]] + + def rate(pred, w=lambda i: 1): + tot = sum(w(i) for i in have) + return round(100 * sum(w(i) for i in have if pred(i)) / tot, 1) if tot else 0 + + s = {i: set(LAB[i]["정답"]) for i in ids} + lo = {i: set(LAB[i]["정답"]) | set(LAB[i]["허용"]) for i in ids} + top = {i: results[i]["top"] for i in ids} + lines = lambda i: BY[i]["줄수"] # noqa: E731 + return { + "고유": len(ids), + "있음": len(have), + "없음": len(none), + "1위 엄격": rate(lambda i: bool(top[i]) and top[i][0] in s[i]), + "1위 느슨": rate(lambda i: bool(top[i]) and top[i][0] in lo[i]), + "5안 엄격": rate(lambda i: bool(set(top[i][:5]) & s[i])), + "5안 느슨": rate(lambda i: bool(set(top[i][:5]) & lo[i])), + "줄 1위 느슨": rate(lambda i: bool(top[i]) and top[i][0] in lo[i], lines), + "줄 5안 느슨": rate(lambda i: bool(set(top[i][:5]) & lo[i]), lines), + "없음인데 후보": sum(1 for i in none if top[i]), + } + + +def table(name, results): + row = {"이름": name, "전체": score(results, list(BY))} + row.update({g: score(results, ids) for g, ids in GROUPS.items()}) + return row + + +def show(row): + def c(s): + return f"{s['1위 엄격']:>5}/{s['1위 느슨']:>5} {s['5안 엄격']:>5}/{s['5안 느슨']:>5}" + + print( + f"{row['이름'][:24]:<24}| 전체 {c(row['전체'])} | 임도 {c(row['북부청임도'])} | 사방 {c(row['북부청사방'])} | 영월 {c(row['동부청영월'])} | 없음후보 {row['전체']['없음인데 후보']}" + ) + + +def run(fn=None): + hm._targets.cache_clear() + hb._ours.cache_clear() + got = hm.targets() + out = {} + for r in ALL: + line = {k: r.get(k) for k in ("이름", "규격", "수량", "단위", "단가", "갈래", "참조", "근거")} + CURRENT["r"] = r + out[r["id"]] = (fn or E.default_line)(line, got, r) + return out + + +CURRENT = {"r": None} + + +# ── ⑫ 블록 글 낱말 — 우리 로직 이름 낱말이 엑셀 단가산출 블록 글에 나오면 덤 · 후보에 들임 ────── +_GENERIC = frozenset( + "인력 기계 장비 소계 합계 노무비 재료비 경비 적용 기준 별도계상 별도 인부 보통인부 특별인부 설치 해체 " + "운반 작업 시공 사용 포함 공구손료 경장비 손료 기계경비 재료 인건비 품셈 표준품셈 건설표준품셈 이하 이상".split() +) + + +def block_words(r) -> str: + text = " ".join((r.get("단산글") or [])[:30]) + return re.sub(r"\s+", "", text) + + +def our_words(name): + return {w for w in hb._loose_words(name) if w not in _GENERIC and len(w) >= 2} + + +def block_line(line, got, r, pool_n=40): + m = hm.match_line(line, [], {}, True, got) + if m["상태"] == "같음": + return {"상태": "같음", "top": [m["대상"]["로직"]]} + text = block_words(r) + basis = hb.candidates(line, got) + named = hm.candidates(line, "로직", got, None) + ordered = hb.merge(basis, named)[:pool_n] + have = {c.get("로직") for c in ordered} + if text: + for one in got["로직"]: + ws = our_words(one["이름"]) + if one["로직"] not in have and ws and all(w in text for w in ws): + ordered.append({"로직": one["로직"], "점수": 0.3}) + have.add(one["로직"]) + rescored = [] + for at, c in enumerate(ordered): + k = c.get("로직") + nm = next((x["이름"] for x in got["로직"] if x["로직"] == k), "") + ws = our_words(nm) + hit = sum(1 for w in ws if w in text) / len(ws) if ws and text else 0 + rescored.append((c["점수"] + 0.4 * hit, -at, k)) + rescored.sort(reverse=True) + top = [k for _, _, k in rescored] + first = next((c.get("로직") for c in named), None) + five = top[:5] if first in top[:5] or first is None else top[:4] + [first] + return {"상태": "후보" if five else "없음", "top": five} + + +# ── ⑬ 같은 이름 접힘 고침 — 이름 후보가 (이름 · 규격 · 단위) 같으면 하나만 남기던 것 → 로직마다 ────── +def candidates_nofold(line, pool, got=None, limit=5): + got = got or hm.targets() + me = hm.tidy(line.get("이름")) + grams, unit = hm._grams(me), line.get("단위") + text = hm.mm.plain_spec(f"{line.get('이름')}{line.get('규격')}") + mine = hm.words(line.get("이름")) + scored = [] + for one in got[pool]: + if not grams & one["grams"] or not hm.near(mine, one["words"]): + continue + s = 2 * len(grams & one["grams"]) / (len(grams) + len(one["grams"])) + s += 1 if one["tidy"] == me else 0 + s += 0.15 if hm.fits(unit, one["단위"]) else 0 + s += 0.05 if one.get("산림") else 0 + s += 0.5 if one.get("spec") and one["spec"] in text else 0 + scored.append((s, one)) + scored.sort(key=lambda x: -x[0]) + return [{**hm._public(o), "점수": round(s, 3)} for s, o in scored[: limit or None]] + + +# ── ⑭ 부문 — 엑셀 글 「공통품셈 · 토목품셈 · (건축)」 이 있으면 그 부문 조항만 「조항 같음」 ──────── +_PART = re.compile(r"(공통|토목|건축|기계설비|유지관리)") + + +def clause_part(theirs, our): + got = clause_prefix(theirs, our) + r = CURRENT["r"] or {} + parts = set(_PART.findall(" ".join((r.get("단산글") or [])[:6]))) + if got[0] >= 0.9 and parts and our["책"] == "건설품셈" and our["부문"] not in parts: + return 0.5, got[1], "앞 마디 같음" + return got + + +def typo_same(line, got, r): + b = dict(line.get("근거") or {}) + if b.get("출처"): + b["출처"] = fix_typo(b["출처"]) + line = {**line, "근거": b} + return same_check_line(line, got, r) + + +def no_seeds(): + return patched(hm, _seeds=lambda: {}) + + +if __name__ == "__main__": + rows = [] + rows.append(table("지금", run())) + with no_seeds(): + rows.append(table("지금(씨앗 끔)", run())) + rows.append(table("이름만(씨앗 끔)", run(name_only))) + rows.append(table("이름만(씨앗 켬)", run(name_only))) + with patched(hm, tidy=tidy_keep): + rows.append(table("①이름 다듬기", run())) + with patched(hb, _clause=clause_prefix): + rows.append(table("⑥조항 윗마디", run())) + rows.append(table("⑦직종·장비", run(compose_line))) + rows.append(table("⑧금액", run(money_line))) + rows.append(table("⑪같음 근거 검사", run(same_check_line))) + rows.append(table("⑫블록 글 낱말", run(block_line))) + with patched(hm, candidates=candidates_nofold): + rows.append(table("⑬같은 이름 접힘 고침", run())) + with patched(hb, _clause=clause_part): + rows.append(table("⑭부문(+⑥)", run())) + with patched(hb, _clause=clause_prefix), patched(hm, tidy=tidy_keep, candidates=candidates_nofold): + rows.append(table("결합 ⑥①⑪⑬+오타", run(typo_same))) + with patched(hb, _clause=clause_part), patched(hm, tidy=tidy_keep, candidates=candidates_nofold): + rows.append(table("결합 ⑥①⑪⑬⑭+오타", run(typo_same))) + rows.append(table("결합 …+⑫", run(lambda a, b, c: typo_same(a, b, c) if hm.match_line(a, [], {}, True, b)["상태"] == "같음" else block_line(a, b, c)))) + for row in rows: + show(row) + (E.HERE / "out_all.json").write_text(json.dumps(rows, ensure_ascii=False, indent=1), "utf-8") diff --git a/docs/raw/조사/2026-10-15_338_실험자료/C1/s10b_diff.py b/docs/raw/조사/2026-10-15_338_실험자료/C1/s10b_diff.py new file mode 100644 index 000000000..5d1cc1de4 --- /dev/null +++ b/docs/raw/조사/2026-10-15_338_실험자료/C1/s10b_diff.py @@ -0,0 +1,29 @@ +from s10_all import * # noqa: F401,F403 + + +def mark(i, top): + s, l = set(LAB[i]["정답"]), set(LAB[i]["정답"]) | set(LAB[i]["허용"]) + t1 = top[0] if top else None + return ("O" if t1 in s else "o" if t1 in l else "x") + ("5" if set(top[:5]) & l else "-") + + +def diff(a, b, title, only="Y"): + print("==", title) + for i in BY: + if not LAB[i]["정답"] or not i.startswith(only): + continue + ma, mb = mark(i, a[i]["top"]), mark(i, b[i]["top"]) + if ma != mb: + print(f" {i} {BY[i]['이름'][:18]:<18} {ma}->{mb} {a[i]['top'][:2]} -> {b[i]['top'][:2]} 정답{LAB[i]['정답']}") + + +if __name__ == "__main__": + base = run() + with no_seeds(): + diff(base, run(), "씨앗 끔") + diff(base, run(same_check_line), "⑪") + with patched(hm, candidates=candidates_nofold): + diff(base, run(), "⑬") + diff(base, run(block_line), "⑫ 영월") + diff(base, run(block_line), "⑫ 임도", "N") + diff(base, run(block_line), "⑫ 사방", "S") diff --git a/docs/raw/조사/2026-10-15_338_실험자료/C1/s11_buttons.py b/docs/raw/조사/2026-10-15_338_실험자료/C1/s11_buttons.py new file mode 100644 index 000000000..dfcb67964 --- /dev/null +++ b/docs/raw/조사/2026-10-15_338_실험자료/C1/s11_buttons.py @@ -0,0 +1,27 @@ +"""화면 단추 정확도 — [같음 모두 확정] · [근거 맞음 모두 확정](TS basisOk · basisPick 그대로 옮김).""" +from s10_all import ALL, LAB + + +def basis_ok(b, line_basis): + if not b: + return False + trades = (line_basis or {}).get("직종") or [] + return b["까닭"].startswith("조항 같음") and bool(b["단위"]) and (bool(b["직종"]) or not any(x != "보통인부" for x in trades)) + + +for f in ("북부청임도", "북부청사방", "동부청영월"): + same = [r for r in ALL if r["파일"] == f and r["맞춤"]["상태"] == "같음" and (r["맞춤"].get("환산") or {}).get("배") is not None] + s_ok = sum(1 for r in same if r["맞춤"]["대상"]["로직"] in LAB[r["id"]]["정답"]) + l_ok = sum(1 for r in same if r["맞춤"]["대상"]["로직"] in set(LAB[r["id"]]["정답"]) | set(LAB[r["id"]]["허용"])) + picks = [] + for r in ALL: + if r["파일"] != f or r["맞춤"]["상태"] != "후보": + continue + c = next((c for c in r["맞춤"].get("후보") or [] if basis_ok(c.get("근거"), r.get("근거"))), None) + if c: + picks.append((r, c["로직"])) + p_s = sum(1 for r, k in picks if k in LAB[r["id"]]["정답"]) + p_l = sum(1 for r, k in picks if k in set(LAB[r["id"]]["정답"]) | set(LAB[r["id"]]["허용"])) + wrong = [(r["id"], r["이름"], k) for r, k in picks if k not in set(LAB[r["id"]]["정답"]) | set(LAB[r["id"]]["허용"])] + cand = sum(1 for r in ALL if r["파일"] == f and r["맞춤"]["상태"] == "후보") + print(f"{f}: 같음 확정 {len(same)} 줄(고유) 엄격 맞음 {s_ok} 느슨 {l_ok} | 근거 맞음 확정 {len(picks)}/{cand} 엄격 {p_s} 느슨 {p_l} 틀림 {wrong}") diff --git a/docs/raw/조사/2026-10-15_338_실험자료/C1/s12_units.py b/docs/raw/조사/2026-10-15_338_실험자료/C1/s12_units.py new file mode 100644 index 000000000..1379430a5 --- /dev/null +++ b/docs/raw/조사/2026-10-15_338_실험자료/C1/s12_units.py @@ -0,0 +1,26 @@ +"""정답 로직과 엑셀 줄 단위 — 환산이 「입력」(사람이 넣어야)인 줄 수 · 단위 별칭(hr=시간 · 공/㎥=공㎥ · 개소=개=본) 뒤.""" +from s10_all import ALL, LAB, hm + +units = {x["로직"]: x["단위"] for x in hm.targets()["로직"]} +ALIAS = {"hr": "시간", "h": "시간", "공/㎥": "공㎥", "개소": "개", "본": "개", "ea": "개", "EA": "개"} + + +def norm(u): + u = str(u or "").strip() + return ALIAS.get(u, ALIAS.get(u.lower(), u)) + + +rows = [] +for r in ALL: + lab = LAB[r["id"]] + if not lab["정답"]: + continue + k = lab["정답"][0] + want = units.get(k) + a = hm.factor(r["단위"], want, r.get("규격")) + b = hm.factor(norm(r["단위"]), " · ".join(norm(x.strip()) for x in str(want).split("·")), r.get("규격")) + rows.append((r["id"], r["이름"], r["단위"], want, a["갈래"], b["갈래"])) +print("정답 줄", len(rows), "· 환산 입력(지금)", sum(1 for x in rows if x[4] == "입력"), "· 별칭 뒤", sum(1 for x in rows if x[5] == "입력")) +for x in rows: + if x[4] == "입력": + print(" ", x) diff --git a/docs/raw/조사/2026-10-15_338_실험자료/C1/s13_money_flag.py b/docs/raw/조사/2026-10-15_338_실험자료/C1/s13_money_flag.py new file mode 100644 index 000000000..e093a952b --- /dev/null +++ b/docs/raw/조사/2026-10-15_338_실험자료/C1/s13_money_flag.py @@ -0,0 +1,20 @@ +"""금액을 「확인 표시」로 쓸 때 — 지금 1순위(또는 같음 대상)의 우리 값 ÷ 엑셀 단가가 ±5% · ±15% 안이면 맞는 비율.""" +from s05_more import money_ratio +from s10_all import ALL, LAB, hm + +units = {x["로직"]: x["단위"] for x in hm.targets()["로직"]} +buckets = {0.05: [0, 0], 0.15: [0, 0], "밖": [0, 0], "못셈": [0, 0]} +for r in ALL: + m = r["맞춤"] + top = (m.get("대상") or {}).get("로직") if m["상태"] == "같음" else ((m.get("후보") or [{}])[0].get("로직")) + if not top: + continue + lab = LAB[r["id"]] + good = top in set(lab["정답"]) | set(lab["허용"]) + line = {k: r.get(k) for k in ("이름", "규격", "단위", "단가", "갈래", "근거")} + q = money_ratio(line, top, units) + key = "못셈" if q is None else 0.05 if abs(q - 1) <= 0.05 else 0.15 if abs(q - 1) <= 0.15 else "밖" + buckets[key][0] += 1 + buckets[key][1] += good +for k, (n, g) in buckets.items(): + print(f" |비-1| {k}: 1순위 {n} 개 중 맞음 {g}") diff --git a/docs/raw/조사/2026-10-15_338_실험자료/C1/s14_fails_all.py b/docs/raw/조사/2026-10-15_338_실험자료/C1/s14_fails_all.py new file mode 100644 index 000000000..441f4ab6b --- /dev/null +++ b/docs/raw/조사/2026-10-15_338_실험자료/C1/s14_fails_all.py @@ -0,0 +1,26 @@ +import json +from s10_all import BY, LAB, E, no_seeds, run, name_only + + +def fails(res): + out = {} + for i in BY: + s = set(LAB[i]["정답"]) + if not s: + continue + top = res[i]["top"] + if not top or top[0] not in s: + lo = s | set(LAB[i]["허용"]) + out[i] = {"1위": top[0] if top else None, "형": "느슨맞음" if top and top[0] in lo else ("5안" if set(top[:5]) & lo else "5밖")} + return out + + +with no_seeds(): + a = fails(run(name_only)) +b = fails(run()) +(E.HERE / "out_fails_all.json").write_text(json.dumps({"이름만": a, "지금": b}, ensure_ascii=False, indent=1), "utf-8") +for name, f in (("이름만(씨앗 끔)", a), ("지금", b)): + print("==", name, len(f)) + for i, v in f.items(): + if i.startswith("Y"): + print(f" {i} {BY[i]['이름'][:22]:<22} {v}") diff --git a/docs/raw/조사/2026-10-15_338_실험자료/C1/s15_cats.py b/docs/raw/조사/2026-10-15_338_실험자료/C1/s15_cats.py new file mode 100644 index 000000000..4f801d1fc --- /dev/null +++ b/docs/raw/조사/2026-10-15_338_실험자료/C1/s15_cats.py @@ -0,0 +1,19 @@ +"""실패 갈래 표 — out_fails_all.json(이름만 · 지금 실패 목록) × cats.py(주 갈래).""" +import json +from cats import NAME, NOW, NOTE, ORDER +from s10_all import BY + +f = json.loads(open("out_fails_all.json", encoding="utf-8").read()) +miss = {k: [i for i in f[k] if i not in (NAME if k == "이름만" else NOW)] for k in f} +print("갈래 빠짐", miss) +rows = [] +for cat in ORDER: + def cnt(k, table): + ids = [i for i in f[k] if (table.get(i) or [None])[0] == cat] + side = [i for i in f[k] if cat in (table.get(i) or [])[1:]] + return len(ids), sum(BY[i]["줄수"] for i in ids), len(side) + a, b = cnt("이름만", NAME), cnt("지금", NOW) + rows.append((cat, a, b)) + print(f"{cat:<22} 이름만 주 {a[0]:>2}(줄 {a[1]:>3}) 곁 {a[2]:>2} | 지금 주 {b[0]:>2}(줄 {b[1]:>3}) 곁 {b[2]:>2}") +print("합", len(f["이름만"]), sum(BY[i]["줄수"] for i in f["이름만"]), "|", len(f["지금"]), sum(BY[i]["줄수"] for i in f["지금"])) +json.dump(rows, open("out_cats.json", "w", encoding="utf-8"), ensure_ascii=False) diff --git a/docs/raw/조사/2026-10-15_338_실험자료/C1/s16_guard.py b/docs/raw/조사/2026-10-15_338_실험자료/C1/s16_guard.py new file mode 100644 index 000000000..922a1e012 --- /dev/null +++ b/docs/raw/조사/2026-10-15_338_실험자료/C1/s16_guard.py @@ -0,0 +1,57 @@ +"""작은 고침 묶음 · ⑪ 판 지킴(옛 판 글이면 같음 안 내림 · 근거 후보가 직종 또는 뜻 낱말을 나눠야).""" +import json +import re + +from s04_eval import clause_prefix, fix_typo, hb, hm, patched, tidy_keep +from s05_more import same_check_line +from s10_all import CURRENT, E, _GENERIC, candidates_nofold, run, show, table, typo_same + +_OLD = re.compile(r"(20(1\d|2[0-4])|1\d년|2[0-4]년|임도품셈|임도표준품셈|사방품셈)") + + +def guarded_same(line, got, r): + text = " ".join((r.get("단산글") or [])[:6]) + m = hm.match_line(line, [], {}, True, got) + if m["상태"] != "같음" or _OLD.search(text): + return same_check_line(line, got, r) if m["상태"] != "같음" else {"상태": "같음", "top": [m["대상"]["로직"]]} + theirs = (line.get("근거") or {}).get("출처") or [] + if not theirs: + return {"상태": "같음", "top": [m["대상"]["로직"]]} + tgt = m["대상"]["로직"] + our = hb.ours().get(tgt) + if our and hb._clause(theirs, our)[0] >= 0.9: + return {"상태": "같음", "top": [tgt]} + mine = {w for w in hb._loose_words(f"{line.get('이름')}") if w not in _GENERIC} + + def strong(c): + if c["근거"]["직종"]: + return True + theirs_w = {w for w in hb._loose_words(c["이름"]) if w not in _GENERIC} + return any(a in b or b in a for a in mine for b in theirs_w) + + basis = [c for c in hb.candidates(line, got) if c["근거"]["까닭"].startswith("조항 같음") and strong(c)] + if not basis: + return {"상태": "같음", "top": [tgt]} + top = list(dict.fromkeys([basis[0]["로직"], tgt] + [c["로직"] for c in basis[1:]]))[:5] + return {"상태": "후보", "top": top} + + +def typo_guard(line, got, r): + b = dict(line.get("근거") or {}) + if b.get("출처"): + b["출처"] = fix_typo(b["출처"]) + line = {**line, "근거": b} + return guarded_same(line, got, r) + + +if __name__ == "__main__": + rows = [table("지금", run())] + with patched(hb, _clause=clause_prefix), patched(hm, candidates=candidates_nofold): + rows.append(table("작은 고침 ⑥⑬+오타", run(lambda a, b, c: E.default_line({**a, "근거": {**(a.get("근거") or {}), "출처": fix_typo((a.get("근거") or {}).get("출처") or [])}} if a.get("근거") else a, b, c)))) + rows.append(table("⑥⑬+오타+⑪(판 지킴)", run(typo_guard))) + rows.append(table("⑪(판 지킴)만", run(guarded_same))) + with patched(hb, _clause=clause_prefix), patched(hm, tidy=tidy_keep, candidates=candidates_nofold): + rows.append(table("⑥①⑪(판 지킴)⑬+오타", run(typo_guard))) + for row in rows: + show(row) + (E.HERE / "out_guard.json").write_text(json.dumps(rows, ensure_ascii=False, indent=1), "utf-8") diff --git a/docs/raw/조사/2026-10-15_338_실험자료/C1/s17_report.py b/docs/raw/조사/2026-10-15_338_실험자료/C1/s17_report.py new file mode 100644 index 000000000..c626027ea --- /dev/null +++ b/docs/raw/조사/2026-10-15_338_실험자료/C1/s17_report.py @@ -0,0 +1,265 @@ +"""최종 보고 — req225/C1_이름맞춤_실측.md 를 결과 파일(out_*.json) · 정답 표시(labels*.py) · 갈래(cats.py)로 만듦.""" + +import json +from pathlib import Path + +from cats import NOTE, ORDER +from s10_all import ALL, LAB + +HERE = Path(__file__).resolve().parent +OUT = HERE.parent / "C1_이름맞춤_실측.md" + + +def load(name): + return {r["이름"]: r for r in json.loads((HERE / name).read_text(encoding="utf-8"))} + + +ALLR = load("out_all.json") +GUARD = load("out_guard.json") +NAME = load("out_name_all.json") +CATS = json.loads((HERE / "out_cats.json").read_text(encoding="utf-8")) +CAT = {c: (a, b) for c, a, b in CATS} + + +def pair(s, a, b): + return f"{s[a]:.1f} / {s[b]:.1f}" + + +def t1(s): + return pair(s, "1위 엄격", "1위 느슨") + + +def t5(s): + return pair(s, "5안 엄격", "5안 느슨") + + +# ── 지표 표 ───────────────────────────────────────────────────────────── +name0 = ALLR["이름만(씨앗 끔)"] +now = ALLR["지금"] +best = GUARD["⑥⑬+오타+⑪(판 지킴)"] +lines_of = {g: sum(r["줄수"] for r in ALL if r["파일"] == g) for g in ("북부청임도", "북부청사방", "동부청영월")} +none_lines = {g: sum(r["줄수"] for r in ALL if r["파일"] == g and not LAB[r["id"]]["정답"]) for g in lines_of} +metric_rows = [] +for g, label in (("북부청임도", "북부청 임도"), ("북부청사방", "북부청 사방"), ("동부청영월", "동부청 영월*"), ("전체", "세 파일")): + s0, s1, s2 = name0[g], now[g], best[g] + nl = sum(lines_of.values()) if g == "전체" else lines_of[g] + nn = sum(none_lines.values()) if g == "전체" else none_lines[g] + metric_rows.append( + f"| {label} | {s1['고유']}({nl}) | {s1['있음']} | {s1['없음']}({nn}) · {100 * s1['없음'] / s1['고유']:.1f}% " + f"| {t1(s0)} | {t5(s0)} | {t1(s1)} | {t5(s1)} | {t1(s2)} | {t5(s2)} |" + ) + +# ── 실패 갈래 표 ─────────────────────────────────────────────────────── +cat_rows = [] +for cat, a, b in CATS: + ex = " · ".join(NOTE.get(cat, [])[:3]) + cat_rows.append(f"| {cat} | {a[0]}({a[1]}) · 곁 {a[2]} | {b[0]}({b[1]}) · 곁 {b[2]} | {ex} |") +tot_a = sum(x[1][0] for x in CATS) +tot_b = sum(x[2][0] for x in CATS) +tot_al = sum(x[1][1] for x in CATS) +tot_bl = sum(x[2][1] for x in CATS) + + +# ── 부록 C — 정답 표시 한 줄씩 ────────────────────────────────────────── +def top_of(r): + m = r["맞춤"] + if m["상태"] == "같음": + return [m["대상"]["로직"]], "같음" + return [c.get("로직") for c in m.get("후보") or [] if c.get("로직")], m["상태"] + + +def mark(i, top): + s, lo = set(LAB[i]["정답"]), set(LAB[i]["정답"]) | set(LAB[i]["허용"]) + if not s: + return "없음 줄 · " + ("엉뚱 후보 보임" if top else "후보 없음") + t = top[0] if top else None + if t in s: + return "✓" + if t in lo: + return "△(허용)" + return "5안" if set(top[:5]) & lo else "✗" + + +app = [] +for r in ALL: + i = r["id"] + lab = LAB[i] + top, state = top_of(r) + right = " · ".join(lab["정답"]) or "우리에 없음" + extra = f" · 허용 {' · '.join(lab['허용'])}" if lab.get("허용") else "" + part = f" · 부분 {' · '.join(lab['부분'])}" if lab.get("부분") else "" + guess = " · 짐작" if lab.get("짐작") else "" + spec = f" · {r['규격']}" if r.get("규격") else "" + app.append( + f"| {i} | {r['이름']}{spec} [{r['단위']}] ×{r['줄수']} | {right}{extra}{part}{guess} | {lab['근거']} | {state} {top[0] if top else '—'} {mark(i, top)} |" + ) + +# ── 방법별 표(숫자) ──────────────────────────────────────────────────── +A = ALLR +N = NAME +G = GUARD + + +def cell(row): + s = row["전체"] + return f"{t1(s)} | {t5(s)}" + + +meth = [ + ("기준 ① 이름만(330 앞 · 씨앗 끔)", cell(A["이름만(씨앗 끔)"]), "—", "—"), + ("기준 ② 지금(330 뒤)", cell(A["지금"]), "—", "—"), + ("① 이름 다듬기(괄호 낱말 살림 · 수 · 단위 · 규격 조각 뺌) — 이름만에", cell(N["이름만+①다듬기"]), "작음", "괄호 속 규격이 엉뚱 후보를 끌어옴 · 느슨 1위 −7.1"), + ("① 이름 다듬기 — 지금에", cell(A["①이름 다듬기"]), "작음", "영월 −2.9 · 사방 터파기(토사)만 나아짐"), + ("② 같은 뜻 말 사전(임도 파일에서 16쌍) — 이름만에", cell(N["이름만+②사전(임도에서 뽑음)"]), "작음(사전) · 키우기 큼", "사방(처음 봄) 느슨 1위 62.8→69.8 · 영월(다른 꼴) 61.8→52.9"), + ("② 같은 뜻 말 사전 — 지금에", cell(N["지금+②사전"]), "작음", "레미콘 → 산림 GF000155 로 넘어감(이름 같음 덤 + 산림 덤) · 엄격 −7.1"), + ("③ 낱말 차례 무시(뜻 낱말 겹침 덤) — 이름만에", cell(N["이름만+③낱말 차례 무시"]), "작음", "다섯 안 느슨 −5.6 · 한국어 붙여 쓴 이름이라 효과 적음"), + ("④ 글자 1·2·3그램 — 이름만에", cell(N["이름만+④1·2·3그램"]), "작음", "변화 거의 없음"), + ("④ difflib(가장 긴 같은 조각) — 이름만에", cell(N["이름만+④difflib"]), "작음", "다섯 안 엄격 ↑ · 느슨 ↓ · 없음 줄 엉뚱 후보 4→10"), + ("⑤ 단위 거름망(단위 안 맞으면 뺌) — 지금에", cell(N["지금+⑤단위 거름망"]), "작음", "해로움 — 맞는 짝 단위가 달라(개소↔M · 개↔㎡ · 조↔㎡ · hr↔시간) 1위 엄격 −7.9"), + ("⑥ 근거 조항 윗마디(엑셀 6-2 ⊃ 우리 6-2-2) — 지금에", cell(A["⑥조항 윗마디"]), "작음(_clause 몇 줄)", "낮음 — 윗마디 = 0.9 · 직종 · 낱말 거름 그대로"), + ("⑦ 직종 · 장비 구성 겹침(조항 없이도) — 지금에", cell(A["⑦직종·장비"]), "중간", "효과 작음 — 직종은 이미 근거 점수에 듦"), + ("⑧ 금액(우리 값 ÷ 엑셀 단가 ±15% 덤) — 지금에", cell(A["⑧금액"]), "중간", "해로움 — 복합 줄 · 입력 기본값 탓 · 확인 표시로는 ±5% 안 1순위 21 중 20 맞음"), + ("⑪ 같음도 근거 검사(판 지킴 없이) — 지금에", cell(A["⑪같음 근거 검사"]), "중간", "영월 −5.8 — 옛 판 1-9-3 이 지금 낙석방지책으로 걸림"), + ("⑪ 같음도 근거 검사(판 지킴: 옛 판 글이면 안 내림 · 직종/뜻 낱말 요구)", cell(G["⑪(판 지킴)만"]), "중간", "낮음 — 같음 → 후보로 바뀌어 사람 확정 한 번 더"), + ("⑫ 블록 글 낱말(우리 이름 낱말이 엑셀 산출 글에 나오면 덤)", cell(A["⑫블록 글 낱말"]), "중간", "섞임 — 느슨 ↑ 엄격 ↓(블록에 단위중량 · 자재 이름이 섞임)"), + ("⑬ 같은 이름 접힘 고침(산림·건설 같은 이름을 하나로 접던 것)", cell(A["⑬같은 이름 접힘 고침"]), "작음(중복 빼기 열쇠)", "다섯 안에 같은 이름 둘 → 화면에 책 표시 필요"), + ("⑭ 부문(공통 · 토목 글로 부문 가름)", cell(A["⑭부문(+⑥)"]), "작음", "이 표본에선 변화 0"), + ("책 맞춤 덤(엑셀 출처 책 = 우리 책 +0.2)", cell(N["지금+책 맞춤 덤"]), "작음", "섞임 — 영월 +11.8 · 임도 −2.0(건설 삭제 조항 · 산림에만 있는 일)"), + ("결합 ⑥ + ⑬ + 조항 오타", cell(G["작은 고침 ⑥⑬+오타"]), "작음", "낮음"), + ("결합 ⑥ + ⑬ + 조항 오타 + ⑪(판 지킴) ← 추천", cell(G["⑥⑬+오타+⑪(판 지킴)"]), "작음~중간", "낮음 — 같음 11 줄이 후보로 바뀜(사람 확정 한 번 더)"), + ("결합 + ① 다듬기", cell(G["⑥①⑪(판 지킴)⑬+오타"]), "중간", "영월 기초잡석 · 큰돌 채집 둘 뒤집힘"), +] +meth_rows = [f"| {a} | {b} | {c} | {d} |" for a, b, c, d in meth] + +TEXT = f"""# C1 외부 일위대가 이름 맞춤 실측 (요구 225 · 검토) + +M02 「외부 일위대가 가져오기」 맞춤을 실제 엑셀로 재고 실패 까닭 · 개선 방법 효과를 가늠한 기록. 코드 안 고침(읽기 · 스크래치패드 흉내만). +기준 = main_laptop_1 31e6adf7d · 우리 돈 로직 1,438(산림 300 · 건설 1,138) · 기억 없음(층 기억 빈 것)으로 잼. + +## 1. 결론 요약 + +1. 잰 자료 = 북부청 임도 · 사방(지금 올라가는 꼴) + 동부청 영월 2024(STmate 꼴 · 지금은 「일위대가 시트 없음」 400 → 스크래치패드에서 따로 읽어 같은 맞춤 함수에 넣음). 로직 줄 고유 136(줄 504) 전수 정답 표시 · 정답 있음 126 · 우리에 없음 10(7.4%). +2. 이름만(330 앞) 1순위 엄격 {t1(name0['전체']).split(' / ')[0]}% · 느슨 {t1(name0['전체']).split(' / ')[1]}% · 다섯 안 {t5(name0['전체'])}% — 이름 문제 실재(같은 뜻 다른 말 · 산림/건설 책 갈래 · 레미콘 같은 줄임말이 주 까닭). +3. 지금(330 근거 맞춤 뒤) 1순위 {t1(now['전체'])}% · 다섯 안 {t5(now['전체'])}% — 엑셀 단가산출 조항 · 직종이 이름 문제 대부분을 덮음(+37pt). 줄 무게로는 1순위 느슨 78.1% · 다섯 안 느슨 97.9%. +4. 지금 남은 1순위 실패 36 줄(고유)의 주 까닭 = 책 갈래 11(「같음」 자동 대상이 엑셀 출처와 다른 책 10 · 이름 같음 덤이 근거를 이김 1) · 같은 뜻 다른 말 11(고임돌 · 조약돌 · 소운반 · 철골설치 · 측구수로관) · 우리 로직이 더 잘게 5(철근 = 가공 + 조립) · 그 밖 5(같은 이름 접힘 · 이름 같음 덤) · 장비 줄 2 · 복합 1 · 괄호 1. +5. 이름 쪽 손질(다듬기 · n-그램 · 낱말 차례 · 단위 거름망)은 효과 없거나 해로움. 사전(임도에서 16쌍)은 사방 +7.0 · 영월 −8.9(느슨 1위) → 한 파일로 만든 사전은 다른 꼴에 안 통함. +6. 효과 큰 것 = 근거 쪽 작은 고침 — 조항 윗마디(6-2 ⊃ 6-2-2) · 같은 이름 접힘 고침 · 조항 오타 → 1순위 {t1(G['작은 고침 ⑥⑬+오타']['전체'])}% · 여기에 「같음도 근거 검사(판 지킴)」 더하면 {t1(best['전체'])}% · 다섯 안 {t5(best['전체'])}%. +7. 금액(우리 값 ÷ 엑셀 단가)은 순위 점수로 넣으면 엄격 −4.7pt(복합 줄 · 입력 기본값) · 확인 표시로는 ±5% 안 1순위 21 중 20 맞음(95%). +8. 기억 — 지금 열쇠(품명|규격|단위)로는 같은 회사 다른 파일 재사용 고유 4/48 · 둘째 열쇠(품명|단위)면 15/48(모두 맞음) · 씨앗(사용자 실무 이름 80)이 영월 1순위 엄격 44.1 → 67.6. +9. 단추 정밀 — [근거 맞음 모두 확정] 고른 55 줄(고유) 모두 맞음(엄격 54) · [같음 모두 확정] 25 중 엄격 20(다른 책 5 · 느슨은 25). +10. 「다른 외부문서도 같음」의 실제 걸림돌 = 저장소 실무 내역 xlsx 6 개를 열어 보니 5 개가 STmate 꼴(일위대가표 · 단가산출근거 · D/M/L/X 코드) → 지금은 올리기부터 400 · 이름 맞춤 앞 단계 문제. + +## 2. 지표 표 + +고유 줄 = 같은 (품명 · 규격 · 단위 · 단산 번호) 한 번 셈(확정 한 번이면 같은 열쇠 줄 모두 기억으로 들어감). 칸 = 엄격 / 느슨(%). 엄격 = 엑셀 조항 · 품과 같은 우리 로직 · 느슨 = 같은 일의 다른 책 로직 · 복합의 곁 몫까지. 정답 있는 줄만 분모 · 「우리에 없음」은 따로. +\\* 영월 = STmate 꼴을 스크래치패드에서 읽은 가정(지금 화면은 400). 「지금」 · 「추천 결합」은 기억 없음 · 씨앗 켬. + +| 파일 | 고유 줄(줄) | 정답 있음 | 우리에 없음(줄) | 이름만 1위 | 이름만 5안 | 지금 1위 | 지금 5안 | 추천 결합 1위 | 추천 결합 5안 | +|---|---|---|---|---|---|---|---|---|---| +{chr(10).join(metric_rows)} + +- 추천 결합 = ⑥ 조항 윗마디 + ⑬ 같은 이름 접힘 고침 + 조항 오타(13-114 → 13-11-4) + ⑪ 같음도 근거 검사(판 지킴). +- 우리에 없음 10 줄 중 지금 엉뚱 후보가 보이는 줄 5(고화매트 → 식생매트 · 잔토정리 → 타설후 정리 · 철강재운반 → 덤프 자재 운반 · 철골공 → 철골 공장용접 · 영월 잔토처리 → 터널굴착 1발파당 인력). +- 줄 무게(같은 줄 되풀이 셈) — 이름만 1위 느슨 56.1 · 지금 78.1 · 추천 결합 85.6 · 다섯 안 느슨 90.0 · 97.9 · 97.9. +- 앞서 잰 「1순위 근거 맞음 123/201」은 근거 꼬리표 기준(정답 대조 아님) — 이번 고유 줄 지금 [근거 맞음] 고름 임도 26/41 · 사방 22/40 과 같은 결. +- 자재 · 별도계상 줄(북부청 임도 142 · 사방 56)은 정답 표시 밖 — 상태만: 임도 자재 같음 12 · 후보 50 · 없음 16 / 별도계상 후보 53 · 없음 11. + +## 3. 실패 까닭 갈래 표 + +1순위 엄격 실패 줄(고유)마다 주 갈래 하나 + 곁 갈래(겹쳐 셈). 칸 = 주 갈래 고유(줄) · 곁 갈래 수. 이름만 = 330 앞 · 씨앗 끔(실패 {tot_a} · 줄 {tot_al}) / 지금 = 330 뒤(실패 {tot_b} · 줄 {tot_bl}). + +| 갈래 | 이름만 실패 | 지금 실패 | 예 | +|---|---|---|---| +{chr(10).join(cat_rows)} + +- 이름만 실패의 {round(100 * (CAT['같은 뜻 다른 말'][0][0] + CAT['책 갈래(산림↔건설)'][0][0]) / tot_a)}%({CAT['같은 뜻 다른 말'][0][0] + CAT['책 갈래(산림↔건설)'][0][0]}/{tot_a})가 같은 뜻 다른 말 · 책 갈래 — 330 근거가 이 둘을 대부분 풂(책 갈래 {CAT['책 갈래(산림↔건설)'][0][0]} → {CAT['책 갈래(산림↔건설)'][1][0]} · 같은 뜻 {CAT['같은 뜻 다른 말'][0][0]} → {CAT['같은 뜻 다른 말'][1][0]}). +- 지금 책 갈래 11 = 「같음」(이름 같음 · 씨앗) 자동 대상이 엑셀 출처와 다른 책 10(산림 로직 ↔ 엑셀 건설 조항 9 · 반대 1 = 돌망태형 옹벽) + 이름 같음 덤(+1)이 근거 점수를 이김 1(영월 합판거푸집(4회)) — 같음에도 근거 검사를 하면 10 이 풀림(⑪). +- 지금 같은 뜻 다른 말 11 = 근거로도 못 품 — 엑셀 출처가 비었거나(사방 철골설치 · 조약돌 9:1) · 단위중량 인용(고임돌 「1-10」) · 옛 판 번호(영월 9-1 · 9-3) · 우리에 그 책 조항 없음(건설 1-2-7 인력운반). + +## 4. 방법별 효과 표 + +세 파일 정답 있는 126 줄(고유) · 칸 = 1위 엄격 / 느슨 · 5안 엄격 / 느슨(%). 「이름만에」 = 330 앞 맞춤에 얹음 · 「지금에」 = 330 맞춤에 얹음. 흉내는 스크래치패드에서 함수를 바꿔 끼워 같은 줄을 다시 돌림. + +| 방법 | 1위 | 5안 | 들 일 | 위험 · 비고 | +|---|---|---|---|---| +{chr(10).join(meth_rows)} +| ⑨ 맞춤 기억 재사용(임도 정답 → 사방) | 지금 열쇠 4/48 · 품명\\|단위 15/48(맞음 15) · 정리이름\\|단위 24/48(맞음 21) | 사방 → 임도 4/52 · 18/52(18) · 23/52(22) | 중간 | 정리이름 열쇠는 괄호 낱말을 버려 엉뚱 3(구조물터파기(토사) ↔ (인력100%)) → 품명\\|단위가 알맞음 | +| ⑨' 씨앗(사용자 실무 이름 80 = 기억의 한 꼴) | 영월 1위 엄격 44.1 → 67.6 · 느슨 55.9 → 73.5 | 영월 79.4 → 88.2 | — | 임도 · 사방은 씨앗 「면고르기 · 문양거푸집 → 산림」 탓에 엄격 −4.1 · −2.3 | +| 단위 별칭(hr=시간 · 공/㎥=공㎥ · 개소=개=본) | 순위 변화 거의 없음 | — | 작음 | 정답 짝 환산 「입력」 20 → 14 줄 | +| 장비 줄 길(갈래 기계 · 단위 시간 → 「기계 시간당 기계경비」 + 기계 입력) | 흉내 안 함 · 남은 실패 2 줄(펌프차 · 크레인) 풂 예상 | — | 작음 | 낮음 | +| 임베딩(한국어 문장 벡터) 재순위 | 기대 — 같은 뜻 다른 말 일부(터파기/굴착 · 레미콘/레디믹스트) | — | 중간~큼(새 패키지 · 모델 · 미리 셈) | 휠라 · 고임돌 · 야면석 같은 현장말은 모를 가능성 · 산림/건설 같은 이름은 벡터도 같아 책 가름 못 함 → 근거와 함께만 | +| 큰 언어 모델 재순위(후보 20 + 엑셀 산출 글 + 우리 호표 · 출처) | 기대 — 남은 실패(복합 · 판 차이 · 같은 뜻)의 대부분 | — | 중간(길 · 열쇠) · 비용 줄당 수 원 | 외부 전송(실무 자료) · 매번 다른 답 · 없는 키 지어냄 → 후보 목록 안에서만 고르게 · 근거 실패 줄(약 15%)에만 · 사람 확정 그대로 | + +## 5. 추천 차례 — 먼저 할 것 셋 + +1. **근거를 이름보다 앞세우는 서버 고침 넷**(들 일 작음~중간 · 위험 낮음) — `M02_Hopyo_Basis._clause` 윗마디(엑셀 6-2 → 우리 6-2-2 · 6-2-3) · `M02_Hopyo_Match.candidates` 접힘 열쇠에 로직 키 · 조항 오타(13-114) · 같음일 때도 근거 검사(엑셀 글이 옛 판 「24년 · 임도품셈 · 사방품셈」이면 안 내림 · 근거 후보가 직종 또는 뜻 낱말을 나눌 때만) → 1순위 엄격 {t1(now['전체']).split(' / ')[0]} → {t1(best['전체']).split(' / ')[0]}% · 다섯 안 엄격 {t5(now['전체']).split(' / ')[0]} → {t5(best['전체']).split(' / ')[0]}%. 덤으로 장비 줄 길(기계 갈래 → 기계 시간당 기계경비 + 기계 입력)과 단위 별칭(hr · 공/㎥ · 개소). +2. **고르기 창 손질**(중간 · 부록 A) — 후보마다 책 · 조항 · 우리 값 ÷ 엑셀 단가(±5% 표시) · 엑셀 산출 글의 직종 · 품과 우리 호표를 나란히 · 같음 줄에도 근거 후보 · [같음 모두 확정]에서 근거 어긋남 빼기 · [다른 로직 찾기]에 엑셀 조항 미리 넣기. 다섯 안 정답이 94%라 남은 일은 「사람이 빨리 고르기」. +3. **기억 넓히기**(중간) — 확정 때 둘째 열쇠(품명|단위)를 「기억 후보」로 · 확정한 엑셀 이름을 층 별칭으로 남겨 이름 후보 · 찾기에 씀(손 사전 대신 사용자 확정이 사전이 됨) → 같은 회사 다음 파일 4/48 → 15/48 · 씨앗 효과(영월 +23.5pt)와 같은 결. + +- 별도로 알아 둘 것 — 다른 외부문서 대부분(STmate 꼴)은 지금 올리기부터 400. 「다른 문서에서도 이름이 다름」을 풀려면 STmate 읽기(일위대가표 · 단가산출근거 · D/L/X 코드로 줄 ↔ 블록 · 노임 · 장비 바로 이음)가 먼저 — 코드가 있어 이름 · 블록 잇기는 북부청 꼴보다 쉬움. +- 손 사전 · 금액 순위 점수 · 단위 거름망 · n-그램은 하지 않음(위 표에서 해로움 · 효과 없음). + +## 부록 A. 화면 사용 흐름 — 사람이 고르기 어렵게 만드는 것(추천 2 의 근거) + +- 후보 단추 글 = 「이름 [단위] · 키」 뿐(View.ts:446-452 · targetText :86-91) — 이름 후보에는 책 · 조항이 안 보여 같은 이름 산림 · 건설(유로폼 설치 및 해체 GF000210 · GC000214)을 구별 못 함. +- 점수 · 까닭은 마우스 올림(title)에만(View.ts:459) · 초록 「근거 맞음」은 조항 같음 + 단위 + 직종 셋 다일 때만(basisOk :122-126) — 윗마디 조항(6-2)이면 늘 회색. +- 「같음」 줄은 서버가 후보를 안 만듦(Match.py:395-403) → 고르기 창에 「가까운 것 없음」만(View.ts:499) · 다른 책으로 바꾸려면 [다른 로직 찾기…]로 이름을 다시 쳐야 함. +- [같음 모두 확정](View.ts:276-282)은 근거를 안 봄 — 이번 표본 25 중 5 가 엑셀과 다른 책. +- 엑셀 근거는 한 줄 요약(출처 · 직종 · 행 · View.ts:464-487) — 계산식(품 · 수량 · 장비)과 우리 로직 호표를 나란히 못 봄 · [엑셀에서 보기]로 창을 하나 더 엶. +- 후보마다 우리 값(엑셀 단위로 환산) ÷ 엑셀 단가가 없음 — ±5% 안이면 95% 맞는 신호인데 안 씀. +- [다른 로직 찾기…](View.ts:488-496)는 고르기 창을 닫고 빈 찾기 창을 엶 — 찾기 칸은 원문번호(조항)로도 걸러지지만(ui_template_logic_find.ts:44-53) 엑셀 조항을 미리 넣어 주지 않음 · 안내 없음. +- 표 「대상」 칸(View.ts:424)도 키 · 책 없이 이름만 — 같은 이름 둘이면 무엇을 골랐는지 안 보임. +- 확정 열쇠 = 품명|규격|단위 정확히 같을 때만(Match.py:68-76) · M01 로직 별칭(208-1)은 로직 1,438 중 0 — 확정한 엑셀 이름이 다음 파일 · 찾기 창에 안 쓰임. + +## 부록 B. 맞춤 흐름(파일:줄) + +- 올리기 — 화면 `M02_MasterTemplete_Hopyo_Side.ts` → `Hopyo_Api.ts:219-231 uploadHopyo` → `M02_Labor_Router.py:206 POST /layers/{{층}}/hopyo` → `_upload :174-184` → `M02_Hopyo_Excel.read_book :211-279`(`_sheets :74-102` 「일위대가 · 일위대가총괄표 · 단가산출」 세 시트만 · 없으면 400 `NO_SHEET :32` · 호표 · 줄 · `kind_of :132-149` 갈래 · 단산N참조 줄에 `Basis.read :84-125` 블록 근거 `{{출처, 직종, 행}}` :257-261) → `Grid.read` → `Store.save :78`(맞춤은 저장 안 함 · 열 때마다 셈). +- 호표 열기 — `Hopyo_View.ts drawOne :261-344` → `Api readHopyoOne :180-181` → `Router :243-252 GET …/hopyo/{{key}}/{{no}}` → `Match.view :460-474` → `match_hopyo :440-453` → `match_line :367-414`. +- `match_line` 차례 — % · 빼기 · 일위N(호표) → 층 기억 `recall :348-350`(열쇠 `line_key :68-76` = 품명|규격|단위) → 이름 같음 · 씨앗 `_exact :286-297`(`_seeds :266-278` 일위대가조합.json 메모) → 하나면 「같음」 끝(:395-403 · 후보 안 만듦) → 아니면 근거 후보 `Basis.candidates :191-235`(`_clause :166-176` 조항 같음 1.0 · 앞 마디 0.5 · 건설 공통/토목 0.1 · 직종 겹침 0.5 · 단위 0.15 · 직종도 뜻 낱말도 안 겹치면 버림 :213-214) + 이름 후보 `Match.candidates :300-329`(정리 이름 `tidy :79-84` 괄호 글 버림 · 글자 두 개 묶음 dice · 정리 이름 같음 +1 · 단위 0.15 · 산림 0.05 · 자재 규격 0.5 · 뜻 낱말 거름 `near :103-105` · (이름 · 규격 · 단위) 같으면 하나로 접음 :320-325) → `Basis.merge :238-256`(로직 키로 점수 합 · 낱말 안 겹친 근거 후보 뒤로) → `Basis.pick :259-266`(이름 1순위 한 자리) → 후보 5. +- 출처 읽기 — `Basis.sources :60-71`(「…품셈」 뒤 20 글자 안 절 번호 · 앞 글에 산림 · 임도 = 산림품셈 · 그 밖 건설품셈 — 부문 · 연도 버림) · 우리 쪽 조항 `M02_Labor_Inputs.sources :68-85` · `_NUMBER :20`. +- 고르기 · 확정 — `View choose :435-505` → `guessHopyo`(Api :183-193) → `Router :263-281` → `Match.guess :417-428`(입력값 · 환산) → [확정] `saveHopyoMemory` → `Router :283-304` → `Match.confirm :495-526`(층 기억). 단추 [같음 모두 확정] `View :276-282` · [근거 맞음 모두 확정] `View :284-291`(basisOk :122-126 · basisPick :153-158). + +## 부록 C. 실제 자료 + +- `resources/knowledge/original/실무문서/2026_실무일위대가 모음/` — `2026년_북부지방산림청_임도사업_설계내역서(공통단가).xls`(호표 68 · 줄 415 · 로직 줄 고유 52) · `2026년_북부지방산림청_사방사업(계류+댐)_설계내역서.xls`(호표 27 · 줄 201 · 고유 48) — 시험 `resources/tester/test_m02_hopyo_excel.py:26-30` 이 씀. 같은 폴더 `2016_상반_…사방사업표준품셈(안).xls` · `2026_상반_…수목진료.xls` = 일위대가 시트 없음(400). +- `resources/knowledge/original/실무문서/05. 동부청 영월/내역서/2024년 임도시설사업2차(산불진화_기번6)-영월.xlsx` — STmate 꼴(일위대가표 211행 · 단가산출근거 5,604행) · D 코드 줄 고유 36(줄 91). 같은 꼴 = 간선임도 기번3 · 울진 소광 · 계류보전 영덕 · 봉화 현동 내역 xlsx(시트 이름 확인) · 거창은 또 다른 꼴. +- 우리 로직 = `resources/master_data/로직_*.json`(M01 `loaded()` 읽기만) · 씨앗 = `resources/master_data/일위대가조합.json` 메모 80. +- tmp 안 실무 엑셀 없음. + +## 부록 D. 정답 표시 근거(고유 줄 136 · 전수) + +칸 「지금」 = 지금 맞춤 상태 · 1순위 키 · 표시(✓ 엄격 맞음 · △ 허용 맞음 · 5안 = 다섯 안에만 · ✗). N = 북부청 임도 · S = 사방 · Y = 영월. 「짐작」 = 블록에 출처가 없거나 우리 로직이 일부만 덮어 사람 판단이 섞임. 영월 「씨앗」 줄은 사용자 실무 이름 → 로직(일위대가조합.json)을 정답으로 봄. + +| id | 엑셀 품명 · 규격 [단위] ×줄 | 정답(엄격) · 허용 | 근거 | 지금 | +|---|---|---|---|---| +{chr(10).join(app)} + +## 부록 E. 스크립트 · 결과 파일 + +폴더 = 스크래치패드 `req225/C1/`. 모두 읽기만(서버 · DB · storage · 저장소 안 씀 · 함수 바꿔 끼움은 같은 프로세스 안). + +- `s01_extract.py` — 두 xls 를 `M02_Hopyo_Excel.read_book` 으로 읽어 줄마다 `match_line`(기억 없음) → `out_lines.json`(616 줄 · 후보 · 점수 · 단산 블록 글) · 우리 로직 표 `out_ours.json`. +- `s02_view.py` · `s03_find.py` — 고유 로직 줄 보기(`out_items.json`) · 우리 로직 찾기. +- `labels.py` · `labels_y.py` — 정답 표시(부록 C 원천) · `cats.py` — 실패 갈래. +- `s04_eval.py` · `s04b_diff.py` — 지표 · ①②④⑥ 흉내 · 줄별 바뀜. +- `s05_more.py` · `s05b_diff.py` — 책 맞춤 · ⑤⑦⑧⑪ · 금액 진단(`out_money.json`). +- `s06_memory.py` — ⑨ 기억 재사용 · 점수 문턱. +- `s09_stmate.py` — 영월 STmate 읽기(흉내) → `out_stmate.json`. +- `s10_all.py` · `s10b_diff.py` — 세 파일 지표 · ⑫⑬⑭ · 씨앗 끔 → `out_all.json`. +- `s11_buttons.py` — 단추 정밀 · `s12_units.py` — 단위 별칭 · `s13_money_flag.py` — 금액 확인 표시 정밀. +- `s14_fails_all.py` · `s15_cats.py` — 실패 목록 · 갈래 표(`out_fails_all.json` · `out_cats.json`). +- `s16_guard.py` — 작은 고침 묶음 · ⑪ 판 지킴 → `out_guard.json` · `s18_name_all.py` — 이름 쪽 방법 세 파일 → `out_name_all.json`. +- `s17_report.py` — 이 글. +- 실행 = 저장소 뿌리 `venv/Scripts/python.exe -B <스크립트>`(스크래치패드 폴더에서 · 각 120 초 안). + +## 부록 F. 정의 · 한계 + +- 엄격 = 엑셀 블록 출처 조항 · 직종 · 품과 같은 우리 로직(복합이면 주된 몫 · 여럿이면 어느 하나). 느슨 = 엄격 + 같은 일 다른 책 · 복합의 곁 몫. 「우리에 없음」은 분모에서 빼고 따로 셈. +- 짐작 표시 31 줄(출처 없음 · 부분 덮음) — 고임돌 · 조약돌 = 산림 13-2-1 인력 채집 몫 · 소운반 = 산림 10-6 인력운반 · 철골설치 = 철골세우기 장비 손료 등은 사람 판단. +- 사전(②)은 임도 파일에서만 뽑아 사방 · 영월을 처음 보는 셈으로 잼 — 같은 파일 점수는 부풀어 있음. +- 금액(⑧)은 자동값 + 규격에서 읽은 입력값으로 우리 로직을 서버 엔진 그대로 셈(`M02_Labor_Doc.run_line`) · 엑셀 노임(2026)과 우리 노임 차이 몇 % 섞임. +- 영월은 지금 화면이 못 올리는 꼴 — 지표는 「읽기를 붙였다고 가정」 · 노임 코드 → 직종 이름이 덜 이어져 근거 직종이 거의 빔. +""" + +OUT.write_text(TEXT, encoding="utf-8") +print("쓴 곳", OUT, "글자", len(TEXT)) diff --git a/docs/raw/조사/2026-10-15_338_실험자료/C1/s18_name_all.py b/docs/raw/조사/2026-10-15_338_실험자료/C1/s18_name_all.py new file mode 100644 index 000000000..e2d8b168c --- /dev/null +++ b/docs/raw/조사/2026-10-15_338_실험자료/C1/s18_name_all.py @@ -0,0 +1,25 @@ +"""이름만 쪽 방법(①②③④ · ⑤ · 책 맞춤)을 세 파일 · 씨앗 끔으로 다시 — out_name_all.json.""" +import json +from s04_eval import SYN_NORTH, grams_123, name_only, patched, tidy_keep, with_syn, hm +from s05_more import candidates_book, name_difflib, name_words, unit_filter_line +from s10_all import E, no_seeds, run, show, table + +rows = [] +with no_seeds(): + rows.append(table("이름만(씨앗 끔)", run(name_only))) + with patched(hm, tidy=tidy_keep): + rows.append(table("이름만+①다듬기", run(name_only))) + with patched(hm, tidy=with_syn(hm.tidy, SYN_NORTH)): + rows.append(table("이름만+②사전(임도에서 뽑음)", run(name_only))) + rows.append(table("이름만+③낱말 차례 무시", run(name_words))) + with patched(hm, _grams=grams_123): + rows.append(table("이름만+④1·2·3그램", run(name_only))) + rows.append(table("이름만+④difflib", run(name_difflib))) +rows.append(table("지금+⑤단위 거름망", run(unit_filter_line))) +with patched(hm, candidates=candidates_book): + rows.append(table("지금+책 맞춤 덤", run())) +with patched(hm, tidy=with_syn(hm.tidy, SYN_NORTH)): + rows.append(table("지금+②사전", run())) +for r in rows: + show(r) +(E.HERE / "out_name_all.json").write_text(json.dumps(rows, ensure_ascii=False, indent=1), "utf-8") diff --git a/docs/raw/조사/2026-10-15_338_실험자료/C4/fp_common.py b/docs/raw/조사/2026-10-15_338_실험자료/C4/fp_common.py new file mode 100644 index 000000000..d1a232adb --- /dev/null +++ b/docs/raw/조사/2026-10-15_338_실험자료/C4/fp_common.py @@ -0,0 +1,256 @@ +"""req226 C4 — 로직 지문 공용: 경로 · 저장소 모듈 · 식 풀기 · 장비 갈래 · 조건 낱말 · 닮음 셈. +저장소 안 고침(읽기만 · 같은 프로세스 안에서만 씀).""" + +import math +import re +import sys +import unicodedata +from pathlib import Path + +ROOT = Path(r"C:\Program_coding\임도설계 및 견적자동화 프로그램 개발") +HERE = Path(__file__).resolve().parent +C1 = HERE.parent.parent / "req225" / "C1" +C3 = HERE.parent.parent / "req225" / "C3" +for p in (str(ROOT), str(C1), str(C3)): + if p not in sys.path: + sys.path.insert(0, p) + +# ── 장비 갈래(엑셀 글 · 우리 EQ 상세구분 공통) ───────────────────────────── +MACH = [ + ("굴착기", ("굴착기", "굴삭기", "백호", "b/h", "backhoe", "back-hoe", "포크레인")), + ("덤프트럭", ("덤프",)), + ("크레인", ("크레인",)), + ("브레이커", ("브레이커", "브레이카")), + ("콘크리트펌프", ("펌프차", "펌프카", "콘크리트펌프", "콘크리트 펌프")), + ("롤러", ("롤러", "로울러")), + ("불도저", ("불도저", "불도우저")), + ("집게", ("집게",)), + ("살수차", ("살수", "물탱크")), + ("모터그레이더", ("그레이더",)), + ("래머", ("래머", "램머", "래 머")), + ("콤팩터", ("콤팩터", "컴팩터")), + ("진동기", ("진동기",)), + ("발전기", ("발전기",)), + ("공기압축기", ("공기압축기", "컴프레셔", "콤프레샤")), + ("드릴", ("드릴", "천공기")), + ("커터", ("커터",)), + ("화물차", ("화물자동차", "화물차", "카고")), + ("로더", ("로더",)), + ("지게차", ("지게차",)), + ("체인톱", ("체인톱",)), + ("트랙터", ("트랙터",)), +] + + +def mtypes(text) -> set: + t = unicodedata.normalize("NFKC", str(text or "")).lower() + return {k for k, ws in MACH if any(w in t for w in ws)} + + +_SPECNUM = re.compile(r"(\d+(?:\.\d+)?)\s*(㎥|m3|톤|ton|t\b|m\b|㎾|kw|ℓ|mm|hp)", re.I) + + +def specnums(text) -> set: + t = unicodedata.normalize("NFKC", str(text or "")) + return {float(a) for a, _ in _SPECNUM.findall(t)} + + +# ── 조건 낱말(축) ────────────────────────────────────────────────────── +SOIL = ("보통토사", "경질토사", "혼합토사", "자갈섞인토사", "사질토", "점질토", "토사") +ROCK = ("리핑암", "발파암", "풍화암", "연암", "보통암", "경암", "극경암") +AXES = { + "토질": SOIL + ROCK, + "메찰": ("메쌓기", "찰쌓기", "메붙임", "찰붙임", "메붙이기", "찰붙이기"), + "형": ("원형", "사각", "타원형"), + "근": ("무근", "철근"), + "난이": ("제물치장", "매우복잡", "복잡", "보통", "간단"), + "규모": ("소량", "소형", "소규모", "중규모", "대규모", "대형"), + "방법": ("인력운반", "인력", "기계", "장비", "펌프"), +} +CLASH = ("토질", "메찰", "형") # 이름에 박힌 값이 엑셀과 어긋나면 깎는 축(방법 · 난이는 안 깎음 — C3 #8) + + +def soil_family(w): + return "암" if w in ROCK or w == "암" else "토사" if w in SOIL else None + + +def conds(text) -> dict: + """{축: {낱말}} + 횟수 · 개월 · Type.""" + t = re.sub(r"\s+", "", unicodedata.normalize("NFKC", str(text or ""))) + out = {} + for ax, ws in AXES.items(): + hit = set() + rest = t + for w in sorted(ws, key=len, reverse=True): + if w in rest: + hit.add(w) + rest = rest.replace(w, " ") + if ax == "토질" and re.search(r"(\(암\)|암\)|[^가-힣]암$|^암$|암절취|\b암\b)", t): + hit.add("암") + if hit: + out[ax] = hit + n = re.findall(r"(\d)회", t) + if n: + out["횟수"] = {f"{x}회" for x in n} + m = re.findall(r"(\d+)개월", t) + if m: + out["개월"] = {f"{x}개월" for x in m} + ty = re.findall(r"type[-\s]*(ⅰ|ⅱ|ii|i)\b", t.lower()) + if ty: + out["type"] = {"Ⅱ" if x in ("ⅱ", "ii") else "Ⅰ" for x in ty} + return out + + +def cond_score(excel: dict, name_c: dict, opt_c: dict) -> tuple: + """(맞음 비율, 어긋남) — 엑셀 조건 낱말이 우리 이름 · 고르기 글에 있나 · 이름 축 어긋남.""" + words = [(ax, w) for ax, ws in excel.items() for w in ws] + if not words: + return 0.0, 0 + hit = 0 + for ax, w in words: + pool = name_c.get(ax, set()) | opt_c.get(ax, set()) + if w in pool or (ax == "토질" and soil_family(w) in {soil_family(x) for x in pool}): + hit += 1 + clash = 0 + for ax in CLASH: + if ax in excel and ax in name_c: + if ax == "토질": + ef = {soil_family(w) for w in excel[ax]} + of = {soil_family(w) for w in name_c[ax]} + if ef and of and not (ef & of): + clash = 1 + elif not (excel[ax] & name_c[ax]): + clash = 1 + return hit / len(words), clash + + +# ── 식 풀기 — 「단가 × 식 = 금액」 → 품(단가로 나눈 몫 · 단가는 버림) ────────────────── +_UNITFRAC = re.compile( + r"(인|개|본|공|EA|ea|hr|ton|Ton|TON|kg|㎥|㎡|m3|m2|m|M|조|회)\s*/\s*(EA|ea|㎥|㎡|m3|m2|m|M|ton|Ton|TON|t|hr|일|개|본|조|공㎥|㎥)" +) +_UNITTAIL = re.compile(r"(?<=[\d.)])\s*(hr|HR|EA|ea|kg|km|ton|Ton|TON|sec|m3|m2|mm|cm|M|m|t)(?![A-Za-z])") +_NOTE = re.compile(r"\(([^()]*[가-힣][^()]*)\)") +_LEAD = re.compile(r"^\s*(\d+(?:\.\d+)?)\s*([*/])\s*(.+)$") +_SAFE = re.compile(r"^[\d.+\-*/() ]+$") + + +def _norm(s: str) -> str: + s = unicodedata.normalize("NFKC", str(s)) + s = s.replace("×", "*").replace("✕", "*").replace("÷", "/").replace("x", "*").replace("X", "*") + s = re.sub(r"(?<=\d),(?=\d{3})", "", s) + return _UNITFRAC.sub("", s) + + +def _split(rest: str, first_op: str): + """맨 바깥 * / 로 쪼갬 → [(연산, 조각)].""" + out, depth, cur, op = [], 0, "", first_op + for ch in rest: + if ch == "(": + depth += 1 + elif ch == ")": + depth -= 1 + if depth == 0 and ch in "*/": + out.append((op, cur)) + op, cur = ch, "" + continue + cur += ch + out.append((op, cur)) + return out + + +def _factor(text: str): + """조각 → (값, 율 표시, 메모) · 변수(P · Q …)면 None.""" + notes = _NOTE.findall(text) + t = _NOTE.sub("", text) + pct = "%" in t + if re.search(r"\d\s*회", t): # 「× 2회」 — 횟수 곱(율로 가름) + notes.append("회") + t = re.sub(r"[가-힣㎥㎡㎝㎜℃˚°'\"]", "", t).replace("%", "") + t = _UNITTAIL.sub("", t).strip() + if not t or re.search(r"[A-Za-z]", t) or not _SAFE.match(t): + return None + try: + v = eval(t, {"__builtins__": {}}, {}) # noqa: S307 — 숫자 · 연산 글만(_SAFE) + except Exception: # noqa: BLE001 + return None + v = float(v) / (100 if pct else 1) + return v, pct or bool(notes), " ".join(notes) + + +def _rates_any(parts): + """모든 조각의 율(메모 · %) — 변수 든 조각(P · Q)도 풀 수 있는 조각만.""" + out = [] + for seg in parts: + m = _LEAD.match(seg) + if not m: + continue + for op, piece in _split(m.group(3), m.group(2)): + f = _factor(piece) + if f and f[1] and f[0] and all(abs(f[0] - v) > 1e-9 for v, _ in out): + out.append((f[0] if op == "*" else 1 / f[0], f[2] or "%")) + return out + + +def parse_rhs(rhs: str): + """「단가 × 식 [= 바꿔 쓴 식] = 금액」 → {단가, 품, 밑품, 율[]} · 못 읽으면 None.""" + parts = _norm(rhs).split("=") + if len(parts) < 2: + return None + every = _rates_any(parts[:-1]) + for seg in parts[:-1]: + m = _LEAD.match(seg) + if not m: + continue + price = float(m.group(1)) + facs = [] + ok = True + for op, piece in _split(m.group(3), m.group(2)): + f = _factor(piece) + if f is None: + ok = False + break + facs.append((op, *f)) + if not ok or not facs: + continue + full = base = 1.0 + rates = [] + for op, v, rate, note in facs: + if v == 0 and op == "/": + ok = False + break + full = full * v if op == "*" else full / v + if rate: + rates.append([round(v if op == "*" else 1 / v, 6), note or "%"]) + else: + base = base * v if op == "*" else base / v + if ok: + if not rates and every: # 바꿔 쓴 식엔 메모가 빠짐 — 앞 조각의 율로 밑품 + for v, note in every: + if v: + base /= v + rates = [[round(v, 6), note] for v, note in every] + plain = [v if op == "*" else 1 / v for op, v, rate, _ in facs if not rate and v] + return {"단가": price, "품": full, "밑품": base, "율": rates, "인자": plain} + return None + + +# ── 닮음 ─────────────────────────────────────────────────────────────── +def close(a, b, tol=0.15) -> float: + """두 양수 가까움 0~1 — 비가 tol 안이면 1 쪽 · ln 거리로 떨어짐.""" + if not a or not b or a <= 0 or b <= 0: + return 0.0 + d = abs(math.log(a / b)) + return 1.0 if d <= 0.03 else max(0.0, 1 - (d - 0.03) / (tol * 4)) + + +def cosine(a: dict, b: dict) -> float: + keys = set(a) | set(b) + num = sum(a.get(k, 0) * b.get(k, 0) for k in keys) + da = math.sqrt(sum(v * v for v in a.values())) + db = math.sqrt(sum(v * v for v in b.values())) + return num / (da * db) if da and db else 0.0 + + +def jacc(a, b) -> float: + a, b = set(a), set(b) + return len(a & b) / len(a | b) if a | b else 0.0 diff --git a/docs/raw/조사/2026-10-15_338_실험자료/C4/p00_peek.py b/docs/raw/조사/2026-10-15_338_실험자료/C4/p00_peek.py new file mode 100644 index 000000000..653ebae08 --- /dev/null +++ b/docs/raw/조사/2026-10-15_338_실험자료/C4/p00_peek.py @@ -0,0 +1,10 @@ +import json, sys +from pathlib import Path +ROOT = Path(r"C:\Program_coding\임도설계 및 견적자동화 프로그램 개발") +sys.path.insert(0, str(ROOT)) +from M01_MasterData import M01_MasterData_Store as m01 +files, whole = m01.loaded() +for code in sys.argv[1:]: + row = whole.logic(code) + print("=====", code) + print(json.dumps(row, ensure_ascii=False, indent=1, default=str)[:6000]) diff --git a/docs/raw/조사/2026-10-15_338_실험자료/C4/p01_run.py b/docs/raw/조사/2026-10-15_338_실험자료/C4/p01_run.py new file mode 100644 index 000000000..0cc8fea0a --- /dev/null +++ b/docs/raw/조사/2026-10-15_338_실험자료/C4/p01_run.py @@ -0,0 +1,17 @@ +import json, sys +from pathlib import Path +ROOT = Path(r"C:\Program_coding\임도설계 및 견적자동화 프로그램 개발") +sys.path.insert(0, str(ROOT)) +from M01_MasterData import M01_MasterData_Store as m01 +files, whole = m01.loaded() +mf = m01.cm.mf +code = sys.argv[1] +given = json.loads(sys.argv[2]) if len(sys.argv) > 2 else {} +logic = whole.logic(code) +auto = {k: v for k, v in m01.mau.values(whole, logic).items() if v is not None} +print("auto", auto) +got = mf.run(whole, code, {**auto, **m01._dec(given)}) +print("keys", list(got.keys())) +for ln in got.get("줄") or []: + print(json.dumps(ln, ensure_ascii=False, default=str)[:400]) +print("중간", json.dumps(got.get("중간"), ensure_ascii=False, default=str)[:1500]) diff --git a/docs/raw/조사/2026-10-15_338_실험자료/C4/p02_survey.py b/docs/raw/조사/2026-10-15_338_실험자료/C4/p02_survey.py new file mode 100644 index 000000000..7436288e5 --- /dev/null +++ b/docs/raw/조사/2026-10-15_338_실험자료/C4/p02_survey.py @@ -0,0 +1,33 @@ +import json, sys, time, math +from collections import Counter +from pathlib import Path +ROOT = Path(r"C:\Program_coding\임도설계 및 견적자동화 프로그램 개발") +sys.path.insert(0, str(ROOT)) +from M01_MasterData import M01_MasterData_Store as m01 +from M02_MasterTemplete import M02_Hopyo_Match as hm +files, whole = m01.loaded() +mf = m01.cm.mf +got = hm.targets() +kinds = Counter(); ok = 0; bad = Counter(); combos = [] +t0 = time.time() +eq = whole.numbers.get("EQ", {}) +print("EQ sample", list(eq.items())[:2]) +for one in got["로직"]: + code = one["로직"] + row = whole.logic(code) + for h in row.get("호표") or []: + kinds[h.get("종류")] += 1 + picks = [len(s.get("고르기") or []) for s in row.get("입력") or [] if s.get("고르기")] + combos.append(math.prod(picks) if picks else 1) + try: + auto = {k: v for k, v in m01.mau.values(whole, row).items() if v is not None} + r = mf.run(whole, code, auto) + ok += 1 + except Exception as e: + bad[type(e).__name__] += 1 +print("logics", len(got["로직"]), "kinds", kinds) +print("default ok", ok, "bad", bad, "sec", round(time.time()-t0, 1)) +c = Counter() +for n in combos: + c["1" if n == 1 else "<=10" if n <= 10 else "<=100" if n <= 100 else "<=1000" if n <= 1000 else ">1000"] += 1 +print("combo sizes", c) diff --git a/docs/raw/조사/2026-10-15_338_실험자료/C4/p03_yw_peek.py b/docs/raw/조사/2026-10-15_338_실험자료/C4/p03_yw_peek.py new file mode 100644 index 000000000..c65edbfbe --- /dev/null +++ b/docs/raw/조사/2026-10-15_338_실험자료/C4/p03_yw_peek.py @@ -0,0 +1,17 @@ +import sys, warnings, re +from pathlib import Path +import openpyxl +warnings.filterwarnings("ignore") +ROOT = Path(r"C:\Program_coding\임도설계 및 견적자동화 프로그램 개발") +F = ROOT / "resources/knowledge/original/실무문서/05. 동부청 영월/내역서/2024년 임도시설사업2차(산불진화_기번6)-영월.xlsx" +book = openpyxl.load_workbook(F, read_only=True, data_only=True) +print(book.sheetnames) +want = sys.argv[1] if len(sys.argv) > 1 else "단가산출근거" +lo, hi = (int(sys.argv[2]), int(sys.argv[3])) if len(sys.argv) > 3 else (1, 60) +for at, r in enumerate(book[want].iter_rows(values_only=True), 1): + if at < lo: continue + if at > hi: break + cells = [str(c) for c in r[:14]] + while cells and cells[-1] in ("None", ""): cells.pop() + print(at, " | ".join(cells)) +book.close() diff --git a/docs/raw/조사/2026-10-15_338_실험자료/C4/p05_why.py b/docs/raw/조사/2026-10-15_338_실험자료/C4/p05_why.py new file mode 100644 index 000000000..c7c449330 --- /dev/null +++ b/docs/raw/조사/2026-10-15_338_실험자료/C4/p05_why.py @@ -0,0 +1,16 @@ +"""지문 실패 줄 뜯어 보기 — 엑셀 지문 · 1~3 위 채널 · 정답 로직의 차례 · 채널.""" +import json, sys +import s3_match as S +ids = sys.argv[1:] +for i in ids: + e = S.EXP[i] + lab = S.A.LAB[i] + print(f"## {i} {e['이름']} | {e['규격']} | {e['단위']} 출처{e['출처']} 옛판{e['옛판']} 조건{ {k: sorted(v) for k, v in e['조건'].items()} }") + print(" 인력", {t: (round(o['품'], 4), round(o['밑품'], 4)) for t, o in e['인력'].items()}, "장비", [(m['갈래'], m['규격'], m['시간'] and round(m['시간'], 4)) for m in e['장비']], "자재", [m['이름'][:14] for m in e['자재']]) + allr = S.rank(e, None, 1438) + pos = {k: n for n, (_, k, _, _) in enumerate(allr)} + for s, k, ch, v in allr[:3]: + print(f" {k} {S.TG[k]['이름'][:22]} [{S.TG[k]['단위']}] {s:.2f}", {a: (round(b, 2) if isinstance(b, float) else b) for a, b in ch.items()}, (v or {}).get('in')) + for k in lab['정답'][:2]: + s, ch, v = S.score_pair(e, next(o for o in S.P if o['로직'] == k)) + print(f" 정답 {k} {S.TG[k]['이름'][:22]} [{S.TG[k]['단위']}] 차례 {pos.get(k)} {s:.2f}", {a: (round(b, 2) if isinstance(b, float) else b) for a, b in ch.items()}, (v or {}).get('in'), {t: round(q, 4) for t, q in ((v or {}).get('L') or {}).items() if q}) diff --git a/docs/raw/조사/2026-10-15_338_실험자료/C4/s1_excel_fp.py b/docs/raw/조사/2026-10-15_338_실험자료/C4/s1_excel_fp.py new file mode 100644 index 000000000..3d4e779fb --- /dev/null +++ b/docs/raw/조사/2026-10-15_338_실험자료/C4/s1_excel_fp.py @@ -0,0 +1,339 @@ +"""req226 C4 ① 바깥 단가산출 블록 → 로직 지문(직종 품 · 장비 · 자재 · 할증 · 계수 · 조건 · 조항). + +북부청 = req225 C1 out_items.json 의 단산 글(산출근거 칸)을 줄마다 풂. +영월 = STmate 「단가산출근거」 코드(L · X · M · S) + QTY 칸 + 노무비 · 중기 · 재료비 · 경비 목록표로 이름 이음. +출력 = excel_fp.json (고유 줄 136 · id 는 앞 조사와 같음) +python s1_excel_fp.py [id …] → 지문 찍어 보기 +""" + +import json +import re +import sys +import warnings + +import fp_common as F +from fp_common import HERE, conds, mtypes, parse_rhs, specnums + +from M02_MasterTemplete import M02_Hopyo_Basis as hb # noqa: E402 + +warnings.filterwarnings("ignore") +NORTH = json.loads((F.C1 / "out_items.json").read_text(encoding="utf-8")) +YW_ITEMS = json.loads((F.C1 / "out_stmate.json").read_text(encoding="utf-8")) +YW_FILE = F.ROOT / "resources/knowledge/original/실무문서/05. 동부청 영월/내역서/2024년 임도시설사업2차(산불진화_기번6)-영월.xlsx" +TRADES = hb.trade_names() + +COST = {"노무비", "재료비", "경비"} +SKIPW = { + "소계", "계", "합계", "인부소계", "자재소계", "주자재소계", "운반", "상차", "기계", "다짐", "적사", "살수", "집토", + "암절취", "채집", "인력", "장비", "작업조건", "1일시공량", "작업면적", "1시간작업량", "기계채집", "구조물터파기", +} +_COLON = re.compile(r"^\s*(?:\d+\)|[0-9가-힣]\.)?\s*(?P[^::=]{1,40}?)\s*[::]\s*(?P.*)$") +_MAT = re.compile(r"^\s*(?P[^\d\s::=][^::=]*?[A-Za-z가-힣)\]])\s+(?P[\d,]+(?:\.\d+)?\s*[×xX*÷/].*=.*)$") +_PCT = re.compile(r"([가-힣]+)의\s*([\d.]+)\s*%") +_COEF = re.compile(r"(? 0) else 1 if name == "경비" else 2 + if prev is None or prio < prev[0]: + got_cost[key] = (prio, mach, got) + continue + if cm and name not in SKIPW and re.search(r"(별도|별산)", cm.group("rhs")): + fp["자재"].append({"이름": cm.group("name").strip(), "품": None, "밑품": None}) + continue + mm_ = _MAT.match(line) + if mm_ and not cm: + got = parse_rhs(mm_.group("rhs")) + if got: + fp["자재"].append({"이름": mm_.group("name").strip(), "품": got["품"], "밑품": got["밑품"]}) + continue + if not (h or s): + ts = mtypes(line) + if ts and "%" not in line and not cm: + mach, mach_label = (ts, specnums(line)), line.strip() + for k, v in _COEF.findall(line): + fp["계수"].setdefault(k, float(v)) + for (sec_, sub_, label), (_, (ts, sp), got) in got_cost.items(): + fp["장비"].append( + {"갈래": sorted(ts), "규격": sorted(sp), "시간": got["품"], "밑시간": got["밑품"], "율": got["율"], "절": f"{sec_}.{sub_}" if sub_ else str(sec_), "글": label} + ) + return fp + + +# ── 영월(STmate) ─────────────────────────────────────────────────────── +def yw_tables(): + import openpyxl + + book = openpyxl.load_workbook(YW_FILE, read_only=True, data_only=True) + + def table(name, cols): + out = {} + for r in book[name].iter_rows(values_only=True): + code = str(r[0] or "").strip() + if re.match(r"^[A-Z]\d{5}$", code): + out[code] = [str(r[c] or "").strip() for c in cols] + return out + + labor = table("노무비목록표", (1,)) + mach = table("중기목록표", (1, 2)) + mats = table("재료비목록표", (1, 2, 3)) + cost = table("경비목록표", (1, 2)) + calc = [list(r) for r in book["단가산출근거"].iter_rows(values_only=True)] + book.close() + blocks, cur = {}, None + for r in calc: + first = str(r[0] or "").strip() + second = str(r[1] or "").strip() if len(r) > 1 else "" + if re.match(r"^D\d{5}$", second) and len(r) > 2 and isinstance(r[2], (int, float)): + cur = blocks.setdefault(second, []) + continue + if cur is not None: + qty = r[7] if len(r) > 7 and isinstance(r[7], (int, float)) else None + cur.append((first, str(r[1] or ""), qty)) + return labor, mach, mats, cost, blocks + + +def yeongwol(item, T): + labor, mach, mats, cost, blocks = T + fp = empty({**item, "파일": "동부청영월"}) + rows = blocks.get(item.get("코드")) or [] + text = [t for _, t, _ in rows if t.strip()] + fp["글"] = [t.strip() for t in text[:60]] + head_text = " ".join([item["이름"], fp["규격"]] + text[:4]) + fp["조건"] = {k: sorted(v) for k, v in conds(head_text).items()} + fp["이름조건"] = {k: sorted(v) for k, v in conds(f"{item['이름']} {fp['규격']}").items()} + fp["옛판"] = bool(_OLD.search(" ".join(text[:4]))) + if not rows: + ts = mtypes(f"{item['이름']} {fp['규격']}") + if ts: + fp["장비"] = [{"갈래": sorted(ts), "규격": sorted(specnums(f"{item['이름']} {fp['규격']}")), "시간": 1.0, "밑시간": None, "절": 0, "글": item["이름"]}] + return fp + sec, sub, seen_x = 0, "", {} + item_rates = rates_in(f"{item['이름']} {fp['규격']}") + sec_rates = set() + for code, t, qty in rows: + h = _HEAD_Y.match(t) + if h: + sec, sub = int(h.group(1)), "" + sec_rates = rates_in(h.group(2)) + fp["절"][str(sec)] = {"이름": h.group(2).strip(), "출처": hb.sources(h.group(2))} + elif _SUB_Y.match(t): + sub = str("①②③④⑤⑥⑦⑧⑨⑩".index(t.strip()[0]) + 1) + top = fp["절"].get(str(sec)) or {"이름": "", "출처": []} + fp["절"][f"{sec}.{sub}"] = {"이름": f"{top['이름']} · {t.strip()[1:]}", "출처": hb.sources(t) or top["출처"]} + elif _SUB_Y.match(t): + sec_rates = sec_rates | rates_in(t) + mix = rate_factor(t, item_rates | sec_rates) if code else None + for k, v in _PCT.findall(t): + fp["율"].append([k, float(v)]) + if not code: + if not h: + for k, v in _COEF.findall(t): + fp["계수"].setdefault(k, float(v)) + continue + base = code[:6] + if code.startswith("L") and qty is not None: + nm = TRADES.get(_flat((labor.get(base) or [""])[0])) + if nm: + b = qty / mix if mix else qty + rates = [[mix, "섞음"]] if mix else [] + add_labor(fp, nm, float(qty), float(b), rates, "QTY", f"{sec}.{sub}" if sub else str(sec)) + elif code.startswith("X") and qty is not None: + nm, sp = mach.get(base) or ["", ""] + key = f"{sec}.{sub}" if sub else str(sec) + prev = seen_x.get((key, base)) + prio = {"L": 0, "S": 1, "M": 2}.get(code[-1], 3) + if prev is None or prio < prev[0]: + seen_x[(key, base)] = (prio, nm, sp, float(qty), mix) + elif code.startswith("M") and qty is not None: + nm, sp, un = mats.get(base) or ["", "", ""] + fp["자재"].append({"이름": f"{nm} {sp}".strip(), "품": float(qty), "밑품": float(qty)}) + elif code.startswith("S") and qty is not None: + nm, sp = cost.get(base) or ["", ""] + ts = mtypes(nm) + if ts: + seen_x.setdefault((f"{sec}.{sub}" if sub else str(sec), base), (5, nm, sp, float(qty), None)) + else: + fp["자재"].append({"이름": f"{nm} {sp}".strip(), "품": float(qty), "밑품": float(qty)}) + for (sec_, base), (_, nm, sp, q, mx) in seen_x.items(): + fp["장비"].append( + {"갈래": sorted(mtypes(nm)), "규격": sorted(specnums(sp) | specnums(nm)), "시간": q, "밑시간": q / mx if mx else q, + "율": [[mx, "섞음"]] if mx else [], "절": sec_, "글": f"{nm} {sp}"} + ) + return fp + + +def main(): + out = [north(r) for r in NORTH] + T = yw_tables() + out += [yeongwol(r, T) for r in YW_ITEMS] + for fp in out: + for one in fp["인력"].values(): + one["품"] = round(one["품"], 6) + one["밑품"] = round(one["밑품"], 6) + (HERE / "excel_fp.json").write_text(json.dumps(out, ensure_ascii=False, indent=1), "utf-8") + stat = { + "고유": len(out), + "인력 있음": sum(1 for f in out if f["인력"]), + "장비 있음": sum(1 for f in out if f["장비"]), + "자재 있음": sum(1 for f in out if f["자재"]), + "율 있음": sum(1 for f in out if f["율"]), + "계수 있음": sum(1 for f in out if f["계수"]), + "조건 있음": sum(1 for f in out if f["조건"]), + "조항 있음": sum(1 for f in out if f["출처"]), + "역산 직종": sum(1 for f in out for o in f["인력"].values() if "역산" in o["어떻게"]), + "아무것도 없음": [f["id"] for f in out if not (f["인력"] or f["장비"] or f["자재"])], + } + print(json.dumps(stat, ensure_ascii=False)) + want = sys.argv[1:] + for fp in out: + if fp["id"] in want: + show = {k: fp[k] for k in ("id", "이름", "규격", "단위", "출처", "인력", "장비", "자재", "율", "계수", "조건", "옛판")} + print(json.dumps(show, ensure_ascii=False)) + + +if __name__ == "__main__": + main() diff --git a/docs/raw/조사/2026-10-15_338_실험자료/C4/s2_ours_fp.py b/docs/raw/조사/2026-10-15_338_실험자료/C4/s2_ours_fp.py new file mode 100644 index 000000000..7048ef393 --- /dev/null +++ b/docs/raw/조사/2026-10-15_338_실험자료/C4/s2_ours_fp.py @@ -0,0 +1,253 @@ +"""req226 C4 ② 우리 돈 로직 1,438 지문 — 정적(직종 · 장비 · 자재 · 덧줄 % · 조항 · 조건 낱말) + +입력 조합마다 M01 엔진으로 단위당 품(인 · 장비 시간 · 자재 수량)을 셈(읽기만 · 원본 안 고침). + +조합 = 기본값(자동값) + 고르기 입력(지역 · 조종원직종 · 보정작업 · 기종 목록 뺌) 곱이 64 이하면 모두 · +넘으면 하나씩 바꿈. 기종 목록 입력은 고를 수 있는 장비 갈래 · 규격만 적음. +python s2_ours_fp.py <시작> <끝> → ours_fp_<시작>.json +python s2_ours_fp.py merge → ours_fp.json + 덮는 범위 찍음 +""" + +import itertools +import json +import math +import re +import sys +import time +from decimal import Decimal + +import fp_common as F +from fp_common import HERE, conds, mtypes, specnums + +from M01_MasterData import M01_MasterData_Store as m01 # noqa: E402 +from M02_MasterTemplete import M02_Hopyo_Basis as hb # noqa: E402 +from M02_MasterTemplete import M02_Hopyo_Match as hm # noqa: E402 + +FILES, WHOLE = m01.loaded() +MF = m01.cm.mf +LB = {str(r.get("키")): str(r.get("이름")) for r in WHOLE.numbers.get("LB", {}).values()} +EQ = WHOLE.numbers.get("EQ", {}) +_PER = re.compile(r"원\s*/\s*(\d[\d,]*\.?\d*|\.\d+)?") +_SUB = re.compile(r"^\s*로직\(\s*(\w+)\s*(?:,(.*))?\)\s*$") +_EQCODE = re.compile(r"^\d{4}-\d{4}$") +_PCT = re.compile(r"\*\s*([\d.]+)\s*/\s*100") +SKIP_IN = ("지역", "조종원직종", "보정작업") +FIXED_MACH = {"GC000267": {"굴착기"}, "GC000270": {"굴착기", "집게"}, "GC000271": {"굴착기", "브레이커"}, "GF000145": {"덤프트럭"}} + + +def is_mach_logic(row) -> bool: + return "시간당" in str(row.get("이름") or "") + + +def eq_of(code): + one = EQ.get(str(code)) or {} + if not one: + return set(), set() + return mtypes(f"{one.get('상세구분')} {one.get('이름')}"), specnums(one.get("규격")) + + +def times_of(row) -> float: + per = _PER.findall(str(row.get("결과단위") or "")) + return float(per[0].replace(",", "")) if per and per[0] else 1.0 + + +def _args(text): + out = {} + for part in re.split(r",(?![^()]*\))", text or ""): + if "=" in part: + k, v = part.split("=", 1) + out[k.strip()] = v.strip() + return out + + +def static(code, row, unit): + our = hb.ours().get(code, {}) + picks = [s for s in row.get("입력") or [] if s.get("고르기")] + labels = [] + opt_mach = [] + for s in picks: + names = s.get("고르기이름") or {} + for v in s.get("고르기") or []: + if isinstance(v, str) and _EQCODE.match(v): + ts, sp = eq_of(v) + if ts: + opt_mach.append([sorted(ts), sorted(sp)]) + else: + labels.append(str(names.get(v) or v)) + trades, mach, mats = set(), [], [] + for h in row.get("호표") or []: + kind = h.get("종류") + if kind == "인력": + trades.add(LB.get(str(h.get("요소")), str(h.get("이름")))) + elif kind == "로직": + m = _SUB.match(str(h.get("요소") or "")) + sub = m.group(1) if m else "" + ts = set(FIXED_MACH.get(sub, set())) | mtypes(h.get("이름")) + for v in _args(m.group(2) if m else "").values(): + lit = v.strip("'\"") + if _EQCODE.match(lit): + a, b = eq_of(lit) + ts |= a + if ts: + mach.append([sorted(ts), sorted(specnums(h.get("이름")))]) + elif kind == "재료": + el = h.get("요소") + words = el.get("검색어") or el.get("이름") or el.get("상세구분") if isinstance(el, dict) else "" + mats.append(f"{h.get('이름')} {words or ''}".strip()) + pct = [] + for d in row.get("덧줄") or []: + for v in _PCT.findall(str(d.get("식") or "")): + pct.append([str(d.get("이름")), float(v)]) + if is_mach_logic(row): + mach.append([sorted(FIXED_MACH.get(code, set())), []]) + return { + "로직": code, + "이름": str(row.get("이름") or ""), + "단위": unit, + "결과단위": str(row.get("결과단위") or ""), + "책": our.get("책"), + "부문": our.get("부문"), + "조항": sorted(our.get("조항") or []), + "원문번호": str(row.get("원문번호") or ""), + "상세구분": str(row.get("상세구분") or ""), + "직종": sorted(trades), + "장비": mach, + "장비고름": opt_mach, + "자재": mats, + "덧줄율": pct, + "이름조건": {k: sorted(v) for k, v in conds(row.get("이름")).items()}, + "고름조건": {k: sorted(v) for k, v in conds(" ".join(labels + [str(s.get("이름")) for s in picks])).items()}, + "기계경비": is_mach_logic(row), + } + + +def combos(row): + picks = [] + for s in row.get("입력") or []: + opts = s.get("고르기") or [] + if not opts or any(w in str(s.get("이름")) for w in SKIP_IN): + continue + if any(isinstance(v, str) and _EQCODE.match(v) for v in opts): + continue + picks.append((str(s["이름"]), list(opts))) + if not picks: + return [{}] + if math.prod(len(o) for _, o in picks) <= 64: + return [dict(zip([n for n, _ in picks], vals)) for vals in itertools.product(*[o for _, o in picks])] + out = [{}] + for n, opts in picks: + out += [{n: v} for v in opts] + return out + + +def run_one(code, row, given, times, by_name): + auto = {k: v for k, v in m01.mau.values(WHOLE, row).items() if v is not None} + got = MF.run(WHOLE, code, {**auto, **m01._dec(given)}) + mid = got.get("중간") or {} + L, X, M, S = {}, [], {}, {} + for line in got.get("줄") or []: + h = by_name.get(line.get("이름")) + if h is None: + continue + try: + q = float(Decimal(str(line.get("수량") or 0))) / times + except ArithmeticError: + continue + kind = h.get("종류") + if kind == "인력": + t = LB.get(str(h.get("요소")), str(h.get("이름"))) + L[t] = L.get(t, 0.0) + q + elif kind == "로직": + m = _SUB.match(str(h.get("요소") or "")) + sub = m.group(1) if m else "" + sub_row = WHOLE.logic(sub) if sub else {} + if sub_row and is_mach_logic(sub_row): + ts, sp = set(FIXED_MACH.get(sub, set())) | mtypes(h.get("이름")), specnums(h.get("이름")) + for v in _args(m.group(2)).values(): + lit = v.strip("'\"") + if not _EQCODE.match(lit): + lit = str(mid.get(lit) or given.get(lit) or auto.get(lit) or "") + if _EQCODE.match(lit): + a, b = eq_of(lit) + ts |= a + sp |= b + if q: + X.append([sorted(ts), sorted(sp), q]) + elif q: + S[sub] = S.get(sub, 0.0) + q + elif kind == "재료" and q: + M[str(h.get("이름"))] = M.get(str(h.get("이름")), 0.0) + q + mix = {} + for k in ("인력비율", "기계비율"): + try: + if mid.get(k) not in (None, ""): + mix[k] = float(Decimal(str(mid.get(k)))) + except ArithmeticError: + pass + return {"L": L, "X": X, "M": M, "S": S, "mix": mix} + + +def _key(v): + r = lambda x: float(f"{x:.4g}") # noqa: E731 + return json.dumps( + [sorted((k, r(q)) for k, q in v["L"].items()), sorted((tuple(a), tuple(b), r(q)) for a, b, q in v["X"]), sorted((k, r(q)) for k, q in v["M"].items())], + ensure_ascii=False, + ) + + +def fingerprint(one): + code = one["로직"] + row = WHOLE.logic(code) + fp = static(code, row, one["단위"]) + times = times_of(row) + by_name = {} + for h in row.get("호표") or []: + by_name.setdefault(h.get("이름"), h) + seen, variants, errors = set(), [], 0 + for given in combos(row) if not fp["기계경비"] else [{}]: + try: + v = run_one(code, row, given, times, by_name) + except Exception: # noqa: BLE001 — 못 도는 조합은 셈만 + errors += 1 + continue + k = _key(v) + if k in seen: + continue + seen.add(k) + variants.append({"in": given, **v}) + fp["갈래수"] = len(variants) + fp["오류"] = errors + fp["변형"] = variants + return fp + + +def main(): + got = hm.targets() + if sys.argv[1:] == ["merge"]: + out = [] + for p in sorted(HERE.glob("ours_fp_*.json"), key=lambda p: int(p.stem.split("_")[-1])): + out += json.loads(p.read_text(encoding="utf-8")) + (HERE / "ours_fp.json").write_text(json.dumps(out, ensure_ascii=False, default=str), "utf-8") + ran = [f for f in out if f["변형"]] + stat = { + "로직": len(out), + "셈 됨(변형 1+)": len(ran), + "못 셈": [f["로직"] for f in out if not f["변형"]][:40], + "인력 있음": sum(1 for f in ran if any(v["L"] for v in f["변형"])), + "장비 있음": sum(1 for f in out if f["장비"] or any(v["X"] for v in f["변형"])), + "자재 있음": sum(1 for f in out if f["자재"]), + "기계경비 로직": sum(1 for f in out if f["기계경비"]), + "변형 합": sum(f["갈래수"] for f in out), + "변형 2+": sum(1 for f in out if f["갈래수"] > 1), + "조항 있음": sum(1 for f in out if f["조항"]), + } + print(json.dumps(stat, ensure_ascii=False)) + return + a, b = int(sys.argv[1]), int(sys.argv[2]) + t0 = time.time() + out = [fingerprint(one) for one in got["로직"][a:b]] + (HERE / f"ours_fp_{a}.json").write_text(json.dumps(out, ensure_ascii=False, default=str), "utf-8") + print(a, b, len(out), "변형", sum(f["갈래수"] for f in out), "오류", sum(f["오류"] for f in out), round(time.time() - t0, 1), "s") + + +if __name__ == "__main__": + main() diff --git a/docs/raw/조사/2026-10-15_338_실험자료/C4/s3_match.py b/docs/raw/조사/2026-10-15_338_실험자료/C4/s3_match.py new file mode 100644 index 000000000..a34830233 --- /dev/null +++ b/docs/raw/조사/2026-10-15_338_실험자료/C4/s3_match.py @@ -0,0 +1,355 @@ +"""req226 C4 ③ 지문 비교 맞춤 흉내 — 직종 · 품 벡터 · 장비 · 자재 · 단위 · 조건 · 조항(한 신호) · 이름(약한 덤). +금액 안 씀. 앞 조사와 같은 정답(labels · labels_y) · 같은 셈(s10_all.score)으로 지금 · 묶음 ① 과 맞댐. + +python s3_match.py → 표 · out_match.json(줄마다 1~5 위 · 채널 · 고른 입력) +""" + +import json +import sys + +import fp_common as F +from fp_common import HERE, close, cond_score, cosine, jacc + +import s10_all as A # noqa: E402 (C1 — 정답 · 셈 · 지금 돌리기) +from cats import NOW, ORDER # noqa: E402 +from s04_eval import clause_prefix # noqa: E402 +from s20_c3_extra import combo # noqa: E402 + +from M01_MasterData import M01_MasterData_Store as m01 # noqa: E402 +from M02_MasterTemplete import M02_Hopyo_Basis as hb # noqa: E402 +from M02_MasterTemplete import M02_Hopyo_Match as hm # noqa: E402 + +EX = {e["id"]: e for e in json.loads((HERE / "excel_fp.json").read_text(encoding="utf-8"))} +OURS = json.loads((HERE / "ours_fp.json").read_text(encoding="utf-8")) +TG = {x["로직"]: x for x in hm.targets()["로직"]} +OURC = hb.ours() +W = {"인력": 3.0, "장비": 2.0, "자재": 1.0, "율": 0.3, "단위": 0.5, "조건": 0.6, "조항": 1.5, "이름": 0.6} +WHOLE = m01.loaded()[1] + + +def prep(o): + """맞춤용 정리 — 기계경비 로직은 조종원 뺌 · 모두 0 인 변형 뺌 · 고를 수 있는 장비.""" + vs = [] + for v in o["변형"]: + L = {} if o["기계경비"] else {t: q for t, q in v["L"].items() if q > 0} + X = [(set(a), set(b), q) for a, b, q in v["X"] if q > 0] + M = {k: q for k, q in v["M"].items() if q > 0} + if L or X or M or not (v["L"] or v["X"] or v["M"]): + vs.append({"in": v["in"], "L": L, "X": X, "M": M}) + mix = v.get("mix") or {} + ri, rm = mix.get("인력비율"), mix.get("기계비율") + if (ri and 0 < ri < 100) or (rm and 0 < rm < 100): # 섞기 전 품(인력 · 기계 100%) + Lu = {t: q * 100 / ri for t, q in L.items()} if ri and 0 < ri < 100 else L + Xu = [(a, b, q * 100 / rm) for a, b, q in X] if rm and 0 < rm < 100 else X + vs.append({"in": {**v["in"], "_섞기전": f"인력 {ri:g}% · 기계 {rm:g}% → 100%"}, "L": Lu, "X": Xu, "M": M}) + if not vs: # 엔진이 못 돈 로직 — 정적 지문(직종 이름만 · 품 모름) + vs = [{"in": None, "L": {t: None for t in o["직종"]}, "X": [(set(a), set(b), None) for a, b in o["장비"] if a], "M": {}}] + opt = [] + if o["기계경비"]: + ts = set().union(*[set(a) for a, _ in o["장비고름"]], *[set(a) for a, _ in o["장비"]]) if (o["장비고름"] or o["장비"]) else set() + sp = set().union(*[set(b) for _, b in o["장비고름"]]) if o["장비고름"] else set() + opt = [(ts, sp)] + mats = set() + for name in o["자재"]: + mats |= mat_words(name) + for v in vs: + for name in v["M"]: + mats |= mat_words(name) + return { + **o, + "vs": vs, + "opt": opt, + "matw": mats, + "pctv": {round(p, 3) for _, p in o["덧줄율"]}, + "nameg": TG[o["로직"]]["grams"], + "nc": {k: set(v) for k, v in o["이름조건"].items()}, + "oc": {k: set(v) for k, v in o["고름조건"].items()}, + "has_L": any(v["L"] for v in vs), + "has_X": any(v["X"] for v in vs) or bool(opt), + } + + +def mat_words(name): + t = F.unicodedata.normalize("NFKC", str(name or "")).lower().replace(".", "").replace(" ", "") + out = set(hm.words(name)) + if "pvc" in t: + out.add("pvc") + return {w for w in out if len(w) >= 2} + + +P = [prep(o) for o in OURS] + + +def lab_sim(E, Eb, O, unit_ok): + if not O: + return 0.0, None + keys = set(E) | set(O) + w = lambda t: 0.5 if t == "보통인부" else 1.0 # noqa: E731 + st = sum(w(t) for t in set(E) & set(O)) / sum(w(t) for t in keys) + if any(q is None for q in O.values()): # 품 모름(정적) — 꼴 = 직종 겹침으로 갈음 · 크기 덤 없음 + return 0.6 * st, None + shape = max(cosine(E, O), cosine(Eb, O)) + if not unit_ok: # 단위가 달라 크기를 못 견줌 — 크기 덤 없음 + return 0.35 * st + 0.25 * shape, None + mag = sum(max(close(E.get(t), O.get(t)), close(Eb.get(t), O.get(t))) for t in keys) / len(keys) + return 0.35 * st + 0.25 * shape + 0.40 * mag, mag + + +def mach_sim(EM, OX, opt, unit_ok): + if not OX and not opt: + return 0.0 + got = [] + for m in EM: + mt, ms = set(m["갈래"]), set(m["규격"]) + best = 0.0 + for ot, osp, oh in OX: + j = jacc(mt, ot) + if not j: + continue + sp = (1.0 if ms & osp else 0.0) if ms and osp else 0.5 + hc = max(close(m.get("시간"), oh), close(m.get("밑시간"), oh)) if unit_ok and oh else 0.0 + best = max(best, 0.6 * j + 0.2 * sp + 0.2 * hc) + for ot, osp in opt: + if mt and mt <= ot: + sp = (1.0 if ms & osp else 0.0) if ms and osp else 0.5 + best = max(best, 0.6 + 0.2 * sp + 0.2 * (1.0 if unit_ok else 0.0)) + got.append(best) + s = sum(got) / len(got) + et = set().union(*[set(m["갈래"]) for m in EM]) + ot_all = set().union(*[t for t, _, _ in OX]) if OX else set() + extra = len(ot_all - et) / max(1, len(ot_all | et)) if ot_all else 0.0 + return s * (1 - 0.5 * extra) + + +def ex_prep(e): + E = {t: o["품"] for t, o in e["인력"].items() if o["품"]} + Eb = {t: o["밑품"] for t, o in e["인력"].items() if o["밑품"]} + mats = [mat_words(m["이름"]) for m in e["자재"]] + return { + **e, + "E": E, + "Eb": Eb, + "matl": [m for m in mats if m], + "pctv": {round(v, 3) for _, v in e["율"]}, + "nameg": hm._grams(hm.tidy(e["이름"])), + "theirs": [list(x) for x in e["출처"]], + "조건": {k: set(v) for k, v in e["조건"].items()}, + "이름조건": {k: set(v) for k, v in e["이름조건"].items()}, + } + + +COUNT = {"개소", "개", "본", "ea", "조", "매", "대", "기", "공"} +HOUR = {"hr", "시간", "h"} + + +def unit_fit(a, b): + """단위 맞음 + 별칭(개소 = 개 = 본 · hr = 시간 · 공/㎥ = 공㎥) — 품 크기 견줌에만.""" + if hm.fits(a, b): + return True + na = F.re.sub(r"[\s/]", "", str(a or "")).lower() + nbs = [F.re.sub(r"[\s/]", "", x).lower() for x in str(b or "").split("·")] + for nb in nbs: + if na == nb or (na in COUNT and nb in COUNT) or (na in HOUR and nb in HOUR): + return True + return False + + +def score_pair(e, o, use=None): + """(점수, 채널, 고른 변형) — use = 쓸 채널(None = 모두).""" + use = use or W + unit_ok = unit_fit(e["단위"], o["단위"]) + ch = {"단위": 1.0 if unit_ok else 0.0} + g1, g2 = e["nameg"], o["nameg"] + ch["이름"] = 2 * len(g1 & g2) / (len(g1) + len(g2)) if g1 and g2 else 0.0 + our = OURC.get(o["로직"]) + c = clause_prefix(e["theirs"], our)[0] if (our and e["theirs"]) else 0.0 + ch["조항"] = c * (0.5 if e["옛판"] else 1.0) + hit, _ = cond_score(e["조건"], o["nc"], o["oc"]) + _, clash = cond_score(e["이름조건"], o["nc"], {}) + ch["조건"] = hit - clash + if e["matl"]: + ch["자재"] = sum(1 for ws in e["matl"] if any(a in b or b in a for a in ws for b in o["matw"])) / len(e["matl"]) + ch["율"] = 1.0 if (e["pctv"] and o["pctv"] and e["pctv"] & o["pctv"]) else 0.0 + base = sum(use.get(k, 0) * v for k, v in ch.items()) + best, bestv, bestch = None, None, None + for v in o["vs"]: + s, vch = base, {} + if e["E"]: + ls, mag = lab_sim(e["E"], e["Eb"], v["L"], unit_ok) + vch["인력"], vch["품"] = ls, mag + s += use.get("인력", 0) * ls + elif v["L"] and "인력" in use: + s -= 0.6 + if e["장비"]: + ms = mach_sim(e["장비"], v["X"], o["opt"], unit_ok) + vch["장비"] = ms + s += use.get("장비", 0) * ms + elif (v["X"] or o["opt"]) and "장비" in use: + s -= 0.4 + if best is None or s > best: + best, bestv, bestch = s, v, vch + return best, {**ch, **bestch}, bestv + + +def rank(e, use=None, n=5): + scored = [] + for o in P: + s, ch, v = score_pair(e, o, use) + scored.append((s, o["로직"], ch, v)) + scored.sort(key=lambda x: -x[0]) + return scored[:n] + + +RB = {0: 1.0, 1: 0.6, 2: 0.45, 3: 0.35, 4: 0.25} + + +def sure(e, pool): + """숫자가 딱 맞음 — 1위의 직종 · 품(3% 안)이 다 맞거나(인력 줄) 장비 갈래 · 시간이 맞음(장비만 줄) · + 2위와 점수 차 0.3 넘음.""" + if not pool: + return False + s1, _, ch, _ = pool[0] + gap = s1 - pool[1][0] if len(pool) > 1 else 9 + if e["E"]: + rich = len(e["E"]) >= 2 or set(e["E"]) != {"보통인부"} or bool(e["장비"]) # 보통인부 하나뿐이면 우연 같음 + ok = rich and (ch.get("품") or 0) >= 0.97 and ch.get("인력", 0) >= 0.97 + elif e["장비"]: + ok = ch.get("장비", 0) >= 0.95 + else: + ok = False + return ok and gap > 0.3 + + +def fuse(fp_top, base, beta=2.0, pool=None): + """지문 점수 + β × 묶음 ① 차례 덤 → 다시 다섯.""" + bonus = {k: RB.get(i, 0) for i, k in enumerate(base["top"][:5])} + cand = {k: s for s, k, _, _ in (pool or fp_top)} + for k in bonus: + if k not in cand: + o = next(x for x in P if x["로직"] == k) + cand[k] = score_pair(EXP[CUR["id"]], o)[0] + out = sorted(cand, key=lambda k: -(cand[k] + beta * bonus.get(k, 0))) + return out[:5] + + +CUR = {"id": None} +EXP = {i: ex_prep(e) for i, e in EX.items()} + + +def as_results(tops): + return {i: {"상태": "후보", "top": t} for i, t in tops.items()} + + +def fails_by_cat(res, base_fail): + """지금 실패 36 의 갈래별 — 고쳐짐 수 / 갈래 수 · 새 실패.""" + out = {} + for i in base_fail: + cat = (NOW.get(i) or ["(새)"])[0] + ok = bool(res[i]["top"]) and res[i]["top"][0] in set(A.LAB[i]["정답"]) + a = out.setdefault(cat, [0, 0, []]) + a[1] += 1 + a[0] += ok + if not ok: + a[2].append(i) + return out + + +def main(): + ids = list(A.BY) + have = [i for i in ids if A.LAB[i]["정답"]] + now = A.run() + rec = combo() + tops, detail, pools = {}, {}, {} + variants = { + "지문 전부": None, + "지문 − 조항": {k: v for k, v in W.items() if k != "조항"}, + "지문 − 이름": {k: v for k, v in W.items() if k != "이름"}, + "지문 − 조항 − 이름(속만)": {k: v for k, v in W.items() if k not in ("조항", "이름")}, + "지문 − 품(직종 · 장비 갈래만)": "noqty", + } + rows, by_var = [], {} + for name, use in variants.items(): + t = {} + for i in ids: + e = EXP[i] + if use == "noqty": + e = {**e, "E": {k: 1.0 for k in e["E"]}, "Eb": {k: 1.0 for k in e["E"]}, "장비": [{**m, "시간": None, "밑시간": None} for m in e["장비"]]} + top = rank(e, None if use == "noqty" else use, 40) + t[i] = [k for _, k, _, _ in top[:5]] + if name == "지문 전부": + pools[i] = top + detail[i] = [{"로직": k, "점수": round(s, 3), "채널": {a: (round(b, 3) if isinstance(b, float) else b) for a, b in ch.items()}, "입력": v["in"] if v else None} for s, k, ch, v in top[:5]] + by_var[name] = t + rows.append(A.table(name, as_results(t))) + base_rows = [A.table("지금", now), A.table("묶음 ①", rec)] + fused = {} + for beta in (1.0, 2.0, 3.0): + t = {} + for i in ids: + CUR["id"] = i + t[i] = fuse(None, rec[i], beta, pools[i]) + fused[beta] = t + rows.append(A.table(f"지문 + 묶음 ① (β={beta:g})", as_results(t))) + t = {} + for i in ids: + CUR["id"] = i + t[i] = fuse(None, now[i], 2.0, pools[i]) + rows.append(A.table("지문 + 지금 (β=2)", as_results(t))) + keep_same = {i: (rec[i]["top"] if rec[i]["상태"] == "같음" else by_var["지문 전부"][i]) for i in ids} + rows.append(A.table("같음은 묶음 ① · 나머지 지문", as_results(keep_same))) + keep_same2 = {i: (rec[i]["top"] if rec[i]["상태"] == "같음" else fused[1.0][i]) for i in ids} + rows.append(A.table("같음은 묶음 ① · 나머지 지문+① β=1", as_results(keep_same2))) + print("묶음 ① 같음 줄", sum(1 for i in ids if rec[i]["상태"] == "같음")) + gate = {} + for i in ids: + gate[i] = by_var["지문 전부"][i] if sure(EXP[i], pools[i]) else fused[2.0][i] + rows.append(A.table("지문 우선(딱 맞음) + 묶음 ①", as_results(gate))) + with A.no_seeds(): + rows.append(A.table("지금 (씨앗 끔)", A.run())) + rec_off = combo() + rows.append(A.table("묶음 ① (씨앗 끔)", rec_off)) + t2, g2 = {}, {} + for i in ids: + CUR["id"] = i + t2[i] = fuse(None, rec_off[i], 2.0, pools[i]) + g2[i] = by_var["지문 전부"][i] if sure(EXP[i], pools[i]) else t2[i] + rows.append(A.table("지문 + 묶음 ① (씨앗 끔 β=2)", as_results(t2))) + rows.append(A.table("지문 우선 + 묶음 ① (씨앗 끔)", as_results(g2))) + k2 = {} + for i in ids: + CUR["id"] = i + k2[i] = rec_off[i]["top"] if rec_off[i]["상태"] == "같음" else fuse(None, rec_off[i], 1.0, pools[i]) + rows.append(A.table("같음은 묶음 ① · 나머지 지문+① β=1 (씨앗 끔)", as_results(k2))) + print("딱 맞음 줄", sum(1 for i in ids if sure(EXP[i], pools[i])), "/", len(ids), + "· 그중 지문 1위 엄격 맞음", sum(1 for i in have if sure(EXP[i], pools[i]) and by_var["지문 전부"][i][:1] and by_var["지문 전부"][i][0] in set(A.LAB[i]["정답"])), + "/", sum(1 for i in have if sure(EXP[i], pools[i]))) + for r in base_rows + rows: + A.show(r) + # 실패 갈래별 + base_fail = [i for i in have if not now[i]["top"] or now[i]["top"][0] not in set(A.LAB[i]["정답"])] + cats = { + "지금": fails_by_cat(now, base_fail), + "묶음 ①": fails_by_cat(rec, base_fail), + "지문": fails_by_cat(as_results(by_var["지문 전부"]), base_fail), + "지문 + 묶음 ①": fails_by_cat(as_results(fused[2.0]), base_fail), + } + for k, v in cats.items(): + print(k, {c: f"{a}/{b}" for c, (a, b, _) in v.items()}) + newfail = {} + for k, res in (("지문", as_results(by_var["지문 전부"])), ("지문 + 묶음 ①", as_results(fused[2.0])), ("묶음 ①", rec)): + newfail[k] = [i for i in have if i not in base_fail and (not res[i]["top"] or res[i]["top"][0] not in set(A.LAB[i]["정답"]))] + print("새 실패", k, newfail[k]) + out = { + "표": base_rows + rows, + "갈래": {k: {c: v for c, v in d.items()} for k, d in cats.items()}, + "새 실패": newfail, + "추천": keep_same2, + "딱 맞음": [i for i in ids if sure(EXP[i], pools[i])], + "줄": {i: {"1~5": detail[i], "지문만": by_var["지문 전부"][i], "지문+묶음①": fused[2.0][i], "묶음①": rec[i]["top"][:5], "지금": now[i]["top"][:5], "정답": A.LAB[i]["정답"], "허용": A.LAB[i]["허용"]} for i in ids}, + } + (HERE / "out_match.json").write_text(json.dumps(out, ensure_ascii=False, indent=1, default=str), "utf-8") + + +if __name__ == "__main__": + sys.setrecursionlimit(10000) + main() diff --git a/docs/raw/조사/2026-10-15_338_실험자료/C4/s4_changes.py b/docs/raw/조사/2026-10-15_338_실험자료/C4/s4_changes.py new file mode 100644 index 000000000..068a5bfca --- /dev/null +++ b/docs/raw/조사/2026-10-15_338_실험자료/C4/s4_changes.py @@ -0,0 +1,330 @@ +"""req226 C4 ④⑤ — 절(몫)마다 맞춤 → 로직 조합 · 「편의대로 바꿈」 꼴 나눔 · 「로직 X · 계수 바뀜」 판정. + +정답 있는 줄 전부(고유 126)에서 엑셀 지문 ↔ 정답 로직(가장 닮은 변형)을 견줌. 금액 안 씀. +python s4_changes.py → 표 찍음 · out_changes.json +""" + +import json +from collections import Counter, defaultdict + +import s3_match as S +from fp_common import HERE, close + +A, P, EXP, TG, OURC = S.A, S.P, S.EXP, S.TG, S.OURC +BYCODE = {o["로직"]: o for o in P} +COMPOSITE = { + **{i: {"GC000206", "GC000207"} for i in ("N003", "N019", "N045", "S055", "S059", "Y010")}, + "N050": {"GC000705", "GC000714", "GC000712"}, + "S062": {"GC000559", "GC000560"}, +} +ALL_CLAUSES = defaultdict(set) +for o in P: + for n in o["조항"]: + ALL_CLAUSES[o["책"]].add(n) +TOL = 0.03 + + +def work_sections(e): + secs = {} + for sec, t, q, b in e["인력절"]: + one = secs.setdefault(str(sec), {"인력": {}, "장비": []}) + a = one["인력"].setdefault(t, {"품": 0.0, "밑품": 0.0, "율": [], "어떻게": []}) + a["품"] += q + a["밑품"] += b + for m in e["장비"]: + secs.setdefault(str(m["절"]), {"인력": {}, "장비": []})["장비"].append(m) + return {k: v for k, v in secs.items() if v["인력"] or v["장비"]} + + +def _order(key): + a, _, b = str(key).partition(".") + return (int(a) if a.isdigit() else 0, b) + + +def section_fp(e, sec, part): + head = e["절"].get(sec) or e["절"].get(str(sec).split(".")[0]) or {} + sub = {**{k: v for k, v in e.items() if k in S.EX[e["id"]]}, "인력": part["인력"], "장비": part["장비"], "자재": [], "율": []} + sub["이름"] = f"{e['이름']} {head.get('이름') or ''}".strip() + sub["출처"] = head.get("출처") or e["출처"] + sub["조건"] = {k: sorted(v) for k, v in e["조건"].items()} + sub["이름조건"] = {} + return S.ex_prep(sub) + + +def combination(i): + e = EXP[i] + secs = work_sections(e) + if len(secs) < 2: + return None + out = [] + for sec, part in sorted(secs.items(), key=lambda x: _order(x[0])): + top = S.rank(section_fp(e, sec, part), None, 3) + out.append({"절": sec, "이름": (e["절"].get(sec) or {}).get("이름", ""), "1위": top[0][1], "점수": round(top[0][0], 2), "2위": top[1][1]}) + return out + + +def compare(e, code): + """엑셀 ↔ 우리 로직(가장 닮은 변형) — 직종마다 (엑셀 품, 밑품, 우리 품) · 판정.""" + o = BYCODE[code] + s, ch, v = S.score_pair(e, o) + O = {t: q for t, q in ((v or {}).get("L") or {}).items() if q} + E = {t: x["품"] for t, x in e["인력"].items()} + B = {t: x["밑품"] for t, x in e["인력"].items()} + unit_ok = S.unit_fit(e["단위"], o["단위"]) + rows, same, base_only, changed = [], True, False, [] + for t in sorted(set(E) | set(O)): + a, b, c = E.get(t), B.get(t), O.get(t) + rows.append([t, a and round(a, 4), b and round(b, 4), c and round(c, 4)]) + if a is None or c is None or not c: + same = False + continue + if abs(a / c - 1) <= TOL: + continue + if b and abs(b / c - 1) <= TOL: + base_only = True + continue + same = False + changed.append(f"{t} {c:.3g} → {a:.3g}") + trades_same = set(E) == set(O) and bool(E) + if not E and not O: + verdict = mach_verdict(e, v, o, unit_ok) + elif not unit_ok: + verdict = "단위 달라 크기 못 견줌" + elif any(q is None for q in ((v or {}).get("L") or {}).values()) or (v or {}).get("in") is None: + verdict = "우리 품 모름(엔진 못 돎)" + elif not trades_same: + verdict = "직종 다름" + elif same and not base_only: + verdict = "같음(3% 안)" + elif same and base_only: + verdict = "섞음 · 할증 붙임(밑품 같음)" + else: + verdict = "계수 바뀜" + return {"판정": verdict, "직종": rows, "바뀜": changed, "변형": (v or {}).get("in"), "점수": round(s, 2), "채널": {k: (round(x, 2) if isinstance(x, float) else x) for k, x in ch.items()}} + + +def mach_verdict(e, v, o, unit_ok): + """인력 없는 줄 — 장비 갈래 · 시간으로.""" + EM = e["장비"] + OX = list((v or {}).get("X") or []) + if not EM: + return "자재 · 그 밖(품 없음)" + if o["기계경비"]: + return "장비 시간 줄(기계경비 로직)" + et = set().union(*[set(m["갈래"]) for m in EM]) + ot = set().union(*[set(a) for a, _, _ in OX]) if OX else set() + if et != ot: + return "장비 갈래 다름" + if not unit_ok: + return "단위 달라 크기 못 견줌" + ok = all(any(set(m["갈래"]) & a and (close(m.get("시간"), q) >= 0.99 or close(m.get("밑시간"), q) >= 0.99) for a, _, q in OX) for m in EM) + return "같음(3% 안)" if ok else "계수 바뀜" + + +def extra_work(e, code): + """엑셀에 있는데 정답 로직(모든 변형 · 고를 수 있는 장비)엔 없는 장비 갈래 · 직종.""" + o = BYCODE[code] + ot = set() + tr = set(o["직종"]) + for v in o["vs"]: + for a, _, _ in v["X"]: + ot |= set(a) + tr |= set(v["L"]) + for a, _ in o["opt"]: + ot |= set(a) + for a, _ in o["장비"]: + ot |= set(a) + et = set().union(*[set(m["갈래"]) for m in e["장비"]]) if e["장비"] else set() + return sorted(et - ot), sorted(set(e["인력"]) - tr) + + +def section_compare(i): + """복합 정답 줄 — 절마다 가장 닮은 정답 몫과 견줌.""" + e = EXP[i] + out = [] + for sec, part in sorted(work_sections(e).items(), key=lambda x: _order(x[0])): + sub = section_fp(e, sec, part) + code = max(COMPOSITE[i], key=lambda k: S.score_pair(sub, BYCODE[k])[0]) + c = compare(sub, code) + out.append({"절": sec, "로직": code, "판정": c["판정"], "바뀜": c["바뀜"]}) + return out + + +def clause_kind(e, code): + if not e["출처"]: + return "출처 없음" + our = OURC.get(code) + c = S.clause_prefix(e["theirs"], our)[0] if our else 0 + if c >= 0.9: + return "조항 같음" + books = {b for b, _ in e["출처"]} + if our and our["책"] not in books: + return "다른 책 조항" + missing = [n for b, n in e["출처"] if n not in ALL_CLAUSES.get(b, set()) and not any(x.startswith(n + "-") for x in ALL_CLAUSES.get(b, set()))] + if e["옛판"] or missing: + return "옛 판 · 없는 번호" + return "다른 번호(같은 책)" + + +def pick_answer(i): + e = EXP[i] + lab = A.LAB[i]["정답"] + return max(lab, key=lambda k: S.score_pair(e, BYCODE[k])[0]) + + +def splits(ids): + """쪼갬 — 같은 파일 · 같은 정답 로직 줄 둘의 품을 더하면 우리 한 변형의 꼴(비율)과 같음(코사인 0.98+) · + 한 줄만으로는 그 꼴이 아님.""" + from fp_common import cosine + + found = set() + by = defaultdict(list) + for i in ids: + if A.LAB[i]["정답"] and i not in COMPOSITE: + by[(A.BY[i]["파일"], pick_answer(i))].append(i) + for (f, code), group in by.items(): + vs = [v["L"] for v in BYCODE[code]["vs"] if v["L"] and all(q for q in v["L"].values())] + for a in group: + for b in group: + if a >= b or not EXP[a]["E"] or not EXP[b]["E"]: + continue + if set(EXP[a]["E"]) == set(EXP[b]["E"]): + continue # 같은 일 되풀이(규격만 다름) + both = {t: EXP[a]["E"].get(t, 0) + EXP[b]["E"].get(t, 0) for t in set(EXP[a]["E"]) | set(EXP[b]["E"])} + whole = max((cosine(both, O) for O in vs), default=0) + alone = max((max(cosine(EXP[a]["E"], O), cosine(EXP[b]["E"], O)) for O in vs), default=0) + if whole >= 0.98 and alone < 0.95: + found |= {a, b} + return found + + +def main(): + ids = [i for i in A.BY if A.LAB[i]["정답"]] + res = json.loads((HERE / "out_match.json").read_text(encoding="utf-8"))["줄"] + # ── 로직 조합(절마다) ── + combos = {i: combination(i) for i in A.BY} + multi = [i for i in A.BY if combos[i]] + comp_rows = [] + for i, truth in COMPOSITE.items(): + got = {c["1위"] for c in combos[i] or []} + top1 = res[i]["지문만"][:1] + comp_rows.append({"id": i, "이름": A.BY[i]["이름"], "정답 조합": sorted(truth), "절 1위": [c["1위"] for c in combos[i] or []], "조합 맞음": truth <= got, "줄 1위": top1}) + print("절 둘 이상 줄", len(multi), "· 복합 정답 줄", len(COMPOSITE), "· 조합 맞음", sum(r["조합 맞음"] for r in comp_rows)) + for r in comp_rows: + print(" ", r) + # 조합 맞음 지표 = 복합 줄은 절 1위 묶음 ⊇ 정답 조합 · 나머지는 줄 1위 엄격 + def combo_ok(i, top1): + if i in COMPOSITE: + return COMPOSITE[i] <= {c["1위"] for c in combos[i] or []} + return bool(top1) and top1[0] in set(A.LAB[i]["정답"]) + + inside = sum(1 for i in ids if combos[i] and set(A.LAB[i]["정답"]) & {c["1위"] for c in combos[i]}) + print("절 둘 이상 줄 중 정답 로직이 절 1위 묶음 안에 있음", inside, "/", sum(1 for i in ids if combos[i])) + for key in ("지금", "묶음①", "지문만", "지문+묶음①"): + ok = sum(combo_ok(i, res[i][key]) for i in ids) + comp_only = sum(1 for i in COMPOSITE if res[i][key][:1] and res[i][key][0] in COMPOSITE[i]) + print(f"조합 맞음 {key}: {ok}/{len(ids)} = {100 * ok / len(ids):.1f}% (복합 8 줄 1위가 한 몫이라도 맞음 {comp_only})") + # ── 바꿈 꼴 ── + sp = splits(ids) + rows, kinds = [], Counter() + for i in ids: + e = EXP[i] + code = sorted(COMPOSITE[i])[0] if i in COMPOSITE else pick_answer(i) + if i in COMPOSITE: + secs = section_compare(i) + bad = [x for x in secs if x["판정"] not in ("같음(3% 안)", "섞음 · 할증 붙임(밑품 같음)")] + cmp_ = {"판정": "조합 · " + ("같음(3% 안)" if not bad else "계수 바뀜"), "바뀜": [f"{x['로직']} {b}" for x in bad for b in x["바뀜"]], "직종": [], "변형": None, "절": secs} + else: + cmp_ = compare(e, code) + ck = clause_kind(e, code) + rates = [r for x in e["인력"].values() for r in x["율"]] + [r for m in e["장비"] for r in (m.get("율") or [])] + mix = any(n in ("%", "섞음") and 0 < v < 1 for v, n in rates) and bool(e["장비"]) and bool(e["인력"]) + bonus = any(n not in ("%", "섞음") for v, n in rates) + xm, xt = extra_work(e, code) + bundle = i in COMPOSITE or bool(xm) + tags = [] + if S.hm.tidy(e["이름"]) != TG[code]["tidy"]: + tags.append("이름 다름") + if ck != "조항 같음": + tags.append(ck) + if bundle: + tags.append("묶음") + if i in sp: + tags.append("쪼갬") + if mix: + tags.append("섞음") + if bonus: + tags.append("할증 · 횟수 · 보정 붙임") + v = cmp_["판정"] + if v.endswith("계수 바뀜"): + tags.append("계수 바뀜") + if v == "직종 다름": + tags.append("직종 다름") + if v in ("단위 달라 크기 못 견줌",): + tags.append("단위 다름") + same = v in ("같음(3% 안)", "섞음 · 할증 붙임(밑품 같음)", "장비 시간 줄(기계경비 로직)") + if same and not bundle and not mix and not bonus and i not in sp: + tags.append("이름만" if ck == "조항 같음" else "인용 조항만" if ck != "출처 없음" else "출처 없음 · 품 같음") + for t in tags: + kinds[t] += 1 + rows.append({"id": i, "이름": e["이름"], "규격": e["규격"], "정답": code, "정답 이름": TG[code]["이름"], "조항": ck, "판정": v, "바뀜": cmp_["바뀜"], "직종": cmp_["직종"], "변형": cmp_["변형"], "더 있는 일": xm + xt, "꼴": tags, "지문 1위": res[i]["지문만"][:1], "1위 맞음": bool(res[i]["지문만"]) and res[i]["지문만"][0] in set(A.LAB[i]["정답"]), "줄수": A.BY[i]["줄수"]}) + print("바꿈 꼴(겹쳐 셈 · 고유 126)", dict(kinds.most_common())) + verdicts = Counter(r["판정"] for r in rows) + print("정답 로직 대 엑셀 품 판정", dict(verdicts)) + hit = [r for r in rows if r["1위 맞음"]] + print("지문 1위 맞은 줄의 판정", dict(Counter(r["판정"] for r in hit)), "/", len(hit)) + # ── 숫자는 어느 책 — 정답 · 허용에 두 책이 다 있는 줄 ── + nb = [] + for i in ids: + e = EXP[i] + cands = [k for k in dict.fromkeys(A.LAB[i]["정답"] + A.LAB[i]["허용"]) if k in BYCODE] + books = {BYCODE[k]["책"] for k in cands} + if len(books) < 2 or i in COMPOSITE: + continue + good = {} + for k in cands: + c = compare(e, k) + if c["판정"] in ("같음(3% 안)", "섞음 · 할증 붙임(밑품 같음)"): + good.setdefault(BYCODE[k]["책"], []).append(k) + cited = sorted({b for b, _ in e["출처"]}) + nb.append({"id": i, "이름": e["이름"], "인용 책": cited, "숫자 맞는 책": sorted(good), "로직": good}) + tally = Counter() + for x in nb: + g, c = x["숫자 맞는 책"], x["인용 책"] + if not g: + tally["어느 책과도 3% 넘게 다름"] += 1 + elif len(g) == 2: + tally["두 책 품 같음(숫자로 못 가름)"] += 1 + elif not c: + tally[f"출처 없음 · 숫자 {g[0]}"] += 1 + elif g[0] in c: + tally["숫자 = 인용 책"] += 1 + else: + tally[f"인용 {'·'.join(c)} · 숫자 {g[0]}"] += 1 + print("두 책 후보 줄", len(nb), dict(tally)) + # ── 책 갈래 11 ── + book = [] + for i in [k for k, v in S.NOW.items() if v[0] == "책 갈래(산림↔건설)"]: + e = EXP[i] + cands = list(dict.fromkeys(A.LAB[i]["정답"] + A.LAB[i]["허용"])) + line = {"id": i, "이름": e["이름"], "엑셀 출처": e["출처"], "엑셀 품": {t: round(x["품"], 4) for t, x in e["인력"].items()}} + for k in cands: + c = compare(e, k) + line[f"{BYCODE[k]['책']} {k}"] = {"판정": c["판정"], "품": {r[0]: r[3] for r in c["직종"]}, "변형": c["변형"]} + book.append(line) + for b in book: + print(" 책", json.dumps(b, ensure_ascii=False, default=str)[:400]) + # ── 없음 줄 — 지문 1위 점수 나눔 ── + none = [i for i in A.BY if not A.LAB[i]["정답"]] + sc = {i: res[i]["1~5"][0]["점수"] if res[i]["1~5"] else 0 for i in A.BY} + ok_sc = sorted(sc[i] for i in ids if res[i]["지문만"][:1] and res[i]["지문만"][0] in set(A.LAB[i]["정답"])) + print("없음 줄 1위 점수", sorted((round(sc[i], 2), i) for i in none)) + print("맞은 1위 점수 아래 10", [round(x, 2) for x in ok_sc[:10]], "· 중앙", round(ok_sc[len(ok_sc) // 2], 2)) + for cut in (2.5, 3.0, 3.5): + print(f" 문턱 {cut}: 없음 줄 숨김 {sum(1 for i in none if sc[i] < cut)}/{len(none)} · 맞은 1위 잃음 {sum(1 for x in ok_sc if x < cut)}/{len(ok_sc)}") + out = {"조합": {i: combos[i] for i in multi}, "복합": comp_rows, "바꿈": rows, "꼴수": dict(kinds), "판정수": dict(verdicts), "책갈래": book, "두책": nb, "두책셈": dict(tally)} + (HERE / "out_changes.json").write_text(json.dumps(out, ensure_ascii=False, indent=1, default=str), "utf-8") + + +if __name__ == "__main__": + main() diff --git a/docs/raw/조사/2026-10-15_338_실험자료/C4/s5_summary.py b/docs/raw/조사/2026-10-15_338_실험자료/C4/s5_summary.py new file mode 100644 index 000000000..3c7c33fb6 --- /dev/null +++ b/docs/raw/조사/2026-10-15_338_실험자료/C4/s5_summary.py @@ -0,0 +1,24 @@ +"""req226 C4 — 보고서용 표 · 예 뽑기(out_match · out_changes · 지문 파일 읽기만).""" +import json +import sys +from fp_common import HERE + +M = json.loads((HERE / "out_match.json").read_text(encoding="utf-8")) +C = json.loads((HERE / "out_changes.json").read_text(encoding="utf-8")) +TG = {x["로직"]: x for x in json.loads((HERE / "ours_fp.json").read_text(encoding="utf-8"))} +part = sys.argv[1] if len(sys.argv) > 1 else "table" +if part == "table": + for r in M["표"]: + a, n, s, y = r["전체"], r["북부청임도"], r["북부청사방"], r["동부청영월"] + print(f"| {r['이름']} | {a['1위 엄격']} / {a['1위 느슨']} | {a['5안 엄격']} / {a['5안 느슨']} | {n['1위 엄격']} | {s['1위 엄격']} | {y['1위 엄격']} | {a['줄 1위 느슨']} | {a['없음인데 후보']} |") +elif part == "combo": + for i in sys.argv[2:]: + print(i, [(c["절"], c["이름"][:18], c["1위"], TG[c["1위"]]["이름"][:22]) for c in C["조합"].get(i) or []]) +elif part == "rows": + want = set(sys.argv[2:]) + for r in C["바꿈"]: + if not want or r["id"] in want or r["판정"] in want or any(t in want for t in r["꼴"]): + print(r["id"], r["이름"][:16], "|", r["규격"][:18], "→", r["정답"], r["정답 이름"][:18], "|", r["판정"], r["바뀜"], "| 조항", r["조항"], "| 꼴", r["꼴"], "| 더", r["더 있는 일"], "| 1위맞음" if r["1위 맞음"] else "") +elif part == "twobook": + for x in C["두책"]: + print(x["id"], x["이름"][:16], "인용", x["인용 책"], "숫자", x["숫자 맞는 책"], x["로직"]) diff --git a/docs/raw/조사/2026-10-15_338_실험자료/C4/s6_specinputs.py b/docs/raw/조사/2026-10-15_338_실험자료/C4/s6_specinputs.py new file mode 100644 index 000000000..14234a6d1 --- /dev/null +++ b/docs/raw/조사/2026-10-15_338_실험자료/C4/s6_specinputs.py @@ -0,0 +1,41 @@ +"""req226 C4 — 엑셀이 쓴 조건값(지금 M02 규격 읽기 `spec_inputs`)으로 정답 로직을 돌린 품 ↔ 엑셀 품. +가장 닮은 변형(s4 판정)과 견줘 「규격 읽기만으로 같은 품이 나오나」를 셈. 읽기만.""" +import json +from collections import Counter + +import s2_ours_fp as O +import s4_changes as C +from fp_common import HERE, close + +rows = json.loads((HERE / "out_changes.json").read_text(encoding="utf-8"))["바꿈"] +cnt, ex = Counter(), [] +for r in rows: + if r["판정"] not in ("같음(3% 안)", "섞음 · 할증 붙임(밑품 같음)"): + continue + i, code = r["id"], r["정답"] + line = {k: C.A.BY[i].get(k) for k in ("이름", "규격", "단위")} + row = O.WHOLE.logic(code) + try: + values, unread = O.hm.spec_inputs(line, O.hm._logic_specs(code)) + by_name = {} + for h in row.get("호표") or []: + by_name.setdefault(h.get("이름"), h) + v = O.run_one(code, row, values, O.times_of(row), by_name) + except Exception as e: # noqa: BLE001 + cnt["못 돎"] += 1 + continue + E = C.EXP[i] + L = v["L"] + mix = v.get("mix") or {} + if mix.get("인력비율") and 0 < mix["인력비율"] < 100: + L = {t: q * 100 / mix["인력비율"] for t, q in L.items()} + if not E["E"]: + cnt["인력 없음(건넘)"] += 1 + continue + ok = set(E["E"]) == {t for t, q in L.items() if q} and all(max(close(E["E"][t], L.get(t)), close(E["Eb"].get(t), L.get(t))) >= 0.99 for t in E["E"]) + cnt["규격 읽기로 같은 품" if ok else "규격 읽기로는 다른 품(다른 변형이 맞음)"] += 1 + if not ok and len(ex) < 8: + ex.append((i, r["이름"], r["규격"], code, values, r["변형"])) +print(dict(cnt)) +for x in ex: + print(" ", x) diff --git a/docs/raw/조사/2026-10-15_338_실험자료/C5/c01_trees.py b/docs/raw/조사/2026-10-15_338_실험자료/C5/c01_trees.py new file mode 100644 index 000000000..ab5b2f1a2 --- /dev/null +++ b/docs/raw/조사/2026-10-15_338_실험자료/C5/c01_trees.py @@ -0,0 +1,489 @@ +"""req226 C5 — 바깥 호표(일위대가) 나무 세우기(읽기만 · 저장소 안 고침). + +호표마다 줄 → 갈래(단산 · 호표 참조 · 노임 · 자재 · 장비 · % · 별도계상 · 근거글 · 기타) + +단산 블록 · STmate D 블록의 몫(절)별 벡터(노임 인 · 장비 시간 · 자재 수). +꼴 셋 = 북부청 xls(지금 읽기 함수) · 거창 xlsx(지금 읽기 함수 + 블록 행 글 이어 붙임) · +STmate xlsx 다섯(새 흉내 읽기 — 줄 코드 = 14 열 「D00002_1」). + +python c01_trees.py → trees.json · 표 +""" + +from __future__ import annotations + +import ast +import json +import operator +import re +import sys +import warnings +from pathlib import Path + +ROOT = Path(r"C:\Program_coding\임도설계 및 견적자동화 프로그램 개발") +sys.path.insert(0, str(ROOT)) +HERE = Path(__file__).resolve().parent +C1 = HERE.parent.parent / "req225" / "C1" +warnings.filterwarnings("ignore") + +from M02_MasterTemplete import M02_Hopyo_Basis as hb # noqa: E402 +from M02_MasterTemplete import M02_Hopyo_Excel as hx # noqa: E402 + +DOCS = ROOT / "resources/knowledge/original/실무문서" +NORTH = { + "북부청임도": DOCS / "2026_실무일위대가 모음/2026년_북부지방산림청_임도사업_설계내역서(공통단가).xls", + "북부청사방": DOCS / "2026_실무일위대가 모음/2026년_북부지방산림청_사방사업(계류+댐)_설계내역서.xls", +} +GEO = {"거창": DOCS / "01.산불거창(거창북상병곡)/01.내역서/00.25년_산불_거창(거창,북상,병곡,산8외)내역+수량.xlsx"} +STM = { + "봉화": DOCS / "04.산불 봉화/01. 내역서/2024년 산불진화임도 실시설계(기번41_현동)_ L=3.46km.xlsx", + "동부청영월": DOCS / "05. 동부청 영월/내역서/2024년 임도시설사업2차(산불진화_기번6)-영월.xlsx", + "울진대흥": DOCS + / "2024년 간선임도(기번3-울진.대흥)(공통)/2024년 간선임도 신설사업(기번3 울진.울진.대흥.산65외2).xlsx", + "영덕계류": DOCS + / "20250527 2025년 계류보전사업(기번1-영덕.병곡.영리.산214-1)(변경)/01.내역자료/2025년 계류보전사업(기번1 영덕.병곡.영.산214)(변경).xlsx", + "울진소광": DOCS / "25년 산불진화임도(기번8)(울진소광)/2025년 산불진화임도 신설사업(기번8 울진.금강송.소광.산29외).xlsx", +} + +# ── 장비 열쇠 ───────────────────────────────────────────────────────── +EQ_TYPES = ( + ("굴착기", ("굴착기", "굴삭기", "백호", "b/h", "backhoe", "back-hoe")), + ("덤프", ("덤프",)), + ("크레인", ("크레인",)), + ("브레이커", ("브레이커", "브레이카")), + ("롤러", ("롤러", "로울러")), + ("펌프차", ("펌프차", "펌프카", "콘크리트펌프")), + ("믹서", ("믹서",)), + ("불도저", ("불도저", "불도우저")), + ("살수", ("살수", "물탱크")), + ("콤팩터", ("콤팩터", "컴팩터", "플레이트")), + ("화물차", ("화물",)), + ("공기압축기", ("공기압축기", "컴프레서")), + ("천공기", ("천공", "드릴")), + ("로더", ("로더",)), + ("그레이더", ("그레이더",)), + ("트레일러", ("트레일러",)), +) +_SIZE = re.compile(r"(\d+(?:\.\d+)?)\s*(㎥|m3|톤|ton|t\b|kw|hp)", re.I) + + +def eq_key(text: str) -> str | None: + """장비 이름 → 「굴착기|0.6」 꼴(크기 없으면 「굴착기|」) · 장비 아니면 None.""" + t = str(text or "").lower().replace("㎥", "㎥") + kind = next((k for k, ws in EQ_TYPES if any(w in t for w in ws)), None) + if kind is None: + return None + if "집게" in t: + kind += "+집게" + m = _SIZE.search(t.replace(",", "")) + size = m.group(1).rstrip("0").rstrip(".") if m else "" + return f"{kind}|{size}" + + +def trade_of(name: str) -> str | None: + """노임 이름 → 우리 인력 이름(빈칸 · 옛이름 풀이) · 아니면 None.""" + flat = re.sub(r"\s+", "", str(name or "")) + flat = re.sub(r"\(.*?\)", "", flat) if flat not in hb.trade_names() else flat + return hb.trade_names().get(flat) + + +# ── 식 풀이(품) — 첫 수(노임 · 장비 단가)를 1 로 두고 나머지 식만 셈 ─────────────────────── +_OPS = {ast.Mult: operator.mul, ast.Div: operator.truediv, ast.Add: operator.add, ast.Sub: operator.sub} + + +def _eval(node): + if isinstance(node, ast.Expression): + return _eval(node.body) + if isinstance(node, ast.Constant) and isinstance(node.value, (int, float)): + return float(node.value) + if isinstance(node, ast.BinOp) and type(node.op) in _OPS: + return _OPS[type(node.op)](_eval(node.left), _eval(node.right)) + if isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.USub): + return -_eval(node.operand) + raise ValueError("식 아님") + + +def qty_of(expr: str) -> float | None: + """「273,540 × 3인 ÷ 55㎥ × 2」 → 3/55×2 · 「59,020 ÷ 23.1 × 90%」 → 1/23.1×0.9.""" + s = re.sub(r"\([^()0-9]*\)", "", str(expr)) # (보정계수) 같은 글 괄호 + s = s.replace(",", "") + s = re.sub(r"(\d+(?:\.\d+)?)\s*%", r"(\1/100)", s) + s = s.replace("×", "*").replace("÷", "/").replace("X", "*").replace("x", "*") + s = re.sub(r"/\s*(?=[^\d(\s.])", " ", s) # 「인/㎥」 의 / 는 단위 + s = re.sub(r"[^\d.+\-*/() ]", " ", s) + s = re.sub(r"(?<=\d)\.(?!\d)", "", s) # 「4.」 → 4 + s = re.sub(r"\(\s*\)", " ", s).strip() + m = re.match(r"\s*\d+(?:\.\d+)?", s) + if not m: + return None + s = "1" + s[m.end() :] + s = re.sub(r"(?<=[\d)])\s+(?=[\d(])", "*", s) # 빈칸으로 붙은 수는 곱 + try: + v = _eval(ast.parse(s, mode="eval")) + except (SyntaxError, ValueError, ZeroDivisionError): + return None + return v if 0 < v < 1e4 else None + + +_TRADE = re.compile(r"^[\s*·\-○◇]*([가-힣][가-힣\s]*?)\s*:\s*([\d,]+(?:\.\d+)?)\s*(.*?)(?:=\s*([\d,]+(?:\.\d+)?))?\s*$") +_SECTION = re.compile(r"^\s*(\d+)\s*\.\s*(.*)$") +_EQ_COST = re.compile(r"^\s*노\s*무\s*비\s*:\s*([\d,]+(?:\.\d+)?)\s*(.*?)(?:=\s*[\d,.]+)?\s*$") +_SKIP_SEC = re.compile(r"^(합\s*계|소\s*계|계)") +_SUBLABEL = re.compile(r"^\s*[가-하]\s*\.\s*(.*)$") +_COSTLINE = re.compile(r"^\s*(노\s*무\s*비|재\s*료\s*비|경\s*비)\s*:") + + +def sections(texts: list[str]) -> list[dict]: + """블록 글 → 몫(맨 윗 번호 절)마다 {제목, 노임, 장비} — 노임 = 「직종 : 노임 × 식 = 금액」 · + 장비 = 장비 이름 줄 뒤 「노무비 : 단가 ÷ Q × 몫」(시간 = 식).""" + out, cur, eq, parent = [], None, None, "" + for raw in texts: + line = str(raw).rstrip() + sec = _SECTION.match(line) + if sec and not re.match(r"^\s*\d+\.\d", line): + title = sec.group(2).strip() + if _SKIP_SEC.match(title.replace(" ", "")): + cur = None + continue + parent = title + cur = {"제목": title, "절": title, "노임": {}, "장비": {}, "글": []} + out.append(cur) + eq = eq_key(title) + continue + sub = _SUBLABEL.match(line) if cur is not None else None + if sub and not _SKIP_SEC.match(sub.group(1).replace(" ", "")): + cur = {"제목": f"{parent} · {sub.group(1).strip()}", "절": parent, "노임": {}, "장비": {}, "글": []} + out.append(cur) + if cur is None: + if out or not line.strip(): + continue + cur = {"제목": "", "절": "", "노임": {}, "장비": {}, "글": []} + out.append(cur) + cur["글"].append(line.strip()) + flat = line.replace(" ", "") + if re.search(r"(소계|합계|인부소계)", flat): + eq = None + continue + sub = _SUBLABEL.match(line) + if sub: + eq = eq_key(sub.group(1)) + elif not _COSTLINE.match(line) and (k := eq_key(line)): + eq = k + m = _EQ_COST.match(line) + if m and eq: + q = qty_of(f"{m.group(1)} {m.group(2)}") + if q and q < 50: + cur["장비"][eq] = cur["장비"].get(eq, 0) + q + continue + t = _TRADE.match(line) + if t: + trade = trade_of(t.group(1)) + if trade: + q = qty_of(f"{t.group(2)} {t.group(3)}") + if q is None and t.group(4): # 식을 못 풀면 금액 ÷ 노임(품 되찾기만) + q = float(t.group(4).replace(",", "")) / float(t.group(2).replace(",", "")) + if q: + cur["노임"][trade] = cur["노임"].get(trade, 0) + q + return [s for s in out if s["노임"] or s["장비"]] + + +# ── 북부청 · 거창 — 지금 읽기 함수 ───────────────────────────────────────── +def _row_text(row: list, start: int = 1) -> str: + cells = [] + for v in row[start:140]: + if v is None or v == "": + continue + if isinstance(v, float) and v == int(v): + v = int(v) + cells.append(str(v).strip()) + return " ".join(cells) + + +def _sub(line: dict) -> str: + kind = line["갈래"] + if kind == "단가산출": + return "단산" + if kind == "호표": + return "호표" + if kind == "기계": + return "장비" + if kind in ("%", "별도계상", "자재"): + return kind + if str(line.get("단위") or "").strip() == "인" and trade_of(line["이름"]): + return "노임" + return "기타" + + +def _ids_north() -> dict: + items = json.loads((C1 / "out_items.json").read_text(encoding="utf-8")) + out = {} + for r in items: + ref = r.get("참조") or {} + out[(r["파일"], r["이름"], r["규격"], r["단위"], ref.get("번") if ref.get("종류") == "단산" else None)] = r["id"] + return out + + +def read_excel(tag: str, path: Path, ids: dict, join_rows: bool) -> dict: + data = path.read_bytes() + book = hx.read_book(path.name, data) + calc = hx._sheets(path.name, data).get(hb.SHEET) or [] + start, col, _ = hb._columns(calc) + heads = sorted(b["행"][0] for b in book["단산"]) + blocks = {} + for b in book["단산"]: + a = b["행"][0] + end = next((h for h in heads if h > a), len(calc) + 1) - 1 + texts = [] + for at in range(a, end + 1): + row = calc[at - 1] + text = _row_text(row) if join_rows else (row[col] if col < len(row) else "") + if isinstance(text, str) and text.strip(): + texts.append(text.rstrip()) + joined = " ".join(texts) + trades = [] + for t in texts: + m = _TRADE.match(t) + if m and (nm := trade_of(m.group(1))) and nm not in trades: + trades.append(nm) + blocks[b["번"]] = { + "이름": b["이름"], + "단위": b["단위"], + "출처": hb.sources(joined), + "직종": trades, + "글": texts[:60], + "몫": sections(texts[1:]), + } + hopyos = [] + for h in book["호표"]: + lines = [] + for one in h["줄"]: + ref = one.get("참조") or {} + kind = _sub(one) + rec = {k: one.get(k) for k in ("이름", "규격", "수량", "단위", "단가", "참조")} + rec["갈래"] = kind + key = (tag, one["이름"], one["규격"], one["단위"], ref.get("번") if ref.get("종류") == "단산" else None) + if key in ids: + rec["id"] = ids[key] + if kind == "단산" and ref.get("번") in blocks: + b = blocks[ref["번"]] + rec["근거"] = one.get("근거") if not join_rows else {"출처": b["출처"], "직종": b["직종"]} + rec["몫"] = b["몫"] + if kind == "노임": + rec["노임"] = {trade_of(one["이름"]): one["수량"]} + if kind == "장비": + rec["장비"] = {eq_key(f"{one['이름']} {one['규격']}"): one["수량"]} + lines.append(rec) + hopyos.append({"번": h["번"], "이름": h["이름"], "규격": h["규격"], "단위": h["단위"], "줄": lines}) + return {"파일": tag, "꼴": "거창" if join_rows else "북부청", "호표": hopyos, "단산": len(blocks)} + + +# ── STmate ─────────────────────────────────────────────────────────── +_HOPYO = re.compile(r"^\s*제\s*(\d+)\s*호표") +_RES = re.compile(r"^([LMXS]\d{5})([LMS])$") + + +def _num(v): + try: + return float(str(v).replace(",", "")) + except (TypeError, ValueError): + return None + + +def _code(row: list) -> str: + raw = str(row[14] if len(row) > 14 and row[14] is not None else "").strip() + return raw.rsplit("_", 1)[0] if "_" in raw else raw + + +def _ids_y() -> dict: + y = json.loads((C1 / "out_stmate.json").read_text(encoding="utf-8")) + return {(r["이름"], r["규격"], r["단위"], r["코드"]): r["id"] for r in y} + + +def _d_blocks(rows: list) -> dict: + """단가산출근거 → {D코드: {이름, 단위, 출처, 직종, 글, 몫}} — 자원 줄 = 0 열 「L00002L」 · 7 열 QTY · + 23 열 이름.""" + out, cur, texts = {}, None, [] + + def close(): + if cur is not None: + cur["글"] = texts[:60] + joined = " ".join(texts) + cur["출처"] = hb.sources(joined) + secs = sections_stm(cur.pop("_res"), texts) + cur["몫"] = secs + cur["직종"] = sorted({t for s in secs for t in s["노임"]}) + + for r in rows: + c0 = str(r[0] or "").strip() + c1 = str(r[1] or "").strip() if len(r) > 1 else "" + meta = re.search(r"BDCOD\|(D\d{5})", str(r[6] if len(r) > 6 and r[6] else "")) + code = meta.group(1) if meta else None + if code: + close() + cur, texts = {"코드": code, "_res": []}, [] + out[code] = cur + continue + if cur is None: + continue + if c0.startswith("총") and "계" in c0: + continue + m = _RES.match(c0) or re.search(r"&([LMXS]\d{5})([LMS])&", str(r[6] if len(r) > 6 and r[6] else "")) + if m: + full = str(r[23] if len(r) > 23 and r[23] else "") + name = full.split("/")[0].strip() + cur["_res"].append((m.group(1), m.group(2), _num(r[7]) if len(r) > 7 else None, name, len(texts), full)) + continue + if c1 and not c1.startswith("소계") and c1 != "계": + if "이름" not in cur: + cur["이름"] = c1 + texts.append(c1) + close() + return out + + +def sections_stm(res: list, texts: list[str]) -> list[dict]: + """STmate 자원 줄 → 몫별 벡터(맨 윗 번호 절 · 없으면 한 몫).""" + marks = [] + for at, t in enumerate(texts): + sec = _SECTION.match(t) + if sec and not re.match(r"^\s*\d+\.\d", t) and not _SKIP_SEC.match(sec.group(2).replace(" ", "")): + marks.append((at, sec.group(2).strip())) + def which(pos): + got = [m for m in marks if m[0] <= pos] + return got[-1][1] if got else "" + secs: dict[str, dict] = {} + for code, part, q, name, pos, full in res: + if q is None: + continue + title = which(pos) + s = secs.setdefault(title, {"제목": title, "노임": {}, "장비": {}, "자재": {}}) + if code[0] == "L" and part == "L": + trade = trade_of(name) + if trade: + s["노임"][trade] = s["노임"].get(trade, 0) + q + elif code[0] == "X" and part == "L": + k = eq_key(full) or f"?{code}" + s["장비"][k] = s["장비"].get(k, 0) + q + elif code[0] == "M": + s["자재"][name or code] = s["자재"].get(name or code, 0) + q + return [s for s in secs.values() if s["노임"] or s["장비"]] + + +def read_stmate(tag: str, path: Path, ids_y: dict) -> dict: + import openpyxl + + book = openpyxl.load_workbook(path, read_only=True, data_only=True) + rows = [list(r) for r in book["일위대가표"].iter_rows(values_only=True)] + calc = [list(r) for r in book["단가산출근거"].iter_rows(values_only=True)] + book.close() + blocks = _d_blocks(calc) + hopyos, cur, want_head = [], None, False + for r in rows: + head = str(r[0] or "") + hit = _HOPYO.match(head) + if hit: + cur = {"번": int(hit.group(1)), "줄": []} + hopyos.append(cur) + want_head = True + continue + if cur is None: + continue + if want_head: + cur.update(이름=str(r[0] or "").strip(), 규격=str(r[1] or "").strip(), 단위=str(r[3] or "").strip(), 코드=_code(r)) + want_head = False + continue + name = str(r[0] or "").strip() + if not name or name.replace(" ", "").startswith("합계"): + continue + code = _code(r) + if code[:1] in ("T", "C") and not code[1:2].isalpha(): + continue # 계 · 계약단가 · 소계 줄 + note = str(r[12] or "").strip() if len(r) > 12 else "" + spec = str(r[1] or "").strip() + price = {"계": _num(r[4]), "노무비": _num(r[6]), "재료비": _num(r[8]), "경비": _num(r[10])} + rec = {"이름": name, "규격": spec, "수량": _num(r[2]), "단위": str(r[3] or "").strip(), "단가": price, "코드": code} + k = code[:1] + if not code: + rec["갈래"] = "근거글" + elif code.startswith("PR") or rec["단위"] == "%": + rec["갈래"] = "%" + elif k == "B": + rec["갈래"], rec["참조"] = "호표", {"종류": "일위", "코드": code} + elif k == "D": + rec["갈래"], rec["참조"] = "단산", {"종류": "단산", "코드": code} + b = blocks.get(code) + if b: + rec["근거"] = {"출처": b["출처"], "직종": b["직종"]} + rec["몫"] = b["몫"] + rec["단산글"] = b["글"] + elif k == "L": + rec["갈래"], rec["노임"] = "노임", {trade_of(name) or name: rec["수량"]} + elif k == "X": + rec["갈래"], rec["장비"] = "장비", {eq_key(f"{name} {spec}") or f"?{code}": rec["수량"]} + elif k == "M": + apart = note.startswith("별산") or spec == "별도계상" or not any(price.values()) + rec["갈래"] = "별도계상" if apart else "자재" + elif k in "SW": + rec["갈래"] = "별도계상" + else: + rec["갈래"] = "기타" + if tag == "동부청영월" and k in "DX": + yk = (name, spec, rec["단위"], str(r[12] or "").strip()) + if yk in ids_y: + rec["id"] = ids_y[yk] + cur["줄"].append(rec) + by_code = {h.get("코드"): h["번"] for h in hopyos} + for h in hopyos: + for one in h["줄"]: + if one["갈래"] == "호표": + one["참조"]["번"] = by_code.get(one["코드"]) + return {"파일": tag, "꼴": "STmate", "호표": hopyos, "단산": len(blocks)} + + +KINDS = ("단산", "호표", "노임", "장비", "자재", "%", "별도계상", "근거글", "기타") + + +def depth(f: dict) -> int: + by = {h["번"]: h for h in f["호표"]} + + def d(no, seen=()): + h = by.get(no) + if not h or no in seen: + return 0 + kids = [one["참조"].get("번") for one in h["줄"] if one["갈래"] == "호표" and one.get("참조")] + return 1 + max((d(k, (*seen, no)) for k in kids if k), default=0) + + return max((d(h["번"]) for h in f["호표"]), default=0) + + +def main(): + ids_n, ids_y = _ids_north(), _ids_y() + files = [read_excel(t, p, ids_n, False) for t, p in NORTH.items()] + files += [read_excel(t, p, {}, True) for t, p in GEO.items()] + files += [read_stmate(t, p, ids_y) for t, p in STM.items()] + (HERE / "trees.json").write_text(json.dumps(files, ensure_ascii=False, indent=1, default=str), "utf-8") + print("| 파일 | 꼴 | 호표 | 줄 | " + " | ".join(KINDS) + " | 직접 줄 호표 | 참조만 호표 | 단산 블록 | 몫 벡터 있는 단산 줄 | 깊이 |") + tot = {} + for f in files: + cnt = {k: 0 for k in KINDS} + direct = refs_only = with_vec = dline = 0 + for h in f["호표"]: + kinds = [one["갈래"] for one in h["줄"]] + for k in kinds: + cnt[k] += 1 + if any(k in ("노임", "장비") for k in kinds): + direct += 1 + elif kinds and all(k in ("단산", "호표", "자재", "별도계상", "%", "근거글") for k in kinds): + refs_only += 1 + for one in h["줄"]: + if one["갈래"] == "단산": + dline += 1 + with_vec += bool(one.get("몫")) + n = sum(cnt.values()) + row = [f["파일"], f["꼴"], len(f["호표"]), n, *cnt.values(), direct, refs_only, f["단산"], f"{with_vec}/{dline}", depth(f)] + for k, v in zip(["호표", "줄", *KINDS, "직접", "참조만", "단산블록"], row[2:13] + [direct, refs_only, f["단산"]]): + tot[k] = tot.get(k, 0) + v + print("| " + " | ".join(map(str, row)) + " |") + print("합", tot) + + +if __name__ == "__main__": + main() diff --git a/docs/raw/조사/2026-10-15_338_실험자료/C5/c02_ourvec.py b/docs/raw/조사/2026-10-15_338_실험자료/C5/c02_ourvec.py new file mode 100644 index 000000000..a62f952db --- /dev/null +++ b/docs/raw/조사/2026-10-15_338_실험자료/C5/c02_ourvec.py @@ -0,0 +1,102 @@ +"""req226 C5 — 우리 로직 단위당 벡터(노임 인 · 장비 시간) — 우리 계산 엔진(`mf.run`)으로 냄(읽기만). + +auto = 자동값 한 벌(모든 돈 로직) · variants(code) = 고르기 입력 갈래(지역 뺌 · 곱이 cap 넘으면 하나씩 바꿈). +python c02_ourvec.py → ourvec_auto.json(돈 로직 1,438 자동값 벡터) +""" + +from __future__ import annotations + +import itertools +import json +import sys +from functools import lru_cache +from pathlib import Path + +HERE = Path(__file__).resolve().parent +sys.path.insert(0, str(HERE)) +from c01_trees import ROOT, eq_key, trade_of # noqa: E402,F401 + +from M01_MasterData import M01_MasterData_Store as m01 # noqa: E402 +from M02_MasterTemplete import M02_Hopyo_Match as hm # noqa: E402 + +mf = m01.cm.mf +WHOLE = m01.loaded()[1] +SKIP_INPUTS = {"지역"} + + +def vec_of(lines: list[dict]) -> dict: + """엔진 줄 → {노임: {직종: 인}, 장비: {열쇠: 시간}}.""" + out = {"노임": {}, "장비": {}} + for one in lines: + unit = str(one.get("단위") or "") + try: + q = float(one.get("수량") or 0) + except (TypeError, ValueError): + continue + if q <= 0: + continue + name = str(one.get("이름") or "") + if unit == "인": + t = trade_of(name) or name + out["노임"][t] = out["노임"].get(t, 0) + q + elif unit in ("시간", "hr", "HR", "h"): + k = eq_key(name) or f"?{name}" + out["장비"][k] = out["장비"].get(k, 0) + q + return out + + +def run(code: str, given: dict | None = None) -> dict | None: + logic = WHOLE.logic(code) + auto = {k: v for k, v in m01.mau.values(WHOLE, logic).items() if v is not None} + try: + got = mf.run(WHOLE, code, {**auto, **(given or {})}) + except Exception: # noqa: BLE001 — 못 도는 갈래는 뺌 + return None + return vec_of(got.get("줄") or []) + + +@lru_cache(maxsize=4096) +def variants(code: str, cap: int = 48) -> tuple: + """((입력 글, 벡터) …) — 자동값 + 고르기 갈래.""" + logic = WHOLE.logic(code) + picks = [ + (str(s["이름"]), list(s["고르기"])) + for s in logic.get("입력") or [] + if s.get("고르기") and str(s.get("이름")) not in SKIP_INPUTS + ] + combos = [{}] + total = 1 + for _, opts in picks: + total *= len(opts) + if picks and total <= cap: + combos = [dict(zip([n for n, _ in picks], vals)) for vals in itertools.product(*[o for _, o in picks])] + elif picks: + combos += [{n: v} for n, opts in picks for v in opts] + out, seen = [], set() + for given in combos: + v = run(code, given) + if v is None or not (v["노임"] or v["장비"]): + continue + key = json.dumps(v, sort_keys=True, ensure_ascii=False) + if key in seen: + continue + seen.add(key) + out.append((json.dumps(given, ensure_ascii=False, default=str), v)) + return tuple(out) + + +def main(): + got = hm.targets() + out = {} + for one in got["로직"]: + v = run(one["로직"]) + if v is not None: + out[one["로직"]] = v + (HERE / "ourvec_auto.json").write_text(json.dumps(out, ensure_ascii=False), "utf-8") + trades = sum(1 for v in out.values() if v["노임"]) + eqs = sum(1 for v in out.values() if v["장비"]) + print("로직", len(got["로직"]), "벡터", len(out), "노임 있음", trades, "장비 있음", eqs) + + +if __name__ == "__main__": + main() diff --git a/docs/raw/조사/2026-10-15_338_실험자료/C5/c02b_varcache.py b/docs/raw/조사/2026-10-15_338_실험자료/C5/c02b_varcache.py new file mode 100644 index 000000000..1d4a0735e --- /dev/null +++ b/docs/raw/조사/2026-10-15_338_실험자료/C5/c02b_varcache.py @@ -0,0 +1,21 @@ +"""갈래 벡터를 조각으로 미리 셈 → ourvec_var.json 에 더함 · python c02b_varcache.py <시작> <끝> [초 한도].""" +import json, sys, time +import c03_vec as V +import c01_trees # noqa: F401 — 저장소 경로 +from M02_MasterTemplete import M02_Hopyo_Match as hm + +a, b = int(sys.argv[1]), int(sys.argv[2]) +limit = float(sys.argv[3]) if len(sys.argv) > 3 else 95 +t0 = time.time() +codes = [x["로직"] for x in hm.targets()["로직"]][a:b] +done = 0 +for c in codes: + if c in V.CACHE: + done += 1 + continue + V.variants(c) + done += 1 + if time.time() - t0 > limit: + break +V.save_cache() +print("done", a, a + done, "of", b, "cache", len(V.CACHE), "sec", round(time.time() - t0, 1)) diff --git a/docs/raw/조사/2026-10-15_338_실험자료/C5/c03_vec.py b/docs/raw/조사/2026-10-15_338_실험자료/C5/c03_vec.py new file mode 100644 index 000000000..1a2df9e2c --- /dev/null +++ b/docs/raw/조사/2026-10-15_338_실험자료/C5/c03_vec.py @@ -0,0 +1,107 @@ +"""req226 C5 — 벡터 맞춤 부품(몫 벡터 ↔ 우리 로직 단위당 벡터) · 갈래 벡터 디스크 캐시. + +점수 = 차원(직종 · 장비 종류)마다 가까움(작은/큰) 가중 평균 · 보통인부 무게 0.5 · +장비는 있음 0.5 + 시간 가까움 0.5(작성자가 효율 · 사이클을 바꿔도 종류는 남음) · 크기 다르면 × 0.8 · +몫(10% · 90% 같은 나눔)은 x ∈ {0.1 … 0.9} 로 늘여 봄(× 0.95). 금액 안 씀. +""" + +from __future__ import annotations + +import json +import sys +from pathlib import Path + +HERE = Path(__file__).resolve().parent +sys.path.insert(0, str(HERE)) + +AUTO = json.loads((HERE / "ourvec_auto.json").read_text(encoding="utf-8")) +CACHE_FILE = HERE / "ourvec_var.json" +try: + CACHE = json.loads(CACHE_FILE.read_text(encoding="utf-8")) +except (OSError, ValueError): + CACHE = {} +COMMON = "보통인부" +SHARES = (0.1, 0.2, 0.3, 0.5, 0.7, 0.8, 0.9) + + +def variants(code: str) -> list: + """[(입력 글, 벡터) …] — 디스크 캐시 · 없으면 엔진으로 셈.""" + if code not in CACHE: + import c02_ourvec as ov + + CACHE[code] = [list(x) for x in ov.variants(code)] + return CACHE[code] or ([["{}", AUTO[code]]] if code in AUTO else []) + + +def save_cache(): + CACHE_FILE.write_text(json.dumps(CACHE, ensure_ascii=False), "utf-8") + + +def _eq_split(k: str): + kind, _, size = k.partition("|") + return kind, size + + +def sim(E: dict, O: dict, x: float = 1.0) -> float: + """E · O = {노임: {..}, 장비: {..}} — 0..1.""" + num = den = 0.0 + et, ot = E.get("노임") or {}, O.get("노임") or {} + for t in set(et) | set(ot): + w = 0.5 if t == COMMON else 1.0 + a, b = et.get(t, 0.0), ot.get(t, 0.0) * x + den += w + if a > 0 and b > 0: + num += w * min(a, b) / max(a, b) + ee, oe = E.get("장비") or {}, O.get("장비") or {} + used = set() + for k, a in ee.items(): + kind, size = _eq_split(k) + best = 0.0 + for k2, b in oe.items(): + kind2, size2 = _eq_split(k2) + if kind2 != kind and kind2.split("+")[0] != kind.split("+")[0]: + continue + f = 1.0 if (not size or not size2 or size == size2) else 0.8 + f *= 1.0 if kind2 == kind else 0.8 # 굴착기 ↔ 굴착기+집게(같은 기종 · 덧장치만 다름) + b *= x + s = f * (0.5 + 0.5 * (min(a, b) / max(a, b) if a > 0 and b > 0 else 0)) + if s > best: + best, pick = s, k2 + den += 1.0 + num += best + if best: + used.add(pick) + den += sum(1.0 for k in oe if k not in used) + return num / den if den else 0.0 + + +def fit(E: dict, code: str) -> tuple[float, str, float]: + """(점수, 입력 글, 몫 x) — 갈래마다 x = 1 과 몫 x 중 가장 좋은 것.""" + best = (0.0, "{}", 1.0) + for given, O in variants(code): + s = sim(E, O) + if s > best[0]: + best = (s, given, 1.0) + for x in SHARES: + s2 = 0.95 * sim(E, O, x) + if s2 > best[0]: + best = (s2, given, x) + return best + + +def dims(E: dict) -> set: + out = {t for t in (E.get("노임") or {}) if t != COMMON} + out |= {_eq_split(k)[0] for k in (E.get("장비") or {})} + return out + + +def index() -> dict: + """{차원: {로직}} — 보통인부 뺀 직종 · 장비 종류(자동값 벡터 기준).""" + out: dict[str, set] = {} + for code, v in AUTO.items(): + for d in dims(v): + out.setdefault(d, set()).add(code) + return out + + +IDX = index() diff --git a/docs/raw/조사/2026-10-15_338_실험자료/C5/c04_closed.py b/docs/raw/조사/2026-10-15_338_실험자료/C5/c04_closed.py new file mode 100644 index 000000000..d2f721dba --- /dev/null +++ b/docs/raw/조사/2026-10-15_338_실험자료/C5/c04_closed.py @@ -0,0 +1,304 @@ +"""req226 C5 — 닫힌 세계 풀이 (가): 지금 맞춤 후보 다섯(그대로 부름)에 호표 묶음 조건을 걸어 다시 고름. + +조건 = ① 같은 책(줄 근거 책 · 없으면 호표 안 근거 책 다수) ② 같은 조건(토질 — 호표 · 줄 이름) · +③ 우리 조합(일위대가조합.json 37) 통째 같음 · 안에 다 듦 · ④ 아래에서 위로(호표 참조 = 그 호표 풀이) — +④는 줄 지표엔 안 듦(참조 줄은 로직 줄 아님) · 조합 지표(c05)에서 씀. +지표 = 앞 조사(C1 · C3)와 같은 정답 · 같은 셈(고유 줄 126 · 씨앗 켬 · 기억 없음). + +python c04_closed.py → 표 · closed.json(줄마다 결과 — c05 가 씀) +""" + +from __future__ import annotations + +import json +import re +import sys +from pathlib import Path + +HERE = Path(__file__).resolve().parent +C1 = HERE.parent.parent / "req225" / "C1" +C3 = HERE.parent.parent / "req225" / "C3" +for p in (HERE, C1, C3): + sys.path.insert(0, str(p)) + +import s10_all as S # noqa: E402 +from s04_eval import clause_prefix, hb, hm, patched # noqa: E402 +from s10_all import candidates_nofold, no_seeds # noqa: E402 +from s16_guard import typo_guard # noqa: E402 + +from c01_trees import ROOT # noqa: E402 + +TREES = json.loads((HERE / "trees.json").read_text(encoding="utf-8")) +LABELED = ("북부청임도", "북부청사방", "동부청영월") +COMBOS = [ + (r["키"], r["이름"], [x["로직"] for x in r["담은로직"]]) + for r in json.loads((ROOT / "resources/master_data/일위대가조합.json").read_text(encoding="utf-8"))["줄"] + if r["키"] != "UA000001" +] + + +def hopyo_members() -> dict: + """{(파일, 호표 번): [id …](차례 · 겹침 뺌)}.""" + out = {} + for f in TREES: + if f["파일"] not in LABELED: + continue + for h in f["호표"]: + ids = list(dict.fromkeys(one["id"] for one in h["줄"] if one.get("id"))) + out[(f["파일"], h["번"])] = ids + return out + + +MEMBERS = hopyo_members() +HOPYO_OF = {} +for key, ids in MEMBERS.items(): + for i in ids: + HOPYO_OF.setdefault(i, []).append(key) +HEADS = {(f["파일"], h["번"]): f"{h['이름']} {h['규격']}" for f in TREES for h in f["호표"]} + + +def line_of(r: dict) -> dict: + return {k: r.get(k) for k in ("이름", "규격", "수량", "단위", "단가", "갈래", "참조", "근거")} + + +def book(code: str) -> str | None: + one = hb.ours().get(code) + return one["책"] if one else None + + +def own_books(r: dict) -> set: + return {b for b, _ in ((r.get("근거") or {}).get("출처") or [])} + + +def pools() -> dict: + """줄마다 지금 합친 후보(근거 + 이름 · 다섯 자르기 전 · 15).""" + got = hm.targets() + out = {} + for r in S.ALL: + line = line_of(r) + basis = hb.candidates(line, got) + named = hm.candidates(line, "로직", got, None) + out[r["id"]] = [c["로직"] for c in hb.merge(basis, named) if c.get("로직")][:15] + return out + + +# ── 같은 일 · 조건 ─────────────────────────────────────────────────── +_NAMES = {} + + +def name(code: str) -> str: + if not _NAMES: + _NAMES.update({x["로직"]: x["이름"] for x in hm.targets()["로직"]}) + return _NAMES.get(code, "") + + +def same_work(a: str, b: str) -> bool: + """두 로직이 같은 일(책만 다름) — 정리 이름 글자 두 개 묶음 겹침 0.5 이상 또는 뜻 낱말 겹침.""" + ta, tb = hm.tidy(name(a)), hm.tidy(name(b)) + ga, gb = hm._grams(ta), hm._grams(tb) + dice = 2 * len(ga & gb) / (len(ga) + len(gb)) if ga and gb else 0 + return dice >= 0.5 or hm.near(hm.words(name(a)), hm.words(name(b))) and dice >= 0.34 + + +SOIL = re.compile(r"(보통토사|혼합토사|경질토사|토사|사질토|점질토)") +ROCK = re.compile(r"(리핑암|발파암|연암|경암|풍화암|보통암|극경암|암절취|\(암\)| 암$|암\))") + + +def soil(text: str) -> str | None: + t = str(text or "") + s, r = bool(SOIL.search(t)), bool(ROCK.search(t)) + return "토사" if s and not r else "암" if r and not s else None + + +# ── 조건 걸기 ───────────────────────────────────────────────────────── +def apply_book(res: dict, pool: dict, mode: str) -> dict: + """mode 'line' = 줄 근거 책 먼저 · 없으면 호표 다수 / 'hopyo' = 근거 없는 줄만 호표 다수.""" + out = {} + for i, got in res.items(): + r = S.BY[i] + own = own_books(r) + votes: dict[str, int] = {} + for key in HOPYO_OF.get(i, []): + for j in MEMBERS[key]: + for b in own_books(S.BY[j]): + votes[b] = votes.get(b, 0) + 1 + top_v = sorted(votes.items(), key=lambda x: -x[1]) + major = top_v[0][0] if top_v and (len(top_v) == 1 or top_v[0][1] > top_v[1][1]) else None + if mode == "line": + want = next(iter(own)) if len(own) == 1 else (major if not own else None) + else: + want = major if not own else None + top = got["top"] + out[i] = got + if not top or want is None or book(top[0]) == want: + continue + alts = top[1:5] + [c for c in pool[i][:10] if c not in top] + pick = next((c for c in alts if book(c) == want and same_work(c, top[0])), None) + if pick: + out[i] = {"상태": "후보", "top": [pick] + [c for c in top if c != pick][:4]} + return out + + +def apply_soil(res: dict) -> dict: + out = {} + for i, got in res.items(): + r = S.BY[i] + want = soil(f"{r['이름']} {r['규격']}") or next( + (s for key in HOPYO_OF.get(i, []) if (s := soil(HEADS.get(key)))), None + ) + top = got["top"] + if not want or len(top) < 2 or got["상태"] == "같음": + out[i] = got + continue + clash = lambda c: (s := soil(name(c))) is not None and s != want # noqa: E731 + out[i] = {**got, "top": sorted(top, key=clash)} + return out + + +def _match(slots: list[list[str]], keys: set) -> dict | None: + """slot 마다 다른 열쇠 하나(slot 후보 안) — 다 맞으면 {slot 차례: 열쇠}.""" + assign: dict[str, int] = {} + + def try_(s, seen): + for k in slots[s]: + if k in keys and k not in seen: + seen.add(k) + if k not in assign or try_(assign[k], seen): + assign[k] = s + return True + return False + + for s in range(len(slots)): + if not try_(s, set()): + return None + return {s: k for k, s in assign.items()} + + +def backed(i: str, code: str | None) -> bool: + """1순위가 줄 근거 조항과 맞음(같은 책 조항 같음 0.9 이상).""" + theirs = (S.BY[i].get("근거") or {}).get("출처") or [] + our = hb.ours().get(code) if code else None + return bool(theirs and our and hb._clause(theirs, our)[0] >= 0.9) + + +def apply_combo(res: dict, mode: str, guard: bool = False) -> tuple[dict, list]: + """mode 'whole' = 호표 로직 줄 수 = 조합 로직 수 · 'inside' = 호표 줄이 모두 한 조합 안(줄 2 이상) · + guard = 1순위가 줄 근거와 맞는 줄은 안 바꿈.""" + out = dict(res) + hits = [] + for key, ids in MEMBERS.items(): + ids = [i for i in ids if res[i]["top"]] + if not ids or (mode == "inside" and len(ids) < 2): + continue + slots = [res[i]["top"][:5] for i in ids] + found = [] + for ck, cname, codes in COMBOS: + keys = set(codes) + if mode == "whole" and len(keys) != len(ids): + continue + m = _match(slots, keys) + if m is not None: + rank = sum(slots[s].index(k) for s, k in m.items()) + found.append((rank, ck, cname, m)) + if not found: + continue + found.sort() + rank, ck, cname, m = found[0] + hits.append((key, ck, cname, len(found), rank)) + for s, k in m.items(): + i = ids[s] + top = res[i]["top"] + if top[0] != k and not (guard and backed(i, top[0])): + out[i] = {"상태": "후보", "top": [k] + [c for c in top if c != k][:4]} + return out, hits + + +def apply_name_vote(res: dict) -> dict: + """한 문서 안 정리 이름이 같은 줄(규격만 다름)은 같은 로직 — 다수 1순위가 다섯 안에 있으면 그것.""" + out = dict(res) + groups: dict = {} + for i in res: + groups.setdefault((S.BY[i]["파일"], hm.tidy(S.BY[i]["이름"])), []).append(i) + for ids in groups.values(): + if len(ids) < 2: + continue + votes: dict = {} + for i in ids: + t = (res[i]["top"] or [None])[0] + if t: + votes[t] = votes.get(t, 0) + S.BY[i]["줄수"] + ranked = sorted(votes.items(), key=lambda x: -x[1]) + if not ranked or (len(ranked) > 1 and ranked[0][1] == ranked[1][1]): + continue + major = ranked[0][0] + for i in ids: + top = res[i]["top"] + if top and top[0] != major and major in top[:5] and not backed(i, top[0]): + out[i] = {"상태": "후보", "top": [major] + [c for c in top if c != major][:4]} + return out + + +def base_runs() -> dict: + """{이름: (결과, 후보 풀)} — 지금 · 묶음 ①(C3 추천 결합) · 씨앗 끔 둘.""" + out = {} + out["지금"] = (S.run(), pools()) + with patched(hb, _clause=clause_prefix), patched(hm, candidates=candidates_nofold): + out["묶음①"] = (S.run(typo_guard), pools()) + with no_seeds(): + out["지금(씨앗 끔)"] = (S.run(), pools()) + with patched(hb, _clause=clause_prefix), patched(hm, candidates=candidates_nofold): + out["묶음①(씨앗 끔)"] = (S.run(typo_guard), pools()) + return out + + +def changed(a: dict, b: dict) -> list: + rows = [] + for i in a: + if not S.LAB[i]["정답"]: + continue + t1, t2 = (a[i]["top"] or [None])[0], (b[i]["top"] or [None])[0] + if t1 != t2: + s, lo = set(S.LAB[i]["정답"]), set(S.LAB[i]["정답"]) | set(S.LAB[i]["허용"]) + mark = lambda t: "O" if t in s else "o" if t in lo else "x" # noqa: E731 + rows.append((i, S.BY[i]["이름"][:18], f"{mark(t1)}→{mark(t2)}", name(t1)[:14], name(t2)[:14])) + return rows + + +def main(): + runs = base_runs() + rows, keep = [], {} + for base, (res, pool) in runs.items(): + steps = {"": res} + steps["+책(줄→호표)"] = apply_book(res, pool, "line") + steps["+책(호표만)"] = apply_book(res, pool, "hopyo") + steps["+토질"] = apply_soil(res) + steps["+조합 통째"], hits_w = apply_combo(res, "whole") + steps["+조합 안"], hits_i = apply_combo(res, "inside") + steps["+조합 통째(근거 지킴)"], _ = apply_combo(res, "whole", True) + steps["+조합 안(근거 지킴)"], _ = apply_combo(res, "inside", True) + steps["+같은 이름 다수(문서)"] = apply_name_vote(res) + good, _ = apply_combo(apply_name_vote(apply_soil(res)), "inside", True) + steps["+(가) 좋은 것(토질 · 이름 다수 · 조합 안 근거 지킴)"] = good + allc = apply_soil(apply_book(res, pool, "line")) + allc, _ = apply_combo(allc, "whole") + steps["+모두(책 · 토질 · 통째)"] = allc + for tag, r in steps.items(): + rows.append(S.table(f"{base}{tag}", r)) + keep[base] = {k: v for k, v in steps.items()} + if base in ("지금", "묶음①"): + print(f"== {base}: 조합 통째 맞음 호표 {len(hits_w)} · 안에 다 듦 {len(hits_i)}") + for key, ck, cname, n, rank in hits_w + hits_i: + print(f" {key} {HEADS.get(key)[:24]} → {ck} {cname} (맞는 조합 {n} · 순위합 {rank})") + for tag in ("+책(줄→호표)", "+책(호표만)", "+토질", "+조합 통째", "+조합 안", "+조합 통째(근거 지킴)", "+조합 안(근거 지킴)", "+같은 이름 다수(문서)"): + for row in changed(res, steps[tag]): + print(f" [{tag}] {row}") + for row in rows: + S.show(row) + (HERE / "closed.json").write_text( + json.dumps({b: {k: v for k, v in s.items()} for b, s in keep.items()}, ensure_ascii=False), "utf-8" + ) + (HERE / "closed_table.json").write_text(json.dumps(rows, ensure_ascii=False, indent=1), "utf-8") + + +if __name__ == "__main__": + main() diff --git a/docs/raw/조사/2026-10-15_338_실험자료/C5/c05_solve.py b/docs/raw/조사/2026-10-15_338_실험자료/C5/c05_solve.py new file mode 100644 index 000000000..2b64e28c1 --- /dev/null +++ b/docs/raw/조사/2026-10-15_338_실험자료/C5/c05_solve.py @@ -0,0 +1,254 @@ +"""req226 C5 — 조합 풀이 엔진(닫힌 세계 · 금액 안 씀). + +블록(단산 · STmate D) 또는 직접 줄 호표의 노임 · 장비 벡터를 우리 로직 단위당 벡터 조합으로 풂: + ① 통째 하나 — 블록 합을 로직 하나가 설명(맞음 ≥ 0.85) + ② 절마다 하나 — 맨 윗 번호 절(1. 2. 3.)마다(맞음 ≥ 0.75) + ③ 몫마다 — 절 안 가. 나. 몫마다 · 장비만 든 몫 = 「기계 시간당 기계경비」(기종 입력 · 닫힌 세계 규칙) · + 노임 몫 = 맞음 ≥ 0.6 +후보 풀 = 지금 맞춤 합친 후보 15(그대로 부름) + 직종 색인(드문 직종 · 장비 40 개 이하) + 절 제목 이름 후보 8. +순위 = 벡터 맞음 + 앞선 믿음(지금 순위 0.3 · 줄 근거 조항 같음(윗마디 포함) 0.3 · 같은 책 0.1). +""" + +from __future__ import annotations + +import re +import sys +from pathlib import Path + +HERE = Path(__file__).resolve().parent +C1 = HERE.parent.parent / "req225" / "C1" +for p in (HERE, C1): + sys.path.insert(0, str(p)) + +import c03_vec as V # noqa: E402 +from c01_trees import ROOT # noqa: E402,F401 +from s04_eval import clause_prefix, hb, hm # noqa: E402 + +MACHINE = {"굴착기": "GC000267"} # 굴착기(무한궤도) 시간당 기계경비 · 그 밖 장비 = GC000268 +MACHINE_ANY = "GC000268" +MACHINE_SET = {"GC000267", "GC000268", "GC000269", "GC000275"} +RARE = {d for d, codes in V.IDX.items() if len(codes) <= 40} + + +def merged(line: dict, got: dict, n: int = 15) -> list[str]: + basis = hb.candidates(line, got) + named = hm.candidates(line, "로직", got, None) + return [c["로직"] for c in hb.merge(basis, named) if c.get("로직")][:n] + + +def title_cands(title: str, unit: str, got: dict, n: int = 8) -> list[str]: + text = re.sub(r"[\d.·:()\[\]%]+", " ", str(title or "")) + text = re.sub(r"\s+", "", text) + if len(text) < 2: + return [] + return [c["로직"] for c in hm.candidates({"이름": text, "단위": unit}, "로직", got, n) if c.get("로직")] + + +class Prior: + """앞선 믿음 — 지금 순위 · 줄 근거 조항(윗마디 포함) · 같은 책.""" + + def __init__(self, line: dict, ranked: list[str]): + self.rank = {c: k for k, c in enumerate(ranked)} + self.n = max(len(ranked), 1) + self.theirs = [list(x) for x in ((line.get("근거") or {}).get("출처") or [])] + self.books = {b for b, _ in self.theirs} + self.table = hb.ours() + + def __call__(self, code: str) -> float: + p = 0.0 + if code in self.rank: + p += 0.3 * (1 - self.rank[code] / self.n) + our = self.table.get(code) + if our and self.theirs: + if clause_prefix(self.theirs, our)[0] >= 0.9: + p += 0.3 + if our["책"] in self.books: + p += 0.1 + return p + + +def _sum(parts: list[dict]) -> dict: + out = {"노임": {}, "장비": {}} + for s in parts: + for k in ("노임", "장비"): + for d, q in (s.get(k) or {}).items(): + out[k][d] = out[k].get(d, 0) + q + return out + + +def best(E: dict, pool: list[str], prior: Prior, share: bool) -> list[tuple]: + """[(순위 점수, 맞음, 몫 x, 로직, 입력)] — 순위 차례.""" + out = [] + for c in pool: + f, given, x = fit(E, c, share) + if f <= 0: + continue + out.append((f + prior(c), f, x, c, given)) + out.sort(key=lambda z: -z[0]) + return out + + +def fit(E: dict, code: str, share: bool) -> tuple: + top = (0.0, "{}", 1.0) + for given, O in V.variants(code): + s = V.sim(E, O) + if s > top[0]: + top = (s, given, 1.0) + if share: + for x in V.SHARES: + s2 = 0.95 * V.sim(E, O, x) + if s2 > top[0]: + top = (s2, given, x) + return top + + +def machine_of(E: dict) -> str: + kinds = {k.split("|")[0] for k in E.get("장비") or {}} + return MACHINE["굴착기"] if kinds == {"굴착기"} else MACHINE_ANY + + +def pool_for(E: dict, base: list[str], extra: list[str]) -> list[str]: + rare = set() + for d in V.dims(E): + if d in RARE: + rare |= V.IDX[d] + return list(dict.fromkeys([*base, *extra, *sorted(rare)])) + + +def solve(parts: list[dict], line: dict, got: dict, base: list[str] | None = None) -> dict: + """parts(몫 목록 · 절 표시) → {고름: [(로직, 어디, 맞음, x, 단계)], 통째: …}.""" + base = base if base is not None else merged(line, got) + prior = Prior(line, base) + unit = str(line.get("단위") or "") + if not parts: + return {"고름": [], "단계": "벡터 없음"} + whole = _sum(parts) + share = any("%" in str(p.get("제목")) for p in parts) + titles = list(dict.fromkeys(str(p.get("절") or p.get("제목") or "") for p in parts)) + extra = [c for t in titles for c in title_cands(t, unit, got)] + pool = pool_for(whole, base, extra) + ranked = best(whole, pool, prior, share) + if ranked and ranked[0][1] >= 0.85: + _, f, x, c, given = ranked[0] + return {"고름": [(c, "통째", round(f, 3), x, "①")], "단계": "①"} + chosen = [] + groups: dict[str, list] = {} + for p in parts: + groups.setdefault(str(p.get("절") or p.get("제목") or ""), []).append(p) + for title, ps in groups.items(): + E = _sum(ps) + sh = share or "%" in title or any("%" in str(p.get("제목")) for p in ps) + cand = best(E, pool_for(E, base, title_cands(title, unit, got)), prior, sh) + if cand and cand[0][1] >= 0.75: + _, f, x, c, given = cand[0] + chosen.append((c, title, round(f, 3), x, "②")) + continue + for p in ps: + if p.get("장비") and not p.get("노임"): + chosen.append((machine_of(p), p.get("제목"), 0.0, 1.0, "③장비")) + continue + sh2 = sh or "%" in str(p.get("제목")) + cand = best(p, pool_for(p, base, title_cands(p.get("제목"), unit, got)), prior, sh2) + if cand and cand[0][1] >= 0.6: + _, f, x, c, given = cand[0] + chosen.append((c, p.get("제목"), round(f, 3), x, "③")) + return {"고름": chosen, "단계": "②③"} + + +def codes(result: dict) -> list[str]: + return list(dict.fromkeys(c for c, *_ in result.get("고름") or [])) + + +# ── 1순위 + 설명 안 된 몫(조합 풀이) ────────────────────────────────────── +_SHARE = re.compile(r"(\d{1,3})\s*%") +_RATIO = re.compile(r"(\d)\s*:\s*(\d)") + + +def cover(E: dict, code: str) -> float: + """E 의 차원을 로직 벡터가 얼마나 덮나(로직의 남는 차원은 안 셈) — 갈래 중 가장 좋은 것.""" + top = 0.0 + for _, O in V.variants(code): + num = den = 0.0 + for t, a in (E.get("노임") or {}).items(): + w = 0.5 if t == V.COMMON else 1.0 + b = (O.get("노임") or {}).get(t, 0.0) + den += w + num += w * (min(a, b) / max(a, b) if a > 0 and b > 0 else 0) + for k, a in (E.get("장비") or {}).items(): + kind = k.split("|")[0] + den += 1 + if any(k2.split("|")[0] == kind for k2 in (O.get("장비") or {})): + num += 1 + top = max(top, num / den if den else 0) + return top + + +def is_mixed(parts: list[dict], line: dict) -> bool: + """몫 나눔(기계 90% + 인력 10% · 9 : 1) — 제목 · 이름 · 규격에 몫 표시 + 노임만 든 몫 + 장비만 든 몫.""" + text = " ".join([str(p.get("제목")) for p in parts] + [str(line.get("이름")), str(line.get("규격"))]) + marked = bool(_SHARE.search(text) or _RATIO.search(text)) + man = any(p.get("노임") and not p.get("장비") for p in parts) + mach = any(p.get("장비") and not p.get("노임") for p in parts) + return marked and man and mach + + +def _hint(title: str) -> float | None: + m = _SHARE.search(str(title or "")) + if m and 0 < int(m.group(1)) < 100: + return int(m.group(1)) / 100 + return None + + +def best_hint(E, pool, prior, share, hint): + out = [] + for score, f, x, c, given in best(E, pool, prior, share): + if hint is not None: + score += 0.1 if abs(x - hint) < 1e-9 else -0.1 + out.append((score, f, x, c, given)) + out.sort(key=lambda z: -z[0]) + return out + + +def combine(primary: str | None, parts: list[dict], line: dict, got: dict, base: list[str]) -> list[tuple]: + """[(로직, 어디, 맞음, x, 단계)] — 1순위 + 1순위가 못 덮는 절 · 몫의 로직.""" + chosen = [(primary, "1순위", None, 1.0, "1순위")] if primary else [] + if not parts: + return chosen + prior = Prior(line, base) + unit = str(line.get("단위") or "") + mixed = is_mixed(parts, line) + groups: dict[str, list] = {} + for p in parts: + groups.setdefault(str(p.get("절") or p.get("제목") or ""), []).append(p) + have = {primary} if primary else set() + for title, ps in groups.items(): + E = _sum(ps) + if primary and cover(E, primary) >= 0.6: + continue + if E["장비"] and not E["노임"]: # 장비만 든 절(상차 · 운반 · 기계 몫) = 기계 시간 로직(닫힌 세계 규칙) + if mixed or len(groups) > 1: + m = machine_of(E) + chosen.append((m, title, None, 1.0, "장비 몫")) + have.add(m) + continue + share = any(_hint(p.get("제목")) for p in ps) or _hint(title) is not None + cand = best_hint(E, pool_for(E, base, title_cands(title, unit, got)), prior, share, _hint(title)) + if cand and cand[0][1] >= 0.75 and cand[0][3] not in have: + chosen.append((cand[0][3], title, round(cand[0][1], 3), cand[0][2], "절")) + have.add(cand[0][3]) + continue + for p in ps: + if primary and cover(p, primary) >= 0.6: + continue + if p.get("장비") and not p.get("노임"): + if mixed or len(groups) > 1: + m = machine_of(p) + chosen.append((m, p.get("제목"), None, 1.0, "장비 몫")) + have.add(m) + continue + hint = _hint(p.get("제목")) + cand = best_hint(p, pool_for(p, base, title_cands(p.get("제목"), unit, got)), prior, hint is not None or share, hint) + if cand and cand[0][1] >= 0.6 and cand[0][3] not in have: + chosen.append((cand[0][3], p.get("제목"), round(cand[0][1], 3), cand[0][2], "몫")) + have.add(cand[0][3]) + return chosen diff --git a/docs/raw/조사/2026-10-15_338_실험자료/C5/c06_lines.py b/docs/raw/조사/2026-10-15_338_실험자료/C5/c06_lines.py new file mode 100644 index 000000000..6d9f7905e --- /dev/null +++ b/docs/raw/조사/2026-10-15_338_실험자료/C5/c06_lines.py @@ -0,0 +1,109 @@ +"""req226 C5 — 정답 있는 세 파일 줄에 조합 풀이(c05) 걸기 → 줄마다 고른 로직 모음 · 갈래별 풂(4 번 물음). + +python c06_lines.py → lines_solved.json · 갈래 표 +""" + +from __future__ import annotations + +import json +import sys +from pathlib import Path + +HERE = Path(__file__).resolve().parent +C1 = HERE.parent.parent / "req225" / "C1" +for p in (HERE, C1): + sys.path.insert(0, str(p)) + +import c05_solve as SV # noqa: E402 +import s10_all as S # noqa: E402 +from s04_eval import clause_prefix, hb, hm, patched # noqa: E402 +from s10_all import candidates_nofold # noqa: E402 + +TREES = json.loads((HERE / "trees.json").read_text(encoding="utf-8")) +PARTS = {} +for f in TREES: + for h in f["호표"]: + for one in h["줄"]: + if one.get("id") and one.get("몫") and one["id"] not in PARTS: + PARTS[one["id"]] = one["몫"] + +FINER = ["N003", "N019", "N045", "S055", "S059", "Y010"] # 우리 로직이 더 잘게(철근 가공 + 조립) +COMPOSITE = { # 엑셀 복합 호표 · 복합 줄 — 모두 들어야 풂 + "S062": {"GC000559", "GC000560"}, + "N050": {"GC000705", "GC000714", "GC000712"}, + "S095": {"GF000135", "GF000284"}, + "S097": {"GF000135", "GF000284"}, + **{i: {"GC000206", "GC000207"} for i in FINER}, +} + + +BEST = "+(가) 좋은 것(토질 · 이름 다수 · 조합 안 근거 지킴)" +CLOSED = json.loads((HERE / "closed.json").read_text(encoding="utf-8")) + + +def mixed_ids() -> list[str]: + """섞인 줄 = 몫 표시(10% · 9 : 1) + 노임만 든 몫 + 장비만 든 몫.""" + return sorted(i for i, parts in PARTS.items() if S.LAB.get(i, {}).get("정답") and SV.is_mixed(parts, S.BY[i])) + + +def run(base_name: str = "묶음①", step: str = BEST) -> dict: + out = {} + tops = CLOSED[base_name][step] + with patched(hb, _clause=clause_prefix), patched(hm, candidates=candidates_nofold): + hm._targets.cache_clear() + hb._ours.cache_clear() + got = hm.targets() + for r in S.ALL: + i = r["id"] + line = {k: r.get(k) for k in ("이름", "규격", "수량", "단위", "단가", "갈래", "참조", "근거")} + primary = (tops[i]["top"] or [None])[0] + base = SV.merged(line, got) + chosen = SV.combine(primary, PARTS.get(i) or [], line, got, base) + out[i] = {"고름": chosen, "단계": "조합"} + return out + + +def main(): + res = run() + SV.V.save_cache() + (HERE / "lines_solved.json").write_text(json.dumps(res, ensure_ascii=False, indent=1), "utf-8") + names = {x["로직"]: x["이름"] for x in hm.targets()["로직"]} + + def show(i): + got = SV.codes(res[i]) + lab = S.LAB[i] + print(f" {i} {S.BY[i]['이름'][:20]:<20} 정답 {lab['정답']} 허용 {lab['허용']}") + for c, where, f, x, step in res[i]["고름"]: + print(f" {step} {c} {names.get(c, '')[:22]:<22} 맞음 {f} x {x} ← {str(where)[:30]}") + return got + + print("== 우리 로직이 더 잘게(철근 가공 + 조립)") + ok = sum(1 for i in FINER if COMPOSITE[i] <= set(show(i))) + print(f" 풂 {ok}/{len(FINER)}") + print("== 엑셀 복합 호표 · 복합 줄") + for i in ("S062", "N050", "S095", "S097"): + got = show(i) + print(f" → {'풂' if COMPOSITE[i] <= set(got) else '못 풂'}") + print("== 섞인 줄(기계 몫 + 인력 몫)") + mix = mixed_ids() + man_ok = both = 0 + for i in mix: + got = set(show(i)) + lab = set(S.LAB[i]["정답"]) | set(S.LAB[i]["허용"]) + m = bool(got & lab) + k = bool(got & SV.MACHINE_SET) + man_ok += m + both += m and k + print(f" → 인력 몫(정답 · 허용) {'O' if m else 'X'} · 기계 몫(기계 시간 로직) {'O' if k else 'X'}") + print(f" 섞인 줄 {len(mix)} {mix} · 인력 몫 맞음 {man_ok} · 둘 다 {both}") + # 홑 정답 줄에 덧붙은 로직(잘못 쪼갬) 셈 + single = [i for i in res if S.LAB[i]["정답"] and i not in COMPOSITE and i not in mix] + split = [i for i in single if len(SV.codes(res[i])) > 1] + hit1 = [i for i in single if set(SV.codes(res[i])) & (set(S.LAB[i]["정답"]) | set(S.LAB[i]["허용"]))] + print(f"== 홑 정답 줄 {len(single)} · 둘 넘게 고름 {len(split)} · 고른 것에 정답(느슨) 듦 {len(hit1)} · 벡터 없음 {sum(1 for i in single if not PARTS.get(i))}") + for i in split[:12]: + print(f" {i} {S.BY[i]['이름'][:18]} → {[names.get(c, c)[:12] for c in SV.codes(res[i])]} 정답 {S.LAB[i]['정답']}") + + +if __name__ == "__main__": + main() diff --git a/docs/raw/조사/2026-10-15_338_실험자료/C5/c07_hopyo.py b/docs/raw/조사/2026-10-15_338_실험자료/C5/c07_hopyo.py new file mode 100644 index 000000000..2d3113eb8 --- /dev/null +++ b/docs/raw/조사/2026-10-15_338_실험자료/C5/c07_hopyo.py @@ -0,0 +1,170 @@ +"""req226 C5 — 새 지표 「조합 맞음」(호표 단위 · 정답 있는 세 파일). + +정답 = 앞 정답(labels · labels_y)을 호표로 모음 — 줄마다 칸(slot) 하나(정답 여럿 = 그중 하나) · +철근 가공 + 조립 · 도장 셋 · 잡석 운반 둘 = 모두(칸 여럿) · 정답 없는 줄 = 「없음」 칸(채울 수 없음) · +호표 참조 줄 = 그 호표 칸을 아래에서 위로 더함. +T2(정할 것 8 다른 안) = 섞인 줄에 「기계 몫」 칸(기계 시간 로직) · 철골 = 가공조립 + 용접 두 칸. +우리 = 줄 1순위 모음(지금 · 묶음 ① · (가)) 또는 + 조합 풀이(c06 고른 것). +지표 = 통째 맞음(칸 다 참 · 남는 것 없음) · 칸 맞음 비율 · 고른 것 맞음 비율. + +python c07_hopyo.py → 표 · hopyo_metric.json +""" + +from __future__ import annotations + +import json +import sys +from pathlib import Path + +HERE = Path(__file__).resolve().parent +C1 = HERE.parent.parent / "req225" / "C1" +for p in (HERE, C1): + sys.path.insert(0, str(p)) + +import c05_solve as SV # noqa: E402 +import s10_all as S # noqa: E402 +from c06_lines import COMPOSITE, FINER, PARTS # noqa: E402 + +TREES = json.loads((HERE / "trees.json").read_text(encoding="utf-8")) +CLOSED = json.loads((HERE / "closed.json").read_text(encoding="utf-8")) +SOLVED = json.loads((HERE / "lines_solved.json").read_text(encoding="utf-8")) +LABELED = ("북부청임도", "북부청사방", "동부청영월") +BEST = "+(가) 좋은 것(토질 · 이름 다수 · 조합 안 근거 지킴)" +ALL_OF = {i: sorted(v) for i, v in COMPOSITE.items() if i != "S062"} +MIXED = {i for i, parts in PARTS.items() if S.LAB.get(i, {}).get("정답") and SV.is_mixed(parts, S.BY[i])} + + +def slots_of(i: str, t2: bool) -> list[tuple[set, set]]: + """[(엄격 칸, 느슨 칸)] — 정답 없으면 (set(), set()).""" + lab = S.LAB[i] + loose_extra = set(lab["허용"]) + if not lab["정답"]: + return [(set(), set())] + if i in ALL_OF: + out = [({x}, {x}) for x in ALL_OF[i]] + else: + out = [(set(lab["정답"]), set(lab["정답"]) | loose_extra)] + if t2 and i == "S062": + out.append(({"GC000560"}, {"GC000560"})) + if t2 and i in MIXED: + out.append((set(SV.MACHINE_SET), set(SV.MACHINE_SET))) + return out + + +def hopyo_index(): + out = {} + for f in TREES: + if f["파일"] not in LABELED: + continue + for h in f["호표"]: + ids = list(dict.fromkeys(one["id"] for one in h["줄"] if one.get("id"))) + refs = [one["참조"].get("번") for one in h["줄"] if one["갈래"] == "호표" and one.get("참조")] + out[(f["파일"], h["번"])] = {"ids": ids, "refs": [r for r in refs if r], "이름": h["이름"]} + return out + + +HOPYO = hopyo_index() + + +def gather(key, seen=()) -> list[str]: + """호표 → 로직 줄 id(참조 호표 것까지 · 아래에서 위로).""" + h = HOPYO.get(key) + if not h or key in seen: + return [] + out = list(h["ids"]) + for no in h["refs"]: + out += gather((key[0], no), (*seen, key)) + return list(dict.fromkeys(out)) + + +FINAL = json.loads((HERE / "final.json").read_text(encoding="utf-8")) if (HERE / "final.json").exists() else {} + + +def chosen_of(i: str, pick: str) -> set: + if pick == "조합+뺌": + return set(FINAL["sets"][i]) - {None} + if pick == "조합+뺌-기계": # 정할 것 8 = 인력 몫 하나(기계 몫 로직은 안 넣음) + top = (FINAL["tops"][i]["top"] or [None])[0] + return {c for c in FINAL["sets"][i] if c and (c == top or c not in SV.MACHINE_SET)} + if pick.startswith("조합"): + return set(SV.codes({"고름": SOLVED[i]["고름"]})) - {None} + base, step = pick.split("|") + top = CLOSED[base][step][i]["top"] + return {top[0]} if top else set() + + +def judge(key, pick: str, t2: bool) -> dict | None: + ids = gather(key) + if not ids: + return None + slots, chosen = [], set() + for i in ids: + slots += slots_of(i, t2) + chosen |= chosen_of(i, pick) + out = {} + for mode, k in (("엄격", 0), ("느슨", 1)): + alts = set().union(*[s[k] for s in slots]) + sat = sum(1 for s in slots if s[k] and chosen & s[k]) + extra = chosen - alts + out[mode] = { + "통째": sat == len(slots) and not extra, + "칸": sat / len(slots), + "고름": (len(chosen & alts) / len(chosen)) if chosen else 0, + } + out["없음칸"] = any(not s[0] for s in slots) + out["칸수"] = len(slots) + return out + + +PICKS = { + "지금": "지금|", + "묶음 ①": "묶음①|", + "묶음 ① + (가)": f"묶음①|{BEST}", + "묶음 ① + (가) + 조합 풀이": "조합", + "… + 못 덮는 1순위 뺌(섞인 줄)": "조합+뺌", + "… 기계 몫 안 넣음(T1 쪽)": "조합+뺌-기계", +} + + +def table(t2: bool) -> list: + rows = [] + for label, pick in PICKS.items(): + res = {k: judge(k, pick, t2) for k in HOPYO} + res = {k: v for k, v in res.items() if v} + row = {"우리": label} + for scope, keys in ( + ("전체", list(res)), + ("덮을 수 있음", [k for k, v in res.items() if not v["없음칸"]]), + ): + for mode in ("엄격", "느슨"): + n = len(keys) + row[f"{scope} {mode} 통째"] = round(100 * sum(res[k][mode]["통째"] for k in keys) / n, 1) + row[f"{scope} {mode} 칸"] = round(100 * sum(res[k][mode]["칸"] for k in keys) / n, 1) + row[f"{scope} {mode} 고름"] = round(100 * sum(res[k][mode]["고름"] for k in keys) / n, 1) + row[f"{scope} 호표"] = len(keys) + for f in LABELED: + keys = [k for k in res if k[0] == f and not res[k]["없음칸"]] + row[f"{f} 엄격 통째"] = f"{sum(res[k]['엄격']['통째'] for k in keys)}/{len(keys)}" + rows.append(row) + return rows + + +def main(): + out = {} + for t2 in (False, True): + name = "T2(섞인 줄 기계 몫 · 철골 두 칸)" if t2 else "T1(앞 정답 그대로)" + rows = table(t2) + out[name] = rows + print("==", name) + for r in rows: + print( + f" {r['우리']:<22} 호표 {r['전체 호표']}(덮을 수 있음 {r['덮을 수 있음 호표']}) | " + f"전체 통째 엄격 {r['전체 엄격 통째']} 느슨 {r['전체 느슨 통째']} · 칸 {r['전체 엄격 칸']}/{r['전체 느슨 칸']} · 고름 {r['전체 엄격 고름']}/{r['전체 느슨 고름']} | " + f"덮을 수 있음 통째 {r['덮을 수 있음 엄격 통째']}/{r['덮을 수 있음 느슨 통째']} · 칸 {r['덮을 수 있음 엄격 칸']}/{r['덮을 수 있음 느슨 칸']} | " + f"임도 {r['북부청임도 엄격 통째']} 사방 {r['북부청사방 엄격 통째']} 영월 {r['동부청영월 엄격 통째']}" + ) + (HERE / "hopyo_metric.json").write_text(json.dumps(out, ensure_ascii=False, indent=1), "utf-8") + + +if __name__ == "__main__": + main() diff --git a/docs/raw/조사/2026-10-15_338_실험자료/C5/c08_drop.py b/docs/raw/조사/2026-10-15_338_실험자료/C5/c08_drop.py new file mode 100644 index 000000000..1ccd69132 --- /dev/null +++ b/docs/raw/조사/2026-10-15_338_실험자료/C5/c08_drop.py @@ -0,0 +1,41 @@ +"""조합 풀이 뒤 「줄 벡터를 못 덮는 1순위는 뺌」(닫힌 세계) — 줄 지표 · 호표 지표 다시 셈.""" +import json, sys +from pathlib import Path +import c05_solve as SV +import s10_all as S +from c06_lines import PARTS, CLOSED, BEST + +SOLVED = json.loads((Path(__file__).parent / "lines_solved.json").read_text(encoding="utf-8")) + + +def regroup(cut: float = 0.35) -> tuple[dict, dict]: + """(줄 결과 {id: {상태, top}}, 고른 것 {id: [로직]}).""" + base = CLOSED["묶음①"][BEST] + tops, sets, dropped = {}, {}, [] + for i, got in base.items(): + chosen = [c for c, *_ in SOLVED[i]["고름"] if c] + chosen = list(dict.fromkeys(chosen)) + top = list(got["top"]) + parts = PARTS.get(i) or [] + if top and len(chosen) > 1 and parts and SV.is_mixed(parts, S.BY[i]): # 몫 나눈 줄만 + whole = SV._sum(parts) + if SV.cover(whole, top[0]) < cut: + rest = [c for c in chosen if c != top[0]] + labor = [c for c, where, f, x, step in SOLVED[i]["고름"] if c in rest and step in ("몫", "절")] + if labor: + dropped.append((i, top[0], labor[0])) + chosen = rest + top = [labor[0]] + [c for c in top if c != labor[0]][:4] + tops[i] = {"상태": got["상태"], "top": top} + sets[i] = chosen + return tops, sets, dropped + + +if __name__ == "__main__": + tops, sets, dropped = regroup(0.35) + (Path(__file__).parent / "final.json").write_text(json.dumps({"tops": tops, "sets": sets}, ensure_ascii=False), "utf-8") + for cut in (0.2, 0.35, 0.5): + tops, sets, dropped = regroup(cut) + S.show(S.table(f"(가)+조합 1순위 뺌 {cut}", tops)) + print(" 뺀 줄", [(i, a, b) for i, a, b in dropped]) + S.show(S.table("(가) 그대로", CLOSED["묶음①"][BEST])) diff --git a/docs/raw/조사/2026-10-15_338_실험자료/C5/c09_direct.py b/docs/raw/조사/2026-10-15_338_실험자료/C5/c09_direct.py new file mode 100644 index 000000000..cb5ee303b --- /dev/null +++ b/docs/raw/조사/2026-10-15_338_실험자료/C5/c09_direct.py @@ -0,0 +1,183 @@ +"""req226 C5 — (나) 직접 줄 호표(노임 · 장비 줄이 호표에 바로) → 우리 로직 조합 풀이. + +E = 호표의 노임 · 장비 줄 벡터(참조 줄 뺌). 셋을 견줌: + 이름만 = 호표 머리(이름 · 규격 · 단위)를 지금 맞춤 함수에 줄로 넣음(330 근거 없음) + 이름 + 인용 = STmate 근거글(「건설표준품셈 7-2-2(찰붙임)」)을 근거 출처로 함께 + 벡터 풀이 = c05 조합 풀이(이름 + 인용 후보 · 드문 직종 색인) — 둘 넘게면 음 아닌 최소제곱 짝까지 +판정 = 인용 정답(인용 조항 · 직종이 맞는 우리 로직 · 짐작) · 거창 다섯 사람 판정(짐작). + +python c09_direct.py → 표 · direct.json +""" + +from __future__ import annotations + +import itertools +import json +import sys +from pathlib import Path + +import numpy as np +from scipy.optimize import nnls + +HERE = Path(__file__).resolve().parent +C1 = HERE.parent.parent / "req225" / "C1" +for p in (HERE, C1): + sys.path.insert(0, str(p)) + +import c05_solve as SV # noqa: E402 +from s04_eval import clause_prefix, hb, hm # noqa: E402 + +TREES = json.loads((HERE / "trees.json").read_text(encoding="utf-8")) +HUMAN = { # 사람 판정(짐작) — 거창 직접 줄 호표 · 일위대가조합 UA000014(거창 5 · 6 출처)와 맞음 + ("거창", 1): {"GC000198"}, + ("거창", 2): {"GC000198"}, + ("거창", 3): {"GC000211"}, + ("거창", 10): {"GC000198"}, + ("거창", 11): {"GC000015"}, +} + + +def direct_hopyos(): + for f in TREES: + for h in f["호표"]: + if not any(one["갈래"] in ("노임", "장비") for one in h["줄"]): + continue + E = {"노임": {}, "장비": {}} + for one in h["줄"]: + for k in ("노임", "장비"): + for d, q in (one.get(k) or {}).items(): + if d and q: + E[k][d] = E[k].get(d, 0) + float(q) + cite = " ".join(f"{one['이름']} {one['규격']}" for one in h["줄"] if one["갈래"] == "근거글") + yield f["파일"], h, E, cite + + +def cite_truth(theirs: list, E: dict) -> set: + trades = {t for t in E["노임"] if t != "보통인부"} + out = set() + table = hb.ours() + for one in hm.targets()["로직"]: + our = table.get(one["로직"]) + if not our or clause_prefix(theirs, our)[0] < 0.9: + continue + if trades and not (trades & our["직종"]): + continue + out.add(one["로직"]) + return out + + +def pair_fit(E: dict, pool: list[str]) -> tuple: + """둘 짝 음 아닌 최소제곱 — (맞음, (로직 · x) …).""" + dims = sorted({("노임", d) for d in E["노임"]} | {("장비", d.split("|")[0]) for d in E["장비"]}) + if len(dims) < 2: + return (0.0, ()) + + def col(O): + out = [] + for k, d in dims: + if k == "노임": + out.append((O.get("노임") or {}).get(d, 0.0)) + else: + out.append(sum(v for kk, v in (O.get("장비") or {}).items() if kk.split("|")[0] == d)) + return np.array(out) + + target = col({"노임": E["노임"], "장비": E["장비"]}) + scale = np.where(target > 0, 1 / np.maximum(target, 1e-9), 1.0) + best = (0.0, ()) + ranked = sorted(pool, key=lambda c: -SV.fit(E, c, False)[0])[:6] + vecs = {c: [O for _, O in SV.V.variants(c)][:3] for c in ranked} + for a, b in itertools.combinations(ranked, 2): + for Oa in vecs[a]: + for Ob in vecs[b]: + A = np.stack([col(Oa) * scale, col(Ob) * scale], axis=1) + x, _ = nnls(A, target * scale) + if (x <= 0.05).any(): + continue + mix = {"노임": {}, "장비": {}} + for O, w in ((Oa, x[0]), (Ob, x[1])): + for k in ("노임", "장비"): + for d, q in (O.get(k) or {}).items(): + mix[k][d] = mix[k].get(d, 0) + q * w + s = SV.V.sim(E, mix) + if s > best[0]: + best = (s, ((a, round(x[0], 2)), (b, round(x[1], 2)))) + return best + + +def main(): + got = hm.targets() + names = {x["로직"]: x["이름"] for x in got["로직"]} + rows = [] + for tag, h, E, cite in direct_hopyos(): + head = {"이름": h["이름"], "규격": h["규격"], "단위": h["단위"], "갈래": "단가산출"} + theirs = hb.sources(cite) if cite else [] + trades = sorted(t for t in E["노임"] if t) + with_cite = {**head, "근거": {"출처": theirs, "직종": trades}} if theirs else head + m0 = hm.match_line(head, [], {}, True, got) + m1 = hm.match_line(with_cite, [], {}, True, got) + + def top(m): + if m["상태"] == "같음": + return m["대상"].get("로직") + return next((c.get("로직") for c in m.get("후보") or [] if c.get("로직")), None) + + base = SV.merged(with_cite, got) + sol = SV.solve([{"제목": "", "노임": E["노임"], "장비": E["장비"]}], with_cite, got, base) + chosen = SV.codes(sol) + sol0 = SV.solve([{"제목": "", "노임": E["노임"], "장비": E["장비"]}], head, got, SV.merged(head, got)) + chosen0 = SV.codes(sol0) + f1 = sol["고름"][0][2] if sol["고름"] else 0 + pair = (0.0, ()) + if not chosen or (f1 or 0) < 0.85: + pair = pair_fit(E, SV.pool_for(E, base, [])[:20]) + truth = HUMAN.get((tag, h["번"])) or (cite_truth(theirs, E) if theirs else set()) + rows.append( + { + "파일": tag, + "호표": h["번"], + "이름": f"{h['이름']} {h['규격']}".strip(), + "E": E, + "인용": cite, + "정답": sorted(truth), + "정답출처": "사람" if (tag, h["번"]) in HUMAN else ("인용" if theirs else ""), + "이름만": top(m0), + "이름+인용": top(m1), + "벡터": chosen, + "벡터(인용 없이)": chosen0, + "벡터맞음": f1, + "짝": pair, + } + ) + SV.V.save_cache() + (HERE / "direct.json").write_text(json.dumps(rows, ensure_ascii=False, indent=1, default=str), "utf-8") + judged = [r for r in rows if r["정답"]] + for key in ("이름만", "이름+인용"): + ok = sum(1 for r in judged if r[key] in r["정답"]) + print(f"{key}: {ok}/{len(judged)}") + ok = sum(1 for r in judged if r["벡터"] and r["벡터"][0] in r["정답"]) + print(f"벡터 풀이 1순위: {ok}/{len(judged)}") + import c04_closed as CC # 느슨 = 같은 일 다른 책(정리 이름 글자 묶음 겹침) + + def loose(c, truth): + return bool(c) and (c in truth or any(CC.same_work(c, t) for t in truth)) + + for key in ("이름만", "이름+인용"): + print(f"{key} 느슨: {sum(1 for r in judged if loose(r[key], r['정답']))}/{len(judged)}") + print(f"벡터 느슨: {sum(1 for r in judged if r['벡터'] and loose(r['벡터'][0], r['정답']))}/{len(judged)}") + v0 = [r for r in judged if r["벡터(인용 없이)"]] + print(f"벡터(인용 없이) 엄격: {sum(1 for r in judged if r['벡터(인용 없이)'] and r['벡터(인용 없이)'][0] in r['정답'])}/{len(judged)} · 느슨 {sum(1 for r in v0 if loose(r['벡터(인용 없이)'][0], r['정답']))}/{len(judged)}") + nojudge = [r for r in rows if not r["정답"]] + print(f"정답 없는 직접 줄 호표 {len(nojudge)} — 벡터 고른 것 있음 {sum(1 for r in nojudge if r['벡터'])} · 맞음 0.85 넘음 {sum(1 for r in nojudge if (r['벡터맞음'] or 0) >= 0.85)}") + both = sum(1 for r in judged if (r["벡터"] and r["벡터"][0] in r["정답"]) or r["이름+인용"] in r["정답"]) + print(f"둘 중 하나: {both}/{len(judged)}") + for r in rows: + mark = lambda c: ("O" if c in r["정답"] else "x") if r["정답"] else "?" # noqa: E731 + v = r["벡터"][0] if r["벡터"] else None + print( + f"{r['파일']} {r['호표']} {r['이름'][:22]:<22} [{r['정답출처']}] 이름만 {mark(r['이름만'])} {names.get(r['이름만'], '')[:12]:<12} | " + f"+인용 {mark(r['이름+인용'])} {names.get(r['이름+인용'], '')[:12]:<12} | 벡터 {mark(v)} {names.get(v, '')[:12]:<12} {r['벡터맞음']} | 짝 {r['짝'][1] if r['짝'][0] else ''}" + ) + + +if __name__ == "__main__": + main() diff --git a/docs/raw/조사/2026-10-15_338_실험자료/C5/c10_others.py b/docs/raw/조사/2026-10-15_338_실험자료/C5/c10_others.py new file mode 100644 index 000000000..d652c0517 --- /dev/null +++ b/docs/raw/조사/2026-10-15_338_실험자료/C5/c10_others.py @@ -0,0 +1,206 @@ +"""req226 C5 — 정답 없는 꼴(거창 · STmate 다섯) 호표 조합을 아래에서 위로 풀고, 일위대가조합.json 37 조합의 +「실무 호표 — 봉화 B00005 …」 출처 표시를 사람 판정(짐작)으로 삼아 견줌. + +우리 = (지금) 줄마다 지금 맞춤 1순위 모음(노임 · 장비 줄도 지금처럼 로직 줄로) / + (조합 풀이) 단산 줄 = 묶음 ① 1순위 + 조합 풀이(기계 몫 뺌 · 섞인 줄 1순위 뺌) · + 직접 노임 · 장비 줄 = c05 벡터 풀이(호표 이름 + 인용) · 호표 참조 = 그 호표 풀이(아래에서 위로). +조합 정답 = 그 호표를 출처로 적은 조합의 로직(출처 하나뿐인 조합 = 통째 견줌 · 여럿 = 고른 것 맞음만). + +python c10_others.py [끔] → 표 · others.json +""" + +from __future__ import annotations + +import json +import re +import sys +from pathlib import Path + +HERE = Path(__file__).resolve().parent +C1 = HERE.parent.parent / "req225" / "C1" +for p in (HERE, C1): + sys.path.insert(0, str(p)) + +import c05_solve as SV # noqa: E402 +from c01_trees import ROOT # noqa: E402 +from s04_eval import clause_prefix, hb, hm, patched # noqa: E402 +from s10_all import candidates_nofold, no_seeds # noqa: E402 +from s16_guard import typo_guard # noqa: E402 + +TREES = json.loads((HERE / "trees.json").read_text(encoding="utf-8")) +FILES = ("거창", "봉화", "동부청영월", "울진대흥", "영덕계류", "울진소광") +ALIAS = {"봉화": "봉화", "울진대흥": "울진대흥", "영월": "동부청영월", "계류보전": "영덕계류", "울진소광": "울진소광", "거창": "거창"} +COMBOS = json.loads((ROOT / "resources/master_data/일위대가조합.json").read_text(encoding="utf-8"))["줄"] + + +def combo_truth() -> dict: + """{(파일, 호표 열쇠): [(조합 키, {로직}, 출처 수)]} — 호표 열쇠 = STmate B 코드 · 거창 번.""" + out: dict = {} + for r in COMBOS: + note = str(r.get("비고") or "") + m = re.search(r"실무 호표 — (.+)$", note, re.M) + if not m: + continue + srcs = [s.strip() for s in m.group(1).split("·")] + codes = {x["로직"] for x in r["담은로직"]} + skip = tuple( + re.sub(r"\s+", "", ln.split(" — ")[0]) + for ln in note.splitlines() + if " — " in ln and ("우리 로직에 없음" in ln or "이름이 달라 뺌" in ln) + ) + for s in srcs: + hit = re.match(r"(\S+)\s+(B\d{5}|제(\d+)호표)", s) + if not hit or hit.group(1) not in ALIAS: + continue + key = (ALIAS[hit.group(1)], int(hit.group(3)) if hit.group(3) else hit.group(2)) + out.setdefault(key, []).append((r["키"], codes, len(srcs), skip)) + return out + + +def line_dict(one: dict) -> dict: + return {k: one.get(k) for k in ("이름", "규격", "수량", "단위", "단가", "갈래", "참조", "근거")} + + +def as_logic_line(one: dict) -> dict: + """지금 읽기 갈래로 — 노임 줄 = 기타 · 장비 = 기계 · 단산 = 단가산출.""" + kind = {"단산": "단가산출", "노임": "기타", "장비": "기계"}.get(one["갈래"], one["갈래"]) + return {**line_dict(one), "갈래": kind} + + +def first(m: dict) -> str | None: + if m["상태"] == "같음": + return (m.get("대상") or {}).get("로직") + return next((c.get("로직") for c in m.get("후보") or [] if c.get("로직")), None) + + +class Solver: + def __init__(self, mode: str, got: dict): + self.mode, self.got = mode, got + self.memo: dict = {} + + def hopyo_set(self, f: dict, no, trail=(), skip: tuple = ()) -> set: + """skip = 사람이 조합에서 뺀 줄 글(「우리 로직에 없음」 · 「이름이 달라 뺌」) — 그 줄은 안 셈.""" + key = (f["파일"], no, skip) + if key in self.memo: + return self.memo[key] + h = next((x for x in f["호표"] if x["번"] == no), None) + if h is None or key in trail: + return set() + out: set = set() + direct = {"노임": {}, "장비": {}} + flat = lambda t: re.sub(r"\s+", "", str(t)) # noqa: E731 + for one in h["줄"]: + kind = one["갈래"] + text = flat(f"{one['이름']}{one['규격']}") + if skip and any(text and (text in x or x in text) for x in skip): + continue + if kind == "호표": + out |= self.hopyo_set(f, (one.get("참조") or {}).get("번"), (*trail, key), skip) + elif kind in ("단산", "노임", "장비") and self.mode == "지금": + t = first(hm.match_line(as_logic_line(one), [], {}, True, self.got)) + if t: + out.add(t) + elif kind == "단산": + line = as_logic_line(one) + primary = typo_guard(line, self.got, {"단산글": one.get("단산글") or []})["top"] + primary = primary[0] if primary else None + parts = one.get("몫") or [] + chosen = SV.combine(primary, parts, line, self.got, SV.merged(line, self.got)) + codes = [c for c, *_ in chosen if c and c not in SV.MACHINE_SET] + if parts and SV.is_mixed(parts, line) and primary and SV.cover(SV._sum(parts), primary) < 0.35: + rest = [c for c, w, ff, x, st in chosen if c != primary and st in ("몫", "절")] + if rest: + codes = [c for c in codes if c != primary] + out |= set(codes) + elif kind in ("노임", "장비"): + for k in ("노임", "장비"): + for d, q in (one.get(k) or {}).items(): + if d and q: + direct[k][d] = direct[k].get(d, 0) + float(q) + if self.mode != "지금" and (direct["노임"] or direct["장비"]): + cite = " ".join(f"{o['이름']} {o['규격']}" for o in h["줄"] if o["갈래"] == "근거글") + head = {"이름": h["이름"], "규격": h["규격"], "단위": h["단위"], "갈래": "단가산출"} + theirs = hb.sources(cite) if cite else [] + if theirs: + head["근거"] = {"출처": theirs, "직종": sorted(direct["노임"])} + sol = SV.solve([{"제목": "", **direct}], head, self.got, SV.merged(head, self.got)) + out |= {c for c in SV.codes(sol) if c not in SV.MACHINE_SET} + self.memo[key] = out + return out + + +def run(mode: str) -> list: + truth = combo_truth() + rows = [] + with patched(hb, _clause=clause_prefix), patched(hm, candidates=candidates_nofold): + hm._targets.cache_clear() + hb._ours.cache_clear() + got = hm.targets() + for f in TREES: + if f["파일"] not in FILES: + continue + sv = Solver(mode, got) + for h in f["호표"]: + hkey = h["번"] if f["파일"] == "거창" else h.get("코드") + for ck, codes, nsrc, skip in truth.get((f["파일"], hkey), []): + ours = sv.hopyo_set(f, h["번"]) + kept = sv.hopyo_set(f, h["번"], (), skip) + rows.append( + { + "파일": f["파일"], + "호표": h["번"], + "이름": f"{h['이름']} {h['규격']}".strip(), + "조합": ck, + "출처수": nsrc, + "정답": sorted(codes), + "우리": sorted(ours), + "우리(사람이 뺀 줄 뺌)": sorted(kept), + } + ) + return rows + + +def report(rows: list, title: str) -> dict: + one = [r for r in rows if r["출처수"] == 1] + many = [r for r in rows if r["출처수"] > 1] + + def stats(rs, exact: bool, col: str = "우리"): + if not rs: + return "0" + ex = sum(1 for r in rs if set(r[col]) == set(r["정답"])) + rec = sum(len(set(r[col]) & set(r["정답"])) / len(r["정답"]) for r in rs) / len(rs) + pre = sum((len(set(r[col]) & set(r["정답"])) / len(r[col])) if r[col] else 0 for r in rs) / len(rs) + head = f"통째 {ex}/{len(rs)} · " if exact else "" + return f"{head}조합 로직 찾음 {round(100 * rec, 1)}% · 고른 것 맞음 {round(100 * pre, 1)}%" + + print(f"== {title}") + print(f" 출처 하나 조합(호표 {len(one)}): {stats(one, True)}") + print(f" └ 사람이 뺀 줄 빼고: {stats(one, True, '우리(사람이 뺀 줄 뺌)')}") + print(f" 출처 여럿 조합(호표 {len(many)}): {stats(many, False)}") + print(f" └ 사람이 뺀 줄 빼고: {stats(many, False, '우리(사람이 뺀 줄 뺌)')}") + return {"하나": stats(one, True), "하나(뺀 줄 뺌)": stats(one, True, "우리(사람이 뺀 줄 뺌)"), "여럿": stats(many, False), "여럿(뺀 줄 뺌)": stats(many, False, "우리(사람이 뺀 줄 뺌)")} + + +def main(): + seeds_off = sys.argv[1:] == ["끔"] + out = {} + for mode in ("지금", "조합 풀이"): + if seeds_off: + with no_seeds(): + rows = run(mode) + else: + rows = run(mode) + out[mode] = {"줄": rows, "표": report(rows, f"{mode}{' (씨앗 끔)' if seeds_off else ''}")} + SV.V.save_cache() + name = "others_off.json" if seeds_off else "others.json" + (HERE / name).write_text(json.dumps(out, ensure_ascii=False, indent=1), "utf-8") + names = {x["로직"]: x["이름"] for x in hm.targets()["로직"]} + for r in out["조합 풀이"]["줄"]: + if r["출처수"] == 1: + miss = [names.get(c, c)[:10] for c in set(r["정답"]) - set(r["우리(사람이 뺀 줄 뺌)"])] + extra = [names.get(c, c)[:10] for c in set(r["우리(사람이 뺀 줄 뺌)"]) - set(r["정답"])] + print(f" {r['파일']} {r['호표']} {r['이름'][:16]:<16} {r['조합']} 빠짐 {miss} 남음 {extra}") + + +if __name__ == "__main__": + main() diff --git a/docs/raw/조사/2026-10-15_338_실험자료/C5/t_ablate.py b/docs/raw/조사/2026-10-15_338_실험자료/C5/t_ablate.py new file mode 100644 index 000000000..b5229b334 --- /dev/null +++ b/docs/raw/조사/2026-10-15_338_실험자료/C5/t_ablate.py @@ -0,0 +1,19 @@ +import c07_hopyo as H +from c07_hopyo import SV, S, FINAL, CLOSED, BEST, SOLVED + +def sets_B(i): # (가) 1순위 + 조합 덧(기계 몫 뺌) · 뺌 없음 + top = (CLOSED["묶음①"][BEST][i]["top"] or [None])[0] + extra = {c for c, *_ in SOLVED[i]["고름"] if c and c not in SV.MACHINE_SET} + return ({top} | extra) - {None} + +def sets_C(i): # 뺌만(섞인 줄 1순위 바꿈) · 덧 없음 + top = (FINAL["tops"][i]["top"] or [None])[0] + return {top} - {None} + +for name, fn in (("B 덧만", sets_B), ("C 뺌만", sets_C)): + H.chosen_of = lambda i, pick, fn=fn: fn(i) + res = {k: H.judge(k, "x", False) for k in H.HOPYO} + res = {k: v for k, v in res.items() if v} + keys = [k for k, v in res.items() if not v["없음칸"]] + ex = sum(res[k]["엄격"]["통째"] for k in keys) + print(name, f"덮을 수 있음 통째 엄격 {ex}/{len(keys)} = {round(100*ex/len(keys),1)}", "칸", round(100*sum(res[k]["엄격"]["칸"] for k in keys)/len(keys),1)) diff --git a/docs/raw/조사/2026-10-15_338_실험자료/C5/t_fail.py b/docs/raw/조사/2026-10-15_338_실험자료/C5/t_fail.py new file mode 100644 index 000000000..97c23dc7b --- /dev/null +++ b/docs/raw/조사/2026-10-15_338_실험자료/C5/t_fail.py @@ -0,0 +1,26 @@ +import sys, collections +from c07_hopyo import * +from M02_MasterTemplete import M02_Hopyo_Match as hm +names = {x["로직"]: x["이름"] for x in hm.targets()["로직"]} +pick = sys.argv[1] if len(sys.argv) > 1 else "조합" +t2 = len(sys.argv) > 2 +miss_c, extra_c = collections.Counter(), collections.Counter() +n = 0 +for key in HOPYO: + ids = gather(key) + if not ids: continue + r = judge(key, pick, t2) + if r["없음칸"] or r["엄격"]["통째"]: continue + n += 1 + slots, chosen = [], set() + for i in ids: + for s in slots_of(i, t2): slots.append((i, s)) + chosen |= chosen_of(i, pick) + alts = set().union(*[s[0] for _, s in slots]) + miss = [(i, sorted(s[0])) for i, s in slots if not chosen & s[0]] + extra = chosen - alts + for i, _ in miss: miss_c[i] += 1 + for e in extra: extra_c[e] += 1 +print("실패 호표", n) +print("빠진 칸(줄 id) 많은 차례", [(i, S.BY[i]["이름"][:12], c) for i, c in miss_c.most_common(20)]) +print("남는 것 많은 차례", [(e, names.get(e, "")[:14], c) for e, c in extra_c.most_common(20)]) diff --git a/docs/raw/조사/2026-10-15_338_실험자료/C5/t_fallback.py b/docs/raw/조사/2026-10-15_338_실험자료/C5/t_fallback.py new file mode 100644 index 000000000..d62fcd02b --- /dev/null +++ b/docs/raw/조사/2026-10-15_338_실험자료/C5/t_fallback.py @@ -0,0 +1,15 @@ +"""품 식 풀이 실패(금액 ÷ 노임으로 되찾음) 셈 — 북부청 · 거창 블록.""" +import c01_trees as T +cnt = {"식": 0, "되찾음": 0} +orig = T.qty_of +def wrapped(expr): + v = orig(expr) + cnt["식" if v is not None else "되찾음"] += 1 + return v +T.qty_of = wrapped +ids = T._ids_north() +for t, p in T.NORTH.items(): + T.read_excel(t, p, ids, False) +for t, p in T.GEO.items(): + T.read_excel(t, p, {}, True) +print(cnt) diff --git a/docs/raw/조사/2026-10-15_338_실험자료/C5/t_off.py b/docs/raw/조사/2026-10-15_338_실험자료/C5/t_off.py new file mode 100644 index 000000000..9d75387c9 --- /dev/null +++ b/docs/raw/조사/2026-10-15_338_실험자료/C5/t_off.py @@ -0,0 +1,33 @@ +"""씨앗 끔 바탕으로 같은 셈(줄 지표 · 호표 조합 지표) — 저장 파일 안 덮음.""" +import json +import c06_lines as L6 +import c07_hopyo as H +from c07_hopyo import SV, S +from c06_lines import CLOSED, BEST, PARTS + +off = L6.run("묶음①(씨앗 끔)", BEST) +base = CLOSED["묶음①(씨앗 끔)"][BEST] +tops, sets = {}, {} +for i, got in base.items(): + chosen = list(dict.fromkeys(c for c, *_ in off[i]["고름"] if c)) + top = list(got["top"]) + parts = PARTS.get(i) or [] + if top and len(chosen) > 1 and parts and SV.is_mixed(parts, S.BY[i]) and SV.cover(SV._sum(parts), top[0]) < 0.35: + labor = [c for c, w, f, x, st in off[i]["고름"] if c != top[0] and st in ("몫", "절")] + if labor: + chosen = [c for c in chosen if c != top[0]] + top = [labor[0]] + [c for c in top if c != labor[0]][:4] + tops[i] = {"상태": got["상태"], "top": top} + sets[i] = chosen +S.show(S.table("씨앗 끔 (가)", base)) +S.show(S.table("씨앗 끔 (가)+조합 뺌", tops)) +for name, fn in ( + ("(가)", lambda i: {(base[i]["top"] or [None])[0]} - {None}), + ("(가)+조합 풀이+뺌-기계", lambda i: {c for c in sets[i] if c and (c == (tops[i]["top"] or [None])[0] or c not in SV.MACHINE_SET)}), +): + H.chosen_of = lambda i, pick, fn=fn: fn(i) + res = {k: H.judge(k, "x", False) for k in H.HOPYO} + res = {k: v for k, v in res.items() if v} + keys = [k for k, v in res.items() if not v["없음칸"]] + ex = sum(res[k]["엄격"]["통째"] for k in keys) + print(name, f"T1 덮을 수 있음 통째 엄격 {ex}/{len(keys)} = {round(100*ex/len(keys),1)} · 칸 {round(100*sum(res[k]['엄격']['칸'] for k in keys)/len(keys),1)}") diff --git a/docs/raw/조사/2026-10-15_338_실험자료/C5/t_prof.py b/docs/raw/조사/2026-10-15_338_실험자료/C5/t_prof.py new file mode 100644 index 000000000..070685244 --- /dev/null +++ b/docs/raw/조사/2026-10-15_338_실험자료/C5/t_prof.py @@ -0,0 +1,18 @@ +import time, itertools +import c09_direct as D +from c09_direct import SV, hm, hb +got = hm.targets() +t = time.time() +for k, (tag, h, E, cite) in enumerate(D.direct_hopyos()): + if time.time() - t > 90: break + t0 = time.time() + head = {"이름": h["이름"], "규격": h["규격"], "단위": h["단위"], "갈래": "단가산출"} + theirs = hb.sources(cite) if cite else [] + with_cite = {**head, "근거": {"출처": theirs, "직종": sorted(E["노임"])}} if theirs else head + m0 = hm.match_line(head, [], {}, True, got); t1 = time.time() + base = SV.merged(with_cite, got); t2 = time.time() + sol = SV.solve([{"제목": "", "노임": E["노임"], "장비": E["장비"]}], with_cite, got, base); t3 = time.time() + pool = SV.pool_for(E, base, []) + pr = D.pair_fit(E, pool[:20]); t4 = time.time() + tr = D.cite_truth(theirs, E) if theirs else set(); t5 = time.time() + print(tag, h["번"], "match", round(t1-t0,2), "merged", round(t2-t1,2), "solve", round(t3-t2,2), "pool", len(pool), "pair", round(t4-t3,2), "truth", round(t5-t4,2)) diff --git a/docs/raw/조사/2026-10-15_338_실험자료/C5/t_rows.py b/docs/raw/조사/2026-10-15_338_실험자료/C5/t_rows.py new file mode 100644 index 000000000..582d272aa --- /dev/null +++ b/docs/raw/조사/2026-10-15_338_실험자료/C5/t_rows.py @@ -0,0 +1,7 @@ +import json +from c06_lines import CLOSED, BEST +import s10_all as S +F = json.load(open("final.json", encoding="utf-8")) +for name, res in (("지금", CLOSED["지금"][""]), ("묶음①", CLOSED["묶음①"][""]), ("(가)", CLOSED["묶음①"][BEST]), ("(가)+조합 뺌", F["tops"])): + t = S.table(name, res)["전체"] + print(name, "고유", t["1위 엄격"], t["1위 느슨"], t["5안 엄격"], t["5안 느슨"], "| 엑셀 줄 무게 1위 느슨", t["줄 1위 느슨"], "5안 느슨", t["줄 5안 느슨"]) diff --git a/docs/raw/조사/2026-10-15_338_실험자료/C5/t_stats.py b/docs/raw/조사/2026-10-15_338_실험자료/C5/t_stats.py new file mode 100644 index 000000000..744dfe1a8 --- /dev/null +++ b/docs/raw/조사/2026-10-15_338_실험자료/C5/t_stats.py @@ -0,0 +1,18 @@ +import json +from c06_lines import PARTS, FINER, COMPOSITE +import s10_all as S +import c05_solve as SV +t = json.load(open("trees.json", encoding="utf-8")) +for f in t: + d = [l for h in f["호표"] for l in h["줄"] if l["갈래"] == "단산"] + src = sum(1 for l in d if (l.get("근거") or {}).get("출처")) + trd = sum(1 for l in d if (l.get("근거") or {}).get("직종")) + vec = sum(1 for l in d if l.get("몫")) + multi = sum(1 for l in d if len({p.get("절") or p.get("제목") for p in l.get("몫") or []}) > 1) + print(f["파일"], "단산 줄", len(d), "출처", src, "직종", trd, "몫 벡터", vec, "절 둘 넘음", multi) +rows = lambda ids: sum(S.BY[i]["줄수"] for i in ids) +mixed = sorted(i for i, p in PARTS.items() if S.LAB.get(i, {}).get("정답") and SV.is_mixed(p, S.BY[i])) +print("철근 줄", FINER, rows(FINER), "섞인", mixed, rows(mixed)) +F = json.load(open("final.json", encoding="utf-8")) +ok = [i for i in FINER if COMPOSITE[i] <= set(F["sets"][i])] +print("철근 풂", ok, rows(ok)) diff --git a/docs/raw/조사/2026-10-15_338_실험자료/C5/t_vec.py b/docs/raw/조사/2026-10-15_338_실험자료/C5/t_vec.py new file mode 100644 index 000000000..9cda1984c --- /dev/null +++ b/docs/raw/조사/2026-10-15_338_실험자료/C5/t_vec.py @@ -0,0 +1,23 @@ +import json, sys, time +import c03_vec as V +import c01_trees # noqa: F401 +from c01_trees import ROOT +from M02_MasterTemplete import M02_Hopyo_Match as hm +t=json.load(open('trees.json',encoding='utf-8')) +names={x['로직']:x['이름'] for x in hm.targets()['로직']} +want={'N000','N003','N005','S062','S066','N039','N008','N047'} +t0=time.time() +for f in t: + for h in f['호표']: + for l in h['줄']: + if l.get('id') in want: + want.discard(l['id']) + print('##', l['id'], l['이름'], l['규격']) + for s in l.get('몫') or []: + pool=set().union(*[V.IDX.get(d,set()) for d in V.dims(s)]) if V.dims(s) else set() + sc=sorted(((V.fit(s,c),c) for c in pool), key=lambda z:-z[0][0])[:4] + print(' 몫', s['제목'][:20], {k:round(v,3) for k,v in s['노임'].items()}, {k:round(v,3) for k,v in s['장비'].items()}, 'pool', len(pool)) + for (sc_,g,x),c in sc: + print(' ', round(sc_,3), c, names.get(c), g[:60], x) +V.save_cache() +print('sec', round(time.time()-t0,1)) diff --git a/docs/raw/조사/2026-10-15_338_실험자료/C5/x00_sheets.py b/docs/raw/조사/2026-10-15_338_실험자료/C5/x00_sheets.py new file mode 100644 index 000000000..a0a87fed6 --- /dev/null +++ b/docs/raw/조사/2026-10-15_338_실험자료/C5/x00_sheets.py @@ -0,0 +1,26 @@ +"""시트 이름 · 크기만 훑음(읽기만).""" +import sys, warnings +from pathlib import Path +import openpyxl +warnings.filterwarnings("ignore") +ROOT = Path(r"C:\Program_coding\임도설계 및 견적자동화 프로그램 개발\resources\knowledge\original\실무문서") +FILES = [ + "01.산불거창(거창북상병곡)/01.내역서/00.25년_산불_거창(거창,북상,병곡,산8외)내역+수량.xlsx", + "04.산불 봉화/01. 내역서/2024년 산불진화임도 실시설계(기번41_현동)_ L=3.46km.xlsx", + "05. 동부청 영월/내역서/2024년 임도시설사업2차(산불진화_기번6)-영월.xlsx", + "2024년 간선임도(기번3-울진.대흥)(공통)/2024년 간선임도 신설사업(기번3 울진.울진.대흥.산65외2).xlsx", + "20250527 2025년 계류보전사업(기번1-영덕.병곡.영리.산214-1)(변경)/01.내역자료/2025년 계류보전사업(기번1 영덕.병곡.영.산214)(변경).xlsx", + "25년 산불진화임도(기번8)(울진소광)/2025년 산불진화임도 신설사업(기번8 울진.금강송.소광.산29외).xlsx", +] +if __name__ == "__main__": + for f in FILES: + p = ROOT / f + head = p.read_bytes()[:8] + print("##", f, head[:4]) + try: + b = openpyxl.load_workbook(p, read_only=True, data_only=True) + except Exception as e: + print(" fail", type(e).__name__, str(e)[:80]); continue + for ws in b.worksheets: + print(" ", repr(ws.title), ws.max_row, ws.max_column, ws.sheet_state) + b.close() diff --git a/docs/raw/조사/2026-10-15_338_실험자료/C5/x01_peek.py b/docs/raw/조사/2026-10-15_338_실험자료/C5/x01_peek.py new file mode 100644 index 000000000..6f74e6524 --- /dev/null +++ b/docs/raw/조사/2026-10-15_338_실험자료/C5/x01_peek.py @@ -0,0 +1,14 @@ +"""시트 앞 몇 줄 보기(읽기만) — python x01_peek.py <파일 번호> <시트> <시작> <끝>""" +import sys, warnings +from pathlib import Path +import openpyxl +warnings.filterwarnings("ignore") +from x00_sheets import FILES, ROOT +i, sheet, a, b = int(sys.argv[1]), sys.argv[2], int(sys.argv[3]), int(sys.argv[4]) +book = openpyxl.load_workbook(ROOT / FILES[i], read_only=True, data_only=True) +ws = book[sheet] +for at, row in enumerate(ws.iter_rows(values_only=True), 1): + if at < a: continue + if at > b: break + cells = [(c, v) for c, v in enumerate(row) if v not in (None, "")] + print(at, " | ".join(f"{c}:{str(v)[:40]}" for c, v in cells)) diff --git a/docs/raw/조사/2026-10-15_338_실험자료/C5/x02_codes.py b/docs/raw/조사/2026-10-15_338_실험자료/C5/x02_codes.py new file mode 100644 index 000000000..dfe4070fc --- /dev/null +++ b/docs/raw/조사/2026-10-15_338_실험자료/C5/x02_codes.py @@ -0,0 +1,21 @@ +"""STmate 일위대가표 줄 코드 갈래 셈(읽기만).""" +import re, warnings, collections +import openpyxl +from x00_sheets import FILES, ROOT +warnings.filterwarnings("ignore") +for i in range(1, 6): + book = openpyxl.load_workbook(ROOT / FILES[i], read_only=True, data_only=True) + rows = [list(r) for r in book["일위대가표"].iter_rows(values_only=True)] + book.close() + c = collections.Counter(); ex = {} + for r in rows: + code = str(r[12] or "").strip() if len(r) > 12 else "" + if not code: continue + m = re.match(r"^(별산)?([A-Z])\d", code) + k = (m.group(1) or "") + m.group(2) if m else "?" + code[:6] + c[k] += 1 + ex.setdefault(k, []).append((str(r[0])[:20], str(r[1])[:20], r[3], code)) + print(FILES[i][:30], dict(c)) + for k, v in ex.items(): + if k[-1] in "B%S?" or k.startswith("?"): + print(" ", k, v[:4]) diff --git a/docs/raw/조사/2026-10-15_338_실험자료/C6/s00_sheets.py b/docs/raw/조사/2026-10-15_338_실험자료/C6/s00_sheets.py new file mode 100644 index 000000000..de333442e --- /dev/null +++ b/docs/raw/조사/2026-10-15_338_실험자료/C6/s00_sheets.py @@ -0,0 +1,20 @@ +"""C6 — 실무 xlsx 시트 이름 · 크기 훑기(읽기만).""" +import sys, warnings, time +from pathlib import Path +import openpyxl +warnings.filterwarnings("ignore") +ROOT = Path(r"C:\Program_coding\임도설계 및 견적자동화 프로그램 개발") +BASE = ROOT / "resources/knowledge/original/실무문서" +for p in sorted(BASE.rglob("*.xlsx")): + if "수량" in p.name and "내역" not in p.name: + continue + t = time.time() + head = p.read_bytes()[:8] + try: + wb = openpyxl.load_workbook(p, read_only=True, data_only=True) + info = [(ws.title, ws.max_row, ws.max_column) for ws in wb.worksheets] + wb.close() + except Exception as e: + info = f"ERR {type(e).__name__}: {e} head={head!r}" + print(p.relative_to(BASE), round(time.time()-t,1)) + print(" ", info) diff --git a/docs/raw/조사/2026-10-15_338_실험자료/C6/s01_peek.py b/docs/raw/조사/2026-10-15_338_실험자료/C6/s01_peek.py new file mode 100644 index 000000000..7ad79db5c --- /dev/null +++ b/docs/raw/조사/2026-10-15_338_실험자료/C6/s01_peek.py @@ -0,0 +1,24 @@ +"""C6 — STmate 꼴 시트 줄 꼴 엿보기(읽기만). python s01_peek.py <파일번호> <시트> <시작> <끝>""" +import sys, warnings +from pathlib import Path +import openpyxl +warnings.filterwarnings("ignore") +ROOT = Path(r"C:\Program_coding\임도설계 및 견적자동화 프로그램 개발") +B = ROOT / "resources/knowledge/original/실무문서" +FILES = { + "봉화": B / "04.산불 봉화/01. 내역서/2024년 산불진화임도 실시설계(기번41_현동)_ L=3.46km.xlsx", + "영월": B / "05. 동부청 영월/내역서/2024년 임도시설사업2차(산불진화_기번6)-영월.xlsx", + "대흥설계": B / "2024년 간선임도(기번3-울진.대흥)(공통)/1. 설계원본/01 내역자료/2024년 간선임도사업(기번3).xlsx", + "대흥착공": B / "2024년 간선임도(기번3-울진.대흥)(공통)/2024년 간선임도 신설사업(기번3 울진.울진.대흥.산65외2).xlsx", + "계류보전": B / "20250527 2025년 계류보전사업(기번1-영덕.병곡.영리.산214-1)(변경)/01.내역자료/2025년 계류보전사업(기번1 영덕.병곡.영.산214)(변경).xlsx", + "소광": B / "25년 산불진화임도(기번8)(울진소광)/2025년 산불진화임도 신설사업(기번8 울진.금강송.소광.산29외).xlsx", + "거창": B / "01.산불거창(거창북상병곡)/01.내역서/00.25년_산불_거창(거창,북상,병곡,산8외)내역+수량.xlsx", +} +if __name__ == "__main__": + tag, sheet, a, b = sys.argv[1], sys.argv[2], int(sys.argv[3]), int(sys.argv[4]) + wb = openpyxl.load_workbook(FILES[tag], read_only=True, data_only=True) + for i, r in enumerate(wb[sheet].iter_rows(values_only=True), 1): + if i < a: continue + if i > b: break + cells = [(j, v) for j, v in enumerate(r) if v not in (None, "")] + print(i, cells) diff --git a/docs/raw/조사/2026-10-15_338_실험자료/C6/s02_read.py b/docs/raw/조사/2026-10-15_338_실험자료/C6/s02_read.py new file mode 100644 index 000000000..640116251 --- /dev/null +++ b/docs/raw/조사/2026-10-15_338_실험자료/C6/s02_read.py @@ -0,0 +1,210 @@ +"""C6 — 실무 내역 넓히기: STmate 꼴 6 파일 + 거창(엑셀꼴) 읽기 → data_<태그>.json (읽기만). + +STmate: 일위대가표(호표 · 줄 코드 D L M X B S W) + 단가산출근거(블록 = BDCOD|D 코드 · 자원 줄 = 12열 「L00002_1」 · +7열 = 단위당 품(계수) · 23열 = 「이름 / 규격」) + 노무비목록표(L 코드 → 직종 이름 · 23열과 맞대 봄). +거창: 저장소 읽기 함수(hx.read_book · hb 블록) 그대로 — 「단산N참조」 줄만 블록이 있음. +북부청 두 파일은 C1 결과(out_items · out_lines)를 씀(정답 L 과 같은 id). + +python s02_read.py → 파일마다 호표 · 줄 · 블록 수 표 + data_<태그>.json +""" + +import json +import re +import sys +import warnings +from pathlib import Path + +import openpyxl + +ROOT = Path(r"C:\Program_coding\임도설계 및 견적자동화 프로그램 개발") +sys.path.insert(0, str(ROOT)) +HERE = Path(__file__).resolve().parent +sys.stdout.reconfigure(encoding="utf-8") +warnings.filterwarnings("ignore") + +from M02_MasterTemplete import M02_Hopyo_Basis as hb # noqa: E402 +from M02_MasterTemplete import M02_Hopyo_Excel as hx # noqa: E402 + +B = ROOT / "resources/knowledge/original/실무문서" +STMATE = { + "봉화": B / "04.산불 봉화/01. 내역서/2024년 산불진화임도 실시설계(기번41_현동)_ L=3.46km.xlsx", + "영월": B / "05. 동부청 영월/내역서/2024년 임도시설사업2차(산불진화_기번6)-영월.xlsx", + "대흥설계": B / "2024년 간선임도(기번3-울진.대흥)(공통)/1. 설계원본/01 내역자료/2024년 간선임도사업(기번3).xlsx", + "대흥착공": B / "2024년 간선임도(기번3-울진.대흥)(공통)/2024년 간선임도 신설사업(기번3 울진.울진.대흥.산65외2).xlsx", + "계류보전": B / "20250527 2025년 계류보전사업(기번1-영덕.병곡.영리.산214-1)(변경)/01.내역자료/2025년 계류보전사업(기번1 영덕.병곡.영.산214)(변경).xlsx", + "소광": B / "25년 산불진화임도(기번8)(울진소광)/2025년 산불진화임도 신설사업(기번8 울진.금강송.소광.산29외).xlsx", +} +EXCEL = {"거창": B / "01.산불거창(거창북상병곡)/01.내역서/00.25년_산불_거창(거창,북상,병곡,산8외)내역+수량.xlsx"} +KIND = {"D": "단가산출", "X": "기계", "L": "노임", "M": "자재", "B": "호표", "S": "별도계상", "W": "일식"} +_HOPYO = re.compile(r"제\s*(\d+)\s*호표") +_LINE = re.compile(r"^([A-Z])(\d{5})_\d+$") +_COD = re.compile(r"COD\|([A-Z]\d{5})") +_BLOCK = re.compile(r"BDCOD\|(D\d{5})") +_HEADB = re.compile(r"BDCOD\|(B\d{5})") + + +def _num(v): + try: + return float(str(v).replace(",", "")) + except (TypeError, ValueError): + return None + + +def _s(v): + return str(v if v is not None else "").strip() + + +def _cell(r, i): + return r[i] if i < len(r) else None + + +def read_stmate(tag, path): + book = openpyxl.load_workbook(path, read_only=True, data_only=True) + rows = [list(r) for r in book["일위대가표"].iter_rows(values_only=True)] + calc = [list(r) for r in book["단가산출근거"].iter_rows(values_only=True)] + labor_rows = [list(r) for r in book["노무비목록표"].iter_rows(values_only=True)] + list_rows = [list(r) for r in book["단가산출근거목록표"].iter_rows(values_only=True)] + book.close() + # 단가산출근거목록표 — D 코드 → 이름 · 규격 · 단위(블록 머리 글은 이름 · 규격이 붙어 있음) + heads = {} + for r in list_rows: + hit = re.search(r"COD\|(D\d{5})", " ".join(_s(v) for v in r)) + if hit and _s(_cell(r, 1)): + heads[hit.group(1)] = {"이름": _s(_cell(r, 1)), "규격": _s(_cell(r, 2)), "단위": _s(_cell(r, 3))} + # 노무비목록표 — L 코드 → 이름(열 7 코드 · 열 1 이름) + labor = {} + for r in labor_rows: + hit, name = re.search(r"COD\|(L\d{5})", " ".join(_s(v) for v in r)), _s(_cell(r, 1)) + if hit and name: + labor[hit.group(1)] = name + # 일위대가표 + hopyos, cur, need_head = [], None, False + for r in rows: + first = _s(_cell(r, 0)) + hit = _HOPYO.match(first) + if hit: + bcode = _HEADB.search(_s(_cell(r, 13))) + cur = {"번": int(hit.group(1)), "코드": bcode.group(1) if bcode else None, "줄": []} + hopyos.append(cur) + need_head = True + continue + if cur is None: + continue + if need_head and first: + cur.update(이름=first, 규격=_s(_cell(r, 1)), 단위=_s(_cell(r, 3))) + need_head = False + continue + if first.replace(" ", "") == "합계": + cur = None + continue + m = _LINE.match(_s(_cell(r, 14))) or _COD.search(_s(_cell(r, 13))) + if not m or not first: + continue + code = m.group(1) + m.group(2) if m.re is _LINE else m.group(1) + cur["줄"].append( + { + "차례": len(cur["줄"]) + 1, + "이름": first, + "규격": _s(_cell(r, 1)), + "수량": _num(_cell(r, 2)), + "단위": _s(_cell(r, 3)), + "단가": {"계": _num(_cell(r, 4)), "노무비": _num(_cell(r, 6)), "재료비": _num(_cell(r, 8)), "경비": _num(_cell(r, 10))}, + "코드": code, + "갈래": KIND.get(code[0], "기타"), + } + ) + # 단가산출근거 블록 + blocks, blk, mismatch = {}, None, 0 + for at, r in enumerate(calc, 1): + joined = " ".join(_s(v) for v in r[:8]) + bm = _BLOCK.search(joined) + if bm: + blk = blocks.setdefault(bm.group(1), {"코드": bm.group(1), "머리": "", "글": [], "자원": [], "행": [at, at]}) + continue + if blk is None: + continue + if _s(_cell(r, 0)).replace(" ", "") == "총계": + blk = None + continue + blk["행"][1] = at + text = _s(_cell(r, 1)) + if _s(_cell(r, 12)) == blk["코드"] and not blk["머리"]: + blk["머리"] = text + continue + if text: + blk["글"].append(text) + lm = _LINE.match(_s(_cell(r, 12))) + if lm: + letter, code = lm.group(1), lm.group(1) + lm.group(2) + name, _, spec = _s(_cell(r, 23)).partition("/") + if letter == "L" and labor.get(code) and labor[code].replace(" ", "") != name.replace(" ", ""): + mismatch += 1 + blk["자원"].append({"갈래": letter, "코드": code, "이름": name.strip() or labor.get(code, ""), "규격": spec.strip(), "품": _num(_cell(r, 7)) or 0.0}) + for code, blk in blocks.items(): + blk.update(heads.get(code) or {}) + return {"파일": tag, "꼴": "STmate", "호표": hopyos, "블록": blocks, "노무목록": labor, "이름어긋남": mismatch} + + +def read_excel(tag, path): + data = path.read_bytes() + book = hx.read_book(path.name, data) + sheets = hx._sheets(path.name, data) + calc = sheets.get(hb.SHEET) or [] + start, col, _ = hb._columns(calc) + blocks = {} + for b in book["단산"]: + text, spread = [], [] + for at in range(b["행"][0], b["행"][1] + 1): + row = calc[at - 1] + t = row[col] if col < len(row) else "" + if isinstance(t, str) and t.strip(): + text.append(t.rstrip()) + # 거창 꼴 — 글이 여러 칸에 흩어짐(한 칸 한 덩이) · 블록 끝 = 다음 머리 앞까지 · 칸을 빈칸으로 이어 붙임 + nxt = min([x["행"][0] for x in book["단산"] if x["행"][0] > b["행"][0]] or [len(calc) + 1]) + for at in range(b["행"][0] + 1, nxt): + row = calc[at - 1] + cells = [str(v).strip() for v in row[col + 1 : 141] if v not in (None, "") and str(v).strip()] + if cells: + spread.append(" ".join(cells)) + blocks[f"단산{b['번']}"] = {"코드": f"단산{b['번']}", "머리": f"{b['이름']} [{b['단위']}]", "글": text[1:] if len(text) > 1 else spread, "흩어짐": len(text) <= 1, "자원": [], "행": b["행"], "이름": b["이름"], "규격": "", "단위": b["단위"]} + hopyos = [] + for h in book["호표"]: + lines = [] + for line in h["줄"]: + ref = line.get("참조") or {} + code = f"단산{ref['번']}" if ref.get("종류") == "단산" else (f"일위{ref['번']}" if ref.get("종류") == "일위" else None) + lines.append({k: hx._json(line.get(k)) for k in ("차례", "이름", "규격", "수량", "단위", "단가", "갈래")} | {"코드": code}) + hopyos.append({"번": h["번"], "코드": f"일위{h['번']}", "이름": h["이름"], "규격": h["규격"], "단위": h["단위"], "줄": lines}) + return {"파일": tag, "꼴": "엑셀꼴", "호표": hopyos, "블록": blocks, "노무목록": {}, "이름어긋남": 0} + + +def summary(doc): + lines = [x for h in doc["호표"] for x in h["줄"]] + kinds = {} + for x in lines: + kinds[x["갈래"]] = kinds.get(x["갈래"], 0) + 1 + logic = [x for x in lines if x["갈래"] in ("단가산출", "기계", "기타")] + uniq = {(x["이름"], x["규격"], x["단위"], x["코드"]) for x in logic} + used = {x["코드"] for x in lines if x["코드"] in doc["블록"]} + return { + "호표": len(doc["호표"]), + "줄": len(lines), + "로직 줄": len(logic), + "로직 고유": len(uniq), + "블록": len(doc["블록"]), + "쓰인 블록": len(used), + "갈래": kinds, + "노무목록": len(doc["노무목록"]), + "이름어긋남": doc["이름어긋남"], + } + + +if __name__ == "__main__": + for tag, path in {**STMATE, **EXCEL}.items(): + try: + doc = read_stmate(tag, path) if tag in STMATE else read_excel(tag, path) + except Exception as e: # noqa: BLE001 + print(tag, "못 읽음", type(e).__name__, e) + continue + (HERE / f"data_{tag}.json").write_text(json.dumps(doc, ensure_ascii=False, indent=0, default=str), "utf-8") + print(tag, summary(doc)) diff --git a/docs/raw/조사/2026-10-15_338_실험자료/C6/s03_items.py b/docs/raw/조사/2026-10-15_338_실험자료/C6/s03_items.py new file mode 100644 index 000000000..503525d3b --- /dev/null +++ b/docs/raw/조사/2026-10-15_338_실험자료/C6/s03_items.py @@ -0,0 +1,202 @@ +"""C6 — 평가 단위(고유 줄) 만들기 + 블록 속(출처 · 직종 · 품 · 장비) 뽑기 → items.json · hopyo.json. + +단위 = 파일마다 「로직 줄」 고유(이름 · 규격 · 단위 · 코드) — STmate 는 호표에 안 쓰인 D 블록(내역서가 바로 씀)도 넣음(호표쓰임 표시) · +북부청 두 파일 = C1 out_items(같은 id · 정답 L) · 영월 = C1 id(Y000~) 이어 씀. +품: STmate = 자원 L 줄 7열(단위당 품) 합 · 엑셀꼴 = 글 「직종 : 단가 × … = 금액」 → 금액 ÷ 단가(가장 큰 수) — 금액은 품을 되찾는 데만 씀. +장비: STmate = 자원 X 줄 이름 · 엑셀꼴 = 글 속 장비 낱말 → 같은 낱말로 모음. +""" + +import json +import re +import sys +from pathlib import Path + +ROOT = Path(r"C:\Program_coding\임도설계 및 견적자동화 프로그램 개발") +sys.path.insert(0, str(ROOT)) +HERE = Path(__file__).resolve().parent +C1 = HERE.parent.parent / "req225/C1" +sys.stdout.reconfigure(encoding="utf-8") + +from M02_MasterTemplete import M02_Hopyo_Basis as hb # noqa: E402 +from M02_MasterTemplete import M02_Hopyo_Match as hm # noqa: E402 + +FILES = ["영월", "봉화", "대흥설계", "계류보전", "소광", "거창"] +PREFIX = {"영월": "Y", "봉화": "B", "대흥설계": "U", "계류보전": "K", "소광": "G", "거창": "C"} +SHOW = {"영월": "동부청영월", "봉화": "봉화", "대흥설계": "울진대흥", "계류보전": "영덕계류", "소광": "울진소광", "거창": "거창"} +LOGIC = ("단가산출", "기계", "기타") +MACH = [ # (찾을 낱말, 모을 이름) — 앞이 먼저 + ("굴삭기", "굴착기"), ("굴착기", "굴착기"), ("백호", "굴착기"), ("b/h", "굴착기"), + ("덤프", "덤프트럭"), ("크레인", "크레인"), ("불도저", "불도저"), ("불도우저", "불도저"), ("도저", "불도저"), + ("진동롤러", "롤러"), ("롤러", "롤러"), ("로울러", "롤러"), ("래머", "래머"), ("램머", "래머"), + ("콤팩터", "콤팩터"), ("컴팩터", "콤팩터"), ("브레이커", "브레이커"), ("브레이카", "브레이커"), + ("펌프카", "펌프차"), ("펌프차", "펌프차"), ("그레이더", "그레이더"), ("살수차", "살수차"), + ("발전기", "발전기"), ("공기압축기", "공기압축기"), ("드릴", "드릴"), ("믹서", "믹서"), ("트레일러", "트레일러"), +] +_LABOR_LINE = re.compile(r"^[\s\d\.\)\(○◇·\-*①-⑳가-힣]*?([가-힣][가-힣\s]*?)\s*:\s*(.+?)=\s*([\d,]+(?:\.\d+)?)\s*$") +_NUMS = re.compile(r"\d[\d,]*(?:\.\d+)?") + + +def machines(text): + low = re.sub(r"\s+", "", str(text or "")).lower() + out = set() + for word, name in MACH: + if word in low: + out.add(name) + low = low.replace(word, " ") + return sorted(out) + + +def trade_name(raw): + return hb.trade_names().get(re.sub(r"\s+", "", str(raw or ""))) + + +def text_labor(lines): + """엑셀꼴 글 → {직종: 품} (금액 ÷ 단가 · 단가 = 왼쪽에서 가장 큰 수 ≥ 1000).""" + out = {} + for t in lines: + m = _LABOR_LINE.match(str(t)) + if not m: + continue + name = trade_name(m.group(1)) + if not name: + continue + nums = [float(x.replace(",", "")) for x in _NUMS.findall(m.group(2))] + amount = float(m.group(3).replace(",", "")) + wage = max(nums) if nums else 0 + if wage >= 1000 and amount > 0: + out[name] = round(out.get(name, 0) + amount / wage, 6) + return out + + +def block_features(blk): + text = blk.get("글") or [] + joined = " ".join(text) + src = hb.sources(joined) + if blk.get("자원"): + labor, mach = {}, set() + for r in blk["자원"]: + if r["갈래"] == "L" and r["품"]: + name = trade_name(r["이름"]) + if name: + labor[name] = round(labor.get(name, 0) + r["품"], 6) + if r["갈래"] == "X": + mach |= set(machines(r["이름"])) + mach = sorted(mach) + else: + labor, mach = text_labor(text), machines(joined) + return {"출처": src, "직종": sorted(labor), "품": labor, "장비": mach} + + +def from_c1(): + """북부청 두 파일 — C1 고유 줄 그대로(id · 근거) + 품 · 장비.""" + out = [] + for r in json.loads((C1 / "out_items.json").read_text(encoding="utf-8")): + f = {"출처": (r.get("근거") or {}).get("출처") or [], "직종": [], "품": {}, "장비": []} + if r.get("단산글"): + g = r["단산글"][1:] + f = block_features({"글": g}) + f["출처"] = (r.get("근거") or {}).get("출처") or f["출처"] + out.append( + { + "id": r["id"], "파일": r["파일"], "이름": r["이름"], "규격": r["규격"], "단위": r["단위"], + "갈래": r["갈래"], "코드": f"단산{r['참조']['번']}" if (r.get("참조") or {}).get("종류") == "단산" else None, + "호표쓰임": True, "줄수": r["줄수"], "호표들": sorted(set(r["호표들"])), + "근거": {"출처": f["출처"], "직종": (r.get("근거") or {}).get("직종") or f["직종"]}, + "품": f["품"], "장비": f["장비"], "글": (r.get("단산글") or [])[1:40], + } + ) + return out + + +def from_file(tag): + d = json.loads((HERE / f"data_{tag}.json").read_text(encoding="utf-8")) + old = {} + if tag == "영월": + for y in json.loads((C1 / "out_stmate.json").read_text(encoding="utf-8")): + old[(y["이름"], y["규격"], y["단위"], y["코드"])] = y["id"] + seen, out, n = {}, [], 0 + feats = {code: block_features(b) for code, b in d["블록"].items()} + + def add(name, spec, unit, kind, code, used, hno): + nonlocal n + key = (name, spec, unit, code) + if key in seen: + if used: + seen[key]["줄수"] += 1 + seen[key]["호표쓰임"] = True + if hno not in seen[key]["호표들"]: + seen[key]["호표들"].append(hno) + return + f = feats.get(code) or {"출처": [], "직종": [], "품": {}, "장비": machines(f"{name} {spec}") if kind == "기계" else []} + rid = old.get(key) + if rid is None: + rid = f"{PREFIX[tag]}{100 + n if tag == '영월' else n:03d}" + n += 1 + rec = { + "id": rid, "파일": SHOW[tag], "이름": name, "규격": spec, "단위": unit, "갈래": kind, "코드": code, + "호표쓰임": used, "줄수": 1 if used else 0, "호표들": [hno] if used else [], + "근거": {"출처": f["출처"], "직종": f["직종"]}, "품": f["품"], "장비": f["장비"], + "글": (d["블록"].get(code) or {}).get("글", [])[:40], + } + seen[key] = rec + out.append(rec) + + for h in d["호표"]: + for x in h["줄"]: + if x["갈래"] in LOGIC: + add(x["이름"], x["규격"], x["단위"], x["갈래"], x["코드"], True, h["번"]) + for code, b in d["블록"].items(): # 호표에 안 쓰인 블록(내역서가 바로 씀) + if not any(r["코드"] == code for r in out): + add(b.get("이름") or b["머리"], b.get("규격") or "", b.get("단위") or "", "단가산출", code, False, None) + return out + + +def hopyo_units(tag): + """호표 = 로직(노임 줄을 바로 적은 호표) — 이름 · 단위 · 직종 품 · 장비 · 줄 코드.""" + if tag in ("북부청임도", "북부청사방"): + return [] + d = json.loads((HERE / f"data_{tag}.json").read_text(encoding="utf-8")) + out = [] + for h in d["호표"]: + labor = {} + for x in h["줄"]: + is_labor = x["갈래"] == "노임" or (x["갈래"] == "기타" and hm.mm.plain_unit(x["단위"]) == "인") + name = trade_name(x["이름"]) if is_labor else None + if name and x.get("수량"): + labor[name] = round(labor.get(name, 0) + float(x["수량"]), 6) + if labor: + mach = sorted({m for x in h["줄"] if x["갈래"] == "기계" for m in machines(x["이름"])}) + out.append({"파일": SHOW[tag], "호표": h["번"], "이름": h.get("이름"), "규격": h.get("규격"), "단위": h.get("단위"), "품": labor, "장비": mach}) + return out + + +def hopyo_map(): + """파일 → 호표 번 → 그 호표 로직 줄 id 목록(호표 조합 지문용).""" + lines = json.loads((C1 / "out_lines.json").read_text(encoding="utf-8")) + out = {} + return out, lines + + +if __name__ == "__main__": + items = from_c1() + units = [] + for tag in FILES: + items += from_file(tag) + units += hopyo_units(tag) + (HERE / "items.json").write_text(json.dumps(items, ensure_ascii=False, indent=0), "utf-8") + (HERE / "hopyo_units.json").write_text(json.dumps(units, ensure_ascii=False, indent=0), "utf-8") + by = {} + for r in items: + s = by.setdefault(r["파일"], {"고유": 0, "호표쓰임": 0, "엑셀줄": 0, "출처있음": 0, "직종있음": 0, "품있음": 0}) + s["고유"] += 1 + s["호표쓰임"] += r["호표쓰임"] + s["엑셀줄"] += r["줄수"] + s["출처있음"] += bool(r["근거"]["출처"]) + s["직종있음"] += bool(r["근거"]["직종"]) + s["품있음"] += bool(r["품"]) + for k, v in by.items(): + print(k, v) + hu = {} + for u in units: + hu[u["파일"]] = hu.get(u["파일"], 0) + 1 + print("호표=로직", hu) diff --git a/docs/raw/조사/2026-10-15_338_실험자료/C6/s04_auto.py b/docs/raw/조사/2026-10-15_338_실험자료/C6/s04_auto.py new file mode 100644 index 000000000..5bf362c89 --- /dev/null +++ b/docs/raw/조사/2026-10-15_338_실험자료/C6/s04_auto.py @@ -0,0 +1,202 @@ +"""C6 — 자동 정답(엄격): 엑셀 블록 인용 조항(같은 책 · 번호 같음) + 직종 묶음 · 품(±3%)이 우리 돈 로직 하나와만 맞으면 그 로직. + +우리 품 = 로직을 입력 조합마다 돌려(고르기 모두 · 범위 끝 둘 · 지역 · 기계 고르기는 견본값) 「인」 줄 수량 — +원본 로직 · 마스터는 읽기만(엔진 run 을 같은 프로세스에서 부름). +python s04_auto.py → auto.json(id → {정답, 까닭, 후보 수}) + 사람 정답(L · LY) · 씨앗과 맞대 본 표 +""" + +import itertools +import json +import re +import sys +from decimal import Decimal +from functools import lru_cache +from pathlib import Path + +ROOT = Path(r"C:\Program_coding\임도설계 및 견적자동화 프로그램 개발") +sys.path.insert(0, str(ROOT)) +HERE = Path(__file__).resolve().parent +C1 = HERE.parent.parent / "req225/C1" +sys.path.insert(0, str(C1)) +sys.stdout.reconfigure(encoding="utf-8") + +from M01_MasterData import M01_MasterData_Store as m01 # noqa: E402 +from M02_MasterTemplete import M02_Hopyo_Basis as hb # noqa: E402 +from M02_MasterTemplete import M02_Hopyo_Match as hm # noqa: E402 + +TOL = 0.03 +LOOSE = "loose" in sys.argv +CAP = 600 +mf, mau = m01.cm.mf, m01.mau +WHOLE = m01.loaded()[1] + + +def _choices(spec): + opts = spec.get("고르기") + if opts: + if spec["이름"] == "지역" or all(re.match(r"^\d{4}-\d{4}$", str(o)) for o in opts): + return None + return list(opts) + rng = spec.get("범위") + if isinstance(rng, list) and len(rng) == 2: + return [Decimal(str(rng[0])), Decimal(str(rng[1]))] + return None + + +_CACHE_PATH = HERE / "vec_cache.json" +try: + _CACHE = {k: tuple(v) for k, v in json.loads(_CACHE_PATH.read_text(encoding="utf-8")).items()} +except (OSError, ValueError): + _CACHE = {} + + +def vectors(key): + if key not in _CACHE: + _CACHE[key] = _vectors(key) + return _CACHE[key] + + +def save_cache(): + _CACHE_PATH.write_text(json.dumps({k: list(v) for k, v in _CACHE.items()}, ensure_ascii=False), "utf-8") + + +def _vectors(key): + """[{직종: 품}] — 입력 조합마다(0 빼고) · 같은 것 한 번.""" + try: + row = WHOLE.logic(key) + base = mau.values(WHOLE, row) + except Exception: # noqa: BLE001 + return () + names, pools = [], [] + for spec in row.get("입력") or []: + c = _choices(spec) + if c: + names.append(spec["이름"]) + pools.append(c) + size = 1 + for p in pools: + size *= len(p) + if size > CAP: # 범위 흔들기 뺌 + keep = [(n, p) for n, p in zip(names, pools) if not isinstance(p[0], Decimal)] + names, pools = [n for n, _ in keep], [p for _, p in keep] + out, seen = [], set() + for combo in itertools.islice(itertools.product(*pools), CAP): + given = mf.fill_empty(row, {**base, **dict(zip(names, combo))}) + try: + res = mf.run(WHOLE, key, given) + except Exception: # noqa: BLE001 + continue + vec = {} + for line in res.get("줄") or []: + if hm.mm.plain_unit(line.get("단위")) == "인": + q = float(line.get("수량") or 0) + if q > 0: + vec[str(line["이름"])] = round(vec.get(str(line["이름"]), 0) + q, 6) + sig = tuple(sorted(vec.items())) + if vec and sig not in seen: + seen.add(sig) + out.append(vec) + return tuple(out) + + +def same_labor(ext, ours): + if set(ext) != set(ours): + return False + return all(abs(ext[t] - ours[t]) <= TOL * max(ext[t], ours[t]) for t in ext) + + +def auto_label(item, table, logics): + src = item["근거"]["출처"] + labor = item.get("품") or {} + if not src: + return None, "출처 없음", 0 + if not labor: + return None, "직종 · 품 없음", 0 + cands = [k for k in logics if any(b == table[k]["책"] and n in table[k]["조항"] for b, n in src)] + if not cands: + return None, "조항 같은 로직 없음", 0 + hits = [k for k in cands if any(same_labor(labor, v) for v in vectors(k))] + if len(hits) == 1: + return hits[0], "하나", len(cands) + if LOOSE and not hits: # 준엄격 — 품 값은 안 보고 직종 묶음만 + hits = [k for k in cands if any(set(labor) == set(v) for v in vectors(k))] + if len(hits) == 1: + return hits[0], "하나(직종만)", len(cands) + if not hits: + return None, "조항은 같으나 직종 · 품 다름", len(cands) + return None, f"둘 이상({len(hits)})", len(cands) + + +def seeds_of(item, got): + s = got["씨앗"] + return s.get(hm.mm.plain(f"{item['이름']}{item['규격']}")) or s.get(hm.mm.plain(item["이름"])) or set() + + +if __name__ == "__main__": + from labels import L # noqa: E402 + from labels_y import LY # noqa: E402 + + hand = {**L, **LY} + items = json.loads((HERE / "items.json").read_text(encoding="utf-8")) + got = hm.targets() + table = hb.ours() + logics = [x["로직"] for x in got["로직"] if x["로직"] in table] + out, why = {}, {} + for r in items: + if r["갈래"] != "단가산출": + out[r["id"]] = {"정답": None, "까닭": "단가산출 줄 아님"} + continue + k, reason, n = auto_label(r, table, logics) + out[r["id"]] = {"정답": k, "까닭": reason, "조항후보": n} + save_cache() + (HERE / ("auto_loose.json" if LOOSE else "auto.json")).write_text(json.dumps(out, ensure_ascii=False, indent=0), "utf-8") + files = list(dict.fromkeys(r["파일"] for r in items)) + print("파일 | 고유 | 단가산출 | 자동정답 | 호표쓰임 중 자동 | 까닭별") + for f in files: + rs = [r for r in items if r["파일"] == f] + d = [r for r in rs if r["갈래"] == "단가산출"] + a = [r for r in d if out[r["id"]]["정답"]] + au = [r for r in a if r["호표쓰임"]] + reasons = {} + for r in d: + if not out[r["id"]]["정답"]: + key = out[r["id"]]["까닭"].split("(")[0] + reasons[key] = reasons.get(key, 0) + 1 + print(f, len(rs), len(d), len(a), f"{len(au)}/{sum(1 for r in d if r['호표쓰임'])}", reasons) + # 사람 정답과 맞대 봄 + agree = {"엄격 같음": 0, "느슨만 같음": 0, "다름": 0, "사람 없음(우리에 없음)": 0} + bad = [] + for r in items: + a = out[r["id"]]["정답"] + if not a or r["id"] not in hand: + continue + lab = hand[r["id"]] + if not lab["정답"]: + agree["사람 없음(우리에 없음)"] += 1 + bad.append((r["id"], r["이름"], a, "사람 없음")) + elif a in lab["정답"]: + agree["엄격 같음"] += 1 + elif a in lab.get("허용", []): + agree["느슨만 같음"] += 1 + bad.append((r["id"], r["이름"], a, lab["정답"])) + else: + agree["다름"] += 1 + bad.append((r["id"], r["이름"], a, lab["정답"], out[r["id"]]["까닭"])) + print("사람 정답 대 자동:", agree) + for b in bad: + print(" ", b) + # 씨앗과 맞대 봄(씨앗 = 같은 실무 파일에서 사람이 골라 둔 짝) + sa = {"같음": 0, "다름": 0} + sbad = [] + for r in items: + a = out[r["id"]]["정답"] + s = seeds_of(r, got) + if a and s: + if a in s: + sa["같음"] += 1 + else: + sa["다름"] += 1 + sbad.append((r["id"], r["이름"], r["규격"], a, sorted(s))) + print("씨앗 대 자동:", sa) + for b in sbad: + print(" ", b) diff --git a/docs/raw/조사/2026-10-15_338_실험자료/C6/s05_handcheck.py b/docs/raw/조사/2026-10-15_338_실험자료/C6/s05_handcheck.py new file mode 100644 index 000000000..be816332b --- /dev/null +++ b/docs/raw/조사/2026-10-15_338_실험자료/C6/s05_handcheck.py @@ -0,0 +1,26 @@ +"""C6 — 손 확인 표본: 사람 정답이 없는 줄의 자동 정답(엄격 먼저 · 모자라면 준엄격) 20 줄 보기.""" +import json, sys +from pathlib import Path +HERE = Path(__file__).resolve().parent +sys.path.insert(0, str(HERE.parent.parent / "req225/C1")) +from s04_auto import hb, hm, vectors, WHOLE # noqa: E402 +from labels import L # noqa: E402 +from labels_y import LY # noqa: E402 +hand = {**L, **LY} +items = {r["id"]: r for r in json.loads((HERE / "items.json").read_text(encoding="utf-8"))} +strict = json.loads((HERE / "auto.json").read_text(encoding="utf-8")) +loose = json.loads((HERE / "auto_loose.json").read_text(encoding="utf-8")) +got = hm.targets(); names = {x["로직"]: x for x in got["로직"]}; table = hb.ours() +pick = [i for i, a in strict.items() if a["정답"] and i not in hand] +extra = [i for i, a in loose.items() if a["정답"] and i not in hand and i not in pick] +sample = (pick + extra)[:20] +print("엄격(사람 정답 없는 줄)", len(pick), "준엄격 더함", len(extra), "표본", len(sample)) +for i in sample: + r = items[i]; a = (strict.get(i) if i in pick else loose[i])["정답"] + o = table[a]; row = WHOLE.logic(a) + print(f"## {i} [{r['파일']}] {'엄격' if i in pick else '준엄격'} 호표쓰임 {r['호표쓰임']} | {r['이름']} | {r['규격']} | {r['단위']}") + print(f" 엑셀 출처 {r['근거']['출처']} 품 {r['품']} 장비 {r['장비']}") + for t in r["글"][:9]: + print(" ·", " ".join(str(t).split())[:110]) + print(f" → {a} {names[a]['이름']} [{names[a]['단위']}] {o['구분']} {sorted(o['조항'])} 입력 {[s['이름'] for s in row.get('입력') or []]}") + print(f" 우리 품 {[v for v in vectors(a)][:4]}") diff --git a/docs/raw/조사/2026-10-15_338_실험자료/C6/s06_match.py b/docs/raw/조사/2026-10-15_338_실험자료/C6/s06_match.py new file mode 100644 index 000000000..06baf340a --- /dev/null +++ b/docs/raw/조사/2026-10-15_338_실험자료/C6/s06_match.py @@ -0,0 +1,69 @@ +"""C6 — 지금 맞춤(hm.match_line · 기억 없음)을 넓힌 자료 모든 줄에 돌림 → match.json. + +갈래(근거를 얼마나 읽었다고 치나 — STmate · 거창만 바꿈 · 북부청은 저장소 읽기 그대로): + 이름만 = 근거 없음(거창 지금 읽기 · STmate 를 이름만 읽는다면) + 출처만 = 조항만(C1 영월 흉내와 같음 — 직종 이음 빠짐) + 출처직종 = 조항 + 노무 목록으로 이은 직종(STmate 읽기 보강 뒤 · 거창 흩어진 칸 붙임 뒤) +씨앗 켬 · 끔 둘 다. 저장소 안 고침 — 씨앗 끔 = 같은 프로세스에서 hm._seeds 를 빈 것으로 바꿔 끼움. +""" + +import json +import sys +from pathlib import Path + +ROOT = Path(r"C:\Program_coding\임도설계 및 견적자동화 프로그램 개발") +sys.path.insert(0, str(ROOT)) +HERE = Path(__file__).resolve().parent +sys.stdout.reconfigure(encoding="utf-8") + +from M02_MasterTemplete import M02_Hopyo_Basis as hb # noqa: E402 +from M02_MasterTemplete import M02_Hopyo_Match as hm # noqa: E402 + +ITEMS = json.loads((HERE / "items.json").read_text(encoding="utf-8")) +OLD = ("북부청임도", "북부청사방") +VARIANTS = ("이름만", "출처만", "출처직종") + + +def line_of(r, variant): + basis = dict(r["근거"]) + if r["파일"] not in OLD: + if variant == "이름만": + basis = {} + elif variant == "출처만": + basis = {"출처": basis.get("출처") or [], "직종": []} + return {"이름": r["이름"], "규격": r["규격"], "단위": r["단위"], "단가": {}, "갈래": r["갈래"], "근거": basis or None} + + +def top_of(m): + if m["상태"] in ("같음", "기억"): + return [m["대상"].get("로직")] if m.get("대상") and m["대상"].get("로직") else [] + return [c.get("로직") for c in m.get("후보") or [] if c.get("로직")] + + +def run(seed, variant, items=ITEMS): + real = hm._seeds + if not seed: + hm._seeds = lambda: {} + hm._targets.cache_clear() + hb._ours.cache_clear() + try: + got = hm.targets() + out = {} + for r in items: + m = hm.match_line(line_of(r, variant), [], {}, True, got) + out[r["id"]] = {"상태": m["상태"], "top": top_of(m)} + return out + finally: + hm._seeds = real + hm._targets.cache_clear() + + +if __name__ == "__main__": + want = sys.argv[1:] or [f"{v}|{s}" for v in VARIANTS for s in ("켬", "끔")] + path = HERE / "match.json" + allm = json.loads(path.read_text(encoding="utf-8")) if path.exists() else {} + for w in want: + v, s = w.split("|") + allm[w] = run(s == "켬", v) + print(w, "done", len(allm[w])) + path.write_text(json.dumps(allm, ensure_ascii=False), "utf-8") diff --git a/docs/raw/조사/2026-10-15_338_실험자료/C6/s07_score.py b/docs/raw/조사/2026-10-15_338_실험자료/C6/s07_score.py new file mode 100644 index 000000000..2603d9b0a --- /dev/null +++ b/docs/raw/조사/2026-10-15_338_실험자료/C6/s07_score.py @@ -0,0 +1,96 @@ +"""C6 — 지금 실력 표(정답 셋 · 갈래 · 씨앗 켬/끔). + +정답 셋: 사람(C1 L · LY · 엄격/느슨) · 자동 엄격(새 파일 · 사람 정답 없는 줄) · 자동 준엄격 · 씨앗 짝(씨앗 끔에서만 · 이름이 씨앗 메모와 같은 줄). +python s07_score.py → 표 찍고 score.json +""" + +import json +import sys +from pathlib import Path + +HERE = Path(__file__).resolve().parent +C1 = HERE.parent.parent / "req225/C1" +sys.path.insert(0, str(C1)) +sys.path.insert(0, str(HERE)) +sys.stdout.reconfigure(encoding="utf-8") + +from labels import L # noqa: E402 +from labels_y import LY # noqa: E402 +from s04_auto import hm, seeds_of # noqa: E402 + +ITEMS = json.loads((HERE / "items.json").read_text(encoding="utf-8")) +BY = {r["id"]: r for r in ITEMS} +M = json.loads((HERE / "match.json").read_text(encoding="utf-8")) +AUTO = json.loads((HERE / "auto.json").read_text(encoding="utf-8")) +LOOSE = json.loads((HERE / "auto_loose.json").read_text(encoding="utf-8")) +HAND = {k: v for k, v in {**L, **LY}.items() if k in BY} +FILES = list(dict.fromkeys(r["파일"] for r in ITEMS)) + + +def answers(source): + """{id: (엄격 set, 느슨 set)} — 정답 있는 줄만.""" + out = {} + if source == "사람": + for i, lab in HAND.items(): + if lab["정답"]: + out[i] = (set(lab["정답"]), set(lab["정답"]) | set(lab.get("허용") or [])) + elif source in ("자동", "준엄격"): + src = AUTO if source == "자동" else LOOSE + for i, a in src.items(): + if a["정답"] and i not in HAND: + out[i] = ({a["정답"]}, {a["정답"]}) + elif source == "씨앗": + got = hm.targets() + for r in ITEMS: + if r["파일"] in ("북부청임도", "북부청사방") or r["id"] in HAND: + continue + s = seeds_of(r, got) + if s: + out[r["id"]] = (set(s), set(s)) + return out + + +def rate(ids, res, ans, k, loose=False, used_only=False): + ids = [i for i in ids if i in ans and (BY[i]["호표쓰임"] or not used_only)] + if not ids: + return None, 0 + hit = 0 + for i in ids: + top = res[i]["top"][:k] + hit += bool(set(top) & ans[i][1 if loose else 0]) + return round(100 * hit / len(ids), 1), len(ids) + + +def table(source, key, used_only=False): + ans = answers(source) + res = M[key] + rows = {} + for f in FILES + ["모두"]: + ids = [r["id"] for r in ITEMS if (r["파일"] == f or f == "모두")] + a1, n = rate(ids, res, ans, 1, used_only=used_only) + if not n: + continue + rows[f] = { + "n": n, + "1위 엄격": a1, + "1위 느슨": rate(ids, res, ans, 1, True, used_only)[0], + "5안 엄격": rate(ids, res, ans, 5, used_only=used_only)[0], + "5안 느슨": rate(ids, res, ans, 5, True, used_only)[0], + } + return rows + + +if __name__ == "__main__": + out = {} + for source in ("사람", "자동", "준엄격", "씨앗"): + for key in M: + if source == "씨앗" and key.endswith("켬"): + continue + for used in (False, True): + t = table(source, key, used) + out[f"{source}|{key}|{'호표만' if used else '모두'}"] = t + (HERE / "score.json").write_text(json.dumps(out, ensure_ascii=False, indent=0), "utf-8") + for k, t in out.items(): + if k.endswith("모두") and not k.startswith("씨앗") or k.startswith("씨앗") and k.endswith("모두"): + cells = " · ".join(f"{f} {v['n']}:{v['1위 엄격']}/{v['5안 엄격']}" for f, v in t.items()) + print(k, "|", cells) diff --git a/docs/raw/조사/2026-10-15_338_실험자료/C6/s08_memory.py b/docs/raw/조사/2026-10-15_338_실험자료/C6/s08_memory.py new file mode 100644 index 000000000..9c0f3c044 --- /dev/null +++ b/docs/raw/조사/2026-10-15_338_실험자료/C6/s08_memory.py @@ -0,0 +1,290 @@ +"""C6 — 기억 흉내: 앞 파일에서 사람이 확정했다고 치고(확정 = 정답) 다음 파일 1순위가 얼마나 오르나 · 틀린 기억 해로움. + +열쇠: a 품명|규격|단위(지금 line_key) · b 품명|단위 · c 로직 속 지문(단위 · 직종:품 · 장비 · 조항 — 금액 안 씀) · +c2 지문(단위 · 직종:품만 — 조항 · 장비 뺌) · d 호표 조합 지문(호표 단위 + 그 호표 로직 줄 지문 묶음 → 그 안 자리). +확정(기억 값) = 사람 정답(C1) ∪ 자동 정답(엄격) ∪ 씨앗 짝(씨앗을 만든 파일 줄 = 사람이 고른 것) · 판정 = 사람 ∪ 자동(엄격)만. +바탕(기억 없음) = 지금 맞춤 · 출처직종 · 씨앗 끔(씨앗도 「앞 파일 확정」 한 꼴로 기억 쪽에 넣음). +python s08_memory.py [호표만|모두] [준엄격] → 표 · memory.json +""" + +import json +import sys +from collections import Counter +from pathlib import Path + +HERE = Path(__file__).resolve().parent +C1 = HERE.parent.parent / "req225/C1" +sys.path.insert(0, str(C1)) +sys.path.insert(0, str(HERE)) +sys.stdout.reconfigure(encoding="utf-8") + +from labels import L # noqa: E402 +from labels_y import LY # noqa: E402 +from s04_auto import hb, hm, seeds_of # noqa: E402 + +USED_ONLY = "모두" not in sys.argv +LOOSE = "준엄격" in sys.argv +ITEMS = json.loads((HERE / "items.json").read_text(encoding="utf-8")) +if USED_ONLY: + ITEMS = [r for r in ITEMS if r["호표쓰임"]] +BY = {r["id"]: r for r in ITEMS} +M = json.loads((HERE / "match.json").read_text(encoding="utf-8")) +BASE = M["출처직종|끔"] +AUTO = json.loads((HERE / ("auto_loose.json" if LOOSE else "auto.json")).read_text(encoding="utf-8")) +HAND = {k: v for k, v in {**L, **LY}.items() if k in BY} +META = { # 파일: (발주처, 해, 꼴) + "울진대흥": ("남부청", 2024, "STmate"), + "봉화": ("남부청", 2024, "STmate"), + "동부청영월": ("동부청", 2024, "STmate"), + "울진소광": ("남부청", 2025, "STmate"), + "영덕계류": ("남부청", 2025, "STmate"), + "거창": ("남부청", 2025, "엑셀꼴"), + "북부청임도": ("북부청", 2026, "엑셀꼴"), + "북부청사방": ("북부청", 2026, "엑셀꼴"), +} +ORDER = list(META) +GOT = hm.targets() +OURS = hb.ours() + + +# ── 정답 · 확정 ──────────────────────────────────────────────────────── +def truth(i): + """판정 정답(엄격 set) — 사람 · 자동만 · 없으면 None.""" + if i in HAND: + return set(HAND[i]["정답"]) or None + a = AUTO.get(i, {}).get("정답") + return {a} if a else None + + +def confirmed(i): + """사람이 확정한 값(기억으로 들어갈 것) — 사람 · 자동 · 씨앗 짝 · 없으면 None. 여럿이면 차례 첫째.""" + t = truth(i) + if t: + return sorted(t)[0] + r = BY[i] + if r["파일"] in ("북부청임도", "북부청사방"): + return None + s = seeds_of(r, GOT) + return sorted(s)[0] if s else None + + +# ── 열쇠 ─────────────────────────────────────────────────────────────── +def k_a(r): + return hm.line_key(r) + + +def k_b(r): + return f"{hm.mm.plain(r['이름'])}|{hm.mm.plain_unit(r['단위'])}" + + +def _labor(r): + return ",".join(f"{t}:{q:.3g}" for t, q in sorted((r.get("품") or {}).items())) + + +def k_c(r): + src = ",".join(sorted(f"{b[:2]}{n}" for b, n in r["근거"]["출처"])) + mach = ",".join(r.get("장비") or []) + if not r.get("품") and not (mach and src): + return None + return f"{hm.mm.plain_unit(r['단위'])}|{_labor(r)}|{mach}|{src}" + + +def k_c2(r): + if not r.get("품"): + return None + return f"{hm.mm.plain_unit(r['단위'])}|{_labor(r)}" + + +HOPYO = {} # (파일, 호표 번) → [id] +for r in ITEMS: + for h in r["호표들"]: + HOPYO.setdefault((r["파일"], h), []).append(r["id"]) + + +def elem(r): + return k_c(r) or f"N:{k_a(r)}" + + +def k_d_all(r): + """줄이 든 호표마다 (호표 조합 열쇠, 자리 열쇠).""" + out = [] + for h in r["호표들"]: + ids = HOPYO[(r["파일"], h)] + combo = "+".join(sorted(elem(BY[i]) for i in ids)) + out.append(f"{len(ids)}#{combo}§{elem(r)}") + return out + + +KEYS = {"a": k_a, "b": k_b, "c": k_c, "c2": k_c2, "d": k_d_all} + + +def keys_of(name, r): + got = KEYS[name](r) + if got is None: + return [] + return got if isinstance(got, list) else [got] + + +# ── 기억 만들기 · 쓰기 ───────────────────────────────────────────────── +def build(files, name, all_items=False): + """{열쇠: [(파일, 답)]} — files 차례대로 · all_items = 확정 없는 줄도(답 None · 「닿음」 셈용).""" + memo = {} + for f in files: + for r in ITEMS: + if r["파일"] != f: + continue + ans = confirmed(r["id"]) + if ans is None and not all_items: + continue + for k in keys_of(name, r): + memo.setdefault(k, []).append((f, ans)) + return memo + + +def recall(name, r, memo, policy="최근"): + """(답, 출처 목록) — 최근 = 마지막에 확정한 답 · 다수 = 표 많은 답(같으면 최근).""" + for k in keys_of(name, r): + if k in memo: + got = [(f, a) for f, a in memo[k] if a] + if not got: + return None, [] + if policy == "다수": + votes = Counter(a for _, a in got) + top = max(votes.values()) + best = [a for _, a in reversed(got) if votes[a] == top] + return best[0], got + return got[-1][1], got + return None, [] + + +def basis_of(r, ans): + """기억 답의 우리 조항이 엑셀 인용 조항과 같은가 — 맞음 · 다름 · 인용 없음.""" + src = r["근거"]["출처"] + if not src: + return "인용 없음" + our = OURS.get(ans) or {} + return "근거 맞음" if any(b == our.get("책") and n in our.get("조항", ()) for b, n in src) else "근거 다름" + + +def evaluate(target, sources, name, policy="최근", cond=None): + memo = build(sources, name) + reach_memo = build(sources, name, all_items=True) + rows = [r for r in ITEMS if r["파일"] == target] + reach = sum(1 for r in rows if any(k in reach_memo for k in keys_of(name, r))) + s = Counter() + for r in rows: + t = truth(r["id"]) + if not t: + continue + base = BASE[r["id"]]["top"] + ans, prov = recall(name, r, memo, policy) + before = bool(base[:1]) and base[0] in t + after = before if ans is None else ans in t + five_before = bool(set(base[:5]) & t) + five_after = five_before if ans is None else bool(set([ans, *[x for x in base if x != ans]][:5]) & t) + who = "사람" if r["id"] in HAND else "자동" + s["판정줄"] += 1 + s[f"판정줄_{who}"] += 1 + s["전 1위"] += before + s["뒤 1위"] += after + s["전 5안"] += five_before + s["뒤 5안"] += five_after + if ans is None: + continue + ok = ans in t + s["기억 닿음"] += 1 + s["기억 맞음"] += ok + s[f"닿음_{who}"] += 1 + s[f"맞음_{who}"] += ok + conflict = len({a for _, a in prov}) > 1 + s["충돌 열쇠"] += conflict + s["해로움"] += before and not after + s["도움"] += after and not before + if cond is not None: + same_org = any(META[f][0] == META[target][0] for f, _ in prov) + same_year = any(META[f][1] == META[target][1] for f, _ in prov) + files = len({f for f, a in prov if a == ans}) + for c in ( + f"열쇠 {name}", + f"{name} · {'같은 발주처' if same_org else '다른 발주처'}", + f"{name} · {'같은 해' if same_year else '다른 해'}", + f"{name} · {basis_of(r, ans)}", + f"{name} · {'두 파일 넘게 같은 답' if files >= 2 else '한 파일'}", + f"{name} · {'충돌 있음' if conflict else '충돌 없음'}", + f"{name} · 판정 {who}", + f"{name} · {'같은 발주처' if same_org else '다른 발주처'} · {basis_of(r, ans)}", + ): + cond.setdefault(c, Counter()) + cond[c]["닿음"] += 1 + cond[c]["맞음"] += ok + cond[c]["해로움"] += before and not after + return {"줄": len(rows), "닿음(모두 확정 셈)": reach, **s} + + +def pct(a, b): + return f"{100 * a / b:.0f}%" if b else "—" + + +def show(title, res): + print(f"\n### {title}") + print("열쇠 | 줄 · 닿음(모두 확정) | 판정줄(사람) · 기억 닿음 · 맞음 (사람 닿음·맞음) | 1위 전→뒤 | 5안 전→뒤 | 도움 · 해로움 · 충돌") + for name, s in res.items(): + n = s.get("판정줄", 0) + print( + f"{name} | {s['줄']} · {s['닿음(모두 확정 셈)']} ({pct(s['닿음(모두 확정 셈)'], s['줄'])}) " + f"| {n}({s.get('판정줄_사람', 0)}) · {s.get('기억 닿음', 0)} · {s.get('기억 맞음', 0)} ({s.get('닿음_사람', 0)}·{s.get('맞음_사람', 0)}) " + f"| {pct(s.get('전 1위', 0), n)}→{pct(s.get('뒤 1위', 0), n)} | {pct(s.get('전 5안', 0), n)}→{pct(s.get('뒤 5안', 0), n)} " + f"| {s.get('도움', 0)} · {s.get('해로움', 0)} · {s.get('충돌 열쇠', 0)}" + ) + + +def add(acc, s): + for k, v in s.items(): + acc[k] = acc.get(k, 0) + v + + +SOUTH24, SOUTH25 = ["울진대흥", "봉화"], ["울진소광", "영덕계류", "거창"] +SCEN = { + "북부청임도 → 북부청사방": (["북부청임도"], ["북부청사방"]), + "북부청사방 → 북부청임도": (["북부청사방"], ["북부청임도"]), + "울진대흥 → 봉화(남부 2024)": (["울진대흥"], ["봉화"]), + "봉화 → 울진대흥(남부 2024)": (["봉화"], ["울진대흥"]), + "남부 2024 → 남부 2025(해 바뀜)": (SOUTH24, SOUTH25), + "북부청 → 동부청영월(다른 발주처)": (["북부청임도", "북부청사방"], ["동부청영월"]), + "동부청영월 → 북부청(다른 발주처)": (["동부청영월"], ["북부청임도", "북부청사방"]), + "동부청영월 → 남부 2024(다른 발주처 · 같은 해)": (["동부청영월"], SOUTH24), + "남부 2024·2025 → 북부청(다른 발주처 · 해 바뀜)": (SOUTH24 + SOUTH25, ["북부청임도", "북부청사방"]), +} + +if __name__ == "__main__": + out = {"설정": {"호표만": USED_ONLY, "준엄격": LOOSE}} + for title, (srcs, dsts) in SCEN.items(): + res = {} + for name in KEYS: + acc = {} + for d in dsts: + add(acc, evaluate(d, srcs, name)) + res[name] = acc + show(title, res) + out[title] = res + cond = {} + for policy in ("최근", "다수"): + seq = {name: {} for name in KEYS} + for at, target in enumerate(ORDER[1:], 1): + for name in KEYS: + add(seq[name], evaluate(target, ORDER[:at], name, policy, cond if policy == "최근" else None)) + show(f"차례 누적(앞 파일 모두 기억 · {' → '.join(ORDER)}) · 충돌 때 {policy}", seq) + out[f"차례누적_{policy}"] = seq + # 짝 모두(한 파일 → 다른 한 파일) — 조건별 맞음 셈을 늘림 + for src in ORDER: + for dst in ORDER: + if src != dst: + for name in KEYS: + evaluate(dst, [src], name, "최근", cond) + print("\n### 조건별 기억 맞음(차례 누적 + 모든 짝 · 판정줄만)") + for c, v in sorted(cond.items()): + print(f"{c} | 닿음 {v['닿음']} · 맞음 {v['맞음']} ({pct(v['맞음'], v['닿음'])}) · 해로움 {v['해로움']}") + out["조건"] = {c: dict(v) for c, v in cond.items()} + (HERE / f"memory{'_모두' if not USED_ONLY else ''}{'_준엄격' if LOOSE else ''}.json").write_text( + json.dumps(out, ensure_ascii=False, indent=0), "utf-8" + ) diff --git a/docs/raw/조사/2026-10-15_338_실험자료/C6/s09_examples.py b/docs/raw/조사/2026-10-15_338_실험자료/C6/s09_examples.py new file mode 100644 index 000000000..8212904b4 --- /dev/null +++ b/docs/raw/조사/2026-10-15_338_실험자료/C6/s09_examples.py @@ -0,0 +1,26 @@ +"""C6 — 기억 예: 해로움(틀린 기억이 맞던 1순위를 뒤집음) · 도움(c 지문이 다른 발주처에서 맞힘).""" +import sys +from s08_memory import BY, ITEMS, ORDER, META, BASE, build, recall, truth, keys_of, basis_of, GOT +names = {x["로직"]: x["이름"] for x in GOT["로직"]} +seen = set() +for name in ("b", "c2", "c", "a"): + print(f"\n## 열쇠 {name}") + for src in ORDER: + for dst in ORDER: + if src == dst or META[src][0] == META[dst][0]: + continue + memo = build([src], name) + for r in ITEMS: + if r["파일"] != dst or not truth(r["id"]): + continue + ans, prov = recall(name, r, memo) + if ans is None: + continue + t = truth(r["id"]); base = BASE[r["id"]]["top"] + before = bool(base[:1]) and base[0] in t + after = ans in t + tag = "해로움" if before and not after else ("도움" if after and not before else None) + if tag and (name, r["id"], tag) not in seen: + seen.add((name, r["id"], tag)) + src_item = next(x for x in ITEMS if x["파일"] == src and any(k in keys_of(name, x) for k in keys_of(name, r))) + print(f"{tag} {src}→{dst} | {r['이름']} {r['규격']} [{r['단위']}] 엑셀출처 {r['근거']['출처']} | 기억 {ans} {names.get(ans)} ({basis_of(r, ans)}) | 정답 {sorted(t)} {[names.get(x) for x in t]} | 앞 파일 줄 {src_item['이름']} {src_item['규격']} 출처 {src_item['근거']['출처']}") diff --git a/docs/raw/조사/2026-10-15_338_실험자료/C6/s10_policy.py b/docs/raw/조사/2026-10-15_338_실험자료/C6/s10_policy.py new file mode 100644 index 000000000..1e5b1de86 --- /dev/null +++ b/docs/raw/조사/2026-10-15_338_실험자료/C6/s10_policy.py @@ -0,0 +1,87 @@ +"""C6 — 「경우에 따라서는」 가름 흉내: 열쇠 차례(a → c → b → c2) + 근거 판정 · 발주처로 「바로 씀 / 1순위 후보 / 뒤 후보」. + +차례 누적(앞 파일 모두 기억)에서 판정줄 1순위 · 다섯 안 · 바로 들어간 줄 맞음 · 해로움. +python s10_policy.py +""" + +import sys +from collections import Counter + +from s08_memory import BASE, BY, ITEMS, META, ORDER, basis_of, build, keys_of, recall, truth # noqa: F401 + +sys.stdout.reconfigure(encoding="utf-8") +CHAIN = ("a", "c", "b", "c2") + + +def first_hit(r, memos, chain=CHAIN): + for name in chain: + ans, prov = recall(name, r, memos[name]) + if ans is not None: + return name, ans, prov + return None, None, [] + + +def tier(rule, name, ans, prov, r, target): + """바로 · 1순위 · 뒤 · 없음.""" + if ans is None: + return "없음" + basis = basis_of(r, ans) + same_org = any(META[f][0] == META[target][0] for f, _ in prov) + if rule == "지금(a 바로)": + return "바로" if name == "a" else "없음" + if rule == "a·b 바로(넓히기만)": + return "바로" if name in ("a", "b") else "없음" + if rule == "모든 열쇠 바로": + return "바로" + if rule == "근거 가름": + if basis == "근거 다름": + return "뒤" + return "바로" if name in ("a", "c") else "1순위" + if rule == "근거 가름 + 발주처": + if same_org and name in ("a", "b", "c"): + return "바로" + if basis == "근거 다름": + return "뒤" + return "바로" if name in ("a", "c") else "1순위" + raise ValueError(rule) + + +RULES = ("지금(a 바로)", "a·b 바로(넓히기만)", "모든 열쇠 바로", "근거 가름", "근거 가름 + 발주처") + +if __name__ == "__main__": + print("규칙 | 판정줄 | 1위 전→뒤 | 5안 전→뒤 | 바로 들어감(맞음) | 1순위 후보(맞음) | 뒤 후보(맞음) | 도움 · 해로움") + for rule in RULES: + s = Counter() + for at, target in enumerate(ORDER[1:], 1): + memos = {name: build(ORDER[:at], name) for name in CHAIN} + for r in ITEMS: + if r["파일"] != target or not truth(r["id"]): + continue + t = truth(r["id"]) + base = BASE[r["id"]]["top"] + chain = {"지금(a 바로)": ("a",), "a·b 바로(넓히기만)": ("a", "b")}.get(rule, CHAIN) + name, ans, prov = first_hit(r, memos, chain) + tr = tier(rule, name, ans, prov, r, target) + if tr in ("바로", "1순위"): + top = [ans, *[x for x in base if x != ans]] + elif tr == "뒤": + top = [*base[:1], ans, *[x for x in base[1:] if x != ans]] + else: + top = base + before = bool(base[:1]) and base[0] in t + after = bool(top[:1]) and top[0] in t + s["n"] += 1 + s["전1"] += before + s["뒤1"] += after + s["전5"] += bool(set(base[:5]) & t) + s["뒤5"] += bool(set(top[:5]) & t) + if tr != "없음": + s[tr] += 1 + s[tr + "맞음"] += ans in t + s["도움"] += after and not before + s["해로움"] += before and not after + n = s["n"] + print( + f"{rule} | {n} | {100 * s['전1'] / n:.1f}→{100 * s['뒤1'] / n:.1f} | {100 * s['전5'] / n:.1f}→{100 * s['뒤5'] / n:.1f} " + f"| {s['바로']}({s['바로맞음']}) | {s['1순위']}({s['1순위맞음']}) | {s['뒤']}({s['뒤맞음']}) | {s['도움']} · {s['해로움']}" + ) diff --git a/docs/raw/조사/2026-10-15_338_실험자료/C6/s11_extra.py b/docs/raw/조사/2026-10-15_338_실험자료/C6/s11_extra.py new file mode 100644 index 000000000..c0d3f111a --- /dev/null +++ b/docs/raw/조사/2026-10-15_338_실험자료/C6/s11_extra.py @@ -0,0 +1,28 @@ +"""C6 — 덧셈: ① 한 파일 안 같은 열쇠 다른 답(충돌) ② 차례 누적에서 열쇠 사슬(a→c→b→c2)이 닿는 엑셀 줄 무게.""" +import sys +from collections import defaultdict +from s08_memory import ITEMS, ORDER, KEYS, keys_of, truth, build +sys.stdout.reconfigure(encoding="utf-8") +print("① 한 파일 안 — 정답 있는 줄 둘 넘게 같은 열쇠: 열쇠 묶음 수 · 답이 갈린 묶음") +for name in KEYS: + groups = defaultdict(list) + for r in ITEMS: + t = truth(r["id"]) + if t: + for k in keys_of(name, r): + groups[(r["파일"], k)].append(frozenset(t)) + multi = [v for v in groups.values() if len(v) > 1] + split = [v for v in multi if not frozenset.intersection(*v)] + print(f" {name}: 묶음 {len(multi)} · 갈림 {len(split)}") +print("② 차례 누적 — 줄(고유) · 엑셀 줄(되풀이 셈) 중 앞 파일 기억이 닿는 몫(모두 확정 셈)") +for chain in (("a",), ("a", "b"), ("a", "c"), ("a", "c", "b", "c2")): + u = w = nu = nw = 0 + for at, target in enumerate(ORDER[1:], 1): + memos = {n: build(ORDER[:at], n, all_items=True) for n in chain} + for r in ITEMS: + if r["파일"] != target: + continue + nu += 1; nw += r["줄수"] + if any(k in memos[n] for n in chain for k in keys_of(n, r)): + u += 1; w += r["줄수"] + print(f" {'→'.join(chain)}: 고유 {u}/{nu} ({100*u/nu:.0f}%) · 엑셀 줄 {w}/{nw} ({100*w/nw:.0f}%)") diff --git a/docs/raw/조사/2026-10-15_338_실험자료/C6/s12_combo.py b/docs/raw/조사/2026-10-15_338_실험자료/C6/s12_combo.py new file mode 100644 index 000000000..5ea7eedf9 --- /dev/null +++ b/docs/raw/조사/2026-10-15_338_실험자료/C6/s12_combo.py @@ -0,0 +1,55 @@ +"""C6 — 새 지표 「로직 조합 맞음」: 호표 하나의 로직 줄(정답 있는 줄)이 모두 1순위 맞으면 그 호표 조합 맞음. + +판정 = 사람 ∪ 자동(엄격) · 정답 없는 줄은 셈에서 뺌 · 판정 줄 하나도 없는 호표는 뺌. +바탕 = 지금 맞춤(출처직종 · 씨앗 켬/끔) · 기억 = 차례 누적 + 「근거 가름(+ 발주처)」 규칙(s10). +""" + +import json +import sys +from collections import defaultdict +from pathlib import Path + +from s08_memory import BY, HOPYO, ITEMS, ORDER, build, truth +from s10_policy import CHAIN, first_hit, tier + +sys.stdout.reconfigure(encoding="utf-8") +HERE = Path(__file__).resolve().parent +M = json.loads((HERE / "match.json").read_text(encoding="utf-8")) + + +def tops(base_key, rule=None): + """{id: 1순위} — rule 있으면 차례 누적 기억을 얹음.""" + base = M[base_key] + out = {i: (v["top"][:1] or [None])[0] for i, v in base.items()} + if rule is None: + return out + for at, target in enumerate(ORDER[1:], 1): + memos = {name: build(ORDER[:at], name) for name in CHAIN} + for r in ITEMS: + if r["파일"] != target: + continue + name, ans, prov = first_hit(r, memos) + if tier(rule, name, ans, prov, r, target) in ("바로", "1순위"): + out[r["id"]] = ans + return out + + +def combo_rate(top): + by_file = defaultdict(lambda: [0, 0]) + for (f, h), ids in HOPYO.items(): + judged = [i for i in ids if i in BY and truth(i)] + if not judged: + continue + by_file[f][0] += 1 + by_file[f][1] += all(top.get(i) in truth(i) for i in judged) + total = [sum(v[0] for v in by_file.values()), sum(v[1] for v in by_file.values())] + return by_file, total + + +if __name__ == "__main__": + print("바탕 · 기억 | 호표(판정 줄 있음) | 조합 맞음 | 파일별(맞음/호표)") + for key in ("출처만|켬", "출처직종|켬", "출처직종|끔"): + for rule in (None, "근거 가름", "근거 가름 + 발주처"): + by, tot = combo_rate(tops(key, rule)) + cells = " · ".join(f"{f} {v[1]}/{v[0]}" for f, v in by.items()) + print(f"{key} · {rule or '기억 없음'} | {tot[0]} | {tot[1]} ({100 * tot[1] / tot[0]:.1f}%) | {cells}") diff --git a/docs/raw/조사/2026-10-15_338_실험자료/C6/s13_distinct.py b/docs/raw/조사/2026-10-15_338_실험자료/C6/s13_distinct.py new file mode 100644 index 000000000..60a172768 --- /dev/null +++ b/docs/raw/조사/2026-10-15_338_실험자료/C6/s13_distinct.py @@ -0,0 +1,23 @@ +"""C6 — 조건별 맞음을 「서로 다른 판정 줄」로 다시 셈(짝 · 누적에서 같은 줄을 여러 번 세지 않게).""" +import sys +from collections import defaultdict +from s08_memory import ITEMS, ORDER, META, KEYS, build, recall, truth, basis_of, HAND +sys.stdout.reconfigure(encoding="utf-8") +seen = defaultdict(dict) # (열쇠, 조건) → {(id, 답): 맞음} +for src_set in [[s] for s in ORDER] + [ORDER[:i] for i in range(1, len(ORDER))]: + for name in KEYS: + memo = build(src_set, name) + for r in ITEMS: + if r["파일"] in src_set or not truth(r["id"]): + continue + ans, prov = recall(name, r, memo) + if ans is None: + continue + org = "같은 발주처" if any(META[f][0] == META[r["파일"]][0] for f, _ in prov) else "다른 발주처" + ok = ans in truth(r["id"]) + who = "사람" if r["id"] in HAND else "자동" + for cond in ("모두", org, basis_of(r, ans), f"{org} · {basis_of(r, ans)}", f"판정 {who}"): + seen[(name, cond)][(r["id"], ans)] = ok +for (name, cond), d in sorted(seen.items()): + n = len(d); ok = sum(d.values()); ids = len({i for i, _ in d}) + print(f"{name} | {cond} | 서로 다른 (줄·답) {n} · 맞음 {ok} ({100*ok/n:.0f}%) · 줄 {ids}") diff --git a/docs/raw/조사/2026-10-15_338_실험자료/C7/k0_lib.py b/docs/raw/조사/2026-10-15_338_실험자료/C7/k0_lib.py new file mode 100644 index 000000000..133fb8755 --- /dev/null +++ b/docs/raw/조사/2026-10-15_338_실험자료/C7/k0_lib.py @@ -0,0 +1,205 @@ +"""req226 C7 — C1 · C3 · C4 · C5 · C6 함수를 한 프로세스에 불러 겹쳐 잼(저장소 안 고침 · 읽기만). + +단계 = 지금 → 묶음 ① → + 로직 지문(C4 추천) → + (가) · 조합 풀이(C5) → + 조건부 기억(C6 근거 가름) → + 읽기 넓힘(영월 직종 이음). +정답 · 셈 = C1 s10_all(고유 136 · 정답 126) · 조합 맞음 = C5 c07 T1(덮을 수 있는 83 호표 통째 엄격). +""" + +import json +import sys +from contextlib import contextmanager +from pathlib import Path + +ROOT = Path(r"C:\Program_coding\임도설계 및 견적자동화 프로그램 개발") +HERE = Path(__file__).resolve().parent +PAD = HERE.parent +C1, C3 = PAD.parent / "req225" / "C1", PAD.parent / "req225" / "C3" +C4, C5, C6 = PAD / "C4", PAD / "C5", PAD / "C6" +for p in (ROOT, C1, C3, C4, C5, C6): + if str(p) not in sys.path: + sys.path.insert(0, str(p)) +sys.stdout.reconfigure(encoding="utf-8") +sys.setrecursionlimit(10000) + +import s10_all as S # noqa: E402 C1 — 정답 · 셈 · 지금 +from s04_eval import clause_prefix, hb, hm, patched # noqa: E402 +from s10_all import candidates_nofold, no_seeds # noqa: E402 +from s20_c3_extra import combo # noqa: E402 C3 — 묶음 ① +import s3_match as F4 # noqa: E402 C4 — 로직 지문 +import c04_closed as CL # noqa: E402 C5 — (가) +import c05_solve as SV # noqa: E402 C5 — 조합 풀이 +from c06_lines import PARTS # noqa: E402 +import c07_hopyo as H # noqa: E402 C5 — 조합 맞음 +import s08_memory as M8 # noqa: E402 C6 — 기억 +from s10_policy import CHAIN, first_hit, tier # noqa: E402 + +LABELED = ("동부청영월", "북부청임도", "북부청사방") +C6BY = {r["id"]: r for r in M8.ITEMS} +IDS = list(S.BY) + + +# ── 단계 ────────────────────────────────────────────────────────────── +def now(): + return S.run() + + +def bundle1(): + return combo() + + +_POOLS = {} + + +def pools(ids=IDS): + for i in ids: + if i not in _POOLS: + _POOLS[i] = F4.rank(F4.EXP[i], None, 40) + return _POOLS + + +def finger(rec, beta=1.0, keep_same=True): + """C4 추천 — 「같음」은 묶음 ① 그대로 · 나머지 = 지문 점수 + β × 묶음 ① 차례 덤.""" + P = pools() + out = {} + for i in IDS: + if keep_same and rec[i]["상태"] == "같음": + out[i] = dict(rec[i]) + continue + F4.CUR["id"] = i + out[i] = {"상태": "후보", "top": F4.fuse(None, rec[i], beta, P[i])} + return out + + +def ga(res): + """C5 (가) 좋은 것 — 토질 · 같은 이름 다수 · 조합 안(근거 지킴).""" + return CL.apply_combo(CL.apply_name_vote(CL.apply_soil(res)), "inside", True)[0] + + +def solve(res, cut=0.35, seeds=True): + """C5 조합 풀이 + 섞인 줄 1순위 바꿈 → (줄 결과, 고른 로직 모음).""" + tops, sets = {}, {} + ctx = no_seeds() if not seeds else _null() + with ctx, patched(hb, _clause=clause_prefix), patched(hm, candidates=candidates_nofold): + hm._targets.cache_clear() + hb._ours.cache_clear() + got = hm.targets() + for r in S.ALL: + i = r["id"] + line = {k: r.get(k) for k in ("이름", "규격", "수량", "단위", "단가", "갈래", "참조", "근거")} + top = list(res[i]["top"]) + parts = PARTS.get(i) or [] + chosen = SV.combine((top or [None])[0], parts, line, got, SV.merged(line, got)) + codes = list(dict.fromkeys(c for c, *_ in chosen if c)) + if top and len(codes) > 1 and parts and SV.is_mixed(parts, S.BY[i]) and SV.cover(SV._sum(parts), top[0]) < cut: + rest = [c for c in codes if c != top[0]] + labor = [c for c, _w, _f, _x, step in chosen if c in rest and step in ("몫", "절")] + if labor: + codes = rest + top = [labor[0]] + [c for c in top if c != labor[0]][:4] + tops[i] = {"상태": res[i]["상태"], "top": top} + sets[i] = codes + return tops, sets + + +@contextmanager +def _null(): + yield + + +RULE_CHAIN = {"지금(a 바로)": ("a",), "a·b 바로(넓히기만)": ("a", "b")} + + +def memory(res, sets=None, rule="근거 가름", order=M8.ORDER): + """C6 조건부 기억 — 차례 누적(앞 파일 모두 확정) · 열쇠 a → c → b → c2 · 근거 판정으로 바로 / 1순위 / 뒤.""" + out = {i: dict(v) for i, v in res.items()} + sets = {i: list(v) for i, v in (sets or {i: (v["top"] or [])[:1] for i, v in res.items()}).items()} + log = {} + for at, target in enumerate(order[1:], 1): + if target not in LABELED: + continue + memos = {name: M8.build(order[:at], name) for name in CHAIN} + for r in M8.ITEMS: + i = r["id"] + if r["파일"] != target or i not in res: + continue + name, ans, prov = first_hit(r, memos, RULE_CHAIN.get(rule, CHAIN)) + tr = tier(rule, name, ans, prov, r, target) + top = list(res[i]["top"]) + if tr in ("바로", "1순위"): + old = top[0] if top else None + out[i] = {"상태": "기억", "top": [ans] + [c for c in top if c != ans][:4]} + sets[i] = [ans] + [c for c in sets.get(i, []) if c not in (old, ans)] + elif tr == "뒤": + out[i] = {**res[i], "top": top[:1] + [ans] + [c for c in top[1:] if c != ans][:3]} + if tr != "없음": + log[i] = (name, tr, ans) + return out, sets, log + + +@contextmanager +def read_wide(): + """읽기 넓힘 — 영월(STmate) 근거 직종을 노무비 목록으로 이음(C6 items 근거) · 끝나면 되돌림.""" + keep = {} + for r in S.ALL: + if r["파일"] == "동부청영월": + keep[r["id"]] = r.get("근거") + r["근거"] = dict(C6BY[r["id"]]["근거"]) + try: + yield + finally: + for r in S.ALL: + if r["id"] in keep: + r["근거"] = keep[r["id"]] + + +# ── 셈 ──────────────────────────────────────────────────────────────── +def hopyo(chosen_fn): + """조합 맞음 — C5 T1 · 덮을 수 있는 호표 통째 엄격 (맞음, 호표 수, 칸 %).""" + H.chosen_of = lambda i, pick: set(chosen_fn(i)) - {None} + res = {k: H.judge(k, "x", False) for k in H.HOPYO} + res = {k: v for k, v in res.items() if v} + keys = [k for k, v in res.items() if not v["없음칸"]] + ok = sum(res[k]["엄격"]["통째"] for k in keys) + cell = round(100 * sum(res[k]["엄격"]["칸"] for k in keys) / len(keys), 1) + return ok, len(keys), cell + + +def top1_set(res): + return lambda i: (res[i]["top"] or [None])[:1] + + +def combo_set(res, sets): + """1순위 + 조합 풀이 덧(기계 시간 로직은 뺌 — 정할 것 8 기본안 T1).""" + + def f(i): + top = (res[i]["top"] or [None])[0] + return [c for c in sets.get(i, []) if c and (c == top or c not in SV.MACHINE_SET)] or [top] + + return f + + +def row(name, res, chosen_fn=None): + t = S.table(name, res) + a = t["전체"] + h = hopyo(chosen_fn or top1_set(res)) + return { + "단계": name, + "1위": f"{a['1위 엄격']} / {a['1위 느슨']}", + "5안": f"{a['5안 엄격']} / {a['5안 느슨']}", + "임도·사방·영월 1위": f"{t['북부청임도']['1위 엄격']} · {t['북부청사방']['1위 엄격']} · {t['동부청영월']['1위 엄격']}", + "줄무게 1위 느슨": a["줄 1위 느슨"], + "조합 맞음": f"{round(100 * h[0] / h[1], 1)} ({h[0]}/{h[1]})", + "칸": h[2], + "없음 줄 후보": a["없음인데 후보"], + } + + +def show(r): + print(" | ".join(f"{k} {v}" for k, v in r.items())) + + +def save(name, obj): + (HERE / name).write_text(json.dumps(obj, ensure_ascii=False, indent=1, default=str), "utf-8") + + +def load(name): + return json.loads((HERE / name).read_text(encoding="utf-8")) diff --git a/docs/raw/조사/2026-10-15_338_실험자료/C7/k1_stack.py b/docs/raw/조사/2026-10-15_338_실험자료/C7/k1_stack.py new file mode 100644 index 000000000..eadf545bf --- /dev/null +++ b/docs/raw/조사/2026-10-15_338_실험자료/C7/k1_stack.py @@ -0,0 +1,82 @@ +"""req226 C7 ① 누적 실력 — 같은 정답 · 같은 셈(C1 고유 126 줄 · 조합 맞음 = C5 T1 83 호표). + +python k1_stack.py [켬|끔] → 표 · stack_<켬|끔>.json +""" + +import sys + +import k0_lib as K +from k0_lib import S + +SEEDS = "끔" not in sys.argv[1:] +MIXED = {i for i in S.BY if K.PARTS.get(i) and K.SV.is_mixed(K.PARTS[i], S.BY[i])} + + +def mixfirst(fp, ga1): + """섞인 줄(몫 표시 + 노임만 든 몫 + 장비만 든 몫)은 몫 풀이 먼저(C5 길) · 나머지는 지문 1순위.""" + return {i: dict(ga1[i] if i in MIXED else fp[i]) for i in S.BY} + + +def stages(wide=False): + ctx = K.read_wide() if wide else K._null() + rows, keep = [], {} + with ctx: + if SEEDS: + now, b1 = K.now(), K.bundle1() + else: + with K.no_seeds(): + now = K.now() + b1 = K.bundle1() + fp = K.finger(b1) + ga1 = K.ga(b1) + naive_t, naive_s = K.solve(fp, seeds=SEEDS) + t3, s3 = K.solve(mixfirst(fp, ga1), seeds=SEEDS) + c5_t, c5_s = K.solve(ga1, seeds=SEEDS) + t4, s4, log = K.memory(t3, s3) + tag = " · 읽기 넓힘" if wide else "" + rows.append(K.row("지금" + tag, now)) + rows.append(K.row("묶음 ①" + tag, b1)) + rows.append(K.row("+ 로직 지문" + tag, fp)) + rows.append(K.row("(참고) 묶음 ① + (가) + 조합 풀이 = C5 길" + tag, c5_t, K.combo_set(c5_t, c5_s))) + rows.append(K.row("(참고) + 지문 + 조합 풀이 그냥 겹침" + tag, naive_t, K.combo_set(naive_t, naive_s))) + rows.append(K.row("+ 조합 풀이(섞인 줄 몫 먼저)" + tag, t3, K.combo_set(t3, s3))) + rows.append(K.row("+ 조건부 기억(근거 가름)" + tag, t4, K.combo_set(t4, s4))) + keep = {"지금": now, "묶음①": b1, "지문": fp, "조합": {"tops": t3, "sets": s3}, "기억": {"tops": t4, "sets": s4, "log": log}} + return rows, keep + + +def effect(before, after, ids=None): + """1순위 엄격 — 도움 · 해로움 줄(정답 있는 줄).""" + ids = ids or [i for i in S.BY if S.LAB[i]["정답"]] + ok = lambda t, i: bool(t) and t[0] in set(S.LAB[i]["정답"]) # noqa: E731 + help_ = [i for i in ids if ok(after[i]["top"], i) and not ok(before[i]["top"], i)] + harm = [i for i in ids if ok(before[i]["top"], i) and not ok(after[i]["top"], i)] + return help_, harm + + +if __name__ == "__main__": + rows, keep = stages(False) + rows_w, keep_w = stages(True) + rows.append({**rows_w[-1], "단계": "+ 읽기 넓힘(영월 직종 이음)"}) + for r in rows + rows_w: + K.show(r) + out = {"표": rows, "넓힘 표": rows_w} + for a, b in (("묶음①", "지문"), ("지문", "조합"), ("조합", "기억")): + x = keep[a]["tops"] if "tops" in keep[a] else keep[a] + y = keep[b]["tops"] if "tops" in keep[b] else keep[b] + h, m = effect(x, y) + out[f"{a}→{b}"] = {"도움": h, "해로움": m} + print(f"{a} → {b}: 도움 {len(h)} {h} · 해로움 {len(m)} {m}") + h, m = effect(keep["기억"]["tops"], keep_w["기억"]["tops"]) + out["기억→읽기"] = {"도움": h, "해로움": m} + print(f"기억 → 읽기 넓힘: 도움 {len(h)} {h} · 해로움 {len(m)} {m}") + log = keep["기억"]["log"] + from collections import Counter + + c = Counter((v[0], v[1]) for v in log.values()) + print("기억 닿음(열쇠 · 처리)", dict(c)) + right = Counter(v[1] for i, v in log.items() if S.LAB[i]["정답"] and v[2] in set(S.LAB[i]["정답"])) + print("기억 답 맞음(처리별)", dict(right), "· 정답 없는 줄 닿음", sum(1 for i in log if not S.LAB[i]["정답"])) + out["기억 닿음"] = {f"{k[0]}|{k[1]}": v for k, v in c.items()} + out["기억 맞음"] = dict(right) + K.save(f"stack_{'켬' if SEEDS else '끔'}.json", {**out, "줄": {k: v for k, v in keep.items()}, "줄 넓힘": keep_w}) diff --git a/docs/raw/조사/2026-10-15_338_실험자료/C7/k2_detail.py b/docs/raw/조사/2026-10-15_338_실험자료/C7/k2_detail.py new file mode 100644 index 000000000..4b1dd12bd --- /dev/null +++ b/docs/raw/조사/2026-10-15_338_실험자료/C7/k2_detail.py @@ -0,0 +1,31 @@ +"""req226 C7 ② 겹침 — 기억을 단계마다 얹어 봄 · 맨 위 남는 실패 줄 목록. +python k2_detail.py [켬|끔]""" +import sys +import k0_lib as K +from k0_lib import S +from cats import NOW + +tag = "끔" if "끔" in sys.argv[1:] else "켬" +st = K.load(f"stack_{tag}.json")["줄"] +names = {x["로직"]: x["이름"] for x in K.hm.targets()["로직"]} +have = [i for i in S.BY if S.LAB[i]["정답"]] +ok = lambda t, i: bool(t) and t[0] in set(S.LAB[i]["정답"]) # noqa: E731 +for nm in ("지금", "묶음①", "지문"): + res = st[nm] + m, _, log = K.memory(res) + a = sum(ok(res[i]["top"], i) for i in have) + b = sum(ok(m[i]["top"], i) for i in have) + hp = sum(1 for i in have if ok(m[i]["top"], i) and not ok(res[i]["top"], i)) + hm_ = sum(1 for i in have if ok(res[i]["top"], i) and not ok(m[i]["top"], i)) + print(f"[{tag}] {nm} + 기억: 1위 엄격 {100*a/len(have):.1f} → {100*b/len(have):.1f} · 도움 {hp} · 해로움 {hm_}") +top = st["기억"]["tops"] +fails = [i for i in have if not ok(top[i]["top"], i)] +print(f"[{tag}] 맨 위 남는 1순위 실패 {len(fails)}") +for i in fails: + t = top[i]["top"] + lab = S.LAB[i]["정답"] + rank = next((k + 1 for k, c in enumerate(t[:5]) if c in set(lab)), "-") + loose = "느슨O" if t and t[0] in set(lab) | set(S.LAB[i]["허용"]) else "" + print(f" {i} {S.BY[i]['이름'][:18]:<18} 줄{S.BY[i]['줄수']:>3} | 1위 {names.get(t[0] if t else None, '')[:16]:<16} | 정답 {names.get(lab[0], '')[:14]} (다섯 안 {rank}) {loose} | {(NOW.get(i) or ['-'])[0]}") +none = [i for i in S.BY if not S.LAB[i]["정답"]] +print(f"[{tag}] 우리에 없음 {len(none)} — 1위 후보:", [(i, S.BY[i]['이름'][:10], names.get((top[i]['top'] or [None])[0], '')[:12]) for i in none]) diff --git a/docs/raw/조사/2026-10-15_338_실험자료/C7/k3_vec.py b/docs/raw/조사/2026-10-15_338_실험자료/C7/k3_vec.py new file mode 100644 index 000000000..4d363a302 --- /dev/null +++ b/docs/raw/조사/2026-10-15_338_실험자료/C7/k3_vec.py @@ -0,0 +1,139 @@ +"""req226 C7 ③ C4 지문 점수를 무게 바꿔 끼울 수 있게 미리 셈(채널 · 변형별) → 가림 시험용. + +점수 = Σ 무게 × 채널(단위 · 이름 · 조항 · 조건 · 자재 · 율) + max_변형(무게 인력 × 인력 − 0.6 · 인력 벌 + 무게 장비 × 장비 − 0.4 · 장비 벌) += C4 s3_match.score_pair 와 같은 식(무게만 바꿈). +python k3_vec.py 지문|가벼움 → vec_<…>.npz +""" + +import re +import sys + +import numpy as np + +import k0_lib as K +from fp_common import conds, mtypes, specnums # C4 + +F4 = K.F4 +P = F4.P +CODES = [o["로직"] for o in P] +IDX = {c: k for k, c in enumerate(CODES)} +SEG = np.cumsum([0] + [len(o["vs"]) for o in P])[:-1] +CH = ("단위", "이름", "조항", "조건", "자재", "율") +W0 = dict(F4.W) + + +def chan(e, o): + unit_ok = F4.unit_fit(e["단위"], o["단위"]) + g1, g2 = e["nameg"], o["nameg"] + our = F4.OURC.get(o["로직"]) + c = F4.clause_prefix(e["theirs"], our)[0] if (our and e["theirs"]) else 0.0 + hit, _ = F4.cond_score(e["조건"], o["nc"], o["oc"]) + _, clash = F4.cond_score(e["이름조건"], o["nc"], {}) + mat = 0.0 + if e["matl"]: + mat = sum(1 for ws in e["matl"] if any(a in b or b in a for a in ws for b in o["matw"])) / len(e["matl"]) + base = ( + 1.0 if unit_ok else 0.0, + 2 * len(g1 & g2) / (len(g1) + len(g2)) if g1 and g2 else 0.0, + c * (0.5 if e["옛판"] else 1.0), + hit - clash, + mat, + 1.0 if (e["pctv"] and o["pctv"] and e["pctv"] & o["pctv"]) else 0.0, + ) + vs = [] + for v in o["vs"]: + if e["E"]: + ls, pl = F4.lab_sim(e["E"], e["Eb"], v["L"], unit_ok)[0], 0.0 + else: + ls, pl = 0.0, 1.0 if v["L"] else 0.0 + if e["장비"]: + ms, px = F4.mach_sim(e["장비"], v["X"], o["opt"], unit_ok), 0.0 + else: + ms, px = 0.0, 1.0 if (v["X"] or o["opt"]) else 0.0 + vs.append((ls, pl, ms, px)) + return base, vs + + +def build(elist): + n, L, V = len(elist), len(P), int(SEG[-1] + len(P[-1]["vs"])) + base = np.zeros((n, L, len(CH))) + var = np.zeros((4, n, V)) + for a, e in enumerate(elist): + col = 0 + for b, o in enumerate(P): + bs, vs = chan(e, o) + base[a, b] = bs + for t in vs: + var[:, a, col] = t + col += 1 + return base, var + + +def scores(base, var, W=None): + W = W or W0 + v = W["인력"] * var[0] - 0.6 * var[1] + W["장비"] * var[2] - 0.4 * var[3] + vmax = np.maximum.reduceat(v, SEG, axis=1) + w6 = np.array([W.get(k, 0.0) for k in CH]) + return base @ w6 + vmax + + +RB = F4.RB + + +def fused_top(srow, base_top, beta, n=40): + """F4.fuse 와 같은 차례 — 지문 상위 40(점수 차례) + 묶음 ① 다섯 · 점수 + β × 차례 덤 · 안정 정렬.""" + pool = np.argsort(-srow, kind="stable")[:n] + cand = [int(k) for k in pool] + for c in base_top[:5]: + k = IDX.get(c) + if k is not None and k not in cand: + cand.append(k) + bonus = {IDX[c]: RB.get(r, 0) for r, c in enumerate(base_top[:5]) if c in IDX} + order = sorted(cand, key=lambda k: -(srow[k] + beta * bonus.get(k, 0))) + return [CODES[k] for k in order[:5]] + + +# ── 넓힌 파일 줄 — C6 items 로 가벼운 엑셀 지문(인력 품 · 장비 갈래 · 조항 · 조건 · 율 — 자재 · 장비 시간 · 섞음 밑품 없음) ── +_PCT = re.compile(r"([가-힣]+)의\s*([\d.]+)\s*%") +_OLD = re.compile(r"((? 1 else "지문" + if kind == "지문": # C4 엑셀 지문 136 + ids = list(K.S.BY) + elist = [F4.EXP[i] for i in ids] + else: # 가벼운 지문 — C6 items(호표 쓰임 · 북부청 · 영월 136 도 견줌용으로) + import json + + items = json.loads((K.C6 / "items.json").read_text(encoding="utf-8")) + w = K.load("wide.json") + want = set(K.S.BY) if sys.argv[2] == "사람" else set(w["자동"]) | set(w["씨앗"]) + rows = [r for r in items if r["id"] in want] + ids = [r["id"] for r in rows] + elist = [lite(r) for r in rows] + base, var = build(elist) + tag = kind if kind == "지문" else f"가벼움_{sys.argv[2]}" + np.savez_compressed(K.HERE / f"vec_{tag}.npz", base=base, var=var, ids=np.array(ids)) + print(tag, base.shape, var.shape) diff --git a/docs/raw/조사/2026-10-15_338_실험자료/C7/k4_wide.py b/docs/raw/조사/2026-10-15_338_실험자료/C7/k4_wide.py new file mode 100644 index 000000000..06a0caa6f --- /dev/null +++ b/docs/raw/조사/2026-10-15_338_실험자료/C7/k4_wide.py @@ -0,0 +1,68 @@ +"""req226 C7 ④ 넓힌 파일 줄(STmate 5 · 거창) — 정답 셋 · 지금 · 묶음 ① (읽기 출처만 / 출처직종 · 씨앗 끔 = 처음 보는 셈). + +정답 = C6 그대로: 자동 엄격(사람 정답 없는 새 줄) · 씨앗 짝(씨앗 끔에서만 · 북부청 · 사람 정답 줄 뺌). +python k4_wide.py → wide.json +""" +import json + +import k0_lib as K +from k0_lib import hb, hm, patched, clause_prefix, candidates_nofold, no_seeds +from s04_eval import default_line +from s16_guard import typo_guard +from s06_match import line_of # C6 +from s04_auto import seeds_of # C6 + +ITEMS = json.loads((K.C6 / "items.json").read_text(encoding="utf-8")) +AUTO = json.loads((K.C6 / "auto.json").read_text(encoding="utf-8")) +HAND = set(K.S.LAB) + + +def answers(): + got = hm.targets() # 씨앗 켬 상태로 씨앗 짝을 읽음 + auto, seed = {}, {} + for r in ITEMS: + i = r["id"] + if i in HAND: + continue + a = AUTO.get(i, {}).get("정답") + if a: + auto[i] = [a] + if r["파일"] not in ("북부청임도", "북부청사방"): + s = seeds_of(r, got) + if s: + seed[i] = sorted(s) + return auto, seed + + +def run(rows, variant, fn): + with no_seeds(), patched(hb, _clause=clause_prefix), patched(hm, candidates=candidates_nofold): + hm._targets.cache_clear() + hb._ours.cache_clear() + got = hm.targets() + return {r["id"]: fn(line_of(r, variant), got, {"단산글": r.get("글") or []}) for r in rows} + + +def run_now(rows, variant): + with no_seeds(): + hm._targets.cache_clear() + got = hm.targets() + return {r["id"]: default_line(line_of(r, variant), got, {}) for r in rows} + + +if __name__ == "__main__": + auto, seed = answers() + ids = set(auto) | set(seed) + rows = [r for r in ITEMS if r["id"] in ids] + out = {"자동": auto, "씨앗": seed, "줄": {r["id"]: {"파일": r["파일"], "이름": r["이름"], "호표쓰임": r["호표쓰임"]} for r in rows}} + for v in ("출처만", "출처직종"): + out[f"지금|{v}"] = run_now(rows, v) + out[f"묶음①|{v}"] = run(rows, v, typo_guard) + K.save("wide.json", out) + from collections import Counter + print("자동 새 줄", len(auto), Counter(ITEMS_BY[i]["파일"] for i in auto) if (ITEMS_BY := {r["id"]: r for r in ITEMS}) else "", "· 씨앗 짝", len(seed), Counter(ITEMS_BY[i]["파일"] for i in seed), "· 겹침", len(set(auto) & set(seed))) + for k in ("지금|출처만", "지금|출처직종", "묶음①|출처만", "묶음①|출처직종"): + for nm, ans in (("자동", auto), ("씨앗", seed)): + n = len(ans) + a1 = sum(1 for i in ans if out[k][i]["top"][:1] and out[k][i]["top"][0] in set(ans[i])) + a5 = sum(1 for i in ans if set(out[k][i]["top"][:5]) & set(ans[i])) + print(f"{k} · {nm} {n}: 1위 {100*a1/n:.1f} · 다섯 안 {100*a5/n:.1f}") diff --git a/docs/raw/조사/2026-10-15_338_실험자료/C7/k5_hold.py b/docs/raw/조사/2026-10-15_338_실험자료/C7/k5_hold.py new file mode 100644 index 000000000..4972c3367 --- /dev/null +++ b/docs/raw/조사/2026-10-15_338_실험자료/C7/k5_hold.py @@ -0,0 +1,115 @@ +"""req226 C7 ⑤ 가림 시험 — 문턱 · 무게를 북부청 임도로만 정하고 사방 · 영월 · 넓힌 파일 줄에서 다시 잼. + +손잡이 = C4 무게(인력 · 장비 · 조항 · 이름 · 조건) · β · 「같음 그대로」 · C5 섞인 줄 뺌 문턱(cut) · C6 기억 규칙. +최종 1순위 = 기억(바로 · 1순위) > 섞인 줄 몫 먼저(C5 길 · cut) > 같음 그대로 > 지문(무게 · β). +python k5_hold.py 준비 → hold_prep.json(섞인 줄 C5 길 · 기억 판단) +python k5_hold.py 그물 <인력> → hold_<인력>.json(설정마다 무리별 1순위 · 다섯 안) +""" + +import itertools +import json +import sys + +import numpy as np + +import k0_lib as K +import k3_vec as V +from k0_lib import S + +FULL = np.load(K.HERE / "vec_지문.npz") +LITE = np.load(K.HERE / "vec_가벼움_넓힘.npz") +IDS_F, IDS_W = [str(x) for x in FULL["ids"]], [str(x) for x in LITE["ids"]] +B1 = K.load("st_b1.json") # 묶음 ① · 씨앗 켬(126) +WIDE = K.load("wide.json") +BW = WIDE["묶음①|출처직종"] # 넓힌 줄 · 씨앗 끔 · 읽기 넓힘 뒤 +MIXED = {i for i in S.BY if K.PARTS.get(i) and K.SV.is_mixed(K.PARTS[i], S.BY[i])} +GROUP_OF = {"북부청임도": "임도", "북부청사방": "사방", "동부청영월": "영월"} +RULES = ("지금(a 바로)", "a·b 바로(넓히기만)", "모든 열쇠 바로", "근거 가름", "근거 가름 + 발주처") +CUTS = (0.2, 0.35, 0.5) +BETAS = (0.0, 0.5, 1.0, 2.0, 3.0) + +# (줄, 무리, 정답) 짝 — 126 사람 정답 · 넓힌 자동 20 · 씨앗 짝 43 +PAIRS = [(i, GROUP_OF[S.BY[i]["파일"]], set(S.LAB[i]["정답"])) for i in IDS_F if S.LAB[i]["정답"]] +PAIRS += [(i, "자동", set(v)) for i, v in WIDE["자동"].items()] +PAIRS += [(i, "씨앗", set(v)) for i, v in WIDE["씨앗"].items()] +GROUPS = ("임도", "사방", "영월", "자동", "씨앗") + + +def mem_dec(rule): + """{id: (처리, 답)} — 차례 누적(앞 파일 모두 확정) · 대상 = 126 + 넓힌 줄.""" + items = {r["id"]: r for r in json.loads((K.C6 / "items.json").read_text(encoding="utf-8"))} + want = set(IDS_F) | set(IDS_W) + out = {} + for at, target in enumerate(K.M8.ORDER[1:], 1): + memos = {name: K.M8.build(K.M8.ORDER[:at], name) for name in K.CHAIN} + for i in want: + r = items[i] + if r["파일"] != target: + continue + name, ans, prov = K.first_hit(r, memos, K.RULE_CHAIN.get(rule, K.CHAIN)) + tr = K.tier(rule, name, ans, prov, r, target) + if tr != "없음": + out[i] = (tr, ans) + return out + + +def prep(): + ga1 = K.ga(B1) + c5 = {str(c): {i: v["top"] for i, v in K.solve(ga1, c)[0].items() if i in MIXED} for c in CUTS} + dec = {r: mem_dec(r) for r in RULES} + K.save("hold_prep.json", {"c5": c5, "dec": dec}) + + +def grid(wl): + pre = K.load("hold_prep.json") + n = len(PAIRS) + gi = np.array([GROUPS.index(g) for _, g, _ in PAIRS]) + same = np.array([i in B1 and B1[i]["상태"] == "같음" for i, _, _ in PAIRS]) + same_ok = np.array([bool(B1[i]["top"]) and B1[i]["top"][0] in a if i in B1 else False for i, _, a in PAIRS]) + samew = np.array([i in BW and BW[i]["상태"] == "같음" for i, _, _ in PAIRS]) + samew_ok = np.array([bool(BW[i]["top"]) and BW[i]["top"][0] in a if i in BW else False for i, _, a in PAIRS]) + is126 = np.array([g in ("임도", "사방", "영월") for _, g, _ in PAIRS]) + mixed = np.array([i in MIXED for i, _, _ in PAIRS]) + c5_ok = {c: np.array([bool(pre["c5"][c].get(i)) and pre["c5"][c][i][0] in a if i in MIXED else False for i, _, a in PAIRS]) for c in pre["c5"]} + mem_has, mem_ok = {}, {} + for r, d in pre["dec"].items(): + mem_has[r] = np.array([i in d and d[i][0] in ("바로", "1순위") for i, _, _ in PAIRS]) + mem_ok[r] = np.array([i in d and d[i][1] in a for i, _, a in PAIRS]) + keep_same = np.where(is126, same, samew) + keep_ok = np.where(is126, same_ok, samew_ok) + rowF = {i: a for a, i in enumerate(IDS_F)} + rowW = {i: a for a, i in enumerate(IDS_W)} + out = [] + for wx, wc, wn, wk in itertools.product((1.0, 2.0, 3.0), (0.75, 1.5, 3.0), (0.3, 0.6, 1.2), (0.3, 0.6, 1.2)): + W = {**V.W0, "인력": wl, "장비": wx, "조항": wc, "이름": wn, "조건": wk} + SF = V.scores(FULL["base"], FULL["var"], W) + SW = V.scores(LITE["base"], LITE["var"], W) + for beta in BETAS: + ok1, ok5 = np.zeros(n, bool), np.zeros(n, bool) + for k, (i, g, a) in enumerate(PAIRS): + if g in ("임도", "사방", "영월"): + top = V.fused_top(SF[rowF[i]], B1[i]["top"], beta) + else: + top = V.fused_top(SW[rowW[i]], BW[i]["top"], beta) + ok1[k] = bool(top) and top[0] in a + ok5[k] = bool(set(top[:5]) & a) + for keep in (True, False): + fp1 = np.where(keep & keep_same, keep_ok, ok1) + fp5 = np.where(keep & keep_same, keep_ok, ok5) + for c in pre["c5"]: + mx1 = np.where(mixed, c5_ok[c], fp1) + for r in RULES: + fin = np.where(mem_has[r], mem_ok[r], mx1) + acc1 = np.bincount(gi, weights=fin, minlength=5) / np.bincount(gi, minlength=5) + acc5 = np.bincount(gi, weights=fp5, minlength=5) / np.bincount(gi, minlength=5) + out.append({"W": [wl, wx, wc, wn, wk], "β": beta, "같음": keep, "cut": c, "기억": r, + "1위": [round(100 * x, 1) for x in acc1], "지문5안": [round(100 * x, 1) for x in acc5]}) + K.save(f"hold_{wl:g}.json", out) + print(wl, len(out)) + + +if __name__ == "__main__": + if sys.argv[1] == "준비": + prep() + else: + grid(float(sys.argv[2])) diff --git a/docs/raw/조사/2026-10-15_338_실험자료/C7/k6_sum.py b/docs/raw/조사/2026-10-15_338_실험자료/C7/k6_sum.py new file mode 100644 index 000000000..7440dfb31 --- /dev/null +++ b/docs/raw/조사/2026-10-15_338_실험자료/C7/k6_sum.py @@ -0,0 +1,55 @@ +"""req226 C7 ⑥ 가림 시험 정리 — 임도만 보고 고른 설정들 대 지금 설정 · 무리별 1순위 엄격.""" +import json +import statistics as st +from collections import Counter + +import k0_lib as K + +rows = [] +for w in ("2", "3", "4"): + rows += K.load(f"hold_{w}.json") +G = ("임도", "사방", "영월", "자동", "씨앗") +print("설정 수", len(rows)) + + +def pick(pred): + return [r for r in rows if pred(r)] + + +def show(name, rs): + if not rs: + return + cols = [] + for k, g in enumerate(G): + v = [r["1위"][k] for r in rs] + cols.append(f"{g} {min(v)}~{st.median(v)}~{max(v)}" if len(rs) > 1 else f"{g} {v[0]}") + print(f"{name} (설정 {len(rs)}) | " + " | ".join(cols)) + + +pub = pick(lambda r: r["W"] == [3, 2, 1.5, 0.6, 0.6] and r["β"] == 1.0 and r["같음"] and r["cut"] == "0.35" and r["기억"] == "근거 가름") +pre = pick(lambda r: r["W"] == [3, 2, 1.5, 0.6, 0.6] and r["β"] == 2.0 and r["같음"] and r["cut"] == "0.35" and r["기억"] == "근거 가름") +show("지금 설정(세 파일 보고 고름 · β=1)", pub) +show("미리 정한 값(β=2)", pre) +best1 = max(r["1위"][0] for r in rows) +tied = pick(lambda r: r["1위"][0] == best1) +best5 = max(r["지문5안"][0] for r in tied) +tied2 = [r for r in tied if r["지문5안"][0] == best5] +show(f"임도만 보고 고름 — 임도 1위 최고 {best1}", tied) +show(f" + 동점 가름(임도 지문 다섯 안 {best5})", tied2) +show("모든 설정(참고)", rows) +for k, g in enumerate(G[1:], 1): + v = [r["1위"][k] for r in tied2] + print(f" {g}: 지금 설정 {pub[0]['1위'][k]} → 임도만 고름 가운데 {st.median(v)} (폭 {round(st.median(v) - pub[0]['1위'][k], 1)}) · 가장 나쁨 {min(v)}") +print("임도만 고른 설정의 손잡이 분포:") +for key in ("β", "같음", "cut", "기억"): + print(" ", key, Counter(str(r[key]) for r in tied2).most_common()) +for j, nm in enumerate(("인력", "장비", "조항", "이름", "조건")): + print(" ", nm, Counter(r["W"][j] for r in tied2).most_common()) +# 손잡이마다 — 가림 무리(사방 · 영월 · 자동 · 씨앗) 평균 1순위, 다른 손잡이는 지금 설정 그대로 +base = pub[0] +for key, vals in (("β", (0.0, 0.5, 1.0, 2.0, 3.0)), ("같음", (True, False)), ("cut", ("0.2", "0.35", "0.5")), ("기억", ("지금(a 바로)", "a·b 바로(넓히기만)", "모든 열쇠 바로", "근거 가름", "근거 가름 + 발주처"))): + cells = [] + for v in vals: + r = next(x for x in rows if x["W"] == base["W"] and all(x[k] == (v if k == key else base[k]) for k in ("β", "같음", "cut", "기억"))) + cells.append(f"{v}: " + "/".join(str(x) for x in r["1위"])) + print(f"[{key}] " + " · ".join(cells)) diff --git a/docs/raw/조사/2026-10-15_338_실험자료/C7/k7_wide_stack.py b/docs/raw/조사/2026-10-15_338_실험자료/C7/k7_wide_stack.py new file mode 100644 index 000000000..1e41fc1e1 --- /dev/null +++ b/docs/raw/조사/2026-10-15_338_실험자료/C7/k7_wide_stack.py @@ -0,0 +1,100 @@ +"""req226 C7 ⑦ 넓힌 파일 줄 누적 · 가벼운 지문 눈금 · 딱 맞음 정밀(파일별 · 가림).""" +import json + +import numpy as np + +import k0_lib as K +import k3_vec as V +from k0_lib import S + +F4 = K.F4 +W = K.load("wide.json") +pre = K.load("hold_prep.json") +dec = pre["dec"]["근거 가름"] +decorg = pre["dec"]["근거 가름 + 발주처"] +LITE = np.load(K.HERE / "vec_가벼움_넓힘.npz") +IDS_W = [str(x) for x in LITE["ids"]] +SW = V.scores(LITE["base"], LITE["var"]) +ANS = {"자동": {i: set(v) for i, v in W["자동"].items()}, "씨앗": {i: set(v) for i, v in W["씨앗"].items()}} + + +def rate(res, ans, k=1): + return round(100 * sum(1 for i in ans if set(res[i]["top"][:k]) & ans[i]) / len(ans), 1) + + +def mem(res, d): + out = {} + for i, v in res.items(): + top = list(v["top"]) + if i in d and d[i][0] in ("바로", "1순위"): + a = d[i][1] + top = [a] + [c for c in top if c != a][:4] + out[i] = {**v, "top": top} + return out + + +def fp(base, keep, beta=1.0): + out = {} + for a, i in enumerate(IDS_W): + if keep and base[i]["상태"] == "같음": + out[i] = dict(base[i]) + else: + out[i] = {"상태": "후보", "top": V.fused_top(SW[a], base[i]["top"], beta)} + return out + + +steps = [ + ("지금(읽기 = 336 영월 흉내)", W["지금|출처만"]), + ("묶음 ①", W["묶음①|출처만"]), + ("+ 읽기 넓힘(STmate 직종 이음 · 거창 행 칸 이음)", W["묶음①|출처직종"]), +] +f_keep = fp(W["묶음①|출처직종"], True) +f_free = fp(W["묶음①|출처직종"], False) +steps += [("+ 로직 지문(가벼움 · 같음 그대로)", f_keep), ("+ 로직 지문(가벼움 · 같음도 지문)", f_free)] +steps += [("+ 조건부 기억(근거 가름) — 같음 그대로 위", mem(f_keep, dec)), ("+ 조건부 기억(근거 가름 + 발주처) — 같음 그대로 위", mem(f_keep, decorg))] +print("단계 | 자동 정답 20 1위 · 다섯 안 | 씨앗 짝 43(처음 보는 셈) 1위 · 다섯 안") +for name, res in steps: + print(f"{name} | {rate(res, ANS['자동'])} · {rate(res, ANS['자동'], 5)} | {rate(res, ANS['씨앗'])} · {rate(res, ANS['씨앗'], 5)}") +print("(참고) 지금 위 기억(근거 가름)", rate(mem(W["지금|출처만"], dec), ANS["씨앗"]), "· 묶음① 위", rate(mem(W["묶음①|출처직종"], dec), ANS["씨앗"])) + +# 가벼운 지문 눈금 — 136 줄에서 가벼움 대 C4 지문(같은 무게 · β=1 · 같음 그대로) +L = np.load(K.HERE / "vec_가벼움_사람.npz") +SL = V.scores(L["base"], L["var"]) +b1 = K.load("st_b1.json") +ok = lambda t, i: bool(t) and t[0] in set(S.LAB[i]["정답"]) # noqa: E731 +have = [i for i in S.BY if S.LAB[i]["정답"]] +rowL = {str(i): a for a, i in enumerate(L["ids"])} +lite_res = {i: (b1[i] if b1[i]["상태"] == "같음" else {"top": V.fused_top(SL[rowL[i]], b1[i]["top"], 1.0)}) for i in S.BY} +full_res = K.load("st_fp.json") +print("가벼운 지문 눈금(126) — C4 지문", round(100 * sum(ok(full_res[i]["top"], i) for i in have) / len(have), 1), + "· 가벼움", round(100 * sum(ok(lite_res[i]["top"], i) for i in have) / len(have), 1)) +for f in ("북부청임도", "북부청사방", "동부청영월"): + ids = [i for i in have if S.BY[i]["파일"] == f] + print(" ", f, round(100 * sum(ok(full_res[i]["top"], i) for i in ids) / len(ids), 1), "→", round(100 * sum(ok(lite_res[i]["top"], i) for i in ids) / len(ids), 1)) + +# 딱 맞음 — 파일별(C4 지문 · 126) + 씨앗 짝(가벼움 · 가림) +P = K.pools() +by = {} +for i in S.BY: + if F4.sure(F4.EXP[i], P[i]): + f = S.BY[i]["파일"] + a = by.setdefault(f, [0, 0, 0]) + a[0] += 1 + if S.LAB[i]["정답"]: + a[1] += 1 + a[2] += P[i][0][1] in set(S.LAB[i]["정답"]) +print("딱 맞음(지문 1위 · 정답 있음 · 맞음) 파일별:", by) +items = {r["id"]: r for r in json.loads((K.C6 / "items.json").read_text(encoding="utf-8"))} +cnt = {"자동": [0, 0], "씨앗": [0, 0]} +wrong = [] +for i in IDS_W: + e = V.lite(items[i]) + pool = F4.rank(e, None, 2) + if F4.sure(e, pool): + for g in ("자동", "씨앗"): + if i in ANS[g]: + cnt[g][0] += 1 + cnt[g][1] += pool[0][1] in ANS[g][i] + if pool[0][1] not in ANS[g][i]: + wrong.append((g, i, items[i]["이름"], pool[0][1], sorted(ANS[g][i]))) +print("딱 맞음(가벼움) 넓힌 줄:", cnt, "틀림", wrong) diff --git a/docs/raw/조사/2026-10-15_338_실험자료/C7/k8_sure_same.py b/docs/raw/조사/2026-10-15_338_실험자료/C7/k8_sure_same.py new file mode 100644 index 000000000..fc36e0a5f --- /dev/null +++ b/docs/raw/조사/2026-10-15_338_실험자료/C7/k8_sure_same.py @@ -0,0 +1,78 @@ +"""req226 C7 ⑧ 「딱 맞음이 같음을 이김」 — 같음 줄이라도 지문이 딱 맞음(숫자 일치 · 2위와 차 큼)이고 다른 로직이면 지문. +126(씨앗 켬 · 끔 · 맨 위 누적까지) · 넓힌 줄(가벼움).""" +import json + +import numpy as np + +import k0_lib as K +import k3_vec as V +from k0_lib import S + +F4 = K.F4 +P = K.pools() +SURE = {i: F4.sure(F4.EXP[i], P[i]) for i in S.BY} +have = [i for i in S.BY if S.LAB[i]["정답"]] +ok = lambda t, i: bool(t) and t[0] in set(S.LAB[i]["정답"]) # noqa: E731 +MIXED = {i for i in S.BY if K.PARTS.get(i) and K.SV.is_mixed(K.PARTS[i], S.BY[i])} + + +def finger2(rec): + out = {} + for i in S.BY: + if rec[i]["상태"] == "같음" and not (SURE[i] and P[i][0][1] != rec[i]["top"][0]): + out[i] = dict(rec[i]) + else: + K.F4.CUR["id"] = i + out[i] = {"상태": "후보", "top": F4.fuse(None, rec[i], 1.0, P[i])} + return out + + +for tag in ("켬", "끔"): + st = K.load(f"stack_{tag}.json")["줄"] + b1 = st["묶음①"] + f1, f2 = st["지문"], finger2(b1) + flips = [(i, S.BY[i]["이름"][:12], ok(f1[i]["top"], i), ok(f2[i]["top"], i)) for i in S.BY if f1[i]["top"][:1] != f2[i]["top"][:1]] + print(f"[{tag}] 지문 같음 그대로 → 딱 맞음이 이김: {K.S.table('x', f1)['전체']['1위 엄격']} → {K.S.table('x', f2)['전체']['1위 엄격']} · 바뀐 줄 {flips}") + ga1 = K.ga(b1) + res = {i: dict(ga1[i] if i in MIXED else f2[i]) for i in S.BY} + t3, s3 = K.solve(res, seeds=(tag == "켬")) + t4, s4, _ = K.memory(t3, s3) + K.show(K.row(f"[{tag}] 맨 위 누적(딱 맞음이 같음을 이김)", t4, K.combo_set(t4, s4))) + K.save(f"st_top2_{tag}.json", {"tops": t4, "sets": s4}) + +# 넓힌 줄 +W = K.load("wide.json") +pre = K.load("hold_prep.json") +LITE = np.load(K.HERE / "vec_가벼움_넓힘.npz") +IDS_W = [str(x) for x in LITE["ids"]] +SW = V.scores(LITE["base"], LITE["var"]) +items = {r["id"]: r for r in json.loads((K.C6 / "items.json").read_text(encoding="utf-8"))} +ANS = {"자동": {i: set(v) for i, v in W["자동"].items()}, "씨앗": {i: set(v) for i, v in W["씨앗"].items()}} +base = W["묶음①|출처직종"] +res = {} +for a, i in enumerate(IDS_W): + e = V.lite(items[i]) + pool = F4.rank(e, None, 2) + sure = F4.sure(e, pool) + if base[i]["상태"] == "같음" and not (sure and pool[0][1] != base[i]["top"][0]): + res[i] = dict(base[i]) + else: + res[i] = {"상태": "후보", "top": V.fused_top(SW[a], base[i]["top"], 1.0)} + + +def rate(r, ans, k=1): + return round(100 * sum(1 for i in ans if set(r[i]["top"][:k]) & ans[i]) / len(ans), 1) + + +def mem(r, d): + out = {} + for i, v in r.items(): + top = list(v["top"]) + if i in d and d[i][0] in ("바로", "1순위"): + top = [d[i][1]] + [c for c in top if c != d[i][1]][:4] + out[i] = {**v, "top": top} + return out + + +for nm, r in (("+ 지문(딱 맞음이 같음을 이김)", res), ("+ 기억(근거 가름)", mem(res, pre["dec"]["근거 가름"])), ("+ 기억(근거 가름 + 발주처)", mem(res, pre["dec"]["근거 가름 + 발주처"]))): + print(f"넓힌 줄 {nm} | 자동 {rate(r, ANS['자동'])} · {rate(r, ANS['자동'], 5)} | 씨앗 {rate(r, ANS['씨앗'])} · {rate(r, ANS['씨앗'], 5)}") diff --git a/docs/raw/조사/2026-10-15_338_실험자료/K5/k5_lib.py b/docs/raw/조사/2026-10-15_338_실험자료/K5/k5_lib.py new file mode 100644 index 000000000..3103f7109 --- /dev/null +++ b/docs/raw/조사/2026-10-15_338_실험자료/K5/k5_lib.py @@ -0,0 +1,485 @@ +"""K5 공용 — 「로직 + 옵션」 거꾸로 찾기: 옵션 공간 · 엔진 셈(캐시) · 엑셀 품 맞대기. + +우리 쪽 = C4 `s2_ours_fp.run_one`(M01 엔진 `mf.run` — `Labor_Doc.run_line` 과 같은 읽기 길) 그대로 · 읽기만. +옵션 공간(로직 입력 칸마다 후보): + 고르기(지역 · 보정작업 · 조종원직종 뺌 — 1,438 로직 모두 품을 안 바꿈 · p01) = 모두 + 기계 고르기(EQ 키) = 모두(엑셀 규격 수가 있으면 그 규격에 맞는 것만 · 맞는 것 없으면 모두) + 범위 = 두 끝 + 범위 안의 엑셀 수 + 찾기 조건에 쓰인 수 = 그 표의 줄 값(`master_auto._fits`) + 엑셀 수(표 값과 같은 것만 남음) + 그 밖 수 = 자동값 + 엑셀 계수(같은 이름 · q f E Cm …) + 엑셀 수(이름 · 규격 · 블록 글) — 「품」 칸은 × 결과단위 +조합 = 곱이 CAP 이하면 모두 · 넘으면 엑셀 조건 낱말로 고르기를 좁힘 · 그래도 넘으면 하나씩 바꿈 + 고르기 둘씩. +""" + +import itertools +import json +import math +import re +import sys +import unicodedata +from decimal import Decimal +from pathlib import Path + +HERE = Path(__file__).resolve().parent +SP = HERE.parent.parent +C4 = SP / "req226" / "C4" +for p in (str(C4), str(SP / "req225" / "C1"), str(SP / "req225" / "C3")): + if p not in sys.path: + sys.path.insert(0, p) +sys.stdout.reconfigure(encoding="utf-8") + +import s2_ours_fp as O # noqa: E402 +from fp_common import conds, mtypes, specnums # noqa: E402 + +WHOLE, m01, mau = O.WHOLE, O.m01, O.m01.mau +EQK = re.compile(r"^\d{4}-\d{4}$") +FIXED = ("지역", "보정작업", "조종원직종") +CAP = 1500 +_NUM = re.compile(r"(? set: + """엑셀 이름 · 규격 · 블록 글의 수 — spec 을 주면 그 칸의 단위가 뒤에 붙거나 칸 이름이 앞에 붙은 수만 + (천 자리 쉼표 든 단가는 뺌).""" + out = set() + flat = [unicodedata.normalize("NFKC", str(t or "")) for t in texts] + if spec is None: + pats = [_NUM] + else: + u = unicodedata.normalize("NFKC", str(spec.get("단위") or "")).strip().lower() + vs = UNITS.get(u, (u,) if u and len(u) <= 3 and "/" not in u else ()) + if u == "%": # 「노무비의 2%」 · 「38%」 같은 율이 감률 칸 후보로 드는 것을 막음 — % 칸은 이름 붙은 수만 + vs = () + num = r"(\d+(?:\.\d+)?)" + pats = [re.compile(r"(? 1: + hit = [o for o in opts if any(w in str(o) for w in words) or (len(str(o)) >= 2 and str(o) in flat)] + if hit: + out.append((name, hit, kind)) + continue + out.append((name, opts, kind)) + return out + + +def combos(sp, ex=None, cap=CAP): + """조합 목록 · 어떻게 냈나(모두 / 좁힘 / 하나씩).""" + size = math.prod(len(o) for _, o, _ in sp) if sp else 1 + if size <= cap: + return [dict(zip([n for n, _, _ in sp], vals)) for vals in itertools.product(*[o for _, o, _ in sp])], "모두" + sp2 = narrow(sp, ex) + size2 = math.prod(len(o) for _, o, _ in sp2) + if size2 <= cap: + return [dict(zip([n for n, _, _ in sp2], vals)) for vals in itertools.product(*[o for _, o, _ in sp2])], "좁힘" + # 하나씩 + 고르기 · 표 · 기계 둘씩(수 칸은 하나씩만) + base = {n: o[0] for n, o, _ in sp2} + out = [dict(base)] + for n, o, _ in sp2: + out += [{**base, n: v} for v in o[1:]] + small = [(n, o) for n, o, k in sp2 if k != "수"] + for (n1, o1), (n2, o2) in itertools.combinations(small, 2): + if len(o1) * len(o2) <= 400: + out += [{**base, n1: a, n2: b} for a in o1 for b in o2] + seen, uniq = set(), [] + for c in out: + k = json.dumps(c, sort_keys=True, default=str, ensure_ascii=False) + if k not in seen: + seen.add(k) + uniq.append(c) + return uniq[:cap], "하나씩" + + +_RUN = {} +_ROWS = {} + + +def _row(code): + if code not in _ROWS: + row = WHOLE.logic(code) + by_name = {} + for h in row.get("호표") or []: + by_name.setdefault(h.get("이름"), h) + _ROWS[code] = (row, by_name, O.times_of(row)) + return _ROWS[code] + + +def run(code, given): + """엔진 셈 → {L, X, M, S, mix} · 못 돌면 None(캐시). + 「기계 시간당 기계경비」 로직 = 고른 기계(EQ 칸)의 갈래 · 규격 1 시간(조종원 줄은 기계경비 속이라 뺌).""" + k = (code, json.dumps(given, sort_keys=True, default=str, ensure_ascii=False)) + if k not in _RUN: + row, by_name, times = _row(code) + try: + v = O.run_one(code, row, dict(given), times, by_name) + except Exception: # noqa: BLE001 + v = None + _RUN[k] = v + v = _RUN[k] + row = _row(code)[0] + if v is not None and O.is_mach_logic(row): + auto = _AUTO.setdefault(code, mau.values(WHOLE, row)) + X = [] + for s in row.get("입력") or []: + opts = s.get("고르기") or [] + if opts and all(isinstance(o, str) and EQK.match(o) for o in opts): + c = given.get(s["이름"], auto.get(s["이름"])) + ts, sp = eq_label(c) + X.append((sorted(ts | set(O.FIXED_MACH.get(code, set()))), sorted(sp), 1.0)) + if not X and O.FIXED_MACH.get(code): + X = [(sorted(O.FIXED_MACH[code]), [], 1.0)] + v = {**v, "L": {}, "X": X} + return v + + +_AUTO = {} + + +def sig(v, nd=4): + """품 꼴 서명 — 직종 · 장비(갈래 · 시간) · 자재 · 딸린 로직 수량(유효 nd 자리).""" + if v is None: + return None + r = lambda x: float(f"{x:.{nd}g}") # noqa: E731 + return json.dumps( + [ + sorted((k, r(q)) for k, q in v["L"].items() if q), + sorted((tuple(a), tuple(b), r(q)) for a, b, q in v["X"] if q), + sorted((k, r(q)) for k, q in v["M"].items() if q), + sorted((k, r(q)) for k, q in v["S"].items() if q), + ], + ensure_ascii=False, + ) + + +# ── 맞대기 ────────────────────────────────────────────────────────────── +def near(a, b, t, r=0.0): + """a(엑셀) ≈ b(우리) — 비 t 안 · r = 엑셀 값이 적힌 자릿수의 반올림 폭(끝수 허용).""" + return a is not None and b is not None and a > 0 and b > 0 and abs(a - b) <= max(t * max(a, b), r) + + +def ru(x): + """엑셀 값의 반올림 폭 — 소수 둘째~넷째 자리로 적힌 값만 · 값의 4% 안(0.15 → 0.005 · 0.1 → 0 · 0.109091 → 0).""" + s = repr(float(x)) + if "e" in s or "." not in s: + return 0.0 + d = len(s.split(".")[1].rstrip("0")) + return min(0.5 * 10 ** (-d), 0.04 * float(x)) if 2 <= d <= 4 else 0.0 + + +UNITW = {"mm", "cm", "kg", "ton", "sec", "kgf", "mpa"} + + +def mat_words(name): + """자재 이름 낱말 — 한글 두 자 넘는 덩이 · 영문 세 자 넘는 덩이(괄호 속도) · 단위 낱말 뺌.""" + t = unicodedata.normalize("NFKC", str(name or "")).lower().replace(".", "") + return {w for w in re.findall(r"[가-힣]{2,}|[a-z]{3,}", t) if w not in UNITW} + + +def compare(e, v, t, base=False, rnd=False): + """엑셀 지문 e(C4 꼴) ↔ 우리 셈 v · 허용 t. + → {직종: 같음/다름/없음, 장비: …, 자재: …, 핵심: bool, 덧: [엑셀만 장비 …]} + base=True 면 엑셀 밑품(섞음 · 할증 뺀 품)으로 견줌.""" + if v is None: + return None + E = {k: (x["밑품"] if base else x["품"]) for k, x in e["인력"].items() if x.get("품")} + O_ = {k: q for k, q in v["L"].items() if q and q > 0} + out = {"덧": []} + if E: + out["직종"] = "같음" if set(E) == set(O_) and all(near(E[k], O_[k], t, ru(E[k]) if rnd else 0) for k in E) else ("직종 같음" if set(E) == set(O_) else "다름") + elif O_: + out["직종"] = "우리만" + else: + out["직종"] = "없음" + # 장비 — 같은 갈래끼리 시간 + EX = [m for m in e["장비"] if m.get("시간")] + OX = [(set(a), q) for a, _, q in v["X"] if q] + if EX and OX: + ok = [] + for m in EX: + mt = set(m["갈래"]) + mine = [q for a, q in OX if a & mt] + if not mine: + out["덧"].append("엑셀만 장비 " + "·".join(sorted(mt))) + continue + h = m["밑시간"] if base and m.get("밑시간") else m["시간"] + ok.append(any(near(h, q, t, ru(h) if rnd else 0) for q in mine) or near(h, sum(mine), t, ru(h) if rnd else 0)) + et = set().union(*[set(m["갈래"]) for m in e["장비"]]) + for a, _ in OX: + if not a & et: + out["덧"].append("우리만 장비 " + "·".join(sorted(a))) + out["장비"] = ("같음" if all(ok) else "다름") if ok else "갈래 안 겹침" + elif EX: + out["장비"] = "엑셀만" + elif OX: + out["장비"] = "우리만" + else: + out["장비"] = "없음" + # 자재 — 이름 낱말이 겹치는 것끼리 수량 + EM = [m for m in e["자재"] if m.get("품")] + OM = {k: q for k, q in v["M"].items() if q} + if EM and OM: + ok = [] + for m in EM: + ws = mat_words(m["이름"]) + mine = [q for k, q in OM.items() if ws & mat_words(k) or any(a in b or b in a for a in ws for b in mat_words(k))] + if mine: + x = m["밑품"] if base and m.get("밑품") else m["품"] + ok.append(any(near(x, q, t, ru(x) if rnd else 0) for q in mine)) + out["자재"] = ("같음" if all(ok) else "다름") if ok else "이름 안 겹침" + elif EM: + out["자재"] = "엑셀만" + else: + out["자재"] = "없음" + core = [] + if E: + core.append(out["직종"] == "같음") + elif O_: + core.append(False) + if out["장비"] in ("같음", "다름"): + core.append(out["장비"] == "같음") + if out["자재"] in ("같음", "다름"): + core.append(out["자재"] == "같음") + out["핵심"] = bool(core) and all(core) + out["직종만"] = bool(E) and out["직종"] == "같음" + return out + + +# ── 믹서 갈래(C4 MACH 에 없음) — 엑셀 · 우리 둘 다 같은 함수를 쓰므로 여기서 한 번 더함 ── +import fp_common as _F # noqa: E402 + +for _k in ("믹서", "분무기"): + if not any(k == _k for k, _ in _F.MACH): + _F.MACH.append((_k, (_k,))) + +# ── 엔진 셈 캐시(명령 사이 이어 씀) ───────────────────────────────────── +import pickle # noqa: E402 + +CACHE = HERE / "run_cache.pkl" + + +def load_cache(): + if CACHE.exists(): + _RUN.update(pickle.loads(CACHE.read_bytes())) + return len(_RUN) + + +def save_cache(): + CACHE.write_bytes(pickle.dumps(_RUN)) + + +def plan(code, ex=None): + """(조합들, 어떻게, 움직이는 칸, 안 닿는 칸, 값 묶음). + 곱이 CAP 이하 = 모두 돌림 → 안 닿는 칸(어떤 자리에서도 품 서명이 안 바뀜) · 수 칸의 같은 값 묶음(같은 표 줄)을 끝에 셈. + 곱이 넘침 = 기본값 · 마지막 값 두 기준점에서 하나씩 바꿔 안 닿는 칸을 기본값으로 접고 · 같은 품 수 값을 한 값으로 · 남은 칸을 좁힘.""" + sp = space(code, ex) + size = math.prod(len(o) for _, o, _ in sp) if sp else 1 + if size <= CAP: + names = [n for n, _, _ in sp] + cb = [dict(zip(names, vals)) for vals in itertools.product(*[o for _, o, _ in sp])] + sg = [sig(run(code, c)) for c in cb] + active, inert, cls = [], [], {} + for n, o, k in sp: + if len(o) < 2: + continue + ctx = {} + for c, s_ in zip(cb, sg): + key = json.dumps({x: str(y) for x, y in c.items() if x != n}, sort_keys=True, ensure_ascii=False) + ctx.setdefault(key, {})[str(c[n])] = s_ + keys = sorted(ctx) + prof = {str(v): tuple(ctx[g].get(str(v)) for g in keys) for v in o} + if len(set(prof.values())) == 1: + inert.append(n) + else: + active.append(n) + if k != "고르기": + ids = {} + cls[n] = {v: ids.setdefault(pf, len(ids)) for v, pf in prof.items()} + return cb, "모두", active, inert, cls + base = {n: o[0] for n, o, _ in sp} + alt = {n: o[-1] for n, o, _ in sp} + active, inert = [], [] + for n, o, k in sp: + if len(o) == 1: + continue + s0 = {sig(run(code, {**base, n: v})) for v in o} + s1 = {sig(run(code, {**alt, n: v})) for v in o} if len(s0) == 1 else s0 + if len(s0) > 1 or len(s1) > 1: + if k != "고르기": # 같은 표 줄에 드는 수(0 · 35 …)는 한 값으로 — 두 기준점 품이 같으면 같은 값 + keep, seen = [], set() + for v in o: + key = (sig(run(code, {**base, n: v})), sig(run(code, {**alt, n: v}))) + if key not in seen: + seen.add(key) + keep.append(v) + o = keep + active.append((n, o, k)) + else: + inert.append((n, o, k)) + cb, how = combos(active, ex) + cb = [{**base, **c} for c in cb] + return cb, how, [n for n, _, _ in active], [n for n, _, _ in inert], {} + + +def vary(hits, act, cls): + """맞은 조합들 사이에서 값이 갈리는 칸 — 수 칸은 같은 값 묶음(같은 표 줄)이면 한 값.""" + out = [] + for n in act: + vals = {cls.get(n, {}).get(str(g.get(n)), str(g.get(n))) for g in hits} + if len(vals) > 1: + out.append(n) + return out + + +def add(vs): + """복합 — 몫 벡터 합.""" + vs = [v for v in vs if v is not None] + if not vs: + return None + out = {"L": {}, "X": [], "M": {}, "S": {}, "mix": {}} + for v in vs: + for k in ("L", "M", "S"): + for a, q in v[k].items(): + out[k][a] = out[k].get(a, 0.0) + q + out["X"] += v["X"] + return out + + +def gap(e, v, base=False): + """닮음 거리(작을수록 가까움) — 직종 묶음 어긋남 1 · 품은 직종마다 |ln 비| 평균 · 장비 시간 같은 갈래끼리.""" + if v is None: + return 99.0 + E = {k: (x["밑품"] if base else x["품"]) for k, x in e["인력"].items() if x.get("품")} + O_ = {k: q for k, q in v["L"].items() if q and q > 0} + d = 0.0 + if E or O_: + keys = set(E) | set(O_) + d += len(set(E) ^ set(O_)) / len(keys) + both = [min(1.0, abs(math.log(E[k] / O_[k]))) for k in set(E) & set(O_)] + d += (sum(both) / len(both)) if both else 1.0 + EX = [m for m in e["장비"] if m.get("시간")] + OX = [(set(a), q) for a, _, q in v["X"] if q] + if EX and OX: + hs = [] + for m in EX: + mine = [q for a, q in OX if a & set(m["갈래"])] + h = m["밑시간"] if base and m.get("밑시간") else m["시간"] + hs.append(min(1.0, min(abs(math.log(h / q)) for q in mine)) if mine else 1.0) + d += 0.5 * sum(hs) / len(hs) + return d + + +def csig(e, v, nd=4): + """엑셀이 보이는 몫만의 품 서명 — 직종 전부 · 엑셀 갈래와 겹치는 장비 시간 · 엑셀 이름과 겹치는 자재 수량.""" + if v is None: + return None + r = lambda x: float(f"{x:.{nd}g}") # noqa: E731 + et = set().union(*[set(m["갈래"]) for m in e["장비"]]) if e["장비"] else set() + em = [mat_words(m["이름"]) for m in e["자재"] if m.get("품")] + + def named(k): + w = mat_words(k) + return any(ws & w or any(a in b or b in a for a in ws for b in w) for ws in em) + + return json.dumps( + [ + sorted((k, r(q)) for k, q in v["L"].items() if q), + sorted((tuple(a), r(q)) for a, _, q in v["X"] if q and set(a) & et), + sorted((k, r(q)) for k, q in v["M"].items() if q and named(k)), + ], + ensure_ascii=False, + ) + + +def strong(e, c): + """딱 맞음(강) — 핵심 맞음 + 엑셀 몫을 다 덮음: 장비 갈래가 양쪽 같음(엑셀만 · 우리만 장비 없음) · + 자재 다름 없음 · 「보통인부 하나뿐 · 장비 없음」 줄 아님(같은 품 로직이 수십이라 우연 맞음).""" + if not c or not c.get("핵심"): + return False + if c.get("장비") in ("엑셀만", "우리만", "갈래 안 겹침") or any(("엑셀만 장비" in d or "우리만 장비" in d) for d in c.get("덧") or []): + return False + if c.get("자재") == "다름": + return False + E = [k for k, x in e["인력"].items() if x.get("품")] + if E == ["보통인부"] and not any(m.get("시간") for m in e["장비"]): + return False + return True diff --git a/docs/raw/조사/2026-10-15_338_실험자료/K5/k5_s1_targets.py b/docs/raw/조사/2026-10-15_338_실험자료/K5/k5_s1_targets.py new file mode 100644 index 000000000..b81710754 --- /dev/null +++ b/docs/raw/조사/2026-10-15_338_실험자료/K5/k5_s1_targets.py @@ -0,0 +1,123 @@ +"""K5 ① 대상 줄 — 정답 로직이 알려진 엑셀 줄의 지문(C4 꼴: 인력 품 · 밑품 · 장비 시간 · 자재 수량 · 계수 · 조건 · 글). + +사람 정답 126(C1 labels · labels_y — 지문은 C4 excel_fp 그대로) + +새 파일 51(C7 wide — 자동 정답 20 · 씨앗 짝 43 · 지문은 C6 STmate 블록 「자원」 L · X · M · S 와 items 품 · +거창은 C4 `north` 식 풀기로 블록 글을 풂). +python k5_s1_targets.py → targets.json +""" + +import json +import re +from collections import Counter + +import k5_lib as K +from fp_common import conds, mtypes, specnums + +import s1_excel_fp as X1 # C4 엑셀 지문(식 풀기 · 북부청 꼴) +from labels import L +from labels_y import LY + +SP, C4 = K.SP, K.C4 +EX = {e["id"]: e for e in json.loads((C4 / "excel_fp.json").read_text(encoding="utf-8"))} +WIDE = json.loads((SP / "req226/C7/wide.json").read_text(encoding="utf-8")) +ITEMS = {r["id"]: r for r in json.loads((SP / "req226/C6/items.json").read_text(encoding="utf-8"))} +DATA = {"봉화": "봉화", "울진대흥": "대흥설계", "울진소광": "소광", "영덕계류": "계류보전", "동부청영월": "영월", "거창": "거창"} +COMPOSITE = { + **{i: ["GC000206", "GC000207"] for i in ("N003", "N019", "N045", "S055", "S059", "Y010")}, + "N050": ["GC000705", "GC000714", "GC000712"], +} +_D = {} + + +def data(f): + if f not in _D: + _D[f] = json.loads((SP / f"req226/C6/data_{DATA[f]}.json").read_text(encoding="utf-8")) + return _D[f] + + +def stmate(r): + d = data(r["파일"]) + b = d["블록"].get(r["코드"]) or {} + fp = X1.empty({**r, "근거": r["근거"]}) + fp["글"] = [str(t) for t in b.get("글") or []][:60] + fp["조건"] = {k: sorted(v) for k, v in conds(" ".join([r["이름"], r["규격"]] + fp["글"][:4])).items()} + fp["이름조건"] = {k: sorted(v) for k, v in conds(f"{r['이름']} {r['규격']}").items()} + fp["옛판"] = bool(X1._OLD.search(" ".join(fp["글"][:4]))) + for t in fp["글"]: + for k, v in X1._COEF.findall(t): + fp["계수"].setdefault(k, float(v)) + rates = X1.rates_in(f"{r['이름']} {r['규격']}") + lab_rate = min(rates) if len(rates) >= 2 else None + mach_rate = max(rates) if len(rates) >= 2 else None + for t, q in (r.get("품") or {}).items(): + fp["인력"][t] = {"품": q, "밑품": q / lab_rate if lab_rate else q, "율": [[lab_rate, "섞음"]] if lab_rate else [], "어떻게": ["QTY"]} + seen = Counter() + for x in b.get("자원") or []: + kind, nm, sp, q = x.get("갈래"), x.get("이름") or "", x.get("규격") or "", x.get("품") + if not q: + continue + key = (x.get("코드"), round(q, 9)) + seen[key] += 1 + if seen[key] > 1: # 노무비 · 재료비 · 경비 세 줄이 같은 품 — 한 번만 + continue + ts = mtypes(f"{nm} {sp}") + if kind == "X" or (kind == "S" and ts): + fp["장비"].append({"갈래": sorted(ts), "규격": sorted(specnums(sp) | specnums(nm)), "시간": q, + "밑시간": q / mach_rate if mach_rate else q, "율": [], "절": "0", "글": f"{nm} {sp}"}) # fmt: skip + elif kind in ("M", "S"): + fp["자재"].append({"이름": f"{nm} {sp}".strip(), "품": q, "밑품": q}) + return fp + + +def geochang(r): + b = data("거창")["블록"].get(r["코드"]) or {} + item = {**r, "단산글": [b.get("머리") or r["이름"]] + [str(t) for t in b.get("글") or []]} + return X1.north(item) + + +def answers(i): + if i in L or i in LY: + lab = {**L, **LY}[i] + return lab["정답"], lab.get("허용", []), "사람" + got = [] + for d in (WIDE["자동"], WIDE["씨앗"]): + for k in d.get(i) or []: + if k not in got: + got.append(k) + kind = "자동+씨앗" if i in WIDE["자동"] and i in WIDE["씨앗"] else "자동" if i in WIDE["자동"] else "씨앗" + return got, [], kind + + +def main(): + out = [] + for i, e in EX.items(): + ans, allow, kind = answers(i) + if not ans: + continue + out.append({**e, "정답": ans, "허용": allow, "무리": kind, "복합": COMPOSITE.get(i)}) + for i in WIDE["줄"]: + if i in EX: + continue + r = ITEMS[i] + fp = geochang(r) if r["파일"] == "거창" else stmate(r) + ans, allow, kind = answers(i) + comp = ans if set(ans) == {"GC000206", "GC000207"} else None + out.append({**fp, "id": i, "파일": r["파일"], "정답": ans, "허용": allow, "무리": kind, "복합": comp, "줄수": r.get("줄수")}) + for e in out: + for one in e["인력"].values(): + one["품"] = round(one["품"], 6) + one["밑품"] = round(one["밑품"], 6) + (K.HERE / "targets.json").write_text(json.dumps(out, ensure_ascii=False, indent=0, default=str), "utf-8") + c = Counter(e["무리"] for e in out) + print("대상", len(out), dict(c)) + print("인력 있음", sum(1 for e in out if e["인력"]), "· 장비 시간", sum(1 for e in out if any(m.get("시간") for m in e["장비"])), + "· 자재 수량", sum(1 for e in out if any(m.get("품") for m in e["자재"])), "· 복합", sum(1 for e in out if e["복합"])) # fmt: skip + for e in out: + if e["무리"] != "사람": + print(" ", e["id"], e["이름"], e["규격"], "| 인력", {k: v["품"] for k, v in e["인력"].items()}, + "| 장비", [(m["갈래"], round(m["시간"], 4)) for m in e["장비"] if m.get("시간")], + "| 자재", [(m["이름"][:14], round(m["품"], 4)) for m in e["자재"] if m.get("품")][:3], "| 정답", e["정답"]) # fmt: skip + + +if __name__ == "__main__": + main() diff --git a/docs/raw/조사/2026-10-15_338_실험자료/K5/k5_s2_answer.py b/docs/raw/조사/2026-10-15_338_실험자료/K5/k5_s2_answer.py new file mode 100644 index 000000000..92540b647 --- /dev/null +++ b/docs/raw/조사/2026-10-15_338_실험자료/K5/k5_s2_answer.py @@ -0,0 +1,302 @@ +"""K5 ② 옵션 거꾸로 찾기 — 정답 로직(복합은 몫 합)의 옵션 조합을 모두(넘치면 엑셀 조건 낱말로 좁혀) 엔진으로 셈 → +엑셀 품(직종 · 장비 시간 · 자재 수량)과 같은 조합: 하나만 / 여럿(옵션 못 가름) / 없음 / 엔진 못 셈 · 허용 0.5% · 3%. +python k5_s2_answer.py <시작> <끝> → ans_<시작>.json (캐시 run_cache.pkl 이어 씀) +python k5_s2_answer.py merge → answer.json +""" + +import itertools +import json +import math +import sys +import time +from decimal import Decimal + +import k5_lib as K +from fp_common import specnums + +TOLS = (0.005, 0.03) +MODES = (("0.005", 0.005, False), ("R", 0.005, True), ("0.03", 0.03, False)) # R = 0.5% + 엑셀 자릿수 반올림 허용 +RANK = {"하나만": 0, "여럿(같은 품)": 1, "여럿(다른 품)": 2, "없음": 3, "못 셈": 4} + + +def ex_of(e): + texts = [e["이름"], e.get("규격") or ""] + [str(t) for t in e.get("글") or []] + sp = set() + for m in e["장비"]: + sp |= set(m.get("규격") or []) + sp |= specnums(f"{e['이름']} {e.get('규격') or ''}") + return { + "글": texts, + "글머리": texts[:8], + "계수": e.get("계수") or {}, + "규격수": sp, + "조건": e.get("조건") or {}, + "품값": {Decimal(str(x["품"])) for x in e["인력"].values() if x.get("품")}, + } + + +def plans(codes, ex): + """몫마다 (조합, 어떻게, 움직임, 안 닿음) → 복합이면 곱(넘치면 몫마다 하나씩).""" + ps = {k: K.plan(k, ex) for k in codes} + if len(codes) == 1: + k = codes[0] + cb, how, act, ine, cls = ps[k] + return [({k: c}) for c in cb], how, {k: act}, {k: ine}, {k: cls} + pools = [[(k, c) for c in ps[k][0]] for k in codes] + size = 1 + for p in pools: + size *= len(p) + if size <= 4000: + out = [dict(x) for x in itertools.product(*pools)] + how = "모두" + else: # 몫마다 하나씩(나머지 몫은 첫 조합) + first = {k: ps[k][0][0] for k in codes} + out = [dict(first)] + [{**first, k: c} for k in codes for c in ps[k][0][1:]] + how = "하나씩" + return out, how, {k: ps[k][2] for k in codes}, {k: ps[k][3] for k in codes}, {k: ps[k][4] for k in codes} + + +def vec(given): + vs = [K.run(k, c) for k, c in given.items()] + if any(v is None for v in vs): + return None + return vs[0] if len(vs) == 1 else K.add(vs) + + +def judge(e, codes): + ex = ex_of(e) + cbs, how, act, ine, cls = plans(codes, ex) + rows = [] + for g in cbs: + v = vec(g) + rows.append((g, v, K.sig(v))) + ran = [r for r in rows if r[1] is not None and (r[1]["L"] or r[1]["X"] or r[1]["M"])] + out = {"로직": codes, "조합": len(cbs), "어떻게": how, "움직임": act, "안닿음": ine, "돈 조합": len(ran), "못 돎": sum(1 for r in rows if r[1] is None)} + if not ran: + out["판정"] = {m: "못 셈" for m, _, _ in MODES} + return out + for mk, t, rnd in MODES: + hit = [(g, v, s) for g, v, s in ran if (K.compare(e, v, t, rnd=rnd) or {}).get("핵심")] + lab = [(g, v, s) for g, v, s in ran if (K.compare(e, v, t, rnd=rnd) or {}).get("직종만")] + base = [(g, v, s) for g, v, s in ran if (K.compare(e, v, t, base=True, rnd=rnd) or {}).get("핵심")] + sigs = {K.csig(e, v) for _, v, _ in hit} + vary = sorted({f"{k}.{n}" for k in codes for n in K.vary([g[k] for g, _, _ in hit], act[k], cls[k])}) + if not hit: + verdict = "없음" + elif len(sigs) > 1: + verdict = "여럿(다른 품)" + elif vary: + verdict = "여럿(같은 품)" + else: + verdict = "하나만" + out.setdefault("판정", {})[mk] = verdict + out[f"맞음{mk}"] = { + "조합": len(hit), + "서명": len(sigs), + "전체서명": len({s for _, _, s in hit}), + "갈리는 칸": vary, + "직종만 맞음": len(lab), + "밑품 맞음": len(base), + "보기": [{k: {n: str(x) for n, x in c.items() if n in act[k]} for k, c in g.items()} for g, _, _ in hit[:3]], + "기본값 맞음": any(all(all(str(c.get(n)) == str(ex_k) for n, ex_k in _defaults(k).items() if n in act[k]) for k, c in g.items()) for g, _, _ in hit), + "덧": sorted({d for _, v, _ in hit[:20] for d in (K.compare(e, v, t, rnd=rnd) or {}).get("덧", [])}), + } + if len(codes) == 1: # 지금 M02 규격 읽기(spec_inputs)로 낸 입력값 — 같은 품이 나오나 + k = codes[0] + try: + vals, unread = K.O.hm.spec_inputs({"이름": e["이름"], "규격": e.get("규격"), "단위": e.get("단위")}, K.O.hm._logic_specs(k)) + except Exception: # noqa: BLE001 + vals, unread = {}, ["(읽기 오류)"] + v = K.run(k, {**_defaults(k), **vals}) + out["지금 읽기"] = {"입력": {n: str(x) for n, x in vals.items()}, "못읽음": unread, + **{m: bool((K.compare(e, v, t, rnd=rnd) or {}).get("핵심")) for m, t, rnd in MODES}} # fmt: skip + if not out["맞음0.03"]["조합"] and not out["맞음R"]["조합"]: + out["풀이"] = explain(e, ran, codes, act) + best = min(ran, key=lambda r: K.gap(e, r[1])) + bb = min(ran, key=lambda r: K.gap(e, r[1], base=True)) + out["가장 가까움"] = {"입력": {k: {n: str(x) for n, x in c.items() if n in act[k]} for k, c in best[0].items()}, "거리": round(K.gap(e, best[1]), 4), + "우리": _show(best[1]), "견줌3": K.compare(e, best[1], 0.03), "밑품거리": round(K.gap(e, bb[1], base=True), 4), + "밑품견줌3": K.compare(e, bb[1], 0.03, base=True), "밑품우리": _show(bb[1])} # fmt: skip + return out + + +def _scale(v, kl, kx=None): + return {**v, "L": {k: q * kl for k, q in v["L"].items()}, "X": [(a, b, q * (kx if kx is not None else kl)) for a, b, q in v["X"]]} + + +def _mean(vs, harmonic=False): + """같은 꼴(직종 묶음 · 장비 수 같음) 벡터들의 평균 — 조화 평균은 일 시공량 평균(1 / 평균(1 / 품)).""" + if len({tuple(sorted(v["L"])) for v in vs}) != 1 or len({len(v["X"]) for v in vs}) != 1: + return None + f = (lambda xs: len(xs) / sum(1 / x for x in xs) if all(xs) else 0.0) if harmonic else (lambda xs: sum(xs) / len(xs)) # noqa: E731 + L = {k: f([v["L"][k] for v in vs]) for k in vs[0]["L"]} + X = [(a, b, f([v["X"][i][2] for v in vs])) for i, (a, b, _) in enumerate(vs[0]["X"])] + return {"L": L, "X": X, "M": vs[0]["M"], "S": vs[0]["S"], "mix": {}} + + +def explain(e, ran, codes, act, t=0.03): + """「없음」 풀이 — 섞기 전(우리 섞음 비율 풂) · 한 배수 · 옵션 둘 합 · 옵션 평균(산술 · 조화).""" + got = {} + for g, v, _ in ran: # 우리 로직의 섞음(인력비율 · 기계비율)을 풀면 같나 + ri, rm = (v.get("mix") or {}).get("인력비율"), (v.get("mix") or {}).get("기계비율") + if ri and 0 < ri < 100: + u = _scale(v, 100 / ri, (100 / rm) if rm and 0 < rm < 100 else 1.0) + if (K.compare(e, u, t) or {}).get("직종만"): + got["섞기 전 맞음"] = f"인력 {ri:g}% → 100%" + break + got.update(ratio(e, ran, act, t)) + pool = sorted(ran, key=lambda r: K.gap(e, r[1]))[:120] + for tt in (0.005, t): # 0.5% 에서 맞으면 그 값(우연 맞음 덜함) · 아니면 3% + for (g1, v1, _), (g2, v2, _) in itertools.combinations(pool, 2): + if (K.compare(e, K.add([v1, v2]), tt) or {}).get("직종만"): + got["옵션 둘 합"] = {"값": [_opt(g1, act), _opt(g2, act)], "허용": tt} + break + if "옵션 둘 합" in got: + break + for tt in (0.005, t): + _avg(e, ran, codes, act, tt, got) + if "옵션 평균" in got: + got["옵션 평균"]["허용"] = tt + break + return got + + +def _avg(e, ran, codes, act, t, got): + if len(codes) == 1: + k0 = codes[0] + for n in act[k0]: + groups = {} + for g, v, _ in ran: + rest = json.dumps({x: str(y) for x, y in g[k0].items() if x != n}, sort_keys=True, ensure_ascii=False) + groups.setdefault(rest, []).append((g, v)) + for members in groups.values(): + for size in (2, 3, 4): + for sub in itertools.combinations(members, size): + for hm_ in (False, True): + m = _mean([v for _, v in sub], hm_) + if m and (K.compare(e, m, t) or {}).get("직종만"): + got["옵션 평균"] = {"칸": n, "값": [str(g[k0].get(n)) for g, _ in sub], "꼴": "조화(일 시공량 평균)" if hm_ else "산술"} + break + if "옵션 평균" in got: + break + if "옵션 평균" in got: + break + if "옵션 평균" in got: + break + if "옵션 평균" in got: + break + + +def _ratios(e, v): + """엑셀/우리 비 — 직종(묶음 같을 때) · 같은 갈래 장비 시간. 직종 묶음이 다르면 None.""" + E = {k: x["품"] for k, x in e["인력"].items() if x.get("품")} + O_ = {k: q for k, q in v["L"].items() if q} + if E and set(E) != set(O_): + return None + rs = [E[k] / O_[k] for k in E] + for m in e["장비"]: + if not m.get("시간"): + continue + mine = [q for a, _, q in v["X"] if q and set(a) & set(m["갈래"])] + if mine: + rs.append(m["시간"] / mine[0]) + return rs or None + + +def ratio(e, ran, act, t=0.03): + """한 배수 — 모든 몫(직종 · 장비 시간)이 같은 배수 k(3% 안 · k ≠ 1)인 조합. + k 고르기: 엑셀 글의 수 · 율로 설명되는 k(÷6 · ×1.2 · ×0.1 …) → 몫 수 많은 것 → 1 에 가까운 것.""" + import re + import unicodedata + + text = unicodedata.normalize("NFKC", " ".join([e["이름"], e.get("규격") or ""] + [str(x) for x in e.get("글") or []])) + divs = {float(n) for n in re.findall(r"[÷/]\s*\(?\s*(\d+(?:\.\d+)?)", text) if float(n) > 0} + muls = {float(n) for n in re.findall(r"[×*x]\s*(\d+(?:\.\d+)?)(?!\s*[,\d])", text) if 0 < float(n) < 50} + pcts = {float(n) / 100 for n in re.findall(r"(\d+(?:\.\d+)?)\s*%", text) if 0 < float(n) < 100} + rates = {float(r[0]) for x in e["인력"].values() for r in x.get("율") or [] if r and r[0]} | pcts + cands = [] + for g, v, _ in ran: + rs = _ratios(e, v) + if not rs or max(rs) / min(rs) > 1 + t: + continue + k = sum(rs) / len(rs) + if abs(k - 1) <= t: + continue + why = next((f"÷ {n:g}" for n in divs if abs(k * n - 1) <= 0.01), None) + why = why or next((f"× {n:g}" for n in muls if abs(k - n) <= 0.01 * max(k, n)), None) + why = why or next((f"율 {r:g}" for r in rates if abs(k - r) <= 0.01 * max(k, r) or abs(k * r - 1) <= 0.01), None) + cands.append((why is not None, len(rs), -abs(math.log(k)), k, why, g)) + if not cands: + return {} + cands.sort(key=lambda c: c[:3], reverse=True) + ok, n, _, k, why, g = cands[0] + return {"한 배수": round(k, 4), "몫 수": n, "배수 까닭": why, "배수 입력": _opt(g, act)} + + +def _opt(g, act): + return {k: {n: str(x) for n, x in c.items() if n in act[k]} for k, c in g.items()} + + +_DEF = {} + + +def _defaults(k): + if k not in _DEF: + sp = K.space(k) + _DEF[k] = {n: o[0] for n, o, _ in sp} + return _DEF[k] + + +def _show(v): + if v is None: + return None + return {"L": {k: round(q, 5) for k, q in v["L"].items() if q}, "X": [[a, round(q, 5)] for a, _, q in v["X"] if q], "M": {k: round(q, 5) for k, q in v["M"].items() if q}} + + +def line(e): + alts = [e["복합"]] if e.get("복합") else [[k] for k in e["정답"]] + got = [] + for codes in alts: + if not all(K.WHOLE.logic(k) for k in codes): + got.append({"로직": codes, "판정": {m: "못 셈" for m, _, _ in MODES}, "까닭": "로직 없음"}) + continue + got.append(judge(e, codes)) + best = {m: min(got, key=lambda g: RANK[g["판정"][m]]) for m, _, _ in MODES} + allow = {} + for k in e.get("허용") or []: + if K.WHOLE.logic(k): + j = judge(e, [k]) + allow[k] = j["판정"] + return {"id": e["id"], "무리": e["무리"], "이름": e["이름"], "규격": e.get("규격"), "단위": e.get("단위"), "정답": e["정답"], "갈래": got, + "판정": {t: best[t]["판정"][t] for t in best}, "판정로직": {t: best[t]["로직"] for t in best}, "허용판정": allow, + "엑셀": {"L": {k: x["품"] for k, x in e["인력"].items()}, "Lb": {k: x["밑품"] for k, x in e["인력"].items()}, + "X": [[m["갈래"], m.get("시간"), m.get("밑시간")] for m in e["장비"] if m.get("시간")], "M": [[m["이름"], m["품"]] for m in e["자재"] if m.get("품")], + "율": [r for x in e["인력"].values() for r in x.get("율") or []], "옛판": e.get("옛판"), "출처": e.get("출처")}} # fmt: skip + + +def main(): + T = json.loads((K.HERE / "targets.json").read_text(encoding="utf-8")) + if sys.argv[1:] == ["merge"]: + out = [] + for p in sorted(K.HERE.glob("ans_*.json"), key=lambda p: int(p.stem.split("_")[1])): + out += json.loads(p.read_text(encoding="utf-8")) + (K.HERE / "answer.json").write_text(json.dumps(out, ensure_ascii=False, indent=0, default=str), "utf-8") + print("합", len(out)) + return + a, b = int(sys.argv[1]), int(sys.argv[2]) + n0 = K.load_cache() + t0 = time.time() + out = [] + for e in T[a:b]: + out.append(line(e)) + if time.time() - t0 > 70: # 명령 하나 120 초 안 — 남은 줄은 다음 명령 + break + K.save_cache() + (K.HERE / f"ans_{a}.json").write_text(json.dumps(out, ensure_ascii=False, indent=0, default=str), "utf-8") + print("끝", a, a + len(out), "캐시", n0, "→", len(K._RUN), f"{time.time() - t0:.1f}s") + for r in out: + print(" ", r["id"], r["이름"][:16], r["판정"], r["판정로직"]["0.03"], [(g["조합"], g["어떻게"]) for g in r["갈래"]]) + + +if __name__ == "__main__": + main() diff --git a/docs/raw/조사/2026-10-15_338_실험자료/K5/k5_s4_sum12.py b/docs/raw/조사/2026-10-15_338_실험자료/K5/k5_s4_sum12.py new file mode 100644 index 000000000..1f82a73af --- /dev/null +++ b/docs/raw/조사/2026-10-15_338_실험자료/K5/k5_s4_sum12.py @@ -0,0 +1,236 @@ +"""K5 ④ 할 일 1 · 2 정리 — 옵션 거꾸로 찾기 표 · 「없음」 나눔. +python k5_s4_sum12.py → 표 찍음 · sum12.json +""" + +import json +import re +import unicodedata +from collections import Counter, defaultdict + +import k5_lib as K + +A = json.loads((K.HERE / "answer.json").read_text(encoding="utf-8")) +T = {e["id"]: e for e in json.loads((K.HERE / "targets.json").read_text(encoding="utf-8"))} +UNIT = {x["로직"]: x.get("단위") for x in K.O.hm.targets()["로직"]} +V = ("하나만", "여럿(같은 품)", "여럿(다른 품)", "없음", "못 셈") +import s10_all as C1 # noqa: E402 (사람 정답 줄의 엑셀 줄 수) + +ROWS = {i: C1.BY[i].get("줄수") or 1 for i in C1.BY} + + +def rows_of(i): + return ROWS.get(i) or T[i].get("줄수") or 1 + + +def split_same(r, t): + """여럿(같은 품) 나눔 — 이름만 다름(모든 몫 같음) / 엑셀에 안 보이는 몫(폼타이 · 기계 규격)이 갈림.""" + g = pick(r, t) + m = g.get(f"맞음{t}") or {} + return "이름만 다름" if m.get("전체서명", 1) <= 1 else "엑셀에 안 보이는 몫 갈림" +COUNT = {"개소", "개", "본", "ea", "조", "매", "대", "기", "공"} +HOUR = {"hr", "시간", "h"} + + +def unit_fit(a, b): + if K.O.hm.fits(a, b): + return True + na = re.sub(r"[\s/]", "", str(a or "")).lower() + for nb in [re.sub(r"[\s/]", "", x).lower() for x in str(b or "").split("·")]: + if na == nb or (na in COUNT and nb in COUNT) or (na in HOUR and nb in HOUR): + return True + return False + + +def group(r): + if r["무리"] == "사람": + return {"N": "사람 · 북부청 임도", "S": "사람 · 북부청 사방", "Y": "사람 · 동부청 영월"}[r["id"][0]] + return "새 파일 · " + r["무리"] + + +def pick(r, t="0.03"): + return next(g for g in r["갈래"] if g["로직"] == r["판정로직"][t]) + + +def flat(e): + return re.sub(r"\s+", "", unicodedata.normalize("NFKC", " ".join([e["이름"], e.get("규격") or ""] + [str(x) for x in e.get("글") or []]))) + + +def plausible(e, found): + """옵션 합 · 평균이 엑셀 글로 받쳐지나 — 갈리는 칸 값의 한글 낱말(두 자+)이 글에 있음.""" + text = flat(e) + if "칸" in found: # 평균 + vals = found["값"] + else: # 합 — 두 조합에서 다른 값 + a, b = found["값"] + vals = [] + for k in a: + for n in a[k]: + if a[k][n] != b[k].get(n): + vals += [a[k][n], b[k][n]] + if not vals: + return False + for v in vals: + ws = re.findall(r"[가-힣]{2,}", unicodedata.normalize("NFKC", str(v))) + if not ws or not any(w in text for w in ws): + return False + return True + + +# 원문(2026 건설 · 2025 산림) 대조로 확인한 줄 — 까닭 · 근거(어느 쪽이 원문과 같나) +CHECKED = { + "U012": ("옛 판 값", "건설 6-2-3 조립 Ⅱ-2 시공량 2026 = 1.1 ton(우리) · 엑셀 1.0 ton 옛 값"), + "Y014": ("옛 판 값", "건설 7-2-2 찰붙임('26 보완) 35cm이하 30㎡/일 = 우리 · 엑셀 석공 0.05 는 옛 값"), + "C016": ("옛 판 값", "건설 2-6-1 동바리('26 보완) 2.5m이하 50공㎥ = 우리 0.06(간격 0.8m 초과 0.054) · 엑셀 0.05 = 옛 60공㎥"), + "B031": ("옛 판 값", "건설 2-6-1 동바리('26 보완) = 우리 · 엑셀 0.054 · 0.021 은 2024 파일 옛 값(2-2-4 손율 인용)"), + "B032": ("옛 판 값", "건설 2-7-1 비계('26 보완) 10m이하 55㎡ = 우리 · 엑셀 0.05 · 0.02 옛 값"), + "N032": ("옛 판 값", "건설 3-5-1 고르기('25 보완) 연질토 250㎡ = 우리 0.032 · 엑셀 0.0276 = 옛 290㎡"), + "N040": ("할증 · 횟수 붙임", "건설 1-6-2 A-Type 20㎝ 100㎥(우리 그대로) × 1.2 = 산림 12-6 ⑧ 종단경사 20% 할증을 건설 로직에 붙임"), + "S072": ("단위 환산", "건설 6-7-2 원문 본당(우리 그대로 · 관경 1000) ÷ 6 m/본 = 엑셀 m 당"), + "S083": ("단위 환산", "건설 6-7-2 본당(관경 500) ÷ 6 m/본"), + "Y115": ("단위 환산", "건설 6-7-2 본당(관경 800) ÷ 6 m/본 · 크레인 0.25/6 을 0.04 로 반올림"), + "N017": ("직종 다름(더함 · 이름)", "건설 6-3-10 다웰바 형틀목공 0.043 · 보통인부 0.009 = 우리 · 엑셀은 철근 가공(6-2) 철근공을 더함"), + "N015": ("자재만 다름", "건설 6-3-4 품 0.07 · 0.03 = 우리 · 엑셀이 문양판 수량에 1/0.72 를 붙임(원문엔 없음)"), + "C018": ("옵션을 둘 넘게 섞음(합 · 평균)", "지주 + 표지판 두 옵션 합이 0.5% 안 — 숫자가 북부청 N051 과 같음(글에 「지주」 낱말만 없음)"), + "N039": ("묶음(다른 일을 한 줄에)", "잔디붙임(건설 4-1-1) 조경공 0.0067 그대로 + 떼꽂이(산림 5-23-3) 보통인부를 더함"), + "S079": ("쪼갬(우리 한 로직을 두 줄로)", "원형돌망태(산림 13-11-1) 한 로직을 조립설치 · 채움 두 줄로 나눔"), + "S080": ("쪼갬(우리 한 로직을 두 줄로)", "원형돌망태(산림 13-11-1) 한 로직을 조립설치 · 채움 두 줄로 나눔"), + "N020": ("할증 · 횟수 붙임", "산림 13-2-3 큰돌 채집 × 품할증 0.75 × 발생석 50% · 개 → ㎡(4 개/㎡)"), + "S094": ("할증 · 횟수 붙임", "산림 13-2-3 큰돌 채집 × 품할증 0.75 · 개 → ㎡"), + "U015": ("다른 책 로직 숫자와 같음", "건설 1-6-2 B-Type 20㎝ 4/50 · 2/50 = 0.08 · 0.04 × 두께 0.2 = 0.016 · 0.008 — 씨앗은 산림 GF000162"), + "Y027": ("옛 판 값", "건설 6-7-1 플륨관('25 보완) 원문은 굴착기 편성 = 우리 · 엑셀(2024)은 크레인 · 다른 시공량(옛 값 추정)"), + "B026": ("옛 판 값", "건설 4-1-1 잔디붙임('24 보완) 평떼 150㎡ = 우리 0.0267 · 엑셀(2024) 0.0291 옛 값"), + "B010": ("옛 판 값", "산림 13-3-2(2025) 75mm 미만 0.019 · 굴착기 0.076 = 우리 · 엑셀(2024) 0.018 · 0.07 옛 값"), + "B017": ("옛 판 값", "건설 6-1-2 기계비빔 무근 0.15 · 0.46 = 우리(GF000156 같은 숫자) · 엑셀(2024) 0.03 · 0.31 옛 값(추정)"), + "Y028": ("옛 판 값", "건설 6-1-1('24 보완) 무근 장비사용 63㎥ = 우리 · 엑셀(2024) 0.09 · 0.02 · 굴착기 0.31 옛 값(추정)"), + "C013": ("옛 판 값", "건설 6-1-1('24 보완) 철근 장비사용 55㎥ = 우리 0.0545 · 엑셀 0.07 · 0.02 옛 값(추정)"), +} +OURC = K.O.hb.ours() +VERIFY_NOTE = "원문 확인" + + +def why_none(r): + """「없음」(3% · R 모두) 한 줄의 까닭(차례대로) + 덧 꼬리표.""" + e = T[r["id"]] + g = pick(r) + p = g.get("풀이") or {} + best = g.get("가장 가까움") or {} + c3 = best.get("견줌3") or {} + m3 = g.get("맞음0.03") or {} + rates = r["엑셀"]["율"] + tags = [] + if c3.get("장비") == "엑셀만" or any("엑셀만 장비" in d for d in (c3.get("덧") or [])): + tags.append("엑셀이 장비 일을 더함") + if r["id"] in CHECKED: + cat, note = CHECKED[r["id"]] + return cat, tags + [VERIFY_NOTE], note + allow = [k for k, v in r["허용판정"].items() if v["0.03"] != "없음" or v.get("R") != "없음"] + if allow: + return "다른 책 로직 숫자와 같음", tags, f"허용 {allow[0]} 이 맞음" + if m3.get("밑품 맞음"): + notes = {n for _, n in rates} + kind = "섞음(엑셀이 인력 % · 기계 % 를 곱함)" if notes & {"섞음", "%"} else "할증 · 횟수 붙임" + return kind, tags, f"율 {rates[:2]}" + if "섞기 전 맞음" in p: + return "섞음 비율을 엑셀이 바꿈(우리 로직 섞음 고정)", tags, p["섞기 전 맞음"] + for key in ("옵션 평균", "옵션 둘 합"): + if key in p and plausible(e, p[key]): + return "옵션을 둘 넘게 섞음(합 · 평균)", tags, json.dumps(p[key], ensure_ascii=False)[:160] + E = r["엑셀"]["L"] + O_ = (best.get("우리") or {}).get("L") or {} + books = {b for b, _ in (r["엑셀"]["출처"] or [])} + ours = {(OURC.get(k) or {}).get("책") for k in g["로직"]} + if "한 배수" in p and p.get("배수 까닭") and (p.get("몫 수", 0) >= 2 or str(p["배수 까닭"]).startswith("율")): + code = g["로직"][0] + if not unit_fit(e.get("단위"), UNIT.get(code)) and str(p["배수 까닭"]).startswith("÷"): + return "단위 환산", tags, f"배수 {p['한 배수']} ({p['배수 까닭']}) · 단위 {e.get('단위')} ↔ {UNIT.get(code)} · {p.get('배수 입력')}" + return "할증 · 횟수 붙임", tags, f"배수 {p['한 배수']} ({p['배수 까닭']}) · {p.get('배수 입력')}" + if books and not (books & ours): + return "다른 책 숫자(짝 · 정답은 다른 책 로직)", tags, f"엑셀 인용 {sorted(books)} ↔ 우리 {sorted(ours)}" + if E and set(E) != set(O_): + return "직종 다름(더함 · 이름)", tags, f"엑셀 {sorted(E)} ↔ 우리 {sorted(O_)}" + if E and c3.get("직종") == "같음": + return "장비 · 자재만 다름", tags, f"장비 {c3.get('장비')} · 자재 {c3.get('자재')}" + if not E: + return "장비만 줄 — 시간 · 갈래 다름", tags, f"엑셀 {[(a, round(h, 4)) for a, h, _ in r['엑셀']['X'][:2]]} ↔ 우리 {(best.get('우리') or {}).get('X')}" + if r["엑셀"]["옛판"]: + return "옛 판 값", tags, f"엑셀 글에 옛 판 표시 · 출처 {r['엑셀']['출처']}" + if "엑셀이 장비 일을 더함" in tags: + return "묶음(다른 일을 한 줄에)", tags, f"엑셀 {E} ↔ 우리 {O_}" + return "계수 다름 — 까닭 못 가림", tags, f"엑셀 {E} ↔ 우리 {O_}" + + +def main(): + groups = defaultdict(list) + for r in A: + groups[group(r)].append(r) + order = ["사람 · 북부청 임도", "사람 · 북부청 사방", "사람 · 동부청 영월", "새 파일 · 자동", "새 파일 · 자동+씨앗", "새 파일 · 씨앗"] + out = {"표1": {}, "없음": [], "반올림": [], "갈리는 칸": {}, "기본값": {}, "지금읽기": {}} + print("== 표 1 — 옵션 거꾸로 찾기(정답 로직 · 고유 줄)") + rows = [(n, groups[n]) for n in order] + [("사람 126", [r for r in A if r["무리"] == "사람"]), ("새 파일 51", [r for r in A if r["무리"] != "사람"]), + ("모두 177", A)] # fmt: skip + for name, rs in rows: + line = {} + for t in ("0.005", "R", "0.03"): + c = Counter(r["판정"][t] for r in rs) + w = Counter() + for r in rs: + w[r["판정"][t]] += rows_of(r["id"]) + line[t] = {v: c.get(v, 0) for v in V} + line[t + "줄무게"] = {v: w.get(v, 0) for v in V} + for t in ("0.005", "R", "0.03"): + line[t + "같은품나눔"] = dict(Counter(split_same(r, t) for r in rs if r["판정"][t] == "여럿(같은 품)")) + out["표1"][name] = {"n": len(rs), **line} + f = lambda d: " · ".join(f"{d[v]}" for v in V) # noqa: E731 + wt = lambda t: f"{sum(line[t + '줄무게'][v] for v in V[:3])}/{sum(line[t + '줄무게'].values())}" # noqa: E731 + print(f"{name:<18} n={len(rs):>3} | 0.5% {f(line['0.005'])} | R {f(line['R'])} | 3% {f(line['0.03'])} | 줄무게 맞음 {wt('0.005')} · {wt('R')} · {wt('0.03')} | 같은품 {line['R같은품나눔']}") + # 맞은 줄 — 기본값 그대로 맞음 · 지금 M02 규격 읽기로 맞음 · 옵션을 바꿔야 맞음 + for t in ("0.005", "R", "0.03"): + ok = [r for r in A if r["판정"][t] in V[:3]] + d = Counter() + for r in ok: + g = pick(r, t) + m = g.get(f"맞음{t}") or {} + now = (g.get("지금 읽기") or {}).get(t) + d["기본값 그대로 맞음"] += bool(m.get("기본값 맞음")) + d["지금 M02 규격 읽기로 맞음"] += bool(now) + d["옵션을 바꿔야 맞음(기본값 아님)"] += not m.get("기본값 맞음") + d["옵션 바꿔야 + 지금 읽기 못 맞춤"] += (not m.get("기본값 맞음")) and not now + out["기본값"][t] = {"맞은 줄": len(ok), **d} + print(t, "맞은 줄", len(ok), dict(d)) + # 여럿 — 갈리는 칸 + for t in ("0.005", "R", "0.03"): + cnt = Counter() + for r in A: + if r["판정"][t].startswith("여럿"): + g = pick(r, t) + m = g.get(f"맞음{t}") or {} + for n in m.get("갈리는 칸") or []: + cnt[n.split(".", 1)[1]] += 1 + out["갈리는 칸"][t] = dict(cnt.most_common()) + print(t, "여럿 줄 갈리는 칸", dict(cnt.most_common(20))) + # 반올림 — 3% 맞음 · 0.5% 없음 + for r in A: + if (r["판정"]["0.03"] in V[:3] or r["판정"]["R"] in V[:3]) and r["판정"]["0.005"] == "없음": + g = pick(r) + b = (g.get("가장 가까움") or {}).get("우리") + out["반올림"].append({"id": r["id"], "이름": r["이름"], "규격": r["규격"], "로직": g["로직"], "엑셀": r["엑셀"]["L"], "엑셀X": r["엑셀"]["X"][:2], "우리": b}) + print("반올림(3% 만 맞음)", len(out["반올림"])) + for x in out["반올림"]: + print(" ", x["id"], x["이름"][:14], x["로직"], x["엑셀"], [(a, round(h, 4)) for a, h, _ in x["엑셀X"]], "| 우리", x["우리"]["L"] if x["우리"] else None, (x["우리"] or {}).get("X")) + # 없음 나눔 + cats = Counter() + for r in A: + if r["판정"]["0.03"] != "없음" or r["판정"]["R"] != "없음": + continue + cat, tags, note = why_none(r) + cats[cat] += 1 + out["없음"].append({"id": r["id"], "무리": r["무리"], "이름": r["이름"], "규격": r["규격"], "로직": pick(r)["로직"], "까닭": cat, "덧": tags, "보기": note}) + print("== 없음(3%) 나눔", sum(cats.values()), dict(cats.most_common())) + for x in out["없음"]: + print(" ", x["id"], x["무리"][:2], x["이름"][:16], x["로직"], "|", x["까닭"], "|", x["보기"][:110], x["덧"]) + out["없음셈"] = dict(cats) + (K.HERE / "sum12.json").write_text(json.dumps(out, ensure_ascii=False, indent=1, default=str), "utf-8") + + +if __name__ == "__main__": + main() diff --git a/docs/raw/조사/2026-10-15_338_실험자료/K5/k5_s5_rank.py b/docs/raw/조사/2026-10-15_338_실험자료/K5/k5_s5_rank.py new file mode 100644 index 000000000..d026a8b47 --- /dev/null +++ b/docs/raw/조사/2026-10-15_338_실험자료/K5/k5_s5_rank.py @@ -0,0 +1,121 @@ +"""K5 ⑤ 맞춤 방법 흉내 — 정답을 모르는 채 (로직 + 옵션) 짝을 엑셀 품 닮음으로 줄 세움 · 금액 안 씀. + +1 단계(거친 순위) = C4 지문 변형(`s3_match.P` · 1,438 로직)을 품 채널(인력 3 · 장비 2 · 자재 1 · 단위 0.5)로만 셈. +2 단계(옵션 찾기) = 1 단계 위 K 로직만 K5 옵션 공간(엑셀 조건 낱말 · 수로 좁힘 · CAP)을 엔진으로 돌려 + 0.5% · 반올림 허용(R) · 3% 「딱 맞음」과 거리. +C4 지문만(`score_pair` 기본 무게) 점수도 같이 냄 — 견줌 · 섞기용. +python k5_s5_rank.py <시작> <끝> → rank_<시작>.json · merge → rank.json +""" + +import json +import sys +import time + +import k5_lib as K +import k5_s2_answer as A2 + +import s3_match as S # C4 — P(우리 지문) · ex_prep · score_pair (같은 프로세스 · 읽기만) + +TOPK = 15 +STAGE_CAP = 400 +QW = {"인력": 3.0, "장비": 2.0, "자재": 1.0, "단위": 0.5} +MODES = A2.MODES + + +def stage_b(e, code): + ex = A2.ex_of(e) + try: + sp = K.space(code, ex) + except Exception: # noqa: BLE001 + return {"수준": 9, "거리": 99.0} + import math + + if math.prod(len(o) for _, o, _ in sp) > 20000: # 너무 큰 공간은 좁힌 하나씩만 + pass + old = K.CAP + K.CAP = STAGE_CAP + try: + cb, how, act, ine, cls = K.plan(code, ex) + finally: + K.CAP = old + best = {"수준": 9, "약수준": 9, "거리": 99.0} + hits = {m: [] for m, _, _ in MODES} + lab3 = 0 + for g in cb: + v = K.run(code, g) + if v is None or not (v["L"] or v["X"] or v["M"]): + continue + d = K.gap(e, v) + lvl = weak = 9 + for i, (m, t, rnd) in enumerate(MODES): + c = K.compare(e, v, t, rnd=rnd) or {} + if c.get("핵심"): + weak = min(weak, i) + if K.strong(e, c): + hits[m].append((g, K.csig(e, v))) + lvl = min(lvl, i) + lab3 += bool((K.compare(e, v, 0.03) or {}).get("직종만")) + if (lvl, weak, d) < (best["수준"], best["약수준"], best["거리"]): + best = {"수준": lvl, "약수준": weak, "거리": round(d, 4), "입력": {n: str(x) for n, x in g.items() if n in act}, + "견줌": K.compare(e, v, 0.03), "직종같음": set(v["L"]) == {k for k, x in e["인력"].items() if x.get("품")} and bool(v["L"])} # fmt: skip + best["맞음수"] = {m: len(h) for m, h in hits.items()} + best["서명수"] = {m: len({s for _, s in h}) for m, h in hits.items()} + best["갈리는 칸"] = {m: K.vary([g for g, _ in h], act, cls) for m, h in hits.items() if h} + best["직종만3"] = lab3 + best["조합"] = len(cb) + best["어떻게"] = how + return best + + +def line(e): + ep = S.ex_prep(e) + coarse, full = [], {} + for o in S.P: + s, ch, v = S.score_pair(ep, o, QW) + coarse.append((s, o["로직"])) + full[o["로직"]] = S.score_pair(ep, o)[0] + coarse.sort(key=lambda x: -x[0]) + top = [k for _, k in coarse[:TOPK]] + fp_rank = sorted(full, key=lambda k: -full[k]) + b = {k: stage_b(e, k) for k in top} + return { + "id": e["id"], + "무리": e["무리"], + "정답": e["정답"], + "허용": e.get("허용") or [], + "복합": e.get("복합"), + "거친": [[k, round(s, 3)] for s, k in coarse[:TOPK]], + "지문만": fp_rank[:15], + "지문점수": {k: round(full[k], 3) for k in set(fp_rank[:15]) | set(top)}, + "옵션": b, + "정답거친차례": next((i + 1 for i, (_, k) in enumerate(coarse) if k in set(e["정답"])), None), + } + + +def main(): + T = json.loads((K.HERE / "targets.json").read_text(encoding="utf-8")) + if sys.argv[1:] == ["merge"]: + out = [] + for p in sorted(K.HERE.glob("rank_*.json"), key=lambda p: int(p.stem.split("_")[1])): + out += json.loads(p.read_text(encoding="utf-8")) + (K.HERE / "rank.json").write_text(json.dumps(out, ensure_ascii=False, indent=0, default=str), "utf-8") + print("합", len(out), len({r["id"] for r in out})) + return + a, b = int(sys.argv[1]), int(sys.argv[2]) + n0 = K.load_cache() + t0 = time.time() + out = [] + for e in T[a:b]: + t1 = time.time() + out.append(line(e)) + print(e["id"], f"{time.time() - t1:.1f}s", flush=True) + if time.time() - t0 > 70: # 명령 하나 120 초 안 — 남은 줄은 다음 명령 + break + K.save_cache() + (K.HERE / f"rank_{a}.json").write_text(json.dumps(out, ensure_ascii=False, indent=0, default=str), "utf-8") + print("끝", a, a + len(out), "캐시", n0, "→", len(K._RUN), f"{time.time() - t0:.1f}s") + + +if __name__ == "__main__": + sys.setrecursionlimit(10000) + main() diff --git a/docs/raw/조사/2026-10-15_338_실험자료/K5/k5_s6_sum34.py b/docs/raw/조사/2026-10-15_338_실험자료/K5/k5_s6_sum34.py new file mode 100644 index 000000000..8b721f9e9 --- /dev/null +++ b/docs/raw/조사/2026-10-15_338_실험자료/K5/k5_s6_sum34.py @@ -0,0 +1,191 @@ +"""K5 ⑥ 할 일 3 · 4 정리 — (로직 + 옵션) 줄 세우기 수치 · 화면 「맞음」 표시 기준 안. +python k5_s6_sum34.py → 표 찍음 · sum34.json +""" + +import json +from collections import Counter, defaultdict + +import k5_lib as K + +import s3_match as S + +R = json.loads((K.HERE / "rank.json").read_text(encoding="utf-8")) +T = {e["id"]: e for e in json.loads((K.HERE / "targets.json").read_text(encoding="utf-8"))} +A = {r["id"]: r for r in json.loads((K.HERE / "answer.json").read_text(encoding="utf-8"))} +C4 = json.loads((K.C4 / "out_match.json").read_text(encoding="utf-8"))["줄"] + + +def single(e): + """숫자 하나뿐 줄 — 직종 하나 · 장비 없음 또는 장비 시간 하나 · 직종 없음(기계 1 시간 줄은 뺌). 같은 숫자 로직이 많아 우연 맞음.""" + E = [k for k, x in e["인력"].items() if x.get("품")] + X = [m for m in e["장비"] if m.get("시간")] + if len(E) + len(X) != 1: + return False + return not (X and abs(X[0]["시간"] - 1.0) < 1e-9) + + +def lvl(r, k): + b = r["옵션"].get(k) + if b is None: + return 9 + return 9 if single(T[r["id"]]) else b["수준"] + + +def clause_ok(r, code): + e = T[r["id"]] + our = S.OURC.get(code) + theirs = [list(x) for x in e.get("출처") or []] + return bool(our and theirs and S.clause_prefix(theirs, our)[0] >= 0.9) + + +def order_P(r): + """품만 — 2 단계 로직: 딱 맞음(강) 수준 → 약한 맞음 → 거리 → 1 단계 점수.""" + sc = {k: s for k, s in r["거친"]} + b = r["옵션"] + return sorted(b, key=lambda k: (lvl(r, k), b[k]["약수준"], b[k]["거리"], -sc.get(k, 0))) + + +def order_P0(r): + return [k for k, _ in r["거친"]] + + +def order_F(r, lv=1): + """지문 + 딱 맞음 먼저 — 2 단계에서 딱 맞음(강, 수준 ≤ lv) 로직을 앞에(지문 점수 차례) · 나머지는 지문만 차례.""" + b, fs = r["옵션"], r["지문점수"] + first = sorted([k for k in b if lvl(r, k) <= lv], key=lambda k: -fs.get(k, -99)) + return first + [k for k in r["지문만"] if k not in first] + + +def order_C(r): + return r["지문만"] + + +def ok_top(order, S_): + return bool(order) and order[0] in S_ + + +def metrics(rows, fn): + n = len(rows) + c = Counter() + for r in rows: + o = fn(r) + Sx, Lx = set(r["정답"]), set(r["정답"]) | set(r["허용"]) + b = r["옵션"].get(o[0]) if o else None + c["1위 엄격"] += ok_top(o, Sx) + c["1위 느슨"] += ok_top(o, Lx) + c["5안 엄격"] += bool(set(o[:5]) & Sx) + c["5안 느슨"] += bool(set(o[:5]) & Lx) + opt = ok_top(o, Sx) and b is not None and lvl(r, o[0]) <= 1 + c["1위 옵션까지(R)"] += opt + c["1위 옵션까지(3%)"] += ok_top(o, Sx) and b is not None and lvl(r, o[0]) <= 2 + c["1위 옵션 하나로"] += opt and not (b.get("갈리는 칸") or {}).get("R") + return {k: round(100 * v / n, 1) for k, v in c.items()} | {"n": n} + + +def display(r, fn=order_F): + """화면 표시 갈래 — 1 위 (로직 + 옵션) 기준.""" + o = fn(r) + b = r["옵션"].get(o[0]) if o else None + exact = [k for k in r["옵션"] if lvl(r, k) <= 1] + if b and lvl(r, o[0]) <= 1: + return "맞음(로직 하나)" if len(exact) == 1 else "맞음(로직 여럿 — 고름)" + if b and lvl(r, o[0]) == 2: + return "거의 맞음(3% 안)" + if b and b.get("직종같음") and clause_ok(r, o[0]): + return "다른 점 보임(직종 · 인용 조항 같음 · 계수 다름)" + return "후보" + + +def main(): + groups = { + "사람 126": [r for r in R if r["무리"] == "사람"], + " 임도 49": [r for r in R if r["id"][0] == "N"], + " 사방 43": [r for r in R if r["id"][0] == "S"], + " 영월 34": [r for r in R if r["id"][0] == "Y"], + "새 파일 51": [r for r in R if r["무리"] != "사람"], + " 자동 정답 20": [r for r in R if "자동" in r["무리"]], + " 씨앗 짝 43": [r for r in R if "씨앗" in r["무리"]], + } + ways = { + "C4 지문만(로직만 · 조항 · 이름 덤)": order_C, + "1 단계만(C4 변형 · 품 채널만)": order_P0, + "품만(로직 + 옵션 · 딱 맞음 먼저)": order_P, + "지문 + 딱 맞음(R) 먼저": order_F, + "지문 + 딱 맞음(3%) 먼저": lambda r: order_F(r, 2), + } + out = {"수치": {}, "표시": {}, "표시줄": {}, "다른로직": [], "덮음": {}} + print("== 할 일 3 — 1위 엄격/느슨 · 다섯 안 엄격/느슨 · 1위 옵션까지(R/3%) · 옵션 하나로") + for g, rows in groups.items(): + for w, fn in ways.items(): + m = metrics(rows, fn) + out["수치"].setdefault(g, {})[w] = m + print(f"{g:<14}| {w:<26}| {m['1위 엄격']:>5}/{m['1위 느슨']:>5} · {m['5안 엄격']:>5}/{m['5안 느슨']:>5} · 옵션 {m['1위 옵션까지(R)']:>5}/{m['1위 옵션까지(3%)']:>5} · 하나로 {m['1위 옵션 하나로']:>5}") + # C4 지문만 재현 확인(126) · C4 추천 수치 + have = [r for r in R if r["무리"] == "사람"] + same = sum(1 for r in have if r["지문만"][:5] == C4[r["id"]]["지문만"][:5]) + print("C4 지문만 다섯 재현", same, "/", len(have)) + rec = sum(1 for r in have if C4[r["id"]]["추천"] if False) if False else None # noqa: SIM210 + # 1 단계가 정답을 15 안에 넣었나 + for g, rows in groups.items(): + cov = sum(1 for r in rows if set(r["정답"]) & {k for k, _ in r["거친"]}) + out["덮음"][g] = f"{cov}/{len(rows)}" + print("1 단계 15 안 정답", out["덮음"]) + # 할 일 4 — 표시 갈래 + print("== 할 일 4 — 표시(지문 + 딱 맞음(R) 먼저 · 1 위)") + for g in ("사람 126", "새 파일 51", " 씨앗 짝 43", " 자동 정답 20"): + rows = groups[g] + cnt = defaultdict(lambda: Counter()) + for r in rows: + d = display(r) + o = order_F(r) + Sx, Lx = set(r["정답"]), set(r["정답"]) | set(r["허용"]) + cnt[d]["줄"] += 1 + cnt[d]["엑셀 줄"] += T[r["id"]].get("줄수") or 1 + cnt[d]["틀림 엄격"] += not ok_top(o, Sx) + cnt[d]["틀림 느슨"] += not ok_top(o, Lx) + if d.startswith("맞음"): + b = r["옵션"][o[0]] + cnt[d]["옵션 하나로"] += not (b.get("갈리는 칸") or {}).get("R") + out["표시줄"].setdefault(g, []).append({"id": r["id"], "표시": d, "1위": o[0] if o else None, "정답": r["정답"], "맞음": ok_top(o, Sx)}) + out["표시"][g] = {d: dict(c) for d, c in cnt.items()} + for d, c in sorted(cnt.items()): + print(f" {g:<14} {d:<28} {dict(c)}") + # C4 딱 맞음(sure)과 견줌 — 126 + sure_ids = set(json.loads((K.C4 / "out_match.json").read_text(encoding="utf-8"))["딱 맞음"]) + mine = {r["id"] for r in have if display(r).startswith("맞음")} + print("C4 딱 맞음(고유 136 기준)", len(sure_ids & {r['id'] for r in have}), "· K5 맞음", len(mine), "· 둘 다", len(mine & sure_ids), "· K5 만", len(mine - sure_ids), "· C4 만", len((sure_ids & {r['id'] for r in have}) - mine)) + out["C4딱맞음견줌"] = {"C4": sorted(sure_ids & {r["id"] for r in have}), "K5": sorted(mine)} + # 정답 로직이 「없음」인데 다른 로직이 엑셀 품을 그대로 냄 + for r in R: + a = A[r["id"]] + if a["판정"]["R"] == "없음" and a["판정"]["0.03"] == "없음": + ex = [k for k in r["옵션"] if lvl(r, k) <= 1] + if ex: + out["다른로직"].append({"id": r["id"], "무리": r["무리"], "이름": T[r["id"]]["이름"], "정답": r["정답"], "딱 맞는 로직": ex, + "입력": {k: r["옵션"][k].get("입력") for k in ex}}) # fmt: skip + print("정답 로직은 없음인데 다른 로직이 딱 맞음(강 · R)", len(out["다른로직"])) + for x in out["다른로직"]: + print(" ", x["id"], x["무리"], x["이름"][:16], x["정답"], "→", x["딱 맞는 로직"], [K.WHOLE.logic(k).get("이름")[:16] for k in x["딱 맞는 로직"]], x["입력"]) + # C4 변형(입력 조합 64 안 · 범위 · 수 칸은 기본값)만으로 1 위 로직의 품이 엑셀과 같나 ↔ K5 옵션 찾기 + c4opt = Counter() + for g, rows in (("사람 126", groups["사람 126"]), ("새 파일 51", groups["새 파일 51"])): + for r in rows: + e = T[r["id"]] + o = order_C(r) + if not o or o[0] not in set(r["정답"]): + continue + ep = S.ex_prep(e) + pp = next(x for x in S.P if x["로직"] == o[0]) + _, _, v = S.score_pair(ep, pp) + vv = {"L": v["L"] if v else {}, "X": [(sorted(a), sorted(b_), q) for a, b_, q in (v["X"] if v else [])], "M": v["M"] if v else {}, "S": {}} + c = K.compare(e, vv, 0.005, rnd=True) if v and v.get("in") is not None else None + c4opt[(g, "C4 변형으로 같은 품")] += bool(c and K.strong(e, c)) and not single(e) + c4opt[(g, "K5 옵션 찾기로 같은 품")] += lvl(r, o[0]) <= 1 + c4opt[(g, "1위 로직 맞음")] += 1 + out["C4변형옵션"] = {f"{a}|{b}": v for (a, b), v in c4opt.items()} + print("C4 변형 대 K5 옵션 찾기(1위 로직 맞은 줄 · R)", dict(c4opt)) + (K.HERE / "sum34.json").write_text(json.dumps(out, ensure_ascii=False, indent=1, default=str), "utf-8") + + +if __name__ == "__main__": + main() diff --git a/docs/raw/조사/2026-10-15_338_실험자료/K5/p00_inputs.py b/docs/raw/조사/2026-10-15_338_실험자료/K5/p00_inputs.py new file mode 100644 index 000000000..048b81926 --- /dev/null +++ b/docs/raw/조사/2026-10-15_338_실험자료/K5/p00_inputs.py @@ -0,0 +1,77 @@ +"""K5 들여다보기 — 정답 로직 · 모든 로직의 입력 칸(고르기 · 범위 · 예시 · 기계 목록) 꼴과 조합 수. 읽기만.""" + +import json +import math +import re +import sys +from collections import Counter +from pathlib import Path + +ROOT = Path(r"C:\Program_coding\임도설계 및 견적자동화 프로그램 개발") +HERE = Path(__file__).resolve().parent +SP = HERE.parent.parent +for p in (str(ROOT), str(SP / "req225" / "C1")): + sys.path.insert(0, p) +sys.stdout.reconfigure(encoding="utf-8") + +from M01_MasterData import M01_MasterData_Store as m01 # noqa: E402 +from M02_MasterTemplete import M02_Hopyo_Match as hm # noqa: E402 +from labels import L # noqa: E402 +from labels_y import LY # noqa: E402 + +FILES, WHOLE = m01.loaded() +EQ = re.compile(r"^\d{4}-\d{4}$") +wide = json.loads((SP / "req226/C7/wide.json").read_text(encoding="utf-8")) +ans = set() +for v in {**L, **LY}.values(): + ans |= set(v["정답"]) +for d in (wide["자동"], wide["씨앗"]): + for v in d.values(): + ans |= set(v) +codes = [x["로직"] for x in hm.targets()["로직"]] +print("돈 로직", len(codes), "· 정답 로직", len(ans)) + + +def kinds(row): + sel, eq, rng, other = [], [], [], [] + for s in row.get("입력") or []: + o = s.get("고르기") or [] + if o and all(isinstance(v, str) and EQ.match(v) for v in o): + eq.append((s["이름"], len(o))) + elif o: + sel.append((s["이름"], len(o))) + elif isinstance(s.get("범위"), list): + rng.append((s["이름"], s.get("범위"), s.get("예시"))) + else: + other.append((s["이름"], s.get("예시"), s.get("단위"))) + return sel, eq, rng, other + + +st = Counter() +names = Counter() +for c in codes: + row = WHOLE.logic(c) + sel, eq, rng, other = kinds(row) + prod = math.prod(n for _, n in sel) if sel else 1 + st["고르기 곱 >64"] += prod > 64 + st["고르기 곱 >512"] += prod > 512 + st["기계 목록 있음"] += bool(eq) + st["범위 있음"] += bool(rng) + st["그 밖 수 입력 있음"] += bool(other) + for n, _ in sel: + names[n] += 1 +print(dict(st)) +print("자주 나오는 고르기 이름", names.most_common(40)) +if "show" in sys.argv: + for c in sorted(ans): + row = WHOLE.logic(c) + if not row: + print(c, "없음") + continue + sel, eq, rng, other = kinds(row) + print(c, row.get("이름"), "| 고르기", sel, "| 기계", eq, "| 범위", rng, "| 그 밖", other[:6]) +if len(sys.argv) > 2: + row = WHOLE.logic(sys.argv[2]) + print(json.dumps(row.get("입력"), ensure_ascii=False, default=str)[:4000]) + print(json.dumps(row.get("호표"), ensure_ascii=False, default=str)[:3000]) + print(json.dumps({k: row.get(k) for k in ("이름", "결과단위", "덧줄", "원문번호")}, ensure_ascii=False, default=str)[:2000]) diff --git a/docs/raw/조사/2026-10-15_338_실험자료/K5/p01_region.py b/docs/raw/조사/2026-10-15_338_실험자료/K5/p01_region.py new file mode 100644 index 000000000..49191e5bf --- /dev/null +++ b/docs/raw/조사/2026-10-15_338_실험자료/K5/p01_region.py @@ -0,0 +1,39 @@ +"""K5 들여다보기 — 「지역」 · 「보정작업」 고르기가 품(직종 · 장비 시간 · 자재 수량)을 바꾸는 로직이 있나 · 엔진 속도. +C4 run_one(같은 엔진 길)으로 기본값 하나와 값 바꾼 것 하나를 견줌. 읽기만.""" + +import sys +import time +from pathlib import Path + +HERE = Path(__file__).resolve().parent +C4 = HERE.parent.parent / "req226" / "C4" +sys.path.insert(0, str(C4)) +sys.stdout.reconfigure(encoding="utf-8") + +import s2_ours_fp as O # noqa: E402 + +codes = [x["로직"] for x in O.hm.targets()["로직"]] +a, b = int(sys.argv[1]), int(sys.argv[2]) +which = sys.argv[3] if len(sys.argv) > 3 else "지역" +t0, runs, changed, err = time.time(), 0, [], 0 +for code in codes[a:b]: + row = O.WHOLE.logic(code) + spec = next((s for s in row.get("입력") or [] if s.get("이름") == which and s.get("고르기")), None) + if not spec: + continue + by_name = {} + for h in row.get("호표") or []: + by_name.setdefault(h.get("이름"), h) + times = O.times_of(row) + keys = set() + for v in spec["고르기"][:: max(1, len(spec["고르기"]) // 6)]: + try: + got = O.run_one(code, row, {which: v}, times, by_name) + runs += 1 + except Exception: # noqa: BLE001 + err += 1 + continue + keys.add(O._key(got)) + if len(keys) > 1: + changed.append(code) +print(which, "로직", b - a, "· 돌림", runs, "· 오류", err, "· 품 바뀜", len(changed), changed[:20], f"{time.time() - t0:.1f}s") diff --git a/docs/raw/조사/2026-10-15_338_실험자료/K5/p02_display.py b/docs/raw/조사/2026-10-15_338_실험자료/K5/p02_display.py new file mode 100644 index 000000000..312537e38 --- /dev/null +++ b/docs/raw/조사/2026-10-15_338_실험자료/K5/p02_display.py @@ -0,0 +1,59 @@ +"""K5 들여다보기 — 표시 갈래 조건 바꿔 보기(다른 점 보임 = 직종 같음 + 조항 같음 / 점수 차) · 틀린 줄 목록.""" +import json +import sys +from collections import Counter + +import k5_lib as K +import k5_s6_sum34 as M +import s3_match as S + +OURC = S.OURC +T = M.T + + +def clause_ok(e, code): + our = OURC.get(code) + theirs = [list(x) for x in e.get("출처") or []] + return bool(our and theirs and S.clause_prefix(theirs, our)[0] >= 0.9) + + +def disp(r, rule): + o = M.order_F(r) + b = r["옵션"].get(o[0]) if o else None + e = T[r["id"]] + exact = [k for k, x in r["옵션"].items() if x["수준"] <= 1] + fs = r["지문점수"] + gap = fs.get(o[0], 0) - (fs.get(o[1], 0) if len(o) > 1 else 0) + if b and b["수준"] <= 1: + return "맞음(하나)" if len(exact) == 1 else "맞음(여럿)" + if b and b["수준"] == 2: + return "거의 맞음" + same = b and b.get("직종같음") + if rule == "직종" and same: + return "다른 점" + if rule == "직종+조항" and same and clause_ok(e, o[0]): + return "다른 점" + if rule == "직종+차" and same and gap > 0.3: + return "다른 점" + if rule == "직종+(조항 또는 차)" and same and (clause_ok(e, o[0]) or gap > 0.3): + return "다른 점" + return "후보" + + +for g, rows in (("사람", [r for r in M.R if r["무리"] == "사람"]), ("새", [r for r in M.R if r["무리"] != "사람"])): + for rule in ("직종", "직종+조항", "직종+차", "직종+(조항 또는 차)"): + c = Counter() + for r in rows: + d = disp(r, rule) + o = M.order_F(r) + c[(d, "줄")] += 1 + c[(d, "틀림")] += not M.ok_top(o, set(r["정답"])) + print(g, rule, {d: f"{c[(d, '줄')]}줄 틀림{c[(d, '틀림')]}" for d in ("맞음(하나)", "맞음(여럿)", "거의 맞음", "다른 점", "후보")}) +if "list" in sys.argv: + for r in M.R: + d = disp(r, "직종+(조항 또는 차)") + o = M.order_F(r) + if d.startswith("맞음") and not M.ok_top(o, set(r["정답"])): + print("틀린 맞음", r["id"], T[r["id"]]["이름"], o[0], r["정답"], r["옵션"][o[0]].get("입력")) + if d == "다른 점" and not M.ok_top(o, set(r["정답"])): + print("틀린 다른 점", r["id"], T[r["id"]]["이름"], o[0], r["정답"]) diff --git a/docs/raw/조사/2026-10-15_338_실험자료/K5/targets.json b/docs/raw/조사/2026-10-15_338_실험자료/K5/targets.json new file mode 100644 index 000000000..e59a2b236 --- /dev/null +++ b/docs/raw/조사/2026-10-15_338_실험자료/K5/targets.json @@ -0,0 +1,22638 @@ +[ +{ +"id": "N000", +"파일": "북부청임도", +"이름": "레미콘타설(장비사용)", +"규격": "철근구조물-소량 타설", +"단위": "㎥", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"6-1-1" +] +], +"직종근거": [ +"콘크리트공", +"보통인부" +], +"인력": { +"콘크리트공": { +"품": 0.109091, +"밑품": 0.109091, +"율": [], +"어떻게": [ +"식" +] +}, +"보통인부": { +"품": 0.036364, +"밑품": 0.036364, +"율": [], +"어떻게": [ +"식" +] +} +}, +"인력절": [ +[ +"2", +"콘크리트공", +0.109091, +0.109091 +], +[ +"2", +"보통인부", +0.036364, +0.036364 +] +], +"장비": [ +{ +"갈래": [ +"굴착기" +], +"규격": [ +0.6 +], +"시간": 0.29, +"밑시간": 0.29, +"율": [], +"절": "3", +"글": "굴착기(타이어형)(0.6㎥)" +} +], +"자재": [ +{ +"이름": "레미콘", +"품": null, +"밑품": null +} +], +"율": [ +[ +"노무비", +2.0 +] +], +"계수": {}, +"조건": { +"근": [ +"철근" +], +"규모": [ +"소량" +], +"방법": [ +"장비" +] +}, +"옛판": false, +"글": [ +"46.레미콘타설(장비사용) (철근구조물-소량 타설) [㎥]", +" [건설표준품셈 6-1-1]", +" 1. 레미콘 : 별도계상", +" 2. 타설비", +" 콘크리트공 : 273,540 × 3인 ÷ 55㎥ × 2 = 29,840.7", +" 보통인부 : 172,068 × 1인 ÷ 55㎥ × 2 = 6,257", +" 인부소계 : 36,097.7", +" 공구손료 및 경장비(콘크리트 진동기등)의 기계경비(노무비의 2%)", +" 36,097.7 × 2% = 721.9", +" 3. 굴착기(타이어형)(0.6㎥)", +" Q = 1일 × 8hr ÷ 55㎥ × 2 = 0.29 hr/㎥", +" 노무비 : 59,020 × 0.29 = 17,115.8", +" 재료비 : 19,792 × 0.29 = 5,739.6", +" 경 비 : 27,120 × 0.29 = 7,864.8", +" 소 계 : 30,720.2", +" 4. 합 계", +" 노무비 : 36,097.7 + 17,115.8 = 53,213", +" 재료비 : 721.9 + 5,739.6 = 6,461", +" 경 비 : 7,864", +" 계 : 67,538" +], +"절": { +"1": { +"이름": "레미콘 : 별도계상", +"출처": [] +}, +"2": { +"이름": "타설비", +"출처": [] +}, +"3": { +"이름": "굴착기(타이어형)(0.6㎥)", +"출처": [] +}, +"4": { +"이름": "합 계", +"출처": [] +} +}, +"이름조건": { +"근": [ +"철근" +], +"규모": [ +"소량" +], +"방법": [ +"장비" +] +}, +"정답": [ +"GC000198" +], +"허용": [ +"GF000155" +], +"무리": "사람", +"복합": null +}, +{ +"id": "N001", +"파일": "북부청임도", +"이름": "합판거푸집", +"규격": "4회", +"단위": "㎡", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"6-3-1" +] +], +"직종근거": [ +"형틀목공", +"보통인부" +], +"인력": { +"형틀목공": { +"품": 0.111111, +"밑품": 0.111111, +"율": [], +"어떻게": [ +"식" +] +}, +"보통인부": { +"품": 0.044444, +"밑품": 0.044444, +"율": [], +"어떻게": [ +"식" +] +} +}, +"인력절": [ +[ +"2", +"형틀목공", +0.111111, +0.111111 +], +[ +"2", +"보통인부", +0.044444, +0.044444 +] +], +"장비": [], +"자재": [ +{ +"이름": "내수합판1급(12mm1220×2440)", +"품": 0.3914, +"밑품": 1.03 +}, +{ +"이름": "각재(거푸집용,외송) (3.6×3.0×3.0)", +"품": 0.01444, +"밑품": 0.038 +} +], +"율": [ +[ +"주자재비", +9.0 +], +[ +"노무비", +1.0 +] +], +"계수": {}, +"조건": { +"난이": [ +"보통" +], +"횟수": [ +"4회" +] +}, +"옛판": false, +"글": [ +"24.합판거푸집(4회)[㎡]", +" 건설공사표준품셈, 6-3-1 합판거푸집", +" ※ 사용횟수 4회 : 보통", +" 1. 재료비", +" 내수합판1급(12mm1220×2440) 10,984 × 1.03㎡ × 38% = 4,299.1", +" 각재(거푸집용,외송) (3.6×3.0×3.0) 626,543 × 0.038㎥ × 38% = 9,047.2", +" 주자재소계 : 13,346.3", +" 소모자재(박리재등)(주자재비의 9%)", +" 13,346.3 × 9% = 1,201.1", +" 2. 인건비", +" Q = 45㎡/일", +" 형틀목공 : 275,790 × 5인 ÷ 45 = 30,643.3", +" 보통인부 : 172,068 × 2인 ÷ 45 = 7,648", +" 인부소계 : 38,291.3", +" 공구손료 및 경장비 기계경비(노무비의 1%)", +" 38,291.3 × 1% = 382.9", +" 3. 합 계", +" 노무비 : 38,291", +" 재료비 : 13,346.3 + 1,201.1 + 382.9 = 14,930", +" 경 비 :", +" 계 : 53,221" +], +"절": { +"1": { +"이름": "재료비", +"출처": [] +}, +"2": { +"이름": "인건비", +"출처": [] +}, +"3": { +"이름": "합 계", +"출처": [] +} +}, +"이름조건": { +"횟수": [ +"4회" +] +}, +"정답": [ +"GC000211" +], +"허용": [ +"GF000160" +], +"무리": "사람", +"복합": null +}, +{ +"id": "N002", +"파일": "북부청임도", +"이름": "기초잡석채집,운반,다짐", +"규격": "발파암활용,덤프15톤,L=500m", +"단위": "㎥", +"갈래": "단가산출", +"출처": [ +[ +"산림품셈", +"12-12" +], +[ +"건설품셈", +"3-4-8" +] +], +"직종근거": [ +"특별인부", +"보통인부" +], +"인력": { +"특별인부": { +"품": 0.011, +"밑품": 0.011, +"율": [], +"어떻게": [ +"식" +] +}, +"보통인부": { +"품": 0.011, +"밑품": 0.011, +"율": [], +"어떻게": [ +"식" +] +} +}, +"인력절": [ +[ +"4.다", +"특별인부", +0.011, +0.011 +], +[ +"4.다", +"보통인부", +0.011, +0.011 +] +], +"장비": [ +{ +"갈래": [ +"굴착기" +], +"규격": [ +0.7 +], +"시간": 0.03339517625231911, +"밑시간": 0.03339517625231911, +"율": [], +"절": "1", +"글": "굴착기(무한궤도)(0.7㎥)" +}, +{ +"갈래": [ +"굴착기" +], +"규격": [ +0.7 +], +"시간": 0.03710575139146568, +"밑시간": 0.03710575139146568, +"율": [], +"절": "2", +"글": "굴착기(무한궤도)(0.7㎥)" +}, +{ +"갈래": [ +"덤프트럭" +], +"규격": [ +15.0 +], +"시간": 0.0693000693000693, +"밑시간": 0.0693000693000693, +"율": [], +"절": "3", +"글": "운 반 : 덤프트럭(15Ton)" +}, +{ +"갈래": [ +"굴착기" +], +"규격": [ +0.2 +], +"시간": 0.08888888888888889, +"밑시간": 0.08888888888888889, +"율": [], +"절": "4.가", +"글": "굴착기(무한궤도)(0.2㎥)" +}, +{ +"갈래": [ +"롤러" +], +"규격": [ +0.7 +], +"시간": 10.416666666666666, +"밑시간": 10.416666666666666, +"율": [], +"절": "4.나", +"글": "진동로울러(핸드가이드식)(0.7Ton)" +} +], +"자재": [], +"율": [], +"계수": { +"L": 500.0, +"q": 0.7, +"K": 0.55, +"f": 1.0, +"E": 0.35, +"Cm": 18.0, +"V1": 5.0, +"V2": 6.0, +"T": 15.0, +"rt": 1.75, +"t3": 1.1, +"t4": 0.9 +}, +"조건": { +"토질": [ +"발파암" +] +}, +"옛판": false, +"글": [ +"33.기초잡석채집,운반,다짐(발파암활용,덤프15톤,L=500m)[㎥]", +" 기초잡석 채집, 운반, 다짐 [산림사업표준품셈 12-12준용]", +" 작업조건 : 발파후 파쇄암중에 뒷채움돌 채집(㎥)", +" 일정량 채취후 상차 및 운반(L=500m)", +" 1. 기초잡석 발파암 채집", +" 굴착기(무한궤도)(0.7㎥)", +" q = 0.7㎥ K = 0.55 f = 1", +" E = 0.35 Cm = 18sec(90˚)", +" 3,600 × q × K × f × E 3,600 × 0.7 × 0.55 × 1 × 0.35", +" Q = ────────────── = ───────────────── = 26.95 ㎥/hr", +" Cm 18", +" 노무비 : 59,020 ÷ 26.95 × 0.9 = 1,970.9", +" 재료비 : 19,473 ÷ 26.95 × 0.9 = 650.3", +" 경 비 : 24,554 ÷ 26.95 × 0.9 = 819.9", +" 소 계 : 3,441.1", +" 2. 적 사", +" 굴착기(무한궤도)(0.7㎥)", +" q = 0.7㎥ K = 0.55 f = 1", +" E = 0.35 Cm = 18sec(90˚)", +" 3,600 × q × K × f × E 3,600 × 0.7 × 0.55 × 1 × 0.35", +" Q = ────────────── = ───────────────── = 26.95 ㎥/hr", +" Cm 18", +" 노무비 : 59,020 ÷ 26.95 = 2,189.9", +" 재료비 : 19,473 ÷ 26.95 = 722.5", +" 경 비 : 24,554 ÷ 26.95 = 911", +" 소 계 : 3,823.4", +" 3. 운 반 : 덤프트럭(15Ton)", +" L = 0.5km V1 = 5km/hr V2 = 6km/hr", +" T = 15.0ton rt = 1.75ton/㎥ E = 0.9", +" q = T / rt = 15 / 1.75 = 8.57 ㎥", +" n = q / (0.7 × K) = 8.57 / (0.7 × 0.55) = 22.26 회", +" t1 = 18 × n / (60×0.35) = 18 × 22.26 / (60×0.35) = 19.08", +" L L 0.5 0.5", +" t2 = (── + ──) × 60 = (── + ──) × 60 = 11 분(왕복)", +" V1 V2 5 6", +" t3 = 1.1분(적하) t4 = 0.9분(대기)", +" Cm = t1 + t2 + t3 + t4 = 19.08 + 11 + 1.1 + 0.9 = 32.08 분", +" 60 × q × f × E 60 × 8.57 × 1 × 0.9", +" Q = ─────────── = ──────────── = 14.43 ㎥/hr", +" Cm 32.08", +" 노무비 : 59,020 ÷ 14.43 = 4,090", +" 재료비 : 30,192 ÷ 14.43 = 2,092.3", +" 경 비 : 20,659 ÷ 14.43 = 1,431.6", +" 소 계 : 7,613.9", +" 4. 기초지정[2025년 표준품셈3-4-8]", +" 가. 굴착기(무한궤도)(0.2㎥)", +" Q = 90÷8 = 11.25㎥/hr", +" 노무비 : 59,020 ÷ 11.25 = 5,246.2", +" 재료비 : 8,324 ÷ 11.25 = 739.9", +" 경 비 : 13,873 ÷ 11.25 = 1,233.1", +" 소 계 : 7,219.2", +" 나. 진동로울러(핸드가이드식)(0.7Ton)", +" 노무비 : 36,248 ÷ 0.096 = 3,418.3", +" 재료비 : 3,420 ÷ 11.25 = 304", +" 경 비 : 1,967 ÷ 11.25 = 174.8", +" 소 계 : 3,897.1", +" 다. 인 부", +" 특별인부 : 226,122 × 0.011인 = 2,487.3", +" 보통인부 : 172,068 × 0.011인 = 1,892.7", +" 인부소계 : 4,380" +], +"절": { +"1": { +"이름": "기초잡석 발파암 채집", +"출처": [] +}, +"2": { +"이름": "적 사", +"출처": [] +}, +"3": { +"이름": "운 반 : 덤프트럭(15Ton)", +"출처": [] +}, +"4": { +"이름": "기초지정[2025년 표준품셈3-4-8]", +"출처": [ +[ +"건설품셈", +"3-4-8" +] +] +}, +"4.가": { +"이름": "기초지정[2025년 표준품셈3-4-8] · 굴착기(무한궤도)(0.2㎥)", +"출처": [ +[ +"건설품셈", +"3-4-8" +] +] +}, +"4.나": { +"이름": "기초지정[2025년 표준품셈3-4-8] · 진동로울러(핸드가이드식)(0.7Ton)", +"출처": [ +[ +"건설품셈", +"3-4-8" +] +] +}, +"4.다": { +"이름": "기초지정[2025년 표준품셈3-4-8] · 인 부", +"출처": [ +[ +"건설품셈", +"3-4-8" +] +] +}, +"4.마": { +"이름": "기초지정[2025년 표준품셈3-4-8] · 소 계", +"출처": [ +[ +"건설품셈", +"3-4-8" +] +] +}, +"5": { +"이름": "합 계", +"출처": [] +} +}, +"이름조건": { +"토질": [ +"발파암" +] +}, +"정답": [ +"GC000110", +"GF000168" +], +"허용": [], +"무리": "사람", +"복합": null +}, +{ +"id": "N003", +"파일": "북부청임도", +"이름": "철근가공조립(현장)", +"규격": "Type-Ⅰ(소량 시공)", +"단위": "Ton", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"6-2" +] +], +"직종근거": [ +"철근공", +"보통인부" +], +"인력": { +"철근공": { +"품": 4.2, +"밑품": 4.2, +"율": [], +"어떻게": [ +"식", +"식" +] +}, +"보통인부": { +"품": 1.4, +"밑품": 1.4, +"율": [], +"어떻게": [ +"식", +"식" +] +} +}, +"인력절": [ +[ +"1.나", +"철근공", +0.67, +0.67 +], +[ +"1.나", +"보통인부", +0.22, +0.22 +], +[ +"2.나", +"철근공", +3.53, +3.53 +], +[ +"2.나", +"보통인부", +1.18, +1.18 +] +], +"장비": [], +"자재": [ +{ +"이름": "철근", +"품": null, +"밑품": null +}, +{ +"이름": "어닐링철선(결속선)(#200.9mm200.4m/kg)", +"품": 6.5, +"밑품": 6.5 +} +], +"율": [ +[ +"인력품", +9.0 +], +[ +"가공노력품", +2.0 +] +], +"계수": {}, +"조건": { +"근": [ +"철근" +], +"규모": [ +"소량" +], +"type": [ +"Ⅰ" +] +}, +"옛판": false, +"글": [ +"47.철근가공조립(현장)(Type-Ⅰ(소량 시공))[Ton]", +" 철근 현장가공 및 조립(Type-Ⅰ) [건설표준품셈6-2]", +" 1. 현장가공", +" 가. 재료비", +" 철근 : 별도계산", +" 나. 인건비", +" 철 근 공 : 268,187 × 0.67인 = 179,685.2", +" 보통인부 : 172,068 × 0.22인 = 37,854.9", +" 인부소계 : 217,540.1", +" 기계경비(인력품의 9%)", +" 217,540.1 × 9% = 19,578.6", +" 다. 소 계", +" 노무비 : 217,540.1", +" 재료비 : 19,578.6", +" 경 비 :", +" 계 : 237,118.7", +" 2. 조 립", +" 가. 재료비", +" 어닐링철선(결속선)(#200.9mm200.4m/kg) 1,760 × 6.5kg = 11,440", +" 나. 인건비", +" 철근공 : 268,187 × 3.53인 = 946,700.1", +" 보통인부 : 172,068 × 1.18인 = 203,040.2", +" 인부소계 : 1,149,740.3", +" 기구손료 (가공노력품의 2%)", +" 1,149,740.3 × 0.02 = 22,994.8", +" 다. 소 계", +" 노무비 : 1,149,740.3", +" 재료비 : 11,440 + 22,994.8 = 34,434.8", +" 경 비 :", +" 계 : 1,184,175.1", +" 3. 합 계", +" 노무비 : 217,540.1 + 1,149,740.3 = 1,367,280", +" 재료비 : 19,578.6 + 34,434.8 = 54,013", +" 경 비 :", +" 계 : 1,421,293" +], +"절": { +"1": { +"이름": "현장가공", +"출처": [] +}, +"1.가": { +"이름": "현장가공 · 재료비", +"출처": [] +}, +"1.나": { +"이름": "현장가공 · 인건비", +"출처": [] +}, +"1.다": { +"이름": "현장가공 · 소 계", +"출처": [] +}, +"2": { +"이름": "조 립", +"출처": [] +}, +"2.가": { +"이름": "조 립 · 재료비", +"출처": [] +}, +"2.나": { +"이름": "조 립 · 인건비", +"출처": [] +}, +"2.다": { +"이름": "조 립 · 소 계", +"출처": [] +}, +"3": { +"이름": "합 계", +"출처": [] +} +}, +"이름조건": { +"근": [ +"철근" +], +"규모": [ +"소량" +], +"type": [ +"Ⅰ" +] +}, +"정답": [ +"GC000206", +"GC000207" +], +"허용": [ +"GF000159" +], +"무리": "사람", +"복합": [ +"GC000206", +"GC000207" +] +}, +{ +"id": "N004", +"파일": "북부청임도", +"이름": "야면석채집 및 운반(계곡내)", +"규격": "굴삭기0.6㎥,35cm,4.5톤,L=500m", +"단위": "㎡", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"8-2" +] +], +"직종근거": [ +"보통인부" +], +"인력": { +"보통인부": { +"품": 0.017, +"밑품": 0.17, +"율": [ +[ +0.1, +"%" +] +], +"어떻게": [ +"식" +] +} +}, +"인력절": [ +[ +"1.가", +"보통인부", +0.017, +0.17 +] +], +"장비": [ +{ +"갈래": [ +"굴착기" +], +"규격": [ +0.6 +], +"시간": 0.012801484230055659, +"밑시간": 0.01422387136672851, +"율": [ +[ +0.9, +"%" +] +], +"절": "1.나", +"글": "기계채집 : 굴착기(타이어형)(0.6㎥)" +}, +{ +"갈래": [ +"굴착기" +], +"규격": [ +0.6 +], +"시간": 0.015804301518587235, +"밑시간": 0.015804301518587235, +"율": [], +"절": "1.다", +"글": "상 차 : 굴착기(타이어형)(0.6㎥)" +}, +{ +"갈래": [ +"덤프트럭" +], +"규격": [ +4.5 +], +"시간": 0.03298909925415949, +"밑시간": 0.03298909925415949, +"율": [], +"절": "3", +"글": "운 반 : 덤프트럭(4.5Ton)" +} +], +"자재": [], +"율": [], +"계수": { +"q": 0.6, +"K": 0.55, +"f": 1.0, +"E": 0.35, +"Cm": 18.0, +"qs": 0.6, +"Es": 0.35, +"L": 0.5, +"V1": 10.0, +"V2": 15.0, +"T": 4.5, +"rt": 1.75, +"t3": 1.1, +"t4": 0.42 +}, +"조건": { +"방법": [ +"인력" +] +}, +"옛판": false, +"글": [ +"6.야면석채집및운반(계곡내)(굴삭기0.6㎥,35cm,4.5톤,L=500m)[㎡]", +" 야면석 채집 및 운반", +" 작업조건 계곡에 산재하여 매몰되어 있는 곳에서 굴삭기 0.6㎥이용 채집", +" 1. 야면석 채집", +" 가. 인력(10%)", +" 보통인부 : 172,068 × 0.17인 × 10% = 2,925.1", +" [건설표준품셈(8-2, 2016년 삭제)] L3=35cm, 인/㎡의 10%로 적용", +" 나. 기계채집 : 굴착기(타이어형)(0.6㎥)", +" q = 0.6㎥ K = 0.55 f = 1", +" E = 0.35 Cm = 18sec(90˚)", +" 3,600 × q × K × f × E 3,600 × 0.6 × 0.55 × 1 × 0.35", +" Q = ────────────── = ───────────────── = 23.1 ㎥/hr", +" Cm 18", +" 노무비 : 59,020 ÷ 23.1 × 575 / 1750 × 90% = 755.5", +" 재료비 : 19,792 ÷ 23.1 × 575 / 1750 × 90% = 253.3", +" 경 비 : 27,120 ÷ 23.1 × 575 / 1750 × 90% = 347.1", +" 소 계 : 1,355.9", +" 다. 상 차 : 굴착기(타이어형)(0.6㎥)", +" qs = 0.6㎥ K = 0.55 f = 1", +" Es = 0.35 Cm = 20sec(135˚)", +" 3,600 × qs × K × f × Es 3,600 × 0.6 × 0.55 × 1 × 0.35", +" Q = ────────────── = ───────────────── = 20.79 ㎥/hr", +" Cm 20", +" 노무비 : 59,020 ÷ 20.79 × 575 / 1750 = 932.7", +" 재료비 : 19,792 ÷ 20.79 × 575 / 1750 = 312.7", +" 경 비 : 27,120 ÷ 20.79 × 575 / 1750 = 428.6", +" 소 계 : 1,674", +" 라. 소 계", +" 노무비 : 2,925.1 + 755.5 + 932.7 = 4,613.3", +" 재료비 : 253.3 + 312.7 = 566", +" 경 비 : 347.1 + 428.6 = 775.7", +" 계 : 5,955", +" 3. 운 반 : 덤프트럭(4.5Ton)", +" L = 0.5km V1 = 10km/hr V2 = 15km/hr", +" T = 4.5ton rt = 1.75ton/㎥ E = 0.9", +" q = T / rt = 4.5 / 1.75 = 2.57 ㎥", +" n = q / (qs × K) = 2.57 / (0.6 × 0.55) = 7.79 회", +" t1 = Cm × n / (60 × Es) = 20 × 7.79 / (60 × 0.35) = 7.42분", +" L L 0.5 0.5", +" t2 = (── + ──) × 60 = (── + ──) × 60 = 5분(왕복)", +" V1 V2 10 15", +" t3 = 1.1분 t4 = 0.42분", +" Cm = t1 + t2 + t3 + t4 = 7.42 + 5 + 1.1 + 0.42 = 13.94 분", +" 60 × q × f × E 60 × 2.57 × 1 × 0.9", +" Q = ─────────── = ──────────── = 9.96 ㎥/hr", +" Cm 13.94", +" 노무비 : 49,778 ÷ 9.96 × 575 / 1750 = 1,642.1", +" 재료비 : 9,494 ÷ 9.96 × 575 / 1750 = 313.1", +" 경 비 : 7,613 ÷ 9.96 × 575 / 1750 = 251.1", +" 소 계 : 2,206.3", +" 4. 합 계", +" 노무비 : 4,613.3 + 1,642.1 = 6,255", +" 재료비 : 566 + 313.1 = 879", +" 경 비 : 775.7 + 251.1 = 1,026", +" 계 : 8,160" +], +"절": { +"1": { +"이름": "야면석 채집", +"출처": [] +}, +"1.가": { +"이름": "야면석 채집 · 인력(10%)", +"출처": [] +}, +"1.나": { +"이름": "야면석 채집 · 기계채집 : 굴착기(타이어형)(0.6㎥)", +"출처": [] +}, +"1.다": { +"이름": "야면석 채집 · 상 차 : 굴착기(타이어형)(0.6㎥)", +"출처": [] +}, +"1.라": { +"이름": "야면석 채집 · 소 계", +"출처": [] +}, +"3": { +"이름": "운 반 : 덤프트럭(4.5Ton)", +"출처": [] +}, +"4": { +"이름": "합 계", +"출처": [] +} +}, +"이름조건": {}, +"정답": [ +"GF000214" +], +"허용": [], +"무리": "사람", +"복합": null +}, +{ +"id": "N005", +"파일": "북부청임도", +"이름": "고임돌 채집 및 운반", +"규격": "굴삭기0.6㎥(90%),4.5톤,L=500m", +"단위": "㎥", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"1-10" +] +], +"직종근거": [ +"보통인부" +], +"인력": { +"보통인부": { +"품": 0.03, +"밑품": 0.3, +"율": [ +[ +0.1, +"%" +] +], +"어떻게": [ +"식" +] +} +}, +"인력절": [ +[ +"1.나", +"보통인부", +0.03, +0.3 +] +], +"장비": [ +{ +"갈래": [ +"굴착기" +], +"규격": [ +0.6 +], +"시간": 0.03896103896103896, +"밑시간": 0.04329004329004329, +"율": [ +[ +0.9, +"%" +] +], +"절": "1.가", +"글": "굴착기(타이어형)(0.6㎥)" +}, +{ +"갈래": [ +"굴착기" +], +"규격": [ +0.6 +], +"시간": 0.02939447383891828, +"밑시간": 0.02939447383891828, +"율": [], +"절": "2", +"글": "상 차 : 굴착기(타이어형)(0.6㎥)" +}, +{ +"갈래": [ +"덤프트럭" +], +"규격": [ +4.5 +], +"시간": 0.07639419404125286, +"밑시간": 0.07639419404125286, +"율": [], +"절": "3", +"글": "운 반 : 덤프트럭(4.5Ton)" +} +], +"자재": [], +"율": [], +"계수": { +"q": 0.6, +"K": 0.55, +"f": 1.0, +"E": 0.35, +"Cm": 18.0, +"qs": 0.6, +"Es": 0.45, +"L": 0.5, +"V1": 10.0, +"V2": 15.0, +"T": 4.5, +"rt": 1.75, +"t3": 1.1, +"t4": 0.42 +}, +"조건": {}, +"옛판": false, +"글": [ +"3.고임돌채집및운반(굴삭기0.6㎥(90%),4.5톤,L=500m)[㎥]", +" 고임돌 채집 및 운반(㎥) [건설표준품셈 준용]", +" 작업조건 : 계곡에 산재한 고임돌 채집", +" 1. 채 집", +" 가. 굴착기(타이어형)(0.6㎥)", +" q = 0.6㎥ K = 0.55 f = 1", +" E = 0.35 Cm = 18sec(90˚)", +" 3,600 × q × K × f × E 3,600 × 0.6 × 0.55 × 1 × 0.35", +" Q = ────────────── = ───────────────── = 23.1 ㎥/hr", +" Cm 18", +" 노무비 : 59,020 ÷ 23.1 × 90% = 2,300", +" 재료비 : 19,792 ÷ 23.1 × 90% = 771.1", +" 경 비 : 27,120 ÷ 23.1 × 90% = 1,056.6", +" 소 계 : 4,127.7", +" 나. 인 력 (10%)", +" 보통인부 : 172,068 × 0.3인/㎥ × 10% = 5,162", +" 다. 소 계", +" 노무비 : 2,300 + 5,162 = 7,462", +" 재료비 : 771.1", +" 경 비 : 1,056.6", +" 계 : 9,289.7", +" 2. 상 차 : 굴착기(타이어형)(0.6㎥)", +" qs = 0.6㎥ K = 0.7 f = 1", +" Es = 0.45 Cm = 20sec(135˚)", +" 3,600 × qs × K × f × Es 3,600 × 0.6 × 0.7 × 1 × 0.45", +" Q = ────────────── = ───────────────── = 34.02 ㎥/hr", +" Cm 20", +" 노무비 : 59,020 ÷ 34.02 = 1,734.8", +" 재료비 : 19,792 ÷ 34.02 = 581.7", +" 경 비 : 27,120 ÷ 34.02 = 797.1", +" 소 계 : 3,113.6", +" 3. 운 반 : 덤프트럭(4.5Ton)", +" L = 0.5km V1 = 10km/hr V2 = 15km/hr", +" T = 4.5ton rt = 1.75ton/㎥ E = 0.9", +" q = T/rt = 4.5/1.75 = 2.57 ㎥", +" 건설표줌품셈(1-10) 자갈(습기)의 평균값 1.75 ton/㎥ 적용", +" n = q / (qs × K) = 2.57 / (0.6 × 0.7) = 6.12 회", +" t1 = 18 × n / (60 × Es) = 18 × 6.12 / (60 × 0.45) = 4.08 분", +" L L 0.5 0.5", +" t2 = (── + ──) × 60 = (── + ──) × 60 = 5분(왕복)", +" V1 V2 10 15", +" t3 = 1.1분 t4 = 0.42분", +" Cm = t1 + t2 + t3 + t4 = 4.08 + 5 + 1.1 + 0.42 = 10.6 분", +" 60 × q × f × E 60 × 2.57 × 1 × 0.9", +" Q = ─────────── = ──────────── = 13.09 ㎥/hr", +" Cm 10.6", +" 노무비 : 49,778 ÷ 13.09 = 3,802.7", +" 재료비 : 9,494 ÷ 13.09 = 725.2", +" 경 비 : 7,613 ÷ 13.09 = 581.5", +" 소 계 : 5,109.4", +" 4. 합 계", +" 노무비 : 7,462 + 1,734.8 + 3,802.7 = 12,999", +" 재료비 : 771.1 + 581.7 + 725.2 = 2,078", +" 경 비 : 1,056.6 + 797.1 + 581.5 = 2,435", +" 계 : 17,512" +], +"절": { +"1": { +"이름": "채 집", +"출처": [] +}, +"1.가": { +"이름": "채 집 · 굴착기(타이어형)(0.6㎥)", +"출처": [] +}, +"1.나": { +"이름": "채 집 · 인 력 (10%)", +"출처": [] +}, +"1.다": { +"이름": "채 집 · 소 계", +"출처": [] +}, +"2": { +"이름": "상 차 : 굴착기(타이어형)(0.6㎥)", +"출처": [] +}, +"3": { +"이름": "운 반 : 덤프트럭(4.5Ton)", +"출처": [] +}, +"4": { +"이름": "합 계", +"출처": [] +} +}, +"이름조건": {}, +"정답": [ +"GF000211" +], +"허용": [ +"GF000212" +], +"무리": "사람", +"복합": null +}, +{ +"id": "N006", +"파일": "북부청임도", +"이름": "야면석소운반", +"규격": "L=35cm", +"단위": "㎡", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"1-2-7" +] +], +"직종근거": [ +"보통인부" +], +"인력": { +"보통인부": { +"품": 0.547619, +"밑품": 0.547619, +"율": [], +"어떻게": [ +"식" +] +} +}, +"인력절": [ +[ +"1", +"보통인부", +0.547619, +0.547619 +] +], +"장비": [], +"자재": [], +"율": [], +"계수": { +"L": 6.0, +"n": 42.0 +}, +"조건": { +"횟수": [ +"1회", +"2회" +] +}, +"옛판": false, +"글": [ +"10.야면석소운반(L=35cm)[㎡]", +" 1. 야면석(L3=35cm내외) 소운반", +" 건설공사표준품셈 1-2-7", +" L = 6m {경사도 = 35°(α=8)} 운반회수 : n = 42회/일", +" P = 1 ㎡ × 575㎏/㎡ × 1회 ÷ 25㎏ × 1일 ÷ n = 1 × 575 × 1 ÷ 25 × 1 ÷ 42 = 0.547619", +" 보통인부 : 172,068 × P = 172,068 × 0.547619 = 94,227.7", +" 2. 합 계", +" 노무비 : 94,227", +" 재료비 :", +" 경 비 :", +" 계 : 94,227" +], +"절": { +"1": { +"이름": "야면석(L3=35cm내외) 소운반", +"출처": [] +}, +"2": { +"이름": "합 계", +"출처": [] +} +}, +"이름조건": {}, +"정답": [ +"GF000280" +], +"허용": [ +"GF000279" +], +"무리": "사람", +"복합": null +}, +{ +"id": "N007", +"파일": "북부청임도", +"이름": "야면석찰붙임", +"규격": "L3=35cm적용,장비사용", +"단위": "㎡", +"갈래": "단가산출", +"출처": [ +[ +"산림품셈", +"13-5-2" +] +], +"직종근거": [ +"석공", +"보통인부" +], +"인력": { +"석공": { +"품": 0.11, +"밑품": 0.11, +"율": [], +"어떻게": [ +"식" +] +}, +"보통인부": { +"품": 0.04, +"밑품": 0.04, +"율": [], +"어떻게": [ +"식" +] +} +}, +"인력절": [ +[ +"1", +"석공", +0.11, +0.11 +], +[ +"1", +"보통인부", +0.04, +0.04 +] +], +"장비": [ +{ +"갈래": [ +"굴착기", +"집게" +], +"규격": [ +0.6 +], +"시간": 0.22, +"밑시간": 0.22, +"율": [], +"절": "2", +"글": "굴착기(타이어형)(0.6㎥+부착용집게)" +} +], +"자재": [], +"율": [], +"계수": {}, +"조건": { +"메찰": [ +"찰붙임" +], +"난이": [ +"보통" +], +"방법": [ +"인력", +"장비" +] +}, +"옛판": false, +"글": [ +"39.야면석찰붙임(L3=35cm적용,장비사용)[㎡]", +" [산림사업표준품셈(13-5-2)]", +" 1. 인력", +" 석 공 : 268,908 × 0.11 = 29,580", +" 보통인부 : 172,068 × 0.04 = 6,882.7", +" 인부소계 : 36,462.7", +" 2. 굴착기(타이어형)(0.6㎥+부착용집게)", +" Q = 0.22hr", +" 노무비 : 59,020 × 0.22 = 12,984.4", +" 재료비 : 18,515 × 0.22 = 4,073.3", +" 경 비 : 32,906 × 0.22 = 7,239.3", +" 소 계 : 24,297", +" 3. 합 계", +" 노무비 : 36,462.7 + 12,984.4 = 49,447", +" 재료비 : 4,073", +" 경 비 : 7,239", +" 계 : 60,759" +], +"절": { +"1": { +"이름": "인력", +"출처": [] +}, +"2": { +"이름": "굴착기(타이어형)(0.6㎥+부착용집게)", +"출처": [] +}, +"3": { +"이름": "합 계", +"출처": [] +} +}, +"이름조건": { +"메찰": [ +"찰붙임" +], +"방법": [ +"장비" +] +}, +"정답": [ +"GF000225" +], +"허용": [ +"GC000263" +], +"무리": "사람", +"복합": null +}, +{ +"id": "N008", +"파일": "북부청임도", +"이름": "구조물터파기(인력100%)", +"규격": "토사", +"단위": "㎥", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"3-2-1" +] +], +"직종근거": [ +"특별인부" +], +"인력": { +"특별인부": { +"품": 0.354167, +"밑품": 0.354167, +"율": [], +"어떻게": [ +"식" +] +} +}, +"인력절": [ +[ +"1", +"특별인부", +0.354167, +0.354167 +] +], +"장비": [], +"자재": [], +"율": [], +"계수": {}, +"조건": { +"토질": [ +"경질토사", +"보통토사", +"자갈섞인토사", +"토사" +], +"난이": [ +"보통" +], +"방법": [ +"인력" +] +}, +"옛판": false, +"글": [ +"93.구조물터파기(인력100%)(토사)[㎥]", +" [건설공사표준품셈 3-2-1 인력굴착]", +" (보통토사+경질토사+자갈섞인 토사)/3 적용", +" 1. 인 력 (100 %) 적용", +" 특별인부 : 226,122 × (3.6+2.7+2.2)/3 ÷ 8 = 80,084.8", +" 2. 합 계", +" 노무비 : 80,084", +" 재료비 :", +" 경 비 :", +" 계 : 80,084" +], +"절": { +"1": { +"이름": "인 력 (100 %) 적용", +"출처": [] +}, +"2": { +"이름": "합 계", +"출처": [] +} +}, +"이름조건": { +"토질": [ +"토사" +], +"방법": [ +"인력" +] +}, +"정답": [ +"GC000083" +], +"허용": [ +"GF000101" +], +"무리": "사람", +"복합": null +}, +{ +"id": "N009", +"파일": "북부청임도", +"이름": "구조물되메우기(인력100%)", +"규격": "", +"단위": "㎥", +"갈래": "단가산출", +"출처": [ +[ +"산림품셈", +"9-14" +] +], +"직종근거": [ +"보통인부" +], +"인력": { +"보통인부": { +"품": 0.1, +"밑품": 0.1, +"율": [], +"어떻게": [ +"식" +] +} +}, +"인력절": [ +[ +"1", +"보통인부", +0.1, +0.1 +] +], +"장비": [], +"자재": [], +"율": [], +"계수": {}, +"조건": { +"난이": [ +"보통" +], +"규모": [ +"소규모" +], +"방법": [ +"인력" +] +}, +"옛판": false, +"글": [ +"12.구조물되메우기(인력100%)[㎥]", +" 산림사업표준품셈 9-14중 인력품만 반영", +" 소규모이므로 협소공간 작업으로 인력100%적용", +" 1. 인 력", +" 보통인부 : 172,068 × 0.1인 = 17,206.8", +" 2. 합 계", +" 노무비 : 17,206", +" 재료비 :", +" 경 비 :", +" 계 : 17,206" +], +"절": { +"1": { +"이름": "인 력", +"출처": [] +}, +"2": { +"이름": "합 계", +"출처": [] +} +}, +"이름조건": { +"방법": [ +"인력" +] +}, +"정답": [ +"GF000113" +], +"허용": [], +"무리": "사람", +"복합": null +}, +{ +"id": "N011", +"파일": "북부청임도", +"이름": "야면석채집및운반(계곡내)", +"규격": "굴삭기0.6㎥,35cm,4.5톤,L=500m", +"단위": "㎡", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"8-2" +] +], +"직종근거": [ +"보통인부" +], +"인력": { +"보통인부": { +"품": 0.017, +"밑품": 0.17, +"율": [ +[ +0.1, +"%" +] +], +"어떻게": [ +"식" +] +} +}, +"인력절": [ +[ +"1.가", +"보통인부", +0.017, +0.17 +] +], +"장비": [ +{ +"갈래": [ +"굴착기" +], +"규격": [ +0.6 +], +"시간": 0.012801484230055659, +"밑시간": 0.01422387136672851, +"율": [ +[ +0.9, +"%" +] +], +"절": "1.나", +"글": "기계채집 : 굴착기(타이어형)(0.6㎥)" +}, +{ +"갈래": [ +"굴착기" +], +"규격": [ +0.6 +], +"시간": 0.015804301518587235, +"밑시간": 0.015804301518587235, +"율": [], +"절": "1.다", +"글": "상 차 : 굴착기(타이어형)(0.6㎥)" +}, +{ +"갈래": [ +"덤프트럭" +], +"규격": [ +4.5 +], +"시간": 0.03298909925415949, +"밑시간": 0.03298909925415949, +"율": [], +"절": "3", +"글": "운 반 : 덤프트럭(4.5Ton)" +} +], +"자재": [], +"율": [], +"계수": { +"q": 0.6, +"K": 0.55, +"f": 1.0, +"E": 0.35, +"Cm": 18.0, +"qs": 0.6, +"Es": 0.35, +"L": 0.5, +"V1": 10.0, +"V2": 15.0, +"T": 4.5, +"rt": 1.75, +"t3": 1.1, +"t4": 0.42 +}, +"조건": { +"방법": [ +"인력" +] +}, +"옛판": false, +"글": [ +"6.야면석채집및운반(계곡내)(굴삭기0.6㎥,35cm,4.5톤,L=500m)[㎡]", +" 야면석 채집 및 운반", +" 작업조건 계곡에 산재하여 매몰되어 있는 곳에서 굴삭기 0.6㎥이용 채집", +" 1. 야면석 채집", +" 가. 인력(10%)", +" 보통인부 : 172,068 × 0.17인 × 10% = 2,925.1", +" [건설표준품셈(8-2, 2016년 삭제)] L3=35cm, 인/㎡의 10%로 적용", +" 나. 기계채집 : 굴착기(타이어형)(0.6㎥)", +" q = 0.6㎥ K = 0.55 f = 1", +" E = 0.35 Cm = 18sec(90˚)", +" 3,600 × q × K × f × E 3,600 × 0.6 × 0.55 × 1 × 0.35", +" Q = ────────────── = ───────────────── = 23.1 ㎥/hr", +" Cm 18", +" 노무비 : 59,020 ÷ 23.1 × 575 / 1750 × 90% = 755.5", +" 재료비 : 19,792 ÷ 23.1 × 575 / 1750 × 90% = 253.3", +" 경 비 : 27,120 ÷ 23.1 × 575 / 1750 × 90% = 347.1", +" 소 계 : 1,355.9", +" 다. 상 차 : 굴착기(타이어형)(0.6㎥)", +" qs = 0.6㎥ K = 0.55 f = 1", +" Es = 0.35 Cm = 20sec(135˚)", +" 3,600 × qs × K × f × Es 3,600 × 0.6 × 0.55 × 1 × 0.35", +" Q = ────────────── = ───────────────── = 20.79 ㎥/hr", +" Cm 20", +" 노무비 : 59,020 ÷ 20.79 × 575 / 1750 = 932.7", +" 재료비 : 19,792 ÷ 20.79 × 575 / 1750 = 312.7", +" 경 비 : 27,120 ÷ 20.79 × 575 / 1750 = 428.6", +" 소 계 : 1,674", +" 라. 소 계", +" 노무비 : 2,925.1 + 755.5 + 932.7 = 4,613.3", +" 재료비 : 253.3 + 312.7 = 566", +" 경 비 : 347.1 + 428.6 = 775.7", +" 계 : 5,955", +" 3. 운 반 : 덤프트럭(4.5Ton)", +" L = 0.5km V1 = 10km/hr V2 = 15km/hr", +" T = 4.5ton rt = 1.75ton/㎥ E = 0.9", +" q = T / rt = 4.5 / 1.75 = 2.57 ㎥", +" n = q / (qs × K) = 2.57 / (0.6 × 0.55) = 7.79 회", +" t1 = Cm × n / (60 × Es) = 20 × 7.79 / (60 × 0.35) = 7.42분", +" L L 0.5 0.5", +" t2 = (── + ──) × 60 = (── + ──) × 60 = 5분(왕복)", +" V1 V2 10 15", +" t3 = 1.1분 t4 = 0.42분", +" Cm = t1 + t2 + t3 + t4 = 7.42 + 5 + 1.1 + 0.42 = 13.94 분", +" 60 × q × f × E 60 × 2.57 × 1 × 0.9", +" Q = ─────────── = ──────────── = 9.96 ㎥/hr", +" Cm 13.94", +" 노무비 : 49,778 ÷ 9.96 × 575 / 1750 = 1,642.1", +" 재료비 : 9,494 ÷ 9.96 × 575 / 1750 = 313.1", +" 경 비 : 7,613 ÷ 9.96 × 575 / 1750 = 251.1", +" 소 계 : 2,206.3", +" 4. 합 계", +" 노무비 : 4,613.3 + 1,642.1 = 6,255", +" 재료비 : 566 + 313.1 = 879", +" 경 비 : 775.7 + 251.1 = 1,026", +" 계 : 8,160" +], +"절": { +"1": { +"이름": "야면석 채집", +"출처": [] +}, +"1.가": { +"이름": "야면석 채집 · 인력(10%)", +"출처": [] +}, +"1.나": { +"이름": "야면석 채집 · 기계채집 : 굴착기(타이어형)(0.6㎥)", +"출처": [] +}, +"1.다": { +"이름": "야면석 채집 · 상 차 : 굴착기(타이어형)(0.6㎥)", +"출처": [] +}, +"1.라": { +"이름": "야면석 채집 · 소 계", +"출처": [] +}, +"3": { +"이름": "운 반 : 덤프트럭(4.5Ton)", +"출처": [] +}, +"4": { +"이름": "합 계", +"출처": [] +} +}, +"이름조건": {}, +"정답": [ +"GF000214" +], +"허용": [], +"무리": "사람", +"복합": null +}, +{ +"id": "N012", +"파일": "북부청임도", +"이름": "고임돌채집및운반", +"규격": "굴삭기0.6㎥(90%),4.5톤,L=500m", +"단위": "㎥", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"1-10" +] +], +"직종근거": [ +"보통인부" +], +"인력": { +"보통인부": { +"품": 0.03, +"밑품": 0.3, +"율": [ +[ +0.1, +"%" +] +], +"어떻게": [ +"식" +] +} +}, +"인력절": [ +[ +"1.나", +"보통인부", +0.03, +0.3 +] +], +"장비": [ +{ +"갈래": [ +"굴착기" +], +"규격": [ +0.6 +], +"시간": 0.03896103896103896, +"밑시간": 0.04329004329004329, +"율": [ +[ +0.9, +"%" +] +], +"절": "1.가", +"글": "굴착기(타이어형)(0.6㎥)" +}, +{ +"갈래": [ +"굴착기" +], +"규격": [ +0.6 +], +"시간": 0.02939447383891828, +"밑시간": 0.02939447383891828, +"율": [], +"절": "2", +"글": "상 차 : 굴착기(타이어형)(0.6㎥)" +}, +{ +"갈래": [ +"덤프트럭" +], +"규격": [ +4.5 +], +"시간": 0.07639419404125286, +"밑시간": 0.07639419404125286, +"율": [], +"절": "3", +"글": "운 반 : 덤프트럭(4.5Ton)" +} +], +"자재": [], +"율": [], +"계수": { +"q": 0.6, +"K": 0.55, +"f": 1.0, +"E": 0.35, +"Cm": 18.0, +"qs": 0.6, +"Es": 0.45, +"L": 0.5, +"V1": 10.0, +"V2": 15.0, +"T": 4.5, +"rt": 1.75, +"t3": 1.1, +"t4": 0.42 +}, +"조건": {}, +"옛판": false, +"글": [ +"3.고임돌채집및운반(굴삭기0.6㎥(90%),4.5톤,L=500m)[㎥]", +" 고임돌 채집 및 운반(㎥) [건설표준품셈 준용]", +" 작업조건 : 계곡에 산재한 고임돌 채집", +" 1. 채 집", +" 가. 굴착기(타이어형)(0.6㎥)", +" q = 0.6㎥ K = 0.55 f = 1", +" E = 0.35 Cm = 18sec(90˚)", +" 3,600 × q × K × f × E 3,600 × 0.6 × 0.55 × 1 × 0.35", +" Q = ────────────── = ───────────────── = 23.1 ㎥/hr", +" Cm 18", +" 노무비 : 59,020 ÷ 23.1 × 90% = 2,300", +" 재료비 : 19,792 ÷ 23.1 × 90% = 771.1", +" 경 비 : 27,120 ÷ 23.1 × 90% = 1,056.6", +" 소 계 : 4,127.7", +" 나. 인 력 (10%)", +" 보통인부 : 172,068 × 0.3인/㎥ × 10% = 5,162", +" 다. 소 계", +" 노무비 : 2,300 + 5,162 = 7,462", +" 재료비 : 771.1", +" 경 비 : 1,056.6", +" 계 : 9,289.7", +" 2. 상 차 : 굴착기(타이어형)(0.6㎥)", +" qs = 0.6㎥ K = 0.7 f = 1", +" Es = 0.45 Cm = 20sec(135˚)", +" 3,600 × qs × K × f × Es 3,600 × 0.6 × 0.7 × 1 × 0.45", +" Q = ────────────── = ───────────────── = 34.02 ㎥/hr", +" Cm 20", +" 노무비 : 59,020 ÷ 34.02 = 1,734.8", +" 재료비 : 19,792 ÷ 34.02 = 581.7", +" 경 비 : 27,120 ÷ 34.02 = 797.1", +" 소 계 : 3,113.6", +" 3. 운 반 : 덤프트럭(4.5Ton)", +" L = 0.5km V1 = 10km/hr V2 = 15km/hr", +" T = 4.5ton rt = 1.75ton/㎥ E = 0.9", +" q = T/rt = 4.5/1.75 = 2.57 ㎥", +" 건설표줌품셈(1-10) 자갈(습기)의 평균값 1.75 ton/㎥ 적용", +" n = q / (qs × K) = 2.57 / (0.6 × 0.7) = 6.12 회", +" t1 = 18 × n / (60 × Es) = 18 × 6.12 / (60 × 0.45) = 4.08 분", +" L L 0.5 0.5", +" t2 = (── + ──) × 60 = (── + ──) × 60 = 5분(왕복)", +" V1 V2 10 15", +" t3 = 1.1분 t4 = 0.42분", +" Cm = t1 + t2 + t3 + t4 = 4.08 + 5 + 1.1 + 0.42 = 10.6 분", +" 60 × q × f × E 60 × 2.57 × 1 × 0.9", +" Q = ─────────── = ──────────── = 13.09 ㎥/hr", +" Cm 10.6", +" 노무비 : 49,778 ÷ 13.09 = 3,802.7", +" 재료비 : 9,494 ÷ 13.09 = 725.2", +" 경 비 : 7,613 ÷ 13.09 = 581.5", +" 소 계 : 5,109.4", +" 4. 합 계", +" 노무비 : 7,462 + 1,734.8 + 3,802.7 = 12,999", +" 재료비 : 771.1 + 581.7 + 725.2 = 2,078", +" 경 비 : 1,056.6 + 797.1 + 581.5 = 2,435", +" 계 : 17,512" +], +"절": { +"1": { +"이름": "채 집", +"출처": [] +}, +"1.가": { +"이름": "채 집 · 굴착기(타이어형)(0.6㎥)", +"출처": [] +}, +"1.나": { +"이름": "채 집 · 인 력 (10%)", +"출처": [] +}, +"1.다": { +"이름": "채 집 · 소 계", +"출처": [] +}, +"2": { +"이름": "상 차 : 굴착기(타이어형)(0.6㎥)", +"출처": [] +}, +"3": { +"이름": "운 반 : 덤프트럭(4.5Ton)", +"출처": [] +}, +"4": { +"이름": "합 계", +"출처": [] +} +}, +"이름조건": {}, +"정답": [ +"GF000211" +], +"허용": [ +"GF000212" +], +"무리": "사람", +"복합": null +}, +{ +"id": "N013", +"파일": "북부청임도", +"이름": "레미콘타설(장비사용)", +"규격": "무근구조물", +"단위": "㎥", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"6-1-1" +] +], +"직종근거": [ +"콘크리트공", +"보통인부" +], +"인력": { +"콘크리트공": { +"품": 0.047619, +"밑품": 0.047619, +"율": [], +"어떻게": [ +"식" +] +}, +"보통인부": { +"품": 0.015873, +"밑품": 0.015873, +"율": [], +"어떻게": [ +"식" +] +} +}, +"인력절": [ +[ +"2", +"콘크리트공", +0.047619, +0.047619 +], +[ +"2", +"보통인부", +0.015873, +0.015873 +] +], +"장비": [ +{ +"갈래": [ +"굴착기" +], +"규격": [ +0.6 +], +"시간": 0.13, +"밑시간": 0.13, +"율": [], +"절": "3", +"글": "굴착기(타이어형)(0.6㎥)" +} +], +"자재": [ +{ +"이름": "레미콘", +"품": null, +"밑품": null +} +], +"율": [ +[ +"노무비", +2.0 +] +], +"계수": {}, +"조건": { +"근": [ +"무근" +], +"방법": [ +"장비" +] +}, +"옛판": false, +"글": [ +"22.레미콘타설(장비사용) (무근구조물) [㎥]", +" 건설표준품셈, 6-1-1 콘크리트 타설", +" 1. 레미콘 : 별도계상", +" 2. 타설비", +" A = 63㎥/일", +" 콘크리트공 : 273,540 × 3인 ÷ 63 = 13,025.7", +" 보 통 인부 : 172,068 × 1인 ÷ 63 = 2,731.2", +" 인 부 소계 : 15,756.9", +" 공구손료 및 경장비(콘크리트 진동기등)의 기계경비(노무비의 2%)", +" 15,756.9 × 2% = 315.1", +" 3. 굴착기(타이어형)(0.6㎥)", +" Q = 8hr ÷ A = 8 ÷ 63 = 0.13hr/㎥", +" 노무비 : 59,020 × 0.13 = 7,672.6", +" 재료비 : 19,792 × 0.13 = 2,572.9", +" 경 비 : 27,120 × 0.13 = 3,525.6", +" 소 계 : 13,771.1", +" 4. 합 계", +" 노무비 : 15,756.9 + 7,672.6 = 23,429", +" 재료비 : 315.1 + 2,572.9 = 2,888", +" 경 비 : 3,525", +" 계 : 29,842" +], +"절": { +"1": { +"이름": "레미콘 : 별도계상", +"출처": [] +}, +"2": { +"이름": "타설비", +"출처": [] +}, +"3": { +"이름": "굴착기(타이어형)(0.6㎥)", +"출처": [] +}, +"4": { +"이름": "합 계", +"출처": [] +} +}, +"이름조건": { +"근": [ +"무근" +], +"방법": [ +"장비" +] +}, +"정답": [ +"GC000198" +], +"허용": [ +"GF000155" +], +"무리": "사람", +"복합": null +}, +{ +"id": "N014", +"파일": "북부청임도", +"이름": "유로폼", +"규격": "보통,0~7m", +"단위": "㎡", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"6-3-3" +] +], +"직종근거": [ +"형틀목공", +"보통인부" +], +"인력": { +"형틀목공": { +"품": 0.114286, +"밑품": 0.114286, +"율": [], +"어떻게": [ +"식" +] +}, +"보통인부": { +"품": 0.028571, +"밑품": 0.028571, +"율": [], +"어떻게": [ +"식" +] +} +}, +"인력절": [ +[ +"3", +"형틀목공", +0.114286, +0.114286 +], +[ +"3", +"보통인부", +0.028571, +0.028571 +] +], +"장비": [], +"자재": [ +{ +"이름": "유로폼(패널) (600×1200)", +"품": 0.095, +"밑품": 0.095 +} +], +"율": [ +[ +"주재료비", +55.0 +], +[ +"노무비", +3.0 +] +], +"계수": {}, +"조건": { +"난이": [ +"보통" +] +}, +"옛판": false, +"글": [ +"26.유로폼(보통,0~7m) [㎡]", +" 건설공사표준품셈 6-3-3", +" 1. 재료비", +" 유로폼(패널) (600×1200) 28,500 × 0.095매 = 2,707.5", +" 2. 잡재료비 (주재료비의 55%)", +" (2,707.5) × 55% = 1,489.1", +" 3. 설치비", +" Q = 35㎡/일", +" 형틀목공 : 275,790 × 4인 ÷ 35 = 31,518.8", +" 보통인부 : 172,068 × 1인 ÷ 35 = 4,916.2", +" 인부소계 : 36,435", +" 4. 공구손료 및 경장비 기계경비 (노무비의 3%)", +" 36,435 × 3% = 1,093", +" 5. 합 계", +" 노무비 : 36,435", +" 재료비 : 2,707.5 + 1,489.1 + 1,093 = 5,289", +" 경 비 :", +" 계 : 41,724" +], +"절": { +"1": { +"이름": "재료비", +"출처": [] +}, +"2": { +"이름": "잡재료비 (주재료비의 55%)", +"출처": [] +}, +"3": { +"이름": "설치비", +"출처": [] +}, +"4": { +"이름": "공구손료 및 경장비 기계경비 (노무비의 3%)", +"출처": [] +}, +"5": { +"이름": "합 계", +"출처": [] +} +}, +"이름조건": { +"난이": [ +"보통" +] +}, +"정답": [ +"GC000214" +], +"허용": [ +"GF000210" +], +"무리": "사람", +"복합": null +}, +{ +"id": "N015", +"파일": "북부청임도", +"이름": "문양거푸집", +"규격": "", +"단위": "㎡", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"6-3-4" +] +], +"직종근거": [ +"형틀목공", +"보통인부" +], +"인력": { +"형틀목공": { +"품": 0.07, +"밑품": 0.07, +"율": [], +"어떻게": [ +"식" +] +}, +"보통인부": { +"품": 0.03, +"밑품": 0.03, +"율": [], +"어떻게": [ +"식" +] +} +}, +"인력절": [ +[ +"2", +"형틀목공", +0.07, +0.07 +], +[ +"2", +"보통인부", +0.03, +0.03 +] +], +"장비": [], +"자재": [ +{ +"이름": "문양거푸집(ABS코팅폼)(600×1200×20~30mm)", +"품": 1.3888888888888888, +"밑품": 1.3888888888888888 +} +], +"율": [ +[ +"재료비", +2.0 +] +], +"계수": {}, +"조건": {}, +"옛판": false, +"글": [ +"18.문양거푸집[㎡]", +" 문양거푸집 [건설표준품셈6-3-4]", +" 1. 재료비", +" 문양거푸집(ABS코팅폼)(600×1200×20~30mm) 4,166 × 1㎡ × (1/0.72)= 5,786.1", +" 잡재료비(재료비의 2%)", +" 5,786.1 × 2% = 116", +" 2. 문양거푸집설치 및 해체", +" 형틀목공 : 275,790 × 0.07인 = 19,305.3", +" 보통인부 : 172,068 × 0.03인 = 5,162", +" 인부소계 : 24,467.3", +" 3. 합 계", +" 노무비 : 24,467", +" 재료비 : 5,786.1 + 116 = 5,902", +" 경 비 :", +" 계 : 30,369" +], +"절": { +"1": { +"이름": "재료비", +"출처": [] +}, +"2": { +"이름": "문양거푸집설치 및 해체", +"출처": [] +}, +"3": { +"이름": "합 계", +"출처": [] +} +}, +"이름조건": {}, +"정답": [ +"GC000215" +], +"허용": [ +"GF000161" +], +"무리": "사람", +"복합": null +}, +{ +"id": "N016", +"파일": "북부청임도", +"이름": "P.V.C파이프설치", +"규격": "Φ50mm", +"단위": "M", +"갈래": "단가산출", +"출처": [ +[ +"산림품셈", +"12-22" +] +], +"직종근거": [], +"인력": {}, +"인력절": [], +"장비": [], +"자재": [ +{ +"이름": "P.V.CPIPE(VG1(50mm)", +"품": 1.0, +"밑품": 1.0 +} +], +"율": [ +[ +"재료비", +5.0 +] +], +"계수": {}, +"조건": {}, +"옛판": false, +"글": [ +"37.P.V.C파이프설치(Φ50mm)[M]", +" [산림사업표준품셈 12-22]", +" 1. 재료비(VG1,Φ50mm)", +" P.V.CPIPE(VG1(50mm) 4,290 × 1m = 4,290", +" 설치비(재료비의 5%)", +" 4,290 × 5% = 214.5", +" 2. 합 계", +" 노무비 :", +" 재료비 : 4,290", +" 경 비 : 214", +" 계 : 4,504" +], +"절": { +"1": { +"이름": "재료비(VG1,Φ50mm)", +"출처": [] +}, +"2": { +"이름": "합 계", +"출처": [] +} +}, +"이름조건": {}, +"정답": [ +"GF000192" +], +"허용": [], +"무리": "사람", +"복합": null +}, +{ +"id": "N017", +"파일": "북부청임도", +"이름": "다웰바설치", +"규격": "Φ32×1000", +"단위": "EA", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"6-3-10" +], +[ +"건설품셈", +"6-2-1" +] +], +"직종근거": [ +"형틀목공", +"보통인부", +"철근공" +], +"인력": { +"형틀목공": { +"품": 0.043, +"밑품": 0.043, +"율": [], +"어떻게": [ +"식" +] +}, +"보통인부": { +"품": 0.017722, +"밑품": 0.017722, +"율": [], +"어떻게": [ +"식", +"식", +"식" +] +}, +"철근공": { +"품": 0.026166, +"밑품": 0.026166, +"율": [], +"어떻게": [ +"식", +"식" +] +} +}, +"인력절": [ +[ +"2", +"형틀목공", +0.043, +0.043 +], +[ +"2", +"보통인부", +0.009, +0.009 +], +[ +"3.가", +"철근공", +0.004174, +0.004174 +], +[ +"3.가", +"보통인부", +0.001371, +0.001371 +], +[ +"3.나", +"철근공", +0.021992, +0.021992 +], +[ +"3.나", +"보통인부", +0.007351, +0.007351 +] +], +"장비": [], +"자재": [ +{ +"이름": "다웰바(DOWELBAR)(Ø32×1,000)", +"품": 1.0, +"밑품": 1.0 +}, +{ +"이름": "P.V.CPIPE(VG1(35mm)", +"품": 1.0, +"밑품": 1.0 +}, +{ +"이름": "어닐링철선(결속선)(#200.9mm200.4m/kg)", +"품": 0.040495, +"밑품": 0.040495 +} +], +"율": [ +[ +"노무비", +9.0 +] +], +"계수": {}, +"조건": {}, +"옛판": false, +"글": [ +"38.다웰바설치(Φ32×1000)[EA]", +" 다웰바 설치-신축 이음부 [건설공사표준품셈6-3-10]", +" 1. 재료비", +" 다웰바(DOWELBAR)(Ø32×1,000) 10,500 × 1EA = 10,500", +" P.V.CPIPE(VG1(35mm) 2,252 × 1m = 2,252", +" 자재소계 : 12,752", +" 2. 인건비", +" 형틀목공 : 275,790 × 0.043인 = 11,858.9", +" 보통인부 : 172,068 × 0.009인 = 1,548.6", +" 인부소계 : 13,407.5", +" 3. 철근가공조립 간단 [건설표준품셈(6-2-1)]", +" 가. 현장가공", +" 1) 철 근 : 6.230kg (다웰바Ø32mm 이형철근 중량 산정)", +" 2) 인 부", +" 철 근 공 : 268,187 × 0.67인 × 0.00623 = 1,119.4", +" 보통인부 : 172,068 × 0.22인 × 0.00623 = 235.8", +" 인부소계 : 1,355.2", +" 기구손료(노무비의 9%)", +" 1,355.2 × 9% = 121.9", +" 나. 조 립", +" 어닐링철선(결속선)(#200.9mm200.4m/kg) 1,760 × 6.5kg × 0.00623 = 71.2", +" 철 근 공 : 268,187 × 3.53인 × 0.00623 = 5,897.9", +" 보통인부 : 172,068 × 1.18인 × 0.00623 = 1,264.9", +" 인부소계 :7,162.8", +" 다. 기구손료", +" 7,162.8 × 2% = 143.2", +" 다. 소 계", +" 노무비 : 1,355.2 + 7,162.8 = 8,518", +" 재료비 : 121.9 + 71.2 + 143.2 = 336.3", +" 경 비 :", +" 계 : 8,854.3", +" 4. 합 계", +" 노무비 : 13,407.5 + 8,518 = 21,925", +" 재료비 : 12,752 + 336.3 = 13,088", +" 경 비 :", +" 계 : 35,013" +], +"절": { +"1": { +"이름": "재료비", +"출처": [] +}, +"2": { +"이름": "인건비", +"출처": [] +}, +"3": { +"이름": "철근가공조립 간단 [건설표준품셈(6-2-1)]", +"출처": [ +[ +"건설품셈", +"6-2-1" +] +] +}, +"3.가": { +"이름": "철근가공조립 간단 [건설표준품셈(6-2-1)] · 현장가공", +"출처": [ +[ +"건설품셈", +"6-2-1" +] +] +}, +"3.나": { +"이름": "철근가공조립 간단 [건설표준품셈(6-2-1)] · 조 립", +"출처": [ +[ +"건설품셈", +"6-2-1" +] +] +}, +"3.다": { +"이름": "철근가공조립 간단 [건설표준품셈(6-2-1)] · 소 계", +"출처": [ +[ +"건설품셈", +"6-2-1" +] +] +}, +"4": { +"이름": "합 계", +"출처": [] +} +}, +"이름조건": {}, +"정답": [ +"GC000226" +], +"허용": [ +"GF000201" +], +"무리": "사람", +"복합": null +}, +{ +"id": "N018", +"파일": "북부청임도", +"이름": "휠라설치", +"규격": "T=15mm,1000×1500mm", +"단위": "㎡", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"6-3-10" +] +], +"직종근거": [ +"형틀목공", +"보통인부" +], +"인력": { +"형틀목공": { +"품": 0.029, +"밑품": 0.029, +"율": [], +"어떻게": [ +"식" +] +}, +"보통인부": { +"품": 0.006, +"밑품": 0.006, +"율": [], +"어떻게": [ +"식" +] +} +}, +"인력절": [ +[ +"2", +"형틀목공", +0.029, +0.029 +], +[ +"2", +"보통인부", +0.006, +0.006 +] +], +"장비": [], +"자재": [], +"율": [], +"계수": { +"T": 15.0 +}, +"조건": {}, +"옛판": false, +"글": [ +"41.휠라설치(T=15mm, 1000×1500mm)[㎡]", +" [건설공사표준품셈(6-3-10)]", +" 1. 재료비", +" 휠라(T=15mm,1000×1000mm) 10,000 × 1.1 = 11,000", +" 2. 설치비", +" 형틀목공 : 275,790 × 0.029인 = 7,997.9", +" 보통인부 : 172,068 × 0.006인 = 1,032.4", +" 인부소계 : 9,030.3", +" 3. 합 계", +" 노무비 : 9,030", +" 재료비 : 11,000", +" 경 비 :", +" 계 : 20,030" +], +"절": { +"1": { +"이름": "재료비", +"출처": [] +}, +"2": { +"이름": "설치비", +"출처": [] +}, +"3": { +"이름": "합 계", +"출처": [] +} +}, +"이름조건": {}, +"정답": [ +"GC000227" +], +"허용": [], +"무리": "사람", +"복합": null +}, +{ +"id": "N019", +"파일": "북부청임도", +"이름": "철근가공조립(현장)", +"규격": "연결철근,Type-Ⅰ×30%적용", +"단위": "Ton", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"6-2" +], +[ +"산림품셈", +"12-9-1" +] +], +"직종근거": [ +"철근공", +"보통인부" +], +"인력": { +"철근공": { +"품": 0.729412, +"밑품": 2.431373, +"율": [ +[ +0.3, +"섞음" +], +[ +0.3, +"섞음" +] +], +"어떻게": [ +"식", +"식" +] +}, +"보통인부": { +"품": 0.243137, +"밑품": 0.810458, +"율": [ +[ +0.3, +"섞음" +], +[ +0.3, +"섞음" +] +], +"어떻게": [ +"식", +"식" +] +} +}, +"인력절": [ +[ +"1.나", +"철근공", +0.2, +0.666667 +], +[ +"1.나", +"보통인부", +0.066667, +0.222222 +], +[ +"2", +"철근공", +0.529412, +1.764706 +], +[ +"2", +"보통인부", +0.176471, +0.588235 +] +], +"장비": [], +"자재": [ +{ +"이름": "철 근", +"품": null, +"밑품": null +} +], +"율": [ +[ +"인력품", +9.0 +], +[ +"가공노력품", +2.0 +] +], +"계수": {}, +"조건": { +"근": [ +"철근" +], +"type": [ +"Ⅰ" +] +}, +"옛판": false, +"글": [ +"29.철근가공조립(현장)(연결철근,Type-Ⅰ×30%적용)[Ton]", +" 철근현장가공및조립(Type-Ⅰ,1-1)의 [건설공사표준품셈6-2참조]", +" 연결철근은 30%적용[산림사업표준품셈12-9-1]", +" 1. 현장가공", +" 가. 철 근 : 별도계상", +" 나. 인건비", +" 철 근 공 : 268,187 × 3인 ÷ 4.5Ton × 0.3 = 53,637.4", +" 보통인부 : 172,068 × 1인 ÷ 4.5Ton × 0.3 = 11,472", +" 인부소계 : 65,109.4", +" 다. 기계경비(인력품의 9%)", +" 65,109.4 × 0.09 = 5,859.8", +" 라. 소 계", +" 노무비 : 65,109.4", +" 재료비 : 5,859.8", +" 경 비 :", +" 계 : 70,969.2", +" 2. 현장조립 인건비", +" 철 근 공 : 268,187 × 6인 ÷ 3.4Ton × 0.3 = 141,981.3", +" 보통인부 : 172,068 × 2인 ÷ 3.4Ton × 0.3 = 30,364.9", +" 인부소계 : 172,346.2", +" 기구손료(가공노력품의 2%)", +" 172,346.2 × 0.02 = 3,446.9", +" 3. 합 계", +" 노무비 : 65,109.4 + 172,346.2 = 237,455", +" 재료비 : 5,859.8 + 3,446.9 = 9,306", +" 경 비 :", +" 계 : 246,761" +], +"절": { +"1": { +"이름": "현장가공", +"출처": [] +}, +"1.가": { +"이름": "현장가공 · 철 근 : 별도계상", +"출처": [] +}, +"1.나": { +"이름": "현장가공 · 인건비", +"출처": [] +}, +"1.다": { +"이름": "현장가공 · 기계경비(인력품의 9%)", +"출처": [] +}, +"1.라": { +"이름": "현장가공 · 소 계", +"출처": [] +}, +"2": { +"이름": "현장조립 인건비", +"출처": [] +}, +"3": { +"이름": "합 계", +"출처": [] +} +}, +"이름조건": { +"근": [ +"철근" +], +"type": [ +"Ⅰ" +] +}, +"정답": [ +"GC000206", +"GC000207" +], +"허용": [ +"GF000159" +], +"무리": "사람", +"복합": [ +"GC000206", +"GC000207" +] +}, +{ +"id": "N020", +"파일": "북부청임도", +"이름": "큰돌채집운반", +"규격": "공사중 발생석 활용 50%적용", +"단위": "㎡", +"갈래": "단가산출", +"출처": [ +[ +"산림품셈", +"13-2-3" +] +], +"직종근거": [ +"작업반장" +], +"인력": { +"작업반장": { +"품": 0.039, +"밑품": 0.052, +"율": [ +[ +0.75, +"품할증" +] +], +"어떻게": [ +"식" +] +} +}, +"인력절": [ +[ +"1.가", +"작업반장", +0.039, +0.052 +] +], +"장비": [ +{ +"갈래": [ +"굴착기" +], +"규격": [ +0.6 +], +"시간": 0.201, +"밑시간": 0.268, +"율": [ +[ +0.75, +"품할증" +] +], +"절": "1.나", +"글": "기 계 : 굴착기(타이어형)(0.6㎥)" +}, +{ +"갈래": [ +"굴착기" +], +"규격": [ +0.6 +], +"시간": 0.201, +"밑시간": 0.268, +"율": [ +[ +0.75, +"품할증" +] +], +"절": "2.가", +"글": "굴착기(타이어형)(0.6㎥)" +}, +{ +"갈래": [ +"집게" +], +"규격": [ +0.8 +], +"시간": 0.201, +"밑시간": 0.268, +"율": [ +[ +0.75, +"품할증" +] +], +"절": "2.나", +"글": "부착용집게(0.7~0.8㎥)" +}, +{ +"갈래": [ +"덤프트럭" +], +"규격": [ +15.0 +], +"시간": 0.021311475409836068, +"밑시간": 0.042622950819672135, +"율": [ +[ +0.5, +"섞음" +] +], +"절": "4", +"글": "덤프트럭(15Ton)" +} +], +"자재": [], +"율": [ +[ +"채집품", +50.0 +], +[ +"노무비", +6.0 +] +], +"계수": { +"L": 0.5, +"V1": 5.0, +"V2": 6.0, +"T": 15.0, +"rt": 2.4, +"E": 0.9, +"k": 0.55, +"Es": 0.35, +"f": 1.0, +"t3": 1.1, +"t4": 0.9 +}, +"조건": {}, +"옛판": true, +"글": [ +"30.큰돌채집운반(공사중 발생석 활용 50%적용)[㎡]", +" 큰돌의 채집 및 운반 [임도표준품셈 13-2-3]", +" 큰돌 뒷길이 60~75㎝내외(뒷길이 67.5cm적용)", +" 공사시 발생하는 큰돌에 대해서는 채집품의 50% 적용", +" 큰돌운반거리는 500m로 가정하여 적용", +" Ta = 1 × 0.675 × 0.77(공극률제외) = 0.52㎥/㎡", +" 1. 큰돌채집", +" P = 4개/㎡ (40cm이상~60cm미만이므로 평균 50cm 적용)", +" 가. 인 력", +" 작업반장 : 215,907×P×0.026인/EA×0.75(품할증)×0.5 = 215,907×4×0.026×0.75×0.5 = 8,420.3", +" 나. 기 계 : 굴착기(타이어형)(0.6㎥)", +" 노무비 : 59,020×P×0.134×0.75(품할증)×0.5 = 59,020×4×0.134×0.75×0.5 = 11,863", +" 재료비 : 19,792×P×0.134×0.75(품할증)×0.5 = 19,792×4×0.134×0.75×0.5 = 3,978.1", +" 경 비 : 27,120×P×0.134×0.75(품할증)×0.5 = 27,120×4×0.134×0.75×0.5 = 5,451.1", +" 소 계 : 21,292.2", +" 다. 소 계", +" 노무비 : 8,420.3 + 11,863 = 20,283.3", +" 재료비 : 3,978.1", +" 경 비 : 5,451.1", +" 계 : 29,712.5", +" 2. 큰돌상차", +" 가. 굴착기(타이어형)(0.6㎥)", +" 노무비 : 59,020×P×0.134×0.75(품할증)×0.5 = 59,020×4×0.134×0.75×0.5 = 11,863", +" 재료비 : 19,792×P×0.134×0.75(품할증)×0.5 = 19,792×4×0.134×0.75×0.5 = 3,978.1", +" 경 비 : 27,120×P×0.134×0.75(품할증)×0.5 = 27,120×4×0.134×0.75×0.5 = 5,451.1", +" 소 계 : 21,292.2", +" 나. 부착용집게(0.7~0.8㎥)", +" 노무비 : 0×P×0.134×0.75(품할증)×0.5 = 0×4×0.134×0.75×0.5 = 0", +" 재료비 : 0×P×0.134×0.75(품할증)×0.5 = 0×4×0.134×0.75×0.5 = 0", +" 경 비 : 5,786×P×0.134×0.75(품할증)×0.5 = 5,786×4×0.134×0.75×0.5 = 1,162.9", +" 소 계 : 1,162.9", +" 다. 소 계", +" 노무비 : 11,863", +" 재료비 : 3,978.1", +" 경 비 : 5,451.1 + 1,162.9 = 6,614", +" 계 : 22,455.1", +" 3. 제잡비(노무비의 6%)", +" (8,420.3 + 11,863 + 11,863) × 6% = 1,928.7", +" 4. 운 반 [임도표준품셈의 발파암 기준 적용]", +" 덤프트럭(15Ton)", +" L = 0.5km V1 = 5km/hr V2 = 6km/hr", +" T = 15.0ton rt = 2.4ton/㎥ E = 0.9 k = 0.55 Es = 0.35", +" q = T/rt × 1.35 = 15/2.4 × 1.35 = 8.44 ㎥ f = 1", +" n = q / (k×0.6) = 8.44 / (0.55×0.6) = 25.58 회", +" t1 = 20 × n / (60 × Es) = 20 × 25.58 / (60 × 0.35) = 24.36 분", +" L L 0.5 0.5", +" t2 = (── + ──) × 60 = (── + ──) × 60 = 11 분(왕복)", +" V1 V2 5 6", +" t3 = 1.1분(적하) t4 = 0.9분(대기)", +" Cm = t1 + t2 + t3 + t4 = 24.36 + 11 + 1.1 + 0.9 = 37.36분", +" 60 × q × f × E 60 × 8.44 × 1 × 0.9", +" Q = ─────────── = ──────────── = 12.2 ㎥/hr", +" Cm 37.36", +" 노무비 : 59,020÷12.2×Ta×0.5 = 59,020÷12.2×0.52×0.5 = 1,257.8", +" 재료비 : 30,192÷12.2×((Cm-t1)/Cm)×Ta×0.5 = 30,192÷12.2×((37.36-24.36)/37.36)×0.52×0.5 = 223.8", +" 경 비 : 20,659÷12.2×Ta×0.5 = 20,659÷12.2×0.52×0.5 = 440.2", +" 소 계 : 1,921.8", +" 5. 합 계", +" 노무비 : 20,283.3 + 11,863 + 1,257.8 = 33,404", +" 재료비 : 3,978.1 + 3,978.1 + 223.8 = 8,180" +], +"절": { +"1": { +"이름": "큰돌채집", +"출처": [] +}, +"1.가": { +"이름": "큰돌채집 · 인 력", +"출처": [] +}, +"1.나": { +"이름": "큰돌채집 · 기 계 : 굴착기(타이어형)(0.6㎥)", +"출처": [] +}, +"1.다": { +"이름": "큰돌채집 · 소 계", +"출처": [] +}, +"2": { +"이름": "큰돌상차", +"출처": [] +}, +"2.가": { +"이름": "큰돌상차 · 굴착기(타이어형)(0.6㎥)", +"출처": [] +}, +"2.나": { +"이름": "큰돌상차 · 부착용집게(0.7~0.8㎥)", +"출처": [] +}, +"2.다": { +"이름": "큰돌상차 · 소 계", +"출처": [] +}, +"3": { +"이름": "제잡비(노무비의 6%)", +"출처": [] +}, +"4": { +"이름": "운 반 [임도표준품셈의 발파암 기준 적용]", +"출처": [] +}, +"5": { +"이름": "합 계", +"출처": [] +} +}, +"이름조건": {}, +"정답": [ +"GF000213" +], +"허용": [], +"무리": "사람", +"복합": null +}, +{ +"id": "N021", +"파일": "북부청임도", +"이름": "큰돌쌓기(메쌓기)", +"규격": "직경40cm이상~60cm미만", +"단위": "㎡", +"갈래": "단가산출", +"출처": [ +[ +"산림품셈", +"13-6-1" +] +], +"직종근거": [ +"작업반장", +"특별인부", +"보통인부" +], +"인력": { +"작업반장": { +"품": 0.083, +"밑품": 0.083, +"율": [], +"어떻게": [ +"식" +] +}, +"특별인부": { +"품": 0.104, +"밑품": 0.104, +"율": [], +"어떻게": [ +"식" +] +}, +"보통인부": { +"품": 0.104, +"밑품": 0.104, +"율": [], +"어떻게": [ +"식" +] +} +}, +"인력절": [ +[ +"1.가", +"작업반장", +0.083, +0.083 +], +[ +"1.가", +"특별인부", +0.104, +0.104 +], +[ +"1.가", +"보통인부", +0.104, +0.104 +] +], +"장비": [ +{ +"갈래": [ +"굴착기", +"집게" +], +"규격": [ +0.8 +], +"시간": 0.384, +"밑시간": 0.384, +"율": [], +"절": "1.나", +"글": "굴착기(무한궤도)(0.8㎥+부착용집게)" +} +], +"자재": [], +"율": [], +"계수": {}, +"조건": { +"메찰": [ +"메쌓기", +"찰쌓기" +] +}, +"옛판": false, +"글": [ +"32.큰돌쌓기(메쌓기)(직경40cm이상~60cm미만)[㎡]", +" [산림사업표준품셈 13-6-1]", +" 1. 큰돌쌓기(찰쌓기)", +" 가. 노무비", +" 작업반장 : 215,907 × 0.83 ÷ 10㎡ = 17,920.2", +" 특별인부 : 226,122 × 1.04 ÷ 10㎡ = 23,516.6", +" 보통인부 : 172,068 × 1.04 ÷ 10㎡ = 17,895", +" 인부소계 : 59,331.8", +" 나. 굴착기(무한궤도)(0.8㎥+부착용집게)", +" Q = 3.84 ÷ 10㎡ = 0.384", +" 노무비 : 59,020 × 0.384 = 22,663.6", +" 재료비 : 24,421 × 0.384 = 9,377.6", +" 경 비 : 32,805 × 0.384 = 12,597.1", +" 소 계 : 44,638.3", +" 라. 잡재료비", +" (59,331.8 + 22,663.6) × 1% = 819.9", +" 마. 소 계", +" 노무비 : 59,331.8 + 22,663.6 + 819.9 = 82,815.3", +" 재료비 : 9,377.6", +" 경 비 : 12,597.1", +" 계 : 104,790", +" 2. 합 계", +" 노무비 : 82,815", +" 재료비 : 9,377", +" 경 비 : 12,597", +" 계 : 104,789" +], +"절": { +"1": { +"이름": "큰돌쌓기(찰쌓기)", +"출처": [] +}, +"1.가": { +"이름": "큰돌쌓기(찰쌓기) · 노무비", +"출처": [] +}, +"1.나": { +"이름": "큰돌쌓기(찰쌓기) · 굴착기(무한궤도)(0.8㎥+부착용집게)", +"출처": [] +}, +"1.라": { +"이름": "큰돌쌓기(찰쌓기) · 잡재료비", +"출처": [] +}, +"1.마": { +"이름": "큰돌쌓기(찰쌓기) · 소 계", +"출처": [] +}, +"2": { +"이름": "합 계", +"출처": [] +} +}, +"이름조건": { +"메찰": [ +"메쌓기" +] +}, +"정답": [ +"GF000226" +], +"허용": [ +"GC000260" +], +"무리": "사람", +"복합": null +}, +{ +"id": "N022", +"파일": "북부청임도", +"이름": "뒷채움돌채집운반뒷채움(발파)", +"규격": "굴삭기0.6㎥(90%),4.5톤,L=500m", +"단위": "㎥", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"1-10" +], +[ +"건설품셈", +"3-4-4" +] +], +"직종근거": [ +"보통인부", +"특별인부" +], +"인력": { +"보통인부": { +"품": 0.039, +"밑품": 0.309, +"율": [ +[ +0.1, +"%" +] +], +"어떻게": [ +"식", +"식" +] +}, +"특별인부": { +"품": 0.009, +"밑품": 0.009, +"율": [], +"어떻게": [ +"식" +] +} +}, +"인력절": [ +[ +"1.나", +"보통인부", +0.03, +0.3 +], +[ +"4.라", +"보통인부", +0.009, +0.009 +], +[ +"4.라", +"특별인부", +0.009, +0.009 +] +], +"장비": [ +{ +"갈래": [ +"굴착기" +], +"규격": [ +0.6 +], +"시간": 0.03896103896103896, +"밑시간": 0.04329004329004329, +"율": [ +[ +0.9, +"%" +] +], +"절": "1.가", +"글": "굴착기(타이어형)(0.6㎥)" +}, +{ +"갈래": [ +"굴착기" +], +"규격": [ +0.6 +], +"시간": 0.02939447383891828, +"밑시간": 0.02939447383891828, +"율": [], +"절": "2", +"글": "상 차 : 굴착기(타이어형)(0.6㎥)" +}, +{ +"갈래": [ +"덤프트럭" +], +"규격": [ +4.5 +], +"시간": 0.10834236186348863, +"밑시간": 0.10834236186348863, +"율": [], +"절": "3", +"글": "운 반 : 덤프트럭(4.5Ton)" +}, +{ +"갈래": [ +"굴착기" +], +"규격": [ +0.2 +], +"시간": 0.07272727272727272, +"밑시간": 0.07272727272727272, +"율": [], +"절": "4.가", +"글": "굴착기(무한궤도)(0.2㎥)" +}, +{ +"갈래": [ +"롤러" +], +"규격": [ +0.7 +], +"시간": 0.07272727272727272, +"밑시간": 0.07272727272727272, +"율": [], +"절": "4.다", +"글": "진동로울러(핸드가이드식)(0.7Ton)" +} +], +"자재": [], +"율": [], +"계수": { +"L": 500.0, +"q": 0.6, +"K": 0.55, +"f": 1.0, +"E": 0.35, +"Cm": 18.0, +"qs": 0.6, +"Es": 0.45, +"V1": 7.0, +"V2": 8.0, +"T": 4.5, +"rt": 1.75, +"t3": 1.1, +"t4": 0.7 +}, +"조건": {}, +"옛판": false, +"글": [ +"2.뒷채움돌채집운반뒷채움(발파)(굴삭기0.6㎥(90%),4.5톤,L=500m)[㎥]", +" 뒷채움돌 채집, 운반, 뒷채움 및 다짐 [건설표준품셈 준용]", +" 작업조건 : 발파후 파쇄암중에 뒷채움돌 채집(㎥)", +" 일정량 채취후 상차 및 운반(L=500m)", +" 1. 채 집", +" 가. 굴착기(타이어형)(0.6㎥)", +" q = 0.6㎥ K = 0.55 f = 1", +" E = 0.35 Cm = 18sec(90˚)", +" 3,600 × q × K × f × E 3,600 × 0.6 × 0.55 × 1 × 0.35", +" Q = ────────────── = ───────────────── = 23.1 ㎥/hr", +" Cm 18", +" 노무비 : 59,020 ÷ 23.1 × 90% = 2,299.4", +" 재료비 : 19,792 ÷ 23.1 × 90% = 771.1", +" 경 비 : 27,120 ÷ 23.1 × 90% = 1,056.6", +" 소 계 : 4,127.1", +" 나. 인 력 (10%)", +" 보통인부 : 172,068 × 0.3인/㎥ × 10% = 5,162", +" 다. 소 계", +" 노무비 : 2,299.4 + 5,162 = 7,461.4", +" 재료비 : 771.1", +" 경 비 : 1,056.6", +" 계 : 9,289.1", +" 2. 상 차 : 굴착기(타이어형)(0.6㎥)", +" qs = 0.6㎥ K = 0.7 f = 1", +" Es = 0.45 Cm = 20sec(135˚)", +" 3,600 × qs × K × f × Es 3,600 × 0.6 × 0.7 × 1 × 0.45", +" Q = ────────────── = ───────────────── = 34.02 ㎥/hr", +" Cm 20", +" 노무비 : 59,020 ÷ 34.02 = 1,734.8", +" 재료비 : 19,792 ÷ 34.02 = 581.7", +" 경 비 : 27,120 ÷ 34.02 = 797.1", +" 소 계 : 3,113.6", +" 3. 운 반 : 덤프트럭(4.5Ton)", +" L = 0.5km V1 = 7km/hr V2 = 8km/hr", +" T = 4.5ton rt = 1.75ton/㎥ E = 0.9", +" q = T/rt = 4.5/1.75 = 2.57 ㎥ K = 0.55", +" 건설표줌품셈(1-10) 자갈(습기)의 평균값 1.75 ton/㎥ 적용", +" n = q / (qs × K) = 2.57 / (0.6 × 0.55) = 7.79 회", +" t1 = 18 × n / (60 × Es) = 18 × 7.79 / (60 × 0.45) = 5.19 분", +" L L 0.5 0.5", +" t2 = (── + ──) × 60 = (── + ──) × 60 = 8.04분(왕복)", +" V1 V2 7 8", +" t3 = 1.1분 t4 = 0.7분", +" Cm = t1 + t2 + t3 + t4 = 5.19 + 8.04 + 1.1 + 0.7 = 15.03 분", +" 60 × q × f × E 60 × 2.57 × 1 × 0.9", +" Q = ─────────── = ──────────── = 9.23 ㎥/hr", +" Cm 15.03", +" 노무비 : 49,778 ÷ 9.23 = 5,393", +" 재료비 : 9,494 ÷ 9.23 = 1,028.6", +" 경 비 : 7,613 ÷ 9.23 = 824.8", +" 소 계 : 7,246.4", +" 4. 뒷채움 및 다짐 [건설공사건설표준품셈 3-4-4] 소형장비", +" 가. 굴착기(무한궤도)(0.2㎥)", +" 1일 시공량 : Qd = 110㎥/일", +" Q = Qd ÷ 8hr = 110 ÷ 8 = 13.75㎥/hr", +" 노무비 : 59,020 ÷ 13.75 = 4,292.3", +" 재료비 : 8,324 ÷ 13.75 = 605.3", +" 경 비 : 13,873 ÷ 13.75 = 1,008.9", +" 소 계 : 5,906.5", +" 다. 진동로울러(핸드가이드식)(0.7Ton)" +], +"절": { +"1": { +"이름": "채 집", +"출처": [] +}, +"1.가": { +"이름": "채 집 · 굴착기(타이어형)(0.6㎥)", +"출처": [] +}, +"1.나": { +"이름": "채 집 · 인 력 (10%)", +"출처": [] +}, +"1.다": { +"이름": "채 집 · 소 계", +"출처": [] +}, +"2": { +"이름": "상 차 : 굴착기(타이어형)(0.6㎥)", +"출처": [] +}, +"3": { +"이름": "운 반 : 덤프트럭(4.5Ton)", +"출처": [] +}, +"4": { +"이름": "합 계", +"출처": [] +}, +"4.가": { +"이름": "뒷채움 및 다짐 [건설공사건설표준품셈 3-4-4] 소형장비 · 굴착기(무한궤도)(0.2㎥)", +"출처": [ +[ +"건설품셈", +"3-4-4" +] +] +}, +"4.다": { +"이름": "뒷채움 및 다짐 [건설공사건설표준품셈 3-4-4] 소형장비 · 진동로울러(핸드가이드식)(0.7Ton)", +"출처": [ +[ +"건설품셈", +"3-4-4" +] +] +}, +"4.라": { +"이름": "뒷채움 및 다짐 [건설공사건설표준품셈 3-4-4] 소형장비 · 인 력", +"출처": [ +[ +"건설품셈", +"3-4-4" +] +] +}, +"4.마": { +"이름": "뒷채움 및 다짐 [건설공사건설표준품셈 3-4-4] 소형장비 · 소 계", +"출처": [ +[ +"건설품셈", +"3-4-4" +] +] +} +}, +"이름조건": {}, +"정답": [ +"GC000106" +], +"허용": [ +"GF000218" +], +"무리": "사람", +"복합": null +}, +{ +"id": "N023", +"파일": "북부청임도", +"이름": "큰돌쌓기(찰쌓기)_배수공", +"규격": "직경40cm이상~60cm미만", +"단위": "㎡", +"갈래": "단가산출", +"출처": [ +[ +"산림품셈", +"13-6-2" +] +], +"직종근거": [ +"작업반장", +"특별인부", +"보통인부" +], +"인력": { +"작업반장": { +"품": 0.083, +"밑품": 0.083, +"율": [], +"어떻게": [ +"식" +] +}, +"특별인부": { +"품": 0.13, +"밑품": 0.13, +"율": [], +"어떻게": [ +"식" +] +}, +"보통인부": { +"품": 0.13, +"밑품": 0.13, +"율": [], +"어떻게": [ +"식" +] +} +}, +"인력절": [ +[ +"1.가", +"작업반장", +0.083, +0.083 +], +[ +"1.가", +"특별인부", +0.13, +0.13 +], +[ +"1.가", +"보통인부", +0.13, +0.13 +] +], +"장비": [ +{ +"갈래": [ +"굴착기", +"집게" +], +"규격": [ +0.8 +], +"시간": 0.48, +"밑시간": 0.48, +"율": [], +"절": "1.나", +"글": "굴착기(무한궤도)(0.8㎥+부착용집게)" +} +], +"자재": [], +"율": [], +"계수": {}, +"조건": { +"메찰": [ +"찰쌓기" +] +}, +"옛판": false, +"글": [ +"31.큰돌쌓기(찰쌓기)_배수공(직경40cm이상~60cm미만)[㎡]", +" 큰돌쌓기(찰쌓기) [산림사업표준품셈 13-6-2]", +" 1. 큰돌쌓기(찰쌓기)", +" 가. 노무비", +" 작업반장 : 215,907 × 0.83 ÷ 10㎡ = 17,920.2", +" 특별인부 : 226,122 × 1.30 ÷ 10㎡ = 29,396", +" 보통인부 : 172,068 × 1.30 ÷ 10㎡ = 22,369", +" 인부소계 : 69,685.2", +" 나. 굴착기(무한궤도)(0.8㎥+부착용집게)", +" Q = 4.8 ÷ 10㎡ = 0.48", +" 노무비 : 59,020 × 0.48 = 28,329.6", +" 재료비 : 24,421 × 0.48 = 11,722", +" 경 비 : 32,805 × 0.48 = 15,746.4", +" 소 계 : 55,798", +" 다. 제잡비 비율(물빼기 파이프 설치시)", +" (69,685.2 + 28,329.6) × 9% = 8,821.3", +" 라. 소 계", +" 노무비 : 69,685.2 + 28,329.6 + 8,821.3 = 106,836.1", +" 재료비 : 11,722", +" 경 비 : 15,746.4", +" 계 : 134,304.5", +" 2. 합 계", +" 노무비 : 106,836", +" 재료비 : 11,722", +" 경 비 : 15,746", +" 계 : 134,304" +], +"절": { +"1": { +"이름": "큰돌쌓기(찰쌓기)", +"출처": [] +}, +"1.가": { +"이름": "큰돌쌓기(찰쌓기) · 노무비", +"출처": [] +}, +"1.나": { +"이름": "큰돌쌓기(찰쌓기) · 굴착기(무한궤도)(0.8㎥+부착용집게)", +"출처": [] +}, +"1.다": { +"이름": "큰돌쌓기(찰쌓기) · 제잡비 비율(물빼기 파이프 설치시)", +"출처": [] +}, +"1.라": { +"이름": "큰돌쌓기(찰쌓기) · 소 계", +"출처": [] +}, +"2": { +"이름": "합 계", +"출처": [] +} +}, +"이름조건": { +"메찰": [ +"찰쌓기" +] +}, +"정답": [ +"GF000227" +], +"허용": [], +"무리": "사람", +"복합": null +}, +{ +"id": "N024", +"파일": "북부청임도", +"이름": "레미콘타설(인력운반)", +"규격": "무근구조물-소량 타설", +"단위": "㎥", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"6-1-1" +] +], +"직종근거": [ +"콘크리트공", +"보통인부" +], +"인력": { +"콘크리트공": { +"품": 0.26087, +"밑품": 0.26087, +"율": [], +"어떻게": [ +"식" +] +}, +"보통인부": { +"품": 0.26087, +"밑품": 0.26087, +"율": [], +"어떻게": [ +"식" +] +} +}, +"인력절": [ +[ +"2", +"콘크리트공", +0.26087, +0.26087 +], +[ +"2", +"보통인부", +0.26087, +0.26087 +] +], +"장비": [], +"자재": [ +{ +"이름": "레미콘", +"품": null, +"밑품": null +} +], +"율": [ +[ +"노무비", +2.0 +] +], +"계수": {}, +"조건": { +"근": [ +"무근" +], +"규모": [ +"소량" +], +"방법": [ +"인력운반" +] +}, +"옛판": false, +"글": [ +"21.레미콘타설(인력운반) (무근구조물-소량 타설) [㎥]", +" 건설표준품셈 6-1-1", +" 1. 레미콘 : 별도계상", +" 2. 타설비", +" A = 23㎥/일", +" 콘크리트공 : 273,540 × 3인 / 23 ×2 = 71,358.2", +" 보 통 인부 : 172,068 × 3인 / 23 ×2 = 44,887.3", +" 인 부 소계 : 116,245.5", +" 공구손료 및 경장비(콘크리트 진동기등)의 기계경비(노무비의 2%)", +" 116,245.5 × 2% = 2,324.9", +" 3. 합 계", +" 노무비 : 116,245", +" 재료비 : 2,324", +" 경 비 :", +" 계 : 118,569" +], +"절": { +"1": { +"이름": "레미콘 : 별도계상", +"출처": [] +}, +"2": { +"이름": "타설비", +"출처": [] +}, +"3": { +"이름": "합 계", +"출처": [] +} +}, +"이름조건": { +"근": [ +"무근" +], +"규모": [ +"소량" +], +"방법": [ +"인력운반" +] +}, +"정답": [ +"GC000198" +], +"허용": [ +"GF000155" +], +"무리": "사람", +"복합": null +}, +{ +"id": "N025", +"파일": "북부청임도", +"이름": "합판거푸집", +"규격": "6회", +"단위": "㎡", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"6-3-1" +] +], +"직종근거": [ +"형틀목공", +"보통인부" +], +"인력": { +"형틀목공": { +"품": 0.1, +"밑품": 0.1, +"율": [], +"어떻게": [ +"식" +] +}, +"보통인부": { +"품": 0.04, +"밑품": 0.04, +"율": [], +"어떻게": [ +"식" +] +} +}, +"인력절": [ +[ +"2", +"형틀목공", +0.1, +0.1 +], +[ +"2", +"보통인부", +0.04, +0.04 +] +], +"장비": [], +"자재": [ +{ +"이름": "내수합판1급(12mm1220×2440)", +"품": 0.33681, +"밑품": 1.03 +}, +{ +"이름": "각재(거푸집용,외송) (3.6×3.0×3.0)", +"품": 0.012426, +"밑품": 0.038 +} +], +"율": [ +[ +"주자재비", +11.0 +], +[ +"노무비", +1.0 +] +], +"계수": {}, +"조건": { +"난이": [ +"간단" +], +"횟수": [ +"6회" +] +}, +"옛판": false, +"글": [ +"25.합판거푸집(6회)[㎡]", +" 건설공사표준품셈, 6-3-1 합판거푸집", +" ※ 사용횟수 6회 : 간단", +" 1. 재료비", +" 내수합판1급(12mm1220×2440) 10,984 × 1.03㎡ × 32.7% = 3,699.5", +" 각재(거푸집용,외송) (3.6×3.0×3.0) 626,543 × 0.038㎥ × 32.7% = 7,785.4", +" 주자재소계 : 11,484.9", +" 소모자재(박리재등)(주자재비의 11%)", +" 11,484.9 × 11% = 1,263.3", +" 2. 인건비", +" Q = 50㎡/일", +" 형틀목공 : 275,790 × 5인 ÷ 50 = 27,579", +" 보통인부 : 172,068 × 2인 ÷ 50 = 6,882.7", +" 인부소계 : 34,461.7", +" 공구손료 및 경장비 기계경비(노무비의 1%)", +" 34,461.7 × 1% = 344.6", +" 3. 합 계", +" 노무비 : 34,461", +" 재료비 : 11,484.9 + 1,263.3 + 344.6 = 13,092", +" 경 비 :", +" 계 : 47,553" +], +"절": { +"1": { +"이름": "재료비", +"출처": [] +}, +"2": { +"이름": "인건비", +"출처": [] +}, +"3": { +"이름": "합 계", +"출처": [] +} +}, +"이름조건": { +"횟수": [ +"6회" +] +}, +"정답": [ +"GC000211" +], +"허용": [ +"GF000160" +], +"무리": "사람", +"복합": null +}, +{ +"id": "N026", +"파일": "북부청임도", +"이름": "도로반사경설치", +"규격": "D=1000(STS)", +"단위": "개소", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"1-8-3" +] +], +"직종근거": [ +"특별인부", +"보통인부" +], +"인력": { +"특별인부": { +"품": 0.25, +"밑품": 0.25, +"율": [], +"어떻게": [ +"식" +] +}, +"보통인부": { +"품": 0.25, +"밑품": 0.25, +"율": [], +"어떻게": [ +"식" +] +} +}, +"인력절": [ +[ +"2", +"특별인부", +0.25, +0.25 +], +[ +"2", +"보통인부", +0.25, +0.25 +] +], +"장비": [], +"자재": [], +"율": [], +"계수": {}, +"조건": {}, +"옛판": false, +"글": [ +"42.도로반사경설치(D=1000(STS))[개소]", +" [건설공사표준품셈 1-8-3]", +" 1. 재료비", +" 도로반사경(지주포함)(D=1000mm(STS) 220,000 × 1EA = 220,000", +" 2. 인건비", +" 특별인부 : 226,122 ÷ 1인 ÷ 4본/일 = 56,530.5", +" 보통인부 : 172,068 ÷ 1인 ÷ 4본/일 = 43,017", +" 인부소계 : 99,547.5", +" 공구손료 및 경장비", +" 99,547.5 × 3% = 2,986.4", +" 3. 합 계", +" 노무비 : 99,547", +" 재료비 : 220,000", +" 경 비 : 2,986", +" 계 : 322,533" +], +"절": { +"1": { +"이름": "재료비", +"출처": [] +}, +"2": { +"이름": "인건비", +"출처": [] +}, +"3": { +"이름": "합 계", +"출처": [] +} +}, +"이름조건": {}, +"정답": [ +"GC000310" +], +"허용": [], +"무리": "사람", +"복합": null +}, +{ +"id": "N027", +"파일": "북부청임도", +"이름": "야면석메붙임", +"규격": "L3 =35cm내외,장비사용", +"단위": "㎡", +"갈래": "단가산출", +"출처": [ +[ +"산림품셈", +"13-5-2" +] +], +"직종근거": [ +"석공", +"보통인부" +], +"인력": { +"석공": { +"품": 0.13, +"밑품": 0.13, +"율": [], +"어떻게": [ +"식" +] +}, +"보통인부": { +"품": 0.04, +"밑품": 0.04, +"율": [], +"어떻게": [ +"식" +] +} +}, +"인력절": [ +[ +"1", +"석공", +0.13, +0.13 +], +[ +"1", +"보통인부", +0.04, +0.04 +] +], +"장비": [ +{ +"갈래": [ +"굴착기", +"집게" +], +"규격": [ +0.6 +], +"시간": 0.25, +"밑시간": 0.25, +"율": [], +"절": "2", +"글": "굴착기(타이어형)(0.6㎥+부착용집게)" +} +], +"자재": [], +"율": [], +"계수": {}, +"조건": { +"메찰": [ +"메붙임", +"찰붙임" +], +"방법": [ +"인력", +"장비" +] +}, +"옛판": false, +"글": [ +"88.야면석메붙임(L3 =35cm내외,장비사용)[㎡]", +" 야면석 메붙임 [산림사업표준품셈 13-5-2]", +" 야면석 찰붙임 [뒷길이35cm적용]", +" 1. 인력", +" 석 공 : 268,908 × 0.13 = 34,958", +" 보통인부 : 172,068 × 0.04 = 6,882.7", +" 인부소계 : 41,840.7", +" 2. 굴착기(타이어형)(0.6㎥+부착용집게)", +" Q = 0.25hr", +" 노무비 : 59,020 × 0.25 = 14,755", +" 재료비 : 18,515 × 0.25 = 4,628.7", +" 경 비 : 32,906 × 0.25 = 8,226.5", +" 소 계 : 27,610.2", +" 3. 합 계", +" 노무비 : 41,840.7 + 14,755 = 56,595", +" 재료비 : 4,628", +" 경 비 : 8,226", +" 계 : 69,449" +], +"절": { +"1": { +"이름": "인력", +"출처": [] +}, +"2": { +"이름": "굴착기(타이어형)(0.6㎥+부착용집게)", +"출처": [] +}, +"3": { +"이름": "합 계", +"출처": [] +} +}, +"이름조건": { +"메찰": [ +"메붙임" +], +"방법": [ +"장비" +] +}, +"정답": [ +"GF000224" +], +"허용": [ +"GC000262" +], +"무리": "사람", +"복합": null +}, +{ +"id": "N028", +"파일": "북부청임도", +"이름": "큰돌메붙임", +"규격": "직경40cm이상~60cm미만", +"단위": "㎡", +"갈래": "단가산출", +"출처": [ +[ +"산림품셈", +"13-7-1" +] +], +"직종근거": [ +"작업반장", +"특별인부", +"보통인부" +], +"인력": { +"작업반장": { +"품": 0.058, +"밑품": 0.058, +"율": [], +"어떻게": [ +"식" +] +}, +"특별인부": { +"품": 0.058, +"밑품": 0.058, +"율": [], +"어떻게": [ +"식" +] +}, +"보통인부": { +"품": 0.083, +"밑품": 0.083, +"율": [], +"어떻게": [ +"식" +] +} +}, +"인력절": [ +[ +"1.가", +"작업반장", +0.058, +0.058 +], +[ +"1.가", +"특별인부", +0.058, +0.058 +], +[ +"1.가", +"보통인부", +0.083, +0.083 +] +], +"장비": [ +{ +"갈래": [ +"굴착기", +"집게" +], +"규격": [ +0.8 +], +"시간": 0.24, +"밑시간": 0.24, +"율": [], +"절": "1.나", +"글": "굴착기(무한궤도)(0.8㎥+부착용집게)" +} +], +"자재": [], +"율": [], +"계수": {}, +"조건": { +"메찰": [ +"메붙이기", +"메붙임" +] +}, +"옛판": false, +"글": [ +"92.큰돌메붙임(직경40cm이상~60cm미만)[㎡]", +" 큰돌메붙임 [산림사업표준품셈 13-7-1]", +" 1. 큰돌메붙이기, 뒷길이 0.60cm기준", +" 가. 노무비", +" 작업반장 : 215,907 × 0.58 ÷ 10㎡ = 12,522.6", +" 특별인부 : 226,122 × 0.58 ÷ 10㎡ = 13,115", +" 보통인부 : 172,068 × 0.83 ÷ 10㎡ = 14,281.6", +" 인부소계 : 39,919.2", +" 나. 굴착기(무한궤도)(0.8㎥+부착용집게)", +" Q = 2.4 ÷ 10㎡= 0.24", +" 노무비 : 59,020 × 0.24 = 14,164.8", +" 재료비 : 24,421 × 0.24 = 5,861", +" 경 비 : 32,805 × 0.24 = 7,873.2", +" 소 계 : 27,899", +" 다. 제잡비 비율", +" (39,919.2 + 14,164.8) × 1% = 540.8", +" 라. 소 계", +" 노무비 : 39,919.2 + 14,164.8 = 54,084", +" 재료비 : 5,861", +" 경 비 : 7,873.2 + 540.8 = 8,414", +" 계 : 68,359", +" 2. 합 계", +" 노무비 : 54,084", +" 재료비 : 5,861", +" 경 비 : 8,414", +" 계 : 68,359" +], +"절": { +"1": { +"이름": "큰돌메붙이기, 뒷길이 0.60cm기준", +"출처": [] +}, +"1.가": { +"이름": "큰돌메붙이기, 뒷길이 0.60cm기준 · 노무비", +"출처": [] +}, +"1.나": { +"이름": "큰돌메붙이기, 뒷길이 0.60cm기준 · 굴착기(무한궤도)(0.8㎥+부착용집게)", +"출처": [] +}, +"1.다": { +"이름": "큰돌메붙이기, 뒷길이 0.60cm기준 · 제잡비 비율", +"출처": [] +}, +"1.라": { +"이름": "큰돌메붙이기, 뒷길이 0.60cm기준 · 소 계", +"출처": [] +}, +"2": { +"이름": "합 계", +"출처": [] +} +}, +"이름조건": { +"메찰": [ +"메붙임" +] +}, +"정답": [ +"GF000229" +], +"허용": [ +"GC000262" +], +"무리": "사람", +"복합": null +}, +{ +"id": "N029", +"파일": "북부청임도", +"이름": "큰돌찰붙임", +"규격": "직경40cm이상~60cm미만", +"단위": "㎡", +"갈래": "단가산출", +"출처": [ +[ +"산림품셈", +"13-7-2" +] +], +"직종근거": [ +"작업반장", +"특별인부", +"보통인부" +], +"인력": { +"작업반장": { +"품": 0.058, +"밑품": 0.058, +"율": [], +"어떻게": [ +"식" +] +}, +"특별인부": { +"품": 0.101, +"밑품": 0.101, +"율": [], +"어떻게": [ +"식" +] +}, +"보통인부": { +"품": 0.101, +"밑품": 0.101, +"율": [], +"어떻게": [ +"식" +] +} +}, +"인력절": [ +[ +"1.가", +"작업반장", +0.058, +0.058 +], +[ +"1.가", +"특별인부", +0.101, +0.101 +], +[ +"1.가", +"보통인부", +0.101, +0.101 +] +], +"장비": [ +{ +"갈래": [ +"굴착기", +"집게" +], +"규격": [ +0.8 +], +"시간": 0.336, +"밑시간": 0.336, +"율": [], +"절": "1.나", +"글": "굴착기(무한궤도)(0.8㎥+부착용집게)" +} +], +"자재": [], +"율": [], +"계수": {}, +"조건": { +"메찰": [ +"찰붙임", +"찰쌓기" +] +}, +"옛판": false, +"글": [ +"91.큰돌찰붙임(직경40cm이상~60cm미만)[㎡]", +" 큰돌찰붙임 [산림사업표준품셈 13-7-2]", +" 1. 큰돌쌓기(찰쌓기)", +" 가. 노무비", +" 작업반장 : 215,907 × 0.58 ÷ 10㎡ = 12,522.6", +" 특별인부 : 226,122 × 1.01 ÷ 10㎡ = 22,838.3", +" 보통인부 : 172,068 × 1.01 ÷ 10㎡ = 17,378.8", +" 인부소계 : 52,739.7", +" 나. 굴착기(무한궤도)(0.8㎥+부착용집게)", +" Q = 3.36 ÷ 10㎡= 0.336", +" 노무비 : 59,020 × 0.336 = 19,830.7", +" 재료비 : 24,421 × 0.336 = 8,205.4", +" 경 비 : 32,805 × 0.336 = 11,022.4", +" 소 계 : 39,058.5", +" 라. 제잡비 비율", +" (52,739.7 + 19,830.7) × 3% = 2,177.1", +" 마. 소 계", +" 노무비 : 52,739.7 + 19,830.7 = 72,570.4", +" 재료비 : 8,205.4", +" 경 비 : 11,022.4 + 2,177.1 = 13,199.5", +" 계 : 93,975.3", +" 2. 합 계", +" 노무비 : 72,570", +" 재료비 : 8,205", +" 경 비 : 13,199", +" 계 : 93,974" +], +"절": { +"1": { +"이름": "큰돌쌓기(찰쌓기)", +"출처": [] +}, +"1.가": { +"이름": "큰돌쌓기(찰쌓기) · 노무비", +"출처": [] +}, +"1.나": { +"이름": "큰돌쌓기(찰쌓기) · 굴착기(무한궤도)(0.8㎥+부착용집게)", +"출처": [] +}, +"1.라": { +"이름": "큰돌쌓기(찰쌓기) · 제잡비 비율", +"출처": [] +}, +"1.마": { +"이름": "큰돌쌓기(찰쌓기) · 소 계", +"출처": [] +}, +"2": { +"이름": "합 계", +"출처": [] +} +}, +"이름조건": { +"메찰": [ +"찰붙임" +] +}, +"정답": [ +"GF000230" +], +"허용": [ +"GC000263" +], +"무리": "사람", +"복합": null +}, +{ +"id": "N030", +"파일": "북부청임도", +"이름": "레미콘타설(장비사용)", +"규격": "소형구조물", +"단위": "㎥", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"6-1-1" +] +], +"직종근거": [ +"콘크리트공", +"보통인부" +], +"인력": { +"콘크리트공": { +"품": 0.095238, +"밑품": 0.095238, +"율": [], +"어떻게": [ +"식" +] +}, +"보통인부": { +"품": 0.031746, +"밑품": 0.031746, +"율": [], +"어떻게": [ +"식" +] +} +}, +"인력절": [ +[ +"2", +"콘크리트공", +0.095238, +0.095238 +], +[ +"2", +"보통인부", +0.031746, +0.031746 +] +], +"장비": [ +{ +"갈래": [ +"굴착기" +], +"규격": [ +0.6 +], +"시간": 0.25, +"밑시간": 0.25, +"율": [], +"절": "3", +"글": "굴착기(타이어형)(0.6㎥)" +} +], +"자재": [ +{ +"이름": "레미콘", +"품": null, +"밑품": null +} +], +"율": [ +[ +"노무비", +2.0 +] +], +"계수": {}, +"조건": { +"규모": [ +"소형" +], +"방법": [ +"장비" +] +}, +"옛판": false, +"글": [ +"57.레미콘타설(장비사용)(소형구조물)[㎥]", +" 건설공사표준품셈 6-1-1", +" 1. 레미콘 : 별도계상", +" 2. 타설비", +" A = 63인", +" 콘크리트공 : 273,540 × 3인 ÷ 63 × 2 = 26,051.4", +" 보통인부 : 172,068 × 1인 ÷ 63 × 2 = 5,462.4", +" 인부소계 : 31,513.8", +" 공구손료 및 경장비(콘크리트 진동기등)의 기계경비(노무비의 2%)", +" 31,513.8 × 2% = 630.2", +" 3. 굴착기(타이어형)(0.6㎥)", +" Q = 1 × 8hr ÷ A × 2 = 1 × 8 ÷ 63 × 2 = 0.25hr/㎥", +" 노무비 : 59,020 × 0.25 = 14,755", +" 재료비 : 19,792 × 0.25 = 4,948", +" 경 비 : 27,120 × 0.25 = 6,780", +" 소 계 : 26,483", +" 4. 합 계", +" 노무비 : 31,513.8 + 14,755 = 46,268", +" 재료비 : 630.2 + 4,948 = 5,578", +" 경 비 : 6,780", +" 계 : 58,626" +], +"절": { +"1": { +"이름": "레미콘 : 별도계상", +"출처": [] +}, +"2": { +"이름": "타설비", +"출처": [] +}, +"3": { +"이름": "굴착기(타이어형)(0.6㎥)", +"출처": [] +}, +"4": { +"이름": "합 계", +"출처": [] +} +}, +"이름조건": { +"규모": [ +"소형" +], +"방법": [ +"장비" +] +}, +"정답": [ +"GC000198" +], +"허용": [ +"GF000155" +], +"무리": "사람", +"복합": null +}, +{ +"id": "N031", +"파일": "북부청임도", +"이름": "야면석찰쌓기", +"규격": "L3=35cm적용,장비사용", +"단위": "㎡", +"갈래": "단가산출", +"출처": [ +[ +"산림품셈", +"13-4-5" +] +], +"직종근거": [ +"석공", +"보통인부" +], +"인력": { +"석공": { +"품": 0.09, +"밑품": 0.09, +"율": [], +"어떻게": [ +"식" +] +}, +"보통인부": { +"품": 0.05, +"밑품": 0.05, +"율": [], +"어떻게": [ +"식" +] +} +}, +"인력절": [ +[ +"1", +"석공", +0.09, +0.09 +], +[ +"1", +"보통인부", +0.05, +0.05 +] +], +"장비": [ +{ +"갈래": [ +"굴착기", +"집게" +], +"규격": [ +0.6 +], +"시간": 0.31, +"밑시간": 0.31, +"율": [], +"절": "2", +"글": "굴착기(타이어형)(0.6㎥+부착용집게)" +} +], +"자재": [], +"율": [], +"계수": {}, +"조건": { +"메찰": [ +"찰쌓기" +], +"난이": [ +"보통" +], +"방법": [ +"인력", +"장비" +] +}, +"옛판": false, +"글": [ +"71.야면석찰쌓기(L3=35cm적용,장비사용)[㎡]", +" 산림사업표준품셈 13-4-5", +" 1. 인력", +" 석 공 : 268,908 × 0.09 = 24,201.7", +" 보통인부 : 172,068 × 0.05 = 8,603.4", +" 인부소계 :32,805.1", +" 2. 굴착기(타이어형)(0.6㎥+부착용집게)", +" Q = 0.31hr", +" 노무비 : 59,020 × 0.31 = 18,296.2", +" 재료비 : 18,515 × 0.31 = 5,739.6", +" 경 비 : 32,906 × 0.31 = 10,200.8", +" 소 계 : 34,236.6", +" 3. 합 계", +" 노무비 : 32,805.1 + 18,296.2 = 51,101", +" 재료비 : 5,739", +" 경 비 : 10,200", +" 계 : 67,040" +], +"절": { +"1": { +"이름": "인력", +"출처": [] +}, +"2": { +"이름": "굴착기(타이어형)(0.6㎥+부착용집게)", +"출처": [] +}, +"3": { +"이름": "합 계", +"출처": [] +} +}, +"이름조건": { +"메찰": [ +"찰쌓기" +], +"방법": [ +"장비" +] +}, +"정답": [ +"GF000222" +], +"허용": [ +"GC000261" +], +"무리": "사람", +"복합": null +}, +{ +"id": "N032", +"파일": "북부청임도", +"이름": "면고르기", +"규격": "", +"단위": "㎡", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"3-5-1" +] +], +"직종근거": [], +"인력": {}, +"인력절": [], +"장비": [ +{ +"갈래": [ +"굴착기" +], +"규격": [ +1.0 +], +"시간": 0.027586206896551724, +"밑시간": 0.027586206896551724, +"율": [], +"절": "1", +"글": "굴착기(무한궤도)(1.0㎥)" +} +], +"자재": [], +"율": [], +"계수": {}, +"조건": { +"토질": [ +"사질토", +"점질토" +] +}, +"옛판": false, +"글": [ +"43.면고르기[㎡]", +" 절토면고르기[2025년 건설표준품셈 3-5-1 절토면 고르기]", +" 연질토 및 불순자갈 토질 기준", +" 1일 시공량 모래,사질토,점토,점질토 390㎥/일", +" 연질토,불순자갈 250㎥/일", +" 호박돌 섞인 고결토, 경질토 230㎥/일", +" 평균치 적용", +" 1. 굴착기(무한궤도)(1.0㎥)", +" Q = (390+250+230)/3 / 8 = 36.25 ㎥/hr", +" 노무비 : 59,020 ÷ 36.25 = 1,628.1", +" 재료비 : 32,735 ÷ 36.25 = 903", +" 경 비 : 29,621 ÷ 36.25 = 817.1", +" 소 계 : 3,348.2", +" 2. 합 계", +" 노무비 : 1,628", +" 재료비 : 903", +" 경 비 : 817", +" 계 : 3,348" +], +"절": { +"1": { +"이름": "굴착기(무한궤도)(1.0㎥)", +"출처": [] +}, +"2": { +"이름": "합 계", +"출처": [] +} +}, +"이름조건": {}, +"정답": [ +"GC000111" +], +"허용": [ +"GF000124" +], +"무리": "사람", +"복합": null +}, +{ +"id": "N033", +"파일": "북부청임도", +"이름": "대형브레이카", +"규격": "+굴착기(0.7㎥)", +"단위": "hr", +"갈래": "기계", +"출처": [], +"직종근거": [], +"인력": {}, +"인력절": [], +"장비": [ +{ +"갈래": [ +"굴착기", +"브레이커" +], +"규격": [ +0.7 +], +"시간": 1.0, +"밑시간": null, +"절": 0, +"글": "대형브레이카" +} +], +"자재": [], +"율": [], +"계수": {}, +"조건": { +"규모": [ +"대형" +] +}, +"옛판": false, +"글": [], +"절": {}, +"이름조건": { +"규모": [ +"대형" +] +}, +"정답": [ +"GC000271" +], +"허용": [], +"무리": "사람", +"복합": null +}, +{ +"id": "N034", +"파일": "북부청임도", +"이름": "와이어메시바닥깔기", +"규격": "#6100×100", +"단위": "㎡", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"8-1-3" +] +], +"직종근거": [ +"특별인부" +], +"인력": { +"특별인부": { +"품": 0.006, +"밑품": 0.006, +"율": [], +"어떻게": [ +"식" +] +} +}, +"인력절": [ +[ +"3", +"특별인부", +0.006, +0.006 +] +], +"장비": [], +"자재": [ +{ +"이름": "와이어메쉬(#6,100×100)", +"품": 1.05, +"밑품": 1.05 +} +], +"율": [ +[ +"주재료비", +3.0 +] +], +"계수": {}, +"조건": {}, +"옛판": false, +"글": [ +"60.와이어메시바닥깔기(#6100×100)[㎡]", +" [건설표준품셈(8-1-3)]", +" 1. 재료비", +" 와이어메쉬(#6,100×100) 4,640 × 1.05 = 4,872", +" 2. 잡재료비(주재료비의 3%)", +" 4,872 × 3% = 146.1", +" 3. 인건비", +" 특별인부 : 226,122 × 0.006 = 1,356.7", +" 4. 합 계", +" 노무비 : 1,356", +" 재료비 : 4,872 + 146.1 = 5,018", +" 경 비 :", +" 계 : 6,374" +], +"절": { +"1": { +"이름": "재료비", +"출처": [] +}, +"2": { +"이름": "잡재료비(주재료비의 3%)", +"출처": [] +}, +"3": { +"이름": "인건비", +"출처": [] +}, +"4": { +"이름": "합 계", +"출처": [] +} +}, +"이름조건": {}, +"정답": [ +"GC000665" +], +"허용": [], +"무리": "사람", +"복합": null +}, +{ +"id": "N035", +"파일": "북부청임도", +"이름": "원형돌망태 설치 및 돌채움", +"규격": "#8,45×45 cm", +"단위": "㎡", +"갈래": "단가산출", +"출처": [ +[ +"산림품셈", +"13-11-1" +] +], +"직종근거": [ +"보통인부" +], +"인력": { +"보통인부": { +"품": 0.25, +"밑품": 0.25, +"율": [], +"어떻게": [ +"식", +"식" +] +} +}, +"인력절": [ +[ +"2", +"보통인부", +0.08, +0.08 +], +[ +"3", +"보통인부", +0.17, +0.17 +] +], +"장비": [], +"자재": [ +{ +"이름": "원형돌망태(몸통) #8 45㎝×45㎝", +"품": null, +"밑품": null +}, +{ +"이름": "원형돌망태(뚜껑) #8 45㎝×45㎝", +"품": null, +"밑품": null +} +], +"율": [], +"계수": {}, +"조건": { +"형": [ +"원형" +] +}, +"옛판": false, +"글": [ +"62.원형돌망태설치및돌채움(#8,45×45cm)[㎡]", +" [산림사업표준품셈 13-11-1]", +" 1. 재료비", +" 원형돌망태(몸통) #8 45㎝×45㎝ : - 별산 -", +" 원형돌망태(뚜껑) #8 45㎝×45㎝ : - 별산 -", +" 2. 조립 설치", +" 보통인부 : 172,068 × 0.08인 = 13,765.4", +" 3. 돌채움", +" 보통인부 : 172,068 × 0.17인 = 29,251.5", +" 4. 합 계", +" 노무비 : 13,765.4 + 29,251.5 = 43,016", +" 재료비 :", +" 경 비 :", +" 계 : 43,016" +], +"절": { +"1": { +"이름": "재료비", +"출처": [] +}, +"2": { +"이름": "조립 설치", +"출처": [] +}, +"3": { +"이름": "돌채움", +"출처": [] +}, +"4": { +"이름": "합 계", +"출처": [] +} +}, +"이름조건": { +"형": [ +"원형" +] +}, +"정답": [ +"GF000328" +], +"허용": [], +"무리": "사람", +"복합": null +}, +{ +"id": "N036", +"파일": "북부청임도", +"이름": "조약돌채집및운반", +"규격": "굴삭기0.6㎥(90%),4.5톤,L=500m", +"단위": "㎥", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"1-2-3" +] +], +"직종근거": [ +"보통인부" +], +"인력": { +"보통인부": { +"품": 0.03, +"밑품": 0.3, +"율": [ +[ +0.1, +"%" +] +], +"어떻게": [ +"식" +] +} +}, +"인력절": [ +[ +"1.나", +"보통인부", +0.03, +0.3 +] +], +"장비": [ +{ +"갈래": [ +"굴착기" +], +"규격": [ +0.6 +], +"시간": 0.03896103896103896, +"밑시간": 0.04329004329004329, +"율": [ +[ +0.9, +"%" +] +], +"절": "1.가", +"글": "굴착기(타이어형)(0.6㎥)" +}, +{ +"갈래": [ +"굴착기" +], +"규격": [ +0.6 +], +"시간": 0.02939447383891828, +"밑시간": 0.02939447383891828, +"율": [], +"절": "2", +"글": "상 차 : 굴착기(타이어형)(0.6㎥)" +}, +{ +"갈래": [ +"덤프트럭" +], +"규격": [ +4.5 +], +"시간": 0.07961783439490445, +"밑시간": 0.07961783439490445, +"율": [], +"절": "3", +"글": "운 반 : 덤프트럭(4.5Ton)" +} +], +"자재": [], +"율": [], +"계수": { +"q": 0.6, +"K": 0.55, +"f": 1.0, +"E": 0.35, +"Cm": 18.0, +"qs": 0.6, +"Es": 0.45, +"L": 0.5, +"V1": 10.0, +"V2": 15.0, +"T": 4.5, +"rt": 1.75, +"t3": 1.1, +"t4": 0.42 +}, +"조건": {}, +"옛판": false, +"글": [ +"61.조약돌채집및운반(굴삭기0.6㎥(90%),4.5톤,L=500m)[㎥]", +" 조약돌 채집 및 운반(㎥) [건설표준품셈 준용]", +" 작업조건 : 계곡에 산재한 조약돌(Φ100~300mm) 채집", +" 1. 채 집", +" 가. 굴착기(타이어형)(0.6㎥)", +" q = 0.6㎥ K = 0.55 f = 1", +" E = 0.35 Cm = 18sec(90˚)", +" 3,600 × q × K × f × E 3,600 × 0.6 × 0.55 × 1 × 0.35", +" Q = ────────────── = ───────────────── = 23.1 ㎥/hr", +" Cm 18", +" 노무비 : 59,020 ÷ 23.1 × 90% = 2,299.4", +" 재료비 : 19,792 ÷ 23.1 × 90% = 771.1", +" 경 비 : 27,120 ÷ 23.1 × 90% = 1,056.6", +" 소 계 : 4,127.1", +" 나. 인 력(10%)", +" 보통인부 : 172,068 × 0.3인/㎥ × 10% = 5,162", +" 다. 소 계", +" 노무비 : 2,299.4 + 5,162 = 7,461.4", +" 재료비 : 771.1", +" 경 비 : 1,056.6", +" 계 : 9,289.1", +" 2. 상 차 : 굴착기(타이어형)(0.6㎥)", +" qs = 0.6㎥ K = 0.7 f = 1", +" Es = 0.45 Cm = 20sec(135˚)", +" 3,600 × qs × K × f × Es 3,600 × 0.6 × 0.7 × 1 × 0.45", +" Q = ────────────── = ───────────────── = 34.02 ㎥/hr", +" Cm 20", +" 노무비 : 59,020 ÷ 34.02 = 1,734.8", +" 재료비 : 19,792 ÷ 34.02 = 581.7", +" 경 비 : 27,120 ÷ 34.02 = 797.1", +" 소 계 : 3,113.6", +" 3. 운 반 : 덤프트럭(4.5Ton)", +" L = 0.5km V1 = 10km/hr V2 = 15km/hr", +" T = 4.5ton rt = 1.75ton/㎥ E = 0.9", +" q = T / rt = 4.5 / 1.75 = 2.57 ㎥", +" 건설표줌품셈(1-2-3) 자갈(습기)의 평균값 1.75 ton/㎥ 적용", +" n = q / (qs × K) = 2.57 / (0.6 × 0.7) = 6.12 회", +" t1 = 20 × n / (60 × Es) = 20 × 6.12 / (60 × 0.45) = 4.53분", +" L L 0.5 0.5", +" t2 = (── + ──) × 60 = (── + ──) × 60 = 5분(왕복)", +" V1 V2 10 15", +" t3 = 1.1분 t4 = 0.42분", +" Cm = t1 + t2 + t3 + t4 = 4.53 + 5 + 1.1 + 0.42 = 11.05 분", +" 60 × q × f × E 60 × 2.57 × 1 × 0.9", +" Q = ─────────── = ──────────── = 12.56 ㎥/hr", +" Cm 11.05", +" 노무비 : 49,778 ÷ 12.56 = 3,963.2", +" 재료비 : 9,494 ÷ 12.56 = 755.8", +" 경 비 : 7,613 ÷ 12.56 = 606.1", +" 소 계 : 5,325.1", +" 4. 합 계", +" 노무비 : 7,461.4 + 1,734.8 + 3,963.2 = 13,159", +" 재료비 : 771.1 + 581.7 + 755.8 = 2,108", +" 경 비 : 1,056.6 + 797.1 + 606.1 = 2,459", +" 계 : 17,726" +], +"절": { +"1": { +"이름": "채 집", +"출처": [] +}, +"1.가": { +"이름": "채 집 · 굴착기(타이어형)(0.6㎥)", +"출처": [] +}, +"1.나": { +"이름": "채 집 · 인 력(10%)", +"출처": [] +}, +"1.다": { +"이름": "채 집 · 소 계", +"출처": [] +}, +"2": { +"이름": "상 차 : 굴착기(타이어형)(0.6㎥)", +"출처": [] +}, +"3": { +"이름": "운 반 : 덤프트럭(4.5Ton)", +"출처": [] +}, +"4": { +"이름": "합 계", +"출처": [] +} +}, +"이름조건": {}, +"정답": [ +"GF000211" +], +"허용": [], +"무리": "사람", +"복합": null +}, +{ +"id": "N037", +"파일": "북부청임도", +"이름": "돌망태형 옹벽설치", +"규격": "사각돌망태", +"단위": "㎡", +"갈래": "단가산출", +"출처": [ +[ +"산림품셈", +"13-114" +] +], +"직종근거": [ +"석공", +"특별인부", +"보통인부" +], +"인력": { +"석공": { +"품": 0.072, +"밑품": 0.072, +"율": [], +"어떻게": [ +"식" +] +}, +"특별인부": { +"품": 0.053, +"밑품": 0.053, +"율": [], +"어떻게": [ +"식" +] +}, +"보통인부": { +"품": 0.013, +"밑품": 0.013, +"율": [], +"어떻게": [ +"식" +] +} +}, +"인력절": [ +[ +"2.나", +"석공", +0.072, +0.072 +], +[ +"2.나", +"특별인부", +0.053, +0.053 +], +[ +"2.나", +"보통인부", +0.013, +0.013 +] +], +"장비": [ +{ +"갈래": [ +"굴착기" +], +"규격": [ +0.7 +], +"시간": 0.101, +"밑시간": 0.101, +"율": [], +"절": "2.가", +"글": "굴착기(무한궤도)(0.7㎥)" +} +], +"자재": [ +{ +"이름": "철망태", +"품": null, +"밑품": null +} +], +"율": [], +"계수": {}, +"조건": { +"형": [ +"사각" +] +}, +"옛판": false, +"글": [ +"63.돌망태형 옹벽설치(사각돌망태)[㎡]", +" [산림사업표준품셈 13-114]", +" 1. 재료비 : 사각돌망태(Φ2.7mm 아연도금1.0×1.0×1.0m)", +" 철망태 : - 별도계상 -", +" 2. 설치비", +" 가. 굴착기(무한궤도)(0.7㎥)", +" Q = 0.101", +" 노무비 : 59,020 × 0.101 = 5,961", +" 재료비 : 19,473 × 0.101 = 1,966.7", +" 경 비 : 24,554 × 0.101 = 2,479.9", +" 소 계 : 10,407.6", +" 나. 인 력", +" 석 공 : 268,908 × 0.072인 = 19,361.3", +" 특별인부 : 226,122 × 0.053인 = 11,984.4", +" 보통인부 : 172,068 × 0.013인 = 2,236.8", +" 인부소계 : 33,582.5", +" 다. 소 계", +" 노무비 : 5,961 + 33,582.5 = 39,543.5", +" 재료비 : 1,966.7", +" 경 비 : 2,479.9", +" 계 : 43,990.1", +" 3. 합 계", +" 노무비 : 39,543", +" 재료비 : 1,966", +" 경 비 : 2,479", +" 계 : 43,988" +], +"절": { +"1": { +"이름": "재료비 : 사각돌망태(Φ2.7mm 아연도금1.0×1.0×1.0m)", +"출처": [] +}, +"2": { +"이름": "설치비", +"출처": [] +}, +"2.가": { +"이름": "설치비 · 굴착기(무한궤도)(0.7㎥)", +"출처": [] +}, +"2.나": { +"이름": "설치비 · 인 력", +"출처": [] +}, +"2.다": { +"이름": "설치비 · 소 계", +"출처": [] +}, +"3": { +"이름": "합 계", +"출처": [] +} +}, +"이름조건": { +"형": [ +"사각" +] +}, +"정답": [ +"GF000235" +], +"허용": [ +"GC000342" +], +"무리": "사람", +"복합": null +}, +{ +"id": "N038", +"파일": "북부청임도", +"이름": "콘크리트포장(인력)", +"규격": "T=20cm, 종단15%미만", +"단위": "㎥", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"1-6-2" +] +], +"직종근거": [ +"포장공", +"보통인부" +], +"인력": { +"포장공": { +"품": 0.04, +"밑품": 0.04, +"율": [], +"어떻게": [ +"식" +] +}, +"보통인부": { +"품": 0.02, +"밑품": 0.02, +"율": [], +"어떻게": [ +"식" +] +} +}, +"인력절": [ +[ +"1", +"포장공", +0.04, +0.04 +], +[ +"1", +"보통인부", +0.02, +0.02 +] +], +"장비": [], +"자재": [], +"율": [ +[ +"노무비", +3.0 +] +], +"계수": { +"T": 0.2 +}, +"조건": { +"방법": [ +"인력" +] +}, +"옛판": false, +"글": [ +"65.콘크리트포장(인력)(T=20cm, 종단15%미만)[㎥]", +" [건설공사표준품셈 1-6-2]", +" ※ 콘크리트 포장두께 : T = 0.2m", +" 1일 시공량 : Qd = 100㎥/일", +" 작업면적 : Q = 1 / T = 1 / 0.2 = 5㎡/㎥", +" 1. 배치인원", +" 포 장 공 : 273,308 × 4.0인 ÷ Qd = 273,308 × 4 ÷ 100 = 10,932.3", +" 보통인부 : 172,068 × 2.0인 ÷ Qd = 172,068 × 2 ÷ 100 = 3,441.3", +" 인부소계 : 14,373.6", +" 2. 공구손료(노무비의 3%)", +" 14,373.6 × 3% = 431.2", +" 3. 합 계", +" 노무비 : 14,373", +" 재료비 :", +" 경 비 : 431", +" 계 : 14,804" +], +"절": { +"1": { +"이름": "배치인원", +"출처": [] +}, +"2": { +"이름": "공구손료(노무비의 3%)", +"출처": [] +}, +"3": { +"이름": "합 계", +"출처": [] +} +}, +"이름조건": { +"방법": [ +"인력" +] +}, +"정답": [ +"GC000297" +], +"허용": [ +"GF000162" +], +"무리": "사람", +"복합": null +}, +{ +"id": "N039", +"파일": "북부청임도", +"이름": "비탈면 보호공", +"규격": "평떼", +"단위": "㎡", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"4-1-1" +], +[ +"산림품셈", +"5-23-3" +] +], +"직종근거": [ +"조경공", +"보통인부" +], +"인력": { +"조경공": { +"품": 0.006667, +"밑품": 0.006667, +"율": [], +"어떻게": [ +"식" +] +}, +"보통인부": { +"품": 0.057527, +"밑품": 0.057527, +"율": [], +"어떻게": [ +"식", +"식" +] +} +}, +"인력절": [ +[ +"2", +"조경공", +0.006667, +0.006667 +], +[ +"2", +"보통인부", +0.026667, +0.026667 +], +[ +"3", +"보통인부", +0.03086, +0.03086 +] +], +"장비": [], +"자재": [], +"율": [], +"계수": {}, +"조건": {}, +"옛판": false, +"글": [ +"64.비탈면보호공(평떼)[㎡]", +" 비탈면보호공-평떼 [건설표준품셈(4-1-1)]", +" 1일 시공량 Q = 150 ㎡/일", +" 1. 떼구입비(0.18×0.18×0.01)", +" 잔디(0.18×0.18×0.01) 200 × 30.86매/㎡ × 1.1 = 6,789.2", +" 2. 떼붙임", +" 조 경 공 : 235,204 ×1/150 = 1,568", +" 보통인부 : 172,068 × 4/150 = 4,588.4", +" 인부소계 : 6,156.4", +" 3. 떼꽂이 제작 및 설치[1일 1인 1000개 제작]", +" [산림사업표준품셈 5-23-3]", +" 보통인부 : 172,068 × 1인 × 30.86 / 1000 = 5,310", +" 4. 합 계", +" 노무비 : 6,156.4 + 5,310 = 11,466", +" 재료비 : 6,789", +" 경 비 :", +" 계 : 18,255" +], +"절": { +"1": { +"이름": "떼구입비(0.18×0.18×0.01)", +"출처": [] +}, +"2": { +"이름": "떼붙임", +"출처": [] +}, +"3": { +"이름": "떼꽂이 제작 및 설치[1일 1인 1000개 제작]", +"출처": [] +}, +"4": { +"이름": "합 계", +"출처": [] +} +}, +"이름조건": {}, +"정답": [ +"GC000143" +], +"허용": [ +"GF000032" +], +"무리": "사람", +"복합": null +}, +{ +"id": "N040", +"파일": "북부청임도", +"이름": "콘크리트포장(인력)", +"규격": "T=20cm,종단15%이상", +"단위": "㎥", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"1-6-2" +] +], +"직종근거": [ +"포장공", +"보통인부" +], +"인력": { +"포장공": { +"품": 0.048, +"밑품": 0.048, +"율": [], +"어떻게": [ +"식" +] +}, +"보통인부": { +"품": 0.024, +"밑품": 0.024, +"율": [], +"어떻게": [ +"식" +] +} +}, +"인력절": [ +[ +"1", +"포장공", +0.048, +0.048 +], +[ +"1", +"보통인부", +0.024, +0.024 +] +], +"장비": [], +"자재": [], +"율": [ +[ +"노무비", +3.0 +] +], +"계수": { +"T": 0.2 +}, +"조건": { +"방법": [ +"인력" +] +}, +"옛판": false, +"글": [ +"66.콘크리트포장(인력)(T=20cm,종단15%이상)[㎥]", +" 콘크리트포장(인력) [건설공사표준품셈1-6-2]", +" ※ 콘크리트 포장두께 : T = 0.2m", +" 1일 시공량 : Qd = 100㎥/일", +" 작업면적 : Q = 1 / T = 1 / 0.2 = 5㎡/㎥", +" 1. 배치인원", +" 포 장 공 : 273,308 × 4.0인 ÷ Qd × 1.2 = 273,308 × 4 ÷ 100 × 1.2 = 13,118.7", +" 보통인부 : 172,068 × 2.0인 ÷ Qd × 1.2 = 172,068 × 2 ÷ 100 × 1.2 = 4,129.6", +" 인부소계 : 17,248.3", +" 2. 공구손료(노무비의 3%)", +" 17,248.3 × 3% = 517.4", +" 3. 합 계", +" 노무비 : 17,248", +" 재료비 :", +" 경 비 : 517", +" 계 : 17,765" +], +"절": { +"1": { +"이름": "배치인원", +"출처": [] +}, +"2": { +"이름": "공구손료(노무비의 3%)", +"출처": [] +}, +"3": { +"이름": "합 계", +"출처": [] +} +}, +"이름조건": { +"방법": [ +"인력" +] +}, +"정답": [ +"GC000297" +], +"허용": [ +"GF000162" +], +"무리": "사람", +"복합": null +}, +{ +"id": "N042", +"파일": "북부청임도", +"이름": "큰돌쌓기(찰쌓기)", +"규격": "직경40cm이상~60cm미만", +"단위": "㎡", +"갈래": "단가산출", +"출처": [ +[ +"산림품셈", +"13-6-2" +] +], +"직종근거": [ +"작업반장", +"특별인부", +"보통인부" +], +"인력": { +"작업반장": { +"품": 0.083, +"밑품": 0.083, +"율": [], +"어떻게": [ +"식" +] +}, +"특별인부": { +"품": 0.13, +"밑품": 0.13, +"율": [], +"어떻게": [ +"식" +] +}, +"보통인부": { +"품": 0.13, +"밑품": 0.13, +"율": [], +"어떻게": [ +"식" +] +} +}, +"인력절": [ +[ +"1.가", +"작업반장", +0.083, +0.083 +], +[ +"1.가", +"특별인부", +0.13, +0.13 +], +[ +"1.가", +"보통인부", +0.13, +0.13 +] +], +"장비": [ +{ +"갈래": [ +"굴착기", +"집게" +], +"규격": [ +0.8 +], +"시간": 0.48, +"밑시간": 0.48, +"율": [], +"절": "1.나", +"글": "굴착기(무한궤도)(0.8㎥+부착용집게)" +} +], +"자재": [], +"율": [], +"계수": {}, +"조건": { +"메찰": [ +"찰쌓기" +] +}, +"옛판": false, +"글": [ +"90.큰돌쌓기(찰쌓기)(직경40cm이상~60cm미만)[㎡]", +" 큰돌쌓기(찰쌓기) [산림사업표준품셈 13-6-2]", +" 1. 큰돌쌓기(찰쌓기)", +" 가. 노무비", +" 작업반장 : 215,907 × 0.83 ÷ 10㎡ = 17,920.2", +" 특별인부 : 226,122 × 1.30 ÷ 10㎡ = 29,395.8", +" 보통인부 : 172,068 × 1.30 ÷ 10㎡ = 22,368.8", +" 인부소계 : 69,684.8", +" 나. 굴착기(무한궤도)(0.8㎥+부착용집게)", +" Q = 4.8 ÷ 10㎡ = 0.48", +" 노무비 : 59,020 × 0.48 = 28,329.6", +" 재료비 : 24,421 × 0.48 = 11,722", +" 경 비 : 32,805 × 0.48 = 15,746.4", +" 소 계 : 55,798", +" 라. 제잡비 비율", +" (69,684.8 + 28,329.6) × 3% = 2,940.4", +" 마. 소 계", +" 노무비 : 69,684.8 + 28,329.6 + 2,940.4 = 100,954.8", +" 재료비 : 11,722", +" 경 비 : 15,746.4", +" 계 : 128,423.2", +" 2. 합 계", +" 노무비 : 100,954", +" 재료비 : 11,722", +" 경 비 : 15,746", +" 계 : 128,422" +], +"절": { +"1": { +"이름": "큰돌쌓기(찰쌓기)", +"출처": [] +}, +"1.가": { +"이름": "큰돌쌓기(찰쌓기) · 노무비", +"출처": [] +}, +"1.나": { +"이름": "큰돌쌓기(찰쌓기) · 굴착기(무한궤도)(0.8㎥+부착용집게)", +"출처": [] +}, +"1.라": { +"이름": "큰돌쌓기(찰쌓기) · 제잡비 비율", +"출처": [] +}, +"1.마": { +"이름": "큰돌쌓기(찰쌓기) · 소 계", +"출처": [] +}, +"2": { +"이름": "합 계", +"출처": [] +} +}, +"이름조건": { +"메찰": [ +"찰쌓기" +] +}, +"정답": [ +"GF000227" +], +"허용": [], +"무리": "사람", +"복합": null +}, +{ +"id": "N043", +"파일": "북부청임도", +"이름": "레미콘타설(장비사용)", +"규격": "철근구조물", +"단위": "㎥", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"6-1-1" +] +], +"직종근거": [ +"콘크리트공", +"보통인부" +], +"인력": { +"콘크리트공": { +"품": 0.054545, +"밑품": 0.054545, +"율": [], +"어떻게": [ +"식" +] +}, +"보통인부": { +"품": 0.018182, +"밑품": 0.018182, +"율": [], +"어떻게": [ +"식" +] +} +}, +"인력절": [ +[ +"2", +"콘크리트공", +0.054545, +0.054545 +], +[ +"2", +"보통인부", +0.018182, +0.018182 +] +], +"장비": [ +{ +"갈래": [ +"굴착기" +], +"규격": [ +0.6 +], +"시간": 0.15, +"밑시간": 0.15, +"율": [], +"절": "3", +"글": "굴착기(타이어형)(0.6㎥)" +} +], +"자재": [ +{ +"이름": "레미콘", +"품": null, +"밑품": null +} +], +"율": [ +[ +"노무비", +2.0 +] +], +"계수": {}, +"조건": { +"근": [ +"철근" +], +"방법": [ +"장비" +] +}, +"옛판": false, +"글": [ +"23.레미콘타설(장비사용) (철근구조물) [㎥]", +" 건설표준품셈, 6-1-1 콘크리트 타설", +" 1. 레미콘 : 별도계상", +" 2. 타설비", +" A = 55㎥/일", +" 콘크리트공 : 273,540 × 3인 ÷ 55 = 14,920.3", +" 보통인부 : 172,068 × 1인 ÷ 55 = 3,129", +" 인부소계 : 18,049.3", +" 공구손료 및 경장비(콘크리트 진동기등)의 기계경비(노무비의 2%)", +" 18,049.3 × 2% = 360.9", +" 3. 굴착기(타이어형)(0.6㎥)", +" Q = 8hr ÷ A = 8 ÷ 55 = 0.15hr/㎥", +" 노무비 : 59,020 × 0.15 = 8,853", +" 재료비 : 19,792 × 0.15 = 2,968.8", +" 경 비 : 27,120 × 0.15 = 4,068", +" 소 계 : 15,889.8", +" 4. 합 계", +" 노무비 : 18,049.3 + 8,853 = 26,902", +" 재료비 : 360.9 + 2,968.8 = 3,329", +" 경 비 : 4,068", +" 계 : 34,299" +], +"절": { +"1": { +"이름": "레미콘 : 별도계상", +"출처": [] +}, +"2": { +"이름": "타설비", +"출처": [] +}, +"3": { +"이름": "굴착기(타이어형)(0.6㎥)", +"출처": [] +}, +"4": { +"이름": "합 계", +"출처": [] +} +}, +"이름조건": { +"근": [ +"철근" +], +"방법": [ +"장비" +] +}, +"정답": [ +"GC000198" +], +"허용": [ +"GF000155" +], +"무리": "사람", +"복합": null +}, +{ +"id": "N044", +"파일": "북부청임도", +"이름": "강관동바리", +"규격": "암거용-1개월사용", +"단위": "공/㎥", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"2-2-4" +], +[ +"건설품셈", +"2-6-1" +] +], +"직종근거": [ +"형틀목공", +"보통인부" +], +"인력": { +"형틀목공": { +"품": 0.06, +"밑품": 0.06, +"율": [], +"어떻게": [ +"식" +] +}, +"보통인부": { +"품": 0.02, +"밑품": 0.02, +"율": [], +"어떻게": [ +"식" +] +} +}, +"인력절": [ +[ +"2", +"형틀목공", +0.06, +0.06 +], +[ +"2", +"보통인부", +0.02, +0.02 +] +], +"장비": [], +"자재": [ +{ +"이름": "강관써포트(V42600-4000mm14.3kg)", +"품": 0.0152, +"밑품": 0.38 +} +], +"율": [ +[ +"재료비", +4.0 +], +[ +"인건비", +100.0 +], +[ +"재료비", +5.0 +] +], +"계수": {}, +"조건": { +"개월": [ +"1개월" +] +}, +"옛판": false, +"글": [ +"73.강관동바리(암거용-1개월사용)[공/㎥]", +" 강간동바리 [건설공사표준품셈 2-2-4 구조물동바리 손율]", +" [건설공사표준품셈 2-6-1 강관동바리 설치및 해체]", +" ※ 암거구조물(1개월사용 : 재료비의 4%, 인건비의 100%)", +" 1. 재료비", +" 강관써포트(V42600-4000mm14.3kg) 24,500 × 0.38 × 4% = 372.4", +" 잡재료비(재료비의 5%)", +" 372.4 × 5% = 18.6", +" 2. 노무비", +" 형틀목공 : 275,790 × 3인 ÷ 50공㎥ = 16,547.4", +" 보통인부 : 172,068 × 1인 ÷ 50공㎥ = 3,441.3", +" 인부소계 : 19,988.7", +" 3. 합 계", +" 노무비 : 19,988", +" 재료비 : 372.4 + 18.6 = 391", +" 경 비 :", +" 계 : 20,379" +], +"절": { +"1": { +"이름": "재료비", +"출처": [] +}, +"372": { +"이름": "4 × 5% = 18.6", +"출처": [] +}, +"2": { +"이름": "노무비", +"출처": [] +}, +"3": { +"이름": "합 계", +"출처": [] +} +}, +"이름조건": { +"개월": [ +"1개월" +] +}, +"정답": [ +"GC000015" +], +"허용": [ +"GF000189" +], +"무리": "사람", +"복합": null +}, +{ +"id": "N045", +"파일": "북부청임도", +"이름": "철근가공조립(현장)", +"규격": "Type-ll", +"단위": "Ton", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"6-2" +] +], +"직종근거": [ +"철근공", +"보통인부" +], +"인력": { +"철근공": { +"품": 2.67, +"밑품": 2.67, +"율": [], +"어떻게": [ +"식", +"식" +] +}, +"보통인부": { +"품": 1.02, +"밑품": 1.02, +"율": [], +"어떻게": [ +"식", +"식" +] +} +}, +"인력절": [ +[ +"1.나", +"철근공", +0.75, +0.75 +], +[ +"1.나", +"보통인부", +0.25, +0.25 +], +[ +"2.나", +"철근공", +1.92, +1.92 +], +[ +"2.나", +"보통인부", +0.77, +0.77 +] +], +"장비": [], +"자재": [ +{ +"이름": "철근", +"품": null, +"밑품": null +}, +{ +"이름": "어닐링철선(결속선)(#200.9mm200.4m/kg)", +"품": 8.0, +"밑품": 8.0 +} +], +"율": [ +[ +"인력품", +9.0 +], +[ +"가공노력품", +2.0 +] +], +"계수": {}, +"조건": { +"근": [ +"철근" +], +"type": [ +"Ⅱ" +] +}, +"옛판": false, +"글": [ +"72.철근가공조립(현장)(Type-ll)[Ton]", +" 철근 현장가공 및 조립(Type-Ⅱ) [건설표준품셈6-2]", +" 1. 현장가공", +" 가. 재료비", +" 철근 : 별도계산", +" 나. 인건비", +" 철 근 공 : 268,187 × 0.75인 = 201,140.2", +" 보통인부 : 172,068 × 0.25인 = 43,017", +" 인부소계 : 244,157.2", +" 기계경비(인력품의 9%)", +" 244,157.2 × 9% = 21,974.1", +" 다. 소 계", +" 노무비 : 244,157.2", +" 재료비 : 21,974.1", +" 경 비 :", +" 계 : 266,131.3", +" 2. 조 립", +" 가. 재료비", +" 어닐링철선(결속선)(#200.9mm200.4m/kg) 1,760 × 8kg = 14,080", +" 나. 인건비", +" 철 근 공 : 268,187 × 1.92인 = 514,919", +" 보통인부 : 172,068 × 0.77인 = 132,492.3", +" 인부소계 : 647,411.3", +" 기구손료 (가공노력품의 2%)", +" 647,411.3 × 0.02 = 12,948.2", +" 다. 소 계", +" 노무비 : 647,411.3", +" 재료비 : 14,080 + 12,948.2 = 27,028.2", +" 경 비 :", +" 계 : 674,439.5", +" 3. 합 계", +" 노무비 : 244,157.2 + 647,411.3 = 891,568", +" 재료비 : 21,974.1 + 27,028.2 = 49,002", +" 경 비 :", +" 계 : 940,570" +], +"절": { +"1": { +"이름": "현장가공", +"출처": [] +}, +"1.가": { +"이름": "현장가공 · 재료비", +"출처": [] +}, +"1.나": { +"이름": "현장가공 · 인건비", +"출처": [] +}, +"1.다": { +"이름": "현장가공 · 소 계", +"출처": [] +}, +"2": { +"이름": "조 립", +"출처": [] +}, +"2.가": { +"이름": "조 립 · 재료비", +"출처": [] +}, +"2.나": { +"이름": "조 립 · 인건비", +"출처": [] +}, +"2.다": { +"이름": "조 립 · 소 계", +"출처": [] +}, +"3": { +"이름": "합 계", +"출처": [] +} +}, +"이름조건": { +"근": [ +"철근" +] +}, +"정답": [ +"GC000206", +"GC000207" +], +"허용": [ +"GF000159" +], +"무리": "사람", +"복합": [ +"GC000206", +"GC000207" +] +}, +{ +"id": "N046", +"파일": "북부청임도", +"이름": "강관비계", +"규격": "10m이하,3개월사용", +"단위": "㎡", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"2-7-1" +], +[ +"건설품셈", +"2-2-5" +] +], +"직종근거": [ +"비계공", +"보통인부" +], +"인력": { +"비계공": { +"품": 0.054545, +"밑품": 0.054545, +"율": [], +"어떻게": [ +"식" +] +}, +"보통인부": { +"품": 0.018182, +"밑품": 0.018182, +"율": [], +"어떻게": [ +"식" +] +} +}, +"인력절": [ +[ +"2", +"비계공", +0.054545, +0.054545 +], +[ +"2", +"보통인부", +0.018182, +0.018182 +] +], +"장비": [], +"자재": [ +{ +"이름": "강관비계 (4000×48.6mm×2.3mm)", +"품": 0.27930000000000005, +"밑품": 3.99 +}, +{ +"이름": "이음철물", +"품": 0.06, +"밑품": 0.5 +}, +{ +"이름": "조임철물 (직교자재)", +"품": 0.2496, +"밑품": 2.08 +}, +{ +"이름": "이음철물", +"품": 0.04, +"밑품": 0.04 +}, +{ +"이름": "받침철물", +"품": 0.0036, +"밑품": 0.04 +} +], +"율": [ +[ +"노무비", +2.0 +] +], +"계수": {}, +"조건": { +"개월": [ +"3개월" +] +}, +"옛판": false, +"글": [ +"74.강관비계(10m이하,3개월사용)[㎡]", +" 강관비계10m이하 3개월사용 [건설공사표준품셈 2-7-1 강관비계설치및해체]", +" [건설공사표준품셈 2-2-5 구조물 비계 손율]", +" 1. 재료비 (3개월손료 적용)", +" 강관비계 (4000×48.6mm×2.3mm) 3,950 × 3.99m × 7% = 1,103.2", +" 이음철물 2,200 × 0.5EA × 12% = 132", +" 조임철물 (직교자재) 2,700 × 2.08EA × 12% = 673.9", +" 이음철물 2,200 × 0.04EA = 88", +" 받침철물 4,000 × 0.04EA × 9% = 14.4", +" 자재소계 : 2,011.5", +" 2. 인건비", +" 비 계 공 : 281,939 × 3인 ÷ 55㎡ = 15,378.4", +" 보통인부 : 172,068 × 1인 ÷ 55㎡ = 3,128.5", +" 인부소계 : 18,506.9", +" 3. 공구손료(노무비의 2%)", +" 18,506.9 × 2% = 370.1", +" 4. 합 계", +" 노무비 : 18,506", +" 재료비 : 2,011.5 + 370.1 = 2,381", +" 경 비 :", +" 계 : 20,887" +], +"절": { +"1": { +"이름": "재료비 (3개월손료 적용)", +"출처": [] +}, +"2": { +"이름": "인건비", +"출처": [] +}, +"3": { +"이름": "공구손료(노무비의 2%)", +"출처": [] +}, +"4": { +"이름": "합 계", +"출처": [] +} +}, +"이름조건": { +"개월": [ +"3개월" +] +}, +"정답": [ +"GC000022" +], +"허용": [ +"GF000188" +], +"무리": "사람", +"복합": null +}, +{ +"id": "N047", +"파일": "북부청임도", +"이름": "흙깍기(토사)", +"규격": "굴삭기1.0㎥,보통토사", +"단위": "㎥", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"3-2-3" +] +], +"직종근거": [], +"인력": {}, +"인력절": [], +"장비": [ +{ +"갈래": [ +"굴착기" +], +"규격": [ +1.0 +], +"시간": 0.025806451612903226, +"밑시간": 0.025806451612903226, +"율": [], +"절": "1", +"글": "굴착기(무한궤도)(1.0㎥)" +} +], +"자재": [], +"율": [], +"계수": {}, +"조건": { +"토질": [ +"보통토사", +"토사" +], +"난이": [ +"보통" +], +"규모": [ +"소규모" +] +}, +"옛판": false, +"글": [ +"49.흙깍기(토사)(굴삭기1.0㎥,보통토사)[㎥]", +" 흙깎기-보통토사 [건설공사 표준품셈3-2-3]", +" 시공량은 소규모 적용(10,000㎥이상이나 불도우저 적용 불가능하고 작업공간이 협소)", +" 1. 굴착기(무한궤도)(1.0㎥)", +" Q = 310㎥ ÷ 8 = 38.75㎥/hr", +" 노무비 : 59,020 ÷ 38.75 = 1,523", +" 재료비 : 32,735 ÷ 38.75 = 844.7", +" 경 비 : 29,621 ÷ 38.75 = 764.4", +" 소 계 : 3,132.1", +" 2. 합 계", +" 노무비 : 1,523", +" 재료비 : 844", +" 경 비 : 764", +" 계 : 3,131" +], +"절": { +"1": { +"이름": "굴착기(무한궤도)(1.0㎥)", +"출처": [] +}, +"2": { +"이름": "합 계", +"출처": [] +} +}, +"이름조건": { +"토질": [ +"보통토사", +"토사" +], +"난이": [ +"보통" +] +}, +"정답": [ +"GC000085" +], +"허용": [ +"GF000089" +], +"무리": "사람", +"복합": null +}, +{ +"id": "N049", +"파일": "북부청임도", +"이름": "잡철물 제작설치", +"규격": "규격철물 설치(일반철재)", +"단위": "Ton", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"8-3-1" +] +], +"직종근거": [ +"철공", +"용접공", +"특별인부", +"보통인부" +], +"인력": { +"철공": { +"품": 7.05, +"밑품": 7.05, +"율": [], +"어떻게": [ +"식" +] +}, +"용접공": { +"품": 2.57, +"밑품": 2.57, +"율": [], +"어떻게": [ +"식" +] +}, +"특별인부": { +"품": 1.92, +"밑품": 1.92, +"율": [], +"어떻게": [ +"식" +] +}, +"보통인부": { +"품": 1.28, +"밑품": 1.28, +"율": [], +"어떻게": [ +"식" +] +} +}, +"인력절": [ +[ +"1", +"철공", +7.05, +7.05 +], +[ +"1", +"용접공", +2.57, +2.57 +], +[ +"1", +"특별인부", +1.92, +1.92 +], +[ +"1", +"보통인부", +1.28, +1.28 +] +], +"장비": [], +"자재": [], +"율": [ +[ +"노무비", +5.0 +], +[ +"노무비", +3.0 +] +], +"계수": {}, +"조건": {}, +"옛판": false, +"글": [ +"82.잡철물 제작설치(규격철물 설치(일반철재))[Ton]", +" < 건설공사표준품셈(건축) 8-3-1 >", +" 1. 노무비", +" 철 공 : 239,808 × 7.05인 = 1,690,646.4", +" 용 접 공 : 282,536 × 2.57인 = 726,117.5", +" 특별인부 : 226,122 × 1.92인 = 434,154.2", +" 보통인부 : 172,068 × 1.28인 = 220,247", +" 인부소계 : 3,071,165.1", +" 2. 공구손료 및 경장비의 기계경비(노무비의 5%)", +" 3,071,165.1 × 5% = 153,558.2", +" 3. 잡재료비(노무비의 3%)", +" 3,071,165.1 × 3% = 92,134.9", +" 4. 합 계", +" 노무비 : 3,071,165", +" 재료비 : 153,558.2 + 92,134.9 = 245,693", +" 경 비 :", +" 계 : 3,316,858" +], +"절": { +"1": { +"이름": "노무비", +"출처": [] +}, +"2": { +"이름": "공구손료 및 경장비의 기계경비(노무비의 5%)", +"출처": [] +}, +"3": { +"이름": "잡재료비(노무비의 3%)", +"출처": [] +}, +"4": { +"이름": "합 계", +"출처": [] +} +}, +"이름조건": {}, +"정답": [ +"GC000676" +], +"허용": [], +"무리": "사람", +"복합": null +}, +{ +"id": "N050", +"파일": "북부청임도", +"이름": "도장(철재면2회)", +"규격": "방청1,유성2", +"단위": "㎡", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"11-1-3" +], +[ +"건설품셈", +"11-2-6" +], +[ +"건설품셈", +"11-2-4" +] +], +"직종근거": [ +"도장공", +"보통인부" +], +"인력": { +"도장공": { +"품": 0.061, +"밑품": 0.041, +"율": [ +[ +1.0, +"회" +], +[ +2.0, +"회" +] +], +"어떻게": [ +"식", +"식", +"식" +] +}, +"보통인부": { +"품": 0.012, +"밑품": 0.008, +"율": [ +[ +1.0, +"회" +], +[ +2.0, +"회" +] +], +"어떻게": [ +"식", +"식", +"식" +] +} +}, +"인력절": [ +[ +"1", +"도장공", +0.006, +0.006 +], +[ +"1", +"보통인부", +0.001, +0.001 +], +[ +"3", +"도장공", +0.015, +0.015 +], +[ +"3", +"보통인부", +0.003, +0.003 +], +[ +"6", +"도장공", +0.04, +0.02 +], +[ +"6", +"보통인부", +0.008, +0.004 +] +], +"장비": [], +"자재": [ +{ +"이름": "광명단조합페인트(1종2류(철재)", +"품": 0.322, +"밑품": 0.161 +}, +{ +"이름": "조합페인트(KSM60202급,백색)", +"품": 0.332, +"밑품": 0.166 +} +], +"율": [ +[ +"인력품", +3.0 +], +[ +"인력품", +2.0 +], +[ +"인력품", +2.0 +] +], +"계수": {}, +"조건": { +"난이": [ +"보통" +], +"횟수": [ +"2회" +] +}, +"옛판": false, +"글": [ +"95.도장(철재면2회)(방청1,유성2)[㎡]", +" 1. 철재면 바탕만들기 [건설공사표준품셈(건축) 11-1-3]", +" 도 장 공 : 263,017 × 0.006인 = 1,578.1", +" 보통인부 : 172,068 × 0.001인 = 172", +" 인부소계 : 1,750.1", +" 2. 공구손료(인력품의 3%)", +" 1,750.1 × 3% = 52.5", +" 3. 녹막이 페인트 칠 [건설공사표준품셈(건축) 11-2-6]", +" 도 장 공 : 263,017 × 0.015인 × 1회 = 3,945.2", +" 보통인부 : 172,068 × 0.003인 × 1회 = 516.2", +" 인부소계 : 4,461.4", +" 4. 공구손료(인력품의 2%)", +" 4,461.4 × 2% = 89.2", +" 5. 재료비", +" 광명단조합페인트(1종2류(철재) 8,400 × 0.161 × 2회 = 2,704.8", +" 신나(KSM60602종(조합페인트류) 3,477 × 0.008ℓ × 2회 = 55.6", +" 자재소계 : 2,760.4", +" 6. 유성페인트 붓칠 [건설공사표준품셈(건축) 11-2-4]", +" 도 장 공 : 263,017 × 0.02인 × 2회 = 10,520.6", +" 보통인부 : 172,068 × 0.004인× 2회 = 1,376.5", +" 인부소계 :11,897.1", +" 7. 공구손료(인력품의 2%)", +" 11,897.1 × 2% = 237.9", +" 8. 재료비", +" 조합페인트(KSM60202급,백색) 5,361 × 0.166인 × 2회 = 1,779.8", +" 신나(KSM60602종(조합페인트류) 3,477 × 0.008ℓ × 2회 = 55.6", +" 소계 : 1,835.4", +" 9. 합 계", +" 노무비 : 1,750.1 + 4,461.4 + 11,897.1 = 18,108", +" 재료비 : 52.5 + 89.2 + 2,760.4 + 237.9 + 1,835.4 = 4,975", +" 경 비 :", +" 계 : 23,083" +], +"절": { +"1": { +"이름": "철재면 바탕만들기 [건설공사표준품셈(건축) 11-1-3]", +"출처": [ +[ +"건설품셈", +"11-1-3" +] +] +}, +"2": { +"이름": "공구손료(인력품의 3%)", +"출처": [] +}, +"3": { +"이름": "녹막이 페인트 칠 [건설공사표준품셈(건축) 11-2-6]", +"출처": [ +[ +"건설품셈", +"11-2-6" +] +] +}, +"4": { +"이름": "공구손료(인력품의 2%)", +"출처": [] +}, +"5": { +"이름": "재료비", +"출처": [] +}, +"6": { +"이름": "유성페인트 붓칠 [건설공사표준품셈(건축) 11-2-4]", +"출처": [ +[ +"건설품셈", +"11-2-4" +] +] +}, +"7": { +"이름": "공구손료(인력품의 2%)", +"출처": [] +}, +"8": { +"이름": "재료비", +"출처": [] +}, +"9": { +"이름": "합 계", +"출처": [] +} +}, +"이름조건": { +"횟수": [ +"2회" +] +}, +"정답": [ +"GC000705", +"GC000714", +"GC000712" +], +"허용": [], +"무리": "사람", +"복합": [ +"GC000705", +"GC000714", +"GC000712" +] +}, +{ +"id": "N051", +"파일": "북부청임도", +"이름": "임도안전표지판설치", +"규격": "삼각,900mm", +"단위": "개소", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"1-8-1" +] +], +"직종근거": [ +"특별인부", +"보통인부" +], +"인력": { +"특별인부": { +"품": 0.257576, +"밑품": 0.257576, +"율": [], +"어떻게": [ +"식", +"식" +] +}, +"보통인부": { +"품": 0.128788, +"밑품": 0.128788, +"율": [], +"어떻게": [ +"식", +"식" +] +} +}, +"인력절": [ +[ +"2", +"특별인부", +0.166667, +0.166667 +], +[ +"2", +"특별인부", +0.090909, +0.090909 +], +[ +"2", +"보통인부", +0.083333, +0.083333 +], +[ +"2", +"보통인부", +0.045455, +0.045455 +] +], +"장비": [ +{ +"갈래": [ +"크레인" +], +"규격": [ +5.0 +], +"시간": 0.67, +"밑시간": 0.67, +"율": [], +"절": "3", +"글": "장비 : 트럭탑재형크레인(5Ton)" +} +], +"자재": [ +{ +"이름": "삼각표지판(삼각,900mm)", +"품": 1.0, +"밑품": 1.0 +} +], +"율": [], +"계수": {}, +"조건": {}, +"옛판": false, +"글": [ +"81.임도안전표지판설치(삼각,900mm)[개소]", +" [건설공사표준품셈 1-8-1]", +" 1. 재 료 비", +" 삼각표지판(삼각,900mm) 84,000 × 1EA = 84,000", +" 표지판지주(D=76.3mm×3.0m) 75,000 × 1식 = 75,000", +" 자재소계 :159,000", +" 2. 인 건 비", +" 특별인부 : 226,122 × 2인 ÷ 12본/일 = 37,687 (지주)", +" 특별인부 : 226,122 × 2인 ÷ 22본/일 = 20,556.5 (표지판)", +" 보통인부 : 172,068 × 1인 ÷ 12본/일 = 14,339 (지주)", +" 보통인부 : 172,068 × 1인 ÷ 22본/일 = 7,821.2 (표지판)", +" 인부소계 : 80,403.7", +" 3. 장비 : 트럭탑재형크레인(5Ton)", +" Q = 8hr / 12개소 = 0.67", +" 노무비 : 49,778 × 0.67 = 33,351.2", +" 재료비 : 8,421 × 0.67 = 5,642", +" 경 비 : 11,117 × 0.67 = 7,448.3", +" 소 계 : 46,441.5", +" 4. 공구손료 및 경장비", +" (80,403.7 + 33,351.2) × 2% = 2,275", +" 5. 합 계", +" 노무비 : 80,403.7 + 33,351.2 = 113,754", +" 재료비 : 159,000 + 5,642 = 164,642", +" 경 비 : 7,448.3 + 2,275 = 9,723", +" 계 : 288,119" +], +"절": { +"1": { +"이름": "재 료 비", +"출처": [] +}, +"2": { +"이름": "인 건 비", +"출처": [] +}, +"3": { +"이름": "장비 : 트럭탑재형크레인(5Ton)", +"출처": [] +}, +"4": { +"이름": "공구손료 및 경장비", +"출처": [] +}, +"5": { +"이름": "합 계", +"출처": [] +} +}, +"이름조건": {}, +"정답": [ +"GC000308" +], +"허용": [], +"무리": "사람", +"복합": null +}, +{ +"id": "S052", +"파일": "북부청사방", +"이름": "레미콘타설(장비사용)", +"규격": "철근구조물", +"단위": "㎥", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"6-1-1" +] +], +"직종근거": [ +"콘크리트공", +"보통인부" +], +"인력": { +"콘크리트공": { +"품": 0.054545, +"밑품": 0.054545, +"율": [], +"어떻게": [ +"식" +] +}, +"보통인부": { +"품": 0.018182, +"밑품": 0.018182, +"율": [], +"어떻게": [ +"식" +] +} +}, +"인력절": [ +[ +"2", +"콘크리트공", +0.054545, +0.054545 +], +[ +"2", +"보통인부", +0.018182, +0.018182 +] +], +"장비": [ +{ +"갈래": [ +"굴착기" +], +"규격": [ +0.6 +], +"시간": 0.15, +"밑시간": 0.15, +"율": [], +"절": "4", +"글": "굴착기(타이어형)(0.6㎥)[hr]" +} +], +"자재": [ +{ +"이름": "레미콘", +"품": null, +"밑품": null +} +], +"율": [ +[ +"노무비", +2.0 +] +], +"계수": {}, +"조건": { +"근": [ +"철근" +], +"규모": [ +"소량" +], +"방법": [ +"장비" +] +}, +"옛판": false, +"글": [ +"22.레미콘타설(장비사용)(철근구조물)[㎥]", +" 2024년 품셈개정, 6-1-1콘크리트 타설", +" ○. 레미콘타설(장비사용타설-철근구조물(소량타설)", +" 1. 레미콘 : 별도계상", +" 2. 타설비", +" 콘크리트공 : 273,540 × 3 ÷ 55인 = 14,920.3", +" 보통인부 : 172,068 × 1 ÷ 55인 = 3,128.5", +" 인부소계 : 18,048.8", +" 3. 공구손료 및 경장비(콘크리트 진동기등)의 기계경비(노무비의 2%)", +" 기계손료 : 18,048.8 × 2% = 360.9", +" 4. 굴착기(타이어형)(0.6㎥)[hr]", +" Q = 8 / 55 = 0.15hr/㎥", +" 노무비 : 59,020 × 0.15 = 8,853", +" 재료비 : 19,792 × 0.15 = 2,968.8", +" 경 비 : 27,120 × 0.15 = 4,068", +" 소 계 : 15,889.8", +" 5. 합 계", +" 노무비 : 18,048.8 + 8,853 = 26,901", +" 재료비 : 360.9 + 2,968.8 = 3,329", +" 경 비 : 4,068", +" 계 : 34,298" +], +"절": { +"1": { +"이름": "레미콘 : 별도계상", +"출처": [] +}, +"2": { +"이름": "타설비", +"출처": [] +}, +"3": { +"이름": "공구손료 및 경장비(콘크리트 진동기등)의 기계경비(노무비의 2%)", +"출처": [] +}, +"4": { +"이름": "굴착기(타이어형)(0.6㎥)[hr]", +"출처": [] +}, +"5": { +"이름": "합 계", +"출처": [] +} +}, +"이름조건": { +"근": [ +"철근" +], +"방법": [ +"장비" +] +}, +"정답": [ +"GC000198" +], +"허용": [ +"GF000155" +], +"무리": "사람", +"복합": null +}, +{ +"id": "S053", +"파일": "북부청사방", +"이름": "레미콘타설(장비사용)", +"규격": "무근구조물", +"단위": "㎥", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"6-1-1" +] +], +"직종근거": [ +"콘크리트공", +"보통인부" +], +"인력": { +"콘크리트공": { +"품": 0.047619, +"밑품": 0.047619, +"율": [], +"어떻게": [ +"식" +] +}, +"보통인부": { +"품": 0.015873, +"밑품": 0.015873, +"율": [], +"어떻게": [ +"식" +] +} +}, +"인력절": [ +[ +"2", +"콘크리트공", +0.047619, +0.047619 +], +[ +"2", +"보통인부", +0.015873, +0.015873 +] +], +"장비": [ +{ +"갈래": [ +"굴착기" +], +"규격": [ +0.6 +], +"시간": 0.13, +"밑시간": 0.13, +"율": [], +"절": "4", +"글": "굴착기(타이어형)(0.6㎥)[hr]" +} +], +"자재": [ +{ +"이름": "레미콘", +"품": null, +"밑품": null +} +], +"율": [ +[ +"노무비", +2.0 +] +], +"계수": {}, +"조건": { +"근": [ +"무근", +"철근" +], +"방법": [ +"장비" +] +}, +"옛판": false, +"글": [ +"20.레미콘타설(장비사용)(무근구조물)[㎥]", +" 2024년 품셈개정, 6-1-1콘크리트 타설", +" ○. 레미콘타설(장비사용타설-철근구조물)", +" 1. 레미콘 : 별도계상", +" 2. 타설비", +" 콘크리트공 : 273,540 × 3 ÷ 63 = 13,025.7", +" 보통인부 : 172,068 × 1 ÷ 63 = 2,731.2", +" 인부소계 : 15,756.9", +" 3. 공구손료 및 경장비(콘크리트 진동기등)의 기계경비(노무비의 2%)", +" 15,756.9 × 2% = 315.1", +" 4. 굴착기(타이어형)(0.6㎥)[hr]", +" Q = 1 × 8hr / 63 = 0.13hr/㎥", +" 노무비 : 59,020 × 0.13 = 7,672.6", +" 재료비 : 19,792 × 0.13 = 2,572.9", +" 경 비 : 27,120 × 0.13 = 3,525.6", +" 소 계 : 13,771.1", +" 5. 합 계", +" 노무비 : 15,756.9 + 7,672.6 = 23,429", +" 재료비 : 315.1 + 2,572.9 = 2,888", +" 경 비 : 3,525", +" 계 : 29,842" +], +"절": { +"1": { +"이름": "레미콘 : 별도계상", +"출처": [] +}, +"2": { +"이름": "타설비", +"출처": [] +}, +"3": { +"이름": "공구손료 및 경장비(콘크리트 진동기등)의 기계경비(노무비의 2%)", +"출처": [] +}, +"4": { +"이름": "굴착기(타이어형)(0.6㎥)[hr]", +"출처": [] +}, +"5": { +"이름": "합 계", +"출처": [] +} +}, +"이름조건": { +"근": [ +"무근" +], +"방법": [ +"장비" +] +}, +"정답": [ +"GC000198" +], +"허용": [ +"GF000155" +], +"무리": "사람", +"복합": null +}, +{ +"id": "S054", +"파일": "북부청사방", +"이름": "유로폼(Euro Form)", +"규격": "6회, 보통", +"단위": "㎡", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"6-3-3" +] +], +"직종근거": [ +"형틀목공", +"보통인부" +], +"인력": { +"형틀목공": { +"품": 0.114286, +"밑품": 0.114286, +"율": [], +"어떻게": [ +"식" +] +}, +"보통인부": { +"품": 0.028571, +"밑품": 0.028571, +"율": [], +"어떻게": [ +"식" +] +} +}, +"인력절": [ +[ +"3", +"형틀목공", +0.114286, +0.114286 +], +[ +"3", +"보통인부", +0.028571, +0.028571 +] +], +"장비": [], +"자재": [ +{ +"이름": "유로폼(패널)(600×1200)", +"품": 0.095, +"밑품": 0.095 +} +], +"율": [ +[ +"주자재비", +55.0 +], +[ +"노무비", +3.0 +] +], +"계수": {}, +"조건": { +"난이": [ +"보통" +], +"횟수": [ +"6회" +] +}, +"옛판": false, +"글": [ +"38.유로폼(Euro Form)(6회, 보통)[㎡]", +" 2024년 품셈개정, 6-3-3 유 로 폼", +" 1. 재료비", +" 유로폼(패널)(600×1200) 28,500 × 0.095매 = 2,707.5", +" 2. 잡재료비(주자재비의 55%)", +" 소모자재 : 2,707.5 × 55% = 1,489.1", +" 3. 설치비", +" 형틀목공 : 275,790 × 4 ÷ 35인 = 31,518.8", +" 보통인부 : 172,068 × 1 ÷ 35인 = 4,916.2", +" 인부소계 : 36,435", +" 4. 공구손료 및 경장비 기계경비(노무비의 3%)", +" 36,435 × 3% = 1,093", +" 5. 합 계", +" 노무비 : 36,435", +" 재료비 : 2,707.5 + 1,489.1 + 1,093 = 5,289", +" 경 비 :", +" 계 : 41,724" +], +"절": { +"1": { +"이름": "재료비", +"출처": [] +}, +"2": { +"이름": "잡재료비(주자재비의 55%)", +"출처": [] +}, +"3": { +"이름": "설치비", +"출처": [] +}, +"4": { +"이름": "공구손료 및 경장비 기계경비(노무비의 3%)", +"출처": [] +}, +"5": { +"이름": "합 계", +"출처": [] +} +}, +"이름조건": { +"난이": [ +"보통" +], +"횟수": [ +"6회" +] +}, +"정답": [ +"GC000214" +], +"허용": [ +"GF000210" +], +"무리": "사람", +"복합": null +}, +{ +"id": "S055", +"파일": "북부청사방", +"이름": "철근가공조립(현장)", +"규격": "Type-Ⅱ", +"단위": "Ton", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"6-2" +] +], +"직종근거": [ +"철근공", +"보통인부" +], +"인력": { +"철근공": { +"품": 2.67, +"밑품": 2.67, +"율": [], +"어떻게": [ +"식", +"식" +] +}, +"보통인부": { +"품": 1.02, +"밑품": 1.02, +"율": [], +"어떻게": [ +"식", +"식" +] +} +}, +"인력절": [ +[ +"1.가", +"철근공", +0.75, +0.75 +], +[ +"1.가", +"보통인부", +0.25, +0.25 +], +[ +"2.나", +"철근공", +1.92, +1.92 +], +[ +"2.나", +"보통인부", +0.77, +0.77 +] +], +"장비": [], +"자재": [ +{ +"이름": "어닐링철선(결속선)(#200.9mm200.4m/kg)", +"품": 8.0, +"밑품": 8.0 +} +], +"율": [ +[ +"인력품", +9.0 +], +[ +"인력품", +2.0 +] +], +"계수": {}, +"조건": { +"근": [ +"철근" +], +"type": [ +"Ⅱ" +] +}, +"옛판": false, +"글": [ +"53.철근가공조립(현장)(Type-Ⅱ)[Ton]", +" 2024년 철근 현장가공 및 조립(Type-Ⅱ) [건설표준품셈6-2]", +" 1. 현장가공", +" 가. 철 근 : 1Ton - 별 산 -", +" 철 근 공 : 268,187 × 0.75인 = 201,140.2", +" 보통인부 : 172,068 × 0.25인 = 43,017", +" 인부소계 : 244,157.2", +" 나. 기계경비(인력품의 9%)", +" 244,157.2 × 9% = 21,974.1", +" 다. 소 계", +" 노무비 : 244,157.2", +" 재료비 : 21,974.1", +" 경 비 :", +" 계 : 266,131.3", +" 2. 현장조립", +" 가. 재료비", +" 어닐링철선(결속선)(#200.9mm200.4m/kg) 1,760 × 8kg = 14,080", +" 나. 인 력", +" 철 근 공 : 268,187 × 1.92인 = 514,919", +" 보통인부 : 172,068 × 0.77인 = 132,492.3", +" 인부소계 : 647,411.3", +" 다. 기구손료(인력품의 2%)", +" 647,411.3 × 2% = 12,948.2", +" 라. 소 계", +" 노무비 : 647,411.3", +" 재료비 : 14,080 + 12,948.2 = 27,028.2", +" 경 비 :", +" 계 : 674,439.5", +" 3. 합 계", +" 노무비 : 244,157.2 + 647,411.3 = 891,568", +" 재료비 : 21,974.1 + 27,028.2 = 49,002", +" 경 비 :", +" 계 : 940,570" +], +"절": { +"1": { +"이름": "현장가공", +"출처": [] +}, +"1.가": { +"이름": "현장가공 · 철 근 : 1Ton - 별 산 -", +"출처": [] +}, +"1.나": { +"이름": "현장가공 · 기계경비(인력품의 9%)", +"출처": [] +}, +"1.다": { +"이름": "현장가공 · 소 계", +"출처": [] +}, +"2": { +"이름": "현장조립", +"출처": [] +}, +"2.가": { +"이름": "현장조립 · 재료비", +"출처": [] +}, +"2.나": { +"이름": "현장조립 · 인 력", +"출처": [] +}, +"2.다": { +"이름": "현장조립 · 기구손료(인력품의 2%)", +"출처": [] +}, +"2.라": { +"이름": "현장조립 · 소 계", +"출처": [] +}, +"3": { +"이름": "합 계", +"출처": [] +} +}, +"이름조건": { +"근": [ +"철근" +], +"type": [ +"Ⅱ" +] +}, +"정답": [ +"GC000206", +"GC000207" +], +"허용": [ +"GF000159" +], +"무리": "사람", +"복합": [ +"GC000206", +"GC000207" +] +}, +{ +"id": "S056", +"파일": "북부청사방", +"이름": "강관비계", +"규격": "(10m이하)", +"단위": "㎡", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"2-7-1" +] +], +"직종근거": [ +"비계공", +"보통인부" +], +"인력": { +"비계공": { +"품": 0.054545, +"밑품": 0.054545, +"율": [], +"어떻게": [ +"식" +] +}, +"보통인부": { +"품": 0.018182, +"밑품": 0.018182, +"율": [], +"어떻게": [ +"식" +] +} +}, +"인력절": [ +[ +"2", +"비계공", +0.054545, +0.054545 +], +[ +"2", +"보통인부", +0.018182, +0.018182 +] +], +"장비": [], +"자재": [ +{ +"이름": "강관비계(48.6mm×2.3mm×6m)[M]", +"품": 0.2394, +"밑품": 0.2394 +}, +{ +"이름": "이음철물", +"품": 0.06, +"밑품": 0.06 +}, +{ +"이름": "조임철물(직교자재)", +"품": 0.2496, +"밑품": 0.2496 +}, +{ +"이름": "받침철물", +"품": 0.0036, +"밑품": 0.0036 +}, +{ +"이름": "철물(직교자재)", +"품": 0.04, +"밑품": 0.04 +} +], +"율": [ +[ +"인건비", +2.0 +] +], +"계수": {}, +"조건": {}, +"옛판": false, +"글": [ +"4.강관비계((10m이하))[㎡]", +" [2026년 표준품셈 2-7-1]", +" 1. 재료비", +" 강관비계(48.6mm×2.3mm×6m)[M] 3,950 × 3.99m × 0.06 = 945.6", +" 이음철물 2,200 × 0.5EA × 0.12 = 132", +" 조임철물(직교자재) 2,700 × 2.08EA × 0.12 = 673.9", +" 받침철물 4,000 × 0.04EA × 0.09 = 14.4", +" 철물(직교자재) 2,700 × 0.04EA = 108", +" 자재소계 : 1,873.9", +" 2. 설치 및 해체", +" 비 계 공 : 281,939 × 3 / 55인 = 15,378.4", +" 보통인부 : 172,068 × 1 / 55인 = 3,128.5", +" 인부소계 : 18,506.9", +" 공구손료 및 경장비(콘크리트 진동기등)의 기계경비 (인건비의 2%)", +" 18,506.9 × 2% = 370.1", +" 3. 합 계", +" 노무비 : 18,506", +" 재료비 : 1,873.9 + 370.1 = 2,244", +" 경 비 :", +" 계 : 20,750" +], +"절": { +"1": { +"이름": "재료비", +"출처": [] +}, +"2": { +"이름": "설치 및 해체", +"출처": [] +}, +"3": { +"이름": "합 계", +"출처": [] +} +}, +"이름조건": {}, +"정답": [ +"GC000022" +], +"허용": [ +"GF000188" +], +"무리": "사람", +"복합": null +}, +{ +"id": "S057", +"파일": "북부청사방", +"이름": "강관동바리", +"규격": "(2.5m이하)", +"단위": "공/㎥", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"2-6-1" +] +], +"직종근거": [ +"형틀목공", +"보통인부" +], +"인력": { +"형틀목공": { +"품": 0.06, +"밑품": 0.06, +"율": [], +"어떻게": [ +"식" +] +}, +"보통인부": { +"품": 0.02, +"밑품": 0.02, +"율": [], +"어떻게": [ +"식" +] +} +}, +"인력절": [ +[ +"2", +"형틀목공", +0.06, +0.06 +], +[ +"2", +"보통인부", +0.02, +0.02 +] +], +"장비": [], +"자재": [ +{ +"이름": "강관써포트(V42600-4000mm14.3kg)", +"품": 0.0152, +"밑품": 0.0152 +} +], +"율": [ +[ +"재료비", +4.0 +], +[ +"인건비", +100.0 +], +[ +"주재료", +5.0 +] +], +"계수": {}, +"조건": { +"개월": [ +"1개월" +] +}, +"옛판": false, +"글": [ +"3.강관동바리((2.5m이하))[공/㎥]", +" * 강관동바리 [2026년 표준품셈(2-6-1)(2.5mm이하)]", +" ※ 암거구조물(1개월사용 : 재료비의 4%, 인건비의 100%)", +" 1. 재료비", +" 강관써포트(V42600-4000mm14.3kg) 24,500 × 0.38 × 0.04 = 372.4", +" 2. 인 력", +" 형틀목공 : 275,790 × 3 / 50인 = 16,547.4", +" 보통인부 : 172,068 × 1 / 50인 = 3,441.3", +" 인부소계 : 19,988.7", +" 3. 잡재료 및 소모재료 (주재료의 5%)", +" (372.4) × 5% = 18.6", +" 4. 합 계", +" 노무비 : 19,988", +" 재료비 : 372.4 + 18.6 = 391", +" 경 비 :", +" 계 : 20,379" +], +"절": { +"1": { +"이름": "재료비", +"출처": [] +}, +"2": { +"이름": "인 력", +"출처": [] +}, +"3": { +"이름": "잡재료 및 소모재료 (주재료의 5%)", +"출처": [] +}, +"4": { +"이름": "합 계", +"출처": [] +} +}, +"이름조건": {}, +"정답": [ +"GC000015" +], +"허용": [ +"GF000189" +], +"무리": "사람", +"복합": null +}, +{ +"id": "S058", +"파일": "북부청사방", +"이름": "유로폼(Euro Form)", +"규격": "6회, 간단", +"단위": "㎡", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"6-3-3" +] +], +"직종근거": [ +"형틀목공", +"보통인부" +], +"인력": { +"형틀목공": { +"품": 0.1, +"밑품": 0.1, +"율": [], +"어떻게": [ +"식" +] +}, +"보통인부": { +"품": 0.025, +"밑품": 0.025, +"율": [], +"어떻게": [ +"식" +] +} +}, +"인력절": [ +[ +"3", +"형틀목공", +0.1, +0.1 +], +[ +"3", +"보통인부", +0.025, +0.025 +] +], +"장비": [], +"자재": [ +{ +"이름": "유로폼(패널)(600×1200)", +"품": 0.095, +"밑품": 0.095 +} +], +"율": [ +[ +"주자재비", +29.0 +], +[ +"노무비", +3.0 +] +], +"계수": {}, +"조건": { +"난이": [ +"간단" +], +"횟수": [ +"6회" +] +}, +"옛판": false, +"글": [ +"37.유로폼(Euro Form)(6회, 간단)[㎡]", +" 2024년 품셈개정, 6-3-3 유로폼", +" 1. 재료비", +" 유로폼(패널)(600×1200) 28,500 × 0.095매 = 2,707.5", +" 2. 잡재료비(주자재비의 29%)", +" 소모자재 : 2,707.5 × 29% = 785.1", +" 3. 설치비", +" 형틀목공 : 275,790 × 4 ÷ 40인 = 27,579", +" 보통인부 : 172,068 × 1 ÷ 40인 = 4,301.7", +" 인부소계 : 31,880.7", +" 4. 공구손료 및 경장비 기계경비(노무비의 3%)", +" 31,880.7 × 3% = 956.4", +" 5. 합 계", +" 노무비 : 31,880", +" 재료비 : 2,707.5 + 785.1 + 956.4 = 4,449", +" 경 비 :", +" 계 : 36,329" +], +"절": { +"1": { +"이름": "재료비", +"출처": [] +}, +"2": { +"이름": "잡재료비(주자재비의 29%)", +"출처": [] +}, +"3": { +"이름": "설치비", +"출처": [] +}, +"4": { +"이름": "공구손료 및 경장비 기계경비(노무비의 3%)", +"출처": [] +}, +"5": { +"이름": "합 계", +"출처": [] +} +}, +"이름조건": { +"난이": [ +"간단" +], +"횟수": [ +"6회" +] +}, +"정답": [ +"GC000214" +], +"허용": [ +"GF000210" +], +"무리": "사람", +"복합": null +}, +{ +"id": "S059", +"파일": "북부청사방", +"이름": "철근가공조립(현장)", +"규격": "Type-Ⅰ(소량)", +"단위": "Ton", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"6-2" +] +], +"직종근거": [ +"철근공", +"보통인부" +], +"인력": { +"철근공": { +"품": 4.2, +"밑품": 4.2, +"율": [], +"어떻게": [ +"식", +"식" +] +}, +"보통인부": { +"품": 1.4, +"밑품": 1.4, +"율": [], +"어떻게": [ +"식", +"식" +] +} +}, +"인력절": [ +[ +"1.가", +"철근공", +0.67, +0.67 +], +[ +"1.가", +"보통인부", +0.22, +0.22 +], +[ +"2.나", +"철근공", +3.53, +3.53 +], +[ +"2.나", +"보통인부", +1.18, +1.18 +] +], +"장비": [], +"자재": [ +{ +"이름": "어닐링철선(결속선)(#200.9mm200.4m/kg)", +"품": 6.5, +"밑품": 6.5 +} +], +"율": [ +[ +"인력품", +9.0 +], +[ +"인력품", +2.0 +] +], +"계수": {}, +"조건": { +"근": [ +"철근" +], +"규모": [ +"소량" +], +"type": [ +"Ⅰ" +] +}, +"옛판": false, +"글": [ +"52.철근가공조립(현장)(Type-Ⅰ(소량))[Ton]", +" 2024년 철근 현장가공 및 조립(Type-Ⅰ) [건설표준품셈6-2]", +" 1. 현장가공", +" 가. 철 근 : 1Ton - 별 산 -", +" 철 근 공 : 268,187 × 0.67인 = 179,685.2", +" 보통인부 : 172,068 × 0.22인 = 37,854.9", +" 인부소계 : 217,540.1", +" 나. 기계경비(인력품의 9%)", +" 217,540.1 × 9% = 19,578.6", +" 다. 소 계", +" 노무비 : 217,540.1", +" 재료비 : 19,578.6", +" 경 비 :", +" 계 : 237,118.7", +" 2. 조 립", +" 가. 재료비", +" 어닐링철선(결속선)(#200.9mm200.4m/kg) 1,760 × 6.5kg = 11,440", +" 나. 인 력", +" 철 근 공 : 268,187 × 3.53인 = 946,700.1", +" 보통인부 : 172,068 × 1.18인 = 203,040.2", +" 인부소계 : 1,149,740.3", +" 다. 기구손료(인력품의 2%)", +" 1,149,740.3 × 2% = 22,994.8", +" 라. 소 계", +" 노무비 : 1,149,740.3", +" 재료비 : 11,440 + 22,994.8 = 34,434.8", +" 경 비 :", +" 계 : 1,184,175.1", +" 3. 합 계", +" 노무비 : 217,540.1 + 1,149,740.3 = 1,367,280", +" 재료비 : 19,578.6 + 34,434.8 = 54,013", +" 경 비 :", +" 계 : 1,421,293" +], +"절": { +"1": { +"이름": "현장가공", +"출처": [] +}, +"1.가": { +"이름": "현장가공 · 철 근 : 1Ton - 별 산 -", +"출처": [] +}, +"1.나": { +"이름": "현장가공 · 기계경비(인력품의 9%)", +"출처": [] +}, +"1.다": { +"이름": "현장가공 · 소 계", +"출처": [] +}, +"2": { +"이름": "조 립", +"출처": [] +}, +"2.가": { +"이름": "조 립 · 재료비", +"출처": [] +}, +"2.나": { +"이름": "조 립 · 인 력", +"출처": [] +}, +"2.다": { +"이름": "조 립 · 기구손료(인력품의 2%)", +"출처": [] +}, +"2.라": { +"이름": "조 립 · 소 계", +"출처": [] +}, +"3": { +"이름": "합 계", +"출처": [] +} +}, +"이름조건": { +"근": [ +"철근" +], +"규모": [ +"소량" +], +"type": [ +"Ⅰ" +] +}, +"정답": [ +"GC000206", +"GC000207" +], +"허용": [ +"GF000159" +], +"무리": "사람", +"복합": [ +"GC000206", +"GC000207" +] +}, +{ +"id": "S061", +"파일": "북부청사방", +"이름": "철골설치", +"규격": "", +"단위": "Ton", +"갈래": "단가산출", +"출처": [], +"직종근거": [], +"인력": {}, +"인력절": [], +"장비": [ +{ +"갈래": [ +"크레인" +], +"규격": [ +10.0 +], +"시간": 1.0, +"밑시간": 1.0, +"율": [], +"절": "1", +"글": "크레인(타이어)(10Ton)" +} +], +"자재": [], +"율": [], +"계수": {}, +"조건": { +"방법": [ +"기계" +] +}, +"옛판": false, +"글": [ +"51.철골설치[Ton]", +" * 철골설치 (철골세우기의 작업능력 : 기계-크레인10Ton)", +" 1. 기 계 (1일 처리능력 8Ton적용)", +" (1일 = 8시간, 8시간/8Ton = 1hr/Ton)", +" 크레인(타이어)(10Ton)", +" Q = 8Ton ÷ 8hr = 1Ton", +" 노무비 : 59,020 ÷ 1 = 59,020", +" 재료비 : 7,268 ÷ 1 = 7,268", +" 경 비 : 31,344 ÷ 1 = 31,344", +" 소 계 : 97,632", +" 2. 합 계", +" 노무비 : 59,020", +" 재료비 : 7,268", +" 경 비 : 31,344", +" 계 : 97,632" +], +"절": { +"1": { +"이름": "기 계 (1일 처리능력 8Ton적용)", +"출처": [] +}, +"2": { +"이름": "합 계", +"출처": [] +} +}, +"이름조건": {}, +"정답": [ +"GC000563" +], +"허용": [ +"GC000561" +], +"무리": "사람", +"복합": null +}, +{ +"id": "S062", +"파일": "북부청사방", +"이름": "철골가공조립(공장)", +"규격": "(대형)", +"단위": "Ton", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"1-1" +], +[ +"건설품셈", +"1-1-3" +] +], +"직종근거": [ +"철골공", +"용접공" +], +"인력": { +"철골공": { +"품": 1.7608, +"밑품": 1.7608, +"율": [], +"어떻게": [ +"식" +] +}, +"용접공": { +"품": 0.218416, +"밑품": 0.1606, +"율": [ +[ +1.36, +"보정계수" +] +], +"어떻게": [ +"식" +] +} +}, +"인력절": [ +[ +"1.가", +"철골공", +1.7608, +1.7608 +], +[ +"2.가", +"용접공", +0.218416, +0.1606 +] +], +"장비": [], +"자재": [ +{ +"이름": "산소(6000ℓ)(99%,120kg/㎠)[ℓ]", +"품": 2216.35, +"밑품": 2216.35 +}, +{ +"이름": "아세칠렌(AC98%,용접용)", +"품": 1.615, +"밑품": 1.615 +}, +{ +"이름": "서비스볼트(고장력볼트)(M20×60)[본]", +"품": 0.95, +"밑품": 0.95 +} +], +"율": [ +[ +"노무비", +60.0 +] +], +"계수": { +"T": 9.0 +}, +"조건": { +"규모": [ +"대형" +], +"방법": [ +"인력" +] +}, +"옛판": false, +"글": [ +"50.철골가공조립(공장)(대형)[Ton]", +" 1. 철골 가공 조립(품셈 1-1 철골 가공 조립(공장 생산))", +" (H형강 부재 제작 71% 적용, 60Ton 미만, 표준 라멘구조 적용)", +" 가. 인 력", +" 철골공 : 252,641 × 2.48 × 0.71 = 444,850.2", +" 제경비로 적용", +" 444,850.2 × 60% = 266,910.1", +" 나. 소요 부자재", +" 강재 사용량 : Q = 0.95", +" 산소(6000ℓ)(99%,120kg/㎠)[ℓ] 3.3 × 2333 × 0.95 = 7,313.9", +" 아세칠렌(AC98%,용접용) 30,000 × 1.7 × 0.95 = 48,450", +" 서비스볼트(고장력볼트)(M20×60)[본] 1,195 × 1 × 0.95 = 1,135.2", +" 보조강재(열연강판)(T=9-12) 1,080 × 2 × 0.95 = 2,052", +" 자재소계 : 58,951.1", +" 다. 소 계", +" 노무비 : 444,850.2", +" 재료비 : 58,951.1", +" 경 비 : 266,910.1", +" 계 : 770,711.4", +" 2. 용접(품셈 1-1-3 용접(공장))", +" (용접길이 20m미만, 총 강재량 30톤 미만, H형강 부재 제작 73% 적용)", +" 가. 인 력", +" 용접공 : 282,536 × 0.22 × 1.36(보정계수) × 0.73 = 61,710.3", +" 제경비로 적용(노무비의 60%)", +" 61,710.3 × 60% = 37,026.1", +" 나. 소 계", +" 노무비 : 61,710.3", +" 재료비 :", +" 경 비 : 37,026.1", +" 계 : 98,736.4", +" 3. 합 계", +" 노무비 : 444,850.2 + 61,710.3 = 506,560", +" 재료비 : 58,951", +" 경 비 : 266,910.1 + 37,026.1 = 303,936", +" 계 : 869,447" +], +"절": { +"1": { +"이름": "철골 가공 조립(품셈 1-1 철골 가공 조립(공장 생산))", +"출처": [ +[ +"건설품셈", +"1-1" +] +] +}, +"1.가": { +"이름": "철골 가공 조립(품셈 1-1 철골 가공 조립(공장 생산)) · 인 력", +"출처": [ +[ +"건설품셈", +"1-1" +] +] +}, +"1.나": { +"이름": "철골 가공 조립(품셈 1-1 철골 가공 조립(공장 생산)) · 소요 부자재", +"출처": [ +[ +"건설품셈", +"1-1" +] +] +}, +"1.다": { +"이름": "철골 가공 조립(품셈 1-1 철골 가공 조립(공장 생산)) · 소 계", +"출처": [ +[ +"건설품셈", +"1-1" +] +] +}, +"2": { +"이름": "용접(품셈 1-1-3 용접(공장))", +"출처": [ +[ +"건설품셈", +"1-1-3" +] +] +}, +"2.가": { +"이름": "용접(품셈 1-1-3 용접(공장)) · 인 력", +"출처": [ +[ +"건설품셈", +"1-1-3" +] +] +}, +"2.나": { +"이름": "용접(품셈 1-1-3 용접(공장)) · 소 계", +"출처": [ +[ +"건설품셈", +"1-1-3" +] +] +}, +"3": { +"이름": "합 계", +"출처": [] +} +}, +"이름조건": { +"규모": [ +"대형" +] +}, +"정답": [ +"GC000559" +], +"허용": [ +"GC000560" +], +"무리": "사람", +"복합": null +}, +{ +"id": "S063", +"파일": "북부청사방", +"이름": "잡철물제작", +"규격": "", +"단위": "Ton", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"8-3-1" +] +], +"직종근거": [ +"철공", +"보통인부", +"용접공", +"특별인부" +], +"인력": { +"철공": { +"품": 12.38, +"밑품": 12.38, +"율": [], +"어떻게": [ +"식" +] +}, +"보통인부": { +"품": 2.25, +"밑품": 2.25, +"율": [], +"어떻게": [ +"식" +] +}, +"용접공": { +"품": 3.38, +"밑품": 3.38, +"율": [], +"어떻게": [ +"식" +] +}, +"특별인부": { +"품": 4.5, +"밑품": 4.5, +"율": [], +"어떻게": [ +"식" +] +} +}, +"인력절": [ +[ +"1", +"철공", +12.38, +12.38 +], +[ +"1", +"보통인부", +2.25, +2.25 +], +[ +"1", +"용접공", +3.38, +3.38 +], +[ +"1", +"특별인부", +4.5, +4.5 +] +], +"장비": [], +"자재": [], +"율": [ +[ +"인력품", +5.0 +], +[ +"인력품", +3.0 +] +], +"계수": {}, +"조건": { +"난이": [ +"보통" +] +}, +"옛판": false, +"글": [ +"42.잡철물제작[Ton]", +" * 각종 잡철물 제작설치 (품셈 8-3-1)", +" 1. 인건비", +" 철 공 : 239,808 × 12.38인 = 2,968,823", +" 보통인부 : 172,068 × 2.25인 = 387,153", +" 용 접 공 : 282,536 × 3.38인 = 954,971.6", +" 특별인부 : 226,122 × 4.50인 = 1,017,549", +" 인부소계 : 5,328,496.6", +" 2. 공구손료 및 경장비 기계경비(인력품의 5% 적용)", +" 5,328,496.6 × 5% = 266,424.8", +" 3. 잡재료비(인력품의 3% 적용)", +" 5,328,496.6 × 3% = 159,854.8", +" 4. 합 계", +" 노무비 : 5,328,496", +" 재료비 : 159,854", +" 경 비 : 266,424", +" 계 : 5,754,774" +], +"절": { +"1": { +"이름": "인건비", +"출처": [] +}, +"2": { +"이름": "공구손료 및 경장비 기계경비(인력품의 5% 적용)", +"출처": [] +}, +"3": { +"이름": "잡재료비(인력품의 3% 적용)", +"출처": [] +}, +"4": { +"이름": "합 계", +"출처": [] +} +}, +"이름조건": {}, +"정답": [ +"GC000676" +], +"허용": [], +"무리": "사람", +"복합": null +}, +{ +"id": "S064", +"파일": "북부청사방", +"이름": "합판거푸집(6회)", +"규격": "", +"단위": "㎡", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"6-3-1" +] +], +"직종근거": [ +"형틀목공", +"보통인부" +], +"인력": { +"형틀목공": { +"품": 0.1, +"밑품": 0.1, +"율": [], +"어떻게": [ +"식" +] +}, +"보통인부": { +"품": 0.04, +"밑품": 0.04, +"율": [], +"어떻게": [ +"식" +] +} +}, +"인력절": [ +[ +"2", +"형틀목공", +0.1, +0.1 +], +[ +"2", +"보통인부", +0.04, +0.04 +] +], +"장비": [], +"자재": [ +{ +"이름": "내수합판1급(12m/m1220×2440)[㎡]", +"품": 0.33681, +"밑품": 1.03 +}, +{ +"이름": "각재(외송)(3.6×3.0×3.0)[㎥]", +"품": 0.012426, +"밑품": 0.038 +} +], +"율": [ +[ +"회", +38.0 +], +[ +"주자재비", +11.0 +], +[ +"노무비", +1.0 +] +], +"계수": {}, +"조건": { +"난이": [ +"간단" +], +"횟수": [ +"1회", +"6회" +] +}, +"옛판": false, +"글": [ +"67.합판거푸집(6회)[㎡]", +" 2024년 품셈개정, 6-3-1 합판거푸집", +" ※ 사용횟수 6회 : 간단", +" 1. 재료비(1회의 38%)", +" 내수합판1급(12m/m1220×2440)[㎡] 11,421 × 1.03㎡ × 32.7% = 3,846.7", +" 각재(외송)(3.6×3.0×3.0)[㎥] 577,844 × 0.038㎥ × 32.7% = 7,180.2", +" 자재소계 : 11,026.9", +" 소모자재(박리재 등)(주자재비의 11%)", +" 11,026.9 × 11% = 1,212.9", +" 2. 인건비", +" 형틀목공 : 275,790 × 5 ÷ 50인 = 27,579", +" 보통인부 : 172,068 × 2 ÷ 50인 = 6,882.7", +" 인부소계 : 34,461.7", +" 공구손료 및 경장비 기계경비(노무비의 1%)", +" 34,461.7 × 1% = 344.6", +" 3. 합 계", +" 노무비 : 34,461", +" 재료비 : 11,026.9 + 1,212.9 + 344.6 = 12,584", +" 경 비 :", +" 계 : 47,045" +], +"절": { +"1": { +"이름": "재료비(1회의 38%)", +"출처": [] +}, +"2": { +"이름": "인건비", +"출처": [] +}, +"3": { +"이름": "합 계", +"출처": [] +} +}, +"이름조건": { +"횟수": [ +"6회" +] +}, +"정답": [ +"GC000211" +], +"허용": [ +"GF000160" +], +"무리": "사람", +"복합": null +}, +{ +"id": "S065", +"파일": "북부청사방", +"이름": "와이어메시 바닥깔기", +"규격": "", +"단위": "㎡", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"8-1-3" +] +], +"직종근거": [ +"특별인부" +], +"인력": { +"특별인부": { +"품": 0.006, +"밑품": 0.006, +"율": [], +"어떻게": [ +"식" +] +} +}, +"인력절": [ +[ +"2", +"특별인부", +0.006, +0.006 +] +], +"장비": [], +"자재": [ +{ +"이름": "와이어매쉬(#6100×100)[㎡]", +"품": 1.05, +"밑품": 1.05 +} +], +"율": [ +[ +"주재료비", +3.0 +] +], +"계수": {}, +"조건": {}, +"옛판": false, +"글": [ +"33.와이어메시 바닥깔기[㎡]", +" * 와이어메시 바닥깔기 [건설표준품셈(8-1-3)]", +" 1. 재료비", +" 와이어매쉬(#6100×100)[㎡] 4,525 × 1.05 = 4,751.2", +" 잡재료비(주재료비의 3%)", +" 4,751.2 × 3% = 142.5", +" 2. 인건비", +" 특별인부 : 226,122 × 0.006 = 1,356.7", +" 3. 합 계", +" 노무비 : 1,356", +" 재료비 : 4,751.2 + 142.5 = 4,893", +" 경 비 :", +" 계 : 6,249" +], +"절": { +"1": { +"이름": "재료비", +"출처": [] +}, +"2": { +"이름": "인건비", +"출처": [] +}, +"3": { +"이름": "합 계", +"출처": [] +} +}, +"이름조건": {}, +"정답": [ +"GC000665" +], +"허용": [], +"무리": "사람", +"복합": null +}, +{ +"id": "S066", +"파일": "북부청사방", +"이름": "구조물터파기(토사)", +"규격": "", +"단위": "㎥", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"3-2-4" +] +], +"직종근거": [ +"특별인부" +], +"인력": { +"특별인부": { +"품": 0.031746, +"밑품": 0.31746, +"율": [ +[ +0.1, +"섞음" +] +], +"어떻게": [ +"식" +] +} +}, +"인력절": [ +[ +"1", +"특별인부", +0.031746, +0.31746 +] +], +"장비": [ +{ +"갈래": [ +"굴착기" +], +"규격": [ +0.6 +], +"시간": 0.048, +"밑시간": 0.05333333333333333, +"율": [ +[ +0.9, +"섞음" +] +], +"절": "2.가", +"글": "구조물터파기 : 굴착기(타이어형)(0.6㎥)[hr]" +} +], +"자재": [], +"율": [ +[ +"기계경비", +2.0 +] +], +"계수": {}, +"조건": { +"토질": [ +"경질토사", +"보통토사", +"토사" +], +"난이": [ +"보통" +], +"방법": [ +"인력" +] +}, +"옛판": false, +"글": [ +"11.구조물터파기(토사)[㎥]", +" 1. 인 력 (10%) 적용", +" 보통토사 + 경질토사 적용", +" 1일 시공량 : Q = (3.6 + 2.7) / 2 = 3.15㎥/일", +" 특별인부 : 226,122 / 3.15 × 0.1 = 7,178.4", +" 2. 기 계(90%) - 혼합토사 [2025년 표준품셈3-2-4]", +" 가. 구조물터파기 : 굴착기(타이어형)(0.6㎥)[hr]", +" * TypeⅢ(작업공간 협소) 반영", +" 1일 시공량 : Qd = 150㎥/일 (혼합토사 기준)", +" Q = Qd / 8 = 150 / 8 = 18.75㎥/hr", +" 노무비 : 59,020 ÷ 18.75 × 0.9 = 2,832.9", +" 재료비 : 19,792 ÷ 18.75 × 0.9 = 950", +" 경 비 : 27,120 ÷ 18.75 × 0.9 = 1,301.7", +" 소 계 : 5,084.6", +" 나. 리퍼경비(기계경비의 2%)", +" (2,832.9 + 950 + 1,301.7) × 2% = 101.6", +" 다. 소 계", +" 노무비 : 2,832.9", +" 재료비 : 950", +" 경 비 : 1,301.7 + 101.6 = 1,403.3", +" 계 : 5,186.2", +" 3. 합 계", +" 노무비 : 7,178.4 + 2,832.9 = 10,011", +" 재료비 : 950", +" 경 비 : 1,403", +" 계 : 12,364" +], +"절": { +"1": { +"이름": "인 력 (10%) 적용", +"출처": [] +}, +"2": { +"이름": "기 계(90%) - 혼합토사 [2025년 표준품셈3-2-4]", +"출처": [ +[ +"건설품셈", +"3-2-4" +] +] +}, +"2.가": { +"이름": "기 계(90%) - 혼합토사 [2025년 표준품셈3-2-4] · 구조물터파기 : 굴착기(타이어형)(0.6㎥)[hr]", +"출처": [ +[ +"건설품셈", +"3-2-4" +] +] +}, +"2.나": { +"이름": "기 계(90%) - 혼합토사 [2025년 표준품셈3-2-4] · 리퍼경비(기계경비의 2%)", +"출처": [ +[ +"건설품셈", +"3-2-4" +] +] +}, +"2.다": { +"이름": "기 계(90%) - 혼합토사 [2025년 표준품셈3-2-4] · 소 계", +"출처": [ +[ +"건설품셈", +"3-2-4" +] +] +}, +"3": { +"이름": "합 계", +"출처": [] +} +}, +"이름조건": { +"토질": [ +"토사" +] +}, +"정답": [ +"GC000089" +], +"허용": [ +"GC000088" +], +"무리": "사람", +"복합": null +}, +{ +"id": "S067", +"파일": "북부청사방", +"이름": "구조물되메우기 및 다짐", +"규격": "", +"단위": "㎥", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"3-4-7" +] +], +"직종근거": [ +"보통인부", +"특별인부" +], +"인력": { +"보통인부": { +"품": 0.00345, +"밑품": 0.00345, +"율": [], +"어떻게": [ +"식" +] +}, +"특별인부": { +"품": 0.00345, +"밑품": 0.00345, +"율": [], +"어떻게": [ +"식" +] +} +}, +"인력절": [ +[ +"1", +"보통인부", +0.00345, +0.00345 +], +[ +"1", +"특별인부", +0.00345, +0.00345 +] +], +"장비": [ +{ +"갈래": [ +"굴착기" +], +"규격": [ +0.6 +], +"시간": 0.027586206896551724, +"밑시간": 0.027586206896551724, +"율": [], +"절": "2", +"글": "기 계 : 굴착기(무한궤도)(0.6㎥)" +}, +{ +"갈래": [ +"롤러" +], +"규격": [ +10.0 +], +"시간": 0.027586206896551724, +"밑시간": 0.027586206896551724, +"율": [], +"절": "3", +"글": "다 짐 : 진동로울러(자주식)(10Ton)" +}, +{ +"갈래": [ +"롤러" +], +"규격": [ +0.7 +], +"시간": 0.027586206896551724, +"밑시간": 0.027586206896551724, +"율": [], +"절": "4", +"글": "다 짐 : 진동로울러(핸드가이드식)(0.7Ton)" +}, +{ +"갈래": [ +"살수차" +], +"규격": [], +"시간": 0.013793103448275862, +"밑시간": 0.013793103448275862, +"율": [], +"절": "5", +"글": "살 수 : 물탱크(5500ℓ)" +} +], +"자재": [], +"율": [], +"계수": {}, +"조건": { +"규모": [ +"대형" +], +"방법": [ +"장비" +] +}, +"옛판": false, +"글": [ +"8.구조물되메우기 및 다짐[㎥]", +" [2025년 건설표준품셈3-4-7]", +" 1일 시공량 : Qd = 290㎥/일 (대형장비 기준)", +" Q = Qd / 8 = 290 / 8 = 36.25㎥/hr", +" 1. 인건비", +" P = 1인 ÷ Qd = 1 ÷ 290 = 0.00345인", +" 보통인부 : 172,068 × P = 172,068 × 0.00345 = 593.6", +" 특별인부 : 226,122 × P = 226,122 × 0.00345 = 780.1", +" 인부소계 : 1,373.7", +" 2. 기 계 : 굴착기(무한궤도)(0.6㎥)", +" 노무비 : 59,020 ÷ 36.25 = 1,628.1", +" 재료비 : 17,122 ÷ 36.25 = 472.3", +" 경 비 : 23,423 ÷ 36.25 = 646.1", +" 소 계 : 2,746.5", +" 3. 다 짐 : 진동로울러(자주식)(10Ton)", +" 노무비 : 59,020 ÷ 36.25 = 1,628.1", +" 재료비 : 25,758 ÷ 36.25 = 710.5", +" 경 비 : 26,662 ÷ 36.25 = 735.5", +" 소 계 : 3,074.1", +" 4. 다 짐 : 진동로울러(핸드가이드식)(0.7Ton)", +" 재료비 : 3,420 ÷ 36.25 = 94.3", +" 경 비 : 1,967 ÷ 36.25 = 54.2", +" 소 계 : 148.5", +" 5. 살 수 : 물탱크(5500ℓ)", +" 노무비 : 49,778 ÷ 36.25 × 0.5 = 686.5", +" 재료비 : 16,635 ÷ 36.25 × 0.5 = 229.4", +" 경 비 : 9,986 ÷ 36.25 × 0.5 = 137.7", +" 소 계 : 1,053.6", +" 6. 합 계", +" 노무비 : 1,373.7 + 1,628.1 + 1,628.1 + 686.5 = 5,316", +" 재료비 : 472.3 + 710.5 + 94.3 + 229.4 = 1,506", +" 경 비 : 646.1 + 735.5 + 54.2 + 137.7 = 1,573", +" 계 : 8,395" +], +"절": { +"1": { +"이름": "인건비", +"출처": [] +}, +"2": { +"이름": "기 계 : 굴착기(무한궤도)(0.6㎥)", +"출처": [] +}, +"3": { +"이름": "다 짐 : 진동로울러(자주식)(10Ton)", +"출처": [] +}, +"4": { +"이름": "다 짐 : 진동로울러(핸드가이드식)(0.7Ton)", +"출처": [] +}, +"5": { +"이름": "살 수 : 물탱크(5500ℓ)", +"출처": [] +}, +"6": { +"이름": "합 계", +"출처": [] +} +}, +"이름조건": {}, +"정답": [ +"GC000109" +], +"허용": [], +"무리": "사람", +"복합": null +}, +{ +"id": "S069", +"파일": "북부청사방", +"이름": "레미콘타설(장비사용)", +"규격": "무근구조물-소량타설", +"단위": "㎥", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"6-1-1" +] +], +"직종근거": [ +"콘크리트공", +"보통인부" +], +"인력": { +"콘크리트공": { +"품": 0.095238, +"밑품": 0.095238, +"율": [], +"어떻게": [ +"식" +] +}, +"보통인부": { +"품": 0.031746, +"밑품": 0.031746, +"율": [], +"어떻게": [ +"식" +] +} +}, +"인력절": [ +[ +"2", +"콘크리트공", +0.095238, +0.095238 +], +[ +"2", +"보통인부", +0.031746, +0.031746 +] +], +"장비": [ +{ +"갈래": [ +"굴착기" +], +"규격": [ +0.6 +], +"시간": 0.25, +"밑시간": 0.25, +"율": [], +"절": "4", +"글": "굴착기(타이어형)(0.6㎥)[hr]" +} +], +"자재": [ +{ +"이름": "레미콘", +"품": null, +"밑품": null +} +], +"율": [ +[ +"노무비", +2.0 +] +], +"계수": {}, +"조건": { +"근": [ +"무근" +], +"규모": [ +"소량" +], +"방법": [ +"장비" +] +}, +"옛판": false, +"글": [ +"21.레미콘타설(장비사용)(무근구조물-소량타설)[㎥]", +" 2024년 품셈개정, 6-1-1콘크리트 타설", +" ○. 레미콘타설(장비사용타설-무근(소량)구조물)", +" 1. 레미콘 : 별도계상", +" 2. 타설비", +" 콘크리트공 : 273,540 × 3 ÷ 63 × 2인 = 26,051.4", +" 보통인부 : 172,068 × 1 ÷ 63 × 2인 = 5,462.4", +" 인부소계 : 31,513.8", +" 3. 공구손료 및 경장비(콘크리트 진동기등)의 기계경비(노무비의 2%)", +" 31,513.8 × 2% = 630.2", +" 4. 굴착기(타이어형)(0.6㎥)[hr]", +" Q = 1 × 8 / 63 × 2 = 0.25hr/㎥", +" 노무비 : 59,020 × 0.25 = 14,755", +" 재료비 : 19,792 × 0.25 = 4,948", +" 경 비 : 27,120 × 0.25 = 6,780", +" 소 계 : 26,483", +" 5. 합 계", +" 노무비 : 31,513.8 + 14,755 = 46,268", +" 재료비 : 630.2 + 4,948 = 5,578", +" 경 비 : 6,780", +" 계 : 58,626" +], +"절": { +"1": { +"이름": "레미콘 : 별도계상", +"출처": [] +}, +"2": { +"이름": "타설비", +"출처": [] +}, +"3": { +"이름": "공구손료 및 경장비(콘크리트 진동기등)의 기계경비(노무비의 2%)", +"출처": [] +}, +"4": { +"이름": "굴착기(타이어형)(0.6㎥)[hr]", +"출처": [] +}, +"5": { +"이름": "합 계", +"출처": [] +} +}, +"이름조건": { +"근": [ +"무근" +], +"규모": [ +"소량" +], +"방법": [ +"장비" +] +}, +"정답": [ +"GC000198" +], +"허용": [ +"GF000155" +], +"무리": "사람", +"복합": null +}, +{ +"id": "S070", +"파일": "북부청사방", +"이름": "합판거푸집(4회)", +"규격": "", +"단위": "㎡", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"6-3-1" +] +], +"직종근거": [ +"형틀목공", +"보통인부" +], +"인력": { +"형틀목공": { +"품": 0.111111, +"밑품": 0.111111, +"율": [], +"어떻게": [ +"식" +] +}, +"보통인부": { +"품": 0.044444, +"밑품": 0.044444, +"율": [], +"어떻게": [ +"식" +] +} +}, +"인력절": [ +[ +"2", +"형틀목공", +0.111111, +0.111111 +], +[ +"2", +"보통인부", +0.044444, +0.044444 +] +], +"장비": [], +"자재": [ +{ +"이름": "내수합판1급(12m/m1220×2440)[㎡]", +"품": 0.3914, +"밑품": 1.03 +}, +{ +"이름": "각재(외송)(3.6×3.0×3.0)[㎥]", +"품": 0.01444, +"밑품": 0.038 +} +], +"율": [ +[ +"회", +38.0 +], +[ +"주자재비", +9.0 +], +[ +"노무비", +1.0 +] +], +"계수": {}, +"조건": { +"난이": [ +"보통" +], +"횟수": [ +"1회", +"4회" +] +}, +"옛판": false, +"글": [ +"66.합판거푸집(4회)[㎡]", +" 2024년 품셈개정, 6-3-1 합판거푸집", +" ※ 사용횟수 4회 : 보통", +" 1. 재료비(1회의 38%)", +" 내수합판1급(12m/m1220×2440)[㎡] 11,421 × 1.03㎡ × 38% = 4,470.1", +" 각재(외송)(3.6×3.0×3.0)[㎥] 577,844 × 0.038㎥ × 38% = 8,344", +" 자재소계 : 12,814.1", +" 소모자재(박리재 등)(주자재비의 9%)", +" 12,814.1 × 9% = 1,153.2", +" 2. 인건비", +" 형틀목공 : 275,790 × 5 ÷ 45인 = 30,643.3", +" 보통인부 : 172,068 × 2 ÷ 45인 = 7,647.4", +" 인부소계 : 38,290.7", +" 공구손료 및 경장비 기계경비(노무비의 1%)", +" 38,290.7 × 1% = 382.9", +" 3. 합 계", +" 노무비 : 38,290", +" 재료비 : 12,814.1 + 1,153.2 + 382.9 = 14,350", +" 경 비 :", +" 계 : 52,640" +], +"절": { +"1": { +"이름": "재료비(1회의 38%)", +"출처": [] +}, +"2": { +"이름": "인건비", +"출처": [] +}, +"3": { +"이름": "합 계", +"출처": [] +} +}, +"이름조건": { +"횟수": [ +"4회" +] +}, +"정답": [ +"GC000211" +], +"허용": [ +"GF000160" +], +"무리": "사람", +"복합": null +}, +{ +"id": "S071", +"파일": "북부청사방", +"이름": "기초잡석다짐 및 채집운반", +"규격": "(9:1)(0.6㎥+4.5Ton)", +"단위": "㎥", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"3-4-8" +] +], +"직종근거": [ +"보통인부", +"특별인부" +], +"인력": { +"보통인부": { +"품": 0.041, +"밑품": 0.311, +"율": [ +[ +0.1, +"섞음" +] +], +"어떻게": [ +"식", +"식" +] +}, +"특별인부": { +"품": 0.011, +"밑품": 0.011, +"율": [], +"어떻게": [ +"식" +] +} +}, +"인력절": [ +[ +"1.나", +"보통인부", +0.03, +0.3 +], +[ +"4.다", +"특별인부", +0.011, +0.011 +], +[ +"4.다", +"보통인부", +0.011, +0.011 +] +], +"장비": [ +{ +"갈래": [ +"굴착기" +], +"규격": [ +0.6 +], +"시간": 0.043774319066147864, +"밑시간": 0.048638132295719845, +"율": [ +[ +0.9, +"섞음" +] +], +"절": "1.가", +"글": "굴착기(타이어형)(0.6㎥)[hr] (90%)" +}, +{ +"갈래": [ +"굴착기" +], +"규격": [ +0.6 +], +"시간": 0.03302509907529722, +"밑시간": 0.03302509907529722, +"율": [], +"절": "2", +"글": "굴착기(타이어형)(0.6㎥)[hr]" +}, +{ +"갈래": [ +"덤프트럭" +], +"규격": [ +4.5 +], +"시간": 0.08368200836820085, +"밑시간": 0.08368200836820085, +"율": [], +"절": "3", +"글": "덤프트럭(4.5Ton)" +}, +{ +"갈래": [ +"굴착기" +], +"규격": [ +0.2 +], +"시간": 0.08888888888888889, +"밑시간": 0.08888888888888889, +"율": [], +"절": "4.가", +"글": "굴착기(무한궤도)(0.2㎥)[hr]" +}, +{ +"갈래": [ +"롤러" +], +"규격": [ +0.7 +], +"시간": 0.08888888888888889, +"밑시간": 0.08888888888888889, +"율": [], +"절": "4.나", +"글": "진동로울러(핸드가이드식)(0.7Ton)" +} +], +"자재": [], +"율": [], +"계수": { +"q": 0.6, +"k": 0.55, +"Cm": 18.0, +"E": 0.35, +"L": 0.5, +"t3": 1.1, +"t4": 0.42 +}, +"조건": { +"방법": [ +"기계", +"인력" +] +}, +"옛판": false, +"글": [ +"12.기초잡석다짐 및 채집운반((9:1)(0.6㎥+4.5Ton))[㎥]", +" 1. 채 집 (기계 : 인력 = 9 : 1)", +" 가. 굴착기(타이어형)(0.6㎥)[hr] (90%)", +" q = 0.6 f = 1 / 1.125 = 0.89 k = 0.55", +" Cm = 18sec(90˚) E = 0.35", +" 3600×q×k×f×E 3,600×0.6×0.55×0.89×0.35", +" Q = ────────── = ─────────────── = 20.56㎥/hr", +" Cm 18", +" 노무비 : 59,020 ÷ 20.56 × 0.9 = 2,583.5", +" 재료비 : 19,792 ÷ 20.56 × 0.9 = 866.3", +" 경 비 : 27,120 ÷ 20.56 × 0.9 = 1,187.1", +" 소 계 : 4,636.9", +" 나. 인 력 (10%)", +" 보통인부 : 172,068 × 0.3 × 0.1 = 5,162", +" 다. 소 계", +" 노무비 : 2,583.5 + 5,162 = 7,745.5", +" 재료비 : 866.3", +" 경 비 : 1,187.1", +" 계 : 9,798.9", +" 2. 적 사", +" 굴착기(타이어형)(0.6㎥)[hr]", +" q = 0.6 f = 1 / 1.125 = 0.89 k = 0.7", +" Cm = 20sec(135˚) E = 0.45", +" 3600×q×k×f×E 3,600×0.6×0.7×0.89×0.45", +" Q = ────────── = ────────────── =30.28㎥/hr", +" Cm 20", +" 노무비 : 59,020 ÷ 30.28 = 1,949.1", +" 재료비 : 19,792 ÷ 30.28 = 653.6", +" 경 비 : 27,120 ÷ 30.28 = 895.6", +" 소 계 : 3,498.3", +" 3. 운 반", +" 덤프트럭(4.5Ton)", +" L = 0.5km", +" E = 0.9 f = 1 / 1.125 = 0.89", +" q1 = (4.5 / 1.75) × 1.125 = 2.89", +" n = q1 / (0.6 × k) = 2.89 / (0.6 × 0.7) = 6.88회", +" t1 = 20 × n / (60 × 0.45) = 20 × 6.88 / (60 × 0.45) = 5.1분", +" t2 = (L / 10 + L / 15) × 60 = (0.5 / 10 + 0.5 / 15) × 60 = 5분", +" t3 = 1.1분 t4 = 0.42분", +" Cm = t1 + t2 + t3 + t4 = 5.1 + 5 + 1.1 + 0.42 = 11.62분", +" 60×q1×f×E 60×2.89×0.89×0.9", +" Q = ──────── = ────────── = 11.95㎥/hr", +" Cm 11.62", +" 노무비 : 49,778 ÷ 11.95 = 4,165.5", +" 재료비 : 9,494 ÷ 11.95 = 794.4", +" 경 비 : 7,613 ÷ 11.95 = 637", +" 소 계 : 5,596.9", +" 4. 기초지정 [2025년 표준품셈3-4-8]", +" 가. 굴착기(무한궤도)(0.2㎥)[hr]", +" 1일 시공량 : Qd = 90㎥/일", +" Q = Qd / 8 = 90 / 8 = 11.25㎥/hr", +" 노무비 : 59,020 ÷ 11.25 = 5,246.2", +" 재료비 : 8,324 ÷ 11.25 = 739.9", +" 경 비 : 13,873 ÷ 11.25 = 1,233.1", +" 소 계 : 7,219.2", +" 나. 진동로울러(핸드가이드식)(0.7Ton)", +" 재료비 : 3,420 ÷ 11.25 = 304", +" 경 비 : 1,967 ÷ 11.25 = 174.8", +" 소 계 : 478.8", +" 다. 인 력" +], +"절": { +"1": { +"이름": "채 집 (기계 : 인력 = 9 : 1)", +"출처": [] +}, +"1.가": { +"이름": "채 집 (기계 : 인력 = 9 : 1) · 굴착기(타이어형)(0.6㎥)[hr] (90%)", +"출처": [] +}, +"1.나": { +"이름": "채 집 (기계 : 인력 = 9 : 1) · 인 력 (10%)", +"출처": [] +}, +"1.다": { +"이름": "채 집 (기계 : 인력 = 9 : 1) · 소 계", +"출처": [] +}, +"2": { +"이름": "적 사", +"출처": [] +}, +"3": { +"이름": "운 반", +"출처": [] +}, +"4": { +"이름": "기초지정 [2025년 표준품셈3-4-8]", +"출처": [ +[ +"건설품셈", +"3-4-8" +] +] +}, +"4.가": { +"이름": "기초지정 [2025년 표준품셈3-4-8] · 굴착기(무한궤도)(0.2㎥)[hr]", +"출처": [ +[ +"건설품셈", +"3-4-8" +] +] +}, +"4.나": { +"이름": "기초지정 [2025년 표준품셈3-4-8] · 진동로울러(핸드가이드식)(0.7Ton)", +"출처": [ +[ +"건설품셈", +"3-4-8" +] +] +}, +"4.다": { +"이름": "기초지정 [2025년 표준품셈3-4-8] · 인 력", +"출처": [ +[ +"건설품셈", +"3-4-8" +] +] +}, +"4.마": { +"이름": "기초지정 [2025년 표준품셈3-4-8] · 소 계", +"출처": [ +[ +"건설품셈", +"3-4-8" +] +] +}, +"5": { +"이름": "합 계", +"출처": [] +} +}, +"이름조건": {}, +"정답": [ +"GC000110", +"GF000168" +], +"허용": [], +"무리": "사람", +"복합": null +}, +{ +"id": "S072", +"파일": "북부청사방", +"이름": "배수관부설", +"규격": "(D=1000mm)", +"단위": "M", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"6-7-2" +] +], +"직종근거": [ +"보통인부" +], +"인력": { +"배관공(수도)": { +"품": 0.053333, +"밑품": 0.053333, +"율": [], +"어떻게": [ +"식" +] +}, +"보통인부": { +"품": 0.026667, +"밑품": 0.026667, +"율": [], +"어떻게": [ +"식" +] +} +}, +"인력절": [ +[ +"2", +"배관공(수도)", +0.053333, +0.053333 +], +[ +"2", +"보통인부", +0.026667, +0.026667 +] +], +"장비": [ +{ +"갈래": [ +"크레인" +], +"규격": [ +5.0 +], +"시간": 0.05, +"밑시간": 0.05, +"율": [], +"절": "4", +"글": "트럭탑재형크레인(5Ton)" +} +], +"자재": [], +"율": [ +[ +"인력품", +2.0 +] +], +"계수": {}, +"조건": {}, +"옛판": false, +"글": [ +"26.배수관부설(D=1000mm)[M]", +" * 배수관부설 [건설표준품셈(6-7-2)]", +" 파형강관, D = 1000mm 별도계상", +" 1. 재료비", +" 파형강관(D = 1000mm) : 1m", +" 2. 인건비", +" 배관공(수도) : 262,580 × 0.32인 ÷ 6 = 14,004.2", +" 보 통 인 부 : 172,068 × 0.16인 ÷ 6 = 4,588.4", +" 인부소계 : 18,592.6", +" 3. 공구손료 및 잡재료비(인력품의 2%)", +" 18,592.6 × 2% = 371.8", +" 4. 트럭탑재형크레인(5Ton)", +" Q = 0.3hr / 6 = 0.05", +" 노무비 : 49,778 × 0.05 = 2,488.9", +" 재료비 : 8,421 × 0.05 = 421", +" 경 비 : 11,117 × 0.05 = 555.8", +" 소 계 : 3,465.7", +" 5. 합 계", +" 노무비 : 18,592.6 + 2,488.9 = 21,081", +" 재료비 : 421", +" 경 비 : 371.8 + 555.8 = 927", +" 계 : 22,429" +], +"절": { +"1": { +"이름": "재료비", +"출처": [] +}, +"2": { +"이름": "인건비", +"출처": [] +}, +"3": { +"이름": "공구손료 및 잡재료비(인력품의 2%)", +"출처": [] +}, +"4": { +"이름": "트럭탑재형크레인(5Ton)", +"출처": [] +}, +"5": { +"이름": "합 계", +"출처": [] +} +}, +"이름조건": {}, +"정답": [ +"GC000404" +], +"허용": [ +"GF000176", +"GF000179" +], +"무리": "사람", +"복합": null +}, +{ +"id": "S075", +"파일": "북부청사방", +"이름": "레미콘타설(인력운반)", +"규격": "무근구조물-소량타설", +"단위": "㎥", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"6-1-1" +] +], +"직종근거": [ +"콘크리트공", +"보통인부" +], +"인력": { +"콘크리트공": { +"품": 0.26087, +"밑품": 0.26087, +"율": [], +"어떻게": [ +"식" +] +}, +"보통인부": { +"품": 0.26087, +"밑품": 0.26087, +"율": [], +"어떻게": [ +"식" +] +} +}, +"인력절": [ +[ +"2", +"콘크리트공", +0.26087, +0.26087 +], +[ +"2", +"보통인부", +0.26087, +0.26087 +] +], +"장비": [], +"자재": [ +{ +"이름": "레미콘", +"품": null, +"밑품": null +} +], +"율": [ +[ +"노무비", +2.0 +] +], +"계수": {}, +"조건": { +"근": [ +"무근" +], +"규모": [ +"소량" +], +"방법": [ +"인력운반", +"장비" +] +}, +"옛판": false, +"글": [ +"19.레미콘타설(인력운반)(무근구조물-소량타설)[㎥]", +" 2024년 품셈개정, 6-1-1콘크리트 타설", +" ○. 레미콘타설(장비사용타설-무근구조물(소량타설)", +" 1. 레미콘 : 별도계상", +" 2. 타설비", +" 콘크리트공 : 273,540 × 3 ÷ 23 × 2인 = 71,358.2", +" 보 통 인부 : 172,068 × 3 ÷ 23 × 2인 = 44,887.3", +" 인부소계 : 116,245.5", +" 3. 공구손료 및 경장비(콘크리트 진동기등)의 기계경비(노무비의 2%)", +" 116,245.5 × 2% = 2,324.9", +" 4. 합 계", +" 노무비 : 116,245", +" 재료비 : 2,324", +" 경 비 :", +" 계 : 118,569" +], +"절": { +"1": { +"이름": "레미콘 : 별도계상", +"출처": [] +}, +"2": { +"이름": "타설비", +"출처": [] +}, +"3": { +"이름": "공구손료 및 경장비(콘크리트 진동기등)의 기계경비(노무비의 2%)", +"출처": [] +}, +"4": { +"이름": "합 계", +"출처": [] +} +}, +"이름조건": { +"근": [ +"무근" +], +"규모": [ +"소량" +], +"방법": [ +"인력운반" +] +}, +"정답": [ +"GC000198" +], +"허용": [ +"GF000155" +], +"무리": "사람", +"복합": null +}, +{ +"id": "S076", +"파일": "북부청사방", +"이름": "구조물터파기", +"규격": "인력", +"단위": "㎥", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"3-2-1" +] +], +"직종근거": [ +"특별인부" +], +"인력": { +"특별인부": { +"품": 0.353357, +"밑품": 0.353357, +"율": [], +"어떻게": [ +"식" +] +} +}, +"인력절": [ +[ +"1", +"특별인부", +0.353357, +0.353357 +] +], +"장비": [], +"자재": [], +"율": [], +"계수": {}, +"조건": { +"토질": [ +"경질토사", +"보통토사", +"자갈섞인토사" +], +"난이": [ +"보통" +], +"방법": [ +"인력" +] +}, +"옛판": false, +"글": [ +"9.구조물터파기(인력)[㎥]", +" [2025년 표준품셈 3-2-1]", +" 보통토사 + 경질토사 + 고사점토 및 자갈섞인토사 적용", +" 1. 인 부", +" 1일 시공량 : Q = (3.6 + 2.7 + 2.2) / 3 = 2.83㎥/일", +" 특별인부 : 226,122 / 2.83 = 79,901.7", +" 2. 합 계", +" 노무비 : 79,901", +" 재료비 :", +" 경 비 :", +" 계 : 79,901" +], +"절": { +"1": { +"이름": "인 부", +"출처": [] +}, +"2": { +"이름": "합 계", +"출처": [] +} +}, +"이름조건": { +"방법": [ +"인력" +] +}, +"정답": [ +"GC000083" +], +"허용": [ +"GF000101" +], +"무리": "사람", +"복합": null +}, +{ +"id": "S077", +"파일": "북부청사방", +"이름": "구조물되메우기", +"규격": "인력", +"단위": "㎥", +"갈래": "단가산출", +"출처": [], +"직종근거": [ +"보통인부" +], +"인력": { +"보통인부": { +"품": 0.1, +"밑품": 0.1, +"율": [], +"어떻게": [ +"식" +] +} +}, +"인력절": [ +[ +"1", +"보통인부", +0.1, +0.1 +] +], +"장비": [], +"자재": [], +"율": [], +"계수": {}, +"조건": { +"난이": [ +"보통" +], +"방법": [ +"인력" +] +}, +"옛판": false, +"글": [ +"7.구조물되메우기(인력)[㎥]", +" 1. 인 부", +" 보통인부 : 172,068 × 0.1 = 17,206.8", +" 2. 합 계", +" 노무비 : 17,206", +" 재료비 :", +" 경 비 :", +" 계 : 17,206" +], +"절": { +"1": { +"이름": "인 부", +"출처": [] +}, +"2": { +"이름": "합 계", +"출처": [] +} +}, +"이름조건": { +"방법": [ +"인력" +] +}, +"정답": [ +"GF000113" +], +"허용": [], +"무리": "사람", +"복합": null +}, +{ +"id": "S079", +"파일": "북부청사방", +"이름": "원형돌망태조립설치", +"규격": "산림사업품셈", +"단위": "조", +"갈래": "단가산출", +"출처": [ +[ +"산림품셈", +"13-11-1" +] +], +"직종근거": [ +"보통인부", +"특별인부" +], +"인력": { +"보통인부": { +"품": 0.00765, +"밑품": 0.00765, +"율": [], +"어떻게": [ +"식" +] +}, +"특별인부": { +"품": 0.018, +"밑품": 0.018, +"율": [], +"어떻게": [ +"식" +] +} +}, +"인력절": [ +[ +"1", +"보통인부", +0.00765, +0.00765 +], +[ +"1", +"특별인부", +0.018, +0.018 +] +], +"장비": [], +"자재": [], +"율": [], +"계수": {}, +"조건": { +"형": [ +"원형" +], +"난이": [ +"보통" +] +}, +"옛판": false, +"글": [ +"35.원형돌망태조립설치(산림사업품셈)[조]", +" 1. 돌망태 조립설치", +" [2026년 산림사업표준품셈 13-11-1]", +" * 원형돌망태 조립설치", +" 보통인부 : 172,068 × 0.45m × 1m × 1조 × 0.017인/㎡ = 1,316.3", +" 특별인부 : 226,122 × 0.45m × 1m × 0.04인/㎡ = 4,070.1", +" 인부소계 : 5,386.4", +" 2. 합 계", +" 노무비 : 5,386", +" 재료비 :", +" 경 비 :", +" 계 : 5,386" +], +"절": { +"1": { +"이름": "돌망태 조립설치", +"출처": [] +}, +"2": { +"이름": "합 계", +"출처": [] +} +}, +"이름조건": { +"형": [ +"원형" +] +}, +"정답": [ +"GF000328" +], +"허용": [], +"무리": "사람", +"복합": null +}, +{ +"id": "S080", +"파일": "북부청사방", +"이름": "원형돌망태채움", +"규격": "산림사업품셈", +"단위": "조", +"갈래": "단가산출", +"출처": [ +[ +"산림품셈", +"13-11-1" +] +], +"직종근거": [ +"석공" +], +"인력": { +"석공": { +"품": 0.0189, +"밑품": 0.0189, +"율": [], +"어떻게": [ +"식" +] +} +}, +"인력절": [ +[ +"1.가", +"석공", +0.0189, +0.0189 +] +], +"장비": [ +{ +"갈래": [ +"굴착기" +], +"규격": [ +1.0 +], +"시간": 0.0135, +"밑시간": 0.0135, +"율": [], +"절": "1.나", +"글": "굴착기(무한궤도)(1.0㎥)[hr]" +} +], +"자재": [], +"율": [], +"계수": {}, +"조건": { +"형": [ +"원형" +], +"방법": [ +"인력" +] +}, +"옛판": false, +"글": [ +"36.원형돌망태채움(산림사업품셈)[조]", +" 1. 돌망태 돌채움", +" [2026년 산림사업표준품셈 13-11-1]", +" * 원형돌망태 채움", +" 가. 인 력", +" 석공 : 268,908 × 0.45m × 1m × 1조 × 0.042인/㎡ = 5,082.3", +" 나. 굴착기(무한궤도)(1.0㎥)[hr]", +" 노무비 : 59,020 × 0.03 × 0.45m × 1m × 1조 = 796.7", +" 재료비 : 32,735 × 0.03 × 0.45m × 1m × 1조 = 441.9", +" 경 비 : 29,621 × 0.03 × 0.45m × 1m × 1조 = 399.8", +" 소 계 : 1,638.4", +" 다. 소 계", +" 노무비 : 5,082.3 + 796.7 = 5,879", +" 재료비 : 441.9", +" 경 비 : 399.8", +" 계 : 6,720.7", +" 2. 합 계", +" 노무비 : 5,879", +" 재료비 : 441", +" 경 비 : 399", +" 계 : 6,719" +], +"절": { +"1": { +"이름": "돌망태 돌채움", +"출처": [] +}, +"1.가": { +"이름": "돌망태 돌채움 · 인 력", +"출처": [] +}, +"1.나": { +"이름": "돌망태 돌채움 · 굴착기(무한궤도)(1.0㎥)[hr]", +"출처": [] +}, +"1.다": { +"이름": "돌망태 돌채움 · 소 계", +"출처": [] +}, +"2": { +"이름": "합 계", +"출처": [] +} +}, +"이름조건": { +"형": [ +"원형" +] +}, +"정답": [ +"GF000328" +], +"허용": [], +"무리": "사람", +"복합": null +}, +{ +"id": "S081", +"파일": "북부청사방", +"이름": "조약돌채집운반", +"규격": "(9:1)(0.6㎥+4.5Ton)", +"단위": "㎥", +"갈래": "단가산출", +"출처": [], +"직종근거": [ +"보통인부" +], +"인력": { +"보통인부": { +"품": 0.06, +"밑품": 0.6, +"율": [ +[ +0.1, +"섞음" +] +], +"어떻게": [ +"식" +] +} +}, +"인력절": [ +[ +"1.나", +"보통인부", +0.06, +0.6 +] +], +"장비": [ +{ +"갈래": [ +"굴착기" +], +"규격": [ +0.6 +], +"시간": 0.043774319066147864, +"밑시간": 0.048638132295719845, +"율": [ +[ +0.9, +"섞음" +] +], +"절": "1.가", +"글": "굴착기(타이어형)(0.6㎥)[hr] : 90%" +}, +{ +"갈래": [ +"굴착기" +], +"규격": [ +0.6 +], +"시간": 0.03302509907529722, +"밑시간": 0.03302509907529722, +"율": [], +"절": "2", +"글": "굴착기(타이어형)(0.6㎥)[hr]" +}, +{ +"갈래": [ +"덤프트럭" +], +"규격": [ +4.5 +], +"시간": 0.08368200836820085, +"밑시간": 0.08368200836820085, +"율": [], +"절": "3", +"글": "덤프트럭(4.5Ton)" +} +], +"자재": [], +"율": [], +"계수": { +"q": 0.6, +"k": 0.55, +"Cm": 18.0, +"E": 0.35, +"L": 0.5, +"t3": 1.1, +"t4": 0.42 +}, +"조건": {}, +"옛판": false, +"글": [ +"43.조약돌채집운반((9:1)(0.6㎥+4.5Ton))[㎥]", +" 1. 채 집", +" 가. 굴착기(타이어형)(0.6㎥)[hr] : 90%", +" q = 0.6 f = 1 / 1.125 = 0.89 k = 0.55", +" Cm = 18sec(90˚) E = 0.35", +" 3600×q×k×f×E 3,600×0.6×0.55×0.89×0.35", +" Q = ────────── = ─────────────── = 20.56 ㎥/hr", +" Cm 18", +" 노무비 : 59,020 ÷ 20.56 × 0.9 = 2,583.5", +" 재료비 : 19,792 ÷ 20.56 × 0.9 = 866.3", +" 경 비 : 27,120 ÷ 20.56 × 0.9 = 1,187.1", +" 소 계 : 4,636.9", +" 나. 인 력 (10%)", +" 보통인부 : 172,068 × 0.6 × 0.1 = 10,324", +" 다. 소 계", +" 노무비 : 2,583.5 + 10,324 = 12,907.5", +" 재료비 : 866.3", +" 경 비 : 1,187.1", +" 계 : 14,960.9", +" 2. 적 사", +" 굴착기(타이어형)(0.6㎥)[hr]", +" q = 0.6 f = 1 / 1.125 = 0.89 k = 0.7", +" Cm = 20sec(135˚) E = 0.45", +" 3600×q×k×f×E 3,600×0.6×0.7×0.89×0.45", +" Q = ────────── = ────────────── = 30.28 ㎥/hr", +" Cm 20", +" 노무비 : 59,020 ÷ 30.28 = 1,949.1", +" 재료비 : 19,792 ÷ 30.28 = 653.6", +" 경 비 : 27,120 ÷ 30.28 = 895.6", +" 소 계 : 3,498.3", +" 3. 운 반", +" 덤프트럭(4.5Ton)", +" L = 0.5km", +" E = 0.9 f = 1 / 1.125 = 0.89", +" q1 = (4.5 / 1.75) × 1.125 = 2.89", +" n = q1 / (0.6 × k) = 2.89 / (0.6 × 0.7) = 6.88 회", +" t1 = 20 × n / (60 × 0.45) = 20 × 6.88 / (60 × 0.45) = 5.1분", +" L L 0.5 0.5", +" t2 = (──+──)×60 = (──+──)×60 = 5분", +" 10 15 10 15", +" t3 = 1.1분 t4 = 0.42분", +" Cm = t1 + t2 + t3 + t4 = 5.1 + 5 + 1.1 + 0.42 = 11.62 분", +" 60×q1×f×E 60×2.89×0.89×0.9", +" Q = ──────── = ────────── = 11.95㎥/hr", +" Cm 11.62", +" 노무비 : 49,778 ÷ 11.95 = 4,165.5", +" 재료비 : 9,494 ÷ 11.95 = 794.4", +" 경 비 : 7,613 ÷ 11.95 = 637", +" 소 계 : 5,596.9", +" 5. 합 계", +" 노무비 : 12,907.5 + 1,949.1 + 4,165.5 = 19,022", +" 재료비 : 866.3 + 653.6 + 794.4 = 2,314", +" 경 비 : 1,187.1 + 895.6 + 637 = 2,719", +" 계 : 24,055" +], +"절": { +"1": { +"이름": "채 집", +"출처": [] +}, +"1.가": { +"이름": "채 집 · 굴착기(타이어형)(0.6㎥)[hr] : 90%", +"출처": [] +}, +"1.나": { +"이름": "채 집 · 인 력 (10%)", +"출처": [] +}, +"1.다": { +"이름": "채 집 · 소 계", +"출처": [] +}, +"2": { +"이름": "적 사", +"출처": [] +}, +"3": { +"이름": "운 반", +"출처": [] +}, +"5": { +"이름": "합 계", +"출처": [] +} +}, +"이름조건": {}, +"정답": [ +"GF000211" +], +"허용": [], +"무리": "사람", +"복합": null +}, +{ +"id": "S082", +"파일": "북부청사방", +"이름": "구조물터파기(암)", +"규격": "", +"단위": "㎥", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"3-2-4" +], +[ +"건설품셈", +"8-2-3" +], +[ +"건설품셈", +"1-2-3" +] +], +"직종근거": [], +"인력": {}, +"인력절": [], +"장비": [ +{ +"갈래": [ +"굴착기", +"브레이커" +], +"규격": [], +"시간": 0.3333333333333333, +"밑시간": 0.3333333333333333, +"율": [], +"절": "1.가", +"글": "암절취 : 굴착기(무한궤도)(1.0+브레이카)" +}, +{ +"갈래": [ +"굴착기" +], +"규격": [ +1.0 +], +"시간": 0.04079967360261118, +"밑시간": 0.04079967360261118, +"율": [], +"절": "2", +"글": "집 토 : 굴착기(무한궤도)(1.0㎥)" +} +], +"자재": [], +"율": [], +"계수": { +"q": 1.0, +"K": 0.55, +"Cm": 21.0 +}, +"조건": { +"토질": [ +"경암", +"보통암", +"암", +"연암", +"풍화암" +], +"난이": [ +"보통" +], +"type": [ +"Ⅱ" +] +}, +"옛판": false, +"글": [ +"10.구조물터파기(암)[㎥]", +" * 구조물터파기 - 풍화암+연암+보통암+경암 [2025년 표준품셈3-2-4]", +" * TypeⅡ(연속작업 곤란) 반영", +" 1일 시공량 : Qd = (35+28+19+14)÷4 = 24㎥/일 (풍화암+연암+보통암+경암 기준)", +" Q = Qd ÷ 8hr = 24 ÷ 8 = 3㎥/hr", +" 1. 기계경비(굴착기 1.0㎥+대형 브레이커)", +" 가. 암절취 : 굴착기(무한궤도)(1.0+브레이카)", +" 노무비 : 59,020 ÷ 3 = 19,673.3", +" 재료비 : 31,125 ÷ 3 = 10,375", +" 경 비 : 48,560 ÷ 3 = 16,186.6", +" 소 계 : 46,234.9", +" 나. 치즐 손료(기계경비의 (2+5+7)/2%)", +" 소모자재", +" (19,673.3 + 10,375 + 16,186.6) × 4.667% = 2,157.7", +" 다. 소 계", +" 노무비 : 19,673.3", +" 재료비 : 10,375", +" 경 비 : 16,186.6 + 2,157.7 = 18,344.3", +" 계 : 48,392.6", +" 2. 집 토 : 굴착기(무한궤도)(1.0㎥)", +" [2025년 표준품셈8-2-3] 파쇄암", +" * 체적환산계수 [2025년 표준품셈1-2-3] 풍화암+연암+보통암+경암 반영", +" q = 1 E = (0.45 + 0.35) / 2 = 0.4 K = 0.55", +" f = 1 / ((1.325+1.4+1.625+1.85) / 4) = 0.65", +" Cm = 21sec (135˚)", +" 3600×q×K×f×E 3,600×1×0.55×0.65×0.4", +" Q = ────────── = ────────────── = 24.51㎥/hr", +" Cm 21", +" 노무비 : 59,020 ÷ 24.51 = 2,407.9", +" 재료비 : 32,735 ÷ 24.51 = 1,335.5", +" 경 비 : 29,621 ÷ 24.51 = 1,208.5", +" 소 계 : 4,951.9", +" 3. 합 계", +" 노무비 : 19,673.3 + 2,407.9 = 22,081", +" 재료비 : 10,375 + 1,335.5 = 11,710", +" 경 비 : 18,344.3 + 1,208.5 = 19,552", +" 계 : 53,343" +], +"절": { +"1": { +"이름": "기계경비(굴착기 1.0㎥+대형 브레이커)", +"출처": [] +}, +"1.가": { +"이름": "기계경비(굴착기 1.0㎥+대형 브레이커) · 암절취 : 굴착기(무한궤도)(1.0+브레이카)", +"출처": [] +}, +"1.나": { +"이름": "기계경비(굴착기 1.0㎥+대형 브레이커) · 치즐 손료(기계경비의 (2+5+7)/2%)", +"출처": [] +}, +"1.다": { +"이름": "기계경비(굴착기 1.0㎥+대형 브레이커) · 소 계", +"출처": [] +}, +"2": { +"이름": "집 토 : 굴착기(무한궤도)(1.0㎥)", +"출처": [] +}, +"3": { +"이름": "합 계", +"출처": [] +} +}, +"이름조건": { +"토질": [ +"암" +] +}, +"정답": [ +"GC000090" +], +"허용": [], +"무리": "사람", +"복합": null +}, +{ +"id": "S083", +"파일": "북부청사방", +"이름": "배수관부설", +"규격": "(D=500mm)", +"단위": "M", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"6-7-2" +] +], +"직종근거": [ +"보통인부" +], +"인력": { +"배관공(수도)": { +"품": 0.021667, +"밑품": 0.021667, +"율": [], +"어떻게": [ +"식" +] +}, +"보통인부": { +"품": 0.011667, +"밑품": 0.011667, +"율": [], +"어떻게": [ +"식" +] +} +}, +"인력절": [ +[ +"2", +"배관공(수도)", +0.021667, +0.021667 +], +[ +"2", +"보통인부", +0.011667, +0.011667 +] +], +"장비": [ +{ +"갈래": [ +"크레인" +], +"규격": [ +5.0 +], +"시간": 0.03, +"밑시간": 0.03, +"율": [], +"절": "4", +"글": "트럭탑재형크레인(5Ton)" +} +], +"자재": [], +"율": [ +[ +"인력품", +2.0 +] +], +"계수": {}, +"조건": {}, +"옛판": false, +"글": [ +"27.배수관부설(D=500mm)[M]", +" * 배수관부설 [건설표준품셈(6-7-2)]", +" 파형강관, D = 500mm", +" 1. 재료비(별도계상)", +" 파형강관(D=500mm) : 1m", +" 2. 인건비", +" 배관공(수도) : 262,580 × 0.13인 ÷ 6 = 5,689.2", +" 보 통 인 부 : 172,068 × 0.07인 ÷ 6 = 2,007.4", +" 인부소계 : 7,696.6", +" 3. 공구손료 및 잡재료비(인력품의 2%)", +" 7,696.6 × 2% = 153.9", +" 4. 트럭탑재형크레인(5Ton)", +" Q = 0.18hr / 6 = 0.03", +" 노무비 : 49,778 × 0.03 = 1,493.3", +" 재료비 : 8,421 × 0.03 = 252.6", +" 경 비 : 11,117 × 0.03 = 333.5", +" 소 계 : 2,079.4", +" 5. 합 계", +" 노무비 : 7,696.6 + 1,493.3 = 9,189", +" 재료비 : 252", +" 경 비 : 153.9 + 333.5 = 487", +" 계 : 9,928" +], +"절": { +"1": { +"이름": "재료비(별도계상)", +"출처": [] +}, +"2": { +"이름": "인건비", +"출처": [] +}, +"3": { +"이름": "공구손료 및 잡재료비(인력품의 2%)", +"출처": [] +}, +"4": { +"이름": "트럭탑재형크레인(5Ton)", +"출처": [] +}, +"5": { +"이름": "합 계", +"출처": [] +} +}, +"이름조건": {}, +"정답": [ +"GC000404" +], +"허용": [ +"GF000176", +"GF000179" +], +"무리": "사람", +"복합": null +}, +{ +"id": "S084", +"파일": "북부청사방", +"이름": "레미콘타설(무근펌프카)(130㎥)", +"규격": "41m(80-95㎥/hr)", +"단위": "㎥", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"6-1-4" +] +], +"직종근거": [ +"콘크리트공", +"특별인부", +"보통인부" +], +"인력": { +"콘크리트공": { +"품": 0.0231, +"밑품": 0.0231, +"율": [], +"어떻게": [ +"식" +] +}, +"특별인부": { +"품": 0.0154, +"밑품": 0.0154, +"율": [], +"어떻게": [ +"식" +] +}, +"보통인부": { +"품": 0.0077, +"밑품": 0.0077, +"율": [], +"어떻게": [ +"식" +] +} +}, +"인력절": [ +[ +"3", +"콘크리트공", +0.0231, +0.0231 +], +[ +"3", +"특별인부", +0.0154, +0.0154 +], +[ +"3", +"보통인부", +0.0077, +0.0077 +] +], +"장비": [ +{ +"갈래": [ +"콘크리트펌프" +], +"규격": [ +41.0, +95.0 +], +"시간": 0.06153846153846154, +"밑시간": 0.06153846153846154, +"율": [], +"절": "2", +"글": "콘크리트펌프차(41m,80~95㎥/hr)" +} +], +"자재": [], +"율": [ +[ +"인력품", +5.0 +] +], +"계수": { +"q1": 130.0, +"f1": 1.0, +"f2": 1.0 +}, +"조건": { +"근": [ +"무근" +], +"방법": [ +"펌프" +], +"횟수": [ +"1회" +] +}, +"옛판": false, +"글": [ +"18.레미콘타설(무근펌프카)(130㎥)(41m(80-95㎥/hr))[㎥]", +" <2024년건설표준품셈 공통6-1-4>", +" 1. 레미콘 : 1㎥ - 별 산 -", +" 2. 콘크리트펌프차(41m,80~95㎥/hr)", +" q1 = 130㎥ : 1회 타설량", +" f1 = 1 : TYPE-2", +" f2 = 1 : TYPE-2", +" a1 = q1 × f1 × f2 = 130 × 1 × 1 = 130 : {1회 타설량}", +" Q = a1 / 8hr = 130 / 8 = 16.25㎥/hr : {시간당 시공량}", +" 노무비 : 59,020 ÷ 16.25 = 3,632", +" 재료비 : 43,282 ÷ 16.25 = 2,663.5", +" 경 비 : 93,192 ÷ 16.25 = 5,734.8", +" 소 계 : 12,030.3", +" 3. 편성인력", +" 콘크리트공 : 273,540 × 0.0231인 = 6,318.7", +" 특 별 인부 : 226,122 × 0.0154인 = 3,482.2", +" 보 통 인부 : 172,068 × 0.0077인 = 1,324.9", +" 인부소계 : 11,125.8", +" 4. 공구손료 및 경장비(콘크리트 진동기등)의 기계경비와 잡재료비(인력품의 5%)", +" 11,125.8 × 5% = 556.2", +" 5. 합 계", +" 노무비 : 3,632 + 11,125.8 = 14,757", +" 재료비 : 2,663.5 + 556.2 = 3,219", +" 경 비 : 5,734", +" 계 : 23,710" +], +"절": { +"1": { +"이름": "레미콘 : 1㎥ - 별 산 -", +"출처": [] +}, +"2": { +"이름": "콘크리트펌프차(41m,80~95㎥/hr)", +"출처": [] +}, +"3": { +"이름": "편성인력", +"출처": [] +}, +"4": { +"이름": "공구손료 및 경장비(콘크리트 진동기등)의 기계경비와 잡재료비(인력품의 5%)", +"출처": [] +}, +"5": { +"이름": "합 계", +"출처": [] +} +}, +"이름조건": { +"근": [ +"무근" +], +"방법": [ +"펌프" +] +}, +"정답": [ +"GC000201" +], +"허용": [ +"GF000182" +], +"무리": "사람", +"복합": null +}, +{ +"id": "S085", +"파일": "북부청사방", +"이름": "콘크리트 펌프차", +"규격": "41m,80~95㎥/hr", +"단위": "hr", +"갈래": "기계", +"출처": [], +"직종근거": [], +"인력": {}, +"인력절": [], +"장비": [ +{ +"갈래": [ +"콘크리트펌프" +], +"규격": [ +41.0, +95.0 +], +"시간": 1.0, +"밑시간": null, +"절": 0, +"글": "콘크리트 펌프차" +} +], +"자재": [], +"율": [], +"계수": {}, +"조건": { +"방법": [ +"펌프" +] +}, +"옛판": false, +"글": [], +"절": {}, +"이름조건": { +"방법": [ +"펌프" +] +}, +"정답": [ +"GC000268", +"GC000269", +"GC000275" +], +"허용": [], +"무리": "사람", +"복합": null +}, +{ +"id": "S086", +"파일": "북부청사방", +"이름": "문양거푸집", +"규격": "", +"단위": "㎡", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"6-3-4" +] +], +"직종근거": [ +"형틀목공", +"보통인부" +], +"인력": { +"형틀목공": { +"품": 0.07, +"밑품": 0.07, +"율": [], +"어떻게": [ +"식" +] +}, +"보통인부": { +"품": 0.03, +"밑품": 0.03, +"율": [], +"어떻게": [ +"식" +] +} +}, +"인력절": [ +[ +"2", +"형틀목공", +0.07, +0.07 +], +[ +"2", +"보통인부", +0.03, +0.03 +] +], +"장비": [], +"자재": [ +{ +"이름": "문양거푸집(ABS코팅품)(600×1200)[㎡]", +"품": 1.0, +"밑품": 1.0 +} +], +"율": [ +[ +"주재료", +2.0 +] +], +"계수": {}, +"조건": {}, +"옛판": false, +"글": [ +"25.문양거푸집[㎡]", +" * 문양거푸집 [2026년 건설표준품셈6-3-4]", +" 1. 재료비", +" 문양거푸집(ABS코팅품)(600×1200)[㎡] 4,166 × 1㎡ = 4,166", +" 2. 노무비", +" 형틀목공 : 275,790 × 0.07인 = 19,305.3", +" 보통인부 : 172,068 × 0.03인 = 5,162", +" 인부소계 : 24,467.3", +" 3. 잡재료 및 소모재료(주재료의 2%)", +" (4,166) × 2% = 83.3", +" 4. 합 계", +" 노무비 : 24,467", +" 재료비 : 4,166 + 83.3 = 4,249", +" 경 비 :", +" 계 : 28,716" +], +"절": { +"1": { +"이름": "재료비", +"출처": [] +}, +"2": { +"이름": "노무비", +"출처": [] +}, +"3": { +"이름": "잡재료 및 소모재료(주재료의 2%)", +"출처": [] +}, +"4": { +"이름": "합 계", +"출처": [] +} +}, +"이름조건": {}, +"정답": [ +"GC000215" +], +"허용": [ +"GF000161" +], +"무리": "사람", +"복합": null +}, +{ +"id": "S087", +"파일": "북부청사방", +"이름": "지수판 설치", +"규격": "산림사업품셈", +"단위": "M", +"갈래": "단가산출", +"출처": [ +[ +"산림품셈", +"12-27-1" +] +], +"직종근거": [ +"특별인부", +"보통인부" +], +"인력": { +"특별인부": { +"품": 0.151, +"밑품": 0.151, +"율": [], +"어떻게": [ +"식" +] +}, +"보통인부": { +"품": 0.116, +"밑품": 0.116, +"율": [], +"어떻게": [ +"식" +] +} +}, +"인력절": [ +[ +"2", +"특별인부", +0.151, +0.151 +], +[ +"2", +"보통인부", +0.116, +0.116 +] +], +"장비": [], +"자재": [ +{ +"이름": "지수판(T5×200)[M]", +"품": 1.04, +"밑품": 1.04 +}, +{ +"이름": "용접봉(연강용)(3.2m/m)", +"품": 0.042, +"밑품": 0.042 +}, +{ +"이름": "어닐링철선(결속선)(#64.0mm10.1m/kg)", +"품": 0.21, +"밑품": 0.21 +} +], +"율": [ +[ +"인건비", +3.0 +] +], +"계수": {}, +"조건": {}, +"옛판": false, +"글": [ +"46.지수판 설치(산림사업품셈)[M]", +" * 지수판 설치 [2026년 산림사업표준품셈 12-27-1]", +" 1. 재료비 (할증4%)", +" 지수판(T5×200)[M] 4,500 × 1.04m = 4,680", +" 용접봉(연강용)(3.2m/m) 4,100 × 0.042kg = 172.2", +" 어닐링철선(결속선)(#64.0mm10.1m/kg) 1,560 × 0.21kg = 327.6", +" 자재소계 : 5,179.8", +" 2. 인건비", +" 특별인부 : 226,122 × 0.151 = 34,144.4", +" 보통인부 : 172,068 × 0.116 = 19,959.8", +" 인부소계 : 54,104.2", +" 3. 공구손료(인건비의 3%)", +" 54,104.2 × 3% = 1,623.1", +" 4. 합 계", +" 노무비 : 54,104", +" 재료비 : 5,179", +" 경 비 : 1,623", +" 계 : 60,906" +], +"절": { +"1": { +"이름": "재료비 (할증4%)", +"출처": [] +}, +"2": { +"이름": "인건비", +"출처": [] +}, +"3": { +"이름": "공구손료(인건비의 3%)", +"출처": [] +}, +"4": { +"이름": "합 계", +"출처": [] +} +}, +"이름조건": {}, +"정답": [ +"GF000196" +], +"허용": [], +"무리": "사람", +"복합": null +}, +{ +"id": "S088", +"파일": "북부청사방", +"이름": "P.V.C 파이프 설치", +"규격": "산림사업품셈", +"단위": "M", +"갈래": "단가산출", +"출처": [ +[ +"산림품셈", +"12-22" +] +], +"직종근거": [], +"인력": {}, +"인력절": [], +"장비": [], +"자재": [ +{ +"이름": "PVCPIPE(Φ50M/M,VG1)", +"품": 1.0, +"밑품": 1.0 +} +], +"율": [ +[ +"재료비", +5.0 +] +], +"계수": {}, +"조건": {}, +"옛판": false, +"글": [ +"1.P.V.C 파이프 설치(산림사업품셈)[M]", +" * P.V.C 파이프 설치 [2026년 산림사업표준품셈 12-22]", +" 1. 재료비", +" PVCPIPE(Φ50M/M,VG1) 4,290 × 1m = 4,290", +" 2. 설치비(재료비의 5%)", +" 4,290 × 5% = 214.5", +" 3. 합 계", +" 노무비 :", +" 재료비 : 4,290", +" 경 비 : 214", +" 계 : 4,504" +], +"절": { +"1": { +"이름": "재료비", +"출처": [] +}, +"2": { +"이름": "설치비(재료비의 5%)", +"출처": [] +}, +"3": { +"이름": "합 계", +"출처": [] +} +}, +"이름조건": {}, +"정답": [ +"GF000192" +], +"허용": [], +"무리": "사람", +"복합": null +}, +{ +"id": "S089", +"파일": "북부청사방", +"이름": "떼붙임(평떼)", +"규격": "산림사업품셈", +"단위": "㎡", +"갈래": "단가산출", +"출처": [ +[ +"산림품셈", +"5-12" +] +], +"직종근거": [ +"보통인부" +], +"인력": { +"보통인부": { +"품": 0.0732, +"밑품": 0.0732, +"율": [], +"어떻게": [ +"식", +"식" +] +} +}, +"인력절": [ +[ +"2", +"보통인부", +0.06, +0.06 +], +[ +"3", +"보통인부", +0.0132, +0.0132 +] +], +"장비": [], +"자재": [], +"율": [], +"계수": { +"L": 20.0 +}, +"조건": { +"난이": [ +"보통" +] +}, +"옛판": false, +"글": [ +"17.떼붙임(평떼)(산림사업품셈)[㎡]", +" 1. 떼 값 : 현장도착도", +" 별도계상", +" 2. 떼붙임 [2026년 산림사업표준품셈(5-12)]", +" 보통인부 : 172,068 × 0.06 = 10,324 \\/㎡", +" 3. 떼소운반", +" L = 20m 경사도 : 15° 운반회수 : 100회/일", +" 보통인부 : P", +" P = 1.0㎡ ×33kg/㎡ ×1회 / 25kg ×1일 / 100회 = 0.0132", +" 보통인부 : 172,068 × P = 172,068 × 0.0132 = 2,271.2", +" 4. 합 계", +" 노무비 : 10,324 + 2,271.2 = 12,595", +" 재료비 :", +" 경 비 :", +" 계 : 12,595" +], +"절": { +"1": { +"이름": "떼 값 : 현장도착도", +"출처": [] +}, +"2": { +"이름": "떼붙임 [2026년 산림사업표준품셈(5-12)]", +"출처": [ +[ +"산림품셈", +"5-12" +] +] +}, +"3": { +"이름": "떼소운반", +"출처": [] +}, +"4": { +"이름": "합 계", +"출처": [] +} +}, +"이름조건": {}, +"정답": [ +"GF000025" +], +"허용": [ +"GF000032" +], +"무리": "사람", +"복합": null +}, +{ +"id": "S090", +"파일": "북부청사방", +"이름": "큰돌메쌓기", +"규격": "40cm-60cm,(산림사업품셈)", +"단위": "㎡", +"갈래": "단가산출", +"출처": [ +[ +"산림품셈", +"13-6-1" +] +], +"직종근거": [ +"작업반장", +"특별인부", +"보통인부" +], +"인력": { +"작업반장": { +"품": 0.083, +"밑품": 0.083, +"율": [], +"어떻게": [ +"식" +] +}, +"특별인부": { +"품": 0.104, +"밑품": 0.104, +"율": [], +"어떻게": [ +"식" +] +}, +"보통인부": { +"품": 0.104, +"밑품": 0.104, +"율": [], +"어떻게": [ +"식" +] +} +}, +"인력절": [ +[ +"1.가", +"작업반장", +0.083, +0.083 +], +[ +"1.가", +"특별인부", +0.104, +0.104 +], +[ +"1.가", +"보통인부", +0.104, +0.104 +] +], +"장비": [ +{ +"갈래": [ +"굴착기", +"집게" +], +"규격": [ +0.8 +], +"시간": 0.384, +"밑시간": 0.384, +"율": [], +"절": "1.나", +"글": "굴착기(무한궤도)(0.8㎥+부착용집게)[hr]" +} +], +"자재": [], +"율": [ +[ +"품", +1.0 +] +], +"계수": { +"T": 0.384 +}, +"조건": { +"메찰": [ +"메쌓기" +] +}, +"옛판": false, +"글": [ +"60.큰돌메쌓기(40cm-60cm,(산림사업품셈))[㎡]", +" 1. 큰돌쌓기(메쌓기) (2026년 산림사업품셈 13-6-1)", +" 가. 노무비", +" 작업반장 : 215,907 × 0.83 ÷ 10 = 17,920.2", +" 특별인부 : 226,122 × 1.04 ÷ 10 = 23,516.6", +" 보통인부 : 172,068 × 1.04 ÷ 10 = 17,895", +" 인부소계 : 59,331.8", +" 나. 기계경비", +" 굴착기(무한궤도)(0.8㎥+부착용집게)[hr]", +" T = 0.384", +" 노무비 : 59,020 × T = 59,020 × 0.384 = 22,663.6", +" 재료비 : 24,421 × T = 24,421 × 0.384 = 9,377.6", +" 경 비 : 32,805 × T = 32,805 × 0.384 = 12,597.1", +" 소 계 : 44,638.3", +" 다. 제잡비(품의 1%)", +" (17,920.2 + 23,516.6 + 17,895 + 22,663.6) × 1% = 819.9", +" 라. 소 계", +" 노무비 : 59,331.8 + 22,663.6 + 819.9 = 82,815.3", +" 재료비 : 9,377.6", +" 경 비 : 12,597.1", +" 계 : 104,790", +" 2. 합 계", +" 노무비 : 82,815", +" 재료비 : 9,377", +" 경 비 : 12,597", +" 계 : 104,789" +], +"절": { +"1": { +"이름": "큰돌쌓기(메쌓기) (2026년 산림사업품셈 13-6-1)", +"출처": [ +[ +"산림품셈", +"13-6-1" +] +] +}, +"1.가": { +"이름": "큰돌쌓기(메쌓기) (2026년 산림사업품셈 13-6-1) · 노무비", +"출처": [ +[ +"산림품셈", +"13-6-1" +] +] +}, +"1.나": { +"이름": "큰돌쌓기(메쌓기) (2026년 산림사업품셈 13-6-1) · 기계경비", +"출처": [ +[ +"산림품셈", +"13-6-1" +] +] +}, +"1.다": { +"이름": "큰돌쌓기(메쌓기) (2026년 산림사업품셈 13-6-1) · 제잡비(품의 1%)", +"출처": [ +[ +"산림품셈", +"13-6-1" +] +] +}, +"1.라": { +"이름": "큰돌쌓기(메쌓기) (2026년 산림사업품셈 13-6-1) · 소 계", +"출처": [ +[ +"산림품셈", +"13-6-1" +] +] +}, +"2": { +"이름": "합 계", +"출처": [] +} +}, +"이름조건": { +"메찰": [ +"메쌓기" +] +}, +"정답": [ +"GF000226" +], +"허용": [ +"GC000260" +], +"무리": "사람", +"복합": null +}, +{ +"id": "S091", +"파일": "북부청사방", +"이름": "고임돌채집및운반", +"규격": "(9:1)(0.6㎥+4.5Ton)", +"단위": "㎥", +"갈래": "단가산출", +"출처": [], +"직종근거": [ +"보통인부" +], +"인력": { +"보통인부": { +"품": 0.03, +"밑품": 0.3, +"율": [ +[ +0.1, +"섞음" +] +], +"어떻게": [ +"식" +] +} +}, +"인력절": [ +[ +"1.나", +"보통인부", +0.03, +0.3 +] +], +"장비": [ +{ +"갈래": [ +"굴착기" +], +"규격": [ +0.6 +], +"시간": 0.043774319066147864, +"밑시간": 0.048638132295719845, +"율": [ +[ +0.9, +"섞음" +] +], +"절": "1.가", +"글": "굴착기(타이어형)(0.6㎥)[hr] : 90%" +}, +{ +"갈래": [ +"굴착기" +], +"규격": [ +0.6 +], +"시간": 0.03302509907529722, +"밑시간": 0.03302509907529722, +"율": [], +"절": "2", +"글": "굴착기(타이어형)(0.6㎥)[hr]" +}, +{ +"갈래": [ +"덤프트럭" +], +"규격": [ +4.5 +], +"시간": 0.08368200836820085, +"밑시간": 0.08368200836820085, +"율": [], +"절": "3", +"글": "덤프트럭(4.5Ton)" +} +], +"자재": [], +"율": [], +"계수": { +"q": 0.6, +"k": 0.55, +"Cm": 18.0, +"E": 0.35, +"L": 0.5, +"t3": 1.1, +"t4": 0.42 +}, +"조건": { +"방법": [ +"기계", +"인력" +] +}, +"옛판": false, +"글": [ +"5.고임돌채집및운반((9:1)(0.6㎥+4.5Ton))[㎥]", +" 1. 채 집 (기계 : 인력 = 9 : 1)", +" 가. 굴착기(타이어형)(0.6㎥)[hr] : 90%", +" q = 0.6 f = 1 / 1.125 = 0.89 k = 0.55", +" Cm = 18sec(90˚) E = 0.35", +" 3600×q×k×f×E 3,600×0.6×0.55×0.89×0.35", +" Q = ────────── = ─────────────── = 20.56㎥/hr", +" Cm 18", +" 노무비 : 59,020 ÷ 20.56 × 0.9 = 2,583.5", +" 재료비 : 19,792 ÷ 20.56 × 0.9 = 866.3", +" 경 비 : 27,120 ÷ 20.56 × 0.9 = 1,187.1", +" 소 계 : 4,636.9", +" 나. 인 력 : 10%", +" 보통인부 : 172,068 × 0.3 × 0.1 = 5,162", +" 다. 소 계", +" 노무비 : 2,583.5 + 5,162 = 7,745.5", +" 재료비 : 866.3", +" 경 비 : 1,187.1", +" 계 : 9,798.9", +" 2. 적 사", +" 굴착기(타이어형)(0.6㎥)[hr]", +" q = 0.6 f = 1 / 1.125 = 0.89 k = 0.7", +" Cm = 20sec(135˚) E = 0.45", +" 3600×q×k×f×E 3,600×0.6×0.7×0.89×0.45", +" Q = ────────── = ────────────── = 30.28㎥/hr", +" Cm 20", +" 노무비 : 59,020 ÷ 30.28 = 1,949.1", +" 재료비 : 19,792 ÷ 30.28 = 653.6", +" 경 비 : 27,120 ÷ 30.28 = 895.6", +" 소 계 : 3,498.3", +" 3. 운 반", +" 덤프트럭(4.5Ton)", +" L = 0.5km", +" E = 0.9 f = 1 / 1.125 = 0.89", +" q1 = (4.5 / 1.75) × 1.125 = 2.89", +" n = q1 / (0.6 × k) = 2.89 / (0.6 × 0.7) = 6.88회", +" t1 = 20 × n / (60 × 0.45) = 20 × 6.88 / (60 × 0.45) = 5.1분", +" t2 = (L / 10 + L / 15) × 60 = (0.5 / 10 + 0.5 / 15) × 60 = 5분", +" t3 = 1.1분 t4 = 0.42분", +" Cm = t1 + t2 + t3 + t4 = 5.1 + 5 + 1.1 + 0.42 = 11.62분", +" 60×q1×f×E 60×2.89×0.89×0.9", +" Q = ──────── = ────────── = 11.95㎥/hr", +" Cm 11.62", +" 노무비 : 49,778 ÷ 11.95 = 4,165.5", +" 재료비 : 9,494 ÷ 11.95 = 794.4", +" 경 비 : 7,613 ÷ 11.95 = 637", +" 소 계 : 5,596.9", +" 4. 합 계", +" 노무비 : 7,745.5 + 1,949.1 + 4,165.5 = 13,860", +" 재료비 : 866.3 + 653.6 + 794.4 = 2,314", +" 경 비 : 1,187.1 + 895.6 + 637 = 2,719", +" 계 : 18,893" +], +"절": { +"1": { +"이름": "채 집 (기계 : 인력 = 9 : 1)", +"출처": [] +}, +"1.가": { +"이름": "채 집 (기계 : 인력 = 9 : 1) · 굴착기(타이어형)(0.6㎥)[hr] : 90%", +"출처": [] +}, +"1.나": { +"이름": "채 집 (기계 : 인력 = 9 : 1) · 인 력 : 10%", +"출처": [] +}, +"1.다": { +"이름": "채 집 (기계 : 인력 = 9 : 1) · 소 계", +"출처": [] +}, +"2": { +"이름": "적 사", +"출처": [] +}, +"3": { +"이름": "운 반", +"출처": [] +}, +"4": { +"이름": "합 계", +"출처": [] +} +}, +"이름조건": {}, +"정답": [ +"GF000211" +], +"허용": [ +"GF000212" +], +"무리": "사람", +"복합": null +}, +{ +"id": "S092", +"파일": "북부청사방", +"이름": "뒷채움잡석채집운반다짐", +"규격": "(9:1)(0.6㎥+4.5Ton)", +"단위": "㎥", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"3-4-4" +] +], +"직종근거": [ +"보통인부", +"특별인부" +], +"인력": { +"보통인부": { +"품": 0.039, +"밑품": 0.309, +"율": [ +[ +0.1, +"섞음" +] +], +"어떻게": [ +"식", +"식" +] +}, +"특별인부": { +"품": 0.009, +"밑품": 0.009, +"율": [], +"어떻게": [ +"식" +] +} +}, +"인력절": [ +[ +"1.나", +"보통인부", +0.03, +0.3 +], +[ +"4.다", +"특별인부", +0.009, +0.009 +], +[ +"4.다", +"보통인부", +0.009, +0.009 +] +], +"장비": [ +{ +"갈래": [ +"굴착기" +], +"규격": [ +0.6 +], +"시간": 0.043774319066147864, +"밑시간": 0.048638132295719845, +"율": [ +[ +0.9, +"섞음" +] +], +"절": "1.가", +"글": "굴착기(타이어형)(0.6㎥)[hr] (90%)" +}, +{ +"갈래": [ +"굴착기" +], +"규격": [ +0.6 +], +"시간": 0.03302509907529722, +"밑시간": 0.03302509907529722, +"율": [], +"절": "2", +"글": "굴착기(타이어형)(0.6㎥)[hr]" +}, +{ +"갈래": [ +"덤프트럭" +], +"규격": [ +4.5 +], +"시간": 0.08368200836820085, +"밑시간": 0.08368200836820085, +"율": [], +"절": "3", +"글": "덤프트럭(4.5Ton)" +}, +{ +"갈래": [ +"굴착기" +], +"규격": [ +0.2 +], +"시간": 0.07272727272727272, +"밑시간": 0.07272727272727272, +"율": [], +"절": "4.가", +"글": "굴착기(무한궤도)(0.2㎥)[hr]" +}, +{ +"갈래": [ +"롤러" +], +"규격": [ +0.7 +], +"시간": 0.07272727272727272, +"밑시간": 0.07272727272727272, +"율": [], +"절": "4.나", +"글": "진동로울러(핸드가이드식)(0.7Ton)" +} +], +"자재": [], +"율": [], +"계수": { +"q": 0.6, +"k": 0.55, +"Cm": 18.0, +"E": 0.35, +"L": 0.5, +"t3": 1.1, +"t4": 0.42 +}, +"조건": { +"방법": [ +"기계", +"인력" +] +}, +"옛판": false, +"글": [ +"16.뒷채움잡석채집운반다짐((9:1)(0.6㎥+4.5Ton))[㎥]", +" * 구조물뒷채움잡석(막자갈) 기계 90%, 인력 10%", +" 1. 채 집", +" 가. 굴착기(타이어형)(0.6㎥)[hr] (90%)", +" q = 0.6 f = 1 / 1.125 = 0.89 k = 0.55", +" Cm = 18sec(90˚) E = 0.35", +" 3600×q×k×f×E 3,600×0.6×0.55×0.89×0.35", +" Q = ────────── = ─────────────── = 20.56㎥/hr", +" Cm 18", +" 노무비 : 59,020 ÷ 20.56 × 0.9 = 2,583.5", +" 재료비 : 19,792 ÷ 20.56 × 0.9 = 866.3", +" 경 비 : 27,120 ÷ 20.56 × 0.9 = 1,187.1", +" 소 계 : 4,636.9", +" 나. 인 력 (10%)", +" 보통인부 : 172,068 × 0.3 × 0.1 = 5,162", +" 다. 소 계", +" 노무비 : 2,583.5 + 5,162 = 7,745.5", +" 재료비 : 866.3", +" 경 비 : 1,187.1", +" 계 : 9,798.9", +" 2. 적 사", +" 굴착기(타이어형)(0.6㎥)[hr]", +" q = 0.6 f = 1 / 1.125 = 0.89 k = 0.7", +" Cm = 20sec(135˚) E = 0.45", +" 3600×q×k×f×E 3,600×0.6×0.7×0.89×0.45", +" Q = ────────── = ────────────── = 30.28㎥/hr", +" Cm 20", +" 노무비 : 59,020 ÷ 30.28 = 1,949.1", +" 재료비 : 19,792 ÷ 30.28 = 653.6", +" 경 비 : 27,120 ÷ 30.28 = 895.6", +" 소 계 : 3,498.3", +" 3. 운 반", +" 덤프트럭(4.5Ton)", +" L = 0.5km", +" E = 0.9 f = 1 / 1.125 = 0.89", +" q1 = (4.5 / 1.75) × 1.125 = 2.89", +" n = q1 / (0.6 × k) = 2.89 / (0.6 × 0.7) = 6.88회", +" t1 = 20 × n / (60 × 0.45) = 20 × 6.88 / (60 × 0.45) = 5.1분", +" L L 0.5 0.5", +" t2 = (──+──)×60 = (──+──)×60 = 5분", +" 10 15 10 15", +" t3 = 1.1분 t4 = 0.42분", +" Cm = t1 + t2 + t3 + t4 = 5.1 + 5 + 1.1 + 0.42 = 11.62분", +" 60×q1×f×E 60×2.89×0.89×0.9", +" Q1 = ──────── = ────────── = 11.95㎥/hr", +" Cm 11.62", +" 노무비 : 49,778 ÷ Q1 = 49,778 ÷ 11.95 = 4,165.5", +" 재료비 : 9,494 ÷ Q1 = 9,494 ÷ 11.95 = 794.4", +" 경 비 : 7,613 ÷ Q1 = 7,613 ÷ 11.95 = 637", +" 소 계 : 5,596.9", +" 4. 뒤채움 및 다짐 [2025년 건설표준품셈3-4-4(소형장비)]", +" 가. 굴착기(무한궤도)(0.2㎥)[hr]", +" 1일 시공량 : Qd = 110㎥/일", +" Q = Qd / 8 = 110 / 8 = 13.75㎥/hr", +" 노무비 : 59,020 ÷ 13.75 = 4,292.3", +" 재료비 : 8,324 ÷ 13.75 = 605.3", +" 경 비 : 13,873 ÷ 13.75 = 1,008.9", +" 소 계 : 5,906.5", +" 나. 진동로울러(핸드가이드식)(0.7Ton)", +" 재료비 : 3,420 ÷ 13.75 = 248.7" +], +"절": { +"1": { +"이름": "채 집", +"출처": [] +}, +"1.가": { +"이름": "채 집 · 굴착기(타이어형)(0.6㎥)[hr] (90%)", +"출처": [] +}, +"1.나": { +"이름": "채 집 · 인 력 (10%)", +"출처": [] +}, +"1.다": { +"이름": "채 집 · 소 계", +"출처": [] +}, +"2": { +"이름": "적 사", +"출처": [] +}, +"3": { +"이름": "운 반", +"출처": [] +}, +"4": { +"이름": "뒤채움 및 다짐 [2025년 건설표준품셈3-4-4(소형장비)]", +"출처": [ +[ +"건설품셈", +"3-4-4" +] +] +}, +"4.가": { +"이름": "뒤채움 및 다짐 [2025년 건설표준품셈3-4-4(소형장비)] · 굴착기(무한궤도)(0.2㎥)[hr]", +"출처": [ +[ +"건설품셈", +"3-4-4" +] +] +}, +"4.나": { +"이름": "뒤채움 및 다짐 [2025년 건설표준품셈3-4-4(소형장비)] · 진동로울러(핸드가이드식)(0.7Ton)", +"출처": [ +[ +"건설품셈", +"3-4-4" +] +] +}, +"4.다": { +"이름": "뒤채움 및 다짐 [2025년 건설표준품셈3-4-4(소형장비)] · 인 력", +"출처": [ +[ +"건설품셈", +"3-4-4" +] +] +}, +"4.라": { +"이름": "뒤채움 및 다짐 [2025년 건설표준품셈3-4-4(소형장비)] · 소 계", +"출처": [ +[ +"건설품셈", +"3-4-4" +] +] +}, +"5": { +"이름": "합 계", +"출처": [] +} +}, +"이름조건": {}, +"정답": [ +"GC000106" +], +"허용": [ +"GF000218" +], +"무리": "사람", +"복합": null +}, +{ +"id": "S093", +"파일": "북부청사방", +"이름": "큰돌찰쌓기", +"규격": "40cm-60cm,(산림사업품셈)", +"단위": "㎡", +"갈래": "단가산출", +"출처": [ +[ +"산림품셈", +"13-6-2" +] +], +"직종근거": [ +"작업반장", +"특별인부", +"보통인부" +], +"인력": { +"작업반장": { +"품": 0.083, +"밑품": 0.083, +"율": [], +"어떻게": [ +"식" +] +}, +"특별인부": { +"품": 0.13, +"밑품": 0.13, +"율": [], +"어떻게": [ +"식" +] +}, +"보통인부": { +"품": 0.13, +"밑품": 0.13, +"율": [], +"어떻게": [ +"식" +] +} +}, +"인력절": [ +[ +"1.가", +"작업반장", +0.083, +0.083 +], +[ +"1.가", +"특별인부", +0.13, +0.13 +], +[ +"1.가", +"보통인부", +0.13, +0.13 +] +], +"장비": [ +{ +"갈래": [ +"굴착기", +"집게" +], +"규격": [ +0.8 +], +"시간": 0.48, +"밑시간": 0.48, +"율": [], +"절": "1.나", +"글": "굴착기(무한궤도)(0.8㎥+부착용집게)[hr]" +} +], +"자재": [], +"율": [ +[ +"노무비", +9.0 +] +], +"계수": { +"T": 0.48 +}, +"조건": { +"메찰": [ +"찰쌓기" +] +}, +"옛판": false, +"글": [ +"62.큰돌찰쌓기(40cm-60cm,(산림사업품셈))[㎡]", +" 1. 큰돌쌓기(찰쌓기) (2026년 산림사업 품셈 13-6-2)", +" 가. 노무비", +" 작업반장 : 215,907 × 0.83 ÷ 10 = 17,920.2", +" 특별인부 : 226,122 × 1.30 ÷ 10 = 29,395.8", +" 보통인부 : 172,068 × 1.30 ÷ 10 = 22,368.8", +" 인부소계 : 69,684.8", +" 나. 기계경비", +" 굴착기(무한궤도)(0.8㎥+부착용집게)[hr]", +" T = 0.48", +" 노무비 : 59,020 × T = 59,020 × 0.48 = 28,329.6", +" 재료비 : 24,421 × T = 24,421 × 0.48 = 11,722", +" 경 비 : 32,805 × T = 32,805 × 0.48 = 15,746.4", +" 소 계 : 55,798", +" 다. 제잡비(노무비의 9%)", +" (17,920.2 + 29,395.8 + 22,368.8 + 28,329.6) × 9% = 8,821.2", +" 라. 소 계", +" 노무비 : 69,684.8 + 28,329.6 + 8,821.2 = 106,835.6", +" 재료비 : 11,722", +" 경 비 : 15,746.4", +" 계 : 134,304", +" 2. 합 계", +" 노무비 : 106,835", +" 재료비 : 11,722", +" 경 비 : 15,746", +" 계 : 134,303" +], +"절": { +"1": { +"이름": "큰돌쌓기(찰쌓기) (2026년 산림사업 품셈 13-6-2)", +"출처": [ +[ +"산림품셈", +"13-6-2" +] +] +}, +"1.가": { +"이름": "큰돌쌓기(찰쌓기) (2026년 산림사업 품셈 13-6-2) · 노무비", +"출처": [ +[ +"산림품셈", +"13-6-2" +] +] +}, +"1.나": { +"이름": "큰돌쌓기(찰쌓기) (2026년 산림사업 품셈 13-6-2) · 기계경비", +"출처": [ +[ +"산림품셈", +"13-6-2" +] +] +}, +"1.다": { +"이름": "큰돌쌓기(찰쌓기) (2026년 산림사업 품셈 13-6-2) · 제잡비(노무비의 9%)", +"출처": [ +[ +"산림품셈", +"13-6-2" +] +] +}, +"1.라": { +"이름": "큰돌쌓기(찰쌓기) (2026년 산림사업 품셈 13-6-2) · 소 계", +"출처": [ +[ +"산림품셈", +"13-6-2" +] +] +}, +"2": { +"이름": "합 계", +"출처": [] +} +}, +"이름조건": { +"메찰": [ +"찰쌓기" +] +}, +"정답": [ +"GF000227" +], +"허용": [ +"GC000261" +], +"무리": "사람", +"복합": null +}, +{ +"id": "S094", +"파일": "북부청사방", +"이름": "큰돌채집운반", +"규격": "산림사업품셈", +"단위": "㎡", +"갈래": "단가산출", +"출처": [ +[ +"산림품셈", +"13-2-3" +] +], +"직종근거": [ +"작업반장" +], +"인력": { +"작업반장": { +"품": 0.0312, +"밑품": 0.0416, +"율": [ +[ +0.75, +"품할증" +] +], +"어떻게": [ +"식" +] +} +}, +"인력절": [ +[ +"1.가", +"작업반장", +0.0312, +0.0416 +] +], +"장비": [ +{ +"갈래": [ +"굴착기" +], +"규격": [ +0.6 +], +"시간": 0.16080000000000003, +"밑시간": 0.21440000000000003, +"율": [ +[ +0.75, +"품할증" +] +], +"절": "1.나", +"글": "굴착기(타이어형)(0.6㎥)[hr]" +}, +{ +"갈래": [ +"굴착기" +], +"규격": [ +0.6 +], +"시간": 0.16080000000000003, +"밑시간": 0.21440000000000003, +"율": [ +[ +0.75, +"품할증" +] +], +"절": "2.가", +"글": "굴착기(타이어형)(0.6㎥)[hr]" +}, +{ +"갈래": [ +"집게" +], +"규격": [ +0.8 +], +"시간": 0.16080000000000003, +"밑시간": 0.21440000000000003, +"율": [ +[ +0.75, +"품할증" +] +], +"절": "2.나", +"글": "부착용집게(0.7~0.8㎥)" +}, +{ +"갈래": [ +"덤프트럭" +], +"규격": [ +15.0 +], +"시간": 0.009737827715355807, +"밑시간": 0.009737827715355807, +"율": [], +"절": "4", +"글": "덤프트럭(15Ton(발파)" +} +], +"자재": [], +"율": [ +[ +"채집돌", +40.0 +], +[ +"노무비", +6.0 +] +], +"계수": { +"L": 0.5, +"k": 0.55, +"E": 0.9, +"t3": 1.1, +"t4": 0.42 +}, +"조건": {}, +"옛판": false, +"글": [ +"63.큰돌채집운반(산림사업품셈)[㎡]", +" ※ 큰돌뒷길이 70cm내외", +" [2026년 산림사업 품셈 13-2-3]", +" T = 1.0 × 0.675 × 0.77 (공극률제외) = 0.52㎥/㎡", +" 1. 큰돌채집 : 채집운반은 채집돌의 40% 적용", +" P = 4개/㎡", +" 가. 인 력", +" 작업반장 : 215,907×P×0.026인/개×0.75(품할증)×0.4 = 215,907×4×0.026×0.75×0.4 = 6,736.2", +" 나. 굴착기(타이어형)(0.6㎥)[hr]", +" 노무비 : 59,020×P×0.134×0.75(품할증)×0.4 = 59,020×4×0.134×0.75×0.4 = 9,490.4", +" 재료비 : 19,792×P×0.134×0.75(품할증)×0.4 = 19,792×4×0.134×0.75×0.4 = 3,182.5", +" 경 비 : 27,120×P×0.134×0.75(품할증)×0.4 = 27,120×4×0.134×0.75×0.4 = 4,360.8", +" 소 계 : 17,033.7", +" 다. 소 계", +" 노무비 : 6,736.2 + 9,490.4 = 16,226.6", +" 재료비 : 3,182.5", +" 경 비 : 4,360.8", +" 계 : 23,769.9", +" 2. 큰돌상차", +" 가. 굴착기(타이어형)(0.6㎥)[hr]", +" 노무비 : 59,020×P×0.134×0.75(품할증)×0.4 = 59,020×4×0.134×0.75×0.4 = 9,490.4", +" 재료비 : 19,792×P×0.134×0.75(품할증)×0.4 = 19,792×4×0.134×0.75×0.4 = 3,182.5", +" 경 비 : 27,120×P×0.134×0.75(품할증)×0.4 = 27,120×4×0.134×0.75×0.4 = 4,360.8", +" 소 계 : 17,033.7", +" 나. 부착용집게(0.7~0.8㎥)", +" 노무비 : 0×P×0.134×0.75(품할증)×0.4 = 0×4×0.134×0.75×0.4 = 0", +" 재료비 : 0×P×0.134×0.75(품할증)×0.4 = 0×4×0.134×0.75×0.4 = 0", +" 경 비 : 5,786×P×0.134×0.75(품할증)×0.4 = 5,786×4×0.134×0.75×0.4 = 930.3", +" 소 계 : 930.3", +" 다. 소 계", +" 노무비 : 9,490.4", +" 재료비 : 3,182.5", +" 경 비 : 4,360.8 + 930.3 = 5,291.1", +" 계 : 17,964", +" 3. 제잡비(노무비의 6%)", +" (6,736.2 + 9,490.4 + 9,490.4 + 0) × 6% = 1,543", +" 4. 운 반", +" 덤프트럭(15Ton(발파)", +" L = 0.5km k = 0.55 E = 0.9 F = 1", +" q1 = (15 / 2.65) × 1 = 5.66", +" n = q1 / (k × 0.8) = 5.66 / (0.55 × 0.8) = 12.86회", +" t1 = 20 × n / (60 × k) = 20 × 12.86 / (60 × 0.55) = 7.79분", +" L L 0.5 0.5", +" t2 = (──+──)×60 = (──+──)×60 = 5분", +" 10 15 10 15", +" t3 = 1.1분 t4 = 0.42분", +" Cm = t1 + t2 + t3 + t4 = 7.79 + 5 + 1.1 + 0.42 = 14.31", +" 60×q1×F×E 60×5.66×1×0.9", +" Q = ──────── = ───────── = 21.36 ㎥/hr", +" Cm 14.31", +" 노무비 : 59,020÷21.36×T×0.4 = 59,020÷21.36×0.52×0.4 = 574.7", +" 재료비 : 30,192÷21.36×((Cm-t1)÷Cm)×T×0.4= 30,192÷21.36×((14.31-7.79)÷14.31)×0.52×0.4= 133.9", +" 경 비 : 24,285÷21.36×T×0.4 = 24,285÷21.36×0.52×0.4 = 236.4", +" 소 계 : 945", +" 5. 합 계", +" 노무비 : 16,226.6 + 9,490.4 + 574.7 = 26,291", +" 재료비 : 3,182.5 + 3,182.5 + 133.9 = 6,498", +" 경 비 : 4,360.8 + 5,291.1 + 1,543 + 236.4 = 11,431", +" 계 : 44,220" +], +"절": { +"1": { +"이름": "큰돌채집 : 채집운반은 채집돌의 40% 적용", +"출처": [] +}, +"1.가": { +"이름": "큰돌채집 : 채집운반은 채집돌의 40% 적용 · 인 력", +"출처": [] +}, +"1.나": { +"이름": "큰돌채집 : 채집운반은 채집돌의 40% 적용 · 굴착기(타이어형)(0.6㎥)[hr]", +"출처": [] +}, +"1.다": { +"이름": "큰돌채집 : 채집운반은 채집돌의 40% 적용 · 소 계", +"출처": [] +}, +"2": { +"이름": "큰돌상차", +"출처": [] +}, +"2.가": { +"이름": "큰돌상차 · 굴착기(타이어형)(0.6㎥)[hr]", +"출처": [] +}, +"2.나": { +"이름": "큰돌상차 · 부착용집게(0.7~0.8㎥)", +"출처": [] +}, +"2.다": { +"이름": "큰돌상차 · 소 계", +"출처": [] +}, +"3": { +"이름": "제잡비(노무비의 6%)", +"출처": [] +}, +"4": { +"이름": "운 반", +"출처": [] +}, +"5": { +"이름": "합 계", +"출처": [] +} +}, +"이름조건": {}, +"정답": [ +"GF000213" +], +"허용": [], +"무리": "사람", +"복합": null +}, +{ +"id": "S095", +"파일": "북부청사방", +"이름": "뒷채움잡석구입운반", +"규격": "", +"단위": "㎥", +"갈래": "단가산출", +"출처": [], +"직종근거": [], +"인력": {}, +"인력절": [], +"장비": [ +{ +"갈래": [ +"덤프트럭" +], +"규격": [ +15.0 +], +"시간": 0.3389830508474576, +"밑시간": 0.3389830508474576, +"율": [], +"절": "2", +"글": "덤프트럭(15Ton)" +} +], +"자재": [], +"율": [], +"계수": { +"q": 0.8, +"K": 0.7, +"Cm": 20.0, +"E": 0.45, +"k": 0.55, +"t3": 1.1, +"t4": 0.42 +}, +"조건": {}, +"옛판": false, +"글": [ +"13.뒷채움잡석구입운반[㎥]", +" 1. 적 사 : 굴착기 0.8㎥", +" q = 0.8 f = 1 / 1.125 = 0.89 K = 0.7", +" Cm = 20sec E = 0.45", +" 3600×q×K×f×E 3,600×0.8×0.7×0.89×0.45", +" Q = ────────── = ────────────── = 40.37㎥/hr", +" Cm 20", +" 2. 운 반 - 홍천골재장", +" 덤프트럭(15Ton)", +" L1 = 18.7km L2 = 2.3km", +" V11 = 30km V12 = 35km V21 = 10km V22 = 15km", +" k = 0.55 E = 0.9 F = 1", +" q1 = (15 / 1.7) × 1.125 = 9.93", +" n = q1 / (0.8 × k) = 9.93 / (0.8 × 0.55) = 22.57 회", +" t1 = 20 × n / (60 × 0.45) = 20 × 22.57 / (60 × 0.45) = 16.72 분", +" L1 L2 L1 L2 18.7 2.3 18.7 2.3", +" t2 = (──+──+──+──)×60 = (───+──+───+──)×60 =162.74분", +" V11 V12 V21 V22 30 35 10 15", +" t3 = 1.1분 t4 = 0.42분 t5 = 0.5분", +" Cm = t1 + t2 + t3 + t4 + t5 = 16.72 + 162.74 + 1.1 + 0.42 + 0.5 = 181.48", +" 60×q1×F×E 60×9.93×1×0.9", +" Q1 = ──────── = ───────── = 2.95㎥/hr", +" Cm 181.48", +" 노무비 : 59,020 ÷ Q1 = 59,020 ÷ 2.95 = 20,006.7", +" 재료비 : 30,192÷Q1×(Cm-t1)÷Cm = 30,192÷2.95×(181.48-16.72)÷181.48 = 9,291.6", +" 경 비 : 20,659 ÷ Q1 = 20,659 ÷ 2.95 = 7,003", +" 소 계 : 36,301.3", +" 3. 합 계", +" 노무비 : 20,006", +" 재료비 : 9,291", +" 경 비 : 7,003", +" 계 : 36,300" +], +"절": { +"1": { +"이름": "적 사 : 굴착기 0.8㎥", +"출처": [] +}, +"2": { +"이름": "운 반 - 홍천골재장", +"출처": [] +}, +"3": { +"이름": "합 계", +"출처": [] +} +}, +"이름조건": {}, +"정답": [ +"GF000135", +"GF000284" +], +"허용": [], +"무리": "사람", +"복합": null +}, +{ +"id": "S096", +"파일": "북부청사방", +"이름": "뒷채움잡석뒷채움", +"규격": "", +"단위": "㎥", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"3-4-4" +] +], +"직종근거": [ +"특별인부", +"보통인부" +], +"인력": { +"특별인부": { +"품": 0.009, +"밑품": 0.009, +"율": [], +"어떻게": [ +"식" +] +}, +"보통인부": { +"품": 0.009, +"밑품": 0.009, +"율": [], +"어떻게": [ +"식" +] +} +}, +"인력절": [ +[ +"1.다", +"특별인부", +0.009, +0.009 +], +[ +"1.다", +"보통인부", +0.009, +0.009 +] +], +"장비": [ +{ +"갈래": [ +"굴착기" +], +"규격": [ +0.2 +], +"시간": 0.07272727272727272, +"밑시간": 0.07272727272727272, +"율": [], +"절": "1.가", +"글": "기 계 : 굴착기(무한궤도)(0.2㎥)" +}, +{ +"갈래": [ +"롤러" +], +"규격": [ +0.7 +], +"시간": 0.07272727272727272, +"밑시간": 0.07272727272727272, +"율": [], +"절": "1.나", +"글": "기 계 : 진동로울러(핸드가이드식)(0.7Ton)" +} +], +"자재": [], +"율": [], +"계수": {}, +"조건": { +"규모": [ +"소형" +], +"방법": [ +"기계", +"장비" +] +}, +"옛판": false, +"글": [ +"15.뒷채움잡석뒷채움[㎥]", +" 1. 구조물뒷채움잡석 뒤채움 및 다짐 [2025년 건설표준품셈3-4-4(소형장비)]", +" 가. 기 계 : 굴착기(무한궤도)(0.2㎥)", +" 1일 시공량 : Qd = 110㎥/일", +" Q = Qd / 8 = 110 / 8 = 13.75㎥/hr", +" 노무비 : 59,020 ÷ 13.75 = 4,292.3", +" 재료비 : 8,324 ÷ 13.75 = 605.3", +" 경 비 : 13,873 ÷ 13.75 = 1,008.9", +" 소 계 : 5,906.5", +" 나. 기 계 : 진동로울러(핸드가이드식)(0.7Ton)", +" 재료비 : 3,420 ÷ 13.75 = 248.7", +" 경 비 : 1,967 ÷ 13.75 = 143", +" 소 계 : 391.7", +" 다. 인건비", +" P = 1 / Qd = 1 / 110 = 0.009인", +" 특별인부 : 226,122 × P = 226,122 × 0.009 = 2,035", +" 보통인부 : 172,068 × P = 172,068 × 0.009 = 1,548.6", +" 인부소계 : 3,583.6", +" 라. 합 계", +" 노무비 : 4,292.3 + 3,583.6 = 7,875", +" 재료비 : 605.3 + 248.7 = 854", +" 경 비 : 1,008.9 + 143 = 1,151", +" 계 : 9,880" +], +"절": { +"1": { +"이름": "구조물뒷채움잡석 뒤채움 및 다짐 [2025년 건설표준품셈3-4-4(소형장비)]", +"출처": [ +[ +"건설품셈", +"3-4-4" +] +] +}, +"1.가": { +"이름": "구조물뒷채움잡석 뒤채움 및 다짐 [2025년 건설표준품셈3-4-4(소형장비)] · 기 계 : 굴착기(무한궤도)(0.2㎥)", +"출처": [ +[ +"건설품셈", +"3-4-4" +] +] +}, +"1.나": { +"이름": "구조물뒷채움잡석 뒤채움 및 다짐 [2025년 건설표준품셈3-4-4(소형장비)] · 기 계 : 진동로울러(핸드가이드식)(0.7Ton)", +"출처": [ +[ +"건설품셈", +"3-4-4" +] +] +}, +"1.다": { +"이름": "구조물뒷채움잡석 뒤채움 및 다짐 [2025년 건설표준품셈3-4-4(소형장비)] · 인건비", +"출처": [ +[ +"건설품셈", +"3-4-4" +] +] +}, +"1.라": { +"이름": "구조물뒷채움잡석 뒤채움 및 다짐 [2025년 건설표준품셈3-4-4(소형장비)] · 합 계", +"출처": [ +[ +"건설품셈", +"3-4-4" +] +] +} +}, +"이름조건": {}, +"정답": [ +"GC000106" +], +"허용": [], +"무리": "사람", +"복합": null +}, +{ +"id": "S097", +"파일": "북부청사방", +"이름": "뒷채움잡석구입운반", +"규격": "(창봉 기번3)(2차)", +"단위": "㎥", +"갈래": "단가산출", +"출처": [], +"직종근거": [], +"인력": {}, +"인력절": [], +"장비": [ +{ +"갈래": [ +"덤프트럭" +], +"규격": [ +15.0 +], +"시간": 0.32894736842105265, +"밑시간": 0.32894736842105265, +"율": [], +"절": "2", +"글": "덤프트럭(15Ton)" +} +], +"자재": [], +"율": [], +"계수": { +"q": 0.8, +"K": 0.7, +"Cm": 20.0, +"E": 0.45, +"k": 0.55, +"t3": 1.1, +"t4": 0.42 +}, +"조건": {}, +"옛판": false, +"글": [ +"14.뒷채움잡석구입운반((창봉 기번3)(2차))[㎥]", +" 1. 적 사 : BACK-HOE 0.8㎥", +" q = 0.8 f = 1 / 1.125 = 0.89 K = 0.7", +" Cm = 20sec E = 0.45", +" 3600×q×K×f×E 3,600×0.8×0.7×0.89×0.45", +" Q = ────────── = ────────────── = 40.37 ㎥/hr", +" Cm 20", +" 2. 운 반 - 홍천골재장", +" 덤프트럭(15Ton)", +" L1 = 18.7km L2 = 1.4km", +" V11 = 30km V12 = 35km V21 = 10km V22 = 15km", +" k = 0.55 E = 0.9 F = 1", +" q1 = (15 / 1.7) × 1.125 = 9.93", +" n = q1 / (0.8 × k) = 9.93 / (0.8 × 0.55) = 22.57 회", +" t1 = 20 × n / (60 × 0.45) = 20 × 22.57 / (60 × 0.45) = 16.72 분", +" L1 L2 L1 L2 18.7 1.4 18.7 1.4", +" t2 = (──+──+──+──)×60 = (───+──+───+──)×60 =157.6분", +" V11 V12 V21 V22 30 35 10 15", +" t3 = 1.1분 t4 = 0.42분 t5 = 0.5분", +" Cm = t1 + t2 + t3 + t4 + t5 = 16.72 + 157.6 + 1.1 + 0.42 + 0.5 = 176.34", +" 60×q1×F×E 60×9.93×1×0.9", +" Q1 = ──────── = ───────── = 3.04 ㎥/hr", +" Cm 176.34", +" 노무비 : 59,020 ÷ Q1 = 59,020 ÷ 3.04 = 19,414.4", +" 재료비 : 30,192÷Q1×(Cm-t1)÷Cm = 30,192÷3.04×(176.34-16.72)÷176.34 = 8,989.8", +" 경 비 : 20,659 ÷ Q1 = 20,659 ÷ 3.04 = 6,795.7", +" 소 계 : 35,199.9", +" 3. 합 계", +" 노무비 : 19,414", +" 재료비 : 8,989", +" 경 비 : 6,795", +" 계 : 35,198" +], +"절": { +"1": { +"이름": "적 사 : BACK-HOE 0.8㎥", +"출처": [] +}, +"2": { +"이름": "운 반 - 홍천골재장", +"출처": [] +}, +"3": { +"이름": "합 계", +"출처": [] +} +}, +"이름조건": {}, +"정답": [ +"GF000135", +"GF000284" +], +"허용": [], +"무리": "사람", +"복합": null +}, +{ +"id": "S098", +"파일": "북부청사방", +"이름": "큰돌메붙임", +"규격": "40cm-60cm,(산림사업품셈)", +"단위": "㎡", +"갈래": "단가산출", +"출처": [ +[ +"산림품셈", +"13-7-1" +] +], +"직종근거": [ +"작업반장", +"특별인부", +"보통인부" +], +"인력": { +"작업반장": { +"품": 0.058, +"밑품": 0.058, +"율": [], +"어떻게": [ +"식" +] +}, +"특별인부": { +"품": 0.058, +"밑품": 0.058, +"율": [], +"어떻게": [ +"식" +] +}, +"보통인부": { +"품": 0.083, +"밑품": 0.083, +"율": [], +"어떻게": [ +"식" +] +} +}, +"인력절": [ +[ +"1.가", +"작업반장", +0.058, +0.058 +], +[ +"1.가", +"특별인부", +0.058, +0.058 +], +[ +"1.가", +"보통인부", +0.083, +0.083 +] +], +"장비": [ +{ +"갈래": [ +"굴착기" +], +"규격": [ +0.8 +], +"시간": 0.24, +"밑시간": 0.24, +"율": [], +"절": "1.나", +"글": "굴착기(무한궤도)(0.8㎥)[hr]" +}, +{ +"갈래": [ +"집게" +], +"규격": [ +0.8 +], +"시간": 0.24, +"밑시간": 0.24, +"율": [], +"절": "1.나", +"글": "부착용집게(0.7~0.8㎥)" +} +], +"자재": [], +"율": [ +[ +"품", +1.0 +] +], +"계수": { +"T": 0.24 +}, +"조건": { +"메찰": [ +"메붙임" +] +}, +"옛판": false, +"글": [ +"59.큰돌메붙임(40cm-60cm,(산림사업품셈))[㎡]", +" 1. 큰돌메붙임 (2026년 산림사업 품셈 13-7-1)", +" 가. 노무비", +" 작업반장 : 215,907 × 0.058 = 12,522.6", +" 특별인부 : 226,122 × 0.058 = 13,115", +" 보통인부 : 172,068 × 0.083 = 14,281.6", +" 인부소계 : 39,919.2", +" 나. 기계경비", +" 굴착기(무한궤도)(0.8㎥)[hr]", +" T = 0.24", +" 노무비 : 59,020 × T = 59,020 × 0.24 = 14,164.8", +" 재료비 : 25,684 × T = 25,684 × 0.24 = 6,164.1", +" 경 비 : 27,018 × T = 27,018 × 0.24 = 6,484.3", +" 소 계 : 26,813.2", +" 부착용집게(0.7~0.8㎥)", +" 노무비 : 0 × T = 0 × 0.24 = 0", +" 재료비 : 0 × T = 0 × 0.24 = 0", +" 경 비 : 5,786 × T = 5,786 × 0.24 = 1,388.6", +" 소 계 : 1,388.6", +" 다. 제잡비(품의 1%)", +" (12,522.6 + 13,115 + 14,281.6 + 14,164.8) × 1% = 540.8", +" 라. 소 계", +" 노무비 : 39,919.2 + 14,164.8 + 540.8 = 54,624.8", +" 재료비 : 6,164.1", +" 경 비 : 6,484.3 + 1,388.6 = 7,872.9", +" 계 : 68,661.8", +" 2. 합 계", +" 노무비 : 54,624", +" 재료비 : 6,164", +" 경 비 : 7,872", +" 계 : 68,660" +], +"절": { +"1": { +"이름": "큰돌메붙임 (2026년 산림사업 품셈 13-7-1)", +"출처": [ +[ +"산림품셈", +"13-7-1" +] +] +}, +"1.가": { +"이름": "큰돌메붙임 (2026년 산림사업 품셈 13-7-1) · 노무비", +"출처": [ +[ +"산림품셈", +"13-7-1" +] +] +}, +"1.나": { +"이름": "큰돌메붙임 (2026년 산림사업 품셈 13-7-1) · 기계경비", +"출처": [ +[ +"산림품셈", +"13-7-1" +] +] +}, +"1.다": { +"이름": "큰돌메붙임 (2026년 산림사업 품셈 13-7-1) · 제잡비(품의 1%)", +"출처": [ +[ +"산림품셈", +"13-7-1" +] +] +}, +"1.라": { +"이름": "큰돌메붙임 (2026년 산림사업 품셈 13-7-1) · 소 계", +"출처": [ +[ +"산림품셈", +"13-7-1" +] +] +}, +"2": { +"이름": "합 계", +"출처": [] +} +}, +"이름조건": { +"메찰": [ +"메붙임" +] +}, +"정답": [ +"GF000229" +], +"허용": [ +"GC000262" +], +"무리": "사람", +"복합": null +}, +{ +"id": "S099", +"파일": "북부청사방", +"이름": "큰돌찰붙임", +"규격": "40cm-60cm,(산림사업품셈)", +"단위": "㎡", +"갈래": "단가산출", +"출처": [ +[ +"산림품셈", +"13-7-2" +] +], +"직종근거": [ +"작업반장", +"특별인부", +"보통인부" +], +"인력": { +"작업반장": { +"품": 0.058, +"밑품": 0.058, +"율": [], +"어떻게": [ +"식" +] +}, +"특별인부": { +"품": 0.101, +"밑품": 0.101, +"율": [], +"어떻게": [ +"식" +] +}, +"보통인부": { +"품": 0.101, +"밑품": 0.101, +"율": [], +"어떻게": [ +"식" +] +} +}, +"인력절": [ +[ +"1.가", +"작업반장", +0.058, +0.058 +], +[ +"1.가", +"특별인부", +0.101, +0.101 +], +[ +"1.가", +"보통인부", +0.101, +0.101 +] +], +"장비": [ +{ +"갈래": [ +"굴착기", +"집게" +], +"규격": [ +0.8 +], +"시간": 0.336, +"밑시간": 0.336, +"율": [], +"절": "1.나", +"글": "굴착기(무한궤도)(0.8㎥+부착용집게)[hr]" +} +], +"자재": [], +"율": [ +[ +"품", +3.0 +] +], +"계수": { +"T": 0.336 +}, +"조건": { +"메찰": [ +"찰붙임" +] +}, +"옛판": false, +"글": [ +"61.큰돌찰붙임(40cm-60cm,(산림사업품셈))[㎡]", +" 1. 큰돌찰붙임 (2026년 산림사업 품셈 13-7-2)", +" 가. 노무비", +" 작업반장 : 215,907 × 0.058 = 12,522.6", +" 특별인부 : 226,122 × 0.101 = 22,838.3", +" 보통인부 : 172,068 × 0.101 = 17,378.8", +" 인부소계 : 52,739.7", +" 나. 기계경비", +" 굴착기(무한궤도)(0.8㎥+부착용집게)[hr]", +" T = 0.336", +" 노무비 : 59,020 × T = 59,020 × 0.336 = 19,830.7", +" 재료비 : 24,421 × T = 24,421 × 0.336 = 8,205.4", +" 경 비 : 32,805 × T = 32,805 × 0.336 = 11,022.4", +" 소 계 : 39,058.5", +" 다. 제잡비(품의 3%)", +" (12,522.6 + 22,838.3 + 17,378.8 + 19,830.7) × 3% = 2,177.1", +" 라. 소 계", +" 노무비 : 52,739.7 + 19,830.7 + 2,177.1 = 74,747.5", +" 재료비 : 8,205.4", +" 경 비 : 11,022.4", +" 계 : 93,975.3", +" 2. 합 계", +" 노무비 : 74,747", +" 재료비 : 8,205", +" 경 비 : 11,022", +" 계 : 93,974" +], +"절": { +"1": { +"이름": "큰돌찰붙임 (2026년 산림사업 품셈 13-7-2)", +"출처": [ +[ +"산림품셈", +"13-7-2" +] +] +}, +"1.가": { +"이름": "큰돌찰붙임 (2026년 산림사업 품셈 13-7-2) · 노무비", +"출처": [ +[ +"산림품셈", +"13-7-2" +] +] +}, +"1.나": { +"이름": "큰돌찰붙임 (2026년 산림사업 품셈 13-7-2) · 기계경비", +"출처": [ +[ +"산림품셈", +"13-7-2" +] +] +}, +"1.다": { +"이름": "큰돌찰붙임 (2026년 산림사업 품셈 13-7-2) · 제잡비(품의 3%)", +"출처": [ +[ +"산림품셈", +"13-7-2" +] +] +}, +"1.라": { +"이름": "큰돌찰붙임 (2026년 산림사업 품셈 13-7-2) · 소 계", +"출처": [ +[ +"산림품셈", +"13-7-2" +] +] +}, +"2": { +"이름": "합 계", +"출처": [] +} +}, +"이름조건": { +"메찰": [ +"찰붙임" +] +}, +"정답": [ +"GF000230" +], +"허용": [ +"GC000263" +], +"무리": "사람", +"복합": null +}, +{ +"id": "Y000", +"파일": "동부청영월", +"이름": "무근콘크리트타설", +"규격": "인력운반타설", +"단위": "M3", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"6-1-1" +] +], +"직종근거": [], +"인력": { +"콘크리트공": { +"품": 0.130435, +"밑품": 0.130435, +"율": [], +"어떻게": [ +"QTY" +] +}, +"보통인부": { +"품": 0.130435, +"밑품": 0.130435, +"율": [], +"어떻게": [ +"QTY" +] +} +}, +"인력절": [ +[ +"1", +"콘크리트공", +0.130435, +0.130435 +], +[ +"1", +"보통인부", +0.130435, +0.130435 +] +], +"장비": [], +"자재": [], +"율": [], +"계수": {}, +"조건": { +"근": [ +"무근" +], +"규모": [ +"소형" +], +"방법": [ +"인력", +"인력운반" +] +}, +"옛판": false, +"글": [ +"무근콘크리트타설 인력운반타설 / M3", +"무근콘크리트타설(소형)(24년 건설표준품셈 6-1-1)", +"1. 인 력", +"콘크리트공 : 3 인 * 1 일 / 23 ㎥ * 261,283 = 34,080.3", +"보통인부 : 3 인 * 1 일 / 23 ㎥ * 165,545 = 21,592.8", +"공구및경장비손료 : 55,673.1 * 2 % = 1,113.4", +"소계", +"계" +], +"절": { +"1": { +"이름": "인 력", +"출처": [] +} +}, +"이름조건": { +"근": [ +"무근" +], +"방법": [ +"인력운반" +] +}, +"정답": [ +"GC000198" +], +"허용": [ +"GF000155" +], +"무리": "사람", +"복합": null +}, +{ +"id": "Y001", +"파일": "동부청영월", +"이름": "합판거푸집(6회)", +"규격": "간단", +"단위": "M2", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"6-3-1" +] +], +"직종근거": [], +"인력": { +"형틀목공": { +"품": 0.1, +"밑품": 0.1, +"율": [], +"어떻게": [ +"QTY" +] +}, +"보통인부": { +"품": 0.04, +"밑품": 0.04, +"율": [], +"어떻게": [ +"QTY" +] +} +}, +"인력절": [ +[ +"2", +"형틀목공", +0.1, +0.1 +], +[ +"2", +"보통인부", +0.04, +0.04 +] +], +"장비": [], +"자재": [ +{ +"이름": "내수합판1급 12mmx1220x2440", +"품": 0.33681, +"밑품": 0.33681 +}, +{ +"이름": "각재 (외송)", +"품": 0.012426, +"밑품": 0.012426 +} +], +"율": [ +[ +"인력품", +1.0 +] +], +"계수": {}, +"조건": { +"난이": [ +"간단" +], +"횟수": [ +"6회" +] +}, +"옛판": false, +"글": [ +"합판거푸집(6회) 간단 / M2", +"합판거푸집(6회) 24년 건설표준품셈 6-3-1", +"1. 재 료 비", +"합판 : 1.03 ㎡ * 0.327 * 10,986 = 3,700.1", +"각재 (외송) : 0.038 ㎥ * 0.327 * 586,826 = 7,291.8", +"잡재료비 : 10,991.9 * 11 % = 1,209.1", +"소계", +"2. 가공 제작 조립 해체비", +"형틀목공 : 5 인 * 1 일 / 50 ㎡ * 274,978 = 27,497.8", +"보통인부 : 2 인 * 1 일 / 50 ㎡ * 165,545 = 6,621.8", +"소계", +"3. 공구손료(인력품의 1%)", +"공구손료 : 34,119.6 * 1 % = 341.1", +"소계", +"계" +], +"절": { +"1": { +"이름": "재 료 비", +"출처": [] +}, +"2": { +"이름": "가공 제작 조립 해체비", +"출처": [] +}, +"3": { +"이름": "공구손료(인력품의 1%)", +"출처": [] +} +}, +"이름조건": { +"난이": [ +"간단" +], +"횟수": [ +"6회" +] +}, +"정답": [ +"GC000211" +], +"허용": [ +"GF000160" +], +"무리": "사람", +"복합": null +}, +{ +"id": "Y002", +"파일": "동부청영월", +"이름": "구조물터파기(인력)", +"규격": "경질토사", +"단위": "M3", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"3-1-3" +] +], +"직종근거": [ +"보통인부" +], +"인력": { +"보통인부": { +"품": 0.26, +"밑품": 0.26, +"율": [], +"어떻게": [ +"QTY" +] +} +}, +"인력절": [ +[ +"0", +"보통인부", +0.26, +0.26 +] +], +"장비": [], +"자재": [], +"율": [], +"계수": {}, +"조건": { +"토질": [ +"경질토사" +], +"방법": [ +"인력" +] +}, +"옛판": false, +"글": [ +"구조물터파기(인력) 경질토사 / M3", +"구조물 터파기 (인력) 건설표준품셈3-1-3", +"○.인 부 :", +"0.26 * 165,545 = 43,041.7", +"소계", +"계" +], +"절": { +"0": { +"이름": "26 * 165,545 = 43,041.7", +"출처": [] +} +}, +"이름조건": { +"토질": [ +"경질토사" +], +"방법": [ +"인력" +] +}, +"정답": [ +"GF000101" +], +"허용": [ +"GC000083" +], +"무리": "사람", +"복합": null +}, +{ +"id": "Y003", +"파일": "동부청영월", +"이름": "구조물되메우기", +"규격": "(인력)", +"단위": "M3", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"3-1-3" +] +], +"직종근거": [ +"보통인부" +], +"인력": { +"보통인부": { +"품": 0.1, +"밑품": 0.1, +"율": [], +"어떻게": [ +"QTY" +] +} +}, +"인력절": [ +[ +"0", +"보통인부", +0.1, +0.1 +] +], +"장비": [], +"자재": [], +"율": [], +"계수": {}, +"조건": { +"방법": [ +"인력" +] +}, +"옛판": false, +"글": [ +"구조물되메우기 (인력) / M3", +"구조물 되메우기 (인력) 건설표준품셈3-1-3", +"○.인 부 :", +"0.1*165,545= 16,554.5", +"소계" +], +"절": { +"0": { +"이름": "1*165,545= 16,554.5", +"출처": [] +} +}, +"이름조건": { +"방법": [ +"인력" +] +}, +"정답": [ +"GF000113" +], +"허용": [], +"무리": "사람", +"복합": null +}, +{ +"id": "Y005", +"파일": "동부청영월", +"이름": "크레인(타이어)", +"규격": "10톤", +"단위": "시간", +"갈래": "기계", +"출처": [], +"직종근거": [], +"인력": {}, +"인력절": [], +"장비": [ +{ +"갈래": [ +"크레인" +], +"규격": [ +10.0 +], +"시간": 1.0, +"밑시간": null, +"절": 0, +"글": "크레인(타이어)" +} +], +"자재": [], +"율": [], +"계수": {}, +"조건": {}, +"옛판": false, +"글": [], +"절": {}, +"이름조건": {}, +"정답": [ +"GC000268", +"GC000269", +"GC000275" +], +"허용": [], +"무리": "사람", +"복합": null +}, +{ +"id": "Y006", +"파일": "동부청영월", +"이름": "구조물터파기", +"규격": "토사(B/H 90%+인력10%)", +"단위": "M3", +"갈래": "단가산출", +"출처": [ +[ +"산림품셈", +"5-1-2" +] +], +"직종근거": [ +"보통인부" +], +"인력": { +"보통인부": { +"품": 0.023, +"밑품": 0.23, +"율": [ +[ +0.1, +"섞음" +] +], +"어떻게": [ +"QTY" +] +} +}, +"인력절": [ +[ +"1", +"보통인부", +0.023, +0.23 +] +], +"장비": [ +{ +"갈래": [ +"굴착기" +], +"규격": [ +0.7 +], +"시간": 0.01717885092574919, +"밑시간": 0.019087612139721325, +"율": [ +[ +0.9, +"섞음" +] +], +"절": "2", +"글": "굴삭기(무한궤도) 0.7㎥" +} +], +"자재": [], +"율": [], +"계수": { +"q": 0.7, +"K": 0.9, +"Cm": 20.0 +}, +"조건": { +"토질": [ +"토사" +], +"방법": [ +"기계", +"인력" +] +}, +"옛판": true, +"글": [ +"구조물터파기 토사(B/H 90%+인력10%) / M3", +"구조물 터파기 (기계90% + 인력10%) 임도품셈 5-1-2", +"1. 인 력 (10 %) 적용", +"인 부 :", +"(0.2+0.26)/2 * 165,545 * 0.1 = 3,807.5", +"소계", +"2. 기 계 ( 유압식 백호우 0.7 m3 90 %)", +"q = 0.7 , f = 1/1.3 = 0.77 , K = 0.9", +"Cm = 20 sec(135˚) ,E=(0.7+0.6)/2-0.05= 0.60", +"Q = 3600*q*K*f*E/Cm = 52.39 m3/hr", +"노 무 비 : 55,700 / Q * 0.9 = 956.8", +"재 료 비 : 18,015 / Q * 0.9 = 309.4", +"경 비 : 23,128 / Q * 0.9 = 397.3", +"소계", +"계" +], +"절": { +"1": { +"이름": "인 력 (10 %) 적용", +"출처": [] +}, +"2": { +"이름": "기 계 ( 유압식 백호우 0.7 m3 90 %)", +"출처": [] +} +}, +"이름조건": { +"토질": [ +"토사" +], +"방법": [ +"인력" +] +}, +"정답": [ +"GF000101" +], +"허용": [ +"GC000089" +], +"무리": "사람", +"복합": null +}, +{ +"id": "Y008", +"파일": "동부청영월", +"이름": "콘크리트타설", +"규격": "임도품셈 5-5", +"단위": "M3", +"갈래": "단가산출", +"출처": [ +[ +"산림품셈", +"5-5" +] +], +"직종근거": [], +"인력": { +"콘크리트공": { +"품": 0.15, +"밑품": 0.15, +"율": [], +"어떻게": [ +"QTY" +] +}, +"보통인부": { +"품": 0.27, +"밑품": 0.27, +"율": [], +"어떻게": [ +"QTY" +] +} +}, +"인력절": [ +[ +"1", +"콘크리트공", +0.15, +0.15 +], +[ +"1", +"보통인부", +0.27, +0.27 +] +], +"장비": [ +{ +"갈래": [ +"진동기" +], +"규격": [ +2.6 +], +"시간": 0.18518518518518517, +"밑시간": 0.18518518518518517, +"율": [], +"절": "2", +"글": "콘크리트 진동기 45φ(2.6㎾)엔진식플렉시블형" +} +], +"자재": [], +"율": [], +"계수": {}, +"조건": { +"방법": [ +"인력" +] +}, +"옛판": true, +"글": [ +"콘크리트타설 임도품셈 5-5 / M3", +"콘크리트타설(임도품셈 5-5)", +"1. 인 력", +"콘크리트공 : 0.15 인/㎥ * 261,283 = 39,192.4", +"보통인부 : 0.27 인/㎥ * 165,545 = 44,697.1", +"소계", +"2. 다 짐", +"Q =5.4 ㎥/ hr", +"재료비: 1,450 / Q = 268.5", +"경 비: 128 / Q = 23.7", +"소계", +"계" +], +"절": { +"1": { +"이름": "인 력", +"출처": [] +}, +"2": { +"이름": "다 짐", +"출처": [] +} +}, +"이름조건": {}, +"정답": [ +"GC000198" +], +"허용": [ +"GF000155", +"GF000166" +], +"무리": "사람", +"복합": null +}, +{ +"id": "Y009", +"파일": "동부청영월", +"이름": "유로폼 설치,해체(복잡)", +"규격": "수직고7.0m 이하", +"단위": "M2", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"6-3-3" +] +], +"직종근거": [], +"인력": { +"형틀목공": { +"품": 0.16, +"밑품": 0.16, +"율": [], +"어떻게": [ +"QTY" +] +}, +"보통인부": { +"품": 0.04, +"밑품": 0.04, +"율": [], +"어떻게": [ +"QTY" +] +} +}, +"인력절": [ +[ +"2", +"형틀목공", +0.16, +0.16 +], +[ +"2", +"보통인부", +0.04, +0.04 +] +], +"장비": [], +"자재": [ +{ +"이름": "유로폼(패널) 600*1200", +"품": 0.089, +"밑품": 0.089 +}, +{ +"이름": "내부코너패널 (200+200)*1200", +"품": 0.003, +"밑품": 0.003 +} +], +"율": [ +[ +"인력품", +3.0 +] +], +"계수": {}, +"조건": { +"난이": [ +"복잡" +] +}, +"옛판": false, +"글": [ +"유로폼 설치,해체(복잡) 수직고7.0m 이하 / M2", +"유로폼 설치,해체(복잡) 24년 건설표준품셈품셈 6-3-3", +"1. 재 료 비", +"패널(600*1200) : 0.089 매 * 29,000 = 2,581.0", +"내부코너패널((200+200)*1200) : 0.003 매 * 18,700 = 56.1", +"부자재비 : 2,637.1 * 79 % = 2,083.3", +"소모자재 : 2,637.1 * 5 % = 131.8", +"소계", +"2. 설치 해체비", +"형틀목공 : 4 인 * 1 일 / 25 ㎡ * 274,978 = 43,996.4", +"보통인부 : 1 인 * 1 일 / 25 ㎡ * 165,545 = 6,621.8", +"소계", +"3. 공구손료(인력품의 3%)", +"공구손료 : 50,618.2 * 3 % = 1,518.5", +"소계", +"계" +], +"절": { +"1": { +"이름": "재 료 비", +"출처": [] +}, +"2": { +"이름": "설치 해체비", +"출처": [] +}, +"3": { +"이름": "공구손료(인력품의 3%)", +"출처": [] +} +}, +"이름조건": { +"난이": [ +"복잡" +] +}, +"정답": [ +"GC000214" +], +"허용": [ +"GF000210" +], +"무리": "사람", +"복합": null +}, +{ +"id": "Y010", +"파일": "동부청영월", +"이름": "철근가공조립(현장)", +"규격": "TYPE Ⅰ", +"단위": "Ton", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"6-2-3" +], +[ +"건설품셈", +"6-2-2" +] +], +"직종근거": [], +"인력": { +"철근공": { +"품": 2.380952, +"밑품": 2.380952, +"율": [], +"어떻게": [ +"QTY", +"QTY" +] +}, +"보통인부": { +"품": 0.793651, +"밑품": 0.793651, +"율": [], +"어떻게": [ +"QTY", +"QTY" +] +} +}, +"인력절": [ +[ +"1", +"철근공", +0.666667, +0.666667 +], +[ +"1", +"보통인부", +0.222222, +0.222222 +], +[ +"2", +"철근공", +1.714286, +1.714286 +], +[ +"2", +"보통인부", +0.571429, +0.571429 +] +], +"장비": [], +"자재": [ +{ +"이름": "이형철근 별도계상", +"품": 1.0, +"밑품": 1.0 +}, +{ +"이름": "어닐링철선(결속선) #20 0.9M/M", +"품": 6.5, +"밑품": 6.5 +} +], +"율": [ +[ +"인건비", +9.0 +], +[ +"인건비", +2.0 +] +], +"계수": {}, +"조건": { +"근": [ +"철근" +], +"type": [ +"Ⅰ" +] +}, +"옛판": false, +"글": [ +"철근가공조립(현장) TYPE Ⅰ / Ton", +"철근가공조립(TYPE Ⅰ) 24년 건설표준품셈 6-2-3", +"1. 현장가공", +"철근(별도계상) : 1 Ton * 0 = 0.0", +"철근공 : 3 인 * 1 일 / 4.5 Ton * 260,137 = 173,424.6", +"보통인부 : 1 인 * 1 일 / 4.5 Ton * 165,545 = 36,787.7", +"공구손료(인건비의9%) : 210,212.3 * 9 % = 18,919.1", +"소계", +"2. 조 립(TYPE Ⅰ) 24년 건설표준품셈 6-2-2", +"결속선 : 6.5 ㎏/Ton * 1,760 = 11,440.0", +"철근공 : 6 인 * 1 일 / 3.5 Ton * 260,137 = 445,949.1", +"보통인부 : 2 인 * 1 일 / 3.5 Ton * 165,545 = 94,597.1", +"공구손료(인건비의2%) : 540,546.2 * 2 % = 10,810.9", +"소계", +"계" +], +"절": { +"1": { +"이름": "현장가공", +"출처": [] +}, +"2": { +"이름": "조 립(TYPE Ⅰ) 24년 건설표준품셈 6-2-2", +"출처": [ +[ +"건설품셈", +"6-2-2" +] +] +} +}, +"이름조건": { +"근": [ +"철근" +], +"type": [ +"Ⅰ" +] +}, +"정답": [ +"GC000206", +"GC000207" +], +"허용": [ +"GF000159" +], +"무리": "사람", +"복합": [ +"GC000206", +"GC000207" +] +}, +{ +"id": "Y011", +"파일": "동부청영월", +"이름": "기초잡석채집및운반부설", +"규격": "4.5톤(L=500)", +"단위": "M3", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"9-3" +], +[ +"건설품셈", +"9-7" +], +[ +"건설품셈", +"5-1-1" +] +], +"직종근거": [], +"인력": { +"보통인부": { +"품": 0.03, +"밑품": 0.3, +"율": [ +[ +0.1, +"섞음" +] +], +"어떻게": [ +"QTY" +] +} +}, +"인력절": [ +[ +"1.2", +"보통인부", +0.03, +0.3 +] +], +"장비": [ +{ +"갈래": [ +"굴착기" +], +"규격": [ +0.4 +], +"시간": 0.0522344747533372, +"밑시간": 0.05803830528148578, +"율": [ +[ +0.9, +"섞음" +] +], +"절": "1.1", +"글": "굴삭기(무한궤도) 0.4㎥" +}, +{ +"갈래": [ +"굴착기" +], +"규격": [ +0.4 +], +"시간": 0.04562043795620438, +"밑시간": 0.04562043795620438, +"율": [], +"절": "2", +"글": "굴삭기(무한궤도) 0.4㎥" +}, +{ +"갈래": [ +"덤프트럭" +], +"규격": [ +4.5 +], +"시간": 0.09541984732824427, +"밑시간": 0.09541984732824427, +"율": [], +"절": "3", +"글": "덤프트럭 4.5톤" +}, +{ +"갈래": [ +"굴착기" +], +"규격": [ +0.2 +], +"시간": 0.07, +"밑시간": 0.07, +"율": [], +"절": "4", +"글": "굴삭기(무한궤도) 0.2㎥" +} +], +"자재": [], +"율": [], +"계수": { +"L": 500.0, +"q": 0.4, +"K": 0.55, +"Cm": 18.0, +"E": 0.45, +"k": 0.7, +"t3": 0.8, +"t4": 0.42 +}, +"조건": { +"방법": [ +"기계", +"인력" +] +}, +"옛판": false, +"글": [ +"기초잡석채집및운반부설 4.5톤(L=500) / M3", +"기초잡석채집및운반부설 / m3", +"1) 채집 : (기계:인력=9:1) 건설표준품셈 9-3", +"① B/H 0.4㎥(90%)", +"q = 0.4 , f =1/1.15 = 0.87 , K = 0.55", +"Cm = 18 sec(135˚) , E = 0.45", +"Q = 3600*q*K*f*E/Cm = 17.23 m3/hr", +"노 무 비 : 55,700 / Q *0.9= 2,909.4", +"재 료 비 : 15,375 / Q *0.9= 803.1", +"경 비 : 16,378 / Q *0.9= 855.4", +"소계", +"②인력(10%) 사방품셈P45", +"인 부: 0.3 인/ ㎥ *165,545* 0.1= 4,966.3", +"소계", +"계", +"2) 적사 (B/H 0.4㎥) 건설표준품셈 9-3", +"q = 0.4 , f = 1/1.15 = 0.87 , K = 0.7", +"Cm = 18 sec(135˚) , E = 0.45", +"Q = 3600*q*K*f*E/Cm = 21.92 m3/hr", +"노 무 비 : 55,700 / Q = 2,541.0", +"재 료 비 : 15,375 / Q = 701.4", +"경 비 : 16,378 / Q = 747.1", +"소계", +"3) 운반 (덤프4.5Ton) 건설표준품셈 9-7", +"L=0.5 Km ,k=0.7,E=0.9,F=1/1.15 = 0.87", +"q1 =(4.5/1.75)*1.15 = 2.96", +"n =q1 / (0.4 * k) = 10.57 회", +"t1 =18 * n / (60 * 0.45) = 7.05 분", +"t2 =(L/10+L/15)* 60 = 5.00 분", +"t3=0.8 분 ,t4=0.42 분", +"Cm = t1 + t2 + t3 + t4 = 13.27", +"Q = 60 * q1 * F * E / Cm = 10.48 m3/hr", +"노 무 비 : 47,231 / Q = 4,506.7", +"재 료 비 : 8,783 / Q = 838.0", +"경 비 : 7,202 / Q = 687.2", +"소계", +"4)기초잡석다짐 건설표준품셈 5-1-1", +"기계 : B/H 0.2㎥", +"Q = 0.07 hr", +"노 무 비 : 55,700 * Q = 3,899.0", +"재 료 비 : 7,701 * Q = 539.0", +"경 비 : 13,041 * Q = 912.8", +"소계", +"계" +], +"절": { +"1": { +"이름": "채집 : (기계:인력=9:1) 건설표준품셈 9-3", +"출처": [ +[ +"건설품셈", +"9-3" +] +] +}, +"1.1": { +"이름": "채집 : (기계:인력=9:1) 건설표준품셈 9-3 · B/H 0.4㎥(90%)", +"출처": [ +[ +"건설품셈", +"9-3" +] +] +}, +"1.2": { +"이름": "채집 : (기계:인력=9:1) 건설표준품셈 9-3 · 인력(10%) 사방품셈P45", +"출처": [ +[ +"건설품셈", +"9-3" +] +] +}, +"2": { +"이름": "적사 (B/H 0.4㎥) 건설표준품셈 9-3", +"출처": [ +[ +"건설품셈", +"9-3" +] +] +}, +"3": { +"이름": "운반 (덤프4.5Ton) 건설표준품셈 9-7", +"출처": [ +[ +"건설품셈", +"9-7" +] +] +}, +"4": { +"이름": "기초잡석다짐 건설표준품셈 5-1-1", +"출처": [ +[ +"건설품셈", +"5-1-1" +] +] +} +}, +"이름조건": {}, +"정답": [ +"GF000168" +], +"허용": [ +"GC000110" +], +"무리": "사람", +"복합": null +}, +{ +"id": "Y012", +"파일": "동부청영월", +"이름": "임도안전표지판설치", +"규격": "삼각,D=900", +"단위": "개소", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"1-9-3" +] +], +"직종근거": [], +"인력": { +"특별인부": { +"품": 0.257576, +"밑품": 0.257576, +"율": [], +"어떻게": [ +"QTY", +"QTY" +] +}, +"보통인부": { +"품": 0.128788, +"밑품": 0.128788, +"율": [], +"어떻게": [ +"QTY", +"QTY" +] +} +}, +"인력절": [ +[ +"2", +"특별인부", +0.166667, +0.166667 +], +[ +"2", +"특별인부", +0.090909, +0.090909 +], +[ +"2", +"보통인부", +0.083333, +0.083333 +], +[ +"2", +"보통인부", +0.045455, +0.045455 +] +], +"장비": [ +{ +"갈래": [ +"크레인" +], +"규격": [ +5.0 +], +"시간": 0.970873786407767, +"밑시간": 0.970873786407767, +"율": [], +"절": "3", +"글": "트럭탑재형 크레인 5톤" +} +], +"자재": [ +{ +"이름": "안전표지판 삼각,D=900", +"품": 1.0, +"밑품": 1.0 +}, +{ +"이름": "표지판지주 D=76.3mm, 3.5M", +"품": 1.0, +"밑품": 1.0 +} +], +"율": [], +"계수": {}, +"조건": {}, +"옛판": false, +"글": [ +"임도안전표지판설치 삼각,D=900 / 개소", +"임도안전표지판설치(삼각,D=900mm 표준품셈 1-9-3)", +"1. 재 료 비", +"-안전표지판(삼각,D=900mm) : 84,000 * 1 EA = 84,000.0", +"-표지판지주(D=76.3mm,3.5m) : 75,600 * 1 식 = 75,600.0", +"소계", +"2. 인 건 비", +"- 특별인부(지주) : 2 인 / 12 본/일 * 214,222 = 35,703.6", +"- 특별인부(표지판) : 2 인 / 22 본/일 * 214,222 = 19,474.7", +"- 보통인부(지주) : 1 인 / 12 본/일 * 165,545 = 13,795.4", +"- 보통인부(표지판) : 1 인 / 22 본/일 * 165,545 = 7,524.7", +"소계", +"3. 장 비(크레인 5Ton)", +"Q = 8 hr / 12 개소 + 8 hr / 22 개소 = 1.03", +"노 무 비 : 47,231 / Q = 45,855.3", +"재 료 비 : 7,790 / Q = 7,563.1", +"경 비 : 9,994 / Q = 9,702.9", +"3. 공구손료 및 경장비", +"공구손료 및 경장비: 139,619.7 * 3 % = 4,188.5", +"소계", +"계" +], +"절": { +"1": { +"이름": "재 료 비", +"출처": [] +}, +"2": { +"이름": "인 건 비", +"출처": [] +}, +"3": { +"이름": "공구손료 및 경장비", +"출처": [] +} +}, +"이름조건": {}, +"정답": [ +"GC000308" +], +"허용": [], +"무리": "사람", +"복합": null +}, +{ +"id": "Y013", +"파일": "동부청영월", +"이름": "야면석 채집 운반", +"규격": "(25x25x35㎝)장비", +"단위": "M2", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"9-3" +], +[ +"건설품셈", +"8-2" +], +[ +"건설품셈", +"9-7" +] +], +"직종근거": [], +"인력": { +"보통인부": { +"품": 0.034, +"밑품": 0.17, +"율": [ +[ +0.2, +"섞음" +] +], +"어떻게": [ +"QTY" +] +} +}, +"인력절": [ +[ +"1.2", +"보통인부", +0.034, +0.17 +] +], +"장비": [ +{ +"갈래": [ +"굴착기" +], +"규격": [ +0.4 +], +"시간": 0.015255783102561978, +"밑시간": 0.01906972887820247, +"율": [ +[ +0.8, +"섞음" +] +], +"절": "1.1", +"글": "굴삭기(무한궤도) 0.4㎥" +}, +{ +"갈래": [ +"굴착기" +], +"규격": [ +0.4 +], +"시간": 0.014989572471324297, +"밑시간": 0.014989572471324297, +"율": [], +"절": "2", +"글": "굴삭기(무한궤도) 0.4㎥" +}, +{ +"갈래": [ +"덤프트럭" +], +"규격": [ +4.5 +], +"시간": 0.011492529855593864, +"밑시간": 0.011492529855593864, +"율": [], +"절": "3", +"글": "덤프트럭 4.5톤" +} +], +"자재": [], +"율": [], +"계수": { +"q": 0.4, +"K": 0.55, +"Cm": 18.0, +"E": 0.45, +"L": 0.5, +"k": 0.7, +"t3": 0.8, +"t4": 0.42 +}, +"조건": { +"방법": [ +"기계", +"인력", +"장비" +] +}, +"옛판": false, +"글": [ +"야면석 채집 운반 (25x25x35㎝)장비 / M2", +"야면석 채집 운반", +"1) 채집 : (기계:인력=8:2)", +"①기계 : B/H 0.4㎥(80%) 건설표준품셈 9-3", +"q = 0.4 , f = 1/ 1.15 = 0.87 , K = 0.55", +"Cm = 18 sec(135˚) , E = 0.45", +"Q = 3600*q*K*f*E/Cm = 17.23 m3/hr", +"노 무 비 : 55,700 / Q * 0.8 * 575/1750 = 849.7", +"재 료 비 : 15,375 / Q * 0.8 * 575/1750 = 234.5", +"경 비 : 16,378 / Q * 0.8 * 575/1750 = 249.8", +"소계", +"②인력(20%) (건설표준품셈 8-2)", +"인 부: 0.17 인/㎡ *165,545* 0.2 = 5,628.5", +"소계", +"계", +"2) 적사 : B/H 0.4㎥ (건설표준품셈 9-3)", +"q = 0.4 , f = 1/1.15 = 0.87 , K = 0.7", +"Cm = 18 sec(135˚) , E = 0.45", +"Q = 3600*q*K*f*E/Cm = 21.92 m3/hr", +"노 무 비 : 55,700 / Q * 575/1750 = 834.9", +"재 료 비 : 15,375 / Q * 575/1750 = 230.4", +"경 비 : 16,378 / Q * 575/1750 = 245.4", +"소계", +"3) 운반 (덤프4.5Ton) 건설표준품셈 9-7", +"L=0.5 Km , k=0.7, E=0.9, F=1/1.15 = 0.87 , q=1 ㎡", +"q1 =(4.5 / 0.575) * 1.15 = 9.00", +"n =q1 / (q * k) = 12.86 회", +"t1 =Cm * n / (60 * 0.45) = 8.57 분", +"t2 =(L/10+L/15)* 60 = 5.00 분", +"t3=0.8 분 ,t4=0.42 분", +"Cm = t1 + t2 + t3 + t4 = 14.79", +"Q = 60 * q1 * F * E / Cm = 28.59 m3/hr", +"노 무 비 : 47,231 / Q * 575/1750 = 542.8", +"재 료 비 : 8,783 / Q * 575/1750 = 100.9", +"경 비 : 7,202 / Q * 575/1750 = 82.7", +"소계", +"계" +], +"절": { +"1": { +"이름": "채집 : (기계:인력=8:2)", +"출처": [] +}, +"1.1": { +"이름": "채집 : (기계:인력=8:2) · 기계 : B/H 0.4㎥(80%) 건설표준품셈 9-3", +"출처": [ +[ +"건설품셈", +"9-3" +] +] +}, +"1.2": { +"이름": "채집 : (기계:인력=8:2) · 인력(20%) (건설표준품셈 8-2)", +"출처": [ +[ +"건설품셈", +"8-2" +] +] +}, +"2": { +"이름": "적사 : B/H 0.4㎥ (건설표준품셈 9-3)", +"출처": [ +[ +"건설품셈", +"9-3" +] +] +}, +"3": { +"이름": "운반 (덤프4.5Ton) 건설표준품셈 9-7", +"출처": [ +[ +"건설품셈", +"9-7" +] +] +} +}, +"이름조건": { +"방법": [ +"장비" +] +}, +"정답": [ +"GF000214" +], +"허용": [], +"무리": "사람", +"복합": null +}, +{ +"id": "Y014", +"파일": "동부청영월", +"이름": "야면석찰붙임", +"규격": "뒷길이 35Cm", +"단위": "M2", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"7-2-2" +] +], +"직종근거": [], +"인력": { +"석공": { +"품": 0.05, +"밑품": 0.05, +"율": [], +"어떻게": [ +"QTY" +] +}, +"보통인부": { +"품": 0.02, +"밑품": 0.02, +"율": [], +"어떻게": [ +"QTY" +] +} +}, +"인력절": [ +[ +"1", +"석공", +0.05, +0.05 +], +[ +"1", +"보통인부", +0.02, +0.02 +] +], +"장비": [ +{ +"갈래": [ +"굴착기" +], +"규격": [ +0.6 +], +"시간": 0.12, +"밑시간": 0.12, +"율": [], +"절": "2", +"글": "굴삭기(타이어) 0.6㎥" +} +], +"자재": [], +"율": [], +"계수": {}, +"조건": { +"메찰": [ +"찰붙임" +] +}, +"옛판": false, +"글": [ +"야면석찰붙임 뒷길이 35Cm / M2", +"ㅇ 야면석찰붙임 (건설표준품셈7-2-2)", +"1. 노무비", +"석 공 :", +"1 ㎡ * 0.05 인/㎡ * 258,935 = 12,946.7", +"보통인부 :", +"1 ㎡ * 0.02 인/㎡ * 165,545 = 3,310.9", +"소계", +"2. 경비(06M3)", +"Q = 0.12 시간/㎡", +"노무비: 55,700 * Q = 6,684.0", +"재료비: 18,310 * Q = 2,197.2", +"경 비: 25,680 * Q = 3,081.6", +"소계", +"계" +], +"절": { +"1": { +"이름": "노무비", +"출처": [] +}, +"2": { +"이름": "경비(06M3)", +"출처": [] +} +}, +"이름조건": { +"메찰": [ +"찰붙임" +] +}, +"정답": [ +"GC000263" +], +"허용": [ +"GF000225" +], +"무리": "사람", +"복합": null +}, +{ +"id": "Y015", +"파일": "동부청영월", +"이름": "고임돌채집운반", +"규격": "4.5톤덤프(L=500)", +"단위": "M3", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"9-3" +], +[ +"건설품셈", +"9-7" +] +], +"직종근거": [], +"인력": { +"보통인부": { +"품": 0.03, +"밑품": 0.3, +"율": [ +[ +0.1, +"섞음" +] +], +"어떻게": [ +"QTY" +] +} +}, +"인력절": [ +[ +"1.2", +"보통인부", +0.03, +0.3 +] +], +"장비": [ +{ +"갈래": [ +"굴착기" +], +"규격": [ +0.4 +], +"시간": 0.0522344747533372, +"밑시간": 0.05803830528148578, +"율": [ +[ +0.9, +"섞음" +] +], +"절": "1.1", +"글": "굴삭기(무한궤도) 0.4㎥" +}, +{ +"갈래": [ +"굴착기" +], +"규격": [ +0.4 +], +"시간": 0.04562043795620438, +"밑시간": 0.04562043795620438, +"율": [], +"절": "2", +"글": "굴삭기(무한궤도) 0.4㎥" +}, +{ +"갈래": [ +"덤프트럭" +], +"규격": [ +4.5 +], +"시간": 0.09541984732824427, +"밑시간": 0.09541984732824427, +"율": [], +"절": "3", +"글": "덤프트럭 4.5톤" +} +], +"자재": [], +"율": [], +"계수": { +"L": 500.0, +"q": 0.4, +"K": 0.55, +"Cm": 18.0, +"E": 0.45, +"k": 0.7, +"t3": 0.8, +"t4": 0.42 +}, +"조건": { +"방법": [ +"기계", +"인력" +] +}, +"옛판": false, +"글": [ +"고임돌채집운반 4.5톤덤프(L=500) / M3", +"고임돌 / m3", +"1) 채집 : (기계:인력=9:1)", +"①기계 : B/H 0.4㎥(90%) 건설표준품셈 9-3", +"q = 0.4 , f = 1/1.15 = 0.87 , K = 0.55", +"Cm = 18 sec(135˚) , E = 0.45", +"Q = 3600*q*K*f*E/Cm = 17.23 m3/hr", +"노 무 비 : 55,700 / Q * 0.9= 2,909.4", +"재 료 비 : 15,375 / Q * 0.9= 803.1", +"경 비 : 16,378 / Q * 0.9= 855.4", +"소계", +"②인력(10%) 사방품셈 P45", +"인 부: 0.3 인/㎥ *165,545* 0.1= 4,966.3", +"소계", +"2) 적사 : B/H 0.4㎥ (건설표준품셈 9-3)", +"q = 0.4 , f = 1/1.15 = 0.87 , K = 0.7", +"Cm = 18 sec(135˚) , E = 0.45", +"Q = 3600*q*K*f*E/Cm = 21.92 m3/hr", +"노 무 비 : 55,700 / Q = 2,541.0", +"재 료 비 : 15,375 / Q = 701.4", +"경 비 : 16,378 / Q = 747.1", +"소계", +"3) 운반 (덤프4.5Ton) 건설표준품셈 9-7", +"L=0.5 Km , k=0.7, E=0.9, F=1/1.15 = 0.87", +"q1 =(4.5/1.75)*1.15 = 2.96", +"n =q1 / (0.4 * k) = 10.57 회", +"t1 =Cm * n / (60 * 0.45) = 7.05 분", +"t2 =(L/10+L/15)* 60 = 5.00 분", +"t3=0.8 분 ,t4=0.42 분", +"Cm = t1 + t2 + t3 + t4 = 13.27", +"Q = 60 * q1 * F * E / Cm = 10.48 m3/hr", +"노 무 비 : 47,231 / Q = 4,506.7", +"재 료 비 : 8,783 / Q = 838.0", +"경 비 : 7,202 / Q = 687.2", +"소계", +"계" +], +"절": { +"1": { +"이름": "채집 : (기계:인력=9:1)", +"출처": [] +}, +"1.1": { +"이름": "채집 : (기계:인력=9:1) · 기계 : B/H 0.4㎥(90%) 건설표준품셈 9-3", +"출처": [ +[ +"건설품셈", +"9-3" +] +] +}, +"1.2": { +"이름": "채집 : (기계:인력=9:1) · 인력(10%) 사방품셈 P45", +"출처": [] +}, +"2": { +"이름": "적사 : B/H 0.4㎥ (건설표준품셈 9-3)", +"출처": [ +[ +"건설품셈", +"9-3" +] +] +}, +"3": { +"이름": "운반 (덤프4.5Ton) 건설표준품셈 9-7", +"출처": [ +[ +"건설품셈", +"9-7" +] +] +} +}, +"이름조건": {}, +"정답": [ +"GF000211" +], +"허용": [ +"GF000212" +], +"무리": "사람", +"복합": null +}, +{ +"id": "Y016", +"파일": "동부청영월", +"이름": "잡석운반및다짐", +"규격": "", +"단위": "M3", +"갈래": "단가산출", +"출처": [ +[ +"산림품셈", +"5-5" +] +], +"직종근거": [], +"인력": { +"보통인부": { +"품": 0.018, +"밑품": 0.018, +"율": [], +"어떻게": [ +"QTY" +] +} +}, +"인력절": [ +[ +"4", +"보통인부", +0.018, +0.018 +] +], +"장비": [ +{ +"갈래": [ +"굴착기" +], +"규격": [ +0.7 +], +"시간": 0.03710575139146568, +"밑시간": 0.03710575139146568, +"율": [], +"절": "1", +"글": "굴삭기(무한궤도) 0.7㎥" +}, +{ +"갈래": [ +"굴착기" +], +"규격": [ +0.7 +], +"시간": 0.03710575139146568, +"밑시간": 0.03710575139146568, +"율": [], +"절": "2", +"글": "굴삭기(무한궤도) 0.7㎥" +}, +{ +"갈래": [ +"덤프트럭" +], +"규격": [ +15.0 +], +"시간": 0.0931098696461825, +"밑시간": 0.0931098696461825, +"율": [], +"절": "3", +"글": "덤프트럭 15톤" +}, +{ +"갈래": [ +"굴착기" +], +"규격": [ +0.2 +], +"시간": 0.07, +"밑시간": 0.07, +"율": [], +"절": "4", +"글": "굴삭기(무한궤도) 0.2㎥" +} +], +"자재": [], +"율": [], +"계수": { +"q": 0.7, +"f": 1.0, +"K": 0.55, +"Cm": 18.0, +"E": 0.35, +"L": 1.0, +"k": 0.55, +"t3": 1.1, +"t4": 0.9 +}, +"조건": {}, +"옛판": true, +"글": [ +"잡석운반및다짐 / M3", +"잡석 채집, 운반, 다짐 [임도표준품셈5-5 준용]", +"작업조건 : 파쇄암중에 뒷채움돌 채집(㎥)", +"1. 잡석 채집", +"1) 기계(굴삭기0.7㎥,100%)", +"q = 0.7 , f =1 , K = 0.55", +"Cm = 18 sec(90˚) , E = 0.35", +"Q = 3600 * q * K * f * E / Cm = 26.95 ㎥/hr", +"노 무 비 : 55,700 / Q = 2,066.7", +"재 료 비 : 18,015 / Q = 668.4", +"경 비 : 23,128 / Q = 858.1", +"계", +"2. 적사(굴삭기0.7㎥)", +"q = 0.7 , f = 1 , K = 0.55", +"Cm = 18 sec(90˚) , E = 0.35", +"Q = 3600 * q * K * f * E / Cm = 26.95 ㎥/hr", +"노 무 비 : 55,700 / Q = 2,066.7", +"재 료 비 : 18,015 / Q = 668.4", +"경 비 : 23,128 / Q = 858.1", +"계", +"3. 운반 (덤프15Ton)", +"L = 1 Km ,k = 0.55, E = 0.9, F = 1", +"q1 = (15/1.75)*1 = 8.57", +"n = q1 / (0.7 * k) = 22.26 회", +"t1 = 18 * n / (60 * 0.35) = 19.08 분", +"t2 = (L/5+L/6)* 60 = 22.00 분", +"t3 = 1.1 분 , t4 = 0.9 분", +"Cm = t1 + t2 + t3 + t4 = 43.08", +"Q = 60 * q1 * F * E / Cm = 10.74 ㎥/hr", +"노 무 비 : 55,700 / Q = 5,186.2", +"재 료 비 : 27,932 / Q = 2,600.7", +"경 비 : 19,631 / Q = 1,827.8", +"계", +"4. 채움 및 다짐", +"기계 : 굴삭기 0.2㎥", +"Q = 0.07 hr", +"노 무 비 : 55,700 * Q = 3,899.0", +"재 료 비 : 7,701 * Q = 539.0", +"경 비 : 13,041 * Q = 912.8", +"소계", +"인력", +"인 부: 0.018 인/㎥ *165,545 = 2,979.8", +"소계", +"계", +"합계" +], +"절": { +"1": { +"이름": "기계(굴삭기0.7㎥,100%)", +"출처": [] +}, +"2": { +"이름": "적사(굴삭기0.7㎥)", +"출처": [] +}, +"3": { +"이름": "운반 (덤프15Ton)", +"출처": [] +}, +"4": { +"이름": "채움 및 다짐", +"출처": [] +} +}, +"이름조건": {}, +"정답": [ +"GF000218" +], +"허용": [], +"무리": "사람", +"복합": null +}, +{ +"id": "Y017", +"파일": "동부청영월", +"이름": "면고르기", +"규격": "", +"단위": "M2", +"갈래": "단가산출", +"출처": [], +"직종근거": [], +"인력": { +"보통인부": { +"품": 0.009, +"밑품": 0.009, +"율": [], +"어떻게": [ +"QTY" +] +} +}, +"인력절": [ +[ +"1", +"보통인부", +0.009, +0.009 +] +], +"장비": [ +{ +"갈래": [ +"굴착기" +], +"규격": [ +0.7 +], +"시간": 0.021, +"밑시간": 0.021, +"율": [], +"절": "2", +"글": "굴삭기(무한궤도) 0.7㎥" +} +], +"자재": [], +"율": [], +"계수": {}, +"조건": { +"난이": [ +"보통" +], +"방법": [ +"인력" +] +}, +"옛판": false, +"글": [ +"면고르기 / M2", +"○.면 고르기", +"1.보통인부 0.009인", +"인력 0.009*165,545= 1,489.9 원/M2", +"소계", +"2.굴삭기(무한궤도) 0.7M3", +"X = 0.021 hr", +"노 무 비 : 55,700 * X = 1,169.7", +"재 료 비 : 18,015 * X = 378.3", +"경 비 : 23,128 * X = 485.6", +"소계", +"계" +], +"절": { +"1": { +"이름": "보통인부 0.009인", +"출처": [] +}, +"2": { +"이름": "굴삭기(무한궤도) 0.7M3", +"출처": [] +} +}, +"이름조건": {}, +"정답": [ +"GF000124" +], +"허용": [ +"GC000111" +], +"무리": "사람", +"복합": null +}, +{ +"id": "Y018", +"파일": "동부청영월", +"이름": "구조물터파기", +"규격": "인력(경질토사)", +"단위": "M3", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"3-1-3" +] +], +"직종근거": [ +"보통인부" +], +"인력": { +"보통인부": { +"품": 0.26, +"밑품": 0.26, +"율": [], +"어떻게": [ +"QTY" +] +} +}, +"인력절": [ +[ +"0", +"보통인부", +0.26, +0.26 +] +], +"장비": [], +"자재": [], +"율": [], +"계수": {}, +"조건": { +"토질": [ +"경질토사" +], +"방법": [ +"인력" +] +}, +"옛판": false, +"글": [ +"구조물터파기 인력(경질토사) / M3", +"구조물 터파기 (인력) 품셈3-1-3", +"○.인 부 :", +"0.26 * 165,545 = 43,041.7", +"소계" +], +"절": { +"0": { +"이름": "26 * 165,545 = 43,041.7", +"출처": [] +} +}, +"이름조건": { +"토질": [ +"경질토사" +], +"방법": [ +"인력" +] +}, +"정답": [ +"GF000101" +], +"허용": [ +"GC000083" +], +"무리": "사람", +"복합": null +}, +{ +"id": "Y019", +"파일": "동부청영월", +"이름": "도로반사경설치", +"규격": "D=1000(STS)", +"단위": "개소", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"1-9-3" +] +], +"직종근거": [], +"인력": { +"특별인부": { +"품": 0.25, +"밑품": 0.25, +"율": [], +"어떻게": [ +"QTY" +] +}, +"보통인부": { +"품": 0.25, +"밑품": 0.25, +"율": [], +"어떻게": [ +"QTY" +] +} +}, +"인력절": [ +[ +"2", +"특별인부", +0.25, +0.25 +], +[ +"2", +"보통인부", +0.25, +0.25 +] +], +"장비": [], +"자재": [ +{ +"이름": "도로반사경(지주포함) D=1000mm(STS)", +"품": 1.0, +"밑품": 1.0 +} +], +"율": [], +"계수": {}, +"조건": {}, +"옛판": false, +"글": [ +"도로반사경설치 D=1000(STS) / 개소", +"도로반사경설치(D=1000mm 표준품셈 1-9-3)", +"1. 재 료 비", +"-도로반사경(D=1000mm STS 지주포함) : 288,000 * 1 EA = 288,000.0", +"소계", +"2. 인 건 비", +"- 특별인부 : 1 인 / 4 본/일 * 214,222 = 53,555.5", +"- 보통인부 : 1 인 / 4 본/일 * 165,545 = 41,386.2", +"소계", +"3. 공구손료 및 경장비", +"공구손료 및 경장비: 94,941.7 * 3 % = 2,848.2", +"소계", +"계" +], +"절": { +"1": { +"이름": "재 료 비", +"출처": [] +}, +"2": { +"이름": "인 건 비", +"출처": [] +}, +"3": { +"이름": "공구손료 및 경장비", +"출처": [] +} +}, +"이름조건": {}, +"정답": [ +"GC000310" +], +"허용": [], +"무리": "사람", +"복합": null +}, +{ +"id": "Y020", +"파일": "동부청영월", +"이름": "철근콘크리트타설", +"규격": "장비사용타설", +"단위": "M3", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"6-1-1" +] +], +"직종근거": [], +"인력": { +"콘크리트공": { +"품": 0.054545, +"밑품": 0.054545, +"율": [], +"어떻게": [ +"QTY" +] +}, +"보통인부": { +"품": 0.018182, +"밑품": 0.018182, +"율": [], +"어떻게": [ +"QTY" +] +} +}, +"인력절": [ +[ +"1", +"콘크리트공", +0.054545, +0.054545 +], +[ +"1", +"보통인부", +0.018182, +0.018182 +] +], +"장비": [ +{ +"갈래": [ +"굴착기" +], +"규격": [ +0.6 +], +"시간": 0.14534883720930233, +"밑시간": 0.14534883720930233, +"율": [], +"절": "1", +"글": "굴삭기(타이어) 0.6㎥" +} +], +"자재": [], +"율": [], +"계수": {}, +"조건": { +"근": [ +"철근" +], +"방법": [ +"인력", +"장비" +] +}, +"옛판": false, +"글": [ +"철근콘크리트타설 장비사용타설 / M3", +"철근콘크리트타설(06㎥) 24년 건설표준품셈6-1-1", +"1. 인 력", +"콘크리트공 : 3 인 * 1 일 / 55 ㎥ * 261,283 = 14,251.8", +"보통인부 : 1 인 * 1 일 / 55 ㎥ * 165,545 = 3,009.9", +"공구및경장비손료 : 17,261.7 * 2 % = 345.2", +"소계", +"2 굴삭기(0.6㎥)", +"Q = 1 일 / 8 시간 * 55 ㎥ = 6.88 ㎥/시간", +"노무비: 55,700 / Q = 8,095.9", +"재료비: 18,310 / Q = 2,661.3", +"경 비: 25,680 / Q = 3,732.5", +"소계", +"계" +], +"절": { +"1": { +"이름": "인 력", +"출처": [] +} +}, +"이름조건": { +"근": [ +"철근" +], +"방법": [ +"장비" +] +}, +"정답": [ +"GC000198" +], +"허용": [ +"GF000155" +], +"무리": "사람", +"복합": null +}, +{ +"id": "Y021", +"파일": "동부청영월", +"이름": "유로폼 설치,해체(보통)", +"규격": "수직고7.0m 이하", +"단위": "M2", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"6-3-3" +] +], +"직종근거": [], +"인력": { +"형틀목공": { +"품": 0.114286, +"밑품": 0.114286, +"율": [], +"어떻게": [ +"QTY" +] +}, +"보통인부": { +"품": 0.028571, +"밑품": 0.028571, +"율": [], +"어떻게": [ +"QTY" +] +} +}, +"인력절": [ +[ +"2", +"형틀목공", +0.114286, +0.114286 +], +[ +"2", +"보통인부", +0.028571, +0.028571 +] +], +"장비": [], +"자재": [ +{ +"이름": "유로폼(패널) 600*1200", +"품": 0.089, +"밑품": 0.089 +}, +{ +"이름": "내부코너패널 (200+200)*1200", +"품": 0.003, +"밑품": 0.003 +} +], +"율": [ +[ +"인력품", +3.0 +] +], +"계수": {}, +"조건": { +"난이": [ +"보통" +] +}, +"옛판": false, +"글": [ +"유로폼 설치,해체(보통) 수직고7.0m 이하 / M2", +"유로폼 설치,해체(보통) 건설표준품셈품셈 6-3-3", +"1. 재 료 비", +"패널(600*1200) : 0.089 매 * 29,000 = 2,581.0", +"내부코너패널((200+200)*1200) : 0.003 매 * 18,700 = 56.1", +"부자재비 : 2,637.1 * 52 % = 1,371.2", +"소모자재 : 2,637.1 * 5 % = 131.8", +"소계", +"2. 설치 해체비", +"형틀목공 : 4 인 * 1 일 / 35 ㎡ * 274,978 = 31,426.0", +"보통인부 : 1 인 * 1 일 / 35 ㎡ * 165,545 = 4,729.8", +"소계", +"3. 공구손료(인력품의 3%)", +"공구손료 : 36,155.8 * 3 % = 1,084.6", +"소계", +"계" +], +"절": { +"1": { +"이름": "재 료 비", +"출처": [] +}, +"2": { +"이름": "설치 해체비", +"출처": [] +}, +"3": { +"이름": "공구손료(인력품의 3%)", +"출처": [] +} +}, +"이름조건": { +"난이": [ +"보통" +] +}, +"정답": [ +"GC000214" +], +"허용": [ +"GF000210" +], +"무리": "사람", +"복합": null +}, +{ +"id": "Y022", +"파일": "동부청영월", +"이름": "지수판 설치(PVC 용접)", +"규격": "200×5t", +"단위": "m", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"6-3-9" +] +], +"직종근거": [], +"인력": { +"특별인부": { +"품": 0.151, +"밑품": 0.151, +"율": [], +"어떻게": [ +"QTY" +] +}, +"보통인부": { +"품": 0.116, +"밑품": 0.116, +"율": [], +"어떻게": [ +"QTY" +] +} +}, +"인력절": [ +[ +"1", +"특별인부", +0.151, +0.151 +], +[ +"2", +"보통인부", +0.116, +0.116 +] +], +"장비": [], +"자재": [ +{ +"이름": "PVC지수판 200x5T KSM3805", +"품": 1.04, +"밑품": 1.04 +}, +{ +"이름": "PVC용접봉 Ø3.2 (국산)", +"품": 0.042, +"밑품": 0.042 +}, +{ +"이름": "어닐링철선(결속선) #20 0.9M/M", +"품": 0.21, +"밑품": 0.21 +} +], +"율": [ +[ +"인건비", +3.0 +] +], +"계수": {}, +"조건": {}, +"옛판": false, +"글": [ +"지수판 설치(PVC 용접) 200×5t / m", +"<<공통품셈 6-3-9>>", +"1. 재 료 비", +"1) P.V.C 지수판 (200 M/M * 5 M/M)", +": 2,600 > * 1.04 = 2,704.0", +"2) PVC 용접봉 : 0.042 > * 5,000 = 210.0", +"3) 결속선 : 0.21 > * 1,760 = 369.6", +"소계", +"2.설 치 비", +"1) 특별인부 : 0.151 <인>> * 214,222 = 32,347.5", +"2) 보통인부 : 0.116 <인>> * 165,545 = 19,203.2", +"소계", +"3. 기구손료 (인건비의 3%)", +"51,550.7 * 0.03 = 1,546.5", +"소계", +"계" +], +"절": { +"1": { +"이름": "특별인부 : 0.151 <인>> * 214,222 = 32,347.5", +"출처": [] +}, +"2": { +"이름": "보통인부 : 0.116 <인>> * 165,545 = 19,203.2", +"출처": [] +}, +"3": { +"이름": "기구손료 (인건비의 3%)", +"출처": [] +} +}, +"이름조건": {}, +"정답": [ +"GC000224" +], +"허용": [], +"무리": "사람", +"복합": null +}, +{ +"id": "Y023", +"파일": "동부청영월", +"이름": "배수관부설및접합(P.V.C)", +"규격": "Ø 50 MM - VG2 일반관", +"단위": "m", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"6-4-2" +] +], +"직종근거": [], +"인력": { +"배관공(수도)": { +"품": 0.01, +"밑품": 0.01, +"율": [], +"어떻게": [ +"QTY" +] +}, +"보통인부": { +"품": 0.005, +"밑품": 0.005, +"율": [], +"어떻게": [ +"QTY" +] +} +}, +"인력절": [ +[ +"2", +"배관공(수도)", +0.01, +0.01 +], +[ +"2", +"보통인부", +0.005, +0.005 +] +], +"장비": [], +"자재": [ +{ +"이름": "PVC파이프(일반용VG2) 50㎜ × 4m", +"품": 1.04, +"밑품": 1.04 +} +], +"율": [], +"계수": {}, +"조건": {}, +"옛판": false, +"글": [ +"배수관부설및접합(P.V.C) Ø 50 MM - VG2 일반관 / m", +"<<토목품셈 6-4-2>>", +"1. 재 료 비", +"P.V.C관(D50) : 1.04 m * 1,410 = 1,466.4", +"소계", +"2. 설치비", +"배관공(수도) : 0.04 인 * 243,168 / 4 = 2,431.6", +"보통인부 : 0.02 인 * 165,545 / 4 = 827.7", +"소계", +"계" +], +"절": { +"1": { +"이름": "재 료 비", +"출처": [] +}, +"2": { +"이름": "설치비", +"출처": [] +} +}, +"이름조건": {}, +"정답": [ +"GC000393" +], +"허용": [], +"무리": "사람", +"복합": null +}, +{ +"id": "Y024", +"파일": "동부청영월", +"이름": "야면석소운반(L=35CM)", +"규격": "(L=20m)", +"단위": "M2", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"9-1" +] +], +"직종근거": [], +"인력": { +"보통인부": { +"품": 0.163666, +"밑품": 0.163666, +"율": [], +"어떻게": [ +"QTY" +] +} +}, +"인력절": [ +[ +"0", +"보통인부", +0.163666, +0.163666 +] +], +"장비": [], +"자재": [], +"율": [], +"계수": { +"L": 35.0, +"t1": 2.0, +"T": 450.0, +"q": 25.0 +}, +"조건": { +"횟수": [ +"3회" +] +}, +"옛판": true, +"글": [ +"야면석소운반(L=35CM) (L=20m) / M2", +"ㅇ 야면석 소운반(건설표준품셈 9-1)", +"L=20 , V=2000 , t1=2 , T=450 , q=25", +"N = (V*T)/(120*L+V*t1) = 140.63 회/일", +"Q = N * q / 575 ㎏/㎡ = 6.11 ㎡/일", +"보통인부 165,545 / Q = 27,094.1", +"소계" +], +"절": {}, +"이름조건": {}, +"정답": [ +"GF000280" +], +"허용": [ +"GF000279" +], +"무리": "사람", +"복합": null +}, +{ +"id": "Y025", +"파일": "동부청영월", +"이름": "콘크리트소운반", +"규격": "(L=20m)", +"단위": "M3", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"9-1" +] +], +"직종근거": [], +"인력": { +"보통인부": { +"품": 0.653595, +"밑품": 0.653595, +"율": [], +"어떻게": [ +"QTY" +] +} +}, +"인력절": [ +[ +"0", +"보통인부", +0.653595, +0.653595 +] +], +"장비": [], +"자재": [], +"율": [], +"계수": { +"L": 20.0, +"t1": 2.0, +"T": 450.0, +"q": 25.0 +}, +"조건": { +"횟수": [ +"3회" +] +}, +"옛판": true, +"글": [ +"콘크리트소운반 (L=20m) / M3", +"ㅇ 콘크리트 소운반(건설표준품셈 9-1)", +"L=20 , V=2000 , t1=2 , T=450 , q=25", +"N = (V*T)/(120*L+V*t1) = 140.63 회/일", +"Q = N * q / 2300 ㎏/㎥ = 1.53 ㎥/일", +"보통인부 165,545 / Q = 108,199.3", +"소계" +], +"절": {}, +"이름조건": {}, +"정답": [ +"GF000279", +"GF000281" +], +"허용": [], +"무리": "사람", +"복합": null +}, +{ +"id": "Y026", +"파일": "동부청영월", +"이름": "야면석메붙임", +"규격": "뒷길이 35Cm", +"단위": "M2", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"7-2-1" +] +], +"직종근거": [], +"인력": { +"석공": { +"품": 0.13, +"밑품": 0.13, +"율": [], +"어떻게": [ +"QTY" +] +}, +"보통인부": { +"품": 0.04, +"밑품": 0.04, +"율": [], +"어떻게": [ +"QTY" +] +} +}, +"인력절": [ +[ +"1", +"석공", +0.13, +0.13 +], +[ +"1", +"보통인부", +0.04, +0.04 +] +], +"장비": [ +{ +"갈래": [ +"굴착기" +], +"규격": [ +0.6 +], +"시간": 0.25, +"밑시간": 0.25, +"율": [], +"절": "2", +"글": "굴삭기(타이어) 0.6㎥" +} +], +"자재": [], +"율": [], +"계수": {}, +"조건": { +"메찰": [ +"메붙임", +"찰붙임" +] +}, +"옛판": false, +"글": [ +"야면석메붙임 뒷길이 35Cm / M2", +"ㅇ 야면석찰붙임 (건설표준품셈7-2-1)", +"1. 노무비", +"석 공 :", +"1 ㎡ * 0.13 인/㎡ * 258,935 = 33,661.5", +"보통인부 :", +"1 ㎡ * 0.04 인/㎡ * 165,545 = 6,621.8", +"소계", +"2. 경비(06M3)", +"Q = 0.25 시간/㎡", +"노무비: 55,700 * Q = 13,925.0", +"재료비: 18,310 * Q = 4,577.5", +"경 비: 25,680 * Q = 6,420.0", +"소계", +"계" +], +"절": { +"1": { +"이름": "노무비", +"출처": [] +}, +"2": { +"이름": "경비(06M3)", +"출처": [] +} +}, +"이름조건": { +"메찰": [ +"메붙임" +] +}, +"정답": [ +"GF000224", +"GC000262" +], +"허용": [], +"무리": "사람", +"복합": null +}, +{ +"id": "Y027", +"파일": "동부청영월", +"이름": "측구수로관부설", +"규격": "500∼700kg미만", +"단위": "m", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"6-7-1" +] +], +"직종근거": [], +"인력": { +"특별인부": { +"품": 0.025, +"밑품": 0.025, +"율": [], +"어떻게": [ +"QTY" +] +}, +"보통인부": { +"품": 0.018, +"밑품": 0.018, +"율": [], +"어떻게": [ +"QTY" +] +} +}, +"인력절": [ +[ +"2", +"특별인부", +0.025, +0.025 +], +[ +"2", +"보통인부", +0.018, +0.018 +] +], +"장비": [ +{ +"갈래": [ +"크레인" +], +"규격": [ +10.0 +], +"시간": 0.09, +"밑시간": 0.09, +"율": [], +"절": "3", +"글": "크레인(타이어) 10톤" +} +], +"자재": [], +"율": [ +[ +"인력품", +8.0 +] +], +"계수": {}, +"조건": {}, +"옛판": false, +"글": [ +"측구수로관부설 500∼700kg미만 / m", +"<<공통품셈 6-7-1>>", +"1. 플륨관 : 1.0 M - 별 산 -", +"2. 인건비", +"특별인부 : (0.05 인 /2 m )* 214,222 = 5,355.5", +"보통인부 : (0.036 인 /2 m )* 165,545 = 2,979.8", +"소계", +"3. 기계경비(트럭크레인 10ton)", +"노 무 비: (0.18/2 m )*55,700 = 5,013.0", +"경 비: (0.18/2 m )*30,103 = 2,709.2", +"재 료 비: (0.18/2 m )*6,723 = 605.0", +"소계", +"4. 공구손료 및 소모재료 (인력품의 8%)", +"8,335.3 * 0.08 = 666.8", +"소계", +"계" +], +"절": { +"1": { +"이름": "플륨관 : 1.0 M - 별 산 -", +"출처": [] +}, +"2": { +"이름": "인건비", +"출처": [] +}, +"3": { +"이름": "기계경비(트럭크레인 10ton)", +"출처": [] +}, +"4": { +"이름": "공구손료 및 소모재료 (인력품의 8%)", +"출처": [] +} +}, +"이름조건": {}, +"정답": [ +"GC000251" +], +"허용": [], +"무리": "사람", +"복합": null +}, +{ +"id": "Y028", +"파일": "동부청영월", +"이름": "콘크리트타설", +"규격": "장비사용타설(소형)", +"단위": "M3", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"6-1-1" +] +], +"직종근거": [], +"인력": { +"콘크리트공": { +"품": 0.09, +"밑품": 0.09, +"율": [], +"어떻게": [ +"QTY" +] +}, +"보통인부": { +"품": 0.02, +"밑품": 0.02, +"율": [], +"어떻게": [ +"QTY" +] +} +}, +"인력절": [ +[ +"1", +"콘크리트공", +0.09, +0.09 +], +[ +"1", +"보통인부", +0.02, +0.02 +] +], +"장비": [ +{ +"갈래": [ +"굴착기" +], +"규격": [ +0.6 +], +"시간": 0.31, +"밑시간": 0.31, +"율": [], +"절": "1", +"글": "굴삭기(타이어) 0.6㎥" +} +], +"자재": [], +"율": [], +"계수": {}, +"조건": { +"근": [ +"철근" +], +"규모": [ +"소형" +], +"방법": [ +"인력", +"장비" +] +}, +"옛판": false, +"글": [ +"콘크리트타설 장비사용타설(소형) / M3", +"철근콘크리트타설(06㎥)품셈6-1-1", +"1. 인 력", +"콘크리트공 : 0.09 인/㎥ * 261,283 = 23,515.4", +"보통인부 : 0.02 인/㎥ * 165,545 = 3,310.9", +"공구및경장비손료 : 26,826.3 * 2 % = 536.5", +"소계", +"2 굴삭기(0.6㎥)", +"Q = 0.31 Hr/㎥", +"노무비: 55,700 * Q = 17,267.0", +"재료비: 18,310 * Q = 5,676.1", +"경 비: 25,680 * Q = 7,960.8", +"소계", +"계" +], +"절": { +"1": { +"이름": "인 력", +"출처": [] +} +}, +"이름조건": { +"규모": [ +"소형" +], +"방법": [ +"장비" +] +}, +"정답": [ +"GC000198" +], +"허용": [ +"GF000155" +], +"무리": "사람", +"복합": null +}, +{ +"id": "Y029", +"파일": "동부청영월", +"이름": "합판거푸집(4회)", +"규격": "보통", +"단위": "M2", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"6-3-1" +] +], +"직종근거": [], +"인력": { +"형틀목공": { +"품": 0.111111, +"밑품": 0.111111, +"율": [], +"어떻게": [ +"QTY" +] +}, +"보통인부": { +"품": 0.044444, +"밑품": 0.044444, +"율": [], +"어떻게": [ +"QTY" +] +} +}, +"인력절": [ +[ +"2", +"형틀목공", +0.111111, +0.111111 +], +[ +"2", +"보통인부", +0.044444, +0.044444 +] +], +"장비": [], +"자재": [ +{ +"이름": "내수합판1급 12mmx1220x2440", +"품": 0.3914, +"밑품": 0.3914 +}, +{ +"이름": "각재 (외송)", +"품": 0.01444, +"밑품": 0.01444 +} +], +"율": [ +[ +"인력품", +1.0 +] +], +"계수": {}, +"조건": { +"난이": [ +"보통" +], +"횟수": [ +"4회", +"6회" +] +}, +"옛판": false, +"글": [ +"합판거푸집(4회) 보통 / M2", +"합판거푸집(6회) 24년 건설표준품셈 6-3-1", +"1. 재 료 비", +"합판 : 1.03 ㎡ * 0.38 * 10,986 = 4,299.9", +"각재 (외송) : 0.038 ㎥ * 0.38 * 586,826 = 8,473.7", +"잡재료비 : 12,773.6 * 9 % = 1,149.6", +"소계", +"2. 가공 제작 조립 해체비", +"형틀목공 : 5 인 * 1 일 / 45 ㎡ * 274,978 = 30,553.1", +"보통인부 : 2 인 * 1 일 / 45 ㎡ * 165,545 = 7,357.5", +"소계", +"3. 공구손료(인력품의 1%)", +"공구손료 : 37,910.6 * 1 % = 379.1", +"소계", +"계" +], +"절": { +"1": { +"이름": "재 료 비", +"출처": [] +}, +"2": { +"이름": "가공 제작 조립 해체비", +"출처": [] +}, +"3": { +"이름": "공구손료(인력품의 1%)", +"출처": [] +} +}, +"이름조건": { +"난이": [ +"보통" +], +"횟수": [ +"4회" +] +}, +"정답": [ +"GC000211" +], +"허용": [ +"GF000160" +], +"무리": "사람", +"복합": null +}, +{ +"id": "Y030", +"파일": "동부청영월", +"이름": "혼합골재부설", +"규격": "영월-산불", +"단위": "M3", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"9-7" +] +], +"직종근거": [], +"인력": {}, +"인력절": [], +"장비": [ +{ +"갈래": [ +"덤프트럭" +], +"규격": [ +15.0 +], +"시간": 0.4444444444444444, +"밑시간": 0.4444444444444444, +"율": [], +"절": "3", +"글": "덤프트럭 15톤" +}, +{ +"갈래": [ +"덤프트럭" +], +"규격": [ +15.0 +], +"시간": 0.4444444444444444, +"밑시간": 0.4444444444444444, +"율": [], +"절": "3", +"글": "덤프트럭 자동덮개시설 15톤용" +}, +{ +"갈래": [ +"굴착기" +], +"규격": [ +1.0 +], +"시간": 0.041220115416323165, +"밑시간": 0.041220115416323165, +"율": [], +"절": "4", +"글": "굴삭기(무한궤도) 1.0㎥" +}, +{ +"갈래": [ +"살수차" +], +"규격": [], +"시간": 0.020610057708161583, +"밑시간": 0.020610057708161583, +"율": [], +"절": "5", +"글": "물탱크(살수차) 16000ℓ" +} +], +"자재": [ +{ +"이름": "혼합골재 영월", +"품": 1.04, +"밑품": 1.04 +} +], +"율": [], +"계수": { +"V1": 10.0, +"V2": 35.0, +"q": 1.72, +"K": 1.0, +"L": 8.0, +"t1": 6.0, +"t2": 14.0, +"E": 0.9, +"t3": 0.8, +"t4": 0.42, +"Cm": 2.25, +"f": 1.0 +}, +"조건": {}, +"옛판": false, +"글": [ +"혼합골재부설 영월-산불 / M3", +"혼 합 골 재 부 설(㎥)", +"L1= 3.4, L2= 18, L3= 9.9", +"○ ○ 현 장", +"------> + -----> + ---->", +"골재장 중 점", +"V1=10, V2=35, V3=10", +"V4=15, V5=35, V6=15", +"평균속도(적재): A1=L1/V1+L2/V2+L3/V3= 1.84 시간", +"평균속도(공차): A2=L1/V4+L2/V5+L3/V6= 1.40 시간", +"1. 보조기층", +"1.04 m3 * 11,400 = 11,856.0", +"소계", +"2. 적 사 (로우더1.72m3):무 대", +"q=1.72 , K=1 , F=0.95/1.15= 0.83", +"E0=0.6 , M=1.8 , L=8 , t1=6 , t2=14", +"CM = M*L+t1 +t2 = 34.40", +"Q= 3600*q*K*F*E0/CM= 89.64 M3/HR", +"3. 운 반 (덤프 15Ton) 품셈9-7", +"F=0.95/1.15 = 0.83 ,E=0.9", +"q1 = (15/1.7) * 1.15 = 10.15", +"N =q1 / (1.72 * k) = 5.90 회", +"t1 =CM * N / (60 * 0.6) = 5.64 분", +"t2 = (A1 + A2) * 60 = 194.40 분", +"t3=0.8 분 ,t4=0.42 분 , t5=0.5 분", +"Cm = t1 + t2 + t3 + t4 +t5 = 201.76 분", +"Q = 60 * q1 * F * E / Cm = 2.25 m3/hr", +"노 무 비 : 55,700 / Q = 24,755.5", +"재 료 비 : 27,932 / Q = 12,414.2", +"경 비 : 19,631 / Q = 8,724.8", +"자동 덮개 : 414 / Q = 184.0", +"소계", +"4) 부설 (굴삭기 0.7㎥) 임도 7-2", +"q = 0.7, K=0.55, f=1, E=0.35, Cm=20 (135˚)", +"Q = 3600*q*K*f*E / Cm = 24.26 ㎥/Hr", +"노 무 비 : 55,700 / Q = 2,295.9", +"재 료 비 : 30,284 / Q = 1,248.3", +"경 비 : 27,901 / Q = 1,150.0", +"소계", +"5)살수차(16000L)", +"노무비: 47,231 / Q * 0.5 = 973.4", +"재료비: 21,348 / Q * 0.5 = 439.9", +"경 비: 18,163 / Q * 0.5 = 374.3", +"소계", +"계" +], +"절": { +"1": { +"이름": "04 m3 * 11,400 = 11,856.0", +"출처": [] +}, +"2": { +"이름": "적 사 (로우더1.72m3):무 대", +"출처": [] +}, +"3": { +"이름": "운 반 (덤프 15Ton) 품셈9-7", +"출처": [ +[ +"건설품셈", +"9-7" +] +] +}, +"4": { +"이름": "부설 (굴삭기 0.7㎥) 임도 7-2", +"출처": [] +}, +"5": { +"이름": "살수차(16000L)", +"출처": [] +} +}, +"이름조건": {}, +"정답": [ +"GF000136" +], +"허용": [], +"무리": "사람", +"복합": null +}, +{ +"id": "Y031", +"파일": "동부청영월", +"이름": "콘크리트포장(인력)", +"규격": "T=20㎝", +"단위": "M3", +"갈래": "단가산출", +"출처": [ +[ +"산림품셈", +"7-1-1" +] +], +"직종근거": [], +"인력": { +"포장공": { +"품": 0.03, +"밑품": 0.03, +"율": [], +"어떻게": [ +"QTY" +] +}, +"보통인부": { +"품": 0.03, +"밑품": 0.03, +"율": [], +"어떻게": [ +"QTY" +] +} +}, +"인력절": [ +[ +"1", +"포장공", +0.03, +0.03 +], +[ +"1", +"보통인부", +0.03, +0.03 +] +], +"장비": [], +"자재": [], +"율": [], +"계수": { +"T": 20.0 +}, +"조건": { +"방법": [ +"인력" +] +}, +"옛판": true, +"글": [ +"콘크리트포장(인력) T=20㎝ / M3", +"콘크리트포장(인력) T=20㎝ 임도품셈 7-1-1", +"Ο 콘크리트포장두께 T=0.2m", +"Ο 1일시공량 : A = 100 ㎥/일", +"Ο 작업면적 : Q = 1 / 0.2 = 5.00 ㎡/㎥", +"1. 배치인원", +"포 장 공 : 3 인 / 100 * 258,360 = 7,750.8", +"보통인부 : 3 인 / 100 * 165,545 = 4,966.3", +"소계", +"계" +], +"절": { +"1": { +"이름": "배치인원", +"출처": [] +} +}, +"이름조건": { +"방법": [ +"인력" +] +}, +"정답": [ +"GF000162" +], +"허용": [ +"GC000297" +], +"무리": "사람", +"복합": null +}, +{ +"id": "Y032", +"파일": "동부청영월", +"이름": "콘크리트포장절단공", +"규격": "임도", +"단위": "M", +"갈래": "단가산출", +"출처": [ +[ +"산림품셈", +"7-1-2" +] +], +"직종근거": [], +"인력": { +"보통인부": { +"품": 0.005714, +"밑품": 0.005714, +"율": [], +"어떻게": [ +"QTY" +] +}, +"특별인부": { +"품": 0.002857, +"밑품": 0.002857, +"율": [], +"어떻게": [ +"QTY" +] +} +}, +"인력절": [ +[ +"2", +"보통인부", +0.005714, +0.005714 +], +[ +"2", +"특별인부", +0.002857, +0.002857 +] +], +"장비": [ +{ +"갈래": [ +"커터" +], +"규격": [ +440.0 +], +"시간": 0.022857142857142857, +"밑시간": 0.022857142857142857, +"율": [], +"절": "3", +"글": "커터(콘크리트 및 아스팔트용) 320-440mm" +}, +{ +"갈래": [], +"규격": [ +4.85 +], +"시간": 0.011428571428571429, +"밑시간": 0.011428571428571429, +"율": [], +"절": "3", +"글": "동력분무기 4.85㎾" +}, +{ +"갈래": [ +"살수차" +], +"규격": [], +"시간": 0.0036363636363636364, +"밑시간": 0.0036363636363636364, +"율": [], +"절": "4", +"글": "물탱크(살수차) 5500ℓ" +} +], +"자재": [ +{ +"이름": "블레이드 14"X 3.2", +"품": 0.0031, +"밑품": 0.0031 +} +], +"율": [], +"계수": { +"E": 0.9, +"L": 1.0, +"t1": 5.0, +"t2": 36.0, +"Cm": 8250.0 +}, +"조건": {}, +"옛판": true, +"글": [ +"콘크리트포장절단공 임도 / M", +"콘크리트포장절단공(임도품셈 7-1-2)", +"콘크리트 절단", +"Ο 1일 시공량 : A = 350 m/일", +"Ο 1일 작업량 : Q = 350 m / 8 = 43.75 m/hr", +"Ο 블레이드 : 0.31 EA / 100 m", +"Ο 물운반 : 3000 L / 100 m", +"1. 재 료 비", +"블레이드 : 0.31 EA / 100 m * 169,800 = 526.3", +"소계", +"2. 배 치 인 원", +"보통인부 : 2 인 / A * 165,545 = 945.9", +"특별인부 : 1 인 / A * 214,222 = 612.0", +"소계", +"3. 기계사용료", +"Ο 콘크리트 커터", +"노무비: 33,571 / Q = 767.3", +"재료비: 8,863 / Q = 202.5", +"경 비: 1,876 / Q = 42.8", +"소계", +"Ο 동력분무기", +"경 비: 244 / Q * 0.5 = 2.7", +"소계", +"4. 물 운 반", +"작업시간", +"E = 0.9 , L = 1 , V = 20", +"흡입준비;5분 흡입:10분", +"살수준비: 5분 살수:10분", +"t1=5+10+5+10= 30.00 분", +"t2 =(1/20)*2*60 = 6.00 분", +"Cm = t1 +t2= 36.00 분", +"Q = 60 * 5500 * E / Cm = 8250.00 L/hr", +"노 무 비 : 47,231 / Q *(3000/100)= 171.7", +"재 료 비 : 15,390 / Q *(3000*0.722/100)= 40.4", +"경 비 : 9,470 / Q *(3000/100)= 34.4", +"소계", +"계", +"합계" +], +"절": { +"1": { +"이름": "재 료 비", +"출처": [] +}, +"2": { +"이름": "배 치 인 원", +"출처": [] +}, +"3": { +"이름": "기계사용료", +"출처": [] +}, +"4": { +"이름": "물 운 반", +"출처": [] +} +}, +"이름조건": {}, +"정답": [ +"GF000163" +], +"허용": [], +"무리": "사람", +"복합": null +}, +{ +"id": "Y033", +"파일": "동부청영월", +"이름": "뒷채움돌채집운반부설", +"규격": "4.5톤 덤프트럭(L=500m)", +"단위": "M3", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"9-3" +], +[ +"건설품셈", +"9-7" +], +[ +"건설품셈", +"5-1-1" +] +], +"직종근거": [], +"인력": { +"보통인부": { +"품": 0.03, +"밑품": 0.3, +"율": [ +[ +0.1, +"섞음" +] +], +"어떻게": [ +"QTY" +] +} +}, +"인력절": [ +[ +"1.2", +"보통인부", +0.03, +0.3 +] +], +"장비": [ +{ +"갈래": [ +"굴착기" +], +"규격": [ +0.4 +], +"시간": 0.0522344747533372, +"밑시간": 0.05803830528148578, +"율": [ +[ +0.9, +"섞음" +] +], +"절": "1.1", +"글": "굴삭기(무한궤도) 0.4㎥" +}, +{ +"갈래": [ +"굴착기" +], +"규격": [ +0.4 +], +"시간": 0.04562043795620438, +"밑시간": 0.04562043795620438, +"율": [], +"절": "2", +"글": "굴삭기(무한궤도) 0.4㎥" +}, +{ +"갈래": [ +"덤프트럭" +], +"규격": [ +4.5 +], +"시간": 0.09541984732824427, +"밑시간": 0.09541984732824427, +"율": [], +"절": "3", +"글": "덤프트럭 4.5톤" +}, +{ +"갈래": [ +"굴착기" +], +"규격": [ +0.2 +], +"시간": 0.07, +"밑시간": 0.07, +"율": [], +"절": "4", +"글": "굴삭기(무한궤도) 0.2㎥" +} +], +"자재": [], +"율": [], +"계수": { +"L": 500.0, +"q": 0.4, +"K": 0.55, +"Cm": 18.0, +"E": 0.45, +"k": 0.7, +"t3": 0.8, +"t4": 0.42 +}, +"조건": { +"방법": [ +"기계", +"인력" +] +}, +"옛판": false, +"글": [ +"뒷채움돌채집운반부설 4.5톤 덤프트럭(L=500m) / M3", +"뒷채움돌채집및운반부설 / m3", +"1) 채집 : (기계:인력=9:1)", +"①기계 : B/H 0.4㎥(90%) 건설표준품셈 9-3", +"q = 0.4 , f = 1/1.15 = 0.87 , K = 0.55", +"Cm = 18 sec(135˚) , E = 0.45", +"Q = 3600*q*K*f*E/Cm = 17.23 m3/hr", +"노 무 비 : 55,700 / Q * 0.9= 2,909.4", +"재 료 비 : 15,375 / Q * 0.9= 803.1", +"경 비 : 16,378 / Q * 0.9= 855.4", +"소계", +"②인력(10%) 사방품셈 P45", +"인 부: 0.3 인/㎥ *165,545* 0.1= 4,966.3", +"소계", +"2) 적사 : B/H 0.4㎥ (품셈9-3)", +"q = 0.4 , f = 1/1.15 = 0.87 , K = 0.7", +"Cm = 18 sec(135˚) , E = 0.45", +"Q = 3600*q*K*f*E/Cm = 21.92 m3/hr", +"노 무 비 : 55,700 / Q = 2,541.0", +"재 료 비 : 15,375 / Q = 701.4", +"경 비 : 16,378 / Q = 747.1", +"소계", +"3) 운반 (덤프4.5Ton) 건설표준품셈 9-7", +"L=0.5 Km ,k=0.7,E=0.9,F=1/1.15= 0.87", +"q1 =(4.5/1.75)*1.15 = 2.96", +"n =q1 / (0.4 * k) = 10.57 회", +"t1 =Cm * n / (60 * 0.45) = 7.05 분", +"t2 =(L/10+L/15)* 60 = 5.00 분", +"t3=0.8 분 ,t4=0.42 분", +"Cm = t1 + t2 + t3 + t4 = 13.27", +"Q = 60 * q1 * F * E / Cm = 10.48 m3/hr", +"노 무 비 : 47,231 / Q = 4,506.7", +"재 료 비 : 8,783 / Q = 838.0", +"경 비 : 7,202 / Q = 687.2", +"소계", +"4) 뒷채움 (건설표준품셈 5-1-1)", +"기계 : B/H 0.2㎥", +"Q = 0.07 hr", +"노 무 비 : 55,700 * Q = 3,899.0", +"재 료 비 : 7,701 * Q = 539.0", +"경 비 : 13,041 * Q = 912.8", +"소계", +"계" +], +"절": { +"1": { +"이름": "채집 : (기계:인력=9:1)", +"출처": [] +}, +"1.1": { +"이름": "채집 : (기계:인력=9:1) · 기계 : B/H 0.4㎥(90%) 건설표준품셈 9-3", +"출처": [ +[ +"건설품셈", +"9-3" +] +] +}, +"1.2": { +"이름": "채집 : (기계:인력=9:1) · 인력(10%) 사방품셈 P45", +"출처": [] +}, +"2": { +"이름": "적사 : B/H 0.4㎥ (품셈9-3)", +"출처": [ +[ +"건설품셈", +"9-3" +] +] +}, +"3": { +"이름": "운반 (덤프4.5Ton) 건설표준품셈 9-7", +"출처": [ +[ +"건설품셈", +"9-7" +] +] +}, +"4": { +"이름": "뒷채움 (건설표준품셈 5-1-1)", +"출처": [ +[ +"건설품셈", +"5-1-1" +] +] +} +}, +"이름조건": {}, +"정답": [ +"GF000218" +], +"허용": [], +"무리": "사람", +"복합": null +}, +{ +"id": "Y034", +"파일": "동부청영월", +"이름": "큰돌메쌓기", +"규격": "뒷길이 0.6~0.8m", +"단위": "M2", +"갈래": "단가산출", +"출처": [], +"직종근거": [], +"인력": { +"작업반장": { +"품": 0.075, +"밑품": 0.075, +"율": [], +"어떻게": [ +"QTY" +] +}, +"특별인부": { +"품": 0.108, +"밑품": 0.108, +"율": [], +"어떻게": [ +"QTY" +] +}, +"보통인부": { +"품": 0.108, +"밑품": 0.108, +"율": [], +"어떻게": [ +"QTY" +] +} +}, +"인력절": [ +[ +"1", +"작업반장", +0.075, +0.075 +], +[ +"1", +"특별인부", +0.108, +0.108 +], +[ +"1", +"보통인부", +0.108, +0.108 +] +], +"장비": [ +{ +"갈래": [ +"굴착기" +], +"규격": [ +0.8 +], +"시간": 0.352, +"밑시간": 0.352, +"율": [], +"절": "2", +"글": "굴삭기(무한궤도) 0.8㎥" +} +], +"자재": [], +"율": [], +"계수": {}, +"조건": { +"메찰": [ +"메쌓기" +] +}, +"옛판": true, +"글": [ +"큰돌메쌓기 뒷길이 0.6~0.8m / M2", +"ㅇ 큰돌메쌓기(사방품셈 P52)", +"1. 노무비", +"작업반장 : 0.075 인/㎡ * 208,713 = 15,653.4", +"특별인부 : 0.108 인/㎡ * 214,222 = 23,135.9", +"보통인부 : 0.108 인/㎡ * 165,545 = 17,878.8", +"공구손료 : 56,668.1 * 1 % = 566.6", +"소계", +"2. 경비(굴삭기 08M3)", +"Q = 0.352 시간/㎡", +"노무비: 55,700 * Q = 19,606.4", +"재료비: 23,761 * Q = 8,363.8", +"경 비: 25,457 * Q = 8,960.8", +"소계", +"계" +], +"절": { +"1": { +"이름": "노무비", +"출처": [] +}, +"2": { +"이름": "경비(굴삭기 08M3)", +"출처": [] +} +}, +"이름조건": { +"메찰": [ +"메쌓기" +] +}, +"정답": [ +"GF000226" +], +"허용": [], +"무리": "사람", +"복합": null +}, +{ +"id": "Y035", +"파일": "동부청영월", +"이름": "큰돌채집운반(뒷길이 0.6~0.8m)", +"규격": "장비(80%) + 인력(20%)", +"단위": "M2", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"9-3" +], +[ +"건설품셈", +"9-7" +] +], +"직종근거": [], +"인력": { +"보통인부": { +"품": 0.072, +"밑품": 0.36, +"율": [ +[ +0.2, +"섞음" +] +], +"어떻게": [ +"QTY" +] +} +}, +"인력절": [ +[ +"1", +"보통인부", +0.072, +0.36 +] +], +"장비": [ +{ +"갈래": [ +"굴착기" +], +"규격": [ +0.7 +], +"시간": 0.07729468599033817, +"밑시간": 0.09661835748792272, +"율": [ +[ +0.8, +"섞음" +] +], +"절": "1", +"글": "굴삭기(무한궤도) 0.7㎥" +}, +{ +"갈래": [ +"굴착기" +], +"규격": [ +0.7 +], +"시간": 0.04488330341113106, +"밑시간": 0.04488330341113106, +"율": [], +"절": "2", +"글": "굴삭기(무한궤도) 0.7㎥" +}, +{ +"갈래": [ +"덤프트럭" +], +"규격": [ +15.0 +], +"시간": 0.027749229188078112, +"밑시간": 0.027749229188078112, +"율": [], +"절": "3", +"글": "덤프트럭 15톤,(암석)" +} +], +"자재": [], +"율": [], +"계수": { +"f": 1.0, +"K": 0.55, +"Cm": 22.0, +"E": 0.45, +"L": 0.5, +"k": 0.55, +"t3": 1.1, +"t4": 0.7 +}, +"조건": { +"방법": [ +"인력", +"장비" +], +"횟수": [ +"1회" +] +}, +"옛판": false, +"글": [ +"큰돌채집운반(뒷길이 0.6~0.8m) 장비(80%) + 인력(20%) / M2", +"◇ 작업조건 :", +"굴삭기(0.7㎥) 버켓용량(q) 1회 1개 적용", +"큰돌크기 4개/㎡ 계상", +"T = 1 * 1 * 0.7 * (1-0.23 공극율 ) = 0.54 ㎥/㎡", +"1) 큰돌채집 : BACK-HOE 0.7M3 품셈 9-3", +"q = 1/2 = 0.50 ㎡ , f = 1 , K = 0.55", +"Cm = 22 sec(180˚) , E = 0.45 *0.5 = 0.23 (작업효율 50%)", +"Q = 3600*q*K*f*E/Cm = 10.35 m2/hr", +"노무비: 55,700 / Q * 0.8 = 4,305.3", +"재료비: 18,015 / Q * 0.8 = 1,392.4", +"경 비: 23,128 / Q * 0.8 = 1,787.6", +"보통인부 : 165,545 * 0.36 * 0.2 = 11,919.2", +"소계", +"2) 큰돌상차 : BACK-HOE 0.7M3 품셈 9-3", +"q = 1/2 = 0.50 ㎡ , f = 1 , K = 0.55", +"Cm = 20 sec(135˚) , E =0.45", +"Q = 3600*q*K*f*E/Cm = 22.28 m2/hr", +"노무비: 55,700 / Q = 2,500.0", +"재료비: 18,015 / Q = 808.5", +"경 비: 23,128 / Q = 1,038.0", +"소계", +"3) 운반 (덤프 15Ton) 품셈 9-7", +"L= 0.5 Km , k=0.55 , E=0.9, F=1", +"q1 = (15/2.65) * 1 = 5.66", +"n = q1 / (0.7 * k ) = 14.70 회", +"t1 =20 * n / (60 * k) = 8.91 분", +"t2 =(L/10 +L/15)* 60 = 5.00 분", +"t3=1.1 분 ,t4=0.7 분", +"Cm = t1 + t2 + t3 + t4 = 15.71", +"Q = 60 * q1 * F * E / Cm = 19.46 m3/hr", +"노무비: 55,700 / Q * T = 1,545.6", +"재료비: 27,932 / Q * T = 775.0", +"경 비: 23,077 / Q * T = 640.3", +"소계", +"계" +], +"절": { +"1": { +"이름": "큰돌채집 : BACK-HOE 0.7M3 품셈 9-3", +"출처": [ +[ +"건설품셈", +"9-3" +] +] +}, +"2": { +"이름": "큰돌상차 : BACK-HOE 0.7M3 품셈 9-3", +"출처": [ +[ +"건설품셈", +"9-3" +] +] +}, +"3": { +"이름": "운반 (덤프 15Ton) 품셈 9-7", +"출처": [ +[ +"건설품셈", +"9-7" +] +] +} +}, +"이름조건": { +"방법": [ +"인력", +"장비" +] +}, +"정답": [ +"GF000213" +], +"허용": [], +"무리": "사람", +"복합": null +}, +{ +"id": "Y115", +"파일": "동부청영월", +"이름": "파형강관부설", +"규격": "Φ800mm", +"단위": "M", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"6-7-2" +] +], +"직종근거": [ +"배관공(수도)", +"보통인부" +], +"인력": { +"배관공(수도)": { +"품": 0.04, +"밑품": 0.04, +"율": [], +"어떻게": [ +"QTY" +] +}, +"보통인부": { +"품": 0.02, +"밑품": 0.02, +"율": [], +"어떻게": [ +"QTY" +] +} +}, +"인력절": [], +"장비": [ +{ +"갈래": [ +"크레인" +], +"규격": [ +5.0 +], +"시간": 0.04, +"밑시간": 0.04, +"율": [], +"절": "0", +"글": "트럭탑재형 크레인 5톤" +} +], +"자재": [ +{ +"이름": "파형강관 별도계상", +"품": 1.0, +"밑품": 1.0 +} +], +"율": [], +"계수": {}, +"조건": {}, +"옛판": false, +"글": [ +"파형강관부설/M (건설표준품셈 6-7-2)", +"1. 재 료 비", +"파형강관(별도계상) : 1 m * 0 = 0.0", +"소계", +"2. 인 건 비", +"배관공 : 0.24 인 / 6 m/본 * 243,168 = 9,726.7", +"보통인부 : 0.12 인 / 6 m/본 * 165,545 = 3,310.9", +"공구손료 : 13,037.6 * 2 % = 260.7", +"소계", +"3. 기계경비", +"5Ton 트럭크레인", +"Q = 0.25 시간/본 / 6 = 0.04 Hr", +"노무비: 47,231 * Q = 1,889.2", +"재료비: 7,790 * Q = 311.6", +"경 비: 9,994 * Q = 399.7", +"소계", +"계" +], +"절": {}, +"이름조건": {}, +"정답": [ +"GC000404" +], +"허용": [], +"무리": "자동", +"복합": null, +"줄수": 0 +}, +{ +"id": "B001", +"파일": "봉화", +"이름": "레미콘타설(장비)", +"규격": "무근,소형", +"단위": "m3", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"6-1-1" +] +], +"직종근거": [ +"보통인부", +"콘크리트공" +], +"인력": { +"콘크리트공": { +"품": 0.095238, +"밑품": 0.095238, +"율": [], +"어떻게": [ +"QTY" +] +}, +"보통인부": { +"품": 0.031746, +"밑품": 0.031746, +"율": [], +"어떻게": [ +"QTY" +] +} +}, +"인력절": [], +"장비": [ +{ +"갈래": [ +"굴착기" +], +"규격": [ +0.7 +], +"시간": 0.2539682539784, +"밑시간": 0.2539682539784, +"율": [], +"절": "0", +"글": "굴삭기(무한궤도) 0.7㎥" +} +], +"자재": [], +"율": [], +"계수": {}, +"조건": { +"근": [ +"무근" +], +"규모": [ +"소형" +], +"방법": [ +"장비" +] +}, +"옛판": false, +"글": [ +"건설표준품셈 6-1-1(레디믹스트콘크리트타설) 적용", +"설치유형 : 장비, 무근", +"작업유형 : 소형(개소별 12.0m3이하)", +"1.노무비", +"콘크리트공 : 261,283 * 3 / (63 / 2) = 24,884.0", +"보통인부 : 165,545 * 1 / (63 / 2 ) = 5,255.3", +"공구손료 및 경장비(콘크리트진동기 등) : 30,139.3 * 2 % = 602.7", +"소계", +"2.기계경비(굴삭기0.7m3)", +"Q = 63 m3/일", +"노 무 비 : 55,700 * 8 / ( Q / 2) = 14,146.0", +"재 료 비 : 18,015 * 8 / ( Q / 2) = 4,575.2", +"경 비 : 23,128 * 8 / ( Q / 2) = 5,873.7", +"소계", +"계" +], +"절": {}, +"이름조건": { +"근": [ +"무근" +], +"규모": [ +"소형" +], +"방법": [ +"장비" +] +}, +"정답": [ +"GC000198" +], +"허용": [], +"무리": "자동+씨앗", +"복합": null, +"줄수": 11 +}, +{ +"id": "B002", +"파일": "봉화", +"이름": "합판거푸집(간단)", +"규격": "간단", +"단위": "m2", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"6-3-1" +] +], +"직종근거": [ +"보통인부", +"형틀목공" +], +"인력": { +"형틀목공": { +"품": 0.1, +"밑품": 0.1, +"율": [], +"어떻게": [ +"QTY" +] +}, +"보통인부": { +"품": 0.04, +"밑품": 0.04, +"율": [], +"어떻게": [ +"QTY" +] +} +}, +"인력절": [], +"장비": [], +"자재": [ +{ +"이름": "합판(내수) 12×1220×2440", +"품": 0.3368100000001, +"밑품": 0.3368100000001 +}, +{ +"이름": "각재 외송", +"품": 0.0124260000001, +"밑품": 0.0124260000001 +} +], +"율": [], +"계수": {}, +"조건": { +"난이": [ +"간단" +], +"횟수": [ +"6회" +] +}, +"옛판": false, +"글": [ +"건설표준품셈 6-3-1(합판거푸집 설치 및 해체) 적용", +"설치유형 : 6회, 간단", +"1.자재비", +"합판(내수) : 11,468 * 1.03 * 0.327 = 3,862.5", +"각재 : 500,661 * 0.038 * 0.327 = 6,221.2", +"소모자재(박리재 등) : 10,083.7 * 11 % = 1,109.2", +"소계", +"2.설치 및 해체", +"형틀목공 : 274,978 * 5 / 50 = 27,497.8", +"보통인부 : 165,545 * 2 / 50 = 6,621.8", +"공구손료 및 경장비 기계경비 : 34,119.6 * 1 % = 341.1", +"소계", +"계" +], +"절": {}, +"이름조건": { +"난이": [ +"간단" +] +}, +"정답": [ +"GC000211" +], +"허용": [], +"무리": "씨앗", +"복합": null, +"줄수": 2 +}, +{ +"id": "B004", +"파일": "봉화", +"이름": "구조물 터파기(토사)", +"규격": "굴삭기 0.2m3", +"단위": "m3", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"3-1-3" +] +], +"직종근거": [], +"인력": {}, +"인력절": [], +"장비": [ +{ +"갈래": [ +"굴착기" +], +"규격": [ +0.2 +], +"시간": 0.0636537237529, +"밑시간": 0.0636537237529, +"율": [], +"절": "0", +"글": "굴삭기(무한궤도) 0.2㎥" +} +], +"자재": [], +"율": [], +"계수": { +"q": 0.2, +"K": 0.7, +"Cm": 15.0, +"E": 0.6 +}, +"조건": { +"토질": [ +"토사" +] +}, +"옛판": false, +"글": [ +"건설표준품셈 3-1-3(터파기),11-3(굴삭기) 적용", +"Q : 시간당 작업량(m3/hr)", +"q : 버킷용량(m3)", +"E : 작업효율", +"f : 토량의 체적 환산계수", +"K : 버킷계수", +"Cm : 1회 사이클 시간(초)", +"적용토질 : 자갈썩인", +"적용기계장비 : 굴삭기(무한궤도 0.2m3 급)", +"기계 및 인력 조합기준 :기계100%", +"흙", +"q = 0.2 , f = 1/1.175 = 0.85 , K = 0.7", +"Cm = 15 sec (90˚) , E = 0.60-0.05 = 0.55", +"Q = 3600*q*K*f*E/Cm = 15.71 m3/hr", +"노 무 비 : 55,700 / Q = 3,545.5", +"재 료 비 : 7,701 / Q = 490.1", +"경 비 : 13,041 / Q = 830.1", +"소계", +"계" +], +"절": {}, +"이름조건": { +"토질": [ +"토사" +] +}, +"정답": [ +"GC000088" +], +"허용": [], +"무리": "씨앗", +"복합": null, +"줄수": 4 +}, +{ +"id": "B005", +"파일": "봉화", +"이름": "구조물 되메우기", +"규격": "굴삭기 0.2m3", +"단위": "m3", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"3-1-3" +] +], +"직종근거": [], +"인력": {}, +"인력절": [], +"장비": [ +{ +"갈래": [ +"굴착기" +], +"규격": [ +0.2 +], +"시간": 0.0418935902908, +"밑시간": 0.0418935902908, +"율": [], +"절": "0", +"글": "굴삭기(무한궤도) 0.2㎥" +} +], +"자재": [], +"율": [], +"계수": { +"q": 0.2, +"K": 0.9, +"Cm": 15.0, +"E": 0.65 +}, +"조건": {}, +"옛판": false, +"글": [ +"건설표준품셈 3-1-3(터파기) <주>⑥ 11-3(굴삭기) 적용", +"Q : 시간당 작업량(m3/hr)", +"q : 버킷용량(m3)", +"E : 작업효율", +"f : 토량의 체적 환산계수", +"K : 버킷계수", +"Cm : 1회 사이클 시간(초)", +"적용토질 : 자갈섞인흙", +"적용기계장비 : 굴삭기(무한궤도 0.2m3 급)", +"기계 및 인력 조합기준 :기계100% + 인력0%", +"되메우기 (기계100% + 인력0%)", +"1)기 계 ( 굴삭기 0.2 m3 100 %)", +"q = 0.2 , f = 1/1.175 = 0.85", +"K = 0.9", +"Cm = 15 sec(90˚) ,E=0.65", +"Q = 3600*q*K*f*E/Cm = 23.87 m3/hr", +"노 무 비 : 55,700 / Q = 2,333.4", +"재 료 비 : 7,701 / Q = 322.6", +"경 비 : 13,041 / Q = 546.3", +"소계", +"계" +], +"절": {}, +"이름조건": {}, +"정답": [ +"GF000113" +], +"허용": [], +"무리": "씨앗", +"복합": null, +"줄수": 4 +}, +{ +"id": "B010", +"파일": "봉화", +"이름": "기초다짐 및 뒤채움", +"규격": "소형 장비사용 시공", +"단위": "M3", +"갈래": "단가산출", +"출처": [], +"직종근거": [ +"보통인부" +], +"인력": { +"보통인부": { +"품": 0.018, +"밑품": 0.018, +"율": [], +"어떻게": [ +"QTY" +] +} +}, +"인력절": [], +"장비": [ +{ +"갈래": [ +"굴착기" +], +"규격": [ +0.2 +], +"시간": 0.0700000000001, +"밑시간": 0.0700000000001, +"율": [], +"절": "0", +"글": "굴삭기(무한궤도) 0.2㎥" +} +], +"자재": [], +"율": [], +"계수": {}, +"조건": { +"난이": [ +"보통" +], +"규모": [ +"소형" +], +"방법": [ +"인력", +"장비" +] +}, +"옛판": false, +"글": [ +"구조물뒷채우기( 막 자 갈 )", +"1. 인 력 : 보통인부: 0.18인/10㎥", +"0.018 * 165,545 = 2,979.8", +"소계", +"2. 기 계 (굴삭기 0.2 ㎥) : 0.7 hr/10㎥", +"Q = 0.07 hr/㎥", +"노 무 비 : 55,700 * Q = 3,899.0", +"재 료 비 : 7,701 * Q = 539.0", +"경 비 : 13,041 * Q = 912.8", +"소계", +"계" +], +"절": {}, +"이름조건": { +"규모": [ +"소형" +], +"방법": [ +"장비" +] +}, +"정답": [ +"GF000218" +], +"허용": [], +"무리": "씨앗", +"복합": null, +"줄수": 6 +}, +{ +"id": "B011", +"파일": "봉화", +"이름": "유로폼 설치 및 해체", +"규격": "간단", +"단위": "m2", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"6-3-3" +] +], +"직종근거": [ +"보통인부", +"형틀목공" +], +"인력": { +"형틀목공": { +"품": 0.1, +"밑품": 0.1, +"율": [], +"어떻게": [ +"QTY" +] +}, +"보통인부": { +"품": 0.025, +"밑품": 0.025, +"율": [], +"어떻게": [ +"QTY" +] +} +}, +"인력절": [], +"장비": [], +"자재": [ +{ +"이름": "패널(유로폼) 600*1200mm", +"품": 0.0890000000001, +"밑품": 0.0890000000001 +}, +{ +"이름": "내부코너패널(유로폼) (100x200)x1200mm", +"품": 0.0030000000001, +"밑품": 0.0030000000001 +} +], +"율": [], +"계수": {}, +"조건": { +"난이": [ +"간단" +] +}, +"옛판": false, +"글": [ +"건설표준품셈 6-3-3(유로폼 설치 및 해체) 적용", +"설치유형 : 간단", +"1.자재비", +"패널(유로폼) : 31,500 * 0.89 / 10 = 2,803.5", +"내부코너패널(유로폼) : 21,000 * 0.03 / 10 = 63.0", +"부자재(웨지핀, 플랫타이, 강관파이프, 훅) : 2,866.5 * 24 % = 687.9", +"소모자재(박리재 등) : 2,866.5 * 5 % = 143.3", +"소계", +"2.설치 및 해체", +"형틀목공 : 274,978 * 4 / 40 = 27,497.8", +"보통인부 : 165,545 * 1 / 40 = 4,138.6", +"공구손료 및 경장비 기계경비 : 31,636.4 * 3 % = 949.0", +"소계", +"계" +], +"절": {}, +"이름조건": { +"난이": [ +"간단" +] +}, +"정답": [ +"GC000214" +], +"허용": [], +"무리": "자동+씨앗", +"복합": null, +"줄수": 8 +}, +{ +"id": "B012", +"파일": "봉화", +"이름": "레미콘타설(장비)", +"규격": "무근", +"단위": "m3", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"6-1-1" +] +], +"직종근거": [ +"보통인부", +"콘크리트공" +], +"인력": { +"콘크리트공": { +"품": 0.047619, +"밑품": 0.047619, +"율": [], +"어떻게": [ +"QTY" +] +}, +"보통인부": { +"품": 0.015873, +"밑품": 0.015873, +"율": [], +"어떻게": [ +"QTY" +] +} +}, +"인력절": [], +"장비": [ +{ +"갈래": [ +"굴착기" +], +"규격": [ +0.7 +], +"시간": 0.1269841269942, +"밑시간": 0.1269841269942, +"율": [], +"절": "0", +"글": "굴삭기(무한궤도) 0.7㎥" +} +], +"자재": [], +"율": [], +"계수": {}, +"조건": { +"근": [ +"무근" +], +"방법": [ +"장비" +] +}, +"옛판": false, +"글": [ +"건설표준품셈 6-1-1(레디믹스트콘크리트타설) 적용", +"설치유형 : 장비, 무근", +"1.노무비", +"콘크리트공 : 261,283 * 3 / 63 = 12,442.0", +"보통인부 : 165,545 * 1 / 63 = 2,627.6", +"공구손료 및 경장비(콘크리트진동기 등) : 15,069.6 * 2 % = 301.3", +"소계", +"2.기계경비(굴삭기0.7m3)", +"Q = 63 m3/일", +"노 무 비 : 55,700 * 8 / Q = 7,073.0", +"재 료 비 : 18,015 * 8 / Q = 2,287.6", +"경 비 : 23,128 * 8 / Q = 2,936.8", +"소계", +"계" +], +"절": {}, +"이름조건": { +"근": [ +"무근" +], +"방법": [ +"장비" +] +}, +"정답": [ +"GC000198" +], +"허용": [], +"무리": "자동+씨앗", +"복합": null, +"줄수": 6 +}, +{ +"id": "B013", +"파일": "봉화", +"이름": "콘크리트믹서사용", +"규격": "0.45 m3", +"단위": "m3", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"6-1-2" +] +], +"직종근거": [], +"인력": {}, +"인력절": [], +"장비": [ +{ +"갈래": [ +"믹서" +], +"규격": [ +0.45 +], +"시간": 0.1851851851953, +"밑시간": 0.1851851851953, +"율": [], +"절": "0", +"글": "콘크리트 믹서 0.45㎥" +} +], +"자재": [], +"율": [], +"계수": { +"q": 0.45, +"E": 0.8 +}, +"조건": {}, +"옛판": false, +"글": [ +"건설표준품셈 6-1-2 콘크리트믹서의 작업량 계산 적용", +"Q : 콘크리트믹서의 시간당 생산량(m3/hr)", +"4 : 4분. 재료투입 혼합배출등 작업시간(분)", +"q : 콘크리트믹서의 용량(m3)", +"E : 작업효율(0.8)", +"q = 0.45, E = 0.8", +"Q = 60 * q * E / 4 = 5.40 m3/Hr", +"노 무 비 : 33,571 / Q = 6,216.8", +"재 료 비 : 5,246 / Q = 971.4", +"경 비 : 1,643 / Q = 304.2", +"소계" +], +"절": {}, +"이름조건": {}, +"정답": [ +"GF000156" +], +"허용": [], +"무리": "씨앗", +"복합": null, +"줄수": 4 +}, +{ +"id": "B014", +"파일": "봉화", +"이름": "유로폼 설치 및 해체", +"규격": "보통", +"단위": "m2", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"6-3-3" +] +], +"직종근거": [ +"보통인부", +"형틀목공" +], +"인력": { +"형틀목공": { +"품": 0.114286, +"밑품": 0.114286, +"율": [], +"어떻게": [ +"QTY" +] +}, +"보통인부": { +"품": 0.028571, +"밑품": 0.028571, +"율": [], +"어떻게": [ +"QTY" +] +} +}, +"인력절": [], +"장비": [], +"자재": [ +{ +"이름": "패널(유로폼) 600*1200mm", +"품": 0.0890000000001, +"밑품": 0.0890000000001 +}, +{ +"이름": "내부코너패널(유로폼) (100x200)x1200mm", +"품": 0.0030000000001, +"밑품": 0.0030000000001 +} +], +"율": [], +"계수": {}, +"조건": { +"난이": [ +"보통" +] +}, +"옛판": false, +"글": [ +"건설표준품셈 6-3-3(유로폼 설치 및 해체) 적용", +"설치유형 : 보통", +"1.자재비", +"패널(유로폼) : 31,500 * 0.89 / 10 = 2,803.5", +"내부코너패널(유로폼) : 21,000 * 0.03 / 10 = 63.0", +"부자재(웨지핀, 플랫타이, 강관파이프, 훅) : 2,866.5 * 52 % = 1,490.5", +"소모자재(박리재 등) : 2,866.5 * 5 % = 143.3", +"소계", +"2.설치 및 해체", +"형틀목공 : 274,978 * 4 / 35 = 31,426.0", +"보통인부 : 165,545 * 1 / 35 = 4,729.8", +"공구손료 및 경장비 기계경비 : 36,155.8 * 3 % = 1,084.6", +"소계", +"계" +], +"절": {}, +"이름조건": { +"난이": [ +"보통" +] +}, +"정답": [ +"GC000214" +], +"허용": [], +"무리": "자동+씨앗", +"복합": null, +"줄수": 9 +}, +{ +"id": "B015", +"파일": "봉화", +"이름": "철근가공 및 조립", +"규격": "Type-Ⅰ-1,현장가공조립", +"단위": "ton", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"6-2" +] +], +"직종근거": [ +"보통인부", +"철근공" +], +"인력": { +"철근공": { +"품": 2.380953, +"밑품": 2.380953, +"율": [], +"어떻게": [ +"QTY" +] +}, +"보통인부": { +"품": 0.793651, +"밑품": 0.793651, +"율": [], +"어떻게": [ +"QTY" +] +} +}, +"인력절": [], +"장비": [], +"자재": [ +{ +"이름": "결속선(철선) #20 , 0.9M/M", +"품": 6.5000000000001, +"밑품": 6.5000000000001 +} +], +"율": [], +"계수": {}, +"조건": { +"근": [ +"철근" +], +"type": [ +"Ⅰ" +] +}, +"옛판": false, +"글": [ +"건설표준품셈 6-2(철근) 적용", +"설치유형 : Type-Ⅰ-1", +"작업유형 : 현장가공 및 현장조립", +"1.현장가공", +"철근공 : 260,137 * 3 / 4.5 = 173,424.6", +"보통인부 : 165,545 * 1 / 4.5 = 36,787.7", +"공구손료 및 경장비(철근가공기 등) : 210,212.3 * 9 % = 18,919.1", +"소계", +"2.현장조립", +"철근공 : 260,137 * 6 / 3.5 = 445,949.1", +"보통인부 : 165,545 * 2 / 3.5 = 94,597.1", +"공구손료 및 경장비 : 540,546.2 * 2 % = 10,810.9", +"소계", +"3.소모재료", +"결속선(철선) : 1,710 * 6.5 = 11,115.0", +"소계", +"계" +], +"절": {}, +"이름조건": { +"근": [ +"철근" +], +"type": [ +"Ⅰ" +] +}, +"정답": [ +"GC000206", +"GC000207" +], +"허용": [], +"무리": "씨앗", +"복합": [ +"GC000206", +"GC000207" +], +"줄수": 9 +}, +{ +"id": "B017", +"파일": "봉화", +"이름": "콘크리트(기계비빔)", +"규격": "무근구조물", +"단위": "M3", +"갈래": "단가산출", +"출처": [], +"직종근거": [ +"보통인부", +"콘크리트공" +], +"인력": { +"콘크리트공": { +"품": 0.03, +"밑품": 0.03, +"율": [], +"어떻게": [ +"QTY" +] +}, +"보통인부": { +"품": 0.31, +"밑품": 0.31, +"율": [], +"어떻게": [ +"QTY" +] +} +}, +"인력절": [], +"장비": [ +{ +"갈래": [ +"믹서" +], +"규격": [ +0.45 +], +"시간": 0.1851851851953, +"밑시간": 0.1851851851953, +"율": [], +"절": "0", +"글": "콘크리트 믹서 0.45㎥" +} +], +"자재": [], +"율": [], +"계수": { +"q": 0.45, +"E": 0.8 +}, +"조건": { +"근": [ +"무근" +], +"방법": [ +"기계" +] +}, +"옛판": false, +"글": [ +"○.콘크리트타설(기계비빔)(무근구조물)(㎥)", +"1. 시멘트 + 모래 + 자갈 : 별도계상", +"2. 콘크리트 비빔", +"콘크리트공 : 261,283 * 0.03 인 = 7,838.4", +"보통인부 : 165,545 * 0.31 인 = 51,318.9", +"소계(제외금액)", +"3. 콘크리트믹서(0.45㎥)", +"q = 0.45 ㎥:콘크리트믹서 용량 , E = 0.8 :작업효율", +"t = 4 분 : 재료투입 혼합배출 등 작업시간", +"Q = 60 / t * q * E = 5.40 ㎥/hr", +"노 무 비 : 33,571 / Q = 6,216.8", +"재 료 비 : 5,246 / Q = 971.4", +"경 비 : 1,643 / Q = 304.2", +"소계", +"계" +], +"절": {}, +"이름조건": { +"근": [ +"무근" +], +"방법": [ +"기계" +] +}, +"정답": [ +"GF000156" +], +"허용": [], +"무리": "씨앗", +"복합": null, +"줄수": 1 +}, +{ +"id": "B018", +"파일": "봉화", +"이름": "구조물터파기(육상토사0~1m)", +"규격": "기계90%+인력10%(굴삭기 0.7M3)", +"단위": "M3", +"갈래": "단가산출", +"출처": [ +[ +"산림품셈", +"5-1-2" +] +], +"직종근거": [ +"보통인부" +], +"인력": { +"보통인부": { +"품": 0.02, +"밑품": 0.2, +"율": [ +[ +0.1, +"섞음" +] +], +"어떻게": [ +"QTY" +] +} +}, +"인력절": [], +"장비": [ +{ +"갈래": [ +"굴착기" +], +"규격": [ +0.7 +], +"시간": 0.0233402489728, +"밑시간": 0.02593360996977778, +"율": [], +"절": "0", +"글": "굴삭기(무한궤도) 0.7㎥" +} +], +"자재": [], +"율": [], +"계수": { +"q": 0.7, +"K": 0.9, +"E": 0.45, +"Cm": 20.0 +}, +"조건": { +"토질": [ +"토사" +], +"방법": [ +"기계", +"인력" +] +}, +"옛판": true, +"글": [ +"▣임도표준품셈 5-1-2 가.육상토사(0~1m) 적용", +"집수정, 맨홀 등 배수구조물의 터파기기 적용함.", +"1. 굴삭기(0.7㎥ : 90%)", +"q=0.7 , K=0.9 , E=0.45-0.05= 0.40", +"f= 1/1.175 = 0.85 , Cm=20 sec(135˚)", +"Q=3600*q*K*f*E/Cm= 38.56 ㎥/hr", +"경 비 23,128 / Q * 0.9 = 539.8", +"노무비 55,700 / Q * 0.9 = 1,300.0", +"재료비 18,015 / Q * 0.9 = 420.4", +"소계", +"2. 보통인부(인력 10%)", +"A=0.2", +"165,545 * A * 0.1 = 3,310.9", +"소계", +"계" +], +"절": {}, +"이름조건": { +"토질": [ +"토사" +], +"방법": [ +"기계", +"인력" +] +}, +"정답": [ +"GF000101" +], +"허용": [], +"무리": "씨앗", +"복합": null, +"줄수": 1 +}, +{ +"id": "B023", +"파일": "봉화", +"이름": "철근가공및조립(현장)", +"규격": "일반(Type-Ⅰ-1)", +"단위": "Ton", +"갈래": "단가산출", +"출처": [], +"직종근거": [ +"보통인부", +"철근공" +], +"인력": { +"철근공": { +"품": 2.380953, +"밑품": 2.380953, +"율": [], +"어떻게": [ +"QTY" +] +}, +"보통인부": { +"품": 0.793651, +"밑품": 0.793651, +"율": [], +"어떻게": [ +"QTY" +] +} +}, +"인력절": [], +"장비": [], +"자재": [ +{ +"이름": "아연도철선(결속선),서울 #20, 0.9mm", +"품": 6.5000000000001, +"밑품": 6.5000000000001 +} +], +"율": [], +"계수": {}, +"조건": { +"근": [ +"철근" +], +"type": [ +"Ⅰ" +] +}, +"옛판": false, +"글": [ +"1. 재 료 비", +"철 근 : (별도계상)", +"아연도철선(결속선) (#20,0.9mm) 2,480 * 6.5 Kg = 16,120.0", +"계", +"2. 인 건 비", +"가. 현장가공 인건비", +"현장가공 일당 시공량 Type-Ⅰ : Q = 4.5 ton/일", +"철 근 공 : 260,137 * 3 인 / Q = 173,424.6", +"보통인부 : 165,545 * 1 인 / Q = 36,787.7", +"기구손료 : (가공노력품의 9%)", +"210,212.3 * 0.09 = 18,919.1", +"소계", +"나. 조립인건비", +"현장조립 일당 시공량 Type-Ⅰ-1 : Q = 3.5 ton/일", +"철 근 공 : 260,137 * 6 인 / Q = 445,949.1", +"보통인부 : 165,545 * 2 인 / Q = 94,597.1", +"기구손료 : (조립노력품의 2%)", +"540,546.2 * 0.02 = 10,810.9", +"소계", +"계", +"합계" +], +"절": {}, +"이름조건": { +"근": [ +"철근" +], +"type": [ +"Ⅰ" +] +}, +"정답": [ +"GC000206", +"GC000207" +], +"허용": [], +"무리": "씨앗", +"복합": [ +"GC000206", +"GC000207" +], +"줄수": 1 +}, +{ +"id": "B024", +"파일": "봉화", +"이름": "합판거푸집", +"규격": "3회(복잡)", +"단위": "M2", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"6-3-1" +] +], +"직종근거": [ +"보통인부", +"형틀목공" +], +"인력": { +"형틀목공": { +"품": 0.166667, +"밑품": 0.166667, +"율": [], +"어떻게": [ +"QTY" +] +}, +"보통인부": { +"품": 0.066667, +"밑품": 0.066667, +"율": [], +"어떻게": [ +"QTY" +] +} +}, +"인력절": [], +"장비": [], +"자재": [ +{ +"이름": "합판(내수) 12×1220×2440", +"품": 0.4562900000001, +"밑품": 0.4562900000001 +}, +{ +"이름": "각재 외송", +"품": 0.0168340000001, +"밑품": 0.0168340000001 +} +], +"율": [], +"계수": {}, +"조건": { +"난이": [ +"복잡" +], +"횟수": [ +"3회" +] +}, +"옛판": false, +"글": [ +"합판거푸집(3회-복잡)[㎡]", +"2024년 품셈개정, 6-3-1 합판거푸집", +"※ 사용횟수 3회 : 복잡", +"일당 설치 및 해체 시공량 Q = 30 ㎡", +"1. 재 료 비(1회의 0.443%)", +"내수합판1급 : 11,468 * 1.03 ㎡ * 0.443 % = 5,232.7", +"각재(거푸집용,외송) : 500,661 * 0.038 ㎥ * 0.443 % = 8,428.1 원/㎡", +"소모자재(박래재등)(주자재비의 8%)", +"손료계산 : 13,660.8 * 8 % = 1,092.8", +"소계", +"2. 인 건 비", +"형틀목공 : 274,978 * 5 인 / Q = 45,829.6", +"보통인부 : 165,545 * 2 인 / Q = 11,036.3", +"공구손료 및 경장비 기계경비(노무비의 1%)", +"손료계산 : 56,865.9 * 1 % = 568.6", +"소계", +"계" +], +"절": {}, +"이름조건": { +"난이": [ +"복잡" +], +"횟수": [ +"3회" +] +}, +"정답": [ +"GC000211" +], +"허용": [], +"무리": "자동+씨앗", +"복합": null, +"줄수": 1 +}, +{ +"id": "B026", +"파일": "봉화", +"이름": "평떼붙임", +"규격": "", +"단위": "m2", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"4-1-1" +] +], +"직종근거": [ +"보통인부", +"조경공" +], +"인력": { +"보통인부": { +"품": 0.029111, +"밑품": 0.029111, +"율": [], +"어떻게": [ +"QTY" +] +}, +"조경공": { +"품": 0.006667, +"밑품": 0.006667, +"율": [], +"어떻게": [ +"QTY" +] +} +}, +"인력절": [], +"장비": [], +"자재": [ +{ +"이름": "떼", +"품": 1.1000000000001, +"밑품": 1.1000000000001 +} +], +"율": [], +"계수": {}, +"조건": {}, +"옛판": false, +"글": [ +"건설표준품셈 4-1-1(잔디붙임)적용", +"건설표준품셈 8-나(품종별 적 상·하 기준) 적용", +"적하시간 : 2인 1조 10분/ton적용", +"단위중량 : 55.5kg/m2 적용", +"1.떼값:", +"4,444 *1.1 = 4,888.4", +"소계", +"2.적하비(2인 1조)", +"2 * 10 / (480-30) * 165,545 * 0.055 = 404.6", +"소계", +"3.떼붙임", +"조경공 : 219,533 * 1 / 150 = 1,463.5", +"보통인부 : 165,545 * 4 / 150 = 4,414.5", +"소계", +"계" +], +"절": {}, +"이름조건": {}, +"정답": [ +"GC000143" +], +"허용": [], +"무리": "씨앗", +"복합": null, +"줄수": 1 +}, +{ +"id": "B028", +"파일": "봉화", +"이름": "레미콘타설(장비)", +"규격": "철근", +"단위": "m3", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"6-1-1" +] +], +"직종근거": [ +"보통인부", +"콘크리트공" +], +"인력": { +"콘크리트공": { +"품": 0.054545, +"밑품": 0.054545, +"율": [], +"어떻게": [ +"QTY" +] +}, +"보통인부": { +"품": 0.018182, +"밑품": 0.018182, +"율": [], +"어떻게": [ +"QTY" +] +} +}, +"인력절": [], +"장비": [ +{ +"갈래": [ +"굴착기" +], +"규격": [ +0.7 +], +"시간": 0.1454545454647, +"밑시간": 0.1454545454647, +"율": [], +"절": "0", +"글": "굴삭기(무한궤도) 0.7㎥" +} +], +"자재": [], +"율": [], +"계수": {}, +"조건": { +"근": [ +"철근" +], +"방법": [ +"장비" +] +}, +"옛판": false, +"글": [ +"건설표준품셈 6-1-1(레디믹스트콘크리트타설) 적용", +"설치유형 : 장비, 철근", +"1.노무비", +"콘크리트공 : 261,283 * 3 / 55 = 14,251.8", +"보통인부 : 165,545 * 1 / 55 = 3,009.9", +"공구손료 및 경장비(콘크리트진동기 등) : 17,261.7 * 2 % = 345.2", +"소계", +"2.기계경비(굴삭기0.7m3)", +"Q = 55 m3/일", +"노 무 비 : 55,700 * 8 / Q = 8,101.8", +"재 료 비 : 18,015 * 8 / Q = 2,620.3", +"경 비 : 23,128 * 8 / Q = 3,364.0", +"소계", +"계" +], +"절": {}, +"이름조건": { +"근": [ +"철근" +], +"방법": [ +"장비" +] +}, +"정답": [ +"GC000198" +], +"허용": [], +"무리": "자동+씨앗", +"복합": null, +"줄수": 1 +}, +{ +"id": "B029", +"파일": "봉화", +"이름": "레미콘타설(장비)", +"규격": "무근,버림", +"단위": "m3", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"6-1-1" +] +], +"직종근거": [ +"보통인부", +"콘크리트공" +], +"인력": { +"콘크리트공": { +"품": 0.047619, +"밑품": 0.047619, +"율": [], +"어떻게": [ +"QTY" +] +}, +"보통인부": { +"품": 0.015873, +"밑품": 0.015873, +"율": [], +"어떻게": [ +"QTY" +] +} +}, +"인력절": [], +"장비": [ +{ +"갈래": [ +"굴착기" +], +"규격": [ +0.7 +], +"시간": 0.1269841269942, +"밑시간": 0.1269841269942, +"율": [], +"절": "0", +"글": "굴삭기(무한궤도) 0.7㎥" +} +], +"자재": [], +"율": [], +"계수": {}, +"조건": { +"근": [ +"무근" +], +"방법": [ +"장비" +] +}, +"옛판": false, +"글": [ +"건설표준품셈 6-1-1(레디믹스트콘크리트타설) 적용", +"설치유형 : 장비, 버림", +"1.노무비", +"콘크리트공 : 261,283 * 3 / 63 = 12,442.0", +"보통인부 : 165,545 * 1 / 63 = 2,627.6", +"공구손료 및 경장비(콘크리트진동기 등) : 15,069.6 * 2 % = 301.3", +"소계", +"2.기계경비(굴삭기0.7m3)", +"Q = 63 m3/일", +"노 무 비 : 55,700 * 8 / Q = 7,073.0", +"재 료 비 : 18,015 * 8 / Q = 2,287.6", +"경 비 : 23,128 * 8 / Q = 2,936.8", +"소계", +"계" +], +"절": {}, +"이름조건": { +"근": [ +"무근" +], +"방법": [ +"장비" +] +}, +"정답": [ +"GC000198" +], +"허용": [], +"무리": "자동", +"복합": null, +"줄수": 1 +}, +{ +"id": "B030", +"파일": "봉화", +"이름": "철근가공 및 조립", +"규격": "Type-Ⅱ-1,현장가공조립", +"단위": "ton", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"6-2" +] +], +"직종근거": [ +"보통인부", +"철근공" +], +"인력": { +"철근공": { +"품": 2.75, +"밑품": 2.75, +"율": [], +"어떻게": [ +"QTY" +] +}, +"보통인부": { +"품": 1.05, +"밑품": 1.05, +"율": [], +"어떻게": [ +"QTY" +] +} +}, +"인력절": [], +"장비": [], +"자재": [ +{ +"이름": "결속선(철선) #20 , 0.9M/M", +"품": 8.0, +"밑품": 8.0 +} +], +"율": [], +"계수": {}, +"조건": { +"근": [ +"철근" +], +"type": [ +"Ⅱ" +] +}, +"옛판": false, +"글": [ +"건설표준품셈 6-2(철근) 적용", +"설치유형 : Type-Ⅱ-1", +"작업유형 : 현장가공 및 현장조립", +"1.현장가공", +"철근공 : 260,137 * 3 / 4.0 = 195,102.7", +"보통인부 : 165,545 * 1 / 4.0 = 41,386.2", +"공구손료 및 경장비(철근가공기 등) : 236,488.9 * 9 % = 21,284.0", +"소계", +"2.현장조립", +"철근공 : 260,137 * 5 / 2.5 = 520,274.0", +"보통인부 : 165,545 * 2 / 2.5 = 132,436.0", +"공구손료 및 경장비 : 652,710.0 * 2 % = 13,054.2", +"소계", +"3.소모재료", +"결속선(철선) : 1,710 * 8.0 = 13,680.0", +"소계", +"계" +], +"절": {}, +"이름조건": { +"근": [ +"철근" +], +"type": [ +"Ⅱ" +] +}, +"정답": [ +"GC000206", +"GC000207" +], +"허용": [], +"무리": "씨앗", +"복합": [ +"GC000206", +"GC000207" +], +"줄수": 1 +}, +{ +"id": "B031", +"파일": "봉화", +"이름": "강관동바리", +"규격": "암거용-3개월사용", +"단위": "공/㎥", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"2-2-4" +] +], +"직종근거": [ +"보통인부", +"형틀목공" +], +"인력": { +"형틀목공": { +"품": 0.054, +"밑품": 0.054, +"율": [], +"어떻게": [ +"QTY" +] +}, +"보통인부": { +"품": 0.021, +"밑품": 0.021, +"율": [], +"어떻게": [ +"QTY" +] +} +}, +"인력절": [], +"장비": [], +"자재": [ +{ +"이름": "강관동바리 Φ48.6x2.4,L=2.6~4.0", +"품": 0.0354000000001, +"밑품": 0.0354000000001 +} +], +"율": [], +"계수": {}, +"조건": { +"개월": [ +"3개월" +] +}, +"옛판": false, +"글": [ +"<품셈 2-2-4, 2-6-1>", +"※2.5m이하 암거구조물(3개월사용 : 재료비의 6%, 인건비의 100%)", +"재 료 비", +"1.강관동바리 : (0.59 * 27,600)*0.06 = 977.0", +"2.잡재료비 (재료비의 5%)", +"977.0 * 5 % = 48.8", +"소계", +"설 치 비", +"1.형틀목공 : 0.054 * 274,978 = 14,848.8", +"2.보통인부 : 0.021 * 165,545 = 3,476.4", +"소계", +"계" +], +"절": {}, +"이름조건": { +"개월": [ +"3개월" +] +}, +"정답": [ +"GC000015" +], +"허용": [], +"무리": "씨앗", +"복합": null, +"줄수": 1 +}, +{ +"id": "B032", +"파일": "봉화", +"이름": "강관비계", +"규격": "3개월사용", +"단위": "M2", +"갈래": "단가산출", +"출처": [], +"직종근거": [ +"보통인부", +"비계공" +], +"인력": { +"비계공": { +"품": 0.05, +"밑품": 0.05, +"율": [], +"어떻게": [ +"QTY" +] +}, +"보통인부": { +"품": 0.02, +"밑품": 0.02, +"율": [], +"어떻게": [ +"QTY" +] +} +}, +"인력절": [], +"장비": [], +"자재": [ +{ +"이름": "강관비계 φ48.6m/m×2.3mm", +"품": 0.2394000000001, +"밑품": 0.2394000000001 +}, +{ +"이름": "이음철물", +"품": 0.0600000000001, +"밑품": 0.0600000000001 +}, +{ +"이름": "조임철물 (직교자재)", +"품": 0.2496000000001, +"밑품": 0.2496000000001 +}, +{ +"이름": "받침철물", +"품": 0.0036000000001, +"밑품": 0.0036000000001 +}, +{ +"이름": "철물", +"품": 0.0400000000001, +"밑품": 0.0400000000001 +} +], +"율": [], +"계수": {}, +"조건": { +"개월": [ +"3개월" +] +}, +"옛판": false, +"글": [ +"건설품셈 강관계비", +"강관비계 3개월사용 / M2", +"1. 재료비", +"1) 강관(48.6mm*2.3mm) : 3.99 m * 4,275 * 0.06 = 1,023.4", +"2) 이음철물 : 0.5 EA * 2,200 * 0.12 = 132.0", +"3) 조임철물(직교자재) : 2.08 EA * 2,700 * 0.12 = 673.9", +"4) 받침철물 : 0.04 EA * 4,000 * 0.09 = 14.4", +"5) 철물(앵커용) : 0.04 EA * 997 = 39.8", +"소계", +"2. 조립 및 해체", +"1) 비계공 : 0.05 인 * 278,151 = 13,907.5", +"2) 보통인부 : 0.02 인 * 165,545 = 3,310.9", +"2) 기구손료(인건비의2%) : 17,218.4 * 2 % = 344.3", +"소계", +"계" +], +"절": {}, +"이름조건": { +"개월": [ +"3개월" +] +}, +"정답": [ +"GC000022" +], +"허용": [], +"무리": "씨앗", +"복합": null, +"줄수": 1 +}, +{ +"id": "U002", +"파일": "울진대흥", +"이름": "콘크리트비빔", +"규격": "채움재", +"단위": "m3", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"7-1-2" +], +[ +"건설품셈", +"6-1-2" +], +[ +"건설품셈", +"6-1-1" +] +], +"직종근거": [ +"보통인부", +"콘크리트공" +], +"인력": { +"콘크리트공": { +"품": 0.02, +"밑품": 0.02, +"율": [], +"어떻게": [ +"QTY" +] +}, +"보통인부": { +"품": 0.33, +"밑품": 0.33, +"율": [], +"어떻게": [ +"QTY" +] +} +}, +"인력절": [], +"장비": [], +"자재": [], +"율": [], +"계수": {}, +"조건": { +"메찰": [ +"찰쌓기" +], +"방법": [ +"기계", +"장비" +] +}, +"옛판": false, +"글": [ +"건설표준품셈 제7장 돌공사 채움재 적용", +"건설표준품셈 7-1-2(찰쌓기) <주3>애 콘크리트 채움을 포함하므로", +"건설표준품셈 6-1-2(현장비빔 및 타설)에서 타설품은 제외하고 비빔만 적용", +"건설표준품셈 6-1-2(현장비빔 및 타설) 기계비빔타설", +"콘크리트공 : 0.15인", +"보통인부 : 0.46인", +"건설표준품셈 6-1-1(레디믹스트콘크리트타설) 인력운반타설", +"콘크리트공 : 3.0÷23 = 0.13인", +"보통인부 : 3.0÷23 = 0.13인", +"1.노무비(비빔)", +"콘크리트공 : 261,283 * (0.15 - 0.13) = 5,225.6", +"보통인부 : 165,545 * (0.46 - 0.13) = 54,629.8", +"소계", +"2.기계경비", +"콘크리트믹서사용 : 7,528 * 1 = 7,528.0", +"소계", +"계" +], +"절": {}, +"이름조건": {}, +"정답": [ +"GF000156" +], +"허용": [], +"무리": "씨앗", +"복합": null, +"줄수": 9 +}, +{ +"id": "U003", +"파일": "울진대흥", +"이름": "유로폼 설치 및 해체", +"규격": "간단", +"단위": "m2", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"6-3-3" +] +], +"직종근거": [ +"보통인부", +"형틀목공" +], +"인력": { +"형틀목공": { +"품": 0.1, +"밑품": 0.1, +"율": [], +"어떻게": [ +"QTY" +] +}, +"보통인부": { +"품": 0.025, +"밑품": 0.025, +"율": [], +"어떻게": [ +"QTY" +] +} +}, +"인력절": [], +"장비": [], +"자재": [ +{ +"이름": "패널(유로폼) 600*1200mm", +"품": 0.089, +"밑품": 0.089 +}, +{ +"이름": "내부코너패널(유로폼) (100+200)*1200mm", +"품": 0.003, +"밑품": 0.003 +} +], +"율": [], +"계수": {}, +"조건": { +"난이": [ +"간단" +] +}, +"옛판": false, +"글": [ +"건설표준품셈 6-3-3(유로폼 설치 및 해체) 적용", +"설치유형 : 간단", +"1.자재비", +"패널(유로폼) : 31,500 * 0.89 / 10 = 2,803.5", +"내부코너패널(유로폼) : 21,000 * 0.03 / 10 = 63.0", +"부자재(웨지핀, 플랫타이, 강관파이프, 훅) : 2,866.5 * 24 % = 687.9", +"소모자재(박리재 등) : 2,866.5 * 5 % = 143.3", +"소계", +"2.설치 및 해체", +"형틀목공 : 274,978 * 4 / 40 = 27,497.8", +"보통인부 : 165,545 * 1 / 40 = 4,138.6", +"공구손료 및 경장비 기계경비 : 31,636.4 * 3 % = 949.0", +"소계", +"계" +], +"절": {}, +"이름조건": { +"난이": [ +"간단" +] +}, +"정답": [ +"GC000214" +], +"허용": [], +"무리": "자동+씨앗", +"복합": null, +"줄수": 5 +}, +{ +"id": "U005", +"파일": "울진대흥", +"이름": "구조물터파기(토사)", +"규격": "굴삭기0.7m3", +"단위": "m3", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"3-1-3" +] +], +"직종근거": [], +"인력": {}, +"인력절": [], +"장비": [ +{ +"갈래": [ +"굴착기" +], +"규격": [ +0.7 +], +"시간": 0.016957775139901644, +"밑시간": 0.016957775139901644, +"율": [], +"절": "0", +"글": "굴삭기(0.7m3) " +} +], +"자재": [], +"율": [], +"계수": { +"Cm": 58.97 +}, +"조건": { +"토질": [ +"토사" +] +}, +"옛판": false, +"글": [ +"터파기 굴삭기0.7m3", +"건설표준품셈 3-1-3(터파기),11-3(굴삭기) 적용", +"1) 굴삭기0.7m3 적용", +"q (버킷용량) = 0.7 , f (체적환산계수) = 1/1.25 = 0.80", +"K (버킷계수) = 0.9", +"Cm (1회 사이클 시간(초) = 20 sec (135˚)", +"E (작업효율) = 0.7-0.05 = 0.65", +"Q (시간당작업량) = 3600*q*K*f*E/Cm = 58.97 m3/hr", +"노 무 비 : 55,700 / Q = 944.5", +"재 료 비 : 18,001 / Q = 305.2", +"경 비 : 23,128 / Q = 392.1", +"소계" +], +"절": {}, +"이름조건": { +"토질": [ +"토사" +] +}, +"정답": [ +"GF000101" +], +"허용": [], +"무리": "씨앗", +"복합": null, +"줄수": 1 +}, +{ +"id": "U006", +"파일": "울진대흥", +"이름": "다지기", +"규격": "굴삭기0.7m3", +"단위": "m3", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"11-3" +] +], +"직종근거": [], +"인력": {}, +"인력절": [], +"장비": [ +{ +"갈래": [ +"굴착기" +], +"규격": [ +0.7 +], +"시간": 0.013227513227513229, +"밑시간": 0.013227513227513229, +"율": [], +"절": "0", +"글": "굴삭기(0.7m3) " +} +], +"자재": [], +"율": [], +"계수": { +"Cm": 75.6 +}, +"조건": { +"횟수": [ +"1회" +] +}, +"옛판": false, +"글": [ +"건설표준품셈 11-3 굴삭기 적용", +"1)다짐기(굴삭기 0.7m3)", +"q (버킷용량) = 0.7 , f (체적환산계수) = 1/1.25 , K (버킷계수) = 0.9", +"Cm (1회 사이클 시간(초)) = 18 sec (90˚) , E (작업효율) = 0.75", +"Q (시간당 작업량) = 3600*q*K*f*E/Cm = 75.60 m3/hr", +"노 무 비 : 55,700 / Q = 736.7", +"재 료 비 : 18,001 / Q = 238.1", +"경 비 : 23,128 / Q = 305.9", +"계", +"합계" +], +"절": {}, +"이름조건": {}, +"정답": [ +"GC000105" +], +"허용": [], +"무리": "씨앗", +"복합": null, +"줄수": 1 +}, +{ +"id": "U007", +"파일": "울진대흥", +"이름": "야면석채집", +"규격": "L3=45cm내외", +"단위": "m2", +"갈래": "단가산출", +"출처": [], +"직종근거": [], +"인력": {}, +"인력절": [], +"장비": [ +{ +"갈래": [ +"굴착기" +], +"규격": [ +0.7 +], +"시간": 0.12468827930174564, +"밑시간": 0.12468827930174564, +"율": [], +"절": "0", +"글": "굴삭기(0.7m3) 0.7㎥,(암석)" +}, +{ +"갈래": [ +"굴착기" +], +"규격": [ +0.7 +], +"시간": 0.0979431929480901, +"밑시간": 0.0979431929480901, +"율": [], +"절": "0", +"글": "굴삭기(0.7m3) 0.7㎥,(암석)" +}, +{ +"갈래": [ +"덤프트럭" +], +"규격": [ +4.5 +], +"시간": 0.12468827930174564, +"밑시간": 0.12468827930174564, +"율": [], +"절": "0", +"글": "덤프트럭4.5ton(암) " +} +], +"자재": [], +"율": [], +"계수": { +"Cm": 8.02, +"t4": 33.31 +}, +"조건": { +"횟수": [ +"1회" +] +}, +"옛판": false, +"글": [ +"야면석채집 (L3=45Cm내외)", +"◇작업조건 :", +"계곡에 산재하여 매몰되어 있는 곳에서", +"굴삭기(0.7m3) 버켓용량(q)1회 2개 적용", +"(1) 채집", +"① 굴삭기 0.7㎥", +"q (버킷용량) = 2/11 = 0.18 m2 , f (체적환산계수) = 1 ,", +"K (버킷계수) = 0.55", +"Cm (1회ㅣ 사이클시간(초)) = 20 sec (135˚) , E (작업효율) = 0.45", +"Q (시간당작업량) = 3600*q*K*f*E/Cm = 8.02 m2/hr", +"노 무 비 : 55,700 / Q = 6,945.1", +"재 료 비 : 18,001 / Q = 2,244.5", +"경 비 : 26,677 / Q = 3,326.3", +"소계", +"계", +"(2) 상차", +"q (버킷용량) = 2/11 = 0.18 m2 , f (체적환산계수) = 1", +"K (버킷계수) = 0.7", +"Cm1 (1회ㅣ 사이클시간(초)) = 20 sec (135˚)", +"E1 (작업효율) = 0.45", +"Q (시간당작업량) = 3600*q*K*f*E1/Cm1 = 10.21 m2/hr", +"노 무 비 : 55,700 / Q = 5,455.4", +"재 료 비 : 18,001 / Q = 1,763.0", +"경 비 : 26,677 / Q = 2,612.8", +"소계", +"2-2 운 반 (덤프4.50톤)", +"L (운반거리) = 0.1 Km", +"q1 (흐트러진상태의 덤프트럭 1회 적재량) = 4500 / 910 = 4.95", +"E (작업효율) = 0.9 , f (체적환산계수) = 1 , k (버킷계수) = 0.7", +"q2 (흐트러진상태의 덤프트럭 1회 적재량) = 2 / 12 = 0.17 m2", +"n (소요되는적재기계의 사이클횟수) = q1 / (q2 * k) = 41.60 회", +"t1 (적재시간) = Cm1 * n / (60 * E1) = 30.81 분", +"t2 (왕복시간) =(L/10+L/15) * 60 = 1.00 분", +"t3 (적하시간) = 0.8 분", +"t4 (적재장소 도착한 때로부터 적재작업이 시작될 때까지의 시간) = 0.7 분", +"Cm (1회 사이클 시간(분)) = t1 + t2 + t3 + t4 = 33.31 분", +"Q (시간당 작업량) = 60 * q1 * f * E / Cm = 8.02 m2/hr", +"노 무 비 : 47,231 / Q = 5,889.1", +"재 료 비 : 8,776 / Q = 1,094.2", +"경 비 : 8,576 / Q = 1,069.3", +"소계", +"계" +], +"절": {}, +"이름조건": {}, +"정답": [ +"GF000214" +], +"허용": [], +"무리": "씨앗", +"복합": null, +"줄수": 6 +}, +{ +"id": "U012", +"파일": "울진대흥", +"이름": "철근가공 및 조립", +"규격": "Type-Ⅱ-2,현장가공조립", +"단위": "ton", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"6-2" +] +], +"직종근거": [ +"보통인부", +"철근공" +], +"인력": { +"철근공": { +"품": 2.75, +"밑품": 2.75, +"율": [], +"어떻게": [ +"QTY" +] +}, +"보통인부": { +"품": 1.25, +"밑품": 1.25, +"율": [], +"어떻게": [ +"QTY" +] +} +}, +"인력절": [], +"장비": [], +"자재": [ +{ +"이름": "결속선(철선) #20 , 0.9M/M", +"품": 8.0, +"밑품": 8.0 +} +], +"율": [], +"계수": {}, +"조건": { +"근": [ +"철근" +], +"type": [ +"Ⅱ" +] +}, +"옛판": false, +"글": [ +"건설표준품셈 6-2(철근) 적용", +"설치유형 : Type-Ⅱ-2", +"작업유형 : 현장가공 및 현장조립", +"1.현장가공", +"철근공 : 260,137 * 3 / 4 = 195,102.7", +"보통인부 : 165,545 * 1 / 4 = 41,386.2", +"공구손료 및 경장비(철근가공기 등) : 236,488.9 * 9 % = 21,284.0", +"소계", +"2.현장조립", +"철근공 : 260,137 * 2 / 1 = 520,274.0", +"보통인부 : 165,545 * 1 / 1 = 165,545.0", +"공구손료 및 경장비 : 685,819.0 * 2 % = 13,716.3", +"소계", +"3.소모재료", +"결속선(철선) : 1,710 * 8 = 13,680.0", +"소계", +"계" +], +"절": {}, +"이름조건": { +"근": [ +"철근" +], +"type": [ +"Ⅱ" +] +}, +"정답": [ +"GC000206", +"GC000207" +], +"허용": [], +"무리": "씨앗", +"복합": [ +"GC000206", +"GC000207" +], +"줄수": 1 +}, +{ +"id": "U013", +"파일": "울진대흥", +"이름": "유로폼 설치 및 해체", +"규격": "보통", +"단위": "m2", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"6-3-3" +] +], +"직종근거": [ +"보통인부", +"형틀목공" +], +"인력": { +"형틀목공": { +"품": 0.114286, +"밑품": 0.114286, +"율": [], +"어떻게": [ +"QTY" +] +}, +"보통인부": { +"품": 0.028571, +"밑품": 0.028571, +"율": [], +"어떻게": [ +"QTY" +] +} +}, +"인력절": [], +"장비": [], +"자재": [ +{ +"이름": "패널(유로폼) 600*1200mm", +"품": 0.089, +"밑품": 0.089 +}, +{ +"이름": "내부코너패널(유로폼) (100x200)x1200mm", +"품": 0.003, +"밑품": 0.003 +} +], +"율": [], +"계수": {}, +"조건": { +"난이": [ +"보통" +] +}, +"옛판": false, +"글": [ +"건설표준품셈 6-3-3(유로폼 설치 및 해체) 적용", +"설치유형 : 보통", +"1.자재비", +"패널(유로폼) : 31,500 * 0.89 / 10 = 2,803.5", +"내부코너패널(유로폼) : 21,000 * 0.03 / 10 = 63.0", +"부자재(웨지핀, 플랫타이, 강관파이프, 훅) : 2,866.5 * 52 % = 1,490.5", +"소모자재(박리재 등) : 2,866.5 * 5 % = 143.3", +"소계", +"2.설치 및 해체", +"형틀목공 : 274,978 * 4 / 35 = 31,426.0", +"보통인부 : 165,545 * 1 / 35 = 4,729.8", +"공구손료 및 경장비 기계경비 : 36,155.8 * 3 % = 1,084.6", +"소계", +"계" +], +"절": {}, +"이름조건": { +"난이": [ +"보통" +] +}, +"정답": [ +"GC000214" +], +"허용": [], +"무리": "자동+씨앗", +"복합": null, +"줄수": 1 +}, +{ +"id": "U014", +"파일": "울진대흥", +"이름": "콘크리트믹서사용", +"규격": "0.45 m3", +"단위": "m3", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"6-1-2" +] +], +"직종근거": [], +"인력": {}, +"인력절": [], +"장비": [ +{ +"갈래": [ +"믹서" +], +"규격": [ +0.45 +], +"시간": 0.18518518518518517, +"밑시간": 0.18518518518518517, +"율": [], +"절": "0", +"글": "콘크리트믹서0.45m3 " +} +], +"자재": [], +"율": [], +"계수": {}, +"조건": {}, +"옛판": false, +"글": [ +"콘크리트믹서사용 0.45 m3 / m3", +"건설표준품셈 6-1-2 콘크리트믹서의 작업량 계산 적용", +"q (콘크리트믹서의 용량(m3)) = 0.45, E (작업효율) = 0.8", +"4 : 4분. 재료투입 혼합배출등 작업시간(분)", +"Q (콘크리트믹서의 시간당 생산량(m3/hr)) = 60 * q * E / 4 = 5.40 m3/Hr", +"노 무 비 : 33,571 / Q = 6,216.8", +"재 료 비 : 5,445 / Q = 1,008.3", +"경 비 : 1,643 / Q = 304.2", +"소계" +], +"절": {}, +"이름조건": {}, +"정답": [ +"GF000156" +], +"허용": [], +"무리": "씨앗", +"복합": null, +"줄수": 1 +}, +{ +"id": "U015", +"파일": "울진대흥", +"이름": "콘크리트포장(B-type)", +"규격": "t=20cm", +"단위": "m2", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"1-6-2" +] +], +"직종근거": [ +"보통인부", +"포장공" +], +"인력": { +"포장공": { +"품": 0.016, +"밑품": 0.016, +"율": [], +"어떻게": [ +"QTY" +] +}, +"보통인부": { +"품": 0.008, +"밑품": 0.008, +"율": [], +"어떻게": [ +"QTY" +] +} +}, +"인력절": [], +"장비": [], +"자재": [], +"율": [], +"계수": {}, +"조건": { +"난이": [ +"보통" +], +"방법": [ +"인력" +] +}, +"옛판": false, +"글": [ +"건설표준품셈 1-6-2(콘크리트포장 표층인력포설) 적용", +"설치유형 : B-type", +"포장공 : 258,360 * 4 / (50 / 0.2) = 4,133.7", +"보통인부 : 165,545 * 2 / (50 / 0.2) = 1,324.3", +"손료계산 : 5,458.0 * 3 % = 163.7", +"소계" +], +"절": {}, +"이름조건": {}, +"정답": [ +"GF000162" +], +"허용": [], +"무리": "씨앗", +"복합": null, +"줄수": 1 +}, +{ +"id": "U016", +"파일": "울진대흥", +"이름": "레미콘타설", +"규격": "무근(장비사용타설)", +"단위": "m3", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"6-1-1" +] +], +"직종근거": [ +"보통인부", +"콘크리트공" +], +"인력": { +"콘크리트공": { +"품": 0.047619, +"밑품": 0.047619, +"율": [], +"어떻게": [ +"QTY" +] +}, +"보통인부": { +"품": 0.015873, +"밑품": 0.015873, +"율": [], +"어떻게": [ +"QTY" +] +} +}, +"인력절": [], +"장비": [ +{ +"갈래": [ +"굴착기" +], +"규격": [ +0.7 +], +"시간": 0.12698412698412698, +"밑시간": 0.12698412698412698, +"율": [], +"절": "0", +"글": "굴삭기(0.7m3) " +} +], +"자재": [], +"율": [], +"계수": {}, +"조건": { +"근": [ +"무근" +], +"방법": [ +"장비" +] +}, +"옛판": false, +"글": [ +"※ 건설표준품셈(공통) : 6-1-1 무근(장비사용타설) 적용", +"□ 일 시공량 : Q = 63 m3/일", +"1. 인부임", +"○콘크리트공: 261,283 * 3 인 / Q = 12,442.0 원/m3", +"○보통인부 : 165,545 * 1 인 / Q = 2,627.6 원/m3", +"소계", +"2. 공구손료 및 경장비(콘크리트 진동기등)의 기계경비(노무비의 2%)", +"공구손료 : 15,069.6 * 2 % = 301.3", +"소계", +"3. 굴삭기 (0.7m3)", +"□ 시간당 시공량 : Q = 63 m3/일r", +"노 무 비 : 55,700 * 8 / Q = 7,073.0", +"재 료 비 : 18,001 * 8 / Q = 2,285.8", +"경 비 : 23,128 * 8 / Q = 2,936.8", +"소계", +"계" +], +"절": {}, +"이름조건": { +"근": [ +"무근" +], +"방법": [ +"장비" +] +}, +"정답": [ +"GC000198" +], +"허용": [], +"무리": "자동", +"복합": null, +"줄수": 3 +}, +{ +"id": "K004", +"파일": "영덕계류", +"이름": "뒤채움", +"규격": "소형장비", +"단위": "m3", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"3-4-4" +] +], +"직종근거": [ +"보통인부", +"특별인부" +], +"인력": { +"특별인부": { +"품": 0.009091, +"밑품": 0.009091, +"율": [], +"어떻게": [ +"QTY" +] +}, +"보통인부": { +"품": 0.009091, +"밑품": 0.009091, +"율": [], +"어떻게": [ +"QTY" +] +} +}, +"인력절": [], +"장비": [ +{ +"갈래": [ +"굴착기" +], +"규격": [ +0.2 +], +"시간": 0.07272727272730001, +"밑시간": 0.07272727272730001, +"율": [], +"절": "0", +"글": "굴착기(무한궤도) 0.2㎥" +} +], +"자재": [], +"율": [], +"계수": {}, +"조건": { +"규모": [ +"소형" +], +"방법": [ +"인력", +"장비" +] +}, +"옛판": false, +"글": [ +"건설표준품셈 3-4-4(뒤채움 및 다짐) 적용", +"설치유형 : 인력, 굴삭기0.2m3", +"작업유형 : 포설 및 고르기를 포함", +"1.노무비", +"특별인부 : 221,506 * 1 / 110 = 2,013.6", +"보통인부 : 169,804 * 1 / 110 = 1,543.6", +"소계", +"2.기계경비(굴삭기0.2m3)", +"Q = 110 m3/일", +"노 무 비 : 57,077 * 8 / Q = 4,151.0", +"재 료 비 : 7,629 * 8 / Q = 554.8", +"경 비 : 13,399 * 8 / Q = 974.4", +"소계", +"계" +], +"절": {}, +"이름조건": { +"규모": [ +"소형" +], +"방법": [ +"장비" +] +}, +"정답": [ +"GC000106" +], +"허용": [], +"무리": "자동", +"복합": null, +"줄수": 0 +}, +{ +"id": "G000", +"파일": "울진소광", +"이름": "유로폼 설치 및 해체", +"규격": "보통", +"단위": "m2", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"6-3-3" +] +], +"직종근거": [ +"보통인부", +"형틀목공" +], +"인력": { +"형틀목공": { +"품": 0.114286, +"밑품": 0.114286, +"율": [], +"어떻게": [ +"QTY" +] +}, +"보통인부": { +"품": 0.028571, +"밑품": 0.028571, +"율": [], +"어떻게": [ +"QTY" +] +} +}, +"인력절": [], +"장비": [], +"자재": [], +"율": [], +"계수": {}, +"조건": { +"난이": [ +"보통" +] +}, +"옛판": false, +"글": [ +"※ 건설표준품셈(공통) : 6-3-3 적용", +"□ 일 시공량 : Q = 35 m2/일", +"1. 인부", +"○ 형틀목공:", +"272,831 * 4.0 인 / Q = 31,180.6 원/m2", +"○보통인부 :", +"169,804 * 1.0 인 / Q = 4,851.5 원/m2", +"소계", +"2. 공구손료 및 경장비 기계손료", +"공구손료 : 36,032.1 * 3 % = 1,080.9", +"소계", +"계" +], +"절": {}, +"이름조건": { +"난이": [ +"보통" +] +}, +"정답": [ +"GC000214" +], +"허용": [], +"무리": "자동+씨앗", +"복합": null, +"줄수": 1 +}, +{ +"id": "G003", +"파일": "울진소광", +"이름": "유로폼 설치 및 해체", +"규격": "간단", +"단위": "m2", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"6-3-3" +] +], +"직종근거": [ +"보통인부", +"형틀목공" +], +"인력": { +"형틀목공": { +"품": 0.1, +"밑품": 0.1, +"율": [], +"어떻게": [ +"QTY" +] +}, +"보통인부": { +"품": 0.025, +"밑품": 0.025, +"율": [], +"어떻게": [ +"QTY" +] +} +}, +"인력절": [], +"장비": [], +"자재": [], +"율": [], +"계수": {}, +"조건": { +"난이": [ +"간단" +] +}, +"옛판": false, +"글": [ +"※ 건설표준품셈(공통) : 6-3-3 적용", +"□ 일 시공량 : Q = 40 m2/일", +"1. 인부", +"○ 형틀목공:", +"272,831 * 4.0 인 / Q = 27,283.1 원/m2", +"○보통인부 :", +"169,804 * 1.0 인 / Q = 4,245.1 원/m2", +"소계", +"2. 공구손료 및 경장비 기계손료", +"공구손료 : 31,528.2 * 3 % = 945.8", +"소계", +"계" +], +"절": {}, +"이름조건": { +"난이": [ +"간단" +] +}, +"정답": [ +"GC000214" +], +"허용": [], +"무리": "자동+씨앗", +"복합": null, +"줄수": 1 +}, +{ +"id": "G005", +"파일": "울진소광", +"이름": "구조물터파기(토사)", +"규격": "굴착기0.6m3", +"단위": "m3", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"3-2-4" +] +], +"직종근거": [], +"인력": {}, +"인력절": [], +"장비": [ +{ +"갈래": [ +"굴착기" +], +"규격": [ +0.6 +], +"시간": 0.0421052631581, +"밑시간": 0.0421052631581, +"율": [], +"절": "0", +"글": "굴삭기(타이어) 0.6㎥" +} +], +"자재": [], +"율": [], +"계수": { +"q1": 2.0 +}, +"조건": { +"토질": [ +"토사" +] +}, +"옛판": false, +"글": [ +"※ 건설표준품셈(공통) : 3-2-4 적용", +"□ 일 시공량 : Q = 190 m3/일", +"※ 작업조건 : Type-Ⅲ (협소)", +"1. 굴착기 (0.6m3)", +"□ 시간당 시공량 : q1 = Q / 8 = 23.75 m3/hr", +"노 무 비 : 57,077 / q1 = 2,403.2", +"재 료 비 : 19,547 / q1 = 823.0", +"경 비 : 26,463 / q1 = 1,114.2", +"소계" +], +"절": {}, +"이름조건": { +"토질": [ +"토사" +] +}, +"정답": [ +"GC000088" +], +"허용": [], +"무리": "씨앗", +"복합": null, +"줄수": 8 +}, +{ +"id": "G006", +"파일": "울진소광", +"이름": "구조물되메우기", +"규격": "굴착기0.6m3", +"단위": "m3", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"3-2-6" +] +], +"직종근거": [ +"보통인부", +"특별인부" +], +"인력": { +"특별인부": { +"품": 0.003448, +"밑품": 0.003448, +"율": [], +"어떻게": [ +"QTY" +] +}, +"보통인부": { +"품": 0.003448, +"밑품": 0.003448, +"율": [], +"어떻게": [ +"QTY" +] +} +}, +"인력절": [], +"장비": [ +{ +"갈래": [ +"굴착기" +], +"규격": [ +0.6 +], +"시간": 0.0275862068968, +"밑시간": 0.0275862068968, +"율": [], +"절": "0", +"글": "굴삭기(타이어) 0.6㎥" +} +], +"자재": [], +"율": [], +"계수": { +"q1": 1.0 +}, +"조건": {}, +"옛판": false, +"글": [ +"※ 건설표준품셈(공통) : 3-2-6 적용", +"□ 일 시공량 : Q = 290 m3/일", +"1) 굴착기 (0.6m3)", +"□ 시간당 시공량 : q1 = Q / 8 = 36.25 m3/hr", +"노 무 비 : 57,077 / q1 = 1,574.5", +"재 료 비 : 19,547 / q1 = 539.2", +"경 비 : 26,463 / q1 = 730.0", +"소계", +"2) 인력", +"○특별인부: 221,506 * 1.0 / Q = 763.8", +"○보통인부: 169,804 * 1.0 / Q = 585.5", +"소계", +"계" +], +"절": {}, +"이름조건": {}, +"정답": [ +"GF000113" +], +"허용": [], +"무리": "씨앗", +"복합": null, +"줄수": 6 +}, +{ +"id": "G011", +"파일": "울진소광", +"이름": "막자갈채우기", +"규격": "150mm내외", +"단위": "m3", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"3-4-5" +] +], +"직종근거": [ +"보통인부", +"특별인부" +], +"인력": { +"특별인부": { +"품": 0.004, +"밑품": 0.004, +"율": [], +"어떻게": [ +"QTY" +] +}, +"보통인부": { +"품": 0.004, +"밑품": 0.004, +"율": [], +"어떻게": [ +"QTY" +] +} +}, +"인력절": [], +"장비": [ +{ +"갈래": [ +"굴착기" +], +"규격": [ +0.6 +], +"시간": 0.0320000000001, +"밑시간": 0.0320000000001, +"율": [], +"절": "0", +"글": "굴삭기(타이어) 0.6㎥" +} +], +"자재": [], +"율": [], +"계수": { +"q1": 1.0 +}, +"조건": {}, +"옛판": false, +"글": [ +"※건설표준품셈 3-4-5 적용", +"막자갈 규격 150mm내외", +"□ 일 시공량 : Q = 250 m3/일", +"□ 시간당 시공량 : q1 = Q / 8 = 31.25 m3/hr", +"①굴삭기(0.6m3 )", +"노 무 비 : 57,077 / q1 = 1,826.4", +"재 료 비 : 19,547 / q1 = 625.5", +"경 비 : 26,463 / q1 = 846.8", +"소계", +"② 인력", +"○특별인부: 221,506 * 1.0 / Q = 886.0", +"○보통인부: 169,804 * 1.0 / Q = 679.2", +"소계", +"계" +], +"절": {}, +"이름조건": {}, +"정답": [ +"GC000107" +], +"허용": [], +"무리": "자동", +"복합": null, +"줄수": 3 +}, +{ +"id": "G012", +"파일": "울진소광", +"이름": "레미콘타설", +"규격": "무근(장비)", +"단위": "m3", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"6-1-1" +] +], +"직종근거": [ +"보통인부", +"콘크리트공" +], +"인력": { +"콘크리트공": { +"품": 0.047619, +"밑품": 0.047619, +"율": [], +"어떻게": [ +"QTY" +] +}, +"보통인부": { +"품": 0.015873, +"밑품": 0.015873, +"율": [], +"어떻게": [ +"QTY" +] +} +}, +"인력절": [], +"장비": [ +{ +"갈래": [ +"굴착기" +], +"규격": [ +0.6 +], +"시간": 0.1269035532997, +"밑시간": 0.1269035532997, +"율": [], +"절": "0", +"글": "굴삭기(타이어) 0.6㎥" +} +], +"자재": [], +"율": [], +"계수": { +"q1": 7.0 +}, +"조건": { +"근": [ +"무근" +], +"방법": [ +"장비" +] +}, +"옛판": false, +"글": [ +"※ 건설표준품셈(공통) : 6-1-1 적용", +"□ 일 시공량 : Q = 63 m3/일", +"1. 인부임", +"○콘크리트공: 266,361 * 3.0 인 / Q = 12,683.8 원/m3", +"○보통인부 : 169,804 * 1.0 인 / Q = 2,695.3 원/m3", +"소계", +"2. 공구손료 및 경장비 기계손료", +"공구손료 : 15,379.1 * 2 % = 307.5", +"소계", +"3. 굴삭기 (06m3)", +"□ 시간당 시공량 : q1 = Q / 8 = 7.88 m3/hr", +"노 무 비 : 57,077 / q1 = 7,243.2", +"재 료 비 : 19,547 / q1 = 2,480.5", +"경 비 : 26,463 / q1 = 3,358.2", +"소계", +"계" +], +"절": {}, +"이름조건": { +"근": [ +"무근" +], +"방법": [ +"장비" +] +}, +"정답": [ +"GC000198" +], +"허용": [], +"무리": "자동+씨앗", +"복합": null, +"줄수": 3 +}, +{ +"id": "G016", +"파일": "울진소광", +"이름": "콘크리포장(T=20cm)포설", +"규격": "인력포설", +"단위": "m3", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"1-6-2" +] +], +"직종근거": [ +"보통인부", +"포장공" +], +"인력": { +"포장공": { +"품": 0.08, +"밑품": 0.08, +"율": [], +"어떻게": [ +"QTY" +] +}, +"보통인부": { +"품": 0.04, +"밑품": 0.04, +"율": [], +"어떻게": [ +"QTY" +] +} +}, +"인력절": [], +"장비": [], +"자재": [], +"율": [], +"계수": {}, +"조건": { +"방법": [ +"인력" +] +}, +"옛판": false, +"글": [ +"※ 건설표준품셈(토목) : 1-6-2 적용", +"□ 일 시공량 : Q = 50 m3/일", +"1. 인부", +"○ 포장공:", +"267,989 * 4.0 인 / Q = 21,439.1 원/m3", +"○보통인부 :", +"169,804 * 2.0 인 / Q = 6,792.1 원/m3", +"소계", +"2. 공구손료 및 경장비 기계손료", +"공구손료 : 28,231.2 * 3 % = 846.9", +"소계", +"계" +], +"절": {}, +"이름조건": { +"방법": [ +"인력" +] +}, +"정답": [ +"GC000297" +], +"허용": [], +"무리": "자동", +"복합": null, +"줄수": 1 +}, +{ +"id": "G018", +"파일": "울진소광", +"이름": "평떼붙이기", +"규격": "", +"단위": "m2", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"4-1-1" +] +], +"직종근거": [ +"보통인부", +"조경공" +], +"인력": { +"조경공": { +"품": 0.006667, +"밑품": 0.006667, +"율": [], +"어떻게": [ +"QTY" +] +}, +"보통인부": { +"품": 0.026667, +"밑품": 0.026667, +"율": [], +"어떻게": [ +"QTY" +] +} +}, +"인력절": [], +"장비": [], +"자재": [], +"율": [], +"계수": {}, +"조건": {}, +"옛판": false, +"글": [ +"※ 건설표준품셈(공통) : 4-1-1 적용", +"□ 일작업량 : Q = 150 m2/일", +"1. 인부임", +"○ 조경공:", +"224,132 * 1.0 인 / Q = 1,494.2 원/m2", +"○보통인부 :", +"169,804 * 4.0 인 / Q = 4,528.1 원/m2", +"소계" +], +"절": {}, +"이름조건": {}, +"정답": [ +"GC000143" +], +"허용": [], +"무리": "자동", +"복합": null, +"줄수": 1 +}, +{ +"id": "G020", +"파일": "울진소광", +"이름": "콘크리트믹서사용", +"규격": "0.45 m3", +"단위": "m3", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"9-18" +] +], +"직종근거": [], +"인력": {}, +"인력절": [], +"장비": [ +{ +"갈래": [ +"믹서" +], +"규격": [ +0.45 +], +"시간": 0.1851851851854, +"밑시간": 0.1851851851854, +"율": [], +"절": "0", +"글": "콘크리트 믹서 0.45㎥" +} +], +"자재": [], +"율": [], +"계수": {}, +"조건": {}, +"옛판": false, +"글": [ +"○ 건설품셈 9-18", +"cm (재료투입 혼합배출 등 작업시간(분) =4", +"q (콘크리트 믹서 용량(m3) =0.45", +"E (작업효율) =0.8", +"Q (콘크리트 믹서의 시간당 생산량) =60*q*E/cm = 5.40 m3/Hr", +"노 무 비 : 35,608 / Q = 6,594.0", +"재 료 비 : 6,038 / Q = 1,118.1", +"경 비 : 1,713 / Q = 317.2", +"소계" +], +"절": {}, +"이름조건": {}, +"정답": [ +"GF000156" +], +"허용": [], +"무리": "씨앗", +"복합": null, +"줄수": 0 +}, +{ +"id": "G033", +"파일": "울진소광", +"이름": "수축줄눈(컷팅)", +"규격": "콘크리트포장", +"단위": "m", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"1-6-6" +] +], +"직종근거": [ +"보통인부", +"특별인부" +], +"인력": { +"특별인부": { +"품": 0.001667, +"밑품": 0.001667, +"율": [], +"어떻게": [ +"QTY" +] +}, +"보통인부": { +"품": 0.001667, +"밑품": 0.001667, +"율": [], +"어떻게": [ +"QTY" +] +} +}, +"인력절": [], +"장비": [ +{ +"갈래": [ +"분무기" +], +"규격": [ +4.85 +], +"시간": 0.0066666666669, +"밑시간": 0.0066666666669, +"율": [], +"절": "0", +"글": "동력분무기 4.85㎾" +}, +{ +"갈래": [ +"커터" +], +"규격": [ +440.0 +], +"시간": 0.0133333333335, +"밑시간": 0.0133333333335, +"율": [], +"절": "0", +"글": "커터(콘크리트 및 아스팔트용) 320-440mm" +} +], +"자재": [ +{ +"이름": "블레이드 d=320~400mm t=3.2", +"품": 0.0031000000001, +"밑품": 0.0031000000001 +}, +{ +"이름": "물", +"품": 30.0, +"밑품": 30.0 +} +], +"율": [], +"계수": {}, +"조건": {}, +"옛판": false, +"글": [ +"※ 건설품셈(토목) : 1-6-6 적용", +"○ 일 작업량 : Q1 = 600 m", +"1. 절단", +"○.블레이드(Φ320-400mm : t=3.2mm) :", +"169,800 * 0.31 / 100 = 526.3", +"○.특별인부 :", +"221,506 * 1.0 인 /Q1 = 369.1 원/m", +"○보통인부 :", +"169,804 * 1.0 인 /Q1 = 283.0 원/m", +"○ 공구손료 및 경장비 기계손료", +"공구손료 : 652.1 * 3 % = 19.5", +"2.살수비(물 운반)", +"※ 100M당 물 3000ℓ 소요", +"M (물 소요량) = 3000/100= 30.00 ℓ/m", +"물 : 1.27 * M = 38.1", +"소계", +"3.동력분무기", +"노 무 비 : 0 / (Q1/8) * 0.5 = 0.0", +"재 료 비 : 2,368 / (Q1/8) * 0.5 = 15.7", +"경 비 : 252 / (Q1/8) * 0.5 = 1.6", +"소계", +"4.컷트기 (320-400mm)", +"V (작업 속도) = Q1/8 = 75.00 m/hr", +"노 무 비 : 35,608 / V = 474.7", +"재 료 비 : 10,200 / V = 136.0", +"경 비 : 1,981 / V = 26.4", +"소계", +"계" +], +"절": {}, +"이름조건": {}, +"정답": [ +"GC000301" +], +"허용": [], +"무리": "자동", +"복합": null, +"줄수": 0 +}, +{ +"id": "C004", +"파일": "거창", +"이름": "유로폼", +"규격": "보통,0~7m", +"단위": "㎡", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"6-3-3" +] +], +"직종근거": [ +"보통인부", +"형틀목공" +], +"인력": { +"형틀목공": { +"품": 0.114286, +"밑품": 0.114286, +"율": [], +"어떻게": [ +"식" +] +}, +"보통인부": { +"품": 0.028571, +"밑품": 0.028571, +"율": [], +"어떻게": [ +"식" +] +} +}, +"인력절": [ +[ +"3", +"형틀목공", +0.114286, +0.114286 +], +[ +"3", +"보통인부", +0.028571, +0.028571 +] +], +"장비": [], +"자재": [], +"율": [ +[ +"주재료", +55.0 +], +[ +"노무비", +3.0 +] +], +"계수": {}, +"조건": { +"난이": [ +"보통" +] +}, +"옛판": false, +"글": [ +"유로폼(보통,0~7m) [㎡]", +"< 토목품셈 6-3-3 유로폼(2026 개정) >", +"1. 재료비", +"유로폼패널 (600×1200㎜) 28,500. × 0.095 매 = A 2,707.5", +"자재소계 : 2,707.5", +"2. 부자재 ( 내부패널 ,웨지핀 ,플랫타이 ,강관파이크 ,후크 ) ( 주재료의 55 % )", +"2,707.5 × 55. % = 1,489.1", +"3. 설치비", +"형틀목공 : 275,790 × 4. 인 / 35. ㎡ = 31,518.8", +"보통인부 : 172,068 × 1. 인 / 35. ㎡ = 4,916.2", +"인부소계 : 36,435.", +"4. 공구손료 및 경장비 기계경비 ( 노무비의 3 % )", +"36,435. × 3. % = 1,093.", +"5. 합 계", +"노무비 : 36,435", +"재료비 : 2,707.5 + 1,489.1 + 1,093. = 5,289", +"경 비 :", +"계 : 41,724" +], +"절": { +"1": { +"이름": "재료비", +"출처": [] +}, +"2": { +"이름": "부자재 ( 내부패널 ,웨지핀 ,플랫타이 ,강관파이크 ,후크 ) ( 주재료의 55 % )", +"출처": [] +}, +"3": { +"이름": "설치비", +"출처": [] +}, +"4": { +"이름": "공구손료 및 경장비 기계경비 ( 노무비의 3 % )", +"출처": [] +}, +"5": { +"이름": "합 계", +"출처": [] +} +}, +"이름조건": { +"난이": [ +"보통" +] +}, +"정답": [ +"GC000214" +], +"허용": [], +"무리": "자동+씨앗", +"복합": null, +"줄수": 5 +}, +{ +"id": "C005", +"파일": "거창", +"이름": "철근가공및조립(현장)", +"규격": "Type-Ⅱ-1", +"단위": "Ton", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"6-2" +] +], +"직종근거": [ +"철근공" +], +"인력": { +"철근공": { +"품": 2.673077, +"밑품": 2.673077, +"율": [], +"어떻게": [ +"식", +"식" +] +}, +"보통인부": { +"품": 1.019231, +"밑품": 1.019231, +"율": [], +"어떻게": [ +"식", +"식" +] +} +}, +"인력절": [ +[ +"2", +"철근공", +0.75, +0.75 +], +[ +"2", +"보통인부", +0.25, +0.25 +], +[ +"2", +"철근공", +1.923077, +1.923077 +], +[ +"2", +"보통인부", +0.769231, +0.769231 +] +], +"장비": [], +"자재": [ +{ +"이름": "철 근", +"품": null, +"밑품": null +} +], +"율": [ +[ +"철근중량", +50.0 +], +[ +"인력품", +9.0 +], +[ +"인력품", +2.0 +] +], +"계수": {}, +"조건": { +"근": [ +"철근" +], +"난이": [ +"복잡" +], +"type": [ +"Ⅱ" +] +}, +"옛판": false, +"글": [ +"철근가공및조립(현장)(Type-Ⅱ-1) [Ton]", +"< 표준품셈(공통). 6-2 철근가공및조립 (24년 품셈 개정)>", +"- - - - - - - - - ┬ - - - - - ┬ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -", +"│ │ - 철근가공및조립 작업이 복잡한 토목시설 ( 라멘교 , 교대 , 암거 , 지하차도 ,", +"│Ⅱ - 1 │ 부벽식 옹벽 등 )", +"Type - Ⅱ│ │ - Type - Ⅰ시설에서 직경 13 mm이하 철근이 전 철근중량의 50 %이상인 경우", +"├ - - - - - ┼ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -", +"│Ⅱ -2. │ - 콘크리트대비 소량의 철근이 사용되는 경우 ( 측구 / 개거 ,중력식 옹벽 ,", +"│ │ 일체형 중앙분리대 등 )", +"- - - - - - - - - ┴ - - - - - ┴ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -", +"1. 재 료 비", +"철 근 : ( 별도계상 )", +"2. 인 건 비", +"가 . 가공 ( Type - Ⅱ )", +"철 근 공 : 268,187 × 3. 인 / 4. ton = 201,140.2", +"보통인부 : 172,068 × 1. 인 / 4. ton = 43,017.", +"인부소계 : 244,157.2", +"공구손료 및 경장비 ( 철근 가공기등 ) 의 기계경비 ( 인력품의 9 % )", +"244,157.2 × 9. % = 21,974.1", +"나 . 조립 ( Type - Ⅱ - 1 )", +"아연도철선(결속선),서울 (#20,0.9mm) 2,480. × 8. Kg = 19,840.", +"철 근 공 : 268,187 × 5. 인 / 2.6 ton = 515,744.2", +"보통인부 : 172,068 × 2. 인 / 2.6 ton = 132,360.", +"인부소계 : 648,104.2", +"공구손료 및 경장비의 기계경비 ( 인력품의 2 % )", +"648,104.2 × 2. % = 12,962.", +"다 . 소 계", +"노무비 : 244,157.2 + 648,104.2 = 892,261.4", +"재료비 : 21,974.1 + 19,840. + 12,962. = 54,776.1", +"경 비 :", +"계 : 947,037.5", +"3. 합 계", +"노무비 : 892,261", +"재료비 : 54,776", +"경 비 :", +"계 : 947,037" +], +"절": { +"1": { +"이름": "재 료 비", +"출처": [] +}, +"2": { +"이름": "인 건 비", +"출처": [] +}, +"3": { +"이름": "합 계", +"출처": [] +} +}, +"이름조건": { +"근": [ +"철근" +], +"type": [ +"Ⅱ" +] +}, +"정답": [ +"GC000206", +"GC000207" +], +"허용": [], +"무리": "씨앗", +"복합": [ +"GC000206", +"GC000207" +], +"줄수": 4 +}, +{ +"id": "C013", +"파일": "거창", +"이름": "레미콘타설(장비사용타설)", +"규격": "철근구조물", +"단위": "㎥", +"갈래": "단가산출", +"출처": [], +"직종근거": [ +"보통인부", +"콘크리트공" +], +"인력": { +"콘크리트공": { +"품": 0.07, +"밑품": 0.07, +"율": [], +"어떻게": [ +"식" +] +}, +"보통인부": { +"품": 0.02, +"밑품": 0.02, +"율": [], +"어떻게": [ +"식" +] +} +}, +"인력절": [ +[ +"2", +"콘크리트공", +0.07, +0.07 +], +[ +"2", +"보통인부", +0.02, +0.02 +] +], +"장비": [ +{ +"갈래": [ +"굴착기" +], +"규격": [ +0.7 +], +"시간": 0.1, +"밑시간": 0.1, +"율": [], +"절": "3", +"글": "굴삭기(무한궤도) (0.7㎥)" +} +], +"자재": [ +{ +"이름": "레미콘", +"품": null, +"밑품": null +} +], +"율": [ +[ +"노무비", +2.0 +] +], +"계수": {}, +"조건": { +"근": [ +"철근" +], +"난이": [ +"보통" +], +"방법": [ +"장비" +] +}, +"옛판": false, +"글": [ +"레미콘타설(장비사용타설) (철근구조물) [㎥]", +"1. 레미콘 : 별도계상", +"2. 인건비", +"콘크리트공 : 273,540 × 0.07 인 = 19,147.8", +"보통인부 : 172,068 × 0.02 인 = 3,441.3", +"인부소계 : 22,589.1", +"공구손료 및 경장비 ( 콘크리트 진동기등 ) 의 기계경비 ( 노무비의 2 % )", +"22,589.1 × 2. % = 451.7", +"3. 굴삭기(무한궤도) (0.7㎥)", +"Q = 0.1 hr / ㎥", +"노무비 : 59,020 × 0.1 = 5,902.", +"재료비 : 19,473 × 0.1 = 1,947.3", +"경 비 : 24,554 × 0.1 = 2,455.4", +"소 계 : 10,304.7", +"4. 합 계", +"노무비 : 22,589.1 + 5,902. = 28,491", +"재료비 : 451.7 + 1,947.3 = 2,399", +"경 비 : 2,455", +"계 : 33,345" +], +"절": { +"1": { +"이름": "레미콘 : 별도계상", +"출처": [] +}, +"2": { +"이름": "인건비", +"출처": [] +}, +"3": { +"이름": "굴삭기(무한궤도) (0.7㎥)", +"출처": [] +}, +"4": { +"이름": "합 계", +"출처": [] +} +}, +"이름조건": { +"근": [ +"철근" +], +"방법": [ +"장비" +] +}, +"정답": [ +"GC000198" +], +"허용": [], +"무리": "씨앗", +"복합": null, +"줄수": 1 +}, +{ +"id": "C014", +"파일": "거창", +"이름": "합판거푸집", +"규격": "4회", +"단위": "㎡", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"6-3-1" +] +], +"직종근거": [ +"보통인부", +"형틀목공" +], +"인력": { +"형틀목공": { +"품": 0.111111, +"밑품": 0.111111, +"율": [], +"어떻게": [ +"식" +] +}, +"보통인부": { +"품": 0.044444, +"밑품": 0.044444, +"율": [], +"어떻게": [ +"식" +] +} +}, +"인력절": [ +[ +"2", +"형틀목공", +0.111111, +0.111111 +], +[ +"2", +"보통인부", +0.044444, +0.044444 +] +], +"장비": [], +"자재": [], +"율": [ +[ +"주자재비", +9.0 +], +[ +"노무비", +1.0 +] +], +"계수": {}, +"조건": { +"난이": [ +"보통" +], +"횟수": [ +"4회" +] +}, +"옛판": false, +"글": [ +"합판거푸집(4회) [㎡]", +"< 토목품셈 6-3-1 합판거푸집(2022개정) >", +"※ 사용횟수 4 회 : 보통", +"1. 재료비", +"합판(내수),대전 (12t×1220×2440mm) 11,455. × 1.03 ㎡ × 38. % = 4,483.4", +"외송각재,대전 (30×30×3600mm) 586,824. × 0.038 ㎥ × 38. % = 8,473.7 ₩ / ㎡", +"주자재소계 : 12,957.1", +"소모자재 ( 박리재등 ) ( 주자재비의 9 % )", +"12,957.1 × 9. % = 1,166.1", +"2. 인건비 ( 24 년 품셈 개정 )", +"형틀목공 : 275,790 × 5. 인 / 45. ㎡ = 30,643.3", +"보통인부 : 172,068 × 2. 인 / 45. ㎡ = 7,647.4", +"인부소계 : 38,290.7", +"공구손료 및 경장비 기계경비 ( 노무비의 1 % )", +"38,290.7 × 1. % = 382.9", +"3. 합 계", +"노무비 : 38,290", +"재료비 : 12,957.1 + 1,166.1 + 382.9 = 14,506", +"경 비 :", +"계 : 52,796" +], +"절": { +"1": { +"이름": "재료비", +"출처": [] +}, +"2": { +"이름": "인건비 ( 24 년 품셈 개정 )", +"출처": [] +}, +"3": { +"이름": "합 계", +"출처": [] +} +}, +"이름조건": { +"횟수": [ +"4회" +] +}, +"정답": [ +"GC000211" +], +"허용": [], +"무리": "씨앗", +"복합": null, +"줄수": 1 +}, +{ +"id": "C016", +"파일": "거창", +"이름": "강관동바리", +"규격": "2.5m이하(1개월)", +"단위": "공㎥", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"2-6-1" +] +], +"직종근거": [ +"보통인부", +"형틀목공" +], +"인력": { +"형틀목공": { +"품": 0.05, +"밑품": 0.05, +"율": [], +"어떻게": [ +"식" +] +}, +"보통인부": { +"품": 0.017, +"밑품": 0.017, +"율": [], +"어떻게": [ +"식" +] +} +}, +"인력절": [ +[ +"2", +"형틀목공", +0.05, +0.05 +], +[ +"2", +"보통인부", +0.017, +0.017 +] +], +"장비": [], +"자재": [], +"율": [ +[ +"자재비", +5.0 +] +], +"계수": { +"L": 2.6 +}, +"조건": { +"개월": [ +"1개월" +] +}, +"옛판": false, +"글": [ +"강관동바리(2.5m이하(1개월)) [공㎥]", +"1. 재료비 : 설계수량 적용 ( 1 개월손율 4 %계상 )", +"강관동바리,서울 (Φ48.6*2.4,L=2.6~4.0) 24,500. × 5.9 본 × 4. % ÷ 10. 공㎥ = 578.2", +"잡재료비 ( 자재비의 5 % )", +"578.2 × 0.05 = 28.9", +"2. 설치비 <토목품셈 2-6-1>", +"형틀목공 : 275,790 × 0.05 인 = 13,789.5", +"보통인부 : 172,068 × 0.017 인 = 2,925.1", +"인부소계 : 16,714.6", +"3. 합 계", +"노무비 : 16,714", +"재료비 : 578.2 + 28.9 = 607", +"경 비 :", +"계 : 17,321" +], +"절": { +"1": { +"이름": "재료비 : 설계수량 적용 ( 1 개월손율 4 %계상 )", +"출처": [] +}, +"578": { +"이름": "2 × 0.05 = 28.9", +"출처": [] +}, +"2": { +"이름": "설치비 <토목품셈 2-6-1>", +"출처": [ +[ +"건설품셈", +"2-6-1" +] +] +}, +"3": { +"이름": "합 계", +"출처": [] +} +}, +"이름조건": { +"개월": [ +"1개월" +] +}, +"정답": [ +"GC000015" +], +"허용": [], +"무리": "씨앗", +"복합": null, +"줄수": 1 +}, +{ +"id": "C017", +"파일": "거창", +"이름": "잡철물제작및설치(현장)", +"규격": "일반철재", +"단위": "Ton", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"8-3-1" +] +], +"직종근거": [ +"용접공" +], +"인력": { +"철공": { +"품": 12.38, +"밑품": 12.38, +"율": [], +"어떻게": [ +"식" +] +}, +"용접공": { +"품": 3.38, +"밑품": 3.38, +"율": [], +"어떻게": [ +"식" +] +}, +"특별인부": { +"품": 4.5, +"밑품": 4.5, +"율": [], +"어떻게": [ +"식" +] +}, +"보통인부": { +"품": 2.25, +"밑품": 2.25, +"율": [], +"어떻게": [ +"식" +] +} +}, +"인력절": [ +[ +"1", +"철공", +12.38, +12.38 +], +[ +"1", +"용접공", +3.38, +3.38 +], +[ +"1", +"특별인부", +4.5, +4.5 +], +[ +"1", +"보통인부", +2.25, +2.25 +] +], +"장비": [], +"자재": [], +"율": [ +[ +"인력품", +5.0 +], +[ +"인력품", +3.0 +] +], +"계수": {}, +"조건": {}, +"옛판": false, +"글": [ +"잡철물제작및설치(현장)(일반철재) [Ton]", +"< 건축품셈. 8-3-1 잡철물제작및설치 (2022 개정)>", +"1. 인 건 비", +"철 공 : 239,808 × 12.38 인 = 2,968,823.", +"용 접 공 : 282,536 × 3.38 인 = 954,971.6", +"특별인부 : 226,122 × 4.5 인 = 1,017,549.", +"보통인부 : 172,068 × 2.25 인 = 387,153.", +"인부소계 : 5,328,496.6", +"2. 공구손료 및 경장비의 기계경비 ( 인력품의 5 % )", +"5,328,496.6 × 5. % = 266,424.8", +"3. 잡재료비 ( 인력품의 3 % )", +"5,328,496.6 × 3. % = 159,854.8", +"4. 합 계", +"노무비 : 5,328,496", +"재료비 : 266,424.8 + 159,854.8 = 426,279", +"경 비 :", +"계 : 5,754,775" +], +"절": { +"1": { +"이름": "인 건 비", +"출처": [] +}, +"2": { +"이름": "공구손료 및 경장비의 기계경비 ( 인력품의 5 % )", +"출처": [] +}, +"3": { +"이름": "잡재료비 ( 인력품의 3 % )", +"출처": [] +}, +"4": { +"이름": "합 계", +"출처": [] +} +}, +"이름조건": {}, +"정답": [ +"GC000821" +], +"허용": [], +"무리": "씨앗", +"복합": null, +"줄수": 1 +}, +{ +"id": "C018", +"파일": "거창", +"이름": "안내표지판 설치", +"규격": "단주식", +"단위": "개소", +"갈래": "단가산출", +"출처": [], +"직종근거": [ +"보통인부" +], +"인력": { +"특별인부": { +"품": 0.2576, +"밑품": 0.2576, +"율": [], +"어떻게": [ +"식" +] +}, +"보통인부": { +"품": 0.1287, +"밑품": 0.1287, +"율": [], +"어떻게": [ +"식" +] +} +}, +"인력절": [ +[ +"1", +"특별인부", +0.2576, +0.2576 +], +[ +"1", +"보통인부", +0.1287, +0.1287 +] +], +"장비": [ +{ +"갈래": [ +"크레인" +], +"규격": [ +5.0 +], +"시간": 0.0833, +"밑시간": 0.0833, +"율": [], +"절": "4", +"글": "트럭탑재형크레인 (5Ton)" +} +], +"자재": [], +"율": [ +[ +"노무비", +2.0 +] +], +"계수": {}, +"조건": { +"난이": [ +"보통" +] +}, +"옛판": false, +"글": [ +"안내표지판 설치(단주식) [개소]", +"1. 설치 인부", +"특별인부 : 226,122 × 0.2576 인 = 58,249.", +"보통인부 : 172,068 × 0.1287 인 = 22,145.1", +"인부소계: 80,394.1", +"3. 기구손료 ( 노무비의 2 % )", +"80,394.1 × 2. % = 1,607.8", +"4. 트럭탑재형크레인 (5Ton)", +"Q = 0.0833", +"노무비 : 49,778 × 0.0833 = 4,146.5", +"재료비 : 8,421 × 0.0833 = 701.4", +"경 비 : 11,117 × 0.0833 = 926.", +"소 계 : 5,773.9", +"5. 합 계", +"노무비 : 80,394.1 + 4,146.5 = 84,540", +"재료비 : 701", +"경 비 : 1,607.8 + 926. = 2,533", +"계 : 87,774" +], +"절": { +"1": { +"이름": "설치 인부", +"출처": [] +}, +"3": { +"이름": "기구손료 ( 노무비의 2 % )", +"출처": [] +}, +"4": { +"이름": "트럭탑재형크레인 (5Ton)", +"출처": [] +}, +"5": { +"이름": "합 계", +"출처": [] +} +}, +"이름조건": {}, +"정답": [ +"GC000308" +], +"허용": [], +"무리": "씨앗", +"복합": null, +"줄수": 1 +}, +{ +"id": "C019", +"파일": "거창", +"이름": "레미콘타설(인력운반타설)", +"규격": "소형구조물", +"단위": "㎥", +"갈래": "단가산출", +"출처": [], +"직종근거": [ +"보통인부", +"콘크리트공" +], +"인력": { +"콘크리트공": { +"품": 0.24, +"밑품": 0.24, +"율": [], +"어떻게": [ +"식" +] +}, +"보통인부": { +"품": 0.3, +"밑품": 0.3, +"율": [], +"어떻게": [ +"식" +] +} +}, +"인력절": [ +[ +"2", +"콘크리트공", +0.24, +0.24 +], +[ +"2", +"보통인부", +0.3, +0.3 +] +], +"장비": [], +"자재": [ +{ +"이름": "레미콘", +"품": null, +"밑품": null +} +], +"율": [ +[ +"노무비", +2.0 +] +], +"계수": {}, +"조건": { +"난이": [ +"보통" +], +"규모": [ +"소형" +], +"방법": [ +"인력운반" +] +}, +"옛판": false, +"글": [ +"레미콘타설(인력운반타설) (소형구조물) [㎥]", +"1. 레미콘 : 별도계상", +"2. 타설비", +"콘크리트공 : 273,540 × 0.24 인 = 65,649.6", +"보통인부 : 172,068 × 0.3 인 = 51,620.4", +"인부소계 : 117,270.", +"공구손료 및 경장비 ( 콘크리트 진동기등 ) 의 기계경비 ( 노무비의 2 % )", +"117,270. × 2. % = 2,345.4", +"3. 합 계", +"노무비 : 117,270", +"재료비 : 2,345", +"경 비 :", +"계 : 119,615" +], +"절": { +"1": { +"이름": "레미콘 : 별도계상", +"출처": [] +}, +"2": { +"이름": "타설비", +"출처": [] +}, +"3": { +"이름": "합 계", +"출처": [] +} +}, +"이름조건": { +"규모": [ +"소형" +], +"방법": [ +"인력운반" +] +}, +"정답": [ +"GF000155" +], +"허용": [], +"무리": "씨앗", +"복합": null, +"줄수": 1 +}, +{ +"id": "C020", +"파일": "거창", +"이름": "합판거푸집", +"규격": "6회, 간단", +"단위": "㎡", +"갈래": "단가산출", +"출처": [ +[ +"건설품셈", +"6-3-1" +] +], +"직종근거": [ +"보통인부" +], +"인력": { +"형틀목공": { +"품": 0.1, +"밑품": 0.1, +"율": [], +"어떻게": [ +"식" +] +}, +"보통인부": { +"품": 0.02, +"밑품": 0.02, +"율": [], +"어떻게": [ +"식" +] +} +}, +"인력절": [ +[ +"2", +"형틀목공", +0.1, +0.1 +], +[ +"2", +"보통인부", +0.02, +0.02 +] +], +"장비": [], +"자재": [], +"율": [ +[ +"주자재비", +11.0 +], +[ +"노무비", +1.0 +] +], +"계수": {}, +"조건": { +"난이": [ +"간단" +], +"횟수": [ +"6회" +] +}, +"옛판": true, +"글": [ +"합판거푸집(6회, 간단) [㎡]", +"< 2017년 품셈 6-3-1 합판거푸집(개정) >", +"※ 사용횟수 6 회 : 간단", +"1. 재료비", +"합판(내수) (12t×1220×2440mm) 8,566. × 1.03 ㎡ × 32.7 % = 2,885.1", +"외송각재 (30×30×3600mm) 406,585. × 0.038 ㎥ × 32.7 % = 5,052.2", +"주자재소계 : 7,937.3", +"소모자재 ( 박리재등 ) ( 주자재비의 11 % )", +"7,937.3 × 11. % = 873.1", +"2. 인건비", +"형틀목공 : 275,790 × 0.1 인 = 27,579.", +"보통인부 : 172,068 × 0.02 인 = 3,441.3", +"인부소계 : 31,020.3", +"공구손료 및 경장비 기계경비 ( 노무비의 1 % )", +"31,020.3 × 1. % = 310.2", +"3. 합 계", +"노무비 : 31,020", +"재료비 : 7,937.3 + 873.1 = 8,810", +"경 비 : 310", +"계 : 40,140" +], +"절": { +"1": { +"이름": "재료비", +"출처": [] +}, +"2": { +"이름": "인건비", +"출처": [] +}, +"3": { +"이름": "합 계", +"출처": [] +} +}, +"이름조건": { +"난이": [ +"간단" +], +"횟수": [ +"6회" +] +}, +"정답": [ +"GC000211" +], +"허용": [], +"무리": "씨앗", +"복합": null, +"줄수": 1 +} +] \ No newline at end of file diff --git a/docs/raw/조사/2026-10-15_338_실험자료/README.md b/docs/raw/조사/2026-10-15_338_실험자료/README.md new file mode 100644 index 000000000..2f05e7079 --- /dev/null +++ b/docs/raw/조사/2026-10-15_338_실험자료/README.md @@ -0,0 +1,11 @@ +# 338 실험 자료 (요구 225~229 · 브레인 스크래치패드 사본) + +옮겨 쓸 알고리즘의 본보기 — 그대로 돌리는 코드 아님(자료 경로 · 중간 파일은 원래 스크래치패드 기준). 저장소 모듈로 옮길 때 읽기만. + +- `C1/` — 정답 표 `labels.py`(북부청 임도 · 사방 · 사람 정답) · `labels_y.py`(영월) · `out_items.json`(줄 id · 단산 번) · 지표 셈 `s04_eval.py` · STmate 흉내 `s09_stmate.py` +- `C4/` — 로직 지문: 엑셀 블록 지문 `s1_excel_fp` · `fp_common` · 우리 로직 지문(M01 엔진 셈) · 순위 · 딱 맞음 +- `C5/` — 조합 풀이: `c01` 나무 · 절 · 몫 · `c05` combine · cover · is_mixed · 호표 조합 맞음 +- `C6/` — STmate · 거창 읽기 · 노무 목록 이음 · 자동 정답 · 기억 흉내 +- `C7/` — 겹쳐 잼 · 섞인 줄 몫 먼저 · 가림 시험 +- `K5/` — 옵션 거꾸로 찾기 · `targets.json`(새 파일 51 줄 정답) +- 설계 = `../2026-10-15_338_일위대가맞춤_설계.md` diff --git a/docs/raw/조사/2026-10-15_338_일위대가맞춤_설계.md b/docs/raw/조사/2026-10-15_338_일위대가맞춤_설계.md new file mode 100644 index 000000000..123feb252 --- /dev/null +++ b/docs/raw/조사/2026-10-15_338_일위대가맞춤_설계.md @@ -0,0 +1,566 @@ +# 요구 229-4 PA4 — 외부 일위대가 맞춤 설계 (연결된 단가산출 → 우리 로직 + 옵션 → 우리 일위대가) + +재료 = 336 조사 셋(목적다시 · 재검토 · 이름맞춤 검토 + 부록3 · 부록4) · 스크래치패드 실험(`req226/C4 · C5 · C6 · C7` · `req227/K5`) · 저장소 코드 HEAD 8a4598c05(M02 맞춤 코드는 510b36eea 뒤 안 바뀜 · 줄 번호 = 지금 파일) · 이번 읽기 확인(`req229/PA4/p01~p05.py` · 읽기만 · 파일마다 1 초 안). 저장소 · git 쓰기 없음. 파일 자리는 따로 적지 않으면 `M02_MasterTemplete/` 아래. +- 실험 자료 = 같은 폴더 `2026-10-15_338_실험자료/`(스크래치패드 사본 · 「338_실험자료/C4/…」 꼴로 적음 · 읽기만). + +## 0. 요약 + +1. 사용자 말(229) = 엑셀 호표 줄 → 이어진 단가산출 블록(목록 · 식) → 우리 단가산출 로직 + 옵션 → 우리 일위대가 하나 — 336 실측이 이 길을 받침(옵션만으로 엑셀 품 그대로 58% · 엑셀 덧셈 20% · 원문과 다른 우리 로직 0). +2. 「하이퍼링크」 실제 — 북부청 · 거창 = 셀 하이퍼링크 0 · HYPERLINK 식 0 · 비고 칸 「단산N참조」 번호 글이 유일한 이음 / STmate = HYPERLINK 식이 있으나 보이는 글이 줄 코드(「D01352 →」)이고 판에 따라 없음(울진대흥 0) → 이음 열쇠 = 블록 번호 · 줄 코드(O 열 · 다섯 파일 모두). +3. 만들 것 = 서버 맞춤 엔진(엑셀 지문 · 우리 지문 캐시 · 순위 · 옵션 거꾸로 · 판정 · 조합) + STmate · 거창 읽기 + 별칭 장부 + 확인 창. +4. 판정은 숫자 — 「맞음」 = 1위 로직 + 찾은 옵션이 엑셀 품(직종 품 · 장비 시간 · 자재 수량)을 그대로 냄 · 「다른 점」 = 로직은 같음(직종 묶음 + 인용 조항) · 품만 다름 · 그 밖 「후보」 · 이름은 약한 덤 · 금액 안 씀. +5. 들어가는 것 = 사용자가 체크하고 [확인하고 사전조합 만들기]를 누른 줄만 · 그때 별칭 장부에 「엑셀 이름 → 로직 + 옵션 + 배」. +6. 셈 = 서버 단독(CLAUDE.md 5장 ③) · 올릴 때 뒤 스레드 · 우리 지문 캐시(마스터 판 열쇠 · 처음 ≈ 2 분 · 다음은 읽기만) · 화면 잠금 없음. +7. 창 셋 — 가 = 맞춤 핵심 서버(새 다섯 파일 + `M02_Hopyo_Match.py`) · 나 = 읽기(STmate · 거창 · 블록 글 · 인용 조항 손질) · 다 = 확인 · 별칭 장부 · 만들기 서버 + 화면 전부. +8. 완료 기준 — 사람 정답 126 줄 1순위 ≥ 92% · 다섯 안 ≥ 96% · 「맞음」 틀림 ≤ 2 · 확인 없이 들어가는 줄 0 · STmate 6 파일 올라감 · master_data 그대로. + +### 0-1. 브레인 이해 — 맞음 / 다름 + +| 브레인 이해 | 판정 | 덧 · 고침 | +|---|---|---| +| 호표 한 줄 = 하이퍼링크 · 「단산N참조」 · STmate 줄 코드(D)로 단가산출 블록 하나에 이어짐 | 반만 맞음 | 엑셀 파일 속 하이퍼링크는 이음이 아님(1 장) · 이음 = 번호 글 · 줄 코드 · 모든 줄이 블록에 이어지지도 않음 — 기계(hr) · 노임 · 자재 · % · 다른 호표 줄은 블록 없음(북부청 임도 415 줄 중 단산 268 · 자재 78 · 별도계상 64 · 기계 3 · % 1 · 호표 1 / STmate 봉화 216 줄 중 단산 103 · 노임 30 · 호표 22) | +| 블록의 목록(직종 · 장비 · 자재 · 수량)과 계산식을 읽음 | 맞음 | 북부청 = 식 글을 풂(맨 앞 단가를 떼고 남은 식 · 514 풂 · 32 못 풂) · STmate = 자원 줄 H 열 품이 곧 품(식 안 풂) · 거창 = 글이 한 줄 여러 칸에 흩어져 이어 붙여야 읽힘 · 지금 코드는 블록에서 출처 · 직종만 읽고 글 · 품을 안 남김 | +| 우리 단가산출 로직(+ 옵션 = 입력 칸 값)을 찾음 | 맞음 | 차례 = 로직 먼저(지문 = 직종 · 품 · 장비 · 조항 · 조건 · 이름 덤) → 그 로직에서 옵션 거꾸로(엔진) · 품만으로 로직을 고르면 1순위 60% 로 약함(K5) | +| 호표 줄을 모아 우리 일위대가(사전조합 = 로직 + 옵션 + 수량 · 줄 별칭 = 엑셀 이름) 하나로 | 맞음 | 덧 — 블록 하나 = 로직 여럿 가능(철근 가공 + 조립 · 섞인 줄 인력 몫) → 사전조합 줄 여럿(같은 별칭) · 자재 줄 = 우리 자재 · 다른 호표 줄 = 그 호표 사전조합 참조 · % 줄 = 안 넣음(우리 덧줄이 셈) · 노임 · 장비를 바로 적은 호표(STmate 49 · 북부청 5 · 거창 5) = 호표 통째를 로직 하나로 | +| 이름은 판정에 거의 안 씀 | 맞음 | 이름 = 순위 덤(무게 0.6 / 약 9.5) · 후보 풀 · 별칭 장부 열쇠(엑셀 이름 → 짝)에만 큼 | +| 금액 안 씀 | 맞음 | 식 맨 앞 수(노임 · 장비 단가)는 떼고 셈 · 식을 못 풀 때만 금액 ÷ 단가로 품을 되찾음(「역산」 꼬리표) · 금액 비교는 만든 뒤 「참고」 한 줄 | +| 확인한 줄만 들어가고 그때 짝이 별칭으로 남음 | 맞음 | 지금 코드는 「같음」 · 「기억」 · 「호표」 줄과 [로직 추가] · 엑셀 보기 창 길이 확인 없이 들어감 → 없앰(4 장) | + +--- + +## 1. 연결 — 호표 줄 → 단가산출 블록 + +### 1-1. 실측 (이번 · 읽기 전용) + +| 파일(꼴) | 셀 하이퍼링크 | HYPERLINK 식 | 다른 시트 참조 식 | 실제 이음 | +|---|---|---|---|---| +| 북부청 임도 · 사방 xls | 0(일위대가 · 단가산출 · 총괄표) | 0 | 0 — 일위대가 시트 식 칸 1 개뿐(값만 든 내보내기) | 비고 「단산N참조」 268 / 268 · 139 / 139 · 「일위N참조」 1 · 2 · 빈 비고 146 · 60 | +| 거창 xlsx | 0 | 0 | 0 — 일위대가 식 432 = 줄 안 합(「J5+L5+N5」) | 같은 「단산N참조」 21 · 「일위N참조」 12 | +| STmate xlsx(봉화 · 영월 · 울진소광) | 2 씩(머리 칸 STmate 안내 풍선) | 일위대가표 249 · 162 · 134 / 단가산출근거 388 · 456 · 237 | — | HYPERLINK = 일위대가표 Z 열 줄마다 「L00004 →」 「D01352 →」(목록표 행으로 감) · B 열 호표 머리 「BDCOD\|B00004 · POSS\|7 · POSE\|9」 · 단가산출근거 G 열 블록 머리 「BDCOD\|D00001 · POSS · POSE」 · Y 열 자원 → 목록표 | +| STmate 줄 코드 칸 | — | — | — | O 열(14) 「D00096_1」 다섯 파일 모두(248 · 161 · 133 · 205 · 39) · Z 열 HYPERLINK 글 넷(울진대흥 0) · M 열(12) 비고는 판마다 다름(「N년개정」 · 「D96」 · 「산근N호표」 · 「대가N」 · 「LN」) | +| STmate 단가산출근거 | — | — | — | 블록 머리 G 열 「BDCOD\|D…」 다섯 모두 · 자원 코드 M 열 「L00002_1」 다섯 모두 · A 열 「L00002L」 셋 · 열 수 25(봉화 · 대흥 · 소광) / 45~47(영덕 · 영월) | + +### 1-2. 지금 읽기 코드가 이음을 쓰는 법 + +- `M02_Hopyo_Excel._REF :34` = 비고 「단산N참조」 · 「일위N참조」만 → 줄 `참조 {종류, 번}` · 블록 = `M02_Hopyo_Basis.read :84-125` 산출근거 열 머리 「N.이름[단위]」 번호 → `read_book :257-261` 이 줄에 `근거 {출처, 직종, 행}`만 붙임(블록 글 · 품 · 장비 · 조건 안 남김). +- 시트 = `Excel._sheets :74-102` 가 「일위대가」 · 「일위대가총괄표」 · 「단가산출」 이름만 읽음 → STmate(일위대가표 · 단가산출근거)는 400 「일위대가 시트 없음」. +- 거창 = 번호 이음은 되나 `Basis.read` 가 산출근거 한 열만 읽어 블록 글 · 출처 · 직종 0. + +### 1-3. 하이퍼링크를 쓰면 더 튼튼한가 — 아님 + +- 북부청 · 거창 — 쓸 하이퍼링크가 없음. 번호 글이 유일한 이음이고 지금도 268 / 268 · 139 / 139 · 21 / 21 이어짐. +- STmate — HYPERLINK 의 보이는 글 = 줄 코드와 같은 글 · 대상 주소는 식(`data_only=False`)으로 읽어야 하고 목록표를 한 번 더 거침 · 판에 따라 빠짐(울진대흥) → 줄 코드 칸보다 약함. +- 그래서 정함 — 열쇠 = 블록 번호 · 줄 코드 · 하이퍼링크는 안 읽음 · 대신 번호 글 꼴 · 코드 칸 자리를 넓혀 판 차이에 견딤(아래). + +### 1-4. 정함 — 이음 열쇠 (나 창) + +| 꼴 | 줄 → 블록 | 넓힘 | +|---|---|---| +| 엑셀꼴(「일위대가」 시트) | 비고 `^(단산\|산근\|단가산출)\s*(\d+)\s*(참조\|호표)?$` → 단산 N · `^(일위\|대가\|일위대가)\s*(\d+)\s*(참조\|호표)?$` → 호표 N(빈칸 무시) | 비고가 비었고 노무비 · 경비 단가가 있는 줄 = 줄 품명 + 단위가 블록 머리 이름 + 단위와 같은 블록이 하나뿐이면 이음 · 꼬리표 「이름으로 이음」 | +| STmate | 줄 M~Z 열에서 코드 꼴 `^(?:COD\|)?([A-Z]{1,2}\d{5})(?:_\d+)?(?: →)?$` 첫 것 → D = 단산(번 = 코드 숫자 · 코드 남김) · B = 호표(호표 머리 코드 표로 번) | 열 번호에 안 묶임 → 판(25 · 45~47 열)이 달라도 읽힘 | +| 블록 머리 | 엑셀꼴 = 산출근거 열 「N.이름[단위]」 · STmate = G 열 「BDCOD\|D…」 + 단가산출근거목록표(이름 · 규격 · 단위) | — | + +--- + +## 2. 맞춤 알고리즘 명세 + +### 2-1. 흐름 + +``` +올리기 ─ 읽기(나) ─ HP 파일(호표 · 줄 · 블록 글 · STmate 자원) ─ 저장 + └─ 뒤 셈(가) Fit.ensure(층, 키) ── 블록마다(같은 블록 · 같은 줄 열쇠는 한 번) + ① 엑셀 지문(Finger) → ② 순위(지문 점수 + β × 인용 조항 차례 덤) → ③ 옵션 거꾸로(위 다섯 · 엔진) + → ④ 판정(맞음 / 다른 점 / 후보) → ⑤ 조합(절 · 몫 · 섞인 줄 몫 먼저) → 맞춤셈 캐시 파일 +보기(Match.view) = 맞춤셈 캐시 + 별칭 장부 + 갈래 규칙 → 줄 상태 · 체크 제안 (싸게 · 요청마다) +고침(Match.guess) = 고른 로직 · 옵션으로 엔진 한 번 → 우리 품 ↔ 엑셀 품 (저장 안 함) +확인(다) = [확인하고 사전조합 만들기] → 서버 재검사 · 우리 엔진으로 셈 · 사전조합 · 별칭 장부 +``` + +### 2-2. ① 엑셀 지문 — 새 `M02_Hopyo_Finger.py` (가 · 약 450 줄) + +옮길 것 = C4 `fp_common.py`(식 풀기 · 장비 갈래 · 조건 낱말 · 닮음 도우미) + `s1_excel_fp.north` · `yeongwol` + C5 `c01_trees.sections` · `sections_stm`(몫) — 한 번 도는 고리에서 지문 · 몫을 함께 냄. + +| 함수 | 입력 → 출력 | +|---|---| +| `excel(block, line) -> dict` | HP 블록(3-1) + 그 줄(이름 · 규격 · 단위) → 지문(아래) | +| `machine(line) -> dict` | 블록 없는 기계(hr) 줄 → 지문 `장비 [{갈래, 규격, 시간 1}]` | +| `direct(hopyo) -> dict` | 노임 · 장비 줄을 바로 적은 호표 → 줄 수량 합 지문 + 근거글 출처 | +| `parse_rhs(rhs) -> {단가, 품, 밑품, 율, 인자} \| None` | 「273,540 × 3인 ÷ 55㎥ × 2 = 29,840」 → 품 0.10909 | +| `mtypes(text)` · `specnums(text)` · `conds(text)` | 장비 갈래 · 규격 수 · 조건 낱말(엑셀 · 우리 같은 함수) | + +지문 꼴: + +``` +{이름, 규격, 단위, + 인력: {직종: {품, 밑품, 율: [[값, 메모]], 어떻게: "식" | "역산" | "QTY"}}, + 장비: [{갈래: [..], 규격: [수..], 시간, 밑시간, 율, 절, 글}], + 자재: [{이름, 품, 밑품}], 율: [[바탕, %]], 계수: {q: 0.5}, + 조건: {축: [낱말]}, 이름조건: {축: [낱말]}, 출처: [[책, 번호]], 옛판: bool, + 몫: [{제목, 절, 노임: {직종: 품}, 장비: {"굴착기|0.6": 시간}}]} +``` + +규칙(실험 그대로): +- 절 = `^\d+\.` 맨 윗 절 · `^[가-하]\.` 작은 절(STmate `^\d+[.)]` · ①~⑩) · 절 제목 장비 낱말 = 지금 장비. +- 직종 줄 「직종 : 단가 × 식 = 금액」(직종 = 우리 인력 이름 · 옛이름 — `Basis.trade_names`) → `parse_rhs` = 맨 앞 수(단가) 떼고 식 셈 · 「× 10%」 · 「(품할증)」 · 「× 2회」 = 율(밑품 = 율 뺀 품) · 바꿔 쓴 식(「= 215,907×4×0.026」)은 앞 조각 율을 이어받음 · 율 메모 없는데 맨 인자가 이름 · 절 머리 몫(「9:1」 · 「(90%)」)과 같으면 「섞음」 율 · 못 풀면 금액 ÷ 단가(「역산」 · 품 되찾기만). +- 장비 = 「노무비 · 경비 · 재료비 : 단가 ÷ Q × 몫」 + 지금 장비(노무비 > 경비 > 재료비 차례) · 자재 = 「이름 단가 × 수량 = 금액」 · 「: 별도계상」. +- 율 「…의 N%」 · 계수 `q|K|f|E|Cm|L|V|T = 수` · 조건 축(토질 · 메찰 · 형 · 무근/철근 · 난이 · 규모 · 방법 · N회 · N개월 · Type) = 이름 · 규격 · 글 앞 다섯 줄 · ※ · 작업조건 줄 · 옛판 = 글 앞 여섯 줄에 2020 이하 해 · 「임도품셈」 · 「사방품셈」. +- STmate = 자원 줄 품(H 열)이 곧 품 · L = 노무비목록표 이름 · X = L 몫(시간) 앞섬(L > S > M) · S 중 장비 낱말 = 장비 · 몫 율 = 절 제목 몫 수가 식 글에 곱으로 있으면. +- 몫(조합용) = 절마다 노임 · 장비 벡터 — C5 `sections` 결과와 같아야 함(시험). + +### 2-3. 우리 로직 지문 · 캐시 — 새 `M02_Hopyo_Ours.py` (가 · 약 300 줄) + +옮길 것 = C4 `s2_ours_fp.py`(`static` · `combos` · `run_one` · `fingerprint`) + C4 `s3_match.prep`(섞기 전 변형 · 고를 수 있는 장비). + +| 함수 | 하는 일 | +|---|---| +| `version() -> str` | 마스터 판 열쇠 = `resources/master_data` 첫 층 *.json(밑줄 뺌) 이름 + `M01_MasterData_Store.version_of`(sha256 16) 묶음의 sha256 앞 16 · 파일 자국(`Store_Cache._stamps`)이 같으면 다시 안 셈 | +| `table() -> list[dict] \| None` | 우리 지문 1,438(돈 로직 = `Match.targets()["로직"]`) · 준비 전 None | +| `ready() -> bool` · `warm() -> Thread` | 디스크 캐시 있으면 읽기만 · 없으면 뒤 스레드 셈(≈ 110 초 · C4) | +| `run(code, given) -> {L, X, M, S, mix} \| None` | 엔진 한 번(`mau.values` 자동값 + 입력 → `m01.cm.mf.run` · `Labor_Doc.run_line` 과 같은 길) · 메모리 캐시(5 만 칸 · ponytail) | + +- 정적 = 호표 인력 → 직종 · 「로직(기계경비 …)」 → 장비 갈래 · 규격(EQ) · 재료 이름 · 덧줄 % · 조항(`Basis.ours`) · 이름 · 단위 · 이름 · 고르기 글 조건 낱말 · 기계경비 로직 표시. +- 변형 = 고르기 입력 곱 ≤ 64 모두 · 넘으면 하나씩 · 뺌(지역 · 조종원직종 · 보정작업 · 기종 목록 칸) · 줄마다 단위당 L · X · M · S(결과단위 「원/100㎡」 ÷ 100) · 인력비율 · 기계비율 든 로직 = 섞기 전 변형 덧. +- 캐시 자리 = `tmp/m02_hopyo/우리지문_<판>.json`(저장소 `tmp/` = git 밖 · 환경 `AISLO_HOPYO_CACHE` 로 바꿈 · 시험은 임시 폴더) · 쓰기 = `atomic_write_json`. +- 시동 = `M02_Labor_Router` 불러올 때 `warm()`(디스크 읽기만 · M01 `store.warm()` 과 같은 자리) · 셈은 처음 필요할 때(올리기 · 보기) 뒤에서. +- 원본 = `m01.loaded()` 읽기 · 엔진 부르기만 · `resources/master_data` 쓰기 없음. + +### 2-4. ② 순위 — 새 `M02_Hopyo_Fit.py` (가 · 약 450 줄) + +옮길 것 = C4 `s3_match.score_pair` · `lab_sim` · `mach_sim` · `unit_fit` · `sure` · `fuse` + C7 `k0_lib.finger` · k8(딱 맞음이 같음을 이김). + +- 점수(로직마다 가장 닮은 변형) = 3.0 인력 + 2.0 장비 + 1.0 자재 + 0.3 율 + 0.5 단위 + 0.6 조건 + 1.5 조항 + 0.6 이름 · 금액 없음. + - 인력 = 0.35 직종 겹침(보통인부 반 무게) + 0.25 꼴(코사인 · 품 · 밑품 큰 쪽) + 0.40 크기(직종마다 ln 비 3% 안 = 1) · 단위가 다르면 크기 덤 없음(단위 별칭 개소 = 개 = 본 · hr = 시간) · 엑셀에 없는 우리 인력 −0.6 · 장비 −0.4. + - 장비 = 갈래 겹침 0.6 + 규격 0.2 + 시간 0.2 · 우리에만 있는 갈래는 깎음. + - 조항 = `Basis.clause`(같음 1 · 윗마디 0.9 · 앞 마디 0.5) × 옛판 0.5 · 조건 = 엑셀 조건 낱말이 우리 이름 · 고르기 글에 든 비율 − 이름 축 어긋남(토질 · 메찰 · 형) · 이름 = 정리 이름 글자 두 개 묶음 닮음. +- 섞기 = 지문 위 40 ∪ B 차례 위 5 → 점수 + β(1) × B 차례 덤(1 · 0.6 · 0.45 · 0.35 · 0.25) → 위 다섯. +- B 차례 = `Match.b_order(line, got)` — 지금 이름 후보 + 근거 후보 합침(`Basis.merge`) + 인용 조항 맞대기 손질(접힘 없음 · 「같음」 근거 검사 · 옛 판 글이면 같음 안 냄) · 씨앗(일위대가조합.json 메모)은 이름 같음과 같은 자리(덤만). +- 같음 규칙 = B 1위가 이름 같음 · 씨앗(근거 검사 통과)이면 1위 고정 — 단 지문 「딱 맞음」(1위 직종 묶음 같음 · 품 3% 안 · 2위와 점수 차 0.3 넘음 · 보통인부 하나뿐 줄 아님)이 다른 로직을 가리키면 지문 1위. +- 기대(사람 126 · 씨앗 켬) = 1순위 88.9 · 다섯 안 96.8(C4 · 336 재검토 ②) · 씨앗 끔 85.7. + +### 2-5. ③ 옵션 거꾸로 — 새 `M02_Hopyo_Option.py` (가 · 약 400 줄) + +옮길 것 = K5 `k5_lib.py`(`excel_numbers` · `space` · `narrow` · `combos` · `plan` · `sig` · `near` · `ru` · `compare` · `gap` · `csig` · `strong` · `vary`) + `k5_s2_answer.MODES` · `ex_of` + `k5_s5_rank.stage_b`. + +| 함수 | 하는 일 | +|---|---| +| `space(code, ex)` | 입력 칸마다 후보 — 고르기 = 모두 · 기계(EQ) = 엑셀 규격 수와 맞는 기종(없으면 모두) · 범위 = 두 끝 + 범위 안 엑셀 수 · 표 찾기 칸 = 그 표 줄 값(`master_auto._fits`) · 그 밖 수 = 자동값 + 같은 이름 계수 + 칸 이름 · 단위 붙은 엑셀 수(% 칸 = 0 덧 · 「품」 칸 = 엑셀 품 × 결과단위) · 뺌 = 지역 · 보정작업 · 조종원직종 | +| `plan(code, ex, cap=400)` | 곱 ≤ cap 모두(안 닿는 칸 · 같은 품 값 묶음) · 넘으면 엑셀 조건 낱말로 고르기 좁힘 · 그래도 넘으면 기준점 둘에서 하나씩 + 고르기 둘씩 | +| `compare(efp, v, tol, rnd, base)` | 직종 묶음 같고 품 허용 안 · 같은 갈래 장비 시간 · 이름 낱말 겹치는 자재 수량 → `{직종, 장비, 자재, 핵심, 덧: ["엑셀만 장비 덤프" …]}` | +| `strong(efp, cmp)` | 핵심 + 엑셀 몫을 다 덮음(엑셀만 · 우리만 장비 없음 · 자재 다름 없음 · 보통인부 하나뿐 줄 아님) | +| `find(code, efp) -> 옵션 결과` | 조합마다 `Ours.run` → 허용 셋(0.5% · R · 3%) 판정 → 아래 꼴 | + +``` +옵션 결과 = {수준: 0 | 1 | 2 | 9, # 0.5% · R(0.5% + 엑셀 값 자릿수 반올림 폭 · 값의 4% 안) · 3% · 없음 + 판정: "하나만" | "여럿(같은 품)" | "여럿(다른 품)" | "없음", + 입력: {움직이는 칸: 값}, 갈리는칸: ["측압", "폼타이규격"], + 견줌: compare(…, 0.03), 우리품: {인력, 장비, 자재}, 거리: 0.0, 섞기전: bool} +``` + +- 같은 수준이면 거리(직종 어긋남 + 품 ln 비 + 장비 시간 반 무게) 작은 것 · 품에 안 닿는 칸(물빼기파이프 · 모래 · 자갈상태 · 강도 · 슬럼프) = 자동값 · 엑셀 자재 이름 · 규격 글이 고르기 글과 같으면 그 값. +- 입력값 출처(칸마다) = 별칭(장부) > 블록(찾은 값이 자동값 · 규격 읽기와 다름) > 규격(`Match.spec_inputs`) > 자동값. +- 기대(K5) = 1위 로직이 맞은 103 줄 중 옵션까지 엑셀 품 그대로 56 · 입력값을 옵션 찾기로만 채우는 줄 46(레미콘 「소량」 → 소량감률 50 · 합판 「4회」 → 유형 보통). + +### 2-6. ④ 판정 (Fit) + +| 상태 | 조건(1위 기준) | 체크 제안 | 글 · 꼬리표 | +|---|---|---|---| +| 맞음 | 옵션 수준 ≤ 1 · `strong` | 체크 | 「맞음 — GC000198 · 장비사용타설 · 철근구조물 · 소량감률 50」 · 딱 맞는 로직이 둘 넘으면 「맞음 둘」 + 둘째를 후보 맨 앞 · 갈리는 칸이면 「측압 · 폼타이규격 못 가름」 | +| 다른 점 | 1위 직종 묶음 = 엑셀 · 엑셀 인용 조항 = 그 로직 조항(`Basis.clause` ≥ 0.9) · 수준 2(3% 안) 포함 | 안 함 | 「특별인부 엑셀 0.354 · 우리 0.370」 · 밑품이 같으면 「섞음 — 엑셀이 × 10%」 · 율 「할증 × 1.2」 · 「엑셀만 장비 덤프」 · 가까운 옵션을 입력값으로 | +| 후보 | 그 밖 | 안 함 | 후보 다섯 — 후보마다 그 로직 옵션 판정 꼬리표(위 다섯은 다 옵션 찾기를 돌림) | + +- 기준 = K5 4-1 · 기대(사람 126) = 맞음 58 · 틀림 1 · 다른 점 25 · 틀림 1(직종만 보면 39 · 틀림 16 → 인용 조항까지). +- 「거의 맞음」 · 「옛 판」 같은 까닭은 자동으로 안 지음 — 숫자 사실(엑셀 · 우리 품)만 · 까닭은 사용자가 [원문 보기]로. + +### 2-7. ⑤ 조합 — 새 `M02_Hopyo_Combine.py` (가 · 약 300 줄) + +옮길 것 = C5 `c03_vec.sim` · `fit` · `dims` · `index` + `c05_solve.combine` · `cover` · `is_mixed` · `machine_of` · `best_hint` · `Prior` · `pool_for` + C7 `k0_lib.solve`(섞인 줄 1순위 바꿈). + +- 할 때 = 블록 몫 둘 넘음 또는 섞인 줄(제목 · 이름 · 규격에 「N%」 · 「a:b」 + 노임만 든 몫 + 장비만 든 몫). +- 우리 벡터 = `Ours` 변형의 L · X(같은 캐시 · 따로 안 셈) · 맞음 = 차원마다 작은/큰 가중 평균(보통인부 0.5 · 장비 있음 0.5 + 시간 0.5 · 크기 다르면 × 0.8 · 굴착기 ↔ 굴착기+집게 × 0.8) · 몫 x ∈ {0.1 … 0.9} × 0.95 · 제목 몫 수와 같은 x 면 +0.1. +- 섞인 줄 몫 먼저 = 1위가 블록 합을 35% 못 덮고 몫 · 절 로직이 있으면 1위를 인력 몫 로직으로 바꿈(x = 몫 · 고임돌 「인력 10%」 → GF000211 × 0.1) — 지문 × 조합을 그냥 겹치면 조합 맞음 49.4% 로 깎이던 것을 막는 규칙. +- 덧 로직 = 1위가 못 덮는(덮음 < 0.6) 절은 절 1위(맞음 ≥ 0.75) · 아니면 몫마다(≥ 0.6) · 장비만 든 몫 = 우리 일 로직이 맞으면 그것 · 아니면 「기계 시간당 기계경비」(GC000267 · GC000268) 후보 — 체크 안 함 · 꼬리표 「시간 = 엑셀 값(우리 셈 아님)」. +- 순위 덤 = 지금 차례 0.3 · 인용 조항 같음 0.3 · 같은 책 0.1 · 후보 풀 = 섞기 위 15 + 드문 직종 · 장비 색인(40 개 이하) + 절 제목 이름 후보 8. +- 결과 = 대상 목록 `[{로직, 어디(절 제목), 몫, 맞음}]` → 덧 로직마다 그 몫 지문으로 옵션 거꾸로. +- 기대(336 재검토 ③) = 1순위 92.9 · 조합 맞음 75.9(덮을 수 있는 83 호표 · 63). + +### 2-8. 블록 없는 줄 · 호표 단위 + +| 줄 | 처리 | +|---|---| +| 기계(hr) 줄 | `Finger.machine` → 같은 순위 · 옵션(기계 칸 = 엑셀 규격 수 기종) → 「기계 시간당 기계경비」 + 기계 · 펌프차 · 크레인 「맞음 둘」 많음 | +| 노임 줄(단위 인 + 우리 직종) | 호표에 노임 · 장비 줄이 있으면 직접 줄 호표 → `Finger.direct` → 같은 순위 · 판정 → 호표 단위 제안 「통째 = GF000225 메붙임(굴착기 + 집게)」(C5 (나) 벡터 + 인용 32 / 34) · 통째로 안 맞으면 노임 줄 「노임 · 그릇 없음」 | +| 자재 · 별도계상 | 지금 이름 · 규격 같음(`Match._exact` 자재) — 하나 = 맞음 · 체크 / 그 밖 후보 | +| % 줄 | 안 들어감(우리 덧줄이 셈) · 체크 없음 | +| 다른 호표(일위N) | 그 호표가 만들 층에 확인돼 있으면 체크 · 아니면 「먼저 확인」 | +| 블록 없는 그 밖 줄 | B 차례 후보(이름 · 근거 · 씨앗) → 「후보」 · 꼬리표 「숫자 판정 없음」 | + +### 2-9. 상수 한 곳 (`M02_Hopyo_Fit.py` 머리 · 손잡이) + +`W = {인력 3.0, 장비 2.0, 자재 1.0, 율 0.3, 단위 0.5, 조건 0.6, 조항 1.5, 이름 0.6}` · `BETA = 1.0` · `RB = (1, .6, .45, .35, .25)` · `POOL = 40` · `TOP_OPT = 5` · `CAP = 400` · `TOL = (0.005, "R", 0.03)` · `SURE = (0.03, 0.3)` · `MIXED_CUT = 0.35` · `COMBO = (0.85, 0.75, 0.6)` · `COVER = 0.6` · `SHARES = (0.1, 0.2, 0.3, 0.5, 0.7, 0.8, 0.9)` — 값은 336 · K5 실험 그대로(무게를 한 파일로 다시 맞추지 않음 · 336 재검토 2-5). + +### 2-10. 뒤 셈 · 캐시 · 계산 자리 + +| 할 것 | 정함 | +|---|---| +| `Fit.ensure(folder, key) -> {끝, 한, 모두, 까닭}` | HP 파일 sha256 16 + 마스터 판 → 캐시 파일 있으면 끝 · 없으면 일감 줄(작업 스레드 하나 · `queue.Queue`)에 넣음 · 블록 차례로 셈 · 20 블록마다 캐시 파일 덮음 | +| `Fit.result(folder, key) -> dict \| None` | 캐시 읽기(보기 · 미리 셈이 씀) | +| 캐시 파일 | `tmp/m02_hopyo/맞춤__<판16>.json`(꼴 3-2) · 층 폴더에 안 둠(마스터 층 `resources/master_template/실무자료` 는 git 안 · 파생 값으로 git 소음) | +| 부르는 때 | 올리기 길 저장 뒤 · 보기(없거나 판이 바뀜) · 우리 지문 준비 전이면 「우리 로직 준비 중」 | +| 셈 단위 | 블록 + 줄 열쇠(`단산:<번>\|<품명\|규격\|단위>`) 한 번 · 블록 없는 줄 `줄:<열쇠>` · 직접 줄 호표 `호표:<번>` | +| 계산 자리 | ③ 서버 단독 · 화면은 보이기만 · 옵션 바꿈 = `guess` 를 뒤에서 · 최신 응답만(요청 차례 번호) · 화면 전체 잠금은 [확인하고 사전조합 만들기]에만 | +| 무게 | 우리 지문 처음 ≈ 110 초(C4) · 캐시 읽기 몇 초 · 옵션 찾기 줄당 0.1~26 초(K5 · 엔진 셈 캐시로 줄어듦) → 북부청 임도 고유 52 줄 · 블록 95 = 몇 분 뒤에서(측정해 기록) | +| 위험 | 엔진을 뒤 스레드에서 돌림 — 요청 스레드 셈과 겹칠 때 값이 그대로인지 시험 · 안 되면 셈 잠금 하나 · `master_auto._uses` · `_fits`(밑줄 함수) 의존은 시험이 잡음 | +| ponytail | 일감 줄 하나 · 서버 여럿이면 같은 파일을 둘이 셈 · 블록 글이 같은 다른 파일도 다시 셈 · 캐시 파일 지우기 없음(작음 · `tmp`) | + +--- + +## 3. 자료 꼴 — 지금 꼴과 이어지게 + +### 3-1. HP 파일(`{층}/실무자료/HP-######.json`)에 더할 것 — 나 창 + +``` +{…지금 칸(키 · 파일 · 올린때 · 올린이 · 시트 · 호표 · 단산 · 검산), + 꼴: "엑셀꼴" | "STmate", # 새 + 호표: [{번, 이름, 규격, 단위, 금액, 행, 코드?: "B00004", # 코드 = STmate + 근거글?: ["건설표준품셈 7-2-2(찰붙임)"], # STmate 빈 코드 줄 + 줄: [{…지금 칸, 갈래: + "노임", # 노임 = 단위 인 + 우리 직종 · STmate L + 참조: {종류: "단산" | "일위", 번: 96, 코드?: "D00096"}, + 이음?: "이름"}]}], # 이름으로 이은 줄만 + 단산: [{번, 이름, 단위, 행, # 지금 + 코드?, 규격?, 출처: [[책, 번호]], 직종: [이름], # 새(지금은 줄 근거에만) + 글: ["1. 현장가공", " 철근공 : 273,540 × …", …], # 머리 뺀 몸 줄 · 200 줄 · 줄마다 300 자 안 + 자원?: [{갈래: "L"|"X"|"M"|"S", 코드, 이름, 규격, 품, 절}]}]} # STmate +``` + +- 줄 `근거 {출처, 직종, 행}`은 그대로(지금 화면 · `Basis.candidates` 가 씀). +- 옛 HP 파일(글 없음) = 숫자 판정 없음 → 「다시 올리면 숫자 판정」 꼬리표(지금 격자 없음과 같은 길). + +### 3-2. 맞춤셈 캐시 — 가 창 (`tmp/m02_hopyo/맞춤__<판16>.json`) + +``` +{판, 지문판: 1, 셈: [한, 모두], 끝: bool, + 블록: {"단산:12|레미콘타설(장비사용)|철근구조물-소량 타설|㎥": { + 지문: {인력, 장비, 자재, 출처, 조건, 옛판}, # 화면 「엑셀 품」 + 순위: [{로직, 점수, 채널: {인력, 장비, 조항 …}, B: 0}], # 위 다섯 + 옵션: {"GC000198": 옵션 결과(2-5), …}, # 위 다섯 · 조합 덧 로직 + 조합: [{로직, 어디, 몫, 맞음}] | null, + 판정: {상태: "맞음" | "다른 점" | "후보", 글, 꼬리표: [..]}}}} +``` + +### 3-3. 맞춤 응답 — 줄 하나 (`GET hopyo/{키}/{번}` · 가 → 다) + +지금 `맞춤 {열쇠, 상태, 대상, 입력값, 못읽음, 환산, 들어감, 후보}`(Api `HopyoMatch :91-100`) → 아래로 바꿈(`들어감` 없앰 → `체크` · `대상` = 목록). + +``` +맞춤: {열쇠: "품명|규격|단위", + 상태: "맞음" | "별칭" | "다른 점" | "후보" | "없음" | "뺌" | "호표" | "%" | "노임", + 체크: bool, # 처음 체크 제안(확인 대기) — 들어감 아님 + 대상: [{로직? | 자재? | 호표?, 이름, 단위, + 입력값: {칸: 값}, 못읽음: [칸], 출처: {칸: "별칭" | "블록" | "규격" | "자동값"}, + 환산: {배, 갈래}, 몫: 0.1 | null, 배: 0.1 | null, # 배 = 환산 × 몫 · 우리 수량 = 엑셀 수량 × 배 + 어디?: "1. 현장가공"}], + 후보: [{로직 | 자재, 이름, 단위, 점수, 판정: "맞음" | "다른 점" | "후보", 글, 근거}], # 다섯 + 글: "맞음 — GC000198 · …", 꼬리표: ["옵션 — 블록에서", "근거 건설 6-1-1"], + 품: {엑셀: {인력: {직종: 품}, 장비: [{갈래, 시간}], 자재: [{이름, 수량}]}, + 우리: {…같은 꼴}, 같음: {"콘크리트공": true, "굴착기": true}}, + 갈리는칸?: ["측압", "폼타이규격"], + 별칭?: {층, 주인, 때, 누가, 처음파일, 규격다름: bool, 근거다름: bool}, + 뺌?: "우리에 없음" | "안 씀"} +``` + +- 호표: `{…지금, 맞춤: [체크 제안 수, 줄 수], 통째?: {대상: [..], 글, 체크}, 셈: {끝, 한, 모두, 까닭?}, 만든?: {키, 확인: bool}}` · 파일 보기 `맞춤` 합도 같은 뜻. +- 만들 층 = 길 덧 물음 `?to=<층>&to_owner=`(없으면 파일 층) — 호표 참조 줄 체크 · `만든` 은 그 층을 봄([로직 추가] 모달 = 위 층 파일 · 문서 층에 만듦). +- 상태 정하는 차례(`Match.match_line` · 가) = ① % ② 호표 참조 ③ 노임 ④ 장부 「우리에 없음 · 안 씀」 → 뺌 ⑤ 장부 짝 → 별칭 ⑥ 맞춤셈 판정 ⑦ 셈 전 · 숫자 없음 → B 후보(「셈 중」 · 「숫자 판정 없음」 꼬리표). +- 별칭 체크 = 짝이 있고 다음이 다 아닐 때 — 규격 다름(품명 \| 단위 느슨 열쇠로 찾음) · 근거 다름(엑셀 인용 조항과 짝 로직 조항이 `Basis.clause` < 0.9) · 숫자 판정 「맞음」이 다른 로직. + +### 3-4. 미리 셈 — `POST hopyo/{키}/{번}/guess` (가) + +- 몸 `{차례, 대상: [{로직 | 자재, 입력값}]}` → `{대상: [(3-3 대상 꼴)], 품: {엑셀, 우리, 같음}, 글}` · 저장 안 함 · 엔진 한 번(ms). +- 지금 몸 `{차례, 대상: {로직}}`(하나)은 목록으로 바꿈. + +### 3-5. 별칭 장부 한 줄 — 다 창 (`{층}/실무자료/맞춤.json` 키움) + +``` +{줄: {"<품명|규격|단위 정규화>": { + 원래: {품명, 규격, 단위}, + 갈래: "짝" | "우리에 없음" | "안 씀", + 짝: [{로직 | 자재 | (호표 통째 = 로직), 입력값: {}, 배: 1, 몫: null}], + 누가: {id, 이름}, 때: "2026-10-15T23:10:00", 처음파일: "…xls", 엑셀조항: [[책, 번호]], + 판: "<마스터 판16>"}, + "호표|<이름|규격|단위>": {…직접 줄 호표 통째}}} +``` + +- 열쇠 = 지금 `Match.line_key :68-76` 그대로 · 찾을 때만 품명 \| 단위 느슨 색인(규격 다름 · 체크 안 함). +- 옛 줄 `{로직 | 자재 | 빼기, 입력값, 환산, 확정}` = 읽을 때 `{갈래 짝, 짝 [{…, 배: 환산}]}` · 빼기 = 「안 씀」으로 바꿔 읽음(파일은 다음 쓰기 때 새 꼴). +- 쓰는 때 = [확인하고 사전조합 만들기] · 장부 화면 [저장] 때만 · 확인한 줄은 모두(이름 같음이라도 — 옵션 · 배를 남기려고) · 같은 열쇠는 덮음 · 처음파일은 있던 값 둠. +- 층 = 만드는 층에 씀 · 읽는 차례 개인 → 회사 → 마스터(`Match.chain :333-341`) · 아래 층이 이김. + +### 3-6. [확인하고 사전조합 만들기] 요청 — `POST /layers/{층}/hopyo-build` (다) + +``` +{층, 주인, 키, 번, 새로: false, # 지금 BuildBody 그대로 + 아래 + 줄: [{차례: 3, 대상: [{로직: "GC000198", 입력값: {…}, 배: 1, 몫: null}]}, + {차례: 4, 대상: [{로직: "GC000206", 입력값: {}, 배: 1}, {로직: "GC000207", 입력값: {}, 배: 1}]}, + {차례: 5, 뺌: "우리에 없음"}, + {차례: 6, 대상: [{자재: "PM000123", 배: 1}]}, + {차례: 7, 대상: [{호표: 12, 배: 1}]}], + 통째?: {대상: [{로직, 입력값, 배: 1}]}} # 직접 줄 호표 +→ {참조, 이름, 다시씀, 빠진줄: [{차례, 이름, 규격, 수량, 단위, 금액, 까닭}], + 비교: {엑셀, 우리, 차이, 까닭}, # 참고 한 줄(판정 아님) + 장부: {씀: n}} +``` + +- 서버 재검사(브라우저 값을 받아 적지 않음 · pydantic `extra="forbid"` — 금액 · 품 칸은 400) — 차례가 그 호표 안 · 대상 하나마다 로직 · 자재 · 호표 중 하나 · 로직 = 돈 로직 · 입력값 이름 = 그 로직 입력 · 배 0 넘고 유한 · 몫 (0, 1] · 자재 = M01 자재품목 · 호표 = 만들 층에 확인된 사전조합이 있음(없으면 409 「제N호표 먼저 확인」) · 줄 500 개 안. +- 빠진 줄 = 몸에 없는 줄(「확인 안 함」) · 뺌(「뺌 — 우리에 없음」) · % 줄(「% 줄 — 우리 로직 덧줄이 셈」). +- `줄` 이 없거나 빈 몸 = 400 「확인한 줄이 없음」(옛 「들어감」 길 없앰). +- 같은 본뜬 사전조합이 있으면 덮음(사용자가 확인한 내용이 앞섬) · 응답 `다시씀` true · 화면 글 「있던 SC-… 를 확인한 내용으로 덮음」. + +### 3-7. 사전조합 줄 — 로직 여럿 · 같은 엑셀 이름 (다) + +``` +{이름: "철근가공조립(현장) Type-Ⅰ", 단위: "ton", 구분: "실무자료", 상세구분: "<파일>", + 담은로직: [ + {로직: "GC000206", 입력값: {…}, 별칭: "철근가공조립(현장)", 메모: "실무 1ton × 1 · Type-Ⅰ", 수량: {손: 1}}, + {로직: "GC000207", 입력값: {…}, 별칭: "철근가공조립(현장)", 메모: "실무 1ton × 1 · Type-Ⅰ", 수량: {손: 1}}, + {로직: "GF000211", 입력값: {…}, 별칭: "고임돌채집및운반", 메모: "실무 0.5㎥ × 0.1(인력 10%)", 수량: {손: 0.05}}, + {참조: {층, 주인, 키}, 별칭: "…", 메모, 수량: {손}}], + 자재: [{코드, 수량: {손}, 별도계상}], + 본뜬: {층: "실무자료", 주인: null, 키: "HP-000001 제3호표", 파일, 확인: {누가: {id, 이름}, 때}}} # 확인 = 새 +``` + +- 같은 로직 둘째(줄 사이 · 한 줄 안) = 지금처럼 한 줄 사전조합으로 감싸 참조(`Labor_Doc.clean` 「같은 로직 두 번 금지」 · `Build.build :151-161`). +- `본뜬.확인` = 「이 호표 확인함」 기록 — 확인 저장소 따로 없음 · 옛 실무자료 사전조합(확인 없음)은 [로직 추가]에서 확인 창으로. +- 수량 손 = 엑셀 수량 × 배 · 값은 우리 엔진(`M02_Labor_Ref.Expand`)이 셈. + +### 3-8. 길 표 (`M02_Labor_Router.py` · 다) + +| 길 | 지금 | 바꿈 | +|---|---|---| +| `POST /layers/{층}/hopyo` | 올리기 | + STmate · 저장 뒤 `Fit.ensure` · DRM 글 | +| `GET /layers/{층}/hopyo/{키}` | 호표 맞춤 n/m | + `셈` · n = 체크 제안 수 | +| `GET /layers/{층}/hopyo/{키}/{번}` | 줄 맞춤 | 3-3 꼴 · `?to=&to_owner=` 만들 층 | +| `POST …/{번}/guess` | 대상 하나 미리 채움 | 3-4 꼴(대상 목록 · 품 비교) | +| `POST /layers/{층}/hopyo-memory` | 줄마다 즉시 확정 | **없앰** | +| `POST /layers/{층}/hopyo-build` | 들어감 줄로 만듦 | 3-6 몸 · 확인 줄만 · 장부 씀 | +| `GET /layers/{층}/hopyo-picks` | 호표마다 n/m | + `확인: {키} \| null`(만들 층에 확인된 사전조합) | +| `GET /layers/{층}/hopyo-ledger` | — | 새 · 이 층 + 위 층 줄 `{열쇠, …(3-5), 층, 주인, 고침: bool}` | +| `PUT /layers/{층}/hopyo-ledger` | — | 새 · `{줄: {열쇠: 값 \| null}}` · 이 층 쓰기만 · 짝 재검사 | + +--- + +## 4. 화면 흐름 (다 창) + +### 4-1. 확인 창 — 새 `M02_MasterTemplete_Hopyo_Check.ts` (본 칸 · 모달 둘 다) + +- 자리 — 실무자료 본 화면 호표 누름(지금 `View.ts` 호표 하나 화면을 바꿔 끼움) · [로직 추가] 「실무 호표」 모달 · 엑셀 보기 창 펼침에서 [확인 창 열기]. +- 머리 — 호표 이름 · 규격 · 단위 · 「제안 n / 줄 m」 · 셈 중이면 「숫자 판정 셈 중 한/모두」(3 초마다 다시 읽음 · 끝나면 멈춤 · 체크는 사용자가 안 만진 줄만 새 제안으로) · 통째 제안(직접 줄 호표)이면 맨 위 한 줄. +- 줄 칸 — [✓] · 갈래 · 엑셀 품명 · 규격 · 수량 · 단위 · 상태(색 — 맞음 초록 · 별칭 파랑 · 다른 점 주황 · 후보 회색) · 우리 로직(이름 · 키 · 몫 · 배 — 여럿이면 칸 안 여러 줄) · 옵션 요약(칸=값 · 출처 꼬리표 · 「못 가름」) · 품 비교(직종 엑셀 → 우리 · 같음 ✓ / 다름 ≠) · 단추. +- 줄 단추 — [고르기](고르기 창) · [옵션](`Labor_Input.ts:53 openLaborInput` 다시 씀 → `guess` 로 품 비교 다시) · [원문 보기](엑셀 인용 조항 · 우리 로직 출처 두 갈래 → `/m02/logic-lib/source` · `Router_Combos.py:117`) · [엑셀에서 보기](엑셀 보기 창 · 그 단가산출 블록 행) · [뺌](까닭 「우리에 없음」 · 「안 씀」). +- 호표 단추 — [맞음 · 별칭 모두 체크] · **[확인하고 사전조합 만들기]**(체크 한 줄 넘을 때 · 누르면 「넣을 줄 n · 빠질 줄 m(까닭별)」 확인 글 → 서버 → 결과 「SC-… 만듦 / 덮음 · 빠진 줄 표 · 값 비교(참고)」 · 화면 잠금은 이때만). +- 캐시 — sessionStorage `m02-hopyo-check:{층}:{주인}:{키}:{번}` = `{체크: {차례: bool}, 대상: {차례: [..]}, 뺌: {차례: 까닭}}` · 조작 즉시 · 만들기 성공하면 지움 · 다시 열면 서버 제안 위에 덮어 보임 · 자동 저장 없음. +- 쓰기 단추 = 만들 층 쓰기 권한만(지금 `canWrite`) · 파일 층은 읽기만 있으면 됨 → 위 층 파일도 내 층으로 확인. + +### 4-2. 고르기 창 — 새 `M02_MasterTemplete_Hopyo_Choose.ts` (`View.ts choose` 를 옮김) + +- 위 = 엑셀 블록 품(직종 · 장비 · 자재) · 인용 조항 · [엑셀에서 보기]. +- 후보 다섯 = 이름 · 키 · 책 · 조항 · 판정 꼬리표(맞음 / 다른 점 / 후보) · 우리 품 · 「근거 맞음」 초록(지금 `basisOk`) · 금액 비 없음. +- 고름 = 한 로직 · 또는 [로직 더함]으로 둘 넘게(조합 · 몫 칸) · 고른 뒤 옵션은 서버 `guess` 가 채움. +- [다른 로직 찾기] = `openLogicPicker`(`M02_MasterTemplete_Labor_Pick.ts:83`)에 찾기 글 미리 넣음 덧(선택 칸 `query` · 엑셀 인용 조항 번호 → 없으면 품명 첫 뜻 낱말 · M01 찾기는 원문번호로도 걸림) · 다른 부르는 곳은 그대로. + +### 4-3. 엑셀 보기 창 · [로직 추가] · 좌측 판 + +- 엑셀 보기 창(`Hopyo_Sheet.ts importPicked :229-278`) — [고른 n개 불러오기] → **[고른 n개 확인하고 만들기]** = ① 고른 호표마다 서버 보기 → 펼침 표(호표 · 넣을 줄 = 체크 제안 · 빠질 줄 · 셈 중 · [확인 창 열기]) ② [이대로 n개 만들기] = 하위 호표부터 차례로 3-6 요청(몸 = 그 호표 체크 줄) · 펼쳐 보이고 한 번 누름 = 사용자 확인(목적다시 7-3). +- [로직 추가] 「실무 호표」(`Hopyo_Pick.ts pickHopyo :82-94`) — `확인` 키가 있으면 그 사전조합을 바로 참조(「확인한 사전조합 SC-…」) · 없으면 확인 창 모달 → 만들기 → `lib.pick` 참조 줄(별칭 = 호표 품명). +- 좌측 판(`Hopyo_Side.ts`) — 파일 목록 아래 **[별칭 장부]** 한 줄 → 본 칸 장부 화면. + +### 4-4. 별칭 장부 화면 — 새 `M02_MasterTemplete_Hopyo_Ledger.ts` + +- 표 — 엑셀 품명 · 규격 · 단위 | 우리 로직(여럿) · 옵션 · 배 · 몫 | 누가 · 때 · 처음 파일 | 층(내 층 · 회사 · 마스터 — 위 층 회색 읽기만). +- 할 일 — 찾기 · 갈래 거름(짝 · 우리에 없음 · 안 씀) · [고치기](고르기 창 · 옵션 창 · 배) · [지우기] · 위 층 줄 [내 층으로 고쳐 씀] · 고친 것은 sessionStorage `m02-hopyo-ledger:{층}:{주인}` → [저장] 때만 `PUT`. +- 안 둠 — 발주처 칸 · 자동 올림 · 자동 합침 · 쓰인 횟수 · 회사로 올리기 단추. + +### 4-5. 없앨 것 (지금 즉시 확정 · 확인 없이 들어감) + +| 자리 | 없앰 | +|---|---| +| `View.ts` 줄 [확정] `:393-402` | 즉시 서버 쓰기 → 체크(캐시) | +| `View.ts` [같음 모두 확정] `:279-282` · [근거 맞음 모두 확정] `:283-291 · 525-541` | → [맞음 · 별칭 모두 체크] | +| `View.ts` [사전조합 만들기] `:292-294` | → [확인하고 사전조합 만들기](체크 줄 몸) | +| `Hopyo_Sheet.ts importPicked` | 바로 만들기 → 펼침 뒤 한 번 | +| `Hopyo_Pick.ts pickHopyo` | 바로 만들기 → 확인된 것 참조 / 확인 창 | +| `Api.ts saveHopyoMemory` · 길 `hopyo-memory` | 없앰 | +| 서버 `Match.match_line` `들어감` · `Build.build` 의 `들어감` 줄 고름 | `체크` 제안 · 몸 줄만 | + +- 파일 크기 — `View.ts` 614 줄 → 확인 창 · 고르기 창을 새 파일로 빼 400 줄 안 · 새 파일 모두 700 줄 안 · 색 = 공용 토큰. + +--- + +## 5. 읽기 넓힘 (나 창) + +### 5-1. STmate — 새 `M02_Hopyo_Stmate.py` (약 350 줄 · C6 `s02_read.read_stmate` + C5 `c01_trees.read_stmate` · `_d_blocks` · `sections_stm` 한 벌) + +| 할 것 | 정함 | +|---|---| +| 가름 | 시트 이름에 「일위대가표」 · 「단가산출근거」 둘 다 → STmate · `Excel._sheets` 가 이름 목록(openpyxl `sheetnames`)을 먼저 보고 STmate 시트 + 목록표(노무비 · 중기 · 재료비 · 경비 · 단가산출근거목록표 — 있으면)를 읽음 | +| 호표 | A 열 「제N호표」 줄 → 그 줄 B~Z 열 「BDCOD\|B…」 = 호표 코드 · 다음 줄 A 이름 · B 규격 · D 단위 | +| 줄 | A 이름(빈 · 「합계」 뺌) · B 규격 · C 수량 · D 단위 · 단가 E 계 · G 노무비 · I 재료비 · K 경비(금액 = 다음 열) · M 열 비고 글 · 코드 = 1-4 꼴 첫 것 · T · C 꼴(계 · 소계) 뺌 | +| 갈래 | D 단가산출(참조 단산) · B 호표(참조 일위 · 번 = 호표 코드 표) · L 노임 · X 기계 · M 자재(비고 「별산」 · 규격 「별도계상」 · 값 없음 = 별도계상) · S · W 별도계상 · PR · 단위 % = % · 빈 코드 = 근거글(호표 `근거글`) · 그 밖 기타 | +| 블록 | 단가산출근거 G 열 「BDCOD\|D…」 머리 → 다음 머리 · 「총계」 앞까지 · 글 = B 열 · 자원 = M 열 「X00000_n」(없으면 A 열 「X00000L」 · G 열 「&X00000L&」) · 품 = H 열 · 이름 · 규격 = X 열 「이름 / 규격」(비면 목록표) · 머리 이름 · 규격 · 단위 = 단가산출근거목록표 · 행 = [머리 행, 끝 행] | +| 이음 | 노임 L → 노무비목록표 이름 → 우리 직종(`Basis.trade_names` · 어긋남 0 · C6) · X → 중기목록표 · M → 재료비목록표 · S → 경비목록표 | +| 출처 · 직종 | 블록 글 + 호표 근거글 → `Basis.sources` · 직종 = 자원 L 이름 | +| 검산 | 안 함 — `검산 {어긋남: [], 총괄표: 0}` · 꼴 「STmate」 | +| 결과 | 3-1 꼴(엑셀꼴과 같은 HP) — 맞춤 · 만들기 · 엑셀 보기 창이 꼴을 안 가림 | + +- 기대 = 6 파일 올라감(봉화 호표 45 · 영월 23 · 울진대흥 설계 34 · 착공(설계와 같음) · 영덕 9 · 울진소광 25) · 영월 근거 직종 5 → 33 / 36(C4). + +### 5-2. 거창 꼴 — `M02_Hopyo_Basis.read :84-125` 고침 + +- 블록 몸 줄 글 = 산출근거 열 글 · 그 열이 비고 같은 줄 다른 칸에 글이 있으면 산출근거 열 오른쪽 ~ 돈 머리 첫 열 앞(돈 머리 없으면 140 열) 칸을 빈칸으로 이어 붙임(C5 `_row_text` · C6 흩어진 칸) — 북부청은 산출근거 열에 글이 다 있어 그대로. +- 블록에 `글` 저장(3-1) · 기대 = 거창 단산 21 줄 출처 17 · 직종 21 · 몫 21(C5). + +### 5-3. 그 밖 읽기 손질 + +| 파일 | 할 것 | +|---|---| +| `M02_Hopyo_Excel.py` | `_REF` 넓힘(1-4) · 이름으로 이음 · `kind_of :132-149` 노임(단위 인 + 우리 직종) · `read_book` 블록 `글` · `꼴` · DRM(파일 머리 「DRMONE」 · PK · OLE 아님) → 400 「문서 보안(DRM)으로 잠긴 파일 — 보안을 푼 사본으로 저장해 올림」 | +| `M02_Hopyo_Basis.py` (B · 인용 조항 맞대기 손질) | `_clause :166-176` → 공개 `clause` · 윗마디 0.9(엑셀 「6-2」 ↔ 우리 「6-2-2」 · 「6-2-3」) · `sources :60-71` 조항 오타(「13-114」 → 「13-11-4」) | +| `M02_Hopyo_Grid.py` | `SHEETS :22` 에 「일위대가표」 · 「단가산출근거」 · 격자 호표 · 단산 행 = HP 행 | + +--- + +## 6. 창 나눔 · 차례 + +### 6-1. 파일 경계 + +| 파일 | 창 | 새 / 고침 | 몫 | +|---|---|---|---| +| `M02_Hopyo_Finger.py` | 가 | 새 | 엑셀 지문 · 식 풀기 · 몫 · 기계 줄 · 직접 줄 호표 | +| `M02_Hopyo_Ours.py` | 가 | 새 | 우리 지문 · 판 열쇠 · 캐시 · 데움 · 엔진 셈 캐시 | +| `M02_Hopyo_Option.py` | 가 | 새 | 옵션 공간 · 거꾸로 찾기 · 견줌 | +| `M02_Hopyo_Fit.py` | 가 | 새 | 순위 · 판정 · 블록 셈 · 뒤 셈 · 맞춤셈 캐시 | +| `M02_Hopyo_Combine.py` | 가 | 새 | 조합 풀이 · 섞인 줄 몫 먼저 | +| `M02_Hopyo_Match.py` | 가 | 고침 | `match_line` 새 상태 · 체크 제안 · `b_order`(접힘 없음 · 같음 근거 검사) · `view` · `guess` 꼴 · 장부 부르기 · `confirm` 없앰 | +| `M02_Hopyo_Stmate.py` | 나 | 새 | STmate 읽기 | +| `M02_Hopyo_Excel.py` · `M02_Hopyo_Grid.py` | 나 | 고침 | 5-3 | +| `M02_Hopyo_Basis.py` | 나 | 고침 | 블록 글 · 거창 이음 · `clause` · 오타 | +| `M02_Hopyo_Ledger.py` | 다 | 새 | 장부 읽기(옛 꼴 바꿔 읽음) · 찾기(열쇠 · 느슨) · 쓰기 · 층 · 재검사 | +| `M02_Hopyo_Build.py` | 다 | 고침 | 3-6 몸 · 로직 여럿 · 배 · 몫 · 통째 · 장부 씀 · `본뜬.확인` · `confirmed(folder)` | +| `M02_Labor_Router.py` | 다 | 고침 | 3-8 길 · 올리기 뒤 `Fit.ensure` · 시동 `Ours.warm()` · picks 에 `확인` | +| TS 새 `Hopyo_Check` · `Hopyo_Choose` · `Hopyo_Ledger` / 고침 `Hopyo_View` · `Hopyo_Sheet` · `Hopyo_Pick` · `Hopyo_Api` · `Hopyo_Side` · `Hopyo.css` · `Labor_Pick`(`query` 선택 칸만) | 다 | | 4 장 | + +- 같은 파일은 한 창만 — `Match.py` = 가(B 의 Match 몫 포함) · `Basis.py` · `Excel.py` = 나 · `Labor_Router.py` · `Build.py` = 다. +- 한 창이 남의 함수를 불러야 하면 약속(6-2)으로만 — 남의 파일 고침 없음. +- 공용 파일(`main.py` · `ui_template/`) 안 고침 · `M02_MasterTemplete_Store` · `M02_Labor_Ref` · `M02_Labor_Doc` · `M02_Hopyo_Store` · M01 · `resources/master_data` 안 고침(HP 덧칸은 `read_book` 결과에 실려 `Store.save` 가 그대로 씀). + +### 6-2. 서로 약속 (자료 꼴은 3 장) + +| 약속 | 낸 창 → 쓰는 창 | 꼴 | +|---|---|---| +| HP 블록 · 줄 꼴 | 나 → 가 · 다 | 3-1(`단산[].글` · `자원` · `출처` · `직종` · 줄 `참조` · `갈래` 노임) | +| `Basis.clause(theirs, our) -> (점수, 조항 글, 꼬리표)` · `Basis.sources` 오타 | 나 → 가 | 윗마디 0.9 · 같음 1 · 앞 마디 0.5 | +| `Fit.ensure(folder, key)` · `Fit.result(folder, key)` · `Ours.warm()` | 가 → 다(길) | 2-10 | +| `Match.view` · `Match.guess` 응답 | 가 → 다(화면) | 3-3 · 3-4 | +| `Ledger.recall(line, folder) -> 장부 줄 \| None`(+ 층 · 규격다름) · `Ledger.key(line)` | 다 → 가 | 3-5 | +| `Build.confirmed(folder) -> {본뜬 키: SC 키}` | 다 → 가(호표 참조 줄 체크 · `만든`) · 다(picks) | 3-7 | + +### 6-3. 차례 + +| 단계 | 가 | 나 | 다 | +|---|---|---|---| +| 1(나란히) | Finger → Ours → Option → Fit → Combine · 시험은 북부청 실물(나 1a 전엔 시험 도우미가 단가산출 시트 글을 직접 읽음) | 1a 블록 글 HP · `clause` · 오타(작음 · 먼저 push · 가 바탕) → 1b STmate · 거창 · Grid · DRM | 1a `Ledger.py` 뼈대(recall · key) 먼저 push → Build 새 몸 · 장부 · 길 · 화면(3-3 꼴 대역으로) | +| 2 | `match_line` 새 상태 · 뒤 셈 · view · guess 꼴 → push | STmate 실물 끝까지(올림 → 셈 → 상태) · 영월 정답 34 줄 | 화면을 실물 응답에 붙임 · [로직 추가] · 엑셀 보기 창 · 장부 화면 · 옛 길 · 단추 없앰 | +| 3(브레인) | 합치기 · 대상 시험 · 골든 · 브라우저 검증(7-4) · 아티팩트 | | | + +- 가 · 다가 가장 큼(새 파일 다섯 · 화면 셋) · 나는 1b 끝나면 가의 골든 시험 · 다의 화면 시험 거듦(브레인 배정). +- 손에 걸리는 곳 — 다의 화면은 가의 3-3 꼴이 굳어야 실물로 붙음 → 가는 1 단계 끝에 응답 꼴 대역 JSON(`resources/tester/fixtures/m02_hopyo_view_sample.json`)을 먼저 올림. + +--- + +## 7. 시험 · 완료 기준 + +### 7-1. 시험 파일 + +| 시험 | 창 | 무엇 | +|---|---|---| +| `test_m02_hopyo_finger.py`(새) | 가 | 식 풀기 일곱 꼴(「273,540 × 3인 ÷ 55㎥ × 2」 → 0.10909 · 「노무비 : 59,020 ÷ 23.1 × 90%」 → 시간 0.03896 · 율 0.9 · 「0.3인/㎥ × 10%」 → 품 0.03 · 밑품 0.3 · 바꿔 쓴 식 · 「× 2회」 · 품할증 · 못 풂 = 역산) · N001 합판 지문(형틀목공 0.1111 · 보통인부 0.0444 · 조건 4회 · 보통 · 조항 건설 6-3-1) · 고임돌 섞음 율 · 몫 = C5 `sections` 와 같음 · STmate 자원 지문 · 기계 줄 | +| `test_m02_hopyo_ours.py`(새) | 가 | 판 열쇠 같음 · 캐시 쓰기 · 읽기 · 준비 전 None · GC000211 「유형=보통」 형틀목공 0.112 · 섞기 전 변형 · master_data sha256 그대로 | +| `test_m02_hopyo_option.py`(새) | 가 | N000 → GC000198 소량감률 50 「하나만」 · N001 → 유형 보통 · 갈리는 칸 측압 · 폼타이규격 · N013 → R 맞음 · 구조물터파기(인력100%) → 없음 · 다른 점 · 펌프차 41m → 기계 칸 | +| `test_m02_hopyo_fit.py`(새) | 가 | 순위(같음 규칙 · 딱 맞음이 같음을 이김) · 판정 셋 · 조합(철근 = GC000206 + GC000207 · 고임돌 → GF000211 × 0.1) · 뒤 셈 캐시 · 「셈 중」 | +| `test_m02_hopyo_golden.py`(새 · 느림) | 가(+ 나 영월) | 정답 표 지표(7-3) · 우리 지문 캐시 있으면 빠름 · 없으면 처음 ≈ 2 분 | +| `test_m02_hopyo_match.py`(고침) | 가 | 새 상태 · 체크 제안 · 별칭 체크 규칙 셋 · % · 호표 · 노임 · 숫자 없음 · 옛 「들어감」 시험 바꿈 | +| `test_m02_hopyo_stmate.py`(새) | 나 | 6 파일 올라감 · 호표 수 · 코드 갈래 · 노임 이름 어긋남 0 · 블록 자원 품 · 이음 열쇠 O 열 · 장수 · 진안 400 글 | +| `test_m02_hopyo_excel.py` · `_basis.py` · `_grid.py`(고침) | 나 | 이음 넓힘 · 노임 갈래 · 블록 글 · 거창 출처 17 · 직종 21 · 윗마디 · 오타 · STmate 격자 | +| `test_m02_hopyo_ledger.py`(새) | 다 | 옛 꼴 바꿔 읽음 · 새 꼴 · 층 차례 · 느슨 찾기 · 우리에 없음 · 안 씀 · 위 층 읽기만 · PUT 재검사 · 권한 | +| `test_m02_hopyo_build.py`(고침) | 다 | 몸 줄만 들어감 · 몸 없음 400 · 빠진 줄 까닭 · 로직 여럿 같은 별칭 · 같은 로직 감쌈 · 재검사 400(없는 로직 · 모르는 입력 · 배 ≤ 0 · 금액 칸) · 하위 호표 409 · 장부 씀 · `본뜬.확인` · 덮음 | +| `test_m02_hopyo_screen.py` + `helper_m02_hopyo_check.cjs`(새) | 다 | 확인 창 도우미(체크 → 몸 · 캐시 키 · 최신 응답만) · 옛 단추 · `saveHopyoMemory` 없음 · `buildHopyo(` 는 확인 창에서만 · 700 줄 · `npm run typecheck` | + +- 돌리는 법 = `./venv/Scripts/python.exe -m pytest resources/tester/<고친 시험> -q` · 전체 시험은 사용자가 시킬 때만. + +### 7-2. 정답 표 옮김 — 옮김(브레인 · 1 단계 앞) + +- 자리 = `resources/tester/fixtures/m02_hopyo_golden.json` · 꼴 `[{id, 파일, 이름, 규격, 단위, 단산 | 코드, 무리: "사람" | "자동" | "씨앗" | "자동+씨앗", 정답: [로직], 허용: [로직], 짐작: bool}]`. +- 재료 = 스크래치패드 `338_실험자료/C1/labels.py` · `labels_y.py`(사람 136 = 정답 126 + 우리에 없음 10) + `338_실험자료/K5/targets.json`(새 파일 51) + 단산 번(`338_실험자료/C1/out_items.json`) · STmate 코드(`338_실험자료/C6/data_*.json`) · 옮김 스크립트는 스크래치패드에(저장소엔 JSON 만). +- 줄 찾기 = 시험이 실물 엑셀을 지금 읽기로 읽어 (파일 · 이름 · 규격 · 단위 · 단산 번 | 코드)로 맞춤 — 엑셀은 저장소 `resources/knowledge/original/실무문서/` (git 안 · 읽기만). +- 호표 「조합 맞음」 정답 = 시험이 줄 정답을 호표로 모음(C5 `c07_hopyo` 와 같은 규칙 · 아래에서 위로). + +### 7-3. 기준 수치 (다시 재고 ±1 줄 · 바꾸면 브레인 · 사용자 판단) + +| 지표 | 기준 | 실험 값 | +|---|---|---| +| 북부청 92 줄 1순위 엄격(씨앗 켬 · 조합 전) | ≥ 88% (81 줄) | 82 줄 — 임도 91.8 · 사방 86.0(C4 지문 + 묶음 ① β=1) | +| 사람 126 줄 1순위 엄격(조합 뒤 · 영월 읽기 뒤) | ≥ 92% (116 줄) | 92.9(336 재검토 ③) | +| 사람 126 줄 다섯 안 엄격 | ≥ 96% (122 줄) | 96.8 | +| 씨앗 끔 1순위 | ≥ 88% (111 줄) | 89.7 (113) | +| 「맞음」 표시(사람 126) | ≥ 55 줄 · 틀림 ≤ 2 | 58 · 1(K5) | +| 「다른 점」 틀림(사람 126) | ≤ 2 | 25 · 1 | +| 옵션까지(1위가 정답인 줄 중 엑셀 품 그대로) | ≥ 50 줄 | 56 / 103 | +| 새 파일 51 「맞음」 틀림 | ≤ 3 | 21 · 2 | +| 조합 맞음(덮을 수 있는 83 호표 · T1) | ≥ 72% (60) | 75.9 (63) | +| 확인 없이 들어가는 줄 | 0 — 몸 없는 만들기 400 · 몸 밖 줄은 사전조합에 없음 · `hopyo-memory` 404 · 화면에 바로 만들기 없음 | — | +| STmate 6 파일 · 거창 | 200 · 호표 수 5-1 · 거창 출처 17 · 직종 21 · 장수 · 진안 400 글 | C5 · C6 | +| 원본 안 고침 | `resources/master_data` *.json · 일위대가조합.json sha256 시험 앞뒤 같음(지금 `master_hash` 도우미) · M01 쓰기 없음 | — | +| 셈 무게 | 우리 지문 캐시 없음 ≤ 180 초(뒤) · 있음 ≤ 3 초 · 북부청 임도 맞춤 셈 시간 기록(목표 ≤ 5 분 · 화면 잠금 없음) | 110 초(C4) | + +- 「검증완료」 한 줄 = 위 시험 통과 + 수치 한 줄(1순위 · 다섯 안 · 맞음 · 틀림) · 검증보고서 파일 없음. + +### 7-4. 브레인 브라우저 검증 목록 (끝에 · ORCA · 검증 층 · 조작한 값 되돌림) + +1. 북부청 임도 올림 → 「숫자 판정 셈 중 n/m」 → 끝 · 화면 안 멈춤. +2. 레미콘 소량 줄 = 「맞음 — GC000198 · 소량감률 50」 체크됨 · 품 비교 콘크리트공 0.1091 = 0.1091 · 굴착기 0.29. +3. 합판 4회 = 「맞음 + 측압 · 폼타이규격 못 가름」 · 구조물터파기 = 「다른 점 — 특별인부 0.354 · 0.370」 체크 안 됨 · [옵션]으로 토질 고르면 품 비교가 서버 값으로 바뀜. +4. 고임돌 = GF000211 × 0.1(인력 10%) · 철근가공조립 = 두 로직 · 같은 별칭. +5. [확인하고 사전조합 만들기] → 사전조합 줄 = 체크한 줄만 · 빠진 줄 까닭 · 장부 줄 생김 · 금액은 우리 셈. +6. 사방 올림 → 같은 이름 줄 「별칭」 체크됨 · 규격만 다른 줄은 체크 안 됨. +7. [로직 추가] 실무 호표 → 확인된 것 = 바로 참조 / 아닌 것 = 확인 창 모달 → 참조 줄. +8. 엑셀 보기 창 [고른 3개 확인하고 만들기] → 펼침 → 한 번. +9. STmate 영월 올림 200 · 호표 23 · 줄 상태 · 직접 줄 호표 통째 제안. +10. 별칭 장부 — 보기 · 고치기 · 지우기 · 위 층 읽기만 · [저장] 때만 씀. +11. 옛 단추([확정] · [같음 모두 확정] · [근거 맞음 모두 확정]) 없음 · 확인 없이 들어감 0. +12. 되돌림 — 만든 사전조합 · 장부 줄 · 올린 파일 지움(검증 층만). + +--- + +## 8. 이번 밖 · 정함 + +### 8-1. 이번 밖 + +- M01 로직 별칭 칸 엮기(찾기용 · 사람 손) · 발주처 칸 · 별칭 자동 올림(개인 → 회사 → 마스터) · [회사로 올리기] 단추 · 자동 합침 · 쓰인 횟수. +- 우리에 없는 로직 만들기(사용자 확인 뒤 시스템 관리자가 M01 [복사해서 만들기]) · 우리 로직 고치기(원문 근거 + 사용자 확인 뒤 따로). +- 노임 줄 그릇(336 정할 것 7 — 「노임 · 그릇 없음」 표시만) · 기억 c 열쇠(로직 지문 열쇠) · 같은 열쇠 다른 답 쌓기 · 마스터 판 바뀜 알림 세부 · 사전조합에서 고친 별칭 · 옵션 → 장부 되돌림. +- 문서 보안(DRM) 풀기 · STmate STC 직접 읽기 · 외부 AI · 엑셀 하이퍼링크 · HYPERLINK 식 읽기. +- 「옛 판」 · 「다른 책」 까닭 자동 글(숫자 사실만 · 까닭은 [원문 보기]). + +### 8-2. 정함 — 브레인 · 사용자가 바꿀 수 있음 + +1. 이음 열쇠 = 엑셀꼴 비고 번호 글(넓힘) · STmate 줄 코드(O 열 꼴 찾기) · 하이퍼링크 안 읽음. +2. 「맞음」 = 숫자(R 허용 · 엑셀 몫 다 덮음) · 체크 제안 = 맞음 · 별칭(조건 셋) · 나머지 체크 안 함 · 이름 같음 · 씨앗은 상태가 아니라 순위 덤(「같음」 상태 없앰). +3. 같은 일 다른 책 = 숫자(품)가 가름 · 숫자가 같으면 인용 책(336 정할 것 1 풀림 그대로). +4. 섞인 줄 = 인력 몫 로직 × 몫이 기본 · 기계 몫 = 우리 일 로직이 맞으면 덧 · 아니면 「기계 시간당 기계경비」 후보(체크 안 함 · 「시간 = 엑셀 값」) · 기본 기대는 336 T1(인력 몫만). +5. 직접 줄 호표 = 호표 통째 로직 하나 제안(호표 단위 체크). +6. 별칭 장부 = `맞춤.json` 키움 · 확인 때만 씀 · 만드는 층 · 옛 기억은 바꿔 읽음. +7. 캐시 = 저장소 `tmp/m02_hopyo/`(우리 지문 · 맞춤셈 · 마스터 판 열쇠) · 셈 = 올릴 때 뒤 스레드 하나 · 시동 땐 디스크 읽기만. +8. 확인 다시 = 같은 본뜬 사전조합 덮음(`본뜬.확인`) · 옛 실무 사전조합은 [로직 추가]에서 확인 창으로. +9. 엑셀 보기 창 여러 호표 = 펼쳐 보인 뒤 한 번 누름 = 사용자 확인. +10. 옛 HP 파일 = 숫자 판정 없음 · 다시 올리면 됨(꼬리표 글) · 지금 storage 실무자료 셋 · 맞춤.json 2 줄뿐. +11. 정답 표 = 저장소 시험 자료(JSON)로 옮김 · 기준 수치는 7-3(±1 줄). +12. 무게 · 문턱 = 336 · K5 값 그대로(2-9) · 한 파일로 다시 맞추지 않음. From 2286031266de512a6921059d23cc521aa8862187 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 15 Oct 2026 23:14:23 +0900 Subject: [PATCH 53/81] =?UTF-8?q?feat(M02=20=EC=9D=BC=EC=9C=84=EB=8C=80?= =?UTF-8?q?=EA=B0=80):=20338-2a=20=EB=8B=A8=EA=B0=80=EC=82=B0=EC=B6=9C=20?= =?UTF-8?q?=EB=B8=94=EB=A1=9D=20=EA=B8=80=20=C2=B7=20=EC=B6=9C=EC=B2=98=20?= =?UTF-8?q?=C2=B7=20=EC=A7=81=EC=A2=85=20HP=20=EC=A0=80=EC=9E=A5=20=C2=B7?= =?UTF-8?q?=20=EA=BC=B4=20=EC=B9=B8=20=C2=B7=20Basis.clause=20=EC=9C=97?= =?UTF-8?q?=EB=A7=88=EB=94=94=200.9=20=C2=B7=20=EC=A1=B0=ED=95=AD=20?= =?UTF-8?q?=EC=98=A4=ED=83=80=2013-114?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 단산 블록마다 글(머리 뺀 몸 줄 · 200 줄 · 300 자 안) · 출처 · 직종 → HP 단산 · 꼴 「엑셀꼴」 - Basis.clause 공개 — 같음 1 · 윗마디 0.9(엑셀 6-2 ⊃ 우리 6-2-2) · 앞 마디 0.5 · 근거 맞음에 윗마디 포함 - Basis.sources 조항 오타 「13-114」 → 13-11-4 - 북부청 단산 268 · 사방 139 이음 그대로 · 시험 basis · excel · grid · match 51 통과 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_014utrjfoW831Vc5wJUd13kL --- M02_MasterTemplete/M02_Hopyo_Basis.py | 31 +++++++++++++++------ M02_MasterTemplete/M02_Hopyo_Excel.py | 7 +++-- resources/tester/test_m02_hopyo_basis.py | 35 ++++++++++++++++++++++++ 3 files changed, 62 insertions(+), 11 deletions(-) diff --git a/M02_MasterTemplete/M02_Hopyo_Basis.py b/M02_MasterTemplete/M02_Hopyo_Basis.py index 7d615ee34..7a7705054 100644 --- a/M02_MasterTemplete/M02_Hopyo_Basis.py +++ b/M02_MasterTemplete/M02_Hopyo_Basis.py @@ -5,8 +5,10 @@ 다음 머리 앞까지 한 블록 · 출처 = 「…품셈」(오타 「품줌」) 뒤 20 글자 안 절 번호 — 앞 글에 산림 · 임도 = 산림품셈 · 그 밖 = 건설품셈 · -직종 = 「이름 : 수」 줄의 빈칸 뺀 이름이 우리 인력(LB) 이름 · 옛이름일 때만(지금 이름으로). -견줌(`candidates`) = 같은 책 조항 같음 1.0 · 앞 마디 같음 0.5(조항이 안 맞으면 후보 아님) · +직종 = 「이름 : 수」 줄의 빈칸 뺀 이름이 우리 인력(LB) 이름 · 옛이름일 때만(지금 이름으로) · +글(338) = 머리 뺀 몸 줄 `GLINES` 줄 · 줄마다 `GCHARS` 자 안(맞춤 지문 재료). +견줌(`candidates`) = 같은 책 조항 같음 1.0 · 윗마디 0.9(엑셀 6-2 ⊃ 우리 6-2-2) · +앞 마디 같음 0.5(조항이 안 맞으면 후보 아님 · `clause`) · 조항 오타 「13-114」 = 「13-11-4」 · 건설 공통 · 토목 덤 0.1 · 직종 겹침(보통인부 뺌) 0.5 · 단위 0.15 · 직종도 뜻 낱말도 안 겹치면 버림 · 같은 점수는 글자 두 개 묶음(괄호 글 포함)이 가름 · 이름 · 낱말(괄호 글 포함 · 인력 · 기계 뺌)이 하나도 안 겹치는 근거 후보는(근거 맞음 = 조항 · 직종 · 단위 다 맞으면 빼고) 겹치는 근거 후보 뒤로 · @@ -31,6 +33,8 @@ BONUS_PARTS = ("공통", "토목") _HEAD = re.compile(r"^(\d+)\.\s*(.*?)\s*\[([^\[\]]*)\]\s*$") _BOOK = re.compile(r"품[셈줌]") _TRADE = re.compile(r"^[\s*·\-]*([가-힣][가-힣\s]*?)\s*:\s*[\d,]") +_TYPO_CLAUSE = re.compile(r"^(\d+)-(\d)(\d)(\d)$") # 「13-114」 = 13-11-4 +GLINES, GCHARS = 200, 300 _TYPOS = (("깍기", "깎기"),) _PLAIN_WORDS = frozenset({"인력", "기계", "장비"}) # 방법 낱말 — 「터널굴착 1발파당 인력」 _MONEY = {"합계": "계", "노무비": "노무비", "재료비": "재료비", "경비": "경비"} @@ -66,8 +70,10 @@ def sources(text: str) -> list[list[str]]: continue before = text[max(0, hit.start() - 12) : hit.start()] book = "산림품셈" if "산림" in before or "임도" in before else "건설품셈" - if [book, num.group()] not in out: - out.append([book, num.group()]) + typo = _TYPO_CLAUSE.match(num.group()) + number = "{}-{}{}-{}".format(*typo.groups()) if typo else num.group() + if [book, number] not in out: + out.append([book, number]) return out @@ -82,7 +88,7 @@ def _columns(rows: list[list[Any]]) -> tuple[int, int, dict[str, int]]: def read(rows: list[list[Any]] | None) -> dict[int, dict[str, Any]]: - """단가산출 시트 줄 → `{N: {번, 이름, 단위, 행: [시작, 끝], 출처, 직종, 금액}}` + """단가산출 시트 줄 → `{N: {번, 이름, 단위, 행: [시작, 끝], 출처, 직종, 글, 금액}}` (행 = 엑셀 1부터).""" if not rows: return {} @@ -105,6 +111,7 @@ def read(rows: list[list[Any]] | None) -> dict[int, dict[str, Any]]: "행": [at, at], "출처": [], "직종": [], + "글": [], "금액": { key: v for key, c in money.items() @@ -117,6 +124,8 @@ def read(rows: list[list[Any]] | None) -> dict[int, dict[str, Any]]: if block is None: continue block["행"][1] = at + if len(block["글"]) < GLINES: + block["글"].append(text.rstrip()[:GCHARS]) block["출처"] += [one for one in sources(text) if one not in block["출처"]] trade = _TRADE.match(text) name = names.get(_flat(trade.group(1))) if trade else None @@ -163,12 +172,16 @@ def _head(number: str) -> str: return number.rsplit("-", 1)[0] -def _clause(theirs: list[list[str]], our: dict[str, Any]) -> tuple[float, list[str], str]: - """(점수, 맞은 우리 조항 글, 꼬리표) — 같은 책만.""" +def clause(theirs: list[list[str]], our: dict[str, Any]) -> tuple[float, list[str], str]: + """엑셀 인용 조항 `[[책, 번호]]` ↔ 우리 로직(`ours()` 한 줄) → (점수, 맞은 우리 조항 글, + 꼬리표) — 같은 책만 · 같음 1 · 윗마디 0.9(엑셀 6-2 ⊃ 우리 6-2-2) · 앞 마디 0.5.""" nums = [n for book, n in theirs if book == our["책"]] same = sorted(n for n in nums if n in our["조항"]) if same: return 1.0, [f"{our['구분']} {n}" for n in same], "조항 같음" + kids = sorted({c for c in our["조항"] for n in nums if c.startswith(n + "-")}) + if kids: + return 0.9, [f"{our['구분']} {n}" for n in kids], "윗마디 같음" heads = {_head(n) for n in nums} near = sorted(n for n in our["조항"] if _head(n) in heads) if near: @@ -206,7 +219,7 @@ def candidates(line: dict[str, Any], got: dict[str, Any]) -> list[dict[str, Any] our = table.get(one["로직"]) if not our: continue - score, clauses, tag = _clause(theirs, our) + score, clauses, tag = clause(theirs, our) if not score: continue shared = sorted(trades & our["직종"]) @@ -227,7 +240,7 @@ def candidates(line: dict[str, Any], got: dict[str, Any]) -> list[dict[str, Any] why = {"조항": clauses, "직종": shared, "단위": unit, "점수": round(score, 3)} theirs_loose = _loose_words(one["이름"]) overlap = any(a in b or b in a for a in loose for b in theirs_loose) - full = tag == "조항 같음" and unit and (bool(shared) or not trades) # 근거 맞음 + full = tag != "앞 마디 같음" and unit and (bool(shared) or not trades) # 근거 맞음 why["까닭"] = " · ".join(tags) extra = {"_겹침": dice, "_낱말": overlap or full} scored.append((score, dice, {**hopyo_match._public(one), "근거": why, **extra})) diff --git a/M02_MasterTemplete/M02_Hopyo_Excel.py b/M02_MasterTemplete/M02_Hopyo_Excel.py index 9995de2b4..c55a1b89c 100644 --- a/M02_MasterTemplete/M02_Hopyo_Excel.py +++ b/M02_MasterTemplete/M02_Hopyo_Excel.py @@ -13,7 +13,8 @@ 어긋남은 글 목록(막지 않음). 행(329-1) = 호표 [제N호표 줄, 마지막 구성 줄] · 구성 줄 한 줄(엑셀 1부터) · 근거(330-1) = 단산N참조 줄에 「단가산출」 블록 `{출처, 직종, 행}`(`M02_Hopyo_Basis.read`) · -`단산` = 블록마다 `{번, 이름, 단위, 행}`. +`단산` = 블록마다 `{번, 이름, 단위, 행, 출처, 직종, 글}`(338 · 글 = 머리 뺀 몸 줄) · +`꼴` = 「엑셀꼴」. """ from __future__ import annotations @@ -270,8 +271,10 @@ def read_book(name: str, data: bytes) -> dict[str, Any]: {**_json(hopyo), "줄": [_json(line) for line in lines]} for hopyo, lines in zip(hopyos, raw, strict=True) ] - basis = [{k: b[k] for k in ("번", "이름", "단위", "행")} for b in blocks.values()] + keep = ("번", "이름", "단위", "행", "출처", "직종", "글") + basis = [{k: b[k] for k in keep} for b in blocks.values()] return { + "꼴": "엑셀꼴", "시트": SHEET, "호표": out, "단산": basis, diff --git a/resources/tester/test_m02_hopyo_basis.py b/resources/tester/test_m02_hopyo_basis.py index 94d21bf83..8bc0a880b 100644 --- a/resources/tester/test_m02_hopyo_basis.py +++ b/resources/tester/test_m02_hopyo_basis.py @@ -3,6 +3,7 @@ 블록 북부청 95 · 사방 67 · 단산93 근거 [건설 3-2-1] · [특별인부] · 후보 93 → GC000083 · 21 → GC000198 · 64 → GC000143 위 둘 · 49 → GC000085 · 잔토처리에 철골재 철거 없음 · 근거 없는 줄 그대로 · 상태 「후보」(자동 확정 없음) · +블록 글 HP(338-2a · 268 · 139 이음 그대로) · `clause` 윗마디 0.9 · 조항 오타 13-114 · master_data 그대로. """ @@ -117,3 +118,37 @@ def test_길_응답_근거_칸(client): assert dig["맞춤"]["후보"][0]["근거"]["까닭"].startswith("조항 같음") assert "근거" not in lines["임도표지석(받침,글새김포함)"] # 자재 줄 assert master_hash() == before + + +def test_블록_글_HP(north): + """338-2a — 단산마다 출처 · 직종 · 글(머리 뺀 몸 줄 · 200 줄 · 300 자 안) · 꼴.""" + assert north["꼴"] == "엑셀꼴" + b93 = north["단산"][92] + assert b93["번"] == 93 and b93["출처"] == [["건설품셈", "3-2-1"]] + assert b93["직종"] == ["특별인부"] + assert b93["글"][0] == " [건설공사표준품셈 3-2-1 인력굴착]" + assert " 특별인부 : 226,122 × (3.6+2.7+2.2)/3 ÷ 8 = 80,084.8" in b93["글"] + assert all(b["글"] and len(b["글"]) <= hb.GLINES for b in north["단산"]) + assert all(len(t) <= hb.GCHARS for b in north["단산"] for t in b["글"]) + lines = [x for h in north["호표"] for x in h["줄"]] + assert sum(1 for x in lines if x.get("근거")) == 268 + sabang = hx.read_book(SABANG.name, SABANG.read_bytes()) + assert sum(1 for h in sabang["호표"] for x in h["줄"] if x.get("근거")) == 139 + assert all(b["글"] for b in sabang["단산"]) + + +def test_조항_윗마디_오타(north): + our = {"책": "건설품셈", "구분": "건설품셈 토목", "조항": {"6-2-2", "6-2-3"}} + assert hb.clause([["건설품셈", "6-2-2"]], our) == (1.0, ["건설품셈 토목 6-2-2"], "조항 같음") + assert hb.clause([["건설품셈", "6-2"]], our) == ( + 0.9, + ["건설품셈 토목 6-2-2", "건설품셈 토목 6-2-3"], + "윗마디 같음", + ) + assert hb.clause([["건설품셈", "6-2-9"]], our)[0] == 0.5 + assert hb.clause([["산림품셈", "6-2"]], our) == (0.0, [], "") # 다른 책 + assert hb.clause([["건설품셈", "6-22"]], our)[0] == 0.0 # 6-2 윗마디 아님 + assert hb.sources("[산림사업표준품셈 13-114]") == [["산림품셈", "13-11-4"]] + assert hb.sources("[산림사업표준품셈 9-14]") == [["산림품셈", "9-14"]] + stone = next(b for b in north["단산"] if b["번"] == 63) + assert stone["출처"] == [["산림품셈", "13-11-4"]] From ea21006352601a9b97689e2ca54f824a5eb670a6 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 15 Oct 2026 23:15:47 +0900 Subject: [PATCH 54/81] =?UTF-8?q?feat(M02):=20338-3a=20=EB=B3=84=EC=B9=AD?= =?UTF-8?q?=20=EC=9E=A5=EB=B6=80=20=EB=BC=88=EB=8C=80=20=E2=80=94=20?= =?UTF-8?q?=EC=97=B4=EC=87=A0=20=C2=B7=20=EC=B8=B5=20=EC=B0=A8=EB=A1=80=20?= =?UTF-8?q?=EC=B0=BE=EA=B8=B0=20=C2=B7=20=EB=8A=90=EC=8A=A8=20=EC=B0=BE?= =?UTF-8?q?=EA=B8=B0=20=C2=B7=20=EC=98=9B=20=EA=BC=B4=20=EB=B0=94=EA=BF=94?= =?UTF-8?q?=20=EC=9D=BD=EC=9D=8C=20=C2=B7=20=EC=9E=AC=EA=B2=80=EC=82=AC=20?= =?UTF-8?q?=C2=B7=20=EC=93=B0=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 새 M02_Hopyo_Ledger.py — key · hopyo_key · recall(같은 층 → 위 층 · 품명|단위 느슨 = 규격다름) · books · rows · check · write · put - 옛 기억 줄(로직|자재|빼기 · 환산 · 확정) = 새 꼴(갈래 짝 · 짝 [{… 배}] · 빼기 = 안 씀)로 읽음 · 쓸 때 새 꼴 - 시험 test_m02_hopyo_ledger.py Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01XWh5wNNycMkbwMpACz1rte --- M02_MasterTemplete/M02_Hopyo_Ledger.py | 261 ++++++++++++++++++++++ resources/tester/test_m02_hopyo_ledger.py | 149 ++++++++++++ 2 files changed, 410 insertions(+) create mode 100644 M02_MasterTemplete/M02_Hopyo_Ledger.py create mode 100644 resources/tester/test_m02_hopyo_ledger.py diff --git a/M02_MasterTemplete/M02_Hopyo_Ledger.py b/M02_MasterTemplete/M02_Hopyo_Ledger.py new file mode 100644 index 000000000..0b97c4b1c --- /dev/null +++ b/M02_MasterTemplete/M02_Hopyo_Ledger.py @@ -0,0 +1,261 @@ +"""M02 일위대가 실무자료 별칭 장부(PLAN 338-3 · 설계 3-5) — 층 `실무자료/맞춤.json` 을 키움. + +줄 = {열쇠(품명|규격|단위 정규화): {원래 {품명, 규격, 단위}, + 갈래 「짝」 · 「우리에 없음」 · 「안 씀」, 짝 [{로직|자재, 입력값, 배, 몫}], + 누가, 때, 처음파일, 엑셀조항, 판}} · +직접 줄 호표 통째 = 열쇠 「호표|품명|규격|단위」. +옛 줄 {로직|자재|빼기, 입력값, 환산, 확정} = 읽을 때 새 꼴로 바꿔 읽음(빼기 = 「안 씀」 · +파일은 다음 쓰기 때 새 꼴). +읽는 차례 = 같은 층 → 위 층(개인 → 회사 → 마스터) · 아래 층이 이김 · 정확 열쇠가 먼저 · 없으면 +품명|단위 느슨 열쇠(`규격다름`). +쓰는 때 = [확인하고 사전조합 만들기] · 장부 화면 [저장]만 · 쓰기 = 그 층만(권한은 길 몫) · +짝은 서버가 다시 검사(돈 로직 · 그 로직 입력 이름 · M01 자재품목 · 배 0 넘음 · 몫 (0, 1]). +""" + +from __future__ import annotations + +from datetime import datetime +from decimal import Decimal +from pathlib import Path +from typing import Any + +from M01_MasterData import M01_MasterData_Store as m01 +from M02_MasterTemplete import M02_Hopyo_Store as hopyo_store +from M02_MasterTemplete import M02_Labor_Inputs as labor_inputs +from M02_MasterTemplete import M02_Labor_Ref as refs +from M02_MasterTemplete import M02_MasterTemplete_Store as store + +mm = m01.cm.mf.mp.mm +KINDS = ("짝", "우리에 없음", "안 씀") +HOPYO = "호표|" + + +# ── 열쇠 ───────────────────────────────────────────────────────────── +def key(line: dict[str, Any]) -> str: + """줄 열쇠 = 품명 | 규격 | 단위(정규화 · `M02_Hopyo_Match.line_key` 와 같음).""" + return "|".join( + ( + mm.plain(line.get("이름")), + mm.plain_spec(line.get("규격")), + mm.plain_unit(line.get("단위")), + ) + ) + + +def hopyo_key(hopyo: dict[str, Any]) -> str: + """직접 줄 호표 통째 열쇠 — 「호표|품명|규격|단위」.""" + return HOPYO + key(hopyo) + + +def _loose(full: str) -> str: + """느슨 열쇠 = 품명 | 단위(규격 뺌).""" + head, _, rest = full.rpartition("|") + return f"{head.rsplit('|', 1)[0]}|{rest}" + + +# ── 옛 꼴 → 새 꼴 ───────────────────────────────────────────────────── +def _plain_of(full: str) -> dict[str, str]: + name, spec, unit = full.removeprefix(HOPYO).split("|") + return {"품명": name, "규격": spec, "단위": unit} + + +def upgrade(full: str, raw: Any) -> dict[str, Any] | None: + """장부 한 줄을 새 꼴로 — 옛 기억 줄 · 새 꼴 · 모르는 꼴(None).""" + if not isinstance(raw, dict): + return None + if raw.get("갈래") in KINDS: + return {"원래": _plain_of(full), "짝": [], **raw} + head = {"원래": _plain_of(full), "때": raw.get("확정"), "누가": None, "처음파일": None} + if raw.get("빼기"): + return {**head, "갈래": "안 씀", "짝": []} + pair = {"로직": raw["로직"]} if raw.get("로직") else {"자재": raw.get("자재")} + if not any(pair.values()): + return None + pair.update(입력값=raw.get("입력값") or {}, 배=raw.get("환산"), 몫=None) + return {**head, "갈래": "짝", "짝": [pair]} + + +def read(folder: str | Path) -> dict[str, dict[str, Any]]: + """한 층 장부 `{열쇠: 새 꼴 줄}`(옛 꼴은 바꿔 읽음 · 모르는 줄 뺌).""" + out = {} + for full, raw in hopyo_store.memory(folder).items(): + if str(full).removeprefix(HOPYO).count("|") == 2 and (got := upgrade(str(full), raw)): + out[str(full)] = got + return out + + +# ── 층 ─────────────────────────────────────────────────────────────── +def chain(folder: str | Path) -> list[Path]: + """장부를 볼 층 폴더 — 같은 층 → 위 층(개인 → 회사 → 마스터).""" + layer, owner = refs.where(folder) + out = [Path(folder)] + if layer == "personal": + out.append(refs.folder_of("company", str(owner).split("/")[0])) + if layer in ("personal", "company"): + out.append(Path(store.FOLDER)) + return out + + +def books(folder: str | Path) -> list[tuple[str, str | None, dict[str, dict[str, Any]]]]: + """층마다 `(층, 주인, 장부)` — 같은 층부터(줄 여럿을 찾을 때 한 번 읽어 넘김).""" + return [(*refs.where(one), read(one)) for one in chain(folder)] + + +def recall( + line: dict[str, Any], folder: str | Path, got: list | None = None, whole: bool = False +) -> dict[str, Any] | None: + """줄(`whole` = 호표 통째)의 장부 짝 — `{…줄, 층, 주인, 규격다름}` · 없으면 None. + 정확 열쇠(아래 층 먼저) → 품명|단위 느슨(규격다름 True · 체크 안 함).""" + full = hopyo_key(line) if whole else key(line) + got = books(folder) if got is None else got + for layer, owner, book in got: + if full in book: + return {**book[full], "층": layer, "주인": owner, "규격다름": False} + loose = _loose(full) + for layer, owner, book in got: + for one, row in book.items(): + if one.startswith(HOPYO) == whole and _loose(one) == loose: + return {**row, "층": layer, "주인": owner, "규격다름": True} + return None + + +def rows(folder: str | Path, writable: bool) -> list[dict[str, Any]]: + """장부 화면 — 이 층 + 위 층 줄 `{열쇠, …줄, 층, 주인, 고침}`(고침 = 이 층 · 쓰기 권한).""" + out = [] + for at, (layer, owner, book) in enumerate(books(folder)): + for full, row in sorted(book.items()): + out.append( + {"열쇠": full, **row, "층": layer, "주인": owner, "고침": writable and not at} + ) + return out + + +# ── 검사 · 쓰기 ─────────────────────────────────────────────────────── +def _bad(text: str) -> store.StoreError: + return store.StoreError(400, text) + + +def _number(raw: Any) -> Decimal | None: + if isinstance(raw, bool): + return None + try: + got = Decimal(str(raw).replace(",", "").strip()) + except ArithmeticError: + return None + return got if got.is_finite() else None + + +def _json(value: Decimal) -> int | float: + value = value.normalize() + return int(value) if value == value.to_integral_value() else float(value) + + +def _known() -> tuple[set[str], set[str]]: + """(돈 로직 키, M01 자재품목 키) — 맞춤 목록 한 벌(`M02_Hopyo_Match.targets`).""" + from M02_MasterTemplete import M02_Hopyo_Match as match # 맞춤이 장부를 부름 — 늦게 읽음 + + got = match.targets() + return {x["로직"] for x in got["로직"]}, {x["자재"] for x in got["자재"]} + + +def input_names(code: str) -> set[str]: + got = labor_inputs.logic_inputs([code])["로직"] + return {str(x.get("이름")) for x in (got[0]["입력"] if got else [])} + + +def check_pair(raw: Any, where: str, known: tuple[set[str], set[str]]) -> dict[str, Any]: + """짝 하나 재검사 → `{로직|자재, 입력값, 배, 몫}` — 틀리면 400.""" + if not isinstance(raw, dict): + raise _bad(f"짝 꼴이 틀림 「{where}」") + extra = set(raw) - {"로직", "자재", "입력값", "배", "몫", "이름", "단위"} + if extra: + raise _bad(f"모르는 칸 {sorted(extra)} 「{where}」") + code, mat = str(raw.get("로직") or ""), str(raw.get("자재") or "") + if bool(code) == bool(mat): + raise _bad(f"돈 로직 · 우리 자재 하나만 「{where}」") + if (code and code not in known[0]) or (mat and mat not in known[1]): + raise _bad(f"없는 {'로직' if code else '자재'} 「{code or mat}」 「{where}」") + values = raw.get("입력값") or {} + if not isinstance(values, dict) or (mat and values): + raise _bad(f"입력값이 틀림 「{where}」") + unknown = set(values) - input_names(code) if code else set() + if unknown: + raise _bad(f"모르는 입력 {sorted(unknown)} 「{where}」") + times = _number(raw.get("배", 1)) + if times is None or times <= 0: + raise _bad(f"배는 0 넘는 수 「{where}」") + share = raw.get("몫") + part = None if share is None else _number(share) + if share is not None and (part is None or not 0 < part <= 1): + raise _bad(f"몫은 0 넘고 1 이하 「{where}」") + head = {"로직": code} if code else {"자재": mat} + return { + **head, + "입력값": values, + "배": _json(times), + "몫": None if part is None else _json(part), + } + + +def check(full: str, raw: Any, known: tuple[set[str], set[str]] | None = None) -> dict[str, Any]: + """장부 한 줄 재검사(장부 화면 [저장]) → 쓸 꼴(누가 · 때 빼고).""" + if str(full).removeprefix(HOPYO).count("|") != 2 or not isinstance(raw, dict): + raise _bad(f"장부 꼴이 틀림 「{full}」") + kind = raw.get("갈래") + if kind not in KINDS: + raise _bad(f"갈래는 {' · '.join(KINDS)} 「{full}」") + pairs = raw.get("짝") or [] + if kind != "짝" and pairs: + raise _bad(f"「{kind}」 줄은 짝 없음 「{full}」") + if kind == "짝" and (not isinstance(pairs, list) or not pairs or len(pairs) > 20): + raise _bad(f"짝은 하나 넘고 스물 이하 「{full}」") + known = known or _known() + origin = raw.get("원래") if isinstance(raw.get("원래"), dict) else _plain_of(full) + return { + "원래": {k: str(origin.get(k) or "") for k in ("품명", "규격", "단위")}, + "갈래": kind, + "짝": [check_pair(p, full, known) for p in pairs], + "엑셀조항": raw.get("엑셀조항") if isinstance(raw.get("엑셀조항"), list) else [], + } + + +def _version() -> str | None: + """마스터 판 열쇠(가 창 `M02_Hopyo_Ours.version` — 아직 없으면 None).""" + try: + from M02_MasterTemplete import M02_Hopyo_Ours as ours + except ImportError: + return None + return ours.version() if hasattr(ours, "version") else None + + +def write( + folder: str | Path, + entries: dict[str, dict[str, Any] | None], + who: Any, + file: str | None = None, +) -> int: + """검사 끝난 줄을 층 장부에 씀(None = 지움) — 같은 열쇠는 덮음 · 처음파일은 있던 값 둠 · + 옛 꼴 줄은 이때 새 꼴로 → 쓴 줄 수.""" + if not entries: + return 0 + have = read(folder) + stamp = datetime.now().isoformat(timespec="seconds") + version = _version() + out: dict[str, dict[str, Any] | None] = dict(have) # 옛 꼴도 새 꼴로 다시 씀 + for full, row in entries.items(): + if row is None: + out[full] = None + continue + first = (have.get(full) or {}).get("처음파일") or file + out[full] = {**row, "누가": who, "때": stamp, "처음파일": first, "판": version} + hopyo_store.remember(folder, out) + return len(entries) + + +def put(folder: str | Path, entries: Any, who: Any) -> dict[str, Any]: + """장부 화면 [저장] — `{열쇠: 줄 | None}` 재검사 뒤 이 층에 씀 → `{씀, 줄}`.""" + if not isinstance(entries, dict) or not entries or len(entries) > 2000: + raise _bad("저장할 줄이 없음") + known = _known() + clean = {str(k): None if v is None else check(str(k), v, known) for k, v in entries.items()} + return {"씀": write(folder, clean, who), "줄": rows(folder, True)} diff --git a/resources/tester/test_m02_hopyo_ledger.py b/resources/tester/test_m02_hopyo_ledger.py new file mode 100644 index 000000000..d60b5b239 --- /dev/null +++ b/resources/tester/test_m02_hopyo_ledger.py @@ -0,0 +1,149 @@ +"""일위대가 실무자료 별칭 장부(PLAN 338-3 · `M02_Hopyo_Ledger`) — 옛 꼴 바꿔 읽음 · 새 꼴 · +층 차례(아래 층이 이김) · 느슨 찾기(규격다름) · 우리에 없음 · 안 씀 · 호표 통째 열쇠 · +위 층 읽기만 · 재검사 · 쓰기(처음파일 둠 · 옛 꼴 새로 씀) · master_data 원본 그대로. 층은 임시 폴더. +""" + +import json + +import pytest + +from M02_MasterTemplete import M02_Hopyo_Ledger as ledger +from M02_MasterTemplete import M02_Hopyo_Match as hm +from M02_MasterTemplete import M02_Hopyo_Store as hs +from M02_MasterTemplete import M02_MasterTemplete_Store as store +from M02_MasterTemplete import M02_Template_Layers as layers + +# 올리기 길 시험의 `client` 마당을 같이 씀 +# ruff: noqa: F811 +from resources.tester.test_m02_hopyo_excel import client, master_hash # noqa: F401 + +DIG = {"이름": "구조물터파기(인력100%)", "규격": "토사", "단위": "㎥"} +DIG_KEY = "구조물터파기(인력100%)|토사|m3" +WHO = {"id": 42, "이름": "갑"} + + +def _put_raw(folder, rows): + path = folder / hs.DIRNAME / f"{hs.MEMORY}.json" + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps({"줄": rows}, ensure_ascii=False), encoding="utf-8") + + +def _raw(folder): + return json.loads((folder / hs.DIRNAME / f"{hs.MEMORY}.json").read_bytes())["줄"] + + +def test_열쇠는_맞춤과_같음(): + assert ledger.key(DIG) == hm.line_key(DIG) == DIG_KEY + assert ledger.hopyo_key(DIG) == "호표|" + DIG_KEY + assert ledger._loose(DIG_KEY) == "구조물터파기(인력100%)|m3" + assert ledger._loose("호표|" + DIG_KEY) == "호표|구조물터파기(인력100%)|m3" + + +def test_옛_꼴_바꿔_읽음(client): + mine = layers.personal_dir(7, 42) + _put_raw( + mine, + { + DIG_KEY: {"로직": "GF000101", "입력값": {"지역": "서울"}, "환산": 2, "확정": "t1"}, + "합판거푸집|4회|m2": {"빼기": True, "확정": "2026-10-02T10:00:00"}, + "표지석||개": {"자재": "MT004258", "입력값": {}, "환산": 1, "확정": "t"}, + "깨진|줄": {"로직": "X"}, + "빈|짝|m": {"입력값": {}}, + }, + ) # fmt: skip + got = ledger.read(mine) + assert set(got) == {DIG_KEY, "합판거푸집|4회|m2", "표지석||개"} # 모르는 꼴 뺌 + assert got[DIG_KEY] == { + "원래": {"품명": "구조물터파기(인력100%)", "규격": "토사", "단위": "m3"}, + "때": "t1", + "누가": None, + "처음파일": None, + "갈래": "짝", + "짝": [{"로직": "GF000101", "입력값": {"지역": "서울"}, "배": 2, "몫": None}], + } + assert got["합판거푸집|4회|m2"]["갈래"] == "안 씀" and got["합판거푸집|4회|m2"]["짝"] == [] + assert got["표지석||개"]["짝"] == [{"자재": "MT004258", "입력값": {}, "배": 1, "몫": None}] + + +def test_층_차례_아래_층이_이김_느슨_찾기(client): + mine, company, system = ledger.chain(layers.personal_dir(7, 42)) + assert (company, system) == (layers.company_dir(7), store.FOLDER) + new = {"갈래": "짝", "짝": [{"로직": "GF000103", "입력값": {}, "배": 1, "몫": None}]} + _put_raw(system, {DIG_KEY: new, "잔토처리(인력100%)||m3": {"갈래": "우리에 없음"}}) + _put_raw(company, {DIG_KEY: {**new, "짝": [{"로직": "GF000102", "배": 1}]}}) + got = ledger.recall(DIG, mine) + assert (got["층"], got["주인"], got["짝"][0]["로직"], got["규격다름"]) == ( + "company", + "7", + "GF000102", + False, + ) + _put_raw(mine, {DIG_KEY: {"로직": "GF000101", "환산": 1}}) + assert ledger.recall(DIG, mine)["짝"][0]["로직"] == "GF000101" # 같은 층 먼저 + rock = {**DIG, "규격": "경암"} + loose = ledger.recall(rock, mine) + assert loose["규격다름"] is True and loose["층"] == "personal" # 품명|단위 느슨 + soil = {"이름": "잔토처리(인력100%)", "규격": "", "단위": "M3"} + assert ledger.recall(soil, mine)["갈래"] == "우리에 없음" + assert ledger.recall({"이름": "없는 줄", "단위": "m"}, mine) is None + assert ledger.recall(DIG, mine, whole=True) is None # 호표 통째 열쇠는 따로 + books = ledger.books(mine) + assert ledger.recall(DIG, mine, books)["짝"][0]["로직"] == "GF000101" # 한 번 읽어 넘김 + rows = ledger.rows(mine, True) + assert [(r["층"], r["고침"]) for r in rows] == [ + ("personal", True), + ("company", False), + ("system", False), + ("system", False), + ] # 위 층 = 읽기만 + assert not any(r["고침"] for r in ledger.rows(mine, False)) + + +def test_재검사(client): + known = ledger._known() + ok = { + "갈래": "짝", + "짝": [{"로직": "GF000101", "입력값": {"지역": "서울"}, "배": "2", "몫": 0.5}], + } + got = ledger.check(DIG_KEY, ok, known) + assert got["짝"] == [{"로직": "GF000101", "입력값": {"지역": "서울"}, "배": 2, "몫": 0.5}] + assert got["원래"] == {"품명": "구조물터파기(인력100%)", "규격": "토사", "단위": "m3"} + assert ledger.check(DIG_KEY, {"갈래": "안 씀"}, known)["짝"] == [] + pair = ok["짝"][0] + bad = [ + ("a|b", ok), + (DIG_KEY, {"갈래": "같음", "짝": ok["짝"]}), + (DIG_KEY, {"갈래": "짝", "짝": []}), + (DIG_KEY, {"갈래": "안 씀", "짝": ok["짝"]}), + (DIG_KEY, {"갈래": "짝", "짝": [{**pair, "로직": "GC000422"}]}), # 돈 아닌 로직 + (DIG_KEY, {"갈래": "짝", "짝": [{**pair, "입력값": {"없는칸": 1}}]}), + (DIG_KEY, {"갈래": "짝", "짝": [{**pair, "배": 0}]}), + (DIG_KEY, {"갈래": "짝", "짝": [{**pair, "배": "nan"}]}), + (DIG_KEY, {"갈래": "짝", "짝": [{**pair, "몫": 1.5}]}), + (DIG_KEY, {"갈래": "짝", "짝": [{**pair, "자재": "MT004258"}]}), + (DIG_KEY, {"갈래": "짝", "짝": [{**pair, "금액": 100}]}), # 금액 칸 받지 않음 + (DIG_KEY, {"갈래": "짝", "짝": [{"자재": "MT000000", "배": 1}]}), + ] + for full, raw in bad: + with pytest.raises(store.StoreError) as error: + ledger.check(full, raw, known) + assert error.value.status == 400, raw + + +def test_쓰기_처음파일_옛_꼴_새로(client): + before = master_hash() + mine = layers.personal_dir(7, 42) + _put_raw(mine, {"합판거푸집|4회|m2": {"빼기": True, "확정": "t"}}) + row = ledger.check(DIG_KEY, {"갈래": "짝", "짝": [{"로직": "GF000101", "배": 1}]}) + assert ledger.write(mine, {DIG_KEY: row}, WHO, "가.xls") == 1 + raw = _raw(mine) + assert raw["합판거푸집|4회|m2"]["갈래"] == "안 씀" # 옛 꼴 = 쓸 때 새 꼴 + assert raw[DIG_KEY]["누가"] == WHO and raw[DIG_KEY]["처음파일"] == "가.xls" + assert raw[DIG_KEY]["때"] + ledger.write(mine, {DIG_KEY: row}, WHO, "나.xls") + assert _raw(mine)[DIG_KEY]["처음파일"] == "가.xls" # 처음파일은 있던 값 + got = ledger.put(mine, {DIG_KEY: None}, WHO) + assert got["씀"] == 1 and DIG_KEY not in _raw(mine) + with pytest.raises(store.StoreError): + ledger.put(mine, {}, WHO) + assert master_hash() == before From bf5d130e32d6cff34d4c53a853cee42fd30f6c7d Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 15 Oct 2026 23:21:09 +0900 Subject: [PATCH 55/81] =?UTF-8?q?feat(M02):=20338-3b=20=ED=99=95=EC=9D=B8?= =?UTF-8?q?=ED=95=9C=20=EC=A4=84=EB=A7=8C=20=EC=82=AC=EC=A0=84=EC=A1=B0?= =?UTF-8?q?=ED=95=A9=20=E2=80=94=20Build=20=EC=83=88=20=EB=AA=B8=20=C2=B7?= =?UTF-8?q?=20=EC=9E=AC=EA=B2=80=EC=82=AC=20=C2=B7=20=EB=B3=84=EC=B9=AD=20?= =?UTF-8?q?=EC=9E=A5=EB=B6=80=20=EC=94=80=20=C2=B7=20=EC=9E=A5=EB=B6=80=20?= =?UTF-8?q?=EA=B8=B8=20=C2=B7=20=EC=98=9B=20=EC=A6=89=EC=8B=9C=20=ED=99=95?= =?UTF-8?q?=EC=A0=95=20=EA=B8=B8=20=EC=97=86=EC=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - M02_Hopyo_Build — 몸 줄만(없으면 400) · 대상 여럿 같은 별칭 · 배 · 몫 · 통째 · 하위 호표 확인 전 409 · 본뜬 원본 · 확인 · 같은 본뜬 덮음 · confirmed(folder, source) - M02_Labor_Router — hopyo-build 엄격 몸(extra forbid) · hopyo-ledger GET · PUT · hopyo-memory 없앰 · picks 확인 · 올린 뒤 Fit.ensure · 시동 Ours.warm(있을 때만) · ?to= 만들 층 - M02_Hopyo_Ledger — times_of 나눔 - 시험 test_m02_hopyo_build.py 새로 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01XWh5wNNycMkbwMpACz1rte --- M02_MasterTemplete/M02_Hopyo_Build.py | 328 +++++++++++++++-------- M02_MasterTemplete/M02_Hopyo_Ledger.py | 22 +- M02_MasterTemplete/M02_Labor_Router.py | 144 +++++++--- resources/tester/test_m02_hopyo_build.py | 237 ++++++++++------ 4 files changed, 508 insertions(+), 223 deletions(-) diff --git a/M02_MasterTemplete/M02_Hopyo_Build.py b/M02_MasterTemplete/M02_Hopyo_Build.py index c6997996a..41309a762 100644 --- a/M02_MasterTemplete/M02_Hopyo_Build.py +++ b/M02_MasterTemplete/M02_Hopyo_Build.py @@ -1,39 +1,44 @@ -"""M02 일위대가 실무자료 → 우리 일위대가 사전조합(PLAN 324-3). +"""M02 일위대가 실무자료 → 우리 일위대가 사전조합(PLAN 324-3 · 338-3 · 설계 3-6 · 3-7). +[확인하고 사전조합 만들기] 몸 = 사용자가 체크한 줄만 — +`줄 [{차례, 대상 [{로직|자재|호표, 입력값, 배, 몫}] | 뺌}]` · 직접 줄 호표 = `통째 {대상}`. +몸이 비면 400(확인 없이 들어가는 길 없음). +서버 재검사(브라우저 값을 받아 적지 않음 · 금액 · 품 칸 없음) — 차례 = 그 호표 줄 · 대상 하나 = +돈 로직 · M01 자재품목 · 확인된 호표 중 하나 · 입력 이름 = 그 로직 입력 · 배 0 넘음 · 몫 (0, 1] · +호표 = 만들 층에 확인된 사전조합(없으면 409 「제N호표 먼저 확인」). 호표 하나 = 사전조합 하나 — 이름 = 품명 + 규격 · 단위 = 엑셀 단위 · 구분 「실무자료」 · -상세구분 = 파일 · 본뜬 = {층 「실무자료」, 키 「HP-###### 제N호표」, 파일} · -만드는 층 = 부르는 층(일위대가 문서 층). 들어가는 줄(`M02_Hopyo_Match` 의 `들어감`)만: - 로직 = {로직, 별칭 = 엑셀 품명 40 자, 메모 = 「실무 0.14㎥ × 1 · 규격」, 입력값, - 수량 {손 = 엑셀 수량 × 환산}} · 같은 로직 둘째부터 = 그 줄 하나짜리 사전조합으로 감싸 참조 - (같은 로직 두 번 금지) · 자재 = {코드, 수량 {손}, 별도계상 = 엑셀 값 없음 「별도계상」 · - 아니면 「직접계산」}(같은 자재 = 수량 더함) · 일위N = 그 호표 사전조합을 먼저 만들어 참조 - (순환 = 빠진 줄). -같은 층 · 같은 파일 · 같은 호표 = 있던 것을 다시 씀 · `새로` = 그 키에 다시 만들어 덮음(감싼 것도). -빠진 줄(% · 못 맞춤 · 빼기 · 환산 입력 · 추정) = 엑셀 금액과 함께 보고 · 만든 값 ↔ 엑셀 호표 단가. +상세구분 = 파일 · 본뜬 = {층 「실무자료」, 키 「HP-###### 제N호표」, 파일, 원본 [층, 주인], +확인 {누가, 때}} · 만드는 층 = 부르는 층(일위대가 문서 층). + 로직 = {로직, 별칭 = 엑셀 품명 40 자, 메모 「실무 0.14㎥ × 1 · 규격」, 입력값, + 수량 {손 = 엑셀 수량 × 배}} · 한 줄 로직 여럿 = 같은 별칭 · 같은 로직 둘째부터 = 한 줄 + 사전조합으로 감싸 참조(같은 로직 두 번 금지) · + 자재 = {코드, 수량 {손}, 별도계상}(같은 자재 = 수량 더함) · 호표 = 확인된 사전조합 참조. +같은 본뜬 사전조합이 있으면 확인한 내용으로 덮음(`다시씀`) · 확인한 줄은 별칭 장부에 남음 +(`M02_Hopyo_Ledger` · 호표 참조 줄 뺌). +빠진 줄(확인 안 함 · 뺌 · %) = 엑셀 금액과 함께 보고 · 만든 값 ↔ 엑셀 호표 단가(참고). 엔진 · 일위대가 계산은 그대로(`M02_Labor_Ref.Expand`). """ from __future__ import annotations import json +from datetime import datetime from decimal import Decimal from pathlib import Path from typing import Any from M01_MasterData import M01_MasterData_Store as m01 -from M02_MasterTemplete import M02_Hopyo_Match as match +from M02_MasterTemplete import M02_Hopyo_Ledger as ledger from M02_MasterTemplete import M02_Hopyo_Store as hopyo_store from M02_MasterTemplete import M02_Labor_Doc as labor from M02_MasterTemplete import M02_Labor_Ref as refs from M02_MasterTemplete import M02_MasterTemplete_Store as store ORIGIN = "실무자료" -WHY = { - "%": "% 줄 — 우리 로직 덧줄이 셈", - "빼기": "뺌(확정)", - "후보": "못 맞춤 — 후보 확정 필요", - "없음": "못 맞춤", -} +LINES_MAX = 500 +UNCHECKED = "확인 안 함" +PERCENT = "% 줄 — 우리 로직 덧줄이 셈" +WHOLE = "호표 통째로 대신" def mark(key: str, no: int, at: int | None = None) -> str: @@ -42,25 +47,46 @@ def mark(key: str, no: int, at: int | None = None) -> str: return head if at is None else f"{head} {at + 1}째 줄" -def _found(origin: str) -> tuple[str, str] | None: - """지금 층 사전조합 가운데 본뜬 = 이 실무 표시 → (키, 판).""" - for path in sorted((store.root() / "precombo").glob("SC-*.json")): +def _origins(folder: Path): + """층 사전조합 가운데 실무자료 본뜬 → (키, 본뜬, 판).""" + for path in sorted((Path(folder) / "precombo").glob("SC-*.json")): try: - doc = json.loads(path.read_bytes()) + raw = path.read_bytes() + doc = json.loads(raw) except (OSError, ValueError): continue got = doc.get("본뜬") if isinstance(doc, dict) else None - if isinstance(got, dict) and got.get("층") == ORIGIN and got.get("키") == origin: - return path.stem, store.version_of(path.read_bytes()) - return None + if isinstance(got, dict) and got.get("층") == ORIGIN: + yield path.stem, got, raw -def _save(doc: dict[str, Any], fresh: bool) -> tuple[dict[str, Any], bool]: - """(참조, 다시씀) — 있으면 그대로(`fresh` 면 그 키에 덮음) · 없으면 새 키.""" +def _same_source(got: dict[str, Any], source: refs.Here | None) -> bool: + """본뜬 원본 층이 같음 — 원본 없는 옛 것 · `source` None 은 가리지 않음.""" + have = got.get("원본") + return source is None or not have or tuple(have) == tuple(source) + + +def confirmed(folder: str | Path, source: refs.Here | None = None) -> dict[str, str]: + """층에 확인된 실무 사전조합 `{본뜬 키: SC 키}`(`source` = 파일 층 (층, 주인)으로 거름).""" + return { + got["키"]: key + for key, got, _ in _origins(Path(folder)) + if got.get("확인") and _same_source(got, source) + } + + +def _save(doc: dict[str, Any]) -> tuple[dict[str, Any], bool]: + """(참조, 다시씀) — 같은 본뜬(키 · 원본)이 있으면 그 키에 덮음 · 없으면 새 키.""" at = refs.here() - have = _found(doc["본뜬"]["키"]) - if have and not fresh: - return {"층": at[0], "주인": at[1], "키": have[0]}, True + want = doc["본뜬"] + have = next( + ( + (key, store.version_of(raw)) + for key, got, raw in _origins(store.root()) + if got.get("키") == want["키"] and _same_source(got, tuple(want["원본"])) + ), + None, + ) if have: store.write("precombo", have[0], have[1], doc) key = have[0] @@ -74,10 +100,11 @@ def _num(value: Decimal) -> int | float: return int(value) if value == value.to_integral_value() else float(value) -def _memo(line: dict[str, Any], times: Any) -> str: +def _memo(line: dict[str, Any], one: dict[str, Any]) -> str: qty = line.get("수량") spec = str(line.get("규격") or "").strip() - head = f"실무 {qty if qty is not None else ''}{line.get('단위') or ''} × {times}" + head = f"실무 {qty if qty is not None else ''}{line.get('단위') or ''} × {one['배']}" + head += f"(몫 {one['몫']})" if one.get("몫") is not None else "" return f"{head} · {spec}" if spec else head @@ -90,96 +117,178 @@ def _missing(line: dict[str, Any], why: str) -> dict[str, Any]: } +def _bad(text: str, status: int = 400) -> store.StoreError: + return store.StoreError(status, text) + + class Builder: - """한 번 만들기 — 파일 · 층 기억 · 우리 쪽 목록을 한 번 읽고 호표마다 만든 참조를 기억.""" + """한 번 만들기 — 파일 · 우리 쪽 목록 · 확인된 호표를 한 번 읽고 몸을 검사한 뒤 씀.""" - def __init__(self, source: Path, folder: Path, key: str, fresh: bool) -> None: + def __init__(self, folder: Path, source: Path, key: str, no: int, who: Any) -> None: + self.folder = Path(folder) self.doc = hopyo_store.read(source, key) - self.key, self.fresh = key, fresh - self.memo, self.units = match.memories(folder), match.units_of(self.doc) - self.got = match.targets() - self.made: dict[int, dict[str, Any]] = {} - self.missing: dict[int, list[dict[str, Any]]] = {} + self.key, self.no, self.who = key, int(no), who + self.source = refs.where(source) + self.done = confirmed(folder, self.source) + self.known = ledger._known() + self.hopyo = next((h for h in self.doc.get("호표") or [] if int(h["번"]) == self.no), None) + if self.hopyo is None: + raise _bad(f"없는 호표 「제{no}호표」", 404) - def hopyo(self, no: int) -> dict[str, Any]: - found = next((h for h in self.doc.get("호표") or [] if int(h["번"]) == int(no)), None) - if found is None: - raise store.StoreError(404, f"없는 호표 「제{no}호표」") - return found + # ── 재검사 ─────────────────────────────────────────────────────── + def _target(self, raw: dict[str, Any], where: str) -> dict[str, Any]: + if raw.get("호표") is None: + return ledger.check_pair( + {k: v for k, v in raw.items() if k != "호표"}, where, self.known + ) + if raw.get("로직") or raw.get("자재") or raw.get("입력값"): + raise _bad(f"호표 대상은 호표 · 배만 「{where}」") + child = int(raw["호표"]) + if child == self.no: + raise _bad(f"호표가 자기를 담음 「{where}」") + if mark(self.key, child) not in self.done: + raise _bad(f"제{child}호표 먼저 확인", 409) + times, part = ledger.times_of(raw, where) + return {"호표": child, "배": times, "몫": part} - def build(self, no: int, trail: tuple[int, ...] = ()) -> dict[str, Any]: - """호표 → 사전조합 참조 — 일위N 은 먼저(순환은 빠진 줄).""" - if no in self.made: - return self.made[no] - hopyo = match.match_hopyo(self.hopyo(no), self.memo, self.units, True, self.got) - lines, mats, missing = [], {}, [] - used: set[str] = set() - for line in hopyo["줄"]: - got = line["맞춤"] - if not got["들어감"]: - conv = got.get("환산") or {} - why = WHY.get(got["상태"]) or ( - "추정 환산 — 확정 필요" if conv.get("갈래") == "추정" else "환산 입력 필요" - ) - missing.append(_missing(line, why)) - continue - times = got["환산"]["배"] - qty = Decimal(str(line.get("수량") or 0)) * Decimal(str(times)) - target = got["대상"] - alias = str(line.get("이름") or "")[: labor.ALIAS_MAX].strip() or None - memo = _memo(line, times) - if "자재" in target: - code = target["자재"] + def check(self, lines: list[dict[str, Any]], whole: dict[str, Any] | None) -> tuple[dict, list]: + """몸 재검사 → ({차례: 확인 줄}, 통째 대상) — 틀리면 400 · 하위 호표 409.""" + if whole is not None and lines: + raise _bad("통째 · 줄 중 하나만") + if not lines and not (whole or {}).get("대상"): + raise _bad("확인한 줄이 없음") + if len(lines) > LINES_MAX: + raise _bad(f"줄은 {LINES_MAX} 개 안") + have = self.hopyo.get("줄") or [] + out: dict[int, dict[str, Any]] = {} + for raw in lines: + at = int(raw.get("차례", -1)) + if not 0 <= at < len(have) or at in out: + raise _bad(f"없는 줄 · 겹친 줄 「제{self.no}호표 {at}」") + where = f"제{self.no}호표 {at + 1}째 줄" + targets = raw.get("대상") or [] + if bool(raw.get("뺌")) == bool(targets): + raise _bad(f"대상 · 뺌 중 하나만 「{where}」") + if raw.get("뺌") and raw["뺌"] not in ledger.KINDS[1:]: + raise _bad(f"뺌 까닭이 틀림 「{where}」") + if len(targets) > 20: + raise _bad(f"대상은 스물 이하 「{where}」") + out[at] = {"뺌": raw.get("뺌")} if raw.get("뺌") else { + "대상": [self._target(t, where) for t in targets] + } # fmt: skip + pairs = [self._target(t, f"제{self.no}호표 통째") for t in (whole or {}).get("대상") or []] + if any("호표" in p for p in pairs) or len(pairs) > 20: + raise _bad("통째 대상은 로직 · 자재만") + return out, pairs + + # ── 쓰기 ───────────────────────────────────────────────────────── + def _origin(self, origin: str, stamp: str) -> dict[str, Any]: + sure = {"누가": self.who, "때": stamp} + file = self.doc.get("파일") + return { + "층": ORIGIN, + "주인": None, + "키": origin, + "파일": file, + "원본": list(self.source), + "확인": sure, + } + + def build(self, checked: dict[int, dict[str, Any]], whole: list[dict[str, Any]]) -> dict: + stamp = datetime.now().isoformat(timespec="seconds") + hopyo, file = self.hopyo, self.doc.get("파일") or "" + lines, mats, missing, used = [], {}, [], set() + + def put(line: dict[str, Any], one: dict[str, Any], alias: str | None) -> None: + qty = Decimal(str(line.get("수량") or 0)) * Decimal(str(one["배"])) + memo = _memo(line, one) + if "자재" in one: apart = "별도계상" if line.get("갈래") == "별도계상" else "직접계산" - have = mats.get(code) + have = mats.get(one["자재"]) hand = qty + Decimal(str(have["수량"]["손"])) if have else qty - mats[code] = {"코드": code, "수량": {"손": _num(hand)}, "별도계상": apart} - continue - if "호표" in target: - child = int(target["호표"]) - if child in (*trail, no): - missing.append(_missing(line, "순환 — 호표가 서로 담음")) - continue - ref = self.build(child, (*trail, no)) + mats[one["자재"]] = { + "코드": one["자재"], + "수량": {"손": _num(hand)}, + "별도계상": apart, + } + return + if "호표" in one: + ref = { + "층": refs.here()[0], + "주인": refs.here()[1], + "키": self.done[mark(self.key, one["호표"])], + } lines.append({"참조": ref, "별칭": alias, "메모": memo, "수량": {"손": _num(qty)}}) - continue - one = {"로직": target["로직"], "입력값": got.get("입력값") or {}, "메모": memo} - if target["로직"] not in used: - used.add(target["로직"]) - lines.append({**one, "별칭": alias, "수량": {"손": _num(qty)}}) - continue + return + row = {"로직": one["로직"], "입력값": one["입력값"], "메모": memo} + if one["로직"] not in used: + used.add(one["로직"]) + lines.append({**row, "별칭": alias, "수량": {"손": _num(qty)}}) + return + at = int(line.get("차례", 0)) wrap = { # 같은 로직 둘째 — 한 줄 사전조합으로 감쌈 "이름": f"{hopyo['이름']} — {line.get('이름')}", - "단위": target.get("단위"), + "단위": line.get("단위") or None, "구분": ORIGIN, - "상세구분": self.doc.get("파일") or "", - "담은로직": [{**one, "별칭": alias, "수량": {"손": 1}}], + "상세구분": file, + "담은로직": [{**row, "별칭": alias, "수량": {"손": 1}}], "자재": [], - "본뜬": self._origin(mark(self.key, no, line["차례"])), + "본뜬": self._origin(f"{mark(self.key, self.no, at)} {one['로직']}", stamp), } - ref, _ = _save(wrap, self.fresh) + ref, _ = _save(wrap) lines.append({"참조": ref, "별칭": alias, "메모": memo, "수량": {"손": _num(qty)}}) + + book: dict[str, dict[str, Any]] = {} + if whole: + for one in whole: + put({"수량": 1, "단위": hopyo.get("단위"), "이름": hopyo.get("이름")}, one, None) + book[ledger.hopyo_key(hopyo)] = self._row(hopyo, "짝", whole) + for at, line in enumerate(hopyo.get("줄") or []): + got = checked.get(at) + if line.get("갈래") == "%": + missing.append(_missing(line, PERCENT)) + continue + if whole or got is None: + missing.append(_missing(line, WHOLE if whole else UNCHECKED)) + continue + if got.get("뺌"): + missing.append(_missing(line, f"뺌 — {got['뺌']}")) + book[ledger.key(line)] = self._row(line, got["뺌"], []) + continue + alias = str(line.get("이름") or "")[: labor.ALIAS_MAX].strip() or None + for one in got["대상"]: + put(line, one, alias) + pairs = [p for p in got["대상"] if "호표" not in p] + if pairs: + book[ledger.key(line)] = self._row(line, "짝", pairs) name = " ".join(x for x in (hopyo["이름"], hopyo.get("규격")) if x) doc = { "이름": name, "단위": hopyo.get("단위") or None, "구분": ORIGIN, - "상세구분": self.doc.get("파일") or "", + "상세구분": file, "담은로직": lines, "자재": list(mats.values()), - "본뜬": self._origin(mark(self.key, no)), + "본뜬": self._origin(mark(self.key, self.no), stamp), } - ref, again = _save(doc, self.fresh) - self.made[no] = {**ref, "다시씀": again} - self.missing[no] = missing - return self.made[no] + ref, again = _save(doc) + wrote = ledger.write(self.folder, book, self.who, file or None) + return {"참조": ref, "다시씀": again, "빠진줄": missing, "장부": {"씀": wrote}} - def _origin(self, origin: str) -> dict[str, Any]: - return {"층": ORIGIN, "주인": None, "키": origin, "파일": self.doc.get("파일")} + def _row(self, line: dict[str, Any], kind: str, pairs: list[dict[str, Any]]) -> dict[str, Any]: + """장부 한 줄 — 엑셀조항 = 줄 근거 출처(있으면).""" + origin = {"품명": line.get("이름"), "규격": line.get("규격"), "단위": line.get("단위")} + clauses = (line.get("근거") or {}).get("출처") or [] + return { + "원래": {k: str(v or "") for k, v in origin.items()}, + "갈래": kind, + "짝": pairs, + "엑셀조항": clauses if isinstance(clauses, list) else [], + } def compare(ref: dict[str, Any], hopyo: dict[str, Any]) -> dict[str, Any]: - """만든 사전조합 1 단위 값 ↔ 엑셀 호표 단가.""" + """만든 사전조합 1 단위 값 ↔ 엑셀 호표 단가(참고 · 판정 아님).""" got = refs.Expand(m01.loaded()[1])({k: ref[k] for k in ("층", "주인", "키")}) base = got.get("기준") or {} ours = float(base["계"]) if "계" in base else None @@ -188,17 +297,18 @@ def compare(ref: dict[str, Any], hopyo: dict[str, Any]) -> dict[str, Any]: return {"엑셀": excel, "우리": ours, "차이": gap, "까닭": got.get("까닭")} -def build(folder: Path, source: Path, key: str, no: int, fresh: bool = False) -> dict[str, Any]: - """실무 호표(`source` 층 파일) → 지금 층(`folder`) 사전조합 — - `{참조, 이름, 다시씀, 빠진줄, 비교}`(부르는 쪽이 `store.at(folder)`).""" - maker = Builder(source, folder, key, fresh) - made = maker.build(int(no)) - hopyo = maker.hopyo(int(no)) - ref = {k: made[k] for k in ("층", "주인", "키")} - return { - "참조": ref, - "이름": hopyo.get("이름"), - "다시씀": made["다시씀"], - "빠진줄": maker.missing.get(int(no), []), - "비교": compare(ref, hopyo), - } +def build( + folder: Path, + source: Path, + key: str, + no: int, + lines: list[dict[str, Any]] | None = None, + whole: dict[str, Any] | None = None, + who: Any = None, +) -> dict[str, Any]: + """실무 호표(`source` 층 파일) + 확인한 줄 → 지금 층(`folder`) 사전조합 · + 별칭 장부 — `{참조, 이름, 다시씀, 빠진줄, 비교, 장부}`(부르는 쪽이 `store.at(folder)`).""" + maker = Builder(folder, source, key, no, who) + checked, pairs = maker.check(list(lines or []), whole) + made = maker.build(checked, pairs) + return {**made, "이름": maker.hopyo.get("이름"), "비교": compare(made["참조"], maker.hopyo)} diff --git a/M02_MasterTemplete/M02_Hopyo_Ledger.py b/M02_MasterTemplete/M02_Hopyo_Ledger.py index 0b97c4b1c..560292915 100644 --- a/M02_MasterTemplete/M02_Hopyo_Ledger.py +++ b/M02_MasterTemplete/M02_Hopyo_Ledger.py @@ -178,9 +178,21 @@ def check_pair(raw: Any, where: str, known: tuple[set[str], set[str]]) -> dict[s values = raw.get("입력값") or {} if not isinstance(values, dict) or (mat and values): raise _bad(f"입력값이 틀림 「{where}」") - unknown = set(values) - input_names(code) if code else set() + unknown = set(values) - input_names(code) if code and values else set() if unknown: raise _bad(f"모르는 입력 {sorted(unknown)} 「{where}」") + times, part = times_of(raw, where) + head = {"로직": code} if code else {"자재": mat} + return { + **head, + "입력값": values, + "배": times, + "몫": part, + } + + +def times_of(raw: dict[str, Any], where: str) -> tuple[int | float, int | float | None]: + """(배, 몫) 재검사 — 배 0 넘는 수 · 몫 없음 또는 (0, 1].""" times = _number(raw.get("배", 1)) if times is None or times <= 0: raise _bad(f"배는 0 넘는 수 「{where}」") @@ -188,13 +200,7 @@ def check_pair(raw: Any, where: str, known: tuple[set[str], set[str]]) -> dict[s part = None if share is None else _number(share) if share is not None and (part is None or not 0 < part <= 1): raise _bad(f"몫은 0 넘고 1 이하 「{where}」") - head = {"로직": code} if code else {"자재": mat} - return { - **head, - "입력값": values, - "배": _json(times), - "몫": None if part is None else _json(part), - } + return _json(times), None if part is None else _json(part) def check(full: str, raw: Any, known: tuple[set[str], set[str]] | None = None) -> dict[str, Any]: diff --git a/M02_MasterTemplete/M02_Labor_Router.py b/M02_MasterTemplete/M02_Labor_Router.py index 55739aa3a..f528dedf1 100644 --- a/M02_MasterTemplete/M02_Labor_Router.py +++ b/M02_MasterTemplete/M02_Labor_Router.py @@ -4,23 +4,28 @@ 여기는 저장하지 않는 셈만 + 서고(단가승격 · 사전조합 · 50-3) 새로 · 복사(키는 서버가 줌 · 고침 · 지우기는 양식 길 그대로) + 개인 자재 목록 · 추가(148-2 · 개인 층만 · 품번은 서버) + 사전조합 나무 자유 폴더(158-1 · `M02_Precombo_Folders` · 읽기 = 층 읽기 · 고침 = 층 쓰기) + -일위대가 실무자료(324 · `M02_Hopyo_*` · 올리기 · 지우기 = 층 쓰기 · 목록 · 읽기 = 층 읽기). +일위대가 실무자료(324 · `M02_Hopyo_*` · 올리기 · 지우기 = 층 쓰기 · 목록 · 읽기 = 층 읽기) · +338 = 확인한 줄만 만들기(몸 없음 400 · 옛 즉시 확정 길 없앰) · 별칭 장부 보기 · 고치기 · +올린 뒤 맞춤 뒤 셈(`M02_Hopyo_Fit.ensure`) · 시동 때 우리 지문 데움(`M02_Hopyo_Ours.warm`). 층 권한 = `Router_Items._root`(`M02_Layer_Access` 한 벌). ⚠ 등록은 `main.py`(로그인만). """ from __future__ import annotations +import importlib +import inspect from pathlib import Path from typing import Any, Literal from fastapi import APIRouter, Depends, File, HTTPException, UploadFile -from pydantic import BaseModel +from pydantic import BaseModel, ConfigDict, Field from common_util.common_util_auth import verify_session from M02_MasterTemplete import M02_Hopyo_Build as hopyo_build from M02_MasterTemplete import M02_Hopyo_Excel as hopyo_excel from M02_MasterTemplete import M02_Hopyo_Grid as hopyo_grid +from M02_MasterTemplete import M02_Hopyo_Ledger as hopyo_ledger from M02_MasterTemplete import M02_Hopyo_Match as hopyo_match from M02_MasterTemplete import M02_Hopyo_Store as hopyo_store from M02_MasterTemplete import M02_Labor_Doc as labor @@ -168,10 +173,27 @@ def new_material( return _call(folder, own.add, folder, body) -# ── 일위대가 실무자료(324) ───────────────────────────────────────────── +# ── 일위대가 실무자료(324 · 338) ─────────────────────────────────────── HOPYO_MAX_BYTES = 20 * 1024 * 1024 +def _later(module: str, name: str): + """맞춤 엔진(가 창) 함수 — 아직 없으면 None(올리기 · 시동은 그대로).""" + try: + got = importlib.import_module(f"M02_MasterTemplete.{module}") + except ImportError: + return None + return getattr(got, name, None) + + +if (_warm := _later("M02_Hopyo_Ours", "warm")) is not None: + _warm() # 시동 — 우리 지문 캐시(디스크) 읽어 둠 + + +def _who(session: Session) -> dict[str, Any]: + return {"id": session.get("user_id"), "이름": session.get("name")} + + def _upload(name: str, data: bytes, who: dict[str, Any]) -> dict[str, Any]: try: parsed = hopyo_excel.read_book(name, data) @@ -181,7 +203,10 @@ def _upload(name: str, data: bytes, who: dict[str, Any]) -> dict[str, Any]: grid = hopyo_grid.read(name, data, parsed) except Exception: # 격자는 보기용 덤 — 못 만들어도 올리기는 됨(보기 창 404) grid = None - return hopyo_store.save(store.root(), name, parsed, who, grid) + row = hopyo_store.save(store.root(), name, parsed, who, grid) + if (ensure := _later("M02_Hopyo_Fit", "ensure")) is not None: + ensure(store.root(), row["키"]) # 맞춤 뒤 셈(일감 줄 · 화면 안 기다림) + return row @router.get("/layers/{layer}/hopyo") @@ -198,9 +223,20 @@ def hopyo_picks( layer: Layer, owner: str | None = None, session: Session = Depends(verify_session) ) -> dict: """[로직 추가] 「실무 호표」 줄 — 이 층 · 위 층 파일의 호표마다 - `{층, 주인, 키, 파일, 번, 이름, 규격, 단위, 맞춤}`(맞춤 n/m = 이 층 기억).""" + `{층, 주인, 키, 파일, 번, 이름, 규격, 단위, 맞춤, 확인}`(확인 = 이 층에 확인된 사전조합).""" folder = _root(session, layer, owner) - return {"줄": _call(folder, hopyo_match.picks, folder)} + return {"줄": _call(folder, _picks, folder)} + + +def _picks(folder: Path) -> list[dict[str, Any]]: + rows, here, done = hopyo_match.picks(folder), refs.where(folder), {} + for row in rows: + src = (row["층"], row.get("주인")) + if src not in done: + done[src] = hopyo_build.confirmed(folder, src) + key = done[src].get(hopyo_build.mark(row["키"], row["번"])) + row["확인"] = {"층": here[0], "주인": here[1], "키": key} if key else None + return rows @router.post("/layers/{layer}/hopyo") @@ -217,8 +253,7 @@ def hopyo_upload( if len(data) > HOPYO_MAX_BYTES: raise HTTPException(status_code=400, detail="파일이 너무 큼(20 MB 넘음)") name = Path(file.filename or "실무자료").name[:120] - who = {"id": session.get("user_id"), "이름": session.get("name")} - return _call(folder, _upload, name, data, who) + return _call(folder, _upload, name, data, _who(session)) @router.get("/layers/{layer}/hopyo/{key}") @@ -246,11 +281,17 @@ def hopyo_one( key: str, no: int, owner: str | None = None, + to: Layer | None = None, + to_owner: str | None = None, session: Session = Depends(verify_session), ) -> dict: - """호표 하나 — 줄마다 맞춤(상태 · 대상 · 입력값 · 환산 · 들어감 · 후보 5).""" + """호표 하나 — 줄마다 맞춤(상태 · 체크 제안 · 대상 · 후보 5) · `to` = 만들 층(없으면 파일 층 · + 호표 참조 줄 체크 · 만든 · 맞춤 엔진이 `to` 를 받을 때만).""" folder = _root(session, layer, owner) - return _call(folder, hopyo_match.view, folder, key, no) + if to is None or "to" not in inspect.signature(hopyo_match.view).parameters: + return _call(folder, hopyo_match.view, folder, key, no) + target = _root(session, to, to_owner) + return _call(folder, lambda: hopyo_match.view(folder, key, no, to=target)) class GuessBody(BaseModel): @@ -274,33 +315,44 @@ def hopyo_guess( return _call(folder, hopyo_match.guess, folder, key, no, body.차례, body.대상) -class MemoryBody(BaseModel): - """`줄` = {줄 열쇠: {로직|자재, 입력값, 환산} | {빼기: true} | null(지움)}.""" - - 줄: dict[str, Any] +class _Strict(BaseModel): + model_config = ConfigDict(extra="forbid") # 금액 · 품 칸 = 400(서버가 셈) -@router.post("/layers/{layer}/hopyo-memory") -def hopyo_memory( - layer: Layer, - body: MemoryBody, - owner: str | None = None, - session: Session = Depends(verify_session), -) -> dict: - """맞춤 확정 · 빼기 → 층 기억(층 쓰기) — 다음부터 같은 줄은 자동.""" - folder = _root(session, layer, owner, write=True) - return _call(folder, hopyo_match.confirm, folder, body.줄) +class BuildTarget(_Strict): + """대상 하나 — 로직 · 자재 · 호표 중 하나 · 배 = 우리 수량 ÷ 엑셀 수량 · 몫 = 섞인 줄 몫.""" + + 로직: str | None = None + 자재: str | None = None + 호표: int | None = None + 입력값: dict[str, Any] = Field(default_factory=dict) + 배: float = 1 + 몫: float | None = None -class BuildBody(BaseModel): - """실무 호표 → 이 층 사전조합 — 파일 층 `층` · `주인` · `키` + 호표 `번` · - `새로` = 있던 것을 덮음.""" +class BuildLine(_Strict): + """확인한 줄 — `차례` = 호표 안 줄(0부터) · `대상` 목록 또는 `뺌` 까닭.""" + + 차례: int + 대상: list[BuildTarget] = Field(default_factory=list) + 뺌: Literal["우리에 없음", "안 씀"] | None = None + + +class BuildWhole(_Strict): + 대상: list[BuildTarget] + + +class BuildBody(_Strict): + """실무 호표 → 이 층 사전조합 — 파일 층 `층` · `주인` · `키` + 호표 `번` + 확인한 `줄` · + 직접 줄 호표 = `통째` · 같은 본뜬은 늘 덮음(`새로` = 옛 화면 호환 · 안 씀).""" 층: Layer 주인: str | None = None 키: str 번: int 새로: bool = False + 줄: list[BuildLine] = Field(default_factory=list, max_length=500) + 통째: BuildWhole | None = None @router.post("/layers/{layer}/hopyo-build") @@ -310,11 +362,43 @@ def hopyo_make( owner: str | None = None, session: Session = Depends(verify_session), ) -> dict: - """실무 호표 → 우리 일위대가 사전조합(이 층 쓰기 · 파일 층 읽기) — - `{참조, 이름, 다시씀, 빠진줄, 비교}`.""" + """[확인하고 사전조합 만들기] — 확인한 줄만(이 층 쓰기 · 파일 층 읽기) · 별칭 장부 씀 → + `{참조, 이름, 다시씀, 빠진줄, 비교, 장부}` · 몸 없음 400 · 하위 호표 확인 전 409.""" folder = _root(session, layer, owner, write=True) source = _root(session, body.층, body.주인) - return _call(folder, hopyo_build.build, folder, source, body.키, body.번, body.새로) + lines = [one.model_dump(exclude_none=True) for one in body.줄] + whole = body.통째.model_dump(exclude_none=True) if body.통째 else None + args = (folder, source, body.키, body.번, lines, whole, _who(session)) + return _call(folder, hopyo_build.build, *args) + + +@router.get("/layers/{layer}/hopyo-ledger") +def hopyo_ledger_read( + layer: Layer, owner: str | None = None, session: Session = Depends(verify_session) +) -> dict: + """별칭 장부 — 이 층 + 위 층 줄 `{열쇠, 원래, 갈래, 짝, 누가, 때, 처음파일, 층, 주인, 고침}` + (고침 = 이 층 · 쓰기 권한).""" + folder = _root(session, layer, owner) + writable = access.grant(session, layer, owner).write + return {"줄": _call(folder, hopyo_ledger.rows, folder, writable)} + + +class LedgerBody(_Strict): + """`줄` = {열쇠: 장부 줄 | null(지움)} — 이 층만 · 서버가 짝을 다시 검사.""" + + 줄: dict[str, Any] + + +@router.put("/layers/{layer}/hopyo-ledger") +def hopyo_ledger_write( + layer: Layer, + body: LedgerBody, + owner: str | None = None, + session: Session = Depends(verify_session), +) -> dict: + """별칭 장부 [저장] — 고친 줄 · 지운 줄(층 쓰기) → `{씀, 줄}`.""" + folder = _root(session, layer, owner, write=True) + return _call(folder, hopyo_ledger.put, folder, body.줄, _who(session)) @router.delete("/layers/{layer}/hopyo/{key}") diff --git a/resources/tester/test_m02_hopyo_build.py b/resources/tester/test_m02_hopyo_build.py index 1284a544c..06b15d287 100644 --- a/resources/tester/test_m02_hopyo_build.py +++ b/resources/tester/test_m02_hopyo_build.py @@ -1,11 +1,13 @@ -"""일위대가 실무자료 → 사전조합 만들기(PLAN 324-3 · `M02_Hopyo_Build`) — 별칭 남음 · -수량 = 엑셀 × 환산 · 자재 · 같은 로직 둘 = 감쌈 · 일위N 참조 · 순환 · -같은 호표 다시 = 같은 사전조합 · 새로 = 덮음 · -빠진 줄 · 값 비교 · 길 권한 · master_data 원본 그대로. 층은 임시 폴더. +"""일위대가 실무자료 → 사전조합 만들기(PLAN 324-3 · 338-3 · `M02_Hopyo_Build`) — 확인한 줄만 · +몸 없음 400 · 빠진 줄 까닭 · 로직 여럿 같은 별칭 · 같은 로직 감쌈 · 배 · 몫 · 자재 · 통째 · +재검사 400(없는 로직 · 모르는 입력 · 배 ≤ 0 · 금액 칸) · 하위 호표 409 → 확인 뒤 참조 · 장부 씀 · +`본뜬.확인` · 덮음 · 옛 즉시 확정 길 404 · 장부 길(보기 · 고치기 · 권한) · picks 확인 · +master_data 원본 그대로. 층은 임시 폴더. """ import json +from M02_MasterTemplete import M02_Hopyo_Build as hb from M02_MasterTemplete import M02_Hopyo_Store as hs from M02_MasterTemplete import M02_Template_Layers as layers @@ -16,18 +18,14 @@ from resources.tester.test_m02_hopyo_excel import URL, _post, client, master_has DIG = "구조물터파기(인력100%)|토사|m3" FILL = "구조물되메우기(인력100%)||m3" SOIL = "잔토처리(인력100%)||m3" -STONE = "임도표지석(받침,글새김포함)|900x600x200x150|m3" -MEMORY = "/api/m02/layers/{}/hopyo-memory" BUILD = "/api/m02/layers/{}/hopyo-build" +LEDGER = "/api/m02/layers/{}/hopyo-ledger" -def _remember(client, layer="personal", **lines): - res = client.post(MEMORY.format(layer), json={"줄": lines}) - assert res.status_code == 200, res.text - - -def _build(client, no, layer="personal", want=200, **extra): +def _build(client, no, lines=None, layer="personal", want=200, **extra): body = {"층": "personal", "키": "HP-000001", "번": no, **extra} + if lines is not None: + body["줄"] = lines res = client.post(BUILD.format(layer), json=body) assert res.status_code == want, res.text return res.json() @@ -37,22 +35,30 @@ def _doc(layer_dir, key): return json.loads((layer_dir / "precombo" / f"{key}.json").read_bytes()) -def test_만들기_별칭_수량_메모_빠진_줄_비교(client): +def _book(layer_dir): + return json.loads((layer_dir / hs.DIRNAME / f"{hs.MEMORY}.json").read_bytes())["줄"] + + +def test_확인한_줄만_별칭_수량_빠진_줄_장부(client): before = master_hash() assert _post(client).status_code == 200 - _remember( - client, - **{ - DIG: {"로직": "GF000101", "입력값": {"지역": "서울"}, "환산": 1}, - FILL: {"로직": "GF000113", "환산": 2}, - }, - ) - got = _build(client, 3) + lines = [ + {"차례": 1, "대상": [{"로직": "GF000101", "입력값": {"지역": "서울"}, "배": 1}]}, + {"차례": 2, "대상": [{"로직": "GF000113", "배": 2}]}, + {"차례": 3, "뺌": "우리에 없음"}, + ] + got = _build(client, 3, lines) mine = layers.personal_dir(7, 42) doc = _doc(mine, got["참조"]["키"]) assert (doc["이름"], doc["단위"], doc["구분"]) == ("임도표지석 910×600×150", "개소", "실무자료") assert doc["상세구분"].endswith("(공통단가).xls") - assert doc["본뜬"]["층"] == "실무자료" and doc["본뜬"]["키"] == "HP-000001 제3호표" + origin = doc["본뜬"] + assert (origin["층"], origin["키"], origin["원본"]) == ( + "실무자료", + "HP-000001 제3호표", + ["personal", "7/42"], + ) + assert origin["확인"]["누가"] == {"id": 42, "이름": "갑"} and origin["확인"]["때"] assert doc["담은로직"] == [ { "로직": "GF000101", @@ -65,88 +71,167 @@ def test_만들기_별칭_수량_메모_빠진_줄_비교(client): "로직": "GF000113", "입력값": {}, "별칭": "구조물되메우기(인력100%)", - "수량": {"손": 0.1}, # 엑셀 0.05 × 환산 2 + "수량": {"손": 0.1}, # 엑셀 0.05 × 배 2 "메모": "실무 0.05㎥ × 2", }, ] - assert [(x["이름"], x["금액"]) for x in got["빠진줄"]] == [ - ("임도표지석(받침,글새김포함)", 1400000), - ("잔토처리(인력100%)", 3097.1), + assert doc["자재"] == [] # 표지석 = 몸에 없음 → 안 들어감 + assert [(x["이름"], x["금액"], x["까닭"]) for x in got["빠진줄"]] == [ + ("임도표지석(받침,글새김포함)", 1400000, "확인 안 함"), + ("잔토처리(인력100%)", 3097.1, "뺌 — 우리에 없음"), ] assert got["비교"]["엑셀"] == 1415169 and got["비교"]["우리"] > 0 - assert got["비교"]["차이"] == round(got["비교"]["우리"] - 1415169, 1) + assert got["다시씀"] is False and got["장부"] == {"씀": 3} + book = _book(mine) + assert book[DIG]["갈래"] == "짝" and book[DIG]["짝"] == [ + {"로직": "GF000101", "입력값": {"지역": "서울"}, "배": 1, "몫": None} + ] + assert book[DIG]["처음파일"].endswith("(공통단가).xls") + assert book[DIG]["원래"] == {"품명": "구조물터파기(인력100%)", "규격": "토사", "단위": "㎥"} + assert book[SOIL]["갈래"] == "우리에 없음" and book[FILL]["짝"][0]["배"] == 2 + assert hb.confirmed(mine) == {"HP-000001 제3호표": got["참조"]["키"]} + assert hb.confirmed(mine, ("system", None)) == {} # 다른 파일 층 = 따로 assert master_hash() == before -def test_자재_같은_로직_감쌈_다시씀_새로(client): +def test_로직_여럿_같은_별칭_감쌈_자재_몫_덮음(client): assert _post(client).status_code == 200 - _remember( - client, - **{ - DIG: {"로직": "GF000101", "환산": 1}, - SOIL: {"로직": "GF000101", "환산": 1}, - STONE: {"자재": "MT004258", "환산": 1}, - }, - ) - got = _build(client, 3) + lines = [ + {"차례": 0, "대상": [{"자재": "MT004258", "배": 1}]}, + {"차례": 1, "대상": [{"로직": "GF000101"}, {"로직": "GF000113", "배": 0.1, "몫": 0.1}]}, + {"차례": 3, "대상": [{"로직": "GF000101"}]}, + ] + got = _build(client, 3, lines) mine = layers.personal_dir(7, 42) doc = _doc(mine, got["참조"]["키"]) - first, wrapped = doc["담은로직"] - assert first["로직"] == "GF000101" and first["별칭"] == "구조물터파기(인력100%)" + first, second, wrapped = doc["담은로직"] + assert first["별칭"] == second["별칭"] == "구조물터파기(인력100%)" # 한 줄 로직 둘 = 같은 별칭 + assert second["수량"] == {"손": 0.014} and second["메모"] == "실무 0.14㎥ × 0.1(몫 0.1) · 토사" assert wrapped["별칭"] == "잔토처리(인력100%)" and wrapped["수량"] == {"손": 0.09} inner = _doc(mine, wrapped["참조"]["키"]) - assert inner["본뜬"]["키"] == "HP-000001 제3호표 4째 줄" + assert inner["본뜬"]["키"] == "HP-000001 제3호표 4째 줄 GF000101" assert inner["담은로직"][0]["로직"] == "GF000101" and inner["담은로직"][0]["수량"] == {"손": 1} assert [(m["코드"], m["수량"], m["별도계상"]) for m in doc["자재"]] == [ ("MT004258", {"손": 1}, "직접계산") ] + stone = next(v for k, v in _book(mine).items() if k.startswith("임도표지석(받침")) + assert stone["짝"][0]["자재"] == "MT004258" files = sorted(p.name for p in (mine / "precombo").glob("*.json")) - again = _build(client, 3) + again = _build(client, 3, lines[:1]) # 다시 확인 = 같은 본뜬 덮음 assert again["다시씀"] is True and again["참조"] == got["참조"] - assert sorted(p.name for p in (mine / "precombo").glob("*.json")) == files - _remember(client, **{SOIL: {"빼기": True}}) - fresh = _build(client, 3, 새로=True) - assert fresh["참조"] == got["참조"] and fresh["다시씀"] is True - assert len(_doc(mine, got["참조"]["키"])["담은로직"]) == 1 # 덮음 + assert _doc(mine, got["참조"]["키"])["담은로직"] == [] assert sorted(p.name for p in (mine / "precombo").glob("*.json")) == files -def test_일위N_참조_먼저_만듦(client): +def test_몸_없음_재검사_400(client): assert _post(client).status_code == 200 - got = _build(client, 45) + one = {"로직": "GF000101", "배": 1} + _build(client, 3, want=400) # 옛 몸(줄 없음) = 확인한 줄이 없음 + _build(client, 3, [], want=400) + bad = [ + [{"차례": 9, "대상": [one]}], # 호표 밖 줄 + [{"차례": 1, "대상": [one]}, {"차례": 1, "대상": [one]}], # 겹친 줄 + [{"차례": 1, "대상": []}], # 대상 · 뺌 없음 + [{"차례": 1, "대상": [one], "뺌": "안 씀"}], + [{"차례": 1, "대상": [{"로직": "XX"}]}], + [{"차례": 1, "대상": [{"로직": "GC000422"}]}], # 돈 아닌 로직 + [{"차례": 1, "대상": [{**one, "입력값": {"없는칸": 1}}]}], + [{"차례": 1, "대상": [{**one, "배": 0}]}], + [{"차례": 1, "대상": [{**one, "배": -1}]}], + [{"차례": 1, "대상": [{**one, "몫": 1.5}]}], + [{"차례": 1, "대상": [{**one, "자재": "MT004258"}]}], + [{"차례": 1, "대상": [{"자재": "MT000000"}]}], + [{"차례": 1, "대상": [{"호표": 3}]}], # 자기를 담음 + ] + for lines in bad: + _build(client, 3, lines, want=400) + for extra in ({"금액": 100}, {"품": {"보통인부": 1}}): # 금액 · 품 칸 = 받지 않음 + assert ( + client.post( + BUILD.format("personal"), + json={ + "층": "personal", + "키": "HP-000001", + "번": 3, + "줄": [{"차례": 1, "대상": [{**one, **extra}]}], + }, + ).status_code + == 422 + ) + assert client.post(BUILD.format("personal"), json={ + "층": "personal", "키": "HP-000001", "번": 3, "금액": 1, + "줄": [{"차례": 1, "대상": [one]}]}).status_code == 422 # fmt: skip + assert not (layers.personal_dir(7, 42) / "precombo").exists() or not list( + (layers.personal_dir(7, 42) / "precombo").glob("SC-*.json") + ) # 틀린 몸은 아무것도 안 씀 + assert client.post("/api/m02/layers/personal/hopyo-memory", json={"줄": {}}).status_code in ( + 404, + 405, + ) # 옛 즉시 확정 길 없음 + + +def test_하위_호표_먼저_확인_퍼센트_빠짐_통째(client): + assert _post(client).status_code == 200 + ref = [{"차례": 0, "대상": [{"호표": 46, "배": 1}]}] + assert "제46호표 먼저 확인" in _build(client, 45, ref, want=409)["detail"] + child = _build(client, 46, [{"차례": 0, "뺌": "안 씀"}]) + assert any(x["까닭"].startswith("% 줄") for x in child["빠진줄"]) + got = _build(client, 45, ref) mine = layers.personal_dir(7, 42) - lines = _doc(mine, got["참조"]["키"])["담은로직"] - ref = lines[0] - assert ref["별칭"] == "고화매트재료비" and ref["수량"] == {"손": 2.3} - child = _doc(mine, ref["참조"]["키"]) - assert child["이름"] == "고화매트재료비" and child["본뜬"]["키"] == "HP-000001 제46호표" - assert any(x["까닭"].startswith("% 줄") for x in _build(client, 46)["빠진줄"]) + line = _doc(mine, got["참조"]["키"])["담은로직"][0] + assert line["참조"] == child["참조"] and line["별칭"] == "고화매트재료비" + assert line["수량"] == {"손": 2.3} + assert "고화매트재료비||m2" not in _book(mine) # 호표 참조 줄 = 장부 안 씀 + whole = _build(client, 3, 통째={"대상": [{"로직": "GF000101", "배": 1}]}) + doc = _doc(mine, whole["참조"]["키"]) + assert [x["로직"] for x in doc["담은로직"]] == ["GF000101"] + assert doc["담은로직"][0]["수량"] == {"손": 1} + assert {x["까닭"] for x in whole["빠진줄"]} == {"호표 통째로 대신"} + head = next(v for k, v in _book(mine).items() if k.startswith("호표|임도표지석|")) + assert head["짝"][0]["로직"] == "GF000101" and head["원래"]["품명"] == "임도표지석" + _build(client, 3, [ref[0] | {"차례": 1}], 통째={"대상": [{"로직": "GF000101"}]}, want=400) -def test_순환은_빠진_줄(client): - folder = layers.personal_dir(7, 42) - _post(client) # 층 뼈대 - line = {"차례": 0, "이름": "남", "규격": "", "수량": 1, "단위": "식", "갈래": "호표"} - money = {"계": 1, "노무비": 1, "재료비": None, "경비": None} - hopyos = [ - {"번": 1, "이름": "가", "규격": "", "단위": "식", "금액": money, "줄": [ - {**line, "참조": {"종류": "일위", "번": 2}, "금액": money}]}, - {"번": 2, "이름": "나", "규격": "", "단위": "식", "금액": money, "줄": [ - {**line, "참조": {"종류": "일위", "번": 1}, "금액": money}]}, - ] # fmt: skip - key = hs.save(folder, "loop.xls", {"시트": "일위대가", "호표": hopyos, "검산": {}}, None)["키"] - got = _build(client, 1, 키=key) - assert got["빠진줄"] == [] - inner = _doc(folder, _doc(folder, got["참조"]["키"])["담은로직"][0]["참조"]["키"]) - assert inner["담은로직"] == [] # 제2호표 안 제1호표 = 순환 → 빠짐 - - -def test_길_권한과_위_층_파일(client): +def test_길_권한과_위_층_파일_picks_확인(client): client.who.update(role="SYSTEM_ADMIN") assert _post(client, "system").status_code == 200 client.who.update(role="USER") - _build(client, 3, layer="system", 층="system", want=403) # 마스터 쓰기 = 시스템 관리자 - got = _build(client, 3, 층="system") # 마스터 파일 → 내 층 사전조합 + lines = [{"차례": 1, "대상": [{"로직": "GF000101"}]}] + _build(client, 3, lines, layer="system", 층="system", want=403) # 마스터 쓰기 = 시스템 관리자 + got = _build(client, 3, lines, 층="system") # 마스터 파일 → 내 층 사전조합 assert got["참조"]["층"] == "personal" and got["참조"]["주인"] == "7/42" - assert _build(client, 99, 층="system", want=404) + _build(client, 99, lines, 층="system", want=404) assert client.get(URL.format("personal")).json()["줄"] == [] # 파일은 마스터에만 + rows = client.get("/api/m02/layers/personal/hopyo-picks").json()["줄"] + three = next(r for r in rows if r["번"] == 3) + assert three["확인"] == {"층": "personal", "주인": "7/42", "키": got["참조"]["키"]} + assert next(r for r in rows if r["번"] == 4)["확인"] is None + + +def test_장부_길_보기_고치기_권한(client): + before = master_hash() + assert _post(client).status_code == 200 + _build(client, 3, [{"차례": 1, "대상": [{"로직": "GF000101"}]}]) + rows = client.get(LEDGER.format("personal")).json()["줄"] + assert [(r["열쇠"], r["층"], r["고침"]) for r in rows] == [(DIG, "personal", True)] + put = { + "줄": {DIG: {"갈래": "짝", "짝": [{"로직": "GF000103", "배": 2}]}, SOIL: {"갈래": "안 씀"}} + } + res = client.put(LEDGER.format("personal"), json=put) + assert res.status_code == 200, res.text + assert res.json()["씀"] == 2 + book = _book(layers.personal_dir(7, 42)) + assert book[DIG]["짝"][0] == {"로직": "GF000103", "입력값": {}, "배": 2, "몫": None} + assert book[DIG]["처음파일"].endswith("(공통단가).xls") # 처음파일 둠 + bad = {"줄": {DIG: {"갈래": "짝", "짝": [{"로직": "XX"}]}}} + assert client.put(LEDGER.format("personal"), json=bad).status_code == 400 + assert client.put(LEDGER.format("personal"), json={"줄": {DIG: None}}).status_code == 200 + assert DIG not in _book(layers.personal_dir(7, 42)) + assert client.put(LEDGER.format("system"), json=put).status_code == 403 # 위 층 = 읽기만 + client.who.update(role="SYSTEM_ADMIN") + assert client.put(LEDGER.format("system"), json=put).status_code == 200 + client.who.update(role="USER") + rows = client.get(LEDGER.format("personal")).json()["줄"] + assert {(r["층"], r["고침"]) for r in rows} == {("personal", True), ("system", False)} + assert client.get(LEDGER.format("system")).json()["줄"][0]["고침"] is False + assert master_hash() == before From 33207a450ce5da7d867291f7c3a9232884554665 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 15 Oct 2026 23:25:05 +0900 Subject: [PATCH 56/81] =?UTF-8?q?chore(=EC=A1=B0=EC=82=AC):=20337=20?= =?UTF-8?q?=EB=82=B4=EC=9E=A5=20CAD=20=EC=98=A4=ED=86=A0=EC=BA=90=EB=93=9C?= =?UTF-8?q?=20=EC=A1=B0=EC=9E=91=20=EC=84=A4=EA=B3=84=20=E2=80=94=20?= =?UTF-8?q?=EC=98=B5=EC=85=98=20=ED=8B=80=20=EC=95=BD=EC=86=8D=20=C2=B7=20?= =?UTF-8?q?=EB=AA=85=EB=A0=B9=2031=20=ED=94=84=EB=A1=AC=ED=94=84=ED=8A=B8?= =?UTF-8?q?=20=EC=82=AC=EC=A0=84=20=C2=B7=20=ED=9D=A0=2023=20=C2=B7=20?= =?UTF-8?q?=EB=A7=88=EC=9A=B0=EC=8A=A4=20=C2=B7=20=ED=9C=A0=20=C2=B7=20?= =?UTF-8?q?=EC=B0=BD=20=EB=8B=A4=EC=84=AF=20=C2=B7=20=EC=8B=9C=ED=97=98?= 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_01C6BNBFXFndbqYxu6sZuknK --- .../2026-10-15_337_CAD오토캐드조작_설계.md | 413 ++++++++++++++++++ 1 file changed, 413 insertions(+) create mode 100644 docs/raw/조사/2026-10-15_337_CAD오토캐드조작_설계.md diff --git a/docs/raw/조사/2026-10-15_337_CAD오토캐드조작_설계.md b/docs/raw/조사/2026-10-15_337_CAD오토캐드조작_설계.md new file mode 100644 index 000000000..159a7f7f1 --- /dev/null +++ b/docs/raw/조사/2026-10-15_337_CAD오토캐드조작_설계.md @@ -0,0 +1,413 @@ +# 요구 229-3 내장 CAD — 오토캐드와 같은 명령 · 옵션 조작 설계 (PA3) + +- 기준 = `main_laptop_1` 8a4598c05(CAD 코드는 335 조사 기준 9c0ef7f82 와 바뀐 줄 0 · 줄 번호 그대로) · 코드 읽기 + 오토캐드 한국어 도움말(2025 KOR) 대조 · 저장소 고침 없음. +- 재료 = `docs/raw/조사/2026-10-15_335_다시조사_오토캐드마우스키보드.md`(대조 146 · 흠 30) · 부록4(K1 공식 · K2 우리 코드) · `2026-10-15_335_CAD입력보강_검토.md`(H1~H12). +- 줄임 — `src/` = `B07_DesignDetail/openwebcad/src/` · `ic/` = `src/inputController/` · `tl/` = `src/tools/` · `T/` = `resources/tester/cad/`. +- 근거 표 — [공] 한국어 도움말에 그 줄 그대로 · [이] 한국어 도움말 옵션 이름 + 영문 공식 줄 꼴로 엮음 · [옮] 영문 공식 줄 옮김(한국어판 이름 못 봄) · [기] 기억(공식 쪽 없음 · 브레인 화면 확인 거리). + +## 0. 요약 + +1. 바닥 셋을 새로 깖 — ① 친 글 처리 한 곳(`ic/prompt-input.ts` 사다리) ② 물음 선언 한 꼴(`src/commands/prompt.ts` 의 PromptSpec · 옵션 사건 `OPTION.<키>`) ③ 명령 끝 = 도구 머신 final 상태 `END` → 쉬는 상태(새 선택 도구)로 바꿔 끼움. +2. 따라 나오는 동작 — 점 · 옵션 묻는 중 명령 이름 거절(「점 또는 옵션 키워드를 요구함.」) · 앞글자 실행 없음 · 대소문자 무관 · Esc 한 번 = 명령 끝 · 빈 Enter = 명령별 뜻(기본값 · 기본 옵션 · 변위 · 끝) · 우클릭 = Enter(쉴 때 반복) · 기본값 세션 기억 · 한 번 그리는 명령(원 · 사각형 · 호 …)은 하나 뒤 끝(오토캐드처럼 Space 로 다시). +3. 결론으로 바뀌는 지금 동작 하나 — 지금은 선 · 원 · 사각형 등이 끝나도 같은 명령 첫 물음으로 돌아감 → 「점 묻는 중 명령 이름 거절」과 맞부딪힘(원 그린 뒤 L 이 안 먹음) → 오토캐드 「끝난 뒤」 열(1 장)대로 끝냄. +4. 명령 31 사전(1 장) — 물음 차례 · 옵션 글자 · 기본값 · 빈 Enter · Esc · 끝난 뒤 · 지금과 다른 곳 · 이번 넣을 옵션 / 뺄 옵션(뺀 옵션은 [ ] 에 안 그림 · 치면 「이 CAD 에 없음」 안내). +5. 흠 30 중 이번 고침 23(H1 · H2 일부 · H3 · H4 · H5 · H7 · H8 · H9 · H11 · N1~N11 · N14 · N17 · N18) + 새 흠 하나(3 번) · 뺌 7(H6 · H10 · H12 · N12 · N13 · N15 · N16) + H2 나머지. +6. 마우스 · 키 넣음 — 고르기 더함 · Shift 뺌 · 명령 중 끌기 상자 · 그립 누르고-옮기고-누르기 · 그립 숫자 · 직교 강제 + Shift 임시 직교 · 휠 밂 = 확대(기본) · 휠 두 번 = 범위 · 우클릭 반복 · Shift+우클릭 스냅 메뉴 · Ctrl+C/X/Shift+C/Shift+V/1 · ↑↓ 최근 값 · ZOOM 옵션(E · A · W · P · nX). +7. 창 다섯 — 가(틀 · 키보드) 먼저 → 나(그리기 12) · 다(고치기 · 재기 · 치수 12) · 라(옮기기 무리 7) 동시 · 마(마우스 · 고르기 · 그립 · 휠 · 보기)는 처음부터 동시 · 겹치는 파일은 줄 몫으로 나눔(5 장). +8. 시험 — 공용 조작 도우미 `T/helpers/cad-drive.ts` + 창마다 새 시험 하나 + 계약 시험(옵션 선언 ↔ 처리 · ESC 맞대기) · 완료 = 시험 · 타입 검사 둘 · CAD 빌드. +9. 브레인 화면 확인 거리 — PLINE Esc 남김 · SPLINE Esc 버림 · ARRAY · TEXT Esc · 한국어판 줄 꼴 · 휠 · 그립 · 스냅 메뉴([기] 줄). + +브레인 정함 → 이 설계 자리: + +| 정함 | 자리 | +|---|---| +| 앞글자 실행 없앰 · 별칭 acad.pgp | 2-5 ⑨ · 2-17 · N18 | +| 대소문자 무관(글 물음만 원문) | 2-4 · N6 | +| Esc = 명령 끝 | 2-7 · 2-8 · 1 장 Esc 열 · N2 · N3 · N4 | +| 점 · 옵션 묻는 중 명령 이름 거절 | 2-5 ⑩ · 2-10 · N1 · H7 | +| 옵션 글자 = 오토캐드 글자 · 빈 Enter 명령별 | 1 장 · 2-6 | +| 기본값 기억(세션) | 2-9 | +| 우클릭 · 고르기 · 그립 · 직교 · 휠 두 번 · 휠 밂 | 4 장 · H3 · H4 · H5 · H8 | +| 식 = 숫자 칸 거절('CAL 밖) | N5 · 7 장 | + +## 1. 프롬프트 사전(명령마다) + +- 줄 꼴 = 「글 또는 [이름(키)/…] <기본값>:」 한 꼴(`renderPrompt` 가 붙임) · 한국어판은 「호의 시작점 또는 [중심(C)] 지정:」처럼 「지정」이 뒤로 가는 줄도 있음[기] → 우리 한 꼴로 맞춤. +- 옵션 적는 꼴 = `닫기(C)·CLOSE` = 한국어 이름(키 글자)·영문 키워드(사건 `OPTION.CLOSE` 의 키도 이 영문) · 「뺌:」 뒤 = 오토캐드엔 있고 이번엔 안 넣음(`absent` 에 둠). +- 기억 = 앞에 쓴 값이 다음 `<기본값>`(세션) · 괄호 안 = 열쇠 이름(오토캐드 변수) · 처음 값. +- 끝난 뒤 — 한 번 = 하나 뒤 명령 끝(쉬는 상태 · Space · Enter · 우클릭 = 같은 명령 다시) · 되풀이 = 빈 Enter · Esc 까지 그 물음 되풀이. +- 지금 = K2(부록4 E 표) 우리 물음 · 동작. + +### 1-1. 그리기 · 글자 · 해치(나) + +| 명령 | 물음 차례 | 옵션(넣음 · 뺌) | 빈 Enter · 기본값 | Esc · 끝난 뒤 | 지금과 다른 곳 | +|---|---|---|---|---|---| +| LINE(L) | 「첫 번째 점 지정:」 → 「다음 점 지정 또는 [명령 취소(U)]:」 → 둘째 토막부터 「다음 점 지정 또는 [닫기(C)/명령 취소(U)]:」 되풀이 [이] | 명령 취소(U)·UNDO = 마지막 토막 지움(되돌림 한 칸 `undo()` · 새 되돌림 안 쌓음) · 닫기(C)·CLOSE = 첫 점으로 잇고 끝 | 첫 점 = 마지막 선 · 폴리선 · 호 끝점에서 시작[공](기억 LINE.END) · 다음 점 = 끝 | 끝(그린 토막 남음) · 되풀이 | Enter · Esc = 새 첫 점(안 끝남) · 첫 토막에도 [닫기] 보임 · 첫 점 Enter 없음 · 글 「첫 점 지정 (x,y):」 | +| PLINE(PL) | 「시작점 지정:」 → 「다음 점 지정 또는 [호(A)/길이(L)/명령 취소(U)]:」 → 둘째 토막부터 [호(A)/닫기(C)/길이(L)/명령 취소(U)] 되풀이 · 호 모드 「호의 끝점 지정 또는 [닫기(CL)/선(L)/명령 취소(U)]:」 되풀이 [이] | 호(A)·ARC = 접선 호 모드(첫 토막이 호면 접선 동쪽 0° · 앞 토막 있으면 그 끝 방향) · 닫기(C)·CLOSE · 길이(L)·LENGTH → 「선의 길이 지정:」[옮](앞 토막 방향 · 호 뒤면 접선) · 명령 취소(U)·UNDO = 마지막 토막 · 호 모드 닫기(CL)·CLOSE · 선(L)·LINE · 명령 취소(U) · 뺌: 반폭(H) · 폭(W) · 호 각도(A) · 중심(CE) · 방향(D) · 반지름(R) · 두 번째 점(S) | 시작점 = 마지막 끝점에서[공](LINE.END) · 다음 점 = 끝(확정) | 끝(그린 데까지 폴리선 남김[기]) · 되풀이 | 옵션 0 · C = 원으로 바뀌며 버림 · U = UNDO · Esc = 다 버리고 다시 · 끝나면 다시 시작점 | +| RECTANG(REC) | 「첫 번째 구석점 지정:」(오토캐드 = 「…또는 [모따기(C)/고도(E)/모깎기(F)/두께(T)/폭(W)]:」[공]) → 「다른 구석점 지정 또는 [치수(D)]:」 [이] | 치수(D)·DIMENSIONS → 「직사각형의 길이 지정 <10.0000>:」 → 「직사각형의 폭 지정 <10.0000>:」[옮] → 다시 「다른 구석점 지정 또는 [치수(D)]:」(누른 쪽 사분면으로 놓음) · 뺌: 모따기(C) · 고도(E) · 모깎기(F) · 두께(T) · 폭(W) · 면적(A) · 회전(R) | 길이 · 폭 기억(RECT.L · RECT.W · 10) | 끝 · 한 번 | 하나 뒤 다시 첫 구석 · 첫 구석 Esc 없음(N3) · D 없음 | +| CIRCLE(C) | 「원에 대한 중심점 지정 또는 [3점(3P)/2점(2P)]:」 → 「원의 반지름 지정 또는 [지름(D)] :」 [이 · 첫 줄 한국어 꼴 기] | 3점(3P)·3P → 「원 위의 첫 번째 점 지정:」 → 「…두 번째 점 지정:」 → 「…세 번째 점 지정:」 · 2점(2P)·2P → 「원 지름의 첫 번째 끝점 지정:」 → 「…두 번째 끝점 지정:」[옮] · 지름(D)·DIAMETER → 「원의 지름 지정 <2r>:」 · 뺌: Ttr - 접선 접선 반지름(T) | 반지름 기억(CIRCLERAD · 처음 없음 → `<>` 안 그림 · Enter = 아무 일 없음) · 누른 점 반지름도 기억 | 끝 · 한 번 | 하나 뒤 다시 중심 · 3P · 2P · D · 기억 없음 · 글 「원 위의 점 지정 또는 반지름 입력:」 | +| ARC(A) | 「호의 시작점 지정:」 → 「호의 두 번째 점 지정 또는 [중심(C)/끝(E)]:」 → 「호의 끝점 지정:」 [이] | 중심(C)·CENTER → 「호의 중심점 지정:」 → 「호의 끝점 지정 또는 [각도(A)]:」 · 각도(A)·ANGLE → 「사이각 지정:」(+ 반시계) · 끝(E)·END → 「호의 끝점 지정:」 → 「호의 중심점 지정 또는 [반지름(R)]:」 · 반지름(R)·RADIUS → 「호의 반지름 지정:」 · 뺌: 시작 단계 중심(C) · 현의 길이(L) · 방향(D) · E 뒤 각도(A) | 시작점 Enter(마지막 선 · 호 끝 접선 이음[공]) = 뺌 → 아무 일 없음 | 끝 · 한 번 | 세 점만 · 하나 뒤 다시 | +| POLYGON(POL) | 「변의 수 입력 <4>:」 → 「폴리곤의 중심 지정 또는 [모서리(E)]:」 → 「옵션 입력 [원에 내접(I)/원에 외접(C)] :」 → 「원의 반지름 지정:」 [이] | 모서리(E)·EDGE → 「모서리의 첫 번째 끝점 지정:」 → 「모서리의 두 번째 끝점 지정:」[옮] · 원에 내접(I)·INSCRIBED · 원에 외접(C)·CIRCUMSCRIBED | 변 수 기억(POLYSIDES · 4 · 3~1024 정수) · 내접/외접 기억(POLYGON.IC · I) · 반지름 숫자 = 아래 변 수평 · 점 = 크기 + 돌림 | 끝 · 한 번 | 면 수 <6> 상수 · 내접만 · E 없음 · 글 「면의 수를 입력하십시오 <6>.」 | +| ELLIPSE(EL) | 「타원의 축 끝점 지정 또는 [중심(C)]:」 → 「축의 다른 끝점 지정:」 → 「다른 축으로 거리를 지정 또는 [회전(R)]:」 [이] | 중심(C)·CENTER → 「타원의 중심 지정:」 → 「축의 끝점 지정:」 → 같은 거리 물음 · 회전(R)·ROTATION → 「주축 둘레로 회전 지정:」[옮] · 뺌: 호(A) | — | 끝 · 한 번 | 중심 방식만 · 셋째 숫자 ≠ 단축 반지름(N10) · 하나 뒤 다시 | +| SPLINE(SPL) | 「첫 번째 점 지정:」 → 「다음 점 입력:」 → 「다음 점 입력 또는 [명령 취소(U)]:」 → 셋째 점부터 [명령 취소(U)/닫기(C)] 되풀이 [이] | 명령 취소(U)·UNDO = 마지막 점 · 닫기(C)·CLOSE = 닫고 끝 · 뺌: 방법(M) · 매듭(K) · 객체(O) · 시작/끝 접촉부(T) · 공차(L) | 다음 점 = 끝(확정) | 버림[기] · 끝 | 점만 · 끝나면 다시 | +| XLINE(XL) | 「점 지정 또는 [수평(H)/수직(V)/각도(A)]:」 → 「통과점을 지정:」 되풀이 [이] | 수평(H)·HOR · 수직(V)·VER → 「통과점을 지정:」 되풀이 · 각도(A)·ANG → 「X선의 각도 입력 <0>:」 → 「통과점을 지정:」 되풀이 · 뺌: 이등분(B) · 간격띄우기(O) · 각도 참조(R) | 통과점 = 끝 · 각도 기억(XLINE.ANGLE · 0) | 끝(그은 선 남음) · 되풀이 | 한 줄 뒤 처음부터 · 옵션 없음 | +| TEXT(DT) | 「문자의 시작점 지정:」 → 「높이 지정 :」 → 「문자의 회전 각도 지정 <0>:」 → 글 줄(커서 곁 글 칸 · Enter = 아래 새 줄 · 빈 줄 Enter = 끝) [이] | 뺌: 자리맞추기(J) · 스타일(S) | 시작점 = 직전 명령도 TEXT 면 앞 글 아래 · 높이 · 각 안 물음[공] · 높이 기억(TEXTSIZE · 처음 지금 글자 크기) · 각 기억(TEXT.ANGLE · 0) | 끝(마친 줄 남음 · 치던 줄 버림[기]) | 시작점 → 한 줄 · 높이 · 각 안 물음 · 줄 이어 치기 없음 | +| MTEXT(T · MT) | 「첫 번째 구석 지정:」 → 「반대 구석 지정 또는 [높이(H)/폭(W)]:」 → 글(편집기 대신 커서 곁 글 칸 · `\P` 줄바꿈 그대로) [이] | 높이(H)·HEIGHT → 「높이 지정 :」 → 다시 반대 구석 · 폭(W)·WIDTH → 「폭 지정:」(점 · 숫자) → 글 · 뺌: 자리맞추기(J) · 행 간격(L) · 회전(R) · 스타일(S) · 열(C) | 높이 = TEXTSIZE | 끝(저장 없이[공]) | 반대 구석 없음 | +| HATCH(H · BH) | 「내부 점 선택 또는 [객체 선택(S)/명령 취소(U)]:」 되풀이 [이] | 내부 점 = 그 점을 품은 닫힌 객체(닫힌 폴리선 · 사각형 · 원) 중 가장 작은 하나 · 객체 선택(S)·SELECT → 「객체 선택:」(지금 방식) · 명령 취소(U)·UNDO = 마지막 경계 · 뺌: 경계 제거(B) · 설정(T) · 그리기(W) | 빈 Enter = 확정 · 끝 | 끝(안 만듦) | 경계 객체 고름만 | + +- POINT · DONUT · RAY 등 31 밖 그리기 = 엔진 바뀜만 따름(한 번 뒤 끝) · 오토캐드 되풀이 명령은 나가 `loopFrom` 둠 — DONUT(중심 되풀이) · RAY(통과점 되풀이) · MLINE · WIPEOUT(되풀이 단계 그대로 · Enter 확정 뒤 끝). + +### 1-2. 고치기 · 재기 · 치수(다) + +| 명령 | 물음 차례 | 옵션(넣음 · 뺌) | 빈 Enter · 기본값 | Esc · 끝난 뒤 | 지금과 다른 곳 | +|---|---|---|---|---|---| +| OFFSET(O) | 「간격띄우기 거리 지정 또는 [통과점(T)/지우기(E)] <통과점>:」 → 「간격띄우기할 객체 선택 또는 [종료(E)/명령 취소(U)] <종료>:」 → 「간격띄우기할 면의 점 지정 또는 [종료(E)/다중(M)/명령 취소(U)] <종료>:」 → 객체 선택으로 되풀이 [이] | 통과점(T)·THROUGH → 객체 → 「통과점 지정 또는 [종료(E)/다중(M)/명령 취소(U)] <종료>:」 · 지우기(E)·ERASE → 「간격띄우기 후 원본 객체를 지우시겠습니까? [예(Y)/아니오(N)] <아니오>:」[옮] · 종료(E)·EXIT · 다중(M)·MULTIPLE = 같은 객체로 쪽 되풀이 · 명령 취소(U)·UNDO = 이 명령 안 마지막 하나 · 뺌: 도면층(L) · 두 점으로 거리 | 거리 기억(OFFSETDIST · 처음 <통과점> = 숫자 대신 통과점 방식) · 객체 · 면 = <종료> | 끝(만든 것 남음) · 되풀이 | 거리 먼저 <1> 상수 · 한 번 뒤 끝 · 옵션 없음 | +| FILLET(F) | 「첫 번째 객체 선택 또는 [명령 취소(U)/반지름(R)/자르기(T)/다중(M)]:」 → 「두 번째 객체 선택 또는 Shift 키를 누른 채 선택하여 구석 적용 또는 [반지름(R)]:」 [이 · Shift 문구 공] | 반지름(R)·RADIUS → 「모깎기 반지름 지정 :」[옮] → 같은 물음 · 자르기(T)·TRIM → 「자르기 모드 옵션 입력 [자르기(T)/자르지 않기(N)] <자르기>:」[이] · 다중(M)·MULTIPLE = 쌍 되풀이 · 명령 취소(U)·UNDO = 다중 중 앞 쌍 · Shift+둘째 누름 = 반지름 0 · 뺌: 폴리선(P) | 반지름 기억(FILLETRAD · 0) · 자르기 기억(TRIMMODE · 자르기) | 끝 · 한 번(다중이면 되풀이 · 빈 Enter 끝) | 반지름 먼저 <0> 상수 · 옵션 · Shift 없음 | +| CHAMFER(CHA) | 「첫 번째 선 선택 또는 [명령 취소(U)/거리(D)/자르기(T)/다중(M)]:」 → 「두 번째 선 선택 또는 Shift 키를 누른 채 선택하여 구석 적용 또는 [거리(D)]:」 [이] | 거리(D)·DISTANCE → 「첫 번째 모따기 거리 지정 :」 → 「두 번째 모따기 거리 지정 :」[옮] · 자르기(T) · 다중(M) · 명령 취소(U) · Shift = 거리 0 · 뺌: 폴리선(P) · 각도(A) · 방법(E) | 거리 기억(CHAMFERA · CHAMFERB · 0 · 둘째 기본 = 첫째) | 끝 · 한 번(다중 되풀이) | 거리 둘 먼저 <1> | +| TRIM(TR) | 「자를 객체 선택 또는 Shift 키를 누른 채 선택하여 연장 또는 [지우기(R)]:」 되풀이 [이 · 빠른 모드] | 지우기(R)·ERASE → 「지울 객체 선택:」(고르기 · Enter = 지우고 돌아옴)[옮] · 명령 취소(U)·UNDO(숨김) · Shift+누름 = 연장 · 뺌: 절단 모서리(T) · 걸치기(C) · 모드(O) · 투영(P) · 끌기 경로 | 빈 Enter = 끝 | 끝(자른 것 남음) · 되풀이 | Enter · Esc 처리 없음(N3) · 멀리 눌러도 자름(N9) · Shift 없음 | +| EXTEND(EX) | 「연장할 객체 선택 또는 Shift 키를 누른 채 선택하여 자르기:」 되풀이 [이 · 빠른 모드] | 누른 쪽 끝을 가장 가까운 경계(모든 객체)까지 · Shift+누름 = 자르기 · 명령 취소(U)(숨김) · 뺌: 경계 모서리(B) · 걸치기(C) · 모드(O) · 투영(P) | 빈 Enter = 끝 | 끝 · 되풀이 | 경계 먼저 고름 → 선 하나 뒤 끝 | +| BREAK(BR) | 「객체 선택:」(누른 점 = 첫 끊기점) → 「두 번째 끊기점 지정 또는 [첫 번째 점(F)]:」 [이] | 첫 번째 점(F)·FIRST → 「첫 번째 끊기점 지정:」 → 「두 번째 끊기점 지정:」[옮] · `@` = 같은 점(틈 없이 둘로) | — | 끝 · 한 번 | 객체 → 첫 점 → 둘째 점(세 번 누름) | +| JOIN(J) | 「한 번에 결합할 원본 객체 또는 여러 객체 선택:」[공](고르기 물음) | — | 빈 Enter = 결합 | 끝 · 한 번 | 글만 다름 | +| LENGTHEN(LEN) | 「측정할 객체 또는 [증분(DE)/퍼센트(P)/전체(T)] 선택 <전체(T)>:」 → (값) → 「변경할 객체 선택 또는 [명령 취소(U)]:」 되풀이 [이] | 객체 누름 = 「현재 길이: 12.3456」 알림 뒤 같은 물음 · 증분(DE)·DELTA → 「증분 길이 입력 <0.0000>:」 · 퍼센트(P)·PERCENT → 「퍼센트 길이 입력 <100.0000>:」 · 전체(T)·TOTAL → 「전체 길이 지정 <1.0000>:」[옮] · 명령 취소(U) · 뺌: 동적(DY) · 각도(A) | 첫 물음 = 마지막 방식(LENGTHEN.MODE · 전체) · 값 = 방식별 기억 | 끝 · 되풀이 | 객체 먼저 → 증분 <10> · 한 번 뒤 끝 · 선만 | +| DIVIDE(DIV) | 「등분할 객체 선택:」 → 「세그먼트 수 입력:」 [이] | 뺌: 블록(B) | 기본 없음(2~32767 정수) | 끝 · 한 번 | <4> 기본 · 정수 검사 없음 | +| MEASURE(ME) | 「길이 분할 객체 선택:」 → 「세그먼트의 길이 지정:」 [이] | 누른 자리 가까운 끝부터[공] · 뺌: 블록(B) | 기본 없음(> 0) | 끝 · 한 번 | <10> 기본 | +| DIST(DI) | 「첫 번째 점 지정:」 → 「두 번째 점 지정:」 → 기록 · 알림 「거리 = d, XY 평면에서의 각도 = a, XY 평면으로부터의 각도 = 0 / X 증분 = dx, Y 증분 = dy, Z 증분 = 0.0000」[옮] | 뺌: 다중 점(M) | — | 끝 · 한 번 | 결과 글 꼴 · 끝나면 다시 | +| DIMLINEAR(DLI) · DIMALIGNED(DAL) | 「첫 번째 치수보조선 원점 지정 또는 <객체 선택>:」 → 「두 번째 치수보조선 원점 지정:」 → 「치수선의 위치 지정 또는 [문자(T)/수평(H)/수직(V)]:」(정렬 = [문자(T)]) → 기록 「치수 문자 = 100」 [이] | 첫 물음 빈 Enter → 「치수 기입할 객체 선택:」(선 · 폴리선 토막 양 끝) · 문자(T)·TEXT → 「치수 문자 입력 <100>:」(`<>` = 잰 값)[옮] → 같은 물음 · 수평(H)·HORIZONTAL · 수직(V)·VERTICAL = 그 방향 고정 → 같은 물음 · 뺌: 여러 줄 문자(M) · 각도(A) · 회전됨(R) | 첫 물음 <객체 선택> | 끝 · 한 번 | 점만 · 객체 고름 · T · H · V 없음 · 하나 뒤 다시 | +| DIMCONTINUE(DCO) · DIMBASELINE(DBA) | 「두 번째 치수보조선 원점 지정 또는 [명령 취소(U)]:」 되풀이 [옮] | 명령 취소(U) = 이 명령 안 마지막 치수 · 뺌: 선택(S) | 빈 Enter = 끝 | 끝 · 되풀이 | 한 번 | +| DIMRADIUS(DRA) · DIMDIAMETER(DDI) | 「호 또는 원 선택:」 → 「치수선의 위치 지정 또는 [문자(T)]:」 [이] | 문자(T)·TEXT · 뺌: 여러 줄 문자(M) · 각도(A) | — | 끝 · 한 번 | 글 | + +- DIM · DIMANGULAR · DIMORDINATE · DIMARC · QDIM = 이번엔 물음 글 · 끝 · Esc 규칙만(옵션 뺌). + +### 1-3. 옮기기 무리(라) + +| 명령 | 물음 차례 | 옵션(넣음 · 뺌) | 빈 Enter · 기본값 | Esc · 끝난 뒤 | 지금과 다른 곳 | +|---|---|---|---|---|---| +| MOVE(M) | 「객체 선택:」(고른 것 있으면 건너뜀) → 「기준점 지정 또는 [변위(D)] <변위>:」 → 「두 번째 점 지정 또는 <첫 번째 점을 변위로 사용>:」 [이 · 둘째 줄 공] | 변위(D)·DISPLACEMENT → 「변위 지정 <0.0000, 0.0000>:」[옮](좌표 = 변위) | 기준점 = <변위> · 두 번째 점 = 첫 점 좌표가 변위[공] · 변위 기억(MOVE.DISP) | 제자리 · 끝 · 한 번 | D · Enter 변위 없음 · Esc 안 끝남(N2) · 한 번 뒤 고르기로 · 딴 명령 = 사라짐(N1) | +| COPY(CO · CP) | 「객체 선택:」 → 「기준점 지정 또는 [변위(D)/모드(O)/다중(M)] <변위>:」[공] → 「두 번째 점 지정 또는 [배열(A)] <첫 번째 점을 변위로 사용>:」[공] → 첫 사본 뒤 「두 번째 점 지정 또는 [배열(A)/종료(E)/명령 취소(U)] <종료>:」 되풀이[옮] | 모드(O)·MODE → 「복사 모드 옵션 입력 [단일(S)/다중(M)] <다중>:」[이] · 다중(M)·MULTIPLE = 이번만 되풀이(모드 단일 때만 보임) · 배열(A)·ARRAY → 「배열할 항목 수 입력:」 → 「두 번째 점 지정 또는 [맞춤(F)]:」[이] · 종료(E)·EXIT · 명령 취소(U)·UNDO | 첫 둘째 점 = 변위 · 다음 = <종료> · 모드 기억(COPYMODE · 다중) | 끝(만든 사본 남음) · 되풀이 | Enter 로 못 끝냄 · D · O · A · U 없음 | +| ROTATE(RO) | 「객체 선택:」 → 「기준점 지정:」 → 「회전 각도 지정 또는 [복사(C)/참조(R)] <0>:」 [이] | 누름 = 기준점 → 누른 점 각(한 번) · 숫자 = 반시계 + · 복사(C)·COPY → 기록 「선택한 객체의 사본을 회전함.」(오토캐드 글 [기] · 명사형으로) → 같은 물음 · 참조(R)·REFERENCE → 「참조 각도를 지정 <0>:」(숫자 · 두 점 · `@` = 기준점) → 「새 각도 지정 또는 [점(P)] <0>:」[옮] · 점(P)·POINTS → 두 점 | 각 기억(ROTATE.ANGLE · 0) | 제자리 · 끝 · 한 번 | 누름 = 기준선 끝 → 방향 점(두 번 · 차례 다름) · C · R 없음 | +| SCALE(SC) | 「객체 선택:」 → 「기준점 지정:」 → 「축척 비율 지정 또는 [복사(C)/참조(R)] <1.0000>:」 [이] | 누름 = 기준점~점 거리 · 숫자 > 0 · 복사(C) · 참조(R) → 「참조 길이 지정 <1.0000>:」(숫자 · 두 점) → 「새 길이 지정 또는 [점(P)] <1.0000>:」[옮] | 비율 기억(SCALE.FACTOR · 1) | 제자리 · 끝 · 한 번 | 누름 = 기준선 끝 → 새 길이 끝(두 번) · C 없음 · 0 이하 조용히 버림 | +| STRETCH(S) | 기록 「걸치기 윈도우 또는 걸치기 폴리곤만큼 신축할 객체 선택...」 → 「객체 선택:」 → 「기준점 지정 또는 [변위(D)] <변위>:」 → 「두 번째 점 지정 또는 <첫 번째 점을 변위로 사용>:」 [이] | 변위(D) = MOVE 와 같음 · 걸침 창 안 꼭짓점만 옮김 · 창 안에 통째 든 것 = 이동[공 뜻] | MOVE 와 같음(STRETCH.DISP) | 제자리 · 끝 · 한 번 | 선 = 기준점 가까운 끝만 · 그 밖 통째 | +| MIRROR(MI) | 「객체 선택:」 → 「대칭선의 첫 번째 점 지정:」 → 「대칭선의 두 번째 점 지정:」 → 「원본 객체를 지우시겠습니까? [예(Y)/아니오(N)] <아니오>:」 [이] | 예(Y)·YES · 아니오(N)·NO | <아니오>(늘) | 끝(안 만듦) · 한 번 | Y/N 물음 없음 | +| ARRAY(AR) | 「객체 선택:」 → 「배열 유형 입력 [직사각형(R)/원형(PO)] <직사각형>:」 [이] → 직사각형: 미리보기 4열 × 3행 → 「그립을 선택하여 배열을 편집하거나 [개수(COU)/간격두기(S)/열(COL)/행(R)/종료(X)] <종료>:」 되풀이 · 원형: 「배열의 중심점 지정:」 → 미리보기 6개 360° → 「그립을 선택하여 배열을 편집하거나 [항목(I)/사이의 각도(A)/채울 각도(F)/항목 회전(ROT)/종료(X)] <종료>:」 되풀이 [이] | 개수(COU)·COUNT → 「열 수 입력 <4>:」 → 「행 수 입력 <3>:」 · 간격두기(S)·SPACING → 「열 사이의 거리 지정 :」 → 「행 사이의 거리 지정 :」 · 열(COL)·COLUMNS → 열 수 → 열 거리 · 행(R)·ROWS → 행 수 → 행 거리 · 항목(I)·ITEMS → 「배열의 항목 수 입력 <6>:」 · 사이의 각도(A)·ANGLE → 「항목 사이의 각도 지정 <60>:」 · 채울 각도(F)·FILL → 「채울 각도 지정(+=ccw, -=cw) <360>:」 · 항목 회전(ROT)·ROTATE → 「배열된 항목을 회전하시겠습니까? [예(Y)/아니오(N)] <예>:」[옮] · 종료(X)·EXIT · 뺌: 경로(PA) · 연관(AS) · 기준점(B) · 수준(L) · 회전축(A) · 표현식 | 유형 기억(ARRAYTYPE · 직사각형) · 처음 간격 = 고른 것 범위 폭 · 높이의 1.5 배[기] · 편집 = <종료> → 만듦 | 안 만듦[기] · 끝 · 한 번 | [직선(L)/원형(R)](R = 원형 · 오토캐드 R = 직사각형) · 간격 두 점 · 한 칸 각 · Esc 일부 없음 · `x,y` 면 복사 0(N17) | + +- 고른 것 소비 때 `rememberSelection(ids)`(고르기 이전(P) 열쇠) · 고른 것 없이 고르기 Enter = 명령 끝(오토캐드 같음). + +### 1-4. 고르기 · 그립 · 보기 · 점 물음 보조(마 · 가-2) + +| 물음 | 차례 | 옵션 · 손짓 | 빈 Enter | Esc | 지금과 다른 곳 | +|---|---|---|---|---|---| +| 고르기(모든 명령) | 「객체 선택:」 되풀이 [공] | 숨은 옵션 전체(ALL)·ALL · 최종(L)·LAST · 이전(P)·PREVIOUS · 누름 = 더함 · Shift+누름 = 뺌 · 빈 곳 = 창(왼→오) · 걸침(오→왼) · 클릭-클릭 · 끌기 둘 다 · 「1개를 찾음, 총 3개」 알림 · 뺌: W · C · F · WP · CP · BOX · G · A · R · M · U · AU · SI | 고르기 끝(없으면 명령 끝) | 명령 끝 | 누름 = 바꿈 · 명령 안 끌기 상자 ○ · P = 화면 이동 안내 · L = LINE | +| 그립 | 그립 누름(뜨거움) → 「** 신축 ** 신축점 지정 또는 [종료(X)]:」[기] | 점 · 좌표 · 거리 숫자 · Tab 칸 · 종료(X)·EXIT · 뺌: 기준점(B) · 복사(C) · 명령 취소(U) · Enter/Space 모드 돌림 | — | 그립 놓음 | 누른 채 끌기만 · 안 끌고 떼도 적용(H3) · 숫자 무시(N8) | +| ZOOM(Z) | 「윈도우 구석 지정, 축척 비율 (nX 또는 nXP) 입력 또는 [전체(A)/범위(E)/이전(P)/윈도우(W)] <실시간>:」[이 · 기] | 전체(A)·ALL = 범위 · 범위(E)·EXTENTS · 이전(P)·PREVIOUS = 앞 화면(ZOOM · 휠 두 번 · ⛶ 로 바꾼 것만 기억) · 윈도우(W)·WINDOW → 「첫 번째 구석 지정:」 → 「반대 구석 지정:」 · 점 누름 = 윈도우 첫 구석 · `2X` = 지금 배율 × 2 · 뺌: 중심(C) · 동적(D) · 축척(S) · 객체(O) · 실시간 | <실시간> = 「실시간 줌은 휠로」 알림 · 닫음 | 닫음 | Z = 바로 범위(Z E 손버릇 = 범위 뒤 E → ERASE 로 이어짐) | +| 점 물음 보조(모든 점 물음 · 가-2) | 스냅 이름 → 「끝점(END) 한 번 — 점 지정」(지금 꼴) · FROM → 「기준점:」 → 「<간격띄우기>:」 · M2P · MTP → 「중간점의 첫 번째 점:」 → 「중간점의 두 번째 점:」[옮] | 스냅 이름 = 앞 세 글자~영문 전체(END · ENDP · ENDPOINT …) · 넣음: END MID CEN QUA INT PER NEA NOD NON(NONE) · 「이 CAD 에 없는 스냅」 안내: TAN EXT INS APP PAR GCE · `@` = 마지막 점(LASTPOINT) · 첫 점 `@dx,dy` = 마지막 점 기준 · `#x,y` · `*x,y` = 절대 | — | 보조 풂 + 명령 끝 | 일곱만 · EXT · INS = 명령이 돎(N14) · FROM · M2P · `@` · `#` 버림 | + +## 2. 옵션 틀 약속(다른 창이 이대로 고침) + +### 2-1. 흐름 + +키 · 마우스 → `InputController`(키 쪽 = 가 · 마우스 쪽 = 마) → `prompt-input.routeTyped`(사다리 한 곳) → 사건(`MOUSE_CLICK` · `ABSOLUTE_POINT_INPUT` · `RELATIVE_POINT_INPUT` · `NUMBER_INPUT` · `TEXT_INPUT` · `OPTION.<키>` · `ENTER` · `ESC` · `DRAW`) → 도구 머신(상태 `meta` 에 물음 선언) → `END`(final) → `end-command.ts` 가 쉬는 상태로. + +### 2-2. 파일 · 내보내는 이름 + +| 파일 | 몫 | 내보냄 | +|---|---|---| +| `src/commands/prompt.ts`(새 · 가) | 물음 꼴 · 그림 · 옵션 맞추기 · 기억 · 공용 옵션 | `OptionSpec` · `DefaultSpec` · `PromptSpec` · `PromptLike` · `renderPrompt` · `matchOption` · `remember` · `recall` · `rememberSelection` · `previousSelection` · `rememberPoint` · `lastPoint` · `OPT` · `SELECT_OBJECTS` | +| `ic/prompt-input.ts`(새 · 가) | 친 글 사다리 · 거절 · 알림 줄 · 덧물음 · 쉼 판정 · 명령 끊기 | `currentPrompt` · `routeTyped` · `isIdle` · `cancelActiveCommand` · `undoCommand` · `redoCommand` · `notice` · `currentNotice` · `pushSubPrompt` · `subPrompt` · `closeSubPrompt` · `SubPrompt` · `SubResult` | +| `tl/factories/end-command.ts`(새 · 가) | `END` 받으면 정리 → 새 선택 도구(동기) | 없음(불러오면 갈고리 붙음 · `registry.ts` 가 불러옴) | +| `tl/tool.types.ts`(고침 · 가) | 사건 · 이름 | `OptionEvent` · `optionKey` · `END_STATE` · `StepKind` · `MouseClickEvent.snapped?` | +| `tl/factories/sequence-tool.ts`(고침 · 가) | 순서 엔진 v2 | 2-13 | +| `ic/input-history.ts`(새 · 가-2) | ↑↓ 명령 기록 · 물음별 최근 값 20 | `recallInput(dir, text)` · `pushInput(text)` | +| `ic/point-modifiers.ts`(새 · 가-2) | FROM · M2P · `@` · `#` · `*` · 스냅 이름 늘림 | `tryPointModifier(T)` | +| `T/helpers/cad-drive.ts`(새 · 가) | 시험 공용 조작 | 6-2 | + +### 2-3. 물음 꼴(타입 · 가가 이대로 만듦) + +```ts +type XY = { x: number; y: number }; +interface OptionSpec { key: string; kw: string; en: string; label: string; hidden?: true } +type DefaultSpec = + | { kind: 'value'; remember: string; initial: number | XY | null; format?: 'len' | 'int' | 'ang' } + | { kind: 'option'; key: string; label?: string } // <변위> · <전체(T)>(label 로 바꿈) + | { kind: 'enter'; label: string }; // <첫 번째 점을 변위로 사용> · <종료> · <객체 선택> +interface PromptSpec { + text: string; // '다음 점 지정' — ' 또는 [..]' · ' <..>' · ':' 는 renderPrompt 가 붙임 + options?: OptionSpec[]; // 넣은 옵션(숨김 포함) + absent?: OptionSpec[]; // 오토캐드엔 있고 우리엔 없음 — 치면 안내 · 안 그림 + default?: DefaultSpec; + number?: { int?: true; min?: number; max?: number; positive?: true }; + reject?: string; // 거절 글 바꿈(없으면 2-10) + history?: string; // ↑↓ 최근 값 묶음(없으면 text) +} +type PromptLike = PromptSpec | ((context: any) => PromptSpec); // 지금 상태 context 로 그림 +// 상태 meta 꼴: { kind?: StepKind; prompt?: PromptLike; instructions?: string /* 옛 꼴 */ } +// StepKind = 'point' | 'number' | 'text' | 'entity' | 'selection' | 'keyword' +interface OptionEvent extends EventObject { type: `OPTION.${string}` } // StateEvent 에 더함 +``` + +- 그림 = `글` + (보이는 옵션 있으면 ` 또는 [이름(키)/…]`) + (기본 있으면 ` <기본>`) + `:` · 「두 번째 객체 선택 또는 Shift 키를 누른 채 선택하여 구석 적용」 같은 덧글은 `text` 에 그대로. +- 기본 그림 — 길이 = 소수 넷(LUPREC 4 · `10.0000`) · int = 정수 · ang = 끝 0 뗌 · XY = `x, y` 소수 넷 · 값이 null = `<>` 안 그림. +- 보이는 곳 — 커서 곁 입력 상자 첫 줄 · 명령행(B07) · B06 상세 · M02 는 커서 곁만(같은 코드). + +### 2-4. 옵션 맞추기(`matchOption`) + +- 친 글 → 앞뒤 빈칸 뗌 · 대문자 · 앞 `_` 하나 뗌(영문 공용 키워드 꼴). +- 맞음 = 글 = kw · 글 = en · 또는 en 이 글로 시작하고 글이 kw 로 시작(kw 보다 짧으면 안 맞음 — 공식 initget 「LType = LT · LTY · LTYP · LTYPE · L 은 모자람」). +- 둘 이상 맞으면 kw 딱 맞는 것 · 숨은 옵션도 맞춤 · `absent` 맞으면 「「고도(E)」 옵션은 이 CAD 에 없음.」. +- 한 물음 안 kw 겹침 없음(계약 시험) · 점 물음에선 스냅 이름 · FROM · M2P 를 옵션보다 먼저 봄(PLINE 호 `CE` 대 스냅 `CEN` — 오토캐드도 CEN = 스냅). +- 대소문자 무관은 이 한 곳 — 캔버스 대문자 바꿈(`input-controller.ts:481`)은 보이는 글만 · 명령행 칸 소문자(N6)도 같은 길. + +### 2-5. 친 글 사다리(`routeTyped` · 한 곳) + +① 덧물음 있음 → 덧물음만(그 물음 꼴로 ④~⑦). +② 글 빔 → 동적 입력 길이 잠금 + 점 물음 = 그 길이 점(지금) → 물음 기본(값 숫자 = `NUMBER_INPUT` · 값 XY = `ABSOLUTE_POINT_INPUT` · 옵션 = `OPTION.키` · enter = `ENTER`) → 쉼 = 앞 명령 반복 → 그 밖 = `ENTER`. +③ 글 물음(`kind: 'text'`) → `TEXT_INPUT`(원문 그대로) · 그 밖 물음에서 `'` 로 시작하면 바로 ⑧. +④ 점 물음 → 동적 각도 칸 숫자 · ` 내 별칭 > 기본 별칭 **딱 맞음만** `runCommand` · 아니면 「알 수 없는 명령 「R」.」(N18 — `registry.ts:132` 앞글자 첫 후보 줄 뺌 · 후보는 목록 · 2-12 고름). +⑩ 거절 → 2-10 글을 알림 줄 · 기록 줄에 · 도구엔 사건 안 감 · 친 글 지움. + +- 쉼 = 맨 위 도구가 SELECT 이고 `WAITING_FOR_FIRST_SELECT_POINT`(상자 그리는 중은 쉼 아님). +- 물음 종류 = 덧물음 → 쉼 → `meta.kind` → 없으면 `on` 으로 짐작(`ABSOLUTE_POINT_INPUT` = 점 · `NUMBER_INPUT` = 숫자 · `MOUSE_CLICK` 만 = 객체 · `OPTION.*` 만 = 키워드) · 평평한 상태만 읽음(지금 `ime-sink.ts:13-16` 과 같음). +- 한글 받개 초점 = `kind: 'text'` 일 때만(`activeToolWantsIme` 를 이것으로) — 숫자 · 키워드 물음은 영문 자리 글자. +- 옛 꼴 통과(전환 중 안 깨지게) — `prompt` 없는 상태: `meta.options`(글 배열)에 맞음 → `TEXT_INPUT(대문자)` · `TEXT_INPUT` 받고 `NUMBER_INPUT` 없는 상태 → `TEXT_INPUT(대문자)` · 나 · 다 · 라 전환 끝나면 가가 지움. + +### 2-6. 빈 Enter · Space · 우클릭 + +- 셋 다 ② 길 · Space 는 글 물음에서만 빈칸(지금 `ime-sink.ts:48-51` 그대로) · 우클릭 = `handleEnterKey(true)`(쉴 때도 반복 · 브레인 정함) · 친 글 있으면 그 글 제출. + +### 2-7. Esc(한 번 = 명령 끝) + +- 친 글 · 한글 조합 · 각도 잠금 · 동적 입력 잠금 · 한 번 스냅 · FROM · 덧물음 모두 풂 → 쉼 아니면 도구에 `ESC`(도구 = 정리 뒤 `END`) → 쉼이면 상자 그리는 중 = 상자 취소 · 아니면 고름 풂 · 기록 줄 「*취소*」. +- 지금(`input-controller.ts:485-500`)은 고른 것 있으면 고름만 · 친 글 있으면 글만 → 오토캐드처럼 한 번에. + +### 2-8. 명령 끝 · 명령 바꿈 · 되돌림 + +- 도구 머신이 `END`(`type: 'final'`)에 들면 `state.ts` 갈고리(`setActiveToolActor` 구독 `status === 'done'`) → `end-command.ts` 가 그림자 · 기준점 · 도움선 끔 · 덧물음 닫음 → 새 선택 도구(같은 틱) · 고른 것 풂은 도구 몫(소비한 도구가 풂 · QSELECT 같은 고르기 명령은 남김). +- 선택 도구 자체의 final(Enter)도 같은 갈고리 → `input-controller.ts:531-537` 지움 · `sequence-tool.ts:240-243` setTimeout 바꿔 끼우기 지움(같은 틱이 xstate 에서 탈 나면 queueMicrotask · 시험 `await tick()`). +- `runCommand`(도구형) = `cancelActiveCommand()`(쉼 아니면 `ESC` · 오토캐드 리본 `^C^C`) 뒤 켬 · 즉시형 = 도구 둠(N1 ②). +- `undoCommand` · `redoCommand` = 끊고(`ESC`) 뒤 되돌림 · 고름 풂(N1 ③ · N11 · 오토캐드 `^C^C_u`) · Ctrl+Z · Ctrl+Y · ↶ ↷ · 손가락 단추 모두 이 길. +- 쉴 때 `U` = UNDO(오토캐드 U 와 같음) · 명령 중 `U` = 그 물음 옵션일 때만(아니면 거절). + +### 2-9. 기본값 기억(세션 = 탭 수명 Map) + +- 쓰는 쪽 = 도구(`remember(열쇠, 값)` — 값이 정해진 때 · 누름으로 정한 값도) · 읽는 쪽 = 물음 그림 · 빈 Enter(`recall`). +- 엔진 숫자 단계 = `prompt.default.remember` 열쇠로 엔진이 저절로 기억. +- 열쇠 — OFFSETDIST · FILLETRAD · TRIMMODE · CHAMFERA · CHAMFERB · CIRCLERAD · POLYSIDES · POLYGON.IC · RECT.L · RECT.W · TEXTSIZE · TEXT.ANGLE · XLINE.ANGLE · LINE.END · ROTATE.ANGLE · ROTATE.REF · SCALE.FACTOR · SCALE.REF · MOVE.DISP · STRETCH.DISP · COPYMODE · ARRAYTYPE · ARRAY.COLS · ARRAY.ROWS · ARRAY.ITEMS · ARRAY.FILL · LENGTHEN.MODE · LENGTHEN.DE · LENGTHEN.P · LENGTHEN.T · LASTPOINT(`rememberPoint` — `state.setAngleGuideOriginPoint` 에 null 아닌 점이 오면) · 앞 고름(`rememberSelection`). + +### 2-10. 거절 · 안내 글(커서 곁 둘째 줄 3 초 또는 다음 키까지 + 기록 줄) + +| 때 | 글 | 근거 | +|---|---|---| +| 점 물음 | 점 또는 옵션 키워드를 요구함. | 오토캐드 한국어판(사용자 글 2차) · 브레인 정함 꼴 | +| 점 물음 + `number`(반지름 · 거리) | 숫자 거리, 두 번째 점 또는 옵션 키워드를 요구함. | [옮] | +| 각 물음(`reject` 로) | 유효한 숫자 각도, 두 번째 점 또는 옵션 키워드를 요구함. | [옮] | +| 숫자 · 정수 · 범위 · 양수 | 숫자 값을 요구함. · 정수 값을 요구함. · 3에서 1024 사이의 정수를 요구함. · 값은 양수이고 0이 아니어야 함. | [옮] | +| 키워드 물음 | 옵션 키워드가 잘못됨. | [옮] | +| 고르기 · 객체 물음 | *선택이 잘못됨* | [옮] | +| 없는 옵션 · 스냅 | 「고도(E)」 옵션은 이 CAD 에 없음. · 「TAN」 스냅은 이 CAD 에 없음. | 우리 | +| 쉴 때 | 알 수 없는 명령 「R」. | [옮] | +| 투명 | 「LINE」 명령은 투명하게 쓸 수 없음. | [옮] | + +- 명령 시작 때 「현재 설정: …」 기록 줄(FILLET · TRIM · OFFSET · COPY · TEXT …) = 넣으면 좋음 · 이번 필수 아님(`run-command.ts` 가 `logLine` 내보냄 · 거절 · 알림 기록도 이 길). + +### 2-11. 덧물음(sub-prompt) + +```ts +interface SubPrompt { prompt: PromptSpec; kind: StepKind; + onPoint?(p: Point): SubResult; onNumber?(n: number): SubResult; + onOption?(key: string): SubResult; onEnter?(): SubResult } +type SubResult = 'stay' | 'done' | { point: Point }; // {point} = 닫고 그 점을 원래 물음에 ABSOLUTE_POINT_INPUT 로 +``` + +- 쓰는 곳 — ZOOM(마 · `ic/zoom-prompt.ts`) · FROM · M2P(가-2) · 도구 머신을 안 바꿔 투명하게 됨(`'Z` 도 같은 길). +- 마우스 누름도 덧물음 먼저(`sendMouseClick` 이 `subPrompt()?.onPoint` 를 먼저 부름 · 마 몫 줄) · 그림 = 덧물음 줄이 도구 물음 대신 · Esc = 2-7(덧물음 닫고 명령도 끝 · 투명 ZOOM Esc 뒤 원래 명령 이어 감 여부[기]). + +### 2-12. ↑↓ · Tab · 후보(가-2) + +- ↑↓ — 쉼 = 친 명령 기록(H11 · 알림 줄 안 섞음) · 명령 중 = 그 물음(`history`) 최근 값 20 · 후보 목록 보일 땐 후보 고름(Enter = 고른 후보 · 안 고르면 친 글 그대로 ⑨) · Shift+↑↓ = 위아래 화면 이동(옛 동작 남김) · ← → = 화면 이동 그대로. +- Tab — 점 물음 = 동적 입력 칸(지금 그대로) · 쉼 + 친 글 = 후보 돌림(글을 후보 이름으로 · 오토캐드 명령행 Tab). +- 명령행(B07 `CommandLine.tsx`)도 같은 함수(`recallInput`) · Tab 첫 후보 채움(`:78-81`) → 돌림. + +### 2-13. 순서 엔진 v2(`sequence-tool.ts` · 가) + +| 칸 | 뜻 | +|---|---| +| `SequenceStep.kind` 에 `'keyword'` | 옵션만 받는 단계 · 고른 옵션 key 가 값(MIRROR 예/아니오 · 배열 유형) | +| `id?` | 값 이름 — `input.get(id)` · `input.has(id)` | +| `prompt?: PromptLike` | 새 꼴 물음(함수면 `SequenceInput` 받음) · 있으면 `instructions` 안 씀 | +| `when?(input)` | 거짓이면 그 단계 건너뜀(갈래 · `mode` 와 같이) | +| `next?` | 값을 받은 뒤 갈 단계 id(없으면 다음 칸 · 되돌아가기 = FILLET 반지름 뒤 첫 물음) | +| `options?: Record` | `OPTION.key` 처리 — `{ mode, next? }` = `input.mode` 세우고 다음(또는 next) · `{ run }` = 설정만 바꾸고 같은 물음 · `'undo'` = 마지막 값 하나 뺌 · `'close'` = `input.closed` 세우고 확정 · `'end'` = 확정 없이 끝 | +| `loopFrom?` | 확정 뒤 그 단계(id · 번호)부터 다시 · 앞 값 둠 — 없으면 확정 뒤 `END` | +| `escCommits?` | Esc 때 모은 값으로 확정 뒤 끝(PLINE) · 없으면 `onCancel` 뒤 끝(값 없어도 있어도 늘 끝 — N4) | +| `pickfirst?`(기본 참) | 첫 단계가 selection 이고 고른 것 있으면 그대로 받고 넘어감(오토캐드 PICKFIRST 1 · 지금 「Enter 한 번 더」) | +| `restart` · `chainFromLastPoint` | 옛 꼴 남김(`restart: true` = `loopFrom: 0`) | +| `SequenceInput` 더함 | `get(id)` · `has(id)` · `mode` · `closed` · 옛 `point(i)` 등 그대로 | + +- 엔진 몫 — 숫자 단계 TEXT_INPUT→숫자 길 지움(`:296-301` · N5) · selection 단계 meta = `{ kind: 'selection', prompt: step.prompt ?? SELECT_OBJECTS }` + `'OPTION.*'` 자식에 넘김 · 빈 고르기 = 끝 · 고른 것 받을 때 `rememberSelection` · 취소 때 selection 단계 있던 명령이면 고름 풂. +- 기본 바뀜이 103 순서 도구 모두에 미침 — 점만 받는 명령도 확정 뒤 끝 · 오토캐드 되풀이 명령만 `loopFrom` 둠(1 장 「되풀이」 줄 · 31 밖은 DONUT · RAY · MLINE · WIPEOUT · 지시선 · 표 놓기(이미 `restart: false`)). + +### 2-14. 손으로 짠 도구 규칙(LINE · CIRCLE · RECTANG · TRIM · MOVE · COPY · ROTATE · SCALE · ARRAY · 그립 · PEDIT) + +1. 물음 상태마다 `meta: { kind, prompt }` · 옛 `instructions` · `options: ['C','U']` 지움. +2. 옵션 처리 = `on: { 'OPTION.CLOSE': {...} }` — `prompt.options` key 마다 하나(숨김 포함 · 계약 시험이 맞댐) · 옵션 글을 `TEXT_INPUT` 으로 받지 않음. +3. `ESC` → 정리(그림자 · 원본 되살림) 뒤 `target: END_STATE` · 한 번 끝나는 명령은 마지막에 `END` · 되풀이는 그 상태로. +4. 고르기 상태 = `meta: { kind: 'selection', prompt: SELECT_OBJECTS }` + `'OPTION.*': { actions: sendTo(자식id, ({ event }) => event) }` + 고른 것 없이 끝 = `END` + 소비 때 `rememberSelection`. +5. 값 기억 = 정해진 때 `remember()` · 기본은 `prompt` 함수에서 `recall()`. +6. 물음 상태는 평평하게(겹 상태 안 · prompt-input 이 평평한 것만 읽음) · 파일 700 줄 넘으면 `*.helpers.ts` 로 가름. + +### 2-15. 고르기 물음 공용 · 앞 고름 + +- `SELECT_OBJECTS` = `{ text: '객체 선택', options: [전체(ALL)·ALL · 최종(L)·LAST · 이전(P)·PREVIOUS](모두 hidden) }` · 선언 = 가 · 처리(`OPTION.ALL` · `.LAST` · `.PREVIOUS`) = 마(선택 도구 첫 점 대기 상태 · 쉴 때 · 자식 둘 다). +- ALL = Ctrl+A 와 같은 목록(`getPickableEntities` · 오토캐드 ALL = 동결 · 잠금 층 뺌 · 끈 층 고름[기] — 다른 곳 표시만) · LAST = 집을 수 있는 마지막 객체 · PREVIOUS = `previousSelection()`. + +### 2-16. 계약 시험(`T/prompt-contract.test.ts` · 가) + +- `TOOL_STATE_MACHINES` 모든 평평한 상태 — `prompt.options` key 마다 `on['OPTION.key']` 또는 `on['OPTION.*']` 있음 · 같은 물음 kw 안 겹침 · `meta.kind` 있는 상태마다 `ESC` 있음 · `prompt` 와 옛 `instructions` 같이 없음 · 옵션 함수 꼴은 빈 context 로 불러 봄. + +### 2-17. 별칭(가) + +- 뺌: `SE`(SELECT · `commands.view.ts:35` · acad SE = DSETTINGS 없음) · 더함: `ED` → TEXTEDIT(`commands.annotate.ts:84` · acad 2018~) · `command-catalog.json` 거울 같이. +- 앞글자 실행 없앰으로 `R`(REDRAW) · `RA` · `W` · `LE`(QLEADER) · `UN` · `TO` · `TA` · `HI` · `IN` · `EXT` · `Q` · `LAY` · `DD` · `OS` · `DS` · `SN` · `V` · `PU` · `LTS` · `OP` = 「알 수 없는 명령」(acad 별칭이 가리키는 명령이 우리에 없음 → 비움). +- 우리 덧 별칭(`TEDIT` · `SAVE` · `UNHIDE` · `UNISOLATE` · `U` = UNDO)은 둠 — acad 와 안 부딪힘 · ☰ 내 별칭 우선 그대로. + +## 3. 흠 고침(이번 목표에 드는 것) + +| 흠 | 까닭(지금) | 자리 | 고침 방향 | 시험 | 창 | +|---|---|---|---|---|---| +| H1 | Ctrl · Alt+글자가 명령 글자로 쌓임(Ctrl+C → 원) | `ic/input-controller.ts:476-482` · `A00_Common/key_shortcut.ts:31-49` | 글자 쌓기에 Ctrl · Alt · ⌘ 거름 · CAD 쪽 `ic/input-shortcut.ts` 에 `cadCtrlCommand`(C=COPYCLIP · X=CUTCLIP · Shift+C=COPYBASE · Shift+V=PASTEBLOCK · 1=PROPERTIES) · 공용 key_shortcut 안 고침 | prompt-frame | 가 | +| H2 일부 | 보기 전용에서 그립 · Ctrl+그립 · Delete 먹음 | `tl/select-tool.helpers.ts:55-72` · `tl/select-tool.ts:73-76` · `src/helpers/draw.ts:56` | `isDrawingReadOnly() \|\| isViewOnly()` 면 그립 시작 · 꼭짓점 지움 · DELETE 안 함 · 그립 안 그림 | mouse-acad | 마 | +| H3 | 그립 누르기만 해도 밀림 | `ic/input-controller.ts:195-212` · `tl/grip-edit-tool.ts:64-112` | 누름이 그립 편집을 켠 것이고 안 끌고(4 px) 떼면 아무 일 없음(뜨거운 그립) · 다음 누름 = 놓음 · 끌어 떼면 놓음 · 손가락 톡도 같은 길 | mouse-acad | 마 | +| H4 | 상자 + Ctrl/Shift 엉뚱 · 상자 = 늘 새로 | `tl/select-tool.helpers.ts:114-169` | 상자 = 앞 고름 ∪ 상자 · Shift+상자 = 앞 고름 − 상자 | mouse-acad | 마 | +| H5 | 명령 중 누름 = 바꿈 · 끌기 상자 ○ · 스침 ○ | `tl/select-tool.helpers.ts:80-99` · `ic/input-controller.ts:283-293,311-330` | 누름 = 더함(쉼 · 명령 중 같음) · Shift = 뺌 · 고르기 단계도 누를 때 첫 점(`ic/selecting.ts` 의 `selectingActor()`) · 스침 강조 | mouse-acad | 마 | +| H7 | 점 찍는 명령 중 C · U = 원 · UNDO | `ic/ime-sink.ts:28-34` · `tl/factories/sequence-tool.ts:282-292` | 점 물음 중 명령 이름 거절(가) + PLINE · SPLINE 옵션 C · U(나) | prompt-frame · draw-acad | 가 · 나 | +| H8 | F3 끄면 직교 · 극 꺼짐 · 직교 강제 아님 | `ic/input-controller.ts:277-280` · `src/helpers/get-draw-guides.ts:37-48` · `src/helpers/get-point-from-event.ts:20-45` | 가이드 계산은 늘 · 객체 스냅점만 F3 · 직교(F8 · Shift 누른 동안 뒤집음)면 기준점에서 가로 · 세로로 늘 내림(커서 · 누름 · 거리 숫자 방향) · 스냅 점(`snapped`) · 친 좌표 · 각도 잠금은 안 덮음(오토캐드) | mouse-acad | 마 | +| H9 | 가이드 잡는 거리 = 도면 단위 15 | `src/helpers/get-draw-guides.ts:42` · `src/App.consts.ts:57` | `SNAP_ANGLE_DISTANCE / 화면 배율`(`maxSnapDistance` 와 같은 셈) | mouse-acad | 마 | +| H11 | ↑ 기록에 알림 줄 | `src/commands/run-command.ts:18-38` · `src/components/CommandLine.tsx:31-39` | 보이는 기록(log)과 친 명령 기록(input-history) 나눔 | prompt-frame | 가-2 | +| N1 | 이동 · 회전 · 축척 도중 딴 명령 → 고른 것 사라짐 · Ctrl+Z → 두 벌 | `tl/move-tool.ts:216-226` · `tl/rotate-tool.ts:241-259` · `tl/scale-tool.ts:247` · `src/commands/run-command.ts:44-50` · `ic/input-controller.ts:616-634` | ① 친 이름 거절 ② `runCommand` 가 `cancelActiveCommand` ③ 되돌림 = 끊고 뒤 ④ 도구 ESC = 원본 되살림 뒤 END | prompt-frame · transform-acad | 가 · 라 | +| N2 | 옮기기 무리 Esc 로 안 끝남 · 한 번 뒤 고르기로 | `tl/move-tool.ts:122-124,172-174` · `tl/copy-tool.ts:121-123` · `tl/rotate-tool.ts:129-131` · `tl/scale-tool.ts:134-136` · `tl/array-tool.ts:113-115` | ESC · 끝 = END | transform-acad | 라 | +| N3 | Esc 없는 단계 | `tl/rectangle-tool.ts:55-70` · `tl/eraser-tool.ts:62-74` · `tl/array-tool.ts:147-277` | 모든 물음 상태 ESC → END(계약 시험) | prompt-contract | 나 · 다 · 라 | +| N4 | PLINE · SPLINE Esc = 다 버리고 다시 | `tl/factories/sequence-tool.ts:252-261` | 늘 끝 · PLINE `escCommits` · SPLINE 버림[기] | draw-acad | 가 · 나 | +| N5 | 숫자 단계 `3.2*1.5` → 3.2 | `tl/factories/sequence-tool.ts:193-201,296-301` | 글 전체가 수일 때만 · 아니면 거절 | prompt-frame | 가 | +| N6 | 명령행 칸 소문자 c → 원 | `ic/input-controller.ts:168-172` · `ic/ime-sink.ts:29-34` | 맞추기 대문자 한 곳(2-4) | prompt-frame | 가 | +| N7 | 명령 안 고르기 중 그립 누름 → 그립 편집으로 바뀜 | `tl/select-tool.helpers.ts:55-72` | 그립 집기는 맨 위 도구가 선택 도구(쉼)일 때만 | mouse-acad | 마 | +| N8 | 그립 중 거리 숫자 · 길이 잠금 무시 | `tl/grip-edit-tool.ts:64-80` | `NUMBER_INPUT`(그립 점에서 커서 쪽) 받음 | mouse-acad | 마 | +| N9 | TRIM 멀리 눌러도 자름 · 사각형 → 지금 층 선 | `tl/eraser-tool.ts:94-100,125-139` | `pickRadius()` 밖 = 아무 일 없음 · 사각형 변 = 원래 층 · 꼴(`copyStyle`) | modify-acad | 다 | +| N10 | 타원 셋째 숫자 ≠ 단축 | `tl/draw/basic-draw-tools.ts:82-105` | 「다른 축으로 거리」 = 중심에서 잰 반 길이 | draw-acad | 나 | +| N11 | U · Ctrl+Z 두 길 | `src/commands/commands.file.ts:100-108` · `ic/input-controller.ts:616-634` | 2-8(되돌림 = 끊고 뒤 · 명령 중 U = 옵션일 때만) | prompt-frame | 가 | +| N14 | 스냅 이름 EXT · INS → 명령 돎 · 나머지 버림 | `ic/input-snap-override.ts:13-21` | 점 물음에서 오토캐드 스냅 이름은 늘 스냅 길 · QUA · NOD 더함 · 없는 것 안내 | prompt-frame | 가-2 | +| N17 | ARRAY `x,y` → 복사 0 | `tl/array-tool.ts:198-206,324-360` | ARRAY 새 차례(1-3)로 없어짐 | transform-acad | 라 | +| N18 | 앞글자 실행 | `src/commands/registry.ts:124-134` | 딱 맞음만 · 후보는 목록 | prompt-frame | 가 | +| 새 | 한 번 그리는 명령이 끝나도 첫 물음으로 돌아감 → 명령 이름 거절과 부딪힘 | `tl/line-tool.ts:133-139` · `tl/circle-tool.ts:88-107` · `tl/rectangle-tool.ts:80-99` · `tl/factories/sequence-tool.ts:231-249` | 1 장 「끝난 뒤」 열대로 END · 되풀이 | 창마다 시험 | 가 · 나 · 다 · 라 | + +- 뺌 — H2 나머지(특성 칸 · 리본 특성 · 도면층 관리 · 내보내기) · H6(숨긴 것 고름) · H10(그리드) · H12(명령행 한글) · N12(오토캐드도 같음) · N13(맥 ⌘V) · N15 · N16. + +## 4. 마우스 · 단축키 · 휠 + +| 무엇 | 오토캐드 | 이번 | 자리 | 창 | +|---|---|---|---|---| +| 왼 누름 고르기 | 누름 = 더함 · Shift = 뺌(PICKADD 2) | 넣음(쉼 · 명령 중 같음) · Ctrl+누름 = 지금처럼 넣고 빼기(오토캐드 2D 뜻 없음 · 둠) | `select-tool.helpers.ts` | 마 | +| 창 · 걸침 | 빈 곳 클릭-클릭 · 끌기 | 명령 중에도 끌기 · 상자 더함 · Shift 상자 뺌 | `input-controller.ts` 마우스 · `ic/selecting.ts`(새) | 마 | +| 스침 강조 | 고르기 물음에서도 | 넣음 | `input-controller.ts:283-293` | 마 | +| 고르기 키워드 | P · L · ALL … | ALL · L · P | `select-tool.ts` · `prompt.ts` | 마 · 가 | +| 그립 | 누름 = 뜨거움 → 옮겨 누름 = 놓음 · 끌기도 | 넣음 · 숫자 · Tab · Esc · X | `input-controller.ts` · `grip-edit-tool.ts` | 마 | +| 그립 모드 돌림 · C · B · 여러 그립 · 메뉴 | 있음 | 뺌 | — | — | +| 우클릭 | 명령 중 = Enter · 쉼 = 반복(설정) · 고른 채 = 메뉴(기본) | 명령 중 = Enter · 쉼(고른 채 포함) = 반복 · 메뉴 없음 | `input-controller.ts:174-180` → `handleEnterKey(true)` | 마 | +| Shift+우클릭 | 한 번 스냅 메뉴 | 넣음 — 점 물음일 때만 · 끝점 · 중간점 · 중심 · 사분점 · 교차점 · 수직 · 근처점 · 노드 · 없음 · 시작점에서(FROM) · 두 점 사이의 중간(M2P) → `submitText(이름)` | `ic/snap-menu.ts`(새 · DOM · 손가락 단추 꼴) | 마(FROM · M2P 처리 = 가-2) | +| 휠 굴림 | 밂(앞) = 확대 · 커서 자리 | 기본 밂 = 확대(☰ 뒤집기 그대로 · 모든 화면 일체) · 배율 10% 그대로 | `A00_Common/input_prefs.ts:23,45`(DEFAULT `push` · 없으면 `push`) | 마 | +| 휠 두 번 | 범위 | 가운데 단추 두 번(400 ms · 5 px 안) = 범위 · 앞 화면 기억 | `input-controller.ts` mousedown | 마 | +| 휠 끌기 · Ctrl+휠 | 이동 · 빠른 줌 | 그대로 | — | — | +| ZOOM | 옵션 물음 | E · A · W · P · nX(덧물음 · `'Z`) · ⛶ 단추 = 바로 범위(`zoomToFitScreen`) | `commands.view.ts:41-52` · `ic/zoom-prompt.ts`(새) · `ViewControls.tsx:31` | 마 | +| 직교 F8 | 강제 | 강제 · F3 와 뗌 | `get-point-from-event.ts` · `calculate-angle-guides-and-snap-points.ts` | 마 | +| Shift 누른 동안 | 직교 뒤집음(임시 우선 키) | 넣음 — 점 물음 · 기준점 있을 때 · 마우스 사건 `shiftKey` 로(keyup 안 씀) | `ic/ortho.ts`(새) | 마 | +| 그 밖 임시 키(Shift+E 등) | 있음 | 뺌 | — | — | +| Ctrl+C · X · Shift+C · Shift+V · 1 | 클립보드 · 특성 | COPYCLIP · CUTCLIP · COPYBASE · PASTEBLOCK · PROPERTIES · Ctrl+V 그대로(paste 사건) | `input-controller.ts` 키 · `input-shortcut.ts` | 가 | +| Ctrl+Z · Y · ↶ ↷ | 끊고 되돌림 | 2-8 | `prompt-input.ts` · `ViewControls.tsx:20-23`(마가 `undoCommand` 부름) | 가 · 마 | +| ↑ ↓ · Tab | 기록 · 최근 값 · 후보 | 2-12 | `input-controller.ts:413-422,464-475` · `CommandLine.tsx` | 가-2 | +| Delete | 고른 것 지움 | 그대로(보기 전용 막음 = H2) | — | 마 | +| F1 · F2 · F11 · F12 · Ctrl+9 · Ctrl+W 등 | 있음 | 뺌(브라우저 몫 · 335 정할 것 14) | — | — | + +- 안내 글 바꿈(가 · 끝에 한 번 · 공유 파일) — `ui_template/ui_template_locale_common.ts:120-125`(오른쪽 단추 = 「명령 중 Enter · 쉴 때 반복 · Shift = 스냅 메뉴」 · 휠 두 번 = 범위) · `:165-169` CAD 줄(「↑↓ = 기록 · 최근 값」 · 「Ctrl+C · X · 1」 · 「방향키 = 화면 옮김」 → 「← → · Shift+↑↓ = 화면 옮김」). + +## 5. 창 나눔 · 차례 + +### 5-1. 맡는 것 · 파일 몫 + +| 창 | 맡는 것 | 혼자 고치는 파일 | 같이 만지는 파일(그 줄만) | +|---|---|---|---| +| 가 | 틀 · 키보드 · 명령 해석 · 엔진 · 별칭 · 시험 도우미 · 안내 글 | 새 `src/commands/prompt.ts` · `ic/prompt-input.ts` · `ic/input-history.ts` · `ic/point-modifiers.ts` · `tl/factories/end-command.ts` · `T/helpers/cad-drive.ts` · `T/prompt-frame.test.ts` · `T/prompt-contract.test.ts` / 고침 `tl/factories/sequence-tool.ts` · `tl/tool.types.ts` · `ic/ime-sink.ts` · `ic/input-snap-override.ts` · `ic/input-shortcut.ts` · `src/commands/registry.ts` · `run-command.ts` · `command-catalog.json` · `src/components/CommandLine.tsx` · `src/state.ts`(갈고리 · LASTPOINT 10 줄 안 · 지금 647 줄) · `T/keyboard-chain.test.ts` · `T/command-catalog.test.ts` · `ic/ime-sink.test.ts` | `ic/input-controller.ts` 키 쪽(`handleKeyStroke` · `handleEnterKey` · `handleEscapeKey` · `handleUndo/Redo` · `submitText` · `sendText` · `draw`) · `commands.view.ts:35` · `commands.annotate.ts:84` · `ui_template_locale_common.ts`(끝에) | +| 나 | 그리기 12(1-1) | `tl/line-tool.ts` · `tl/circle-tool.ts` · `tl/rectangle-tool.ts` · `tl/draw/basic-draw-tools.ts` · `tl/draw/construction-tools.ts` · `tl/annotate/text-tools.ts` · `tl/draw/fill-tools.ts` · 새 `tl/draw/pline-tool.ts` 등(옛 파일에서 다시 내보내 `commands.*.ts` 안 만짐) · `T/draw-acad.test.ts` | — | +| 다 | 고치기 · 재기 · 치수 12(1-2) | `tl/eraser-tool.ts`(+helpers) · `tl/modify/corner-tools.ts` · `tl/modify/structure-tools.ts` · `tl/draw/divide-tools.ts` · `tl/utility/inquiry-tools.ts` · `tl/annotate/dimension-tools.ts` · `dimension-radial-tools.ts` · 새 `tl/modify/offset-tool.ts` · `lengthen-tool.ts` · `T/modify-acad.test.ts` | `commands.modify.ts` OFFSET · LENGTHEN import 두 줄 | +| 라 | 옮기기 무리 7 + PEDIT 고르기 상태 | `tl/move-tool.ts` · `copy-tool.ts` · `rotate-tool.ts` · `scale-tool.ts` · `array-tool.ts`(+새 helpers) · `pedit-tool.ts` · `tl/modify/transform-tools.ts`(MIRROR · STRETCH · 다 합친 뒤 옛 OFFSET · LENGTHEN 지움) · `T/transform-acad.test.ts` | — | +| 마 | 마우스 · 고르기 · 그립 · 휠 · 직교 · 보기 전용 문 · ZOOM · 스냅 메뉴 | `tl/select-tool.ts` · `select-tool.helpers.ts` · `grip-edit-tool.ts` · `src/helpers/get-point-from-event.ts` · `get-draw-guides.ts` · `calculate-angle-guides-and-snap-points.ts` · `src/helpers/draw.ts` · `drawControllers/fit-view.ts`(윈도우 줌) · 새 `ic/selecting.ts` · `ic/ortho.ts` · `ic/snap-menu.ts` · `ic/zoom-prompt.ts` · `src/components/ViewControls.tsx` · `A00_Common/input_prefs.ts` · 휠 시험들 · `T/mouse-acad.test.ts` | `ic/input-controller.ts` 마우스 쪽(constructor 마우스 줄 · `handleMouse*` · `sendMouseClick` · `getCanvasPoint`) · `commands.view.ts:41-52`(ZOOM) | + +- `input-controller.ts`(지금 635 줄) — 가는 사다리를 `prompt-input.ts` 로 빼서 줄이고 · 마는 새 논리를 `selecting.ts` · `ortho.ts` 등에 두고 이 파일엔 부르는 줄만. + +### 5-2. 차례 + +1. 가-1 — 2 장 계약 전부(타입 · 사다리 · 거절 · Esc · 빈 Enter · 우클릭 뜻 · END 갈고리 · 엔진 v2 · 기억 · 덧물음 · 별칭 · N1 ②③ · N5 · N6 · N11 · N18 · H1 · 옛 꼴 통과 · 계약 시험 · cad-drive · keyboard-chain 손질) → push → 브레인이 나 · 다 · 라 · 마에 「받기」. +2. 마-1 = 처음부터(가 안 기다림) — 휠 기본 · 고르기 더함/뺌 · 상자 · 명령 중 끌기 · 스침 · N7 · 그립 누르고-옮기고-누르기 · 그립 숫자 · H2 문 · 직교 강제 · Shift 직교 · H8 · H9 · 휠 두 번. +3. 가-1 받은 뒤 동시 — 나 · 다 · 라 · 마-2(ZOOM 덧물음 · 스냅 메뉴 · ALL/L/P · `snapped` 채움 · 우클릭 반복 · ↶↷). +4. 가-2 = 가-1 바로 뒤(나 · 다 · 라와 동시) — ↑↓ · Tab 후보 · FROM · M2P · `@` · `#` · `*` · 스냅 이름 늘림 · H11. +5. 끝 — 가: 옛 꼴 통과 지움 · 안내 글 · 라: 옛 OFFSET · LENGTHEN 지움 · 브레인 화면 검증 · 아티팩트. + +- 창 안 차례(먼저 → 나중) — 나 = LINE → PLINE → RECTANG → CIRCLE → POLYGON → ARC → ELLIPSE → XLINE → SPLINE → TEXT → MTEXT → HATCH · 다 = OFFSET → FILLET → TRIM → EXTEND → CHAMFER → BREAK → LENGTHEN → JOIN → DIVIDE → MEASURE → DIST → DIM · 라 = MOVE → COPY → ROTATE → SCALE → MIRROR → STRETCH → ARRAY. +- 어림(시험 포함 · 화면 확인 별도) — 가-1 1.5 일 · 가-2 1 일 · 나 2.5 일 · 다 2.5 일 · 라 2 일 · 마 2 일. +- 알릴 것 — 가-1 push(모두 받기) · 엔진 기본 바뀜(모든 순서 도구 확정 뒤 끝 · 고른 것 바로 받음) · 휠 기본 바뀜(모든 화면 · 마) · 공유 파일 `ui_template_locale_common.ts`(가). + +## 6. 시험 · 완료 기준 + +### 6-1. 돌리는 법 + +- CAD 시험 — `cd B07_DesignDetail/openwebcad && npx vitest run --dir ../../resources/tester/cad <이름>`(`keyboard-chain.test.ts` 머리 그대로) · src 단위 — `npx vitest run src/inputController/ime-sink.test.ts` 꼴. +- 타입 — 뿌리 `npm run typecheck` + `npm --prefix B07_DesignDetail/openwebcad run check-types` · 빌드 — `npm --prefix B07_DesignDetail/openwebcad run build`. +- 파이썬 — `./venv/Scripts/python.exe -m pytest resources/tester/<시험> -q`(휠 기본 바뀜 손질 때만). +- 전체 시험 안 돌림(사용자 지시 때만) · 고친 대상 시험만. + +### 6-2. 공용 도우미 `T/helpers/cad-drive.ts`(가-1) + +- `initIdle()`(빈 도면 · 선택 도구로 시작 — 지금 `test/helpers/init-application.ts` 는 LINE 으로 시작해 새 규칙에선 「l」이 거절됨) · `key(k, mods?)` · `type(text)`(글자 + Space) · `typeEnter(text)` · `move(x, y)` · `click(x, y, mods?)` · `press` · `release` · `drag(a, b)` · `rightClick(shift?)` · `middleDouble(x, y)` · `entities(type?)` · `promptLine()` · `noticeLine()` · `activeTool()` · `tick()`. +- 시험 쪽 flatten-js 는 다른 사본 — 점은 앱 코드로만 만들고 숫자로 판정(`keyboard-chain.test.ts:3` 그대로). + +### 6-3. 창마다 새 시험 · 꼭 넣을 줄 + +| 창 | 시험 | 꼭 볼 것 | +|---|---|---| +| 가 | `T/prompt-frame.test.ts` | 맞추기(대소문자 · `_` · kw · en · 사이 길이 · 짧으면 안 맞음 · 숨김 · absent) · 그림(「다음 점 지정 또는 [닫기(C)/명령 취소(U)]:」 · `<10.0000>` · `<변위>`) · 빈 Enter 넷 갈래 · 거절(점 물음 「L」 → 글 · 도구 그대로 · 숫자 「3.2*1.5」 「2m」 · 정수 범위) · 쉼(「R」 「LE」 → 알 수 없는 명령 · 「l」 → LINE · 내 별칭 우선 · SE 없음 · ED) · Esc 한 번 끝 · 우클릭 쉼 반복 · END → 선택 도구(같은 틱) · N1(MOVE 기준점 뒤 `runCommand(CIRCLE)` → 선 1 개 · Ctrl+Z 뒤 같은 id 두 벌 없음) · 엔진(loopFrom · escCommits · pickfirst · 빈 고르기 끝 · when · mode · next · keyword) · 기억 · Ctrl+C → COPYCLIP · Ctrl+글자 안 쌓임 · 가-2(↑↓ · Tab · FROM · M2P · `@` · `#` · END/ENDP · QUA · H11) | +| 가 | `T/prompt-contract.test.ts` | 2-16 | +| 나 | `T/draw-acad.test.ts` | LINE(U · C 둘째부터 · Enter 끝 → 쉼 · Space 반복 · 첫 점 Enter 이음 · Esc 남김) · PLINE(C · U · L · A 접선 호 · CL · Esc 남김) · RECTANG D(기억 · 사분면) · CIRCLE(D · 3P · 2P · 기억 · 한 번 뒤 끝) · POLYGON(E · I/C · 기억 · 2.5 거절) · ARC(C · E · R · A) · ELLIPSE(N10 · C · R) · SPLINE(U · C · Esc 버림) · XLINE(H · V · A · 되풀이) · TEXT(높이 · 각 · 줄 이어 · 빈 줄 끝 · 시작 Enter 앞 글 아래) · MTEXT(H · W) · HATCH(내부 점 · S · U) | +| 다 | `T/modify-acad.test.ts` | OFFSET(처음 <통과점> · 거리 기억 · 되풀이 · E · M · U · T) · FILLET(객체 먼저 · R 기억 · Shift 0 · T · M · U) · CHAMFER(D 기억 · Shift) · TRIM(되풀이 · 반경 밖 무시 · Shift 연장 · R · U · Esc · 사각형 층) · EXTEND(빠른 · Shift 자르기) · BREAK(누른 점 · F · `@`) · LENGTHEN(DE · P · T · 마지막 방식 · 누름 길이) · JOIN · DIVIDE 정수 · MEASURE 끝 · DIST 글 · DIMLINEAR(<객체 선택> · T · H · V) · DIMCONTINUE U | +| 라 | `T/transform-acad.test.ts` | MOVE(D · Enter 변위 · Esc 제자리 끝 · 한 번 뒤 끝 · N1) · COPY(되풀이 · Enter 끝 · O 단일 · A · F · U · E) · ROTATE(누름 한 번 = 각 · C · R `@` · P) · SCALE(누름 거리 · C · R) · MIRROR(Y · N · Enter = N) · STRETCH(걸침 꼭짓점만 · D · Enter) · ARRAY(R = 직사각형 · COU · S · COL · R · X · PO · I · A · F · ROT · Esc 안 만듦) · 고르기 ALL · P · L · 빈 Enter 끝 | +| 마 | `T/mouse-acad.test.ts` | 누름 더함 · Shift 뺌 · 상자 더함/뺌 · 명령 중 끌기 상자 · 스침 · N7 · 그립 누름-뗌 대기 → 다음 누름 놓음 · 끌어 놓음 · 그립 숫자 · H2 문 · 직교 강제(커서 · 누름 · 거리) · 스냅 점은 안 덮음 · Shift 임시 직교 · F3 꺼도 직교 · H9 · 우클릭(쉼 반복 · 명령 중 Enter) · 휠 두 번 범위 · 휠 기본 밂 · ZOOM E · W · P · 2X · `'Z` · 스냅 메뉴 → 한 번 스냅 | + +- 휠 기본 바뀜 손질(마) — `ic/input-wheel.test.ts` · `T/input-prefs-cad.test.ts` · `resources/tester/test_input_prefs.py`(+`helper_input_prefs.cjs`) · `test_app_settings.py` · `test_164_detail_zoom.py`(+helper) · `helper_266_detail_home.cjs` 쓰는 시험 · `test_281_1_drainage_zoom_range.py`(+helper) · `test_244_4` · `_6` · `_8` · `helper_218_1_loupe.cjs` · `helper_230_1_popout_wheel.cjs` · `test_touch_bridge.py` — 방향이 뜻인 시험 = 기대값 뒤집음 · 방향과 무관 = 시험 앞에 pull 박음(그 시험 뜻대로) · 브라우저 띄우는 시험은 돌린 창이 결과만 보고. +- 옛 시험 손질(가) — `keyboard-chain.test.ts`(「선 중 다른 명령 이름은 그대로 명령」 → 거절 · C 뒤 쉼 · 글 바뀜 · `initIdle`) · `ime-sink.test.ts`(「빈 우클릭은 반복 안 함」 → 반복) · `command-catalog.test.ts`(SE · ED). + +### 6-4. 완료 기준(창마다) + +- 맡은 명령마다 1 장 줄대로 물음 글 · 옵션 · 기본값 · 빈 Enter · Esc · 끝난 뒤가 시험으로 확인. +- `prompt-contract` 초록(옵션 선언 ↔ 처리 · ESC) · 고친 대상 시험 + 손질한 옛 시험 초록. +- 타입 검사 둘 · CAD 빌드 성공 · 파일 700 줄 안 · prettier. +- PLAN 해당 줄 「검증완료」 한 줄 · 커밋(`git commit -F`) · `.claude/git-autopush.ps1` · push. +- worker_done 에 화면 확인 거리(1 장 [기] 줄 · 휠 · 그립 · 스냅 메뉴 · ZOOM) — 브레인이 끝에 브라우저 검증. + +## 7. 이번 밖 + +- 'CAL · 식 · QuickCalc 값 넣기 · 단위 꼴(3m · 피트) · 도분초 · 라디안 · 측량 각. +- TK · TT · 좌표 거르개(.X) · 극 추적 각 고르기 · 스냅 종류 끄기 · 켬 상태 기억 · 격자 스냅 · F9 · F11 · F12. +- 우클릭 메뉴(기본 · 편집 · 명령) · 시간차 우클릭 · 명령행 옵션 누르기 · DI ↓ 옵션 목록 · DI 절대/상대 바꾸기(정할 것 18 · 지금 = 절대 그대로). +- 그립 모드 돌림 · 그립 C · B · 여러 그립 · 다기능 그립 메뉴 · 끌어 옮기기 · Ctrl+화살표 · 두 번 눌러 고치기 늘림(N16). +- 고르기 W · C · F · WP · CP · 올가미 · 겹친 것 목록 · 「고른 채 명령」 더 넓히기. +- 1 장 「뺌」 옵션 전부 · 연관 배열 · 경로 배열 · MTEXT 편집기 · 글 자리맞추기 · HATCH 설정 · 3D 옵션 · OFFSET 도면층 · FILLET 폴리선. +- H2 나머지 · H6 · H10 · H12 · N13 · N15 · 명령 단위 되돌림 묶음(LINE 여러 토막을 쉴 때 U 한 번에). +- 다른 CAD · 리습 · 스크립트 · 손가락 몸짓 바꿈 · 토목 전용(비탈 1:n). + +## 8. 근거 쪽 + +- 한국어 도움말 2025 KOR(옵션 이름 · 줄) — LINE `GUID-E8C1190C` · PLINE `GUID-11883C70` · RECTANG `GUID-188B2DDA`(첫 줄 그대로) · CIRCLE `GUID-C60B6D5D` · ARC `GUID-30ECFD30` · POLYGON `GUID-E5CD464D` · ELLIPSE `GUID-45F9C588` · SPLINE `GUID-5E7D51E2` · XLINE `GUID-40650DCE` · OFFSET(2016 KOR) `GUID-C0E4246D` · FILLET `GUID-64F8B700` · CHAMFER `GUID-B1DCF991` · TRIM `GUID-B1A185EF` · EXTEND `GUID-89DD7B0F` · MOVE `GUID-47CE7325` · COPY `GUID-1CF9287F`(두 줄 그대로) · ROTATE `GUID-1C265537` · SCALE `GUID-D4E17E51` · STRETCH `GUID-F000A502` · MIRROR `GUID-595277C8` · ARRAY `GUID-E23F6125` · ARRAYRECT `GUID-BB3DA888` · ARRAYPOLAR `GUID-A6E74297` · BREAK `GUID-36A1CDE0` · JOIN `GUID-4D2A28C3`(줄 그대로) · LENGTHEN `GUID-76C74C62` · DIVIDE `GUID-3427CA9F` · MEASURE `GUID-212933E8` · DIST `GUID-A72EADC7` · TEXT `GUID-D1C664DD` · MTEXT 옵션 `GUID-F94BE932` · DIMLINEAR `GUID-E908848F` · HATCH `GUID-27C104F2` · 명령 입력 `GUID-945C8F74`(주소 꼴 `help.autodesk.com/cloudhelp/2025/KOR/AutoCAD-Core/files/.htm`). +- 키워드 줄임 규칙 — AutoLISP `initget`(2015 ENU `GUID-9ED8841B`) 「LType … LT · LTY · LTYP 됨 · L 모자람 · 대소문자 무관」. +- 거절 글 「점 또는 옵션 키워드를 요구함.」 — 오토캐드 한국어판 사용자 글(gocad.co.kr 질문 · 2차) · 나머지 거절 글 = 영문 기억 옮김. +- 영문 줄 · 동작 = 부록4 K1(E1 · E2 표) · 코드 줄 = 부록4 K2 + 이번 다시 읽음. From 193e03927b773c6e3569353e4ccc5eff302ec545 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 15 Oct 2026 23:28:57 +0900 Subject: [PATCH 57/81] =?UTF-8?q?feat(M02=20=EC=9D=BC=EC=9C=84=EB=8C=80?= =?UTF-8?q?=EA=B0=80):=20338-2b=20STmate=20=EA=BC=B4=20=EC=9D=BD=EA=B8=B0?= =?UTF-8?q?=20=C2=B7=20=EA=B1=B0=EC=B0=BD=20=ED=9D=A9=EC=96=B4=EC=A7=84=20?= =?UTF-8?q?=EC=B9=B8=20=EC=9D=B4=EC=9D=8C=20=C2=B7=20=EC=9D=B4=EC=9D=8C=20?= =?UTF-8?q?=EB=84=93=ED=9E=98=20=C2=B7=20=EB=85=B8=EC=9E=84=20=EA=B0=88?= =?UTF-8?q?=EB=9E=98=20=C2=B7=20=EB=AC=B8=EC=84=9C=20=EB=B3=B4=EC=95=88=20?= =?UTF-8?q?400=20=C2=B7=20STmate=20=EA=B2=A9=EC=9E=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 새 M02_Hopyo_Stmate — 일위대가표 · 단가산출근거 · 목록표 → 엑셀꼴과 같은 HP(줄 코드 = M~Z 열 코드 꼴 첫 것 · 블록 글 · 자원 품 · 절 · 출처 · 직종 · 호표 근거글) - 여섯 파일 올라감 — 봉화 45 · 영월 23 · 울진대흥 설계 · 착공 34 · 영덕 9 · 울진소광 25 호표 · 노임 이름 어긋남 0 - Basis.read 거창 꼴 — 빈 산출근거 칸은 오른쪽 칸 이어 붙임 · 돈 머리 맨 오른쪽 묶음 · 단산 줄 21 · 출처 17 · 직종 21 - Excel 이음 넓힘(단산 · 산근 · 단가산출 / 일위 · 대가 · 일위대가 N · 참조 · 호표) · 빈 비고 줄 이름으로 이음 · 노임 갈래 · DRM 400 글(장수 · 진안) - Grid STmate 시트 · 숨은 칸 글 뺌 - 시험 stmate · excel · basis · grid · match · build 68 통과 · 원본 엑셀 · master_data sha256 그대로 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_014utrjfoW831Vc5wJUd13kL --- M02_MasterTemplete/M02_Hopyo_Basis.py | 30 ++- M02_MasterTemplete/M02_Hopyo_Excel.py | 43 +++- M02_MasterTemplete/M02_Hopyo_Grid.py | 10 +- M02_MasterTemplete/M02_Hopyo_Stmate.py | 264 ++++++++++++++++++++++ resources/tester/test_m02_hopyo_basis.py | 17 ++ resources/tester/test_m02_hopyo_excel.py | 54 ++++- resources/tester/test_m02_hopyo_grid.py | 20 ++ resources/tester/test_m02_hopyo_stmate.py | 185 +++++++++++++++ 8 files changed, 607 insertions(+), 16 deletions(-) create mode 100644 M02_MasterTemplete/M02_Hopyo_Stmate.py create mode 100644 resources/tester/test_m02_hopyo_stmate.py diff --git a/M02_MasterTemplete/M02_Hopyo_Basis.py b/M02_MasterTemplete/M02_Hopyo_Basis.py index 7a7705054..2dd647063 100644 --- a/M02_MasterTemplete/M02_Hopyo_Basis.py +++ b/M02_MasterTemplete/M02_Hopyo_Basis.py @@ -6,7 +6,8 @@ 출처 = 「…품셈」(오타 「품줌」) 뒤 20 글자 안 절 번호 — 앞 글에 산림 · 임도 = 산림품셈 · 그 밖 = 건설품셈 · 직종 = 「이름 : 수」 줄의 빈칸 뺀 이름이 우리 인력(LB) 이름 · 옛이름일 때만(지금 이름으로) · -글(338) = 머리 뺀 몸 줄 `GLINES` 줄 · 줄마다 `GCHARS` 자 안(맞춤 지문 재료). +글(338) = 머리 뺀 몸 줄 `GLINES` 줄 · 줄마다 `GCHARS` 자 안(맞춤 지문 재료) · 산출근거 열이 빈 줄은 +그 오른쪽 ~ 돈 머리(맨 오른쪽 묶음) 앞 칸을 빈칸으로 이어 붙임(거창 꼴 · 돈 머리 없으면 `WIDE` 열). 견줌(`candidates`) = 같은 책 조항 같음 1.0 · 윗마디 0.9(엑셀 6-2 ⊃ 우리 6-2-2) · 앞 마디 같음 0.5(조항이 안 맞으면 후보 아님 · `clause`) · 조항 오타 「13-114」 = 「13-11-4」 · 건설 공통 · 토목 덤 0.1 · 직종 겹침(보통인부 뺌) 0.5 · 단위 0.15 · 직종도 뜻 낱말도 안 겹치면 버림 · @@ -34,7 +35,7 @@ _HEAD = re.compile(r"^(\d+)\.\s*(.*?)\s*\[([^\[\]]*)\]\s*$") _BOOK = re.compile(r"품[셈줌]") _TRADE = re.compile(r"^[\s*·\-]*([가-힣][가-힣\s]*?)\s*:\s*[\d,]") _TYPO_CLAUSE = re.compile(r"^(\d+)-(\d)(\d)(\d)$") # 「13-114」 = 13-11-4 -GLINES, GCHARS = 200, 300 +GLINES, GCHARS, WIDE = 200, 300, 140 _TYPOS = (("깍기", "깎기"),) _PLAIN_WORDS = frozenset({"인력", "기계", "장비"}) # 방법 낱말 — 「터널굴착 1발파당 인력」 _MONEY = {"합계": "계", "노무비": "노무비", "재료비": "재료비", "경비": "경비"} @@ -45,6 +46,12 @@ def _flat(value: Any) -> str: # ── 엑셀 블록 읽기 ───────────────────────────────────────────────────── +def _cell_text(value: Any) -> str: + if isinstance(value, float) and value == int(value): + value = int(value) + return str(value).strip() if value is not None else "" + + @lru_cache(maxsize=2) def _trade_names(whole_id: int) -> dict[str, str]: """{빈칸 뺀 이름 · 옛이름: 지금 이름} — 우리 인력(LB).""" @@ -78,11 +85,15 @@ def sources(text: str) -> list[list[str]]: def _columns(rows: list[list[Any]]) -> tuple[int, int, dict[str, int]]: - """(자료 첫 줄, 산출근거 열, {계 · 노무비 · 재료비 · 경비: 열}) — 머리 없으면 (0, 1, {}).""" + """(자료 첫 줄, 산출근거 열, {계 · 노무비 · 재료비 · 경비: 열}) — 머리 없으면 (0, 1, {}) · + 돈 머리가 둘 묶음이면 맨 오른쪽(거창 꼴 · 값이 든 쪽).""" for at, row in enumerate(rows[:10]): flat = [_flat(v) for v in row] if "산출근거" in flat: - money = {key: flat.index(head) for head, key in _MONEY.items() if head in flat} + last = len(flat) - 1 + money = { + key: last - flat[::-1].index(head) for head, key in _MONEY.items() if head in flat + } return at + 1, flat.index("산출근거"), money return 0, 1, {} @@ -93,14 +104,19 @@ def read(rows: list[list[Any]] | None) -> dict[int, dict[str, Any]]: if not rows: return {} start, col, money = _columns(rows) + end = min(money.values()) if money and min(money.values()) > col + 1 else WIDE names = trade_names() blocks: dict[int, dict[str, Any]] = {} block: dict[str, Any] | None = None for at, row in enumerate(rows[start:], start + 1): text = row[col] if col < len(row) else None - if not isinstance(text, str) or not text.strip(): - continue - head = None if text[:1].isspace() else _HEAD.match(text) + head = None + if isinstance(text, str) and text.strip(): + head = None if text[:1].isspace() else _HEAD.match(text) + else: + text = " ".join(_cell_text(v) for v in row[col + 1 : end] if _cell_text(v)) + if not text: + continue if head: block = blocks.setdefault( int(head.group(1)), diff --git a/M02_MasterTemplete/M02_Hopyo_Excel.py b/M02_MasterTemplete/M02_Hopyo_Excel.py index c55a1b89c..e103d1d07 100644 --- a/M02_MasterTemplete/M02_Hopyo_Excel.py +++ b/M02_MasterTemplete/M02_Hopyo_Excel.py @@ -7,7 +7,12 @@ 호표 줄 = 「제N호표」 · 호표 1 단위 값 = 금액 열 · 구성 줄 = 호표 칸 빔 · 품명 있음 · 끝 = 다음 호표. 갈래 = % · 호표(비고 일위N참조) · 단가산출(단산N참조) · 기계(단위 hr) · 별도계상(값 없음) · -자재(재료비만) · 기타 — 규격은 원문 그대로 둠. +자재(재료비만) · 노임(단위 인 + 우리 인력 이름) · 기타 — 규격은 원문 그대로 둠. +이음(338) = 비고 「단산 · 산근 · 단가산출 N(참조 · 호표)」 → 단산 N · 「일위 · 대가 · 일위대가 N」 → +호표 N(빈칸 무시) · 비고 빈 줄(노무비 · 경비 단가 있음)은 품명 + 단위가 같은 블록이 하나뿐이면 +이음(줄 `이음` = 「이름」) · 하이퍼링크 안 읽음. +STmate 꼴(「일위대가표」 · 「단가산출근거」 시트) = `M02_Hopyo_Stmate.read` · +문서 보안(DRM) 파일 = 400 글. 검산 = 줄 금액 floor(단가 × 수량 × 10) / 10 · 합계 단가 = 셋 합 · % 줄 = floor(바탕 × 율 / 100 × 10) / 10 · 호표 비목 = floor(줄 금액 합) · 총괄표 합계 — 어긋남은 글 목록(막지 않음). @@ -25,6 +30,7 @@ from decimal import ROUND_FLOOR, Decimal, InvalidOperation from typing import Any from M02_MasterTemplete import M02_Hopyo_Basis as hopyo_basis +from M02_MasterTemplete import M02_Hopyo_Stmate as hopyo_stmate SHEET = "일위대가" SUMMARY = "일위대가총괄표" @@ -32,7 +38,9 @@ COSTS = ("노무비", "재료비", "경비") HEADS = ("호표", "품명", "규격", "수량", "단위", "비고") NO_SHEET = "일위대가 시트 없음" _HOPYO = re.compile(r"제\s*(\d+)\s*호표") -_REF = re.compile(r"^(단산|일위)\s*(\d+)\s*참조$") +_REF = re.compile(r"^(?:(단산|산근|단가산출)|(일위대가|일위|대가))(\d+)(?:참조|호표)?$") +DRM = "문서 보안(DRM)으로 잠긴 파일 — 보안을 푼 사본으로 저장해 올림" +_OLE = b"\xd0\xcf\x11\xe0" _MACHINE_UNITS = ("hr", "시간", "h") @@ -77,11 +85,15 @@ def _sheets(name: str, data: bytes) -> dict[str, list[list[Any]]]: wanted = (SHEET, SUMMARY, hopyo_basis.SHEET) if not data: raise ExcelError("빈 파일") + if b"DRMONE" in data[:256] and data[:4] not in (b"PK\x03\x04", _OLE): + raise ExcelError(DRM) try: if data[:4] == b"PK\x03\x04" or name.lower().endswith(".xlsx"): import openpyxl book = openpyxl.load_workbook(io.BytesIO(data), read_only=True, data_only=True) + if hopyo_stmate.is_stmate([t.strip() for t in book.sheetnames]): + wanted = hopyo_stmate.SHEETS out = { ws.title.strip(): [list(row) for row in ws.iter_rows(values_only=True)] for ws in book.worksheets @@ -143,6 +155,8 @@ def kind_of(line: dict[str, Any]) -> str: return "단가산출" if unit in _MACHINE_UNITS: return "기계" + if unit == "인" and _flat(line.get("이름")) in hopyo_basis.trade_names(): + return "노임" if not any(price.get(k) for k in ("계", *COSTS)): return "별도계상" if price.get("재료비") and not price.get("노무비") and not price.get("경비"): @@ -208,10 +222,28 @@ def _summary(rows: list[list[Any]] | None) -> dict[int, Decimal]: return out +def _by_name(raw: list[list[dict[str, Any]]], blocks: dict[int, dict[str, Any]]) -> None: + """비고 빈 줄(노무비 · 경비 단가 있음) → 품명 + 단위가 같은 블록이 하나뿐이면 단산 이음.""" + index: dict[tuple[str, str], list[int]] = {} + for no, block in blocks.items(): + index.setdefault((_flat(block["이름"]), _flat(block["단위"]).lower()), []).append(no) + for line in (one for lines in raw for one in lines): + if line["참조"] or line["비고"] or line["갈래"] == "%": + continue + if not (line["단가"].get("노무비") or line["단가"].get("경비")): + continue + hit = index.get((_flat(line["이름"]), _flat(line["단위"]).lower())) or [] + if len(hit) == 1: + line["참조"], line["이음"] = {"종류": "단산", "번": hit[0]}, "이름" + line["갈래"] = kind_of(line) + + # ── 읽기 ────────────────────────────────────────────────────────────── def read_book(name: str, data: bytes) -> dict[str, Any]: """엑셀 → `{시트, 호표: [...], 검산: {어긋남: [글 …], 총괄표: 맞춘 수}}` — 값은 JSON 수.""" sheets = _sheets(name, data) + if hopyo_stmate.is_stmate(sheets): + return hopyo_stmate.read(sheets) rows = sheets.get(SHEET) if rows is None: raise ExcelError(NO_SHEET) @@ -238,7 +270,7 @@ def read_book(name: str, data: bytes) -> dict[str, Any]: if head or not name_ or not hopyos: continue note = _text(_cell(row, cols.get("비고"))) - ref = _REF.match(note.replace(" ", "")) + ref = _REF.match(_flat(note)) line = { "차례": len(raw[-1]), "이름": name_, @@ -248,7 +280,9 @@ def read_book(name: str, data: bytes) -> dict[str, Any]: "단가": _money(row, cols, 0), "금액": _money(row, cols, 1), "비고": note, - "참조": {"종류": ref.group(1), "번": int(ref.group(2))} if ref else None, + "참조": {"종류": "단산" if ref.group(1) else "일위", "번": int(ref.group(3))} + if ref + else None, } line["갈래"] = kind_of(line) line["행"] = hopyos[-1]["행"][1] = at @@ -256,6 +290,7 @@ def read_book(name: str, data: bytes) -> dict[str, Any]: if not hopyos: raise ExcelError(f"{SHEET} 시트에 호표(제N호표)가 없음") blocks = hopyo_basis.read(sheets.get(hopyo_basis.SHEET)) + _by_name(raw, blocks) for line in (one for lines in raw for one in lines): ref = line["참조"] or {} if ref.get("종류") == "단산" and (block := blocks.get(ref["번"])): diff --git a/M02_MasterTemplete/M02_Hopyo_Grid.py b/M02_MasterTemplete/M02_Hopyo_Grid.py index 39af897fa..b41f477f2 100644 --- a/M02_MasterTemplete/M02_Hopyo_Grid.py +++ b/M02_MasterTemplete/M02_Hopyo_Grid.py @@ -1,11 +1,12 @@ -"""M02 일위대가 실무자료 엑셀꼴 격자(PLAN 329-1) — 올린 엑셀 「일위대가」 · 「단가산출」 시트 → +"""M02 일위대가 실무자료 엑셀꼴 격자(PLAN 329-1) — 올린 엑셀 「일위대가」 · 「단가산출」 시트 +(STmate = 「일위대가표」 · 「단가산출근거」 · 338) → A00 산출근거 스프레드시트 Workbook(`A00_Common/spreadsheet/spreadsheet_types.ts` 꼴 · 읽기 전용 보기). xls = xlrd `formatting_info` — 값 · 병합 · 열 폭(÷256 글자) · 행 높이(÷20 pt · 시트 기본과 다른 것만) · 서식 표(글꼴 · 굵게 · 맞춤 · 줄바꿈 · 테두리 · 채움 · 숫자 형식) · 틀고정(파일 값 · 없으면 머리 4행). -xlsx = 값만 · 넓은 시트는 머리 글(앞 4 줄) 마지막 열까지. +xlsx = 값만 · 넓은 시트는 머리 글(앞 4 줄) 마지막 열까지 · STmate 숨은 칸 글(「_x0005_`BDCOD…」) 뺌. 결과 `{책, 호표: [{번, 이름, 행}], 단산: [{번, 이름, 행}]}` — 행 = 엑셀 1부터 [시작, 끝]. """ @@ -18,8 +19,9 @@ from openpyxl.utils import get_column_letter from M02_MasterTemplete import M02_Hopyo_Basis as hopyo_basis from M02_MasterTemplete import M02_Hopyo_Excel as hopyo_excel +from M02_MasterTemplete import M02_Hopyo_Stmate as hopyo_stmate -SHEETS = (hopyo_excel.SHEET, hopyo_basis.SHEET) +SHEETS = (hopyo_excel.SHEET, hopyo_basis.SHEET, hopyo_stmate.SHEET, hopyo_stmate.CALC) HEAD_ROWS = 4 _HALIGN = {1: "left", 2: "center", 3: "right", 6: "centerContinuous", 7: "distributed"} _VALIGN = {0: "top", 1: "center"} @@ -192,7 +194,7 @@ def _xlsx(name: str, data: bytes) -> dict[str, Any]: } for r, row in enumerate(rows) for c, v in enumerate(row[:width]) - if v not in (None, "") + if v not in (None, "") and not str(v).startswith("_x000") # STmate 숨은 칸 글 } sheets.append({"id": f"s{at}", "이름": title, "칸": cells, "틀고정": {"행": HEAD_ROWS}}) return {"서식": [{}], "시트": sheets} diff --git a/M02_MasterTemplete/M02_Hopyo_Stmate.py b/M02_MasterTemplete/M02_Hopyo_Stmate.py new file mode 100644 index 000000000..5d3a6cdd8 --- /dev/null +++ b/M02_MasterTemplete/M02_Hopyo_Stmate.py @@ -0,0 +1,264 @@ +"""M02 일위대가 실무자료 STmate 꼴 읽기(PLAN 338-2b) — 「일위대가표」 · 「단가산출근거」 시트 + +목록표(노무비 · 중기 · 재료비 · 경비 · 단가산출근거) → 엑셀꼴과 같은 HP 꼴 +(`M02_Hopyo_Excel.read_book`). + +일위대가표 = A 열 「제N호표」 줄(B~Z 열 「BDCOD|B…」 = 호표 코드) → 다음 줄 A 이름 · B 규격 · +D 단위 → 구성 줄(A 이름 · B 규격 · C 수량 · D 단위 · E 계 · G 노무비 · I 재료비 · K 경비 단가 · +다음 열 금액 · M 비고) → 「합계」 줄에서 끝. +줄 코드 = M~Z 열에서 `_CODE` 꼴 첫 것(판마다 열 수가 달라도 읽힘 · 하이퍼링크 안 읽음) · +코드 없는 줄 = 호표 `근거글`. +갈래 = D 단가산출(참조 단산 · 번 = 코드 숫자) · B 호표(참조 일위 · 번 = 호표 코드 표) · L 노임 · +X 기계 · M 자재(비고 「별산」 · 규격 「별도계상」 · 값 없음 = 별도계상) · S · W 별도계상 · +PR · 단위 % = % · 그 밖 기타. +단가산출근거 = 「BDCOD|D…」 머리 → 다음 머리 · 「총계」 앞까지 한 블록 · 글 = B 열(머리 이름 줄 · +소계 · 계 뺌) · 자원 = M 열 「X00000_n」 줄(품 = H 열 · 이름 · 규격 = X 열 「이름 / 규격」 · +비면 목록표 · 기계 셋 줄(노무비 · 재료비 · 경비)은 하나로 · 절 = 그 앞 번호 절 제목) · +머리 이름 · 규격 · 단위 = 단가산출근거목록표 · 출처 = 블록 글 `Basis.sources` · +직종 = 자원 L 이름(우리 인력 · 옛이름 = 지금 이름). +검산 안 함 — `검산 {어긋남: [], 총괄표: 0}`. +""" + +from __future__ import annotations + +import re +from typing import Any + +from M02_MasterTemplete import M02_Hopyo_Basis as hopyo_basis +from M02_MasterTemplete import M02_Hopyo_Excel as hopyo_excel # 서로 부름 — 부를 때만 씀 + +SHEET = "일위대가표" +CALC = "단가산출근거" +LISTS = ("단가산출근거목록표", "노무비목록표", "중기목록표", "재료비목록표", "경비목록표") +SHEETS = (SHEET, CALC, *LISTS) +_CODE = re.compile(r"^(?:COD\|)?([A-Z]{1,2}\d{5}|PR[A-Z]?\d+|[TC]\d+)(?:_\d+)?(?: →)?$") +_META = re.compile(r"(? bool: + return SHEET in names and CALC in names + + +def _cell(row: list[Any], col: int) -> Any: + return row[col] if col < len(row) else None + + +def _s(value: Any) -> str: + return hopyo_excel._text(value) + + +def _flat(value: Any) -> str: + return re.sub(r"\s+", "", str(value or "")) + + +def _code(row: list[Any], cols: Any = CODE_COLS) -> str | None: + for col in cols: + hit = _CODE.match(_s(_cell(row, col))) + if hit: + return hit.group(1) + return None + + +def _lists(sheets: dict[str, list[list[Any]]]) -> dict[str, dict[str, str]]: + """목록표 → {코드: {이름, 규격, 단위}} — B 이름 · C 규격 · D 단위 · 코드 = 숨은 칸 + 「COD|…」 · 없으면 코드 꼴 첫 칸(판마다 A 열 · 뒤 열).""" + out: dict[str, dict[str, str]] = {} + for title in LISTS: + for row in sheets.get(title) or []: + meta = _META.search(" ".join(_s(v) for v in row)) + code = meta.group(1) if meta else _code(row, range(len(row))) + if code and _s(_cell(row, 1)): + out[code] = { + "이름": _s(row[1]), + "규격": _s(_cell(row, 2)), + "단위": _s(_cell(row, 3)), + } + return out + + +# ── 단가산출근거 블록 ────────────────────────────────────────────────── +def _blocks(rows: list[list[Any]], table: dict[str, dict[str, str]]) -> dict[str, dict[str, Any]]: + """{D 코드: {번, 코드, 이름, 규격, 단위, 행, 출처, 직종, 글, 자원}}.""" + names = hopyo_basis.trade_names() + out: dict[str, dict[str, Any]] = {} + block: dict[str, Any] | None = None + title = "" + for at, row in enumerate(rows, 1): + head = next( + (hit for v in row[:8] if (hit := _HEAD_D.search(_s(v)))), None + ) # 머리 = G 열(판마다 B 열 글이 다름) + if head: + code = head.group(1) + info = table.get(code) or {} + block = out.setdefault( + code, + { + "번": int(code[1:]), + "코드": code, + "이름": info.get("이름", ""), + "규격": info.get("규격", ""), + "단위": info.get("단위", ""), + "행": [at, at], + "글": [], + "자원": [], + }, + ) + title = "" + continue + if block is None: + continue + first = _flat(_cell(row, 0)) + if first.startswith("총계"): + block["행"][1] = at + block = None + continue + text = _s(_cell(row, 1)) + res = _RES.match(_s(_cell(row, RES_COL))) + code = res.group(1) + res.group(2) if res else None + if not code: + alt = _RES_A.match(first) or _RES_G.search(_s(_cell(row, 6))) + code = alt.group(1) if alt else None + if not text and not code: + continue + block["행"][1] = at + if _s(_cell(row, RES_COL)) == block["코드"]: # 머리 이름 줄 「이름 규격 / 단위」 + if not block["이름"]: + name, _, unit = text.rpartition("/") + block["이름"], block["단위"] = name.strip() or text, unit.strip() + continue + if text and _flat(text) not in ("소계", "계"): + if len(block["글"]) < hopyo_basis.GLINES: + block["글"].append(text[: hopyo_basis.GCHARS]) + if (sec := _SECTION.match(text)) and not re.match(r"^\s*\d+\.\d", text): + title = sec.group(1).strip() + if not code: + continue + last = block["자원"][-1] if block["자원"] else None + if code[0] == "X" and last and last["코드"] == code and last["절"] == title: + continue # 기계 노무비 · 재료비 · 경비 셋 줄 = 하나 + name, _, spec = _s(_cell(row, NAME_COL)).partition("/") + info = table.get(code) or {} + qty = hopyo_excel._num(_cell(row, QTY_COL)) + block["자원"].append( + { + "갈래": code[0], + "코드": code, + "이름": name.strip() or info.get("이름", ""), + "규격": spec.strip() if name.strip() else info.get("규격", ""), + "품": float(qty) if qty is not None else None, + "절": title, + } + ) + for block in out.values(): + block["출처"] = hopyo_basis.sources(" ".join(block["글"])) + trades = [names.get(_flat(r["이름"])) for r in block["자원"] if r["갈래"] == "L"] + block["직종"] = list(dict.fromkeys(t for t in trades if t)) + return out + + +# ── 일위대가표 ──────────────────────────────────────────────────────── +def _kind(code: str, line: dict[str, Any]) -> str: + if code.startswith("PR") or _flat(line["단위"]) == "%": + return "%" + if code[0] == "M": + apart = line["비고"].startswith("별산") or line["규격"] == "별도계상" + return "별도계상" if apart or not any(line["단가"].values()) else "자재" + return _KIND.get(code[0], "기타") + + +def _hopyos(rows: list[list[Any]]) -> tuple[list[dict[str, Any]], list[list[dict[str, Any]]]]: + hopyos: list[dict[str, Any]] = [] + raw: list[list[dict[str, Any]]] = [] + want_head = done = False + for at, row in enumerate(rows, 1): + first = _s(_cell(row, 0)) + hit = hopyo_excel._HOPYO.fullmatch(_flat(first)) + if hit: + code = next((m.group(1) for v in row[1:26] if (m := _HEAD_B.search(_s(v)))), None) + hopyos.append({"번": int(hit.group(1)), "코드": code, "행": [at, at], "근거글": []}) + raw.append([]) + want_head, done = True, False + continue + if not hopyos or done: + continue + hopyo = hopyos[-1] + if want_head: + hopyo.update(이름=first, 규격=_s(_cell(row, 1)), 단위=_s(_cell(row, 3))) + hopyo["금액"] = {} + want_head = False + continue + if _flat(first).startswith("합계") or _flat(first) == "계": + hopyo["금액"] = hopyo_excel._money(row, _COLS, 1) + done = True # 끝 — 다음 호표까지 줄 안 받음 + continue + if not first: + continue + code = _code(row) + hopyo["행"][1] = at + if not code: + if re.search(r"[0-9A-Za-z가-힣]", first): + hopyo["근거글"].append(f"{first} {_s(_cell(row, 1))}".strip()) + continue + if code[0] in "TC" and code[1].isdigit(): + continue # 계 · 소계 · 계약단가 + line = { + "차례": len(raw[-1]), + "이름": first, + "규격": _s(_cell(row, 1)), + "수량": hopyo_excel._num(_cell(row, 2)), + "단위": _s(_cell(row, 3)), + "단가": hopyo_excel._money(row, _COLS, 0), + "금액": hopyo_excel._money(row, _COLS, 1), + "비고": _s(_cell(row, NOTE)), + "코드": code, + "행": at, + } + line["갈래"] = _kind(code, line) + line["참조"] = ( + {"종류": "단산", "번": int(code[1:]), "코드": code} + if code[0] == "D" + else {"종류": "일위", "번": None, "코드": code} + if code[0] == "B" + else None + ) + raw[-1].append(line) + return hopyos, raw + + +def read(sheets: dict[str, list[list[Any]]]) -> dict[str, Any]: + """STmate 시트 → HP 꼴 `{꼴, 시트, 호표, 단산, 검산}`(값은 JSON 수).""" + hopyos, raw = _hopyos(sheets.get(SHEET) or []) + if not hopyos: + raise hopyo_excel.ExcelError(f"{SHEET} 시트에 호표(제N호표)가 없음") + table = _lists(sheets) + blocks = _blocks(sheets.get(CALC) or [], table) + by_code = {h["코드"]: h["번"] for h in hopyos if h["코드"]} + for line in (one for lines in raw for one in lines): + ref = line["참조"] or {} + if ref.get("종류") == "일위": + ref["번"] = by_code.get(ref["코드"]) + elif ref.get("종류") == "단산" and (block := blocks.get(ref["코드"])): + line["근거"] = {k: block[k] for k in ("출처", "직종", "행")} + out = [] + for hopyo, lines in zip(hopyos, raw, strict=True): + for key in ("이름", "규격", "단위"): + hopyo.setdefault(key, "") + if not hopyo["근거글"]: + del hopyo["근거글"] + out.append({**hopyo_excel._json(hopyo), "줄": [hopyo_excel._json(x) for x in lines]}) + keep = ("번", "코드", "이름", "규격", "단위", "행", "출처", "직종", "글", "자원") + return { + "꼴": "STmate", + "시트": SHEET, + "호표": out, + "단산": [{k: b[k] for k in keep} for b in blocks.values()], + "검산": {"어긋남": [], "총괄표": 0}, + } diff --git a/resources/tester/test_m02_hopyo_basis.py b/resources/tester/test_m02_hopyo_basis.py index 8bc0a880b..1adb7f3be 100644 --- a/resources/tester/test_m02_hopyo_basis.py +++ b/resources/tester/test_m02_hopyo_basis.py @@ -4,6 +4,7 @@ 후보 93 → GC000083 · 21 → GC000198 · 64 → GC000143 위 둘 · 49 → GC000085 · 잔토처리에 철골재 철거 없음 · 근거 없는 줄 그대로 · 상태 「후보」(자동 확정 없음) · 블록 글 HP(338-2a · 268 · 139 이음 그대로) · `clause` 윗마디 0.9 · 조항 오타 13-114 · +거창 꼴 흩어진 칸 이어 붙임(단산 줄 21 · 출처 17 · 직종 21 · 338-2b) · master_data 그대로. """ @@ -152,3 +153,19 @@ def test_조항_윗마디_오타(north): assert hb.sources("[산림사업표준품셈 9-14]") == [["산림품셈", "9-14"]] stone = next(b for b in north["단산"] if b["번"] == 63) assert stone["출처"] == [["산림품셈", "13-11-4"]] + + +def test_거창_흩어진_칸(): + path = ( + base.ROOT + / "resources/knowledge/original/실무문서/01.산불거창(거창북상병곡)/01.내역서" + / "00.25년_산불_거창(거창,북상,병곡,산8외)내역+수량.xlsx" + ) + book = hx.read_book(path.name, path.read_bytes()) + lines = [x for h in book["호표"] for x in h["줄"] if x["갈래"] == "단가산출"] + assert len(lines) == 21 and all(x.get("근거") for x in lines) + assert sum(1 for x in lines if x["근거"]["출처"]) == 17 + assert sum(1 for x in lines if x["근거"]["직종"]) == 21 + first = book["단산"][0] + assert first["이름"] == "측구터파기(토사)" and first["직종"] == ["보통인부"] + assert any(t.startswith("보통인부 : 172,068") for t in first["글"]) diff --git a/resources/tester/test_m02_hopyo_excel.py b/resources/tester/test_m02_hopyo_excel.py index 6e6b14fdf..7129e28de 100644 --- a/resources/tester/test_m02_hopyo_excel.py +++ b/resources/tester/test_m02_hopyo_excel.py @@ -2,7 +2,8 @@ 견본 = `resources/knowledge/original/실무문서/2026_실무일위대가 모음/`(북부청 임도 · 사방) · 검산 어긋남 0 · 호표 값 · 시트 없음 400 · xlsx 길 · 역할 표 · 엉터리 · 20 MB · 지우기 · -양식 목록에 안 섞임 · +양식 목록에 안 섞임 · 이음 넓힘(산근 · 대가 · 호표 · 이름으로 이음) · 노임 갈래 · +문서 보안 400(338) · master_data 원본 그대로(sha256). 층은 임시 폴더. """ @@ -207,3 +208,54 @@ def test_마스터_층은_마스터_폴더(client): client.who.update(role="USER") assert client.get(URL.format("system")).json()["줄"][0]["키"] == "HP-000001" # 읽기 = 누구나 assert client.delete(URL.format("system") + "/HP-000001").status_code == 403 + + +def _linked_xlsx() -> bytes: + """338 — 비고 「산근 2 호표」 · 「대가1」 · 빈 비고 + 이름 같은 블록 · 노임 줄.""" + import openpyxl + + book = openpyxl.Workbook() + ws = book.active + ws.title = "일위대가" + ws.append([None, "호표", "품명", "규격", "수량", "단위", "합계", None, "노무비", None, "비고"]) + ws.append([None, None, None, None, None, None, "단가", "금액", "단가", "금액"]) + ws.append([None, "제1호표", "표지석", "", None, "개소", None, 1000, None, 1000]) + ws.append([None, None, "터파기", "토사", 1, "㎥", 100, 100, 100, 100, "산근 2 호표"]) + ws.append([None, None, "되메우기", "", 1, "㎥", 100, 100, 100, 100, None]) + ws.append([None, None, "보통인부", "", 1, "인", 100, 100, 100, 100, None]) + ws.append([None, None, "호표 줄", "", 1, "개소", 100, 100, 100, 100, "대가1"]) + calc = book.create_sheet("단가산출") + calc.append([None, "산출근거", "합계", "노무비"]) + calc.append([None, "2.터파기[㎥]"]) + calc.append([None, " [건설공사표준품셈 3-2-1]"]) + calc.append([None, "3.되메우기[㎥]"]) + calc.append([None, " 보통인부 : 100 × 1 = 100"]) + out = io.BytesIO() + book.save(out) + return out.getvalue() + + +def test_이음_넓힘_노임(): + got = hx.read_book("a.xlsx", _linked_xlsx()) + lines = {x["이름"]: x for x in got["호표"][0]["줄"]} + assert lines["터파기"]["참조"] == {"종류": "단산", "번": 2} and "이음" not in lines["터파기"] + assert lines["터파기"]["근거"]["출처"] == [["건설품셈", "3-2-1"]] + assert lines["되메우기"]["참조"] == {"종류": "단산", "번": 3} # 빈 비고 · 이름 같은 블록 하나 + assert lines["되메우기"]["이음"] == "이름" and lines["되메우기"]["갈래"] == "단가산출" + assert lines["되메우기"]["근거"]["직종"] == ["보통인부"] + assert lines["보통인부"]["갈래"] == "노임" and "이음" not in lines["보통인부"] + assert lines["호표 줄"]["참조"] == {"종류": "일위", "번": 1} + for note, want in ( + ("단산12참조", ("단산", 12)), + ("단가산출 3", ("단산", 3)), + ("일위대가4호표", ("일위", 4)), + ("일위 5 참조", ("일위", 5)), + ): + hit = hx._REF.match(hx._flat(note)) + assert ("단산" if hit.group(1) else "일위", int(hit.group(3))) == want + assert hx._REF.match("단산참조") is None and hx._REF.match("D96") is None + + +def test_문서_보안_400(): + with pytest.raises(hx.ExcelError, match="문서 보안"): + hx.read_book("a.xlsx", bytes([0x9B]) + b" DRMONE This Document is secured" + bytes(64)) diff --git a/resources/tester/test_m02_hopyo_grid.py b/resources/tester/test_m02_hopyo_grid.py index 58c6cac90..4db5efaa0 100644 --- a/resources/tester/test_m02_hopyo_grid.py +++ b/resources/tester/test_m02_hopyo_grid.py @@ -2,6 +2,7 @@ 북부청 임도 xls — 병합 11 · 15열 · B20 「제3호표」 · `#,##0` · 호표 68 행 범위 안 겹침 · 제3호표 [20, 24] · 올리면 격자도 · 200 / 옛 파일 404 · 지우면 격자도 · 역할 · xlsx 값만 · +STmate 격자(일위대가표 · 단가산출근거 · 숨은 칸 글 뺌 · 338) · master_data 그대로. """ @@ -131,3 +132,22 @@ def test_xlsx_값만(): assert sheet["칸"]["B5"] == {"값": "제 1 호표"} and sheet["칸"]["E6"] == {"값": 0.5} assert "K6" not in sheet["칸"] # 머리 글(앞 4 줄) 마지막 열 J 까지 assert grid["호표"] == [{"번": 1, "이름": "표지석", "행": [5, 6]}] and grid["단산"] == [] + + +def test_STmate_격자(): + from resources.tester.test_m02_hopyo_stmate import _path + + path = _path("영월") + data = path.read_bytes() + parsed = hx.read_book(path.name, data) + grid = hg.read(path.name, data, parsed) + assert [s["이름"] for s in grid["책"]["시트"]] == ["일위대가표", "단가산출근거"] + cells = _sheet(grid, "일위대가표")["칸"] + one = grid["호표"][0] + assert one == {"번": 1, "이름": "녹생토(종자배합)", "행": [5, 10]} + assert cells[f"A{one['행'][0]}"]["값"].strip().startswith("제") + assert cells["A10"]["값"] == "보통인부" + calc = _sheet(grid, "단가산출근거")["칸"] + block = next(b for b in grid["단산"] if b["번"] == 2) + assert calc[f"B{block['행'][0] + 1}"]["값"].startswith("구조물터파기(인력)") + assert not any(str(c.get("값")).startswith("_x000") for c in {**cells, **calc}.values()) diff --git a/resources/tester/test_m02_hopyo_stmate.py b/resources/tester/test_m02_hopyo_stmate.py new file mode 100644 index 000000000..82ad43569 --- /dev/null +++ b/resources/tester/test_m02_hopyo_stmate.py @@ -0,0 +1,185 @@ +"""일위대가 실무자료 STmate 꼴 읽기(PLAN 338-2b · `M02_Hopyo_Stmate`). + +여섯 파일 올라감(봉화 45 · 영월 23 · 울진대흥 설계 · 착공 34 · 영덕 9 · 울진소광 25 호표) · +봉화 줄 216(구성 209 + 근거글 7) · 단산 103 · 노임 30 · 호표 22 · 줄 코드 갈래 · +노임 이름 어긋남 0 · 블록 자원 품 · 이음 열쇠(M~Z 열 코드 꼴 첫 것) · +문서 보안(장수 · 진안) 400 글 · 원본 엑셀 · master_data 그대로. +""" + +import hashlib +from pathlib import Path + +import pytest + +from M02_MasterTemplete import M02_Hopyo_Basis as hb +from M02_MasterTemplete import M02_Hopyo_Excel as hx +from M02_MasterTemplete import M02_Hopyo_Stmate as st +from M02_MasterTemplete import M02_Hopyo_Store as hs +from M02_MasterTemplete import M02_Template_Layers as layers +from resources.tester import test_m02_hopyo_excel as base +from resources.tester.test_m02_hopyo_excel import ROOT, _post, master_hash + +client = base.client # 공용 고정물(임시 층) +DOCS = ROOT / "resources" / "knowledge" / "original" / "실무문서" +FILES = { + "봉화": "04.산불 봉화/01. 내역서/2024년 산불진화임도 실시설계(기번41_현동)_ L=3.46km.xlsx", + "영월": "05. 동부청 영월/내역서/2024년 임도시설사업2차(산불진화_기번6)-영월.xlsx", + "대흥설계": "2024년 간선임도(기번3-울진.대흥)(공통)/1. 설계원본/01 내역자료/" + "2024년 간선임도사업(기번3).xlsx", + "대흥착공": "2024년 간선임도(기번3-울진.대흥)(공통)/" + "2024년 간선임도 신설사업(기번3 울진.울진.대흥.산65외2).xlsx", + "영덕": "20250527 2025년 계류보전사업(기번1-영덕.병곡.영리.산214-1)(변경)/01.내역자료/" + "2025년 계류보전사업(기번1 영덕.병곡.영.산214)(변경).xlsx", + "소광": "25년 산불진화임도(기번8)(울진소광)/" + "2025년 산불진화임도 신설사업(기번8 울진.금강송.소광.산29외).xlsx", +} +HOPYOS = {"봉화": 45, "영월": 23, "대흥설계": 34, "대흥착공": 34, "영덕": 9, "소광": 25} +LOCKED = { + "장수": "02.산불 장수/01.내역서/" + "00.단가변경_26년_산불_무주(장수,장수,덕산,산54-1)내역+수량.xlsx", + "진안": "03.산불진안/01.내역서/00.단가변경_26년_산불_무주(진안,백운,백암,산1-2)내역+수량.xlsx", +} + + +def _path(tag: str) -> Path: + return DOCS / {**FILES, **LOCKED}[tag] + + +@pytest.fixture(scope="module") +def books() -> dict[str, dict]: + return {tag: hx.read_book(_path(tag).name, _path(tag).read_bytes()) for tag in FILES} + + +def _lines(book: dict) -> list[dict]: + return [x for h in book["호표"] for x in h["줄"]] + + +def test_여섯_파일_호표_수(books): + assert {tag: len(b["호표"]) for tag, b in books.items()} == HOPYOS + for book in books.values(): + assert (book["꼴"], book["시트"]) == ("STmate", "일위대가표") + assert book["검산"] == {"어긋남": [], "총괄표": 0} + assert len(_lines(books["대흥설계"])) == len(_lines(books["대흥착공"])) == 171 # 같은 설계 + + +def test_봉화_줄_갈래(books): + bong = books["봉화"] + lines = _lines(bong) + kinds = {k: sum(1 for x in lines if x["갈래"] == k) for k in ("단가산출", "노임", "호표", "%")} + assert kinds == {"단가산출": 103, "노임": 30, "호표": 22, "%": 7} + assert len(lines) + sum(len(h.get("근거글") or []) for h in bong["호표"]) == 216 + assert all(x["코드"] for x in lines) + for book in books.values(): + for x in _lines(book): + if x["갈래"] == "호표": + assert x["참조"]["번"] in {h["번"] for h in book["호표"]} + if x["갈래"] == "단가산출": + assert x["참조"] == {"종류": "단산", "번": int(x["코드"][1:]), "코드": x["코드"]} + assert "근거" in x # 블록 모두 이어짐 + by = {h["번"]: h for h in bong["호표"]} + assert by[13]["근거글"] == ["건설표준품셈 7-2-2(찰붙임)"] # 코드 없는 줄 = 호표 근거글 + assert hb.sources(" ".join(by[13]["근거글"])) == [["건설품셈", "7-2-2"]] + + +def test_영월_줄과_블록(books): + yw = books["영월"] + one = yw["호표"][0] + assert (one["번"], one["코드"], one["이름"], one["단위"]) == ( + 1, + "B00260", + "녹생토(종자배합)", + "M3", + ) + assert one["행"] == [5, 10] and one["금액"]["계"] == 155184 + assert [(x["이름"], x["코드"], x["갈래"], x["수량"]) for x in one["줄"]] == [ + ("산림종자", "M00024", "자재", 0.25), + ("산림종자", "M00211", "자재", 0.25), + ("인공토양(녹생토)", "M00832", "자재", 1.1), + ("보통인부", "L00002", "노임", 0.43), + ] + two = {x["이름"]: x for x in yw["호표"][1]["줄"]} + assert two["국가지점번호판 검증수수료"]["갈래"] == "별도계상" # 비고 「별산」 + assert two["구조물터파기(인력)"]["근거"] == { + "출처": [["건설품셈", "3-1-3"]], + "직종": ["보통인부"], + "행": [5, 54], + } + block = next(b for b in yw["단산"] if b["코드"] == "D00002") + assert (block["번"], block["이름"], block["규격"], block["단위"]) == ( + 2, + "구조물터파기(인력)", + "경질토사", + "M3", + ) + assert block["자원"] == [ + {"갈래": "L", "코드": "L00002", "이름": "보통인부", "규격": "", "품": 0.26, "절": ""} + ] + assert any("건설표준품셈3-1-3" in t for t in block["글"]) + + +def test_기계_자원_하나로(books): + block = next(b for b in books["봉화"]["단산"] if b["코드"] == "D00002") + machines = [r for r in block["자원"] if r["갈래"] == "X"] + assert len(machines) == 1 # 노무비 · 재료비 · 경비 셋 줄 + assert machines[0]["이름"] == "굴삭기(무한궤도)" and machines[0]["규격"] == "0.2㎥" + assert machines[0]["품"] == pytest.approx(0.0636537237529) + + +def test_노임_이름_어긋남_0(books): + names = hb.trade_names() + for tag in FILES: + sheets = hx._sheets(_path(tag).name, _path(tag).read_bytes()) + table = st._lists(sheets) + res = [r for b in books[tag]["단산"] for r in b["자원"] if r["갈래"] == "L" and r["이름"]] + assert res + assert sum(1 for r in res if r["코드"] in table) >= len(res) // 2 + for r in res: + if r["코드"] in table: # 목록표에 있는 직종 — X 열 이름과 같음 + assert st._flat(r["이름"]) == st._flat(table[r["코드"]]["이름"]) + if table.get(r["코드"], {}).get("단위") == "인": # L 코드 % 줄(기구손료) 뺌 + assert st._flat(r["이름"]) in names, (tag, r) + labor = [x for x in _lines(books[tag]) if x["갈래"] == "노임"] + assert all(st._flat(x["이름"]) in names for x in labor) + + +def test_이음_열쇠_코드_칸(): + row = [None] * 26 + row[12], row[14], row[15] = "별산M00985", "M00985_1", "T7_1" + assert st._code(row) == "M00985" + row = [None] * 26 + row[25] = "D01352 →" # O 열 없는 판 + assert st._code(row) == "D01352" + row = [None] * 47 + row[14], row[13] = "PRA01_1", "_x0007_`COD|PR_x0005_`EXI" + assert st._code(row) == "PRA01" + assert ( + st._code(["계", None, None, None, None, None, None, None] + [None] * 6 + ["T7_1"]) == "T7" + ) + assert st._code([None] * 12 + ["D96"]) is None # 비고 글은 코드 아님 + + +def test_문서_보안_400(client): + for tag in LOCKED: + data = _path(tag).read_bytes() + with pytest.raises(hx.ExcelError, match="문서 보안"): + hx.read_book(_path(tag).name, data) + res = _post(client, data=data, name=_path(tag).name) + assert res.status_code == 400 and res.json()["detail"] == hx.DRM + + +def test_여섯_파일_올라감(client): + before = master_hash() + sources = {tag: hashlib.sha256(_path(tag).read_bytes()).hexdigest() for tag in FILES} + for at, tag in enumerate(FILES, 1): + res = _post(client, data=_path(tag).read_bytes(), name=_path(tag).name) + assert res.status_code == 200, res.text + assert res.json()["호표수"] == HOPYOS[tag] + doc = hs.read(layers.personal_dir(7, 42), f"HP-{at:06d}") + assert doc["꼴"] == "STmate" + assert sum(1 for b in doc["단산"] if not b["글"]) <= 3 # 봉화 무대처리 · 흙 쌓 기 = 값 0 + assert any(b["자원"] for b in doc["단산"]) + first = doc["호표"][0]["번"] + view = client.get(f"{base.URL.format('personal')}/HP-{at:06d}/{first}") + assert view.status_code == 200, view.text # 맞춤 보기가 STmate 줄을 받음 + assert {tag: hashlib.sha256(_path(tag).read_bytes()).hexdigest() for tag in FILES} == sources + assert master_hash() == before From 75b8815dac6dd12a0f8162a639016d2139fd9667 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 15 Oct 2026 23:30:44 +0900 Subject: [PATCH 58/81] =?UTF-8?q?test(M02=20=EC=9D=BC=EC=9C=84=EB=8C=80?= =?UTF-8?q?=EA=B0=80):=20338-2c=20=EC=98=81=EC=9B=94=20=EC=A0=95=EB=8B=B5?= =?UTF-8?q?=20=EC=9E=90=EB=A6=AC=20y=5Fids=20=E2=80=94=20D=20=C2=B7=20X=20?= =?UTF-8?q?=EA=B3=A0=EC=9C=A0=20=EC=A4=84=2036(Y000~Y035)=20=EC=8B=A4?= =?UTF-8?q?=ED=97=98=20C1=20=EC=B0=A8=EB=A1=80=EC=99=80=20=EA=B0=99?= =?UTF-8?q?=EC=9D=8C=20=C2=B7=20=EA=B3=A8=EB=93=A0=20=EC=8B=9C=ED=97=98?= =?UTF-8?q?=EC=9A=A9?= 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_014utrjfoW831Vc5wJUd13kL --- resources/tester/test_m02_hopyo_stmate.py | 34 ++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/resources/tester/test_m02_hopyo_stmate.py b/resources/tester/test_m02_hopyo_stmate.py index 82ad43569..671a2daf9 100644 --- a/resources/tester/test_m02_hopyo_stmate.py +++ b/resources/tester/test_m02_hopyo_stmate.py @@ -3,7 +3,8 @@ 여섯 파일 올라감(봉화 45 · 영월 23 · 울진대흥 설계 · 착공 34 · 영덕 9 · 울진소광 25 호표) · 봉화 줄 216(구성 209 + 근거글 7) · 단산 103 · 노임 30 · 호표 22 · 줄 코드 갈래 · 노임 이름 어긋남 0 · 블록 자원 품 · 이음 열쇠(M~Z 열 코드 꼴 첫 것) · -문서 보안(장수 · 진안) 400 글 · 원본 엑셀 · master_data 그대로. +문서 보안(장수 · 진안) 400 글 · 원본 엑셀 · master_data 그대로 · +영월 정답 자리(`y_ids` — 338 실험 C1 `labels_y` Y000~Y035 = D · X 고유 줄 차례 · 골든 시험이 씀). """ import hashlib @@ -183,3 +184,34 @@ def test_여섯_파일_올라감(client): assert view.status_code == 200, view.text # 맞춤 보기가 STmate 줄을 받음 assert {tag: hashlib.sha256(_path(tag).read_bytes()).hexdigest() for tag in FILES} == sources assert master_hash() == before + + +def y_ids(book: dict) -> dict[str, dict]: + """영월 정답 자리 — D · X 줄을 (이름, 규격, 단위, 코드) 고유로 처음 나온 차례 → + `{"Y000": {호표, 차례, 열쇠}}`(338 실험 C1 `s09_stmate` 와 같은 번).""" + out: dict[str, dict] = {} + seen: set[tuple] = set() + for hopyo in book["호표"]: + for line in hopyo["줄"]: + key = (line["이름"], line["규격"], line["단위"], line["코드"]) + if line["코드"][0] in "DX" and key not in seen: + seen.add(key) + out[f"Y{len(out):03d}"] = {"호표": hopyo["번"], "차례": line["차례"], "열쇠": key} + return out + + +def test_영월_정답_자리(books): + got = y_ids(books["영월"]) + assert len(got) == 36 # 정답 34 + 우리에 없음 둘(Y004 · Y007) + assert got["Y000"] == { + "호표": 2, + "차례": 1, + "열쇠": ("무근콘크리트타설", "인력운반타설", "M3", "D02058"), + } + assert got["Y005"]["열쇠"] == ("크레인(타이어)", "10톤", "시간", "X02104") + assert got["Y010"]["열쇠"][3] == "D00060" and got["Y035"]["열쇠"][3] == "D01810" + by = {h["번"]: h for h in books["영월"]["호표"]} + for one in got.values(): + line = by[one["호표"]]["줄"][one["차례"]] + assert (line["이름"], line["규격"], line["단위"], line["코드"]) == one["열쇠"] + assert line["갈래"] == "기계" or "근거" in line # 단산 줄은 블록 이음 From e9e949112c98126418ee4ad20401fdbabf4cd614 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 15 Oct 2026 23:36:23 +0900 Subject: [PATCH 59/81] =?UTF-8?q?feat(=EC=9D=BC=EC=9C=84=EB=8C=80=EA=B0=80?= =?UTF-8?q?=EC=8B=A4=EB=AC=B4):=20338-1=20=EB=A7=9E=EC=B6=A4=20=ED=95=B5?= =?UTF-8?q?=EC=8B=AC=20=EC=84=9C=EB=B2=84=201=20=EB=8B=A8=EA=B3=84=20?= =?UTF-8?q?=E2=80=94=20=EC=97=91=EC=85=80=20=EC=A7=80=EB=AC=B8=20=C2=B7=20?= =?UTF-8?q?=EC=9A=B0=EB=A6=AC=20=EB=A1=9C=EC=A7=81=20=EC=A7=80=EB=AC=B8=20?= =?UTF-8?q?=EC=BA=90=EC=8B=9C=20=C2=B7=20=EC=98=B5=EC=85=98=20=EA=B1=B0?= =?UTF-8?q?=EA=BE=B8=EB=A1=9C=20=C2=B7=20=EC=88=9C=EC=9C=84=20=C2=B7=20?= =?UTF-8?q?=ED=8C=90=EC=A0=95=20=C2=B7=20=EC=A1=B0=ED=95=A9=20=C2=B7=20?= =?UTF-8?q?=EB=92=A4=20=EC=85=88=20+=20=EC=A0=95=EB=8B=B5=20=ED=91=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 새 파일 — Words(식 풀기 · 장비 갈래 · 조건 낱말) · Finger(블록 글 · STmate 자원 → 지문 · 기계 줄 · 직접 줄 호표) · Shares(절 몫 벡터) · Ours(마스터 판 열쇠 · 우리 지문 캐시 tmp/m02_hopyo · 엔진 셈 캐시 · 자재 값 찾기 건너뜀) · Option(옵션 공간 · 거꾸로 찾기 · 견줌) · Fit(손잡이 한 곳 · 순위 · 뒤 셈 · 맞춤셈 캐시) · Combine(조합 · 섞인 줄 몫 먼저) · Verdict(블록 셈 · 맞음 / 다른 점 / 후보) - Match — b_order(이름 · 근거 차례 · 같음 근거 검사 · 옛 판 글 · 조항 오타) · clause(윗마디 0.9 · 나 창 Basis.clause 있으면 그것) · 이름 후보 로직은 접지 않음 - 정답 표 fixtures/m02_hopyo_golden.json(사람 136 · 새 파일 51) · 골든 도우미 - 북부청 92 줄 실측 — 1순위 87 · 다섯 안 91 · 맞음 41 틀림 0 · 다른 점 28 틀림 0 · 조합 맞음 57/66 · 우리 지문 셈 37 초 · 캐시 읽기 0.05 초 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Ji4nuyLVHcxrXWHTdb5nRD --- M02_MasterTemplete/M02_Hopyo_Combine.py | 300 ++ M02_MasterTemplete/M02_Hopyo_Finger.py | 309 ++ M02_MasterTemplete/M02_Hopyo_Fit.py | 411 ++ M02_MasterTemplete/M02_Hopyo_Match.py | 82 +- M02_MasterTemplete/M02_Hopyo_Option.py | 497 +++ M02_MasterTemplete/M02_Hopyo_Ours.py | 364 ++ M02_MasterTemplete/M02_Hopyo_Shares.py | 177 + M02_MasterTemplete/M02_Hopyo_Verdict.py | 200 + M02_MasterTemplete/M02_Hopyo_Words.py | 247 ++ .../tester/fixtures/m02_hopyo_golden.json | 3380 +++++++++++++++++ resources/tester/helper_m02_hopyo_golden.py | 254 ++ resources/tester/test_m02_hopyo_finger.py | 147 + resources/tester/test_m02_hopyo_fit.py | 122 + resources/tester/test_m02_hopyo_option.py | 63 + resources/tester/test_m02_hopyo_ours.py | 77 + 15 files changed, 6629 insertions(+), 1 deletion(-) create mode 100644 M02_MasterTemplete/M02_Hopyo_Combine.py create mode 100644 M02_MasterTemplete/M02_Hopyo_Finger.py create mode 100644 M02_MasterTemplete/M02_Hopyo_Fit.py create mode 100644 M02_MasterTemplete/M02_Hopyo_Option.py create mode 100644 M02_MasterTemplete/M02_Hopyo_Ours.py create mode 100644 M02_MasterTemplete/M02_Hopyo_Shares.py create mode 100644 M02_MasterTemplete/M02_Hopyo_Verdict.py create mode 100644 M02_MasterTemplete/M02_Hopyo_Words.py create mode 100644 resources/tester/fixtures/m02_hopyo_golden.json create mode 100644 resources/tester/helper_m02_hopyo_golden.py create mode 100644 resources/tester/test_m02_hopyo_finger.py create mode 100644 resources/tester/test_m02_hopyo_fit.py create mode 100644 resources/tester/test_m02_hopyo_option.py create mode 100644 resources/tester/test_m02_hopyo_ours.py diff --git a/M02_MasterTemplete/M02_Hopyo_Combine.py b/M02_MasterTemplete/M02_Hopyo_Combine.py new file mode 100644 index 000000000..25f3c4450 --- /dev/null +++ b/M02_MasterTemplete/M02_Hopyo_Combine.py @@ -0,0 +1,300 @@ +"""M02 일위대가 맞춤 — 조합 풀이(PLAN 338-1 · 설계 2-7): 블록 몫(절마다 노임 · 장비 벡터)을 우리 +로직 단위당 벡터 조합으로 풂 · 섞인 줄은 인력 몫 로직이 1순위(몫 먼저). + +맞음 = 차원(직종 · 장비 갈래)마다 작은/큰 가중 평균(보통인부 0.5 · 장비 있음 0.5 + 시간 0.5 · +크기 다르면 × 0.8 · 갈래 일부만 같으면 × 0.8) · 몫 x(0.1 … 0.9) × 0.95 · 제목 몫 수와 같은 x 면 ++0.1. +덧 로직 = 1순위가 못 덮는(< COVER) 절은 절 1위(≥ 0.75) · 아니면 몫마다(≥ 0.6) · 장비만 든 몫 = +「기계 시간당 기계경비」(체크 안 함 · 시간 = 엑셀 값). +순위 덤 = 앞 차례 0.3 · 인용 조항 같음 0.3 · 같은 책 0.1 · 후보 풀 = 앞 차례 15 + 드문 직종 · 장비 +색인 +(40 개 이하) + 절 제목 이름 후보 8. 우리 벡터 = `M02_Hopyo_Ours` 지문 변형 L · X(따로 안 셈). +규칙 · 문턱은 336 실험(C5 `c03_vec` · `c05_solve` · C7 `solve`) 그대로 · 문턱은 `M02_Hopyo_Fit` +머리. +""" + +from __future__ import annotations + +import re +from typing import Any + +from M02_MasterTemplete import M02_Hopyo_Basis as hopyo_basis +from M02_MasterTemplete import M02_Hopyo_Fit as fit +from M02_MasterTemplete import M02_Hopyo_Match as hopyo_match +from M02_MasterTemplete import M02_Hopyo_Ours as ours + +COMMON = "보통인부" +MACHINE = {"굴착기": "GC000267"} # 굴착기(무한궤도) 시간당 기계경비 · 그 밖 장비 = GC000268 +MACHINE_ANY = "GC000268" +MACHINE_SET = {"GC000267", "GC000268", "GC000269", "GC000275"} +_SHARE = re.compile(r"(\d{1,3})\s*%") +_RATIO = re.compile(r"(\d)\s*:\s*(\d)") +_STATE: dict[str, Any] = {"판": None} + + +# ── 우리 벡터 · 색인 ─────────────────────────────────────────────────── +def _vec(v: dict[str, Any]) -> dict[str, Any]: + eq = [(frozenset(a), frozenset(b), q) for a, b, q in v.get("X") or [] if a and q] + return {"노임": {t: q for t, q in (v.get("L") or {}).items() if q}, "장비": eq} + + +def _load() -> dict[str, Any]: + """{변형: {로직: [(입력, 벡터)]}, 색인: {차원: {로직}}} — 우리 지문 판마다 한 번.""" + table = ours.table() or [] + ver = ours.version() + if _STATE["판"] != (ver, id(table)): + var: dict[str, list] = {} + idx: dict[str, set] = {} + for o in table: + vs = [(x.get("in") or {}, _vec(x)) for x in o.get("변형") or []] + var[o["로직"]] = [(g, v) for g, v in vs if v["노임"] or v["장비"]] + if var[o["로직"]]: + for d in dims(var[o["로직"]][0][1]): + idx.setdefault(d, set()).add(o["로직"]) + _STATE.update(판=(ver, id(table)), 변형=var, 색인=idx) + return _STATE + + +def variants(code: str) -> list[tuple[dict, dict]]: + return _load()["변형"].get(code) or [] + + +def _kinds(key: str) -> tuple[frozenset, str]: + kind, _, size = key.partition("|") + return frozenset(kind.split("+")), size + + +def dims(E: dict[str, Any]) -> set[str]: + out = {t for t in E.get("노임") or {} if t != COMMON} + eq = E.get("장비") or {} + if isinstance(eq, dict): + return out | {"+".join(sorted(_kinds(k)[0])) for k in eq} + return out | {"+".join(sorted(a)) for a, _, _ in eq} + + +def sim(E: dict[str, Any], ov: dict[str, Any], x: float = 1.0) -> float: + """E = 엑셀 몫 {노임, 장비: {「굴착기|0.6」: 시간}} · ov = 우리 벡터 — 0..1.""" + num = den = 0.0 + et, ot = E.get("노임") or {}, ov.get("노임") or {} + for t in set(et) | set(ot): + w = 0.5 if t == COMMON else 1.0 + a, b = et.get(t, 0.0), ot.get(t, 0.0) * x + den += w + num += w * min(a, b) / max(a, b) if a > 0 and b > 0 else 0 + oe = ov.get("장비") or [] + used = set() + for k, a in (E.get("장비") or {}).items(): + kinds, size = _kinds(k) + best, pick = 0.0, None + for at, (kinds2, sizes2, b) in enumerate(oe): + if not kinds & kinds2: + continue + f = 1.0 if (not size or not sizes2 or float(size) in sizes2) else 0.8 + f *= 1.0 if kinds == kinds2 else 0.8 # 굴착기 ↔ 굴착기+집게 + b *= x + s = f * (0.5 + 0.5 * (min(a, b) / max(a, b) if a > 0 and b > 0 else 0)) + if s > best: + best, pick = s, at + den += 1.0 + num += best + if pick is not None: + used.add(pick) + den += sum(1.0 for at in range(len(oe)) if at not in used) + return num / den if den else 0.0 + + +def fit_one(E: dict[str, Any], code: str, share: bool) -> tuple[float, dict, float]: + """(맞음, 입력, 몫 x) — 변형마다 x = 1 과 몫 x 중 가장 좋은 것.""" + top = (0.0, {}, 1.0) + for given, ov in variants(code): + s = sim(E, ov) + if s > top[0]: + top = (s, given, 1.0) + if share: + for x in fit.SHARES: + s2 = 0.95 * sim(E, ov, x) + if s2 > top[0]: + top = (s2, given, x) + return top + + +def cover(E: dict[str, Any], code: str) -> float: + """E 의 차원을 로직 벡터가 얼마나 덮나(로직의 남는 차원은 안 셈).""" + top = 0.0 + for _, ov in variants(code): + num = den = 0.0 + for t, a in (E.get("노임") or {}).items(): + w = 0.5 if t == COMMON else 1.0 + b = (ov.get("노임") or {}).get(t, 0.0) + den += w + num += w * (min(a, b) / max(a, b) if a > 0 and b > 0 else 0) + for k in E.get("장비") or {}: + den += 1 + num += any(_kinds(k)[0] & a for a, _, _ in ov.get("장비") or []) + top = max(top, num / den if den else 0) + return top + + +# ── 후보 풀 · 앞선 믿음 ──────────────────────────────────────────────── +def title_cands(title: str, unit: str, got: dict, n: int = 8) -> list[str]: + text = re.sub(r"\s+", "", re.sub(r"[\d.·:()\[\]%]+", " ", str(title or ""))) + if len(text) < 2: + return [] + found = hopyo_match.candidates({"이름": text, "단위": unit}, "로직", got, n) + return [c["로직"] for c in found if c.get("로직")] + + +class Prior: + """앞선 믿음 — 앞 차례 · 줄 근거 조항(윗마디 포함) · 같은 책.""" + + def __init__(self, line: dict[str, Any], ranked: list[str]): + self.rank = {c: k for k, c in enumerate(ranked)} + self.n = max(len(ranked), 1) + self.theirs = [list(x) for x in ((line.get("근거") or {}).get("출처") or [])] + self.books = {b for b, _ in self.theirs} + self.table = hopyo_basis.ours() + + def __call__(self, code: str) -> float: + p = 0.3 * (1 - self.rank[code] / self.n) if code in self.rank else 0.0 + our = self.table.get(code) + if our and self.theirs: + p += 0.3 if hopyo_match.clause(self.theirs, our)[0] >= 0.9 else 0 + p += 0.1 if our["책"] in self.books else 0 + return p + + +def _sum(parts: list[dict]) -> dict[str, Any]: + out: dict[str, dict] = {"노임": {}, "장비": {}} + for s in parts: + for k in ("노임", "장비"): + for d, q in (s.get(k) or {}).items(): + out[k][d] = out[k].get(d, 0) + q + return out + + +def pool_for(E: dict[str, Any], base: list[str], extra: list[str]) -> list[str]: + idx = _load()["색인"] + rare: set[str] = set() + for d in dims(E): + if len(idx.get(d, ())) <= 40: + rare |= idx.get(d, set()) + return list(dict.fromkeys([*base, *extra, *sorted(rare)])) + + +def _hint(title: Any) -> float | None: + m = _SHARE.search(str(title or "")) + return int(m.group(1)) / 100 if m and 0 < int(m.group(1)) < 100 else None + + +def ranked(E: dict, pool: list[str], prior: Prior, share: bool, hint: float | None) -> list[tuple]: + """[(순위 점수, 맞음, 몫 x, 로직, 입력)] — 순위 차례 · 제목 몫과 같은 x 면 +0.1.""" + out = [] + for c in pool: + f, given, x = fit_one(E, c, share) + if f <= 0: + continue + score = f + prior(c) + if hint is not None: + score += 0.1 if abs(x - hint) < 1e-9 else -0.1 + out.append((score, f, x, c, given)) + out.sort(key=lambda z: -z[0]) + return out + + +def is_mixed(parts: list[dict], line: dict[str, Any]) -> bool: + """섞인 줄 — 제목 · 이름 · 규격에 몫 표시 + 노임만 든 몫 + 장비만 든 몫.""" + text = " ".join( + [str(p.get("제목")) for p in parts] + [str(line.get(k)) for k in ("이름", "규격")] + ) + marked = bool(_SHARE.search(text) or _RATIO.search(text)) + man = any(p.get("노임") and not p.get("장비") for p in parts) + mach = any(p.get("장비") and not p.get("노임") for p in parts) + return marked and man and mach + + +def machine_of(E: dict[str, Any]) -> str: + kinds = {frozenset(_kinds(k)[0]) for k in E.get("장비") or {}} + return MACHINE["굴착기"] if kinds == {frozenset({"굴착기"})} else MACHINE_ANY + + +# ── 풀이 ─────────────────────────────────────────────────────────────── +def combine( + primary: str | None, parts: list[dict], line: dict, got: dict, base: list[str] +) -> list[dict[str, Any]]: + """[{로직, 어디, 맞음, 몫, 단계, 입력}] — 1순위 + 1순위가 못 덮는 절 · 몫의 로직.""" + one = lambda c, w, f, x, step, g=None: { # noqa: E731 + "로직": c, + "어디": w, + "맞음": f, + "몫": x, + "단계": step, + "입력": g or {}, + } + chosen = [one(primary, "1순위", None, 1.0, "1순위")] if primary else [] + if not parts: + return chosen + prior, unit = Prior(line, base), str(line.get("단위") or "") + mixed = is_mixed(parts, line) + groups: dict[str, list] = {} + for p in parts: + groups.setdefault(str(p.get("절") or p.get("제목") or ""), []).append(p) + have = {primary} if primary else set() + + def machine(E: dict, where: str) -> None: + if mixed or len(groups) > 1: + m = machine_of(E) + chosen.append(one(m, where, None, 1.0, "장비 몫")) + have.add(m) + + for title, ps in groups.items(): + E = _sum(ps) + if primary and cover(E, primary) >= fit.COVER: + continue + if E["장비"] and not E["노임"]: # 장비만 든 절 = 기계 시간 로직(닫힌 세계 규칙) + machine(E, title) + continue + share = any(_hint(p.get("제목")) for p in ps) or _hint(title) is not None + pool = pool_for(E, base, title_cands(title, unit, got)) + cand = ranked(E, pool, prior, share, _hint(title)) + if cand and cand[0][1] >= fit.COMBO[1] and cand[0][3] not in have: + chosen.append( + one(cand[0][3], title, round(cand[0][1], 3), cand[0][2], "절", cand[0][4]) + ) + have.add(cand[0][3]) + continue + for p in ps: + if primary and cover(p, primary) >= fit.COVER: + continue + if p.get("장비") and not p.get("노임"): + machine(p, p.get("제목")) + continue + hint = _hint(p.get("제목")) + pool = pool_for(p, base, title_cands(p.get("제목"), unit, got)) + cand = ranked(p, pool, prior, hint is not None or share, hint) + if cand and cand[0][1] >= fit.COMBO[2] and cand[0][3] not in have: + f, x, c, g = round(cand[0][1], 3), cand[0][2], cand[0][3], cand[0][4] + chosen.append(one(c, p.get("제목"), f, x, "몫", g)) + have.add(c) + return chosen + + +def solve( + top: list[str], parts: list[dict], line: dict, got: dict, base: list[str] +) -> tuple[list[str], list[dict[str, Any]]]: + """(새 차례, 조합 대상) — 섞인 줄이고 1순위가 블록 합을 MIXED_CUT 못 덮으면 1순위를 인력 몫 + 로직으로 바꿈(몫 먼저) · 조합이 하나뿐이면 대상 빈 것.""" + chosen = combine((top or [None])[0], parts, line, got, base) + codes = list(dict.fromkeys(c["로직"] for c in chosen if c["로직"])) + if top and len(codes) > 1 and parts and is_mixed(parts, line): + if cover(_sum(parts), top[0]) < fit.MIXED_CUT: + labor = [c for c in chosen if c["로직"] != top[0] and c["단계"] in ("몫", "절")] + if labor: + chosen = [c for c in chosen if c["로직"] != top[0]] + top = [labor[0]["로직"]] + [c for c in top if c != labor[0]["로직"]][:4] + seen: set[str] = set() + chosen = [ + c for c in chosen if not (c["로직"] in seen or seen.add(c["로직"])) + ] # 같은 로직은 처음 것 + if len(chosen) < 2: + return top, [] + return top, [{**c, "체크": c["로직"] not in MACHINE_SET or c["로직"] == top[0]} for c in chosen] diff --git a/M02_MasterTemplete/M02_Hopyo_Finger.py b/M02_MasterTemplete/M02_Hopyo_Finger.py new file mode 100644 index 000000000..b5ee84a45 --- /dev/null +++ b/M02_MasterTemplete/M02_Hopyo_Finger.py @@ -0,0 +1,309 @@ +"""M02 일위대가 실무자료 엑셀 지문(PLAN 338-1 · 설계 2-2) — 호표 줄 + 이어진 단가산출 블록 → +직종 품 · 장비 시간 · 자재 수량 · 율 · 계수 · 조건 · 조항 · 몫. 금액 안 씀(식 맨 앞 단가는 뗌). + +엑셀꼴 = 블록 `글`(머리 뺀 몸 줄)을 줄마다 풂 — 절 `^\\d+\\.` · 작은 절 `^[가-하]\\.` · +직종 줄 「직종 : 단가 × 식 = 금액」(우리 인력 이름 · 옛이름) · 장비 = 「노무비 · 경비 · 재료비 : +단가 ÷ Q」 ++ 지금 장비(노무비 > 경비 > 재료비) · 자재 = 「이름 단가 × 수량 = 금액」 · 「: 별도계상」 · +못 풀면 금액 ÷ 단가(「역산」 · 품 되찾기만). +STmate = 블록 `자원`(L · X · M · S) 품이 곧 품(노무비 · 재료비 · 경비 세 줄 같은 품은 한 번) · +섞음 = 이름 · 규격 몫 둘(작은 쪽 인력 · 큰 쪽 장비). +몫(조합용 · 설계 2-7) = 맨 윗 절마다 노임 · 장비 벡터(`M02_Hopyo_Shares`). +블록 없는 기계(hr) 줄 = `machine` · 노임 · 장비를 바로 적은 호표 = `direct`. +값 · 규칙은 336 실험(C4 `s1_excel_fp` · K5 `stmate`) 그대로. +""" + +from __future__ import annotations + +import re +from collections import Counter +from typing import Any + +from M02_MasterTemplete import M02_Hopyo_Basis as hopyo_basis +from M02_MasterTemplete import M02_Hopyo_Shares as shares +from M02_MasterTemplete.M02_Hopyo_Shares import trade +from M02_MasterTemplete.M02_Hopyo_Words import conds, mtypes, parse_rhs, specnums + +COST = {"노무비", "재료비", "경비"} +SKIPW = { + "소계", + "계", + "합계", + "인부소계", + "자재소계", + "주자재소계", + "운반", + "상차", + "기계", + "다짐", + "적사", + "살수", + "집토", + "암절취", + "채집", + "인력", + "장비", + "작업조건", + "1일시공량", + "작업면적", + "1시간작업량", + "기계채집", + "구조물터파기", +} +_COLON = re.compile( + r"^\s*(?:\d+\)|[0-9가-힣]\.)?\s*(?P[^::=]{1,40}?)\s*[::]\s*(?P.*)$" +) +_MAT = re.compile( + r"^\s*(?P[^\d\s::=][^::=]*?[A-Za-z가-힣)\]])\s+(?P[\d,]+(?:\.\d+)?\s*[×xX*÷/].*=.*)$" +) +_PCT = re.compile(r"([가-힣]+)의\s*([\d.]+)\s*%") +_COEF = re.compile( + r"(? str: + return re.sub(r"\s+", "", str(s or "")) + + +def _ratio(rhs: str) -> float | None: + """식을 못 풀 때만 — 금액 ÷ 단가(품 되찾기용).""" + m = re.match(r"^\s*([\d,]+(?:\.\d+)?)", rhs) + n = _LASTNUM.search(rhs) + if not m or not n: + return None + p, a = float(m.group(1).replace(",", "")), float(n.group(1).replace(",", "")) + return a / p if p else None + + +def rates_in(text: Any) -> set[float]: + """글 속 섞음 비율 후보 — (N%) · 8:2 · 9:1.""" + t = str(text or "") + out = {round(float(n) / 100, 4) for n in re.findall(r"(\d+(?:\.\d+)?)\s*%", t)} + out = {v for v in out if 0 < v < 1} + for a, b in re.findall(r"(\d+)\s*:\s*(\d+)", t): + a, b = float(a), float(b) + if a and b and a + b <= 20: + out |= {round(a / (a + b), 4), round(b / (a + b), 4)} + return out + + +def empty(line: dict[str, Any], block: dict[str, Any] | None = None) -> dict[str, Any]: + block = block or {} + basis = line.get("근거") or {} + name, spec = str(line.get("이름") or ""), str(line.get("규격") or "") + return { + "이름": name, + "규격": spec, + "단위": str(line.get("단위") or ""), + "출처": [list(x) for x in block.get("출처") or basis.get("출처") or []], + "인력": {}, # 직종: {품, 밑품, 율, 어떻게} + "장비": [], # {갈래, 규격, 시간, 밑시간, 율, 절, 글} + "자재": [], # {이름, 품, 밑품} + "율": [], # [바탕, %] + "계수": {}, + "조건": {}, + "이름조건": {k: sorted(v) for k, v in conds(f"{name} {spec}").items()}, + "옛판": False, + "몫": [], + } + + +def _labor(fp: dict, name: str, q: float, base: float, rates: list, how: str) -> None: + one = fp["인력"].setdefault(name, {"품": 0.0, "밑품": 0.0, "율": [], "어떻게": []}) + one["품"] += q + one["밑품"] += base + one["율"] += rates + one["어떻게"].append(how) + + +def _round(fp: dict[str, Any]) -> dict[str, Any]: + for one in fp["인력"].values(): + one["품"], one["밑품"] = round(one["품"], 6), round(one["밑품"], 6) + return fp + + +def excel(block: dict[str, Any] | None, line: dict[str, Any]) -> dict[str, Any]: + """HP 블록(설계 3-1) + 그 줄(이름 · 규격 · 단위) → 지문 · 블록이 없거나 글 · 자원이 없으면 + 기계(hr) 줄 지문(장비 1 시간) 또는 빈 지문.""" + if block and block.get("자원"): + return _round(_stmate(block, line)) + if block and block.get("글"): + return _round(_text(block, line)) + return machine(line) + + +def machine(line: dict[str, Any]) -> dict[str, Any]: + """블록 없는 기계(hr) 줄 → 장비 [{갈래, 규격, 시간 1}].""" + fp = empty(line) + text = f"{fp['이름']} {fp['규격']}" + types = mtypes(text) + if types: + hours = 1.0 if _HOURS.search(fp["단위"]) else None + one = {"갈래": sorted(types), "규격": sorted(specnums(text)), "시간": hours} + fp["장비"] = [{**one, "밑시간": None, "율": [], "절": "0", "글": fp["이름"]}] + fp["조건"] = fp["이름조건"] + return fp + + +def _text(block: dict[str, Any], line: dict[str, Any]) -> dict[str, Any]: + """엑셀꼴 — 블록 글 줄마다 풂(C4 `north`).""" + fp = empty(line, block) + head = f"{block.get('번', '')}.{block.get('이름', '')} [{block.get('단위', '')}]" + text = [head, *[str(t) for t in block.get("글") or []]] + front = [t for t in text[:5]] + [t for t in text if "※" in t or "작업조건" in t] + head_text = " ".join([fp["이름"], fp["규격"], *front]) + fp["조건"] = {k: sorted(v) for k, v in conds(head_text).items()} + fp["옛판"] = bool(OLD.search(" ".join(text[:6]))) + sec, sub, mach, label = 0, "", None, "" + got_cost: dict[tuple, tuple] = {} + item_rates = rates_in(f"{fp['이름']} {fp['규격']}") + sec_rates: set[float] = set() + + def unmix(got: dict | None) -> dict | None: + """율 메모가 없는데 맨 인자가 섞음 비율 후보와 같으면 율로 가름.""" + if got and not got["율"]: + for v in got.get("인자") or []: + if round(v, 4) in item_rates | sec_rates and 0 < v < 1: + return {**got, "밑품": got["밑품"] / v, "율": [[round(v, 4), "섞음"]]} + return got + + for raw in text[1:]: + row = raw.rstrip() + h, s = _HEAD.match(row), _SUB.match(row) + if h or s: + if h: + sec, sub = int(h.group(1)), "" + sec_rates = rates_in(h.group(2)) + else: + sub = s.group(1) + sec_rates = sec_rates | rates_in(s.group(2)) + title = (h or s).group(2) + ts = mtypes(title) + mach = (ts, specnums(title)) if ts else None + label = title.strip() if ts else "" + for k, v in _PCT.findall(row): + fp["율"].append([k, float(v)]) + cm = _COLON.match(row) + name = re.sub(r"^[0-9가-힣]\.", "", _flat(cm.group("name"))) if cm else "" + who = hopyo_basis.trade_names().get(name) if cm else None + if who: + rhs = cm.group("rhs") + got = unmix(parse_rhs(rhs)) + if got: + _labor(fp, who, got["품"], got["밑품"], got["율"], "식") + elif r := _ratio(rhs): + _labor(fp, who, r, r, [], "역산") + continue + if cm and name in COST: + got = unmix(parse_rhs(cm.group("rhs"))) + if got and mach: + key = (sec, sub, label) + prio = 0 if (name == "노무비" and got["단가"] > 0) else 1 if name == "경비" else 2 + if key not in got_cost or prio < got_cost[key][0]: + got_cost[key] = (prio, mach, got) + continue + if cm and name not in SKIPW and re.search(r"(별도|별산)", cm.group("rhs")): + fp["자재"].append({"이름": cm.group("name").strip(), "품": None, "밑품": None}) + continue + mm = _MAT.match(row) + if mm and not cm and (got := parse_rhs(mm.group("rhs"))): + fp["자재"].append( + {"이름": mm.group("name").strip(), "품": got["품"], "밑품": got["밑품"]} + ) + continue + if not (h or s): + ts = mtypes(row) + if ts and "%" not in row and not cm: + mach, label = (ts, specnums(row)), row.strip() + for k, v in _COEF.findall(row): + fp["계수"].setdefault(k, float(v)) + for (sec_, sub_, lab), (_, (ts, sp), got) in got_cost.items(): + where = f"{sec_}.{sub_}" if sub_ else str(sec_) + one = {"갈래": sorted(ts), "규격": sorted(sp), "시간": got["품"], "밑시간": got["밑품"]} + fp["장비"].append({**one, "율": got["율"], "절": where, "글": lab}) + fp["몫"] = shares.sections(text[1:]) + return fp + + +def _stmate(block: dict[str, Any], line: dict[str, Any]) -> dict[str, Any]: + """STmate — 자원 품이 곧 품(K5 `stmate`).""" + fp = empty(line, block) + text = [str(t) for t in block.get("글") or []][:60] + fp["조건"] = { + k: sorted(v) for k, v in conds(" ".join([fp["이름"], fp["규격"], *text[:4]])).items() + } + fp["옛판"] = bool(OLD.search(" ".join(text[:4]))) + for t in text: + for k, v in _COEF.findall(t): + fp["계수"].setdefault(k, float(v)) + for k, v in _PCT.findall(t): + fp["율"].append([k, float(v)]) + rates = rates_in(f"{fp['이름']} {fp['규격']}") + lab_rate = min(rates) if len(rates) >= 2 else None + mach_rate = max(rates) if len(rates) >= 2 else None + seen: Counter = Counter() + for x in block.get("자원") or []: + kind, q = x.get("갈래"), x.get("품") + nm, sp = str(x.get("이름") or ""), str(x.get("규격") or "") + if not q: + continue + seen[(x.get("코드"), round(float(q), 9))] += 1 + if seen[(x.get("코드"), round(float(q), 9))] > 1: # 노무비 · 재료비 · 경비 세 줄 같은 품 + continue + q = float(q) + ts = mtypes(f"{nm} {sp}") + if kind == "L": + if who := trade(nm): + base = q / lab_rate if lab_rate else q + _labor(fp, who, q, base, [[lab_rate, "섞음"]] if lab_rate else [], "QTY") + elif kind == "X" or (kind == "S" and ts): + one = {"갈래": sorted(ts), "규격": sorted(specnums(sp) | specnums(nm)), "시간": q} + base = q / mach_rate if mach_rate else q + fp["장비"].append({**one, "밑시간": base, "율": [], "절": "0", "글": f"{nm} {sp}"}) + elif kind in ("M", "S"): + fp["자재"].append({"이름": f"{nm} {sp}".strip(), "품": q, "밑품": q}) + fp["몫"] = shares.sections_stm(block.get("자원") or [], text) + return fp + + +def direct(hopyo: dict[str, Any]) -> dict[str, Any] | None: + """노임 · 장비를 바로 적은 호표 → 줄 수량 합 지문(호표 1 단위) · 근거글 출처 — 없으면 None.""" + lines = hopyo.get("줄") or [] + fp = empty({k: hopyo.get(k) for k in ("이름", "규격", "단위")}) + texts = [str(t) for t in hopyo.get("근거글") or []] + texts += [f"{x.get('이름')} {x.get('규격')}" for x in lines if x.get("갈래") == "근거글"] + fp["출처"] = hopyo_basis.sources(" ".join(texts)) + hit = False + for x in lines: + q = x.get("수량") + if not isinstance(q, (int, float)) or q <= 0: + continue + if kind_of(x) == "노임" and (who := trade(x.get("이름"))): + _labor(fp, who, float(q), float(q), [], "QTY") + hit = True + elif kind_of(x) == "기계": + text = f"{x.get('이름')} {x.get('규격')}" + if ts := mtypes(text): + one = {"갈래": sorted(ts), "규격": sorted(specnums(text)), "시간": float(q)} + fp["장비"].append({**one, "밑시간": float(q), "율": [], "절": "0", "글": text}) + hit = True + return _round(fp) if hit and fp["인력"] else None + + +def kind_of(line: dict[str, Any]) -> str: + """줄 갈래 + 「노임」(단위 인 + 우리 직종 · 나 창 읽기 전에도).""" + kind = str(line.get("갈래") or "") + unit = _flat(line.get("단위")) + if kind in ("기타", "자재", "노임") and unit in TRADE_UNITS and trade(line.get("이름")): + return "노임" + return kind diff --git a/M02_MasterTemplete/M02_Hopyo_Fit.py b/M02_MasterTemplete/M02_Hopyo_Fit.py new file mode 100644 index 000000000..3d74d5389 --- /dev/null +++ b/M02_MasterTemplete/M02_Hopyo_Fit.py @@ -0,0 +1,411 @@ +"""M02 일위대가 맞춤 — 순위 · 판정 · 블록 셈 · 뒤 셈 · 맞춤셈 캐시(PLAN 338-1 · 설계 2-4 · 2-6 · +2-10). + +순위 = 로직마다 가장 닮은 변형의 지문 점수(인력 · 장비 · 자재 · 율 · 단위 · 조건 · 조항 · 이름 · +금액 없음) +→ 지문 위 POOL ∪ 이름 · 근거 차례(`Match.b_order`) 위 다섯 → 점수 + BETA × 차례 덤 → 위 다섯 · +같음(이름 같음 · 씨앗) 1위 고정 — 지문 「딱 맞음」이 다른 로직이면 지문 · 섞인 줄은 이름 · 근거 +차례로 +몫 풀이(`Combine.solve`). +판정(1위) = 맞음(옵션 수준 ≤ 1 · 엑셀 몫을 다 덮음 · 숫자 하나뿐 줄 아님) · 다른 점(옵션 수준 2 또는 +직종 묶음 같음 + 인용 조항 같음) · 후보 — 글은 숫자 사실만(엑셀 · 우리 품). +뒤 셈 = 올리기 · 보기가 `ensure` → 일감 줄 하나 · 블록 차례로 셈 · 20 블록마다 캐시 덮음 · +캐시 = `tmp/m02_hopyo/맞춤__<판16>.json`. 서버 단독 · 원본 읽기만. +손잡이(아래 상수)는 336 · K5 실험 값 그대로 — 한 파일로 다시 맞추지 않음. +""" + +from __future__ import annotations + +import hashlib +import json +import queue +import threading +from pathlib import Path +from typing import Any + +from common_util.common_util_json import atomic_write_json +from M02_MasterTemplete import M02_Hopyo_Basis as hopyo_basis +from M02_MasterTemplete import M02_Hopyo_Finger as finger +from M02_MasterTemplete import M02_Hopyo_Match as hopyo_match +from M02_MasterTemplete import M02_Hopyo_Ours as ours +from M02_MasterTemplete import M02_Hopyo_Store as hopyo_store +from M02_MasterTemplete.M02_Hopyo_Words import close, cond_score, cosine, jacc, nfkc + +# ── 손잡이(한 곳) ────────────────────────────────────────────────────── +W = { + "인력": 3.0, + "장비": 2.0, + "자재": 1.0, + "율": 0.3, + "단위": 0.5, + "조건": 0.6, + "조항": 1.5, + "이름": 0.6, +} +BETA = 1.0 +RB = (1, 0.6, 0.45, 0.35, 0.25) +POOL = 40 +TOP_OPT = 5 +CAP = 400 +TOL = (0.005, "R", 0.03) +SURE = (0.03, 0.3) +MIXED_CUT = 0.35 +COMBO = (0.85, 0.75, 0.6) +COVER = 0.6 +SHARES = (0.1, 0.2, 0.3, 0.5, 0.7, 0.8, 0.9) +EVERY = 20 # 블록 몇 개마다 캐시 덮음 +COUNT = {"개소", "개", "본", "ea", "조", "매", "대", "기", "공"} +HOUR = {"hr", "시간", "h"} +NO_NUMBER = "숫자 판정 없음" +_STATE: dict[str, Any] = {"판": None} + + +# ── 우리 지문 정리(점수용) ───────────────────────────────────────────── +def mat_words(name: Any) -> set[str]: + t = nfkc(name).lower().replace(".", "").replace(" ", "") + out = set(hopyo_match.words(name)) | ({"pvc"} if "pvc" in t else set()) + return {w for w in out if len(w) >= 2} + + +def _prep(o: dict[str, Any], grams: dict[str, set]) -> dict[str, Any]: + """기계경비 로직은 조종원 뺌 · 모두 0 인 변형 뺌 · 섞기 전 변형 덧 · 고를 수 있는 장비.""" + vs = [] + for v in o["변형"]: + L = {} if o["기계경비"] else {t: q for t, q in v["L"].items() if q > 0} + X = [(set(a), set(b), q) for a, b, q in v["X"] if q > 0] + M = {k: q for k, q in v["M"].items() if q > 0} + if not (L or X or M) and (v["L"] or v["X"] or v["M"]): + continue + vs.append({"in": v["in"], "L": L, "X": X, "M": M}) + ri, rm = (v.get("mix") or {}).get("인력비율"), (v.get("mix") or {}).get("기계비율") + if (ri and 0 < ri < 100) or (rm and 0 < rm < 100): # 섞기 전 품(인력 · 기계 100%) + Lu = {t: q * 100 / ri for t, q in L.items()} if ri and 0 < ri < 100 else L + Xu = [(a, b, q * 100 / rm) for a, b, q in X] if rm and 0 < rm < 100 else X + vs.append({"in": {**v["in"], "_섞기전": "100%"}, "L": Lu, "X": Xu, "M": M}) + if not vs: # 엔진이 못 돈 로직 — 직종 이름만(품 모름) + X0 = [(set(a), set(b), None) for a, b in o["장비"] if a] + vs = [{"in": None, "L": {t: None for t in o["직종"]}, "X": X0, "M": {}}] + opt = [] + if o["기계경비"] and (o["장비고름"] or o["장비"]): + ts = set().union(*[set(a) for a, _ in o["장비고름"] + o["장비"]]) + sp = set().union(*[set(b) for _, b in o["장비고름"]]) if o["장비고름"] else set() + opt = [(ts, sp)] + mats = set().union(*[mat_words(n) for n in o["자재"]], set()) + for v in vs: + for n in v["M"]: + mats |= mat_words(n) + return { + "로직": o["로직"], + "단위": o["단위"], + "vs": vs, + "opt": opt, + "matw": mats, + "pctv": {round(p, 3) for _, p in o["덧줄율"]}, + "nameg": grams.get(o["로직"], set()), + "nc": {k: set(v) for k, v in o["이름조건"].items()}, + "oc": {k: set(v) for k, v in o["고름조건"].items()}, + } + + +def prepared() -> list[dict[str, Any]] | None: + """점수용 우리 지문 — 우리 지문 준비 전 None.""" + table = ours.table() + if table is None: + return None + if _STATE["판"] != id(table): + grams = {x["로직"]: x["grams"] for x in hopyo_match.targets()["로직"]} + _STATE.update(판=id(table), 표=[_prep(o, grams) for o in table]) + return _STATE["표"] + + +# ── 엑셀 지문 정리 · 점수 ────────────────────────────────────────────── +def ex_prep(efp: dict[str, Any]) -> dict[str, Any]: + return { + **efp, + "E": {t: o["품"] for t, o in efp["인력"].items() if o["품"]}, + "Eb": {t: o["밑품"] for t, o in efp["인력"].items() if o["밑품"]}, + "matl": [w for w in (mat_words(m["이름"]) for m in efp["자재"]) if w], + "pctv": {round(v, 3) for _, v in efp["율"]}, + "nameg": hopyo_match._grams(hopyo_match.tidy(efp["이름"])), + "theirs": [list(x) for x in efp["출처"]], + "조건s": {k: set(v) for k, v in efp["조건"].items()}, + "이름조건s": {k: set(v) for k, v in efp["이름조건"].items()}, + } + + +def unit_fit(a: Any, b: Any) -> bool: + """단위 맞음 + 별칭(개소 = 개 = 본 · hr = 시간) — 품 크기 견줌에만.""" + if hopyo_match.fits(a, b): + return True + na = "".join(str(a or "").split()).replace("/", "").lower() + for nb in ("".join(x.split()).replace("/", "").lower() for x in str(b or "").split("·")): + if na == nb or (na in COUNT and nb in COUNT) or (na in HOUR and nb in HOUR): + return True + return False + + +def lab_sim(E: dict, Eb: dict, ov: dict, unit_ok: bool) -> tuple[float, float | None]: + if not ov: + return 0.0, None + keys = set(E) | set(ov) + w = lambda t: 0.5 if t == "보통인부" else 1.0 # noqa: E731 + st = sum(w(t) for t in set(E) & set(ov)) / sum(w(t) for t in keys) + if any(q is None for q in ov.values()): # 품 모름 — 직종 겹침만 + return 0.6 * st, None + shape = max(cosine(E, ov), cosine(Eb, ov)) + if not unit_ok: + return 0.35 * st + 0.25 * shape, None + mag = sum(max(close(E.get(t), ov.get(t)), close(Eb.get(t), ov.get(t))) for t in keys) / len( + keys + ) + return 0.35 * st + 0.25 * shape + 0.40 * mag, mag + + +def mach_sim(EM: list, OX: list, opt: list, unit_ok: bool) -> float: + if not OX and not opt: + return 0.0 + got = [] + for m in EM: + mt, ms = set(m["갈래"]), set(m["규격"]) + best = 0.0 + for ot, osp, oh in OX: + j = jacc(mt, ot) + if not j: + continue + sp = (1.0 if ms & osp else 0.0) if ms and osp else 0.5 + hc = max(close(m.get("시간"), oh), close(m.get("밑시간"), oh)) if unit_ok and oh else 0 + best = max(best, 0.6 * j + 0.2 * sp + 0.2 * hc) + for ot, osp in opt: + if mt and mt <= ot: + sp = (1.0 if ms & osp else 0.0) if ms and osp else 0.5 + best = max(best, 0.6 + 0.2 * sp + 0.2 * (1.0 if unit_ok else 0.0)) + got.append(best) + et = set().union(*[set(m["갈래"]) for m in EM]) + ot_all = set().union(*[t for t, _, _ in OX]) if OX else set() + extra = len(ot_all - et) / max(1, len(ot_all | et)) if ot_all else 0.0 + return sum(got) / len(got) * (1 - 0.5 * extra) + + +def score_pair(e: dict, o: dict, table: dict | None = None) -> tuple[float, dict, dict | None]: + """(점수, 채널, 고른 변형) — 로직마다 가장 닮은 변형 · `table` = `Basis.ours()`(한 번 읽어 + 넘김).""" + unit_ok = unit_fit(e["단위"], o["단위"]) + ch = {"단위": 1.0 if unit_ok else 0.0} + g1, g2 = e["nameg"], o["nameg"] + ch["이름"] = 2 * len(g1 & g2) / (len(g1) + len(g2)) if g1 and g2 else 0.0 + our = (table if table is not None else hopyo_basis.ours()).get(o["로직"]) + c = hopyo_match.clause(e["theirs"], our)[0] if (our and e["theirs"]) else 0.0 + ch["조항"] = c * (0.5 if e["옛판"] else 1.0) + hit, _ = cond_score(e["조건s"], o["nc"], o["oc"]) + _, clash = cond_score(e["이름조건s"], o["nc"], {}) + ch["조건"] = hit - clash + if e["matl"]: + near = lambda ws: any(a in b or b in a for a in ws for b in o["matw"]) # noqa: E731 + ch["자재"] = sum(1 for ws in e["matl"] if near(ws)) / len(e["matl"]) + ch["율"] = 1.0 if (e["pctv"] and o["pctv"] and e["pctv"] & o["pctv"]) else 0.0 + base = sum(W.get(k, 0) * v for k, v in ch.items()) + best, bestv, bestch = None, None, {} + for v in o["vs"]: + s, vch = base, {} + if e["E"]: + ls, mag = lab_sim(e["E"], e["Eb"], v["L"], unit_ok) + vch["인력"], vch["품"] = ls, mag + s += W["인력"] * ls + elif v["L"]: + s -= 0.6 + if e["장비"]: + vch["장비"] = mach_sim(e["장비"], v["X"], o["opt"], unit_ok) + s += W["장비"] * vch["장비"] + elif v["X"] or o["opt"]: + s -= 0.4 + if best is None or s > best: + best, bestv, bestch = s, v, vch + return best, {**ch, **bestch}, bestv + + +def rank(e: dict, n: int = POOL) -> list[tuple]: + """[(점수, 로직, 채널, 변형)] — 위 n.""" + table = hopyo_basis.ours() + out = [(*_swap(score_pair(e, o, table), o["로직"]),) for o in prepared() or []] + out.sort(key=lambda x: -x[0]) + return out[:n] + + +def _swap(got: tuple, code: str) -> tuple: + return got[0], code, got[1], got[2] + + +def sure(e: dict, pool: list[tuple]) -> bool: + """딱 맞음 — 1위 직종 · 품(3% 안)이 다 맞음(보통인부 하나뿐 줄 아님) 또는 장비 갈래 · 시간 맞음 + · + 2위와 점수 차 SURE[1] 넘음.""" + if not pool: + return False + s1, _, ch, _ = pool[0] + gap = s1 - pool[1][0] if len(pool) > 1 else 9 + if e["E"]: + rich = len(e["E"]) >= 2 or set(e["E"]) != {"보통인부"} or bool(e["장비"]) + ok = rich and (ch.get("품") or 0) >= 1 - SURE[0] and ch.get("인력", 0) >= 1 - SURE[0] + else: + ok = bool(e["장비"]) and ch.get("장비", 0) >= 0.95 + return ok and gap > SURE[1] + + +def fuse(e: dict, pool: list[tuple], border: list[str]) -> list[str]: + """지문 점수 + BETA × 이름 · 근거 차례 덤 → 위 다섯.""" + bonus = {k: RB[i] for i, k in enumerate(border[:5])} + cand = {k: s for s, k, _, _ in pool} + by = {o["로직"]: o for o in prepared() or []} + table = hopyo_basis.ours() + for k in bonus: + if k not in cand and k in by: + cand[k] = score_pair(e, by[k], table)[0] + return sorted(cand, key=lambda k: -(cand[k] + BETA * bonus.get(k, 0)))[:TOP_OPT] + + +def single(e: dict) -> bool: + """숫자 하나뿐 줄 — 직종 하나 또는 장비 시간 하나(기계 1 시간 줄 뺌) · 같은 숫자 로직이 많아 + 우연 맞음.""" + E = [k for k, x in e["인력"].items() if x.get("품")] + X = [m for m in e["장비"] if m.get("시간")] + return len(E) + len(X) == 1 and not (X and abs(X[0]["시간"] - 1.0) < 1e-9) + + +def has_numbers(efp: dict) -> bool: + return bool(efp["인력"] or any(m.get("시간") for m in efp["장비"]) or efp["자재"]) + + +# ── 블록 셈 ──────────────────────────────────────────────────────────── +def compute(block: dict | None, line: dict, got: dict | None = None) -> dict[str, Any]: + """블록 + 줄 하나 → 맞춤셈 한 칸(설계 3-2 꼴).""" + from M02_MasterTemplete import M02_Hopyo_Verdict as verdict # 판정이 이 파일을 부름 + + return verdict.block(block, line, got or hopyo_match.targets()) + + +# ── 뒤 셈 · 캐시 ─────────────────────────────────────────────────────── +_JOBS: queue.Queue = queue.Queue() +_PENDING: set[str] = set() +_WORKER: dict[str, Any] = {"스레드": None} +_LOCK = threading.Lock() + + +def _hp_path(folder: Path, key: str) -> Path: + return hopyo_store._path(folder, key) + + +def cache_path(folder: Path, key: str) -> Path: + hp = hashlib.sha256(_hp_path(folder, key).read_bytes()).hexdigest()[:16] + return ours.folder() / f"맞춤_{hp}_{ours.version()}.json" + + +def units(doc: dict[str, Any]) -> list[tuple[str, dict | None, dict]]: + """셈 단위 [(열쇠, 블록, 줄)] — 블록 + 줄 열쇠 한 번 · 블록 없는 기계 줄 · 직접 줄 호표.""" + blocks = {int(b["번"]): b for b in doc.get("단산") or [] if b.get("번") is not None} + out, seen = [], set() + for h in doc.get("호표") or []: + for line in h.get("줄") or []: + ref = line.get("참조") or {} + kind = finger.kind_of(line) + if ref.get("종류") == "단산": + key = unit_key(line) + if key not in seen: + seen.add(key) + out.append((key, blocks.get(int(ref.get("번") or 0)), line)) + elif kind == "기계": + key = unit_key(line) + if key not in seen: + seen.add(key) + out.append((key, None, line)) + if (fp := finger.direct(h)) is not None and f"호표:{h['번']}" not in seen: + seen.add(f"호표:{h['번']}") + out.append( + (f"호표:{h['번']}", {"_지문": fp}, {k: h.get(k) for k in ("이름", "규격", "단위")}) + ) + return out + + +def unit_key(line: dict[str, Any]) -> str: + ref = line.get("참조") or {} + if ref.get("종류") == "단산": + return f"단산:{ref.get('번')}|{hopyo_match.line_key(line)}" + return f"줄:{hopyo_match.line_key(line)}" + + +def _read(path: Path) -> dict[str, Any] | None: + try: + doc = json.loads(path.read_bytes()) + except (OSError, ValueError): + return None + return doc if isinstance(doc, dict) else None + + +def result(folder: Path, key: str) -> dict[str, Any] | None: + """맞춤셈 캐시(이 HP 파일 · 지금 판) — 없으면 None.""" + try: + return _read(cache_path(folder, key)) + except OSError: + return None + + +def status(folder: Path, key: str) -> dict[str, Any]: + """{끝, 한, 모두, 까닭?}.""" + got = result(folder, key) + if got and got.get("끝"): + return {"끝": True, "한": got["셈"][0], "모두": got["셈"][1]} + why = None if ours.ready() else "우리 로직 준비 중" + done, total = got["셈"] if got else (0, None) + return {"끝": False, "한": done, "모두": total, **({"까닭": why} if why else {})} + + +def run_job(folder: Path, key: str) -> dict[str, Any]: + """HP 파일 하나를 끝까지 셈(뒤 스레드 · 시험이 바로 부름) → 캐시.""" + if ours.table() is None: + thread = ours.warm(build_missing=True) + if thread is not None: + thread.join() + path = cache_path(folder, key) + doc = hopyo_store.read(folder, key) + todo = units(doc) + got = _read(path) or {} + done = got.get("블록") if got.get("판") == ours.version() else None + out = {"판": ours.version(), "지문판": ours.FP_VERSION, "셈": [0, len(todo)], "끝": False} + out["블록"] = dict(done or {}) + targets = hopyo_match.targets() + for at, (unit, block, line) in enumerate(todo, 1): + if unit not in out["블록"]: + out["블록"][unit] = compute(block, line, targets) + out["셈"] = [at, len(todo)] + if at % EVERY == 0: + atomic_write_json(path, out) + out["끝"] = True + atomic_write_json(path, out) + return out + + +def _work() -> None: + while True: + folder, key = _JOBS.get() + try: + run_job(folder, key) + except Exception: # noqa: BLE001 — 한 파일이 깨져도 일감 줄은 계속(보기에서 「셈 중」 그대로) + pass + finally: + with _LOCK: + _PENDING.discard(f"{folder}|{key}") + + +def ensure(folder: Path, key: str) -> dict[str, Any]: + """캐시 있으면 끝 · 없으면 일감 줄에 넣음(같은 파일은 한 번) → {끝, 한, 모두, 까닭?}.""" + now = status(folder, key) + if now["끝"]: + return now + with _LOCK: + tag = f"{folder}|{key}" + if tag not in _PENDING: + _PENDING.add(tag) + _JOBS.put((Path(folder), key)) + if _WORKER["스레드"] is None or not _WORKER["스레드"].is_alive(): + _WORKER["스레드"] = threading.Thread(target=_work, name="m02-hopyo-fit", daemon=True) + _WORKER["스레드"].start() + return now diff --git a/M02_MasterTemplete/M02_Hopyo_Match.py b/M02_MasterTemplete/M02_Hopyo_Match.py index c45f24f13..d1dd71ef1 100644 --- a/M02_MasterTemplete/M02_Hopyo_Match.py +++ b/M02_MasterTemplete/M02_Hopyo_Match.py @@ -319,7 +319,7 @@ def candidates( scored.sort(key=lambda x: -x[0]) out = [] for score, one in scored: - label = (one["이름"], one.get("규격"), one["단위"]) + label = one.get("로직") or (one["이름"], one.get("규격"), one["단위"]) # 로직은 접지 않음 if label in seen: continue seen.add(label) @@ -329,6 +329,86 @@ def candidates( return out +# ── 인용 조항 · 이름 · 근거 차례(338 · 설계 2-4 B 차례) ───────────────────── +_OLD_TEXT = re.compile(r"(20(1\d|2[0-4])|1\d년|2[0-4]년|임도품셈|임도표준품셈|사방품셈)") +_TYPO_CLAUSE = re.compile(r"^(\d+)-(\d)(\d)(\d)$") # 13-114 → 13-11-4 +_GENERIC = frozenset( + "인력 기계 장비 소계 합계 노무비 재료비 경비 적용 기준 별도계상 별도 인부 보통인부 특별인부 " + "설치 해체 운반 작업 시공 사용 포함 공구손료 경장비 손료 기계경비 재료 인건비 품셈 표준품셈 " + "건설표준품셈 이하 이상".split() +) + + +def fix_typo(theirs: list) -> list[list[str]]: + """엑셀 인용 조항 오타 — 「13-114」 → 「13-11-4」.""" + out = [] + for book, n in theirs or []: + m = _TYPO_CLAUSE.match(str(n)) + out.append([book, f"{m.group(1)}-{m.group(2)}{m.group(3)}-{m.group(4)}" if m else n]) + return out + + +def clause(theirs: list, our: dict[str, Any]) -> tuple[float, list[str], str]: + """(점수, 맞은 우리 조항 글, 꼬리표) — 같음 1 · 윗마디 0.9(엑셀 6-2 ↔ 우리 6-2-2) · 앞 마디 0.5 + · + 나 창 `Basis.clause` 가 있으면 그것.""" + if hasattr(hopyo_basis, "clause"): + return hopyo_basis.clause(theirs, our) + nums = [n for book, n in theirs if book == our["책"]] + same = sorted(n for n in nums if n in our["조항"]) + if same: + return 1.0, [f"{our['구분']} {n}" for n in same], "조항 같음" + kids = sorted(c for c in our["조항"] for n in nums if c.startswith(n + "-")) + if kids: + return 0.9, [f"{our['구분']} {n}" for n in kids], "조항 같음" + heads = {n.rsplit("-", 1)[0] for n in nums} + near_ = sorted(n for n in our["조항"] if n.rsplit("-", 1)[0] in heads) + if near_: + return 0.5, [f"{our['구분']} {n}" for n in near_], "앞 마디 같음" + return 0.0, [], "" + + +def _strong_basis(line: dict[str, Any], cand: dict[str, Any]) -> bool: + if cand["근거"]["직종"]: + return True + mine = {w for w in hopyo_basis._loose_words(f"{line.get('이름')}") if w not in _GENERIC} + theirs = {w for w in hopyo_basis._loose_words(cand["이름"]) if w not in _GENERIC} + return any(a in b or b in a for a in mine for b in theirs) + + +def b_order(line: dict[str, Any], got: dict[str, Any] | None = None, text: Any = ()) -> dict: + """이름 · 근거 차례 `{상태: 같음 | 후보 | 없음, top: [로직 5], 합침: [로직 15]}` — 이름 후보 + + 근거 후보 합침(접힘 없음) · 「같음」도 근거 검사(인용 조항이 다른 로직을 가리키면 후보) · + 옛 판 글이면 같음 그대로 · 씨앗은 이름 같음 자리.""" + got = got or targets() + basis_ = dict(line.get("근거") or {}) + if basis_.get("출처"): + line = {**line, "근거": {**basis_, "출처": fix_typo(basis_["출처"])}} + basis = hopyo_basis.candidates(line, got) + named = candidates(line, "로직", got, None) + merged = [c.get("로직") for c in hopyo_basis.merge(basis, named) if c.get("로직")] + found = _exact(line, "로직", got) + if len(found) == 1: + tgt = found[0]["로직"] + theirs = (line.get("근거") or {}).get("출처") or [] + our = hopyo_basis.ours().get(tgt) + if _OLD_TEXT.search(" ".join(str(t) for t in list(text)[:5])) or not theirs: + return {"상태": "같음", "top": [tgt], "합침": merged[:15]} + if our and clause(theirs, our)[0] >= 0.9: + return {"상태": "같음", "top": [tgt], "합침": merged[:15]} + strong = [c for c in basis if c["근거"]["까닭"].startswith("조항 같음")] + strong = [c for c in strong if _strong_basis(line, c)] + if not strong: + return {"상태": "같음", "top": [tgt], "합침": merged[:15]} + top = list(dict.fromkeys([strong[0]["로직"], tgt] + [c["로직"] for c in strong[1:]])) + return {"상태": "후보", "top": top[:5], "합침": merged[:15]} + top = [x["로직"] for x in found] + rest = [c for c in hopyo_basis.merge(basis, named) if c.get("로직") not in top] + first = next((c.get("로직") for c in named if c.get("로직") not in top), None) + top += [c["로직"] for c in hopyo_basis.pick(rest, first if basis else None, 5 - len(top))] + return {"상태": "후보" if top else "없음", "top": top[:5], "합침": merged[:15]} + + # ── 층 기억 ─────────────────────────────────────────────────────────── def chain(folder: Path) -> list[Path]: """기억을 볼 층 폴더 — 같은 층 → 위 층(개인 → 회사 → 마스터).""" diff --git a/M02_MasterTemplete/M02_Hopyo_Option.py b/M02_MasterTemplete/M02_Hopyo_Option.py new file mode 100644 index 000000000..f6b4a6c44 --- /dev/null +++ b/M02_MasterTemplete/M02_Hopyo_Option.py @@ -0,0 +1,497 @@ +"""M02 일위대가 맞춤 — 옵션 거꾸로 찾기(PLAN 338-1 · 설계 2-5): 고른 우리 로직의 입력 칸마다 +후보(옵션 공간)를 엔진으로 돌려 엑셀 품(직종 품 · 장비 시간 · 자재 수량)과 같은 입력을 찾음. + +공간 = 고르기 모두 · 기계(EQ) = 엑셀 규격 수와 맞는 기종(없으면 모두) · 범위 = 두 끝 + 범위 안 엑셀 +수 · +표 찾기 칸 = 그 표 줄 값 · 그 밖 수 = 자동값 + 같은 이름 계수 + 단위 붙은 엑셀 수(% 칸 = 0 덧 · +「품」 칸 = 엑셀 품 × 결과단위) · 뺌 = 지역 · 보정작업 · 조종원직종. +조합 = 곱 ≤ CAP 모두(안 닿는 칸 · 같은 품 값 묶음 셈) · 넘으면 엑셀 조건 낱말로 고르기 좁힘 · +그래도 넘으면 기준점 둘에서 하나씩 + 고르기 둘씩. +수준 = 0(0.5%) · 1(R = 0.5% + 엑셀 값 자릿수 반올림 폭) · 2(3%) · 9(없음) — 「딱 맞음」(핵심 + 엑셀 +몫을 +다 덮음)만 셈 · 같은 수준이면 거리 작은 것. 규칙 · 문턱은 336 K5(`k5_lib` · `stage_b`) 그대로. +엔진 = `M02_Hopyo_Ours.run`(읽기만). +""" + +from __future__ import annotations + +import itertools +import json +import math +import re +from decimal import Decimal +from typing import Any + +from M01_MasterData import M01_MasterData_Store as m01 +from M02_MasterTemplete import M02_Hopyo_Fit as fit # 손잡이(CAP · TOL) — 부를 때만 씀 +from M02_MasterTemplete import M02_Hopyo_Ours as ours +from M02_MasterTemplete.M02_Hopyo_Words import nfkc, specnums + +FIXED = ("지역", "보정작업", "조종원직종") +_NUM = re.compile(r"(? dict[str, Any]: + """엑셀 지문 → 옵션 공간 재료(글 · 글머리 · 계수 · 규격 수 · 조건 · 품 값).""" + texts = [efp["이름"], efp.get("규격") or ""] + [str(t) for t in text or []] + sp: set[float] = set() + for m in efp["장비"]: + sp |= set(m.get("규격") or []) + sp |= specnums(f"{efp['이름']} {efp.get('규격') or ''}") + return { + "글": texts, + "글머리": texts[:8], + "계수": efp.get("계수") or {}, + "규격수": sp, + "조건": efp.get("조건") or {}, + "품값": {Decimal(str(x["품"])) for x in efp["인력"].values() if x.get("품")}, + } + + +def excel_numbers(texts: list[str], spec: dict[str, Any] | None = None) -> set[Decimal]: + """글의 수 — spec 이 있으면 그 칸 단위가 뒤에 붙거나 칸 이름이 앞에 붙은 수만(쉼표 단가 뺌).""" + out: set[Decimal] = set() + flat = [nfkc(t) for t in texts] + if spec is None: + pats = [_NUM] + else: + u = nfkc(spec.get("단위")).strip().lower() + vs = UNITS.get(u, (u,) if u and len(u) <= 3 and "/" not in u else ()) + vs = () if u == "%" else vs # 「노무비의 2%」 같은 율이 감률 칸 후보로 드는 것을 막음 + num = r"(\d+(?:\.\d+)?)" + pats = [] + if vs: + alt = "|".join(re.escape(x) for x in vs) + pats.append( + re.compile(r"(? set[float]: + """기계 규격 수 — 단위 붙은 수 + 맨 수(「41 [80∼95]」 → 41 · 80 · 95).""" + one = ours.whole().numbers.get("EQ", {}).get(str(code)) or {} + bare = {float(n) for n in re.findall(r"\d+(?:\.\d+)?", nfkc(one.get("규격")))} + return ours.eq_of(code)[1] | bare + + +def _uses(row: dict) -> tuple[dict, set]: + try: + return m01.mau._uses(row) + except Exception: # noqa: BLE001 — 식을 못 읽는 로직은 찾기 칸 없음 + return {}, set() + + +def space(code: str, ex: dict[str, Any] | None = None) -> list[tuple[str, list, str]]: + """[(입력 이름, 후보, 갈래)] — 갈래 = 기계 · 고르기 · 범위 · 표 · 수.""" + whole = ours.whole() + row = whole.logic(code) + auto = m01.mau.values(whole, row) + finds, _ = _uses(row) + ex = ex or {} + texts = ex.get("글") or [] + coef = {k.lower(): Decimal(str(v)) for k, v in (ex.get("계수") or {}).items()} + spn = ex.get("규격수") or set() + times = Decimal(str(ours.times_of(row))) + out = [] + for s in row.get("입력") or []: + name, opts = s["이름"], s.get("고르기") or [] + if name in FIXED: + continue + if opts and all(isinstance(v, str) and ours.EQ_CODE.match(v) for v in opts): + hits = {v: len(eq_numbers(v) & spn) for v in opts} if spn else {} + most = max(hits.values(), default=0) + keep = [ + v for v in opts if most and hits[v] == most + ] # 엑셀 규격 수가 가장 많이 겹치는 기종 + out.append((name, keep or list(opts), "기계")) + elif opts: + out.append((name, list(opts), "고르기")) + elif isinstance(s.get("범위"), list) and len(s["범위"]) == 2: + lo, hi = (Decimal(str(x)) for x in s["범위"]) + vals = {lo, hi} | {v for v in excel_numbers(texts, s) if lo <= v <= hi} + out.append((name, sorted(vals), "범위")) + elif name in finds: + rows = set() + for ref, cond in finds[name]: + rows |= set(m01.mau._fits(whole, ref, cond)) + out.append((name, sorted(rows) or [auto.get(name)], "표")) + else: + base = auto.get(name) + vals = {base} if base is not None else set() + if name.lower() in coef: + vals.add(coef[name.lower()]) + vals |= excel_numbers(texts, s) if texts else set() + if str(s.get("단위") or "").strip() == "%": + vals.add(Decimal(0)) + if name == "품": + vals |= {v * times for v in ex.get("품값", set())} + vals.discard(None) + out.append((name, sorted(vals, key=lambda v: (v != base, v)) or [base], "수")) + return out + + +def _product(sp: list) -> list[dict]: + names = [n for n, _, _ in sp] + return [dict(zip(names, vals)) for vals in itertools.product(*[o for _, o, _ in sp])] + + +def narrow(sp: list, ex: dict[str, Any] | None) -> list: + """고르기 후보를 엑셀 조건 낱말로 좁힘 — 낱말이 든 후보가 있으면 그것만.""" + words = {w for ws in ((ex or {}).get("조건") or {}).values() for w in ws} + flat = re.sub(r"\s+", "", nfkc(" ".join((ex or {}).get("글머리") or []))) + out = [] + for name, opts, kind in sp: + if kind == "고르기" and len(opts) > 1: + hit = [ + o + for o in opts + if any(w in str(o) for w in words) or (len(str(o)) >= 2 and str(o) in flat) + ] + if hit: + out.append((name, hit, kind)) + continue + out.append((name, opts, kind)) + return out + + +def modes() -> tuple[tuple[str, float, bool], ...]: + """허용 셋 — 0.5% · R(0.5% + 엑셀 값 자릿수 반올림 폭) · 3%.""" + return (("0.5%", fit.TOL[0], False), ("R", fit.TOL[0], True), ("3%", fit.TOL[2], False)) + + +def combos( + sp: list, ex: dict[str, Any] | None = None, cap: int | None = None +) -> tuple[list[dict], str]: + """(조합, 어떻게 — 모두 · 좁힘 · 하나씩).""" + cap = cap or fit.CAP + if (math.prod(len(o) for _, o, _ in sp) if sp else 1) <= cap: + return _product(sp), "모두" + sp2 = narrow(sp, ex) + if math.prod(len(o) for _, o, _ in sp2) <= cap: + return _product(sp2), "좁힘" + base = {n: o[0] for n, o, _ in sp2} + out = [dict(base)] + [{**base, n: v} for n, o, _ in sp2 for v in o[1:]] + small = [(n, o) for n, o, k in sp2 if k != "수"] + for (n1, o1), (n2, o2) in itertools.combinations(small, 2): + if len(o1) * len(o2) <= 400: + out += [{**base, n1: a, n2: b} for a in o1 for b in o2] + seen, uniq = set(), [] + for c in out: + k = json.dumps(c, sort_keys=True, default=str, ensure_ascii=False) + if k not in seen: + seen.add(k) + uniq.append(c) + return uniq[:cap], "하나씩" + + +def sig(v: dict | None, nd: int = 4) -> str | None: + """품 꼴 서명 — 직종 · 장비(갈래 · 시간) · 자재 · 딸린 로직(유효 nd 자리).""" + if v is None: + return None + r = lambda x: float(f"{x:.{nd}g}") # noqa: E731 + return json.dumps( + [ + sorted((k, r(q)) for k, q in v["L"].items() if q), + sorted((tuple(a), tuple(b), r(q)) for a, b, q in v["X"] if q), + sorted((k, r(q)) for k, q in v["M"].items() if q), + sorted((k, r(q)) for k, q in v["S"].items() if q), + ], + ensure_ascii=False, + ) + + +def plan(code: str, ex: dict[str, Any] | None = None) -> tuple[list[dict], str, list, list, dict]: + """(조합, 어떻게, 움직이는 칸, 안 닿는 칸, 값 묶음).""" + sp = space(code, ex) + if (math.prod(len(o) for _, o, _ in sp) if sp else 1) <= fit.CAP: + cb = _product(sp) + sg = [sig(ours.run(code, c)) for c in cb] + active, inert, cls = [], [], {} + for n, o, k in sp: + if len(o) < 2: + continue + ctx: dict[str, dict] = {} + for c, s_ in zip(cb, sg, strict=True): + rest = {x: str(y) for x, y in c.items() if x != n} + ctx.setdefault(json.dumps(rest, sort_keys=True, ensure_ascii=False), {})[ + str(c[n]) + ] = s_ + keys = sorted(ctx) + prof = {str(v): tuple(ctx[g].get(str(v)) for g in keys) for v in o} + if len(set(prof.values())) == 1: + inert.append(n) + continue + active.append(n) + if k != "고르기": + ids: dict = {} + cls[n] = {v: ids.setdefault(pf, len(ids)) for v, pf in prof.items()} + return cb, "모두", active, inert, cls + base = {n: o[0] for n, o, _ in sp} + alt = {n: o[-1] for n, o, _ in sp} + act, inert = [], [] + for n, o, k in sp: + if len(o) == 1: + continue + s0 = {sig(ours.run(code, {**base, n: v})) for v in o} + s1 = {sig(ours.run(code, {**alt, n: v})) for v in o} if len(s0) == 1 else s0 + if len(s0) == 1 and len(s1) == 1: + inert.append(n) + continue + if k != "고르기": # 같은 표 줄에 드는 수는 한 값으로 — 두 기준점 품이 같으면 같은 값 + keep, seen = [], set() + for v in o: + pair = (sig(ours.run(code, {**base, n: v})), sig(ours.run(code, {**alt, n: v}))) + if pair not in seen: + seen.add(pair) + keep.append(v) + o = keep + act.append((n, o, k)) + cb, how = combos(act, ex) + return [{**base, **c} for c in cb], how, [n for n, _, _ in act], inert, {} + + +def vary(hits: list[dict], act: list[str], cls: dict) -> list[str]: + """맞은 조합들 사이에서 값이 갈리는 칸 — 수 칸은 같은 값 묶음이면 한 값.""" + return [ + n for n in act if len({cls.get(n, {}).get(str(g.get(n)), str(g.get(n))) for g in hits}) > 1 + ] + + +# ── 맞대기 ────────────────────────────────────────────────────────────── +def near(a: Any, b: Any, t: float, r: float = 0.0) -> bool: + """a(엑셀) ≈ b(우리) — 비 t 안 · r = 엑셀 값 자릿수 반올림 폭.""" + return bool(a and b and a > 0 and b > 0 and abs(a - b) <= max(t * max(a, b), r)) + + +def ru(x: float) -> float: + """엑셀 값 반올림 폭 — 소수 둘째~넷째 자리로 적힌 값만 · 값의 4% 안.""" + s = repr(float(x)) + if "e" in s or "." not in s: + return 0.0 + d = len(s.split(".")[1].rstrip("0")) + return min(0.5 * 10 ** (-d), 0.04 * float(x)) if 2 <= d <= 4 else 0.0 + + +def mat_words(name: Any) -> set[str]: + """자재 이름 낱말 — 한글 두 자 넘는 덩이 · 영문 세 자 넘는 덩이 · 단위 낱말 뺌.""" + t = nfkc(name).lower().replace(".", "") + return {w for w in re.findall(r"[가-힣]{2,}|[a-z]{3,}", t) if w not in UNITW} + + +def _named(ws: set[str], k: str) -> bool: + w = mat_words(k) + return bool(ws & w) or any(a in b or b in a for a in ws for b in w) + + +def compare( + e: dict, v: dict | None, t: float, base: bool = False, rnd: bool = False +) -> dict | None: + """엑셀 지문 ↔ 우리 셈 → {직종, 장비, 자재, 핵심, 직종만, 덧} · base = 엑셀 밑품으로.""" + if v is None: + return None + tol = lambda x: ru(x) if rnd else 0 # noqa: E731 + E = {k: (x["밑품"] if base else x["품"]) for k, x in e["인력"].items() if x.get("품")} + ov = {k: q for k, q in v["L"].items() if q and q > 0} + out: dict[str, Any] = {"덧": []} + if E: + same = set(E) == set(ov) + ok = same and all(near(E[k], ov[k], t, tol(E[k])) for k in E) + out["직종"] = "같음" if ok else "직종 같음" if same else "다름" + else: + out["직종"] = "우리만" if ov else "없음" + EX = [m for m in e["장비"] if m.get("시간")] + OX = [(set(a), q) for a, _, q in v["X"] if q] + if EX and OX: + ok = [] + for m in EX: + mine = [q for a, q in OX if a & set(m["갈래"])] + if not mine: + out["덧"].append("엑셀만 장비 " + "·".join(sorted(m["갈래"]))) + continue + h = m["밑시간"] if base and m.get("밑시간") else m["시간"] + hit = any(near(h, q, t, tol(h)) for q in mine) or near(h, sum(mine), t, tol(h)) + ok.append(hit) + et = set().union(*[set(m["갈래"]) for m in e["장비"]]) + out["덧"] += ["우리만 장비 " + "·".join(sorted(a)) for a, _ in OX if not a & et] + out["장비"] = ("같음" if all(ok) else "다름") if ok else "갈래 안 겹침" + else: + out["장비"] = "엑셀만" if EX else "우리만" if OX else "없음" + EM = [m for m in e["자재"] if m.get("품")] + OM = {k: q for k, q in v["M"].items() if q} + if EM and OM: + ok = [] + for m in EM: + mine = [q for k, q in OM.items() if _named(mat_words(m["이름"]), k)] + if mine: + x = m["밑품"] if base and m.get("밑품") else m["품"] + ok.append(any(near(x, q, t, tol(x)) for q in mine)) + out["자재"] = ("같음" if all(ok) else "다름") if ok else "이름 안 겹침" + else: + out["자재"] = "엑셀만" if EM else "없음" + core = [out["직종"] == "같음"] if E else [False] if ov else [] + core += [out[k] == "같음" for k in ("장비", "자재") if out[k] in ("같음", "다름")] + out["핵심"] = bool(core) and all(core) + out["직종만"] = bool(E) and out["직종"] == "같음" + return out + + +def strong(e: dict, c: dict | None) -> bool: + """딱 맞음 — 핵심 + 엑셀 몫을 다 덮음(엑셀만 · 우리만 장비 없음 · 자재 다름 없음 · + 「보통인부 하나뿐 · 장비 없음」 줄 아님).""" + if not c or not c.get("핵심"): + return False + if c.get("장비") in ("엑셀만", "우리만", "갈래 안 겹침"): + return False + if any("엑셀만 장비" in d or "우리만 장비" in d for d in c.get("덧") or []): + return False + if c.get("자재") == "다름": + return False + labor = [k for k, x in e["인력"].items() if x.get("품")] + return not (labor == ["보통인부"] and not any(m.get("시간") for m in e["장비"])) + + +def gap(e: dict, v: dict | None, base: bool = False) -> float: + """닮음 거리 — 직종 묶음 어긋남 + 품 |ln 비| 평균 + 장비 시간 반 무게.""" + if v is None: + return 99.0 + E = {k: (x["밑품"] if base else x["품"]) for k, x in e["인력"].items() if x.get("품")} + ov = {k: q for k, q in v["L"].items() if q and q > 0} + d = 0.0 + if E or ov: + d += len(set(E) ^ set(ov)) / len(set(E) | set(ov)) + both = [min(1.0, abs(math.log(E[k] / ov[k]))) for k in set(E) & set(ov)] + d += sum(both) / len(both) if both else 1.0 + EX = [m for m in e["장비"] if m.get("시간")] + OX = [(set(a), q) for a, _, q in v["X"] if q] + if EX and OX: + hs = [] + for m in EX: + mine = [q for a, q in OX if a & set(m["갈래"])] + h = m["밑시간"] if base and m.get("밑시간") else m["시간"] + hs.append(min(1.0, min(abs(math.log(h / q)) for q in mine)) if mine else 1.0) + d += 0.5 * sum(hs) / len(hs) + return d + + +def csig(e: dict, v: dict | None, nd: int = 4) -> str | None: + """엑셀이 보이는 몫만의 품 서명 — 직종 · 엑셀 갈래와 겹치는 장비 · 엑셀 이름과 겹치는 자재.""" + if v is None: + return None + r = lambda x: float(f"{x:.{nd}g}") # noqa: E731 + et = set().union(*[set(m["갈래"]) for m in e["장비"]]) if e["장비"] else set() + em = [mat_words(m["이름"]) for m in e["자재"] if m.get("품")] + return json.dumps( + [ + sorted((k, r(q)) for k, q in v["L"].items() if q), + sorted((tuple(a), r(q)) for a, _, q in v["X"] if q and set(a) & et), + sorted((k, r(q)) for k, q in v["M"].items() if q and any(_named(ws, k) for ws in em)), + ], + ensure_ascii=False, + ) + + +def ours_qty(v: dict | None) -> dict[str, Any]: + """우리 품(화면 꼴) — {인력: {직종: 품}, 장비: [{갈래, 시간}], 자재: [{이름, 수량}]}.""" + if v is None: + return {"인력": {}, "장비": [], "자재": []} + return { + "인력": {k: round(q, 6) for k, q in v["L"].items() if q}, + "장비": [{"갈래": list(a), "시간": round(q, 6)} for a, _, q in v["X"] if q], + "자재": [{"이름": k, "수량": round(q, 6)} for k, q in v["M"].items() if q], + } + + +def _plain(value: Any) -> Any: + if isinstance(value, Decimal): + value = value.normalize() + return int(value) if value == value.to_integral_value() else float(value) + return value + + +def find(code: str, efp: dict[str, Any], text: list[str] | None = None) -> dict[str, Any]: + """로직 하나의 옵션 거꾸로 찾기 → {수준, 판정, 입력, 갈리는칸, 견줌, 우리품, 거리, 섞기전}.""" + none = {"수준": 9, "판정": "없음", "입력": {}, "갈리는칸": [], "견줌": None} + none.update(우리품=ours_qty(None), 거리=99.0, 섞기전=False, 직종같음=False) + try: + ex = ex_of(efp, text) + cb, how, act, _, cls = plan(code, ex) + except Exception: # noqa: BLE001 — 로직 없음 · 식 못 읽음 + return {**none, "판정": "못 셈"} + best = (9, 9, 99.0, None, None) + hits: dict[int, list] = {0: [], 1: [], 2: []} + for g in cb: + v = ours.run(code, g) + if v is None or not (v["L"] or v["X"] or v["M"]): + continue + lvl = weak = 9 + for i, (_, t, rnd) in enumerate(modes()): + c = compare(efp, v, t, rnd=rnd) or {} + if c.get("핵심"): + weak = min(weak, i) + if strong(efp, c): + hits[i].append((g, csig(efp, v))) + lvl = min(lvl, i) + d = gap(efp, v) + if (lvl, weak, d) < best[:3]: + best = (lvl, weak, d, g, v) + lvl, _, d, g, v = best + if g is None: + return none + got = [h for h in hits.get(lvl, [])] if lvl < 9 else [] + split = vary([h for h, _ in got], act, cls) + if not got: + verdict = "없음" + elif len({s for _, s in got}) > 1: + verdict = "여럿(다른 품)" + else: + verdict = "여럿(같은 품)" if split else "하나만" + auto = m01.mau.values(ours.whole(), ours.whole().logic(code)) # 움직인 칸 + 자동값과 다른 칸 + base = compare(efp, v, fit.TOL[2], base=True) or {} + labor = {k for k, x in efp["인력"].items() if x.get("품")} + return { + "수준": lvl, + "판정": verdict, + "입력": {n: _plain(x) for n, x in g.items() if n in act or str(x) != str(auto.get(n))}, + "모든입력": {n: _plain(x) for n, x in g.items()}, + "갈리는칸": split, + "견줌": compare(efp, v, fit.TOL[2]), + "직종같음": bool(v["L"]) and set(k for k, q in v["L"].items() if q) == labor, + "우리품": ours_qty(v), + "거리": round(d, 4), + "섞기전": lvl == 9 and bool(base.get("핵심")), + "어떻게": how, + } diff --git a/M02_MasterTemplete/M02_Hopyo_Ours.py b/M02_MasterTemplete/M02_Hopyo_Ours.py new file mode 100644 index 000000000..667958381 --- /dev/null +++ b/M02_MasterTemplete/M02_Hopyo_Ours.py @@ -0,0 +1,364 @@ +"""M02 일위대가 맞춤 — 우리 돈 로직 지문 · 마스터 판 열쇠 · 디스크 캐시 · 엔진 셈 캐시 +(PLAN 338-1 · 설계 2-3). + +지문 = 정적(호표 인력 → 직종 · 「로직(기계경비 …)」 → 장비 갈래 · 규격 · 재료 이름 · 덧줄 % · +이름 · 고르기 글 조건 낱말 · 기계경비 로직) + 변형(고르기 입력 곱 64 이하 모두 · 넘으면 하나씩 · +지역 · 조종원직종 · 보정작업 · 기종 목록 칸 뺌)마다 단위당 L(직종 인) · X(장비 시간) · M(자재) · +S(딸린 로직) · mix(인력비율 · 기계비율). +캐시 = `tmp/m02_hopyo/우리지문_<판>.json`(환경 `AISLO_HOPYO_CACHE` 로 바꿈) · 판 = 마스터 첫 층 JSON +sha256 묶음 · 처음 셈은 뒤 스레드(≈ 2 분) · 다음은 읽기만. +엔진 = `Labor_Doc.run_line` 과 같은 길(자동값 + 입력 → `mf.run`) · 셈 잠금 하나(뒤 셈 · 요청 겹침) · +메모리 캐시. 원본(`resources/master_data`) 읽기만. 규칙은 336 실험(C4 `s2_ours_fp` · K5 `run`) +그대로. +""" + +from __future__ import annotations + +import hashlib +import itertools +import json +import math +import os +import re +import threading +import time +from decimal import Decimal +from pathlib import Path +from typing import Any + +from common_util.common_util_json import atomic_write_json +from M01_MasterData import M01_MasterData_Store as m01 +from M01_MasterData import M01_MasterData_Store_Cache as m01_cache +from M02_MasterTemplete import M02_Hopyo_Match as hopyo_match +from M02_MasterTemplete.M02_Hopyo_Words import conds, mtypes, specnums + +FP_VERSION = 1 # 지문 꼴이 바뀌면 올림 — 옛 캐시 버림 +ROOT = Path(__file__).resolve().parents[1] +_PER = re.compile(r"원\s*/\s*(\d[\d,]*\.?\d*|\.\d+)?") +_SUB = re.compile(r"^\s*로직\(\s*(\w+)\s*(?:,(.*))?\)\s*$") +EQ_CODE = re.compile(r"^\d{4}-\d{4}$") +_PCT = re.compile(r"\*\s*([\d.]+)\s*/\s*100") +SKIP_IN = ("지역", "조종원직종", "보정작업") +FIXED_MACH = { + "GC000267": {"굴착기"}, + "GC000270": {"굴착기", "집게"}, + "GC000271": {"굴착기", "브레이커"}, + "GF000145": {"덤프트럭"}, +} +RUN_CAP = 50_000 # ponytail: 엔진 셈 캐시 칸 수 — 넘으면 통째 비움(LRU 는 필요해지면) +_ENGINE = threading.RLock() # ponytail: 셈 잠금 하나 — 엔진을 뒤 · 요청 스레드가 같이 부를 때 +_STATE: dict[str, Any] = {"판": None, "자국": None, "표": None, "스레드": None} +_RUN: dict[tuple, Any] = {} +_LOCK = threading.Lock() +_NO_PRICE = {str(i): "-" for i in range(400)} # 고른 자재 = 없는 키 → 값 없음 · 찾기 안 함 +WHOLE_TTL = 2.0 # ponytail: 마스터 색인을 2 초 붙듦 — 줄마다 파일 자국을 다시 안 봄 + + +def folder() -> Path: + return Path(os.environ.get("AISLO_HOPYO_CACHE") or ROOT / "tmp" / "m02_hopyo") + + +# ── 마스터 판 열쇠 ───────────────────────────────────────────────────── +def version() -> str: + """마스터 판 = 첫 층 *.json(밑줄 뺌) 이름 + sha256 16 묶음의 sha256 앞 16 — 자국이 같으면 다시 + 안 셈.""" + stamps = m01_cache._stamps(Path(m01.FOLDER)) + if _STATE["자국"] != stamps: + parts = [ + f"{name}:{m01.version_of((Path(m01.FOLDER) / name).read_bytes())}" for name in stamps + ] + _STATE.update(자국=stamps, 판=hashlib.sha256("\n".join(parts).encode()).hexdigest()[:16]) + return _STATE["판"] + + +def path_of(ver: str | None = None) -> Path: + return folder() / f"우리지문_{ver or version()}.json" + + +# ── 엔진 한 번 ───────────────────────────────────────────────────────── +def whole() -> Any: + now = time.monotonic() + if _STATE.get("색인") is None or now - _STATE.get("색인때", 0) > WHOLE_TTL: + _STATE.update(색인=m01.loaded()[1], 색인때=now) + return _STATE["색인"] + + +def is_mach_logic(row: dict[str, Any]) -> bool: + return "시간당" in str(row.get("이름") or "") + + +def eq_of(code: Any) -> tuple[set[str], set[float]]: + one = whole().numbers.get("EQ", {}).get(str(code)) or {} + if not one: + return set(), set() + return mtypes(f"{one.get('상세구분')} {one.get('이름')}"), specnums(one.get("규격")) + + +def times_of(row: dict[str, Any]) -> float: + per = _PER.findall(str(row.get("결과단위") or "")) + return float(per[0].replace(",", "")) if per and per[0] else 1.0 + + +def _args(text: str | None) -> dict[str, str]: + out = {} + for part in re.split(r",(?![^()]*\))", text or ""): + if "=" in part: + k, v = part.split("=", 1) + out[k.strip()] = v.strip() + return out + + +def _labor_names() -> dict[str, str]: + w = whole() + if _STATE.get("LB판") is not w: + lb = {str(r.get("키")): str(r.get("이름")) for r in w.numbers.get("LB", {}).values()} + _STATE.update(LB판=w, LB=lb) + return _STATE["LB"] + + +def _engine(code: str, given: dict[str, Any]) -> dict[str, Any]: + """{L, X, M, S, mix} — 줄 수량 ÷ 결과단위 수(단위당).""" + w = whole() + row = w.logic(code) + times = times_of(row) + by_name: dict[Any, dict] = {} + for h in row.get("호표") or []: + by_name.setdefault(h.get("이름"), h) + auto = {k: v for k, v in m01.mau.values(w, row).items() if v is not None} + with _ENGINE, m01.cm.mf.mp.choosing(_NO_PRICE): # 품만 씀 — 자재 값 찾기 건너뜀(수량 같음) + got = m01.cm.mf.run(w, code, {**auto, **m01._dec(given)}) + mid, lb = got.get("중간") or {}, _labor_names() + L: dict[str, float] = {} + X: list[list] = [] + M: dict[str, float] = {} + S: dict[str, float] = {} + for line in got.get("줄") or []: + h = by_name.get(line.get("이름")) + if h is None: + continue + try: + q = float(Decimal(str(line.get("수량") or 0))) / times + except ArithmeticError: + continue + kind = h.get("종류") + if kind == "인력": + t = lb.get(str(h.get("요소")), str(h.get("이름"))) + L[t] = L.get(t, 0.0) + q + elif kind == "로직": + m = _SUB.match(str(h.get("요소") or "")) + sub = m.group(1) if m else "" + sub_row = w.logic(sub) if sub else {} + if sub_row and is_mach_logic(sub_row): + ts = set(FIXED_MACH.get(sub, set())) | mtypes(h.get("이름")) + sp = specnums(h.get("이름")) + for v in _args(m.group(2)).values(): + lit = v.strip("'\"") + if not EQ_CODE.match(lit): + lit = str(mid.get(lit) or given.get(lit) or auto.get(lit) or "") + if EQ_CODE.match(lit): + a, b = eq_of(lit) + ts |= a + sp |= b + if q: + X.append([sorted(ts), sorted(sp), q]) + elif q: + S[sub] = S.get(sub, 0.0) + q + elif kind == "재료" and q: + M[str(h.get("이름"))] = M.get(str(h.get("이름")), 0.0) + q + mix = {} + for k in ("인력비율", "기계비율"): + try: + if mid.get(k) not in (None, ""): + mix[k] = float(Decimal(str(mid.get(k)))) + except ArithmeticError: + pass + return {"L": L, "X": X, "M": M, "S": S, "mix": mix} + + +def _mach_view(code: str, row: dict, given: dict, v: dict) -> dict: + """「기계 시간당 기계경비」 = 고른 기계(EQ 칸)의 갈래 · 규격 1 시간(조종원은 기계경비 속).""" + auto = m01.mau.values(whole(), row) + X = [] + for s in row.get("입력") or []: + opts = s.get("고르기") or [] + if opts and all(isinstance(o, str) and EQ_CODE.match(o) for o in opts): + ts, sp = eq_of(given.get(s["이름"], auto.get(s["이름"]))) + X.append([sorted(ts | FIXED_MACH.get(code, set())), sorted(sp), 1.0]) + if not X and FIXED_MACH.get(code): + X = [[sorted(FIXED_MACH[code]), [], 1.0]] + return {**v, "L": {}, "X": X} + + +def run(code: str, given: dict[str, Any] | None = None) -> dict[str, Any] | None: + """엔진 한 번 → {L, X, M, S, mix} · 못 돌면 None(메모리 캐시).""" + given = dict(given or {}) + key = (code, json.dumps(given, sort_keys=True, default=str, ensure_ascii=False)) + if key not in _RUN: + try: + v = _engine(code, given) + row = whole().logic(code) + v = _mach_view(code, row, given, v) if is_mach_logic(row) else v + except Exception: # noqa: BLE001 — 못 도는 조합은 없음으로(엔진 예외가 여럿) + v = None + if len(_RUN) >= RUN_CAP: + _RUN.clear() + _RUN[key] = v + return _RUN[key] + + +# ── 지문 ─────────────────────────────────────────────────────────────── +def _static(code: str, row: dict[str, Any], unit: str) -> dict[str, Any]: + picks = [s for s in row.get("입력") or [] if s.get("고르기")] + labels, opt_mach = [], [] + for s in picks: + names = s.get("고르기이름") or {} + for v in s.get("고르기") or []: + if isinstance(v, str) and EQ_CODE.match(v): + ts, sp = eq_of(v) + if ts: + opt_mach.append([sorted(ts), sorted(sp)]) + else: + labels.append(str(names.get(v) or v)) + lb = _labor_names() + trades, mach, mats = set(), [], [] + for h in row.get("호표") or []: + kind = h.get("종류") + if kind == "인력": + trades.add(lb.get(str(h.get("요소")), str(h.get("이름")))) + elif kind == "로직": + m = _SUB.match(str(h.get("요소") or "")) + ts = set(FIXED_MACH.get(m.group(1) if m else "", set())) | mtypes(h.get("이름")) + for v in _args(m.group(2) if m else "").values(): + if EQ_CODE.match(v.strip("'\"")): + ts |= eq_of(v.strip("'\""))[0] + if ts: + mach.append([sorted(ts), sorted(specnums(h.get("이름")))]) + elif kind == "재료": + el = h.get("요소") + words = ( + (el.get("검색어") or el.get("이름") or el.get("상세구분")) + if isinstance(el, dict) + else "" + ) + mats.append(f"{h.get('이름')} {words or ''}".strip()) + pct = [ + [str(d.get("이름")), float(v)] + for d in row.get("덧줄") or [] + for v in _PCT.findall(str(d.get("식") or "")) + ] + if is_mach_logic(row): + mach.append([sorted(FIXED_MACH.get(code, set())), []]) + pick_text = " ".join(labels + [str(s.get("이름")) for s in picks]) + return { + "로직": code, + "이름": str(row.get("이름") or ""), + "단위": unit, + "직종": sorted(trades), + "장비": mach, + "장비고름": opt_mach, + "자재": mats, + "덧줄율": pct, + "이름조건": {k: sorted(v) for k, v in conds(row.get("이름")).items()}, + "고름조건": {k: sorted(v) for k, v in conds(pick_text).items()}, + "기계경비": is_mach_logic(row), + } + + +def combos(row: dict[str, Any]) -> list[dict[str, Any]]: + """변형 입력 — 고르기 곱 64 이하 모두 · 넘으면 하나씩(기본 + 칸마다).""" + picks = [] + for s in row.get("입력") or []: + opts = s.get("고르기") or [] + if not opts or any(w in str(s.get("이름")) for w in SKIP_IN): + continue + if any(isinstance(v, str) and EQ_CODE.match(v) for v in opts): + continue + picks.append((str(s["이름"]), list(opts))) + if not picks: + return [{}] + if math.prod(len(o) for _, o in picks) <= 64: + names = [n for n, _ in picks] + return [dict(zip(names, vals)) for vals in itertools.product(*[o for _, o in picks])] + return [{}] + [{n: v} for n, opts in picks for v in opts] + + +def _sig(v: dict) -> str: + r = lambda x: float(f"{x:.4g}") # noqa: E731 + return json.dumps( + [ + sorted((k, r(q)) for k, q in v["L"].items()), + sorted((tuple(a), tuple(b), r(q)) for a, b, q in v["X"]), + sorted((k, r(q)) for k, q in v["M"].items()), + ], + ensure_ascii=False, + ) + + +def fingerprint(code: str, unit: str) -> dict[str, Any]: + """로직 하나 → 정적 지문 + 변형(서로 다른 품 꼴만).""" + row = whole().logic(code) + fp = _static(code, row, unit) + seen, variants, errors = set(), [], 0 + for given in combos(row) if not fp["기계경비"] else [{}]: + try: + v = _engine(code, given) # 기계경비 로직도 엔진 그대로(맞춤이 고르기 장비로 봄) + except Exception: # noqa: BLE001 — 못 도는 조합은 셈만 + errors += 1 + continue + if (k := _sig(v)) not in seen: + seen.add(k) + variants.append({"in": {n: str(x) for n, x in given.items()}, **v}) + return {**fp, "갈래수": len(variants), "오류": errors, "변형": variants} + + +# ── 표 · 캐시 · 데움 ─────────────────────────────────────────────────── +def _read(ver: str) -> list[dict] | None: + try: + doc = json.loads(path_of(ver).read_bytes()) + except (OSError, ValueError): + return None + ok = isinstance(doc, dict) and doc.get("판") == ver and doc.get("지문판") == FP_VERSION + return doc.get("표") if ok else None + + +def build(ver: str | None = None) -> list[dict]: + """1,438 돈 로직 지문을 셈 → 디스크 캐시(같은 판이면 덮음).""" + ver = ver or version() + table = [fingerprint(one["로직"], one["단위"]) for one in hopyo_match.targets()["로직"]] + atomic_write_json(path_of(ver), {"판": ver, "지문판": FP_VERSION, "표": table}) + return table + + +def table() -> list[dict] | None: + """우리 지문(판이 같은 것) · 준비 전 None — 디스크에 있으면 읽어 둠.""" + ver = version() + with _LOCK: + if _STATE.get("표판") != ver: + _STATE.update(표=_read(ver), 표판=ver) + return _STATE["표"] + + +def ready() -> bool: + return table() is not None + + +def warm(build_missing: bool = False) -> threading.Thread | None: + """디스크 캐시 읽기(뒤) · `build_missing` 이면 없을 때 뒤 스레드로 셈 — 셈 중이면 그 스레드.""" + running = _STATE.get("스레드") + if running is not None and running.is_alive(): + return running + + def work() -> None: + try: + if table() is None and build_missing: + ver = version() + got = build(ver) + with _LOCK: + _STATE.update(표=got, 표판=ver) + except Exception: # noqa: BLE001 — 마스터가 깨져도 서버를 막지 않음(보기에서 「준비 중」) + pass + + thread = threading.Thread(target=work, name="m02-hopyo-ours", daemon=True) + _STATE["스레드"] = thread + thread.start() + return thread diff --git a/M02_MasterTemplete/M02_Hopyo_Shares.py b/M02_MasterTemplete/M02_Hopyo_Shares.py new file mode 100644 index 000000000..bbf87d026 --- /dev/null +++ b/M02_MasterTemplete/M02_Hopyo_Shares.py @@ -0,0 +1,177 @@ +"""M02 일위대가 맞춤 몫(PLAN 338-1 · 설계 2-2 · 2-7) — 단가산출 블록을 맨 윗 절(1. 2. 3.) +· 작은 절(가. 나.)로 갈라 몫마다 노임(직종 → 인) · 장비(「굴착기|0.6」 → 시간) 벡터. + +엑셀꼴 = 「직종 : 노임 × 식 = 금액」(식을 못 풀면 금액 ÷ 노임) · 장비 이름 줄 뒤 「노무비 : 단가 ÷ Q +× 몫」 · +소계 · 합계 절 뺌. STmate = 자원 L · X(절 = 자원 `절` · 없으면 한 몫). 금액 안 씀. +규칙은 336 실험(C5 `c01_trees.sections` · `sections_stm`) 그대로 · 장비 갈래는 +`M02_Hopyo_Words.mtypes`. +""" + +from __future__ import annotations + +import ast +import operator +import re +from typing import Any + +from M02_MasterTemplete import M02_Hopyo_Basis as hopyo_basis +from M02_MasterTemplete.M02_Hopyo_Words import mtypes, nfkc + +_SIZE = re.compile(r"(\d+(?:\.\d+)?)\s*(㎥|m3|톤|ton|t\b|kw|hp)", re.I) +_OPS = { + ast.Mult: operator.mul, + ast.Div: operator.truediv, + ast.Add: operator.add, + ast.Sub: operator.sub, +} +_TRADE = re.compile( + r"^[\s*·\-○◇]*([가-힣][가-힣\s]*?)\s*:\s*([\d,]+(?:\.\d+)?)\s*(.*?)(?:=\s*([\d,]+(?:\.\d+)?))?\s*$" +) +_SECTION = re.compile(r"^\s*(\d+)\s*\.\s*(.*)$") +_EQ_COST = re.compile(r"^\s*노\s*무\s*비\s*:\s*([\d,]+(?:\.\d+)?)\s*(.*?)(?:=\s*[\d,.]+)?\s*$") +_SKIP_SEC = re.compile(r"^(합\s*계|소\s*계|계)") +_SUBLABEL = re.compile(r"^\s*[가-하]\s*\.\s*(.*)$") +_COSTLINE = re.compile(r"^\s*(노\s*무\s*비|재\s*료\s*비|경\s*비)\s*:") + + +def trade(name: Any) -> str | None: + """노임 이름 → 우리 인력 이름(빈칸 · 옛이름 · 괄호 글 풀이) · 아니면 None.""" + names = hopyo_basis.trade_names() + flat = re.sub(r"\s+", "", str(name or "")) + return names.get(flat) or names.get(re.sub(r"\(.*?\)", "", flat)) + + +def eq_key(text: Any) -> str | None: + """장비 글 → 「굴착기|0.6」(갈래 + 로 묶음 · 크기 없으면 「굴착기|」) · 장비 아니면 None.""" + kinds = mtypes(text) + if not kinds: + return None + m = _SIZE.search(nfkc(text).replace(",", "")) + size = ( + m.group(1).rstrip("0").rstrip(".") if m and "." in m.group(1) else m.group(1) if m else "" + ) + return f"{'+'.join(sorted(kinds))}|{size}" + + +def _eval(node: ast.AST) -> float: + if isinstance(node, ast.Expression): + return _eval(node.body) + if isinstance(node, ast.Constant) and isinstance(node.value, (int, float)): + return float(node.value) + if isinstance(node, ast.BinOp) and type(node.op) in _OPS: + return _OPS[type(node.op)](_eval(node.left), _eval(node.right)) + if isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.USub): + return -_eval(node.operand) + raise ValueError("식 아님") + + +def qty_of(expr: str) -> float | None: + """「273,540 × 3인 ÷ 55㎥ × 2」 → 3/55×2 · 「59,020 ÷ 23.1 × 90%」 → 1/23.1×0.9.""" + s = re.sub(r"\([^()0-9]*\)", "", str(expr)).replace(",", "") + s = re.sub(r"(\d+(?:\.\d+)?)\s*%", r"(\1/100)", s) + s = s.replace("×", "*").replace("÷", "/").replace("X", "*").replace("x", "*") + s = re.sub(r"/\s*(?=[^\d(\s.])", " ", s) # 「인/㎥」 의 / 는 단위 + s = re.sub(r"[^\d.+\-*/() ]", " ", s) + s = re.sub(r"(?<=\d)\.(?!\d)", "", s) # 「4.」 → 4 + s = re.sub(r"\(\s*\)", " ", s).strip() + m = re.match(r"\s*\d+(?:\.\d+)?", s) + if not m: + return None + s = "1" + s[m.end() :] + s = re.sub(r"(?<=[\d)])\s+(?=[\d(])", "*", s) # 빈칸으로 붙은 수는 곱 + try: + v = _eval(ast.parse(s, mode="eval")) + except (SyntaxError, ValueError, ZeroDivisionError): + return None + return v if 0 < v < 1e4 else None + + +def _part(title: str, parent: str) -> dict[str, Any]: + return {"제목": title, "절": parent, "노임": {}, "장비": {}} + + +def sections(texts: list[str]) -> list[dict[str, Any]]: + """블록 몸 글 → 몫마다 {제목, 절, 노임, 장비} — 노임 · 장비가 하나도 없는 몫은 뺌.""" + out: list[dict[str, Any]] = [] + cur: dict[str, Any] | None = None + eq, parent = None, "" + for raw in texts: + line = str(raw).rstrip() + sec = _SECTION.match(line) + if sec and not re.match(r"^\s*\d+\.\d", line): + title = sec.group(2).strip() + if _SKIP_SEC.match(title.replace(" ", "")): + cur = None + continue + parent = title + cur = _part(title, title) + out.append(cur) + eq = eq_key(title) + continue + sub = _SUBLABEL.match(line) if cur is not None else None + if sub and not _SKIP_SEC.match(sub.group(1).replace(" ", "")): + cur = _part(f"{parent} · {sub.group(1).strip()}", parent) + out.append(cur) + if cur is None: + if out or not line.strip(): + continue + cur = _part("", "") + out.append(cur) + if re.search(r"(소계|합계|인부소계)", line.replace(" ", "")): + eq = None + continue + sub = _SUBLABEL.match(line) + if sub: + eq = eq_key(sub.group(1)) + elif not _COSTLINE.match(line) and (k := eq_key(line)): + eq = k + m = _EQ_COST.match(line) + if m and eq: + q = qty_of(f"{m.group(1)} {m.group(2)}") + if q and q < 50: + cur["장비"][eq] = cur["장비"].get(eq, 0) + q + continue + t = _TRADE.match(line) + if t and (who := trade(t.group(1))): + q = qty_of(f"{t.group(2)} {t.group(3)}") + if q is None and t.group(4): # 식을 못 풀면 금액 ÷ 노임(품 되찾기만) + q = float(t.group(4).replace(",", "")) / float(t.group(2).replace(",", "")) + if q: + cur["노임"][who] = cur["노임"].get(who, 0) + q + return [s for s in out if s["노임"] or s["장비"]] + + +def sections_stm(res: list[dict[str, Any]], texts: list[str]) -> list[dict[str, Any]]: + """STmate 자원(L · X) → 몫(자원 `절` = 글 줄 차례면 그 앞 맨 윗 절 · 글이면 그 제목 · 없으면 한 + 몫).""" + marks = [] + for at, t in enumerate(texts): + sec = _SECTION.match(t) + if sec and not re.match(r"^\s*\d+\.\d", t): + if not _SKIP_SEC.match(sec.group(2).replace(" ", "")): + marks.append((at, sec.group(2).strip())) + + def which(where: Any) -> str: + if isinstance(where, str): + return where + if isinstance(where, int) and not isinstance(where, bool): + got = [m for m in marks if m[0] <= where] + return got[-1][1] if got else "" + return "" + + secs: dict[str, dict[str, Any]] = {} + seen = set() + for x in res: + q, kind = x.get("품"), x.get("갈래") + if not q or kind not in ("L", "X") or (x.get("코드"), q, x.get("절")) in seen: + continue + seen.add((x.get("코드"), q, x.get("절"))) + title = which(x.get("절")) + s = secs.setdefault(title, _part(title, title)) + if kind == "L" and (who := trade(x.get("이름"))): + s["노임"][who] = s["노임"].get(who, 0) + float(q) + elif kind == "X": + k = eq_key(f"{x.get('이름') or ''} {x.get('규격') or ''}") or f"?{x.get('코드')}" + s["장비"][k] = s["장비"].get(k, 0) + float(q) + return [s for s in secs.values() if s["노임"] or s["장비"]] diff --git a/M02_MasterTemplete/M02_Hopyo_Verdict.py b/M02_MasterTemplete/M02_Hopyo_Verdict.py new file mode 100644 index 000000000..0539033e0 --- /dev/null +++ b/M02_MasterTemplete/M02_Hopyo_Verdict.py @@ -0,0 +1,200 @@ +"""M02 일위대가 맞춤 — 블록 하나 셈 · 판정(PLAN 338-1 · 설계 2-1 · 2-6 · 3-2). + +블록 + 줄 → 엑셀 지문(`Finger`) → 이름 · 근거 차례(`Match.b_order`) → 지문 순위 섞기(`Fit`) · +같음 1위 고정(딱 맞음이 이김) → 섞인 줄 몫 먼저 · 조합(`Combine.solve`) → 위 다섯 옵션 +거꾸로(`Option`) → +판정 맞음 · 다른 점 · 후보 + 후보 다섯(로직마다 옵션 판정 꼬리표). +글 · 꼬리표는 숫자 사실만(엑셀 · 우리 품) — 「옛 판」 같은 까닭은 짓지 않음. +""" + +from __future__ import annotations + +from typing import Any + +from M02_MasterTemplete import M02_Hopyo_Basis as hopyo_basis +from M02_MasterTemplete import M02_Hopyo_Combine as combine +from M02_MasterTemplete import M02_Hopyo_Finger as finger +from M02_MasterTemplete import M02_Hopyo_Fit as fit +from M02_MasterTemplete import M02_Hopyo_Match as hopyo_match +from M02_MasterTemplete import M02_Hopyo_Option as option + + +def _num(x: float) -> str: + return f"{x:.4g}" + + +def excel_qty(efp: dict[str, Any]) -> dict[str, Any]: + """엑셀 품(화면 꼴) — {인력: {직종: 품}, 장비: [{갈래, 시간}], 자재: [{이름, 수량}]}.""" + return { + "인력": {k: round(x["품"], 6) for k, x in efp["인력"].items() if x.get("품")}, + "장비": [{"갈래": m["갈래"], "시간": m["시간"]} for m in efp["장비"] if m.get("시간")], + "자재": [{"이름": m["이름"], "수량": m["품"]} for m in efp["자재"] if m.get("품")], + } + + +def _public(efp: dict[str, Any]) -> dict[str, Any]: + keep = ("인력", "장비", "자재", "출처", "조건", "옛판", "율", "계수") + return {k: efp.get(k) for k in keep} + + +def _label(code: str, values: dict[str, Any]) -> str: + """옵션 요약 — 고르기 글은 그대로 · 수 칸은 「칸 값」 · 기계 코드는 고르기이름.""" + specs = {s.get("이름"): s for s in hopyo_match._logic_specs(code)} + out = [] + for name, v in values.items(): + names = (specs.get(name) or {}).get("고르기이름") or {} + out.append(str(names.get(v) or v) if isinstance(v, str) else f"{name} {v}") + return " · ".join(out) + + +def _part_fp(part: dict[str, Any], x: float, line: dict[str, Any]) -> dict[str, Any]: + """조합 몫 → 옵션 찾기용 지문(몫 x 를 풀어 100% 품).""" + fp = finger.empty(line) + k = x or 1.0 + for t, q in (part.get("노임") or {}).items(): + fp["인력"][t] = {"품": q / k, "밑품": q / k, "율": [], "어떻게": ["몫"]} + return fp + + +def _diffs(efp: dict, found: dict) -> list[str]: + """숫자 사실 꼬리표 — 직종 품 다름 · 섞음 · 율 · 엑셀만 · 우리만 장비.""" + tags = [] + ours_l = (found.get("우리품") or {}).get("인력") or {} + for t, x in efp["인력"].items(): + a, b = x.get("품"), ours_l.get(t) + if a and b and not option.near(a, b, fit.TOL[2]): + tags.append(f"{t} 엑셀 {_num(a)} · 우리 {_num(b)}") + elif a and not b: + tags.append(f"{t} 엑셀만 {_num(a)}") + tags += [f"{t} 우리만 {_num(q)}" for t, q in ours_l.items() if t not in efp["인력"]] + for x in efp["인력"].values(): + for v, note in x.get("율") or []: + tag = f"섞음 — 엑셀이 × {_num(v)}" if note == "섞음" else f"율 {note} × {_num(v)}" + if v and tag not in tags: + tags.append(tag) + tags += [d for d in ((found.get("견줌") or {}).get("덧") or []) if d not in tags] + return tags + + +def _state(efp: dict, code: str, found: dict | None, theirs: list) -> str: + lvl = 9 if (found is None or fit.single(efp)) else found["수준"] + if lvl <= 1: + return "맞음" + our = hopyo_basis.ours().get(code) + clause_ok = bool(our and theirs and hopyo_match.clause(theirs, our)[0] >= 0.9) + if lvl == 2 or (found and found.get("직종같음") and clause_ok): + return "다른 점" + return "후보" + + +def _no_numbers(entry: dict, top: list[str], why: str) -> dict[str, Any]: + entry["순위"] = [{"로직": k, "점수": None, "B": i} for i, k in enumerate(top[: fit.TOP_OPT])] + entry["차례"] = top[: fit.TOP_OPT] + entry["판정"] = {"상태": "후보", "글": why, "꼬리표": [why]} + return entry + + +def block(block_: dict | None, line: dict[str, Any], got: dict[str, Any]) -> dict[str, Any]: + """블록(또는 `{_지문}` 직접 줄 호표) + 줄 → 맞춤셈 한 칸(설계 3-2).""" + if block_ and "_지문" in block_: + efp, text = block_["_지문"], [] + else: + efp, text = finger.excel(block_, line), (block_ or {}).get("글") or [] + border = hopyo_match.b_order(line, got, text) + entry: dict[str, Any] = {"지문": _public(efp), "품": excel_qty(efp), "B": border["상태"]} + entry.update(옵션={}, 조합=None) + if not fit.has_numbers(efp): + return _no_numbers(entry, border["top"], fit.NO_NUMBER) + if fit.prepared() is None: + return _no_numbers(entry, border["top"], "우리 로직 준비 중") + e = fit.ex_prep(efp) + pool = fit.rank(e) + btop = border["top"] + scores = {k: s for s, k, _, _ in pool} + fused = fit.fuse(e, pool, btop) + if border["상태"] == "같음": # 같음 1위 고정 — 지문 딱 맞음이 다른 로직이면 지문 1위 + first = pool[0][1] if fit.sure(e, pool) and pool[0][1] != btop[0] else btop[0] + top = [first] + [k for k in fused if k != first][:4] + else: + top = fused + parts = efp.get("몫") or [] + if parts and btop and combine.is_mixed(parts, line): # 섞인 줄 — 이름 · 근거 차례로 몫 먼저 + top = btop[: fit.TOP_OPT] + top, combo = combine.solve(top, parts, line, got, border["합침"]) + opts = {k: option.find(k, efp, text) for k in top[: fit.TOP_OPT]} + for c in combo: + if c["로직"] not in opts and c["체크"]: + part = next((p for p in parts if p.get("제목") == c["어디"]), None) + pfp = _part_fp(part, c["몫"], line) if part else efp + opts[c["로직"]] = option.find(c["로직"], pfp, text) if pfp["인력"] else None + theirs = efp["출처"] + chans = {k: ch for _, k, ch, _ in pool} + entry["순위"] = [ + { + "로직": k, + "점수": round(scores[k], 3) if k in scores else None, + "채널": {a: round(b, 3) for a, b in (chans.get(k) or {}).items() if b is not None}, + "B": btop.index(k) if k in btop else None, + } + for k in top[: fit.TOP_OPT] + ] + entry["차례"] = top[: fit.TOP_OPT] + entry["옵션"] = opts + entry["조합"] = combo or None + entry["판정"] = judge(efp, top, opts, theirs, got) + entry["후보"] = candidates(efp, top, opts, theirs, got, scores) + return entry + + +def judge(efp: dict, top: list[str], opts: dict, theirs: list, got: dict) -> dict[str, Any]: + """1위 판정 {상태, 글, 꼬리표}.""" + if not top: + return {"상태": "후보", "글": "후보 없음", "꼬리표": []} + names = {x["로직"]: x["이름"] for x in got["로직"]} + t1, found = top[0], opts.get(top[0]) or {} + state = _state(efp, t1, opts.get(t1), theirs) + exact = [k for k in top[: fit.TOP_OPT] if _state(efp, k, opts.get(k), theirs) == "맞음"] + tags = [] + if state == "맞음": + label = _label(t1, found.get("입력") or {}) + text = " · ".join(x for x in ("맞음 — " + t1, names.get(t1, ""), label) if x) + if len(exact) > 1: + tags.append("맞음 둘") + if found.get("갈리는칸"): + tags.append(f"{' · '.join(found['갈리는칸'])} 못 가름") + return {"상태": state, "글": text, "꼬리표": tags} + if state == "다른 점": + tags = _diffs(efp, found) + return {"상태": state, "글": " · ".join(["다른 점 — " + t1, *tags[:2]]), "꼬리표": tags} + return {"상태": "후보", "글": f"후보 {len(top[: fit.TOP_OPT])}", "꼬리표": []} + + +def candidates( + efp: dict, top: list[str], opts: dict, theirs: list, got: dict, scores: dict +) -> list[dict[str, Any]]: + """후보 다섯 — 로직마다 옵션 판정 꼬리표 · 「맞음 둘」이면 둘째 맞음이 맨 앞.""" + by = {x["로직"]: x for x in got["로직"]} + table = hopyo_basis.ours() + out = [] + for k in top[: fit.TOP_OPT]: + found = opts.get(k) or {} + state = _state(efp, k, opts.get(k), theirs) + our = table.get(k) + sc, clauses, why = hopyo_match.clause(theirs, our) if (our and theirs) else (0, [], "") + one = by.get(k) or {"로직": k, "이름": "", "단위": ""} + text = state if state != "다른 점" else " · ".join(["다른 점", *_diffs(efp, found)[:1]]) + out.append( + { + "로직": k, + "이름": one["이름"], + "단위": one["단위"], + "점수": round(scores[k], 3) if k in scores else None, + "판정": state, + "글": text, + "근거": {"조항": clauses, "까닭": why}, + } + ) + first = [c for c in out[1:] if c["판정"] == "맞음"][:1] + if out and out[0]["판정"] == "맞음" and first: + out = [out[0], *first, *[c for c in out[1:] if c is not first[0]]] + return out diff --git a/M02_MasterTemplete/M02_Hopyo_Words.py b/M02_MasterTemplete/M02_Hopyo_Words.py new file mode 100644 index 000000000..8820efa1c --- /dev/null +++ b/M02_MasterTemplete/M02_Hopyo_Words.py @@ -0,0 +1,247 @@ +"""M02 일위대가 맞춤 낱말 · 식 도우미(PLAN 338-1 · 설계 2-2) — 엑셀 지문 · 우리 지문이 같이 씀. + +장비 갈래(`mtypes`) · 규격 수(`specnums`) · 조건 낱말(`conds` · `cond_score`) · +식 풀기(`parse_rhs` — 「단가 × 식 = 금액」 → 품 · 맨 앞 단가는 뗌 · 금액 안 씀) · 닮음(`close` · +`cosine` · +`jacc`). 값 · 문턱은 336 실험(C4 `fp_common`) 그대로. +""" + +from __future__ import annotations + +import math +import re +import unicodedata +from typing import Any + +# ── 장비 갈래(엑셀 글 · 우리 EQ 상세구분 공통) ───────────────────────────── +MACH = ( + ("굴착기", ("굴착기", "굴삭기", "백호", "b/h", "backhoe", "back-hoe", "포크레인")), + ("덤프트럭", ("덤프",)), + ("크레인", ("크레인",)), + ("브레이커", ("브레이커", "브레이카")), + ("콘크리트펌프", ("펌프차", "펌프카", "콘크리트펌프", "콘크리트 펌프")), + ("롤러", ("롤러", "로울러")), + ("불도저", ("불도저", "불도우저")), + ("집게", ("집게",)), + ("살수차", ("살수", "물탱크")), + ("모터그레이더", ("그레이더",)), + ("래머", ("래머", "램머", "래 머")), + ("콤팩터", ("콤팩터", "컴팩터")), + ("진동기", ("진동기",)), + ("발전기", ("발전기",)), + ("공기압축기", ("공기압축기", "컴프레셔", "콤프레샤")), + ("드릴", ("드릴", "천공기")), + ("커터", ("커터",)), + ("화물차", ("화물자동차", "화물차", "카고")), + ("로더", ("로더",)), + ("지게차", ("지게차",)), + ("체인톱", ("체인톱",)), + ("트랙터", ("트랙터",)), + ("믹서", ("믹서",)), + ("분무기", ("분무기",)), +) +_SPECNUM = re.compile(r"(\d+(?:\.\d+)?)\s*(㎥|m3|톤|ton|t\b|m\b|㎾|kw|ℓ|mm|hp)", re.I) + + +def nfkc(text: Any) -> str: + return unicodedata.normalize("NFKC", str(text or "")) + + +def mtypes(text: Any) -> set[str]: + t = nfkc(text).lower() + return {k for k, ws in MACH if any(w in t for w in ws)} + + +def specnums(text: Any) -> set[float]: + return {float(a) for a, _ in _SPECNUM.findall(nfkc(text))} + + +# ── 조건 낱말(축) ────────────────────────────────────────────────────── +SOIL = ("보통토사", "경질토사", "혼합토사", "자갈섞인토사", "사질토", "점질토", "토사") +ROCK = ("리핑암", "발파암", "풍화암", "연암", "보통암", "경암", "극경암") +AXES = { + "토질": SOIL + ROCK, + "메찰": ("메쌓기", "찰쌓기", "메붙임", "찰붙임", "메붙이기", "찰붙이기"), + "형": ("원형", "사각", "타원형"), + "근": ("무근", "철근"), + "난이": ("제물치장", "매우복잡", "복잡", "보통", "간단"), + "규모": ("소량", "소형", "소규모", "중규모", "대규모", "대형"), + "방법": ("인력운반", "인력", "기계", "장비", "펌프"), +} +CLASH = ("토질", "메찰", "형") # 이름에 박힌 값이 엑셀과 어긋나면 깎는 축 +_ROCKISH = re.compile(r"(\(암\)|암\)|[^가-힣]암$|^암$|암절취|\b암\b)") + + +def soil_family(word: str) -> str | None: + if word in ROCK or word == "암": + return "암" + return "토사" if word in SOIL else None + + +def conds(text: Any) -> dict[str, set[str]]: + """{축: {낱말}} + 횟수 · 개월 · Type.""" + t = re.sub(r"\s+", "", nfkc(text)) + out: dict[str, set[str]] = {} + for ax, ws in AXES.items(): + hit, rest = set(), t + for w in sorted(ws, key=len, reverse=True): + if w in rest: + hit.add(w) + rest = rest.replace(w, " ") + if ax == "토질" and _ROCKISH.search(t): + hit.add("암") + if hit: + out[ax] = hit + if n := re.findall(r"(\d)회", t): + out["횟수"] = {f"{x}회" for x in n} + if m := re.findall(r"(\d+)개월", t): + out["개월"] = {f"{x}개월" for x in m} + if ty := re.findall(r"type[-\s]*(ⅰ|ⅱ|ii|i)\b", t.lower()): + out["type"] = {"Ⅱ" if x in ("ⅱ", "ii") else "Ⅰ" for x in ty} + return out + + +def cond_score(excel: dict, name_c: dict, opt_c: dict) -> tuple[float, int]: + """(맞음 비율, 어긋남) — 엑셀 조건 낱말이 우리 이름 · 고르기 글에 있나 · 이름 축 어긋남.""" + words = [(ax, w) for ax, ws in excel.items() for w in ws] + if not words: + return 0.0, 0 + hit = 0 + for ax, w in words: + pool = name_c.get(ax, set()) | opt_c.get(ax, set()) + fams = {soil_family(x) for x in pool} + if w in pool or (ax == "토질" and soil_family(w) in fams): + hit += 1 + clash = 0 + for ax in CLASH: + if ax in excel and ax in name_c: + if ax == "토질": + ef = {soil_family(w) for w in excel[ax]} + of = {soil_family(w) for w in name_c[ax]} + clash |= bool(ef and of and not ef & of) + elif not excel[ax] & name_c[ax]: + clash = 1 + return hit / len(words), clash + + +# ── 식 풀기 — 「단가 × 식 = 금액」 → 품(단가로 나눈 몫 · 단가는 버림) ───────── +_UNITFRAC = re.compile( + r"(인|개|본|공|EA|ea|hr|ton|Ton|TON|kg|㎥|㎡|m3|m2|m|M|조|회)\s*/\s*" + r"(EA|ea|㎥|㎡|m3|m2|m|M|ton|Ton|TON|t|hr|일|개|본|조|공㎥|㎥)" +) +_UNITTAIL = re.compile( + r"(?<=[\d.)])\s*(hr|HR|EA|ea|kg|km|ton|Ton|TON|sec|m3|m2|mm|cm|M|m|t)(?![A-Za-z])" +) +_NOTE = re.compile(r"\(([^()]*[가-힣][^()]*)\)") +_LEAD = re.compile(r"^\s*(\d+(?:\.\d+)?)\s*([*/])\s*(.+)$") +_SAFE = re.compile(r"^[\d.+\-*/() ]+$") + + +def _norm(s: str) -> str: + s = nfkc(s).replace("×", "*").replace("✕", "*").replace("÷", "/") + s = s.replace("x", "*").replace("X", "*") + s = re.sub(r"(?<=\d),(?=\d{3})", "", s) + return _UNITFRAC.sub("", s) + + +def _split(rest: str, first_op: str) -> list[tuple[str, str]]: + """맨 바깥 * / 로 쪼갬 → [(연산, 조각)].""" + out, depth, cur, op = [], 0, "", first_op + for ch in rest: + depth += (ch == "(") - (ch == ")") + if depth == 0 and ch in "*/": + out.append((op, cur)) + op, cur = ch, "" + continue + cur += ch + out.append((op, cur)) + return out + + +def _factor(text: str) -> tuple[float, bool, str] | None: + """조각 → (값, 율 표시, 메모) · 변수(P · Q …)면 None.""" + notes = _NOTE.findall(text) + t = _NOTE.sub("", text) + pct = "%" in t + if re.search(r"\d\s*회", t): # 「× 2회」 — 횟수 곱(율로 가름) + notes.append("회") + t = re.sub(r"[가-힣㎥㎡㎝㎜℃˚°'\"]", "", t).replace("%", "") + t = _UNITTAIL.sub("", t).strip() + if not t or re.search(r"[A-Za-z]", t) or not _SAFE.match(t): + return None + try: + v = eval(t, {"__builtins__": {}}, {}) # noqa: S307 — 숫자 · 연산 글만(_SAFE) + except Exception: # noqa: BLE001 — 0 나눔 · 괄호 짝 틀림 + return None + return float(v) / (100 if pct else 1), pct or bool(notes), " ".join(notes) + + +def _rates_any(parts: list[str]) -> list[tuple[float, str]]: + """모든 조각의 율(메모 · %) — 풀 수 있는 조각만.""" + out: list[tuple[float, str]] = [] + for seg in parts: + m = _LEAD.match(seg) + if not m: + continue + for op, piece in _split(m.group(3), m.group(2)): + f = _factor(piece) + if f and f[1] and f[0] and all(abs(f[0] - v) > 1e-9 for v, _ in out): + out.append((f[0] if op == "*" else 1 / f[0], f[2] or "%")) + return out + + +def parse_rhs(rhs: str) -> dict[str, Any] | None: + """「단가 × 식 [= 바꿔 쓴 식] = 금액」 → {단가, 품, 밑품, 율, 인자} · 못 읽으면 None.""" + parts = _norm(rhs).split("=") + if len(parts) < 2: + return None + every = _rates_any(parts[:-1]) + for seg in parts[:-1]: + m = _LEAD.match(seg) + if not m: + continue + facs = [] + for op, piece in _split(m.group(3), m.group(2)): + f = _factor(piece) + if f is None: + break + facs.append((op, *f)) + else: + if not facs or any(v == 0 and op == "/" for op, v, _, _ in facs): + continue + full = base = 1.0 + rates = [] + for op, v, rate, note in facs: + full = full * v if op == "*" else full / v + if rate: + rates.append([round(v if op == "*" else 1 / v, 6), note or "%"]) + else: + base = base * v if op == "*" else base / v + if not rates and every: # 바꿔 쓴 식엔 메모가 빠짐 — 앞 조각의 율로 밑품 + for v, _ in every: + base /= v or 1 + rates = [[round(v, 6), note] for v, note in every] + plain = [v if op == "*" else 1 / v for op, v, rate, _ in facs if not rate and v] + return {"단가": float(m.group(1)), "품": full, "밑품": base, "율": rates, "인자": plain} + return None + + +# ── 닮음 ─────────────────────────────────────────────────────────────── +def close(a: Any, b: Any, tol: float = 0.15) -> float: + """두 양수 가까움 0~1 — ln 비 3% 안 = 1 · 그 밖은 떨어짐.""" + if not a or not b or a <= 0 or b <= 0: + return 0.0 + d = abs(math.log(a / b)) + return 1.0 if d <= 0.03 else max(0.0, 1 - (d - 0.03) / (tol * 4)) + + +def cosine(a: dict, b: dict) -> float: + num = sum(a.get(k, 0) * b.get(k, 0) for k in set(a) | set(b)) + da = math.sqrt(sum(v * v for v in a.values())) + db = math.sqrt(sum(v * v for v in b.values())) + return num / (da * db) if da and db else 0.0 + + +def jacc(a: Any, b: Any) -> float: + a, b = set(a), set(b) + return len(a & b) / len(a | b) if a | b else 0.0 diff --git a/resources/tester/fixtures/m02_hopyo_golden.json b/resources/tester/fixtures/m02_hopyo_golden.json new file mode 100644 index 000000000..7750fc8a1 --- /dev/null +++ b/resources/tester/fixtures/m02_hopyo_golden.json @@ -0,0 +1,3380 @@ +[ + { + "id": "N000", + "파일": "북부청임도", + "이름": "레미콘타설(장비사용)", + "규격": "철근구조물-소량 타설", + "단위": "㎥", + "갈래": "단가산출", + "단산": 46, + "무리": "사람", + "정답": [ + "GC000198" + ], + "허용": [ + "GF000155" + ], + "짐작": false, + "복합": null, + "줄수": 13 + }, + { + "id": "N001", + "파일": "북부청임도", + "이름": "합판거푸집", + "규격": "4회", + "단위": "㎡", + "갈래": "단가산출", + "단산": 24, + "무리": "사람", + "정답": [ + "GC000211" + ], + "허용": [ + "GF000160" + ], + "짐작": false, + "복합": null, + "줄수": 11 + }, + { + "id": "N002", + "파일": "북부청임도", + "이름": "기초잡석채집,운반,다짐", + "규격": "발파암활용,덤프15톤,L=500m", + "단위": "㎥", + "갈래": "단가산출", + "단산": 33, + "무리": "사람", + "정답": [ + "GC000110", + "GF000168" + ], + "허용": [], + "짐작": true, + "복합": null, + "줄수": 18 + }, + { + "id": "N003", + "파일": "북부청임도", + "이름": "철근가공조립(현장)", + "규격": "Type-Ⅰ(소량 시공)", + "단위": "Ton", + "갈래": "단가산출", + "단산": 47, + "무리": "사람", + "정답": [ + "GC000206", + "GC000207" + ], + "허용": [ + "GF000159" + ], + "짐작": false, + "복합": [ + "GC000206", + "GC000207" + ], + "줄수": 12 + }, + { + "id": "N004", + "파일": "북부청임도", + "이름": "야면석채집 및 운반(계곡내)", + "규격": "굴삭기0.6㎥,35cm,4.5톤,L=500m", + "단위": "㎡", + "갈래": "단가산출", + "단산": 6, + "무리": "사람", + "정답": [ + "GF000214" + ], + "허용": [], + "짐작": true, + "복합": null, + "줄수": 1 + }, + { + "id": "N005", + "파일": "북부청임도", + "이름": "고임돌 채집 및 운반", + "규격": "굴삭기0.6㎥(90%),4.5톤,L=500m", + "단위": "㎥", + "갈래": "단가산출", + "단산": 3, + "무리": "사람", + "정답": [ + "GF000211" + ], + "허용": [ + "GF000212" + ], + "짐작": true, + "복합": null, + "줄수": 1 + }, + { + "id": "N006", + "파일": "북부청임도", + "이름": "야면석소운반", + "규격": "L=35cm", + "단위": "㎡", + "갈래": "단가산출", + "단산": 10, + "무리": "사람", + "정답": [ + "GF000280" + ], + "허용": [ + "GF000279" + ], + "짐작": true, + "복합": null, + "줄수": 3 + }, + { + "id": "N007", + "파일": "북부청임도", + "이름": "야면석찰붙임", + "규격": "L3=35cm적용,장비사용", + "단위": "㎡", + "갈래": "단가산출", + "단산": 39, + "무리": "사람", + "정답": [ + "GF000225" + ], + "허용": [ + "GC000263" + ], + "짐작": false, + "복합": null, + "줄수": 7 + }, + { + "id": "N008", + "파일": "북부청임도", + "이름": "구조물터파기(인력100%)", + "규격": "토사", + "단위": "㎥", + "갈래": "단가산출", + "단산": 93, + "무리": "사람", + "정답": [ + "GC000083" + ], + "허용": [ + "GF000101" + ], + "짐작": false, + "복합": null, + "줄수": 4 + }, + { + "id": "N009", + "파일": "북부청임도", + "이름": "구조물되메우기(인력100%)", + "규격": "", + "단위": "㎥", + "갈래": "단가산출", + "단산": 12, + "무리": "사람", + "정답": [ + "GF000113" + ], + "허용": [], + "짐작": false, + "복합": null, + "줄수": 4 + }, + { + "id": "N010", + "파일": "북부청임도", + "이름": "잔토처리(인력100%)", + "규격": "", + "단위": "㎥", + "갈래": "단가산출", + "단산": 13, + "무리": "사람", + "정답": [], + "허용": [], + "짐작": false, + "복합": null, + "줄수": 4 + }, + { + "id": "N011", + "파일": "북부청임도", + "이름": "야면석채집및운반(계곡내)", + "규격": "굴삭기0.6㎥,35cm,4.5톤,L=500m", + "단위": "㎡", + "갈래": "단가산출", + "단산": 6, + "무리": "사람", + "정답": [ + "GF000214" + ], + "허용": [], + "짐작": true, + "복합": null, + "줄수": 7 + }, + { + "id": "N012", + "파일": "북부청임도", + "이름": "고임돌채집및운반", + "규격": "굴삭기0.6㎥(90%),4.5톤,L=500m", + "단위": "㎥", + "갈래": "단가산출", + "단산": 3, + "무리": "사람", + "정답": [ + "GF000211" + ], + "허용": [ + "GF000212" + ], + "짐작": true, + "복합": null, + "줄수": 17 + }, + { + "id": "N013", + "파일": "북부청임도", + "이름": "레미콘타설(장비사용)", + "규격": "무근구조물", + "단위": "㎥", + "갈래": "단가산출", + "단산": 22, + "무리": "사람", + "정답": [ + "GC000198" + ], + "허용": [ + "GF000155" + ], + "짐작": false, + "복합": null, + "줄수": 13 + }, + { + "id": "N014", + "파일": "북부청임도", + "이름": "유로폼", + "규격": "보통,0~7m", + "단위": "㎡", + "갈래": "단가산출", + "단산": 26, + "무리": "사람", + "정답": [ + "GC000214" + ], + "허용": [ + "GF000210" + ], + "짐작": false, + "복합": null, + "줄수": 13 + }, + { + "id": "N015", + "파일": "북부청임도", + "이름": "문양거푸집", + "규격": "", + "단위": "㎡", + "갈래": "단가산출", + "단산": 18, + "무리": "사람", + "정답": [ + "GC000215" + ], + "허용": [ + "GF000161" + ], + "짐작": false, + "복합": null, + "줄수": 4 + }, + { + "id": "N016", + "파일": "북부청임도", + "이름": "P.V.C파이프설치", + "규격": "Φ50mm", + "단위": "M", + "갈래": "단가산출", + "단산": 37, + "무리": "사람", + "정답": [ + "GF000192" + ], + "허용": [], + "짐작": false, + "복합": null, + "줄수": 11 + }, + { + "id": "N017", + "파일": "북부청임도", + "이름": "다웰바설치", + "규격": "Φ32×1000", + "단위": "EA", + "갈래": "단가산출", + "단산": 38, + "무리": "사람", + "정답": [ + "GC000226" + ], + "허용": [ + "GF000201" + ], + "짐작": false, + "복합": null, + "줄수": 4 + }, + { + "id": "N018", + "파일": "북부청임도", + "이름": "휠라설치", + "규격": "T=15mm,1000×1500mm", + "단위": "㎡", + "갈래": "단가산출", + "단산": 41, + "무리": "사람", + "정답": [ + "GC000227" + ], + "허용": [], + "짐작": false, + "복합": null, + "줄수": 4 + }, + { + "id": "N019", + "파일": "북부청임도", + "이름": "철근가공조립(현장)", + "규격": "연결철근,Type-Ⅰ×30%적용", + "단위": "Ton", + "갈래": "단가산출", + "단산": 29, + "무리": "사람", + "정답": [ + "GC000206", + "GC000207" + ], + "허용": [ + "GF000159" + ], + "짐작": false, + "복합": [ + "GC000206", + "GC000207" + ], + "줄수": 4 + }, + { + "id": "N020", + "파일": "북부청임도", + "이름": "큰돌채집운반", + "규격": "공사중 발생석 활용 50%적용", + "단위": "㎡", + "갈래": "단가산출", + "단산": 30, + "무리": "사람", + "정답": [ + "GF000213" + ], + "허용": [], + "짐작": false, + "복합": null, + "줄수": 4 + }, + { + "id": "N021", + "파일": "북부청임도", + "이름": "큰돌쌓기(메쌓기)", + "규격": "직경40cm이상~60cm미만", + "단위": "㎡", + "갈래": "단가산출", + "단산": 32, + "무리": "사람", + "정답": [ + "GF000226" + ], + "허용": [ + "GC000260" + ], + "짐작": false, + "복합": null, + "줄수": 2 + }, + { + "id": "N022", + "파일": "북부청임도", + "이름": "뒷채움돌채집운반뒷채움(발파)", + "규격": "굴삭기0.6㎥(90%),4.5톤,L=500m", + "단위": "㎥", + "갈래": "단가산출", + "단산": 2, + "무리": "사람", + "정답": [ + "GC000106" + ], + "허용": [ + "GF000218" + ], + "짐작": true, + "복합": null, + "줄수": 11 + }, + { + "id": "N023", + "파일": "북부청임도", + "이름": "큰돌쌓기(찰쌓기)_배수공", + "규격": "직경40cm이상~60cm미만", + "단위": "㎡", + "갈래": "단가산출", + "단산": 31, + "무리": "사람", + "정답": [ + "GF000227" + ], + "허용": [], + "짐작": false, + "복합": null, + "줄수": 2 + }, + { + "id": "N024", + "파일": "북부청임도", + "이름": "레미콘타설(인력운반)", + "규격": "무근구조물-소량 타설", + "단위": "㎥", + "갈래": "단가산출", + "단산": 21, + "무리": "사람", + "정답": [ + "GC000198" + ], + "허용": [ + "GF000155" + ], + "짐작": false, + "복합": null, + "줄수": 3 + }, + { + "id": "N025", + "파일": "북부청임도", + "이름": "합판거푸집", + "규격": "6회", + "단위": "㎡", + "갈래": "단가산출", + "단산": 25, + "무리": "사람", + "정답": [ + "GC000211" + ], + "허용": [ + "GF000160" + ], + "짐작": false, + "복합": null, + "줄수": 12 + }, + { + "id": "N026", + "파일": "북부청임도", + "이름": "도로반사경설치", + "규격": "D=1000(STS)", + "단위": "개소", + "갈래": "단가산출", + "단산": 42, + "무리": "사람", + "정답": [ + "GC000310" + ], + "허용": [], + "짐작": false, + "복합": null, + "줄수": 1 + }, + { + "id": "N027", + "파일": "북부청임도", + "이름": "야면석메붙임", + "규격": "L3 =35cm내외,장비사용", + "단위": "㎡", + "갈래": "단가산출", + "단산": 88, + "무리": "사람", + "정답": [ + "GF000224" + ], + "허용": [ + "GC000262" + ], + "짐작": false, + "복합": null, + "줄수": 1 + }, + { + "id": "N028", + "파일": "북부청임도", + "이름": "큰돌메붙임", + "규격": "직경40cm이상~60cm미만", + "단위": "㎡", + "갈래": "단가산출", + "단산": 92, + "무리": "사람", + "정답": [ + "GF000229" + ], + "허용": [ + "GC000262" + ], + "짐작": false, + "복합": null, + "줄수": 2 + }, + { + "id": "N029", + "파일": "북부청임도", + "이름": "큰돌찰붙임", + "규격": "직경40cm이상~60cm미만", + "단위": "㎡", + "갈래": "단가산출", + "단산": 91, + "무리": "사람", + "정답": [ + "GF000230" + ], + "허용": [ + "GC000263" + ], + "짐작": false, + "복합": null, + "줄수": 4 + }, + { + "id": "N030", + "파일": "북부청임도", + "이름": "레미콘타설(장비사용)", + "규격": "소형구조물", + "단위": "㎥", + "갈래": "단가산출", + "단산": 57, + "무리": "사람", + "정답": [ + "GC000198" + ], + "허용": [ + "GF000155" + ], + "짐작": false, + "복합": null, + "줄수": 2 + }, + { + "id": "N031", + "파일": "북부청임도", + "이름": "야면석찰쌓기", + "규격": "L3=35cm적용,장비사용", + "단위": "㎡", + "갈래": "단가산출", + "단산": 71, + "무리": "사람", + "정답": [ + "GF000222" + ], + "허용": [ + "GC000261" + ], + "짐작": false, + "복합": null, + "줄수": 2 + }, + { + "id": "N032", + "파일": "북부청임도", + "이름": "면고르기", + "규격": "", + "단위": "㎡", + "갈래": "단가산출", + "단산": 43, + "무리": "사람", + "정답": [ + "GC000111" + ], + "허용": [ + "GF000124" + ], + "짐작": false, + "복합": null, + "줄수": 4 + }, + { + "id": "N033", + "파일": "북부청임도", + "이름": "대형브레이카", + "규격": "+굴착기(0.7㎥)", + "단위": "hr", + "갈래": "기계", + "무리": "사람", + "정답": [ + "GC000271" + ], + "허용": [], + "짐작": false, + "복합": null, + "줄수": 3 + }, + { + "id": "N034", + "파일": "북부청임도", + "이름": "와이어메시바닥깔기", + "규격": "#6100×100", + "단위": "㎡", + "갈래": "단가산출", + "단산": 60, + "무리": "사람", + "정답": [ + "GC000665" + ], + "허용": [], + "짐작": false, + "복합": null, + "줄수": 1 + }, + { + "id": "N035", + "파일": "북부청임도", + "이름": "원형돌망태 설치 및 돌채움", + "규격": "#8,45×45 cm", + "단위": "㎡", + "갈래": "단가산출", + "단산": 62, + "무리": "사람", + "정답": [ + "GF000328" + ], + "허용": [], + "짐작": false, + "복합": null, + "줄수": 6 + }, + { + "id": "N036", + "파일": "북부청임도", + "이름": "조약돌채집및운반", + "규격": "굴삭기0.6㎥(90%),4.5톤,L=500m", + "단위": "㎥", + "갈래": "단가산출", + "단산": 61, + "무리": "사람", + "정답": [ + "GF000211" + ], + "허용": [], + "짐작": true, + "복합": null, + "줄수": 5 + }, + { + "id": "N037", + "파일": "북부청임도", + "이름": "돌망태형 옹벽설치", + "규격": "사각돌망태", + "단위": "㎡", + "갈래": "단가산출", + "단산": 63, + "무리": "사람", + "정답": [ + "GF000235" + ], + "허용": [ + "GC000342" + ], + "짐작": false, + "복합": null, + "줄수": 4 + }, + { + "id": "N038", + "파일": "북부청임도", + "이름": "콘크리트포장(인력)", + "규격": "T=20cm, 종단15%미만", + "단위": "㎥", + "갈래": "단가산출", + "단산": 65, + "무리": "사람", + "정답": [ + "GC000297" + ], + "허용": [ + "GF000162" + ], + "짐작": false, + "복합": null, + "줄수": 4 + }, + { + "id": "N039", + "파일": "북부청임도", + "이름": "비탈면 보호공", + "규격": "평떼", + "단위": "㎡", + "갈래": "단가산출", + "단산": 64, + "무리": "사람", + "정답": [ + "GC000143" + ], + "허용": [ + "GF000032" + ], + "짐작": false, + "복합": null, + "줄수": 6 + }, + { + "id": "N040", + "파일": "북부청임도", + "이름": "콘크리트포장(인력)", + "규격": "T=20cm,종단15%이상", + "단위": "㎥", + "갈래": "단가산출", + "단산": 66, + "무리": "사람", + "정답": [ + "GC000297" + ], + "허용": [ + "GF000162" + ], + "짐작": false, + "복합": null, + "줄수": 3 + }, + { + "id": "N041", + "파일": "북부청임도", + "이름": "고화매트 설치", + "규격": "", + "단위": "㎡", + "갈래": "단가산출", + "단산": 70, + "무리": "사람", + "정답": [], + "허용": [], + "짐작": false, + "복합": null, + "줄수": 1 + }, + { + "id": "N042", + "파일": "북부청임도", + "이름": "큰돌쌓기(찰쌓기)", + "규격": "직경40cm이상~60cm미만", + "단위": "㎡", + "갈래": "단가산출", + "단산": 90, + "무리": "사람", + "정답": [ + "GF000227" + ], + "허용": [], + "짐작": false, + "복합": null, + "줄수": 1 + }, + { + "id": "N043", + "파일": "북부청임도", + "이름": "레미콘타설(장비사용)", + "규격": "철근구조물", + "단위": "㎥", + "갈래": "단가산출", + "단산": 23, + "무리": "사람", + "정답": [ + "GC000198" + ], + "허용": [ + "GF000155" + ], + "짐작": false, + "복합": null, + "줄수": 7 + }, + { + "id": "N044", + "파일": "북부청임도", + "이름": "강관동바리", + "규격": "암거용-1개월사용", + "단위": "공/㎥", + "갈래": "단가산출", + "단산": 73, + "무리": "사람", + "정답": [ + "GC000015" + ], + "허용": [ + "GF000189" + ], + "짐작": false, + "복합": null, + "줄수": 7 + }, + { + "id": "N045", + "파일": "북부청임도", + "이름": "철근가공조립(현장)", + "규격": "Type-ll", + "단위": "Ton", + "갈래": "단가산출", + "단산": 72, + "무리": "사람", + "정답": [ + "GC000206", + "GC000207" + ], + "허용": [ + "GF000159" + ], + "짐작": false, + "복합": [ + "GC000206", + "GC000207" + ], + "줄수": 7 + }, + { + "id": "N046", + "파일": "북부청임도", + "이름": "강관비계", + "규격": "10m이하,3개월사용", + "단위": "㎡", + "갈래": "단가산출", + "단산": 74, + "무리": "사람", + "정답": [ + "GC000022" + ], + "허용": [ + "GF000188" + ], + "짐작": false, + "복합": null, + "줄수": 6 + }, + { + "id": "N047", + "파일": "북부청임도", + "이름": "흙깍기(토사)", + "규격": "굴삭기1.0㎥,보통토사", + "단위": "㎥", + "갈래": "단가산출", + "단산": 49, + "무리": "사람", + "정답": [ + "GC000085" + ], + "허용": [ + "GF000089" + ], + "짐작": false, + "복합": null, + "줄수": 1 + }, + { + "id": "N048", + "파일": "북부청임도", + "이름": "잔토정리", + "규격": "굴삭기0.6㎥", + "단위": "㎥", + "갈래": "단가산출", + "단산": 56, + "무리": "사람", + "정답": [], + "허용": [], + "짐작": true, + "복합": null, + "줄수": 1 + }, + { + "id": "N049", + "파일": "북부청임도", + "이름": "잡철물 제작설치", + "규격": "규격철물 설치(일반철재)", + "단위": "Ton", + "갈래": "단가산출", + "단산": 82, + "무리": "사람", + "정답": [ + "GC000676" + ], + "허용": [], + "짐작": false, + "복합": null, + "줄수": 1 + }, + { + "id": "N050", + "파일": "북부청임도", + "이름": "도장(철재면2회)", + "규격": "방청1,유성2", + "단위": "㎡", + "갈래": "단가산출", + "단산": 95, + "무리": "사람", + "정답": [ + "GC000705", + "GC000714", + "GC000712" + ], + "허용": [], + "짐작": false, + "복합": [ + "GC000705", + "GC000714", + "GC000712" + ], + "줄수": 1 + }, + { + "id": "N051", + "파일": "북부청임도", + "이름": "임도안전표지판설치", + "규격": "삼각,900mm", + "단위": "개소", + "갈래": "단가산출", + "단산": 81, + "무리": "사람", + "정답": [ + "GC000308" + ], + "허용": [], + "짐작": false, + "복합": null, + "줄수": 1 + }, + { + "id": "S052", + "파일": "북부청사방", + "이름": "레미콘타설(장비사용)", + "규격": "철근구조물", + "단위": "㎥", + "갈래": "단가산출", + "단산": 22, + "무리": "사람", + "정답": [ + "GC000198" + ], + "허용": [ + "GF000155" + ], + "짐작": false, + "복합": null, + "줄수": 1 + }, + { + "id": "S053", + "파일": "북부청사방", + "이름": "레미콘타설(장비사용)", + "규격": "무근구조물", + "단위": "㎥", + "갈래": "단가산출", + "단산": 20, + "무리": "사람", + "정답": [ + "GC000198" + ], + "허용": [ + "GF000155" + ], + "짐작": false, + "복합": null, + "줄수": 4 + }, + { + "id": "S054", + "파일": "북부청사방", + "이름": "유로폼(Euro Form)", + "규격": "6회, 보통", + "단위": "㎡", + "갈래": "단가산출", + "단산": 38, + "무리": "사람", + "정답": [ + "GC000214" + ], + "허용": [ + "GF000210" + ], + "짐작": false, + "복합": null, + "줄수": 2 + }, + { + "id": "S055", + "파일": "북부청사방", + "이름": "철근가공조립(현장)", + "규격": "Type-Ⅱ", + "단위": "Ton", + "갈래": "단가산출", + "단산": 53, + "무리": "사람", + "정답": [ + "GC000206", + "GC000207" + ], + "허용": [ + "GF000159" + ], + "짐작": false, + "복합": [ + "GC000206", + "GC000207" + ], + "줄수": 1 + }, + { + "id": "S056", + "파일": "북부청사방", + "이름": "강관비계", + "규격": "(10m이하)", + "단위": "㎡", + "갈래": "단가산출", + "단산": 4, + "무리": "사람", + "정답": [ + "GC000022" + ], + "허용": [ + "GF000188" + ], + "짐작": false, + "복합": null, + "줄수": 2 + }, + { + "id": "S057", + "파일": "북부청사방", + "이름": "강관동바리", + "규격": "(2.5m이하)", + "단위": "공/㎥", + "갈래": "단가산출", + "단산": 3, + "무리": "사람", + "정답": [ + "GC000015" + ], + "허용": [ + "GF000189" + ], + "짐작": false, + "복합": null, + "줄수": 1 + }, + { + "id": "S058", + "파일": "북부청사방", + "이름": "유로폼(Euro Form)", + "규격": "6회, 간단", + "단위": "㎡", + "갈래": "단가산출", + "단산": 37, + "무리": "사람", + "정답": [ + "GC000214" + ], + "허용": [ + "GF000210" + ], + "짐작": false, + "복합": null, + "줄수": 1 + }, + { + "id": "S059", + "파일": "북부청사방", + "이름": "철근가공조립(현장)", + "규격": "Type-Ⅰ(소량)", + "단위": "Ton", + "갈래": "단가산출", + "단산": 52, + "무리": "사람", + "정답": [ + "GC000206", + "GC000207" + ], + "허용": [ + "GF000159" + ], + "짐작": false, + "복합": [ + "GC000206", + "GC000207" + ], + "줄수": 3 + }, + { + "id": "S060", + "파일": "북부청사방", + "이름": "철강재운반(할증포함)", + "규격": "(구역화물)", + "단위": "Ton", + "갈래": "단가산출", + "단산": 49, + "무리": "사람", + "정답": [], + "허용": [], + "짐작": true, + "복합": null, + "줄수": 1 + }, + { + "id": "S061", + "파일": "북부청사방", + "이름": "철골설치", + "규격": "", + "단위": "Ton", + "갈래": "단가산출", + "단산": 51, + "무리": "사람", + "정답": [ + "GC000563" + ], + "허용": [ + "GC000561" + ], + "짐작": true, + "복합": null, + "줄수": 1 + }, + { + "id": "S062", + "파일": "북부청사방", + "이름": "철골가공조립(공장)", + "규격": "(대형)", + "단위": "Ton", + "갈래": "단가산출", + "단산": 50, + "무리": "사람", + "정답": [ + "GC000559" + ], + "허용": [ + "GC000560" + ], + "짐작": false, + "복합": null, + "줄수": 1 + }, + { + "id": "S063", + "파일": "북부청사방", + "이름": "잡철물제작", + "규격": "", + "단위": "Ton", + "갈래": "단가산출", + "단산": 42, + "무리": "사람", + "정답": [ + "GC000676" + ], + "허용": [], + "짐작": false, + "복합": null, + "줄수": 2 + }, + { + "id": "S064", + "파일": "북부청사방", + "이름": "합판거푸집(6회)", + "규격": "", + "단위": "㎡", + "갈래": "단가산출", + "단산": 67, + "무리": "사람", + "정답": [ + "GC000211" + ], + "허용": [ + "GF000160" + ], + "짐작": false, + "복합": null, + "줄수": 7 + }, + { + "id": "S065", + "파일": "북부청사방", + "이름": "와이어메시 바닥깔기", + "규격": "", + "단위": "㎡", + "갈래": "단가산출", + "단산": 33, + "무리": "사람", + "정답": [ + "GC000665" + ], + "허용": [], + "짐작": false, + "복합": null, + "줄수": 2 + }, + { + "id": "S066", + "파일": "북부청사방", + "이름": "구조물터파기(토사)", + "규격": "", + "단위": "㎥", + "갈래": "단가산출", + "단산": 11, + "무리": "사람", + "정답": [ + "GC000089" + ], + "허용": [ + "GC000088" + ], + "짐작": false, + "복합": null, + "줄수": 18 + }, + { + "id": "S067", + "파일": "북부청사방", + "이름": "구조물되메우기 및 다짐", + "규격": "", + "단위": "㎥", + "갈래": "단가산출", + "단산": 8, + "무리": "사람", + "정답": [ + "GC000109" + ], + "허용": [], + "짐작": false, + "복합": null, + "줄수": 6 + }, + { + "id": "S068", + "파일": "북부청사방", + "이름": "잔토처리", + "규격": "", + "단위": "㎥", + "갈래": "단가산출", + "단산": 39, + "무리": "사람", + "정답": [], + "허용": [], + "짐작": true, + "복합": null, + "줄수": 18 + }, + { + "id": "S069", + "파일": "북부청사방", + "이름": "레미콘타설(장비사용)", + "규격": "무근구조물-소량타설", + "단위": "㎥", + "갈래": "단가산출", + "단산": 21, + "무리": "사람", + "정답": [ + "GC000198" + ], + "허용": [ + "GF000155" + ], + "짐작": false, + "복합": null, + "줄수": 7 + }, + { + "id": "S070", + "파일": "북부청사방", + "이름": "합판거푸집(4회)", + "규격": "", + "단위": "㎡", + "갈래": "단가산출", + "단산": 66, + "무리": "사람", + "정답": [ + "GC000211" + ], + "허용": [ + "GF000160" + ], + "짐작": false, + "복합": null, + "줄수": 1 + }, + { + "id": "S071", + "파일": "북부청사방", + "이름": "기초잡석다짐 및 채집운반", + "규격": "(9:1)(0.6㎥+4.5Ton)", + "단위": "㎥", + "갈래": "단가산출", + "단산": 12, + "무리": "사람", + "정답": [ + "GC000110", + "GF000168" + ], + "허용": [], + "짐작": true, + "복합": null, + "줄수": 1 + }, + { + "id": "S072", + "파일": "북부청사방", + "이름": "배수관부설", + "규격": "(D=1000mm)", + "단위": "M", + "갈래": "단가산출", + "단산": 26, + "무리": "사람", + "정답": [ + "GC000404" + ], + "허용": [ + "GF000176", + "GF000179" + ], + "짐작": false, + "복합": null, + "줄수": 1 + }, + { + "id": "S073", + "파일": "북부청사방", + "이름": "철골공", + "규격": "", + "단위": "인", + "갈래": "기타", + "무리": "사람", + "정답": [], + "허용": [], + "짐작": false, + "복합": null, + "줄수": 1 + }, + { + "id": "S074", + "파일": "북부청사방", + "이름": "특별인부", + "규격": "", + "단위": "인", + "갈래": "기타", + "무리": "사람", + "정답": [], + "허용": [], + "짐작": false, + "복합": null, + "줄수": 1 + }, + { + "id": "S075", + "파일": "북부청사방", + "이름": "레미콘타설(인력운반)", + "규격": "무근구조물-소량타설", + "단위": "㎥", + "갈래": "단가산출", + "단산": 19, + "무리": "사람", + "정답": [ + "GC000198" + ], + "허용": [ + "GF000155" + ], + "짐작": false, + "복합": null, + "줄수": 1 + }, + { + "id": "S076", + "파일": "북부청사방", + "이름": "구조물터파기", + "규격": "인력", + "단위": "㎥", + "갈래": "단가산출", + "단산": 9, + "무리": "사람", + "정답": [ + "GC000083" + ], + "허용": [ + "GF000101" + ], + "짐작": false, + "복합": null, + "줄수": 1 + }, + { + "id": "S077", + "파일": "북부청사방", + "이름": "구조물되메우기", + "규격": "인력", + "단위": "㎥", + "갈래": "단가산출", + "단산": 7, + "무리": "사람", + "정답": [ + "GF000113" + ], + "허용": [], + "짐작": true, + "복합": null, + "줄수": 1 + }, + { + "id": "S078", + "파일": "북부청사방", + "이름": "잔토처리", + "규격": "인력", + "단위": "㎥", + "갈래": "단가산출", + "단산": 40, + "무리": "사람", + "정답": [], + "허용": [], + "짐작": false, + "복합": null, + "줄수": 1 + }, + { + "id": "S079", + "파일": "북부청사방", + "이름": "원형돌망태조립설치", + "규격": "산림사업품셈", + "단위": "조", + "갈래": "단가산출", + "단산": 35, + "무리": "사람", + "정답": [ + "GF000328" + ], + "허용": [], + "짐작": false, + "복합": null, + "줄수": 1 + }, + { + "id": "S080", + "파일": "북부청사방", + "이름": "원형돌망태채움", + "규격": "산림사업품셈", + "단위": "조", + "갈래": "단가산출", + "단산": 36, + "무리": "사람", + "정답": [ + "GF000328" + ], + "허용": [], + "짐작": false, + "복합": null, + "줄수": 1 + }, + { + "id": "S081", + "파일": "북부청사방", + "이름": "조약돌채집운반", + "규격": "(9:1)(0.6㎥+4.5Ton)", + "단위": "㎥", + "갈래": "단가산출", + "단산": 43, + "무리": "사람", + "정답": [ + "GF000211" + ], + "허용": [], + "짐작": true, + "복합": null, + "줄수": 1 + }, + { + "id": "S082", + "파일": "북부청사방", + "이름": "구조물터파기(암)", + "규격": "", + "단위": "㎥", + "갈래": "단가산출", + "단산": 10, + "무리": "사람", + "정답": [ + "GC000090" + ], + "허용": [], + "짐작": false, + "복합": null, + "줄수": 2 + }, + { + "id": "S083", + "파일": "북부청사방", + "이름": "배수관부설", + "규격": "(D=500mm)", + "단위": "M", + "갈래": "단가산출", + "단산": 27, + "무리": "사람", + "정답": [ + "GC000404" + ], + "허용": [ + "GF000176", + "GF000179" + ], + "짐작": false, + "복합": null, + "줄수": 1 + }, + { + "id": "S084", + "파일": "북부청사방", + "이름": "레미콘타설(무근펌프카)(130㎥)", + "규격": "41m(80-95㎥/hr)", + "단위": "㎥", + "갈래": "단가산출", + "단산": 18, + "무리": "사람", + "정답": [ + "GC000201" + ], + "허용": [ + "GF000182" + ], + "짐작": false, + "복합": null, + "줄수": 1 + }, + { + "id": "S085", + "파일": "북부청사방", + "이름": "콘크리트 펌프차", + "규격": "41m,80~95㎥/hr", + "단위": "hr", + "갈래": "기계", + "무리": "사람", + "정답": [ + "GC000268", + "GC000269", + "GC000275" + ], + "허용": [], + "짐작": true, + "복합": null, + "줄수": 1 + }, + { + "id": "S086", + "파일": "북부청사방", + "이름": "문양거푸집", + "규격": "", + "단위": "㎡", + "갈래": "단가산출", + "단산": 25, + "무리": "사람", + "정답": [ + "GC000215" + ], + "허용": [ + "GF000161" + ], + "짐작": false, + "복합": null, + "줄수": 1 + }, + { + "id": "S087", + "파일": "북부청사방", + "이름": "지수판 설치", + "규격": "산림사업품셈", + "단위": "M", + "갈래": "단가산출", + "단산": 46, + "무리": "사람", + "정답": [ + "GF000196" + ], + "허용": [], + "짐작": false, + "복합": null, + "줄수": 1 + }, + { + "id": "S088", + "파일": "북부청사방", + "이름": "P.V.C 파이프 설치", + "규격": "산림사업품셈", + "단위": "M", + "갈래": "단가산출", + "단산": 1, + "무리": "사람", + "정답": [ + "GF000192" + ], + "허용": [], + "짐작": false, + "복합": null, + "줄수": 1 + }, + { + "id": "S089", + "파일": "북부청사방", + "이름": "떼붙임(평떼)", + "규격": "산림사업품셈", + "단위": "㎡", + "갈래": "단가산출", + "단산": 17, + "무리": "사람", + "정답": [ + "GF000025" + ], + "허용": [ + "GF000032" + ], + "짐작": false, + "복합": null, + "줄수": 2 + }, + { + "id": "S090", + "파일": "북부청사방", + "이름": "큰돌메쌓기", + "규격": "40cm-60cm,(산림사업품셈)", + "단위": "㎡", + "갈래": "단가산출", + "단산": 60, + "무리": "사람", + "정답": [ + "GF000226" + ], + "허용": [ + "GC000260" + ], + "짐작": false, + "복합": null, + "줄수": 1 + }, + { + "id": "S091", + "파일": "북부청사방", + "이름": "고임돌채집및운반", + "규격": "(9:1)(0.6㎥+4.5Ton)", + "단위": "㎥", + "갈래": "단가산출", + "단산": 5, + "무리": "사람", + "정답": [ + "GF000211" + ], + "허용": [ + "GF000212" + ], + "짐작": true, + "복합": null, + "줄수": 9 + }, + { + "id": "S092", + "파일": "북부청사방", + "이름": "뒷채움잡석채집운반다짐", + "규격": "(9:1)(0.6㎥+4.5Ton)", + "단위": "㎥", + "갈래": "단가산출", + "단산": 16, + "무리": "사람", + "정답": [ + "GC000106" + ], + "허용": [ + "GF000218" + ], + "짐작": true, + "복합": null, + "줄수": 4 + }, + { + "id": "S093", + "파일": "북부청사방", + "이름": "큰돌찰쌓기", + "규격": "40cm-60cm,(산림사업품셈)", + "단위": "㎡", + "갈래": "단가산출", + "단산": 62, + "무리": "사람", + "정답": [ + "GF000227" + ], + "허용": [ + "GC000261" + ], + "짐작": false, + "복합": null, + "줄수": 10 + }, + { + "id": "S094", + "파일": "북부청사방", + "이름": "큰돌채집운반", + "규격": "산림사업품셈", + "단위": "㎡", + "갈래": "단가산출", + "단산": 63, + "무리": "사람", + "정답": [ + "GF000213" + ], + "허용": [], + "짐작": false, + "복합": null, + "줄수": 5 + }, + { + "id": "S095", + "파일": "북부청사방", + "이름": "뒷채움잡석구입운반", + "규격": "", + "단위": "㎥", + "갈래": "단가산출", + "단산": 13, + "무리": "사람", + "정답": [ + "GF000135", + "GF000284" + ], + "허용": [], + "짐작": true, + "복합": [ + "GF000135", + "GF000284" + ], + "줄수": 1 + }, + { + "id": "S096", + "파일": "북부청사방", + "이름": "뒷채움잡석뒷채움", + "규격": "", + "단위": "㎥", + "갈래": "단가산출", + "단산": 15, + "무리": "사람", + "정답": [ + "GC000106" + ], + "허용": [], + "짐작": false, + "복합": null, + "줄수": 2 + }, + { + "id": "S097", + "파일": "북부청사방", + "이름": "뒷채움잡석구입운반", + "규격": "(창봉 기번3)(2차)", + "단위": "㎥", + "갈래": "단가산출", + "단산": 14, + "무리": "사람", + "정답": [ + "GF000135", + "GF000284" + ], + "허용": [], + "짐작": true, + "복합": [ + "GF000135", + "GF000284" + ], + "줄수": 1 + }, + { + "id": "S098", + "파일": "북부청사방", + "이름": "큰돌메붙임", + "규격": "40cm-60cm,(산림사업품셈)", + "단위": "㎡", + "갈래": "단가산출", + "단산": 59, + "무리": "사람", + "정답": [ + "GF000229" + ], + "허용": [ + "GC000262" + ], + "짐작": false, + "복합": null, + "줄수": 2 + }, + { + "id": "S099", + "파일": "북부청사방", + "이름": "큰돌찰붙임", + "규격": "40cm-60cm,(산림사업품셈)", + "단위": "㎡", + "갈래": "단가산출", + "단산": 61, + "무리": "사람", + "정답": [ + "GF000230" + ], + "허용": [ + "GC000263" + ], + "짐작": false, + "복합": null, + "줄수": 7 + }, + { + "id": "Y000", + "파일": "동부청영월", + "이름": "무근콘크리트타설", + "규격": "인력운반타설", + "단위": "M3", + "갈래": "단가산출", + "코드": "D02058", + "무리": "사람", + "정답": [ + "GC000198" + ], + "허용": [ + "GF000155" + ], + "짐작": false, + "복합": null, + "줄수": 3 + }, + { + "id": "Y001", + "파일": "동부청영월", + "이름": "합판거푸집(6회)", + "규격": "간단", + "단위": "M2", + "갈래": "단가산출", + "코드": "D00056", + "무리": "사람", + "정답": [ + "GC000211" + ], + "허용": [ + "GF000160" + ], + "짐작": false, + "복합": null, + "줄수": 6 + }, + { + "id": "Y002", + "파일": "동부청영월", + "이름": "구조물터파기(인력)", + "규격": "경질토사", + "단위": "M3", + "갈래": "단가산출", + "코드": "D00002", + "무리": "사람", + "정답": [ + "GF000101" + ], + "허용": [ + "GC000083" + ], + "짐작": true, + "복합": null, + "줄수": 3 + }, + { + "id": "Y003", + "파일": "동부청영월", + "이름": "구조물되메우기", + "규격": "(인력)", + "단위": "M3", + "갈래": "단가산출", + "코드": "D00007", + "무리": "사람", + "정답": [ + "GF000113" + ], + "허용": [], + "짐작": false, + "복합": null, + "줄수": 4 + }, + { + "id": "Y004", + "파일": "동부청영월", + "이름": "잔토처리", + "규격": "인력100%", + "단위": "M3", + "갈래": "단가산출", + "코드": "D00009", + "무리": "사람", + "정답": [], + "허용": [], + "짐작": false, + "복합": null, + "줄수": 4 + }, + { + "id": "Y005", + "파일": "동부청영월", + "이름": "크레인(타이어)", + "규격": "10톤", + "단위": "시간", + "갈래": "기계", + "코드": "X02104", + "무리": "사람", + "정답": [ + "GC000268", + "GC000269", + "GC000275" + ], + "허용": [], + "짐작": true, + "복합": null, + "줄수": 1 + }, + { + "id": "Y006", + "파일": "동부청영월", + "이름": "구조물터파기", + "규격": "토사(B/H 90%+인력10%)", + "단위": "M3", + "갈래": "단가산출", + "코드": "D00003", + "무리": "사람", + "정답": [ + "GF000101" + ], + "허용": [ + "GC000089" + ], + "짐작": true, + "복합": null, + "줄수": 1 + }, + { + "id": "Y007", + "파일": "동부청영월", + "이름": "잔토처리", + "규격": "토사(B/H 100%)", + "단위": "M3", + "갈래": "단가산출", + "코드": "D00010", + "무리": "사람", + "정답": [], + "허용": [], + "짐작": false, + "복합": null, + "줄수": 1 + }, + { + "id": "Y008", + "파일": "동부청영월", + "이름": "콘크리트타설", + "규격": "임도품셈 5-5", + "단위": "M3", + "갈래": "단가산출", + "코드": "D00037", + "무리": "사람", + "정답": [ + "GC000198" + ], + "허용": [ + "GF000155", + "GF000166" + ], + "짐작": true, + "복합": null, + "줄수": 6 + }, + { + "id": "Y009", + "파일": "동부청영월", + "이름": "유로폼 설치,해체(복잡)", + "규격": "수직고7.0m 이하", + "단위": "M2", + "갈래": "단가산출", + "코드": "D00059", + "무리": "사람", + "정답": [ + "GC000214" + ], + "허용": [ + "GF000210" + ], + "짐작": false, + "복합": null, + "줄수": 5 + }, + { + "id": "Y010", + "파일": "동부청영월", + "이름": "철근가공조립(현장)", + "규격": "TYPE Ⅰ", + "단위": "Ton", + "갈래": "단가산출", + "코드": "D00060", + "무리": "사람", + "정답": [ + "GC000206", + "GC000207" + ], + "허용": [ + "GF000159" + ], + "짐작": false, + "복합": [ + "GC000206", + "GC000207" + ], + "줄수": 6 + }, + { + "id": "Y011", + "파일": "동부청영월", + "이름": "기초잡석채집및운반부설", + "규격": "4.5톤(L=500)", + "단위": "M3", + "갈래": "단가산출", + "코드": "D00600", + "무리": "사람", + "정답": [ + "GF000168" + ], + "허용": [ + "GC000110" + ], + "짐작": true, + "복합": null, + "줄수": 11 + }, + { + "id": "Y012", + "파일": "동부청영월", + "이름": "임도안전표지판설치", + "규격": "삼각,D=900", + "단위": "개소", + "갈래": "단가산출", + "코드": "D01926", + "무리": "사람", + "정답": [ + "GC000308" + ], + "허용": [], + "짐작": false, + "복합": null, + "줄수": 1 + }, + { + "id": "Y013", + "파일": "동부청영월", + "이름": "야면석 채집 운반", + "규격": "(25x25x35㎝)장비", + "단위": "M2", + "갈래": "단가산출", + "코드": "D00411", + "무리": "사람", + "정답": [ + "GF000214" + ], + "허용": [], + "짐작": true, + "복합": null, + "줄수": 4 + }, + { + "id": "Y014", + "파일": "동부청영월", + "이름": "야면석찰붙임", + "규격": "뒷길이 35Cm", + "단위": "M2", + "갈래": "단가산출", + "코드": "D00644", + "무리": "사람", + "정답": [ + "GC000263" + ], + "허용": [ + "GF000225" + ], + "짐작": false, + "복합": null, + "줄수": 3 + }, + { + "id": "Y015", + "파일": "동부청영월", + "이름": "고임돌채집운반", + "규격": "4.5톤덤프(L=500)", + "단위": "M3", + "갈래": "단가산출", + "코드": "D01361", + "무리": "사람", + "정답": [ + "GF000211" + ], + "허용": [ + "GF000212" + ], + "짐작": true, + "복합": null, + "줄수": 5 + }, + { + "id": "Y016", + "파일": "동부청영월", + "이름": "잡석운반및다짐", + "규격": "", + "단위": "M3", + "갈래": "단가산출", + "코드": "D01928", + "무리": "사람", + "정답": [ + "GF000218" + ], + "허용": [], + "짐작": false, + "복합": null, + "줄수": 2 + }, + { + "id": "Y017", + "파일": "동부청영월", + "이름": "면고르기", + "규격": "", + "단위": "M2", + "갈래": "단가산출", + "코드": "D01927", + "무리": "사람", + "정답": [ + "GF000124" + ], + "허용": [ + "GC000111" + ], + "짐작": false, + "복합": null, + "줄수": 2 + }, + { + "id": "Y018", + "파일": "동부청영월", + "이름": "구조물터파기", + "규격": "인력(경질토사)", + "단위": "M3", + "갈래": "단가산출", + "코드": "D01942", + "무리": "사람", + "정답": [ + "GF000101" + ], + "허용": [ + "GC000083" + ], + "짐작": true, + "복합": null, + "줄수": 1 + }, + { + "id": "Y019", + "파일": "동부청영월", + "이름": "도로반사경설치", + "규격": "D=1000(STS)", + "단위": "개소", + "갈래": "단가산출", + "코드": "D01759", + "무리": "사람", + "정답": [ + "GC000310" + ], + "허용": [], + "짐작": false, + "복합": null, + "줄수": 1 + }, + { + "id": "Y020", + "파일": "동부청영월", + "이름": "철근콘크리트타설", + "규격": "장비사용타설", + "단위": "M3", + "갈래": "단가산출", + "코드": "D02071", + "무리": "사람", + "정답": [ + "GC000198" + ], + "허용": [ + "GF000155" + ], + "짐작": false, + "복합": null, + "줄수": 2 + }, + { + "id": "Y021", + "파일": "동부청영월", + "이름": "유로폼 설치,해체(보통)", + "규격": "수직고7.0m 이하", + "단위": "M2", + "갈래": "단가산출", + "코드": "D00058", + "무리": "사람", + "정답": [ + "GC000214" + ], + "허용": [ + "GF000210" + ], + "짐작": false, + "복합": null, + "줄수": 1 + }, + { + "id": "Y022", + "파일": "동부청영월", + "이름": "지수판 설치(PVC 용접)", + "규격": "200×5t", + "단위": "m", + "갈래": "단가산출", + "코드": "D01854", + "무리": "사람", + "정답": [ + "GC000224" + ], + "허용": [], + "짐작": false, + "복합": null, + "줄수": 1 + }, + { + "id": "Y023", + "파일": "동부청영월", + "이름": "배수관부설및접합(P.V.C)", + "규격": "Ø 50 MM - VG2 일반관", + "단위": "m", + "갈래": "단가산출", + "코드": "D01855", + "무리": "사람", + "정답": [ + "GC000393" + ], + "허용": [], + "짐작": false, + "복합": null, + "줄수": 1 + }, + { + "id": "Y024", + "파일": "동부청영월", + "이름": "야면석소운반(L=35CM)", + "규격": "(L=20m)", + "단위": "M2", + "갈래": "단가산출", + "코드": "D01466", + "무리": "사람", + "정답": [ + "GF000280" + ], + "허용": [ + "GF000279" + ], + "짐작": true, + "복합": null, + "줄수": 2 + }, + { + "id": "Y025", + "파일": "동부청영월", + "이름": "콘크리트소운반", + "규격": "(L=20m)", + "단위": "M3", + "갈래": "단가산출", + "코드": "D00450", + "무리": "사람", + "정답": [ + "GF000279", + "GF000281" + ], + "허용": [], + "짐작": true, + "복합": null, + "줄수": 1 + }, + { + "id": "Y026", + "파일": "동부청영월", + "이름": "야면석메붙임", + "규격": "뒷길이 35Cm", + "단위": "M2", + "갈래": "단가산출", + "코드": "D01843", + "무리": "사람", + "정답": [ + "GF000224", + "GC000262" + ], + "허용": [], + "짐작": false, + "복합": null, + "줄수": 1 + }, + { + "id": "Y027", + "파일": "동부청영월", + "이름": "측구수로관부설", + "규격": "500∼700kg미만", + "단위": "m", + "갈래": "단가산출", + "코드": "D00258", + "무리": "사람", + "정답": [ + "GC000251" + ], + "허용": [], + "짐작": false, + "복합": null, + "줄수": 1 + }, + { + "id": "Y028", + "파일": "동부청영월", + "이름": "콘크리트타설", + "규격": "장비사용타설(소형)", + "단위": "M3", + "갈래": "단가산출", + "코드": "D01844", + "무리": "사람", + "정답": [ + "GC000198" + ], + "허용": [ + "GF000155" + ], + "짐작": false, + "복합": null, + "줄수": 1 + }, + { + "id": "Y029", + "파일": "동부청영월", + "이름": "합판거푸집(4회)", + "규격": "보통", + "단위": "M2", + "갈래": "단가산출", + "코드": "D01841", + "무리": "사람", + "정답": [ + "GC000211" + ], + "허용": [ + "GF000160" + ], + "짐작": false, + "복합": null, + "줄수": 1 + }, + { + "id": "Y030", + "파일": "동부청영월", + "이름": "혼합골재부설", + "규격": "영월-산불", + "단위": "M3", + "갈래": "단가산출", + "코드": "D02001", + "무리": "사람", + "정답": [ + "GF000136" + ], + "허용": [], + "짐작": true, + "복합": null, + "줄수": 1 + }, + { + "id": "Y031", + "파일": "동부청영월", + "이름": "콘크리트포장(인력)", + "규격": "T=20㎝", + "단위": "M3", + "갈래": "단가산출", + "코드": "D01748", + "무리": "사람", + "정답": [ + "GF000162" + ], + "허용": [ + "GC000297" + ], + "짐작": false, + "복합": null, + "줄수": 1 + }, + { + "id": "Y032", + "파일": "동부청영월", + "이름": "콘크리트포장절단공", + "규격": "임도", + "단위": "M", + "갈래": "단가산출", + "코드": "D01752", + "무리": "사람", + "정답": [ + "GF000163" + ], + "허용": [], + "짐작": false, + "복합": null, + "줄수": 1 + }, + { + "id": "Y033", + "파일": "동부청영월", + "이름": "뒷채움돌채집운반부설", + "규격": "4.5톤 덤프트럭(L=500m)", + "단위": "M3", + "갈래": "단가산출", + "코드": "D01519", + "무리": "사람", + "정답": [ + "GF000218" + ], + "허용": [], + "짐작": false, + "복합": null, + "줄수": 2 + }, + { + "id": "Y034", + "파일": "동부청영월", + "이름": "큰돌메쌓기", + "규격": "뒷길이 0.6~0.8m", + "단위": "M2", + "갈래": "단가산출", + "코드": "D00081", + "무리": "사람", + "정답": [ + "GF000226" + ], + "허용": [], + "짐작": false, + "복합": null, + "줄수": 2 + }, + { + "id": "Y035", + "파일": "동부청영월", + "이름": "큰돌채집운반(뒷길이 0.6~0.8m)", + "규격": "장비(80%) + 인력(20%)", + "단위": "M2", + "갈래": "단가산출", + "코드": "D01810", + "무리": "사람", + "정답": [ + "GF000213" + ], + "허용": [], + "짐작": false, + "복합": null, + "줄수": 2 + }, + { + "id": "Y115", + "파일": "동부청영월", + "이름": "파형강관부설", + "규격": "Φ800mm", + "단위": "M", + "갈래": "단가산출", + "코드": "D00071", + "무리": "자동", + "정답": [ + "GC000404" + ], + "허용": [], + "짐작": false, + "복합": null, + "줄수": 1 + }, + { + "id": "B001", + "파일": "봉화", + "이름": "레미콘타설(장비)", + "규격": "무근,소형", + "단위": "m3", + "갈래": "단가산출", + "코드": "D00096", + "무리": "자동+씨앗", + "정답": [ + "GC000198" + ], + "허용": [], + "짐작": false, + "복합": null, + "줄수": 11 + }, + { + "id": "B002", + "파일": "봉화", + "이름": "합판거푸집(간단)", + "규격": "간단", + "단위": "m2", + "갈래": "단가산출", + "코드": "D00102", + "무리": "씨앗", + "정답": [ + "GC000211" + ], + "허용": [], + "짐작": false, + "복합": null, + "줄수": 2 + }, + { + "id": "B004", + "파일": "봉화", + "이름": "구조물 터파기(토사)", + "규격": "굴삭기 0.2m3", + "단위": "m3", + "갈래": "단가산출", + "코드": "D00002", + "무리": "씨앗", + "정답": [ + "GC000088" + ], + "허용": [], + "짐작": false, + "복합": null, + "줄수": 4 + }, + { + "id": "B005", + "파일": "봉화", + "이름": "구조물 되메우기", + "규격": "굴삭기 0.2m3", + "단위": "m3", + "갈래": "단가산출", + "코드": "D00007", + "무리": "씨앗", + "정답": [ + "GF000113" + ], + "허용": [], + "짐작": false, + "복합": null, + "줄수": 4 + }, + { + "id": "B010", + "파일": "봉화", + "이름": "기초다짐 및 뒤채움", + "규격": "소형 장비사용 시공", + "단위": "M3", + "갈래": "단가산출", + "코드": "D00008", + "무리": "씨앗", + "정답": [ + "GF000218" + ], + "허용": [], + "짐작": false, + "복합": null, + "줄수": 6 + }, + { + "id": "B011", + "파일": "봉화", + "이름": "유로폼 설치 및 해체", + "규격": "간단", + "단위": "m2", + "갈래": "단가산출", + "코드": "D00093", + "무리": "자동+씨앗", + "정답": [ + "GC000214" + ], + "허용": [], + "짐작": false, + "복합": null, + "줄수": 8 + }, + { + "id": "B012", + "파일": "봉화", + "이름": "레미콘타설(장비)", + "규격": "무근", + "단위": "m3", + "갈래": "단가산출", + "코드": "D00095", + "무리": "자동+씨앗", + "정답": [ + "GC000198" + ], + "허용": [], + "짐작": false, + "복합": null, + "줄수": 6 + }, + { + "id": "B013", + "파일": "봉화", + "이름": "콘크리트믹서사용", + "규격": "0.45 m3", + "단위": "m3", + "갈래": "단가산출", + "코드": "D00089", + "무리": "씨앗", + "정답": [ + "GF000156" + ], + "허용": [], + "짐작": false, + "복합": null, + "줄수": 4 + }, + { + "id": "B014", + "파일": "봉화", + "이름": "유로폼 설치 및 해체", + "규격": "보통", + "단위": "m2", + "갈래": "단가산출", + "코드": "D00094", + "무리": "자동+씨앗", + "정답": [ + "GC000214" + ], + "허용": [], + "짐작": false, + "복합": null, + "줄수": 9 + }, + { + "id": "B015", + "파일": "봉화", + "이름": "철근가공 및 조립", + "규격": "Type-Ⅰ-1,현장가공조립", + "단위": "ton", + "갈래": "단가산출", + "코드": "D00098", + "무리": "씨앗", + "정답": [ + "GC000206", + "GC000207" + ], + "허용": [], + "짐작": false, + "복합": [ + "GC000206", + "GC000207" + ], + "줄수": 9 + }, + { + "id": "B017", + "파일": "봉화", + "이름": "콘크리트(기계비빔)", + "규격": "무근구조물", + "단위": "M3", + "갈래": "단가산출", + "코드": "D00012", + "무리": "씨앗", + "정답": [ + "GF000156" + ], + "허용": [], + "짐작": false, + "복합": null, + "줄수": 1 + }, + { + "id": "B018", + "파일": "봉화", + "이름": "구조물터파기(육상토사0~1m)", + "규격": "기계90%+인력10%(굴삭기 0.7M3)", + "단위": "M3", + "갈래": "단가산출", + "코드": "D00023", + "무리": "씨앗", + "정답": [ + "GF000101" + ], + "허용": [], + "짐작": false, + "복합": null, + "줄수": 1 + }, + { + "id": "B023", + "파일": "봉화", + "이름": "철근가공및조립(현장)", + "규격": "일반(Type-Ⅰ-1)", + "단위": "Ton", + "갈래": "단가산출", + "코드": "D00081", + "무리": "씨앗", + "정답": [ + "GC000206", + "GC000207" + ], + "허용": [], + "짐작": false, + "복합": [ + "GC000206", + "GC000207" + ], + "줄수": 1 + }, + { + "id": "B024", + "파일": "봉화", + "이름": "합판거푸집", + "규격": "3회(복잡)", + "단위": "M2", + "갈래": "단가산출", + "코드": "D00077", + "무리": "자동+씨앗", + "정답": [ + "GC000211" + ], + "허용": [], + "짐작": false, + "복합": null, + "줄수": 1 + }, + { + "id": "B026", + "파일": "봉화", + "이름": "평떼붙임", + "규격": "", + "단위": "m2", + "갈래": "단가산출", + "코드": "D00090", + "무리": "씨앗", + "정답": [ + "GC000143" + ], + "허용": [], + "짐작": false, + "복합": null, + "줄수": 1 + }, + { + "id": "B028", + "파일": "봉화", + "이름": "레미콘타설(장비)", + "규격": "철근", + "단위": "m3", + "갈래": "단가산출", + "코드": "D00097", + "무리": "자동+씨앗", + "정답": [ + "GC000198" + ], + "허용": [], + "짐작": false, + "복합": null, + "줄수": 1 + }, + { + "id": "B029", + "파일": "봉화", + "이름": "레미콘타설(장비)", + "규격": "무근,버림", + "단위": "m3", + "갈래": "단가산출", + "코드": "D00103", + "무리": "자동", + "정답": [ + "GC000198" + ], + "허용": [], + "짐작": false, + "복합": null, + "줄수": 1 + }, + { + "id": "B030", + "파일": "봉화", + "이름": "철근가공 및 조립", + "규격": "Type-Ⅱ-1,현장가공조립", + "단위": "ton", + "갈래": "단가산출", + "코드": "D00099", + "무리": "씨앗", + "정답": [ + "GC000206", + "GC000207" + ], + "허용": [], + "짐작": false, + "복합": [ + "GC000206", + "GC000207" + ], + "줄수": 1 + }, + { + "id": "B031", + "파일": "봉화", + "이름": "강관동바리", + "규격": "암거용-3개월사용", + "단위": "공/㎥", + "갈래": "단가산출", + "코드": "D00113", + "무리": "씨앗", + "정답": [ + "GC000015" + ], + "허용": [], + "짐작": false, + "복합": null, + "줄수": 1 + }, + { + "id": "B032", + "파일": "봉화", + "이름": "강관비계", + "규격": "3개월사용", + "단위": "M2", + "갈래": "단가산출", + "코드": "D00112", + "무리": "씨앗", + "정답": [ + "GC000022" + ], + "허용": [], + "짐작": false, + "복합": null, + "줄수": 1 + }, + { + "id": "U002", + "파일": "울진대흥", + "이름": "콘크리트비빔", + "규격": "채움재", + "단위": "m3", + "갈래": "단가산출", + "코드": "D02200", + "무리": "씨앗", + "정답": [ + "GF000156" + ], + "허용": [], + "짐작": false, + "복합": null, + "줄수": 9 + }, + { + "id": "U003", + "파일": "울진대흥", + "이름": "유로폼 설치 및 해체", + "규격": "간단", + "단위": "m2", + "갈래": "단가산출", + "코드": "D02195", + "무리": "자동+씨앗", + "정답": [ + "GC000214" + ], + "허용": [], + "짐작": false, + "복합": null, + "줄수": 5 + }, + { + "id": "U005", + "파일": "울진대흥", + "이름": "구조물터파기(토사)", + "규격": "굴삭기0.7m3", + "단위": "m3", + "갈래": "단가산출", + "코드": "D01343", + "무리": "씨앗", + "정답": [ + "GF000101" + ], + "허용": [], + "짐작": false, + "복합": null, + "줄수": 1 + }, + { + "id": "U006", + "파일": "울진대흥", + "이름": "다지기", + "규격": "굴삭기0.7m3", + "단위": "m3", + "갈래": "단가산출", + "코드": "D02139", + "무리": "씨앗", + "정답": [ + "GC000105" + ], + "허용": [], + "짐작": false, + "복합": null, + "줄수": 1 + }, + { + "id": "U007", + "파일": "울진대흥", + "이름": "야면석채집", + "규격": "L3=45cm내외", + "단위": "m2", + "갈래": "단가산출", + "코드": "D01129", + "무리": "씨앗", + "정답": [ + "GF000214" + ], + "허용": [], + "짐작": false, + "복합": null, + "줄수": 6 + }, + { + "id": "U012", + "파일": "울진대흥", + "이름": "철근가공 및 조립", + "규격": "Type-Ⅱ-2,현장가공조립", + "단위": "ton", + "갈래": "단가산출", + "코드": "D02251", + "무리": "씨앗", + "정답": [ + "GC000206", + "GC000207" + ], + "허용": [], + "짐작": false, + "복합": [ + "GC000206", + "GC000207" + ], + "줄수": 1 + }, + { + "id": "U013", + "파일": "울진대흥", + "이름": "유로폼 설치 및 해체", + "규격": "보통", + "단위": "m2", + "갈래": "단가산출", + "코드": "D02246", + "무리": "자동+씨앗", + "정답": [ + "GC000214" + ], + "허용": [], + "짐작": false, + "복합": null, + "줄수": 1 + }, + { + "id": "U014", + "파일": "울진대흥", + "이름": "콘크리트믹서사용", + "규격": "0.45 m3", + "단위": "m3", + "갈래": "단가산출", + "코드": "D00075", + "무리": "씨앗", + "정답": [ + "GF000156" + ], + "허용": [], + "짐작": false, + "복합": null, + "줄수": 1 + }, + { + "id": "U015", + "파일": "울진대흥", + "이름": "콘크리트포장(B-type)", + "규격": "t=20cm", + "단위": "m2", + "갈래": "단가산출", + "코드": "D02248", + "무리": "씨앗", + "정답": [ + "GF000162" + ], + "허용": [], + "짐작": false, + "복합": null, + "줄수": 1 + }, + { + "id": "U016", + "파일": "울진대흥", + "이름": "레미콘타설", + "규격": "무근(장비사용타설)", + "단위": "m3", + "갈래": "단가산출", + "코드": "D02262", + "무리": "자동", + "정답": [ + "GC000198" + ], + "허용": [], + "짐작": false, + "복합": null, + "줄수": 3 + }, + { + "id": "K004", + "파일": "영덕계류", + "이름": "뒤채움", + "규격": "소형장비", + "단위": "m3", + "갈래": "단가산출", + "코드": "D01426", + "무리": "자동", + "정답": [ + "GC000106" + ], + "허용": [], + "짐작": false, + "복합": null, + "줄수": 1 + }, + { + "id": "G000", + "파일": "울진소광", + "이름": "유로폼 설치 및 해체", + "규격": "보통", + "단위": "m2", + "갈래": "단가산출", + "코드": "D01352", + "무리": "자동+씨앗", + "정답": [ + "GC000214" + ], + "허용": [], + "짐작": false, + "복합": null, + "줄수": 1 + }, + { + "id": "G003", + "파일": "울진소광", + "이름": "유로폼 설치 및 해체", + "규격": "간단", + "단위": "m2", + "갈래": "단가산출", + "코드": "D01351", + "무리": "자동+씨앗", + "정답": [ + "GC000214" + ], + "허용": [], + "짐작": false, + "복합": null, + "줄수": 1 + }, + { + "id": "G005", + "파일": "울진소광", + "이름": "구조물터파기(토사)", + "규격": "굴착기0.6m3", + "단위": "m3", + "갈래": "단가산출", + "코드": "D01456", + "무리": "씨앗", + "정답": [ + "GC000088" + ], + "허용": [], + "짐작": false, + "복합": null, + "줄수": 8 + }, + { + "id": "G006", + "파일": "울진소광", + "이름": "구조물되메우기", + "규격": "굴착기0.6m3", + "단위": "m3", + "갈래": "단가산출", + "코드": "D01474", + "무리": "씨앗", + "정답": [ + "GF000113" + ], + "허용": [], + "짐작": false, + "복합": null, + "줄수": 6 + }, + { + "id": "G011", + "파일": "울진소광", + "이름": "막자갈채우기", + "규격": "150mm내외", + "단위": "m3", + "갈래": "단가산출", + "코드": "D00867", + "무리": "자동", + "정답": [ + "GC000107" + ], + "허용": [], + "짐작": false, + "복합": null, + "줄수": 3 + }, + { + "id": "G012", + "파일": "울진소광", + "이름": "레미콘타설", + "규격": "무근(장비)", + "단위": "m3", + "갈래": "단가산출", + "코드": "D01336", + "무리": "자동+씨앗", + "정답": [ + "GC000198" + ], + "허용": [], + "짐작": false, + "복합": null, + "줄수": 3 + }, + { + "id": "G016", + "파일": "울진소광", + "이름": "콘크리포장(T=20cm)포설", + "규격": "인력포설", + "단위": "m3", + "갈래": "단가산출", + "코드": "D01354", + "무리": "자동", + "정답": [ + "GC000297" + ], + "허용": [], + "짐작": false, + "복합": null, + "줄수": 1 + }, + { + "id": "G018", + "파일": "울진소광", + "이름": "평떼붙이기", + "규격": "", + "단위": "m2", + "갈래": "단가산출", + "코드": "D01329", + "무리": "자동", + "정답": [ + "GC000143" + ], + "허용": [], + "짐작": false, + "복합": null, + "줄수": 1 + }, + { + "id": "G020", + "파일": "울진소광", + "이름": "콘크리트믹서사용", + "규격": "0.45 m3", + "단위": "m3", + "갈래": "단가산출", + "코드": "D00083", + "무리": "씨앗", + "정답": [ + "GF000156" + ], + "허용": [], + "짐작": false, + "복합": null, + "줄수": 1 + }, + { + "id": "G033", + "파일": "울진소광", + "이름": "수축줄눈(컷팅)", + "규격": "콘크리트포장", + "단위": "m", + "갈래": "단가산출", + "코드": "D01488", + "무리": "자동", + "정답": [ + "GC000301" + ], + "허용": [], + "짐작": false, + "복합": null, + "줄수": 1 + }, + { + "id": "C004", + "파일": "거창", + "이름": "유로폼", + "규격": "보통,0~7m", + "단위": "㎡", + "갈래": "단가산출", + "단산": 30, + "무리": "자동+씨앗", + "정답": [ + "GC000214" + ], + "허용": [], + "짐작": false, + "복합": null, + "줄수": 5 + }, + { + "id": "C005", + "파일": "거창", + "이름": "철근가공및조립(현장)", + "규격": "Type-Ⅱ-1", + "단위": "Ton", + "갈래": "단가산출", + "단산": 38, + "무리": "씨앗", + "정답": [ + "GC000206", + "GC000207" + ], + "허용": [], + "짐작": false, + "복합": [ + "GC000206", + "GC000207" + ], + "줄수": 4 + }, + { + "id": "C013", + "파일": "거창", + "이름": "레미콘타설(장비사용타설)", + "규격": "철근구조물", + "단위": "㎥", + "갈래": "단가산출", + "단산": 40, + "무리": "씨앗", + "정답": [ + "GC000198" + ], + "허용": [], + "짐작": false, + "복합": null, + "줄수": 1 + }, + { + "id": "C014", + "파일": "거창", + "이름": "합판거푸집", + "규격": "4회", + "단위": "㎡", + "갈래": "단가산출", + "단산": 41, + "무리": "씨앗", + "정답": [ + "GC000211" + ], + "허용": [], + "짐작": false, + "복합": null, + "줄수": 1 + }, + { + "id": "C016", + "파일": "거창", + "이름": "강관동바리", + "규격": "2.5m이하(1개월)", + "단위": "공㎥", + "갈래": "단가산출", + "단산": 42, + "무리": "씨앗", + "정답": [ + "GC000015" + ], + "허용": [], + "짐작": false, + "복합": null, + "줄수": 1 + }, + { + "id": "C017", + "파일": "거창", + "이름": "잡철물제작및설치(현장)", + "규격": "일반철재", + "단위": "Ton", + "갈래": "단가산출", + "단산": 43, + "무리": "씨앗", + "정답": [ + "GC000821" + ], + "허용": [], + "짐작": false, + "복합": null, + "줄수": 1 + }, + { + "id": "C018", + "파일": "거창", + "이름": "안내표지판 설치", + "규격": "단주식", + "단위": "개소", + "갈래": "단가산출", + "단산": 44, + "무리": "씨앗", + "정답": [ + "GC000308" + ], + "허용": [], + "짐작": false, + "복합": null, + "줄수": 1 + }, + { + "id": "C019", + "파일": "거창", + "이름": "레미콘타설(인력운반타설)", + "규격": "소형구조물", + "단위": "㎥", + "갈래": "단가산출", + "단산": 37, + "무리": "씨앗", + "정답": [ + "GF000155" + ], + "허용": [], + "짐작": false, + "복합": null, + "줄수": 1 + }, + { + "id": "C020", + "파일": "거창", + "이름": "합판거푸집", + "규격": "6회, 간단", + "단위": "㎡", + "갈래": "단가산출", + "단산": 45, + "무리": "씨앗", + "정답": [ + "GC000211" + ], + "허용": [], + "짐작": false, + "복합": null, + "줄수": 1 + } +] diff --git a/resources/tester/helper_m02_hopyo_golden.py b/resources/tester/helper_m02_hopyo_golden.py new file mode 100644 index 000000000..7ced12420 --- /dev/null +++ b/resources/tester/helper_m02_hopyo_golden.py @@ -0,0 +1,254 @@ +"""338 골든 도우미 — 실물 엑셀(`resources/knowledge/original/실무문서`)을 지금 읽기로 읽어 정답 표 +(`fixtures/m02_hopyo_golden.json`)와 맞대고 지표(설계 7-3)를 셈. 읽기만. + +블록 글 = HP 블록에 `글` 이 있으면 그것(나 창 2a 뒤) · 없으면 단가산출 시트 산출근거 열 글(엑셀꼴) · +거창 = 줄 칸을 이어 붙임. STmate 파일은 읽기(`M02_Hopyo_Excel.read_book`)가 되면 들어감 · 아니면 뺌. +python resources/tester/helper_m02_hopyo_golden.py → 지표 찍음 · tmp/m02_338/golden.json +""" + +from __future__ import annotations + +import json +import sys +import time +from functools import lru_cache +from pathlib import Path +from typing import Any + +ROOT = Path(__file__).resolve().parents[2] +if str(ROOT) not in sys.path: + sys.path.insert(0, str(ROOT)) + +from M02_MasterTemplete import M02_Hopyo_Basis as hb # noqa: E402 +from M02_MasterTemplete import M02_Hopyo_Excel as hx # noqa: E402 +from M02_MasterTemplete import M02_Hopyo_Match as hm # noqa: E402 +from M02_MasterTemplete import M02_Hopyo_Verdict as verdict # noqa: E402 + +DOCS = ROOT / "resources/knowledge/original/실무문서" +FIXTURE = Path(__file__).resolve().parent / "fixtures/m02_hopyo_golden.json" +FILES = { + "북부청임도": DOCS + / "2026_실무일위대가 모음/2026년_북부지방산림청_임도사업_설계내역서(공통단가).xls", + "북부청사방": DOCS + / "2026_실무일위대가 모음/2026년_북부지방산림청_사방사업(계류+댐)_설계내역서.xls", + "거창": DOCS + / "01.산불거창(거창북상병곡)/01.내역서/00.25년_산불_거창(거창,북상,병곡,산8외)내역+수량.xlsx", + "봉화": DOCS + / "04.산불 봉화/01. 내역서/2024년 산불진화임도 실시설계(기번41_현동)_ L=3.46km.xlsx", + "동부청영월": DOCS / "05. 동부청 영월/내역서/2024년 임도시설사업2차(산불진화_기번6)-영월.xlsx", + "울진대흥": DOCS + / "2024년 간선임도(기번3-울진.대흥)(공통)" + / "2024년 간선임도 신설사업(기번3 울진.울진.대흥.산65외2).xlsx", + "영덕계류": DOCS + / "20250527 2025년 계류보전사업(기번1-영덕.병곡.영리.산214-1)(변경)/01.내역자료" + / "2025년 계류보전사업(기번1 영덕.병곡.영.산214)(변경).xlsx", + "울진소광": DOCS + / "25년 산불진화임도(기번8)(울진소광)" + / "2025년 산불진화임도 신설사업(기번8 울진.금강송.소광.산29외).xlsx", +} +LABELED = ("북부청임도", "북부청사방", "동부청영월") + + +def labels() -> list[dict[str, Any]]: + return json.loads(FIXTURE.read_text(encoding="utf-8")) + + +def _row_text(row: list, start: int) -> str: + cells = [] + for v in row[start:140]: + if v is None or v == "": + continue + cells.append(str(int(v) if isinstance(v, float) and v == int(v) else v).strip()) + return " ".join(cells) + + +@lru_cache(maxsize=8) +def read(tag: str) -> dict[str, Any] | None: + """파일 → HP 꼴(블록 `글` 붙임) · 못 읽으면 None(STmate 읽기 전).""" + path = FILES[tag] + data = path.read_bytes() + try: + book = hx.read_book(path.name, data) + except hx.ExcelError: + return None + if any(b.get("글") or b.get("자원") for b in book.get("단산") or []): + return book + calc = hx._sheets(path.name, data).get(hb.SHEET) or [] + _, col, _ = hb._columns(calc) + join = tag == "거창" + for b in book.get("단산") or []: + a, z = b["행"] + rows = calc[a:z] + text = [_row_text(r, col) if join else (r[col] if col < len(r) else "") for r in rows] + b["글"] = [str(t).rstrip() for t in text if isinstance(t, str) and t.strip()] + return book + + +def _key(line: dict[str, Any]) -> tuple: + ref = line.get("참조") or {} + no = ref.get("번") if ref.get("종류") == "단산" else None + code = str(ref.get("코드") or line.get("코드") or "") + return line.get("이름"), line.get("규격") or "", line.get("단위") or "", no, code[:6] + + +def _lab_key(lab: dict[str, Any]) -> tuple: + return lab["이름"], lab["규격"], lab["단위"], lab.get("단산"), str(lab.get("코드") or "")[:6] + + +def line_of(lid: str) -> tuple[dict | None, dict[str, Any]]: + """정답 표 id → (블록, 줄) — 시험이 실물 줄 하나를 꺼냄.""" + lab = next(x for x in labels() if x["id"] == lid) + book = read(lab["파일"]) + blocks = {int(b["번"]): b for b in book.get("단산") or []} + for h in book["호표"]: + for line in h["줄"]: + if _key(line) == _lab_key(lab) or ( + not line.get("참조") and _key(line)[:3] == _lab_key(lab)[:3] + ): + ref = line.get("참조") or {} + return (blocks.get(int(ref["번"])) if ref.get("종류") == "단산" else None), line + raise KeyError(lid) + + +def evaluate(tags: tuple[str, ...] | None = None) -> dict[str, Any]: + """{줄: {id: 결과}, 호표: {(파일, 번): (ids, refs)}, 읽음: [파일], 시간}.""" + labs = labels() + tags = tags or tuple(dict.fromkeys(lab["파일"] for lab in labs)) + got = hm.targets() + out: dict[str, Any] = {"줄": {}, "호표": {}, "읽음": [], "못읽음": [], "시간": {}} + for tag in tags: + book = read(tag) + if book is None: + out["못읽음"].append(tag) + continue + out["읽음"].append(tag) + want = {_lab_key(lab): lab for lab in labs if lab["파일"] == tag} + loose = {k[:3]: lab for k, lab in want.items()} + blocks = {int(b["번"]): b for b in book.get("단산") or []} + t0 = time.time() + for h in book["호표"]: + ids, refs = [], [] + for line in h["줄"]: + ref = line.get("참조") or {} + if ref.get("종류") == "일위" and ref.get("번"): + refs.append(int(ref["번"])) + lab = want.get(_key(line)) or (loose.get(_key(line)[:3]) if not ref else None) + if lab is None: + continue + ids.append(lab["id"]) + if lab["id"] in out["줄"]: + continue + block = blocks.get(int(ref["번"])) if ref.get("종류") == "단산" else None + out["줄"][lab["id"]] = {"lab": lab, "셈": verdict.block(block, line, got)} + out["호표"][f"{tag}|{h['번']}"] = (list(dict.fromkeys(ids)), refs) + out["시간"][tag] = round(time.time() - t0, 1) + return out + + +def chosen_of(entry: dict[str, Any]) -> set[str]: + """줄이 고른 로직(T1) — 조합이면 체크한 것 · 아니면 1위.""" + combo = entry.get("조합") or [] + top = (entry.get("차례") or [None])[0] + codes = {c["로직"] for c in combo if c["로직"] and c.get("체크")} + return codes or ({top} if top else set()) + + +def _gather(res: dict, key: str, seen: tuple = ()) -> list[str]: + tag = key.split("|")[0] + if key not in res["호표"] or key in seen: + return [] + ids, refs = res["호표"][key] + out = list(ids) + for no in refs: + out += _gather(res, f"{tag}|{no}", (*seen, key)) + return list(dict.fromkeys(out)) + + +def metrics(res: dict[str, Any], group: set[str] | None = None) -> dict[str, Any]: + """설계 7-3 지표 — 사람 정답 줄 · 새 파일 · 조합 맞음(T1 · 덮을 수 있는 호표).""" + rows = [r for r in res["줄"].values() if group is None or r["lab"]["무리"] in group] + have = [r for r in rows if r["lab"]["정답"]] + ok1 = lambda r: (r["셈"]["차례"] or [None])[0] in set(r["lab"]["정답"]) # noqa: E731 + state = lambda r: r["셈"]["판정"]["상태"] # noqa: E731 + opt_ok = 0 + for r in have: + top = (r["셈"]["차례"] or [None])[0] + found = (r["셈"]["옵션"] or {}).get(top) or {} + if ok1(r) and found.get("수준", 9) <= 1 and state(r) == "맞음": + opt_ok += 1 + out = { + "줄": len(have), + "1순위": sum(ok1(r) for r in have), + "다섯 안": sum(bool(set(r["셈"]["차례"][:5]) & set(r["lab"]["정답"])) for r in have), + "맞음": sum(state(r) == "맞음" for r in rows), + "맞음 틀림": sum(state(r) == "맞음" and not ok1(r) for r in rows), + "다른 점": sum(state(r) == "다른 점" for r in rows), + "다른 점 틀림": sum(state(r) == "다른 점" and not ok1(r) for r in rows), + "옵션까지": opt_ok, + } + keys = [k for k in res["호표"] if k.split("|")[0] in LABELED] + whole = cover = 0 + by = res["줄"] + for key in keys: + ids = [ + i + for i in _gather(res, key) + if i in by and (group is None or by[i]["lab"]["무리"] in group) + ] + if not ids: + continue + slots, chosen = [], set() + for i in ids: + lab = by[i]["lab"] + if not lab["정답"]: + slots.append(set()) + elif lab.get("복합") and lab["id"] != "S062": + slots += [{x} for x in lab["복합"]] + else: + slots.append(set(lab["정답"])) + chosen |= chosen_of(by[i]["셈"]) + if any(not s for s in slots): + continue + cover += 1 + alts = set().union(*slots) + whole += all(chosen & s for s in slots) and not (chosen - alts) + out["조합 호표"], out["조합 맞음"] = cover, whole + return out + + +def show(res: dict[str, Any]) -> dict[str, Any]: + table = { + "사람": metrics(res, {"사람"}), + "새 파일": metrics(res, {"자동", "씨앗", "자동+씨앗"}), + **{ + tag: metrics( + {**res, "줄": {i: r for i, r in res["줄"].items() if r["lab"]["파일"] == tag}}, + {"사람"}, + ) + for tag in LABELED + if tag in res["읽음"] + }, + } + return {"읽음": res["읽음"], "못읽음": res["못읽음"], "시간": res["시간"], "지표": table} + + +if __name__ == "__main__": + t = time.time() + res = evaluate(tuple(sys.argv[1:]) or None) + summary = show(res) + print(json.dumps(summary, ensure_ascii=False, indent=1)) + print("전체", round(time.time() - t, 1), "s") + dump = { + i: { + "lab": r["lab"], + "차례": r["셈"]["차례"], + "판정": r["셈"]["판정"], + "조합": r["셈"]["조합"], + "B": r["셈"]["B"], + } + for i, r in res["줄"].items() + } + (ROOT / "tmp/m02_338").mkdir(parents=True, exist_ok=True) + (ROOT / "tmp/m02_338/golden.json").write_text( + json.dumps(dump, ensure_ascii=False, indent=1, default=str), "utf-8" + ) diff --git a/resources/tester/test_m02_hopyo_finger.py b/resources/tester/test_m02_hopyo_finger.py new file mode 100644 index 000000000..b0e48acce --- /dev/null +++ b/resources/tester/test_m02_hopyo_finger.py @@ -0,0 +1,147 @@ +"""338-1 엑셀 지문(`M02_Hopyo_Finger` · `_Words` · `_Shares`) — 식 풀기 일곱 꼴 · 합판 지문 · 고임돌 +섞음 · +몫 · STmate 자원 · 기계 줄 · 직접 줄 호표. 실물 = 북부청 임도(읽기만).""" + +import pytest + +from M02_MasterTemplete import M02_Hopyo_Finger as finger +from M02_MasterTemplete import M02_Hopyo_Shares as shares +from M02_MasterTemplete.M02_Hopyo_Words import conds, mtypes, parse_rhs +from resources.tester.helper_m02_hopyo_golden import line_of + +approx = pytest.approx + + +# ── 식 풀기 ──────────────────────────────────────────────────────────── +def test_식_단가_떼고_품(): + got = parse_rhs("273,540 × 3인 ÷ 55㎥ × 2 = 29,840.7") + assert got["단가"] == 273540 and got["품"] == approx(0.10909, abs=1e-5) and got["율"] == [] + + +def test_식_퍼센트_율(): + got = parse_rhs("59,020 ÷ 23.1 × 90% = 2,299.5") + assert got["품"] == approx(0.03896, abs=1e-5) + assert got["밑품"] == approx(1 / 23.1) and got["율"] == [[0.9, "%"]] + + +def test_식_인당_단위_섞음(): + got = parse_rhs("172,068 × 0.3인/㎥ × 10% = 5,162") + assert got["품"] == approx(0.03) and got["밑품"] == approx(0.3) + + +def test_식_바꿔_쓴_식은_앞_율을_이어받음(): + got = parse_rhs("215,907 × 4인 ÷ P × 1.2(할증) = 215,907 × 0.104 × 1.2 = 26,945") + assert got["품"] == approx(0.1248) and got["밑품"] == approx(0.104) + assert got["율"] == [[1.2, "할증"]] + + +def test_식_횟수_품할증(): + got = parse_rhs("172,068 × 0.05인 × 2회 = 17,206") + assert got["품"] == approx(0.1) and got["밑품"] == approx(0.05) and got["율"] == [[2.0, "회"]] + got = parse_rhs("273,540 × 0.1 × 1.2(품할증) = 32,824") + assert got["밑품"] == approx(0.1) and got["율"] == [[1.2, "품할증"]] + + +def test_식_못_풀면_역산(): + assert parse_rhs("273,540 × P = 30,000") is None + block = {"번": 1, "이름": "시험", "단위": "㎥", "글": [" 특별인부 : 273,540 × P = 27,354"]} + fp = finger.excel(block, {"이름": "시험", "규격": "", "단위": "㎥"}) + assert fp["인력"]["특별인부"]["품"] == approx(0.1) and fp["인력"]["특별인부"]["어떻게"] == [ + "역산" + ] + + +def test_장비_갈래_조건_낱말(): + assert mtypes("유압식 백호우 0.7㎥") == {"굴착기"} and mtypes("펌프카 41m") == {"콘크리트펌프"} + assert conds("합판거푸집 4회 보통")["횟수"] == {"4회"} + assert conds("구조물터파기(암)")["토질"] == {"암"} + + +# ── 실물 지문 ────────────────────────────────────────────────────────── +def test_합판_4회_지문(): + block, line = line_of("N001") + fp = finger.excel(block, line) + assert fp["인력"]["형틀목공"]["품"] == approx(0.1111, abs=1e-4) + assert fp["인력"]["보통인부"]["품"] == approx(0.0444, abs=1e-4) + assert "4회" in fp["조건"]["횟수"] and fp["출처"] == [["건설품셈", "6-3-1"]] + assert {m["이름"][:4] for m in fp["자재"] if m["품"]} >= {"내수합판"} + + +def test_레미콘_소량_지문_장비_시간(): + block, line = line_of("N000") + fp = finger.excel(block, line) + assert fp["인력"]["콘크리트공"]["품"] == approx(0.10909, abs=1e-5) + assert [(m["갈래"], round(m["시간"], 2)) for m in fp["장비"]] == [(["굴착기"], 0.29)] + + +def test_고임돌_섞음_율과_몫(): + block, line = line_of("N005") + fp = finger.excel(block, line) + labor = fp["인력"]["보통인부"] + assert labor["밑품"] == approx(0.3, abs=1e-3) and labor["품"] == approx(0.03, abs=1e-3) + assert any(note in ("%", "섞음") and v == approx(0.1) for v, note in labor["율"]) + parts = fp["몫"] + assert any(p["노임"] and not p["장비"] for p in parts) # 인력 몫 + assert any(p["장비"] and not p["노임"] for p in parts) # 장비 몫 + + +def test_몫_절마다_노임_장비(): + texts = [ + " 1. 굴착기 0.6㎥ 채집(90%)", + " 노무비 : 59,020 ÷ 23.1 × 90% = 2,299", + " 2. 인력채집(10%)", + " 보통인부 : 172,068 × 0.3인/㎥ × 10% = 5,162", + " 3. 합 계", + " 보통인부 : 172,068 × 1 = 172,068", + ] + parts = shares.sections(texts) + assert [p["제목"] for p in parts] == ["굴착기 0.6㎥ 채집(90%)", "인력채집(10%)"] + assert parts[0]["장비"] == {"굴착기|0.6": approx(0.9 / 23.1)} + assert parts[1]["노임"] == {"보통인부": approx(0.03)} + + +def test_STmate_자원_지문(): + block = { + "글": ["잔토처리 (기계100%) 건설표준품셈9-3", "q = 0.7 , K = 0.9"], + "자원": [ + {"갈래": "L", "코드": "L00002", "이름": "보통인부", "규격": "", "품": 0.2}, + *[ + { + "갈래": "X", + "코드": "X00205", + "이름": "굴삭기(무한궤도)", + "규격": "0.7㎥", + "품": 0.0229, + } + ] + * 3, + {"갈래": "M", "코드": "M00001", "이름": "모래", "규격": "", "품": 1.2}, + ], + } + fp = finger.excel(block, {"이름": "잔토처리", "규격": "토사", "단위": "M3"}) + assert fp["인력"] == {"보통인부": {"품": 0.2, "밑품": 0.2, "율": [], "어떻게": ["QTY"]}} + assert [(m["갈래"], m["규격"], m["시간"]) for m in fp["장비"]] == [(["굴착기"], [0.7], 0.0229)] + assert fp["자재"] == [{"이름": "모래", "품": 1.2, "밑품": 1.2}] and fp["계수"]["K"] == 0.9 + + +def test_기계_줄_블록_없음(): + fp = finger.excel(None, {"이름": "콘크리트 펌프차", "규격": "41m,80~95㎥/hr", "단위": "hr"}) + assert [(m["갈래"], m["시간"]) for m in fp["장비"]] == [(["콘크리트펌프"], 1.0)] + assert 41.0 in fp["장비"][0]["규격"] and fp["인력"] == {} + + +def test_직접_줄_호표(): + hopyo = { + "이름": "돌붙임", + "규격": "", + "단위": "㎡", + "줄": [ + {"이름": "석 공", "규격": "", "수량": 0.1, "단위": "인", "갈래": "기타"}, + {"이름": "보통인부", "규격": "", "수량": 0.05, "단위": "인", "갈래": "기타"}, + {"이름": "굴착기", "규격": "0.6㎥", "수량": 0.2, "단위": "hr", "갈래": "기계"}, + ], + } + fp = finger.direct(hopyo) + assert fp["인력"]["석공"]["품"] == 0.1 and fp["인력"]["보통인부"]["품"] == 0.05 + assert [(m["갈래"], m["시간"]) for m in fp["장비"]] == [(["굴착기"], 0.2)] + assert finger.direct({**hopyo, "줄": hopyo["줄"][2:]}) is None # 노임 없으면 호표 통째 아님 diff --git a/resources/tester/test_m02_hopyo_fit.py b/resources/tester/test_m02_hopyo_fit.py new file mode 100644 index 000000000..a3b18050b --- /dev/null +++ b/resources/tester/test_m02_hopyo_fit.py @@ -0,0 +1,122 @@ +"""338-1 순위 · 판정 · 조합 · 뒤 셈(`M02_Hopyo_Fit` · `_Verdict` · `_Combine`) — 같음 규칙 · 딱 +맞음이 +같음을 이김 · 맞음 · 다른 점 · 후보 · 철근 둘 · 고임돌 인력 10% · 맞춤셈 캐시 · 「셈 중」. +실물 = 북부청 임도 · 사방(읽기만) · 층 · 캐시 폴더는 임시.""" + +import shutil +import time + +import pytest + +from M02_MasterTemplete import M02_Hopyo_Fit as fit +from M02_MasterTemplete import M02_Hopyo_Match as hm +from M02_MasterTemplete import M02_Hopyo_Ours as ours +from M02_MasterTemplete import M02_Hopyo_Store as hopyo_store +from M02_MasterTemplete import M02_Hopyo_Verdict as verdict +from resources.tester.helper_m02_hopyo_golden import line_of, read +from resources.tester.test_m02_hopyo_excel import master_hash + + +@pytest.fixture(scope="module") +def got(): + if not ours.ready(): + ours.build() + return hm.targets() + + +def _block(lid: str, got: dict, **change) -> dict: + block, line = line_of(lid) + if change: + line = {**line, "근거": {}} + block = {**block, "출처": []} + return verdict.block(block, line, got) + + +def test_같음_규칙_이름_같음_1위_고정(got): + r = _block("N014", got) + assert r["B"] == "같음" and r["차례"][0] == "GC000214" and r["판정"]["상태"] == "맞음" + + +def test_딱_맞음이_같음을_이김(got): + r = _block("N001", got, 근거=None) # 인용 조항을 지우면 이름 같음 = 산림 GF000160 + assert r["B"] == "같음" and hm.b_order(line_of("N001")[1], got)["상태"] == "후보" + assert r["차례"][0] == "GC000211" and r["판정"]["상태"] == "맞음" + + +def test_판정_셋(got): + a = _block("N000", got) + assert a["판정"]["상태"] == "맞음" and "소량감률 50" in a["판정"]["글"] + assert a["후보"][0]["판정"] == "맞음" and a["후보"][0]["근거"]["까닭"] == "조항 같음" + b = _block("N008", got) + assert b["판정"]["상태"] == "다른 점" and b["차례"][0] == "GC000083" + assert "특별인부 엑셀 0.3542 · 우리 0.3704" in b["판정"]["꼬리표"] + c = _block("S084", got) + assert c["판정"]["상태"] == "후보" and len(c["후보"]) == 5 + + +def test_합판_맞음_못_가름_꼬리표(got): + r = _block("N001", got) + assert r["판정"]["상태"] == "맞음" + assert any( + t.endswith("못 가름") and {"측압", "폼타이규격"} <= set(t.split()) + for t in r["판정"]["꼬리표"] + ) + + +def test_조합_철근_가공_조립(got): + r = _block("N003", got) + assert {c["로직"] for c in r["조합"] if c["체크"]} == {"GC000206", "GC000207"} + + +def test_조합_고임돌_인력_몫_먼저(got): + r = _block("N005", got) + assert r["차례"][0] == "GF000211" + labor = next(c for c in r["조합"] if c["로직"] == "GF000211") + assert labor["몫"] == 0.1 and labor["체크"] + assert all(not c["체크"] for c in r["조합"] if c["로직"] in ("GC000267", "GC000268")) + + +def test_숫자_없음_옛_HP(got): + block, line = line_of("N000") + r = verdict.block({**block, "글": []}, line, got) + assert r["판정"]["상태"] == "후보" and r["판정"]["글"] == fit.NO_NUMBER + assert r["차례"][0] == "GC000198" # 이름 · 근거 차례 그대로 + + +@pytest.fixture +def layer(tmp_path, monkeypatch, got): + cache = tmp_path / "cache" + cache.mkdir() + shutil.copy(ours.path_of(), cache / ours.path_of().name) + monkeypatch.setenv("AISLO_HOPYO_CACHE", str(cache)) + book = read("북부청임도") + small = {**book, "호표": book["호표"][:3]} + brief = hopyo_store.save(tmp_path, "북부청.xls", small, {"id": "t"}) + return tmp_path, brief["키"], cache + + +def test_뒤_셈_캐시_셈_중(layer): + folder, key, cache = layer + before = master_hash() + assert fit.result(folder, key) is None + first = fit.ensure(folder, key) + assert first["끝"] is False and first["한"] == 0 + for _ in range(300): + if fit.status(folder, key)["끝"]: + break + time.sleep(0.2) + done = fit.status(folder, key) + assert done["끝"] and done["한"] == done["모두"] > 0 + got = fit.result(folder, key) + assert got["판"] == ours.version() and len(got["블록"]) == done["모두"] + assert fit.cache_path(folder, key).parent == cache + assert fit.ensure(folder, key)["끝"] # 두 번째는 캐시만 봄 + assert master_hash() == before + + +def test_셈_단위_열쇠(got): + book = read("북부청임도") + units = fit.units({**book, "호표": book["호표"][:3]}) + keys = [k for k, _, _ in units] + assert len(keys) == len(set(keys)) and keys[0].startswith("단산:") + assert all(k.startswith(("단산:", "줄:", "호표:")) for k in keys) diff --git a/resources/tester/test_m02_hopyo_option.py b/resources/tester/test_m02_hopyo_option.py new file mode 100644 index 000000000..1eb81da0c --- /dev/null +++ b/resources/tester/test_m02_hopyo_option.py @@ -0,0 +1,63 @@ +"""338-1 옵션 거꾸로 찾기(`M02_Hopyo_Option`) — 레미콘 소량 「하나만」 · 합판 갈리는 칸 · 무근 R +맞음 · +구조물터파기 없음(다른 점) · 펌프차 기계 칸 · 견줌 · 반올림 폭. 실물 = 북부청 임도 · +사방(읽기만).""" + +import pytest + +from M02_MasterTemplete import M02_Hopyo_Finger as finger +from M02_MasterTemplete import M02_Hopyo_Option as option +from M02_MasterTemplete import M02_Hopyo_Ours as ours +from resources.tester.helper_m02_hopyo_golden import line_of + + +def _find(lid: str, code: str) -> dict: + block, line = line_of(lid) + return option.find(code, finger.excel(block, line), (block or {}).get("글")) + + +def test_레미콘_소량_하나만(): + got = _find("N000", "GC000198") + assert got["수준"] == 0 and got["판정"] == "하나만" + assert got["입력"] == {"타설방법": "장비사용타설", "구조물": "철근구조물", "소량감률": 50} + assert got["우리품"]["인력"] == {"콘크리트공": 0.109091, "보통인부": 0.036364} + + +def test_합판_보통_갈리는_칸(): + got = _find("N001", "GC000211") + assert got["수준"] == 0 and got["판정"] == "여럿(같은 품)" + assert got["입력"]["유형"] == "보통" and got["입력"]["사용횟수"] == "4회" + assert set(got["갈리는칸"]) == {"측압", "폼타이규격"} + + +def test_무근_반올림_맞음(): + got = _find("N013", "GC000198") + assert got["수준"] <= 1 and got["입력"]["구조물"] == "무근구조물" + + +def test_구조물터파기_없음_가까운_옵션(): + got = _find("N008", "GC000083") + assert got["수준"] == 9 and got["판정"] == "없음" and got["직종같음"] + assert got["견줌"]["직종"] == "직종 같음" and got["거리"] < 0.1 + + +def test_펌프차_기계_칸(): + efp = finger.machine({"이름": "콘크리트 펌프차", "규격": "41m,80~95㎥/hr", "단위": "hr"}) + got = option.find("GC000268", efp) + assert got["수준"] == 0 + code = got["입력"]["기계"] + assert ours.eq_of(code)[0] == {"콘크리트펌프"} and 41.0 in option.eq_numbers(code) + + +def test_없는_로직_못_셈(): + efp = finger.machine({"이름": "굴착기", "규격": "0.6㎥", "단위": "hr"}) + assert option.find("없는로직", efp)["판정"] == "못 셈" + + +def test_반올림_폭_견줌(): + assert option.ru(0.15) == pytest.approx(0.005) and option.ru(0.1) == 0.0 + assert option.ru(0.109091) == 0.0 + e = {"인력": {"보통인부": {"품": 0.15, "밑품": 0.15}}, "장비": [], "자재": []} + v = {"L": {"보통인부": 0.1545}, "X": [], "M": {}, "S": {}} + assert not option.compare(e, v, 0.005)["핵심"] and option.compare(e, v, 0.005, rnd=True)["핵심"] + assert not option.strong(e, option.compare(e, v, 0.03)) # 보통인부 하나뿐 = 우연 맞음 diff --git a/resources/tester/test_m02_hopyo_ours.py b/resources/tester/test_m02_hopyo_ours.py new file mode 100644 index 000000000..4027d9d66 --- /dev/null +++ b/resources/tester/test_m02_hopyo_ours.py @@ -0,0 +1,77 @@ +"""338-1 우리 로직 지문(`M02_Hopyo_Ours`) — 판 열쇠 · 캐시 쓰기 · 읽기 · 준비 전 None · 합판 품 · +기계경비 로직 · 섞기 전 변형 · 엔진 셈 캐시 · master_data 원본 그대로. 캐시 폴더는 임시.""" + +import pytest + +from M02_MasterTemplete import M02_Hopyo_Fit as fit +from M02_MasterTemplete import M02_Hopyo_Match as hm +from M02_MasterTemplete import M02_Hopyo_Ours as ours +from resources.tester.test_m02_hopyo_excel import master_hash + +approx = pytest.approx + + +@pytest.fixture +def cache(tmp_path, monkeypatch): + """임시 캐시 폴더 + 로직 셋만 셈(1,438 대신) — 끝나면 메모리 표를 비움.""" + monkeypatch.setenv("AISLO_HOPYO_CACHE", str(tmp_path)) + few = [x for x in hm.targets()["로직"] if x["로직"] in ("GC000211", "GC000198", "GC000268")] + monkeypatch.setattr(hm, "targets", lambda: {**hm._targets(id(ours.whole())), "로직": few}) + monkeypatch.setitem(ours._STATE, "표판", None) + monkeypatch.setitem(ours._STATE, "표", None) + yield tmp_path + ours._STATE.update(표판=None, 표=None) + + +def test_판_열쇠_같음(): + a, b = ours.version(), ours.version() + assert a == b and len(a) == 16 and ours.path_of(a).name == f"우리지문_{a}.json" + + +def test_캐시_쓰기_읽기_준비_전_none(cache): + before = master_hash() + assert ours.table() is None and not ours.ready() + built = ours.build() + assert [o["로직"] for o in built] == ["GC000198", "GC000211", "GC000268"] + assert ours.path_of().parent == cache and ours.path_of().is_file() + ours._STATE.update(표판=None, 표=None) + assert [o["로직"] for o in ours.table()] == ["GC000198", "GC000211", "GC000268"] + assert master_hash() == before + + +def test_데움_뒤_스레드(cache): + thread = ours.warm(build_missing=True) + thread.join(timeout=120) + assert ours.ready() and len(ours.table()) == 3 + + +def test_합판_보통_형틀목공(): + fp = ours.fingerprint("GC000211", "㎡") + plain = next(v for v in fp["변형"] if v["in"].get("유형") == "보통") + assert plain["L"]["형틀목공"] == approx(0.112, abs=1e-3) + assert plain["L"]["보통인부"] == approx(0.045, abs=1e-3) + assert fp["직종"] == ["보통인부", "형틀목공"] and not fp["기계경비"] + assert fp["고름조건"]["횟수"] # 고르기 글 「4회사용시」 낱말 + + +def test_기계경비_로직은_고른_기계_1시간(): + v = ours.run("GC000268", {}) + assert v["L"] == {} and len(v["X"]) == 1 and v["X"][0][2] == 1.0 + assert ours.fingerprint("GC000268", "hr")["기계경비"] + + +def test_섞기_전_변형(): + table = ours.table() or ours.build() + mixed = next( + o + for o in table + if any(0 < ((v.get("mix") or {}).get("인력비율") or 0) < 100 for v in o["변형"]) + ) + prep = fit._prep(mixed, {}) + assert any(v["in"] and v["in"].get("_섞기전") for v in prep["vs"]) + + +def test_엔진_셈_캐시(): + a = ours.run("GC000198", {"소량감률": 50}) + assert ours.run("GC000198", {"소량감률": 50}) is a # 같은 입력은 다시 안 셈 + assert ours.run("없는로직", {}) is None From 01d3b0d2f2a52311d5201469c50b1e18e5240ee7 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 15 Oct 2026 23:37:24 +0900 Subject: [PATCH 60/81] =?UTF-8?q?feat(M02):=20338-3c=20=ED=99=95=EC=9D=B8?= =?UTF-8?q?=20=EC=B0=BD=20=C2=B7=20=EA=B3=A0=EB=A5=B4=EA=B8=B0=20=EC=B0=BD?= =?UTF-8?q?=20=C2=B7=20=EB=B3=84=EC=B9=AD=20=EC=9E=A5=EB=B6=80=20=ED=99=94?= =?UTF-8?q?=EB=A9=B4=20=E2=80=94=20=EC=B2=B4=ED=81=AC=ED=95=9C=20=EC=A4=84?= =?UTF-8?q?=EB=A7=8C=20[=ED=99=95=EC=9D=B8=ED=95=98=EA=B3=A0=20=EC=82=AC?= =?UTF-8?q?=EC=A0=84=EC=A1=B0=ED=95=A9=20=EB=A7=8C=EB=93=A4=EA=B8=B0]=20?= =?UTF-8?q?=C2=B7=20=EC=98=9B=20=EC=A6=89=EC=8B=9C=20=ED=99=95=EC=A0=95=20?= =?UTF-8?q?=C2=B7=20=EB=B0=94=EB=A1=9C=20=EB=A7=8C=EB=93=A4=EA=B8=B0=20?= =?UTF-8?q?=EC=97=86=EC=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 새 Hopyo_Check(확인 창 · 본 칸 · 모달) — 상태 색 · 체크 제안 · 우리 로직 여럿 · 배 · 몫 · 옵션 · 품 비교 · 뺌 · 원문 보기 · 캐시 m02-hopyo-check · 셈 중 3 초 다시 읽음 · 최신 응답만 - 새 Hopyo_Choose(고르기 창 · 후보 판정 · 여럿 고름 · 몫 · 찾기 글 미리 넣음) · 새 Hopyo_Ledger(장부 화면 · 고친 것 캐시 · [저장] 때만 PUT) - 고침 View(호표 하나 = 확인 창) · Sheet(펼침 뒤 한 번 · 하위 호표 먼저) · Pick(확인된 것 바로 참조 · 아니면 확인 창) · Api(3-3 꼴 · 만들기 몸 · 장부 길) · Side([별칭 장부]) · css · Labor_Pick(query) - 없앰 saveHopyoMemory · [확정] · [같음 모두 확정] · [근거 맞음 모두 확정] · 바로 만들기 - 시험 test_m02_hopyo_screen.py 새로 · helper_m02_hopyo_check.cjs 새 · pick · seam 도우미 손질 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01XWh5wNNycMkbwMpACz1rte --- .../M02_MasterTemplete_Hopyo.css | 67 +- .../M02_MasterTemplete_Hopyo_Api.ts | 169 ++++- .../M02_MasterTemplete_Hopyo_Check.ts | 680 ++++++++++++++++++ .../M02_MasterTemplete_Hopyo_Choose.ts | 231 ++++++ .../M02_MasterTemplete_Hopyo_Ledger.ts | 358 +++++++++ .../M02_MasterTemplete_Hopyo_Pick.ts | 36 +- .../M02_MasterTemplete_Hopyo_Sheet.ts | 234 ++++-- .../M02_MasterTemplete_Hopyo_Side.ts | 27 +- .../M02_MasterTemplete_Hopyo_View.ts | 480 +------------ .../M02_MasterTemplete_Labor_Pick.ts | 8 +- resources/tester/helper_m02_hopyo_check.cjs | 184 +++++ resources/tester/helper_m02_hopyo_pick.cjs | 36 +- resources/tester/helper_m02_hopyo_seam.cjs | 22 +- resources/tester/test_m02_hopyo_screen.py | 426 +++++++---- resources/tester/test_m02_hopyo_seam.py | 10 +- 15 files changed, 2204 insertions(+), 764 deletions(-) create mode 100644 M02_MasterTemplete/M02_MasterTemplete_Hopyo_Check.ts create mode 100644 M02_MasterTemplete/M02_MasterTemplete_Hopyo_Choose.ts create mode 100644 M02_MasterTemplete/M02_MasterTemplete_Hopyo_Ledger.ts create mode 100644 resources/tester/helper_m02_hopyo_check.cjs diff --git a/M02_MasterTemplete/M02_MasterTemplete_Hopyo.css b/M02_MasterTemplete/M02_MasterTemplete_Hopyo.css index 8e771bad7..9229291f9 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Hopyo.css +++ b/M02_MasterTemplete/M02_MasterTemplete_Hopyo.css @@ -49,8 +49,8 @@ font-variant-numeric: tabular-nums; } -/* 줄 표 — 갈래 · 수량 · 단위 · 엑셀 단가 · 맞춤 · 입력값 · 환산 · 단추 = 줄바꿈 없음(글자가 세로로 안 - 갈라짐 · 폭 = 글 폭) · 품명 · 규격 · 대상이 남는 폭 · 칸 여백 좁힘(공용 표보다 앞서게 두 갈래 이름) */ +/* 확인 창 줄 표(338) — 체크 · 갈래 · 수량 · 단위 · 상태 · 단추 = 줄바꿈 없음(글자가 세로로 안 갈라짐 · + 폭 = 글 폭) · 품명 · 규격 · 우리 로직 · 옵션 · 품 비교가 남는 폭 · 칸 여백 좁힘(공용 표보다 앞서게 두 갈래 이름) */ .m02-hopyo__table.m02-hopyo__lines :is(th, td) { padding: var(--spacing-8); } @@ -58,19 +58,17 @@ .m02-hopyo__table.m02-hopyo__lines :is(th, td):is( :nth-child(1), - :nth-child(4), + :nth-child(2), :nth-child(5), :nth-child(6), :nth-child(7), - :nth-child(9), - :nth-child(10), :nth-child(11) ) { white-space: nowrap; } -/* 입력값 — 길면 줄임표(전체 글은 칸 풍선 도움말) */ -.m02-hopyo__table.m02-hopyo__lines td:nth-child(9) { +/* 옵션 · 품 비교 — 길면 줄임표(전체 글은 칸 풍선 도움말) */ +.m02-hopyo__table.m02-hopyo__lines td:is(:nth-child(9), :nth-child(10)) { max-width: 24em; overflow: hidden; text-overflow: ellipsis; @@ -89,23 +87,56 @@ white-space: nowrap; } -.m02-hopyo__state.is-기억, -.m02-hopyo__state.is-같음, +/* 상태 색(338) — 맞음 초록 · 별칭 파랑 · 다른 점 주황 · 후보 회색(테두리 기본) · 없음 빨강 */ +.m02-hopyo__state.is-맞음, .m02-hopyo__state.is-호표 { border-color: var(--color-success); color: var(--color-success); } -.m02-hopyo__state.is-후보 { +.m02-hopyo__state.is-별칭 { + border-color: var(--color-info); + color: var(--color-info); +} + +.m02-hopyo__state.is-다른점 { border-color: var(--color-warning); color: var(--color-warning); } +.m02-hopyo__state.is-후보, +.m02-hopyo__state.is-뺌, +.m02-hopyo__state.is-노임 { + color: var(--color-text-secondary); +} + .m02-hopyo__state.is-없음 { border-color: var(--color-danger); color: var(--color-danger); } +/* 우리 로직 칸 — 대상 여럿 = 칸 안 여러 줄(이름 · 키 · 몫 · 배 칸 · [옵션]) */ +.m02-hopyo__parts { + min-width: 14em; +} + +.m02-hopyo__part { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: var(--spacing-4); +} + +.m02-hopyo__part + .m02-hopyo__part { + margin-top: var(--spacing-4); +} + +.m02-hopyo__whole { + padding: var(--spacing-4) var(--spacing-8); + border: 1px solid var(--color-border); + border-radius: var(--radius-sm); +} + .m02-hopyo__times { width: 6em; } @@ -150,6 +181,22 @@ margin-bottom: var(--spacing-8); } +.m02-hopyo__cand { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: var(--spacing-4); +} + +/* 별칭 장부(338) — 위 층 줄 = 회색 읽기만 · 고친 줄 = 왼쪽 띠 */ +.m02-hopyo-ledger__row.is-upper { + color: var(--color-text-muted); +} + +.m02-hopyo-ledger__row.is-edited > td:first-child { + box-shadow: inset 3px 0 0 var(--color-accent); +} + /* 엑셀 보기 창(329) — 왼쪽 읽기 전용 시트 · 오른쪽 호표 목록 · 불러오기 결과 */ .m02-hopyo-sheet__dialog > .ui-modal__body { overflow: hidden; diff --git a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Api.ts b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Api.ts index 12312fba2..9cd7fc52e 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Api.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Api.ts @@ -1,7 +1,8 @@ /* ============================================================================= * M02_MasterTemplete_Hopyo_Api.ts - * 일위대가 실무자료(PLAN 324) 서버 길 — `/api/m02/layers/{층}/hopyo…` · 층 = 화면 층(`Api_Fetch` 지금 층) · - * 올리기 = FormData(숨은 file 입력) · 그 밖 = 공용 `call`(서버 까닭 글 그대로). + * 일위대가 실무자료(PLAN 324 · 338) 서버 길 — `/api/m02/layers/{층}/hopyo…` · 층 = 화면 층(`Api_Fetch` 지금 층) · + * 올리기 = FormData(숨은 file 입력) · 그 밖 = 공용 `call`(서버 까닭 글 그대로) · + * 맞춤 응답 꼴 = 설계 3-3(상태 · 체크 제안 · 대상 목록 · 품 비교) · 만들기 몸 = 확인한 줄만(3-6) · 별칭 장부(3-5). * ========================================================================== */ import { API_BASE_URL } from "@config/config_frontend"; @@ -33,7 +34,7 @@ export interface HopyoBrief { 규격: string; 단위: string; 금액: Money; - /** [들어감 수, 줄 수] */ + /** [체크 제안 수, 줄 수] */ 맞춤: [number, number]; /** 일위대가 시트 행 [시작, 끝](엑셀 1부터 · 329 · 옛 파일 없음) */ 행?: [number, number]; @@ -75,7 +76,7 @@ export interface HopyoLineBasis { 행: [number, number]; } -/** 맞춤 대상 — 로직 · 자재 · 다른 호표 */ +/** 후보 · 고른 대상 글 — 로직 · 자재 · 다른 호표 */ export interface HopyoTarget { 로직?: string; 자재?: string; @@ -86,17 +87,55 @@ export interface HopyoTarget { 점수?: number; /** 근거 맞춤(330 · 옛 파일 · 근거 없는 줄은 없음) */ 근거?: HopyoBasisHit; + /** 후보 판정(338) — 맞음 · 다른 점 · 후보 */ + 판정?: "맞음" | "다른 점" | "후보"; + 글?: string; } +/** 맞춤 대상 하나(338 · 로직 여럿 가능) — 배 = 환산 × 몫 · 우리 수량 = 엑셀 수량 × 배 */ +export interface HopyoPart extends HopyoTarget { + 입력값: Record; + 못읽음?: string[]; + /** 칸마다 값 출처 — 별칭 · 블록 · 규격 · 자동값 */ + 출처?: Record; + 환산?: { 배: number | null; 갈래: string } | null; + 몫?: number | null; + 배: number | null; + /** 블록 절 제목(조합 덧 로직) */ + 어디?: string; +} + +/** 품 묶음 — 직종 품 · 장비 시간 · 자재 수량 */ +export interface HopyoQty { + 인력?: Record; + 장비?: { 갈래: string | string[]; 시간: number }[]; + 자재?: { 이름: string; 수량: number }[]; +} + +export type HopyoState = + "맞음" | "별칭" | "다른 점" | "후보" | "없음" | "뺌" | "호표" | "%" | "노임"; + export interface HopyoMatch { 열쇠: string; - 상태: "기억" | "같음" | "후보" | "없음" | "빼기" | "호표" | "%"; - 대상: HopyoTarget | null; - 입력값: Record; - 못읽음: string[]; - 환산: { 배: number | null; 갈래: string } | null; - 들어감: boolean; + 상태: HopyoState; + /** 처음 체크 제안(확인 대기 — 들어감 아님) */ + 체크: boolean; + 대상: HopyoPart[]; 후보?: HopyoTarget[]; + 글?: string; + 꼬리표?: string[]; + 품?: { 엑셀: HopyoQty; 우리: HopyoQty; 같음: Record }; + 갈리는칸?: string[]; + 별칭?: { 층: Layer; 주인: string | null; 규격다름?: boolean; 근거다름?: boolean }; + 뺌?: string; +} + +/** 숫자 판정 셈 — 끝 · 한 블록 · 모두 · 까닭(우리 로직 준비 중 등) */ +export interface HopyoCalc { + 끝: boolean; + 한: number; + 모두: number; + 까닭?: string; } export interface HopyoLine { @@ -115,14 +154,38 @@ export interface HopyoLine { } export interface HopyoOne extends HopyoFile { - 호표: HopyoBrief & { 줄: HopyoLine[] }; + 호표: HopyoBrief & { + 줄: HopyoLine[]; + /** 직접 줄 호표 통째 제안(노임 · 장비를 바로 적은 호표) */ + 통째?: { 대상: HopyoPart[]; 글?: string; 체크: boolean } | null; + 셈?: HopyoCalc; + /** 만들 층에 이 호표로 만든 사전조합 */ + 만든?: { 키: string; 확인: boolean } | null; + }; } +/** 미리 셈(저장 안 함) — 고른 대상 · 옵션으로 우리 품 */ export interface HopyoGuess { - 대상: HopyoTarget; - 입력값: Record; - 못읽음: string[]; - 환산: { 배: number | null; 갈래: string }; + 대상: HopyoPart[]; + 품?: HopyoMatch["품"]; + 글?: string; +} + +/** 만들기 몸 대상 하나 — 로직 · 자재 · 호표 중 하나(금액 · 품 칸 없음 — 서버가 셈) */ +export interface BuildTarget { + 로직?: string; + 자재?: string; + 호표?: number; + 입력값?: Record; + 배: number; + 몫?: number | null; +} + +/** 확인한 줄 — 대상 목록 또는 뺌 까닭 */ +export interface BuildLine { + 차례: number; + 대상?: BuildTarget[]; + 뺌?: "우리에 없음" | "안 씀"; } export interface HopyoMissing { @@ -141,22 +204,45 @@ export interface HopyoBuilt { 다시씀: boolean; 빠진줄: HopyoMissing[]; 비교: { 엑셀: number | null; 우리: number | null; 차이: number | null; 까닭: string | null }; + 장부?: { 씀: number }; } -/** [로직 추가] 「실무 호표」 줄 하나 */ +/** [로직 추가] 「실무 호표」 줄 하나 — 확인 = 문서 층에 확인된 사전조합 */ export interface HopyoPickRow extends Omit { 층: Layer; 주인: string | null; 키: string; 파일: string; + 확인?: LaborRef | null; } -/** 확정 한 줄 — 로직 · 자재 · 빼기 · null(지움) */ -export type HopyoMemory = - | { 로직: string; 입력값: Record; 환산: number } - | { 자재: string; 입력값: Record; 환산: number } - | { 빼기: true } - | null; +/** 별칭 장부 짝 하나 */ +export interface LedgerPair { + 로직?: string; + 자재?: string; + 입력값: Record; + 배: number; + 몫: number | null; +} + +/** 별칭 장부 한 줄(쓰기 꼴) */ +export interface LedgerEntry { + 원래: { 품명: string; 규격: string; 단위: string }; + 갈래: "짝" | "우리에 없음" | "안 씀"; + 짝: LedgerPair[]; + 엑셀조항?: unknown[]; +} + +/** 별칭 장부 보기 한 줄 — 층 · 고침(이 층 · 쓰기 권한) */ +export interface LedgerRow extends LedgerEntry { + 열쇠: string; + 누가: { id: number; 이름: string | null } | null; + 때: string | null; + 처음파일: string | null; + 층: Layer; + 주인: string | null; + 고침: boolean; +} const enc = encodeURIComponent; /** 지금 화면 층(구조물 조합 층) */ @@ -177,39 +263,58 @@ export const readHopyo = (scope: HopyoScope, key: string): Promise => export const readHopyoGrid = (scope: HopyoScope, key: string): Promise => call(at(scope, `/hopyo-grid/${enc(key)}`)); -export const readHopyoOne = (scope: HopyoScope, key: string, no: number): Promise => - call(at(scope, `/hopyo/${enc(key)}/${no}`)); +/** 호표 하나 — `to` = 만들 층(호표 참조 줄 체크 · 만든 · 없으면 파일 층) */ +export const readHopyoOne = ( + scope: HopyoScope, + key: string, + no: number, + to?: HopyoScope, +): Promise => { + const want = to ? `?to=${to.layer}${to.owner ? `&to_owner=${enc(to.owner)}` : ""}` : ""; + return call(at(scope, `/hopyo/${enc(key)}/${no}${want}`)); +}; +/** 미리 셈(저장 안 함) — 고른 대상 목록 · 옵션 → 우리 품 */ export const guessHopyo = ( scope: HopyoScope, key: string, no: number, 차례: number, - 대상: HopyoTarget, + 대상: BuildTarget[], ): Promise => call(at(scope, `/hopyo/${enc(key)}/${no}/guess`), { method: "POST", body: JSON.stringify({ 차례, 대상 }), }); -export const saveHopyoMemory = ( +export const readLedger = async (scope: HopyoScope): Promise => + (await call<{ 줄: LedgerRow[] }>(at(scope, "/hopyo-ledger"))).줄; + +/** 별칭 장부 [저장] — 고친 줄 · null(지움) · 이 층만 */ +export const saveLedger = async ( scope: HopyoScope, - 줄: Record, -): Promise => - call(at(scope, "/hopyo-memory"), { method: "POST", body: JSON.stringify({ 줄 }) }); + 줄: Record, +): Promise => + ( + await call<{ 줄: LedgerRow[] }>(at(scope, "/hopyo-ledger"), { + method: "PUT", + body: JSON.stringify({ 줄 }), + }) + ).줄; export const deleteHopyo = (scope: HopyoScope, key: string): Promise => call(at(scope, `/hopyo/${enc(key)}`), { method: "DELETE" }); -/** 실무 호표 → `target` 층 사전조합(파일 층 = `from`) */ +/** [확인하고 사전조합 만들기] — 확인한 줄만 · 직접 줄 호표 = `통째` → `target` 층 사전조합(파일 층 = `from`) */ export const buildHopyo = ( target: HopyoScope, from: { 층: Layer; 주인: string | null; 키: string; 번: number }, - 새로 = false, + 줄: BuildLine[], + 통째?: { 대상: BuildTarget[] } | null, ): Promise => call(at(target, "/hopyo-build"), { method: "POST", - body: JSON.stringify({ ...from, 새로 }), + body: JSON.stringify(통째 ? { ...from, 통째 } : { ...from, 줄 }), }); export const listHopyoPicks = async (scope: HopyoScope): Promise => diff --git a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Check.ts b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Check.ts new file mode 100644 index 000000000..7087db5b9 --- /dev/null +++ b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Check.ts @@ -0,0 +1,680 @@ +/* ============================================================================= + * M02_MasterTemplete_Hopyo_Check.ts + * 일위대가 실무자료 확인 창(PLAN 338-3 · 설계 4-1) — 호표 하나의 줄마다 서버 맞춤(상태 · 체크 제안 · + * 우리 로직 · 옵션 · 품 비교)을 보이고, 사용자가 체크한 줄만 [확인하고 사전조합 만들기]로 서버에 보냄. + * · 자리 = 실무자료 본 칸(호표 누름) · [로직 추가] 「실무 호표」 모달 · 엑셀 보기 창 펼침 [확인 창 열기] + * · 머리 = 호표 · 「제안 n / 줄 m」 · 숫자 판정 셈 중이면 3 초마다 다시 읽음(끝나면 멈춤 · 만진 줄은 그대로) + * · 줄 = [✓] · 갈래 · 품명 · 규격 · 수량 · 단위 · 상태(맞음 · 별칭 · 다른 점 · 후보) · 우리 로직(여럿 · 몫 · 배) · + * 옵션(칸=값 · 출처 · 못 가름) · 품 비교(엑셀 → 우리 ✓ / ≠) · [고르기] [원문 보기] [엑셀에서 보기] · 뺌 까닭 + * · 대상 · 옵션을 바꾸면 서버 `guess`(저장 안 함 · 줄마다 최신 응답만)가 우리 품을 다시 셈 + * · 캐시 = sessionStorage `m02-hopyo-check:{층}:{주인}:{키}:{번}`(조작 즉시 · 만들기 성공하면 지움) · 자동 저장 없음 + * · 화면 잠금은 [확인하고 사전조합 만들기] 때만 · 쓰기 단추 = 만들 층 쓰기 권한만. + * 계산은 서버(`M02_Hopyo_Match` · `M02_Hopyo_Build`) — 화면은 보이기 · 체크 · 고름만. + * ========================================================================== */ + +import { + createButton, + el, + hideLoadingOverlay, + showConfirmDialog, + showLoadingOverlay, + showToast, +} from "@ui/ui_template_elements"; +import { loadingError, showLoading } from "@ui/ui_template_loading"; +import { openModal } from "@ui/ui_template_modal"; +import { createSourceView } from "../M01_MasterData/M01_MasterData_UI_LogicLab_Source"; +import { + buildHopyo, + guessHopyo, + readHopyoOne, + type BuildLine, + type BuildTarget, + type HopyoBuilt, + type HopyoLine, + type HopyoMatch, + type HopyoOne, + type HopyoPart, + type HopyoScope, +} from "./M02_MasterTemplete_Hopyo_Api"; +import { + builtWord, + inputsText, + openHopyoChoose, + sourceText, + targetText, +} from "./M02_MasterTemplete_Hopyo_Choose"; +import type { HopyoSheetOptions } from "./M02_MasterTemplete_Hopyo_Sheet"; +import { fetchLogicInputs } from "./M02_MasterTemplete_Labor_Api"; +import { openLaborInput } from "./M02_MasterTemplete_Labor_Input"; +import "./M02_MasterTemplete_Hopyo.css"; + +export const POLL_MS = 3000; +const OUT = ["우리에 없음", "안 씀"] as const; + +/** 확인 창 캐시 — 사용자가 만진 것만(서버 제안 위에 덮어 보임) */ +export interface CheckDraft { + 체크: Record; + 대상: Record; + 뺌: Record; + 품: Record; + 통째?: boolean; +} + +const empty = (): CheckDraft => ({ 체크: {}, 대상: {}, 뺌: {}, 품: {} }); +export const draftKey = (scope: HopyoScope, key: string, no: number): string => + `m02-hopyo-check:${scope.layer}:${scope.owner ?? ""}:${key}:${no}`; + +export function loadDraft(k: string): CheckDraft { + try { + const raw = sessionStorage.getItem(k); + return raw ? { ...empty(), ...(JSON.parse(raw) as Partial) } : empty(); + } catch { + return empty(); + } +} +export function saveDraft(k: string, d: CheckDraft): void { + try { + sessionStorage.setItem(k, JSON.stringify(d)); + } catch { + /* 캐시 못 씀 = 이 화면에만 남음 */ + } +} +export function dropDraft(k: string): void { + try { + sessionStorage.removeItem(k); + } catch { + /* 없음 */ + } +} + +/** 줄의 지금 대상 — 사용자가 고른 것(캐시) · 없으면 서버 제안 */ +export const partsOf = (line: HopyoLine, d: CheckDraft): HopyoPart[] => { + const got = d.대상[line.차례] ?? line.맞춤.대상; + return Array.isArray(got) ? got : []; // 옛 응답(대상 하나) = 대상 없음 +}; +/** 체크 — 사용자가 만진 줄은 캐시 · 아니면 서버 제안 */ +export const isChecked = (line: HopyoLine, d: CheckDraft): boolean => + d.체크[line.차례] ?? !!line.맞춤.체크; +/** 체크할 수 있음 — % 줄 아님 · 뺌 까닭 또는 대상(배 0 넘음) */ +export function checkable(line: HopyoLine, d: CheckDraft): boolean { + if (line.맞춤.상태 === "%") return false; + if (d.뺌[line.차례]) return true; + const parts = partsOf(line, d); + return parts.length > 0 && parts.every((p) => p.배 !== null && p.배 > 0); +} + +/** 대상 → 만들기 몸 대상(이름 · 품 같은 보이기 칸은 안 보냄) */ +export function toTarget(p: HopyoPart): BuildTarget { + if (p.호표 !== undefined) return { 호표: p.호표, 배: p.배 ?? 1 }; + const head = p.로직 ? { 로직: p.로직, 입력값: p.입력값 ?? {} } : { 자재: p.자재 }; + return { ...head, 배: p.배 ?? 1, ...(p.몫 != null ? { 몫: p.몫 } : {}) }; +} + +/** 체크한 줄만 → 만들기 몸 `줄` */ +export const bodyOf = (lines: HopyoLine[], d: CheckDraft): BuildLine[] => + lines + .filter((l) => isChecked(l, d) && checkable(l, d)) + .map((l) => + d.뺌[l.차례] + ? { 차례: l.차례, 뺌: d.뺌[l.차례] as BuildLine["뺌"] } + : { 차례: l.차례, 대상: partsOf(l, d).map(toTarget) }, + ); + +/** 빠질 줄 까닭별 수 — 확인 안 함 · 뺌 · % 줄 */ +export function leftOut(lines: HopyoLine[], d: CheckDraft): Record { + const out: Record = {}; + for (const l of lines) { + const why = + l.맞춤.상태 === "%" + ? "% 줄" + : !isChecked(l, d) || !checkable(l, d) + ? "확인 안 함" + : d.뺌[l.차례] + ? "뺌" + : null; + if (why) out[why] = (out[why] ?? 0) + 1; + } + return out; +} + +const n4 = (v: number | undefined): string => + v === undefined ? "–" : String(Math.round(v * 10000) / 10000); + +/** 품 비교 글 — 「콘크리트공 0.1091 → 0.1091 ✓ · 굴착기 0.29 → 0.31 ≠」 */ +export function compareText(q: HopyoMatch["품"] | undefined): string { + if (!q) return ""; + const ex = q.엑셀.인력 ?? {}; + const ours = q.우리.인력 ?? {}; + const mark = (k: string): string => (q.같음[k] ? "✓" : "≠"); + const out = [...new Set([...Object.keys(ex), ...Object.keys(ours)])].map( + (k) => `${k} ${n4(ex[k])} → ${n4(ours[k])} ${mark(k)}`, + ); + const name = (g: string | string[]): string => [g].flat().join("+"); + for (const x of q.엑셀.장비 ?? []) { + const o = (q.우리.장비 ?? []).find((y) => name(y.갈래) === name(x.갈래)); + out.push(`${name(x.갈래)} ${n4(x.시간)} → ${n4(o?.시간)} ${mark([x.갈래].flat()[0])}`); + } + return out.join(" · "); +} + +/** 줄마다 최신 요청만 — `ask(줄)` 이 돌려준 함수가 그 뒤 같은 줄 요청이 없을 때만 true */ +export function newest(): (key: number) => () => boolean { + const last = new Map(); + return (key) => { + const my = (last.get(key) ?? 0) + 1; + last.set(key, my); + return () => last.get(key) === my; + }; +} + +/** [맞음 · 별칭 모두 체크] 대상 줄 */ +export const sureLines = (lines: HopyoLine[], d: CheckDraft): HopyoLine[] => + lines.filter((l) => ["맞음", "별칭"].includes(l.맞춤.상태) && checkable(l, d)); + +const why = (error: unknown): string => (error instanceof Error ? error.message : String(error)); +const won = (v: number | null | undefined): string => + v === null || v === undefined ? "" : v.toLocaleString("ko-KR"); +const fail = (error: unknown): void => showToast(`실패 — ${why(error)}`, "error"); +const note = (text: string): HTMLElement => el("span", { className: "m02-hopyo__note", text }); +const td = (text: string, cls = ""): HTMLElement => + el("td", { text, className: cls, attrs: { title: text } }); +const openSheet = (o: HopyoSheetOptions): void => + void import("./M02_MasterTemplete_Hopyo_Sheet").then((m) => m.openHopyoSheet(o), fail); + +export interface CheckOptions { + /** 실무자료 파일 층 */ + scope: HopyoScope; + key: string; + no: number; + /** 사전조합을 만들 층(없으면 파일 층) */ + target?: HopyoScope; + /** 만들 층 쓰기 권한 — 없으면 체크 · 만들기 없음(보기만) */ + canWrite: boolean; + /** [← 호표 목록](본 칸) */ + onBack?: () => void; + /** 만들었음 — [로직 추가] 참조 줄 · 모달 닫기 */ + onBuilt?: (got: HopyoBuilt, hopyo: HopyoOne["호표"]) => void; +} + +/** 확인 창 몸 — 본 칸 · 모달이 같이 씀 */ +export function buildHopyoCheck(opt: CheckOptions): HTMLElement { + const root = el("div", { + className: "m02-hopyo m02-hopyo-check", + attrs: { "data-hopyo-check": `${opt.key}|${opt.no}` }, + }); + const target = opt.target ?? opt.scope; + const k = draftKey(opt.scope, opt.key, opt.no); + let d = loadDraft(k); + let got: HopyoOne | null = null; + let built: HopyoBuilt | null = null; + let seq = 0; + let timer = 0; + const ask = newest(); // 줄마다 최신 미리 셈만 + const keep = (): void => saveDraft(k, d); + + async function load(quiet = false): Promise { + const my = ++seq; + const stop = quiet ? () => {} : showLoading(root); + try { + const one = await readHopyoOne(opt.scope, opt.key, opt.no, opt.target); + if (my !== seq) return; + got = one; + draw(); + } catch (error) { + if (my === seq && !quiet) root.replaceChildren(loadingError(why(error), () => void load())); + } finally { + stop(); + } + } + + /** 대상 바꿈 → 캐시 · 체크 · 서버 미리 셈(최신 응답만) */ + function setParts(line: HopyoLine, parts: HopyoPart[]): void { + d.대상[line.차례] = parts; + d.체크[line.차례] = true; + delete d.뺌[line.차례]; + keep(); + draw(); + const mine = ask(line.차례); + guessHopyo(opt.scope, opt.key, opt.no, line.차례, parts.map(toTarget)).then((g) => { + if (!mine()) return; + if (Array.isArray(g.대상)) d.대상[line.차례] = g.대상; + if (g.품) d.품[line.차례] = g.품; + keep(); + draw(); + }, fail); + } + + function editOptions(line: HopyoLine, at: number): void { + const parts = partsOf(line, d); + const p = parts[at]; + if (!p?.로직) return; + fetchLogicInputs([p.로직]).then( + (res) => + openLaborInput({ + title: `${p.로직} ${p.이름 ?? ""}`.trim(), + logic: res.로직[0] ?? null, + values: (p.입력값 ?? {}) as never, + memo: null, + readOnly: !opt.canWrite, + done: (r) => + setParts( + line, + parts.map((x, i) => (i === at ? { ...x, 입력값: r.입력값 } : x)), + ), + }), + fail, + ); + } + + /** [원문 보기] — 엑셀 인용 조항 · 우리 로직 출처 두 갈래 */ + async function showSource(line: HopyoLine): Promise { + const excel = (line.근거?.출처 ?? []).map((x) => x.join(" ").trim()).filter(Boolean); + const codes = partsOf(line, d).flatMap((p) => (p.로직 ? [p.로직] : [])); + const ours = codes.length + ? (await fetchLogicInputs(codes).catch(() => ({ 로직: [] }))).로직.flatMap((x) => + x.출처 ? [`${x.로직} — ${x.출처}`] : [], + ) + : []; + openModal({ + title: `원문 — ${line.이름}`, + closeLabel: "닫기", + wide: true, + dialogClass: "m02-modal m02-hopyo__src", + mount: (body) => { + const part = (head: string, list: string[], cut: (s: string) => string): void => { + body.append(el("h4", { text: head })); + if (!list.length) body.append(note("없음")); + for (const s of list) { + const view = createSourceView({ + path: "/m02/logic-lib/source", + pdf: "/m02/logic-lib/source-pdf", + }); + view.show(cut(s)); + body.append(note(s), view.root); + } + }; + part("엑셀 인용 조항", excel, (s) => s); + part("우리 로직 출처", ours, (s) => s.split(" — ").slice(1).join(" — ")); + }, + }); + } + + function partCell(line: HopyoLine): HTMLElement { + const parts = partsOf(line, d); + return el("td", { + className: "m02-hopyo__parts", + children: parts.map((p, at) => { + const times = el("input", { + className: "ui-input ui-input--sm m02-hopyo__times", + attrs: { type: "number", step: "any", min: "0", value: p.배 == null ? "" : String(p.배) }, + }) as HTMLInputElement; + times.title = "배 — 우리 수량 = 엑셀 수량 × 배"; + times.disabled = !opt.canWrite; + times.addEventListener("change", () => { + const v = times.value === "" ? null : Number(times.value); + d.대상[line.차례] = parts.map((x, i) => (i === at ? { ...x, 배: v } : x)); + keep(); + draw(); + }); + const share = p.몫 != null ? ` · 몫 ${p.몫}` : ""; + return el("div", { + className: "m02-hopyo__part", + children: [ + el("span", { + text: `${targetText(p)} ${p.로직 ?? p.자재 ?? ""}${share}`.trim(), + attrs: { title: p.어디 ?? "" }, + }), + times, + ...(p.로직 + ? [ + createButton({ + label: "옵션", + variant: "ghost", + size: "sm", + onClick: () => editOptions(line, at), + }), + ] + : []), + ], + }); + }), + }); + } + + function lineRow(line: HopyoLine): HTMLElement { + const m = line.맞춤; + const parts = partsOf(line, d); + const box = el("input", { attrs: { type: "checkbox", "data-check": String(line.차례) } }); + const input = box as HTMLInputElement; + input.checked = isChecked(line, d) && checkable(line, d); + input.disabled = !opt.canWrite || !checkable(line, d); + input.addEventListener("change", () => { + d.체크[line.차례] = input.checked; + keep(); + draw(); + }); + const out = el("select", { + className: "ui-input ui-input--sm", + attrs: { title: "뺌 — 사전조합에 안 넣고 장부에 남김" }, + children: [ + el("option", { text: "넣음", attrs: { value: "" } }), + ...OUT.map((x) => el("option", { text: `뺌 — ${x}`, attrs: { value: x } })), + ], + }) as HTMLSelectElement; + out.value = d.뺌[line.차례] ?? ""; + out.disabled = !opt.canWrite || m.상태 === "%"; + out.addEventListener("change", () => { + if (out.value) d.뺌[line.차례] = out.value; + else delete d.뺌[line.차례]; + d.체크[line.차례] = !!out.value || d.체크[line.차례]; + keep(); + draw(); + }); + const sheet = (행: [number, number]): void => + openSheet({ + scope: opt.scope, + key: opt.key, + 파일: got?.파일 ?? "", + canWrite: opt.canWrite, + target, + 단산행: 행, + }); + const acts = [ + ...(opt.canWrite && m.상태 !== "%" + ? [ + createButton({ + label: "고르기", + variant: "ghost", + size: "sm", + onClick: () => + openHopyoChoose({ + line, + openSheet: sheet, + done: (ts) => + setParts( + line, + ts.map((t) => ({ ...t, 입력값: t.입력값 ?? {}, 몫: t.몫 ?? null })), + ), + }), + }), + ] + : []), + createButton({ + label: "원문 보기", + variant: "ghost", + size: "sm", + onClick: () => void showSource(line), + }), + ...(line.근거 + ? [ + createButton({ + label: "엑셀에서 보기", + variant: "ghost", + size: "sm", + onClick: () => sheet(line.근거!.행), + }), + ] + : []), + out, + ]; + const tags = [m.글, ...(m.꼬리표 ?? []), sourceText(line.근거)].filter(Boolean).join("\n"); + const opts = parts + .filter((p) => p.로직) + .map((p) => inputsText(p.입력값 ?? {}, p.못읽음 ?? [], p.출처)) + .filter(Boolean); + if (m.갈리는칸?.length) opts.push(`⚠ 못 가름: ${m.갈리는칸.join(", ")}`); + const alias = m.별칭?.규격다름 ? " · 규격 다름" : m.별칭?.근거다름 ? " · 근거 다름" : ""; + return el("tr", { + className: `m02-hopyo__line is-${input.checked ? "in" : "out"}`, + attrs: { "data-line": String(line.차례), "data-state": m.상태 }, + children: [ + el("td", { children: [box] }), + td(line.갈래), + td(line.이름), + td(line.규격), + td(line.수량 === null ? "" : String(line.수량), "m02-hopyo__num"), + td(line.단위), + el("td", { + attrs: { title: tags }, + children: [ + el("span", { + className: `m02-hopyo__state is-${m.상태.replace(" ", "")}`, + text: `${m.상태}${alias}`, + }), + ], + }), + partCell(line), + td(opts.join(" / ")), + td(compareText(d.품[line.차례] ?? m.품)), + el("td", { className: "m02-hopyo__acts", children: acts }), + ], + }); + } + + async function make(h: HopyoOne["호표"]): Promise { + const whole = !!h.통째 && (d.통째 ?? h.통째.체크); + const lines = whole ? [] : bodyOf(h.줄, d); + const out = leftOut(h.줄, d); + const put = lines.filter((x) => x.대상).length; + const outs = lines.length - put; + const ask = whole + ? `호표 통째로 만듦 — ${h.통째!.대상.map(targetText).join(" · ")}` + : `넣을 줄 ${put} · 뺌 ${outs} · 빠질 줄 ${Object.values(out).reduce((a, b) => a + b, 0)}` + + (Object.keys(out).length + ? `(${Object.entries(out) + .map(([a, b]) => `${a} ${b}`) + .join(" · ")})` + : "") + + " — 확인하고 사전조합을 만듦"; + if (!(await showConfirmDialog(ask, "확인하고 만들기"))) return; + const from = { 층: opt.scope.layer, 주인: opt.scope.owner, 키: opt.key, 번: opt.no }; + showLoadingOverlay(); + try { + const res = await buildHopyo( + target, + from, + lines, + whole ? { 대상: h.통째!.대상.map(toTarget) } : null, + ); + dropDraft(k); + d = empty(); + built = res; + const miss = res.빠진줄.length; + showToast(`${res.참조.키} ${builtWord(res)}${miss ? ` — 빠진 줄 ${miss}` : ""}`, "success"); + opt.onBuilt?.(res, h); + await load(true); + } catch (error) { + fail(error); + } finally { + hideLoadingOverlay(); + } + } + + function builtBlock(res: HopyoBuilt): HTMLElement { + const c = res.비교; + const rows = res.빠진줄.map((x) => + el("tr", { + children: [ + td(x.이름), + td(x.규격), + td(x.수량 === null ? "" : String(x.수량), "m02-hopyo__num"), + td(x.단위), + td(won(x.금액), "m02-hopyo__num"), + td(x.까닭), + ], + }), + ); + return el("div", { + className: "m02-hopyo__block m02-hopyo__built", + attrs: { "data-built": res.참조.키 }, + children: [ + note(res.다시씀 ? `있던 ${res.참조.키} 를 확인한 내용으로 덮음` : `${res.참조.키} 만듦`), + ...(rows.length + ? [ + el("h4", { text: `빠진 줄 ${rows.length}` }), + el("table", { + className: "ui-general-block__table m02-hopyo__table", + children: [ + el("thead", { + children: [ + el("tr", { + children: ["품명", "규격", "수량", "단위", "엑셀 금액", "까닭"].map((t) => + el("th", { text: t }), + ), + }), + ], + }), + el("tbody", { children: rows }), + ], + }), + ] + : []), + note( + `값 비교(참고) — 엑셀 ${won(c.엑셀)} · 우리 ${won(c.우리 === null ? null : Math.round(c.우리))}` + + ` · 차이 ${won(c.차이)}${c.까닭 ? ` · ${c.까닭}` : ""}`, + ), + ], + }); + } + + function draw(): void { + if (!got) return; + const h = got.호표; + const checked = h.줄.filter((l) => isChecked(l, d) && checkable(l, d)).length; + const calc = h.셈; + window.clearTimeout(timer); + if (calc && !calc.끝) + timer = window.setTimeout(() => root.isConnected && void load(true), POLL_MS); + const tools: HTMLElement[] = []; + if (opt.canWrite) { + const sure = sureLines(h.줄, d); + const all = createButton({ label: `맞음 · 별칭 모두 체크 (${sure.length})`, size: "sm" }); + all.disabled = !sure.length; + all.addEventListener("click", () => { + for (const l of sure) d.체크[l.차례] = true; + keep(); + draw(); + }); + const whole = !!h.통째 && (d.통째 ?? h.통째.체크); + const go = createButton({ label: "확인하고 사전조합 만들기", variant: "filled", size: "sm" }); + go.disabled = !whole && checked < 1; + go.setAttribute("data-hopyo-make", ""); + go.addEventListener("click", () => void make(h)); + tools.push(all, go); + } + const back = opt.onBack + ? [createButton({ label: "← 호표 목록", variant: "ghost", size: "sm", onClick: opt.onBack })] + : []; + const head = el("div", { + className: "m02-hopyo__bar", + children: [ + ...back, + el("strong", { text: `제${h.번}호표 ${h.이름}` }), + note(h.규격), + note(h.단위), + note(`엑셀 단가 ${won(h.금액.계)}`), + note(`제안 ${h.맞춤[0]} / 줄 ${h.맞춤[1]} · 체크 ${checked}`), + ...(calc && !calc.끝 + ? [note(`숫자 판정 셈 중 ${calc.한}/${calc.모두}${calc.까닭 ? ` · ${calc.까닭}` : ""}`)] + : []), + ...(h.만든 ? [note(`만든 사전조합 ${h.만든.키}${h.만든.확인 ? " · 확인함" : ""}`)] : []), + createButton({ + label: "엑셀에서 보기", + size: "sm", + onClick: () => + openSheet({ + scope: opt.scope, + key: opt.key, + 파일: got!.파일, + canWrite: opt.canWrite, + target, + 호표: opt.no, + }), + }), + ...tools, + ], + }); + const whole = h.통째 + ? (() => { + const box = el("input", { + attrs: { type: "checkbox", "data-hopyo-whole": "" }, + }) as HTMLInputElement; + box.checked = d.통째 ?? h.통째.체크; + box.disabled = !opt.canWrite; + box.addEventListener("change", () => { + d.통째 = box.checked; + keep(); + draw(); + }); + return [ + el("label", { + className: "m02-hopyo__bar m02-hopyo__whole", + children: [ + box, + note( + `통째 = ${h.통째.대상.map(targetText).join(" · ")}${h.통째.글 ? ` — ${h.통째.글}` : ""}`, + ), + ], + }), + ]; + })() + : []; + const heads = [ + "", + "갈래", + "품명", + "규격", + "수량", + "단위", + "상태", + "우리 로직 · 배", + "옵션", + "품 비교", + "", + ]; + root.replaceChildren( + head, + ...whole, + h.줄.length + ? el("div", { + className: "ui-general-block__table-wrap ui-general-block__table-wrap--sticky-head", + children: [ + el("table", { + className: "ui-general-block__table m02-hopyo__table m02-hopyo__lines", + children: [ + el("thead", { + children: [el("tr", { children: heads.map((t) => el("th", { text: t })) })], + }), + el("tbody", { children: h.줄.map(lineRow) }), + ], + }), + ], + }) + : note("구성 줄 없음"), + ...(built ? [builtBlock(built)] : []), + ); + } + + void load(); + return root; +} + +/** 확인 창 모달 — [로직 추가] 「실무 호표」 · 엑셀 보기 창 [확인 창 열기] · 만들면 닫음 */ +export function openHopyoCheck(opt: CheckOptions & { title?: string }): void { + openModal({ + title: opt.title ?? `확인 — 제${opt.no}호표`, + closeLabel: "닫기", + wide: true, + dialogClass: "m02-modal m02-hopyo__check-modal", + mount: (body, close) => + body.append( + buildHopyoCheck({ + ...opt, + onBuilt: (got, h) => { + opt.onBuilt?.(got, h); + close(); + }, + }), + ), + }); +} diff --git a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Choose.ts b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Choose.ts new file mode 100644 index 000000000..26046f7fc --- /dev/null +++ b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Choose.ts @@ -0,0 +1,231 @@ +/* ============================================================================= + * M02_MasterTemplete_Hopyo_Choose.ts + * 일위대가 실무자료 고르기 창(PLAN 338-3 · 설계 4-2 · 옛 `Hopyo_View.choose` 를 옮김) + 글 도우미. + * · 위 = 엑셀 블록 품(직종 · 장비 · 자재) · 인용 조항 · [엑셀에서 보기](단가산출 그 행) + * · 후보 다섯 = 이름 · 키 · 판정 꼬리표(맞음 / 다른 점 / 후보) · 글 · 근거 꼬리표(근거 맞음 초록) · 금액 비 없음 + * · 고름 = [고름](한 로직) · 또는 체크 + 몫 칸 → [고른 n개 함께](조합) · 옵션은 부르는 쪽이 서버 `guess` 로 채움 + * · [다른 로직 찾기] = `openLogicPicker` 에 찾기 글 미리 넣음(엑셀 인용 조항 번호 → 없으면 품명 첫 뜻 낱말) + * 저장 안 함 — 고른 것은 부르는 쪽 캐시(확인 창)로만 감. + * ========================================================================== */ + +import { createButton, el } from "@ui/ui_template_elements"; +import { openModal } from "@ui/ui_template_modal"; +import type { + BuildTarget, + HopyoBasisHit, + HopyoLine, + HopyoLineBasis, + HopyoQty, + HopyoTarget, +} from "./M02_MasterTemplete_Hopyo_Api"; +import { openLogicPicker } from "./M02_MasterTemplete_Labor_Pick"; +import { pickMaterial } from "./M02_Material_Pick"; +import "./M02_MasterTemplete_Hopyo.css"; + +const MATERIAL = new Set(["자재", "별도계상"]); +const note = (text: string): HTMLElement => el("span", { className: "m02-hopyo__note", text }); +const num = (v: number): string => String(Math.round(v * 10000) / 10000); + +/** 대상 글 — 「이름 [단위]」 · 자재는 규격도 · 호표 = 「제N호표」 */ +export const targetText = (t: HopyoTarget | null | undefined): string => { + if (!t) return ""; + if (t.호표 !== undefined) return `제${t.호표}호표 ${t.단위 ? `[${t.단위}]` : ""}`.trim(); + const spec = t.규격 ? ` ${t.규격}` : ""; + return `${t.이름 ?? t.로직 ?? t.자재 ?? ""}${spec}${t.단위 ? ` [${t.단위}]` : ""}`; +}; + +/** 입력값 글 — 「이름=값」 · 칸 출처(블록 · 규격 · 별칭) · 못 가르는 칸 표시 */ +export const inputsText = ( + values: Record, + unread: string[], + from: Record = {}, +): string => + [ + Object.entries(values) + .map(([k, v]) => `${k}=${String(v)}${from[k] && from[k] !== "자동값" ? `(${from[k]})` : ""}`) + .join(", "), + unread.length ? `⚠ 못 가름: ${unread.join(", ")}` : "", + ] + .filter(Boolean) + .join(" · "); + +/** 만들기 결과 낱말 — 있던 것을 덮음 · 새로 만듦 */ +export const builtWord = (got: { 다시씀: boolean }): string => + got.다시씀 ? "있던 것을 확인한 내용으로 덮음" : "만듦"; + +/** 근거 맞음 — 조항 같음(앞 마디 아님) · 단위 맞음 · 직종 겹침 + * (줄 근거 직종이 보통인부뿐이거나 없으면 — 기계 등 — 직종은 안 봄) */ +export const basisOk = (b: HopyoBasisHit | undefined, line?: HopyoLineBasis): boolean => + !!b && + b.까닭.startsWith("조항 같음") && + b.단위 && + (b.직종.length > 0 || !(line?.직종 ?? []).some((x) => x !== "보통인부")); + +/** 후보 근거 꼬리표 — 「조항 건설 공통 3-2-1 · 직종 특별인부 · 단위 같음」 · 근거 없으면 "" */ +export const basisTag = (b: HopyoBasisHit | undefined): string => + b + ? [ + b.조항.length ? `조항 ${b.조항.join(", ")}` : "", + b.직종.length ? `직종 ${b.직종.join(", ")}` : "", + b.단위 ? "단위 같음" : "", + ] + .filter(Boolean) + .join(" · ") + : ""; + +/** 엑셀 근거 줄 — 「출처 건설표준품셈 3-2-1 · 직종 특별인부 · 단가산출 93–101행」 · 없으면 "" */ +export const sourceText = (b: HopyoLineBasis | undefined): string => + b + ? [ + b.출처.length ? `출처 ${b.출처.map((x) => x.join(" ").trim()).join(", ")}` : "", + b.직종.length ? `직종 ${b.직종.join(", ")}` : "", + `단가산출 ${b.행[0] === b.행[1] ? b.행[0] : `${b.행[0]}–${b.행[1]}`}행`, + ] + .filter(Boolean) + .join(" · ") + : ""; + +/** 품 글 — 「콘크리트공 0.1091 · 굴착기 0.29hr · 레미콘 1.02」 */ +export const qtyText = (q: HopyoQty | undefined): string => + q + ? [ + ...Object.entries(q.인력 ?? {}).map(([k, v]) => `${k} ${num(v)}`), + ...(q.장비 ?? []).map((x) => `${[x.갈래].flat().join("+")} ${num(x.시간)}hr`), + ...(q.자재 ?? []).map((x) => `${x.이름} ${num(x.수량)}`), + ].join(" · ") + : ""; + +/** [다른 로직 찾기] 미리 넣을 글 — 엑셀 인용 조항 번호 → 없으면 품명 괄호 밖 첫 뜻 낱말 */ +export function searchHint(line: Pick): string { + const clause = line.근거?.출처?.find((x) => x[1])?.[1]; + if (clause) return clause; + return /[가-힣A-Za-z]{2,}/.exec(line.이름.replace(/\([^()]*\)/g, " "))?.[0] ?? ""; +} + +/** 고른 후보 → 만들기 대상(배 1 · 몫 있으면 배 = 몫) */ +export const pickedTargets = (picked: { c: HopyoTarget; 몫: number | null }[]): BuildTarget[] => + picked.map(({ c, 몫 }) => ({ + ...(c.로직 ? { 로직: c.로직 } : { 자재: c.자재 }), + 입력값: {}, + 배: 몫 ?? 1, + 몫, + })); + +export interface ChooseOptions { + line: HopyoLine; + /** [엑셀에서 보기] — 단가산출 그 행 */ + openSheet?: (행: [number, number]) => void; + done: (targets: BuildTarget[]) => void; +} + +/** 고르기 창 — 엑셀 품 · 인용 조항 · 후보 다섯(하나 또는 여럿 · 몫) · 다른 로직 · 자재 찾기 */ +export function openHopyoChoose(opt: ChooseOptions): void { + const { line } = opt; + const material = MATERIAL.has(line.갈래); + openModal({ + title: `고르기 — ${line.이름} ${line.규격}`.trim(), + closeLabel: "닫기", + dialogClass: "m02-modal m02-hopyo__choose", + mount: (body, close) => { + const finish = (targets: BuildTarget[]): void => { + close(); + opt.done(targets); + }; + const picked = new Map(); + const together = createButton({ label: "고른 0개 함께", size: "sm", disabled: true }); + const paint = (): void => { + together.querySelector(".ui-btn__label")!.textContent = `고른 ${picked.size}개 함께`; + together.disabled = picked.size < 2; + }; + together.addEventListener("click", () => + finish( + pickedTargets( + [...picked].map(([c, box]) => ({ c, 몫: box.value === "" ? null : Number(box.value) })), + ), + ), + ); + const list = (line.맞춤.후보 ?? []).map((c) => { + const share = el("input", { + className: "ui-input ui-input--sm m02-hopyo__times", + attrs: { type: "number", step: "0.05", min: "0", max: "1", placeholder: "몫" }, + }) as HTMLInputElement; + const box = el("input", { attrs: { type: "checkbox", title: "함께 고름(조합)" } }); + box.addEventListener("change", () => { + if ((box as HTMLInputElement).checked) picked.set(c, share); + else picked.delete(c); + paint(); + }); + const tag = basisTag(c.근거); + return el("div", { + className: "m02-hopyo__cand", + attrs: { "data-cand": c.로직 ?? c.자재 ?? "" }, + children: [ + box, + createButton({ + label: `${targetText(c)} · ${c.로직 ?? c.자재 ?? ""}`, + variant: "ghost", + size: "sm", + onClick: () => finish(pickedTargets([{ c, 몫: null }])), + }), + ...(c.판정 + ? [ + el("span", { + className: `m02-hopyo__state is-${c.판정.replace(" ", "")}`, + text: c.판정, + }), + ] + : []), + ...(c.글 ? [note(c.글)] : []), + ...(tag && c.근거 + ? [ + el("span", { + className: `m02-hopyo__basis${basisOk(c.근거, line.근거) ? " is-ok" : ""}`, + text: tag, + attrs: { title: `${c.근거.까닭} · 점수 ${c.근거.점수}` }, + }), + ] + : []), + share, + ], + }); + }); + const excel = qtyText(line.맞춤.품?.엑셀); + const basis = line.근거; + const head = el("div", { + className: "m02-hopyo__bar m02-hopyo__source", + children: [ + ...(excel ? [note(`엑셀 품 — ${excel}`)] : []), + ...(basis ? [note(`엑셀 근거 — ${sourceText(basis)}`)] : []), + ...(basis && opt.openSheet + ? [ + createButton({ + label: "엑셀에서 보기", + variant: "ghost", + size: "sm", + onClick: () => opt.openSheet?.(basis.행), + }), + ] + : []), + ], + }); + const more = createButton({ + label: material ? "다른 자재 찾기…" : "다른 로직 찾기…", + size: "sm", + onClick: () => { + close(); + if (material) pickMaterial((x) => opt.done([{ 자재: x.코드, 배: 1 }])); + else + openLogicPicker((x) => opt.done([{ 로직: x.키, 입력값: {}, 배: 1 }]), undefined, { + query: searchHint(line), + }); + }, + }); + body.append( + ...(head.childElementCount ? [head] : []), + note(list.length ? "후보 — 하나 고름 · 여럿은 체크(몫 칸) 뒤 [함께]" : "가까운 것 없음"), + el("div", { className: "m02-hopyo__cands", children: list }), + el("div", { className: "m02-hopyo__bar", children: [together, more] }), + ); + }, + }); +} diff --git a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Ledger.ts b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Ledger.ts new file mode 100644 index 000000000..73dd80f8c --- /dev/null +++ b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Ledger.ts @@ -0,0 +1,358 @@ +/* ============================================================================= + * M02_MasterTemplete_Hopyo_Ledger.ts + * 일위대가 실무자료 별칭 장부 화면(PLAN 338-3 · 설계 4-4) — 본 칸 · 좌측 판 [별칭 장부] 로 엶. + * · 표 = 엑셀 품명 · 규격 · 단위 | 갈래 | 우리 로직(여럿) · 옵션 · 배 · 몫 | 누가 · 때 · 처음 파일 | 층(위 층 회색 읽기만) + * · 할 일 = 찾기 · 갈래 거름 · 갈래 바꿈 · [로직 바꿈] · [로직 더함] · 배 · 몫 · [옵션] · [지우기] · + * 위 층 줄 [내 층으로 고쳐 씀] + * · 고친 것 = sessionStorage `m02-hopyo-ledger:{층}:{주인}` → [저장] 때만 `PUT`(서버가 짝을 다시 검사) + * · 안 둠 = 발주처 칸 · 자동 올림 · 자동 합침 · 쓰인 횟수 · 회사로 올리기. + * ========================================================================== */ + +import { createButton, el, showToast } from "@ui/ui_template_elements"; +import { loadingError, showLoading } from "@ui/ui_template_loading"; +import { + readLedger, + saveLedger, + type HopyoScope, + type LedgerEntry, + type LedgerPair, + type LedgerRow, +} from "./M02_MasterTemplete_Hopyo_Api"; +import { fetchLogicInputs } from "./M02_MasterTemplete_Labor_Api"; +import { openLaborInput } from "./M02_MasterTemplete_Labor_Input"; +import { openLogicPicker } from "./M02_MasterTemplete_Labor_Pick"; +import "./M02_MasterTemplete_Hopyo.css"; + +export const LEDGER_TITLE = "별칭 장부"; +const KINDS = ["짝", "우리에 없음", "안 씀"] as const; +const LAYER_TEXT: Record = { system: "마스터", company: "회사", personal: "개인" }; +type Edits = Record; +/** 화면 한 줄 — 서버 줄 위에 고친 것 */ +export type LedgerView = LedgerRow & { 고친?: boolean }; + +export const ledgerKey = (scope: HopyoScope): string => + `m02-hopyo-ledger:${scope.layer}:${scope.owner ?? ""}`; +function loadEdits(k: string): Edits { + try { + return JSON.parse(sessionStorage.getItem(k) ?? "{}") as Edits; + } catch { + return {}; + } +} +function saveEdits(k: string, edits: Edits): void { + try { + if (Object.keys(edits).length) sessionStorage.setItem(k, JSON.stringify(edits)); + else sessionStorage.removeItem(k); + } catch { + /* 캐시 못 씀 = 이 화면에만 남음 */ + } +} + +/** 쓰기 꼴 — 보기 칸(층 · 누가 · 때 …) 뺌 */ +export const entryOf = (row: LedgerEntry): LedgerEntry => ({ + 원래: row.원래, + 갈래: row.갈래, + 짝: + row.갈래 === "짝" + ? row.짝.map((p) => ({ ...p, 입력값: p.입력값 ?? {}, 몫: p.몫 ?? null })) + : [], + 엑셀조항: row.엑셀조항 ?? [], +}); + +/** 서버 줄 + 고친 것 → 화면 줄 — 이 층 줄은 고친 값 · 지운 것 뺌 · 위 층에서 옮긴 줄은 이 층 줄로 더함 */ +export function mergeRows(rows: LedgerRow[], edits: Edits, scope: HopyoScope): LedgerView[] { + const mine = rows.filter((r) => r.고침); + const out: LedgerView[] = []; + for (const r of mine) { + if (!(r.열쇠 in edits)) out.push(r); + else if (edits[r.열쇠]) out.push({ ...r, ...edits[r.열쇠]!, 고친: true }); + } + for (const [key, entry] of Object.entries(edits)) { + if (!entry || mine.some((r) => r.열쇠 === key)) continue; + out.push({ + ...entry, + 열쇠: key, + 누가: null, + 때: null, + 처음파일: null, + 층: scope.layer, + 주인: scope.owner, + 고침: true, + 고친: true, + }); + } + return [...out, ...rows.filter((r) => !r.고침)]; +} + +/** 짝 글 — 「GC000198 × 1 · 몫 0.1 · 소량감률=50」 */ +export const pairText = (p: LedgerPair): string => + [ + `${p.로직 ?? p.자재 ?? ""} × ${p.배}`, + p.몫 != null ? `몫 ${p.몫}` : "", + Object.entries(p.입력값 ?? {}) + .map(([k, v]) => `${k}=${String(v)}`) + .join(", "), + ] + .filter(Boolean) + .join(" · "); + +/** 찾기 · 갈래 거름 */ +export const filterRows = (rows: LedgerView[], q: string, kind: string): LedgerView[] => { + const want = q.trim().toLowerCase(); + return rows.filter( + (r) => + (!kind || r.갈래 === kind) && + (!want || + `${r.원래.품명} ${r.원래.규격} ${r.짝.map(pairText).join(" ")}` + .toLowerCase() + .includes(want)), + ); +}; + +const why = (error: unknown): string => (error instanceof Error ? error.message : String(error)); +const td = (text: string, cls = ""): HTMLElement => + el("td", { text, className: cls, attrs: { title: text } }); +const note = (text: string): HTMLElement => el("span", { className: "m02-hopyo__note", text }); + +export interface LedgerOptions { + scope: HopyoScope; + /** 이 층 쓰기 권한 — 없으면 보기만 */ + canWrite: boolean; +} + +export function buildHopyoLedger(opt: LedgerOptions): HTMLElement { + const root = el("div", { + className: "m02-hopyo m02-hopyo-ledger", + attrs: { "data-hopyo-ledger": "" }, + }); + const k = ledgerKey(opt.scope); + let edits = loadEdits(k); + let rows: LedgerRow[] = []; + const search = el("input", { + className: "ui-input ui-input--sm m02-hopyo__search", + attrs: { type: "search", placeholder: "품명 · 규격 · 로직 찾기" }, + }) as HTMLInputElement; + const kind = el("select", { + className: "ui-input ui-input--sm", + children: [ + el("option", { text: "갈래 전체", attrs: { value: "" } }), + ...KINDS.map((x) => el("option", { text: x, attrs: { value: x } })), + ], + }) as HTMLSelectElement; + const save = createButton({ label: "저장", variant: "filled", size: "sm" }); + const drop = createButton({ label: "고친 것 버림", variant: "ghost", size: "sm" }); + const body = el("tbody"); + const keep = (): void => { + saveEdits(k, edits); + paint(); + }; + const edit = (r: LedgerView, next: Partial): void => { + edits[r.열쇠] = entryOf({ ...entryOf(r), ...next }); + keep(); + }; + + function pairCell(r: LedgerView, mine: boolean): HTMLElement { + const pairs: HTMLElement[] = r.짝.map((p, at) => { + const parts: HTMLElement[] = [el("span", { text: pairText(p) })]; + if (mine) { + const times = el("input", { + className: "ui-input ui-input--sm m02-hopyo__times", + attrs: { type: "number", step: "any", min: "0", value: String(p.배), title: "배" }, + }) as HTMLInputElement; + times.addEventListener("change", () => + edit(r, { 짝: r.짝.map((x, i) => (i === at ? { ...x, 배: Number(times.value) } : x)) }), + ); + parts.push(times); + if (p.로직) + parts.push( + createButton({ + label: "옵션", + variant: "ghost", + size: "sm", + onClick: () => + void fetchLogicInputs([p.로직!]).then( + (got) => + openLaborInput({ + title: p.로직!, + logic: got.로직[0] ?? null, + values: p.입력값 as never, + memo: null, + readOnly: false, + done: (res) => + edit(r, { + 짝: r.짝.map((x, i) => (i === at ? { ...x, 입력값: res.입력값 } : x)), + }), + }), + (error) => showToast(why(error), "error"), + ), + }), + createButton({ + label: "×", + variant: "ghost", + size: "sm", + onClick: () => edit(r, { 짝: r.짝.filter((_, i) => i !== at) }), + }), + ); + } + return el("div", { className: "m02-hopyo__part", children: parts }); + }); + if (mine && r.갈래 === "짝") + pairs.push( + createButton({ + label: "로직 더함", + variant: "ghost", + size: "sm", + onClick: () => + openLogicPicker((x) => + edit(r, { 짝: [...r.짝, { 로직: x.키, 입력값: {}, 배: 1, 몫: null }] }), + ), + }), + ); + return el("td", { className: "m02-hopyo__parts", children: pairs }); + } + + function rowOf(r: LedgerView): HTMLElement { + const mine = opt.canWrite && r.고침; + const kindCell = mine + ? (() => { + const s = el("select", { + className: "ui-input ui-input--sm", + children: KINDS.map((x) => el("option", { text: x, attrs: { value: x } })), + }) as HTMLSelectElement; + s.value = r.갈래; + s.addEventListener("change", () => + edit(r, { 갈래: s.value as LedgerEntry["갈래"], 짝: s.value === "짝" ? r.짝 : [] }), + ); + return el("td", { children: [s] }); + })() + : td(r.갈래); + const acts: HTMLElement[] = []; + if (mine) + acts.push( + createButton({ + label: "지우기", + variant: "danger", + size: "sm", + onClick: () => { + edits[r.열쇠] = null; + keep(); + }, + }), + ); + else if (opt.canWrite && !rows.some((x) => x.고침 && x.열쇠 === r.열쇠) && !edits[r.열쇠]) + acts.push( + createButton({ + label: "내 층으로 고쳐 씀", + variant: "ghost", + size: "sm", + onClick: () => { + edits[r.열쇠] = entryOf(r); + keep(); + }, + }), + ); + const when = [r.누가?.이름, r.때?.replace("T", " ").slice(0, 16), r.처음파일] + .filter(Boolean) + .join(" · "); + return el("tr", { + className: `m02-hopyo-ledger__row${r.고침 ? "" : " is-upper"}${r.고친 ? " is-edited" : ""}`, + attrs: { "data-ledger": r.열쇠 }, + children: [ + td(r.원래.품명), + td(r.원래.규격), + td(r.원래.단위), + kindCell, + pairCell(r, mine), + td(when), + td(r.고침 ? "내 층" : (LAYER_TEXT[r.층] ?? r.층)), + el("td", { className: "m02-hopyo__acts", children: acts }), + ], + }); + } + + function paint(): void { + const all = mergeRows(rows, edits, opt.scope); + const shown = filterRows(all, search.value, kind.value); + const n = Object.keys(edits).length; + save.querySelector(".ui-btn__label")!.textContent = n ? `저장 (${n})` : "저장"; + save.disabled = drop.disabled = !n; + body.replaceChildren( + ...(shown.length + ? shown.map(rowOf) + : [el("tr", { children: [el("td", { text: "장부 줄 없음", attrs: { colspan: "8" } })] })]), + ); + } + search.addEventListener("input", paint); + kind.addEventListener("change", paint); + drop.addEventListener("click", () => { + edits = {}; + keep(); + }); + save.addEventListener("click", async () => { + save.disabled = true; + try { + rows = await saveLedger(opt.scope, edits); + edits = {}; + showToast("장부 저장", "success"); + keep(); + } catch (error) { + showToast(`저장 못 함 — ${why(error)}`, "error"); + paint(); + } + }); + + async function load(): Promise { + const stop = showLoading(root); + try { + rows = await readLedger(opt.scope); + } catch (error) { + root.replaceChildren(loadingError(why(error), () => void load())); + return; + } finally { + stop(); + } + root.replaceChildren( + el("div", { + className: "m02-hopyo__bar", + children: [ + el("strong", { text: LEDGER_TITLE }), + note("엑셀 이름 → 우리 로직 + 옵션 + 배 · 확인하고 만들 때 남음 · 아래 층이 이김"), + search, + kind, + ...(opt.canWrite ? [save, drop] : []), + ], + }), + el("div", { + className: "ui-general-block__table-wrap ui-general-block__table-wrap--sticky-head", + children: [ + el("table", { + className: "ui-general-block__table m02-hopyo__table", + children: [ + el("thead", { + children: [ + el("tr", { + children: [ + "엑셀 품명", + "규격", + "단위", + "갈래", + "우리 로직 · 배 · 몫 · 옵션", + "누가 · 때 · 처음 파일", + "층", + "", + ].map((t) => el("th", { text: t })), + }), + ], + }), + body, + ], + }), + ], + }), + ); + paint(); + } + + void load(); + return root; +} diff --git a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Pick.ts b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Pick.ts index c460ce6a3..def3c122d 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Pick.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Pick.ts @@ -1,13 +1,13 @@ /* ============================================================================= * M02_MasterTemplete_Hopyo_Pick.ts - * [로직 추가] 창의 「실무 호표」 줄(PLAN 324-4) — 일위대가 문서 층 · 위 층 실무자료의 호표마다 한 줄(맞춤 n/m) · - * 고르면 서버가 그 호표를 문서 층 사전조합으로 만들고(`/hopyo-build` · 있던 것은 다시 씀) `lib.pick` 으로 - * 참조 줄(별칭 = 호표 품명) · 빠진 줄이 있으면 알림 · 파일마다 「엑셀로 보기 · 여러 호표」 줄(329 엑셀 보기 창 · - * 고른 호표마다 참조 줄). 프로젝트 층엔 없음. + * [로직 추가] 창의 「실무 호표」 줄(PLAN 324-4 · 338-3) — 일위대가 문서 층 · 위 층 실무자료의 호표마다 한 줄(제안 n/m) · + * 고름 = 문서 층에 확인된 사전조합이 있으면 그것을 바로 참조 · 없으면 확인 창 모달(`Hopyo_Check` · 늦게 불러옴) → + * [확인하고 사전조합 만들기] → `lib.pick` 참조 줄(별칭 = 호표 품명) · 확인 없이 만들지 않음 · + * 파일마다 「엑셀로 보기 · 여러 호표」 줄(329 엑셀 보기 창 · 고른 호표마다 참조 줄). 프로젝트 층엔 없음. * ========================================================================== */ import { showToast } from "@ui/ui_template_elements"; -import { buildHopyo, listHopyoPicks, type HopyoPickRow } from "./M02_MasterTemplete_Hopyo_Api"; +import { listHopyoPicks, type HopyoPickRow } from "./M02_MasterTemplete_Hopyo_Api"; import type { LibraryPick, PickExtra } from "./M02_MasterTemplete_Labor_Pick"; import { tc } from "./M02_MasterTemplete_Labor_Text"; @@ -73,21 +73,29 @@ export function hopyoExtra(row: HopyoPickRow, lib: LibraryPick): PickExtra { tag: "실무 호표", name: [row.이름, row.규격].filter(Boolean).join(" "), sub: `${row.파일} · 제${row.번}호표 · ${LAYER_TEXT[row.층] ?? row.층}`, - unit: `${row.단위} · 맞춤 ${row.맞춤[0]}/${row.맞춤[1]}`, + unit: `${row.단위} · ${row.확인 ? `확인 ${row.확인.키}` : `제안 ${row.맞춤[0]}/${row.맞춤[1]}`}`, onPick: () => void pickHopyo(row, lib), }; } -/** 고름 — 서버가 사전조합을 만들고(다시 씀) 참조 줄을 더함 */ +/** 고름 — 확인된 사전조합이면 바로 참조 · 아니면 확인 창 모달 → 만든 뒤 참조 */ export async function pickHopyo(row: HopyoPickRow, lib: LibraryPick): Promise { + const alias = row.이름.slice(0, ALIAS_MAX).trim() || null; + if (row.확인) { + showToast(`「${row.이름}」 → 확인한 사전조합 ${row.확인.키}`, "success"); + return lib.pick(row.확인, row.이름, alias); + } try { - const from = { 층: row.층, 주인: row.주인, 키: row.키, 번: row.번 }; - const got = await buildHopyo({ layer: lib.layer, owner: lib.owner }, from); - const miss = got.빠진줄.length; - const head = `「${row.이름}」 → 사전조합 ${got.참조.키}${got.다시씀 ? "(있던 것)" : ""}`; - if (miss) showToast(`${head} · 빠진 줄 ${miss} — 실무자료 화면에서 맞춤`, "warning", 6000); - else showToast(head, "success"); - lib.pick(got.참조, got.이름, row.이름.slice(0, ALIAS_MAX).trim() || null); + const check = await import("./M02_MasterTemplete_Hopyo_Check"); + check.openHopyoCheck({ + title: `확인 — ${row.파일} 제${row.번}호표 ${row.이름}`, + scope: { layer: row.층, owner: row.주인 }, + key: row.키, + no: row.번, + target: { layer: lib.layer, owner: lib.owner }, + canWrite: true, + onBuilt: (got) => lib.pick(got.참조, got.이름, alias), + }); } catch (error) { showToast(error instanceof Error ? error.message : String(error), "error"); } diff --git a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Sheet.ts b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Sheet.ts index e547de48e..ab8f851b5 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Sheet.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Sheet.ts @@ -1,15 +1,23 @@ /* ============================================================================= * M02_MasterTemplete_Hopyo_Sheet.ts - * 일위대가 실무자료 엑셀 보기 창(PLAN 329) — 큰 모달 · 왼쪽 = 올린 엑셀을 연 것처럼 읽기 전용 시트(A00 산출근거 - * 스프레드시트 · 탭 일위대가 · 단가산출) · 오른쪽 = 호표 목록(체크 · 찾기 · 맞춤 n/m) + [고른 n개 불러오기]. + * 일위대가 실무자료 엑셀 보기 창(PLAN 329 · 338-3) — 큰 모달 · 왼쪽 = 올린 엑셀을 연 것처럼 읽기 전용 시트(A00 산출근거 + * 스프레드시트 · 탭 일위대가 · 단가산출) · 오른쪽 = 호표 목록(체크 · 찾기 · 제안 n/m) + [고른 n개 확인하고 만들기]. * · 목록 누름 → 시트에서 그 호표 영역(B열 ~ 끝 열) 고름 · 칸 누름 → 그 행 호표를 목록에서 강조 · 영역 고름 - * · 불러오기 = 고른 호표마다 차례로 사전조합 만들기(`/hopyo-build`) → 결과 표(사전조합 키 · 빠진 줄 · 값 차이) · + * · ① [고른 n개 확인하고 만들기] = 고른 호표마다 서버 보기 → 펼침 표(넣을 줄 = 체크 제안 · 빠질 줄 · 셈 중 · + * [확인 창 열기]) ② [이대로 n개 만들기] = 하위 호표부터 차례로 `/hopyo-build`(몸 = 그 호표 체크 줄) — + * 펼쳐 보이고 한 번 누름 = 사용자 확인 · 결과 표(사전조합 키 · 빠진 줄 · 값 차이) · * `onImport` 가 있으면 호표마다 알림([로직 추가] 참조 줄) - * · 격자 = 서버 `/hopyo-grid/{키}` · 옛 파일(격자 없음)은 시트 자리에 까닭 글 · 목록 · 불러오기는 됨 + * · 격자 = 서버 `/hopyo-grid/{키}` · 옛 파일(격자 없음)은 시트 자리에 까닭 글 · 목록 · 확인하고 만들기는 됨 * ⚠ 부르는 쪽은 `import()` 로 엶 · 스프레드시트도 열 때 불러옴(다른 화면 번들에 안 섞이게). * ========================================================================== */ -import { createButton, el, showToast } from "@ui/ui_template_elements"; +import { + createButton, + el, + hideLoadingOverlay, + showLoadingOverlay, + showToast, +} from "@ui/ui_template_elements"; import { openModal } from "@ui/ui_template_modal"; import type { Sheet } from "../A00_Common/spreadsheet/spreadsheet_types"; import type { SpreadsheetHandle } from "../A00_Common/spreadsheet/spreadsheet"; @@ -17,13 +25,24 @@ import { buildHopyo, readHopyo, readHopyoGrid, + readHopyoOne, type HopyoBrief, type HopyoBuilt, type HopyoGrid, + type HopyoOne, type HopyoScope, type HopyoSpan, } from "./M02_MasterTemplete_Hopyo_Api"; -import { builtWord } from "./M02_MasterTemplete_Hopyo_View"; +import { + bodyOf, + draftKey, + leftOut, + loadDraft, + openHopyoCheck, + partsOf, + toTarget, +} from "./M02_MasterTemplete_Hopyo_Check"; +import { builtWord } from "./M02_MasterTemplete_Hopyo_Choose"; import "./M02_MasterTemplete_Hopyo.css"; export interface HopyoSheetOptions { @@ -31,7 +50,7 @@ export interface HopyoSheetOptions { scope: HopyoScope; key: string; 파일: string; - /** 만들 층 쓰기 권한 — 없으면 체크 · 불러오기 없음(보기만) */ + /** 만들 층 쓰기 권한 — 없으면 체크 · 만들기 없음(보기만) */ canWrite: boolean; /** 사전조합을 만들 층(없으면 `scope` · [로직 추가] = 문서 층) */ target?: HopyoScope; @@ -68,6 +87,19 @@ export function lastColumn(sheet: Pick): string { return best; } +/** 만들 차례 — 하위 호표(호표 참조 줄이 가리키는 고른 호표)부터 · 순환은 남은 차례대로 */ +export function buildOrder(ones: { 번: number; 아래: number[] }[]): number[] { + const left = new Map(ones.map((x) => [x.번, x.아래.filter((n) => n !== x.번)])); + const out: number[] = []; + while (left.size) { + const ready = [...left].find(([, kids]) => kids.every((n) => !left.has(n))); + const no = ready ? ready[0] : [...left.keys()][0]; + out.push(no); + left.delete(no); + } + return out; +} + /** 단가산출 행 → 행 전체 범위 A1(`4:9`) */ export const rowsText = (행: number | [number, number]): string => typeof 행 === "number" ? `${행}:${행}` : `${행[0]}:${행[1]}`; @@ -94,7 +126,7 @@ export function openHopyoSheet(opt: HopyoSheetOptions): void { }) as HTMLInputElement; const list = el("div", { className: "m02-hopyo-sheet__list" }); const count = note(""); - const go = createButton({ label: "고른 0개 불러오기", variant: "filled", size: "sm" }); + const go = createButton({ label: "고른 0개 확인하고 만들기", variant: "filled", size: "sm" }); go.disabled = true; const result = el("div", { className: "m02-hopyo-sheet__result" }); const right = el("div", { @@ -122,7 +154,7 @@ export function openHopyoSheet(opt: HopyoSheetOptions): void { }); const paintGo = (): void => { - go.querySelector(".ui-btn__label")!.textContent = `고른 ${checked.size}개 불러오기`; + go.querySelector(".ui-btn__label")!.textContent = `고른 ${checked.size}개 확인하고 만들기`; go.disabled = !checked.size; }; @@ -226,57 +258,147 @@ export function openHopyoSheet(opt: HopyoSheetOptions): void { } } - async function importPicked(): Promise { - const nos = [...checked].sort((a, b) => a - b); - const target = opt.target ?? opt.scope; - const rows: HTMLElement[] = []; - let ok = 0; - go.disabled = true; - for (const no of nos) { - const h = briefs.find((x) => x.번 === no); - if (!h || closed) continue; - const head = el("td", { text: `제${no}호표 ${h.이름}`.trim() }); - try { - const from = { 층: opt.scope.layer, 주인: opt.scope.owner, 키: opt.key, 번: no }; - const got = await buildHopyo(target, from); - ok += 1; - opt.onImport?.(got, h); - rows.push( - el("tr", { - attrs: { "data-built": got.참조.키 }, - children: [ - head, - el("td", { text: `${got.참조.키} · ${builtWord(got, false)}` }), - el("td", { text: String(got.빠진줄.length), className: "m02-hopyo__num" }), - el("td", { text: won(got.비교.차이), className: "m02-hopyo__num" }), - ], - }), - ); - } catch (error) { - rows.push(el("tr", { children: [head, el("td", { text: `실패 — ${why(error)}` })] })); - } - result.replaceChildren( - el("table", { - className: "ui-general-block__table m02-hopyo__table", - children: [ - el("thead", { - children: [ - el("tr", { - children: ["호표", "사전조합", "빠진 줄", "값 차이"].map((t) => - el("th", { text: t }), - ), - }), - ], - }), - el("tbody", { children: rows }), - ], + const table = (heads: string[], rows: HTMLElement[]): HTMLElement => + el("table", { + className: "ui-general-block__table m02-hopyo__table", + children: [ + el("thead", { + children: [el("tr", { children: heads.map((t) => el("th", { text: t })) })], }), - ); + el("tbody", { children: rows }), + ], + }); + + /** 호표 하나의 몸 — 확인 창 캐시 위 체크 제안 · 통째면 줄 없음 */ + function bodyFor(one: HopyoOne): Parameters[2] { + const h = one.호표; + const d = loadDraft(draftKey(opt.scope, opt.key, h.번)); + return h.통째 && (d.통째 ?? h.통째.체크) ? [] : bodyOf(h.줄, d); + } + function wholeFor(one: HopyoOne): Parameters[3] { + const h = one.호표; + const d = loadDraft(draftKey(opt.scope, opt.key, h.번)); + return h.통째 && (d.통째 ?? h.통째.체크) ? { 대상: h.통째.대상.map(toTarget) } : null; + } + + /** ① 고른 호표마다 서버 보기 → 펼침 표 */ + async function review(): Promise { + const target = opt.target ?? opt.scope; + const nos = [...checked].sort((a, b) => a - b); + go.disabled = true; + const ones: HopyoOne[] = []; + for (const no of nos) { + if (closed) return; + try { + ones.push(await readHopyoOne(opt.scope, opt.key, no, target)); + } catch (error) { + showToast(`제${no}호표 — ${why(error)}`, "error"); + } } paintGo(); - showToast(`불러옴 ${ok} / ${nos.length}`, ok === nos.length ? "success" : "warning"); + const rows = ones.map((one) => { + const h = one.호표; + const d = loadDraft(draftKey(opt.scope, opt.key, h.번)); + const out = Object.entries(leftOut(h.줄, d)); + const calc = h.셈 && !h.셈.끝 ? `셈 중 ${h.셈.한}/${h.셈.모두}` : ""; + const brief = briefs.find((x) => x.번 === h.번) ?? h; + return el("tr", { + attrs: { "data-review": String(h.번) }, + children: [ + el("td", { text: `제${h.번}호표 ${h.이름}`.trim() }), + el("td", { + text: wholeFor(one) ? "통째" : String(bodyFor(one).filter((x) => x.대상).length), + className: "m02-hopyo__num", + }), + el("td", { text: out.map(([a, b]) => `${a} ${b}`).join(" · ") }), + el("td", { text: calc }), + el("td", { + children: [ + createButton({ + label: "확인 창 열기", + variant: "ghost", + size: "sm", + onClick: () => + openHopyoCheck({ + scope: opt.scope, + key: opt.key, + no: h.번, + target, + canWrite: opt.canWrite, + onBuilt: (got) => opt.onImport?.(got, brief), + }), + }), + ], + }), + ], + }); + }); + const ready = ones.filter((one) => bodyFor(one).length || wholeFor(one)); + const all = createButton({ + label: `이대로 ${ready.length}개 만들기`, + variant: "filled", + size: "sm", + disabled: !ready.length, + onClick: () => void makeAll(ready), + }); + all.setAttribute("data-hopyo-make-all", ""); + result.replaceChildren( + table(["호표", "넣을 줄", "빠질 줄", "숫자 판정", ""], rows), + el("div", { + className: "m02-hopyo__bar", + children: [all, note("넣을 줄 = 체크 제안 · 확인 창에서 고친 것")], + }), + ); } - go.addEventListener("click", () => void importPicked()); + + /** ② 한 번 누름 = 사용자 확인 — 하위 호표부터 차례로 만들기 */ + async function makeAll(ones: HopyoOne[]): Promise { + const target = opt.target ?? opt.scope; + const by = new Map(ones.map((one) => [one.호표.번, one])); + const order = buildOrder( + ones.map((one) => { + const d = loadDraft(draftKey(opt.scope, opt.key, one.호표.번)); + const kids = one.호표.줄.flatMap((l) => + partsOf(l, d).flatMap((p) => (p.호표 !== undefined ? [p.호표] : [])), + ); + return { 번: one.호표.번, 아래: kids }; + }), + ); + const rows: HTMLElement[] = []; + let ok = 0; + showLoadingOverlay(); + try { + for (const no of order) { + const one = by.get(no)!; + const h = briefs.find((x) => x.번 === no) ?? one.호표; + const head = el("td", { text: `제${no}호표 ${h.이름}`.trim() }); + try { + const from = { 층: opt.scope.layer, 주인: opt.scope.owner, 키: opt.key, 번: no }; + const got = await buildHopyo(target, from, bodyFor(one), wholeFor(one)); + ok += 1; + opt.onImport?.(got, h); + rows.push( + el("tr", { + attrs: { "data-built": got.참조.키 }, + children: [ + head, + el("td", { text: `${got.참조.키} · ${builtWord(got)}` }), + el("td", { text: String(got.빠진줄.length), className: "m02-hopyo__num" }), + el("td", { text: won(got.비교.차이), className: "m02-hopyo__num" }), + ], + }), + ); + } catch (error) { + rows.push(el("tr", { children: [head, el("td", { text: `실패 — ${why(error)}` })] })); + } + } + } finally { + hideLoadingOverlay(); + } + result.replaceChildren(table(["호표", "사전조합", "빠진 줄", "값 차이(참고)"], rows)); + showToast(`만듦 ${ok} / ${order.length}`, ok === order.length ? "success" : "warning"); + } + go.addEventListener("click", () => void review()); void load(); } diff --git a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Side.ts b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Side.ts index 3bd583efb..ef48f51ca 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Side.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Side.ts @@ -3,11 +3,13 @@ * 좌측 「일위대가 실무자료」 컨테이너(PLAN 324-4 · 일위대가 사전조합 아래) — 층 = 구조물 조합 층(화면 층) · * [엑셀 올리기](숨은 file 입력 .xls · .xlsx · 쓰기 권한 없으면 꺼짐) + 파일 줄 → 누르면 본 화면 * (`M02_MasterTemplete_Hopyo_View`) · 고름 표시 = 본 화면이 메인 칸에 붙어 있는 동안만 · - * 올린 직후 엑셀 보기 창(329 · `M02_MasterTemplete_Hopyo_Sheet` 늦게 불러옴)을 자동으로 엶. + * 올린 직후 엑셀 보기 창(329 · `M02_MasterTemplete_Hopyo_Sheet` 늦게 불러옴)을 자동으로 엶 · + * 파일 목록 아래 [별칭 장부](338-3) → 본 칸 장부 화면(`M02_MasterTemplete_Hopyo_Ledger`). * ========================================================================== */ import { createButton, el, showToast } from "@ui/ui_template_elements"; import { listHopyo, pageScope, uploadHopyo, type HopyoFile } from "./M02_MasterTemplete_Hopyo_Api"; +import { buildHopyoLedger, LEDGER_TITLE } from "./M02_MasterTemplete_Hopyo_Ledger"; import { buildHopyoView } from "./M02_MasterTemplete_Hopyo_View"; export const HOPYO_TITLE = "일위대가 실무자료"; @@ -48,9 +50,19 @@ export function buildHopyoSection(opt: HopyoSectionOptions): HopyoSection { onClick: () => input.click(), }); upload.title = opt.canWrite ? "설계내역서 엑셀(일위대가 시트)" : "이 층 쓰기 권한 없음"; + const ledger = el("button", { + className: "m02-side__item ui-sidebar-row", + text: LEDGER_TITLE, + attrs: { + type: "button", + title: "엑셀 이름 → 우리 로직 짝 보기 · 고치기", + "data-hopyo-ledger-open": "", + }, + }); + ledger.addEventListener("click", () => void showLedger()); const body = el("div", { className: "m02-hopyo-side", - children: [el("div", { className: "m02-side__row", children: [upload, input] }), list], + children: [el("div", { className: "m02-side__row", children: [upload, input] }), list, ledger], }); let files: HopyoFile[] = []; let open: { key: string; view: HTMLElement } | null = null; @@ -75,6 +87,7 @@ export function buildHopyoSection(opt: HopyoSectionOptions): HopyoSection { }); function paintList(): void { + ledger.classList.toggle("is-active", open?.key === LEDGER_TITLE); list.replaceChildren( ...(files.length ? files.map((f) => { @@ -107,6 +120,16 @@ export function buildHopyoSection(opt: HopyoSectionOptions): HopyoSection { paintList(); } + async function showLedger(): Promise { + if (open?.key === LEDGER_TITLE && open.view.isConnected) return; + if (!(await opt.confirmLeave())) return; + const view = buildHopyoLedger({ scope, canWrite: opt.canWrite }); + opt.show(`${HOPYO_TITLE} — ${LEDGER_TITLE}`, view); + opt.onOpen(); + open = { key: LEDGER_TITLE, view }; + paintList(); + } + async function refresh(): Promise { const mine = ++seq; try { diff --git a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_View.ts b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_View.ts index 4af98e977..b7f58f5e4 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_View.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_View.ts @@ -1,58 +1,31 @@ /* ============================================================================= * M02_MasterTemplete_Hopyo_View.ts - * 일위대가 실무자료 본 화면(PLAN 324-4) — 파일 하나: 파일 · 시트 · 호표 수 · 검산 · 맞춤 n/m · 호표 표 + 찾기 · - * [지우기] / 호표 하나: 줄 표(갈래 · 품명 · 규격 · 수량 · 단위 · 엑셀 단가 · 맞춤 · 대상 · 입력값 · 환산 · - * [고르기] [확정] [빼기]) · [같음 모두 확정] · 빠진 줄 · [사전조합 만들기] → 값 비교 · 다시 씀이면 [새로 만들기] · - * [엑셀로 보기] · [엑셀에서 보기](329 엑셀 보기 창 · 늦게 불러옴 · 호표 화면은 그 호표 고른 채) · - * 고르기 창 후보 근거 꼬리표 · 엑셀 근거 줄 + [엑셀에서 보기](단가산출 그 행) · [근거 맞음 모두 확정](330). + * 일위대가 실무자료 본 화면(PLAN 324-4 · 338-3) — 파일 하나: 파일 · 시트 · 호표 수 · 검산 · 제안 n/m · 호표 표 + 찾기 · + * [지우기] · [엑셀로 보기](329 엑셀 보기 창 · 늦게 불러옴) / 호표 하나 = 확인 창(`M02_MasterTemplete_Hopyo_Check` · + * 체크한 줄만 [확인하고 사전조합 만들기]) — 즉시 확정 · 바로 만들기 단추 없음. * 맞춤 · 만들기는 서버(`M02_Hopyo_Match` · `M02_Hopyo_Build`) · 쓰기 단추는 층 쓰기 권한만. * ========================================================================== */ import { createButton, el, showConfirmDialog, showToast } from "@ui/ui_template_elements"; import { loadingError, showLoading } from "@ui/ui_template_loading"; -import { openModal } from "@ui/ui_template_modal"; import { - buildHopyo, deleteHopyo, - guessHopyo, readHopyo, - readHopyoOne, - saveHopyoMemory, - type HopyoBasisHit, - type HopyoBuilt, - type HopyoGuess, - type HopyoLine, - type HopyoLineBasis, - type HopyoMemory, - type HopyoOne, type HopyoScope, - type HopyoTarget, type HopyoView, } from "./M02_MasterTemplete_Hopyo_Api"; +import { buildHopyoCheck } from "./M02_MasterTemplete_Hopyo_Check"; import type { HopyoSheetOptions } from "./M02_MasterTemplete_Hopyo_Sheet"; -import { openLogicPicker } from "./M02_MasterTemplete_Labor_Pick"; -import { pickMaterial } from "./M02_Material_Pick"; import "./M02_MasterTemplete_Hopyo.css"; export interface HopyoViewOptions { scope: HopyoScope; - /** 층 쓰기 권한 — 없으면 확정 · 만들기 · 지우기 단추 없음 */ + /** 층 쓰기 권한 — 없으면 체크 · 만들기 · 지우기 단추 없음 */ canWrite: boolean; /** 파일을 지웠음 — 좌측 목록 다시 */ onDeleted: () => void; } -const STATE: Record = { - 기억: "기억", - 같음: "같음", - 후보: "후보", - 없음: "없음", - 빼기: "뺌", - 호표: "호표", - "%": "% 줄", -}; -const MATERIAL = new Set(["자재", "별도계상"]); - const why = (error: unknown): string => (error instanceof Error ? error.message : String(error)); const won = (v: number | null | undefined): string => v === null || v === undefined ? "" : v.toLocaleString("ko-KR"); @@ -61,12 +34,12 @@ const fail = (error: unknown): void => showToast(`실패 — ${why(error)}`, "er const openSheet = (o: HopyoSheetOptions): void => void import("./M02_MasterTemplete_Hopyo_Sheet").then((m) => m.openHopyoSheet(o), fail); -const grid = (heads: string[], body: HTMLElement, cls = ""): HTMLElement => +const grid = (heads: string[], body: HTMLElement): HTMLElement => el("div", { className: "ui-general-block__table-wrap ui-general-block__table-wrap--sticky-head", children: [ el("table", { - className: `ui-general-block__table m02-hopyo__table ${cls}`.trim(), + className: "ui-general-block__table m02-hopyo__table", children: [ el("thead", { children: [el("tr", { children: heads.map((t) => el("th", { text: t })) })], @@ -76,87 +49,10 @@ const grid = (heads: string[], body: HTMLElement, cls = ""): HTMLElement => }), ], }); -const table = (heads: string[], rows: HTMLElement[]): HTMLElement => - grid(heads, el("tbody", { children: rows })); const td = (text: string, cls = ""): HTMLElement => el("td", { text, className: cls, attrs: { title: text } }); const note = (text: string): HTMLElement => el("span", { className: "m02-hopyo__note", text }); -/** 대상 글 — 「이름 [단위]」 · 자재는 규격도 · 호표 = 「제N호표」 */ -export const targetText = (t: HopyoTarget | null | undefined): string => { - if (!t) return ""; - if (t.호표 !== undefined) return `제${t.호표}호표 ${t.단위 ? `[${t.단위}]` : ""}`.trim(); - const spec = t.규격 ? ` ${t.규격}` : ""; - return `${t.이름 ?? t.로직 ?? t.자재 ?? ""}${spec}${t.단위 ? ` [${t.단위}]` : ""}`; -}; - -/** 입력값 글 — 「이름=값」 · 못 읽은 입력 표시 */ -export const inputsText = (values: Record, unread: string[]): string => - [ - Object.entries(values) - .map(([k, v]) => `${k}=${String(v)}`) - .join(", "), - unread.length ? `⚠ 못 읽음: ${unread.join(", ")}` : "", - ] - .filter(Boolean) - .join(" · "); - -/** 만들기 결과 낱말 — [새로 만들기] = 「새로 만들어 덮음」 · 있던 것 = 「다시 씀」 · 그 밖 「만듦」 */ -export const builtWord = (got: Pick, fresh: boolean): string => - fresh ? "새로 만들어 덮음" : got.다시씀 ? "다시 씀" : "만듦"; - -/** 확정 한 줄 몸 — 대상 · 입력값 · 환산(0 넘는 수) · 없으면 null */ -export function memoryOf( - target: HopyoTarget | null, - values: Record, - times: number | null, -): HopyoMemory { - if (!target || times === null || !(times > 0)) return null; - if (target.로직) return { 로직: target.로직, 입력값: values, 환산: times }; - if (target.자재) return { 자재: target.자재, 입력값: values, 환산: times }; - return null; -} - -/** 근거 맞음(330 [근거 맞음 모두 확정]) — 조항 같음(앞 마디 아님) · 단위 맞음 · 직종 겹침 - * (줄 근거 직종이 보통인부뿐이거나 없으면 — 기계 등 — 직종은 안 봄) */ -export const basisOk = (b: HopyoBasisHit | undefined, line?: HopyoLineBasis): boolean => - !!b && - b.까닭.startsWith("조항 같음") && - b.단위 && - (b.직종.length > 0 || !(line?.직종 ?? []).some((x) => x !== "보통인부")); - -/** 후보 근거 꼬리표 — 「조항 건설 공통 3-2-1 · 직종 특별인부 · 단위 같음」 · 근거 없으면 "" */ -export const basisTag = (b: HopyoBasisHit | undefined): string => - b - ? [ - b.조항.length ? `조항 ${b.조항.join(", ")}` : "", - b.직종.length ? `직종 ${b.직종.join(", ")}` : "", - b.단위 ? "단위 같음" : "", - ] - .filter(Boolean) - .join(" · ") - : ""; - -/** 엑셀 근거 줄 — 「출처 건설표준품셈 3-2-1 · 직종 특별인부 · 단가산출 93–101행」 · 없으면 "" */ -export const sourceText = (b: HopyoLineBasis | undefined): string => - b - ? [ - b.출처.length ? `출처 ${b.출처.map((x) => x.join(" ").trim()).join(", ")}` : "", - b.직종.length ? `직종 ${b.직종.join(", ")}` : "", - `단가산출 ${b.행[0] === b.행[1] ? b.행[0] : `${b.행[0]}–${b.행[1]}`}행`, - ] - .filter(Boolean) - .join(" · ") - : ""; - -/** [근거 맞음 모두 확정] 대상 — 「후보」 줄의 지금 대상이 근거 맞으면 그것 · 아니면 근거 맞는 첫 후보 · 없으면 null */ -export function basisPick(line: HopyoLine): HopyoTarget | null { - const m = line.맞춤; - if (m.상태 !== "후보") return null; - if (m.대상 && basisOk(m.대상.근거, line.근거)) return m.대상; - return (m.후보 ?? []).find((c) => basisOk(c.근거, line.근거)) ?? null; -} - export function buildHopyoView(key: string, opt: HopyoViewOptions): HTMLElement { const view = el("div", { className: "m02-hopyo", attrs: { "data-hopyo-view": key } }); let nav = 0; // 늦게 온 응답 버림 @@ -223,7 +119,7 @@ export function buildHopyoView(key: string, opt: HopyoViewOptions): HTMLElement note(`시트 ${file.시트}`), note(`호표 ${file.호표수}`), note(wrong.length ? `검산 어긋남 ${wrong.length}` : "검산 맞음"), - note(`맞춤 ${file.맞춤[0]} / ${file.맞춤[1]}`), + note(`제안 ${file.맞춤[0]} / 줄 ${file.맞춤[1]}`), createButton({ label: "엑셀로 보기", size: "sm", @@ -235,7 +131,7 @@ export function buildHopyoView(key: string, opt: HopyoViewOptions): HTMLElement }), ...(wrong.length ? [el("p", { className: "m02-hopyo__warn", text: wrong.join("\n") })] : []), search, - grid(["호표", "품명", "규격", "단위", "엑셀 단가", "맞춤"], body), + grid(["호표", "품명", "규격", "단위", "엑셀 단가", "제안 / 줄"], body), ); } @@ -245,7 +141,7 @@ export function buildHopyoView(key: string, opt: HopyoViewOptions): HTMLElement variant: "danger", size: "sm", onClick: async () => { - const ask = `실무자료 「${file.파일}」을 지움 — 만든 사전조합 · 맞춤 기억은 그대로`; + const ask = `실무자료 「${file.파일}」을 지움 — 만든 사전조합 · 별칭 장부는 그대로`; if (!(await showConfirmDialog(ask, "지우기"))) return; try { await deleteHopyo(opt.scope, key); @@ -257,358 +153,20 @@ export function buildHopyoView(key: string, opt: HopyoViewOptions): HTMLElement }, }); - /* ── 호표 하나 — 줄 맞춤 ────────────────────────────────────── */ - async function drawOne(no: number, built?: HopyoBuilt & { 새로: boolean }): Promise { - const got = await load( - () => readHopyoOne(opt.scope, key, no), - () => void drawOne(no), - ); - if (!got) return; - const h = got.호표; - const picked = new Map(); // 고른 대상(확정 전) - const back = createButton({ label: "← 호표 목록", variant: "ghost", size: "sm" }); - back.addEventListener("click", () => void drawFile()); - const rows = el("tbody"); - const paintRows = (): void => { - rows.replaceChildren(...h.줄.map((line) => lineRow(got, line, picked, paintRows))); - }; - paintRows(); - const same = h.줄.filter((x) => x.맞춤.상태 === "같음" && x.맞춤.환산?.배 != null); - const tools: HTMLElement[] = []; - if (opt.canWrite) { - const all = createButton({ label: `같음 모두 확정 (${same.length})`, size: "sm" }); - all.disabled = !same.length; - all.addEventListener("click", () => void confirmAll(no, same)); - tools.push(all); - // 근거(330) — 옛 파일 · 근거 없는 호표면 단추 없음 - if (h.줄.some((x) => x.근거 || x.맞춤.후보?.some((c) => c.근거))) { - const fit = h.줄.filter((x) => basisPick(x)); - const ok = createButton({ label: `근거 맞음 모두 확정 (${fit.length})`, size: "sm" }); - ok.title = "조항 · 직종 · 단위가 모두 맞는 후보만"; - ok.disabled = !fit.length; - ok.addEventListener("click", () => void confirmBasis(no, fit)); - tools.push(ok); - } - const make = createButton({ label: "사전조합 만들기", variant: "filled", size: "sm" }); - make.addEventListener("click", () => void build(no, false)); - tools.push(make); - } - const missing = h.줄.filter((x) => !x.맞춤.들어감); + /* ── 호표 하나 — 확인 창 ────────────────────────────────────── */ + function drawOne(no: number): void { + nav += 1; // 파일 표 늦은 응답 버림 view.replaceChildren( - el("div", { - className: "m02-hopyo__bar", - children: [ - back, - el("strong", { text: `제${h.번}호표 ${h.이름}` }), - note(h.규격), - note(h.단위), - note(`엑셀 단가 ${won(h.금액.계)}`), - note(`맞춤 ${h.맞춤[0]} / ${h.맞춤[1]}`), - createButton({ - label: "엑셀에서 보기", - size: "sm", - onClick: () => - openSheet({ - scope: opt.scope, - key, - 파일: got.파일, - canWrite: opt.canWrite, - 호표: no, - }), - }), - ...tools, - ], + buildHopyoCheck({ + scope: opt.scope, + key, + no, + canWrite: opt.canWrite, + onBack: () => void drawFile(), }), - h.줄.length - ? grid( - [ - "갈래", - "품명", - "규격", - "수량", - "단위", - "엑셀 단가", - "맞춤", - "대상", - "입력값", - "환산", - "", - ], - rows, - "m02-hopyo__lines", - ) - : el("p", { className: "m02-hopyo__note", text: "구성 줄 없음" }), - ...(missing.length ? [missingBlock(missing)] : []), - ...(built ? [builtBlock(no, built)] : []), ); } - function lineRow( - got: HopyoOne, - line: HopyoLine, - picked: Map, - repaint: () => void, - ): HTMLElement { - const m = line.맞춤; - const mine = picked.get(line.차례); - const target = mine?.대상 ?? m.대상; - const values = mine?.입력값 ?? m.입력값; - const conv = mine?.환산 ?? m.환산; - const fixed = ["%", "호표", "빼기"].includes(m.상태); - const times = el("input", { - className: "ui-input ui-input--sm m02-hopyo__times", - attrs: { - type: "number", - step: "any", - min: "0", - value: conv?.배 != null ? String(conv.배) : "", - }, - }) as HTMLInputElement; - times.disabled = !opt.canWrite || fixed || !target; - const state = mine ? "고름" : (STATE[m.상태] ?? m.상태); - const actions: HTMLElement[] = []; - if (opt.canWrite && m.상태 === "빼기") { - actions.push( - createButton({ - label: "되살림", - variant: "ghost", - size: "sm", - onClick: () => void remember(got.호표.번, { [m.열쇠]: null }), - }), - ); - } else if (opt.canWrite && !["%", "호표"].includes(m.상태)) { - actions.push( - createButton({ - label: "고르기", - variant: "ghost", - size: "sm", - onClick: () => - choose(line, got.파일, (t) => - guessHopyo(opt.scope, key, got.호표.번, line.차례, t).then( - (g) => (picked.set(line.차례, g), repaint()), - fail, - ), - ), - }), - createButton({ - label: "확정", - size: "sm", - disabled: !target, - onClick: () => { - const body = memoryOf(target, values, times.value === "" ? null : Number(times.value)); - if (!body) return showToast("대상과 0 넘는 환산을 넣어야 확정", "error"); - void remember(got.호표.번, { [m.열쇠]: body }); - }, - }), - createButton({ - label: "빼기", - variant: "ghost", - size: "sm", - onClick: () => void remember(got.호표.번, { [m.열쇠]: { 빼기: true } }), - }), - ); - } - return el("tr", { - className: `m02-hopyo__line is-${m.들어감 ? "in" : "out"}`, - attrs: { "data-line": String(line.차례), "data-state": m.상태 }, - children: [ - td(line.갈래), - td(line.이름), - td(line.규격), - td(line.수량 === null ? "" : String(line.수량), "m02-hopyo__num"), - td(line.단위), - td(won(line.단가.계), "m02-hopyo__num"), - el("td", { - children: [el("span", { className: `m02-hopyo__state is-${m.상태}`, text: state })], - }), - td(targetText(target)), - td(inputsText(values, mine?.못읽음 ?? m.못읽음)), - el("td", { - children: [times, note(conv?.갈래 ? ` ${conv.갈래}` : "")], - }), - el("td", { className: "m02-hopyo__acts", children: actions }), - ], - }); - } - - /** 대상 고르기 — 엑셀 근거 줄 · 후보 5(근거 꼬리표) + 다른 로직 · 자재 찾기 */ - function choose(line: HopyoLine, 파일: string, done: (t: HopyoTarget) => void): void { - const material = MATERIAL.has(line.갈래); - openModal({ - title: `고르기 — ${line.이름} ${line.규격}`.trim(), - closeLabel: "닫기", - dialogClass: "m02-modal m02-hopyo__choose", - mount: (body, close) => { - const pick = (t: HopyoTarget): void => { - close(); - done(t); - }; - const list = (line.맞춤.후보 ?? []).map((c) => { - const button = createButton({ - label: `${targetText(c)} · ${c.로직 ?? c.자재 ?? ""}`, - variant: "ghost", - size: "sm", - onClick: () => pick(c), - }); - const tag = basisTag(c.근거); - if (tag && c.근거) - button.append( - el("span", { - className: `m02-hopyo__basis${basisOk(c.근거, line.근거) ? " is-ok" : ""}`, - text: tag, - attrs: { title: `${c.근거.까닭} · 점수 ${c.근거.점수}` }, - }), - ); - return button; - }); - const basis = line.근거; - const source = basis - ? [ - el("div", { - className: "m02-hopyo__bar m02-hopyo__source", - children: [ - note(`엑셀 근거 — ${sourceText(basis)}`), - createButton({ - label: "엑셀에서 보기", - variant: "ghost", - size: "sm", - onClick: () => - openSheet({ - scope: opt.scope, - key, - 파일, - canWrite: opt.canWrite, - 단산행: basis.행, - }), - }), - ], - }), - ] - : []; - const more = createButton({ - label: material ? "다른 자재 찾기…" : "다른 로직 찾기…", - size: "sm", - onClick: () => { - close(); - if (material) pickMaterial((x) => done({ 자재: x.코드 })); - else openLogicPicker((x) => done({ 로직: x.키 })); - }, - }); - body.append( - ...source, - el("p", { className: "m02-hopyo__note", text: list.length ? "후보" : "가까운 것 없음" }), - el("div", { className: "m02-hopyo__cands", children: list }), - more, - ); - }, - }); - } - - async function remember(no: number, 줄: Record): Promise { - try { - await saveHopyoMemory(opt.scope, 줄); - await drawOne(no); - } catch (error) { - fail(error); - } - } - - async function confirmAll(no: number, lines: HopyoLine[]): Promise { - const body: Record = {}; - for (const x of lines) { - const one = memoryOf(x.맞춤.대상, x.맞춤.입력값, x.맞춤.환산?.배 ?? null); - if (one) body[x.맞춤.열쇠] = one; - } - await remember(no, body); - } - - /** 근거 맞는 후보로 확정 — 지금 대상이 아니면 입력값 · 환산을 서버에서 다시 읽음 */ - async function confirmBasis(no: number, lines: HopyoLine[]): Promise { - const body: Record = {}; - for (const x of lines) { - const c = basisPick(x); - const m = x.맞춤; - if (!c) continue; - const g = - c === m.대상 - ? { 대상: c, 입력값: m.입력값, 환산: m.환산 } - : await guessHopyo(opt.scope, key, no, x.차례, c).catch(() => null); - const one = g && memoryOf(g.대상, g.입력값, g.환산?.배 ?? null); - if (one) body[m.열쇠] = one; - } - if (!Object.keys(body).length) return showToast("확정할 줄 없음 — 환산을 못 읽음", "warning"); - await remember(no, body); - } - - async function build(no: number, fresh: boolean): Promise { - try { - const from = { 층: opt.scope.layer, 주인: opt.scope.owner, 키: key, 번: no }; - const got = await buildHopyo(opt.scope, from, fresh); - const word = builtWord(got, fresh); - const miss = got.빠진줄.length; - showToast(miss ? `${word} — 빠진 줄 ${miss}` : word, miss ? "warning" : "success"); - await drawOne(no, { ...got, 새로: fresh }); - } catch (error) { - fail(error); - } - } - - const missingBlock = (lines: HopyoLine[]): HTMLElement => - el("div", { - className: "m02-hopyo__block", - children: [ - el("h4", { text: `빠진 줄 ${lines.length} — 사전조합에 안 들어감` }), - table( - ["품명", "규격", "수량", "단위", "엑셀 금액", "까닭"], - lines.map((x) => - el("tr", { - children: [ - td(x.이름), - td(x.규격), - td(x.수량 === null ? "" : String(x.수량), "m02-hopyo__num"), - td(x.단위), - td(won(x.금액.계), "m02-hopyo__num"), - td(STATE[x.맞춤.상태] ?? x.맞춤.상태), - ], - }), - ), - ), - ], - }); - - function builtBlock(no: number, got: HopyoBuilt & { 새로: boolean }): HTMLElement { - const c = got.비교; - const again = got.다시씀 - ? [ - note( - got.새로 - ? `사전조합 ${got.참조.키} 를 새로 만들어 덮음` - : `있던 사전조합 ${got.참조.키} 를 다시 씀`, - ), - createButton({ - label: "새로 만들기", - variant: "danger", - size: "sm", - onClick: async () => { - const ask = `사전조합 ${got.참조.키} 를 지금 맞춤으로 다시 만들어 덮음`; - if (await showConfirmDialog(ask, "새로 만들기")) await build(no, true); - }, - }), - ] - : [note(`새 사전조합 ${got.참조.키}`)]; - return el("div", { - className: "m02-hopyo__block m02-hopyo__built", - attrs: { "data-built": got.참조.키 }, - children: [ - el("div", { className: "m02-hopyo__bar", children: again }), - note( - `값 비교 — 엑셀 ${won(c.엑셀)} · 우리 ${won(c.우리 === null ? null : Math.round(c.우리))}` + - ` · 차이 ${won(c.차이)}${c.까닭 ? ` · ${c.까닭}` : ""}`, - ), - ], - }); - } - void drawFile(); return view; } diff --git a/M02_MasterTemplete/M02_MasterTemplete_Labor_Pick.ts b/M02_MasterTemplete/M02_MasterTemplete_Labor_Pick.ts index 1ac24376e..bccd33756 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Labor_Pick.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Labor_Pick.ts @@ -80,7 +80,12 @@ const LAYER_TEXT = { personal: "Lib_Personal", } as const; -export function openLogicPicker(done: (logic: LogicSummary) => void, lib?: LibraryPick): void { +/** `pre.query` = 찾기 칸에 미리 넣을 글(338 실무자료 고르기 창 · 인용 조항 번호 · 품명 낱말) */ +export function openLogicPicker( + done: (logic: LogicSummary) => void, + lib?: LibraryPick, + pre?: { query?: string }, +): void { const subs: SubBrief[] = []; let classes: ReturnType = []; // 사전조합 새 분류(149-2) — 서고 줄이 있을 때만 let all: LogicSummary[] = []; @@ -112,6 +117,7 @@ export function openLogicPicker(done: (logic: LogicSummary) => void, lib?: Libra filters: [...(kindSel ? [kindSel.root] : []), sub.root, detail.root], extra: [layerBar], }); + if (pre?.query) finder.search.value = pre.query; const find = (): void => { const q = finder.search.value.trim().toLowerCase(); diff --git a/resources/tester/helper_m02_hopyo_check.cjs b/resources/tester/helper_m02_hopyo_check.cjs new file mode 100644 index 000000000..0af39f459 --- /dev/null +++ b/resources/tester/helper_m02_hopyo_check.cjs @@ -0,0 +1,184 @@ +/* M02 일위대가 실무자료 확인 창(338-3) 헬퍼 — `Hopyo_Check` · `Hopyo_Choose` · `Hopyo_Ledger` · `Hopyo_Sheet` + * 도우미와 `Hopyo_Api` 길을 트랜스파일해 Node 로 돌림 · fetch 대역이 부른 길 · 몸을 모음 · sessionStorage 대역 · + * stdin = {lines, rows, ledger} · stdout = {check, choose, ledger, sheet, calls, pick} JSON. */ +const fs = require("fs"); +const path = require("path"); +const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript")); + +const ROOT = path.join(__dirname, "..", ".."); +const STUBS = { "@config/config_frontend": { API_BASE_URL: "/api" } }; +// DOM · 모듈 메타를 쓰는 창은 대역(도우미 · 길만 시험) +const STUB_FILES = [ + "M02_MasterTemplete_Labor_Pick", + "M02_Material_Pick", + "M02_MasterTemplete_Labor_Api", + "M02_MasterTemplete_Labor_Input", + "M01_MasterData_UI_LogicLab_Source", +]; +const opened = []; +const stub = new Proxy( + {}, + { + get: (_t, k) => + k === "currentLanguageIndex" + ? () => 0 + : k === "openModal" + ? (o) => opened.push(o.title) + : () => {}, + }, +); +const cache = new Map(); +function loadTs(file) { + const full = path.resolve(file); + if (cache.has(full)) return cache.get(full); + const js = ts.transpileModule(fs.readFileSync(full, "utf8"), { + compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 }, + }).outputText; + const box = { exports: {} }; + cache.set(full, box.exports); + const localRequire = (request) => { + if (STUBS[request]) return STUBS[request]; + if (STUB_FILES.some((name) => request.endsWith(name))) return stub; + if (request.startsWith(".") && !request.endsWith(".css")) + return loadTs(`${path.resolve(path.dirname(full), request)}.ts`); + return stub; + }; + new Function("exports", "module", "require", js)(box.exports, box, localRequire); + return box.exports; +} + +const store = new Map(); +global.sessionStorage = { + getItem: (k) => (store.has(k) ? store.get(k) : null), + setItem: (k, v) => store.set(k, String(v)), + removeItem: (k) => store.delete(k), +}; + +(async () => { + const input = JSON.parse(fs.readFileSync(0, "utf8")); + const calls = []; + global.fetch = async (url, init = {}) => { + calls.push({ + url, + method: init.method ?? "GET", + body: init.body ? JSON.parse(init.body) : null, + }); + const body = url.includes("/hopyo-picks") || url.includes("/hopyo-ledger") ? { 줄: [] } : {}; + return { status: 200, ok: true, json: async () => body }; + }; + const M02 = path.join(ROOT, "M02_MasterTemplete"); + const check = loadTs(path.join(M02, "M02_MasterTemplete_Hopyo_Check.ts")); + const choose = loadTs(path.join(M02, "M02_MasterTemplete_Hopyo_Choose.ts")); + const ledger = loadTs(path.join(M02, "M02_MasterTemplete_Hopyo_Ledger.ts")); + const sheet = loadTs(path.join(M02, "M02_MasterTemplete_Hopyo_Sheet.ts")); + const api = loadTs(path.join(M02, "M02_MasterTemplete_Hopyo_Api.ts")); + const pick = loadTs(path.join(M02, "M02_MasterTemplete_Hopyo_Pick.ts")); + const { lines } = input; + const scope = { layer: "personal", owner: null }; + const key = check.draftKey(scope, "HP-000001", 3); + + // 서버 제안 그대로 → 사용자가 만짐(체크 끔 · 뺌 · 고른 대상) → 캐시 다시 읽음 + const fresh = check.loadDraft(key); + const suggested = check.bodyOf(lines, fresh); + const d = check.loadDraft(key); + d.체크[1] = false; // 맞음 줄 체크 끔 + d.뺌[3] = "우리에 없음"; + d.체크[3] = true; + d.대상[4] = [{ 로직: "GF000211", 이름: "고임돌", 입력값: { 유형: "보통" }, 배: 0.1, 몫: 0.1 }]; + d.체크[4] = true; + check.saveDraft(key, d); + const again = check.loadDraft(key); + const ask = check.newest(); + const a = ask(1); + const b = ask(1); + const c = ask(2); + const out = { + check: { + key, + suggested, + edited: check.bodyOf(lines, again), + leftOut: check.leftOut(lines, again), + checkable: lines.map((l) => check.checkable(l, fresh)), + sure: check.sureLines(lines, fresh).map((l) => l.차례), + compare: check.compareText(lines[1].맞춤.품), + toTarget: [ + check.toTarget({ 호표: 46, 이름: "고화매트", 입력값: {}, 배: 2.3 }), + check.toTarget({ 자재: "MT1", 이름: "레미콘", 입력값: {}, 배: 1, 몫: null }), + ], + newest: [a(), b(), c()], + dropped: (check.dropDraft(key), store.has(key)), + }, + choose: { + hint: [ + choose.searchHint({ + 이름: "레미콘타설(장비사용)", + 근거: { 출처: [["건설품셈", "6-1-1"]], 직종: [], 행: [1, 1] }, + }), + choose.searchHint({ 이름: "(소)고임돌 채집 및 운반" }), + ], + picked: choose.pickedTargets([ + { c: { 로직: "GC000206" }, 몫: null }, + { c: { 로직: "GF000211" }, 몫: 0.1 }, + ]), + inputs: choose.inputsText({ 소량감률: 50, 지역: "서울" }, ["측압"], { + 소량감률: "블록", + 지역: "자동값", + }), + word: [choose.builtWord({ 다시씀: false }), choose.builtWord({ 다시씀: true })], + qty: choose.qtyText(lines[1].맞춤.품.엑셀), + target: choose.targetText({ 호표: 46, 단위: "㎡" }), + }, + ledger: (() => { + const rows = input.ledger; + const edits = { + "가|b|m3": null, // 지움 + "나||m3": { 원래: { 품명: "나", 규격: "", 단위: "m3" }, 갈래: "안 씀", 짝: [] }, + "다||개": ledger.entryOf(rows[2]), // 위 층 → 내 층 + }; + const merged = ledger.mergeRows(rows, edits, { layer: "personal", owner: "7/42" }); + return { + key: ledger.ledgerKey({ layer: "company", owner: "7" }), + merged: merged.map((r) => [r.열쇠, r.층, r.고침, !!r.고친, r.갈래]), + filter: ledger.filterRows(merged, "MT1", "").map((r) => r.열쇠), + kind: ledger.filterRows(merged, "", "안 씀").map((r) => r.열쇠), + pair: ledger.pairText({ 로직: "GC000198", 입력값: { 소량감률: 50 }, 배: 1, 몫: 0.1 }), + entry: ledger.entryOf({ ...rows[0], 갈래: "우리에 없음" }), + }; + })(), + sheet: { + order: sheet.buildOrder([ + { 번: 45, 아래: [46] }, + { 번: 46, 아래: [] }, + { 번: 3, 아래: [] }, + { 번: 7, 아래: [8] }, + { 번: 8, 아래: [7] }, + ]), + }, + }; + // 길 — 만들기(줄 · 통째) · 호표 하나(만들 층) · 미리 셈 · 장부 + const from = { 층: "system", 주인: null, 키: "HP-000001", 번: 3 }; + const target = { layer: "company", owner: "7" }; + await api.buildHopyo(target, from, suggested); + await api.buildHopyo(target, from, [], { 대상: [{ 로직: "GF000225", 배: 1 }] }); + await api.readHopyoOne({ layer: "system", owner: null }, "HP-000001", 3, target); + await api.guessHopyo(scope, "HP-000001", 3, 4, [ + { 로직: "GF000211", 입력값: {}, 배: 0.1, 몫: 0.1 }, + ]); + await api.readLedger(target); + await api.saveLedger(target, { "가|b|m3": null }); + out.calls = calls.splice(0); + // [로직 추가] 실무 호표 — 확인된 것 = 바로 참조 · 아닌 것 = 확인 창(만들기 길 안 부름) + const picked = []; + const lib = { + layer: "company", + owner: "7", + pick: (ref, name, alias) => picked.push({ ref, name, alias }), + }; + for (const row of input.rows) await pick.pickHopyo(row, lib); + await new Promise((r) => setTimeout(r, 0)); + out.pick = { picked, calls: calls.splice(0), opened }; + process.stdout.write(JSON.stringify(out)); +})().catch((error) => { + process.stderr.write(String(error?.stack ?? error)); + process.exit(1); +}); diff --git a/resources/tester/helper_m02_hopyo_pick.cjs b/resources/tester/helper_m02_hopyo_pick.cjs index fb5daaa03..3434f4991 100644 --- a/resources/tester/helper_m02_hopyo_pick.cjs +++ b/resources/tester/helper_m02_hopyo_pick.cjs @@ -1,5 +1,5 @@ -/* M02 일위대가 실무자료(324-4) 헬퍼 — `M02_MasterTemplete_Hopyo_Pick.ts`(고르기 창 「실무 호표」 덧줄) · - * `M02_MasterTemplete_Hopyo_View.ts` 글 도우미 · `M02_MasterTemplete_Hopyo_Sheet.ts` 엑셀 보기 창 도우미(329 · 행 → 호표)를 +/* M02 일위대가 실무자료(324-4 · 338-3) 헬퍼 — `M02_MasterTemplete_Hopyo_Pick.ts`(고르기 창 「실무 호표」 덧줄) · + * `M02_MasterTemplete_Hopyo_Choose.ts` 글 도우미 · `M02_MasterTemplete_Hopyo_Sheet.ts` 엑셀 보기 창 도우미(329 · 행 → 호표)를 * 트랜스파일해 Node 로 돌림 · fetch 대역이 부른 길 · 몸을 모음 · * stdin = {rows, built} · stdout = {calls, kinds, rows, picked, project, view, sheet} JSON. */ const fs = require("fs"); @@ -8,8 +8,14 @@ const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "t const ROOT = path.join(__dirname, "..", ".."); const STUBS = { "@config/config_frontend": { API_BASE_URL: "/api" } }; -// 본 화면이 부르는 고르기 창 · 자재 창은 DOM · 모듈 메타를 씀 — 대역(글 도우미만 시험) -const STUB_FILES = ["M02_MasterTemplete_Labor_Pick", "M02_Material_Pick"]; +// 고르기 창 · 자재 창 · 입력 창 · 원문 창은 DOM · 모듈 메타를 씀 — 대역(글 도우미만 시험) +const STUB_FILES = [ + "M02_MasterTemplete_Labor_Pick", + "M02_Material_Pick", + "M02_MasterTemplete_Labor_Api", + "M02_MasterTemplete_Labor_Input", + "M01_MasterData_UI_LogicLab_Source", +]; const stub = new Proxy({}, { get: (_t, k) => (k === "currentLanguageIndex" ? () => 0 : () => {}) }); const cache = new Map(); function loadTs(file) { @@ -55,7 +61,7 @@ function loadTs(file) { const rows = (await wrapped.more()).map(({ onPick: _, ...r }) => r); await pick.pickHopyo(input.rows[0], lib); const project = pick.withHopyo({ ...lib, layer: "project" }); - const view = loadTs(path.join(M02, "M02_MasterTemplete_Hopyo_View.ts")); + const view = loadTs(path.join(M02, "M02_MasterTemplete_Hopyo_Choose.ts")); const sheet = loadTs(path.join(M02, "M02_MasterTemplete_Hopyo_Sheet.ts")); const api = loadTs(path.join(M02, "M02_MasterTemplete_Hopyo_Api.ts")); await api.readHopyoGrid({ layer: "company", owner: "7" }, "HP-000001"); @@ -76,17 +82,6 @@ function loadTs(file) { view.targetText({ 호표: 46, 단위: "㎡" }), ], inputs: view.inputsText({ 횟수: "4회사용시", 지역: "서울" }, ["보정작업"]), - memory: [ - view.memoryOf({ 로직: "GF000101" }, { 지역: "서울" }, 1), - view.memoryOf({ 자재: "MT1" }, {}, 0.5), - view.memoryOf({ 로직: "GF000101" }, {}, 0), - view.memoryOf(null, {}, 1), - ], - word: [ - view.builtWord({ 다시씀: false }, false), - view.builtWord({ 다시씀: true }, false), - view.builtWord({ 다시씀: true }, true), - ], }, basis: (() => { const hit = { @@ -99,9 +94,6 @@ function loadTs(file) { const half = { ...hit, 직종: [] }; const trade = { 출처: [["건설품셈", "3-2-1"]], 직종: ["특별인부", "보통인부"], 행: [1, 1] }; const plain = { ...trade, 직종: ["보통인부"] }; - const line = (상태, 대상, 후보) => ({ 근거: trade, 맞춤: { 상태, 대상, 후보 } }); - const a = { 로직: "GF000101", 근거: half }; - const b = { 로직: "GC000083", 근거: hit }; return { ok: [hit, half, { ...hit, 단위: false }, { ...hit, 까닭: "앞 마디 같음" }, undefined].map( (b) => view.basisOk(b, trade), @@ -114,12 +106,6 @@ function loadTs(file) { view.sourceText({ 출처: [], 직종: [], 행: [7, 7] }), view.sourceText(undefined), ], - pick: [ - line("후보", a, [a, b]), - line("후보", b, [b]), - line("같음", b, [b]), - line("후보", a, [a]), - ].map((x) => view.basisPick(x)?.로직 ?? null), }; })(), sheet: { diff --git a/resources/tester/helper_m02_hopyo_seam.cjs b/resources/tester/helper_m02_hopyo_seam.cjs index 55616cebd..705d02497 100644 --- a/resources/tester/helper_m02_hopyo_seam.cjs +++ b/resources/tester/helper_m02_hopyo_seam.cjs @@ -1,15 +1,21 @@ /* M02 일위대가 실무자료 이음매(329 · 330) 헬퍼 — 실제 서버 응답(격자 · 호표 맞춤)을 - * `M02_MasterTemplete_Hopyo_Sheet.ts`(행 → 호표 · 영역 A1) · `M02_MasterTemplete_Hopyo_View.ts` - * (근거 꼬리표 · 엑셀 근거 줄 · 근거 맞음 대상)에 먹임 · - * stdin = {grid, lines, all} · stdout = {at, span, rows, lines: [{이름, source, tag, ok, pick}], - * count: {top, pick, half}} JSON — count = 「후보」 줄 1순위 근거 맞음 · 근거 맞음 대상 · 그 가운데 앞 마디. */ + * `M02_MasterTemplete_Hopyo_Sheet.ts`(행 → 호표 · 영역 A1) · `M02_MasterTemplete_Hopyo_Choose.ts` + * (근거 꼬리표 · 엑셀 근거 줄 · 근거 맞음)에 먹임 · + * stdin = {grid, lines, all} · stdout = {at, span, rows, lines: [{이름, source, tag, ok}], + * count: {top}} JSON — count = 「후보」 줄 1순위 근거 맞음. */ const fs = require("fs"); const path = require("path"); const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript")); const ROOT = path.join(__dirname, "..", ".."); const STUBS = { "@config/config_frontend": { API_BASE_URL: "/api" } }; -const STUB_FILES = ["M02_MasterTemplete_Labor_Pick", "M02_Material_Pick"]; +const STUB_FILES = [ + "M02_MasterTemplete_Labor_Pick", + "M02_Material_Pick", + "M02_MasterTemplete_Labor_Api", + "M02_MasterTemplete_Labor_Input", + "M01_MasterData_UI_LogicLab_Source", +]; const stub = new Proxy({}, { get: (_t, k) => (k === "currentLanguageIndex" ? () => 0 : () => {}) }); const cache = new Map(); function loadTs(file) { @@ -34,7 +40,7 @@ function loadTs(file) { const input = JSON.parse(fs.readFileSync(0, "utf8")); const M02 = path.join(ROOT, "M02_MasterTemplete"); const sheet = loadTs(path.join(M02, "M02_MasterTemplete_Hopyo_Sheet.ts")); -const view = loadTs(path.join(M02, "M02_MasterTemplete_Hopyo_View.ts")); +const view = loadTs(path.join(M02, "M02_MasterTemplete_Hopyo_Choose.ts")); const { grid } = input; const ilwi = grid.책.시트.find((s) => s.이름 === "일위대가"); const three = grid.호표.find((h) => h.번 === 3); @@ -49,15 +55,11 @@ const out = { source: view.sourceText(line.근거), tag: view.basisTag(top?.근거), ok: view.basisOk(top?.근거, line.근거), - pick: view.basisPick(line)?.로직 ?? null, }; }), }; const open = (input.all ?? []).filter((line) => line.맞춤.상태 === "후보"); -const picks = open.map((line) => view.basisPick(line)).filter(Boolean); out.count = { top: open.filter((line) => view.basisOk((line.맞춤.후보 ?? [])[0]?.근거, line.근거)).length, - pick: picks.length, - half: picks.filter((c) => !c.근거.까닭.startsWith("조항 같음")).length, }; process.stdout.write(JSON.stringify(out)); diff --git a/resources/tester/test_m02_hopyo_screen.py b/resources/tester/test_m02_hopyo_screen.py index 3eef9079e..6492f4e37 100644 --- a/resources/tester/test_m02_hopyo_screen.py +++ b/resources/tester/test_m02_hopyo_screen.py @@ -1,12 +1,15 @@ -"""일위대가 실무자료 화면(PLAN 324-4) — 좌측 판 · 본 화면 · [로직 추가] 「실무 호표」 줄. +"""일위대가 실무자료 화면(PLAN 324-4 · 338-3) — 좌측 판 · 본 화면 · 확인 창 · 고르기 창 · +별칭 장부 · [로직 추가] 「실무 호표」 줄 · 엑셀 보기 창. -- 고르기 창 덧줄(Node · fetch 대역): 이 층 · 위 층 호표 줄(맞춤 n/m) · 종류 고르기 · - 고르면 문서 층에 만들기 길 → `lib.pick` 참조 줄(별칭 = 호표 품명) · 프로젝트 층엔 없음 · - 본 화면 글 도우미 -- 고르기 창 근거(330-3): 꼬리표 · 엑셀 근거 줄 · [근거 맞음 모두 확정] 대상 -- 엑셀 보기 창(329): 행 → 호표 · 호표 영역 A1 · 격자 길 · 여는 자리 넷 · 늦게 불러옴 -- 소스: 좌측 판 자리(사전조합 아래 · 구조물 조합 층 · 쓰기 권한 없으면 올리기 꺼짐 · - 숨은 file 입력) · Labor_Detail 줄 수 그대로 · 새 파일 이름 · 700 줄 · 색 = 공용 토큰 +- 확인 창 도우미(Node · fetch · sessionStorage 대역 · `helper_m02_hopyo_check.cjs`): + 체크 → 몸(체크한 줄만 · 뺌 · 고른 대상 · 몫 · 배 · 호표) · 빠질 줄 까닭 · 캐시 키 · + 최신 응답만 · 품 비교 · 만들기 · 통째 · 만들 층 · 미리 셈 · 장부 길 · 장부 화면 합침 · + 하위 호표 먼저 · 확인된 것 = 바로 참조 +- 고르기 창 덧줄(`helper_m02_hopyo_pick.cjs`): 이 층 · 위 층 호표 줄 · 종류 고르기 · + 프로젝트 층엔 없음 · 글 도우미 · 근거 꼬리표 · 엑셀 보기 창 행 → 호표 +- 소스: 옛 즉시 확정 · 바로 만들기 없음(saveHopyoMemory · [확정] · [같음 모두 확정] · + [근거 맞음 모두 확정] · [사전조합 만들기]) · `buildHopyo(` 는 확인 창 · 엑셀 보기 창 펼침에서만 · + 좌측 판 자리 · 700 줄 · 색 = 공용 토큰 """ import json @@ -17,45 +20,181 @@ from pathlib import Path HERE = os.path.dirname(os.path.abspath(__file__)) M02 = Path(HERE).parents[1] / "M02_MasterTemplete" -NEW = ("Hopyo_Api", "Hopyo_Pick", "Hopyo_Side", "Hopyo_View", "Hopyo_Sheet") +NEW = ( + "Hopyo_Api", + "Hopyo_Pick", + "Hopyo_Side", + "Hopyo_View", + "Hopyo_Sheet", + "Hopyo_Check", + "Hopyo_Choose", + "Hopyo_Ledger", +) +ROW = { + "층": "system", + "주인": None, + "키": "HP-000001", + "파일": "북부청.xls", + "번": 3, + "이름": "임도표지석", + "규격": "910×600×150", + "단위": "개소", + "맞춤": [2, 4], +} +SURE = {"로직": "GC000198", "이름": "장비사용타설", "입력값": {"소량감률": 50}, "배": 1, "몫": None} +QTY = { + "엑셀": {"인력": {"콘크리트공": 0.1091}, "장비": [{"갈래": "굴착기", "시간": 0.29}]}, + "우리": {"인력": {"콘크리트공": 0.1091}, "장비": [{"갈래": "굴착기", "시간": 0.31}]}, + "같음": {"콘크리트공": True, "굴착기": False}, +} def _text(name: str) -> str: return (M02 / name).read_text(encoding="utf-8") -def _run() -> dict: - row = { - "층": "system", - "주인": None, - "키": "HP-000001", - "파일": "북부청.xls", - "번": 3, - "이름": "임도표지석", - "규격": "910×600×150", - "단위": "개소", - "맞춤": [2, 4], - } - built = { - "참조": {"층": "company", "주인": "7", "키": "SC-000009"}, - "이름": "임도표지석", - "다시씀": False, - "빠진줄": [{"차례": 0}], - "비교": {}, - } +def _node(helper: str, payload: dict) -> dict: proc = subprocess.run( - ["node", os.path.join(HERE, "helper_m02_hopyo_pick.cjs")], - input=json.dumps({"rows": [row], "built": built}, ensure_ascii=False), + ["node", os.path.join(HERE, helper)], + input=json.dumps(payload, ensure_ascii=False), capture_output=True, text=True, encoding="utf-8", - check=True, + check=False, ) + assert proc.returncode == 0, proc.stderr return json.loads(proc.stdout) -def test_고르기_창_실무_호표_줄과_고름(): - got = _run() +def _line(no, state, check, parts, **extra): + match = {"열쇠": f"줄{no}||m3", "상태": state, "체크": check, "대상": parts, **extra} + return {"차례": no, "이름": f"줄{no}", "규격": "", "수량": 1, "단위": "㎥", "갈래": "단가산출", + "맞춤": match} # fmt: skip + + +def _check() -> dict: + lines = [ + _line(0, "후보", False, [{"자재": "MT004258", "입력값": {}, "배": 1}]), + _line(1, "맞음", True, [SURE], 품=QTY), + _line(2, "별칭", True, [{"로직": "GF000101", "입력값": {}, "배": 1}]), + _line(3, "다른 점", False, [{"로직": "GF000103", "입력값": {}, "배": 1}]), + _line(4, "후보", False, []), + _line(5, "%", False, []), + _line(6, "호표", True, [{"호표": 46, "입력값": {}, "배": 2.3}]), + _line(7, "노임", False, [{"로직": "GF000300", "입력값": {}, "배": None}]), + ] + ledger = [ + {"열쇠": "가|b|m3", "원래": {"품명": "가", "규격": "b", "단위": "m3"}, "갈래": "짝", + "짝": [{"로직": "GC000198", "입력값": {}, "배": 1, "몫": None}], "층": "personal", + "주인": "7/42", "고침": True}, + {"열쇠": "나||m3", "원래": {"품명": "나", "규격": "", "단위": "m3"}, "갈래": "짝", + "짝": [{"로직": "GF000101", "입력값": {}, "배": 1, "몫": None}], "층": "personal", + "주인": "7/42", "고침": True}, + {"열쇠": "다||개", "원래": {"품명": "다", "규격": "", "단위": "개"}, "갈래": "짝", + "짝": [{"자재": "MT1", "입력값": {}, "배": 1, "몫": None}], "층": "system", + "주인": None, "고침": False}, + ] # fmt: skip + rows = [ + {**ROW, "확인": {"층": "company", "주인": "7", "키": "SC-000009"}}, + {**ROW, "번": 4, "이름": "배수관", "확인": None}, + ] + return _node("helper_m02_hopyo_check.cjs", {"lines": lines, "ledger": ledger, "rows": rows}) + + +def test_확인_창_체크한_줄만_몸(): + got = _check()["check"] + assert got["key"] == "m02-hopyo-check:personal::HP-000001:3" + assert got["suggested"] == [ # 서버 체크 제안 그대로 · 배 없는 줄 · % 줄 · 후보는 안 감 + {"차례": 1, "대상": [{"로직": "GC000198", "입력값": {"소량감률": 50}, "배": 1}]}, + {"차례": 2, "대상": [{"로직": "GF000101", "입력값": {}, "배": 1}]}, + {"차례": 6, "대상": [{"호표": 46, "배": 2.3}]}, + ] + assert got["edited"] == [ # 캐시 — 맞음 줄 체크 끔 · 뺌 · 고른 대상(몫 · 배) + {"차례": 2, "대상": [{"로직": "GF000101", "입력값": {}, "배": 1}]}, + {"차례": 3, "뺌": "우리에 없음"}, + { + "차례": 4, + "대상": [{"로직": "GF000211", "입력값": {"유형": "보통"}, "배": 0.1, "몫": 0.1}], + }, + {"차례": 6, "대상": [{"호표": 46, "배": 2.3}]}, + ] + assert got["leftOut"] == {"확인 안 함": 3, "뺌": 1, "% 줄": 1} + assert got["checkable"] == [True, True, True, True, False, False, True, False] + assert got["sure"] == [1, 2] # [맞음 · 별칭 모두 체크] + assert got["compare"] == "콘크리트공 0.1091 → 0.1091 ✓ · 굴착기 0.29 → 0.31 ≠" + assert got["toTarget"] == [{"호표": 46, "배": 2.3}, {"자재": "MT1", "배": 1}] + assert got["newest"] == [False, True, True] # 같은 줄 늦은 응답 버림 · 다른 줄은 따로 + assert got["dropped"] is False # 만들기 성공 = 캐시 지움 + + +def test_고르기_창_도우미(): + got = _check()["choose"] + assert got["hint"] == ["6-1-1", "고임돌"] # 인용 조항 번호 → 없으면 품명 첫 뜻 낱말 + assert got["picked"] == [ + {"로직": "GC000206", "입력값": {}, "배": 1, "몫": None}, + {"로직": "GF000211", "입력값": {}, "배": 0.1, "몫": 0.1}, + ] + assert got["inputs"] == "소량감률=50(블록), 지역=서울 · ⚠ 못 가름: 측압" + assert got["word"] == ["만듦", "있던 것을 확인한 내용으로 덮음"] + assert got["qty"] == "콘크리트공 0.1091 · 굴착기 0.29hr" + assert got["target"] == "제46호표 [㎡]" + + +def test_별칭_장부_화면_합침(): + got = _check()["ledger"] + assert got["key"] == "m02-hopyo-ledger:company:7" + assert got["merged"] == [ + ["나||m3", "personal", True, True, "안 씀"], # 고친 줄 + ["다||개", "personal", True, True, "짝"], # 위 층 → 내 층으로 고쳐 씀 + ["다||개", "system", False, False, "짝"], # 위 층 = 그대로 읽기만 + ] # 지운 줄(가) 없음 + assert got["filter"] == ["다||개", "다||개"] and got["kind"] == ["나||m3"] + assert got["pair"] == "GC000198 × 1 · 몫 0.1 · 소량감률=50" + assert got["entry"]["짝"] == [] and "층" not in got["entry"] + + +def test_길_만들기_통째_만들_층_미리_셈_장부(): + calls = _check()["calls"] + build = "/api/m02/layers/company/hopyo-build?owner=7" + head = {"층": "system", "주인": None, "키": "HP-000001", "번": 3} + assert calls[0]["url"] == build and calls[0]["body"]["줄"][0]["차례"] == 1 + assert {k: calls[0]["body"][k] for k in head} == head and "새로" not in calls[0]["body"] + assert calls[1]["body"] == {**head, "통째": {"대상": [{"로직": "GF000225", "배": 1}]}} + assert calls[2] == { + "url": "/api/m02/layers/system/hopyo/HP-000001/3?to=company&to_owner=7", + "method": "GET", + "body": None, + } + assert calls[3]["url"] == "/api/m02/layers/personal/hopyo/HP-000001/3/guess" + assert calls[3]["body"]["대상"][0]["몫"] == 0.1 + assert calls[4]["url"] == "/api/m02/layers/company/hopyo-ledger?owner=7" + assert calls[5]["method"] == "PUT" and calls[5]["body"] == {"줄": {"가|b|m3": None}} + + +def test_로직_추가_확인된_것은_바로_참조_아니면_확인_창(): + got = _check()["pick"] + assert got["picked"] == [ + { + "ref": {"층": "company", "주인": "7", "키": "SC-000009"}, + "name": "임도표지석", + "alias": "임도표지석", + } + ] + assert got["calls"] == [] # 확인 없이 만들지 않음 + assert got["opened"] == ["확인 — 북부청.xls 제4호표 배수관"] + + +def test_엑셀_보기_창_하위_호표_먼저(): + assert _check()["sheet"]["order"] == [46, 45, 3, 7, 8] # 순환은 남은 차례 + + +def _pick() -> dict: + row = {**ROW, "확인": {"층": "company", "주인": "7", "키": "SC-000009"}} + return _node("helper_m02_hopyo_pick.cjs", {"rows": [row], "built": {}}) + + +def test_고르기_창_실무_호표_줄(): + got = _pick() assert got["calls"][0] == { "url": "/api/m02/layers/company/hopyo-picks?owner=7", "method": "GET", @@ -69,7 +208,7 @@ def test_고르기_창_실무_호표_줄과_고름(): "tag": "실무 호표", "name": "임도표지석 910×600×150", "sub": "북부청.xls · 제3호표 · 마스터", - "unit": "개소 · 맞춤 2/4", + "unit": "개소 · 확인 SC-000009", }, { "kind": "hopyo", @@ -80,119 +219,16 @@ def test_고르기_창_실무_호표_줄과_고름(): "unit": "호표 1", }, ] - build = got["calls"][1] - assert build["url"] == "/api/m02/layers/company/hopyo-build?owner=7" # 만드는 층 = 문서 층 - assert build["body"] == { - "층": "system", - "주인": None, - "키": "HP-000001", - "번": 3, - "새로": False, - } - assert got["picked"] == [ - { - "ref": {"층": "company", "주인": "7", "키": "SC-000009"}, - "name": "임도표지석", - "alias": "임도표지석", - } - ] + assert got["picked"][0]["ref"]["키"] == "SC-000009" assert got["project"] is True # 프로젝트 층 = 덧줄 없음 -def test_본_화면_글_도우미(): - view = _run()["view"] +def test_글_도우미와_근거_꼬리표(): + got = _pick() + view = got["view"] assert view["target"] == ["구조물터파기 [㎥]", "레미콘 25-21-120 [㎥]", "제46호표 [㎡]"] - assert view["inputs"] == "횟수=4회사용시, 지역=서울 · ⚠ 못 읽음: 보정작업" - assert view["memory"] == [ - {"로직": "GF000101", "입력값": {"지역": "서울"}, "환산": 1}, - {"자재": "MT1", "입력값": {}, "환산": 0.5}, - None, # 환산 0 = 확정 못 함 - None, # 대상 없음 - ] - assert view["word"] == ["만듦", "다시 씀", "새로 만들어 덮음"] # 알림 · 판 글 - - -def test_줄_표_줄바꿈_없음과_고르기_창_머리(): - css = _text("M02_MasterTemplete_Hopyo.css") - rule = css.split(".m02-hopyo__table.m02-hopyo__lines\n :is(th, td):is(")[1].split("}")[0] - cols = [int(n) for n in re.findall(r":nth-child\((\d+)\)", rule)] - assert cols == [1, 4, 5, 6, 7, 9, 10, 11] and "white-space: nowrap;" in rule - heads = ["갈래", "품명", "규격", "수량", "단위", "엑셀 단가", "맞춤", "대상", "입력값", "환산"] - view = _text("M02_MasterTemplete_Hopyo_View.ts") - flat = re.sub(r"\s+", "", view) - assert re.sub(r"\s+", "", ",".join(f'"{h}"' for h in heads)) in flat # 열 차례 그대로 - top = css.split(".m02-hopyo__choose .ui-modal__top > .ui-btn {")[1].split("}")[0] - assert "flex-shrink: 0;" in top and "white-space: nowrap;" in top - assert '"가까운 것 없음"' in view and "builtWord(got, fresh)" in view - - -def test_좌측_판_자리와_권한(): - side = _text("M02_MasterTemplete_UI_Side.ts") - assert 'import { buildHopyoSection } from "./M02_MasterTemplete_Hopyo_Side";' in side - host = side.split('className: "m02-side__lists ui-sidebar-panel"')[1] - assert host.index("precomboSec,") < host.index("section(hopyo.title, hopyo.body),") - assert host.index("section(hopyo.title, hopyo.body),") < host.index("extras ?") - assert "canWrite: canEdit(layer, opt.isAdmin)," in side # 구조물 조합 층 쓰기 권한 - assert "hopyo.paint();" in side.split("function paint(): void {")[1] - names = set(re.findall(r"\b\w*[Pp]ractice\w*\b", side)) - assert names <= {"buildPracticeSection", "practice", "M02_MasterTemplete_Practice"} # 옛 이름만 - assert "LaborSection" not in side and "labor?." not in side and "picked" not in side - hopyo_side = _text("M02_MasterTemplete_Hopyo_Side.ts") - assert 'accept: ".xls,.xlsx"' in hopyo_side and "input.hidden = true;" in hopyo_side - assert "disabled: !opt.canWrite," in hopyo_side and "pageScope()" in hopyo_side - - -def test_로직_추가_한_곳과_파일_규칙(): - detail = _text("M02_MasterTemplete_Labor_Detail.ts") - assert len(detail.splitlines()) == 693 # 줄 수 그대로(제자리 고침) - assert "withHopyo(library)," in detail - assert 'cur.담은로직.push({ 로직: "", 참조: ref, 별칭, 메모: null' in detail - pick = _text("M02_MasterTemplete_Labor_Pick.ts") - assert "more?: () => Promise;" in pick and "...later" in pick - for name in NEW: - text = _text(f"M02_MasterTemplete_{name}.ts") - assert len(text.splitlines()) <= 700, name - css = _text("M02_MasterTemplete_Hopyo.css") - assert "var(--color-" in css and not re.search(r"#[0-9a-fA-F]{3,6}\b", css) - assert '"./M02_MasterTemplete_Hopyo.css"' in _text("M02_MasterTemplete_Hopyo_View.ts") - - -def test_엑셀_보기_창_행과_호표(): - got = _run() - assert got["calls"][2] == { - "url": "/api/m02/layers/company/hopyo-grid/HP-000001?owner=7", - "method": "GET", - "body": None, - } - sheet = got["sheet"] - assert sheet["at"] == [None, 3, 3, 3, 4, 4, None] # 22행 → 제3호표 · 범위 밖 = 없음 - assert sheet["span"] == "B20:O24" # 제3호표 영역 = B열 ~ 끝 열(열 폭 정보 O) - assert sheet["last"] == ["AA", "A"] - assert sheet["rows"] == ["4345:4345", "93:101"] # 단가산출 행 전체 - - -def test_엑셀_보기_창_여는_자리와_늦게_불러옴(): - lazy = 'import("./M02_MasterTemplete_Hopyo_Sheet")' - side = _text("M02_MasterTemplete_Hopyo_Side.ts") - assert lazy in side.split("await show(got);")[1].split("})")[0] # 올린 직후 자동 - view = _text("M02_MasterTemplete_Hopyo_View.ts") - assert lazy in view and '"엑셀로 보기"' in view and '"엑셀에서 보기"' in view - assert "호표: no" in view # 호표 화면 = 그 호표 고른 채 - pick = _text("M02_MasterTemplete_Hopyo_Pick.ts") - assert lazy in pick and "lib.pick(got.참조" in pick - static = r'^import (?!type )[^(;]*"\./M02_MasterTemplete_Hopyo_Sheet"' - for text in (side, view, pick): - assert not re.search(static, text, re.M) # 정적 import 없음(타입만) - sheet = _text("M02_MasterTemplete_Hopyo_Sheet.ts") - assert 'import("../A00_Common/spreadsheet/spreadsheet")' in sheet - assert not re.search( - r'^import \{[^}]*\} from "\.\./A00_Common/spreadsheet/spreadsheet"', sheet, re.M - ) - assert "readOnly: true" in sheet and "wide: true" in sheet - - -def test_고르기_창_근거_꼬리표와_근거_맞음(): - basis = _run()["basis"] + assert view["inputs"] == "횟수=4회사용시, 지역=서울 · ⚠ 못 가름: 보정작업" + basis = got["basis"] assert basis["ok"] == [True, False, False, False, False] # 조항 같음 · 직종 · 단위 모두 assert basis["plain"] == [True, True] # 줄 직종이 보통인부뿐 · 없음 = 직종 안 봄 assert basis["tag"] == [ @@ -205,8 +241,104 @@ def test_고르기_창_근거_꼬리표와_근거_맞음(): "단가산출 7행", "", ] - # 지금 대상이 근거 안 맞으면 근거 맞는 후보 · 「후보」 줄만 · 근거 맞는 것 없으면 없음 - assert basis["pick"] == ["GC000083", "GC000083", None, None] + + +def test_엑셀_보기_창_행과_호표(): + got = _pick() + assert got["calls"][1] == { + "url": "/api/m02/layers/company/hopyo-grid/HP-000001?owner=7", + "method": "GET", + "body": None, + } + sheet = got["sheet"] + assert sheet["at"] == [None, 3, 3, 3, 4, 4, None] # 22행 → 제3호표 · 범위 밖 = 없음 + assert sheet["span"] == "B20:O24" # 제3호표 영역 = B열 ~ 끝 열(열 폭 정보 O) + assert sheet["last"] == ["AA", "A"] + assert sheet["rows"] == ["4345:4345", "93:101"] # 단가산출 행 전체 + + +# ── 소스 ────────────────────────────────────────────────────────────── +def test_옛_즉시_확정과_바로_만들기_없음(): + texts = {name: _text(f"M02_MasterTemplete_{name}.ts") for name in NEW} + every = "\n".join(texts.values()) + for gone in ("saveHopyoMemory", "hopyo-memory", "HopyoMemory", "memoryOf", "basisPick"): + assert gone not in every, gone + for label in ( + "같음 모두 확정", + "근거 맞음 모두 확정", + '"확정"', + '"사전조합 만들기"', + "불러오기", + ): + assert label not in every, label + users = sorted(name for name, text in texts.items() if "buildHopyo(" in text) + assert users == ["Hopyo_Check", "Hopyo_Sheet"] # 확인 창 · 펼침 뒤 한 번 + check = texts["Hopyo_Check"] + assert '"확인하고 사전조합 만들기"' in check and "맞음 · 별칭 모두 체크" in check + assert "showLoadingOverlay()" in check and "dropDraft(k)" in check + assert "m02-hopyo-check:" in check and "POLL_MS" in check + sheet = texts["Hopyo_Sheet"] + assert "확인하고 만들기" in sheet and "이대로 ${ready.length}개 만들기" in sheet + assert "확인 창 열기" in sheet and "buildOrder(" in sheet + pick = texts["Hopyo_Pick"] + assert "if (row.확인)" in pick and 'import("./M02_MasterTemplete_Hopyo_Check")' in pick + view = texts["Hopyo_View"] + assert "buildHopyoCheck(" in view and "choose(" not in view + ledger = texts["Hopyo_Ledger"] + assert "m02-hopyo-ledger:" in ledger and "saveLedger(" in ledger + for gone in ("발주처", "회사로 올리기", "쓰인 횟수"): + assert gone not in ledger.split("*/", 1)[1] # 머리 글 「안 둠」 밖 + + +def test_좌측_판_자리와_권한(): + side = _text("M02_MasterTemplete_UI_Side.ts") + assert 'import { buildHopyoSection } from "./M02_MasterTemplete_Hopyo_Side";' in side + host = side.split('className: "m02-side__lists ui-sidebar-panel"')[1] + assert host.index("precomboSec,") < host.index("section(hopyo.title, hopyo.body),") + assert host.index("section(hopyo.title, hopyo.body),") < host.index("extras ?") + assert "canWrite: canEdit(layer, opt.isAdmin)," in side # 구조물 조합 층 쓰기 권한 + assert "hopyo.paint();" in side.split("function paint(): void {")[1] + hopyo_side = _text("M02_MasterTemplete_Hopyo_Side.ts") + assert 'accept: ".xls,.xlsx"' in hopyo_side and "input.hidden = true;" in hopyo_side + assert "disabled: !opt.canWrite," in hopyo_side and "pageScope()" in hopyo_side + assert "buildHopyoLedger({ scope, canWrite: opt.canWrite })" in hopyo_side # [별칭 장부] + assert hopyo_side.index("list, ledger]") > 0 + + +def test_로직_추가_한_곳과_파일_규칙(): + detail = _text("M02_MasterTemplete_Labor_Detail.ts") + assert len(detail.splitlines()) == 693 # 줄 수 그대로 + assert "withHopyo(library)," in detail + pick = _text("M02_MasterTemplete_Labor_Pick.ts") + assert "more?: () => Promise;" in pick and "...later" in pick + assert "if (pre?.query) finder.search.value = pre.query;" in pick # 찾기 글 미리 넣음 + for name in NEW: + text = _text(f"M02_MasterTemplete_{name}.ts") + assert len(text.splitlines()) <= 700, name + css = _text("M02_MasterTemplete_Hopyo.css") + assert "var(--color-" in css and not re.search(r"#[0-9a-fA-F]{3,6}\b", css) + for state in ("맞음", "별칭", "다른점", "후보"): + assert f".m02-hopyo__state.is-{state}" in css + assert '"./M02_MasterTemplete_Hopyo.css"' in _text("M02_MasterTemplete_Hopyo_Check.ts") + + +def test_엑셀_보기_창_여는_자리와_늦게_불러옴(): + lazy = 'import("./M02_MasterTemplete_Hopyo_Sheet")' + side = _text("M02_MasterTemplete_Hopyo_Side.ts") + assert lazy in side.split("await show(got);")[1].split("})")[0] # 올린 직후 자동 view = _text("M02_MasterTemplete_Hopyo_View.ts") - assert "단산행: basis.행" in view and "근거 맞음 모두 확정" in view - assert "x.근거 || x.맞춤.후보?.some((c) => c.근거)" in view # 옛 파일 = 단추 없음 + assert lazy in view and '"엑셀로 보기"' in view + check = _text("M02_MasterTemplete_Hopyo_Check.ts") + assert lazy in check and '"엑셀에서 보기"' in check and "호표: opt.no" in check + assert "단산행: 행" in check + pick = _text("M02_MasterTemplete_Hopyo_Pick.ts") + assert lazy in pick + static = r'^import (?!type )[^(;]*"\./M02_MasterTemplete_Hopyo_Sheet"' + for text in (side, view, pick, check): + assert not re.search(static, text, re.M) # 정적 import 없음(타입만) + sheet = _text("M02_MasterTemplete_Hopyo_Sheet.ts") + assert 'import("../A00_Common/spreadsheet/spreadsheet")' in sheet + assert not re.search( + r'^import \{[^}]*\} from "\.\./A00_Common/spreadsheet/spreadsheet"', sheet, re.M + ) + assert "readOnly: true" in sheet and "wide: true" in sheet diff --git a/resources/tester/test_m02_hopyo_seam.py b/resources/tester/test_m02_hopyo_seam.py index 54ac54462..58b1aa8a6 100644 --- a/resources/tester/test_m02_hopyo_seam.py +++ b/resources/tester/test_m02_hopyo_seam.py @@ -2,8 +2,8 @@ 화면 도우미(`helper_m02_hopyo_seam.cjs` → Hopyo_Sheet · Hopyo_View)에 먹임. 행 22 → 제3호표 · 25 빈 줄 · 26 → 제4호표 · 영역 B20:O24 · 단산93 행 4229:4244 · -엑셀 근거 줄 · 근거 꼬리표 · 근거 맞음 대상 · 북부청 「후보」 줄 1순위 근거 맞음 = 서버 기준 123 · -앞 마디 같음 0(330 덧고침 2). +엑셀 근거 줄 · 근거 꼬리표 · 근거 맞음 · 북부청 「후보」 줄 1순위 근거 맞음 = 서버 기준 123 +(338 — 근거 맞음 모두 확정 단추 없앰 · 도우미는 고르기 창 `Hopyo_Choose`). """ import json @@ -42,14 +42,12 @@ def test_실제_응답_화면_도우미(client): res = client.get(f"{URL.format('personal')}/HP-000001/{one['번']}").json() every += [x for x in res["호표"]["줄"] if x["갈래"] == "단가산출"] got = _node({"grid": grid, "lines": lines, "all": every}) - assert got["count"]["top"] == 123 and got["count"]["half"] == 0 - assert got["count"]["pick"] >= 123 + assert got["count"]["top"] == 123 assert got["at"] == [None, 3, 3, 3, None, 4] # 25행 = 호표 사이 빈 줄 assert got["span"] == "B20:O24" and got["rows"] == "4229:4244" by = {x["이름"]: x for x in got["lines"]} dig = by["구조물터파기(인력100%)"] assert dig["source"] == "출처 건설품셈 3-2-1 · 직종 특별인부 · 단가산출 4229–4244행" assert dig["tag"] == "조항 건설품셈 공통 3-2-1 · 직종 특별인부 · 단위 같음" - assert dig["ok"] is True and dig["pick"] == "GC000083" - assert by["비탈면 보호공"]["pick"] == "GC000143" + assert dig["ok"] is True assert by["흙깍기(토사)"]["ok"] is True # 엑셀 직종 없는 줄(기계) = 조항 같음 + 단위 From db8462c6d4293f60b30e2ed9363de9eee02a7661 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 15 Oct 2026 23:41:46 +0900 Subject: [PATCH 61/81] =?UTF-8?q?feat(B07=20CAD):=20337-2=20=EB=A7=88-1=20?= =?UTF-8?q?=EC=98=A4=ED=86=A0=EC=BA=90=EB=93=9C=20=EB=A7=88=EC=9A=B0?= =?UTF-8?q?=EC=8A=A4=20=E2=80=94=20=EB=88=84=EB=A6=84=20=EB=8D=94=ED=95=A8?= =?UTF-8?q?=20=C2=B7=20Shift=20=EB=BA=8C=20=C2=B7=20=EC=83=81=EC=9E=90=20?= =?UTF-8?q?=EB=8D=94=ED=95=A8/=EB=BA=8C=20=C2=B7=20=EB=AA=85=EB=A0=B9=20?= =?UTF-8?q?=EC=A4=91=20=EB=81=8C=EA=B8=B0=20=EC=83=81=EC=9E=90=20=C2=B7=20?= =?UTF-8?q?=EC=8A=A4=EC=B9=A8=20=C2=B7=20=EA=B7=B8=EB=A6=BD=20=EB=88=84?= =?UTF-8?q?=EB=A5=B4=EA=B3=A0-=EC=98=AE=EA=B8=B0=EA=B3=A0-=EB=88=84?= =?UTF-8?q?=EB=A5=B4=EA=B8=B0=20=C2=B7=20=EA=B7=B8=EB=A6=BD=20=EC=88=AB?= =?UTF-8?q?=EC=9E=90=20=C2=B7=20=EB=B3=B4=EA=B8=B0=20=EC=A0=84=EC=9A=A9=20?= =?UTF-8?q?=EB=AC=B8=20=C2=B7=20=EC=A7=81=EA=B5=90=20=EA=B0=95=EC=A0=9C=20?= =?UTF-8?q?=C2=B7=20Shift=20=EC=9E=84=EC=8B=9C=20=EC=A7=81=EA=B5=90=20?= =?UTF-8?q?=C2=B7=20=ED=9C=A0=20=EA=B8=B0=EB=B3=B8=20=EB=B0=82=20=C2=B7=20?= =?UTF-8?q?=ED=9C=A0=20=EB=91=90=20=EB=B2=88=20=EB=B2=94=EC=9C=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 고르기: 누름 = 더함 · Shift = 뺌 · Ctrl = 넣고 빼기 · 상자 = 앞 고름 ∪ 상자 · Shift 상자 = 뺌(H4 · H5) · 명령 안 고르기 단계도 누를 때 첫 점 · 스침 강조(ic/selecting.ts) - 그립: 쉴 때만 집음(N7) · 누르고 떼면 대기 → 다음 누름에 놓음 · 끌어 놓기(H3) · 거리 숫자(N8) · 확정 · 읽기 전용 도면은 그립 · Delete 막음(H2) - 직교: F8 = 기준점에서 가로 · 세로 강제 · Shift 누른 동안 뒤집음 · 객체 스냅 점 · 좌표 · 각도 잠금은 안 덮음 · 클릭 사건 snapped 채움(ic/ortho.ts · H8) · 가이드는 F3 와 뗌 · 가이드 잡는 거리 = 화면 픽셀(H9) - 휠: 기본 밂(앞으로 굴리면 확대 · 오토캐드 · 모든 화면 · ☰ 설정 뒤집기 그대로) · 가운데 단추 두 번 = 범위 줌 · ZOOM · ⛶ 앞 화면 기억(ic/zoom-prompt.ts) - 시험: mouse-acad 새로 · 휠 방향 시험 손질(input-wheel · input-prefs-cad · input_prefs · app_settings · 164 · 220_1 · 221_1 · 244_8 기대값 뒤집음 · 266 pull 박음) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Se7ytqbohmBQnfYpwYFnYf --- A00_Common/input_prefs.ts | 8 +- .../openwebcad/src/commands/commands.view.ts | 3 +- .../src/components/ViewControls.tsx | 3 +- .../calculate-angle-guides-and-snap-points.ts | 35 +- .../openwebcad/src/helpers/draw.ts | 4 +- .../openwebcad/src/helpers/get-draw-guides.ts | 6 +- .../src/helpers/get-point-from-event.ts | 27 +- .../src/inputController/input-controller.ts | 69 ++-- .../src/inputController/input-wheel.test.ts | 7 +- .../src/inputController/input-wheel.ts | 2 +- .../openwebcad/src/inputController/ortho.ts | 45 +++ .../src/inputController/selecting.ts | 26 ++ .../src/inputController/zoom-prompt.ts | 48 +++ .../openwebcad/src/tools/grip-edit-tool.ts | 10 +- .../src/tools/select-tool.helpers.ts | 109 +++---- .../openwebcad/src/tools/select-tool.ts | 7 +- resources/tester/cad/input-prefs-cad.test.ts | 22 +- resources/tester/cad/mouse-acad.test.ts | 308 ++++++++++++++++++ resources/tester/helper_164_detail_zoom.cjs | 10 +- resources/tester/helper_218_1_loupe.cjs | 30 +- resources/tester/helper_266_detail_home.cjs | 6 +- resources/tester/helper_input_prefs.cjs | 4 +- resources/tester/test_164_detail_zoom.py | 2 +- resources/tester/test_220_1_loupe_wheel.py | 6 +- .../test_244_8_b08_milestone_no_scroll.py | 8 +- resources/tester/test_app_settings.py | 27 +- resources/tester/test_input_prefs.py | 21 +- 27 files changed, 655 insertions(+), 198 deletions(-) create mode 100644 B07_DesignDetail/openwebcad/src/inputController/ortho.ts create mode 100644 B07_DesignDetail/openwebcad/src/inputController/selecting.ts create mode 100644 B07_DesignDetail/openwebcad/src/inputController/zoom-prompt.ts create mode 100644 resources/tester/cad/mouse-acad.test.ts diff --git a/A00_Common/input_prefs.ts b/A00_Common/input_prefs.ts index 49fff86db..a13c87f7c 100644 --- a/A00_Common/input_prefs.ts +++ b/A00_Common/input_prefs.ts @@ -4,7 +4,7 @@ * * - 값 = `aislo:pref:input-prefs`(등록표 `input-prefs`) JSON 한 벌 · 부를 때마다 읽음(새로고침 없이 바로 따름) * - 없음 · 깨짐 · Node(저장소 없음) = 기본값(= 지금 동작) · 의존 없음 · 불러올 때 window 안 만짐(CAD 도 import) - * - 휠 확대 방향 하나로 모든 화면 일체 — 당김 = 휠을 몸 쪽으로(deltaY > 0) 굴리면 확대 + * - 휠 확대 방향 하나로 모든 화면 일체 — 기본 밂 = 휠을 앞으로(deltaY < 0) 굴리면 확대(오토캐드) · 당김 = 몸 쪽으로 * - 핀치(트랙패드 · 손가락)는 설정과 상관없이 늘 벌리면 확대(deltaY < 0) * ========================================================================== */ @@ -20,7 +20,7 @@ export interface InputPrefs { /** `stateKey("input-prefs")` 와 같은 키(시험이 맞춰 봄) — 등록표를 불러오면 window 가 딸려 와서 글자로 둠 */ export const INPUT_PREFS_KEY = "aislo:pref:input-prefs"; -export const DEFAULT_INPUT_PREFS: InputPrefs = { wheelZoom: "pull", cadAliases: {} }; +export const DEFAULT_INPUT_PREFS: InputPrefs = { wheelZoom: "push", cadAliases: {} }; const cleanAliases = (raw: unknown): Record => { const out: Record = {}; @@ -42,7 +42,7 @@ export function inputPrefs(): InputPrefs { if (!raw) return { ...DEFAULT_INPUT_PREFS, cadAliases: {} }; const value = JSON.parse(raw) as Partial | null; return { - wheelZoom: value?.wheelZoom === "push" ? "push" : "pull", + wheelZoom: value?.wheelZoom === "pull" ? "pull" : "push", cadAliases: cleanAliases(value?.cadAliases), }; } catch { @@ -52,7 +52,7 @@ export function inputPrefs(): InputPrefs { /** 휠 한 번이 확대인가 — 설정 방향을 따름 */ export const wheelIn = (deltaY: number): boolean => - inputPrefs().wheelZoom === "push" ? deltaY < 0 : deltaY > 0; + inputPrefs().wheelZoom === "pull" ? deltaY > 0 : deltaY < 0; /** 트랙패드 핀치 — 브라우저가 Ctrl+휠 · 잘게 쪼갠 값으로 냄 * ponytail: |deltaY| < 50 로 마우스 Ctrl+휠(한 칸 ≥ 약 100)과 가름 · 잘게 도는 마우스는 핀치로 봄 */ diff --git a/B07_DesignDetail/openwebcad/src/commands/commands.view.ts b/B07_DesignDetail/openwebcad/src/commands/commands.view.ts index 0c21cedee..006dfb571 100644 --- a/B07_DesignDetail/openwebcad/src/commands/commands.view.ts +++ b/B07_DesignDetail/openwebcad/src/commands/commands.view.ts @@ -6,6 +6,7 @@ import { bumpSceneVersion } from '../helpers/scene-version'; import { getScreenCanvasDrawController, setEntities, getEntities } from '../state'; import { Tool } from '../tools'; import { selectToolStateMachine } from '../tools/select-tool'; +import { zoomExtents } from '../inputController/zoom-prompt'; const zoomBy = (factor: number): string => { const controller = getScreenCanvasDrawController(); @@ -45,7 +46,7 @@ export const VIEW_COMMANDS: CadCommand[] = [ glyph: '⛶', hint: '도면 전체가 보이도록 화면을 맞춘다', run: () => { - getScreenCanvasDrawController().zoomToFitScreen(); + zoomExtents(); // 앞 화면 기억(이전(P)) return '도면 전체 보기'; }, }, diff --git a/B07_DesignDetail/openwebcad/src/components/ViewControls.tsx b/B07_DesignDetail/openwebcad/src/components/ViewControls.tsx index c5c16e0c2..75d0baa20 100644 --- a/B07_DesignDetail/openwebcad/src/components/ViewControls.tsx +++ b/B07_DesignDetail/openwebcad/src/components/ViewControls.tsx @@ -1,6 +1,7 @@ /** 화면 우하단 탐색 막대 (줌 표시·전체 보기) — 제목줄이 없는 화면(M02)은 되돌리기 · 다시 하기도 */ import type { FC } from 'react'; import { runCommandInput } from '../commands/run-command'; +import { zoomExtents } from '../inputController/zoom-prompt'; import { getScreenCanvasDrawController, isDrawingReadOnly, @@ -28,7 +29,7 @@ export const ViewControls: FC = () => { - diff --git a/B07_DesignDetail/openwebcad/src/helpers/get-point-from-event.ts b/B07_DesignDetail/openwebcad/src/helpers/get-point-from-event.ts index e715c829f..1bebb63db 100644 --- a/B07_DesignDetail/openwebcad/src/helpers/get-point-from-event.ts +++ b/B07_DesignDetail/openwebcad/src/helpers/get-point-from-event.ts @@ -29,7 +29,7 @@ export function getPointFromEvent(startPoint: Point | null, event: PointInputEve : mouse; } if (event.type === ActorEvent.MOUSE_CLICK) { - const clickEvent = event as MouseClickEvent & { snapped?: boolean }; + const clickEvent = event as MouseClickEvent; const click = clickEvent.worldMouseLocation; if (locked) return projectOnLock(startPoint, click); return startPoint && !clickEvent.snapped && orthoOn(!!clickEvent.holdingShift) diff --git a/B07_DesignDetail/openwebcad/src/inputController/input-controller.ts b/B07_DesignDetail/openwebcad/src/inputController/input-controller.ts index a90301703..0268bfcf7 100644 --- a/B07_DesignDetail/openwebcad/src/inputController/input-controller.ts +++ b/B07_DesignDetail/openwebcad/src/inputController/input-controller.ts @@ -38,6 +38,7 @@ import { cadCtrlCommand, ctrlShortcut } from './input-shortcut.ts'; import { wheelZoom } from './input-wheel.ts'; import { isObjectSnap, noteShift, snapAt } from './ortho.ts'; import { isWaitingForSecondSelectPoint, selectingActor } from './selecting.ts'; +import { openSnapMenu } from './snap-menu.ts'; import { isMiddleDouble, zoomExtents } from './zoom-prompt.ts'; import { attachTouch } from './input-touch.ts'; import { NUMBER_REGEXP, type PointTextEvent } from './input-patterns.ts'; @@ -163,10 +164,14 @@ export class InputController { public handleMouseUp(evt: MouseEvent) { if (evt.button === MouseButton.Right) { - // 우클릭 = Enter(명령 중 확정 · 쉴 때 반복) + // 우클릭 = Enter(명령 중 확정 · 쉴 때 반복) · Shift+우클릭 = 점 물음에서 한 번 스냅 메뉴 evt.preventDefault(); evt.stopPropagation(); - this.handleEnterKey(true); + if (evt.shiftKey && currentPrompt().kind === 'point') { + openSnapMenu(evt.clientX, evt.clientY, (name) => this.submitText(name)); + } else { + this.handleEnterKey(true); + } } // If ancestor parent exist with class .controls => ignore clicks, since a button was clicked instead of the canvas diff --git a/B07_DesignDetail/openwebcad/src/inputController/prompt-input.ts b/B07_DesignDetail/openwebcad/src/inputController/prompt-input.ts index 414d961bc..f387dadc3 100644 --- a/B07_DesignDetail/openwebcad/src/inputController/prompt-input.ts +++ b/B07_DesignDetail/openwebcad/src/inputController/prompt-input.ts @@ -60,6 +60,8 @@ export interface SubPrompt { onNumber?(n: number): SubResult; onOption?(key: string): SubResult; onEnter?(): SubResult; + /** 꼴이 정해지지 않은 글(ZOOM 「2X」) — undefined = 안 먹음 · 사다리 그대로 */ + onText?(text: string): SubResult | undefined; } /** `{point}` = 닫고 그 점을 원래 물음에 ABSOLUTE_POINT_INPUT 로 */ export type SubResult = 'stay' | 'done' | { point: Point }; @@ -358,6 +360,8 @@ export function routeTyped(raw: string): void { return; } if (text.startsWith("'")) return routeTransparent(text.slice(1)); + const own = subPrompt()?.onText?.(text); + if (own) return settle(own); if (prompt.kind === 'idle') { // ⑨ 이름 > 내 별칭 > 기본 별칭 딱 맞음만 const command = resolveCommandInput(text); diff --git a/B07_DesignDetail/openwebcad/src/inputController/selecting.ts b/B07_DesignDetail/openwebcad/src/inputController/selecting.ts index f095a6ccb..3205b0869 100644 --- a/B07_DesignDetail/openwebcad/src/inputController/selecting.ts +++ b/B07_DesignDetail/openwebcad/src/inputController/selecting.ts @@ -6,6 +6,11 @@ import type { AnyActorRef } from 'xstate'; import { getActiveToolActor } from '../state.ts'; import { Tool } from '../tools.ts'; import { SelectState } from '../tools/select-tool.ts'; +import { setKeywordNotice } from '../tools/select-tool.helpers.ts'; +import { notice } from './prompt-input.ts'; + +// 고르기 키워드(ALL · L · P) 「n개를 찾음」 → 알림 줄 +setKeywordNotice(notice); const isSelect = (actor: AnyActorRef | undefined): boolean => actor?.getSnapshot()?.context?.type === Tool.SELECT; diff --git a/B07_DesignDetail/openwebcad/src/inputController/snap-menu.css b/B07_DesignDetail/openwebcad/src/inputController/snap-menu.css new file mode 100644 index 000000000..8d313cc24 --- /dev/null +++ b/B07_DesignDetail/openwebcad/src/inputController/snap-menu.css @@ -0,0 +1,29 @@ +/* Shift+우클릭 한 번 스냅 메뉴(`snap-menu.ts`) — 커서 자리에 세로 목록 */ +.cad-snap-menu { + position: fixed; + z-index: 60; + display: flex; + flex-direction: column; + min-width: 160px; + padding: 4px; + border: 1px solid var(--color-border); + border-radius: 6px; + background: var(--color-surface-raised); + box-shadow: var(--shadow-sm); +} + +.cad-snap-menu > button { + padding: 4px 10px; + border: 0; + border-radius: 4px; + background: transparent; + color: var(--color-text); + font-size: 13px; + text-align: left; + cursor: pointer; +} + +.cad-snap-menu > button:hover, +.cad-snap-menu > button:focus-visible { + background: var(--color-surface-hover, rgba(127, 127, 127, 0.15)); +} diff --git a/B07_DesignDetail/openwebcad/src/inputController/snap-menu.ts b/B07_DesignDetail/openwebcad/src/inputController/snap-menu.ts new file mode 100644 index 000000000..bfb33b789 --- /dev/null +++ b/B07_DesignDetail/openwebcad/src/inputController/snap-menu.ts @@ -0,0 +1,76 @@ +/** + * Shift+우클릭 한 번 스냅 메뉴(337 마 · 오토캐드) — 점 물음일 때만 · 고르면 그 이름을 친 것과 같은 길(`submitText`). + * 스냅 이름 · FROM · M2P 의 뜻은 친 글 사다리(`prompt-input.ts`) 몫 · 메뉴는 고르기만. + */ +import './snap-menu.css'; + +/** [친 글, 이름] — 오토캐드 객체 스냅 메뉴 차례 */ +export const SNAP_MENU: readonly [string, string][] = [ + ['FROM', '시작점에서'], + ['M2P', '두 점 사이의 중간'], + ['END', '끝점'], + ['MID', '중간점'], + ['INT', '교차점'], + ['CEN', '중심'], + ['QUA', '사분점'], + ['PER', '수직'], + ['NOD', '노드'], + ['NEA', '근처점'], + ['NON', '없음'], +]; + +let current: { onPick: (name: string) => void; element: HTMLElement | null } | null = null; + +const onKey = (evt: KeyboardEvent) => { + if (evt.key !== 'Escape') return; + evt.preventDefault(); + evt.stopImmediatePropagation(); // Esc = 메뉴만 닫음(명령은 그대로) + closeSnapMenu(); +}; +const onOutside = (evt: MouseEvent) => { + if (!current?.element?.contains(evt.target as Node)) closeSnapMenu(); +}; + +export const isSnapMenuOpen = (): boolean => current !== null; + +export function closeSnapMenu(): void { + if (!current) return; + current.element?.remove(); + current = null; + if (typeof window === 'undefined') return; + window.removeEventListener('keydown', onKey, true); + window.removeEventListener('mousedown', onOutside, true); +} + +/** 메뉴에서 하나 고름 — 닫고 그 이름을 넘김 */ +export function pickSnapMenu(name: string): void { + const picked = current; + closeSnapMenu(); + picked?.onPick(name); +} + +/** 화면 (x, y) 에 메뉴를 띄움 — DOM 없는 시험에선 상태만 */ +export function openSnapMenu(x: number, y: number, onPick: (name: string) => void): void { + closeSnapMenu(); + current = { onPick, element: null }; + if (typeof document === 'undefined') return; + const menu = document.createElement('div'); + menu.className = 'cad-snap-menu controls'; + menu.setAttribute('role', 'menu'); + for (const [name, label] of SNAP_MENU) { + const item = document.createElement('button'); + item.type = 'button'; + item.setAttribute('role', 'menuitem'); + item.textContent = `${label} (${name})`; + item.addEventListener('click', () => pickSnapMenu(name)); + menu.append(item); + } + document.body.append(menu); + // 화면 밖으로 안 나가게 + const box = menu.getBoundingClientRect(); + menu.style.left = `${Math.min(x, window.innerWidth - box.width - 4)}px`; + menu.style.top = `${Math.min(y, window.innerHeight - box.height - 4)}px`; + current.element = menu; + window.addEventListener('keydown', onKey, true); + window.addEventListener('mousedown', onOutside, true); +} diff --git a/B07_DesignDetail/openwebcad/src/inputController/zoom-prompt.ts b/B07_DesignDetail/openwebcad/src/inputController/zoom-prompt.ts index d8146e02e..f17b6b848 100644 --- a/B07_DesignDetail/openwebcad/src/inputController/zoom-prompt.ts +++ b/B07_DesignDetail/openwebcad/src/inputController/zoom-prompt.ts @@ -1,9 +1,12 @@ /** - * 보기 바꿈(337 마) — 범위 줌 · 앞 화면(ZOOM 이전(P)) · 휠 두 번 = 범위. + * 보기 바꿈(337 마) — 범위 줌 · 앞 화면(ZOOM 이전(P)) · 휠 두 번 = 범위 · ZOOM 덧물음. * 앞 화면 기억 = ZOOM · 휠 두 번 · ⛶ 로 바꾼 것만(휠 굴림 · 팬은 안 쌓음) · 오토캐드처럼 열 개. + * ZOOM 은 덧물음 — 도구를 안 바꿔 명령 중 `'Z` 도 같은 길(투명). */ -import type { Point } from '@flatten-js/core'; +import { Point } from '@flatten-js/core'; +import type { OptionSpec, PromptSpec } from '../commands/prompt.ts'; import { getScreenCanvasDrawController } from '../state.ts'; +import { notice, pushSubPrompt, type SubPrompt, type SubResult } from './prompt-input.ts'; type View = { scale: number; offset: Point }; @@ -46,3 +49,110 @@ export function isMiddleDouble(point: { x: number; y: number }, time = Date.now( lastMiddle = double ? null : { time, x: point.x, y: point.y }; return double; } + +/** 배율만 바꿈 — 화면 가운데 자리를 붙잡음 */ +function setScaleAboutCenter(scale: number): void { + const controller = getScreenCanvasDrawController(); + const size = controller.getCanvasSize(); + const offset = controller.getScreenOffset(); + const old = controller.getScreenScale(); + const cx = offset.x + size.x / 2 / old; + const cy = offset.y + size.y / 2 / old; + controller.setScreenScale(scale); + controller.setScreenOffset(new Point(cx - size.x / 2 / scale, cy - size.y / 2 / scale)); +} + +/** 윈도우 줌 — 두 구석 상자를 화면에 꽉(종횡비 유지) */ +export function zoomWindow(a: Point, b: Point): void { + const controller = getScreenCanvasDrawController(); + const size = controller.getCanvasSize(); + const width = Math.abs(a.x - b.x); + const height = Math.abs(a.y - b.y); + const scale = Math.min( + width > 0 ? size.x / width : Number.POSITIVE_INFINITY, + height > 0 ? size.y / height : Number.POSITIVE_INFINITY + ); + if (!Number.isFinite(scale)) return; // 같은 점 두 번 — 안 바꿈 + const cx = (a.x + b.x) / 2; + const cy = (a.y + b.y) / 2; + changeView(() => { + controller.setScreenScale(scale); + controller.setScreenOffset(new Point(cx - size.x / 2 / scale, cy - size.y / 2 / scale)); + }); +} + +const zoomOption = (key: string, kw: string, label: string): OptionSpec => ({ + key, + kw, + en: key, + label, +}); + +/** 오토캐드 ZOOM 첫 줄 — 넣은 넷 · 뺀 넷 · <실시간> */ +const ZOOM_PROMPT: PromptSpec = { + text: '윈도우 구석 지정, 축척 비율 (nX 또는 nXP) 입력', + options: [ + zoomOption('ALL', 'A', '전체'), + zoomOption('EXTENTS', 'E', '범위'), + zoomOption('PREVIOUS', 'P', '이전'), + zoomOption('WINDOW', 'W', '윈도우'), + ], + absent: [ + zoomOption('CENTER', 'C', '중심'), + zoomOption('DYNAMIC', 'D', '동적'), + zoomOption('SCALE', 'S', '축척'), + zoomOption('OBJECT', 'O', '객체'), + ], + default: { kind: 'enter', label: '실시간' }, +}; +const FIRST_CORNER: PromptSpec = { text: '첫 번째 구석 지정' }; +const OTHER_CORNER: PromptSpec = { text: '반대 구석 지정' }; +const SCALE_REGEXP = /^(\d*\.?\d+)\s*XP?$/i; + +/** ZOOM — 덧물음 하나가 단계(첫 줄 → 첫 구석 → 반대 구석)를 품음 */ +export function startZoomPrompt(): void { + let stage: 'main' | 'first' | 'other' = 'main'; + let corner: Point | null = null; + const sub: SubPrompt = { + kind: 'point', + get prompt() { + return stage === 'main' ? ZOOM_PROMPT : stage === 'first' ? FIRST_CORNER : OTHER_CORNER; + }, + onPoint(point): SubResult { + if (stage === 'other' && corner) { + zoomWindow(corner, point); + return 'done'; + } + corner = point; // 첫 줄에서 누름 = 윈도우 첫 구석(오토캐드) + stage = 'other'; + return 'stay'; + }, + onOption(key): SubResult { + if (key === 'WINDOW') { + stage = 'first'; + return 'stay'; + } + if (key === 'PREVIOUS') { + if (!zoomPrevious()) notice('앞 화면이 없음.'); + return 'done'; + } + zoomExtents(); // 전체(A) = 범위(이 CAD 엔 도면 한계 없음) + return 'done'; + }, + onEnter(): SubResult { + if (stage !== 'main') return 'stay'; + notice('실시간 줌은 휠로.'); + return 'done'; + }, + onText(text) { + const match = stage === 'main' ? SCALE_REGEXP.exec(text) : null; + if (!match) return undefined; + const factor = Number.parseFloat(match[1]); + if (factor <= 0) return undefined; + const scale = getScreenCanvasDrawController().getScreenScale() * factor; + changeView(() => setScaleAboutCenter(scale)); + return 'done'; + }, + }; + pushSubPrompt(sub); +} diff --git a/B07_DesignDetail/openwebcad/src/tools/grip-edit-tool.ts b/B07_DesignDetail/openwebcad/src/tools/grip-edit-tool.ts index 9845aa846..0f60ca6d2 100644 --- a/B07_DesignDetail/openwebcad/src/tools/grip-edit-tool.ts +++ b/B07_DesignDetail/openwebcad/src/tools/grip-edit-tool.ts @@ -1,10 +1,11 @@ /** * 그립 편집 도구 — 선택 도구에서 그립을 집으면 켜진다(뜨거운 그립). * 오토캐드처럼 누르고-옮기고-누르기 · 끌어 놓기 둘 다(누름 · 뗌 가름은 input-controller) · - * 거리 숫자 = 그립 점에서 커서 쪽(N8) · 좌표 · Esc = 그립 놓음. + * 거리 숫자 = 그립 점에서 커서 쪽(N8) · 좌표 · Esc · 종료(X) = 그립 놓음. */ import type { Point } from '@flatten-js/core'; import { Actor, assign, createMachine } from 'xstate'; +import type { OptionSpec, PromptSpec } from '../commands/prompt'; import type { Entity } from '../entities/Entity'; import { getPointFromEvent } from '../helpers/get-point-from-event'; import { type Grip, applyGrip } from '../helpers/grips'; @@ -20,6 +21,24 @@ import { Tool } from '../tools'; import { selectToolStateMachine } from './select-tool'; import { ActorEvent, type PointInputEvent, type StateEvent, type ToolContext } from './tool.types'; +const gripOption = (key: string, kw: string, label: string): OptionSpec => ({ + key, + kw, + en: key, + label, +}); + +/** 오토캐드 그립 신축 줄 — 종료(X)만 · 기준점(B) · 복사(C) · 명령 취소(U) 는 뺌 */ +const GRIP_PROMPT: PromptSpec = { + text: '** 신축 ** 신축점 지정', + options: [gripOption('EXIT', 'X', '종료')], + absent: [ + gripOption('BASE', 'B', '기준점'), + gripOption('COPY', 'C', '복사'), + gripOption('UNDO', 'U', '명령 취소'), + ], +}; + let editedEntity: Entity | null = null; let editedGrip: Grip | null = null; @@ -64,7 +83,7 @@ export const gripEditToolStateMachine = createMachine( }, [GripEditState.WAITING_FOR_TARGET_POINT]: { description: '그립을 옮길 위치를 지정한다', - meta: { instructions: '** 신축 ** 신축점 지정' }, + meta: { kind: 'point', prompt: GRIP_PROMPT }, on: { [ActorEvent.DRAW]: { actions: GripEditAction.DRAW_TEMP_GRIP_EDIT }, [ActorEvent.MOUSE_CLICK]: { @@ -80,6 +99,7 @@ export const gripEditToolStateMachine = createMachine( actions: [GripEditAction.APPLY_GRIP_EDIT, GripEditAction.SWITCH_TO_SELECT_TOOL], }, [ActorEvent.ESC]: { actions: GripEditAction.SWITCH_TO_SELECT_TOOL }, + 'OPTION.EXIT': { actions: GripEditAction.SWITCH_TO_SELECT_TOOL }, }, }, }, diff --git a/B07_DesignDetail/openwebcad/src/tools/select-tool.helpers.ts b/B07_DesignDetail/openwebcad/src/tools/select-tool.helpers.ts index 4ec7b98ed..97decd56b 100644 --- a/B07_DesignDetail/openwebcad/src/tools/select-tool.helpers.ts +++ b/B07_DesignDetail/openwebcad/src/tools/select-tool.helpers.ts @@ -31,6 +31,7 @@ import { setSelectedEntityIds, } from '../state'; import { Tool } from '../tools'; +import { previousSelection } from '../commands/prompt'; import { startGripEdit } from './grip-edit-tool'; import type { SelectContext } from './select-tool'; import type { MouseClickEvent } from './tool.types'; @@ -113,6 +114,26 @@ export function combineSelection(ids: string[], mode: 'add' | 'remove' | 'toggle return [...before, ...ids.filter((id) => !before.includes(id))]; } +/** 알림 줄 — 입력 층이 붙임(`ic/selecting.ts` · prompt-input 을 바로 부르면 불러오기 고리) */ +let notice: (text: string) => void = () => {}; +export const setKeywordNotice = (sink: (text: string) => void): void => { + notice = sink; +}; + +/** 고르기 키워드 — 전체(ALL = Ctrl+A 와 같은 목록) · 최종(LAST = 집을 수 있는 마지막) · 이전(P) · 앞 고름에 더함 */ +export function selectByKeyword(key: 'ALL' | 'LAST' | 'PREVIOUS'): void { + const pickable = getPickableEntities(); + const found = + key === 'ALL' + ? pickable.map((entity) => entity.id) + : key === 'LAST' + ? pickable.slice(-1).map((entity) => entity.id) + : previousSelection().filter((id) => pickable.some((entity) => entity.id === id)); + const ids = combineSelection(expandSelectionWithGroups(found), 'add'); + setSelectedEntityIds(ids); + notice(`${found.length}개를 찾음, 총 ${ids.length}개`); +} + /** 그립 집기 · 그리기 — 쉴 때 · 고칠 수 있는 도면만 */ export const gripsEditable = (): boolean => !isViewOnly() && diff --git a/B07_DesignDetail/openwebcad/src/tools/select-tool.ts b/B07_DesignDetail/openwebcad/src/tools/select-tool.ts index 91d8770d1..f529d08d7 100644 --- a/B07_DesignDetail/openwebcad/src/tools/select-tool.ts +++ b/B07_DesignDetail/openwebcad/src/tools/select-tool.ts @@ -14,6 +14,7 @@ import { assign, createMachine } from 'xstate'; import { drawTempSelectionRectangle, handleFirstSelectionPoint, + selectByKeyword, selectEntitiesInsideRectangle, } from './select-tool.helpers'; @@ -76,6 +77,10 @@ export const selectToolStateMachine = createMachine( actions: SelectAction.DELETE_SELECTED_ENTITIES, target: SelectState.INIT, }, + // 고르기 숨은 키워드(SELECT_OBJECTS) — 전체 · 최종 · 이전 + 'OPTION.ALL': { actions: () => selectByKeyword('ALL') }, + 'OPTION.LAST': { actions: () => selectByKeyword('LAST') }, + 'OPTION.PREVIOUS': { actions: () => selectByKeyword('PREVIOUS') }, }, }, [SelectState.CHECK_SELECTION]: { diff --git a/resources/tester/cad/mouse-acad.test.ts b/resources/tester/cad/mouse-acad.test.ts index d901f1d05..64d2f25c3 100644 --- a/resources/tester/cad/mouse-acad.test.ts +++ b/resources/tester/cad/mouse-acad.test.ts @@ -1,7 +1,8 @@ /** * CAD 마우스 · 고르기 · 그립 · 직교 · 휠 — 오토캐드 조작(PLAN 337-2 마 · 요구 229-3 · 설계 4 장 · 6-3 마). * 누름 더함 · Shift 뺌 · 상자 더함/뺌 · 명령 중 끌기 상자 · 스침 · N7 · 그립 누름-뗌 대기 · 끌어 놓음 · 그립 숫자 · - * H2 문 · 직교 강제 · Shift 임시 직교 · 스냅 점 안 덮음 · F3 꺼도 직교 · H9 · 휠 두 번 범위 · 휠 기본 밂. + * H2 문 · 직교 강제 · Shift 임시 직교 · 스냅 점 안 덮음 · F3 꺼도 직교 · H9 · 휠 두 번 범위 · 휠 기본 밂 · + * 마-2: 우클릭(쉼 반복 · 명령 중 Enter) · ZOOM(E · A · W · P · 2X · 'Z) · Shift+우클릭 스냅 메뉴 · 고르기 ALL/L/P · 그립 X · Esc. * 시험 쪽 flatten-js 는 다른 사본 — 점은 앱 코드(LINE 도구)로만 만들고 숫자로 판정. * 실행: cd B07_DesignDetail/openwebcad && npx vitest run --dir ../../resources/tester/cad mouse-acad */ @@ -14,6 +15,17 @@ vi.mock("react-toastify", () => ({ import { Actor } from "xstate"; import { InputController } from "../../../B07_DesignDetail/openwebcad/src/inputController/input-controller"; import { zoomPrevious } from "../../../B07_DesignDetail/openwebcad/src/inputController/zoom-prompt"; +import { + isSnapMenuOpen, + pickSnapMenu, +} from "../../../B07_DesignDetail/openwebcad/src/inputController/snap-menu"; +import { getSnapOverride } from "../../../B07_DesignDetail/openwebcad/src/inputController/input-snap-override"; +import { selectingActor } from "../../../B07_DesignDetail/openwebcad/src/inputController/selecting"; +import { + currentNotice, + currentPrompt, +} from "../../../B07_DesignDetail/openwebcad/src/inputController/prompt-input"; +import { rememberSelection } from "../../../B07_DesignDetail/openwebcad/src/commands/prompt"; import { wheelZoom } from "../../../B07_DesignDetail/openwebcad/src/inputController/input-wheel"; import { getActiveToolActor, @@ -66,6 +78,11 @@ const release = (x: number, y: number, mods?: Mods) => { move(x, y, mods); controller.handleMouseUp(ev(x, y, mods)); }; +/** 가운데 단추 누름 + 뗌(뗌 = 팬 끝) */ +const middle = (x: number, y: number) => { + press(x, y, { button: 1 }); + controller.handleMouseUp(ev(x, y, { button: 1 })); +}; const click = (x: number, y: number, mods?: Mods) => { press(x, y, mods); release(x, y, mods); @@ -85,6 +102,8 @@ const key = (k: string) => preventDefault: vi.fn(), stopPropagation: vi.fn(), } as unknown as KeyboardEvent); +const rightClick = (shift = false) => + controller.handleMouseUp(ev(0, 0, { button: 2, shiftKey: shift })); const typeEnter = (text: string) => { for (const ch of text) key(ch); key("Enter"); @@ -184,6 +203,20 @@ describe("그립 — 누르고-옮기고-누르기 · 끌기 · 숫자 · N7 · expect(toolType()).toBe(Tool.SELECT); }); + it("뜨거운 그립 Esc · 종료(X) = 놓음(안 바뀜) · 물음 줄", () => { + click(50, 0); + click(100, 0); + expect(currentPrompt().line).toBe("** 신축 ** 신축점 지정 또는 [종료(X)]:"); + move(150, 90); + key("Escape"); + expect(toolType()).toBe(Tool.SELECT); + expectXy(segOf(a).end, 100, 0); + click(100, 0); // 고름은 남음 — 다시 그립 + typeEnter("x"); + expect(toolType()).toBe(Tool.SELECT); + expectXy(segOf(a).end, 100, 0); + }); + it("그립 끌어 놓음 = 놓은 자리", () => { click(50, 0); press(100, 0); @@ -292,17 +325,128 @@ describe("휠", () => { view.zoomToFitScreen = vi.fn(() => view.setScreenScale(3)); const now = vi.spyOn(Date, "now"); now.mockReturnValue(1000); - press(10, 10, { button: 1 }); + middle(10, 10); now.mockReturnValue(1300); - press(11, 11, { button: 1 }); + middle(11, 11); expect(view.zoomToFitScreen).toHaveBeenCalledTimes(1); expect(view.getScreenScale()).toBe(3); expect(zoomPrevious()).toBe(true); expect(view.getScreenScale()).toBe(1); now.mockReturnValue(5000); - press(10, 10, { button: 1 }); + middle(10, 10); now.mockReturnValue(5600); - press(10, 10, { button: 1 }); + middle(10, 10); expect(view.zoomToFitScreen).toHaveBeenCalledTimes(1); }); }); + +describe("우클릭 · 고르기 키워드(마-2)", () => { + it("우클릭 = 쉴 때 앞 명령 반복 · 명령 중 Enter(고르기 끝)", () => { + typeEnter("DRAWORDER"); + expect(toolType()).toBe(Tool.DRAWORDER_FRONT); + key("Escape"); + expect(toolType()).toBe(Tool.SELECT); + rightClick(); + expect(toolType()).toBe(Tool.DRAWORDER_FRONT); + expect(selectingActor()).not.toBeNull(); + click(50, 0); + rightClick(); // 고르기 끝 → 명령 확정 + expect(selectingActor()?.getSnapshot()?.context?.type).toBe(Tool.SELECT); + expect(toolType()).toBe(Tool.SELECT); // 쉼으로 + }); + + it("고르기 ALL · L(최종) · P(이전) — 앞 고름에 더함 · 「n개를 찾음」", () => { + typeEnter("DRAWORDER"); + typeEnter("all"); + expect(getSelectedEntityIds().sort()).toEqual([a, b].sort()); + expect(currentNotice()).toBe("2개를 찾음, 총 2개"); + key("Escape"); + setSelectedEntityIds([]); + typeEnter("DRAWORDER"); + typeEnter("L"); + expect(getSelectedEntityIds()).toEqual([b]); + key("Escape"); + setSelectedEntityIds([]); + rememberSelection([a]); + typeEnter("DRAWORDER"); + typeEnter("p"); + expect(getSelectedEntityIds()).toEqual([a]); + }); +}); + +describe("ZOOM 덧물음 · 스냅 메뉴(마-2)", () => { + const view = () => + getScreenCanvasDrawController() as unknown as { + zoomToFitScreen: () => void; + setScreenScale: (s: number) => void; + getScreenScale: () => number; + getCanvasSize: () => Xy; + }; + + it("Z 물음 줄 · E = 범위 · P = 앞 화면 · Enter = 실시간 안내", () => { + view().zoomToFitScreen = vi.fn(() => view().setScreenScale(4)); + typeEnter("z"); + expect(currentPrompt().line).toBe( + "윈도우 구석 지정, 축척 비율 (nX 또는 nXP) 입력 또는 [전체(A)/범위(E)/이전(P)/윈도우(W)] <실시간>:", + ); + typeEnter("e"); + expect(view().getScreenScale()).toBe(4); + expect(toolType()).toBe(Tool.SELECT); + typeEnter("Z"); + typeEnter("P"); + expect(view().getScreenScale()).toBe(1); + typeEnter("Z"); + typeEnter("C"); + expect(currentNotice()).toBe("「중심(C)」 옵션은 이 CAD 에 없음."); + key("Enter"); + expect(currentNotice()).toBe("실시간 줌은 휠로."); + expect(currentPrompt().kind).toBe("idle"); + }); + + it("2X = 배율 두 배(가운데 붙잡음) · W = 두 구석 · 누름 = 바로 윈도우", () => { + typeEnter("Z"); + typeEnter("2x"); + expect(view().getScreenScale()).toBe(2); + expect(zoomPrevious()).toBe(true); // 배율 · 원점 되돌림 + const size = view().getCanvasSize(); + typeEnter("Z"); + typeEnter("W"); + expect(currentPrompt().line).toBe("첫 번째 구석 지정:"); + click(0, 0); + expect(currentPrompt().line).toBe("반대 구석 지정:"); + click(100, 50); + expect(view().getScreenScale()).toBeCloseTo(Math.min(size.x / 100, size.y / 50), 6); + expect(getSelectedEntityIds()).toEqual([]); // 고르기로 안 샘 + zoomPrevious(); + typeEnter("Z"); + click(0, 0); + click(50, 100); + expect(view().getScreenScale()).toBeCloseTo(Math.min(size.x / 50, size.y / 100), 6); + zoomPrevious(); + }); + + it("'Z 투명 — 선 그리는 중 2X 뒤 선 이어 감", () => { + lineTool(); + click(300, 300); + typeEnter("'z"); + typeEnter("2X"); + expect(view().getScreenScale()).toBe(2); + expect(toolType()).toBe(Tool.LINE); + zoomPrevious(); + }); + + it("Shift+우클릭 = 점 물음에서만 스냅 메뉴 → 한 번 스냅", () => { + rightClick(true); // 쉼 — 메뉴 없음(앞 명령 반복) + expect(isSnapMenuOpen()).toBe(false); + key("Escape"); // 반복된 명령 끝 + lineTool(); + click(300, 300); + rightClick(true); + expect(isSnapMenuOpen()).toBe(true); + pickSnapMenu("END"); + expect(isSnapMenuOpen()).toBe(false); + expect(getSnapOverride()).toBe("END"); + click(96, 4); + expectXy(lastSeg().end, 100, 0); + }); +}); From 00a379595b0c5e3299f08165967731725b457f5d Mon Sep 17 00:00:00 2001 From: umsangdon Date: Fri, 16 Oct 2026 00:14:29 +0900 Subject: [PATCH 69/81] =?UTF-8?q?feat(CAD):=20337-4a=20=EB=8B=A4-1=20?= =?UTF-8?q?=EA=B3=A0=EC=B9=98=EA=B8=B0=207=20=E2=80=94=20OFFSET=20=C2=B7?= =?UTF-8?q?=20FILLET=20=C2=B7=20CHAMFER=20=C2=B7=20TRIM=20=C2=B7=20EXTEND?= =?UTF-8?q?=20=C2=B7=20BREAK=20=C2=B7=20JOIN=20=EC=98=A4=ED=86=A0=EC=BA=90?= =?UTF-8?q?=EB=93=9C=20=EB=AC=BC=EC=9D=8C=20=EC=B0=A8=EB=A1=80=20=C2=B7=20?= =?UTF-8?q?=EC=98=B5=EC=85=98=20=C2=B7=20=EA=B8=B0=EB=B3=B8=EA=B0=92=20?= =?UTF-8?q?=EA=B8=B0=EC=96=B5=20=C2=B7=20=EB=B9=88=20Enter=20=C2=B7=20Esc?= =?UTF-8?q?=20=C2=B7=20=EB=81=9D=EB=82=9C=20=EB=92=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - OFFSET 새 파일(offset-tool.ts) — 거리 <통과점> · 통과점(T) · 지우기(E) 예/아니오 · 객체 → 면의 점 되풀이 · 다중(M) <다음 객체> · 명령 취소(U) · 기억 OFFSETDIST · OFFSETERASE - FILLET · CHAMFER 한 꼴 — 객체 먼저 · 반지름(R) · 거리(D) 둘(둘째 기본 = 첫째) · 자르기(T) 모드 · 다중(M) · 명령 취소(U) · Shift = 구석 · 기억 FILLETRAD · CHAMFERA/B · TRIMMODE - TRIM · EXTEND 빠른 모드 한 꼴 — 되풀이 · Shift = 반대 일 · 집기 반경 밖 무시 · 지우기(R) · 숨은 U · 사각형 변 원래 층 · 꼴(N9) · 자른 선 꼴 유지 - BREAK — 누른 점 = 첫 끊기점 · 첫 번째 점(F) · @0,0 = 같은 점 · JOIN 물음 글 - 시험 resources/tester/cad/modify-acad.test.ts 13 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013QyNXY2TUKXCFMwAca2APG --- .../src/commands/commands.modify.ts | 2 +- .../src/tools/eraser-tool.helpers.ts | 65 ++- .../openwebcad/src/tools/eraser-tool.ts | 243 +++++++--- .../src/tools/modify/corner-tools.ts | 420 ++++++++++++++++-- .../src/tools/modify/offset-tool.ts | 219 +++++++++ .../src/tools/modify/structure-tools.ts | 34 +- resources/tester/cad/modify-acad.test.ts | 374 ++++++++++++++++ 7 files changed, 1216 insertions(+), 141 deletions(-) create mode 100644 B07_DesignDetail/openwebcad/src/tools/modify/offset-tool.ts create mode 100644 resources/tester/cad/modify-acad.test.ts diff --git a/B07_DesignDetail/openwebcad/src/commands/commands.modify.ts b/B07_DesignDetail/openwebcad/src/commands/commands.modify.ts index 14e078a66..1efeadeaf 100644 --- a/B07_DesignDetail/openwebcad/src/commands/commands.modify.ts +++ b/B07_DesignDetail/openwebcad/src/commands/commands.modify.ts @@ -32,11 +32,11 @@ import { extendToolStateMachine, joinToolStateMachine, } from '../tools/modify/structure-tools'; +import { offsetToolStateMachine } from '../tools/modify/offset-tool'; import { alignToolStateMachine, lengthenToolStateMachine, mirrorToolStateMachine, - offsetToolStateMachine, stretchToolStateMachine, } from '../tools/modify/transform-tools'; import { moveToolStateMachine } from '../tools/move-tool'; diff --git a/B07_DesignDetail/openwebcad/src/tools/eraser-tool.helpers.ts b/B07_DesignDetail/openwebcad/src/tools/eraser-tool.helpers.ts index 9238c284c..50475a2a9 100644 --- a/B07_DesignDetail/openwebcad/src/tools/eraser-tool.helpers.ts +++ b/B07_DesignDetail/openwebcad/src/tools/eraser-tool.helpers.ts @@ -1,15 +1,68 @@ -import { type Circle, Point, type Segment } from '@flatten-js/core'; +import { type Circle, Point, Segment } from '@flatten-js/core'; import { compact } from 'es-toolkit'; import { ArcEntity } from '../entities/ArcEntity'; import type { CircleEntity } from '../entities/CircleEntity'; import type { Entity } from '../entities/Entity'; -import type { LineEntity } from '../entities/LineEntity'; +import { LineEntity } from '../entities/LineEntity'; +import { findClosestEntity } from '../helpers/find-closest-entity'; import { findNeighboringPointsOnArc } from '../helpers/find-neighboring-points-on-arc'; import { findNeighboringPointsOnCircle } from '../helpers/find-neighboring-points-on-circle'; import { findNeighboringPointsOnLine } from '../helpers/find-neighboring-points-on-line'; +import { sampleEntityPoints } from '../helpers/geometry/sample-entity'; +import { intersectLines } from '../helpers/geometry/shape-points'; import { getAngleWithXAxis } from '../helpers/get-angle-with-x-axis.ts'; import { isPointEqual } from '../helpers/is-point-equal'; -import { addEntities, deleteEntities, getActiveLayerId } from '../state'; +import { pickRadius } from '../helpers/pick-radius'; +import { queryEntitiesNearPoint } from '../helpers/spatial-index'; +import { addEntities, deleteEntities, getActiveLayerId, getLayerById } from '../state'; +import { copyStyle, isBetween } from './modify/modify.helpers'; + +/** 누른 자리 집기 반경 안 가장 가까운 객체 — 잠금 도면층 뺌(순서 엔진과 같은 셈) */ +export function pickEntityAt(point: Point): Entity | null { + const radius = pickRadius(); + const candidates = queryEntitiesNearPoint(point.x, point.y, radius).filter( + (entity) => !getLayerById(entity.layerId)?.isLocked + ); + const { distance, entity } = findClosestEntity(point, candidates); + return entity && distance <= radius ? entity : null; +} + +/** 점을 객체 위로 내림(가장 가까운 자리) */ +export const closestPointOn = (entity: Entity, point: Point): Point => + findClosestEntity(point, [entity]).segment?.start ?? point; + +/** EXTEND 빠른 모드 — 누른 쪽 끝을 그 방향 가장 가까운 경계(다른 모든 객체)까지 · 선만 */ +export function extendLineEnd(line: Entity, pick: Point, boundaries: Entity[]): Entity | null { + const shape = line.getShape(); + if (!(shape instanceof Segment)) return null; + const fromStart = pick.distanceTo(shape.start)[0] < pick.distanceTo(shape.end)[0]; + const tip = fromStart ? shape.start : shape.end; + const tail = fromStart ? shape.end : shape.start; + const length = tail.distanceTo(tip)[0]; + if (!length) return null; + const dx = (tip.x - tail.x) / length; + const dy = (tip.y - tail.y) / length; + let best = Number.POSITIVE_INFINITY; + let hit: Point | null = null; + for (const boundary of boundaries) { + if (boundary.id === line.id) continue; + const points = sampleEntityPoints(boundary); + for (let index = 1; index < points.length; index++) { + const crossing = intersectLines(tail, tip, points[index - 1], points[index]); + if (!crossing || !isBetween(crossing, points[index - 1], points[index])) continue; + const ahead = (crossing.x - tip.x) * dx + (crossing.y - tip.y) * dy; + if (ahead > 1e-9 && ahead < best) { + best = ahead; + hit = crossing; + } + } + } + if (!hit) return null; + return copyStyle( + line, + new LineEntity(line.layerId, fromStart ? hit : tail, fromStart ? tail : hit) + ); +} export function getAllIntersectionPoints(entity: Entity, entities: Entity[]): Point[] { // TODO see if we need to make this list unique @@ -39,8 +92,10 @@ export function eraseLineSegment( ); const cutLines: Entity[] = line.cutAtPoints([firstCutPoint, secondCutPoint]); - // Remove the segment that has the clickedPointOnShape point on it - const remainingLines = cutLines.filter((line) => !line.containsPointOnShape(clickedPointOnShape)); + // Remove the segment that has the clickedPointOnShape point on it — 남는 토막은 원래 층 · 꼴(N9) + const remainingLines = cutLines + .filter((piece) => !piece.containsPointOnShape(clickedPointOnShape)) + .map((piece) => copyStyle(line, piece)); deleteEntities([line], false); // Helper functions should never trigger an undo state, since they can be called multiple times during one user operation diff --git a/B07_DesignDetail/openwebcad/src/tools/eraser-tool.ts b/B07_DesignDetail/openwebcad/src/tools/eraser-tool.ts index 933a54d2e..ca2746b20 100644 --- a/B07_DesignDetail/openwebcad/src/tools/eraser-tool.ts +++ b/B07_DesignDetail/openwebcad/src/tools/eraser-tool.ts @@ -1,100 +1,91 @@ +/** + * TRIM · EXTEND 빠른 모드(337 설계 1-2) — 누를 때마다 자름(연장) · Shift+누름 = 반대 일 · 되풀이 · + * 빈 Enter · Esc = 끝 · 집기 반경 밖 누름은 아무 일 없음(N9) · U = 이 명령 안 마지막 하나 · + * TRIM 지우기(R) = 고르기 → Enter 로 지우고 돌아옴. + */ import type { Point, Polygon } from '@flatten-js/core'; -import { assign, createMachine } from 'xstate'; +import { assign, createMachine, sendTo } from 'xstate'; +import { + OPT, + type OptionSpec, + type PromptSpec, + SELECT_OBJECTS, + rememberSelection, +} from '../commands/prompt'; import type { ArcEntity } from '../entities/ArcEntity'; import type { CircleEntity } from '../entities/CircleEntity'; import { EntityName } from '../entities/Entity'; import { LineEntity } from '../entities/LineEntity'; import type { RectangleEntity } from '../entities/RectangleEntity'; import { findClosestEntity } from '../helpers/find-closest-entity'; +import { pickRadius } from '../helpers/pick-radius'; import { polygonToSegments } from '../helpers/polygon-to-segments'; import { addEntities, deleteEntities, - getActiveLayerId, getEntities, getPickableEntities, + getSelectedEntities, setEntities, setGhostHelperEntities, + setSelectedEntityIds, setShouldDrawHelpers, + undo, } from '../state'; import { Tool } from '../tools'; import { eraseArcSegment, eraseCircleSegment, eraseLineSegment, + extendLineEnd, getAllIntersectionPoints, + pickEntityAt, } from './eraser-tool.helpers'; -import type { MouseClickEvent, StateEvent, ToolContext } from './tool.types'; +import { copyStyle } from './modify/modify.helpers'; +import { selectToolStateMachine } from './select-tool'; +import { END_STATE, type MouseClickEvent, type StateEvent, type ToolContext } from './tool.types'; export interface EraserContext extends ToolContext { - startPoint: Point | null; + /** 이 명령에서 바꾼 횟수(U) */ + made: number; } export enum EraserState { INIT = 'INIT', WAITING_FOR_FIRST_CLICK = 'WAITING_FOR_FIRST_CLICK', + ERASE_PICK = 'ERASE_PICK', } -export enum EraserAction { - INIT_ERASER_TOOL = 'INIT_ERASER_TOOL', - HANDLE_MOUSE_CLICK = 'HANDLE_MOUSE_CLICK', -} +const absent = (key: string, kw: string, label: string): OptionSpec => ({ + key, + kw, + en: key, + label, +}); +const HIDDEN_UNDO: OptionSpec = { ...OPT.UNDO, hidden: true }; +const CROSSING = absent('CROSSING', 'C', '걸치기'); +const MODE = absent('MODE', 'O', '모드'); +const PROJECT = absent('PROJECT', 'P', '투영'); -export const eraserToolStateMachine = createMachine( - { - types: {} as { - context: EraserContext; - events: StateEvent; - }, - context: { - startPoint: null, - type: Tool.ERASER, - }, - initial: EraserState.INIT, - states: { - [EraserState.INIT]: { - description: 'Initializing the eraser tool', - always: { - actions: EraserAction.INIT_ERASER_TOOL, - target: EraserState.WAITING_FOR_FIRST_CLICK, - }, - }, - [EraserState.WAITING_FOR_FIRST_CLICK]: { - description: '지울 선 선택', - meta: { - instructions: '지울 선 선택', - }, - on: { - // TODO implement a DRAW action to draw the segment that will be erased in dotted line - MOUSE_CLICK: { - actions: EraserAction.HANDLE_MOUSE_CLICK, - target: EraserState.WAITING_FOR_FIRST_CLICK, - }, - }, - }, - // TODO implement rectangle selection to delete - }, - }, - { - actions: { - [EraserAction.INIT_ERASER_TOOL]: assign(() => { - setShouldDrawHelpers(false); - setGhostHelperEntities([]); - return {}; - }), - [EraserAction.HANDLE_MOUSE_CLICK]: assign(({ context, event }) => { - handleMouseClick((event as MouseClickEvent).worldMouseLocation); +const TRIM_PROMPT: PromptSpec = { + text: '자를 객체 선택 또는 Shift 키를 누른 채 선택하여 연장', + options: [{ key: 'ERASE', kw: 'R', en: 'ERASE', label: '지우기' }, HIDDEN_UNDO], + absent: [absent('CUTTINGEDGES', 'T', '절단 모서리'), CROSSING, MODE, PROJECT], +}; +const EXTEND_PROMPT: PromptSpec = { + text: '연장할 객체 선택 또는 Shift 키를 누른 채 선택하여 자르기', + options: [HIDDEN_UNDO], + absent: [absent('BOUNDARYEDGES', 'B', '경계 모서리'), CROSSING, MODE, PROJECT], +}; - return context; - }), - }, - } -); - -export function handleMouseClick(worldMouseLocation: Point) { +/** TRIM 한 번 — 누른 자리 반경 안 객체만(없으면 false) · 사각형 변은 원래 층 · 꼴 */ +export function handleMouseClick( + worldMouseLocation: Point, + radius = Number.POSITIVE_INFINITY +): boolean { const closestEntity = findClosestEntity(worldMouseLocation, getPickableEntities()); - if (!closestEntity) { - return; + if (!closestEntity.entity || closestEntity.distance > radius) { + return false; } const clickedPointOnShape = closestEntity.segment.start; @@ -125,8 +116,8 @@ export function handleMouseClick(worldMouseLocation: Point) { case EntityName.Rectangle: { const rectangle = closestEntity.entity as RectangleEntity; const segments = polygonToSegments(rectangle.getShape() as Polygon); - const segmentEntities = segments.map( - (segment) => new LineEntity(getActiveLayerId(), segment) + const segmentEntities = segments.map((segment) => + copyStyle(rectangle, new LineEntity(rectangle.layerId, segment)) ); // Find the closest segment to the clicked point @@ -151,11 +142,129 @@ export function handleMouseClick(worldMouseLocation: Point) { break; } - case EntityName.Image: { - // TODO implement image eraser - // Switch to rectangle entity and delete the image and the line that was closest to the cursor - } + default: + return false; } setEntities(getEntities(), true); // Force a new undo state entry + return true; } + +/** EXTEND 한 번 — 누른 쪽 끝을 가장 가까운 경계까지 · 못 늘리면 false */ +export function extendAt(worldMouseLocation: Point): boolean { + const target = pickEntityAt(worldMouseLocation); + if (!target) return false; + const extended = extendLineEnd(target, worldMouseLocation, getPickableEntities()); + if (!extended) return false; + deleteEntities([target], false); + addEntities([extended], true); + return true; +} + +const trimAt = (location: Point) => handleMouseClick(location, pickRadius()); + +type Ctx = { context: EraserContext; event: StateEvent }; + +/** 빠른 자르기 · 연장 머신 — `act(점, shift)` 가 바꿨으면 true */ +function createQuickEditTool( + tool: Tool, + prompt: PromptSpec, + act: (location: Point, shift: boolean) => boolean +) { + const undoLast = assign(({ context }: Ctx) => { + if (context.made === 0) return {}; + undo(); + return { made: context.made - 1 }; + }); + const end = { target: END_STATE }; + const forward = { actions: sendTo('trimErase', ({ event }: Ctx) => event) }; + const takesErase = prompt.options?.some((option) => option.key === 'ERASE'); + + return createMachine( + { + types: {} as { context: EraserContext; events: StateEvent }, + context: { made: 0, type: tool }, + initial: EraserState.INIT, + // biome-ignore lint/suspicious/noExplicitAny: 상태마다 사건 꼴이 달라 느슨하게(순서 엔진과 같음) + states: >{ + [EraserState.INIT]: { + always: { + actions: () => { + setShouldDrawHelpers(false); + setGhostHelperEntities([]); + }, + target: EraserState.WAITING_FOR_FIRST_CLICK, + }, + }, + [EraserState.WAITING_FOR_FIRST_CLICK]: { + meta: { kind: 'entity', prompt }, + on: { + MOUSE_CLICK: { + actions: assign(({ context, event }: Ctx) => { + const click = event as MouseClickEvent; + return act(click.worldMouseLocation, !!click.holdingShift) + ? { made: context.made + 1 } + : {}; + }), + }, + ...(takesErase ? { 'OPTION.ERASE': { target: EraserState.ERASE_PICK } } : {}), + 'OPTION.UNDO': { actions: undoLast }, + ENTER: end, + ESC: end, + }, + }, + ...(takesErase + ? { + [EraserState.ERASE_PICK]: { + meta: { + kind: 'selection', + prompt: { text: '지울 객체 선택', options: SELECT_OBJECTS.options }, + }, + entry: () => setShouldDrawHelpers(false), + invoke: { + id: 'trimErase', + src: selectToolStateMachine, + onDone: { + actions: assign(({ context }: Ctx) => { + const picked = getSelectedEntities(); + if (!picked.length) return {}; + rememberSelection(picked.map((entity) => entity.id)); + setSelectedEntityIds([]); + deleteEntities(picked, true); + return { made: context.made + 1 }; + }), + target: EraserState.WAITING_FOR_FIRST_CLICK, + }, + }, + on: { + MOUSE_CLICK: forward, + ENTER: forward, + DRAW: forward, + 'OPTION.*': forward, + ESC: { actions: () => setSelectedEntityIds([]), target: END_STATE }, + }, + }, + } + : {}), + [END_STATE]: { type: 'final' }, + }, + }, + { + // 고르기 자식(선택 도구)의 액션 구현을 함께 넘긴다 + // biome-ignore lint/suspicious/noExplicitAny: 자식 머신의 컨텍스트 타입이 달라 그대로 넘긴다 + actions: { ...(selectToolStateMachine.implementations.actions as any) }, + } + ); +} + +export const eraserToolStateMachine = createQuickEditTool( + Tool.ERASER, + TRIM_PROMPT, + (location, shift) => (shift ? extendAt(location) : trimAt(location)) +); + +export const extendToolStateMachine = createQuickEditTool( + Tool.EXTEND, + EXTEND_PROMPT, + (location, shift) => (shift ? trimAt(location) : extendAt(location)) +); diff --git a/B07_DesignDetail/openwebcad/src/tools/modify/corner-tools.ts b/B07_DesignDetail/openwebcad/src/tools/modify/corner-tools.ts index 425b352a4..d971a1ea6 100644 --- a/B07_DesignDetail/openwebcad/src/tools/modify/corner-tools.ts +++ b/B07_DesignDetail/openwebcad/src/tools/modify/corner-tools.ts @@ -1,57 +1,290 @@ -/** 모깎기·모따기·곡선 혼합·끊기 (조사표 2절) */ +/** + * 모깎기·모따기·곡선 혼합·끊기 (조사표 2절 · 337 설계 1-2) + * FILLET · CHAMFER = 오토캐드 차례 — 첫 객체(또는 U · R/D · T · M) → 둘째 객체(Shift = 구석) · 값 · 자르기 모드 기억 · + * 다중이면 쌍 되풀이 · 빈 Enter · Esc = 끝. BREAK = 누른 점이 첫 끊기점 · F = 첫 점 다시 · `@0,0` = 같은 점. + */ +import type { Point } from '@flatten-js/core'; import { toast } from 'react-toastify'; +import { assign, createMachine } from 'xstate'; +import { OPT, type OptionSpec, type PromptSpec, recall, remember } from '../../commands/prompt'; import type { Entity } from '../../entities/Entity'; -import { addEntities, deleteEntities } from '../../state'; +import { getPointFromEvent } from '../../helpers/get-point-from-event'; +import { + addEntities, + deleteEntities, + getEntities, + setAngleGuideOriginPoint, + setGhostHelperEntities, + setShouldDrawHelpers, + undo, +} from '../../state'; import { Tool } from '../../tools'; +import { closestPointOn, pickEntityAt } from '../eraser-tool.helpers'; import { createSequenceTool } from '../factories/sequence-tool'; +import { + END_STATE, + type MouseClickEvent, + type PointInputEvent, + type StateEvent, + type ToolContext, +} from '../tool.types'; import { blendEntities, breakEntity, chamferLines, filletLines } from './corner.helpers'; import type { CornerResult } from './corner.helpers'; -function applyCorner(first: Entity, second: Entity, result: CornerResult | null): void { +const option = (key: string, kw: string, label: string): OptionSpec => ({ + key, + kw, + en: key, + label, +}); +const entityById = (id: string | null): Entity | undefined => + getEntities().find((entity) => entity.id === id); + +// ---- FILLET · CHAMFER ---- + +interface CornerContext extends ToolContext { + firstId: string | null; + firstPick: Point | null; + multiple: boolean; + /** 이 명령에서 만든 쌍 수(U) */ + made: number; + /** 값 · 자르기 물음 뒤 돌아갈 물음 */ + back: 'FIRST' | 'SECOND'; + /** 방금 누른 쌍을 처리했는가 */ + ok: boolean; +} + +interface CornerSpec { + tool: Tool; + first: string; + second: string; + /** 반지름(R) · 거리(D) */ + value: OptionSpec; + absentFirst: OptionSpec[]; + absentSecond: OptionSpec[]; + /** 값 물음 차례 — shown = 그 열쇠 값을 <기본> 으로 보임 */ + values: Array<{ text: string; remember: string; shown?: string }>; + /** corner = Shift+누름(반지름 · 거리 0) */ + apply( + first: Entity, + firstPick: Point, + second: Entity, + secondPick: Point, + corner: boolean + ): CornerResult | null; +} + +const TRIM_OPTION = option('TRIM', 'T', '자르기'); +const POLYLINE = option('POLYLINE', 'P', '폴리선'); +const TRIM_MODE_PROMPT = (): PromptSpec => ({ + text: '자르기 모드 옵션 입력', + options: [TRIM_OPTION, option('NOTRIM', 'N', '자르지 않기')], + default: { kind: 'option', key: recall('TRIMMODE') ?? 'TRIM' }, +}); + +type CornerCtx = { context: CornerContext; event: StateEvent }; + +function applyPair(spec: CornerSpec, context: CornerContext, click: MouseClickEvent): boolean { + const first = entityById(context.firstId); + const second = pickEntityAt(click.worldMouseLocation); + if (!first || !second || !context.firstPick) return false; + const result = spec.apply( + first, + context.firstPick, + second, + click.worldMouseLocation, + !!click.holdingShift + ); if (!result) { toast.warn('두 직선을 선택해야 합니다. 두 선이 평행하면 처리할 수 없습니다.'); - return; + return false; + } + if (recall('TRIMMODE') === 'NOTRIM') { + if (!result.corner) return false; + addEntities([result.corner], true); + return true; } deleteEntities([first, second], false); addEntities([...result.trimmed, ...(result.corner ? [result.corner] : [])], true); + return true; } -export const filletToolStateMachine = createSequenceTool({ +function createCornerTool(spec: CornerSpec) { + const end = { target: END_STATE }; + const back = [ + { guard: ({ context }: CornerCtx) => context.back === 'SECOND', target: 'SECOND' }, + { target: 'FIRST' }, + ]; + // 값 물음 — 마지막 값 뒤 = 돌아갈 물음 + const valueStates = Object.fromEntries( + spec.values.map((value, index) => { + const store = { + actions: ({ event }: CornerCtx) => + remember(value.remember, (event as { value: number }).value), + }; + const isLast = index === spec.values.length - 1; + return [ + `VALUE_${index}`, + { + meta: { + kind: 'number', + prompt: { + text: value.text, + default: { kind: 'value', remember: value.shown ?? value.remember, initial: 0 }, + number: { min: 0 }, + } satisfies PromptSpec, + }, + on: { + NUMBER_INPUT: isLast + ? back.map((transition) => ({ ...transition, ...store })) + : { ...store, target: `VALUE_${index + 1}` }, + ESC: end, + }, + }, + ]; + }) + ); + + return createMachine({ + types: {} as { context: CornerContext; events: StateEvent }, + context: { + firstId: null, + firstPick: null, + multiple: false, + made: 0, + back: 'FIRST', + ok: false, + type: spec.tool, + }, + initial: 'INIT', + // biome-ignore lint/suspicious/noExplicitAny: 상태마다 사건 꼴이 달라 느슨하게(순서 엔진과 같음) + states: >{ + INIT: { + always: { + actions: () => { + setShouldDrawHelpers(false); + setGhostHelperEntities([]); + }, + target: 'FIRST', + }, + }, + FIRST: { + meta: { + kind: 'entity', + prompt: { + text: spec.first, + options: [OPT.UNDO, spec.value, TRIM_OPTION, OPT.MULTIPLE], + absent: spec.absentFirst, + } satisfies PromptSpec, + }, + on: { + MOUSE_CLICK: { + guard: ({ event }: CornerCtx) => + !!pickEntityAt((event as MouseClickEvent).worldMouseLocation), + actions: assign(({ event }: CornerCtx) => { + const location = (event as MouseClickEvent).worldMouseLocation; + return { firstId: pickEntityAt(location)?.id ?? null, firstPick: location }; + }), + target: 'SECOND', + }, + [`OPTION.${spec.value.key}`]: { + actions: assign({ back: 'FIRST' as const }), + target: 'VALUE_0', + }, + 'OPTION.TRIM': { target: 'TRIM_MODE' }, + 'OPTION.MULTIPLE': { actions: assign({ multiple: true }) }, + 'OPTION.UNDO': { + actions: assign(({ context }: CornerCtx) => { + if (context.made === 0) return {}; + undo(); + return { made: context.made - 1 }; + }), + }, + ENTER: end, + ESC: end, + }, + }, + SECOND: { + meta: { + kind: 'entity', + prompt: { + text: spec.second, + options: [spec.value], + absent: spec.absentSecond, + } satisfies PromptSpec, + }, + on: { + MOUSE_CLICK: { + actions: assign(({ context, event }: CornerCtx) => { + const ok = applyPair(spec, context, event as MouseClickEvent); + return { ok, made: context.made + (ok ? 1 : 0) }; + }), + target: 'APPLIED', + }, + [`OPTION.${spec.value.key}`]: { + actions: assign({ back: 'SECOND' as const }), + target: 'VALUE_0', + }, + ENTER: end, + ESC: end, + }, + }, + APPLIED: { + always: [ + { guard: ({ context }: CornerCtx) => !context.ok, target: 'SECOND' }, + { guard: ({ context }: CornerCtx) => context.multiple, target: 'FIRST' }, + { target: END_STATE }, + ], + }, + TRIM_MODE: { + meta: { kind: 'keyword', prompt: TRIM_MODE_PROMPT }, + on: { + 'OPTION.TRIM': { actions: () => remember('TRIMMODE', 'TRIM'), target: 'FIRST' }, + 'OPTION.NOTRIM': { actions: () => remember('TRIMMODE', 'NOTRIM'), target: 'FIRST' }, + ESC: end, + }, + }, + ...valueStates, + [END_STATE]: { type: 'final' }, + }, + }); +} + +const radius = () => recall('FILLETRAD') ?? 0; + +export const filletToolStateMachine = createCornerTool({ tool: Tool.FILLET, - helpers: false, - steps: [ - { kind: 'number', instructions: '모깎기 반지름을 입력하십시오 <0>.', defaultValue: 0 }, - { kind: 'entity', instructions: '첫 번째 객체를 남길 쪽에서 선택하십시오.' }, - { kind: 'entity', instructions: '두 번째 객체를 남길 쪽에서 선택하십시오.' }, - ], - commit: (input) => { - const first = input.entity(1); - const second = input.entity(2); - applyCorner( - first, - second, - filletLines(first, input.pick(1), second, input.pick(2), input.number(0)) - ); - }, + first: '첫 번째 객체 선택', + second: '두 번째 객체 선택 또는 Shift 키를 누른 채 선택하여 구석 적용', + value: option('RADIUS', 'R', '반지름'), + absentFirst: [POLYLINE], + absentSecond: [], + values: [{ text: '모깎기 반지름 지정', remember: 'FILLETRAD' }], + apply: (first, firstPick, second, secondPick, corner) => + filletLines(first, firstPick, second, secondPick, corner ? 0 : radius()), }); -export const chamferToolStateMachine = createSequenceTool({ +const ANGLE = option('ANGLE', 'A', '각도'); +const METHOD = option('METHOD', 'E', '방법'); + +export const chamferToolStateMachine = createCornerTool({ tool: Tool.CHAMFER, - helpers: false, - steps: [ - { kind: 'number', instructions: '첫 번째 모따기 거리를 입력하십시오 <1>.', defaultValue: 1 }, - { kind: 'number', instructions: '두 번째 모따기 거리를 입력하십시오 <1>.', defaultValue: 1 }, - { kind: 'entity', instructions: '첫 번째 객체를 남길 쪽에서 선택하십시오.' }, - { kind: 'entity', instructions: '두 번째 객체를 남길 쪽에서 선택하십시오.' }, + first: '첫 번째 선 선택', + second: '두 번째 선 선택 또는 Shift 키를 누른 채 선택하여 구석 적용', + value: option('DISTANCE', 'D', '거리'), + absentFirst: [POLYLINE, ANGLE, METHOD], + absentSecond: [ANGLE, METHOD], + values: [ + { text: '첫 번째 모따기 거리 지정', remember: 'CHAMFERA' }, + { text: '두 번째 모따기 거리 지정', remember: 'CHAMFERB', shown: 'CHAMFERA' }, ], - commit: (input) => { - const first = input.entity(2); - const second = input.entity(3); - applyCorner( - first, - second, - chamferLines(first, input.pick(2), second, input.pick(3), input.number(0), input.number(1)) - ); + apply: (first, firstPick, second, secondPick, corner) => { + const a = recall('CHAMFERA') ?? 0; + const b = recall('CHAMFERB') ?? 0; + // 거리 0 · Shift = 구석(모깎기 반지름 0 과 같음) + return corner || (a === 0 && b === 0) + ? filletLines(first, firstPick, second, secondPick, 0) + : chamferLines(first, firstPick, second, secondPick, a, b); }, }); @@ -72,18 +305,111 @@ export const blendToolStateMachine = createSequenceTool({ }, }); -export const breakToolStateMachine = createSequenceTool({ - tool: Tool.BREAK, - steps: [ - { kind: 'entity', instructions: '끊을 객체를 선택하십시오.' }, - { kind: 'point', instructions: '첫 번째 끊기 점을 지정하십시오.' }, - { kind: 'point', instructions: '두 번째 끊기 점을 지정하십시오.' }, - ], - commit: (input) => { - const entity = input.entity(0); - const pieces = breakEntity(entity, input.point(1), input.point(2)); - deleteEntities([entity], false); - addEntities(pieces, true); +// ---- BREAK ---- + +interface BreakContext extends ToolContext { + entityId: string | null; + first: Point | null; +} + +type BreakCtx = { context: BreakContext; event: StateEvent }; + +/** 두 끊기점 사이를 지움 · 같은 점 = 틈 없이 둘로 */ +function breakNow(context: BreakContext, event: StateEvent) { + const entity = entityById(context.entityId); + if (!entity || !context.first) return; + const first = closestPointOn(entity, context.first); + const second = closestPointOn(entity, getPointFromEvent(context.first, event as PointInputEvent)); + const same = Math.hypot(first.x - second.x, first.y - second.y) < 1e-9; + const pieces = same ? breakEntity(entity, first) : breakEntity(entity, first, second); + if (same && pieces.length < 2) return; + deleteEntities([entity], false); + addEntities(pieces, true); +} + +const POINT_EVENTS = [ + 'MOUSE_CLICK', + 'ABSOLUTE_POINT_INPUT', + 'RELATIVE_POINT_INPUT', + 'NUMBER_INPUT', +]; +const onPoint = (transition: object) => + Object.fromEntries(POINT_EVENTS.map((type) => [type, transition])); +const FIRST_POINT = option('FIRST', 'F', '첫 번째 점'); + +export const breakToolStateMachine = createMachine({ + types: {} as { context: BreakContext; events: StateEvent }, + context: { entityId: null, first: null, type: Tool.BREAK }, + initial: 'INIT', + states: { + INIT: { + always: { + actions: () => { + setShouldDrawHelpers(false); + setGhostHelperEntities([]); + }, + target: 'OBJECT', + }, + }, + OBJECT: { + meta: { kind: 'entity', prompt: { text: '객체 선택' } satisfies PromptSpec }, + on: { + MOUSE_CLICK: { + guard: ({ event }: BreakCtx) => + !!pickEntityAt((event as MouseClickEvent).worldMouseLocation), + actions: assign(({ event }: BreakCtx) => { + const location = (event as MouseClickEvent).worldMouseLocation; + const entity = pickEntityAt(location) as Entity; + const first = closestPointOn(entity, location); + setAngleGuideOriginPoint(first); + return { entityId: entity.id, first }; + }), + target: 'SECOND', + }, + ENTER: { target: END_STATE }, + ESC: { target: END_STATE }, + }, + }, + SECOND: { + meta: { + kind: 'point', + prompt: { text: '두 번째 끊기점 지정', options: [FIRST_POINT] } satisfies PromptSpec, + }, + entry: () => setShouldDrawHelpers(true), + on: { + ...onPoint({ + actions: ({ context, event }: BreakCtx) => breakNow(context, event), + target: END_STATE, + }), + 'OPTION.FIRST': { target: 'FIRST_POINT' }, + ESC: { target: END_STATE }, + }, + }, + FIRST_POINT: { + meta: { kind: 'point', prompt: { text: '첫 번째 끊기점 지정' } satisfies PromptSpec }, + on: { + ...onPoint({ + actions: assign(({ context, event }: BreakCtx) => { + const first = getPointFromEvent(context.first, event as PointInputEvent); + setAngleGuideOriginPoint(first); + return { first }; + }), + target: 'SECOND_POINT', + }), + ESC: { target: END_STATE }, + }, + }, + SECOND_POINT: { + meta: { kind: 'point', prompt: { text: '두 번째 끊기점 지정' } satisfies PromptSpec }, + on: { + ...onPoint({ + actions: ({ context, event }: BreakCtx) => breakNow(context, event), + target: END_STATE, + }), + ESC: { target: END_STATE }, + }, + }, + [END_STATE]: { type: 'final' }, }, }); diff --git a/B07_DesignDetail/openwebcad/src/tools/modify/offset-tool.ts b/B07_DesignDetail/openwebcad/src/tools/modify/offset-tool.ts new file mode 100644 index 000000000..2d2a0d9ae --- /dev/null +++ b/B07_DesignDetail/openwebcad/src/tools/modify/offset-tool.ts @@ -0,0 +1,219 @@ +/** + * OFFSET(337 설계 1-2) — 오토캐드 간격띄우기 차례: 거리(또는 통과점(T) · 지우기(E)) → 객체 → 면의 점 → + * 객체로 되풀이 · 다중(M) = 방금 만든 것을 같은 거리로 되풀이 · U = 이 명령 안 마지막 하나 · 빈 Enter · Esc = 끝. + * 거리 기억 = OFFSETDIST(처음 <통과점>) · 원본 지우기 기억 = OFFSETERASE. + */ +import { type Point, Segment } from '@flatten-js/core'; +import { toast } from 'react-toastify'; +import { assign, createMachine } from 'xstate'; +import { OPT, type OptionSpec, type PromptSpec, recall, remember } from '../../commands/prompt'; +import type { Entity } from '../../entities/Entity'; +import { findClosestEntity } from '../../helpers/find-closest-entity'; +import { getPointFromEvent } from '../../helpers/get-point-from-event'; +import { + addEntities, + deleteEntities, + getEntities, + setAngleGuideOriginPoint, + setGhostHelperEntities, + setShouldDrawHelpers, + undo, +} from '../../state'; +import { Tool } from '../../tools'; +import { pickEntityAt } from '../eraser-tool.helpers'; +import { + type DrawEvent, + END_STATE, + type MouseClickEvent, + type PointInputEvent, + type StateEvent, + type ToolContext, +} from '../tool.types'; +import { offsetEntity } from './modify.helpers'; + +interface OffsetContext extends ToolContext { + /** 간격띄울 객체 */ + sourceId: string | null; + /** 처음 고른 객체 — 원본 지우기는 이것만 */ + pickedId: string | null; + multiple: boolean; + /** 이 명령에서 만든 수(U) */ + made: number; +} + +type Ctx = { context: OffsetContext; event: StateEvent }; + +const option = (key: string, kw: string, label: string): OptionSpec => ({ + key, + kw, + en: key, + label, +}); +const THROUGH = option('THROUGH', 'T', '통과점'); + +/** 기억한 거리 · null = 통과점 */ +const offsetDistance = (): number | null => { + const distance = recall('OFFSETDIST'); + return typeof distance === 'number' ? distance : null; +}; + +const DISTANCE_PROMPT = (): PromptSpec => { + const distance = offsetDistance(); + return { + text: '간격띄우기 거리 지정', + options: [THROUGH, option('ERASE', 'E', '지우기')], + absent: [option('LAYER', 'L', '도면층')], + default: + distance === null + ? { kind: 'option', key: 'THROUGH' } + : { kind: 'value', remember: 'OFFSETDIST', initial: distance }, + number: { positive: true }, + }; +}; +const ERASE_PROMPT = (): PromptSpec => ({ + text: '간격띄우기 후 원본 객체를 지우시겠습니까?', + options: [OPT.YES, OPT.NO], + default: { kind: 'option', key: recall('OFFSETERASE') ? 'YES' : 'NO' }, +}); +const OBJECT_PROMPT: PromptSpec = { + text: '간격띄우기할 객체 선택', + options: [OPT.EXIT, OPT.UNDO], + default: { kind: 'enter', label: '종료' }, +}; +const SIDE_PROMPT = (context: Partial): PromptSpec => ({ + text: offsetDistance() === null ? '통과점 지정' : '간격띄우기할 면의 점 지정', + options: context.multiple ? [OPT.EXIT, OPT.UNDO] : [OPT.EXIT, OPT.MULTIPLE, OPT.UNDO], + default: { kind: 'enter', label: context.multiple ? '다음 객체' : '종료' }, +}); + +/** 통과점까지 거리 — 선은 무한 직선까지(오토캐드) · 그 밖은 모양까지 */ +function throughDistance(entity: Entity, point: Point): number { + const shape = entity.getShape(); + if (shape instanceof Segment) { + const { start, end } = shape; + const length = start.distanceTo(end)[0] || 1; + return ( + Math.abs((end.x - start.x) * (point.y - start.y) - (end.y - start.y) * (point.x - start.x)) / + length + ); + } + return findClosestEntity(point, [entity]).distance; +} + +function offsetThrough(source: Entity, point: Point): Entity | null { + return offsetEntity(source, offsetDistance() ?? throughDistance(source, point), point); +} + +const sourceOf = (context: OffsetContext) => + getEntities().find((entity) => entity.id === context.sourceId); + +/** 면의 점 하나 → 새 객체 · 다중이면 그것이 다음 원본 */ +const placeOffset = assign(({ context, event }: Ctx): Partial => { + const source = sourceOf(context); + if (!source) return { sourceId: null }; + const point = getPointFromEvent(null, event as PointInputEvent); + const result = offsetThrough(source, point); + setGhostHelperEntities([]); + if (!result) { + toast.warn('이 객체는 지정한 거리로 간격을 띄울 수 없습니다.'); + return {}; + } + if (recall('OFFSETERASE') && source.id === context.pickedId) deleteEntities([source], false); + addEntities([result], true); + return { made: context.made + 1, sourceId: context.multiple ? result.id : null }; +}); + +const undoLast = assign(({ context }: Ctx): Partial => { + setGhostHelperEntities([]); + if (context.made === 0) return {}; + undo(); + return { made: context.made - 1, sourceId: null, multiple: false }; +}); + +const end = { target: END_STATE }; +const toObject = { actions: assign({ sourceId: null, multiple: false }), target: 'OBJECT' }; + +// biome-ignore lint/suspicious/noExplicitAny: 상태마다 사건 꼴이 달라 느슨하게(순서 엔진과 같음) +const states: Record = { + INIT: { + always: { + actions: () => { + setShouldDrawHelpers(false); + setGhostHelperEntities([]); + setAngleGuideOriginPoint(null); + }, + target: 'DISTANCE', + }, + }, + DISTANCE: { + meta: { kind: 'number', prompt: DISTANCE_PROMPT }, + on: { + NUMBER_INPUT: { + actions: ({ event }: Ctx) => remember('OFFSETDIST', (event as { value: number }).value), + target: 'OBJECT', + }, + 'OPTION.THROUGH': { actions: () => remember('OFFSETDIST', 'THROUGH'), target: 'OBJECT' }, + 'OPTION.ERASE': { target: 'ERASE' }, + ESC: end, + }, + }, + ERASE: { + meta: { kind: 'keyword', prompt: ERASE_PROMPT }, + on: { + 'OPTION.YES': { actions: () => remember('OFFSETERASE', true), target: 'DISTANCE' }, + 'OPTION.NO': { actions: () => remember('OFFSETERASE', false), target: 'DISTANCE' }, + ESC: end, + }, + }, + OBJECT: { + meta: { kind: 'entity', prompt: OBJECT_PROMPT }, + entry: () => setShouldDrawHelpers(false), + on: { + MOUSE_CLICK: { + guard: ({ event }: Ctx) => !!pickEntityAt((event as MouseClickEvent).worldMouseLocation), + actions: assign(({ event }: Ctx) => { + const id = pickEntityAt((event as MouseClickEvent).worldMouseLocation)?.id ?? null; + return { sourceId: id, pickedId: id }; + }), + target: 'SIDE', + }, + 'OPTION.EXIT': end, + 'OPTION.UNDO': { actions: undoLast }, + ENTER: end, + ESC: end, + }, + }, + SIDE: { + meta: { kind: 'point', prompt: SIDE_PROMPT }, + entry: () => setShouldDrawHelpers(true), + on: { + DRAW: { + actions: ({ context, event }: Ctx) => { + const source = sourceOf(context); + const preview = + source && offsetThrough(source, getPointFromEvent(null, event as DrawEvent)); + setGhostHelperEntities(preview ? [preview] : []); + }, + }, + MOUSE_CLICK: { actions: placeOffset, target: 'PLACED' }, + ABSOLUTE_POINT_INPUT: { actions: placeOffset, target: 'PLACED' }, + 'OPTION.EXIT': end, + 'OPTION.MULTIPLE': { actions: assign({ multiple: true }) }, + 'OPTION.UNDO': { actions: undoLast, target: 'OBJECT' }, + ENTER: [{ guard: ({ context }: Ctx) => context.multiple, ...toObject }, end], + ESC: end, + }, + }, + /** 다중이면 같은 물음 · 아니면 객체로 */ + PLACED: { + always: [{ guard: ({ context }: Ctx) => !!context.sourceId, target: 'SIDE' }, toObject], + }, + [END_STATE]: { type: 'final' }, +}; + +export const offsetToolStateMachine = createMachine({ + types: {} as { context: OffsetContext; events: StateEvent }, + context: { sourceId: null, pickedId: null, multiple: false, made: 0, type: Tool.OFFSET }, + initial: 'INIT', + states, +}); diff --git a/B07_DesignDetail/openwebcad/src/tools/modify/structure-tools.ts b/B07_DesignDetail/openwebcad/src/tools/modify/structure-tools.ts index b49e1bd3f..a5535b388 100644 --- a/B07_DesignDetail/openwebcad/src/tools/modify/structure-tools.ts +++ b/B07_DesignDetail/openwebcad/src/tools/modify/structure-tools.ts @@ -1,35 +1,27 @@ /** 연장·결합·분해·지우기 (조사표 2절) */ import { toast } from 'react-toastify'; +import { SELECT_OBJECTS } from '../../commands/prompt'; import type { Entity } from '../../entities/Entity'; import { addEntities, deleteEntities, setSelectedEntityIds } from '../../state'; import { Tool } from '../../tools'; import { createSequenceTool } from '../factories/sequence-tool'; -import { explodeEntity, extendLineToBoundary, joinEntities } from './modify.helpers'; +import { explodeEntity, joinEntities } from './modify.helpers'; -export const extendToolStateMachine = createSequenceTool({ - tool: Tool.EXTEND, - helpers: false, - steps: [ - { kind: 'entity', instructions: '경계로 쓸 객체를 선택하십시오.' }, - { kind: 'entity', instructions: '연장할 선을 선택하십시오.' }, - ], - commit: (input) => { - const boundary = input.entity(0); - const target = input.entity(1); - const extended = extendLineToBoundary(target, boundary); - if (!extended) { - toast.warn('경계와 만나도록 연장할 수 없습니다.'); - return; - } - deleteEntities([target], false); - addEntities([extended], true); - }, -}); +/** EXTEND 빠른 모드는 TRIM 과 한 머신 꼴(eraser-tool.ts) */ +export { extendToolStateMachine } from '../eraser-tool'; export const joinToolStateMachine = createSequenceTool({ tool: Tool.JOIN, helpers: false, - steps: [{ kind: 'selection', instructions: '결합할 객체를 선택한 뒤 ENTER.' }], + steps: [ + { + kind: 'selection', + prompt: { + text: '한 번에 결합할 원본 객체 또는 여러 객체 선택', + options: SELECT_OBJECTS.options, + }, + }, + ], commit: (input) => { const entities = input.entities(0); const joined = joinEntities(entities); diff --git a/resources/tester/cad/modify-acad.test.ts b/resources/tester/cad/modify-acad.test.ts new file mode 100644 index 000000000..844fafe23 --- /dev/null +++ b/resources/tester/cad/modify-acad.test.ts @@ -0,0 +1,374 @@ +/** + * 337 다-1 고치기 7 — OFFSET · FILLET · CHAMFER · TRIM · EXTEND · BREAK · JOIN 을 오토캐드 물음 차례 · 옵션 · 기본값 · + * 빈 Enter · Esc · 끝난 뒤 동작으로(설계 `docs/raw/조사/2026-10-15_337_CAD오토캐드조작_설계.md` 1-2 · 6-3 다 줄). + * 기억(OFFSETDIST · FILLETRAD · CHAMFERA/B · TRIMMODE)은 파일 안 세션 하나 — 시험 차례대로 이어짐. + * 실행: cd B07_DesignDetail/openwebcad && npx vitest run --dir ../../resources/tester/cad modify-acad + */ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +vi.mock("react-toastify", () => ({ + toast: Object.assign(vi.fn(), { error: vi.fn(), info: vi.fn(), warn: vi.fn(), success: vi.fn() }), +})); + +import { Tool } from "../../../B07_DesignDetail/openwebcad/src/tools"; +import { activeTool, click, entities, initIdle, key, promptLine, type } from "./helpers/cad-drive"; + +type Shape = { start: { x: number; y: number }; end: { x: number; y: number } }; +const enter = () => key("Enter"); +const r = (value: number) => Math.round(value * 1e4) / 1e4; +/** 선 하나 — 쉼에서 그리고 Esc 로 끝 */ +const line = (a: string, b: string) => { + type("l"); + type(a); + type(b); + key("Escape"); +}; +/** 선마다 [x1, y1, x2, y2] — 차례 무관(정렬) */ +const segs = () => + entities("Line") + .map((entity) => { + const { start, end } = entity.getShape() as unknown as Shape; + return [r(start.x), r(start.y), r(end.x), r(end.y)]; + }) + .sort((a, b) => a[0] - b[0] || a[1] - b[1] || a[2] - b[2] || a[3] - b[3]); +const ys = () => + segs() + .map((s) => s[1]) + .sort((a, b) => a - b); + +beforeEach(() => { + initIdle(); +}); + +describe("OFFSET", () => { + it("처음 <통과점> · 통과점 · 되풀이 · 거리 기억 · T · 빈 Enter 끝", () => { + line("0,0", "100,0"); + type("o"); + expect(activeTool()).toBe(Tool.OFFSET); + expect(promptLine()).toBe("간격띄우기 거리 지정 또는 [통과점(T)/지우기(E)] <통과점>:"); + enter(); // <통과점> + expect(promptLine()).toBe("간격띄우기할 객체 선택 또는 [종료(E)/명령 취소(U)] <종료>:"); + click(50, 0); + expect(promptLine()).toBe("통과점 지정 또는 [종료(E)/다중(M)/명령 취소(U)] <종료>:"); + type("30,20"); + expect(ys()).toEqual([0, 20]); + expect(promptLine()).toBe("간격띄우기할 객체 선택 또는 [종료(E)/명령 취소(U)] <종료>:"); // 되풀이 + key("Escape"); + expect(activeTool()).toBe(Tool.SELECT); + + type("o"); + expect(promptLine()).toBe("간격띄우기 거리 지정 또는 [통과점(T)/지우기(E)] <통과점>:"); // 통과점 기억 + type("0"); + expect(activeTool()).toBe(Tool.OFFSET); // 0 거절 + type("5"); + click(50, 0); + expect(promptLine()).toBe( + "간격띄우기할 면의 점 지정 또는 [종료(E)/다중(M)/명령 취소(U)] <종료>:", + ); + type("50,-10"); + click(50, 20); + type("50,30"); + expect(ys()).toEqual([-5, 0, 20, 25]); + enter(); // <종료> + expect(activeTool()).toBe(Tool.SELECT); + + type("o"); + expect(promptLine()).toBe("간격띄우기 거리 지정 또는 [통과점(T)/지우기(E)] <5.0000>:"); + type("t"); + click(50, 25); + expect(promptLine()).toBe("통과점 지정 또는 [종료(E)/다중(M)/명령 취소(U)] <종료>:"); + type("e"); // 종료 + expect(activeTool()).toBe(Tool.SELECT); + type("o"); + expect(promptLine()).toBe("간격띄우기 거리 지정 또는 [통과점(T)/지우기(E)] <통과점>:"); + key("Escape"); + }); + + it("지우기(E) 예 · 다중(M) · 명령 취소(U) · 없는 옵션 L 안내", () => { + line("0,0", "100,0"); + type("o"); + type("l"); + expect(activeTool()).toBe(Tool.OFFSET); // 도면층(L) = 이 CAD 에 없음 + type("e"); + expect(promptLine()).toBe( + "간격띄우기 후 원본 객체를 지우시겠습니까? [예(Y)/아니오(N)] <아니오>:", + ); + type("y"); + type("10"); + click(50, 0); + type("50,5"); + expect(ys()).toEqual([10]); // 원본 지움 + key("Escape"); + + type("o"); + type("e"); + expect(promptLine()).toBe("간격띄우기 후 원본 객체를 지우시겠습니까? [예(Y)/아니오(N)] <예>:"); + type("n"); + enter(); // <10.0000> + click(50, 10); + type("m"); + expect(promptLine()).toBe("간격띄우기할 면의 점 지정 또는 [종료(E)/명령 취소(U)] <다음 객체>:"); + type("50,100"); + type("50,100"); + expect(ys()).toEqual([10, 20, 30]); // 방금 만든 것에서 되풀이 · 원본 둠 + type("u"); + expect(ys()).toEqual([10, 20]); + expect(promptLine()).toBe("간격띄우기할 객체 선택 또는 [종료(E)/명령 취소(U)] <종료>:"); + type("u"); + type("u"); // 이 명령에서 만든 것 없음 = 그대로 + expect(ys()).toEqual([10]); + enter(); + expect(activeTool()).toBe(Tool.SELECT); + }); +}); + +/** ㄱ 자 — 가로 (0,0)~(100,0) · 세로 (80,−20)~(80,100) · 구석 (80, 0) */ +const corner = () => { + line("0,0", "100,0"); + line("80,-20", "80,100"); +}; + +describe("FILLET", () => { + it("물음 글 · R 기억 · 쌍 하나 뒤 끝", () => { + corner(); + type("f"); + expect(promptLine()).toBe("첫 번째 객체 선택 또는 [명령 취소(U)/반지름(R)/자르기(T)/다중(M)]:"); + type("r"); + expect(promptLine()).toBe("모깎기 반지름 지정 <0.0000>:"); + type("10"); + expect(promptLine()).toBe("첫 번째 객체 선택 또는 [명령 취소(U)/반지름(R)/자르기(T)/다중(M)]:"); + click(30, 0); + expect(promptLine()).toBe( + "두 번째 객체 선택 또는 Shift 키를 누른 채 선택하여 구석 적용 또는 [반지름(R)]:", + ); + click(80, 60); + expect(segs()).toEqual([ + [0, 0, 70, 0], + [80, 100, 80, 10], + ]); + expect(entities("Arc")).toHaveLength(1); + expect(activeTool()).toBe(Tool.SELECT); + + type("f"); + type("r"); + expect(promptLine()).toBe("모깎기 반지름 지정 <10.0000>:"); + key("Escape"); + expect(activeTool()).toBe(Tool.SELECT); + }); + + it("다중(M) · Shift = 반지름 0 구석 · 명령 취소(U) · 자르기(T) 자르지 않기 · 빈 Enter 끝", () => { + corner(); + const before = segs(); + type("f"); + type("m"); + click(30, 0); + click(80, 60, { shiftKey: true }); + expect(segs()).toEqual([ + [0, 0, 80, 0], + [80, 100, 80, 0], + ]); + expect(entities("Arc")).toHaveLength(0); + expect(promptLine()).toBe("첫 번째 객체 선택 또는 [명령 취소(U)/반지름(R)/자르기(T)/다중(M)]:"); // 다중 되풀이 + type("u"); + expect(segs()).toEqual(before); + type("t"); + expect(promptLine()).toBe("자르기 모드 옵션 입력 [자르기(T)/자르지 않기(N)] <자르기>:"); + type("n"); + click(30, 0); + click(80, 60); + expect(segs()).toEqual(before); // 안 자름 + expect(entities("Arc")).toHaveLength(1); + enter(); + expect(activeTool()).toBe(Tool.SELECT); + + type("f"); + type("t"); + expect(promptLine()).toBe("자르기 모드 옵션 입력 [자르기(T)/자르지 않기(N)] <자르지 않기>:"); + type("t"); + key("Escape"); + }); +}); + +describe("CHAMFER", () => { + it("D 기억(둘째 기본 = 첫째) · 모따기 선 · 한 번 뒤 끝", () => { + corner(); + type("cha"); + expect(activeTool()).toBe(Tool.CHAMFER); + expect(promptLine()).toBe("첫 번째 선 선택 또는 [명령 취소(U)/거리(D)/자르기(T)/다중(M)]:"); + type("d"); + expect(promptLine()).toBe("첫 번째 모따기 거리 지정 <0.0000>:"); + type("10"); + expect(promptLine()).toBe("두 번째 모따기 거리 지정 <10.0000>:"); + enter(); + click(30, 0); + expect(promptLine()).toBe( + "두 번째 선 선택 또는 Shift 키를 누른 채 선택하여 구석 적용 또는 [거리(D)]:", + ); + click(80, 60); + expect(segs()).toEqual([ + [0, 0, 70, 0], + [70, 0, 80, 10], + [80, 100, 80, 10], + ]); + expect(activeTool()).toBe(Tool.SELECT); + + type("cha"); + type("d"); + expect(promptLine()).toBe("첫 번째 모따기 거리 지정 <10.0000>:"); + type("4"); + expect(promptLine()).toBe("두 번째 모따기 거리 지정 <4.0000>:"); + key("Escape"); + }); + + it("Shift = 거리 0 구석", () => { + corner(); + type("cha"); + click(30, 0); + click(80, 60, { shiftKey: true }); + expect(segs()).toEqual([ + [0, 0, 80, 0], + [80, 100, 80, 0], + ]); + }); +}); + +describe("TRIM", () => { + /** 가로 (0,0)~(100,0) · 세로 둘 x = 30 · 70 */ + const fence = () => { + line("0,0", "100,0"); + line("30,-20", "30,20"); + line("70,-20", "70,20"); + }; + + it("되풀이 · 반경 밖 무시 · U · 지우기(R) · 빈 Enter 끝", () => { + fence(); + type("tr"); + expect(activeTool()).toBe(Tool.ERASER); + expect(promptLine()).toBe( + "자를 객체 선택 또는 Shift 키를 누른 채 선택하여 연장 또는 [지우기(R)]:", + ); + click(50, 0); + expect(segs()).toContainEqual([0, 0, 30, 0]); + expect(segs()).toContainEqual([70, 0, 100, 0]); + expect(segs()).toHaveLength(4); + click(50, 50); // 집기 반경 밖 + expect(segs()).toHaveLength(4); + type("u"); + expect(segs()).toContainEqual([0, 0, 100, 0]); + click(50, 0); + click(30, 15); + expect(segs()).toContainEqual([30, -20, 30, 0]); + expect(activeTool()).toBe(Tool.ERASER); // 되풀이 + type("r"); + expect(promptLine()).toBe("지울 객체 선택:"); + click(70, 10); + enter(); + expect(segs().some((s) => s[0] === 70 && s[2] === 70)).toBe(false); + expect(promptLine()).toBe( + "자를 객체 선택 또는 Shift 키를 누른 채 선택하여 연장 또는 [지우기(R)]:", + ); + type("c"); + expect(activeTool()).toBe(Tool.ERASER); // 걸치기(C) = 이 CAD 에 없음 + enter(); + expect(activeTool()).toBe(Tool.SELECT); + }); + + it("Esc 한 번 = 끝 · Shift = 연장", () => { + line("0,0", "50,0"); + line("80,-20", "80,20"); + type("tr"); + click(40, 0, { shiftKey: true }); + expect(segs()).toContainEqual([0, 0, 80, 0]); + key("Escape"); + expect(activeTool()).toBe(Tool.SELECT); + }); + + it("사각형 변 = 원래 도면층 · 꼴(N9)", () => { + type("rec"); + type("0,0"); + type("100,40"); + key("Escape"); + const rectangle = entities("Rectangle")[0]; + rectangle.layerId = "L2"; + rectangle.lineColor = "#ff0000"; + line("20,-10", "20,50"); + type("tr"); + click(60, 40); + const sides = entities("Line").filter((entity) => entity.layerId === "L2"); + expect(sides.length).toBe(4); + expect(sides.every((entity) => entity.lineColor === "#ff0000")).toBe(true); + expect(segs()).toContainEqual([0, 40, 20, 40]); + }); +}); + +describe("EXTEND", () => { + it("빠른 연장 · 되풀이 · Shift = 자르기 · 빈 Enter 끝", () => { + line("0,0", "50,0"); + line("80,-20", "80,20"); + type("ex"); + expect(activeTool()).toBe(Tool.EXTEND); + expect(promptLine()).toBe("연장할 객체 선택 또는 Shift 키를 누른 채 선택하여 자르기:"); + click(40, 0); + expect(segs()).toContainEqual([0, 0, 80, 0]); + click(80, 10, { shiftKey: true }); + expect(segs()).toContainEqual([80, -20, 80, 0]); + expect(activeTool()).toBe(Tool.EXTEND); + enter(); + expect(activeTool()).toBe(Tool.SELECT); + }); +}); + +describe("BREAK", () => { + it("누른 점 = 첫 끊기점 · 둘째 점 · 한 번 뒤 끝", () => { + line("0,0", "100,0"); + type("br"); + expect(promptLine()).toBe("객체 선택:"); + click(20, 0); + expect(promptLine()).toBe("두 번째 끊기점 지정 또는 [첫 번째 점(F)]:"); + type("60,0"); + expect(segs()).toEqual([ + [0, 0, 20, 0], + [60, 0, 100, 0], + ]); + expect(activeTool()).toBe(Tool.SELECT); + }); + + it("첫 번째 점(F) · `@0,0` = 같은 점(틈 없이 둘)", () => { + line("0,0", "100,0"); + type("br"); + click(50, 0); + type("f"); + expect(promptLine()).toBe("첫 번째 끊기점 지정:"); + type("10,0"); + expect(promptLine()).toBe("두 번째 끊기점 지정:"); + type("30,0"); + expect(segs()).toEqual([ + [0, 0, 10, 0], + [30, 0, 100, 0], + ]); + + type("br"); + click(60, 0); + type("@0,0"); + expect(segs()).toEqual([ + [0, 0, 10, 0], + [30, 0, 60, 0], + [60, 0, 100, 0], + ]); + }); +}); + +describe("JOIN", () => { + it("고르기 물음 글 · 빈 Enter = 결합 · 한 번 뒤 끝", () => { + line("0,0", "40,0"); + line("40,0", "40,40"); + type("j"); + expect(promptLine()).toBe("한 번에 결합할 원본 객체 또는 여러 객체 선택:"); + click(20, 0); + click(40, 20); + enter(); + expect(entities("Line")).toHaveLength(0); + expect(entities("PolyLine")).toHaveLength(1); + expect(activeTool()).toBe(Tool.SELECT); + }); +}); From c15be5116a9320afa37dd094ca6d3884b7375f7e Mon Sep 17 00:00:00 2001 From: umsangdon Date: Fri, 16 Oct 2026 00:15:35 +0900 Subject: [PATCH 70/81] =?UTF-8?q?feat(B07=20CAD):=20337-4b=20=EB=8B=A4-2?= =?UTF-8?q?=20=EB=8A=98=EC=9D=B4=EA=B8=B0=20=C2=B7=20=EC=9E=AC=EA=B8=B0=20?= =?UTF-8?q?=C2=B7=20=EC=B9=98=EC=88=98=20=EC=98=A4=ED=86=A0=EC=BA=90?= =?UTF-8?q?=EB=93=9C=20=EC=A1=B0=EC=9E=91=20=E2=80=94=20LENGTHEN=20=C2=B7?= =?UTF-8?q?=20DIVIDE=20=C2=B7=20MEASURE=20=C2=B7=20DIST=20=C2=B7=20DIM=20?= =?UTF-8?q?=EB=A5=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - LENGTHEN 새 도구(lengthen-tool.ts) — 측정할 객체 [증분(DE)/퍼센트(P)/전체(T)] <마지막 방식> · 누름 = 현재 길이 알림 · 방식별 값 기억 · 변경할 객체 되풀이 · U = 이 명령 안 바꿈만 · 빈 Enter · Esc 끝 · 선 · 호 - DIVIDE · MEASURE — 기본값 없음 · 정수 2~32767 · 양수 · 블록(B) 없음 안내 · MEASURE 누른 끝부터 - DIST — 오토캐드 꼴 두 줄 기록(거리 · 각도 · 증분) · 다중 점(M) 없음 안내 - DIMLINEAR · DIMALIGNED — 빈 Enter = 객체 선택 · [문자(T)/수평(H)/수직(V)] · 치수 문자 = 기록 · 치수 객체 문자 바꿈 칸(<> = 잰 값) - DIMCONTINUE · DIMBASELINE — 되풀이 · 명령 취소(U) · 빈 Enter 끝 · DIMRADIUS · DIMDIAMETER [문자(T)] · 그 밖 치수 물음 글 - 시험 measure-acad 11 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_014utrjfoW831Vc5wJUd13kL --- .../src/commands/commands.modify.ts | 2 +- .../src/entities/MeasurementEntity.ts | 24 +- .../tools/annotate/dimension-radial-tools.ts | 132 +++--- .../src/tools/annotate/dimension-tools.ts | 375 ++++++++++++++---- .../openwebcad/src/tools/draw/divide-tools.ts | 33 +- .../src/tools/modify/lengthen-tool.ts | 212 ++++++++++ .../src/tools/utility/inquiry-tools.ts | 29 +- resources/tester/cad/measure-acad.test.ts | 363 +++++++++++++++++ 8 files changed, 1022 insertions(+), 148 deletions(-) create mode 100644 B07_DesignDetail/openwebcad/src/tools/modify/lengthen-tool.ts create mode 100644 resources/tester/cad/measure-acad.test.ts diff --git a/B07_DesignDetail/openwebcad/src/commands/commands.modify.ts b/B07_DesignDetail/openwebcad/src/commands/commands.modify.ts index 14e078a66..7dd784897 100644 --- a/B07_DesignDetail/openwebcad/src/commands/commands.modify.ts +++ b/B07_DesignDetail/openwebcad/src/commands/commands.modify.ts @@ -17,6 +17,7 @@ import { chamferToolStateMachine, filletToolStateMachine, } from '../tools/modify/corner-tools'; +import { lengthenToolStateMachine } from '../tools/modify/lengthen-tool'; import { drawOrderBackToolStateMachine, drawOrderFrontToolStateMachine, @@ -34,7 +35,6 @@ import { } from '../tools/modify/structure-tools'; import { alignToolStateMachine, - lengthenToolStateMachine, mirrorToolStateMachine, offsetToolStateMachine, stretchToolStateMachine, diff --git a/B07_DesignDetail/openwebcad/src/entities/MeasurementEntity.ts b/B07_DesignDetail/openwebcad/src/entities/MeasurementEntity.ts index 08504b3b9..36f3e211b 100644 --- a/B07_DesignDetail/openwebcad/src/entities/MeasurementEntity.ts +++ b/B07_DesignDetail/openwebcad/src/entities/MeasurementEntity.ts @@ -51,6 +51,8 @@ export class MeasurementEntity implements Entity { private startPoint: Point; private endPoint: Point; private offsetPoint: Point; + /** 치수 문자 바꿈(DIMLINEAR 문자(T)) — `<>` = 잰 값 · 없으면 잰 값 */ + public textOverride?: string; constructor(layerId: string, startPoint: Point, endPoint: Point, offsetPoint: Point) { this.layerId = layerId; @@ -75,6 +77,17 @@ export class MeasurementEntity implements Entity { this.offsetPoint = point; } + /** 잰 값(치수 소수 자릿수) */ + public getMeasuredText(): string { + return String(round(pointDistance(this.startPoint, this.endPoint), getDimDecimals())); + } + + /** 보이는 치수 문자 */ + public getLabel(): string { + const measured = this.getMeasuredText(); + return this.textOverride ? this.textOverride.replaceAll('<>', measured) : measured; + } + public getDrawPoints() { // Return if measurement is zero length if (isPointEqual(this.startPoint, this.endPoint)) { @@ -301,7 +314,7 @@ export class MeasurementEntity implements Entity { drawController.drawLine(offsetStartPointMargin, offsetStartPointExtend); drawController.drawLine(offsetEndPointMargin, offsetEndPointExtend); - const distance = String(round(pointDistance(this.startPoint, this.endPoint), getDimDecimals())); + const distance = this.getLabel(); const originalTextDirection = normalUnit.rotate90CW(); let finalTextDirection = originalTextDirection; if ( @@ -343,12 +356,14 @@ export class MeasurementEntity implements Entity { } public clone(): MeasurementEntity { - return new MeasurementEntity( + const copy = new MeasurementEntity( getActiveLayerId(), this.startPoint.clone(), this.endPoint.clone(), this.offsetPoint.clone() ); + copy.textOverride = this.textOverride; + return copy; } public intersectsWithBox(box: Box): boolean { @@ -416,7 +431,7 @@ export class MeasurementEntity implements Entity { ]; // Calculate text properties - const distance = String(round(pointDistance(this.startPoint, this.endPoint), getDimDecimals())); + const distance = this.getLabel(); const worldFactor = annotationWorldFactor(); const textHeight = getDimTextHeight() / worldFactor; // Estimate width: textString.length * fontSize * aspectRatioFactor @@ -582,6 +597,7 @@ export class MeasurementEntity implements Entity { startPoint: { x: this.startPoint.x, y: this.startPoint.y }, endPoint: { x: this.endPoint.x, y: this.endPoint.y }, offsetPoint: { x: this.offsetPoint.x, y: this.offsetPoint.y }, + ...(this.textOverride ? { textOverride: this.textOverride } : {}), }, }; } @@ -611,6 +627,7 @@ export class MeasurementEntity implements Entity { measurementEntity.lineColor = jsonEntity.lineColor; measurementEntity.lineWidth = jsonEntity.lineWidth; measurementEntity.lineDash = jsonEntity.lineDash; + measurementEntity.textOverride = jsonEntity.shapeData.textOverride; return measurementEntity; } } @@ -619,4 +636,5 @@ export interface MeasurementJsonData { startPoint: { x: number; y: number }; endPoint: { x: number; y: number }; offsetPoint: { x: number; y: number }; + textOverride?: string; } diff --git a/B07_DesignDetail/openwebcad/src/tools/annotate/dimension-radial-tools.ts b/B07_DesignDetail/openwebcad/src/tools/annotate/dimension-radial-tools.ts index 174c26035..3776892b5 100644 --- a/B07_DesignDetail/openwebcad/src/tools/annotate/dimension-radial-tools.ts +++ b/B07_DesignDetail/openwebcad/src/tools/annotate/dimension-radial-tools.ts @@ -1,6 +1,12 @@ -/** 반지름·지름·각도·호길이·세로좌표 치수와 중심 표식 (조사표 5절) */ +/** + * 반지름·지름·각도·호길이·세로좌표 치수와 중심 표식 (조사표 5절). + * 오토캐드 물음 차례(337 설계 1-2) — DIMRADIUS · DIMDIAMETER = 「호 또는 원 선택:」 → + * 「치수선의 위치 지정 또는 [문자(T)]:」 · 한 번 뒤 끝 · 그 밖 치수 = 물음 글만. + */ import { Arc, Circle, Point } from '@flatten-js/core'; import { toast } from 'react-toastify'; +import { OPT, type OptionSpec } from '../../commands/prompt'; +import { logLine } from '../../commands/run-command'; import { getDimDecimals } from '../../commands/dim-settings'; import type { Entity } from '../../entities/Entity'; import { MeasurementEntity } from '../../entities/MeasurementEntity'; @@ -8,7 +14,7 @@ import { polylineLength, sampleEntityPoints } from '../../helpers/geometry/sampl import { addEntities, getEntities, setEntities, setSelectedEntityIds } from '../../state'; import { Tool } from '../../tools'; import { arcEntity, lineEntity } from '../factories/entity-factory'; -import { createSequenceTool } from '../factories/sequence-tool'; +import { createSequenceTool, type SequenceInput } from '../factories/sequence-tool'; import { annotationText, arrowHead, dimensionText } from './annotation.helpers'; interface CircularShape { @@ -24,7 +30,24 @@ function circularOf(entity: Entity): CircularShape | null { return null; } -function radialDimension(entity: Entity, textPoint: Point, diameter: boolean): Entity[] | null { +/** 잰 값 글(「R10.00」 · 「Ø20.00」) */ +function radialLabel(entity: Entity, diameter: boolean): string | null { + const circular = circularOf(entity); + if (!circular) return null; + const value = diameter ? circular.radius * 2 : circular.radius; + return `${diameter ? 'Ø' : 'R'}${value.toFixed(getDimDecimals())}`; +} + +/** 바꾼 글(`<>` = 잰 값) · 없으면 잰 값 */ +const labelWith = (measured: string, text?: string) => + text && text !== '<>' ? text.replaceAll('<>', measured) : measured; + +function radialDimension( + entity: Entity, + textPoint: Point, + diameter: boolean, + text?: string +): Entity[] | null { const circular = circularOf(entity); if (!circular) return null; @@ -43,52 +66,71 @@ function radialDimension(entity: Entity, textPoint: Point, diameter: boolean): E return [ lineEntity(start, textPoint), arrowHead(edge, circular.center), - dimensionText( - diameter ? circular.radius * 2 : circular.radius, - textPoint, - diameter ? 'Ø' : 'R' - ), + text && text !== '<>' + ? annotationText(labelWith(radialLabel(entity, diameter) as string, text), textPoint) + : dimensionText( + diameter ? circular.radius * 2 : circular.radius, + textPoint, + diameter ? 'Ø' : 'R' + ), ]; } -export const dimRadiusToolStateMachine = createSequenceTool({ - tool: Tool.DIMRADIUS, - steps: [ - { kind: 'entity', instructions: '반지름을 기입할 원 또는 호를 선택하십시오.' }, - { kind: 'point', instructions: '치수 문자 위치를 지정하십시오.' }, - ], - commit: (input) => { - const parts = radialDimension(input.entity(0), input.point(1), false); - if (!parts) { - toast.warn('원 또는 호를 선택하십시오.'); - return; - } - addEntities(parts, true); - }, -}); +const MTEXT: OptionSpec = { key: 'MTEXT', kw: 'M', en: 'MTEXT', label: '여러 줄 문자' }; +const ANGLE: OptionSpec = { key: 'ANGLE', kw: 'A', en: 'ANGLE', label: '각도' }; -export const dimDiameterToolStateMachine = createSequenceTool({ - tool: Tool.DIMDIAMETER, - steps: [ - { kind: 'entity', instructions: '지름을 기입할 원 또는 호를 선택하십시오.' }, - { kind: 'point', instructions: '치수 문자 위치를 지정하십시오.' }, - ], - commit: (input) => { - const parts = radialDimension(input.entity(0), input.point(1), true); - if (!parts) { - toast.warn('원 또는 호를 선택하십시오.'); - return; - } - addEntities(parts, true); - }, -}); +/** DIMRADIUS · DIMDIAMETER — 원 · 호 → [문자(T)] → 위치 */ +function radialTool(tool: Tool, diameter: boolean) { + const measured = (input: SequenceInput) => + input.has('obj') ? radialLabel(input.get('obj'), diameter) : null; + return createSequenceTool({ + tool, + steps: [ + { kind: 'entity', id: 'obj', prompt: { text: '호 또는 원 선택' } }, + { + kind: 'text', + id: 'txt', + when: (input) => input.mode === 'text', + next: 'place', + defaultValue: '<>', + prompt: (input) => { + const value = measured(input); + return { + text: '치수 문자 입력', + ...(value === null ? {} : { default: { kind: 'enter' as const, label: value } }), + }; + }, + }, + { + kind: 'point', + id: 'place', + prompt: { text: '치수선의 위치 지정', options: [OPT.TEXT], absent: [MTEXT, ANGLE] }, + options: { TEXT: { mode: 'text', next: 'txt' } }, + }, + ], + commit: (input) => { + const entity = input.get('obj'); + const text = input.has('txt') ? input.get('txt') : undefined; + const parts = radialDimension(entity, input.get('place'), diameter, text); + if (!parts) { + toast.warn('원 또는 호를 선택하십시오.'); + return; + } + addEntities(parts, true); + logLine(`치수 문자 = ${labelWith(radialLabel(entity, diameter) as string, text)}`); + }, + }); +} + +export const dimRadiusToolStateMachine = radialTool(Tool.DIMRADIUS, false); +export const dimDiameterToolStateMachine = radialTool(Tool.DIMDIAMETER, true); export const dimAngularToolStateMachine = createSequenceTool({ tool: Tool.DIMANGULAR, steps: [ - { kind: 'point', instructions: '각의 꼭짓점을 지정하십시오.' }, - { kind: 'point', instructions: '첫 번째 변 위의 점을 지정하십시오.' }, - { kind: 'point', instructions: '두 번째 변 위의 점을 지정하십시오.' }, + { kind: 'point', prompt: { text: '각도 정점 지정' } }, + { kind: 'point', prompt: { text: '첫 번째 각도 끝점 지정' } }, + { kind: 'point', prompt: { text: '두 번째 각도 끝점 지정' } }, ], commit: (input) => { const [vertex, first, second] = input.points(); @@ -128,8 +170,8 @@ export const dimAngularToolStateMachine = createSequenceTool({ export const dimArcToolStateMachine = createSequenceTool({ tool: Tool.DIMARC, steps: [ - { kind: 'entity', instructions: '호 길이를 기입할 호를 선택하십시오.' }, - { kind: 'point', instructions: '치수 문자 위치를 지정하십시오.' }, + { kind: 'entity', prompt: { text: '호 또는 폴리선 호 세그먼트 선택' } }, + { kind: 'point', prompt: { text: '호 길이 치수 위치 지정' } }, ], commit: (input) => { const entity = input.entity(0); @@ -146,8 +188,8 @@ export const dimArcToolStateMachine = createSequenceTool({ export const dimOrdinateToolStateMachine = createSequenceTool({ tool: Tool.DIMORDINATE, steps: [ - { kind: 'point', instructions: '좌표를 기입할 피처 위치를 지정하십시오.' }, - { kind: 'point', instructions: '지시선 끝점을 지정하십시오.' }, + { kind: 'point', prompt: { text: '피쳐 위치 지정' } }, + { kind: 'point', prompt: { text: '지시선 끝점 지정' } }, ], commit: (input) => { const [feature, leaderEnd] = input.points(); diff --git a/B07_DesignDetail/openwebcad/src/tools/annotate/dimension-tools.ts b/B07_DesignDetail/openwebcad/src/tools/annotate/dimension-tools.ts index c29a53dae..02c0a5c51 100644 --- a/B07_DesignDetail/openwebcad/src/tools/annotate/dimension-tools.ts +++ b/B07_DesignDetail/openwebcad/src/tools/annotate/dimension-tools.ts @@ -1,27 +1,55 @@ -/** 선형 치수 계열 — 선형·정렬·기준선·연속·빠른 치수·자동 치수 (조사표 5절) */ +/** + * 선형 치수 계열 — 선형·정렬·기준선·연속·빠른 치수·자동 치수 (조사표 5절). + * 오토캐드 물음 차례(337 설계 1-2) — DIMLINEAR · DIMALIGNED = 원점 둘(빈 Enter = 객체 선택) → 치수선 위치 + * [문자(T)/수평(H)/수직(V)] · 한 번 뒤 끝 · DIMCONTINUE · DIMBASELINE = 원점 되풀이 [명령 취소(U)] · 빈 Enter 끝. + */ import { Circle, Point } from '@flatten-js/core'; import { toast } from 'react-toastify'; -import { EntityName } from '../../entities/Entity'; +import { assign, createMachine } from 'xstate'; +import { type OptionSpec, type PromptSpec, OPT, SELECT_OBJECTS } from '../../commands/prompt'; +import { logLine } from '../../commands/run-command'; +import { EntityName, type Entity } from '../../entities/Entity'; import { MeasurementEntity } from '../../entities/MeasurementEntity'; +import type { PolyLineEntity } from '../../entities/PolyLineEntity'; import { sampleEntityPoints } from '../../helpers/geometry/sample-entity'; -import { addEntities, getActiveLayerId, setSelectedEntityIds } from '../../state'; +import { getPointFromEvent } from '../../helpers/get-point-from-event'; +import { notice } from '../../inputController/prompt-input'; +import { + addEntities, + getActiveLayerId, + setAngleGuideOriginPoint, + setSelectedEntityIds, + setShouldDrawHelpers, + undo, +} from '../../state'; import { Tool } from '../../tools'; import { styled } from '../factories/entity-factory'; -import { createSequenceTool } from '../factories/sequence-tool'; +import { createSequenceTool, type SequenceInput } from '../factories/sequence-tool'; +import { END_STATE, type PointInputEvent, type StateEvent, type ToolContext } from '../tool.types'; import { getLastDimension, rememberDimension } from './annotation.helpers'; /** 치수 객체 하나 만들기 — 현재 선 특성을 입히고 직전 치수로 기억한다 */ -function makeDimension(start: Point, end: Point, offset: Point): MeasurementEntity { +function makeDimension(start: Point, end: Point, offset: Point, text?: string): MeasurementEntity { const dimension = styled(new MeasurementEntity(getActiveLayerId(), start, end, offset)); + if (text && text !== '<>') dimension.textOverride = text; rememberDimension(dimension); return dimension; } +type Fixed = 'H' | 'V' | null; + /** - * 선형 치수 — 치수선을 놓은 방향으로 수평/수직을 고른다. + * 선형 치수 — 수평(H) · 수직(V) 고정이면 그 방향, 아니면 치수선을 놓은 방향으로 고른다. * 위·아래에 놓으면 가로 거리, 좌·우에 놓으면 세로 거리를 잰다 (AutoCAD와 같다). */ -function projectForLinear(start: Point, end: Point, offset: Point): [Point, Point] { +function projectForLinear( + start: Point, + end: Point, + offset: Point, + fixed: Fixed = null +): [Point, Point] { + if (fixed === 'H') return [start, new Point(end.x, start.y)]; + if (fixed === 'V') return [start, new Point(start.x, end.y)]; const horizontalSpan = Math.abs(end.x - start.x); const verticalSpan = Math.abs(end.y - start.y); const offsetIsVertical = @@ -32,89 +60,268 @@ function projectForLinear(start: Point, end: Point, offset: Point): [Point, Poin return [start, new Point(start.x, end.y)]; } -export const dimLinearToolStateMachine = createSequenceTool({ - tool: Tool.DIMLINEAR, - steps: [ - { kind: 'point', instructions: '첫 번째 치수보조선 원점을 지정하십시오.' }, - { kind: 'point', instructions: '두 번째 치수보조선 원점을 지정하십시오.' }, - { kind: 'point', instructions: '치수선 위치를 지정하십시오.' }, - ], - preview: (input) => { - const points = input.points(); - if (points.length !== 2) return []; - const [start, end] = projectForLinear(points[0], points[1], input.cursor); - return [new MeasurementEntity(getActiveLayerId(), start, end, input.cursor)]; - }, - commit: (input) => { - const [first, second, offset] = input.points(); - const [start, end] = projectForLinear(first, second, offset); - addEntities([makeDimension(start, end, offset)], true); - }, -}); - -export const dimAlignedToolStateMachine = createSequenceTool({ - tool: Tool.DIMALIGNED, - steps: [ - { kind: 'point', instructions: '첫 번째 치수보조선 원점을 지정하십시오.' }, - { kind: 'point', instructions: '두 번째 치수보조선 원점을 지정하십시오.' }, - { kind: 'point', instructions: '치수선 위치를 지정하십시오.' }, - ], - preview: (input) => { - const points = input.points(); - if (points.length !== 2) return []; - return [new MeasurementEntity(getActiveLayerId(), points[0], points[1], input.cursor)]; - }, - commit: (input) => { - const [start, end, offset] = input.points(); - addEntities([makeDimension(start, end, offset)], true); - }, -}); - -export const dimBaselineToolStateMachine = createSequenceTool({ - tool: Tool.DIMBASELINE, - steps: [{ kind: 'point', instructions: '다음 치수보조선 원점을 지정하십시오.' }], - commit: (input) => { - const previous = getLastDimension(); - if (!previous) { - toast.warn('먼저 치수를 하나 작성하십시오.'); - return; - } - const start = previous.getStartPoint(); - const offset = previous.getOffsetPoint(); - // 기준선 치수는 같은 시작점에서 재고, 치수선을 한 칸 더 띄운다 - const spacing = offset.distanceTo(start)[0] * 0.35 || 10; - const direction = new Point(offset.x - start.x, offset.y - start.y); - const length = Math.hypot(direction.x, direction.y) || 1; - const nextOffset = new Point( - offset.x + (direction.x / length) * spacing, - offset.y + (direction.y / length) * spacing +/** 객체 선택 — 선 · 폴리선(누른 토막) · 그 밖은 처음 · 끝 점 */ +function endsOf(entity: Entity, pick: Point): [Point, Point] | null { + let target = entity; + if (entity.getType() === EntityName.PolyLine) { + const pieces = (entity as PolyLineEntity).getEntities(); + target = pieces.reduce( + (best, piece) => + (piece.getShape()?.distanceTo(pick)[0] ?? Number.POSITIVE_INFINITY) < + (best.getShape()?.distanceTo(pick)[0] ?? Number.POSITIVE_INFINITY) + ? piece + : best, + pieces[0] ?? entity ); - addEntities([makeDimension(start, input.point(0), nextOffset)], true); - }, -}); + } + const points = sampleEntityPoints(target); + if (points.length < 2) return null; + return [points[0], points[points.length - 1]]; +} -export const dimContinueToolStateMachine = createSequenceTool({ - tool: Tool.DIMCONTINUE, - steps: [{ kind: 'point', instructions: '다음 치수보조선 원점을 지정하십시오.' }], - commit: (input) => { - const previous = getLastDimension(); - if (!previous) { - toast.warn('먼저 치수를 하나 작성하십시오.'); - return; - } - addEntities( - [makeDimension(previous.getEndPoint(), input.point(0), previous.getOffsetPoint())], - true - ); - }, +const option = (key: string, kw: string, label: string): OptionSpec => ({ + key, + kw, + en: key, + label, }); +/** 빈 Enter = 객체 선택 — 숨은 옵션(그림은 <객체 선택>) */ +const OBJECT = { ...option('OBJECT', 'OBJECT', '객체 선택'), hidden: true as const }; +const HORIZONTAL = option('HORIZONTAL', 'H', '수평'); +const VERTICAL = option('VERTICAL', 'V', '수직'); +const MTEXT = option('MTEXT', 'M', '여러 줄 문자'); +const ANGLE = option('ANGLE', 'A', '각도'); +const ROTATED = option('ROTATED', 'R', '회전됨'); +const SELECT = option('SELECT', 'S', '선택'); + +/** 원점 둘 또는 객체 끝 */ +function originsOf(input: SequenceInput): [Point, Point] | null { + if (input.has('obj')) { + const entity = input.get('obj'); + return endsOf(entity, input.pick(input.values.indexOf(entity))); + } + if (!input.has('p2')) return null; + return [input.get('p1'), input.get('p2')]; +} + +/** 수평 · 수직 고정 — 명령 하나 동안(확정 · Esc 에서 풂) */ +let fixed: Fixed = null; + +/** DIMLINEAR · DIMALIGNED 공통 물음 */ +function linearSteps(tool: Tool, aligned: boolean) { + const measure = (input: SequenceInput, offset: Point): [Point, Point] | null => { + const origins = originsOf(input); + if (!origins) return null; + return aligned ? origins : projectForLinear(origins[0], origins[1], offset, fixed); + }; + const measured = (input: SequenceInput): string | null => { + const origins = originsOf(input); + if (!origins) return null; + const [a, b] = origins; + const wide = Math.abs(b.x - a.x) >= Math.abs(b.y - a.y); + const [start, end] = aligned ? origins : projectForLinear(a, b, b, fixed ?? (wide ? 'H' : 'V')); + return new MeasurementEntity(getActiveLayerId(), start, end, end).getMeasuredText(); + }; + return createSequenceTool({ + tool, + steps: [ + { + kind: 'point', + id: 'p1', + prompt: { + text: '첫 번째 치수보조선 원점 지정 또는', + options: [OBJECT], + default: { kind: 'option', key: 'OBJECT', label: '객체 선택' }, + }, + options: { OBJECT: { mode: 'object', next: 'obj' } }, + }, + { + kind: 'entity', + id: 'obj', + when: (input) => input.mode === 'object' && !input.has('obj'), + next: 'place', + prompt: { text: '치수 기입할 객체 선택' }, + }, + { + kind: 'point', + id: 'p2', + when: (input) => !input.has('obj'), + prompt: { text: '두 번째 치수보조선 원점 지정' }, + }, + { + kind: 'text', + id: 'txt', + when: (input) => input.mode === 'text', + next: 'place', + defaultValue: '<>', + prompt: (input) => { + const value = measured(input); + return { + text: '치수 문자 입력', + ...(value === null ? {} : { default: { kind: 'enter' as const, label: value } }), + }; + }, + }, + { + kind: 'point', + id: 'place', + prompt: { + text: '치수선의 위치 지정', + options: aligned ? [OPT.TEXT] : [OPT.TEXT, HORIZONTAL, VERTICAL], + absent: aligned ? [MTEXT, ANGLE] : [MTEXT, ANGLE, ROTATED], + }, + options: { + TEXT: { mode: 'text', next: 'txt' }, + ...(aligned + ? {} + : { + HORIZONTAL: { + run: () => { + fixed = 'H'; + }, + }, + VERTICAL: { + run: () => { + fixed = 'V'; + }, + }, + }), + }, + }, + ], + preview: (input) => { + if (input.has('place') || !originsOf(input)) return []; + const span = measure(input, input.cursor); + if (!span) return []; + const ghost = new MeasurementEntity(getActiveLayerId(), span[0], span[1], input.cursor); + if (input.has('txt')) ghost.textOverride = input.get('txt'); + return [ghost]; + }, + commit: (input) => { + const offset = input.get('place'); + const span = measure(input, offset); + fixed = null; + if (!span) { + toast.warn('치수를 넣을 수 없는 객체입니다.'); + return; + } + const dimension = makeDimension( + span[0], + span[1], + offset, + input.has('txt') ? input.get('txt') : undefined + ); + addEntities([dimension], true); + logLine(`치수 문자 = ${dimension.getLabel()}`); + }, + onCancel: () => { + fixed = null; + }, + }); +} + +export const dimLinearToolStateMachine = linearSteps(Tool.DIMLINEAR, false); +export const dimAlignedToolStateMachine = linearSteps(Tool.DIMALIGNED, true); + +/** 기준선 치수는 같은 시작점에서 재고, 치수선을 한 칸 더 띄운다 */ +function baselineDimension(previous: MeasurementEntity, point: Point): MeasurementEntity { + const start = previous.getStartPoint(); + const offset = previous.getOffsetPoint(); + const spacing = offset.distanceTo(start)[0] * 0.35 || 10; + const direction = new Point(offset.x - start.x, offset.y - start.y); + const length = Math.hypot(direction.x, direction.y) || 1; + const nextOffset = new Point( + offset.x + (direction.x / length) * spacing, + offset.y + (direction.y / length) * spacing + ); + return makeDimension(start, point, nextOffset); +} + +const continueDimension = (previous: MeasurementEntity, point: Point): MeasurementEntity => + makeDimension(previous.getEndPoint(), point, previous.getOffsetPoint()); + +interface ChainContext extends ToolContext { + /** 명령 시작 때 직전 치수 · 이 명령이 만든 치수(U 가 뒤에서 뺌) */ + first: MeasurementEntity | null; + made: MeasurementEntity[]; +} +type ChainCtx = { context: ChainContext; event: StateEvent }; + +const CHAIN_PROMPT: PromptSpec = { + text: '두 번째 치수보조선 원점 지정', + options: [OPT.UNDO], + absent: [SELECT], +}; + +/** DIMCONTINUE · DIMBASELINE — 앞 치수에 이어 원점 되풀이 · U = 이 명령 안 마지막 치수 · 빈 Enter · Esc = 끝 */ +function chainTool( + tool: Tool, + build: (previous: MeasurementEntity, point: Point) => MeasurementEntity +) { + const base = (context: ChainContext) => context.made[context.made.length - 1] ?? context.first; + const hasBase = ({ context }: ChainCtx) => !!base(context); + const place = { + guard: hasBase, + actions: assign(({ context, event }: ChainCtx) => { + const previous = base(context) as MeasurementEntity; + const point = getPointFromEvent(previous.getEndPoint(), event as PointInputEvent); + const dimension = build(previous, point); + addEntities([dimension], true); + logLine(`치수 문자 = ${dimension.getLabel()}`); + setAngleGuideOriginPoint(dimension.getEndPoint()); + return { made: [...context.made, dimension] }; + }), + }; + const noBase = { + target: END_STATE, + actions: () => notice('먼저 치수를 하나 작성하십시오.'), + }; + return createMachine({ + types: {} as { context: ChainContext; events: StateEvent }, + context: () => ({ type: tool, first: getLastDimension(), made: [] }), + initial: 'PICK', + states: { + PICK: { + meta: { kind: 'point', prompt: CHAIN_PROMPT }, + entry: ({ context }: ChainCtx) => { + setShouldDrawHelpers(true); + setAngleGuideOriginPoint(base(context)?.getEndPoint() ?? null); + }, + on: { + MOUSE_CLICK: [place, noBase], + ABSOLUTE_POINT_INPUT: [place, noBase], + RELATIVE_POINT_INPUT: [place, noBase], + NUMBER_INPUT: [place, noBase], + 'OPTION.UNDO': { + actions: assign(({ context }: ChainCtx) => { + if (!context.made.length) return {}; + undo(); + const made = context.made.slice(0, -1); + const previous = made[made.length - 1] ?? context.first; + if (previous) rememberDimension(previous); + setAngleGuideOriginPoint(previous?.getEndPoint() ?? null); + return { made }; + }), + }, + ENTER: END_STATE, + ESC: END_STATE, + }, + }, + [END_STATE]: { type: 'final' }, + // biome-ignore lint/suspicious/noExplicitAny: 손으로 짠 상태 — 사건마다 assign 꼴이 달라 풀어 둠(sequence-tool 과 같음) + } as Record, + }); +} + +export const dimBaselineToolStateMachine = chainTool(Tool.DIMBASELINE, baselineDimension); +export const dimContinueToolStateMachine = chainTool(Tool.DIMCONTINUE, continueDimension); export const qDimToolStateMachine = createSequenceTool({ tool: Tool.QDIM, helpers: false, steps: [ - { kind: 'selection', instructions: '치수를 넣을 객체를 선택한 뒤 ENTER.' }, - { kind: 'point', instructions: '치수선 위치를 지정하십시오.' }, + { kind: 'selection', prompt: { ...SELECT_OBJECTS, text: '치수 기입할 형상 선택' } }, + { kind: 'point', prompt: { text: '치수선의 위치 지정' } }, ], commit: (input) => { const offset = input.point(1); @@ -140,8 +347,8 @@ export const dimAutoToolStateMachine = createSequenceTool({ tool: Tool.DIM, helpers: false, steps: [ - { kind: 'entity', instructions: '치수를 넣을 객체를 선택하십시오.' }, - { kind: 'point', instructions: '치수선 위치를 지정하십시오.' }, + { kind: 'entity', prompt: { text: '치수 기입할 객체 선택' } }, + { kind: 'point', prompt: { text: '치수선의 위치 지정' } }, ], commit: (input) => { const entity = input.entity(0); diff --git a/B07_DesignDetail/openwebcad/src/tools/draw/divide-tools.ts b/B07_DesignDetail/openwebcad/src/tools/draw/divide-tools.ts index 19ddd7c3c..8cd40d337 100644 --- a/B07_DesignDetail/openwebcad/src/tools/draw/divide-tools.ts +++ b/B07_DesignDetail/openwebcad/src/tools/draw/divide-tools.ts @@ -1,5 +1,9 @@ -/** 등분(DIVIDE)·길이분할(MEASURE) — 객체를 자르지 않고 점만 놓는다 */ +/** + * 등분(DIVIDE)·길이분할(MEASURE) — 객체를 자르지 않고 점만 놓는다. + * 오토캐드 물음 차례(337 설계 1-2) · 기본값 없음 · 한 번 뒤 끝 · 블록(B) 없음. + */ import { toast } from 'react-toastify'; +import type { OptionSpec } from '../../commands/prompt'; import { dividePoints, measurePoints, @@ -10,12 +14,21 @@ import { Tool } from '../../tools'; import { pointEntity } from '../factories/entity-factory'; import { createSequenceTool } from '../factories/sequence-tool'; +const BLOCK: OptionSpec = { key: 'BLOCK', kw: 'B', en: 'BLOCK', label: '블록' }; + export const divideToolStateMachine = createSequenceTool({ tool: Tool.DIVIDE, helpers: false, steps: [ - { kind: 'entity', instructions: '등분할 객체를 선택하십시오.' }, - { kind: 'number', instructions: '세그먼트 수를 입력하십시오 <4>.', defaultValue: 4 }, + { kind: 'entity', prompt: { text: '등분할 객체 선택' } }, + { + kind: 'number', + prompt: { + text: '세그먼트 수 입력', + absent: [BLOCK], + number: { int: true, min: 2, max: 32767 }, + }, + }, ], commit: (input) => { const points = dividePoints(sampleEntityPoints(input.entity(0)), input.number(1)); @@ -31,11 +44,19 @@ export const measureLengthToolStateMachine = createSequenceTool({ tool: Tool.MEASURE_LENGTH, helpers: false, steps: [ - { kind: 'entity', instructions: '길이로 분할할 객체를 선택하십시오.' }, - { kind: 'number', instructions: '세그먼트 길이를 입력하십시오 <10>.', defaultValue: 10 }, + { kind: 'entity', prompt: { text: '길이 분할 객체 선택' } }, + { + kind: 'number', + prompt: { text: '세그먼트의 길이 지정', absent: [BLOCK], number: { positive: true } }, + }, ], commit: (input) => { - const points = measurePoints(sampleEntityPoints(input.entity(0)), input.number(1)); + // 누른 자리에 가까운 끝부터 잼(오토캐드) + const path = sampleEntityPoints(input.entity(0)); + const pick = input.pick(0); + const nearEnd = + path.length > 1 && path[path.length - 1].distanceTo(pick)[0] < path[0].distanceTo(pick)[0]; + const points = measurePoints(nearEnd ? [...path].reverse() : path, input.number(1)); if (!points.length) { toast.warn('지정한 길이로 나눌 수 없습니다.'); return; diff --git a/B07_DesignDetail/openwebcad/src/tools/modify/lengthen-tool.ts b/B07_DesignDetail/openwebcad/src/tools/modify/lengthen-tool.ts new file mode 100644 index 000000000..1c5290d84 --- /dev/null +++ b/B07_DesignDetail/openwebcad/src/tools/modify/lengthen-tool.ts @@ -0,0 +1,212 @@ +/** + * LENGTHEN(LEN) — 오토캐드 물음 차례(337 설계 1-2). + * 「측정할 객체 선택 또는 [증분(DE)/퍼센트(P)/전체(T)] <전체(T)>:」(누름 = 현재 길이 알림) + * → 방식 값 → 「변경할 객체 선택 또는 [명령 취소(U)]:」 되풀이(빈 Enter · Esc = 끝 · 바꾼 것 남음). + * 손으로 짠 도구(설계 2-14) — 바꾸고 같은 물음에 머무는 되풀이 · U 는 이 명령 안 바꿈만 되돌림. + */ +import { Arc, type Point, Segment } from '@flatten-js/core'; +import { assign, createMachine } from 'xstate'; +import { type OptionSpec, type PromptSpec, recall, remember } from '../../commands/prompt'; +import { ArcEntity } from '../../entities/ArcEntity'; +import type { Entity } from '../../entities/Entity'; +import { findClosestEntity } from '../../helpers/find-closest-entity'; +import { polylineLength, sampleEntityPoints } from '../../helpers/geometry/sample-entity'; +import { pickRadius } from '../../helpers/pick-radius'; +import { queryEntitiesNearPoint } from '../../helpers/spatial-index'; +import { notice } from '../../inputController/prompt-input'; +import { addEntities, deleteEntities, getLayerById, setShouldDrawHelpers, undo } from '../../state'; +import { Tool } from '../../tools'; +import { + END_STATE, + type MouseClickEvent, + type NumberInputEvent, + type StateEvent, + type ToolContext, +} from '../tool.types'; +import { copyStyle, lengthenLine } from './modify.helpers'; + +type Mode = 'DELTA' | 'PERCENT' | 'TOTAL'; + +const MODE_OPTIONS: Record = { + DELTA: { key: 'DELTA', kw: 'DE', en: 'DELTA', label: '증분' }, + PERCENT: { key: 'PERCENT', kw: 'P', en: 'PERCENT', label: '퍼센트' }, + TOTAL: { key: 'TOTAL', kw: 'T', en: 'TOTAL', label: '전체' }, +}; +const DYNAMIC: OptionSpec = { key: 'DYNAMIC', kw: 'DY', en: 'DYNAMIC', label: '동적' }; +const ANGLE: OptionSpec = { key: 'ANGLE', kw: 'A', en: 'ANGLE', label: '각도' }; +const UNDO: OptionSpec = { key: 'UNDO', kw: 'U', en: 'UNDO', label: '명령 취소' }; + +/** 방식별 값 물음 — 기억 열쇠 LENGTHEN.DE · .P · .T */ +const VALUE_PROMPTS: Record = { + DELTA: { + text: '증분 길이 입력', + absent: [ANGLE], + default: { kind: 'value', remember: 'LENGTHEN.DE', initial: 0 }, + }, + PERCENT: { + text: '퍼센트 길이 입력', + number: { positive: true }, + default: { kind: 'value', remember: 'LENGTHEN.P', initial: 100 }, + }, + TOTAL: { + text: '전체 길이 지정', + absent: [ANGLE], + number: { positive: true }, + default: { kind: 'value', remember: 'LENGTHEN.T', initial: 1 }, + }, +}; +const REMEMBER_KEY: Record = { + DELTA: 'LENGTHEN.DE', + PERCENT: 'LENGTHEN.P', + TOTAL: 'LENGTHEN.T', +}; + +const lastMode = (): Mode => recall('LENGTHEN.MODE') ?? 'TOTAL'; + +const MEASURE_PROMPT = (): PromptSpec => { + const mode = MODE_OPTIONS[lastMode()]; + return { + text: '측정할 객체 선택', + options: [MODE_OPTIONS.DELTA, MODE_OPTIONS.PERCENT, MODE_OPTIONS.TOTAL], + absent: [DYNAMIC], + default: { kind: 'option', key: mode.key, label: `${mode.label}(${mode.kw})` }, + }; +}; +const TARGET_PROMPT: PromptSpec = { text: '변경할 객체 선택', options: [UNDO] }; + +/** 누른 자리 가장 가까운 객체(잠금 층 뺌 · 반경 안만) */ +function pickAt(point: Point): Entity | null { + const radius = pickRadius(); + const candidates = queryEntitiesNearPoint(point.x, point.y, radius).filter( + (entity) => !getLayerById(entity.layerId)?.isLocked + ); + const { distance, entity } = findClosestEntity(point, candidates); + return entity && distance <= radius ? entity : null; +} + +export function entityLength(entity: Entity): number { + const shape = entity.getShape(); + if (shape instanceof Segment || shape instanceof Arc) return shape.length; + return polylineLength(sampleEntityPoints(entity)); +} + +/** 호 — 누른 쪽 끝을 호 길이 delta 만큼(사이각 0 ~ 360° 안) */ +function lengthenArc(entity: Entity, arc: Arc, delta: number, near: Point): Entity | null { + const radius = Number(arc.r); + const turn = delta / radius; + const sweep = arc.sweep + turn; + if (sweep <= 0 || sweep >= 2 * Math.PI) return null; + const atStart = arc.start.distanceTo(near)[0] < arc.end.distanceTo(near)[0]; + const sign = arc.counterClockwise ? 1 : -1; + const startAngle = atStart ? arc.startAngle - sign * turn : arc.startAngle; + const endAngle = atStart ? arc.endAngle : arc.endAngle + sign * turn; + return copyStyle( + entity, + new ArcEntity(entity.layerId, arc.center, radius, startAngle, endAngle, arc.counterClockwise) + ); +} + +/** 방식 · 값 → 바뀐 객체(못 바꾸면 null) */ +function changed(entity: Entity, mode: Mode, value: number, near: Point): Entity | null { + const shape = entity.getShape(); + if (!(shape instanceof Segment) && !(shape instanceof Arc)) return null; + const length = shape.length; + const target = + mode === 'DELTA' ? length + value : mode === 'PERCENT' ? (length * value) / 100 : value; + if (target <= 1e-9) return null; + const delta = target - length; + return shape instanceof Arc + ? lengthenArc(entity, shape, delta, near) + : lengthenLine(entity, delta, near); +} + +interface LengthenContext extends ToolContext { + mode: Mode; + value: number; + /** 이 명령 안에서 바꾼 횟수 — U 는 이만큼만 되돌림 */ + changes: number; +} + +type Ctx = { context: LengthenContext; event: StateEvent }; + +const toValue = (mode: Mode) => ({ + target: `VALUE_${mode}`, + actions: assign({ mode: () => mode }), +}); + +const clickPoint = (event: StateEvent) => (event as MouseClickEvent).worldMouseLocation; + +export const lengthenToolStateMachine = createMachine({ + types: {} as { context: LengthenContext; events: StateEvent }, + context: { type: Tool.LENGTHEN, mode: 'TOTAL', value: 1, changes: 0 }, + initial: 'MEASURE', + states: { + MEASURE: { + meta: { kind: 'entity', prompt: MEASURE_PROMPT }, + entry: () => setShouldDrawHelpers(false), + on: { + MOUSE_CLICK: { + actions: ({ event }: Ctx) => { + const entity = pickAt(clickPoint(event)); + if (entity) notice(`현재 길이: ${entityLength(entity).toFixed(4)}`); + }, + }, + 'OPTION.DELTA': toValue('DELTA'), + 'OPTION.PERCENT': toValue('PERCENT'), + 'OPTION.TOTAL': toValue('TOTAL'), + ESC: END_STATE, + }, + }, + ...Object.fromEntries( + (Object.keys(VALUE_PROMPTS) as Mode[]).map((mode) => [ + `VALUE_${mode}`, + { + meta: { kind: 'number', prompt: VALUE_PROMPTS[mode] }, + on: { + NUMBER_INPUT: { + target: 'TARGET', + actions: assign(({ event }: Ctx) => { + const value = (event as NumberInputEvent).value; + remember('LENGTHEN.MODE', mode); + remember(REMEMBER_KEY[mode], value); + return { value }; + }), + }, + ESC: END_STATE, + }, + }, + ]) + ), + TARGET: { + meta: { kind: 'entity', prompt: TARGET_PROMPT }, + on: { + MOUSE_CLICK: { + actions: assign(({ context, event }: Ctx) => { + const near = clickPoint(event); + const entity = pickAt(near); + if (!entity) return {}; + const next = changed(entity, context.mode, context.value, near); + if (!next) { + notice('이 객체는 길이를 바꿀 수 없음.'); + return {}; + } + deleteEntities([entity], false); + addEntities([next], true); + return { changes: context.changes + 1 }; + }), + }, + 'OPTION.UNDO': { + actions: assign(({ context }: Ctx) => { + if (!context.changes) return {}; + undo(); + return { changes: context.changes - 1 }; + }), + }, + ENTER: END_STATE, + ESC: END_STATE, + }, + }, + [END_STATE]: { type: 'final' }, + // biome-ignore lint/suspicious/noExplicitAny: 손으로 짠 상태 — 사건마다 assign 꼴이 달라 풀어 둠(sequence-tool 과 같음) + } as Record, +}); diff --git a/B07_DesignDetail/openwebcad/src/tools/utility/inquiry-tools.ts b/B07_DesignDetail/openwebcad/src/tools/utility/inquiry-tools.ts index 84706b436..6babd6ab6 100644 --- a/B07_DesignDetail/openwebcad/src/tools/utility/inquiry-tools.ts +++ b/B07_DesignDetail/openwebcad/src/tools/utility/inquiry-tools.ts @@ -1,6 +1,8 @@ /** 조회 명령 — 거리·반지름·각도·면적·좌표·리스트·계산기 (조사표 3절 유틸리티) */ import { Circle, type Point } from '@flatten-js/core'; import { toast } from 'react-toastify'; +import type { OptionSpec } from '../../commands/prompt'; +import { logLine } from '../../commands/run-command'; import { EntityName } from '../../entities/Entity'; import { entitiesToLoop } from '../../helpers/geometry/entity-loop'; import { polylineLength, sampleEntityPoints } from '../../helpers/geometry/sample-entity'; @@ -22,21 +24,30 @@ export function polygonArea(points: Point[]): number { return Math.abs(total) / 2; } +const MULTIPLE_POINTS: OptionSpec = { key: 'MULTIPLE', kw: 'M', en: 'MULTIPLE', label: '다중 점' }; + +/** DIST 결과 두 줄(오토캐드 꼴 · 길이 소수 넷 · 각도 AUPREC 0) */ +export function distanceReport(first: Point, second: Point): [string, string] { + const dx = second.x - first.x; + const dy = second.y - first.y; + const angle = (toDegrees(Math.atan2(dy, dx)) + 360) % 360; + return [ + `거리 = ${format(Math.hypot(dx, dy), 4)}, XY 평면에서의 각도 = ${Math.round(angle) % 360}, XY 평면으로부터의 각도 = 0`, + `X 증분 = ${format(dx, 4)}, Y 증분 = ${format(dy, 4)}, Z 증분 = ${format(0, 4)}`, + ]; +} + export const distanceToolStateMachine = createSequenceTool({ tool: Tool.DIST, steps: [ - { kind: 'point', instructions: '거리를 잴 첫 점을 지정하십시오.' }, - { kind: 'point', instructions: '두 번째 점을 지정하십시오.' }, + { kind: 'point', prompt: { text: '첫 번째 점 지정' } }, + { kind: 'point', prompt: { text: '두 번째 점 지정', absent: [MULTIPLE_POINTS] } }, ], commit: (input) => { const [first, second] = input.points(); - const distance = first.distanceTo(second)[0]; - const angle = toDegrees(Math.atan2(second.y - first.y, second.x - first.x)); - toast.info( - `거리 ${format(distance)} · X 증분 ${format(second.x - first.x)} · Y 증분 ${format( - second.y - first.y - )} · 각도 ${format(angle, 2)}°` - ); + const lines = distanceReport(first, second); + for (const line of lines) logLine(line); + toast.info(lines.join('\n')); }, }); diff --git a/resources/tester/cad/measure-acad.test.ts b/resources/tester/cad/measure-acad.test.ts new file mode 100644 index 000000000..4cc62edf8 --- /dev/null +++ b/resources/tester/cad/measure-acad.test.ts @@ -0,0 +1,363 @@ +/** + * 337 다-2 — 늘이기 · 재기 · 치수 오토캐드 조작(설계 1-2 · 6-3 다 줄 중 LENGTHEN · DIVIDE · MEASURE · DIST · DIM). + * 물음 글 · 옵션 글자 · 기본값 · 빈 Enter · Esc · 끝난 뒤를 키 · 마우스로 넣어 숫자로 판정. + * 객체는 앱 코드(fromJson)로만 만듦 — 시험 쪽 flatten-js 는 다른 사본. + * 실행: cd B07_DesignDetail/openwebcad && npx vitest run --dir ../../resources/tester/cad measure-acad + */ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +vi.mock("react-toastify", () => ({ + toast: Object.assign(vi.fn(), { error: vi.fn(), info: vi.fn(), warn: vi.fn(), success: vi.fn() }), +})); + +import { getCommandHistory } from "../../../B07_DesignDetail/openwebcad/src/commands/run-command"; +import { getDimDecimals } from "../../../B07_DesignDetail/openwebcad/src/commands/dim-settings"; +import { ArcEntity } from "../../../B07_DesignDetail/openwebcad/src/entities/ArcEntity"; +import { CircleEntity } from "../../../B07_DesignDetail/openwebcad/src/entities/CircleEntity"; +import { EntityName, type Entity } from "../../../B07_DesignDetail/openwebcad/src/entities/Entity"; +import { LineEntity } from "../../../B07_DesignDetail/openwebcad/src/entities/LineEntity"; +import type { MeasurementEntity } from "../../../B07_DesignDetail/openwebcad/src/entities/MeasurementEntity"; +import { getActiveLayerId, setEntities } from "../../../B07_DesignDetail/openwebcad/src/state"; +import { Tool } from "../../../B07_DesignDetail/openwebcad/src/tools"; +import { + activeTool, + click, + entities, + initIdle, + key, + noticeLine, + promptLine, + type, + typeEnter, +} from "./helpers/cad-drive"; + +type Xy = { x: number; y: number }; +const enter = () => key("Enter"); +const esc = () => key("Escape"); +const base = () => ({ lineColor: "#fff", lineWidth: 1, layerId: getActiveLayerId() }); + +async function addLine(x1: number, y1: number, x2: number, y2: number): Promise { + const line = await LineEntity.fromJson({ + id: `line-${x1}-${y1}-${x2}-${y2}`, + type: EntityName.Line, + ...base(), + shapeData: { startPoint: { x: x1, y: y1 }, endPoint: { x: x2, y: y2 } }, + } as never); + setEntities([...entities(), line], true); // 그린 것처럼 되돌림 기록에 올림 + return line; +} + +async function addArc(): Promise { + // 중심 (0, 0) · 반지름 50 · 0° → 90° 반시계(길이 25π) + const arc = await ArcEntity.fromJson({ + id: "arc", + type: EntityName.Arc, + ...base(), + shapeData: { center: { x: 0, y: 0 }, radius: 50, startAngle: 0, endAngle: Math.PI / 2, counterClockwise: true }, + } as never); + setEntities([...entities(), arc], true); // 그린 것처럼 되돌림 기록에 올림 + return arc; +} + +async function addCircle(): Promise { + const circle = await CircleEntity.fromJson({ + id: "circle", + type: EntityName.Circle, + ...base(), + shapeData: { center: { x: 300, y: 300 }, radius: 10 }, + } as never); + setEntities([...entities(), circle], true); // 그린 것처럼 되돌림 기록에 올림 + return circle; +} + +const ends = (entity: Entity): [Xy, Xy] => { + const shape = entity.getShape() as unknown as { start: Xy; end: Xy }; + return [ + { x: shape.start.x, y: shape.start.y }, + { x: shape.end.x, y: shape.end.y }, + ]; +}; +const onlyLine = () => entities("Line")[0]; +const lineLength = () => { + const [a, b] = ends(onlyLine()); + return Math.hypot(b.x - a.x, b.y - a.y); +}; +const points = () => + entities("Point") + .map((entity) => { + const p = entity.getShape() as unknown as Xy; + return { x: Math.round(p.x * 1000) / 1000, y: Math.round(p.y * 1000) / 1000 }; + }) + .sort((a, b) => a.x - b.x); +const dims = () => entities("Measurement") as MeasurementEntity[]; +const lastLog = (n = 1) => getCommandHistory().slice(-n); +/** 선형 치수 글(반올림 · 끝 0 뗌) · 반지름 치수 글(소수 자릿수 그대로) */ +const fmt = (value: number) => String(Number(value.toFixed(getDimDecimals()))); +const fixed = (value: number) => value.toFixed(getDimDecimals()); + +beforeEach(() => { + initIdle(); +}); + +describe("LENGTHEN(LEN)", () => { + const FIRST = "측정할 객체 선택 또는 [증분(DE)/퍼센트(P)/전체(T)]"; + + it("누름 = 현재 길이 · DE → 되풀이 · U = 이 명령 안 바꿈만 · 빈 Enter = 끝", async () => { + await addLine(0, 0, 100, 0); + typeEnter("len"); + expect(activeTool()).toBe(Tool.LENGTHEN); + expect(promptLine()).toBe(`${FIRST} <전체(T)>:`); + click(50, 0); + expect(noticeLine()).toBe("현재 길이: 100.0000"); + expect(promptLine()).toBe(`${FIRST} <전체(T)>:`); + type("d"); // DE 보다 짧음 — 안 맞음 + expect(noticeLine()).toBe("*선택이 잘못됨*"); + type("dy"); + expect(noticeLine()).toBe("「동적(DY)」 옵션은 이 CAD 에 없음."); + type("de"); + expect(promptLine()).toBe("증분 길이 입력 <0.0000>:"); + type("10"); + expect(promptLine()).toBe("변경할 객체 선택 또는 [명령 취소(U)]:"); + click(95, 0); // 끝 쪽 + expect(ends(onlyLine())[1].x).toBeCloseTo(110); + click(105, 0); + expect(ends(onlyLine())[1].x).toBeCloseTo(120); + expect(activeTool()).toBe(Tool.LENGTHEN); // 되풀이 + type("u"); + expect(ends(onlyLine())[1].x).toBeCloseTo(110); + type("u"); + type("u"); // 이 명령 안 바꿈 다 되돌린 뒤 = 아무 일 없음(앞 객체 지키기) + expect(entities("Line")).toHaveLength(1); + expect(lineLength()).toBeCloseTo(100); + click(5, 0); // 시작 쪽 + expect(ends(onlyLine())[0].x).toBeCloseTo(-10); + enter(); + expect(activeTool()).toBe(Tool.SELECT); + }); + + it("마지막 방식 · 값 기억 — 첫 물음 빈 Enter = 그 방식 · 값 빈 Enter = 앞 값", async () => { + await addLine(0, 0, 100, 0); + typeEnter("len"); + type("de"); + type("10"); + esc(); + typeEnter("len"); + expect(promptLine()).toBe(`${FIRST} <증분(DE)>:`); + enter(); + expect(promptLine()).toBe("증분 길이 입력 <10.0000>:"); + enter(); + click(95, 0); + expect(lineLength()).toBeCloseTo(110); + esc(); // Esc = 끝 · 바꾼 것 남음 + expect(activeTool()).toBe(Tool.SELECT); + expect(lineLength()).toBeCloseTo(110); + }); + + it("P · T — 퍼센트 · 전체 · 양수 거절 · 호", async () => { + await addLine(0, 0, 100, 0); + typeEnter("len"); + type("p"); + expect(promptLine()).toBe("퍼센트 길이 입력 <100.0000>:"); + type("-5"); + expect(noticeLine()).toBe("값은 양수이고 0이 아니어야 함."); + type("50"); + click(90, 0); + expect(ends(onlyLine())).toEqual([ + { x: 0, y: 0 }, + { x: 50, y: 0 }, + ]); + esc(); + typeEnter("len"); + expect(promptLine()).toBe(`${FIRST} <퍼센트(P)>:`); + type("t"); + expect(promptLine()).toBe("전체 길이 지정 <1.0000>:"); + type("a"); + expect(noticeLine()).toBe("「각도(A)」 옵션은 이 CAD 에 없음."); + type("200"); + click(45, 0); + expect(lineLength()).toBeCloseTo(200); + await addArc(); + click(0, 50); // 호 끝(90°) 쪽 + const arc = entities("Arc")[0].getShape() as unknown as { length: number; endAngle: number }; + expect(arc.length).toBeCloseTo(200); + expect(arc.endAngle).toBeCloseTo(4); // 200 / 50 rad + esc(); + }); +}); + +describe("DIVIDE(DIV) · MEASURE(ME)", () => { + it("DIVIDE — 기본 없음 · 정수 2~32767 · 블록 없음 · 한 번 뒤 끝", async () => { + await addLine(0, 0, 100, 0); + typeEnter("div"); + expect(promptLine()).toBe("등분할 객체 선택:"); + click(50, 0); + expect(promptLine()).toBe("세그먼트 수 입력:"); + enter(); // 기본 없음 — 그대로 + expect(promptLine()).toBe("세그먼트 수 입력:"); + type("2.5"); + expect(noticeLine()).toBe("정수 값을 요구함."); + type("1"); + expect(noticeLine()).toBe("2에서 32767 사이의 정수를 요구함."); + type("b"); + expect(noticeLine()).toBe("「블록(B)」 옵션은 이 CAD 에 없음."); + type("4"); + expect(points()).toEqual([ + { x: 25, y: 0 }, + { x: 50, y: 0 }, + { x: 75, y: 0 }, + ]); + expect(activeTool()).toBe(Tool.SELECT); + }); + + it("MEASURE — 누른 자리 가까운 끝부터 · 양수", async () => { + await addLine(0, 0, 100, 0); + typeEnter("me"); + expect(promptLine()).toBe("길이 분할 객체 선택:"); + click(90, 0); // 끝 쪽 + expect(promptLine()).toBe("세그먼트의 길이 지정:"); + type("0"); + expect(noticeLine()).toBe("값은 양수이고 0이 아니어야 함."); + type("30"); + expect(points().map((p) => p.x)).toEqual([10, 40, 70]); + expect(activeTool()).toBe(Tool.SELECT); + }); +}); + +describe("DIST(DI)", () => { + it("두 점 → 오토캐드 꼴 두 줄 기록 · 한 번 뒤 끝 · 다중 점 없음", () => { + typeEnter("di"); + expect(promptLine()).toBe("첫 번째 점 지정:"); + type("0,0"); + expect(promptLine()).toBe("두 번째 점 지정:"); + type("m"); + expect(noticeLine()).toBe("「다중 점(M)」 옵션은 이 CAD 에 없음."); + type("30,40"); + expect(lastLog(2)).toEqual([ + "거리 = 50.0000, XY 평면에서의 각도 = 53, XY 평면으로부터의 각도 = 0", + "X 증분 = 30.0000, Y 증분 = 40.0000, Z 증분 = 0.0000", + ]); + expect(activeTool()).toBe(Tool.SELECT); + }); +}); + +describe("DIMLINEAR(DLI) · DIMALIGNED(DAL)", () => { + const PLACE = "치수선의 위치 지정 또는 [문자(T)/수평(H)/수직(V)]:"; + + it("원점 둘 → V 고정 → T 문자(`<>` = 잰 값) → 놓음 · 기록 · 한 번 뒤 끝", () => { + typeEnter("dli"); + expect(promptLine()).toBe("첫 번째 치수보조선 원점 지정 또는 <객체 선택>:"); + type("0,0"); + expect(promptLine()).toBe("두 번째 치수보조선 원점 지정:"); + type("100,30"); + expect(promptLine()).toBe(PLACE); + type("m"); + expect(noticeLine()).toBe("「여러 줄 문자(M)」 옵션은 이 CAD 에 없음."); + type("v"); + expect(promptLine()).toBe(PLACE); + type("t"); + expect(promptLine()).toBe(`치수 문자 입력 <${fmt(30)}>:`); + typeEnter("L=<>"); + expect(promptLine()).toBe(PLACE); + type("120,15"); + const [dim] = dims(); + expect(dim.getLabel()).toBe(`L=${fmt(30)}`); + expect(dim.getEndPoint().x).toBeCloseTo(0); // 수직 — 끝점 x 를 시작점에 맞춤 + expect(dim.getEndPoint().y).toBeCloseTo(30); + expect(lastLog()).toEqual([`치수 문자 = L=${fmt(30)}`]); + expect(activeTool()).toBe(Tool.SELECT); + }); + + it("첫 물음 빈 Enter = 객체 선택 → H 고정 · T 빈 Enter = 잰 값 그대로", async () => { + await addLine(0, 0, 80, 60); + typeEnter("dli"); + enter(); + expect(promptLine()).toBe("치수 기입할 객체 선택:"); + click(40, 30); + expect(promptLine()).toBe(PLACE); + type("h"); + type("t"); + expect(promptLine()).toBe(`치수 문자 입력 <${fmt(80)}>:`); + enter(); + type("40,-20"); // 수평 고정이라 옆에 놓아도 가로 + const [dim] = dims(); + expect(dim.getLabel()).toBe(fmt(80)); + expect(dim.textOverride).toBeUndefined(); + expect(activeTool()).toBe(Tool.SELECT); + }); + + it("Esc = 끝 · 안 만듦 · DIMALIGNED 는 [문자(T)] 만", () => { + typeEnter("dli"); + type("0,0"); + type("10,0"); + esc(); + expect(activeTool()).toBe(Tool.SELECT); + expect(dims()).toHaveLength(0); + typeEnter("dal"); + type("0,0"); + type("30,40"); + expect(promptLine()).toBe("치수선의 위치 지정 또는 [문자(T)]:"); + type("h"); + expect(noticeLine()).toBe("점 또는 옵션 키워드를 요구함."); + type("0,50"); + expect(dims()[0].getLabel()).toBe(fmt(50)); + }); +}); + +describe("DIMCONTINUE(DCO) · DIMBASELINE(DBA)", () => { + const CHAIN = "두 번째 치수보조선 원점 지정 또는 [명령 취소(U)]:"; + + it("되풀이 · U = 이 명령 안 마지막 치수 · 빈 Enter = 끝", () => { + typeEnter("dli"); + type("0,0"); + type("100,0"); + type("50,20"); + expect(dims()).toHaveLength(1); + typeEnter("dco"); + expect(promptLine()).toBe(CHAIN); + type("s"); + expect(noticeLine()).toBe("「선택(S)」 옵션은 이 CAD 에 없음."); + type("150,0"); + type("200,0"); + expect(dims()).toHaveLength(3); + expect(dims()[2].getStartPoint().x).toBeCloseTo(150); // 앞 치수 끝에서 이어짐 + expect(promptLine()).toBe(CHAIN); + type("u"); + expect(dims()).toHaveLength(2); + type("u"); + type("u"); // 이 명령 밖 치수는 안 지움 + expect(dims()).toHaveLength(1); + type("130,0"); + expect(dims()[1].getStartPoint().x).toBeCloseTo(100); // U 뒤 앞 치수에서 다시 이음 + enter(); + expect(activeTool()).toBe(Tool.SELECT); + typeEnter("dba"); + expect(promptLine()).toBe(CHAIN); + type("180,0"); + expect(dims()).toHaveLength(3); + expect(dims()[2].getStartPoint().x).toBeCloseTo(100); // 기준선 = 앞 치수 시작점 + esc(); + expect(activeTool()).toBe(Tool.SELECT); + }); +}); + +describe("DIMRADIUS(DRA) · DIMDIAMETER(DDI)", () => { + it("호 또는 원 → [문자(T)] → 위치 · 기록 · 한 번 뒤 끝", async () => { + await addCircle(); + typeEnter("dra"); + expect(promptLine()).toBe("호 또는 원 선택:"); + click(310, 300); + expect(promptLine()).toBe("치수선의 위치 지정 또는 [문자(T)]:"); + type("a"); + expect(noticeLine()).toBe("「각도(A)」 옵션은 이 CAD 에 없음."); + type("t"); + expect(promptLine()).toBe(`치수 문자 입력 :`); + typeEnter("<> BASE"); + type("330,300"); + expect(lastLog()).toEqual([`치수 문자 = R${fixed(10)} BASE`]); + expect(activeTool()).toBe(Tool.SELECT); + typeEnter("ddi"); + click(310, 300); + type("330,300"); + expect(lastLog()).toEqual([`치수 문자 = Ø${fixed(20)}`]); + }); +}); From 703c89c617b48a92120c2ceededa6da1e50ee289 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Fri, 16 Oct 2026 00:15:37 +0900 Subject: [PATCH 71/81] =?UTF-8?q?fix(B07=20CAD):=20337-2=20=EA=B0=80-2=20?= =?UTF-8?q?=ED=95=A9=EC=B9=A8=20=EC=96=B4=EA=B8=8B=EB=82=A8=20=E2=80=94=20?= =?UTF-8?q?ZOOM=20onText=20=EB=8B=B5=EC=9D=84=20=EA=B0=80-2=20settle(sub,?= =?UTF-8?q?=20result)=20=EA=BC=B4=EB=A1=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - prompt-input.ts routeTyped 덧물음 onText 답 = settle(그 덧물음, 답) — 옛 settle(답) 한 인자로 남아 타입 오류 · 「'Z 2X」 뒤 덧물음이 안 닫혀 다음 누름(스냅 메뉴 시험)을 먹던 것 - 가-2 base · 스냅 이름 · FROM · M2P 그대로 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Se7ytqbohmBQnfYpwYFnYf --- .../openwebcad/src/inputController/prompt-input.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/B07_DesignDetail/openwebcad/src/inputController/prompt-input.ts b/B07_DesignDetail/openwebcad/src/inputController/prompt-input.ts index 44978e933..d08439020 100644 --- a/B07_DesignDetail/openwebcad/src/inputController/prompt-input.ts +++ b/B07_DesignDetail/openwebcad/src/inputController/prompt-input.ts @@ -379,8 +379,9 @@ export function routeTyped(raw: string): void { return; } if (text.startsWith("'")) return routeTransparent(text.slice(1)); - const own = subPrompt()?.onText?.(text); - if (own) return settle(own); + const textSub = subPrompt(); + const own = textSub?.onText?.(text); + if (textSub && own) return settle(textSub, own); if (prompt.kind === 'idle') { // ⑨ 이름 > 내 별칭 > 기본 별칭 딱 맞음만 const command = resolveCommandInput(text); From 3cebb37ed06b03638df6a99f92d29ac4422b8836 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Fri, 16 Oct 2026 00:16:15 +0900 Subject: [PATCH 72/81] =?UTF-8?q?feat(CAD):=20337-3a=20=EB=82=98-1=20?= =?UTF-8?q?=EA=B7=B8=EB=A6=AC=EA=B8=B0=208=20=E2=80=94=20=EC=98=A4?= =?UTF-8?q?=ED=86=A0=EC=BA=90=EB=93=9C=20=EB=AC=BC=EC=9D=8C=20=EC=B0=A8?= =?UTF-8?q?=EB=A1=80=20=C2=B7=20=EC=98=B5=EC=85=98=20=C2=B7=20=EA=B8=B0?= =?UTF-8?q?=EB=B3=B8=EA=B0=92=20=C2=B7=20=EB=B9=88=20Enter=20=C2=B7=20Esc?= =?UTF-8?q?=20=C2=B7=20=EB=81=9D=EB=82=9C=20=EB=92=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - LINE: 「첫 번째 점 지정」 빈 Enter = 마지막 끝점에서 이음 · U = 마지막 토막(되돌림 한 칸) · 둘째 토막부터 C · Enter · Esc = 끝(토막 남음) - PLINE(새 pline-tool): 호(A) 접선 호 · 길이(L) · 닫기(C · 호 모드 CL) · 명령 취소(U) · 선(L) · Esc · Enter = 그린 데까지 남김 - RECTANG: 치수(D) 길이 · 폭 기억(RECT.L · RECT.W) → 누른 쪽 사분면 · 없는 옵션 안내 - CIRCLE: 3P · 2P · 지름(D) <2r> · 반지름 기억(CIRCLERAD · 처음 <> 없음) · 한 번 뒤 끝 - POLYGON(새 polygon-tool): 변 수 기억 <4> · 3~1024 정수 · 모서리(E) · 내접/외접 기억 · 숫자 = 아래 변 수평 - ARC(새 arc-tool): 세 점 · 중심(C) · 각도(A) · 끝(E) · 반지름(R) - ELLIPSE(새 ellipse-tool): 축 두 끝 · 중심(C) · 회전(R) · 다른 축 숫자 = 중심에서 반 길이(N10) - SPLINE: 다음 점 입력 · U · 넷째 점부터 C · Esc 버림 · DONUT 중심 되풀이(loopFrom) - 시험 draw-acad 22 · keyboard-chain 옛 선 · 폴리선 줄 두 곳 새 동작으로 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01VmC34fsxxbweEG3RUjs3Mn --- .../openwebcad/src/tools/circle-tool.ts | 303 ++++++----- .../openwebcad/src/tools/draw/arc-tool.ts | 126 +++++ .../src/tools/draw/basic-draw-tools.ts | 153 ++---- .../openwebcad/src/tools/draw/draw-steps.ts | 39 ++ .../openwebcad/src/tools/draw/ellipse-tool.ts | 176 +++++++ .../openwebcad/src/tools/draw/pline-tool.ts | 277 ++++++++++ .../openwebcad/src/tools/draw/polygon-tool.ts | 220 ++++++++ .../openwebcad/src/tools/line-tool.ts | 214 +++----- .../openwebcad/src/tools/rectangle-tool.ts | 205 ++++---- resources/tester/cad/draw-acad.test.ts | 491 ++++++++++++++++++ resources/tester/cad/keyboard-chain.test.ts | 11 +- 11 files changed, 1757 insertions(+), 458 deletions(-) create mode 100644 B07_DesignDetail/openwebcad/src/tools/draw/arc-tool.ts create mode 100644 B07_DesignDetail/openwebcad/src/tools/draw/draw-steps.ts create mode 100644 B07_DesignDetail/openwebcad/src/tools/draw/ellipse-tool.ts create mode 100644 B07_DesignDetail/openwebcad/src/tools/draw/pline-tool.ts create mode 100644 B07_DesignDetail/openwebcad/src/tools/draw/polygon-tool.ts create mode 100644 resources/tester/cad/draw-acad.test.ts diff --git a/B07_DesignDetail/openwebcad/src/tools/circle-tool.ts b/B07_DesignDetail/openwebcad/src/tools/circle-tool.ts index 8f992f6c9..50d9b6681 100644 --- a/B07_DesignDetail/openwebcad/src/tools/circle-tool.ts +++ b/B07_DesignDetail/openwebcad/src/tools/circle-tool.ts @@ -1,172 +1,229 @@ +/** + * 원(CIRCLE) — 337 설계 1-1 · 오토캐드 차례. + * 「원에 대한 중심점 지정 또는 [3점(3P)/2점(2P)]:」 → 「원의 반지름 지정 또는 [지름(D)] :」 · + * D → 「원의 지름 지정 <2r>:」 · 3P → 원 위 세 점 · 2P → 지름 두 끝점. + * 반지름 기억(CIRCLERAD · 처음 없음 → <> 안 그림 · 빈 Enter 아무 일 없음) · 누른 점 · 3P · 2P 반지름도 기억. 하나 그리면 끝. + */ +import { Point } from '@flatten-js/core'; +import { assign, createMachine } from 'xstate'; +import { type PromptSpec, recall, remember } from '../commands/prompt'; import { CircleEntity } from '../entities/CircleEntity'; -import type { Point } from '@flatten-js/core'; +import { getPointFromEvent } from '../helpers/get-point-from-event.ts'; +import { arcThroughThreePoints } from '../helpers/geometry/shape-points'; import { addEntities, getActiveLayerId, - getActiveLineColor, - getActiveLineDash, - getActiveLineWidth, - setActiveToolActor, setAngleGuideOriginPoint, setGhostHelperEntities, setSelectedEntityIds, setShouldDrawHelpers, } from '../state'; -import type { DrawEvent, PointInputEvent, StateEvent, ToolContext } from './tool.types'; import { Tool } from '../tools'; -import { Actor, assign, createMachine } from 'xstate'; -import { pointDistance } from '../helpers/distance-between-points'; -import { LineState } from './line-tool.ts'; -import { selectToolStateMachine } from './select-tool.ts'; -import { getPointFromEvent } from '../helpers/get-point-from-event.ts'; +import { END, firstPointEvents, option, pointEvents } from './draw/draw-steps'; +import { styled } from './factories/entity-factory'; +import { + type DrawEvent, + END_STATE, + type NumberInputEvent, + type PointInputEvent, + type StateEvent, + type ToolContext, +} from './tool.types'; export interface CircleContext extends ToolContext { centerPoint: Point | null; + /** 3P · 2P 에서 이미 찍은 점 */ + points: Point[]; + threePoints: boolean; } export enum CircleState { - WAITING_FOR_CENTER_POINT = 'WAITING_FOR_CENTER_POINT', - WAITING_FOR_POINT_ON_CIRCLE = 'WAITING_FOR_POINT_ON_CIRCLE', INIT = 'INIT', + WAITING_FOR_CENTER_POINT = 'WAITING_FOR_CENTER_POINT', + WAITING_FOR_RADIUS = 'WAITING_FOR_RADIUS', + WAITING_FOR_DIAMETER = 'WAITING_FOR_DIAMETER', + THREE_POINTS = 'THREE_POINTS', + TWO_POINTS = 'TWO_POINTS', } -export enum CircleAction { - INIT_CIRCLE_TOOL = 'INIT_CIRCLE_TOOL', - RECORD_START_POINT = 'RECORD_START_POINT', - DRAW_TEMP_CIRCLE = 'DRAW_TEMP_CIRCLE', - DRAW_FINAL_CIRCLE = 'DRAW_FINAL_CIRCLE', - SWITCH_TO_SELECT_TOOL = 'SWITCH_TO_SELECT_TOOL', +const RADIUS_KEY = 'CIRCLERAD'; +/** 지름 기본 = 2 × 기억 반지름 — 이 열쇠엔 쓰지 않음 */ +const DIAMETER_KEY = 'CIRCLERAD.D'; + +const CENTER_PROMPT: PromptSpec = { + text: '원에 대한 중심점 지정', + options: [option('3P', '3P', '3점'), option('2P', '2P', '2점')], + absent: [option('TTR', 'T', 'Ttr - 접선 접선 반지름')], +}; +const RADIUS_PROMPT: PromptSpec = { + text: '원의 반지름 지정', + options: [option('DIAMETER', 'D', '지름')], + default: { kind: 'value', remember: RADIUS_KEY, initial: null }, + number: { positive: true }, +}; +const diameterPrompt = (): PromptSpec => { + const radius = recall(RADIUS_KEY); + return { + text: '원의 지름 지정', + default: { kind: 'value', remember: DIAMETER_KEY, initial: radius ? radius * 2 : null }, + number: { positive: true }, + }; +}; +const ORDINAL = ['첫 번째', '두 번째', '세 번째']; +const threePrompt = (context: Partial): PromptSpec => ({ + text: `원 위의 ${ORDINAL[context.points?.length ?? 0]} 점 지정`, +}); +const twoPrompt = (context: Partial): PromptSpec => ({ + text: `원 지름의 ${ORDINAL[context.points?.length ?? 0]} 끝점 지정`, +}); + +function addCircle(center: Point, radius: number) { + setGhostHelperEntities([]); + if (!(radius > 0)) return; + addEntities([styled(new CircleEntity(getActiveLayerId(), center, radius))], true); + remember(RADIUS_KEY, radius); } +/** 3P · 2P — 찍은 점들 + 마지막 점으로 원(점 모자람 · 한 줄 = null) */ +function circleThrough(context: CircleContext, last: Point) { + const [a, b] = context.points; + if (!context.threePoints) { + return a + ? { + center: new Point((a.x + last.x) / 2, (a.y + last.y) / 2), + radius: a.distanceTo(last)[0] / 2, + } + : null; + } + const arc = b ? arcThroughThreePoints(a, b, last) : null; + return arc ? { center: arc.center, radius: arc.radius } : null; +} + +/** 점 사건 → 점(앞 점 기준 · 첫 점이면 기준 없음) */ +const pointAt = (base: Point | null, event: StateEvent) => + getPointFromEvent(base, event as PointInputEvent); +const lastOf = (points: Point[]) => points[points.length - 1] ?? null; + +/** 3P 는 세 점 · 2P 는 두 점째에 원 */ +const pointsEvents = (count: number, self: CircleState) => { + const enough = ({ context }: { context: CircleContext }) => context.points.length === count - 1; + const done = { guard: enough, actions: 'drawThrough', target: END_STATE }; + const more = { actions: 'recordPoint', target: self }; + return { + DRAW: { actions: 'previewThrough' }, + MOUSE_CLICK: [done, more], + ABSOLUTE_POINT_INPUT: [done, more], + RELATIVE_POINT_INPUT: [done, more], + NUMBER_INPUT: [done, more], + ESC: { target: END_STATE }, + }; +}; + export const circleToolStateMachine = createMachine( { - types: {} as { - context: CircleContext; - events: StateEvent; - }, - context: { - centerPoint: null, - type: Tool.CIRCLE, - }, + types: {} as { context: CircleContext; events: StateEvent }, + context: { centerPoint: null, points: [], threePoints: false, type: Tool.CIRCLE }, initial: CircleState.INIT, states: { [CircleState.INIT]: { - description: 'Initializing the circle tool', - always: { - actions: CircleAction.INIT_CIRCLE_TOOL, - target: CircleState.WAITING_FOR_CENTER_POINT, - }, + always: { actions: 'init', target: CircleState.WAITING_FOR_CENTER_POINT }, }, [CircleState.WAITING_FOR_CENTER_POINT]: { - description: 'Select the center point of the circle tool', - meta: { - instructions: '중심점 지정 (x,y):', - }, + meta: { kind: 'point', prompt: CENTER_PROMPT }, on: { - MOUSE_CLICK: { - actions: CircleAction.RECORD_START_POINT, - target: CircleState.WAITING_FOR_POINT_ON_CIRCLE, - }, - ABSOLUTE_POINT_INPUT: { - actions: CircleAction.RECORD_START_POINT, - target: CircleState.WAITING_FOR_POINT_ON_CIRCLE, - }, - ESC: { - actions: CircleAction.SWITCH_TO_SELECT_TOOL, - }, + ...firstPointEvents('recordCenter', CircleState.WAITING_FOR_RADIUS), + 'OPTION.3P': { actions: 'pickThree', target: CircleState.THREE_POINTS }, + 'OPTION.2P': { actions: 'pickTwo', target: CircleState.TWO_POINTS }, + ESC: { target: END_STATE }, }, }, - [CircleState.WAITING_FOR_POINT_ON_CIRCLE]: { - description: 'Select a point on the circle', - meta: { - instructions: '원 위의 점 지정 또는 반지름 입력:', - }, + [CircleState.WAITING_FOR_RADIUS]: { + meta: { kind: 'point', prompt: RADIUS_PROMPT }, on: { - DRAW: { - actions: CircleAction.DRAW_TEMP_CIRCLE, - }, - MOUSE_CLICK: { - actions: CircleAction.DRAW_FINAL_CIRCLE, - target: CircleState.INIT, - }, - NUMBER_INPUT: { - actions: CircleAction.DRAW_FINAL_CIRCLE, - target: LineState.INIT, - }, - ABSOLUTE_POINT_INPUT: { - actions: CircleAction.DRAW_FINAL_CIRCLE, - target: LineState.INIT, - }, - RELATIVE_POINT_INPUT: { - actions: CircleAction.DRAW_FINAL_CIRCLE, - target: LineState.INIT, - }, - ESC: { - target: CircleState.INIT, - }, + DRAW: { actions: 'previewRadius' }, + ...pointEvents('drawByRadius', END_STATE), + 'OPTION.DIAMETER': { target: CircleState.WAITING_FOR_DIAMETER }, + ESC: { target: END_STATE }, }, }, + [CircleState.WAITING_FOR_DIAMETER]: { + meta: { kind: 'point', prompt: diameterPrompt }, + on: { + DRAW: { actions: 'previewDiameter' }, + ...pointEvents('drawByDiameter', END_STATE), + ESC: { target: END_STATE }, + }, + }, + [CircleState.THREE_POINTS]: { + meta: { kind: 'point', prompt: threePrompt }, + on: pointsEvents(3, CircleState.THREE_POINTS), + }, + [CircleState.TWO_POINTS]: { + meta: { kind: 'point', prompt: twoPrompt }, + on: pointsEvents(2, CircleState.TWO_POINTS), + }, + ...END, }, }, { actions: { - [CircleAction.INIT_CIRCLE_TOOL]: assign(() => { + init: assign(() => { setShouldDrawHelpers(true); setGhostHelperEntities([]); setSelectedEntityIds([]); setAngleGuideOriginPoint(null); - return { - centerPoint: null, - }; + return { centerPoint: null, points: [], threePoints: false }; }), - [CircleAction.RECORD_START_POINT]: assign(({ event }) => { - const startPoint = getPointFromEvent(null, event as PointInputEvent); - setAngleGuideOriginPoint(startPoint); - return { - centerPoint: startPoint, - }; + pickThree: assign({ threePoints: true }), + pickTwo: assign({ threePoints: false }), + recordCenter: assign(({ event }) => { + const centerPoint = pointAt(null, event); + setAngleGuideOriginPoint(centerPoint); + return { centerPoint }; }), - [CircleAction.DRAW_TEMP_CIRCLE]: ({ context, event }) => { - const activeCircle = new CircleEntity( - getActiveLayerId(), - context.centerPoint as Point, - pointDistance( - (event as DrawEvent).drawController.getWorldMouseLocation(), - context.centerPoint as Point - ) - ); - activeCircle.lineColor = getActiveLineColor(); - activeCircle.lineWidth = getActiveLineWidth(); - activeCircle.lineDash = getActiveLineDash(); - setGhostHelperEntities([activeCircle]); + previewRadius: ({ context, event }) => { + const center = context.centerPoint as Point; + const radius = center.distanceTo(getPointFromEvent(center, event as DrawEvent))[0]; + setGhostHelperEntities([styled(new CircleEntity(getActiveLayerId(), center, radius))]); }, - [CircleAction.DRAW_FINAL_CIRCLE]: assign(({ context, event }) => { - if (!context.centerPoint) { - throw new Error( - 'Trying to DRAW_FINAL_CIRCLE when centerPoint is not yet defined in circle tool' - ); - } - const pointOnCircle: Point = getPointFromEvent( - context.centerPoint, - event as PointInputEvent - ); - const activeCircle = new CircleEntity( - getActiveLayerId(), - context.centerPoint as Point, - pointDistance(pointOnCircle, context.centerPoint as Point) - ); - activeCircle.lineColor = getActiveLineColor(); - activeCircle.lineWidth = getActiveLineWidth(); - activeCircle.lineDash = getActiveLineDash(); - addEntities([activeCircle], true); - - setGhostHelperEntities([]); - return { - centerPoint: null, - }; + previewDiameter: ({ context, event }) => { + const center = context.centerPoint as Point; + const radius = center.distanceTo(getPointFromEvent(center, event as DrawEvent))[0] / 2; + setGhostHelperEntities([styled(new CircleEntity(getActiveLayerId(), center, radius))]); + }, + drawByRadius: ({ context, event }) => { + const center = context.centerPoint as Point; + const radius = + event.type === 'NUMBER_INPUT' + ? (event as NumberInputEvent).value + : center.distanceTo(pointAt(center, event))[0]; + addCircle(center, radius); + }, + drawByDiameter: ({ context, event }) => { + const center = context.centerPoint as Point; + const diameter = + event.type === 'NUMBER_INPUT' + ? (event as NumberInputEvent).value + : center.distanceTo(pointAt(center, event))[0]; + addCircle(center, diameter / 2); + }, + recordPoint: assign(({ context, event }) => { + const point = pointAt(lastOf(context.points), event); + setAngleGuideOriginPoint(point); + return { points: [...context.points, point] }; }), - [CircleAction.SWITCH_TO_SELECT_TOOL]: () => { - setActiveToolActor(new Actor(selectToolStateMachine)); + previewThrough: ({ context, event }) => { + const last = lastOf(context.points); + if (!last) return; + const circle = circleThrough(context, getPointFromEvent(last, event as DrawEvent)); + setGhostHelperEntities( + circle ? [styled(new CircleEntity(getActiveLayerId(), circle.center, circle.radius))] : [] + ); + }, + drawThrough: ({ context, event }) => { + const circle = circleThrough(context, pointAt(lastOf(context.points), event)); + if (circle) addCircle(circle.center, circle.radius); + else setGhostHelperEntities([]); }, }, } diff --git a/B07_DesignDetail/openwebcad/src/tools/draw/arc-tool.ts b/B07_DesignDetail/openwebcad/src/tools/draw/arc-tool.ts new file mode 100644 index 000000000..3ce1f17d9 --- /dev/null +++ b/B07_DesignDetail/openwebcad/src/tools/draw/arc-tool.ts @@ -0,0 +1,126 @@ +/** + * 호(ARC) — 337 설계 1-1 · 오토캐드 차례(순서 엔진 v2). + * 「호의 시작점 지정:」 → 「호의 두 번째 점 지정 또는 [중심(C)/끝(E)]:」 → 「호의 끝점 지정:」(세 점) · + * C → 「호의 중심점 지정:」 → 「호의 끝점 지정 또는 [각도(A)]:」(A → 「사이각 지정:」) · + * E → 「호의 끝점 지정:」 → 「호의 중심점 지정 또는 [반지름(R)]:」(R → 「호의 반지름 지정:」) · + * 중심 · 끝 · 반지름 방식은 시작점에서 반시계(오토캐드) · 사이각 − = 시계 · 반지름 = 작은 호. 하나 그리면 끝. + */ +import { Point } from '@flatten-js/core'; +import type { Entity } from '../../entities/Entity'; +import { type ArcDefinition, arcThroughThreePoints } from '../../helpers/geometry/shape-points'; +import { addEntities } from '../../state'; +import { Tool } from '../../tools'; +import { arcEntity, lineEntity } from '../factories/entity-factory'; +import { type SequenceInput, createSequenceTool } from '../factories/sequence-tool'; +import { option, rememberDrawEnd } from './draw-steps'; + +const angleOf = (center: Point, point: Point) => Math.atan2(point.y - center.y, point.x - center.x); + +/** 중심 · 시작점 · 끝각 — 반시계(사이각 − 면 시계) */ +const arcAround = (center: Point, start: Point, endAngle: number, ccw = true): ArcDefinition => ({ + center, + radius: center.distanceTo(start)[0], + startAngle: angleOf(center, start), + endAngle, + counterClockwise: ccw, +}); + +/** 시작 · 끝 · 반지름 — 반시계 작은 호 · 반지름이 현의 반보다 작으면 null */ +function arcByRadius(start: Point, end: Point, radius: number): ArcDefinition | null { + const half = start.distanceTo(end)[0] / 2; + if (half === 0 || radius < half) return null; + const rise = Math.sqrt(radius * radius - half * half); + const ux = (end.x - start.x) / (2 * half); + const uy = (end.y - start.y) / (2 * half); + const center = new Point((start.x + end.x) / 2 - uy * rise, (start.y + end.y) / 2 + ux * rise); + return arcAround(center, start, angleOf(center, end)); +} + +/** 지금 값으로 호 — 커서(미리보기)가 마지막 빈 값을 채움 */ +function arcOf(input: SequenceInput, cursor?: Point): ArcDefinition | null { + const start = input.get('start'); + const at = (id: string) => (input.has(id) ? input.get(id) : cursor); + if (input.mode === 'C' || input.mode === 'CA') { + const center = input.get('center'); + if (input.mode === 'CA') { + const sweep = input.has('angle') ? input.get('angle') : null; + if (sweep === null) return null; + return arcAround(center, start, angleOf(center, start) + (sweep * Math.PI) / 180, sweep > 0); + } + const end = at('cEnd'); + return end ? arcAround(center, start, angleOf(center, end)) : null; + } + if (input.mode === 'E' || input.mode === 'ER') { + const end = input.get('eEnd'); + if (input.mode === 'ER') { + const radius = at('radius'); + return radius ? arcByRadius(start, end, end.distanceTo(radius)[0]) : null; + } + const center = at('eCenter'); + return center ? arcAround(center, start, angleOf(center, end)) : null; + } + const third = at('third'); + return third ? arcThroughThreePoints(start, input.get('second'), third) : null; +} + +const POINT_TEXT = '호의 끝점 지정'; +const when = + (...modes: (string | null)[]) => + (input: SequenceInput) => + modes.includes(input.mode); + +export const arcToolStateMachine = createSequenceTool({ + tool: Tool.ARC, + steps: [ + { kind: 'point', id: 'start', prompt: { text: '호의 시작점 지정' } }, + { + kind: 'point', + id: 'second', + prompt: { + text: '호의 두 번째 점 지정', + options: [option('CENTER', 'C', '중심'), option('END', 'E', '끝')], + }, + options: { CENTER: { mode: 'C', next: 'center' }, END: { mode: 'E', next: 'eEnd' } }, + }, + { kind: 'point', id: 'third', prompt: { text: POINT_TEXT }, when: when(null) }, + { kind: 'point', id: 'center', prompt: { text: '호의 중심점 지정' }, when: when('C', 'CA') }, + { + kind: 'point', + id: 'cEnd', + prompt: { text: POINT_TEXT, options: [option('ANGLE', 'A', '각도')] }, + options: { ANGLE: { mode: 'CA', next: 'angle' } }, + when: when('C'), + }, + { kind: 'number', id: 'angle', prompt: { text: '사이각 지정' }, when: when('CA') }, + { kind: 'point', id: 'eEnd', prompt: { text: POINT_TEXT }, when: when('E', 'ER') }, + { + kind: 'point', + id: 'eCenter', + prompt: { text: '호의 중심점 지정', options: [option('RADIUS', 'R', '반지름')] }, + options: { RADIUS: { mode: 'ER', next: 'radius' } }, + when: when('E'), + }, + { kind: 'point', id: 'radius', prompt: { text: '호의 반지름 지정' }, when: when('ER') }, + ], + preview: (input) => { + const arc = input.values.length > 1 ? arcOf(input, input.cursor) : null; + const shown: Entity[] = arc ? [arcEntity(arc)] : []; + const last = input.points()[input.points().length - 1]; + return arc || !last ? shown : [lineEntity(last, input.cursor)]; + }, + commit: (input) => { + const arc = arcOf(input); + if (!arc) { + // 세 점 한 줄 = 선(오토캐드는 거절 — 버리지 않게 선으로) + if (input.mode === null) { + addEntities([lineEntity(input.get('start'), input.get('third'))], true); + } + return; + } + addEntities([arcEntity(arc)], true); + rememberDrawEnd({ + x: arc.center.x + arc.radius * Math.cos(arc.endAngle), + y: arc.center.y + arc.radius * Math.sin(arc.endAngle), + }); + }, +}); diff --git a/B07_DesignDetail/openwebcad/src/tools/draw/basic-draw-tools.ts b/B07_DesignDetail/openwebcad/src/tools/draw/basic-draw-tools.ts index 5e052f815..43eb3d126 100644 --- a/B07_DesignDetail/openwebcad/src/tools/draw/basic-draw-tools.ts +++ b/B07_DesignDetail/openwebcad/src/tools/draw/basic-draw-tools.ts @@ -1,114 +1,65 @@ -/** 그리기 명령 — 폴리선·호·다각형·타원·스플라인·점 (조사표 1절) */ +/** 그리기 명령 — 스플라인·점·도넛 + 폴리선·호·다각형·타원 다시 내보냄 (조사표 1절 · 337 설계 1-1) */ import { CircleEntity } from '../../entities/CircleEntity'; import type { Entity } from '../../entities/Entity'; -import { - arcThroughThreePoints, - ellipsePoints, - regularPolygonPoints, - splinePoints, -} from '../../helpers/geometry/shape-points'; +import type { Point } from '@flatten-js/core'; +import { OPT } from '../../commands/prompt'; +import { splinePoints } from '../../helpers/geometry/shape-points'; import { addEntities, getActiveLayerId } from '../../state'; import { Tool } from '../../tools'; -import { - arcEntity, - lineEntity, - pointEntity, - polyLineEntity, - styled, -} from '../factories/entity-factory'; +import { pointEntity, polyLineEntity, styled } from '../factories/entity-factory'; import { createSequenceTool } from '../factories/sequence-tool'; +import { option } from './draw-steps'; -export const plineToolStateMachine = createSequenceTool({ - tool: Tool.PLINE, - steps: [ - { kind: 'point', instructions: '폴리선의 시작점을 지정하십시오.' }, - { kind: 'point', instructions: '다음 점을 지정하십시오. ENTER로 종료합니다.' }, - ], - repeatLastStep: true, - preview: (input) => { - const preview = polyLineEntity([...input.points(), input.cursor]); - return preview ? [preview] : []; - }, - commit: (input) => { - const polyline = polyLineEntity(input.points()); - if (polyline) addEntities([polyline], true); - }, -}); +export { arcToolStateMachine } from './arc-tool'; +export { ellipseToolStateMachine } from './ellipse-tool'; +export { plineToolStateMachine } from './pline-tool'; +export { polygonToolStateMachine } from './polygon-tool'; -export const arcToolStateMachine = createSequenceTool({ - tool: Tool.ARC, - steps: [ - { kind: 'point', instructions: '호의 시작점을 지정하십시오.' }, - { kind: 'point', instructions: '호가 지나갈 두 번째 점을 지정하십시오.' }, - { kind: 'point', instructions: '호의 끝점을 지정하십시오.' }, - ], - preview: (input) => { - const points = input.points(); - if (points.length === 1) return [lineEntity(points[0], input.cursor)]; - if (points.length === 2) { - const definition = arcThroughThreePoints(points[0], points[1], input.cursor); - return definition ? [arcEntity(definition)] : [lineEntity(points[0], input.cursor)]; - } - return []; - }, - commit: (input) => { - const [start, through, end] = input.points(); - const definition = arcThroughThreePoints(start, through, end); - // 세 점이 일직선이면 호가 성립하지 않으므로 선으로 대체한다 - addEntities([definition ? arcEntity(definition) : lineEntity(start, end)], true); - }, -}); +/** 닫힌 스플라인 — 끝 둘레 이웃을 이어 붙여 그린 뒤 이음 구간만 남김 */ +function closedSplinePoints(points: Point[]): Point[] { + const spans = 12; + const wrapped = splinePoints([points[points.length - 1], ...points, points[0], points[1]], spans); + const loop = wrapped.slice(spans, spans * (points.length + 1)); + return [...loop, loop[0]]; +} -export const polygonToolStateMachine = createSequenceTool({ - tool: Tool.POLYGON, - steps: [ - { kind: 'number', instructions: '면의 수를 입력하십시오 <6>.', defaultValue: 6 }, - { kind: 'point', instructions: '다각형의 중심을 지정하십시오.' }, - { kind: 'point', instructions: '꼭짓점을 지정하십시오 (중심에서의 반지름).' }, - ], - preview: (input) => { - const points = input.points(); - if (points.length !== 1) return []; - const polygon = polyLineEntity(regularPolygonPoints(points[0], input.cursor, input.number(0))); - return polygon ? [polygon] : []; - }, - commit: (input) => { - const [center, vertex] = input.points(); - const polygon = polyLineEntity(regularPolygonPoints(center, vertex, input.number(0))); - if (polygon) addEntities([polygon], true); - }, -}); - -export const ellipseToolStateMachine = createSequenceTool({ - tool: Tool.ELLIPSE, - steps: [ - { kind: 'point', instructions: '타원의 중심을 지정하십시오.' }, - { kind: 'point', instructions: '장축의 끝점을 지정하십시오.' }, - { kind: 'point', instructions: '단축 거리를 지정하십시오.' }, - ], - preview: (input) => { - const points = input.points(); - if (points.length === 1) return [lineEntity(points[0], input.cursor)]; - if (points.length === 2) { - const minor = points[0].distanceTo(input.cursor)[0]; - const ellipse = polyLineEntity(ellipsePoints(points[0], points[1], minor)); - return ellipse ? [ellipse] : []; - } - return []; - }, - commit: (input) => { - const [center, majorPoint, minorPoint] = input.points(); - const minorRadius = center.distanceTo(minorPoint)[0]; - const ellipse = polyLineEntity(ellipsePoints(center, majorPoint, minorRadius)); - if (ellipse) addEntities([ellipse], true); - }, -}); +const NEXT_POINT = '다음 점 입력'; +const TOLERANCE = option('TOLERANCE', 'L', '공차'); +/** + * 스플라인(SPLINE) — 337 설계 1-1. 「첫 번째 점 지정:」 → 「다음 점 입력:」 → 「다음 점 입력 또는 [명령 취소(U)]:」 → + * 넷째 점부터 [명령 취소(U)/닫기(C)] 되풀이 · 빈 Enter = 확정 · C = 닫고 끝 · Esc = 버림. + */ export const splineToolStateMachine = createSequenceTool({ tool: Tool.SPLINE, steps: [ - { kind: 'point', instructions: '스플라인의 첫 점을 지정하십시오.' }, - { kind: 'point', instructions: '다음 점을 지정하십시오. ENTER로 종료합니다.' }, + { + kind: 'point', + prompt: { + text: '첫 번째 점 지정', + absent: [ + option('METHOD', 'M', '방법'), + option('KNOTS', 'K', '매듭'), + option('OBJECT', 'O', '객체'), + ], + }, + }, + { + kind: 'point', + prompt: { + text: NEXT_POINT, + absent: [option('TANGENT', 'T', '시작 접촉부'), TOLERANCE], + }, + }, + { + kind: 'point', + prompt: (input) => ({ + text: NEXT_POINT, + options: input.points().length >= 3 ? [OPT.UNDO, OPT.CLOSE] : [OPT.UNDO], + absent: [option('TANGENT', 'T', '끝 접촉부'), TOLERANCE], + }), + options: { UNDO: 'undo', CLOSE: 'close' }, + }, ], repeatLastStep: true, preview: (input) => { @@ -116,7 +67,8 @@ export const splineToolStateMachine = createSequenceTool({ return preview ? [preview] : []; }, commit: (input) => { - const spline = polyLineEntity(splinePoints(input.points())); + const points = input.points(); + const spline = polyLineEntity(input.closed ? closedSplinePoints(points) : splinePoints(points)); if (spline) addEntities([spline], true); }, }); @@ -138,6 +90,7 @@ export const donutToolStateMachine = createSequenceTool({ { kind: 'number', instructions: '도넛의 외부 지름을 입력하십시오 <2>.', defaultValue: 2 }, { kind: 'point', instructions: '도넛의 중심을 지정하십시오.' }, ], + loopFrom: 2, // 오토캐드 — 중심 되풀이 · 빈 Enter 끝 preview: (input) => donutEntities(input.number(0), input.number(1), input.cursor), commit: (input) => { addEntities(donutEntities(input.number(0), input.number(1), input.point(2)), true); diff --git a/B07_DesignDetail/openwebcad/src/tools/draw/draw-steps.ts b/B07_DesignDetail/openwebcad/src/tools/draw/draw-steps.ts new file mode 100644 index 000000000..4a7cbb7e2 --- /dev/null +++ b/B07_DesignDetail/openwebcad/src/tools/draw/draw-steps.ts @@ -0,0 +1,39 @@ +/** + * 손으로 짠 그리기 도구 공용(337 설계 2-14) — 점 물음 사건 · 옵션 한 줄 · 끝 상태 · 마지막 그린 끝점(LINE.END). + * 선 · 폴리선 · 원 · 사각형 · 다각형 · 타원이 같이 씀. + */ +import { Point } from '@flatten-js/core'; +import { type OptionSpec, type XY, recall, remember } from '../../commands/prompt'; +import { END_STATE } from '../tool.types'; + +/** 옵션 하나 — 그림 「label(kw)」 · 사건 `OPTION.` · 맞추기 kw · en */ +export const option = (key: string, kw: string, label: string, en = key): OptionSpec => ({ + key, + kw, + en, + label, +}); + +/** 첫 점 물음(기준점 없음) — 누름 · 절대 좌표만 */ +export const firstPointEvents = (actions: string | string[], target: string) => ({ + MOUSE_CLICK: { actions, target }, + ABSOLUTE_POINT_INPUT: { actions, target }, +}); + +/** 기준점 있는 점 물음 — 누름 · 절대 · 상대 · 거리 숫자 */ +export const pointEvents = (actions: string | string[], target: string) => ({ + ...firstPointEvents(actions, target), + RELATIVE_POINT_INPUT: { actions, target }, + NUMBER_INPUT: { actions, target }, +}); + +/** 명령 끝 상태 — 들면 쉬는 상태(선택 도구)로 */ +export const END = { [END_STATE]: { type: 'final' as const } }; + +// 마지막 선 · 폴리선 · 호 끝점 — LINE · PLINE 첫 점 빈 Enter 가 여기서 이어 그림(오토캐드) +const LINE_END = 'LINE.END'; +export const rememberDrawEnd = (point: XY): void => remember(LINE_END, { x: point.x, y: point.y }); +export const lastDrawEnd = (): Point | null => { + const point = recall(LINE_END); + return point ? new Point(point.x, point.y) : null; +}; diff --git a/B07_DesignDetail/openwebcad/src/tools/draw/ellipse-tool.ts b/B07_DesignDetail/openwebcad/src/tools/draw/ellipse-tool.ts new file mode 100644 index 000000000..019c0008c --- /dev/null +++ b/B07_DesignDetail/openwebcad/src/tools/draw/ellipse-tool.ts @@ -0,0 +1,176 @@ +/** + * 타원(ELLIPSE) — 337 설계 1-1 · 오토캐드 차례. + * 「타원의 축 끝점 지정 또는 [중심(C)]:」 → 「축의 다른 끝점 지정:」 → 「다른 축으로 거리를 지정 또는 [회전(R)]:」 · + * C → 「타원의 중심 지정:」 → 「축의 끝점 지정:」 → 같은 거리 물음 · R → 「주축 둘레로 회전 지정:」(다른 축 = 축 × cos). + * 다른 축 거리 = 중심에서 잰 반 길이(숫자도 · N10). 하나 그리면 끝. + */ +import { Point } from '@flatten-js/core'; +import { assign, createMachine } from 'xstate'; +import { getPointFromEvent } from '../../helpers/get-point-from-event'; +import { ellipsePoints } from '../../helpers/geometry/shape-points'; +import { + addEntities, + setAngleGuideOriginPoint, + setGhostHelperEntities, + setSelectedEntityIds, + setShouldDrawHelpers, +} from '../../state'; +import { Tool } from '../../tools'; +import { lineEntity, polyLineEntity } from '../factories/entity-factory'; +import { + type DrawEvent, + END_STATE, + type NumberInputEvent, + type PointInputEvent, + type StateEvent, + type ToolContext, +} from '../tool.types'; +import { END, firstPointEvents, option, pointEvents } from './draw-steps'; + +export interface EllipseContext extends ToolContext { + /** 축 끝점 방식의 첫 끝 · 중심 방식의 중심 */ + first: Point | null; + center: Point | null; + axisEnd: Point | null; +} + +enum S { + INIT = 'INIT', + AXIS_START = 'AXIS_START', + AXIS_END = 'AXIS_END', + CENTER = 'CENTER', + CENTER_AXIS = 'CENTER_AXIS', + DISTANCE = 'DISTANCE', + ROTATION = 'ROTATION', +} + +const ellipseOf = (context: EllipseContext, other: number) => + polyLineEntity(ellipsePoints(context.center as Point, context.axisEnd as Point, other)); + +const add = (context: EllipseContext, other: number) => { + const ellipse = ellipseOf(context, other); + setGhostHelperEntities([]); + if (ellipse) addEntities([ellipse], true); +}; + +/** 축이 정해짐 — 다른 축 거리의 고무줄 · 거리 숫자 기준 = 중심 */ +const axisDone = (center: Point, axisEnd: Point) => { + setAngleGuideOriginPoint(center); + return { center, axisEnd }; +}; + +export const ellipseToolStateMachine = createMachine( + { + types: {} as { context: EllipseContext; events: StateEvent }, + context: { first: null, center: null, axisEnd: null, type: Tool.ELLIPSE }, + initial: S.INIT, + states: { + [S.INIT]: { always: { actions: 'init', target: S.AXIS_START } }, + [S.AXIS_START]: { + meta: { + kind: 'point', + prompt: { + text: '타원의 축 끝점 지정', + options: [option('CENTER', 'C', '중심')], + absent: [option('ARC', 'A', '호')], + }, + }, + on: { + ...firstPointEvents('recordFirst', S.AXIS_END), + 'OPTION.CENTER': { target: S.CENTER }, + ESC: { target: END_STATE }, + }, + }, + [S.AXIS_END]: { + meta: { kind: 'point', prompt: { text: '축의 다른 끝점 지정' } }, + on: { + DRAW: { actions: 'previewAxis' }, + ...pointEvents('recordAxisEnd', S.DISTANCE), + ESC: { target: END_STATE }, + }, + }, + [S.CENTER]: { + meta: { kind: 'point', prompt: { text: '타원의 중심 지정' } }, + on: { ...firstPointEvents('recordFirst', S.CENTER_AXIS), ESC: { target: END_STATE } }, + }, + [S.CENTER_AXIS]: { + meta: { kind: 'point', prompt: { text: '축의 끝점 지정' } }, + on: { + DRAW: { actions: 'previewAxis' }, + ...pointEvents('recordCenterAxis', S.DISTANCE), + ESC: { target: END_STATE }, + }, + }, + [S.DISTANCE]: { + meta: { + kind: 'point', + prompt: { + text: '다른 축으로 거리를 지정', + options: [option('ROTATION', 'R', '회전')], + number: { positive: true }, + }, + }, + on: { + DRAW: { actions: 'previewDistance' }, + ...pointEvents('drawByDistance', END_STATE), + 'OPTION.ROTATION': { target: S.ROTATION }, + ESC: { target: END_STATE }, + }, + }, + [S.ROTATION]: { + meta: { + kind: 'number', + prompt: { text: '주축 둘레로 회전 지정', number: { min: 0, max: 89.4 } }, + }, + on: { + NUMBER_INPUT: { actions: 'drawByRotation', target: END_STATE }, + ESC: { target: END_STATE }, + }, + }, + ...END, + }, + }, + { + actions: { + init: () => { + setShouldDrawHelpers(true); + setSelectedEntityIds([]); + setGhostHelperEntities([]); + setAngleGuideOriginPoint(null); + }, + recordFirst: assign(({ event }) => { + const first = getPointFromEvent(null, event as PointInputEvent); + setAngleGuideOriginPoint(first); + return { first }; + }), + previewAxis: ({ context, event }) => { + const first = context.first as Point; + setGhostHelperEntities([lineEntity(first, getPointFromEvent(first, event as DrawEvent))]); + }, + recordAxisEnd: assign(({ context, event }) => { + const first = context.first as Point; + const end = getPointFromEvent(first, event as PointInputEvent); + return axisDone(new Point((first.x + end.x) / 2, (first.y + end.y) / 2), end); + }), + recordCenterAxis: assign(({ context, event }) => { + const center = context.first as Point; + return axisDone(center, getPointFromEvent(center, event as PointInputEvent)); + }), + previewDistance: ({ context, event }) => { + const center = context.center as Point; + const other = center.distanceTo(getPointFromEvent(center, event as DrawEvent))[0]; + const ellipse = ellipseOf(context, other); + setGhostHelperEntities(ellipse ? [ellipse] : []); + }, + drawByDistance: ({ context, event }) => { + const center = context.center as Point; + add(context, center.distanceTo(getPointFromEvent(center, event as PointInputEvent))[0]); + }, + drawByRotation: ({ context, event }) => { + const axis = (context.center as Point).distanceTo(context.axisEnd as Point)[0]; + const angle = ((event as NumberInputEvent).value * Math.PI) / 180; + add(context, axis * Math.cos(angle)); + }, + }, + } +); diff --git a/B07_DesignDetail/openwebcad/src/tools/draw/pline-tool.ts b/B07_DesignDetail/openwebcad/src/tools/draw/pline-tool.ts new file mode 100644 index 000000000..1b02babb0 --- /dev/null +++ b/B07_DesignDetail/openwebcad/src/tools/draw/pline-tool.ts @@ -0,0 +1,277 @@ +/** + * 폴리선(PLINE) — 337 설계 1-1 · 오토캐드 차례. + * 「시작점 지정:」(빈 Enter = 마지막 끝점에서) → 「다음 점 지정 또는 [호(A)/길이(L)/명령 취소(U)]:」 되풀이 · + * 둘째 토막부터 [호(A)/닫기(C)/길이(L)/명령 취소(U)] · 호 모드 「호의 끝점 지정 또는 [닫기(CL)/선(L)/명령 취소(U)]:」. + * 호 = 앞 토막 끝 방향에 접하는 호(첫 토막이면 동쪽 0°) · 길이 = 앞 토막 방향으로 그 길이 · + * 빈 Enter · Esc = 그린 데까지 폴리선 남기고 끝. + */ +import { Point } from '@flatten-js/core'; +import { assign, createMachine } from 'xstate'; +import { OPT, type PromptSpec, type XY } from '../../commands/prompt'; +import type { Entity } from '../../entities/Entity'; +import { PolyLineEntity } from '../../entities/PolyLineEntity'; +import { getPointFromEvent } from '../../helpers/get-point-from-event'; +import type { ArcDefinition } from '../../helpers/geometry/shape-points'; +import { + addEntities, + getActiveLayerId, + setAngleGuideOriginPoint, + setGhostHelperEntities, + setSelectedEntityIds, + setShouldDrawHelpers, +} from '../../state'; +import { Tool } from '../../tools'; +import { arcEntity, lineEntity, styled } from '../factories/entity-factory'; +import { + type DrawEvent, + END_STATE, + type NumberInputEvent, + type PointInputEvent, + type StateEvent, + type ToolContext, +} from '../tool.types'; +import { + END, + firstPointEvents, + lastDrawEnd, + option, + pointEvents, + rememberDrawEnd, +} from './draw-steps'; + +/** 토막 하나 — 끝점 · 호면 그 정의(없으면 선) */ +interface Segment { + end: Point; + arc: ArcDefinition | null; +} + +export interface PlineContext extends ToolContext { + start: Point | null; + segments: Segment[]; +} + +enum S { + INIT = 'INIT', + START = 'START', + LINE = 'LINE', + ARC = 'ARC', + LENGTH = 'LENGTH', +} + +const EAST: XY = { x: 1, y: 0 }; + +const lastEnd = (context: PlineContext): Point => + context.segments[context.segments.length - 1]?.end ?? (context.start as Point); + +/** 마지막 토막 끝의 진행 방향(단위) — 선 = 그 방향 · 호 = 끝 접선 · 토막 없음 = 동쪽 */ +function endDirection(context: PlineContext): XY { + const count = context.segments.length; + if (!count) return EAST; + const { end, arc } = context.segments[count - 1]; + if (arc) { + const rx = (end.x - arc.center.x) / arc.radius; + const ry = (end.y - arc.center.y) / arc.radius; + return arc.counterClockwise ? { x: -ry, y: rx } : { x: ry, y: -rx }; + } + const from = count > 1 ? context.segments[count - 2].end : (context.start as Point); + const length = Math.hypot(end.x - from.x, end.y - from.y); + return length > 0 ? { x: (end.x - from.x) / length, y: (end.y - from.y) / length } : EAST; +} + +/** from 에서 방향 dir 에 접하고 to 를 지나는 호 — 한 줄 위면 null(선) */ +export function tangentArc(from: Point, dir: XY, to: Point): ArcDefinition | null { + const dx = to.x - from.x; + const dy = to.y - from.y; + const cross = dir.x * dy - dir.y * dx; + if (Math.abs(cross) < 1e-9) return null; + const signed = (dx * dx + dy * dy) / (2 * cross); // + = 왼쪽 중심(반시계) + const center = new Point(from.x - dir.y * signed, from.y + dir.x * signed); + return { + center, + radius: Math.abs(signed), + startAngle: Math.atan2(from.y - center.y, from.x - center.x), + endAngle: Math.atan2(to.y - center.y, to.x - center.x), + counterClockwise: cross > 0, + }; +} + +const segmentEntity = (from: Point, segment: Segment): Entity => + segment.arc ? arcEntity(segment.arc) : lineEntity(from, segment.end); + +function segmentEntities(context: PlineContext, extra?: Segment): Entity[] { + const list = extra ? [...context.segments, extra] : context.segments; + let from = context.start as Point; + return list.map((segment) => { + const entity = segmentEntity(from, segment); + from = segment.end; + return entity; + }); +} + +const lineTo = (end: Point): Segment => ({ end, arc: null }); +const arcTo = (context: PlineContext, end: Point): Segment => ({ + end, + arc: tangentArc(lastEnd(context), endDirection(context), end), +}); + +const ARC = option('ARC', 'A', '호'); +const LENGTH = option('LENGTH', 'L', '길이'); +const LINE = option('LINE', 'L', '선'); +const ARC_CLOSE = option('CLOSE', 'CL', '닫기'); +const WIDTHS = [option('HALFWIDTH', 'H', '반폭'), option('WIDTH', 'W', '폭')]; +const hasSegment = (context: Partial) => (context.segments?.length ?? 0) > 0; + +const linePrompt = (context: Partial): PromptSpec => ({ + text: '다음 점 지정', + options: hasSegment(context) ? [ARC, OPT.CLOSE, LENGTH, OPT.UNDO] : [ARC, LENGTH, OPT.UNDO], + absent: WIDTHS, +}); +const arcPrompt = (context: Partial): PromptSpec => ({ + text: '호의 끝점 지정', + options: hasSegment(context) ? [ARC_CLOSE, LINE, OPT.UNDO] : [LINE, OPT.UNDO], + absent: [ + option('ANGLE', 'A', '각도'), + option('CENTER', 'CE', '중심'), + option('DIRECTION', 'D', '방향'), + option('RADIUS', 'R', '반지름'), + option('SECOND', 'S', '두 번째 점'), + ...WIDTHS, + ], +}); + +/** 토막 하나 더함 — 기준점 · 끝점 기억 */ +const pushSegment = (context: PlineContext, segment: Segment) => { + setGhostHelperEntities([]); + setAngleGuideOriginPoint(segment.end); + return { segments: [...context.segments, segment] }; +}; + +export const plineToolStateMachine = createMachine( + { + types: {} as { context: PlineContext; events: StateEvent }, + context: { start: null, segments: [], type: Tool.PLINE }, + initial: S.INIT, + states: { + [S.INIT]: { always: { actions: 'init', target: S.START } }, + [S.START]: { + meta: { kind: 'point', prompt: { text: '시작점 지정' } }, + on: { + ...firstPointEvents('recordStart', S.LINE), + ENTER: { guard: () => !!lastDrawEnd(), actions: 'continueFromLast', target: S.LINE }, + ESC: { target: END_STATE }, + }, + }, + [S.LINE]: { + meta: { kind: 'point', prompt: linePrompt }, + on: { + DRAW: { actions: 'previewLine' }, + ...pointEvents('addLine', S.LINE), + 'OPTION.ARC': { target: S.ARC }, + 'OPTION.LENGTH': { target: S.LENGTH }, + 'OPTION.CLOSE': { + guard: 'hasSegment', + actions: ['closeLine', 'commit'], + target: END_STATE, + }, + 'OPTION.UNDO': { actions: 'undoSegment' }, + ENTER: { actions: 'commit', target: END_STATE }, + ESC: { actions: 'commit', target: END_STATE }, + }, + }, + [S.ARC]: { + meta: { kind: 'point', prompt: arcPrompt }, + on: { + DRAW: { actions: 'previewArc' }, + ...pointEvents('addArc', S.ARC), + 'OPTION.LINE': { target: S.LINE }, + 'OPTION.CLOSE': { + guard: 'hasSegment', + actions: ['closeArc', 'commit'], + target: END_STATE, + }, + 'OPTION.UNDO': { actions: 'undoSegment' }, + ENTER: { actions: 'commit', target: END_STATE }, + ESC: { actions: 'commit', target: END_STATE }, + }, + }, + [S.LENGTH]: { + meta: { kind: 'number', prompt: { text: '선의 길이 지정', number: { positive: true } } }, + on: { + NUMBER_INPUT: { actions: 'addLength', target: S.LINE }, + ESC: { actions: 'commit', target: END_STATE }, + }, + }, + ...END, + }, + }, + { + // 옵션 사건은 이름 붙인 guard 로(줄 안 guard 는 글자 사건 꼴과 타입이 안 맞음) + guards: { hasSegment: ({ context }) => hasSegment(context) }, + actions: { + init: assign(() => { + setShouldDrawHelpers(true); + setSelectedEntityIds([]); + setGhostHelperEntities([]); + setAngleGuideOriginPoint(null); + return { start: null, segments: [] }; + }), + recordStart: assign(({ event }) => { + const start = getPointFromEvent(null, event as PointInputEvent); + setAngleGuideOriginPoint(start); + return { start }; + }), + continueFromLast: assign(() => { + const start = lastDrawEnd() as Point; + setAngleGuideOriginPoint(start); + return { start }; + }), + previewLine: ({ context, event }) => { + const end = getPointFromEvent(lastEnd(context), event as DrawEvent); + setGhostHelperEntities(segmentEntities(context, lineTo(end))); + }, + previewArc: ({ context, event }) => { + const end = getPointFromEvent(lastEnd(context), event as DrawEvent); + setGhostHelperEntities(segmentEntities(context, arcTo(context, end))); + }, + addLine: assign(({ context, event }) => + pushSegment(context, lineTo(getPointFromEvent(lastEnd(context), event as PointInputEvent))) + ), + addArc: assign(({ context, event }) => + pushSegment( + context, + arcTo(context, getPointFromEvent(lastEnd(context), event as PointInputEvent)) + ) + ), + addLength: assign(({ context, event }) => { + const from = lastEnd(context); + const dir = endDirection(context); + const length = (event as NumberInputEvent).value; + return pushSegment( + context, + lineTo(new Point(from.x + dir.x * length, from.y + dir.y * length)) + ); + }), + closeLine: assign(({ context }) => ({ + segments: [...context.segments, lineTo(context.start as Point)], + })), + closeArc: assign(({ context }) => ({ + segments: [...context.segments, arcTo(context, context.start as Point)], + })), + undoSegment: assign(({ context }) => { + const segments = context.segments.slice(0, -1); + setGhostHelperEntities([]); + setAngleGuideOriginPoint(segments[segments.length - 1]?.end ?? context.start); + return { segments }; + }), + commit: ({ context }) => { + setGhostHelperEntities([]); + if (!context.segments.length) return; + addEntities( + [styled(new PolyLineEntity(getActiveLayerId(), segmentEntities(context)))], + true + ); + rememberDrawEnd(lastEnd(context)); + }, + }, + } +); diff --git a/B07_DesignDetail/openwebcad/src/tools/draw/polygon-tool.ts b/B07_DesignDetail/openwebcad/src/tools/draw/polygon-tool.ts new file mode 100644 index 000000000..9a1dd83ed --- /dev/null +++ b/B07_DesignDetail/openwebcad/src/tools/draw/polygon-tool.ts @@ -0,0 +1,220 @@ +/** + * 폴리곤(POLYGON) — 337 설계 1-1 · 오토캐드 차례. + * 「변의 수 입력 <4>:」(3~1024 정수 · 기억) → 「폴리곤의 중심 지정 또는 [모서리(E)]:」 → + * 「옵션 입력 [원에 내접(I)/원에 외접(C)] :」(기억) → 「원의 반지름 지정:」 · + * E → 「모서리의 첫 번째 끝점 지정:」 → 「모서리의 두 번째 끝점 지정:」(그 모서리에서 반시계). + * 반지름 숫자 = 아래 변 수평 · 점 = 크기 + 돌림(내접 = 꼭짓점 · 외접 = 변 가운데). 하나 그리면 끝. + */ +import { Point } from '@flatten-js/core'; +import { assign, createMachine } from 'xstate'; +import { type PromptSpec, recall, remember } from '../../commands/prompt'; +import { getPointFromEvent } from '../../helpers/get-point-from-event'; +import { regularPolygonPoints } from '../../helpers/geometry/shape-points'; +import { + addEntities, + setAngleGuideOriginPoint, + setGhostHelperEntities, + setSelectedEntityIds, + setShouldDrawHelpers, +} from '../../state'; +import { Tool } from '../../tools'; +import { polyLineEntity } from '../factories/entity-factory'; +import { + type DrawEvent, + END_STATE, + type NumberInputEvent, + type PointInputEvent, + type StateEvent, + type ToolContext, +} from '../tool.types'; +import { END, firstPointEvents, option, pointEvents } from './draw-steps'; + +export interface PolygonContext extends ToolContext { + sides: number; + center: Point | null; + edgeStart: Point | null; +} + +enum S { + INIT = 'INIT', + SIDES = 'SIDES', + CENTER = 'CENTER', + FIT = 'FIT', + RADIUS = 'RADIUS', + EDGE_START = 'EDGE_START', + EDGE_END = 'EDGE_END', +} + +const SIDES_KEY = 'POLYSIDES'; +const FIT_KEY = 'POLYGON.IC'; +const INSCRIBED = option('INSCRIBED', 'I', '원에 내접'); +const CIRCUMSCRIBED = option('CIRCUMSCRIBED', 'C', '원에 외접'); +const circumscribed = () => recall(FIT_KEY) === CIRCUMSCRIBED.key; + +const SIDES_PROMPT: PromptSpec = { + text: '변의 수 입력', + default: { kind: 'value', remember: SIDES_KEY, initial: 4, format: 'int' }, + number: { int: true, min: 3, max: 1024 }, +}; +const fitPrompt = (): PromptSpec => { + const fit = circumscribed() ? CIRCUMSCRIBED : INSCRIBED; + return { + text: '옵션 입력', + options: [INSCRIBED, CIRCUMSCRIBED], + default: { kind: 'option', key: fit.key, label: fit.kw }, + }; +}; + +/** + * 첫 꼭짓점 — 내접 = 원 위 꼭짓점 · 외접 = 변 가운데가 원 위(꼭짓점은 반 칸 돌아 R / cos(π/n)). + * 각 null = 아래 변 수평. + */ +function firstVertex(center: Point, radius: number, angle: number | null, sides: number): Point { + const half = Math.PI / sides; + const outer = circumscribed(); + const toward = angle ?? -Math.PI / 2 + (outer ? 0 : half); // 아래 변 가운데(외접) · 그 오른쪽 꼭짓점(내접) + const at = outer ? toward - half : toward; + const reach = outer ? radius / Math.cos(half) : radius; + return new Point(center.x + reach * Math.cos(at), center.y + reach * Math.sin(at)); +} + +function polygonFrom(center: Point, point: Point | null, radius: number, sides: number) { + const angle = point ? Math.atan2(point.y - center.y, point.x - center.x) : null; + return polyLineEntity( + regularPolygonPoints(center, firstVertex(center, radius, angle, sides), sides) + ); +} + +/** 모서리 a → b 에서 반시계로 — 중심은 모서리 왼쪽 */ +function polygonOnEdge(a: Point, b: Point, sides: number) { + const length = a.distanceTo(b)[0]; + if (!length) return null; + const apothem = length / 2 / Math.tan(Math.PI / sides); + const center = new Point( + (a.x + b.x) / 2 - ((b.y - a.y) / length) * apothem, + (a.y + b.y) / 2 + ((b.x - a.x) / length) * apothem + ); + return polyLineEntity(regularPolygonPoints(center, a, sides)); +} + +const add = (entity: ReturnType) => { + setGhostHelperEntities([]); + if (entity) addEntities([entity], true); +}; + +export const polygonToolStateMachine = createMachine( + { + types: {} as { context: PolygonContext; events: StateEvent }, + context: { sides: 4, center: null, edgeStart: null, type: Tool.POLYGON }, + initial: S.INIT, + states: { + [S.INIT]: { always: { actions: 'init', target: S.SIDES } }, + [S.SIDES]: { + meta: { kind: 'number', prompt: SIDES_PROMPT }, + on: { + NUMBER_INPUT: { actions: 'recordSides', target: S.CENTER }, + ESC: { target: END_STATE }, + }, + }, + [S.CENTER]: { + meta: { + kind: 'point', + prompt: { text: '폴리곤의 중심 지정', options: [option('EDGE', 'E', '모서리')] }, + }, + on: { + ...firstPointEvents('recordCenter', S.FIT), + 'OPTION.EDGE': { target: S.EDGE_START }, + ESC: { target: END_STATE }, + }, + }, + [S.FIT]: { + meta: { kind: 'keyword', prompt: fitPrompt }, + on: { + 'OPTION.INSCRIBED': { actions: 'recordFit', target: S.RADIUS }, + 'OPTION.CIRCUMSCRIBED': { actions: 'recordFit', target: S.RADIUS }, + ESC: { target: END_STATE }, + }, + }, + [S.RADIUS]: { + meta: { + kind: 'point', + prompt: { text: '원의 반지름 지정', number: { positive: true } }, + }, + on: { + DRAW: { actions: 'previewRadius' }, + ...pointEvents('drawByRadius', END_STATE), + ESC: { target: END_STATE }, + }, + }, + [S.EDGE_START]: { + meta: { kind: 'point', prompt: { text: '모서리의 첫 번째 끝점 지정' } }, + on: { ...firstPointEvents('recordEdge', S.EDGE_END), ESC: { target: END_STATE } }, + }, + [S.EDGE_END]: { + meta: { kind: 'point', prompt: { text: '모서리의 두 번째 끝점 지정' } }, + on: { + DRAW: { actions: 'previewEdge' }, + ...pointEvents('drawByEdge', END_STATE), + ESC: { target: END_STATE }, + }, + }, + ...END, + }, + }, + { + actions: { + init: () => { + setShouldDrawHelpers(true); + setSelectedEntityIds([]); + setGhostHelperEntities([]); + setAngleGuideOriginPoint(null); + }, + recordSides: assign(({ event }) => { + const sides = (event as NumberInputEvent).value; + remember(SIDES_KEY, sides); + return { sides }; + }), + recordCenter: assign(({ event }) => { + const center = getPointFromEvent(null, event as PointInputEvent); + setAngleGuideOriginPoint(center); // 반지름 물음의 고무줄 · 거리 숫자 기준 + return { center }; + }), + recordFit: ({ event }) => remember(FIT_KEY, event.type.slice('OPTION.'.length)), + previewRadius: ({ context, event }) => { + const center = context.center as Point; + const point = getPointFromEvent(center, event as DrawEvent); + const polygon = polygonFrom(center, point, center.distanceTo(point)[0], context.sides); + setGhostHelperEntities(polygon ? [polygon] : []); + }, + drawByRadius: ({ context, event }) => { + const center = context.center as Point; + if (event.type === 'NUMBER_INPUT') { + add(polygonFrom(center, null, (event as NumberInputEvent).value, context.sides)); + return; + } + const point = getPointFromEvent(center, event as PointInputEvent); + add(polygonFrom(center, point, center.distanceTo(point)[0], context.sides)); + }, + recordEdge: assign(({ event }) => { + const edgeStart = getPointFromEvent(null, event as PointInputEvent); + setAngleGuideOriginPoint(edgeStart); + return { edgeStart }; + }), + previewEdge: ({ context, event }) => { + const start = context.edgeStart as Point; + const polygon = polygonOnEdge( + start, + getPointFromEvent(start, event as DrawEvent), + context.sides + ); + setGhostHelperEntities(polygon ? [polygon] : []); + }, + drawByEdge: ({ context, event }) => { + const start = context.edgeStart as Point; + add( + polygonOnEdge(start, getPointFromEvent(start, event as PointInputEvent), context.sides) + ); + }, + }, + } +); diff --git a/B07_DesignDetail/openwebcad/src/tools/line-tool.ts b/B07_DesignDetail/openwebcad/src/tools/line-tool.ts index 2f02d7f85..3a4bd31ca 100644 --- a/B07_DesignDetail/openwebcad/src/tools/line-tool.ts +++ b/B07_DesignDetail/openwebcad/src/tools/line-tool.ts @@ -1,35 +1,44 @@ +/** + * 선(LINE) — 337 설계 1-1 · 오토캐드 차례. + * 「첫 번째 점 지정:」(빈 Enter = 마지막 선 · 폴리선 · 호 끝점에서) → 「다음 점 지정 또는 [명령 취소(U)]:」 되풀이 · + * 둘째 토막부터 [닫기(C)/명령 취소(U)]. 토막마다 바로 도면에 · U = 마지막 토막(되돌림 한 칸) · C = 첫 점으로 닫고 끝 · + * 빈 Enter · Esc = 끝(그린 토막 남음). + */ import type { Point } from '@flatten-js/core'; -import { LineEntity } from '../entities/LineEntity'; +import { assign, createMachine } from 'xstate'; +import { OPT, type PromptSpec } from '../commands/prompt'; +import type { LineEntity } from '../entities/LineEntity'; +import { getPointFromEvent } from '../helpers/get-point-from-event.ts'; import { addEntities, - deleteEntities, - getActiveLayerId, - getActiveLineColor, - getActiveLineDash, - getActiveLineWidth, - setActiveToolActor, setAngleGuideOriginPoint, setGhostHelperEntities, setSelectedEntityIds, setShouldDrawHelpers, + undo, } from '../state'; import { Tool } from '../tools'; -import { Actor, assign, createMachine } from 'xstate'; -import type { - DrawEvent, - PointInputEvent, - StateEvent, - TextInputEvent, - ToolContext, +import { + END, + firstPointEvents, + lastDrawEnd, + pointEvents, + rememberDrawEnd, +} from './draw/draw-steps'; +import { lineEntity } from './factories/entity-factory'; +import { + type DrawEvent, + END_STATE, + type PointInputEvent, + type StateEvent, + type ToolContext, } from './tool.types'; -import { selectToolStateMachine } from './select-tool.ts'; -import { getPointFromEvent } from '../helpers/get-point-from-event.ts'; export interface LineContext extends ToolContext { startPoint: Point | null; /** 이번 이어 그리기의 첫 점 — 닫기(C) */ firstPoint: Point | null; - /** 이번 이어 그리기에서 그린 구간 — 명령취소(U) */ + /** 이번 이어 그리기에서 그린 구간 — 명령 취소(U) */ segments: LineEntity[]; } @@ -42,13 +51,25 @@ export enum LineState { export enum LineAction { INIT_LINE_TOOL = 'INIT_LINE_TOOL', RECORD_START_POINT = 'RECORD_START_POINT', + CONTINUE_FROM_LAST = 'CONTINUE_FROM_LAST', DRAW_TEMP_LINE = 'DRAW_TEMP_LINE', DRAW_FINAL_LINE = 'DRAW_FINAL_LINE', CLOSE_LINE = 'CLOSE_LINE', UNDO_SEGMENT = 'UNDO_SEGMENT', - SWITCH_TO_SELECT_TOOL = 'SWITCH_TO_SELECT_TOOL', } +const START_PROMPT: PromptSpec = { text: '첫 번째 점 지정' }; +const canClose = (context: Partial) => (context.segments?.length ?? 0) >= 2; +const nextPrompt = (context: Partial): PromptSpec => ({ + text: '다음 점 지정', + options: canClose(context) ? [OPT.CLOSE, OPT.UNDO] : [OPT.UNDO], +}); + +const startAt = (startPoint: Point) => { + setAngleGuideOriginPoint(startPoint); + return { startPoint, firstPoint: startPoint, segments: [] }; +}; + export const lineToolStateMachine = createMachine( { types: {} as { @@ -64,161 +85,94 @@ export const lineToolStateMachine = createMachine( initial: LineState.INIT, states: { [LineState.INIT]: { - description: 'Initializing the line tool', always: { actions: LineAction.INIT_LINE_TOOL, target: LineState.WAITING_FOR_START_POINT, }, }, [LineState.WAITING_FOR_START_POINT]: { - description: 'Select the start point of the line', - meta: { - instructions: '첫 점 지정 (x,y):', - }, + meta: { kind: 'point', prompt: START_PROMPT }, on: { - MOUSE_CLICK: { - actions: LineAction.RECORD_START_POINT, + ...firstPointEvents(LineAction.RECORD_START_POINT, LineState.WAITING_FOR_END_POINT), + ENTER: { + guard: () => !!lastDrawEnd(), + actions: LineAction.CONTINUE_FROM_LAST, target: LineState.WAITING_FOR_END_POINT, }, - ABSOLUTE_POINT_INPUT: { - actions: LineAction.RECORD_START_POINT, - target: LineState.WAITING_FOR_END_POINT, - }, - ESC: { - actions: LineAction.SWITCH_TO_SELECT_TOOL, - }, + ESC: { target: END_STATE }, }, }, [LineState.WAITING_FOR_END_POINT]: { - description: 'Select the end point of the line', - meta: { - instructions: '다음 점 지정 또는 [닫기(C)/명령취소(U)]:', - options: ['C', 'U'], - }, + meta: { kind: 'point', prompt: nextPrompt }, on: { - DRAW: { - actions: LineAction.DRAW_TEMP_LINE, + DRAW: { actions: LineAction.DRAW_TEMP_LINE }, + ...pointEvents(LineAction.DRAW_FINAL_LINE, LineState.WAITING_FOR_END_POINT), + 'OPTION.CLOSE': { + guard: 'canClose', + actions: LineAction.CLOSE_LINE, + target: END_STATE, }, - MOUSE_CLICK: { - actions: LineAction.DRAW_FINAL_LINE, - target: LineState.WAITING_FOR_END_POINT, - }, - NUMBER_INPUT: { - actions: LineAction.DRAW_FINAL_LINE, - target: LineState.WAITING_FOR_END_POINT, - }, - ABSOLUTE_POINT_INPUT: { - actions: LineAction.DRAW_FINAL_LINE, - target: LineState.WAITING_FOR_END_POINT, - }, - RELATIVE_POINT_INPUT: { - actions: LineAction.DRAW_FINAL_LINE, - target: LineState.WAITING_FOR_END_POINT, - }, - TEXT_INPUT: [ + 'OPTION.UNDO': [ { - // 닫기 — 둘째 선부터 · 첫 점으로 잇고 끝 - guard: ({ context, event }) => - (event as TextInputEvent).value === 'C' && context.segments.length >= 2, - actions: LineAction.CLOSE_LINE, - target: LineState.INIT, - }, - { - // 명령취소 — 마지막 구간만 - guard: ({ context, event }) => - (event as TextInputEvent).value === 'U' && context.segments.length > 0, + guard: 'hasSegment', actions: LineAction.UNDO_SEGMENT, }, + // 그린 토막 없음 — 첫 점부터 다시 + { actions: LineAction.INIT_LINE_TOOL, target: LineState.WAITING_FOR_START_POINT }, ], - ESC: { - target: LineState.INIT, - }, - ENTER: { - target: LineState.INIT, - }, + ENTER: { target: END_STATE }, + ESC: { target: END_STATE }, }, }, + ...END, }, }, { + // 옵션 사건(`OPTION.*`)은 이름 붙인 guard 로 — 글자 사건 꼴이라 줄 안 guard 는 타입이 안 맞음 + guards: { + canClose: ({ context }) => canClose(context), + hasSegment: ({ context }) => context.segments.length > 0, + }, actions: { [LineAction.INIT_LINE_TOOL]: assign(() => { setShouldDrawHelpers(true); setSelectedEntityIds([]); setGhostHelperEntities([]); setAngleGuideOriginPoint(null); - return { - startPoint: null, - firstPoint: null, - segments: [], - }; - }), - [LineAction.RECORD_START_POINT]: assign(({ event }) => { - const startPoint = getPointFromEvent(null, event as PointInputEvent); - setAngleGuideOriginPoint(startPoint); - return { - startPoint, - firstPoint: startPoint, - segments: [], - }; + return { startPoint: null, firstPoint: null, segments: [] }; }), + [LineAction.RECORD_START_POINT]: assign(({ event }) => + startAt(getPointFromEvent(null, event as PointInputEvent)) + ), + [LineAction.CONTINUE_FROM_LAST]: assign(() => startAt(lastDrawEnd() as Point)), [LineAction.DRAW_TEMP_LINE]: ({ context, event }) => { - const activeLine = new LineEntity( - getActiveLayerId(), - context.startPoint as Point, - getPointFromEvent(context.startPoint, event as DrawEvent) - ); - activeLine.lineColor = getActiveLineColor(); - activeLine.lineWidth = getActiveLineWidth(); - activeLine.lineDash = getActiveLineDash(); - setGhostHelperEntities([activeLine]); + const start = context.startPoint as Point; + setGhostHelperEntities([lineEntity(start, getPointFromEvent(start, event as DrawEvent))]); }, [LineAction.DRAW_FINAL_LINE]: assign(({ context, event }) => { - if (!context.startPoint) { - throw new Error('Start point is not set during DRAW_FINAL_LINE in LineEntity'); - } - - const endPoint = getPointFromEvent(context.startPoint, event as PointInputEvent); - const activeLine = new LineEntity( - getActiveLayerId(), - context.startPoint as Point, - endPoint - ); - activeLine.lineColor = getActiveLineColor(); - activeLine.lineWidth = getActiveLineWidth(); - activeLine.lineDash = getActiveLineDash(); - addEntities([activeLine], true); - - // Keep drawing from the last point - setGhostHelperEntities([new LineEntity(getActiveLayerId(), endPoint, endPoint)]); + const start = context.startPoint as Point; + const endPoint = getPointFromEvent(start, event as PointInputEvent); + const segment = lineEntity(start, endPoint); + addEntities([segment], true); + rememberDrawEnd(endPoint); + setGhostHelperEntities([]); setAngleGuideOriginPoint(endPoint); - return { - startPoint: endPoint, - segments: [...context.segments, activeLine], - }; + return { startPoint: endPoint, segments: [...context.segments, segment] }; }), [LineAction.CLOSE_LINE]: ({ context }) => { - const closing = new LineEntity( - getActiveLayerId(), - context.startPoint as Point, - context.firstPoint as Point - ); - closing.lineColor = getActiveLineColor(); - closing.lineWidth = getActiveLineWidth(); - closing.lineDash = getActiveLineDash(); - addEntities([closing], true); + const first = context.firstPoint as Point; + addEntities([lineEntity(context.startPoint as Point, first)], true); + rememberDrawEnd(first); }, [LineAction.UNDO_SEGMENT]: assign(({ context }) => { const last = context.segments[context.segments.length - 1]; - deleteEntities([last], true); + undo(); // 토막 하나 = 되돌림 한 칸 const startPoint = last.getStartPoint(); - setGhostHelperEntities([new LineEntity(getActiveLayerId(), startPoint, startPoint)]); + rememberDrawEnd(startPoint); + setGhostHelperEntities([]); setAngleGuideOriginPoint(startPoint); return { startPoint, segments: context.segments.slice(0, -1) }; }), - [LineAction.SWITCH_TO_SELECT_TOOL]: () => { - setActiveToolActor(new Actor(selectToolStateMachine)); - }, }, } ); diff --git a/B07_DesignDetail/openwebcad/src/tools/rectangle-tool.ts b/B07_DesignDetail/openwebcad/src/tools/rectangle-tool.ts index afee826d8..a1c886ccd 100644 --- a/B07_DesignDetail/openwebcad/src/tools/rectangle-tool.ts +++ b/B07_DesignDetail/openwebcad/src/tools/rectangle-tool.ts @@ -1,152 +1,159 @@ -import type { Point } from '@flatten-js/core'; +/** + * 직사각형(RECTANG) — 337 설계 1-1 · 오토캐드 차례. + * 「첫 번째 구석점 지정:」 → 「다른 구석점 지정 또는 [치수(D)]:」 · + * D → 「직사각형의 길이 지정 <10.0000>:」 → 「직사각형의 폭 지정 <10.0000>:」(기억 RECT.L · RECT.W) → + * 다시 「다른 구석점 지정 또는 [치수(D)]:」(누른 쪽 사분면으로 놓음). 하나 그리면 끝. + */ +import { Point } from '@flatten-js/core'; +import { assign, createMachine } from 'xstate'; +import { type PromptSpec, remember } from '../commands/prompt'; import { RectangleEntity } from '../entities/RectangleEntity'; +import { getPointFromEvent } from '../helpers/get-point-from-event.ts'; import { addEntities, getActiveLayerId, - getActiveLineColor, - getActiveLineDash, - getActiveLineWidth, setAngleGuideOriginPoint, setGhostHelperEntities, setSelectedEntityIds, setShouldDrawHelpers, } from '../state'; -import type { DrawEvent, PointInputEvent, StateEvent, ToolContext } from './tool.types'; import { Tool } from '../tools'; -import { assign, createMachine } from 'xstate'; -import { getPointFromEvent } from '../helpers/get-point-from-event.ts'; +import { END, firstPointEvents, option, pointEvents } from './draw/draw-steps'; +import { styled } from './factories/entity-factory'; +import { + type DrawEvent, + END_STATE, + type NumberInputEvent, + type PointInputEvent, + type StateEvent, + type ToolContext, +} from './tool.types'; export interface RectangleContext extends ToolContext { startPoint: Point | null; + /** 치수(D)로 정한 길이 · 폭 — 있으면 다른 구석 = 사분면만 */ + length: number | null; + width: number | null; } export enum RectangleState { INIT = 'INIT', WAITING_FOR_START_POINT = 'WAITING_FOR_START_POINT', WAITING_FOR_END_POINT = 'WAITING_FOR_END_POINT', + WAITING_FOR_LENGTH = 'WAITING_FOR_LENGTH', + WAITING_FOR_WIDTH = 'WAITING_FOR_WIDTH', } -export enum RectangleAction { - INIT_RECTANGLE_TOOL = 'INIT_RECTANGLE_TOOL', - RECORD_START_POINT = 'RECORD_START_POINT', - DRAW_TEMP_RECTANGLE = 'DRAW_TEMP_RECTANGLE', - DRAW_FINAL_RECTANGLE = 'DRAW_FINAL_RECTANGLE', +const START_PROMPT: PromptSpec = { + text: '첫 번째 구석점 지정', + absent: [ + option('CHAMFER', 'C', '모따기'), + option('ELEVATION', 'E', '고도'), + option('FILLET', 'F', '모깎기'), + option('THICKNESS', 'T', '두께'), + option('WIDTH', 'W', '폭'), + ], +}; +const END_PROMPT: PromptSpec = { + text: '다른 구석점 지정', + options: [option('DIMENSIONS', 'D', '치수')], + absent: [option('AREA', 'A', '면적'), option('ROTATION', 'R', '회전')], +}; +const sizePrompt = (text: string, key: string): PromptSpec => ({ + text, + default: { kind: 'value', remember: key, initial: 10 }, + number: { positive: true }, +}); + +/** 다른 구석 — 치수가 있으면 누른 쪽 사분면에 길이 × 폭 */ +function cornerOf(context: RectangleContext, point: Point): Point { + const start = context.startPoint as Point; + if (context.length === null || context.width === null) return point; + const sx = point.x < start.x ? -1 : 1; + const sy = point.y < start.y ? -1 : 1; + return new Point(start.x + sx * context.length, start.y + sy * context.width); } +const rectangle = (context: RectangleContext, point: Point) => + styled( + new RectangleEntity(getActiveLayerId(), context.startPoint as Point, cornerOf(context, point)) + ); + export const rectangleToolStateMachine = createMachine( { - types: {} as { - context: RectangleContext; - events: StateEvent; - }, - context: { - startPoint: null, - type: Tool.RECTANGLE, - }, + types: {} as { context: RectangleContext; events: StateEvent }, + context: { startPoint: null, length: null, width: null, type: Tool.RECTANGLE }, initial: RectangleState.INIT, states: { [RectangleState.INIT]: { - description: 'Initializing the rectangle tool', - always: { - actions: RectangleAction.INIT_RECTANGLE_TOOL, - target: RectangleState.WAITING_FOR_START_POINT, - }, + always: { actions: 'init', target: RectangleState.WAITING_FOR_START_POINT }, }, [RectangleState.WAITING_FOR_START_POINT]: { - description: '첫 모서리 지정 (x,y):', - meta: { - instructions: '첫 모서리 지정 (x,y):', - }, + meta: { kind: 'point', prompt: START_PROMPT }, on: { - MOUSE_CLICK: { - actions: RectangleAction.RECORD_START_POINT, - target: RectangleState.WAITING_FOR_END_POINT, - }, - ABSOLUTE_POINT_INPUT: { - actions: RectangleAction.RECORD_START_POINT, - target: RectangleState.WAITING_FOR_END_POINT, - }, + ...firstPointEvents('recordStart', RectangleState.WAITING_FOR_END_POINT), + ESC: { target: END_STATE }, }, }, [RectangleState.WAITING_FOR_END_POINT]: { - description: '맞은 모서리 지정 (@x,y):', - meta: { - instructions: '맞은 모서리 지정 (@x,y):', - }, + meta: { kind: 'point', prompt: END_PROMPT }, on: { - DRAW: { - actions: RectangleAction.DRAW_TEMP_RECTANGLE, - }, - MOUSE_CLICK: { - actions: RectangleAction.DRAW_FINAL_RECTANGLE, - target: RectangleState.INIT, - }, - NUMBER_INPUT: { - actions: RectangleAction.DRAW_FINAL_RECTANGLE, // TODO see if we want to add a flow where you enter the width and then the height if one of the dimensions of the "direction + distance" comes out to 0 - target: RectangleState.INIT, - }, - ABSOLUTE_POINT_INPUT: { - actions: RectangleAction.DRAW_FINAL_RECTANGLE, - target: RectangleState.INIT, - }, - RELATIVE_POINT_INPUT: { - actions: RectangleAction.DRAW_FINAL_RECTANGLE, - target: RectangleState.INIT, - }, - ESC: { - target: RectangleState.INIT, - }, + DRAW: { actions: 'preview' }, + ...pointEvents('draw', END_STATE), + 'OPTION.DIMENSIONS': { target: RectangleState.WAITING_FOR_LENGTH }, + ESC: { target: END_STATE }, }, }, + [RectangleState.WAITING_FOR_LENGTH]: { + meta: { kind: 'number', prompt: sizePrompt('직사각형의 길이 지정', 'RECT.L') }, + on: { + NUMBER_INPUT: { actions: 'recordLength', target: RectangleState.WAITING_FOR_WIDTH }, + ESC: { target: END_STATE }, + }, + }, + [RectangleState.WAITING_FOR_WIDTH]: { + meta: { kind: 'number', prompt: sizePrompt('직사각형의 폭 지정', 'RECT.W') }, + on: { + NUMBER_INPUT: { actions: 'recordWidth', target: RectangleState.WAITING_FOR_END_POINT }, + ESC: { target: END_STATE }, + }, + }, + ...END, }, }, { actions: { - [RectangleAction.INIT_RECTANGLE_TOOL]: () => { + init: assign(() => { setShouldDrawHelpers(true); setGhostHelperEntities([]); setSelectedEntityIds([]); setAngleGuideOriginPoint(null); - }, - [RectangleAction.RECORD_START_POINT]: assign(({ event }) => { + return { startPoint: null, length: null, width: null }; + }), + recordStart: assign(({ event }) => { const startPoint = getPointFromEvent(null, event as PointInputEvent); setAngleGuideOriginPoint(startPoint); - return { - startPoint, - }; + return { startPoint }; }), - [RectangleAction.DRAW_TEMP_RECTANGLE]: ({ context, event }) => { - if (!context.startPoint) { - throw new Error('[RECTANGLE]: calling draw without start point being set'); - } - const activeRectangle = new RectangleEntity( - getActiveLayerId(), - context.startPoint as Point, - (event as DrawEvent).drawController.getWorldMouseLocation() - ); - activeRectangle.lineColor = getActiveLineColor(); - activeRectangle.lineWidth = getActiveLineWidth(); - activeRectangle.lineDash = getActiveLineDash(); - setGhostHelperEntities([activeRectangle]); + preview: ({ context, event }) => { + const point = getPointFromEvent(context.startPoint, event as DrawEvent); + setGhostHelperEntities([rectangle(context, point)]); }, - [RectangleAction.DRAW_FINAL_RECTANGLE]: ({ context, event }) => { - if (!context.startPoint) { - throw Error( - 'Trying to DRAW_FINAL_RECTANGLE when startPoint is not defined in rectangle-tool' - ); - } - const endPoint = getPointFromEvent(context.startPoint, event as PointInputEvent); - - const activeRectangle = new RectangleEntity( - getActiveLayerId(), - context.startPoint as Point, - endPoint - ); - activeRectangle.lineColor = getActiveLineColor(); - activeRectangle.lineWidth = getActiveLineWidth(); - activeRectangle.lineDash = getActiveLineDash(); - addEntities([activeRectangle], true); + draw: ({ context, event }) => { + const point = getPointFromEvent(context.startPoint, event as PointInputEvent); + setGhostHelperEntities([]); + addEntities([rectangle(context, point)], true); }, + recordLength: assign(({ event }) => { + const length = (event as NumberInputEvent).value; + remember('RECT.L', length); + return { length }; + }), + recordWidth: assign(({ event }) => { + const width = (event as NumberInputEvent).value; + remember('RECT.W', width); + return { width }; + }), }, } ); diff --git a/resources/tester/cad/draw-acad.test.ts b/resources/tester/cad/draw-acad.test.ts new file mode 100644 index 000000000..2ab8282e1 --- /dev/null +++ b/resources/tester/cad/draw-acad.test.ts @@ -0,0 +1,491 @@ +/** + * 337-3a 나-1 그리기 8 — LINE · PLINE · RECTANG · CIRCLE · POLYGON · ARC · ELLIPSE · SPLINE 를 오토캐드 차례로. + * 설계 `docs/raw/조사/2026-10-15_337_CAD오토캐드조작_설계.md` 1-1 · 6-3 나 줄. + * 실행: cd B07_DesignDetail/openwebcad && npx vitest run --dir ../../resources/tester/cad draw-acad + */ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +vi.mock("react-toastify", () => ({ + toast: Object.assign(vi.fn(), { error: vi.fn(), info: vi.fn(), warn: vi.fn(), success: vi.fn() }), +})); + +import { remember } from "../../../B07_DesignDetail/openwebcad/src/commands/prompt"; +import { Tool } from "../../../B07_DesignDetail/openwebcad/src/tools"; +import { + activeTool, + click, + entities, + initIdle, + key, + move, + noticeLine, + promptLine, + type, + typed, +} from "./helpers/cad-drive"; + +type Xy = { x: number; y: number }; +type Ends = { getStartPoint(): Xy; getEndPoint(): Xy }; + +const enter = () => key("Enter"); +const esc = () => key("Escape"); +const expectXy = (point: Xy, x: number, y: number) => { + expect(point.x).toBeCloseTo(x, 3); + expect(point.y).toBeCloseTo(y, 3); +}; +const lines = () => entities("Line") as unknown as Ends[]; +const expectLine = (line: Ends, a: [number, number], b: [number, number]) => { + expectXy(line.getStartPoint(), ...a); + expectXy(line.getEndPoint(), ...b); +}; + +beforeEach(() => { + initIdle(); + for (const name of ["LINE.END", "CIRCLERAD", "RECT.L", "RECT.W", "POLYSIDES", "POLYGON.IC"]) { + remember(name, undefined); // 세션 기억 — 시험끼리 안 섞임 + } +}); + +describe("LINE", () => { + it("U = 마지막 토막 · 그 시작점에서 이음 · 그린 토막 없으면 첫 점부터", () => { + type("l"); + expect(promptLine()).toBe("첫 번째 점 지정:"); + type("0,0"); + expect(promptLine()).toBe("다음 점 지정 또는 [명령 취소(U)]:"); + type("10,0"); + type("10,10"); + type("u"); + expect(lines()).toHaveLength(1); + expect(promptLine()).toBe("다음 점 지정 또는 [명령 취소(U)]:"); + type("20,0"); + expect(lines()).toHaveLength(2); + expectLine(lines()[1], [10, 0], [20, 0]); + type("u"); + type("u"); + type("u"); + expect(lines()).toHaveLength(0); + expect(promptLine()).toBe("첫 번째 점 지정:"); + expect(activeTool()).toBe(Tool.LINE); + }); + + it("C 는 둘째 토막부터 — 그 전엔 거절 · 닫고 끝 → 쉼", () => { + type("l"); + type("0,0"); + type("10,0"); + type("c"); + expect(noticeLine()).toBe("점 또는 옵션 키워드를 요구함."); + expect(lines()).toHaveLength(1); + type("10,10"); + expect(promptLine()).toBe("다음 점 지정 또는 [닫기(C)/명령 취소(U)]:"); + type("C"); + expect(lines()).toHaveLength(3); + expectLine(lines()[2], [10, 10], [0, 0]); + expect(activeTool()).toBe(Tool.SELECT); + }); + + it("Enter = 끝 → 쉼 · Space = 다시 LINE · 첫 점 빈 Enter = 마지막 끝점에서 이음", () => { + type("l"); + type("0,0"); + type("10,0"); + enter(); + expect(activeTool()).toBe(Tool.SELECT); + expect(lines()).toHaveLength(1); + key(" "); + expect(activeTool()).toBe(Tool.LINE); + expect(promptLine()).toBe("첫 번째 점 지정:"); + key(" "); + expect(promptLine()).toBe("다음 점 지정 또는 [명령 취소(U)]:"); + type("10,5"); + expectLine(lines()[1], [10, 0], [10, 5]); + }); + + it("첫 점 빈 Enter — 그린 것 없으면 아무 일 없음", () => { + type("l"); + enter(); + expect(activeTool()).toBe(Tool.LINE); + expect(promptLine()).toBe("첫 번째 점 지정:"); + }); + + it("Esc = 끝 · 그린 토막 남음", () => { + type("l"); + type("0,0"); + type("10,0"); + type("20,0"); + move(30, 5); + esc(); + expect(activeTool()).toBe(Tool.SELECT); + expect(lines()).toHaveLength(2); + }); +}); + +type Box = { xmin: number; ymin: number; xmax: number; ymax: number }; +type Shape = { getBoundingBox(): Box; getType(): string }; +type Poly = Shape & { getEntities(): (Shape & Ends)[] }; +const expectBox = (shape: Shape, xmin: number, ymin: number, xmax: number, ymax: number) => { + const box = shape.getBoundingBox(); + expect([box.xmin, box.ymin, box.xmax, box.ymax].map((v) => Number(v.toFixed(3)))).toEqual([ + xmin, + ymin, + xmax, + ymax, + ]); +}; +const polys = () => entities("PolyLine") as unknown as Poly[]; +const arcShape = (arc: Shape) => + (arc as unknown as { getShape(): { center: Xy; r: number } }).getShape(); + +describe("PLINE", () => { + it("L = 앞 방향 길이 · U · A 접선 호 · L 선 · 호 뒤 길이 = 접선 · C 닫기", () => { + type("pline"); + expect(promptLine()).toBe("시작점 지정:"); + type("0,0"); + expect(promptLine()).toBe("다음 점 지정 또는 [호(A)/길이(L)/명령 취소(U)]:"); + type("10,0"); + expect(promptLine()).toBe("다음 점 지정 또는 [호(A)/닫기(C)/길이(L)/명령 취소(U)]:"); + type("l"); + expect(promptLine()).toBe("선의 길이 지정:"); + type("5"); + type("u"); + type("a"); + expect(promptLine()).toBe("호의 끝점 지정 또는 [닫기(CL)/선(L)/명령 취소(U)]:"); + type("20,10"); + type("l"); + type("L"); + type("5"); + type("c"); + expect(activeTool()).toBe(Tool.SELECT); + expect(polys()).toHaveLength(1); + const parts = polys()[0].getEntities(); + expect(parts.map((part) => part.getType())).toEqual(["Line", "Arc", "Line", "Line"]); + expectLine(parts[0], [0, 0], [10, 0]); + expectXy(arcShape(parts[1]).center, 10, 10); // 동쪽 접선 → 중심 (10, 10) + expectLine(parts[2], [20, 10], [20, 15]); // 호 끝 접선 = 북쪽 + expectLine(parts[3], [20, 15], [0, 0]); + }); + + it("첫 토막 호 = 동쪽 접선 · CL = 호로 닫음 · 호 모드 C 는 거절", () => { + type("pline"); + type("0,0"); + type("a"); + expect(promptLine()).toBe("호의 끝점 지정 또는 [선(L)/명령 취소(U)]:"); + type("10,10"); + type("c"); + expect(noticeLine()).toBe("점 또는 옵션 키워드를 요구함."); + type("cl"); + const parts = polys()[0].getEntities(); + expect(parts.map((part) => part.getType())).toEqual(["Arc", "Arc"]); + expectXy(arcShape(parts[1]).center, 0, 10); + expectXy(parts[1].getEndPoint(), 0, 0); + }); + + it("Esc = 그린 데까지 남김 · 시작점만이면 없음 · 빈 Enter 끝 · 시작점 Enter = 마지막 끝점", () => { + type("pline"); + type("0,0"); + type("10,0"); + type("20,5"); + move(40, 40); + esc(); + expect(activeTool()).toBe(Tool.SELECT); + expect(polys()).toHaveLength(1); + expect(polys()[0].getEntities()).toHaveLength(2); + type("pline"); + type("1,1"); + esc(); + expect(polys()).toHaveLength(1); + type("pline"); + enter(); + type("@0,10"); + enter(); + expect(polys()).toHaveLength(2); + expectLine(polys()[1].getEntities()[0], [20, 5], [20, 15]); + }); +}); + +describe("RECTANG", () => { + it("D = 길이 · 폭(기억) → 누른 쪽 사분면 · 없는 옵션 안내 · 하나 뒤 끝", () => { + type("rec"); + expect(promptLine()).toBe("첫 번째 구석점 지정:"); + type("e"); + expect(noticeLine()).toBe("「고도(E)」 옵션은 이 CAD 에 없음."); + type("0,0"); + expect(promptLine()).toBe("다른 구석점 지정 또는 [치수(D)]:"); + type("d"); + expect(promptLine()).toBe("직사각형의 길이 지정 <10.0000>:"); + type("30"); + expect(promptLine()).toBe("직사각형의 폭 지정 <10.0000>:"); + type("20"); + expect(promptLine()).toBe("다른 구석점 지정 또는 [치수(D)]:"); + type("-1,-1"); + expect(activeTool()).toBe(Tool.SELECT); + expectBox(entities("Rectangle")[0], -30, -20, 0, 0); + key(" "); + type("100,100"); + type("d"); + expect(promptLine()).toBe("직사각형의 길이 지정 <30.0000>:"); + enter(); + expect(promptLine()).toBe("직사각형의 폭 지정 <20.0000>:"); + enter(); + type("101,99"); + expectBox(entities("Rectangle")[1], 100, 80, 130, 100); + }); + + it("두 구석 · Esc 첫 구석 = 끝", () => { + type("rec"); + esc(); + expect(activeTool()).toBe(Tool.SELECT); + type("rec"); + type("0,0"); + type("@5,3"); + expectBox(entities("Rectangle")[0], 0, 0, 5, 3); + expect(activeTool()).toBe(Tool.SELECT); + }); +}); + +describe("CIRCLE", () => { + const radius = (index: number) => + (entities("Circle")[index] as unknown as { getRadius(): number }).getRadius(); + + it("반지름 · 처음엔 <> 없음(Enter 아무 일 없음) · 기억 · 한 번 뒤 끝 · 음수 거절 · Ttr 없음", () => { + type("c"); + expect(promptLine()).toBe("원에 대한 중심점 지정 또는 [3점(3P)/2점(2P)]:"); + type("t"); + expect(noticeLine()).toBe("「Ttr - 접선 접선 반지름(T)」 옵션은 이 CAD 에 없음."); + type("0,0"); + expect(promptLine()).toBe("원의 반지름 지정 또는 [지름(D)]:"); + enter(); + expect(entities("Circle")).toHaveLength(0); + type("-5"); + expect(noticeLine()).toBe("값은 양수이고 0이 아니어야 함."); + type("5"); + expect(activeTool()).toBe(Tool.SELECT); + expect(radius(0)).toBeCloseTo(5); + key(" "); + type("20,0"); + expect(promptLine()).toBe("원의 반지름 지정 또는 [지름(D)] <5.0000>:"); + enter(); + expect(radius(1)).toBeCloseTo(5); + expectBox(entities("Circle")[1], 15, -5, 25, 5); + }); + + it("D 지름 <2r> · 누른 점 반지름도 기억", () => { + remember("CIRCLERAD", 3); + type("c"); + type("0,0"); + type("d"); + expect(promptLine()).toBe("원의 지름 지정 <6.0000>:"); + type("8"); + expect(radius(0)).toBeCloseTo(4); + type("c"); + type("50,50"); + expect(promptLine()).toBe("원의 반지름 지정 또는 [지름(D)] <4.0000>:"); + type("@0,7"); + expect(radius(1)).toBeCloseTo(7); + type("c"); + type("90,90"); + expect(promptLine()).toBe("원의 반지름 지정 또는 [지름(D)] <7.0000>:"); + }); + + it("3P · 2P", () => { + type("c"); + type("3p"); + expect(promptLine()).toBe("원 위의 첫 번째 점 지정:"); + type("0,0"); + expect(promptLine()).toBe("원 위의 두 번째 점 지정:"); + type("10,0"); + expect(promptLine()).toBe("원 위의 세 번째 점 지정:"); + type("5,5"); + expect(activeTool()).toBe(Tool.SELECT); + expectBox(entities("Circle")[0], 0, -5, 10, 5); + type("c"); + type("2P"); + expect(promptLine()).toBe("원 지름의 첫 번째 끝점 지정:"); + type("20,0"); + expect(promptLine()).toBe("원 지름의 두 번째 끝점 지정:"); + type("40,0"); + expectBox(entities("Circle")[1], 20, -10, 40, 10); + expect(activeTool()).toBe(Tool.SELECT); + }); +}); + +/** 폴리선 꼭짓점(소수 셋) */ +const corners = (poly: Poly) => + poly.getEntities().map((part) => { + const point = part.getStartPoint(); + return [Number(point.x.toFixed(3)) + 0, Number(point.y.toFixed(3)) + 0]; + }); + +describe("POLYGON", () => { + it("변 수 <4> · 2.5 · 2 거절 · 외접 숫자 = 아래 변 수평 · 기억 · 한 번 뒤 끝", () => { + type("pol"); + expect(promptLine()).toBe("변의 수 입력 <4>:"); + type("2.5"); + expect(noticeLine()).toBe("정수 값을 요구함."); + type("2"); + expect(noticeLine()).toBe("3에서 1024 사이의 정수를 요구함."); + type("6"); + expect(promptLine()).toBe("폴리곤의 중심 지정 또는 [모서리(E)]:"); + type("0,0"); + expect(promptLine()).toBe("옵션 입력 [원에 내접(I)/원에 외접(C)] :"); + type("c"); + expect(promptLine()).toBe("원의 반지름 지정:"); + type("10"); + expect(activeTool()).toBe(Tool.SELECT); + const hexagon = polys()[0]; + expect(hexagon.getEntities()).toHaveLength(6); + expect(hexagon.getBoundingBox().ymin).toBeCloseTo(-10); // 아래 변 = 반지름 거리 · 수평 + expect(corners(hexagon).filter(([, y]) => y === -10)).toHaveLength(2); + key(" "); + expect(promptLine()).toBe("변의 수 입력 <6>:"); + enter(); + type("100,0"); + expect(promptLine()).toBe("옵션 입력 [원에 내접(I)/원에 외접(C)] :"); + type("i"); + type("10"); + const inscribed = polys()[1]; + expect(corners(inscribed).filter(([, y]) => y === -8.66)).toHaveLength(2); // 내접 · 아래 변 수평(꼭짓점 −60° · −120°) + expect(inscribed.getBoundingBox().xmax).toBeCloseTo(110); + }); + + it("점 = 크기 + 돌림(내접 꼭짓점) · E 모서리 = 왼쪽(반시계)", () => { + type("pol"); + type("4"); + type("0,0"); + enter(); // + type("0,10"); + expect(corners(polys()[0])).toContainEqual([0, 10]); + type("pol"); + enter(); + type("e"); + expect(promptLine()).toBe("모서리의 첫 번째 끝점 지정:"); + type("0,0"); + expect(promptLine()).toBe("모서리의 두 번째 끝점 지정:"); + type("10,0"); + expectBox(polys()[1], 0, 0, 10, 10); + }); +}); + +describe("ARC", () => { + it("세 점 · 한 번 뒤 끝", () => { + type("a"); + expect(promptLine()).toBe("호의 시작점 지정:"); + type("10,0"); + expect(promptLine()).toBe("호의 두 번째 점 지정 또는 [중심(C)/끝(E)]:"); + type("0,10"); + expect(promptLine()).toBe("호의 끝점 지정:"); + type("-10,0"); + expect(activeTool()).toBe(Tool.SELECT); + expectBox(entities("Arc")[0], -10, 0, 10, 10); + }); + + it("C 중심 → 끝점 · A 사이각(− = 시계)", () => { + type("a"); + type("10,0"); + type("c"); + expect(promptLine()).toBe("호의 중심점 지정:"); + type("0,0"); + expect(promptLine()).toBe("호의 끝점 지정 또는 [각도(A)]:"); + type("0,5"); + expectBox(entities("Arc")[0], 0, 0, 10, 10); + type("a"); + type("10,0"); + type("c"); + type("0,0"); + type("a"); + expect(promptLine()).toBe("사이각 지정:"); + type("-90"); + expectBox(entities("Arc")[1], 0, -10, 10, 0); + }); + + it("E 끝 → 중심 · R 반지름 · 시작점 Enter 없음", () => { + type("a"); + enter(); + expect(promptLine()).toBe("호의 시작점 지정:"); + type("10,0"); + type("e"); + expect(promptLine()).toBe("호의 끝점 지정:"); + type("-10,0"); + expect(promptLine()).toBe("호의 중심점 지정 또는 [반지름(R)]:"); + type("0,0"); + expectBox(entities("Arc")[0], -10, 0, 10, 10); + type("a"); + type("10,0"); + type("e"); + type("0,10"); + type("r"); + expect(promptLine()).toBe("호의 반지름 지정:"); + type("10"); + expect(arcShape(entities("Arc")[1]).r).toBeCloseTo(10); + expectXy(arcShape(entities("Arc")[1]).center, 0, 0); + }); +}); + +describe("ELLIPSE", () => { + it("축 두 끝 · 다른 축 거리 숫자 = 중심에서 반 길이(N10) · 호 없음", () => { + type("el"); + expect(promptLine()).toBe("타원의 축 끝점 지정 또는 [중심(C)]:"); + type("a"); + expect(noticeLine()).toBe("「호(A)」 옵션은 이 CAD 에 없음."); + type("0,0"); + expect(promptLine()).toBe("축의 다른 끝점 지정:"); + type("20,0"); + expect(promptLine()).toBe("다른 축으로 거리를 지정 또는 [회전(R)]:"); + move(10, 30); + type("5"); + expect(activeTool()).toBe(Tool.SELECT); + expectBox(polys()[0], 0, -5, 20, 5); + }); + + it("C 중심 · R 회전 60° = 축 × cos", () => { + type("el"); + type("c"); + expect(promptLine()).toBe("타원의 중심 지정:"); + type("0,0"); + expect(promptLine()).toBe("축의 끝점 지정:"); + type("10,0"); + type("r"); + expect(promptLine()).toBe("주축 둘레로 회전 지정:"); + type("60"); + expectBox(polys()[0], -10, -5, 10, 5); + }); +}); + +describe("SPLINE", () => { + it("물음 차례 · U · 넷째 점부터 C(닫고 끝)", () => { + type("spl"); + expect(promptLine()).toBe("첫 번째 점 지정:"); + type("0,0"); + expect(promptLine()).toBe("다음 점 입력:"); + type("10,0"); + expect(promptLine()).toBe("다음 점 입력 또는 [명령 취소(U)]:"); + type("c"); + expect(noticeLine()).toBe("점 또는 옵션 키워드를 요구함."); + type("10,10"); + expect(promptLine()).toBe("다음 점 입력 또는 [명령 취소(U)/닫기(C)]:"); + type("u"); + expect(promptLine()).toBe("다음 점 입력 또는 [명령 취소(U)]:"); + type("10,10"); + type("0,10"); + type("c"); + expect(activeTool()).toBe(Tool.SELECT); + const parts = polys()[0].getEntities(); + expectXy(parts[parts.length - 1].getEndPoint(), 0, 0); + expectXy(parts[0].getStartPoint(), 0, 0); + }); + + it("빈 Enter = 확정 · Esc = 버림", () => { + type("spl"); + type("0,0"); + type("10,0"); + type("20,5"); + enter(); + expect(polys()).toHaveLength(1); + expect(activeTool()).toBe(Tool.SELECT); + type("spl"); + type("0,0"); + type("10,0"); + type("20,5"); + esc(); + expect(polys()).toHaveLength(1); + expect(activeTool()).toBe(Tool.SELECT); + }); +}); diff --git a/resources/tester/cad/keyboard-chain.test.ts b/resources/tester/cad/keyboard-chain.test.ts index eccaba6e8..37d2c5d70 100644 --- a/resources/tester/cad/keyboard-chain.test.ts +++ b/resources/tester/cad/keyboard-chain.test.ts @@ -143,13 +143,13 @@ describe("331-1 각도 고정 → 길이", () => { type(controller, "@10,0"); const snapshot = getActiveToolActor()?.getSnapshot(); expect(snapshot?.status).toBe("active"); - expect(snapshot?.value).toBe("STEP_0"); + expect(snapshot?.value).toBe("START"); // 337 나-1 폴리선 손 머신 expect(toast.error).not.toHaveBeenCalled(); expect(noticeLine()).toBe("점 또는 옵션 키워드를 요구함."); // 기준점 없는 거리 · 상대 좌표 = 거절 type(controller, "0,0"); type(controller, "<0"); type(controller, "100"); - expect(getActiveToolActor()?.getSnapshot()?.context.values).toHaveLength(2); + expect(getActiveToolActor()?.getSnapshot()?.context.segments).toHaveLength(1); }); }); @@ -210,16 +210,15 @@ describe("331-3 선 중 C · U · 한 번 스냅 · 한글 안내", () => { type(controller, "10,10"); }; - it("C = 첫 점으로 닫고 끝 · 안내 「[닫기(C)/명령취소(U)]」", () => { + it("C = 첫 점으로 닫고 끝 · 안내 「[닫기(C)/명령 취소(U)]」", () => { drawOpen(); - expect(getLastStateInstructions()).toBe("다음 점 지정 또는 [닫기(C)/명령취소(U)]:"); + expect(getLastStateInstructions()).toBe("다음 점 지정 또는 [닫기(C)/명령 취소(U)]:"); type(controller, "c"); const drawn = lines(); expect(drawn).toHaveLength(3); expectXy(drawn[2].start, 10, 10); expectXy(drawn[2].end, 0, 0); - expect(getActiveToolActor()?.getSnapshot()?.value).toBe("WAITING_FOR_START_POINT"); - expect(getLastStateInstructions()).toBe("첫 점 지정 (x,y):"); + expect(getActiveToolActor()?.getSnapshot()?.context.type).toBe("SELECT"); // 337 — 닫으면 명령 끝 }); it("C 는 둘째 선부터 — 한 구간이면 무시", () => { From d35bdfa69d985e78aa5580c0cd48ad96d622c221 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Fri, 16 Oct 2026 00:17:16 +0900 Subject: [PATCH 73/81] =?UTF-8?q?feat(CAD):=20337-5=20=EB=9D=BC=20?= =?UTF-8?q?=EC=98=AE=EA=B8=B0=EA=B8=B0=20=EB=AC=B4=EB=A6=AC=201=20?= =?UTF-8?q?=EB=8B=A8=EA=B3=84=20=E2=80=94=20MOVE=20=C2=B7=20ROTATE=20?= =?UTF-8?q?=C2=B7=20SCALE=20=EC=98=A4=ED=86=A0=EC=BA=90=EB=93=9C=20?= =?UTF-8?q?=EB=AC=BC=EC=9D=8C=20=EC=B0=A8=EB=A1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - MOVE: 기준점 [변위(D)] <변위> · 두 번째 점 <첫 번째 점을 변위로 사용> · 변위 기억(MOVE.DISP) · 한 번 뒤 끝 · Esc 제자리 끝 - ROTATE · SCALE: 공용 틀(pivot-tool) — 누름 한 번 = 값 · 복사(C) · 참조(R) → 새 값 [점(P)] · 각 · 비율 기억 · 0 이하 축척 거절 - 공용 고르기 상태(transform.helpers) — PICKFIRST · 빈 고르기 Enter = 끝 · OPTION.* 넘김 · 원본은 확정까지 도면에 그대로(N1 · N2) - 시험 transform-acad(15) · scale-tool 단위 시험 새 상태 이름 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01TteK8mfYZetk5P3bEZkyVZ --- .../openwebcad/src/tools/move-tool.ts | 455 +++++++----------- .../openwebcad/src/tools/pivot-tool.ts | 345 +++++++++++++ .../src/tools/rotate-tool.helpers.ts | 22 - .../openwebcad/src/tools/rotate-tool.ts | 358 +------------- .../src/tools/scale-tool.helpers.ts | 24 - .../openwebcad/src/tools/scale-tool.test.ts | 10 +- .../openwebcad/src/tools/scale-tool.ts | 362 +------------- .../openwebcad/src/tools/transform.helpers.ts | 145 ++++++ resources/tester/cad/transform-acad.test.ts | 318 ++++++++++++ 9 files changed, 1020 insertions(+), 1019 deletions(-) create mode 100644 B07_DesignDetail/openwebcad/src/tools/pivot-tool.ts delete mode 100644 B07_DesignDetail/openwebcad/src/tools/rotate-tool.helpers.ts delete mode 100644 B07_DesignDetail/openwebcad/src/tools/scale-tool.helpers.ts create mode 100644 B07_DesignDetail/openwebcad/src/tools/transform.helpers.ts create mode 100644 resources/tester/cad/transform-acad.test.ts diff --git a/B07_DesignDetail/openwebcad/src/tools/move-tool.ts b/B07_DesignDetail/openwebcad/src/tools/move-tool.ts index 3517c1ce4..1a812f7f7 100644 --- a/B07_DesignDetail/openwebcad/src/tools/move-tool.ts +++ b/B07_DesignDetail/openwebcad/src/tools/move-tool.ts @@ -1,306 +1,181 @@ +/** + * MOVE(337 라 · 설계 1-3) — 「객체 선택:」 → 「기준점 지정 또는 [변위(D)] <변위>:」 → + * 「두 번째 점 지정 또는 <첫 번째 점을 변위로 사용>:」 · D → 「변위 지정 <0.0000, 0.0000>:」. + * 한 번 뒤 끝 · Esc = 제자리 끝 · 변위 기억(MOVE.DISP). STRETCH 도 이 틀(`createDisplaceTool`). + */ import type { Point } from '@flatten-js/core'; -import { - addEntities, - deleteEntities, - getActiveLayerId, - getSelectedEntities, - getSelectedEntityIds, - setAngleGuideOriginPoint, - setGhostHelperEntities, - setSelectedEntityIds, - setShouldDrawHelpers, -} from '../state'; -import { Tool } from '../tools'; -import type { DrawEvent, PointInputEvent, StateEvent, ToolContext } from './tool.types'; -import { getPointFromEvent } from '../helpers/get-point-from-event'; -import { assign, createMachine, sendTo } from 'xstate'; -import { selectToolStateMachine } from './select-tool'; +import { assign, createMachine } from 'xstate'; +import { OPT, type PromptSpec, remember } from '../commands/prompt'; import type { Entity } from '../entities/Entity'; -import { compact } from 'es-toolkit'; -import { cloneKeepingLook, moveEntities } from './move-tool.helpers'; -import { LineEntity } from '../entities/LineEntity'; -import { GUIDE_LINE_COLOR, GUIDE_LINE_STYLE, GUIDE_LINE_WIDTH } from '../App.consts'; - -export interface MoveContext extends ToolContext { - startPoint: Point | null; - originalSelectedEntities: Entity[]; - movedEntities: Entity[]; - lastDrawLocation: Point | null; -} +import { getPointFromEvent } from '../helpers/get-point-from-event'; +import { setAngleGuideOriginPoint, setGhostHelperEntities, setShouldDrawHelpers } from '../state'; +import { Tool } from '../tools'; +import { + type DrawEvent, + END_STATE, + type PointInputEvent, + type StateEvent, + type ToolContext, +} from './tool.types'; +import { + type PickedContext, + changedCopies, + endState, + guideLine, + replaceInPlace, + selectionState, + startState, +} from './transform.helpers'; export enum MoveState { INIT = 'INIT', - CHECK_SELECTION = 'CHECK_SELECTION', WAITING_FOR_SELECTION = 'WAITING_FOR_SELECTION', - WAITING_FOR_START_MOVE_POINT = 'WAITING_FOR_START_MOVE_POINT', - WAITING_FOR_END_MOVE_POINT = 'WAITING_FOR_END_MOVE_POINT', + WAITING_FOR_BASE_POINT = 'WAITING_FOR_BASE_POINT', + WAITING_FOR_SECOND_POINT = 'WAITING_FOR_SECOND_POINT', + WAITING_FOR_DISPLACEMENT = 'WAITING_FOR_DISPLACEMENT', } -export enum MoveAction { - INIT_MOVE_TOOL = 'INIT_MOVE_TOOL', - ENABLE_HELPERS = 'ENABLE_HELPERS', - RECORD_START_POINT = 'RECORD_START_POINT', - COPY_SELECTION_BEFORE_MOVE = 'COPY_SELECTION_BEFORE_MOVE', - DRAW_TEMP_MOVE_ENTITIES = 'DRAW_TEMP_MOVE_ENTITIES', - MOVE_SELECTION = 'MOVE_SELECTION', - DESELECT_ENTITIES = 'DESELECT_ENTITIES', - RESTORE_ORIGINAL_ENTITIES = 'RESTORE_ORIGINAL_ENTITIES', +export interface DisplaceContext extends ToolContext, PickedContext { + base: Point | null; + /** 고르기 중 마지막 걸침 창(STRETCH) */ + window: [Point, Point] | null; } -/** - * Move tool state machine - * This state machine is responsible for moving entities around the canvas - * It uses the select tool state machine to select entities to move - * When the user presses enter, the selected entities are marked for moving - * The user can then select a start point - * When the user moves their mouse, the selected entities will be updated to move according to the vector: startpoint => mouse location - * When the user clicks again, the end point is selected and the entities are moved to the new location - */ -export const moveToolStateMachine = createMachine( - { - types: {} as { - context: MoveContext; - events: StateEvent; - }, - context: { - startPoint: null, - originalSelectedEntities: [], - movedEntities: [], - lastDrawLocation: null, - type: Tool.MOVE, - }, - initial: MoveState.INIT, - states: { - [MoveState.INIT]: { - description: 'Initializing the move tool', - always: { - actions: MoveAction.INIT_MOVE_TOOL, - target: MoveState.CHECK_SELECTION, - }, - }, - [MoveState.CHECK_SELECTION]: { - description: 'Check if there is something selected', - always: [ - { - guard: () => { - return getSelectedEntityIds().length > 0; - }, - target: MoveState.WAITING_FOR_START_MOVE_POINT, - }, - { - guard: () => { - return getSelectedEntityIds().length === 0; - }, - target: MoveState.WAITING_FOR_SELECTION, - }, - ], - }, - [MoveState.WAITING_FOR_SELECTION]: { - description: 'Select what you want to move', - meta: { - instructions: '이동할 객체 선택 뒤 Enter', - }, - invoke: { - id: 'selectToolInsideTheMoveTool', - src: selectToolStateMachine, - onDone: { - actions: assign(() => { - return { - startPoint: null, - }; - }), - target: MoveState.CHECK_SELECTION, - }, - }, - on: { - MOUSE_CLICK: { - // Forward the event to the select tool - actions: sendTo('selectToolInsideTheMoveTool', ({ event }) => { - return event; - }), - }, - ESC: { - actions: [MoveAction.DESELECT_ENTITIES, MoveAction.INIT_MOVE_TOOL], - }, - ENTER: { - // Forward the event to the select tool - actions: sendTo('selectToolInsideTheMoveTool', ({ event }) => { - return event; - }), - }, - DRAW: { - // Forward the event to the select tool - actions: sendTo('selectToolInsideTheMoveTool', ({ event }) => { - return event; - }), - }, - }, - }, - [MoveState.WAITING_FOR_START_MOVE_POINT]: { - description: '기준점 지정 (x,y):', - meta: { - instructions: '기준점 지정 (x,y):', - }, - always: { - actions: MoveAction.ENABLE_HELPERS, - }, - on: { - MOUSE_CLICK: { - actions: [MoveAction.RECORD_START_POINT, MoveAction.COPY_SELECTION_BEFORE_MOVE], - target: MoveState.WAITING_FOR_END_MOVE_POINT, - }, - ABSOLUTE_POINT_INPUT: { - actions: [MoveAction.RECORD_START_POINT, MoveAction.COPY_SELECTION_BEFORE_MOVE], - target: MoveState.WAITING_FOR_END_MOVE_POINT, - }, - ESC: { - actions: MoveAction.DESELECT_ENTITIES, - target: MoveState.INIT, - }, - }, - }, - [MoveState.WAITING_FOR_END_MOVE_POINT]: { - description: '둘째 점 지정 (길이 · <각도 · @x,y):', - meta: { - instructions: '둘째 점 지정 (길이 · <각도 · @x,y):', - }, - on: { - DRAW: { - actions: [MoveAction.DRAW_TEMP_MOVE_ENTITIES], - }, - MOUSE_CLICK: { - actions: [MoveAction.MOVE_SELECTION, MoveAction.DESELECT_ENTITIES], - target: MoveState.WAITING_FOR_SELECTION, - }, - NUMBER_INPUT: { - actions: [MoveAction.MOVE_SELECTION, MoveAction.DESELECT_ENTITIES], - target: MoveState.WAITING_FOR_SELECTION, - }, - ABSOLUTE_POINT_INPUT: { - actions: [MoveAction.MOVE_SELECTION, MoveAction.DESELECT_ENTITIES], - target: MoveState.WAITING_FOR_SELECTION, - }, - RELATIVE_POINT_INPUT: { - actions: [MoveAction.MOVE_SELECTION, MoveAction.DESELECT_ENTITIES], - target: MoveState.WAITING_FOR_SELECTION, - }, - ESC: { - actions: MoveAction.RESTORE_ORIGINAL_ENTITIES, - target: MoveState.INIT, - }, - }, - }, - }, - }, - { - actions: { - [MoveAction.INIT_MOVE_TOOL]: assign(() => { - setShouldDrawHelpers(false); - setGhostHelperEntities([]); - setAngleGuideOriginPoint(null); - return { - startPoint: null, - originalSelectedEntities: [], - movedEntities: [], - lastDrawLocation: null, - }; - }), - [MoveAction.ENABLE_HELPERS]: () => { - setShouldDrawHelpers(true); - }, - [MoveAction.RECORD_START_POINT]: assign(({ event }) => { - const startPoint = getPointFromEvent(null, event as PointInputEvent); - setAngleGuideOriginPoint(startPoint); - return { startPoint }; - }), - [MoveAction.COPY_SELECTION_BEFORE_MOVE]: assign(({ context }) => { - const selectedEntities = getSelectedEntities(); +export interface DisplaceConfig { + tool: Tool; + /** 변위 기억 열쇠 */ + remember: string; + /** 변위만큼 바꾼 사본(id 그대로 · 안 바뀐 것은 빼도 됨) */ + change: (context: DisplaceContext, dx: number, dy: number) => Entity[]; + /** 고르기 누름 엿봄(걸침 창 기억) */ + watch?: unknown; + /** 시작 기록 줄 */ + intro?: string; +} - // Move the selected entities to the ghost helper entities, so they are drawn on the canvas, but do not interact with the snap points / angle guides - setGhostHelperEntities(selectedEntities); - // Remove the selected entities from the regular entity list, so they do not get used for determining snap points / angle guides - deleteEntities(selectedEntities, false); +type Ctx = { context: DisplaceContext; event: StateEvent }; - // TODO keep a copy of the original entities in the entities list, but set their line color to grey, so the user can see where the entities were before being moved and the original entities also are used for snap points / angle guides +export const BASE_PROMPT: PromptSpec = { + text: '기준점 지정', + options: [OPT.DISPLACEMENT], + default: { kind: 'option', key: OPT.DISPLACEMENT.key }, +}; +export const SECOND_PROMPT: PromptSpec = { + // 옵션 없이 「또는 <…>」 — 오토캐드 줄 그대로 + text: '두 번째 점 지정 또는', + default: { kind: 'enter', label: '첫 번째 점을 변위로 사용' }, +}; - setSelectedEntityIds([]); - return { - startPoint: context.startPoint, - // Make a copy of the selected entities before moving them, so we can restore them when the user cancels the move action - originalSelectedEntities: compact( - selectedEntities.map((entity) => cloneKeepingLook(entity, true)) - ), - movedEntities: selectedEntities, - }; - }), - [MoveAction.DRAW_TEMP_MOVE_ENTITIES]: ({ context, event }) => { - if (!context.startPoint) { - throw new Error('[MOVE] Calling draw temp move line without a start point'); +export function createDisplaceTool(config: DisplaceConfig) { + const S = MoveState; + const DISP_PROMPT: PromptSpec = { + text: '변위 지정', + default: { kind: 'value', remember: config.remember, initial: { x: 0, y: 0 } }, + }; + + const commit = (context: DisplaceContext, dx: number, dy: number) => { + replaceInPlace(config.change(context, dx, dy)); + remember(config.remember, { x: dx, y: dy }); + }; + const toward = ({ context, event }: Ctx) => + getPointFromEvent(context.base, event as PointInputEvent); + const commitSecond = (args: Ctx) => { + const base = args.context.base as Point; + const dest = toward(args); + commit(args.context, dest.x - base.x, dest.y - base.y); + }; + // 첫 점 좌표가 곧 변위(빈 Enter) · D 변위 물음의 점 + const commitPointAsVector = (point: Point, context: DisplaceContext) => + commit(context, point.x, point.y); + + const done = (actions: unknown) => ({ actions, target: END_STATE }); + const pointEvents = (actions: unknown) => ({ + MOUSE_CLICK: done(actions), + ABSOLUTE_POINT_INPUT: done(actions), + RELATIVE_POINT_INPUT: done(actions), + NUMBER_INPUT: done(actions), + }); + + // biome-ignore lint/suspicious/noExplicitAny: 공용 상태 조각(여러 도구 공통)을 섞어 만든다 + const states: Record = { + [S.INIT]: { + ...startState(S.WAITING_FOR_BASE_POINT), + exit: () => { + if (config.intro) { + const intro = config.intro; + void import('../commands/run-command').then(({ logLine }) => logLine(intro)); } - - const endPointTemp = getPointFromEvent(context.startPoint, event as DrawEvent); - - // Move the entities to the new location - // Draw all selected entities according to translation vector, so the user gets visual feedback of where the entities will be moved; - const movedEntities = context.originalSelectedEntities.map((entity) => - cloneKeepingLook(entity, true) - ); - moveEntities( - movedEntities, - endPointTemp.x - context.startPoint.x, - endPointTemp.y - context.startPoint.y - ); - - // // Draw a dashed line between the start move point and the current mouse location - const activeMoveLine = new LineEntity( - getActiveLayerId(), - context.startPoint as Point, - endPointTemp - ); - activeMoveLine.lineColor = GUIDE_LINE_COLOR; - activeMoveLine.lineWidth = GUIDE_LINE_WIDTH; - activeMoveLine.lineDash = GUIDE_LINE_STYLE; - setGhostHelperEntities([activeMoveLine, ...movedEntities]); }, - [MoveAction.MOVE_SELECTION]: ({ context, event }) => { - if (!context.startPoint) { - throw new Error('[MOVE] Calling move selection without a start point'); - } - - // Move the entities one final time - // 클릭 · 거리 · 절대 · 상대 좌표(키보드) - const currentEndPoint = getPointFromEvent(context.startPoint, event as PointInputEvent); - moveEntities( - context.originalSelectedEntities, - currentEndPoint.x - context.startPoint.x, - currentEndPoint.y - context.startPoint.y - ); - - // Switch the moved entities back from the ghost helper entities to the real entities - addEntities(context.originalSelectedEntities, true); - setGhostHelperEntities([]); - setSelectedEntityIds([]); - }, - [MoveAction.DESELECT_ENTITIES]: assign(() => { - setGhostHelperEntities([]); - setSelectedEntityIds([]); - return { - startPoint: null, - originalSelectedEntities: [], - lastDrawLocation: null, - }; - }), - [MoveAction.RESTORE_ORIGINAL_ENTITIES]: assign(({ context }) => { - addEntities(context.originalSelectedEntities, false); // This should already be the last state of the undo stack - setGhostHelperEntities([]); - setSelectedEntityIds([]); - return { - startPoint: null, - originalSelectedEntities: [], - lastDrawLocation: null, - }; - }), - ...selectToolStateMachine.implementations.actions, }, - } -); + [S.WAITING_FOR_SELECTION]: selectionState(S.WAITING_FOR_BASE_POINT, config.watch), + [S.WAITING_FOR_BASE_POINT]: { + meta: { kind: 'point', prompt: BASE_PROMPT }, + entry: () => setShouldDrawHelpers(true), + on: { + MOUSE_CLICK: { + actions: assign(({ event }) => { + const base = getPointFromEvent(null, event as PointInputEvent); + setAngleGuideOriginPoint(base); + return { base }; + }), + target: S.WAITING_FOR_SECOND_POINT, + }, + ABSOLUTE_POINT_INPUT: { + actions: assign(({ event }) => { + const base = getPointFromEvent(null, event as PointInputEvent); + setAngleGuideOriginPoint(base); + return { base }; + }), + target: S.WAITING_FOR_SECOND_POINT, + }, + 'OPTION.DISPLACEMENT': { target: S.WAITING_FOR_DISPLACEMENT }, + ESC: { target: END_STATE }, + }, + }, + [S.WAITING_FOR_SECOND_POINT]: { + meta: { kind: 'point', prompt: SECOND_PROMPT }, + on: { + DRAW: { + actions: (args: Ctx) => { + const base = args.context.base as Point; + const dest = getPointFromEvent(base, args.event as DrawEvent); + setGhostHelperEntities([ + guideLine(base, dest), + ...config.change(args.context, dest.x - base.x, dest.y - base.y), + ]); + }, + }, + ...pointEvents(commitSecond), + ENTER: done(({ context }: Ctx) => commitPointAsVector(context.base as Point, context)), + ESC: { target: END_STATE }, + }, + }, + [S.WAITING_FOR_DISPLACEMENT]: { + meta: { kind: 'point', prompt: DISP_PROMPT }, + entry: () => setAngleGuideOriginPoint(null), + on: { + MOUSE_CLICK: done(({ context, event }: Ctx) => + commitPointAsVector(getPointFromEvent(null, event as PointInputEvent), context) + ), + ABSOLUTE_POINT_INPUT: done(({ context, event }: Ctx) => + commitPointAsVector(getPointFromEvent(null, event as PointInputEvent), context) + ), + ESC: { target: END_STATE }, + }, + }, + [END_STATE]: endState, + }; + + return createMachine({ + types: {} as { context: DisplaceContext; events: StateEvent }, + context: { originals: [], base: null, window: null, type: config.tool }, + initial: S.INIT, + states, + }); +} + +export const moveToolStateMachine = createDisplaceTool({ + tool: Tool.MOVE, + remember: 'MOVE.DISP', + change: (context, dx, dy) => changedCopies(context.originals, (entity) => entity.move(dx, dy)), +}); diff --git a/B07_DesignDetail/openwebcad/src/tools/pivot-tool.ts b/B07_DesignDetail/openwebcad/src/tools/pivot-tool.ts new file mode 100644 index 000000000..99d82da37 --- /dev/null +++ b/B07_DesignDetail/openwebcad/src/tools/pivot-tool.ts @@ -0,0 +1,345 @@ +/** + * ROTATE · SCALE 공용 틀(337 라 · 설계 1-3) — 「객체 선택:」 → 「기준점 지정:」 → + * 「<값> 지정 또는 [복사(C)/참조(R)] <기본>:」(누름 = 기준점에서 그 점까지 잰 값 · 숫자 = 값 · 빈 Enter = 기억 값) · + * C = 사본 켬/끔 · R → 「참조 … <기본>:」(숫자 · 두 점) → 「새 … 지정 또는 [점(P)] <기본>:」(숫자 · 누름 · P 두 점). + * 한 번 뒤 끝 · Esc = 제자리 끝 · 원본은 확정까지 도면에 그대로. + */ +import type { Point } from '@flatten-js/core'; +import { assign, createMachine } from 'xstate'; +import { OPT, type OptionSpec, type PromptSpec, remember } from '../commands/prompt'; +import type { Entity } from '../entities/Entity'; +import { getPointFromEvent } from '../helpers/get-point-from-event'; +import { setAngleGuideOriginPoint, setGhostHelperEntities, setShouldDrawHelpers } from '../state'; +import type { Tool } from '../tools'; +import { + type DrawEvent, + END_STATE, + type NumberInputEvent, + type PointInputEvent, + type StateEvent, + type ToolContext, +} from './tool.types'; +import { + type PickedContext, + addCopies, + changedCopies, + endState, + guideLine, + replaceInPlace, + selectionState, + showGhosts, + startState, +} from './transform.helpers'; + +export enum PivotState { + INIT = 'INIT', + WAITING_FOR_SELECTION = 'WAITING_FOR_SELECTION', + WAITING_FOR_BASE_POINT = 'WAITING_FOR_BASE_POINT', + WAITING_FOR_VALUE = 'WAITING_FOR_VALUE', + WAITING_FOR_REFERENCE = 'WAITING_FOR_REFERENCE', + WAITING_FOR_REFERENCE_SECOND_POINT = 'WAITING_FOR_REFERENCE_SECOND_POINT', + WAITING_FOR_NEW_VALUE = 'WAITING_FOR_NEW_VALUE', + WAITING_FOR_NEW_FIRST_POINT = 'WAITING_FOR_NEW_FIRST_POINT', + WAITING_FOR_NEW_SECOND_POINT = 'WAITING_FOR_NEW_SECOND_POINT', +} + +export interface PivotContext extends ToolContext, PickedContext { + base: Point | null; + /** 복사(C) — 원본 두고 사본을 바꿈 */ + copy: boolean; + /** 참조 값(R) */ + reference: number | null; + /** 참조 · 새 값 두 점의 첫 점 */ + first: Point | null; +} + +interface ValuePrompt { + text: string; + remember: string; + initial: number; +} + +export interface PivotConfig { + tool: Tool; + format: 'ang' | 'len'; + value: ValuePrompt; + reference: ValuePrompt; + next: ValuePrompt; + /** 값 · 참조 · 새 값 숫자 검사 */ + number?: PromptSpec['number']; + reject?: string; + /** C 켬 · 끔 알림 */ + copyNotice: [string, string]; + /** 못 쓰는 값 알림 */ + invalidNotice: string; + /** 두 점으로 잰 값(각 · 거리) */ + measure: (a: Point, b: Point) => number; + /** 새 값 · 참조 → 값(못 쓰면 null) */ + combine: (next: number, reference: number) => number | null; + valid: (value: number) => boolean; + apply: (entity: Entity, base: Point, value: number) => void; +} + +const POINTS: OptionSpec = { key: 'POINTS', kw: 'P', en: 'POINTS', label: '점' }; + +type Ctx = { context: PivotContext; event: StateEvent }; + +const noticeLater = (text: string) => { + void import('../inputController/prompt-input').then(({ notice }) => notice(text)); +}; + +export function createPivotTool(config: PivotConfig) { + const S = PivotState; + const valuePrompt = (spec: ValuePrompt, options?: OptionSpec[]): PromptSpec => ({ + text: spec.text, + ...(options ? { options } : {}), + default: { + kind: 'value', + remember: spec.remember, + initial: spec.initial, + format: config.format, + }, + ...(config.number ? { number: config.number } : {}), + ...(config.reject ? { reject: config.reject } : {}), + }); + const PROMPT = { + base: { text: '기준점 지정' }, + value: valuePrompt(config.value, [OPT.COPY, OPT.REFERENCE]), + reference: valuePrompt(config.reference), + second: { text: '두 번째 점 지정' }, + next: valuePrompt(config.next, [POINTS]), + first: { text: '첫 번째 점 지정' }, + } satisfies Record; + + const pointOf = (from: Point | null, event: StateEvent) => + getPointFromEvent(from, event as PointInputEvent); + const ok = (value: number | null): value is number => + value !== null && Number.isFinite(value) && config.valid(value); + + const transform = (context: PivotContext, value: number) => (entity: Entity) => + config.apply(entity, context.base as Point, value); + const commit = (context: PivotContext, value: number) => { + if (context.copy) addCopies(context.originals, transform(context, value)); + else replaceInPlace(changedCopies(context.originals, transform(context, value))); + remember(config.value.remember, value); + }; + const preview = (context: PivotContext, value: number | null, from: Point, cursor: Point) => { + if (ok(value)) + showGhosts(context.originals, transform(context, value), guideLine(from, cursor)); + else setGhostHelperEntities([guideLine(from, cursor)]); + }; + + /** 값을 셈해 맞으면 확정 · 끝 · 아니면 알림 뒤 그 물음 */ + const settle = (compute: (args: Ctx) => number | null) => [ + { + guard: (args: Ctx) => ok(compute(args)), + actions: (args: Ctx) => commit(args.context, compute(args) as number), + target: END_STATE, + }, + { actions: () => noticeLater(config.invalidNotice) }, + ]; + const onPoints = (handler: unknown) => ({ + MOUSE_CLICK: handler, + ABSOLUTE_POINT_INPUT: handler, + RELATIVE_POINT_INPUT: handler, + NUMBER_INPUT: handler, + }); + const numberOr = (args: Ctx, point: (p: Point) => number) => + args.event.type === 'NUMBER_INPUT' + ? (args.event as NumberInputEvent).value + : point(pointOf(args.context.base, args.event)); + const nextValue = (args: Ctx, raw: number) => { + remember(config.next.remember, raw); + return config.combine(raw, args.context.reference ?? 0); + }; + const cursorOf = (event: StateEvent, from: Point) => getPointFromEvent(from, event as DrawEvent); + const esc = { ESC: { target: END_STATE } }; + + // biome-ignore lint/suspicious/noExplicitAny: 공용 상태 조각(여러 도구 공통)을 섞어 만든다 + const states: Record = { + [S.INIT]: startState(S.WAITING_FOR_BASE_POINT), + [S.WAITING_FOR_SELECTION]: selectionState(S.WAITING_FOR_BASE_POINT), + [S.WAITING_FOR_BASE_POINT]: { + meta: { kind: 'point', prompt: PROMPT.base }, + entry: () => setShouldDrawHelpers(true), + on: { + MOUSE_CLICK: { + actions: assign(({ event }: Ctx) => { + const base = pointOf(null, event); + setAngleGuideOriginPoint(base); + return { base }; + }), + target: S.WAITING_FOR_VALUE, + }, + ABSOLUTE_POINT_INPUT: { + actions: assign(({ event }: Ctx) => { + const base = pointOf(null, event); + setAngleGuideOriginPoint(base); + return { base }; + }), + target: S.WAITING_FOR_VALUE, + }, + ...esc, + }, + }, + [S.WAITING_FOR_VALUE]: { + meta: { kind: 'point', prompt: PROMPT.value }, + on: { + DRAW: { + actions: ({ context, event }: Ctx) => { + const base = context.base as Point; + const cursor = cursorOf(event, base); + preview(context, config.measure(base, cursor), base, cursor); + }, + }, + ...onPoints( + settle((args) => numberOr(args, (p) => config.measure(args.context.base as Point, p))) + ), + 'OPTION.COPY': { + actions: [ + assign(({ context }: Ctx) => ({ copy: !context.copy })), + ({ context }: Ctx) => noticeLater(config.copyNotice[context.copy ? 0 : 1]), + ], + }, + 'OPTION.REFERENCE': { target: S.WAITING_FOR_REFERENCE }, + ...esc, + }, + }, + [S.WAITING_FOR_REFERENCE]: { + meta: { kind: 'point', prompt: PROMPT.reference }, + entry: () => setGhostHelperEntities([]), + on: { + NUMBER_INPUT: { + actions: assign(({ event }: Ctx) => { + const reference = (event as NumberInputEvent).value; + remember(config.reference.remember, reference); + return { reference }; + }), + target: S.WAITING_FOR_NEW_VALUE, + }, + ...Object.fromEntries( + ['MOUSE_CLICK', 'ABSOLUTE_POINT_INPUT', 'RELATIVE_POINT_INPUT'].map((type) => [ + type, + { + actions: assign(({ context, event }: Ctx) => { + const first = pointOf(context.base, event); + setAngleGuideOriginPoint(first); + return { first }; + }), + target: S.WAITING_FOR_REFERENCE_SECOND_POINT, + }, + ]) + ), + ...esc, + }, + }, + [S.WAITING_FOR_REFERENCE_SECOND_POINT]: { + meta: { kind: 'point', prompt: PROMPT.second }, + on: { + DRAW: { + actions: ({ context, event }: Ctx) => { + const first = context.first as Point; + setGhostHelperEntities([guideLine(first, cursorOf(event, first))]); + }, + }, + ...onPoints({ + actions: assign(({ context, event }: Ctx) => { + const first = context.first as Point; + const reference = config.measure(first, pointOf(first, event)); + remember(config.reference.remember, reference); + setAngleGuideOriginPoint(context.base); + return { reference, first: null }; + }), + target: S.WAITING_FOR_NEW_VALUE, + }), + ...esc, + }, + }, + [S.WAITING_FOR_NEW_VALUE]: { + meta: { kind: 'point', prompt: PROMPT.next }, + on: { + DRAW: { + actions: ({ context, event }: Ctx) => { + const base = context.base as Point; + const cursor = cursorOf(event, base); + preview( + context, + config.combine(config.measure(base, cursor), context.reference ?? 0), + base, + cursor + ); + }, + }, + ...onPoints( + settle((args) => + nextValue( + args, + numberOr(args, (p) => config.measure(args.context.base as Point, p)) + ) + ) + ), + 'OPTION.POINTS': { target: S.WAITING_FOR_NEW_FIRST_POINT }, + ...esc, + }, + }, + [S.WAITING_FOR_NEW_FIRST_POINT]: { + meta: { kind: 'point', prompt: PROMPT.first }, + entry: () => setGhostHelperEntities([]), + on: { + ...Object.fromEntries( + ['MOUSE_CLICK', 'ABSOLUTE_POINT_INPUT', 'RELATIVE_POINT_INPUT'].map((type) => [ + type, + { + actions: assign(({ context, event }: Ctx) => { + const first = pointOf(context.base, event); + setAngleGuideOriginPoint(first); + return { first }; + }), + target: S.WAITING_FOR_NEW_SECOND_POINT, + }, + ]) + ), + ...esc, + }, + }, + [S.WAITING_FOR_NEW_SECOND_POINT]: { + meta: { kind: 'point', prompt: PROMPT.second }, + on: { + DRAW: { + actions: ({ context, event }: Ctx) => { + const first = context.first as Point; + const cursor = cursorOf(event, first); + preview( + context, + config.combine(config.measure(first, cursor), context.reference ?? 0), + first, + cursor + ); + }, + }, + ...onPoints( + settle((args) => { + const first = args.context.first as Point; + return nextValue(args, config.measure(first, pointOf(first, args.event))); + }) + ), + ...esc, + }, + }, + [END_STATE]: endState, + }; + + return createMachine({ + types: {} as { context: PivotContext; events: StateEvent }, + context: { + originals: [], + base: null, + copy: false, + reference: null, + first: null, + type: config.tool, + }, + initial: S.INIT, + states, + }); +} diff --git a/B07_DesignDetail/openwebcad/src/tools/rotate-tool.helpers.ts b/B07_DesignDetail/openwebcad/src/tools/rotate-tool.helpers.ts deleted file mode 100644 index d29b34ba7..000000000 --- a/B07_DesignDetail/openwebcad/src/tools/rotate-tool.helpers.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Line, type Point } from '@flatten-js/core'; -import type { Entity } from '../entities/Entity'; - -/** - * Rotate entities round a base point by a certain angle - * @param entities - * @param rotateOrigin - * @param startAnglePoint - * @param endAnglePoint - */ -export function rotateEntities( - entities: Entity[], - rotateOrigin: Point, - startAnglePoint: Point, - endAnglePoint: Point -) { - const rotationAngle = - new Line(rotateOrigin, endAnglePoint).slope - new Line(rotateOrigin, startAnglePoint).slope; - for (const entity of entities) { - entity.rotate(rotateOrigin, rotationAngle); - } -} diff --git a/B07_DesignDetail/openwebcad/src/tools/rotate-tool.ts b/B07_DesignDetail/openwebcad/src/tools/rotate-tool.ts index 619c7e63e..488c4574c 100644 --- a/B07_DesignDetail/openwebcad/src/tools/rotate-tool.ts +++ b/B07_DesignDetail/openwebcad/src/tools/rotate-tool.ts @@ -1,341 +1,25 @@ -import type { Point } from '@flatten-js/core'; -import { - addEntities, - deleteEntities, - getSelectedEntities, - getSelectedEntityIds, - setAngleGuideOriginPoint, - setGhostHelperEntities, - setSelectedEntityIds, - setShouldDrawHelpers, -} from '../state'; -import { Tool } from '../tools'; -import type { - DrawEvent, - MouseClickEvent, - NumberInputEvent, - PointInputEvent, - StateEvent, - ToolContext, -} from './tool.types'; -import { getPointFromEvent } from '../helpers/get-point-from-event'; -import { assign, createMachine, sendTo } from 'xstate'; -import { selectToolStateMachine } from './select-tool'; -import type { Entity } from '../entities/Entity'; -import { compact } from 'es-toolkit'; -import { rotateEntities } from './rotate-tool.helpers'; -import { cloneKeepingLook } from './move-tool.helpers'; - -export interface RotateContext extends ToolContext { - rotationOrigin: Point | null; - angleStartPoint: Point | null; - originalSelectedEntities: Entity[]; -} - -export enum RotateState { - INIT = 'INIT', - CHECK_SELECTION = 'CHECK_SELECTION', - WAITING_FOR_SELECTION = 'WAITING_FOR_SELECTION', - WAITING_FOR_ROTATION_ORIGIN = 'WAITING_FOR_ROTATION_ORIGIN', - WAITING_FOR_ANGLE_START_POINT = 'WAITING_FOR_ANGLE_START_POINT', - WAITING_FOR_ANGLE_END_POINT = 'WAITING_FOR_ANGLE_END_POINT', -} - -export enum RotateAction { - INIT_ROTATE_TOOL = 'INIT_ROTATE_TOOL', - ENABLE_HELPERS = 'ENABLE_HELPERS', - RECORD_ROTATION_ORIGIN = 'RECORD_ROTATION_ORIGIN', - RECORD_ROTATION_ANGLE_START_POINT = 'RECORD_ROTATION_ANGLE_START_POINT', - COPY_SELECTION_BEFORE_ROTATE = 'COPY_SELECTION_BEFORE_ROTATE', - DRAW_TEMP_ROTATE_ENTITIES = 'DRAW_TEMP_ROTATE_ENTITIES', - ROTATE_SELECTION = 'ROTATE_SELECTION', - ROTATE_BY_ANGLE = 'ROTATE_BY_ANGLE', - DESELECT_ENTITIES = 'DESELECT_ENTITIES', - RESTORE_ORIGINAL_ENTITIES = 'RESTORE_ORIGINAL_ENTITIES', -} - /** - * Rotate tool state machine - * This state machine is responsible for rotating entities around a point (rotation origin) by a certain angle - * It uses the select tool state machine to select entities to rotate - * When the user presses enter, the selected entities are marked for rotating - * The user can then select a rotation origin point - * The user can then select the start point of the rotation angle - * When the user moves their mouse, the selected entities will be updated to rotate according to the angle: start angle point => rotation origin => mouse location - * When the user clicks again, the angle is locked in and the entities are rotated around the rotation origin + * ROTATE(337 라 · 설계 1-3) — 「기준점 지정:」 → 「회전 각도 지정 또는 [복사(C)/참조(R)] <0>:」 · + * 누름 한 번 = 기준점에서 그 점 방향 각 · 숫자 = 반시계 + · R → 「참조 각도를 지정 <0>:」(숫자 · 두 점 · `@` = 기준점) → + * 「새 각도 지정 또는 [점(P)] <0>:」 · 각 기억(ROTATE.ANGLE). */ -export const rotateToolStateMachine = createMachine( - { - types: {} as { - context: RotateContext; - events: StateEvent; - }, - context: { - rotationOrigin: null, - angleStartPoint: null, - originalSelectedEntities: [], - type: Tool.ROTATE, - }, - initial: RotateState.INIT, - states: { - [RotateState.INIT]: { - description: 'Initializing the rotate tool', - always: { - actions: RotateAction.INIT_ROTATE_TOOL, - target: RotateState.CHECK_SELECTION, - }, - }, - [RotateState.CHECK_SELECTION]: { - description: 'Check if there is something selected', - always: [ - { - guard: () => { - return getSelectedEntityIds().length > 0; - }, - target: RotateState.WAITING_FOR_ROTATION_ORIGIN, - }, - { - guard: () => { - return getSelectedEntityIds().length === 0; - }, - target: RotateState.WAITING_FOR_SELECTION, - }, - ], - }, - [RotateState.WAITING_FOR_SELECTION]: { - description: 'Select what you want to rotate', - meta: { - instructions: '회전할 객체 선택 뒤 Enter', - }, - invoke: { - id: 'selectToolInsideTheRotateTool', - src: selectToolStateMachine, - onDone: { - actions: assign(({ context }) => { - return { - ...context, - }; - }), - target: RotateState.CHECK_SELECTION, - }, - }, - on: { - MOUSE_CLICK: { - // Forward the event to the select tool - actions: sendTo('selectToolInsideTheRotateTool', ({ event }) => { - return event; - }), - }, - ESC: { - actions: [RotateAction.DESELECT_ENTITIES, RotateAction.INIT_ROTATE_TOOL], - }, - ENTER: { - // Forward the event to the select tool - actions: sendTo('selectToolInsideTheRotateTool', ({ event }) => { - return event; - }), - }, - DRAW: { - // Forward the event to the select tool - actions: sendTo('selectToolInsideTheRotateTool', ({ event }) => { - return event; - }), - }, - }, - }, - [RotateState.WAITING_FOR_ROTATION_ORIGIN]: { - description: '기준점 지정 (x,y):', - meta: { - instructions: '기준점 지정 (x,y):', - }, - always: { - actions: RotateAction.ENABLE_HELPERS, - }, - on: { - MOUSE_CLICK: { - actions: [RotateAction.RECORD_ROTATION_ORIGIN], - target: RotateState.WAITING_FOR_ANGLE_START_POINT, - }, - ABSOLUTE_POINT_INPUT: { - actions: [RotateAction.RECORD_ROTATION_ORIGIN], - target: RotateState.WAITING_FOR_ANGLE_START_POINT, - }, - ESC: { - actions: RotateAction.DESELECT_ENTITIES, - target: RotateState.INIT, - }, - }, - }, - [RotateState.WAITING_FOR_ANGLE_START_POINT]: { - description: '회전 각도 입력 또는 기준선 끝점 지정:', - meta: { - instructions: '회전 각도 입력 또는 기준선 끝점 지정:', - }, - on: { - MOUSE_CLICK: { - actions: [ - RotateAction.RECORD_ROTATION_ANGLE_START_POINT, - RotateAction.COPY_SELECTION_BEFORE_ROTATE, - ], - target: RotateState.WAITING_FOR_ANGLE_END_POINT, - }, - NUMBER_INPUT: { - // 각도(도) 바로 — 반시계 + - actions: [ - RotateAction.COPY_SELECTION_BEFORE_ROTATE, - RotateAction.ROTATE_BY_ANGLE, - RotateAction.DESELECT_ENTITIES, - ], - target: RotateState.WAITING_FOR_SELECTION, - }, - ESC: { - actions: RotateAction.RESTORE_ORIGINAL_ENTITIES, - target: RotateState.INIT, - }, - }, - }, - [RotateState.WAITING_FOR_ANGLE_END_POINT]: { - description: '회전 방향 점 지정:', - meta: { - instructions: '회전 방향 점 지정:', - }, - on: { - DRAW: { - actions: [RotateAction.DRAW_TEMP_ROTATE_ENTITIES], - }, - MOUSE_CLICK: { - actions: [RotateAction.ROTATE_SELECTION, RotateAction.DESELECT_ENTITIES], - target: RotateState.WAITING_FOR_SELECTION, - }, - ESC: { - actions: RotateAction.RESTORE_ORIGINAL_ENTITIES, - target: RotateState.INIT, - }, - }, - }, - }, - }, - { - actions: { - [RotateAction.INIT_ROTATE_TOOL]: () => { - setShouldDrawHelpers(false); - setGhostHelperEntities([]); - setAngleGuideOriginPoint(null); - }, - [RotateAction.ENABLE_HELPERS]: () => { - setShouldDrawHelpers(true); - }, - [RotateAction.RECORD_ROTATION_ORIGIN]: assign(({ context, event }): RotateContext => { - const rotationOrigin = getPointFromEvent(null, event as PointInputEvent); - setAngleGuideOriginPoint(rotationOrigin); - return { ...context, rotationOrigin }; - }), - [RotateAction.RECORD_ROTATION_ANGLE_START_POINT]: assign( - ({ context, event }): RotateContext => { - return { - ...context, - angleStartPoint: (event as MouseClickEvent).worldMouseLocation, - }; - } - ), - [RotateAction.COPY_SELECTION_BEFORE_ROTATE]: assign(({ context }): RotateContext => { - const selectedEntities = getSelectedEntities(); +import { Tool } from '../tools'; +import { PivotState, createPivotTool } from './pivot-tool'; +import { angleOf, toRadians } from './transform.helpers'; - // Rotate the selected entities to the ghost helper entities, so they are drawn on the canvas, but do not interact with the snap points / angle guides - setGhostHelperEntities(selectedEntities); - // Re-rotate the selected entities from the regular entity list, so they do not get used for determining snap points / angle guides - deleteEntities(selectedEntities, false); +export { PivotState as RotateState }; - // TODO keep a copy of the original entities in the entities list, but set their line color to grey, so the user can see where the entities were before being rotated and the original entities also are used for snap points / angle guides - - setSelectedEntityIds([]); - return { - ...context, - // Make a copy of the selected entities before rotating them, so we can restore them when the user cancels the rotate action - originalSelectedEntities: compact( - selectedEntities.map((entity) => cloneKeepingLook(entity, true)) - ), - }; - }), - [RotateAction.DRAW_TEMP_ROTATE_ENTITIES]: ({ context, event }) => { - if (!context.rotationOrigin || !context.angleStartPoint) { - throw new Error( - '[ROTATE] Calling draw temp rotate entities without a base start point or base end point' - ); - } - - const angleEndpoint = (event as DrawEvent).drawController.getWorldMouseLocation(); - - // Draw all selected entities according to rotate vector, so the user gets visual feedback of where the entities will be end up after rotating - const rotatedEntities = compact( - context.originalSelectedEntities.map((entity) => cloneKeepingLook(entity, true)) - ); - rotateEntities( - rotatedEntities, - context.rotationOrigin, - context.angleStartPoint, - angleEndpoint - ); - - setGhostHelperEntities(rotatedEntities); - }, - [RotateAction.ROTATE_SELECTION]: ({ context, event }) => { - if (!context.rotationOrigin || !context.angleStartPoint) { - throw new Error('[ROTATE] Calling rotate selection without some rotate vector endpoints'); - } - const angleEndpoint = (event as MouseClickEvent).worldMouseLocation; - - // Rotate the entities one final time - const rotatedEntities = compact( - context.originalSelectedEntities.map((entity) => cloneKeepingLook(entity, true)) - ); - rotateEntities( - rotatedEntities, - context.rotationOrigin, - context.angleStartPoint, - angleEndpoint - ); - - // Switch the rotated entities back from the ghost helper entities to the real entities - addEntities(rotatedEntities, true); - setGhostHelperEntities([]); - setSelectedEntityIds([]); - }, - [RotateAction.ROTATE_BY_ANGLE]: ({ context, event }) => { - if (!context.rotationOrigin) { - throw new Error('[ROTATE] Calling rotate by angle without a rotation origin'); - } - const radians = ((event as NumberInputEvent).value * Math.PI) / 180; - const rotatedEntities = compact( - context.originalSelectedEntities.map((entity) => cloneKeepingLook(entity, true)) - ); - for (const entity of rotatedEntities) entity.rotate(context.rotationOrigin, radians); - addEntities(rotatedEntities, true); - setGhostHelperEntities([]); - setSelectedEntityIds([]); - }, - [RotateAction.DESELECT_ENTITIES]: assign(({ context }): RotateContext => { - setGhostHelperEntities([]); - setSelectedEntityIds([]); - return { - ...context, - rotationOrigin: null, - angleStartPoint: null, - originalSelectedEntities: [], - }; - }), - [RotateAction.RESTORE_ORIGINAL_ENTITIES]: assign(({ context }): RotateContext => { - addEntities(context.originalSelectedEntities, false); // This should already be the last state on the undo stack - setGhostHelperEntities([]); - setSelectedEntityIds([]); - return { - ...context, - rotationOrigin: null, - angleStartPoint: null, - originalSelectedEntities: [], - }; - }), - ...selectToolStateMachine.implementations.actions, - }, - } -); +export const rotateToolStateMachine = createPivotTool({ + tool: Tool.ROTATE, + format: 'ang', + value: { text: '회전 각도 지정', remember: 'ROTATE.ANGLE', initial: 0 }, + reference: { text: '참조 각도를 지정', remember: 'ROTATE.REF', initial: 0 }, + next: { text: '새 각도 지정', remember: 'ROTATE.NEW', initial: 0 }, + reject: '유효한 숫자 각도, 두 번째 점 또는 옵션 키워드를 요구함.', + copyNotice: ['선택한 객체의 사본을 회전함.', '선택한 객체를 회전함.'], + invalidNotice: '유효한 숫자 각도, 두 번째 점 또는 옵션 키워드를 요구함.', + measure: angleOf, + combine: (next, reference) => next - reference, + valid: () => true, + apply: (entity, base, degrees) => entity.rotate(base, toRadians(degrees)), +}); diff --git a/B07_DesignDetail/openwebcad/src/tools/scale-tool.helpers.ts b/B07_DesignDetail/openwebcad/src/tools/scale-tool.helpers.ts deleted file mode 100644 index fb8284d7d..000000000 --- a/B07_DesignDetail/openwebcad/src/tools/scale-tool.helpers.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type { Point } from '@flatten-js/core'; -import type { Entity } from '../entities/Entity'; -import { pointDistance } from '../helpers/distance-between-points'; - -/** - * Scale entities by base vector to destination scale vector - * @param entities - * @param baseVectorStartPoint - * @param baseVectorEndPoint - * @param scaleVectorEndPoint - */ -export function scaleEntities( - entities: Entity[], - baseVectorStartPoint: Point, - baseVectorEndPoint: Point, - scaleVectorEndPoint: Point -) { - const scaleFactor = - pointDistance(baseVectorStartPoint, scaleVectorEndPoint) / - pointDistance(baseVectorStartPoint, baseVectorEndPoint); - for (const entity of entities) { - entity.scale(baseVectorStartPoint, scaleFactor); - } -} diff --git a/B07_DesignDetail/openwebcad/src/tools/scale-tool.test.ts b/B07_DesignDetail/openwebcad/src/tools/scale-tool.test.ts index ee52ef7ae..880997a76 100644 --- a/B07_DesignDetail/openwebcad/src/tools/scale-tool.test.ts +++ b/B07_DesignDetail/openwebcad/src/tools/scale-tool.test.ts @@ -27,7 +27,7 @@ function startAtBasePoint() { resetUndoBaseline(); setSelectedEntityIds([a.id]); const actor = createActor(scaleToolStateMachine).start(); - expect(actor.getSnapshot().value).toBe(ScaleState.WAITING_FOR_BASE_VECTOR_START_POINT); + expect(actor.getSnapshot().value).toBe(ScaleState.WAITING_FOR_BASE_POINT); actor.send({ type: ActorEvent.MOUSE_CLICK, worldMouseLocation: new Point(2, 0), @@ -35,7 +35,7 @@ function startAtBasePoint() { holdingCtrl: false, holdingShift: false, }); - expect(actor.getSnapshot().value).toBe(ScaleState.WAITING_FOR_BASE_VECTOR_END_POINT); + expect(actor.getSnapshot().value).toBe(ScaleState.WAITING_FOR_VALUE); return { actor, a, b }; } @@ -53,8 +53,8 @@ describe('축척 — 기준점 + 숫자 배율 (226-1)', () => { expect(getEntities()[0].id).toBe(a.id); expect(getEntities()[1]).toBe(b); expect(ends(1)).toEqual([0, 5, 4, 5]); - // 끝나면 선택 풀고 다음 고르기로 - expect(actor.getSnapshot().value).toBe(ScaleState.WAITING_FOR_SELECTION); + // 한 번 뒤 끝(오토캐드) — 쉬는 상태로 + expect(actor.getSnapshot().status).toBe('done'); }); it('배율 0.5 = 절반', () => { @@ -84,6 +84,6 @@ describe('축척 — 기준점 + 숫자 배율 (226-1)', () => { const { actor } = startAtBasePoint(); typeNumber(actor, 0); expect(ends(0)).toEqual([0, 0, 10, 0]); - expect(actor.getSnapshot().value).toBe(ScaleState.WAITING_FOR_BASE_VECTOR_END_POINT); + expect(actor.getSnapshot().value).toBe(ScaleState.WAITING_FOR_VALUE); }); }); diff --git a/B07_DesignDetail/openwebcad/src/tools/scale-tool.ts b/B07_DesignDetail/openwebcad/src/tools/scale-tool.ts index c73e27c6e..af39c892a 100644 --- a/B07_DesignDetail/openwebcad/src/tools/scale-tool.ts +++ b/B07_DesignDetail/openwebcad/src/tools/scale-tool.ts @@ -1,345 +1,25 @@ -import type { Point } from '@flatten-js/core'; -import { - addEntities, - deleteEntities, - getEntities, - getSelectedEntities, - getSelectedEntityIds, - setAngleGuideOriginPoint, - setEntities, - setGhostHelperEntities, - setSelectedEntityIds, - setShouldDrawHelpers, -} from '../state'; -import { Tool } from '../tools'; -import type { - DrawEvent, - MouseClickEvent, - NumberInputEvent, - StateEvent, - ToolContext, -} from './tool.types'; -import { assign, createMachine, sendTo } from 'xstate'; -import { selectToolStateMachine } from './select-tool'; -import type { Entity } from '../entities/Entity'; -import { compact } from 'es-toolkit'; -import { scaleEntities } from './scale-tool.helpers'; -import { cloneKeepingLook } from './move-tool.helpers'; - -export interface ScaleContext extends ToolContext { - baseVectorStartPoint: Point | null; - baseVectorEndPoint: Point | null; - scaleVectorEndPoint: Point | null; - originalSelectedEntities: Entity[]; -} - -export enum ScaleState { - INIT = 'INIT', - CHECK_SELECTION = 'CHECK_SELECTION', - WAITING_FOR_SELECTION = 'WAITING_FOR_SELECTION', - WAITING_FOR_BASE_VECTOR_START_POINT = 'WAITING_FOR_BASE_VECTOR_START_POINT', - WAITING_FOR_BASE_VECTOR_END_POINT = 'WAITING_FOR_BASE_VECTOR_END_POINT', - WAITING_FOR_SCALE_VECTOR_END_POINT = 'WAITING_FOR_SCALE_VECTOR_END_POINT', -} - -export enum ScaleAction { - INIT_SCALE_TOOL = 'INIT_SCALE_TOOL', - ENABLE_HELPERS = 'ENABLE_HELPERS', - RECORD_BASE_VECTOR_START_POINT = 'RECORD_BASE_VECTOR_START_POINT', - RECORD_BASE_VECTOR_END_POINT = 'RECORD_BASE_VECTOR_END_POINT', - COPY_SELECTION_BEFORE_SCALE = 'COPY_SELECTION_BEFORE_SCALE', - DRAW_TEMP_SCALE_ENTITIES = 'DRAW_TEMP_SCALE_ENTITIES', - SCALE_SELECTION = 'SCALE_SELECTION', - SCALE_SELECTION_BY_FACTOR = 'SCALE_SELECTION_BY_FACTOR', - DESELECT_ENTITIES = 'DESELECT_ENTITIES', - RESTORE_ORIGINAL_ENTITIES = 'RESTORE_ORIGINAL_ENTITIES', -} - /** - * Scale tool state machine - * This state machine is responsible for scaling entities from a base vector to a certain scale vector - * It uses the select tool state machine to select entities to scale - * When the user presses enter, the selected entities are marked for scaling - * The user can then select a base scale vector start point - * The user can then select the base scale vector end point - * When the user moves their mouse, the selected entities will be updated to scale according to the vector: base vector => scale vector from start point to mouse location - * When the user clicks again, the scale vector end point is selected and the entities are scaled according to the scale vector + * SCALE(337 라 · 설계 1-3) — 「기준점 지정:」 → 「축척 비율 지정 또는 [복사(C)/참조(R)] <1.0000>:」 · + * 누름 = 기준점~점 거리 · 숫자 > 0 · R → 「참조 길이 지정 <1.0000>:」(숫자 · 두 점) → 「새 길이 지정 또는 [점(P)] <1.0000>:」 · + * 0 이하 거절 · 비율 기억(SCALE.FACTOR). */ -export const scaleToolStateMachine = createMachine( - { - types: {} as { - context: ScaleContext; - events: StateEvent; - }, - context: { - baseVectorStartPoint: null, - baseVectorEndPoint: null, - scaleVectorEndPoint: null, - originalSelectedEntities: [], - type: Tool.SCALE, - }, - initial: ScaleState.INIT, - states: { - [ScaleState.INIT]: { - description: 'Initializing the scale tool', - always: { - actions: ScaleAction.INIT_SCALE_TOOL, - target: ScaleState.CHECK_SELECTION, - }, - }, - [ScaleState.CHECK_SELECTION]: { - description: 'Check if there is something selected', - always: [ - { - guard: () => { - return getSelectedEntityIds().length > 0; - }, - target: ScaleState.WAITING_FOR_BASE_VECTOR_START_POINT, - }, - { - guard: () => { - return getSelectedEntityIds().length === 0; - }, - target: ScaleState.WAITING_FOR_SELECTION, - }, - ], - }, - [ScaleState.WAITING_FOR_SELECTION]: { - description: 'Select what you want to scale', - meta: { - instructions: '축척할 객체를 선택한 뒤 ENTER.', - }, - invoke: { - id: 'selectToolInsideTheScaleTool', - src: selectToolStateMachine, - onDone: { - actions: assign(() => { - return { - baseVectorStartPoint: null, - baseVectorEndPoint: null, - scaleVectorEndPoint: null, - originalSelectedEntities: [], - }; - }), - target: ScaleState.CHECK_SELECTION, - }, - }, - on: { - MOUSE_CLICK: { - // Forward the event to the select tool - actions: sendTo('selectToolInsideTheScaleTool', ({ event }) => { - return event; - }), - }, - ESC: { - actions: [ScaleAction.DESELECT_ENTITIES, ScaleAction.INIT_SCALE_TOOL], - }, - ENTER: { - // Forward the event to the select tool - actions: sendTo('selectToolInsideTheScaleTool', ({ event }) => { - return event; - }), - }, - DRAW: { - // Forward the event to the select tool - actions: sendTo('selectToolInsideTheScaleTool', ({ event }) => { - return event; - }), - }, - }, - }, - [ScaleState.WAITING_FOR_BASE_VECTOR_START_POINT]: { - description: 'Select the origin of the scale operation', - meta: { - instructions: '기준점을 지정하십시오. (Esc 취소)', - }, - always: { - actions: ScaleAction.ENABLE_HELPERS, - }, - on: { - MOUSE_CLICK: { - actions: [ScaleAction.RECORD_BASE_VECTOR_START_POINT], - target: ScaleState.WAITING_FOR_BASE_VECTOR_END_POINT, - }, - ESC: { - actions: ScaleAction.DESELECT_ENTITIES, - target: ScaleState.INIT, - }, - }, - }, - [ScaleState.WAITING_FOR_BASE_VECTOR_END_POINT]: { - description: 'Select the end of the base scale line', - meta: { - instructions: - '배율을 입력하고 ENTER — 또는 끌어서 맞출 기준선 끝을 클릭하십시오. (Esc 취소)', - }, - on: { - // 오토캐드 SCALE — 기준점 뒤 숫자 배율(2 = 두 배 · 0.5 = 절반) - NUMBER_INPUT: { - guard: ({ event }) => - (event as NumberInputEvent).value > 0 && getSelectedEntityIds().length > 0, - actions: [ScaleAction.SCALE_SELECTION_BY_FACTOR, ScaleAction.DESELECT_ENTITIES], - target: ScaleState.INIT, - }, - MOUSE_CLICK: { - actions: [ - ScaleAction.RECORD_BASE_VECTOR_END_POINT, - ScaleAction.COPY_SELECTION_BEFORE_SCALE, - ], - target: ScaleState.WAITING_FOR_SCALE_VECTOR_END_POINT, - }, - ESC: { - actions: ScaleAction.RESTORE_ORIGINAL_ENTITIES, - target: ScaleState.INIT, - }, - }, - }, - [ScaleState.WAITING_FOR_SCALE_VECTOR_END_POINT]: { - description: 'Select the end of the scale line', - meta: { - instructions: '새 길이의 끝을 클릭하십시오. (Esc 취소)', - }, - on: { - DRAW: { - actions: [ScaleAction.DRAW_TEMP_SCALE_ENTITIES], - }, - MOUSE_CLICK: { - actions: [ScaleAction.SCALE_SELECTION, ScaleAction.DESELECT_ENTITIES], - target: ScaleState.WAITING_FOR_SELECTION, - }, - ESC: { - actions: ScaleAction.RESTORE_ORIGINAL_ENTITIES, - target: ScaleState.INIT, - }, - }, - }, - }, - }, - { - actions: { - [ScaleAction.INIT_SCALE_TOOL]: () => { - setShouldDrawHelpers(false); - setGhostHelperEntities([]); - setAngleGuideOriginPoint(null); - }, - [ScaleAction.ENABLE_HELPERS]: () => { - setShouldDrawHelpers(true); - }, - [ScaleAction.RECORD_BASE_VECTOR_START_POINT]: assign(({ context, event }) => { - setAngleGuideOriginPoint((event as MouseClickEvent).worldMouseLocation); - return { - ...context, - baseVectorStartPoint: (event as MouseClickEvent).worldMouseLocation, - }; - }), - [ScaleAction.RECORD_BASE_VECTOR_END_POINT]: assign(({ context, event }) => { - return { - ...context, - baseVectorEndPoint: (event as MouseClickEvent).worldMouseLocation, - }; - }), - [ScaleAction.COPY_SELECTION_BEFORE_SCALE]: assign(({ context }) => { - const selectedEntities = getSelectedEntities(); +import { Tool } from '../tools'; +import { PivotState, createPivotTool } from './pivot-tool'; +import { distanceOf } from './transform.helpers'; - // Scale the selected entities to the ghost helper entities, so they are drawn on the canvas, but do not interact with the snap points / angle guides - setGhostHelperEntities(selectedEntities); - // Rescale the selected entities from the regular entity list, so they do not get used for determining snap points / angle guides - deleteEntities(selectedEntities, false); +export { PivotState as ScaleState }; - // TODO keep a copy of the original entities in the entities list, but set their line color to grey, so the user can see where the entities were before being scaled and the original entities also are used for snap points / angle guides - - setSelectedEntityIds([]); - return { - ...context, - // Make a copy of the selected entities before scaling them, so we can restore them when the user cancels the scale action - originalSelectedEntities: compact( - selectedEntities.map((entity) => cloneKeepingLook(entity, true)) - ), - }; - }), - [ScaleAction.DRAW_TEMP_SCALE_ENTITIES]: ({ context, event }) => { - if (!context.baseVectorStartPoint || !context.baseVectorEndPoint) { - throw new Error( - '[SCALE] Calling draw temp scale entities without a base start point or base end point' - ); - } - - const scaleVectorEndPointTemp = (event as DrawEvent).drawController.getWorldMouseLocation(); - - // Draw all selected entities according to scale vector, so the user gets visual feedback of where the entities will be end up after scaling - const scaledEntities = compact( - context.originalSelectedEntities.map((entity) => cloneKeepingLook(entity, true)) - ); - scaleEntities( - scaledEntities, - context.baseVectorStartPoint, - context.baseVectorEndPoint, - scaleVectorEndPointTemp - ); - - setGhostHelperEntities(scaledEntities); - }, - [ScaleAction.SCALE_SELECTION]: ({ context, event }) => { - if (!context.baseVectorStartPoint || !context.baseVectorEndPoint) { - throw new Error('[SCALE] Calling scale selection without some scale vector endpoints'); - } - const scaleVectorEndPoint = (event as MouseClickEvent).worldMouseLocation; - - // Scale the entities one final time - const scaledEntities = compact( - context.originalSelectedEntities.map((entity) => cloneKeepingLook(entity, true)) - ); - scaleEntities( - scaledEntities, - context.baseVectorStartPoint, - context.baseVectorEndPoint, - scaleVectorEndPoint - ); - - // Switch the scaled entities back from the ghost helper entities to the real entities - addEntities(scaledEntities, true); - setGhostHelperEntities([]); - setSelectedEntityIds([]); - }, - [ScaleAction.SCALE_SELECTION_BY_FACTOR]: ({ context, event }) => { - const origin = context.baseVectorStartPoint; - if (!origin) { - throw new Error('[SCALE] Calling scale by factor without a base point'); - } - const factor = (event as NumberInputEvent).value; - const selectedIds = new Set(getSelectedEntityIds()); - // 제자리 교체 — 복사본을 키워 같은 순서에 넣고 되돌리기 한 칸으로 남긴다 - setEntities( - getEntities().map((entity) => { - if (!selectedIds.has(entity.id)) return entity; - const scaled = cloneKeepingLook(entity, true); - scaled.scale(origin, factor); - return scaled; - }), - true - ); - }, - [ScaleAction.DESELECT_ENTITIES]: assign(() => { - setGhostHelperEntities([]); - setSelectedEntityIds([]); - return { - startPoint: null, - originalSelectedEntities: [], - lastDrawLocation: null, - }; - }), - [ScaleAction.RESTORE_ORIGINAL_ENTITIES]: assign(({ context }) => { - addEntities(context.originalSelectedEntities, false); // This should already be the last state on the undo stack - setGhostHelperEntities([]); - setGhostHelperEntities([]); - setSelectedEntityIds([]); - return { - startPoint: null, - originalSelectedEntities: [], - lastDrawLocation: null, - }; - }), - ...selectToolStateMachine.implementations.actions, - }, - } -); +export const scaleToolStateMachine = createPivotTool({ + tool: Tool.SCALE, + format: 'len', + value: { text: '축척 비율 지정', remember: 'SCALE.FACTOR', initial: 1 }, + reference: { text: '참조 길이 지정', remember: 'SCALE.REF', initial: 1 }, + next: { text: '새 길이 지정', remember: 'SCALE.NEW', initial: 1 }, + number: { positive: true }, + copyNotice: ['선택한 객체의 사본을 축척함.', '선택한 객체를 축척함.'], + invalidNotice: '값은 양수이고 0이 아니어야 함.', + measure: distanceOf, + combine: (next, reference) => (reference > 0 ? next / reference : null), + valid: (factor) => factor > 0, + apply: (entity, base, factor) => entity.scale(base, factor), +}); diff --git a/B07_DesignDetail/openwebcad/src/tools/transform.helpers.ts b/B07_DesignDetail/openwebcad/src/tools/transform.helpers.ts new file mode 100644 index 000000000..5e83e54af --- /dev/null +++ b/B07_DesignDetail/openwebcad/src/tools/transform.helpers.ts @@ -0,0 +1,145 @@ +/** + * 옮기기 무리 공용(337 라 · 설계 1-3 · 2-14) — 시작(PICKFIRST) · 고르기 물음 · 끝 · 제자리 바꿔 끼움 · 사본 · 그림자 · 각 · 거리. + * 원본은 확정 때까지 도면에 그대로 둠 — Esc · 딴 명령 · 되돌림(N1)에도 제자리 · 두 벌 없음. + */ +import { Point } from '@flatten-js/core'; +import { assign, sendTo } from 'xstate'; +import { GUIDE_LINE_COLOR, GUIDE_LINE_STYLE, GUIDE_LINE_WIDTH } from '../App.consts'; +import { SELECT_OBJECTS, rememberSelection } from '../commands/prompt'; +import type { Entity } from '../entities/Entity'; +import { LineEntity } from '../entities/LineEntity'; +import { + addEntities, + getActiveLayerId, + getEntities, + getSelectedEntities, + setAngleGuideOriginPoint, + setEntities, + setGhostHelperEntities, + setSelectedEntityIds, + setShouldDrawHelpers, +} from '../state'; +import { cloneKeepingLook } from './move-tool.helpers'; +import { selectToolStateMachine } from './select-tool'; +import { END_STATE } from './tool.types'; + +export const SELECT_STATE = 'WAITING_FOR_SELECTION'; +export const SELECT_CHILD = 'selectInsideTransform'; + +export interface PickedContext { + /** 고른 원본(도면에 그대로 있음) */ + originals: Entity[]; +} + +export type Change = (entity: Entity) => void; + +/** 고른 것 받음 — 고르기 이전(P) 열쇠 */ +const takeSelection = assign(() => { + const picked = getSelectedEntities(); + rememberSelection(picked.map((entity) => entity.id)); + return { originals: picked }; +}); + +/** 끝 정리 — 그림자 · 기준점 · 고름 풂 */ +export const endClean = () => { + setGhostHelperEntities([]); + setAngleGuideOriginPoint(null); + setSelectedEntityIds([]); +}; + +/** 시작 — 고른 것 있으면 받고 next 로(PICKFIRST) · 없으면 고르기 */ +export const startState = (next: string) => ({ + entry: () => { + setShouldDrawHelpers(false); + setGhostHelperEntities([]); + setAngleGuideOriginPoint(null); + }, + always: [ + { guard: () => getSelectedEntities().length > 0, actions: takeSelection, target: next }, + { target: SELECT_STATE }, + ], +}); + +/** + * 고르기 물음(설계 2-14 ④) — 자식 선택 도구에 넘김(ALL · L · P 포함) · 고른 것 없이 Enter = 끝. + * `watch` = 넘기기 전 누름 엿보는 액션(자식은 아직 그 누름 전 · STRETCH 걸침 창). + */ +export const selectionState = (next: string, watch?: unknown) => { + const send = sendTo(SELECT_CHILD, ({ event }) => event); + const forward = { actions: send }; + return { + meta: { kind: 'selection', prompt: SELECT_OBJECTS }, + entry: () => setShouldDrawHelpers(false), + invoke: { + id: SELECT_CHILD, + src: selectToolStateMachine, + onDone: [ + { guard: () => getSelectedEntities().length === 0, target: END_STATE }, + { actions: takeSelection, target: next }, + ], + }, + on: { + MOUSE_CLICK: watch ? { actions: [watch, send] } : forward, + ENTER: forward, + DRAW: forward, + 'OPTION.*': forward, + ESC: { target: END_STATE }, + }, + }; +}; + +/** 끝 상태 — 들면 쉬는 상태로(end-command) */ +export const endState = { type: 'final' as const, entry: endClean }; + +/** 바꾼 사본(id 그대로) — 제자리 바꿔 끼울 것 */ +export const changedCopies = (originals: Entity[], change: Change): Entity[] => + originals.map((entity) => { + const copy = cloneKeepingLook(entity, true); + change(copy); + return copy; + }); + +/** 원본 자리에 바꾼 것 끼움 — 순서 · id 그대로 · 되돌림 한 칸 */ +export function replaceInPlace(copies: Entity[]) { + if (!copies.length) return; + const byId = new Map(copies.map((copy) => [copy.id, copy])); + setEntities( + getEntities().map((entity) => byId.get(entity.id) ?? entity), + true + ); +} + +/** 새 사본 더함(새 id) — 되돌림 한 칸 */ +export function addCopies(originals: Entity[], change: Change): Entity[] { + const copies = originals.map((entity) => { + const copy = cloneKeepingLook(entity); + change(copy); + return copy; + }); + if (copies.length) addEntities(copies, true); + return copies; +} + +/** 기준점 → 커서 점선 */ +export function guideLine(from: Point, to: Point): LineEntity { + const line = new LineEntity(getActiveLayerId(), from, to); + line.lineColor = GUIDE_LINE_COLOR; + line.lineWidth = GUIDE_LINE_WIDTH; + line.lineDash = GUIDE_LINE_STYLE; + return line; +} + +/** 미리보기 — 원본은 두고 바꾼 그림자만 */ +export const showGhosts = (originals: Entity[], change: Change, ...extra: Entity[]) => + setGhostHelperEntities([...extra, ...changedCopies(originals, change)]); + +/** a → b 방향 각(도 · 동쪽 0 · 반시계 +) */ +export const angleOf = (a: Point, b: Point): number => + (Math.atan2(b.y - a.y, b.x - a.x) * 180) / Math.PI; + +export const distanceOf = (a: Point, b: Point): number => Math.hypot(b.x - a.x, b.y - a.y); + +export const toRadians = (degrees: number) => (degrees * Math.PI) / 180; + +/** 변위 → 점(좌표 꼴) */ +export const asVector = (from: Point, to: Point) => new Point(to.x - from.x, to.y - from.y); diff --git a/resources/tester/cad/transform-acad.test.ts b/resources/tester/cad/transform-acad.test.ts new file mode 100644 index 000000000..7d31dd1c7 --- /dev/null +++ b/resources/tester/cad/transform-acad.test.ts @@ -0,0 +1,318 @@ +/** + * 337 라 — 옮기기 무리 7(MOVE · COPY · ROTATE · SCALE · MIRROR · STRETCH · ARRAY) + PEDIT 고르기 · 오토캐드 물음 차례. + * 설계 `docs/raw/조사/2026-10-15_337_CAD오토캐드조작_설계.md` 1-3 · 2-14 · 6-3 라 줄. + * 시험 쪽 flatten-js 는 다른 사본 — 점은 앱 코드(LINE · 친 좌표)로만 만들고 숫자로 판정. + * 실행: cd B07_DesignDetail/openwebcad && npx vitest run --dir ../../resources/tester/cad transform-acad + */ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +vi.mock("react-toastify", () => ({ + toast: Object.assign(vi.fn(), { error: vi.fn(), info: vi.fn(), warn: vi.fn(), success: vi.fn() }), +})); + +import { remember } from "../../../B07_DesignDetail/openwebcad/src/commands/prompt"; +import { runCommandInput } from "../../../B07_DesignDetail/openwebcad/src/commands/run-command"; +import type { Entity } from "../../../B07_DesignDetail/openwebcad/src/entities/Entity"; +import { + getSelectedEntityIds, + setSelectedEntityIds, +} from "../../../B07_DesignDetail/openwebcad/src/state"; +import { Tool } from "../../../B07_DesignDetail/openwebcad/src/tools"; +import { + activeTool, + click, + entities, + initIdle, + key, + move, + noticeLine, + promptLine, + type, +} from "./helpers/cad-drive"; + +type Xy = { x: number; y: number }; +const r = (value: number) => Math.round(value * 1000) / 1000 + 0; +const ends = (entity: Entity) => { + const line = entity as unknown as { getStartPoint(): Xy; getEndPoint(): Xy }; + const a = line.getStartPoint(); + const b = line.getEndPoint(); + return [r(a.x), r(a.y), r(b.x), r(b.y)]; +}; +/** 선들 끝점(정렬) */ +const lineEnds = () => + entities("Line") + .map(ends) + .sort((p, q) => p[0] - q[0] || p[1] - q[1] || p[2] - q[2] || p[3] - q[3]); +const enter = () => key("Enter"); +const esc = () => key("Escape"); + +/** 선 하나 그림(쉼으로 돌아옴) */ +const line = (a: string, b: string) => { + type("l"); + type(a); + type(b); + esc(); +}; +/** 선 그리고 고름 */ +const lineSelected = (a = "0,0", b = "20,0") => { + line(a, b); + setSelectedEntityIds(entities().map((entity) => entity.id)); +}; + +/** 세션 기억 — 시험마다 처음 값으로 */ +const KEYS = [ + "MOVE.DISP", + "STRETCH.DISP", + "COPYMODE", + "ROTATE.ANGLE", + "ROTATE.REF", + "ROTATE.NEW", + "SCALE.FACTOR", + "SCALE.REF", + "SCALE.NEW", + "ARRAYTYPE", + "ARRAY.COLS", + "ARRAY.ROWS", + "ARRAY.ITEMS", + "ARRAY.FILL", +]; + +beforeEach(() => { + initIdle(); + for (const name of KEYS) remember(name, undefined); +}); + +describe("MOVE(1-3)", () => { + it("물음 차례 · 두 번째 점 = 기준점에서 변위 · 한 번 뒤 끝 · 고름 풂", () => { + lineSelected(); + const id = entities()[0].id; + type("m"); + expect(promptLine()).toBe("기준점 지정 또는 [변위(D)] <변위>:"); + type("0,0"); + expect(promptLine()).toBe("두 번째 점 지정 또는 <첫 번째 점을 변위로 사용>:"); + type("@10,5"); + expect(lineEnds()).toEqual([[10, 5, 30, 5]]); + expect(entities()[0].id).toBe(id); + expect(activeTool()).toBe(Tool.SELECT); + expect(getSelectedEntityIds()).toEqual([]); + }); + + it("빈 Enter 두 번째 점 = 첫 점 좌표가 변위", () => { + lineSelected(); + type("move"); + type("3,4"); + enter(); + expect(lineEnds()).toEqual([[3, 4, 23, 4]]); + expect(activeTool()).toBe(Tool.SELECT); + }); + + it("D · 기준점 빈 Enter = <변위> · 변위 기억(MOVE.DISP)", () => { + lineSelected(); + type("m"); + type("d"); + expect(promptLine()).toBe("변위 지정 <0.0000, 0.0000>:"); + type("10,5"); + expect(lineEnds()).toEqual([[10, 5, 30, 5]]); + setSelectedEntityIds(entities().map((entity) => entity.id)); + type("m"); + enter(); // 기준점 빈 Enter = 변위(D) + expect(promptLine()).toBe("변위 지정 <10.0000, 5.0000>:"); + enter(); // 기억한 변위 + expect(lineEnds()).toEqual([[20, 10, 40, 10]]); + }); + + it("Esc = 제자리 끝(기준점 뒤 · 미리보기 중)", () => { + lineSelected(); + type("m"); + type("0,0"); + move(50, 50); + esc(); + expect(lineEnds()).toEqual([[0, 0, 20, 0]]); + expect(activeTool()).toBe(Tool.SELECT); + expect(getSelectedEntityIds()).toEqual([]); + }); + + it("고른 것 없음 → 「객체 선택:」 · 누름 고름 · Enter → 기준점 · 빈 고르기 Enter = 끝", () => { + line("0,0", "20,0"); + type("m"); + expect(promptLine()).toBe("객체 선택:"); + enter(); + expect(activeTool()).toBe(Tool.SELECT); + type("m"); + click(10, 0); + enter(); + expect(promptLine()).toBe("기준점 지정 또는 [변위(D)] <변위>:"); + type("0,0"); + type("0,10"); + expect(lineEnds()).toEqual([[0, 10, 20, 10]]); + }); + + it("점 묻는 중 명령 이름 = 거절(도구 그대로)", () => { + lineSelected(); + type("m"); + type("0,0"); + type("c"); + expect(noticeLine()).toBe("점 또는 옵션 키워드를 요구함."); + expect(activeTool()).toBe(Tool.MOVE); + }); + + it("N1 — 기준점 뒤 딴 명령 · Ctrl+Z → 원본 제자리 · 두 벌 없음", () => { + lineSelected(); + const id = entities()[0].id; + type("m"); + type("0,0"); + move(40, 40); + runCommandInput("CIRCLE"); + expect(activeTool()).toBe(Tool.CIRCLE); + expect(lineEnds()).toEqual([[0, 0, 20, 0]]); + expect(entities()[0].id).toBe(id); + esc(); + setSelectedEntityIds([id]); + type("m"); + type("0,0"); + key("z", { ctrlKey: true, code: "KeyZ" }); + expect(activeTool()).toBe(Tool.SELECT); + expect(entities().filter((entity) => entity.id === id)).toHaveLength( + entities().length > 0 ? 1 : 0, + ); + }); +}); + +describe("ROTATE(1-3)", () => { + it("물음 차례 · 숫자 = 반시계 + · 한 번 뒤 끝 · 각 기억", () => { + lineSelected("10,0", "20,0"); + type("ro"); + expect(promptLine()).toBe("기준점 지정:"); + type("0,0"); + expect(promptLine()).toBe("회전 각도 지정 또는 [복사(C)/참조(R)] <0>:"); + type("90"); + expect(lineEnds()).toEqual([[0, 10, 0, 20]]); + expect(activeTool()).toBe(Tool.SELECT); + setSelectedEntityIds(entities().map((entity) => entity.id)); + type("ro"); + type("0,0"); + expect(promptLine()).toBe("회전 각도 지정 또는 [복사(C)/참조(R)] <90>:"); + enter(); // 기억한 90 + expect(lineEnds()).toEqual([[-10, 0, -20, 0]]); + }); + + it("누름 한 번 = 기준점에서 그 점 방향 각", () => { + lineSelected("10,0", "20,0"); + type("ro"); + type("0,0"); + type("-30,30"); // 135° + const [x1, y1] = lineEnds()[0]; + expect([x1, y1]).toEqual([r(-10 / Math.SQRT2), r(10 / Math.SQRT2)]); + }); + + it("C = 사본 회전(원본 둠)", () => { + lineSelected("10,0", "20,0"); + type("ro"); + type("0,0"); + type("c"); + expect(activeTool()).toBe(Tool.ROTATE); + type("90"); + expect(lineEnds()).toEqual([ + [0, 10, 0, 20], + [10, 0, 20, 0], + ]); + }); + + it("R — 참조 두 점(@ = 기준점) → 새 각도 · 참조 숫자 · P 두 점", () => { + lineSelected("10,0", "20,0"); + type("ro"); + type("0,0"); + type("r"); + expect(promptLine()).toBe("참조 각도를 지정 <0>:"); + type("@0,0"); // 기준점 + expect(promptLine()).toBe("두 번째 점 지정:"); + type("10,10"); // 참조 45° + expect(promptLine()).toBe("새 각도 지정 또는 [점(P)] <0>:"); + type("90"); // 90 − 45 + const s = 10 / Math.SQRT2; + expect(lineEnds()).toEqual([[r(s), r(s), r(2 * s), r(2 * s)]]); + + setSelectedEntityIds(entities().map((entity) => entity.id)); + type("ro"); + type("0,0"); + type("r"); + expect(promptLine()).toBe("참조 각도를 지정 <45>:"); + type("45"); + type("p"); + expect(promptLine()).toBe("첫 번째 점 지정:"); + type("0,0"); + type("-5,0"); // 새 180 → 135 돌림 + expect(lineEnds()).toEqual([[-10, 0, -20, 0]]); + }); + + it("Esc = 제자리 끝 · 글 = 각 거절", () => { + lineSelected("10,0", "20,0"); + type("ro"); + type("0,0"); + type("abc"); + expect(noticeLine()).toBe("유효한 숫자 각도, 두 번째 점 또는 옵션 키워드를 요구함."); + move(0, 40); + esc(); + expect(lineEnds()).toEqual([[10, 0, 20, 0]]); + expect(activeTool()).toBe(Tool.SELECT); + }); +}); + +describe("SCALE(1-3)", () => { + it("물음 차례 · 숫자 비율 · 점 = 거리 · 기억", () => { + lineSelected(); + type("sc"); + expect(promptLine()).toBe("기준점 지정:"); + type("0,0"); + expect(promptLine()).toBe("축척 비율 지정 또는 [복사(C)/참조(R)] <1.0000>:"); + type("2"); + expect(lineEnds()).toEqual([[0, 0, 40, 0]]); + expect(activeTool()).toBe(Tool.SELECT); + setSelectedEntityIds(entities().map((entity) => entity.id)); + type("sc"); + type("0,0"); + expect(promptLine()).toBe("축척 비율 지정 또는 [복사(C)/참조(R)] <2.0000>:"); + type("0,0.5"); // 거리 0.5 + expect(lineEnds()).toEqual([[0, 0, 20, 0]]); + }); + + it("0 · 음수 거절(도구 그대로) · C = 사본", () => { + lineSelected(); + type("sc"); + type("0,0"); + type("0"); + expect(noticeLine()).toBe("값은 양수이고 0이 아니어야 함."); + type("-2"); + expect(noticeLine()).toBe("값은 양수이고 0이 아니어야 함."); + expect(activeTool()).toBe(Tool.SCALE); + type("c"); + type("0.5"); + expect(lineEnds()).toEqual([ + [0, 0, 10, 0], + [0, 0, 20, 0], + ]); + }); + + it("R — 참조 숫자 → 새 길이 · 참조 두 점 → P 두 점", () => { + lineSelected(); + type("sc"); + type("0,0"); + type("r"); + expect(promptLine()).toBe("참조 길이 지정 <1.0000>:"); + type("10"); + expect(promptLine()).toBe("새 길이 지정 또는 [점(P)] <1.0000>:"); + type("5"); + expect(lineEnds()).toEqual([[0, 0, 10, 0]]); + + setSelectedEntityIds(entities().map((entity) => entity.id)); + type("sc"); + type("0,0"); + type("r"); + type("0,0"); + type("10,0"); // 참조 10 + type("p"); + type("0,0"); + type("0,30"); // 새 30 + expect(lineEnds()).toEqual([[0, 0, 30, 0]]); + }); +}); From 1d5cdff0a50ba82f9b7e151c669a1d2c9a9d9e04 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Fri, 16 Oct 2026 00:25:20 +0900 Subject: [PATCH 74/81] =?UTF-8?q?feat(CAD):=20337-5=20=EB=9D=BC=20?= =?UTF-8?q?=EC=98=AE=EA=B8=B0=EA=B8=B0=20=EB=AC=B4=EB=A6=AC=202=20?= =?UTF-8?q?=EB=8B=A8=EA=B3=84=20=E2=80=94=20COPY=20=C2=B7=20MIRROR=20?= =?UTF-8?q?=C2=B7=20STRETCH=20=C2=B7=20ARRAY=20=C2=B7=20PEDIT=20=EA=B3=A0?= =?UTF-8?q?=EB=A5=B4=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - COPY: 기준점 [변위(D)/모드(O)/다중(M)] · 두 번째 점 [배열(A)] → [배열(A)/종료(E)/명령 취소(U)] <종료> 되풀이 · 첫 빈 Enter = 변위 · COPYMODE 기억 · A 항목 수 · F 맞춤 - MIRROR: 대칭선 두 점 → 원본 객체를 지우시겠습니까? [예(Y)/아니오(N)] <아니오> · Esc = 안 만듦 - STRETCH: MOVE 틀 + 걸침 창 안 꼭짓점만(선 · 폴리선 토막 · 사각형) · 창 안 통째 · 하나씩 고른 것 = 이동 · STRETCH.DISP - ARRAY: 유형 [직사각형(R)/원형(PO)] <직사각형>(ARRAYTYPE) → 편집 물음 되풀이(COU · S · COL · R · X / I · A · F · ROT · X) · 미리보기 4 × 3 · 6 개 360° · 처음 간격 범위 1.5 배 · Esc = 안 만듦(N17 없어짐) - PEDIT: 공용 고르기 상태(객체 선택 · 빈 Enter · Esc = 끝 · 묶은 뒤 끝) - 시험 transform-acad 29 + ALL · L · P 1(선택 도구 처리 뒤 돎) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01TteK8mfYZetk5P3bEZkyVZ --- .../src/tools/array-tool.helpers.ts | 169 +++++ .../openwebcad/src/tools/array-tool.ts | 618 ++++++++---------- .../openwebcad/src/tools/copy-tool.ts | 581 ++++++++-------- .../src/tools/modify/transform-tools.ts | 188 ++++-- .../openwebcad/src/tools/pedit-tool.ts | 146 +---- resources/tester/cad/transform-acad.test.ts | 357 ++++++++++ 6 files changed, 1260 insertions(+), 799 deletions(-) create mode 100644 B07_DesignDetail/openwebcad/src/tools/array-tool.helpers.ts diff --git a/B07_DesignDetail/openwebcad/src/tools/array-tool.helpers.ts b/B07_DesignDetail/openwebcad/src/tools/array-tool.helpers.ts new file mode 100644 index 000000000..388576e69 --- /dev/null +++ b/B07_DesignDetail/openwebcad/src/tools/array-tool.helpers.ts @@ -0,0 +1,169 @@ +/** ARRAY 계산 · 물음(337 라 · 설계 1-3) — 직사각형 열 × 행 · 원형 항목 · 채울 각도 · 항목 회전 */ +import type { Box, Point } from '@flatten-js/core'; +import type { OptionSpec, PromptSpec } from '../commands/prompt'; +import type { Entity } from '../entities/Entity'; +import { cloneKeepingLook } from './move-tool.helpers'; +import type { PickedContext } from './transform.helpers'; + +export interface ArrayValues extends PickedContext { + columns: number; + rows: number; + columnSpacing: number; + rowSpacing: number; + center: Point | null; + items: number; + /** 채울 각도(도 · + 반시계) */ + fill: number; + rotateItems: boolean; +} + +const opt = (key: string, kw: string, label: string, en = key): OptionSpec => ({ + key, + kw, + en, + label, +}); + +export const ARRAY_OPT = { + RECTANGULAR: opt('RECTANGULAR', 'R', '직사각형'), + POLAR: opt('POLAR', 'PO', '원형'), + PATH: opt('PATH', 'PA', '경로'), + COUNT: opt('COUNT', 'COU', '개수'), + SPACING: opt('SPACING', 'S', '간격두기'), + COLUMNS: opt('COLUMNS', 'COL', '열'), + ROWS: opt('ROWS', 'R', '행'), + POLAR_ROWS: opt('ROWS', 'ROW', '행'), + ITEMS: opt('ITEMS', 'I', '항목'), + ANGLE: opt('ANGLE', 'A', '사이의 각도'), + FILL: opt('FILL', 'F', '채울 각도'), + ROTATE: opt('ROTATE', 'ROT', '항목 회전'), + EXIT: opt('EXIT', 'X', '종료'), + ASSOCIATIVE: opt('ASSOCIATIVE', 'AS', '연관'), + BASE: opt('BASE', 'B', '기준점'), + LEVELS: opt('LEVELS', 'L', '수준'), + AXIS: opt('AXIS', 'A', '회전축'), +}; + +const O = ARRAY_OPT; +const COUNT_RULE = { int: true, min: 1, max: 32767 } as const; + +/** 항목 사이 각 — 채울 각이 한 바퀴면 항목 수로 나눔 · 아니면 사이 수로 */ +export const angleBetween = (values: Pick): number => { + if (Math.abs(values.fill) >= 360) return values.fill / values.items; + return values.items > 1 ? values.fill / (values.items - 1) : 0; +}; + +const value = (text: string, remember: string, initial: number, format?: 'int' | 'ang') => ({ + text, + default: { kind: 'value' as const, remember, initial, ...(format ? { format } : {}) }, +}); + +/** 물음 — 함수 꼴은 지금 값(빈 context 면 처음 값) */ +export const ARRAY_PROMPT = { + type: (fallback: string): PromptSpec => ({ + text: '배열 유형 입력', + options: [O.RECTANGULAR, O.POLAR], + absent: [O.PATH], + default: { kind: 'option', key: fallback }, + }), + center: { text: '배열의 중심점 지정', absent: [O.BASE, O.AXIS] } satisfies PromptSpec, + editRect: { + text: '그립을 선택하여 배열을 편집하거나', + options: [O.COUNT, O.SPACING, O.COLUMNS, O.ROWS, O.EXIT], + absent: [O.ASSOCIATIVE, O.BASE, O.LEVELS], + default: { kind: 'option', key: O.EXIT.key }, + } satisfies PromptSpec, + editPolar: { + text: '그립을 선택하여 배열을 편집하거나', + options: [O.ITEMS, O.ANGLE, O.FILL, O.ROTATE, O.EXIT], + absent: [O.ASSOCIATIVE, O.BASE, O.POLAR_ROWS, O.LEVELS], + default: { kind: 'option', key: O.EXIT.key }, + } satisfies PromptSpec, + columns: (v: Partial): PromptSpec => ({ + ...value('열 수 입력', 'ARRAY.COLS', v.columns ?? 4, 'int'), + number: COUNT_RULE, + }), + rows: (v: Partial): PromptSpec => ({ + ...value('행 수 입력', 'ARRAY.ROWS', v.rows ?? 3, 'int'), + number: COUNT_RULE, + }), + columnSpacing: (v: Partial): PromptSpec => + value('열 사이의 거리 지정', 'ARRAY.COLDIST', v.columnSpacing ?? 1), + rowSpacing: (v: Partial): PromptSpec => + value('행 사이의 거리 지정', 'ARRAY.ROWDIST', v.rowSpacing ?? 1), + items: (v: Partial): PromptSpec => ({ + ...value('배열의 항목 수 입력', 'ARRAY.ITEMS', v.items ?? 6, 'int'), + number: COUNT_RULE, + }), + angle: (v: Partial): PromptSpec => + value( + '항목 사이의 각도 지정', + 'ARRAY.ANGLE', + angleBetween({ items: v.items ?? 6, fill: v.fill ?? 360 }), + 'ang' + ), + fill: (v: Partial): PromptSpec => + value('채울 각도 지정(+=ccw, -=cw)', 'ARRAY.FILL', v.fill ?? 360, 'ang'), + rotateItems: (v: Partial): PromptSpec => ({ + text: '배열된 항목을 회전하시겠습니까?', + options: [opt('YES', 'Y', '예'), opt('NO', 'N', '아니오')], + default: { kind: 'option', key: v.rotateItems === false ? 'NO' : 'YES' }, + }), +}; + +/** 고른 것 범위 — 처음 간격 = 폭 · 높이의 1.5 배(0 이면 다른 쪽 · 1) */ +export function initialSpacing(originals: Entity[]): { column: number; row: number } { + const box = originals.reduce((all, entity) => { + const next = entity.getBoundingBox(); + return all ? all.merge(next) : next; + }, null); + const width = box ? box.xmax - box.xmin : 0; + const height = box ? box.ymax - box.ymin : 0; + const fallback = Math.max(width, height) || 1; + return { column: 1.5 * (width || fallback), row: 1.5 * (height || fallback) }; +} + +/** 고른 것 범위 가운데 — 항목 회전 끔일 때 옮김 기준 */ +function selectionCenter(originals: Entity[]): Point | null { + const boxes = originals.map((entity) => entity.getBoundingBox()); + if (!boxes.length) return null; + return boxes.reduce((all, next) => all.merge(next)).center; +} + +/** 직사각형 — (0,0) 원본 빼고 열 × 행 사본 */ +export function rectangularCopies(values: ArrayValues): Entity[] { + const copies: Entity[] = []; + for (let row = 0; row < values.rows; row++) { + for (let column = 0; column < values.columns; column++) { + if (!row && !column) continue; + for (const entity of values.originals) { + const copy = cloneKeepingLook(entity); + copy.move(column * values.columnSpacing, row * values.rowSpacing); + copies.push(copy); + } + } + } + return copies; +} + +/** 원형 — 중심 둘레 항목 수 − 1 사본 · 항목 회전 끔이면 돌지 않고 자리만 */ +export function polarCopies(values: ArrayValues, center = values.center): Entity[] { + if (!center) return []; + const step = (angleBetween(values) * Math.PI) / 180; + const anchor = selectionCenter(values.originals); + const copies: Entity[] = []; + for (let index = 1; index < values.items; index++) { + const radians = step * index; + for (const entity of values.originals) { + const copy = cloneKeepingLook(entity); + if (values.rotateItems || !anchor) { + copy.rotate(center, radians); + } else { + const moved = anchor.rotate(radians, center); + copy.move(moved.x - anchor.x, moved.y - anchor.y); + } + copies.push(copy); + } + } + return copies; +} diff --git a/B07_DesignDetail/openwebcad/src/tools/array-tool.ts b/B07_DesignDetail/openwebcad/src/tools/array-tool.ts index be817e7d2..828fe2a89 100644 --- a/B07_DesignDetail/openwebcad/src/tools/array-tool.ts +++ b/B07_DesignDetail/openwebcad/src/tools/array-tool.ts @@ -1,369 +1,293 @@ -import { type Point, Vector } from '@flatten-js/core'; -import { assign, createMachine, sendTo } from 'xstate'; -import { GUIDE_LINE_COLOR, GUIDE_LINE_STYLE, GUIDE_LINE_WIDTH, TO_RADIANS } from '../App.consts.ts'; -import type { Entity } from '../entities/Entity'; -import { LineEntity } from '../entities/LineEntity.ts'; -import { getPointFromEvent } from '../helpers/get-point-from-event.ts'; -import { - addEntities, - getActiveLayerId, - getSelectedEntities, - getSelectedEntityIds, - setAngleGuideOriginPoint, - setGhostHelperEntities, - setSelectedEntityIds, - setShouldDrawHelpers, -} from '../state'; +/** + * ARRAY(337 라 · 설계 1-3) — 「객체 선택:」 → 「배열 유형 입력 [직사각형(R)/원형(PO)] <직사각형>:」(ARRAYTYPE 기억) → + * 직사각형: 미리보기 4열 × 3행 → 「그립을 선택하여 배열을 편집하거나 [개수(COU)/간격두기(S)/열(COL)/행(R)/종료(X)] <종료>:」 되풀이 · + * 원형: 「배열의 중심점 지정:」 → 미리보기 6개 360° → 「… [항목(I)/사이의 각도(A)/채울 각도(F)/항목 회전(ROT)/종료(X)] <종료>:」 되풀이. + * 종료(X) · 빈 Enter = 만듦 · Esc = 안 만듦 · 처음 간격 = 고른 것 범위 폭 · 높이의 1.5 배. + */ +import type { Point } from '@flatten-js/core'; +import { assign, createMachine } from 'xstate'; +import { recall, remember } from '../commands/prompt'; +import { getPointFromEvent } from '../helpers/get-point-from-event'; +import { addEntities, setGhostHelperEntities, setShouldDrawHelpers } from '../state'; import { Tool } from '../tools'; -import { CopyAction } from './copy-tool.ts'; -import { cloneKeepingLook } from './move-tool.helpers.ts'; -import { selectToolStateMachine } from './select-tool.ts'; -import type { - AbsolutePointInputEvent, - DrawEvent, - MouseClickEvent, - NumberInputEvent, - StateEvent, - TextInputEvent, - ToolContext, +import { + ARRAY_OPT, + ARRAY_PROMPT, + type ArrayValues, + angleBetween, + initialSpacing, + polarCopies, + rectangularCopies, +} from './array-tool.helpers'; +import { + type DrawEvent, + END_STATE, + type NumberInputEvent, + type PointInputEvent, + type StateEvent, + type ToolContext, } from './tool.types'; +import { endState, selectionState, startState } from './transform.helpers'; export enum ArrayState { INIT = 'INIT', WAITING_FOR_SELECTION = 'WAITING_FOR_SELECTION', - CHECK_SELECTION = 'CHECK_SELECTION', - ASK_COPY_MODE = 'ASK_COPY_MODE', - ASK_NUMBER_OF_COPIES = 'ASK_NUMBER_OF_COPIES', - LINEAR_WAITING_FOR_START_POINT = 'LINEAR_WAITING_FOR_START_POINT', - LINEAR_WAITING_FOR_END_POINT = 'LINEAR_WAITING_FOR_END_POINT', - RADIAL_WAITING_FOR_PIVOT_POINT = 'RADIAL_WAITING_FOR_PIVOT_POINT', - RADIAL_WAITING_FOR_ANGLE = 'RADIAL_WAITING_FOR_ANGLE', - EXECUTE_COPY = 'EXECUTE_COPY', + WAITING_FOR_TYPE = 'WAITING_FOR_TYPE', + WAITING_FOR_CENTER = 'WAITING_FOR_CENTER', + EDIT_RECTANGULAR = 'EDIT_RECTANGULAR', + EDIT_POLAR = 'EDIT_POLAR', + WAITING_FOR_COLUMNS = 'WAITING_FOR_COLUMNS', + WAITING_FOR_ROWS = 'WAITING_FOR_ROWS', + WAITING_FOR_COLUMN_SPACING = 'WAITING_FOR_COLUMN_SPACING', + WAITING_FOR_ROW_SPACING = 'WAITING_FOR_ROW_SPACING', + WAITING_FOR_ITEMS = 'WAITING_FOR_ITEMS', + WAITING_FOR_ANGLE = 'WAITING_FOR_ANGLE', + WAITING_FOR_FILL = 'WAITING_FOR_FILL', + WAITING_FOR_ROTATE_ITEMS = 'WAITING_FOR_ROTATE_ITEMS', } -export enum ArrayAction { - INIT_ARRAY_TOOL = 'INIT_ARRAY_TOOL', - PERFORM_COPY = 'PERFORM_COPY', - DESELECT_ENTITIES = 'DESELECT_ENTITIES', - DRAW_TEMP_DISTANCE_LINE = 'DRAW_TEMP_DISTANCE_LINE', +/** 편집 옵션이 이어 묻는 차례 — 개수 = 열 → 행 · 간격두기 = 열 거리 → 행 거리 · 열 = 수 → 거리 · 행 = 수 → 거리 */ +type Flow = 'COUNT' | 'SPACING' | 'COLUMNS' | 'ROWS' | null; + +export interface ArrayContext extends ToolContext, ArrayValues { + flow: Flow; } -export interface ArrayContext extends ToolContext { - copyMode: CopyMode | null; - startDistanceVector: Point | null; - endDistanceVector: Point | null; - pivotPoint: Point | null; - angleStep: number | null; - numberOfCopies: number; -} +type Ctx = { context: ArrayContext; event: StateEvent }; -enum CopyMode { - LINEAR = 'LINEAR', - RADIAL = 'RADIAL', -} +const S = ArrayState; +const O = ARRAY_OPT; +const esc = { ESC: { target: END_STATE } }; +const numberOf = (event: StateEvent) => (event as NumberInputEvent).value; -const initialArrayContext: ArrayContext = { - copyMode: null, - startDistanceVector: null, - endDistanceVector: null, - pivotPoint: null, - angleStep: null, - numberOfCopies: 0, - type: Tool.ARRAY, +/** 물음 기본값 = 지금 값(빈 Enter 가 지금 값을 다시 보냄) */ +const syncRect = (values: ArrayValues) => { + remember('ARRAY.COLS', values.columns); + remember('ARRAY.ROWS', values.rows); + remember('ARRAY.COLDIST', values.columnSpacing); + remember('ARRAY.ROWDIST', values.rowSpacing); +}; +const syncPolar = (values: ArrayValues) => { + remember('ARRAY.ITEMS', values.items); + remember('ARRAY.FILL', values.fill); + remember('ARRAY.ANGLE', angleBetween(values)); }; -export const arrayToolStateMachine = createMachine( - { - types: {} as { - context: ArrayContext; - events: StateEvent; +const showRect = ({ context }: Ctx) => { + syncRect(context); + setGhostHelperEntities(rectangularCopies(context)); +}; +const showPolar = ({ context }: Ctx) => { + syncPolar(context); + setGhostHelperEntities(polarCopies(context)); +}; +const create = (copies: (values: ArrayValues) => unknown[]) => ({ + actions: ({ context }: Ctx) => { + const made = copies(context) as Parameters[0]; + if (made.length) addEntities(made, true); + }, + target: END_STATE, +}); + +/** 숫자 물음 — 받아서 assign · 흐름 따라 다음 */ +const numberStep = ( + prompt: unknown, + take: (value: number, context: ArrayContext) => Partial | null, + next: { flow?: Flow; target: string }[] +) => ({ + meta: { kind: 'number', prompt }, + on: { + NUMBER_INPUT: [ + ...next.map(({ flow, target }) => ({ + guard: ({ context, event }: Ctx) => + !!take(numberOf(event), context) && (flow === undefined || context.flow === flow), + actions: assign(({ context, event }: Ctx) => take(numberOf(event), context) ?? {}), + target, + })), + { actions: () => noticeLater('값이 범위를 벗어남.') }, + ], + ...esc, + }, +}); + +const noticeLater = (text: string) => { + void import('../inputController/prompt-input').then(({ notice }) => notice(text)); +}; + +const fillOk = (fill: number) => fill !== 0 && Math.abs(fill) <= 360; + +// biome-ignore lint/suspicious/noExplicitAny: 공용 상태 조각(여러 도구 공통)을 섞어 만든다 +const states: Record = { + [S.INIT]: startState(S.WAITING_FOR_TYPE), + [S.WAITING_FOR_SELECTION]: selectionState(S.WAITING_FOR_TYPE), + [S.WAITING_FOR_TYPE]: { + meta: { + kind: 'keyword', + prompt: () => ARRAY_PROMPT.type(recall('ARRAYTYPE') ?? O.RECTANGULAR.key), }, - context: initialArrayContext, - initial: ArrayState.INIT, - states: { - [ArrayState.INIT]: { - description: 'Initializing the array copy tool', - always: { - actions: ArrayState.INIT, - target: ArrayState.CHECK_SELECTION, - }, - }, - [ArrayState.WAITING_FOR_SELECTION]: { - description: 'Select what you want to copy', - meta: { - instructions: '배열할 객체 선택 뒤 Enter', - }, - invoke: { - id: 'selectToolInsideTheCopyTool', - src: selectToolStateMachine, - onDone: { - actions: assign(() => { - return initialArrayContext; - }), - target: ArrayState.CHECK_SELECTION, - }, - }, - on: { - MOUSE_CLICK: { - // Forward the event to the select tool - actions: sendTo('selectToolInsideTheCopyTool', ({ event }) => { - return event; - }), - }, - ESC: { - actions: [ArrayAction.DESELECT_ENTITIES, ArrayAction.INIT_ARRAY_TOOL], - }, - ENTER: { - // Forward the event to the select tool - actions: sendTo('selectToolInsideTheCopyTool', ({ event }) => { - return event; - }), - }, - DRAW: { - // Forward the event to the select tool - actions: sendTo('selectToolInsideTheCopyTool', ({ event }) => { - return event; - }), - }, - }, - }, - [ArrayState.CHECK_SELECTION]: { - description: 'Check if there is something selected', - always: [ - { - guard: () => { - return getSelectedEntityIds().length > 0; - }, - target: ArrayState.ASK_COPY_MODE, - }, - { - guard: () => { - return getSelectedEntityIds().length === 0; - }, - target: ArrayState.WAITING_FOR_SELECTION, - }, + on: { + 'OPTION.RECTANGULAR': { + actions: [ + () => remember('ARRAYTYPE', O.RECTANGULAR.key), + assign(({ context }: Ctx) => { + const spacing = initialSpacing(context.originals); + return { + columns: recall('ARRAY.COLS') ?? 4, + rows: recall('ARRAY.ROWS') ?? 3, + columnSpacing: spacing.column, + rowSpacing: spacing.row, + }; + }), ], + target: S.EDIT_RECTANGULAR, }, - [ArrayState.ASK_COPY_MODE]: { - meta: { instructions: '배열 방식 [직선(L)/원형(R)] <직선>:' }, - on: { - TEXT_INPUT: { - actions: assign(({ event }) => { - return { - copyMode: - (event as TextInputEvent).value === 'R' ? CopyMode.RADIAL : CopyMode.LINEAR, - }; - }), - target: ArrayState.ASK_NUMBER_OF_COPIES, - }, - ENTER: { - actions: assign(() => ({ - copyMode: CopyMode.LINEAR, - })), - target: ArrayState.ASK_NUMBER_OF_COPIES, - }, - ESC: { - actions: ArrayAction.INIT_ARRAY_TOOL, - }, - }, - }, - [ArrayState.ASK_NUMBER_OF_COPIES]: { - meta: { instructions: '복사 개수 입력:' }, - on: { - NUMBER_INPUT: [ - { - guard: ({ context }) => { - return context.copyMode === CopyMode.LINEAR; - }, - actions: assign(({ event }) => ({ - numberOfCopies: (event as NumberInputEvent).value, - })), - target: ArrayState.LINEAR_WAITING_FOR_START_POINT, - }, - { - guard: ({ context }) => { - return context.copyMode === CopyMode.RADIAL; - }, - actions: assign(({ event }) => ({ - numberOfCopies: (event as NumberInputEvent).value, - })), - target: ArrayState.RADIAL_WAITING_FOR_PIVOT_POINT, - }, - ], - ESC: { - actions: ArrayAction.INIT_ARRAY_TOOL, - }, - }, - }, - [ArrayState.LINEAR_WAITING_FOR_START_POINT]: { - meta: { instructions: '간격 시작점 지정:' }, - on: { - ABSOLUTE_POINT_INPUT: { - actions: assign(({ event }) => ({ - startDistanceVector: (event as AbsolutePointInputEvent).value, - })), - target: ArrayState.EXECUTE_COPY, - }, - MOUSE_CLICK: { - actions: assign(({ event }) => ({ - startDistanceVector: (event as MouseClickEvent).worldMouseLocation, - })), - target: ArrayState.LINEAR_WAITING_FOR_END_POINT, - }, - ESC: { - actions: ArrayAction.INIT_ARRAY_TOOL, - }, - }, - }, - [ArrayState.LINEAR_WAITING_FOR_END_POINT]: { - meta: { instructions: '간격 끝점 지정:' }, - on: { - RELATIVE_POINT_INPUT: { - actions: assign(({ event, context }) => ({ - endDistanceVector: getPointFromEvent(context.startDistanceVector, event), - })), - target: ArrayState.EXECUTE_COPY, - }, - ABSOLUTE_POINT_INPUT: { - actions: assign(({ event, context }) => ({ - endDistanceVector: getPointFromEvent(context.startDistanceVector, event), - })), - target: ArrayState.EXECUTE_COPY, - }, - MOUSE_CLICK: { - actions: assign(({ event, context }) => ({ - endDistanceVector: getPointFromEvent(context.startDistanceVector, event), - })), - target: ArrayState.EXECUTE_COPY, - }, - ESC: { - actions: ArrayAction.INIT_ARRAY_TOOL, - }, - DRAW: { - actions: ArrayAction.DRAW_TEMP_DISTANCE_LINE, - }, - }, - }, - [ArrayState.RADIAL_WAITING_FOR_PIVOT_POINT]: { - meta: { instructions: '회전 중심점 지정:' }, - on: { - ABSOLUTE_POINT_INPUT: { - actions: assign(({ event }) => ({ - pivotPoint: (event as AbsolutePointInputEvent).value, - })), - target: ArrayState.RADIAL_WAITING_FOR_ANGLE, - }, - MOUSE_CLICK: { - actions: assign(({ event }) => ({ - pivotPoint: (event as MouseClickEvent).worldMouseLocation, - })), - target: ArrayState.RADIAL_WAITING_FOR_ANGLE, - }, - ESC: { - actions: ArrayAction.INIT_ARRAY_TOOL, - }, - }, - }, - [ArrayState.RADIAL_WAITING_FOR_ANGLE]: { - meta: { instructions: '한 칸 각도(도) 입력:' }, - on: { - NUMBER_INPUT: { - target: ArrayState.EXECUTE_COPY, - actions: assign(({ event }) => ({ - angleStep: (event as NumberInputEvent).value, - })), - }, - }, - }, - [ArrayState.EXECUTE_COPY]: { - entry: ArrayAction.PERFORM_COPY, - always: ArrayState.INIT, + 'OPTION.POLAR': { + actions: [ + () => remember('ARRAYTYPE', O.POLAR.key), + assign(() => ({ + items: recall('ARRAY.ITEMS') ?? 6, + fill: recall('ARRAY.FILL') ?? 360, + rotateItems: true, + })), + ], + target: S.WAITING_FOR_CENTER, }, + ...esc, }, }, - { - actions: { - [ArrayAction.INIT_ARRAY_TOOL]: assign(() => { - setShouldDrawHelpers(true); - setGhostHelperEntities([]); - setSelectedEntityIds([]); - setAngleGuideOriginPoint(null); - return { - copyMode: null, - distanceVector: null, - pivotPoint: null, - angleStep: null, - numberOfCopies: 0, - selectedEntities: [], - }; - }), - - [ArrayAction.DRAW_TEMP_DISTANCE_LINE]: ({ context, event }) => { - if (!context.startDistanceVector) { - throw new Error('[ARRAY] Calling draw temp distance line without a start point'); - } - - const endPointTemp = (event as DrawEvent).drawController.getWorldMouseLocation(); - - // Draw the array entities to show the result - // Draw all selected entities according to distance vector and number of copies, so the user gets visual feedback of where the entities will be copied; - // TODO - - // // Draw a dashed line between the start move point and the current mouse location - const activeDistanceLine = new LineEntity( - getActiveLayerId(), - context.startDistanceVector, - endPointTemp - ); - activeDistanceLine.lineColor = GUIDE_LINE_COLOR; - activeDistanceLine.lineWidth = GUIDE_LINE_WIDTH; - activeDistanceLine.lineDash = GUIDE_LINE_STYLE; - setGhostHelperEntities([activeDistanceLine]); + [S.WAITING_FOR_CENTER]: { + meta: { kind: 'point', prompt: ARRAY_PROMPT.center }, + entry: () => setShouldDrawHelpers(true), + on: { + DRAW: { + actions: ({ context, event }: Ctx) => + setGhostHelperEntities(polarCopies(context, getPointFromEvent(null, event as DrawEvent))), }, - - [ArrayAction.PERFORM_COPY]: ({ context }) => { - const resultEntities: Entity[] = []; - - for (let i = 1; i <= context.numberOfCopies; i++) { - if ( - context.copyMode === 'LINEAR' && - context.startDistanceVector && - context.endDistanceVector - ) { - const distanceVector = new Vector( - context.endDistanceVector.x - context.startDistanceVector.x, - context.endDistanceVector.y - context.startDistanceVector.y - ); - const x = distanceVector.x * i; - const y = distanceVector.y * i; - resultEntities.push( - ...getSelectedEntities().map((entity) => { - const clone = cloneKeepingLook(entity); - clone.move(x, y); - return clone; - }) - ); - } else if (context.copyMode === 'RADIAL' && context.pivotPoint && context.angleStep) { - const angleRad = context.angleStep * i * TO_RADIANS; - resultEntities.push( - ...getSelectedEntities().map((entity) => { - const clone = cloneKeepingLook(entity); - clone.rotate(context.pivotPoint as Point, angleRad); - return clone; - }) - ); - } - } - - addEntities(resultEntities, true); - setGhostHelperEntities([]); - setSelectedEntityIds([]); - }, - [CopyAction.DESELECT_ENTITIES]: assign(() => { - setGhostHelperEntities([]); - setSelectedEntityIds([]); - return initialArrayContext; - }), + ...Object.fromEntries( + ['MOUSE_CLICK', 'ABSOLUTE_POINT_INPUT'].map((type) => [ + type, + { + actions: assign(({ event }: Ctx) => ({ + center: getPointFromEvent(null, event as PointInputEvent) as Point, + })), + target: S.EDIT_POLAR, + }, + ]) + ), + ...esc, }, - } -); + }, + [S.EDIT_RECTANGULAR]: { + meta: { kind: 'keyword', prompt: ARRAY_PROMPT.editRect }, + entry: [() => setShouldDrawHelpers(false), assign({ flow: null }), showRect], + on: { + 'OPTION.COUNT': { + actions: assign({ flow: 'COUNT' as Flow }), + target: S.WAITING_FOR_COLUMNS, + }, + 'OPTION.SPACING': { + actions: assign({ flow: 'SPACING' as Flow }), + target: S.WAITING_FOR_COLUMN_SPACING, + }, + 'OPTION.COLUMNS': { + actions: assign({ flow: 'COLUMNS' as Flow }), + target: S.WAITING_FOR_COLUMNS, + }, + 'OPTION.ROWS': { actions: assign({ flow: 'ROWS' as Flow }), target: S.WAITING_FOR_ROWS }, + 'OPTION.EXIT': create(rectangularCopies), + ...esc, + }, + }, + [S.WAITING_FOR_COLUMNS]: numberStep( + ARRAY_PROMPT.columns, + (columns) => { + remember('ARRAY.COLS', columns); + return { columns }; + }, + [{ flow: 'COUNT', target: S.WAITING_FOR_ROWS }, { target: S.WAITING_FOR_COLUMN_SPACING }] + ), + [S.WAITING_FOR_ROWS]: numberStep( + ARRAY_PROMPT.rows, + (rows) => { + remember('ARRAY.ROWS', rows); + return { rows }; + }, + [{ flow: 'ROWS', target: S.WAITING_FOR_ROW_SPACING }, { target: S.EDIT_RECTANGULAR }] + ), + [S.WAITING_FOR_COLUMN_SPACING]: numberStep( + ARRAY_PROMPT.columnSpacing, + (columnSpacing) => ({ columnSpacing }), + [{ flow: 'SPACING', target: S.WAITING_FOR_ROW_SPACING }, { target: S.EDIT_RECTANGULAR }] + ), + [S.WAITING_FOR_ROW_SPACING]: numberStep( + ARRAY_PROMPT.rowSpacing, + (rowSpacing) => ({ rowSpacing }), + [{ target: S.EDIT_RECTANGULAR }] + ), + [S.EDIT_POLAR]: { + meta: { kind: 'keyword', prompt: ARRAY_PROMPT.editPolar }, + entry: [() => setShouldDrawHelpers(false), showPolar], + on: { + 'OPTION.ITEMS': { target: S.WAITING_FOR_ITEMS }, + 'OPTION.ANGLE': { target: S.WAITING_FOR_ANGLE }, + 'OPTION.FILL': { target: S.WAITING_FOR_FILL }, + 'OPTION.ROTATE': { target: S.WAITING_FOR_ROTATE_ITEMS }, + 'OPTION.EXIT': create(polarCopies), + ...esc, + }, + }, + [S.WAITING_FOR_ITEMS]: numberStep( + ARRAY_PROMPT.items, + (items) => { + remember('ARRAY.ITEMS', items); + return { items }; + }, + [{ target: S.EDIT_POLAR }] + ), + // 사이 각 → 채울 각 = 사이 각 × (항목 − 1)(같은 자리) + [S.WAITING_FOR_ANGLE]: numberStep( + ARRAY_PROMPT.angle, + (angle, context) => { + const fill = angle * Math.max(context.items - 1, 1); + return fillOk(fill) ? { fill } : null; + }, + [{ target: S.EDIT_POLAR }] + ), + [S.WAITING_FOR_FILL]: numberStep( + ARRAY_PROMPT.fill, + (fill) => { + if (!fillOk(fill)) return null; + remember('ARRAY.FILL', fill); + return { fill }; + }, + [{ target: S.EDIT_POLAR }] + ), + [S.WAITING_FOR_ROTATE_ITEMS]: { + meta: { kind: 'keyword', prompt: ARRAY_PROMPT.rotateItems }, + on: { + 'OPTION.YES': { actions: assign({ rotateItems: true }), target: S.EDIT_POLAR }, + 'OPTION.NO': { actions: assign({ rotateItems: false }), target: S.EDIT_POLAR }, + ...esc, + }, + }, + [END_STATE]: endState, +}; + +export const arrayToolStateMachine = createMachine({ + types: {} as { context: ArrayContext; events: StateEvent }, + context: { + originals: [], + columns: 4, + rows: 3, + columnSpacing: 1, + rowSpacing: 1, + center: null, + items: 6, + fill: 360, + rotateItems: true, + flow: null, + type: Tool.ARRAY, + }, + initial: S.INIT, + states, +}); diff --git a/B07_DesignDetail/openwebcad/src/tools/copy-tool.ts b/B07_DesignDetail/openwebcad/src/tools/copy-tool.ts index 2a24a8503..28cb0cb7f 100644 --- a/B07_DesignDetail/openwebcad/src/tools/copy-tool.ts +++ b/B07_DesignDetail/openwebcad/src/tools/copy-tool.ts @@ -1,304 +1,321 @@ -import type { Point } from '@flatten-js/core'; +/** + * COPY(337 라 · 설계 1-3) — 「객체 선택:」 → 「기준점 지정 또는 [변위(D)/모드(O)/다중(M)] <변위>:」 → + * 「두 번째 점 지정 또는 [배열(A)] <첫 번째 점을 변위로 사용>:」 → 첫 사본 뒤 + * 「두 번째 점 지정 또는 [배열(A)/종료(E)/명령 취소(U)] <종료>:」 되풀이(빈 Enter · Esc = 끝 · 만든 사본 남음). + * O → 「복사 모드 옵션 입력 [단일(S)/다중(M)] <다중>:」(COPYMODE 기억) · M = 이번만 되풀이(단일 때만 보임) · + * A → 「배열할 항목 수 입력:」 → 「두 번째 점 지정 또는 [맞춤(F)]:」 · U = 마지막 사본 되돌림. + */ +import { Point } from '@flatten-js/core'; +import { assign, createMachine } from 'xstate'; +import { OPT, type OptionSpec, type PromptSpec, recall, remember } from '../commands/prompt'; +import type { Entity } from '../entities/Entity'; +import { getPointFromEvent } from '../helpers/get-point-from-event'; import { addEntities, - getActiveLayerId, - getSelectedEntities, - getSelectedEntityIds, setAngleGuideOriginPoint, setGhostHelperEntities, - setSelectedEntityIds, setShouldDrawHelpers, + undo, } from '../state'; import { Tool } from '../tools'; -import type { DrawEvent, PointInputEvent, StateEvent, ToolContext } from './tool.types'; -import { getPointFromEvent } from '../helpers/get-point-from-event'; -import { assign, createMachine, sendTo } from 'xstate'; -import { selectToolStateMachine } from './select-tool'; -import type { Entity } from '../entities/Entity'; -import { compact } from 'es-toolkit'; -import { LineEntity } from '../entities/LineEntity'; -import { GUIDE_LINE_COLOR, GUIDE_LINE_STYLE, GUIDE_LINE_WIDTH } from '../App.consts'; -import { cloneKeepingLook, moveEntities } from './move-tool.helpers'; - -export interface CopyContext extends ToolContext { - startPoint: Point | null; - originalSelectedEntities: Entity[]; - copiedEntities: Entity[]; - lastDrawLocation: Point | null; -} +import { cloneKeepingLook } from './move-tool.helpers'; +import { + type DrawEvent, + END_STATE, + type NumberInputEvent, + type PointInputEvent, + type StateEvent, + type ToolContext, +} from './tool.types'; +import { + type PickedContext, + endState, + guideLine, + selectionState, + startState, +} from './transform.helpers'; export enum CopyState { INIT = 'INIT', - CHECK_SELECTION = 'CHECK_SELECTION', WAITING_FOR_SELECTION = 'WAITING_FOR_SELECTION', - WAITING_FOR_START_COPY_POINT = 'WAITING_FOR_START_COPY_POINT', - WAITING_FOR_END_COPY_POINT = 'WAITING_FOR_END_COPY_POINT', + WAITING_FOR_BASE_POINT = 'WAITING_FOR_BASE_POINT', + WAITING_FOR_COPY_MODE = 'WAITING_FOR_COPY_MODE', + WAITING_FOR_SECOND_POINT = 'WAITING_FOR_SECOND_POINT', + WAITING_FOR_ARRAY_COUNT = 'WAITING_FOR_ARRAY_COUNT', + WAITING_FOR_ARRAY_POINT = 'WAITING_FOR_ARRAY_POINT', + WAITING_FOR_DISPLACEMENT = 'WAITING_FOR_DISPLACEMENT', } -export enum CopyAction { - INIT_COPY_TOOL = 'INIT_COPY_TOOL', - ENABLE_HELPERS = 'ENABLE_HELPERS', - RECORD_START_POINT = 'RECORD_START_POINT', - COPY_SELECTION_BEFORE_COPY = 'COPY_SELECTION_BEFORE_COPY', - DRAW_TEMP_COPY_ENTITIES = 'DRAW_TEMP_COPY_ENTITIES', - COPY_SELECTION = 'COPY_SELECTION', - DESELECT_ENTITIES = 'DESELECT_ENTITIES', - RESTORE_ORIGINAL_ENTITIES = 'RESTORE_ORIGINAL_ENTITIES', +export interface CopyContext extends ToolContext, PickedContext { + base: Point | null; + /** COPYMODE 단일 */ + single: boolean; + /** 다중(M) — 이번만 되풀이 */ + multiple: boolean; + /** 만든 사본 묶음 수(U 되돌림 · 물음 바뀜) */ + made: number; + /** 배열(A) 항목 수(원본 포함) */ + items: number; + /** 맞춤(F) — 두 번째 점 = 마지막 항목 */ + fit: boolean; } -/** - * Copy tool state machine - * This state machine is responsible for copying entities around the canvas - * It uses the select tool state machine to select entities to copy - * When the user presses enter, the selected entities are marked for copying - * The user can then select a start point - * When the user moves their mouse, the selected entities will be updated to move according to the vector: startpoint => mouse location - * When the user clicks again, the end point is selected and the entities are copied to the new location - */ -export const copyToolStateMachine = createMachine( +const MODE: OptionSpec = { key: 'MODE', kw: 'O', en: 'MODE', label: '모드' }; +const SINGLE: OptionSpec = { key: 'SINGLE', kw: 'S', en: 'SINGLE', label: '단일' }; +const ARRAY: OptionSpec = { key: 'ARRAY', kw: 'A', en: 'ARRAY', label: '배열' }; +const FIT: OptionSpec = { key: 'FIT', kw: 'F', en: 'FIT', label: '맞춤' }; + +/** 한 번만 만들고 끝 — 단일 모드이고 다중(M) 안 고름 */ +const once = (context: Partial) => !!context.single && !context.multiple; + +const PROMPT = { + base: (context: Partial): PromptSpec => ({ + text: '기준점 지정', + options: [OPT.DISPLACEMENT, MODE, ...(once(context) ? [OPT.MULTIPLE] : [])], + default: { kind: 'option', key: OPT.DISPLACEMENT.key }, + }), + mode: (context: Partial): PromptSpec => ({ + text: '복사 모드 옵션 입력', + options: [SINGLE, OPT.MULTIPLE], + default: { kind: 'option', key: context.single ? SINGLE.key : OPT.MULTIPLE.key }, + }), + second: (context: Partial): PromptSpec => + context.made + ? { + text: '두 번째 점 지정', + options: [ARRAY, OPT.EXIT, OPT.UNDO], + default: { kind: 'enter', label: '종료' }, + } + : { + text: '두 번째 점 지정', + options: [ARRAY], + default: { kind: 'enter', label: '첫 번째 점을 변위로 사용' }, + }, + count: { + text: '배열할 항목 수 입력', + number: { int: true, min: 2, max: 32767 }, + } satisfies PromptSpec, + arrayPoint: (context: Partial): PromptSpec => ({ + text: '두 번째 점 지정', + ...(context.fit ? {} : { options: [FIT] }), + }), + displacement: { + text: '변위 지정', + default: { kind: 'value', remember: 'COPY.DISP', initial: { x: 0, y: 0 } }, + } satisfies PromptSpec, +}; + +type Ctx = { context: CopyContext; event: StateEvent }; + +/** 변위 (dx, dy) 를 1 · 2 · … 배 한 사본들 — 배열(A)이면 항목 수 − 1 벌 */ +function copiesAt(context: CopyContext, dx: number, dy: number, sets: number): Entity[] { + const copies: Entity[] = []; + for (let index = 1; index <= sets; index++) { + for (const entity of context.originals) { + const copy = cloneKeepingLook(entity); + copy.move(dx * index, dy * index); + copies.push(copy); + } + } + return copies; +} + +/** 배열(A) — 맞춤이면 두 번째 점이 마지막 항목 */ +function arrayCopies(context: CopyContext, dx: number, dy: number): Entity[] { + const sets = context.items - 1; + const scale = context.fit ? 1 / sets : 1; + return copiesAt(context, dx * scale, dy * scale, sets); +} + +const vectorTo = ({ context, event }: Ctx) => { + const base = context.base as Point; + const dest = getPointFromEvent(base, event as PointInputEvent); + return new Point(dest.x - base.x, dest.y - base.y); +}; + +const place = (copies: Entity[]) => { + if (copies.length) addEntities(copies, true); + setGhostHelperEntities([]); +}; +const bump = assign(({ context }: Ctx) => ({ made: context.made + 1, fit: false })); + +const onPoints = (handler: unknown) => ({ + MOUSE_CLICK: handler, + ABSOLUTE_POINT_INPUT: handler, + RELATIVE_POINT_INPUT: handler, + NUMBER_INPUT: handler, +}); +const esc = { ESC: { target: END_STATE } }; + +/** 사본 놓음 → 단일이면 끝 · 아니면 다시 두 번째 점 */ +const placeThen = (copies: (args: Ctx) => Entity[]) => [ { - types: {} as { - context: CopyContext; - events: StateEvent; - }, - context: { - startPoint: null, - originalSelectedEntities: [], - copiedEntities: [], - lastDrawLocation: null, - type: Tool.COPY, - }, - initial: CopyState.INIT, - states: { - [CopyState.INIT]: { - description: 'Initializing the copy tool', - always: { - actions: CopyAction.INIT_COPY_TOOL, - target: CopyState.CHECK_SELECTION, - }, - }, - [CopyState.CHECK_SELECTION]: { - description: 'Check if there is something selected', - always: [ - { - guard: () => { - return getSelectedEntityIds().length > 0; - }, - target: CopyState.WAITING_FOR_START_COPY_POINT, - }, - { - guard: () => { - return getSelectedEntityIds().length === 0; - }, - target: CopyState.WAITING_FOR_SELECTION, - }, - ], - }, - [CopyState.WAITING_FOR_SELECTION]: { - description: 'Select what you want to copy', - meta: { - instructions: '복사할 객체 선택 뒤 Enter', - }, - invoke: { - id: 'selectToolInsideTheCopyTool', - src: selectToolStateMachine, - onDone: { - actions: assign(() => { - return { - startPoint: null, - }; - }), - target: CopyState.CHECK_SELECTION, - }, - }, - on: { - MOUSE_CLICK: { - // Forward the event to the select tool - actions: sendTo('selectToolInsideTheCopyTool', ({ event }) => { - return event; - }), - }, - ESC: { - actions: [CopyAction.DESELECT_ENTITIES, CopyAction.INIT_COPY_TOOL], - }, - ENTER: { - // Forward the event to the select tool - actions: sendTo('selectToolInsideTheCopyTool', ({ event }) => { - return event; - }), - }, - DRAW: { - // Forward the event to the select tool - actions: sendTo('selectToolInsideTheCopyTool', ({ event }) => { - return event; - }), - }, - }, - }, - [CopyState.WAITING_FOR_START_COPY_POINT]: { - description: '기준점 지정 (x,y):', - meta: { - instructions: '기준점 지정 (x,y):', - }, - always: { - actions: CopyAction.ENABLE_HELPERS, - }, - on: { - MOUSE_CLICK: { - actions: [CopyAction.RECORD_START_POINT, CopyAction.COPY_SELECTION_BEFORE_COPY], - target: CopyState.WAITING_FOR_END_COPY_POINT, - }, - ABSOLUTE_POINT_INPUT: { - actions: [CopyAction.RECORD_START_POINT, CopyAction.COPY_SELECTION_BEFORE_COPY], - target: CopyState.WAITING_FOR_END_COPY_POINT, - }, - ESC: { - actions: CopyAction.DESELECT_ENTITIES, - target: CopyState.INIT, - }, - }, - }, - [CopyState.WAITING_FOR_END_COPY_POINT]: { - description: '둘째 점 지정 (길이 · <각도 · @x,y):', - meta: { - instructions: '둘째 점 지정 (길이 · <각도 · @x,y):', - }, - on: { - DRAW: { - actions: [CopyAction.DRAW_TEMP_COPY_ENTITIES], - }, - MOUSE_CLICK: { - actions: [CopyAction.COPY_SELECTION], - target: CopyState.WAITING_FOR_END_COPY_POINT, - }, - NUMBER_INPUT: { - actions: [CopyAction.COPY_SELECTION], - target: CopyState.WAITING_FOR_END_COPY_POINT, - }, - ABSOLUTE_POINT_INPUT: { - actions: [CopyAction.COPY_SELECTION], - target: CopyState.WAITING_FOR_END_COPY_POINT, - }, - RELATIVE_POINT_INPUT: { - actions: [CopyAction.COPY_SELECTION], - target: CopyState.WAITING_FOR_END_COPY_POINT, - }, - ESC: { - actions: CopyAction.DESELECT_ENTITIES, - target: CopyState.INIT, - }, - }, - }, - }, + guard: ({ context }: Ctx) => once(context), + actions: (args: Ctx) => place(copies(args)), + target: END_STATE, }, { - actions: { - [CopyAction.INIT_COPY_TOOL]: assign(() => { - setShouldDrawHelpers(false); - setGhostHelperEntities([]); - setAngleGuideOriginPoint(null); - return { - startPoint: null, - originalSelectedEntities: [], - copiedEntities: [], - lastDrawLocation: null, - }; - }), - [CopyAction.ENABLE_HELPERS]: () => { - setShouldDrawHelpers(true); - }, - [CopyAction.RECORD_START_POINT]: assign(({ event }) => { - const startPoint = getPointFromEvent(null, event as PointInputEvent); - setAngleGuideOriginPoint(startPoint); - return { startPoint }; - }), - [CopyAction.COPY_SELECTION_BEFORE_COPY]: assign(({ context }) => { - const selectedEntities = getSelectedEntities(); + actions: [(args: Ctx) => place(copies(args)), bump], + target: CopyState.WAITING_FOR_SECOND_POINT, + }, +]; - // Copy the selected entities to the ghost helper entities, so they are drawn on the canvas, but do not interact with the snap points / angle guides - setGhostHelperEntities(selectedEntities); +const recordBase = { + actions: assign(({ event }: Ctx) => { + const base = getPointFromEvent(null, event as PointInputEvent); + setAngleGuideOriginPoint(base); + return { base }; + }), + target: CopyState.WAITING_FOR_SECOND_POINT, +}; - // TODO keep a copy of the original entities in the entities list, but set their line color to grey, so the user can see where the entities were before being copied and the original entities also are used for snap points / angle guides +const S = CopyState; - setSelectedEntityIds([]); - return { - startPoint: context.startPoint, - // Make a copy of the selected entities before copying them, so we can restore them when the user cancels the copy action - originalSelectedEntities: compact( - selectedEntities.map((entity) => cloneKeepingLook(entity)) - ), - copiedEntities: selectedEntities, - }; - }), - [CopyAction.DRAW_TEMP_COPY_ENTITIES]: ({ context, event }) => { - if (!context.startPoint) { - throw new Error('[COPY] Calling draw temp copy line without a start point'); - } - - const endPointTemp = getPointFromEvent(context.startPoint, event as DrawEvent); - - // Copy the entities to the new location - // Draw all selected entities according to translation vector, so the user gets visual feedback of where the entities will be copied; - const movedEntities = context.originalSelectedEntities.map((entity) => - cloneKeepingLook(entity) - ); - moveEntities( - movedEntities, - endPointTemp.x - context.startPoint.x, - endPointTemp.y - context.startPoint.y - ); - - // // Draw a dashed line between the start copy point and the current mouse location - const activeCopyLine = new LineEntity( - getActiveLayerId(), - context.startPoint as Point, - endPointTemp - ); - activeCopyLine.lineColor = GUIDE_LINE_COLOR; - activeCopyLine.lineWidth = GUIDE_LINE_WIDTH; - activeCopyLine.lineDash = GUIDE_LINE_STYLE; - setGhostHelperEntities([activeCopyLine, ...movedEntities]); - }, - [CopyAction.COPY_SELECTION]: ({ context, event }) => { - if (!context.startPoint) { - throw new Error('[COPY] Calling copy selection without a start point'); - } - - // Copy the entities one final time - // 클릭 · 거리 · 절대 · 상대 좌표(키보드) - const currentEndPoint = getPointFromEvent(context.startPoint, event as PointInputEvent); - const copiedEntities = context.originalSelectedEntities.map((entity) => - cloneKeepingLook(entity) - ); - moveEntities( - copiedEntities, - currentEndPoint.x - context.startPoint.x, - currentEndPoint.y - context.startPoint.y - ); - - // 원본은 목록에 그대로 있음 — 복사본만 더함(원본까지 더하면 누를 때마다 원본이 겹겹이 쌓임). - addEntities(copiedEntities, true); - }, - [CopyAction.DESELECT_ENTITIES]: assign(() => { - setGhostHelperEntities([]); - setSelectedEntityIds([]); - return { - startPoint: null, - originalSelectedEntities: [], - lastDrawLocation: null, - }; - }), - [CopyAction.RESTORE_ORIGINAL_ENTITIES]: assign(({ context }) => { - addEntities(context.originalSelectedEntities, false); // This should already be the last state of the undo stack - setGhostHelperEntities([]); - setSelectedEntityIds([]); - return { - startPoint: null, - originalSelectedEntities: [], - lastDrawLocation: null, - }; - }), - ...selectToolStateMachine.implementations.actions, +// biome-ignore lint/suspicious/noExplicitAny: 공용 상태 조각(여러 도구 공통)을 섞어 만든다 +const states: Record = { + [S.INIT]: startState(S.WAITING_FOR_BASE_POINT), + [S.WAITING_FOR_SELECTION]: selectionState(S.WAITING_FOR_BASE_POINT), + [S.WAITING_FOR_BASE_POINT]: { + meta: { kind: 'point', prompt: PROMPT.base }, + entry: () => setShouldDrawHelpers(true), + on: { + MOUSE_CLICK: recordBase, + ABSOLUTE_POINT_INPUT: recordBase, + 'OPTION.DISPLACEMENT': { target: S.WAITING_FOR_DISPLACEMENT }, + 'OPTION.MODE': { target: S.WAITING_FOR_COPY_MODE }, + 'OPTION.MULTIPLE': { actions: assign({ multiple: true }) }, + ...esc, }, - } -); + }, + [S.WAITING_FOR_COPY_MODE]: { + meta: { kind: 'keyword', prompt: PROMPT.mode }, + on: { + 'OPTION.SINGLE': { + actions: [assign({ single: true }), () => remember('COPYMODE', SINGLE.key)], + target: S.WAITING_FOR_BASE_POINT, + }, + 'OPTION.MULTIPLE': { + actions: [assign({ single: false }), () => remember('COPYMODE', OPT.MULTIPLE.key)], + target: S.WAITING_FOR_BASE_POINT, + }, + ...esc, + }, + }, + [S.WAITING_FOR_SECOND_POINT]: { + meta: { kind: 'point', prompt: PROMPT.second }, + on: { + DRAW: { + actions: ({ context, event }: Ctx) => { + const base = context.base as Point; + const dest = getPointFromEvent(base, event as DrawEvent); + setGhostHelperEntities([ + guideLine(base, dest), + ...copiesAt(context, dest.x - base.x, dest.y - base.y, 1), + ]); + }, + }, + ...onPoints( + placeThen((args) => { + const vector = vectorTo(args); + return copiesAt(args.context, vector.x, vector.y, 1); + }) + ), + // 첫 물음 빈 Enter = 기준점 좌표가 변위(한 벌 뒤 끝) · 그 뒤 = <종료> + ENTER: [ + { + guard: ({ context }: Ctx) => context.made === 0, + actions: ({ context }: Ctx) => { + const base = context.base as Point; + place(copiesAt(context, base.x, base.y, 1)); + }, + target: END_STATE, + }, + { target: END_STATE }, + ], + 'OPTION.ARRAY': { target: S.WAITING_FOR_ARRAY_COUNT }, + 'OPTION.EXIT': { target: END_STATE }, + 'OPTION.UNDO': { + guard: ({ context }: Ctx) => context.made > 0, + actions: [ + () => { + undo(); + setGhostHelperEntities([]); + }, + assign(({ context }: Ctx) => ({ made: context.made - 1 })), + ], + }, + ...esc, + }, + }, + [S.WAITING_FOR_ARRAY_COUNT]: { + meta: { kind: 'number', prompt: PROMPT.count }, + entry: () => setGhostHelperEntities([]), + on: { + NUMBER_INPUT: { + actions: assign(({ event }: Ctx) => ({ items: (event as NumberInputEvent).value })), + target: S.WAITING_FOR_ARRAY_POINT, + }, + ...esc, + }, + }, + [S.WAITING_FOR_ARRAY_POINT]: { + meta: { kind: 'point', prompt: PROMPT.arrayPoint }, + on: { + DRAW: { + actions: ({ context, event }: Ctx) => { + const base = context.base as Point; + const dest = getPointFromEvent(base, event as DrawEvent); + setGhostHelperEntities([ + guideLine(base, dest), + ...arrayCopies(context, dest.x - base.x, dest.y - base.y), + ]); + }, + }, + ...onPoints( + placeThen((args) => { + const vector = vectorTo(args); + return arrayCopies(args.context, vector.x, vector.y); + }) + ), + 'OPTION.FIT': { actions: assign({ fit: true }) }, + ...esc, + }, + }, + [S.WAITING_FOR_DISPLACEMENT]: { + meta: { kind: 'point', prompt: PROMPT.displacement }, + entry: () => setAngleGuideOriginPoint(null), + on: { + ...Object.fromEntries( + ['MOUSE_CLICK', 'ABSOLUTE_POINT_INPUT'].map((type) => [ + type, + { + actions: ({ context, event }: Ctx) => { + const vector = getPointFromEvent(null, event as PointInputEvent); + remember('COPY.DISP', { x: vector.x, y: vector.y }); + place(copiesAt(context, vector.x, vector.y, 1)); + }, + target: END_STATE, + }, + ]) + ), + ...esc, + }, + }, + [END_STATE]: endState, +}; + +export const copyToolStateMachine = createMachine({ + types: {} as { context: CopyContext; events: StateEvent }, + context: () => ({ + originals: [], + base: null, + single: recall('COPYMODE') === SINGLE.key, + multiple: false, + made: 0, + items: 2, + fit: false, + type: Tool.COPY, + }), + initial: S.INIT, + states, +}); diff --git a/B07_DesignDetail/openwebcad/src/tools/modify/transform-tools.ts b/B07_DesignDetail/openwebcad/src/tools/modify/transform-tools.ts index 01475b9de..21e573119 100644 --- a/B07_DesignDetail/openwebcad/src/tools/modify/transform-tools.ts +++ b/B07_DesignDetail/openwebcad/src/tools/modify/transform-tools.ts @@ -1,37 +1,80 @@ /** 대칭·정렬·간격띄우기·신축·길이조정 (조사표 2절) */ -import { Point } from '@flatten-js/core'; +import { Box, Point, Polygon } from '@flatten-js/core'; import { toast } from 'react-toastify'; +import { type AnyActorRef, assign } from 'xstate'; +import { OPT } from '../../commands/prompt'; import type { Entity } from '../../entities/Entity'; import { LineEntity } from '../../entities/LineEntity'; -import { addEntities, deleteEntities, getActiveLayerId, setSelectedEntityIds } from '../../state'; +import { PolyLineEntity } from '../../entities/PolyLineEntity'; +import { RectangleEntity } from '../../entities/RectangleEntity'; +import { + addEntities, + deleteEntities, + getActiveLayerId, + getEntities, + setEntities, + setSelectedEntityIds, +} from '../../state'; import { Tool } from '../../tools'; import { createSequenceTool } from '../factories/sequence-tool'; +import { createDisplaceTool } from '../move-tool'; +import { SelectState } from '../select-tool'; +import type { MouseClickEvent, StateEvent } from '../tool.types'; +import { SELECT_CHILD } from '../transform.helpers'; import { copyStyle, lengthenLine, offsetEntity } from './modify.helpers'; /** 원본 특성을 유지한 복사본 */ const cloneStyled = (entity: Entity): Entity => copyStyle(entity, entity.clone()); +/** 대칭선 — 두 점이 같으면 없음 */ +const mirrorAxis = (first: Point, second: Point): LineEntity | null => + first.distanceTo(second)[0] < 1e-9 ? null : new LineEntity(getActiveLayerId(), first, second); + +const mirrored = (originals: Entity[], axis: LineEntity): Entity[] => + originals.map((entity) => { + const copy = cloneStyled(entity); + copy.mirror(axis); + return copy; + }); + +/** + * MIRROR(337 라 · 설계 1-3) — 「객체 선택:」 → 「대칭선의 첫 번째 점 지정:」 → 「대칭선의 두 번째 점 지정:」 → + * 「원본 객체를 지우시겠습니까? [예(Y)/아니오(N)] <아니오>:」 · Esc = 안 만듦 · 한 번 뒤 끝. + */ export const mirrorToolStateMachine = createSequenceTool({ tool: Tool.MIRROR, steps: [ - { kind: 'selection', instructions: '대칭 복사할 객체를 선택한 뒤 ENTER.' }, - { kind: 'point', instructions: '대칭축의 첫 점을 지정하십시오.' }, - { kind: 'point', instructions: '대칭축의 두 번째 점을 지정하십시오.' }, + { kind: 'selection' }, + { kind: 'point', id: 'first', prompt: { text: '대칭선의 첫 번째 점 지정' } }, + { kind: 'point', id: 'second', prompt: { text: '대칭선의 두 번째 점 지정' } }, + { + kind: 'keyword', + id: 'erase', + prompt: { + text: '원본 객체를 지우시겠습니까?', + options: [OPT.YES, OPT.NO], + default: { kind: 'option', key: OPT.NO.key }, + }, + }, ], preview: (input) => { - const points = input.points(); - if (points.length !== 1) return []; - return [new LineEntity(getActiveLayerId(), points[0], input.cursor)]; + const [first, second] = input.points(); + const axis = first ? mirrorAxis(first, second ?? input.cursor) : null; + if (!axis) return []; + return [...(second ? [] : [axis]), ...mirrored(input.entities(0), axis)]; }, commit: (input) => { const [first, second] = input.points(); - const axis = new LineEntity(getActiveLayerId(), first, second); - const mirrored = input.entities(0).map((entity) => { - const copy = cloneStyled(entity); - copy.mirror(axis); - return copy; - }); - if (mirrored.length) addEntities(mirrored, true); + const axis = mirrorAxis(first, second); + const originals = input.entities(0); + if (!axis || !originals.length) return; + const copies = mirrored(originals, axis); + if (input.get('erase') === OPT.YES.key) { + const gone = new Set(originals.map((entity) => entity.id)); + setEntities([...getEntities().filter((entity) => !gone.has(entity.id)), ...copies], true); + } else { + addEntities(copies, true); + } setSelectedEntityIds([]); }, }); @@ -84,44 +127,85 @@ export const offsetToolStateMachine = createSequenceTool({ }, }); -export const stretchToolStateMachine = createSequenceTool({ - tool: Tool.STRETCH, - steps: [ - { kind: 'selection', instructions: '신축할 객체를 선택한 뒤 ENTER.' }, - { kind: 'point', instructions: '기준점을 지정하십시오.' }, - { kind: 'point', instructions: '이동할 위치를 지정하십시오.' }, - ], - commit: (input) => { - const [base, destination] = input.points(); - const dx = destination.x - base.x; - const dy = destination.y - base.y; - const replaced: Entity[] = []; - const removed: Entity[] = []; +/** 바꾼 것을 원본 자리에 — id · 묶음 · 꼴 그대로 */ +const inPlaceOf = (source: Entity, made: T): T => { + copyStyle(source, made); + made.id = source.id; + made.groupId = source.groupId; + made.opacity = source.opacity; + return made; +}; - for (const entity of input.entities(0)) { - const shape = entity.getShape(); - // 선은 기준점에 가까운 끝점만 끌어당긴다. 그 밖의 객체는 통째로 옮긴다. - if (shape && 'start' in shape && 'end' in shape) { - const start = (shape as { start: Point }).start; - const end = (shape as { end: Point }).end; - const moveStart = start.distanceTo(base)[0] < end.distanceTo(base)[0]; - const newLine = new LineEntity( - entity.layerId, - moveStart ? new Point(start.x + dx, start.y + dy) : start, - moveStart ? end : new Point(end.x + dx, end.y + dy) - ); - replaced.push(copyStyle(entity, newLine)); - removed.push(entity); - } else { - const copy = cloneStyled(entity); - copy.move(dx, dy); - replaced.push(copy); - removed.push(entity); - } - } - if (removed.length) deleteEntities(removed, false); - addEntities(replaced, true); - setSelectedEntityIds([]); +/** + * 걸침 창 신축 — 창 안 꼭짓점만 (dx, dy) · 창 안에 통째 든 것 · 하나씩 집은 것(창 밖) = 이동 · 그 밖 = 그대로(null). + * ponytail: 선 · 폴리선(선 토막) · 사각형만 꼭짓점 신축 — 원 · 호 · 글은 걸치면 그대로(오토캐드는 중심 · 끝점 기준). + */ +function stretchEntity(entity: Entity, box: Box | null, dx: number, dy: number): Entity | null { + const inside = (point: Point) => + !!box && + point.x >= box.xmin && + point.x <= box.xmax && + point.y >= box.ymin && + point.y <= box.ymax; + const shift = (point: Point) => (inside(point) ? new Point(point.x + dx, point.y + dy) : point); + if (!box || entity.isContainedInBox(box) || !entity.intersectsWithBox(box)) { + const copy = cloneStyled(entity); + copy.move(dx, dy); + return inPlaceOf(entity, copy); + } + if (entity instanceof LineEntity) { + const start = entity.getStartPoint(); + const end = entity.getEndPoint(); + if (!inside(start) && !inside(end)) return null; + return inPlaceOf(entity, new LineEntity(entity.layerId, shift(start), shift(end))); + } + if (entity instanceof PolyLineEntity) { + let changed = false; + const parts = entity.getEntities().map((part) => { + if (!(part instanceof LineEntity)) return cloneStyled(part); + const start = part.getStartPoint(); + const end = part.getEndPoint(); + if (inside(start) || inside(end)) changed = true; + return copyStyle(part, new LineEntity(part.layerId, shift(start), shift(end))); + }); + return changed ? inPlaceOf(entity, new PolyLineEntity(entity.layerId, parts)) : null; + } + if (entity instanceof RectangleEntity) { + const corners = (entity.getShape() as Polygon).vertices; + if (!corners.some(inside)) return null; + return inPlaceOf(entity, new RectangleEntity(entity.layerId, new Polygon(corners.map(shift)))); + } + return null; +} + +/** 고르기 중 마지막 걸침 창 — 선택 도구가 두 번째 구석을 기다릴 때의 누름 */ +const watchWindow = assign(({ event, self }: { event: StateEvent; self: AnyActorRef }) => { + const snapshot = self.getSnapshot().children[SELECT_CHILD]?.getSnapshot(); + const start = snapshot?.context?.startPoint as Point | null | undefined; + if (snapshot?.value !== SelectState.WAITING_FOR_SECOND_SELECT_POINT || !start) return {}; + return { window: [start, (event as MouseClickEvent).worldMouseLocation] as [Point, Point] }; +}); + +/** + * STRETCH(337 라 · 설계 1-3) — 기록 「걸치기 윈도우 또는 걸치기 폴리곤만큼 신축할 객체 선택...」 → 「객체 선택:」 → + * MOVE 와 같은 기준점 [변위(D)] · 두 번째 점 · 변위 기억(STRETCH.DISP) · 걸침 창 안 꼭짓점만 옮김. + */ +export const stretchToolStateMachine = createDisplaceTool({ + tool: Tool.STRETCH, + remember: 'STRETCH.DISP', + intro: '걸치기 윈도우 또는 걸치기 폴리곤만큼 신축할 객체 선택...', + watch: watchWindow, + change: (context, dx, dy) => { + const window = context.window; + const box = window + ? new Box( + Math.min(window[0].x, window[1].x), + Math.min(window[0].y, window[1].y), + Math.max(window[0].x, window[1].x), + Math.max(window[0].y, window[1].y) + ) + : null; + return context.originals.flatMap((entity) => stretchEntity(entity, box, dx, dy) ?? []); }, }); diff --git a/B07_DesignDetail/openwebcad/src/tools/pedit-tool.ts b/B07_DesignDetail/openwebcad/src/tools/pedit-tool.ts index 83c7f330b..ffc5ccb68 100644 --- a/B07_DesignDetail/openwebcad/src/tools/pedit-tool.ts +++ b/B07_DesignDetail/openwebcad/src/tools/pedit-tool.ts @@ -1,138 +1,48 @@ +/** + * PEDIT — 고른 선 · 호를 폴리선 하나로 묶음. 고르기 물음 = 오토캐드 「객체 선택:」(337 라 · 설계 2-14 ④) · + * 고른 것 있으면 바로 · 빈 고르기 Enter · Esc = 끝 · 묶은 뒤 끝(쉬는 상태). + */ import { toast } from 'react-toastify'; -import { assign, createMachine, sendTo } from 'xstate'; +import { createMachine } from 'xstate'; import { PolyLineEntity } from '../entities/PolyLineEntity.ts'; import { getActiveLayerId, getNotSelectedEntities, getSelectedEntities, - getSelectedEntityIds, - setAngleGuideOriginPoint, setEntities, - setGhostHelperEntities, - setSelectedEntityIds, - setShouldDrawHelpers, } from '../state'; import { Tool } from '../tools'; -import { selectToolStateMachine } from './select-tool'; -import type { StateEvent, ToolContext } from './tool.types'; +import { END_STATE, type StateEvent, type ToolContext } from './tool.types'; +import { endState, selectionState, startState } from './transform.helpers'; export interface PeditContext extends ToolContext {} export enum PeditState { INIT = 'INIT', - CHECK_SELECTION = 'CHECK_SELECTION', WAITING_FOR_SELECTION = 'WAITING_FOR_SELECTION', CONVERT_SELECTION_TO_POLYLINE = 'CONVERT_SELECTION_TO_POLYLINE', } -export enum PeditAction { - INIT_PEDIT_TOOL = 'INIT_PEDIT_TOOL', - DESELECT_ENTITIES = 'DESELECT_ENTITIES', - CONVERT_SELECTION_TO_POLYLINE = 'CONVERT_SELECTION_TO_POLYLINE', -} +const convert = () => { + const newPolyLine = new PolyLineEntity(getActiveLayerId(), getSelectedEntities()); + setEntities([...getNotSelectedEntities(), newPolyLine], true); + toast.success(`Created polyline with ${newPolyLine.numberOfSegments()} segments`); +}; -/** - * Pedit tool state machine - * This state machine is responsible for combining entities into a polyline - * It uses the select tool state machine to select entities to combine - * When the user presses enter, the selected entities are combined into one polyline - */ -export const peditToolStateMachine = createMachine( - { - types: {} as { - context: PeditContext; - events: StateEvent; - }, - context: { - type: Tool.PEDIT, - }, - initial: PeditState.INIT, - states: { - [PeditState.INIT]: { - description: 'Initializing the pedit tool', - always: { - actions: PeditAction.INIT_PEDIT_TOOL, - target: PeditState.CHECK_SELECTION, - }, - }, - [PeditState.CHECK_SELECTION]: { - description: 'Check if there is something selected', - always: [ - { - guard: () => { - return getSelectedEntityIds().length > 0; - }, - target: PeditState.CONVERT_SELECTION_TO_POLYLINE, - }, - { - guard: () => { - return getSelectedEntityIds().length === 0; - }, - target: PeditState.WAITING_FOR_SELECTION, - }, - ], - }, - [PeditState.WAITING_FOR_SELECTION]: { - description: 'Select the entities that you want to combine into a polyline', - meta: { - instructions: '폴리선으로 합칠 객체 선택 뒤 Enter', - }, - invoke: { - id: 'selectToolInsideThePeditTool', - src: selectToolStateMachine, - onDone: { - target: PeditState.CHECK_SELECTION, - }, - }, - on: { - MOUSE_CLICK: { - // Forward the event to the select tool - actions: sendTo('selectToolInsideThePeditTool', ({ event }) => { - return event; - }), - }, - ESC: { - actions: [PeditAction.DESELECT_ENTITIES, PeditAction.INIT_PEDIT_TOOL], - }, - ENTER: { - // Forward the event to the select tool - actions: sendTo('selectToolInsideThePeditTool', ({ event }) => { - return event; - }), - }, - DRAW: { - // Forward the event to the select tool - actions: sendTo('selectToolInsideThePeditTool', ({ event }) => { - return event; - }), - }, - }, - }, - [PeditState.CONVERT_SELECTION_TO_POLYLINE]: { - always: { - actions: PeditAction.CONVERT_SELECTION_TO_POLYLINE, - target: PeditState.INIT, - }, - }, - }, +// biome-ignore lint/suspicious/noExplicitAny: 공용 상태 조각(여러 도구 공통)을 섞어 만든다 +const states: Record = { + [PeditState.INIT]: startState(PeditState.CONVERT_SELECTION_TO_POLYLINE), + [PeditState.WAITING_FOR_SELECTION]: selectionState(PeditState.CONVERT_SELECTION_TO_POLYLINE), + [PeditState.CONVERT_SELECTION_TO_POLYLINE]: { + always: { actions: convert, target: END_STATE }, }, - { - actions: { - [PeditAction.INIT_PEDIT_TOOL]: assign(() => { - setShouldDrawHelpers(false); - setGhostHelperEntities([]); - setAngleGuideOriginPoint(null); - return {}; - }), - [PeditAction.CONVERT_SELECTION_TO_POLYLINE]: assign(() => { - const newPolyLine = new PolyLineEntity(getActiveLayerId(), getSelectedEntities()); - const newEntities = [...getNotSelectedEntities(), newPolyLine]; - setEntities(newEntities, true); - setSelectedEntityIds([]); - toast.success(`Created polyline with ${newPolyLine.numberOfSegments()} segments`); - return {}; - }), - ...selectToolStateMachine.implementations.actions, - }, - } -); + [END_STATE]: endState, +}; + +/** 고른 객체를 폴리선 하나로 */ +export const peditToolStateMachine = createMachine({ + types: {} as { context: PeditContext & { originals: unknown[] }; events: StateEvent }, + context: { originals: [], type: Tool.PEDIT }, + initial: PeditState.INIT, + states, +}); diff --git a/resources/tester/cad/transform-acad.test.ts b/resources/tester/cad/transform-acad.test.ts index 7d31dd1c7..02974f246 100644 --- a/resources/tester/cad/transform-acad.test.ts +++ b/resources/tester/cad/transform-acad.test.ts @@ -13,7 +13,9 @@ vi.mock("react-toastify", () => ({ import { remember } from "../../../B07_DesignDetail/openwebcad/src/commands/prompt"; import { runCommandInput } from "../../../B07_DesignDetail/openwebcad/src/commands/run-command"; import type { Entity } from "../../../B07_DesignDetail/openwebcad/src/entities/Entity"; +import { selectToolStateMachine } from "../../../B07_DesignDetail/openwebcad/src/tools/select-tool"; import { + getGhostHelperEntities, getSelectedEntityIds, setSelectedEntityIds, } from "../../../B07_DesignDetail/openwebcad/src/state"; @@ -316,3 +318,358 @@ describe("SCALE(1-3)", () => { expect(lineEnds()).toEqual([[0, 0, 30, 0]]); }); }); + +describe("COPY(1-3)", () => { + it("물음 차례 · 되풀이 · U 마지막 사본 되돌림 · 빈 Enter = 끝(사본 남음)", () => { + lineSelected(); + type("co"); + expect(promptLine()).toBe("기준점 지정 또는 [변위(D)/모드(O)] <변위>:"); + type("0,0"); + expect(promptLine()).toBe("두 번째 점 지정 또는 [배열(A)] <첫 번째 점을 변위로 사용>:"); + type("0,10"); + expect(promptLine()).toBe("두 번째 점 지정 또는 [배열(A)/종료(E)/명령 취소(U)] <종료>:"); + type("0,20"); + expect(entities("Line")).toHaveLength(3); + type("u"); + expect(entities("Line")).toHaveLength(2); + expect(activeTool()).toBe(Tool.COPY); + enter(); + expect(activeTool()).toBe(Tool.SELECT); + expect(lineEnds()).toEqual([ + [0, 0, 20, 0], + [0, 10, 20, 10], + ]); + }); + + it("첫 물음 빈 Enter = 기준점 좌표가 변위(한 벌 뒤 끝) · E = 끝 · Esc = 끝(사본 남음)", () => { + lineSelected(); + type("co"); + type("5,5"); + enter(); + expect(activeTool()).toBe(Tool.SELECT); + expect(lineEnds()).toEqual([ + [0, 0, 20, 0], + [5, 5, 25, 5], + ]); + setSelectedEntityIds([entities()[0].id]); + type("co"); + type("0,0"); + type("0,-5"); + type("e"); + expect(activeTool()).toBe(Tool.SELECT); + setSelectedEntityIds([entities()[0].id]); + type("co"); + type("0,0"); + type("0,-9"); + esc(); + expect(entities("Line")).toHaveLength(4); + }); + + it("O 단일(COPYMODE 기억) · 단일 땐 M 보임 · M = 이번만 되풀이", () => { + lineSelected(); + type("co"); + type("o"); + expect(promptLine()).toBe("복사 모드 옵션 입력 [단일(S)/다중(M)] <다중>:"); + type("s"); + expect(promptLine()).toBe("기준점 지정 또는 [변위(D)/모드(O)/다중(M)] <변위>:"); + type("0,0"); + type("0,10"); + expect(activeTool()).toBe(Tool.SELECT); // 단일 = 한 벌 뒤 끝 + expect(entities("Line")).toHaveLength(2); + setSelectedEntityIds([entities()[0].id]); + type("co"); + expect(promptLine()).toBe("기준점 지정 또는 [변위(D)/모드(O)/다중(M)] <변위>:"); + type("m"); + type("0,0"); + type("0,20"); + type("0,30"); + expect(activeTool()).toBe(Tool.COPY); + expect(entities("Line")).toHaveLength(4); + }); + + it("A 배열 · F 맞춤 · D 변위", () => { + lineSelected(); + type("co"); + type("0,0"); + type("a"); + expect(promptLine()).toBe("배열할 항목 수 입력:"); + type("1.5"); + expect(noticeLine()).toBe("정수 값을 요구함."); + type("3"); + expect(promptLine()).toBe("두 번째 점 지정 또는 [맞춤(F)]:"); + type("0,10"); + expect(lineEnds().map((ends) => ends[1])).toEqual([0, 10, 20]); + type("a"); + type("3"); + type("f"); + type("0,-10"); // 맞춤 — 마지막 항목이 −10 + expect(lineEnds().map((ends) => ends[1])).toEqual([-10, -5, 0, 10, 20]); + esc(); + setSelectedEntityIds([entities()[0].id]); + type("co"); + type("d"); + expect(promptLine()).toBe("변위 지정 <0.0000, 0.0000>:"); + type("100,0"); + expect(activeTool()).toBe(Tool.SELECT); + expect(entities("Line")).toHaveLength(6); + }); +}); + +describe("MIRROR(1-3)", () => { + it("물음 차례 · 빈 Enter = 아니오(원본 둠) · Y = 원본 지움 · N", () => { + lineSelected("0,10", "20,10"); + type("mi"); + expect(promptLine()).toBe("대칭선의 첫 번째 점 지정:"); + type("0,0"); + expect(promptLine()).toBe("대칭선의 두 번째 점 지정:"); + type("10,0"); + expect(promptLine()).toBe("원본 객체를 지우시겠습니까? [예(Y)/아니오(N)] <아니오>:"); + enter(); + expect(activeTool()).toBe(Tool.SELECT); + expect(lineEnds()).toEqual([ + [0, -10, 20, -10], + [0, 10, 20, 10], + ]); + setSelectedEntityIds([entities()[0].id]); + type("mi"); + type("0,0"); + type("0,10"); + type("y"); + expect(lineEnds()).toEqual([ + [0, -10, 20, -10], + [0, 10, -20, 10], + ]); + setSelectedEntityIds([entities()[0].id]); + type("mi"); + type("0,0"); + type("0,10"); + type("n"); + expect(entities("Line")).toHaveLength(3); + }); + + it("Esc = 안 만듦(예/아니오 물음에서도)", () => { + lineSelected("0,10", "20,10"); + type("mi"); + type("0,0"); + type("10,0"); + esc(); + expect(lineEnds()).toEqual([[0, 10, 20, 10]]); + expect(activeTool()).toBe(Tool.SELECT); + }); +}); + +describe("STRETCH(1-3)", () => { + it("걸침 창 안 꼭짓점만 옮김 · 창 밖 객체 그대로", () => { + line("0,0", "20,0"); + line("100,0", "120,0"); + type("s"); + expect(promptLine()).toBe("객체 선택:"); + click(30, 5); // 걸침(오→왼) + click(15, -5); + enter(); + expect(promptLine()).toBe("기준점 지정 또는 [변위(D)] <변위>:"); + type("0,0"); + type("@10,0"); + expect(lineEnds()).toEqual([ + [0, 0, 30, 0], + [100, 0, 120, 0], + ]); + expect(activeTool()).toBe(Tool.SELECT); + }); + + it("창 안에 통째 든 것 = 이동 · D 변위 · 하나씩 고른 것(창 없음) = 이동", () => { + line("0,0", "20,0"); + type("s"); + click(-5, 5); + click(25, -5); + enter(); + type("d"); + type("0,7"); + expect(lineEnds()).toEqual([[0, 7, 20, 7]]); + setSelectedEntityIds([entities()[0].id]); + type("s"); + type("0,0"); + type("3,0"); + expect(lineEnds()).toEqual([[3, 7, 23, 7]]); + }); +}); + +describe("ARRAY(1-3)", () => { + it("R = 직사각형 · 미리보기 4 × 3 · 처음 간격 1.5 배 · 빈 Enter = 만듦", () => { + lineSelected("0,0", "10,0"); + type("ar"); + expect(promptLine()).toBe("배열 유형 입력 [직사각형(R)/원형(PO)] <직사각형>:"); + type("r"); + expect(promptLine()).toBe( + "그립을 선택하여 배열을 편집하거나 [개수(COU)/간격두기(S)/열(COL)/행(R)/종료(X)] <종료>:", + ); + expect(getGhostHelperEntities()).toHaveLength(11); + enter(); + expect(activeTool()).toBe(Tool.SELECT); + expect(entities("Line")).toHaveLength(12); + expect(lineEnds()[11]).toEqual([45, 30, 55, 30]); + }); + + it("COU · S · COL · R · X", () => { + lineSelected("0,0", "10,0"); + type("ar"); + enter(); // <직사각형> + type("cou"); + expect(promptLine()).toBe("열 수 입력 <4>:"); + type("2"); + expect(promptLine()).toBe("행 수 입력 <3>:"); + type("2"); + type("s"); + expect(promptLine()).toBe("열 사이의 거리 지정 <15.0000>:"); + type("20"); + expect(promptLine()).toBe("행 사이의 거리 지정 <15.0000>:"); + type("30"); + type("col"); + type("3"); + expect(promptLine()).toBe("열 사이의 거리 지정 <20.0000>:"); + enter(); + type("r"); + expect(promptLine()).toBe("행 수 입력 <2>:"); + type("1"); + expect(promptLine()).toBe("행 사이의 거리 지정 <30.0000>:"); + enter(); + type("x"); + expect(lineEnds()).toEqual([ + [0, 0, 10, 0], + [20, 0, 30, 0], + [40, 0, 50, 0], + ]); + }); + + it("PO — 중심 · I · F · A · ROT · 유형 기억", () => { + lineSelected("10,0", "20,0"); + type("ar"); + type("po"); + expect(promptLine()).toBe("배열의 중심점 지정:"); + type("0,0"); + expect(promptLine()).toBe( + "그립을 선택하여 배열을 편집하거나 [항목(I)/사이의 각도(A)/채울 각도(F)/항목 회전(ROT)/종료(X)] <종료>:", + ); + expect(getGhostHelperEntities()).toHaveLength(5); // 6 개 360° + type("i"); + expect(promptLine()).toBe("배열의 항목 수 입력 <6>:"); + type("4"); + type("x"); + expect(lineEnds()).toEqual([ + [-10, 0, -20, 0], + [0, -10, 0, -20], + [0, 10, 0, 20], + [10, 0, 20, 0], + ]); + + setSelectedEntityIds([entities()[0].id]); + type("ar"); + expect(promptLine()).toBe("배열 유형 입력 [직사각형(R)/원형(PO)] <원형>:"); + enter(); + type("0,0"); + type("i"); + type("3"); + type("f"); + expect(promptLine()).toBe("채울 각도 지정(+=ccw, -=cw) <360>:"); + type("180"); + type("a"); + expect(promptLine()).toBe("항목 사이의 각도 지정 <90>:"); + type("45"); + type("rot"); + expect(promptLine()).toBe("배열된 항목을 회전하시겠습니까? [예(Y)/아니오(N)] <예>:"); + type("n"); + type("x"); + // 원본 (10,0)-(20,0) · 가운데 (15,0) 을 45° · 90° 자리로 옮김(돌지 않음) + const c45 = 15 / Math.SQRT2; + expect(lineEnds()).toContainEqual([r(c45 - 5), r(c45), r(c45 + 5), r(c45)]); + expect(lineEnds()).toContainEqual([-5, 15, 5, 15]); + }); + + it("Esc = 안 만듦(편집 물음 · 숫자 물음)", () => { + lineSelected("0,0", "10,0"); + type("ar"); + type("r"); + esc(); + expect(entities("Line")).toHaveLength(1); + expect(getGhostHelperEntities()).toHaveLength(0); + setSelectedEntityIds([entities()[0].id]); + type("ar"); + type("po"); + type("0,0"); + type("i"); + esc(); + expect(entities("Line")).toHaveLength(1); + expect(activeTool()).toBe(Tool.SELECT); + }); +}); + +describe("PEDIT 고르기 · 고르기 공용(2-14 ④ · 2-15)", () => { + it("PEDIT — 고른 것 바로 묶음 · 없으면 「객체 선택:」 · 빈 Enter · Esc = 끝", () => { + line("0,0", "10,0"); + line("10,0", "10,10"); + type("pe"); + expect(promptLine()).toBe("객체 선택:"); + enter(); + expect(activeTool()).toBe(Tool.SELECT); + type("pe"); + esc(); + expect(activeTool()).toBe(Tool.SELECT); + setSelectedEntityIds(entities().map((entity) => entity.id)); + type("pe"); + expect(activeTool()).toBe(Tool.SELECT); + expect(entities("Line")).toHaveLength(0); + expect(entities()).toHaveLength(1); + }); + + it("고르기 물음의 ALL · L · P 는 거절 없이 선택 도구로 넘어감", () => { + line("0,0", "10,0"); + type("m"); + for (const word of ["all", "l", "p"]) { + type(word); + expect(noticeLine()).toBeNull(); + expect(activeTool()).toBe(Tool.MOVE); + } + }); + + const selectTakesKeywords = !!( + selectToolStateMachine.states.WAITING_FOR_FIRST_SELECT_POINT.config.on as Record< + string, + unknown + > + )?.["OPTION.ALL"]; + + it.skipIf(!selectTakesKeywords)( + "ALL · P(앞 고름) · L(마지막 객체) — 마 선택 도구 처리 뒤", + () => { + line("0,0", "10,0"); + line("0,20", "10,20"); + type("m"); + type("all"); + enter(); + type("0,0"); + type("0,5"); + expect(lineEnds()).toEqual([ + [0, 5, 10, 5], + [0, 25, 10, 25], + ]); + type("m"); + type("p"); + enter(); + type("0,0"); + type("5,0"); + expect(lineEnds()).toEqual([ + [5, 5, 15, 5], + [5, 25, 15, 25], + ]); + type("m"); + type("l"); + enter(); + type("0,0"); + type("0,100"); + expect(lineEnds()).toEqual([ + [5, 5, 15, 5], + [5, 125, 15, 125], + ]); + }, + ); +}); From 4ff989c1259d5882638d9a87ecab6a1736baa42f Mon Sep 17 00:00:00 2001 From: umsangdon Date: Fri, 16 Oct 2026 00:25:51 +0900 Subject: [PATCH 75/81] =?UTF-8?q?feat(=EC=9D=BC=EC=9C=84=EB=8C=80=EA=B0=80?= =?UTF-8?q?=EC=8B=A4=EB=AC=B4):=20338-4=20=EC=9D=B4=EC=9D=8C=EB=A7=A4=20?= =?UTF-8?q?=E2=80=94=20=ED=99=95=EC=9D=B8=20=ED=99=94=EB=A9=B4=20=E2=86=94?= =?UTF-8?q?=20=EC=8B=A4=EB=AC=BC=20=EB=A7=9E=EC=B6=A4=20=EC=9D=91=EB=8B=B5?= =?UTF-8?q?=20=C2=B7=20guess=20=EA=B8=B8=20=EB=AA=A9=EB=A1=9D=20=C2=B7=20?= =?UTF-8?q?=EB=81=9D=EA=B9=8C=EC=A7=80=20=EC=8B=9C=ED=97=98=20=C2=B7=20?= =?UTF-8?q?=EC=9D=B4=EC=9D=8C=EB=A7=A4=20=EA=B8=B0=EC=A4=80=20=EC=83=88=20?= =?UTF-8?q?=EB=9C=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 길 — guess 몸 대상 list | dict · to = 만들 층 - 줄 맞춤 — 섞인 줄 1위 대상에 몫 · 배(고임돌 GF000211 × 0.1) · 장비 품 비교 열쇠 「+」(화면과 같음) · guess 가 사용자 배 그대로 - 화면 — 품 비교 장비 표시 열쇠를 갈래 묶음 이름으로 - 시험 — client 고정물 맞춤셈 캐시 = 임시 폴더 · 뒤 셈 안 돌림(tmp/m02_hopyo 안 남음 · 결과 흔들림 없음) · 끝까지(북부청 올림 → 셈 → 보기 → 화면 몸 → 만들기 → 장부 → 다시 올림 별칭 · 몸 없음 400 · hopyo-memory 404 · 영월 STmate) · 실물 대역 화면 시험 · 이음매 268 줄 맞음 122 · 다른 점 86 · 후보 60 · 후보 1순위 근거 맞음 123 → 9 - 응답 대역 JSON 실물로 다시 뽑음 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Ji4nuyLVHcxrXWHTdb5nRD --- M02_MasterTemplete/M02_Hopyo_Line.py | 10 +- M02_MasterTemplete/M02_Labor_Router.py | 13 +- .../M02_MasterTemplete_Hopyo_Check.ts | 2 +- .../fixtures/m02_hopyo_view_sample.json | 126 ++++++++++++-- resources/tester/helper_m02_hopyo_check.cjs | 31 ++++ resources/tester/test_m02_hopyo_excel.py | 17 ++ resources/tester/test_m02_hopyo_flow.py | 158 ++++++++++++++++++ resources/tester/test_m02_hopyo_match.py | 15 -- resources/tester/test_m02_hopyo_screen.py | 26 +++ resources/tester/test_m02_hopyo_seam.py | 15 +- 10 files changed, 370 insertions(+), 43 deletions(-) create mode 100644 resources/tester/test_m02_hopyo_flow.py diff --git a/M02_MasterTemplete/M02_Hopyo_Line.py b/M02_MasterTemplete/M02_Hopyo_Line.py index 2d77dd5e5..3e533a65b 100644 --- a/M02_MasterTemplete/M02_Hopyo_Line.py +++ b/M02_MasterTemplete/M02_Hopyo_Line.py @@ -102,7 +102,8 @@ def _qty(entry: dict, found: dict | None) -> dict: for m in ex["장비"]: mine = [x["시간"] for x in us["장비"] if set(x["갈래"]) & set(m["갈래"])] hit = any(option.near(m["시간"], q, fit.TOL[0], option.ru(m["시간"])) for q in mine) - same["·".join(m["갈래"])] = hit or same.get("·".join(m["갈래"]), False) + name = "+".join(m["갈래"]) # 화면 품 비교 열쇠와 같음(갈래 + 로 묶음) + same[name] = hit or same.get(name, False) return {"엑셀": ex, "우리": us, "같음": same} @@ -123,7 +124,10 @@ def _numeric(line: dict, ctx: Ctx, entry: dict) -> dict: for c in combo ] else: - targets = [_target(top, line, ctx, found, use)] if top else [] + part = next((c for c in entry.get("조합") or [] if c["로직"] == top), {}) + share = part.get("몫") if part.get("몫") not in (None, 1, 1.0) else None # 섞인 줄 인력 몫 + extra = {"몫": share, "어디": part["어디"]} if share else {} + targets = [_target(top, line, ctx, found, use, **extra)] if top else [] tags = list(judged.get("꼬리표") or []) tags += [f"조합 — {' + '.join(c['로직'] for c in combo)}"] if combo else [] for c in entry.get("조합") or []: @@ -304,6 +308,8 @@ def guess( raise store.StoreError(400, "돈 로직 · 우리 자재가 아님") given = t.get("입력값") one = _target(t["로직"], line, ctx, None, False, 몫=t.get("몫")) + if isinstance(t.get("배"), (int, float)) and t["배"] > 0: + one["배"] = t["배"] # 사용자가 정한 배는 그대로 if isinstance(given, dict): one["입력값"] = {**one["입력값"], **given} one["출처"].update({k: "고름" for k in given}) diff --git a/M02_MasterTemplete/M02_Labor_Router.py b/M02_MasterTemplete/M02_Labor_Router.py index f528dedf1..7480fb174 100644 --- a/M02_MasterTemplete/M02_Labor_Router.py +++ b/M02_MasterTemplete/M02_Labor_Router.py @@ -295,10 +295,10 @@ def hopyo_one( class GuessBody(BaseModel): - """`차례` = 호표 안 줄(0부터) · `대상` = {로직} | {자재}.""" + """`차례` = 호표 안 줄(0부터) · `대상` = [{로직 | 자재, 입력값?, 몫?}](낱개도 받음).""" 차례: int - 대상: dict[str, Any] + 대상: list[dict[str, Any]] | dict[str, Any] @router.post("/layers/{layer}/hopyo/{key}/{no}/guess") @@ -308,11 +308,16 @@ def hopyo_guess( no: int, body: GuessBody, owner: str | None = None, + to: Layer | None = None, + to_owner: str | None = None, session: Session = Depends(verify_session), ) -> dict: - """고른 대상 미리 채움(저장 안 함) — 규격 → 입력값 · 환산.""" + """미리 셈(저장 안 함 · 설계 3-4) — 고른 대상 · 옵션으로 엔진 한 번 → `{대상, 품, 글}` · + `to` = 만들 층(없으면 파일 층).""" folder = _root(session, layer, owner) - return _call(folder, hopyo_match.guess, folder, key, no, body.차례, body.대상) + target = _root(session, to, to_owner) if to is not None else None + args = (folder, key, no, body.차례, body.대상) + return _call(folder, lambda: hopyo_match.guess(*args, to=target)) class _Strict(BaseModel): diff --git a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Check.ts b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Check.ts index 7087db5b9..264872dbf 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Check.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Check.ts @@ -152,7 +152,7 @@ export function compareText(q: HopyoMatch["품"] | undefined): string { const name = (g: string | string[]): string => [g].flat().join("+"); for (const x of q.엑셀.장비 ?? []) { const o = (q.우리.장비 ?? []).find((y) => name(y.갈래) === name(x.갈래)); - out.push(`${name(x.갈래)} ${n4(x.시간)} → ${n4(o?.시간)} ${mark([x.갈래].flat()[0])}`); + out.push(`${name(x.갈래)} ${n4(x.시간)} → ${n4(o?.시간)} ${mark(name(x.갈래))}`); } return out.join(" · "); } diff --git a/resources/tester/fixtures/m02_hopyo_view_sample.json b/resources/tester/fixtures/m02_hopyo_view_sample.json index c7de6e3f7..3a6ba98ce 100644 --- a/resources/tester/fixtures/m02_hopyo_view_sample.json +++ b/resources/tester/fixtures/m02_hopyo_view_sample.json @@ -1,5 +1,5 @@ { - "_설명": "338 맞춤 응답 대역(설계 3-3 · 3-4) — 실물(북부청 임도 제1 · 4호표) 응답 · 가 창 1 단계 엔진", + "_설명": "338 맞춤 응답 대역(설계 3-3 · 3-4) — 실물(북부청 임도 제1 · 4호표) 응답", "길": { "파일": "GET /layers/{층}/hopyo/{키}", "호표": "GET /layers/{층}/hopyo/{키}/{번}?to=&to_owner=", @@ -8,7 +8,7 @@ "파일": { "키": "HP-000001", "파일": "2026년_북부지방산림청_임도사업_설계내역서(공통단가).xls", - "올린때": "2026-10-15T23:51:45", + "올린때": "2026-10-16T00:18:16", "올린이": { "id": 0, "이름": "시험" @@ -75,7 +75,7 @@ "호표": { "키": "HP-000001", "파일": "2026년_북부지방산림청_임도사업_설계내역서(공통단가).xls", - "올린때": "2026-10-15T23:51:45", + "올린때": "2026-10-16T00:18:16", "올린이": { "id": 0, "이름": "시험" @@ -250,7 +250,12 @@ "조항": [ "건설품셈 공통 6-1-1" ], - "까닭": "조항 같음" + "직종": [ + "콘크리트공" + ], + "단위": true, + "점수": 1.75, + "까닭": "조항 같음 · 건설 공통 · 직종 콘크리트공 · 단위 맞음" } }, { @@ -262,6 +267,9 @@ "글": "후보", "근거": { "조항": [], + "직종": [], + "단위": true, + "점수": 0.0, "까닭": "" } }, @@ -276,7 +284,12 @@ "조항": [ "건설품셈 공통 6-1-2" ], - "까닭": "앞 마디 같음" + "직종": [ + "콘크리트공" + ], + "단위": true, + "점수": 1.25, + "까닭": "앞 마디 같음 · 건설 공통 · 직종 콘크리트공 · 단위 맞음" } }, { @@ -288,6 +301,9 @@ "글": "후보", "근거": { "조항": [], + "직종": [], + "단위": true, + "점수": 0.0, "까닭": "" } }, @@ -302,7 +318,12 @@ "조항": [ "건설품셈 공통 6-1-4" ], - "까닭": "앞 마디 같음" + "직종": [ + "콘크리트공" + ], + "단위": true, + "점수": 1.25, + "까닭": "앞 마디 같음 · 건설 공통 · 직종 콘크리트공 · 단위 맞음" } } ], @@ -439,7 +460,12 @@ "조항": [ "건설품셈 공통 6-3-1" ], - "까닭": "조항 같음" + "직종": [ + "형틀목공" + ], + "단위": true, + "점수": 1.75, + "까닭": "조항 같음 · 건설 공통 · 직종 형틀목공 · 단위 맞음" } }, { @@ -451,6 +477,9 @@ "글": "후보", "근거": { "조항": [], + "직종": [], + "단위": true, + "점수": 0.0, "까닭": "" } }, @@ -465,7 +494,12 @@ "조항": [ "건설품셈 공통 6-3-3" ], - "까닭": "앞 마디 같음" + "직종": [ + "형틀목공" + ], + "단위": true, + "점수": 1.25, + "까닭": "앞 마디 같음 · 건설 공통 · 직종 형틀목공 · 단위 맞음" } }, { @@ -479,7 +513,12 @@ "조항": [ "건설품셈 공통 6-3-7" ], - "까닭": "앞 마디 같음" + "직종": [ + "형틀목공" + ], + "단위": true, + "점수": 1.25, + "까닭": "앞 마디 같음 · 건설 공통 · 직종 형틀목공 · 단위 맞음" } }, { @@ -493,7 +532,12 @@ "조항": [ "건설품셈 공통 6-3-4" ], - "까닭": "앞 마디 같음" + "직종": [ + "형틀목공" + ], + "단위": true, + "점수": 1.25, + "까닭": "앞 마디 같음 · 건설 공통 · 직종 형틀목공 · 단위 맞음" } } ], @@ -634,7 +678,12 @@ "조항": [ "건설품셈 공통 3-4-8" ], - "까닭": "조항 같음" + "직종": [ + "특별인부" + ], + "단위": true, + "점수": 1.75, + "까닭": "조항 같음 · 건설 공통 · 직종 특별인부 · 단위 맞음" } }, { @@ -648,7 +697,12 @@ "조항": [ "건설품셈 공통 3-4-4" ], - "까닭": "앞 마디 같음" + "직종": [ + "특별인부" + ], + "단위": true, + "점수": 1.25, + "까닭": "앞 마디 같음 · 건설 공통 · 직종 특별인부 · 단위 맞음" } }, { @@ -662,7 +716,12 @@ "조항": [ "건설품셈 공통 3-4-6" ], - "까닭": "앞 마디 같음" + "직종": [ + "특별인부" + ], + "단위": true, + "점수": 1.25, + "까닭": "앞 마디 같음 · 건설 공통 · 직종 특별인부 · 단위 맞음" } }, { @@ -676,7 +735,12 @@ "조항": [ "건설품셈 공통 3-4-5" ], - "까닭": "앞 마디 같음" + "직종": [ + "특별인부" + ], + "단위": true, + "점수": 1.25, + "까닭": "앞 마디 같음 · 건설 공통 · 직종 특별인부 · 단위 맞음" } }, { @@ -690,7 +754,12 @@ "조항": [ "건설품셈 공통 3-4-7" ], - "까닭": "앞 마디 같음" + "직종": [ + "특별인부" + ], + "단위": true, + "점수": 1.25, + "까닭": "앞 마디 같음 · 건설 공통 · 직종 특별인부 · 단위 맞음" } } ], @@ -947,7 +1016,12 @@ "조항": [ "건설품셈 공통 6-2-3" ], - "까닭": "윗마디 같음" + "직종": [ + "철근공" + ], + "단위": true, + "점수": 1.65, + "까닭": "윗마디 같음 · 건설 공통 · 직종 철근공 · 단위 맞음" } }, { @@ -961,7 +1035,12 @@ "조항": [ "건설품셈 공통 6-2-2" ], - "까닭": "윗마디 같음" + "직종": [ + "철근공" + ], + "단위": true, + "점수": 1.65, + "까닭": "윗마디 같음 · 건설 공통 · 직종 철근공 · 단위 맞음" } }, { @@ -973,6 +1052,9 @@ "글": "후보", "근거": { "조항": [], + "직종": [], + "단위": true, + "점수": 0.0, "까닭": "" } }, @@ -985,6 +1067,9 @@ "글": "후보", "근거": { "조항": [], + "직종": [], + "단위": true, + "점수": 0.0, "까닭": "" } }, @@ -999,7 +1084,12 @@ "조항": [ "건설품셈 공통 6-2-4" ], - "까닭": "윗마디 같음" + "직종": [ + "철근공" + ], + "단위": true, + "점수": 1.65, + "까닭": "윗마디 같음 · 건설 공통 · 직종 철근공 · 단위 맞음" } } ], diff --git a/resources/tester/helper_m02_hopyo_check.cjs b/resources/tester/helper_m02_hopyo_check.cjs index 0af39f459..d91ae5ea6 100644 --- a/resources/tester/helper_m02_hopyo_check.cjs +++ b/resources/tester/helper_m02_hopyo_check.cjs @@ -54,6 +54,7 @@ global.sessionStorage = { removeItem: (k) => store.delete(k), }; +let out0 = null; (async () => { const input = JSON.parse(fs.readFileSync(0, "utf8")); const calls = []; @@ -73,6 +74,36 @@ global.sessionStorage = { const sheet = loadTs(path.join(M02, "M02_MasterTemplete_Hopyo_Sheet.ts")); const api = loadTs(path.join(M02, "M02_MasterTemplete_Hopyo_Api.ts")); const pick = loadTs(path.join(M02, "M02_MasterTemplete_Hopyo_Pick.ts")); + if (input.real) { + // 실물 맞춤 응답(338-4) — 서버 줄을 그대로 먹여 체크 · 몸 · 빠질 줄 · 품 비교 · 글 + const lines = input.real; + const d = check.loadDraft("real"); + out0 = { + body: check.bodyOf(lines, d), + leftOut: check.leftOut(lines, d), + sure: check.sureLines(lines, d).map((l) => l.차례), + lines: lines.map((l) => { + const p = check.partsOf(l, d)[0]; + return { + 이름: l.이름, + 상태: l.맞춤.상태, + checkable: check.checkable(l, d), + checked: check.isChecked(l, d), + compare: check.compareText(l.맞춤.품), + qty: choose.qtyText(l.맞춤.품?.엑셀), + target: choose.targetText(p), + inputs: p ? choose.inputsText(p.입력값 ?? {}, p.못읽음 ?? [], p.출처 ?? {}) : "", + cands: (l.맞춤.후보 ?? []).map((c) => [ + choose.targetText(c), + c.판정 ?? "", + choose.basisTag(c.근거), + ]), + }; + }), + }; + process.stdout.write(JSON.stringify(out0)); + return; + } const { lines } = input; const scope = { layer: "personal", owner: null }; const key = check.draftKey(scope, "HP-000001", 3); diff --git a/resources/tester/test_m02_hopyo_excel.py b/resources/tester/test_m02_hopyo_excel.py index 7129e28de..3622b9d39 100644 --- a/resources/tester/test_m02_hopyo_excel.py +++ b/resources/tester/test_m02_hopyo_excel.py @@ -9,6 +9,7 @@ master_data 원본 그대로(sha256). 층은 임시 폴더. import hashlib import io +import shutil from pathlib import Path import pytest @@ -18,6 +19,8 @@ from fastapi.testclient import TestClient from common_util.common_util_auth import verify_session from config import config_system from M02_MasterTemplete import M02_Hopyo_Excel as hx +from M02_MasterTemplete import M02_Hopyo_Fit as fit +from M02_MasterTemplete import M02_Hopyo_Ours as ours from M02_MasterTemplete import M02_Hopyo_Store as hs from M02_MasterTemplete import M02_Labor_Router as labor_router from M02_MasterTemplete import M02_MasterTemplete_Store as store @@ -120,6 +123,19 @@ def test_xlsx_길(): # ── 저장 · 길 ────────────────────────────────────────────────────────── +def calc_cache(tmp_path, monkeypatch) -> Path: + """맞춤셈(338) — 캐시 = 임시 폴더(우리 지문 사본 · 시험 끝에 같이 지움) · 뒤 셈 안 돌림 + (`ensure` = 상태만) — 숫자 판정이 필요한 시험은 `fit.run_job` 을 바로 부름.""" + if not ours.ready(): + ours.build() + cache = tmp_path / "hopyo_cache" + cache.mkdir(exist_ok=True) + shutil.copy(ours.path_of(), cache / ours.path_of().name) + monkeypatch.setenv("AISLO_HOPYO_CACHE", str(cache)) + monkeypatch.setattr(fit, "ensure", fit.status) + return cache + + @pytest.fixture() def client(tmp_path, monkeypatch): system = tmp_path / "system" @@ -129,6 +145,7 @@ def client(tmp_path, monkeypatch): monkeypatch.setattr(layers, "SYSTEM_ROOT", system) (tmp_path / "storage").mkdir() monkeypatch.setattr(config_system, "STORAGE_BASE_DIR", str(tmp_path / "storage")) + calc_cache(tmp_path, monkeypatch) app = FastAPI() app.include_router(labor_router.router) who = dict(USER) diff --git a/resources/tester/test_m02_hopyo_flow.py b/resources/tester/test_m02_hopyo_flow.py new file mode 100644 index 000000000..a65f991c9 --- /dev/null +++ b/resources/tester/test_m02_hopyo_flow.py @@ -0,0 +1,158 @@ +"""338-4 이음매 — 끝까지(시험 클라이언트 · 임시 층): 북부청 임도 올림 → 셈 끝 → 호표 보기(맞음 · +다른 점 · +후보) → 화면 도우미(`helper_m02_hopyo_check.cjs` · 실물 응답)가 만든 몸(체크한 줄만) → +[확인하고 사전조합 만들기] → 사전조합 줄(로직 여럿 같은 별칭 · 배 · 몫) · 별칭 장부 → 다시 올림 = +별칭 체크 · +몸 없는 만들기 400 · 옛 hopyo-memory 404 · STmate 영월 올림 → 상태 · master_data 원본 그대로. +""" + +import json +import os +import subprocess + +from M02_MasterTemplete import M02_Hopyo_Fit as fit +from M02_MasterTemplete import M02_Template_Layers as layers +from resources.tester.helper_m02_hopyo_golden import FILES +from resources.tester.test_m02_hopyo_excel import URL, _post, client, master_hash # noqa: F401 + +# ruff: noqa: F811 +HERE = os.path.dirname(os.path.abspath(__file__)) +FOLDER = (7, 42) # USER 의 개인 층 + + +def _node(payload: dict) -> dict: + proc = subprocess.run( + ["node", os.path.join(HERE, "helper_m02_hopyo_check.cjs")], + input=json.dumps(payload, ensure_ascii=False), + capture_output=True, + text=True, + encoding="utf-8", + check=False, + ) + assert proc.returncode == 0, proc.stderr + return json.loads(proc.stdout) + + +def _up(client, key: str, name: str | None = None, data: bytes | None = None) -> None: + res = _post(client, data=data, **({"name": name} if name else {})) + assert res.status_code == 200, res.text + assert res.json()["키"] == key + fit.run_job(layers.personal_dir(*FOLDER), key) # 셈 끝(뒤 셈 대신 바로) + + +def _one(client, key: str, no: int) -> dict: + res = client.get(f"{URL.format('personal')}/{key}/{no}") + assert res.status_code == 200, res.text + return res.json()["호표"] + + +def _find(client, key: str, name: str) -> tuple[int, dict]: + whole = client.get(f"{URL.format('personal')}/{key}").json() + assert whole["셈"]["끝"] + for h in whole["호표"]: + one = _one(client, key, h["번"]) + if any(x["이름"] == name for x in one["줄"]): + return h["번"], one + raise KeyError(name) + + +def _build(client, key: str, no: int, lines: list, **extra) -> dict: + body = {"층": "personal", "키": key, "번": no, "줄": lines, **extra} + res = client.post("/api/m02/layers/personal/hopyo-build", json=body) + assert res.status_code == 200, res.text + return res.json() + + +def _doc(ref: dict) -> dict: + path = layers.personal_dir(*FOLDER) / "precombo" / f"{ref['키']}.json" + return json.loads(path.read_text(encoding="utf-8")) + + +def test_끝까지_확인한_줄만_별칭(client): + before = master_hash() + _up(client, "HP-000001") + no, hopyo = _find(client, "HP-000001", "레미콘타설(장비사용)") + by = {x["이름"]: x for x in hopyo["줄"]} + assert by["레미콘타설(장비사용)"]["맞춤"]["상태"] == "맞음" + screen = _node({"real": hopyo["줄"]}) + seen = {x["이름"]: x for x in screen["lines"]} + remicon = seen["레미콘타설(장비사용)"] + assert remicon["checked"] and remicon["checkable"] + assert remicon["compare"].startswith("콘크리트공 0.1091 → 0.1091 ✓") + assert "굴착기 0.29 → 0.2909 ✓" in remicon["compare"] + assert "소량감률=50(블록)" in remicon["inputs"] and remicon["target"].endswith("[㎥]") + states = {x["상태"] for x in screen["lines"]} + assert {"맞음", "다른 점", "후보"} <= states + body = screen["body"] # 화면이 만든 몸 = 서버 체크 제안 줄만 + suggested = [x["차례"] for x in hopyo["줄"] if x["맞춤"]["체크"]] + assert [x["차례"] for x in body] == suggested and screen["leftOut"]["확인 안 함"] > 0 + rebar = next(x for x in hopyo["줄"] if x["이름"] == "철근가공조립(현장)") + two = [{k: p[k] for k in ("로직", "입력값", "배")} for p in rebar["맞춤"]["대상"]] + assert {p["로직"] for p in two} == {"GC000206", "GC000207"} # 다른 점 → 사용자가 체크 + body = body + [{"차례": rebar["차례"], "대상": two}] + made = _build(client, "HP-000001", no, body) + assert made["다시씀"] is False and made["장부"]["씀"] == len(body) + assert all(m["까닭"] for m in made["빠진줄"]) # 확인 안 함 · % 줄 + doc = _doc(made["참조"]) + logics = [x for x in doc["담은로직"] if x.get("로직")] + alias = {x["로직"]: x["별칭"] for x in logics} + assert alias["GC000206"] == alias["GC000207"] == "철근가공조립(현장)" # 로직 여럿 · 같은 별칭 + assert doc["본뜬"]["확인"] and doc["본뜬"]["키"] == f"HP-000001 제{no}호표" + rows = client.get("/api/m02/layers/personal/hopyo-ledger").json()["줄"] + keys = {r["열쇠"]: r for r in rows} + assert {p["로직"] for p in keys[rebar["맞춤"]["열쇠"]]["짝"]} == {"GC000206", "GC000207"} + # 다시 올림 = 확인한 줄은 별칭 · 체크 · 만든 것이 보임 + _up(client, "HP-000002") + again = {x["이름"]: x["맞춤"] for x in _one(client, "HP-000002", no)["줄"]} + assert again["레미콘타설(장비사용)"]["상태"] == "별칭" and again["레미콘타설(장비사용)"]["체크"] + assert again["철근가공조립(현장)"]["상태"] == "별칭" + assert again["철근가공조립(현장)"]["대상"][0]["출처"] == { + k: "별칭" for k in again["철근가공조립(현장)"]["대상"][0]["입력값"] + } + first = _one(client, "HP-000001", no) + assert first["만든"] == {"키": made["참조"]["키"], "확인": True} + assert master_hash() == before + + +def test_섞인_줄_몫_배(client): + _up(client, "HP-000001") + no, hopyo = _find(client, "HP-000001", "고임돌 채집 및 운반") + line = next(x for x in hopyo["줄"] if x["이름"] == "고임돌 채집 및 운반") + part = line["맞춤"]["대상"][0] + assert (part["로직"], part["몫"], part["배"]) == ("GF000211", 0.1, 0.1) + screen = _node({"real": [line]}) + assert screen["lines"][0]["checkable"] and not screen["lines"][0]["checked"] + one = {k: part[k] for k in ("로직", "입력값", "배", "몫")} + made = _build(client, "HP-000001", no, [{"차례": line["차례"], "대상": [one]}]) + row = next(x for x in _doc(made["참조"])["담은로직"] if x.get("로직") == "GF000211") + assert row["수량"]["손"] == round(line["수량"] * 0.1, 6) + + +def test_막는_길(client): + _up(client, "HP-000001") + empty = client.post( + "/api/m02/layers/personal/hopyo-build", + json={"층": "personal", "키": "HP-000001", "번": 1, "줄": []}, + ) + assert empty.status_code == 400 and "확인한 줄이 없음" in empty.text + money = client.post( + "/api/m02/layers/personal/hopyo-build", + json={"층": "personal", "키": "HP-000001", "번": 1, "줄": [{"차례": 1, "금액": 1}]}, + ) + assert money.status_code in (400, 422) + gone = client.post("/api/m02/layers/personal/hopyo-memory", json={"줄": {}}) + assert gone.status_code in (404, 405) + url = f"{URL.format('personal')}/HP-000001/1/guess" + many = client.post(url, json={"차례": 1, "대상": [{"로직": "GC000198", "입력값": {}}]}) + assert many.status_code == 200 and many.json()["대상"][0]["로직"] == "GC000198" + + +def test_STmate_영월(client): + path = FILES["동부청영월"] + _up(client, "HP-000001", path.name, path.read_bytes()) + whole = client.get(f"{URL.format('personal')}/HP-000001").json() + assert whole["셈"]["끝"] and len(whole["호표"]) == 23 and whole["맞춤"][0] > 0 + states = set() + for h in whole["호표"]: + states |= {x["맞춤"]["상태"] for x in _one(client, "HP-000001", h["번"])["줄"]} + assert {"맞음", "후보"} <= states diff --git a/resources/tester/test_m02_hopyo_match.py b/resources/tester/test_m02_hopyo_match.py index 3a2e2d385..ec1231685 100644 --- a/resources/tester/test_m02_hopyo_match.py +++ b/resources/tester/test_m02_hopyo_match.py @@ -2,13 +2,10 @@ 맞춤 차례 · 기억 우선 · 길(보기 · 미리 채움 · 확정) · master_data 원본 그대로. 층은 임시 폴더. """ -import shutil - import pytest from M02_MasterTemplete import M02_Hopyo_Fit as fit from M02_MasterTemplete import M02_Hopyo_Match as hm -from M02_MasterTemplete import M02_Hopyo_Ours as ours from M02_MasterTemplete import M02_Template_Layers as layers # 올리기 길 시험의 `client` 마당을 같이 씀 @@ -91,18 +88,6 @@ def test_규격에서_입력값(): # ── 맞춤 응답(338 · 설계 3-3) ───────────────────────────────────────── -@pytest.fixture(autouse=True) -def calc(tmp_path, monkeypatch): - """맞춤셈 캐시 = 임시 폴더(우리 지문 사본) · 뒤 셈은 시험이 바로 돌림(`run_job`).""" - if not ours.ready(): - ours.build() - cache = tmp_path / "hopyo_cache" - cache.mkdir() - shutil.copy(ours.path_of(), cache / ours.path_of().name) - monkeypatch.setenv("AISLO_HOPYO_CACHE", str(cache)) - monkeypatch.setattr(fit, "ensure", fit.status) - - def _hopyo(client, no, layer="personal"): res = client.get(f"{URL.format(layer)}/HP-000001/{no}") assert res.status_code == 200, res.text diff --git a/resources/tester/test_m02_hopyo_screen.py b/resources/tester/test_m02_hopyo_screen.py index 6492f4e37..547ba5573 100644 --- a/resources/tester/test_m02_hopyo_screen.py +++ b/resources/tester/test_m02_hopyo_screen.py @@ -342,3 +342,29 @@ def test_엑셀_보기_창_여는_자리와_늦게_불러옴(): r'^import \{[^}]*\} from "\.\./A00_Common/spreadsheet/spreadsheet"', sheet, re.M ) assert "readOnly: true" in sheet and "wide: true" in sheet + + +def test_실물_응답_대역_확인_창(): + """338-4 — 가 창 실물 응답(`fixtures/m02_hopyo_view_sample.json`)을 확인 창 · 고르기 창 도우미에 + 그대로 먹임 — 체크 = 서버 제안 · 몸 꼴 · 품 비교 · 옵션 출처 · 후보 판정 꼬리표.""" + sample = json.loads((Path(HERE) / "fixtures" / "m02_hopyo_view_sample.json").read_bytes()) + lines = sample["호표"]["호표"]["줄"] + got = _node("helper_m02_hopyo_check.cjs", {"real": lines}) + by = {x["이름"]: x for x in got["lines"]} + remicon = by["레미콘타설(장비사용)"] + assert remicon["상태"] == "맞음" and remicon["checked"] and remicon["checkable"] + assert ( + remicon["compare"] + == "콘크리트공 0.1091 → 0.1091 ✓ · 보통인부 0.0364 → 0.0364 ✓ · 굴착기 0.29 → 0.2909 ✓" + ) + assert ( + remicon["inputs"] + == "타설방법=장비사용타설(규격), 구조물=철근구조물(규격), 소량감률=50(블록)" + ) + assert remicon["qty"] == "콘크리트공 0.1091 · 보통인부 0.0364 · 굴착기 0.29hr" + assert remicon["cands"][0][1] == "맞음" and remicon["cands"][0][2].startswith("조항 건설품셈") + rebar = by["철근가공조립(현장)"] + assert rebar["상태"] == "다른 점" and not rebar["checked"] and rebar["checkable"] + assert [x["차례"] for x in got["body"]] == [x["차례"] for x in lines if x["맞춤"]["체크"]] + assert all({"로직", "입력값", "배"} <= set(t) for x in got["body"] for t in x["대상"]) + assert got["sure"] == [x["차례"] for x in lines if x["맞춤"]["상태"] == "맞음"] diff --git a/resources/tester/test_m02_hopyo_seam.py b/resources/tester/test_m02_hopyo_seam.py index 58b1aa8a6..45d5af159 100644 --- a/resources/tester/test_m02_hopyo_seam.py +++ b/resources/tester/test_m02_hopyo_seam.py @@ -2,14 +2,18 @@ 화면 도우미(`helper_m02_hopyo_seam.cjs` → Hopyo_Sheet · Hopyo_View)에 먹임. 행 22 → 제3호표 · 25 빈 줄 · 26 → 제4호표 · 영역 B20:O24 · 단산93 행 4229:4244 · -엑셀 근거 줄 · 근거 꼬리표 · 근거 맞음 · 북부청 「후보」 줄 1순위 근거 맞음 = 서버 기준 123 -(338 — 근거 맞음 모두 확정 단추 없앰 · 도우미는 고르기 창 `Hopyo_Choose`). +엑셀 근거 줄 · 근거 꼬리표 · 근거 맞음 · 도우미는 고르기 창 `Hopyo_Choose`. +338 — 같음 상태 없앰 · 숫자 판정(셈 끝)으로 북부청 단가산출 268 줄 = 맞음 122 · 다른 점 86 · 후보 60 +· +「후보」 줄 1순위 근거 맞음 123 → 9(근거 맞는 줄은 거의 맞음 · 다른 점으로 옮겨 감). """ import json import os import subprocess +from M02_MasterTemplete import M02_Hopyo_Fit as fit +from M02_MasterTemplete import M02_Template_Layers as layers from resources.tester import test_m02_hopyo_excel as base from resources.tester.test_m02_hopyo_excel import URL, _post @@ -32,6 +36,7 @@ def _node(payload: dict) -> dict: def test_실제_응답_화면_도우미(client): assert _post(client).status_code == 200 + fit.run_job(layers.personal_dir(7, 42), "HP-000001") # 셈 끝(338 숫자 판정) grid = client.get("/api/m02/layers/personal/hopyo-grid/HP-000001").json() lines = [] for no in (3, 12, 38, 58): @@ -42,7 +47,11 @@ def test_실제_응답_화면_도우미(client): res = client.get(f"{URL.format('personal')}/HP-000001/{one['번']}").json() every += [x for x in res["호표"]["줄"] if x["갈래"] == "단가산출"] got = _node({"grid": grid, "lines": lines, "all": every}) - assert got["count"]["top"] == 123 + states = { + k: sum(1 for x in every if x["맞춤"]["상태"] == k) for k in ("맞음", "다른 점", "후보") + } + assert states == {"맞음": 122, "다른 점": 86, "후보": 60} # 268 줄 · 셈 끝 + assert got["count"]["top"] == 9 # 「후보」 줄 1순위 근거 맞음(근거 맞는 줄은 거의 숫자로 풀림) assert got["at"] == [None, 3, 3, 3, None, 4] # 25행 = 호표 사이 빈 줄 assert got["span"] == "B20:O24" and got["rows"] == "4229:4244" by = {x["이름"]: x for x in got["lines"]} From 933c4c5d718379dcb1259e7b7e45f81aa3ab127a Mon Sep 17 00:00:00 2001 From: umsangdon Date: Fri, 16 Oct 2026 00:34:02 +0900 Subject: [PATCH 76/81] =?UTF-8?q?refactor(CAD):=20337-6=20=EA=B0=80=20?= =?UTF-8?q?=EB=81=9D=20=EB=AA=AB=20=E2=80=94=20=EC=98=9B=20OFFSET=20=C2=B7?= =?UTF-8?q?=20LENGTHEN=20=C2=B7=20=EC=98=9B=20=EA=BC=B4=20=ED=86=B5?= =?UTF-8?q?=EA=B3=BC=20=EC=A7=80=EC=9B=80=20=C2=B7=20ALL=20=C2=B7=20L=20?= =?UTF-8?q?=C2=B7=20P=20=EC=8B=A4=EC=A0=9C=20=EA=B3=A0=EB=A6=84=20?= =?UTF-8?q?=EC=8B=9C=ED=97=98=20=C2=B7=20=EC=95=88=EB=82=B4=20=EA=B8=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - transform-acad: 고르기 물음 ALL · L · P = 「n개를 찾음, 총 m개」 · 다시 「객체 선택:」 · 빈 Enter = 고르기 끝(설계 1-4 · 2-15) - transform-tools: 안 쓰는 옛 OFFSET · LENGTHEN 머신 지움(새 offset-tool · lengthen-tool 이 대신) - prompt-input · prompt · sequence-tool: 옛 꼴 통과(meta.options 글 배열 · TEXT_INPUT 대문자 통과 · chainFromLastPoint) 지움 - prompt-contract: 옛 꼴 안 쓰는지 지키는 시험 더함 - 안내 글(설계 4 장 끝): 오른쪽 단추 · 휠 두 번 · CAD 키 줄 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01QfHKQs7J3hQELKMWQMUJEy --- .../openwebcad/src/commands/prompt.ts | 2 - .../src/inputController/prompt-input.ts | 20 +-------- .../src/tools/factories/sequence-tool.ts | 18 +++----- .../src/tools/modify/transform-tools.ts | 44 +------------------ resources/tester/cad/prompt-contract.test.ts | 11 +++++ resources/tester/cad/transform-acad.test.ts | 21 ++++++--- ui_template/ui_template_locale_common.ts | 8 ++-- 7 files changed, 39 insertions(+), 85 deletions(-) diff --git a/B07_DesignDetail/openwebcad/src/commands/prompt.ts b/B07_DesignDetail/openwebcad/src/commands/prompt.ts index dc38a47aa..5efa82684 100644 --- a/B07_DesignDetail/openwebcad/src/commands/prompt.ts +++ b/B07_DesignDetail/openwebcad/src/commands/prompt.ts @@ -47,8 +47,6 @@ export interface PromptMeta { kind?: StepKind; prompt?: PromptLike; instructions?: string; - /** 옛 꼴 — 점 물음이 받는 옵션 글자(대문자) */ - options?: string[]; } /** 함수 꼴 물음을 지금 context 로 풂 */ diff --git a/B07_DesignDetail/openwebcad/src/inputController/prompt-input.ts b/B07_DesignDetail/openwebcad/src/inputController/prompt-input.ts index d08439020..92192699e 100644 --- a/B07_DesignDetail/openwebcad/src/inputController/prompt-input.ts +++ b/B07_DesignDetail/openwebcad/src/inputController/prompt-input.ts @@ -147,14 +147,12 @@ export interface CurrentPrompt { spec: PromptSpec | null; /** 그린 한 줄(새 꼴) 또는 옛 instructions */ line: string | null; - /** 옛 꼴 — 점 물음이 받는 옵션 글자 */ - legacyOptions: string[]; } export function currentPrompt(): CurrentPrompt { const sub = subPrompt(); if (sub) { - return { kind: sub.kind, spec: sub.prompt, line: renderPrompt(sub.prompt, sub.kind), legacyOptions: [] }; + return { kind: sub.kind, spec: sub.prompt, line: renderPrompt(sub.prompt, sub.kind) }; } const meta = activeMeta(); const spec = resolvePrompt(meta?.prompt, activeSnapshot()?.context); @@ -163,7 +161,6 @@ export function currentPrompt(): CurrentPrompt { kind, spec, line: spec ? renderPrompt(spec, meta?.kind) : (meta?.instructions ?? null), - legacyOptions: meta?.options ?? [], }; } @@ -276,12 +273,6 @@ function numberProblem(value: number, rule: PromptSpec['number']): string | null // ---- 사다리(설계 2-5) ---- -/** 옛 꼴 통과 — 새 물음이 없는 상태가 TEXT_INPUT 을 받고 숫자는 안 받음 */ -const legacyTakesText = (): boolean => { - const on = (flatConfig()?.on ?? {}) as Record; - return !!on.TEXT_INPUT && !on.NUMBER_INPUT; -}; - /** ② 빈 Enter · Space · 우클릭 */ function routeEmpty(prompt: CurrentPrompt) { const lockedLength = getLockedLength(); @@ -400,17 +391,8 @@ export function routeTyped(raw: string): void { notice(`「${hit.absent.label}(${hit.absent.kw})」 옵션은 이 CAD 에 없음.`); return; } - const upper = normalizeKeyword(text); - if (!prompt.spec && prompt.legacyOptions.includes(upper)) { - send({ type: ActorEvent.TEXT_INPUT, value: upper }); // 옛 꼴 통과 - return; - } if (prompt.kind === 'point') return routePointText(text, prompt); if (prompt.kind === 'number') return routeNumber(text, prompt); - if (!prompt.spec && legacyTakesText()) { - send({ type: ActorEvent.TEXT_INPUT, value: upper }); // 옛 꼴 통과 - return; - } reject(prompt); // ⑦ ⑩ } diff --git a/B07_DesignDetail/openwebcad/src/tools/factories/sequence-tool.ts b/B07_DesignDetail/openwebcad/src/tools/factories/sequence-tool.ts index 2100d7062..7569d4553 100644 --- a/B07_DesignDetail/openwebcad/src/tools/factories/sequence-tool.ts +++ b/B07_DesignDetail/openwebcad/src/tools/factories/sequence-tool.ts @@ -118,8 +118,6 @@ export interface SequenceToolConfig { pickfirst?: boolean; /** 옛 꼴 — true = `loopFrom: 0` · false = 끝 */ restart?: boolean; - /** 옛 꼴 — 확정 뒤 마지막 점을 첫 값으로 이어받아 계속 그린다 */ - chainFromLastPoint?: boolean; /** 점 입력 중 스냅·각도 가이드 사용 여부 (기본 true) */ helpers?: boolean; /** ESC 로 확정 없이 그만둘 때 */ @@ -201,9 +199,7 @@ export function createSequenceTool(config: SequenceToolConfig) { ? indexOf(config.loopFrom) : config.restart === true ? 0 - : config.chainFromLastPoint && config.restart !== false - ? 1 - : null; + : null; for (const step of steps) if (step.next !== undefined) indexOf(step.next); const isRepeat = (index: number) => !!config.repeatLastStep && index === lastIndex; @@ -375,18 +371,16 @@ export function createSequenceTool(config: SequenceToolConfig) { setGhostHelperEntities([]); }; - /** 확정 뒤 되풀이 — loopFrom 앞 값만 둠(옛 chainFromLastPoint = 마지막 점 이어받음) */ + /** 확정 뒤 되풀이 — loopFrom 앞 값만 둠 */ const loopAgain = assign(({ context }: Ctx): Partial => { const from = loopIndex as number; - const tail = lastPoint(context.values); - const chain = config.chainFromLastPoint && config.loopFrom === undefined && tail; - const keep = context.values.map((_, index) => chain || context.stepOf[index] < from); - const values = chain ? [tail] : context.values.filter((_, index) => keep[index]); + const keep = context.values.map((_, index) => context.stepOf[index] < from); + const values = context.values.filter((_, index) => keep[index]); const next = { ...context, values, - picks: chain ? [tail] : context.picks.filter((_, index) => keep[index]), - stepOf: chain ? [0] : context.stepOf.filter((_, index) => keep[index]), + picks: context.picks.filter((_, index) => keep[index]), + stepOf: context.stepOf.filter((_, index) => keep[index]), closed: false, loops: context.loops + 1, }; diff --git a/B07_DesignDetail/openwebcad/src/tools/modify/transform-tools.ts b/B07_DesignDetail/openwebcad/src/tools/modify/transform-tools.ts index 21e573119..7ab0210b2 100644 --- a/B07_DesignDetail/openwebcad/src/tools/modify/transform-tools.ts +++ b/B07_DesignDetail/openwebcad/src/tools/modify/transform-tools.ts @@ -1,6 +1,5 @@ -/** 대칭·정렬·간격띄우기·신축·길이조정 (조사표 2절) */ +/** 대칭·정렬·신축 (조사표 2절) */ import { Box, Point, Polygon } from '@flatten-js/core'; -import { toast } from 'react-toastify'; import { type AnyActorRef, assign } from 'xstate'; import { OPT } from '../../commands/prompt'; import type { Entity } from '../../entities/Entity'; @@ -21,7 +20,7 @@ import { createDisplaceTool } from '../move-tool'; import { SelectState } from '../select-tool'; import type { MouseClickEvent, StateEvent } from '../tool.types'; import { SELECT_CHILD } from '../transform.helpers'; -import { copyStyle, lengthenLine, offsetEntity } from './modify.helpers'; +import { copyStyle } from './modify.helpers'; /** 원본 특성을 유지한 복사본 */ const cloneStyled = (entity: Entity): Entity => copyStyle(entity, entity.clone()); @@ -110,23 +109,6 @@ export const alignToolStateMachine = createSequenceTool({ }, }); -export const offsetToolStateMachine = createSequenceTool({ - tool: Tool.OFFSET, - steps: [ - { kind: 'number', instructions: '간격띄우기 거리를 입력하십시오 <1>.', defaultValue: 1 }, - { kind: 'entity', instructions: '간격띄우기할 객체를 선택하십시오.' }, - { kind: 'point', instructions: '간격을 띄울 방향의 점을 지정하십시오.' }, - ], - commit: (input) => { - const result = offsetEntity(input.entity(1), input.number(0), input.point(2)); - if (!result) { - toast.warn('이 객체는 지정한 거리로 간격을 띄울 수 없습니다.'); - return; - } - addEntities([result], true); - }, -}); - /** 바꾼 것을 원본 자리에 — id · 묶음 · 꼴 그대로 */ const inPlaceOf = (source: Entity, made: T): T => { copyStyle(source, made); @@ -208,25 +190,3 @@ export const stretchToolStateMachine = createDisplaceTool({ return context.originals.flatMap((entity) => stretchEntity(entity, box, dx, dy) ?? []); }, }); - -export const lengthenToolStateMachine = createSequenceTool({ - tool: Tool.LENGTHEN, - steps: [ - { kind: 'entity', instructions: '길이를 바꿀 선을 늘릴 쪽 끝 근처에서 선택하십시오.' }, - { - kind: 'number', - instructions: '증분 길이를 입력하십시오 (음수는 단축) <10>.', - defaultValue: 10, - }, - ], - commit: (input) => { - const entity = input.entity(0); - const changed = lengthenLine(entity, input.number(1), input.pick(0)); - if (!changed) { - toast.warn('선만 길이를 조정할 수 있습니다.'); - return; - } - deleteEntities([entity], false); - addEntities([changed], true); - }, -}); diff --git a/resources/tester/cad/prompt-contract.test.ts b/resources/tester/cad/prompt-contract.test.ts index 634b321d1..b94ad3240 100644 --- a/resources/tester/cad/prompt-contract.test.ts +++ b/resources/tester/cad/prompt-contract.test.ts @@ -62,4 +62,15 @@ describe("물음 약속(2-16)", () => { } expect(problems).toEqual([]); }); + + it("옛 꼴 통과 안 씀 — 옵션 글 배열 없음 · 숫자 없이 TEXT_INPUT 만 받는 상태 없음(글 물음 빼고)", () => { + const problems: string[] = []; + for (const { where, meta, on } of rows) { + if ((meta as { options?: unknown } | undefined)?.options) + problems.push(`${where}: meta.options`); + if (on.TEXT_INPUT && !on.NUMBER_INPUT && meta?.kind !== "text") + problems.push(`${where}: TEXT_INPUT 만`); + } + expect(problems).toEqual([]); + }); }); diff --git a/resources/tester/cad/transform-acad.test.ts b/resources/tester/cad/transform-acad.test.ts index 02974f246..f905d9868 100644 --- a/resources/tester/cad/transform-acad.test.ts +++ b/resources/tester/cad/transform-acad.test.ts @@ -621,14 +621,23 @@ describe("PEDIT 고르기 · 고르기 공용(2-14 ④ · 2-15)", () => { expect(entities()).toHaveLength(1); }); - it("고르기 물음의 ALL · L · P 는 거절 없이 선택 도구로 넘어감", () => { + it("고르기 물음의 ALL · L · P = 실제 고름 · 「n개를 찾음」 · 다시 「객체 선택:」 · 빈 Enter = 고르기 끝", () => { line("0,0", "10,0"); + line("0,20", "10,20"); type("m"); - for (const word of ["all", "l", "p"]) { - type(word); - expect(noticeLine()).toBeNull(); - expect(activeTool()).toBe(Tool.MOVE); - } + type("all"); + expect(noticeLine()).toBe("2개를 찾음, 총 2개"); + expect(promptLine()).toBe("객체 선택:"); + expect(activeTool()).toBe(Tool.MOVE); + type("l"); + expect(noticeLine()).toBe("1개를 찾음, 총 2개"); + expect(promptLine()).toBe("객체 선택:"); + type("p"); + expect(noticeLine()).toMatch(/^\d+개를 찾음, 총 2개$/); + expect(promptLine()).toBe("객체 선택:"); + enter(); + expect(activeTool()).toBe(Tool.MOVE); + expect(promptLine()).toBe("기준점 지정 또는 [변위(D)] <변위>:"); }); const selectTakesKeywords = !!( diff --git a/ui_template/ui_template_locale_common.ts b/ui_template/ui_template_locale_common.ts index f78b3f6c8..b2035a21e 100644 --- a/ui_template/ui_template_locale_common.ts +++ b/ui_template/ui_template_locale_common.ts @@ -121,8 +121,8 @@ export const ui_locales_common = { App_Settings_Rule_Pan: ["화면 옮기기 = 가운데 단추 끌기", "Pan = drag with the middle button"], App_Settings_Rule_Pick: ["고르기 = 왼쪽 단추", "Select = left button"], App_Settings_Rule_Right: [ - "오른쪽 단추 = 메뉴 · CAD 에서는 Enter", - "Right button = menu · Enter in CAD", + "오른쪽 단추 = 메뉴 · CAD 에서는 명령 중 Enter · 쉴 때 반복 · Shift = 스냅 메뉴 · CAD 휠 두 번 = 범위", + "Right button = menu · in CAD Enter during a command · repeat when idle · Shift = snap menu · CAD wheel double-click = extents", ], App_Settings_Rule_Pinch: [ "트랙패드 · 손가락 벌림 = 늘 확대(휠 방향과 상관없음)", @@ -164,8 +164,8 @@ export const ui_locales_common = { ], App_Settings_Keys_Cad: ["CAD", "CAD"], App_Settings_Keys_Cad_Keys: [ - "F3 스냅 · F7 그리드 · F8 직교 · F10 45° · Ctrl+Z · Ctrl+Y · Ctrl+A · Ctrl+S(M02) · Delete · Esc · Enter · Space = Enter · 방향키 = 화면 옮김 · 글자 = 명령", - "F3 snap · F7 grid · F8 ortho · F10 45° · Ctrl+Z · Ctrl+Y · Ctrl+A · Ctrl+S (M02) · Delete · Esc · Enter · Space = Enter · arrows = pan · letters = command", + "F3 스냅 · F7 그리드 · F8 직교 · F10 45° · Ctrl+Z · Ctrl+Y · Ctrl+A · Ctrl+S(M02) · Delete · Esc · Enter · Space = Enter · ↑↓ = 기록 · 최근 값 · Ctrl+C · X · 1 · ← → · Shift+↑↓ = 화면 옮김 · 글자 = 명령", + "F3 snap · F7 grid · F8 ortho · F10 45° · Ctrl+Z · Ctrl+Y · Ctrl+A · Ctrl+S (M02) · Delete · Esc · Enter · Space = Enter · ↑↓ = history · recent values · Ctrl+C · X · 1 · ← → · Shift+↑↓ = pan · letters = command", ], App_Settings_Keys_Table: ["B08 · B09 표", "B08 · B09 tables"], App_Settings_Keys_Table_Keys: [ From d9cc66b5e9f2bf8381cc157222a4706bb31fb190 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Fri, 16 Oct 2026 01:03:02 +0900 Subject: [PATCH 77/81] =?UTF-8?q?fix(CAD):=20339=20=EB=8D=A7=EA=B3=A0?= =?UTF-8?q?=EC=B9=A8=20=E2=80=94=20CAD=20=EC=8B=9C=EC=9E=91=20=EC=83=81?= =?UTF-8?q?=ED=83=9C=EB=A5=BC=20=E3=80=8C=EB=AA=85=EB=A0=B9:=E3=80=8D=20?= =?UTF-8?q?=EC=89=BC(=EC=84=A0=ED=83=9D=20=EB=8F=84=EA=B5=AC)=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=C2=B7=20=EC=84=A0=20=EB=AA=85=EB=A0=B9=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EC=8B=9C=EC=9E=91=ED=95=98=EB=8D=98=20=EA=B2=83=20?= =?UTF-8?q?=EA=B3=A0=EC=B9=A8=20=C2=B7=20=EC=8B=9C=EC=9E=91=20=EB=8F=84?= =?UTF-8?q?=EA=B5=AC=20=EC=8B=9C=ED=97=98?= 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_01QfHKQs7J3hQELKMWQMUJEy --- B07_DesignDetail/openwebcad/src/main.tsx | 5 ++--- resources/tester/cad/prompt-frame.test.ts | 20 ++++++++++++++++++++ 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/B07_DesignDetail/openwebcad/src/main.tsx b/B07_DesignDetail/openwebcad/src/main.tsx index abf4f4d7b..9e4b392d7 100644 --- a/B07_DesignDetail/openwebcad/src/main.tsx +++ b/B07_DesignDetail/openwebcad/src/main.tsx @@ -174,9 +174,8 @@ function initApplication() { startDrawLoop(screenCanvasDrawController, 0); - const lineToolActor = new Actor(TOOL_STATE_MACHINES[Tool.LINE]); - lineToolActor.start(); - setActiveToolActor(lineToolActor); + // 오토캐드처럼 「명령:」 쉼(선택 도구)으로 시작 + setActiveToolActor(new Actor(TOOL_STATE_MACHINES[Tool.SELECT])); } } diff --git a/resources/tester/cad/prompt-frame.test.ts b/resources/tester/cad/prompt-frame.test.ts index 986366abc..e5005bf8b 100644 --- a/resources/tester/cad/prompt-frame.test.ts +++ b/resources/tester/cad/prompt-frame.test.ts @@ -3,6 +3,7 @@ * 설계 `docs/raw/조사/2026-10-15_337_CAD오토캐드조작_설계.md` 2 장 · 6-3 가 줄. * 실행: cd B07_DesignDetail/openwebcad && npx vitest run --dir ../../resources/tester/cad prompt-frame */ +import { readFileSync } from "node:fs"; import { beforeEach, describe, expect, it, vi } from "vitest"; vi.mock("react-toastify", () => ({ @@ -21,6 +22,7 @@ import { remember, renderPrompt, } from "../../../B07_DesignDetail/openwebcad/src/commands/prompt"; +import { TOOL_STATE_MACHINES } from "../../../B07_DesignDetail/openwebcad/src/commands/registry"; import { getLastCommandId, runCommandInput, @@ -418,6 +420,24 @@ describe("엔진 v2 — loopFrom · when · mode · next · escCommits · undo }); describe("쉼 · 명령 이름 · 별칭(2-5 ⑨ · 2-17 · N18)", () => { + it("앱 시작 = 쉼(선택 도구) — main.tsx 가 켜는 도구 · 「l ⏎」 = 첫 번째 점 지정", () => { + const main = readFileSync( + new URL("../../../B07_DesignDetail/openwebcad/src/main.tsx", import.meta.url), + "utf8", + ); + const started = [...main.matchAll(/TOOL_STATE_MACHINES\[Tool\.(\w+)\]/g)].map((m) => m[1]); + expect(started).toEqual(["SELECT"]); + start(TOOL_STATE_MACHINES[Tool.SELECT]); + expect(activeTool()).toBe(Tool.SELECT); + const idle = promptLine(); + expect(idle).not.toBe("첫 번째 점 지정:"); + typeEnter("l"); + expect(activeTool()).toBe(Tool.LINE); + expect(promptLine()).toBe("첫 번째 점 지정:"); + key("Escape"); + expect(promptLine()).toBe(idle); + }); + it("딱 맞음만 — R · LE · CIR · SE = 알 수 없는 명령 · l = LINE · ed = TEXTEDIT", () => { for (const text of ["r", "le", "cir", "se"]) { type(text); From 7f33ffe2bd5a8bbbf62fd78ebb0c32d1d4605d0f Mon Sep 17 00:00:00 2001 From: umsangdon Date: Fri, 16 Oct 2026 01:09:47 +0900 Subject: [PATCH 78/81] =?UTF-8?q?fix(M02=20=EC=8B=A4=EB=AC=B4=EC=9E=90?= =?UTF-8?q?=EB=A3=8C):=20339=20=EB=8D=A7=EA=B3=A0=EC=B9=A8=20=E2=80=94=20?= =?UTF-8?q?=ED=99=95=EC=9D=B8=20=EC=B0=BD=20=ED=91=9C=20=EA=BC=B4=20=C2=B7?= =?UTF-8?q?=20=EB=A7=8C=EB=93=A4=EA=B8=B0=20=EB=92=A4=20=EC=82=AC=EC=A0=84?= =?UTF-8?q?=EC=A1=B0=ED=95=A9=20=EB=AA=A9=EB=A1=9D=20=EA=B0=B1=EC=8B=A0=20?= =?UTF-8?q?=C2=B7=20=EC=97=91=EC=85=80=20=EB=B3=B4=EA=B8=B0=20=EC=B0=BD=20?= =?UTF-8?q?=EC=A0=9C=EC=95=88=20=EC=88=98=20=EB=8B=A4=EC=8B=9C=20=EC=9D=BD?= =?UTF-8?q?=EC=9D=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 확인 창 줄 표: 한글 낱말 단위 줄바꿈 · 품명 · 규격 최소 폭 · 옵션 · 품 비교 항목마다 한 줄 · 단추 칸 세로 쌓음(좁게) - 별칭 장부 표: 품명 · 규격 같은 손질 - 만들기(확인 창 · 엑셀 보기 창 일괄) 뒤 document 알림 → 좌측 「일위대가 사전조합」 목록 다시 읽음 - 엑셀 보기 창: 뒤 셈 중이면 3 초마다 목록 「제안 n/m」 다시 읽음 · 펼침 표 숫자 판정 칸 = 맞음 n · 다른 점 n - 품 비교 판정 함수 · 서버 그대로 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01XWh5wNNycMkbwMpACz1rte --- .../M02_MasterTemplete_Hopyo.css | 54 +++++++++++++------ .../M02_MasterTemplete_Hopyo_Api.ts | 2 + .../M02_MasterTemplete_Hopyo_Check.ts | 13 +++-- .../M02_MasterTemplete_Hopyo_Ledger.ts | 2 +- .../M02_MasterTemplete_Hopyo_Sheet.ts | 28 ++++++++-- .../M02_MasterTemplete_UI_Side.ts | 7 +++ resources/tester/test_m02_hopyo_screen.py | 26 +++++++++ 7 files changed, 110 insertions(+), 22 deletions(-) diff --git a/M02_MasterTemplete/M02_MasterTemplete_Hopyo.css b/M02_MasterTemplete/M02_MasterTemplete_Hopyo.css index 9229291f9..21202d055 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Hopyo.css +++ b/M02_MasterTemplete/M02_MasterTemplete_Hopyo.css @@ -49,29 +49,53 @@ font-variant-numeric: tabular-nums; } -/* 확인 창 줄 표(338) — 체크 · 갈래 · 수량 · 단위 · 상태 · 단추 = 줄바꿈 없음(글자가 세로로 안 갈라짐 · - 폭 = 글 폭) · 품명 · 규격 · 우리 로직 · 옵션 · 품 비교가 남는 폭 · 칸 여백 좁힘(공용 표보다 앞서게 두 갈래 이름) */ +/* 실무자료 표 — 한글은 낱말 단위로만 줄바꿈(두세 글자마다 안 갈라짐) · 너무 긴 낱말만 쪼갬 */ +.m02-hopyo__table :is(th, td) { + word-break: keep-all; + overflow-wrap: break-word; +} + +/* 확인 창 줄 표(338 · 339) — 체크 · 갈래 · 수량 · 단위 · 상태 = 줄바꿈 없음(폭 = 글 폭) · 단추 = 세로로 쌓아 좁게 · + 품명 · 규격 · 우리 로직 · 옵션 · 품 비교가 남는 폭 · 칸 여백 좁힘(공용 표보다 앞서게 두 갈래 이름) */ .m02-hopyo__table.m02-hopyo__lines :is(th, td) { padding: var(--spacing-8); } .m02-hopyo__table.m02-hopyo__lines - :is(th, td):is( - :nth-child(1), - :nth-child(2), - :nth-child(5), - :nth-child(6), - :nth-child(7), - :nth-child(11) - ) { + :is(th, td):is(:nth-child(1), :nth-child(2), :nth-child(5), :nth-child(6), :nth-child(7)) { white-space: nowrap; } -/* 옵션 · 품 비교 — 길면 줄임표(전체 글은 칸 풍선 도움말) */ +/* 품명 · 규격 — 낱말 두셋은 한 줄에 */ +.m02-hopyo__table.m02-hopyo__lines td:nth-child(3), +.m02-hopyo-ledger__table td:nth-child(1) { + min-width: 7em; +} + +.m02-hopyo__table.m02-hopyo__lines td:nth-child(4), +.m02-hopyo-ledger__table td:nth-child(2) { + min-width: 6em; +} + +/* 옵션 · 품 비교 — 항목마다 한 줄(글의 줄바꿈 그대로) */ .m02-hopyo__table.m02-hopyo__lines td:is(:nth-child(9), :nth-child(10)) { - max-width: 24em; - overflow: hidden; - text-overflow: ellipsis; + min-width: 9em; + white-space: pre-line; +} + +/* 단추 칸 — 단추 · 넣음/뺌을 세로로 쌓아 칸 폭 = 가장 긴 단추 */ +.m02-hopyo__table.m02-hopyo__lines td.m02-hopyo__acts { + width: 1%; +} + +.m02-hopyo__lines .m02-hopyo__acts > * { + display: flex; + width: 100%; + white-space: nowrap; +} + +.m02-hopyo__lines .m02-hopyo__acts > * + * { + margin: var(--spacing-4) 0 0; } .m02-hopyo__line.is-out { @@ -117,7 +141,7 @@ /* 우리 로직 칸 — 대상 여럿 = 칸 안 여러 줄(이름 · 키 · 몫 · 배 칸 · [옵션]) */ .m02-hopyo__parts { - min-width: 14em; + min-width: 11em; } .m02-hopyo__part { diff --git a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Api.ts b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Api.ts index 9cd7fc52e..818aec359 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Api.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Api.ts @@ -58,6 +58,8 @@ export interface HopyoView extends HopyoFile { 검산: { 어긋남: string[]; 총괄표: number }; 호표: HopyoBrief[]; 맞춤: [number, number]; + /** 뒤 숫자 판정 셈(끝나면 맞춤 수가 바뀜) */ + 셈?: HopyoCalc; } /** 후보가 단가산출 근거와 맞는 것(330-2) — 맞은 조항 글 · 겹친 직종 · 단위 같음 · 점수 · 까닭 꼬리표 */ diff --git a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Check.ts b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Check.ts index 264872dbf..1625fec29 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Check.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Check.ts @@ -49,6 +49,8 @@ import { openLaborInput } from "./M02_MasterTemplete_Labor_Input"; import "./M02_MasterTemplete_Hopyo.css"; export const POLL_MS = 3000; +/** 사전조합을 만들었음(document 알림) — 좌측 「일위대가 사전조합」 목록 다시 */ +export const HOPYO_BUILT = "m02-hopyo-built"; const OUT = ["우리에 없음", "안 씀"] as const; /** 확인 창 캐시 — 사용자가 만진 것만(서버 제안 위에 덮어 보임) */ @@ -420,7 +422,7 @@ export function buildHopyoCheck(opt: CheckOptions): HTMLElement { const tags = [m.글, ...(m.꼬리표 ?? []), sourceText(line.근거)].filter(Boolean).join("\n"); const opts = parts .filter((p) => p.로직) - .map((p) => inputsText(p.입력값 ?? {}, p.못읽음 ?? [], p.출처)) + .flatMap((p) => inputsText(p.입력값 ?? {}, p.못읽음 ?? [], p.출처).split(/, (?=[^,=]+=)| · /)) .filter(Boolean); if (m.갈리는칸?.length) opts.push(`⚠ 못 가름: ${m.갈리는칸.join(", ")}`); const alias = m.별칭?.규격다름 ? " · 규격 다름" : m.별칭?.근거다름 ? " · 근거 다름" : ""; @@ -444,8 +446,12 @@ export function buildHopyoCheck(opt: CheckOptions): HTMLElement { ], }), partCell(line), - td(opts.join(" / ")), - td(compareText(d.품[line.차례] ?? m.품)), + td(opts.join("\n")), // 옵션 · 품 비교 = 항목마다 한 줄 + td( + compareText(d.품[line.차례] ?? m.품) + .split(" · ") + .join("\n"), + ), el("td", { className: "m02-hopyo__acts", children: acts }), ], }); @@ -481,6 +487,7 @@ export function buildHopyoCheck(opt: CheckOptions): HTMLElement { built = res; const miss = res.빠진줄.length; showToast(`${res.참조.키} ${builtWord(res)}${miss ? ` — 빠진 줄 ${miss}` : ""}`, "success"); + document.dispatchEvent(new Event(HOPYO_BUILT)); opt.onBuilt?.(res, h); await load(true); } catch (error) { diff --git a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Ledger.ts b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Ledger.ts index 73dd80f8c..f245bdb4d 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Ledger.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Ledger.ts @@ -326,7 +326,7 @@ export function buildHopyoLedger(opt: LedgerOptions): HTMLElement { className: "ui-general-block__table-wrap ui-general-block__table-wrap--sticky-head", children: [ el("table", { - className: "ui-general-block__table m02-hopyo__table", + className: "ui-general-block__table m02-hopyo__table m02-hopyo-ledger__table", children: [ el("thead", { children: [ diff --git a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Sheet.ts b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Sheet.ts index ab8f851b5..e9b7bef0a 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Sheet.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Sheet.ts @@ -1,9 +1,10 @@ /* ============================================================================= * M02_MasterTemplete_Hopyo_Sheet.ts * 일위대가 실무자료 엑셀 보기 창(PLAN 329 · 338-3) — 큰 모달 · 왼쪽 = 올린 엑셀을 연 것처럼 읽기 전용 시트(A00 산출근거 - * 스프레드시트 · 탭 일위대가 · 단가산출) · 오른쪽 = 호표 목록(체크 · 찾기 · 제안 n/m) + [고른 n개 확인하고 만들기]. + * 스프레드시트 · 탭 일위대가 · 단가산출) · 오른쪽 = 호표 목록(체크 · 찾기 · 제안 n/m — 뒤 셈 중이면 3 초마다 다시) + * + [고른 n개 확인하고 만들기]. * · 목록 누름 → 시트에서 그 호표 영역(B열 ~ 끝 열) 고름 · 칸 누름 → 그 행 호표를 목록에서 강조 · 영역 고름 - * · ① [고른 n개 확인하고 만들기] = 고른 호표마다 서버 보기 → 펼침 표(넣을 줄 = 체크 제안 · 빠질 줄 · 셈 중 · + * · ① [고른 n개 확인하고 만들기] = 고른 호표마다 서버 보기 → 펼침 표(넣을 줄 = 체크 제안 · 빠질 줄 · 셈 중 또는 맞음 · 다른 점 수 · * [확인 창 열기]) ② [이대로 n개 만들기] = 하위 호표부터 차례로 `/hopyo-build`(몸 = 그 호표 체크 줄) — * 펼쳐 보이고 한 번 누름 = 사용자 확인 · 결과 표(사전조합 키 · 빠진 줄 · 값 차이) · * `onImport` 가 있으면 호표마다 알림([로직 추가] 참조 줄) @@ -32,14 +33,17 @@ import { type HopyoOne, type HopyoScope, type HopyoSpan, + type HopyoView, } from "./M02_MasterTemplete_Hopyo_Api"; import { bodyOf, draftKey, + HOPYO_BUILT, leftOut, loadDraft, openHopyoCheck, partsOf, + POLL_MS, toTarget, } from "./M02_MasterTemplete_Hopyo_Check"; import { builtWord } from "./M02_MasterTemplete_Hopyo_Choose"; @@ -239,6 +243,18 @@ export function openHopyoSheet(opt: HopyoSheetOptions): void { }); } + /** 뒤 숫자 판정 셈 중이면 3 초 뒤 목록 「제안 n/m」 다시 읽음 — 셈 끝 · 창 닫음 · 못 읽음이면 멈춤 */ + function poll(file: HopyoView): void { + if (!file.셈 || file.셈.끝) return; + window.setTimeout(async () => { + const next = closed ? null : await readHopyo(opt.scope, opt.key).catch(() => null); + if (!next || closed) return; + briefs = next.호표; + paintList(); + poll(next); + }, POLL_MS); + } + async function load(): Promise { try { const [file, grid] = await Promise.all([ @@ -249,6 +265,7 @@ export function openHopyoSheet(opt: HopyoSheetOptions): void { briefs = file.호표; spans = grid instanceof Error ? [] : grid.호표; paintList(); + poll(file); if (grid instanceof Error) return void left.replaceChildren(note(grid.message)); await mountSheet(grid); if (opt.단산행 !== undefined) sheet?.select(rowsText(opt.단산행), DANSAN); @@ -300,7 +317,11 @@ export function openHopyoSheet(opt: HopyoSheetOptions): void { const h = one.호표; const d = loadDraft(draftKey(opt.scope, opt.key, h.번)); const out = Object.entries(leftOut(h.줄, d)); - const calc = h.셈 && !h.셈.끝 ? `셈 중 ${h.셈.한}/${h.셈.모두}` : ""; + const tally = (s: string): number => h.줄.filter((l) => l.맞춤.상태 === s).length; + const calc = + h.셈 && !h.셈.끝 + ? `셈 중 ${h.셈.한}/${h.셈.모두}` + : `맞음 ${tally("맞음")} · 다른 점 ${tally("다른 점")}`; const brief = briefs.find((x) => x.번 === h.번) ?? h; return el("tr", { attrs: { "data-review": String(h.번) }, @@ -395,6 +416,7 @@ export function openHopyoSheet(opt: HopyoSheetOptions): void { } finally { hideLoadingOverlay(); } + if (ok) document.dispatchEvent(new Event(HOPYO_BUILT)); result.replaceChildren(table(["호표", "사전조합", "빠진 줄", "값 차이(참고)"], rows)); showToast(`만듦 ${ok} / ${order.length}`, ok === order.length ? "success" : "warning"); } diff --git a/M02_MasterTemplete/M02_MasterTemplete_UI_Side.ts b/M02_MasterTemplete/M02_MasterTemplete_UI_Side.ts index c6d835e7a..41914016b 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_UI_Side.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_UI_Side.ts @@ -32,6 +32,7 @@ import { import { STRUCTURE_TABLE } from "./M02_MasterTemplete_Structure"; import { buildOptionsSection } from "./StructOptions/M02_SO_Options"; import { buildPracticeSection } from "./M02_MasterTemplete_Practice"; +import { HOPYO_BUILT } from "./M02_MasterTemplete_Hopyo_Check"; import { buildHopyoSection } from "./M02_MasterTemplete_Hopyo_Side"; import { canEdit, type Selection } from "./M02_MasterTemplete_UI_Main"; import { @@ -150,6 +151,12 @@ export function buildSide(opt: SideOptions): SideHandle { getOpen: opt.getOpen, }); const precomboSec = section(PRECOMBO_TITLE, precombo.body); + // 실무자료로 사전조합을 만들었음(확인 창 · 엑셀 보기 창) → 목록 다시 · 이 패널이 내려갔으면 듣기 끝 + const onBuilt = (): void => + precombo.body.isConnected + ? void precombo.refresh() + : document.removeEventListener(HOPYO_BUILT, onBuilt); + document.addEventListener(HOPYO_BUILT, onBuilt); // 구조물 옵션 정의 · 도면 양식은 시스템 층(마스터 템플릿)에만 · 정의 고쳐 씀은 시스템 관리자만 · 다른 역할은 읽기만(PLAN 48-2) const extras = layer === "system" diff --git a/resources/tester/test_m02_hopyo_screen.py b/resources/tester/test_m02_hopyo_screen.py index 547ba5573..978b8e679 100644 --- a/resources/tester/test_m02_hopyo_screen.py +++ b/resources/tester/test_m02_hopyo_screen.py @@ -368,3 +368,29 @@ def test_실물_응답_대역_확인_창(): assert [x["차례"] for x in got["body"]] == [x["차례"] for x in lines if x["맞춤"]["체크"]] assert all({"로직", "입력값", "배"} <= set(t) for x in got["body"] for t in x["대상"]) assert got["sure"] == [x["차례"] for x in lines if x["맞춤"]["상태"] == "맞음"] + + +def test_확인_창_손질_339(): + """339 덧고침 — ① 줄 표 꼴(한글 낱말 줄바꿈 · 옵션 · 품 비교 한 줄씩 · 단추 세로) · 장부 품명 · 규격 폭 + ② 만들기 뒤 좌측 사전조합 목록 다시 ③ 엑셀 보기 창 목록 셈 끝까지 다시 읽음 · 펼침 표 숫자 판정 칸.""" + css = _text("M02_MasterTemplete_Hopyo.css") + assert "word-break: keep-all;" in css and "white-space: pre-line;" in css + assert ( + ".m02-hopyo__lines .m02-hopyo__acts > *" in css + and "text-overflow" + not in css.split(".m02-hopyo__lines td:is(:nth-child(9)")[-1].split("}")[0] + ) + assert ".m02-hopyo-ledger__table td:nth-child(1)" in css + assert "m02-hopyo-ledger__table" in _text("M02_MasterTemplete_Hopyo_Ledger.ts") + check = _text("M02_MasterTemplete_Hopyo_Check.ts") + assert r'td(opts.join("\n"))' in check and r'.split(" · ")' in check + assert r'.join("\n"),' in check # 품 비교 = 항목마다 한 줄(판정 글 함수는 그대로) + make = check.split("async function make(")[1].split("function builtBlock(")[0] + assert make.index("document.dispatchEvent(new Event(HOPYO_BUILT));") < make.index("opt.onBuilt") + side = _text("M02_MasterTemplete_UI_Side.ts") + assert "document.addEventListener(HOPYO_BUILT, onBuilt);" in side + assert "void precombo.refresh()" in side.split("const onBuilt")[1].split(";")[0] + sheet = _text("M02_MasterTemplete_Hopyo_Sheet.ts") + assert "poll(file);" in sheet and "poll(next);" in sheet and "POLL_MS" in sheet + assert "if (ok) document.dispatchEvent(new Event(HOPYO_BUILT));" in sheet + assert '`맞음 ${tally("맞음")} · 다른 점 ${tally("다른 점")}`' in sheet From ca5485b208f4d47f7ff85bd32c071d19ee039427 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Fri, 16 Oct 2026 01:13:13 +0900 Subject: [PATCH 79/81] =?UTF-8?q?fix(M02=20=EC=8B=A4=EB=AC=B4=EC=9E=90?= =?UTF-8?q?=EB=A3=8C):=20339=20=EB=8D=A7=EA=B3=A0=EC=B9=A8=202=20=E2=80=94?= =?UTF-8?q?=20=EB=B3=B8=20=EC=B9=B8=20=ED=98=B8=ED=91=9C=20=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D=20=EC=A0=9C=EC=95=88=20=EC=88=98=20=C2=B7=20=EC=85=88?= =?UTF-8?q?=20=EC=A4=91=20=ED=91=9C=EC=8B=9C=20=EC=85=88=20=EB=81=9D?= =?UTF-8?q?=EA=B9=8C=EC=A7=80=20=EB=8B=A4=EC=8B=9C=20=EC=9D=BD=EC=9D=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 본 칸 호표 표: 뒤 숫자 판정 셈 중이면 3 초마다 다시 읽어 「제안 n / 줄 m」 · 호표 줄 갱신(찾기 글 그대로) - 머리 줄 「숫자 판정 셈 중 n/m」 표시 · 셈 끝 · 호표 하나로 감 · 못 읽음이면 멈춤 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01XWh5wNNycMkbwMpACz1rte --- .../M02_MasterTemplete_Hopyo_View.ts | 34 +++++++++++++++++-- resources/tester/test_m02_hopyo_screen.py | 11 ++++++ 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_View.ts b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_View.ts index b7f58f5e4..433c83713 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_View.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_View.ts @@ -14,7 +14,7 @@ import { type HopyoScope, type HopyoView, } from "./M02_MasterTemplete_Hopyo_Api"; -import { buildHopyoCheck } from "./M02_MasterTemplete_Hopyo_Check"; +import { buildHopyoCheck, POLL_MS } from "./M02_MasterTemplete_Hopyo_Check"; import type { HopyoSheetOptions } from "./M02_MasterTemplete_Hopyo_Sheet"; import "./M02_MasterTemplete_Hopyo.css"; @@ -79,6 +79,7 @@ export function buildHopyoView(key: string, opt: HopyoViewOptions): HTMLElement () => void drawFile(), ); if (!file) return; + let hopyos = file.호표; const search = el("input", { className: "ui-input ui-input--sm m02-hopyo__search", attrs: { type: "search", placeholder: "호표 · 품명 · 규격 찾기", value: query }, @@ -88,7 +89,7 @@ export function buildHopyoView(key: string, opt: HopyoViewOptions): HTMLElement const q = search.value.trim().toLowerCase(); query = search.value; body.replaceChildren( - ...file.호표 + ...hopyos .filter((h) => !q || `제${h.번}호표 ${h.이름} ${h.규격}`.toLowerCase().includes(q)) .map((h) => { const tr = el("tr", { @@ -110,6 +111,32 @@ export function buildHopyoView(key: string, opt: HopyoViewOptions): HTMLElement }; search.addEventListener("input", paint); paint(); + // 뒤 숫자 판정 셈 중이면 3 초마다 다시 읽어 「제안 n / 줄 m」 갱신 — 셈 끝 · 다른 화면 · 못 읽음이면 멈춤 + const ready = note(""); + const calc = note(""); + const show = (f: HopyoView): void => { + ready.textContent = `제안 ${f.맞춤[0]} / 줄 ${f.맞춤[1]}`; + const c = f.셈 && !f.셈.끝 ? f.셈 : null; + calc.textContent = c + ? `숫자 판정 셈 중 ${c.한}/${c.모두 ?? "?"}${c.까닭 ? ` · ${c.까닭}` : ""}` + : ""; + calc.hidden = !c; + }; + const my = nav; + const live = (): boolean => my === nav && view.isConnected; + const poll = (f: HopyoView): void => { + if (!f.셈 || f.셈.끝) return; + window.setTimeout(async () => { + const next = live() ? await readHopyo(opt.scope, key).catch(() => null) : null; + if (!next || !live()) return; + hopyos = next.호표; + show(next); + paint(); + poll(next); + }, POLL_MS); + }; + show(file); + poll(file); const wrong = file.검산.어긋남; view.replaceChildren( el("div", { @@ -119,7 +146,8 @@ export function buildHopyoView(key: string, opt: HopyoViewOptions): HTMLElement note(`시트 ${file.시트}`), note(`호표 ${file.호표수}`), note(wrong.length ? `검산 어긋남 ${wrong.length}` : "검산 맞음"), - note(`제안 ${file.맞춤[0]} / 줄 ${file.맞춤[1]}`), + ready, + calc, createButton({ label: "엑셀로 보기", size: "sm", diff --git a/resources/tester/test_m02_hopyo_screen.py b/resources/tester/test_m02_hopyo_screen.py index 978b8e679..99c3c9d5e 100644 --- a/resources/tester/test_m02_hopyo_screen.py +++ b/resources/tester/test_m02_hopyo_screen.py @@ -394,3 +394,14 @@ def test_확인_창_손질_339(): assert "poll(file);" in sheet and "poll(next);" in sheet and "POLL_MS" in sheet assert "if (ok) document.dispatchEvent(new Event(HOPYO_BUILT));" in sheet assert '`맞음 ${tally("맞음")} · 다른 점 ${tally("다른 점")}`' in sheet + + +def test_본_칸_호표_목록_셈_끝까지_다시_읽음_339(): + """339 덧고침 2 — 본 칸 호표 표 「제안 n / 줄 m」 · 머리 「셈 중」 — 셈 중이면 3 초마다 다시 읽음.""" + view = _text("M02_MasterTemplete_Hopyo_View.ts") + assert "poll(file);" in view and "poll(next);" in view and "POLL_MS" in view + assert "hopyos = next.호표;" in view and "...hopyos" in view + assert ( + "const live = (): boolean => my === nav && view.isConnected;" in view + ) # 호표 하나로 가면 멈춤 + assert "숫자 판정 셈 중" in view From 912575d9bb059d9c2de8fae9d2687448186aaec3 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Fri, 16 Oct 2026 01:32:53 +0900 Subject: [PATCH 80/81] =?UTF-8?q?fix(=EC=9D=BC=EC=9C=84=EB=8C=80=EA=B0=80?= =?UTF-8?q?=EC=8B=A4=EB=AC=B4):=20339=20=EB=8D=A7=EA=B3=A0=EC=B9=A8=20?= =?UTF-8?q?=EB=A7=9E=EC=B6=A4=20=EC=85=8B=20=E2=80=94=20=EA=B7=9C=EA=B2=A9?= =?UTF-8?q?=20=EB=8B=A4=EB=A5=B8=20=EB=B3=84=EC=B9=AD=20=EA=BC=AC=EB=A6=AC?= =?UTF-8?q?=ED=91=9C=EB=A7=8C=20=C2=B7=20=EA=B0=99=EC=9D=80=20=EC=9E=A5?= =?UTF-8?q?=EB=B9=84=20=EC=97=AC=EB=9F=AC=20=EC=A4=84=20=ED=92=88=20?= =?UTF-8?q?=EB=B9=84=EA=B5=90=20=ED=95=A9=EC=B9=A8=20=C2=B7=20=EA=B7=BC?= =?UTF-8?q?=EA=B1=B0=20=EC=95=88=20=EB=8B=BF=EC=9D=80=20=EB=8D=A7=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=ED=9B=84=EB=B3=B4=EB=A1=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 규격 다른 별칭(품명|단위 느슨 열쇠) = 꼬리표 「별칭 · 규격 다름」 · 짝은 후보 끝 덤 · 상태 · 옵션은 줄 숫자 맞춤 - 품 비교 같은 갈래 장비 여러 줄은 합쳐 견줌(서버 _qty · 화면 compareText) - 조합 덧 로직 = 단위 맞음 + 조항 · 직종 · 이름 하나 닿을 때만 · 아니면 덧 후보 · 섞인 줄 그대로 · 맞춤셈 판 2 - 골든 사람 126 1순위 118 · 다섯 안 122 · 맞음 57 틀림 0 · 조합 62/83 그대로 · 338 대상 시험 142 통과 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Ji4nuyLVHcxrXWHTdb5nRD --- M02_MasterTemplete/M02_Hopyo_Combine.py | 49 +++++++++++---- M02_MasterTemplete/M02_Hopyo_Fit.py | 2 +- M02_MasterTemplete/M02_Hopyo_Line.py | 60 ++++++++++++++----- M02_MasterTemplete/M02_Hopyo_Verdict.py | 10 +++- .../M02_MasterTemplete_Hopyo_Check.ts | 12 ++-- resources/tester/helper_m02_hopyo_check.cjs | 1 + resources/tester/test_m02_hopyo_fit.py | 8 +++ resources/tester/test_m02_hopyo_match.py | 23 ++++++- resources/tester/test_m02_hopyo_screen.py | 9 ++- 9 files changed, 136 insertions(+), 38 deletions(-) diff --git a/M02_MasterTemplete/M02_Hopyo_Combine.py b/M02_MasterTemplete/M02_Hopyo_Combine.py index 25f3c4450..6537cca0b 100644 --- a/M02_MasterTemplete/M02_Hopyo_Combine.py +++ b/M02_MasterTemplete/M02_Hopyo_Combine.py @@ -6,6 +6,8 @@ +0.1. 덧 로직 = 1순위가 못 덮는(< COVER) 절은 절 1위(≥ 0.75) · 아니면 몫마다(≥ 0.6) · 장비만 든 몫 = 「기계 시간당 기계경비」(체크 안 함 · 시간 = 엑셀 값). +덧은 근거가 닿을 때만(`reached` — 단위 맞음 + 조항 · 직종(보통인부 뺌) · 이름 하나) · 아니면 덧 +안 붙임(후보로만) · 섞인 줄(몫 먼저)은 그대로. 순위 덤 = 앞 차례 0.3 · 인용 조항 같음 0.3 · 같은 책 0.1 · 후보 풀 = 앞 차례 15 + 드문 직종 · 장비 색인 (40 개 이하) + 절 제목 이름 후보 8. 우리 벡터 = `M02_Hopyo_Ours` 지문 변형 L · X(따로 안 셈). @@ -51,7 +53,8 @@ def _load() -> dict[str, Any]: if var[o["로직"]]: for d in dims(var[o["로직"]][0][1]): idx.setdefault(d, set()).add(o["로직"]) - _STATE.update(판=(ver, id(table)), 변형=var, 색인=idx) + trades = {o["로직"]: set(o.get("직종") or ()) for o in table} + _STATE.update(판=(ver, id(table)), 변형=var, 색인=idx, 직종=trades) return _STATE @@ -217,11 +220,28 @@ def machine_of(E: dict[str, Any]) -> str: return MACHINE["굴착기"] if kinds == {frozenset({"굴착기"})} else MACHINE_ANY +def reached(code: str, line: dict, where: Any, E: dict, prior: Prior, got: dict) -> bool: + """덧 근거 닿음 — 단위 맞음 + (인용 조항 같음 · 직종 겹침(보통인부 뺌) · 이름 낱말 겹침).""" + one = next((x for x in got["로직"] if x["로직"] == code), None) + if one is None or not fit.unit_fit(line.get("단위"), one["단위"]): + return False + our = prior.table.get(code) + theirs = prior.theirs + hopyo_basis.sources(str(where or "")) + if our and theirs and hopyo_match.clause(theirs, our)[0] >= 0.9: + return True + if (set(E.get("노임") or {}) - {COMMON}) & _load()["직종"].get(code, set()): + return True + loose = lambda t: hopyo_basis._loose_words(t) - hopyo_match._GENERIC # noqa: E731 + mine, theirs_w = loose(f"{where or ''} {line.get('이름') or ''}"), loose(one["이름"]) + return any(a in b or b in a for a in mine for b in theirs_w) + + # ── 풀이 ─────────────────────────────────────────────────────────────── def combine( primary: str | None, parts: list[dict], line: dict, got: dict, base: list[str] ) -> list[dict[str, Any]]: - """[{로직, 어디, 맞음, 몫, 단계, 입력}] — 1순위 + 1순위가 못 덮는 절 · 몫의 로직.""" + """[{로직, 어디, 맞음, 몫, 단계, 입력}] — 1순위 + 1순위가 못 덮는 절 · 몫의 로직 · 근거 안 닿은 + 덧은 단계 「후보」(조합에 안 듦).""" one = lambda c, w, f, x, step, g=None: { # noqa: E731 "로직": c, "어디": w, @@ -257,11 +277,12 @@ def combine( pool = pool_for(E, base, title_cands(title, unit, got)) cand = ranked(E, pool, prior, share, _hint(title)) if cand and cand[0][1] >= fit.COMBO[1] and cand[0][3] not in have: - chosen.append( - one(cand[0][3], title, round(cand[0][1], 3), cand[0][2], "절", cand[0][4]) - ) + ok = mixed or reached(cand[0][3], line, title, E, prior, got) + f, x, c, g = round(cand[0][1], 3), cand[0][2], cand[0][3], cand[0][4] + chosen.append(one(c, title, f, x, "절" if ok else "후보", g)) have.add(cand[0][3]) - continue + if ok: + continue for p in ps: if primary and cover(p, primary) >= fit.COVER: continue @@ -273,17 +294,20 @@ def combine( cand = ranked(p, pool, prior, hint is not None or share, hint) if cand and cand[0][1] >= fit.COMBO[2] and cand[0][3] not in have: f, x, c, g = round(cand[0][1], 3), cand[0][2], cand[0][3], cand[0][4] - chosen.append(one(c, p.get("제목"), f, x, "몫", g)) + ok = mixed or reached(c, line, p.get("제목"), p, prior, got) + chosen.append(one(c, p.get("제목"), f, x, "몫" if ok else "후보", g)) have.add(c) return chosen def solve( top: list[str], parts: list[dict], line: dict, got: dict, base: list[str] -) -> tuple[list[str], list[dict[str, Any]]]: - """(새 차례, 조합 대상) — 섞인 줄이고 1순위가 블록 합을 MIXED_CUT 못 덮으면 1순위를 인력 몫 - 로직으로 바꿈(몫 먼저) · 조합이 하나뿐이면 대상 빈 것.""" +) -> tuple[list[str], list[dict[str, Any]], list[str]]: + """(새 차례, 조합 대상, 덧 후보) — 섞인 줄이고 1순위가 블록 합을 MIXED_CUT 못 덮으면 1순위를 + 인력 몫 로직으로 바꿈(몫 먼저) · 조합이 하나뿐이면 대상 빈 것 · 덧 후보 = 근거 안 닿은 덧.""" chosen = combine((top or [None])[0], parts, line, got, base) + spare = [c["로직"] for c in chosen if c["단계"] == "후보"] + chosen = [c for c in chosen if c["단계"] != "후보"] codes = list(dict.fromkeys(c["로직"] for c in chosen if c["로직"])) if top and len(codes) > 1 and parts and is_mixed(parts, line): if cover(_sum(parts), top[0]) < fit.MIXED_CUT: @@ -296,5 +320,6 @@ def solve( c for c in chosen if not (c["로직"] in seen or seen.add(c["로직"])) ] # 같은 로직은 처음 것 if len(chosen) < 2: - return top, [] - return top, [{**c, "체크": c["로직"] not in MACHINE_SET or c["로직"] == top[0]} for c in chosen] + return top, [], spare + combo = [{**c, "체크": c["로직"] not in MACHINE_SET or c["로직"] == top[0]} for c in chosen] + return top, combo, spare diff --git a/M02_MasterTemplete/M02_Hopyo_Fit.py b/M02_MasterTemplete/M02_Hopyo_Fit.py index 78761091e..b4a3c16a4 100644 --- a/M02_MasterTemplete/M02_Hopyo_Fit.py +++ b/M02_MasterTemplete/M02_Hopyo_Fit.py @@ -54,7 +54,7 @@ COMBO = (0.85, 0.75, 0.6) COVER = 0.6 SHARES = (0.1, 0.2, 0.3, 0.5, 0.7, 0.8, 0.9) EVERY = 20 # 블록 몇 개마다 캐시 덮음 -CALC_VERSION = 1 # 맞춤셈 꼴 · 셈이 바뀌면 올림 — 옛 캐시 안 씀 +CALC_VERSION = 2 # 맞춤셈 꼴 · 셈이 바뀌면 올림 — 옛 캐시 안 씀 COUNT = {"개소", "개", "본", "ea", "조", "매", "대", "기", "공"} HOUR = {"hr", "시간", "h"} NO_NUMBER = "숫자 판정 없음" diff --git a/M02_MasterTemplete/M02_Hopyo_Line.py b/M02_MasterTemplete/M02_Hopyo_Line.py index 3e533a65b..60a842428 100644 --- a/M02_MasterTemplete/M02_Hopyo_Line.py +++ b/M02_MasterTemplete/M02_Hopyo_Line.py @@ -4,10 +4,12 @@ 상태 차례 = ① % ② 호표 참조(만들 층에 확인된 사전조합이면 체크) ③ 노임(그릇 없음) ④ 장부 「우리에 없음 · -안 씀」 → 뺌 ⑤ 장부 짝 → 별칭(규격 다름 · 근거 다름 · 숫자 판정 맞음이 다른 로직이면 체크 안 함) ⑥ +안 씀」 → 뺌 ⑤ 장부 짝 → 별칭(근거 다름 · 숫자 판정 맞음이 다른 로직이면 체크 안 함) ⑥ 맞춤셈 판정(맞음 = 체크 · 다른 점 · 후보) ⑦ 셈 전 · 숫자 없음 → 이름 · 근거 후보(「셈 중」 · 「숫자 판정 없음」). +장부 짝이 규격 다름(품명|단위 느슨 열쇠)이면 ⑤ 아님 — 꼬리표 「별칭 · 규격 다름」 · 짝은 후보 +끝 덤만. 자재 · 별도계상 = 이름 · 규격 같음 하나면 맞음(체크) · 그 밖 후보. 입력값 출처(칸마다) = 별칭 > 블록(옵션 찾기 값이 규격 읽기와 다름) > 규격(`spec_inputs`) > 자동값. 체크 = 처음 제안일 뿐 — 들어감은 [확인하고 사전조합 만들기](다 창)만. 저장 없음 · 셈은 서버 단독. @@ -92,18 +94,24 @@ def _pair(pair: dict, line: dict, ctx: Ctx) -> dict: def _qty(entry: dict, found: dict | None) -> dict: - """품 비교 — 엑셀 · 우리 · 같음(직종 · 장비 갈래마다 R 허용).""" + """품 비교 — 엑셀 · 우리 · 같음(직종 · 장비 갈래마다 R 허용) · 같은 갈래 여러 줄은 합쳐 견줌 + (화면 품 비교 열쇠 = 갈래 가나다 차례 + 로 묶음).""" ex = entry.get("품") or {"인력": {}, "장비": [], "자재": []} us = (found or {}).get("우리품") or option.ours_qty(None) same = {} for t, a in ex["인력"].items(): b = us["인력"].get(t) same[t] = bool(b) and option.near(a, b, fit.TOL[0], option.ru(a)) + sums: dict[str, list[float]] = {} # 열쇠 → [엑셀 합, 반올림 폭 합, 우리 합] for m in ex["장비"]: - mine = [x["시간"] for x in us["장비"] if set(x["갈래"]) & set(m["갈래"])] - hit = any(option.near(m["시간"], q, fit.TOL[0], option.ru(m["시간"])) for q in mine) - name = "+".join(m["갈래"]) # 화면 품 비교 열쇠와 같음(갈래 + 로 묶음) - same[name] = hit or same.get(name, False) + one = sums.setdefault("+".join(sorted(m["갈래"])), [0.0, 0.0, 0.0]) + one[0] += m["시간"] + one[1] += option.ru(m["시간"]) + for x in us["장비"]: + if (one := sums.get("+".join(sorted(x["갈래"])))) is not None: + one[2] += x["시간"] + for name, (a, r, b) in sums.items(): + same[name] = option.near(a, b, fit.TOL[0], r) return {"엑셀": ex, "우리": us, "같음": same} @@ -201,17 +209,39 @@ def match_line(line: dict[str, Any], ctx: Ctx, full: bool = True) -> dict[str, A if kind == "노임": return {**out, "상태": "노임", "글": "노임 · 그릇 없음"} alias = ledger.recall(line, ctx.to, ctx.books) + loose, alias = (alias, None) if alias and alias.get("규격다름") else (None, alias) entry = ctx.cache.get(fit.unit_key(line)) if kind not in hm.MATERIAL_KINDS else None if alias and alias.get("갈래") in SKIP: return {**out, "상태": "뺌", "뺌": alias["갈래"], "별칭": _alias_info(alias, False)} if alias and alias.get("갈래") == "짝" and alias.get("짝"): return {**out, **_aliased(line, ctx, alias, entry)} if kind in hm.MATERIAL_KINDS: - return {**out, **_material(line, ctx, full)} - if entry and entry["판정"]["글"] not in (fit.NO_NUMBER, "우리 로직 준비 중"): - return {**out, **(_numeric(line, ctx, entry) if full else _brief(entry))} - why = "셈 중" if not ctx.calc["끝"] else fit.NO_NUMBER - return {**out, **_names_only(line, ctx, why, full)} + got = _material(line, ctx, full) + elif entry and entry["판정"]["글"] not in (fit.NO_NUMBER, "우리 로직 준비 중"): + got = _numeric(line, ctx, entry) if full else _brief(entry) + else: + why = "셈 중" if not ctx.calc["끝"] else fit.NO_NUMBER + got = _names_only(line, ctx, why, full) + return {**out, **(_loose(got, loose, line, ctx) if loose and full else got)} + + +def _loose(got: dict, alias: dict, line: dict, ctx: Ctx) -> dict: + """규격 다른 별칭 — 꼬리표 「별칭 · 규격 다름」 + 짝 로직 · 자재가 후보에 없으면 끝에 덤(상태 · + 대상 · 옵션은 이 줄 숫자 그대로).""" + tags = [*(got.get("꼬리표") or []), "별칭 · 규격 다름"] + have = {c.get("로직") or c.get("자재") for c in got.get("후보") or []} + have |= {t.get("로직") or t.get("자재") for t in got.get("대상") or []} + extra = [] + for p in alias.get("짝") or []: + code = p.get("로직") or p.get("자재") + if code and code not in have: + one = { + k: v + for k, v in _pair(p, line, ctx).items() + if k in ("로직", "자재", "이름", "단위") + } + extra.append({**one, "점수": None, "판정": "후보", "글": "별칭 · 규격 다름"}) + return {**got, "꼬리표": tags, "후보": [*(got.get("후보") or []), *extra]} def _brief(entry: dict) -> dict: @@ -225,7 +255,7 @@ def _alias_info(alias: dict, basis_off: bool) -> dict: def _aliased(line: dict, ctx: Ctx, alias: dict, entry: dict | None) -> dict: - """⑤ 장부 짝 — 체크 = 규격 같음 · 근거 같음 · 숫자 판정 맞음이 다른 로직 아님.""" + """⑤ 장부 짝(정확 열쇠) — 체크 = 근거 같음 · 숫자 판정 맞음이 다른 로직 아님.""" targets = [_pair(p, line, ctx) for p in alias["짝"]] codes = {t["로직"] for t in targets if t.get("로직")} theirs = ((line.get("근거") or {}).get("출처")) or [] @@ -234,9 +264,9 @@ def _aliased(line: dict, ctx: Ctx, alias: dict, entry: dict | None) -> dict: hm.clause(theirs, table[c])[0] < 0.9 for c in codes if c in table ) other = bool(entry and entry["판정"]["상태"] == "맞음" and entry["차례"][0] not in codes) - check = not alias.get("규격다름") and not off and not other + check = not off and not other tags = ["별칭 — " + " · ".join(str(x) for x in (alias.get("층"), alias.get("때")) if x)] - tags += ["규격 다름"] * bool(alias.get("규격다름")) + ["근거 다름"] * off + tags += ["근거 다름"] * off tags += [f"숫자 판정 맞음 = {entry['차례'][0]}"] if other else [] out = {"상태": "별칭", "체크": check, "대상": targets, "글": "별칭 장부 짝", "꼬리표": tags} out["별칭"] = _alias_info(alias, off) @@ -249,7 +279,7 @@ def whole(hopyo: dict, ctx: Ctx) -> dict | None: """직접 줄 호표 통째 제안 — 장부 짝(호표 열쇠) 먼저 · 맞춤셈 「호표:번」.""" head = {k: hopyo.get(k) for k in ("이름", "규격", "단위")} alias = ledger.recall(head, ctx.to, ctx.books, whole=True) - if alias and alias.get("갈래") == "짝" and alias.get("짝"): + if alias and alias.get("갈래") == "짝" and alias.get("짝") and not alias.get("규격다름"): return { "대상": [_pair(p, head, ctx) for p in alias["짝"]], "글": "별칭 장부 짝", diff --git a/M02_MasterTemplete/M02_Hopyo_Verdict.py b/M02_MasterTemplete/M02_Hopyo_Verdict.py index d7020eb8b..000f5b706 100644 --- a/M02_MasterTemplete/M02_Hopyo_Verdict.py +++ b/M02_MasterTemplete/M02_Hopyo_Verdict.py @@ -1,8 +1,8 @@ """M02 일위대가 맞춤 — 블록 하나 셈 · 판정(PLAN 338-1 · 설계 2-1 · 2-6 · 3-2). 블록 + 줄 → 엑셀 지문(`Finger`) → 이름 · 근거 차례(`Match.b_order`) → 지문 순위 섞기(`Fit`) · -같음 1위 고정(딱 맞음이 이김) → 섞인 줄 몫 먼저 · 조합(`Combine.solve`) → 위 다섯 옵션 -거꾸로(`Option`) → +같음 1위 고정(딱 맞음이 이김) → 섞인 줄 몫 먼저 · 조합(`Combine.solve` · 근거 안 닿은 덧 = 후보 끝) +→ 위 다섯 옵션 거꾸로(`Option`) → 위 다섯 가운데 딱 맞음(옵션까지)이 있는데 1위가 아니면 그것이 1위(조합 없는 줄) → 판정 맞음 · 다른 점 · 후보 + 후보 다섯(로직마다 옵션 판정 꼬리표). 글 · 꼬리표는 숫자 사실만(엑셀 · 우리 품) — 「옛 판」 같은 까닭은 짓지 않음. @@ -122,7 +122,7 @@ def block(block_: dict | None, line: dict[str, Any], got: dict[str, Any]) -> dic parts = efp.get("몫") or [] if parts and btop and combine.is_mixed(parts, line): # 섞인 줄 — 이름 · 근거 차례로 몫 먼저 top = btop[: fit.TOP_OPT] - top, combo = combine.solve(top, parts, line, got, border["합침"]) + top, combo, spare = combine.solve(top, parts, line, got, border["합침"]) opts = {k: option.find(k, efp, text) for k in top[: fit.TOP_OPT]} for c in combo: if c["로직"] not in opts and c["체크"]: @@ -150,6 +150,10 @@ def block(block_: dict | None, line: dict[str, Any], got: dict[str, Any]) -> dic entry["조합"] = combo or None entry["판정"] = judge(efp, top, opts, theirs, got) entry["후보"] = candidates(efp, top, opts, theirs, got, scores, border.get("근거") or {}) + names = {x["로직"]: x for x in got["로직"]} + for k in (k for k in spare if k not in top[: fit.TOP_OPT] and k in names): # 근거 안 닿은 덧 + one = {key: names[k][key] for key in ("로직", "이름", "단위")} + entry["후보"].append({**one, "점수": None, "판정": "후보", "글": "덧 후보 — 근거 안 닿음"}) return entry diff --git a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Check.ts b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Check.ts index 264872dbf..4c6132e55 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Check.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Hopyo_Check.ts @@ -149,10 +149,14 @@ export function compareText(q: HopyoMatch["품"] | undefined): string { const out = [...new Set([...Object.keys(ex), ...Object.keys(ours)])].map( (k) => `${k} ${n4(ex[k])} → ${n4(ours[k])} ${mark(k)}`, ); - const name = (g: string | string[]): string => [g].flat().join("+"); - for (const x of q.엑셀.장비 ?? []) { - const o = (q.우리.장비 ?? []).find((y) => name(y.갈래) === name(x.갈래)); - out.push(`${name(x.갈래)} ${n4(x.시간)} → ${n4(o?.시간)} ${mark(name(x.갈래))}`); + // 같은 갈래 여러 줄은 합쳐 한 줄(서버 `_qty` 와 같은 열쇠 — 갈래 가나다 차례 + 로 묶음) + const name = (g: string | string[]): string => [g].flat().sort().join("+"); + const sum = (xs: { 갈래: string | string[]; 시간: number }[], k: string): number | undefined => + xs + .filter((y) => name(y.갈래) === k) + .reduce((s, y) => (s ?? 0) + y.시간, undefined); + for (const k of new Set((q.엑셀.장비 ?? []).map((x) => name(x.갈래)))) { + out.push(`${k} ${n4(sum(q.엑셀.장비 ?? [], k))} → ${n4(sum(q.우리.장비 ?? [], k))} ${mark(k)}`); } return out.join(" · "); } diff --git a/resources/tester/helper_m02_hopyo_check.cjs b/resources/tester/helper_m02_hopyo_check.cjs index d91ae5ea6..dc55d7522 100644 --- a/resources/tester/helper_m02_hopyo_check.cjs +++ b/resources/tester/helper_m02_hopyo_check.cjs @@ -132,6 +132,7 @@ let out0 = null; checkable: lines.map((l) => check.checkable(l, fresh)), sure: check.sureLines(lines, fresh).map((l) => l.차례), compare: check.compareText(lines[1].맞춤.품), + compareMany: check.compareText(lines[3].맞춤.품), toTarget: [ check.toTarget({ 호표: 46, 이름: "고화매트", 입력값: {}, 배: 2.3 }), check.toTarget({ 자재: "MT1", 이름: "레미콘", 입력값: {}, 배: 1, 몫: null }), diff --git a/resources/tester/test_m02_hopyo_fit.py b/resources/tester/test_m02_hopyo_fit.py index a198d451b..5ed65a34d 100644 --- a/resources/tester/test_m02_hopyo_fit.py +++ b/resources/tester/test_m02_hopyo_fit.py @@ -77,6 +77,14 @@ def test_조합_고임돌_인력_몫_먼저(got): assert all(not c["체크"] for c in r["조합"] if c["로직"] in ("GC000267", "GC000268")) +def test_조합_근거_안_닿은_덧은_후보(got): + r = _block("N009", got) # 구조물되메우기(인력100%) — 훈증더미 인력 제거(개)는 덧 아님 + assert r["차례"][0] == "GF000113" and not r["조합"] + assert any(c["로직"] == "GF000083" and c["글"].startswith("덧 후보") for c in r["후보"]) + r = _block("N017", got) # 다웰바(EA) — PSC빔 쉬즈관(m) 덧 아님 + assert r["차례"][0] == "GC000226" and not r["조합"] + + def test_숫자_없음_옛_HP(got): block, line = line_of("N000") r = verdict.block({**block, "글": []}, line, got) diff --git a/resources/tester/test_m02_hopyo_match.py b/resources/tester/test_m02_hopyo_match.py index ec1231685..e593dd4d1 100644 --- a/resources/tester/test_m02_hopyo_match.py +++ b/resources/tester/test_m02_hopyo_match.py @@ -160,13 +160,32 @@ def test_별칭_체크_규칙(client): other = first["레미콘타설(장비사용)"] # 숫자 판정 맞음 = GC000198 → 체크 안 함 assert other["상태"] == "별칭" and other["체크"] is False assert "숫자 판정 맞음 = GC000198" in other["꼬리표"] - loose = first["합판거푸집"] # 규격 4회 ↔ 장부 6회 — 느슨 열쇠 - assert loose["상태"] == "별칭" and loose["별칭"]["규격다름"] and loose["체크"] is False + loose = first["합판거푸집"] # 규격 4회 ↔ 장부 6회 — 느슨 열쇠 = 꼬리표 · 후보 덤만(229-4 ①) + assert loose["상태"] == "맞음" and "별칭" not in loose and "별칭 · 규격 다름" in loose["꼬리표"] + assert all("별칭" not in t["출처"].values() for t in loose["대상"]) + assert "GC000211" in {c["로직"] for c in loose["후보"]} | {t["로직"] for t in loose["대상"]} + _ledger( + client, {remicon: {"갈래": "짝", "짝": [{"로직": "GC000198", "입력값": {"소량감률": 50}}]}} + ) + plain = _hopyo(client, 7)["레미콘타설(장비사용)"] # 무근구조물 ↔ 장부 철근구조물-소량타설 + assert plain["상태"] == "맞음" and plain["체크"] is True and "별칭" not in plain + assert plain["대상"][0]["로직"] == "GC000198" and plain["대상"][0]["입력값"]["소량감률"] == 0 + assert "별칭 · 규격 다름" in plain["꼬리표"] _ledger(client, {dig: {"갈래": "짝", "짝": [{"로직": "GF000101", "배": 1}]}}) off = _hopyo(client, 3)["구조물터파기(인력100%)"] # 산림 로직 ↔ 엑셀 건설 3-2-1 assert off["별칭"]["근거다름"] and off["체크"] is False and "근거 다름" in off["꼬리표"] +def test_품_비교_같은_장비_여러_줄(): + from M02_MasterTemplete import M02_Hopyo_Line as lines + + ex = {"인력": {}, "장비": [{"갈래": ["굴착기"], "시간": t} for t in (0.0334, 0.0371, 0.0889)]} + one = {"인력": {}, "장비": [{"갈래": ["굴착기"], "시간": 0.0889}], "자재": []} + assert not lines._qty({"품": ex}, {"우리품": one})["같음"]["굴착기"] # 값이 달라도 ✓ 이던 흠 + many = {**one, "장비": ex["장비"]} + assert lines._qty({"품": ex}, {"우리품": many})["같음"]["굴착기"] + + def test_장부_뺌_위_층(client): client.who.update(role="SYSTEM_ADMIN") key = "구조물터파기(인력100%)|토사|m3" diff --git a/resources/tester/test_m02_hopyo_screen.py b/resources/tester/test_m02_hopyo_screen.py index 547ba5573..5dfa41e89 100644 --- a/resources/tester/test_m02_hopyo_screen.py +++ b/resources/tester/test_m02_hopyo_screen.py @@ -48,6 +48,12 @@ QTY = { "같음": {"콘크리트공": True, "굴착기": False}, } +QTY_MANY = { # 같은 장비 여러 줄(229-4 ②) — 합쳐 한 줄로 견줌 + "엑셀": {"장비": [{"갈래": "굴착기", "시간": t} for t in (0.0334, 0.0371, 0.0889)]}, + "우리": {"장비": [{"갈래": "굴착기", "시간": 0.0889}]}, + "같음": {"굴착기": False}, +} + def _text(name: str) -> str: return (M02 / name).read_text(encoding="utf-8") @@ -77,7 +83,7 @@ def _check() -> dict: _line(0, "후보", False, [{"자재": "MT004258", "입력값": {}, "배": 1}]), _line(1, "맞음", True, [SURE], 품=QTY), _line(2, "별칭", True, [{"로직": "GF000101", "입력값": {}, "배": 1}]), - _line(3, "다른 점", False, [{"로직": "GF000103", "입력값": {}, "배": 1}]), + _line(3, "다른 점", False, [{"로직": "GF000103", "입력값": {}, "배": 1}], 품=QTY_MANY), _line(4, "후보", False, []), _line(5, "%", False, []), _line(6, "호표", True, [{"호표": 46, "입력값": {}, "배": 2.3}]), @@ -122,6 +128,7 @@ def test_확인_창_체크한_줄만_몸(): assert got["checkable"] == [True, True, True, True, False, False, True, False] assert got["sure"] == [1, 2] # [맞음 · 별칭 모두 체크] assert got["compare"] == "콘크리트공 0.1091 → 0.1091 ✓ · 굴착기 0.29 → 0.31 ≠" + assert got["compareMany"] == "굴착기 0.1594 → 0.0889 ≠" # 같은 장비 여러 줄 = 합쳐 한 줄 assert got["toTarget"] == [{"호표": 46, "배": 2.3}, {"자재": "MT1", "배": 1}] assert got["newest"] == [False, True, True] # 같은 줄 늦은 응답 버림 · 다른 줄은 따로 assert got["dropped"] is False # 만들기 성공 = 캐시 지움 From 94838450d8c137e79ebf4b8bb6056bb7bc9e8b69 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Fri, 16 Oct 2026 06:26:09 +0900 Subject: [PATCH 81/81] =?UTF-8?q?fix(=EC=9D=BC=EC=9C=84=EB=8C=80=EA=B0=80?= =?UTF-8?q?=EC=8B=A4=EB=AC=B4):=20339=20=EB=8D=A7=EA=B3=A0=EC=B9=A8=204=20?= =?UTF-8?q?=E2=80=94=20=EC=9A=B0=EB=A6=AC=20=EC=A7=80=EB=AC=B8=20build()?= =?UTF-8?q?=20=EA=B0=80=20=EB=A9=94=EB=AA=A8(=ED=91=9C=20=C2=B7=20?= =?UTF-8?q?=ED=91=9C=ED=8C=90)=EA=B9=8C=EC=A7=80=20=EA=B0=B1=EC=8B=A0=20?= =?UTF-8?q?=C2=B7=20warm()=20=EC=A4=91=EB=B3=B5=20=EA=B0=B1=EC=8B=A0=20?= =?UTF-8?q?=EC=A4=84=20=EC=A7=80=EC=9B=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 빈 캐시 폴더에서 table() 이 None 을 먼저 메모하면 build() 뒤에도 ready() 거짓 · 맞춤 「맞음」 0 이던 흠 - 시험: 캐시 쓰기 읽기 준비 전 none 에 build 직후 ready() 참 · table() = build 결과 한 줄 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Ji4nuyLVHcxrXWHTdb5nRD --- M02_MasterTemplete/M02_Hopyo_Ours.py | 7 +++---- resources/tester/test_m02_hopyo_ours.py | 1 + 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/M02_MasterTemplete/M02_Hopyo_Ours.py b/M02_MasterTemplete/M02_Hopyo_Ours.py index 667958381..92272c4ad 100644 --- a/M02_MasterTemplete/M02_Hopyo_Ours.py +++ b/M02_MasterTemplete/M02_Hopyo_Ours.py @@ -326,6 +326,8 @@ def build(ver: str | None = None) -> list[dict]: ver = ver or version() table = [fingerprint(one["로직"], one["단위"]) for one in hopyo_match.targets()["로직"]] atomic_write_json(path_of(ver), {"판": ver, "지문판": FP_VERSION, "표": table}) + with _LOCK: + _STATE.update(표=table, 표판=ver) return table @@ -351,10 +353,7 @@ def warm(build_missing: bool = False) -> threading.Thread | None: def work() -> None: try: if table() is None and build_missing: - ver = version() - got = build(ver) - with _LOCK: - _STATE.update(표=got, 표판=ver) + build(version()) except Exception: # noqa: BLE001 — 마스터가 깨져도 서버를 막지 않음(보기에서 「준비 중」) pass diff --git a/resources/tester/test_m02_hopyo_ours.py b/resources/tester/test_m02_hopyo_ours.py index 4027d9d66..bc9414960 100644 --- a/resources/tester/test_m02_hopyo_ours.py +++ b/resources/tester/test_m02_hopyo_ours.py @@ -34,6 +34,7 @@ def test_캐시_쓰기_읽기_준비_전_none(cache): built = ours.build() assert [o["로직"] for o in built] == ["GC000198", "GC000211", "GC000268"] assert ours.path_of().parent == cache and ours.path_of().is_file() + assert ours.ready() and ours.table() == built ours._STATE.update(표판=None, 표=None) assert [o["로직"] for o in ours.table()] == ["GC000198", "GC000211", "GC000268"] assert master_hash() == before