"""Render an enriched quote dict to 05_estimate_ko.md, 05_estimate.xlsx, data/estimate.json. Handles all kinds: effort (modules), coaching (lessons), procurement (items).""" import json import os from openpyxl import Workbook from openpyxl.styles import Font, PatternFill ROLE_KO = {"ceo": "대표", "evp": "전무", "svp": "상무", "technical_advisor": "기술고문", "director": "이사", "senior_manager": "부장", "deputy_manager": "차장", "manager": "과장", "assistant_manager": "대리", "junior": "주임", "associate": "사원", "intern": "인턴"} NAVY = "11243D" def won(n): return f"{int(round(n)):,}원" def _header_md(q): svc = q.get("label", q["service"]) if q["kind"] == "effort": svc += f" ({q['tier']})" L = [f"# 견적서 — {q['prospect'].get('name', '(prospect)')}", "", f"- **제공 서비스**: {svc}", f"- **견적번호**: {q['quote_no']} · **작성일**: {q['date']} · **유효기간**: ~{q['valid_until']}", f"- **공급자**: {q['company']['legal_name']} (대표 {q['company']['ceo']}, {q['company']['contact']})"] if q.get("stub"): L.append("- ⚠️ **STUB 카탈로그** — 실제 단가 미반영(placeholder). 실제 견적 자료로 교체 필요.") L.append("") return L def _md(q, path): L = _header_md(q) if q["kind"] == "effort": if q["scope"]["hours_multiplier"] != 1.0: dl = "브랜드/템플릿" if q["scope"]["driver"] == "subbrands_total" else "프로퍼티" L += [f"> 규모 반영: {dl} {q['scope']['driver_count']}개 기준 On-page 업무시간 ×{q['scope']['hours_multiplier']:g}", ""] L += ["## 견적 내역", "", "| 구분 | 세부 업무 | 담당 | 시간(h) | 합계 |", "|---|---|:--:|--:|--:|"] for m in q["modules"]: for i, t in enumerate(m["tasks"]): grp = m["name"] if i == 0 else "" mark = " *" if t["scaled"] else "" role_lbl = "고정" if t["role"] == "fixed" else ROLE_KO.get(t["role"], t["role"]) hrs = f"{t['hours']:g}" if t.get("hours") is not None else "—" umark = " /월" if t.get("unit") == "monthly" else "" L.append(f"| {grp} | {t['task']}{mark} | {role_lbl} | {hrs} | {won(t['amount'])}{umark} |") L.append(f"| | **{m['name']} 소계** | | | **{won(m['subtotal'])}** |") plabel = "제안가(월/retainer)" if q.get("recurring") else "제안가(절사 적용)" L += ["", f"- 합계: **{won(q['subtotal_sum'])}** · 청구율 {int(q['billing_rate']*100)}% · 일8h/월4주", f"- **{plabel}: {won(q['proposal'])}** ({q['terms']['vat']})"] if q.get("recurring"): L.append("- ※ '/월' 항목은 월 정기(retainer) 비용 — 계약 기간에 따라 합산.") elif q["kind"] == "coaching": L += ["## 견적 내역", "", "| 구분 | 세부 | 방식 | 시간 | 단가 | 합계 |", "|---|---|:--:|--:|--:|--:|"] for it in q["lessons"]: L.append(f"| {it['subject']} | {it['title']} | {it['type']} | {it['hours']:g} | {won(it['unit_price'])} | {won(it['amount'])} |") L += ["", f"- 합계: **{won(q['subtotal_sum'])}** ({q['pricing_mode']} 단가, 수강생 {q['students']}명)"] if q["discount_rate"]: L.append(f"- 할인({int(q['discount_rate']*100)}%): -{won(q['discount_amount'])}") L.append(f"- **제안가: {won(q['proposal'])}** ({q['terms']['vat']})") elif q["kind"] == "procurement": L += ["## 조달 내역", "", "| 항목 | 단가 | 수량 | 수수료 | 합계 |", "|---|--:|--:|:--:|--:|"] for it in q["items"]: L.append(f"| {it['label']} | {it['unit_cost']:,}{it['currency']} | {it['qty']:g} | {int(it['markup']*100)}% | {won(it['amount'])} |") L += ["", f"- **합계: {won(q['proposal'])}** ({q['terms']['vat']})"] L += ["", "---", f"> {q['disclaimer']}"] if q["kind"] == "effort" and any(t["scaled"] for m in q["modules"] for t in m["tasks"]): L.append("> \\* 포트폴리오 규모에 따라 업무시간이 스케일된 항목.") for n in q.get("notes", []): L.append(f"> {n}") with open(path, "w", encoding="utf-8") as fh: fh.write("\n".join(L) + "\n") def _xlsx(q, path): wb = Workbook() ws = wb.active ws.title = "견적" ws.append([f"견적서 — {q['prospect'].get('name', '(prospect)')} ({q.get('label', q['service'])})"]) ws.append([f"견적번호 {q['quote_no']}", f"작성일 {q['date']}", f"유효 ~{q['valid_until']}", q["terms"]["vat"]]) ws.append([]) def hdr(cols): ws.append(cols) for c in range(1, len(cols) + 1): cell = ws.cell(row=ws.max_row, column=c) cell.fill = PatternFill("solid", fgColor=NAVY) cell.font = Font(color="FFFFFF", bold=True) if q["kind"] == "effort": hdr(["구분", "세부 업무", "담당", "시간(h)", "합계(원)"]) for m in q["modules"]: for i, t in enumerate(m["tasks"]): role_lbl = "고정" if t["role"] == "fixed" else ROLE_KO.get(t["role"], t["role"]) hrs = t["hours"] if t.get("hours") is not None else "—" ws.append([m["name"] if i == 0 else "", t["task"], role_lbl, hrs, int(round(t["amount"]))]) ws.append(["", f"{m['name']} 소계", "", "", int(round(m["subtotal"]))]) ws.append([]) ws.append(["", "제안가(월/retainer)" if q.get("recurring") else "제안가(절사 적용)", "", "", int(q["proposal"])]) widths = [22, 40, 8, 8, 16] elif q["kind"] == "coaching": hdr(["구분", "세부", "방식", "시간", "단가", "합계(원)"]) for it in q["lessons"]: ws.append([it["subject"], it["title"], it["type"], it["hours"], int(it["unit_price"]), int(round(it["amount"]))]) ws.append([]) ws.append(["", "제안가", "", "", "", int(q["proposal"])]) widths = [18, 34, 8, 6, 12, 14] else: # procurement hdr(["항목", "단가", "수량", "수수료", "합계(원)"]) for it in q["items"]: ws.append([it["label"], it["unit_cost"], it["qty"], it["markup"], int(round(it["amount"]))]) ws.append([]) ws.append(["", "합계", "", "", int(q["proposal"])]) widths = [34, 14, 8, 10, 16] ws.cell(row=ws.max_row, column=2).font = Font(bold=True) ws.cell(row=ws.max_row, column=len(widths)).font = Font(bold=True, color="C0392B") ws.append([]) ws.append([q["disclaimer"]]) for idx, w in enumerate(widths, 1): ws.column_dimensions[chr(64 + idx)].width = w wb.save(path) def write_all(q, out_dir): os.makedirs(out_dir, exist_ok=True) ddir = os.path.join(out_dir, "data") os.makedirs(ddir, exist_ok=True) with open(os.path.join(ddir, "estimate.json"), "w", encoding="utf-8") as fh: json.dump(q, fh, ensure_ascii=False, indent=2) _md(q, os.path.join(out_dir, "05_estimate_ko.md")) _xlsx(q, os.path.join(out_dir, "05_estimate.xlsx"))