kintex/docs/deliverables/프로그램사양서/_gen/gen_program_spec_pptx.py
zio 6c9891c7e2 feat: mobile harness + design v2.1 (84 screens) + PLANNING v3.1 + deliverables + Stitch screens
- Harness: kintex-mobile-dev agent + kintex-mobile-orchestrator skill (WISE mobile ref, Stitch-first design rule, dual app targets B2B/B2C)
- design.md v2.1: full 84-screen inventory (web 51 / admin 10 / public 8 / mobile 15) with Stitch prompts incl. ticketing (SCR-P7/P8, M14/M15)
- PLANNING v3.1: unified account + split signup tracks (2FA required for staff, light signup/guest for visitors), one codebase / two app targets
- Deliverables: dev plan (21s), user/operator/developer guides (17/14/15s), program spec (44s, 65 programs, 8 flowcharts), DA (DB design 14s + table spec xlsx 35 tables/299 cols)
- Benchmark: ticketing-app-benchmark.md (7 apps) -> IMPLEMENTATION_BACKLOG Phase F (14 items)
- Stitch: 23 generated screens saved (mobile 10, admin 6, web core 5, ticket 2)
- mobile/: Expo scaffold (SDK 51, expo-router, secure store JWT)
- frontend: SCR-13~17 QA fixes, icons.tsx, kintexEvents, V10 seed migration
- ci/: KINTEX CI logo assets

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 23:31:45 +09:00

