test(776-1): 경쟁사 출력 단계 고정 시험 — 여덟 파일 S1(정규화 투영 해시 · 수 · 갈래 · 호표 참조 그래프) · S2(셈 단위 275 지문 해시)

- 새 test_621_hopyo_stages.py + fixtures/m02_hopyo_stages.json · 15 초 · --write 로 새 판 굳힘
- 투영 = 지금 HP 칸만 — Book 에 칸 더함은 그대로 · 빼기 · 이름 바꿈은 깨짐
- helper_m02_hopyo_golden 죽은 갈래 걷음 — EST 블록 글 다시 채움(읽기가 이미 글 붙임 · 223 블록 모두)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-04 08:36:49 +09:00
co-authored by Claude Opus 5.5
parent 4e0ea28aec
commit 9d26e5fcd6
3 changed files with 708 additions and 26 deletions
@@ -0,0 +1,518 @@
{
"북부청임도": {
"S1": {
"해시": "2a4df1c8e7a2",
"꼴": "엑셀꼴",
"호표": 68,
"줄": 415,
"블록": 95,
"갈래": {
"%": 1,
"기계": 3,
"단가산출": 268,
"별도계상": 64,
"자재": 78,
"호표": 1
},
"참조": {
"참조줄": 1,
"부르는": 1,
"불리는": 1,
"깊이": {
"1": 67,
"2": 1
},
"순환": 0
}
},
"S2": {
"단산:46|레미콘타설(장비사용)|철근구조물-소량타설|m3": "14b94bd44235",
"단산:24|합판거푸집|4회|m2": "dc9227913b7d",
"단산:33|기초잡석채집,운반,다짐|발파암활용,덤프15톤,l=500m|m3": "569f93218071",
"단산:47|철근가공조립(현장)|type-i(소량시공)|톤": "f333a56a3d0e",
"단산:6|야면석채집및운반(계곡내)|굴삭기0.6m3,35cm,4.5톤,l=500m|m2": "06a8c10d89e7",
"단산:3|고임돌채집및운반|굴삭기0.6m3(90%),4.5톤,l=500m|m3": "4c1b4b4cb599",
"단산:10|야면석소운반|l=35cm|m2": "e96d400b173b",
"단산:39|야면석찰붙임|l3=35cm적용,장비사용|m2": "e781e3163b81",
"단산:93|구조물터파기(인력100%)|토사|m3": "c523b43c617b",
"단산:12|구조물되메우기(인력100%)||m3": "559c125ef42e",
"단산:13|잔토처리(인력100%)||m3": "eb0395c76e02",
"단산:22|레미콘타설(장비사용)|무근구조물|m3": "0c3cebbe76de",
"단산:26|유로폼|보통,0~7m|m2": "a301adcd84cd",
"단산:18|문양거푸집||m2": "9f4efb5510bf",
"단산:37|p.v.c파이프설치|ø50mm|m": "a408f8e02b28",
"단산:38|다웰바설치|ø32x1000|개": "13410747be6e",
"단산:41|휠라설치|t=15mm,1000x1500mm|m2": "4df1b309aca4",
"단산:29|철근가공조립(현장)|연결철근,type-ix30%적용|톤": "2b96b0b322d2",
"단산:30|큰돌채집운반|공사중발생석활용50%적용|m2": "a0d7114b5599",
"단산:32|큰돌쌓기(메쌓기)|직경40cm이상~60cm미만|m2": "06881edcaad6",
"단산:2|뒷채움돌채집운반뒷채움(발파)|굴삭기0.6m3(90%),4.5톤,l=500m|m3": "8d60db527bae",
"단산:31|큰돌쌓기(찰쌓기)_배수공|직경40cm이상~60cm미만|m2": "c0610580cae7",
"단산:21|레미콘타설(인력운반)|무근구조물-소량타설|m3": "7ee6fe1e8d9d",
"단산:25|합판거푸집|6회|m2": "b5b6ec2a0beb",
"단산:42|도로반사경설치|d=1000(sts)|개소": "e521922cb4ba",
"단산:88|야면석메붙임|l3=35cm내외,장비사용|m2": "91ac9e8d76f7",
"단산:92|큰돌메붙임|직경40cm이상~60cm미만|m2": "26e6c1b118c5",
"단산:91|큰돌찰붙임|직경40cm이상~60cm미만|m2": "bd288a8d830f",
"단산:57|레미콘타설(장비사용)|소형구조물|m3": "b4bee2367968",
"단산:71|야면석찰쌓기|l3=35cm적용,장비사용|m2": "1649ce3151fb",
"단산:43|면고르기||m2": "f45d3e3af178",
"줄:대형브레이카|+굴착기(0.7m3)|시간": "fc11668f29eb",
"단산:60|와이어메시바닥깔기|#6100x100|m2": "567c16874a89",
"단산:62|원형돌망태설치및돌채움|#8,45x45cm|m2": "9ad93f86f385",
"단산:61|조약돌채집및운반|굴삭기0.6m3(90%),4.5톤,l=500m|m3": "ab7d8e14de22",
"단산:63|돌망태형옹벽설치|사각돌망태|m2": "6487dbf12ab6",
"단산:65|콘크리트포장(인력)|t=20cm,종단15%미만|m3": "2c48d8cc2468",
"단산:64|비탈면보호공|평떼|m2": "5d9d68fa8679",
"단산:66|콘크리트포장(인력)|t=20cm,종단15%이상|m3": "2374789b6ae0",
"단산:70|고화매트설치||m2": "80be3cc78047",
"단산:90|큰돌쌓기(찰쌓기)|직경40cm이상~60cm미만|m2": "52a0f47bb751",
"단산:23|레미콘타설(장비사용)|철근구조물|m3": "fe0d22d627da",
"단산:73|강관동바리|암거용-1개월사용|공/m3": "bc3f5092d701",
"단산:72|철근가공조립(현장)|type-ll|톤": "75dda2f569a2",
"단산:74|강관비계|10m이하,3개월사용|m2": "90ce7f0da95c",
"단산:49|흙깍기(토사)|굴삭기1.0m3,보통토사|m3": "f9aea56b06bf",
"단산:56|잔토정리|굴삭기0.6m3|m3": "9f76f0e32710",
"단산:82|잡철물제작설치|규격철물설치(일반철재)|톤": "531650fd115f",
"단산:95|도장(철재면2회)|방청1,유성2|m2": "92867210fce0",
"단산:81|임도안전표지판설치|삼각,900mm|개소": "ee09c0727967"
}
},
"북부청사방": {
"S1": {
"해시": "54e46e934128",
"꼴": "엑셀꼴",
"호표": 27,
"줄": 201,
"블록": 67,
"갈래": {
"%": 1,
"기계": 1,
"노임": 2,
"단가산출": 139,
"별도계상": 18,
"자재": 38,
"호표": 2
},
"참조": {
"참조줄": 2,
"부르는": 2,
"불리는": 2,
"깊이": {
"1": 25,
"2": 2
},
"순환": 0
}
},
"S2": {
"단산:22|레미콘타설(장비사용)|철근구조물|m3": "e2940f403c6c",
"단산:20|레미콘타설(장비사용)|무근구조물|m3": "d0347d9e519e",
"단산:38|유로폼(euroform)|6회,보통|m2": "0cd2081fa29f",
"단산:53|철근가공조립(현장)|type-ii|톤": "d6d92c499d78",
"단산:4|강관비계|(10m이하)|m2": "7c1c9f2d283a",
"단산:3|강관동바리|(2.5m이하)|공/m3": "83d2a92252c6",
"단산:37|유로폼(euroform)|6회,간단|m2": "d6ac6fccfcfd",
"단산:52|철근가공조립(현장)|type-i(소량)|톤": "e3d8470f9851",
"단산:49|철강재운반(할증포함)|(구역화물)|톤": "a619ce7fe816",
"단산:51|철골설치||톤": "8e292d7859f8",
"단산:50|철골가공조립(공장)|(대형)|톤": "b386a69202ef",
"단산:42|잡철물제작||톤": "ef730a79e03f",
"단산:67|합판거푸집(6회)||m2": "b3ba20147436",
"단산:33|와이어메시바닥깔기||m2": "2f898ef1909d",
"단산:11|구조물터파기(토사)||m3": "5b2c549e3ec2",
"단산:8|구조물되메우기및다짐||m3": "184c85a346db",
"단산:39|잔토처리||m3": "147afeac7acd",
"단산:21|레미콘타설(장비사용)|무근구조물-소량타설|m3": "6b7edafbfd22",
"단산:66|합판거푸집(4회)||m2": "39bdcddca57c",
"단산:12|기초잡석다짐및채집운반|(9:1)(0.6m3+4.5ton)|m3": "dc7a46817506",
"단산:26|배수관부설|(d=1000mm)|m": "e6f492785d36",
"호표:7": "9238de708278",
"단산:19|레미콘타설(인력운반)|무근구조물-소량타설|m3": "728a28bc9224",
"단산:9|구조물터파기|인력|m3": "06855a4c159a",
"단산:7|구조물되메우기|인력|m3": "8d23c8175496",
"단산:40|잔토처리|인력|m3": "e62dedcec317",
"단산:35|원형돌망태조립설치|산림사업품셈|조": "05d490e21236",
"단산:36|원형돌망태채움|산림사업품셈|조": "a4b290a399fd",
"단산:43|조약돌채집운반|(9:1)(0.6m3+4.5ton)|m3": "3d10f7bb409b",
"단산:10|구조물터파기(암)||m3": "884a1241cb59",
"단산:27|배수관부설|(d=500mm)|m": "e00f055d8b21",
"단산:18|레미콘타설(무근펌프카)(130m3)|41m(80-95m3/hr)|m3": "07d3e7ce5cf4",
"줄:콘크리트펌프차|41m,80~95m3/hr|시간": "6dfaed1dd20a",
"단산:25|문양거푸집||m2": "fba1f776a83c",
"단산:46|지수판설치|산림사업품셈|m": "3ddb72dcd81d",
"단산:1|p.v.c파이프설치|산림사업품셈|m": "ec1cd531e2e6",
"단산:17|떼붙임(평떼)|산림사업품셈|m2": "fdd2e6bd02f0",
"단산:60|큰돌메쌓기|40cm-60cm,(산림사업품셈)|m2": "b44908334fae",
"단산:5|고임돌채집및운반|(9:1)(0.6m3+4.5ton)|m3": "6d67aa013e60",
"단산:16|뒷채움잡석채집운반다짐|(9:1)(0.6m3+4.5ton)|m3": "2f37d4203fdc",
"단산:62|큰돌찰쌓기|40cm-60cm,(산림사업품셈)|m2": "6b65c0b531df",
"단산:63|큰돌채집운반|산림사업품셈|m2": "09662a8f8d36",
"단산:13|뒷채움잡석구입운반||m3": "797df59d7756",
"단산:15|뒷채움잡석뒷채움||m3": "012a67631d07",
"단산:14|뒷채움잡석구입운반|(창봉기번3)(2차)|m3": "42b81776b3e4",
"단산:59|큰돌메붙임|40cm-60cm,(산림사업품셈)|m2": "cc09e80f4533",
"단산:61|큰돌찰붙임|40cm-60cm,(산림사업품셈)|m2": "469d3c56b621"
}
},
"거창": {
"S1": {
"해시": "bf450b2215f9",
"꼴": "엑셀꼴",
"호표": 14,
"줄": 68,
"블록": 61,
"갈래": {
"%": 6,
"기계": 3,
"노임": 10,
"단가산출": 21,
"별도계상": 3,
"자재": 13,
"호표": 12
},
"참조": {
"참조줄": 12,
"부르는": 6,
"불리는": 6,
"깊이": {
"1": 8,
"2": 4,
"3": 2
},
"순환": 0
}
},
"S2": {
"줄:굴삭기(타이어형)|0.6m3|시간": "7e2ab592356e",
"호표:1": "1f6345e0085e",
"호표:2": "dacbaeeaa6c9",
"호표:3": "03d873100e35",
"단산:30|유로폼|보통,0~7m|m2": "e278a75b7213",
"단산:38|철근가공및조립(현장)|type-ii-1|톤": "4ea51eae487f",
"단산:39|강관비계|10m이하(3개월)|m2": "769cb5147b54",
"단산:29|유로폼|간단,0~7m|m2": "dcf229abaa91",
"호표:10": "fc2cc4cd2868",
"호표:11": "998a0263fed9",
"단산:36|레미콘타설(장비사용타설)|무근구조물|m3": "39982206412d",
"단산:40|레미콘타설(장비사용타설)|철근구조물|m3": "41c11b60b1e1",
"단산:41|합판거푸집|4회|m2": "dfd5ab302d05",
"단산:31|철근가공및조립(현장)|type-i-1(소형)|톤": "a98e998b35c0",
"단산:42|강관동바리|2.5m이하(1개월)|공m3": "99d65ebd3dcc",
"단산:43|잡철물제작및설치(현장)|일반철재|톤": "71c1f965449f",
"단산:44|안내표지판설치|단주식|개소": "38427ebd6d28",
"단산:37|레미콘타설(인력운반타설)|소형구조물|m3": "6c2df8e4fd6a",
"단산:45|합판거푸집|6회,간단|m2": "85dcc3e13e28"
}
},
"봉화": {
"S1": {
"해시": "fcf03906734a",
"꼴": "STmate",
"호표": 45,
"줄": 209,
"블록": 65,
"갈래": {
"%": 7,
"기계": 7,
"노임": 30,
"단가산출": 103,
"별도계상": 7,
"자재": 33,
"호표": 22
},
"참조": {
"참조줄": 22,
"부르는": 18,
"불리는": 19,
"깊이": {
"1": 27,
"2": 14,
"3": 4
},
"순환": 0
}
},
"S2": {
"줄:굴삭기(타이어)|0.6m3,(부착용집게)|시간": "7ba959ec2e9d",
"호표:1": "ee8a116be0f0",
"단산:96|레미콘타설(장비)|무근,소형|m3": "fcbfc0642dc2",
"단산:102|합판거푸집(간단)|간단|m2": "70042bc48cb6",
"단산:4|글자새김|자당6x6|자": "ff7e1309d192",
"단산:2|구조물터파기(토사)|굴삭기0.2m3|m3": "2ce888963761",
"단산:7|구조물되메우기|굴삭기0.2m3|m3": "c72b4a1494d6",
"단산:49|잔토처리|굴삭기0.2m3|m3": "800b67112b8d",
"호표:5": "566b3ec95fef",
"단산:43|깬잡석채집|l=45cm내외|m2": "6c67d6e7d946",
"단산:5|고임돌채집|기계(0.7m3,100%)|m3": "304739062d51",
"단산:6|막자갈채집|기계(0.7m3,100%)|m3": "b149f96f7918",
"단산:8|기초다짐및뒤채움|소형장비사용시공|m3": "d1e2a67c4989",
"단산:93|유로폼설치및해체|간단|m2": "5174b34972ac",
"단산:95|레미콘타설(장비)|무근|m3": "1e45fdfca996",
"단산:89|콘크리트믹서사용|0.45m3|m3": "71825ddc35da",
"단산:94|유로폼설치및해체|보통|m2": "1985ee7ba47c",
"단산:98|철근가공및조립|type-i-1,현장가공조립|톤": "f91f3febebf1",
"호표:9": "992b00a50c65",
"호표:13": "2ecc59f87b37",
"단산:51|깬잡석채집|l=30cm내외|m2": "d23b63b9635a",
"단산:12|콘크리트(기계비빔)|무근구조물|m3": "c9ca9357a44a",
"호표:17": "b50496d3f922",
"호표:18": "d796d9bbd4bd",
"호표:22": "2c27c0d9b1e1",
"단산:23|구조물터파기(육상토사0~1m)|기계90%+인력10%(굴삭기0.7m3)|m3": "1e4b4e03372f",
"단산:24|잔토처리|굴삭기0.7m3|m3": "b34905b7e0c2",
"줄:굴삭기(타이어)|0.6m3|시간": "733bdb0730f2",
"호표:24": "24b7a61d2cac",
"단산:3|깬잡석채집|l=35cm내외|m2": "6fa38565606b",
"호표:26": "253290e88b9f",
"단산:52|깬잡석채집|l=60cm내외|m2": "daab119d679a",
"호표:28": "10cf341d2ca3",
"단산:81|철근가공및조립(현장)|일반(type-i-1)|톤": "39d3a7e2e7a9",
"호표:31": "82b74ab253f3",
"단산:77|합판거푸집|3회(복잡)|m2": "deaabc9a391f",
"호표:33": "35144fc92a5c",
"호표:34": "9b6cffd83699",
"호표:35": "4eb05246e0c3",
"단산:10|레미콘타설(장비사용)|소형구조물|m3": "8717ad5b8652",
"단산:90|평떼붙임||m2": "13cce5a5166b",
"단산:91|단끊기||m": "8701e94849ea",
"단산:97|레미콘타설(장비)|철근|m3": "6c8931bf2865",
"단산:103|레미콘타설(장비)|무근,버림|m3": "c99fd33aad21",
"단산:99|철근가공및조립|type-ii-1,현장가공조립|톤": "e6dc6a25683b",
"단산:113|강관동바리|암거용-3개월사용|공/m3": "55bd7c4f030a",
"단산:112|강관비계|3개월사용|m2": "2390793aeb4b",
"호표:45": "ca2600044054"
}
},
"동부청영월": {
"S1": {
"해시": "5dd9ce597d5d",
"꼴": "STmate",
"호표": 23,
"줄": 138,
"블록": 69,
"갈래": {
"기계": 1,
"노임": 3,
"단가산출": 90,
"별도계상": 18,
"자재": 23,
"호표": 3
},
"참조": {
"참조줄": 3,
"부르는": 3,
"불리는": 2,
"깊이": {
"1": 20,
"2": 2,
"3": 1
},
"순환": 0
}
},
"S2": {
"호표:1": "7cf2b5da2c8d",
"단산:2058|무근콘크리트타설|인력운반타설|m3": "b661921b3a81",
"단산:56|합판거푸집(6회)|간단|m2": "6275e4b9a5fd",
"단산:2|구조물터파기(인력)|경질토사|m3": "706e398f653a",
"단산:7|구조물되메우기|(인력)|m3": "daf607a62e39",
"단산:9|잔토처리|인력100%|m3": "aef7e1b84e30",
"줄:크레인(타이어)|10톤|시간": "96a1f07e863b",
"호표:3": "6c59b8689986",
"단산:3|구조물터파기|토사(b/h90%+인력10%)|m3": "acefa16cc78a",
"단산:10|잔토처리|토사(b/h100%)|m3": "c425e33125c6",
"단산:37|콘크리트타설|임도품셈5-5|m3": "65c3e427122f",
"단산:59|유로폼설치,해체(복잡)|수직고7.0m이하|m2": "6b60ee0cf532",
"단산:60|철근가공조립(현장)|typei|톤": "d1c00f23be62",
"단산:600|기초잡석채집및운반부설|4.5톤(l=500)|m3": "91acb80ac602",
"단산:1926|임도안전표지판설치|삼각,d=900|개소": "de7833d5056a",
"단산:411|야면석채집운반|(25x25x35cm)장비|m2": "aeaa86e728d4",
"단산:644|야면석찰붙임|뒷길이35cm|m2": "6771a23d197b",
"단산:1361|고임돌채집운반|4.5톤덤프(l=500)|m3": "203a9315635b",
"단산:1928|잡석운반및다짐||m3": "3a7b79a11b47",
"단산:1927|면고르기||m2": "3d642227dc7b",
"단산:1942|구조물터파기|인력(경질토사)|m3": "b25a97237575",
"단산:1759|도로반사경설치|d=1000(sts)|개소": "8f206fca6ae6",
"단산:2071|철근콘크리트타설|장비사용타설|m3": "7a4603d42158",
"단산:58|유로폼설치,해체(보통)|수직고7.0m이하|m2": "de90b05e1449",
"단산:1854|지수판설치(pvc용접)|200x5t|m": "3aec0042517c",
"단산:1855|배수관부설및접합(p.v.c)|ø50mm-vg2일반관|m": "7200d398af99",
"단산:1466|야면석소운반(l=35cm)|(l=20m)|m2": "79a4f66ac624",
"단산:450|콘크리트소운반|(l=20m)|m3": "9d06f1876010",
"단산:1843|야면석메붙임|뒷길이35cm|m2": "2e66aa2c2197",
"단산:258|측구수로관부설|500∼700kg미만|m": "56683ae7c5bb",
"단산:1844|콘크리트타설|장비사용타설(소형)|m3": "cdd8f38898be",
"단산:1841|합판거푸집(4회)|보통|m2": "154931aee1b7",
"단산:2001|혼합골재부설|영월-산불|m3": "3c98157c0505",
"단산:1748|콘크리트포장(인력)|t=20cm|m3": "68e42fbf6871",
"단산:1752|콘크리트포장절단공|임도|m": "50eb38496dbf",
"단산:1519|뒷채움돌채집운반부설|4.5톤덤프트럭(l=500m)|m3": "17e1fe46f3c8",
"단산:81|큰돌메쌓기|뒷길이0.6~0.8m|m2": "99f8c85542df",
"단산:1810|큰돌채집운반(뒷길이0.6~0.8m)|장비(80%)+인력(20%)|m2": "8c9b791dab4b"
}
},
"울진대흥": {
"S1": {
"해시": "a2052e0b170c",
"꼴": "STmate",
"호표": 34,
"줄": 171,
"블록": 47,
"갈래": {
"%": 3,
"기계": 11,
"노임": 29,
"단가산출": 69,
"별도계상": 16,
"자재": 15,
"호표": 28
},
"참조": {
"참조줄": 28,
"부르는": 17,
"불리는": 8,
"깊이": {
"1": 17,
"2": 17
},
"순환": 0
}
},
"S2": {
"호표:1": "7cf20aa31fa9",
"호표:2": "cac691e8d485",
"줄:굴삭기+부착용집게(0.6m3)||시간": "68272419c2ca",
"호표:3": "fb9671860caf",
"호표:4": "9b0429327796",
"호표:5": "5e88ce0e4c68",
"호표:6": "31bd5eea19c8",
"단산:75|콘크리트믹서사용|0.45m3|m3": "bb028865ae10",
"호표:7": "3476f12e55bf",
"호표:8": "b4f5087c15f6",
"호표:9": "51afe3b087b1",
"호표:10": "fb1dd2cf4c6a",
"줄:굴삭기(0.7m3)||시간": "9c5792c9e5eb",
"호표:11": "af2de3ed3f1c",
"단산:1124|고임돌채집|기계|m3": "d92fb83f5e54",
"단산:159|뒷채움|150m/m내외|m3": "6c3ab2cbc163",
"단산:959|막자갈채집|기계|m3": "f84d5f824e2d",
"단산:2200|콘크리트비빔|채움재|m3": "91472fd9fe57",
"줄:트럭탑재형크레인|5톤|시간": "71cbed958600",
"호표:17": "ad84568a1c33",
"호표:18": "6f1c4c076fd4",
"단산:1129|야면석채집|l3=45cm내외|m2": "629829a9d347",
"단산:2119|야면석채집|l3=35cm내외|m2": "245dfe68e79f",
"단산:1343|구조물터파기(토사)|굴삭기0.7m3|m3": "aa20475c3d30",
"단산:2139|다지기|굴삭기0.7m3|m3": "e1a21a5a7744",
"단산:2195|유로폼설치및해체|간단|m2": "2ada005f71d9",
"단산:2262|레미콘타설|무근(장비사용타설)|m3": "0011fde861d6",
"호표:28": "fd2ef8bc220b",
"줄:굴삭기(타이어)|0.6m3,(부착용집게,0.7~0.8m3)|시간": "94a580e075dd",
"호표:29": "02e551e5e700",
"호표:30": "92eda2cf44f5",
"단산:2248|콘크리트포장(b-type)|t=20cm|m2": "ad040befe954",
"단산:2251|철근가공및조립|type-ii-2,현장가공조립|톤": "b878bf35ed64",
"단산:2246|유로폼설치및해체|보통|m2": "1985ee7ba47c"
}
},
"영덕계류": {
"S1": {
"해시": "3d03cb61741c",
"꼴": "STmate",
"호표": 9,
"줄": 31,
"블록": 15,
"갈래": {
"%": 3,
"기계": 5,
"노임": 14,
"단가산출": 2,
"자재": 4,
"호표": 3
},
"참조": {
"참조줄": 3,
"부르는": 2,
"불리는": 3,
"깊이": {
"1": 7,
"2": 2
},
"순환": 0
}
},
"S2": {
"줄:굴착기(타이어)|0.6m3,(부착용집게)|시간": "9c24ee2c8c3d",
"호표:5": "b534ae76dc78",
"호표:6": "e3b9e469607e",
"호표:7": "1cd3bb8c5e6d",
"호표:8": "f5f08c081e83",
"단산:150|고임돌채집|기계|m3": "6156ce99f6a7",
"호표:10": "c5b7c43c3ee1",
"단산:1453|인력운반|기타|톤": "355f5eac9936",
"호표:12": "c46a6d7e568f",
"호표:13": "d4bfe9643e53"
}
},
"울진소광": {
"S1": {
"해시": "af7a97c94ed7",
"꼴": "STmate",
"호표": 25,
"줄": 117,
"블록": 40,
"갈래": {
"%": 9,
"기계": 5,
"노임": 17,
"단가산출": 43,
"별도계상": 9,
"자재": 15,
"호표": 19
},
"참조": {
"참조줄": 19,
"부르는": 11,
"불리는": 13,
"깊이": {
"1": 14,
"2": 10,
"3": 1
},
"순환": 0
}
},
"S2": {
"단산:1352|유로폼설치및해체|보통|m2": "2b8ac7770966",
"호표:2": "217ebea3fe00",
"호표:3": "98a36785cfcd",
"호표:4": "a3d428a8fc6a",
"줄:굴삭기+부착용집게(06)||시간": "c7e3b6e6cbea",
"호표:6": "c5ca59b8e3e9",
"호표:7": "e37dd3ced06c",
"줄:트럭탑재형크레인|5톤|시간": "657e83282c02",
"호표:8": "a902f332f29c",
"단산:1351|유로폼설치및해체|간단|m2": "86c99fa46000",
"호표:11": "b3ae0549b0fd",
"호표:12": "39f4044d3634",
"줄:굴삭기(타이어)|0.6m3|시간": "733bdb0730f2",
"호표:13": "af5f52dc7ecb",
"단산:1456|구조물터파기(토사)|굴착기0.6m3|m3": "5369cb770862",
"단산:1474|구조물되메우기|굴착기0.6m3|m3": "c42bf66f9e58",
"단산:1475|구조물잔토처리|굴착기0.6m3|m3": "ea935c401ec0",
"단산:492|채움콘크리트|기계비빔|m3": "76df4f4c944d",
"단산:91|고임돌채집|굴삭기0.7m3|m3": "445470e48368",
"단산:408|막자갈(파쇄석)채집||m3": "6f29521df826",
"단산:867|막자갈채우기|150mm내외|m3": "f77710424c09",
"단산:1336|레미콘타설|무근(장비)|m3": "5ff7079ff5a0",
"단산:918|채움con'c|기계비빔|m3": "0c75dd9a29d2",
"단산:1448|파쇄석채집|l3=35cm내외|m2": "5d1cf0f37342",
"단산:1030|고임돌채집|굴삭기0.2m3|m3": "f483cf51e53b",
"단산:1354|콘크리포장(t=20cm)포설|인력포설|m3": "ec05dd9a542f",
"단산:794|단끊기|절취없음|m": "6aa07c8cba17",
"단산:1329|평떼붙이기||m2": "0522e960d244",
"단산:1129|떼하차비||m2": "5329db32bc8a"
}
}
}
+3 -26
View File
@@ -1,8 +1,7 @@
"""338 골든 도우미 — 실물 엑셀(`resources/knowledge/original/실무문서`)을 지금 읽기로 읽어 정답 표
(`fixtures/m02_hopyo_golden.json`)와 맞대고 지표(설계 7-3)를 셈. 읽기만.
블록 글 = HP 블록에 `글` 이 있으면 그것(나 창 2a 뒤) · 없으면 단가산출 시트 산출근거 열 글(엑셀꼴) ·
거창 = 줄 칸을 이어 붙임. STmate 파일은 읽기(`M02_Hopyo_Excel.read_book`)가 되면 들어감 · 아니면 뺌.
블록 글 = 읽기가 HP 블록에 붙인 `글`(EST) · `자원`(STmate) 그대로.
python resources/tester/helper_m02_hopyo_golden.py → 지표 찍음 · tmp/m02_338/golden.json
"""
@@ -20,7 +19,6 @@ ROOT = Path(__file__).resolve().parents[2]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
from M02_MasterTemplete import M02_Hopyo_Basis as hb # noqa: E402
from M02_MasterTemplete import M02_Hopyo_Excel as hx # noqa: E402
from M02_MasterTemplete import M02_Hopyo_Match as hm # noqa: E402
from M02_MasterTemplete import M02_Hopyo_Verdict as verdict # noqa: E402
@@ -55,35 +53,14 @@ def labels() -> list[dict[str, Any]]:
return json.loads(FIXTURE.read_text(encoding="utf-8"))
def _row_text(row: list, start: int) -> str:
cells = []
for v in row[start:140]:
if v is None or v == "":
continue
cells.append(str(int(v) if isinstance(v, float) and v == int(v) else v).strip())
return " ".join(cells)
@lru_cache(maxsize=8)
def read(tag: str) -> dict[str, Any] | None:
"""파일 → HP 꼴(블록 `글` 붙임) · 못 읽으면 None(STmate 읽기 전)."""
"""파일 → HP 꼴 · 못 읽으면 None."""
path = FILES[tag]
data = path.read_bytes()
try:
book = hx.read_book(path.name, data)
return hx.read_book(path.name, path.read_bytes())
except hx.ExcelError:
return None
if any(b.get("글") or b.get("자원") for b in book.get("단산") or []):
return book
calc = hx._sheets(path.name, data).get(hb.SHEET) or []
_, col, _ = hb._columns(calc)
join = tag == "거창"
for b in book.get("단산") or []:
a, z = b["행"]
rows = calc[a:z]
text = [_row_text(r, col) if join else (r[col] if col < len(r) else "") for r in rows]
b["글"] = [str(t).rstrip() for t in text if isinstance(t, str) and t.strip()]
return book
def _keys(line: dict[str, Any]) -> list[tuple]:
+187
View File
@@ -0,0 +1,187 @@
"""621 단계 고정(776-1) — 여덟 실물 엑셀의 단계 출력을 해시로 굳힘. 읽기만 · 서버 없음.
S1 = 정규화 출력(HP · 뒤 Book) 공통 칸 투영 해시 + 호표 · 줄 · 블록 수 · 줄 갈래 · 호표 참조 그래프.
투영 = 지금 HP 칸만(KEEP) — Book 에 칸을 더해도 그대로 · 빼거나 이름을 바꾸면 깨짐.
S2 = 셈 단위(`Fit.units`)마다 지문(`Finger.excel`) 해시.
옮기는 커밋마다 그대로여야 함 — 깨지면 그 커밋을 되돌림. 일부러 바꾼 걸음만 새 판으로 굳힘:
python resources/tester/test_621_hopyo_stages.py --write
"""
from __future__ import annotations
import hashlib
import json
import sys
import warnings
from collections import Counter
from functools import lru_cache
from pathlib import Path
from typing import Any
import pytest
ROOT = Path(__file__).resolve().parents[2]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
from M02_MasterTemplete import M02_Hopyo_Excel as hx # noqa: E402
from M02_MasterTemplete import M02_Hopyo_Finger as finger # noqa: E402
from M02_MasterTemplete import M02_Hopyo_Fit as fit # noqa: E402
from resources.tester.helper_m02_hopyo_golden import FILES # noqa: E402
warnings.filterwarnings("ignore", category=UserWarning, module="openpyxl")
FIXTURE = Path(__file__).resolve().parent / "fixtures/m02_hopyo_stages.json"
# 지금 HP 칸 — 층마다 이 칸만 해시에 넣음(새 칸은 투영 밖)
KEEP = {
"책": ("꼴", "시트", "호표", "단산", "검산"),
"호표": ("번", "코드", "이름", "규격", "단위", "금액", "행", "근거글", "줄"),
"줄": (
"차례",
"이름",
"규격",
"수량",
"단위",
"단가",
"금액",
"비고",
"코드",
"행",
"갈래",
"참조",
"근거",
"이음",
),
"참조": ("종류", "번", "코드"),
"근거": ("출처", "직종", "행"),
"단산": ("번", "코드", "이름", "규격", "단위", "행", "출처", "직종", "글", "자원"),
"자원": ("갈래", "코드", "이름", "규격", "품", "절"),
}
def _h(x: Any) -> str:
raw = json.dumps(x, ensure_ascii=False, sort_keys=True, default=str)
return hashlib.sha256(raw.encode()).hexdigest()[:12]
def _pick(d: dict, level: str) -> dict:
return {k: d[k] for k in KEEP[level] if k in d}
def project(hp: dict[str, Any]) -> dict[str, Any]:
"""HP · Book → 지금 칸만."""
out = _pick(hp, "책")
hopyo = []
for h in hp["호표"]:
row = _pick(h, "호표")
lines = []
for line in h["줄"]:
got = _pick(line, "줄")
for k in ("참조", "근거"):
if isinstance(got.get(k), dict):
got[k] = _pick(got[k], k)
lines.append(got)
row["줄"] = lines
hopyo.append(row)
out["호표"] = hopyo
blocks = []
for b in hp.get("단산") or []:
got = _pick(b, "단산")
if got.get("자원"):
got["자원"] = [_pick(r, "자원") for r in got["자원"]]
blocks.append(got)
out["단산"] = blocks
return out
def ref_graph(hp: dict[str, Any]) -> dict[str, Any]:
"""호표 참조(일위) 줄 · 부르는 · 불리는 호표 · 깊이 분포(1 = 참조 없음) · 순환."""
kids: dict[int, list[int]] = {int(h["번"]): [] for h in hp["호표"]}
for h in hp["호표"]:
for line in h["줄"]:
ref = line.get("참조") or {}
if ref.get("종류") == "일위":
kids[int(h["번"])].append(int(ref["번"]))
depth: dict[int, int] = {}
loops = 0
def walk(no: int, path: tuple[int, ...]) -> int:
nonlocal loops
if no in path:
loops += 1
return 0
if no not in depth:
depth[no] = 1 + max((walk(k, (*path, no)) for k in kids.get(no, [])), default=0)
return depth[no]
for no in kids:
walk(no, ())
return {
"참조줄": sum(len(v) for v in kids.values()),
"부르는": sum(1 for v in kids.values() if v),
"불리는": len({k for v in kids.values() for k in v}),
"깊이": dict(sorted(Counter(str(d) for d in depth.values()).items())),
"순환": loops,
}
def s1(hp: dict[str, Any]) -> dict[str, Any]:
lines = [line for h in hp["호표"] for line in h["줄"]]
return {
"해시": _h(project(hp)),
"꼴": hp["꼴"],
"호표": len(hp["호표"]),
"줄": len(lines),
"블록": len(hp.get("단산") or []),
"갈래": dict(sorted(Counter(str(x.get("갈래")) for x in lines).items())),
"참조": ref_graph(hp),
}
def s2(hp: dict[str, Any]) -> dict[str, str]:
out = {}
for key, block, line in fit.units(hp):
fp = block["_지문"] if block and "_지문" in block else finger.excel(block, line)
out[key] = _h(fp)
return out
@lru_cache(maxsize=1)
def measure() -> dict[str, Any]:
out = {}
for tag, path in FILES.items():
hp = hx.read_book(path.name, path.read_bytes())
one = s1(hp) # S2 가 블록에 덧칸을 달 수 있어 S1 먼저
out[tag] = {"S1": one, "S2": s2(hp)}
return out
def _want() -> dict[str, Any]:
return json.loads(FIXTURE.read_text(encoding="utf-8"))
@pytest.mark.parametrize("tag", list(FILES))
def test_S1_정규화_출력_그대로(tag):
assert measure()[tag]["S1"] == _want()[tag]["S1"]
@pytest.mark.parametrize("tag", list(FILES))
def test_S2_셈단위_지문_그대로(tag):
got, want = measure()[tag]["S2"], _want()[tag]["S2"]
diff = sorted(k for k in set(got) | set(want) if got.get(k) != want.get(k))
assert not diff, f"{tag} 지문 달라진 셈 단위 {len(diff)}: {diff[:10]}"
def test_여덟_파일_합():
got = measure()
assert sum(v["S1"]["호표"] for v in got.values()) == 245
assert sum(v["S1"]["줄"] for v in got.values()) == 1350
assert sum(len(v["S2"]) for v in got.values()) == 275
assert sum(v["S1"]["참조"]["참조줄"] for v in got.values()) == 90
assert max(int(d) for v in got.values() for d in v["S1"]["참조"]["깊이"]) == 3
assert all(v["S1"]["참조"]["순환"] == 0 for v in got.values())
if __name__ == "__main__" and "--write" in sys.argv:
FIXTURE.write_text(json.dumps(measure(), ensure_ascii=False, indent=1) + "\n", encoding="utf-8")
print("굳힘", FIXTURE)