# -*- coding: utf-8 -*-
"""
킨텍스 자동전시시스템 구축 — 제안서(기술제안서) PPTX 생성기
- 소스: _workspace/proposal/{proposal_content.md, design_system.md, visual_concepts.md, tech_advisory.md, assets/icons/*.svg, assets/screens/*}
- 디자인: KINTEX Blue #0066B3 / AI Purple #6D4AFF / 맑은 고딕 / 16:9 · 개발계획서(gen_dev_plan_pptx.py)와 동일 팔레트·톤(산출물 패밀리)
- 도식 15종은 python-pptx 도형으로 직접 작도(visual_concepts 좌표·색·라벨 그대로). 선 아이콘은 SVG→PNG(icon_raster) 삽입.
- 부록 D: assets/screens/ + screens_manifest.json 재스캔 → 화면 설계(안) 갤러리 자동 재구성.
- 보안 불변: 키·비번·IP·SSH 미기재. AI 생성 이미지 워터마크·금지 고지 유지.
재실행: python build_deck.py
"""
import os, json, glob
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 icon_raster import render_icon

BASE = os.path.dirname(os.path.abspath(__file__))
ICONPNG = os.path.join(BASE, "_iconpng")
SCREENS = os.path.join(BASE, "assets", "screens")
os.makedirs(ICONPNG, exist_ok=True)

# ---------------- 팔레트 (design_system §1) ----------------
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)
HALL_BG   = RGBColor(0xF2, 0xF4, 0xF7)
WHITE     = RGBColor(0xFF, 0xFF, 0xFF)
CARD      = WHITE
GREEN     = RGBColor(0x12, 0x9E, 0x63)
AMBER     = RGBColor(0xE0, 0x8A, 0x00)
RED       = RGBColor(0xD9, 0x2D, 0x20)
CYAN      = RGBColor(0x0B, 0xA5, 0xEC)
TEAL      = RGBColor(0x0E, 0x93, 0x84)
ZEBRA     = RGBColor(0xFC, 0xFC, 0xFD)
BLUEMID   = RGBColor(0x4A, 0x90, 0xD9)
NAVY      = RGBColor(0x2B, 0x4C, 0x7E)
POLY      = RGBColor(0xD8, 0xDE, 0xE8)
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 slide(): return prs.slides.add_slide(BLANK)

def _fill(sp, color):
    if color is None: sp.fill.background()
    else: sp.fill.solid(); sp.fill.fore_color.rgb = color

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))
    _fill(sp, fill)
    if line is None:
        sp.line.fill.background()
    else:
        sp.line.color.rgb = line; sp.line.width = Pt(line_w)
        if dash:
            ln = sp.line._get_or_add_ln()
            d = ln.makeelement(qn('a:prstDash'), {'val': dash}); ln.append(d)
    sp.shadow.inherit = False  # creates an empty <a:effectLst/>
    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'})
        alp = clr.makeelement(qn('a:alpha'), {'val':'16000'})
        clr.append(alp); 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()
        d = el.makeelement(qn('a:prstDash'), {'val': dash}); el.append(d)
    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):
    """content: str | list[para]; para = str | seg-dict | list[seg]; seg={'t','size','color','bold'}"""
    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 page_bg(s, color=BG): rect(s, -0.1, -0.1, 13.53, 7.7, fill=color)

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)

# ---- 선 아이콘(SVG→PNG) ----
def icon(s, name, x, y, size, color=BLUE):
    hexc = '%02X%02X%02X' % (color[0], color[1], color[2])
    out = os.path.join(ICONPNG, f"{name}__{hexc}.png")
    if not os.path.exists(out):
        render_icon(name, (color[0], color[1], color[2]), out, px=160)
    s.shapes.add_picture(out, inch(x), inch(y), inch(size), inch(size))

# ---- 이미지 aspect-fit 삽입 ----
def img_fit(s, path, x, y, w, h, frame=True, framecolor=LINE, bg=WHITE):
    from PIL import Image
    iw, ih = Image.open(path).size
    ar = iw/ih; box = w/h
    if ar > box:
        dw = w; dh = w/ar
    else:
        dh = h; dw = h*ar
    dx = x + (w-dw)/2; dy = y + (h-dh)/2
    if frame:
        rect(s, x, y, w, h, fill=bg, line=framecolor, line_w=1.0, shape=MSO_SHAPE.ROUNDED_RECTANGLE, shadow=True)
    s.shapes.add_picture(path, inch(dx), inch(dy), inch(dw), inch(dh))

# ---- 헤더/푸터 ----
FOOTER = "킨텍스(KINTEX) 자동전시시스템 구축 · 기술제안서"
def content_header(s, kicker, title, idx, req=None):
    page_bg(s, BG)
    rect(s, 0, 0, 13.333, 0.12, fill=BLUE)
    rect(s, 0.55, 0.5, 0.62, 0.62, fill=BLUE, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
    text(s, 0.55, 0.5, 0.62, 0.62, f"{idx:02d}" if isinstance(idx,int) else idx,
         size=20, color=WHITE, bold=True, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)
    text(s, 1.32, 0.5, 9.3, 0.28, kicker, size=11, color=PURPLE, bold=True)
    tw = (11.46 - 0.082*len(req)) if req else 11.3   # REQ 배지와 겹치지 않도록 폭 축소(길면 자동 줄바꿈)
    text(s, 1.32, 0.72, tw-1.32, 0.56, title, size=19.5, color=INK, bold=True, line_spacing=0.96)
    rect(s, 0.55, 1.34, 12.23, 0.02, fill=LINE)
    text(s, 0.55, 7.06, 8, 0.3, FOOTER, size=8.5, color=MUTED)
    text(s, 11.0, 7.06, 1.78, 0.3, "2026-07-11", size=8.5, color=MUTED, align=PP_ALIGN.RIGHT)
    if req:
        rw = 0.15 + 0.082*len(req)
        rect(s, 12.78-rw, 0.55, rw, 0.3, fill=RGBColor(0xEE,0xF0,0xF3), shape=MSO_SHAPE.ROUNDED_RECTANGLE)
        text(s, 12.78-rw, 0.55, rw, 0.3, req, size=8, color=MUTED, bold=True, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)

def caption(s, txt, y=6.62):
    text(s, 0.55, y, 12.23, 0.3, txt, size=9.5, color=MUTED, align=PP_ALIGN.LEFT)

# 라벨 컬러(검증됨/구현 계획/협의)
LABELC = {"[검증됨]":GREEN, "[구현 계획]":BLUE, "[협의]":AMBER, "[가정]":AMBER, "[Non-Goal]":SLATE}
def _segify(t):
    """문자열에서 [라벨] 토큰을 색 run으로 분리."""
    segs=[]; i=0
    while i < len(t):
        hit=None
        for lab in LABELC:
            if t.startswith(lab, i): hit=lab; break
        if hit:
            segs.append({"t":hit, "color":LABELC[hit], "bold":True}); i+=len(hit)
        else:
            j=i
            while j < len(t):
                if any(t.startswith(l, j) for l in LABELC): break
                j+=1
            segs.append({"t":t[i:j]}); i=j
    return segs

def bullets(s, x, y, w, items, dot=BLUE, size=10.5, gap=0.46, 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 = _segify(it) if isinstance(it, str) else 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=15):
    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 wm_badge(s, x, y, w, h=0.34, size=8.5):
    rect(s, x, y, w, h, fill=RGBColor(0xFC,0xE9,0xE7), line=RED, line_w=1.0, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
    text(s, x+0.12, y, w-0.2, h, "AI 생성 예상 이미지 — 실제 시공과 다를 수 있음. 계약·심사 서류 사용 금지.",
         size=size, color=RED, bold=True, anchor=MSO_ANCHOR.MIDDLE)

def arrow(s, x, y, big=False, color=SLATE):
    text(s, x, y, 0.3, 0.5, "›" if not big else "→", size=17 if not big else 15,
         color=color, bold=True, align=PP_ALIGN.CENTER, 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=_segify(it) if isinstance(it,str) else 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):
            b = (i in hl_rows) and j==0
            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]

# ================================================================
# COVER (S01)
# ================================================================
def cover():
    s = slide()
    rect(s, -0.1, -0.1, 13.53, 7.7, 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 · WISE AI", size=15, color=WHITE, bold=True, anchor=MSO_ANCHOR.MIDDLE)
    chip(s, 0.85, 2.3, 1.75, 0.42, "기술제안서", PURPLE, WHITE, size=12)
    chip(s, 2.72, 2.3, 2.25, 0.42, "Technical Proposal", NAVY, WHITE, size=10.5)
    text(s, 0.85, 2.88, 11, 1.9, [
        [{"t":"킨텍스 자동전시시스템","size":44,"color":WHITE,"bold":True}],
        [{"t":"구축 기술제안서","size":44,"color":WHITE,"bold":True}],
    ], line_spacing=1.02)
    # 슬로건 배너
    rect(s, 0.85, 4.7, 11.4, 0.66, fill=RGBColor(0x14,0x35,0x63), shape=MSO_SHAPE.ROUNDED_RECTANGLE)
    rect(s, 0.85, 4.7, 0.1, 0.66, fill=PURPLE, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
    icon(s, "camera-render", 1.06, 4.86, 0.34, WHITE)
    text(s, 1.55, 4.7, 10.6, 0.66, "신청서를 내는 순간, 시공 후 사진을 먼저 본다.",
         size=18, color=WHITE, bold=True, anchor=MSO_ANCHOR.MIDDLE)
    text(s, 0.88, 5.55, 11.4, 0.4,
         "설계 → 시각화 → 발주 → 시공 → 운영 → 분석. 킨텍스에서 한 흐름으로 끝난다.",
         size=12.5, color=SUBLT)
    rect(s, 0.85, 6.12, 11.63, 0.02, fill=RGBColor(0x3A,0x5A,0x8C))
    text(s, 0.85, 6.32, 8.5, 0.4, [[{"t":"사업명  ","size":10.5,"color":RGBColor(0x90,0xA6,0xC9)},
        {"t":"킨텍스(KINTEX) 자동전시시스템 구축","size":11,"color":WHITE,"bold":True}]])
    text(s, 0.85, 6.68, 9.2, 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.5,"color":WHITE}]])
    text(s, 9.7, 6.32, 2.78, 0.4, "작성일 2026-07-11", size=11, color=WHITE, bold=True, align=PP_ALIGN.RIGHT)
    text(s, 9.7, 6.68, 2.78, 0.4, "제안 · GUARDiA/WISE 팀", size=9, color=RGBColor(0x90,0xA6,0xC9), align=PP_ALIGN.RIGHT)

# ================================================================
# 간지 divider (배점 배지)
# ================================================================
def divider(no, kr, en, points, score, score_fill=BLUE):
    s = slide()
    rect(s, -0.1, -0.1, 13.53, 7.7, 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, 2.05, 5, 2.2, no, size=150, color=RGBColor(0x2C,0x4E,0x82), bold=True)
    rect(s, 1.15, 3.9, 0.9, 0.09, fill=PURPLE)
    text(s, 1.15, 4.05, 7, 0.8, kr, size=34, color=WHITE, bold=True)
    text(s, 1.18, 4.9, 7, 0.4, en, size=13, color=RGBColor(0x9C,0xB4,0xD8), bold=True)
    for i, p in enumerate(points):
        yy = 2.5 + 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)
    # 배점 배지
    rect(s, 10.05, 0.7, 2.5, 0.6, fill=score_fill, shape=MSO_SHAPE.ROUNDED_RECTANGLE, shadow=True)
    text(s, 10.05, 0.7, 2.5, 0.6, score, size=13, color=WHITE, bold=True, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)
    return s

# ================================================================
# 도식 ① 3중 해자 벤다이어그램
# ================================================================
def venn(s, detail=False):
    cx = 5.0 if not detail else 4.3
    A=(cx-0.0, 3.55); B=(cx+2.5, 3.55); C=(cx+1.25, 5.35); r=1.95
    # 원(연한 fill + 굵은 stroke)
    rect(s, A[0]-r/2, A[1]-r/2, r, r, fill=PURPLE_LT, line=PURPLE, line_w=2.5, shape=MSO_SHAPE.OVAL)
    rect(s, B[0]-r/2, B[1]-r/2, r, r, fill=BLUE_LT, line=BLUE, line_w=2.5, shape=MSO_SHAPE.OVAL)
    rect(s, C[0]-r/2, C[1]-r/2, r, r, fill=RGBColor(0xE1,0xF3,0xF1), line=TEAL, line_w=2.5, shape=MSO_SHAPE.OVAL)
    # 라벨
    icon(s, "ai-sparkle", A[0]-1.35, A[1]-1.15, 0.3, PURPLE)
    text(s, A[0]-2.35, A[1]-1.5, 2.3, 0.3, "나노바나나 시공 예측", size=11.5, color=PURPLE, bold=True, align=PP_ALIGN.RIGHT)
    text(s, A[0]-2.35, A[1]-1.2, 2.3, 0.3, "image-to-image·40초·S1~S7", size=8, color=MUTED, align=PP_ALIGN.RIGHT)
    icon(s, "postgis-spatial", B[0]+1.05, B[1]-1.15, 0.3, BLUE)
    text(s, B[0]+0.05, B[1]-1.5, 2.4, 0.3, "PostGIS 실측 공간데이터", size=11.5, color=BLUE, bold=True)
    text(s, B[0]+0.05, B[1]-1.2, 2.5, 0.3, "폴리곤·트렌치·배선 단일 원천", size=8, color=MUTED)
    icon(s, "closed-loop", C[0]-0.15, C[1]+0.95, 0.3, TEAL)
    text(s, C[0]-2.6, C[1]+1.0, 2.3, 0.3, "공사 옥션 폐루프", size=11.5, color=TEAL, bold=True, align=PP_ALIGN.RIGHT)
    text(s, C[0]+0.25, C[1]+1.0, 2.7, 0.3, "AI자료→응찰→낙찰→발주 자동전환", size=8, color=MUTED)
    # 중앙 해자 배지
    mx=cx+1.25
    rect(s, mx-1.1, 3.95, 2.2, 0.78, fill=INK, shape=MSO_SHAPE.ROUNDED_RECTANGLE, shadow=True)
    text(s, mx-1.1, 4.0, 2.2, 0.34, "우리만의 해자(Moat)", size=11, color=WHITE, bold=True, align=PP_ALIGN.CENTER)
    text(s, mx-1.1, 4.34, 2.2, 0.3, "재현 난이도 높은 결합", size=8.5, color=SUBLT, align=PP_ALIGN.CENTER)
    if detail:
        rc = titlecard(s, 8.95, 1.55, 3.83, 5.0, "3결합 재현이 어려운 이유", PURPLE, [
            "원A [검증됨] — G1 승인·나노바나나 워커 라이브, image-to-image 구조보존 파이프라인 상주",
            "원B [검증됨] — M2~M5 PostGIS 매퍼 라이브, 부스 폴리곤·트렌치·배선 단일 공간 원천",
            "원C [구현 계획] — Phase D·M15, AI 설계자료가 곧 응찰 근거(attached_refs)",
            "결합의 희소성 — 단일 요소는 모방 가능하나 셋의 폐루프 결합은 경쟁 전시테크·범용 SI가 재현 곤란",
            "실증 우위 — '말이 아니라 이미 돈다'(인프라·인증·PostGIS·나노바나나·자동배포 기동 중)",
        ], headfill=PURPLE, bodysize=9.3, igap=0.86)


# ================================================================
# 후행 정의 헬퍼 (조립 시 상단 hoist)
# ================================================================