1116 lines
71 KiB
Python
Raw 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 M1~M18 + §5B 공통레이어), docs/design.md(§2.8 화면 80종 SCR),
docs/architecture/app.md(응답 봉투·에러·감사로그), IMPLEMENTATION_BACKLOG.md
- 디자인: 개발계획서(gen_dev_plan_pptx.py) 스타일 재사용 — KINTEX Blue #0066B3 / AI Purple #6D4AFF / 맑은 고딕 / 16:9
- 순서도는 python-pptx 도형(단자·처리·판단·데이터·커넥터)으로 직접 작도(외부 이미지 금지)
- 보안 불변: 비밀번호/API키/서버IP/SSH 계정 미기재
재실행: python gen_program_spec_pptx.py
"""
import os
from pptx import Presentation
from pptx.util import Pt
from pptx.dml.color import RGBColor
from pptx.enum.text import PP_ALIGN, MSO_ANCHOR
from pptx.enum.shapes import MSO_SHAPE, MSO_CONNECTOR
from pptx.oxml.ns import qn
# ---------------- 팔레트 (개발계획서와 동일) ----------------
BLUE = RGBColor(0x00, 0x66, 0xB3)
BLUE_DK = RGBColor(0x00, 0x44, 0x7A)
BLUE_LT = RGBColor(0xE1, 0xEF, 0xF9)
PURPLE = RGBColor(0x6D, 0x4A, 0xFF)
PURPLE_LT = RGBColor(0xEC, 0xE8, 0xFF)
INK = RGBColor(0x10, 0x18, 0x28)
MUTED = RGBColor(0x66, 0x70, 0x85)
LINE = RGBColor(0xE4, 0xE7, 0xEC)
BG = RGBColor(0xF9, 0xFA, 0xFB)
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)
ROW_ALT = RGBColor(0xFC, 0xFC, 0xFD)
FONT = "맑은 고딕"
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
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)
# 텍스트 없는 도형은 자동맞춤 방지
sp.text_frame.word_wrap = True
return sp
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):
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)
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)
FOOT = "킨텍스 자동전시시스템 구축 · 프로그램사양서"
def content_header(s, kicker, title, idx, chip_txt=None, chip_col=BLUE):
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=chip_col, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
text(s, 0.55, 0.5, 0.62, 0.62, (chip_txt if chip_txt else f"{idx:02d}"),
size=(20 if not chip_txt else 22), 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=21, color=INK, bold=True)
rect(s, 0.55, 1.28, 12.23, 0.02, fill=LINE)
text(s, 0.55, 7.06, 8, 0.3, FOOT, 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 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
# ---------------- 순서도(플로우차트) 도형 헬퍼 ----------------
def fnode(s, cx, cy, w, h, txt, kind="proc"):
"""중심좌표 기준 순서도 노드. return dict(cx,cy,l,r,t,b)."""
x = cx - w/2; y = cy - h/2
if kind == "term": # 시작/끝 단자
sh, fill, line, tcol = MSO_SHAPE.FLOWCHART_TERMINATOR, BLUE, BLUE, WHITE
elif kind == "proc": # 처리
sh, fill, line, tcol = MSO_SHAPE.FLOWCHART_PROCESS, WHITE, BLUE, INK
elif kind == "dec": # 판단
sh, fill, line, tcol = MSO_SHAPE.FLOWCHART_DECISION, PURPLE_LT, PURPLE, PURPLE
elif kind == "io": # 입출력(데이터)
sh, fill, line, tcol = MSO_SHAPE.FLOWCHART_DATA, BLUE_LT, BLUE, BLUE_DK
elif kind == "db": # 데이터베이스
sh, fill, line, tcol = MSO_SHAPE.FLOWCHART_MAGNETIC_DISK, HALL_BG, SLATE, INK
elif kind == "wkr": # 사전정의 처리(워커/외부)
sh, fill, line, tcol = MSO_SHAPE.FLOWCHART_PREDEFINED_PROCESS, PURPLE_LT, PURPLE, PURPLE
elif kind == "proc_ok": # 성공 처리(강조)
sh, fill, line, tcol = MSO_SHAPE.FLOWCHART_PROCESS, RGBColor(0xEC,0xF7,0xF0), GREEN, RGBColor(0x0C,0x6B,0x43)
elif kind == "proc_bad": # 반려/차단 처리
sh, fill, line, tcol = MSO_SHAPE.FLOWCHART_PROCESS, RGBColor(0xFD,0xF1,0xE3), AMBER, RGBColor(0x8A,0x54,0x00)
else:
sh, fill, line, tcol = MSO_SHAPE.FLOWCHART_PROCESS, WHITE, BLUE, INK
sp = rect(s, x, y, w, h, fill=fill, line=line, line_w=1.3, shape=sh)
sz = 8.4 if kind != "dec" else 8.0
text(s, x, y, w, h, txt, size=sz, color=tcol, bold=(kind in ("term","dec")),
align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE, line_spacing=0.95)
return {"cx":cx,"cy":cy,"l":x,"r":x+w,"t":y,"b":y+h}
def _arrowline(cxn, color, w):
cxn.line.color.rgb = color; cxn.line.width = Pt(w)
ln = cxn.line._get_or_add_ln()
tail = ln.makeelement(qn('a:tailEnd'), {'type':'triangle','w':'med','len':'med'})
ln.append(tail)
def arrow(s, x1, y1, x2, y2, color=SLATE, w=1.6, label=None, lcol=MUTED):
cxn = s.shapes.add_connector(MSO_CONNECTOR.STRAIGHT, inch(x1), inch(y1), inch(x2), inch(y2))
_arrowline(cxn, color, w)
if label:
mx, my = (x1+x2)/2, (y1+y2)/2
text(s, mx-0.35, my-0.16, 0.9, 0.24, label, size=8, color=lcol, bold=True,
align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)
def arrow_elbow(s, x1, y1, x2, y2, first='v', color=SLATE, w=1.6, label=None, lcol=MUTED):
"""L자 커넥터. first='v'면 수직→수평, 'h'면 수평→수직. 마지막 구간에 화살촉."""
if first == 'v':
seg1 = s.shapes.add_connector(MSO_CONNECTOR.STRAIGHT, inch(x1), inch(y1), inch(x1), inch(y2))
seg1.line.color.rgb = color; seg1.line.width = Pt(w)
cxn = s.shapes.add_connector(MSO_CONNECTOR.STRAIGHT, inch(x1), inch(y2), inch(x2), inch(y2))
else:
seg1 = s.shapes.add_connector(MSO_CONNECTOR.STRAIGHT, inch(x1), inch(y1), inch(x2), inch(y1))
seg1.line.color.rgb = color; seg1.line.width = Pt(w)
cxn = s.shapes.add_connector(MSO_CONNECTOR.STRAIGHT, inch(x2), inch(y1), inch(x2), inch(y2))
_arrowline(cxn, color, w)
if label:
text(s, x1-0.05, (y1+y2)/2-0.12, 1.1, 0.24, label, size=8, color=lcol, bold=True,
anchor=MSO_ANCHOR.MIDDLE)
def vchain(s, cx, y0, items, w=3.7, h=0.6, gap=0.34):
"""세로 스택 + 순차 연결. items=[(txt,kind),...] return nodes."""
nodes = []; y = y0
for txt, kind in items:
n = fnode(s, cx, y + h/2, w, h, txt, kind); nodes.append(n); y += h + gap
for a, b in zip(nodes, nodes[1:]):
arrow(s, a["cx"], a["b"], b["cx"], b["t"])
return nodes
def fc_legend(s):
items = [("시작/끝",BLUE,"term"),("처리",WHITE,"proc"),("판단",PURPLE_LT,"dec"),("입출력",BLUE_LT,"io")]
x0 = 8.0
for i,(lb,cl,kd) in enumerate(items):
x = x0 + i*1.2
sh = {"term":MSO_SHAPE.FLOWCHART_TERMINATOR,"proc":MSO_SHAPE.FLOWCHART_PROCESS,
"dec":MSO_SHAPE.FLOWCHART_DECISION,"io":MSO_SHAPE.FLOWCHART_DATA}[kd]
ln = {"term":BLUE,"proc":BLUE,"dec":PURPLE,"io":BLUE}[kd]
rect(s, x, 1.34, 0.26, 0.2, fill=cl, line=ln, line_w=1.0, shape=sh)
text(s, x+0.31, 1.31, 0.85, 0.26, lb, size=8, color=MUTED, anchor=MSO_ANCHOR.MIDDLE)
# ================================================================
# 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.95, 0.42, "프로그램사양서", PURPLE, WHITE, size=12)
chip(s, 2.92, 2.35, 2.35, 0.42, "Program Specification", 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 — 기능별 상세 사양 + 업무 순서도",
size=16, color=RGBColor(0xC7,0xDD,0xF2))
text(s, 0.88, 5.35, 11.4, 0.4,
"프로그램 식별체계 PGM-ID = M{모듈}-{유형}-{일련} (예: M02-BATCH-01)",
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, 9, 0.4, [
[{"t":"대상 ","size":11,"color":RGBColor(0x90,0xA6,0xC9)},
{"t":"모듈 M1~M18 + 공통·시스템관리 레이어(§5B)","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 · 나노바나나 워커","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 · dev team", size=9.5, color=RGBColor(0x90,0xA6,0xC9), align=PP_ALIGN.RIGHT)
# ================================================================
# 2. 목차
# ================================================================
s = slide()
content_header(s, "CONTENTS", "목차", 0, chip_txt="C", chip_col=PURPLE)
toc = [
("01","개요 · 식별체계","문서 목적 / PGM-ID 부여 체계 / 프로그램 유형"),
("02","프로그램 목록 총괄","모듈(M1~M18+공통)별 프로그램 ID·명·유형·화면"),
("03","핵심 프로그램 상세 사양","20개 대표 프로그램 — 입출력·처리로직·테이블·예외"),
("04","핵심 업무 순서도","8종 — 배치/검증/배선/렌더/옥션/등록/승인/배포"),
("05","공통 규약","API 응답 봉투 · 에러 처리 · 감사 로그"),
]
for i,(no,t,d) in enumerate(toc):
y = 1.75 + i*1.0
rect(s, 0.7, y, 11.9, 0.86, fill=CARD, line=LINE, line_w=1.0, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
rect(s, 0.7, y, 0.09, 0.86, fill=(BLUE if i%2==0 else PURPLE), shape=MSO_SHAPE.ROUNDED_RECTANGLE)
text(s, 1.0, y, 0.95, 0.86, no, size=26, color=(BLUE if i%2==0 else PURPLE), bold=True, anchor=MSO_ANCHOR.MIDDLE)
text(s, 2.1, y+0.15, 9.8, 0.34, t, size=15, color=INK, bold=True)
text(s, 2.1, y+0.52, 9.8, 0.3, d, size=10, color=MUTED)
# ================================================================
# 3. 개요 · 식별체계
# ================================================================
divider("01", "개요 · 식별체계", "OVERVIEW & ID SCHEME",
["문서 목적 · 범위", "PGM-ID 부여 체계", "프로그램 유형 정의"])
s = slide()
content_header(s, "01 · 개요 · 식별체계", "프로그램 식별 체계 및 유형 정의", 1)
# 목적
card(s, 0.55, 1.5, 12.23, 1.02)
text(s, 0.8, 1.66, 11.7, 0.3, "문서 목적", size=12, color=BLUE, bold=True)
text(s, 0.8, 1.98, 11.75, 0.5,
"본 사양서는 킨텍스 자동전시시스템(M1~M18 + 공통·시스템관리 §5B)의 프로그램 단위를 식별·정의하고, 핵심 프로그램의 상세 처리 사양과 주요 업무 순서도를 규정한다. 화면(SCR)·모듈 정의의 권위 출처는 PLANNING v3.0·design.md v2.0이며, 본 문서는 이를 프로그램 단위로 분해한다.",
size=10, color=INK, line_spacing=1.02)
# ID 체계
text(s, 0.55, 2.78, 8, 0.3, "PGM-ID 부여 체계", size=12.5, color=PURPLE, bold=True)
seg = [("M02","모듈 코드 (M1~M18 / SYS·CMN=공통)",BLUE),
("BATCH","유형·업무 약어",PURPLE),
("01","모듈 내 일련번호",BLUE_DK)]
x = 0.55
for i,(v,d,cl) in enumerate(seg):
w = 2.0 if i!=1 else 3.5
rect(s, x, 3.14, w, 0.72, fill=WHITE, line=cl, line_w=1.4, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
text(s, x, 3.22, w, 0.34, v, size=15, color=cl, bold=True, align=PP_ALIGN.CENTER)
text(s, x, 3.56, w, 0.26, d, size=8, color=MUTED, align=PP_ALIGN.CENTER)
if i<2:
text(s, x+w-0.02, 3.14, 0.34, 0.72, "-", size=20, color=SLATE, bold=True, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)
x += w + 0.34
text(s, 8.5, 3.28, 4.3, 0.5, [[{"t":"예) ","size":11,"color":MUTED},
{"t":"M02-BATCH-01","size":13,"color":INK,"bold":True}],
[{"t":"= 플로어플랜 배치 3안 생성 엔진","size":9,"color":MUTED}]], line_spacing=1.1)
# 유형 정의
text(s, 0.55, 4.1, 8, 0.3, "프로그램 유형", size=12.5, color=BLUE, bold=True)
types = [
("온라인","ONL","사용자 화면 트랜잭션(React↔REST), 조회·입력·처리",BLUE),
("API","API","엔진·서비스 API(룰·배치·배선·집계), 화면 비종속",BLUE_DK),
("배치","BAT","스케줄러/야간 적재·역산 알림·주기 수집",PURPLE),
("워커","WKR","Redis 큐 소비 비동기 워커(나노바나나 렌더 등)",PURPLE),
]
tw = 2.98
for i,(kr,ab,d,cl) in enumerate(types):
x = 0.55 + i*(tw+0.11)
card(s, x, 4.46, tw, 1.72)
rect(s, x, 4.46, tw, 0.5, fill=cl, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
text(s, x, 4.46, tw, 0.5, f"{kr} ({ab})", size=12, color=WHITE, bold=True, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)
text(s, x+0.2, 5.1, tw-0.4, 1.0, d, size=9.5, color=INK, line_spacing=1.05)
text(s, 0.55, 6.4, 12.2, 0.3,
"표기 원칙 온라인 프로그램은 관련 화면(SCR ID)을 명시. 하나의 화면이 복수 프로그램을, 하나의 엔진 API가 복수 화면을 지원할 수 있다(N:M).",
size=9, color=MUTED)
# ================================================================
# 4. 프로그램 목록 총괄
# ================================================================
divider("02", "프로그램 목록 총괄", "PROGRAM CATALOG",
["모듈별 PGM-ID·명·유형", "관련 화면(SCR) 매핑", "약 60개 프로그램"])
# 총괄 통계
s = slide()
content_header(s, "02 · 프로그램 목록 총괄", "프로그램 총괄 개요", 2)
stats = [("18","도메인 모듈 (M1~M18)"),("60+","프로그램 (온라인·API·배치·워커)"),
("80","화면 (SCR·웹/모바일/공개/관리자)"),("4","프로그램 유형")]
mw = 2.94
for i,(v,l) in enumerate(stats):
x = 0.55 + i*(mw+0.1)
rect(s, x, 1.55, mw, 1.15, fill=(BLUE if i%2==0 else PURPLE), shape=MSO_SHAPE.ROUNDED_RECTANGLE)
text(s, x, 1.72, mw, 0.6, v, size=34, color=WHITE, bold=True, align=PP_ALIGN.CENTER)
text(s, x, 2.36, mw, 0.28, l, size=9, color=WHITE, align=PP_ALIGN.CENTER)
# 모듈군 구성 요약
groups = [
("공통 · 시스템관리 (§5B / SYS·CMN)", BLUE_DK,
"인증(JWT+OTP)·사용자·RBAC·공통코드·메뉴·감사로그·설정 + 업무일지·일정·쪽지·공지·검색·회의록·보고·알림"),
("설계·시각화 코어 (M2~M5 · P0)", PURPLE,
"배치 3안 생성·규정검증·부스설계·배선 최단경로·나노바나나 RenderJob 워커"),
("판매·발주·정산 (M1·M6~M9·M15)", BLUE,
"자동 견적·서류/마일스톤·등록업체 매칭·물류 슬롯·정산결제·공사 옥션(역경매·낙찰)"),
("관람·참가·마케팅 (M10~M14)", BLUE,
"관람객 등록·배지·체크인·리드캡처·비즈매칭·EDM/공개사이트·wayfinding·현장운영"),
("경영·콘텐츠·관리 (M16~M18)", PURPLE,
"BI 데이터마트/대시보드·CMS/마이크로사이트·마스터데이터·룰셋·테넌트 관리"),
]
y = 2.92
for i,(t,cl,d) in enumerate(groups):
rect(s, 0.55, y, 12.23, 0.72, fill=CARD, line=LINE, line_w=1.0, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
rect(s, 0.55, y, 0.1, 0.72, fill=cl, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
text(s, 0.82, y+0.1, 5.0, 0.5, t, size=11, color=cl, bold=True, anchor=MSO_ANCHOR.MIDDLE)
text(s, 5.9, y+0.1, 6.75, 0.52, d, size=9, color=MUTED, anchor=MSO_ANCHOR.MIDDLE, line_spacing=0.95)
y += 0.8
# --- 프로그램 목록 표 헬퍼 ---
LIST_COLS = [(0.6,1.55,"PGM-ID"),(2.2,3.35,"프로그램명"),(5.62,1.05,"유형"),
(6.75,1.45,"화면(SCR)"),(8.25,4.5,"개요")]
def list_slide(title, rows, note=None):
s = slide()
content_header(s, "02 · 프로그램 목록 총괄", title, 2)
# 헤더
rect(s, 0.55, 1.5, 12.23, 0.42, fill=BLUE, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
for x,w,h in LIST_COLS:
text(s, x+0.1, 1.5, w-0.12, 0.42, h, size=9.5, color=WHITE, bold=True, anchor=MSO_ANCHOR.MIDDLE)
y = 1.96
rh = 0.445 if len(rows) > 10 else 0.49
for i,(pid,name,typ,scr,desc) in enumerate(rows):
if i % 2 == 0:
rect(s, 0.55, y, 12.23, rh, fill=ROW_ALT)
rect(s, 0.55, y+rh, 12.23, 0.008, fill=LINE)
text(s, 0.7, y, 1.5, rh, pid, size=8.6, color=BLUE_DK, bold=True, anchor=MSO_ANCHOR.MIDDLE)
text(s, 2.3, y, 3.25, rh, name, size=8.8, color=INK, anchor=MSO_ANCHOR.MIDDLE, line_spacing=0.9)
tcol = {"온라인":BLUE,"API":BLUE_DK,"배치":PURPLE,"워커":PURPLE,"온라인/WS":BLUE}.get(typ,MUTED)
text(s, 5.62, y, 1.05, rh, typ, size=8.2, color=tcol, bold=True, anchor=MSO_ANCHOR.MIDDLE)
text(s, 6.85, y, 1.35, rh, scr, size=8.0, color=MUTED, anchor=MSO_ANCHOR.MIDDLE)
text(s, 8.35, y, 4.35, rh, desc, size=8.0, color=MUTED, anchor=MSO_ANCHOR.MIDDLE, line_spacing=0.9)
y += rh
if note:
text(s, 0.55, 6.72, 12.2, 0.3, note, size=8.2, color=MUTED)
return s
# 목록 1 — 공통 · 시스템관리 (SYS/CMN)
list_slide("프로그램 목록 (1/6) — 공통 · 시스템관리 (§5B)", [
("SYS-AUTH-01","로그인·JWT 발급","온라인","SCR-01","ID/PW 인증→JWT 발급, 행사 워크스페이스 선택"),
("SYS-AUTH-02","2차 인증 OTP(TOTP)","API","SCR-51","TOTP RFC6238 검증·QR 등록·재설정"),
("SYS-AUTH-03","로그인 실패 잠금·해제","API","SCR-01","실패 횟수 잠금 + 관리자 해제"),
("SYS-USER-01","사용자 관리","온라인","SCR-A1","6역할+등록업체·관람객 계정 CRUD·상태"),
("SYS-RBAC-01","역할·권한(RBAC) 관리","온라인","SCR-A2","플랫폼·행사 이중 평가 권한 매핑"),
("SYS-CODE-01","공통코드 관리","온라인","SCR-A3","홀·부스유형·공종14분류·요금코드"),
("SYS-MENU-01","메뉴 관리","온라인","SCR-A4","역할별 포털 IA 메뉴 트리·권한"),
("SYS-AUDIT-01","감사로그 조회","온라인","SCR-A5","승인·낙찰·룰셋개정·리드접근 전수 조회"),
("SYS-CONF-01","시스템설정","온라인","SCR-A6","요율/규정 룰셋 버전·마감정책·AI 게이트"),
], note="§5B = UIWS/WISE 표준 이식. 시스템관리는 M18 백오피스와 통합(중복 재구현 없음).")
list_slide("프로그램 목록 (2/6) — 공통 업무 기능 (§5B)", [
("CMN-WORK-01","업무일지(worklog)","온라인","SCR-39","홀매니저 현장일지·업체 시공일지·댓글"),
("CMN-SCHD-01","통합 일정·캘린더","온라인","SCR-40","마일스톤·옥션마감·반입슬롯 통합 뷰"),
("CMN-MSG-01","쪽지(message)","온라인","SCR-41","행사 내 주최자↔참가↔업체↔홀매니저"),
("CMN-NOTI-01","공지(notice)","온라인","SCR-42","킨텍스/행사 공지·팝업 (M17 CMS와 정합)"),
("CMN-SRCH-01","통합검색(search)","API","SCR-44","행사·부스·업체·문서·콘텐츠 크로스 검색"),
("CMN-MEET-01","회의록(STT→PDF)","배치","SCR-45","사전협의(D-30) 녹음→STT→Jasper PDF"),
("CMN-RPT-01","업무보고·통계","배치","SCR-46","일/주/월/분기/연 보고·통계 Jasper PDF"),
("CMN-ALRM-01","알림센터(notification)","온라인/WS","SCR-47","D-데이·낙찰·승인·결제 알림 WebSocket"),
("CMN-PREF-01","마이페이지·개인화","온라인","SCR-48","즐겨찾기·최근방문·환경설정"),
])
# 목록 3 — M1~M5 코어
list_slide("프로그램 목록 (3/6) — M1 배정견적 · M2~M5 코어 (P0)", [
("M01-QUOTE-01","자동 견적 계산 엔진","API","SCR-18","2,250원/㎡×면적×일수×성수기·전시장 계수"),
("M01-ONL-01","홀 배정 신청","온라인","SCR-18","가용성 캘린더→홀/반홀 선택→신청서 생성"),
("M01-ONL-02","부스 판매 인벤토리","온라인","SCR-19","부스 재고·상태·셀프선택 판매 관리"),
("M02-BATCH-01","부스 배치 3안 생성 엔진","배치","SCR-03","제약 솔버+휴리스틱, 목표 다양화 3안"),
("M02-RULE-01","배치 규정 자동검증","API","SCR-03","통로·비상구·바닥하중·복층 위반 플래깅"),
("M02-ONL-01","부스 배치 에디터","온라인","SCR-03","PostGIS 폴리곤 캔버스 편집·확정"),
("M02-ONL-02","배치안 비교·병합","온라인","SCR-04","레이어 토글·병합→규정 재검증(S7 조감)"),
("M03-DSN-01","부스 설계 3안 생성","API","SCR-06","레이아웃·파라메트릭 구조 3안(용도 다양화)"),
("M03-ONL-01","부스 설계 스튜디오","온라인","SCR-06","조립/독립 설계·3D 프리뷰·선택/병합"),
("M03-RULE-01","설계 규정 사전검증","API","SCR-09","높이5m·리깅·복층·방염·금지작업 플래깅"),
("M04-WIRE-01","배선 최단경로 산출 엔진","API","SCR-07","트렌치→분전반 통로횡단 최소 배선(PostGIS)"),
("M04-ONL-01","유틸리티 배선 뷰","온라인","SCR-07","전기/네트워크/급배수 배선·조명 배치"),
("M04-ONL-02","신청 요약·위치표시도","온라인","SCR-08","단말 클릭→위치표시도 자동생성·견적"),
("M05-API-01","RenderJob 큐 발행","API","SCR-12","씬 컴파일→스키마해시→Redis 큐 발행"),
("M05-WKR-01","나노바나나 렌더 워커","워커","SCR-12","Gemini image-to-image·방어·워터마크"),
("M05-ONL-01","시각화 갤러리","온라인","SCR-12","표준 샷 S1~S7·Before/After 슬라이더"),
], note="M2~M5 = 핵심 차별화 P0. 배치·배선은 결정적 알고리즘(솔버) 중심, LLM은 조건해석 보조.")
# 목록 4 — M6~M9, M15
list_slide("프로그램 목록 (4/6) — M6~M9 판매운영 · M15 옥션", [
("M06-ONL-01","서류·마일스톤 워크플로","온라인","SCR-22","D-150/30/25/7 마일스톤·서류 상태"),
("M06-DOC-01","신고서류 웹폼→HWP/PDF","API","SCR-23","7종 서식 자동생성·LLM 누락/불일치 검수"),
("M06-BAT-01","마일스톤 D-데이 알림","배치","SCR-40","역산 리마인더 스케줄 발행→알림센터"),
("M07-ONL-01","등록업체 매칭·RFQ","온라인","SCR-24","규모·업종·예산·지역 추천→RFQ 발송"),
("M07-BAT-01","등록업체 DB 주기 수집","배치","SCR-A7","14분류×739개 공개데이터 수집·정규화"),
("M08-ONL-01","반입/반출 슬롯 예약","온라인","SCR-25","하역장 슬롯·중량물 우선·통행증 QR"),
("M09-ONL-01","정산·결제 대시보드","온라인","SCR-21","납부 스케줄·PG 결제·예치금 정산 내역"),
("M09-BAT-01","납부 스케줄 자동 생성","배치","SCR-21","계약금20/중도금30+20/잔금30+예치금"),
("M15-ONL-01","옥션 목록·개설","온라인","SCR-26","유형·라운드·마감·낙찰기준·가중치 설정"),
("M15-ONL-02","옥션 상세·실시간 순위","온라인/WS","SCR-27","자료열람·응찰·실시간 순위 WebSocket"),
("M15-DOC-01","견적서 작성·PDF 산출","API","SCR-28","라인아이템·총액·부가세·버전·PDF"),
("M15-AWD-01","낙찰 비교·선정(Award)","API","SCR-29","최저가/종합점수(가격+평판+납기) 비교"),
], note="M15 = 폐루프 연결고리. M2~M5 산출물(물량서·시공 예상 이미지)이 응찰 근거 자료.")
# 목록 5 — M10~M14 관람·현장
list_slide("프로그램 목록 (5/6) — M10~M14 관람 · 참가 · 현장", [
("M10-ONL-01","관람객 등록 관리","온라인","SCR-30","유형별 사전등록 폼·현황 대시보드"),
("M10-BADGE-01","배지/QR 발급","API","SCR-P4","모바일 배지/QR 발급·배지 템플릿(M17)"),
("M10-ONL-02","현장 체크인","온라인","SCR-31","QR 체크인·즉석 배지·오프라인 폴백"),
("M10-LEAD-01","리드캡처·AI 스코어링","API","SCR-32","배지 QR 스캔→연락처·관심도·팔로업"),
("M11-MATCH-01","AI 미팅 추천","API","SCR-M8","프로필·intent 매칭→미팅 슬롯 예약"),
("M12-EDM-01","EDM·캠페인 관리","배치","SCR-33","세그먼트 캠페인·리마인더·수신동의"),
("M12-PUB-01","공개 홍보 사이트","온라인","SCR-P1","행사소개·일정·교통, SEO·다국어(한영중일)"),
("M12-ONL-01","스폰서십 패키지 판매","온라인","SCR-34","스폰서 등급·재고·판매 관리"),
("M13-NAV-01","실내 내비 경로 산출","API","SCR-M6","블루닷/존레벨 경로(부스·시설·비상구)"),
("M14-ONL-01","홀 현장운영 대시보드","온라인","SCR-14","입장·혼잡·전력부하·주차·안전 플래그"),
])
# 목록 6 — M16~M18 경영·콘텐츠·관리
list_slide("프로그램 목록 (6/6) — M16 BI · M17 CMS · M18 관리", [
("M16-ETL-01","BI 데이터마트 적재","배치","SCR-13","스타스키마 Fact/Dim 야간 적재(KpiSnapshot)"),
("M16-ONL-01","경영분석 대시보드","온라인","SCR-13","가동률·매출구성·P&L·리텐션·수요예측"),
("M17-ONL-01","CMS 콘텐츠·게시 워크플로","온라인","SCR-35","초안→검수→게시·버전·다국어"),
("M17-ONL-02","마이크로사이트 편집기","온라인","SCR-36","참가업체 부스소개·제품·예상샷 게시"),
("M17-ONL-03","다국어 콘텐츠 관리","온라인","SCR-37","한/영/중/일 콘텐츠·번역 검수"),
("M18-ONL-01","마스터데이터 관리","온라인","SCR-A7","홀·요율·유틸요금·부스표준·등록업체"),
("M18-ONL-02","규정 룰셋 버전 관리","온라인","SCR-A8","연 단위 요율·규정 개정 버전·이력"),
("M18-ONL-03","테넌트 관리·온보딩","온라인","SCR-A9","전시관(테넌트) 마스터·온보딩(플랫폼관)"),
("M18-ONL-04","AI 플랫폼 설정(AiConfig)","온라인","SCR-A10","Claude 기본·설정형 전환·폴백"),
], note="M18 = §5B-1 시스템관리 + 킨텍스 마스터데이터 확장. 테넌트/AiConfig는 v3.0·F100 반영.")
# ================================================================
# 5. 핵심 프로그램 상세 사양
# ================================================================
divider("03", "핵심 프로그램 상세 사양", "PROGRAM DETAIL SPEC",
["모듈 대표 20개 프로그램", "입력·출력·처리로직", "관련 테이블·예외 처리"])
def detail_slide(idx_total, d):
s = slide()
page_bg(s, BG)
rect(s, 0, 0, 13.333, 0.12, fill=BLUE)
cl = d.get("color", BLUE)
# PGM-ID chip
rect(s, 0.55, 0.42, 2.35, 0.62, fill=cl, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
text(s, 0.55, 0.42, 2.35, 0.62, d["id"], size=15, color=WHITE, bold=True,
align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)
text(s, 3.02, 0.4, 8.5, 0.34, d["name"], size=18, color=INK, bold=True)
text(s, 3.02, 0.78, 9.7, 0.26, d["module"], size=9.5, color=MUTED)
# 메타 칩 (유형·화면)
chip(s, 10.6, 0.42, 1.05, 0.3, d["type"], cl, WHITE, size=9)
chip(s, 11.72, 0.42, 1.06, 0.3, d["scr"], RGBColor(0x2B,0x4C,0x7E), WHITE, size=9)
text(s, 10.6, 0.78, 2.2, 0.24, f"상세사양 {idx_total}/20", size=8, color=MUTED, align=PP_ALIGN.RIGHT)
rect(s, 0.55, 1.18, 12.23, 0.02, fill=LINE)
text(s, 0.55, 7.06, 8, 0.3, FOOT, size=8.5, color=MUTED)
text(s, 11.3, 7.06, 1.48, 0.3, d["id"], size=8.5, color=MUTED, align=PP_ALIGN.RIGHT)
def block(x, y, w, h, title, tcol, items, numbered=False):
card(s, x, y, w, h)
rect(s, x, y+0.12, 0.09, h-0.24, fill=tcol, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
text(s, x+0.26, y+0.13, w-0.4, 0.3, title, size=10.5, color=tcol, bold=True)
yy = y + 0.5
for i,it in enumerate(items):
if numbered:
rect(s, x+0.26, yy+0.015, 0.26, 0.22, fill=tcol, shape=MSO_SHAPE.OVAL)
text(s, x+0.26, yy, 0.26, 0.22, str(i+1), size=8, color=WHITE, bold=True,
align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)
text(s, x+0.62, yy-0.02, w-0.78, 0.44, it, size=8.6, color=INK, line_spacing=0.92)
else:
rect(s, x+0.28, yy+0.07, 0.08, 0.08, fill=tcol, shape=MSO_SHAPE.OVAL)
text(s, x+0.5, yy-0.01, w-0.66, 0.44, it, size=8.7, color=INK, line_spacing=0.92)
yy += d.get("lh", 0.315)
return
# 좌: 기능설명 / 입력 / 출력
block(0.55, 1.32, 6.0, 1.42, "기능 설명", cl, d["desc"])
block(0.55, 2.86, 2.9, 3.05, "입력 (Input)", BLUE_DK, d["input"])
block(3.65, 2.86, 2.9, 3.05, "출력 (Output)", BLUE_DK, d["output"])
# 우: 처리 로직 / 테이블 / 예외
block(6.72, 1.32, 6.06, 3.05, "처리 로직", PURPLE, d["logic"], numbered=True)
block(6.72, 4.49, 6.06, 1.42, "예외 처리", AMBER, d["exc"])
# 하단: 관련 테이블 스트립
rect(s, 0.55, 6.05, 6.0, 0.86, fill=HALL_BG, line=LINE, line_w=1.0, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
text(s, 0.75, 6.14, 5.7, 0.26, "관련 테이블 / 데이터", size=9.5, color=BLUE_DK, bold=True)
text(s, 0.75, 6.42, 5.7, 0.44, " · ".join(d["tables"]), size=8.6, color=INK, line_spacing=0.95)
# 하단 우: 연동
rect(s, 6.72, 6.05, 6.06, 0.86, fill=BLUE_LT, line=BLUE, line_w=1.0, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
text(s, 6.92, 6.14, 5.8, 0.26, "연동 / 비고", size=9.5, color=BLUE_DK, bold=True)
text(s, 6.92, 6.42, 5.8, 0.44, d["link"], size=8.6, color=INK, line_spacing=0.95)
DETAILS = [
{"id":"M01-QUOTE-01","name":"자동 견적 계산 엔진","type":"API","scr":"SCR-18","color":BLUE,
"module":"M1 행사·홀 배정 및 자동 견적","lh":0.30,
"desc":["규칙 엔진이 홀/반홀·기간·성수기 조건으로 임대 견적을 즉시 산정",
"HWP 신청→협의→견적(수일)을 즉시 자동 회신으로 전환"],
"input":["행사 기간(장치·전시·철거일)","홀/반홀 선택·면적(㎡)","부대(로비·옥외·회의실)","요율 룰셋 버전(테넌트)"],
"output":["임대료·관리비 예치금 소계","성수기/전시장 계수 반영 총액","견적 명세(항목별) JSON","킨텍스 제출 신청서식"],
"logic":["요율 마스터·성수기 규칙 로딩(tenant_id 스코프)",
"기본액 = 2,250원/㎡ × 면적 × 일수",
"계수 적용: 성수기 +10% / 비수기 -10% / 1전시장 +10%",
"초과시간(1일 임대료 1/10)·용도외 30% 할증 가산",
"예치금 15~20% 산출 → 견적 명세 조립·반환"],
"tables":["hall_master","rate_master","event","quotation_estimate"],
"exc":["요율 룰셋 미확정 → RULESET_NOT_FOUND 반환","면적/기간 유효성 위반 → VALIDATION 오류"],
"link":"연동 M9 정산·결제 · 행사일정 DB. 최종 배정 확정은 킨텍스 내부 의사결정(가견적까지)."},
{"id":"M02-BATCH-01","name":"부스 배치 3안 생성 엔진","type":"배치","scr":"SCR-03","color":PURPLE,
"module":"M2 플로어플랜 스튜디오 (P0)","lh":0.30,
"desc":["제약충족 솔버+휴리스틱으로 서로 다른 최적화 목표의 배치 정확히 3안 생성",
"생성형 LLM이 아닌 결정적 알고리즘 중심(LLM은 조건 해석 보조)"],
"input":["홀 선택·실측 지오메트리","목표 부스 수·프리미엄 비율","주출입구·무대·라운지 조건","통로폭·비상구·트렌치 제약"],
"output":["배치 3안(부스 폴리곤 집합)","안별 최적화 지표(부스수·동선·안전)","안별 규정검증 결과","S7 조감 시각화 입력"],
"logic":["홀 좌표계·제약(바닥하중·비상구·트렌치) 로딩",
"목표별 파라미터 분기(1안 부스수 / 2안 동선 / 3안 안전)",
"솔버로 부스 폴리곤 배치·통로 확보",
"각 안 M02-RULE-01 규정검증 호출",
"3안 + 지표·위반 요약 저장·반환"],
"tables":["hall_master","booth(PostGIS polygon)","design_plan(버전·출처)","trench_grid"],
"exc":["제약 충족 해 없음 → 부분안+완화 제안","트렌치 좌표 미확보 → 근사도면 경고 라벨"],
"link":"연동 M02-RULE-01·M03·M5·M6(제출 서류). 소방 최종 유권해석은 관할기관."},
{"id":"M02-RULE-01","name":"배치 규정 자동검증","type":"API","scr":"SCR-03·09","color":PURPLE,
"module":"M2 플로어플랜 스튜디오 (P0)","lh":0.30,
"desc":["배치안을 수치 규정에 대조해 위반을 차단(hard)·경고(soft)로 플래깅",
"병합/편집 결과는 재검증 필수(제약 파손 가능)"],
"input":["대상 배치안(부스 폴리곤)","홀 규정 룰셋(버전)","바닥하중·비상구·통로 기준","복층 가능 홀 매트릭스"],
"output":["위반 목록(항목·좌표·등급)","차단/경고 분류","검증 통과 여부(boolean)","검수 리포트(SCR-09)"],
"logic":["룰셋 로딩(tenant_id·버전 고정)",
"피난 통로폭·비상구 접근 거리 판정",
"홀별 바닥하중(2~5t/㎡)·복층 1/2 판정",
"이격(벽30cm·천장60cm)·소방 체크리스트 대조",
"위반 등급화 → hard=확정 차단, soft=경고"],
"tables":["ruleset_version","booth","hall_master","compliance_result"],
"exc":["룰셋 버전 불일치 → 최신 버전 강제·경고","좌표 결손 폴리곤 → 검증 제외·플래그"],
"link":"연동 M2·M3(SCR-09)·M10 승인. 최종 판정은 사람(홀매니저·구조기술사)."},
{"id":"M03-DSN-01","name":"부스 설계 3안 생성","type":"API","scr":"SCR-06","color":PURPLE,
"module":"M3 부스 설계 스튜디오 (P0)","lh":0.30,
"desc":["부스 크기·업종·예산 입력으로 레이아웃·파라메트릭 구조 초안 3안 생성",
"조립부스는 옵션 선택형, 독립부스는 AI 초안+장치업체 편집"],
"input":["부스 좌표·크기(M2)","업종·전시품·예산","용도존(시연/상담/창고)","조립/독립 유형"],
"output":["설계 3안(벽체·트러스·사인)","존 배치·집기 블록","M5 예상 사진 프롬프트 입력","규정 사전검증 플래그"],
"logic":["부스 골격·표준 자재 카탈로그 로딩",
"목표별 3안 분기(시연/상담/예산절감)",
"파라메트릭 구조·존 레이아웃 생성",
"M03-RULE-01 사전검증 호출(높이·방염)",
"3안 저장 → 선택/병합 대기"],
"tables":["booth","design_plan","material_catalog","design_zone"],
"exc":["예산/크기 비현실 → 경고·근사안","업로드 도면 인식 저정확 → 참고용 라벨"],
"link":"연동 M7 매칭·M5 시각화·kxwp 제출. 시공 상세도·구조판정은 장치업체 책임."},
{"id":"M04-WIRE-01","name":"배선 최단경로 산출 엔진","type":"API","scr":"SCR-07","color":PURPLE,
"module":"M4 유틸리티 설계 (P0)","lh":0.30,
"desc":["단말 위치에서 최근접 트렌치까지 통로 횡단 최소 배선 경로를 PostGIS로 산출",
"위치표시도 수기 작도를 자동 생성으로 대체"],
"input":["부스 내 단말 좌표(전기·LAN·급배수)","홀 트렌치 그리드 좌표","기기 목록·kW/회선","유틸리티 요금 마스터"],
"output":["배선 LineString(전기적/망청/급배수녹)","분전반 용량·수량","위치표시도·자동 견적","S6 오버레이 입력"],
"logic":["트렌치 그리드·공급 매트릭스 로딩",
"단말→최근접 트렌치 매칭(ST_ClosestPoint)",
"통로 횡단 최소 경로 산출(ST_ShortestLine)",
"kW 합산→분전반 용량·수량 산정",
"요금 견적·위치표시도 조립·반환"],
"tables":["trench_grid","utility_order(LineString)","rate_utility","booth"],
"exc":["트렌치 좌표 미확보 → 산출 보류·근사 경고","인터넷 마감(D-25) 초과 → 신청 차단 안내"],
"link":"연동 M9 결제·M5(S6 오버레이는 백엔드 래스터 우선)·kxwp/사무국 릴레이."},
{"id":"M05-WKR-01","name":"나노바나나 렌더 워커","type":"워커","scr":"SCR-12","color":PURPLE,
"module":"M5 나노바나나 시각화 (P0·핵심 차별화)","lh":0.30,
"desc":["Redis 큐의 RenderJob을 소비해 Gemini image-to-image로 시공 후 예상 사진 생성",
"ReRoomAI 방어 로직 이식 + 워터마크·고지 강제"],
"input":["RenderJob(씬 스키마·샷 프리셋)","참조 이미지(간이 렌더·홀 사진)","프롬프트 조각(유형·스타일·레이어)","행사별 렌더 쿼터"],
"output":["생성 이미지(오브젝트 스토리지)","워터마크·메타데이터 임베드","WebSocket 완료 푸시","스키마 해시 캐시"],
"logic":["큐 소비 → 크기 가드(8MB·base64 재검증)",
"참조 이미지 1024px 정규화·mimeType 감지",
"Gemini generateContent 호출(inlineData+text)",
"SAFETY/quota 에러 분기→친화 메시지",
"성공 시에만 쿼터 차감·워터마크→푸시"],
"tables":["render_job(상태·이미지)","render_quota","object_storage(메타)"],
"exc":["finishReason=SAFETY → 차단 에러","RESOURCE_EXHAUSTED/429 → 재시도·상한","한글 간판 렌더 불안정 → 후처리 합성"],
"link":"연동 M2~M4(자료)·WebSocket 푸시·M15 응찰 근거. API키는 서버 env(GEMINI_API_KEY)."},
{"id":"M06-DOC-01","name":"신고서류 웹폼→HWP/PDF","type":"API","scr":"SCR-23","color":BLUE,
"module":"M6 서류·마일스톤 워크플로","lh":0.30,
"desc":["7종 신고서류를 웹폼 입력으로 킨텍스 제출 형식(HWP/PDF) 자동 생성",
"LLM이 누락 항목·배치도-운영계획서 불일치 검수"],
"input":["서식 유형·행사·부스 데이터","배치도(M2)·정산(M9) 참조","작성자 입력 필드","서식 템플릿(마스터)"],
"output":["HWP/PDF 제출 파일","검수 요약 리포트(누락·불일치)","마일스톤 제출 상태","업로드 안내 릴레이"],
"logic":["서식 템플릿·행사 데이터 병합",
"필수 항목 누락 검출",
"배치도-운영계획서 부스수 등 교차 검증",
"HWP/PDF 렌더링·서명 필드 생성",
"검수 리포트→홀매니저 전달"],
"tables":["document","doc_template","milestone","event"],
"exc":["필수 필드 누락 → 생성 차단·목록 반환","AI 생성 이미지 포함 시 → 서류에서 자동 배제"],
"link":"연동 kxwp(제출 릴레이·API 미공개)·M10 승인. 계약·심사 서류엔 도면만 유효."},
{"id":"M07-ONL-01","name":"등록업체 매칭·RFQ","type":"온라인","scr":"SCR-24","color":BLUE,
"module":"M7 등록업체 매칭·견적","lh":0.30,
"desc":["14분류×739개 등록업체를 조건 기반 추천, 설계안 첨부 RFQ 발송·비교",
"미등록 업체 시공 엄금 규정을 시스템이 강제"],
"input":["부스 규모·업종·예산·지역","M3 설계안 첨부","공종 분류 필터","등록업체 DB(검증)"],
"output":["추천 업체 목록·점수","RFQ 발송·수신 현황","견적 비교표","옥션 초대 자격(적격)"],
"logic":["등록업체 DB 조회·자격 검증",
"조건 매칭 점수화·정렬",
"RFQ에 설계안·물량 첨부 발송",
"회신 견적 취합·비교표 구성",
"적격 업체만 M15 옥션 초대 허용"],
"tables":["company(등록업체)","rfq","rfq_quote","booth"],
"exc":["미등록 업체 초대 시도 → 원천 차단","응답 없음 → 리마인더·대체 추천"],
"link":"연동 M3(설계)·M15(옥션 자격 게이트)·CCPY 공개 DB 수집(M07-BAT-01)."},
{"id":"M09-BAT-01","name":"납부 스케줄 자동 생성","type":"배치","scr":"SCR-21","color":BLUE,
"module":"M9 정산·결제","lh":0.30,
"desc":["계약 확정 시 단계별 납부 스케줄을 자동 생성하고 역산 알림을 발행",
"예치금 대비 실사용 정산 내역 투명화"],
"input":["계약 총액·확정일","납부 규칙(20/30+20/30)","예치금 비율(15~20%)","행사 일정"],
"output":["납부 스케줄(회차·금액·기일)","알림센터 리마인더 이벤트","예치금 정산 기준","결제 대기 항목"],
"logic":["계약 총액·규칙 로딩",
"계약금20→중도금30+20→잔금30 분할",
"예치금 15~20% 별도 스케줄",
"기일 역산 알림 이벤트 발행",
"PG 결제 대기 항목 생성"],
"tables":["payment","payment_schedule","event","rate_master"],
"exc":["계약 미확정 → 스케줄 생성 보류","PG 실패 → 재시도·수동 처리 플래그"],
"link":"연동 M1 견적·PG 결제·CMN-ALRM 알림. 세금계산서 등 재무연동 협의."},
{"id":"M15-ONL-02","name":"옥션 상세·실시간 순위·응찰","type":"온라인/WS","scr":"SCR-27","color":BLUE,
"module":"M15 공사/장치 옥션 (핵심 플로우)","lh":0.30,
"desc":["적격 등록업체가 AI 자료 열람 후 견적서로 응찰, 실시간 순위를 WebSocket으로 노출",
"역경매(라운드 내 인하)·마감 자동 처리"],
"input":["옥션(유형·라운드·마감·기준)","M2~M5 자료·물량서","응찰 견적서(총액·납기)","업체 자격(M7 검증)"],
"output":["실시간 순위(현재/최저/내 위치)","응찰 이력(버전)","라운드 마감 상태","비교표 입력(Award)"],
"logic":["응찰 자격 검증(등록업체만)",
"AI 자료 열람 권한 부여",
"견적서 제출→종합점수 산정",
"실시간 순위 WebSocket 브로드캐스트",
"마감 도달→자동 마감·집계"],
"tables":["auction","quotation(=bid·버전)","company","render_job"],
"exc":["미등록 응찰 → 차단","마감 후 제출 → 거부","동점 → 납기·평판 tie-break"],
"link":"연동 M2~M5·M7·M15-AWD(낙찰)·M6/M9. 익명 순위 옵션 지원."},
{"id":"M15-AWD-01","name":"낙찰 비교·선정(Award)","type":"API","scr":"SCR-29","color":BLUE,
"module":"M15 공사/장치 옥션 (핵심 플로우)","lh":0.30,
"desc":["최저가 또는 종합평가(가격+평판+납기) 가중 스코어로 견적서를 비교·낙찰",
"낙찰 견적서를 계약/발주 문서로 전환"],
"input":["옥션 응찰 견적서 집합","낙찰기준·가중치","평판 데이터(규정준수·평점)","납기 조건"],
"output":["항목별 비교표·순위","낙찰(Award) 결과·사유","계약/발주 링크(M6/M9)","시공 일정 연계(M8)"],
"logic":["응찰 견적서 정규화(사양 정렬)",
"가격/평판/납기 점수화·가중합",
"최저가 or 종합점수 순위 산정",
"전시업체 낙찰 선정(사유 기록)",
"낙찰서→계약/발주 전환·감사기록"],
"tables":["auction","quotation","award","company_reputation"],
"exc":["유효 응찰 0건 → 유찰·재개설","가중치 합≠100% → 검증 오류"],
"link":"연동 M6 계약·M9 발주정산·M8 시공일정·SYS-AUDIT(낙찰 전수기록)."},
{"id":"M10-BADGE-01","name":"배지/QR 발급","type":"API","scr":"SCR-P4","color":BLUE,
"module":"M10 관람객 등록·배지·체크인","lh":0.30,
"desc":["사전등록 관람객에게 모바일 배지/QR을 발급, 현장 즉석 배지 인쇄 대비",
"배지 템플릿은 M17 CMS 관리"],
"input":["관람객 등록 정보(유형)","배지 템플릿(M17)","행사·부스 매핑","개인정보 동의 상태"],
"output":["모바일 배지/QR(서명)","배지 인쇄 데이터","체크인 검증 토큰","리드 스캔 대상 매핑"],
"logic":["등록·동의 상태 검증",
"배지 템플릿 병합·QR 토큰 생성",
"서명·만료 정책 적용",
"모바일 발급·인쇄 데이터 산출",
"체크인/리드 스캔 매핑 등록"],
"tables":["visitor","registration","badge","badge_template"],
"exc":["개인정보 미동의 → 발급 보류","중복 등록 → 기존 배지 재사용"],
"link":"연동 M10 체크인·M10-LEAD·M13 wayfinding. PII 동의·보존정책 필수(R10)."},
{"id":"M10-LEAD-01","name":"리드캡처·AI 스코어링","type":"API","scr":"SCR-32","color":BLUE,
"module":"M10 관람객 (참가업체 리드)","lh":0.30,
"desc":["참가업체가 배지 QR을 스캔해 연락처·관심도·메모를 저장하고 AI로 스코어링",
"팔로업 EDM(M12)·ROI 측정(M16)으로 연계"],
"input":["스캔 배지 QR 토큰","관심도 평점·메모","부스·참가업체 컨텍스트","동의 범위"],
"output":["리드 레코드(연락처·점수)","AI 리드 스코어","팔로업 세그먼트","리드 접근 감사로그"],
"logic":["QR 토큰 검증·관람객 해소",
"동의 범위 내 연락처 취득",
"관심도·행동신호 AI 스코어링",
"리드 저장·세그먼트 분류",
"리드 접근 감사로그 기록"],
"tables":["lead","visitor","booth","audit_log"],
"exc":["동의 범위 밖 필드 → 마스킹","오프라인 스캔 → 큐 저장 후 동기화"],
"link":"연동 M12 EDM·M16 BI. 리드 접근은 개인정보 전수 감사(§5B-1)."},
{"id":"M11-MATCH-01","name":"AI 미팅 추천","type":"API","scr":"SCR-M8","color":BLUE,
"module":"M11 비즈니스 매칭","lh":0.30,
"desc":["프로필·관심업종·의향(intent) 기반으로 미팅을 추천하고 슬롯 예약",
"부스 위치(M2)·wayfinding(M13) 길안내 연계"],
"input":["관람/참가 프로필","관심 업종·intent 신호","부스 업종 태그(M3)","가용 미팅 슬롯"],
"output":["매칭 추천 목록·점수","미팅 슬롯 예약","길안내 경로(M13)","매칭 성과(M16)"],
"logic":["프로필·행동신호 수집",
"관심·의향 임베딩 유사도 산정",
"상호 의향 매칭 랭킹",
"미팅 슬롯 예약·충돌 해소",
"부스 위치·경로 연계 반환"],
"tables":["visitor","match_recommendation","meeting_slot","booth"],
"exc":["프로필 결손 → 저신뢰 라벨","외부 모델 게이트 위반 → 승인 모델만"],
"link":"연동 M10·M13·M16. AI 추천은 온프레미스/승인 모델 범위(R12)."},
{"id":"M16-ETL-01","name":"BI 데이터마트 적재","type":"배치","scr":"SCR-13","color":PURPLE,
"module":"M16 경영분석 BI (P1 승격)","lh":0.30,
"desc":["운영 DB를 스타 스키마 데이터마트로 야간 집계해 운영 부하 없이 지표 제공",
"운영사 관점·참가사 관점 지표 분리"],
"input":["M1 배정·M9 정산 이력","M10 관람·M15 옥션","행사일정·비용 마스터","전 적재 스냅샷"],
"output":["Fact/Dim 테이블 적재","KpiSnapshot(가동률·매출·P&L)","수요예측·수율 입력","이상탐지 대상"],
"logic":["증분 소스 추출(읽기 복제)",
"DimHall/Event/Date/Exhibitor 갱신",
"FactBooking/Settlement/Utility/Auction/Visitor 적재",
"KPI 집계·스냅샷 생성",
"품질 검증→커밋(멱등)"],
"tables":["fact_booking","fact_settlement","dim_hall","kpi_snapshot"],
"exc":["소스 정합 결손 → 부분적재·경고","적재 실패 → 재시도·이전 스냅샷 유지"],
"link":"연동 전 모듈(소비)·M18 권한. 크로스테넌트 집계는 플랫폼 슈퍼관리자 전용."},
{"id":"M17-ONL-01","name":"CMS 콘텐츠·게시 워크플로","type":"온라인","scr":"SCR-35","color":PURPLE,
"module":"M17 CMS","lh":0.30,
"desc":["전시 콘텐츠·공지를 초안→검수→게시 워크플로와 버전관리로 발행",
"다국어(한/영/중/일)·사이니지 연계"],
"input":["콘텐츠 초안·미디어","게시 대상·채널","다국어 번역","승인자·게시 일정"],
"output":["게시 콘텐츠(버전)","예약 게시 큐","다국어 렌더","사이니지 배포 콘텐츠"],
"logic":["콘텐츠 작성·미디어 첨부",
"검수 요청→승인/반려",
"다국어 번역 검수",
"게시/예약 게시 처리",
"사이니지·공개사이트 배포"],
"tables":["content(버전)","content_publish","content_i18n","microsite"],
"exc":["승인 없이 게시 → 차단","번역 누락 → 기본 로케일 폴백"],
"link":"연동 M12 마케팅·M13 wayfinding·M10 배지. 공지 채널은 CMN-NOTI와 정합."},
{"id":"M18-ONL-02","name":"규정 룰셋 버전 관리","type":"온라인","scr":"SCR-A8","color":PURPLE,
"module":"M18 관리자 백오피스","lh":0.30,
"desc":["요율·규정 룰셋을 버전으로 관리해 연 단위 개정을 안전하게 반영",
"룰 엔진(M1·M2 검증)이 버전 고정으로 참조"],
"input":["룰셋 초안(요율·규정 값)","적용 시작일·테넌트","개정 사유·근거","승인자"],
"output":["룰셋 버전(불변 스냅샷)","활성 버전 포인터","개정 이력·감사로그","엔진 참조 캐시 무효화"],
"logic":["룰셋 초안 작성·검증",
"버전 스냅샷 생성(불변)",
"승인→활성 버전 전환(시작일)",
"엔진 캐시 무효화·재로딩",
"개정 감사로그 기록"],
"tables":["ruleset_version","rate_master","audit_log","tenant"],
"exc":["활성 버전 삭제 시도 → 차단","시작일 소급 → 경고·승인 필요"],
"link":"연동 M1 견적·M2 검증·전 모듈. 테넌트 스코프 격리(§1A)."},
{"id":"SYS-AUTH-02","name":"2차 인증 OTP(TOTP) 검증","type":"API","scr":"SCR-51","color":BLUE_DK,
"module":"§5B-3 인증 (UIWS 표준 이식)","lh":0.30,
"desc":["TOTP(RFC6238)로 로그인 2단계 인증을 수행하고 QR 등록·재설정을 제공",
"관리자·홀매니저·주최자·업체 필수, 관람객 선택"],
"input":["1차 인증 통과 컨텍스트","OTP 6자리 코드","OTP 시크릿(등록 시)","서버 시간"],
"output":["OTP 검증 결과","QR 등록 데이터(최초)","실패 카운트·잠금 상태","인증 완료 토큰"],
"logic":["사용자 OTP 시크릿 조회",
"SHA1·30s·±1 윈도우 코드 산정",
"입력 코드 대조",
"실패 시 카운트 증가·잠금 판정",
"성공→인증 완료·감사기록"],
"tables":["user(otp_secret)","login_attempt","audit_log"],
"exc":["코드 불일치 → 실패 카운트·잠금","관리자 초기화 → otp_secret=NULL 재등록"],
"link":"연동 SYS-AUTH-01·SYS-AUTH-03. 시크릿·비번은 응답/로그 미노출."},
{"id":"SYS-RBAC-01","name":"역할·권한(RBAC) 관리","type":"온라인","scr":"SCR-A2","color":BLUE_DK,
"module":"§5B-1 시스템관리 / M18","lh":0.30,
"desc":["플랫폼·행사 이중 평가 권한 모델을 관리(테넌트 격리 ⊃ 행사 RBAC)",
"역할별 포털 IA·메뉴 접근을 구동"],
"input":["역할 정의·계층","권한(리소스·액션) 매핑","테넌트·행사 스코프","사용자-역할 배정"],
"output":["역할-권한 매트릭스","메뉴·API 게이트 규칙","이중 평가 정책","권한 변경 감사로그"],
"logic":["역할 계층 로딩(플랫폼/테넌트/행사)",
"권한 매핑 편집·검증",
"테넌트 격리 제약 적용",
"메뉴·API 게이트 규칙 생성",
"변경 감사로그 기록"],
"tables":["role","permission","user_role","menu"],
"exc":["테넌트 경계 위반 배정 → 차단","슈퍼관리자 외 크로스테넌트 → 거부"],
"link":"연동 SYS-MENU·전 포털. 행사 RBAC는 테넌트 내부로 스코프(§1A-4)."},
{"id":"CMN-ALRM-01","name":"알림센터(notification)","type":"온라인/WS","scr":"SCR-47","color":BLUE,
"module":"§5B-2 공통 업무 기능","lh":0.30,
"desc":["마감(D-데이)·낙찰·승인·결제 등 이벤트를 단일 채널로 수집·푸시",
"M10·M12·M15가 이벤트를 발행, 발송 채널은 공통 단일화"],
"input":["이벤트(유형·대상·페이로드)","수신자·역할","알림 설정·구독","WebSocket 세션"],
"output":["알림 레코드(읽음 상태)","실시간 WebSocket 푸시","배지 카운트","이메일/모바일 팬아웃"],
"logic":["이벤트 수신·정규화",
"수신자 해소(역할·구독)",
"알림 저장·중복 억제",
"WebSocket 실시간 푸시",
"미접속 시 이메일/푸시 팬아웃"],
"tables":["notification","notification_pref","event_outbox"],
"exc":["WebSocket 미연결 → 오프라인 큐","발송 실패 → 재시도·데드레터"],
"link":"연동 M6 마일스톤·M15 낙찰·M9 결제. 마케팅 발송은 M12(수신동의)."},
]
for i, d in enumerate(DETAILS):
detail_slide(i+1, d)
# ================================================================
# 6. 핵심 업무 순서도 (도형 작도 8종)
# ================================================================
divider("04", "핵심 업무 순서도", "KEY WORKFLOW FLOWCHARTS",
["8종 핵심 업무 흐름", "python-pptx 도형 직접 작도", "단자·처리·판단·커넥터"])
# --- 순서도 배치 헬퍼 ---
def after(nodes, h, gap=0.28, cx=None):
n = nodes[-1]
return (cx if cx is not None else n["cx"], n["b"] + gap + h/2)
# --- FC1: 부스 배치 3안 생성 → 선택/병합 ---
s = slide()
content_header(s, "04 · 핵심 업무 순서도 (1/8)", "부스 배치 3안 AI 생성 → 선택 / 병합", 4, chip_txt="F1", chip_col=PURPLE)
fc_legend(s)
sp = vchain(s, 2.7, 1.55, [
(["설계 조건 입력", "(홀·부스수·프리미엄)"], "term"),
(["제약 로딩(통로·비상구", "·트렌치·바닥하중)"], "proc"),
(["솔버+휴리스틱 3안 생성", "(목표 다양화)"], "proc"),
(["3안 규정검증·시각화(S7)"], "io"),
], w=3.4, h=0.56, gap=0.22)
dcx, dcy = after(sp, 0.86, gap=0.26)
dec = fnode(s, dcx, dcy, 2.2, 0.86, ["선택 or", "병합?"], "dec")
arrow(s, sp[-1]["cx"], sp[-1]["b"], dec["cx"], dec["t"])
sel = fnode(s, 6.6, dcy-1.45, 3.0, 0.62, ["단일 안 선택"], "proc")
mrg = fnode(s, 6.6, dcy, 3.0, 0.62, ["레이어 병합 편집", "(안별 블록 조합)"], "proc")
rechk = fnode(s, 10.4, dcy-0.72, 2.8, 0.66, ["병합/선택안", "규정 재검증"], "proc")
fin = fnode(s, 7.6, 6.4, 7.4, 0.5, ["최종안 확정·버전기록 → 참가업체 초대링크(M3/M4)"], "proc_ok")
arrow(s, dec["r"], dec["cy"], sel["l"], sel["cy"], label="선택")
arrow(s, dec["r"], dec["cy"], mrg["l"], mrg["cy"], label="병합")
arrow(s, sel["r"], sel["cy"], rechk["l"], rechk["cy"])
arrow(s, mrg["r"], mrg["cy"], rechk["l"], rechk["cy"])
arrow(s, rechk["cx"], rechk["b"], rechk["cx"], fin["t"])
text(s, 0.55, 6.66, 12, 0.3, "메모 규정 재검증에서 위반 발견 시 병합 편집으로 되돌려 재수정 후 재검증(hard 위반은 확정 차단).", size=9, color=MUTED)
# --- FC2: 규정 자동검증 ---
s = slide()
content_header(s, "04 · 핵심 업무 순서도 (2/8)", "규정 자동검증 (위반 차단 / 경고)", 4, chip_txt="F2", chip_col=PURPLE)
fc_legend(s)
sp = vchain(s, 2.9, 1.6, [
(["배치/설계안 제출"], "term"),
(["룰셋 버전 로딩", "(tenant·버전 고정)"], "proc"),
(["규정 항목 순차 판정", "(통로·하중·높이·방염·이격)"], "proc"),
], w=3.5, h=0.6, gap=0.3)
dcx, dcy = after(sp, 0.94, gap=0.28)
dec = fnode(s, 2.9, dcy, 2.4, 0.94, ["위반", "등급?"], "dec")
arrow(s, sp[-1]["cx"], sp[-1]["b"], dec["cx"], dec["t"])
hard = fnode(s, 6.95, dcy-1.3, 3.1, 0.66, ["HARD 위반", "→ 제출 차단"], "proc_bad")
soft = fnode(s, 6.95, dcy, 3.1, 0.66, ["SOFT 위반", "→ 경고(진행 가능)"], "proc")
none = fnode(s, 6.95, dcy+1.3, 3.1, 0.6, ["위반 없음 → 통과·확정"], "proc_ok")
rep = fnode(s, 11.45, dcy, 1.5, 2.0, ["검증", "리포트", "(SCR-09)", "위반좌표", "오버레이"], "io")
arrow(s, dec["r"], dec["cy"], hard["l"], hard["cy"], label="차단")
arrow(s, dec["r"], dec["cy"], soft["l"], soft["cy"], label="경고")
arrow(s, dec["r"], dec["cy"], none["l"], none["cy"], label="정상")
arrow(s, hard["r"], hard["cy"], rep["l"], rep["cy"]-0.6)
arrow(s, soft["r"], soft["cy"], rep["l"], rep["cy"])
arrow(s, none["r"], none["cy"], rep["l"], rep["cy"]+0.6)
text(s, 0.55, 6.66, 12, 0.3, "원칙 hard=확정 차단, soft=경고 후 진행 가능. 최종 판정은 홀매니저·구조기술사(사람).", size=9, color=MUTED)
# --- FC3: 배선 최단경로 산출 (단일 수직 흐름) ---
s = slide()
content_header(s, "04 · 핵심 업무 순서도 (3/8)", "유틸리티 배선 최단경로 산출 (PostGIS)", 4, chip_txt="F3", chip_col=PURPLE)
fc_legend(s)
vchain(s, 5.6, 1.66, [
(["단말 위치 클릭(전기·LAN·급배수)"], "term"),
(["기기 목록·kW/회선 입력"], "io"),
(["트렌치 그리드·공급 매트릭스 로딩"], "proc"),
(["최근접 트렌치 매칭(ST_ClosestPoint)"], "proc"),
(["통로 횡단 최소 경로(ST_ShortestLine)"], "proc"),
(["kW 합산 → 분전반 용량·수량 + 요금 견적"], "proc"),
(["배선 LineString 저장·위치표시도 자동생성"], "proc_ok"),
(["S6 오버레이(백엔드 래스터 합성)"], "term"),
], w=5.4, h=0.48, gap=0.14)
text(s, 0.6, 6.72, 12, 0.3, "예외 트렌치 좌표 미확보 → 산출 보류·근사도면 경고. 인터넷 마감(D-25) 초과 → 신청 차단. S6는 생성모델 아닌 결정적 래스터 합성 우선.", size=8.6, color=MUTED)
# --- FC4: 나노바나나 RenderJob ---
s = slide()
content_header(s, "04 · 핵심 업무 순서도 (4/8)", "나노바나나 RenderJob (큐→워커→워터마크→푸시)", 4, chip_txt="F4", chip_col=PURPLE)
fc_legend(s)
sp = vchain(s, 3.0, 1.6, [
(["시각화 요청(M2~M4)"], "term"),
(["씬 컴파일·스키마 해시"], "proc"),
(["Redis 큐 발행(RenderJob)"], "io"),
], w=3.4, h=0.58, gap=0.26)
dcx, dcy = after(sp, 0.86, gap=0.28)
dec = fnode(s, 3.0, dcy, 2.3, 0.86, ["캐시", "HIT?"], "dec")
arrow(s, sp[-1]["cx"], sp[-1]["b"], dec["cx"], dec["t"])
gem = fnode(s, 8.0, 1.95, 3.3, 0.64, ["워커 소비·Gemini 호출", "(inlineData + text)"], "wkr")
derr = fnode(s, 8.0, 3.25, 3.3, 0.86, ["응답", "상태?"], "dec")
bad = fnode(s, 11.6, 3.25, 1.4, 0.86, ["SAFETY/429", "친화 에러", "쿼터 미차감"], "proc_bad")
o1 = fnode(s, 8.0, 4.55, 3.3, 0.56, ["워터마크·메타 임베드"], "proc")
o2 = fnode(s, 8.0, 5.55, 3.3, 0.56, ["스토리지 저장 + 성공 시 쿼터 차감"], "proc")
o3 = fnode(s, 8.0, 6.55, 3.3, 0.56, ["WebSocket 완료 푸시(S1~S7)"], "proc_ok")
arrow(s, dec["r"], dec["cy"], gem["l"], gem["cy"], label="MISS")
arrow(s, gem["cx"], gem["b"], derr["cx"], derr["t"])
arrow(s, derr["r"], derr["cy"], bad["l"], bad["cy"], label="실패")
arrow(s, derr["cx"], derr["b"], o1["cx"], o1["t"], label="성공")
arrow(s, o1["cx"], o1["b"], o2["cx"], o2["t"])
arrow(s, o2["cx"], o2["b"], o3["cx"], o3["t"])
arrow_elbow(s, dec["cx"], dec["b"], o3["l"], o3["cy"], first='v', label="HIT")
# --- FC5: 공사 옥션 ---
s = slide()
content_header(s, "04 · 핵심 업무 순서도 (5/8)", "공사 옥션 (견적 → 역경매 → 낙찰)", 4, chip_txt="F5", chip_col=BLUE)
fc_legend(s)
sp = vchain(s, 3.0, 1.5, [
(["옥션 개설(유형·라운드·마감)"], "term"),
(["M2~M5 자료·물량서 첨부"], "io"),
(["응찰 자격 검증(M7) 등록업체만"], "proc"),
(["적격 업체 자료 열람"], "proc"),
(["견적서 제출(응찰)"], "proc"),
], w=3.7, h=0.52, gap=0.18)
dcx, dcy = after(sp, 0.82, gap=0.24)
dec = fnode(s, 3.0, dcy, 2.2, 0.82, ["마감/라운드", "종료?"], "dec")
arrow(s, sp[-1]["cx"], sp[-1]["b"], dec["cx"], dec["t"])
rebid = fnode(s, 6.7, dcy-1.25, 3.0, 0.62, ["재응찰", "(라운드 내 인하)"], "proc")
arrow(s, dec["r"], dec["cy"], rebid["l"], rebid["cy"], label="No")
arrow_elbow(s, rebid["cx"], rebid["b"], dec["cx"]+1.1, dec["t"]+0.15, first='v')
yes = vchain(s, 9.4, 1.6, [
(["종합점수 산정(가격+평판+납기)"], "proc"),
(["견적서 비교표·순위"], "io"),
(["전시업체 낙찰(Award)"], "proc"),
(["계약/발주 전환(M6·M9)·감사기록"], "proc_ok"),
], w=3.5, h=0.58, gap=0.24)
arrow(s, dec["r"], dec["cy"]-0.1, yes[0]["l"], yes[0]["cy"], label="Yes")
# --- FC6: 관람객 등록 → 배지 → 체크인 ---
s = slide()
content_header(s, "04 · 핵심 업무 순서도 (6/8)", "관람객 등록 → 배지 → 체크인", 4, chip_txt="F6", chip_col=BLUE)
fc_legend(s)
sp = vchain(s, 3.0, 1.55, [
(["온라인 사전등록(유형별 폼)"], "term"),
(["개인정보 동의 확인"], "proc"),
(["배지/QR 발급(서명·만료)"], "proc"),
(["모바일 배지 수신"], "io"),
], w=3.4, h=0.54, gap=0.2)
dcx, dcy = after(sp, 0.86, gap=0.26)
dec = fnode(s, 3.0, dcy, 2.3, 0.86, ["현장", "네트워크?"], "dec")
arrow(s, sp[-1]["cx"], sp[-1]["b"], dec["cx"], dec["t"])
onl = fnode(s, 6.9, dcy, 3.0, 0.62, ["QR 온라인 체크인"], "proc")
off = fnode(s, 6.9, dcy-1.35, 3.0, 0.62, ["오프라인 폴백 큐"], "proc_bad")
arrow(s, dec["r"], dec["cy"], onl["l"], onl["cy"], label="정상")
arrow(s, dec["r"], dec["cy"], off["l"], off["cy"], label="불안정")
arrow(s, off["cx"], off["b"], onl["cx"], onl["t"], label="복구 동기화")
tail = vchain(s, 10.7, dcy-1.35, [
(["즉석 배지 인쇄·입장 집계"], "proc"),
(["리드캡처 매핑(M10-LEAD)"], "proc"),
(["체크인 데이터 → M16 BI"], "proc_ok"),
], w=3.1, h=0.6, gap=0.24)
arrow(s, onl["r"], onl["cy"], tail[0]["l"], tail[0]["cy"])
# --- FC7: 승인 워크플로 (2FA 포함) ---
s = slide()
content_header(s, "04 · 핵심 업무 순서도 (7/8)", "승인 워크플로 (2FA 포함)", 4, chip_txt="F7", chip_col=BLUE_DK)
fc_legend(s)
sp = vchain(s, 3.0, 1.55, [
(["로그인(ID/PW)"], "term"),
(["JWT 발급"], "proc"),
], w=3.2, h=0.56, gap=0.22)
dcx, dcy = after(sp, 0.86, gap=0.24)
dec = fnode(s, 3.0, dcy, 2.3, 0.86, ["2FA 대상", "역할?"], "dec")
arrow(s, sp[-1]["cx"], sp[-1]["b"], dec["cx"], dec["t"])
otp = fnode(s, 3.0, dcy+1.35, 3.1, 0.6, ["OTP(TOTP) 검증·실패 잠금"], "proc")
arrow(s, dec["cx"], dec["b"], otp["cx"], otp["t"], label="필수")
que = fnode(s, 8.3, 2.2, 3.2, 0.62, ["승인 큐 진입(AI 사전심사 플래그)"], "io")
rev = fnode(s, 8.3, 3.4, 3.2, 0.6, ["검수 상세(도면+위반 오버레이)"], "proc")
dap = fnode(s, 8.3, 4.65, 2.4, 0.86, ["승인", "판정?"], "dec")
rej = fnode(s, 11.6, 4.65, 1.4, 0.86, ["반려", "사유 통지", "재제출"], "proc_bad")
ap1 = fnode(s, 8.3, 5.85, 3.2, 0.56, ["승인 확정·상태 전이"], "proc")
ap2 = fnode(s, 8.3, 6.6, 3.2, 0.5, ["감사로그 기록 + 알림센터 통지(CMN-ALRM)"], "proc_ok")
arrow(s, dec["r"], dec["cy"], que["l"], que["cy"], label="선택/면제")
arrow(s, otp["r"], otp["cy"], que["l"], que["cy"]+0.1)
arrow(s, que["cx"], que["b"], rev["cx"], rev["t"])
arrow(s, rev["cx"], rev["b"], dap["cx"], dap["t"])
arrow(s, dap["r"], dap["cy"], rej["l"], rej["cy"], label="반려")
arrow(s, dap["cx"], dap["b"], ap1["cx"], ap1["t"], label="승인")
arrow(s, ap1["cx"], ap1["b"], ap2["cx"], ap2["t"])
# --- FC8: CI/CD 배포 ---
s = slide()
content_header(s, "04 · 핵심 업무 순서도 (8/8)", "CI/CD 배포 (빌드 → health → 롤백)", 4, chip_txt="F8", chip_col=BLUE_DK)
fc_legend(s)
sp = vchain(s, 3.0, 1.55, [
(["Gitea push(main)"], "term"),
(["webhook 수신(deploy 서버)"], "proc"),
(["백엔드 bootJar + 프론트 Vite 빌드"], "proc"),
(["나노바나나 워커 패키징"], "proc"),
(["배포·서비스 재기동"], "proc"),
], w=3.7, h=0.56, gap=0.22)
dcx, dcy = after(sp, 0.9, gap=0.24)
dec = fnode(s, 3.0, dcy, 2.3, 0.9, ["health", "check", "OK?"], "dec")
arrow(s, sp[-1]["cx"], sp[-1]["b"], dec["cx"], dec["t"])
okb = fnode(s, 7.3, dcy, 3.4, 0.62, ["트래픽 전환·배포 완료"], "proc_ok")
rb = fnode(s, 7.3, dcy-1.3, 3.4, 0.62, ["직전 버전 롤백"], "proc_bad")
arrow(s, dec["r"], dec["cy"], okb["l"], okb["cy"], label="UP")
arrow(s, dec["r"], dec["cy"], rb["l"], rb["cy"], label="FAIL")
arrow(s, rb["cx"], rb["b"], okb["cx"], okb["t"], label="복구")
fin8 = fnode(s, 7.3, dcy+1.1, 3.4, 0.56, ["배포 결과 알림 + 감사로그·배포 이력 기록"], "term")
arrow(s, okb["cx"], okb["b"], fin8["cx"], fin8["t"])
text(s, 0.55, 6.74, 12, 0.3, "보안 불변 API키·비밀번호·서버IP·SSH 계정은 코드·커밋·로그·응답에 미기재(서버 env 로드).", size=9, color=MUTED)
# ================================================================
# 7. 공통 규약
# ================================================================
divider("05", "공통 규약", "COMMON CONVENTIONS",
["API 응답 봉투(ApiResponse)", "에러 코드·처리", "감사 로그(@Audited)"])
s = slide()
content_header(s, "05 · 공통 규약", "API 응답 봉투 · 에러 처리 · 감사 로그", 5)
# 응답 봉투
card(s, 0.55, 1.5, 6.05, 2.55)
rect(s, 0.55, 1.5, 6.05, 0.46, fill=BLUE, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
text(s, 0.55, 1.5, 6.05, 0.46, "API 응답 봉투 (ApiResponse<T>)", size=12, color=WHITE, bold=True, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)
text(s, 0.78, 2.05, 5.6, 0.28, "모든 REST 응답은 공통 봉투 사용(예외 없음)", size=9, color=MUTED)
rect(s, 0.78, 2.38, 5.6, 0.62, fill=RGBColor(0x0E,0x14,0x22), shape=MSO_SHAPE.ROUNDED_RECTANGLE)
text(s, 0.95, 2.44, 5.3, 0.52, [
[{"t":'성공 ',"size":8.5,"color":GREEN,"bold":True},{"t":'{ "success": true, "data": {…}, "error": null }',"size":8.5,"color":RGBColor(0xD6,0xE2,0xF0)}],
[{"t":'실패 ',"size":8.5,"color":AMBER,"bold":True},{"t":'{ "success": false, "data": null, "error": {code,message} }',"size":8.5,"color":RGBColor(0xD6,0xE2,0xF0)}],
], line_spacing=1.1)
for i,t in enumerate([
"성공: 컨트롤러 ApiResponse.ok(dto)",
"실패: ApiException throw → GlobalExceptionHandler 봉투 변환",
"페이징: 공통 PageResponse<T>",
]):
y=3.12+i*0.3
rect(s, 0.82, y+0.06, 0.09, 0.09, fill=BLUE, shape=MSO_SHAPE.OVAL)
text(s, 1.02, y, 5.4, 0.28, t, size=9, color=INK)
# 에러 처리
card(s, 6.75, 1.5, 6.03, 2.55)
rect(s, 6.75, 1.5, 6.03, 0.46, fill=AMBER, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
text(s, 6.75, 1.5, 6.03, 0.46, "에러 처리 (ErrorCode·ApiException)", size=12, color=WHITE, bold=True, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)
for i,(h,d) in enumerate([
("단일 매핑","ErrorCode(enum)가 코드↔HTTP 단일 매핑, 신규는 여기에만 추가"),
("3자 세트","ErrorCode → ApiException(코드+요약) → GlobalExceptionHandler"),
("스택 미노출","error.message는 요약만, 상세는 서버 로그(include-* : never)"),
("남용 금지","RuntimeException 남발 금지 — 최종 방어선만 INTERNAL"),
("보안","IP·SSH·비번·PII를 응답·에러에 미노출"),
]):
y=2.08+i*0.38
rect(s, 7.0, y+0.04, 0.11, 0.11, fill=AMBER, shape=MSO_SHAPE.OVAL)
text(s, 7.22, y, 5.4, 0.36, [[{"t":h+"","size":9.4,"color":INK,"bold":True},{"t":d,"size":8.8,"color":MUTED}]], line_spacing=0.92)
# 감사 로그
card(s, 0.55, 4.28, 12.23, 2.15)
rect(s, 0.55, 4.28, 12.23, 0.46, fill=BLUE_DK, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
text(s, 0.55, 4.28, 12.23, 0.46, "감사 로그 (@Audited AOP → TB_AUDIT_LOG)", size=12, color=WHITE, bold=True, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)
audit = [
("기록 대상","상태 변경 API — 승인·낙찰(M15)·설계 변경·룰셋 개정·리드 접근(PII)"),
("기록 항목","액터 · 행사 · 대상 · before/after 요약 · 룰셋 버전 · 시각"),
("미기록","민감정보 · 비밀번호 · 스택트레이스 (보안 불변)"),
("스키마","WISE TB_AUDIT_LOG 이식 · 멱등 DDL(sql.init mode=always + continue-on-error)"),
]
for i,(h,d) in enumerate(audit):
x=0.75+(i%2)*6.1
y=4.9+(i//2)*0.72
rect(s, x, y, 5.85, 0.62, fill=HALL_BG, line=LINE, line_w=1.0, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
text(s, x+0.18, y+0.1, 5.55, 0.26, h, size=10, color=BLUE_DK, bold=True)
text(s, x+0.18, y+0.36, 5.55, 0.24, d, size=8.6, color=MUTED)
text(s, 0.55, 6.62, 12.2, 0.3,
"적용 원칙 ApiResponse·ErrorCode·감사로그는 전 프로그램 공통(architecture/app.md §5-2·5-3·7-1 정본). 신규 프로그램도 예외 없이 준수.",
size=9, color=MUTED)
# ================================================================
# 저장
# ================================================================
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))