diff --git a/docs/deliverables/POC계획보고서/_gen/gen_poc_plan.py b/docs/deliverables/POC계획보고서/_gen/gen_poc_plan.py new file mode 100644 index 0000000..f42a275 --- /dev/null +++ b/docs/deliverables/POC계획보고서/_gen/gen_poc_plan.py @@ -0,0 +1,852 @@ +# -*- 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