chore(tester): scratch 분석·감사 도구를 resources/tester/scratch 로 이동 및 conftest 등록

This commit is contained in:
2026-09-18 22:29:50 +09:00
parent 09e7dcdfa8
commit 9629430aff
184 changed files with 128469 additions and 0 deletions
+2
View File
@@ -4,3 +4,5 @@ import os
import sys
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
collect_ignore = ["scratch"]
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
@@ -0,0 +1,88 @@
import json
import sys
import re
from pathlib import Path
sys.stdout.reconfigure(encoding='utf-8')
# Load basis_missing_2026-01-01.json
missing_file = Path('resources/data_work_item_master/basis_missing_2026-01-01.json')
with open(missing_file, 'r', encoding='utf-8') as f:
missing_data = json.load(f)
print("basis_missing keys:", list(missing_data.keys()))
missing_records = missing_data.get('items', [])
print(f"Total missing records: {len(missing_records)}")
# Load original MD lines
md_path = Path("resources/knowledge/original/행정규칙/임도 품셈 적용기준 (현 산림사업 표준품셈)/첨부/(산림청고시 제2025-82호) 산림사업 표준품셈.md")
with open(md_path, "r", encoding="utf-8") as f:
md_lines = f.readlines()
# Load enriched tables
sys.path.insert(0, '.')
from scratch.enrich_tables import tables
tables_by_id = {t['table_id']: t for t in tables}
# Check each of the 88 missing tables
# Category 1: Dimensionless / Coefficient / Reference / Pure text guide (원문에 밑수 개념 자체가 없음)
# Category 2: Header / Pre-context contains explicit quantity/unit (원문에 있는데 파서가 못 읽음)
category_no_basis_in_md = []
category_has_basis_in_md = []
basis_pattern = re.compile(r'\[\s*([0-9\.\,]*)\s*([a-zA-Z㎡㎥㏊ha본개공mkm㎏gtonLℓ대인組]+)\s*당\s*\]')
basis_pattern2 = re.compile(r'\(단위\s*:\s*([^\)]+)\)')
for rec in missing_records:
tid = rec.get('pum_table_id')
t = tables_by_id.get(tid)
if not t:
continue
pre_lines = t.get('pre_context', [])
header_line = t.get('lines', [''])[0]
# Check if pre_lines or headers have basis
found_basis = None
for pl in reversed(pre_lines):
m = basis_pattern.search(pl)
if m:
found_basis = pl
break
m2 = basis_pattern2.search(pl)
if m2:
found_basis = pl
break
# Check table form or characteristics
pform = rec.get('pum_form')
if found_basis:
category_has_basis_in_md.append({
'table_id': tid,
'section': rec.get('section'),
'pum_form': pform,
'line': t.get('start_line'),
'evidence': found_basis
})
else:
category_no_basis_in_md.append({
'table_id': tid,
'section': rec.get('section'),
'pum_form': pform,
'line': t.get('start_line'),
'reason': '계수표·할증률표·규격기준표 또는 원문에 단위수량 없음'
})
print(f"\nAnalysis of 88 Missing Basis Tables:")
print(f"1. 원문에 밑수가 분명히 명시되어 있으나 파서가 아직 못 읽은 것: {len(category_has_basis_in_md)}")
print(f"2. 원문 자체에 밑수(단위수량)가 없는 무차원·계수·참고 표: {len(category_no_basis_in_md)}")
print("\n--- [그룹 1: 원문에 있는데 아직 못 읽은 표] 전수 명세 ---")
for it in category_has_basis_in_md:
print(f" [{it['table_id']} | line {it['line']}] {it['section']} (form: {it['pum_form']}) -> 증거: {it['evidence']}")
print("\n--- [그룹 2: 원문에 밑수 개념이 없는 표] 샘플 15개 ---")
for it in category_no_basis_in_md[:15]:
print(f" [{it['table_id']} | line {it['line']}] {it['section']} (form: {it['pum_form']}) -> {it['reason']}")
@@ -0,0 +1,36 @@
import os, sys, re, json
sys.stdout.reconfigure(encoding='utf-8')
ch13_path = "resources/knowledge/original/원가계산/건설공사_표준품셈/04_기계설비부문/제13장_플랜트설비공사.md"
with open(ch13_path, 'r', encoding='utf-8') as f:
lines = f.readlines()
print(f"Total lines in 제13장: {len(lines)}")
collapsed_rows = []
for idx, line in enumerate(lines):
line_str = line.strip()
if line_str.startswith('|') and line_str.endswith('|'):
cells = [c.strip() for c in line_str.split('|')[1:-1]]
# Find if any cell has >= 10 space-separated tokens that look like numbers or specifications
for c_idx, c in enumerate(cells):
tokens = c.split()
if len(tokens) >= 8 and any(tok[0].isdigit() for tok in tokens):
collapsed_rows.append({
"line": idx + 1,
"col_idx": c_idx,
"token_count": len(tokens),
"first_cell": cells[0][:30],
"cell_snippet": c[:100],
"tokens": tokens
})
break
print(f"Found {len(collapsed_rows)} collapsed multi-token rows in 제13장")
for r in collapsed_rows:
print(f"L{r['line']} (tokens: {r['token_count']}) [{r['first_cell']}]: {r['cell_snippet'][:80]}")
with open('scratch/ch13_collapsed_rows.json', 'w', encoding='utf-8') as f:
json.dump(collapsed_rows, f, ensure_ascii=False, indent=2)
@@ -0,0 +1,18 @@
import json
import re
with open('resources/data_work_item_link/work_item_link_2026-01-01.json', 'r', encoding='utf-8') as f:
data = json.load(f)
links = data.get('links', [])
print(f"Total links: {len(links)}")
const_keys = set()
for link in links:
ck = link.get('const_key')
if ck:
const_keys.add(ck)
print(f"Unique const_keys: {len(const_keys)}")
for ck in sorted(const_keys):
print(ck)
@@ -0,0 +1,18 @@
import json, os, sys
sys.stdout.reconfigure(encoding='utf-8')
data = json.load(open('scratch/deep_scan_flaws.json', encoding='utf-8'))
by_file = {}
for d in data:
f = os.path.basename(d['file'])
by_file.setdefault(f, []).append(d)
print(f"Total Flaws: {len(data)}")
for f, items in sorted(by_file.items()):
print(f"\n=== {f} ({len(items)} items) ===")
for item in items:
line_no = item.get('line', item.get('start_line', '?'))
itype = item.get('type')
detail = item.get('detail', '')
print(f" Line {line_no}: [{itype}] {detail[:100]}")
@@ -0,0 +1,66 @@
import sys, os, re, json
sys.stdout.reconfigure(encoding='utf-8')
with open('scratch/confirmed_attached_headers.json', 'r', encoding='utf-8') as f:
fixes = json.load(f)
fixes_by_file = {}
for fix in fixes:
f = fix['file']
if f not in fixes_by_file:
fixes_by_file[f] = []
fixes_by_file[f].append(fix)
base_dir = "resources/knowledge/original/원가계산/건설공사_표준품셈"
stats = []
for rel_file, file_fixes in fixes_by_file.items():
fpath = os.path.join(base_dir, rel_file)
with open(fpath, 'r', encoding='utf-8') as f:
text = f.read()
lines = text.split('\n')
orig_count = len(lines)
# Sort fixes descending by line
file_fixes_sorted = sorted(file_fixes, key=lambda x: (x['line'], x['matched_pos']), reverse=True)
modified_lines = list(lines)
fixed_count = 0
for fix in file_fixes_sorted:
l_idx = fix['line'] - 1
sec_num = fix['sec_num']
cur_line = modified_lines[l_idx]
# Regex to find exact boundary: not space, not quote/bracket, then sec_num + space
pattern = re.compile(rf'([^\s#\-\*\>\|\(\'"\[])({re.escape(sec_num)}\s+)')
m = pattern.search(cur_line)
if m:
new_line = cur_line[:m.start(2)] + '\n' + cur_line[m.start(2):]
modified_lines[l_idx] = new_line
fixed_count += 1
new_text = '\n'.join(modified_lines)
new_lines = new_text.split('\n')
added_lines = len(new_lines) - orig_count
with open(fpath, 'w', encoding='utf-8') as f:
f.write(new_text)
stats.append({
"file": rel_file,
"original_lines": orig_count,
"new_lines": len(new_lines),
"lines_added": added_lines,
"fixes_applied": fixed_count
})
print(f"Fixed {rel_file}: {fixed_count} headers restored (lines {orig_count} -> {len(new_lines)})")
with open('scratch/headers_fix_stats.json', 'w', encoding='utf-8') as f:
json.dump(stats, f, ensure_ascii=False, indent=2)
total_fixes = sum(s['fixes_applied'] for s in stats)
total_lines_added = sum(s['lines_added'] for s in stats)
print(f"\n=== Completed: {total_fixes} attached headers restored, {total_lines_added} lines added across {len(stats)} files ===")
@@ -0,0 +1,680 @@
[
{
"key": "CW-00009",
"section": "1-2-2",
"name": "단위표준",
"chapter": "공통_제1장",
"pdf_pages": [
60
],
"header_separation": "⚠ 앞 문장과 경계 붙음 (줄바꿈 누락)",
"preceding_context": "8. 절토(切土)량은 자연상태의 설계도의 양으로 한다.",
"flaws_detected": [
"표 한 줄 뭉침 (15개 단어): | 공 사 연 장 | m | 2 | m | - | |...",
"표 한 줄 뭉침 (15개 단어): | 공 사 폭 원 | - | - | m | 1 | |...",
"표 한 줄 뭉침 (15개 단어): | 직 공 인 부 | - | - | 인 | 2 | |...",
"표 한 줄 뭉침 (15개 단어): | 공 사 면 적 | - | - | ㎡ | 1 | |...",
"표 한 줄 뭉침 (15개 단어): | 용 지 면 적 | - | - | ㎡ | - | |...",
"표 한 줄 뭉침 (20개 단어): | 토 적 ( 높 이 , 너 비 ) | - | - | m | 2 | |...",
"표 한 줄 뭉침 (18개 단어): | 토 적 ( 단 면 적 ) | - | - | ㎡ | 1 | |...",
"표 한 줄 뭉침 (17개 단어): | 토 적 ( 체 적 ) | - | - | ㎥ | 2 | |...",
"표 한 줄 뭉침 (19개 단어): | 토 적 ( 체 적 합 계 ) | - | - | ㎥ | - | |...",
"표 한 줄 뭉침 (16개 단어): | 모 래 , 자 갈 | ㎝ | - | ㎥ | 2 | |...",
"표 한 줄 뭉침 (17개 단어): | 견 치 돌 , 깬 돌 | ㎝ | - | ㎡ | 1 | |...",
"표 한 줄 뭉침 (17개 단어): | 견 치 돌 , 깬 돌 | ㎝ | - | 개 | - | |...",
"표 한 줄 뭉침 (19개 단어): | 야 면 석 ( 野 面 石 ) | ㎝ | - | 개 | - | |...",
"표 한 줄 뭉침 (19개 단어): | 야 면 석 ( 야 面 石 ) | ㎝ | - | ㎥ | 1 | |...",
"표 한 줄 뭉침 (19개 단어): | 야 면 석 ( 野 面 石 ) | ㎝ | - | ㎡ | 1 | |...",
"표 한 줄 뭉침 (18개 단어): | 돌 쌓 기 및 돌 붙 임 | ㎝ | - | ㎥ | 1 | |...",
"페이지 전이 찌꺼기 삽입: →4공통부문",
"표 한 줄 뭉침 (311개 단어): | 돌 쌓 기 및 돌 붙 임 사 석 ( 捨 石 ) 다 듬 돌 ( 切 石 , 板 石 ) 벽 돌 블 록 시 멘 ...",
"페이지 전이 찌꺼기 삽입: →5제1장 적용기준",
"표 한 줄 뭉침 (126개 단어): | 도 료 ( 塗 料 ) 도 장 ( 塗 裝 ) 관 류 ( 管 類 ) 수 로 연 장 옹 벽 승강장옹벽 및 울타...",
"표 한 줄 뭉침 (15개 단어): | 설 계 서 의 금 액 란 | 원 | 1 | 미만버림 |...",
"표 한 줄 뭉침 (16개 단어): | 일 위 대 가 표 의 계 금 | 원 | 1 | 미만버림 |...",
"표 한 줄 뭉침 (17개 단어): | 일 위 대 가 표 의 금 액 란 | 원 | 0.1 | 미만버림 |..."
]
},
{
"key": "CW-00010",
"section": "1-2-3",
"name": "토질",
"chapter": "공통_제1장",
"pdf_pages": [
62
],
"header_separation": "⚠ 앞 문장과 경계 붙음 (줄바꿈 누락)",
"preceding_context": "이 불가피할 경우에는 소수자리의 정도를 조정 계산한다.",
"flaws_detected": [
"페이지 전이 찌꺼기 삽입: - ⑦ 깬돌(割石) : 견치돌에 준한 재두방추형(栽頭方錐形)으로서 견치돌보다 치수가 불규칙하고 일반적으로뒷면",
"표 한 줄 뭉침 (15개 단어): | 보 통 암 ( 普 通 硬 岩 ) | 1.551.70 | 1.201.40 |...",
"표 한 줄 뭉침 (21개 단어): | 고 결 ( 固 結 ) 된 역 질 토 ( 礫 質 土 ) | 1.251.45 | 1.101.30 |...",
"표 한 줄 뭉침 (21개 단어): | 암 괴 ( 岩 塊 ) 나 호 박 돌 이 섞 인 모 래 | 1.151.20 | 0.901.00 |...",
"표 한 줄 뭉침 (23개 단어): | 암 괴 ( 岩 塊 ) 나 호 박 돌 이 섞 인 모 래 질 흙 | 1.401.45 | 0.900.95 ...",
"페이지 전이 찌꺼기 삽입: →8공통부문",
"표 한 줄 뭉침 (21개 단어): | 역 ( 礫 ) 이 섞 인 점 질 토 ( 粘 質 土 ) | 1.351.40 | 0.901.00 |...",
"표 한 줄 뭉침 (22개 단어): | 암 괴 ( 岩 塊 ) 나 호 박 돌 이 섞 인 점 질 토 | 1.401.45 | 0.900.95 |...",
"표 한 줄 뭉침 (21개 단어): | 암 괴 ( 岩 塊 ) 나 호 박 돌 이 섞 인 점 토 | 1.401.45 | 0.900.95 |...",
"표 한 줄 뭉침 (15개 단어): | 자 연 상 태 의 체 적 | 1 | L | C |..."
]
},
{
"key": "CW-00014",
"section": "1-2-7",
"name": "운반",
"chapter": "공통_제1장",
"pdf_pages": [
71,
630
],
"header_separation": "⚠ 앞 문장과 경계 붙음 (줄바꿈 누락)",
"preceding_context": "계경비산정표에 명시되지 아니한 소규모의 것.14공통부문",
"flaws_detected": [
"표 한 줄 뭉침 (17개 단어): | 토 사 류 석 재 류 | 1.5분 2분 | 3,000 | 2,500 | 2,000 |...",
"페이지 전이 찌꺼기 삽입: - ⑥ 적재운반 적하는 1인을 기준으로 한다.15제1장 적용기준",
"표 한 줄 뭉침 (18개 단어): | 보 통 인 부 | 인 | 0.44 | 0.56 | 0.74 | 0.96 | 1.19 |...",
"표 한 줄 뭉침 (17개 단어): | 비 고 | - 리프트를 사용할 경우 보통인부 0.31인을 적용한다. | | | | | |...",
"표 한 줄 뭉침 (25개 단어): | 경사도 | % | 10 | 20 | 30 | 40 | 50 | 60 | 70 | 80 | 90 | 100...",
"표 한 줄 뭉침 (24개 단어): | | 각도 | 6 | 11 | 17 | 22 | 27 | 31 | 35 | 39 | 42 | 45 |...",
"표 한 줄 뭉침 (24개 단어): | 환산계수(α) | | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |...",
"표 한 줄 뭉침 (18개 단어): | 종 별 | 규 격 | 단위 | 적 재 량 | | | | 비고 |...",
"표 한 줄 뭉침 (22개 단어): | 목 재 (원 목) | 길이가 긴 것은 낱개 | ㎥ | 7.7 | 10 | 13 | - | |...",
"표 한 줄 뭉침 (18개 단어): | 목 재 (제재목) | 〃 | 〃 | 9.0 | 12 | 16 | - | |...",
"표 한 줄 뭉침 (16개 단어): | 경유・휘발유 | 200 | 드럼 | 30 | 40 | 55 | - | |...",
"표 한 줄 뭉침 (19개 단어): | 아 스 팔 트 | 〃 | 〃 | 24 | 35 | 50 | - | |...",
"표 한 줄 뭉침 (18개 단어): | 새 끼 | 12㎜, 9.4㎏ | 다발 | 480 | 640 | - | - | |...",
"표 한 줄 뭉침 (17개 단어): | 벽 돌 | 19㎝×9㎝×5.7㎝(표준형) | 개 | 2,930 | 3,900 | 5,300 | - | ...",
"표 한 줄 뭉침 (17개 단어): | 기 와 | 34㎝×30㎝×1.5㎝ | 매 | 1,860 | 2,480 | 3,400 | - | |...",
"표 한 줄 뭉침 (19개 단어): | 보 도 블 록 | 30㎝×45㎝×6㎝ | 개 | 490 | 650 | 890 | - | |...",
"표 한 줄 뭉침 (19개 단어): | 견 치 돌 | 뒷길이 45㎝ | 개 | 100 | 135 | 180 | - | |...",
"표 한 줄 뭉침 (18개 단어): | 블 록 | 두께 10㎝ | 〃 | 650 | 860 | 1,180 | - | |...",
"표 한 줄 뭉침 (17개 단어): | 〃 | 두께 15㎝ | 〃 | 450 | 600 | 820 | - | |...",
"표 한 줄 뭉침 (17개 단어): | 〃 | 두께 20㎝ | 〃 | 350 | 460 | 630 | - | |...",
"표 한 줄 뭉침 (41개 단어): | 타 일 크 링 커 타 일 | 두께 6㎜ (8㎜) 두께 24㎜ | ㎡ 〃 | 500 (350) 150 | ...",
"표 한 줄 뭉침 (49개 단어): | 합 판 유 리 페 인 트 아 스 타 일 | 12×900×1,800㎜ 두께 3㎜ 4(18)/통 3㎜×3...",
"표 한 줄 뭉침 (18개 단어): | 흄 관 | ø300㎜ L=2.5m | 본 | 27 | 36 | 52 | - | |...",
"표 한 줄 뭉침 (17개 단어): | 〃 | 450 〃 | 〃 | 15 | 20 | 27 | - | |...",
"표 한 줄 뭉침 (17개 단어): | 〃 | 600 〃 | 〃 | 8 | 12 | 15 | - | |...",
"표 한 줄 뭉침 (17개 단어): | 〃 | 800 〃 | 〃 | 4 | 6 | 9 | - | |...",
"표 한 줄 뭉침 (17개 단어): | 〃 | 900 〃 | 〃 | 4 | 5 | 7 | - | |...",
"표 한 줄 뭉침 (17개 단어): | 〃 | 1,000 〃 | 〃 | 3 | 4 | 5 | 10 | |...",
"표 한 줄 뭉침 (17개 단어): | 〃 | 1,200 〃 | 〃 | 2 | 3 | 4 | 7 | |...",
"표 한 줄 뭉침 (17개 단어): | 〃 | 1,500 〃 | 〃 | 1 | 2 | 2 | 5 | |...",
"표 한 줄 뭉침 (21개 단어): | 콘 크 리 트 관 | ø250㎜ L=1m | 본 | 60 | 80 | 110 | - | |...",
"표 한 줄 뭉침 (17개 단어): | 〃 | 300 〃 | 〃 | 52 | 70 | 96 | - | |...",
"표 한 줄 뭉침 (17개 단어): | 〃 | 350 〃 | 〃 | 42 | 60 | 82 | - | |...",
"표 한 줄 뭉침 (17개 단어): | 〃 | 450 〃 | 〃 | 25 | 30 | 41 | - | |...",
"표 한 줄 뭉침 (17개 단어): | 〃 | 600 〃 | 〃 | 16 | 20 | 27 | - | |...",
"표 한 줄 뭉침 (17개 단어): | 〃 | 900 〃 | 〃 | 9 | 12 | 16 | - | |...",
"표 한 줄 뭉침 (17개 단어): | 〃 | 1,0001,500 〃 | 〃 | 36 | 48 | 510 | 12 | |..."
]
},
{
"key": "CW-00018",
"section": "1-3-1",
"name": "재료의 할증",
"chapter": "공통_제1장",
"pdf_pages": [
76,
631,
923
],
"header_separation": "⚠ 앞 문장과 경계 붙음 (줄바꿈 누락)",
"preceding_context": " 않는다.19제1장 적용기준1-3 재료 및 노임의 할증",
"flaws_detected": [
"페이지 전이 찌꺼기 삽입: 공종 시공 시에는 본 할증을 적용하지 않는다.19제1장 적용기준1-3 재료 및 노임의 할증1-3-1 재료의 ",
"표 한 줄 뭉침 (48개 단어): | 치 환 모 래 ( 置 換 砂 ) 깔 모 래 ( 敷 砂 ) 사 항 용 모 래 ( 砂 抗 用 砂 ) 압 입 ...",
"표 한 줄 뭉침 (16개 단어): | | 사석두께 | 2m미만 | 2m이상 | 2m미만 | 2m이상 | 2m미만 | 2m이상 |...",
"표 한 줄 뭉침 (19개 단어): | 기 초 사 석 | | 25% | 20% | 30% | 25% | 50% | 40% |...",
"표 한 줄 뭉침 (23개 단어): | 피 복 석 ( 被 覆 石 ) | | 15% | 15% | 15% | 15% | 20% | 20% |...",
"표 한 줄 뭉침 (20개 단어): | 뒤 채 움 사 석 | | 20% | 20% | 20% | 20% | 25% | 25% |...",
"표 한 줄 뭉침 (120개 단어): | 원 형 철 근 이 형 철 근 이 형 철 근 ( 교 량 ・ 지 하 철 및 이 와 유 사 한 복 잡 한 구 ...",
"표 한 줄 뭉침 (17개 단어): | 프 레 스 접 합 식 스 테 인 리 스 강 관 | 5 |...",
"페이지 전이 찌꺼기 삽입: - ④ 현장 여건상 절단 및 가공 등이 불필요한 경우, 상기 할증률을 조정하여 적용할 수 있다.21제1장 적",
"표 한 줄 뭉침 (275개 단어): | 각 재 목 재 판 재 일 반 용 합 판 합 판 수 장 용 합 판 쉬 즈 관 쉬 즈 판 P V C 관 / ...",
"페이지 전이 찌꺼기 삽입: →22공통부문",
"표 한 줄 뭉침 (72개 단어): | 테 라 콧 타 블 록 기 와 슬 레 이 트 모 자 이 크 도 기 자 기 아 스 팔 트 타 일 리 노 륨 ...",
"페이지 전이 찌꺼기 삽입: - 5. 할증율(%)은 요소별 일반적인 작업조건을 기준으로 제시하였으며, 일부의 작업에 영향을 미치는경우 할"
]
},
{
"key": "CW-00151",
"section": "3-3-6",
"name": "암발파(대규모발파 TYPE-Ⅵ)",
"chapter": "공통_제3장",
"pdf_pages": [
127
],
"header_separation": "정상 분리",
"preceding_context": "",
"flaws_detected": [
"표 한 줄 뭉침 (16개 단어): | 유 압 식 크 롤 러 드 릴 | 110㎾ | hr | 0.012 |...",
"표 한 줄 뭉침 (17개 단어): | 보 통 인 부 굴 착 기 | 1.0㎥ | 인 hr | 0.125 1.000 |...",
"표 한 줄 뭉침 (18개 단어): | 굴 착 기 + 대 형 브 레 이 커 | 0.60.8㎥ | 9 | 11 |...",
"표 한 줄 뭉침 (16개 단어): | 유 압 식 크 롤 러 드 릴 | 110㎾ | hr | 0.121 |...",
"표 한 줄 뭉침 (18개 단어): | 굴 착 기 + 대 형 브 레 이 커 | 1.0㎥ | hr | 0.121 |...",
"표 한 줄 뭉침 (16개 단어): | 소 형 브 레 이 커 | 2.7㎥/min | hr | 0.474 | 0.313 |...",
"표 한 줄 뭉침 (15개 단어): | 공 기 압 축 기 | 10.3㎥/min | hr | 0.158 | 0.104 |...",
"표 한 줄 뭉침 (16개 단어): | 구 분 | 규 격 | 단 위 | 수 량 | 시공량(㎥) | |...",
"표 한 줄 뭉침 (16개 단어): | 특 별 인 부 | - | 인 | 1 | 1,150 | 770 |...",
"표 한 줄 뭉침 (19개 단어): | 진 동 롤 러 ( 자 주 식 ) | 10.0ton | 대 | 1 | | |...",
"표 한 줄 뭉침 (18개 단어): | 물 탱 크 ( 살 수 차 ) | 16,000 | 대 | 0.5 | | |...",
"표 한 줄 뭉침 (15개 단어): | 구 분 | 규 격 | 단 위 | 수 량 | 시공량(㎥) |..."
]
},
{
"key": "CW-00158",
"section": "3-4-3",
"name": "흙 다지기",
"chapter": "공통_제3장",
"pdf_pages": [
131
],
"header_separation": "⚠ 앞 문장과 경계 붙음 (줄바꿈 누락)",
"preceding_context": "- ② 포설 및 다짐작업을 포함한다.",
"flaws_detected": [
"표 한 줄 뭉침 (18개 단어): | 구 분 | 규 격 | 단 위 | 수 량 | 다짐두께 | 시공량(㎥) | |...",
"표 한 줄 뭉침 (18개 단어): | 특 별 인 부 | - | 인 | 1 | 15cm | 18 | 11 |...",
"표 한 줄 뭉침 (18개 단어): | 보 통 인 부 | - | 인 | 1 | 30cm | 24 | 15 |...",
"표 한 줄 뭉침 (18개 단어): | 특 별 인 부 | - | 인 | 1 | 15cm | 14 | 9 |...",
"표 한 줄 뭉침 (18개 단어): | 보 통 인 부 | - | 인 | 1 | 30cm | 20 | 13 |...",
"표 한 줄 뭉침 (15개 단어): | 구 분 | 규 격 | 단 위 | 수 량 | 시공량(㎥) |...",
"표 한 줄 뭉침 (19개 단어): | 특 별 인 부 보 통 인 부 | | 인 인 | 1 1 | 110 |...",
"표 한 줄 뭉침 (15개 단어): | 구 분 | 규 격 | 단 위 | 수 량 | 시공량(㎥) |...",
"표 한 줄 뭉침 (19개 단어): | 특 별 인 부 보 통 인 부 | | 인 인 | 1 1 | 250 |...",
"표 한 줄 뭉침 (15개 단어): | 구 분 | 규 격 | 단 위 | 수 량 | 시공량(㎥) |...",
"표 한 줄 뭉침 (18개 단어): | 특 별 인 부 보 통 인 부 | | 인 | 1 1 | 130 |...",
"표 한 줄 뭉침 (15개 단어): | 구 분 | 규 격 | 단 위 | 수 량 | 시공량(㎥) |...",
"표 한 줄 뭉침 (19개 단어): | 특 별 인 부 보 통 인 부 | | 인 인 | 1 1 | 290 |...",
"표 한 줄 뭉침 (19개 단어): | 구 분 | 규 격 | 단 위 | 모래지정 | | 자갈지정 | | 잡석지정 | |...",
"표 한 줄 뭉침 (22개 단어): | | | | 수 량 | 시공량 (㎥) | 수 량 | 시공량 (㎥) | 수 량 | 시공량 (㎥) |...",
"표 한 줄 뭉침 (29개 단어): | 특 별 인 부 보 통 인 부 | | 인 인 | 1 1 | 110 | 1 1 | 100 | 1 1 | 9...",
"표 한 줄 뭉침 (18개 단어): | 굴 착 기 | 0.2㎥ | 대 | 1 | | 1 | | 1 | |...",
"표 한 줄 뭉침 (22개 단어): | 플 레 이 트 콤 팩 터 | 1.5ton | 대 | 1 | | - | | - | |...",
"표 한 줄 뭉침 (16개 단어): | 진동롤러(핸드가이드식) | 0.7ton | 대 | - | | 1 | | 1 | |...",
"표 한 줄 뭉침 (18개 단어): | 구 분 | 규 격 | 단 위 | 수 량 | 토질(암) 분류 | 시공량(㎡) |...",
"표 한 줄 뭉침 (23개 단어): | 굴 착 기 | 1.0㎥ | 대 | 1 | 모래・사질토・점토・점질토 연질토・불순자갈 호박돌 섞인 고결토・경...",
"표 한 줄 뭉침 (15개 단어): | 굴 착 기 | 1.0㎥ | 대 | 1 | 연암 | 80 |...",
"표 한 줄 뭉침 (16개 단어): | 구 분 | 규 격 | 단 위 | 수 량 | 시공량(㎡) | |...",
"표 한 줄 뭉침 (23개 단어): | 특 별 인 부 보 통 인 부 | - - | 인 인 | 2 5 | 19 | 25 |...",
"표 한 줄 뭉침 (15개 단어): | 구 분 | 규 격 | 단 위 | 수 량 | 시공량(㎡) |...",
"표 한 줄 뭉침 (17개 단어): | 조 경 공 보 통 인 부 | 인 인 | 1 5 | 670 |..."
]
},
{
"key": "CW-00165",
"section": "3-5-1",
"name": "절토면 고르기",
"chapter": "공통_제3장",
"pdf_pages": [
133
],
"header_separation": "⚠ 앞 문장과 경계 붙음 (줄바꿈 누락)",
"preceding_context": "경비 산정시 조정원은 계상하지 않는다.3-5 절토부대공",
"flaws_detected": [
"표 한 줄 뭉침 (18개 단어): | 구 분 | 규 격 | 단 위 | 수 량 | 토질(암) 분류 | 시공량(㎡) |...",
"표 한 줄 뭉침 (23개 단어): | 굴 착 기 | 1.0㎥ | 대 | 1 | 모래・사질토・점토・점질토 연질토・불순자갈 호박돌 섞인 고결토・경...",
"표 한 줄 뭉침 (15개 단어): | 굴 착 기 | 1.0㎥ | 대 | 1 | 연암 | 80 |...",
"표 한 줄 뭉침 (16개 단어): | 구 분 | 규 격 | 단 위 | 수 량 | 시공량(㎡) | |...",
"표 한 줄 뭉침 (23개 단어): | 특 별 인 부 보 통 인 부 | - - | 인 인 | 2 5 | 19 | 25 |...",
"표 한 줄 뭉침 (15개 단어): | 구 분 | 규 격 | 단 위 | 수 량 | 시공량(㎡) |...",
"표 한 줄 뭉침 (17개 단어): | 조 경 공 보 통 인 부 | 인 인 | 1 5 | 670 |...",
"표 한 줄 뭉침 (15개 단어): | 구 분 | | 단 위 | 수 량 | 시공량(㎡) | | |...",
"표 한 줄 뭉침 (18개 단어): | | | | | 비탈경사 1:1.5 이상 | 비탈경사 1:1.0이상∼ 1:1.5 미만 | 비탈경사 ...",
"표 한 줄 뭉침 (24개 단어): | 인력 | 특 별 인 부 보 통 인 부 | 인 인 | 2 3 | 27 | 24 | 22 |...",
"표 한 줄 뭉침 (24개 단어): | 기계 | 특 별 인 부 보 통 인 부 | 인 인 | 2 2 | 41 | 38 | 35 |...",
"표 한 줄 뭉침 (20개 단어): | 비 고 | | - 비탈틀을 고정하기 위한 유항(留杭)을 설치하는 경우는 보통인부 0.4인/10본당을 계...",
"표 한 줄 뭉침 (15개 단어): | 구 분 | 규 격 | 단 위 | 수 량 | 시공량(개소) |...",
"표 한 줄 뭉침 (35개 단어): | 중 급 기 술 자 보 링 공 특 별 인 부 보 통 인 부 | - - - - | 인 인 인 인 | 1 1 ...",
"표 한 줄 뭉침 (15개 단어): | 강 연 선 인 장 기 | 60ton | 대 | 1 | |...",
"표 한 줄 뭉침 (18개 단어): | 특 별 인 부 보 통 인 부 | 인 인 | 3 2 | 290 |...",
"표 한 줄 뭉침 (15개 단어): | 구 분 | 단 위 | 수 량 | 뿜어붙이기 두께 | 시공량(㎡) |...",
"표 한 줄 뭉침 (30개 단어): | 비 고 | - 수직고 20m 이상인 경우 시공량에 다음 할증률을 감한다. 수직고 20 30m 30 ...",
"표 한 줄 뭉침 (15개 단어): | 규격 | ø16, 0.5m | ø16, 0.35m | ø3.258×58 PVC코팅 | #8 PVC코팅 |..."
]
},
{
"key": "CW-00168",
"section": "3-6-1",
"name": "성토면 고르기",
"chapter": "공통_제3장",
"pdf_pages": [
134
],
"header_separation": "⚠ 앞 문장과 경계 붙음 (줄바꿈 누락)",
"preceding_context": " 기계경비는 인력품의 2%로 계상한다.3-6 성토부대공",
"flaws_detected": [
"표 한 줄 뭉침 (15개 단어): | 구 분 | 규 격 | 단 위 | 수 량 | 시공량(㎡) |...",
"표 한 줄 뭉침 (17개 단어): | 조 경 공 보 통 인 부 | 인 인 | 1 5 | 670 |...",
"표 한 줄 뭉침 (15개 단어): | 구 분 | | 단 위 | 수 량 | 시공량(㎡) | | |...",
"표 한 줄 뭉침 (18개 단어): | | | | | 비탈경사 1:1.5 이상 | 비탈경사 1:1.0이상∼ 1:1.5 미만 | 비탈경사 ...",
"표 한 줄 뭉침 (24개 단어): | 인력 | 특 별 인 부 보 통 인 부 | 인 인 | 2 3 | 27 | 24 | 22 |...",
"표 한 줄 뭉침 (24개 단어): | 기계 | 특 별 인 부 보 통 인 부 | 인 인 | 2 2 | 41 | 38 | 35 |...",
"표 한 줄 뭉침 (20개 단어): | 비 고 | | - 비탈틀을 고정하기 위한 유항(留杭)을 설치하는 경우는 보통인부 0.4인/10본당을 계...",
"표 한 줄 뭉침 (15개 단어): | 구 분 | 규 격 | 단 위 | 수 량 | 시공량(개소) |...",
"표 한 줄 뭉침 (35개 단어): | 중 급 기 술 자 보 링 공 특 별 인 부 보 통 인 부 | - - - - | 인 인 인 인 | 1 1 ...",
"표 한 줄 뭉침 (15개 단어): | 강 연 선 인 장 기 | 60ton | 대 | 1 | |...",
"표 한 줄 뭉침 (18개 단어): | 특 별 인 부 보 통 인 부 | 인 인 | 3 2 | 290 |...",
"표 한 줄 뭉침 (15개 단어): | 구 분 | 단 위 | 수 량 | 뿜어붙이기 두께 | 시공량(㎡) |...",
"표 한 줄 뭉침 (30개 단어): | 비 고 | - 수직고 20m 이상인 경우 시공량에 다음 할증률을 감한다. 수직고 20 30m 30 ...",
"표 한 줄 뭉침 (15개 단어): | 규격 | ø16, 0.5m | ø16, 0.35m | ø3.258×58 PVC코팅 | #8 PVC코팅 |...",
"표 한 줄 뭉침 (18개 단어): | 구 분 | 규 격 | 단 위 | 수 량 | 뿜어붙이기 두께 | 시공량(㎡) |...",
"표 한 줄 뭉침 (30개 단어): | 조 경 공 | - - - - | 인 인 인 인 | 1 1 2 2 | 5cm 7cm 10cm 15cm | ...",
"표 한 줄 뭉침 (18개 단어): | 취 부 기 ( 녹 생 토 ) | 18.65kW | 대 | 1 | | |...",
"표 한 줄 뭉침 (15개 단어): | 공 기 압 축 기 | 21㎥/min | 대 | 1 | | |..."
]
},
{
"key": "CW-00196",
"section": "4-1-3",
"name": "초류종자 살포(기계살포)",
"chapter": "공통_제4장",
"pdf_pages": [
147
],
"header_separation": "⚠ 앞 문장과 경계 붙음 (줄바꿈 누락)",
"preceding_context": "차 등의 장비가 필요한 경우 기계경비는 별도 계상한다.",
"flaws_detected": [
"표 한 줄 뭉침 (15개 단어): | 구 분 | 규 격 | 단 위 | 수 량 | 시공량(㎡) |...",
"표 한 줄 뭉침 (18개 단어): | 조 경 공 보 통 인 부 | | 인 인 | 2 1 | 3,100 |...",
"표 한 줄 뭉침 (35개 단어): | 종 자 비 료 피 복 제 침 식 방 지 안 정 제 색 소 | 복합비료 화이버/펄프류 합성접착제 착색제 |...",
"표 한 줄 뭉침 (21개 단어): | 조 경 공 보 통 인 부 | 인 인 | 3 1 | 2,700 | 1,800 | 1,100 |...",
"표 한 줄 뭉침 (17개 단어): | 조 경 공 보 통 인 부 | 인 인 | 3 1 | 1,600 |...",
"표 한 줄 뭉침 (25개 단어): | 조 경 공 보 통 인 부 | 인 인 | 3 1 | 0.3미만 0.3~0.7이하 0.8~1.1이하 1.2~...",
"표 한 줄 뭉침 (25개 단어): | 조 경 공 보 통 인 부 | 인 인 | 3 1 | 0.3미만 0.3~0.7이하 0.8~1.1이하 1.2~...",
"표 한 줄 뭉침 (25개 단어): | 조 경 공 보 통 인 부 | 인 인 | 3 1 | 0.3미만 0.3~0.7이하 0.8~1.1이하 1.2~...",
"표 한 줄 뭉침 (17개 단어): | 수관폭(㎝) | 20 | 30 | 40 | 50 | 60 | 80 | 100 |...",
"표 한 줄 뭉침 (17개 단어): | 주수 | 32 | 14 | 8 | 5 | 4 | 2 | 1 |..."
]
},
{
"key": "CW-00198",
"section": "4-1-5",
"name": "거적덮기",
"chapter": "공통_제4장",
"pdf_pages": [
148
],
"header_separation": "⚠ 앞 문장과 경계 붙음 (줄바꿈 누락)",
"preceding_context": "하천변・ 절개지 등 안전사고의 ㉰ 불량위험이 있는 경우",
"flaws_detected": [
"표 한 줄 뭉침 (17개 단어): | 조 경 공 보 통 인 부 | 인 인 | 3 1 | 1,600 |...",
"표 한 줄 뭉침 (25개 단어): | 조 경 공 보 통 인 부 | 인 인 | 3 1 | 0.3미만 0.3~0.7이하 0.8~1.1이하 1.2~...",
"표 한 줄 뭉침 (25개 단어): | 조 경 공 보 통 인 부 | 인 인 | 3 1 | 0.3미만 0.3~0.7이하 0.8~1.1이하 1.2~...",
"표 한 줄 뭉침 (25개 단어): | 조 경 공 보 통 인 부 | 인 인 | 3 1 | 0.3미만 0.3~0.7이하 0.8~1.1이하 1.2~...",
"표 한 줄 뭉침 (17개 단어): | 수관폭(㎝) | 20 | 30 | 40 | 50 | 60 | 80 | 100 |...",
"표 한 줄 뭉침 (17개 단어): | 주수 | 32 | 14 | 8 | 5 | 4 | 2 | 1 |...",
"표 한 줄 뭉침 (18개 단어): | 구 분 | | 규 격 | 단 위 | 수 량 | 나무높이(m) | 시공량(주) |...",
"표 한 줄 뭉침 (24개 단어): | 인 력 시 공 | 조경공 보통인부 | | 인 인 | 4 2 | 2.0이하 3.0이하 5.0이하 | 70...",
"표 한 줄 뭉침 (24개 단어): | 기 계 시 공 | 조경공 보통인부 | | 인 인 | 3 1 | 2.0이하 3.0이하 5.0이하 | 90...",
"표 한 줄 뭉침 (17개 단어): | 비 고 | - 분이 없는 경우 시공량의 25%를 가산한다. | | | | | |...",
"표 한 줄 뭉침 (20개 단어): | 구 분 | | 규 격 | 단 위 | 수 량 | 근원(흉고)직경 (㎝) | 시공량 (주) |...",
"표 한 줄 뭉침 (24개 단어): | 인 력 시 공 | 조경공 보통인부 | | 인 인 | 4 2 | 5(4)이하 6~7(5~6) 8~9(7~...",
"표 한 줄 뭉침 (28개 단어): | 기 계 시 공 | 조경공 보통인부 | | 인 인 | 3 1 | 5(4)이하 6~7(5~6) 8~9(7~...",
"표 한 줄 뭉침 (26개 단어): | 기 계 시 공 | 조경공 보통인부 | | 인 인 | 3 1 | 20~29(17~24) 30~39(25~...",
"표 한 줄 뭉침 (15개 단어): | | 굴착기 크레인 | 0.6㎥ | 대 대 | 1 1 | | |..."
]
},
{
"key": "CW-00244",
"section": "6-1-1",
"name": "레디믹스트콘크리트 타설",
"chapter": "공통_제6장",
"pdf_pages": [
185
],
"header_separation": "❌ MD 본문 완전 부재",
"preceding_context": "",
"flaws_detected": []
},
{
"key": "CW-00267",
"section": "6-3-9",
"name": "지수판 설치",
"chapter": "공통_제6장",
"pdf_pages": [
198
],
"header_separation": "⚠ 앞 문장과 경계 붙음 (줄바꿈 누락)",
"preceding_context": "후 최상층전까지 각 층마다 조립 후 해체하는 기준이다.",
"flaws_detected": [
"표 한 줄 뭉침 (27개 단어): | P V C 지 수 판 P V C 용 접 봉 철 선 | 200×5t #8 | m ㎏ ㎏ | 1.04 0.0...",
"표 한 줄 뭉침 (15개 단어): | 구 분 | 규 격 | 단 위 | 수 량 | 시공량(m) |...",
"표 한 줄 뭉침 (19개 단어): | 형 틀 목 공 보 통 인 부 | | 인 인 | 2 1 | 45 |...",
"표 한 줄 뭉침 (17개 단어): | 철 근 공 보 통 인 부 | 인 인 | 2 1 | 90 |...",
"표 한 줄 뭉침 (18개 단어): | 형 틀 목 공 보 통 인 부 | 인 인 | 1 1 | 12 |...",
"표 한 줄 뭉침 (15개 단어): | 구 분 | 단 위 | 수 량 | 시공량(강연선 규격, ton) | |...",
"표 한 줄 뭉침 (26개 단어): | 기 계 설 비 공 철 근 공 보 통 인 부 | 인 인 인 | 1 3 1 | 5.0 | 6.0 |...",
"표 한 줄 뭉침 (20개 단어): | 구 분 | | 규 격 | 단 위 | 수 량 | 수 량(강연선 규격, 개소) | |...",
"표 한 줄 뭉침 (31개 단어): | 인 력 | 기 계 설 비 공 특 별 인 부 보 통 인 부 | | 인 인 인 | 1 3 1 | 22 | ...",
"표 한 줄 뭉침 (25개 단어): | 장 비 | 강 연 선 인 장 기 크 레 인 | 250t 5ton | 대 대 | 1 1 | | |...",
"표 한 줄 뭉침 (15개 단어): | 구 분 | 규 격 | 단 위 | 수 량 | 시공량(m) |...",
"표 한 줄 뭉침 (19개 단어): | 특 별 인 부 보 통 인 부 | | 인 인 | 2 4 | 400 |..."
]
},
{
"key": "CW-00416",
"section": "1-6-6",
"name": "포장줄눈 절단",
"chapter": "토목_제1장",
"pdf_pages": [
373,
931
],
"header_separation": "⚠ 앞 문장과 경계 붙음 (줄바꿈 누락)",
"preceding_context": ", 용접기 등)의 기계경비는 인력품의 3%로 계상한다.",
"flaws_detected": [
"표 한 줄 뭉침 (15개 단어): | 구 분 | 규 격 | 단 위 | 수 량 | 시공량(m) |...",
"표 한 줄 뭉침 (19개 단어): | 특 별 인 부 보 통 인 부 | | 인 인 | 1 1 | 600 |...",
"표 한 줄 뭉침 (18개 단어): | 특 별 인 부 보 통 인 부 | 인 인 | 3 2 | 900 |...",
"페이지 전이 찌꺼기 삽입: - ③ 줄눈재, 백업재 등 재료비는 별도 계상한다.317제1장 도로포장공사1-7 저속도로포장('08년 신설)",
"표 한 줄 뭉침 (16개 단어): | 구 분 | 규 격 | 단 위 | A-Type | | B-Type | |...",
"표 한 줄 뭉침 (30개 단어): | 포 장 공 특 별 인 부 보 통 인 부 | | 인 인 인 | 3 2 2 | 300 | 2 2 1 | 1...",
"표 한 줄 뭉침 (15개 단어): | 굴 착 기 | 0.6㎥ | 대 | 1 | | - | |...",
"표 한 줄 뭉침 (15개 단어): | 굴 착 기 | 0.4㎥ | 대 | - | | 1 | |...",
"표 한 줄 뭉침 (29개 단어): | 비 고 | - 유도・점자블록을 설치하는 경우 시공량의 10%를 감하여 적용한다. - 블록 정밀절단(전동절...",
"표 한 줄 뭉침 (19개 단어): | A - T y p e | - 공원, 단지・택지조성공사의 보도 등 장비이동 및 적재가 용이한 구간 |...",
"표 한 줄 뭉침 (20개 단어): | B - T y p e | - 차도인접, 주택가 보도 등 장비이동 및 적재 공간이 협소한 구간 |...",
"표 한 줄 뭉침 (16개 단어): | 구 분 | 규 격 | 단 위 | A-Type | | B-Type | |...",
"표 한 줄 뭉침 (30개 단어): | 포 장 공 특 별 인 부 보 통 인 부 | | 인 인 인 | 3 2 2 | 190 | 2 2 1 | 1...",
"표 한 줄 뭉침 (15개 단어): | 굴 착 기 | 0.6㎥ | 대 | 1 | | - | |...",
"표 한 줄 뭉침 (15개 단어): | 굴 착 기 | 0.4㎥ | 대 | - | | 1 | |...",
"표 한 줄 뭉침 (29개 단어): | 비 고 | - 유도・점자블록을 설치하는 경우 시공량의 10%를 감하여 적용한다. - 블록 정밀절단(전동절...",
"표 한 줄 뭉침 (19개 단어): | A - T y p e | - 공원, 단지・택지조성공사의 보도 등 장비이동 및 적재가 용이한 구간 |...",
"표 한 줄 뭉침 (20개 단어): | B - T y p e | - 차도인접, 주택가 보도 등 장비이동 및 적재 공간이 협소한 구간 |...",
"표 한 줄 뭉침 (18개 단어): | 포 장 공 보 통 인 부 | | 인 인 | 2 1 | 250 |...",
"표 한 줄 뭉침 (16개 단어): | 로 더 ( 타 이 어 ) | 0.57㎥ | 대 | 1 | |...",
"표 한 줄 뭉침 (16개 단어): | 플 레 이 트 콤 팩 터 | 1.5ton | 대 | 1 | |...",
"표 한 줄 뭉침 (15개 단어): | 구 분 | 규 격 | 단 위 | 수 량 | 시공량(㎡) |...",
"표 한 줄 뭉침 (18개 단어): | 포 장 공 보 통 인 부 | | 인 인 | 3 1 | 1,200 |...",
"표 한 줄 뭉침 (16개 단어): | 아 스 팔 트 피 니 셔 | 1.7m | 대 | 1 | |...",
"페이지 전이 찌꺼기 삽입: - ⑤ 현장여건에 따라 장비조합 및 규격을 변경하여 적용할 수 있다.319제1장 도로포장공사1-7-5 탄성포",
"표 한 줄 뭉침 (19개 단어): | 특 별 인 부 보 통 인 부 | | 인 인 | 5 3 | 120 |...",
"표 한 줄 뭉침 (16개 단어): | 구 분 | 규 격 | 단 위 | 수 량 | 시공량(개소) | |...",
"표 한 줄 뭉침 (21개 단어): | 특 별 인 부 보 통 인 부 | | 인 인 | 2 1 | 12 | - |..."
]
},
{
"key": "CW-00538",
"section": "6-6",
"name": "원심력 철근콘크리트관",
"chapter": "토목_제6장",
"pdf_pages": [
428
],
"header_separation": "⚠ 앞 문장과 경계 붙음 (줄바꿈 누락)",
"preceding_context": "력품의 5%로 계상한다.371제6장 관부설 및 접합공사",
"flaws_detected": [
"표 한 줄 뭉침 (45개 단어): | 배 관 공 ( 수 도 ) 보 통 인 부 양 중 장 비 | 인 인 대 | 2 1 1 | 250 300 35...",
"표 한 줄 뭉침 (37개 단어): | 배 관 공 ( 수 도 ) 보 통 인 부 양 중 장 비 | 인 인 대 | 3 1 1 | 900 1,000 ...",
"표 한 줄 뭉침 (17개 단어): | 배 관 공 ( 수 도 ) | 인 | 4 | 1,500 | 3.5 |...",
"표 한 줄 뭉침 (45개 단어): | 배 관 공 ( 수 도 ) 보 통 인 부 양 중 장 비 | 인 인 대 | 2 1 1 | 250 300 35...",
"표 한 줄 뭉침 (17개 단어): | 배 관 공 ( 수 도 ) | 인 | 3 | 900~1,000 | 5 |...",
"표 한 줄 뭉침 (17개 단어): | 배 관 공 ( 수 도 ) | 인 | 4 | 1,500 | 3.5 |...",
"표 한 줄 뭉침 (18개 단어): | 500이하 | 150 200 250 300 | 0.050 0.070 0.090 0.120 | 0.050 ...",
"표 한 줄 뭉침 (19개 단어): | 500초과~ 900이하 | 150 200 250 300 | 0.070 0.090 0.110 0.130 |...",
"표 한 줄 뭉침 (19개 단어): | 900초과~ 1200이하 | 150 200 250 300 | 0.080 0.110 0.120 0.150 ..."
]
},
{
"key": "CW-00539",
"section": "6-6-1",
"name": "소켓관 부설 및 접합",
"chapter": "토목_제6장",
"pdf_pages": [
428
],
"header_separation": "⚠ 앞 문장과 경계 붙음 (줄바꿈 누락)",
"preceding_context": "사6-6 원심력 철근콘크리트관('10, '18년 보완)",
"flaws_detected": [
"표 한 줄 뭉침 (45개 단어): | 배 관 공 ( 수 도 ) 보 통 인 부 양 중 장 비 | 인 인 대 | 2 1 1 | 250 300 35...",
"표 한 줄 뭉침 (37개 단어): | 배 관 공 ( 수 도 ) 보 통 인 부 양 중 장 비 | 인 인 대 | 3 1 1 | 900 1,000 ...",
"표 한 줄 뭉침 (17개 단어): | 배 관 공 ( 수 도 ) | 인 | 4 | 1,500 | 3.5 |...",
"표 한 줄 뭉침 (45개 단어): | 배 관 공 ( 수 도 ) 보 통 인 부 양 중 장 비 | 인 인 대 | 2 1 1 | 250 300 35...",
"표 한 줄 뭉침 (17개 단어): | 배 관 공 ( 수 도 ) | 인 | 3 | 900~1,000 | 5 |...",
"표 한 줄 뭉침 (17개 단어): | 배 관 공 ( 수 도 ) | 인 | 4 | 1,500 | 3.5 |...",
"표 한 줄 뭉침 (18개 단어): | 500이하 | 150 200 250 300 | 0.050 0.070 0.090 0.120 | 0.050 ...",
"표 한 줄 뭉침 (19개 단어): | 500초과~ 900이하 | 150 200 250 300 | 0.070 0.090 0.110 0.130 |...",
"표 한 줄 뭉침 (19개 단어): | 900초과~ 1200이하 | 150 200 250 300 | 0.080 0.110 0.120 0.150 ..."
]
},
{
"key": "CW-00844",
"section": "9-1-1",
"name": "모르타르 배합",
"chapter": "건축_제9장",
"pdf_pages": [
635,
636,
638,
639
],
"header_separation": "⚠ 앞 문장과 경계 붙음 (줄바꿈 누락)",
"preceding_context": "미장공사제9장9-1 모르타르 바름 및 타설",
"flaws_detected": [
"표 한 줄 뭉침 (15개 단어): | 구 분 | | 단 위 | 수 량 | 시공량(㎡) | | |...",
"표 한 줄 뭉침 (31개 단어): | 시 공 높 이 3 . 6 m 이하 | 미 장 공 보 통 인 부 | 인 인 | 2 1 | 40 | 29 |...",
"표 한 줄 뭉침 (32개 단어): | 시 공 높 이 3 . 6 m 초과 ~7.2m이하 | 미 장 공 보 통 인 부 | 인 인 | 2 2 | 3...",
"표 한 줄 뭉침 (32개 단어): | 비 고 | - 바탕의 폭 30㎝이하이거나 원주 바름면일 때에는 시공량의 17%를 감한다. - 비계사용 시...",
"표 한 줄 뭉침 (26개 단어): | 미 장 공 기 계 설 비 공 보 통 인 부 | 인 인 인 | 2 1 2 | 50 | 65 |...",
"표 한 줄 뭉침 (34개 단어): | 모 르 타 르 타 설 장 비 | 모 르 타 르 펌 프 믹 서 양 수 기 배 관 파 이 프 | 37㎾ 0....",
"표 한 줄 뭉침 (29개 단어): | 비 고 | - 현장 조건에 따라 작업대기 등이 발생되는 경우, 인력품에 다음 할증까지 가산하여 적용한다....",
"표 한 줄 뭉침 (25개 단어): | 비 고 | - 천장은 본 품의 20%를 가산한다. - 비계사용 시 높이 7.2m초과하는 경우 3.6m마다...",
"표 한 줄 뭉침 (28개 단어): | 시 공 높 이 3 . 6 m 이 하 | 미 장 공 보 통 인 부 | 인 인 | 2 1 | 170 |...",
"표 한 줄 뭉침 (35개 단어): | 시 공 높 이 3 . 6 m 초 과 ~ 7 . 2 m 이 하 | 미 장 공 보 통 인 부 | 인 인 | ...",
"표 한 줄 뭉침 (25개 단어): | 비 고 | - 천장은 시공량의 17%를 감한다. - 비계사용 시 높이 7.2m초과하는 경우 3.6m마다 ...",
"표 한 줄 뭉침 (28개 단어): | 시 공 높 이 3 . 6 m 이 하 | 미 장 공 보 통 인 부 | 인 인 | 2 1 | 120 |...",
"표 한 줄 뭉침 (35개 단어): | 시 공 높 이 3 . 6 m 초 과 ~ 7 . 2 m 이 하 | 미 장 공 보 통 인 부 | 인 인 | ...",
"표 한 줄 뭉침 (25개 단어): | 비 고 | - 천장은 시공량의 17%를 감한다. - 비계사용 시 높이 7.2m초과하는 경우 3.6m마다 ..."
]
},
{
"key": "CW-01225",
"section": "1-2-5",
"name": "관목 전정",
"chapter": "유지관리_제1장",
"pdf_pages": [
917
],
"header_separation": "⚠ 앞 문장과 경계 붙음 (줄바꿈 누락)",
"preceding_context": "비(전정기 등)의 기계경비는 인력품의 3%로 계상한다.",
"flaws_detected": [
"표 한 줄 뭉침 (19개 단어): | 조 경 공 보 통 인 부 | 인 인 | 2 1 | 540 | 330 |...",
"표 한 줄 뭉침 (17개 단어): | 구 분 | | 단 위 | 수 량 | 시공량(주) | | | | |...",
"표 한 줄 뭉침 (15개 단어): | | | | | 11㎝미만 | 1121㎝미만 | 2131㎝미만 | 3141㎝미만 | 4151...",
"표 한 줄 뭉침 (28개 단어): | 수간보호 ( 조 형 ) | 조 경 공 보통인부 | 인 인 | 2 1 | 24 | 13 | 6 | 3 | ...",
"표 한 줄 뭉침 (28개 단어): | 수간보호 ( 일 반 ) | 조 경 공 보통인부 | 인 인 | 2 1 | 38 | 24 | 16 | 11 ...",
"표 한 줄 뭉침 (16개 단어): | 구 분 | 단 위 | 수 량 | 시공량(주) | | | | |...",
"표 한 줄 뭉침 (18개 단어): | | | | 11㎝미만 | 11 21㎝미만 | 21∼ 31㎝미만 | 31 41㎝미만 | 41 5...",
"표 한 줄 뭉침 (25개 단어): | 조 경 공 보 통 인 부 | 인 인 | 2 1 | 85 | 68 | 54 | 42 | 30 |...",
"표 한 줄 뭉침 (16개 단어): | 구 분 | 단 위 | 수 량 | 시공량(주) | | | | |...",
"표 한 줄 뭉침 (20개 단어): | 보 통 인 부 | 인 | 1 | 33 | 25 | 17 | 13 | 10 |...",
"표 한 줄 뭉침 (22개 단어): | 구 분 | 규격 | | 단 위 | 수 량 | | | | 시공량(식재면적 ㎡) | | | |...",
"표 한 줄 뭉침 (19개 단어): | | | | | 소형장비 | | 중형장비 | 대형장비 | 소형장비 | | 중형장비 | 대형장비 ...",
"표 한 줄 뭉침 (24개 단어): | 보 통 인 부 | | | 인 | 1 | | 1 | 1 | 700 | | 1,100 | 2,200 ...",
"표 한 줄 뭉침 (28개 단어): | 물탱크( 살수차) 물탱크( 살수차) 물탱크( 살수차) | 1,800L 3,800L 5,500~6,500L...",
"표 한 줄 뭉침 (30개 단어): | 비 고 | - 이동거리가 5㎞를 초과하면 5㎞마다 다음을 가산한다. 구 분 1,800 3,800 5,...",
"표 한 줄 뭉침 (18개 단어): | | | 구 분 | | | 1,800 | | 3,800 | | 5,500~6,500 | ...",
"표 한 줄 뭉침 (16개 단어): | | | 물탱크(살수차) | | | 0.07h/100㎡ | | | | 0.04h/100㎡ | ...",
"표 한 줄 뭉침 (17개 단어): | 구 분 | | | 단 위 | | 수 량 | | 시공량(㎡) | |...",
"표 한 줄 뭉침 (26개 단어): | 배 부 식 | 특 별 인 부 보 통 인 부 | | 인 인 | | 3 1 | | 3,300 | |...",
"표 한 줄 뭉침 (24개 단어): | 핸드가이드식 | 특 별 인 부 보 통 인 부 | | 인 인 | | 1 1 | | 4,000 | |...",
"표 한 줄 뭉침 (47개 단어): | 비 고 | - 보행자 보호 등 돌튐방지 조치를 위해 그물막이 필요한 경우 현장조건을 고려하여 보통인부 2...",
"표 한 줄 뭉침 (16개 단어): | | | 구 분 | | 연1회 | | 연2회 | | 연3회 이상 |...",
"표 한 줄 뭉침 (15개 단어): | | | 시공량 할증율 | | -30% | | -20% | | - |...",
"표 한 줄 뭉침 (15개 단어): | 구 분 | | | 단 위 | 수 량 | | 시공량(㎡) |...",
"표 한 줄 뭉침 (21개 단어): | 특 별 인 부 보 통 인 부 | | | 인 인 | 3 1 | | 2,500 |...",
"표 한 줄 뭉침 (57개 단어): | 비 고 | - 보행자 보호 등 돌튐방지 조치를 위해 그물막이 필요한 경우 현장조건을 고려하여 보통인부 2..."
]
},
{
"key": "CW-01228",
"section": "1-2-8",
"name": "인력관수",
"chapter": "유지관리_제1장",
"pdf_pages": [
918
],
"header_separation": "⚠ 앞 문장과 경계 붙음 (줄바꿈 누락)",
"preceding_context": "- ② 설치폭은 30cm~45cm를 설치하는 기준이다.",
"flaws_detected": [
"표 한 줄 뭉침 (16개 단어): | 구 분 | 단 위 | 수 량 | 시공량(주) | | | | |...",
"표 한 줄 뭉침 (20개 단어): | 보 통 인 부 | 인 | 1 | 33 | 25 | 17 | 13 | 10 |...",
"표 한 줄 뭉침 (22개 단어): | 구 분 | 규격 | | 단 위 | 수 량 | | | | 시공량(식재면적 ㎡) | | | |...",
"표 한 줄 뭉침 (19개 단어): | | | | | 소형장비 | | 중형장비 | 대형장비 | 소형장비 | | 중형장비 | 대형장비 ...",
"표 한 줄 뭉침 (24개 단어): | 보 통 인 부 | | | 인 | 1 | | 1 | 1 | 700 | | 1,100 | 2,200 ...",
"표 한 줄 뭉침 (28개 단어): | 물탱크( 살수차) 물탱크( 살수차) 물탱크( 살수차) | 1,800L 3,800L 5,500~6,500L...",
"표 한 줄 뭉침 (30개 단어): | 비 고 | - 이동거리가 5㎞를 초과하면 5㎞마다 다음을 가산한다. 구 분 1,800 3,800 5,...",
"표 한 줄 뭉침 (18개 단어): | | | 구 분 | | | 1,800 | | 3,800 | | 5,500~6,500 | ...",
"표 한 줄 뭉침 (16개 단어): | | | 물탱크(살수차) | | | 0.07h/100㎡ | | | | 0.04h/100㎡ | ...",
"표 한 줄 뭉침 (17개 단어): | 구 분 | | | 단 위 | | 수 량 | | 시공량(㎡) | |...",
"표 한 줄 뭉침 (26개 단어): | 배 부 식 | 특 별 인 부 보 통 인 부 | | 인 인 | | 3 1 | | 3,300 | |...",
"표 한 줄 뭉침 (24개 단어): | 핸드가이드식 | 특 별 인 부 보 통 인 부 | | 인 인 | | 1 1 | | 4,000 | |...",
"표 한 줄 뭉침 (47개 단어): | 비 고 | - 보행자 보호 등 돌튐방지 조치를 위해 그물막이 필요한 경우 현장조건을 고려하여 보통인부 2...",
"표 한 줄 뭉침 (16개 단어): | | | 구 분 | | 연1회 | | 연2회 | | 연3회 이상 |...",
"표 한 줄 뭉침 (15개 단어): | | | 시공량 할증율 | | -30% | | -20% | | - |...",
"표 한 줄 뭉침 (15개 단어): | 구 분 | | | 단 위 | 수 량 | | 시공량(㎡) |...",
"표 한 줄 뭉침 (21개 단어): | 특 별 인 부 보 통 인 부 | | | 인 인 | 3 1 | | 2,500 |...",
"표 한 줄 뭉침 (57개 단어): | 비 고 | - 보행자 보호 등 돌튐방지 조치를 위해 그물막이 필요한 경우 현장조건을 고려하여 보통인부 2...",
"표 한 줄 뭉침 (18개 단어): | 구 분 | | 단 위 | 수 량 | 시공량(주) | | | | | |...",
"표 한 줄 뭉침 (17개 단어): | | | | | 11㎝ | 1121㎝ | 2131㎝ | 3141㎝ | 4151㎝ | 51㎝ ...",
"표 한 줄 뭉침 (17개 단어): | | | | | 미만 | 미만 | 미만 | 미만 | 미만 | 이상 |...",
"표 한 줄 뭉침 (32개 단어): | 환 상 시 비 | 조 경 공 보 통 인 부 | 인 인 | 2 1 | 76 | 61 | 51 | 44 | ...",
"표 한 줄 뭉침 (29개 단어): | 방사형시비 | 조 경 공 보 통 인 부 | 인 인 | 2 1 | 100 | 82 | 69 | 59 | 5...",
"표 한 줄 뭉침 (17개 단어): | 조 경 공 보 통 인 부 | 인 인 | 2 1 | 300 |...",
"표 한 줄 뭉침 (15개 단어): | 구 분 | 규 격 | 단 위 | 수 량 | 시공량(㎡) |...",
"표 한 줄 뭉침 (18개 단어): | 조 경 공 보 통 인 부 | | 인 인 | 2 1 | 22,500 |...",
"표 한 줄 뭉침 (15개 단어): | 구 분 | 규 격 | 단 위 | 수 량 | 시공량() |..."
]
},
{
"key": "CW-01229",
"section": "1-2-9",
"name": "살수차관수",
"chapter": "유지관리_제1장",
"pdf_pages": [
918
],
"header_separation": "⚠ 앞 문장과 경계 붙음 (줄바꿈 누락)",
"preceding_context": "[주] 본 품은 인력에 의한 교목관수 기준이다.",
"flaws_detected": [
"표 한 줄 뭉침 (22개 단어): | 구 분 | 규격 | | 단 위 | 수 량 | | | | 시공량(식재면적 ㎡) | | | |...",
"표 한 줄 뭉침 (19개 단어): | | | | | 소형장비 | | 중형장비 | 대형장비 | 소형장비 | | 중형장비 | 대형장비 ...",
"표 한 줄 뭉침 (24개 단어): | 보 통 인 부 | | | 인 | 1 | | 1 | 1 | 700 | | 1,100 | 2,200 ...",
"표 한 줄 뭉침 (28개 단어): | 물탱크( 살수차) 물탱크( 살수차) 물탱크( 살수차) | 1,800L 3,800L 5,500~6,500L...",
"표 한 줄 뭉침 (30개 단어): | 비 고 | - 이동거리가 5㎞를 초과하면 5㎞마다 다음을 가산한다. 구 분 1,800 3,800 5,...",
"표 한 줄 뭉침 (18개 단어): | | | 구 분 | | | 1,800 | | 3,800 | | 5,500~6,500 | ...",
"표 한 줄 뭉침 (16개 단어): | | | 물탱크(살수차) | | | 0.07h/100㎡ | | | | 0.04h/100㎡ | ...",
"표 한 줄 뭉침 (17개 단어): | 구 분 | | | 단 위 | | 수 량 | | 시공량(㎡) | |...",
"표 한 줄 뭉침 (26개 단어): | 배 부 식 | 특 별 인 부 보 통 인 부 | | 인 인 | | 3 1 | | 3,300 | |...",
"표 한 줄 뭉침 (24개 단어): | 핸드가이드식 | 특 별 인 부 보 통 인 부 | | 인 인 | | 1 1 | | 4,000 | |...",
"표 한 줄 뭉침 (47개 단어): | 비 고 | - 보행자 보호 등 돌튐방지 조치를 위해 그물막이 필요한 경우 현장조건을 고려하여 보통인부 2...",
"표 한 줄 뭉침 (16개 단어): | | | 구 분 | | 연1회 | | 연2회 | | 연3회 이상 |...",
"표 한 줄 뭉침 (15개 단어): | | | 시공량 할증율 | | -30% | | -20% | | - |...",
"표 한 줄 뭉침 (15개 단어): | 구 분 | | | 단 위 | 수 량 | | 시공량(㎡) |...",
"표 한 줄 뭉침 (21개 단어): | 특 별 인 부 보 통 인 부 | | | 인 인 | 3 1 | | 2,500 |...",
"표 한 줄 뭉침 (57개 단어): | 비 고 | - 보행자 보호 등 돌튐방지 조치를 위해 그물막이 필요한 경우 현장조건을 고려하여 보통인부 2...",
"표 한 줄 뭉침 (18개 단어): | 구 분 | | 단 위 | 수 량 | 시공량(주) | | | | | |...",
"표 한 줄 뭉침 (17개 단어): | | | | | 11㎝ | 1121㎝ | 2131㎝ | 3141㎝ | 4151㎝ | 51㎝ ...",
"표 한 줄 뭉침 (17개 단어): | | | | | 미만 | 미만 | 미만 | 미만 | 미만 | 이상 |...",
"표 한 줄 뭉침 (32개 단어): | 환 상 시 비 | 조 경 공 보 통 인 부 | 인 인 | 2 1 | 76 | 61 | 51 | 44 | ...",
"표 한 줄 뭉침 (29개 단어): | 방사형시비 | 조 경 공 보 통 인 부 | 인 인 | 2 1 | 100 | 82 | 69 | 59 | 5...",
"표 한 줄 뭉침 (17개 단어): | 조 경 공 보 통 인 부 | 인 인 | 2 1 | 300 |...",
"표 한 줄 뭉침 (15개 단어): | 구 분 | 규 격 | 단 위 | 수 량 | 시공량(㎡) |...",
"표 한 줄 뭉침 (18개 단어): | 조 경 공 보 통 인 부 | | 인 인 | 2 1 | 22,500 |...",
"표 한 줄 뭉침 (15개 단어): | 구 분 | 규 격 | 단 위 | 수 량 | 시공량() |...",
"표 한 줄 뭉침 (18개 단어): | 조 경 공 보 통 인 부 | | 인 인 | 1 1 | 2,600 |..."
]
},
{
"key": "CW-01325",
"section": "3-1-1",
"name": "콘크리트구조물 헐기(인력)",
"chapter": "유지관리_제3장",
"pdf_pages": [
967
],
"header_separation": "⚠ 앞 문장과 경계 붙음 (줄바꿈 누락)",
"preceding_context": "건 축제3장3-1 구조물 철거공사",
"flaws_detected": [
"표 한 줄 뭉침 (16개 단어): | 구 분 | 규 격 | 단 위 | 수 량 | 시공량(㎥) | |...",
"표 한 줄 뭉침 (18개 단어): | 착 암 공 보 통 인 부 | | | 2 1 | 2.7 | 2.3 |...",
"표 한 줄 뭉침 (16개 단어): | 구 분 | | 규 격 | 단 위 | 수 량 | 시공량(㎥) |...",
"표 한 줄 뭉침 (26개 단어): | 장 애 물 미 제 거 | 특 별 인 부 보 통 인 부 | | 인 인 | 2 1 | 50 |...",
"표 한 줄 뭉침 (17개 단어): | | 굴 착 기 + 압 쇄 기 | 1.0㎥ | 대 | 1 | |...",
"표 한 줄 뭉침 (30개 단어): | 장 애 물 제 거 | 용 접 공 특 별 인 부 보 통 인 부 | | 인 인 인 | 1 2 1 | 45 ...",
"표 한 줄 뭉침 (17개 단어): | | 굴 착 기 + 압 쇄 기 | 1.0㎥ | 대 | 1 | |...",
"표 한 줄 뭉침 (17개 단어): | 용 접 공 보 통 인 부 | 인 인 | 3 2 | 1.4 |...",
"표 한 줄 뭉침 (15개 단어): | 구 분 | 규 격 | 단 위 | 수 량 | 시공량(ton) |...",
"표 한 줄 뭉침 (19개 단어): | 특 별 인 부 보 통 인 부 | | 인 인 | 2 1 | 22 |..."
]
},
{
"key": "CW-01326",
"section": "3-1-2",
"name": "콘크리트구조물 헐기(기계)",
"chapter": "유지관리_제3장",
"pdf_pages": [
967
],
"header_separation": "⚠ 앞 문장과 경계 붙음 (줄바꿈 누락)",
"preceding_context": "- ④ 잡재료비(치즐 등)는 인력품의 1%로 계상한다.",
"flaws_detected": [
"표 한 줄 뭉침 (16개 단어): | 구 분 | | 규 격 | 단 위 | 수 량 | 시공량(㎥) |...",
"표 한 줄 뭉침 (26개 단어): | 장 애 물 미 제 거 | 특 별 인 부 보 통 인 부 | | 인 인 | 2 1 | 50 |...",
"표 한 줄 뭉침 (17개 단어): | | 굴 착 기 + 압 쇄 기 | 1.0㎥ | 대 | 1 | |...",
"표 한 줄 뭉침 (30개 단어): | 장 애 물 제 거 | 용 접 공 특 별 인 부 보 통 인 부 | | 인 인 인 | 1 2 1 | 45 ...",
"표 한 줄 뭉침 (17개 단어): | | 굴 착 기 + 압 쇄 기 | 1.0㎥ | 대 | 1 | |...",
"표 한 줄 뭉침 (17개 단어): | 용 접 공 보 통 인 부 | 인 인 | 3 2 | 1.4 |...",
"표 한 줄 뭉침 (15개 단어): | 구 분 | 규 격 | 단 위 | 수 량 | 시공량(ton) |...",
"표 한 줄 뭉침 (19개 단어): | 특 별 인 부 보 통 인 부 | | 인 인 | 2 1 | 22 |..."
]
},
{
"key": "CW-01327",
"section": "3-1-3",
"name": "철골재 철거(인력)",
"chapter": "유지관리_제3장",
"pdf_pages": [
968
],
"header_separation": "⚠ 앞 문장과 경계 붙음 (줄바꿈 누락)",
"preceding_context": "150기압으로 압축용기에 넣어 사용하는 것을 기준한다.",
"flaws_detected": [
"표 한 줄 뭉침 (17개 단어): | 용 접 공 보 통 인 부 | 인 인 | 3 2 | 1.4 |...",
"표 한 줄 뭉침 (15개 단어): | 구 분 | 규 격 | 단 위 | 수 량 | 시공량(ton) |...",
"표 한 줄 뭉침 (19개 단어): | 특 별 인 부 보 통 인 부 | | 인 인 | 2 1 | 22 |..."
]
},
{
"key": "CW-01328",
"section": "3-1-4",
"name": "철골재 철거(기계)",
"chapter": "유지관리_제3장",
"pdf_pages": [
968
],
"header_separation": "⚠ 앞 문장과 경계 붙음 (줄바꿈 누락)",
"preceding_context": "150기압으로 압축용기에 넣어 사용하는 것을 기준한다.",
"flaws_detected": [
"표 한 줄 뭉침 (15개 단어): | 구 분 | 규 격 | 단 위 | 수 량 | 시공량(ton) |...",
"표 한 줄 뭉침 (19개 단어): | 특 별 인 부 보 통 인 부 | | 인 인 | 2 1 | 22 |...",
"표 한 줄 뭉침 (33개 단어): | 착 암 공 보 통 인 부 소 형 브 레 이 커 공 기 압 축 기 | 1.3㎥/min 3.5㎥/min | ..."
]
},
{
"key": "CW-01329",
"section": "3-1-5",
"name": "석축 헐기(인력)",
"chapter": "유지관리_제3장",
"pdf_pages": [
56,
969
],
"header_separation": "⚠ 앞 문장과 경계 붙음 (줄바꿈 누락)",
"preceding_context": " 기계경비는 인력품의 4%로 계상한다.유지관리부문912",
"flaws_detected": [
"표 한 줄 뭉침 (33개 단어): | 착 암 공 보 통 인 부 소 형 브 레 이 커 공 기 압 축 기 | 1.3㎥/min 3.5㎥/min | ..."
]
}
]
@@ -0,0 +1,114 @@
import sys, os, re, json
import fitz # PyMuPDF
sys.stdout.reconfigure(encoding='utf-8')
# Load target items
with open('scratch/located_const_sections.json', 'r', encoding='utf-8') as f:
located_items = json.load(f)
md_map = {
"공통_제1장": "resources/knowledge/original/원가계산/건설공사_표준품셈/01_공통부문/제1장_적용기준.md",
"공통_제3장": "resources/knowledge/original/원가계산/건설공사_표준품셈/01_공통부문/제3장_토공사.md",
"공통_제4장": "resources/knowledge/original/원가계산/건설공사_표준품셈/01_공통부문/제4장_조경공사.md",
"공통_제6장": "resources/knowledge/original/원가계산/건설공사_표준품셈/01_공통부문/제6장_철근콘크리트공사.md",
"토목_제1장": "resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제1장_도로포장공사.md",
"토목_제6장": "resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제6장_관부설및접합공사.md",
"건축_제9장": "resources/knowledge/original/원가계산/건설공사_표준품셈/03_건축부문/제9장_미장공사.md",
"유지관리_제1장": "resources/knowledge/original/원가계산/건설공사_표준품셈/05_유지관리부문/제1장_공통.md",
"유지관리_제3장": "resources/knowledge/original/원가계산/건설공사_표준품셈/05_유지관리부문/제3장_건축.md",
}
md_texts = {}
for k, p in md_map.items():
if os.path.exists(p):
with open(p, 'r', encoding='utf-8') as f:
md_texts[k] = f.read()
pdf_path = "resources/knowledge/original/원가계산/건설공사_표준품셈/2026년_건설공사_표준품셈.pdf"
doc = fitz.open(pdf_path)
audit_details = []
for item in located_items:
k = item['key']
num = item['number']
name = item['name']
pn = item['path_name']
pdf_pages = item.get('pdf_pages', [])
# Exclude chapter 8
if "공통부문 건설기계" in pn:
continue
# Determine chapter
md_key = None
if "공통부문 적용기준" in pn: md_key = "공통_제1장"
elif "공통부문 토공사" in pn: md_key = "공통_제3장"
elif "공통부문 조경공사" in pn: md_key = "공통_제4장"
elif "공통부문 철근콘크리트공사" in pn: md_key = "공통_제6장"
elif "토목부문 도로포장공사" in pn: md_key = "토목_제1장"
elif "토목부문 관부설" in pn: md_key = "토목_제6장"
elif "건축부문 미장공사" in pn: md_key = "건축_제9장"
elif "유지관리부문 공 통" in pn: md_key = "유지관리_제1장"
elif "유지관리부문 건 축" in pn: md_key = "유지관리_제3장"
raw_md = md_texts.get(md_key, "")
# Find how this section starts in raw_md
# Look for num in raw_md
escaped_num = re.escape(num)
# Search with regex
m = re.search(rf'([^\n]{{0,50}})({escaped_num}\s+[^\n]+)', raw_md)
header_status = "정상 분리"
preceding_context = ""
if m:
preceding = m.group(1)
header_line = m.group(2)
if preceding.strip():
header_status = "⚠ 앞 문장과 경계 붙음 (줄바꿈 누락)"
preceding_context = preceding.strip()[-30:]
else:
# Check if number appears at all
if num in raw_md:
header_status = "본문 중 인라인 출현"
else:
header_status = "❌ MD 본문 완전 부재"
# Find PDF text for relevant pages
actual_pages = [p for p in pdf_pages if p > 50]
pdf_text = ""
for p in actual_pages:
pdf_text += doc[p - 1].get_text()
# Check table collapse in this section's neighborhood
# Grab 50 lines around the match
section_flaws = []
if m:
start_pos = m.start()
# look forward 3000 characters
chunk = raw_md[start_pos:start_pos + 4000]
# check collapsed table rows in chunk
for line in chunk.split('\n'):
if line.strip().startswith('|') and line.strip().endswith('|'):
tokens = line.split()
if len(tokens) >= 15:
section_flaws.append(f"표 한 줄 뭉침 ({len(tokens)}개 단어): {line.strip()[:60]}...")
if '' in line or '제1장' in line or '공통부문' in line:
if re.search(r'\d+|\d+제\d+장', line):
section_flaws.append(f"페이지 전이 찌꺼기 삽입: {line.strip()[:60]}")
audit_entry = {
"key": k,
"section": num,
"name": name,
"chapter": md_key,
"pdf_pages": actual_pages,
"header_separation": header_status,
"preceding_context": preceding_context,
"flaws_detected": section_flaws
}
audit_details.append(audit_entry)
print(f"[{k}] {num} {name}: {header_status} | flaws={len(section_flaws)}")
with open('scratch/audit_42_detailed_breakdown.json', 'w', encoding='utf-8') as f:
json.dump(audit_details, f, ensure_ascii=False, indent=2)
@@ -0,0 +1,103 @@
import os, sys, re
sys.stdout.reconfigure(encoding='utf-8')
target_files = [
# 02_토목부문
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제1장_도로포장공사.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제2장_하천공사.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제3장_터널공사.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제4장_궤도공사.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제5장_강구조공사.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제6장_관부설및접합공사.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제7장_항만공사.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제8장_지반조사.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제9장_측량.md',
# 05_유지관리부문
'resources/knowledge/original/원가계산/건설공사_표준품셈/05_유지관리부문/제1장_공통.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/05_유지관리부문/제2장_토목.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/05_유지관리부문/제3장_건축.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/05_유지관리부문/제4장_기계설비.md',
# 04_기계설비부문 제13장
'resources/knowledge/original/원가계산/건설공사_표준품셈/04_기계설비부문/제13장_플랜트설비공사.md',
]
print("=== Strict Audit of Duplicate Substantive Data Rows across 14 Target Chapters ===")
strict_candidates = []
for fpath in target_files:
fname = os.path.basename(fpath)
with open(fpath, 'r', encoding='utf-8') as f:
lines = f.readlines()
# Collect all table rows
all_rows = []
for idx, l in enumerate(lines):
line_str = l.strip()
if not (line_str.startswith('|') and line_str.endswith('|')):
continue
cells = [c.strip() for c in line_str.split('|')[1:-1]]
if all(re.match(r'^:?-+:?$', c) for c in cells if c):
continue
if cells and cells[0] in ["비고", "비 고", "", "[주]"]:
continue
if any(c in ["구 분", "구분", "품 명", "품명", "규 격", "규격", "단 위", "단위", "수 량", "수량", "직 종", "직종"] for c in cells):
continue
# Is it a substantive row?
# Must have at least one specific item name, spec, or multi-values
# Filter out generic worker rows with no spec (e.g. '| 보통인부 | 인 | 1 | |')
non_empty = [c for c in cells if c and c not in ["-", ""]]
if len(non_empty) <= 1:
continue
# Check if it's just a generic worker row
if len(non_empty) <= 3 and any(w in line_str for w in ["보통인부", "특별인부", "인부", "기계설비공", "용접공"]) and not any(re.search(r'\d+\.\d{2,}', c) for c in cells):
# Check if all numbers are small ints like 1, 2, 3
nums = re.findall(r'\b\d+(?:\.\d+)?\b', line_str)
if all(n in ["1", "2", "3", "4", "5", "10", "20"] for n in nums):
continue
all_rows.append((idx + 1, line_str, cells))
# Now check for duplicates within distance of 80 rows
for i in range(len(all_rows)):
lno_i, str_i, cells_i = all_rows[i]
for j in range(i + 1, min(len(all_rows), i + 80)):
lno_j, str_j, cells_j = all_rows[j]
# Exact match of non-trivial row
if cells_i == cells_j:
strict_candidates.append({
'file': fname,
'line_1': lno_i,
'line_2': lno_j,
'type': 'exact_duplicate_substantive',
'row': str_i[:90]
})
# Check if str_j is a squashed row containing str_i
# E.g. cells_j has multiple values per cell and covers cells_i
else:
words_i = set(re.findall(r'[가-힣A-Za-z0-9.]+', str_i))
words_j = set(re.findall(r'[가-힣A-Za-z0-9.]+', str_j))
# Remove generic tokens
clean_i = {w for w in words_i if w not in ["인부", "보통인부", "특별인부", "", "", "", "m", "", "", "ton"]}
if len(clean_i) >= 4 and clean_i.issubset(words_j) and len(words_j) > len(words_i) + 4:
strict_candidates.append({
'file': fname,
'line_1': lno_i,
'line_2': lno_j,
'type': 'squashed_superset_of_line_1',
'row_1': str_i[:80],
'row_2': str_j[:80]
})
print(f"Total strict candidates found: {len(strict_candidates)}")
for sc in strict_candidates:
print(f"\n[{sc['file']}] L{sc['line_1']} & L{sc['line_2']} - {sc['type']}")
if sc['type'] == 'exact_duplicate_substantive':
print(f" Row: {sc['row']}")
else:
print(f" Row 1: {sc['row_1']}")
print(f" Row 2: {sc['row_2']}")
@@ -0,0 +1,102 @@
import subprocess, sys, re, os
sys.stdout.reconfigure(encoding='utf-8')
# Get diff of our recent commits where we split worker rows and squashed tables
# 3cd61cad: wide criteria fix
# f354ba22: civil squashed
# 41a15e54: maint squashed
# 948dd509: mech/plant squashed
commits_to_check = ['3cd61cad', 'f354ba22', '41a15e54', '948dd509']
print("=== Checking all our restored locations for potential duplication with existing lines ===")
all_modifications = []
for c in commits_to_check:
res = subprocess.run(['git', '-c', 'core.quotepath=false', 'diff', '-U3', f'{c}~1..{c}'], capture_output=True, text=True, encoding='utf-8')
diff_text = res.stdout
file_diffs = diff_text.split('diff --git ')
for fd in file_diffs[1:]:
lines = fd.split('\n')
header = lines[0]
filename = header.split(' ')[0][2:]
if "2026년_건설공사_표준품셈.md" in filename or "resources/knowledge/technical_info" in filename:
continue
raw_hunks = re.split(r'\n@@\s+[^@]+\s+@@\n', fd)[1:]
for h_idx, h in enumerate(raw_hunks):
h_lines = h.split('\n')
del_lines = [l[1:].strip() for l in h_lines if l.startswith('-') and l[1:].strip().startswith('|')]
add_lines = [l[1:].strip() for l in h_lines if l.startswith('+') and l[1:].strip().startswith('|')]
ctx_lines = [l[1:].strip() for l in h_lines if l.startswith(' ') and l[1:].strip().startswith('|')]
all_modifications.append({
'commit': c,
'file': filename,
'del': del_lines,
'add': add_lines,
'context': ctx_lines
})
print(f"Total hunk modifications checked: {len(all_modifications)}")
# Now for each modification, inspect the file at HEAD to see if the table has DUPLICATE items
files_cache = {}
duplication_alerts = []
for mod in all_modifications:
fpath = mod['file']
if fpath not in files_cache:
with open(fpath, 'r', encoding='utf-8') as f:
files_cache[fpath] = f.readlines()
f_lines = files_cache[fpath]
# Check if added lines duplicate any other lines in the same table
for al in mod['add']:
if not al.startswith('|'):
continue
cells = [c.strip() for c in al.split('|')[1:-1]]
if all(re.match(r'^:?-+:?$', c) for c in cells if c):
continue
# Find line number of al in f_lines
for idx, fl in enumerate(f_lines):
if fl.strip() == al:
# Look 30 lines up and 30 lines down within the same table
up_bound = idx
while up_bound > 0 and f_lines[up_bound-1].strip().startswith('|'):
up_bound -= 1
down_bound = idx
while down_bound < len(f_lines)-1 and f_lines[down_bound+1].strip().startswith('|'):
down_bound += 1
table_lines = [f_lines[k].strip() for k in range(up_bound, down_bound+1)]
# Check how many times cells (or c0, c1, and key numbers) match other rows in table_lines
c0 = cells[0] if len(cells) > 0 else ""
c1 = cells[1] if len(cells) > 1 else ""
key_cells = [c for c in cells if c and c not in ["-", "", "", "", "1", "2"]]
matches = 0
for tl in table_lines:
t_cells = [c.strip() for c in tl.split('|')[1:-1]]
if t_cells == cells:
matches += 1
if matches > 1:
duplication_alerts.append({
'commit': mod['commit'],
'file': os.path.basename(fpath),
'line': idx + 1,
'added_row': al[:80],
'matches_in_table': matches
})
break
print(f"\nTotal Duplication Alerts in Restored Locations: {len(duplication_alerts)}")
for da in duplication_alerts:
print(f"[{da['file']}:L{da['line']}] (Commit {da['commit']}) appears {da['matches_in_table']} times in table!")
print(f" Row: {da['added_row']}")
+123
View File
@@ -0,0 +1,123 @@
import json
import sys
import re
from pathlib import Path
sys.stdout.reconfigure(encoding='utf-8')
# Load MD
with open("resources/knowledge/original/행정규칙/임도 품셈 적용기준 (현 산림사업 표준품셈)/첨부/(산림청고시 제2025-82호) 산림사업 표준품셈.md", "r", encoding="utf-8") as f:
md_lines = f.readlines()
# Load Master
with open('resources/data_work_item_master/work_item_master_2026-01-01.json', 'r', encoding='utf-8') as f:
master = json.load(f)
sys.path.insert(0, str(Path(__file__).parent.parent))
from scratch.enrich_tables import tables, get_chapter
# Map master tables
master_tables = {}
wi_of_table = {}
for wi in master['work_items']:
for t in wi.get('tables', []):
tid = t.get('pum_table_id')
if tid:
master_tables[tid] = t
wi_of_table[tid] = wi
for t in master.get('orphan_tables', []):
tid = t.get('pum_table_id')
if tid:
master_tables[tid] = t
# Patterns for basis in pre_context:
# e.g., "[100㎡ 당]", "[10a 당]", "[100m 당]", "[10㎥ 당]", "[본당]", "[1공당]", "[개소당]", "[1m3 당]"
# Also patterns in table header or first line
basis_pattern = re.compile(r'\[\s*([0-9\.\,]*)\s*([a-zA-Z㎡㎥㏊ha본개공mkm㎏gtonLℓ대인組]+)\s*당\s*\]')
basis_pattern2 = re.compile(r'\(단위\s*:\s*([^\)]+)\)')
basis_audit = []
for i, t in enumerate(tables):
tid = t['table_id']
chapter = t['chapter']
pre_lines = t['pre_context']
# 1. Search pre-context for basis
found_basis_qty = None
found_basis_unit = None
found_basis_src = None
# Search backwards in pre_lines
for pl in reversed(pre_lines):
m = basis_pattern.search(pl)
if m:
qty_str = m.group(1).replace(',', '').strip()
found_basis_qty = float(qty_str) if qty_str else 1.0
found_basis_unit = m.group(2).strip()
found_basis_src = f"pre_context: '{pl}'"
break
m2 = basis_pattern2.search(pl)
if m2:
u = m2.group(1).strip()
found_basis_qty = 1.0
found_basis_unit = u
found_basis_src = f"unit_parenthesis: '{pl}'"
break
# 2. Search table header cells
header_line = t['lines'][0] if t['lines'] else ""
# Look for "단위수량", "단위", etc.
# 3. Compare with master
mt = master_tables.get(tid)
if not mt:
continue
m_qty = mt.get('basis_quantity')
m_unit = mt.get('basis_unit')
pform = mt.get('pum_form')
# Check if MD has explicit basis but master is None or different
if found_basis_unit is not None:
if m_unit is None or m_qty is None:
basis_audit.append({
'table_id': tid,
'chapter': chapter,
'line': t['start_line'],
'section': t['section'],
'pum_form': pform,
'issue': 'MD_HAS_BASIS_MASTER_MISSING',
'md_qty': found_basis_qty,
'md_unit': found_basis_unit,
'md_src': found_basis_src,
'master_qty': m_qty,
'master_unit': m_unit
})
elif m_qty != found_basis_qty or m_unit != found_basis_unit:
# Check unit normalization (e.g. ㎡ vs m2)
if not (str(found_basis_qty) == str(m_qty) and found_basis_unit.replace('','m2').replace('','m3') == m_unit.replace('','m2').replace('','m3')):
basis_audit.append({
'table_id': tid,
'chapter': chapter,
'line': t['start_line'],
'section': t['section'],
'pum_form': pform,
'issue': 'BASIS_MISMATCH',
'md_qty': found_basis_qty,
'md_unit': found_basis_unit,
'md_src': found_basis_src,
'master_qty': m_qty,
'master_unit': m_unit
})
print(f"Total Basis issues identified: {len(basis_audit)}")
from collections import Counter
print("By issue type:", Counter(b['issue'] for b in basis_audit))
print("By chapter:", Counter(b['chapter'] for b in basis_audit))
print("\n--- Sample Basis Issues (First 15) ---")
for b in basis_audit[:15]:
print(f"[{b['table_id']} | Line {b['line']}] {b['chapter']} | {b['section']}")
print(f" MD: {b['md_qty']} {b['md_unit']} ({b['md_src']}) vs Master: {b['master_qty']} {b['master_unit']} (form: {b['pum_form']})")
@@ -0,0 +1,124 @@
import json
import sys
import re
from pathlib import Path
sys.stdout.reconfigure(encoding='utf-8')
# Load files
md_path = Path("resources/knowledge/original/행정규칙/임도 품셈 적용기준 (현 산림사업 표준품셈)/첨부/(산림청고시 제2025-82호) 산림사업 표준품셈.md")
with open(md_path, "r", encoding="utf-8") as f:
md_lines = f.readlines()
with open('resources/data_cost_input_value/pum_forest_2026.json', 'r', encoding='utf-8') as f:
pum_data = json.load(f)
pum_tables = pum_data.get('variables', {}).get('pum', {}).get('tables', [])
pum_map = {t['table_id']: t for t in pum_tables}
with open('resources/data_work_item_master/work_item_master_2026-01-01.json', 'r', encoding='utf-8') as f:
master = json.load(f)
master_tables = {}
for wi in master['work_items']:
for t in wi.get('tables', []):
tid = t.get('pum_table_id')
if tid:
master_tables[tid] = t
for t in master.get('orphan_tables', []):
tid = t.get('pum_table_id')
if tid:
master_tables[tid] = t
# Extract all markdown tables from MD
md_tables = []
current_table = []
start_line = -1
for idx, line in enumerate(md_lines):
line_str = line.strip()
if line_str.startswith('|') and line_str.endswith('|'):
if not current_table:
start_line = idx + 1
current_table.append((idx + 1, line))
else:
if current_table:
has_sep = any(re.match(r'^\|(?:\s*:?-+:?\s*\|)+$', l.strip()) for _, l in current_table)
if has_sep:
md_tables.append({
'start_line': start_line,
'end_line': current_table[-1][0],
'lines': [l for _, l in current_table]
})
current_table = []
if current_table:
has_sep = any(re.match(r'^\|(?:\s*:?-+:?\s*\|)+$', l.strip()) for _, l in current_table)
if has_sep:
md_tables.append({
'start_line': start_line,
'end_line': current_table[-1][0],
'lines': [l for _, l in current_table]
})
print(f"MD tables: {len(md_tables)}, pum_forest tables: {len(pum_tables)}")
# Function to parse table rows from md lines
def parse_md_cells(lines):
rows = []
for l in lines:
l_str = l.strip()
if re.match(r'^\|(?:\s*:?-+:?\s*\|)+$', l_str):
continue
cells = [c.strip() for c in l_str.split('|')[1:-1]]
rows.append(cells)
return rows
# Compare each table's cells between MD and pum_forest
cell_mismatches = []
row_count_mismatches = []
for i, mdt in enumerate(md_tables):
tid = f"F{i+1:04d}"
pt = pum_map.get(tid)
if not pt:
continue
md_rows = parse_md_cells(mdt['lines'])
# In pum_tables, headers is first row(s), rows is data rows
pt_headers = pt.get('headers', [])
pt_data_rows = pt.get('rows', [])
pt_all_rows = []
if pt_headers:
pt_all_rows.append(pt_headers)
pt_all_rows.extend(pt_data_rows)
# Check row counts
if len(md_rows) != len(pt_all_rows):
row_count_mismatches.append((tid, mdt['start_line'], len(md_rows), len(pt_all_rows)))
continue
for r_idx, (mr, pr) in enumerate(zip(md_rows, pt_all_rows)):
if len(mr) != len(pr):
cell_mismatches.append((tid, mdt['start_line'] + r_idx, f"Col count diff: md={len(mr)} vs pt={len(pr)}"))
continue
for c_idx, (mc, pc) in enumerate(zip(mr, pr)):
# Normalize strings for comparison
# remove spaces, trailing .0, etc.
mc_norm = mc.replace(" ", "").replace(",", "")
pc_norm = str(pc).replace(" ", "").replace(",", "")
if mc_norm != pc_norm:
# Check numeric equivalence
try:
if float(mc_norm) == float(pc_norm):
continue
except:
pass
cell_mismatches.append((tid, mdt['start_line'] + r_idx, f"Cell[{r_idx},{c_idx}] diff: md='{mc}' vs pt='{pc}'"))
print(f"Row count mismatches: {len(row_count_mismatches)}")
for r in row_count_mismatches[:10]:
print(f" {r[0]} line {r[1]}: md rows={r[2]} vs pt rows={r[3]}")
print(f"\nCell mismatches: {len(cell_mismatches)}")
for cm in cell_mismatches[:20]:
print(f" {cm[0]} line {cm[1]}: {cm[2]}")
+65
View File
@@ -0,0 +1,65 @@
import json
import sys
import re
from pathlib import Path
sys.stdout.reconfigure(encoding='utf-8')
# 1. Load coef_2026.json
coef_path = Path('resources/data_cost_input_value/coef_2026.json')
with open(coef_path, 'r', encoding='utf-8') as f:
coef = json.load(f)
# 2. Check soil conversion factors in MD (Table F0004, Line 709~727)
# MD Table 4 has: 종별, L, C
with open('resources/data_cost_input_value/pum_forest_2026.json', 'r', encoding='utf-8') as f:
pum = json.load(f)
pum_tables = {t['table_id']: t for t in pum.get('variables', {}).get('pum', {}).get('tables', [])}
print("=== 기초값 대조: 토량환산계수 L, C (F0004) ===")
f0004 = pum_tables.get('F0004')
if f0004:
print(f"F0004 headers: {f0004['headers']}")
print("F0004 rows count:", len(f0004['rows']))
for r in f0004['rows'][:5]:
print(" ", r)
coef_vars = coef.get('variables', {})
soil_L = coef_vars.get('coef_soil_L', {})
soil_C = coef_vars.get('coef_soil_C', {})
print(f"coef_2026 soil_L entries: {len(soil_L)}, soil_C entries: {len(soil_C)}")
for k in list(soil_L.keys())[:5]:
print(f" Soil: {k} -> L={soil_L.get(k)}, C={soil_C.get(k)}")
# Check discrepancies between F0004 rows and coef_soil_L/C
soil_mismatches = []
for r in f0004['rows']:
name = r[0].strip()
l_val_str = r[1].strip()
c_val_str = r[2].strip()
# Try match with soil_L
matched_l = None
for k, v in soil_L.items():
if k in name or name in k:
matched_l = v
break
# print diff if any
print(f"MD [{name}]: L={l_val_str}, C={c_val_str} | coef_2026 match: {matched_l}")
print("\n=== 기초값 대조: 자재할증률 (F0008, F0009, F0010, F0011, F0012, F0013) ===")
mat_surcharges = coef_vars.get('surcharge_mat', {})
print(f"coef_2026 surcharge_mat count: {len(mat_surcharges)}")
for k, v in list(mat_surcharges.items())[:10]:
print(f" {k}: {v}")
print("\n=== 기초값 대조: 공구손료 및 경장비 (rate_tool) ===")
rate_tool = coef_vars.get('rate_tool', {})
print(f"coef_2026 rate_tool: {rate_tool}")
print("\n=== 기초값 대조: 노임할증 (surcharge_labor) ===")
surcharge_labor = coef_vars.get('surcharge_labor', {})
print(f"coef_2026 surcharge_labor entries: {len(surcharge_labor)}")
for k, v in list(surcharge_labor.items())[:10]:
print(f" {k}: {v}")
@@ -0,0 +1,77 @@
import json
import sys
import re
from pathlib import Path
sys.stdout.reconfigure(encoding='utf-8')
# Load enriched tables
sys.path.insert(0, '.')
from scratch.enrich_tables import tables
print("=== 원문 전체에서 '분모가 둘'이거나 복합 단위인 표 전수 조사 ===")
complex_units = []
# Pattern to find parenthesis units in pre_context or headers
unit_regex = re.compile(r'[\(\[\{]\s*단위\s*[:\=]?\s*([^\)\]\}]+)[\)\]\}]')
for t in tables:
tid = t['table_id']
pre_lines = t['pre_context']
header_line = t['lines'][0] if t['lines'] else ""
found_u = None
src_line = None
for pl in reversed(pre_lines):
m = unit_regex.search(pl)
if m:
found_u = m.group(1).strip()
src_line = pl
break
if not found_u:
m_h = unit_regex.search(header_line)
if m_h:
found_u = m_h.group(1).strip()
src_line = header_line
if found_u:
# Check if it has multiple slashes or complex compound units
# e.g. "인/1일 1km당", "㎥/1인/1일", "인/ha", "인/개당", "㎥/㎡당", "100본/ha당", "/일,대"
is_double_denom = False
# double denominator pattern: has two '/' or '/일' and 'km당' etc.
slash_count = found_u.count('/')
has_per_day_and_unit = ('' in found_u and any(k in found_u for k in ['km', 'm', 'ha', '', '', '', '']))
has_compound_slash = slash_count >= 2 or ('/' in found_u and '' in found_u and any(k in found_u for k in ['1일', '일,', '일당', '인당']))
if slash_count >= 2 or has_per_day_and_unit or '㎥/㎡' in found_u or '인/10개' in found_u or '인/100m' in found_u:
is_double_denom = True
complex_units.append({
'table_id': tid,
'chapter': t['chapter'],
'section': t['section'],
'line': t['start_line'],
'raw_unit': found_u,
'src_line': src_line,
'slash_count': slash_count,
'is_double_denom': is_double_denom
})
print(f"Total tables with parenthesis units found: {len(complex_units)}")
double_denoms = [u for u in complex_units if u['is_double_denom']]
print(f"Total tables with '분모가 둘인 꼴 (복합 분모)' : {len(double_denoms)}\n")
print("--- [분모가 둘인 꼴 전수 명세] ---")
for idx, d in enumerate(double_denoms):
print(f"{idx+1:2d}. [{d['table_id']} | line {d['line']}] {d['section']}")
print(f" 원문 단위 표기: '{d['raw_unit']}' (출처: {d['src_line']})")
print("\n--- [슬래시 1개인 단순 몫 단위(인/ha, 인/개 등) 표들] ---")
single_slash = [u for u in complex_units if not u['is_double_denom'] and u['slash_count'] == 1]
print(f"Total single slash units: {len(single_slash)}개 (sample 15):")
for s in single_slash[:15]:
print(f" [{s['table_id']}] {s['section']} -> '{s['raw_unit']}'")
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,140 @@
import sys, os, json, re
import fitz # PyMuPDF
sys.stdout.reconfigure(encoding='utf-8')
pdf_path = "resources/knowledge/original/원가계산/건설공사_표준품셈/2026년_건설공사_표준품셈.pdf"
doc = fitz.open(pdf_path)
# MD file mapping for chapters
md_map = {
"공통_제1장": "resources/knowledge/original/원가계산/건설공사_표준품셈/01_공통부문/제1장_적용기준.md",
"공통_제3장": "resources/knowledge/original/원가계산/건설공사_표준품셈/01_공통부문/제3장_토공사.md",
"공통_제4장": "resources/knowledge/original/원가계산/건설공사_표준품셈/01_공통부문/제4장_조경공사.md",
"공통_제6장": "resources/knowledge/original/원가계산/건설공사_표준품셈/01_공통부문/제6장_철근콘크리트공사.md",
"토목_제1장": "resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제1장_도로포장공사.md",
"토목_제6장": "resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제6장_관부설및접합공사.md",
"건축_제9장": "resources/knowledge/original/원가계산/건설공사_표준품셈/03_건축부문/제9장_미장공사.md",
"유지관리_제1장": "resources/knowledge/original/원가계산/건설공사_표준품셈/05_유지관리부문/제1장_공통.md",
"유지관리_제3장": "resources/knowledge/original/원가계산/건설공사_표준품셈/05_유지관리부문/제3장_건축.md",
}
# Read MD contents into memory
md_contents = {}
for k, p in md_map.items():
if os.path.exists(p):
with open(p, 'r', encoding='utf-8') as f:
md_contents[k] = f.read()
else:
print(f"MD missing: {p}")
with open('scratch/located_const_sections.json', 'r', encoding='utf-8') as f:
located_items = json.load(f)
print(f"Loaded located items: {len(located_items)}")
results = []
for item in located_items:
k = item['key']
num = item['number']
name = item['name']
pn = item['path_name']
pdf_pages = item.get('pdf_pages', [])
# Exclude chapter 8 (already audited by codex)
if "공통부문 건설기계" in pn:
continue
print(f"\n==================================================")
print(f"Auditing: {k} | {num} {name} | {pn}")
print(f"PDF Pages: {pdf_pages}")
# Determine MD chapter
md_key = None
if "공통부문 적용기준" in pn:
md_key = "공통_제1장"
elif "공통부문 토공사" in pn:
md_key = "공통_제3장"
elif "공통부문 조경공사" in pn:
md_key = "공통_제4장"
elif "공통부문 철근콘크리트공사" in pn:
md_key = "공통_제6장"
elif "토목부문 도로포장공사" in pn:
md_key = "토목_제1장"
elif "토목부문 관부설" in pn:
md_key = "토목_제6장"
elif "건축부문 미장공사" in pn:
md_key = "건축_제9장"
elif "유지관리부문 공 통" in pn:
md_key = "유지관리_제1장"
elif "유지관리부문 건 축" in pn:
md_key = "유지관리_제3장"
md_text = md_contents.get(md_key, "")
# Extract MD section chunk
# Usually in MD, sections start with "### 1-2-2" or "## 1-2-2" or "1-2-2."
escaped_num = re.escape(num)
sec_pattern = re.compile(rf"(^|\n)(#+\s+{escaped_num}[\.\s].*?)(?=\n#+\s+\d+-\d+-\d+[\.\s]|\Z)", re.DOTALL)
m = sec_pattern.search(md_text)
if not m:
# try without leading hash
sec_pattern2 = re.compile(rf"(^|\n)({escaped_num}[\.\s].*?)(?=\n\d+-\d+-\d+[\.\s]|\Z)", re.DOTALL)
m = sec_pattern2.search(md_text)
md_sec_text = m.group(2) if m else ""
if not md_sec_text:
print(f"⚠ Warning: Could not isolate section in MD: {num}")
else:
print(f"MD Section found (len={len(md_sec_text)} chars)")
# Extract PDF page text
pdf_text = ""
# Use the relevant page (typically the 2nd match if first was toc/index, or the page > 50)
actual_pages = [p for p in pdf_pages if p > 50]
for p in actual_pages:
pdf_text += f"\n--- PDF Page {p} ---\n" + doc[p - 1].get_text()
# Check 8 branches
audit = {
"work_item_key": k,
"section_number": num,
"section_name": name,
"path_name": pn,
"md_file": md_map.get(md_key),
"pdf_pages": actual_pages,
"branch1_basis_unit": {"status": "ok", "notes": []},
"branch2_multilevel_header": {"status": "ok", "notes": []},
"branch3_notes_boundary": {"status": "ok", "notes": []},
"branch4_page_break_row_merge": {"status": "ok", "notes": []},
"branch5_cell_values": {"status": "ok", "notes": []},
"branch6_missing_sections": {"status": "ok", "notes": []},
"branch7_notes_formula_boundary": {"status": "ok", "notes": []},
"branch8_hidden_materials_ref": {"status": "ok", "notes": []}
}
# 1. Check if section exists in MD
if not md_sec_text:
audit["branch6_missing_sections"]["status"] = "missing_in_md"
audit["branch6_missing_sections"]["notes"].append(f"Section {num} not found in MD {md_key}")
else:
# Check notes [주]
pdf_notes = re.findall(r"\[주\]|\[\s*주\s*\]|[①-⑩]", pdf_text)
md_notes = re.findall(r"\[주\]|\[\s*주\s*\]|[①-⑩]", md_sec_text)
if len(pdf_notes) > 0 and len(md_notes) == 0:
audit["branch3_notes_boundary"]["status"] = "suspicious_notes_missing"
audit["branch3_notes_boundary"]["notes"].append(f"PDF has {len(pdf_notes)} note markers, MD has 0")
# Check units in PDF vs MD
units_in_pdf = re.findall(r"\((?:단위\s*:\s*)?([^\)]+당|[^\)]+)\)", pdf_text)
# Check table presence
if "|" in pdf_text and "|" not in md_sec_text:
audit["branch4_page_break_row_merge"]["status"] = "table_flattened_to_text"
audit["branch4_page_break_row_merge"]["notes"].append("Table in PDF not rendered as markdown table")
results.append(audit)
with open('scratch/audit_const_linked_results.json', 'w', encoding='utf-8') as f:
json.dump(results, f, ensure_ascii=False, indent=2)
print(f"\nAudit completed for {len(results)} linked non-machine sections.")
@@ -0,0 +1,51 @@
import glob, re, os, sys, fitz
sys.stdout.reconfigure(encoding='utf-8')
pdf_path = 'resources/knowledge/original/원가계산/건설공사_표준품셈/2026년_건설공사_표준품셈.pdf'
doc = fitz.open(pdf_path)
# Ditot / remarks targets
target_sections = [
('제4장_궤도공사.md', '4-5-2', 205),
('제7장_항만공사.md', '7-1-2', 29),
('제7장_항만공사.md', '7-1-4', 86),
('제8장_지반조사.md', '8-4-2', 271),
('제9장_측량.md', '9-3-1', 703),
('제9장_측량.md', '9-5-4', 2701),
('제13장_플랜트설비공사.md', '13-4-3', 1544),
('제13장_플랜트설비공사.md', '13-5-1', 1703),
('제13장_플랜트설비공사.md', '13-5-2', 1734),
('제13장_플랜트설비공사.md', '13-7-14', 3012),
('제13장_플랜트설비공사.md', '13-7-16', 3040),
('제13장_플랜트설비공사.md', '13-7-17', 3053),
('제13장_플랜트설비공사.md', '13-7-18', 3067),
('제13장_플랜트설비공사.md', '13-7-19', 3079),
('제13장_플랜트설비공사.md', '13-7-20', 3096),
('제13장_플랜트설비공사.md', '13-7-21', 3110),
('제13장_플랜트설비공사.md', '13-10-2', 3513),
('제13장_플랜트설비공사.md', '13-10-4', 3586)
]
print("=== Checking Ditot Sections in PDF ===")
for fname, sec, line in target_sections:
# Find page in PDF
found_pages = []
for p_num in range(len(doc)):
text = doc[p_num].get_text()
if sec in text:
found_pages.append(p_num)
print(f"\n[{fname}] Section {sec} (md line {line}) -> Found in PDF page(s): {[p+1 for p in found_pages]}")
for p in found_pages[:2]:
page = doc[p]
tabs = page.find_tables()
for t_idx, tab in enumerate(tabs):
df = tab.extract()
# print first 3 and last 2 rows
print(f" --- Table {t_idx} on Page {p+1} (rows: {len(df)}) ---")
for r in df[:5]:
print(f" {r}")
if len(df) > 5:
print(" ...")
for r in df[-2:]:
print(f" {r}")
@@ -0,0 +1,83 @@
import json
import sys
import re
from pathlib import Path
sys.stdout.reconfigure(encoding='utf-8')
# Load enriched tables
sys.path.insert(0, '.')
from scratch.enrich_tables import tables
with open('resources/data_work_item_master/work_item_master_2026-01-01.json', 'r', encoding='utf-8') as f:
master = json.load(f)
master_tables = {}
for wi in master['work_items']:
for t in wi.get('tables', []):
tid = t.get('pum_table_id')
if tid:
master_tables[tid] = (wi, t)
print("=== 1. 마스터에서 basis_unit 이 ''으로 들어간 표 전수 조사 ===")
master_in_tables = []
for tid, (wi, t) in master_tables.items():
bu = t.get('basis_unit')
if bu and '' in str(bu):
master_in_tables.append((tid, t.get('section'), t.get('basis_quantity'), bu, t.get('pum_form'), t.get('basis_source')))
print(f"Total tables with '' in basis_unit in Master: {len(master_in_tables)}")
for it in master_in_tables:
print(f" [{it[0]}] {it[1]} -> {it[2]} {it[3]} (form: {it[4]}, src: {it[5]})")
print("\n=== 2. 원문 MD/PDF 에서 '' 또는 '인당'이 단위로 쓰인 표 전수 조사 ===")
unit_in_tables = []
for t in tables:
tid = t['table_id']
pre_lines = t['pre_context']
header_line = t['lines'][0] if t['lines'] else ""
# search for parenthesis unit containing '인'
found = []
for pl in reversed(pre_lines):
m = re.search(r'[\(\[\{]\s*단위\s*[:\=]?\s*([^\)\]\}]+)[\)\]\}]', pl)
if m:
u_str = m.group(1).strip()
if '' in u_str:
found.append(('pre_context', pl, u_str))
break
# also check headers
m_h = re.search(r'[\(\[]\s*단위\s*[:\=]?\s*([^\)\]]+)[\)\]]', header_line)
if m_h:
u_str = m_h.group(1).strip()
if '' in u_str:
found.append(('header', header_line, u_str))
# Also check if table row has "인당" in applicable standard (적용기준)
has_apply_in = any('1인당' in l or '인당' in l for l in t['lines'])
if found or has_apply_in:
unit_in_tables.append({
'table_id': tid,
'chapter': t['chapter'],
'section': t['section'],
'line': t['start_line'],
'found_units': found,
'has_apply_in': has_apply_in
})
print(f"Total tables where 원문 has '' in unit or apply text: {len(unit_in_tables)}")
print("\n--- [A. 순수 '' 또는 '인당' (무복합) 단위 표] ---")
pure_in = []
for it in unit_in_tables:
for src, pl, u in it['found_units']:
if u in ['', '인당', '인/일', '인 / 일', '1인/1일']:
pure_in.append((it['table_id'], it['section'], u, pl))
print(f" [{it['table_id']}] {it['section']} -> unit: '{u}' (from: {pl})")
print(f"\n--- [B. 적용기준 셀에 '1인당'이 들어있어 마스터가 ''으로 잘못 뽑은 표] ---")
for it in unit_in_tables:
if it['has_apply_in']:
t_master = master_tables.get(it['table_id'])
m_bu = t_master[1].get('basis_unit') if t_master else None
print(f" [{it['table_id']}] {it['section']} -> Master basis_unit: {m_bu}")
@@ -0,0 +1,93 @@
import os, sys, re, json
sys.stdout.reconfigure(encoding='utf-8')
base_dir = "resources/knowledge/original/원가계산/건설공사_표준품셈"
all_md_files = []
for root, dirs, files in os.walk(base_dir):
for f in files:
if f.endswith('.md') and not f.startswith('_') and '개정사항' not in f and '2026년_건설공사_표준품셈.md' not in f:
if '01_공통부문\\제8장' in os.path.join(root, f) or '01_공통부문/제8장' in os.path.join(root, f):
continue
all_md_files.append(os.path.join(root, f))
# Also add merged file
all_md_files.append(os.path.join(base_dir, "2026년_건설공사_표준품셈.md"))
print(f"Auditing {len(all_md_files)} files for 2 flaws...")
header_divider_mismatches = []
spec_3row_shift_candidates = []
for fpath in all_md_files:
rel_path = os.path.relpath(fpath, base_dir)
with open(fpath, 'r', encoding='utf-8') as f:
lines = f.readlines()
in_table = False
header_rows = []
divider_cols = 0
divider_line = 0
for idx, line in enumerate(lines):
line_num = idx + 1
line_str = line.strip()
if line_str.startswith('|') and line_str.endswith('|'):
cells = [c.strip() for c in line_str.split('|')[1:-1]]
# Check if this is a divider row
if all(re.match(r'^:?-+:?$', c) for c in cells if c):
divider_cols = len(cells)
divider_line = line_num
# Check preceding header row
if header_rows:
last_hdr = header_rows[-1]
if len(last_hdr['cells']) != divider_cols:
header_divider_mismatches.append({
"file": rel_path,
"line": divider_line,
"header_line": last_hdr['line'],
"header_cols": len(last_hdr['cells']),
"divider_cols": divider_cols,
"header_snippet": last_hdr['snippet']
})
in_table = True
elif not in_table:
header_rows.append({
"line": line_num,
"cells": cells,
"snippet": line_str[:80]
})
else:
# Table body row
# Check Flaw 1: 3-row spec shifted (e.g. output ㎾ on a row alone or pushed to next line)
# Looking for orphaned spec line with ㎾, ton, HP etc. where rest of row is empty or mismatched
if len(cells) > 2:
c0 = cells[0]
c1 = cells[1] if len(cells) > 1 else ""
# If first cell is just a unit/spec like "출력 ㎾" or "㎾" and following data columns are misaligned
if re.match(r'^(출력|규격|용량)?\s*(\(?㎾\)?|\(?ton\)?|\(?HP\)?|\(?PS\)?)$', c0):
spec_3row_shift_candidates.append({
"file": rel_path,
"line": line_num,
"snippet": line_str[:100]
})
else:
in_table = False
header_rows = []
print(f"\n1. Header vs Divider column mismatches: {len(header_divider_mismatches)}")
for m in header_divider_mismatches:
print(f" {m['file']} L{m['line']}: header={m['header_cols']} vs divider={m['divider_cols']} ({m['header_snippet']})")
print(f"\n2. Spec 3-row shifted candidates: {len(spec_3row_shift_candidates)}")
for s in spec_3row_shift_candidates:
print(f" {s['file']} L{s['line']}: {s['snippet']}")
with open('scratch/laptop_sub_flaws_audit.json', 'w', encoding='utf-8') as f:
json.dump({
"header_divider_mismatches": header_divider_mismatches,
"spec_3row_shift_candidates": spec_3row_shift_candidates
}, f, ensure_ascii=False, indent=2)
@@ -0,0 +1,81 @@
import json
import sys
import re
from pathlib import Path
sys.stdout.reconfigure(encoding='utf-8')
# Load enriched tables from scratch
sys.path.insert(0, '.')
from scratch.enrich_tables import tables
with open('resources/data_work_item_master/work_item_master_2026-01-01.json', 'r', encoding='utf-8') as f:
master = json.load(f)
master_tables = {}
for wi in master['work_items']:
for t in wi.get('tables', []):
tid = t.get('pum_table_id')
if tid:
master_tables[tid] = t
# Compare MD notes vs Master notes for all 476 tables
truncated_or_missing_notes = []
empty_in_master_has_in_md = []
fewer_lines_in_master = []
for t in tables:
tid = t['table_id']
mt = master_tables.get(tid)
if not mt:
continue
md_notes = t.get('notes', [])
master_notes = mt.get('notes', [])
# Clean up md_notes
# Filter out empty or pure markdown formatting if any
md_clean = [n.strip() for n in md_notes if n.strip()]
m_clean = [n.strip() for n in master_notes if n.strip()]
if md_clean and not m_clean:
empty_in_master_has_in_md.append({
'table_id': tid,
'chapter': t['chapter'],
'section': t['section'],
'line': t['start_line'],
'md_notes_count': len(md_clean),
'md_notes': md_clean
})
elif len(md_clean) > len(m_clean):
# Master has fewer lines than MD. Check what was lost!
lost_lines = []
for line in md_clean:
# Check if this line is in master_notes
if not any(line in mn or mn in line for mn in m_clean):
lost_lines.append(line)
if lost_lines:
fewer_lines_in_master.append({
'table_id': tid,
'chapter': t['chapter'],
'section': t['section'],
'line': t['start_line'],
'md_count': len(md_clean),
'master_count': len(m_clean),
'lost_lines': lost_lines
})
print(f"Tables where MD has notes but Master is completely EMPTY: {len(empty_in_master_has_in_md)}")
print(f"Tables where Master lost lines compared to MD: {len(fewer_lines_in_master)}")
print("\n--- 1. Tables where MD has notes but Master is EMPTY (First 15) ---")
for it in empty_in_master_has_in_md[:15]:
print(f"[{it['table_id']} | line {it['line']}] {it['chapter']} | {it['section']} (MD {it['md_notes_count']} lines)")
for n in it['md_notes'][:3]:
print(f" {n}")
print("\n--- 2. Tables where Master lost lines (Sample 10) ---")
for it in fewer_lines_in_master[:10]:
print(f"[{it['table_id']} | line {it['line']}] {it['chapter']} | {it['section']} (MD: {it['md_count']} vs Master: {it['master_count']})")
for l in it['lost_lines'][:3]:
print(f" LOST: {l}")
@@ -0,0 +1,23 @@
import json
import re
from pathlib import Path
md_path = Path(r"resources/knowledge/original/행정규칙/임도 품셈 적용기준 (현 산림사업 표준품셈)/첨부/(산림청고시 제2025-82호) 산림사업 표준품셈.md")
master_path = Path(r"resources/data_work_item_master/work_item_master_2026-01-01.json")
pum_forest_path = Path(r"resources/data_cost_input_value/pum_forest_2026.json")
coef_path = Path(r"resources/data_cost_input_value/coef_2026.json")
with open(master_path, "r", encoding="utf-8") as f:
master = json.load(f)
with open(pum_forest_path, "r", encoding="utf-8") as f:
pum_forest = json.load(f)
with open(coef_path, "r", encoding="utf-8") as f:
coef = json.load(f)
with open(md_path, "r", encoding="utf-8") as f:
lines = f.readlines()
print(f"Loaded master with {len(master['work_items'])} work items, {master['stats']['tables_attached']} attached tables, {len(master['orphan_tables'])} orphan tables.")
print(f"Loaded markdown with {len(lines)} lines.")
@@ -0,0 +1,64 @@
import sys, os, re, json
sys.stdout.reconfigure(encoding='utf-8')
base_dir = "resources/knowledge/original/원가계산/건설공사_표준품셈"
merged_file = os.path.join(base_dir, "2026년_건설공사_표준품셈.md")
with open(merged_file, 'r', encoding='utf-8') as f:
merged_text = f.read()
all_md_files = []
for root, dirs, files in os.walk(base_dir):
for f in files:
if f.endswith('.md') and not f.startswith('_') and '개정사항' not in f and '2026년_건설공사_표준품셈.md' not in f:
all_md_files.append(os.path.join(root, f))
# 1. Check divider column mismatch in all MD files (including merged)
files_to_check = all_md_files + [merged_file]
divider_mismatches = []
for fpath in files_to_check:
rel = os.path.relpath(fpath, base_dir)
with open(fpath, 'r', encoding='utf-8') as f:
lines = f.readlines()
for i in range(len(lines) - 1):
l1 = lines[i].strip()
l2 = lines[i+1].strip()
# Check if l2 is divider
if l1.startswith('|') and l1.endswith('|') and l2.startswith('|') and l2.endswith('|'):
# check if l2 consists of --- and |
if re.match(r'^\|(?:\s*:?-+:?\s*\|)+$', l2):
c1 = len(l1.split('|')) - 2
c2 = len(l2.split('|')) - 2
if c1 != c2:
divider_mismatches.append({
"file": rel,
"line": i + 2,
"header_cols": c1,
"divider_cols": c2,
"header": l1[:60],
"divider": l2[:40]
})
print(f"=== Divider Column Mismatches across all files: {len(divider_mismatches)} ===")
for dm in divider_mismatches[:20]:
print(f" - {dm['file']} (L{dm['line']}): header={dm['header_cols']} vs divider={dm['divider_cols']} | {dm['header']}")
# 2. Check 3-line spec displacement
# Tables where header has 형식 and 출력(kW) or similar
spec_displacements = []
for fpath in files_to_check:
rel = os.path.relpath(fpath, base_dir)
with open(fpath, 'r', encoding='utf-8') as f:
lines = f.readlines()
for i, l in enumerate(lines):
if '출력' in l and '' in l and '|' in l:
# check subsequent rows
# if next rows have misplaced cell values or shifted empty cells
pass
with open('scratch/divider_mismatches.json', 'w', encoding='utf-8') as f:
json.dump(divider_mismatches, f, ensure_ascii=False, indent=2)
+69
View File
@@ -0,0 +1,69 @@
import json
import sys
import re
from pathlib import Path
from collections import defaultdict
sys.stdout.reconfigure(encoding='utf-8')
# Load master
with open('resources/data_work_item_master/work_item_master_2026-01-01.json', 'r', encoding='utf-8') as f:
master = json.load(f)
# Load pum_forest
with open('resources/data_cost_input_value/pum_forest_2026.json', 'r', encoding='utf-8') as f:
pum_data = json.load(f)
pum_tables = pum_data.get('variables', {}).get('pum', {}).get('tables', [])
pum_map = {t['table_id']: t for t in pum_tables}
# Map master tables
master_wi_tables = {}
for wi in master['work_items']:
for t in wi.get('tables', []):
tid = t.get('pum_table_id')
if tid:
master_wi_tables[tid] = (wi, t)
orphan_map = {t.get('pum_table_id'): t for t in master.get('orphan_tables', []) if t.get('pum_table_id')}
print("=== 1. 표(Table) 누락 및 매핑 현황 ===")
all_f_ids = [f"F{i+1:04d}" for i in range(476)]
in_wi = []
in_orphan = []
missing = []
for tid in all_f_ids:
if tid in master_wi_tables:
in_wi.append(tid)
elif tid in orphan_map:
in_orphan.append(tid)
else:
missing.append(tid)
print(f"Total Tables in MD/pum_forest: {len(all_f_ids)}")
print(f"Tables mapped to Work Items: {len(in_wi)}")
print(f"Tables in orphan_tables: {len(in_orphan)}")
print(f"Tables missing from master completely: {len(missing)}")
for tid in missing:
pt = pum_map.get(tid, {})
print(f" Missing: {tid} | line {pt.get('line')} | section: {pt.get('section')}")
print("\n--- Orphan Tables Analysis (22 tables) ---")
orphan_by_section = defaultdict(list)
for tid in in_orphan:
ot = orphan_map[tid]
pt = pum_map.get(tid, {})
orphan_by_section[pt.get('section', ot.get('section'))].append((tid, pt.get('line'), ot.get('pum_form')))
for sec, tbls in orphan_by_section.items():
print(f"Section: {sec}")
for tid, line, pform in tbls:
print(f" - {tid} (line {line}): form={pform}")
print("\n--- Sections in MD vs Work Items ---")
# Check which chapters/sections exist in MD but have 0 work items or orphan only
wi_sections = {wi.get('toc_number'): wi for wi in master['work_items']}
wi_codes = {wi.get('work_item_code'): wi for wi in master['work_items']}
print(f"Work items count: {len(master['work_items'])}")
@@ -0,0 +1,96 @@
import os, sys, re
sys.stdout.reconfigure(encoding='utf-8')
target_files = [
# 02_토목부문
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제1장_도로포장공사.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제2장_하천공사.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제3장_터널공사.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제4장_궤도공사.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제5장_강구조공사.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제6장_관부설및접합공사.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제7장_항만공사.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제8장_지반조사.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제9장_측량.md',
# 05_유지관리부문
'resources/knowledge/original/원가계산/건설공사_표준품셈/05_유지관리부문/제1장_공통.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/05_유지관리부문/제2장_토목.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/05_유지관리부문/제3장_건축.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/05_유지관리부문/제4장_기계설비.md',
# 04_기계설비부문 제13장
'resources/knowledge/original/원가계산/건설공사_표준품셈/04_기계설비부문/제13장_플랜트설비공사.md',
]
print("=== Scanning for Page Boundary Duplicate/Overlapped Rows ===")
page_markers = [
r'^\s*→\s*\d+',
r'^\s*\d+\s*[가-힣]+부문',
r'^\s*[가-힣]+부문\s*\d+',
r'^\s*제\d+장\s*[가-힣]+\s*\d+',
r'^\s*\d+\s*제\d+장',
r'^\s*→\s*$',
r'^\s*→[가-힣]+부문\d+',
]
page_regex = re.compile('|'.join(page_markers))
boundary_cases = []
for fpath in target_files:
fname = os.path.basename(fpath)
with open(fpath, 'r', encoding='utf-8') as f:
lines = f.readlines()
for idx, line in enumerate(lines):
line_str = line.strip()
if page_regex.search(line_str):
l_no = idx + 1
# Found page boundary at l_no
# Look at 30 lines before and 30 lines after
before_lines = [lines[i].strip() for i in range(max(0, idx-30), idx) if lines[i].strip().startswith('|')]
after_lines = [lines[i].strip() for i in range(idx+1, min(len(lines), idx+31)) if lines[i].strip().startswith('|')]
# Filter out divider lines
before_data = [l for l in before_lines if not all(re.match(r'^:?-+:?$', c) for c in [x.strip() for x in l.split('|')[1:-1]] if c)]
after_data = [l for l in after_lines if not all(re.match(r'^:?-+:?$', c) for c in [x.strip() for x in l.split('|')[1:-1]] if c)]
if not before_data or not after_data:
continue
# Check for duplicate rows or squashed rows between before and after
for b_idx, bl in enumerate(before_data):
b_cells = [c.strip() for c in bl.split('|')[1:-1]]
b_nums = set(re.findall(r'\b\d+(?:\.\d+)?\b', bl))
b_words = set(re.findall(r'[가-힣A-Za-z0-9]+', bl))
for a_idx, al in enumerate(after_data):
a_cells = [c.strip() for c in al.split('|')[1:-1]]
# 1. Exact match
if b_cells == a_cells and len([c for c in b_cells if c not in ["", "-", ""]]) >= 2:
boundary_cases.append({
'file': fname,
'boundary_line': l_no,
'marker': line_str,
'type': 'exact_match_across_boundary',
'before_row': bl[:80],
'after_row': al[:80]
})
# 2. After row contains multiple before rows (squashed overlap)
a_words = set(re.findall(r'[가-힣A-Za-z0-9]+', al))
a_nums = set(re.findall(r'\b\d+(?:\.\d+)?\b', al))
if len(b_nums) >= 2 and b_nums.issubset(a_nums) and len(b_words.intersection(a_words)) >= 2:
boundary_cases.append({
'file': fname,
'boundary_line': l_no,
'marker': line_str,
'type': 'squashed_overlap_across_boundary',
'before_row': bl[:80],
'after_row': al[:80]
})
print(f"Total boundary cases found: {len(boundary_cases)}")
for bc in boundary_cases:
print(f"[{bc['file']}:L{bc['boundary_line']}] {bc['marker']} - {bc['type']}")
print(f" Before: {bc['before_row']}")
print(f" After: {bc['after_row']}")
@@ -0,0 +1,473 @@
[
{
"div": "2 토목부문",
"chap": "제1장_도로포장공사",
"tables": 213,
"standalone_secs": 57,
"stuck_secs": 3,
"sample_stuck": [
"1-1-1 교통통제",
"1-8-1 교통",
"1-9-1 방음벽"
],
"flat_headers": 18
},
{
"div": "2 토목부문",
"chap": "제2장_하천공사",
"tables": 19,
"standalone_secs": 8,
"stuck_secs": 3,
"sample_stuck": [
"2-1-1 사석부설",
"2-2-1 타원형",
"2-3-1 식생매트"
],
"flat_headers": 0
},
{
"div": "2 토목부문",
"chap": "제3장_터널공사",
"tables": 45,
"standalone_secs": 12,
"stuck_secs": 3,
"sample_stuck": [
"3-1-1 터널노임",
"3-3-1 터널",
"3-4-1 터널"
],
"flat_headers": 8
},
{
"div": "2 토목부문",
"chap": "제4장_궤도공사",
"tables": 73,
"standalone_secs": 24,
"stuck_secs": 7,
"sample_stuck": [
"4-1-1 철도안전처리",
"4-2-1 궤광조립",
"4-3-1 궤광조립"
],
"flat_headers": 27
},
{
"div": "2 토목부문",
"chap": "제5장_강구조공사",
"tables": 74,
"standalone_secs": 11,
"stuck_secs": 5,
"sample_stuck": [
"5-2-1 소재",
"5-3-1 강재거더",
"6-1-1 적용기준"
],
"flat_headers": 12
},
{
"div": "2 토목부문",
"chap": "제6장_관부설및접합공사",
"tables": 111,
"standalone_secs": 27,
"stuck_secs": 6,
"sample_stuck": [
"6-3-1 부설",
"6-4 P",
"6-4-1 T"
],
"flat_headers": 17
},
{
"div": "2 토목부문",
"chap": "제7장_항만공사",
"tables": 73,
"standalone_secs": 20,
"stuck_secs": 4,
"sample_stuck": [
"7-1-1 수중공사",
"7-3-1 케이슨",
"7-4-1 배송관"
],
"flat_headers": 2
},
{
"div": "2 토목부문",
"chap": "제8장_지반조사",
"tables": 83,
"standalone_secs": 15,
"stuck_secs": 4,
"sample_stuck": [
"8-1-1 기계기구",
"8-2-1 표준관입시험",
"8-3-1 굴절법"
],
"flat_headers": 13
},
{
"div": "2 토목부문",
"chap": "제9장_측량",
"tables": 1269,
"standalone_secs": 64,
"stuck_secs": 17,
"sample_stuck": [
"9-1-1 GNSS에",
"9-2-1 기본",
"9-3-1 지형현황"
],
"flat_headers": 41
},
{
"div": "3 건축부문",
"chap": "제10장_창호및유리공사",
"tables": 31,
"standalone_secs": 10,
"stuck_secs": 2,
"sample_stuck": [
"0-2-1 도어체크",
"0-3-1 창호유리"
],
"flat_headers": 10
},
{
"div": "3 건축부문",
"chap": "제11장_칠공사",
"tables": 43,
"standalone_secs": 19,
"stuck_secs": 2,
"sample_stuck": [
"1-1-1 콘크리트",
"1-3-1 무늬코트칠"
],
"flat_headers": 19
},
{
"div": "3 건축부문",
"chap": "제1장_철골공사",
"tables": 134,
"standalone_secs": 18,
"stuck_secs": 2,
"sample_stuck": [
"1-1-1 기본철골공수",
"1-4-1 부대철골"
],
"flat_headers": 16
},
{
"div": "3 건축부문",
"chap": "제2장_조적공사",
"tables": 48,
"standalone_secs": 9,
"stuck_secs": 3,
"sample_stuck": [
"2-1-1 벽돌",
"2-2-1 블록쌓기",
"2-3-1 ALC블록"
],
"flat_headers": 13
},
{
"div": "3 건축부문",
"chap": "제3장_타일공사",
"tables": 23,
"standalone_secs": 6,
"stuck_secs": 2,
"sample_stuck": [
"3-1-1 바탕",
"3-2-1 떠붙이기"
],
"flat_headers": 7
},
{
"div": "3 건축부문",
"chap": "제4장_목공사",
"tables": 29,
"standalone_secs": 13,
"stuck_secs": 3,
"sample_stuck": [
"4-1-1 먹매김",
"4-2-1 벽체틀",
"4-3-1 토대설치"
],
"flat_headers": 11
},
{
"div": "3 건축부문",
"chap": "제5장_수장공사",
"tables": 107,
"standalone_secs": 25,
"stuck_secs": 3,
"sample_stuck": [
"5-1-1 PVC계",
"5-2-1 흡음텍스",
"5-4-1 단열재"
],
"flat_headers": 28
},
{
"div": "3 건축부문",
"chap": "제6장_방수공사",
"tables": 70,
"standalone_secs": 23,
"stuck_secs": 7,
"sample_stuck": [
"6-1-1 바탕처리",
"6-2-1 도막바름",
"6-3-1 가열식시트"
],
"flat_headers": 20
},
{
"div": "3 건축부문",
"chap": "제7장_지붕및홈통공사",
"tables": 27,
"standalone_secs": 11,
"stuck_secs": 0,
"sample_stuck": [],
"flat_headers": 9
},
{
"div": "3 건축부문",
"chap": "제8장_금속공사",
"tables": 50,
"standalone_secs": 14,
"stuck_secs": 3,
"sample_stuck": [
"8-1-1 계단논슬립",
"8-2-1 용접식난간",
"8-3-1 잡철물"
],
"flat_headers": 15
},
{
"div": "3 건축부문",
"chap": "제9장_미장공사",
"tables": 62,
"standalone_secs": 14,
"stuck_secs": 5,
"sample_stuck": [
"9-1-1 모르타르",
"9-2-1 콘크리트면",
"9-3-1 창호주위"
],
"flat_headers": 17
},
{
"div": "4 기계설비부문",
"chap": "제10장_소방설비공사",
"tables": 37,
"standalone_secs": 16,
"stuck_secs": 7,
"sample_stuck": [
"0-3-1 지하식",
"0-4-1 일반송수구",
"0-5-1 압력공기탱크설치"
],
"flat_headers": 4
},
{
"div": "4 기계설비부문",
"chap": "제11장_가스설비공사",
"tables": 26,
"standalone_secs": 8,
"stuck_secs": 3,
"sample_stuck": [
"1-2-1 버트",
"1-3-1 분기공",
"2-1-1 계기반"
],
"flat_headers": 2
},
{
"div": "4 기계설비부문",
"chap": "제12장_자동제어설비공사",
"tables": 33,
"standalone_secs": 9,
"stuck_secs": 3,
"sample_stuck": [
"2-2-1 자동제어기기",
"2-3-1 중앙처리장치",
"3-1-1 플랜트"
],
"flat_headers": 10
},
{
"div": "4 기계설비부문",
"chap": "제13장_플랜트설비공사",
"tables": 785,
"standalone_secs": 52,
"stuck_secs": 39,
"sample_stuck": [
"3-2-1 강관절단",
"3-3-1 pipe보온",
"3-4-1 보통"
],
"flat_headers": 413
},
{
"div": "4 기계설비부문",
"chap": "제1장_배관공사",
"tables": 70,
"standalone_secs": 19,
"stuck_secs": 6,
"sample_stuck": [
"1-1-1 용접접합",
"1-2-1 용접접합",
"1-3-1 용접접합"
],
"flat_headers": 19
},
{
"div": "4 기계설비부문",
"chap": "제2장_덕트공사",
"tables": 40,
"standalone_secs": 12,
"stuck_secs": 2,
"sample_stuck": [
"2-1-1 아연도금강판덕트",
"2-2-1 취출구"
],
"flat_headers": 8
},
{
"div": "4 기계설비부문",
"chap": "제3장_보온공사",
"tables": 34,
"standalone_secs": 11,
"stuck_secs": 4,
"sample_stuck": [
"3-1-1 일반마감",
"3-2-1 일반마감",
"3-4-1 발열선"
],
"flat_headers": 7
},
{
"div": "4 기계설비부문",
"chap": "제4장_펌프및공기설비공사",
"tables": 24,
"standalone_secs": 9,
"stuck_secs": 3,
"sample_stuck": [
"4-2-1 송풍기",
"5-1-1 일반밸브",
"5-2-1 스팀트랩"
],
"flat_headers": 5
},
{
"div": "4 기계설비부문",
"chap": "제5장_밸브설비공사",
"tables": 10,
"standalone_secs": 5,
"stuck_secs": 3,
"sample_stuck": [
"5-3-1 익스팬션조인트",
"5-4-1 수격방지기",
"6-1-1 유량계"
],
"flat_headers": 1
},
{
"div": "4 기계설비부문",
"chap": "제6장_측정기기공사",
"tables": 20,
"standalone_secs": 7,
"stuck_secs": 2,
"sample_stuck": [
"6-2-1 세대용",
"7-1-1 소변기"
],
"flat_headers": 5
},
{
"div": "4 기계설비부문",
"chap": "제7장_위생기구설비공사",
"tables": 46,
"standalone_secs": 16,
"stuck_secs": 3,
"sample_stuck": [
"7-2-1 매립형",
"7-3-1 욕실거울",
"8-1-1 냉동기"
],
"flat_headers": 14
},
{
"div": "4 기계설비부문",
"chap": "제8장_공기조화설비공사",
"tables": 66,
"standalone_secs": 19,
"stuck_secs": 5,
"sample_stuck": [
"8-2-1 공기가열기",
"8-3-1 보일러",
"8-4-1 전기온수기"
],
"flat_headers": 7
},
{
"div": "4 기계설비부문",
"chap": "제9장_기타공사",
"tables": 41,
"standalone_secs": 13,
"stuck_secs": 6,
"sample_stuck": [
"9-1-1 입상관",
"9-3-1 슬리브",
"9-4-1 기밀시험"
],
"flat_headers": 17
},
{
"div": "5 유지관리부문",
"chap": "제1장_공통",
"tables": 141,
"standalone_secs": 26,
"stuck_secs": 7,
"sample_stuck": [
"1-1-1 비탈면",
"1-2 조경공사",
"1-2-3 조형전정"
],
"flat_headers": 10
},
{
"div": "5 유지관리부문",
"chap": "제2장_토목",
"tables": 309,
"standalone_secs": 51,
"stuck_secs": 16,
"sample_stuck": [
"2-1-1 교통통제",
"1-6-7 포장줄눈",
"2-1-6 아스팔트"
],
"flat_headers": 40
},
{
"div": "5 유지관리부문",
"chap": "제3장_건축",
"tables": 64,
"standalone_secs": 22,
"stuck_secs": 5,
"sample_stuck": [
"3-1-1 콘크리트구조물",
"3-2 해체공사",
"3-3 칠공사"
],
"flat_headers": 15
},
{
"div": "5 유지관리부문",
"chap": "제4장_기계설비",
"tables": 34,
"standalone_secs": 9,
"stuck_secs": 19,
"sample_stuck": [
"4-1-1 배관",
"4-1-3 스파이럴덕트",
"4-1-5 덕트보온"
],
"flat_headers": 2
}
]
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
@@ -0,0 +1,45 @@
import json
import sys
import re
import subprocess
from pathlib import Path
sys.stdout.reconfigure(encoding='utf-8')
# Run git diff for const_work_item_master_2026-01-01.json in commit 32c4c5cf
cmd = ["git", "diff", "32c4c5cf~1", "32c4c5cf", "--", "resources/data_work_item_master/const_work_item_master_2026-01-01.json"]
res = subprocess.run(cmd, capture_output=True, text=True, encoding='utf-8')
diff_lines = res.stdout.splitlines()
print(f"Total diff lines in const master: {len(diff_lines)}")
# Extract lines starting with '-' (removed lines) inside "notes": [...]
removed_notes = []
current_section = "미식별"
for l in diff_lines:
if '"section":' in l:
current_section = l.strip()
if l.startswith('-') and not l.startswith('---'):
cleaned = l[1:].strip()
if cleaned.startswith('"') and cleaned.endswith('",'):
cleaned = cleaned[1:-2]
elif cleaned.startswith('"') and cleaned.endswith('"'):
cleaned = cleaned[1:-1]
if cleaned and cleaned not in ['notes": [', ']']:
removed_notes.append((current_section, cleaned))
print(f"Total removed notes lines: {len(removed_notes)}")
# Filter lines that look like actual rules (containing %, 할증, 손료, 가산, 공제, etc.)
rule_keywords = ['%', '가산', '할증', '공제', '손료', '손율', '감한다', '할인', '포함한다', 'Q=', '적용한다']
removed_rules = []
for sec, line in removed_notes:
if any(k in line for k in rule_keywords):
# check if it was not just a title
if not re.match(r'^[0-9]+[\.\)]\s*[^:%]+$', line):
removed_rules.append((sec, line))
print(f"Removed lines containing pricing keywords in const master: {len(removed_rules)}")
print("\n--- Sample 20 Removed Rules in Const Master ---")
for sec, r in removed_rules[:20]:
print(f"{sec} -> {r}")
@@ -0,0 +1,63 @@
import json
import sys
import re
from pathlib import Path
sys.stdout.reconfigure(encoding='utf-8')
sys.path.insert(0, '.')
from scratch.enrich_tables import tables
with open('resources/data_work_item_master/work_item_master_2026-01-01.json', 'r', encoding='utf-8') as f:
master = json.load(f)
master_tables = {}
for wi in master['work_items']:
for t in wi.get('tables', []):
tid = t.get('pum_table_id')
if tid:
master_tables[tid] = t
# We want to know: Did the trimming cut away REAL pricing coefficients/conditions?
# Keywords that indicate actual pricing rules:
rule_keywords = ['%', '가산', '할증', '공제', '손료', '손율', '감한다', '할인', '포함한다', '별도', '규격', '작업량']
cut_real_rules = []
for t in tables:
tid = t['table_id']
mt = master_tables.get(tid)
if not mt:
continue
md_notes = t.get('notes', [])
master_notes = mt.get('notes', [])
# Check lines in md_notes that are NOT in master_notes
for line in md_notes:
line_clean = line.strip()
if not line_clean:
continue
# check if line_clean is present in master_notes
if not any(line_clean in mn for mn in master_notes):
# Is this line a section title like "- 3. ..." or "- 라. ..."?
# Check if it looks like a section heading
is_heading = bool(re.match(r'^[-*•]?\s*(?:[0-9]+[\.\)]|[가-힣][\.\)]|[A-Z][\.\)])\s*[^:%~]+$', line_clean))
is_example = '예시' in line_clean or line_clean.startswith('>')
# Check if it has real pricing rules
has_rule = any(kw in line_clean for kw in rule_keywords)
if has_rule and not is_example and not is_heading:
cut_real_rules.append({
'table_id': tid,
'chapter': t['chapter'],
'section': t['section'],
'line': t['start_line'],
'lost_rule': line_clean
})
print(f"Total potential REAL RULES cut away: {len(cut_real_rules)}")
for cr in cut_real_rules:
print(f"[{cr['table_id']} | line {cr['line']}] {cr['section']}")
print(f" CUT: {cr['lost_rule']}")
@@ -0,0 +1,46 @@
import json
import sys
from pathlib import Path
sys.stdout.reconfigure(encoding='utf-8')
# Load updated master
with open('resources/data_work_item_master/work_item_master_2026-01-01.json', 'r', encoding='utf-8') as f:
master = json.load(f)
tables = {}
for wi in master['work_items']:
for t in wi.get('tables', []):
tid = t.get('pum_table_id')
if tid:
tables[tid] = t
# Check the critical 34 lost rules from round 2
test_targets = [
('F0251', '고철공제', '9-8-1 철근절단 T=30cm 미만'),
('F0252', '고철공제', '9-8-2 철근절단 T=30cm 이상'),
('F0088', '100%를 가산', '4-3 위험목 베기 인력 가산'),
('F0086', '30%를 할증', '4-2-1 벌채 관목 할증'),
('F0086', '20%를 할증', '4-2-1 벌채 조재 할증'),
('F0191', '10%', '7-11 트랙터 집재 주행/잔존목 할증'),
('F0080', '1-4-5', '3-6 산물 임내정리 경사도 할증'),
('F0164', '1-4-3', '6-6 가지치기 할증 연계'),
('F0173', '1-4-5', '7-2 수라집재 할증 연계')
]
print("=== 2바퀴 34건 계수 절단 복원 여부 점검 ===")
for tid, keyword, desc in test_targets:
t = tables.get(tid)
if not t:
print(f"[{tid}] {desc}: TABLE NOT FOUND")
continue
notes = t.get('notes', [])
notes_str = " ".join(notes)
if keyword in notes_str:
print(f"[{tid}] {desc}: [복원됨/포함됨] ({len(notes)} notes)")
else:
print(f"[{tid}] {desc}: [여전히 잘려있음/누락] ({len(notes)} notes)")
if notes:
print(f" 현재 notes: {notes[:2]}")
else:
print(f" 현재 notes: [] (공백)")
@@ -0,0 +1,11 @@
import json, sys
sys.stdout.reconfigure(encoding='utf-8')
with open('scratch/all_candidates_collapsed_ex8.json', 'r', encoding='utf-8') as f:
data = json.load(f)
embedded = [d for d in data if d.get('is_embedded_table')]
print(f"Total embedded table rows: {len(embedded)}")
for e in embedded:
print(f"{e['file']} L{e['line']}: {e['cell_snippet'][:100]}")
@@ -0,0 +1,34 @@
import json
import sys
import subprocess
sys.stdout.reconfigure(encoding='utf-8')
# Run git diff between 02544795 and HEAD for work_item_master_2026-01-01.json
cmd = ["git", "diff", "02544795", "HEAD", "--", "resources/data_work_item_master/work_item_master_2026-01-01.json"]
res = subprocess.run(cmd, capture_output=True, text=True, encoding='utf-8')
diff_lines = res.stdout.splitlines()
print(f"Total diff lines in work_item_master: {len(diff_lines)}")
# Parse what tables changed
# Look for pum_table_id context
changed_tables = {}
cur_tid = None
cur_changes = []
for l in diff_lines:
if '"pum_table_id":' in l:
m = l.split('"pum_table_id":')[1].strip().replace('"', '').replace(',', '')
cur_tid = m
if cur_tid not in changed_tables:
changed_tables[cur_tid] = []
if (l.startswith('+') or l.startswith('-')) and not l.startswith('+++') and not l.startswith('---'):
if cur_tid:
changed_tables[cur_tid].append(l)
print(f"Number of tables with diffs: {len(changed_tables)}")
for tid, chs in list(changed_tables.items())[:15]:
print(f"\n[{tid}] changes ({len(chs)} lines):")
for c in chs[:4]:
print(f" {c}")
@@ -0,0 +1,52 @@
import os, sys, re, json
sys.stdout.reconfigure(encoding='utf-8')
merged_path = "resources/knowledge/original/원가계산/건설공사_표준품셈/2026년_건설공사_표준품셈.md"
with open(merged_path, 'r', encoding='utf-8') as f:
text_merged = f.read()
# Check how many target strings from our previous fixes exist in merged file
from scratch.fix_common_squashed_tables import (
target_2_8_7, repl_2_8_7,
target_2_8_10, repl_2_8_10,
target_2_11_5, repl_2_11_5,
target_2_11_7, repl_2_11_7,
target_4_4_4, repl_4_4_4,
target_5_1_3, repl_5_1_3,
target_5_1_8, repl_5_1_8,
target_5_pbd1, repl_5_pbd1,
target_5_pbd2, repl_5_pbd2,
target_5_pbd3, repl_5_pbd3,
target_5_base, repl_5_base,
target_6_slip, repl_6_slip,
target_6_socket, repl_6_socket,
target_6_grout, repl_6_grout,
target_6_anchor, repl_6_anchor,
target_6_sheath, repl_6_sheath,
target_7_4_2, repl_7_4_2
)
targets_common = [
(target_2_8_7, repl_2_8_7),
(target_2_8_10, repl_2_8_10),
(target_2_11_5, repl_2_11_5),
(target_2_11_7, repl_2_11_7),
(target_4_4_4, repl_4_4_4),
(target_5_1_3, repl_5_1_3),
(target_5_1_8, repl_5_1_8),
(target_5_pbd1, repl_5_pbd1),
(target_5_pbd2, repl_5_pbd2),
(target_5_pbd3, repl_5_pbd3),
(target_5_base, repl_5_base),
(target_6_slip, repl_6_slip),
(target_6_socket, repl_6_socket),
(target_6_grout, repl_6_grout),
(target_6_anchor, repl_6_anchor),
(target_6_sheath, repl_6_sheath),
(target_7_4_2, repl_7_4_2),
]
match_count = sum(1 for t, r in targets_common if t in text_merged)
print(f"Common targets found in merged file: {match_count} / {len(targets_common)}")
@@ -0,0 +1,48 @@
import sys, os, re, json
sys.stdout.reconfigure(encoding='utf-8')
base_dir = "resources/knowledge/original/원가계산/건설공사_표준품셈"
merged_file = os.path.join(base_dir, "2026년_건설공사_표준품셈.md")
with open(merged_file, 'r', encoding='utf-8') as f:
merged_text = f.read()
# Check key restored tables in merged_text:
# 1. 제8장 손료표 11곳: 0230, 3601, 5220, 6532, 6801, 7101, 7120, 7830, 7995, 9020, 9060
loss_codes = ['0230', '3601', '5220', '6532', '6801', '7101', '7120', '7830', '7995', '9020', '9060']
print("=== Checking 11 손료표 in 합본 ===")
for code in loss_codes:
# search for e.g. "({code})" followed by table
pattern = re.compile(rf'\({code}\)[^\n]*\n+(\|[^\n]+\|\n)+', re.DOTALL)
m = pattern.search(merged_text)
if m:
# count rows in table
rows = [l for l in m.group(0).split('\n') if l.strip().startswith('|') and not re.match(r'^\|(?:\s*:?-+:?\s*\|)+$', l.strip())]
print(f" - ({code}): Restored as table with {len(rows)} rows in 합본")
else:
# check if it exists as text
m2 = re.search(rf'\({code}\)', merged_text)
if m2:
print(f" ❌ ({code}): Exists as TEXT only (NOT TABLE) in 합본!")
else:
print(f" ❌ ({code}): Missing in 합본!")
# 2. Check 1-2-2 in merged_text
print("\n=== Checking 1-2-2 in 합본 ===")
if "돌 쌓 기 및 돌 붙 임" in merged_text:
# check if it is collapsed or separate rows
p_122 = re.compile(r'\|\s*돌\s*쌓\s*기\s*및\s*돌\s*붙\s*임\s*사\s*석')
if p_122.search(merged_text):
print(" ❌ 1-2-2 is COLLAPSED in 합본!")
else:
print(" ✅ 1-2-2 is properly expanded in 합본")
else:
print(" ❓ 1-2-2 not matched")
# 3. Check 6-1-1 in merged_text
print("\n=== Checking 6-1-1 in 합본 ===")
if "6-1-1 레디믹스트콘크리트 타설" in merged_text:
print(" ✅ 6-1-1 exists in 합본")
else:
print(" ❌ 6-1-1 is MISSING in 합본!")
+7
View File
@@ -0,0 +1,7 @@
import sys
from find_unclassified_grouped_tables import grouped_tables
sys.stdout.reconfigure(encoding='utf-8')
for t in grouped_tables:
if t['file'] == '제1장_도로포장공사.md':
print(f"L{t['line']}: {t['title']} | {t['groups']}")
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,33 @@
import os, sys
sys.stdout.reconfigure(encoding='utf-8')
# 제5장 강구조공사
ch5_path = "resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제5장_강구조공사.md"
with open(ch5_path, 'r', encoding='utf-8') as f:
text5 = f.read()
target_5_1 = "| 비 고 | - 인력에 의한 부설 및 접합을 수행하는 경우 다음 품을 적용한다. 구 분 단 위 수 량 관경(㎜) 시공량(본) 배관공(수도) 인 3 80 15 보 통 인 부 인 1 100 13 배관공(수도) 인 4 120 13 보 통 인 부 인 1 150 10 | | | |"
repl_5_1 = "| 비 고 | - 인력에 의한 부설 및 접합을 수행하는 경우 다음 품을 적용한다. (아래 표 참조) | | | |"
target_5_2 = "| 비 고 | - 인력에 의한 부설 및 접합을 수행하는 경우 다음 품을 적용한다. 구 분 단 위 수 량 관경(㎜) 시공량(본) 배관공(수도) 인 3 80 13 보 통 인 부 인 1 100 12 배관공(수도) 인 4 120 13 보 통 인 부 인 1 150 9 | | | |"
repl_5_2 = "| 비 고 | - 인력에 의한 부설 및 접합을 수행하는 경우 다음 품을 적용한다. (아래 표 참조) | | | |"
text5 = text5.replace(target_5_1, repl_5_1)
text5 = text5.replace(target_5_2, repl_5_2)
with open(ch5_path, 'w', encoding='utf-8') as f:
f.write(text5)
print("Updated 제5장_강구조공사.md successfully.")
# 제6장 관부설및접합공사
ch6_path = "resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제6장_관부설및접합공사.md"
with open(ch6_path, 'r', encoding='utf-8') as f:
text6 = f.read()
target_6 = "| 비 고 | - 인력에 의한 부설을 수행하는 경우 다음 품을 적용한다. 구 분 단 위 수 량 관경(㎜) 시공량(본) 배관공(수도) 인 2 80 8 보 통 인 부 인 1 100 6 배관공(수도) 인 3 125 7 보 통 인 부 인 1 150 6 배관공(수도) 인 4 200 5 보 통 인 부 인 1 250 4 | | | | |"
repl_6 = "| 비 고 | - 인력에 의한 부설을 수행하는 경우 다음 품을 적용한다. (아래 표 참조) | | | | |"
text6 = text6.replace(target_6, repl_6)
with open(ch6_path, 'w', encoding='utf-8') as f:
f.write(text6)
print("Updated 제6장_관부설및접합공사.md successfully.")
@@ -0,0 +1,150 @@
import glob, re, os, sys, fitz
sys.stdout.reconfigure(encoding='utf-8')
pdf_path = 'resources/knowledge/original/원가계산/건설공사_표준품셈/2026년_건설공사_표준품셈.pdf'
doc = fitz.open(pdf_path)
target_files = []
target_files.extend(glob.glob('resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/*.md'))
target_files.extend(glob.glob('resources/knowledge/original/원가계산/건설공사_표준품셈/05_유지관리부문/*.md'))
target_files.append('resources/knowledge/original/원가계산/건설공사_표준품셈/04_기계설비부문/제13장_플랜트설비공사.md')
print(f"Target files: {len(target_files)}")
# Build PDF index of section headings
print("Indexing PDF pages for section headings...")
sec_to_pdf_page = {}
for p_num in range(len(doc)):
text = doc[p_num].get_text()
# Find all patterns like X-Y-Z or X-Y
matches = re.findall(r'(\d+-\d+(?:-\d+)?)', text)
for m in matches:
sec_to_pdf_page.setdefault(m, []).append(p_num)
print(f"Indexed {len(sec_to_pdf_page)} section numbers in PDF.")
# Now parse each markdown file's tables that have a Remarks column
discrepancies = []
for fpath in target_files:
fname = os.path.basename(fpath)
with open(fpath, 'r', encoding='utf-8') as f:
lines = f.readlines()
current_sec_num = 'UNKNOWN'
current_sec_title = 'UNKNOWN'
in_table = False
headers = []
table_rows = []
start_line = 0
for idx, line in enumerate(lines):
line_s = line.strip()
sec_m = re.search(r'(\d+-\d+(?:-\d+)?)(\s+[^\n(]+)?', line_s)
if sec_m and not line_s.startswith('|'):
current_sec_num = sec_m.group(1).strip()
current_sec_title = line_s
if line_s.startswith('|') and not line_s.startswith('|---'):
cols = [c.strip() for c in line_s.split('|')[1:-1]]
if not in_table:
# check if header has 비고 or 비 고
if any('비고' in c or '비 고' in c for c in cols):
headers = cols
in_table = True
start_line = idx + 1
table_rows = []
continue
if in_table:
table_rows.append((idx + 1, cols))
else:
if in_table and (line_s == '' or not line_s.startswith('|')):
# Check this table against PDF!
remarks_col_idx = -1
for ci, h in enumerate(headers):
if '비고' in h or '비 고' in h:
remarks_col_idx = ci
break
if remarks_col_idx != -1 and len(table_rows) > 0:
md_remarks = [r[1][remarks_col_idx] if remarks_col_idx < len(r[1]) else '' for r in table_rows]
# Find matching page in PDF
cand_pages = sec_to_pdf_page.get(current_sec_num, [])
# filter to body pages (> 45)
body_pages = [p for p in cand_pages if p > 45]
found_pdf_table = False
for p in body_pages:
page = doc[p]
tabs = page.find_tables()
for tab in tabs:
df = tab.extract()
if not df or len(df) < 2:
continue
pdf_header = [c if c else '' for c in df[0]]
# Check if pdf_header has 비고
pdf_rem_idx = -1
for pci, ph in enumerate(pdf_header):
if '비고' in ph or '비 고' in ph:
pdf_rem_idx = pci
break
if pdf_rem_idx != -1:
found_pdf_table = True
# Extract pdf remarks
pdf_remarks_raw = [r[pdf_rem_idx] if pdf_rem_idx < len(r) and r[pdf_rem_idx] else '' for r in df[1:]]
# Flatten any multi-line in pdf remarks
pdf_remarks_split = []
for pr in pdf_remarks_raw:
pdf_remarks_split.extend([sub.strip() for sub in pr.split('\n') if sub.strip()])
# Compare count of non-empty remarks or ditto presence
md_non_empty = [m for m in md_remarks if m]
pdf_non_empty = [p for p in pdf_remarks_split if p]
# Check if ditto marks or numbers mismatch
has_ditto = any('' in m or '"' in m for m in md_remarks) or any('' in p for p in pdf_non_empty)
# If row count differs significantly or ditto count differs
md_ditto_cnt = sum(1 for m in md_remarks if '' in m or '"' in m)
pdf_ditto_cnt = sum(1 for p in pdf_non_empty if '' in p or '"' in p)
if has_ditto and md_ditto_cnt != pdf_ditto_cnt:
discrepancies.append({
'file': fname,
'sec': current_sec_num,
'line': start_line,
'type': 'Ditto count mismatch',
'md_dittos': md_ditto_cnt,
'pdf_dittos': pdf_ditto_cnt,
'md_remarks': md_remarks,
'pdf_remarks': pdf_remarks_raw,
'page': p + 1
})
elif len(md_remarks) != len(df) - 1:
# Row count mismatch in table with remarks
discrepancies.append({
'file': fname,
'sec': current_sec_num,
'line': start_line,
'type': 'Row count mismatch in remarks table',
'md_rows': len(md_remarks),
'pdf_rows': len(df) - 1,
'md_remarks': md_remarks,
'pdf_remarks': pdf_remarks_raw,
'page': p + 1
})
in_table = False
headers = []
table_rows = []
print(f"\nTotal Discrepancies found: {len(discrepancies)}")
for d in discrepancies:
print(f"\n[{d['file']}] {d['sec']} (L{d['line']}, PDF page {d.get('page')}) -> {d['type']}")
if 'md_dittos' in d:
print(f" MD dittos: {d['md_dittos']} vs PDF dittos: {d['pdf_dittos']}")
if 'md_rows' in d:
print(f" MD rows: {d['md_rows']} vs PDF rows: {d['pdf_rows']}")
print(f" MD remarks: {d['md_remarks'][:6]}")
print(f" PDF remarks: {d['pdf_remarks'][:6]}")
@@ -0,0 +1,134 @@
import sys, os, re, json
import fitz
sys.stdout.reconfigure(encoding='utf-8')
base_dir = "resources/knowledge/original/원가계산/건설공사_표준품셈"
merged_file = os.path.join(base_dir, "2026년_건설공사_표준품셈.md")
with open(merged_file, 'r', encoding='utf-8') as f:
merged_text = f.read()
all_split_files = []
for root, dirs, files in os.walk(base_dir):
for f in files:
if f.endswith('.md') and not f.startswith('_') and '개정사항' not in f and '2026년_건설공사_표준품셈.md' not in f:
all_split_files.append(os.path.join(root, f))
print(f"Total split files: {len(all_split_files)}")
# Function to extract all markdown tables from text
def extract_tables(text, file_label):
lines = text.split('\n')
tables = []
curr_table = []
start_line = 0
preceding_header = ""
for idx, line in enumerate(lines):
line_num = idx + 1
l_strip = line.strip()
# Track last seen section header
m_head = re.match(r'^(?:#+\s*)?(\d+-\d+(?:-\d+)?\s+[^\n]+|\(\d{4}\)\s+[^\n]+)', l_strip)
if m_head:
preceding_header = m_head.group(1).strip()
if l_strip.startswith('|') and l_strip.endswith('|'):
if not curr_table:
start_line = line_num
curr_table.append((line_num, l_strip))
else:
if curr_table:
# Check if it has a divider row
has_divider = False
for _, r in curr_table:
if re.match(r'^\|(?:\s*:?-+:?\s*\|)+$', r):
has_divider = True
break
if has_divider:
tables.append({
"file": file_label,
"start_line": start_line,
"end_line": start_line + len(curr_table) - 1,
"header_hint": preceding_header,
"rows": curr_table,
"row_count": len(curr_table),
"col_count": len(curr_table[0][1].split('|')) - 2,
"raw_header": curr_table[0][1]
})
curr_table = []
if curr_table:
has_divider = any(re.match(r'^\|(?:\s*:?-+:?\s*\|)+$', r) for _, r in curr_table)
if has_divider:
tables.append({
"file": file_label,
"start_line": start_line,
"end_line": start_line + len(curr_table) - 1,
"header_hint": preceding_header,
"rows": curr_table,
"row_count": len(curr_table),
"col_count": len(curr_table[0][1].split('|')) - 2,
"raw_header": curr_table[0][1]
})
return tables
merged_tables = extract_tables(merged_text, "합본")
print(f"Total tables in 합본: {len(merged_tables)}")
split_tables = []
for fpath in sorted(all_split_files):
rel = os.path.relpath(fpath, base_dir)
with open(fpath, 'r', encoding='utf-8') as f:
txt = f.read()
t_list = extract_tables(txt, rel)
split_tables.extend(t_list)
print(f"Total tables across 45 분할본: {len(split_tables)}")
# Now compare:
# 1. Tables present in split but missing in merged
# We match tables by header_hint or raw_header + first data row
def get_table_signature(t):
# take header hint, col_count, and first 30 chars of row 0
h_hint = t['header_hint'].split('(')[0].strip()
r0 = t['rows'][0][1][:40]
return f"{h_hint} || {t['col_count']} cols || {r0}"
merged_signatures = set()
for mt in merged_tables:
merged_signatures.add(get_table_signature(mt))
split_signatures = set()
for st in split_tables:
split_signatures.add(get_table_signature(st))
only_in_split = []
for st in split_tables:
sig = get_table_signature(st)
if sig not in merged_signatures:
only_in_split.append(st)
only_in_merged = []
for mt in merged_tables:
sig = get_table_signature(mt)
if sig not in split_signatures:
only_in_merged.append(mt)
print(f"\nTables only in 분할본 (potential missing/collapsed in 합본): {len(only_in_split)}")
for t in only_in_split[:15]:
print(f" - {t['file']} (L{t['start_line']}): hint='{t['header_hint']}', cols={t['col_count']}, rows={t['row_count']}")
print(f"\nTables only in 합본 (potential missing/collapsed in 분할본): {len(only_in_merged)}")
for t in only_in_merged[:15]:
print(f" - {t['file']} (L{t['start_line']}): hint='{t['header_hint']}', cols={t['col_count']}, rows={t['row_count']}")
with open('scratch/tables_diff.json', 'w', encoding='utf-8') as f:
json.dump({
"merged_table_count": len(merged_tables),
"split_table_count": len(split_tables),
"only_in_split_count": len(only_in_split),
"only_in_split": [{k: v for k, v in t.items() if k != 'rows'} for t in only_in_split],
"only_in_merged_count": len(only_in_merged),
"only_in_merged": [{k: v for k, v in t.items() if k != 'rows'} for t in only_in_merged]
}, f, ensure_ascii=False, indent=2)
@@ -0,0 +1,59 @@
import sys, os, re
sys.stdout.reconfigure(encoding='utf-8')
base_dir = "resources/knowledge/original/원가계산/건설공사_표준품셈"
merged_file = os.path.join(base_dir, "2026년_건설공사_표준품셈.md")
with open(merged_file, 'r', encoding='utf-8') as f:
merged_text = f.read()
def count_tables_in_text(text):
lines = text.split('\n')
tables = 0
curr = []
for l in lines:
ls = l.strip()
if ls.startswith('|') and ls.endswith('|'):
curr.append(ls)
else:
if curr:
if any(re.match(r'^\|(?:\s*:?-+:?\s*\|)+$', r) for r in curr):
tables += 1
curr = []
if curr and any(re.match(r'^\|(?:\s*:?-+:?\s*\|)+$', r) for r in curr):
tables += 1
return tables
# Split merged_text by divisions:
# 공통부문, 토목부문, 건축부문, 기계설비부문, 유지관리부문
div_names = ["공통부문", "토목부문", "건축부문", "기계설비부문", "유지관리부문"]
# find division boundaries in merged
div_spans = []
for dn in div_names:
# search for '# ' or main header of division
m = re.search(rf'(?:^|\n)#+\s*{dn}', merged_text)
if not m:
m = re.search(rf'(?:^|\n){dn}', merged_text)
pos = m.start() if m else -1
div_spans.append((dn, pos))
print(f"Division spans in merged: {div_spans}")
# Count per division folder in split
all_split_files = []
for root, dirs, files in os.walk(base_dir):
for f in files:
if f.endswith('.md') and not f.startswith('_') and '개정사항' not in f and '2026년_건설공사_표준품셈.md' not in f:
all_split_files.append(os.path.join(root, f))
split_by_div = {}
for p in all_split_files:
rel = os.path.relpath(p, base_dir)
div = rel.split(os.sep)[0]
with open(p, 'r', encoding='utf-8') as f:
txt = f.read()
c = count_tables_in_text(txt)
split_by_div[div] = split_by_div.get(div, 0) + c
for d, cnt in sorted(split_by_div.items()):
print(f"Split {d}: {cnt} tables")
@@ -0,0 +1,57 @@
import json
import sys
from pathlib import Path
sys.stdout.reconfigure(encoding='utf-8')
# Load master
master_path = Path('resources/data_work_item_master/work_item_master_2026-01-01.json')
with open(master_path, 'r', encoding='utf-8') as f:
master = json.load(f)
# Load pum_forest
pum_path = Path('resources/data_cost_input_value/pum_forest_2026.json')
with open(pum_path, 'r', encoding='utf-8') as f:
pum_data = json.load(f)
pum_tables = pum_data.get('variables', {}).get('pum', {}).get('tables', [])
print(f"pum_tables count: {len(pum_tables)}")
print(f"master work_items: {len(master['work_items'])}")
master_tables = []
for wi in master['work_items']:
master_tables.extend(wi.get('tables', []))
orphan_tables = master.get('orphan_tables', [])
print(f"master tables in work_items: {len(master_tables)}")
print(f"master orphan_tables: {len(orphan_tables)}")
total_master_tables = master_tables + orphan_tables
print(f"total master tables: {len(total_master_tables)}")
# Compare pum_tables and master_tables by pum_table_id
pum_ids = {t['pum_table_id'] for t in pum_tables if 'pum_table_id' in t}
master_ids = {t['pum_table_id'] for t in total_master_tables if 'pum_table_id' in t}
print(f"Unique table IDs in pum_forest: {len(pum_ids)}")
print(f"Unique table IDs in master: {len(master_ids)}")
diff_pum_master = pum_ids - master_ids
diff_master_pum = master_ids - pum_ids
print(f"IDs in pum_forest but NOT in master: {diff_pum_master}")
print(f"IDs in master but NOT in pum_forest: {diff_master_pum}")
# Check sample pum_table
t0 = pum_tables[0]
print("\nSample pum_table keys:", list(t0.keys()))
print("Sample pum_table metadata:")
for k in ['pum_table_id', 'section', 'source_line', 'pum_form', 'form_basis', 'basis_quantity', 'basis_unit', 'basis_source']:
print(f" {k}: {t0.get(k)}")
# Check raw_markdown presence
has_raw = sum(1 for t in pum_tables if 'raw_markdown' in t and t['raw_markdown'])
print(f"\nTables with raw_markdown in pum_forest: {has_raw}/{len(pum_tables)}")
# Check pum_form distribution
from collections import Counter
form_counts = Counter(t.get('pum_form') for t in pum_tables)
print("\npum_form distribution in pum_forest:")
for form, cnt in form_counts.most_common():
print(f" {form}: {cnt}")
@@ -0,0 +1,47 @@
import os, sys, re
sys.stdout.reconfigure(encoding='utf-8')
merged_path = 'resources/knowledge/original/원가계산/건설공사_표준품셈/2026년_건설공사_표준품셈.md'
with open(merged_path, 'r', encoding='utf-8') as f:
text_merged = f.read()
target_files = [
('02_토목부문', 'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제1장_도로포장공사.md'),
('02_토목부문', 'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제2장_하천공사.md'),
('02_토목부문', 'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제3장_터널공사.md'),
('02_토목부문', 'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제4장_궤도공사.md'),
('02_토목부문', 'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제5장_강구조공사.md'),
('02_토목부문', 'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제6장_관부설및접합공사.md'),
('02_토목부문', 'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제7장_항만공사.md'),
('02_토목부문', 'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제8장_지반조사.md'),
('02_토목부문', 'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제9장_측량.md'),
('05_유지관리부문', 'resources/knowledge/original/원가계산/건설공사_표준품셈/05_유지관리부문/제1장_공통.md'),
('05_유지관리부문', 'resources/knowledge/original/원가계산/건설공사_표준품셈/05_유지관리부문/제2장_토목.md'),
('05_유지관리부문', 'resources/knowledge/original/원가계산/건설공사_표준품셈/05_유지관리부문/제3장_건축.md'),
('05_유지관리부문', 'resources/knowledge/original/원가계산/건설공사_표준품셈/05_유지관리부문/제4장_기계설비.md'),
('04_기계설비부문', 'resources/knowledge/original/원가계산/건설공사_표준품셈/04_기계설비부문/제13장_플랜트설비공사.md'),
]
print("=== Checking Substantive Lines in Split Files vs Merged File ===")
for part, fpath in target_files:
fname = os.path.basename(fpath)
with open(fpath, 'r', encoding='utf-8') as f:
split_lines = [l.rstrip() for l in f.readlines()]
missing_in_merged = []
checked_count = 0
for idx, l in enumerate(split_lines):
l_str = l.strip()
if not l_str or l_str.startswith('#') or l_str.startswith('> 원문:'):
continue
# Table rows or section headers
checked_count += 1
if l_str not in text_merged:
missing_in_merged.append((idx+1, l_str))
print(f"[{fname}] Checked lines: {checked_count}, Missing in merged: {len(missing_in_merged)}")
if missing_in_merged:
for l_no, l_str in missing_in_merged[:5]:
print(f" L{l_no}: {l_str[:80]}")
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,36 @@
import json, re
with open('scratch/audit_42_detailed_breakdown.json', 'r', encoding='utf-8') as f:
audit_42 = json.load(f)
# count attached headers and collapsed tables in the 42 linked sections
attached_count = 0
collapsed_count = 0
sections_with_attached = []
sections_with_collapsed = []
for item in audit_42:
sec = item['section']
name = item['name']
header_sep = item['header_separation']
flaws = item['flaws_detected']
is_attached = "경계 붙음" in header_sep
if is_attached:
attached_count += 1
sections_with_attached.append(f"{sec} {name}")
has_collapsed = any("표 한 줄 뭉침" in f for f in flaws)
if has_collapsed:
collapsed_count += 1
sections_with_collapsed.append(f"{sec} {name}")
print(f"=== Linked 42 sections (non-machine 24 target nodes) ===")
print(f"Attached headers in our target: {attached_count} sections")
print(f"Collapsed tables in our target neighborhood: {collapsed_count} sections")
print("\nAttached sections list:")
for s in sections_with_attached:
print(f" - {s}")
print("\nCollapsed sections list:")
for s in sections_with_collapsed:
print(f" - {s}")
@@ -0,0 +1,47 @@
import sys, os, re, json
sys.stdout.reconfigure(encoding='utf-8')
base_dir = "resources/knowledge/original/원가계산/건설공사_표준품셈"
merged_file = os.path.join(base_dir, "2026년_건설공사_표준품셈.md")
with open(merged_file, 'r', encoding='utf-8') as f:
merged_text = f.read()
# Helper to count tables in text
def count_tables_in_text(text):
lines = text.split('\n')
tables = 0
curr = []
for l in lines:
ls = l.strip()
if ls.startswith('|') and ls.endswith('|'):
curr.append(ls)
else:
if curr:
if any(re.match(r'^\|(?:\s*:?-+:?\s*\|)+$', r) for r in curr):
tables += 1
curr = []
if curr and any(re.match(r'^\|(?:\s*:?-+:?\s*\|)+$', r) for r in curr):
tables += 1
return tables
# Count tables in each split file
all_split_files = []
for root, dirs, files in os.walk(base_dir):
for f in files:
if f.endswith('.md') and not f.startswith('_') and '개정사항' not in f and '2026년_건설공사_표준품셈.md' not in f:
all_split_files.append(os.path.join(root, f))
total_split_tables = 0
split_counts = {}
for p in sorted(all_split_files):
rel = os.path.relpath(p, base_dir)
with open(p, 'r', encoding='utf-8') as f:
t = f.read()
c = count_tables_in_text(t)
split_counts[rel] = c
total_split_tables += c
print(f"Total split tables: {total_split_tables}")
merged_total = count_tables_in_text(merged_text)
print(f"Total merged tables: {merged_total}")
@@ -0,0 +1,90 @@
import glob, re, os, sys, fitz
sys.stdout.reconfigure(encoding='utf-8')
pdf_path = 'resources/knowledge/original/원가계산/건설공사_표준품셈/2026년_건설공사_표준품셈.pdf'
doc = fitz.open(pdf_path)
target_files = []
target_files.extend(glob.glob('resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/*.md'))
target_files.extend(glob.glob('resources/knowledge/original/원가계산/건설공사_표준품셈/05_유지관리부문/*.md'))
target_files.append('resources/knowledge/original/원가계산/건설공사_표준품셈/04_기계설비부문/제13장_플랜트설비공사.md')
suspicious_cases = []
# Scan markdown tables for suspicious remarks or 조건 cells
for fpath in target_files:
fname = os.path.basename(fpath)
with open(fpath, 'r', encoding='utf-8') as f:
lines = f.readlines()
current_sec = 'UNKNOWN'
in_table = False
headers = []
table_rows = []
header_idx = 0
for idx, line in enumerate(lines):
line_s = line.strip()
sec_m = re.search(r'(\d+-\d+(?:-\d+)?(?:\s+[^\n(]+)?)', line_s)
if sec_m and not line_s.startswith('|'):
current_sec = sec_m.group(1).strip()
if line_s.startswith('|') and not line_s.startswith('|---'):
cols = [c.strip() for c in line_s.split('|')[1:-1]]
if not in_table:
# Header row
if any('비고' in c or '비 고' in c or '구분' in c or '직종' in c or '규격' in c for c in cols):
headers = cols
in_table = True
header_idx = idx + 1
table_rows = []
continue
if in_table:
table_rows.append((idx + 1, cols))
else:
if in_table and (line_s == '' or not line_s.startswith('|')):
# Check table_rows for suspicious patterns
# Find remarks col index if any
remarks_idx = -1
for ci, h in enumerate(headers):
if '비고' in h or '비 고' in h:
remarks_idx = ci
break
for ri, (ln, rcols) in enumerate(table_rows):
# Check remarks cell
if remarks_idx != -1 and remarks_idx < len(rcols):
rval = rcols[remarks_idx]
# Patterns: starts with %, contains lonely number/percentage, or short fragment
if re.match(r'^[\d\.]+\s*[%]', rval) or rval in ['45%', '50%', '30%'] or re.match(r'^[0-9]+[가-힣a-zA-Z]*$', rval) and len(rval) <= 4:
suspicious_cases.append({
'type': 'Suspicious Remarks Value',
'file': fname,
'sec': current_sec,
'line': ln,
'row': rcols,
'remarks': rval,
'reason': f'Remarks starts with percentage/number: {rval}'
})
# Check if rcols has ditto mark following an empty cell or strange transition
for ci, cval in enumerate(rcols):
if cval in ['', '"', ''] and ri == 0:
suspicious_cases.append({
'type': 'Ditto on First Row',
'file': fname,
'sec': current_sec,
'line': ln,
'row': rcols,
'col_idx': ci,
'reason': f'Ditto mark on first row of table: {cval}'
})
in_table = False
headers = []
table_rows = []
print(f"Total suspicious cases found in MD: {len(suspicious_cases)}")
for sc in suspicious_cases:
print(f"[{sc['file']}] {sc['sec']} (L{sc['line']}): {sc['reason']}")
print(f" Row: {sc['row']}")
@@ -0,0 +1,39 @@
import json
import sys
from pathlib import Path
sys.stdout.reconfigure(encoding='utf-8')
# Load 1st audit results
with open('resources/knowledge/technical_info/01_임도/05_원가정보/품셈_원문md_마스터데이터_대조_검증보고서.json', 'r', encoding='utf-8') as f:
audit_v1 = json.load(f)
refs_27 = audit_v1['item5_references']
# Load enriched tables to get original MD lines
sys.path.insert(0, '.')
from scratch.enrich_tables import tables
tables_by_id = {t['table_id']: t for t in tables}
print(f"Analyzing all {len(refs_27)} suspect reference tables from original MD:\n")
for r in refs_27:
tid = r['table_id']
t = tables_by_id.get(tid)
if not t:
continue
print(f"==================== [{tid} | Line {t['start_line']}] {t['chapter']} | {t['section']} ====================")
# Print pre-context (last 5 lines before table)
print("--- Pre-context ---")
for pl in t['pre_context'][-5:]:
print(f" {pl}")
print("--- Table Header & First 2 Rows ---")
for tl in t['lines'][:4]:
print(f" {tl.strip()}")
if t['notes']:
print("--- Notes ---")
for n in t['notes'][:3]:
print(f" {n}")
print()
@@ -0,0 +1,142 @@
import os, sys, re, json, glob, fitz
sys.stdout.reconfigure(encoding='utf-8')
base_dir = 'resources/knowledge/original/원가계산/건설공사_표준품셈'
divisions = ['02_토목부문', '03_건축부문', '04_기계설비부문', '05_유지관리부문']
flaws = []
# Known Col 0 fragments that indicate broken vertical merge
fragment_patterns = [
r'^(구\s*간|설\s*치|준\s*비|조\s*립|해\s*체|교\s*환|철\s*거|운\s*반|도\s*장|가\s*공|시\s*공)$',
r'^(A-Type|B-Type|C-Type)$',
r'^(3시간\s*차단|4시간\s*차단)$',
r'^(수\s*상|수\s*중|육\s*상)$',
r'^(충\s*적|난\s*적)$',
r'^(50㎏|60㎏)$',
r'^(직\s*종|공\s*종)$'
]
for div in divisions:
div_path = os.path.join(base_dir, div)
if not os.path.exists(div_path): continue
for fpath in sorted(glob.glob(os.path.join(div_path, '*.md'))):
fname = os.path.basename(fpath)
if fname.startswith('_') or '개정사항' in fname: continue
with open(fpath, 'r', encoding='utf-8') as f:
lines = f.readlines()
current_sec = 'UNKNOWN'
in_table = False
headers = []
table_rows = []
start_line = 0
for idx, line in enumerate(lines):
line_s = line.strip()
sec_m = re.search(r'(\d+-\d+(?:-\d+)?(?:\s+[^\n(]+)?)', line_s)
if sec_m and not line_s.startswith('|'):
current_sec = sec_m.group(1).strip()
# Check for inline squashed tables outside table
if not line_s.startswith('|') and len(line_s) > 30:
if (('직 종' in line_s or '직종' in line_s) and ('단위' in line_s or '단 위' in line_s) and ('수량' in line_s or '수 량' in line_s)) or \
(('작업효율' in line_s or '효율(E)' in line_s) and any(d in line_s for d in ['0.', '1.'])):
# Ignore known text lines
if not any(k in line_s for k in ['참조', '따른다', '적용한다', '기준이다']) or '0.' in line_s:
flaws.append({
'div': div,
'file': fname,
'sec': current_sec,
'line': idx + 1,
'type': 'Inline Squashed Table in Text',
'detail': line_s[:100]
})
if line_s.startswith('|') and not line_s.startswith('|---'):
cols = [c.strip() for c in line_s.split('|')[1:-1]]
if not in_table:
headers = cols
in_table = True
start_line = idx + 1
table_rows = []
continue
if in_table:
table_rows.append((idx + 1, cols))
else:
if in_table and (line_s == '' or not line_s.startswith('|')):
# Check table for flaws
col0_list = [r[1][0] if len(r[1]) > 0 else '' for r in table_rows]
# 1. Check Col 0 fragments
for ri, (ln, rcols) in enumerate(table_rows):
if len(rcols) == 0: continue
c0 = rcols[0]
for pat in fragment_patterns:
if re.match(pat, c0):
# Check if previous row had the first part of this word
flaws.append({
'div': div,
'file': fname,
'sec': current_sec,
'line': ln,
'type': 'Col 0 Fragment (Broken Vertical Merge)',
'detail': f"Row has fragment '{c0}', prev row was '{col0_list[ri-1] if ri > 0 else 'None'}'"
})
# 2. Check for remarks shifting / lonely percentage
rem_idx = -1
for ci, h in enumerate(headers):
if '비고' in h or '비 고' in h:
rem_idx = ci
break
if rem_idx != -1:
for ri, (ln, rcols) in enumerate(table_rows):
if rem_idx < len(rcols):
rval = rcols[rem_idx]
if re.match(r'^[\d\.]+\s*[%]$', rval) or rval in ['45%', '50%', '30%', '65%', '100%']:
flaws.append({
'div': div,
'file': fname,
'sec': current_sec,
'line': ln,
'type': 'Lonely Percentage in Remarks',
'detail': f"Remarks has isolated percentage '{rval}'"
})
# 3. Check for description sentence occupying Col 0
for ri, (ln, rcols) in enumerate(table_rows):
if len(rcols) == 0: continue
c0 = rcols[0]
# Long explanation in Col 0 of a non-first row
if len(c0) > 15 and any(k in c0 for k in ['하여', '하고', '', '위한', '설치', '운반', '정리']) and ri > 0:
# if other cols have workers/numbers, this might be a description pushed to Col 0
if len(rcols) > 2 and any(re.search(r'[\d\.]+', c) for c in rcols[1:]):
flaws.append({
'div': div,
'file': fname,
'sec': current_sec,
'line': ln,
'type': 'Description Pushed into Col 0',
'detail': f"Col 0 has description '{c0[:40]}...'"
})
in_table = False
headers = []
table_rows = []
print(f"Total potential flaws found: {len(flaws)}")
by_div = {}
for f in flaws:
by_div.setdefault(f['div'], []).append(f)
for d, flist in by_div.items():
print(f"\n[{d}]: {len(flist)} potential flaws")
for item in flist:
print(f" - [{item['file']}] L{item['line']} {item['sec']} -> {item['type']}")
print(f" {item['detail']}")
with open('scratch/deep_scan_flaws.json', 'w', encoding='utf-8') as fp:
json.dump(flaws, fp, ensure_ascii=False, indent=2)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,111 @@
import os, sys, re
sys.stdout.reconfigure(encoding='utf-8')
target_files = [
# 02_토목부문
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제1장_도로포장공사.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제2장_하천공사.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제3장_터널공사.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제4장_궤도공사.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제5장_강구조공사.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제6장_관부설및접합공사.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제7장_항만공사.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제8장_지반조사.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제9장_측량.md',
# 05_유지관리부문
'resources/knowledge/original/원가계산/건설공사_표준품셈/05_유지관리부문/제1장_공통.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/05_유지관리부문/제2장_토목.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/05_유지관리부문/제3장_건축.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/05_유지관리부문/제4장_기계설비.md',
# 04_기계설비부문 제13장
'resources/knowledge/original/원가계산/건설공사_표준품셈/04_기계설비부문/제13장_플랜트설비공사.md',
]
print("=== Deep Scan for Multi-item Rows Duplicating Preceding Items ===")
suspects = []
for fpath in target_files:
fname = os.path.basename(fpath)
with open(fpath, 'r', encoding='utf-8') as f:
lines = f.readlines()
tables = []
cur_t = []
cur_start = 0
for idx, l in enumerate(lines):
line_str = l.strip()
if line_str.startswith('|') and line_str.endswith('|'):
if not cur_t:
cur_start = idx + 1
cur_t.append((idx + 1, line_str))
else:
if cur_t:
tables.append((cur_start, cur_t))
cur_t = []
if cur_t:
tables.append((cur_start, cur_t))
for t_start, t_rows in tables:
data_rows = []
for lno, r_str in t_rows:
cells = [c.strip() for c in r_str.split('|')[1:-1]]
if all(re.match(r'^:?-+:?$', c) for c in cells if c):
continue
if cells and cells[0] in ["비고", "비 고", "", "[주]"]:
continue
# Header check
if any(c in ["구 분", "구분", "품 명", "품명", "규 격", "규격", "단 위", "단위", "수 량", "수량", "직 종", "직종"] for c in cells):
continue
data_rows.append((lno, r_str, cells))
if len(data_rows) < 3:
continue
# Collect individual item names from preceding rows (cell 0 or cell 1)
preceding_items = {} # item_name -> list of line numbers
for r_idx, (lno, r_str, cells) in enumerate(data_rows):
c0 = cells[0] if len(cells) > 0 else ""
c1 = cells[1] if len(cells) > 1 else ""
# Check if this row has multi-item in c0 or c1
# E.g., "시 멘 트 전 주 〃" or "도 복 장 강 관 〃 〃 〃 〃"
# Normalize whitespace: "시 멘 트" -> "시멘트"
# If multiple items:
c0_clean = re.sub(r'\s+', '', c0)
c1_clean = re.sub(r'\s+', '', c1)
# Check against preceding items
# Preceding items normalized
matched_items = []
for p_item, p_lnos in preceding_items.items():
if len(p_item) >= 2 and p_item in c0_clean and p_item != c0_clean:
matched_items.append((p_item, p_lnos))
elif len(p_item) >= 2 and p_item in c1_clean and p_item != c1_clean:
matched_items.append((p_item, p_lnos))
if len(matched_items) >= 2:
# 2 or more distinct preceding items appear in this single row's item cell!
suspects.append({
'file': fname,
'line': lno,
'table_start': t_start,
'row': r_str[:90],
'matched_preceding_items': matched_items
})
# Add single items from this row
# If c0 looks like a single item (not too many spaces/words)
words0 = c0.split()
if 1 <= len(words0) <= 2 and len(c0_clean) >= 2:
preceding_items.setdefault(c0_clean, []).append(lno)
words1 = c1.split()
if 1 <= len(words1) <= 2 and len(c1_clean) >= 2:
preceding_items.setdefault(c1_clean, []).append(lno)
print(f"Total suspects found: {len(suspects)}")
for s in suspects:
print(f"\n[{s['file']}:L{s['line']}] (Table starting at L{s['table_start']})")
print(f" Suspect Row: {s['row']}")
print(f" Matches preceding items: {s['matched_preceding_items']}")
@@ -0,0 +1,126 @@
import os, sys, re
sys.stdout.reconfigure(encoding='utf-8')
target_files = [
# 02_토목부문
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제1장_도로포장공사.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제2장_하천공사.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제3장_터널공사.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제4장_궤도공사.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제5장_강구조공사.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제6장_관부설및접합공사.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제7장_항만공사.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제8장_지반조사.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제9장_측량.md',
# 05_유지관리부문
'resources/knowledge/original/원가계산/건설공사_표준품셈/05_유지관리부문/제1장_공통.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/05_유지관리부문/제2장_토목.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/05_유지관리부문/제3장_건축.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/05_유지관리부문/제4장_기계설비.md',
# 04_기계설비부문 제13장
'resources/knowledge/original/원가계산/건설공사_표준품셈/04_기계설비부문/제13장_플랜트설비공사.md',
]
print("=== Scanning 14 target files for duplicated/overlapped rows ===")
exact_duplicates = []
squashed_overlaps = []
header_repeats = []
for fpath in target_files:
fname = os.path.basename(fpath)
with open(fpath, 'r', encoding='utf-8') as f:
lines = f.readlines()
# Track recent data rows (sliding window of 40 rows)
recent_rows = [] # list of (line_no, line_str, cells, numbers_set, text_words_set)
recent_headers = []
in_table = False
for idx, line in enumerate(lines):
line_str = line.strip()
l_no = idx + 1
if not (line_str.startswith('|') and line_str.endswith('|')):
in_table = False
# If leaving table, keep recent_rows for a bit or clear?
# A duplicate table might start right after!
continue
cells = [c.strip() for c in line_str.split('|')[1:-1]]
is_divider = all(re.match(r'^:?-+:?$', c) for c in cells if c)
if is_divider:
continue
# Is it a header row?
is_header = any(c in ["구 분", "구분", "품 명", "품명", "규 격", "규격", "단 위", "단위", "수 량", "수량", "직 종", "직종"] for c in cells)
if is_header:
# Check if identical header was seen recently (within 50 lines)
for h_lno, h_cells in recent_headers[-5:]:
if h_cells == cells:
header_repeats.append({
'file': fname,
'line': l_no,
'prev_line': h_lno,
'header': cells
})
recent_headers.append((l_no, cells))
continue
# Skip notes
if cells and cells[0] in ["비고", "비 고", "", "[주]"]:
continue
# Extract numbers and words
row_numbers = set(re.findall(r'\b\d+(?:\.\d+)?\b', line_str))
row_words = set(w for w in re.findall(r'[가-힣A-Za-z0-9]+', line_str) if len(w) > 1)
# 1. Check exact duplicate in sliding window
for r_lno, r_str, r_cells, r_nums, r_words in recent_rows[-30:]:
# If cells match exactly, and not just all empty or standard dashes
non_empty = [c for c in cells if c not in ["", "-", ""]]
if len(non_empty) >= 2 and cells == r_cells:
exact_duplicates.append({
'file': fname,
'line': l_no,
'prev_line': r_lno,
'row': line_str[:80]
})
break
# 2. Check squashed overlap:
# If this row contains multiple words/numbers that appeared across MULTIPLE preceding separate rows
# E.g., row_words has words from at least 3 distinct preceding rows, or numbers from 3 distinct rows
matches_per_row = []
for r_lno, r_str, r_cells, r_nums, r_words in recent_rows[-20:]:
common_nums = row_numbers.intersection(r_nums)
common_words = row_words.intersection(r_words)
if len(common_nums) >= 2 or len(common_words) >= 2:
matches_per_row.append((r_lno, common_words, common_nums))
if len(matches_per_row) >= 3:
# This row overlaps with 3 or more distinct preceding rows!
squashed_overlaps.append({
'file': fname,
'line': l_no,
'matched_prev_lines': [m[0] for m in matches_per_row],
'row': line_str[:90]
})
recent_rows.append((l_no, line_str, cells, row_numbers, row_words))
if len(recent_rows) > 40:
recent_rows.pop(0)
print(f"\n1. Exact duplicate rows in window: {len(exact_duplicates)}")
for d in exact_duplicates:
print(f" [{d['file']}:L{d['line']} (prev L{d['prev_line']})] {d['row']}")
print(f"\n2. Squashed overlaps (matching 3+ preceding rows): {len(squashed_overlaps)}")
for s in squashed_overlaps:
print(f" [{s['file']}:L{s['line']} (matched {s['matched_prev_lines']})] {s['row']}")
print(f"\n3. Header repeats (same table header repeated within 50 lines): {len(header_repeats)}")
for h in header_repeats:
print(f" [{h['file']}:L{h['line']} (prev L{h['prev_line']})] {h['header']}")
@@ -0,0 +1,15 @@
import sys
from audit_page_boundary_overlaps import boundary_cases
sys.stdout.reconfigure(encoding='utf-8')
by_file = {}
for b in boundary_cases:
by_file.setdefault(b['file'], []).append(b)
for f, cases in sorted(by_file.items()):
data_cases = [c for c in cases if not any(w in c['before_row'] for w in ['구 분', '직 종', '품 명', '공 정 별', '장 비 명', '작 업 구 분', '단 위', '수 량'])]
print(f"=== {f}: {len(cases)} boundary cases (Data row overlaps: {len(data_cases)}) ===")
for dc in data_cases:
print(f" L{dc['boundary_line']} [{dc['type']}]:")
print(f" Before: {dc['before_row']}")
print(f" After: {dc['after_row']}")
@@ -0,0 +1 @@
[]
@@ -0,0 +1,13 @@
import pymupdf
import sys
sys.stdout.reconfigure(encoding='utf-8')
doc = pymupdf.open("resources/knowledge/original/행정규칙/임도 품셈 적용기준 (현 산림사업 표준품셈)/첨부/(산림청고시 제2025-82호) 산림사업 표준품셈.pdf")
# Print entire page 52 (0-indexed 51) and 53 (0-indexed 52)
print("==================== PDF PAGE 52 (1-indexed) ====================")
print(doc[51].get_text())
print("==================== PDF PAGE 53 (1-indexed) ====================")
print(doc[52].get_text())
+117
View File
@@ -0,0 +1,117 @@
import re
import sys
import json
from pathlib import Path
sys.stdout.reconfigure(encoding='utf-8')
md_path = Path("resources/knowledge/original/행정규칙/임도 품셈 적용기준 (현 산림사업 표준품셈)/첨부/(산림청고시 제2025-82호) 산림사업 표준품셈.md")
with open(md_path, "r", encoding="utf-8") as f:
lines = f.readlines()
# 1. Parse Chapters and Sections
# Track current chapter, section, and text before tables
chapters = []
for idx, line in enumerate(lines):
m = re.match(r'^(#{1,3})\s*(제\s*\d+\s*장(?:\s+[^\,\n]+)?)$', line.strip())
if m and not any(k in line for k in ['참조', '적용기준,', '토질,']):
chapters.append((idx + 1, m.group(2).strip()))
def get_chapter(line_no):
cur_ch = "제0장 머리말"
for lno, ch in chapters:
if line_no >= lno:
cur_ch = ch
else:
break
return cur_ch
# 2. Extract tables and their contexts
tables = []
current_table = []
start_line = -1
for idx, line in enumerate(lines):
line_str = line.strip()
if line_str.startswith('|') and line_str.endswith('|'):
if not current_table:
start_line = idx + 1
current_table.append((idx + 1, line))
else:
if current_table:
has_sep = any(re.match(r'^\|(?:\s*:?-+:?\s*\|)+$', l.strip()) for _, l in current_table)
if has_sep:
tables.append({
'start_line': start_line,
'end_line': current_table[-1][0],
'lines': [l for _, l in current_table]
})
current_table = []
if current_table:
has_sep = any(re.match(r'^\|(?:\s*:?-+:?\s*\|)+$', l.strip()) for _, l in current_table)
if has_sep:
tables.append({
'start_line': start_line,
'end_line': current_table[-1][0],
'lines': [l for _, l in current_table]
})
print(f"Extracted {len(tables)} tables.")
# Enrich each table with pre-context (up to 15 lines before) and post-context ([주] up to next table/heading)
for i, t in enumerate(tables):
t_id = f"F{i+1:04d}"
t['table_id'] = t_id
t['chapter'] = get_chapter(t['start_line'])
# Pre-context: lines before start_line
pre_start = max(0, t['start_line'] - 15)
pre_lines = [lines[j].strip() for j in range(pre_start, t['start_line'] - 1) if lines[j].strip()]
t['pre_context'] = pre_lines
# Find section title from pre-lines (looking backwards)
section = "미식별"
for pl in reversed(pre_lines):
if re.match(r'^(?:#{1,4}\s*)?(?:\d+[-\.]\d+|\d+\.)', pl):
section = pl
break
t['section'] = section
# Post-context: lines after end_line until next table start or heading
post_end = len(lines)
if i + 1 < len(tables):
post_end = min(tables[i+1]['start_line'] - 1, t['end_line'] + 30)
else:
post_end = min(len(lines), t['end_line'] + 30)
post_lines = []
notes = []
in_note = False
for j in range(t['end_line'], post_end):
lj = lines[j].strip()
if not lj:
if in_note and len(notes) > 5 and not lj: # allow blank lines inside note
pass
continue
if lj.startswith('#') or (lj.startswith('|') and lj.endswith('|')):
break
post_lines.append(lj)
if '[주]' in lj or '【주】' in lj or re.match(r'^\s*주\s*[:\)]', lj):
in_note = True
if in_note:
notes.append(lj)
# if line looks like next section heading, stop
if re.match(r'^\d+\s*[\.-]\s*\d+', lj) and not re.match(r'^\s*[\(\[\d]+\s*[\.\)]', lj):
break
t['post_lines'] = post_lines
t['notes'] = notes
print(f"Sample table 10 (F0010):")
t10 = tables[9]
print(f" ID: {t10['table_id']}, Chapter: {t10['chapter']}, Section: {t10['section']}")
print(f" Start line: {t10['start_line']}, End line: {t10['end_line']}")
print(f" Pre-context (last 3): {t10['pre_context'][-3:]}")
print(f" Notes count: {len(t10['notes'])}")
if t10['notes']:
print(f" Notes sample: {t10['notes'][:3]}")
@@ -0,0 +1,72 @@
import glob, re, os, sys, fitz
sys.stdout.reconfigure(encoding='utf-8')
pdf_path = 'resources/knowledge/original/원가계산/건설공사_표준품셈/2026년_건설공사_표준품셈.pdf'
doc = fitz.open(pdf_path)
target_files = []
target_files.extend(glob.glob('resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/*.md'))
target_files.extend(glob.glob('resources/knowledge/original/원가계산/건설공사_표준품셈/05_유지관리부문/*.md'))
target_files.append('resources/knowledge/original/원가계산/건설공사_표준품셈/04_기계설비부문/제13장_플랜트설비공사.md')
active_remarks_tables = []
for fpath in target_files:
fname = os.path.basename(fpath)
with open(fpath, 'r', encoding='utf-8') as f:
lines = f.readlines()
current_sec = 'UNKNOWN'
in_table = False
headers = []
table_rows = []
start_line = 0
for idx, line in enumerate(lines):
line_s = line.strip()
sec_m = re.search(r'(\d+-\d+(?:-\d+)?(?:\s+[^\n(]+)?)', line_s)
if sec_m and not line_s.startswith('|'):
current_sec = sec_m.group(1).strip()
if line_s.startswith('|') and not line_s.startswith('|---'):
cols = [c.strip() for c in line_s.split('|')[1:-1]]
if not in_table:
if any('비고' in c or '비 고' in c for c in cols):
headers = cols
in_table = True
start_line = idx + 1
table_rows = []
continue
if in_table:
table_rows.append((idx + 1, cols))
else:
if in_table and (line_s == '' or not line_s.startswith('|')):
# Check if remarks column has non-empty text
rem_idx = -1
for ci, h in enumerate(headers):
if '비고' in h or '비 고' in h:
rem_idx = ci
break
if rem_idx != -1:
non_empty_remarks = [r[1][rem_idx] for r in table_rows if rem_idx < len(r[1]) and r[1][rem_idx]]
if len(non_empty_remarks) > 0:
active_remarks_tables.append({
'file': fname,
'sec': current_sec,
'line': start_line,
'row_count': len(table_rows),
'headers': headers,
'non_empty_cnt': len(non_empty_remarks),
'remarks_sample': non_empty_remarks[:5],
'all_remarks': non_empty_remarks
})
in_table = False
headers = []
table_rows = []
print(f"Total active remarks tables found: {len(active_remarks_tables)}")
for art in active_remarks_tables:
print(f"\n[{art['file']}] {art['sec']} (L{art['line']}) -> {art['row_count']} rows, {art['non_empty_cnt']} non-empty remarks")
print(f" Headers: {art['headers']}")
print(f" Remarks: {art['remarks_sample']}")
@@ -0,0 +1,64 @@
import sys, os, re, json
sys.stdout.reconfigure(encoding='utf-8')
base_dir = "resources/knowledge/original/원가계산/건설공사_표준품셈"
all_md_files = []
for root, dirs, files in os.walk(base_dir):
for f in files:
if f.endswith('.md') and not f.startswith('_') and '개정사항' not in f and '2026년_건설공사_표준품셈.md' not in f:
all_md_files.append(os.path.join(root, f))
total_candidates = []
# Section pattern: digit-digit(-digit)? followed by Korean letters (section title)
# We want to detect when it is NOT preceded by newline/start of line
# e.g. "한다.1-2-2 단위표준" or "계산한다.1-2-3 토질"
sec_regex = re.compile(r'([^\n#\-\*\|\s])(\d+-\d+(?:-\d+)?\s+[가-힣\w\(\\,\s]+?(?:\(\'\d+[^)]*년\s*(?:보완|제정|신설)[^)]*\)|(?=\n|\([일|㎥|㎡|m|인|개|km|ton|hr|대|본|개소|소|개련|조|ha|회]))?)')
for fpath in sorted(all_md_files):
rel_path = os.path.relpath(fpath, base_dir)
with open(fpath, 'r', encoding='utf-8') as f:
text = f.read()
lines = text.split('\n')
file_matches = []
for line_idx, line in enumerate(lines):
line_num = line_idx + 1
# find patterns in line
# check if line contains a section header that didn't start at beginning
# skip markdown table rows (| ... |)
if line.strip().startswith('|') and line.strip().endswith('|'):
continue
# Regex to find attached headers
# Look for e.g. text followed immediately by \d+-\d+
for m in re.finditer(r'([^\s#\-\*\>\|])(\d+-\d+(?:-\d+)?)\s+([가-힣][가-힣\s\w\(\)\/·]+)', line):
prev_char = m.group(1)
sec_num = m.group(2)
sec_title = m.group(3).strip()
# Filter out citations: e.g. '[공통부문] 5-3-1' or '5-3-1'
if prev_char in ["'", '"', '', '', '[', '(', '', '', '·', ':', '', '-', '/']:
continue
if '참고' in sec_title[:10] or '따른다' in sec_title[:10]:
continue
file_matches.append({
"file": rel_path,
"line": line_num,
"prev_char": prev_char,
"sec_num": sec_num,
"sec_title": sec_title[:30],
"full_match": m.group(0)[:60],
"line_snippet": line[:120]
})
if file_matches:
print(f"{rel_path}: {len(file_matches)} candidates")
total_candidates.extend(file_matches)
print(f"\nTotal attached section header candidates across all 45 files: {len(total_candidates)}")
with open('scratch/all_attached_headers_candidates.json', 'w', encoding='utf-8') as f:
json.dump(total_candidates, f, ensure_ascii=False, indent=2)
@@ -0,0 +1,70 @@
import os, sys, re, json
sys.stdout.reconfigure(encoding='utf-8')
base_dir = "resources/knowledge/original/원가계산/건설공사_표준품셈"
# Exclude 8장 (건설기계) and 합본
all_md_files = []
for root, dirs, files in os.walk(base_dir):
for f in files:
if f.endswith('.md') and not f.startswith('_') and '개정사항' not in f and '2026년_건설공사_표준품셈.md' not in f:
if '01_공통부문\\제8장' in os.path.join(root, f) or '01_공통부문/제8장' in os.path.join(root, f):
continue
all_md_files.append(os.path.join(root, f))
print(f"Total target files (excluding 8장 and merged): {len(all_md_files)}")
results = []
for fpath in sorted(all_md_files):
rel_path = os.path.relpath(fpath, base_dir)
with open(fpath, 'r', encoding='utf-8') as f:
lines = f.readlines()
for idx, line in enumerate(lines):
line_num = idx + 1
line_str = line.strip()
if not (line_str.startswith('|') and line_str.endswith('|')):
continue
cells = [c.strip() for c in line_str.split('|')[1:-1]]
# Skip header separators |---|---|
if all(re.match(r'^:?-+:?$', c) for c in cells if c):
continue
# Check for multi-token collapsed cells
# Conditions:
# A) cell has >= 8 tokens and contains numbers or spec symbols
# B) cell contains embedded table headers like '구 분', '규 격', '단 위' etc
# C) multiple numeric tokens that look like a row data squashed together
for c_idx, c in enumerate(cells):
tokens = c.split()
# If cell has embedded table structure
is_embedded_table = ('구 분' in c or '구분' in c) and ('단위' in c or '단 위' in c or '수량' in c or '수 량' in c)
# If cell has many tokens
is_many_tokens = len(tokens) >= 8 and sum(1 for tok in tokens if any(char.isdigit() for char in tok)) >= 4
if is_embedded_table or is_many_tokens:
results.append({
"file": rel_path,
"line": line_num,
"col_idx": c_idx,
"cell_snippet": c[:120],
"token_count": len(tokens),
"is_embedded_table": is_embedded_table
})
break
print(f"Total candidate collapsed rows found: {len(results)}")
by_file = {}
for r in results:
f = r['file']
by_file[f] = by_file.get(f, 0) + 1
for f, cnt in sorted(by_file.items(), key=lambda x: x[1], reverse=True):
print(f" {cnt:3d} rows: {f}")
with open('scratch/all_candidates_collapsed_ex8.json', 'w', encoding='utf-8') as f:
json.dump(results, f, ensure_ascii=False, indent=2)
@@ -0,0 +1,57 @@
import os, sys, re, json
sys.stdout.reconfigure(encoding='utf-8')
base_dir = "resources/knowledge/original/원가계산/건설공사_표준품셈"
all_md_files = []
for root, dirs, files in os.walk(base_dir):
for f in files:
if f.endswith('.md') and not f.startswith('_') and '개정사항' not in f and '2026년_건설공사_표준품셈.md' not in f:
if '01_공통부문\\제8장' in os.path.join(root, f) or '01_공통부문/제8장' in os.path.join(root, f):
continue
all_md_files.append(os.path.join(root, f))
squashed_tables = []
for fpath in sorted(all_md_files):
rel_path = os.path.relpath(fpath, base_dir)
with open(fpath, 'r', encoding='utf-8') as f:
lines = f.readlines()
for idx, line in enumerate(lines):
line_num = idx + 1
line_str = line.strip()
if line_str.startswith('|'):
continue
# Check if line contains a squashed table
# Sign 1: contains both '구 분' (or '구분') and ('단위' or '단 위') and ('수량' or '수 량')
# Sign 2: contains sec header followed immediately by table elements
has_table_headers = ('구 분' in line_str or '구분' in line_str) and \
('단 위' in line_str or '단위' in line_str) and \
('수 량' in line_str or '수량' in line_str or '' in line_str)
# Also check for tables without '수량' like '규격' '단위'
has_spec_unit = ('규 격' in line_str or '규격' in line_str) and \
('단 위' in line_str or '단위' in line_str) and \
any(term in line_str for term in ['공(인)', '인부', '비고', '시공량'])
if has_table_headers or has_spec_unit:
# Check length to avoid simple sentences
if len(line_str) > 40:
squashed_tables.append({
"file": rel_path,
"line": line_num,
"length": len(line_str),
"line_text": line_str
})
print(f"Total squashed table lines in non-table text: {len(squashed_tables)}")
for st in squashed_tables:
print(f"\n{st['file']} L{st['line']} (len: {st['length']}):")
print(f" {st['line_text'][:120]}")
with open('scratch/squashed_tables_in_text.json', 'w', encoding='utf-8') as f:
json.dump(squashed_tables, f, ensure_ascii=False, indent=2)
@@ -0,0 +1,72 @@
import os, sys, re
sys.stdout.reconfigure(encoding='utf-8')
target_files = [
# 02_토목부문
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제1장_도로포장공사.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제2장_하천공사.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제3장_터널공사.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제4장_궤도공사.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제5장_강구조공사.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제6장_관부설및접합공사.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제7장_항만공사.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제8장_지반조사.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제9장_측량.md',
# 05_유지관리부문
'resources/knowledge/original/원가계산/건설공사_표준품셈/05_유지관리부문/제1장_공통.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/05_유지관리부문/제2장_토목.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/05_유지관리부문/제3장_건축.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/05_유지관리부문/제4장_기계설비.md',
# 04_기계설비부문 제13장
'resources/knowledge/original/원가계산/건설공사_표준품셈/04_기계설비부문/제13장_플랜트설비공사.md',
]
# Suspect sentence continuation words in Col 0
CONTINUATION_PATTERNS = [
r'^구\s*간$', r'^설\s*치$', r'^후\s*설치$', r'^조립\s*설치$', r'^취급함', r'^정돈함',
r'^[가-힣\w\s]+(하고|하며|하여|혹은|및|등|까지|위해|위하여|취부함|포함|제외|운반|조양함)\b',
r'^→', r'^[A-Za-z0-9#]+\s*$', r'^분기기$', r'^교\s*환$', r'^매\s*립\s*식$', r'^\( 직 결 형 \)$'
]
print("=== Scanning All Target Files for Broken Groupings / Split Col 0 Sentences ===")
results = []
for fpath in target_files:
fname = os.path.basename(fpath)
with open(fpath, 'r', encoding='utf-8') as f:
lines = f.readlines()
for idx, line in enumerate(lines):
if not line.startswith('|'):
continue
cells = [c.strip() for c in line.split('|')[1:-1]]
if not cells or all(re.match(r'^:?-+:?$', c) for c in cells if c):
continue
c0 = cells[0]
# check if c0 matches any continuation patterns
for pat in CONTINUATION_PATTERNS:
if re.search(pat, c0):
# Also check surrounding lines to confirm it's part of a split
prev_line = lines[idx-1].strip() if idx > 0 else ""
next_line = lines[idx+1].strip() if idx + 1 < len(lines) else ""
results.append({
'file': fname,
'line': idx + 1,
'c0': c0,
'full_line': line.strip()[:100],
'matched_pat': pat
})
break
print(f"Total Suspect Split Col 0 Cells Found: {len(results)}")
by_file = {}
for r in results:
by_file.setdefault(r['file'], []).append(r)
for f, items in sorted(by_file.items()):
print(f"\n[{f}] ({len(items)}건)")
for it in items:
print(f" L{it['line']:<5} | Col0: '{it['c0']}' | {it['full_line']}")
@@ -0,0 +1,100 @@
import os, sys, re
sys.stdout.reconfigure(encoding='utf-8')
target_files = [
# 02_토목부문 (전체 9개 파일)
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제1장_도로포장공사.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제2장_하천공사.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제3장_터널공사.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제4장_궤도공사.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제5장_강구조공사.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제6장_관부설및접합공사.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제7장_항만공사.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제8장_지반조사.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제9장_측량.md',
# 05_유지관리부문 (전체 4개 파일)
'resources/knowledge/original/원가계산/건설공사_표준품셈/05_유지관리부문/제1장_공통.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/05_유지관리부문/제2장_토목.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/05_유지관리부문/제3장_건축.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/05_유지관리부문/제4장_기계설비.md',
# 04_기계설비부문 제13장
'resources/knowledge/original/원가계산/건설공사_표준품셈/04_기계설비부문/제13장_플랜트설비공사.md',
]
print("=== 1. Checking for duplicated or overlapped table rows ===")
candidates = []
for fpath in target_files:
fname = os.path.basename(fpath)
with open(fpath, 'r', encoding='utf-8') as f:
lines = f.readlines()
tables = []
current_table = []
current_start = 0
for idx, line in enumerate(lines):
line_str = line.strip()
if line_str.startswith('|') and line_str.endswith('|'):
if not current_table:
current_start = idx + 1
current_table.append((idx + 1, line_str))
else:
if current_table:
tables.append((current_start, current_table))
current_table = []
if current_table:
tables.append((current_start, current_table))
# Check each table
for start_line, t_rows in tables:
# Separate header and data rows
data_rows = []
for r_idx, (l_no, r_str) in enumerate(t_rows):
cells = [c.strip() for c in r_str.split('|')[1:-1]]
if all(re.match(r'^:?-+:?$', c) for c in cells if c):
continue
if r_idx == 0:
header_cells = cells
continue
data_rows.append((l_no, cells, r_str))
# Check if later data rows duplicate earlier data rows or contain words of earlier rows
earlier_first_cells = []
earlier_words = set()
for l_no, cells, r_str in data_rows:
if not cells:
continue
c0 = cells[0]
# Check if this row is a collapsed multi-item row
# Split words in c0
c0_words = [w for w in c0.split() if len(w) > 1 and w not in ["-", "·", ""]]
# Check overlap
overlap_words = [w for w in c0_words if w in earlier_words]
if len(overlap_words) >= 2:
candidates.append({
'file': fname,
'line': l_no,
'type': 'word_overlap_in_table',
'overlap': overlap_words,
'row': r_str[:80],
'earlier_first_cells': earlier_first_cells[-5:]
})
elif len(earlier_first_cells) > 0 and c0 in earlier_first_cells:
# Same first cell appears again - could be normal (e.g. different spec) or duplicate
# Check if entire row or multiple cells are identical
pass
earlier_first_cells.append(c0)
for w in c0_words:
earlier_words.add(w)
print(f"Total word overlap candidates in tables: {len(candidates)}")
for c in candidates:
print(f"[{c['file']}:L{c['line']}] {c['type']} - overlap: {c['overlap']}")
print(f" row: {c['row']}")
print(f" earlier: {c['earlier_first_cells']}")
@@ -0,0 +1,112 @@
import os, sys, re
sys.stdout.reconfigure(encoding='utf-8')
target_files = [
# 02_토목부문 (전체 9개 파일)
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제1장_도로포장공사.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제2장_하천공사.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제3장_터널공사.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제4장_궤도공사.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제5장_강구조공사.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제6장_관부설및접합공사.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제7장_항만공사.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제8장_지반조사.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제9장_측량.md',
# 05_유지관리부문 (전체 4개 파일)
'resources/knowledge/original/원가계산/건설공사_표준품셈/05_유지관리부문/제1장_공통.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/05_유지관리부문/제2장_토목.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/05_유지관리부문/제3장_건축.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/05_유지관리부문/제4장_기계설비.md',
# 04_기계설비부문 제13장
'resources/knowledge/original/원가계산/건설공사_표준품셈/04_기계설비부문/제13장_플랜트설비공사.md',
]
print("=== Scanning Tables for Trailing Duplicate/Overlapped Rows ===")
tail_duplicates = []
for fpath in target_files:
fname = os.path.basename(fpath)
with open(fpath, 'r', encoding='utf-8') as f:
lines = f.readlines()
tables = []
cur_t = []
cur_start = 0
for idx, l in enumerate(lines):
line_str = l.strip()
if line_str.startswith('|') and line_str.endswith('|'):
if not cur_t:
cur_start = idx + 1
cur_t.append((idx + 1, line_str))
else:
if cur_t:
tables.append((cur_start, cur_t))
cur_t = []
if cur_t:
tables.append((cur_start, cur_t))
for t_start, t_rows in tables:
# Separate divider
data_rows = []
for lno, r_str in t_rows:
cells = [c.strip() for c in r_str.split('|')[1:-1]]
if all(re.match(r'^:?-+:?$', c) for c in cells if c):
continue
if cells and cells[0] in ["비고", "비 고", "", "[주]"]:
continue
data_rows.append((lno, r_str, cells))
if len(data_rows) < 4:
continue
# Check every row against all PRECEDING rows in the table
# We look for:
# 1. Exact duplicate rows (not consecutive 〃 or standard repeats)
# 2. Rows whose cells contain multiple tokens from distinct preceding rows
seen_cells_set = [] # list of (lno, cells, text_tokens, num_tokens)
for r_idx, (lno, r_str, cells) in enumerate(data_rows):
# Tokens
text_tokens = set(re.findall(r'[가-힣A-Za-z]+', r_str))
text_tokens = {t for t in text_tokens if len(t) > 1 and t not in ["인부", "보통인부", "특별인부", "플랜트", "설비공", "용접공", "배관공"]}
num_tokens = set(re.findall(r'\b\d+(?:\.\d+)?\b', r_str))
# Check 1: Exact duplicate of an earlier row that is NOT adjacent
for prev_lno, prev_cells, prev_tt, prev_nt in seen_cells_set[:-1]:
non_empty = [c for c in cells if c not in ["", "-", ""]]
if len(non_empty) >= 2 and cells == prev_cells:
tail_duplicates.append({
'file': fname,
'line': lno,
'type': 'exact_duplicate_of_earlier_row',
'dup_of_line': prev_lno,
'row': r_str[:80]
})
break
# Check 2: Squashed composite row that combines tokens of 2+ distinct preceding rows
matching_earlier = []
for prev_lno, prev_cells, prev_tt, prev_nt in seen_cells_set:
if len(prev_tt) >= 2 and prev_tt.issubset(text_tokens) and len(prev_nt.intersection(num_tokens)) >= 1:
matching_earlier.append(prev_lno)
if len(matching_earlier) >= 2:
tail_duplicates.append({
'file': fname,
'line': lno,
'type': 'composite_squashed_duplicate',
'dup_of_lines': matching_earlier,
'row': r_str[:80]
})
seen_cells_set.append((lno, cells, text_tokens, num_tokens))
print(f"Total potential trailing duplicates found: {len(tail_duplicates)}")
for td in tail_duplicates:
if td['type'] == 'exact_duplicate_of_earlier_row':
print(f"[{td['file']}:L{td['line']}] EXACT DUP of L{td['dup_of_line']}: {td['row']}")
else:
print(f"[{td['file']}:L{td['line']}] COMPOSITE DUP of {td['dup_of_lines']}: {td['row']}")
@@ -0,0 +1,119 @@
import os, sys, re
sys.stdout.reconfigure(encoding='utf-8')
target_files = [
# 02_토목부문
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제1장_도로포장공사.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제2장_하천공사.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제3장_터널공사.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제4장_궤도공사.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제5장_강구조공사.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제6장_관부설및접합공사.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제7장_항만공사.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제8장_지반조사.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제9장_측량.md',
# 05_유지관리부문
'resources/knowledge/original/원가계산/건설공사_표준품셈/05_유지관리부문/제1장_공통.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/05_유지관리부문/제2장_토목.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/05_유지관리부문/제3장_건축.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/05_유지관리부문/제4장_기계설비.md',
# 04_기계설비부문 제13장
'resources/knowledge/original/원가계산/건설공사_표준품셈/04_기계설비부문/제13장_플랜트설비공사.md',
]
print("=== Scanning Tables for Exact Numeric/Text Subsets or Duplicates ===")
true_overlaps = []
for fpath in target_files:
fname = os.path.basename(fpath)
with open(fpath, 'r', encoding='utf-8') as f:
lines = f.readlines()
tables = []
cur_t = []
cur_start = 0
for idx, l in enumerate(lines):
line_str = l.strip()
if line_str.startswith('|') and line_str.endswith('|'):
if not cur_t:
cur_start = idx + 1
cur_t.append((idx + 1, line_str))
else:
if cur_t:
tables.append((cur_start, cur_t))
cur_t = []
if cur_t:
tables.append((cur_start, cur_t))
for t_start, t_rows in tables:
data_rows = []
for lno, r_str in t_rows:
cells = [c.strip() for c in r_str.split('|')[1:-1]]
if all(re.match(r'^:?-+:?$', c) for c in cells if c):
continue
if cells and cells[0] in ["비고", "비 고", "", "[주]"]:
continue
data_rows.append((lno, r_str, cells))
if len(data_rows) < 3:
continue
# For each row, extract list of numbers (excluding trivial 0, 1, 2)
row_num_tuples = []
for lno, r_str, cells in data_rows:
nums = re.findall(r'\b\d+(?:\.\d+)?\b', r_str)
# filter out non-informative
substantive_nums = [n for n in nums if n not in ["1", "2", "3", "4", "5", "1.0", "2.0"]]
row_num_tuples.append((lno, r_str, cells, substantive_nums))
for i in range(len(row_num_tuples)):
lno_i, str_i, cells_i, nums_i = row_num_tuples[i]
if len(nums_i) < 2:
continue
for j in range(i + 1, len(row_num_tuples)):
lno_j, str_j, cells_j, nums_j = row_num_tuples[j]
if len(nums_j) < 2:
continue
# Check if nums_i is identical to nums_j
if nums_i == nums_j and len(nums_i) >= 3:
# Same 3+ substantive numbers!
# Check text
t_i = set(re.findall(r'[가-힣A-Za-z]+', str_i))
t_j = set(re.findall(r'[가-힣A-Za-z]+', str_j))
common_t = t_i.intersection(t_j)
if len(common_t) >= 1:
true_overlaps.append({
'file': fname,
'line_1': lno_i,
'line_2': lno_j,
'type': 'identical_substantive_numbers',
'nums': nums_i,
'common_text': list(common_t),
'row_1': str_i[:80],
'row_2': str_j[:80]
})
# Check if nums_j contains nums_i and more (squashed combination)
elif set(nums_i).issubset(set(nums_j)) and len(nums_i) >= 3 and len(nums_j) > len(nums_i):
t_i = set(re.findall(r'[가-힣A-Za-z]+', str_i))
t_j = set(re.findall(r'[가-힣A-Za-z]+', str_j))
if t_i.intersection(t_j):
true_overlaps.append({
'file': fname,
'line_1': lno_i,
'line_2': lno_j,
'type': 'subset_squashed_numbers',
'nums_subset': nums_i,
'nums_superset': nums_j,
'row_1': str_i[:80],
'row_2': str_j[:80]
})
print(f"Total True Overlaps Found: {len(true_overlaps)}")
for to in true_overlaps:
print(f"\n[{to['file']}] Lines L{to['line_1']} & L{to['line_2']} - {to['type']}")
print(f" Row 1: {to['row_1']}")
print(f" Row 2: {to['row_2']}")
@@ -0,0 +1,121 @@
import os, sys, re
sys.stdout.reconfigure(encoding='utf-8')
target_files = [
# 02_토목부문
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제1장_도로포장공사.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제2장_하천공사.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제3장_터널공사.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제4장_궤도공사.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제5장_강구조공사.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제6장_관부설및접합공사.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제7장_항만공사.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제8장_지반조사.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제9장_측량.md',
# 05_유지관리부문
'resources/knowledge/original/원가계산/건설공사_표준품셈/05_유지관리부문/제1장_공통.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/05_유지관리부문/제2장_토목.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/05_유지관리부문/제3장_건축.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/05_유지관리부문/제4장_기계설비.md',
# 04_기계설비부문 제13장
'resources/knowledge/original/원가계산/건설공사_표준품셈/04_기계설비부문/제13장_플랜트설비공사.md',
]
print("=== Scanning by Section for Overlapping / Redundant Table Rows ===")
findings = []
for fpath in target_files:
fname = os.path.basename(fpath)
with open(fpath, 'r', encoding='utf-8') as f:
content = f.read()
# Split content by section pattern: e.g. \n\d+-\d+(?:-\d+)?
# Or line by line tracking current section
lines = content.split('\n')
current_sec = "TOP"
sec_lines = []
sections = [] # (sec_name, start_lno, lines)
for idx, line in enumerate(lines):
# Section header match e.g. "1-2-4 ", "## 1-2-4", etc.
m = re.match(r'^(?:#+\s*)?(\d+-\d+(?:-\d+)?)\s+', line.strip())
if m:
if sec_lines:
sections.append((current_sec, idx - len(sec_lines) + 1, sec_lines))
current_sec = m.group(1)
sec_lines = [line]
else:
sec_lines.append(line)
if sec_lines:
sections.append((current_sec, len(lines) - len(sec_lines) + 1, sec_lines))
for sec_name, start_lno, s_lines in sections:
# Extract tables in this section
tables = []
cur_t = []
cur_t_start = 0
for s_idx, sl in enumerate(s_lines):
l_str = sl.strip()
if l_str.startswith('|') and l_str.endswith('|'):
if not cur_t:
cur_t_start = start_lno + s_idx
cur_t.append((start_lno + s_idx, l_str))
else:
if cur_t:
tables.append((cur_t_start, cur_t))
cur_t = []
if cur_t:
tables.append((cur_t_start, cur_t))
# Check within each table or across tables in the same section
# Collect all rows in this section
all_rows = []
for t_start, t_rows in tables:
for r_lno, r_str in t_rows:
cells = [c.strip() for c in r_str.split('|')[1:-1]]
if all(re.match(r'^:?-+:?$', c) for c in cells if c):
continue
if cells and cells[0] in ["비고", "비 고", "", "[주]"]:
continue
# Skip header rows
if any(c in ["구 분", "구분", "품 명", "품명", "규 격", "규격", "단 위", "단위", "수 량", "수량", "직 종", "직종"] for c in cells):
continue
all_rows.append((r_lno, r_str, cells))
# Now check if later rows duplicate earlier rows in the same section!
# Two types:
# Type A: Exact same data in 3 or more consecutive rows
# Type B: A single row that squashes 2 or more preceding rows from the same section
for i in range(len(all_rows)):
r_lno, r_str, cells = all_rows[i]
# Check if this row is a squashed version of preceding rows
# Extract distinct tokens from this row
row_tokens = set(re.findall(r'[가-힣A-Za-z0-9.]+', r_str))
row_tokens = {t for t in row_tokens if len(t) > 1 and t not in ["인부", "보통인부", "특별인부", "0.0", "1.0"]}
matching_preceding = []
for j in range(0, i):
prev_lno, prev_str, prev_cells = all_rows[j]
prev_tokens = set(re.findall(r'[가-힣A-Za-z0-9.]+', prev_str))
prev_tokens = {t for t in prev_tokens if len(t) > 1 and t not in ["인부", "보통인부", "특별인부", "0.0", "1.0"]}
# Check how much prev_tokens is contained in row_tokens
if len(prev_tokens) >= 3 and prev_tokens.issubset(row_tokens):
matching_preceding.append(prev_lno)
if len(matching_preceding) >= 2:
findings.append({
'file': fname,
'section': sec_name,
'line': r_lno,
'squashed_row': r_str[:80],
'duplicates_of_lines': matching_preceding
})
print(f"Total findings of squashed rows duplicating preceding rows in same section: {len(findings)}")
for f in findings:
print(f"[{f['file']}:{f['section']}:L{f['line']}] duplicates lines {f['duplicates_of_lines']}")
print(f" Squashed row: {f['squashed_row']}")
@@ -0,0 +1,94 @@
import sys, os, re, json
sys.stdout.reconfigure(encoding='utf-8')
base_dir = "resources/knowledge/original/원가계산/건설공사_표준품셈"
merged_file = os.path.join(base_dir, "2026년_건설공사_표준품셈.md")
with open(merged_file, 'r', encoding='utf-8') as f:
merged_text = f.read()
all_split_files = []
for root, dirs, files in os.walk(base_dir):
for f in files:
if f.endswith('.md') and not f.startswith('_') and '개정사항' not in f and '2026년_건설공사_표준품셈.md' not in f:
all_split_files.append(os.path.join(root, f))
# Function to extract all tables with their unique text fingerprints
def extract_table_blocks(text, label):
lines = text.split('\n')
tables = []
curr_lines = []
start_line = 0
for idx, line in enumerate(lines):
line_num = idx + 1
l_strip = line.strip()
if l_strip.startswith('|') and l_strip.endswith('|'):
if not curr_lines:
start_line = line_num
curr_lines.append(l_strip)
else:
if curr_lines:
# check divider
if any(re.match(r'^\|(?:\s*:?-+:?\s*\|)+$', r) for r in curr_lines):
# take first 2 rows as key
header_str = " // ".join(curr_lines[:2])
tables.append({
"file": label,
"start_line": start_line,
"line_count": len(curr_lines),
"header_key": header_str,
"first_row": curr_lines[0],
"sample_cell": curr_lines[min(2, len(curr_lines)-1)]
})
curr_lines = []
if curr_lines and any(re.match(r'^\|(?:\s*:?-+:?\s*\|)+$', r) for r in curr_lines):
tables.append({
"file": label,
"start_line": start_line,
"line_count": len(curr_lines),
"header_key": " // ".join(curr_lines[:2]),
"first_row": curr_lines[0],
"sample_cell": curr_lines[min(2, len(curr_lines)-1)]
})
return tables
merged_tbls = extract_table_blocks(merged_text, "합본")
split_tbls = []
for p in sorted(all_split_files):
rel = os.path.relpath(p, base_dir)
with open(p, 'r', encoding='utf-8') as f:
t = f.read()
split_tbls.extend(extract_table_blocks(t, rel))
print(f"Merged tables: {len(merged_tbls)}")
print(f"Split tables: {len(split_tbls)}")
# Match each split table in merged_text
# If first_row and sample_cell not found together in merged_text
truly_missing_in_merged = []
for st in split_tbls:
# check if first_row exists in merged_text
r0 = st['first_row']
# remove spacing differences
norm_r0 = re.sub(r'\s+', '', r0)
# Check if this row exists in any merged table
found = False
for mt in merged_tbls:
norm_mt = re.sub(r'\s+', '', mt['first_row'])
if norm_r0 == norm_mt:
# check sample_cell too
norm_sc = re.sub(r'\s+', '', st['sample_cell'])
norm_msc = re.sub(r'\s+', '', mt['sample_cell'])
if norm_sc == norm_msc or norm_sc[:15] in norm_msc or norm_msc[:15] in norm_sc:
found = True
break
if not found:
truly_missing_in_merged.append(st)
print(f"\nTruly missing tables in 합본: {len(truly_missing_in_merged)}")
for t in truly_missing_in_merged:
print(f" - {t['file']} (L{t['start_line']}): {t['first_row'][:60]} | sample={t['sample_cell'][:40]}")
with open('scratch/truly_missing_in_merged.json', 'w', encoding='utf-8') as f:
json.dump(truly_missing_in_merged, f, ensure_ascii=False, indent=2)
@@ -0,0 +1,92 @@
import os, sys, re
sys.stdout.reconfigure(encoding='utf-8')
target_files = [
# 02_토목부문
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제1장_도로포장공사.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제2장_하천공사.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제3장_터널공사.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제4장_궤도공사.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제5장_강구조공사.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제6장_관부설및접합공사.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제7장_항만공사.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제8장_지반조사.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제9장_측량.md',
# 05_유지관리부문
'resources/knowledge/original/원가계산/건설공사_표준품셈/05_유지관리부문/제1장_공통.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/05_유지관리부문/제2장_토목.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/05_유지관리부문/제3장_건축.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/05_유지관리부문/제4장_기계설비.md',
# 04_기계설비부문 제13장
'resources/knowledge/original/원가계산/건설공사_표준품셈/04_기계설비부문/제13장_플랜트설비공사.md',
]
print("=== Scanning for Non-Adjacent Duplicate Rows within the EXACT SAME Table ===")
duplicates_in_table = []
for fpath in target_files:
fname = os.path.basename(fpath)
with open(fpath, 'r', encoding='utf-8') as f:
lines = f.readlines()
tables = []
cur_t = []
cur_start = 0
for idx, l in enumerate(lines):
line_str = l.strip()
if line_str.startswith('|') and line_str.endswith('|'):
if not cur_t:
cur_start = idx + 1
cur_t.append((idx + 1, line_str))
else:
if cur_t:
tables.append((cur_start, cur_t))
cur_t = []
if cur_t:
tables.append((cur_start, cur_t))
for t_start, t_rows in tables:
data_rows = []
for lno, r_str in t_rows:
cells = [c.strip() for c in r_str.split('|')[1:-1]]
if all(re.match(r'^:?-+:?$', c) for c in cells if c):
continue
if cells and cells[0] in ["비고", "비 고", "", "[주]"]:
continue
if any(c in ["구 분", "구분", "품 명", "품명", "규 격", "규격", "단 위", "단위", "수 량", "수량", "직 종", "직종"] for c in cells):
continue
data_rows.append((lno, r_str, cells))
if len(data_rows) < 4:
continue
# Check if any non-trivial row appears multiple times in THIS SAME TABLE
for i in range(len(data_rows)):
lno_i, str_i, cells_i = data_rows[i]
# Filter trivial (like empty cells, or only generic worker names)
non_empty = [c for c in cells_i if c not in ["", "-", ""]]
if len(non_empty) <= 1:
continue
# Generic worker row check
if len(non_empty) <= 3 and any(w in str_i for w in ["보통인부", "특별인부", "인부", "기계설비공", "용접공"]) and not any(re.search(r'\d+\.\d{2,}', c) for c in cells_i):
nums = re.findall(r'\b\d+(?:\.\d+)?\b', str_i)
if all(n in ["1", "2", "3", "4", "5", "10", "20"] for n in nums):
continue
for j in range(i + 2, len(data_rows)): # Non-adjacent (distance >= 2)
lno_j, str_j, cells_j = data_rows[j]
if cells_i == cells_j:
duplicates_in_table.append({
'file': fname,
'table_start': t_start,
'line_1': lno_i,
'line_2': lno_j,
'row': str_i[:85]
})
print(f"Total Non-Adjacent Duplicates in Same Table: {len(duplicates_in_table)}")
for d in duplicates_in_table:
print(f"\n[{d['file']}] Table starting at L{d['table_start']}: L{d['line_1']} & L{d['line_2']}")
print(f" Row: {d['row']}")
@@ -0,0 +1,108 @@
import os, sys, re
sys.stdout.reconfigure(encoding='utf-8')
target_files = [
# 02_토목부문
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제1장_도로포장공사.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제2장_하천공사.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제3장_터널공사.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제4장_궤도공사.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제5장_강구조공사.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제6장_관부설및접합공사.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제7장_항만공사.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제8장_지반조사.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제9장_측량.md',
# 05_유지관리부문
'resources/knowledge/original/원가계산/건설공사_표준품셈/05_유지관리부문/제1장_공통.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/05_유지관리부문/제2장_토목.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/05_유지관리부문/제3장_건축.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/05_유지관리부문/제4장_기계설비.md',
# 04_기계설비부문 제13장
'resources/knowledge/original/원가계산/건설공사_표준품셈/04_기계설비부문/제13장_플랜트설비공사.md',
]
print("=== Scanning for Overlapped / Duplicated Squashed Rows in Same Table ===")
overlap_cases = []
for fpath in target_files:
fname = os.path.basename(fpath)
with open(fpath, 'r', encoding='utf-8') as f:
lines = f.readlines()
tables = []
cur_t = []
cur_start = 0
for idx, l in enumerate(lines):
line_str = l.strip()
if line_str.startswith('|') and line_str.endswith('|'):
if not cur_t:
cur_start = idx + 1
cur_t.append((idx + 1, line_str))
else:
if cur_t:
tables.append((cur_start, cur_t))
cur_t = []
if cur_t:
tables.append((cur_start, cur_t))
for t_start, t_rows in tables:
# Separate header and data rows
data_rows = []
for lno, r_str in t_rows:
cells = [c.strip() for c in r_str.split('|')[1:-1]]
if all(re.match(r'^:?-+:?$', c) for c in cells if c):
continue
if cells and cells[0] in ["비고", "비 고", "", "[주]"]:
continue
data_rows.append((lno, r_str, cells))
if len(data_rows) < 3:
continue
# For each row in the table, check if it is a "squashed row"
# (contains multiple words or numbers in a single cell)
for r_idx, (lno, r_str, cells) in enumerate(data_rows):
# Check if any cell has multi-values
is_squashed = False
multi_words = []
for c in cells:
# E.g. 2 or more distinct words (length >= 2) in the cell
w_list = [w for w in c.split() if len(w) >= 2 and w not in ["-", "·", ""]]
if len(w_list) >= 2:
is_squashed = True
multi_words.extend(w_list)
if not is_squashed:
continue
# Now, check if these multi_words appear in PRECEDING rows of the SAME table as separate entries!
matched_preceding_rows = []
for prev_idx in range(r_idx):
prev_lno, prev_str, prev_cells = data_rows[prev_idx]
prev_words = set(w for c in prev_cells for w in c.split() if len(w) >= 2)
# Does prev_words share words with multi_words?
overlap = prev_words.intersection(set(multi_words))
# Filter out generic words like '인부', '보통인부'
overlap = {w for w in overlap if w not in ["인부", "보통인부", "특별인부", "플랜트", "설비공", "용접공", "배관공"]}
if overlap:
matched_preceding_rows.append((prev_lno, list(overlap), prev_str[:60]))
if len(matched_preceding_rows) >= 2:
# This squashed row matches 2 or more preceding distinct rows in the same table!
overlap_cases.append({
'file': fname,
'line': lno,
'table_start': t_start,
'squashed_row': r_str[:90],
'matched_preceding': matched_preceding_rows
})
print(f"Total squashed rows overlapping with preceding rows in same table: {len(overlap_cases)}")
for oc in overlap_cases:
print(f"\n[{oc['file']}:L{oc['line']}] (Table starting at L{oc['table_start']})")
print(f" Squashed row: {oc['squashed_row']}")
print(f" Matches {len(oc['matched_preceding'])} preceding rows:")
for plno, ow, pstr in oc['matched_preceding'][:5]:
print(f" L{plno} (overlap: {ow}): {pstr}")
@@ -0,0 +1,24 @@
import json, sys
sys.stdout.reconfigure(encoding='utf-8')
ch13_path = "resources/knowledge/original/원가계산/건설공사_표준품셈/04_기계설비부문/제13장_플랜트설비공사.md"
with open(ch13_path, 'r', encoding='utf-8') as f:
lines = f.readlines()
remaining = []
for idx, line in enumerate(lines):
line_str = line.strip()
if not (line_str.startswith('|') and line_str.endswith('|')):
continue
cells = [c.strip() for c in line_str.split('|')[1:-1]]
# Check if first cell has multiple numbers or multiple specs
for c_idx, c in enumerate(cells):
toks = c.split()
if len(toks) >= 6 and any(t[0].isdigit() for t in toks if t):
remaining.append((idx + 1, c_idx, len(toks), c[:80]))
break
print(f"Total multi-token rows in 제13장: {len(remaining)}")
for l_num, c_idx, cnt, snip in remaining[:20]:
print(f"L{l_num} col{c_idx} (tokens: {cnt}): {snip}")
@@ -0,0 +1,119 @@
import os, sys, re
sys.stdout.reconfigure(encoding='utf-8')
target_files = [
# 02_토목부문
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제1장_도로포장공사.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제2장_하천공사.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제3장_터널공사.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제4장_궤도공사.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제5장_강구조공사.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제6장_관부설및접합공사.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제7장_항만공사.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제8장_지반조사.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제9장_측량.md',
# 05_유지관리부문
'resources/knowledge/original/원가계산/건설공사_표준품셈/05_유지관리부문/제1장_공통.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/05_유지관리부문/제2장_토목.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/05_유지관리부문/제3장_건축.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/05_유지관리부문/제4장_기계설비.md',
# 04_기계설비부문 제13장
'resources/knowledge/original/원가계산/건설공사_표준품셈/04_기계설비부문/제13장_플랜트설비공사.md',
]
# Common worker / equipment names that do NOT represent condition categories
WORKER_EQUIP_KEYWORDS = {
'특별인부', '보통인부', '인부', '조력공', '포장공', '철근공', '형틀목공', '목공', '비계공', '석공',
'용접공', '배관공', '덕트공', '도장공', '미장공', '방수공', '잠수부', '보링공', '갱부', '착암공',
'궤도공', '전공', '통신공', '플랜트배관공', '기계설치공', '제관공', '용접기', '크레인', '트럭',
'굴착기', '로더', '덤프트럭', '진동롤러', '머캐덤롤러', '탠덤롤러', '타이어롤러', '살수차', '지게차',
'발전기', '공기압축기', '모터그레이더', '불도저', '아스팔트피니셔', '콘크리트페이버', '플레이트콤팩터',
'중급기술자', '초급기술자', '고급기술자', '특급기술자', '측량보조', '측량기사', '일반기계운전사'
}
def is_worker_or_equip(text):
clean = re.sub(r'[\s\(\)\d\+]+', '', text)
return any(w in clean for w in WORKER_EQUIP_KEYWORDS)
true_grouped_tables = []
for fpath in target_files:
fname = os.path.basename(fpath)
with open(fpath, 'r', encoding='utf-8') as f:
lines = f.readlines()
tables = []
cur_t = []
cur_start = 0
cur_title = ""
for idx, l in enumerate(lines):
line_str = l.strip()
if not line_str.startswith('|'):
if line_str and not line_str.startswith('>'):
cur_title = line_str
if cur_t:
tables.append((cur_start, cur_title, cur_t))
cur_t = []
else:
if not cur_t:
cur_start = idx + 1
cur_t.append((idx + 1, line_str))
if cur_t:
tables.append((cur_start, cur_title, cur_t))
for t_start, t_title, t_rows in tables:
data_rows = []
for lno, r_str in t_rows:
cells = [c.strip() for c in r_str.split('|')[1:-1]]
if all(re.match(r'^:?-+:?$', c) for c in cells if c):
continue
if cells and cells[0] in ["비고", "비 고", "", "[주]"]:
continue
data_rows.append((lno, r_str, cells))
if len(data_rows) < 3:
continue
# Check classification number in rows
has_item_no = False
for lno, r_str, cells in data_rows[1:]:
c0 = cells[0] if cells else ""
if re.match(r'^\d+-\d+(?:-\d+)?', c0) or re.match(r'^[A-Z]\d{4}', c0):
has_item_no = True
break
if has_item_no:
continue
# Analyze first column (and second column) to see if there are multi-row condition groupings
groups = {}
cur_group = None
for lno, r_str, cells in data_rows[1:]:
c0 = cells[0] if cells else ""
if c0 and not is_worker_or_equip(c0):
cur_group = c0
if cur_group:
groups.setdefault(cur_group, []).append((lno, r_str))
# If there are 2 or more true non-worker groups, this is a grouped table!
if len(groups) >= 2 and any(len(rows) >= 1 for rows in groups.values()):
true_grouped_tables.append({
'file': fname,
'line': t_start,
'title': t_title[:60],
'groups': list(groups.keys()),
'group_details': {k: len(v) for k, v in groups.items()},
'total_rows': len(data_rows)
})
print(f"=== Truly Grouped / Condition Branch Tables: {len(true_grouped_tables)} found ===")
by_f = {}
for tgt in true_grouped_tables:
by_f.setdefault(tgt['file'], []).append(tgt)
for f, tbls in sorted(by_f.items()):
print(f"\n[{f}] ({len(tbls)}개)")
for t in tbls:
g_str = ", ".join([f"{k}({v}행)" for k, v in t['group_details'].items()][:4])
print(f" L{t['line']:<5} | {t['title'][:35]:<35} | {g_str}")
@@ -0,0 +1,41 @@
import os, sys, re, json
sys.stdout.reconfigure(encoding='utf-8')
base_dir = "resources/knowledge/original/원가계산/건설공사_표준품셈"
all_md_files = []
for root, dirs, files in os.walk(base_dir):
for f in files:
if f.endswith('.md') and not f.startswith('_') and '개정사항' not in f and '2026년_건설공사_표준품셈.md' not in f:
if '01_공통부문\\제8장' in os.path.join(root, f) or '01_공통부문/제8장' in os.path.join(root, f):
continue
all_md_files.append(os.path.join(root, f))
text_tables = []
for fpath in sorted(all_md_files):
rel_path = os.path.relpath(fpath, base_dir)
with open(fpath, 'r', encoding='utf-8') as f:
lines = f.readlines()
for idx, line in enumerate(lines):
line_num = idx + 1
line_str = line.strip()
# Non-table line containing table keywords
if not line_str.startswith('|'):
if ('구 분' in line_str or '구분' in line_str) and ('단위' in line_str or '단 위' in line_str or '수량' in line_str or '수 량' in line_str):
# Filter out obvious false positives
if len(line_str) > 30:
text_tables.append({
"file": rel_path,
"line": line_num,
"snippet": line_str[:120]
})
print(f"Total non-table lines with embedded tables: {len(text_tables)}")
for tt in text_tables:
print(f"{tt['file']} L{tt['line']}: {tt['snippet']}")
with open('scratch/text_inline_tables.json', 'w', encoding='utf-8') as f:
json.dump(text_tables, f, ensure_ascii=False, indent=2)
@@ -0,0 +1,105 @@
import os, sys, re
sys.stdout.reconfigure(encoding='utf-8')
target_files = [
# 02_토목부문
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제1장_도로포장공사.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제2장_하천공사.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제3장_터널공사.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제4장_궤도공사.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제5장_강구조공사.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제6장_관부설및접합공사.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제7장_항만공사.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제8장_지반조사.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제9장_측량.md',
# 05_유지관리부문
'resources/knowledge/original/원가계산/건설공사_표준품셈/05_유지관리부문/제1장_공통.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/05_유지관리부문/제2장_토목.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/05_유지관리부문/제3장_건축.md',
'resources/knowledge/original/원가계산/건설공사_표준품셈/05_유지관리부문/제4장_기계설비.md',
# 04_기계설비부문 제13장
'resources/knowledge/original/원가계산/건설공사_표준품셈/04_기계설비부문/제13장_플랜트설비공사.md',
]
print("=== Scanning for Condition/Grouped Tables without Item Classification Numbers ===")
grouped_tables = []
for fpath in target_files:
fname = os.path.basename(fpath)
with open(fpath, 'r', encoding='utf-8') as f:
lines = f.readlines()
tables = []
cur_t = []
cur_start = 0
cur_title = ""
for idx, l in enumerate(lines):
line_str = l.strip()
if not line_str.startswith('|'):
if line_str and not line_str.startswith('>'):
cur_title = line_str
if cur_t:
tables.append((cur_start, cur_title, cur_t))
cur_t = []
else:
if not cur_t:
cur_start = idx + 1
cur_t.append((idx + 1, line_str))
if cur_t:
tables.append((cur_start, cur_title, cur_t))
for t_start, t_title, t_rows in tables:
data_rows = []
for lno, r_str in t_rows:
cells = [c.strip() for c in r_str.split('|')[1:-1]]
if all(re.match(r'^:?-+:?$', c) for c in cells if c):
continue
if cells and cells[0] in ["비고", "비 고", "", "[주]"]:
continue
data_rows.append((lno, r_str, cells))
if len(data_rows) < 3:
continue
# Check if rows have item numbers like 1-1, 1-2, 1-1-1 or 4-3-1 etc.
has_item_numbers = False
for lno, r_str, cells in data_rows[1:]:
c0 = cells[0] if cells else ""
if re.match(r'^\d+-\d+(?:-\d+)?', c0) or re.match(r'^[A-Z]\d{4}', c0):
has_item_numbers = True
break
if has_item_numbers:
continue # Safe, has explicit classification numbers
# Check if this table has condition/branch groups
# (e.g. multi-level condition headers or blank cells inheriting above groups)
has_grouping = False
group_col_values = []
for lno, r_str, cells in data_rows[1:]:
c0 = cells[0] if cells else ""
c1 = cells[1] if len(cells) > 1 else ""
# Inherited blank cell or condition keyword
if c0 == "" or c0 == "" or any(k in c0 for k in ["기층", "보조기층", "포장", "Type", "", "식재", "토사", "암반", "외업", "내업", "직선", "곡선"]):
has_grouping = True
if c0:
group_col_values.append(c0)
if has_grouping and len(set(group_col_values)) >= 2:
grouped_tables.append({
'file': fname,
'line': t_start,
'title': t_title[:60],
'rows_count': len(data_rows),
'groups': list(set(group_col_values))[:6],
'sample_header': data_rows[0][1][:70]
})
print(f"Total Condition/Grouped Tables found in our scope: {len(grouped_tables)}")
for gt in grouped_tables:
print(f"\n[{gt['file']}:L{gt['line']}] {gt['title']}")
print(f" Header: {gt['sample_header']}")
print(f" Groups: {gt['groups']} (Rows: {gt['rows_count']})")
@@ -0,0 +1,316 @@
import os, sys, re
sys.stdout.reconfigure(encoding='utf-8')
# 1. 03_건축부문/제1장_철골공사.md
ch1_path = "resources/knowledge/original/원가계산/건설공사_표준품셈/03_건축부문/제1장_철골공사.md"
with open(ch1_path, 'r', encoding='utf-8') as f:
text1 = f.read()
target_1_4_4 = "1-4-4 경량형강철골조 조립설치(ton당)수 량구 분단 위비 고내 력 식비 내 력 식철공인15.9312.54[주] ① 본 품은 건축구조용 표면처리 경량형강을 기준한 것이다."
repl_1_4_4 = """1-4-4 경량형강철골조 조립설치
(ton당)
| | | | | |
|---|---|---|---|---|
| | | | | |
| 철공 | | 15.93 | 12.54 | |
[] 품은 건축구조용 표면처리 경량형강을 기준한 것이다."""
text1 = text1.replace(target_1_4_4, repl_1_4_4)
with open(ch1_path, 'w', encoding='utf-8') as f:
f.write(text1)
print("Updated 제1장_철골공사.md successfully.")
# 2. 03_건축부문/제2장_조적공사.md
ch2_path = "resources/knowledge/original/원가계산/건설공사_표준품셈/03_건축부문/제2장_조적공사.md"
with open(ch2_path, 'r', encoding='utf-8') as f:
text2 = f.read()
target_2_1_4 = "2-1-4 아치쌓기 치장줄눈 설치('13년 보완)(1,000매당)수 량 (벽두께)구 분단 위1.0B1.5B줄눈공인0.40.3[주] ① 본 품은 아치쌓기 구간에 치장줄눈을 채우는 기준이다."
repl_2_1_4 = """2-1-4 아치쌓기 치장줄눈 설치('13년 보완)
(1,000매당)
| | | (벽두께) | |
|---|---|---|---|
| | | 1.0B | 1.5B |
| 줄눈공 | | 0.4 | 0.3 |
[] 품은 아치쌓기 구간에 치장줄눈을 채우는 기준이다."""
target_2_3_2 = "2-3-2 ALC패널 설치('13, '25년 보완)(일당)구 분단 위수 량패널두께(mm)시공량(㎡)7522조적공인3100191251615014보통인부인11751320011[주] ① 본 품은 경량콘크리트 패널의 내벽설치 기준이다."
repl_2_3_2 = """2-3-2 ALC패널 설치('13, '25년 보완)
(일당)
| | | | 패널두께(mm) | 시공량() |
|---|---|---|---|---|
| 조적공 | | 3 | 75 | 22 |
| 보통인부 | | 1 | 100 | 19 |
| | | | 125 | 16 |
| | | | 150 | 14 |
| | | | 175 | 13 |
| | | | 200 | 11 |
[] 품은 경량콘크리트 패널의 내벽설치 기준이다."""
text2 = text2.replace(target_2_1_4, repl_2_1_4)
text2 = text2.replace(target_2_3_2, repl_2_3_2)
with open(ch2_path, 'w', encoding='utf-8') as f:
f.write(text2)
print("Updated 제2장_조적공사.md successfully.")
# 3. 03_건축부문/제3장_타일공사.md
ch3_path = "resources/knowledge/original/원가계산/건설공사_표준품셈/03_건축부문/제3장_타일공사.md"
with open(ch3_path, 'r', encoding='utf-8') as f:
text3 = f.read()
target_3_2_3 = "3-2-3 접착 붙이기('98년 신설, '13, '16, '20, '25년 보완)(일당)구 분단 위수 량타일규격(㎡)시공량(㎡)0.04~0.10이하25타일공인20.11~0.20이하26보통인부인10.21~0.40이하28"
repl_3_2_3 = """3-2-3 접착 붙이기('98년 신설, '13, '16, '20, '25년 보완)
(일당)
| | | | 타일규격() | 시공량() |
|---|---|---|---|---|
| 타일공 | | 2 | 0.04~0.10이하 | 25 |
| 보통인부 | | 1 | 0.11~0.20이하 | 26 |
| | | | 0.21~0.40이하 | 28 |"""
text3 = text3.replace(target_3_2_3, repl_3_2_3)
with open(ch3_path, 'w', encoding='utf-8') as f:
f.write(text3)
print("Updated 제3장_타일공사.md successfully.")
# 4. 03_건축부문/제4장_목공사.md
ch4_path = "resources/knowledge/original/원가계산/건설공사_표준품셈/03_건축부문/제4장_목공사.md"
with open(ch4_path, 'r', encoding='utf-8') as f:
text4 = f.read()
target_4_3_3 = "4-3-3 목재데크틀 설치('24년 보완)(일당)구 분단 위수 량시공량(ton)철공인3평구조용접공인10.40보통인부인1철공인4계단구조용접공인10.32보통인부인2[주] ① 본 품은 철물(각관 및 형강)을 사용하여 데크틀(H-Beam 등 철골류 제외)을 설치하는 기준이다."
repl_4_3_3 = """4-3-3 목재데크틀 설치('24년 보완)
(일당)
| | | | 시공량(ton) |
|---|---|---|---|
| **평구조** | | | |
| 철공 | | 3 | 0.40 |
| 용접공 | | 1 | |
| 보통인부 | | 1 | |
| **계단구조** | | | |
| 철공 | | 4 | 0.32 |
| 용접공 | | 1 | |
| 보통인부 | | 2 | |
[] 품은 철물(각관 형강) 사용하여 데크틀(H-Beam 철골류 제외) 설치하는 기준이다."""
text4 = text4.replace(target_4_3_3, repl_4_3_3)
with open(ch4_path, 'w', encoding='utf-8') as f:
f.write(text4)
print("Updated 제4장_목공사.md successfully.")
# 5. 03_건축부문/제5장_수장공사.md
ch5_path = "resources/knowledge/original/원가계산/건설공사_표준품셈/03_건축부문/제5장_수장공사.md"
with open(ch5_path, 'r', encoding='utf-8') as f:
text5 = f.read()
target_5_4_2 = "5-4-2 단열재 접착제 붙이기('22, '24년 보완)(일당)시공량(㎡)구 분단 위수 량단열두께(mm)벽천장50이하4740내 장 공인2100이하4235200이하4032보통인부인1300이하3830[주] ① 본 품은 바탕면에 접착제를 사용하여 단열재를 설치하는 기준이다."
repl_5_4_2 = """5-4-2 단열재 접착제 붙이기('22, '24년 보완)
(일당)
| | | | 단열두께(mm) | 시공량() | |
|---|---|---|---|---|---|
| | | | | | 천장 |
| 내장공 | | 2 | 50이하 | 47 | 40 |
| 보통인부 | | 1 | 100이하 | 42 | 35 |
| | | | 200이하 | 40 | 32 |
| | | | 300이하 | 38 | 30 |
[] 품은 바탕면에 접착제를 사용하여 단열재를 설치하는 기준이다."""
target_5_4_9 = "5-4-9 외벽단열공법('99년 신설, '15, '22년 보완)(㎡당)단열두께(mm)구 분단 위60㎜이하100㎜이하200㎜이하내장공인0.0600.0630.081미장공인0.0380.0400.052보통인부인0.0310.0330.042"
repl_5_4_9 = """5-4-9 외벽단열공법('99년 신설, '15, '22년 보완)
()
| | | 단열두께(mm) | | |
|---|---|---|---|---|
| | | 60이하 | 100이하 | 200이하 |
| 내장공 | | 0.060 | 0.063 | 0.081 |
| 미장공 | | 0.038 | 0.040 | 0.052 |
| 보통인부 | | 0.031 | 0.033 | 0.042 |"""
# L149 주석 안 줄눈재 설치
target_5_joint = "| 비 고 | - 줄눈재 설치가 필요한 경우 다음을 적용한다. (일당) 구 분 단 위 수 량 시공량(m) 내 장 공 인 1 37"
repl_5_joint = """| 비 고 | - 줄눈재 설치가 필요한 경우 다음을 적용한다. (일당)
| | | | 시공량(m) |
|---|---|---|---|
| 내장공 | | 1 | 37 |"""
text5 = text5.replace(target_5_4_2, repl_5_4_2)
text5 = text5.replace(target_5_4_9, repl_5_4_9)
text5 = text5.replace(target_5_joint, repl_5_joint)
with open(ch5_path, 'w', encoding='utf-8') as f:
f.write(text5)
print("Updated 제5장_수장공사.md successfully.")
# 6. 03_건축부문/제6장_방수공사.md
ch6_path = "resources/knowledge/original/원가계산/건설공사_표준품셈/03_건축부문/제6장_방수공사.md"
with open(ch6_path, 'r', encoding='utf-8') as f:
text6 = f.read()
target_6_1_4 = "6-1-4 방수층 누름철물 설치('18년 신설)(m당)구 분단 위수 량방수공인0.011보통인부인0.011[주] 본 품은 시트 및 보호재 상부의 누름철물 마감 작업을 기준한 것이다."
repl_6_1_4 = """6-1-4 방수층 누름철물 설치('18년 신설)
(m당)
| | | |
|---|---|---|
| 방수공 | | 0.011 |
| 보통인부 | | 0.011 |
[] 품은 시트 보호재 상부의 누름철물 마감 작업을 기준한 것이다."""
target_6_6_2 = "6-6-2 줄눈 절단('18년 신설)(m당)구 분규 격단 위수 량방수공인0.005보통인부인0.001커터320~400㎜hr0.017[주] ① 본 품은 옥상 보호콘크리트의 절단을 기준한 것이다."
repl_6_6_2 = """6-6-2 줄눈 절단('18년 신설)
(m당)
| | | | |
|---|---|---|---|
| 방수공 | | | 0.005 |
| 보통인부 | | | 0.001 |
| 커터 | 320~400 | hr | 0.017 |
[] 품은 옥상 보호콘크리트의 절단을 기준한 것이다."""
target_6_6_3 = "6-6-3 줄눈 설치('18년 신설)(m당)구 분단 위수 량방수공인0.005보통인부인0.001[주] ① 본 품은 옥상 보호콘크리트의 줄눈 설치를 기준한 것이다."
repl_6_6_3 = """6-6-3 줄눈 설치('18년 신설)
(m당)
| | | |
|---|---|---|
| 방수공 | | 0.005 |
| 보통인부 | | 0.001 |
[] 품은 옥상 보호콘크리트의 줄눈 설치를 기준한 것이다."""
text6 = text6.replace(target_6_1_4, repl_6_1_4)
text6 = text6.replace(target_6_6_2, repl_6_6_2)
text6 = text6.replace(target_6_6_3, repl_6_6_3)
with open(ch6_path, 'w', encoding='utf-8') as f:
f.write(text6)
print("Updated 제6장_방수공사.md successfully.")
# 7. 03_건축부문/제7장_지붕및홈통공사.md
ch7_path = "resources/knowledge/original/원가계산/건설공사_표준품셈/03_건축부문/제7장_지붕및홈통공사.md"
with open(ch7_path, 'r', encoding='utf-8') as f:
text7 = f.read()
target_7_3_1 = "7-3 드레인7-3-1 루프드레인 설치('16년 보완)(개소당)구 분단 위수 량배관공인0.17보통인부인0.04[주] ① 본 품은 루프드레인 규격 ø100㎜∼150㎜의 설치 기준이다."
repl_7_3_1 = """7-3 드레인
7-3-1 루프드레인 설치('16년 보완)
(개소당)
| | | |
|---|---|---|
| 배관공 | | 0.17 |
| 보통인부 | | 0.04 |
[] 품은 루프드레인 규격 ø100150 설치 기준이다."""
text7 = text7.replace(target_7_3_1, repl_7_3_1)
with open(ch7_path, 'w', encoding='utf-8') as f:
f.write(text7)
print("Updated 제7장_지붕및홈통공사.md successfully.")
# 8. 03_건축부문/제9장_미장공사.md
ch9_path = "resources/knowledge/original/원가계산/건설공사_표준품셈/03_건축부문/제9장_미장공사.md"
with open(ch9_path, 'r', encoding='utf-8') as f:
text9 = f.read()
target_9_1_5 = "9-1-5 라스 붙임('17년 신설)(10㎡당)구 분단 위수 량미장공인0.14[주] 본 품은 미장면 보강을 위해 미장 시 메탈라스 또는 유리섬유메쉬를 붙이는 작업을 기준한 것이다."
repl_9_1_5 = """9-1-5 라스 붙임('17년 신설)
(10)
| | | |
|---|---|---|
| 미장공 | | 0.14 |
[] 품은 미장면 보강을 위해 미장 메탈라스 또는 유리섬유메쉬를 붙이는 작업을 기준한 것이다."""
target_9_3_2 = "9-3-2 창호주위 발포우레탄 충전('14년 신설, '20년 보완)(10m당)구 분단 위수 량미장공인0.08보통인부인0.03[주] ① 본 품은 창호틀 주위에 발포우레탄을 사용하여 충전하는 기준이다."
repl_9_3_2 = """9-3-2 창호주위 발포우레탄 충전('14년 신설, '20년 보완)
(10m당)
| | | |
|---|---|---|
| 미장공 | | 0.08 |
| 보통인부 | | 0.03 |
[] 품은 창호틀 주위에 발포우레탄을 사용하여 충전하는 기준이다."""
target_9_door = "| 비 고 | - 문선을 설치하는 경우 다음 품을 추가 계상한다. (m당) 구 분 단 위 수 량 창 호 공 인 0.010"
repl_9_door = """| 비 고 | - 문선을 설치하는 경우 다음 품을 추가 계상한다. (m당)
| | | |
|---|---|---|
| 창호공 | | 0.010 |"""
text9 = text9.replace(target_9_1_5, repl_9_1_5)
text9 = text9.replace(target_9_3_2, repl_9_3_2)
text9 = text9.replace(target_9_door, repl_9_door)
with open(ch9_path, 'w', encoding='utf-8') as f:
f.write(text9)
print("Updated 제9장_미장공사.md successfully.")
# 9. 03_건축부문/제10장_창호및유리공사.md
ch10_path = "resources/knowledge/original/원가계산/건설공사_표준품셈/03_건축부문/제10장_창호및유리공사.md"
with open(ch10_path, 'r', encoding='utf-8') as f:
text10 = f.read()
target_10_3_2 = "10-3-2 커튼월유리 설치('14, '20, '26년 보완)(㎡당)수 량구 분단 위12㎜이하16㎜이하18㎜이하22㎜이하24㎜이하28㎜이하유리공인0.1200.1310.1370.1390.1450.155보통인부인0.0200.0210.0220.0230.0240.025[주] ①본 품은 커튼월 프레임에 구조용실란트를 사용하여 복층유리를 부착하는 기준이다."
repl_10_3_2 = """10-3-2 커튼월유리 설치('14, '20, '26년 보완)
()
| | | | | | | | |
|---|---|---|---|---|---|---|---|
| | | 12이하 | 16이하 | 18이하 | 22이하 | 24이하 | 28이하 |
| 유리공 | | 0.120 | 0.131 | 0.137 | 0.139 | 0.145 | 0.155 |
| 보통인부 | | 0.020 | 0.021 | 0.022 | 0.023 | 0.024 | 0.025 |
[] 품은 커튼월 프레임에 구조용실란트를 사용하여 복층유리를 부착하는 기준이다."""
text10 = text10.replace(target_10_3_2, repl_10_3_2)
with open(ch10_path, 'w', encoding='utf-8') as f:
f.write(text10)
print("Updated 제10장_창호및유리공사.md successfully.")
# 10. 03_건축부문/제11장_칠공사.md
ch11_path = "resources/knowledge/original/원가계산/건설공사_표준품셈/03_건축부문/제11장_칠공사.md"
with open(ch11_path, 'r', encoding='utf-8') as f:
text11 = f.read()
target_11_3_3 = "11-3-3 석재도료칠('14년 신설)(100㎡당)구 분규 격단 위줄눈무늬(無)줄눈무늬(有)도장공인0.6200.810보통인부인0.1000.130고소작업차3tonhr3.2704.280[주] ① 본 품은 석재가 포함된 도료를 1회 뿜칠하는 기준이다."
repl_11_3_3 = """11-3-3 석재도료칠('14년 신설)
(100)
| | | | 줄눈무늬() | 줄눈무늬() |
|---|---|---|---|---|
| 도장공 | | | 0.620 | 0.810 |
| 보통인부 | | | 0.100 | 0.130 |
| 고소작업차 | 3ton | hr | 3.270 | 4.280 |
[] 품은 석재가 포함된 도료를 1 뿜칠하는 기준이다."""
text11 = text11.replace(target_11_3_3, repl_11_3_3)
with open(ch11_path, 'w', encoding='utf-8') as f:
f.write(text11)
print("Updated 제11장_칠공사.md successfully.")
@@ -0,0 +1,89 @@
import os, sys, re
sys.stdout.reconfigure(encoding='utf-8')
ch13_path = "resources/knowledge/original/원가계산/건설공사_표준품셈/04_기계설비부문/제13장_플랜트설비공사.md"
with open(ch13_path, 'r', encoding='utf-8') as f:
text = f.read()
# 1. L36: 배관용 탄소강관 KS D3507 (350 ~ 500)
old_l36 = "| 배 관 용 탄 소 강 관 KS D3507 | 350 〃 〃 400 〃 〃 450 〃 〃 500 〃 〃 〃 〃 | 355.6 〃 〃 406.4 〃 〃 457.2 〃 〃 508.0 〃 〃 〃 〃 | 6.0 6.4 7.9 6.0 6.4 7.9 6.0 6.4 7.9 6.0 6.4 7.9 8.7 9.5 | 51.7 55.1 67.7 59.2 63.1 77.6 66.8 71.1 87.5 74.3 79.2 97.4 107 117 | 19.3 18.7 16.8 19.5 19.5 16.7 19.4 19.5 16.7 19.5 19.4 16.6 16.2 13.3 | 9.7 9.3 8.4 9.3 8.4 8.4 9.3 8.3 8.3 9.2 8.3 8.3 7.6 9.5 | 9.7 9.3 8.4 9.3 8.4 8.4 9.3 8.3 8.3 9.2 8.3 8.3 7.6 9.5 | 19.3 18.7 16.8 19.5 19.5 16.7 19.4 19.5 16.7 19.5 19.4 16.6 16.2 13.3 |"
s36 = "350 〃 〃 400 〃 〃 450 〃 〃 500 〃 〃 〃 〃".split()
o36 = "355.6 〃 〃 406.4 〃 〃 457.2 〃 〃 508.0 〃 〃 〃 〃".split()
t36 = "6.0 6.4 7.9 6.0 6.4 7.9 6.0 6.4 7.9 6.0 6.4 7.9 8.7 9.5".split()
w36 = "51.7 55.1 67.7 59.2 63.1 77.6 66.8 71.1 87.5 74.3 79.2 97.4 107 117".split()
yw36 = "19.3 18.7 16.8 19.5 19.5 16.7 19.4 19.5 16.7 19.5 19.4 16.6 16.2 13.3".split()
yb36 = "9.7 9.3 8.4 9.3 8.4 8.4 9.3 8.3 8.3 9.2 8.3 8.3 7.6 9.5".split()
yi36 = "9.7 9.3 8.4 9.3 8.4 8.4 9.3 8.3 8.3 9.2 8.3 8.3 7.6 9.5".split()
nb36 = "19.3 18.7 16.8 19.5 19.5 16.7 19.4 19.5 16.7 19.5 19.4 16.6 16.2 13.3".split()
rows_36 = []
for i in range(len(s36)):
div_name = "배관용 탄소강관 KS D3507" if i == 0 else ""
row = f"| {div_name} | {s36[i]} | {o36[i]} | {t36[i]} | {w36[i]} | {yw36[i]} | {yb36[i]} | {yi36[i]} | {nb36[i]} |"
rows_36.append(row)
new_l36 = "\n".join(rows_36)
text = text.replace(old_l36, new_l36)
# 2. L45: 배관용 탄소강관 KS D3507 (550 ~ 600)
old_l45 = "| 배 관 용 탄 소 강 관 KS D3507 | 550 〃 〃 〃 600 〃 〃 〃 | 558.8 〃 〃 〃 609.6 〃 〃 〃 | 6.0 6.4 7.9 9.5 6.0 6.4 7.1 7.9 | 81.8 87.2 107 129 89.0 95.2 106 117 | 19.1 18.5 16.7 15.1 19.1 18.4 17.5 16.6 | 9.5 9.2 8.3 7.6 9.5 9.2 8.7 8.3 | 9.5 9.2 8.3 7.6 9.5 9.2 8.7 8.3 | 19.1 18.5 16.7 15.1 19.1 18.4 17.5 16.6 |"
s45 = "550 〃 〃 〃 600 〃 〃 〃".split()
o45 = "558.8 〃 〃 〃 609.6 〃 〃 〃".split()
t45 = "6.0 6.4 7.9 9.5 6.0 6.4 7.1 7.9".split()
w45 = "81.8 87.2 107 129 89.0 95.2 106 117".split()
yw45 = "19.1 18.5 16.7 15.1 19.1 18.4 17.5 16.6".split()
yb45 = "9.5 9.2 8.3 7.6 9.5 9.2 8.7 8.3".split()
yi45 = "9.5 9.2 8.3 7.6 9.5 9.2 8.7 8.3".split()
nb45 = "19.1 18.5 16.7 15.1 19.1 18.4 17.5 16.6".split()
rows_45 = []
for i in range(len(s45)):
div_name = "배관용 탄소강관 KS D3507" if i == 0 else ""
row = f"| {div_name} | {s45[i]} | {o45[i]} | {t45[i]} | {w45[i]} | {yw45[i]} | {yb45[i]} | {yi45[i]} | {nb45[i]} |"
rows_45.append(row)
new_l45 = "\n".join(rows_45)
text = text.replace(old_l45, new_l45)
# 3. L308: 13-1-2 관만곡 설치 (PDF 732쪽 대조 18행 전개)
old_l308 = "| ø25 32 40 50 65 80 90 100 125 150 200 250 300 350 400 450 500 600 | | 0.035 0.040 0.045 0.050 0.060 0.070 0.085 0.100 0.130 0.160 0.20 0.28 0.38 0.48 0.63 0.81 1.00 1.50 | 0.015 0.015 0.020 0.020 0.025 0.030 0.035 0.045 0.055 0.070 0.09 0.12 0.16 0.20 0.27 0.35 0.45 0.75 | 0.040 0.045 0.055 0.065 0.075 0.085 0.110 0.120 0.130 0.170 0.25 0.32 0.45 0.57 0.76 0.96 1.19 1.70 | 0.020 0.020 0.020 0.025 0.030 0.035 0.045 0.050 0.060 0.075 0.11 0.14 0.19 0.24 0.32 0.42 0.52 0.75 | 0.040 0.050 0.060 0.075 0.090 0.100 0.110 0.140 0.170 0.200 0.28 0.38 0.53 0.77 1.10 1.55 | 0.020 0.020 0.025 0.030 0.035 0.045 0.050 0.060 0.075 0.085 0.12 0.17 0.23 0.33 0.51 0.73 | 0.050 0.055 0.065 0.075 0.100 0.120 0.135 0.160 0.200 0.240 0.32 0.46 0.63 1.00 1.40 1.75 | 0.020 0.025 0.030 0.035 0.045 0.050 0.060 0.070 0.085 0.110 0.14 0.20 0.27 0.43 0.60 0.75 | 0.055 0.060 0.065 0.080 0.100 0.115 0.130 0.150 0.200 0.270 0.28 0.38 0.52 0.68 0.90 1.15 1.46 2.30 | 0.020 0.025 0.030 0.035 0.040 0.045 0.055 0.065 0.080 0.095 0.12 0.16 0.22 0.29 0.38 0.49 0.62 0.90 |"
cols_308 = [c.strip().split() for c in old_l308.split('|')[1:-1]]
specs_308 = cols_308[0] # 18 specs
rows_308 = []
for i in range(len(specs_308)):
r = [specs_308[i], ""]
# cols 2..5: 18 values
for c_idx in [2, 3, 4, 5]:
r.append(cols_308[c_idx][i])
# cols 6..9: 16 values (500, 600 have no U-곡관)
for c_idx in [6, 7, 8, 9]:
r.append(cols_308[c_idx][i] if i < 16 else "-")
# cols 10..11: 18 values
for c_idx in [10, 11]:
r.append(cols_308[c_idx][i])
rows_308.append("| " + " | ".join(r) + " |")
new_l308 = "\n".join(rows_308)
text = text.replace(old_l308, new_l308)
# 4. L345: 13-1-2 관만곡 U곡관 (PDF 733쪽 대조 18행 전개)
old_l345 = "| ø25 32 40 50 65 80 90 100 125 150 200 250 300 350 400 450 500 600 | | 0.075 0.090 0.110 0.130 0.160 0.190 0.230 0.260 0.320 0.380 0.540 0.740 1.000 1.450 2.170 | 0.035 0.040 0.045 0.055 0.070 0.080 0.095 0.110 0.130 0.160 0.230 0.310 0.420 0.620 0.930 | 0.100 0.120 0.140 0.170 0.200 0.230 0.270 0.310 0.380 0.440 0.560 0.860 1.200 1.660 2.200 | 0.040 0.050 0.060 0.070 0.080 0.095 0.110 0.130 0.160 0.190 0.240 0.360 0.510 0.710 0.940 | 0.100 0.110 0.130 0.150 0.180 0.220 0.270 0.320 0.380 0.480 0.590 0.840 1.330 1.830 - | 0.040 0.050 0.060 0.070 0.080 0.095 0.110 0.125 0.160 0.200 0.250 0.360 0.570 0.830 - | 0.120 0.140 0.160 0.190 0.220 0.250 0.290 0.330 0.430 0.540 0.700 0.990 1.400 - - | 0.050 0.060 0.070 0.080 0.095 0.110 0.125 0.145 0.190 0.230 0.300 0.420 0.510 - - |"
cols_345 = [c.strip().split() for c in old_l345.split('|')[1:-1]]
specs_345 = cols_345[0] # 18 specs
rows_345 = []
for i in range(len(specs_345)):
r = [specs_345[i], ""]
# cols 2..5: 15 values (25..400), 450..600 are "-"
for c_idx in [2, 3, 4, 5]:
r.append(cols_345[c_idx][i] if i < 15 else "-")
# cols 6..7: 14 values (25..350), 400..600 are "-"
for c_idx in [6, 7]:
r.append(cols_345[c_idx][i] if i < 14 else "-")
# cols 8..9: 13 values (25..300), 350..600 are "-"
for c_idx in [8, 9]:
r.append(cols_345[c_idx][i] if i < 13 else "-")
rows_345.append("| " + " | ".join(r) + " |")
new_l345 = "\n".join(rows_345)
text = text.replace(old_l345, new_l345)
with open(ch13_path, 'w', encoding='utf-8') as f:
f.write(text)
print("Updated L36, L45, L308, L345 in 제13장_플랜트설비공사.md successfully.")
@@ -0,0 +1,53 @@
import os, sys
sys.stdout.reconfigure(encoding='utf-8')
ch13_path = "resources/knowledge/original/원가계산/건설공사_표준품셈/04_기계설비부문/제13장_플랜트설비공사.md"
with open(ch13_path, 'r', encoding='utf-8') as f:
text = f.read()
# 1. Line 83
old_l83 = "| 배 관 용 탄 소 강 관 KS D3507 | 600 〃 650 〃 〃 〃 〃 700 〃 〃 〃 〃 750 〃 〃 〃 800 〃 〃 〃 850 〃 | 609.6 〃 660.4 〃 〃 〃 〃 711.2 〃 〃 〃 〃 762.0 〃 〃 〃 812.8 〃 〃 〃 863.6 〃 | 9.5 10.3 6.0 6.4 7.1 7.9 11.1 6.0 6.4 7.1 7.9 11.9 6.4 7.1 7.9 11.9 6.4 7.1 7.9 11.9 6.4 7.1 | 141 152 96.8 103 114 127 178 104 111 123 137 205 119 132 147 220 127 141 157 235 135 150 | 15.1 14.5 19.0 18.4 17.5 16.6 14.0 19.0 18.4 17.5 16.5 13.5 18.4 17.5 16.5 13.5 18.3 17.4 16.5 13.5 18.3 17.4 | 7.6 7.3 9.5 9.2 8.8 8.3 7.0 9.5 9.2 8.7 8.3 6.7 9.2 8.7 8.3 6.7 9.2 8.7 8.2 6.7 9.2 8.7 | 7.6 7.3 9.5 9.2 8.8 8.3 7.0 9.5 9.2 8.7 8.3 6.7 9.2 8.7 8.3 6.7 9.2 8.7 8.2 6.7 9.2 8.7 | 15.1 14.5 19.0 18.4 17.5 16.6 14.0 19.0 18.4 17.5 16.5 13.5 18.4 17.5 16.5 13.5 18.3 17.4 16.5 13.5 18.3 17.4 |"
c_specs = "600 〃 650 〃 〃 〃 〃 700 〃 〃 〃 〃 750 〃 〃 〃 800 〃 〃 〃 850 〃".split()
c_out = "609.6 〃 660.4 〃 〃 〃 〃 711.2 〃 〃 〃 〃 762.0 〃 〃 〃 812.8 〃 〃 〃 863.6 〃".split()
c_th = "9.5 10.3 6.0 6.4 7.1 7.9 11.1 6.0 6.4 7.1 7.9 11.9 6.4 7.1 7.9 11.9 6.4 7.1 7.9 11.9 6.4 7.1".split()
c_wt = "141 152 96.8 103 114 127 178 104 111 123 137 205 119 132 147 220 127 141 157 235 135 150".split()
c_yw = "15.1 14.5 19.0 18.4 17.5 16.6 14.0 19.0 18.4 17.5 16.5 13.5 18.4 17.5 16.5 13.5 18.3 17.4 16.5 13.5 18.3 17.4".split()
c_yb = "7.6 7.3 9.5 9.2 8.8 8.3 7.0 9.5 9.2 8.7 8.3 6.7 9.2 8.7 8.3 6.7 9.2 8.7 8.2 6.7 9.2 8.7".split()
c_yi = "7.6 7.3 9.5 9.2 8.8 8.3 7.0 9.5 9.2 8.7 8.3 6.7 9.2 8.7 8.3 6.7 9.2 8.7 8.2 6.7 9.2 8.7".split()
c_nb = "15.1 14.5 19.0 18.4 17.5 16.6 14.0 19.0 18.4 17.5 16.5 13.5 18.4 17.5 16.5 13.5 18.3 17.4 16.5 13.5 18.3 17.4".split()
rows_l83 = []
for i in range(len(c_specs)):
div_name = "배관용 탄소강관 KS D3507" if i == 0 else ""
row = f"| {div_name} | {c_specs[i]} | {c_out[i]} | {c_th[i]} | {c_wt[i]} | {c_yw[i]} | {c_yb[i]} | {c_yi[i]} | {c_nb[i]} |"
rows_l83.append(row)
new_l83 = "\n".join(rows_l83)
text = text.replace(old_l83, new_l83)
# 2. Line 176
old_l176 = "| 압력배관용 탄 소 강 관 KSD3562 SCH#40 | 15 20 25 32 40 50 65 80 90 100 125 150 200 250 300 350 400 450 500 | 21.7 27.2 34.0 42.7 48.6 60.5 76.3 89.1 101.6 114.3 139.8 165.2 216.3 267.4 318.5 355.6 406.4 457.2 508.0 | 2.8 2.9 3.4 3.6 3.7 3.9 5.2 5.5 5.7 6.0 6.6 7.1 8.2 9.3 10.3 11.1 12.7 14.3 15.1 | 1.31 1.74 2.57 3.47 4.10 5.44 9.12 11.3 13.5 16.0 21.7 27.7 42.1 59.2 78.3 94.3 123 156 184 | 47.0 41.8 35.2 32.0 30.4 28.2 23.4 22.2 21.5 20.7 19.3 18.4 16.0 15.7 14.8 14.2 13.3 12.5 12.1 | 23.5 20.9 17.6 16.0 15.2 14.1 11.7 11.1 10.7 10.3 9.7 9.2 8.0 7.8 7.4 7.1 6.6 6.2 6.0 | 23.5 20.9 17.6 16.0 15.2 14.1 11.7 11.1 10.7 10.3 9.7 9.2 8.0 7.8 7.4 7.1 6.6 6.2 6.0 | 47.0 41.8 35.2 32.0 30.4 28.2 23.4 22.2 21.5 20.7 19.3 18.4 16.0 15.7 14.8 14.2 13.3 12.5 12.1 |"
p_specs = "15 20 25 32 40 50 65 80 90 100 125 150 200 250 300 350 400 450 500".split()
p_out = "21.7 27.2 34.0 42.7 48.6 60.5 76.3 89.1 101.6 114.3 139.8 165.2 216.3 267.4 318.5 355.6 406.4 457.2 508.0".split()
p_th = "2.8 2.9 3.4 3.6 3.7 3.9 5.2 5.5 5.7 6.0 6.6 7.1 8.2 9.3 10.3 11.1 12.7 14.3 15.1".split()
p_wt = "1.31 1.74 2.57 3.47 4.10 5.44 9.12 11.3 13.5 16.0 21.7 27.7 42.1 59.2 78.3 94.3 123 156 184".split()
p_yw = "47.0 41.8 35.2 32.0 30.4 28.2 23.4 22.2 21.5 20.7 19.3 18.4 16.0 15.7 14.8 14.2 13.3 12.5 12.1".split()
p_yb = "23.5 20.9 17.6 16.0 15.2 14.1 11.7 11.1 10.7 10.3 9.7 9.2 8.0 7.8 7.4 7.1 6.6 6.2 6.0".split()
p_yi = "23.5 20.9 17.6 16.0 15.2 14.1 11.7 11.1 10.7 10.3 9.7 9.2 8.0 7.8 7.4 7.1 6.6 6.2 6.0".split()
p_nb = "47.0 41.8 35.2 32.0 30.4 28.2 23.4 22.2 21.5 20.7 19.3 18.4 16.0 15.7 14.8 14.2 13.3 12.5 12.1".split()
rows_l176 = []
for i in range(len(p_specs)):
div_name = "압력배관용 탄소강관 KSD3562 SCH#40" if i == 0 else ""
row = f"| {div_name} | {p_specs[i]} | {p_out[i]} | {p_th[i]} | {p_wt[i]} | {p_yw[i]} | {p_yb[i]} | {p_yi[i]} | {p_nb[i]} |"
rows_l176.append(row)
new_l176 = "\n".join(rows_l176)
text = text.replace(old_l176, new_l176)
with open(ch13_path, 'w', encoding='utf-8') as f:
f.write(text)
print("Updated L83 and L176 in 제13장_플랜트설비공사.md successfully.")
@@ -0,0 +1,69 @@
import os, sys
sys.stdout.reconfigure(encoding='utf-8')
ch13_path = "resources/knowledge/original/원가계산/건설공사_표준품셈/04_기계설비부문/제13장_플랜트설비공사.md"
with open(ch13_path, 'r', encoding='utf-8') as f:
text = f.read()
# 1. L149
old_l149 = "| 배 관 용 탄 소 강 관 KS D3 50 7 | 850 〃 〃 900 〃 〃 〃 1000 〃 1100 〃 1200 〃 1350 〃 | 863.6 〃 〃 914.4 〃 〃 〃 1016.0 〃 1117.6 〃 1219.2 〃 1371.6 〃 | 7.9 9.5 12.7 6.4 7.9 8.7 12.7 8.7 10.3 10.3 11.1 11.1 11.9 11.9 12.7 | 167 200 266 143 177 194 282 216 255 281 303 331 354 399 426 | 16.5 15.1 13.1 18.3 16.5 15.7 13.0 15.7 14.5 14.4 13.8 13.9 13.4 13.4 12.9 | 8.2 7.5 6.5 9.2 8.2 7.9 6.5 7.8 7.2 7.2 6.9 6.9 6.7 6.7 6.5 | 8.2 7.5 6.5 9.2 8.2 7.9 6.5 7.8 7.2 7.2 6.9 6.9 6.7 6.7 6.5 | 16.5 15.1 13.1 18.3 16.5 15.7 13.0 15.7 14.5 14.4 13.8 13.9 13.4 13.4 12.9 |"
s149 = "850 〃 〃 900 〃 〃 〃 1000 〃 1100 〃 1200 〃 1350 〃".split()
o149 = "863.6 〃 〃 914.4 〃 〃 〃 1016.0 〃 1117.6 〃 1219.2 〃 1371.6 〃".split()
t149 = "7.9 9.5 12.7 6.4 7.9 8.7 12.7 8.7 10.3 10.3 11.1 11.1 11.9 11.9 12.7".split()
w149 = "167 200 266 143 177 194 282 216 255 281 303 331 354 399 426".split()
yw149 = "16.5 15.1 13.1 18.3 16.5 15.7 13.0 15.7 14.5 14.4 13.8 13.9 13.4 13.4 12.9".split()
yb149 = "8.2 7.5 6.5 9.2 8.2 7.9 6.5 7.8 7.2 7.2 6.9 6.9 6.7 6.7 6.5".split()
yi149 = "8.2 7.5 6.5 9.2 8.2 7.9 6.5 7.8 7.2 7.2 6.9 6.9 6.7 6.7 6.5".split()
nb149 = "16.5 15.1 13.1 18.3 16.5 15.7 13.0 15.7 14.5 14.4 13.8 13.9 13.4 13.4 12.9".split()
rows_149 = []
for i in range(len(s149)):
div_name = "배관용 탄소강관 KS D3507" if i == 0 else ""
row = f"| {div_name} | {s149[i]} | {o149[i]} | {t149[i]} | {w149[i]} | {yw149[i]} | {yb149[i]} | {yi149[i]} | {nb149[i]} |"
rows_149.append(row)
new_l149 = "\n".join(rows_149)
text = text.replace(old_l149, new_l149)
# 2. L158
old_l158 = "| 배 관 용 탄 소 강 관 KS D3 50 7 | 1350 1500 〃 〃 | 1371.6 1574 〃 〃 | 13.1 12.7 13.1 15.1 | 439 473 488 562 | 12.7 13.1 12.9 12.1 | 6.4 6.6 6.5 6.0 | 6.4 6.6 6.5 6.0 | 12.7 13.1 12.9 12.1 |"
s158 = "1350 1500 〃 〃".split()
o158 = "1371.6 1574 〃 〃".split()
t158 = "13.1 12.7 13.1 15.1".split()
w158 = "439 473 488 562".split()
yw158 = "12.7 13.1 12.9 12.1".split()
yb158 = "6.4 6.6 6.5 6.0".split()
yi158 = "6.4 6.6 6.5 6.0".split()
nb158 = "12.7 13.1 12.9 12.1".split()
rows_158 = []
for i in range(len(s158)):
div_name = "배관용 탄소강관 KS D3507" if i == 0 else ""
row = f"| {div_name} | {s158[i]} | {o158[i]} | {t158[i]} | {w158[i]} | {yw158[i]} | {yb158[i]} | {yi158[i]} | {nb158[i]} |"
rows_158.append(row)
new_l158 = "\n".join(rows_158)
text = text.replace(old_l158, new_l158)
# 3. L159
old_l159 = "| 압력배관용 탄 소 강 관 KSD3562 SCH#40 | 6 8 10 | 10.5 13.8 17.3 | 1.7 2.2 2.3 | 0.369 0.629 0.851 | 101.3 70.7 59.9 | 50.7 35.3 29.9 | 50.7 35.3 29.9 | 101.3 70.7 59.9 |"
s159 = "6 8 10".split()
o159 = "10.5 13.8 17.3".split()
t159 = "1.7 2.2 2.3".split()
w159 = "0.369 0.629 0.851".split()
yw159 = "101.3 70.7 59.9".split()
yb159 = "50.7 35.3 29.9".split()
yi159 = "50.7 35.3 29.9".split()
nb159 = "101.3 70.7 59.9".split()
rows_159 = []
for i in range(len(s159)):
div_name = "압력배관용 탄소강관 KSD3562 SCH#40" if i == 0 else ""
row = f"| {div_name} | {s159[i]} | {o159[i]} | {t159[i]} | {w159[i]} | {yw159[i]} | {yb159[i]} | {yi159[i]} | {nb159[i]} |"
rows_159.append(row)
new_l159 = "\n".join(rows_159)
text = text.replace(old_l159, new_l159)
with open(ch13_path, 'w', encoding='utf-8') as f:
f.write(text)
print("Updated L149, L158, L159 in 제13장_플랜트설비공사.md successfully.")
@@ -0,0 +1,107 @@
import os, sys, re
sys.stdout.reconfigure(encoding='utf-8')
ch13_path = "resources/knowledge/original/원가계산/건설공사_표준품셈/04_기계설비부문/제13장_플랜트설비공사.md"
with open(ch13_path, 'r', encoding='utf-8') as f:
text = f.read()
# 1. L459 (Flange Type Valve)
# Notice: old_l459
lines = text.split('\n')
target_idx = -1
for i, l in enumerate(lines):
if l.startswith("| ø50 65 80 90 100 125 150 200 250 300 350 400 450 500 600 | 0.100 0.133"):
target_idx = i
break
if target_idx != -1:
l_str = lines[target_idx]
cells = [c.strip() for c in l_str.split('|')[1:-1]]
specs = cells[0].split() # 15 specs
# cells 1..10 each have 15 or 10 values (col 11, 12 have 10 values)
cols = [c.split() for c in cells]
rows = []
for r_i in range(len(specs)):
r = [specs[r_i]]
for c_i in range(1, len(cols)):
vals = cols[c_i]
val = vals[r_i] if r_i < len(vals) else "-"
r.append(val)
rows.append("| " + " | ".join(r) + " |")
lines[target_idx] = "\n".join(rows)
print("Fixed Flange Type Valve table (15 rows)")
# 2. Check 13-1-4 Fitting Screwed / Welded (around L492)
for i, l in enumerate(lines):
if l.startswith("| ø50 65 80 90 100 150 200 250 300 350 400 450 500 600 |"):
cells = [c.strip() for c in l.split('|')[1:-1]]
specs = cells[0].split()
cols = [c.split() for c in cells]
rows = []
for r_i in range(len(specs)):
r = [specs[r_i]]
for c_i in range(1, len(cols)):
vals = cols[c_i]
val = vals[r_i] if r_i < len(vals) else "-"
r.append(val)
rows.append("| " + " | ".join(r) + " |")
lines[i] = "\n".join(rows)
print(f"Fixed Fitting table at line {i+1} ({len(specs)} rows)")
# 3. Check phi 50..600 table (around L542)
for i, l in enumerate(lines):
if l.startswith("| φ 50 65 80 90 100 125 150 200 250 300 350 400 450 500 600 |"):
cells = [c.strip() for c in l.split('|')[1:-1]]
specs = cells[0].split()
cols = [c.split() for c in cells]
rows = []
for r_i in range(len(specs)):
r = [specs[r_i]]
for c_i in range(1, len(cols)):
vals = cols[c_i]
val = vals[r_i] if r_i < len(vals) else "-"
r.append(val)
rows.append("| " + " | ".join(r) + " |")
lines[i] = "\n".join(rows)
print(f"Fixed phi table at line {i+1} ({len(specs)} rows)")
# 4. Check phi 15..350 table (around L852)
for i, l in enumerate(lines):
if l.startswith("| φ 15 20 25 40 50 65 80 90 100 125 150 200 250 300 350 |"):
cells = [c.strip() for c in l.split('|')[1:-1]]
specs = cells[0].split()
cols = [c.split() for c in cells]
rows = []
for r_i in range(len(specs)):
r = [specs[r_i]]
for c_i in range(1, len(cols)):
vals = cols[c_i]
val = vals[r_i] if r_i < len(vals) else "-"
r.append(val)
rows.append("| " + " | ".join(r) + " |")
lines[i] = "\n".join(rows)
print(f"Fixed phi 15..350 table at line {i+1} ({len(specs)} rows)")
# 5. Check phi 15..150 table (around L882)
for i, l in enumerate(lines):
if l.startswith("| φ 15 20 25 40 50 65 80 90 100 125 150 |"):
cells = [c.strip() for c in l.split('|')[1:-1]]
specs = cells[0].split()
cols = [c.split() for c in cells]
rows = []
for r_i in range(len(specs)):
r = [specs[r_i]]
for c_i in range(1, len(cols)):
vals = cols[c_i]
val = vals[r_i] if r_i < len(vals) else "-"
r.append(val)
rows.append("| " + " | ".join(r) + " |")
lines[i] = "\n".join(rows)
print(f"Fixed phi 15..150 table at line {i+1} ({len(specs)} rows)")
text = "\n".join(lines)
with open(ch13_path, 'w', encoding='utf-8') as f:
f.write(text)
print("Saved updated 제13장_플랜트설비공사.md successfully.")
@@ -0,0 +1,246 @@
import os, sys, re
sys.stdout.reconfigure(encoding='utf-8')
# 1. 02_토목부문/제1장_도로포장공사.md
ch1_path = "resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제1장_도로포장공사.md"
with open(ch1_path, 'r', encoding='utf-8') as f:
text1 = f.read()
target_1_5_2 = "1-5-2 아스팔트 기층 소규모포설(일당)배치인원(인)규 격단 위수 량시공량(㎡)포장공인2보통인부인1플레이트콤팩터1.5ton대1320진동롤러( 핸드가이드식)0.7ton대1로더(타이어)0.57㎥대1살수차5,500L대0.5[주] ① 본 품은 소로, 주택가내 도로 등 피니셔를 사용하지 못하는 소규모 아스팔트 기층 포설 기준이다."
repl_1_5_2 = """1-5-2 아스팔트 기층 소규모포설
(일당)
| | | | | 시공량() |
|---|---|---|---|---|
| 포장공 | | | 2 | 320 |
| 보통인부 | | | 1 | |
| 플레이트콤팩터 | 1.5ton | | 1 | |
| 진동롤러(핸드가이드식) | 0.7ton | | 1 | |
| 로더(타이어) | 0.57 | | 1 | |
| 살수차 | 5,500L | | 0.5 | |
[] 품은 소로, 주택가내 도로 피니셔를 사용하지 못하는 소규모 아스팔트 기층 포설 기준이다."""
target_1_6_1 = "1-6-1 린 콘크리트 기층 포설(일당)시공량(㎥)구 분규 격단 위수 량일반포장터널포장포장공인2보통인부인2아스팔트피니셔3m대1550500타이어롤러815ton대1진동롤러10ton대1[주] ① 본 품은 피니셔를 사용한 린 콘크리트의 기층 포설 기준이다."
repl_1_6_1 = """1-6-1 린 콘크리트 기층 포설
(일당)
| | | | | 시공량() | |
|---|---|---|---|---|---|
| | | | | 일반포장 | 터널포장 |
| 포장공 | | | 2 | 550 | 500 |
| 보통인부 | | | 2 | | |
| 아스팔트피니셔 | 3m | | 1 | | |
| 타이어롤러 | 815ton | | 1 | | |
| 진동롤러 | 10ton | | 1 | | |
[] 품은 피니셔를 사용한 콘크리트의 기층 포설 기준이다."""
target_wire1 = "- 2. 와이어 설치(일당)구 분단 위수 량시공량(m)특별인부인4200보통인부인2[주] ① 본 품은 높이 3m이하 낙석방지책의 와이어를 설치하는 기준이다."
repl_wire1 = """- 2. 와이어 설치(일당)
| | | | 시공량(m) |
|---|---|---|---|
| 특별인부 | | 4 | 200 |
| 보통인부 | | 2 | |
[] 품은 높이 3m이하 낙석방지책의 와이어를 설치하는 기준이다."""
target_wire2 = "- 2. 철망 및 와이어 설치(일당)구 분규 격단 위수 량시공량(㎡)특별인부인2기계식보통인부인3400크레인50ton대1특별인부인2인력식100보통인부인3[주] ① 본 품은 낙석방지망(포켓식, 비포켓식)의 철망 및 와이어로프를 설치하는 기준이다."
repl_wire2 = """- 2. 철망 및 와이어 설치(일당)
| | | | | 시공량() |
|---|---|---|---|---|
| **기계식** | | | | |
| 특별인부 | | | 2 | 400 |
| 보통인부 | | | 3 | |
| 크레인 | 50ton | | 1 | |
| **인력식** | | | | |
| 특별인부 | | | 2 | 100 |
| 보통인부 | | | 3 | |
[] 품은 낙석방지망(포켓식, 비포켓식) 철망 와이어로프를 설치하는 기준이다."""
text1 = text1.replace(target_1_5_2, repl_1_5_2)
text1 = text1.replace(target_1_6_1, repl_1_6_1)
text1 = text1.replace(target_wire1, repl_wire1)
text1 = text1.replace(target_wire2, repl_wire2)
with open(ch1_path, 'w', encoding='utf-8') as f:
f.write(text1)
print("Updated 제1장_도로포장공사.md successfully.")
# 2. 02_토목부문/제2장_하천공사.md
ch2_path = "resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제2장_하천공사.md"
with open(ch2_path, 'r', encoding='utf-8') as f:
text2 = f.read()
target_2_2_2 = "2-2-2 매트리스형 돌망태 설치('07, '12년 보완)(㎡당)구 분규 격단 위수 량석공-인0.027특별인부-인0.010보통인부-인0.010굴착기1.0㎥hr0.025[주] ① 본 품은 매트리스형 돌망태(폭 200㎝, 높이 30㎝)를 설치하는 기준이다."
repl_2_2_2 = """2-2-2 매트리스형 돌망태 설치('07, '12년 보완)
()
| | | | |
|---|---|---|---|
| 석공 | - | | 0.027 |
| 특별인부 | - | | 0.010 |
| 보통인부 | - | | 0.010 |
| 굴착기 | 1.0 | hr | 0.025 |
[] 품은 매트리스형 돌망태( 200, 높이 30) 설치하는 기준이다."""
target_2_3_2 = "2-3-2 블록 붙이기(인력)('12년 보완)(㎡당)구 분규 격단 위수 량인0.076특별인부보통인부인0.066[주] ① 본 품은 하천제방에 인력으로 호안블록을 설치하는 기준이다."
repl_2_3_2 = """2-3-2 블록 붙이기(인력)('12년 보완)
()
| | | | |
|---|---|---|---|
| 특별인부 | - | | 0.076 |
| 보통인부 | - | | 0.066 |
[] 품은 하천제방에 인력으로 호안블록을 설치하는 기준이다."""
target_2_3_3 = "2-3-3 블록 붙이기(기계)('12년 보완)(㎡당)구 분규 격단 위수 량특별인부인0.017보통인부인0.007크레인10 톤시간0.048[주] ① 본 품은 하천제방에 장비를 사용하여 호안블록을 설치하는 기준이다."
repl_2_3_3 = """2-3-3 블록 붙이기(기계)('12년 보완)
()
| | | | |
|---|---|---|---|
| 특별인부 | - | | 0.017 |
| 보통인부 | - | | 0.007 |
| 크레인 | 10 | 시간 | 0.048 |
[] 품은 하천제방에 장비를 사용하여 호안블록을 설치하는 기준이다."""
text2 = text2.replace(target_2_2_2, repl_2_2_2)
text2 = text2.replace(target_2_3_2, repl_2_3_2)
text2 = text2.replace(target_2_3_3, repl_2_3_3)
with open(ch2_path, 'w', encoding='utf-8') as f:
f.write(text2)
print("Updated 제2장_하천공사.md successfully.")
# 3. 02_토목부문/제4장_궤도공사.md
ch4_path = "resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제4장_궤도공사.md"
with open(ch4_path, 'r', encoding='utf-8') as f:
text4 = f.read()
target_4_6_10 = "4-6-10 목침목 탄성체결장치 설치('12년 보완)(침목 개소당)구 분단 위수 량궤도공인0.028보통인부인0.022[주] ① 본 품은 목침목에 탄성체결장치를 설치하는 기준이다."
repl_4_6_10 = """4-6-10 목침목 탄성체결장치 설치('12년 보완)
(침목 개소당)
| | | |
|---|---|---|
| 궤도공 | | 0.028 |
| 보통인부 | | 0.022 |
[] 품은 목침목에 탄성체결장치를 설치하는 기준이다."""
text4 = text4.replace(target_4_6_10, repl_4_6_10)
with open(ch4_path, 'w', encoding='utf-8') as f:
f.write(text4)
print("Updated 제4장_궤도공사.md successfully.")
# 4. 02_토목부문/제5장_강구조공사.md
ch5_path = "resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제5장_강구조공사.md"
with open(ch5_path, 'r', encoding='utf-8') as f:
text5 = f.read()
target_5_p1 = "- 인력에 의한 부설 및 접합을 수행하는 경우 다음 품을 적용한다. 구 분 단 위 수 량 관경(㎜) 시공량(본) 배관공(수도) 인 3 80 15 보 통 인 부 인 1 100 13 배관공(수도) 인 3 150 11 보 통 인 부 인 1.5 200 9"
repl_5_p1 = """- 인력에 의한 부설 및 접합을 수행하는 경우 다음 품을 적용한다.
| 관경() | 시공량() | 배관공(수도) () | 보통인부 () |
|---|---|---|---|
| 80 | 15 | 3 | 1 |
| 100 | 13 | 3 | 1 |
| 150 | 11 | 3 | 1.5 |
| 200 | 9 | 3 | 1.5 |"""
target_5_p2 = "- 인력에 의한 부설 및 접합을 수행하는 경우 다음 품을 적용한다. 구 분 단 위 수 량 관경(㎜) 시공량(본) 배관공(수도) 인 3 80 13 보 통 인 부 인 1 100 12 배관공(수도) 인 3 150 10 보 통 인 부 인 1.5 200 8"
repl_5_p2 = """- 인력에 의한 부설 및 접합을 수행하는 경우 다음 품을 적용한다.
| 관경() | 시공량() | 배관공(수도) () | 보통인부 () |
|---|---|---|---|
| 80 | 13 | 3 | 1 |
| 100 | 12 | 3 | 1 |
| 150 | 10 | 3 | 1.5 |
| 200 | 8 | 3 | 1.5 |"""
text5 = text5.replace(target_5_p1, repl_5_p1)
text5 = text5.replace(target_5_p2, repl_5_p2)
with open(ch5_path, 'w', encoding='utf-8') as f:
f.write(text5)
print("Updated 제5장_강구조공사.md successfully.")
# 5. 02_토목부문/제6장_관부설및접합공사.md
ch6_path = "resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제6장_관부설및접합공사.md"
with open(ch6_path, 'r', encoding='utf-8') as f:
text6 = f.read()
target_6_p = "- 인력에 의한 부설을 수행하는 경우 다음 품을 적용한다. 구 분 단 위 수 량 관경(㎜) 시공량(본) 배관공(수도) 인 2 80 8 보 통 인 부 인 1 100 6 배관공(수도) 인 3 150 4 보 통 인 부 인 1 200 3"
repl_6_p = """- 인력에 의한 부설을 수행하는 경우 다음 품을 적용한다.
| 관경() | 시공량() | 배관공(수도) () | 보통인부 () |
|---|---|---|---|
| 80 | 8 | 2 | 1 |
| 100 | 6 | 2 | 1 |
| 150 | 4 | 3 | 1 |
| 200 | 3 | 3 | 1 |"""
text6 = text6.replace(target_6_p, repl_6_p)
with open(ch6_path, 'w', encoding='utf-8') as f:
f.write(text6)
print("Updated 제6장_관부설및접합공사.md successfully.")
# 6. 02_토목부문/제7장_항만공사.md
ch7_path = "resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제7장_항만공사.md"
with open(ch7_path, 'r', encoding='utf-8') as f:
text7 = f.read()
target_7_2_2 = "7-2-2 해상투하('19년 보완)(10㎥당)수 량구 분단 위0.03㎥ 이하 굴착기 투하0.1㎥ 이상 크레인 투하잠수부조0.070.09특별인부인0.040.20보통인부인0.120.22[주] ① 본 품은 해상 투하장소에 도착하여 대선위에서 투하하는 것이다."
repl_7_2_2 = """7-2-2 해상투하('19년 보완)
(10)
| | | | |
|---|---|---|---|
| | | 0.03 이하 굴착기 투하 | 0.1 이상 크레인 투하 |
| 잠수부 | | 0.07 | 0.09 |
| 특별인부 | | 0.04 | 0.20 |
| 보통인부 | | 0.12 | 0.22 |
[] 품은 해상 투하장소에 도착하여 대선위에서 투하하는 것이다."""
text7 = text7.replace(target_7_2_2, repl_7_2_2)
with open(ch7_path, 'w', encoding='utf-8') as f:
f.write(text7)
print("Updated 제7장_항만공사.md successfully.")
# 7. 02_토목부문/제9장_측량.md
ch9_path = "resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제9장_측량.md"
with open(ch9_path, 'r', encoding='utf-8') as f:
text9 = f.read()
target_9_cad = "- 3. 기존 도로대장(종이, PDF, CAD 등)을 이용하여 작성하는 경우(단위 : 10㎞)투입인원작업구분비 고특급기술자고급기술자중급기술자초급기술자작업계획 및 준비0.30.7정위치편집0.81.22.0구조화편집1.64.02.4정리 및 점검0.10.50.4계0.43.65.64.4[주] ① 기존 도로대장(종이, PDF, CAD 등)을 활용하여 도로대장을 작성하는 경우라 함은 현행화된 기존 도로대장을 디지털화하여 작성하는 일련의 작업과정을 의미하며, 본 품은 CAD 형태의 기존 도로대장을 이용한 작성을 기준으로 한 것이다.㉮ 본 품에서 기존 도로대장 형태가 종이일 경우 ‘[토목부문] 9-5-4/3. 자동입력’의 자동독취, 벡터편집 작업 품을 별도 계상한다.㉯ 본 품에서 기존 도로대장 형태가 PDF일 경우 ‘[토목부문] 9-5-4/3. 자동입력’의 벡터편집 작업 품을 별도 계상한다."
repl_9_cad = """- 3. 기존 도로대장(종이, PDF, CAD 등)을 이용하여 작성하는 경우
(단위 : 10)
| 작업구분 | 투입인원 | | | | |
|---|---|---|---|---|---|
| | 특급기술자 | 고급기술자 | 중급기술자 | 초급기술자 | |
| 작업계획 준비 | 0.3 | 0.7 | - | - | |
| 정위치편집 | - | 0.8 | 1.2 | 2.0 | |
| 구조화편집 | - | 1.6 | 4.0 | 2.4 | |
| 정리 점검 | 0.1 | 0.5 | 0.4 | - | |
| | 0.4 | 3.6 | 5.6 | 4.4 | |
[] 기존 도로대장(종이, PDF, CAD ) 활용하여 도로대장을 작성하는 경우라 함은 현행화된 기존 도로대장을 디지털화하여 작성하는 일련의 작업과정을 의미하며, 품은 CAD 형태의 기존 도로대장을 이용한 작성을 기준으로 것이다.
품에서 기존 도로대장 형태가 종이일 경우 [토목부문] 9-5-4/3. 자동입력 자동독취, 벡터편집 작업 품을 별도 계상한다.
품에서 기존 도로대장 형태가 PDF일 경우 [토목부문] 9-5-4/3. 자동입력 벡터편집 작업 품을 별도 계상한다."""
text9 = text9.replace(target_9_cad, repl_9_cad)
with open(ch9_path, 'w', encoding='utf-8') as f:
f.write(text9)
print("Updated 제9장_측량.md successfully.")
@@ -0,0 +1,125 @@
import os, sys, re
sys.stdout.reconfigure(encoding='utf-8')
base_dir = "resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문"
job_words = [
"포장공", "보통인부", "특별인부", "철근공", "보링공", "기계설비공", "철공", "조경공",
"착암공", "용접공", "배관공(수도)", "배관공", "할석공", "석공", "줄눈공", "미장공", "도장공", "방수공",
"타일공", "내장공", "석면해체공", "궤도공", "중급기술자", "초급기술자", "고급기술자",
"형틀목공", "목도", "잠수부", "비계공", "조적공"
]
all_md_files = [os.path.join(base_dir, f) for f in os.listdir(base_dir) if f.endswith('.md')]
total_fixed = 0
for fpath in sorted(all_md_files):
fname = os.path.basename(fpath)
# Exclude 제9장_측량.md for separate specialized handling
if "제9장" in fname:
continue
with open(fpath, 'r', encoding='utf-8') as f:
lines = f.readlines()
new_lines = []
file_fixes = 0
for idx, line in enumerate(lines):
line_str = line.strip()
if not (line_str.startswith('|') and line_str.endswith('|')):
new_lines.append(line)
continue
cells = [c.strip() for c in line_str.split('|')[1:-1]]
# Skip divider or note
if all(re.match(r'^:?-+:?$', c) for c in cells if c) or (cells and cells[0] in ["비고", "비 고", "", "[주]"]):
new_lines.append(line)
continue
# Find which cell has multiple job titles
job_cell_idx = -1
detected_jobs = []
for c_idx, c in enumerate(cells):
toks = c.split()
if len(toks) >= 2 and all(any(tok.endswith(end) for end in ["", "", "", ""]) for tok in toks):
job_cell_idx = c_idx
detected_jobs = toks
break
c_nospace = c.replace(" ", "")
cur_str = c_nospace
matched_jobs = []
for jw in sorted(job_words, key=len, reverse=True):
while jw in cur_str:
matched_jobs.append(jw)
cur_str = cur_str.replace(jw, "", 1)
if len(matched_jobs) >= 2 and cur_str == "":
ordered_jobs = []
pos = 0
while pos < len(c_nospace):
found = False
for jw in sorted(job_words, key=len, reverse=True):
if c_nospace[pos:].startswith(jw):
ordered_jobs.append(jw)
pos += len(jw)
found = True
break
if not found:
break
if len(ordered_jobs) >= 2:
job_cell_idx = c_idx
detected_jobs = ordered_jobs
break
if job_cell_idx != -1 and len(detected_jobs) >= 2:
N = len(detected_jobs)
unit_cell_idx = -1
for c_idx in range(job_cell_idx + 1, len(cells)):
toks = cells[c_idx].split()
if len(toks) == N and all(t in ["", "", "hr", "시간", "", "-", ""] for t in toks):
unit_cell_idx = c_idx
break
if unit_cell_idx != -1:
can_split = False
col_splits = {}
for c_idx in range(unit_cell_idx + 1, len(cells)):
toks = cells[c_idx].split()
if len(toks) == N and all(re.match(r'^-?\d+(?:,\d+)?(?:\.\d+)?$', t) for t in toks):
col_splits[c_idx] = toks
can_split = True
if can_split:
unit_toks = cells[unit_cell_idx].split()
for r_idx in range(N):
new_row_cells = []
for c_idx in range(len(cells)):
if c_idx == job_cell_idx:
new_row_cells.append(detected_jobs[r_idx])
elif c_idx == unit_cell_idx:
new_row_cells.append(unit_toks[r_idx])
elif c_idx in col_splits:
new_row_cells.append(col_splits[c_idx][r_idx])
else:
if r_idx == 0:
new_row_cells.append(cells[c_idx])
else:
new_row_cells.append("")
new_line_str = "| " + " | ".join(new_row_cells) + " |\n"
new_lines.append(new_line_str)
file_fixes += 1
total_fixed += 1
continue
new_lines.append(line)
if file_fixes > 0:
print(f"{fname}: fixed {file_fixes} multi-worker squashed rows")
with open(fpath, 'w', encoding='utf-8') as f:
f.writelines(new_lines)
print(f"Total multi-worker rows fixed in 토목부문 (excluding 9장): {total_fixed}")
@@ -0,0 +1,295 @@
import os, sys, re
sys.stdout.reconfigure(encoding='utf-8')
# 1. 01_공통부문/제2장_가설공사.md
ch2_path = "resources/knowledge/original/원가계산/건설공사_표준품셈/01_공통부문/제2장_가설공사.md"
with open(ch2_path, 'r', encoding='utf-8') as f:
text2 = f.read()
# Fix 2-8-7
target_2_8_7 = "2-8-7 철골 안전망 설치 및 해체('18년 보완)(10㎡당)구 분단위수 량비계공인0.17보통인부인0.05[주] ① 본 품은 철골공사 시공 중 철골사이에 설치되는 안전망의 설치 및 해체 작업 기준이다."
repl_2_8_7 = """2-8-7 철골 안전망 설치 및 해체('18년 보완)
(10)
| | | |
|---|---|---|
| 비계공 | | 0.17 |
| 보통인부 | | 0.05 |
[] 품은 철골공사 시공 철골사이에 설치되는 안전망의 설치 해체 작업 기준이다."""
# Fix 2-8-10
target_2_8_10 = "2-8-10 수직형 추락방망 설치 및 해체('20년 신설)(10개소당)개구부 면적구 분단 위1.0㎡이하1.0~3.0㎡이하3.0~6.0㎡이하6.0~9.0㎡이하9.0~12.0㎡이하비계공인0.490.631.011.301.60[주] ① 본 품은 창호, 발코니 등 개구부에 추락의 위험을 방지하기 위한 수직형 방망을 설치 및 해체하는 기준이다."
repl_2_8_10 = """2-8-10 수직형 추락방망 설치 및 해체('20년 신설)
(10개소당)
| | | 개구부 면적 | | | | |
|---|---|---|---|---|---|---|
| | | 1.0이하 | 1.0~3.0이하 | 3.0~6.0이하 | 6.0~9.0이하 | 9.0~12.0이하 |
| 비계공 | | 0.49 | 0.63 | 1.01 | 1.30 | 1.60 |
[] 품은 창호, 발코니 개구부에 추락의 위험을 방지하기 위한 수직형 방망을 설치 해체하는 기준이다."""
# Fix 2-11-5
target_2_11_5 = "2-11-5 비산먼지 발생 억제를 위한 살수('02년 신설, '09년 보완)(100㎡당)구 분규 격단 위수 량물탱크(살수차)16,000ℓ시간0.008[주] ① 본 품은 공사현장의 비산먼지 발생억제를 위하여 물탱크(살수차)로 살수하는 품이다."
repl_2_11_5 = """2-11-5 비산먼지 발생 억제를 위한 살수('02년 신설, '09년 보완)
(100)
| | | | |
|---|---|---|---|
| 물탱크(살수차) | 16,000 | 시간 | 0.008 |
[] 품은 공사현장의 비산먼지 발생억제를 위하여 물탱크(살수차) 살수하는 품이다."""
# Fix 2-11-7
target_2_11_7 = "2-11-7 슬러지 제거('19년 신설)(회당)구 분규 격단 위수 량특별인부인0.63굴착기0.2㎥hr1.00[주] ① 본 품은 자동세륜기(슬러지함 2.0×1.2×1.2m) 슬러지를 제거하는 기준이다."
repl_2_11_7 = """2-11-7 슬러지 제거('19년 신설)
(회당)
| | | | |
|---|---|---|---|
| 특별인부 | | | 0.63 |
| 굴착기 | 0.2 | hr | 1.00 |
[] 품은 자동세륜기(슬러지함 2.0×1.2×1.2m) 슬러지를 제거하는 기준이다."""
# Replace page header attachments
text2 = text2.replace("인력품의 2%로 계상한다.52공통부문\n" + target_2_8_7, "인력품의 2%로 계상한다.\n\n52 공통부문\n\n" + repl_2_8_7)
text2 = text2.replace(target_2_8_7, repl_2_8_7)
text2 = text2.replace("1회사용 후 100%로 한다.53제2장 가설공사\n" + target_2_8_10, "1회사용 후 100%로 한다.\n\n53 제2장 가설공사\n\n" + repl_2_8_10)
text2 = text2.replace(target_2_8_10, repl_2_8_10)
text2 = text2.replace(target_2_11_5, repl_2_11_5)
text2 = text2.replace("인력품의 2%로 계상한다.62공통부문\n" + target_2_11_7, "인력품의 2%로 계상한다.\n\n62 공통부문\n\n" + repl_2_11_7)
text2 = text2.replace(target_2_11_7, repl_2_11_7)
# Clean up L348 & L371 embedded table residue in 비고
target_bg1 = "비 고 | - 수평연결재가 필요한 경우는 다음과 같이 계상한다. (1단 설치일 때, ㎡당 구 분 규 격 단 위 수 량 형 틀 목 공 설치, 해체 인 0.02 보 통 인 부 설치, 해체 인 0.01 ※ 전체동바리 연결을 기준으로 산정된 것이다. - 설치간격에 따라 시공량에 다음 요율을 적용한다. 설치간격 0.6m이하 0.6m초과∼0.8m이하 0.8m초과 요 율 - 17% - + 11% ※ 설치간격은 멍에간격을 기준한 것이다."
repl_bg1 = "비 고 | - 수평연결재가 필요한 경우는 다음과 같이 계상한다. (1단 설치일 때, ㎡당, 아래 표 참조) ※ 전체동바리 연결을 기준으로 산정된 것이다. - 설치간격에 따라 시공량에 다음 요율을 적용한다. (아래 표 참조) ※ 설치간격은 멍에간격을 기준한 것이다."
text2 = text2.replace(target_bg1, repl_bg1)
target_bg2 = "비 고 | - 수평연결재가 필요한 경우는 다음과 같이 계상한다. (1단설치일 때, ㎡당 구 분 규 격 단 위 수 량 형 틀 목 공 설치, 해체 인 0.02 보 통 인 부 설치, 해체 인 0.01 ※ 전체동바리 연결을 기준으로 산정된 것이다. - 설치간격에 따라 시공량에 다음 요율을 적용한다. 설치간격 0.6m이하 0.6m초과∼0.8m이하 0.8m초과 요 율 - 17% - + 11% ※ 설치간격은 멍에간격을 기준한다."
repl_bg2 = "비 고 | - 수평연결재가 필요한 경우는 다음과 같이 계상한다. (1단설치일 때, ㎡당, 아래 표 참조) ※ 전체동바리 연결을 기준으로 산정된 것이다. - 설치간격에 따라 시공량에 다음 요율을 적용한다. (아래 표 참조) ※ 설치간격은 멍에간격을 기준한다."
text2 = text2.replace(target_bg2, repl_bg2)
with open(ch2_path, 'w', encoding='utf-8') as f:
f.write(text2)
print("Updated 제2장_가설공사.md successfully.")
# 2. 01_공통부문/제4장_조경공사.md
ch4_path = "resources/knowledge/original/원가계산/건설공사_표준품셈/01_공통부문/제4장_조경공사.md"
with open(ch4_path, 'r', encoding='utf-8') as f:
text4 = f.read()
target_4_4_4 = "4-4-4 야자섬유매트포장('22년 신설, '24년 보완)(일당)시공량(㎡)구 분단 위수 량폭 1.5m이하폭 2.0m이하조경공인290130보통인부인1[주] ① 본 품은 설치위치의 토공사가 완료된 상태에서 야자섬유매트로 포장하는 기준이다."
repl_4_4_4 = """4-4-4 야자섬유매트포장('22년 신설, '24년 보완)
(일당)
| | | | 시공량() | |
|---|---|---|---|---|
| | | | 1.5m이하 | 2.0m이하 |
| 조경공 | | 2 | 90 | 130 |
| 보통인부 | | 1 | | |
[] 품은 설치위치의 토공사가 완료된 상태에서 야자섬유매트로 포장하는 기준이다."""
text4 = text4.replace(target_4_4_4, repl_4_4_4)
with open(ch4_path, 'w', encoding='utf-8') as f:
f.write(text4)
print("Updated 제4장_조경공사.md successfully.")
# 3. 01_공통부문/제5장_기초공사.md
ch5_path = "resources/knowledge/original/원가계산/건설공사_표준품셈/01_공통부문/제5장_기초공사.md"
with open(ch5_path, 'r', encoding='utf-8') as f:
text5 = f.read()
target_5_1_3 = "- ⑥ 장비(크레인)의 규격은 작업여건(시공높이, 시공위치 등) 및 안전율(적정하중, 작업반경 등)을 고려하여적합한 규격을 적용한다.99제5장 기초공사5-1-3 H-Beam 설치(버팀보)('26년 보완)(일당)시공량(본)구 분단 위수 량규격(m)H=250500H=6008005m이하1210철골공인36~8m119용접공인29~11m108보통인부인112~14m97크레인대115~18m86[주] ① 본 품은 수평지보공(H-Beam)의 버팀보 설치 품이다."
repl_5_1_3 = """- ⑥ 장비(크레인)의 규격은 작업여건(시공높이, 시공위치 등) 및 안전율(적정하중, 작업반경 등)을 고려하여적합한 규격을 적용한다.
99 제5장 기초공사
5-1-3 H-Beam 설치(버팀보)('26년 보완)
(일당)
| | | | 규격(m) | 시공량() | |
|---|---|---|---|---|---|
| | | | | H=250500 | H=600800 |
| 철골공 | | 3 | 5m이하 | 12 | 10 |
| 용접공 | | 2 | 6~8m | 11 | 9 |
| 보통인부 | | 1 | 9~11m | 10 | 8 |
| 크레인 | | 1 | 12~14m | 9 | 7 |
| | | | 15~18m | 8 | 6 |
[] 품은 수평지보공(H-Beam) 버팀보 설치 품이다."""
target_5_1_8 = "5-1-8 복공판 설치·철거('25년 신설)(일당)시공량(㎡)구 분단 위수 량설치철거철골공인2용접공인2120170보통인부인1크레인대1[주] ① 본 품은 주형보 상부에 복공판을 설치 및 철거하는 기준이다."
repl_5_1_8 = """5-1-8 복공판 설치·철거('25년 신설)
(일당)
| | | | 시공량() | |
|---|---|---|---|---|
| | | | 설치 | 철거 |
| 철골공 | | 2 | 120 | 170 |
| 용접공 | | 2 | | |
| 보통인부 | | 1 | | |
| 크레인 | | 1 | | |
[] 품은 주형보 상부에 복공판을 설치 철거하는 기준이다."""
target_5_pbd1 = "- 2. 장비조립 및 해체(회당)구 분단 위수 량기계설비공인1인력철공〃2특별인부〃1장비크레인대1조립일2소요일수해체일1[주] ① 본 품은 PBD천공기를 1회 조립 및 해체하는 기준이며, 시공조건(외부 반입/반출)에 따라 조립․해체를 반복 적용한다."
repl_5_pbd1 = """- 2. 장비조립 및 해체(회당)
| | | |
|---|---|---|
| 기계설비공 | | 1 |
| 철공 | | 2 |
| 특별인부 | | 1 |
| 크레인 | | 1 |
| 조립 | | 2 |
| 해체 | | 1 |
[] 품은 PBD천공기를 1 조립 해체하는 기준이며, 시공조건(외부 반입/반출) 따라 조립해체를 반복 적용한다."""
target_5_pbd2 = "- 3. 장비 및 인력편성구 분규 격단 위수 량특별인부인2보통인부〃1PBD천공기147㎾, 38m(리더길이)대1[주] ① 부속장비(자동기록기, 계측기, 맨드릴 등)의 경비는 인력품에 다음 요율을 적용한다."
repl_5_pbd2 = """- 3. 장비 및 인력편성
| | | | |
|---|---|---|---|
| 특별인부 | | | 2 |
| 보통인부 | | | 1 |
| PBD천공기 | 147, 38m(리더길이) | | 1 |
[] 부속장비(자동기록기, 계측기, 맨드릴 ) 경비는 인력품에 다음 요율을 적용한다."""
target_5_pbd3 = "- 2. 장비조립・해체(회당)구 분단 위외부 반출/반입작업구간내 이동기계설비공인11철공〃22특별인부〃11크레인대11조립일32소요일수해체〃1.51"
repl_5_pbd3 = """- 2. 장비조립・해체(회당)
| | | 외부 반출/반입 | 작업구간내 이동 |
|---|---|---|---|
| 기계설비공 | | 1 | 1 |
| 철공 | | 2 | 2 |
| 특별인부 | | 1 | 1 |
| 크레인 | | 1 | 1 |
| 조립 | | 3 | 2 |
| 해체 | | 1.5 | 1 |"""
text5 = text5.replace(target_5_1_3, repl_5_1_3)
text5 = text5.replace(target_5_1_8, repl_5_1_8)
text5 = text5.replace(target_5_pbd1, repl_5_pbd1)
text5 = text5.replace(target_5_pbd2, repl_5_pbd2)
text5 = text5.replace(target_5_pbd3, repl_5_pbd3)
# L150 주석 안 좌대 설치 및 해체
target_5_base = "- 좌대 설치 및 해체가 필요한 경우 다음 품을 적용한다. (일당) 시공량(개소) 구 분 단 위 수 량 설치 해체 철 공 인 1 18 35 보 통 인 부 인 1"
repl_5_base = """- 좌대 설치 및 해체가 필요한 경우 다음 품을 적용한다. (일당)
| | | | 시공량(개소) | |
|---|---|---|---|---|
| | | | 설치 | 해체 |
| 철공 | | 1 | 18 | 35 |
| 보통인부 | | 1 | | |"""
text5 = text5.replace(target_5_base, repl_5_base)
with open(ch5_path, 'w', encoding='utf-8') as f:
f.write(text5)
print("Updated 제5장_기초공사.md successfully.")
# 4. 01_공통부문/제6장_철근콘크리트공사.md
ch6_path = "resources/knowledge/original/원가계산/건설공사_표준품셈/01_공통부문/제6장_철근콘크리트공사.md"
with open(ch6_path, 'r', encoding='utf-8') as f:
text6 = f.read()
target_6_slip = "- 1. 설치 및 해체(㎡당)설 치해 체구 분단 위수 량구 분단 위수 량비계공인0.199특수비계공인0.154보통인부인0.091보통인부인0.064크레인hr0.132 크레인hr0.170[주] ① 슬립폼 제작비용은 별도계상하되, 단면형상은 고정단면을 기준으로 한 것이다."
repl_6_slip = """- 1. 설치 및 해체(㎡당)
| | | | | | |
|---|---|---|---|---|---|
| | | | | | |
| 비계공 | | 0.199 | 특수비계공 | | 0.154 |
| 보통인부 | | 0.091 | 보통인부 | | 0.064 |
| 크레인 | hr | 0.132 | 크레인 | hr | 0.170 |
[] 슬립폼 제작비용은 별도계상하되, 단면형상은 고정단면을 기준으로 것이다."""
target_6_socket = "- 2. 소켓 연결(m당)구 분단 위수 량특별인부인0.085보통인부인0.029[주] ① 본 품은 지수판 연결재(소켓)를 사용한 지수판 설치를 기준한 것이다."
repl_6_socket = """- 2. 소켓 연결(m당)
| | | |
|---|---|---|
| 특별인부 | | 0.085 |
| 보통인부 | | 0.029 |
[] 품은 지수판 연결재(소켓) 사용한 지수판 설치를 기준한 것이다."""
target_6_grout = "- 7. 그라우팅(일당)구 분규 격단 위수 량시공량(㎥)기계설비공인1인력특별인부인3보통인부인11.50그라우팅 믹서190×2ℓ대1장비그라우팅 펌프30~60ℓ/min대1[주] ① 본 품은 쉬즈관 내부 그라우팅 작업 기준이다."
repl_6_grout = """- 7. 그라우팅(일당)
| | | | | 시공량() |
|---|---|---|---|---|
| 기계설비공 | | | 1 | 1.50 |
| 특별인부 | | | 3 | |
| 보통인부 | | | 1 | |
| 그라우팅 믹서 | 190×2 | | 1 | |
| 그라우팅 펌프 | 30~60/min | | 1 | |
[] 품은 쉬즈관 내부 그라우팅 작업 기준이다."""
target_6_anchor = "- 2. 정착구 설치(개당)수 량(쉬즈관 규격)구 분단 위 ø75㎜ 이하ø100㎜ 이하ø130㎜ 이하형틀목공인0.380.480.61보통인부인0.180.230.29비고"
repl_6_anchor = """- . 정착구 설치(개당)
| | | (쉬즈관 규격) | | | |
|---|---|---|---|---|---|
| | | ø75 이하 | ø100 이하 | ø130 이하 | |
| 형틀목공 | | 0.38 | 0.48 | 0.61 | |
| 보통인부 | | 0.18 | 0.23 | 0.29 | |"""
target_6_sheath = "- 3. 쉬즈관 설치(m당)수 량(쉬즈관 규격)구 분단위ø75㎜ 이하ø100㎜이하ø130㎜이하철근공인0.030.050.07보통인부인0.020.020.03[주] ① 본 품은 쉬즈관을 철근에 연결하여 설치하는 기준이다."
repl_6_sheath = """- 3. 쉬즈관 설치(m당)
| | | (쉬즈관 규격) | | |
|---|---|---|---|---|
| | | ø75 이하 | ø100 이하 | ø130 이하 |
| 철근공 | | 0.03 | 0.05 | 0.07 |
| 보통인부 | | 0.02 | 0.02 | 0.03 |
[] 품은 쉬즈관을 철근에 연결하여 설치하는 기준이다."""
text6 = text6.replace(target_6_slip, repl_6_slip)
text6 = text6.replace(target_6_socket, repl_6_socket)
text6 = text6.replace(target_6_grout, repl_6_grout)
text6 = text6.replace(target_6_anchor, repl_6_anchor)
text6 = text6.replace(target_6_sheath, repl_6_sheath)
with open(ch6_path, 'w', encoding='utf-8') as f:
f.write(text6)
print("Updated 제6장_철근콘크리트공사.md successfully.")
# 5. 01_공통부문/제7장_돌공사.md
ch7_path = "resources/knowledge/original/원가계산/건설공사_표준품셈/01_공통부문/제7장_돌공사.md"
with open(ch7_path, 'r', encoding='utf-8') as f:
text7 = f.read()
target_7_4_2 = "7-4-2 앵커 긴결공법('19, '26년 보완)(일당)시공량(㎡)구 분단 위수 량0.3㎡이하0.3㎡초과~0.8㎡이하석공인3줄눈공인11011보통인부인2[주] ① 본 품은 구조물 벽체에 앵커로 고정하여 석재판(비구조요소)을 설치하는 기준이다."
repl_7_4_2 = """7-4-2 앵커 긴결공법('19, '26년 보완)
(일당)
| | | | 시공량() | |
|---|---|---|---|---|
| | | | 0.3이하 | 0.3초과~0.8이하 |
| 석공 | | 3 | 10 | 11 |
| 줄눈공 | | 1 | | |
| 보통인부 | | 2 | | |
[] 품은 구조물 벽체에 앵커로 고정하여 석재판(비구조요소) 설치하는 기준이다."""
text7 = text7.replace(target_7_4_2, repl_7_4_2)
with open(ch7_path, 'w', encoding='utf-8') as f:
f.write(text7)
print("Updated 제7장_돌공사.md successfully.")
@@ -0,0 +1,220 @@
import os, sys, re
sys.stdout.reconfigure(encoding='utf-8')
# 1. 제1장 공통
ch1_path = "resources/knowledge/original/원가계산/건설공사_표준품셈/05_유지관리부문/제1장_공통.md"
with open(ch1_path, 'r', encoding='utf-8') as f:
text1 = f.read()
target_1_2_10 = "[주] 살수차의 운전시간에는 급수시간 및 1회당 5㎞까지의 이동시간을 포함한다.유지관리부문8621-2-10 제초('14, '19, '22, '26년 보완)(일당)시공량(㎡) 구 분단 위수 량일반 잔디지역지장물 지역조경공인11,400 1,000 보통인부인5[주] ① 본 품은 인력으로 잡초를 제거하는 기준이다."
repl_1_2_10 = """[주] 살수차의 운전시간에는 급수시간 및 1회당 5㎞까지의 이동시간을 포함한다.
유지관리부문 862
1-2-10 제초('14, '19, '22, '26 보완)
(일당)
| | | | 시공량() | |
|---|---|---|---|---|
| | | | 일반 잔디지역 | 지장물 지역 |
| 조경공 | | 1 | 1,400 | 1,000 |
| 보통인부 | | 5 | | |
[] 품은 인력으로 잡초를 제거하는 기준이다."""
text1 = text1.replace(target_1_2_10, repl_1_2_10)
with open(ch1_path, 'w', encoding='utf-8') as f:
f.write(text1)
print("Updated 제1장_공통.md successfully.")
# 2. 제2장 토목
ch2_path = "resources/knowledge/original/원가계산/건설공사_표준품셈/05_유지관리부문/제2장_토목.md"
with open(ch2_path, 'r', encoding='utf-8') as f:
text2 = f.read()
target_2_1_5 = "2-1-5 절삭 후 콘크리트 덧씌우기('20년 보완)(일당)시공량(㎡)구 분 규 격단 위수 량밀링깊이100㎜밀링깊이150㎜포장공인4특별인부인1보통인부(절삭)인1보통인부(청소)인1보통인부(포설)인42,5001,600콘크리트 페이버75㎾대1조면마무리기7.95m대1노면파쇄기2m대1로더(타이어)0.57㎥대1[주] ① 본 품은 아스팔트 포장 절삭 후 콘크리트 덧씌우기의 포장면 절삭 및 청소, 포설, 양생, 조면마무리에 대한 품이다."
repl_2_1_5 = """2-1-5 절삭 후 콘크리트 덧씌우기('20년 보완)
(일당)
| | | | | 시공량() | |
|---|---|---|---|---|---|
| | | | | 밀링깊이 100 | 밀링깊이 150 |
| 포장공 | | | 4 | 2,500 | 1,600 |
| 특별인부 | | | 1 | | |
| 보통인부(절삭) | | | 1 | | |
| 보통인부(청소) | | | 1 | | |
| 보통인부(포설) | | | 4 | | |
| 콘크리트 페이버 | 75 | | 1 | | |
| 조면마무리기 | 7.95m | | 1 | | |
| 노면파쇄기 | 2m | | 1 | | |
| 로더(타이어) | 0.57 | | 1 | | |
[] 품은 아스팔트 포장 절삭 콘크리트 덧씌우기의 포장면 절삭 청소, 포설, 양생, 조면마무리에 대한 품이다."""
target_2_1_27 = "- ④ 공구손료 및 경장비(전동드릴 등)의 기계경비는 인력품의 3%로 계상한다.유지관리부문8882-1-27 보도용 블록 인력철거('21, '24년, 25년 보완)(일당)A-TypeB-Type구 분규격단 위수 량시공량(㎡)수 량시공량(㎡)포장공인22보통인부인23601260트럭2.5ton대11[주] ① 본 품은 유용할 목적으로 철거하거나 또는 장비를 사용하지 못하는 구간의 철거 작업 기준이다."
repl_2_1_27 = """- ④ 공구손료 및 경장비(전동드릴 등)의 기계경비는 인력품의 3%로 계상한다.
유지관리부문 888
2-1-27 보도용 블록 인력철거('21, '24, 25 보완)
(일당)
| | | | A-Type | | B-Type | |
|---|---|---|---|---|---|---|
| | | | | 시공량() | | 시공량() |
| 포장공 | | | 2 | 360 | 2 | 260 |
| 보통인부 | | | 2 | | 1 | |
| 트럭 | 2.5ton | | 1 | | 1 | |
[] 품은 유용할 목적으로 철거하거나 또는 장비를 사용하지 못하는 구간의 철거 작업 기준이다."""
target_2_2_15 = "- ③ 자갈다지기는 별도 계상한다.유지관리부문9002-2-15 교상가드레일 철거('12, '19년 보완)(km당)구 분규 격단 위수 량궤도공-인30보통인부-인11굴착기+ 부착용집게0.2㎥hr34.8[주] ① 본 품은 교상에 가드레일을 철거하는 기준이다."
repl_2_2_15 = """- ③ 자갈다지기는 별도 계상한다.
유지관리부문 900
2-2-15 교상가드레일 철거('12, '19 보완)
(km당)
| | | | |
|---|---|---|---|
| 궤도공 | - | | 30 |
| 보통인부 | - | | 11 |
| 굴착기+부착용집게 | 0.2 | hr | 34.8 |
[] 품은 교상에 가드레일을 철거하는 기준이다."""
target_2_2_16 = "2-2-16 목침목 탄성체결장치 철거('12, '19년 보완)(침목 개소당)구 분단 위수 량궤도공인0.028보통인부인0.022[주] ① 본 품은 목침목에 탄성체결장치를 설치 또는 해체하는 기준이다."
repl_2_2_16 = """2-2-16 목침목 탄성체결장치 철거('12, '19년 보완)
(침목 개소당)
| | | |
|---|---|---|
| 궤도공 | | 0.028 |
| 보통인부 | | 0.022 |
[] 품은 목침목에 탄성체결장치를 설치 또는 해체하는 기준이다."""
target_2_4_1 = "- ⑥ 공구손료 및 경장비의 기계경비는 인력품의 3%로 계상한다.유지관리부문9022-4 관부설 및 접합2-4-1 상수관 세척('18년 신설)(일당)구 분단 위수 량시공량(구간)배관공(수도)인1보통인부인32시험기구식1[주] ① 본 품은 양측의 제수밸브와 소화전을 이용한 상수관(300㎜이하)의 물세척(플러싱) 작업 기준이다."
repl_2_4_1 = """- ⑥ 공구손료 및 경장비의 기계경비는 인력품의 3%로 계상한다.
유지관리부문 902
2-4 관부설 접합
2-4-1 상수관 세척('18년 신설)
(일당)
| | | | 시공량(구간) |
|---|---|---|---|
| 배관공(수도) | | 1 | 2 |
| 보통인부 | | 3 | |
| 시험기구 | | 1 | |
[] 품은 양측의 제수밸브와 소화전을 이용한 상수관(300이하) 물세척(플러싱) 작업 기준이다."""
text2 = text2.replace(target_2_1_5, repl_2_1_5)
text2 = text2.replace(target_2_1_27, repl_2_1_27)
text2 = text2.replace(target_2_2_15, repl_2_2_15)
text2 = text2.replace(target_2_2_16, repl_2_2_16)
text2 = text2.replace(target_2_4_1, repl_2_4_1)
with open(ch2_path, 'w', encoding='utf-8') as f:
f.write(text2)
print("Updated 제2장_토목.md successfully.")
# 3. 제3장 건축
ch3_path = "resources/knowledge/original/원가계산/건설공사_표준품셈/05_유지관리부문/제3장_건축.md"
with open(ch3_path, 'r', encoding='utf-8') as f:
text3 = f.read()
target_3_1_5 = "- ⑦ 공구손료 및 경장비(살수장비 등)의 기계경비는 인력품의 4%로 계상한다.유지관리부문9123-1-5 석축 헐기(인력)('22년 보완)구 분단 위할석공(인)보통인부(인)메쌓기 뒷길이 45∼60㎝㎡당-0.2메쌓기 뒷길이 60∼90㎝㎡당-0.3찰쌓기㎡당-0.6절석 ( 마름돌) 쌓기㎥당0.11.1[주] ① 본 품은 기준높이 3.6m일 때의 인력헐기를 기준한 것이며, 그 이상일 때의 작업 안전설비 및 특수 조건에 대한 품은 별도 계상한다."
repl_3_1_5 = """- ⑦ 공구손료 및 경장비(살수장비 등)의 기계경비는 인력품의 4%로 계상한다.
유지관리부문 912
3-1-5 석축 헐기(인력)('22년 보완)
| | | 할석공() | 보통인부() |
|---|---|---|---|
| 메쌓기 뒷길이 4560 | | - | 0.2 |
| 메쌓기 뒷길이 6090 | | - | 0.3 |
| 찰쌓기 | | - | 0.6 |
| 절석(마름돌) 쌓기 | | 0.1 | 1.1 |
[] 품은 기준높이 3.6m일 때의 인력헐기를 기준한 것이며, 이상일 때의 작업 안전설비 특수 조건에 대한 품은 별도 계상한다."""
target_3_2_12 = "- ④ 바탕처리에 사용되는 재료(퍼티, 방수테이프 등)는 별도 계상한다.유지관리부문9163-2-12 석면건축자재 해체('09년 신설, '11년 보완)(㎡당)구 분단 위내장재외장재뿜칠재석면해체공인0.1200.0450.5보통인부인0.0170.011-[주] ① 본 품은 석면이 함유된 자재를 해체하는 품으로 적용기준은 다음과 같다.㉮ 내장재는 건축물의 내부 천장재, 내벽체, 간막이재 철거를 기준한 것이다.㉯ 외장재는 슬레이트 지붕재 해체를 기준한 것이다.㉰ 뿜칠재는 철골내화피복재를 기준으로 한 것으로 철골면의 하부면, 측면부, 상부면 등의 해체공사와 철재로 시공된 천장면에 부착되어 있는 뿜칠재의 해체를 기준한 것이다."
repl_3_2_12 = """- ④ 바탕처리에 사용되는 재료(퍼티, 방수테이프 등)는 별도 계상한다.
유지관리부문 916
3-2-12 석면건축자재 해체('09년 신설, '11 보완)
()
| | | 내장재 | 외장재 | 뿜칠재 |
|---|---|---|---|---|
| 석면해체공 | | 0.120 | 0.045 | 0.5 |
| 보통인부 | | 0.017 | 0.011 | - |
[] 품은 석면이 함유된 자재를 해체하는 품으로 적용기준은 다음과 같다.
내장재는 건축물의 내부 천장재, 내벽체, 간막이재 철거를 기준한 것이다.
외장재는 슬레이트 지붕재 해체를 기준한 것이다.
뿜칠재는 철골내화피복재를 기준으로 것으로 철골면의 하부면, 측면부, 상부면 등의 해체공사와 철재로 시공된 천장면에 부착되어 있는 뿜칠재의 해체를 기준한 것이다."""
target_3_4_4 = "- ③ 가구 등 지장물의 운반은 별도 계상한다.제3장 건축9193-4-4 PVC계바닥재 교체('22년 신설)(일당)구 분단 위수 량시공량(㎡)내장공인261비고"
repl_3_4_4 = """- ③ 가구 등 지장물의 운반은 별도 계상한다.
제3장 건축 919
3-4-4 PVC계바닥재 교체('22년 신설)
(일당)
| | | | 시공량() | |
|---|---|---|---|---|
| 내장공 | | 2 | 61 | |"""
target_3_4_5 = "3-4-5 타일 교체('22년 신설)(일당)시공량(㎡)구 분단 위수 량떠붙이기(벽)압착붙이기(바닥)타일공인278비고"
repl_3_4_5 = """3-4-5 타일 교체('22년 신설)
(일당)
| | | | 시공량() | | |
|---|---|---|---|---|---|
| | | | 떠붙이기() | 압착붙이기(바닥) | |
| 타일공 | | 2 | 7 | 8 | |"""
text3 = text3.replace(target_3_1_5, repl_3_1_5)
text3 = text3.replace(target_3_2_12, repl_3_2_12)
text3 = text3.replace(target_3_4_4, repl_3_4_4)
text3 = text3.replace(target_3_4_5, repl_3_4_5)
with open(ch3_path, 'w', encoding='utf-8') as f:
f.write(text3)
print("Updated 제3장_건축.md successfully.")
# 4. 제4장 기계설비
ch4_path = "resources/knowledge/original/원가계산/건설공사_표준품셈/05_유지관리부문/제4장_기계설비.md"
with open(ch4_path, 'r', encoding='utf-8') as f:
text4 = f.read()
target_4_3_3 = "4-3-3 배관누수 검사('22년 신설)(일당)구 분단 위수 량시공량(회)배관공인22.8[주] ① 본 품은 급수용, 급탕용, 난방용 옥내배관(∅50mm이하)의 누수보수를 위해 배관을 검사하는 기준이다."
repl_4_3_3 = """4-3-3 배관누수 검사('22년 신설)
(일당)
| | | | 시공량() |
|---|---|---|---|
| 배관공 | | 2 | 2.8 |
[] 품은 급수용, 급탕용, 난방용 옥내배관(50mm이하) 누수보수를 위해 배관을 검사하는 기준이다."""
text4 = text4.replace(target_4_3_3, repl_4_3_3)
with open(ch4_path, 'w', encoding='utf-8') as f:
f.write(text4)
print("Updated 제4장_기계설비.md successfully.")
@@ -0,0 +1,142 @@
import os, sys, re
sys.stdout.reconfigure(encoding='utf-8')
base_dir = "resources/knowledge/original/원가계산/건설공사_표준품셈/05_유지관리부문"
job_words = [
"포장공", "보통인부", "특별인부", "철근공", "보링공", "기계설비공", "철공", "조경공",
"착암공", "용접공", "배관공", "할석공", "석공", "줄눈공", "미장공", "도장공", "방수공",
"타일공", "내장공", "석면해체공", "궤도공", "중급기술자", "초급기술자", "고급기술자"
]
files = [
"제1장_공통.md",
"제2장_토목.md",
"제3장_건축.md",
"제4장_기계설비.md"
]
total_fixed = 0
for fname in files:
fpath = os.path.join(base_dir, fname)
with open(fpath, 'r', encoding='utf-8') as f:
lines = f.readlines()
new_lines = []
file_fixes = 0
for idx, line in enumerate(lines):
line_str = line.strip()
if not (line_str.startswith('|') and line_str.endswith('|')):
new_lines.append(line)
continue
cells = [c.strip() for c in line_str.split('|')[1:-1]]
# Skip divider or note
if all(re.match(r'^:?-+:?$', c) for c in cells if c) or (cells and cells[0] in ["비고", "비 고", "", "[주]"]):
new_lines.append(line)
continue
# Find which cell has multiple job titles
job_cell_idx = -1
detected_jobs = []
for c_idx, c in enumerate(cells):
# Check if this cell contains multiple known job words
# Notice in markdown it may be spaced: "철 근 공 보 통 인 부"
# Normalize spaces inside tokens
# Split tokens:
toks = c.split()
# If tokens end with common job characters
if len(toks) >= 2 and all(any(tok.endswith(end) for end in ["", "", "", ""]) for tok in toks):
# Candidate job cell
# Check if next cells have matching "인 인..." and numbers
job_cell_idx = c_idx
detected_jobs = toks
break
# Also handle spaced letters like "포 장 공 보 통 인 부"
# Combine characters and match
c_nospace = c.replace(" ", "")
matched_jobs = []
cur_str = c_nospace
for jw in sorted(job_words, key=len, reverse=True):
while jw in cur_str:
matched_jobs.append(jw)
cur_str = cur_str.replace(jw, "", 1)
if len(matched_jobs) >= 2 and cur_str == "":
# Reconstruct in order of original string
ordered_jobs = []
pos = 0
while pos < len(c_nospace):
found = False
for jw in sorted(job_words, key=len, reverse=True):
if c_nospace[pos:].startswith(jw):
ordered_jobs.append(jw)
pos += len(jw)
found = True
break
if not found:
break
if len(ordered_jobs) >= 2:
job_cell_idx = c_idx
detected_jobs = ordered_jobs
break
if job_cell_idx != -1 and len(detected_jobs) >= 2:
N = len(detected_jobs)
# Find unit cell and qty cell
unit_cell_idx = -1
qty_cell_idx = -1
for c_idx in range(job_cell_idx + 1, len(cells)):
toks = cells[c_idx].split()
if len(toks) == N and all(t in ["", "", "hr", "시간", "", "-"] for t in toks):
unit_cell_idx = c_idx
break
if unit_cell_idx != -1:
# Find qty cell(s)
# Check if there are columns with N numbers
can_split = False
col_splits = {} # c_idx -> list of N values
for c_idx in range(unit_cell_idx + 1, len(cells)):
toks = cells[c_idx].split()
if len(toks) == N and all(re.match(r'^-?\d+(?:,\d+)?(?:\.\d+)?$', t) for t in toks):
col_splits[c_idx] = toks
can_split = True
if can_split:
# Generate N rows
# In other cells, first row gets the cell value, other rows get empty ""
unit_toks = cells[unit_cell_idx].split()
for r_idx in range(N):
new_row_cells = []
for c_idx in range(len(cells)):
if c_idx == job_cell_idx:
new_row_cells.append(detected_jobs[r_idx])
elif c_idx == unit_cell_idx:
new_row_cells.append(unit_toks[r_idx])
elif c_idx in col_splits:
new_row_cells.append(col_splits[c_idx][r_idx])
else:
# First row keeps existing value, subsequent rows get empty string
if r_idx == 0:
new_row_cells.append(cells[c_idx])
else:
new_row_cells.append("")
new_line_str = "| " + " | ".join(new_row_cells) + " |\n"
new_lines.append(new_line_str)
file_fixes += 1
total_fixed += 1
continue
new_lines.append(line)
if file_fixes > 0:
print(f"{fname}: fixed {file_fixes} multi-worker squashed rows")
with open(fpath, 'w', encoding='utf-8') as f:
f.writelines(new_lines)
print(f"Total multi-worker rows fixed in 유지관리부문: {total_fixed}")
@@ -0,0 +1,237 @@
import os, sys, re
sys.stdout.reconfigure(encoding='utf-8')
# 1. 제1장 배관공사
ch1_path = "resources/knowledge/original/원가계산/건설공사_표준품셈/04_기계설비부문/제1장_배관공사.md"
with open(ch1_path, 'r', encoding='utf-8') as f:
text1 = f.read()
target_1_6_2 = "1-6-2 폴리부틸렌(PB) 이중관 접합 및 배관('13, '19년 보완)(m당)수 량 (규격)구 분단 위ø16mmø20mm배관공인0.0480.053보통인부인0.0210.023[주] ① 본 품은 합성수지제 휨(가요) 전선관 중 CD(Combine Duct)관 내에 폴리부틸렌(PB)관이 삽입된 이중관의 옥내바닥배관 기준이다."
repl_1_6_2 = """1-6-2 폴리부틸렌(PB) 이중관 접합 및 배관('13, '19년 보완)
(m당)
| | | (규격) | |
|---|---|---|---|
| | | ø16mm | ø20mm |
| 배관공 | | 0.048 | 0.053 |
| 보통인부 | | 0.021 | 0.023 |
[] 품은 합성수지제 (가요) 전선관 CD(Combine Duct) 내에 폴리부틸렌(PB)관이 삽입된 이중관의 옥내바닥배관 기준이다."""
target_1_6_3 = "1-6-3 가교화 폴리에틸렌관 접합 및 배관('13, '19년 보완)(m당)수 량 (규격)구 분단 위ø16mmø20mm배관공인0.0290.036보통인부인0.0140.018[주] ① 본 품은 가교화 폴리에틸렌(PE-X)관의 옥내난방배관 기준이다."
repl_1_6_3 = """1-6-3 가교화 폴리에틸렌관 접합 및 배관('13, '19년 보완)
(m당)
| | | (규격) | |
|---|---|---|---|
| | | ø16mm | ø20mm |
| 배관공 | | 0.029 | 0.036 |
| 보통인부 | | 0.014 | 0.018 |
[] 품은 가교화 폴리에틸렌(PE-X)관의 옥내난방배관 기준이다."""
text1 = text1.replace(target_1_6_2, repl_1_6_2)
text1 = text1.replace(target_1_6_3, repl_1_6_3)
with open(ch1_path, 'w', encoding='utf-8') as f:
f.write(text1)
print("Updated 제1장_배관공사.md successfully.")
# 2. 제3장 보온공사
ch3_path = "resources/knowledge/original/원가계산/건설공사_표준품셈/04_기계설비부문/제3장_보온공사.md"
with open(ch3_path, 'r', encoding='utf-8') as f:
text3 = f.read()
target_3_3_1 = "3-3 덕트보온3-3-1 각형덕트 보온('14, '20, '24년 보완)(일당)시공량(㎡)보온두께구 분단 위수 량고무발포보온재유리면보온재(mm)발포폴리에틸렌보온재(글라스울)보온공인225이하9.58.0보통인부인150이하8.57.0[주] ① 본 품은 접착제가 부착된 고무발포 보온재, 발포 폴리에틸렌 보온재와 접착제가 부착되지 않은 유리면보온재(글라스울)를 사용한 각형덕트 보온 기준이다."
repl_3_3_1 = """3-3 덕트보온
3-3-1 각형덕트 보온('14, '20, '24년 보완)
(일당)
| | | | 보온두께(mm) | 시공량() | |
|---|---|---|---|---|---|
| | | | | 고무발포보온재 / 발포폴리에틸렌보온재 | 유리면보온재(글라스울) |
| 보온공 | | 2 | 25이하 | 9.5 | 8.0 |
| 보통인부 | | 1 | 50이하 | 8.5 | 7.0 |
[] 품은 접착제가 부착된 고무발포 보온재, 발포 폴리에틸렌 보온재와 접착제가 부착되지 않은 유리면보온재(글라스울) 사용한 각형덕트 보온 기준이다."""
target_3_4_2 = "3-4-2 분전함 설치('06년 신설, '14, '20년 보완)(개소당)구 분단 위수 량기계설비공인 0.271보통인부인0.135[주] ① 본 품은 발열선의 작동을 위한 분전함(제어부) 설치 기준이다."
repl_3_4_2 = """3-4-2 분전함 설치('06년 신설, '14, '20년 보완)
(개소당)
| | | |
|---|---|---|
| 기계설비공 | | 0.271 |
| 보통인부 | | 0.135 |
[] 품은 발열선의 작동을 위한 분전함(제어부) 설치 기준이다."""
text3 = text3.replace(target_3_3_1, repl_3_3_1)
text3 = text3.replace(target_3_4_2, repl_3_4_2)
with open(ch3_path, 'w', encoding='utf-8') as f:
f.write(text3)
print("Updated 제3장_보온공사.md successfully.")
# 3. 제6장 측정기기공사
ch6_path = "resources/knowledge/original/원가계산/건설공사_표준품셈/04_기계설비부문/제6장_측정기기공사.md"
with open(ch6_path, 'r', encoding='utf-8') as f:
text6 = f.read()
target_6_1_3 = "6-1-3 세대용 계량기함 설치('26년 신설)(개소당)구 분단 위수 량배관공인0.086보통인부인0.086[주] ① 본 품은 유량계(ø32mm이하) 보호를 위한 거치대 계량기함(2단 이하)의 설치 기준이다."
repl_6_1_3 = """6-1-3 세대용 계량기함 설치('26년 신설)
(개소당)
| | | |
|---|---|---|
| 배관공 | | 0.086 |
| 보통인부 | | 0.086 |
[] 품은 유량계(ø32mm이하) 보호를 위한 거치대 계량기함(2 이하) 설치 기준이다."""
target_6_2_3 = "6-2-3 산업용 적산열량계 설치('19, '26년 보완)(대당)수 량 (규격)구 분단 위ø32mmø50mmø100mmø150mm플랜트배관공인0.710.750.850.95특별인부인0.710.750.850.95계장공인0.710.750.850.95[주] ① 본 품은 발전소 등 집단에너지의 생산·수송·분배 또는 사용을 위한 시설에 산업용으로 적산열량계를 설치하는 기준이다"
repl_6_2_3 = """6-2-3 산업용 적산열량계 설치('19, '26년 보완)
(대당)
| | | (규격) | | | |
|---|---|---|---|---|---|
| | | ø32mm | ø50mm | ø100mm | ø150mm |
| 플랜트배관공 | | 0.71 | 0.75 | 0.85 | 0.95 |
| 특별인부 | | 0.71 | 0.75 | 0.85 | 0.95 |
| 계장공 | | 0.71 | 0.75 | 0.85 | 0.95 |
[] 품은 발전소 집단에너지의 생산·수송·분배 또는 사용을 위한 시설에 산업용으로 적산열량계를 설치하는 기준이다."""
text6 = text6.replace(target_6_1_3, repl_6_1_3)
text6 = text6.replace(target_6_2_3, repl_6_2_3)
with open(ch6_path, 'w', encoding='utf-8') as f:
f.write(text6)
print("Updated 제6장_측정기기공사.md successfully.")
# 4. 제7장 위생기구설비공사
ch7_path = "resources/knowledge/original/원가계산/건설공사_표준품셈/04_기계설비부문/제7장_위생기구설비공사.md"
with open(ch7_path, 'r', encoding='utf-8') as f:
text7 = f.read()
target_7_shower = "| 비 고 | - 샤워헤드걸이를 설치는 다음을 적용하여 가산한다. (개당) 구 분 단 위 고정식 높이조절식 위 생 공 인 0.071 0.099"
repl_7_shower = """| 비 고 | - 샤워헤드걸이 설치는 다음을 적용하여 가산한다. (개당)
| | | 고정식 | 높이조절식 |
|---|---|---|---|
| 위생공 | | 0.071 | 0.099 |"""
text7 = text7.replace(target_7_shower, repl_7_shower)
with open(ch7_path, 'w', encoding='utf-8') as f:
f.write(text7)
print("Updated 제7장_위생기구설비공사.md successfully.")
# 5. 제8장 공기조화설비공사
ch8_path = "resources/knowledge/original/원가계산/건설공사_표준품셈/04_기계설비부문/제8장_공기조화설비공사.md"
with open(ch8_path, 'r', encoding='utf-8') as f:
text8 = f.read()
target_8_2_5 = "8-2-5 전열교환기 설치('20년 신설)(대당)수 량 (풍량 ㎥/h)구 분단 위250이하500이하800이하기계설비공인0.210.280.36보통인부인0.120.160.20[주] ① 본 품은 천장에 설치하여 덕트와 연결하는 환기시스템(전열교환기) 기준이다."
repl_8_2_5 = """8-2-5 전열교환기 설치('20년 신설)
(대당)
| | | (풍량 /h) | | |
|---|---|---|---|---|
| | | 250이하 | 500이하 | 800이하 |
| 기계설비공 | | 0.21 | 0.28 | 0.36 |
| 보통인부 | | 0.12 | 0.16 | 0.20 |
[] 품은 천장에 설치하여 덕트와 연결하는 환기시스템(전열교환기) 기준이다."""
target_8_3_3 = "8-3-3 가스보일러(가정용) 설치('92년 신설, '16, '20년 보완)(대당)수 량구 분단 위13,000 16,000 20,000 25,000 30,000 Kcal/hrKcal/hrKcal/hrKcal/hrKcal/hr보일러공인0.8450.9521.0281.1231.218보통인부인0.1640.1840.1990.2170.236비고"
repl_8_3_3 = """8-3-3 가스보일러(가정용) 설치('92년 신설, '16, '20년 보완)
(대당)
| | | (Kcal/hr) | | | | | |
|---|---|---|---|---|---|---|---|
| | | 13,000 | 16,000 | 20,000 | 25,000 | 30,000 | |
| 보일러공 | | 0.845 | 0.952 | 1.028 | 1.123 | 1.218 | |
| 보통인부 | | 0.164 | 0.184 | 0.199 | 0.217 | 0.236 | |"""
target_8_4_2 = "8-4-2 전기온수기(벽걸이형) 설치('20년 신설)(대당)수 량구 분단 위15L30L50L보일러공인0.170.180.23보통인부인0.070.080.09[주] ① 본 품은 벽걸이형 전기온수기 설치 기준이다."
repl_8_4_2 = """8-4-2 전기온수기(벽걸이형) 설치('20년 신설)
(대당)
| | | (용량) | | |
|---|---|---|---|---|
| | | 15L | 30L | 50L |
| 보일러공 | | 0.17 | 0.18 | 0.23 |
| 보통인부 | | 0.07 | 0.08 | 0.09 |
[] 품은 벽걸이형 전기온수기 설치 기준이다."""
text8 = text8.replace(target_8_2_5, repl_8_2_5)
text8 = text8.replace(target_8_3_3, repl_8_3_3)
text8 = text8.replace(target_8_4_2, repl_8_4_2)
with open(ch8_path, 'w', encoding='utf-8') as f:
f.write(text8)
print("Updated 제8장_공기조화설비공사.md successfully.")
# 6. 제10장 소방설비공사
ch10_path = "resources/knowledge/original/원가계산/건설공사_표준품셈/04_기계설비부문/제10장_소방설비공사.md"
with open(ch10_path, 'r', encoding='utf-8') as f:
text10 = f.read()
target_10_8_2 = "10-8-2 자동식 소화기 설치('99년 신설, '14년 보완)(개당)구 분 단 위수 량기계설비공인 0.212보통인부인0.117[주] ① 본 품은 세대내 레인지후드에 자동식 소화기를 설치하는 품이다."
repl_10_8_2 = """10-8-2 자동식 소화기 설치('99년 신설, '14년 보완)
(개당)
| | | |
|---|---|---|
| 기계설비공 | | 0.212 |
| 보통인부 | | 0.117 |
[] 품은 세대내 레인지후드에 자동식 소화기를 설치하는 품이다."""
target_10_9_1 = "10-9 피난기구10-9-1 완강기 설치('04년 신설, '09, '14년 보완)(개당)구 분 단 위수 량기계설비공인 0.094보통인부인0.046[주] ① 본 품은 피난용 완강기를 설치하는 품이다."
repl_10_9_1 = """10-9 피난기구
10-9-1 완강기 설치('04년 신설, '09, '14년 보완)
(개당)
| | | |
|---|---|---|
| 기계설비공 | | 0.094 |
| 보통인부 | | 0.046 |
[] 품은 피난용 완강기를 설치하는 품이다."""
text10 = text10.replace(target_10_8_2, repl_10_8_2)
text10 = text10.replace(target_10_9_1, repl_10_9_1)
with open(ch10_path, 'w', encoding='utf-8') as f:
f.write(text10)
print("Updated 제10장_소방설비공사.md successfully.")
# 7. 제13장 플랜트설비공사 Buckstay
ch13_path = "resources/knowledge/original/원가계산/건설공사_표준품셈/04_기계설비부문/제13장_플랜트설비공사.md"
with open(ch13_path, 'r', encoding='utf-8') as f:
text13 = f.read()
target_13_buck = "→제13장 플랜트설비공사771작 업 구 분직 종단 위수 량Buckstay 조립조정플랜트기계설치공인/ton1.61조양된 buckstay를 alignment하고 tiebar 특별인부〃1.81취급함.플랜트용접공〃1.41[참고]"
repl_13_buck = """→제13장 플랜트설비공사 771
| | | | | |
|---|---|---|---|---|
| Buckstay 조립조정 | 플랜트기계설치공 | /ton | 1.61 | 조양된 buckstay를 alignment하고 tiebar 취급함. |
| | 특별인부 | | 1.81 | |
| | 플랜트용접공 | | 1.41 | |
[참고]"""
text13 = text13.replace(target_13_buck, repl_13_buck)
with open(ch13_path, 'w', encoding='utf-8') as f:
f.write(text13)
print("Updated 제13장_플랜트설비공사.md successfully.")
@@ -0,0 +1,91 @@
import os, sys, re
sys.stdout.reconfigure(encoding='utf-8')
ch9_path = "resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제9장_측량.md"
with open(ch9_path, 'r', encoding='utf-8') as f:
lines = f.readlines()
new_lines = []
fixed_cnt = 0
tech_words = [
"특급기술자", "고급기술자", "중급기술자", "초급기술자", "초급기능사(측량)", "초급기능사", "인부", "측량사", "측량사보"
]
for idx, line in enumerate(lines):
line_str = line.strip()
if not (line_str.startswith('|') and line_str.endswith('|')):
new_lines.append(line)
continue
cells = [c.strip() for c in line_str.split('|')[1:-1]]
if len(cells) < 3 or cells[0] in ["", "합계"]:
new_lines.append(line)
continue
# Check if first cell contains multiple technician grades
c0 = cells[0]
c0_clean = c0.replace(" ", "")
matched_grades = []
temp_s = c0_clean
for tw in sorted(tech_words, key=len, reverse=True):
while tw in temp_s:
matched_grades.append(tw)
temp_s = temp_s.replace(tw, "", 1)
if len(matched_grades) >= 2 and temp_s == "":
# Reconstruct in order
ordered_grades = []
pos = 0
while pos < len(c0_clean):
for tw in sorted(tech_words, key=len, reverse=True):
if c0_clean[pos:].startswith(tw):
ordered_grades.append(tw)
pos += len(tw)
break
N = len(ordered_grades)
# Check column 1 (수량/산식)
# Often contains N formulas ending with = number
# e.g. "3×10/16×1.2×1.12= 2.52 24.5×10/16..."
c1 = cells[1]
# Match formulas like "...= <num>"
f_matches = list(re.finditer(r'(.*?=\s*\d+(?:\.\d+)?)\s*(?=[^=]*=\s*\d+|$)', c1))
c1_parts = [m.group(1).strip() for m in f_matches if m.group(1).strip()]
# Check column 2 (단가)
c2 = cells[2] if len(cells) > 2 else ""
c2_toks = c2.split()
# Check column 3 (금액)
c3 = cells[3] if len(cells) > 3 else ""
w_matches = list(re.finditer(r'(W\s*=\s*[^W]+?)(?=\s*W\s*=|(?:\s*1\s+)?$)', c3))
c3_parts = [m.group(1).strip() for m in w_matches if m.group(1).strip()]
if len(c1_parts) == N:
# We can split!
for r_idx in range(N):
g_name = ordered_grades[r_idx]
qty_val = c1_parts[r_idx]
rate_val = c2_toks[r_idx] if len(c2_toks) == N else (c2_toks[0] if c2_toks else "")
amt_val = c3_parts[r_idx] if len(c3_parts) == N else ""
row_cells = [g_name, qty_val, rate_val, amt_val]
# If there were more columns, append them
for extra_c in range(4, len(cells)):
row_cells.append("" if r_idx > 0 else cells[extra_c])
new_line_str = "| " + " | ".join(row_cells) + " |\n"
new_lines.append(new_line_str)
fixed_cnt += 1
print(f"L{idx+1}: Fixed survey calc table with {N} grades: {c0[:40]}")
continue
new_lines.append(line)
print(f"\nTotal survey calc tables fixed: {fixed_cnt}")
if fixed_cnt > 0:
with open(ch9_path, 'w', encoding='utf-8') as f:
f.writelines(new_lines)
print("Saved 제9장_측량.md successfully.")
@@ -0,0 +1,87 @@
import os, sys, re
sys.stdout.reconfigure(encoding='utf-8')
ch9_path = "resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문/제9장_측량.md"
with open(ch9_path, 'r', encoding='utf-8') as f:
lines = f.readlines()
new_lines = []
fixed_tables = 0
# Common tasks lists in survey
known_task_patterns = [
["계획준비", "답사선점", "조표(매설)", "관 측", "계 산", "정리점검"],
["계획준비", "답사선점", "조표(매설)", "관측", "계산", "정리점검"],
["계획준비", "답사선점", "관 측", "계 산", "정리점검"],
["계획준비", "답사선점", "관측", "계산", "정리점검"],
["계획준비", "답사", "선점", "매설", "관측", "계산", "정리점검"],
["계획준비", "선점", "매설", "관측", "계산", "정리점검"],
]
for idx, line in enumerate(lines):
line_str = line.strip()
if not (line_str.startswith('|') and line_str.endswith('|')):
new_lines.append(line)
continue
cells = [c.strip() for c in line_str.split('|')[1:-1]]
# Check if first cell contains tasks
matched_tasks = None
c0 = cells[0]
# Try exact task sequence match
for pat in known_task_patterns:
pat_str = " ".join(pat)
if c0 == pat_str or c0.replace(" ", "") == "".join(pat).replace(" ", ""):
matched_tasks = pat
break
if not matched_tasks:
# Try generic tasks split if tokens are known tasks
toks = c0.split()
if len(toks) >= 4 and all(any(k in t for k in ["계획", "준비", "답사", "선점", "조표", "매설", "관측", "계산", "정리", "점검", "수준", "측량", "항공", "도화", "편집"]) for t in toks):
matched_tasks = toks
if matched_tasks:
N = len(matched_tasks)
# Check if following data columns can be split into N tokens
# Some columns might have single value (like 비고) or N tokens
can_split = True
col_splits = {}
for c_idx in range(1, len(cells)):
val = cells[c_idx]
if not val or val == "-":
col_splits[c_idx] = [val] * N
continue
v_toks = val.split()
if len(v_toks) == N:
col_splits[c_idx] = v_toks
elif c_idx == len(cells) - 1: # Last column is often 비고
col_splits[c_idx] = [val] + [""] * (N - 1)
else:
# Cannot cleanly split this column
can_split = False
break
if can_split:
# Generate N rows
for r_idx in range(N):
row_cells = [matched_tasks[r_idx]]
for c_idx in range(1, len(cells)):
row_cells.append(col_splits[c_idx][r_idx])
new_line_str = "| " + " | ".join(row_cells) + " |\n"
new_lines.append(new_line_str)
fixed_tables += 1
print(f"L{idx+1}: Fixed survey table with {N} tasks: {cells[0][:30]}...")
continue
new_lines.append(line)
print(f"\nTotal survey task tables fixed in 제9장_측량.md: {fixed_tables}")
if fixed_tables > 0:
with open(ch9_path, 'w', encoding='utf-8') as f:
f.writelines(new_lines)
print("Saved 제9장_측량.md successfully.")
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,317 @@
[
{
"file": "01_공통부문\\제1장_적용기준.md",
"original_lines": 728,
"new_lines": 759,
"lines_added": 31,
"fixes_applied": 31
},
{
"file": "01_공통부문\\제2장_가설공사.md",
"original_lines": 1102,
"new_lines": 1184,
"lines_added": 82,
"fixes_applied": 82
},
{
"file": "01_공통부문\\제3장_토공사.md",
"original_lines": 909,
"new_lines": 948,
"lines_added": 39,
"fixes_applied": 39
},
{
"file": "01_공통부문\\제4장_조경공사.md",
"original_lines": 261,
"new_lines": 279,
"lines_added": 18,
"fixes_applied": 18
},
{
"file": "01_공통부문\\제5장_기초공사.md",
"original_lines": 974,
"new_lines": 989,
"lines_added": 15,
"fixes_applied": 15
},
{
"file": "01_공통부문\\제6장_철근콘크리트공사.md",
"original_lines": 998,
"new_lines": 1037,
"lines_added": 39,
"fixes_applied": 39
},
{
"file": "01_공통부문\\제7장_돌공사.md",
"original_lines": 117,
"new_lines": 125,
"lines_added": 8,
"fixes_applied": 8
},
{
"file": "01_공통부문\\제8장_건설기계.md",
"original_lines": 3889,
"new_lines": 3923,
"lines_added": 34,
"fixes_applied": 34
},
{
"file": "02_토목부문\\제1장_도로포장공사.md",
"original_lines": 935,
"new_lines": 992,
"lines_added": 57,
"fixes_applied": 57
},
{
"file": "02_토목부문\\제2장_하천공사.md",
"original_lines": 76,
"new_lines": 84,
"lines_added": 8,
"fixes_applied": 8
},
{
"file": "02_토목부문\\제3장_터널공사.md",
"original_lines": 231,
"new_lines": 241,
"lines_added": 10,
"fixes_applied": 10
},
{
"file": "02_토목부문\\제4장_궤도공사.md",
"original_lines": 321,
"new_lines": 345,
"lines_added": 24,
"fixes_applied": 24
},
{
"file": "02_토목부문\\제5장_강구조공사.md",
"original_lines": 319,
"new_lines": 329,
"lines_added": 10,
"fixes_applied": 10
},
{
"file": "02_토목부문\\제6장_관부설및접합공사.md",
"original_lines": 688,
"new_lines": 711,
"lines_added": 23,
"fixes_applied": 23
},
{
"file": "02_토목부문\\제7장_항만공사.md",
"original_lines": 322,
"new_lines": 341,
"lines_added": 19,
"fixes_applied": 19
},
{
"file": "02_토목부문\\제8장_지반조사.md",
"original_lines": 320,
"new_lines": 333,
"lines_added": 13,
"fixes_applied": 13
},
{
"file": "02_토목부문\\제9장_측량.md",
"original_lines": 4604,
"new_lines": 4664,
"lines_added": 60,
"fixes_applied": 60
},
{
"file": "03_건축부문\\제10장_창호및유리공사.md",
"original_lines": 142,
"new_lines": 151,
"lines_added": 9,
"fixes_applied": 9
},
{
"file": "03_건축부문\\제11장_칠공사.md",
"original_lines": 267,
"new_lines": 286,
"lines_added": 19,
"fixes_applied": 19
},
{
"file": "03_건축부문\\제1장_철골공사.md",
"original_lines": 371,
"new_lines": 385,
"lines_added": 14,
"fixes_applied": 14
},
{
"file": "03_건축부문\\제2장_조적공사.md",
"original_lines": 162,
"new_lines": 171,
"lines_added": 9,
"fixes_applied": 9
},
{
"file": "03_건축부문\\제3장_타일공사.md",
"original_lines": 99,
"new_lines": 105,
"lines_added": 6,
"fixes_applied": 6
},
{
"file": "03_건축부문\\제4장_목공사.md",
"original_lines": 141,
"new_lines": 153,
"lines_added": 12,
"fixes_applied": 12
},
{
"file": "03_건축부문\\제5장_수장공사.md",
"original_lines": 385,
"new_lines": 409,
"lines_added": 24,
"fixes_applied": 24
},
{
"file": "03_건축부문\\제6장_방수공사.md",
"original_lines": 274,
"new_lines": 297,
"lines_added": 23,
"fixes_applied": 23
},
{
"file": "03_건축부문\\제7장_지붕및홈통공사.md",
"original_lines": 138,
"new_lines": 148,
"lines_added": 10,
"fixes_applied": 10
},
{
"file": "03_건축부문\\제8장_금속공사.md",
"original_lines": 219,
"new_lines": 232,
"lines_added": 13,
"fixes_applied": 13
},
{
"file": "03_건축부문\\제9장_미장공사.md",
"original_lines": 213,
"new_lines": 226,
"lines_added": 13,
"fixes_applied": 13
},
{
"file": "04_기계설비부문\\제10장_소방설비공사.md",
"original_lines": 170,
"new_lines": 184,
"lines_added": 14,
"fixes_applied": 14
},
{
"file": "04_기계설비부문\\제11장_가스설비공사.md",
"original_lines": 177,
"new_lines": 183,
"lines_added": 6,
"fixes_applied": 6
},
{
"file": "04_기계설비부문\\제12장_자동제어설비공사.md",
"original_lines": 167,
"new_lines": 173,
"lines_added": 6,
"fixes_applied": 6
},
{
"file": "04_기계설비부문\\제13장_플랜트설비공사.md",
"original_lines": 3498,
"new_lines": 3537,
"lines_added": 39,
"fixes_applied": 39
},
{
"file": "04_기계설비부문\\제1장_배관공사.md",
"original_lines": 357,
"new_lines": 376,
"lines_added": 19,
"fixes_applied": 19
},
{
"file": "04_기계설비부문\\제2장_덕트공사.md",
"original_lines": 174,
"new_lines": 186,
"lines_added": 12,
"fixes_applied": 12
},
{
"file": "04_기계설비부문\\제3장_보온공사.md",
"original_lines": 152,
"new_lines": 162,
"lines_added": 10,
"fixes_applied": 10
},
{
"file": "04_기계설비부문\\제4장_펌프및공기설비공사.md",
"original_lines": 134,
"new_lines": 143,
"lines_added": 9,
"fixes_applied": 9
},
{
"file": "04_기계설비부문\\제5장_밸브설비공사.md",
"original_lines": 88,
"new_lines": 93,
"lines_added": 5,
"fixes_applied": 5
},
{
"file": "04_기계설비부문\\제6장_측정기기공사.md",
"original_lines": 79,
"new_lines": 86,
"lines_added": 7,
"fixes_applied": 7
},
{
"file": "04_기계설비부문\\제7장_위생기구설비공사.md",
"original_lines": 201,
"new_lines": 216,
"lines_added": 15,
"fixes_applied": 15
},
{
"file": "04_기계설비부문\\제8장_공기조화설비공사.md",
"original_lines": 345,
"new_lines": 364,
"lines_added": 19,
"fixes_applied": 19
},
{
"file": "04_기계설비부문\\제9장_기타공사.md",
"original_lines": 193,
"new_lines": 205,
"lines_added": 12,
"fixes_applied": 12
},
{
"file": "05_유지관리부문\\제1장_공통.md",
"original_lines": 505,
"new_lines": 529,
"lines_added": 24,
"fixes_applied": 24
},
{
"file": "05_유지관리부문\\제2장_토목.md",
"original_lines": 1284,
"new_lines": 1326,
"lines_added": 42,
"fixes_applied": 42
},
{
"file": "05_유지관리부문\\제3장_건축.md",
"original_lines": 297,
"new_lines": 316,
"lines_added": 19,
"fixes_applied": 19
},
{
"file": "05_유지관리부문\\제4장_기계설비.md",
"original_lines": 184,
"new_lines": 193,
"lines_added": 9,
"fixes_applied": 9
}
]
@@ -0,0 +1,13 @@
import sys, fitz
sys.stdout.reconfigure(encoding='utf-8')
pdf_path = "resources/knowledge/original/원가계산/건설공사_표준품셈/2026년_건설공사_표준품셈.pdf"
doc = fitz.open(pdf_path)
# Check 1-2-7 덤프적재표 (PDF page 73-75, printed page 17-19)
print("=== 1-2-7 운반 PDF Page 74 (Printed page 18) ===")
print(doc[73].get_text()[:1500])
# Check 1-3-1 재료할증 (PDF page 76-78, printed page 20-22)
print("\n=== 1-3-1 재료할증 PDF Page 76 (Printed page 20) ===")
print(doc[75].get_text()[:1500])
@@ -0,0 +1,13 @@
import os, sys, json
sys.stdout.reconfigure(encoding='utf-8')
with open('scratch/const_md_flaws_scan.json', 'r', encoding='utf-8') as f:
data = json.load(f)
for k, v in data.items():
t_list = v.get('collapsed_table_rows', [])
print(f"\n=== {k}: {len(t_list)} collapsed rows ===")
for idx, t in enumerate(t_list):
snip = t['snippet'].replace('\n', ' ')
print(f" [{idx+1}] L{t['line']} (tokens: {t['token_count']}): {snip[:90]}")
@@ -0,0 +1,13 @@
import json, sys
sys.stdout.reconfigure(encoding='utf-8')
with open('scratch/wide_criteria_my_scope.json', 'r', encoding='utf-8') as f:
data = json.load(f)
survey_items = [d for d in data if '제9장_측량' in d['file']]
print(f"Total wide rows in 제9장 측량: {len(survey_items)}")
for item in survey_items[:15]:
print(f"\nL{item['line']}: {item['reasons']}")
print(f" RAW: {item['raw_line']}")
@@ -0,0 +1,60 @@
import json
import sys
import re
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent))
from scratch.enrich_tables import tables, get_chapter
sys.stdout.reconfigure(encoding='utf-8')
with open('resources/data_work_item_master/work_item_master_2026-01-01.json', 'r', encoding='utf-8') as f:
master = json.load(f)
master_tables = {}
wi_map = {}
for wi in master['work_items']:
for t in wi.get('tables', []):
tid = t.get('pum_table_id')
if tid:
master_tables[tid] = t
wi_map[tid] = wi
# Check chapters 1, 2, 5, 9, 13
for ch_num in ['제1장', '제2장', '제5장', '제9장', '제13장']:
print(f"\n==================== {ch_num} ====================")
ch_tables = [t for t in tables if ch_num in t['chapter']]
print(f"Total tables in {ch_num}: {len(ch_tables)}")
# Analyze form, basis, notes, variants
for t in ch_tables:
tid = t['table_id']
mt = master_tables.get(tid)
if not mt:
continue
m_qty = mt.get('basis_quantity')
m_unit = mt.get('basis_unit')
pform = mt.get('pum_form')
pre_text = " // ".join(t['pre_context'][-3:])
notes_text = " // ".join(t['notes'][:3]) if t['notes'] else ""
header_text = t['lines'][0].strip() if t['lines'] else ""
# Report issues
issues = []
if pform == 'reference':
# Check if this table contains actual numbers/costs
issues.append(f"FORM_REFERENCE_SUSPECT (form={pform})")
if m_qty is None or m_unit is None:
# Check if MD has basis
issues.append(f"BASIS_NULL (master={m_qty} {m_unit})")
if notes_text and not mt.get('condition_note'):
issues.append(f"NOTE_MISSING_IN_MASTER ({len(t['notes'])} notes)")
if issues:
print(f"[{tid} | line {t['start_line']}] {t['section']} -> {', '.join(issues)}")
if "REFERENCE" in str(issues) or "BASIS" in str(issues):
print(f" pre: {pre_text}")
print(f" hdr: {header_text}")
if notes_text:
print(f" notes: {notes_text}")
@@ -0,0 +1,47 @@
import json
import sys
import re
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent))
from scratch.enrich_tables import tables, get_chapter
sys.stdout.reconfigure(encoding='utf-8')
with open('resources/data_work_item_master/work_item_master_2026-01-01.json', 'r', encoding='utf-8') as f:
master = json.load(f)
master_tables = {}
wi_map = {}
for wi in master['work_items']:
for t in wi.get('tables', []):
tid = t.get('pum_table_id')
if tid:
master_tables[tid] = t
wi_map[tid] = wi
# Check chapter 5, 9, 12 specifically
for ch_num in ['제5장', '제9장', '제12장']:
print(f"\n==================== {ch_num} ====================")
ch_tables = [t for t in tables if ch_num in t['chapter']]
for t in ch_tables:
tid = t['table_id']
mt = master_tables.get(tid)
if not mt:
continue
m_qty = mt.get('basis_quantity')
m_unit = mt.get('basis_unit')
pform = mt.get('pum_form')
# Look for unit patterns in pre_context and headers
pre_text = " // ".join(t['pre_context'][-5:])
notes_text = " // ".join(t['notes'][:3]) if t['notes'] else ""
header_text = t['lines'][0].strip() if t['lines'] else ""
# Check if basis missing or interesting
if m_qty is None or m_unit is None or pform == 'reference':
print(f"[{tid} | line {t['start_line']}] sec: {t['section']}")
print(f" pre: {pre_text}")
print(f" hdr: {header_text}")
print(f" master basis: {m_qty} {m_unit} | form: {pform}")
if notes_text:
print(f" notes: {notes_text}")
@@ -0,0 +1,34 @@
import os, sys
sys.path.append('scratch')
from find_strict_grouped_tables import true_grouped_tables
sys.stdout.reconfigure(encoding='utf-8')
target_fnames = [
'제3장_터널공사.md',
'제4장_궤도공사.md',
'제5장_강구조공사.md',
'제6장_관부설및접합공사.md',
'제7장_항만공사.md',
'제8장_지반조사.md',
]
for fname in target_fnames:
tbls = [t for t in true_grouped_tables if t['file'] == fname]
print(f"\n#######################################################")
print(f"### {fname} (총 {len(tbls)}개 표)")
print(f"#######################################################")
# find path
fpath = os.path.join('resources/knowledge/original/원가계산/건설공사_표준품셈/02_토목부문', fname)
with open(fpath, 'r', encoding='utf-8') as f:
lines = f.readlines()
for t in tbls:
lno = t['line']
print(f"\n--- [L{lno}] {t['title']} ---")
print(f"Groups: {t['groups']}")
start = max(0, lno - 1)
end = min(len(lines), lno + t['total_rows'] + 2)
for i in range(start, end):
print(f"{i+1:4d}: {lines[i].rstrip()}")
@@ -0,0 +1,38 @@
import pypdf, sys, os
sys.stdout.reconfigure(encoding='utf-8')
reader = pypdf.PdfReader('resources/knowledge/original/원가계산/건설공사_표준품셈/2026년_건설공사_표준품셈.pdf')
targets = [
("2-8-7", "철골 안전망"),
("2-8-10", "수직형 추락방망"),
("2-11-5", "비산먼지"),
("2-11-7", "슬러지 제거"),
("4-4-4", "야자섬유매트"),
("5-1-3", "버팀보"),
("5-1-8", "복공판"),
("7-4-2", "앵커 긴결"),
("슬립폼", "설치 및 해체"),
("지수판", "소켓 연결"),
("정착구 설치", "쉬즈관"),
]
for sec, kw in targets:
found = False
for idx, page in enumerate(reader.pages[:300]): # common part is in first 300 pages
txt = page.extract_text()
if sec in txt or (kw in txt and ("" in txt or "" in txt)):
print(f"=== Found '{sec}' on PDF page index {idx} ===")
# print surrounding lines
lines = txt.split('\n')
for l_idx, l in enumerate(lines):
if sec in l or kw in l:
start = max(0, l_idx - 3)
end = min(len(lines), l_idx + 15)
print("\n".join(lines[start:end]))
print("-" * 50)
found = True
break
if found:
break
@@ -0,0 +1,28 @@
import json
import sys
from pathlib import Path
sys.stdout.reconfigure(encoding='utf-8')
# Check const_work_item_master
const_path = Path('resources/data_work_item_master/const_work_item_master_2026-01-01.json')
with open(const_path, 'r', encoding='utf-8') as f:
const_master = json.load(f)
print("const_work_item_master keys:", list(const_master.keys()))
const_items = const_master.get('work_items', [])
print(f"const work_items count: {len(const_items)}")
# Count tables with notes
tables_with_notes = 0
total_const_tables = 0
for wi in const_items:
for t in wi.get('tables', []):
total_const_tables += 1
if t.get('notes'):
tables_with_notes += 1
print(f"Total const tables: {total_const_tables}, with notes: {tables_with_notes}")
# Inspect git diff on const_work_item_master for notes removal
# Run a git command to see what lines were removed in commit 32c4c5cf for const master
+34
View File
@@ -0,0 +1,34 @@
import json
from pathlib import Path
pum_forest_path = Path('resources/data_cost_input_value/pum_forest_2026.json')
with open(pum_forest_path, 'r', encoding='utf-8') as f:
p = json.load(f)
pum_var = p.get('variables', {}).get('pum', {})
print("pum_var keys:", list(pum_var.keys()))
for k, v in pum_var.items():
if isinstance(v, dict):
print(f" {k}: dict with {len(v)} keys (sample: {list(v.keys())[:5]})")
elif isinstance(v, list):
print(f" {k}: list with {len(v)} items")
else:
print(f" {k}: {type(v)}")
# Check work_item_master structure details
master_path = Path('resources/data_work_item_master/work_item_master_2026-01-01.json')
with open(master_path, 'r', encoding='utf-8') as f:
w = json.load(f)
print("\n--- Work Item Sample ---")
wi_with_tables = [wi for wi in w['work_items'] if wi.get('tables')][:2]
for wi in wi_with_tables:
print(f"WI: {wi['work_item_code']} / {wi['name']} / level {wi['level']}")
for tbl in wi['tables']:
print(f" Table: {tbl.get('pum_table_id')} / {tbl.get('section')} / basis: {tbl.get('basis_quantity')} {tbl.get('basis_unit')} / form: {tbl.get('pum_form')}")
if 'raw_markdown' in tbl:
print(f" raw_markdown lines: {len(tbl['raw_markdown'].splitlines())}")
print(f"\nOrphan tables count: {len(w.get('orphan_tables', []))}")
for ot in w.get('orphan_tables', [])[:5]:
print(f" Orphan Table: {ot.get('pum_table_id')} / {ot.get('section')} / basis: {ot.get('basis_quantity')} {ot.get('basis_unit')}")

Some files were not shown because too many files have changed in this diff Show More