"""Independent arithmetic and serialized-output checks for the audited 12 samples.""" import json, math, hashlib from pathlib import Path from openpyxl import load_workbook, Workbook from openpyxl.styles import Font, PatternFill, Alignment from sample_support import ROOT, BASE out=ROOT/'추출결과_12개교량_20261029' summary=json.loads((out/'추출요약.json').read_text(encoding='utf-8')) assert len(summary)==12 checked=0;piers=0;bearings=0;issues=[] for s in summary: path=Path(s['file']);data=json.loads(path.with_suffix('.json').read_text(encoding='utf-8')) cells=data['cells'];wb=load_workbook(path) for sheet,values in cells.items(): for addr,value in values.items(): actual=wb[sheet][addr].value assert actual==value,(path.name,sheet,addr,actual,value) if isinstance(value,(int,float)):assert math.isfinite(value) checked+=1 for addr,p in cells['Pier'].items(): if not addr.startswith('B'):continue r=addr[1:];piers+=1 assert cells['Pier']['K'+r]==(2 if cells['Pier']['C'+r]=='π형' else 1) br=[a[1:] for a,v in cells['Bearing'].items() if a.startswith('B') and v==p] ip=[a[1:] for a,v in cells['IP'].items() if a.startswith('B') and v==p] assert len(ip)==3 def xy(sheet,r):return tuple(cells[sheet][c+r] for c in 'DE') pts=[xy('Bearing',b) for b in br] assert all(all(isinstance(x,(int,float)) for x in point) for point in pts) maximum=max(math.dist(a,b) for a in pts for b in pts)*1000 assert cells['Pier']['I'+r]==(math.floor(round(maximum,6)/500)+1)*500 ips=[xy('IP',i) for i in ip] assert len(set(ips))==3 length=math.dist(ips[0],ips[2])*1000 assert cells['Pier']['J'+r]==(math.floor(round(length,6)/500)+1)*500 for b in br:assert cells['Bearing']['H'+b] in ['전열','후열'] bearings+=s['bearings'] for rec in data['records']: if rec['value'] is None and not(rec['sheet']=='IP' and rec['cell'].startswith('F')): row=rec['cell'][1:];p=cells[rec['sheet']].get('B'+row) issues.append([s['bridge'],p,rec['sheet'],rec['cell'],wb[rec['sheet']][rec['cell'][0]+'2'].value,rec['status'],' / '.join(rec['messages'])]) for e in rec['evidence']:assert e['doc'] in data['sources'] wb.close() inventory=json.loads((BASE/'inventory.json').read_text(encoding='utf-8')) assert len(inventory)==127 for src in inventory: assert 'error' not in src,src assert hashlib.sha256(Path(src['path']).read_bytes()).hexdigest()==src['sha256'] assert (piers,bearings)==(64,652) book=Workbook();ws=book.active;ws.title='추출현황' ws.append(['Sample','교량','교각 수','받침 수','미확정 셀 수','결과 파일']) for s in summary: ws.append([s['sample'],s['bridge'],s['piers'],s['bearings'],s['missing'],Path(s['file']).name]) ws.cell(ws.max_row,6).hyperlink=Path(s['file']).name ws.cell(ws.max_row,6).style='Hyperlink' ws.append(['합계','',piers,bearings,len(issues)]) review=book.create_sheet('미확정목록');review.append(['교량','교각','시트','셀','항목','상태','사유']); for row in issues:review.append(row) rules=book.create_sheet('적용기준') for row in [ ['항목','기준 / 범위'], ['교각형식','정면/평면 대표 형상 판독; T형 1개, π형 2개. 산외교 P9~P11 π형, 나머지 T형.'], ['두부보 EL / 받침 Z','ELEVATION 교좌면 EL 최소 / 교량받침 EL(몰탈상단).'], ['폭 / 길이','최대 받침 XY 거리 / IP1~IP3 XY 거리를 엄격히 초과하는 다음 500mm. 설계 확정 치수 아님.'], ['말뚝','길이 최소값 mm. 직접기초 확인 시 0, 길이 미확인/VAR은 빈칸.'], ['IP','도면 좌표 지시점 위→아래. 계문철교 원본 두 번째 P1 블록은 도면 위치에 따라 P2로 보정.'], ['기초 출처','종평면도. 목동교 좌/우 EL 및 기양천교 상충 EL은 병기 후 미확정.'], ['대호지교','종평면도 및 ELEVATION 산출근거도 부재: 관련 수치 빈칸.'], ['기존 엑셀 사용 범위','대상 교각/방향/지시점 대조용. 숫자는 CAD 문자에서 재추출. 이 배치 스크립트는 12개 샘플에 맞춘 판독 보정 포함.'], ['검증 범위','12개 XLSX 재읽기, 수치/단위/치수 산식/전후열/64교각·652받침/127원본 DWG 해시 검사.'], ['검증 한계','동일 작성자의 자기검증. 형상은 대표도면 표본 판독이며 모든 원도면의 독립 전수 육안 검증은 아님. 미확정값은 추정 대입하지 않음.'], ['연동','각 교량 XLSX 및 동일 이름 JSON/CSV 제공. 배포 wheel 재빌드 및 GUI 통합은 이번 배치에 포함하지 않음.']]:rules.append(row) for sh in book: sh.freeze_panes='A2';sh.auto_filter.ref=sh.dimensions for c in sh[1]:c.font=Font(name='맑은 고딕',bold=True,color='FFFFFF');c.fill=PatternFill('solid',fgColor='203864') for row in sh.iter_rows(min_row=2): for c in row:c.font=Font(name='맑은 고딕',size=10);c.alignment=Alignment(vertical='top',wrap_text=True) for col in 'ABCDEFG':sh.column_dimensions[col].width=18 sh.row_dimensions[1].height=28 ws.column_dimensions['F'].width=55;review.column_dimensions['G'].width=100 rules.column_dimensions['A'].width=27;rules.column_dimensions['B'].width=115 for r in range(2,rules.max_row+1):rules.row_dimensions[r].height=42 book.save(out/'00_추출현황_미확정목록.xlsx') report=dict(xlsx_count=12,piers=piers,bearings=bearings,cells_checked=checked,source_dwg_hashes_checked=len(inventory),unresolved_cells=len(issues),checks='PASS',independent_review=False) (out/'검증결과.json').write_text(json.dumps(report,ensure_ascii=False,indent=2),encoding='utf-8') print(json.dumps(report,ensure_ascii=False))