# -*- coding: utf-8 -*- """ 킨텍스(KINTEX) AI 시스템 — PoC(개념증명) 추진계획서 PPTX 생성기 - 목차: PoC 계획보고서 표준 5단계(추진개요 / 검증목표·평가기준 / 수행방안·방법 / 추진일정·조직 / 리스크·기대효과) - 결과보고서와 달리 "무엇을·어떻게·언제 검증할 것인가"(계획·미래시제) 관점으로 구성 - 디자인: KINTEX Blue #0066B3 / AI Purple #6D4AFF / 맑은 고딕 / 16:9 · 슬라이드 마스터 사용 재실행: python gen_poc_plan.py """ import os from pptx import Presentation from pptx.util import Emu, Pt from pptx.dml.color import RGBColor from pptx.enum.text import PP_ALIGN, MSO_ANCHOR from pptx.enum.shapes import MSO_SHAPE from pptx.oxml.ns import qn from pptx.chart.data import CategoryChartData from pptx.enum.chart import XL_CHART_TYPE, XL_LEGEND_POSITION, XL_LABEL_POSITION from pptx.shapes.autoshape import Shape, AutoShapeType BASE = os.path.dirname(os.path.abspath(__file__)) OUT = os.path.join(os.path.dirname(BASE), "킨텍스_AI_POC계획보고서.pptx") # ---------------- 팔레트 ---------------- BLUE = RGBColor(0x00, 0x66, 0xB3) BLUE_DK = RGBColor(0x00, 0x44, 0x7A) BLUE_LT = RGBColor(0xE1, 0xEF, 0xF9) PURPLE = RGBColor(0x6D, 0x4A, 0xFF) PURPLE_DK = RGBColor(0x4A, 0x2F, 0xCC) PURPLE_LT = RGBColor(0xEC, 0xE8, 0xFF) INK = RGBColor(0x10, 0x18, 0x28) MUTED = RGBColor(0x66, 0x70, 0x85) SLATE = RGBColor(0x98, 0xA2, 0xB3) LINE = RGBColor(0xE4, 0xE7, 0xEC) BG = RGBColor(0xF9, 0xFA, 0xFB) PANEL = RGBColor(0xF2, 0xF4, 0xF7) WHITE = RGBColor(0xFF, 0xFF, 0xFF) CARD = WHITE GREEN = RGBColor(0x12, 0x9E, 0x63) GREEN_LT = RGBColor(0xE3, 0xF5, 0xEC) AMBER = RGBColor(0xE0, 0x8A, 0x00) AMBER_LT = RGBColor(0xFC, 0xF1, 0xDD) RED = RGBColor(0xD9, 0x2D, 0x20) RED_LT = RGBColor(0xFC, 0xE9, 0xE7) CYAN = RGBColor(0x0B, 0xA5, 0xEC) TEAL = RGBColor(0x0E, 0x93, 0x84) NAVY = RGBColor(0x2B, 0x4C, 0x7E) ZEBRA = RGBColor(0xFC, 0xFC, 0xFD) SUBLT = RGBColor(0xC7, 0xDD, 0xF2) FONT = "맑은 고딕" EMU_W = 12192000; EMU_H = 6858000 IN = 914400 def inch(v): return int(v * IN) prs = Presentation() prs.slide_width = EMU_W; prs.slide_height = EMU_H BLANK = prs.slide_layouts[6] # ================================================================ # 슬라이드 마스터 — 공통 배경·상단 액센트바·푸터 # ================================================================ def _set_fill(sp, color): if color is None: sp.fill.background() else: sp.fill.solid(); sp.fill.fore_color.rgb = color def _no_line(sp): sp.line.fill.background() _mid = [900] def _mnext(): _mid[0] += 1; return _mid[0] def _master_shape(shapes, shape_type, x, y, w, h, fill, line=None): ast = AutoShapeType(shape_type); id_ = _mnext() sp = shapes._spTree.add_autoshape(id_, '%s %d' % (ast.basename, id_), ast.prst, inch(x), inch(y), inch(w), inch(h)) shp = Shape(sp, shapes) _set_fill(shp, fill) if line is None: _no_line(shp) else: shp.line.color.rgb = line; shp.line.width = Pt(1.0) shp.shadow.inherit = False return shp def _master_text(shapes, x, y, w, h, txt, size, color, bold=False, align=PP_ALIGN.LEFT): id_ = _mnext() sp = shapes._spTree.add_textbox(id_, 'TextBox %d' % id_, inch(x), inch(y), inch(w), inch(h)) tb = Shape(sp, shapes) tf = tb.text_frame; tf.word_wrap = True tf.margin_left = 0; tf.margin_right = 0; tf.margin_top = 0; tf.margin_bottom = 0 p = tf.paragraphs[0]; p.alignment = align r = p.add_run(); r.text = txt r.font.name = FONT; r.font.size = Pt(size); r.font.bold = bold; r.font.color.rgb = color rPr = r._r.get_or_add_rPr(); rPr.append(rPr.makeelement(qn('a:ea'), {'typeface': FONT})) return tb def setup_master(): master = prs.slide_masters[0] shapes = master.shapes bg = _master_shape(shapes, MSO_SHAPE.RECTANGLE, -0.15, -0.15, 13.63, 7.8, BG) spTree = shapes._spTree spTree.remove(bg._element); spTree.insert(2, bg._element) _master_shape(shapes, MSO_SHAPE.RECTANGLE, -0.15, 0.0, 13.63, 0.11, BLUE) _master_shape(shapes, MSO_SHAPE.RECTANGLE, -0.15, 0.0, 4.4, 0.11, PURPLE) _master_text(shapes, 0.55, 7.02, 8.6, 0.3, "킨텍스(KINTEX) AI 시스템 · PoC(개념증명) 추진계획서", 8.5, MUTED) _master_text(shapes, 9.4, 7.02, 3.38, 0.3, "2026-08-11 · CONFIDENTIAL", 8.5, MUTED, align=PP_ALIGN.RIGHT) setup_master() def slide(): return prs.slides.add_slide(BLANK) # ================================================================ # 공통 드로잉 헬퍼 # ================================================================ def rect(s, x, y, w, h, fill=None, line=None, line_w=1.0, shape=MSO_SHAPE.RECTANGLE, shadow=False, dash=None): sp = s.shapes.add_shape(shape, inch(x), inch(y), inch(w), inch(h)) _set_fill(sp, fill) if line is None: _no_line(sp) else: sp.line.color.rgb = line; sp.line.width = Pt(line_w) if dash: ln = sp.line._get_or_add_ln(); ln.append(ln.makeelement(qn('a:prstDash'), {'val': dash})) sp.shadow.inherit = False if shadow: el = sp._element.spPr ef = el.find(qn('a:effectLst')) if ef is None: ef = el.makeelement(qn('a:effectLst'), {}); el.append(ef) sh = ef.makeelement(qn('a:outerShdw'), {'blurRad':'90000','dist':'38100','dir':'5400000','rotWithShape':'0'}) clr = sh.makeelement(qn('a:srgbClr'), {'val':'101828'}) clr.append(clr.makeelement(qn('a:alpha'), {'val':'15000'})) sh.append(clr); ef.append(sh) return sp def line_conn(s, x1, y1, x2, y2, color=SLATE, w=1.0, dash=None): ln = s.shapes.add_connector(2, inch(x1), inch(y1), inch(x2), inch(y2)) ln.line.color.rgb = color; ln.line.width = Pt(w); ln.shadow.inherit = False if dash: el = ln.line._get_or_add_ln(); el.append(el.makeelement(qn('a:prstDash'), {'val': dash})) return ln def text(s, x, y, w, h, content, size=11, color=INK, bold=False, align=PP_ALIGN.LEFT, anchor=MSO_ANCHOR.TOP, line_spacing=1.0, space_after=2, wrap=True): tb = s.shapes.add_textbox(inch(x), inch(y), inch(w), inch(h)) tf = tb.text_frame; tf.word_wrap = wrap; tf.vertical_anchor = anchor tf.margin_left = 0; tf.margin_right = 0; tf.margin_top = 0; tf.margin_bottom = 0 if isinstance(content, str): content = [content] for i, para in enumerate(content): p = tf.paragraphs[0] if i == 0 else tf.add_paragraph() p.alignment = align; p.line_spacing = line_spacing p.space_after = Pt(space_after); p.space_before = Pt(0) if isinstance(para, str): segs = [{"t": para}] elif isinstance(para, dict): segs = [para] else: segs = para for seg in segs: r = p.add_run(); r.text = seg.get("t","") r.font.name = FONT; r.font.size = Pt(seg.get("size", size)) r.font.bold = seg.get("bold", bold); r.font.color.rgb = seg.get("color", color) rPr = r._r.get_or_add_rPr(); rPr.append(rPr.makeelement(qn('a:ea'), {'typeface':FONT})) return tb def chip(s, x, y, w, h, label, fill, tc=WHITE, size=9.5, bold=True, line=None): rect(s, x, y, w, h, fill=fill, line=line, line_w=1.0, shape=MSO_SHAPE.ROUNDED_RECTANGLE) text(s, x, y, w, h, label, size=size, color=tc, bold=bold, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE) def card(s, x, y, w, h, fill=CARD, line=LINE, shadow=True): return rect(s, x, y, w, h, fill=fill, line=line, line_w=1.0, shape=MSO_SHAPE.ROUNDED_RECTANGLE, shadow=shadow) def bullets(s, x, y, w, items, dot=BLUE, size=10.5, gap=0.44, lh=1.0, dotsize=0.1, tcolor=INK): for i, it in enumerate(items): yy = y + i*gap rect(s, x, yy+0.07, dotsize, dotsize, fill=dot, shape=MSO_SHAPE.OVAL) segs = it if isinstance(it, list) else [{"t": it}] for sg in segs: sg.setdefault("size", size); sg.setdefault("color", tcolor) text(s, x+0.24, yy, w-0.24, gap, [segs], line_spacing=lh) def lead(s, x, y, w, h, txt, fill=BLUE, tc=WHITE, size=14): rect(s, x, y, w, h, fill=fill, shape=MSO_SHAPE.ROUNDED_RECTANGLE) text(s, x+0.25, y, w-0.4, h, txt, size=size, color=tc, bold=True, anchor=MSO_ANCHOR.MIDDLE) def titlecard(s, x, y, w, h, title, tcolor, items, headfill=None, bodysize=9.5, hsize=12, igap=None): card(s, x, y, w, h) if headfill: rect(s, x, y, w, 0.46, fill=headfill, shape=MSO_SHAPE.ROUNDED_RECTANGLE) text(s, x, y, w, 0.46, title, size=hsize, color=WHITE, bold=True, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE) y0 = y+0.62 else: rect(s, x, y, 0.09, h, fill=tcolor, shape=MSO_SHAPE.ROUNDED_RECTANGLE) text(s, x+0.24, y+0.13, w-0.4, 0.32, title, size=hsize, color=tcolor, bold=True) y0 = y+0.55 g = igap if igap else min(0.4, (h-(y0-y)-0.1)/max(1,len(items))) for i, it in enumerate(items): yy = y0 + i*g rect(s, x+0.22, yy+0.06, 0.09, 0.09, fill=tcolor, shape=MSO_SHAPE.OVAL) segs = it if isinstance(it, list) else [{"t": it}] for sg in segs: sg.setdefault("size", bodysize); sg.setdefault("color", INK) text(s, x+0.42, yy, w-0.6, g, [segs], line_spacing=0.94) def table(s, x, y, w, cols, rows, colw, headfill=BLUE, rowh=0.4, hsize=9.5, bsize=9, hl_rows=None, hl_fill=BLUE_LT): hl_rows = hl_rows or set() rect(s, x, y, w, rowh, fill=headfill, shape=MSO_SHAPE.ROUNDED_RECTANGLE) cx = x for j, c in enumerate(cols): text(s, cx+0.1, y, colw[j]-0.12, rowh, c, size=hsize, color=WHITE, bold=True, anchor=MSO_ANCHOR.MIDDLE, align=PP_ALIGN.CENTER if j>0 else PP_ALIGN.LEFT) cx += colw[j] for i, row in enumerate(rows): ry = y + rowh + i*rowh if i in hl_rows: rect(s, x, ry, w, rowh, fill=hl_fill) elif i % 2 == 1: rect(s, x, ry, w, rowh, fill=ZEBRA) rect(s, x, ry, w, 0.008, fill=LINE) cx = x for j, cell in enumerate(row): col = INK if j==0 else MUTED if isinstance(cell, tuple): txt, col = cell else: txt = cell text(s, cx+0.1, ry, colw[j]-0.12, rowh, txt, size=bsize, color=(INK if i in hl_rows else col), bold=(i in hl_rows), anchor=MSO_ANCHOR.MIDDLE, align=PP_ALIGN.CENTER if j>0 else PP_ALIGN.LEFT) cx += colw[j] def content_header(s, kicker, title, idx): rect(s, 0.55, 0.42, 0.62, 0.62, fill=BLUE, shape=MSO_SHAPE.ROUNDED_RECTANGLE) text(s, 0.55, 0.42, 0.62, 0.62, idx, size=(20 if len(idx)<3 else 12), color=WHITE, bold=True, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE) text(s, 1.32, 0.42, 11.3, 0.28, kicker, size=11, color=PURPLE, bold=True) text(s, 1.32, 0.66, 11.3, 0.5, title, size=18.5, color=INK, bold=True, line_spacing=0.96) rect(s, 0.55, 1.26, 12.23, 0.02, fill=LINE) def caption(s, txt, y=6.62): text(s, 0.55, y, 12.23, 0.3, txt, size=9.3, color=MUTED) def kpi(s, x, y, w, h, big, unit, label, sub, cl): card(s, x, y, w, h) rect(s, x, y, w, 0.08, fill=cl, shape=MSO_SHAPE.ROUNDED_RECTANGLE) text(s, x+0.2, y+0.28, w-0.4, 0.62, [[{"t":big,"size":30,"color":cl,"bold":True}, {"t":" "+unit,"size":12,"color":cl,"bold":True}]], anchor=MSO_ANCHOR.MIDDLE) text(s, x+0.2, y+0.96, w-0.4, 0.3, label, size=11, color=INK, bold=True) text(s, x+0.2, y+1.26, w-0.4, 0.5, sub, size=8.6, color=MUTED, line_spacing=0.95) def timeline(s, x, y, w, steps, cl=BLUE): n = len(steps); seg = w/n rect(s, x, y+0.5, w, 0.05, fill=LINE) rect(s, x, y+0.5, w*0.999, 0.05, fill=cl) for i,(date,title,desc) in enumerate(steps): cx = x + seg*i + seg/2 rect(s, cx-0.11, y+0.4, 0.22, 0.22, fill=cl, shape=MSO_SHAPE.OVAL, line=WHITE, line_w=2.0) text(s, cx-seg/2+0.1, y+0.0, seg-0.2, 0.32, date, size=9.5, color=cl, bold=True, align=PP_ALIGN.CENTER) card(s, cx-seg/2+0.14, y+0.78, seg-0.28, 1.5) text(s, cx-seg/2+0.24, y+0.9, seg-0.48, 0.5, title, size=10, color=INK, bold=True, align=PP_ALIGN.CENTER, line_spacing=0.92) text(s, cx-seg/2+0.24, y+1.42, seg-0.48, 0.78, desc, size=8.3, color=MUTED, align=PP_ALIGN.CENTER, line_spacing=0.95) def divider(no, kr, en, points): s = slide() rect(s, -0.15, -0.15, 13.63, 7.8, fill=BLUE_DK) rect(s, 8.6, -1.5, 8, 11, fill=BLUE, shape=MSO_SHAPE.PARALLELOGRAM) rect(s, 10.9, -2, 6, 12, fill=PURPLE, shape=MSO_SHAPE.PARALLELOGRAM) text(s, 1.0, 1.9, 5, 2.2, no, size=150, color=RGBColor(0x2C,0x4E,0x82), bold=True) rect(s, 1.15, 3.78, 0.9, 0.09, fill=PURPLE) text(s, 1.15, 3.95, 7, 0.8, kr, size=33, color=WHITE, bold=True) text(s, 1.18, 4.82, 7, 0.4, en, size=13, color=RGBColor(0x9C,0xB4,0xD8), bold=True) for i, p in enumerate(points): yy = 2.4 + i*0.62 rect(s, 7.55, yy+0.07, 0.16, 0.16, fill=PURPLE, shape=MSO_SHAPE.OVAL) text(s, 7.85, yy, 4.7, 0.5, p, size=12.5, color=WHITE, anchor=MSO_ANCHOR.MIDDLE) return s def add_bar(s, x, y, w, h, cats, series, title=None, colors=None, ctype=XL_CHART_TYPE.COLUMN_CLUSTERED, maxv=None, show_val=True): cd = CategoryChartData(); cd.categories = cats for nm, vals in series: cd.add_series(nm, vals) gf = s.shapes.add_chart(ctype, inch(x), inch(y), inch(w), inch(h), cd) ch = gf.chart ch.has_title = bool(title) if title: ch.chart_title.text_frame.text = title r = ch.chart_title.text_frame.paragraphs[0].runs[0] r.font.size = Pt(11); r.font.bold = True; r.font.name = FONT; r.font.color.rgb = INK ch.has_legend = len(series) > 1 if ch.has_legend: ch.legend.position = XL_LEGEND_POSITION.TOP; ch.legend.include_in_layout = False ch.legend.font.size = Pt(9); ch.legend.font.name = FONT plot = ch.plots[0]; plot.gap_width = 60 if colors and len(series)==1: for i, pt in enumerate(plot.series[0].points): pt.format.fill.solid(); pt.format.fill.fore_color.rgb = colors[i % len(colors)] elif colors: for si, sr in enumerate(plot.series): sr.format.fill.solid(); sr.format.fill.fore_color.rgb = colors[si % len(colors)] if show_val: plot.has_data_labels = True dl = plot.data_labels; dl.font.size = Pt(8.5); dl.font.name = FONT; dl.font.bold = True dl.number_format = '0'; dl.number_format_is_linked = False try: ca = ch.value_axis; ca.has_major_gridlines = True ca.tick_labels.font.size = Pt(8.5); ca.tick_labels.font.name = FONT if maxv: ca.maximum_scale = maxv; ca.minimum_scale = 0 except Exception: pass ch.category_axis.tick_labels.font.size = Pt(8.5); ch.category_axis.tick_labels.font.name = FONT return ch def add_doughnut(s, x, y, w, h, cats, vals, colors, title=None): cd = CategoryChartData(); cd.categories = cats; cd.add_series('s', vals) gf = s.shapes.add_chart(XL_CHART_TYPE.DOUGHNUT, inch(x), inch(y), inch(w), inch(h), cd) ch = gf.chart ch.has_title = bool(title) if title: ch.chart_title.text_frame.text = title r = ch.chart_title.text_frame.paragraphs[0].runs[0] r.font.size = Pt(10.5); r.font.bold = True; r.font.name = FONT; r.font.color.rgb = INK ch.has_legend = True; ch.legend.position = XL_LEGEND_POSITION.RIGHT ch.legend.include_in_layout = False; ch.legend.font.size = Pt(9); ch.legend.font.name = FONT pts = ch.plots[0].series[0].points for i, pt in enumerate(pts): pt.format.fill.solid(); pt.format.fill.fore_color.rgb = colors[i % len(colors)] return ch # ================================================================ # S01 · 표지 # ================================================================ def cover(): s = slide() rect(s, -0.15, -0.15, 13.63, 7.8, fill=BLUE_DK) rect(s, 6.4, -1.5, 9, 11, fill=BLUE, shape=MSO_SHAPE.PARALLELOGRAM) rect(s, 9.2, -2.0, 7, 12, fill=PURPLE, shape=MSO_SHAPE.PARALLELOGRAM) rect(s, 0.85, 0.8, 0.42, 0.42, fill=WHITE, shape=MSO_SHAPE.OVAL) rect(s, 0.97, 0.92, 0.18, 0.18, fill=PURPLE, shape=MSO_SHAPE.OVAL) text(s, 1.42, 0.78, 6, 0.46, "KINTEX · AI EXHIBITION SYSTEM", size=14, color=WHITE, bold=True, anchor=MSO_ANCHOR.MIDDLE) chip(s, 0.85, 2.15, 2.3, 0.44, "PoC 추진계획서", PURPLE, WHITE, size=13) chip(s, 3.27, 2.15, 2.9, 0.44, "Proof of Concept Plan", NAVY, WHITE, size=10.5) text(s, 0.85, 2.78, 11.5, 1.9, [ [{"t":"킨텍스 AI 전시관리 시스템","size":42,"color":WHITE,"bold":True}], [{"t":"개념증명(PoC) 추진계획서","size":42,"color":WHITE,"bold":True}], ], line_spacing=1.03) rect(s, 0.85, 4.62, 11.5, 0.66, fill=RGBColor(0x14,0x35,0x63), shape=MSO_SHAPE.ROUNDED_RECTANGLE) rect(s, 0.85, 4.62, 0.1, 0.66, fill=PURPLE, shape=MSO_SHAPE.ROUNDED_RECTANGLE) text(s, 1.15, 4.62, 11.0, 0.66, "무엇을·어떻게·언제 검증할 것인가 — 실증으로 도입 타당성을 증명하기 위한 PoC 실행 계획.", size=14.5, color=WHITE, bold=True, anchor=MSO_ANCHOR.MIDDLE) rect(s, 0.85, 5.95, 11.63, 0.02, fill=RGBColor(0x3A,0x5A,0x8C)) text(s, 0.85, 6.15, 8.8, 0.4, [[{"t":"검증 대상 ","size":10.5,"color":RGBColor(0x90,0xA6,0xC9)}, {"t":"부스 배치·설계·배선 자동화 + 나노바나나 시공 예측 + 공사 옥션 폐루프","size":10.5,"color":WHITE,"bold":True}]]) text(s, 0.85, 6.52, 9.4, 0.4, [[{"t":"기술스택 ","size":10.5,"color":RGBColor(0x90,0xA6,0xC9)}, {"t":"React(Vite)+TS · Spring Boot 3.x(Java17)+MyBatis · PostgreSQL(PostGIS) · Redis · 나노바나나 워커 · Claude AI","size":9,"color":WHITE}]]) text(s, 10.0, 6.15, 2.5, 0.4, "작성일 2026-08-11", size=11, color=WHITE, bold=True, align=PP_ALIGN.RIGHT) text(s, 10.0, 6.52, 2.5, 0.4, "수행 · GUARDiA/WISE 팀", size=9, color=RGBColor(0x90,0xA6,0xC9), align=PP_ALIGN.RIGHT) # ================================================================ # S02 · 목차 # ================================================================ def toc(): s = slide(); content_header(s, "CONTENTS", "목차 — PoC 추진계획서 표준 5단계", "―") items=[("1","추진 개요","배경·목적 / 검증 대상 솔루션·기술 / 검증 범위·대상",BLUE), ("2","검증 목표 및 평가 기준","핵심 검증 항목(KPI) / 성공·실패 기준 / 테스트 환경·아키텍처",PURPLE), ("3","PoC 수행 방안 및 방법","기능·성능 시나리오 / 데이터 준비·적용범위 / 시각화 검증 방법",TEAL), ("4","추진 일정 및 수행 조직","WBS·마일스톤 / 수행조직·R&R / 산출물·형상관리",BLUE), ("5","리스크 관리 및 기대효과","리스크·대응 / 보안·품질 관리 / 기대효과·본사업 전환",PURPLE), ("부록","APPENDIX","기술스택 상세 · 게이트 정의 · 체크리스트",NAVY)] for i,(no,t,d,cl) in enumerate(items): col=i//3; row=i%3 x=0.6+col*6.23; y=1.5+row*1.68 rect(s, x, y, 5.95, 1.5, fill=CARD, line=LINE, shape=MSO_SHAPE.ROUNDED_RECTANGLE, shadow=True) rect(s, x, y, 0.1, 1.5, fill=cl, shape=MSO_SHAPE.ROUNDED_RECTANGLE) text(s, x+0.3, y, 1.4, 1.5, no, size=(30 if no!="부록" else 15), color=cl, bold=True, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE) text(s, x+1.75, y+0.3, 4.0, 0.5, t, size=15, color=INK, bold=True) text(s, x+1.75, y+0.82, 4.0, 0.6, d, size=9.3, color=MUTED, line_spacing=1.0) caption(s, "본 계획서는 PoC 표준 목차(추진개요·검증목표·수행방안·일정조직·리스크)에 킨텍스 자동전시시스템 실증 계획을 정렬하여 구성.", y=6.7) # ================================================================ # S03 · Executive Summary # ================================================================ def exec_summary(): s = slide(); content_header(s, "EXECUTIVE SUMMARY", "핵심 요약 — 14일 집중 PoC로 자동전시 파이프라인의 실현 가능성을 실증한다", "요약") kpi(s, 0.55, 1.45, 2.95, 1.85, "3", "대 핵심가설", "검증 초점 3결합", "나노바나나 시공예측 · PostGIS 공간데이터 · 공사 옥션 폐루프", PURPLE) kpi(s, 3.63, 1.45, 2.95, 1.85, "14", "일 집중검증", "2026-07-11~07-24", "기획·아키텍처 → 코어 → AI/시각화 → 시연·QA, 6세션 구성", BLUE) kpi(s, 6.71, 1.45, 2.95, 1.85, "8", "개 KPI", "정량 성공 지표", "모듈·화면·시각화·배치·옥션·데이터·배포·모바일 목표 정의", TEAL) kpi(s, 9.79, 1.45, 2.99, 1.85, "6", "역할 포털", "웹·모바일 분리 검증", "주최자·참가사·공사사·관람객·관리자·운영, RBAC(JWT)+2FA", GREEN) card(s, 0.55, 3.55, 7.55, 2.75) rect(s, 0.55, 3.55, 7.55, 0.46, fill=INK, shape=MSO_SHAPE.ROUNDED_RECTANGLE) text(s, 0.55, 3.55, 7.55, 0.46, "PoC 추진 방향", size=12.5, color=WHITE, bold=True, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE) bullets(s, 0.78, 4.18, 7.1, [ [{"t":"검증 가설 — ","bold":True,"color":PURPLE},{"t":"“신청하는 순간 시공 후 사진을 먼저 본다” — 설계·시각화·발주·운영·분석을 한 흐름으로 자동화 가능한가."}], [{"t":"실증 방식 — ","bold":True,"color":BLUE},{"t":"개발서버(kintex.zioinfo.co.kr:8021)에서 E2E 라이브로 기능·성능·데이터 파이프라인을 실측 검증."}], [{"t":"평가 기준 — ","bold":True,"color":TEAL},{"t":"KPI 8종을 3등급(성공/조건부/실패)으로 판정, health 게이트·보안 불변을 통과 조건화."}], [{"t":"기반 재사용 — ","bold":True,"color":GREEN},{"t":"WISE 표준 프레임워크를 재사용하여 착수 공수를 절감하고 본사업 전환 타당성을 확인."}], ], gap=0.56, size=9.6, lh=0.98) titlecard(s, 8.28, 3.55, 4.5, 2.75, "성공의 정의", GREEN, [ [{"t":"기능 검증 ","bold":True},{"t":"— 핵심 3결합 E2E 성립","color":GREEN,"bold":True}], [{"t":"성능/안정성 ","bold":True},{"t":"— 개발서버 기준 충족","color":GREEN,"bold":True}], [{"t":"데이터 파이프라인 ","bold":True},{"t":"— 크롤·시드 폐루프 구동","color":GREEN,"bold":True}], [{"t":"운영 성숙도 ","bold":True},{"t":"— 본사업 전환 시 보완","color":AMBER,"bold":True}], "→ KPI 8종 달성 + 게이트 통과 시 '본사업 전환 권고'로 결론", ], headfill=GREEN, bodysize=9.3, igap=0.5) caption(s, "상세 목표는 §2 검증 목표·평가 기준, 실행 방법은 §3 수행 방안, 일정은 §4 추진 일정을 참조.", y=6.62) # ================================================================ # Ⅰ. 추진 개요 # ================================================================ def sec1_divider(): divider("1", "추진 개요", "PROJECT OVERVIEW", ["PoC 추진 배경 및 목적", "검증 대상 솔루션·기술 개요", "검증 범위 및 대상(Scope)"]) def s_background(): s = slide(); content_header(s, "1. 추진 개요", "1-1. PoC 추진 배경 및 목적", "1") probs = [("문제 ① 설계-발주 단절","주최·참가사가 도면을 읽기 어려워 시공 결과를 사전에 확신하지 못함 — 재작업·분쟁 발생", RED), ("문제 ② 공간데이터 분산","부스 배치·트렌치·배선 정보가 도구별로 흩어져 검증·정산·분석이 수작업", AMBER), ("문제 ③ 공사 발주 비효율","AI 설계자료와 응찰·낙찰·발주가 단절 — 견적 비교·계약 전환에 시간 소요", PURPLE)] for i,(t,d,cl) in enumerate(probs): x=0.55+i*4.12 card(s, x, 1.45, 3.95, 1.7) rect(s, x, 1.45, 3.95, 0.08, fill=cl, shape=MSO_SHAPE.ROUNDED_RECTANGLE) text(s, x+0.24, 1.66, 3.5, 0.4, t, size=11.5, color=cl, bold=True) text(s, x+0.24, 2.12, 3.5, 0.9, d, size=9.5, color=INK, line_spacing=1.05) lead(s, 0.55, 3.4, 12.23, 0.6, "PoC 목적 — “신청서를 내는 순간, 시공 후 사진을 먼저 본다”: 설계·시각화·발주·운영·분석을 한 흐름으로 자동화할 수 있음을 실증한다.", fill=BLUE, size=13) titlecard(s, 0.55, 4.2, 6.0, 2.1, "추진 목적(검증 가설)", BLUE, [ "AI가 부스 배치·설계·배선을 자동 생성하고, 나노바나나가 시공 후 사진을 예측할 수 있는가", "PostGIS 단일 공간원천으로 검증·정산·BI가 일관되게 도는가", "AI 설계자료→응찰→낙찰→발주 폐루프가 성립하는가", ], headfill=BLUE, bodysize=9.6, igap=0.55) titlecard(s, 6.73, 4.2, 6.05, 2.1, "기대 효과(가설이 참이면)", PURPLE, [ [{"t":"의사결정 가속 ","bold":True},{"t":"— 도면 없이 사진으로 즉시 컨펌"}], [{"t":"데이터 일관성 ","bold":True},{"t":"— 공간 단일원천으로 재작업·오류 감소"}], [{"t":"발주 자동화 ","bold":True},{"t":"— 옥션 폐루프로 계약 리드타임 단축"}], ], headfill=PURPLE, bodysize=9.6, igap=0.55) caption(s, "배경·목적은 docs/PLANNING.md 및 OWNER_FEEDBACK.md(소유자 지시 로그)를 근거로 정리.") def s_solution(): s = slide(); content_header(s, "1. 추진 개요", "1-2. 검증 대상 솔루션 및 기술 개요", "1") text(s, 0.55, 1.4, 12.2, 0.3, "검증 대상 = 킨텍스 자동전시시스템의 핵심 3결합(해자) + 이를 떠받치는 확정 기술스택", size=11, color=INK, bold=True) assets = [("나노바나나 시공 예측","도면을 못 읽어도 신청 시점에 '시공 후 사진'으로 의사결정 (image-to-image·구조보존)", PURPLE), ("PostGIS 실측 공간데이터","부스 폴리곤·트렌치 포인트·배선 LineString을 단일 원천에서 검증·정산·BI", BLUE), ("공사 옥션 폐루프","AI 설계자료 → 응찰 → 낙찰 → 발주 자동전환, 한 흐름에서 종결", TEAL)] for i,(t,d,cl) in enumerate(assets): x=0.55+i*4.12 card(s, x, 1.78, 3.95, 1.6) rect(s, x, 1.78, 0.09, 1.6, fill=cl, shape=MSO_SHAPE.ROUNDED_RECTANGLE) text(s, x+0.28, 1.94, 3.5, 0.4, t, size=12, color=cl, bold=True) text(s, x+0.28, 2.42, 3.55, 0.9, d, size=9.4, color=INK, line_spacing=1.05) stack = [("프론트엔드","React 18/19 + Vite + TypeScript","반응형 — 데스크톱=설계/에디터, 모바일=조회·승인·현장"), ("백엔드","Spring Boot 3.x (Java 17) + MyBatis","REST + WebSocket(STOMP), 룰 엔진·배치/배선 엔진"), ("데이터베이스","PostgreSQL + PostGIS","부스 폴리곤·트렌치 포인트·배선 LineString 공간 데이터"), ("비동기","Redis 작업 큐","RenderJob·서류·알림 비동기 처리"), ("이미지 생성","나노바나나 Python 워커(google-genai)","gemini-3.1-flash-image-preview, 오브젝트 스토리지+WS 완료 푸시"), ("AI","Claude API 기본 + 설정형 전환","AiTextRouter/AiConfig — 조건해석·검수·추천·조회·번역"), ("인증·보안","JWT + 2FA(TOTP RFC6238) · SSO(Keycloak OIDC)","6역할 RBAC·테넌트 격리, PostGIS 공간검증")] table(s, 0.55, 3.6, 12.23, ["레이어","기술","검증 관점"], stack, [2.2, 4.7, 5.33], rowh=0.4, hsize=9.5, bsize=8.5) caption(s, "확정 스택(2026-07-11, 소유자 지정) — GUARDiA 표준 프레임워크(Spring Boot 3.5 + React 19 + MyBatis + PostgreSQL)와 정렬.", y=6.68) def s_scope(): s = slide(); content_header(s, "1. 추진 개요", "1-3. 검증 범위 및 대상 (Scope)", "1") # In-scope 카드 card(s, 0.55, 1.45, 6.05, 3.55) rect(s, 0.55, 1.45, 6.05, 0.46, fill=GREEN, shape=MSO_SHAPE.ROUNDED_RECTANGLE) text(s, 0.55, 1.45, 6.05, 0.46, "검증 범위 (In-Scope)", size=12, color=WHITE, bold=True, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE) bullets(s, 0.78, 2.08, 5.6, [ [{"t":"부스 코어(M2~M5) ","bold":True},{"t":"— 배치·설계·배선·시각화 자동화"}], [{"t":"공사 옥션 폐루프 ","bold":True},{"t":"— 응찰→낙찰→발주 전환"}], [{"t":"6역할 포털 ","bold":True},{"t":"— 주최자·참가사·공사사·관람객·관리자·운영"}], [{"t":"공통·시스템관리 ","bold":True},{"t":"— WISE 이식(인증 2FA·코드·메뉴·감사)"}], [{"t":"데이터 파이프라인 ","bold":True},{"t":"— 크롤 적재 + 데모 시드 폐루프"}], [{"t":"배포 자동화 ","bold":True},{"t":"— CI/CD + health 게이트 실증"}], ], gap=0.48, size=9.5, dot=GREEN) # Out-of-scope 카드 card(s, 6.73, 1.45, 6.05, 3.55) rect(s, 6.73, 1.45, 6.05, 0.46, fill=SLATE, shape=MSO_SHAPE.ROUNDED_RECTANGLE) text(s, 6.73, 1.45, 6.05, 0.46, "검증 제외 (Out-of-Scope · 본사업 이관)", size=12, color=WHITE, bold=True, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE) bullets(s, 6.96, 2.08, 5.6, [ [{"t":"운영 HA/이중화 ","bold":True},{"t":"— 개발서버 단일 노드 범위"}], [{"t":"대규모 부하테스트 ","bold":True},{"t":"— 성능 SLA 튜닝은 본사업"}], [{"t":"외부 연동 실계약 ","bold":True},{"t":"— PG 정산·전자계약 실서비스"}], [{"t":"보안 인증·취약점 진단 ","bold":True},{"t":"— 정식 모의해킹 별도"}], [{"t":"스토어 정식 배포 ","bold":True},{"t":"— 모바일은 EAS 내부빌드까지"}], [{"t":"실 운영 데이터 이행 ","bold":True},{"t":"— 마이그레이션은 본사업"}], ], gap=0.48, size=9.5, dot=SLATE) lead(s, 0.55, 5.25, 12.23, 0.55, "대상 환경 — 개발서버 kintex.zioinfo.co.kr:8021 (PostGIS+Redis+Flyway) · 운영 kintex.wise.ai.kr 은 본사업 전환 후 구성", fill=NAVY, size=12) caption(s, "PoC는 '실현 가능성 실증'에 집중 — 비기능(HA·부하·보안점검)은 조건부 항목으로 본사업 전환 전제.", y=6.7) # ================================================================ # Ⅱ. 검증 목표 및 평가 기준 # ================================================================ def sec2_divider(): divider("2", "검증 목표 및 평가 기준", "GOALS & CRITERIA", ["핵심 검증 항목(KPI·요구사항)", "성공/실패 판단 기준", "테스트 환경 및 구성(Architecture)"]) def s_kpi(): s = slide(); content_header(s, "2. 검증 목표 및 평가 기준", "2-1. 핵심 검증 항목 (KPI 및 목표치)", "2") rows = [("KPI-01","모듈 구현","부스 코어 M2~M5 100% + 도메인","코어 완결 · 도메인 모듈 실동작", ("정량",BLUE)), ("KPI-02","화면 구현","설계 89화면 + 트랙 셸","전 화면 라우팅·데이터 배선", ("정량",BLUE)), ("KPI-03","나노바나나 시각화","시공 예측 E2E 라이브","렌더 완료·구조보존·워터마크(목표 ≤60초)", ("정량",PURPLE)), ("KPI-04","AI 자동배치·규정","자연어 해석·규정 플래깅","3안 생성 + 규정(5m·방염·리깅) 자동 검출", ("정량",PURPLE)), ("KPI-05","공사 옥션 폐루프","응찰→낙찰→발주 전이","역경매 스코어링·상태 무결 전이", ("정량",TEAL)), ("KPI-06","데이터 시드·크롤","실측 데이터 반영","다수 테이블 시드 + 포스터·행사 크롤 적재", ("정량",TEAL)), ("KPI-07","배포 자동화","CI/CD health 게이트","무중단 배포 + 공개 API 스모크 통과", ("정량",GREEN)), ("KPI-08","모바일 앱 빌드","EAS APK 산출","현장·관람 앱 내부빌드 QR 배포", ("정량",GREEN))] table(s, 0.55, 1.45, 12.23, ["ID","검증 항목","검증 목표","성공 판정 기준(목표치)","구분"], rows, [1.1, 2.5, 2.6, 4.73, 1.3], rowh=0.53, hsize=9.5, bsize=8.6) caption(s, "핵심 검증 항목은 PLANNING §8 아키텍처·design.md·백로그에서 도출. 목표치는 개발서버(kintex.zioinfo.co.kr:8021) 기준.", y=6.72) def s_criteria(): s = slide(); content_header(s, "2. 검증 목표 및 평가 기준", "2-2. 성공/실패 판단 기준 정의", "2") grades = [("성공 (Pass)","목표치 100% 달성 · 라이브 실증", GREEN, GREEN_LT), ("조건부 (Conditional)","핵심 달성 · 운영/비기능 일부 보완 필요", AMBER, AMBER_LT), ("실패 (Fail)","핵심 가설 미검증 · 재설계 필요", RED, RED_LT)] for i,(t,d,cl,bg) in enumerate(grades): x=0.55+i*4.12 rect(s, x, 1.45, 3.95, 1.15, fill=bg, line=cl, shape=MSO_SHAPE.ROUNDED_RECTANGLE) rect(s, x, 1.45, 0.09, 1.15, fill=cl, shape=MSO_SHAPE.ROUNDED_RECTANGLE) text(s, x+0.28, 1.62, 3.5, 0.4, t, size=12.5, color=cl, bold=True) text(s, x+0.28, 2.08, 3.55, 0.5, d, size=9.4, color=INK, line_spacing=1.02) rows = [("배치 자동생성","규정(높이 5m·방염·리깅 6.5~8.5m) 위반 자동 플래깅","솔버 미수렴·규정 위반 미검출"), ("나노바나나 예측","렌더 완료·구조보존·워터마크 강제 부착","타임아웃 / 원본 구조 붕괴 / 워터마크 누락"), ("공간 정합","ST_Area/ST_Intersects/ST_Length 결과 일치","공간 연산 오차·좌표계 불일치"), ("옥션 폐루프","응찰→낙찰→발주 상태 무결 전이","상태 전이 누락·데이터 불일치"), ("보안 불변","자격증명·PII·스택트레이스 미노출·2FA 통과","민감정보 노출 / 권한 우회 발생"), ("배포 게이트","health 200 + 공개 API 스모크 통과","health 실패 / 스모크 미달 / 롤백 미작동")] table(s, 0.55, 2.85, 12.23, ["검증 항목","성공 기준 (Pass)","실패 기준 (Fail)"], rows, [2.6, 5.2, 4.43], rowh=0.44, hsize=9.5, bsize=8.8, headfill=INK) caption(s, "판정은 3등급(성공/조건부/실패). 비기능(HA·부하·보안점검)은 개발서버 PoC 범위 밖 → 조건부 항목으로 분류.", y=6.66) def s_architecture(): s = slide(); content_header(s, "2. 검증 목표 및 평가 기준", "2-3. 테스트 환경 및 구성 (Architecture)", "2") def box(x,y,w,h,t,sub,fill,tc,ln=None): rect(s,x,y,w,h,fill=fill,line=(ln or fill),line_w=1.2,shape=MSO_SHAPE.ROUNDED_RECTANGLE,shadow=True) text(s,x,y+0.1,w,0.3,t,size=10,color=tc,bold=True,align=PP_ALIGN.CENTER) text(s,x,y+0.42,w,h-0.46,sub,size=7.8,color=(WHITE if fill!=WHITE else MUTED),align=PP_ALIGN.CENTER,line_spacing=0.9) rect(s,0.55,1.45,12.23,1.0,fill=BLUE_LT,line=BLUE,shape=MSO_SHAPE.ROUNDED_RECTANGLE) text(s,0.7,1.5,2.4,0.3,"클라이언트 계층",size=9.5,color=BLUE_DK,bold=True) for i,(t,sub) in enumerate([("주최자 웹","설계·운영"),("참가사 웹","신청·승인"),("공사사 포털","응찰·발주"),("관람객 웹/앱","등록·현장"),("관리자","백오피스"),("모바일","Expo/EAS")]): box(0.75+i*1.99,1.82,1.82,0.52,t,sub,WHITE,INK,BLUE) rect(s,0.55,2.62,12.23,1.55,fill=PURPLE_LT,line=PURPLE,shape=MSO_SHAPE.ROUNDED_RECTANGLE) text(s,0.7,2.67,6.0,0.3,"애플리케이션 계층 (Spring Boot 3.x · Java 17 · MyBatis)",size=9.5,color=PURPLE_DK,bold=True) for i,(t,sub) in enumerate([("REST API","6포털 서비스"),("WebSocket(STOMP)","렌더·옥션 실시간"),("룰 엔진","규정·요율"),("배치/배선 엔진","PostGIS 연산"),("AiTextRouter","Claude 기본")]): box(0.75+i*2.4,3.0,2.25,0.98,t,sub,WHITE,INK,PURPLE) rect(s,0.55,4.32,12.23,1.55,fill=RGBColor(0xEC,0xF6,0xF3),line=TEAL,shape=MSO_SHAPE.ROUNDED_RECTANGLE) text(s,0.7,4.37,6.0,0.3,"데이터 · 워커 · 인프라 계층",size=9.5,color=TEAL,bold=True) for i,(t,sub,cl) in enumerate([("PostgreSQL+PostGIS","공간 단일원천",BLUE),("Redis","작업 큐",RED),("나노바나나 워커","Gemini 이미지",PURPLE),("오브젝트 스토리지","렌더 결과",NAVY),("Gitea CI/CD","자동배포·health",GREEN)]): box(0.75+i*2.4,4.72,2.25,0.98,t,sub,WHITE,INK,cl) lead(s, 0.55, 6.02, 12.23, 0.5, "PoC 서버 — kintex.zioinfo.co.kr:8021 (개발) · Flyway 마이그레이션 라이브 dry-run(BEGIN…ROLLBACK) 후 반영 · 시크릿 fail-fast 프로파일", fill=NAVY, size=11.5) # ================================================================ # Ⅲ. PoC 수행 방안 및 방법 # ================================================================ def sec3_divider(): divider("3", "PoC 수행 방안 및 방법", "EXECUTION METHOD", ["기능·성능 검증 시나리오", "데이터 준비 및 적용 범위", "나노바나나 시각화 검증 방법"]) def s_scenario(): s = slide(); content_header(s, "3. PoC 수행 방안 및 방법", "3-1. 기능·성능 검증 시나리오", "3") text(s, 0.55, 1.4, 12.2, 0.3, "핵심 사용자 여정을 E2E 시나리오로 정의하고, 각 단계의 통과 조건을 사전 명세 — 개발서버에서 라이브 실행", size=10.5, color=INK, bold=True) steps = [("S-1 신청·설계","참가사 신청 → AI 부스 배치 3안 생성 → 규정 자동 플래깅 → 배선/물량 산출", BLUE), ("S-2 시각화","확정 배치 → 나노바나나 시공 후 사진 렌더 → Before/After 비교 → 워터마크 확인", PURPLE), ("S-3 발주 옥션","AI 설계자료 공개 → 공사사 응찰 → 역경매 스코어링 → 낙찰·발주 전환", TEAL), ("S-4 운영·분석","현장 체크인·리드 캡처 → 정산 집계 → BI 대시보드 → KPI 리포트", GREEN)] for i,(t,d,cl) in enumerate(steps): y=1.85+i*0.82 rect(s, 0.55, y, 0.5, 0.7, fill=cl, shape=MSO_SHAPE.ROUNDED_RECTANGLE) text(s, 0.55, y, 0.5, 0.7, "S%d"%(i+1), size=13, color=WHITE, bold=True, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE) card(s, 1.2, y, 11.58, 0.7) text(s, 1.42, y+0.09, 3.0, 0.5, t, size=11, color=cl, bold=True, anchor=MSO_ANCHOR.MIDDLE) text(s, 4.4, y+0.09, 8.2, 0.5, d, size=9.4, color=INK, anchor=MSO_ANCHOR.MIDDLE, line_spacing=0.95) # 성능 관점 titlecard(s, 0.55, 5.28, 6.05, 1.1, "성능 검증 관점", NAVY, [ [{"t":"렌더 응답 ","bold":True},{"t":"— 나노바나나 렌더 목표 ≤60초, WS 완료 푸시"}], [{"t":"공간 연산 ","bold":True},{"t":"— PostGIS ST_* 쿼리 정확·응답성"}], ], headfill=NAVY, bodysize=9.2, igap=0.32) titlecard(s, 6.73, 5.28, 6.05, 1.1, "검증 방법론", BLUE, [ [{"t":"점진 QA ","bold":True},{"t":"— 각 모듈 완성 직후 경계면(shape) 교차검증"}], [{"t":"게이트 ","bold":True},{"t":"— Phase별 QA PASS 후 다음 단계 진행"}], ], headfill=BLUE, bodysize=9.2, igap=0.32) caption(s, "시나리오·통과조건은 design.md 화면흐름 + 백로그 인수기준에 정렬. 실패 시 원인 분석 → 보완 → 재검증 루프.", y=6.5) def s_data(): s = slide(); content_header(s, "3. PoC 수행 방안 및 방법", "3-2. 데이터 준비 및 적용 범위", "3") text(s, 0.55, 1.4, 12.2, 0.3, "실감형 실증을 위해 실측 자산 크롤 + 멱등 데모 시드(Flyway)로 전 모듈 데이터를 사전 구축", size=10.5, color=INK, bold=True) # 데이터 소스 3분류 cats = [("실측 자산 (크롤)","kintex.com 행사·포스터·관람안내·교통 정보를 크롤 → 멱등 시드로 적재", PURPLE, PURPLE_LT), ("도면·공간 데이터","제1전시장 평면도 JPG 15장 + CAD(DWG, 트렌치 포함) → PostGIS 폴리곤·포인트·라인", BLUE, BLUE_LT), ("데모 트랜잭션 시드","행사·부스·참가사·옥션·리드·정산·결재·공지·알림을 폐루프로 연결", TEAL, RGBColor(0xE9,0xF6,0xF4))] for i,(t,d,cl,bg) in enumerate(cats): x=0.55+i*4.12 rect(s, x, 1.82, 3.95, 1.5, fill=bg, line=cl, shape=MSO_SHAPE.ROUNDED_RECTANGLE) rect(s, x, 1.82, 0.09, 1.5, fill=cl, shape=MSO_SHAPE.ROUNDED_RECTANGLE) text(s, x+0.28, 1.98, 3.55, 0.4, t, size=11, color=cl, bold=True) text(s, x+0.28, 2.42, 3.55, 0.85, d, size=9.2, color=INK, line_spacing=1.05) # 데이터 준비 원칙 표 rows = [("멱등성","Flyway 마이그레이션으로 재실행 안전 — 라이브 dry-run(BEGIN…ROLLBACK) 후 반영"), ("행사 스코프 정합","데모 해소행사에 시드를 묶어 '화면이 비어 보이는' 오탐 방지"), ("데이터 표준","공통코드·view/mview·배치 집계 — FK 최소화로 이식성 확보"), ("PII·보안","테스트 데이터는 가명 처리, 민감정보 미노출 불변 준수"), ("공간 검증","좌표계(SRID) 일관 · ST_Area/ST_Length로 물량·정산 교차검증")] table(s, 0.55, 3.55, 12.23, ["준비 원칙","내용"], rows, [2.6, 9.63], rowh=0.46, hsize=9.5, bsize=9, headfill=NAVY) caption(s, "확보 자산: docs/assets/floorplans/ 홀별 평면도 15장 + CAD. 데이터 표준은 data.md, 크롤은 kintex-crawler 트랙.", y=6.5) def s_nanobanana(): s = slide(); content_header(s, "3. PoC 수행 방안 및 방법", "3-3. 나노바나나 시각화 검증 방법", "3") text(s, 0.55, 1.4, 12.2, 0.3, "PoC 최대 차별점 — '시공 후 사진 예측'을 image-to-image 파이프라인으로 검증한다", size=11, color=INK, bold=True) # 파이프라인 단계 flow = [("설계 입력","배치·부스 도면",BLUE),("프롬프트 생성","보존/교체 규칙",PURPLE),("Gemini 렌더","워커 큐 처리",TEAL), ("구조 검증","원본 보존 확인",NAVY),("워터마크·저장","오브젝트 스토리지",GREEN)] n=len(flow); seg=12.23/n for i,(t,d,cl) in enumerate(flow): x=0.55+i*seg card(s, x+0.06, 1.85, seg-0.4, 1.1) rect(s, x+0.06, 1.85, seg-0.4, 0.08, fill=cl, shape=MSO_SHAPE.ROUNDED_RECTANGLE) text(s, x+0.06, 2.02, seg-0.4, 0.4, t, size=10.5, color=cl, bold=True, align=PP_ALIGN.CENTER) text(s, x+0.06, 2.44, seg-0.4, 0.45, d, size=8.6, color=MUTED, align=PP_ALIGN.CENTER, line_spacing=0.95) if i