"""Keep the original OOXML package (including WMF and shapes); patch selected cells only.""" from pathlib import Path import zipfile,json,sys,re,hashlib,html,base64 from xml.etree import ElementTree as ET ROOT=Path(__file__).resolve().parent.parent NS={'s':'http://schemas.openxmlformats.org/spreadsheetml/2006/main'} def write(payload_path,output_path): source=ROOT/'교각수량산출.xlsx';output=Path(output_path).resolve() if not output.is_relative_to((ROOT/'outputs').resolve()) or output.exists():raise ValueError('결과 경로는 수량산출/outputs 내부의 새 파일이어야 합니다.') data=json.loads(Path(payload_path).read_text(encoding='utf-8-sig')) if hashlib.sha256(source.read_bytes()).hexdigest()!=data['templateHash']:raise ValueError('원본 템플릿이 변경되었습니다. 분석을 다시 실행하세요.') with zipfile.ZipFile(source) as zin: wb=ET.fromstring(zin.read('xl/workbook.xml'));rels=ET.fromstring(zin.read('xl/_rels/workbook.xml.rels')) targets={r.attrib['Id']:r.attrib['Target'] for r in rels} paths={s.attrib['name']:('xl/'+targets[s.attrib['{http://schemas.openxmlformats.org/officeDocument/2006/relationships}id']].lstrip('/').removeprefix('xl/')) for s in wb.find('s:sheets',NS)} updates={} for sheet,edits in data['edits'].items(): path=paths[sheet];xml=zin.read(path).decode('utf8') for cell,change in edits.items(): pattern=r']*\br="'+re.escape(cell)+r'"[^>]*)(?:>(.*?)|/>)' def replace(match): attrs=match[1];body=match[2] or '' if 'value' in change: attrs=re.sub(r'\s+t="[^"]*"','',attrs);v=change['value'] if v is None:return '' if isinstance(v,str):return ''+html.escape(v)+'' return ''+str(v)+'' if ']*>.*?',lambda _: ''+html.escape(change['formula'].removeprefix('='))+'',body,flags=re.S) body=re.sub(r']*)?>.*?|','',body,flags=re.S) return ''+body+''+str(change['cached'])+'' xml,count=re.subn(pattern,replace,xml,count=1,flags=re.S) if count!=1:raise ValueError('기존 입력셀을 찾을 수 없습니다: '+sheet+'!'+cell) updates[path]=xml.encode('utf8') xml=zin.read('xl/workbook.xml').decode('utf8') xml=re.sub(r']*/>', '',xml) updates['xl/workbook.xml']=xml.encode('utf8') updates['xl/_rels/workbook.xml.rels']=re.sub(r']*Type="[^"]*/calcChain"[^>]*/>','',zin.read('xl/_rels/workbook.xml.rels').decode('utf8')).encode('utf8') updates['[Content_Types].xml']=re.sub(r']*PartName="/xl/calcChain.xml"[^>]*/>','',zin.read('[Content_Types].xml').decode('utf8')).encode('utf8') additions={} if data.get('planPng'): image=base64.b64decode(data['planPng'],validate=True) if not image.startswith(b'\x89PNG\r\n\x1a\n') or len(image)>1200000:raise ValueError('KEY PLAN PNG가 올바르지 않습니다.') # Only the visible 3.2 sample alignment picture is replaced. Source # workbook and all calculation cells / other figures remain intact. drawing=ET.fromstring(zin.read('xl/drawings/drawing5.xml')) dns={'x':'http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing','a':'http://schemas.openxmlformats.org/drawingml/2006/main'} target=next(a for a in drawing if a.find('x:from/x:col',dns).text=='0') rid=target.find('.//a:blip',dns).get('{http://schemas.openxmlformats.org/officeDocument/2006/relationships}embed') relpath='xl/drawings/_rels/drawing5.xml.rels' relxml=zin.read(relpath).decode('utf8') pattern=r']*\bId="'+re.escape(rid)+r'")[^>]*/>' relxml,count=re.subn(pattern,lambda m:re.sub(r'Target="[^"]*"','Target="../media/quantity-project-plan.png"',m[0]),relxml) if count!=1:raise ValueError('3.2 KEY PLAN 연결을 찾지 못했습니다.') updates[relpath]=relxml.encode('utf8') additions['xl/media/quantity-project-plan.png']=image output.parent.mkdir(parents=True,exist_ok=True) with zipfile.ZipFile(output,'w',zipfile.ZIP_DEFLATED) as zout: for info in zin.infolist(): if info.filename=='xl/calcChain.xml':continue zout.writestr(info,updates.get(info.filename,zin.read(info.filename))) for name,content in additions.items():zout.writestr(name,content) report={'status':'generated','sheets':len(paths),'patched_cells':sum(map(len,data['edits'].values())),'original_preserved':True,'straight_limit_formulas':sum('formula' in c for cells in data['edits'].values() for c in cells.values()),'unchanged_package_parts':len(zin.namelist())-len(updates)-('xl/calcChain.xml' in zin.namelist()),'formula_policy':'P1 source formulas with explicit straight-limit overrides when applicable; P2-P6 unused blank; native full recalculation on open'} output.with_suffix('.validation.json').write_text(json.dumps(report,ensure_ascii=False,indent=2),encoding='utf8') return report if __name__=='__main__': try:print(json.dumps(write(sys.argv[1],sys.argv[2]),ensure_ascii=False)) except Exception as e:print(json.dumps({'error':str(e)},ensure_ascii=False));sys.exit(1)