Merge remote-tracking branch 'origin/dev' into 안티그래비티
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,640 @@
|
||||
{
|
||||
"schema_version": "1.0",
|
||||
"dataset_id": "work_item_link",
|
||||
"effective_date": "2026-01-01",
|
||||
"note": "산림사업 표준품셈과 건설공사 표준품셈의 연결 밑그림. 확정된 단일 절은 FW-/CW- 불변 열쇠로 잇고, 원문이 건설 절을 밝히지 않거나 구판-현행 대응이 확인되지 않은 행은 열쇠 없이 미판정으로 둔다.",
|
||||
"policy": {
|
||||
"both_precedence": "forest",
|
||||
"fallback": "construction",
|
||||
"applies_to": "branch=both,status=확정",
|
||||
"unconfirmed_action": "do_not_link",
|
||||
"source": "산림사업 표준품셈 총칙 1-1 제7호, 원문 L535",
|
||||
"rule": "산림품셈에 명시된 품은 산림값을 우선하고, 명시되지 않은 품만 건설 등 타 부문 표준품셈으로 보완한다."
|
||||
},
|
||||
"links": [
|
||||
{
|
||||
"forest_key": "",
|
||||
"const_key": "",
|
||||
"branch": "forest_only",
|
||||
"evidence": "산림 제2장 조림·제3장 숲가꾸기·제4장 산림병해충방제·제6장 산림경영계획·제7장 수확; 2026 건설품셈 5부문 45장에서 같은 산림사업 공종표 미확인",
|
||||
"difference_note": "㉮ 근거가 될 절이 아예 없음 — 조경·유지관리의 비슷한 이름을 같은 공종으로 볼 근거가 없음",
|
||||
"status": "미판정"
|
||||
},
|
||||
{
|
||||
"forest_key": "",
|
||||
"const_key": "",
|
||||
"branch": "forest_only",
|
||||
"evidence": "산림 13-13-1 목재틀흙막이; 2026 건설품셈에서 같은 구조·규격별 표 미확인",
|
||||
"difference_note": "㉮ 근거가 될 절이 아예 없음 — 건설품셈 전체에서 '목재틀흙막이'·'목재틀' 절을 찾지 못함",
|
||||
"status": "미판정"
|
||||
},
|
||||
{
|
||||
"forest_key": "",
|
||||
"const_key": "CW-00844",
|
||||
"branch": "const_only",
|
||||
"evidence": "산림 13-4 돌쌓기에는 배합 원단위 없음; 건설 건축 9-1-1 모르타르 배합 원문 L6~22; masonry_wet AX-WK-c0842a0d 사용",
|
||||
"difference_note": "1㎥당 1:3 배합은 시멘트 510kg·모래 1.10㎥·보통인부 0.66인",
|
||||
"status": "확정"
|
||||
},
|
||||
{
|
||||
"forest_key": "",
|
||||
"const_key": "CW-00018",
|
||||
"branch": "const_only",
|
||||
"evidence": "산림 1-3-1 재료의 할증에 없는 재료를 건설 공통 1-3-1로 보완; material_surcharge_2026-01-01.json source.supplementary",
|
||||
"difference_note": "산림표가 정본이며 산림표에 없는 재료만 건설표 사용",
|
||||
"status": "확정"
|
||||
},
|
||||
{
|
||||
"forest_key": "FW-00260",
|
||||
"const_key": "CW-00330",
|
||||
"branch": "both",
|
||||
"evidence": "산림 9-7-1 원문 L4814~4828이 건설 공통 8-2-13을 직접 참조하고 토목·산림의 적용값을 비교",
|
||||
"difference_note": "버킷계수 K와 체적환산계수 f는 같지만 작업효율 E는 임도 0.35·토목 0.45(임도가 0.10 낮음), 사이클 타임은 임도 180°·22초와 토목 90°·18초(임도가 4초, 22.2% 김). 임도의 협소한 공간과 추락위험을 반영한 차이",
|
||||
"status": "확정"
|
||||
},
|
||||
{
|
||||
"forest_key": "",
|
||||
"const_key": "",
|
||||
"branch": "both",
|
||||
"evidence": "산림 9-21 원문 L5510~5527; 건설 공통 3-9-2 원문 L692~703; 현행 계산은 건설표의 1,000㎡당 밑수만 사용",
|
||||
"difference_note": "㉰ 축이 달라 못 이음 — 굴삭기 0.2㎥ 기준 산림은 소밀도별 0.80/1.01/1.22hr, 건설은 굴삭기+그래플 3.76hr/1,000㎡로 소밀도끼리도 2.96hr·4.7배 차이. 품은 산림값을 쓰고 건설에서는 단위 밑수만 빌림",
|
||||
"status": "미판정"
|
||||
},
|
||||
{
|
||||
"forest_key": "",
|
||||
"const_key": "",
|
||||
"branch": "both",
|
||||
"evidence": "산림 13-4-4 원문 L7132 이후; 건설 공통 7-1 돌쌓기; stone_kind_2026-01-01.json 구판 건설표 fallback",
|
||||
"difference_note": "㉰ 축이 달라 못 이음 — 산림은 석재 종류·찰/메·뒷길이별 ㎡당 품. 건설은 3인+1인+굴삭기 작업조의 1일 시공량으로 뒷길이 35/55/75cm에서 메 23/26/28㎡·찰 27/30/34㎡. 표 축·포함범위가 달라 직접 치환 불가",
|
||||
"status": "미판정"
|
||||
},
|
||||
{
|
||||
"forest_key": "",
|
||||
"const_key": "",
|
||||
"branch": "both",
|
||||
"evidence": "산림 10-4·10-9 운반식·적재량; 건설 공통 8-1-6(2) 운반거리·속도·왕복시간; B09 운반 계산에서 부분 결합",
|
||||
"difference_note": "산림은 현장 공종·적재 조건, 건설은 운반 시간 산정층을 제공하므로 어느 한쪽으로 대체하지 않음",
|
||||
"status": "확정"
|
||||
},
|
||||
{
|
||||
"forest_key": "",
|
||||
"const_key": "",
|
||||
"branch": "both",
|
||||
"evidence": "산림 1-3-2 및 임도 토공 표; 건설 공통 1-3-2 및 토공 관련 표",
|
||||
"difference_note": "㉯ 이름은 닮았으나 내용이 다름 — 토량환산 표의 산림 토질 행에 같은 이름처럼 보이면서 서로 다른 계수가 있어 이름만으로 수치 대응 불가",
|
||||
"status": "미판정"
|
||||
},
|
||||
{
|
||||
"forest_key": "FW-00016",
|
||||
"const_key": "CW-00018",
|
||||
"branch": "both",
|
||||
"evidence": "산림 1-3-1과 건설 공통 1-3-1 재료의 할증; material_surcharge_2026-01-01.json",
|
||||
"difference_note": "같은 재료는 산림값 우선, 산림에 없는 재료만 건설값 사용",
|
||||
"status": "확정"
|
||||
},
|
||||
{
|
||||
"forest_key": "FW-00008",
|
||||
"const_key": "CW-00009",
|
||||
"branch": "both",
|
||||
"evidence": "산림 1-2-2 단위 표준 원문 L573~664; 건설 공통 1-2-2 단위표준 원문 L46~99",
|
||||
"difference_note": "㉱ 추가 대조로 확정 — 설계서 수량·소수자리와 금액 단위표가 일치하며 both에서는 산림표 우선",
|
||||
"status": "확정"
|
||||
},
|
||||
{
|
||||
"forest_key": "",
|
||||
"const_key": "",
|
||||
"branch": "both",
|
||||
"evidence": "산림 1-2-5는 '노임'으로 시중노임단가 적용만 규정(L768~770); 건설 공통 1-2-5 '인력'은 직종 선정·작업반장·안전인력을 규정(L260~263)",
|
||||
"difference_note": "㉯ 이름은 닮았으나 내용이 다름 — 산림의 노임 단가 규정과 건설의 인력 편성 규정이라 절 대응 불가",
|
||||
"status": "미판정"
|
||||
},
|
||||
{
|
||||
"forest_key": "FW-00401",
|
||||
"const_key": "CW-00267",
|
||||
"branch": "both",
|
||||
"evidence": "산림 12-27-1 지수판 설치 원문 L6731~6742; 건설 공통 6-3-9 지수판 설치 원문 L472~489",
|
||||
"difference_note": "㉱ 추가 대조로 확정 — 두 표 모두 m당 특별인부 0.151인·보통인부 0.116인, PVC 지수판 1.04m·용접봉 0.042kg·철선 0.21kg, 공구손료 3%로 일치",
|
||||
"status": "확정"
|
||||
},
|
||||
{
|
||||
"forest_key": "FW-00009",
|
||||
"const_key": "CW-00010",
|
||||
"branch": "both",
|
||||
"evidence": "산림 1-2-3 토질 원문 L665~750; 건설 공통 1-2-3 토질 원문 L99~185",
|
||||
"difference_note": "㉱ 추가 대조로 확정 — 지반설계, 토질·암 분류와 체적환산계수의 항목·값이 일치하며 both에서는 산림표 우선",
|
||||
"status": "확정"
|
||||
},
|
||||
{
|
||||
"forest_key": "",
|
||||
"const_key": "",
|
||||
"branch": "both",
|
||||
"evidence": "산림 12-2 표면 마무리 원문 L6157~6165; 건설 공통 6-1-3 표면 마무리 원문 L12~18",
|
||||
"difference_note": "㉰ 축이 달라 못 이음 — 작업 설명과 숫자 0.34인은 같지만 산림은 ㎡당, 건설은 100㎡당이라 동일 면적 환산 시 100배 차이. 원문 단위 불일치가 해소되기 전 연결 금지",
|
||||
"status": "미판정"
|
||||
},
|
||||
{
|
||||
"forest_key": "FW-00013",
|
||||
"const_key": "CW-00014",
|
||||
"branch": "const_only",
|
||||
"evidence": "산림 1-2-7 원문 L844가 건설 공통 1-2-7 운반·지게운반을 직접 지시",
|
||||
"difference_note": "산림 운반 계산에서 건설 지게운반 품을 보완",
|
||||
"status": "확정"
|
||||
},
|
||||
{
|
||||
"forest_key": "FW-00013",
|
||||
"const_key": "CW-00338",
|
||||
"branch": "const_only",
|
||||
"evidence": "산림 1-2-7 원문 L843~844가 건설 공통 8-2-21 경운기의 적재·적하시간과 속도를 직접 지시",
|
||||
"difference_note": "경운기 운반의 적재·적하시간과 속도를 건설품셈으로 보완",
|
||||
"status": "확정"
|
||||
},
|
||||
{
|
||||
"forest_key": "FW-00078",
|
||||
"const_key": "CW-00352",
|
||||
"branch": "const_only",
|
||||
"evidence": "산림 2-2-11 원문 L1720이 건설 공통 8-3-1 [00] 토공기계를 직접 지시",
|
||||
"difference_note": "굴착기 손료를 건설품셈으로 보완",
|
||||
"status": "확정"
|
||||
},
|
||||
{
|
||||
"forest_key": "FW-00078",
|
||||
"const_key": "CW-00359",
|
||||
"branch": "const_only",
|
||||
"evidence": "산림 2-2-11 원문 L1721이 건설 공통 8-3-8 [70] 기타기계를 직접 지시",
|
||||
"difference_note": "부착용집게 손료를 건설품셈으로 보완",
|
||||
"status": "확정"
|
||||
},
|
||||
{
|
||||
"forest_key": "FW-00078",
|
||||
"const_key": "CW-00354",
|
||||
"branch": "const_only",
|
||||
"evidence": "산림 2-2-11 원문 L1722~1723이 건설 공통 8-3-3 [20] 운반 및 하역기계를 직접 지시",
|
||||
"difference_note": "트럭탑재형·타이어형 크레인 손료를 건설품셈으로 보완",
|
||||
"status": "확정"
|
||||
},
|
||||
{
|
||||
"forest_key": "FW-00144",
|
||||
"const_key": "CW-01225",
|
||||
"branch": "const_only",
|
||||
"evidence": "산림 5-22-4 원문 L2740이 건설 유지관리 1-2-5 관목 전정을 직접 지시",
|
||||
"difference_note": "관목 전정 품을 건설품셈으로 보완",
|
||||
"status": "확정"
|
||||
},
|
||||
{
|
||||
"forest_key": "FW-00150",
|
||||
"const_key": "CW-00196",
|
||||
"branch": "const_only",
|
||||
"evidence": "산림 5-24-1 원문 L2788이 2025 건설 공통 4-1-2 종자뿜어붙이기를 직접 지시; 2026판 4-1-3",
|
||||
"difference_note": "판 개정으로 절 번호가 4-1-2에서 4-1-3으로 이동",
|
||||
"status": "확정"
|
||||
},
|
||||
{
|
||||
"forest_key": "FW-00150",
|
||||
"const_key": "CW-01228",
|
||||
"branch": "const_only",
|
||||
"evidence": "산림 5-24-1 원문 L2790이 건설 유지관리 1-2-8 인력관수를 직접 지시",
|
||||
"difference_note": "물주기 필요 시 인력관수 품을 보완",
|
||||
"status": "확정"
|
||||
},
|
||||
{
|
||||
"forest_key": "FW-00150",
|
||||
"const_key": "CW-01229",
|
||||
"branch": "const_only",
|
||||
"evidence": "산림 5-24-1 원문 L2790이 건설 유지관리 1-2-9 살수차관수를 직접 지시",
|
||||
"difference_note": "물주기 필요 시 살수차관수 품을 보완",
|
||||
"status": "확정"
|
||||
},
|
||||
{
|
||||
"forest_key": "FW-00152",
|
||||
"const_key": "CW-00198",
|
||||
"branch": "const_only",
|
||||
"evidence": "산림 5-25 원문 L2868이 2025 건설 공통 4-1-4 거적덮기를 직접 지시; 2026판 4-1-5",
|
||||
"difference_note": "판 개정으로 절 번호가 4-1-4에서 4-1-5로 이동",
|
||||
"status": "확정"
|
||||
},
|
||||
{
|
||||
"forest_key": "FW-00161",
|
||||
"const_key": "CW-00320",
|
||||
"branch": "const_only",
|
||||
"evidence": "산림 5-29 원문 L2950이 건설 공통 8-2-3 굴착기를 직접 지시",
|
||||
"difference_note": "굴착기 운전 품을 건설품셈으로 보완",
|
||||
"status": "확정"
|
||||
},
|
||||
{
|
||||
"forest_key": "FW-00258",
|
||||
"const_key": "CW-00151",
|
||||
"branch": "const_only",
|
||||
"evidence": "산림 9-6-1 원문 L4795가 건설 공통 3-3-6 대규모발파 TYPE-Ⅵ [주]⑦·⑧을 직접 지시",
|
||||
"difference_note": "기계소할의 발파 관련 품을 보완",
|
||||
"status": "확정"
|
||||
},
|
||||
{
|
||||
"forest_key": "FW-00261",
|
||||
"const_key": "CW-01325",
|
||||
"branch": "const_only",
|
||||
"evidence": "산림 9-7-2 원문 L4842가 건설 유지관리 3-1-1 콘크리트구조물 헐기(인력)를 직접 지시",
|
||||
"difference_note": "인력 헐기 품을 보완",
|
||||
"status": "확정"
|
||||
},
|
||||
{
|
||||
"forest_key": "FW-00261",
|
||||
"const_key": "CW-01326",
|
||||
"branch": "const_only",
|
||||
"evidence": "산림 9-7-2 원문 L4842가 건설 유지관리 3-1-2 콘크리트구조물 헐기(기계)를 직접 지시",
|
||||
"difference_note": "기계 헐기 품을 보완",
|
||||
"status": "확정"
|
||||
},
|
||||
{
|
||||
"forest_key": "FW-00263",
|
||||
"const_key": "CW-00330",
|
||||
"branch": "const_only",
|
||||
"evidence": "산림 9-8-1 원문 L4871이 건설 공통 8-2-13 대형브레이커를 직접 지시",
|
||||
"difference_note": "철골재 철거의 대형브레이커 운전 품을 보완",
|
||||
"status": "확정"
|
||||
},
|
||||
{
|
||||
"forest_key": "FW-00263",
|
||||
"const_key": "CW-01327",
|
||||
"branch": "const_only",
|
||||
"evidence": "산림 9-8-1 원문 L4871이 건설 유지관리 3-1-3 철골재 철거(인력)를 직접 지시",
|
||||
"difference_note": "인력 철거 품을 보완",
|
||||
"status": "확정"
|
||||
},
|
||||
{
|
||||
"forest_key": "FW-00263",
|
||||
"const_key": "CW-01328",
|
||||
"branch": "const_only",
|
||||
"evidence": "산림 9-8-1 원문 L4871이 건설 유지관리 3-1-4 철골재 철거(기계)를 직접 지시",
|
||||
"difference_note": "기계 철거 품을 보완",
|
||||
"status": "확정"
|
||||
},
|
||||
{
|
||||
"forest_key": "FW-00264",
|
||||
"const_key": "CW-01327",
|
||||
"branch": "const_only",
|
||||
"evidence": "산림 9-8-2 원문 L4890이 건설 유지관리 3-1-3 철골재 철거(인력)를 직접 지시",
|
||||
"difference_note": "인력 철거 품을 보완",
|
||||
"status": "확정"
|
||||
},
|
||||
{
|
||||
"forest_key": "FW-00264",
|
||||
"const_key": "CW-01328",
|
||||
"branch": "const_only",
|
||||
"evidence": "산림 9-8-2 원문 L4890이 건설 유지관리 3-1-4 철골재 철거(기계)를 직접 지시",
|
||||
"difference_note": "기계 철거 품을 보완",
|
||||
"status": "확정"
|
||||
},
|
||||
{
|
||||
"forest_key": "FW-00265",
|
||||
"const_key": "CW-01329",
|
||||
"branch": "const_only",
|
||||
"evidence": "산림 9-9 원문 L4903이 건설 유지관리 3-1-5 석축 헐기(인력)를 직접 지시",
|
||||
"difference_note": "석축 헐기 품을 보완",
|
||||
"status": "확정"
|
||||
},
|
||||
{
|
||||
"forest_key": "FW-00267",
|
||||
"const_key": "CW-00330",
|
||||
"branch": "const_only",
|
||||
"evidence": "산림 9-10-1 원문 L4919가 건설 공통 8-2-13 대형브레이커를 직접 지시",
|
||||
"difference_note": "브레이커 운전 품을 보완",
|
||||
"status": "확정"
|
||||
},
|
||||
{
|
||||
"forest_key": "FW-00268",
|
||||
"const_key": "CW-00330",
|
||||
"branch": "const_only",
|
||||
"evidence": "산림 9-10-2 원문 L4938이 건설 공통 8-2-13 대형브레이커를 직접 지시",
|
||||
"difference_note": "브레이커 운전 품을 보완",
|
||||
"status": "확정"
|
||||
},
|
||||
{
|
||||
"forest_key": "FW-00270",
|
||||
"const_key": "CW-00416",
|
||||
"branch": "const_only",
|
||||
"evidence": "산림 9-11-1 원문 L4957이 건설 토목 1-6-6 포장 절단을 직접 지시",
|
||||
"difference_note": "포장 절단 품을 건설품셈으로 보완",
|
||||
"status": "확정"
|
||||
},
|
||||
{
|
||||
"forest_key": "FW-00298",
|
||||
"const_key": "CW-00320",
|
||||
"branch": "const_only",
|
||||
"evidence": "산림 9-15-1 원문 L5286이 건설 공통 8-2-3 굴착기를 직접 지시",
|
||||
"difference_note": "굴착기 운전 품을 보완",
|
||||
"status": "확정"
|
||||
},
|
||||
{
|
||||
"forest_key": "FW-00299",
|
||||
"const_key": "CW-00318",
|
||||
"branch": "const_only",
|
||||
"evidence": "산림 9-15-2 원문 L5308이 건설 공통 8-2-1 불도저를 직접 지시",
|
||||
"difference_note": "불도저 운전 품을 보완",
|
||||
"status": "확정"
|
||||
},
|
||||
{
|
||||
"forest_key": "FW-00302",
|
||||
"const_key": "CW-00320",
|
||||
"branch": "const_only",
|
||||
"evidence": "산림 9-16-2 원문 L5340이 건설 공통 8-2-3 굴착기를 직접 지시",
|
||||
"difference_note": "굴착기 운전 품을 보완",
|
||||
"status": "확정"
|
||||
},
|
||||
{
|
||||
"forest_key": "FW-00302",
|
||||
"const_key": "CW-00326",
|
||||
"branch": "const_only",
|
||||
"evidence": "산림 9-16-2 원문 L5340이 건설 공통 8-2-9 롤러를 직접 지시",
|
||||
"difference_note": "롤러 운전 품을 보완",
|
||||
"status": "확정"
|
||||
},
|
||||
{
|
||||
"forest_key": "FW-00305",
|
||||
"const_key": "CW-00332",
|
||||
"branch": "const_only",
|
||||
"evidence": "산림 9-17-1 원문 L5372가 건설 공통 8-2-15 법면다짐기를 직접 지시",
|
||||
"difference_note": "법면다짐기 운전 품을 보완",
|
||||
"status": "확정"
|
||||
},
|
||||
{
|
||||
"forest_key": "FW-00306",
|
||||
"const_key": "CW-00318",
|
||||
"branch": "const_only",
|
||||
"evidence": "산림 9-17-2 원문 L5391이 건설 공통 8-2-1 불도저를 직접 지시",
|
||||
"difference_note": "불도저 운전 품을 보완",
|
||||
"status": "확정"
|
||||
},
|
||||
{
|
||||
"forest_key": "FW-00307",
|
||||
"const_key": "CW-00318",
|
||||
"branch": "const_only",
|
||||
"evidence": "산림 9-18 원문 L5403이 건설 공통 8-2-1 불도저를 직접 지시",
|
||||
"difference_note": "불도저 운전 품을 보완",
|
||||
"status": "확정"
|
||||
},
|
||||
{
|
||||
"forest_key": "FW-00310",
|
||||
"const_key": "CW-00165",
|
||||
"branch": "const_only",
|
||||
"evidence": "산림 9-19-2 원문 L5448이 건설 공통 3-5-1 절토면 고르기를 직접 지시",
|
||||
"difference_note": "절토면 고르기 품을 보완",
|
||||
"status": "확정"
|
||||
},
|
||||
{
|
||||
"forest_key": "FW-00310",
|
||||
"const_key": "CW-00168",
|
||||
"branch": "const_only",
|
||||
"evidence": "산림 9-19-2 원문 L5448이 건설 공통 3-6-1 성토면 고르기를 직접 지시",
|
||||
"difference_note": "성토면 고르기 품을 보완",
|
||||
"status": "확정"
|
||||
},
|
||||
{
|
||||
"forest_key": "FW-00311",
|
||||
"const_key": "CW-00165",
|
||||
"branch": "const_only",
|
||||
"evidence": "산림 9-19-3 원문 L5465가 건설 공통 3-5-1 절토면 고르기를 직접 지시",
|
||||
"difference_note": "절토면 고르기 품을 보완",
|
||||
"status": "확정"
|
||||
},
|
||||
{
|
||||
"forest_key": "FW-00311",
|
||||
"const_key": "CW-00168",
|
||||
"branch": "const_only",
|
||||
"evidence": "산림 9-19-3 원문 L5465가 건설 공통 3-6-1 성토면 고르기를 직접 지시",
|
||||
"difference_note": "성토면 고르기 품을 보완",
|
||||
"status": "확정"
|
||||
},
|
||||
{
|
||||
"forest_key": "FW-00313",
|
||||
"const_key": "CW-00320",
|
||||
"branch": "const_only",
|
||||
"evidence": "산림 9-20-1 원문 L5499가 건설 공통 8-2-3 굴착기를 직접 지시",
|
||||
"difference_note": "굴착기 운전 품을 보완",
|
||||
"status": "확정"
|
||||
},
|
||||
{
|
||||
"forest_key": "FW-00316",
|
||||
"const_key": "CW-00320",
|
||||
"branch": "const_only",
|
||||
"evidence": "산림 9-22 원문 L5545가 건설 공통 8-2-3 굴착기를 직접 지시",
|
||||
"difference_note": "굴착기 운전 품을 보완",
|
||||
"status": "확정"
|
||||
},
|
||||
{
|
||||
"forest_key": "FW-00344",
|
||||
"const_key": "CW-00318",
|
||||
"branch": "const_only",
|
||||
"evidence": "산림 10-11 원문 L5920이 건설 공통 8-2-1 불도저를 직접 지시",
|
||||
"difference_note": "불도저 운전 품을 보완",
|
||||
"status": "확정"
|
||||
},
|
||||
{
|
||||
"forest_key": "FW-00356",
|
||||
"const_key": "CW-00324",
|
||||
"branch": "const_only",
|
||||
"evidence": "산림 11-4 원문 L6080이 건설 공통 8-2-7 모터그레이더를 직접 지시",
|
||||
"difference_note": "모터그레이더 운전 품을 보완",
|
||||
"status": "확정"
|
||||
},
|
||||
{
|
||||
"forest_key": "FW-00373",
|
||||
"const_key": "CW-00244",
|
||||
"branch": "const_only",
|
||||
"evidence": "산림 12-9-2 원문 L6325가 건설 공통 6-1-1 레디믹스트콘크리트 타설을 직접 지시",
|
||||
"difference_note": "레디믹스트콘크리트 타설 품을 보완",
|
||||
"status": "확정"
|
||||
},
|
||||
{
|
||||
"forest_key": "FW-00377",
|
||||
"const_key": "CW-00538",
|
||||
"branch": "const_only",
|
||||
"evidence": "산림 12-11-1 원문 L6374가 건설 토목 6-6 원심력 철근콘크리트관 부설을 직접 지시",
|
||||
"difference_note": "원심력 철근콘크리트관 부설 품을 보완",
|
||||
"status": "확정"
|
||||
},
|
||||
{
|
||||
"forest_key": "FW-00377",
|
||||
"const_key": "CW-00539",
|
||||
"branch": "const_only",
|
||||
"evidence": "산림 12-11-1 원문 L6374가 건설 토목 6-6-1 소켓관 접합을 직접 지시",
|
||||
"difference_note": "소켓관 접합 품을 보완",
|
||||
"status": "확정"
|
||||
},
|
||||
{
|
||||
"forest_key": "FW-00429",
|
||||
"const_key": "CW-00320",
|
||||
"branch": "const_only",
|
||||
"evidence": "산림 13-2-3 원문 L7019이 건설 공통 8-2-3 굴착기를 직접 지시",
|
||||
"difference_note": "굴착기 운전 품을 보완",
|
||||
"status": "확정"
|
||||
},
|
||||
{
|
||||
"forest_key": "FW-00445",
|
||||
"const_key": "CW-00320",
|
||||
"branch": "const_only",
|
||||
"evidence": "산림 13-6-1 원문 L7306이 건설 공통 8-2-3 굴착기를 직접 지시",
|
||||
"difference_note": "굴착기 운전 품을 보완",
|
||||
"status": "확정"
|
||||
},
|
||||
{
|
||||
"forest_key": "FW-00446",
|
||||
"const_key": "CW-00320",
|
||||
"branch": "const_only",
|
||||
"evidence": "산림 13-6-2 원문 L7333이 건설 공통 8-2-3 굴착기를 직접 지시",
|
||||
"difference_note": "굴착기 운전 품을 보완",
|
||||
"status": "확정"
|
||||
},
|
||||
{
|
||||
"forest_key": "FW-00447",
|
||||
"const_key": "CW-00320",
|
||||
"branch": "const_only",
|
||||
"evidence": "산림 13-6-3 원문 L7359이 건설 공통 8-2-3 굴착기를 직접 지시",
|
||||
"difference_note": "굴착기 운전 품을 보완",
|
||||
"status": "확정"
|
||||
},
|
||||
{
|
||||
"forest_key": "FW-00449",
|
||||
"const_key": "CW-00320",
|
||||
"branch": "const_only",
|
||||
"evidence": "산림 13-7-1 원문 L7384이 건설 공통 8-2-3 굴착기를 직접 지시",
|
||||
"difference_note": "굴착기 운전 품을 보완",
|
||||
"status": "확정"
|
||||
},
|
||||
{
|
||||
"forest_key": "FW-00450",
|
||||
"const_key": "CW-00320",
|
||||
"branch": "const_only",
|
||||
"evidence": "산림 13-7-2 원문 L7407이 건설 공통 8-2-3 굴착기를 직접 지시",
|
||||
"difference_note": "굴착기 운전 품을 보완",
|
||||
"status": "확정"
|
||||
},
|
||||
{
|
||||
"forest_key": "FW-00477",
|
||||
"const_key": "CW-00320",
|
||||
"branch": "const_only",
|
||||
"evidence": "산림 14-2 원문 L7820이 건설 공통 8-2-3 굴착기를 직접 지시",
|
||||
"difference_note": "굴착기 운전 품을 보완",
|
||||
"status": "확정"
|
||||
},
|
||||
{
|
||||
"forest_key": "FW-00455",
|
||||
"const_key": "CW-00339",
|
||||
"branch": "const_only",
|
||||
"evidence": "산림 13-10-2 원문 L7511이 건설 공통 8-2-22 디젤파일해머를 직접 지시",
|
||||
"difference_note": "디젤파일해머 운전 품을 보완",
|
||||
"status": "확정"
|
||||
},
|
||||
{
|
||||
"forest_key": "",
|
||||
"const_key": "",
|
||||
"branch": "const_only",
|
||||
"evidence": "산림 2-2-9 원문 L1702; 건설 공통 8-3 기계손료표 확인",
|
||||
"difference_note": "㉮ 건설품셈에 그 규격이 안 실림 — 산림은 1톤 방제차량을 지시하지만 건설 8-3 손료표는 덤프트럭 2.5톤 이상만 수록한다. 8-1-2 [주]④의 개별 산정 대상이며 단일 CW 열쇠로 연결하지 않음",
|
||||
"status": "미판정"
|
||||
},
|
||||
{
|
||||
"forest_key": "",
|
||||
"const_key": "",
|
||||
"branch": "const_only",
|
||||
"evidence": "산림 9-7-1 원문 L4810",
|
||||
"difference_note": "㉮ 원문이 절을 안 밝힘 — 건설표준품셈 토목을 적용하라고만 하고 절 번호를 밝히지 않음",
|
||||
"status": "미판정"
|
||||
},
|
||||
{
|
||||
"forest_key": "",
|
||||
"const_key": "",
|
||||
"branch": "const_only",
|
||||
"evidence": "산림 9-12-1 원문 L4988",
|
||||
"difference_note": "㉱ 구판-현행 절 대응 미확인 — 2018 건설 토목 3-1-3을 밝히지만 현행 2026판 대응 절을 원문만으로 확정할 수 없음",
|
||||
"status": "미판정"
|
||||
},
|
||||
{
|
||||
"forest_key": "FW-00070",
|
||||
"const_key": "CW-00314",
|
||||
"branch": "const_only",
|
||||
"evidence": "산림 2-2-3 원문 L1645가 세부 손료에 건설 공통 8-1-5 기계경비 용어와 정의를 준용; 건설 합본 L5324",
|
||||
"difference_note": "상각비율·정비비율·관리비율 정의를 건설품셈으로 보완",
|
||||
"status": "확정"
|
||||
},
|
||||
{
|
||||
"forest_key": "FW-00070",
|
||||
"const_key": "CW-00315",
|
||||
"branch": "const_only",
|
||||
"evidence": "산림 2-2-3 원문 L1645가 세부 손료에 건설 공통 8-1-6 기계경비 적산요령을 준용; 건설 합본 L5324 이후",
|
||||
"difference_note": "시간당 손료계수 산정과 1일 운전시간 원칙을 건설품셈으로 보완",
|
||||
"status": "확정"
|
||||
},
|
||||
{
|
||||
"forest_key": "FW-00085",
|
||||
"const_key": "CW-00320",
|
||||
"branch": "const_only",
|
||||
"evidence": "산림 3-4-2 원문 L1796이 굴착기 0.2㎥ 시간당 작업량을 건설 공통 8-2-3에 따라 산출하도록 지시; 건설 합본 L5446",
|
||||
"difference_note": "굴착기 작업능력 Q식을 건설품셈으로 보완",
|
||||
"status": "확정"
|
||||
},
|
||||
{
|
||||
"forest_key": "FW-00085",
|
||||
"const_key": "CW-00352",
|
||||
"branch": "const_only",
|
||||
"evidence": "산림 3-4-2 원문 L1796이 굴착기 0.2㎥ 시간당 경비를 건설 공통 8-3-1에 따라 산출하도록 지시",
|
||||
"difference_note": "굴착기 0.2㎥ 손료계수를 건설품셈으로 보완",
|
||||
"status": "확정"
|
||||
},
|
||||
{
|
||||
"forest_key": "FW-00085",
|
||||
"const_key": "CW-00363",
|
||||
"branch": "const_only",
|
||||
"evidence": "산림 3-4-2 원문 L1796이 굴착기 0.2㎥ 시간당 경비를 건설 공통 8-4-1에 따라 산출하도록 지시",
|
||||
"difference_note": "굴착기 0.2㎥ 운전경비를 건설품셈으로 보완",
|
||||
"status": "확정"
|
||||
},
|
||||
{
|
||||
"forest_key": "FW-00124",
|
||||
"const_key": "CW-00158",
|
||||
"branch": "const_only",
|
||||
"evidence": "산림 5-12 원문 L2504가 성토면 흙다짐 필요 시 건설 공통 3-4-3 흙 다지기를 적용하도록 지시; 건설 합본 L2331",
|
||||
"difference_note": "콤팩터·래머·인력 흙다지기 품을 건설품셈으로 보완",
|
||||
"status": "확정"
|
||||
},
|
||||
{
|
||||
"forest_key": "FW-00130",
|
||||
"const_key": "CW-00014",
|
||||
"branch": "const_only",
|
||||
"evidence": "산림 5-16-2 원문 L2594가 200m 초과 떼 운반에 건설 공통 1-2-7 제3호 지게운반을 적용하도록 지시; 건설 합본 L453",
|
||||
"difference_note": "200m 초과 지게운반의 운반량·속도·시간을 건설품셈으로 보완",
|
||||
"status": "확정"
|
||||
},
|
||||
{
|
||||
"forest_key": "FW-00339",
|
||||
"const_key": "CW-00325",
|
||||
"branch": "const_only",
|
||||
"evidence": "산림 10-9 원문 L5857이 4륜구동 1톤 덤프트럭 작업량에 건설 공통 8-2-8을 적용하도록 지시; 건설 합본 L5648",
|
||||
"difference_note": "덤프트럭 작업량 Q식을 건설품셈으로 보완",
|
||||
"status": "확정"
|
||||
},
|
||||
{
|
||||
"forest_key": "FW-00339",
|
||||
"const_key": "CW-00352",
|
||||
"branch": "const_only",
|
||||
"evidence": "산림 10-9 원문 L5857이 손료 산정에 건설 공통 8-3-1의 2.5톤 덤프트럭을 적용하도록 지시",
|
||||
"difference_note": "2.5톤 덤프트럭 손료계수를 대용",
|
||||
"status": "확정"
|
||||
},
|
||||
{
|
||||
"forest_key": "FW-00339",
|
||||
"const_key": "CW-00363",
|
||||
"branch": "const_only",
|
||||
"evidence": "산림 10-9 원문 L5857이 운전경비 산정에 건설 공통 8-4-1의 2.5톤 덤프트럭을 적용하도록 지시",
|
||||
"difference_note": "2.5톤 덤프트럭 운전경비와 주연료 80%를 적용",
|
||||
"status": "확정"
|
||||
},
|
||||
{
|
||||
"forest_key": "FW-00340",
|
||||
"const_key": "CW-00014",
|
||||
"branch": "const_only",
|
||||
"evidence": "산림 10-9-1 원문 L5871이 표에 규격이 없는 재료에 건설 공통 1-2-7 제7호 화물자동차 적재량을 적용하도록 지시; 건설 합본 L489",
|
||||
"difference_note": "산림표에 없는 재료의 1회 표준 적재량을 건설품셈으로 보완",
|
||||
"status": "확정"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"schema_version": "1.0",
|
||||
"dataset_id": "data_work_item_master_manifest",
|
||||
"generated_at": "2026-09-15T05:49:19+09:00",
|
||||
"generated_at": "2026-09-17T18:52:20+09:00",
|
||||
"built_by": "B08_Quantity/B08_Quantity_Build_WorkItemMaster.py",
|
||||
"source": {
|
||||
"dataset_id": "pum_forest",
|
||||
@@ -12,8 +12,8 @@
|
||||
"files": [
|
||||
{
|
||||
"file": "work_item_master_2026-01-01.json",
|
||||
"sha256": "2a0049e73fe48e44c79dda354ae13cf850a61227acdc22645cb94d52e89bbb2b",
|
||||
"size_bytes": 839589
|
||||
"sha256": "546624601feaf9e9c7d757d6013b7c9bc3bc973817fb42ded70f3b2306b6778d",
|
||||
"size_bytes": 924964
|
||||
},
|
||||
{
|
||||
"file": "form_undetermined_2026-01-01.json",
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"schema_version": "1.0",
|
||||
"dataset_id": "data_work_item_master_manifest",
|
||||
"generated_at": "2026-09-17T18:52:53+09:00",
|
||||
"built_by": "B08_Quantity/B08_Quantity_Build_WorkItemMaster.py",
|
||||
"source": {
|
||||
"dataset_id": "pum_const",
|
||||
"effective_date": "2026-01-01",
|
||||
"sha256": "dbc46ef1316b83ed710f25bc402dcecc4a979bbfc4a6641a0554252bca326ca7",
|
||||
"file": "pum_const_2026.json"
|
||||
},
|
||||
"files": [
|
||||
{
|
||||
"file": "const_work_item_master_2026-01-01.json",
|
||||
"sha256": "e8e3bc48c2563246b7a6fd4b5bc5129fe00c5f112fa190d78119912beddf40ff",
|
||||
"size_bytes": 3839500
|
||||
},
|
||||
{
|
||||
"file": "const_form_undetermined_2026-01-01.json",
|
||||
"sha256": "dbb4f245cb9aee71325ab73c1325ee5f06ff19e0e7527de2053d08a7eb16282e",
|
||||
"size_bytes": 486761
|
||||
},
|
||||
{
|
||||
"file": "const_basis_missing_2026-01-01.json",
|
||||
"sha256": "0ae14342e37ba5f394c36cc4dd8cea1b505b13d6520e0a12979c21676d6ffcdb",
|
||||
"size_bytes": 74228
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
{
|
||||
"schema_version": "1.0",
|
||||
"dataset_id": "work_item_axis_policy",
|
||||
"effective_date": "2026-01-01",
|
||||
"note": "공종 축의 줄 구실(axis_role) 잣대. 코드에 박지 않고 여기 둔다 — 기관이 정하는 범위라 시스템관리자가 고칠 값이다.",
|
||||
"policy": {
|
||||
"never_delete": true,
|
||||
"why": "표 없는 마디 가운데 상당수가 `parent_mode`(계산 규칙)를 지니고, 총칙 장에는 원가가 실제로 읽는 값표가 붙어 있다. 지우면 계산 규칙과 값이 함께 사라진다(2026-09-17 산림에서 확인 · 브레인 승인).",
|
||||
"flat_list_filter": "pum_tables > 0",
|
||||
"flat_list_why": "「잎」으로 거르면 부모 마디에 붙은 표가 통째로 안 뜬다(산림 5건)."
|
||||
},
|
||||
"roles": {
|
||||
"work_item": {
|
||||
"name_ko": "공종",
|
||||
"rule": "품셈 표가 붙은 줄",
|
||||
"keep": true,
|
||||
"list_label": "공종",
|
||||
"hidden_by_default": false
|
||||
},
|
||||
"group": {
|
||||
"name_ko": "묶는 마디",
|
||||
"rule": "표는 없고 아래를 묶는 줄",
|
||||
"keep": true,
|
||||
"why": "`parent_mode`(choose_one·sum_steps)가 여기 붙는다 — 계산 규칙을 지닌 줄이다.",
|
||||
"list_label": "묶는 마디",
|
||||
"hidden_by_default": true
|
||||
},
|
||||
"general_provision": {
|
||||
"name_ko": "총칙",
|
||||
"rule": "`general_provision` 뿌리 아래의 줄",
|
||||
"keep": true,
|
||||
"why": "문서 구역이나 값표가 붙어 원가가 읽는다.",
|
||||
"list_label": "공종 아님 · 규정·기준",
|
||||
"hidden_by_default": true
|
||||
},
|
||||
"empty": {
|
||||
"name_ko": "빈 잎",
|
||||
"rule": "표도 아래도 없는 잎",
|
||||
"keep": true,
|
||||
"why": "원문에 표가 안 붙은 자리. 지우면 「있었는지 없었는지」가 안 남는다.",
|
||||
"list_label": "빈 잎",
|
||||
"hidden_by_default": true
|
||||
}
|
||||
},
|
||||
"decision_order": [
|
||||
"general_provision",
|
||||
"work_item",
|
||||
"group",
|
||||
"empty"
|
||||
],
|
||||
"axes": {
|
||||
"forest": {
|
||||
"name_ko": "산림 공종",
|
||||
"settled": true,
|
||||
"source": "resources/data_cost_input_value/pum_forest_2026.json",
|
||||
"toc_source": "목차표 F0001 — 원문이 목차를 표로 싣는다",
|
||||
"code_prefix": "FP",
|
||||
"key_prefix": "FW",
|
||||
"general_provision_roots": [
|
||||
"FP-01",
|
||||
"FP-02"
|
||||
],
|
||||
"general_provision_basis": "FP-01 적용기준 51줄 · FP-02 소요재료 및 기계손료 27줄. ⚠ 지우면 안 됨 — `B09_Estimation_Consumables.py` 가 FP-02-01-01·FP-02-01-08(연료 F0042·F0043·F0058)과 FP-02-02-01/02/04/05/06(손료계수 F0064~F0069)을 읽는다.",
|
||||
"counts": {
|
||||
"rows": 477,
|
||||
"work_item": 304,
|
||||
"group": 82,
|
||||
"general_provision": 78,
|
||||
"empty": 13
|
||||
},
|
||||
"known_defects": [
|
||||
"갈래 문자열이 그대로 줄 열쇠가 됨(`@id` = `FW-00105#1.1∼1.5`) — 품셈 원문이 물결표를 두 가지로 쓰고(∼ U+223C · ~ U+FF5E) 자간 공백만 다른 짝도 있어, 문자열이 손질되면 열쇠가 갈린다. ⬜ 갈래도 장부에 올릴지 판정 대기(2026-09-17 데스크탑 서브)."
|
||||
]
|
||||
},
|
||||
"const": {
|
||||
"name_ko": "건설 공종",
|
||||
"settled": true,
|
||||
"source": "resources/data_cost_input_value/pum_const_2026.json",
|
||||
"toc_source": "합본 원문 머리 「목 차」 글줄 — 목차표가 없어 랩탑 메인이 글에서 읽음(부문 5 · 장 45 · 1,367줄)",
|
||||
"code_prefix": "CP",
|
||||
"key_prefix": "CW",
|
||||
"code_shape": "CP-<부문2>-<장2>-<절…>",
|
||||
"code_shape_why": "⚠ **부문마다 장 번호가 1부터 다시 시작한다** — 공통 1장은 적용기준, 토목 1장은 도로포장공사다. 부문 자리가 없으면 부문이 다른 공종끼리 덮는다(산림에서 목차 번호 겹침 둘이 실제로 덮고 있었다).",
|
||||
"general_provision_roots": [
|
||||
"01_공통부문/제1장_적용기준.md",
|
||||
"01_공통부문/제8장_건설기계.md"
|
||||
],
|
||||
"general_provision_basis": "공통부문 제1장 적용기준(`CP-01-01`) 줄 37 · 표 46 — 산림 FP-01 과 짝. 공통부문 제8장 건설기계(`CP-01-08`) 줄 75 · 표 351 — 산림 FP-02(소요재료 및 기계손료)와 짝. 시공능력·손료 파라미터라 값을 정하는 자리다(C0426·C0427 은 이미 기초데이터로 꺼냈다). 2026-09-17 안티그래비티 조사 · 브레인 확정.",
|
||||
"general_provision_pending": [],
|
||||
"known_defects": [
|
||||
"형태 미판정 881 / 2,192 — B09 표 읽기가 산림 표로만 검증됨",
|
||||
"밑수 미확보 447 — 곱하면 안 되는 줄이라 받는 쪽이 가려야 함",
|
||||
"갈래 키 비어 있음 — 랩탑 메인이 뒤로 미룸"
|
||||
],
|
||||
"roots_are": "**장 파일 이름**(「01_공통부문/제1장_적용기준.md」). 뽑는 코드가 목차 코드(`CP-01-01`)로 바꿔 준다 — 관리자가 부문 번호를 셀 필요가 없다.",
|
||||
"counts": {
|
||||
"rows": 1367,
|
||||
"work_item": 997,
|
||||
"group": 270,
|
||||
"general_provision": 0,
|
||||
"empty": 100,
|
||||
"note": "총칙 표시 전 셈(랩탑 메인 첫 산출). 뿌리를 붙이면 `general_provision` 으로 옮겨 간다 — 줄이 줄지는 않는다."
|
||||
},
|
||||
"settled_note": "유지관리부문 제1장 「공 통」(`CP-05-01`) 표 40 은 **공종**으로 둔다 — 총칙이 아니다(2026-09-17 안티그래비티 조사 · 브레인 확정)."
|
||||
}
|
||||
},
|
||||
"list_default": {
|
||||
"filter": "axis_role",
|
||||
"value": "work_item",
|
||||
"why": "목록 기본은 **공종만**(산림 306 · 건설 983). 총칙 줄은 표가 붙어 있어 목록에 섞여 뜨는데, 공종이 아니라 규정·기준이다. 상자를 따로 만들지 않고 **기본 감춤 + 고르면 보임**으로 간다 (2026-09-17 브레인). ⚠ 지운 것이 아니라 감춘 것이다."
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -8,8 +8,8 @@ sources:
|
||||
- doc: 산림사업 표준품셈 (산림청고시 제2025-82호)
|
||||
version: 2026-01-01 시행
|
||||
loc: original/행정규칙/임도 품셈 적용기준 (현 산림사업 표준품셈)/
|
||||
status: verified
|
||||
last_updated: 2026-08-15
|
||||
status: draft
|
||||
last_updated: 2026-09-17
|
||||
---
|
||||
|
||||
관련 문서: [품셈_적용기준](품셈_적용기준.md) · [수량산출_일반](수량산출_일반.md) · [기계경비_산정](../05_원가정보/기계경비_산정.md) · [원가_기초데이터_원천](../05_원가정보/원가_기초데이터_원천.md)
|
||||
@@ -43,14 +43,81 @@ last_updated: 2026-08-15
|
||||
| 유지보수 (기존 임도 보수) | [05_유지관리부문](../../../original/원가계산/건설공사_표준품셈/05_유지관리부문/) 4장 | 낮음 | 해체·보수 품 ('22 신설 다수) |
|
||||
| (참고) 건축·기계설비 부문 | 03_건축 11장 · 04_기계설비 13장 | 낮음 | 임도 통상 미사용 — 특수 부대시설 시 참조 |
|
||||
|
||||
## 3. 적용 시 주의
|
||||
## 3. 두 품셈 연결고리 (2026-09-17 밑그림)
|
||||
|
||||
### 3.1 판정 원칙
|
||||
|
||||
- 산림품셈 총칙 1-1 제7호는 `본 품셈에 명시되지 않은 품`에 타 부문 표준품셈을 적용하고, 타 부문과 유사한 공종은 산림품셈을 우선하도록 한다(산림 원문 L535). 따라서 이름이 같거나 비슷하다는 이유만으로 자동 연결하지 않는다.
|
||||
- 아래 **확정**은 산림 원문의 직접 지시 또는 현행 데이터·계산의 출처 기록이 있는 것만 뜻한다. 같은 기능처럼 보여도 절 대 절 대응 근거가 없으면 **미판정**이다.
|
||||
- 이 표는 연결의 존재를 기록한 것이며, 한쪽 품을 다른 쪽 품으로 통째로 바꿀 수 있다는 뜻이 아니다.
|
||||
- 기계 판독용 연결은 `resources/data_work_item_link/work_item_link_2026-01-01.json`에 둔다. 단일 절로 확정된 연결만 FW-/CW- 불변 열쇠를 붙이고, 여러 절을 묶은 행과 미판정 행에는 대표 열쇠를 임의로 붙이지 않는다.
|
||||
- 건설품셈 절 번호는 공통·토목·건축·기계설비·유지관리 **부문마다 1장부터 다시 시작**한다. 따라서 `8-1` 같은 번호만으로 CW- 열쇠를 찾지 않고 부문과 전체 경로를 함께 확인한다.
|
||||
|
||||
### 3.2 산림에만 확인된 갈래
|
||||
|
||||
| 산림품셈 범위 | 건설품셈 대응 | 판정·근거 |
|
||||
|---|---|---|
|
||||
| 제2장 조림·제3장 숲가꾸기·제4장 산림병해충방제·제6장 산림경영계획·제7장 수확 | 미판정 | 2026 건설품셈 5부문 45장에 같은 산림사업 공종표는 확인되지 않음. 비슷한 이름의 조경·유지관리 품과 자동 연결하지 않음 |
|
||||
| 13-13-1 목재틀흙막이 | 미판정 | 산림 원문에 구조·규격별 품이 있으나 2026 건설품셈에서 같은 표를 확인하지 못함 |
|
||||
| 10-9-1 5t 미만 운반장비의 표준적재량(산악지 사방공사) | 미판정 | 산림 원문 고유 조건의 표. 건설 운반표와 절 대 절 대응 근거 없음 |
|
||||
|
||||
### 3.3 산림에 값이 없어 건설품셈으로 잇는 갈래
|
||||
|
||||
| 산림 공종·쓰임 | 건설품셈 근거 | 연결 상태 |
|
||||
|---|---|---|
|
||||
| 전 공종의 건설기계 손료·운전경비 | 공통 8-1 적용기준, 8-3 기계손료, 8-4 운전경비 산정 | **확정·사용 중** — 기계 기초값·손료·운전경비 층. 산림 원문도 굴삭기·크레인 등에서 건설품셈을 직접 지시(2-2-11, L1720~1723) |
|
||||
| 모르타르 1:3 배합 | 건축 9-1-1 모르타르 배합(시멘트 510kg, 모래 1.10㎥, 보통인부 0.66인/㎥) | **확정·사용 중** — 산림 13-4 돌쌓기에는 배합 원단위가 없어 `masonry_wet`의 `AX-WK-c0842a0d`가 사용 |
|
||||
| 재료 할증 중 산림 1-3-1에 없는 재료 | 공통 1-3-1 재료의 할증 | **확정·사용 중** — 산림표가 정본이고 누락 재료만 건설표가 보완함 |
|
||||
| 12-9-2 산마루 측구의 레미콘 타설 | 공통 6-1-1 레디믹스트콘크리트 타설 | **확정·원문 직접 지시** — 산림 원문 L6325 |
|
||||
| 12-11-1 VR관 소켓접합 | 토목 6-6, 6-6-1 철근콘크리트관 부설 | **확정·원문 직접 지시** — 산림 원문 L6374 |
|
||||
| 9-6~9-10 구조물 철거·깨기 | 공통 8-2-13 브레이커, 유지관리 3-1-1~5 철거 | **확정·원문 직접 지시** — 산림 원문 L4795, L4814~4938 |
|
||||
| 9-11-1 포장 절단 | 토목 1-6-6 포장 절단 | **확정·원문 직접 지시** — 산림 원문 L4957 |
|
||||
| 13-10-2 강재·콘크리트 말뚝박기 | 공통 8-2-22 디젤파일해머 | **확정·원문 직접 지시** — 산림 원문 L7511. 목재말뚝 품 자체와 혼용하지 않음 |
|
||||
|
||||
그 밖에 산림 원문이 직접 지시한 보완은 지게·경운기 운반(1-2-7, L844), 기계식 비탈면 마무리(9-19-2·3, L5448~5465), 노상 다짐·정지·층따기(9-16-2~9-18, L5340~5403), 쇄석·혼합석 부설의 모터그레이더(11-4, L6080)이다. 각각 지시된 기계 또는 세부 품만 잇고 공종 전체를 치환하지 않는다.
|
||||
|
||||
### 3.4 둘 다 있으나 그대로 잇지 못하는 갈래
|
||||
|
||||
| 주제 | 산림품셈 | 건설품셈 | 판정 |
|
||||
|---|---|---|---|
|
||||
| 뿌리제거 | 9-21: 굴삭기 0.2㎥ 기준 소밀도별 0.80/1.01/1.22hr | 공통 3-9-2: 1,000㎡당 굴삭기+그래플 0.2㎥ 3.76hr | **값·구성 다름** — 현행 계산은 건설표에서 `1,000㎡당` 밑수만 빌리고 품은 산림표를 씀. 0.80과 3.76은 4.7배 차이이므로 절 자동 대응은 미판정 |
|
||||
| 무근콘크리트 깨기 | 9-7-1: K=0.55, f=0.67, E=0.35, 180°·22초 | 공통 8-2-13: 산림 원문이 비교한 토목값 E=0.45, 90°·18초 | **확정 연결·값 다름** — K·f는 같고, 임도는 협소한 작업공간·추락위험 때문에 E가 0.10 낮고 사이클이 4초(22.2%) 김(산림 원문 L4814~4828). 건설값으로 치환하지 않음 |
|
||||
| 돌쌓기 | 13-4-4: 석재 종류·찰/메·뒷길이별 ㎡당 품 | 공통 7-1: 작업조 1일 시공량(뒷길이 35/55/75cm, 메 23/26/28㎡·찰 27/30/34㎡) | **표 축·포함범위 다름** — 직접 값 치환 불가. `stone_kind`의 구판 건설품셈 규격별 소요량 fallback은 2026 개정판에서 삭제 검토된 표이므로 현행 연결은 미판정 |
|
||||
| 운반 | 10-4·10-9: 산림 현장용 운반식·적재량 | 공통 8-1-6(2): 운반거리·속도·왕복시간 | **부분 결합** — 산림의 공종·적재 조건과 건설의 시간 산정층을 함께 쓰며 어느 한쪽으로 대체하지 않음 |
|
||||
| 토량환산 | 산림 1-3-2 및 임도 토공 표 | 공통 1-3-2·토공 관련 표 | **미판정** — 산림표 안에도 같은 이름처럼 보이는 토질 행이 다른 계수로 중복되어 이름 대응 금지 |
|
||||
| 재료 할증 | 산림 1-3-1 | 공통 1-3-1 | **부분 결합** — 같은 재료는 산림값 우선, 산림에 없는 재료만 건설값 사용 |
|
||||
| 표면 마무리 | 산림 12-2: 미장공 0.34인, 단위 ㎡당 | 공통 6-1-3: 미장공 0.34인, 단위 100㎡당 | **단위 100배 차이·미판정** — 이름과 숫자만 같고 면적 밑수가 다르므로 연결하면 금액이 100배 어긋남(산림 L6157~6165, 건설 L12~18) |
|
||||
|
||||
> **미판정 재조사 결과:** FW-/CW- 열쇠로 다시 확인했으나 8행의 미판정 사유(대응 절 없음, 내용 차이, 표 축·단위 차이)는 해소되지 않았다. 단위표준·지수판설치·토질은 원문 대조로 확정했지만, 인력은 내용이 다르고 표면마무리는 100배 단위 차이가 있어 연결하지 않는다.
|
||||
|
||||
### 3.5 산림 원문 목차 번호 오기
|
||||
|
||||
| 목차 표기 | 본문에서 확인한 절 | 처리 원칙 |
|
||||
|---|---|---|
|
||||
| `12-17-2`가 철근 펌프카 타설과 무근 진동기 제외에 중복 | 철근은 `12-17-2`, 무근 진동기 제외는 `12-17-3`(본문 L6574, L6586) | 본문 절 번호를 기준으로 열쇠를 붙임 |
|
||||
| `12-24-1`이 뒷채움과 지수판 설치에 중복 | 뒷채움은 `12-24-1`, 지수판 설치는 `12-27-1`(본문 L6671, L6731) | 본문 절 번호를 기준으로 열쇠를 붙임 |
|
||||
|
||||
이는 원문 목차의 중복 표기로, 번호만 읽은 자료 생성에서는 앞 절의 표와 뒤 절의 이름이 합쳐질 수 있다. 연결·갱신 시 목차 번호뿐 아니라 본문 표제와 원문 줄을 함께 확인한다.
|
||||
|
||||
### 3.6 축 범위와 2026-09-17 전수 집계
|
||||
|
||||
| 구분 | 확인 결과 | 근거 |
|
||||
|---|---|---|
|
||||
| 산림 총칙 범위 | 제1장 적용기준과 제2장 소요재료 및 기계손료가 총칙 뿌리다. 공종 목록에서 감출 수는 있어도 원가가 읽는 단위·할증·연료·손료계수 표가 있으므로 삭제하지 않는다. | 산림 원문 L522~1770, `axis_policy.json`의 `forest.general_provision_roots` |
|
||||
| 건설 총칙 범위 | 공통부문 제1장 적용기준은 37줄·46표로 총칙이다. 공통부문 제8장 건설기계 351표는 시공능력·손료 값을 함께 가져 총칙 여부가 미확정이다. 유지관리부문 제1장 공통 40표는 원문 대조에서 공종으로 판정했지만 현재 `axis_policy.json`에는 아직 검토 대기로 남아 있어 합치기 전에는 이름만 보고 총칙으로 빼지 않는다. | 건설 공통 제1장 원문 L1 이후, 제8장 원문, 유지관리 제1장 원문; `axis_policy.json`의 `const.general_provision_basis/pending` |
|
||||
| 장 번호 | 건설품셈은 공통·토목·건축·기계설비·유지관리 **각 부문에서 장 번호가 1부터 다시 시작**한다. | 2026 건설품셈 목차, `_meta.md`; 예: 공통 1장 적용기준·토목 1장 도로포장공사 |
|
||||
| 원문 직접 지시 연결 | 산림 원문에서 건설 절을 밝힌 조사 44건을 중복 쌍으로 접고, 한 줄이 두 절을 밝힌 경우는 나눴다. 기존 직접 참조까지 합친 현행 자료에는 FW-/CW- 50쌍이 있으며, `건설표준품셈에 따른다`고만 한 7건은 열쇠 없이 미판정이다. | 산림 원문 L844·L1720~1723·L2740~7820; `work_item_link_2026-01-01.json` |
|
||||
|
||||
성격·밑수 조사는 **조사 장부와 현재 마스터가 아직 같은 판이 아니다.** 2026-09-17 조사 장부는 산림 458표(소요량 279·계수 42·참조 99·생산성 24·미판정 14), 건설 2,192표(소요량 1,206·계수 497·참조 437·생산성 52)로 분류했다. 또한 밑수 미확보 256표 가운데 계수·서식 101표는 곱셈 밑수가 원래 없는 표여서 실질 미확보는 155표로 판정했다. 그러나 현재 `work_item_master_2026-01-01.json`은 붙은 산림표 460개(소요량 281·계수 42·참조 99·생산성 24·미판정 14), `const_work_item_master_2026-01-01.json`은 건설표 2,192개 중 `pum_form` 미판정 881개·`basis_missing` 447개로 기록한다. 따라서 앞의 조사 장부 수치를 프로그램의 현행 마스터 통계로 읽지 않으며, 분류 결과가 자료 파일에 합쳐진 뒤 다시 검산한다.
|
||||
|
||||
## 4. 적용 시 주의
|
||||
|
||||
- **md 변환 한계**: 장 분할은 표제 탐지 기준 — 장 경계 ±1페이지 겹침 가능, 본문 프로즈 어절 공백 붙음. **수치 인용·검증 시 원본 PDF 대조** (각 장 md 머리 주석 동일).
|
||||
- 원문 표에 가로선이 없는 표는 행 병합 오류 가능성 — 발견 시 원문 PDF 기준으로 복원 (2026-08-14 4-1-1 배관해체 사례).
|
||||
- 단위·수량 기준(C.G.S, 소수 처리)·재료 할증은 [수량산출_일반](수량산출_일반.md), 품 할증 계수는 [품셈_적용기준 §2](품셈_적용기준.md).
|
||||
- 개정 대조: 매년 [개정사항 판](../../../original/원가계산/건설공사_표준품셈/2026년_건설공사_표준품셈_개정사항.md)으로 임도 관련 공종 변경 여부 확인 후 데이터셋 갱신.
|
||||
|
||||
## 4. 설계내역서 연결
|
||||
## 5. 설계내역서 연결
|
||||
|
||||
```
|
||||
공종·수량 (04_수량분석정보 각 문서)
|
||||
@@ -67,6 +134,6 @@ last_updated: 2026-08-15
|
||||
|
||||
| 항목 | 출처 | 위치 |
|
||||
|---|---|---|
|
||||
| 산림품셈 우선·건설품셈 보완 | 산림품셈 1-2, 별표2 | [품셈_적용기준 §1](품셈_적용기준.md) |
|
||||
| 산림품셈 우선·건설품셈 보완 | 산림품셈 1-1 제7호, 별표2 | [품셈_적용기준 §1](품셈_적용기준.md) |
|
||||
| 부문·장 구성 (5부문 45장) | 2026 건설품셈 목차 | [_meta](../../../original/원가계산/건설공사_표준품셈/_meta.md) |
|
||||
| 2026 개정 임도 직결 공종 (돌쌓기 계열) | 개정사항 판 + ESTX 공지 대조 | [_meta](../../../original/원가계산/건설공사_표준품셈/_meta.md) 참고란 |
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
---
|
||||
title: form 미판정 895표 분류
|
||||
category: 05_원가정보
|
||||
status: working
|
||||
last_updated: 2026-09-17
|
||||
sources:
|
||||
- resources/data_work_item_master/work_item_master_2026-01-01.json
|
||||
- resources/data_work_item_master/const_work_item_master_2026-01-01.json
|
||||
- resources/data_work_item_link/work_item_link_2026-01-01.json
|
||||
---
|
||||
|
||||
# form 미판정 895표 분류
|
||||
|
||||
현행 공종 마스터의 `pum_form=undetermined` 895표를 **왜 자동 판정하지 못했는지** 표 모양 신호로 1차 분류한 장부다. 산림 14표와 건설 881표를 나눴다.
|
||||
|
||||
## 1. 결론
|
||||
|
||||
| 구분 | 미판정 | 실제 금액에 닿는 우선 대상 | 판정 |
|
||||
|---|---:|---:|---|
|
||||
| 산림품셈 | 14 | **14** | 전부 산림 `axis_role=work_item`에 붙고 작업량·자원량·계수·운반식에 쓰이므로 우선 해소 |
|
||||
| 건설품셈 | 881 | **0** | 881표 모두 건설 `work_item`에 붙지만, 현재 확정된 산림↔건설 연결의 CW 열쇠에 붙은 표는 0. 임도 연결이 생길 때 해당 표부터 해소 |
|
||||
| 합계 | 895 | **현재 급함 14** | 건설 881표 전체 해석은 지금 하지 않음 |
|
||||
|
||||
> 건설 881표가 영구적으로 금액과 무관하다는 뜻은 아니다. 현재 임도 연결고리가 그 표들을 소비하지 않는다는 뜻이다. 사용자가 건설 공종을 직접 세우거나 새 연결을 확정하면 그 공종의 미판정 표가 금액 대상이 된다.
|
||||
|
||||
## 2. 분류 방법
|
||||
|
||||
빈 셀을 제외한 `raw_row`를 아래 우선순위로 한 번씩만 분류했다.
|
||||
|
||||
1. **빈 표** — 값 셀이 하나도 없음.
|
||||
2. **공식·설명 합침** — 긴 설명문 또는 `=`가 든 공식이 한 셀에 합쳐짐.
|
||||
3. **조건·요율표** — `요율·할증률·손율·Type·작업환경·조건` 표로, 자원명·단위·수량 머리가 없음.
|
||||
4. **글값** — `견적처리·별도계상·별산·필요시`처럼 숫자 대신 처리 지시가 값에 들어감.
|
||||
5. **표 머리 소실** — 첫 행 절반 이상이 숫자·규격인데 자원명·단위·수량 머리를 읽지 못함.
|
||||
6. **혼합 행렬** — 다단 머리, 가로 전개, 병합 셀 등 나머지 복합 구조.
|
||||
|
||||
이 분류는 해결 순서를 정하는 구조 판독이며 `requirement·coefficient·reference·productivity`의 최종 판정은 아니다.
|
||||
|
||||
## 3. 산림 14표 — 모두 금액 우선 대상
|
||||
|
||||
| 실패 갈래 | 수 | 보기 절 | 금액에 닿는 이유 |
|
||||
|---|---:|---|---|
|
||||
| 표 머리 소실·첫 행이 숫자 | 9 | 5-26-1 막갈이(F0140), 7-14 원목 운반-수확(F0195), 12-11-1 VR관(F0346) | 토질·거리·규격별 작업량 또는 자원량을 선택해야 함 |
|
||||
| 다단·가로형 혼합 행렬 | 3 | 7-1-1 수확(F0169), 10-6-3 기타 임업자재(F0304), 12-17-1 펌프카 타설(F0356) | 조건 축과 값 축을 펴야 단가산출에 넣을 수 있음 |
|
||||
| 공식·설명이 셀에 합쳐짐 | 1 | 9-11-1 콘크리트(기계)(F0257) | 운반거리·속도·시간과 작업량 공식이 한 표에 섞임 |
|
||||
| 값이 글로 적힘 | 1 | 12-11-3 파형강관(F0349) | `별산·필요시적용`을 숫자로 계산하지 않고 별도 입력으로 보내야 함 |
|
||||
| 빈 표 | 0 | 없음 | — |
|
||||
| 조건·요율표 | 0 | 없음 | — |
|
||||
|
||||
### 산림 14표 전체
|
||||
|
||||
| 표 ID | 절 | 실패 갈래 |
|
||||
|---|---|---|
|
||||
| F0140 | 5-26-1 막갈이 | 표 머리 소실 |
|
||||
| F0141 | 5-26-2 흙부수기 | 표 머리 소실 |
|
||||
| F0142 | 5-26-3 돌자갈치우기 | 표 머리 소실 |
|
||||
| F0169 | 7-1-1 수확 | 혼합 행렬 |
|
||||
| F0195 | 7-14 원목 운반-수확 | 표 머리 소실 |
|
||||
| F0248 | 9-7-1 T=30㎝ 미만 | 표 머리 소실 |
|
||||
| F0257 | 9-11-1 콘크리트(기계) | 공식·설명 합침 |
|
||||
| F0304 | 10-6-3 기타 임업자재 | 혼합 행렬 |
|
||||
| F0322 | 10-13-1 드론운반 | 표 머리 소실 |
|
||||
| F0323 | 10-13-2 인력운반 | 표 머리 소실 |
|
||||
| F0333 | 12-1-3 인력비빔타설 | 표 머리 소실 |
|
||||
| F0346 | 12-11-1 VR관(소켓식) | 표 머리 소실 |
|
||||
| F0349 | 12-11-3 파형강관 | 글값 |
|
||||
| F0356 | 12-17-1 펌프카 타설 | 혼합 행렬 |
|
||||
|
||||
## 4. 건설 881표 — 현재 임도 연결 대상 0표
|
||||
|
||||
| 실패 갈래 | 수 | 보기 절 | 임도 우선도 |
|
||||
|---|---:|---|---|
|
||||
| 표 머리 소실·첫 행이 숫자 | 482 | 2-1-2 현장사무소 규모(C0050), 2-2-2 주요자재(C0055), 2-6-2 강관 동바리(C0080) | 새 CW 연결 또는 건설 공종 직접 사용 시 확인 |
|
||||
| 다단·가로형 혼합 행렬 | 297 | 2-2-3 가설시설물(C0059), 2-2-5 구조물 비계(C0060), 2-5-2 목재 수평규준틀(C0072) | 동일 |
|
||||
| 공식·설명이 셀에 합쳐짐 | 84 | 2-1-2 현장사무소 규모(C0049), 5-1-9 CIP(C0268), 5-3-1 기성말뚝 기초(C0301) | 동일 |
|
||||
| 조건·요율표라 자원표 머리가 없음 | 18 | 2-2-3 가설시설물(C0056·C0058), 2-2-6 축중계(C0061) | 계수·참조표일 가능성이 높아 자원표로 강제하지 않음 |
|
||||
| 값이 글로 적힘 | 0 | 없음 | — |
|
||||
| 빈 표 | 0 | 없음 | — |
|
||||
|
||||
건설 881표는 모두 건설 축의 `work_item`에 붙어 있다. 그러나 `work_item_link_2026-01-01.json`에서 `status=확정`인 CW 열쇠와 교차하면 0표다. 따라서 임도 계산의 현재 급한 범위는 산림 14표이고, 건설은 연결 확정 시 해당 CW 공종만 좁혀 판독한다.
|
||||
|
||||
## 5. 처리 순서
|
||||
|
||||
1. 산림 14표의 원문 머리·단위·조건 축을 복원한다.
|
||||
2. F0349의 `별산·필요시적용`은 숫자로 만들지 않고 사용자 입력/별도계상 상태로 보존한다.
|
||||
3. 각 표의 `pum_form`과 밑수를 함께 확정한다. 모양만 고치고 계산 성격을 추정하지 않는다.
|
||||
4. 건설 881표는 새 CW 연결 또는 직접 사용이 생긴 공종만 판독한다.
|
||||
5. 갱신 뒤 `undetermined` 수와 확정 연결 CW 교차 수를 다시 센다.
|
||||
@@ -0,0 +1,61 @@
|
||||
---
|
||||
title: 산림 form 미판정 14표 원문 읽기
|
||||
category: 05_원가정보
|
||||
status: verified
|
||||
last_updated: 2026-09-17
|
||||
sources:
|
||||
- 산림청고시 제2025-82호 산림사업 표준품셈 PDF
|
||||
- resources/knowledge/original/행정규칙/임도 품셈 적용기준 (현 산림사업 표준품셈)/첨부/(산림청고시 제2025-82호) 산림사업 표준품셈.md
|
||||
---
|
||||
|
||||
# 산림 form 미판정 14표 원문 읽기
|
||||
|
||||
`work_item_master_2026-01-01.json`에서 `pum_form=undetermined`인 산림 14표를 공식 PDF 지면과 변환 MD로 대조했다. **자료값은 수정하지 않고**, 각 표를 어떤 머리·축·상태로 읽어야 하는지만 기록한다.
|
||||
|
||||
## 1. 결론
|
||||
|
||||
- 13표는 PDF 지면에서 머리와 읽기 축을 확인했다.
|
||||
- F0333은 머리 구조는 확인했지만 공식 PDF 자체가 앞 표와 같은 숫자를 온도표에 반복한다. 원문 이상 가능성이 있어 숫자의 의미를 임의 보정하면 안 된다.
|
||||
- `별산·별도계산·필요시·설계수량`은 숫자 0이 아니라 별도 입력/계상 상태다.
|
||||
|
||||
## 2. 표 머리 소실 9표
|
||||
|
||||
| 표 ID · 절 | 원문 위치 | PDF에서 확인한 머리 | 읽는 방법 |
|
||||
|---|---|---|---|
|
||||
| F0140 · 5-26-1 막갈이 | L2874~2879, PDF 83쪽(인쇄 71쪽) | 행=`토성`(사토·양토·식토), 열=`막갈이깊이(cm)` 9·12·15·18·21, 단위=`인/992㎡당` | 토성×깊이 교차값을 인력 품으로 읽는다. 단위가 표 아래에 있어 파서가 놓쳤다. |
|
||||
| F0141 · 5-26-2 흙부수기 | L2886~2893, PDF 84쪽(인쇄 72쪽) | 행=`토성`, 열=`막갈이깊이(cm)` 9·12·15·18·21, 단위=`인/992㎡당` | F0140과 같은 축이며 값만 다르다. 표 아래 단위를 붙인다. |
|
||||
| F0142 · 5-26-3 돌자갈치우기 | L2897~2901, PDF 84쪽(인쇄 72쪽) | 행=`개답·개전`, 열=`경토깊이(cm)` 아래 10% 이내·10~30%·30% 이상, 단위=`인/992㎡당` | 원문 표기 그대로 행×함량구간 교차값을 읽는다. `토성/경토깊이` 머리와 백분율 열의 의미가 어색하므로 이름을 고쳐 쓰지 않는다. |
|
||||
| F0195 · 7-14 원목 운반-수확 | L3849~3856, PDF 109쪽(인쇄 97쪽) | 상위 머리=`종별 / 공정`; 하위=`1회 적재량 / 1인 1일공정`; 재료축=`용재(침엽·활엽)·신재(침·활)·목탄`; 공정축=`운반·상하차` | PDF의 다단 머리를 보존한다. ㎥·kg·연km처럼 열마다 단위가 다르므로 한 공통 단위를 붙이지 않는다. |
|
||||
| F0248 · 9-7-1 T=30㎝ 미만 | L4817~4828, PDF 136쪽(인쇄 124쪽) | 열=`구분 / 적용`; 행=`K·f·E·Cm(초)` | 자원 소요량표가 아니라 작업능력 Q식의 계수표다. K=0.55, f=0.67, E=0.35, Cm=22초(180°)를 각각 이름 있는 계수로 읽는다. |
|
||||
| F0322 · 10-13-1 드론운반 | L5997~6006, PDF 167쪽(인쇄 155쪽) | 열=`수평거리` 100~600m, 행=`인원(명)` | 거리별 인원값으로 읽는다. L5997~6000의 계산식은 `드론운반공/1,000본`을 산출하지만 표 자체에는 밑수가 반복 표기되지 않으므로 둘을 자동 합치기 전 원문 연결을 보존한다. |
|
||||
| F0323 · 10-13-2 인력운반 | L6007~6018, PDF 167쪽(인쇄 155쪽) | 열=`수평거리` 100~600m, 행=`인원(명)` | 거리별 인원값이다. 표에 별도 밑수 문구가 없으므로 `명` 이외의 기준량을 추정하지 않는다. |
|
||||
| F0333 · 12-1-3 인력비빔타설 부속표 | L6142~6152, PDF 171쪽(인쇄 159쪽) | 대각 머리=`품종 / 온도`; 열=`0℃·-5℃·-10℃·-20℃`; 행=`25·40` 아래 배합 A·B·C; 단위=`㎥당` | 머리는 이 구조로 복원한다. 다만 값 357·893·931 등은 직전 골재·배합 재료량 표(L6128~6135)와 동일하게 반복된다. 공식 PDF 자체의 이상 가능성이 있어 `시후카 양`으로 단정하거나 값을 고치지 않는다. |
|
||||
| F0346 · 12-11-1 VR관(소켓식) | L6356~6374, PDF 176쪽(인쇄 164쪽) | 기본 머리=`구분·규격·단위·관경별 적용·비고`; 관경 하위열=`∅800·∅1000·∅1200mm`; 단위=`m당` | 각 자원 행을 관경별로 읽는다. `별도계산·설계수량`은 숫자값이 아니라 별도계상 상태다. |
|
||||
|
||||
## 3. 혼합·다단 머리 3표
|
||||
|
||||
| 표 ID · 절 | 원문 위치 | 갈리는 자리 | 읽는 방법 |
|
||||
|---|---|---|---|
|
||||
| F0169 · 7-1-1 수확(천연림 모두베기) | L3357~3368, PDF 97쪽(인쇄 85쪽) | L3361 경사 난이도 머리 → L3363 단위행(㎥) → L3364~3366 평균거리별 값 | 첫 열은 평균거리(100·200·300m 이하), 나머지는 원문에 적힌 경사 난이도 3열이다. 단위행을 값 행으로 읽지 않는다. |
|
||||
| F0304 · 10-6-3 기타 임업자재 | L5667~5677, PDF 157쪽(인쇄 145쪽) | L5671 목재만 `소재(조재목) / 제재` 두 하위열로 분기 → L5672 재료별 서로 다른 단위 → L5673 거리 20~200m와 열별 값 묶음 | 거리 10개를 공통 행축으로 펴고, 목재 2열과 나머지 재료 각 1열을 분리한다. 단위는 인/㎥·인/100속·인/100매·인/100개·인/100본·인/1000㎡·인/톤으로 열마다 보존한다. |
|
||||
| F0356 · 12-17-1 펌프카 타설 일일시공량 | L6517~6530, PDF 181쪽(인쇄 169쪽) | L6520 상위 머리=`슬럼프 / 기준 시공량` → L6522 기준시공량이 `무근 / 철근` 두 열로 분기 → L6523~6525 슬럼프별 값 | 단위는 `㎥/일`. 슬럼프 8~12·15·18cm 이상별로 무근/철근 값을 읽고, 이후 f1·f2 계수는 별도 표로 적용한다. |
|
||||
|
||||
## 4. 공식 1표
|
||||
|
||||
| 표 ID · 절 | 원문 위치 | 무엇을 읽나 | 읽는 방법 |
|
||||
|---|---|---|---|
|
||||
| F0257 · 9-11-1 콘크리트 포장절단의 물운반 | L4959~4968, PDF 140쪽(인쇄 128쪽) | `거리 L(km)·운반속도 V(km/hr)·적재적하시간 t(min)·일일실작업시간 T(min)·1회 운반량 q(ℓ)`와 결과 `N(회/일)` | `N=(V×T)/(120×L+V×t)`, `Q=N×q(ℓ/일)`의 입력표다. L=0.1, V=2.5, t=4.0, T=480-30, q=25를 이름 있는 변수로 읽고 N·Q는 계산값으로 둔다. |
|
||||
|
||||
## 5. 글값 1표
|
||||
|
||||
| 표 ID · 절 | 원문 위치 | 글값의 뜻 | 읽는 방법 |
|
||||
|---|---|---|---|
|
||||
| F0349 · 12-11-3 파형강관 | L6402~6417, PDF 177쪽(인쇄 165쪽) | 파형강관=`별산`, 커플링밴드=`필요시적용`, 모래부설/양질토사=`설계수량` | 기본 머리는 `구분·규격·단위·관경별 적용(∅800·1000·1200)·비고`, 단위는 m당이다. 세 글값은 0이 아니라 각각 별도 가격, 조건부 추가, 설계 입력 상태로 보존한다. 커플링밴드는 [주]①의 `1EA/6m`를 필요 시 별도 산정한다. |
|
||||
|
||||
## 6. 자료 반영 시 주의
|
||||
|
||||
- 이 문서는 원문 읽기 명세이며 마스터 값을 직접 바꾸지 않는다.
|
||||
- PDF 아래·위에 떨어진 단위도 표와 함께 보존한다.
|
||||
- 다단 머리는 한 줄로 합치지 말고 상위축·하위축을 나눈다.
|
||||
- 공식의 입력값과 계산 결과를 같은 자원값으로 저장하지 않는다.
|
||||
- F0333은 원문 정오 근거가 나오기 전 숫자 의미를 확정하지 않는다.
|
||||
@@ -0,0 +1,93 @@
|
||||
---
|
||||
title: 현재 프로그램 미판정 목록
|
||||
category: 05_원가정보
|
||||
status: working
|
||||
last_updated: 2026-09-17
|
||||
sources:
|
||||
- resources/data_work_item_link/work_item_link_2026-01-01.json
|
||||
- resources/data_work_item_master/work_item_master_2026-01-01.json
|
||||
- resources/data_work_item_master/const_work_item_master_2026-01-01.json
|
||||
- resources/knowledge/technical_info/01_임도/05_원가정보/산림품셈_결측밑수_155개_자리명세.json
|
||||
- antigravity commit 2dab2ce2
|
||||
---
|
||||
|
||||
# 현재 프로그램 미판정 목록
|
||||
|
||||
2026-09-17 판에서 프로그램이 자동으로 결정하지 못하는 자리를 한 벌로 모은 장부다. **미판정은 임의 기본값으로 메우지 않는다.** 금액 영향은 공종이 실제 설계에 서는 경우를 기준으로 가렸다.
|
||||
|
||||
## 1. 한눈에 보기
|
||||
|
||||
| 층 | 미판정 수 | 금액 영향 |
|
||||
|---|---:|---|
|
||||
| 산림↔건설 연결고리 | 10행 / 전체 78행 | 확정 차단 3건, 조건부 차단 1건, 나머지는 산림값 사용 또는 표시·근거 문제 |
|
||||
| 공종 축 역할 | `axis_role=미판정` 0줄; 대신 표·자식이 없는 `empty` 산림 11줄·건설 83줄 | 그 줄 자체에는 계산표가 없어 직접 금액은 만들지 않음. 실제 표가 잘못 빠졌는지는 별도 확인 필요 |
|
||||
| 표 모양 | 산림 14표·건설 881표가 `pum_form=undetermined` | 선택된 표가 소요량인지 계수·참조인지 몰라 자동 계산을 보류해야 함 |
|
||||
| 현행 마스터의 밑수 미확보 | 산림 115표·건설 447표 | 소요량·생산성 표이면 곱셈 기준량이 없어 자동 계산이 막힘 |
|
||||
| 산림 원문 별도 감사 | 결측 후보 256표 중 무차원 계수·서식 101표 제외, 읽어야 할 밑수 155표 | 155표 중 소요량 79·생산성 11 = **금액에 닿는 90표**. 참조 60·형태 미판정 5표 |
|
||||
|
||||
> 현행 마스터의 `basis_missing` 115·447과 원문 감사의 155는 집계 정의와 생성 시점이 다르다. 더하거나 같은 수로 보지 않는다. 155표의 실제 자리는 `산림품셈_결측밑수_155개_자리명세.json`에 표 ID·절·원문 줄·읽기 규칙으로 남아 있다(본문 위 48, 표 머리 107).
|
||||
|
||||
## 2. 연결고리 미판정 10건
|
||||
|
||||
### ㉮ 근거될 현행 절이 없음 — 4건
|
||||
|
||||
| 대상 | 미판정 이유 | 금액 영향 |
|
||||
|---|---|---|
|
||||
| 산림 제2·3·4·6·7장 산림사업 공종군 | 건설 5부문 45장에서 같은 공종표를 확인하지 못함 | **차단 아님** — 산림품셈이 정본. 비슷한 조경·유지관리 품으로 자동 대체하지 않음 |
|
||||
| 산림 13-13-1 목재틀흙막이 | 건설품셈에 같은 구조·규격별 절이 없음 | **차단 아님** — 산림표 사용. 건설 보완값은 없음 |
|
||||
| 산림 9-7-1 무근콘크리트 깨기 | 원문 L4810은 `건설표준품셈 토목`만 쓰고 절을 밝히지 않음 | **차단 아님** — 같은 공종의 브레이커 FW-00260↔CW-00330 연결과 산림 고유 효율값은 별도로 확정됨 |
|
||||
| 산림 9-12-1 인력 터파기 | 원문 L4988은 2018 건설 토목 3-1-3만 밝히며 현행 2026 대응 절 근거가 없음 | **확정 차단** — 해당 공종이 서면 현행 품을 자동 선택할 수 없어 수동 근거가 필요 |
|
||||
|
||||
### ㉯ 이름은 닮았으나 내용이 다름 — 2건
|
||||
|
||||
| 대상 | 미판정 이유 | 금액 영향 |
|
||||
|---|---|---|
|
||||
| 산림·건설 1-3-2 토량환산 | 같은 토질명처럼 보여도 계수와 적용표가 다름 | 산림 공종은 산림값 우선이므로 연결 자체는 차단 아님. 이름 자동 매칭 시 토공량·금액 오산 위험 |
|
||||
| 산림·건설 1-2-5 | 산림은 노임단가 규정, 건설은 작업반장·안전인력 등 인력 편성 규정 | 직접 차단 아님. 같은 절 번호라는 이유로 연결하면 다른 규칙이 붙음 |
|
||||
|
||||
### ㉰ 표 축·단위가 달라 못 이음 — 3건
|
||||
|
||||
| 대상 | 미판정 이유 | 금액 영향 |
|
||||
|---|---|---|
|
||||
| 뿌리제거: 산림 9-21 ↔ 건설 공통 3-9-2 | 산림은 소밀도별 0.80/1.01/1.22hr, 건설은 굴착기+그래플 3.76hr/1,000㎡ | 산림 품을 쓰면 직접 차단 아님. 건설값으로 치환하면 최소 비교값 기준 4.7배 오산 |
|
||||
| 돌쌓기: 산림 13-4-4 ↔ 건설 공통 7-1 | 산림은 석재·찰/메·뒷길이별 ㎡당, 건설은 작업조 1일 시공량 | **조건부 차단** — 현행 규격이 산림표에 없고 구판 건설 fallback에만 있으면 수동 판정 필요 |
|
||||
| 표면 마무리: 산림 12-2 ↔ 건설 공통 6-1-3 | 숫자 0.34는 같지만 산림은 ㎡당, 건설은 100㎡당 | **확정 차단** — 단위 불일치 해소 전 자동 연결 금지. 잘못 연결하면 100배 오산 |
|
||||
|
||||
### ㉱ 규격이 원문에 없음 — 1건
|
||||
|
||||
| 대상 | 미판정 이유 | 금액 영향 |
|
||||
|---|---|---|
|
||||
| 산림 2-2-9 1톤 방제차량 | 산림 L1702는 건설품셈 준용을 지시하지만 건설 8-3 손료표에는 덤프트럭 2.5톤 이상만 있고 1톤 방제차량 규격이 없음 | **확정 차단** — 8-1-2 [주]④에 따른 개별 산정 전에는 손료·운전경비를 만들 수 없음 |
|
||||
|
||||
## 3. 공종 축과 표 판독 미판정
|
||||
|
||||
### 3.1 `axis_role`
|
||||
|
||||
- 현행 두 마스터에 `axis_role="미판정"` 값은 없다.
|
||||
- 아직 역할을 확정하지 못한 것에 가장 가까운 값은 `empty`다: 산림 11줄, 건설 83줄.
|
||||
- `empty`는 표도 자식도 없는 목차 줄이라 직접 계산값은 없다. 다만 원문 표가 다른 줄에 잘못 붙었거나 누락된 것인지는 별도 대조해야 한다.
|
||||
- 산림 예: 3-4-1 신설 및 보수 기준, 3-4-2 임산물 운반로 및 작업로 신설비 산정, 9-17-1 비탈면 다짐. 건설 예: 공통 2-2-4 구조물 동바리, 8-1-5 기계경비 용어와 정의.
|
||||
|
||||
### 3.2 표 모양과 밑수
|
||||
|
||||
| 판독 상태 | 산림 | 건설 | 프로그램 처리 |
|
||||
|---|---:|---:|---|
|
||||
| `pum_form=undetermined` | 14 | 881 | 소요량·계수·참조·생산성 중 무엇인지 확정 전 자동 계산 금지 |
|
||||
| `basis_missing` | 115 | 447 | 소요량·생산성 표는 기준량을 읽기 전 계산 금지. 계수·참조표는 밑수가 원래 없을 수 있어 먼저 성격 판정 |
|
||||
| 산림 원문에서 회수 가능한 밑수 | 155 | 해당 없음 | 본문 윗줄 48·표 머리 107. 소요량·생산성 90표부터 반영 우선 |
|
||||
|
||||
## 4. 금액 계산을 실제로 막는 우선순위
|
||||
|
||||
1. **1톤 방제차량** — 현행 건설품셈 규격 자체가 없어 개별 산정 필요.
|
||||
2. **9-12-1 인력 터파기** — 2018 절과 2026 절 대응 근거 필요.
|
||||
3. **12-2 표면 마무리** — ㎡와 100㎡ 중 어느 판 단위가 맞는지 원문 정정·해석 필요.
|
||||
4. **13-4-4 돌쌓기 규격 fallback** — 해당 규격을 쓰는 설계에서 현행 근거를 수동 지정해야 함.
|
||||
5. **밑수 90표** — 산림 결측 155표 중 소요량 79·생산성 11표. 표가 실제 공종에 선택되면 기준량 판독 전 계산 보류.
|
||||
6. **건설 `basis_missing` 447표·형태 미판정 881표** — 임도 설계에 선택되는 표부터 원문 대조. 전부가 금액 공종은 아님.
|
||||
|
||||
## 5. 갱신 원칙
|
||||
|
||||
- 근거 절·원문 줄·단위가 확인된 항목만 이 목록에서 제거한다.
|
||||
- 연결고리 행을 확정하면 FW-/CW- 열쇠 존재와 중복을 검사한다.
|
||||
- `pum_form`과 밑수는 같은 표에서 함께 확인한다. 계수·참조표에 억지 밑수를 넣지 않는다.
|
||||
- 집계 수는 `work_item_link`, 산림·건설 공종 마스터, 결측밑수 자리명세를 다시 읽어 갱신한다.
|
||||
@@ -53,6 +53,9 @@ WORK_ITEM_KEYS = {
|
||||
"origin",
|
||||
# 공종 코드가 본 품셈 판 — B09 가 마스터 판과 다르면 값을 안 씀(명세 17장 · 2026-09-13 추가).
|
||||
"pum_edition",
|
||||
# 불변 열쇠(FW-·AX-) — 목차 번호가 개정으로 밀려도 안 바뀜(2026-09-17 8-6 이행 길목 ·
|
||||
# 받는 쪽 B09 `parse_handoff` 가 읽음). 코드 없는 줄은 None.
|
||||
"work_item_key",
|
||||
}
|
||||
|
||||
#: **자재 줄**이 내보내는 칸. ⚠ 여기에 `work_item_code` 가 들어가면 안 된다(축이 다르다).
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
@@ -21,7 +22,13 @@ LABELS_PATH = ROOT / "resources" / "data_master_labels" / "labels_2026-01-01.jso
|
||||
#: 브레인 갈래 나눔표(2026-09-15). 폴더 장부(_manifest)는 그 31 밖이라 따로 센다.
|
||||
#: 2026-09-16 사용자 판정 — `structure_unit_observed`(울진소광 한 현장 관찰값)와
|
||||
#: `revetment_sabang`(교본값에 설명 글 섞임)은 기초데이터가 아니라 **참고 사례**다.
|
||||
BRAIN_KIND_COUNTS = {"logic": 15, "base_value": 9, "byproduct": 4, "seed": 1, "reference": 2}
|
||||
BRAIN_KIND_COUNTS = {
|
||||
"logic": 19,
|
||||
"base_value": 9,
|
||||
"byproduct": 6,
|
||||
"reference": 2,
|
||||
"seed": 1,
|
||||
}
|
||||
MANIFEST_PREFIX = "manifest_"
|
||||
|
||||
META_KEYS = {
|
||||
@@ -130,7 +137,9 @@ def test_갈래_나눔이_브레인_표와_같다(labels):
|
||||
continue
|
||||
counted[entry["kind"]] = counted.get(entry["kind"], 0) + 1
|
||||
assert counted == BRAIN_KIND_COUNTS
|
||||
assert sum(counted.values()) == 31
|
||||
# 2026-09-17 31 → 37 — 공종 축 산출 일곱을 이름표에 올림(건설 마스터·미판정·밑수 · 열쇠 장부 둘 ·
|
||||
# 구실 잣대 · 건설 폴더 지문). 지문은 manifest 라 이 셈에서 빠진다.
|
||||
assert sum(counted.values()) == 37
|
||||
|
||||
|
||||
def test_갈래_이름이_다_풀려_있다(labels):
|
||||
@@ -192,6 +201,31 @@ def test_표_이름이_비지_않았다(labels):
|
||||
assert not blank, f"한글 이름이 빈 표: {blank}"
|
||||
|
||||
|
||||
def test_이름이_영문으로_떨어진_칸이_없다(labels):
|
||||
"""⚠ 빈칸이 아니라 **영문 키가 그대로 이름 자리에 앉은** 것을 잡는다(2026-09-17 데스크탑 서브).
|
||||
|
||||
새 파일을 이름표에 올릴 때 이름을 못 지으면 키를 그대로 베끼기 쉬운데, 그러면 화면에
|
||||
`general_provision_roots` 같은 영문이 뜬다. 한글이 한 글자도 없으면 이름을 안 지은 것이다.
|
||||
"""
|
||||
hangul = re.compile(r"[가-힣]")
|
||||
bad = []
|
||||
for entry in labels["files"]:
|
||||
for table in entry["tables"]:
|
||||
for column in table["columns"]:
|
||||
if not hangul.search(column["name_ko"]):
|
||||
bad.append(f"{entry['file_id']}::{table['key']}/{column['key']}")
|
||||
if not hangul.search(table["name_ko"]):
|
||||
bad.append(f"{entry['file_id']}::{table['key']}")
|
||||
for group in entry["value_groups"]:
|
||||
if not hangul.search(group["name_ko"]):
|
||||
bad.append(f"{entry['file_id']}::{group['key']}")
|
||||
for merged in labels["merged_tables"]:
|
||||
for column in merged["columns"]:
|
||||
if not hangul.search(column["name_ko"]):
|
||||
bad.append(f"{merged['key']}/{column['key']}")
|
||||
assert not bad, f"영문으로 뜨는 칸: {bad}"
|
||||
|
||||
|
||||
def test_값_묶음_이름이_비지_않았다(labels):
|
||||
blank = [
|
||||
f"{entry['file_id']}::{group['key']}"
|
||||
@@ -470,6 +504,8 @@ MERGED_KEYS = (
|
||||
"stone_kind",
|
||||
"masonry_class",
|
||||
"machine_productivity",
|
||||
"work_item_forest", # 2026-09-17 공종 축 산림
|
||||
"work_item_const", # 2026-09-17 공종 축 건설
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
"""공종 불변 열쇠 **길목** — PLAN_공종축 8-6 이행(2026-09-17 브레인 · 코덱스 조사).
|
||||
|
||||
잣대 = **금액 불변.** 열쇠를 더하는 일이지 값을 바꾸는 일이 아님 —
|
||||
같은 인계로 세운 내역서가 열쇠가 있든(새 인계) 없든(옛 인계) **한 원도** 안 달라야 함.
|
||||
길목은 경계 두 곳뿐(B08 인계 출구 · B09 인계 입구) · `work_item_code` 는 목차 번호로 그대로 남음.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import json
|
||||
import re
|
||||
from functools import lru_cache
|
||||
|
||||
from B08_Quantity.B08_Quantity_Engine_Handoff import build_handoff
|
||||
from B08_Quantity.B08_Quantity_Engine_MaterialSummary import build_table as build_material_table
|
||||
from B08_Quantity.B08_Quantity_Engine_UnitQuantity import build_table as build_unit_table
|
||||
from B09_Estimation.B09_Estimation_BillOfQuantities import bill_summary, build_bill
|
||||
from B09_Estimation.B09_Estimation_BillOfQuantities_Input import parse_handoff
|
||||
from B09_Estimation.B09_Estimation_UnitPrice import build_unit_prices
|
||||
from common_util.common_util_work_item_key import MASTER_DIR, attach_keys, work_item_key
|
||||
|
||||
MAPPING = MASTER_DIR.parent / "data_work_item_mapping" / "work_item_mapping_2026-01-01.json"
|
||||
|
||||
|
||||
def _strip_keys(value):
|
||||
"""옛 인계 — 길목이 더한 칸을 뗌."""
|
||||
if isinstance(value, dict):
|
||||
return {
|
||||
k: _strip_keys(v)
|
||||
for k, v in value.items()
|
||||
if k not in ("work_item_key", "unkeyed_work_item_codes")
|
||||
}
|
||||
if isinstance(value, list):
|
||||
return [_strip_keys(v) for v in value]
|
||||
return value
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def _handoff() -> dict:
|
||||
"""토공·운반·구조물·자재가 다 선 인계 — 금액이 실제로 붙는 줄이 여럿."""
|
||||
unit = build_unit_table(
|
||||
[
|
||||
{
|
||||
"structure_id": "s1",
|
||||
"type_id": "masonry_wet",
|
||||
"start_m": 35.0,
|
||||
"end_m": 45.0,
|
||||
"options": {"height_m": 1.5, "length_m": 10.0},
|
||||
}
|
||||
]
|
||||
)
|
||||
rows = [
|
||||
{"group": "흙깎기", "item": "토사", "spec": "", "unit": "㎥", "amount": 1234.5},
|
||||
{"group": "측구터파기", "item": "토사", "spec": "", "unit": "㎥", "amount": 77.0},
|
||||
{"group": "층따기", "item": "", "spec": "", "unit": "㎥", "amount": 12.0},
|
||||
]
|
||||
haul = {
|
||||
"rows": [
|
||||
{
|
||||
"equipment": "dump_truck",
|
||||
"ground": "토사",
|
||||
"volume_m3": 500.0,
|
||||
"average_distance_m": 1200.0,
|
||||
"in_bill": True,
|
||||
}
|
||||
]
|
||||
}
|
||||
return build_handoff(
|
||||
summary_table={"rows": rows},
|
||||
haul_table=haul,
|
||||
unit_quantity_table=unit,
|
||||
material_table=build_material_table(unit),
|
||||
)
|
||||
|
||||
|
||||
def test_목차_코드는_불변_열쇠로_난수_코드는_그대로() -> None:
|
||||
forest = json.loads(
|
||||
(MASTER_DIR / "work_item_master_2026-01-01.json").read_text(encoding="utf-8")
|
||||
)
|
||||
for item in forest["work_items"]:
|
||||
assert work_item_key(item["work_item_code"], forest["pum_edition"]) == item["work_item_key"]
|
||||
assert work_item_key("FP-09-03-02") == work_item_key("FP-09-03-02", "2026-01-01")
|
||||
assert re.fullmatch(r"CW-\d{5}", work_item_key("CP-01-03-02-01") or "")
|
||||
assert work_item_key("AX-WK-c0842a0d") == "AX-WK-c0842a0d" # 난수 8자리 — 이미 불변
|
||||
assert work_item_key("AX-ST-0123abcd") == "AX-ST-0123abcd"
|
||||
assert work_item_key("FP-09-03-02", "1999-01-01") is None # 판이 다르면 안 줌
|
||||
assert work_item_key("FP-99-99") is None and work_item_key(None) is None
|
||||
|
||||
|
||||
def test_매핑의_코드가_다_열쇠를_받음() -> None:
|
||||
"""B08 이 인계에 싣는 코드의 샘 — 하나라도 못 받으면 그 줄은 열쇠 없이 감."""
|
||||
text = MAPPING.read_text(encoding="utf-8")
|
||||
codes = set(re.findall(r'"((?:FP|CP|AX-WK|AX-ST)-[0-9A-Za-z-]+)"', text))
|
||||
assert len(codes) > 50
|
||||
assert [c for c in sorted(codes) if work_item_key(c, "2026-01-01") is None] == []
|
||||
|
||||
|
||||
def test_B08_인계_출구에서_열쇠가_붙고_목차_코드는_그대로() -> None:
|
||||
handoff = _handoff()
|
||||
coded = [row for row in handoff["work_items"] if row.get("work_item_code")]
|
||||
assert len(coded) >= 5
|
||||
for row in coded:
|
||||
assert row["work_item_key"] == work_item_key(row["work_item_code"], row["pum_edition"])
|
||||
assert row["work_item_code"].startswith(("FP-", "AX-")) # 사람이 보는 번호는 남음
|
||||
assert handoff["unkeyed_work_item_codes"] == []
|
||||
assert all("work_item_key" not in row for row in handoff["materials"]) # 자재는 공종 축 아님
|
||||
|
||||
|
||||
def test_못_찾은_코드는_지어내지_않고_목록() -> None:
|
||||
rows = [
|
||||
{"work_item_code": "FP-99-99"},
|
||||
{"work_item_code": None},
|
||||
{"work_item_code": "FP-09-18"},
|
||||
]
|
||||
assert attach_keys(rows) == ["FP-99-99"]
|
||||
assert rows[0]["work_item_key"] is None and rows[1]["work_item_key"] is None
|
||||
assert rows[2]["work_item_key"].startswith("FW-")
|
||||
|
||||
|
||||
def test_B09_입구는_옛_인계에도_같은_열쇠() -> None:
|
||||
new, _ = parse_handoff(_handoff())
|
||||
old, _ = parse_handoff(_strip_keys(copy.deepcopy(_handoff())))
|
||||
assert [w.work_item_key for w in old] == [w.work_item_key for w in new]
|
||||
assert any(w.work_item_key.startswith("FW-") for w in new)
|
||||
|
||||
|
||||
def test_금액_불변_열쇠가_있든_없든_내역서가_한_원도_안_다름() -> None:
|
||||
"""⭐ 이번 일감의 잣대(브레인) — 새 인계(열쇠 있음)와 옛 인계(열쇠 없음)로 세운 내역서가 같음."""
|
||||
build = build_unit_prices()
|
||||
new = build_bill(copy.deepcopy(_handoff()), build=build)
|
||||
old = build_bill(_strip_keys(copy.deepcopy(_handoff())), build=build)
|
||||
assert bill_summary(new) == bill_summary(old)
|
||||
assert [row.as_dict() for row in new.rows] == [row.as_dict() for row in old.rows]
|
||||
assert [row.as_dict() for row in new.material_rows] == [
|
||||
row.as_dict() for row in old.material_rows
|
||||
]
|
||||
priced = [row for row in new.rows if not row.is_group and row.amount_krw]
|
||||
assert len(priced) >= 3 and new.body_total_krw > 0 # 금액이 실제로 선 줄로 잼
|
||||
|
||||
|
||||
#: ⭐ 이행 **전** 코드(4bfbf98f^ · 길목 없음)로 위 `_handoff()` 를 세워 뜬 금액 — 2026-09-17 랩탑 메인.
|
||||
#: 이행 뒤 코드도 내역서 줄 18·성분·원가계산서 지문까지 한 글자 같았음(옛 두 파일을 따로 얹어 맞댐).
|
||||
#: ⚠ 단가 자료(노임·기계·자재·요율)가 바뀌면 이 수도 바뀜 — 그땐 길목 탓이 아닌지 먼저 볼 것
|
||||
#: (열쇠 있든 없든 같은지 = 위 시험) · 길목 탓이면 고칠 것은 코드, 아니면 다시 뜸.
|
||||
PRE_MIGRATION_KRW = {
|
||||
"material_cost": "1158206",
|
||||
"labor_cost": "4400597",
|
||||
"expense": "1991430",
|
||||
"direct_construction_cost": "5983724",
|
||||
"net_construction_cost": "7550233",
|
||||
"total_cost": "9203637",
|
||||
"grand_total": "10124000",
|
||||
}
|
||||
|
||||
|
||||
def test_금액_불변_이행_전_코드로_뜬_원가계산서와_한_원도_같음() -> None:
|
||||
from B09_Estimation.B09_Estimation_BillOfQuantities import cost_input_from_bill
|
||||
from B09_Estimation.B09_Estimation_Engine_Cost import calculate_cost
|
||||
|
||||
bill = build_bill(copy.deepcopy(_handoff()), build=build_unit_prices())
|
||||
totals = calculate_cost(cost_input_from_bill(bill)).totals
|
||||
assert {key: str(totals[key]) for key in PRE_MIGRATION_KRW} == PRE_MIGRATION_KRW
|
||||
@@ -0,0 +1,359 @@
|
||||
"""공종 축 불변 열쇠 검사 — 2026-09-17 브레인 ①.
|
||||
|
||||
여기서 못박는 것은 하나다. **금액이 움직이면 안 된다.**
|
||||
열쇠를 얹는 일은 값을 바꾸는 일이 아니므로, 새 칸을 떼어내면 옛 벌과 **한 글자도 달라선 안 된다.**
|
||||
그 자리를 지키는 것이 `test_옛_값이_한_글자도_안_바뀜` 의 지문(sha256)이다.
|
||||
|
||||
나머지는 열쇠의 약속 — 안 겹침·꼴·두 번 지어도 같음 · **짐작으로 안 이음** ·
|
||||
계층(`parent_mode`)이 그대로 있음 · 총칙 2장을 지우지 않았음(B09 연료·손료가 읽는다).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from B08_Quantity.B08_Quantity_Build_WorkItemMaster import OUT_DIR # noqa: E402
|
||||
from B08_Quantity.B08_Quantity_Build_WorkItemMaster_Keys import ( # noqa: E402
|
||||
assign_keys,
|
||||
axis_role,
|
||||
empty_registry,
|
||||
assign_variant_keys,
|
||||
format_key,
|
||||
general_provision_roots,
|
||||
load_policy,
|
||||
path_names,
|
||||
toc_edition,
|
||||
variant_display,
|
||||
variant_match,
|
||||
toc_slot,
|
||||
)
|
||||
|
||||
MASTER = OUT_DIR / "work_item_master_2026-01-01.json"
|
||||
|
||||
#: 열쇠를 얹기 **전** 벌(`work_items`)의 지문. 2026-09-17 데스크탑_서브가 뜬 값.
|
||||
#: ⚠ 이 값이 흔들리면 열쇠 작업이 값을 건드렸다는 뜻이다 — 지문을 고치지 말고 원인을 찾을 것.
|
||||
#: 2026-09-17 랩탑 메인 — 목차 오기 둘을 본문 번호로 바로잡아(브레인 지시) **네 줄만** 바뀌어 새로 뜸
|
||||
#: (옛 f8b8fba1…). 네 줄 모습은 `test_목차_오기는_본문_번호로_바로잡고_열쇠는_그대로` 가 잼 ·
|
||||
#: 미판정·밑수 목록 파일은 한 글자도 안 바뀜.
|
||||
#: 2026-09-17 랩탑 메인 둘째 — 헛단위 뺌(인용을 제목으로 읽은 13-3 표 셋 · 부록 사례 번호 겹침 일곱)으로 다시 뜸
|
||||
#: (옛 be88f730…). 바뀐 줄은 13-3·4-1·4-2·2-1·2-2·3-1·3-2·3-3·13-4-1·13-5-2 의 표 귀속뿐 —
|
||||
#: `test_work_item_master_toc` 가 자리를 잼 · 단가표 대조는 그 파일 머리.
|
||||
BASELINE_WORK_ITEMS_SHA = "2accafe5e366c32eb6e95123642b4330e28537b2199dee851247e996fcd839da"
|
||||
|
||||
#: 열쇠 작업이 새로 얹은 칸. 지문을 잴 때만 떼어낸다.
|
||||
ADDED_FIELDS = (
|
||||
"work_item_key",
|
||||
"toc_code",
|
||||
"toc_number",
|
||||
"toc_edition",
|
||||
"path_name",
|
||||
"axis_role",
|
||||
"variants",
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def master() -> dict:
|
||||
return json.loads(MASTER.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def _fingerprint(items: list[dict]) -> str:
|
||||
stripped = [{k: v for k, v in it.items() if k not in ADDED_FIELDS} for it in items]
|
||||
blob = json.dumps(stripped, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
|
||||
return hashlib.sha256(blob.encode()).hexdigest()
|
||||
|
||||
|
||||
def test_옛_값이_한_글자도_안_바뀜(master: dict) -> None:
|
||||
"""금액을 낳는 값(표·밑수·형태)이 그대로인가 — 새 칸만 떼고 지문을 맞댄다."""
|
||||
assert _fingerprint(master["work_items"]) == BASELINE_WORK_ITEMS_SHA
|
||||
|
||||
|
||||
def test_모든_줄에_열쇠가_있고_안_겹침(master: dict) -> None:
|
||||
items = master["work_items"]
|
||||
keys = [it["work_item_key"] for it in items]
|
||||
assert all(re.fullmatch(r"FW-\d{5}", k) for k in keys)
|
||||
assert len(set(keys)) == len(items)
|
||||
|
||||
|
||||
def test_목차_번호는_칸으로만_남음(master: dict) -> None:
|
||||
"""`FP-…` 는 지우지 않고 「그 판의 목차 번호」 칸으로 남았는가 — 164 파일이 아직 쓴다."""
|
||||
assert master["policy"]["row_key"] == "work_item_key"
|
||||
assert master["policy"]["toc_is_column_not_key"] is True
|
||||
assert master["toc_edition"] == "산림청고시제2025-82호"
|
||||
for it in master["work_items"]:
|
||||
assert it["toc_code"] == it["work_item_code"]
|
||||
assert it["toc_number"] == it["number"]
|
||||
assert it["toc_edition"] == master["toc_edition"]
|
||||
|
||||
|
||||
def test_장부가_있으면_같은_열쇠가_다시_나옴(master: dict) -> None:
|
||||
"""두 번 지어도 같은가 — 장부를 그대로 두고 다시 발급해 본다."""
|
||||
registry = json.loads((OUT_DIR / "work_item_keys.json").read_text(encoding="utf-8"))
|
||||
before = copy.deepcopy(registry)
|
||||
nodes = [
|
||||
{
|
||||
"work_item_code": it["work_item_code"],
|
||||
"number": it["number"],
|
||||
"name": it["name"],
|
||||
"sort_order": it["sort_order"],
|
||||
}
|
||||
for it in master["work_items"]
|
||||
]
|
||||
newly, _ = assign_keys(nodes, registry, edition=master["toc_edition"])
|
||||
assert newly == [] # 다시 발급하지 않는다
|
||||
assert registry["next_serial"] == before["next_serial"]
|
||||
assert [n["work_item_key"] for n in nodes] == [
|
||||
it["work_item_key"] for it in master["work_items"]
|
||||
]
|
||||
|
||||
|
||||
def test_장부에_없는_코드는_새_열쇠를_받음() -> None:
|
||||
"""⚠ 짐작으로 안 이음 — 이름이 같아도 옛 열쇠를 물려주지 않고 새로 내고 목록에 싣는다."""
|
||||
registry = empty_registry()
|
||||
old = [{"work_item_code": "FP-09-03", "number": "9-3", "name": "토사깍기", "sort_order": 256}]
|
||||
assign_keys(old, registry, edition="산림청고시제2025-82호")
|
||||
# 새 판에서 번호가 밀린 같은 이름의 절
|
||||
new = [{"work_item_code": "FP-09-04", "number": "9-4", "name": "토사깍기", "sort_order": 256}]
|
||||
newly, _ = assign_keys(new, registry, edition="산림청고시제9999-99호")
|
||||
assert new[0]["work_item_key"] != old[0]["work_item_key"]
|
||||
assert [n["work_item_key"] for n in newly] == [new[0]["work_item_key"]]
|
||||
|
||||
|
||||
def test_열쇠_차례는_목차_차례() -> None:
|
||||
registry = empty_registry()
|
||||
nodes = [
|
||||
{"work_item_code": "FP-02", "number": "2", "name": "둘", "sort_order": 512},
|
||||
{"work_item_code": "FP-01", "number": "1", "name": "하나", "sort_order": 256},
|
||||
]
|
||||
assign_keys(nodes, registry, edition="e")
|
||||
assert nodes[1]["work_item_key"] == format_key(1)
|
||||
assert nodes[0]["work_item_key"] == format_key(2)
|
||||
|
||||
|
||||
def test_목차_오기는_본문_번호로_바로잡고_열쇠는_그대로(master: dict) -> None:
|
||||
"""⚠ 품셈 목차가 12-17-2·12-24-1 을 두 번 적었음 — 본문은 12-17-3 무근진동기 제외 · 12-27-1 지수판 설치
|
||||
(2026-09-17 코덱스 원문 대조). 목차를 믿으면 뒤 줄이 앞 표를 덮어써 혼성 줄이 서고 F0364·F0375 가 사라짐.
|
||||
열쇠는 같은 공종이라 그대로(장부에 손으로 이음 · `relinked`)."""
|
||||
assert master["toc_duplicate_rows"] == []
|
||||
assert [(c["toc_number"], c["body_number"]) for c in master["toc_corrections"]] == [
|
||||
("12-17-2", "12-17-3"),
|
||||
("12-24-1", "12-27-1"),
|
||||
]
|
||||
by_key = {it["work_item_key"]: it for it in master["work_items"]}
|
||||
for key, number, name, tables in (
|
||||
("FW-00387", "12-17-2", "철근, 펌프카 0-15m", ["F0363"]),
|
||||
("FW-00388", "12-17-3", "무근진동기 제외", ["F0364"]),
|
||||
("FW-00396", "12-24-1", "뒷채움", ["F0371"]),
|
||||
("FW-00401", "12-27-1", "지수판 설치", ["F0375"]),
|
||||
):
|
||||
it = by_key[key]
|
||||
assert (it["number"], it["name"], [t["pum_table_id"] for t in it["tables"]]) == (
|
||||
number,
|
||||
name,
|
||||
tables,
|
||||
)
|
||||
assert not {o["pum_table_id"] for o in master["orphan_tables"]} & {"F0364", "F0375"}
|
||||
|
||||
|
||||
def test_슬롯_이름() -> None:
|
||||
assert toc_slot("FP-12-17-02", 1) == "FP-12-17-02"
|
||||
assert toc_slot("FP-12-17-02", 2) == "FP-12-17-02#2"
|
||||
|
||||
|
||||
def test_잎_경로_이름이_안_겹침(master: dict) -> None:
|
||||
"""「인력」 3곳 · 「수확」 6곳처럼 겹치던 잎 이름이 경로 이름으로 갈리는가."""
|
||||
items = master["work_items"]
|
||||
has_kid = {it.get("parent_code") for it in items}
|
||||
leaves = [it for it in items if it["work_item_code"] not in has_kid and it["tables"]]
|
||||
names = [it["name"] for it in leaves]
|
||||
paths = [it["path_name"] for it in leaves]
|
||||
assert len(set(names)) < len(names) # 이름만으로는 겹친다 — 경로가 필요한 까닭
|
||||
assert len(set(paths)) == len(paths)
|
||||
사람 = next(it for it in items if it["work_item_code"] == "FP-09-03-01")
|
||||
assert 사람["path_name"] == "토공 › 토사깍기 › 인력"
|
||||
|
||||
|
||||
def test_계층과_계산_규칙은_그대로(master: dict) -> None:
|
||||
"""`parent_mode` 가 계산 규칙을 지닌다 — 묶는 마디를 지우지 않았는가."""
|
||||
items = master["work_items"]
|
||||
modes = [it.get("parent_mode") for it in items]
|
||||
assert modes.count("choose_one") == 91
|
||||
assert modes.count("sum_steps") == 3
|
||||
발파암 = next(it for it in items if it["work_item_code"] == "FP-09-05")
|
||||
assert [s["code"] for s in 발파암["steps"]] == ["FP-09-05-01", "FP-09-05-02", "FP-09-05-03"]
|
||||
|
||||
|
||||
def test_총칙은_지우지_않고_구실로_가름(master: dict) -> None:
|
||||
"""⚠ `B09_Estimation_Consumables` 가 총칙 2장 표를 읽는다 — 지우면 연료·손료가 사라진다."""
|
||||
by_code = {it["work_item_code"]: it for it in master["work_items"]}
|
||||
for code, table_id in (
|
||||
("FP-02-01-01", "F0042"), # 체인톱 보통휘발유
|
||||
("FP-02-02-01", "F0064"), # 체인톱 손료계수
|
||||
("FP-02-02-06", "F0069"), # 양수기 손료계수
|
||||
):
|
||||
node = by_code[code]
|
||||
assert node["axis_role"] == "general_provision"
|
||||
assert table_id in [t["pum_table_id"] for t in node["tables"]]
|
||||
roles = master["stats"]["axis_roles"]
|
||||
assert sum(roles.values()) == len(master["work_items"])
|
||||
assert roles["general_provision"] == 78
|
||||
|
||||
|
||||
def test_구실_가름_규칙() -> None:
|
||||
assert (
|
||||
axis_role({"work_item_code": "FP-01-02", "tables": []}, has_children=True)
|
||||
== "general_provision"
|
||||
)
|
||||
assert (
|
||||
axis_role({"work_item_code": "FP-09-03-01", "tables": [{}]}, has_children=False)
|
||||
== "work_item"
|
||||
)
|
||||
assert axis_role({"work_item_code": "FP-09-03", "tables": []}, has_children=True) == "group"
|
||||
assert axis_role({"work_item_code": "FP-09-01", "tables": []}, has_children=False) == "empty"
|
||||
|
||||
|
||||
def test_총칙_범위는_코드가_아니라_자료가_정함() -> None:
|
||||
"""⚠ 뿌리를 코드에 박으면 관리자가 못 고친다 — `axis_policy.json` 이 정본."""
|
||||
assert general_provision_roots("forest") == ("FP-01", "FP-02")
|
||||
const = load_policy("const")
|
||||
assert const["key_prefix"] == "CW"
|
||||
assert const["settled"] is True # 2026-09-17 안티그래비티 조사 · 브레인 확정
|
||||
# 건설 뿌리는 **장 파일 이름**으로 적는다 — 뽑는 코드가 목차 코드로 바꿔 준다(랩탑 메인 `file_place`).
|
||||
# ⚠ 바뀐 코드에는 부문 자리가 들어간다(`CP-01-01`) — 부문마다 장 번호가 1부터 다시 시작해서다.
|
||||
assert const["general_provision_roots"] == [
|
||||
"01_공통부문/제1장_적용기준.md",
|
||||
"01_공통부문/제8장_건설기계.md", # 산림 FP-02(소요재료·기계손료)와 짝
|
||||
]
|
||||
# 유지관리 제1장 「공 통」은 총칙이 아니라 **공종** — 미확정 목록이 비었다
|
||||
assert const["general_provision_pending"] == []
|
||||
# 뿌리를 바꿔 주면 구실도 따라 바뀐다 — 잣대가 자료에 있다는 증거
|
||||
assert (
|
||||
axis_role({"work_item_code": "FP-09-03", "tables": []}, has_children=True, roots=("FP-09",))
|
||||
== "general_provision"
|
||||
)
|
||||
|
||||
|
||||
def test_건설_총칙_뿌리가_실제로_붙음() -> None:
|
||||
"""잣대가 건설 산출에 닿았는가 — 뿌리 아래 37줄이 `general_provision` 으로 섰다."""
|
||||
doc = json.loads(
|
||||
(OUT_DIR / "const_work_item_master_2026-01-01.json").read_text(encoding="utf-8")
|
||||
)
|
||||
roles = doc["stats"]["axis_roles"]
|
||||
assert roles["general_provision"] == 112 # 적용기준 37 + 건설기계 75
|
||||
assert sum(roles.values()) == len(doc["work_items"]) == 1367
|
||||
for root, count in (("CP-01-01", 37), ("CP-01-08", 75)):
|
||||
under = [
|
||||
it
|
||||
for it in doc["work_items"]
|
||||
if it["work_item_code"] == root or it["work_item_code"].startswith(root + "-")
|
||||
]
|
||||
assert len(under) == count
|
||||
assert {it["axis_role"] for it in under} == {"general_provision"}
|
||||
# 유지관리 제1장 「공 통」은 공종으로 남는다(총칙 아님)
|
||||
공통 = next(it for it in doc["work_items"] if it["work_item_code"] == "CP-05-01")
|
||||
assert 공통["axis_role"] != "general_provision"
|
||||
|
||||
|
||||
def test_갈래에도_불변_열쇠가_있다(master: dict) -> None:
|
||||
"""갈래 열쇠는 `FW-00105#01` — **글자가 아니라 번호**다(2026-09-17 브레인).
|
||||
|
||||
글자를 열쇠에 담으면 원문 자간·물결표가 다듬어지는 날 열쇠가 갈린다. 공종 열쇠와 같은 병이다.
|
||||
"""
|
||||
items = master["work_items"]
|
||||
keys, pairs = [], []
|
||||
for it in items:
|
||||
for v in it.get("variants") or []:
|
||||
assert re.fullmatch(r"\d{2}", v["variant_key"]), v
|
||||
keys.append(f"{it['work_item_key']}#{v['variant_key']}")
|
||||
pairs.append((v["name"], v["name_clean"]))
|
||||
assert len(keys) == 332 # 갈래 수 — 헛단위 13-3 갈래 3 뺌(335 → 332 · 2026-09-17 · 장부 번호는 남음)
|
||||
assert len(set(keys)) == len(keys) # 겹침 0
|
||||
# 한 공종 안에서 번호는 1부터 빠짐없이
|
||||
for it in items:
|
||||
got = [v["variant_key"] for v in it.get("variants") or []]
|
||||
assert got == [f"{i + 1:02d}" for i in range(len(got))], it["work_item_key"]
|
||||
# 이름 칸 둘 — 원문은 손대지 않고, 다듬은 것이 따로
|
||||
raw = [it.get("variant_keys") or [] for it in items]
|
||||
assert [n for it in items for n in (it.get("variant_keys") or [])] == [p[0] for p in pairs]
|
||||
# 자간·물결표가 다듬어진 줄 — 14 → 70(2026-09-17 한 글자 낱말 자간 붙이기 「메 붙 임」→「메붙임」 · 열쇠는 그대로)
|
||||
assert sum(1 for a, b in pairs if a != b) == 70
|
||||
assert raw # 원문 목록은 그대로 남아 있다
|
||||
|
||||
|
||||
def test_갈래_열쇠는_글자가_바뀌어도_안_흔들린다() -> None:
|
||||
"""맞대는 꼴이 공백·물결표를 흡수한다 — 이름 다듬기가 들어와도 번호가 그대로다."""
|
||||
reg = empty_registry()
|
||||
node = {
|
||||
"work_item_key": "FW-00105",
|
||||
"sort_order": 1,
|
||||
"variant_keys": ["1.1∼1.5", "간 단"],
|
||||
}
|
||||
assign_variant_keys([node], reg)
|
||||
before = [v["variant_key"] for v in node["variants"]]
|
||||
# 원문을 다듬어도(물결표 바꿈 · 자간 정리) 같은 번호가 나와야 한다
|
||||
node2 = {"work_item_key": "FW-00105", "sort_order": 1, "variant_keys": ["1.1~1.5", "간 단"]}
|
||||
newly = assign_variant_keys([node2], reg)
|
||||
assert [v["variant_key"] for v in node2["variants"]] == before
|
||||
assert newly == [] # 새 번호를 내지 않는다
|
||||
assert variant_match("간 단") == variant_match("간 단") == "간단"
|
||||
assert variant_display("직경 60㎝이상 ~80㎝미만") == "직경 60㎝이상 ∼80㎝미만"
|
||||
|
||||
|
||||
def test_갈래_문자열이_열쇠라_흔들림을_못박음(master: dict) -> None:
|
||||
"""⚠ 갈래가 줄 열쇠에 그대로 들어간다(`@id` = `FW-00105#1.1∼1.5`).
|
||||
|
||||
품셈 원문이 물결표를 두 가지로 쓰고 자간 공백도 들쭉날쭉이라, 문자열을 손질하면 **열쇠가 갈린다.**
|
||||
지금 몇 개가 그런지 못박아 둔다 — 늘면 손질이 들어간 것이고, 줄면 열쇠가 바뀐 것이다.
|
||||
"""
|
||||
import collections
|
||||
|
||||
keys = [k for it in master["work_items"] for k in (it.get("variant_keys") or [])]
|
||||
assert len(keys) == 332 # 2026-09-17 헛단위 13-3 갈래 3 뺌(335 → 332)
|
||||
tilde = collections.Counter(ch for k in keys for ch in k if ch in "∼~")
|
||||
assert tilde == {"∼": 26, "~": 8}, "물결표 쓰임이 바뀜 — 열쇠가 갈렸는지 볼 것"
|
||||
squeezed = collections.defaultdict(set)
|
||||
for k in keys:
|
||||
squeezed["".join(k.split()).replace("~", "∼")].add(k)
|
||||
messy = {n: v for n, v in squeezed.items() if len(v) > 1}
|
||||
assert len(messy) == 6, f"공백·물결표만 다른 짝이 {len(messy)} — 6 이던 것"
|
||||
|
||||
|
||||
def test_어느_구실도_지우지_않음() -> None:
|
||||
"""브레인 승인(2026-09-17) — 「덜어내기」가 아니라 「가름만」."""
|
||||
import json as _json
|
||||
from pathlib import Path as _Path
|
||||
|
||||
policy = _json.loads(
|
||||
_Path("resources/data_work_item_master/axis_policy.json").read_text(encoding="utf-8")
|
||||
)
|
||||
assert policy["policy"]["never_delete"] is True
|
||||
assert policy["policy"]["flat_list_filter"] == "pum_tables > 0"
|
||||
assert all(role["keep"] for role in policy["roles"].values())
|
||||
assert list(policy["roles"]) == ["work_item", "group", "general_provision", "empty"]
|
||||
|
||||
|
||||
def test_경로_이름_잇기() -> None:
|
||||
nodes = [
|
||||
{"work_item_code": "A", "parent_code": None, "name": "토공"},
|
||||
{"work_item_code": "A-1", "parent_code": "A", "name": "토사깍기"},
|
||||
{"work_item_code": "A-1-1", "parent_code": "A-1", "name": "인력"},
|
||||
]
|
||||
assert path_names(nodes)[2] == "토공 › 토사깍기 › 인력"
|
||||
|
||||
|
||||
def test_고시_번호를_원문_경로에서_읽음() -> None:
|
||||
sources = [
|
||||
{"path": "resources/knowledge/original/…/(산림청고시 제2025-82호) 산림사업 표준품셈.md"}
|
||||
]
|
||||
assert toc_edition(sources, "2026-01-01") == "산림청고시제2025-82호"
|
||||
assert toc_edition([], "2026-01-01") == "2026-01-01"
|
||||
@@ -0,0 +1,138 @@
|
||||
"""연결고리 표 소비 — 산림·건설 어느 품을 쓸지 **표가 정함** (2026-09-17 브레인 · 코덱스 크로스체크 ②).
|
||||
|
||||
고시 총칙 7 「유사 공종은 본 품셈 우선」 이 규칙으로만 있고 아무도 안 읽던 자리. 못박는 것:
|
||||
① 둘 다 있는 공종은 표의 `both_precedence` 쪽 — 표를 바꾸면 고르는 쪽도 바뀜(코드에 안 박힘)
|
||||
② 미판정은 안 고르고 막음 · 사유 그대로 · 금액에 안 듦
|
||||
③ 지금 인계 코드는 한 줄도 판정이 안 달라짐 = **금액 불변**(이행 전 금액은 `test_work_item_key_gate` 가 못박음)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from B09_Estimation.B09_Estimation_BillOfQuantities import bill_summary, build_bill
|
||||
from B09_Estimation.B09_Estimation_BillOfQuantities_Input import (
|
||||
BLOCKED_LINK_UNDECIDED,
|
||||
parse_handoff,
|
||||
)
|
||||
from B09_Estimation.B09_Estimation_UnitPrice import build_unit_prices
|
||||
from common_util import common_util_work_item_link as link
|
||||
from common_util.common_util_work_item_key import MASTER_DIR, work_item_code_of, work_item_key
|
||||
|
||||
MAPPING = MASTER_DIR.parent / "data_work_item_mapping" / "work_item_mapping_2026-01-01.json"
|
||||
|
||||
|
||||
def _table(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, links: list, precedence="forest"
|
||||
) -> None:
|
||||
folder = tmp_path / "data_work_item_link"
|
||||
folder.mkdir(parents=True)
|
||||
doc = {
|
||||
"policy": {"both_precedence": precedence, "unconfirmed_action": "do_not_link"},
|
||||
"links": links,
|
||||
}
|
||||
(folder / "work_item_link_2026-01-01.json").write_text(
|
||||
json.dumps(doc, ensure_ascii=False), encoding="utf-8"
|
||||
)
|
||||
monkeypatch.setattr(link, "FOLDER", folder)
|
||||
|
||||
|
||||
def test_지금_인계_코드는_판정이_안_달라짐_금액_불변() -> None:
|
||||
codes = set(
|
||||
re.findall(r'"((?:FP|AX-WK|AX-ST)-[0-9A-Za-z-]+)"', MAPPING.read_text(encoding="utf-8"))
|
||||
)
|
||||
changed = []
|
||||
for code in sorted(codes):
|
||||
key = work_item_key(code, "2026-01-01")
|
||||
choice = link.choose(key)
|
||||
if choice.key != key or choice.blocked:
|
||||
changed.append((code, key, choice))
|
||||
assert changed == [] # 달라지면 그동안 잘못 고르고 있었다는 뜻 — 브레인에 먼저 알릴 것
|
||||
|
||||
|
||||
def test_둘_다_있으면_표가_적은_쪽() -> None:
|
||||
"""실제 표 — 산림 9-7-1 무근콘크리트 깨기 ↔ 건설 8-2-13 대형브레이커(E 0.35 ↔ 0.45)."""
|
||||
assert link.choose("CW-00330").key == "FW-00260"
|
||||
assert link.choose("FW-00260").key == "FW-00260"
|
||||
assert link.choose("CW-00844").key == "CW-00844" # 건설에만(모르타르 배합) — 그대로
|
||||
assert link.choose("FW-00249").key == "FW-00249" # 표에 없음 — 제 품셈
|
||||
|
||||
|
||||
def test_우선_품셈은_코드가_아니라_표가_정함(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
rows = [{"forest_key": "FW-00260", "const_key": "CW-00330", "branch": "both", "status": "확정"}]
|
||||
_table(tmp_path, monkeypatch, rows, precedence="construction")
|
||||
assert link.choose("FW-00260").key == "CW-00330"
|
||||
_table(tmp_path / "b", monkeypatch, rows, precedence="")
|
||||
assert link.choose("FW-00260").key is None and link.choose("FW-00260").blocked
|
||||
|
||||
|
||||
def test_미판정은_안_고르고_막음_금액에_안_듦(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
payload = {
|
||||
"work_items": [
|
||||
{"work_item_code": "FP-09-03-02", "name": "토사깍기", "unit": "㎥", "quantity": 100},
|
||||
{"work_item_code": "FP-09-12-01", "name": "측구터파기", "unit": "㎥", "quantity": 7},
|
||||
],
|
||||
"materials": [],
|
||||
}
|
||||
build = build_unit_prices()
|
||||
before = build_bill(json.loads(json.dumps(payload)), build=build)
|
||||
ditch = work_item_key("FP-09-12-01", "2026-01-01")
|
||||
rows = [
|
||||
{
|
||||
"forest_key": ditch,
|
||||
"const_key": "",
|
||||
"branch": "both",
|
||||
"status": "미판정",
|
||||
"evidence": "시험 — 측구 터파기 건설 대응 미확인",
|
||||
}
|
||||
]
|
||||
_table(tmp_path, monkeypatch, rows)
|
||||
items, _ = parse_handoff(json.loads(json.dumps(payload)))
|
||||
assert (
|
||||
items[1].blocked_kind == BLOCKED_LINK_UNDECIDED and "시험 — 측구" in items[1].blocked_reason
|
||||
)
|
||||
after = build_bill(json.loads(json.dumps(payload)), build=build)
|
||||
blocked = [m for m in after.missing if m.get("blocked_kind") == BLOCKED_LINK_UNDECIDED]
|
||||
assert [m["name"] for m in blocked] == ["측구터파기"]
|
||||
ditch_amount = sum(
|
||||
r.amount_krw or 0 for r in before.rows if not r.is_group and r.name == "측구터파기"
|
||||
)
|
||||
assert ditch_amount > 0 and after.body_total_krw == before.body_total_krw - ditch_amount
|
||||
assert bill_summary(after)["link_undecided"][0]["evidence"].startswith("시험")
|
||||
|
||||
|
||||
def test_건설_열쇠로_온_줄은_표가_고른_산림_열쇠와_목차_코드로() -> None:
|
||||
const_code = work_item_code_of("CW-00330")
|
||||
assert const_code and const_code.startswith("CP-")
|
||||
items, _ = parse_handoff(
|
||||
{
|
||||
"work_items": [
|
||||
{
|
||||
"work_item_code": const_code,
|
||||
"name": "대형브레이커",
|
||||
"unit": "㎥",
|
||||
"quantity": 1,
|
||||
"pum_edition": "2026-01-01",
|
||||
}
|
||||
],
|
||||
"materials": [],
|
||||
}
|
||||
)
|
||||
assert (items[0].work_item_key, items[0].work_item_code) == (
|
||||
"FW-00260",
|
||||
work_item_code_of("FW-00260"),
|
||||
)
|
||||
assert items[0].work_item_code.startswith("FP-")
|
||||
|
||||
|
||||
def test_미판정_행은_열쇠가_비어도_목록으로_드러남() -> None:
|
||||
undecided = link.undecided_links()
|
||||
assert undecided and all(row["evidence"] for row in undecided)
|
||||
@@ -0,0 +1,191 @@
|
||||
"""건설 공종 축(`CW-00001`) 뽑기 — 2026-09-17 브레인 8-2.
|
||||
|
||||
못박는 것
|
||||
① 목차 글줄 가르기 — 쪽 번호가 다음 번호에 붙은 꼴(「131-2-6」 = 13쪽 + 1-2-6) · 부문마다 장 번호 1부터
|
||||
② 표는 **본문 절 제목**으로 붙음 — 인용(「3-2-4 터파기(기계)’를 참고하여」)은 제목이 아님
|
||||
③ 목차 오기 없음 — 연혁 표시(「('20년 보완)」)가 붙은 본문 제목이 모두 목차 번호·이름과 같음
|
||||
(산림에서 목차가 번호를 잘못 적어 표가 딴 줄에 붙은 병 · 건설도 같은 함정)
|
||||
④ 못 붙인 표는 목록으로 · 열쇠 CW- 안 겹침 · 다시 뽑아도 같은 열쇠
|
||||
⑤ 산출 파일 = 지금 코드로 뽑은 것(산림도 같이 — 두 뽑기가 표 읽기 한 벌을 나눠 씀 · 금액 불변)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
|
||||
import pytest
|
||||
|
||||
from B08_Quantity import B08_Quantity_Build_WorkItemMaster as forest
|
||||
from B08_Quantity import B08_Quantity_Build_WorkItemMaster_Const as const
|
||||
|
||||
OUT = forest.OUT_DIR
|
||||
CONST_MASTER = OUT / "const_work_item_master_2026-01-01.json"
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def built() -> tuple:
|
||||
return const.build_const()
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def master() -> dict:
|
||||
return json.loads(CONST_MASTER.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def _same_except_time(fresh: dict, saved: dict) -> None:
|
||||
fresh = json.loads(json.dumps(fresh, ensure_ascii=False))
|
||||
for doc in (fresh, saved):
|
||||
doc.pop("generated_at")
|
||||
doc["stats"].pop("keys_newly_issued") # 첫 뽑기만 발급 · 다시 뽑으면 0
|
||||
doc["stats"].pop("variant_keys_newly_issued", None) # 갈래 열쇠도 같음
|
||||
assert fresh == saved
|
||||
|
||||
|
||||
def test_목차_글줄_가르기_쪽_번호가_붙은_꼴() -> None:
|
||||
text = "제1장적용기준31-1 일반사항····31-1-1 목적····131-2-6 공구 및 경장비····19제2장가설공사352-1 가설물의 한도 ····35"
|
||||
assert const.toc_entries(text) == [
|
||||
("1", "1", "적용기준"),
|
||||
("1", "1-1", "일반사항"),
|
||||
("1", "1-1-1", "목적"),
|
||||
("1", "1-2-6", "공구 및 경장비"),
|
||||
("2", "2", "가설공사"),
|
||||
("2", "2-1", "가설물의 한도"),
|
||||
]
|
||||
assert const.toc_entries("제11장칠공사61111-1 공통사항····611")[1] == ("11", "11-1", "공통사항")
|
||||
|
||||
|
||||
def test_인용은_제목이_아님() -> None:
|
||||
names = {("공통부문", "3-2-4"): "터파기(기계)", ("공통부문", "3-2-5"): "되메우기"}
|
||||
chapters = {"2", "3", "4"}
|
||||
assert (
|
||||
const.titles_in("’3-2-4 터파기(기계)’를 참고하여 적용한다.", "공통부문", chapters, names)
|
||||
== []
|
||||
)
|
||||
assert const.titles_in("…경우3-2-4 터파기(기계)('25년 신설)", "공통부문", chapters, names) == [
|
||||
"3-2-4"
|
||||
]
|
||||
assert const.titles_in("853-2-5 되메우기Q = n·q", "공통부문", chapters, names) == ["3-2-5"]
|
||||
|
||||
|
||||
def test_목차_부문_다섯_장_마흔다섯(built: tuple) -> None:
|
||||
items = built[0]["work_items"]
|
||||
roots = [it for it in items if it["parent_code"] is None]
|
||||
assert [r["name"] for r in roots] == list(const.DIVISIONS)
|
||||
assert sum(1 for it in items if it["level"] == 2) == 45
|
||||
codes = [it["work_item_code"] for it in items]
|
||||
assert len(set(codes)) == len(codes)
|
||||
|
||||
|
||||
def test_표는_다_붙고_못_붙인_표는_목록(built: tuple) -> None:
|
||||
master = built[0]
|
||||
stats = master["stats"]
|
||||
assert stats["tables_total"] == 2192
|
||||
assert stats["tables_attached"] + len(master["orphan_tables"]) == stats["tables_total"]
|
||||
assert stats["tables_orphan"] == len(master["orphan_tables"]) == 0
|
||||
|
||||
|
||||
def test_목차_오기_없음_연혁_붙은_본문_제목이_목차와_같음(built: tuple) -> None:
|
||||
"""본문 제목 번호가 목차에 없거나 이름이 다르면 그 절 표가 **앞 절에 조용히 붙음** — 빨강."""
|
||||
items = built[0]["work_items"]
|
||||
names = {(it["division"], it["number"]): it["name"] for it in items if it["level"] > 1}
|
||||
titled = re.compile(r"(\d+(?:-\d+)+)\s*([^’'\[\]()]{1,40}?)\s*\(\s*['‘’]\d\d")
|
||||
data = json.loads(const.SOURCE.read_text(encoding="utf-8"))
|
||||
files = sorted({t["source_file"] for t in data["variables"]["pum"]["tables"]})
|
||||
seen, bad = 0, []
|
||||
for source_file in files:
|
||||
division, chapter = const.file_place(source_file)
|
||||
chapters = {str(chapter - 1), str(chapter), str(chapter + 1)}
|
||||
for line in (const.ROOT / source_file).read_text(encoding="utf-8").splitlines():
|
||||
for found in titled.finditer(line):
|
||||
raw, name = found.group(1), "".join(found.group(2).split())
|
||||
number = next(
|
||||
(
|
||||
raw[cut:]
|
||||
for cut in range(len(raw.split("-")[0]))
|
||||
if raw[cut:].split("-")[0] in chapters
|
||||
),
|
||||
None,
|
||||
)
|
||||
if number is None:
|
||||
continue
|
||||
seen += 1
|
||||
toc = "".join(names.get((division, number), "").split())
|
||||
if not toc or not (name.startswith(toc) or toc.startswith(name)):
|
||||
bad.append(
|
||||
f"{division} {number} 「{found.group(2)}」 목차 「{names.get((division, number))}」"
|
||||
)
|
||||
assert seen > 600
|
||||
assert not bad, bad
|
||||
|
||||
|
||||
def test_열쇠_CW_안_겹침_다시_뽑아도_같음(built: tuple, master: dict) -> None:
|
||||
keys = [it["work_item_key"] for it in master["work_items"]]
|
||||
assert all(re.fullmatch(r"CW-\d{5}", k) for k in keys)
|
||||
assert len(set(keys)) == len(keys)
|
||||
assert built[3][1] == [] # 장부가 있으니 새로 안 냄
|
||||
assert [it["work_item_key"] for it in built[0]["work_items"]] == keys
|
||||
|
||||
|
||||
def test_산출_파일이_지금_코드로_뽑은_것(built: tuple, master: dict) -> None:
|
||||
_same_except_time(built[0], master)
|
||||
|
||||
|
||||
def test_산림_산출도_그대로_금액_불변() -> None:
|
||||
"""표 읽기를 떼어내고(`_Table`) 건설과 나눠 써도 산림 산출은 한 글자도 안 바뀜."""
|
||||
saved = json.loads((OUT / "work_item_master_2026-01-01.json").read_text(encoding="utf-8"))
|
||||
master, undetermined, basis_missing, _ = forest.build()
|
||||
_same_except_time(master, saved)
|
||||
for name, fresh in (("form_undetermined", undetermined), ("basis_missing", basis_missing)):
|
||||
saved_items = json.loads((OUT / f"{name}_2026-01-01.json").read_text(encoding="utf-8"))[
|
||||
"items"
|
||||
]
|
||||
assert json.loads(json.dumps(fresh, ensure_ascii=False)) == saved_items
|
||||
|
||||
|
||||
def test_산림을_고르는_쪽이_건설_파일을_안_집음() -> None:
|
||||
"""⚠ `work_item_master_*` 로 끝 파일을 고르는 자리가 있음(B08 밑수 대조 · B09 근거표) —
|
||||
건설 산출을 `work_item_master_const_…` 로 두면 그쪽이 건설을 집어 밑수 대조가 통째로 빠졌음(2026-09-17)."""
|
||||
from B08_Quantity.B08_Quantity_Engine_BasisUnit import load_master
|
||||
|
||||
assert load_master()["dataset_id"] == "work_item_master_forest"
|
||||
assert not list(OUT.glob("work_item_master_*const*.json"))
|
||||
|
||||
|
||||
def test_건설_갈래도_이름을_다듬어도_같은_열쇠(master: dict) -> None:
|
||||
"""④ 건설 갈래 펴기(2026-09-17) — 갈래 열쇠는 번호(`CW-00063#01`) · 자간·물결표를 손질해도 새로 안 남."""
|
||||
import copy
|
||||
|
||||
from B08_Quantity.B08_Quantity_Build_WorkItemMaster_Keys import (
|
||||
assign_variant_keys,
|
||||
load_registry,
|
||||
)
|
||||
|
||||
items = master["work_items"]
|
||||
spread = [it for it in items if it.get("variants")]
|
||||
assert len(spread) > 100 and sum(len(it["variants"]) for it in spread) > 400
|
||||
registry = load_registry(const.CONST_KEY_REGISTRY, const.KEY_PREFIX)
|
||||
nodes = copy.deepcopy(spread)
|
||||
for node in nodes: # 사람이 이름을 다듬은 날 — 자간 한 칸 · 물결표 다른 벌
|
||||
node["variant_keys"] = [
|
||||
" ".join(k.split()).replace("∼", "~") for k in node["variant_keys"]
|
||||
]
|
||||
assert assign_variant_keys(nodes, registry) == [] # 새 번호 0
|
||||
for before, after in zip(spread, nodes):
|
||||
assert [v["variant_key"] for v in after["variants"]] == [
|
||||
v["variant_key"] for v in before["variants"]
|
||||
]
|
||||
|
||||
|
||||
def test_자간_다듬기는_진짜_띄어쓰기를_안_붙임() -> None:
|
||||
from B08_Quantity.B08_Quantity_Build_WorkItemMaster_Keys import tidy_spacing
|
||||
|
||||
assert tidy_spacing("측 량") == "측량"
|
||||
assert tidy_spacing("평 규 준 틀") == "평규준틀"
|
||||
assert tidy_spacing("굴 삭 기 (무한궤도)") == "굴삭기 (무한궤도)"
|
||||
assert tidy_spacing("메 붙 임 · 깬 돌 · 뒷길이 25㎝") == "메붙임 · 깬돌 · 뒷길이 25㎝"
|
||||
assert tidy_spacing("토목부문 › 측 량 › 기준점 측량") == "토목부문 › 측량 › 기준점 측량"
|
||||
assert (
|
||||
tidy_spacing("그 외 자재의 운반품셈") == "그 외 자재의 운반품셈"
|
||||
) # 한 글자 낱말 둘은 진짜 띄어쓰기
|
||||
assert tidy_spacing("호박돌 및 야면석 · 뒷길이 25㎝") == "호박돌 및 야면석 · 뒷길이 25㎝"
|
||||
@@ -0,0 +1,100 @@
|
||||
"""공종 축 — **이름과 붙은 표의 절이 어긋나면 빨강** (2026-09-17 브레인 · 코덱스 원문 대조).
|
||||
|
||||
이 병은 한 번 나면 아무도 못 알아챔: 품셈 목차가 같은 번호를 두 번 적자(12-17-2 · 12-24-1)
|
||||
뒤 줄(「무근진동기 제외」)이 앞 표(F0363 「철근, 펌프카 0-15m」)를 물고, 진짜 표(F0364)는 orphan 으로 사라졌음.
|
||||
표 번호로는 안 잡힘(번호는 같았음) — **표 절 제목의 이름이 다른 줄 이름과 똑같은데 그 줄이 아닌 데 붙음**이 그 꼴.
|
||||
⚠ 이름 글자만 조금 다른 자리(목차 「깍기」 · 본문 「깎기」)는 판정하지 않음 — 번호·이름이 딴 줄을 가리킬 때만.
|
||||
⚠ 2026-09-17 둘째 — **헛단위**(목록에 없는 공종이 섬 · 서브 훑기 · 브레인):
|
||||
· [주] 인용 「‘13-3. 기초다짐 및 뒤채움’ 항을 적용한다」를 제목으로 읽어 표 셋(F0405·F0406·F0418)이 13-3 에 붙음
|
||||
· 부록 사례 표 일곱(F0455·F0456·F0472~F0476)이 본문 절 번호와 겹쳐 4-1·4-2 따위에 붙음
|
||||
⇒ 인용은 제목이 아님 · 번호만 겹치고 이름이 딴판이면 못 붙인 표 목록으로.
|
||||
단가표 대조(677 제목 전후) — 헛단위 B-FP-13-03 셋이 빠지고 **B-FP-13-05-02 가 메붙임+찰붙임 합으로 약 두 배**
|
||||
(쓰는 곳 0 · `KnownGaps` 에 「쓰면 안 됨」 표시 · 갈래 가르기 대기). 나머지 단가 한 원도 안 바뀜.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
MASTER = ROOT / "resources" / "data_work_item_master" / "work_item_master_2026-01-01.json"
|
||||
_TITLE = re.compile(r"^\s*(\d+(?:-\d+){0,3})\.\s+(.+)$")
|
||||
|
||||
|
||||
def _squeeze(text: str) -> str:
|
||||
return "".join(str(text).split())
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def master() -> dict:
|
||||
return json.loads(MASTER.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def test_목차_번호가_안_겹침(master: dict) -> None:
|
||||
numbers = [it["number"] for it in master["work_items"]]
|
||||
assert len(set(numbers)) == len(numbers)
|
||||
|
||||
|
||||
def test_표_절_제목이_다른_줄을_가리키지_않음(master: dict) -> None:
|
||||
items = master["work_items"]
|
||||
by_name: dict[str, set[str]] = {} # 이름 → 그 이름 줄의 열쇠(번호가 겹친 줄도 갈림)
|
||||
for it in items:
|
||||
by_name.setdefault(_squeeze(it["name"]), set()).add(it["work_item_key"])
|
||||
bad = []
|
||||
for it in items:
|
||||
for table in it["tables"]:
|
||||
title = _TITLE.match(table["section"])
|
||||
if title is None:
|
||||
continue
|
||||
number, name = title.group(1), _squeeze(title.group(2))
|
||||
others = by_name.get(name, set()) - {it["work_item_key"]}
|
||||
if number != it["number"] or (others and name != _squeeze(it["name"])):
|
||||
bad.append(
|
||||
f"{table['pum_table_id']} 「{table['section']}」 → {it['number']} {it['name']}"
|
||||
)
|
||||
assert not bad, bad
|
||||
|
||||
|
||||
def test_사라진_표는_목록으로_남음(master: dict) -> None:
|
||||
"""orphan 을 조용히 버리지 않음 — 목록 수와 집계가 같아야 함."""
|
||||
assert master["stats"]["tables_orphan"] == len(master["orphan_tables"])
|
||||
assert (
|
||||
master["stats"]["tables_attached"] + len(master["orphan_tables"])
|
||||
== master["stats"]["tables_total"]
|
||||
)
|
||||
|
||||
|
||||
APPENDIX = {"F0455", "F0456", "F0472", "F0473", "F0474", "F0475", "F0476"}
|
||||
|
||||
|
||||
def test_인용은_제목이_아니라_표가_제_절에_붙음(master: dict) -> None:
|
||||
by_code = {it["work_item_code"]: it for it in master["work_items"]}
|
||||
tables = lambda code: [t["pum_table_id"] for t in by_code[code]["tables"]] # noqa: E731
|
||||
assert tables("FP-13-03") == [] # 헛단위 — 표 없는 묶는 마디로
|
||||
assert {"F0405", "F0406"} <= set(tables("FP-13-04-01"))
|
||||
assert "F0418" in tables("FP-13-05-02")
|
||||
|
||||
|
||||
def test_부록_사례_표는_번호가_겹쳐도_안_붙고_목록으로(master: dict) -> None:
|
||||
attached = {t["pum_table_id"] for it in master["work_items"] for t in it["tables"]}
|
||||
assert not attached & APPENDIX
|
||||
assert APPENDIX <= {o["pum_table_id"] for o in master["orphan_tables"]}
|
||||
by_code = {it["work_item_code"]: it for it in master["work_items"]}
|
||||
assert by_code["FP-04-01"]["tables"] == [] and by_code["FP-04-02"]["tables"] == []
|
||||
|
||||
|
||||
def test_이름_닮음_잣대() -> None:
|
||||
from B08_Quantity.B08_Quantity_Build_WorkItemMaster import _is_section_title, title_names_node
|
||||
|
||||
assert not _is_section_title("13-3. 기초다짐 및 뒤채움’ 항을 적용한다.", {})
|
||||
assert _is_section_title(
|
||||
"2-9. 어린나무가꾸기, (1) 치수림단계 (제거대상 피복도 ‘소’, 가지치기)", {}
|
||||
)
|
||||
assert not title_names_node("4-1. 소나무재선충병방제", {"name": "수확베기"})
|
||||
assert title_names_node("9-19-2. 비탈면 고르기(암절취)", {"name": "비탈면 면고르기(암절취)"})
|
||||
assert title_names_node("12-13. 면벽", {"name": "면벅"})
|
||||
assert title_names_node("9-7-1. T=30㎝ 미만", {"name": "T=30cm 미만"})
|
||||
@@ -23,6 +23,7 @@ from fastapi.testclient import TestClient
|
||||
from Z01_MasterData import Z01_MasterData_BasePrices as base_prices
|
||||
from Z01_MasterData import Z01_MasterData_Overrides as overrides
|
||||
from Z01_MasterData import Z01_MasterData_Router as router_module
|
||||
from Z01_MasterData import Z01_MasterData_WorkItems as work_items
|
||||
|
||||
NEW_KINDS = (
|
||||
"coef",
|
||||
@@ -60,7 +61,10 @@ def _ids(table: dict) -> list[str]:
|
||||
|
||||
|
||||
def test_아홉이_기초단가와_같은_길로_섬(client: TestClient) -> None:
|
||||
assert base_prices.KINDS == ("labor", "machine", "material", "oil", "rate") + NEW_KINDS
|
||||
assert (
|
||||
base_prices.KINDS
|
||||
== ("labor", "machine", "material", "oil", "rate") + NEW_KINDS + work_items.KINDS
|
||||
)
|
||||
for kind in NEW_KINDS:
|
||||
table = _get(client, kind, size=500)
|
||||
assert table["total"] == len(table["rows"]) > 0, kind
|
||||
@@ -81,6 +85,9 @@ def test_돌쌓기_경사는_도면에도_닿는다고_알림(client: TestClient
|
||||
"""아홉 중 하나만 수량 말고 **그림**에도 닿음 — 화면이 사전을 가지면 문구가 두 벌(브레인)."""
|
||||
slope = _get(client, "masonry_slope", size=1)["notice"]
|
||||
assert any("횡단 도면" in line for line in slope) and len(slope) == 2
|
||||
# ⚠ 지금 상태를 먼저 — 덮개를 읽는 곳이 Z01 밖에 0(2026-09-17 브레인 「없으면 없다고」)
|
||||
assert any(line.startswith("⚠ **B06 횡단설계가 아직") for line in slope)
|
||||
assert slope[0].startswith("⚠ **아직 어디에도 안 쓰임**")
|
||||
assert all(
|
||||
not any("횡단 도면" in line for line in _get(client, kind, size=1)["notice"])
|
||||
for kind in NEW_KINDS
|
||||
@@ -286,7 +293,7 @@ def test_kind_목록을_서버가_줌_새_kind_도_저절로(
|
||||
assert [i["kind"] for i in items] == list(base_prices.KINDS)
|
||||
groups = {i["kind"]: i["group"] for i in items}
|
||||
assert groups["labor"] == "base_price" and groups["machine_productivity"] == "pumsem_basis"
|
||||
assert {i["group"] for i in items} == {"base_price", "pumsem_basis"}
|
||||
assert {i["group"] for i in items} == {"base_price", "pumsem_basis", "work_item"}
|
||||
labor = next(i for i in items if i["kind"] == "labor")
|
||||
assert labor["label"] == "노임" and labor["rows"] == 261 # 이름은 이름표에서
|
||||
assert next(i for i in items if i["kind"] == "machine_productivity")["rows"] == 47
|
||||
|
||||
@@ -25,19 +25,30 @@ _RUNNER = """
|
||||
import { writeFileSync } from "node:fs";
|
||||
import {
|
||||
cellInfo, cellText, clampPage, columnTitle, latestOnly, pageCount, parseCellInput, rowCells,
|
||||
boldSegments, groupKinds, nextSort, noticeLines, outdatedBanner, shownColumns, valueWithUnit,
|
||||
boldSegments, filterValue, groupKinds, nextSort, noticeLines, outdatedBanner, shownColumns, valueWithUnit,
|
||||
} from "./Z01_MasterData_UI_Cells.js";
|
||||
|
||||
// kind 목록은 서버가 줌 — 화면이 제 목록을 들면 kind 가 늘 때마다 빠짐(2026-09-16 브레인)
|
||||
const served = [
|
||||
{ group: "base_price", kind: "labor", label: "노임", rows: 261 },
|
||||
{ group: "base_price", kind: "oil", label: "유가", rows: 36 },
|
||||
{ group: "pumsem_basis", kind: "coef", label: "토량환산계수", rows: 32 },
|
||||
{ group: "base_price", group_label: "기초단가", kind: "labor", label: "노임", rows: 261 },
|
||||
{ group: "base_price", group_label: "기초단가", kind: "oil", label: "유가", rows: 36 },
|
||||
{ group: "pumsem_basis", group_label: "품셈 기준", kind: "coef", label: "토량환산계수", rows: 32 },
|
||||
// 서버가 새 kind·새 갈래를 더해도 화면이 그대로 세움
|
||||
{ group: "pumsem_basis", kind: "machine_productivity", label: "기계 작업량", rows: 47 },
|
||||
{ group: "pumsem_basis", group_label: "품셈 기준", kind: "machine_productivity", label: "기계 작업량", rows: 47 },
|
||||
{ group: "work_axis", group_label: "공종 축", kind: "forest", label: "산림품셈 기준", rows: 358 },
|
||||
// 갈래 이름이 안 오면 키가 그대로 떠 빠진 것이 드러남(때우지 않음)
|
||||
{ group: "later_group", kind: "brand_new", label: "새 갈래 것", rows: 3 },
|
||||
];
|
||||
const grouped = groupKinds(served).map((g) => [g.group, g.items.map((i) => i.kind)]);
|
||||
const grouped = groupKinds(served).map((g) => [g.label, g.items.map((i) => i.kind)]);
|
||||
|
||||
// 거르기 — 안 골랐으면 서버가 이미 건 기본값을 보임(보이는 것 = 걸린 것) · 고르면 고른 값
|
||||
const roadFilter = {
|
||||
key: "use",
|
||||
label: "보기",
|
||||
options: [{ value: "all", label: "전부", rows: 995 }, { value: "road", label: "임도가 쓰는 것", rows: 120 }],
|
||||
default: "road",
|
||||
};
|
||||
const filtered = [filterValue(roadFilter, {}), filterValue(roadFilter, { use: "all" })];
|
||||
|
||||
// 열 제목 누르기 — 없음 → 오름 → 내림 → 없음(원본 차례) · 다른 열을 누르면 그 열 오름부터
|
||||
const sorts = [
|
||||
@@ -168,6 +179,9 @@ writeFileSync(process.argv[2], JSON.stringify({
|
||||
cellText("보통인부", null),
|
||||
// 소수 끝 떠돌이 자리(부동소수) — 기계 손료계수 0.0001811 이 0.00018109999999999998 로 보이던 것
|
||||
cellText(0.00018109999999999998, null),
|
||||
// 공종 축 목록 칸 — 괄호·따옴표 글자로 뜨던 것(2026-09-17 브레인 ④)
|
||||
cellText(["연암", "보통암", "경암"], null),
|
||||
cellText([], null),
|
||||
],
|
||||
pages: [pageCount(0, 50), pageCount(6999, 50), pageCount(50, 50)],
|
||||
row: rowCells({ extra: "x", daily_wage_krw: 250000, occupation_name: null }, columns),
|
||||
@@ -205,6 +219,7 @@ writeFileSync(process.argv[2], JSON.stringify({
|
||||
bolds,
|
||||
sorts,
|
||||
grouped,
|
||||
filtered,
|
||||
}));
|
||||
"""
|
||||
|
||||
@@ -252,6 +267,8 @@ def test_칸_글자와_숨김_열(tmp_path: Path) -> None:
|
||||
"예",
|
||||
"보통인부",
|
||||
"0.0001811",
|
||||
"연암, 보통암, 경암",
|
||||
"",
|
||||
]
|
||||
assert got["pages"] == [1, 140, 1]
|
||||
# columns 차례 · id 는 줄에 없어 「—」 · null 은 빈칸 · extra 는 안 그림
|
||||
@@ -323,9 +340,12 @@ def test_칸_글자와_숨김_열(tmp_path: Path) -> None:
|
||||
None,
|
||||
{"key": "item_code", "desc": False},
|
||||
]
|
||||
# 서버 차례 그대로 갈래별로 묶임 · 새 kind·새 갈래도 저절로 섬(화면 목록이 없음)
|
||||
# 서버 차례 그대로 갈래별로 묶임 · 상자 이름은 서버 group_label · 새 kind·새 갈래도 저절로 섬
|
||||
assert got["grouped"] == [
|
||||
["base_price", ["labor", "oil"]],
|
||||
["pumsem_basis", ["coef", "machine_productivity"]],
|
||||
["기초단가", ["labor", "oil"]],
|
||||
["품셈 기준", ["coef", "machine_productivity"]],
|
||||
["공종 축", ["forest"]],
|
||||
["later_group", ["brand_new"]],
|
||||
]
|
||||
# 거르기 상자 — 첫 선택지가 아니라 서버 기본값(어긋나면 「전부」로 보이며 걸러진 목록이 뜸)
|
||||
assert got["filtered"] == ["road", "all"]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Z01 마스터 데이터 — 읽기 API(갈래 → 파일 → 표 · 표 줄 쪽 나누기·검색) · 2026-09-15 브레인 새 판.
|
||||
|
||||
약속(브레인): 파일 목록·갈래·한글 이름·단위·보임은 **이름표 파일**(`resources/data_master_labels/`)에서만 ·
|
||||
표 = 화면이 표로 그릴 마디(이름표 91 이 정본) · ⭐ 트리 표 id 와 이름표 표 id 를 **양쪽으로** 세어 둘 다 0 ·
|
||||
표 = 화면이 표로 그릴 마디(이름표 92 이 정본) · ⭐ 트리 표 id 와 이름표 표 id 를 **양쪽으로** 세어 둘 다 0 ·
|
||||
열 이름 찾는 차례 「파일id/열key」 → 「열key」 → 영문 key · 줄 한 줄 = {열key: 값} · file·table 은 트리의 id.
|
||||
"""
|
||||
|
||||
@@ -50,12 +50,13 @@ def test_갈래는_이름표_차례_파일_34_강우_캐시는_없음(client: Te
|
||||
assert [g["key"] for g in groups] == [k for k in LABELS["kinds"]]
|
||||
counts = {g["key"]: len(g["files"]) for g in groups}
|
||||
# 2026-09-16 사용자 판정 — 한 현장 관찰값·교본 설명 섞임 둘은 참고 사례로 옮겼다.
|
||||
# 2026-09-17 공종 축 산출 일곱을 올림 — logic 15 → 19 · byproduct 4 → 6 · fingerprint 3 → 4
|
||||
assert counts == {
|
||||
"logic": 15,
|
||||
"logic": 19,
|
||||
"base_value": 9,
|
||||
"byproduct": 4,
|
||||
"byproduct": 6,
|
||||
"reference": 2,
|
||||
"fingerprint": 3,
|
||||
"fingerprint": 4,
|
||||
"seed": 1,
|
||||
}
|
||||
assert {g["key"]: g["label"] for g in groups}["base_value"] == "기초값"
|
||||
@@ -83,7 +84,9 @@ def test_트리_표_id_와_이름표_표_id_는_양쪽으로_같음(client: Test
|
||||
label_only += [f"{entry['file_id']}::{i}" for i in sorted(label_ids - tree_ids)]
|
||||
assert not_labelled == [], not_labelled
|
||||
assert label_only == [], label_only
|
||||
assert sum(len(t) for t in files.values()) == LABELS["counts"]["tables"] == 91
|
||||
# 2026-09-17 92 → 101 — 공종 축 산출 일곱 + 갈래 열쇠 장부 표.
|
||||
# 2026-09-17 101 → 102 — 건설 열쇠 장부에도 갈래 표(건설 갈래 펴기 ④).
|
||||
assert sum(len(t) for t in files.values()) == LABELS["counts"]["tables"] == 102
|
||||
for entry in LABELS["files"]: # 이름이 실제로 붙음 · 줄 수도 이름표 셈과 같음
|
||||
named = {t["key"]: t for t in entry["tables"]}
|
||||
for t in files[entry["file_id"]]:
|
||||
|
||||
@@ -0,0 +1,309 @@
|
||||
"""Z01 공종 축 API 틀 — 산림·건설 두 kind 를 기초단가와 같은 길로(2026-09-17 브레인 · PLAN_공종축 8-4).
|
||||
|
||||
못박는 것: 평평한 잎 목록 · 전체 경로 이름(겹침 없음) · 계산 규칙(choose_one 91 · sum_steps 3) ·
|
||||
불변 열쇠가 `@id` · 건설도 같은 길(자료가 있어야 상자가 섬) · 고치기는 막힘.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from Z01_MasterData import Z01_MasterData_Overrides as overrides
|
||||
from Z01_MasterData import Z01_MasterData_Router as router_module
|
||||
from Z01_MasterData import Z01_MasterData_WorkItems as work_items
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> TestClient:
|
||||
from common_util.common_util_auth import verify_session
|
||||
|
||||
monkeypatch.setattr(overrides, "OVERRIDE_DIR", tmp_path / "data_master_override")
|
||||
app = FastAPI()
|
||||
app.include_router(router_module.router)
|
||||
app.dependency_overrides[verify_session] = lambda: {"user_id": 7, "role": "SYSTEM_ADMIN"}
|
||||
return TestClient(app)
|
||||
|
||||
|
||||
def _get(client: TestClient, kind: str, **params) -> dict:
|
||||
res = client.get(f"/api/master-data/base-prices/{kind}", params=params)
|
||||
assert res.status_code == 200, res.text
|
||||
return res.json()
|
||||
|
||||
|
||||
def test_산림_평평한_잎_목록과_계산_규칙(client: TestClient) -> None:
|
||||
listed = client.get("/api/master-data/base-prices").json()["kinds"]
|
||||
forest = next(i for i in listed if i["kind"] == "work_item_forest")
|
||||
assert (
|
||||
forest["group"] == "work_item" and forest["rows"] == 620
|
||||
) # 갈래마다 한 줄 · 헛단위 7줄 뺌(2026-09-17)
|
||||
# 상자 이름은 이름표 것 — 영문 키(work_item)로 뜨던 자리(2026-09-17 브레인 ①)
|
||||
assert {i["group"]: i["group_label"] for i in listed} == {
|
||||
"base_price": "기초단가",
|
||||
"pumsem_basis": "품셈 기준",
|
||||
"work_item": "공종 축",
|
||||
}
|
||||
|
||||
table = _get(client, "work_item_forest", size=500, axis_role="") # 「전부」 — 총칙까지
|
||||
assert table["total"] == 620 and table["stats"] == {"work_items": 360, "units": 620}
|
||||
assert table["editable"] == [] and set(table["locked"]) == set(table["sortable"])
|
||||
rows = table["rows"] + _get(client, "work_item_forest", size=500, page=2, axis_role="")["rows"]
|
||||
assert len({r["@id"] for r in rows}) == 620
|
||||
assert (
|
||||
len({r["@key"] for r in rows}) == len({r["name"] for r in rows}) == 360
|
||||
) # 잎 이름 겹침 — 경로로 풀림 · 목차 오기 바로잡아 358 → 360(12-17-2 · 12-24-1)
|
||||
by_code = {r["work_item_code"]: r for r in rows}
|
||||
assert by_code["FP-09-03-02"]["name"] == "토공 › 토사깍기 › 기계"
|
||||
|
||||
modes = [r["mode"] for r in table["rules"]]
|
||||
assert modes.count("choose_one") == 91 and modes.count("sum_steps") == 3
|
||||
blast = next(r for r in table["rules"] if r["work_item_code"] == "FP-09-05")
|
||||
assert [s["weight"] for s in blast["steps"]] == ["0.1", "0.9", "1"]
|
||||
# 계산 규칙 칸은 이름표 이름 — 「sum_steps」 영문으로는 더해야 하는 줄로 안 읽힘(브레인 ②)
|
||||
assert by_code["FP-09-05-02"]["rule"] == "아래를 단계로 다 더함"
|
||||
assert by_code["FP-09-03-02"]["rule"] == "아래 하나를 고름"
|
||||
assert by_code["FP-09-05-02"]["step_weight"] == "0.9"
|
||||
# 2026-09-17 FW- 가 옴(데스크탑 서브) — 알림이 사라지는 것이 곧 열쇠가 섰다는 증거.
|
||||
assert all(r["@id"].startswith("FW-") for r in rows) # 불변 열쇠(데스크탑 서브 7dd2c515)
|
||||
assert not any("불변 열쇠" in line for line in table["notice"])
|
||||
|
||||
found = _get(client, "work_item_forest", q="발파암 ›")
|
||||
assert found["total"] == 5 # 절취 · 깍기(연암·보통암·경암) · 집토
|
||||
|
||||
|
||||
def test_건설_공종_축도_같은_길(client: TestClient) -> None:
|
||||
"""건설(2026-09-17 뽑음) — 부문이 뿌리라 경로가 부문부터 · 열쇠 CW- · 계산 규칙은 전부 choose_one.
|
||||
갈래도 산림과 같은 표 읽기로 폄(④ · 공종 997 → 줄 1,293 · 갈래 열쇠 `CW-00063#01`)."""
|
||||
table = _get(client, "work_item_const", size=500, axis_role="")
|
||||
assert table["stats"] == {"work_items": 997, "units": 1293} and table["total"] == 1293
|
||||
assert sum(1 for r in table["rows"] if "#" in r["@id"]) > 0
|
||||
rows = table["rows"]
|
||||
assert all(r["@id"].startswith("CW-") for r in rows)
|
||||
assert all(r["name"].split(" › ")[0].endswith("부문") for r in rows)
|
||||
assert {r["mode"] for r in table["rules"]} == {"choose_one"}
|
||||
assert not any("불변 열쇠" in line for line in table["notice"])
|
||||
found = _get(client, "work_item_const", q="굴착(인력/토사)")
|
||||
assert [r["name"] for r in found["rows"]] == ["공통부문 › 토공사 › 굴착 › 굴착(인력/토사)"]
|
||||
|
||||
|
||||
def test_불변_열쇠가_오면_id_로_씀_자료_파일은_판_id_로_가림(
|
||||
client: TestClient, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
folder = tmp_path / "data_work_item_master"
|
||||
folder.mkdir()
|
||||
fake = {
|
||||
"dataset_version": {"dataset_id": "pum_const"},
|
||||
"effective_date": "2026-01-01",
|
||||
"work_items": [
|
||||
{"work_item_code": "CP-01", "work_item_key": "CW-00001", "number": "1",
|
||||
"name": "토공", "parent_code": None, "tables": [], "parent_mode": "sum_steps",
|
||||
"steps": [{"code": "CP-01-01", "weight": "0.5"}], "steps_basis": "시험", "variant_keys": []},
|
||||
{"work_item_code": "CP-01-01", "work_item_key": "CW-00002", "number": "1-1",
|
||||
"name": "인력", "parent_code": "CP-01", "tables": [{"pum_table_id": "C0001"}],
|
||||
"variant_keys": ["갑", "을"],
|
||||
"variants": [{"variant_key": "01", "name": "갑", "name_clean": "갑"},
|
||||
{"variant_key": "02", "name": "을", "name_clean": "을"}]},
|
||||
],
|
||||
} # fmt: skip
|
||||
(folder / "const_work_item_master_2026-01-01.json").write_text(
|
||||
json.dumps(fake, ensure_ascii=False), encoding="utf-8"
|
||||
)
|
||||
monkeypatch.setattr(work_items, "FOLDER", folder)
|
||||
|
||||
table = _get(client, "work_item_const")
|
||||
# 갈래마다 한 줄 · 열쇠는 **글자가 아니라 번호**(2026-09-17 브레인 — 장부가 준다)
|
||||
assert [r["@id"] for r in table["rows"]] == ["CW-00002#01", "CW-00002#02"]
|
||||
assert {r["@key"] for r in table["rows"]} == {"CW-00002"}
|
||||
assert table["rows"][0]["work_item_code"] == "CP-01-01" # 목차 코드는 칸으로 남음
|
||||
assert table["rows"][0]["name"] == "토공 › 인력"
|
||||
assert table["stats"] == {"work_items": 1, "units": 2}
|
||||
assert table["rules"][0]["steps"] == [{"@id": "CW-00002", "weight": "0.5"}]
|
||||
assert _get(client, "work_item_forest")["total"] == 0 # 산림 파일은 이 폴더에 없음
|
||||
|
||||
res = client.put(
|
||||
"/api/master-data/base-prices/work_item_const/CW-00002%2301",
|
||||
json={"values": {"name": "바꿈"}},
|
||||
)
|
||||
assert res.status_code == 400 and "공종 축 뼈대" in res.json()["detail"]
|
||||
|
||||
|
||||
def test_거르기는_자료에_있는_가름만_서버가_판정(client: TestClient) -> None:
|
||||
"""화면(서브 b890fba0)이 `filters` 를 받으면 상자를 세우고 고른 값만 보냄 — 판정은 서버 한 곳.
|
||||
건설 부문 다섯 · 줄 구실(이름은 axis_policy.json) · 연결(연결고리 표)."""
|
||||
const = _get(client, "work_item_const", size=1, axis_role="")
|
||||
by_key = {f["key"]: f for f in const["filters"]}
|
||||
assert set(by_key) == {"link", "division", "axis_role"}
|
||||
division = by_key["division"]
|
||||
assert division["default"] == ""
|
||||
assert division["options"][0] == {"value": "", "label": "전부", "rows": const["total"]}
|
||||
assert [o["value"] for o in division["options"][1:]] == [
|
||||
"공통부문",
|
||||
"토목부문",
|
||||
"건축부문",
|
||||
"기계설비부문",
|
||||
"유지관리부문",
|
||||
]
|
||||
assert sum(o["rows"] for o in division["options"][1:]) == const["total"]
|
||||
# 2026-09-17 목록에서는 「총칙」이 아니라 **공종이 아님**을 드러내는 이름(잣대 `list_label`).
|
||||
assert {o["value"]: o["label"] for o in by_key["axis_role"]["options"]}[
|
||||
"general_provision"
|
||||
] == "공종 아님 · 규정·기준"
|
||||
assert by_key["axis_role"]["default"] == "work_item" # 감춘 것이지 지운 것이 아님
|
||||
|
||||
civil = _get(client, "work_item_const", size=500, division="토목부문", axis_role="")
|
||||
assert civil["total"] == next(
|
||||
o["rows"] for o in division["options"] if o["value"] == "토목부문"
|
||||
)
|
||||
assert all(r["name"].startswith("토목부문 › ") for r in civil["rows"])
|
||||
rules = _get(
|
||||
client, "work_item_const", size=500, division="공통부문", axis_role="general_provision"
|
||||
)
|
||||
assert rules["total"] > 0
|
||||
# 2026-09-17 건설 총칙 뿌리가 둘 — 적용기준(37) + 건설기계(75 · 산림 FP-02 와 짝)
|
||||
assert all(
|
||||
r["name"].startswith(("공통부문 › 적용기준", "공통부문 › 건설기계")) for r in rules["rows"]
|
||||
)
|
||||
role_names = {o["value"]: o["label"] for o in by_key["axis_role"]["options"]}
|
||||
# 표시 칸 — 이름은 잣대(`list_label` · 「공종 아님 · 규정·기준」) 한 곳
|
||||
assert {r["axis_role"] for r in rules["rows"]} == {role_names["general_provision"]}
|
||||
|
||||
assert [f["key"] for f in _get(client, "work_item_forest", size=1)["filters"]] == [
|
||||
"link",
|
||||
"axis_role",
|
||||
]
|
||||
assert _get(client, "labor", size=1, division="토목부문")["total"] == 261 # 다른 표는 안 거름
|
||||
|
||||
|
||||
def test_연결고리_표가_이은_줄에_연결됨_감추지_않고_표시만(client: TestClient) -> None:
|
||||
"""브레인 2026-09-17 — 「임도가 쓰는 것」 = 연결고리 표(work_item_link)가 **확정**으로 이은 줄.
|
||||
기본은 다 보임 · 표시 「연결됨」 · 「연결된 것만」 상자 하나 · 판정은 그 표(코드에 목록 없음)."""
|
||||
from Z01_MasterData import Z01_MasterData_WorkItems as work_items
|
||||
|
||||
link_doc = json.loads(
|
||||
sorted(work_items.LINK_FOLDER.glob("work_item_link_*.json"))[-1].read_text(encoding="utf-8")
|
||||
)
|
||||
confirmed = {
|
||||
key
|
||||
for link in link_doc["links"]
|
||||
if link["status"] == "확정"
|
||||
for key in (link["forest_key"], link["const_key"])
|
||||
if key
|
||||
}
|
||||
unconfirmed = {
|
||||
key
|
||||
for link in link_doc["links"]
|
||||
if link["status"] != "확정"
|
||||
for key in (link["forest_key"], link["const_key"])
|
||||
if key
|
||||
} - confirmed
|
||||
for kind, prefix in (("work_item_const", "CW-"), ("work_item_forest", "FW-")):
|
||||
every = _get(client, kind, size=500, axis_role="")
|
||||
rows = [
|
||||
row
|
||||
for page in range(1, every["total"] // 500 + 2)
|
||||
for row in _get(client, kind, size=500, page=page, axis_role="")["rows"]
|
||||
]
|
||||
assert len(rows) == every["total"] # 연결 거르기 기본은 「전부」 — 다 보임
|
||||
marked = {r["@key"] for r in rows if r["link"] == "연결됨"} # 갈래 줄은 공종 열쇠로
|
||||
listed = {r["@key"] for r in rows} # 표가 안 붙은 묶는 마디(CW-01329 따위)는 목록 밖
|
||||
assert marked == {k for k in confirmed if k.startswith(prefix) and k in listed}
|
||||
assert marked # 연결이 늘면 저절로 늘어남 — 지금도 한 줄 이상
|
||||
assert not marked & unconfirmed
|
||||
only = _get(client, kind, size=500, link="연결됨", axis_role="")
|
||||
assert {r["@key"] for r in only["rows"]} == marked
|
||||
box = next(f for f in every["filters"] if f["key"] == "link")
|
||||
assert box["default"] == "" and box["options"][1]["rows"] == only["total"]
|
||||
|
||||
|
||||
def test_갈래마다_한_줄이라_편_하나를_집는다(client: TestClient) -> None:
|
||||
"""2026-09-17 브레인 ③ — 일위대가는 갈래마다 다름. 교목굴취(나무높이) 한 줄이면 어느 높이 값인지 못 고름."""
|
||||
rows = _get(client, "work_item_forest", size=50, q="교목굴취(나무높이)")["rows"]
|
||||
assert [r["variant"] for r in rows] == [
|
||||
"1.0 이하", "1.1∼1.5", "1.6∼2.0", "2.1∼2.5", "2.6∼3.0",
|
||||
"3.1∼3.5", "3.6∼4.0", "4.1∼4.5", "4.6∼5.0",
|
||||
] # fmt: skip
|
||||
# 열쇠에 갈래 **글자**를 담지 않는다 — 원문 물결표·자간이 다듬어지면 열쇠가 갈린다(브레인).
|
||||
assert rows[1]["@id"] == "FW-00105#02" and rows[1]["@key"] == "FW-00105"
|
||||
assert rows[1]["variant"] == "1.1∼1.5" # 글자는 보일 이름 칸으로
|
||||
# 갈래 없는 공종은 한 줄 · 열쇠에 # 없음
|
||||
plain = _get(client, "work_item_forest", q="교목굴취(근원직경)")["rows"]
|
||||
assert [(r["@id"], r["variant"]) for r in plain] == [("FW-00106", None)]
|
||||
# 갈래 이름으로도 찾힘
|
||||
assert _get(client, "work_item_forest", q="2.1∼2.5")["total"] >= 1
|
||||
|
||||
|
||||
def test_총칙_줄은_기본_감춤_전부를_고르면_보임(client: TestClient) -> None:
|
||||
"""2026-09-17 브레인 — 값을 안 보내면 상자 기본값(공종)으로 거름 · 화면은 그 기본값을 상자에 보임."""
|
||||
for kind in ("work_item_forest", "work_item_const"):
|
||||
shown = _get(client, kind, size=500)
|
||||
role = next(f for f in shown["filters"] if f["key"] == "axis_role")
|
||||
assert role["default"] == "work_item"
|
||||
counts = {o["value"]: o["rows"] for o in role["options"]}
|
||||
assert counts["general_provision"] > 0
|
||||
# 기본 = 공종만 · 찾기 결과로 기본이 흔들리지 않음(총칙만 걸리는 낱말도 기본은 감춤)
|
||||
assert shown["total"] == counts["work_item"]
|
||||
assert (
|
||||
_get(client, kind, size=500, q="적용기준")["total"]
|
||||
< _get(client, kind, size=500, q="적용기준", axis_role="")["total"]
|
||||
)
|
||||
# 「전부」(빈 값을 보냄) → 총칙까지
|
||||
assert _get(client, kind, size=1, axis_role="")["total"] == counts[""]
|
||||
assert (
|
||||
_get(client, kind, size=1, axis_role="general_provision")["total"]
|
||||
== (counts["general_provision"])
|
||||
)
|
||||
|
||||
|
||||
def test_찾기는_물결표_가운뎃점_띄어쓰기를_같게_봄(client: TestClient) -> None:
|
||||
"""원문이 물결표를 둘(∼·~)로 쓰고 자간 공백이 들쭉날쭉 — 사람이 친 「~」·붙여 쓴 이름으로도 찾힘.
|
||||
⚠ 찾기만 — 줄 값·열쇠는 원문 그대로(서브 훑기 · 브레인 2026-09-17)."""
|
||||
wavy = _get(client, "work_item_forest", size=50, q="2.1∼2.5")["total"]
|
||||
assert wavy >= 1
|
||||
assert _get(client, "work_item_forest", size=50, q="2.1~2.5")["total"] == wavy
|
||||
assert _get(client, "work_item_forest", size=50, q="2.1~2.5")["total"] == wavy
|
||||
rows = _get(client, "work_item_forest", size=50, q="2.1~2.5")["rows"]
|
||||
assert all("~" not in r["@id"] for r in rows) # 열쇠는 손질 안 함
|
||||
spaced = _get(client, "work_item_forest", size=50, q="토사 깍기", axis_role="")["total"]
|
||||
assert (
|
||||
spaced == _get(client, "work_item_forest", size=50, q="토사깍기", axis_role="")["total"] > 0
|
||||
)
|
||||
|
||||
|
||||
def test_이름_다듬기는_보일_이름만_열쇠는_그대로(
|
||||
client: TestClient, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""⭐ 갈래 불변 열쇠를 세운 까닭(브레인 2026-09-17) — 자간 공백(「측 량」·「평 규 준 틀」)을 다듬어도
|
||||
줄 열쇠(`FW-…#01`)가 한 글자도 안 흔들림. 다듬기를 끈 목록과 켠 목록의 열쇠를 맞댐."""
|
||||
from B08_Quantity import B08_Quantity_Build_WorkItemMaster_Keys as keys
|
||||
from Z01_MasterData import Z01_MasterData_WorkItems as work_items
|
||||
|
||||
for kind in work_items.KINDS:
|
||||
tidy = work_items.base_rows(kind)
|
||||
with monkeypatch.context() as patch:
|
||||
patch.setattr(keys, "tidy_spacing", lambda text: " ".join(str(text or "").split()))
|
||||
raw = work_items.base_rows(kind)
|
||||
assert [r["@id"] for r in tidy] == [r["@id"] for r in raw]
|
||||
if kind == "work_item_const":
|
||||
assert (
|
||||
sum(a["name"] != b["name"] for a, b in zip(tidy, raw)) > 0
|
||||
) # 실제로 다듬어진 줄이 있음
|
||||
shown = _get(client, "work_item_const", size=50, q="기준점 측량", axis_role="")["rows"]
|
||||
assert shown and all("측 량" not in r["name"] and "› 측량 ›" in r["name"] for r in shown)
|
||||
|
||||
|
||||
def test_품셈_번호는_마디마다_수로_세움(client: TestClient) -> None:
|
||||
"""글자로 세우면 「10-1」이 「2-1」 앞에 섬(서브 훑기) — 마디마다 수로."""
|
||||
rows = _get(client, "work_item_forest", size=500, sort="number", axis_role="")["rows"]
|
||||
numbers = [tuple(int(x) for x in r["number"].split("-")) for r in rows if "-" in r["number"]]
|
||||
assert len(numbers) > 300 and numbers == sorted(numbers)
|
||||
|
||||
|
||||
def test_건설_늘_빈_칸은_감춤(client: TestClient) -> None:
|
||||
"""건설은 합산형 선언이 없어 「단계 무게」가 늘 빈칸 — 감춤(산림은 보임)."""
|
||||
hidden = lambda kind: {c["key"]: c["hidden"] for c in _get(client, kind, size=1)["columns"]} # noqa: E731
|
||||
assert hidden("work_item_const")["step_weight"] is True
|
||||
assert hidden("work_item_forest")["step_weight"] is False
|
||||
Reference in New Issue
Block a user