# -*- coding: utf-8 -*-
"""
킨텍스(KINTEX) AI 시스템 — PoC(개념증명) 추진계획서 PPTX 생성기
- 목차: PoC 계획보고서 표준 5단계(추진개요 / 검증목표·평가기준 / 수행방안·방법 / 추진일정·조직 / 리스크·기대효과)
- 결과보고서와 달리 "무엇을·어떻게·언제 검증할 것인가"(계획·미래시제) 관점으로 구성
- 디자인: KINTEX Blue #0066B3 / AI Purple #6D4AFF / 맑은 고딕 / 16:9 · 슬라이드 마스터 사용
재실행: python gen_poc_plan.py
"""
import os
from pptx import Presentation
from pptx.util import Emu, Pt
from pptx.dml.color import RGBColor
from pptx.enum.text import PP_ALIGN, MSO_ANCHOR
from pptx.enum.shapes import MSO_SHAPE
from pptx.oxml.ns import qn
from pptx.chart.data import CategoryChartData
from pptx.enum.chart import XL_CHART_TYPE, XL_LEGEND_POSITION, XL_LABEL_POSITION
from pptx.shapes.autoshape import Shape, AutoShapeType

BASE = os.path.dirname(os.path.abspath(__file__))
OUT  = os.path.join(os.path.dirname(BASE), "킨텍스_AI_POC계획보고서.pptx")

# ---------------- 팔레트 ----------------
BLUE      = RGBColor(0x00, 0x66, 0xB3)
BLUE_DK   = RGBColor(0x00, 0x44, 0x7A)
BLUE_LT   = RGBColor(0xE1, 0xEF, 0xF9)
PURPLE    = RGBColor(0x6D, 0x4A, 0xFF)
PURPLE_DK = RGBColor(0x4A, 0x2F, 0xCC)
PURPLE_LT = RGBColor(0xEC, 0xE8, 0xFF)
INK       = RGBColor(0x10, 0x18, 0x28)
MUTED     = RGBColor(0x66, 0x70, 0x85)
SLATE     = RGBColor(0x98, 0xA2, 0xB3)
LINE      = RGBColor(0xE4, 0xE7, 0xEC)
BG        = RGBColor(0xF9, 0xFA, 0xFB)
PANEL     = RGBColor(0xF2, 0xF4, 0xF7)
WHITE     = RGBColor(0xFF, 0xFF, 0xFF)
CARD      = WHITE
GREEN     = RGBColor(0x12, 0x9E, 0x63)
GREEN_LT  = RGBColor(0xE3, 0xF5, 0xEC)
AMBER     = RGBColor(0xE0, 0x8A, 0x00)
AMBER_LT  = RGBColor(0xFC, 0xF1, 0xDD)
RED       = RGBColor(0xD9, 0x2D, 0x20)
RED_LT    = RGBColor(0xFC, 0xE9, 0xE7)
CYAN      = RGBColor(0x0B, 0xA5, 0xEC)
TEAL      = RGBColor(0x0E, 0x93, 0x84)
NAVY      = RGBColor(0x2B, 0x4C, 0x7E)
ZEBRA     = RGBColor(0xFC, 0xFC, 0xFD)
SUBLT     = RGBColor(0xC7, 0xDD, 0xF2)
FONT = "맑은 고딕"

EMU_W = 12192000; EMU_H = 6858000
IN = 914400
def inch(v): return int(v * IN)

prs = Presentation()
prs.slide_width = EMU_W; prs.slide_height = EMU_H
BLANK = prs.slide_layouts[6]

# ================================================================
# 슬라이드 마스터 — 공통 배경·상단 액센트바·푸터
# ================================================================
def _set_fill(sp, color):
    if color is None: sp.fill.background()
    else: sp.fill.solid(); sp.fill.fore_color.rgb = color

def _no_line(sp): sp.line.fill.background()

_mid = [900]
def _mnext(): _mid[0] += 1; return _mid[0]

def _master_shape(shapes, shape_type, x, y, w, h, fill, line=None):
    ast = AutoShapeType(shape_type); id_ = _mnext()
    sp = shapes._spTree.add_autoshape(id_, '%s %d' % (ast.basename, id_), ast.prst,
                                      inch(x), inch(y), inch(w), inch(h))
    shp = Shape(sp, shapes)
    _set_fill(shp, fill)
    if line is None: _no_line(shp)
    else: shp.line.color.rgb = line; shp.line.width = Pt(1.0)
    shp.shadow.inherit = False
    return shp

def _master_text(shapes, x, y, w, h, txt, size, color, bold=False, align=PP_ALIGN.LEFT):
    id_ = _mnext()
    sp = shapes._spTree.add_textbox(id_, 'TextBox %d' % id_, inch(x), inch(y), inch(w), inch(h))
    tb = Shape(sp, shapes)
    tf = tb.text_frame; tf.word_wrap = True
    tf.margin_left = 0; tf.margin_right = 0; tf.margin_top = 0; tf.margin_bottom = 0
    p = tf.paragraphs[0]; p.alignment = align
    r = p.add_run(); r.text = txt
    r.font.name = FONT; r.font.size = Pt(size); r.font.bold = bold; r.font.color.rgb = color
    rPr = r._r.get_or_add_rPr(); rPr.append(rPr.makeelement(qn('a:ea'), {'typeface': FONT}))
    return tb

def setup_master():
    master = prs.slide_masters[0]
    shapes = master.shapes
    bg = _master_shape(shapes, MSO_SHAPE.RECTANGLE, -0.15, -0.15, 13.63, 7.8, BG)
    spTree = shapes._spTree
    spTree.remove(bg._element); spTree.insert(2, bg._element)
    _master_shape(shapes, MSO_SHAPE.RECTANGLE, -0.15, 0.0, 13.63, 0.11, BLUE)
    _master_shape(shapes, MSO_SHAPE.RECTANGLE, -0.15, 0.0, 4.4, 0.11, PURPLE)
    _master_text(shapes, 0.55, 7.02, 8.6, 0.3,
                 "킨텍스(KINTEX) AI 시스템 · PoC(개념증명) 추진계획서", 8.5, MUTED)
    _master_text(shapes, 9.4, 7.02, 3.38, 0.3, "2026-08-11 · CONFIDENTIAL", 8.5, MUTED, align=PP_ALIGN.RIGHT)

setup_master()

def slide(): return prs.slides.add_slide(BLANK)

# ================================================================
# 공통 드로잉 헬퍼
# ================================================================
def rect(s, x, y, w, h, fill=None, line=None, line_w=1.0, shape=MSO_SHAPE.RECTANGLE, shadow=False, dash=None):
    sp = s.shapes.add_shape(shape, inch(x), inch(y), inch(w), inch(h))
    _set_fill(sp, fill)
    if line is None: _no_line(sp)
    else:
        sp.line.color.rgb = line; sp.line.width = Pt(line_w)
        if dash:
            ln = sp.line._get_or_add_ln(); ln.append(ln.makeelement(qn('a:prstDash'), {'val': dash}))
    sp.shadow.inherit = False
    if shadow:
        el = sp._element.spPr
        ef = el.find(qn('a:effectLst'))
        if ef is None: ef = el.makeelement(qn('a:effectLst'), {}); el.append(ef)
        sh = ef.makeelement(qn('a:outerShdw'), {'blurRad':'90000','dist':'38100','dir':'5400000','rotWithShape':'0'})
        clr = sh.makeelement(qn('a:srgbClr'), {'val':'101828'})
        clr.append(clr.makeelement(qn('a:alpha'), {'val':'15000'}))
        sh.append(clr); ef.append(sh)
    return sp

def line_conn(s, x1, y1, x2, y2, color=SLATE, w=1.0, dash=None):
    ln = s.shapes.add_connector(2, inch(x1), inch(y1), inch(x2), inch(y2))
    ln.line.color.rgb = color; ln.line.width = Pt(w); ln.shadow.inherit = False
    if dash:
        el = ln.line._get_or_add_ln(); el.append(el.makeelement(qn('a:prstDash'), {'val': dash}))
    return ln

def text(s, x, y, w, h, content, size=11, color=INK, bold=False, align=PP_ALIGN.LEFT,
         anchor=MSO_ANCHOR.TOP, line_spacing=1.0, space_after=2, wrap=True):
    tb = s.shapes.add_textbox(inch(x), inch(y), inch(w), inch(h))
    tf = tb.text_frame; tf.word_wrap = wrap; tf.vertical_anchor = anchor
    tf.margin_left = 0; tf.margin_right = 0; tf.margin_top = 0; tf.margin_bottom = 0
    if isinstance(content, str): content = [content]
    for i, para in enumerate(content):
        p = tf.paragraphs[0] if i == 0 else tf.add_paragraph()
        p.alignment = align; p.line_spacing = line_spacing
        p.space_after = Pt(space_after); p.space_before = Pt(0)
        if isinstance(para, str): segs = [{"t": para}]
        elif isinstance(para, dict): segs = [para]
        else: segs = para
        for seg in segs:
            r = p.add_run(); r.text = seg.get("t","")
            r.font.name = FONT; r.font.size = Pt(seg.get("size", size))
            r.font.bold = seg.get("bold", bold); r.font.color.rgb = seg.get("color", color)
            rPr = r._r.get_or_add_rPr(); rPr.append(rPr.makeelement(qn('a:ea'), {'typeface':FONT}))
    return tb