def scr_path(scr): return os.path.join(SCREENS, scr+".png")
def screen_panel(s, scr, x, y, w, h, label):
    p = scr_path(scr)
    if os.path.exists(p):
        img_fit(s, p, x, y, w, h)
        chip(s, x+0.12, y+h-0.36, 0.95, 0.28, scr, PURPLE, size=8)
        text(s, x+1.15, y+h-0.37, w-1.3, 0.3, label, size=8.5, color=MUTED, anchor=MSO_ANCHOR.MIDDLE)
    else:
        rect(s, x, y, w, h, fill=HALL_BG, line=LINE, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
        text(s, x, y, w, h, label, size=10, color=MUTED, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)

def org(x,y,w,h,t,sub,fill,tc,ln=None,big=False):
    rect(s,x,y,w,h,fill=fill,line=(ln or fill),line_w=1.2,shape=MSO_SHAPE.ROUNDED_RECTANGLE,shadow=big)
    text(s,x,y+(0.08 if sub else 0),w,(0.3 if sub else h),t,size=(11.5 if big else 10),color=tc,bold=True,
         align=PP_ALIGN.CENTER,anchor=(MSO_ANCHOR.TOP if sub else MSO_ANCHOR.MIDDLE))
    if sub: text(s,x,y+0.34,w,h-0.38,sub,size=7.8,color=(MUTED if fill==WHITE else WHITE),align=PP_ALIGN.CENTER)

def _modbox(col, ry, rh, code, name, cl, icn, strong=False):
    x=mx0+col*mcw+0.03
    rect(s, x, ry, mcw-0.12, rh, fill=(PURPLE_LT if cl==PURPLE else (BLUE_LT if cl==BLUE else HALL_BG)),
         line=cl, line_w=(2.2 if strong else 1.0), shape=MSO_SHAPE.ROUNDED_RECTANGLE)
    text(s, x+0.08, ry+0.05, mcw-0.24, 0.28, [[{"t":code+" ","size":9,"color":cl,"bold":True},{"t":name,"size":7.6,"color":INK}]], line_spacing=0.9)
    if icn: icon(s, icn, x+mcw-0.44, ry+0.06, 0.26, cl)

def band(y, h, fill, ln, label):
    rect(s, 0.55, y, 10.6, h, fill=fill, line=ln, line_w=1.0, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
    rect(s, 0.55, y, 1.45, h, fill=ln, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
    text(s, 0.55, y, 1.45, h, label, size=9, color=WHITE, bold=True, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE, line_spacing=0.9)
def nd(x, y, w, h, t, fill, tc, ln=None, sz=8.2):
    rect(s, x, y, w, h, fill=fill, line=(ln or LINE), line_w=0.8, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
    text(s, x+0.04, y, w-0.08, h, t, size=sz, color=tc, bold=True, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE, line_spacing=0.88)

# ================================================================
# 슬라이드 시퀀스 (나라장터 표준 7대장)
# ================================================================

cover()


# ---- 목차 (표준 7대장) ----
s = slide(); content_header(s, "CONTENTS", "목차 — 나라장터 정보시스템 표준 7대장", "―")
toc=[("Ⅰ","일반현황","제안사·수행조직·핵심 투입인력(M/M)·정량 대응",BLUE),
     ("Ⅱ","전략 및 방법론","사업이해·추진전략·적용기술·표준FW·개발방법론",PURPLE),
     ("Ⅲ","기술 및 기능","나노바나나·배치설계배선·옥션·관람·BI·데이터·보안",PURPLE),
     ("Ⅳ","성능 및 품질","성능·SLA·품질·인터페이스·관측성",BLUE),
     ("Ⅴ","프로젝트 관리","일정·관리방법론·위험이슈·품질형상",BLUE),
     ("Ⅵ","프로젝트 지원","인수인계·교육·기술지원·유지보수(SM)·비상대책",BLUE),
     ("Ⅶ","그 밖의 사항","확장·상생협력·하도급·기대효과",PURPLE),
     ("부록","A·B·C·D","확인필요·REQ커버리지·성숙도·화면 갤러리",NAVY)]
colw=5.9
for i,(no,t,d,cl) in enumerate(toc):
    col=i//4; row=i%4
    x=0.6+col*(colw+0.33); y=1.55+row*1.24
    rect(s, x, y, colw, 1.08, fill=CARD, line=LINE, shape=MSO_SHAPE.ROUNDED_RECTANGLE, shadow=True)
    rect(s, x, y, 0.09, 1.08, fill=cl, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
    text(s, x+0.26, y, 1.0, 1.08, no, size=(20 if no!="부록" else 13), color=cl, bold=True, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)
    text(s, x+1.35, y+0.2, colw-1.6, 0.4, t, size=13.5, color=INK, bold=True)
    text(s, x+1.35, y+0.62, colw-1.6, 0.4, d, size=8.6, color=MUTED, line_spacing=0.95)
caption(s, "표준 7대장(Ⅰ~Ⅶ) + 부록 — 평가부문과 장 구조를 1:1 정렬. Ⅲ(기술·기능)에 제안 무게 집중.", y=6.68)

# ---- 배점 대응 맵 (90/10) ----
s = slide(); content_header(s, "표준 목차 · 평가배점 대응 맵", "정보화사업 표준 배점 — 기술 90(정성70+정량20) / 가격 10에 장별 정렬", "―")
strip=[("기술능력 90","정성 70 + 정량 20",BLUE,7.4),("가격 10","입찰가격평가",PURPLE,4.83)]
sx=0.55
for name,sub,cl,ww in strip:
    rect(s, sx, 1.5, ww, 0.72, fill=cl, shape=MSO_SHAPE.ROUNDED_RECTANGLE, shadow=True)
    text(s, sx+0.2, 1.55, ww-0.3, 0.36, name, size=13, color=WHITE, bold=True, anchor=MSO_ANCHOR.MIDDLE)
    text(s, sx+0.2, 1.92, ww-0.3, 0.26, sub, size=8.5, color=SUBLT, anchor=MSO_ANCHOR.MIDDLE)
    sx+=ww+0.15
heat=[("전략 및 방법론","Ⅱ","사업이해·추진전략·적용기술·표준FW·개발방법론",PURPLE),
      ("기술 및 기능 (최다)","Ⅲ","나노바나나·배치설계배선·옥션·관람·BI·기술보안",PURPLE),
      ("성능 및 품질","Ⅳ","성능·SLA·품질·인터페이스·관측성",BLUE),
      ("프로젝트 관리","Ⅴ","일정·관리방법론·위험이슈·품질형상",BLUE),
      ("프로젝트 지원","Ⅵ","인수인계·교육·기술지원·유지보수(SM)·비상",BLUE),
      ("정량(보유인력·경영·실적·상생)","Ⅰ·Ⅶ","일반현황 증빙·상생협력·하도급",TEAL)]
y0=2.5
for i,(name,ch,desc,cl) in enumerate(heat):
    y=y0+i*0.62
    rect(s, 0.55, y, 4.0, 0.5, fill=cl, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
    text(s, 0.72, y, 2.95, 0.5, name, size=9.8, color=WHITE, bold=True, anchor=MSO_ANCHOR.MIDDLE)
    rect(s, 3.85, y+0.06, 0.62, 0.38, fill=WHITE, line=cl, line_w=1.2, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
    text(s, 3.85, y+0.05, 0.62, 0.4, ch, size=11, color=cl, bold=True, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)
    text(s, 4.7, y, 8.0, 0.5, desc, size=9.2, color=INK, anchor=MSO_ANCHOR.MIDDLE)
caption(s, "배점은 수요기관 재량 — 정보화 지침 제18조 원칙값(기술90/가격10, 정성70+정량20) 표기. 실제 공고 확인 시 재정렬.", y=6.5)

# ---- [가정] 고지 (표준 7대장) ----
s = slide(); content_header(s, "표준 목차 · [가정] 고지 간지", "가정은 숨기지 않고 드러냅니다 — 실제 공고 확인 시 즉시 재정렬합니다", "―")
card(s, 0.55, 1.6, 12.23, 2.15)
rect(s, 0.55, 1.6, 0.1, 2.15, fill=AMBER, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
icon(s, "risk-warning", 0.85, 1.85, 0.55, AMBER)
text(s, 1.6, 1.85, 11.0, 0.4, "표준 7대장 재편 — 평가배점·목차 가정 고지", size=13, color=AMBER, bold=True)
bullets(s, 1.6, 2.4, 10.9, [
    "목차 — 조달청 별지 제5호서식(정보시스템 구축) 표준 7대장(Ⅰ~Ⅶ)으로 재편",
    "배점 — 정보화사업 표준 기술 90(정성70+정량20)/가격 10 [가정], 실제 공고 확인 시 재정렬",
    "상생협력·하도급·투입인력 실명·단가는 [공고·컨소시엄 확정 시 기재]",
    "확인 필요(T1~T5)는 부록 A에 집약 → 정직성을 강점으로 전환",
], dot=AMBER, gap=0.42)
lead(s, 0.55, 4.05, 12.23, 0.56, "우리의 태도 — 약한 고리를 먼저 드러내고 방어 논리를 붙인다. 발주처 협업 자세로 신뢰를 얻는다.", fill=INK, size=13)
caption(s, "실제 공고가 확인되면 배점·목차·분량·투입계획을 즉시 재정렬한다.")

# ================================================================
# Ⅰ 일반현황
# ================================================================

divider("Ⅰ", "일반현황", "GENERAL STATUS", ["제안사·수행조직", "핵심 투입인력 M/M", "정량평가 대응"], "정량 대응 · 20점", TEAL)

# ---- S02 제안 개요 ----
s = slide(); content_header(s, "Ⅰ. 일반현황 · 제안 개요", "도면이 아니라 사진으로 컨펌하고, 그 사진으로 발주까지 종결한다", "Ⅰ", "REQ-A-001")
assets = [("camera-render","나노바나나 시공 예측","도면을 못 읽어도 신청 시점에 '시공 후 사진'으로 의사결정", PURPLE),
          ("postgis-spatial","PostGIS 실측 공간데이터","부스 폴리곤·트렌치·배선 LineString 단일 원천에서 검증·정산·BI", BLUE),
          ("closed-loop","공사 옥션 폐루프","AI 설계자료→응찰→낙찰→발주 자동전환, 한 흐름에서 종결", TEAL)]
cw=3.95
for i,(ic,t,d,cl) in enumerate(assets):
    x=0.55+i*(cw+0.19)
    card(s, x, 1.55, cw, 2.15)
    rect(s, x, 1.55, cw, 0.08, fill=cl, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
    icon(s, ic, x+0.28, 1.85, 0.62, cl)
    text(s, x+1.05, 1.9, cw-1.2, 0.5, t, size=13, color=cl, bold=True)
    text(s, x+0.28, 2.68, cw-0.5, 0.9, d, size=10, color=INK, line_spacing=1.0)
lead(s, 0.55, 3.95, 12.23, 0.62, "차별점 한 줄  —  “말이 아니라 이미 돈다”: 인프라·인증·PostGIS 코어·나노바나나·자동배포가 라이브로 기동 중", fill=BLUE, size=14)
bullets(s, 0.55, 4.85, 12.0, [
    "우리는 누구 — GUARDiA 표준 프레임워크(WISE/UIWS) 검증 자산 위에 전시 도메인 코어를 얹은 팀",
    "왜 우리인가 — ①나노바나나 시공 예측 ②PostGIS 실측 공간데이터 ③공사 옥션 폐루프, 세 결합을 이미 라이브로 검증 [검증됨]",
    "제안 무게 — 기술·기능(Ⅲ) 배점 비중이 가장 큼, 제안서 분량을 그에 맞춰 배분",
], dot=PURPLE, gap=0.5)
caption(s, "단일 요소는 모방할 수 있으나, 세 결합은 재현하기 어렵다 — 우리만의 해자.")

# ---- Ⅰ 핵심 투입인력 (M/M) ----
s = slide(); content_header(s, "Ⅰ. 일반현황 · 핵심 투입인력", "직군·등급·투입 M/M로 수행역량을 정량 제시 — 실명·단가는 공고 확정 시 확정", "Ⅰ")
mm_rows=[("총괄 PM","4","4","4","12"),
    ("아키텍트(AA·SA·TA·DA·NA)","8","4","2","14"),
    ("백엔드(공통·코어·도메인)","12","16","12","40"),
    ("프론트엔드(역할별 6포털)","8","12","8","28"),
    ("DB 엔지니어(PostGIS)","4","4","2","10"),
    ("AI 개발(Claude·제약 솔버)","4","4","2","10"),
    ("시각화(나노바나나 워커)","4","2","2","8"),
    ("QA","4","6","6","16"),
    ("DevOps/인프라","2","2","4","8"),
    ("소계 (핵심 개발)","50","54","42","146")]
table(s, 0.55, 1.6, 8.7, ["직군","P1","P2","P3","합계(M/M)"], mm_rows, [3.4,1.05,1.05,1.05,2.15], rowh=0.42, hsize=9.5, bsize=9, hl_rows={9})
titlecard(s, 9.45, 1.6, 3.33, 4.6, "투입 요약", PURPLE, [
    "핵심 개발 약 146 M/M + PMO·품질 오버헤드 4~8 → 총 ~150 M/M",
    "12개월(Phase 1~3) 기준 · 상주 1.0 FTE PM",
    "라이브 스캐폴드·인증·인프라·나노바나나로 착수 공수 실측 절감",
    "실명 대신 직군·등급·M/M만 기재(개인정보·단가 미노출)",
], headfill=PURPLE, bodysize=9.0, igap=0.98)
chip(s, 0.55, 6.35, 5.7, 0.4, "가격제안 투입인력표와 1:1 매핑", BLUE, size=10)
caption(s, "직군별 공수(M/M) 산정 — 총 ~150 M/M. 예산·기간은 [가정](공공 SI 통례), 공고 확인 시 재정렬.", y=6.8)

# S15 추진체계도 (도식 ⑬)
s = slide(); content_header(s, "Ⅰ. 일반현황 · 수행조직·추진체계", "거버넌스·아키텍트·코어·도메인·AI·QA·DevOps가 게이트로 맞물린다", "Ⅰ", "REQ-N-004")
org(4.9,1.45,3.55,0.7,"프로젝트 총괄 (PM)","범위·일정·리스크·게이트(G1·G2)",BLUE,WHITE,big=True)
line_conn(s,6.67,2.15,6.67,2.45,SLATE,1.2)
org(2.95,2.48,3.35,0.62,"개발 PM","WBS·스프린트·품질게이트",PURPLE,WHITE)
org(7.05,2.48,3.35,0.62,"PMO","관리표준·형상·QA 거버넌스",PURPLE,WHITE)
rect(s,0.55,3.35,12.23,0.38,fill=BLUE_LT,shape=MSO_SHAPE.ROUNDED_RECTANGLE)
text(s,0.72,3.35,4,0.38,"아키텍트 그룹",size=10.5,color=BLUE_DK,bold=True,anchor=MSO_ANCHOR.MIDDLE)
for i,(a,d) in enumerate([("AA","앱 아키텍처"),("SA","시스템·NFR"),("TA","기술표준"),("DA","데이터·ERD"),("NA","네트워크·보안")]):
    org(0.55+i*2.46,3.82,2.32,0.62,a,d,WHITE,BLUE,BLUE)
rect(s,0.55,4.6,12.23,0.38,fill=PURPLE_LT,shape=MSO_SHAPE.ROUNDED_RECTANGLE)
text(s,0.72,4.6,4,0.38,"구현 그룹",size=10.5,color=PURPLE,bold=True,anchor=MSO_ANCHOR.MIDDLE)
for i,(a,d) in enumerate([("공통/시스템","common-dev"),("백엔드","backend-dev"),("프론트","frontend-dev"),("DB","db-engineer"),("도메인","옥션·관람·CMS·BI·관리자"),("AI·시각화","ai-dev·visualizer")]):
    org(0.55+i*2.05,5.07,1.93,0.66,a,d,WHITE,PURPLE,PURPLE)
org(3.4,5.88,3.0,0.6,"품질 (kintex-qa)","점진 QA·경계면·보안 불변",GREEN,WHITE)
org(6.95,5.88,3.0,0.6,"배포 (devops-dev)","CI/CD·systemd·env",NAVY,WHITE)
caption(s, "선행 게이트(G1·G2) + Phase 게이트 QA 통과로 진행 — 거버넌스·아키텍트·실행 계층.")

# ---- Ⅰ 정량평가 대응 ----
s = slide(); content_header(s, "Ⅰ. 일반현황 · 정량평가 대응", "정량평가(보유인력·경영·실적·상생) 항목별 증빙 대응 — 총 20점 이내", "Ⅰ")
q=[("보유인력","투입 직군·등급·자격 증빙, 상주 PM·아키텍트 확보","org-people",BLUE),
   ("경영상태","기업신용평가 등급 제출(공고 기준)","dashboard-chart",PURPLE),
   ("수행실적","동일·유사 정보시스템 구축 실적 증빙","document-rtm",TEAL),
   ("상생협력","중소기업 공동수급 참여지분율(별표11, 최대 5점)","tenant-building",BLUE_DK),
   ("사회적책임·지리","가점 항목 대응(공고 지정 시)","check-verify",NAVY)]
for i,(t,d,ic,cl) in enumerate(q):
    x=0.55+(i%3)*4.11; y=1.6+(i//3)*1.75
    card(s, x, y, 3.97, 1.55)
    icon(s, ic, x+0.22, y+0.28, 0.5, cl)
    text(s, x+0.9, y+0.28, 2.9, 0.4, t, size=12, color=cl, bold=True)
    text(s, x+0.9, y+0.75, 2.9, 0.7, d, size=9, color=INK, line_spacing=1.0)
lead(s, 0.55, 5.35, 12.23, 0.6, "정량은 증빙 미제출 시 최하점 — 발주 확인 후 별첨(증빙)으로 100% 대응, 배점 20점 이내 확보.", fill=INK, size=12.5)
caption(s, "정량평가 항목 대응 맵 — 보유인력·경영상태·수행실적·상생협력·사회적책임. [가정] 공고 확인 시 확정.")

# ================================================================
# Ⅱ 전략 및 방법론
# ================================================================

divider("Ⅱ", "전략 및 방법론", "STRATEGY & METHODOLOGY", ["사업이해·추진전략", "적용기술(나노바나나·AI)", "표준FW·개발방법론"], "정성 · 전략및방법론", PURPLE)

# ---- S04 핵심 요약 (도식 ① 요약) ----
s = slide(); content_header(s, "Ⅱ. 전략·방법론 · 제안 핵심 요약", "단일 요소는 모방할 수 있으나, 세 결합은 재현하기 어렵다 — 우리만의 해자", "Ⅱ", "REQ-A-001·002")
venn(s, detail=False)
# 우측 Win Theme 요약
wl = ["WT-1 시공 예측", "WT-2 배치·설계·배선 3안+규정검증", "WT-3 옥션 폐루프", "WT-4 보안 3중 격리", "WT-5 멀티테넌트 SaaS"]
for i,w in enumerate(wl):
    y=1.7+i*0.52
    rect(s, 9.1, y, 3.68, 0.44, fill=BLUE_LT, line=BLUE, line_w=1.0, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
    text(s, 9.28, y, 3.5, 0.44, w, size=9.5, color=BLUE_DK, bold=True, anchor=MSO_ANCHOR.MIDDLE)
caption(s, "셋의 결합은 경쟁 전시테크·범용 SI가 재현하기 어렵다. 최다 배점은 Ⅲ(기술·기능).")

# S07 배경·목적
s = slide(); content_header(s, "Ⅱ. 전략·방법론 · 사업 이해도", "108,011㎡의 아시아권 상위 전시장, 준비는 여전히 HWP·CAD 수작업이다", "Ⅱ")
mets = [("108,011㎡","현 전시면적",BLUE),("178,000㎡","2028 제3전시장 완공",PURPLE),("수일~수주","CAD 외주 배치 리드타임",AMBER),("HWP·육안","신고·검수 방식",SLATE)]
mw=2.94
for i,(v,l,cl) in enumerate(mets):
    x=0.55+i*(mw+0.16)
    rect(s, x, 1.55, mw, 1.1, fill=cl, shape=MSO_SHAPE.ROUNDED_RECTANGLE, shadow=True)
    text(s, x, 1.68, mw, 0.5, v, size=22, color=WHITE, bold=True, align=PP_ALIGN.CENTER)
    text(s, x, 2.24, mw, 0.32, l, size=9.5, color=WHITE, align=PP_ALIGN.CENTER)
titlecard(s, 0.55, 2.9, 6.05, 3.3, "현행 병목 (As-Is)", SLATE, [
    "부스 배치 — CAD 외주 수작업, 수일~수주 소요",
    "신고서류 — HWP 수기 작성·이메일 제출",
    "배선·위치표시도 — 육안 작도, 수기 위치표시",
    "규정 검수 — 홀매니저 육안 검토 병목",
    "시공 결과 — 개장일에 처음 대면(사전 시각화 부재)",
], headfill=SLATE, bodysize=9.8, igap=0.5)
titlecard(s, 6.75, 2.9, 6.03, 3.3, "추진 목적 (To-Be)", PURPLE, [
    "전시 준비 전 과정을 AI로 자동화 — 설계·검증·시각화",
    "리드타임·오류·외주비용을 동시 절감",
    "핵심 차별화 — 시공 전 '결과 사진' 선(先)시각화",
    "설계→시각화→발주→시공→운영→분석 폐루프 완성",
    "KINTEX 기준 + 다중 전시관 멀티테넌트 SaaS 확장",
], headfill=PURPLE, bodysize=9.8, igap=0.5)
caption(s, "규모는 국내 최대·아시아권 상위이나, 준비 실무는 수작업에 정체 — 자동화 여지 큼.")

# S08 As-Is 타임라인
s = slide(); content_header(s, "Ⅱ. 전략·방법론 · 사업 이해도", "D-150부터 D-7까지, 마디마다 사람이 기다리고 CAD를 외주한다", "Ⅱ")
steps = [("D-150","판매·견적","견적을 담당자가 수기 회신 → 대기 발생",BLUE),
         ("D-30","배치·설계","CAD 외주 수일~수주, 규정 위반 육안 검수",AMBER),
         ("D-25","유틸리티","전기·네트워크·급배수 신청이 채널별 파편화",AMBER),
         ("D-7","제출·검수","kxwp 제출·서류 육안 검토",RED)]
line_conn(s, 0.9, 3.1, 12.4, 3.1, color=SLATE, w=1.6)
bw=2.85
for i,(d,t,pain,cl) in enumerate(steps):
    x=0.7+i*(bw+0.18)
    rect(s, x+bw/2-0.13, 2.97, 0.26, 0.26, fill=cl, shape=MSO_SHAPE.OVAL)
    chip(s, x+bw/2-0.6, 2.3, 1.2, 0.42, d, cl, size=12)
    card(s, x, 3.45, bw, 1.5)
    text(s, x+0.2, 3.6, bw-0.4, 0.35, t, size=12, color=cl, bold=True)
    text(s, x+0.2, 4.0, bw-0.4, 0.85, pain, size=9.5, color=INK, line_spacing=1.0)
    icon(s, "risk-warning", x+bw-0.55, 3.58, 0.32, cl)
lead(s, 0.55, 5.3, 12.23, 0.62, "진단 — 각 마디의 대기·외주·육안 작업이 리드타임과 오류의 원천. To-Be 자동화의 근거.", fill=INK, size=13)
caption(s, "As-Is 프로세스 병목 — D-데이별 Pain Point.")

# S09 페르소나
s = slide(); content_header(s, "Ⅱ. 전략·방법론 · 사업 이해도", "6역할+2관리자, 각자의 과업·고통·기대를 정확히 안다", "Ⅱ", "REQ-F-081")
pers = [("org-people","주최자 (organizer)","배치·행사 총괄","CAD 수작업 고통",BLUE),
        ("visitor-badge","참가업체 (exhibitor)","부스 신청·설계","도면 못 읽어 개장일 첫 대면",PURPLE),
        ("auction-gavel","공사·장치업체","견적·시공","리스트+엑셀 매칭, 비교 부재",TEAL),
        ("check-verify","홀매니저 (ops)","규정 검수·승인","육안 검수 병목",AMBER),
        ("mobile","관람객 (visitor)","등록·현장","배지·체크인 대기",BLUE),
        ("dashboard-chart","일반 대중 (public)","공개 열람","일정·플로어플랜 접근",SLATE)]
cw=3.95
for i,(ic,role,task,pain,cl) in enumerate(pers):
    x=0.55+(i%3)*(cw+0.19); y=1.55+(i//3)*2.35
    card(s, x, y, cw, 2.15)
    rect(s, x, y, cw, 0.08, fill=cl, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
    icon(s, ic, x+0.24, y+0.28, 0.5, cl)
    text(s, x+0.9, y+0.32, cw-1.0, 0.5, role, size=11.5, color=cl, bold=True, line_spacing=0.92)
    text(s, x+0.24, y+1.0, cw-0.48, 0.32, [[{"t":"과업  ","color":MUTED,"size":9,"bold":True},{"t":task,"color":INK,"size":9}]])
    text(s, x+0.24, y+1.35, cw-0.48, 0.32, [[{"t":"Pain  ","color":RED,"size":9,"bold":True},{"t":pain,"color":INK,"size":9}]])
    text(s, x+0.24, y+1.7, cw-0.48, 0.32, [[{"t":"기대  ","color":GREEN,"size":9,"bold":True},{"t":"각 과업의 자동화·가시화","color":INK,"size":9}]])
caption(s, "+ 플랫폼 관리자·테넌트 관리자 2관리자 → 역할별 포털 분리 근거(REQ-F-081).")

# S10 To-Be 정량
s = slide(); content_header(s, "Ⅱ. 전략·방법론 · 사업 이해도", "견적은 즉시, 배치는 수 분, 검수는 자동, 결과는 사진으로", "Ⅱ", "REQ-F-002·007·009")
rows=[("홀 배정 견적","수일 대기","즉시 (규칙엔진 2,250원/㎡ 자동)"),
      ("배치도 초안","수주 CAD 외주","수 분 3안 자동생성"),
      ("규정 검수","육안 검토","제출 전 자동 플래깅"),
      ("위치표시도","수기 작도","좌표 클릭 자동 작도"),
      ("시공 결과","개장일 첫 대면","신청 시점 사진 예측")]
table(s, 0.55, 1.7, 12.23, ["개선 항목","As-Is","To-Be"], rows, [3.6,3.9,4.73], rowh=0.62, hsize=11, bsize=10.5)
for i in range(len(rows)):
    text(s, 7.35, 2.32+i*0.62, 0.4, 0.5, "→", size=13, color=PURPLE, bold=True, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)
caption(s, "As-Is→To-Be 정량 개선 5행 — 리드타임 수일→즉시·수 분, 검수 육안→자동.")

# S11 비전·포지셔닝
s = slide(); content_header(s, "Ⅱ. 전략·방법론 · 추진전략", "도면·표를 보여주는 전시테크가 아니라, 사진으로 컨펌하는 베뉴 운영 플랫폼", "Ⅱ")
lead(s, 0.55, 1.6, 12.23, 1.05, "“도면이 아니라 사진으로 컨펌한다”", fill=BLUE_DK, size=26)
titlecard(s, 0.55, 2.9, 3.95, 3.3, "사진급 의사결정", PURPLE, [
    "기존 전시테크 = 도면·3D 뷰어 수준",
    "우리 = 사진급 시공 예측",
    "신청 시점에 결과를 본다",
], headfill=PURPLE, bodysize=10, igap=0.55)
titlecard(s, 4.65, 2.9, 3.95, 3.3, "단일 폐루프", BLUE, [
    "설계 자료가 곧 발주 근거",
    "발주까지 한 흐름에서 종결",
    "재입력·중복 작업 제거",
], headfill=BLUE, bodysize=10, igap=0.55)
titlecard(s, 8.75, 2.9, 4.03, 3.3, "데이터 주권", TEAL, [
    "온프레미스 Ollama 기본",
    "외부 AI(Claude·Gemini)는 승인 예외만",
    "데이터존 아웃바운드 0",
], headfill=TEAL, bodysize=10, igap=0.55)
caption(s, "포지셔닝 — 사진 수준 의사결정 + 폐루프 종결 + 데이터 주권.")

# S12 3중 해자 상세 (도식 ①)
s = slide(); content_header(s, "Ⅱ. 전략·방법론 · 추진전략", "셋 중 하나는 흉내낼 수 있어도, 셋의 결합은 재현 난이도가 높다", "Ⅱ", "REQ-A-001·002")
venn(s, detail=True)
caption(s, "3중 해자 — 나노바나나 시공 예측 ∩ PostGIS 실측 공간데이터 ∩ 공사 옥션 폐루프.")

# S13 Win Theme 5
s = slide(); content_header(s, "Ⅱ. 전략·방법론 · 추진전략", "다섯 개의 차별화 메시지가 배점 다섯 곳을 정확히 겨눈다", "Ⅱ", "REQ-A-001·S-009")
wts = [("WT-1","도면이 아니라 사진으로 컨펌한다","AI 시각화 핵심","20점",PURPLE,"camera-render"),
       ("WT-2","수 분 내 3안, 제출 전 규정 자동검증","시각화+기능 교차","20+15",BLUE,"booth-layout"),
       ("WT-3","사진 수준 자료로 응찰하고 발주까지","옥션 폐루프","15점",TEAL,"auction-gavel"),
       ("WT-4","AI가 만든 것은 표시, 남의 데이터는 차단","보안 3중","10점",BLUE_DK,"shield-security"),
       ("WT-5","코드 배포 없이 데이터 온보딩으로 확장","멀티테넌트","5점",BLUE,"tenant-building")]
cw=2.4
for i,(no,msg,sub,sc,cl,ic) in enumerate(wts):
    x=0.55+i*(cw+0.06)
    card(s, x, 1.6, cw, 4.6)
    rect(s, x, 1.6, cw, 0.5, fill=cl, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
    text(s, x, 1.6, cw, 0.5, no, size=13, color=WHITE, bold=True, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)
    icon(s, ic, x+cw/2-0.32, 2.35, 0.64, cl)
    text(s, x+0.16, 3.2, cw-0.32, 1.4, msg, size=10.5, color=INK, bold=True, align=PP_ALIGN.CENTER, line_spacing=1.02)
    text(s, x+0.16, 4.9, cw-0.32, 0.5, sub, size=9, color=MUTED, align=PP_ALIGN.CENTER)
    chip(s, x+cw/2-0.6, 5.5, 1.2, 0.44, sc, cl, size=11)
caption(s, "5개 Win Theme가 평가배점 5개 항목(20·20+15·15·10·5)에 1:1 대응.")

# S17 나노바나나 개요
s = slide(); content_header(s, "Ⅱ. 전략·방법론 · 적용기술(나노바나나)", "시공 전에 사진으로 의사결정한다 — 경쟁 전시테크가 못 하는 차별화", "Ⅱ", "REQ-A-001")
bullets(s, 0.55, 1.6, 7.0, [
    "문제 — 참가업체는 도면을 못 읽어 개장일에야 결과를 처음 본다",
    "해법 — image-to-image로 빈 부스(도면/실측)를 '시공 후 사진'으로 예측",
    "실증 — Gemini 라이브(G1 소유자 승인 완료), 워커 상주 [검증됨]",
    "효과 — 조감도 외주(비용·시간) 대체, 신청 시점 의사결정",
], dot=PURPLE, gap=0.62, size=11)
screen_panel(s, "SCR-12", 7.85, 1.6, 4.93, 3.5, "시각화 갤러리 — 표준 샷 · 워터마크 표기")
wm_badge(s, 7.85, 5.2, 4.93, 0.62)
lead(s, 0.55, 4.5, 7.0, 0.9, "“도면이 아니라 사진으로 컨펌한다” — 신청 시점에 결과 사진으로 의사결정하는 유일 차별화.", fill=PURPLE, size=13)
caption(s, "Gemini gemini-3.1-flash-image-preview 워커 라이브 [검증됨] · 모든 컷 워터마크 항상 포함.")

# S19 image-to-image 구조보존
s = slide(); content_header(s, "Ⅱ. 전략·방법론 · 적용기술(나노바나나)", "골격·치수·통로·카메라 앵글은 잠그고, 스타일·재질만 사실화한다", "Ⅱ", "REQ-A-002·003")
titlecard(s, 0.55, 1.6, 6.05, 3.4, "보존 (잠금) — 왜곡·과장 방지", BLUE, [
    "부스 골격·치수",
    "통로·동선",
    "카메라 앵글·시점",
    "참조 이미지 inlineData 기준 구조 고정",
], headfill=BLUE_DK, bodysize=11, igap=0.58)
titlecard(s, 6.75, 1.6, 6.03, 3.4, "교체 (사실화) — 사진 리얼리티", PURPLE, [
    "스타일·컨셉",
    "재질·마감",
    "조명·분위기",
    "구조화 프롬프트 사전(BOOTH_TYPES·STYLES·FIXTURE_LAYERS)",
], headfill=PURPLE, bodysize=11, igap=0.58)
lead(s, 0.55, 5.25, 12.23, 0.62, "검증 패턴 — ReRoomAI의 image-to-image 보존/교체 분리를 이식(reroomai-source.md).", fill=INK, size=13)
caption(s, "보존/교체 파라미터 분리로 왜곡·과장 방지 + 사진급 리얼리티 동시 달성.")

# S20 파이프라인 (도식 ③)
s = slide(); content_header(s, "Ⅱ. 전략·방법론 · 적용기술(나노바나나)", "생성은 전면 비동기 — 사용자는 대기하지 않고, 완료는 WebSocket으로 밀어준다", "Ⅱ", "REQ-A-007·N-006")
boxes=[("사용자\n렌더 요청",BLUE_LT,BLUE,"mobile"),("Spring 백엔드\nRenderJob·쿼터",WHITE,BLUE,None),
       ("Redis 큐\nleftPush",WHITE,BLUE,"pipeline-flow"),("워커\nBLPOP",PURPLE_LT,PURPLE,"ai-sparkle"),
       ("Gemini\nimage-to-image",PURPLE_LT,PURPLE,None),("워터마크·S6\n로컬 PIL 래스터",PURPLE_LT,PURPLE,"watermark"),
       ("오브젝트\n스토리지",WHITE,BLUE,None),("WebSocket\n완료 푸시",BLUE_LT,BLUE,None)]
bw=1.44; bx0=0.55; by=2.35; bh=1.0
for i,(t,fl,cl,icn) in enumerate(boxes):
    x=bx0+i*(bw+0.075)
    rect(s, x, by, bw, bh, fill=fl, line=cl, line_w=1.3, shape=MSO_SHAPE.ROUNDED_RECTANGLE, shadow=True)
    if icn: icon(s, icn, x+bw/2-0.18, by+0.1, 0.36, cl)
    text(s, x+0.06, by+(0.46 if icn else 0.14), bw-0.12, bh-0.5, t, size=8.3, color=(PURPLE_DK if cl==PURPLE else BLUE_DK), bold=True, align=PP_ALIGN.CENTER, line_spacing=0.9)
    if i in(3,4): chip(s, x+bw/2-0.35, by-0.28, 0.7, 0.24, "라이브", GREEN, size=7.5)
    if i<len(boxes)-1: text(s, x+bw-0.02, by, 0.12, bh, "›", size=13, color=SLATE, bold=True, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)
# 분기 콜아웃
rect(s, 0.55, 3.75, 7.4, 0.72, fill=RGBColor(0xFF,0xF6,0xE8), line=AMBER, line_w=1.0, shape=MSO_SHAPE.ROUNDED_RECTANGLE, dash="dash")
text(s, 0.72, 3.75, 7.2, 0.72, "degraded — G1 미승인/Gemini 장애 시 목(mock) 응답으로 구조·워터마크 유지(REQ-N-005)", size=9.5, color=AMBER, bold=True, anchor=MSO_ANCHOR.MIDDLE)
chip(s, 8.1, 3.9, 2.0, 0.42, "성공 시에만 쿼터 차감", GREEN, size=9)
rect(s, 10.25, 3.83, 2.53, 0.56, fill=RGBColor(0xFC,0xE9,0xE7), line=RED, line_w=1.0, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
icon(s, "shield-security", 10.35, 3.9, 0.34, RED)
text(s, 10.75, 3.83, 2.0, 0.56, "GEMINI_API_KEY = 워커 env only (백엔드 미취급)", size=8, color=RED, bold=True, anchor=MSO_ANCHOR.MIDDLE)
caption(s, "이미지 생성 전면 비동기 — 사용자 동기 대기 없음, 평균 40초.")

# S23 AI 신뢰
s = slide(); content_header(s, "Ⅱ. 전략·방법론 · 적용기술(AI 신뢰)", "결정적 알고리즘이 중심, LLM은 보조, 최종 확정은 사람 — 워터마크는 불변", "Ⅱ", "REQ-S-009·A-017")
tiers=[("결정적 알고리즘 (중심)","제약 솔버·PostGIS 공간연산 — 배치·배선·규정판정의 권위",BLUE,"postgis-spatial"),
       ("LLM (보조)","조건 해석·초안·검수 보조 — RAG 근거·인용·환각 차단(abstain)",PURPLE,"ai-sparkle"),
       ("사람 (최종 확정)","선택·병합·최종 승인 — AI는 제안, 확정은 담당자",GREEN,"check-verify")]
for i,(t,d,cl,icn) in enumerate(tiers):
    y=1.65+i*1.35
    card(s, 0.55, y, 8.5, 1.2)
    rect(s, 0.55, y, 0.1, 1.2, fill=cl, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
    icon(s, icn, 0.85, y+0.35, 0.5, cl)
    text(s, 1.6, y+0.2, 7.2, 0.4, t, size=13, color=cl, bold=True)
    text(s, 1.6, y+0.66, 7.2, 0.4, d, size=10, color=INK)
    chip(s, 8.2, y+0.42, 0.7, 0.36, str(i+1), cl, size=13)
    if i<2: text(s, 4.5, y+1.18, 0.4, 0.2, "▼", size=10, color=SLATE, align=PP_ALIGN.CENTER)
rect(s, 9.35, 1.65, 3.43, 3.9, fill=RGBColor(0xFC,0xE9,0xE7), line=RED, line_w=1.2, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
icon(s, "watermark", 9.6, 1.9, 0.5, RED)
text(s, 9.6, 2.5, 2.9, 0.4, "워터마크 불변", size=12, color=RED, bold=True)
text(s, 9.6, 2.95, 2.95, 2.4, "M5 응답 watermarkRequired:true + text + notice(계약·심사 서류 금지). 제거 불가 [검증됨].\n\nAI 답변은 RAG 근거·인용·환각 차단으로 오판 리스크 차단(R-6).", size=9.3, color=INK, line_spacing=1.05)
caption(s, "3층 신뢰 구조 — 결정론(블루)이 중심, LLM(퍼플)은 보조, 사람(그린)이 최종 확정.")

# S44 AI 라우팅
s = slide(); content_header(s, "Ⅱ. 전략·방법론 · 적용기술(AI 라우팅)", "Claude 기본, 장애·폐쇄망이면 Ollama 폴백 — LLM 직접호출 금지, 중앙 경유만", "Ⅱ", "REQ-A-007·016")
route=[("요청",BLUE),("AiTextRouter",PURPLE),("Claude (api.anthropic.com·승인 예외)",PURPLE),("실패?",AMBER),("Ollama 폴백",TEAL)]
for i,(t,cl) in enumerate(route):
    x=0.55+i*2.5
    rect(s, x, 1.9, 2.3, 1.0, fill=(PURPLE_LT if cl==PURPLE else (RGBColor(0xFF,0xF6,0xE8) if cl==AMBER else (RGBColor(0xE1,0xF3,0xF1) if cl==TEAL else BLUE_LT))),
         line=cl, line_w=1.3, shape=MSO_SHAPE.ROUNDED_RECTANGLE, shadow=True)
    text(s, x+0.1, 1.9, 2.1, 1.0, t, size=9.5, color=INK, bold=True, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE, line_spacing=0.95)
    if i<4: text(s, x+2.3-0.05, 1.9, 0.2, 1.0, "›", size=15, color=SLATE, bold=True, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)
bullets(s, 0.55, 3.4, 12.0, [
    "AiTextRouter — Claude 기본(승인 예외) → Ollama 폴백(REQ-A-016)",
    "나노바나나는 큐/콜백만(백엔드는 이미지 미취급), degraded 시 목 응답(REQ-A-007·N-005)",
    "AiTextRouter/AiConfig는 UIWS 패턴 [구현 계획: 착수 대기]",
], dot=PURPLE, gap=0.55, size=11)
lead(s, 0.55, 5.35, 12.23, 0.62, "온프레미스 우선 — 외부 AI(Claude·Gemini)는 승인 예외만, 데이터 주권 확보.", fill=INK, size=13)
caption(s, "AI 라우터 폴백 — Claude→실패→Ollama, 중앙 경유만(직접 호출 신설 금지).")

# ---- Ⅱ 표준 프레임워크 적용 ----
s = slide(); content_header(s, "Ⅱ. 전략·방법론 · 표준 프레임워크 적용", "검증된 GUARDiA/WISE 표준 프레임워크 위에 전시 도메인을 얹는다", "Ⅱ")
titlecard(s, 0.55, 1.6, 6.05, 3.6, "적용 표준 프레임워크", BLUE, [
    "GUARDiA 표준(UIWS/WISE) — Spring Boot 3.x(Java17)+MyBatis+React18/19+PostgreSQL",
    "인증(JWT+2FA/OTP)·시스템관리·공통 업무모듈 이식(재설계 금지)",
    "단일 jar·모듈러 모놀리스·4계층(Controller/Service/Mapper/DTO) 표준",
    "CI/CD Fail-Safe(빌드→배포→헬스체크→롤백) 표준 [검증됨]",
], headfill=BLUE, bodysize=9.3, igap=0.72)
titlecard(s, 6.75, 1.6, 6.03, 3.6, "표준 준수·정합성", PURPLE, [
    "별표3의2 제약사항 '표준 프레임워크 적용' 대응 — 개발표준·명명·형상 표준화",
    "전자정부 표준프레임워크 적용 요구 여부는 [협의: 공고 확인](현행=GUARDiA 표준)",
    "표준 위 도메인 코어만 신규 — 착수 리스크·재사용성 우위",
    "공공 SW 개발보안·시큐어코딩 가이드 준수",
], headfill=PURPLE, bodysize=9.3, igap=0.72)
lead(s, 0.55, 5.35, 12.23, 0.6, "표준 프레임워크 재사용 — 검증 자산 위에 도메인만 구현, 품질·일관성·유지보수성 확보.", fill=INK, size=13)
caption(s, "표준 프레임워크 적용 — 별표3의2 제약사항 대응. 전자정부 표준FW 정합성은 공고 확인.")

# S14 생애주기 폐루프
s = slide(); content_header(s, "Ⅱ. 전략·방법론 · 개발 방법론", "설계 → 시각화 → 발주 → 시공 → 운영 → 분석이 하나의 순환으로 닫힌다", "Ⅱ")
loop = [("판매·기획","M1·M16",BLUE),("설계·시각화","M2~M5",PURPLE),("발주·계약","M15·M6·M9",BLUE),
        ("참가·관람","M10·M11",BLUE),("현장운영","M8·M13·M14",BLUE),("사후·경영","M16",PURPLE)]
import math as _m
cx,cy,R=6.66,4.15,1.95
n=len(loop)
for i,(t,mod,cl) in enumerate(loop):
    ang=-90+i*360/n
    px=cx+R*_m.cos(_m.radians(ang))-1.15
    py=cy+R*_m.sin(_m.radians(ang))-0.42
    rect(s, px, py, 2.3, 0.84, fill=(PURPLE_LT if cl==PURPLE else BLUE_LT), line=cl, line_w=1.4, shape=MSO_SHAPE.ROUNDED_RECTANGLE, shadow=True)
    text(s, px, py+0.12, 2.3, 0.36, t, size=11, color=(PURPLE if cl==PURPLE else BLUE_DK), bold=True, align=PP_ALIGN.CENTER)
    text(s, px, py+0.48, 2.3, 0.3, mod, size=9, color=MUTED, align=PP_ALIGN.CENTER)
icon(s, "closed-loop", cx-0.55, cy-0.55, 1.1, PURPLE)
caption(s, "각 단계 산출물이 다음 단계 입력 → 분석(M16)이 다음 행사 기획(M1)으로 환류.")

# ================================================================
# Ⅲ 기술 및 기능
# ================================================================

divider("Ⅲ", "기술 및 기능", "TECHNOLOGY & FUNCTION", ["나노바나나·배치설계배선·옥션", "관람·BI·데이터", "기술적 보안"], "정성 · 기술및기능(최다)", PURPLE)

# S16 모듈맵 (도식 ②)
s = slide(); content_header(s, "Ⅲ. 기술·기능 · 모듈 조망", "18개 모듈+공통 레이어를 한 장으로 조망 — P0 코어 4개가 심장이다", "Ⅲ", "REQ-F-073~076")
cols=["판매·기획","설계·시각화","발주·계약","참가·관람","현장운영","사후·경영"]
mx0=1.5; mcw=(12.78-mx0)/6
for j,c in enumerate(cols):
    rect(s, mx0+j*mcw, 1.5, mcw-0.06, 0.4, fill=BLUE, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
    text(s, mx0+j*mcw, 1.5, mcw-0.06, 0.4, c, size=9.5, color=WHITE, bold=True, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)
rowlab=[("P0",PURPLE,1.98,1.2),("P1",BLUE,3.3,1.2),("P2",SLATE,4.62,1.0)]
for lb,cl,ry,rh in rowlab:
    rect(s, 0.55, ry, 0.9, rh, fill=cl, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
    text(s, 0.55, ry, 0.9, rh, lb, size=13, color=WHITE, bold=True, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)
# P0 row
_modbox(1,1.98,0.56,"M2","플로어플랜",PURPLE,"booth-layout",True)
_modbox(1,2.58,0.56,"M3","부스설계",PURPLE,"camera-render",True)
_modbox(2,1.98,0.56,"M4","유틸리티",PURPLE,"wiring-power",True)
_modbox(2,2.58,0.56,"M5","나노바나나",PURPLE,"ai-sparkle",True)
# P1 row
_modbox(0,3.3,0.52,"M1","판매·홀배정",BLUE,None)
_modbox(2,3.3,0.52,"M15","옥션(역경매)",BLUE,"auction-gavel",True)
_modbox(2,3.88,0.52,"M6·M9","서류·정산",BLUE,None)
_modbox(3,3.3,0.52,"M10","관람·등록",BLUE,"visitor-badge")
_modbox(3,3.88,0.52,"M7·M11","매칭",BLUE,None)
_modbox(5,3.3,0.52,"M16","경영분석 BI",BLUE,"dashboard-chart")
_modbox(5,3.88,0.52,"M12·M17","CMS·공개",BLUE,None)
_modbox(0,3.88,0.52,"M18","관리자",BLUE,None)
# P2 row
_modbox(4,4.62,0.5,"M8·M13","물류·내비",SLATE,None)
_modbox(4,5.14,0.5,"M14","현장 IoT",SLATE,None)
_modbox(3,4.62,0.5,"M11","비즈매칭",SLATE,None)
# 하단 레이어 띠
rect(s, 0.55, 5.78, 12.23, 0.34, fill=BLUE_LT, line=BLUE, line_w=1.0, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
text(s, 0.7, 5.78, 12.0, 0.34, "공통/시스템관리 레이어(§5B) — 전 모듈 선행 기반  [검증됨] 공통코드·메뉴·업무모듈·회의록·알림센터(WISE 이식 Flyway V1~V9)", size=9, color=BLUE_DK, bold=True, anchor=MSO_ANCHOR.MIDDLE)
rect(s, 0.55, 6.16, 12.23, 0.34, fill=PURPLE_LT, line=PURPLE, line_w=1.0, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
text(s, 0.7, 6.16, 12.0, 0.34, "멀티테넌시(§1A) — tenant_id 격리 기반  [구현 계획]     │     100대 기능 성숙도: 기구현 63 · 부분 18 · 신규 19", size=9, color=PURPLE_DK, bold=True, anchor=MSO_ANCHOR.MIDDLE)
caption(s, "생애주기 6단계 × 우선순위(P0/P1/P2) 매트릭스 — P0 4개(굵은 테두리)가 심장.", y=6.62)

# S18 샷세트 S1~S7
s = slide(); content_header(s, "Ⅲ. 기술·기능 · 기능(나노바나나 샷세트)", "정면·야간·통로·내부·Before/After·배선·홀전경 — 7컷을 평균 40초에", "Ⅲ", "REQ-A-001·F-019")
shots=[("S1","정면 부스 전경"),("S2","야간 조명 연출"),("S3","통로 시점"),("S4","부스 내부"),
       ("S5","Before/After"),("S6","배선 오버레이"),("S7","홀 전경")]
sw=1.68
for i,(sid,nm) in enumerate(shots):
    x=0.55+i*(sw+0.06)
    card(s, x, 1.7, sw, 2.5)
    rect(s, x, 1.7, sw, 0.42, fill=(PURPLE if sid in("S1","S7") else BLUE_DK), shape=MSO_SHAPE.ROUNDED_RECTANGLE)
    text(s, x, 1.7, sw, 0.42, sid, size=12, color=WHITE, bold=True, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)
    rect(s, x+0.14, 2.24, sw-0.28, 1.2, fill=HALL_BG, line=LINE, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
    icon(s, "camera-render", x+sw/2-0.24, 2.6, 0.48, SLATE)
    text(s, x+0.1, 3.5, sw-0.2, 0.5, nm, size=9, color=INK, bold=True, align=PP_ALIGN.CENTER, line_spacing=0.9)
    wmc = "자동생성" if sid in("S1","S7") else "온디맨드"
    chip(s, x+sw/2-0.5, 3.95, 1.0, 0.22, wmc, (GREEN if sid in("S1","S7") else SLATE), size=7.5)
lead(s, 0.55, 4.5, 12.23, 0.62, "자동생성은 비용·품질 제어 위해 S1·S7 한정, 나머지는 온디맨드  [구현 계획: 샷세트 파이프라인]", fill=BLUE, size=12.5)
wm_badge(s, 0.55, 5.35, 12.23, 0.5)
caption(s, "표준 샷세트 7컷 — 모든 컷에 워터마크·금지 고지 항상 포함.", y=6.0)

# S21 Before/After (도식 ④)
s = slide(); content_header(s, "Ⅲ. 기술·기능 · 기능(Before/After)", "드래그 한 번으로 빈 부스와 시공 후를 비교하고, 배선을 색으로 겹쳐 본다", "Ⅲ", "REQ-A-004·005")
bx,by,bw2,bh2=0.55,1.6,8.6,3.5
rect(s, bx, by, bw2/2, bh2, fill=HALL_BG, line=LINE, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
for gy in range(4):
    rect(s, bx+0.3, by+0.5+gy*0.7, bw2/2-0.6, 0.008, fill=SLATE)
rect(s, bx+bw2/2, by, bw2/2, bh2, fill=PURPLE, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
rect(s, bx+bw2/2, by, bw2/2, bh2, fill=BLUE)  # gradient hint base
rect(s, bx+bw2/2, by, bw2/2, bh2, fill=None, line=LINE, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
icon(s, "camera-render", bx+bw2*0.75-0.3, by+1.1, 0.6, WHITE)
# handle
rect(s, bx+bw2/2-0.02, by, 0.05, bh2, fill=WHITE)
rect(s, bx+bw2/2-0.22, by+bh2/2-0.22, 0.44, 0.44, fill=WHITE, line=BLUE, line_w=1.5, shape=MSO_SHAPE.OVAL, shadow=True)
text(s, bx+bw2/2-0.22, by+bh2/2-0.24, 0.44, 0.44, "‹›", size=13, color=BLUE, bold=True, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)
chip(s, bx+0.15, by+0.15, 2.3, 0.32, "Before: 빈 부스 (도면/실측)", SLATE, size=8.5)
chip(s, bx+bw2/2+0.15, by+0.15, 2.6, 0.32, "After: 시공 후 예상 (AI 생성)", PURPLE, size=8.5)
# 우측 배선 범례
titlecard(s, 9.35, 1.6, 3.43, 3.5, "배선 색상 범례", TEAL, [], headfill=BLUE)
legs=[("전기","RED 실선",RED,"solid"),("네트워크","CYAN 파선",CYAN,"dash"),("급배수","TEAL 점선",TEAL,"sysDot")]
for i,(nm,st,cl,ds) in enumerate(legs):
    yy=2.35+i*0.55
    rect(s, 9.6, yy+0.1, 0.7, 0.06, fill=cl, dash=(None if ds=="solid" else ds))
    text(s, 10.45, yy, 2.2, 0.36, [[{"t":nm+" ","color":INK,"bold":True,"size":10},{"t":st,"color":MUTED,"size":9}]], anchor=MSO_ANCHOR.MIDDLE)
text(s, 9.6, 4.15, 3.0, 0.8, "ARIA slider · 키보드 좌우 조작 (WCAG AA)", size=8.8, color=MUTED, line_spacing=1.0)
icon(s, "check-verify", 9.6, 4.55, 0.3, GREEN)
wm_badge(s, 0.55, 5.35, 12.23, 0.5)
caption(s, "S6 배선 오버레이는 Gemini 미경유 — 백엔드/워커 로컬 PIL 결정적 래스터(좌표 정확성).", y=6.0)

# S22 RenderJob 방어
s = slide(); content_header(s, "Ⅲ. 기술·기능 · 기능(RenderJob 방어)", "다층 방어로 실패를 흡수하고, 성공했을 때만 쿼터를 쓴다", "Ⅲ", "REQ-A-006·008·N-007")
defs=[("입력 가드","크기가드·mimeType 자동감지·SAFETY 분기",BLUE),
      ("친화적 에러","한글 에러 메시지·재시도 안내",BLUE),
      ("간판 후처리","한글 렌더 실패 시 간판영역 합성 (R-5 방어)",PURPLE),
      ("대량 제어","자동생성 S1·S7 한정 + 스키마해시 캐시",TEAL),
      ("쿼터","행사별 쿼터, 성공 시에만 차감",GREEN)]
bw=2.4
for i,(t,d,cl) in enumerate(defs):
    x=0.55+i*(bw+0.06)
    card(s, x, 1.7, bw, 2.6)
    rect(s, x, 1.7, bw, 0.5, fill=cl, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
    text(s, x, 1.7, bw, 0.5, f"{i+1}", size=15, color=WHITE, bold=True, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)
    text(s, x+0.15, 2.35, bw-0.3, 0.4, t, size=11, color=cl, bold=True, align=PP_ALIGN.CENTER)
    text(s, x+0.18, 2.85, bw-0.36, 1.3, d, size=9.3, color=INK, align=PP_ALIGN.CENTER, line_spacing=1.0)
    if i<len(defs)-1: text(s, x+bw-0.03, 1.7, 0.14, 2.6, "›", size=15, color=SLATE, bold=True, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)
lead(s, 0.55, 4.55, 12.23, 0.62, "결과 — 실패를 흡수(degraded)하고 성공 시에만 과금, 대량 생성은 캐시·쿼터로 제어.", fill=INK, size=13)
caption(s, "RenderJob 상태 시퀀스 — 다층 방어 계층.")

# S24 플로어플랜 3안 (도식 ⑤)
s = slide(); content_header(s, "Ⅲ. 기술·기능 · 기능(배치 3안)", "제약 솔버가 상이한 목표로 정확히 3안 — 사람이 선택·병합·확정한다", "Ⅲ", "REQ-F-007·008·010·A-009")
rect(s, 0.55, 1.5, 12.23, 0.44, fill=BLUE_LT, line=BLUE, line_w=1.0, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
icon(s, "booth-layout", 0.68, 1.55, 0.34, BLUE)
text(s, 1.15, 1.5, 11.5, 0.44, "제1전시장 5홀 · 판매가능 10,080㎡ · 통로규정 v3", size=10.5, color=BLUE_DK, bold=True, anchor=MSO_ANCHOR.MIDDLE)
plans=[("안 1 — 통로 효율 최대",BLUE),("안 2 — 프리미엄 부스 최대",PURPLE),("안 3 — 균형",TEAL)]
cw=3.9
for i,(t,cl) in enumerate(plans):
    x=0.55+i*(cw+0.28)
    card(s, x, 2.1, cw, 3.35)
    rect(s, x, 2.1, cw, 0.44, fill=cl, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
    text(s, x, 2.1, cw, 0.44, t, size=11, color=WHITE, bold=True, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)
    # mini layout
    rect(s, x+0.2, 2.68, cw-0.4, 1.5, fill=HALL_BG, line=LINE, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
    for gx in range(4):
        for gy in range(3):
            big = (i==1 and gx<2 and gy<1)
            rect(s, x+0.35+gx*0.8, 2.82+gy*0.42, 0.66, 0.32, fill=(cl if big else POLY), line=WHITE, line_w=0.5)
    for k,(lb,val,ok) in enumerate([("판매면적","9,240㎡" if i==0 else ("8,900㎡" if i==1 else "9,050㎡"),False),
                                     ("부스 수",str(112-i*4)+"부스",False),("규정위반","0건",True)]):
        yy=4.32+k*0.34
        text(s, x+0.24, yy, 1.8, 0.3, lb, size=8.8, color=MUTED)
        text(s, x+2.0, yy, cw-2.2, 0.3, [[{"t":val,"color":(GREEN if ok else INK),"bold":True,"size":9}]], align=PP_ALIGN.RIGHT)
        if ok: icon(s, "check-verify", x+cw-0.5, yy-0.02, 0.26, GREEN)
# 액션 바
acts=[("[안 선택]",BLUE),("[구역·블록 병합]",WHITE),("[병합 후 재검증]",PURPLE),("[최종안 확정·초대링크]",TEAL)]
for i,(t,cl) in enumerate(acts):
    x=0.55+i*3.11
    chip(s, x, 5.65, 2.9, 0.44, t, cl if cl!=WHITE else WHITE, tc=(WHITE if cl!=WHITE else BLUE), size=10, line=(BLUE if cl==WHITE else None))
caption(s, "제약 솔버(비-LLM)가 상이한 최적화 목표로 3안 생성·선택/병합 편집(REQ-F-008) · 확정 시 부스좌표 잠금·버전 기록·참가업체 초대링크 발급(REQ-F-010).")

# S25 규정 자동검증
s = slide(); content_header(s, "Ⅲ. 기술·기능 · 기능(규정 검증)", "피난통로·바닥하중·비상구를 제출 전에 자동으로 플래깅한다", "Ⅲ", "REQ-F-009·A-010")
bullets(s, 0.55, 1.6, 6.9, [
    "룰엔진(compliance-v*.json 버전 데이터) + PostGIS 공간 판정",
    "통로폭 ST_Distance/ST_Buffer · 비상구 ST_Intersects",
    "홀 이탈 ST_Contains · 바닥하중 홀별 판정",
    "병합 후 재검증 필수 · 위반 시 COMPLIANCE_BLOCKED(422)",
    "규정검증=플래깅까지 · 구조안전 판정은 구조기술사/킨텍스 [Non-Goal] (R-6)",
], dot=BLUE, gap=0.58, size=10.5)
screen_panel(s, "SCR-09", 7.7, 1.6, 5.08, 3.7, "규정 검증 리포트 — 위반 항목·홀별 하중·비상구 교차")
caption(s, "제출 전 자동 플래깅 — 홀매니저 육안 검수 병목 해소. 최종 유권해석은 관할·구조기술사.")

# S26 부스 설계 M3
s = slide(); content_header(s, "Ⅲ. 기술·기능 · 기능(부스 설계)", "조립부스 3D 프리뷰·독립부스 3안, 높이·리깅·방염·이격은 사전 검증", "Ⅲ", "REQ-F-011~014")
bullets(s, 0.55, 1.6, 6.9, [
    "조립부스 옵션·3D 프리뷰(REQ-F-011)",
    "독립부스 설계 3안·선택/병합·버전(REQ-F-012)",
    "설계 규정 사전검증 — 높이 5m·리깅 6.5~8.5m·방염·이격(REQ-F-013)",
    "도면 업로드 비전 추출·검증(REQ-F-014·A-020)",
], dot=PURPLE, gap=0.56, size=10.5)
checks=[("높이 ≤ 5m",GREEN),("리깅 6.5~8.5m",GREEN),("방염 자재",GREEN),("이격 거리",AMBER)]
for i,(t,cl) in enumerate(checks):
    y=4.1+i*0.42
    icon(s, "check-verify", 0.6, y, 0.28, cl)
    text(s, 0.95, y, 3.0, 0.34, t, size=10, color=INK, anchor=MSO_ANCHOR.MIDDLE)
    chip(s, 3.6, y+0.02, 1.3, 0.28, "사전검증" if cl==GREEN else "확인", cl, size=8)
screen_panel(s, "SCR-06", 7.7, 1.6, 5.08, 3.7, "부스 설계 스튜디오 — 옵션·3D 프리뷰·규정 체크")
caption(s, "설계 3안 + 규정 체크리스트 — 높이·리깅·방염·이격을 설계 단계에서 사전 검증.")

# S27 유틸리티 배선 (도식 ⑥)
s = slide(); content_header(s, "Ⅲ. 기술·기능 · 기능(유틸리티 배선)", "기기→kW→분전반 자동 산출, 트렌치→분전반 최단 배선을 PostGIS가 그린다", "Ⅲ", "REQ-F-015·016")
rect(s, 0.55, 1.6, 8.6, 4.6, fill=HALL_BG, line=LINE, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
import random as _r
_r.seed(7)
# 부스 폴리곤
for gx in range(6):
    for gy in range(4):
        rect(s, 0.95+gx*1.3, 1.95+gy*1.0, 1.0, 0.7, fill=POLY, line=WHITE, line_w=0.6)
# 분전반
rect(s, 8.35, 3.7, 0.34, 0.34, fill=INK)
text(s, 7.75, 4.08, 1.0, 0.24, "분전반", size=8, color=INK, bold=True, align=PP_ALIGN.CENTER)
# 트렌치 포인트 + 배선
trench=[(1.6,3.0),(3.3,4.2),(5.2,2.6),(6.6,4.3)]
for tx,ty in trench:
    rect(s, tx-0.1, ty-0.1, 0.2, 0.2, fill=TEAL, shape=MSO_SHAPE.DIAMOND)
cablecols=[(RED,"solid"),(CYAN,"dash"),(TEAL,"sysDot")]
srcs=[(1.45,2.3),(4.2,2.3),(3.9,4.3)]
for i,(sx,sy) in enumerate(srcs):
    tx,ty=trench[i]; cl,ds=cablecols[i]
    line_conn(s, sx, sy, tx, ty, color=cl, w=2.0, dash=(None if ds=="solid" else ds))
    line_conn(s, tx, ty, 8.5, 3.87, color=cl, w=2.0, dash=(None if ds=="solid" else ds))
chip(s, 5.5, 2.35, 1.9, 0.3, "가정 좌표(실측 대기)", AMBER, size=7.5)
# 우측 패널
titlecard(s, 9.35, 1.6, 3.43, 4.6, "배선 색상 범례", TEAL, [], headfill=BLUE)
for i,(nm,st,cl,ds) in enumerate([("전기","RED 실선",RED,"solid"),("네트워크","CYAN 파선",CYAN,"dash"),("급배수","TEAL 점선",TEAL,"sysDot")]):
    yy=2.35+i*0.5
    rect(s, 9.6, yy+0.1, 0.7, 0.06, fill=cl, dash=(None if ds=="solid" else ds))
    text(s, 10.45, yy, 2.2, 0.34, [[{"t":nm+" ","color":INK,"bold":True,"size":10},{"t":st,"color":MUTED,"size":8.5}]], anchor=MSO_ANCHOR.MIDDLE)
notes=["최근접 트렌치 KNN (geom <-> point)","최단 경로 ST_Length · 통로 횡단 최소","SRID 0 홀 로컬 미터 · GiST 실시간"]
for i,nt in enumerate(notes):
    yy=4.05+i*0.6
    rect(s, 9.6, yy+0.06, 0.09, 0.09, fill=BLUE, shape=MSO_SHAPE.OVAL)
    text(s, 9.8, yy, 2.85, 0.55, nt, size=8.6, color=INK, line_spacing=0.95)
caption(s, "홀별 평면도 15장 + CAD 확보 [검증됨] · 미확보 구간은 '가정 좌표(실측 대기)' 라벨(R-2).")

# S28 위치표시도·유틸리티 신청
s = slide(); content_header(s, "Ⅲ. 기술·기능 · 기능(위치표시도)", "좌표 클릭으로 위치표시도를 자동 작도하고, 신청·견적은 D-25 역산으로 챙긴다", "Ⅲ", "REQ-F-017·018")
bullets(s, 0.55, 1.6, 6.9, [
    "위치표시도 자동생성 — 좌표 클릭 자동 작도, 수기 작도 폐지(REQ-F-017)",
    "네트워크·급배수·압축공기 신청 + 마감 리마인더(D-25 역산)(REQ-F-018)",
    "자동 견적 연동 — 신청과 동시에 요율 기반 견적 산출",
], dot=BLUE, gap=0.6, size=11)
screen_panel(s, "SCR-08", 7.7, 1.6, 5.08, 3.7, "유틸리티 신청 요약·위치표시도")
caption(s, "수기 위치표시도 폐지 — 좌표 클릭 자동 작도로 오류·리드타임 제거.")

# ---- P3-D 옥션 ----
# S29 옥션 폐루프 (도식 ⑦)
s = slide(); content_header(s, "Ⅲ. 기술·기능 · 기능(옥션 폐루프)", "AI가 만든 설계 자료가 곧 응찰 근거이고, 한 흐름에서 발주까지 끝난다", "Ⅲ", "REQ-F-025·026·031")
# 좌측 AI 자료 스택
rect(s, 0.55, 2.0, 2.7, 3.4, fill=PURPLE_LT, line=PURPLE, line_w=1.3, shape=MSO_SHAPE.ROUNDED_RECTANGLE, shadow=True)
icon(s, "ai-sparkle", 1.6, 2.15, 0.5, PURPLE)
text(s, 0.7, 2.72, 2.4, 0.3, "AI 자료 패키지", size=11, color=PURPLE, bold=True, align=PP_ALIGN.CENTER)
for i,t in enumerate(["M2 배치","M3 설계","M4 배선/BOQ","M5 시공예측 이미지"]):
    yy=3.15+i*0.5
    rect(s, 0.75, yy, 2.3, 0.4, fill=WHITE, line=PURPLE, line_w=0.8, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
    text(s, 0.75, yy, 2.3, 0.4, t, size=9, color=INK, bold=True, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)
flow=[("옥션 개설\n(역경매/RFQ/고정가)",BLUE,"auction-gavel"),
      ("등록업체 검증 게이트\n미등록 응찰 차단 403",RED,"shield-security"),
      ("견적서 제출\nPDF·버전",BLUE,"document-rtm"),
      ("실시간 순위·라운드 마감\nRedis 정렬셋·WS 델타",BLUE,"dashboard-chart"),
      ("종합평가 낙찰\n가격+평판+납기",PURPLE,"check-verify"),
      ("Award → 계약/발주 자동전환\nM6·M9",TEAL,"closed-loop")]
positions=[(3.55,2.0),(6.65,2.0),(9.75,2.0),(9.75,3.75),(6.65,3.75),(3.55,3.75)]
fw=2.9; fh=1.4
for i,((t,cl,icn),(x,y)) in enumerate(zip(flow,positions)):
    rect(s, x, y, fw, fh, fill=(PURPLE_LT if cl==PURPLE else (RGBColor(0xFC,0xE9,0xE7) if cl==RED else (RGBColor(0xE1,0xF3,0xF1) if cl==TEAL else BLUE_LT))),
         line=cl, line_w=(2.0 if cl==RED else 1.2), shape=MSO_SHAPE.ROUNDED_RECTANGLE, shadow=True)
    icon(s, icn, x+0.14, y+0.14, 0.34, cl)
    text(s, x+0.56, y+0.12, fw-0.6, fh-0.2, t, size=9.3, color=(INK), bold=True, line_spacing=0.95, anchor=MSO_ANCHOR.MIDDLE)
    text(s, x+fw/2-0.12, y-0.02, 0.3, 0.4, str(i+1), size=9, color=WHITE, bold=True, align=PP_ALIGN.CENTER)
    chip(s, x+fw/2-0.15, y-0.02, 0.3, 0.3, str(i+1), cl, size=8)
line_conn(s, 3.25, 3.0, 3.55, 2.7, color=PURPLE, w=1.5)
caption(s, "AI 설계자료가 곧 발주 근거. 사진 수준 자료로 응찰→발주까지 한 흐름  [구현 계획: Phase D-M15].")

# S30 역경매 순위
s = slide(); content_header(s, "Ⅲ. 기술·기능 · 기능(역경매)", "라운드·마감·최저가·내 위치를 실시간으로, 자동마감까지", "Ⅲ", "REQ-F-027")
rows=[("A 장치","1,240만원","4.2점","D+3","1위"),("B 시공","1,285만원","4.5점","D+2","2위"),
      ("C 종합","1,310만원","4.1점","D+4","3위"),("D 부스","1,360만원","3.9점","D+3","4위")]
table(s, 0.55, 1.7, 7.6, ["업체","현재 최저가","평판","납기","순위"], rows, [1.9,1.9,1.2,1.2,1.4], rowh=0.5, hl_rows={0})
titlecard(s, 8.35, 1.7, 4.43, 3.5, "실시간 메커니즘", BLUE, [
    "실시간 순위 = Redis 정렬셋 + WebSocket 델타 푸시",
    "라운드 마감 = Redis 타이머(자동마감)",
    "익명 옵션 · 내 위치 표시",
    "스캐폴드 4계층·큐·WS 패턴 확정 [구현 계획]",
], headfill=BLUE, bodysize=9.5, igap=0.6)
chip(s, 0.55, 4.4, 3.0, 0.5, "잔여 시간 02:14:37", RED, size=13)
caption(s, "실시간 순위 보드 — 업체 순위·최저가·잔여 시간, 라운드 자동마감.")

# S31 종합평가 낙찰 (도식 ⑧)
s = slide(); content_header(s, "Ⅲ. 기술·기능 · 기능(종합평가 낙찰)", "가격만이 아니라 평판·납기까지 가중 스코어로 투명하게 낙찰한다", "Ⅲ", "REQ-F-028·030·031")
text(s, 0.55, 1.5, 12.2, 0.3, "가중치(예): 가격 0.6 / 평판 0.25 / 납기 0.15 — 옥션별 설정형(weight jsonb)", size=9.5, color=MUTED)
rows=[("B 시공","1,285만원(92)","4.5(90)","D+2(95)","91.6","1위"),
      ("A 장치","1,240만원(96)","4.2(84)","D+3(88)","91.2","2위"),
      ("C 종합","1,310만원(88)","4.1(82)","D+4(80)","85.3","3위"),
      ("D 부스","1,360만원(80)","3.9(78)","D+3(88)","81.3","4위")]
table(s, 0.55, 1.9, 12.23, ["업체","견적금액(점수)","평판(점수)","납기(점수)","가중 종합점수","순위"],
      rows, [2.2,3.0,2.2,2.2,1.83,0.78], rowh=0.56, hl_rows={0})
# 낙찰 배지
icon(s, "check-verify", 11.6, 2.5, 0.3, GREEN)
lead(s, 0.55, 4.8, 12.23, 0.62, "선정 사유·항목별 비교 근거 자동 산출 — 가격만이 아니라 평판·납기 가중으로 투명·최적 발주.", fill=INK, size=13)
bullets(s, 0.55, 5.7, 12.0, [
    "등록업체 AI 매칭 추천(REQ-F-030) · 물량서(BOQ) 자동산출 근거(REQ-F-031)",
], dot=PURPLE, gap=0.4, size=10)
caption(s, "1위(B 시공) = BLUE_LT 강조 + 낙찰(Award). 종합점수 가중합 시각화.", y=6.35)

# S32 등록 게이트·발주 전환
s = slide(); content_header(s, "Ⅲ. 기술·기능 · 기능(등록 게이트·발주)", "미등록 업체는 응찰 자체가 차단(403)되고, 낙찰은 계약·발주로 자동 전환된다", "Ⅲ", "REQ-F-029·032·S-007")
seq=[("응찰 시도",BLUE),("M7 등록 검증\nTB_COMPANY.kintex_registered",PURPLE),("미등록?\nNOT_REGISTERED_COMPANY 403",RED),("등록 확인 → 응찰 허용",GREEN)]
for i,(t,cl) in enumerate(seq):
    x=0.55+i*3.12
    rect(s, x, 1.7, 2.9, 1.1, fill=(RGBColor(0xFC,0xE9,0xE7) if cl==RED else (BLUE_LT if cl==BLUE else (PURPLE_LT if cl==PURPLE else RGBColor(0xE3,0xF6,0xEC)))),
         line=cl, line_w=1.3, shape=MSO_SHAPE.ROUNDED_RECTANGLE, shadow=True)
    text(s, x+0.1, 1.7, 2.7, 1.1, t, size=9.5, color=INK, bold=True, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE, line_spacing=0.95)
    if i<3: text(s, x+2.9-0.05, 1.7, 0.24, 1.1, "›", size=16, color=SLATE, bold=True, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)
lead(s, 0.55, 3.2, 12.23, 0.7, "킨텍스 '등록업체 시공 필수' 규정을 시스템이 강제 — 미등록 = 응찰 원천 차단(403).", fill=RED, size=13)
titlecard(s, 0.55, 4.15, 12.23, 2.15, "발주 자동전환", TEAL, [
    "Award → 계약/발주 자동전환(M6 서류·M9 정산 연동)",
    "이벤트 디커플 — 모듈 간 직접 호출 없이 이벤트로 연동, 순환 의존 금지",
    "낙찰 확정 시점에 계약서·발주서·납부 스케줄 자동 생성 흐름으로 연결",
], headfill=TEAL, bodysize=10, igap=0.5)
caption(s, "등록 검증 시퀀스 + 발주 전환 — 규정 강제와 폐루프 완결.")

# ---- P3-E 판매·서류 ----
# S33 판매 M1
s = slide(); content_header(s, "Ⅲ. 기술·기능 · 기능(판매·서류)", "가용성은 실시간 캘린더로, 견적은 규칙엔진으로 즉시, 신청은 웹폼→HWP로", "Ⅲ", "REQ-F-001~006")
bullets(s, 0.55, 1.6, 6.9, [
    "홀/반홀 가용성 실시간 캘린더(REQ-F-001)",
    "규칙엔진 자동 견적 — 2,250원/㎡ [협의: C4 요율 공식본](REQ-F-002)",
    "배정신청 웹폼→HWP 자동생성(REQ-F-003)",
    "부스 판매 인벤토리·온라인 셀프 선택(REQ-F-004·005)",
    "임대계약 전자서명(선택, REQ-F-006)",
], dot=BLUE, gap=0.56, size=10.5)
screen_panel(s, "SCR-02", 7.7, 1.6, 5.08, 3.7, "주최자 대시보드 — 행사·가용성·신청 현황")
caption(s, "견적 수일 대기 → 즉시(규칙엔진). 신청 웹폼→HWP 자동생성으로 수작업 제거.")

# S34 서류 M6
s = slide(); content_header(s, "Ⅲ. 기술·기능 · 기능(서류·마일스톤)", "D-150/30/25/7을 역산해 알리고, 웹폼은 HWP/PDF로, 검수는 AI가 돕는다", "Ⅲ", "REQ-F-020~024·A-011·012")
mile=[("D-150","판매·계약",BLUE),("D-30","배치·설계 마감",AMBER),("D-25","유틸리티 신청",AMBER),("D-7","제출·검수",RED)]
line_conn(s, 0.9, 2.35, 8.2, 2.35, color=SLATE, w=1.5)
for i,(d,t,cl) in enumerate(mile):
    x=0.7+i*1.9
    rect(s, x+0.75, 2.22, 0.26, 0.26, fill=cl, shape=MSO_SHAPE.OVAL)
    chip(s, x+0.3, 1.65, 1.15, 0.4, d, cl, size=11)
    text(s, x, 2.6, 1.85, 0.5, t, size=9, color=INK, bold=True, align=PP_ALIGN.CENTER, line_spacing=0.9)
bullets(s, 0.55, 3.4, 8.0, [
    "마일스톤 자동생성·역산 알림(F-020) · 신고서류 웹폼→HWP/PDF 7종(F-021)",
    "서류 버전·전자결재 워크플로(F-023) · OCR 계약·납품서 자동등록(F-024·A-020)",
    "AI 서류 검수(누락·불일치)(A-011) · 규정 자연어 챗봇(근거·인용)(A-012)",
    "kxwp 제출 릴레이 — 웹폼→HWP/PDF까지 완결, 릴레이만 킨텍스 IT 협의 [협의: C2, R-3](F-022)",
], dot=PURPLE, gap=0.55, size=10)
screen_panel(s, "SCR-10", 9.0, 3.35, 3.78, 2.95, "홀매니저 승인 큐 — 서류·전자결재")
caption(s, "D-데이 역산 알림 + 웹폼→HWP/PDF + AI 검수 — 서류 리드타임·오류 절감.")

# ---- P3-F 관람·참가 ----
# S35 관람 M10
s = slide(); content_header(s, "Ⅲ. 기술·기능 · 기능(관람·등록)", "사전등록→모바일 배지→현장 체크인→리드까지, 오프라인에서도 끊기지 않는다", "Ⅲ", "REQ-F-037~044·A-018")
journey=[("사전등록","유형별·중복검증",BLUE,"document-rtm"),("모바일 배지/QR","발급",PURPLE,"mobile"),
         ("현장 체크인","QR·즉석 인쇄·집계",BLUE,"visitor-badge"),("리드 캡처","AI 스코어링·분류",PURPLE,"dashboard-chart")]
jw=2.9
for i,(t,d,cl,icn) in enumerate(journey):
    x=0.55+i*(jw+0.14)
    card(s, x, 1.65, jw, 2.0)
    rect(s, x, 1.65, jw, 0.08, fill=cl, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
    icon(s, icn, x+jw/2-0.28, 1.9, 0.56, cl)
    text(s, x+0.1, 2.6, jw-0.2, 0.4, t, size=11.5, color=cl, bold=True, align=PP_ALIGN.CENTER)
    text(s, x+0.1, 3.05, jw-0.2, 0.5, d, size=9, color=MUTED, align=PP_ALIGN.CENTER)
    if i<3: text(s, x+jw-0.02, 1.65, 0.2, 2.0, "›", size=16, color=SLATE, bold=True, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)
chip(s, 4.8, 3.85, 3.7, 0.44, "오프라인 체크인 폴백 (REQ-F-040)", GREEN, size=10)
bullets(s, 0.55, 4.55, 12.0, [
    "참가업체 리드캡처(F-041) · AI 리드 스코어링·분류(F-042·A-018) · 세그먼트 EDM(F-043)",
    "티켓발권·AI폼빌더·스마트배지(선택, F-044)",
], dot=PURPLE, gap=0.45, size=10)
caption(s, "등록→체크인→리드 여정 — 오프라인에서도 끊기지 않는 현장 폴백.")

# S36 비즈매칭·CMS·공개사이트
s = slide(); content_header(s, "Ⅲ. 기술·기능 · 기능(비즈매칭·공개)", "AI가 매칭을 추천하고, 다국어 공개사이트·마이크로사이트가 SEO로 관람객을 모은다", "Ⅲ", "REQ-F-045~051·067~069")
titlecard(s, 0.55, 1.6, 6.05, 3.1, "비즈매칭·마케팅 (M11)", PURPLE, [
    "AI 비즈매칭 추천(F-045·A-018)",
    "미팅 슬롯·성과 리포트(F-046)",
    "이벤트 모바일앱·세션·채팅·설문(F-047)",
    "세그먼트 EDM·캠페인 자동화(F-050)·스폰서십(F-051)",
], headfill=PURPLE, bodysize=9.8, igap=0.52)
titlecard(s, 6.75, 1.6, 6.03, 3.1, "CMS·공개사이트 (M12·M17)", BLUE, [
    "공개 홍보 사이트(SEO·다국어)(F-048)",
    "공개 인터랙티브 플로어플랜(F-049)",
    "CMS 게시 워크플로·버전(F-067)·마이크로사이트(F-068)",
    "다국어 콘텐츠·hreflang(F-069) · AI 카피·번역(A-015)",
], headfill=BLUE, bodysize=9.8, igap=0.52)
lead(s, 0.55, 5.0, 12.23, 0.62, "AI 매칭으로 성과를 높이고, 다국어 SEO 공개사이트로 관람객·바이어를 모은다.", fill=INK, size=13)
caption(s, "공개사이트 + 마이크로사이트 + AI 매칭 — 참가·관람 유입 확대.")

# ---- P3-G 정산·BI·현장 ----
# S37 정산 M9
s = slide(); content_header(s, "Ⅲ. 기술·기능 · 기능(정산·결제)", "납부는 스케줄로, 결제는 PG로, 예치금은 실사용까지 투명하게 정산한다", "Ⅲ", "REQ-F-056~060")
flow=[("납부 스케줄\n자동생성·알림",BLUE),("PG 결제\n유틸리티·부대",PURPLE),("예치금\n실사용 정산",TEAL),("세금계산서\n수수료 정산",BLUE)]
for i,(t,cl) in enumerate(flow):
    x=0.55+i*3.12
    rect(s, x, 1.7, 2.9, 1.3, fill=(PURPLE_LT if cl==PURPLE else (RGBColor(0xE1,0xF3,0xF1) if cl==TEAL else BLUE_LT)),
         line=cl, line_w=1.3, shape=MSO_SHAPE.ROUNDED_RECTANGLE, shadow=True)
    text(s, x+0.1, 1.7, 2.7, 1.3, t, size=11, color=INK, bold=True, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE, line_spacing=1.0)
    if i<3: text(s, x+2.9-0.05, 1.7, 0.24, 1.3, "›", size=16, color=SLATE, bold=True, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)
bullets(s, 0.55, 3.4, 12.0, [
    "납부 스케줄 자동생성·알림(F-056) · 유틸리티·부대 PG 결제(F-057)",
    "예치금 실사용 정산 투명화(F-058) · 세금계산서·옥션 수수료 정산(F-059)",
    "환불·취소 규정 자동(선택, F-060)",
], dot=BLUE, gap=0.5, size=10.5)
lead(s, 0.55, 5.1, 12.23, 0.62, "예치금은 실사용까지 투명 정산 — PG 결제·세금계산서·옥션 수수료를 한 흐름에.", fill=INK, size=13)
caption(s, "정산 흐름 — 납부→결제→예치금 정산→세금계산서.")

# S38 BI-1
s = slide(); content_header(s, "Ⅲ. 기술·기능 · 기능(경영분석 BI)", "가동률·매출구성·행사별 P&L·㎡당 수익을 운영사 관점 한 화면에", "Ⅲ", "REQ-F-061~063")
bullets(s, 0.55, 1.6, 6.9, [
    "홀·기간별 가동률 대시보드(REQ-F-061)",
    "매출 구성·행사별 P&L(REQ-F-062)",
    "전시장 ROI·RevPAD·㎡당 수익(REQ-F-063)",
    "BI 집계·조회는 읽기복제 오프로드(운영 프라이머리 보호)",
], dot=BLUE, gap=0.58, size=10.5)
screen_panel(s, "SCR-13", 7.7, 1.6, 5.08, 3.7, "경영분석 대시보드 — 가동률·매출·P&L·RevPAD")
caption(s, "운영사 관점 한 화면 — Recharts 대시보드, 읽기복제로 운영 부하 격리.")

# S39 BI-2
s = slide(); content_header(s, "Ⅲ. 기술·기능 · 기능(경영분석 BI)", "리텐션·LTV·참가사 ROI를 권한 격리로, AI가 수요를 예측하고 KPI를 브리핑한다", "Ⅲ", "REQ-F-064~066·A-013·014")
bullets(s, 0.55, 1.6, 7.0, [
    "참가사 리텐션·LTV 코호트(REQ-F-064)",
    "참가업체 관점 ROI — 권한 격리(자사 데이터만)(REQ-F-065)",
    "경영진 KPI 대시보드·드릴다운(REQ-F-066)",
    "AI 수요예측·수율/동적가격(REQ-A-013)",
    "AI KPI 브리핑·자연어 조회 Text-to-SQL(REQ-A-014)",
], dot=PURPLE, gap=0.55, size=10.5)
titlecard(s, 7.85, 1.6, 4.93, 3.4, "데이터 거버넌스", TEAL, [
    "BI 마트는 개인 식별자 비반입(집계만)",
    "참가업체는 자사 데이터만(격리)",
    "AI 인사이트는 근거·인용 병기",
], headfill=TEAL, bodysize=10, igap=0.6)
caption(s, "KPI 드릴다운 + AI 인사이트 — 권한 격리와 개인정보 비반입 원칙 준수.")

# S40 현장운영·물류
s = slide(); content_header(s, "Ⅲ. 기술·기능 · 기능(현장운영·물류)", "반입/반출 슬롯·통행증 QR·wayfinding·이상탐지 — 선택 기능은 시설 협의 전제로", "Ⅲ", "REQ-F-033~036·052~055")
bullets(s, 0.55, 1.6, 6.9, [
    "반입/반출 슬롯 예약·통행증 QR·중량물 우선(F-033·034)",
    "지게차·부대장비 렌탈·철거 대기열 시뮬(F-035·036)",
    "실내 wayfinding·POI·경로(F-052)",
    "혼잡 모니터·예측·전력부하·주차(F-053·054·A-019)",
    "안전 위반·이상탐지(F-055·A-019) · 사이니지 배포(F-070)",
], dot=BLUE, gap=0.55, size=10.5)
screen_panel(s, "SCR-14", 7.7, 1.6, 5.08, 3.0, "홀 현장운영 대시보드 — 혼잡·전력·안전·물류")
rect(s, 7.7, 4.75, 5.08, 0.85, fill=RGBColor(0xFF,0xF6,0xE8), line=AMBER, line_w=1.0, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
text(s, 7.85, 4.75, 4.8, 0.85, "[협의] wayfinding 비콘·IoT·사이니지 HW는 킨텍스 시설 인프라 협의 전제(C7, R-7). 미협의 시에도 코어 가치(WT-1~4)는 HW 독립.", size=8.8, color=AMBER, bold=True, anchor=MSO_ANCHOR.MIDDLE, line_spacing=0.95)
caption(s, "현장 운영 상황판 — 선택 기능(비콘·IoT·사이니지)은 시설 협의 전제, 코어는 HW 독립.")

# S41 전체 아키텍처 (도식 ⑨)
s = slide(); content_header(s, "Ⅲ. 기술·기능 · 시스템 장비구성", "확정 스택으로 이미 기동 중 — 도상이 아니라 동작하는 아키텍처다", "Ⅲ", "REQ-A-016·F-081")
band(1.45, 0.5, BLUE_LT, BLUE, "엣지/DMZ")
nd(2.15,1.5,4.3,0.4,"CDN·정적캐시",WHITE,INK,BLUE); nd(6.55,1.5,4.5,0.4,"WAF/Reverse Proxy(nginx)·TLS·레이트리밋",WHITE,INK,BLUE)
band(2.02, 0.5, BLUE_LT, BLUE, "역할별\n프론트")
for i,p in enumerate(["organizer","exhibitor","contractor","ops","admin","public/visitor"]):
    nd(2.15+i*1.5,2.07,1.42,0.4,p,WHITE,(MUTED if p=="admin" else INK),(SLATE if p=="admin" else BLUE),7.6)
band(2.59, 0.46, BLUE, BLUE_DK, "SSO·RBAC")
nd(2.15,2.62,8.9,0.4,"JWT SSO + TOTP 2FA · 이중 RBAC(플랫폼/행사)",BLUE_LT,BLUE_DK,BLUE)
band(3.13, 0.86, WHITE, BLUE, "공유\n백엔드")
nd(2.15,3.2,8.9,0.34,"REST + WebSocket / STOMP",BLUE_LT,BLUE_DK,BLUE)
for i,t in enumerate(["룰엔진","배치·배선·PostGIS","옥션 M15","BI M16","CMS M17","공개 API(RO)"]):
    nd(2.15+i*1.49,3.58,1.42,0.36,t,WHITE,INK,BLUE,7.4)
band(4.07, 0.5, PURPLE_LT, PURPLE, "비동기")
nd(2.15,4.12,3.0,0.4,"Redis 큐·순위·타이머·쿼터",WHITE,INK,PURPLE)
nd(5.3,4.12,2.9,0.4,"나노바나나 워커",PURPLE_LT,PURPLE,PURPLE); nd(8.3,4.12,2.75,0.4,"서류·EDM 워커",WHITE,INK,PURPLE)
band(4.64, 0.5, HALL_BG, BLUE_DK, "데이터")
nd(2.15,4.69,3.3,0.4,"PostgreSQL + PostGIS",WHITE,INK,BLUE_DK); nd(5.55,4.69,2.7,0.4,"읽기복제(BI·공개)",WHITE,INK,BLUE_DK); nd(8.35,4.69,2.7,0.4,"오브젝트 스토리지",WHITE,INK,BLUE_DK)
# 우측 외부 게이트 컬럼
rect(s, 11.35, 1.45, 1.43, 3.69, fill=INK, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
text(s, 11.35, 1.52, 1.43, 0.3, "외부 게이트", size=8.5, color=WHITE, bold=True, align=PP_ALIGN.CENTER)
for i,(g,live) in enumerate([("Gemini",True),("Claude",False),("PG 결제",False),("kxwp 릴레이",False),("등록업체 DB",False)]):
    yy=1.9+i*0.62
    rect(s, 11.45, yy, 1.23, 0.5, fill=WHITE, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
    text(s, 11.45, yy+0.02, 1.23, 0.3, g, size=8, color=INK, bold=True, align=PP_ALIGN.CENTER)
    if live: chip(s, 11.6, yy+0.26, 0.95, 0.2, "라이브", GREEN, size=6.5)
caption(s, "6밴드 계층 + 외부 게이트 — 인프라·CI/CD·인증·PostGIS 코어·나노바나나 [검증됨].")

# S42 단일 공간원천
s = slide(); content_header(s, "Ⅲ. 기술·기능 · 데이터 요구사항", "부스 폴리곤·트렌치 포인트·배선 LineString은 하나의 원천에서만 산다", "Ⅲ", "REQ-N-008·017")
rect(s, 4.4, 1.7, 4.5, 1.0, fill=PURPLE, shape=MSO_SHAPE.ROUNDED_RECTANGLE, shadow=True)
icon(s, "postgis-spatial", 4.6, 1.9, 0.6, WHITE)
text(s, 5.3, 1.78, 3.5, 0.5, "PostGIS 단일 공간 원천", size=13, color=WHITE, bold=True)
text(s, 5.3, 2.24, 3.5, 0.4, "SRID 0 홀 로컬 미터 · GiST 인덱스", size=9, color=SUBLT)
srcs=[("M2 부스 폴리곤","booth-layout"),("M4 트렌치 포인트","postgis-spatial"),("M4 배선 LineString","wiring-power")]
for i,(t,icn) in enumerate(srcs):
    x=1.0+i*3.9
    rect(s, x, 3.4, 3.5, 0.9, fill=BLUE_LT, line=BLUE, line_w=1.2, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
    icon(s, icn, x+0.2, 3.6, 0.44, BLUE)
    text(s, x+0.8, 3.4, 2.6, 0.9, t, size=10.5, color=BLUE_DK, bold=True, anchor=MSO_ANCHOR.MIDDLE)
    line_conn(s, x+1.75, 3.4, 6.65, 2.7, color=SLATE, w=1.0)
bullets(s, 0.55, 4.7, 12.0, [
    "M2 폴리곤·M4 배선 매퍼가 권위 — 소비 모듈은 조회만·중복 저장 금지(REQ-N-017)",
    "전 geom GiST 인덱스로 공간 쿼리 실시간(REQ-N-008)",
    "검증 게이트 — ArchUnit·계약 검수로 중복 저장 방지",
], dot=PURPLE, gap=0.5, size=10.5)
caption(s, "공간 데이터 단일 원천 — 검증·시각화·정산·BI·wayfinding이 하나의 원천을 소비.")

# S46 보안 개요
s = slide(); content_header(s, "Ⅲ. 기술·기능 · 보안 요구사항", "AI가 만든 것은 표시하고, 남의 데이터는 차단한다 — REQ-S 20항목 전량 커버", "Ⅲ", "REQ-S 전량")
inv=[("외부 API 격리","Ollama 기본·Claude/Gemini만 승인 예외, egress DROP",BLUE_DK,"shield-security"),
     ("워터마크 항상","AI 생성 이미지 watermarkRequired 불변 [검증됨]",PURPLE,"watermark"),
     ("PII 격리","동의·마스킹·암호화·응답 제외·테넌트 격리",BLUE,"check-verify"),
     ("테넌트 fail-closed","한 층이라도 불일치면 차단",TEAL,"tenant-building")]
cw=2.94
for i,(t,d,cl,icn) in enumerate(inv):
    x=0.55+i*(cw+0.16)
    card(s, x, 1.6, cw, 2.5)
    rect(s, x, 1.6, cw, 0.08, fill=cl, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
    icon(s, icn, x+cw/2-0.3, 1.85, 0.6, cl)
    text(s, x+0.1, 2.6, cw-0.2, 0.5, t, size=11, color=cl, bold=True, align=PP_ALIGN.CENTER, line_spacing=0.9)
    text(s, x+0.15, 3.15, cw-0.3, 0.9, d, size=8.8, color=INK, align=PP_ALIGN.CENTER, line_spacing=1.0)
lead(s, 0.55, 4.45, 12.23, 0.62, "보안 성숙도 — 인증·워터마크·admin env는 [검증됨](설계가 아니라 동작하는 통제).", fill=INK, size=13)
kcards=[("100%","REQ-S 커버리지",GREEN),("4대","불변 원칙",BLUE),("20","보안 요구항목",PURPLE)]
for i,(v,l,cl) in enumerate(kcards):
    x=0.55+i*4.11
    rect(s, x, 5.35, 3.97, 0.9, fill=cl, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
    text(s, x+0.3, 5.45, 1.5, 0.7, v, size=26, color=WHITE, bold=True, anchor=MSO_ANCHOR.MIDDLE)
    text(s, x+2.0, 5.35, 1.8, 0.9, l, size=11, color=WHITE, bold=True, anchor=MSO_ANCHOR.MIDDLE)
caption(s, "불변 원칙 4대 + REQ-S 20항목 커버리지 100%.", y=6.55)

# S47 보안영역 분리 (도식 ⑩)
s = slide(); content_header(s, "Ⅲ. 기술·기능 · 보안 요구사항", "공개 DMZ는 쓰기 권한이 없고, 데이터존은 아웃바운드가 0이다", "Ⅲ", "REQ-S-020·018·N-018")
zones=[("인터넷",SLATE),("엣지\nCDN·WAF·DDoS",BLUE_LT),("DMZ(공개)\n공개LB·visitor GW\n쓰기 없음",BLUE),
       ("내부망(인증)\nSSO/2FA·공유 백엔드",BLUE_DK),("AI 워커존\nRedis·나노바나나·Ollama\negress 제한",PURPLE),
       ("데이터존(최내곽)\nPostgreSQL+PostGIS\n아웃바운드 전면 차단",INK)]
zw=1.9
for i,(t,cl) in enumerate(zones):
    x=0.55+i*(zw+0.06)
    tc = INK if cl in (BLUE_LT,) else WHITE
    fl = cl if cl not in (SLATE,) else RGBColor(0xE9,0xEC,0xF1)
    rect(s, x, 2.3, zw, 2.7, fill=(fl if cl!=SLATE else RGBColor(0xE9,0xEC,0xF1)), line=(cl if cl!=BLUE_LT else BLUE), line_w=1.0+i*0.2, shape=MSO_SHAPE.ROUNDED_RECTANGLE, shadow=True)
    text(s, x+0.08, 2.4, zw-0.16, 2.5, t, size=8.6, color=(INK if cl in (SLATE,BLUE_LT) else WHITE), bold=True, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE, line_spacing=1.0)
    if i<5: text(s, x+zw-0.04, 3.4, 0.14, 0.5, "›", size=13, color=SLATE, bold=True, align=PP_ALIGN.CENTER)
# 상단 관리존
rect(s, 0.55, 1.55, 12.23, 0.5, fill=NAVY, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
text(s, 0.55, 1.55, 12.23, 0.5, "관리존 — 배스천·관측성 · VPN/허용IP+2FA · 인터넷 미도달", size=10, color=WHITE, bold=True, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)
# 철칙 배지
for i,t in enumerate(["DMZ → 데이터존 직접 접근 절대 금지","데이터존 아웃바운드 0 (유출경로 제거)","egress 승인 4목적지만 — Claude·Gemini·PG·SMTP, 그 외 DROP"]):
    x=0.55+i*4.11
    rect(s, x, 5.25, 3.97, 0.7, fill=RGBColor(0xFC,0xE9,0xE7), line=RED, line_w=1.4, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
    text(s, x+0.1, 5.25, 3.77, 0.7, t, size=8.8, color=RED, bold=True, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE, line_spacing=0.92)
caption(s, "공개/내부 물리·논리 분리 · 데이터존 무egress로 유출경로 자체 제거.")

# S48 인증·접근통제
s = slide(); content_header(s, "Ⅲ. 기술·기능 · 보안 요구사항", "JWT+2FA는 이미 라이브, admin 비번은 env로 재시드, RBAC는 3중이다", "Ⅲ", "REQ-S-004~008·F-071")
bullets(s, 0.55, 1.6, 7.0, [
    "JWT(HS256)+TOTP 2FA(RFC6238)+로그인 실패 잠금 [검증됨: Flyway V7/V8]",
    "admin 비번 env 재시드(AES-256-GCM, admin123 금지) [검증됨: env 시더]",
    "행사 RBAC 3중 — 테넌트 격리 ⊃ 행사 RBAC ⊃ 열람/편집 게이트",
    "admin/internal 엔드포인트 게이트(REQ-S-008)",
    "미등록 업체 차단(403)과 연계(REQ-S-007)",
], dot=BLUE, gap=0.56, size=10.5)
rect(s, 8.0, 1.6, 4.78, 1.5, fill=INK, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
text(s, 8.15, 1.7, 4.5, 0.35, "RBAC 3중 구조", size=12, color=WHITE, bold=True, align=PP_ALIGN.CENTER)
text(s, 8.15, 2.15, 4.5, 0.9, "테넌트(전시관) 격리\n⊃  행사(Event) RBAC\n⊃  열람/편집 게이트", size=10, color=SUBLT, bold=True, align=PP_ALIGN.CENTER, line_spacing=1.05)
for i,(t,d,cl) in enumerate([("JWT + RBAC","6역할 게이트",BLUE),("2FA / OTP","TOTP RFC6238 [검증됨]",PURPLE),("RBAC·감사 백오피스","F-071",BLUE_DK)]):
    y=3.4+i*0.9
    rect(s, 8.0, y, 4.78, 0.75, fill=WHITE, line=cl, line_w=1.3, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
    text(s, 8.2, y+0.1, 4.4, 0.3, t, size=11, color=cl, bold=True)
    text(s, 8.2, y+0.42, 4.4, 0.28, [_segify(d)[0] if _segify(d) else {"t":d}] if False else _segify(d), size=9, color=MUTED)
caption(s, "JWT+2FA·admin env 재시드는 [검증됨: Flyway V7/V8] — 설계가 아니라 동작하는 통제.")

# S49 AI·이미지 보안
s = slide(); content_header(s, "Ⅲ. 기술·기능 · 보안 요구사항", "생성 이미지는 워터마크·금지 고지 불변, 외부 API 키는 워커 env에만 산다", "Ⅲ", "REQ-S-001·002·009")
titlecard(s, 0.55, 1.6, 6.05, 3.5, "워터마크 항상 [검증됨]", RED, [
    "M5 응답 watermarkRequired:true + text + notice",
    "계약·심사 서류 사용 금지 고지 — 제거 불가",
    "RAG 근거·인용·환각 차단(abstain)(A-017)",
], headfill=RED, bodysize=10, igap=0.6)
titlecard(s, 6.75, 1.6, 6.03, 3.5, "외부 API 격리", BLUE_DK, [
    "Ollama 기본 + Claude/Gemini만 승인 예외, egress 그 외 DROP(S-001)",
    "GEMINI_API_KEY 워커 env only(백엔드 미취급)(S-002)",
    "워커존 단일 egress — 유출 경로 최소화",
], headfill=BLUE_DK, bodysize=10, igap=0.6)
wm_badge(s, 0.55, 5.35, 12.23, 0.5)
caption(s, "워터마크 실물 + 외부 API 격리(워커존 단일 egress) — 시크릿·키값 미기재.", y=6.0)

# S50 개인정보·감사
s = slide(); content_header(s, "Ⅲ. 기술·기능 · 보안 요구사항", "PII는 동의·마스킹·암호화·응답 제외, 민감 액션은 전수 감사, 코드는 안전하게", "Ⅲ", "REQ-S-010~017·N-018")
titlecard(s, 0.55, 1.6, 6.05, 4.5, "개인정보(PII) 보호", BLUE, [
    "동의 분리(privacy/marketing/share)·최소수집(S-013)",
    "*_enc AES-256-GCM·응답 완전 제외·보존/파기",
    "자격증명·키 완전 제외(응답/로그/커밋 마스킹)(S-003)",
    "스택트레이스 미노출(S-010)·DataAccessException 요약(S-011)",
    "관측성 로그·메트릭·트레이스에 PII 미기록(N-018)",
], headfill=BLUE, bodysize=9.5, igap=0.56)
titlecard(s, 6.75, 1.6, 6.03, 4.5, "감사·안전코딩", PURPLE, [
    "민감 액션 감사 전수(TB_AUDIT_LOG tenant_id·@Audited AOP)(S-012)",
    "입력 검증(S-014)·XSS/CSP(S-015)",
    "인젝션 차단 MyBatis #{}(S-016)",
    "IDOR/CSRF/JWT 회전(S-017)",
    "의존성 SCA CI 게이트(S-019)",
], headfill=PURPLE, bodysize=9.5, igap=0.56)
caption(s, "PII 마스킹·암호화·응답 제외 + 민감 액션 전수 감사 + 안전코딩 통제.")

# S56 격리 아키텍처 (도식 ⑪)
s = slide(); content_header(s, "Ⅲ. 기술·기능 · 시스템 운영(테넌트 격리)", "한 층이라도 불일치하면 차단 — 컨텍스트→서비스→MyBatis→PostGIS 4층 방어", "Ⅲ", "REQ-S-018·N-020")
# 좌측 진입
rect(s, 0.55, 2.0, 3.0, 1.3, fill=PURPLE_LT, line=PURPLE, line_w=1.3, shape=MSO_SHAPE.ROUNDED_RECTANGLE, shadow=True)
icon(s, "tenant-building", 0.75, 2.15, 0.44, PURPLE)
text(s, 0.75, 2.62, 2.7, 0.6, "요청 — 서브도메인(kintex./coex.) + JWT 소속 → 컨텍스트 해소(불일치 시 거부)", size=8.6, color=INK, bold=True, line_spacing=0.9)
line_conn(s, 3.55, 2.65, 4.1, 2.65, color=SLATE, w=1.5)
# 4층 스택
layers=[("① 컨텍스트 필터","서브도메인+소속 불일치 거부",BLUE_LT,BLUE),
        ("② 서비스 가드","tenant 스코프 검증",BLUE,WHITE),
        ("③ MyBatis 인터셉터","tenant_id 자동 주입",BLUE_DK,WHITE),
        ("④ PostGIS/쿼리","tenant_id 선두 복합 인덱스",PURPLE,WHITE)]
for i,(t,d,fl,tc) in enumerate(layers):
    y=1.7+i*1.05
    rect(s, 4.1, y, 5.0, 0.9, fill=fl, line=(fl if tc==WHITE else BLUE), line_w=1.2, shape=MSO_SHAPE.ROUNDED_RECTANGLE, shadow=True)
    text(s, 4.35, y+0.12, 4.6, 0.35, t, size=11, color=tc, bold=True)
    text(s, 4.35, y+0.48, 4.6, 0.32, d, size=9, color=(SUBLT if tc==WHITE else MUTED))
    if i<3: text(s, 6.5, y+0.88, 0.4, 0.22, "▼", size=11, color=SLATE, align=PP_ALIGN.CENTER)
# 우측 데이터 파티션
rect(s, 9.35, 1.7, 3.43, 3.35, fill=WHITE, line=LINE, shape=MSO_SHAPE.ROUNDED_RECTANGLE, shadow=True)
text(s, 9.5, 1.85, 3.1, 0.3, "TB_* WHERE tenant_id = ctx", size=9.5, color=INK, bold=True)
rect(s, 9.55, 2.3, 3.0, 1.0, fill=BLUE_LT, line=BLUE, line_w=1.0, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
text(s, 9.55, 2.3, 3.0, 1.0, "KINTEX (#1)", size=12, color=BLUE_DK, bold=True, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)
rect(s, 9.55, 3.45, 3.0, 1.0, fill=PURPLE_LT, line=PURPLE, line_w=1.0, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
text(s, 9.55, 3.45, 3.0, 1.0, "COEX (#2)", size=12, color=PURPLE_DK, bold=True, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)
rect(s, 0.55, 5.4, 12.23, 0.55, fill=RGBColor(0xFC,0xE9,0xE7), line=RED, line_w=1.4, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
text(s, 0.55, 5.4, 12.23, 0.55, "fail-closed — 한 층이라도 불일치면 차단  [구현 계획: 백필 전 단일테넌트 무중단 → 검증 후 필터 활성화]", size=10.5, color=RED, bold=True, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)
caption(s, "KINTEX #1 검증 → COEX #2 코드 배포 없이 데이터 온보딩. 기능 불변, 데이터·권한만 분리.")

# S57 온보딩·2계층 관리자
s = slide(); content_header(s, "Ⅲ. 기술·기능 · 시스템 운영(온보딩)", "코드 배포 없이 데이터 온보딩 6단계로 새 전시관을 연다", "Ⅲ", "REQ-F-077~079")
steps=["테넌트 생성","홀·구역 입력","요율 룰셋","규정 룰셋","마스터데이터","컨텍스트 활성화"]
sw=1.95
for i,st in enumerate(steps):
    x=0.55+i*(sw+0.1)
    rect(s, x, 1.7, sw, 1.0, fill=BLUE_LT, line=BLUE, line_w=1.2, shape=MSO_SHAPE.ROUNDED_RECTANGLE, shadow=True)
    chip(s, x+sw/2-0.22, 1.55, 0.44, 0.32, str(i+1), BLUE, size=11)
    text(s, x+0.08, 1.95, sw-0.16, 0.7, st, size=10, color=BLUE_DK, bold=True, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE, line_spacing=0.9)
    if i<5: text(s, x+sw-0.03, 1.7, 0.16, 1.0, "›", size=15, color=SLATE, bold=True, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)
titlecard(s, 0.55, 3.15, 6.05, 3.0, "2계층 관리자", PURPLE, [
    "플랫폼 슈퍼관리자 — 테넌트 스위처(F-079)",
    "테넌트 관리자 — 단일 스코프",
    "테넌트 컨텍스트 해소(F-078)",
], headfill=PURPLE, bodysize=10, igap=0.6)
titlecard(s, 6.75, 3.15, 6.03, 3.0, "재사용 기반", BLUE, [
    "온보딩 = 데이터 입력 수준(코드 배포 없음)(F-077)",
    "공통코드·메뉴 관리(§5B, F-073) 재사용",
    "홀·요율·규정 룰셋·마스터데이터만 주입",
], headfill=BLUE, bodysize=10, igap=0.6)
caption(s, "온보딩 6단계 + 2계층 관리자 — 코드 배포 없이 새 전시관 개설.")

# ================================================================
# Ⅳ 성능 및 품질
# ================================================================

divider("Ⅳ", "성능 및 품질", "PERFORMANCE & QUALITY", ["성능·SLA·확장성", "품질·관측성", "인터페이스"], "정성 · 성능·품질", BLUE)

# S51 성능·가용성
s = slide(); content_header(s, "Ⅳ. 성능·품질 · 성능 요구사항", "코어 SLO 99.5%(성수기 99.9% 지향), 무중단 롤링·PITR·degraded로 버틴다", "Ⅳ", "REQ-N-001~005·011")
mets=[("99.5%","코어 SLO(성수기 99.9%)",BLUE),("2","백엔드 인스턴스 롤링",PURPLE),("PITR","PG 스탠바이·failover",TEAL),("degraded","3중 폴백 모드",AMBER)]
mw=2.94
for i,(v,l,cl) in enumerate(mets):
    x=0.55+i*(mw+0.16)
    rect(s, x, 1.6, mw, 1.1, fill=cl, shape=MSO_SHAPE.ROUNDED_RECTANGLE, shadow=True)
    text(s, x, 1.72, mw, 0.5, v, size=22, color=WHITE, bold=True, align=PP_ALIGN.CENTER)
    text(s, x, 2.28, mw, 0.34, l, size=9, color=WHITE, align=PP_ALIGN.CENTER)
bullets(s, 0.55, 3.0, 12.0, [
    "코어 SLO 99.5%(성수기 99.9% 지향), 무상태 백엔드+Redis 외부화(N-001)",
    "백엔드 2인스턴스·헬스체크 LB·롤링 무중단(N-002) · PG 스탠바이 복제·자동 failover·PITR(N-003)",
    "degraded — Gemini 장애=목 응답, Claude 장애=Ollama 폴백, PG 복제지연=프라이머리 폴백(N-005)",
    "워커 장애 재큐(멱등)(N-011)",
], dot=BLUE, gap=0.55, size=10.5)
lead(s, 0.55, 5.4, 12.23, 0.6, "무중단·복원 — 롤링 배포·PITR·degraded 3중 폴백으로 성수기 부하를 버틴다.", fill=INK, size=13)
caption(s, "SLA 수치 + HA 구성 — 프론트·백엔드·Redis·PG·워커 다중화.")

# S52 나노바나나 성능
s = slide(); content_header(s, "Ⅳ. 성능·품질 · 성능 요구사항", "단건 평균 40초, 1024px 전처리·자동생성 한정·캐시·쿼터로 대량을 제어한다", "Ⅳ", "REQ-N-006·007")
mets=[("≤40s","단건 평균(전면 비동기)",PURPLE),("1024px","Canvas 다운스케일·JPEG 0.85",BLUE),
      ("S1·S7","자동생성 한정",TEAL),("쿼터","성공 시에만 차감",GREEN)]
mw=2.94
for i,(v,l,cl) in enumerate(mets):
    x=0.55+i*(mw+0.16)
    card(s, x, 1.6, mw, 2.0)
    rect(s, x, 1.6, mw, 0.08, fill=cl, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
    text(s, x, 2.0, mw, 0.7, v, size=24, color=cl, bold=True, align=PP_ALIGN.CENTER)
    text(s, x+0.12, 2.8, mw-0.24, 0.7, l, size=9.5, color=INK, align=PP_ALIGN.CENTER, line_spacing=1.0)
bullets(s, 0.55, 4.0, 12.0, [
    "단건 평균 ≤40s, 전면 비동기(WS 완료 푸시)(N-006)",
    "전처리 — 클라 Canvas 1024px 다운스케일 + JPEG 0.85",
    "대량 제어 — 자동생성 S1·S7 한정 + 동일 스키마해시 캐시 + 행사별 쿼터(N-007)",
], dot=PURPLE, gap=0.55, size=11)
caption(s, "렌더 성능 지표 — 40초·1024px·캐시 히트·쿼터로 비용/지연 제어.")

# S43 확장성·비동기
s = slide(); content_header(s, "Ⅳ. 성능·품질 · 성능·확장성", "워커는 큐 깊이로 독립 확장하고, BI·공개는 읽기복제로 오프로드한다", "Ⅳ", "REQ-N-009·010·011")
titlecard(s, 0.55, 1.6, 6.05, 4.5, "독립 확장 (워커)", PURPLE, [
    "나노바나나·서류·EDM 워커 = 큐 깊이 기반 증감(N-009)",
    "워커 장애 재큐 — 멱등 Job(N-011)",
    "GEMINI 워커존 단일 egress",
    "역할별 프론트 번들 분리(F-081, 방식 확정 대기 [구현 계획])",
], headfill=PURPLE, bodysize=10, igap=0.62)
titlecard(s, 6.75, 1.6, 6.03, 4.5, "오프로드 (읽기복제·CDN)", BLUE, [
    "BI 집계·공개 조회·플로어플랜 열람 = 읽기복제 라우팅(N-010)",
    "공개영역 CDN·요청률 상한(N-019)",
    "무상태 백엔드 2인스턴스 롤링(N-002)",
    "운영 프라이머리 보호 — 읽기 부하 분리",
], headfill=BLUE, bodysize=10, igap=0.62)
caption(s, "배포 토폴로지 — 프론트·백엔드 2인스턴스·워커 N·읽기복제·CDN.")

# S53 접근성·다국어·테마·반응형
s = slide(); content_header(s, "Ⅳ. 성능·품질 · 품질 요구사항", "WCAG 2.1 AA+KWCAG 2.2, 한/영/중/일, 라이트/다크, 데스크톱/모바일 전부", "Ⅳ", "REQ-N-012~016")
items=[("웹 접근성","WCAG 2.1 AA + 공공 KWCAG 2.2 병행(N-012)",BLUE,"check-verify"),
       ("비시각 대안","캔버스/맵 데이터 테이블·키보드 부스 선택(N-013)",BLUE,"mobile"),
       ("다국어(i18n)","한 기본+영/중/일·hreflang·로케일 포맷(N-014)",PURPLE,"document-rtm"),
       ("라이트/다크","CSS 토큰 2벌·prefers-color-scheme(N-015)",PURPLE,"dashboard-chart"),
       ("반응형","데스크톱=설계/에디터·모바일=현장/조회/승인(N-016)",TEAL,"mobile")]
for i,(t,d,cl,icn) in enumerate(items):
    x=0.55+(i%3)*4.11; y=1.6+(i//3)*1.7
    card(s, x, y, 3.97, 1.5)
    icon(s, icn, x+0.2, y+0.25, 0.44, cl)
    text(s, x+0.8, y+0.2, 3.0, 0.4, t, size=11, color=cl, bold=True)
    text(s, x+0.8, y+0.62, 3.0, 0.8, d, size=8.8, color=INK, line_spacing=1.0)
chip(s, 8.75, 3.3, 3.97, 1.5, "[구현 계획: 전면 스윕 대기]", SLATE, size=11)
caption(s, "접근성·다국어·테마·반응형 매트릭스 — 공공 KWCAG 병행.")

# S54 관측성·데이터
s = slide(); content_header(s, "Ⅳ. 성능·품질 · 품질·관측성", "로그·메트릭·트레이스에 PII·스택트레이스는 남기지 않고, BI는 배치 ETL로 정합한다", "Ⅳ", "REQ-N-018·019·021")
obs=[("수집","로그·메트릭·트레이스",BLUE),("저장","관리존 · PII·스택트레이스 미기록",PURPLE),("대시보드","관측성 · include-stacktrace: never",TEAL)]
for i,(t,d,cl) in enumerate(obs):
    x=0.55+i*4.11
    rect(s, x, 1.7, 3.85, 1.2, fill=(BLUE_LT if cl==BLUE else (PURPLE_LT if cl==PURPLE else RGBColor(0xE1,0xF3,0xF1))), line=cl, line_w=1.3, shape=MSO_SHAPE.ROUNDED_RECTANGLE, shadow=True)
    text(s, x+0.15, 1.82, 3.55, 0.35, t, size=12, color=cl, bold=True, align=PP_ALIGN.CENTER)
    text(s, x+0.15, 2.2, 3.55, 0.6, d, size=9, color=INK, align=PP_ALIGN.CENTER, line_spacing=0.95)
    if i<2: text(s, x+3.85, 1.7, 0.26, 1.2, "›", size=16, color=SLATE, bold=True, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)
bullets(s, 0.55, 3.4, 12.0, [
    "관측성 — 자격증명·PII·스택트레이스 미기록(include-stacktrace: never)(N-018)",
    "BI 데이터마트 — 스타 스키마(FACT/DIM) 야간 배치 ETL 또는 읽기복제 적재(N-021)",
    "KpiSnapshot(원천-집계 정합 오차 0) · 공개영역 CDN·요청률 상한(N-019)",
], dot=BLUE, gap=0.55, size=10.5)
caption(s, "관측성 파이프라인(수집→저장→대시보드) — PII·스택트레이스 미기록.")

# S45 연동·마스터데이터
s = slide(); content_header(s, "Ⅳ. 성능·품질 · 인터페이스 요구사항", "외부는 게이트웨이로, 룰셋·요율은 버전 데이터로 무중단 개정한다", "Ⅳ", "REQ-F-072·080")
titlecard(s, 0.55, 1.6, 6.05, 4.5, "연동 게이트웨이", BLUE, [
    "API·웹훅·CRM/ERP 연동(선택, F-080)",
    "외부 게이트 — Gemini(워커존)·Claude(백엔드)",
    "PG 결제·kxwp 릴레이·등록업체 DB 739",
    "내부↔외부 경계는 게이트웨이 단일 통로",
], headfill=BLUE, bodysize=10, igap=0.62)
titlecard(s, 6.75, 1.6, 6.03, 4.5, "마스터데이터·룰셋 버전", PURPLE, [
    "룰셋·마스터데이터 버전관리(요율·규정 룰셋)",
    "M18 백오피스·감사 기록으로 무중단 개정(F-072)",
    "규정 개정 = 코드 배포 없이 데이터 버전 교체",
    "요율 공식본은 [협의: C4]",
], headfill=PURPLE, bodysize=10, igap=0.62)
caption(s, "연동 인터페이스 맵 — 내부↔외부 게이트웨이 + 버전 데이터로 무중단 개정.")

# ================================================================
# Ⅴ 프로젝트 관리
# ================================================================

divider("Ⅴ", "프로젝트 관리", "PROJECT MANAGEMENT", ["일정계획", "관리방법론·역량", "위험·이슈·품질형상"], "정성 · 사업관리", BLUE)

# S58 일정 간트 (도식 ⑫)
s = slide(); content_header(s, "Ⅴ. 프로젝트 관리 · 일정계획", "Phase 1 코어 → Phase 2 워크플로/운영 → Phase 3 현장/확장, 게이트로 진행", "Ⅴ")
for i,(lb,cl) in enumerate([("완료",GREEN),("진행",AMBER),("예정",SLATE)]):
    rect(s, 9.9+i*1.0, 1.4, 0.24, 0.24, fill=cl, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
    text(s, 10.18+i*1.0, 1.38, 0.9, 0.28, lb, size=9, color=INK, anchor=MSO_ANCHOR.MIDDLE)
gx=4.3; gw=8.4; units=12; uw=gw/units
for i in range(units+1):
    rect(s, gx+i*uw, 1.95, 0.008, 4.05, fill=RGBColor(0xF0,0xF1,0xF4))
for i,lab in enumerate(["Phase 1 (~4M)","Phase 2 (~4M)","Phase 3 (~4M+)"]):
    text(s, gx+i*4*uw, 1.7, 2.8, 0.24, lab, size=8.5, color=BLUE_DK, bold=True)
rows=[("P1","아키텍처(A)",0,3,GREEN,"완료"),
      ("P1","공통레이어 B (2FA·시스템관리)",0.5,3,GREEN,"완료"),
      ("P1","P0 코어 M2·M3·M4·M5",1.5,2.5,AMBER,"진행"),
      ("P1","  ├ 스캐폴드·인증·매퍼·룰엔진·나노바나나",1.5,1.5,GREEN,"완료"),
      ("P1","  └ 3안·병합·실데이터",3.0,1.0,AMBER,"진행"),
      ("P2","옥션 M15·관람 M10",4,2,SLATE,"예정"),
      ("P2","BI M16·관리자 M18",4.3,2,SLATE,"예정"),
      ("P2","판매·서류·정산 M1·M6·M7·M9·CMS M12·M17",4.5,2.5,SLATE,"예정"),
      ("P3","멀티테넌시·M11·M13·M14",8,2.5,SLATE,"예정"),
      ("P3","배포 안정화·SM 이관",8.5,3,AMBER,"진행")]
ry=2.02; rh=0.375
for i,(ph,nm,st,dur,cl,lab) in enumerate(rows):
    y=ry+i*rh
    if i%2==0: rect(s, 0.55, y-0.01, 12.23, rh-0.01, fill=ZEBRA)
    text(s, 0.6, y, 0.5, rh, ph, size=7.5, color=BLUE_DK, bold=True, anchor=MSO_ANCHOR.MIDDLE)
    text(s, 1.1, y, 3.2, rh, nm, size=7.6, color=INK, anchor=MSO_ANCHOR.MIDDLE)
    bx=gx+st*uw; bw=max(dur*uw,0.28)
    rect(s, bx, y+0.05, bw, rh-0.14, fill=cl, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
    text(s, bx, y+0.03, bw, rh-0.1, lab, size=6.8, color=WHITE, bold=True, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)
# 마일스톤
for mx,ml in [(gx+0.5*uw,"G1"),(gx+0.9*uw,"G2"),(gx+4*uw,"P게이트"),(gx+8*uw,"QA"),(gx+12*uw,"오픈")]:
    rect(s, mx-0.09, 5.85, 0.18, 0.18, fill=(GREEN if ml in("G1","G2") else BLUE), shape=MSO_SHAPE.DIAMOND)
    text(s, mx-0.4, 6.02, 0.8, 0.2, ml, size=7, color=INK, bold=True, align=PP_ALIGN.CENTER)
nowx=gx+2.7*uw
rect(s, nowx, 1.95, 0.02, 4.05, fill=PURPLE)
text(s, nowx-0.4, 6.02, 0.8, 0.2, "현재", size=7.5, color=PURPLE, bold=True, align=PP_ALIGN.CENTER)
caption(s, "선행 게이트 통제 + Phase 게이트 QA 통과 후 진행. G1·G2 완료 — 실증 기반. 총 ~150 M/M.", y=6.45)

# ---- Ⅴ 관리방법론 ----
s = slide(); content_header(s, "Ⅴ. 프로젝트 관리 · 관리방법론", "PMBOK 기반 통합관리 + ITSM 운영 프로세스로 일정·품질·리스크를 통제한다", "Ⅴ")
mm=[("통합·범위관리","PMBOK 기반 착수→계획→실행→감시→종료, 범위기술서·WBS","booth-layout",BLUE),
   ("일정·자원관리","Phase 게이트·마일스톤·번다운, 직군별 M/M 투입계획","dashboard-chart",PURPLE),
   ("의사소통·보고","주간/월간 보고·이슈 에스컬레이션·주관기관 정례 협의","document-rtm",TEAL),
   ("형상·산출물관리","Gitea 형상관리·산출물 목록·변경관리·감리 대응","check-verify",BLUE_DK)]
for i,(t,d,ic,cl) in enumerate(mm):
    x=0.55+(i%2)*6.19; y=1.6+(i//2)*2.15
    card(s, x, y, 6.05, 1.9)
    rect(s, x, y, 0.1, 1.9, fill=cl, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
    icon(s, ic, x+0.3, y+0.35, 0.55, cl)
    text(s, x+1.15, y+0.33, 4.7, 0.4, t, size=12.5, color=cl, bold=True)
    text(s, x+1.15, y+0.9, 4.7, 0.85, d, size=9.5, color=INK, line_spacing=1.05)
lead(s, 0.55, 6.0, 12.23, 0.5, "관리방법론·관리역량 — 통합·범위·일정·의사소통·형상관리 표준 체계로 실행력 확보.", fill=INK, size=11.5)
caption(s, "관리방법론·관리역량 독립 절 — 표준 평가부문(프로젝트 관리) 정렬.", y=6.62)

# S59 리스크 매트릭스 (도식 ⑭)
s = slide(); content_header(s, "Ⅴ. 프로젝트 관리 · 위험·이슈 관리", "약한 고리를 숨기지 않고 드러내 방어한다 — 선제 대응이 곧 신뢰다", "Ⅴ")
# 매트릭스
mxx,myy,mww,mhh=2.2,1.7,6.3,4.3
rect(s, mxx, myy, mww, mhh, fill=WHITE, line=LINE, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
rect(s, mxx+mww/2, myy, mww/2, mhh/2, fill=RGBColor(0xFC,0xE9,0xE7))  # 우상 위험
rect(s, mxx, myy+mhh/2, mww/2, mhh/2, fill=RGBColor(0xE3,0xF6,0xEC))  # 좌하 안전
rect(s, mxx+mww/2, myy, 0.006, mhh, fill=LINE); rect(s, mxx, myy+mhh/2, mww, 0.006, fill=LINE)
text(s, mxx, myy+mhh+0.05, mww, 0.24, "발생가능성 →", size=8.5, color=MUTED, align=PP_ALIGN.CENTER)
text(s, mxx-0.55, myy, 0.5, mhh, "영향도 →", size=8.5, color=MUTED, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)
bubbles=[("R-1",0.62,0.78,AMBER),("R-2",0.58,0.8,AMBER),("R-3",0.5,0.5,SLATE),("R-4",0.5,0.45,SLATE),
         ("R-5",0.48,0.5,SLATE),("R-6",0.28,0.82,AMBER),("R-7",0.35,0.35,SLATE),("R-8",0.22,0.22,GREEN)]
for lb,px,py,cl in bubbles:
    bx=mxx+px*mww-0.22; by=myy+(1-py)*mhh-0.22
    rect(s, bx, by, 0.44, 0.44, fill=cl, shape=MSO_SHAPE.OVAL, shadow=True)
    text(s, bx, by, 0.44, 0.44, lb, size=8, color=WHITE, bold=True, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)
# 우측 게이트·대응
titlecard(s, 8.75, 1.7, 4.03, 4.3, "선행 게이트·대응", GREEN, [
    "G1 나노바나나 승인 [검증됨: 완료]",
    "G2 배포서버 [검증됨: 완료]",
    "R-1/2 외부API·CAD — 승인 게이트+degraded·가정 라벨",
    "R-3 kxwp — 단계적 접근 · R-5 간판 — 후처리 합성",
    "R-6 AI 오판 — 사람 확정 · R-8 확장 — 격리 레이어만",
], headfill=GREEN, bodysize=8.8, igap=0.62)
caption(s, "발생×영향 매트릭스에 R-1~R-8 배치 — 약한 고리를 선제 노출·방어(숨기지 않아 신뢰).")

# S60 품질·형상관리
s = slide(); content_header(s, "Ⅴ. 프로젝트 관리 · 품질·형상관리", "배포는 헬스체크로 지키고 실패하면 롤백, 의존성은 SCA 게이트로 막는다", "Ⅴ", "REQ-N-004·S-019")
gate=[("빌드","compileJava·tsc",BLUE),("테스트","점진 경계면 검증",PURPLE),("SCA","OWASP Dep-Check",AMBER),
      ("헬스체크","health 200",GREEN),("롤백","실패 시 이전 jar",RED)]
for i,(t,d,cl) in enumerate(gate):
    x=0.55+i*2.5
    rect(s, x, 1.8, 2.3, 1.1, fill=(BLUE_LT if cl==BLUE else (PURPLE_LT if cl==PURPLE else (RGBColor(0xFF,0xF6,0xE8) if cl==AMBER else (RGBColor(0xE3,0xF6,0xEC) if cl==GREEN else RGBColor(0xFC,0xE9,0xE7))))),
         line=cl, line_w=1.3, shape=MSO_SHAPE.ROUNDED_RECTANGLE, shadow=True)
    text(s, x+0.1, 1.9, 2.1, 0.4, t, size=11, color=cl, bold=True, align=PP_ALIGN.CENTER)
    text(s, x+0.1, 2.32, 2.1, 0.5, d, size=8.8, color=INK, align=PP_ALIGN.CENTER, line_spacing=0.9)
    if i<4: text(s, x+2.3-0.05, 1.8, 0.2, 1.1, "›", size=15, color=SLATE, bold=True, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)
bullets(s, 0.55, 3.3, 12.0, [
    "Fail-Safe 배포 — 백업→배포→헬스체크 200→실패 시 롤백(이전 jar 유지) [검증됨: deploy_kintex.sh](N-004)",
    "QA 게이트 — 점진 경계면 검증·보안 불변·빌드 회귀(Phase 게이트 통과 후 진행)",
    "SCA CI 게이트 — OWASP Dependency-Check 정기 스캔·패치(S-019)",
    "형상관리 — Gitea zio/kintex·Conventional Commits·main 보호·환경 분리(dev/staging/prod)",
], dot=BLUE, gap=0.55, size=10.5)
caption(s, "품질 게이트 흐름 — 빌드→테스트→SCA→헬스체크→롤백.")

# ================================================================
# Ⅵ 프로젝트 지원
# ================================================================

divider("Ⅵ", "프로젝트 지원", "PROJECT SUPPORT", ["인수인계·교육·기술지원", "유지보수(SM)·SLA", "장애대응·기밀보안"], "정성 · 사업지원", NAVY)

# ---- Ⅵ 인수인계·교육·기술지원 ----
s = slide(); content_header(s, "Ⅵ. 프로젝트 지원 · 인수인계·교육·기술지원", "구축 후 지속가능성 — 이관·교육·지원·하자보수를 표준 절차로 보장한다", "Ⅵ")
hv=[("인수인계","이관계획·검사 대상/방법·충족조건, 지식이전(운영자·개발자 지침서 PPT·프로그램 사양서)","closed-loop",BLUE),
   ("교육훈련","운영자·관리자·사용자 대상 교육 — 내용·방법·기간·인원·횟수, 실습·매뉴얼 제공","org-people",PURPLE),
   ("기술지원","지원 범위·수준·기술 매뉴얼, 서비스데스크(SR·문의) 채널·응답 기준","shield-security",TEAL),
   ("하자보수","하자담보 기간 내 하자보수 범위·조치절차·대응(계약 조건 준수)","check-verify",BLUE_DK)]
for i,(t,d,ic,cl) in enumerate(hv):
    x=0.55+(i%2)*6.19; y=1.6+(i//2)*2.15
    card(s, x, y, 6.05, 1.9)
    rect(s, x, y, 0.1, 1.9, fill=cl, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
    icon(s, ic, x+0.3, y+0.35, 0.55, cl)
    text(s, x+1.15, y+0.33, 4.7, 0.4, t, size=12.5, color=cl, bold=True)
    text(s, x+1.15, y+0.9, 4.7, 0.85, d, size=9.3, color=INK, line_spacing=1.05)
caption(s, "프로젝트 지원 4영역 — 인수인계·교육훈련·기술지원·하자보수. WORK_STATUS 인수인계 체계 활용.")

# ---- Ⅵ 유지보수(SM)·SLA ----
s = slide(); content_header(s, "Ⅵ. 프로젝트 지원 · 유지보수(SM)·SLA", "라이브 자동배포를 실적으로 — SLA·장애대응·정기점검을 ITSM 프로세스로 운영한다", "Ⅵ", "REQ-N-004")
sla=[("코어 서비스 가용성","99.5% (성수기 99.9% 지향)","월 가용성·health"),
     ("장애 응답시간","P1 즉시·P2 30분·P3 4시간","티켓 타임스탬프"),
     ("장애 복구시간(MTTR)","P1 ≤2h·P2 ≤1영업일","복구 완료 기록"),
     ("배포 성공률","≥99% (Fail-Safe 자동 롤백)","배포 로그 게이트"),
     ("나노바나나 큐 소진","best-effort(워커 스케일아웃)","큐 적체·처리량")]
table(s, 0.55, 1.6, 8.7, ["SLA 지표","목표","측정"], sla, [3.1,3.0,2.6], rowh=0.5, hsize=10, bsize=8.8)
titlecard(s, 9.45, 1.6, 3.33, 3.55, "운영 체계·조직", TEAL, [
    "ITSM — 요청(SR)·장애·변경·배포·문제(RCA)",
    "조직 — 운영총괄+백/프론트+DB/인프라+AI워커+QA",
    "근무 — 평일 상주 + 성수기(3~5·9~11월) 온콜",
    "CI/CD 자동배포 [검증됨: deploy_kintex.sh]",
], headfill=TEAL, bodysize=8.8, igap=0.7)
lead(s, 0.55, 4.7, 8.7, 0.55, "월간 SLA 리포트(가용성·MTTR·티켓·배포) — 페널티 체계는 계약 조건에 따름(공고 확인).", fill=INK, size=11)
caption(s, "SM 운영 — SLA 5지표 + ITSM 조직. 자동배포는 라이브 실적(과장 아님).", y=6.5)

# ---- Ⅵ 장애대응·비상대책 ----
s = slide(); content_header(s, "Ⅵ. 프로젝트 지원 · 장애대응·비상대책", "등급분류→에스컬레이션→RCA, degraded·PITR·롤백으로 비상 상황을 흡수한다", "Ⅵ", "REQ-N-005")
inc=[("장애 등급·에스컬레이션","P1(중단)~P4(경미) 분류, 자동 알림(WS·알림센터)·온콜 에스컬레이션",RED,"risk-warning"),
     ("RCA·재발방지","근본원인 분석 리포트·재발방지 티켓화, 감사로그·관측성 추적(PII 미기록)",PURPLE,"check-verify"),
     ("degraded / BCP·DR","Gemini=목응답·Claude=Ollama 폴백·PG=failover+PITR·배포=자동 롤백",TEAL,"shield-security"),
     ("정기점검·예방정비","DB 인덱스/복제·백업(PITR)·Redis AOF·워커 하트비트·TLS 만료·용량 점검",BLUE,"pipeline-flow")]
for i,(t,d,cl,ic) in enumerate(inc):
    y=1.62+i*1.16
    card(s, 0.55, y, 12.23, 1.02)
    rect(s, 0.55, y, 0.1, 1.02, fill=cl, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
    icon(s, ic, 0.85, y+0.28, 0.48, cl)
    text(s, 1.55, y+0.14, 4.0, 0.74, t, size=12, color=cl, bold=True, anchor=MSO_ANCHOR.MIDDLE)
    text(s, 5.7, y+0.14, 6.9, 0.74, d, size=9.5, color=INK, anchor=MSO_ANCHOR.MIDDLE, line_spacing=1.0)
caption(s, "장애대응·비상대책(BCP/DR) — RPO 최소·RTO 분 단위, degraded 3중 폴백으로 무중단 지향.")

# ---- Ⅵ 기밀보안(관리적) ----
s = slide(); content_header(s, "Ⅵ. 프로젝트 지원 · 기밀보안(관리적)", "기술적 통제(Ⅲ) 위에 관리적·물리적 기밀보안 체계를 얹는다", "Ⅵ", "REQ-S-012")
sec=[("보안 조직·서약","보안책임자 지정·보안서약서·보안교육 정례",BLUE,"org-people"),
     ("문서·산출물 보안","산출물 분류·접근통제·반출입 통제·형상 이력",PURPLE,"document-rtm"),
     ("인력·물리 보안","출입통제·개발환경 망분리·퇴사자 권한 회수",TEAL,"tenant-building"),
     ("감사·점검","민감 액션 전수 감사(TB_AUDIT_LOG)·정기 보안점검·SCA",BLUE_DK,"check-verify")]
for i,(t,d,cl,ic) in enumerate(sec):
    x=0.55+(i%2)*6.19; y=1.6+(i//2)*1.95
    card(s, x, y, 6.05, 1.72)
    rect(s, x, y, 6.05, 0.5, fill=cl, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
    text(s, x, y, 6.05, 0.5, t, size=12, color=WHITE, bold=True, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)
    icon(s, ic, x+0.3, y+0.72, 0.5, cl)
    text(s, x+1.0, y+0.66, 4.85, 0.95, d, size=9.8, color=INK, anchor=MSO_ANCHOR.MIDDLE, line_spacing=1.05)
lead(s, 0.55, 5.72, 12.23, 0.5, "관리적 보안 — 기술 보안(Ⅲ §3 보안 요구사항)과 분리 배치, 표준 채점 구조 정렬.", fill=INK, size=11.5)
caption(s, "기밀보안관리 — 관리적·물리적 통제(조직·문서·인력·감사). 기술적 보안은 Ⅲ §3.", y=6.55)

# ================================================================
# Ⅶ 그 밖의 사항
# ================================================================

divider("Ⅶ", "그 밖의 사항", "OTHERS", ["멀티테넌시·확장", "상생협력·하도급", "기대효과"], "가점 · 상생·부가", PURPLE)

# S55 멀티테넌트 비전
s = slide(); content_header(s, "Ⅶ. 그 밖의 사항 · 멀티테넌시·확장", "KINTEX #1에서 검증하고, COEX #2로 확장하며, 제3전시장을 표준으로 맞는다", "Ⅶ")
road=[("KINTEX","기준 테넌트 #1 · 검증 완료",GREEN),("COEX","테넌트 #2 · 온보딩 대상",BLUE),
      ("제3전시장","2028 · 178,000㎡",PURPLE),("타 전시관","국내 전시산업 표준 SaaS",SLATE)]
line_conn(s, 1.3, 2.9, 11.9, 2.9, color=SLATE, w=1.6)
for i,(t,d,cl) in enumerate(road):
    x=0.7+i*3.1
    rect(s, x+1.15, 2.77, 0.26, 0.26, fill=cl, shape=MSO_SHAPE.OVAL)
    card(s, x, 3.3, 2.85, 1.5)
    rect(s, x, 3.3, 2.85, 0.08, fill=cl, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
    icon(s, "tenant-building", x+0.2, 3.5, 0.44, cl)
    text(s, x+0.8, 3.5, 1.9, 0.4, t, size=12, color=cl, bold=True)
    text(s, x+0.2, 4.05, 2.5, 0.65, d, size=9, color=INK, line_spacing=1.0)
bullets(s, 0.55, 5.2, 12.0, [
    "개발 부담 최소화 — 멀티테넌시=격리 레이어만 추가, 기능 로직 불변(R-8)",
], dot=PURPLE, gap=0.4, size=11)
caption(s, "테넌트 확장 로드맵 — KINTEX→COEX→제3전시장→타 전시관 SaaS.")

# ---- Ⅶ 상생협력·하도급 ----
s = slide(); content_header(s, "Ⅶ. 그 밖의 사항 · 상생협력·하도급", "중소기업 참여·하도급 계획을 원칙으로 제시 — 구체 수치는 공고·컨소시엄 확정 시 기재", "Ⅶ")
titlecard(s, 0.55, 1.6, 6.05, 3.5, "상생협력 (정량 별표11)", BLUE, [
    "중소기업 공동수급체 참여 — 참여지분율 배점(최대 5점) 대응",
    "지분율 50%↑=최고등급 지향 [공고·컨소 확정 시 기재]",
    "중소기업 기술·인력 참여로 상생 생태계",
    "구체 지분·업체는 [공고 확정 시 기재]",
], headfill=BLUE, bodysize=9.5, igap=0.66)
titlecard(s, 6.75, 1.6, 6.03, 3.5, "하도급 계획 (정성 적정성)", PURPLE, [
    "하도급 금액/계약금액 비율 적정성(배점 5점 이상)",
    "20억원 이상 SW사업은 하도급 평가 제외 불가 — 계획서 필수",
    "하도급 승인·관리·대금 직불 등 표준 준수",
    "하도급 범위·업체·비율은 [공고 확정 시 기재]",
], headfill=PURPLE, bodysize=9.5, igap=0.66)
lead(s, 0.55, 5.3, 12.23, 0.62, "상생협력·하도급 — 일반 원칙 수준 제시, 구체 수치는 공고·컨소시엄 확정 후 확정 기재.", fill=INK, size=13)
caption(s, "정량 별표11(상생협력)·정성 하도급 적정성 대응. 구체 수치는 [공고 확정 시 기재](T3·T5).")

# S61 정량 기대효과 (도식 ⑮)
s = slide(); content_header(s, "Ⅶ. 그 밖의 사항 · 기대효과", "견적 수일→즉시, 배치 수주→수 분, 검수 육안→자동, 결과는 사진으로", "Ⅶ", "REQ-F-002·007·009·017")
quant=[("홀 배정 견적","수일","즉시","document-rtm"),("배치도 초안","수일~수주","수 분","booth-layout"),
       ("규정 검수","육안 검토","자동 플래깅","check-verify"),("유틸리티 신청","수기 작도","클릭+자동견적","wiring-power"),
       ("시공 결과 예측","불가(외주)","표준 샷 자동생성","camera-render")]
qw=2.4
for i,(k,a,b,icn) in enumerate(quant):
    x=0.55+i*(qw+0.06)
    card(s, x, 1.6, qw, 2.15)
    icon(s, icn, x+qw/2-0.24, 1.75, 0.48, PURPLE)
    text(s, x, 2.32, qw, 0.3, k, size=10, color=INK, bold=True, align=PP_ALIGN.CENTER)
    text(s, x, 2.68, qw, 0.24, a, size=9.5, color=SLATE, align=PP_ALIGN.CENTER)
    text(s, x, 2.94, qw, 0.24, "↓", size=13, color=PURPLE, bold=True, align=PP_ALIGN.CENTER)
    text(s, x, 3.2, qw, 0.3, b, size=10.5, color=BLUE, bold=True, align=PP_ALIGN.CENTER)
imp=[("40초","단건 렌더",BLUE),("99.5%","코어 SLO",PURPLE),("142","REQ 커버",BLUE),("18","도메인 모듈",PURPLE)]
mw=2.94
for i,(v,l,cl) in enumerate(imp):
    x=0.55+i*(mw+0.16)
    rect(s, x, 4.05, mw, 1.05, fill=cl, shape=MSO_SHAPE.ROUNDED_RECTANGLE, shadow=True)
    text(s, x+0.25, 4.15, 1.4, 0.85, v, size=26, color=WHITE, bold=True, anchor=MSO_ANCHOR.MIDDLE)
    text(s, x+1.65, 4.05, mw-1.7, 1.05, l, size=10.5, color=WHITE, bold=True, anchor=MSO_ANCHOR.MIDDLE)
lead(s, 0.55, 5.4, 12.23, 0.6, "리드타임 수일→즉시·수 분, 검수 육안→자동 — 정량 개선을 숫자로 각인.", fill=INK, size=13)
caption(s, "5지표 Before/After + 임팩트 지표 스트립.")

# S62 정성·전략 효과
s = slide(); content_header(s, "Ⅶ. 그 밖의 사항 · 기대효과", "참가업체는 영업력을, 홀매니저는 시간을, 킨텍스는 데이터 주권을 얻는다", "Ⅶ")
qual=[("참가업체","사진급 자료로 영업력·수주력 강화",PURPLE,"camera-render"),
      ("홀매니저","규정 검수 병목 해소로 처리량 증대",BLUE,"check-verify"),
      ("킨텍스(운영사)","온프레미스 우선·외부 AI 격리로 데이터 주권 확보",TEAL,"shield-security"),
      ("전시산업","국내 전시산업 표준 플랫폼화",NAVY,"tenant-building")]
for i,(who,d,cl,icn) in enumerate(qual):
    x=0.55+(i%2)*6.19; y=1.65+(i//2)*2.25
    card(s, x, y, 6.05, 2.0)
    rect(s, x, y, 0.1, 2.0, fill=cl, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
    icon(s, icn, x+0.3, y+0.35, 0.6, cl)
    text(s, x+1.15, y+0.35, 4.7, 0.5, who, size=14, color=cl, bold=True)
    text(s, x+1.15, y+0.95, 4.7, 0.9, d, size=10.5, color=INK, line_spacing=1.05)
caption(s, "이해관계자별 정성·전략 효과 — 영업력·시간·데이터 주권·산업 표준화.")

# S63 확장 비전·마무리
s = slide(); content_header(s, "Ⅶ. 그 밖의 사항 · 기대효과·마무리", "킨텍스에서 검증하고, 코엑스로 확장하며, 다시 사진으로 컨펌한다", "Ⅶ", "REQ-F-077")
road=["KINTEX 검증","COEX 확장","제3전시장 표준","타 전시관 SaaS"]
for i,r in enumerate(road):
    x=0.55+i*3.12
    rect(s, x, 1.6, 2.9, 0.7, fill=(PURPLE if i%2 else BLUE), shape=MSO_SHAPE.ROUNDED_RECTANGLE, shadow=True)
    text(s, x, 1.6, 2.9, 0.7, r, size=11, color=WHITE, bold=True, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)
    if i<3: text(s, x+2.9-0.05, 1.6, 0.24, 0.7, "›", size=16, color=SLATE, bold=True, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)
bullets(s, 0.55, 2.7, 12.0, [
    "확장 — COEX·제3전시장·타 전시관 SaaS 확장",
    "재사용 — 검증된 공통 프레임워크(WISE/UIWS) 위에 도메인 코어 이식",
], dot=PURPLE, gap=0.5, size=11)
rect(s, 0.55, 3.9, 12.23, 1.7, fill=BLUE_DK, shape=MSO_SHAPE.ROUNDED_RECTANGLE, shadow=True)
rect(s, 8.5, 3.9, 4.28, 1.7, fill=PURPLE, shape=MSO_SHAPE.PARALLELOGRAM)
icon(s, "camera-render", 0.95, 4.35, 0.7, WHITE)
text(s, 1.9, 4.15, 8.0, 1.2, "“신청서를 내는 순간,\n시공 후 사진을 먼저 본다.”", size=24, color=WHITE, bold=True, line_spacing=1.05, anchor=MSO_ANCHOR.MIDDLE)
caption(s, "확장 비전 로드맵 + 클로징 — 킨텍스를 전시 운영 표준 플랫폼으로 전환.")

# ================================================================
# 부록 A/B/C
# ================================================================

divider("A", "부록", "APPENDIX",
        ["확인 필요·협의", "REQ 커버리지", "100대 기능 성숙도"], "부록", NAVY)

# 부록 A 확인필요
s = slide(); content_header(s, "부록 A · 확인 필요·협의 사항", "가정·협의를 1장에 집약 — 발주처 협업 자세", "A")
rows=[("C1","평가배점·제출규격 (가정)","공공 SI 통례 기준, 공고 확인 시 재정렬"),
      ("C2","kxwp API (REQ-F-022)","웹폼→HWP/PDF 완결, 릴레이만 킨텍스 IT 협의"),
      ("C3","CAD·트렌치 실측 (REQ-F-016)","평면도 15장+CAD 확보, 미확보 구간 실측 협의"),
      ("C4","인터넷 요율 공식본 (REQ-F-002)","2,250원/㎡ 가정, 공식 요율표 확인 필요"),
      ("C5","나노바나나 G1 (완료)","소유자 승인·워커 라이브 [검증됨]"),
      ("C6","코엑스 실측 (REQ-F-077)","COEX 온보딩 시 홀·요율·규정 데이터"),
      ("C7","사이니지·비콘·IoT HW (F-070·052~055)","킨텍스 시설 인프라 협의 전제"),
      ("C8","기타 협의","연동·데이터·운영 세부 협의 항목")]
table(s, 0.55, 1.6, 12.23, ["ID","확인·협의 항목","비고"], rows, [0.9,5.0,6.33], rowh=0.55, hsize=10)
caption(s, "모든 [가정]·[협의]를 1장 집약 → 정직성을 강점으로 전환(리스크 R-4 방어).", y=6.5)

# 부록 B REQ 커버리지
s = slide(); content_header(s, "부록 B · REQ 커버리지 매트릭스", "142 REQ ↔ 슬라이드 1:1 — 미반영 0", "B")
cov=[("REQ-F (기능)","81","81","0",BLUE),("REQ-A (AI)","20","20","0",PURPLE),
     ("REQ-N (비기능)","21","21","0",TEAL),("REQ-S (보안)","20","20","0",BLUE_DK)]
cw=2.94
for i,(t,tot,cvd,miss,cl) in enumerate(cov):
    x=0.55+i*(cw+0.16)
    card(s, x, 1.6, cw, 2.3)
    rect(s, x, 1.6, cw, 0.5, fill=cl, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
    text(s, x, 1.6, cw, 0.5, t, size=11, color=WHITE, bold=True, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)
    text(s, x, 2.25, cw, 0.7, cvd, size=34, color=cl, bold=True, align=PP_ALIGN.CENTER)
    text(s, x, 3.05, cw, 0.3, f"/ {tot} 반영", size=10, color=INK, align=PP_ALIGN.CENTER)
    chip(s, x+cw/2-0.7, 3.4, 1.4, 0.32, f"미반영 {miss}", GREEN, size=9)
rect(s, 0.55, 4.2, 12.23, 1.0, fill=GREEN, shape=MSO_SHAPE.ROUNDED_RECTANGLE, shadow=True)
text(s, 0.75, 4.32, 3.0, 0.76, "142", size=40, color=WHITE, bold=True, anchor=MSO_ANCHOR.MIDDLE)
text(s, 3.0, 4.2, 9.5, 1.0, "총 142 REQ 전량 반영 · 미반영 0 — 상세 RTM(REQ→주 반영 슬라이드)은 proposal_content.md RTM 반영표 참조", size=12, color=WHITE, bold=True, anchor=MSO_ANCHOR.MIDDLE, line_spacing=1.05)
caption(s, "QA는 이 표로 142 REQ 반영상태를 최종 검증 — 미반영 0.")

# 부록 C 100대 기능 성숙도
s = slide(); content_header(s, "부록 C · 100대 기능 성숙도", "기구현 63 · 부분 18 · 신규 19 — 정량 성숙도", "C")
mat=[("기구현","63","라이브·검증 완료",GREEN),("부분 구현","18","일부 완성·보강 필요",AMBER),("신규","19","설계 확정·구현 대기",SLATE)]
for i,(t,v,d,cl) in enumerate(mat):
    x=0.55+i*4.11
    card(s, x, 1.7, 3.97, 2.6)
    rect(s, x, 1.7, 3.97, 0.08, fill=cl, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
    text(s, x, 2.05, 3.97, 1.0, v, size=56, color=cl, bold=True, align=PP_ALIGN.CENTER)
    text(s, x, 3.25, 3.97, 0.4, t, size=14, color=INK, bold=True, align=PP_ALIGN.CENTER)
    text(s, x, 3.7, 3.97, 0.4, d, size=9.5, color=MUTED, align=PP_ALIGN.CENTER)
# stacked bar
rect(s, 0.55, 4.7, 12.23, 0.6, fill=HALL_BG, line=LINE, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
total=100; x0=0.65; wtot=12.03
segs=[(63,GREEN),(18,AMBER),(19,SLATE)]
cx=x0
for v,cl in segs:
    w=wtot*v/total
    rect(s, cx, 4.78, w-0.03, 0.44, fill=cl, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
    text(s, cx, 4.78, w-0.03, 0.44, str(v), size=12, color=WHITE, bold=True, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)
    cx+=w
caption(s, "라이브 확인분(인프라·CI/CD·나노바나나·인증·워터마크·PostGIS 코어·평면도 자산)만 [검증됨].", y=5.7)

# ================================================================
# 부록 D · 화면 설계(안) 갤러리
# ================================================================

def build_gallery():
    manifest_path = os.path.join(SCREENS, "screens_manifest.json")
    scr_files = sorted(glob.glob(os.path.join(SCREENS, "*.png")))
    if not scr_files:
        return 0
    meta = {}
    group_order = []
    if os.path.exists(manifest_path):
        data = json.load(open(manifest_path, encoding="utf-8"))
        group_order = data.get("group_order", [])
        meta = data.get("screens", {})
    # 폴더 재스캔 — 매니페스트에 없는 파일도 수록(파일명 기반 캡션)
    by_group = {}
    for fp in scr_files:
        fn = os.path.basename(fp)
        m = meta.get(fn)
        if not m:
            scrid = os.path.splitext(fn)[0]
            m = {"scr":scrid, "module":"", "name":"", "group":"기타 화면", "device":"DESKTOP", "order":900}
        by_group.setdefault(m["group"], []).append((fp, m))
    for g in by_group: by_group[g].sort(key=lambda t: t[1].get("order", 900))
    order = [g for g in group_order if g in by_group] + [g for g in by_group if g not in group_order]
    total = sum(len(v) for v in by_group.values())

    divider("D", "화면 설계(안) 갤러리", "SCREEN DESIGN GALLERY",
            ["역할·모듈별 화면", "Google Stitch AI 시안", f"수록 {total}화면"], "부록 D", PURPLE)
    # intro 고지
    s = slide(); content_header(s, "부록 D · 화면 설계(안) 갤러리", "역할·모듈별 화면 설계 시안 — 구현 시 조정될 수 있음", "D")
    rect(s, 0.55, 1.6, 12.23, 1.3, fill=RGBColor(0xFF,0xF6,0xE8), line=AMBER, line_w=1.2, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
    icon(s, "risk-warning", 0.85, 1.9, 0.55, AMBER)
    text(s, 1.6, 1.78, 11.0, 0.4, "고지", size=13, color=AMBER, bold=True)
    text(s, 1.6, 2.2, 11.0, 0.6, "본 화면은 Google Stitch 기반 AI 생성 디자인 시안이며, 실제 구현 시 조정될 수 있음. 화면 문자열·수치·데이터는 예시입니다.", size=11, color=INK, bold=True, line_spacing=1.05)
    # 그룹 인덱스
    text(s, 0.55, 3.2, 12.0, 0.3, "수록 그룹", size=12, color=BLUE, bold=True)
    for i, g in enumerate(order):
        x=0.55+(i%3)*4.11; y=3.6+(i//3)*0.62
        cnt=len(by_group[g])
        rect(s, x, y, 3.97, 0.5, fill=CARD, line=LINE, shape=MSO_SHAPE.ROUNDED_RECTANGLE, shadow=True)
        rect(s, x, y, 0.09, 0.5, fill=PURPLE, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
        text(s, x+0.25, y, 3.0, 0.5, g, size=10, color=INK, bold=True, anchor=MSO_ANCHOR.MIDDLE)
        chip(s, x+3.35, y+0.1, 0.5, 0.3, str(cnt), PURPLE, size=8.5)
    caption(s, f"총 {total}개 화면을 역할·모듈 그룹으로 수록 — SCR ID·화면명 캡션 병기.", y=6.55)

    # 그룹별 갤러리 슬라이드
    for g in order:
        items = by_group[g]
        device = items[0][1].get("device", "DESKTOP")
        per = 4 if device == "MOBILE" else 3
        for c in range(0, len(items), per):
            chunk = items[c:c+per]
            s = slide(); content_header(s, "부록 D · 화면 설계(안) 갤러리", g, "D")
            n=len(chunk)
            if device == "MOBILE":
                cw=2.9; gap=0.19; cardh=4.6; x0=(13.333-(n*cw+(n-1)*gap))/2
                for i,(fp,m) in enumerate(chunk):
                    x=x0+i*(cw+gap)
                    card(s, x, 1.55, cw, cardh)
                    img_fit(s, fp, x+0.12, 1.67, cw-0.24, cardh-0.7, frame=False)
                    chip(s, x+0.12, 1.55+cardh-0.42, 0.9, 0.3, m.get("scr",""), PURPLE, size=8)
                    text(s, x+1.1, 1.55+cardh-0.43, cw-1.2, 0.32, m.get("name",""), size=8.5, color=INK, bold=True, anchor=MSO_ANCHOR.MIDDLE)
            else:
                cw=3.95; gap=0.19; cardh=3.9; x0=(13.333-(n*cw+(n-1)*gap))/2
                for i,(fp,m) in enumerate(chunk):
                    x=x0+i*(cw+gap); y=1.65
                    card(s, x, y, cw, cardh)
                    img_fit(s, fp, x+0.12, y+0.12, cw-0.24, cardh-0.75, frame=False)
                    chip(s, x+0.12, y+cardh-0.5, 1.0, 0.32, m.get("scr",""), PURPLE, size=9)
                    modtxt = m.get("module","")
                    text(s, x+1.2, y+cardh-0.51, cw-1.3, 0.34, [[{"t":m.get("name",""),"size":9.5,"color":INK,"bold":True}]], anchor=MSO_ANCHOR.MIDDLE)
                    if modtxt:
                        text(s, x+0.14, y+cardh-0.15, cw-0.28, 0.2, modtxt+" · 대상: "+g, size=7.5, color=MUTED)
            caption(s, "Google Stitch AI 생성 디자인 시안 — 실제 구현 시 조정될 수 있음.", y=6.7)
    return total

gallery_count = build_gallery()

# ================================================================
# 저장
# ================================================================
OUT = r"C:\GUARDiA\workspace\kintex\docs\deliverables\제안서.pptx"
os.makedirs(os.path.dirname(OUT), exist_ok=True)
prs.save(OUT)
print("SAVED:", OUT)
print("TOTAL SLIDES:", len(prs.slides._sldIdLst))
print("GALLERY SCREENS:", gallery_count)
