kintex/docs/deliverables/_gen/gen_dev_plan_pptx.py

786 lines
41 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 생성기
- 소스: docs/PLANNING.md(v3.0), docs/IMPLEMENTATION_BACKLOG.md, docs/WORK_STATUS.md,
docs/architecture/*, CLAUDE.md 기술 스택표
- 디자인: KINTEX Blue #0066B3 / AI Purple #6D4AFF / 중립 그레이 / 맑은 고딕 / 16:9
- 도식(추진체계도·시스템 구성도·간트)은 python-pptx 도형으로 직접 그림(외부 이미지 금지)
- 보안 불변: 비밀번호/API키/서버IP/SSH 계정 미기재
재실행: python gen_dev_plan_pptx.py
"""
import os
from pptx import Presentation
from pptx.util import Emu, Pt
from pptx.dml.color import RGBColor
from pptx.enum.text import PP_ALIGN, MSO_ANCHOR
from pptx.enum.shapes import MSO_SHAPE
from pptx.enum.text import MSO_AUTO_SIZE
from pptx.oxml.ns import qn
# ---------------- 팔레트 ----------------
BLUE = RGBColor(0x00, 0x66, 0xB3) # KINTEX Blue (primary)
BLUE_DK = RGBColor(0x00, 0x44, 0x7A) # deep
BLUE_LT = RGBColor(0xE1, 0xEF, 0xF9) # light tint
PURPLE = RGBColor(0x6D, 0x4A, 0xFF) # AI accent
PURPLE_LT = RGBColor(0xEC, 0xE8, 0xFF)
INK = RGBColor(0x10, 0x18, 0x28) # text
MUTED = RGBColor(0x66, 0x70, 0x85) # secondary text
LINE = RGBColor(0xE4, 0xE7, 0xEC) # borders
BG = RGBColor(0xF9, 0xFA, 0xFB) # page bg
WHITE = RGBColor(0xFF, 0xFF, 0xFF)
GREEN = RGBColor(0x12, 0x9E, 0x63) # 완료
AMBER = RGBColor(0xE0, 0x8A, 0x00) # 진행
SLATE = RGBColor(0x98, 0xA2, 0xB3) # 예정
CARD = RGBColor(0xFF, 0xFF, 0xFF)
HALL_BG = RGBColor(0xF2, 0xF4, 0xF7)
FONT = "맑은 고딕"
# 16:9
EMU_W = 12192000
EMU_H = 6858000
prs = Presentation()
prs.slide_width = EMU_W
prs.slide_height = EMU_H
BLANK = prs.slide_layouts[6]
IN = 914400
def inch(v): return int(v * IN)
# ---------------- 헬퍼 ----------------
def slide():
return prs.slides.add_slide(BLANK)
def _set_fill(shape, color):
if color is None:
shape.fill.background()
else:
shape.fill.solid()
shape.fill.fore_color.rgb = color
def _no_line(shape):
shape.line.fill.background()
def rect(s, x, y, w, h, fill=None, line=None, line_w=1.0, shape=MSO_SHAPE.RECTANGLE, shadow=False):
sp = s.shapes.add_shape(shape, inch(x), inch(y), inch(w), inch(h))
_set_fill(sp, fill)
if line is None:
_no_line(sp)
else:
sp.line.color.rgb = line
sp.line.width = Pt(line_w)
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':'18000'})
clr.append(alp); sh.append(clr); ef.append(sh)
return sp
def _apply_runs(tf, para, segs, align, default_size, default_color, default_bold):
para.alignment = align
first = True
for seg in segs:
text = seg.get("t","")
r = para.add_run() if not first else para.runs[0] if para.runs else para.add_run()
# ensure a run exists
if first and not para.runs:
r = para.add_run()
r.text = text
r.font.name = FONT
r.font.size = Pt(seg.get("size", default_size))
r.font.bold = seg.get("bold", default_bold)
r.font.color.rgb = seg.get("color", default_color)
# set east asian font
rPr = r._r.get_or_add_rPr()
ea = rPr.makeelement(qn('a:ea'), {'typeface':FONT})
rPr.append(ea)
first = False
def text(s, x, y, w, h, content, size=14, 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 또는 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()
ea = rPr.makeelement(qn('a:ea'), {'typeface':FONT})
rPr.append(ea)
return tb
def page_bg(s, color=BG):
rect(s, -0.1, -0.1, 13.53, 7.7, fill=color)
PAGE = {"n": 0}
def content_header(s, kicker, title, idx):
"""본문 슬라이드 공통 헤더 + 푸터"""
page_bg(s, BG)
# top accent bar
rect(s, 0, 0, 13.333, 0.12, fill=BLUE)
# section number chip
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}", size=20, color=WHITE, bold=True,
align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)
text(s, 1.32, 0.5, 10.5, 0.28, kicker, size=11, color=PURPLE, bold=True)
text(s, 1.32, 0.74, 11.4, 0.44, title, size=22, color=INK, bold=True)
rect(s, 0.55, 1.28, 12.23, 0.02, fill=LINE)
# footer
text(s, 0.55, 7.06, 8, 0.3, "킨텍스 자동전시시스템 구축 · 개발계획서", size=8.5, color=MUTED)
text(s, 11.3, 7.06, 1.48, 0.3, "2026-07-11", size=8.5, color=MUTED, align=PP_ALIGN.RIGHT)
def chip(s, x, y, w, h, label, fill, txtcolor=WHITE, size=9.5, bold=True):
rect(s, x, y, w, h, fill=fill, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
text(s, x, y, w, h, label, size=size, color=txtcolor, bold=bold,
align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)
def card(s, x, y, w, h, fill=CARD, line=LINE):
return rect(s, x, y, w, h, fill=fill, line=line, line_w=1.0,
shape=MSO_SHAPE.ROUNDED_RECTANGLE, shadow=True)
# ================================================================
# 1. 표지
# ================================================================
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.35, 1.75, 0.42, "개발계획서", PURPLE, WHITE, size=12)
chip(s, 2.72, 2.35, 2.1, 0.42, "Development Plan", RGBColor(0x2B,0x4C,0x7E), WHITE, size=10.5)
# 타이틀
text(s, 0.85, 2.95, 11, 1.9, [
[{"t":"킨텍스 자동전시시스템", "size":46, "color":WHITE, "bold":True}],
[{"t":"구축 개발계획서", "size":46, "color":WHITE, "bold":True}],
], line_spacing=1.02)
text(s, 0.88, 4.78, 11, 0.5,
"Exhibition Automation Platform — 다중 전시관 멀티테넌트 SaaS",
size=16, color=RGBColor(0xC7,0xDD,0xF2), bold=False)
# 폐루프 한 줄
text(s, 0.88, 5.35, 11.4, 0.4,
"판매 → AI 설계·시각화 → 공사 옥션 → 시공 → 운영 → 경영분석 폐루프 자동화",
size=12.5, color=RGBColor(0xB9,0xC7,0xEA))
# 하단 메타 바
rect(s, 0.85, 6.15, 11.63, 0.02, fill=RGBColor(0x3A,0x5A,0x8C))
text(s, 0.85, 6.35, 8, 0.4, [
[{"t":"사업명 ","size":11,"color":RGBColor(0x8FA,0x00,0x00) if False else RGBColor(0x90,0xA6,0xC9)},
{"t":"킨텍스 자동전시시스템 구축","size":11.5,"color":WHITE,"bold":True}],
])
text(s, 0.85, 6.72, 9, 0.4, [
[{"t":"기술스택 ","size":11,"color":RGBColor(0x90,0xA6,0xC9)},
{"t":"React(Vite)+TS · Spring Boot 3.x(Java17)+MyBatis · PostgreSQL(PostGIS) · Redis · 나노바나나 워커 · Claude AI","size":10.5,"color":WHITE}],
])
text(s, 9.7, 6.35, 2.78, 0.4, "작성일 2026-07-11", size=11, color=WHITE, bold=True, align=PP_ALIGN.RIGHT)
text(s, 9.7, 6.72, 2.78, 0.4, "planner · designer · dev team", size=9.5, color=RGBColor(0x90,0xA6,0xC9), align=PP_ALIGN.RIGHT)
# ================================================================
# 2. 목차
# ================================================================
s = slide()
content_header(s, "CONTENTS", "목차", 0)
# 위 헤더의 00 chip을 목차용으로 덮기
rect(s, 0.55, 0.5, 0.62, 0.62, fill=PURPLE, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
text(s, 0.55, 0.5, 0.62, 0.62, "C", size=22, color=WHITE, bold=True, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)
toc = [
("01", "사업 개요", "배경·목적·범위 / 전시 생애주기 폐루프"),
("02", "시스템 구성", "아키텍처 개요 / 역할별 포털 / 나노바나나 파이프라인"),
("03", "개발 범위", "모듈 M1~M18 + 공통·시스템관리 레이어(§5B)"),
("04", "추진 체계", "거버넌스·아키텍트·개발·QA·DevOps 조직"),
("05", "추진 일정", "Phase A~E 마스터 일정(완료/진행/예정)"),
("06", "개발 방법론·품질관리", "에이전트 파이프라인 · 점진 QA · CI/CD"),
("07", "보안 · 비기능요건(NFR)", "외부API 통제 · AI 워터마크 · 접근성·다국어"),
("08", "리스크 및 대응", "기술·데이터·연동·보안 리스크"),
("09", "기대 효과", "정량·정성 효과 / 확장 로드맵"),
]
col_w = 5.85
for i, (no, t, d) in enumerate(toc):
col = i // 5
row = i % 5
x = 0.7 + col * (col_w + 0.35)
y = 1.62 + row * 1.02
rect(s, x, y, col_w, 0.88, fill=CARD, line=LINE, line_w=1.0, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
rect(s, x, y, 0.09, 0.88, fill=BLUE if col==0 else PURPLE, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
text(s, x+0.28, y, 0.9, 0.88, no, size=24, color=(BLUE if col==0 else PURPLE), bold=True, anchor=MSO_ANCHOR.MIDDLE)
text(s, x+1.15, y+0.15, col_w-1.3, 0.35, t, size=14.5, color=INK, bold=True)
text(s, x+1.15, y+0.5, col_w-1.3, 0.32, d, size=9.5, color=MUTED)
# ================================================================
# 섹션 간지 헬퍼
# ================================================================
def divider(num, kr, en, points):
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.15, 5, 2.2, num, 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, 8, 0.8, kr, size=34, color=WHITE, bold=True)
text(s, 1.18, 4.85, 8, 0.4, en, size=13, color=RGBColor(0x9C,0xB4,0xD8), bold=True)
# 우측 포인트
for i, p in enumerate(points):
yy = 2.35 + i*0.62
rect(s, 8.05, yy+0.06, 0.16, 0.16, fill=PURPLE, shape=MSO_SHAPE.OVAL)
text(s, 8.35, yy, 4.4, 0.5, p, size=12.5, color=WHITE, anchor=MSO_ANCHOR.MIDDLE)
return s
# ================================================================
# 3. 사업 개요 (간지 + 본문)
# ================================================================
divider("01", "사업 개요", "PROJECT OVERVIEW",
["배경 · 목적 · 범위", "전시 생애주기 폐루프", "다중 전시관 SaaS 비전"])
s = slide()
content_header(s, "01 · 사업 개요", "배경 · 목적 · 추진 범위", 1)
# 좌: 배경/목적
card(s, 0.55, 1.5, 6.05, 2.55)
text(s, 0.85, 1.7, 5.5, 0.35, "추진 배경 (As-Is)", size=13, color=BLUE, bold=True)
for i, t in enumerate([
"전시면적 108,011㎡ 국내 최대 전시장이나 준비 실무는 HWP 서식·이메일 제출에 정체",
"CAD 수작업 배치도 → 홀매니저 육안 검수, 수기 위치표시도 기반 유틸리티 신청",
"참가업체 유틸리티 신청이 행사별 사무국 시스템으로 파편화(공통 플랫폼 부재)",
"시공 결과를 개장일에 처음 확인 — 사전 시각화 수단 없음",
]):
y = 2.1 + i*0.46
rect(s, 0.9, y+0.07, 0.1, 0.1, fill=MUTED, shape=MSO_SHAPE.OVAL)
text(s, 1.12, y, 5.25, 0.44, t, size=10.5, color=INK, line_spacing=1.0)
card(s, 6.75, 1.5, 6.03, 2.55)
text(s, 7.05, 1.7, 5.5, 0.35, "추진 목적 (To-Be)", size=13, color=PURPLE, bold=True)
for i, t in enumerate([
"전시장 운영 전 과정을 AI로 자동 설계·검증하는 베뉴 운영 플랫폼 구축",
"핵심 차별화: 나노바나나(Gemini)로 시공 전 '공사 후 결과 사진' 선(先)시각화",
"설계 → 시각화 → 발주(옥션) → 시공 → 운영 → 분석의 폐루프 완성",
"KINTEX 기준 테넌트 + 코엑스 등 다중 전시관 멀티테넌트 SaaS 확장",
]):
y = 2.1 + i*0.46
rect(s, 7.1, y+0.07, 0.1, 0.1, fill=PURPLE, shape=MSO_SHAPE.OVAL)
text(s, 7.32, y, 5.25, 0.44, t, size=10.5, color=INK, line_spacing=1.0)
# 하단: 비전 배너 + 폐루프
rect(s, 0.55, 4.28, 12.23, 0.62, fill=BLUE, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
text(s, 0.85, 4.28, 11.6, 0.62,
"비전 “신청서를 내는 순간, 시공 후 사진을 먼저 본다”",
size=17, color=WHITE, bold=True, anchor=MSO_ANCHOR.MIDDLE)
text(s, 0.55, 5.12, 6, 0.3, "전시 생애주기 폐루프", size=12, color=INK, bold=True)
loop = [("판매·기획", BLUE, "M1·M16"), ("AI 설계·시각화", PURPLE, "M2~M5"),
("공사 옥션", BLUE, "M15"), ("시공", BLUE, "M7·M8"),
("운영", BLUE, "M10·M14"), ("경영분석", PURPLE, "M16")]
bw = 1.83; gap = 0.19; x0 = 0.55; yb = 5.5
for i, (lb, cl, mod) in enumerate(loop):
x = x0 + i*(bw+gap)
rect(s, x, yb, bw, 0.78, fill=(PURPLE_LT if cl==PURPLE else BLUE_LT), line=cl, line_w=1.2, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
text(s, x, yb+0.1, bw, 0.34, lb, size=11.5, color=(PURPLE if cl==PURPLE else BLUE_DK), bold=True, align=PP_ALIGN.CENTER)
text(s, x, yb+0.44, bw, 0.28, mod, size=9, color=MUTED, align=PP_ALIGN.CENTER)
if i < len(loop)-1:
rect(s, x+bw+0.01, yb+0.28, gap+0.02, 0.22, fill=None)
text(s, x+bw-0.03, yb+0.16, 0.28, 0.5, "", size=15, color=SLATE, bold=True, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)
# ================================================================
# 4. 시스템 구성 (간지 + 아키텍처 도식)
# ================================================================
divider("02", "시스템 구성", "SYSTEM ARCHITECTURE",
["3-Tier + 나노바나나 사이드카", "역할별 6개 포털 분리", "PostGIS 공간 데이터 모델"])
s = slide()
content_header(s, "02 · 시스템 구성", "전체 아키텍처 개요도", 2)
def band(y, h, fill, line, label, labelcolor):
rect(s, 0.55, y, 12.23, h, fill=fill, line=line, line_w=1.0, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
rect(s, 0.55, y, 1.55, h, fill=line, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
text(s, 0.55, y, 1.55, h, label, size=10.5, color=WHITE, bold=True, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)
def node(x, y, w, h, title, sub, fill, tcol, line=None):
rect(s, x, y, w, h, fill=fill, line=(line or LINE), line_w=1.0, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
text(s, x, y+0.08, w, 0.3, title, size=10, color=tcol, bold=True, align=PP_ALIGN.CENTER)
if sub:
text(s, x, y+0.36, w, 0.24, sub, size=7.8, color=MUTED, align=PP_ALIGN.CENTER)
# Presentation tier — 포털
band(1.5, 0.86, BLUE_LT, BLUE, "프론트\n(React)", WHITE)
ports = ["주최자 콘솔","참가업체 포털","업체 포털","운영 대시보드","관리자 백오피스","공개사이트·앱"]
pw = 1.62
for i,p in enumerate(ports):
node(2.25+i*1.72, 1.66, pw, 0.54, p, "", WHITE, INK, BLUE)
# API tier
band(2.55, 0.9, WHITE, BLUE_DK, "백엔드\n(Spring)", WHITE)
node(2.25, 2.72, 2.7, 0.56, "REST + WebSocket(STOMP)", "JWT·RBAC·2FA(OTP)", WHITE, INK, BLUE)
node(5.1, 2.72, 2.4, 0.56, "룰 엔진 / 배치·배선 엔진", "요율·규정·PostGIS 연산", WHITE, INK, BLUE)
node(7.65, 2.72, 2.2, 0.56, "AiTextRouter", "Claude 기본·설정형 전환", WHITE, PURPLE, PURPLE)
node(10.0, 2.72, 2.65, 0.56, "RenderJob 큐 발행", "Redis 비동기", WHITE, INK, BLUE)
# AI / 워커 tier
band(3.62, 0.9, PURPLE_LT, PURPLE, "AI·시각화", WHITE)
node(2.25, 3.79, 4.15, 0.56, "나노바나나 Python 워커 (사이드카)", "google-genai · gemini-3.1-flash-image-preview", PURPLE_LT, PURPLE, PURPLE)
node(6.55, 3.79, 3.0, 0.56, "Claude API (AI 지능)", "설계·검수·예측 · Ollama 폴백", PURPLE_LT, PURPLE, PURPLE)
node(9.72, 3.79, 2.93, 0.56, "WebSocket 완료 푸시", "렌더 결과 실시간 통지", WHITE, INK, PURPLE)
# Data tier
band(4.69, 0.9, HALL_BG, BLUE_DK, "데이터", WHITE)
node(2.25, 4.86, 3.3, 0.56, "PostgreSQL + PostGIS", "부스 폴리곤·트렌치·배선 LineString", WHITE, INK, BLUE_DK)
node(5.7, 4.86, 2.4, 0.56, "Redis", "작업 큐·세션·캐시", WHITE, INK, BLUE_DK)
node(8.25, 4.86, 2.05, 0.56, "오브젝트 스토리지", "렌더 이미지·문서", WHITE, INK, BLUE_DK)
node(10.45, 4.86, 2.2, 0.56, "감사로그·마스터", "룰셋·요율 버전", WHITE, INK, BLUE_DK)
# 외부 연동
text(s, 0.55, 5.86, 12, 0.3, "외부·기존 시스템 연동", size=10.5, color=INK, bold=True)
ext = ["kxwp 작업신고","등록업체 DB 739","행사일정 시스템","PG 결제","Gemini","사이니지/wayfinding"]
for i,e in enumerate(ext):
node(0.55+i*2.06, 6.16, 1.96, 0.5, e, "", WHITE, MUTED, LINE)
# ================================================================
# 5. 개발 범위 (모듈 표)
# ================================================================
divider("03", "개발 범위", "DEVELOPMENT SCOPE",
["코어 M2~M5 (P0)", "도메인 M1·M6~M18", "공통·시스템관리 §5B"])
s = slide()
content_header(s, "03 · 개발 범위", "모듈 맵 M1 ~ M18", 3)
def module_card(x, y, w, h, code, name, pr, cl):
rect(s, x, y, w, h, fill=CARD, line=LINE, line_w=1.0, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
rect(s, x, y, w, 0.07, fill=cl, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
text(s, x+0.14, y+0.14, w-0.28, 0.26, [[{"t":code+" ","size":11,"color":cl,"bold":True},
{"t":pr,"size":8,"color":WHITE,"bold":True}]], )
# priority chip
rect(s, x+w-0.72, y+0.13, 0.58, 0.24, fill=cl, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
text(s, x+w-0.72, y+0.13, 0.58, 0.24, pr, size=8, color=WHITE, bold=True, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)
text(s, x+0.14, y+0.4, w-0.28, 0.5, name, size=9.2, color=INK, line_spacing=0.95)
# 계열별 컬럼
groups = [
("설계·시각화 코어", PURPLE, [
("M2","플로어플랜 스튜디오","P0"),
("M3","부스 설계 스튜디오","P0"),
("M4","유틸리티 설계(전기·조명·네트워크)","P0"),
("M5","나노바나나 시각화","P0"),
]),
("판매·발주·정산", BLUE, [
("M1","홀 배정·자동 견적","P1"),
("M6","서류·마일스톤 워크플로","P1"),
("M7","등록업체 매칭·견적","P1"),
("M15","공사/장치 옥션(역경매)","P1"),
("M8·M9","반입/반출 물류 · 정산·결제","P1·P2"),
]),
("관람·참가·마케팅", BLUE, [
("M10","관람객 등록·배지·리드캡처","P1"),
("M12","마케팅·공개 홍보 사이트","P1"),
("M11·M13","비즈매칭 · wayfinding","P2"),
("M14","현장운영(혼잡·안전·에너지)","P2"),
]),
("경영·콘텐츠·관리", PURPLE, [
("M16","경영분석 BI(P&L·가동률·예측)","P1"),
("M17","CMS·마이크로사이트·다국어","P1"),
("M18","관리자 백오피스·마스터데이터","P1"),
]),
]
cw = 2.94; gap = 0.16; x0 = 0.55
for gi,(gname, gcolor, mods) in enumerate(groups):
x = x0 + gi*(cw+gap)
rect(s, x, 1.5, cw, 0.44, fill=gcolor, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
text(s, x, 1.5, cw, 0.44, gname, size=11, color=WHITE, bold=True, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)
for mi,(code,name,pr) in enumerate(mods):
y = 2.06 + mi*0.9
module_card(x, y, cw, 0.82, code, name, pr, gcolor)
# 범례
text(s, 0.55, 6.62, 12, 0.3, [[
{"t":"P0 ","size":9.5,"color":PURPLE,"bold":True},{"t":"핵심 차별화·첫 상용 필수 ","size":9.5,"color":MUTED},
{"t":"P1 ","size":9.5,"color":BLUE,"bold":True},{"t":"운영 효율 핵심 ","size":9.5,"color":MUTED},
{"t":"P2 ","size":9.5,"color":SLATE,"bold":True},{"t":"확장 │ 총 18개 도메인 모듈 + 공통·시스템관리 레이어(§5B)","size":9.5,"color":MUTED},
]])
# 개발 범위 2 — 공통 레이어 §5B
s = slide()
content_header(s, "03 · 개발 범위", "공통 · 시스템관리 레이어 (§5B · UIWS/WISE 표준 이식)", 3)
text(s, 0.55, 1.45, 12.2, 0.4,
"GUARDiA 표준 프레임워크(UIWS)와 동일 스택 — 시스템관리·공통 업무기능·인증을 재설계 없이 이식하여 전 도메인 모듈의 선행 기반으로 정착",
size=11, color=MUTED)
# 3개 카드 영역
def scard(x, y, w, h, title, tcolor, items):
card(s, x, y, w, h)
rect(s, x, y, w, 0.5, fill=tcolor, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
text(s, x, y, w, 0.5, title, size=12.5, color=WHITE, bold=True, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)
for i,it in enumerate(items):
yy = y+0.66+i*0.4
rect(s, x+0.2, yy+0.07, 0.09, 0.09, fill=tcolor, shape=MSO_SHAPE.OVAL)
text(s, x+0.4, yy, w-0.55, 0.38, it, size=9.6, color=INK, line_spacing=0.95)
scard(0.55, 2.0, 3.95, 4.35, "시스템관리 (M18 통합)", BLUE, [
"사용자 관리 (6역할 + 등록업체·관람객 계정)",
"역할/권한 RBAC (플랫폼·행사 이중 평가)",
"공통코드 (홀·부스유형·공종14분류·요금)",
"메뉴 관리 (역할별 포털 IA 구동)",
"감사로그 (승인·낙찰·룰셋개정·리드접근)",
"시스템설정 (요율/규정 룰셋 버전)",
"킨텍스 마스터데이터 (홀·요율·규정·업체)",
])
scard(4.65, 2.0, 3.95, 4.35, "공통 업무 기능", PURPLE, [
"worklog 업무일지 · schedule 일정",
"message 쪽지 · notice 공지",
"opinion 의견 · search 통합검색",
"meeting 회의록(STT→Jasper PDF)",
"report/stats 업무보고·통계(PDF)",
"notification 알림센터(WebSocket)",
"preference 개인화(즐겨찾기·최근방문)",
])
scard(8.75, 2.0, 4.03, 4.35, "인증 (JWT + 2FA/OTP)", BLUE_DK, [
"JWT + RBAC 역할 게이트",
"2차 인증 OTP (TOTP RFC6238)",
"최초 QR 등록 · 재설정 · 관리자 초기화",
"로그인 실패 잠금 + 관리자 해제",
"admin 비번 env 주입 (AES-256-GCM)",
"admin123 하드코딩 시드 금지",
"TB_USER·OTP·TB_AUDIT_LOG 멱등 이식",
])
# ================================================================
# 6. 추진 체계 (조직도)
# ================================================================
divider("04", "추진 체계", "PROJECT ORGANIZATION",
["거버넌스 (PM·개발PM·PMO)", "아키텍트 5인 (AA/SA/TA/DA/NA)", "구현·품질·배포 팀"])
s = slide()
content_header(s, "04 · 추진 체계", "프로젝트 조직 및 역할", 4)
def org(x, y, w, h, title, sub, fill, tcol, line=None, big=False):
rect(s, x, y, w, h, fill=fill, line=(line or fill), line_w=1.2, shape=MSO_SHAPE.ROUNDED_RECTANGLE, shadow=big)
text(s, x, y+(0.1 if sub else 0), w, (0.32 if sub else h), title, size=(12 if big else 10.5),
color=tcol, bold=True, align=PP_ALIGN.CENTER, anchor=(MSO_ANCHOR.TOP if sub else MSO_ANCHOR.MIDDLE))
if sub:
subcol = MUTED if fill == WHITE else WHITE
text(s, x, y+0.36, w, h-0.4, sub, size=8, color=subcol, align=PP_ALIGN.CENTER)
# 최상위 거버넌스
org(4.9, 1.5, 3.55, 0.72, "프로젝트 총괄 (PM)", "범위·일정·리스크·게이트(G1·G2)", BLUE, WHITE, big=True)
# 연결선
rect(s, 6.66, 2.22, 0.02, 0.3, fill=SLATE)
# 2단: 개발PM / PMO
org(2.95, 2.55, 3.35, 0.66, "개발 PM", "WBS·스프린트·번다운·품질게이트", PURPLE, WHITE)
org(7.05, 2.55, 3.35, 0.66, "PMO", "관리표준·형상관리·QA 거버넌스·변경관리", PURPLE, WHITE)
rect(s, 6.66, 2.52, 0.02, 0.16, fill=SLATE)
# 3단: 아키텍트 그룹 + 구현 그룹
rect(s, 0.55, 3.5, 12.23, 0.4, fill=BLUE_LT, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
text(s, 0.7, 3.5, 6, 0.4, "아키텍트 그룹", size=11, color=BLUE_DK, bold=True, anchor=MSO_ANCHOR.MIDDLE)
arch = [("AA","앱 아키텍처"),("SA","시스템·NFR"),("TA","기술표준"),("DA","데이터·ERD"),("NA","네트워크·보안")]
for i,(a,d) in enumerate(arch):
org(0.55+i*2.46, 4.0, 2.32, 0.66, a, d, WHITE, BLUE, BLUE)
rect(s, 0.55, 4.86, 12.23, 0.4, fill=PURPLE_LT, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
text(s, 0.7, 4.86, 6, 0.4, "구현 그룹", size=11, color=PURPLE, bold=True, anchor=MSO_ANCHOR.MIDDLE)
impl = [("공통/시스템","common-dev"),("백엔드","backend-dev"),("프론트","frontend-dev"),
("DB","db-engineer"),("도메인","bidding·visitor\ncms·bi·admin"),("AI·시각화","ai-dev·visualizer")]
for i,(a,d) in enumerate(impl):
org(0.55+i*2.05, 5.36, 1.93, 0.72, a, d, WHITE, PURPLE, PURPLE)
# 하단: 품질·배포
org(3.4, 6.2, 3.0, 0.66, "품질 (kintex-qa)", "점진 QA·경계면·보안 불변", GREEN, WHITE)
org(6.95, 6.2, 3.0, 0.66, "배포 (devops-dev)", "CI/CD·systemd·env", RGBColor(0x2B,0x4C,0x7E), WHITE)
# ================================================================
# 7. 추진 일정 (간트)
# ================================================================
divider("05", "추진 일정", "PROJECT SCHEDULE",
["Phase A~E 마스터 일정", "완료 / 진행 / 예정 구분", "공통 레이어 선행 원칙"])
s = slide()
content_header(s, "05 · 추진 일정", "Phase A ~ E 마스터 일정 (WORK_STATUS 반영)", 5)
# 상태 범례
for i,(lb,cl) in enumerate([("완료",GREEN),("진행",AMBER),("예정",SLATE)]):
rect(s, 9.8+i*1.0, 1.42, 0.24, 0.24, fill=cl, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
text(s, 10.08+i*1.0, 1.4, 0.9, 0.28, lb, size=9.5, color=INK, anchor=MSO_ANCHOR.MIDDLE)
# 간트 좌표계
gx = 3.55 # 바 시작 x
gw = 9.15 # 바 영역 폭
units = 10 # 타임 단위
uw = gw/units
# 타임 눈금 라벨
for i in range(units+1):
text(s, gx+i*uw-0.4, 1.72, 0.8, 0.24, f"T{i}", size=7.5, color=SLATE, align=PP_ALIGN.CENTER)
rect(s, gx+i*uw, 1.98, 0.008, 4.2, fill=RGBColor(0xF0,0xF1,0xF4))
rows = [
("Phase A", "아키텍처·거버넌스 (AA/SA/TA/DA/NA)", 0, 1.5, GREEN, "완료"),
("Phase B", "공통·시스템관리 레이어 (2FA/OTP·공통기능)", 1.2, 2.0, GREEN, "완료"),
("Phase C", "P0 부스 코어 M2~M5 (배치·설계·배선·시각화)", 2.6, 2.6, AMBER, "진행"),
("Phase C", " ├ 스캐폴드·인증·매퍼 배선·룰엔진·나노바나나", 2.6, 1.6, GREEN, "완료"),
("Phase C", " └ 3안 생성·병합·실데이터 전환·모바일", 4.2, 1.4, AMBER, "진행"),
("Phase D", "P1 도메인 — 옥션 M15·관람객 M10", 4.8, 1.9, SLATE, "예정"),
("Phase D", "P1 도메인 — CMS/공개사이트 M12·M17·BI M16·관리자 M18", 5.4, 2.2, SLATE, "예정"),
("Phase D", "P1 배정·서류·매칭·정산 M1·M6·M7·M9", 5.8, 1.8, SLATE, "예정"),
("Phase E", "P2 확장 (매칭 M11·wayfinding M13·현장운영 M14)", 7.2, 1.6, SLATE, "예정"),
("Phase E", "빌드·배포 (역할별 번들·CI/CD·멀티테넌시)", 7.6, 2.2, AMBER, "진행"),
]
ry = 2.12
rh = 0.42
for i,(ph, name, start, dur, cl, st) in enumerate(rows):
y = ry + i*rh
if i % 2 == 0:
rect(s, 0.55, y-0.02, 12.23, rh-0.02, fill=RGBColor(0xFC,0xFC,0xFD))
text(s, 0.6, y, 0.85, rh-0.06, ph, size=8.5, color=BLUE_DK, bold=True, anchor=MSO_ANCHOR.MIDDLE)
text(s, 1.45, y, 2.05, rh-0.06, name, size=8.2, color=INK, anchor=MSO_ANCHOR.MIDDLE)
bx = gx + start*uw
bw = max(dur*uw, 0.3)
rect(s, bx, y+0.06, bw, rh-0.18, fill=cl, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
text(s, bx, y+0.04, bw, rh-0.14, st, size=7.5, color=WHITE, bold=True, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)
# 현재 시점 마커
nowx = gx + 4.9*uw
rect(s, nowx, 1.98, 0.02, 4.2, fill=PURPLE)
text(s, nowx-0.5, 6.2, 1.0, 0.24, "현재", size=8, color=PURPLE, bold=True, align=PP_ALIGN.CENTER)
text(s, 0.55, 6.55, 12.2, 0.3,
"원칙 Phase B(공통 레이어)는 전 도메인 선행 기반 — 인증·시스템관리·공통기능 정착 후 코어/도메인 착수. 각 모듈 완성 직후 점진 QA.",
size=9, color=MUTED)
# ================================================================
# 8. 개발 방법론·품질
# ================================================================
divider("06", "개발 방법론 · 품질관리", "METHODOLOGY & QUALITY",
["에이전트 파이프라인", "점진 QA · 보안 불변 게이트", "CI/CD 자동배포"])
s = slide()
content_header(s, "06 · 개발 방법론 · 품질관리", "에이전트 파이프라인 · 점진 QA · CI/CD", 6)
# 파이프라인
text(s, 0.55, 1.5, 8, 0.3, "① 에이전트 기반 개발 파이프라인", size=12, color=BLUE, bold=True)
pipe = [("기획","planner",PURPLE),("설계","AA/SA/TA/DA/NA",BLUE),("디자인","designer",PURPLE),
("구현","backend·frontend·db·domain",BLUE),("검증","kintex-qa",GREEN),("배포","devops-dev",BLUE_DK)]
pw2 = 1.9; g2 = 0.14
for i,(t,d,cl) in enumerate(pipe):
x = 0.55+i*(pw2+g2)
rect(s, x, 1.9, pw2, 0.74, fill=WHITE, line=cl, line_w=1.3, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
text(s, x, 1.98, pw2, 0.3, t, size=11, color=cl, bold=True, align=PP_ALIGN.CENTER)
text(s, x, 2.3, pw2, 0.3, d, size=7.6, color=MUTED, align=PP_ALIGN.CENTER)
if i < len(pipe)-1:
text(s, x+pw2-0.05, 1.9, 0.24, 0.74, "", size=16, color=SLATE, bold=True, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)
# ② 점진 QA
card(s, 0.55, 2.95, 6.05, 2.05)
text(s, 0.8, 3.12, 5.5, 0.3, "② 점진 QA — 모듈 완성 직후 검증", size=12, color=PURPLE, bold=True)
for i,t in enumerate([
"백엔드 응답 shape ↔ 프론트 훅/컴포넌트 호출 경계면 교차 비교",
"공간 로직(배치·배선·규정검증) 정합 · PostGIS 연산 검증",
"보안 불변: 자격증명·PII·스택트레이스 미노출 강제",
"AI 생성 이미지 워터마크·고지 강제 · 등록업체 응찰 게이트",
"빌드 회귀(compileJava·tsc) · 통과까지 반려",
]):
y=3.5+i*0.29
rect(s, 0.85, y+0.06, 0.09, 0.09, fill=GREEN, shape=MSO_SHAPE.OVAL)
text(s, 1.05, y, 5.4, 0.28, t, size=9.3, color=INK)
# ③ CI/CD
card(s, 6.75, 2.95, 6.03, 2.05)
text(s, 7.0, 3.12, 5.5, 0.3, "③ CI/CD 자동배포 파이프라인", size=12, color=BLUE, bold=True)
cicd = ["push","Gitea webhook","deploy_kintex.sh","백엔드 bootJar + 프론트 Vite 빌드","배포 → health check","실패 시 롤백"]
for i,c in enumerate(cicd):
y=3.5+i*0.245
rect(s, 7.05, y, 0.28, 0.2, fill=BLUE, shape=MSO_SHAPE.OVAL)
text(s, 7.06, y-0.01, 0.28, 0.22, str(i+1), size=8, color=WHITE, bold=True, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)
text(s, 7.45, y-0.02, 5.2, 0.24, c, size=9.3, color=INK, anchor=MSO_ANCHOR.MIDDLE)
# 하단 지표 스트립
metrics = [("18","도메인 모듈"),("6","역할별 포털"),("39+","Flyway 테이블(현재)"),("A~E","5개 Phase"),("3-Tier","+ AI 사이드카")]
mw=2.34
for i,(v,l) in enumerate(metrics):
x=0.55+i*(mw+0.09)
rect(s, x, 5.2, mw, 0.95, fill=BLUE if i%2==0 else PURPLE, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
text(s, x, 5.34, mw, 0.5, v, size=26, color=WHITE, bold=True, align=PP_ALIGN.CENTER)
text(s, x, 5.86, mw, 0.26, l, size=9.5, color=WHITE, align=PP_ALIGN.CENTER)
text(s, 0.55, 6.5, 12.2, 0.3,
"AI 지능 = Claude 기본 + 설정형 전환(AiTextRouter/AiConfig) · Ollama 폴백 │ 이미지 = 나노바나나(Gemini) 라이브",
size=9, color=MUTED)
# ================================================================
# 9. 보안·NFR
# ================================================================
divider("07", "보안 · 비기능요건", "SECURITY & NFR",
["외부 API 통제 (예외 명시)", "자격증명 보호 · AI 워터마크", "접근성·다국어·테마"])
s = slide()
content_header(s, "07 · 보안 · 비기능요건(NFR)", "보안 불변 원칙 및 품질 속성", 7)
# 보안 불변 (좌)
card(s, 0.55, 1.5, 6.05, 3.05)
rect(s, 0.55, 1.5, 6.05, 0.5, fill=BLUE_DK, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
text(s, 0.55, 1.5, 6.05, 0.5, "보안 불변 (Security Invariants)", size=13, color=WHITE, bold=True, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)
sec = [
("외부 API 통제","온프레미스 원칙 · 예외 = Claude(승인)·Gemini(G1). 키는 서버 env 로드, 코드·커밋·로그 미기재"),
("자격증명 보호","IP·SSH계정·비밀번호를 API 응답·에러메시지에 미노출"),
("암호화 저장","민감 자격증명 AES-256-GCM · admin 비번 env 주입(admin123 금지)"),
("AI 이미지 고지","나노바나나 생성 전 이미지 워터마크·실물 상이 고지 강제"),
("에러 응답","스택트레이스 미노출 — 요약 메시지만 전달"),
]
for i,(h,d) in enumerate(sec):
y=2.15+i*0.47
rect(s, 0.8, y+0.04, 0.11, 0.11, fill=BLUE, shape=MSO_SHAPE.OVAL)
text(s, 1.02, y, 5.45, 0.44, [[{"t":h+"","size":9.6,"color":INK,"bold":True},
{"t":d,"size":9,"color":MUTED}]], line_spacing=0.95)
# NFR (우)
card(s, 6.75, 1.5, 6.03, 3.05)
rect(s, 6.75, 1.5, 6.03, 0.5, fill=PURPLE, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
text(s, 6.75, 1.5, 6.03, 0.5, "비기능요건 (NFR)", size=13, color=WHITE, bold=True, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)
nfr = [
("웹 접근성","WCAG 준수 · 키보드·스크린리더 대응"),
("다국어(i18n)","한 · 영 · 중 · 일 (공개사이트·CMS 우선)"),
("라이트/다크 테마","전 화면 테마 토큰 · 반응형 풀스크린"),
("반응형 채널 분리","데스크톱=설계/에디터 · 모바일=현장/조회/승인"),
("성능·확장","Redis 비동기 큐 · 렌더 워커 사이드카 분리"),
("멀티테넌시 격리","tenant_id 전파 · 전시관 단위 데이터·권한 격리"),
]
for i,(h,d) in enumerate(nfr):
y=2.15+i*0.4
rect(s, 7.0, y+0.04, 0.11, 0.11, fill=PURPLE, shape=MSO_SHAPE.OVAL)
text(s, 7.22, y, 5.4, 0.38, [[{"t":h+"","size":9.6,"color":INK,"bold":True},
{"t":d,"size":9,"color":MUTED}]], line_spacing=0.95)
# 하단: 3중 권한 구조
rect(s, 0.55, 4.78, 12.23, 0.42, fill=INK, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
text(s, 0.55, 4.78, 12.23, 0.42, "3중 권한 구조 테넌트(전시관) 격리 ⊃ 행사(Event) 워크스페이스 ⊃ 행사 RBAC",
size=11.5, color=WHITE, bold=True, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)
# 인증 요약 3칩
for i,(t,d,cl) in enumerate([("JWT + RBAC","6역할 게이트",BLUE),("2FA / OTP","TOTP RFC6238",PURPLE),
("감사로그","전수 기록·개인정보 접근",BLUE_DK)]):
x=0.55+i*4.11
rect(s, x, 5.4, 3.97, 0.72, fill=WHITE, line=cl, line_w=1.3, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
text(s, x+0.2, 5.5, 3.7, 0.3, t, size=12, color=cl, bold=True)
text(s, x+0.2, 5.82, 3.7, 0.26, d, size=9.5, color=MUTED)
# ================================================================
# 10. 리스크 및 대응
# ================================================================
divider("08", "리스크 및 대응", "RISK MANAGEMENT",
["기술·데이터·연동 리스크", "영향도·발생가능성 평가", "완화 방안"])
s = slide()
content_header(s, "08 · 리스크 및 대응", "주요 리스크 및 대응 방안", 8)
# 표 헤더
hx=[0.55, 1.3, 5.9, 8.0] # 구분/리스크/영향/대응 시작
cols = [(0.55,0.75,"구분"),(1.3,4.6,"리스크"),(5.9,2.1,"영향"),(8.0,4.78,"대응 방안")]
rect(s, 0.55, 1.5, 12.23, 0.44, fill=BLUE, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
for x,w,h in cols:
text(s, x+0.12, 1.5, w-0.15, 0.44, h, size=10.5, color=WHITE, bold=True, anchor=MSO_ANCHOR.MIDDLE)
risks = [
("기술","나노바나나 생성 이미지가 실제 시공과 상이","높음",AMBER,"전 이미지 워터마크·고지 강제, 참조이미지 구조보존 파이프라인, 검수 단계"),
("데이터","홀 실측 CAD·트렌치 좌표 미확보","높음",AMBER,"확보 자산(평면도 15장+CAD) 활용, 미확보 시 공개규격 근사도면으로 Phase 진행"),
("연동","kxwp 내부 API 미공개(폐쇄형)","중간",SLATE,"초기 제출용 파일 생성+업로드 안내 릴레이, 연동은 킨텍스 협의 후"),
("보안","외부 API 호출·자격증명 노출","높음",AMBER,"Claude·Gemini만 예외 승인, env 로드·미커밋, 응답 미노출, 감사로그"),
("규정","소방·구조 최종 유권해석은 시스템 밖","중간",SLATE,"AI는 사전 필터(위반 플래깅)까지, 최종 판정은 관할기관·구조기술사"),
("일정","공통 레이어 지연 시 도메인 착수 지연","중간",SLATE,"Phase B 선행 완료(현재 완료), 모듈 병렬화·점진 QA로 흡수"),
("시장","옥션 업체 참여율·응찰 품질 불확실","중간",SLATE,"등록업체 DB 검증 게이트, AI 자료 기반 동일사양 비교로 참여 유인"),
]
ry=1.98; rh=0.56
for i,(g,r,imp,impc,resp) in enumerate(risks):
y=ry+i*rh
if i%2==0:
rect(s, 0.55, y, 12.23, rh, fill=RGBColor(0xFC,0xFC,0xFD))
rect(s, 0.55, y, 12.23, 0.01, fill=LINE)
text(s, 0.67, y, 0.7, rh, g, size=9.5, color=BLUE_DK, bold=True, anchor=MSO_ANCHOR.MIDDLE)
text(s, 1.42, y, 4.4, rh, r, size=9.5, color=INK, anchor=MSO_ANCHOR.MIDDLE)
rect(s, 5.9, y+0.16, 0.95, 0.24, fill=impc, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
text(s, 5.9, y+0.15, 0.95, 0.26, imp, size=8.5, color=WHITE, bold=True, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)
text(s, 8.0, y, 4.7, rh, resp, size=8.8, color=MUTED, anchor=MSO_ANCHOR.MIDDLE, line_spacing=0.92)
# ================================================================
# 11. 기대 효과
# ================================================================
divider("09", "기대 효과", "EXPECTED BENEFITS",
["정량 효과 (리드타임)", "정성 효과 (의사결정·투명성)", "확장 로드맵"])
s = slide()
content_header(s, "09 · 기대 효과", "정량 · 정성 기대 효과", 9)
text(s, 0.55, 1.48, 8, 0.3, "정량 효과 — 리드타임 단축", size=12.5, color=BLUE, bold=True)
quant = [
("홀 배정 견적","수일","즉시"),
("배치도 초안","수일~수주","수 분"),
("규정 검수","육안 검토","자동 플래깅"),
("유틸리티 신청","수기 작도","클릭 + 자동견적"),
("시공 결과 예측","불가(외주)","표준 샷 자동생성"),
]
qw=2.4
for i,(k,a,b) in enumerate(quant):
x=0.55+i*(qw+0.06)
card(s, x, 1.85, qw, 1.85)
text(s, x, 2.0, qw, 0.3, k, size=10, color=INK, bold=True, align=PP_ALIGN.CENTER)
rect(s, x+0.3, 2.34, qw-0.6, 0.02, fill=LINE)
text(s, x, 2.42, qw, 0.26, "As-Is", size=7.5, color=SLATE, align=PP_ALIGN.CENTER)
text(s, x, 2.62, qw, 0.3, a, size=10.5, color=MUTED, align=PP_ALIGN.CENTER)
text(s, x, 2.98, qw, 0.24, "", size=12, color=PURPLE, bold=True, align=PP_ALIGN.CENTER)
text(s, x, 3.2, qw, 0.3, b, size=11, color=BLUE, bold=True, align=PP_ALIGN.CENTER)
text(s, 0.55, 3.95, 8, 0.3, "정성 효과", size=12.5, color=PURPLE, bold=True)
qual = [
("의사결정 가속","도면을 읽지 못해도 사진 수준 시각화로 주최자·참가업체 의사결정"),
("영업 자료화","완공 예상 이미지로 참가업체 유치·컨펌 커뮤니케이션 비용 절감"),
("가격 투명화","옥션 역경매로 동일 사양 위 공정 비교 → 시공 발주 투명·최적화"),
("데이터 일원화","공간 데이터(PostGIS) 단일 원천에서 검증·시각화·정산·BI·wayfinding"),
]
for i,(h,d) in enumerate(qual):
x=0.55+(i%2)*6.15
y=4.32+(i//2)*0.66
card(s, x, y, 6.05, 0.58)
rect(s, x, y, 0.08, 0.58, fill=PURPLE, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
text(s, x+0.24, y+0.08, 5.7, 0.28, h, size=10.5, color=INK, bold=True)
text(s, x+0.24, y+0.34, 5.7, 0.22, d, size=8.6, color=MUTED)
# 확장 로드맵
rect(s, 0.55, 5.78, 12.23, 0.44, fill=BLUE, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
text(s, 0.7, 5.78, 4, 0.44, "확장 로드맵", size=11, color=WHITE, bold=True, anchor=MSO_ANCHOR.MIDDLE)
road = ["KINTEX 기준 운영","코엑스 등 다중 전시관 온보딩","전시테크 SaaS 사업화","kxwp·PG·사이니지 정식 연동"]
for i,r in enumerate(road):
text(s, 2.6+i*2.55, 5.78, 2.5, 0.44, " "+r, size=9.3, color=WHITE, anchor=MSO_ANCHOR.MIDDLE)
text(s, 0.55, 6.4, 12.2, 0.5,
"종합 “신청서를 내는 순간 시공 후 사진을 먼저 본다” — 설계·시각화·발주·운영·분석 폐루프를 하나의 데이터·계정 체계로 자동화하여 킨텍스를 전시 운영 표준 플랫폼으로 전환",
size=10, color=INK, bold=True, line_spacing=1.0)
# ================================================================
# 저장
# ================================================================
OUT = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "개발계획서.pptx")
prs.save(OUT)
print("SAVED:", OUT)
print("SLIDES:", len(prs.slides._sldIdLst))