# -*- 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)", 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", ]): 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))