kintex/docs/deliverables/제안서/_gen/build_deck.py

1943 lines
136 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# -*- 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)