def chip(s, x, y, w, h, label, fill, tc=WHITE, size=9.5, bold=True, line=None):
    rect(s, x, y, w, h, fill=fill, line=line, line_w=1.0, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
    text(s, x, y, w, h, label, size=size, color=tc, bold=bold, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)

def card(s, x, y, w, h, fill=CARD, line=LINE, shadow=True):
    return rect(s, x, y, w, h, fill=fill, line=line, line_w=1.0, shape=MSO_SHAPE.ROUNDED_RECTANGLE, shadow=shadow)

def bullets(s, x, y, w, items, dot=BLUE, size=10.5, gap=0.44, lh=1.0, dotsize=0.1, tcolor=INK):
    for i, it in enumerate(items):
        yy = y + i*gap
        rect(s, x, yy+0.07, dotsize, dotsize, fill=dot, shape=MSO_SHAPE.OVAL)
        segs = it if isinstance(it, list) else [{"t": it}]
        for sg in segs: sg.setdefault("size", size); sg.setdefault("color", tcolor)
        text(s, x+0.24, yy, w-0.24, gap, [segs], line_spacing=lh)

def lead(s, x, y, w, h, txt, fill=BLUE, tc=WHITE, size=14):
    rect(s, x, y, w, h, fill=fill, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
    text(s, x+0.25, y, w-0.4, h, txt, size=size, color=tc, bold=True, anchor=MSO_ANCHOR.MIDDLE)

def titlecard(s, x, y, w, h, title, tcolor, items, headfill=None, bodysize=9.5, hsize=12, igap=None):
    card(s, x, y, w, h)
    if headfill:
        rect(s, x, y, w, 0.46, fill=headfill, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
        text(s, x, y, w, 0.46, title, size=hsize, color=WHITE, bold=True, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)
        y0 = y+0.62
    else:
        rect(s, x, y, 0.09, h, fill=tcolor, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
        text(s, x+0.24, y+0.13, w-0.4, 0.32, title, size=hsize, color=tcolor, bold=True)
        y0 = y+0.55
    g = igap if igap else min(0.4, (h-(y0-y)-0.1)/max(1,len(items)))
    for i, it in enumerate(items):
        yy = y0 + i*g
        rect(s, x+0.22, yy+0.06, 0.09, 0.09, fill=tcolor, shape=MSO_SHAPE.OVAL)
        segs = it if isinstance(it, list) else [{"t": it}]
        for sg in segs: sg.setdefault("size", bodysize); sg.setdefault("color", INK)
        text(s, x+0.42, yy, w-0.6, g, [segs], line_spacing=0.94)

def table(s, x, y, w, cols, rows, colw, headfill=BLUE, rowh=0.4, hsize=9.5, bsize=9,
          hl_rows=None, hl_fill=BLUE_LT):
    hl_rows = hl_rows or set()
    rect(s, x, y, w, rowh, fill=headfill, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
    cx = x
    for j, c in enumerate(cols):
        text(s, cx+0.1, y, colw[j]-0.12, rowh, c, size=hsize, color=WHITE, bold=True, anchor=MSO_ANCHOR.MIDDLE,
             align=PP_ALIGN.CENTER if j>0 else PP_ALIGN.LEFT)
        cx += colw[j]
    for i, row in enumerate(rows):
        ry = y + rowh + i*rowh
        if i in hl_rows: rect(s, x, ry, w, rowh, fill=hl_fill)
        elif i % 2 == 1: rect(s, x, ry, w, rowh, fill=ZEBRA)
        rect(s, x, ry, w, 0.008, fill=LINE)
        cx = x
        for j, cell in enumerate(row):
            col = INK if j==0 else MUTED
            if isinstance(cell, tuple): txt, col = cell
            else: txt = cell
            text(s, cx+0.1, ry, colw[j]-0.12, rowh, txt, size=bsize,
                 color=(INK if i in hl_rows else col), bold=(i in hl_rows),
                 anchor=MSO_ANCHOR.MIDDLE, align=PP_ALIGN.CENTER if j>0 else PP_ALIGN.LEFT)
            cx += colw[j]

def content_header(s, kicker, title, idx):
    rect(s, 0.55, 0.42, 0.62, 0.62, fill=BLUE, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
    text(s, 0.55, 0.42, 0.62, 0.62, idx, size=(20 if len(idx)<3 else 12), color=WHITE, bold=True,
         align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)
    text(s, 1.32, 0.42, 11.3, 0.28, kicker, size=11, color=PURPLE, bold=True)
    text(s, 1.32, 0.66, 11.3, 0.5, title, size=18.5, color=INK, bold=True, line_spacing=0.96)
    rect(s, 0.55, 1.26, 12.23, 0.02, fill=LINE)

def caption(s, txt, y=6.62):
    text(s, 0.55, y, 12.23, 0.3, txt, size=9.3, color=MUTED)

def kpi(s, x, y, w, h, big, unit, label, sub, cl):
    card(s, x, y, w, h)
    rect(s, x, y, w, 0.08, fill=cl, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
    text(s, x+0.2, y+0.28, w-0.4, 0.62, [[{"t":big,"size":30,"color":cl,"bold":True},
        {"t":" "+unit,"size":12,"color":cl,"bold":True}]], anchor=MSO_ANCHOR.MIDDLE)
    text(s, x+0.2, y+0.96, w-0.4, 0.3, label, size=11, color=INK, bold=True)
    text(s, x+0.2, y+1.26, w-0.4, 0.5, sub, size=8.6, color=MUTED, line_spacing=0.95)

def timeline(s, x, y, w, steps, cl=BLUE):
    n = len(steps); seg = w/n
    rect(s, x, y+0.5, w, 0.05, fill=LINE)
    rect(s, x, y+0.5, w*0.999, 0.05, fill=cl)
    for i,(date,title,desc) in enumerate(steps):
        cx = x + seg*i + seg/2
        rect(s, cx-0.11, y+0.4, 0.22, 0.22, fill=cl, shape=MSO_SHAPE.OVAL, line=WHITE, line_w=2.0)
        text(s, cx-seg/2+0.1, y+0.0, seg-0.2, 0.32, date, size=9.5, color=cl, bold=True, align=PP_ALIGN.CENTER)
        card(s, cx-seg/2+0.14, y+0.78, seg-0.28, 1.5)
        text(s, cx-seg/2+0.24, y+0.9, seg-0.48, 0.5, title, size=10, color=INK, bold=True,
             align=PP_ALIGN.CENTER, line_spacing=0.92)
        text(s, cx-seg/2+0.24, y+1.42, seg-0.48, 0.78, desc, size=8.3, color=MUTED,
             align=PP_ALIGN.CENTER, line_spacing=0.95)

def divider(no, kr, en, points):
    s = slide()
    rect(s, -0.15, -0.15, 13.63, 7.8, fill=BLUE_DK)
    rect(s, 8.6, -1.5, 8, 11, fill=BLUE, shape=MSO_SHAPE.PARALLELOGRAM)
    rect(s, 10.9, -2, 6, 12, fill=PURPLE, shape=MSO_SHAPE.PARALLELOGRAM)
    text(s, 1.0, 1.9, 5, 2.2, no, size=150, color=RGBColor(0x2C,0x4E,0x82), bold=True)
    rect(s, 1.15, 3.78, 0.9, 0.09, fill=PURPLE)
    text(s, 1.15, 3.95, 7, 0.8, kr, size=33, color=WHITE, bold=True)
    text(s, 1.18, 4.82, 7, 0.4, en, size=13, color=RGBColor(0x9C,0xB4,0xD8), bold=True)
    for i, p in enumerate(points):
        yy = 2.4 + i*0.62
        rect(s, 7.55, yy+0.07, 0.16, 0.16, fill=PURPLE, shape=MSO_SHAPE.OVAL)
        text(s, 7.85, yy, 4.7, 0.5, p, size=12.5, color=WHITE, anchor=MSO_ANCHOR.MIDDLE)
    return s

def add_bar(s, x, y, w, h, cats, series, title=None, colors=None, ctype=XL_CHART_TYPE.COLUMN_CLUSTERED,
            maxv=None, show_val=True):
    cd = CategoryChartData(); cd.categories = cats
    for nm, vals in series: cd.add_series(nm, vals)
    gf = s.shapes.add_chart(ctype, inch(x), inch(y), inch(w), inch(h), cd)
    ch = gf.chart
    ch.has_title = bool(title)
    if title:
        ch.chart_title.text_frame.text = title
        r = ch.chart_title.text_frame.paragraphs[0].runs[0]
        r.font.size = Pt(11); r.font.bold = True; r.font.name = FONT; r.font.color.rgb = INK
    ch.has_legend = len(series) > 1
    if ch.has_legend:
        ch.legend.position = XL_LEGEND_POSITION.TOP; ch.legend.include_in_layout = False
        ch.legend.font.size = Pt(9); ch.legend.font.name = FONT
    plot = ch.plots[0]; plot.gap_width = 60
    if colors and len(series)==1:
        for i, pt in enumerate(plot.series[0].points):
            pt.format.fill.solid(); pt.format.fill.fore_color.rgb = colors[i % len(colors)]
    elif colors:
        for si, sr in enumerate(plot.series):
            sr.format.fill.solid(); sr.format.fill.fore_color.rgb = colors[si % len(colors)]
    if show_val:
        plot.has_data_labels = True
        dl = plot.data_labels; dl.font.size = Pt(8.5); dl.font.name = FONT; dl.font.bold = True
        dl.number_format = '0'; dl.number_format_is_linked = False
    try:
        ca = ch.value_axis; ca.has_major_gridlines = True
        ca.tick_labels.font.size = Pt(8.5); ca.tick_labels.font.name = FONT
        if maxv: ca.maximum_scale = maxv; ca.minimum_scale = 0
    except Exception: pass
    ch.category_axis.tick_labels.font.size = Pt(8.5); ch.category_axis.tick_labels.font.name = FONT
    return ch

def add_doughnut(s, x, y, w, h, cats, vals, colors, title=None):
    cd = CategoryChartData(); cd.categories = cats; cd.add_series('s', vals)
    gf = s.shapes.add_chart(XL_CHART_TYPE.DOUGHNUT, inch(x), inch(y), inch(w), inch(h), cd)
    ch = gf.chart
    ch.has_title = bool(title)
    if title:
        ch.chart_title.text_frame.text = title
        r = ch.chart_title.text_frame.paragraphs[0].runs[0]
        r.font.size = Pt(10.5); r.font.bold = True; r.font.name = FONT; r.font.color.rgb = INK
    ch.has_legend = True; ch.legend.position = XL_LEGEND_POSITION.RIGHT
    ch.legend.include_in_layout = False; ch.legend.font.size = Pt(9); ch.legend.font.name = FONT
    pts = ch.plots[0].series[0].points
    for i, pt in enumerate(pts):
        pt.format.fill.solid(); pt.format.fill.fore_color.rgb = colors[i % len(colors)]
    return ch

# ================================================================
# S01 · 표지
# ================================================================
def cover():
    s = slide()
    rect(s, -0.15, -0.15, 13.63, 7.8, fill=BLUE_DK)
    rect(s, 6.4, -1.5, 9, 11, fill=BLUE, shape=MSO_SHAPE.PARALLELOGRAM)
    rect(s, 9.2, -2.0, 7, 12, fill=PURPLE, shape=MSO_SHAPE.PARALLELOGRAM)
    rect(s, 0.85, 0.8, 0.42, 0.42, fill=WHITE, shape=MSO_SHAPE.OVAL)
    rect(s, 0.97, 0.92, 0.18, 0.18, fill=PURPLE, shape=MSO_SHAPE.OVAL)
    text(s, 1.42, 0.78, 6, 0.46, "KINTEX · AI EXHIBITION SYSTEM", size=14, color=WHITE, bold=True, anchor=MSO_ANCHOR.MIDDLE)
    chip(s, 0.85, 2.15, 2.3, 0.44, "PoC 추진계획서", PURPLE, WHITE, size=13)
    chip(s, 3.27, 2.15, 2.9, 0.44, "Proof of Concept Plan", NAVY, WHITE, size=10.5)
    text(s, 0.85, 2.78, 11.5, 1.9, [
        [{"t":"킨텍스 AI 전시관리 시스템","size":42,"color":WHITE,"bold":True}],
        [{"t":"개념증명(PoC) 추진계획서","size":42,"color":WHITE,"bold":True}],
    ], line_spacing=1.03)
    rect(s, 0.85, 4.62, 11.5, 0.66, fill=RGBColor(0x14,0x35,0x63), shape=MSO_SHAPE.ROUNDED_RECTANGLE)
    rect(s, 0.85, 4.62, 0.1, 0.66, fill=PURPLE, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
    text(s, 1.15, 4.62, 11.0, 0.66, "무엇을·어떻게·언제 검증할 것인가 — 실증으로 도입 타당성을 증명하기 위한 PoC 실행 계획.",
         size=14.5, color=WHITE, bold=True, anchor=MSO_ANCHOR.MIDDLE)
    rect(s, 0.85, 5.95, 11.63, 0.02, fill=RGBColor(0x3A,0x5A,0x8C))
    text(s, 0.85, 6.15, 8.8, 0.4, [[{"t":"검증 대상  ","size":10.5,"color":RGBColor(0x90,0xA6,0xC9)},
        {"t":"부스 배치·설계·배선 자동화 + 나노바나나 시공 예측 + 공사 옥션 폐루프","size":10.5,"color":WHITE,"bold":True}]])
    text(s, 0.85, 6.52, 9.4, 0.4, [[{"t":"기술스택  ","size":10.5,"color":RGBColor(0x90,0xA6,0xC9)},
        {"t":"React(Vite)+TS · Spring Boot 3.x(Java17)+MyBatis · PostgreSQL(PostGIS) · Redis · 나노바나나 워커 · Claude AI","size":9,"color":WHITE}]])
    text(s, 10.0, 6.15, 2.5, 0.4, "작성일 2026-08-11", size=11, color=WHITE, bold=True, align=PP_ALIGN.RIGHT)
    text(s, 10.0, 6.52, 2.5, 0.4, "수행 · GUARDiA/WISE 팀", size=9, color=RGBColor(0x90,0xA6,0xC9), align=PP_ALIGN.RIGHT)

# ================================================================
# S02 · 목차
# ================================================================
def toc():
    s = slide(); content_header(s, "CONTENTS", "목차 — PoC 추진계획서 표준 5단계", "―")
    items=[("1","추진 개요","배경·목적 / 검증 대상 솔루션·기술 / 검증 범위·대상",BLUE),
           ("2","검증 목표 및 평가 기준","핵심 검증 항목(KPI) / 성공·실패 기준 / 테스트 환경·아키텍처",PURPLE),
           ("3","PoC 수행 방안 및 방법","기능·성능 시나리오 / 데이터 준비·적용범위 / 시각화 검증 방법",TEAL),
           ("4","추진 일정 및 수행 조직","WBS·마일스톤 / 수행조직·R&R / 산출물·형상관리",BLUE),
           ("5","리스크 관리 및 기대효과","리스크·대응 / 보안·품질 관리 / 기대효과·본사업 전환",PURPLE),
           ("부록","APPENDIX","기술스택 상세 · 게이트 정의 · 체크리스트",NAVY)]
    for i,(no,t,d,cl) in enumerate(items):
        col=i//3; row=i%3
        x=0.6+col*6.23; y=1.5+row*1.68
        rect(s, x, y, 5.95, 1.5, fill=CARD, line=LINE, shape=MSO_SHAPE.ROUNDED_RECTANGLE, shadow=True)
        rect(s, x, y, 0.1, 1.5, fill=cl, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
        text(s, x+0.3, y, 1.4, 1.5, no, size=(30 if no!="부록" else 15), color=cl, bold=True,
             align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)
        text(s, x+1.75, y+0.3, 4.0, 0.5, t, size=15, color=INK, bold=True)
        text(s, x+1.75, y+0.82, 4.0, 0.6, d, size=9.3, color=MUTED, line_spacing=1.0)
    caption(s, "본 계획서는 PoC 표준 목차(추진개요·검증목표·수행방안·일정조직·리스크)에 킨텍스 자동전시시스템 실증 계획을 정렬하여 구성.", y=6.7)

# ================================================================
# S03 · Executive Summary
# ================================================================
def exec_summary():
    s = slide(); content_header(s, "EXECUTIVE SUMMARY", "핵심 요약 — 14일 집중 PoC로 자동전시 파이프라인의 실현 가능성을 실증한다", "요약")
    kpi(s, 0.55, 1.45, 2.95, 1.85, "3", "대 핵심가설", "검증 초점 3결합", "나노바나나 시공예측 · PostGIS 공간데이터 · 공사 옥션 폐루프", PURPLE)
    kpi(s, 3.63, 1.45, 2.95, 1.85, "14", "일 집중검증", "2026-07-11~07-24", "기획·아키텍처 → 코어 → AI/시각화 → 시연·QA, 6세션 구성", BLUE)
    kpi(s, 6.71, 1.45, 2.95, 1.85, "8", "개 KPI", "정량 성공 지표", "모듈·화면·시각화·배치·옥션·데이터·배포·모바일 목표 정의", TEAL)
    kpi(s, 9.79, 1.45, 2.99, 1.85, "6", "역할 포털", "웹·모바일 분리 검증", "주최자·참가사·공사사·관람객·관리자·운영, RBAC(JWT)+2FA", GREEN)
    card(s, 0.55, 3.55, 7.55, 2.75)
    rect(s, 0.55, 3.55, 7.55, 0.46, fill=INK, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
    text(s, 0.55, 3.55, 7.55, 0.46, "PoC 추진 방향", size=12.5, color=WHITE, bold=True, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)
    bullets(s, 0.78, 4.18, 7.1, [
        [{"t":"검증 가설 — ","bold":True,"color":PURPLE},{"t":"“신청하는 순간 시공 후 사진을 먼저 본다” — 설계·시각화·발주·운영·분석을 한 흐름으로 자동화 가능한가."}],
        [{"t":"실증 방식 — ","bold":True,"color":BLUE},{"t":"개발서버(kintex.zioinfo.co.kr:8021)에서 E2E 라이브로 기능·성능·데이터 파이프라인을 실측 검증."}],
        [{"t":"평가 기준 — ","bold":True,"color":TEAL},{"t":"KPI 8종을 3등급(성공/조건부/실패)으로 판정, health 게이트·보안 불변을 통과 조건화."}],
        [{"t":"기반 재사용 — ","bold":True,"color":GREEN},{"t":"WISE 표준 프레임워크를 재사용하여 착수 공수를 절감하고 본사업 전환 타당성을 확인."}],
    ], gap=0.56, size=9.6, lh=0.98)
    titlecard(s, 8.28, 3.55, 4.5, 2.75, "성공의 정의", GREEN, [
        [{"t":"기능 검증 ","bold":True},{"t":"— 핵심 3결합 E2E 성립","color":GREEN,"bold":True}],
        [{"t":"성능/안정성 ","bold":True},{"t":"— 개발서버 기준 충족","color":GREEN,"bold":True}],
        [{"t":"데이터 파이프라인 ","bold":True},{"t":"— 크롤·시드 폐루프 구동","color":GREEN,"bold":True}],
        [{"t":"운영 성숙도 ","bold":True},{"t":"— 본사업 전환 시 보완","color":AMBER,"bold":True}],
        "→ KPI 8종 달성 + 게이트 통과 시 '본사업 전환 권고'로 결론",
    ], headfill=GREEN, bodysize=9.3, igap=0.5)
    caption(s, "상세 목표는 §2 검증 목표·평가 기준, 실행 방법은 §3 수행 방안, 일정은 §4 추진 일정을 참조.", y=6.62)

# ================================================================
# Ⅰ. 추진 개요
# ================================================================
def sec1_divider():
    divider("1", "추진 개요", "PROJECT OVERVIEW",
            ["PoC 추진 배경 및 목적", "검증 대상 솔루션·기술 개요", "검증 범위 및 대상(Scope)"])

def s_background():
    s = slide(); content_header(s, "1. 추진 개요", "1-1. PoC 추진 배경 및 목적", "1")
    probs = [("문제 ①  설계-발주 단절","주최·참가사가 도면을 읽기 어려워 시공 결과를 사전에 확신하지 못함 — 재작업·분쟁 발생", RED),
             ("문제 ②  공간데이터 분산","부스 배치·트렌치·배선 정보가 도구별로 흩어져 검증·정산·분석이 수작업", AMBER),
             ("문제 ③  공사 발주 비효율","AI 설계자료와 응찰·낙찰·발주가 단절 — 견적 비교·계약 전환에 시간 소요", PURPLE)]
    for i,(t,d,cl) in enumerate(probs):
        x=0.55+i*4.12
        card(s, x, 1.45, 3.95, 1.7)
        rect(s, x, 1.45, 3.95, 0.08, fill=cl, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
        text(s, x+0.24, 1.66, 3.5, 0.4, t, size=11.5, color=cl, bold=True)
        text(s, x+0.24, 2.12, 3.5, 0.9, d, size=9.5, color=INK, line_spacing=1.05)
    lead(s, 0.55, 3.4, 12.23, 0.6, "PoC 목적  —  “신청서를 내는 순간, 시공 후 사진을 먼저 본다”: 설계·시각화·발주·운영·분석을 한 흐름으로 자동화할 수 있음을 실증한다.", fill=BLUE, size=13)
    titlecard(s, 0.55, 4.2, 6.0, 2.1, "추진 목적(검증 가설)", BLUE, [
        "AI가 부스 배치·설계·배선을 자동 생성하고, 나노바나나가 시공 후 사진을 예측할 수 있는가",
        "PostGIS 단일 공간원천으로 검증·정산·BI가 일관되게 도는가",
        "AI 설계자료→응찰→낙찰→발주 폐루프가 성립하는가",
    ], headfill=BLUE, bodysize=9.6, igap=0.55)
    titlecard(s, 6.73, 4.2, 6.05, 2.1, "기대 효과(가설이 참이면)", PURPLE, [
        [{"t":"의사결정 가속 ","bold":True},{"t":"— 도면 없이 사진으로 즉시 컨펌"}],
        [{"t":"데이터 일관성 ","bold":True},{"t":"— 공간 단일원천으로 재작업·오류 감소"}],
        [{"t":"발주 자동화 ","bold":True},{"t":"— 옥션 폐루프로 계약 리드타임 단축"}],
    ], headfill=PURPLE, bodysize=9.6, igap=0.55)
    caption(s, "배경·목적은 docs/PLANNING.md 및 OWNER_FEEDBACK.md(소유자 지시 로그)를 근거로 정리.")

def s_solution():
    s = slide(); content_header(s, "1. 추진 개요", "1-2. 검증 대상 솔루션 및 기술 개요", "1")
    text(s, 0.55, 1.4, 12.2, 0.3, "검증 대상 = 킨텍스 자동전시시스템의 핵심 3결합(해자) + 이를 떠받치는 확정 기술스택", size=11, color=INK, bold=True)
    assets = [("나노바나나 시공 예측","도면을 못 읽어도 신청 시점에 '시공 후 사진'으로 의사결정 (image-to-image·구조보존)", PURPLE),
              ("PostGIS 실측 공간데이터","부스 폴리곤·트렌치 포인트·배선 LineString을 단일 원천에서 검증·정산·BI", BLUE),
              ("공사 옥션 폐루프","AI 설계자료 → 응찰 → 낙찰 → 발주 자동전환, 한 흐름에서 종결", TEAL)]
    for i,(t,d,cl) in enumerate(assets):
        x=0.55+i*4.12
        card(s, x, 1.78, 3.95, 1.6)
        rect(s, x, 1.78, 0.09, 1.6, fill=cl, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
        text(s, x+0.28, 1.94, 3.5, 0.4, t, size=12, color=cl, bold=True)
        text(s, x+0.28, 2.42, 3.55, 0.9, d, size=9.4, color=INK, line_spacing=1.05)
    stack = [("프론트엔드","React 18/19 + Vite + TypeScript","반응형 — 데스크톱=설계/에디터, 모바일=조회·승인·현장"),
             ("백엔드","Spring Boot 3.x (Java 17) + MyBatis","REST + WebSocket(STOMP), 룰 엔진·배치/배선 엔진"),
             ("데이터베이스","PostgreSQL + PostGIS","부스 폴리곤·트렌치 포인트·배선 LineString 공간 데이터"),
             ("비동기","Redis 작업 큐","RenderJob·서류·알림 비동기 처리"),
             ("이미지 생성","나노바나나 Python 워커(google-genai)","gemini-3.1-flash-image-preview, 오브젝트 스토리지+WS 완료 푸시"),
             ("AI","Claude API 기본 + 설정형 전환","AiTextRouter/AiConfig — 조건해석·검수·추천·조회·번역"),
             ("인증·보안","JWT + 2FA(TOTP RFC6238) · SSO(Keycloak OIDC)","6역할 RBAC·테넌트 격리, PostGIS 공간검증")]
    table(s, 0.55, 3.6, 12.23, ["레이어","기술","검증 관점"], stack, [2.2, 4.7, 5.33], rowh=0.4, hsize=9.5, bsize=8.5)
    caption(s, "확정 스택(2026-07-11, 소유자 지정) — GUARDiA 표준 프레임워크(Spring Boot 3.5 + React 19 + MyBatis + PostgreSQL)와 정렬.", y=6.68)

def s_scope():
    s = slide(); content_header(s, "1. 추진 개요", "1-3. 검증 범위 및 대상 (Scope)", "1")
    # In-scope 카드
    card(s, 0.55, 1.45, 6.05, 3.55)
    rect(s, 0.55, 1.45, 6.05, 0.46, fill=GREEN, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
    text(s, 0.55, 1.45, 6.05, 0.46, "검증 범위 (In-Scope)", size=12, color=WHITE, bold=True, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)
    bullets(s, 0.78, 2.08, 5.6, [
        [{"t":"부스 코어(M2~M5) ","bold":True},{"t":"— 배치·설계·배선·시각화 자동화"}],
        [{"t":"공사 옥션 폐루프 ","bold":True},{"t":"— 응찰→낙찰→발주 전환"}],
        [{"t":"6역할 포털 ","bold":True},{"t":"— 주최자·참가사·공사사·관람객·관리자·운영"}],
        [{"t":"공통·시스템관리 ","bold":True},{"t":"— WISE 이식(인증 2FA·코드·메뉴·감사)"}],
        [{"t":"데이터 파이프라인 ","bold":True},{"t":"— 크롤 적재 + 데모 시드 폐루프"}],
        [{"t":"배포 자동화 ","bold":True},{"t":"— CI/CD + health 게이트 실증"}],
    ], gap=0.48, size=9.5, dot=GREEN)
    # Out-of-scope 카드
    card(s, 6.73, 1.45, 6.05, 3.55)
    rect(s, 6.73, 1.45, 6.05, 0.46, fill=SLATE, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
    text(s, 6.73, 1.45, 6.05, 0.46, "검증 제외 (Out-of-Scope · 본사업 이관)", size=12, color=WHITE, bold=True, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)
    bullets(s, 6.96, 2.08, 5.6, [
        [{"t":"운영 HA/이중화 ","bold":True},{"t":"— 개발서버 단일 노드 범위"}],
        [{"t":"대규모 부하테스트 ","bold":True},{"t":"— 성능 SLA 튜닝은 본사업"}],
        [{"t":"외부 연동 실계약 ","bold":True},{"t":"— PG 정산·전자계약 실서비스"}],
        [{"t":"보안 인증·취약점 진단 ","bold":True},{"t":"— 정식 모의해킹 별도"}],
        [{"t":"스토어 정식 배포 ","bold":True},{"t":"— 모바일은 EAS 내부빌드까지"}],
        [{"t":"실 운영 데이터 이행 ","bold":True},{"t":"— 마이그레이션은 본사업"}],
    ], gap=0.48, size=9.5, dot=SLATE)
    lead(s, 0.55, 5.25, 12.23, 0.55, "대상 환경  —  개발서버 kintex.zioinfo.co.kr:8021 (PostGIS+Redis+Flyway) · 운영 kintex.wise.ai.kr 은 본사업 전환 후 구성", fill=NAVY, size=12)
    caption(s, "PoC는 '실현 가능성 실증'에 집중 — 비기능(HA·부하·보안점검)은 조건부 항목으로 본사업 전환 전제.", y=6.7)

# ================================================================
# Ⅱ. 검증 목표 및 평가 기준
# ================================================================
def sec2_divider():
    divider("2", "검증 목표 및 평가 기준", "GOALS & CRITERIA",
            ["핵심 검증 항목(KPI·요구사항)", "성공/실패 판단 기준", "테스트 환경 및 구성(Architecture)"])

def s_kpi():
    s = slide(); content_header(s, "2. 검증 목표 및 평가 기준", "2-1. 핵심 검증 항목 (KPI 및 목표치)", "2")
    rows = [("KPI-01","모듈 구현","부스 코어 M2~M5 100% + 도메인","코어 완결 · 도메인 모듈 실동작", ("정량",BLUE)),
            ("KPI-02","화면 구현","설계 89화면 + 트랙 셸","전 화면 라우팅·데이터 배선", ("정량",BLUE)),
            ("KPI-03","나노바나나 시각화","시공 예측 E2E 라이브","렌더 완료·구조보존·워터마크(목표 ≤60초)", ("정량",PURPLE)),
            ("KPI-04","AI 자동배치·규정","자연어 해석·규정 플래깅","3안 생성 + 규정(5m·방염·리깅) 자동 검출", ("정량",PURPLE)),
            ("KPI-05","공사 옥션 폐루프","응찰→낙찰→발주 전이","역경매 스코어링·상태 무결 전이", ("정량",TEAL)),
            ("KPI-06","데이터 시드·크롤","실측 데이터 반영","다수 테이블 시드 + 포스터·행사 크롤 적재", ("정량",TEAL)),
            ("KPI-07","배포 자동화","CI/CD health 게이트","무중단 배포 + 공개 API 스모크 통과", ("정량",GREEN)),
            ("KPI-08","모바일 앱 빌드","EAS APK 산출","현장·관람 앱 내부빌드 QR 배포", ("정량",GREEN))]
    table(s, 0.55, 1.45, 12.23, ["ID","검증 항목","검증 목표","성공 판정 기준(목표치)","구분"],
          rows, [1.1, 2.5, 2.6, 4.73, 1.3], rowh=0.53, hsize=9.5, bsize=8.6)
    caption(s, "핵심 검증 항목은 PLANNING §8 아키텍처·design.md·백로그에서 도출. 목표치는 개발서버(kintex.zioinfo.co.kr:8021) 기준.", y=6.72)

def s_criteria():
    s = slide(); content_header(s, "2. 검증 목표 및 평가 기준", "2-2. 성공/실패 판단 기준 정의", "2")
    grades = [("성공 (Pass)","목표치 100% 달성 · 라이브 실증", GREEN, GREEN_LT),
              ("조건부 (Conditional)","핵심 달성 · 운영/비기능 일부 보완 필요", AMBER, AMBER_LT),
              ("실패 (Fail)","핵심 가설 미검증 · 재설계 필요", RED, RED_LT)]
    for i,(t,d,cl,bg) in enumerate(grades):
        x=0.55+i*4.12
        rect(s, x, 1.45, 3.95, 1.15, fill=bg, line=cl, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
        rect(s, x, 1.45, 0.09, 1.15, fill=cl, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
        text(s, x+0.28, 1.62, 3.5, 0.4, t, size=12.5, color=cl, bold=True)
        text(s, x+0.28, 2.08, 3.55, 0.5, d, size=9.4, color=INK, line_spacing=1.02)
    rows = [("배치 자동생성","규정(높이 5m·방염·리깅 6.5~8.5m) 위반 자동 플래깅","솔버 미수렴·규정 위반 미검출"),
            ("나노바나나 예측","렌더 완료·구조보존·워터마크 강제 부착","타임아웃 / 원본 구조 붕괴 / 워터마크 누락"),
            ("공간 정합","ST_Area/ST_Intersects/ST_Length 결과 일치","공간 연산 오차·좌표계 불일치"),
            ("옥션 폐루프","응찰→낙찰→발주 상태 무결 전이","상태 전이 누락·데이터 불일치"),
            ("보안 불변","자격증명·PII·스택트레이스 미노출·2FA 통과","민감정보 노출 / 권한 우회 발생"),
            ("배포 게이트","health 200 + 공개 API 스모크 통과","health 실패 / 스모크 미달 / 롤백 미작동")]
    table(s, 0.55, 2.85, 12.23, ["검증 항목","성공 기준 (Pass)","실패 기준 (Fail)"],
          rows, [2.6, 5.2, 4.43], rowh=0.44, hsize=9.5, bsize=8.8, headfill=INK)
    caption(s, "판정은 3등급(성공/조건부/실패). 비기능(HA·부하·보안점검)은 개발서버 PoC 범위 밖 → 조건부 항목으로 분류.", y=6.66)

def s_architecture():
    s = slide(); content_header(s, "2. 검증 목표 및 평가 기준", "2-3. 테스트 환경 및 구성 (Architecture)", "2")
    def box(x,y,w,h,t,sub,fill,tc,ln=None):
        rect(s,x,y,w,h,fill=fill,line=(ln or fill),line_w=1.2,shape=MSO_SHAPE.ROUNDED_RECTANGLE,shadow=True)
        text(s,x,y+0.1,w,0.3,t,size=10,color=tc,bold=True,align=PP_ALIGN.CENTER)
        text(s,x,y+0.42,w,h-0.46,sub,size=7.8,color=(WHITE if fill!=WHITE else MUTED),align=PP_ALIGN.CENTER,line_spacing=0.9)
    rect(s,0.55,1.45,12.23,1.0,fill=BLUE_LT,line=BLUE,shape=MSO_SHAPE.ROUNDED_RECTANGLE)
    text(s,0.7,1.5,2.4,0.3,"클라이언트 계층",size=9.5,color=BLUE_DK,bold=True)
    for i,(t,sub) in enumerate([("주최자 웹","설계·운영"),("참가사 웹","신청·승인"),("공사사 포털","응찰·발주"),("관람객 웹/앱","등록·현장"),("관리자","백오피스"),("모바일","Expo/EAS")]):
        box(0.75+i*1.99,1.82,1.82,0.52,t,sub,WHITE,INK,BLUE)
    rect(s,0.55,2.62,12.23,1.55,fill=PURPLE_LT,line=PURPLE,shape=MSO_SHAPE.ROUNDED_RECTANGLE)
    text(s,0.7,2.67,6.0,0.3,"애플리케이션 계층 (Spring Boot 3.x · Java 17 · MyBatis)",size=9.5,color=PURPLE_DK,bold=True)
    for i,(t,sub) in enumerate([("REST API","6포털 서비스"),("WebSocket(STOMP)","렌더·옥션 실시간"),("룰 엔진","규정·요율"),("배치/배선 엔진","PostGIS 연산"),("AiTextRouter","Claude 기본")]):
        box(0.75+i*2.4,3.0,2.25,0.98,t,sub,WHITE,INK,PURPLE)
    rect(s,0.55,4.32,12.23,1.55,fill=RGBColor(0xEC,0xF6,0xF3),line=TEAL,shape=MSO_SHAPE.ROUNDED_RECTANGLE)
    text(s,0.7,4.37,6.0,0.3,"데이터 · 워커 · 인프라 계층",size=9.5,color=TEAL,bold=True)
    for i,(t,sub,cl) in enumerate([("PostgreSQL+PostGIS","공간 단일원천",BLUE),("Redis","작업 큐",RED),("나노바나나 워커","Gemini 이미지",PURPLE),("오브젝트 스토리지","렌더 결과",NAVY),("Gitea CI/CD","자동배포·health",GREEN)]):
        box(0.75+i*2.4,4.72,2.25,0.98,t,sub,WHITE,INK,cl)
    lead(s, 0.55, 6.02, 12.23, 0.5, "PoC 서버  —  kintex.zioinfo.co.kr:8021  (개발) · Flyway 마이그레이션 라이브 dry-run(BEGIN…ROLLBACK) 후 반영 · 시크릿 fail-fast 프로파일", fill=NAVY, size=11.5)

# ================================================================
# Ⅲ. PoC 수행 방안 및 방법
# ================================================================
def sec3_divider():
    divider("3", "PoC 수행 방안 및 방법", "EXECUTION METHOD",
            ["기능·성능 검증 시나리오", "데이터 준비 및 적용 범위", "나노바나나 시각화 검증 방법"])

def s_scenario():
    s = slide(); content_header(s, "3. PoC 수행 방안 및 방법", "3-1. 기능·성능 검증 시나리오", "3")
    text(s, 0.55, 1.4, 12.2, 0.3, "핵심 사용자 여정을 E2E 시나리오로 정의하고, 각 단계의 통과 조건을 사전 명세 — 개발서버에서 라이브 실행", size=10.5, color=INK, bold=True)
    steps = [("S-1  신청·설계","참가사 신청 → AI 부스 배치 3안 생성 → 규정 자동 플래깅 → 배선/물량 산출", BLUE),
             ("S-2  시각화","확정 배치 → 나노바나나 시공 후 사진 렌더 → Before/After 비교 → 워터마크 확인", PURPLE),
             ("S-3  발주 옥션","AI 설계자료 공개 → 공사사 응찰 → 역경매 스코어링 → 낙찰·발주 전환", TEAL),
             ("S-4  운영·분석","현장 체크인·리드 캡처 → 정산 집계 → BI 대시보드 → KPI 리포트", GREEN)]
    for i,(t,d,cl) in enumerate(steps):
        y=1.85+i*0.82
        rect(s, 0.55, y, 0.5, 0.7, fill=cl, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
        text(s, 0.55, y, 0.5, 0.7, "S%d"%(i+1), size=13, color=WHITE, bold=True, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)
        card(s, 1.2, y, 11.58, 0.7)
        text(s, 1.42, y+0.09, 3.0, 0.5, t, size=11, color=cl, bold=True, anchor=MSO_ANCHOR.MIDDLE)
        text(s, 4.4, y+0.09, 8.2, 0.5, d, size=9.4, color=INK, anchor=MSO_ANCHOR.MIDDLE, line_spacing=0.95)
    # 성능 관점
    titlecard(s, 0.55, 5.28, 6.05, 1.1, "성능 검증 관점", NAVY, [
        [{"t":"렌더 응답 ","bold":True},{"t":"— 나노바나나 렌더 목표 ≤60초, WS 완료 푸시"}],
        [{"t":"공간 연산 ","bold":True},{"t":"— PostGIS ST_* 쿼리 정확·응답성"}],
    ], headfill=NAVY, bodysize=9.2, igap=0.32)
    titlecard(s, 6.73, 5.28, 6.05, 1.1, "검증 방법론", BLUE, [
        [{"t":"점진 QA ","bold":True},{"t":"— 각 모듈 완성 직후 경계면(shape) 교차검증"}],
        [{"t":"게이트 ","bold":True},{"t":"— Phase별 QA PASS 후 다음 단계 진행"}],
    ], headfill=BLUE, bodysize=9.2, igap=0.32)
    caption(s, "시나리오·통과조건은 design.md 화면흐름 + 백로그 인수기준에 정렬. 실패 시 원인 분석 → 보완 → 재검증 루프.", y=6.5)

def s_data():
    s = slide(); content_header(s, "3. PoC 수행 방안 및 방법", "3-2. 데이터 준비 및 적용 범위", "3")
    text(s, 0.55, 1.4, 12.2, 0.3, "실감형 실증을 위해 실측 자산 크롤 + 멱등 데모 시드(Flyway)로 전 모듈 데이터를 사전 구축", size=10.5, color=INK, bold=True)
    # 데이터 소스 3분류
    cats = [("실측 자산 (크롤)","kintex.com 행사·포스터·관람안내·교통 정보를 크롤 → 멱등 시드로 적재", PURPLE, PURPLE_LT),
            ("도면·공간 데이터","제1전시장 평면도 JPG 15장 + CAD(DWG, 트렌치 포함) → PostGIS 폴리곤·포인트·라인", BLUE, BLUE_LT),
            ("데모 트랜잭션 시드","행사·부스·참가사·옥션·리드·정산·결재·공지·알림을 폐루프로 연결", TEAL, RGBColor(0xE9,0xF6,0xF4))]
    for i,(t,d,cl,bg) in enumerate(cats):
        x=0.55+i*4.12
        rect(s, x, 1.82, 3.95, 1.5, fill=bg, line=cl, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
        rect(s, x, 1.82, 0.09, 1.5, fill=cl, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
        text(s, x+0.28, 1.98, 3.55, 0.4, t, size=11, color=cl, bold=True)
        text(s, x+0.28, 2.42, 3.55, 0.85, d, size=9.2, color=INK, line_spacing=1.05)
    # 데이터 준비 원칙 표
    rows = [("멱등성","Flyway 마이그레이션으로 재실행 안전 — 라이브 dry-run(BEGIN…ROLLBACK) 후 반영"),
            ("행사 스코프 정합","데모 해소행사에 시드를 묶어 '화면이 비어 보이는' 오탐 방지"),
            ("데이터 표준","공통코드·view/mview·배치 집계 — FK 최소화로 이식성 확보"),
            ("PII·보안","테스트 데이터는 가명 처리, 민감정보 미노출 불변 준수"),
            ("공간 검증","좌표계(SRID) 일관 · ST_Area/ST_Length로 물량·정산 교차검증")]
    table(s, 0.55, 3.55, 12.23, ["준비 원칙","내용"], rows, [2.6, 9.63], rowh=0.46, hsize=9.5, bsize=9, headfill=NAVY)
    caption(s, "확보 자산: docs/assets/floorplans/ 홀별 평면도 15장 + CAD. 데이터 표준은 data.md, 크롤은 kintex-crawler 트랙.", y=6.5)

def s_nanobanana():
    s = slide(); content_header(s, "3. PoC 수행 방안 및 방법", "3-3. 나노바나나 시각화 검증 방법", "3")
    text(s, 0.55, 1.4, 12.2, 0.3, "PoC 최대 차별점 — '시공 후 사진 예측'을 image-to-image 파이프라인으로 검증한다", size=11, color=INK, bold=True)
    # 파이프라인 단계
    flow = [("설계 입력","배치·부스 도면",BLUE),("프롬프트 생성","보존/교체 규칙",PURPLE),("Gemini 렌더","워커 큐 처리",TEAL),
            ("구조 검증","원본 보존 확인",NAVY),("워터마크·저장","오브젝트 스토리지",GREEN)]
    n=len(flow); seg=12.23/n
    for i,(t,d,cl) in enumerate(flow):
        x=0.55+i*seg
        card(s, x+0.06, 1.85, seg-0.4, 1.1)
        rect(s, x+0.06, 1.85, seg-0.4, 0.08, fill=cl, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
        text(s, x+0.06, 2.02, seg-0.4, 0.4, t, size=10.5, color=cl, bold=True, align=PP_ALIGN.CENTER)
        text(s, x+0.06, 2.44, seg-0.4, 0.45, d, size=8.6, color=MUTED, align=PP_ALIGN.CENTER, line_spacing=0.95)
        if i<n-1:
            text(s, x+seg-0.36, 1.85, 0.34, 1.1, "▶", size=13, color=SLATE, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)
    titlecard(s, 0.55, 3.25, 6.05, 3.0, "검증 항목 (Acceptance)", PURPLE, [
        [{"t":"라이브 렌더 ","bold":True},{"t":"— Gemini 유료전환·실제 이미지 생성(모의 아님)"}],
        [{"t":"구조 보존 ","bold":True},{"t":"— 부스 형태·배치 유지, 마감/조명만 변화"}],
        [{"t":"응답 시간 ","bold":True},{"t":"— 렌더 완료 목표 ≤60초, WS 완료 푸시 수신"}],
        [{"t":"워터마크 강제 ","bold":True},{"t":"— AI 생성 이미지 식별 표식 필수 부착"}],
        [{"t":"Before/After ","bold":True},{"t":"— 슬라이더 비교 UI로 시각 검수"}],
    ], headfill=PURPLE, bodysize=9.4, igap=0.46)
    titlecard(s, 6.73, 3.25, 6.05, 3.0, "검증 방법·안전장치", NAVY, [
        [{"t":"참조 방식 ","bold":True},{"t":"— ReRoomAI image-to-image 보존/교체 프롬프트 차용"}],
        [{"t":"모델 ","bold":True},{"t":"— gemini-3.1-flash-image-preview"}],
        [{"t":"키 관리 ","bold":True},{"t":"— GEMINI_API_KEY 환경변수·마스킹·미커밋"}],
        [{"t":"라이브 게이트 ","bold":True},{"t":"— NANOBANANA_LIVE=1 로 실호출 활성"}],
        [{"t":"실패 대응 ","bold":True},{"t":"— 타임아웃·재시도, 큐 기반 비동기 격리"}],
    ], headfill=NAVY, bodysize=9.4, igap=0.46)
    caption(s, "호출은 tools/nanobanana 모듈만 경유(규칙). 선행 게이트 G1(나노바나나 라이브)이 착수 전제.", y=6.5)

# ================================================================
# Ⅳ. 추진 일정 및 수행 조직
# ================================================================
def sec4_divider():
    divider("4", "추진 일정 및 수행 조직", "SCHEDULE & TEAM",
            ["추진 일정 (WBS · 마일스톤)", "수행 조직 및 R&R", "산출물 및 형상관리 계획"])

def s_schedule():
    s = slide(); content_header(s, "4. 추진 일정 및 수행 조직", "4-1. 추진 일정 (WBS · 마일스톤)", "4")
    text(s, 0.55, 1.38, 12.2, 0.3, "PoC 집중 기간 2026-07-11 ~ 07-24 (14일·6세션) · 결과보고 08-11 (시간 진행 순)", size=11, color=INK, bold=True)
    timeline(s, 0.55, 1.72, 12.23, [
        ("07-11","기획·아키텍처","스택 확정, 하네스(전문 에이전트 12팀) 구성, PLANNING·design·백로그"),
        ("07-12~13","공통·시드·UI","대량 시드, 공개사이트·AI 관람도우미, Nifty 정렬, 신규 도메인"),
        ("07-14~15","AI·나노바나나 E2E","자연어 배치 해석, ReRoom 스타일, Gemini 유료전환, DWG·트렌치 캘리브"),
        ("07-23~24","시연·QA 종결","공개사이트·멀티테넌시, 마이그레이션 반영, QA PASS · 결과보고 08-11"),
    ])
    # 게이트
    text(s, 0.55, 4.35, 12.2, 0.3, "선행 게이트 (착수 전제) · Phase 게이트", size=11, color=INK, bold=True)
    gates = [("G1  나노바나나 라이브","Gemini 실렌더(API키·LIVE=1)·워터마크·미커밋", PURPLE),
             ("G2  개발 배포서버","kintex.zioinfo.co.kr:8021 · PostGIS+Redis+Flyway", BLUE),
             ("G3  모바일 EAS 빌드","외부 클라우드 빌드 — 소유자 승인 후 실행", TEAL),
             ("Phase 게이트","각 단계 QA PASS 후 다음 단계 진행", GREEN)]
    for i,(t,d,cl) in enumerate(gates):
        x=0.55+i*3.08
        card(s, x, 4.72, 2.92, 1.35)
        rect(s, x, 4.72, 2.92, 0.08, fill=cl, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
        text(s, x+0.2, 4.9, 2.55, 0.5, t, size=10, color=cl, bold=True, line_spacing=0.95)
        text(s, x+0.2, 5.42, 2.55, 0.6, d, size=8.6, color=MUTED, line_spacing=0.98)
    caption(s, "일정은 6세션 단위 스프린트. 배포 전 필수: Flyway 라이브 dry-run·시크릿 확인·파일단위 커밋·health 게이트.", y=6.5)

def s_org():
    s = slide(); content_header(s, "4. 추진 일정 및 수행 조직", "4-2. 수행 조직 및 R&R", "4")
    text(s, 0.55, 1.4, 12.2, 0.3, "수행 조직 — 거버넌스 · 아키텍트 · 구현 · 품질/배포 (전문 에이전트 12팀 + 범용)", size=11, color=INK, bold=True)
    rows = [("거버넌스","kintex-pm · dev-pm · pmo","범위·일정·리스크·게이트(G1/G2)·품질 거버넌스", NAVY),
            ("아키텍트","AA · SA · TA · DA · NA","앱·시스템·기술·데이터·네트워크 아키텍처 표준", BLUE),
            ("공통·코어","common-dev · backend · frontend · db-engineer","WISE 공통레이어 이식 + 부스코어(M2~M5)", PURPLE),
            ("도메인","bidding · visitor · cms · bi · admin","옥션·관람·CMS·경영분석·관리자 백오피스", TEAL),
            ("AI·품질·배포","ai-dev · visualizer · qa · devops","Claude AI·나노바나나·점진 QA·CI/CD", GREEN)]
    y=1.85
    for i,(cat,mem,desc,cl) in enumerate(rows):
        yy=y+i*0.62
        rect(s, 0.55, yy, 2.5, 0.52, fill=cl, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
        text(s, 0.7, yy, 2.3, 0.52, cat, size=10.5, color=WHITE, bold=True, anchor=MSO_ANCHOR.MIDDLE)
        rect(s, 3.2, yy, 5.1, 0.52, fill=PANEL, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
        text(s, 3.35, yy, 4.95, 0.52, mem, size=9.3, color=INK, bold=True, anchor=MSO_ANCHOR.MIDDLE)
        rect(s, 8.45, yy, 4.33, 0.52, fill=CARD, line=LINE, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
        text(s, 8.6, yy, 4.05, 0.52, desc, size=8.8, color=MUTED, anchor=MSO_ANCHOR.MIDDLE, line_spacing=0.9)
    lead(s, 0.55, 5.15, 12.23, 0.55, "실행 모드  —  하이브리드(전문가 풀 + 감독자): 다중 도메인은 팀, 단일·분석·점검은 서브 에이전트", fill=BLUE, size=12)
    caption(s, "선행 게이트(G1·G2) + Phase 게이트 QA 통과로 진행. 실명 대신 직군·역할로 표기.", y=6.5)

def s_deliverables():
    s = slide(); content_header(s, "4. 추진 일정 및 수행 조직", "4-3. 산출물 및 형상관리 계획", "4")
    text(s, 0.55, 1.4, 12.2, 0.3, "PoC 산출물은 문서·코드·데이터·보고로 구분하고, Gitea 저장소에서 형상관리한다", size=11, color=INK, bold=True)
    rows = [("기획","PLANNING.md · IMPLEMENTATION_BACKLOG.md · OWNER_FEEDBACK.md","planner","docs/"),
            ("설계","design.md(화면 스펙·Stitch 프롬프트) · data.md · 아키텍처 문서","designer·아키텍트","docs/"),
            ("소스","src/backend(Spring) · src/frontend(React) · mobile(Expo) · tools/nanobanana","구현팀","src/·mobile/"),
            ("데이터","Flyway 마이그레이션(스키마·인덱스·시드) · 크롤 적재 스크립트","db-engineer·crawler","db/migration"),
            ("품질","QA 리포트 · GAP_AUDIT · 보안 체크리스트","qa","docs/"),
            ("보고","PoC 추진계획서 · 결과보고서(PPTX) · 완료보고","pmo","docs/deliverables/")]
    table(s, 0.55, 1.85, 12.23, ["구분","주요 산출물","담당","위치"], rows, [1.5, 6.6, 2.3, 1.83], rowh=0.5, hsize=9.5, bsize=8.7, headfill=NAVY)
    titlecard(s, 0.55, 5.15, 6.05, 1.15, "형상관리 원칙", BLUE, [
        [{"t":"저장소 ","bold":True},{"t":"— Gitea(zio/kintex), origin SSH, webhook 자동배포"}],
        [{"t":"커밋 ","bold":True},{"t":"— 파일단위 커밋으로 공유 트리 교차 방지"}],
    ], headfill=BLUE, bodysize=9.2, igap=0.34)
    titlecard(s, 6.73, 5.15, 6.05, 1.15, "품질 게이트", GREEN, [
        [{"t":"배포 전 ","bold":True},{"t":"— Flyway dry-run·시크릿 확인·health 200"}],
        [{"t":"검수 ","bold":True},{"t":"— Phase별 QA PASS + 소유자 피드백 반영"}],
    ], headfill=GREEN, bodysize=9.2, igap=0.34)
    caption(s, "디자인은 항상 Google Stitch 경유(design.md → Stitch → React 이식). 문서는 한국어, 코드/커밋은 영어.", y=6.5)

# ================================================================
# Ⅴ. 리스크 관리 및 기대효과
# ================================================================
def sec5_divider():
    divider("5", "리스크 관리 및 기대효과", "RISK & EXPECTED VALUE",
            ["리스크 식별 및 대응 계획", "보안 및 품질 관리", "기대효과 및 본사업 전환 계획"])

def s_risk():
    s = slide(); content_header(s, "5. 리스크 관리 및 기대효과", "5-1. 리스크 식별 및 대응 계획", "5")
    rows = [("R1","나노바나나 렌더 품질·지연","高","프롬프트 보존/교체 튜닝 · 큐 비동기 · 타임아웃·재시도, 목표 ≤60초", ("대응",PURPLE)),
            ("R2","공간데이터 좌표계·정합 오류","中","SRID 표준화 · ST_* 교차검증 · CAD/트렌치 캘리브레이션", ("대응",BLUE)),
            ("R3","Stitch 디자인 도구 불안정","中","design.md 스펙 직접 구현 폴백(문서화) · Nifty 시스템 준수", ("완화",AMBER)),
            ("R4","배포 롤백·캐시·교차커밋","中","Flyway dry-run · 파일단위 커밋 · health 게이트 · 롤백 절차", ("대응",GREEN)),
            ("R5","AI 토큰 비용·응답 편차","低","AiTextRouter 설정형 전환 · 토큰 최소화 기획 · 캐싱", ("완화",TEAL)),
            ("R6","일정 압축(14일)·범위 초과","中","코어 우선(M2~M5) · Out-of-scope 명확화 · 세션 게이트", ("대응",NAVY)),
            ("R7","보안(자격증명·PII 노출)","高","불변식 강제 · 2FA · 스택트레이스 미노출 · 키 마스킹·미커밋", ("대응",RED))]
    table(s, 0.55, 1.45, 12.23, ["ID","리스크","심각도","대응 방안","구분"],
          rows, [0.9, 3.6, 1.3, 5.13, 1.3], rowh=0.6, hsize=9.5, bsize=8.6, headfill=INK)
    caption(s, "리스크는 세션 게이트마다 재점검. 高 리스크(R1·R7)는 선행 게이트·불변식으로 사전 통제.", y=6.62)

def s_security():
    s = slide(); content_header(s, "5. 리스크 관리 및 기대효과", "5-2. 보안 및 품질 관리", "5")
    # 보안 불변식
    card(s, 0.55, 1.45, 6.05, 3.2)
    rect(s, 0.55, 1.45, 6.05, 0.46, fill=RED, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
    text(s, 0.55, 1.45, 6.05, 0.46, "보안 불변식 (통과 조건)", size=12, color=WHITE, bold=True, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)
    bullets(s, 0.78, 2.08, 5.6, [
        [{"t":"인증 ","bold":True},{"t":"— JWT + 2FA(TOTP RFC6238), 6역할 RBAC"}],
        [{"t":"격리 ","bold":True},{"t":"— 멀티테넌시 테넌트 격리, 권한 우회 차단"}],
        [{"t":"노출 금지 ","bold":True},{"t":"— 자격증명·PII·스택트레이스 미노출"}],
        [{"t":"AI 표식 ","bold":True},{"t":"— 나노바나나 생성 이미지 워터마크 강제"}],
        [{"t":"키 관리 ","bold":True},{"t":"— 시크릿 fail-fast·마스킹·미커밋"}],
        [{"t":"모바일 ","bold":True},{"t":"— 루트/탈옥 탐지·무결성·화면캡처 방지"}],
    ], gap=0.44, size=9.4, dot=RED)
    # 품질 관리
    card(s, 6.73, 1.45, 6.05, 3.2)
    rect(s, 6.73, 1.45, 6.05, 0.46, fill=GREEN, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
    text(s, 6.73, 1.45, 6.05, 0.46, "품질 관리 계획", size=12, color=WHITE, bold=True, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)
    bullets(s, 6.96, 2.08, 5.6, [
        [{"t":"점진 QA ","bold":True},{"t":"— 모듈 완성 직후 경계면(shape) 교차검증"}],
        [{"t":"공간 정합 ","bold":True},{"t":"— 배치·배선·규정 검증 로직 정합 확인"}],
        [{"t":"미개발 감사 ","bold":True},{"t":"— 메뉴↔라우트↔API 4자 체인 GAP 감사"}],
        [{"t":"빌드 회귀 ","bold":True},{"t":"— 각 배포 전 빌드·스모크 회귀 점검"}],
        [{"t":"게이트 ","bold":True},{"t":"— Phase별 QA PASS 후 진행"}],
        [{"t":"피드백 루프 ","bold":True},{"t":"— 소유자 지시 즉시 반영·재검증"}],
    ], gap=0.44, size=9.4, dot=GREEN)
    lead(s, 0.55, 4.9, 12.23, 0.55, "보안 점검(모의해킹)·부하테스트는 PoC 범위 밖 → 본사업 전환 전 별도 수행(조건부 항목)", fill=NAVY, size=12)
    caption(s, "보안 체크리스트: docs/security/mobile-security-*.md. QA는 kintex-qa 점진 검증 트랙.", y=6.5)

def s_expected():
    s = slide(); content_header(s, "5. 리스크 관리 및 기대효과", "5-3. 기대효과 및 본사업 전환 계획", "5")
    # 기대효과 3
    effs = [("정성적 가치","도면 없이 사진으로 즉시 의사결정 · 데이터 일관성 · 발주 자동화", PURPLE),
            ("정량적 목표","KPI 8종 달성 · 렌더 ≤60초 · 무중단 배포 · 크롤·시드 폐루프", BLUE),
            ("사업적 효과","WISE 재사용으로 착수 공수 절감 · 본사업 전환 타당성 근거 확보", TEAL)]
    for i,(t,d,cl) in enumerate(effs):
        x=0.55+i*4.12
        card(s, x, 1.45, 3.95, 1.45)
        rect(s, x, 1.45, 0.09, 1.45, fill=cl, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
        text(s, x+0.28, 1.6, 3.55, 0.4, t, size=11.5, color=cl, bold=True)
        text(s, x+0.28, 2.05, 3.55, 0.8, d, size=9.3, color=INK, line_spacing=1.05)
    # 본사업 전환 로드맵
    text(s, 0.55, 3.1, 12.2, 0.3, "본사업 전환 로드맵 (PoC 성공 판정 시)", size=11, color=INK, bold=True)
    timeline(s, 0.55, 3.42, 12.23, [
        ("전환 판정","도입 권고","PoC 결과보고 → 도입 타당성 승인 → 본사업 착수"),
        ("운영 고도화","HA·부하·보안","이중화·부하테스트·모의해킹·성능 SLA 튜닝"),
        ("연동 정식화","외부 실계약","PG 정산·전자계약·SSO/HR 실연동, 데이터 이행"),
        ("정식 배포","운영 오픈","kintex.wise.ai.kr 운영 · 모바일 스토어 배포"),
    ], cl=PURPLE)
    caption(s, "PoC는 '전환 타당성 근거'를 만드는 단계 — 조건부 항목(운영·비기능)은 본사업 초기 스프린트에서 해소.", y=6.5)

def s_closing():
    s = slide()
    rect(s, -0.15, -0.15, 13.63, 7.8, fill=BLUE_DK)
    rect(s, 7.5, -1.5, 9, 11, fill=BLUE, shape=MSO_SHAPE.PARALLELOGRAM)
    rect(s, 10.2, -2.0, 7, 12, fill=PURPLE, shape=MSO_SHAPE.PARALLELOGRAM)
    text(s, 0.9, 1.35, 11, 0.5, "PoC 추진계획 — 종합", size=15, color=RGBColor(0x9C,0xB4,0xD8), bold=True)
    text(s, 0.9, 1.9, 11.5, 1.7, [
        [{"t":"“말이 아니라, 이미 도는 것으로 증명한다.”","size":32,"color":WHITE,"bold":True}],
    ], line_spacing=1.05)
    rect(s, 0.95, 3.35, 0.9, 0.09, fill=PURPLE)
    bullets(s, 0.95, 3.7, 7.6, [
        [{"t":"14일 집중 PoC로 핵심 3결합(나노바나나·PostGIS·옥션 폐루프)을 개발서버에서 라이브 실증","color":WHITE}],
        [{"t":"KPI 8종 + 게이트 통과를 성공의 정의로 삼아 3등급 판정","color":WHITE}],
        [{"t":"WISE 표준 재사용으로 공수 절감, 본사업 전환 타당성 근거 확보","color":WHITE}],
    ], gap=0.6, size=12, dot=PURPLE, tcolor=WHITE)
    rect(s, 0.9, 5.95, 11.5, 0.02, fill=RGBColor(0x3A,0x5A,0x8C))
    text(s, 0.9, 6.15, 8, 0.4, "킨텍스(KINTEX) AI 전시관리 시스템 · PoC 추진계획서", size=11, color=RGBColor(0x90,0xA6,0xC9), bold=True)
    text(s, 8.9, 6.15, 3.5, 0.4, "2026-08-11 · GUARDiA/WISE 팀", size=11, color=RGBColor(0x90,0xA6,0xC9), bold=True, align=PP_ALIGN.RIGHT)

# ================================================================
# 부록
# ================================================================
def appendix():
    s = slide(); content_header(s, "APPENDIX", "부록 — PoC 통과 체크리스트 및 참조", "부록")
    # 체크리스트
    card(s, 0.55, 1.45, 6.05, 4.5)
    rect(s, 0.55, 1.45, 6.05, 0.46, fill=INK, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
    text(s, 0.55, 1.45, 6.05, 0.46, "PoC 통과 체크리스트", size=12, color=WHITE, bold=True, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)
    checks = ["부스 배치 3안 생성 + 규정 자동 플래깅","나노바나나 라이브 렌더·구조보존·워터마크",
              "PostGIS ST_* 공간연산 정합","옥션 응찰→낙찰→발주 무결 전이",
              "6역할 RBAC + 2FA 인증 통과","크롤 적재 + 데모 시드 폐루프 구동",
              "CI/CD 무중단 배포 + health 게이트 200","모바일 EAS APK 내부빌드",
              "보안 불변식(자격증명·PII·스택트레이스) 준수"]
    for i,c in enumerate(checks):
        yy=2.02+i*0.42
        rect(s, 0.78, yy+0.03, 0.24, 0.24, fill=GREEN_LT, line=GREEN, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
        text(s, 0.78, yy+0.01, 0.24, 0.24, "✓", size=10, color=GREEN, bold=True, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)
        text(s, 1.14, yy, 5.4, 0.4, c, size=9.4, color=INK, anchor=MSO_ANCHOR.MIDDLE)
    # 참조 자산
    card(s, 6.73, 1.45, 6.05, 4.5)
    rect(s, 6.73, 1.45, 6.05, 0.46, fill=NAVY, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
    text(s, 6.73, 1.45, 6.05, 0.46, "참조 자산 · 게이트 정의", size=12, color=WHITE, bold=True, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)
    refs = [("docs/PLANNING.md","시스템 기획서 — 모듈·아키텍처·나노바나나 파이프라인"),
            ("docs/design.md","UI 디자인 스펙 — 화면 Stitch 프롬프트"),
            ("docs/IMPLEMENTATION_BACKLOG.md","구현 백로그 — Phase·모듈·의존"),
            ("docs/analysis/reroomai-source.md","ReRoomAI 소스 — image-to-image 차용"),
            ("G1 나노바나나","GEMINI_API_KEY·NANOBANANA_LIVE=1·워터마크"),
            ("G2 개발서버","kintex.zioinfo.co.kr:8021·PostGIS+Redis+Flyway"),
            ("G3 EAS 빌드","외부 클라우드 빌드 — 소유자 승인 후")]
    for i,(t,d) in enumerate(refs):
        yy=2.05+i*0.55
        rect(s, 6.95, yy+0.05, 0.09, 0.3, fill=BLUE, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
        text(s, 7.15, yy, 5.5, 0.3, t, size=9.4, color=BLUE_DK, bold=True)
        text(s, 7.15, yy+0.28, 5.5, 0.28, d, size=8.4, color=MUTED)
    caption(s, "본 계획서 승인 후 착수 — 선행 게이트(G1·G2) 해소를 전제로 6세션 스프린트 진행.", y=6.5)

# ================================================================
# BUILD
# ================================================================
cover()             # 1
toc()               # 2
exec_summary()      # 3
sec1_divider()      # 4
s_background()      # 5
s_solution()        # 6
s_scope()           # 7
sec2_divider()      # 8
s_kpi()             # 9
s_criteria()        # 10
s_architecture()    # 11
sec3_divider()      # 12
s_scenario()        # 13
s_data()            # 14
s_nanobanana()      # 15
sec4_divider()      # 16
s_schedule()        # 17
s_org()             # 18
s_deliverables()    # 19
sec5_divider()      # 20
s_risk()            # 21
s_security()        # 22
s_expected()        # 23
s_closing()         # 24
appendix()          # 25

prs.save(OUT)
print(f"saved: {OUT}  ({len(prs.slides.__iter__.__self__._sldIdLst)} slides)")
