Merge remote-tracking branches 'origin/main_desktop_1', 'origin/sub_desktop_1', 'origin/sub_laptop_1', 'origin/sub_laptop_2', 'origin/sub_laptop_3' and 'origin/sub_laptop_4' into main_laptop_1

This commit is contained in:
27 changed files with 35982 additions and 30861 deletions
@@ -0,0 +1,461 @@
"""재료_자재품목.json 의 구분 · 상세구분 두 칸 채움 — 품명 글자 규칙(위에서 아래로 첫 일치) · 다시 돌리면 같은 결과.
· 조달청 줄(출처 「자재품목 조달청 …」) = 조달청 품명 표(조달청_분류) 를 따름
· 못 가린 줄 = 구분 「미분류」 · 상세구분 null
· 값 · 이름 · 키는 건드리지 않음 (줄 텍스트의 두 칸만 바꿈 · 나머지 글자 그대로)
· 같이 씀: ref/_묶음_자재품목.md (구분별 상세구분 · 줄 수) 를 새로 씀
사용: python scripts/build_자재품목_분류.py (JSON · 묶음 md 갱신)
python scripts/build_자재품목_분류.py --미분류 (못 가린 품명 앞쪽 나열)
"""
import collections
import json
import re
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
SRC = ROOT / "재료_자재품목.json"
OUT_MD = ROOT / "ref" / "_묶음_자재품목.md"
# 조달청 품명 → (구분, 상세구분)
조달청_분류 = {
"조경용수목": ("식생·종자", "수목"),
"수목보호용지지대": ("식생·종자", "수목지주·보호"),
"부숙유기질비료": ("식생·종자", "비료"),
"일반용폴리에틸렌관": ("관·이음관", "PE관"),
"수도용폴리에틸렌관": ("관·이음관", "PE관"),
"도로안전표지판지주": ("안전시설", "표지판·지주"),
"철제가드레일": ("안전시설", "가드레일"),
"낙석방지책": ("안전시설", "낙석방지책"),
"금속돌망태": ("매트·토목섬유", "돌망태"),
"식생매트": ("매트·토목섬유", "식생매트"),
"잔디보호매트": ("매트·토목섬유", "잔디보호매트"),
"토목용부직포": ("매트·토목섬유", "부직포"),
"자연석경계석": ("석재", "경계석"),
"투수골재포장재": ("아스콘·포장", "투수포장재"),
"목재덱": ("목재", "목재덱"),
"목재판재": ("목재", "판재"),
}
# (구분, 상세구분, 정규식) — 이름에서 공백을 모두 뺀 글자에 대고 위에서부터 첫 일치 · 좁은 규칙이 위
R = [
# 지반조사 도구
("기계·설비", "기계류", r"드라이브파이프|코아리프터|샘플러|신월튜브|코아바렐|리밍쉘"),
# 운송 · 시험 · 폐기물
("운송·용역", "화물운임", r"구역화물|폐기물운반|항만하역|운반비|운송비"),
("운송·용역", "폐기물", r"폐기물처리"),
("운송·용역", "시험·검사", r"시험(?!기|편)|출장비"),
(
"기계·설비",
"측정·시험기기",
r"시험기|시험편|탐상|측정기|계측기|측량기|판독기|분석기|필림|세륜기|축중기|레벨기|트랜싯",
),
# 레미콘 · 아스콘
("레미콘", "레미콘", r"레미콘|레디믹스"),
("아스콘·포장", "아스콘", r"아스콘|순환아스"),
("도료·방수·접착", "방수·실링", r"아스팔트(조인트|실란트|프라이머|루핑|펠트|콤파운드|방수)"),
("아스콘·포장", "아스팔트", r"^(블론|개질유화|유화)?아스팔트(유제)?$|록크하드"),
(
"아스콘·포장",
"포장블록",
r"잔디블록|바닥벽돌|투수콘|투수블[록럭]|고압블록|규사블록|다색(저속|블록)|점자블록|인터로킹|보도블[록럭]|차도블록",
),
# 밸브 · 수도
(
"밸브·수도자재",
"밸브",
r"밸브|제수|체크|게이트|감압|스트레이너|수전|분수전|공기변|수문|콕크",
),
("밸브·수도자재", "수도계량기·유량계", r"수도미터|계량기|유량계|미터기|보호통"),
("밸브·수도자재", "스프링클러·호스", r"스프링클러|살수|급수호스|송수호스|호스|분사|헤드|점적"),
("밸브·수도자재", "펌프", r"펌프|양수기"),
("도료·방수·접착", "방수·실링", r"백업재"),
("관·이음관", "단열이중관", r"단열이중"),
("기계·설비", "보일러·공조", r"정화조"),
# 거푸집 (문양거푸집 등 — 단열 글자보다 먼저)
(
"거푸집·가설재",
"거푸집",
r"거푸집|유로폼|알폼|코팅합판|폼타이|플랫타이|분리제|박리제|폼핀|핀폴|패널(?!스위치)|내부코너",
),
# 단열 · 보온
(
"단열·보온",
"단열재",
r"단열(?!문)|보온|샌드위치|폴리스티렌|스티로폼|암면|글라스울|압출법|페놀폼|은박지",
),
# 동바리·비계 (철물·브라켓 글자보다 먼저)
(
"거푸집·가설재",
"동바리·비계",
r"동바리|비계|낙하물|가설|가시설|버팀|흙막이|스트럿|안전발판|건설발판|발판|사다리|가림막|가세틀|바퀴\(|조립식틀|시스템동바리|스카폴드",
),
# 볼트 · 앵커 · 철물 중 관·형강과 겹치는 것을 먼저
("볼트·앵커·철물", "철근커플러", r"철근커플러|커플러|정착구"),
("볼트·앵커·철물", "앵커", r"앵커|앙카|케미칼|카트리지|인서트"),
(
"볼트·앵커·철물",
"볼트·너트",
r"볼트|볼트너트|못트|너트|와셔|스크류|스크루|리벳|스터드|턴버클|고정핀|앵커핀|나사(?!식)|철나사|직결",
),
("볼트·앵커·철물", "", r"철못|타정|콘크리트못|STS못|벨가못|꺾쇠|표시못"),
(
"볼트·앵커·철물",
"와이어로프·클램프",
r"와이어로프|P\.P로프|클램프|훅클램프|행거|철물|지지금구|연결금구|브라켓|샤클|피뢰침",
),
("전기·통신", "전선관·트레이", r"전선관"),
# 도료 · 방수 · 접착 (좁은 것)
(
"도료·방수·접착",
"방수·실링",
r"방수|실런트|실란트|실링|코킹|지수재|씰$|백업재|충진|퍼티|벤토나이트(?!매트)|방식테이프|줄눈재|물끊기",
),
(
"도료·방수·접착",
"도료",
r"페인트(?!붓)|프라이머|신너|시너|희석제|도료|코팅제|래커|에나멜|방청|광명단|징크|안료|염료|표면처리제|표면강화제|피막양생|양생제|나노큐브|하도|상도",
),
(
"도료·방수·접착",
"접착·혼화",
r"접착|본드|(?<!합성)수지(?!\))|레진|혼화|감수제|AE제|지연제|경화제|바인더|라텍스|폴리탑|메도킹|고무패킹|패킹|가스켓",
),
# 식생 · 종자
("식생·종자", "종자", r"종자|산림종묘|배합종자"),
(
"식생·종자",
"비료·토양개량",
r"비료|생육|영양제|침식안정제|토양개량|피트모스|펄라이트|유기질|인공토양|상토|배양토|퇴비",
),
("식생·종자", "농약·약제", r"살충제|살균제|제초제|농약|훈증약제|방제약|기피제"),
(
"식생·종자",
"수목지주·보호",
r"수목지주|지주대|수목보호|수간주입|수목표찰|수목영양|나무보호|수목받침|수목고정",
),
(
"식생·종자",
"잔디·지피",
r"잔디|줄떼|평떼|지피|초화|비비추|맥문동|원추리|꽃양귀비|수레국화|패랭이|국화|억새|갈대|송엽국|들꽃|야생화|구근",
),
(
"식생·종자",
"수목",
r"나무(\(|,|$)|나무$|소나무|곰솔|해송|주목|금송|낙우송|메타세쿼이아|측백|향나무|철쭉|산홍|산수유|목련|단풍|벚|동백|무궁화|회양목|배롱|은행|느티|팽나무|회화|서어|물푸레|이팝|층층|칠엽수|계수|산딸|먼나무|모과|감나무|대추|마가목|복자기|산사|살구|사철|쥐똥|꽝꽝|남천|피라칸|조팝|개나리|장미|수국|라일락|수수꽃다리|박태기|낙상홍|화살|병꽃|불두화|히어리|야광|다정큼|당종려|종려|소철|대나무|조릿대|등나무|능소화|담쟁이|송악|마삭|인동|덩굴|아이비|칠자화|왕버들|수양|능수|진달래|백송|과꽃|옥매화|꽃양배추|화백|버들|유카|율마|허브|채송|봉선|일일초|안개초|꽃무릇|영춘화|양매자|식물재배화단|철쭉류|사스레|황매|명자|백량금|협죽도|만리화|말발도리|골담초|아로니아|오죽|올괴불|황근|풍년화|태산목|용버들|옥향|담팔수|무화과|나한송|구상나무|편백|눈향|눈주목|비자|가시|후박|녹나무|후피향|아왜|굴거리|팔손이|목서|치자|서향|광나무|보리수|버드|갯버들|포플러|사시|아까시|싸리|자귀|주엽|쉬나무|개오동|때죽|쪽동백|고로쇠|당단풍|산벚|왕벚|겹벚|자엽|해당화|꽃사과|꽃복숭|모감주|황칠|노각|밤|호두|가래|음나무|두릅|오동|참죽|옻|붉나무|말채|백당|덜꿩|병아리|고욤|헛개|꾸지|뽕|닥|미루|사과|배$|돌배|앵두|자두|석류|유자|귤|감귤|매실|살구|블루엔젤|에메랄드|쏠라데리|사피니아|라벤다|아스타|제라늄|버베나|빈카|범부채|아게라|익소라|에키네시아|카네이션|실유카|채진목|숙근|웨이브페츄니아|나리|매미꽃|금낭화|땅채송화|사초|붓꽃|할미꽃|돌나물|돌장미|초롱|프리뮬라|팬지|비올라|페튜니아|메리골드|샐비어|맨드라미|코스모스|해바라기|수선|튤립|히아신스|크로커스|백합|창포|부들|연꽃|수련|물칸나|칸나|달리아|글라디올러스|아이리스|맥문|옥잠|가는잎|수크령|갈풀|띠|톱풀|꿩의비름|세덤|기린초|돌단풍|둥굴레|은방울|모란|작약|우산|양지꽃|뚝갈|벌개미취|구절초|쑥부쟁이|층꽃|꽃댕강|댕강|황금|미선|서부|미국|서양|중국|무늬|은사초|그린톱|그라스|핑크뮬리|팜파스|수생|정수식물",
),
("식생·종자", "수목", r"^H\d{1,2}(\.\d+)?[×xX*,]\s*[RBWrbw]\d"),
# 시멘트 · 골재
(
"시멘트·골재",
"시멘트",
r"^(백|초속경|초조강|무수축|포틀랜드)?시멘트(?!벽돌|블록|라이닝|몰탈|모르타르|판|기와)",
),
("시멘트·골재", "모르타르", r"모르타르|몰탈|그라우트|무수축"),
(
"시멘트·골재",
"골재",
r"골재|쇄석|^모래|석분|자갈|잡석|부순돌|마사토|호박돌|성토재|사질토|규사",
),
# 안전시설 (관·형강보다 먼저 — 휀스 · 지주)
("안전시설", "가드레일", r"가드레일|방호울타리|방호책|충격흡수|완충시설|방호벽"),
(
"안전시설",
"표지판·지주",
r"표지판|표지병|안전표지|도로표지|안내판|표찰|반사경|반사지|시선유도|경계표주|알림표지|지주(?!대)",
),
(
"안전시설",
"낙석방지·휀스",
r"낙석방지|방책|방음|볼라드|휀스|펜스|난간|라바콘|안전화|안전모|안전대|안전망|추락방지|방지턱|안전띠|바리케이",
),
# 거푸집 · 가설재
# 목재
("목재", "합판", r"합판|MDF|OSB|파티클|섬유판|텍스|석고보드"),
(
"목재",
"각재·판재",
r"각재|판재|미송|외송|방부|원목|목재|우드칩|통나무|낙엽송|구조목|멀바우|데크(?!플레이트|패널|고정)|삼나무|편백(?!나무)|경계목|면목|침목",
),
# 매트 · 토목섬유
("매트·토목섬유", "돌망태·사방", r"돌망태|개비온|망태석|사석|녹생토|녹화|사방|이불형"),
(
"매트·토목섬유",
"부직포·토목섬유",
r"부직포|지오텍스|지오그리드|지오콤|지오셀|토목섬유|차수시트|드레인보드|배수판|투수시트|섬유지|네트|오탁방지|보호망|그물",
),
("매트·토목섬유", "매트", r"매트|멀칭|코이어|방초|고화매트|벤토나이트매트"),
# 석재
("석재", "경계석", r"경계석|도로경계|측석"),
(
"석재",
"조경석·자연석",
r"조경석|자연석|호박석|^산석|경관석|판석|사고석|야면석|막돌|다듬돌|석재|화강석|대리석|현무암|사암|점판암",
),
# 콘크리트제품
(
"콘크리트제품",
"말뚝·파일",
r"콘크리트말뚝|PC말뚝|PHC|콘크리트파일|복합말뚝|강관파일|강관말뚝|쉬트파일|시트파일|강널말뚝|팽이파일|측량말뚝|전기저항용접강관파일",
),
("콘크리트제품", "PC구조물", r"조립식PC|프리캐스트|PC맨홀|PC박스|PC개량|PC우수"),
(
"콘크리트제품",
"경계석·블록",
r"콘크리트경계|보차도경계|경계블록|시멘트벽돌|시멘트블록|보강토|GS|옹벽블록|호안블록|생태호안|식생호안|블록|벽돌",
),
# 맨홀 · 수로 · 그레이팅
("맨홀·수로·그레이팅", "그레이팅", r"그레이팅|트렌치|격자"),
(
"맨홀·수로·그레이팅",
"맨홀",
r"뚜껑|맨홀|인상링|우수받이|집수정|측구뚜껑|주철뚜껑|집수구|물받이(?!후)|침투측구|받이",
),
(
"맨홀·수로·그레이팅",
"수로관",
r"수로|플륨|암거|개거|배수로|측구|벤치플|[UL]형(측구|수로|플륨|배수)",
),
# 관 — 재질별
("잡재료", "소모품", r"PVC(용접봉|코팅|시트|바닥|선|천막)"),
("관·이음관", "흄관", r""),
("관·이음관", "파형강관", r"파형강관|파형관|커플링밴드|수밀벨트"),
(
"관·이음관",
"PVC관",
r"(?<![A-Za-z])(PVC|P\.V\.C|VR|VG|XL|PB|CPVC|PRC)|내충격|염화비닐|고강성|레진관",
),
(
"관·이음관",
"PE관",
r"^PE|폴리에틸렌|HDPE|(?<![A-Za-z])PE|HI-?(PER|GP|3R|3P|VP)|이중벽|다중벽|삼중벽|유공관|하수관|우수관|융착",
),
(
"관·이음관",
"주철관",
r"주철|(?<![A-Za-z])(KP|PM|HP)|덕타일|닥타일|타이튼|메카니칼|압륜|이탈방지|접합부속",
),
(
"관·이음관",
"유리섬유·FRP관",
r"FRP(파이프|덕트|관|이음|곡관)|GFRP(관|파이프)|유리섬유(곡관|복합관|관|파이프|덕트)|섬유복합관",
),
("관·이음관", "동관·스텐관", r"동관|스텐관|스테인리스관|STS관|SUS관|STS(파이프|배관)"),
(
"관·이음관",
"강관",
r"강관|배관용|압력배관|흑관|백관|가스관|피복강관|도복장|에폭시강관|쉬즈관|비굴착|관세척|나팔관|스틸파이프|STEELPIPE|파이프",
),
(
"관·이음관",
"이음쇠·부속",
r"STS(티|캡|엘보|레듀)|이경|티이|엘보|니플|레듀[샤셔]|유니온|소켓|플랜지|곡관|편락|엔드캡|엔드플러그|이형관|이음쇠|이음관|분기관|새들|플럭|마개|선단캡|노말밴드|˚밴드|연결구|주름관|플렉시블|플렉스",
),
# 철근 · 강재
("철근·강재", "철근", r"철근"),
("철근·강재", "철망·와이어메쉬", r"와이어메[쉬시]|용접철망|^철망|크림프철망|메쉬"),
("철근·강재", "철선", r"철선|결속선|어닐링|아연도철|PC강선|강선"),
(
"철근·강재",
"형강",
r"[Hh]형강|[ㄱㄷ]형강|^[ICT]형강|C형강|^(STS)?앵글$|평강|평철|각철|4각강|환봉|봉강|철평석|강봉|H빔|[Hh]-?빔",
),
(
"철근·강재",
"강판",
r"(?<!보)강판|후판|동판|열연|냉연|철판|스틸플레이트|데크플레이트|기초플레이트|복공판|탄소주강",
),
("철근·강재", "구조용강관", r"구조용|각관|반경철관|철관"),
# 전기 · 통신
("전기·통신", "케이블·전선", r"케이블|CABLE|^전선|전선$|HFIX|HIV|접지|절연|배선(?!용)"),
("전기·통신", "전선관·트레이", r"전선관|TRAY|트레이|풀박스|분전함|박스"),
(
"전기·통신",
"판넬·차단기",
r"차단기|접촉기|릴레이|분전반|제어기|제어반|스위치|콘센트|PLC|타이머|인버터|변압기|SK판넬|TS판넬|퓨즈|전원용",
),
(
"전기·통신",
"조명·전등",
r"LED|조명|램프|등기구|투광|보안등|안정기|경광등|경고등|유도등|신호등|가로등|등주",
),
# 기계 · 설비
(
"기계·설비",
"보일러·공조",
r"보일러|공조|에어컨|환풍|송풍|히터|화장로|버너|탱크|물탱크|저류조|열교환|팬틸트|정화조",
),
(
"잡재료",
"용접·가스",
r"프로판가스|탄산가스|프레온가스|아세틸렌|^(용접|산소|알곤|아르곤)|용접봉",
),
("기계·설비", "소방·가스", r"소방|소화(?!화)|소화전|가스믹서|가스버너|경보|감지기|방화"),
(
"기계·설비",
"기계류",
r"유압잭|잭써포트|모터|전동기|기어|감속기|베어링|해머|브레이커|톱|드릴|비트|블레이드|코아|롤러|다짐|믹서|용접기|절단기|발전기|콤프레샤|컴프레샤|비트|리밍쉘|스텝|공구",
),
(
"기계·설비",
"놀이·운동·시설물",
r"놀이|운동기구|의자|벤치|파고라|퍼걸라|쓰레기통|분리수거함|음수대|조형물|정자",
),
# 건축마감
("건축마감", "타일·기와", r"타일|테라조|기와|세라믹|도기"),
(
"건축마감",
"문·창·유리",
r"도어|(?<![가-힣])문|문(?=\(|,|$)|문틀|문비|창호|(?<!섬)유리|샤시|경첩|손잡이|자물쇠|힌지|셔터|블라인드",
),
(
"건축마감",
"천장·바닥·마감재",
r"천장|천정|흡음|석고|후레싱|후레임|칸막이|바닥재|카페트|마감|몰딩|걸레받이|조이너|루버|점검구|코너비드|논슬립|계단",
),
("건축마감", "위생기구", r"대변기|세면기|소변기|양변기|욕조|싱크"),
# 교량 · 구조 부속 · 특수포장 · 기타 좁은 규칙
(
"교량·구조부속",
"신축이음·받침",
r"신축이음|교량받침|익스펜션조인트|익스팬션조인트|EJ|내진탄성받침|교량배수|받침",
),
(
"교량·구조부속",
"보강재·정착",
r"PC강연선|GFRP보강근|DB정착바|강섬유|다웰바|스페이셔|SOC-?BAR|간격재|장선연결|콘크리트근가|SRR보강재",
),
("아스콘·포장", "특수포장", r"소일콘포장|탄성포장|황토포장|미끄럼방지포장|탑쏘일|포장재"),
("거푸집·가설재", "가설건축물", r"콘테이너|컨테이너"),
("매트·토목섬유", "부직포·토목섬유", r"그리드|구갑망|메탈라스|스텐리스망|식생토낭"),
(
"전기·통신",
"부속·기타",
r"슬리브|부스바|애자|수평도체|터미날|CONNECTOR|백열|형광등|보조등|철주|콘크리트주|보행등|접속관",
),
(
"안전시설",
"차선·방호",
r"차선규제|계선주|단부레일|라운드레일|DS연결대|DS차광망|핸드레일촉지판",
),
(
"관·이음관",
"이음쇠·부속",
r"부싱|STS직관|안장티|THP|(?<![A-Za-z])STS(플러그|캡)|SUS밴드|연결관|알루미늄관|후렉시블|유공도수관|다발관|조립식배수구|에어밴트|수도꼭지",
),
("볼트·앵커·철물", "와이어로프·클램프", r"로프|가마니|클립|타카핀|착지핀|칼블럭"),
(
"도료·방수·접착",
"도료",
r"락카|왁스|바탕조정제|주입용에폭시|주입재|씰링재|구체강화제|타르페이퍼|봉함재",
),
("잡재료", "화약", r"폭약|뇌관"),
("잡재료", "용수", r"^물\(상수도"),
("잡재료", "고철·기타금속", r"고철|봉땜납"),
("잡재료", "용접·가스", r"아세톤"),
("철근·강재", "형강", r"트러스|중도리"),
("건축마감", "천장·바닥·마감재", r"T-?BAR"),
("거푸집·가설재", "동바리·비계", r"수평재|수직재"),
("도료·방수·접착", "접착·혼화", r"기포제"),
("잡재료", "소모품", r"^풀\(|보양지|소창"),
# 잡재료
("잡재료", "용접·가스", r"\(날\)|알곤용접봉"),
(
"잡재료",
"소모품",
r"장갑|걸레|비닐|페인트붓|^붓$|사포|브러[시쉬]|톱날|연마|숫돌|기름|오일|윤활|그리스|목탄|비누|현수막|끈|밧줄|포대|마대|볏짚|양생포|필름|테이프|와이어",
),
]
RX = [(a, b, re.compile(c, re.I)) for a, b, c in R]
def 분류(이름: str, 출처: str):
n = re.sub(r"\s+", "", 이름 or "")
if 출처.startswith("자재품목 조달청"):
return 조달청_분류.get(n, ("미분류", None))
if not n:
return ("미분류", None)
for a, b, rx in RX:
if rx.search(n):
return (a, b)
return ("미분류", None)
LINE = re.compile(r'("구분": )(?:null|"[^"]*")(, "상세구분": )(?:null|"[^"]*")')
NAME = re.compile(r'"이름": "((?:[^"\\]|\\.)*)"')
SRC_ = re.compile(r'"출처": "((?:[^"\\]|\\.)*)"')
NOTE_OLD = "구분 · 상세구분 = 원문에 대·중분류 칸이 없어 비움(지어내지 않음)"
NOTE_NEW = "구분 · 상세구분 = 품명 글자 규칙으로 채움(scripts/build_자재품목_분류.py · 묶음 목록 ref/_묶음_자재품목.md) · 조달청 줄은 조달청 품명을 따름 · 못 가린 줄은 「미분류」"
def q(s):
return "null" if s is None else json.dumps(s, ensure_ascii=False)
def main():
text = SRC.read_text(encoding="utf-8")
rows = json.loads(text)[""]
out, res = [], []
for ln in text.split("\n"):
m = LINE.search(ln)
if m and '"": "MT' in ln:
nm = json.loads('"' + NAME.search(ln).group(1) + '"')
sc = json.loads('"' + SRC_.search(ln).group(1) + '"')
a, b = 분류(nm, sc)
ln = LINE.sub(lambda _: f'"구분": {q(a)}, "상세구분": {q(b)}', ln, count=1)
res.append((a, b, nm))
out.append(ln.replace(NOTE_OLD, NOTE_NEW))
assert len(res) == len(rows), (len(res), len(rows))
return "\n".join(out), res
def 묶음_md(res):
by = collections.defaultdict(collections.Counter)
for a, b, _ in res:
by[a][b or ""] += 1
ks = sorted((k for k in by if k != "미분류"), key=lambda k: -sum(by[k].values()))
ndet = sum(len(by[k]) for k in ks)
L = [
"# 자재품목 묶음 (구분 · 상세구분)",
"",
"`scripts/build_자재품목_분류.py` 가 만든 표 — 손으로 고치지 않음.",
"",
f"- 구분 {len(ks)} · 상세구분 {ndet} · 미분류 {sum(by['미분류'].values())} 줄 (전체 {len(res)} 줄)",
"",
"## 규칙",
"",
"- 품명에서 공백을 모두 뺀 글자에 정규식을 위에서부터 대어 첫 일치를 따름 — 순서가 곧 우선순위.",
"- 조달청 줄(출처 「자재품목 조달청 …」)은 조달청 품명 16 개 표를 따름.",
"- 못 가린 줄은 구분 「미분류」 · 상세구분 없음.",
"- 값 · 이름 · 키는 바꾸지 않음(두 칸만).",
"- 다시 돌리면 같은 결과.",
"",
"## 구분별 상세구분 (줄 수)",
"",
]
for k in ks:
L.append(f"### {k} ({sum(by[k].values())})")
L.append("")
L.append(" · ".join(f"{d} {c}" for d, c in by[k].most_common()))
L.append("")
return "\n".join(L)
if __name__ == "__main__":
new, res = main()
if "--미분류" in sys.argv:
c = collections.Counter(re.sub(r"\s+", "", n) for a, b, n in res if a == "미분류")
print(sum(c.values()), len(c))
print(
" ".join(
f"{k}:{v}"
for k, v in c.most_common(int(sys.argv[-1]) if sys.argv[-1].isdigit() else 300)
)
)
sys.exit()
SRC.write_text(new, encoding="utf-8", newline="\n")
OUT_MD.write_text(묶음_md(res), encoding="utf-8", newline="\n")
print("구분·상세구분 채움", len(res), "")
@@ -6,7 +6,7 @@
(1) 틀 — 파일 이름 · 머리 칸 · 줄 칸 · 출처 모양 · 키 모양·겹침 · 대장
(2) 본문 — 표형 요소의 출처 절을 본문 md 에서 찾아 수를 맞댐 · 결손(본문 표에 있고 요소에 없음)·허구(요소에 있고 본문 절에 없음)
(3) 로직 — 변수가 가리키는 요소·표·칸 · 찾기 조건 이름 · 로직 입력 · 돌고 도는 참조 · 끊긴 키
(3) 로직 — 변수가 가리키는 요소·표·칸 · 찾기 조건 이름 · 로직 입력 · 재료 고르기 조건 · 돌고 도는 참조 · 끊긴 키
(4) 계산 — 로직 키와 입력값으로 호표 줄별 수량·단가·금액 · 비목 합 (`master_formula.py`)
수 뽑기는 `resources/knowledge/original/_pipeline/pum_md_tool.py` 의 것을 씀.
"""
@@ -260,8 +260,10 @@ class Master:
raise FormulaError(f"없는 요소 「{ref}")
return row
def label(self, ref: str) -> str:
"""알림 글 — 키 옆에 이름."""
def label(self, ref) -> str:
"""알림 글 — 키 옆에 이름 · 고르기 조건은 조건 글."""
if isinstance(ref, dict):
return "재료 고르기 " + " · ".join(str(v) for v in ref.values() if v)
try:
row = self.get(ref)
except FormulaError:
@@ -476,7 +478,8 @@ def run(master: Master, ref: str, given: dict, depth: int = 0) -> dict:
else {item["비목"]: qty * price}
)
else:
ref = fill(item["요소"], env)
# 고르기 조건 묶음은 그대로 넘김 — `element` 가 대표 줄·첫 줄로 풂
ref = item["요소"] if isinstance(item["요소"], dict) else fill(item["요소"], env)
try:
price, source = element(master, ref, env)
except FormulaError:
@@ -654,6 +657,11 @@ def check_logic(master: Master, row: dict) -> tuple[list[str], set]:
item.get("종류") != "로직" and "비목" not in item
):
out.append(f"{where} · 비목 「{item.get('비목')}")
if isinstance(item["요소"], dict): # 재료 고르기 조건
if item.get("종류") != "재료":
out.append(f"{where} · 고르기 조건은 종류 「재료」 만")
out += check_pick(master, where + " 요소", item["요소"])
continue
one(item["요소"], where + " 요소", known)
group = mk.GROUP_OF.get(mk.id_of(str(item["요소"])))
if item.get("종류") != "로직" and group != item.get("종류"):
@@ -687,4 +695,14 @@ def find_loops(graph: dict[str, set]) -> list[list[str]]:
# 품셈재료 단가 — 서로 부르는 모듈이라 끝에서 읽음
from master_material import LINKED, REGION, SLOTS, TARIFF, element # noqa: E402, F401
from master_material import ( # noqa: E402, F401
LINKED,
PICK_KEYS,
REGION,
SLOTS,
TARIFF,
candidates,
check_pick,
element,
pick,
)
@@ -16,6 +16,7 @@ LINKED = "MP" # 품셈재료 테이블ID
MARKET = "MT" # 자재품목 테이블ID
PRICED = "MO" # 유가전력 테이블ID — 값 한 칸
SLOTS = ("물가자료", "유통물가", "물가정보", "거래가격", "관급") # 자재품목 값 열
PICK_KEYS = ("구분", "상세구분", "규격", "대표") # 재료 고르기 조건 칸 (`_틀.md` 7장)
REGION = "자재지역" # 로직 입력 — 「지역」 은 유가 고르기가 씀
TARIFF = "전력 계약종별" # 로직 입력 — 품셈·계약예규에 계약종별 규정 없음
_UNITS = {"ea": "", "ton": "", "t": "", "l": "", "리터": "", "set": "", "세트": ""}
@@ -104,8 +105,58 @@ def _candidates(master: mf.Master, ref: str, row: dict, env: dict) -> list[str]:
return [key for _, key in hits]
def element(master: mf.Master, ref: str, env: dict) -> tuple[object, str | None]:
"""요소 값과 출처 — 자재품목은 제 줄, 품셈재료는 연결을 따라가 낮은 값 · 그 밖은 줄의 `값`."""
# ── 재료 고르기 ────────────────────────────────────────────────────────
def candidates(master: mf.Master, cond: dict, env: dict | None = None) -> list[str]:
"""고르기 조건 안 자재품목 키 — 구분 · 상세구분 · 규격 낱말이 모두 든 줄 · 파일 차례대로.
입력 `자재지역` 이 있으면 이름에 그 지역이 든 줄만(그런 줄이 없으면 조건 안 전부)."""
want = [plain_spec(w) for w in str(cond.get("규격") or "").split()]
detail = cond.get("상세구분")
hits = [
key
for key, row in master.index.get(MARKET, {}).items()
if row.get("구분") == cond.get("구분")
and (not detail or row.get("상세구분") == detail)
and all(w in plain_spec(row.get("규격")) for w in want)
]
region = plain((env or {}).get(REGION, ""))
near = [k for k in hits if region in plain(master.get(k).get("이름"))] if region else []
return near or hits
def pick(master: mf.Master, cond: dict, env: dict) -> str:
"""시험 계산이 쓸 줄 — 관리자가 정한 `대표`, 없으면 조건 안 값 있는 첫 줄."""
if cond.get("대표"):
return str(cond["대표"])
keys = candidates(master, cond, env)
keys = [k for k in keys if _offers(master, k)] or keys
if not keys:
raise mf.FormulaError(f"재료 고르기 후보 없음 — {cond}")
return keys[0]
def check_pick(master: mf.Master, where: str, cond: dict) -> list[str]:
"""고르기 조건 검사 — 칸 이름 · 구분·상세구분이 자재품목에 있는지 · 대표 줄 · 후보 0."""
if set(cond) - set(PICK_KEYS) or not cond.get("구분"):
return [f"{where} · 재료 고르기 조건 모양 「{cond}」 — {' · '.join(PICK_KEYS)}"]
out, rows = [], master.index.get(MARKET, {})
for slot in ("구분", "상세구분"):
if cond.get(slot) and not any(r.get(slot) == cond[slot] for r in rows.values()):
out.append(f"{where} · 자재품목에 없는 {slot}{cond[slot]}")
rep = cond.get("대표")
if rep and str(rep) not in rows:
out.append(f"{where} · 대표 줄 「{rep}」 이 자재품목에 없음")
elif rep and str(rep) not in candidates(master, cond):
out.append(f"{where} · 대표 줄 「{rep}」 이 고르기 조건 밖")
if not out and not candidates(master, cond):
out.append(f"{where} · 재료 고르기 후보 0")
return out
def element(master: mf.Master, ref, env: dict) -> tuple[object, str | None]:
"""요소 값과 출처 — 자재품목은 제 줄, 품셈재료는 연결을 따라가 낮은 값 · 그 밖은 줄의 `값`.
`ref` 가 고르기 조건 묶음이면 대표 줄(없으면 조건 안 첫 줄)로 풂."""
if isinstance(ref, dict):
ref = pick(master, ref, env)
row = master.get(ref)
if ref[:2] == MARKET: # 자재품목을 바로 가리키는 호표 줄
return _lowest(master, ref)
@@ -0,0 +1,53 @@
"""산림품셈 로직 복잡도 재기 — 입력 수 · 표 찾기 수 · 로직 부르기 · 덧줄 · 재료 가짓수.
사용: python resources/master_data/scripts/로직_복잡도_재기.py [상위 N]
점수 = 입력 + 찾기 + 로직부르기*2 + 덧줄 + 재료 (읽기 전용 · 아무것도 안 씀)
"""
import json
import re
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
FIND = re.compile(r"찾기\(")
CALL = re.compile(r"로직\(")
def measure(row: dict) -> dict:
ho = row.get("호표") or []
text = " ".join(str(h.get("수량", "")) + str(h.get("요소", "")) for h in ho)
text += " ".join(str(m.get("", "")) for m in row.get("중간") or [])
text += " ".join(str(m.get("", "")) for m in row.get("덧줄") or [])
m = {
"입력": len(row.get("입력") or []),
"찾기": len(FIND.findall(text)),
"로직": len(CALL.findall(text)),
"덧줄": len(row.get("덧줄") or []),
"재료": sum(1 for h in ho if h.get("종류") == "재료"),
}
m["점수"] = m["입력"] + m["찾기"] + m["로직"] * 2 + m["덧줄"] + m["재료"]
return m
def main() -> None:
top = int(sys.argv[1]) if len(sys.argv) > 1 else 25
out = []
for f in sorted(ROOT.glob("로직_산림품셈_*.json")):
for row in json.loads(f.read_text(encoding="utf-8"))[""]:
out.append(
{"": row[""], "번호": row["원문번호"], "이름": row["이름"], **measure(row)}
)
out.sort(key=lambda r: -r["점수"])
for r in out[:top]:
print(
r[""],
r["번호"],
r["이름"],
{k: r[k] for k in ("입력", "찾기", "로직", "덧줄", "재료", "점수")},
)
print("", len(out))
if __name__ == "__main__":
main()