"""3시트 Excel 초안 생성기 (§3 스키마).""" from __future__ import annotations import os import tempfile from datetime import datetime import openpyxl from openpyxl.styles import Font, PatternFill, Alignment from .rules import get_all_measurements, get_all_modifiers, get_all_special_prefixes INSTRUMENT_COLS = [ "instrument_id", "display_name", "parent_base_tag", "role", "loop", "area", "measures", "data_points", "from", "to", "description", "confidence", "needs_review", "inference_basis", "operator_notes", "delete", ] POWER_EQUIPMENT_COLS = [ "instrument_id", "display_name", "parent_base_tag", "role", "loop", "area", "equipment_type", "data_points", "description", "confidence", "needs_review", "inference_basis", "operator_notes", "delete", ] HEADER_FILL = PatternFill(start_color="4472C4", end_color="4472C4", fill_type="solid") HEADER_FONT = Font(bold=True, color="FFFFFF", size=11) LOW_FILL = PatternFill(start_color="FFC7CE", end_color="FFC7CE", fill_type="solid") MEDIUM_FILL = PatternFill(start_color="FFEB9C", end_color="FFEB9C", fill_type="solid") def generate_excel(instruments: list[dict], unmatched: list[str], power_equipment: list[dict] | None = None) -> str: """ 4시트 Excel 생성 후 파일 경로 반환. Args: instruments: infer.py 결과 (instrument dict 리스트) unmatched: 룰 미매칭 base_tag 목록 power_equipment: 동력기기 (펌프, 압축기, 교반기 등) output_dir: 출력 디렉토리 (기본: /tmp) Returns: 생성된 xlsx 파일의 절대 경로 """ output_dir = tempfile.gettempdir() os.makedirs(output_dir, exist_ok=True) ts = datetime.now().strftime("%Y%m%d_%H%M%S") filepath = os.path.join(output_dir, f"instruments_draft_{ts}.xlsx") wb = openpyxl.Workbook() # ── 시트 1: instruments ── _build_instruments_sheet(wb, instruments) # ── 시트 2: power_equipment ── _build_power_equipment_sheet(wb, power_equipment or []) # ── 시트 3: unmatched_tags ── _build_unmatched_sheet(wb, unmatched) # ── 시트 4: naming_convention ── _build_naming_convention_sheet(wb) wb.save(filepath) return filepath def _build_instruments_sheet(wb: openpyxl.Workbook, instruments: list[dict]) -> None: ws = wb.active ws.title = "instruments" # 헤더 for col_idx, col_name in enumerate(INSTRUMENT_COLS, 1): cell = ws.cell(row=1, column=col_idx, value=col_name) cell.font = HEADER_FONT cell.fill = HEADER_FILL cell.alignment = Alignment(horizontal="center", wrap_text=True) # 데이터 행 for row_idx, inst in enumerate(instruments, 2): for col_idx, col_name in enumerate(INSTRUMENT_COLS, 1): val = inst.get(col_name, "") cell = ws.cell(row=row_idx, column=col_idx, value=val) # confidence별 색상 if col_name == "confidence": if val == "low": cell.fill = LOW_FILL elif val == "medium": cell.fill = MEDIUM_FILL # 컬럼 너비 자동 조정 for col_idx in range(1, len(INSTRUMENT_COLS) + 1): max_len = 0 for row in range(1, ws.max_row + 1): val = str(ws.cell(row=row, column=col_idx).value or "") max_len = max(max_len, len(val)) ws.column_dimensions[openpyxl.utils.get_column_letter(col_idx)].width = min(max_len + 4, 40) def _build_naming_convention_sheet(wb: openpyxl.Workbook) -> None: ws = wb.create_sheet("naming_convention") # 측정량 표 ws.cell(row=1, column=1, value="측정량 (첫 글자)").font = HEADER_FONT ws.cell(row=1, column=2, value="의미").font = HEADER_FONT meas = get_all_measurements() for i, (letter, meaning) in enumerate(meas.items(), 2): ws.cell(row=i, column=1, value=letter) ws.cell(row=i, column=2, value=meaning) # 수식어 표 (D열부터) ws.cell(row=1, column=4, value="수식어 (두 번째 이후)").font = HEADER_FONT ws.cell(row=1, column=5, value="role").font = HEADER_FONT ws.cell(row=1, column=6, value="가상").font = HEADER_FONT mods = get_all_modifiers() for i, (letter, info) in enumerate(mods.items(), 2): ws.cell(row=i, column=4, value=letter) ws.cell(row=i, column=5, value=info.get("role", "")) ws.cell(row=i, column=6, value=info.get("virtual", False)) # 특수 prefix 표 (G열부터) ws.cell(row=1, column=7, value="특수 prefix").font = HEADER_FONT ws.cell(row=1, column=8, value="role").font = HEADER_FONT sps = get_all_special_prefixes() for i, (prefix, info) in enumerate(sps.items(), 2): ws.cell(row=i, column=7, value=prefix) ws.cell(row=i, column=8, value=info.get("role", "")) def _build_unmatched_sheet(wb: openpyxl.Workbook, unmatched: list[str]) -> None: ws = wb.create_sheet("unmatched_tags") ws.cell(row=1, column=1, value="base_tag").font = HEADER_FONT ws.cell(row=1, column=2, value="area").font = HEADER_FONT ws.cell(row=1, column=3, value="action").font = HEADER_FONT ws.cell(row=1, column=4, value="operator_notes").font = HEADER_FONT for i, tag in enumerate(unmatched, 2): ws.cell(row=i, column=1, value=tag) ws.cell(row=i, column=2, value="(none)") ws.cell(row=i, column=3, value="운영자가 instruments 시트에 행 추가 필요") ws.cell(row=i, column=4, value="") def _build_power_equipment_sheet(wb: openpyxl.Workbook, equipment: list[dict]) -> None: ws = wb.create_sheet("power_equipment") for col_idx, col_name in enumerate(POWER_EQUIPMENT_COLS, 1): cell = ws.cell(row=1, column=col_idx, value=col_name) cell.font = HEADER_FONT cell.fill = HEADER_FILL cell.alignment = Alignment(horizontal="center", wrap_text=True) for row_idx, eq in enumerate(equipment, 2): for col_idx, col_name in enumerate(POWER_EQUIPMENT_COLS, 1): val = eq.get(col_name, "") cell = ws.cell(row=row_idx, column=col_idx, value=val) if col_name == "confidence": if val == "low": cell.fill = LOW_FILL elif val == "medium": cell.fill = MEDIUM_FILL for col_idx in range(1, len(POWER_EQUIPMENT_COLS) + 1): max_len = 0 for row in range(1, ws.max_row + 1): val = str(ws.cell(row=row, column=col_idx).value or "") max_len = max(max_len, len(val)) ws.column_dimensions[openpyxl.utils.get_column_letter(col_idx)].width = min(max_len + 4, 40)