kintex/docs/deliverables/지침서/_gen/gen_guides_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

1026 lines
64 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 -*-
"""
킨텍스 자동전시시스템 — 지침서 3종 PPTX 일괄 생성기
· 사용자지침서.pptx · 운영자지침서.pptx · 개발자지침서.pptx
- 디자인: docs/deliverables/_gen/gen_dev_plan_pptx.py 스타일 헬퍼 그대로 차용
(KINTEX Blue #0066B3 / AI Purple #6D4AFF / 맑은 고딕 / 16:9,
표지·간지·본문 마스터, 도식은 python-pptx 도형 직접 작도)
- 내용 소스(사실 기반): docs/PLANNING.md v3.0, docs/design.md v2.0(§2.8 80화면 인벤토리),
docs/OPS_RUNBOOK.md, docs/ENV_SETUP.md, docs/BUILD_DEPLOY.md,
docs/DEVELOPMENT_GUIDE.md, docs/SECURITY.md, mobile/README.md
- 보안 불변: 비밀번호/API키/서버IP/SSH 계정 미기재(env 키 '이름'만 언급, 값 금지)
재실행: python gen_guides_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
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)
RED = RGBColor(0xD0, 0x3A, 0x3A)
SLATE = RGBColor(0x98, 0xA2, 0xB3)
CARD = RGBColor(0xFF, 0xFF, 0xFF)
HALL_BG = RGBColor(0xF2, 0xF4, 0xF7)
FONT = "맑은 고딕"
EMU_W, EMU_H = 12192000, 6858000
IN = 914400
def inch(v): return int(v * IN)
# 문서 컨텍스트 (build 시 세팅)
PRS = None
BLANK = None
DOC_FOOTER = ""
DOC_DATE = "2026-07-11"
# ---------------- 헬퍼 ----------------
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':'18000'})
clr.append(alp); sh.append(clr); ef.append(sh)
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)
def content_header(s, kicker, title, idx):
page_bg(s, BG)
rect(s, 0, 0, 13.333, 0.12, fill=BLUE)
rect(s, 0.55, 0.5, 0.62, 0.62, fill=BLUE, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
text(s, 0.55, 0.5, 0.62, 0.62, f"{idx:02d}", size=20, color=WHITE, bold=True,
align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)
text(s, 1.32, 0.5, 10.5, 0.28, kicker, size=11, color=PURPLE, bold=True)
text(s, 1.32, 0.74, 11.4, 0.44, title, size=22, color=INK, bold=True)
rect(s, 0.55, 1.28, 12.23, 0.02, fill=LINE)
text(s, 0.55, 7.06, 9, 0.3, DOC_FOOTER, size=8.5, color=MUTED)
text(s, 11.3, 7.06, 1.48, 0.3, DOC_DATE, size=8.5, color=MUTED, align=PP_ALIGN.RIGHT)
def bullets(s, x, y, w, items, dot=BLUE, size=10.5, gap=0.46, color=INK, ls=1.0, dotsz=0.1):
"""items: str 또는 [head, desc] 튜플"""
for i, it in enumerate(items):
yy = y + i*gap
rect(s, x, yy+0.07, dotsz, dotsz, fill=dot, shape=MSO_SHAPE.OVAL)
if isinstance(it, (tuple, list)):
text(s, x+0.24, yy, w-0.24, gap, [[{"t":it[0]+"","size":size,"color":INK,"bold":True},
{"t":it[1],"size":size-0.6,"color":MUTED}]], line_spacing=ls)
else:
text(s, x+0.24, yy, w-0.24, gap, it, size=size, color=color, line_spacing=ls)
def titled_card(s, x, y, w, h, title, tcolor, items, item_size=9.6, gap=0.4):
card(s, x, y, w, h)
rect(s, x, y, w, 0.5, fill=tcolor, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
text(s, x, y, w, 0.5, title, size=12.5, color=WHITE, bold=True,
align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)
for i, it in enumerate(items):
yy = y+0.66+i*gap
rect(s, x+0.2, yy+0.07, 0.09, 0.09, fill=tcolor, shape=MSO_SHAPE.OVAL)
if isinstance(it, (tuple, list)):
text(s, x+0.4, yy, w-0.55, gap, [[{"t":it[0]+" ","size":item_size,"color":INK,"bold":True},
{"t":it[1],"size":item_size-0.8,"color":MUTED}]], line_spacing=0.95)
else:
text(s, x+0.4, yy, w-0.55, gap, it, size=item_size, color=INK, line_spacing=0.95)
def steps(s, x, y, w, items, band=BLUE, sep=""):
"""가로 단계 파이프라인. items: [(title, sub, color)]"""
n = len(items); g = 0.14
bw = (w - (n-1)*g) / n
for i, (t, sub, cl) in enumerate(items):
bx = x + i*(bw+g)
rect(s, bx, y, bw, 0.78, fill=WHITE, line=cl, line_w=1.3, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
text(s, bx, y+0.1, bw, 0.3, t, size=11, color=cl, bold=True, align=PP_ALIGN.CENTER)
if sub:
text(s, bx, y+0.42, bw, 0.3, sub, size=7.6, color=MUTED, align=PP_ALIGN.CENTER)
if i < n-1:
text(s, bx+bw-0.03, y, 0.22, 0.78, sep, size=16, color=SLATE, bold=True,
align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)
# ---------------- 공통 슬라이드: 표지 ----------------
def cover(badge_kr, badge_en, title_lines, subtitle, closing, meta_pairs):
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, badge_kr, PURPLE, WHITE, size=12)
chip(s, 2.92, 2.35, 2.35, 0.42, badge_en, RGBColor(0x2B,0x4C,0x7E), WHITE, size=10.5)
text(s, 0.85, 2.95, 11, 1.9, [[{"t":ln, "size":46, "color":WHITE, "bold":True}] for ln in title_lines],
line_spacing=1.02)
text(s, 0.88, 2.95 + 0.92*len(title_lines) + 0.08, 11, 0.5, subtitle,
size=16, color=RGBColor(0xC7,0xDD,0xF2))
text(s, 0.88, 5.35, 11.4, 0.4, closing, size=12.5, color=RGBColor(0xB9,0xC7,0xEA))
rect(s, 0.85, 6.15, 11.63, 0.02, fill=RGBColor(0x3A,0x5A,0x8C))
ly = 6.35
for label, val in meta_pairs[:2]:
text(s, 0.85, ly, 9, 0.4, [[{"t":label+" ","size":11,"color":RGBColor(0x90,0xA6,0xC9)},
{"t":val,"size":11,"color":WHITE,"bold":True}]])
ly += 0.37
text(s, 9.7, 6.35, 2.78, 0.4, "작성일 "+DOC_DATE, size=11, color=WHITE, bold=True, align=PP_ALIGN.RIGHT)
text(s, 9.7, 6.72, 2.78, 0.4, "KINTEX 자동전시시스템", size=9.5, color=RGBColor(0x90,0xA6,0xC9), align=PP_ALIGN.RIGHT)
# ---------------- 공통 슬라이드: 목차 ----------------
def toc(items):
s = slide()
content_header(s, "CONTENTS", "목차", 0)
rect(s, 0.55, 0.5, 0.62, 0.62, fill=PURPLE, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
text(s, 0.55, 0.5, 0.62, 0.62, "C", size=22, color=WHITE, bold=True,
align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)
n = len(items)
per_col = (n + 1) // 2 if n > 5 else n
col_w = 5.85
for i, (no, t, d) in enumerate(items):
col = i // per_col
row = i % per_col
x = 0.7 + col * (col_w + 0.35)
y = 1.62 + row * 1.02
rect(s, x, y, col_w, 0.88, fill=CARD, line=LINE, line_w=1.0, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
rect(s, x, y, 0.09, 0.88, fill=BLUE if col==0 else PURPLE, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
text(s, x+0.28, y, 0.9, 0.88, no, size=24, color=(BLUE if col==0 else PURPLE), bold=True, anchor=MSO_ANCHOR.MIDDLE)
text(s, x+1.15, y+0.15, col_w-1.3, 0.35, t, size=14.5, color=INK, bold=True)
text(s, x+1.15, y+0.5, col_w-1.3, 0.32, d, size=9.5, color=MUTED)
# ---------------- 공통 슬라이드: 간지 ----------------
def divider(num, kr, en, points):
s = slide()
rect(s, -0.1, -0.1, 13.53, 7.7, fill=BLUE_DK)
rect(s, 8.6, -1.5, 8, 11, fill=BLUE, shape=MSO_SHAPE.PARALLELOGRAM)
rect(s, 10.9, -2, 6, 12, fill=PURPLE, shape=MSO_SHAPE.PARALLELOGRAM)
text(s, 1.0, 2.15, 5, 2.2, num, size=150, color=RGBColor(0x2C,0x4E,0x82), bold=True)
rect(s, 1.15, 3.9, 0.9, 0.09, fill=PURPLE)
text(s, 1.15, 4.05, 8, 0.8, kr, size=34, color=WHITE, bold=True)
text(s, 1.18, 4.85, 8, 0.4, en, size=13, color=RGBColor(0x9C,0xB4,0xD8), bold=True)
for i, p in enumerate(points):
yy = 2.35 + i*0.62
rect(s, 8.05, yy+0.06, 0.16, 0.16, fill=PURPLE, shape=MSO_SHAPE.OVAL)
text(s, 8.35, yy, 4.4, 0.5, p, size=12.5, color=WHITE, anchor=MSO_ANCHOR.MIDDLE)
return s
# 안내(고지) 배너
def banner(s, y, msg, fill=BLUE, h=0.62, size=13, x=0.55, w=12.23):
rect(s, x, y, w, h, fill=fill, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
text(s, x+0.3, y, w-0.6, h, msg, size=size, color=WHITE, bold=True, anchor=MSO_ANCHOR.MIDDLE)
# =====================================================================
# 1) 사용자지침서
# =====================================================================
def build_user():
global PRS, BLANK, DOC_FOOTER
PRS = Presentation(); PRS.slide_width = EMU_W; PRS.slide_height = EMU_H
BLANK = PRS.slide_layouts[6]
DOC_FOOTER = "킨텍스 자동전시시스템 · 사용자지침서"
cover("사용자지침서", "User Guide",
["킨텍스 자동전시시스템", "사용자 지침서"],
"역할별 사용자를 위한 화면·업무 흐름 안내서",
"로그인 → 부스 설계·시각화 → 옥션 → 관람객 등록 → 승인까지 한 흐름",
[("대상", "주최자 · 참가업체 · 장치/공사업체 · 홀매니저 · 관람객"),
("구성", "접속 · 대시보드 · 핵심업무 · 모바일 · FAQ")])
toc([
("01", "시스템 소개 · 접속", "개요 / 로그인·2FA / 아이디기억·비번찾기"),
("02", "역할별 홈 · 대시보드", "6역할 포털 / MDI 탭 / 마일스톤 대시보드"),
("03", "핵심 업무 흐름", "부스 설계·병합 / 배선·신청 / 시각화 / 옥션 / 등록·승인"),
("04", "모바일 앱 사용법", "현장 체크리스트 / 검수 / 시각화 갤러리"),
("05", "FAQ · 문의", "자주 묻는 질문 / 지원 채널"),
])
# ---- 01 소개·접속 ----
divider("01", "시스템 소개 · 접속", "INTRODUCTION & SIGN-IN",
["전시 생애주기 폐루프", "역할별 포털", "로그인 · 2차 인증"])
s = slide(); content_header(s, "01 · 시스템 소개", "무엇을 할 수 있나요", 1)
text(s, 0.55, 1.45, 12.2, 0.5,
"킨텍스 자동전시시스템은 홀 배정·부스 설계·유틸리티 배선·규정 검증·시공 발주(옥션)·관람객 운영·경영분석을 하나의 계정으로 잇는 전시 운영 플랫폼입니다.",
size=11.5, color=MUTED)
banner(s, 2.05, "핵심 “신청서를 내는 순간, 시공 후 사진을 먼저 봅니다.” — 나노바나나(AI) 시공 예상 이미지",
fill=BLUE, size=15)
loop = [("판매·기획", BLUE), ("AI 설계·시각화", PURPLE), ("공사 옥션", BLUE),
("시공", BLUE), ("운영·관람", BLUE), ("경영분석", PURPLE)]
bw = 1.83; gap = 0.19; x0 = 0.55; yb = 3.05
for i, (lb, cl) in enumerate(loop):
x = x0 + i*(bw+gap)
rect(s, x, yb, bw, 0.7, fill=(PURPLE_LT if cl==PURPLE else BLUE_LT), line=cl, line_w=1.2, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
text(s, x, yb, bw, 0.7, lb, size=11.5, color=(PURPLE if cl==PURPLE else BLUE_DK), bold=True, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)
if i < len(loop)-1:
text(s, x+bw-0.03, yb, 0.28, 0.7, "", size=15, color=SLATE, bold=True, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)
titled_card(s, 0.55, 4.1, 3.95, 2.55, "내가 얻는 것", BLUE, [
("주최자", "자동 견적·배치·마일스톤 트래킹"),
("참가업체", "클릭 신청 + 시공 전 부스 사진"),
("장치·공사업체", "제출 전 규정검증·옥션 응찰"),
("홀매니저", "위반 자동 플래깅·부하 집계"),
("관람객", "사전등록·모바일 배지·길찾기"),
], item_size=9.8, gap=0.36)
titled_card(s, 4.65, 4.1, 3.95, 2.55, "AI가 돕는 일", PURPLE, [
("배치 3안", "조건 입력 → 3안 자동생성"),
("규정 검증", "높이·통로·바닥하중 자동 점검"),
("배선 설계", "트렌치 최단 경로·자동 견적"),
("시각화", "시공 후 예상 사진(S1~S7)"),
("검수 보조", "AI 사전심사·위반 플래그"),
], item_size=9.8, gap=0.36)
titled_card(s, 8.75, 4.1, 4.03, 2.55, "꼭 알아둘 점", BLUE_DK, [
("AI 이미지", "‘예상’ 이미지 — 실물과 다를 수 있음"),
("워터마크", "계약·인허가 서류로 사용 불가"),
("등록업체", "미등록 업체는 초대·응찰 불가"),
("최종 판정", "소방·구조는 관할기관·기술사 몫"),
("2차 인증", "로그인 시 OTP/이메일 코드 필요"),
], item_size=9.8, gap=0.36)
s = slide(); content_header(s, "01 · 접속 · 로그인", "로그인 · 2차 인증 · 계정 관리", 1)
steps(s, 0.55, 1.5, 12.23, [
("① 이메일·비밀번호", "또는 킨텍스 SSO", BLUE),
("② 2차 인증", "OTP 또는 이메일 코드", PURPLE),
("③ 행사 선택", "워크스페이스 카드", BLUE),
("④ 역할별 홈", "대시보드 진입", GREEN),
])
titled_card(s, 0.55, 2.55, 6.05, 2.15, "로그인 화면(SCR-01)", BLUE, [
"이메일/비밀번호 또는 ‘킨텍스 내부 계정(SSO) 로그인",
"‘아이디 기억’ 체크 — 이메일만 저장(비밀번호는 저장하지 않음)",
"1차 로그인 후 2차 인증코드 입력(OTP/이메일)",
"로그인 후 참여 행사 카드에서 워크스페이스 선택",
"초대 코드로 참여 — 하단 ‘초대 코드로 참여하기’",
], item_size=9.8, gap=0.36)
titled_card(s, 6.75, 2.55, 6.03, 2.15, "2차 인증·계정(SCR-49~51)", PURPLE, [
("회원가입", "SCR-49 웹 회원가입 폼"),
("비밀번호 찾기", "SCR-50 이메일 인증 후 재설정"),
("OTP 설정", "SCR-51 최초 QR 등록·재설정"),
("잠금 해제", "실패 반복 잠금 시 관리자 초기화"),
("마이페이지", "SCR-48 OTP 재설정·개인화"),
], item_size=9.8, gap=0.36)
banner(s, 4.95, "안내 미등록 장치업체가 초대를 수락하면 ‘킨텍스 등록업체만 참여할 수 있습니다’ 안내와 함께 차단됩니다.",
fill=BLUE_DK, size=11.5, h=0.5)
text(s, 0.55, 5.6, 12.2, 0.9, [
[{"t":"비밀번호를 잊었을 때 ","size":10.5,"color":INK,"bold":True},
{"t":"로그인 화면 → ‘비밀번호 찾기’ → 이메일 인증코드 → 새 비밀번호 설정. ","size":10,"color":MUTED}],
[{"t":"OTP 기기를 바꿨을 때 ","size":10.5,"color":INK,"bold":True},
{"t":"마이페이지에서 재설정하거나, 접근 불가 시 관리자에게 OTP 초기화를 요청하세요.","size":10,"color":MUTED}],
], line_spacing=1.15, space_after=6)
# ---- 02 역할별 홈·대시보드 ----
divider("02", "역할별 홈 · 대시보드", "ROLE HOMES & DASHBOARD",
["6역할 포털 분리", "MDI 탭 작업환경", "마일스톤 대시보드"])
s = slide(); content_header(s, "02 · 역할별 포털", "내 역할에 맞는 포털로 들어갑니다", 2)
text(s, 0.55, 1.4, 12.2, 0.35,
"역할마다 목적이 다르므로 포털이 분리되어 있습니다. 데스크톱은 설계·대시보드, 모바일은 현장·조회·승인에 최적화됩니다.",
size=10.5, color=MUTED)
rect(s, 0.55, 1.9, 12.23, 0.44, fill=BLUE, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
for x,w,t in [(0.55,2.0,"역할"),(2.55,4.6,"웹 포털(데스크톱)"),(7.15,3.1,"모바일"),(10.25,2.53,"주 사용")]:
text(s, x+0.15, 1.9, w-0.2, 0.44, t, size=10.5, color=WHITE, bold=True, anchor=MSO_ANCHOR.MIDDLE)
portals = [
("주최자", "주최자 콘솔 — 홀 배정·배치·옥션 발주·BI", "조회·승인·상황판", "M1·M2·M15·M16"),
("참가업체", "참가업체 포털 — 부스 신청·설계·유틸리티", "체크인·리드캡처", "M3·M4·M5·M10"),
("장치·공사업체", "업체 포털 — 도면 제출·설계 열람·옥션 응찰", "시공·반입·안전", "M3·M15·M8"),
("홀매니저·운영", "운영 대시보드 — 검수·규정 플래그·부하", "현장 검수·혼잡", "M2·M6·M14"),
("관리자", "백오피스 — 사용자·RBAC·마스터데이터·룰셋", "(웹 전용)", "M18"),
("관람객·대중", "공개 홍보 사이트 — 일정·사전등록", "배지·길찾기·매칭", "M12·M10·M13"),
]
ry = 2.42; rh = 0.62
for i,(r,web,mob,mod) in enumerate(portals):
y = ry + i*rh
if i%2==0: rect(s, 0.55, y, 12.23, rh, fill=RGBColor(0xFC,0xFC,0xFD))
rect(s, 0.55, y, 12.23, 0.01, fill=LINE)
text(s, 0.67, y, 1.9, rh, r, size=9.8, color=BLUE_DK, bold=True, anchor=MSO_ANCHOR.MIDDLE)
text(s, 2.7, y, 4.45, rh, web, size=9.2, color=INK, anchor=MSO_ANCHOR.MIDDLE)
text(s, 7.3, y, 2.95, rh, mob, size=9.2, color=MUTED, anchor=MSO_ANCHOR.MIDDLE)
text(s, 10.4, y, 2.4, rh, mod, size=8.8, color=PURPLE, bold=True, anchor=MSO_ANCHOR.MIDDLE)
text(s, 0.55, 6.35, 12.2, 0.4,
"접속 서브도메인으로 전시관(테넌트)이 고정됩니다 — 자신이 소속되지 않은 전시관의 데이터는 조회·검색되지 않습니다.",
size=9.5, color=MUTED)
s = slide(); content_header(s, "02 · 대시보드 · 작업환경", "마일스톤 대시보드와 MDI 탭", 2)
titled_card(s, 0.55, 1.5, 6.05, 3.3, "주최자 대시보드(SCR-02)", BLUE, [
("마일스톤 타임라인", "D-150 배정 → D-30 협의 → D-25 유틸리티 → D-7 서류 → D-0 개장"),
("완료·현재·임박", "녹색 체크 / 파란 진행 / 주황 펄스로 표시"),
("KPI 카드", "참가업체 신청·배치 확정률·유틸리티 완료·위반 미해결"),
("참가업체 현황표", "부스번호·설계 상태·유틸리티·예상 사진 썸네일"),
("알림 피드", "승인·반려·마감 이벤트 실시간"),
("홀 전경 조감", "S7 미니 이미지 카드(AI 생성)"),
], item_size=9.6, gap=0.44)
titled_card(s, 6.75, 1.5, 6.03, 3.3, "MDI 탭 작업환경(§2.7)", PURPLE, [
("여러 문서 동시", "부스 에디터·승인 큐·설계 스튜디오를 탭으로 병행"),
("상태 보존", "탭 전환해도 미저장 편집·스크롤·줌 유지"),
("미저장 표시", "탭에 ● 표시, 닫을 때 저장 확인"),
("탭 열기", "메뉴·링크 클릭 → 새 탭(같은 문서는 1개)"),
("단축키", "Ctrl+Tab 전환 · Ctrl+W 닫기 · Ctrl+1~9 이동"),
("모바일", "MDI 미적용 — 한 화면 집중(오조작 방지)"),
], item_size=9.6, gap=0.44)
banner(s, 5.0, "팁 좌측 사이드바에서 행사(이벤트)를 바꾸면 그 행사의 탭 세트로 전환됩니다. 미저장 탭이 있으면 먼저 확인합니다.",
fill=BLUE_DK, size=11, h=0.5)
text(s, 0.55, 5.7, 12.2, 0.9, [
[{"t":"화면 딥링크 ","size":10.5,"color":INK,"bold":True},
{"t":"알림·메일 링크를 클릭하면 해당 문서 탭이 바로 열립니다(예: 검수 상세·배치 에디터). ","size":10,"color":MUTED}],
[{"t":"세션 복원 ","size":10.5,"color":INK,"bold":True},
{"t":"새로고침·재접속 시 열려 있던 탭 목록이 복원됩니다(서버 미저장 편집 내용 자체는 복원되지 않음).","size":10,"color":MUTED}],
], line_spacing=1.15, space_after=6)
# ---- 03 핵심 업무 흐름 ----
divider("03", "핵심 업무 흐름", "CORE WORKFLOWS",
["부스 설계 3안·병합", "배선·신청·규정검증", "시각화 · 옥션 · 등록·승인"])
s = slide(); content_header(s, "03 · 부스 설계 스튜디오", "3안 생성 · 선택 · 병합 (M2·M3)", 3)
steps(s, 0.55, 1.5, 12.23, [
("① 조건 입력", "홀·목표부스·비율", BLUE),
("② AI 3안 생성", "1·2·3안 자동", PURPLE),
("③ 선택 / 병합", "구역·블록 조합", BLUE),
("④ 규정 재검증", "병합 후 필수", AMBER),
("⑤ 확정·버전", "부스번호 발급", GREEN),
])
titled_card(s, 0.55, 2.6, 6.05, 2.7, "배치·설계(SCR-03·04·06)", BLUE, [
("AI 자동배치", "보라 버튼 → 제약조건 풀이로 3안 생성"),
("배치안 비교", "S7 조감 이미지·지표(부스수·통로폭·위반) 비교"),
("선택 또는 병합", "1안 통로 + 2안 프리미엄존 + 3안 무대 조합"),
("위반 오버레이", "통로 미달 빨강 밴드·바닥하중 초과 주황 빗금"),
("부스 설계", "조립부스 옵션 선택 / 독립부스 3안 레이아웃"),
], item_size=9.6, gap=0.44)
titled_card(s, 6.75, 2.6, 6.03, 2.7, "규정 검증·검수(SCR-09~11)", PURPLE, [
("제출 전 자동검증", "높이 5m·리깅 6.5~8.5m·복층·방염 체크리스트"),
("규정 리포트", "차단/경고 요약 + 규정 근거·위치 보기"),
("승인 큐", "홀매니저 AI 사전심사 플래그로 검토"),
("검수 상세", "도면 위 위반 오버레이 확인 후 승인/반려"),
("최종 판정", "소방·구조 유권해석은 관할기관·구조기술사"),
], item_size=9.6, gap=0.44)
banner(s, 5.5, "중요 ‘병합’으로 제약이 깨질 수 있으므로, 병합 결과는 반드시 규정 검증을 다시 실행한 뒤 확정하세요.",
fill=AMBER, size=11.5, h=0.5)
s = slide(); content_header(s, "03 · 유틸리티 · 시각화", "배선 신청과 시각화 갤러리 (M4·M5)", 3)
titled_card(s, 0.55, 1.5, 6.05, 3.15, "유틸리티 배선·신청(SCR-07·08)", BLUE, [
("기기 목록 입력", "전시장비·조명·PC → kW 합산·분전반 자동 산출"),
("단말 위치 클릭", "부스 도면 위 클릭 → 트렌치 최단 배선 자동"),
("위치표시도 자동", "수기 작도 폐지 — 자동 생성"),
("자동 견적", "전기·급배수·압축공기·인터넷 요금 자동"),
("마감 리마인더", "D-25 역산 알림 — 인터넷은 현장 추가 불가"),
("배선 오버레이", "M5 이미지로 배선 확인"),
], item_size=9.5, gap=0.42)
titled_card(s, 6.75, 1.5, 6.03, 3.15, "시각화 갤러리(SCR-12)", PURPLE, [
("표준 샷 세트", "부스 정면·야간 조명·통로 뷰·홀 전경(S1~S7)"),
("Before/After", "설계 데이터 → 시공 후 예상 사진"),
("워터마크 상시", "모든 이미지에 AI 생성 예상 이미지’ 고지"),
("컨펌 자료", "참가업체·장치업체 합의용으로 공유"),
("생성 대기", "완료 시 실시간 푸시로 알림"),
], item_size=9.5, gap=0.42)
banner(s, 4.9, "AI 워터마크 고지 나노바나나 생성 이미지는 참고용 예상 이미지입니다 — 계약·심사·인허가 서류로 사용할 수 없습니다.",
fill=RED, size=11.5, h=0.55)
text(s, 0.55, 5.7, 12.2, 0.9, [
[{"t":"유틸리티 신청 팁 ","size":10.5,"color":INK,"bold":True},
{"t":"인터넷·전화·급배수·압축공기는 현장 추가신청이 어렵습니다. D-25 마감 알림을 놓치지 마세요. ","size":10,"color":MUTED}],
[{"t":"전기 용량 ","size":10.5,"color":INK,"bold":True},
{"t":"과소 신청 시 현장 증설이 불가할 수 있어, 기기 목록 기반 자동 산출값을 확인하고 신청하세요.","size":10,"color":MUTED}],
], line_spacing=1.15, space_after=6)
s = slide(); content_header(s, "03 · 공사 옥션 · 관람객", "옥션(견적·낙찰)과 관람객 등록 (M15·M10)", 3)
steps(s, 0.55, 1.45, 12.23, [
("① 옥션 개설", "설계자료 첨부", BLUE),
("② 응찰(견적서)", "업체 제출", PURPLE),
("③ 실시간 순위", "역경매", BLUE),
("④ 낙찰 비교", "동일사양 비교", AMBER),
("⑤ 선정·발주", "정산 연계", GREEN),
])
titled_card(s, 0.55, 2.5, 6.05, 2.75, "공사/장치 옥션(SCR-26~29)", BLUE, [
("옥션 목록·개설", "AI 설계·물량·시공 예상 이미지를 응찰 근거로"),
("응찰·순위(업체)", "견적서 작성·제출 → 실시간 순위 확인"),
("낙찰 비교(발주자)", "동일 사양 위 공정 비교로 선정"),
("등록업체 게이트", "킨텍스 등록업체만 초대·응찰 가능"),
("발주 종결", "낙찰 → M9 정산·결제로 연계"),
], item_size=9.6, gap=0.44)
titled_card(s, 6.75, 2.5, 6.03, 2.75, "관람객 등록·배지·리드(SCR-30~32)", PURPLE, [
("사전등록", "공개 사이트 폼(SCR-P4)에서 관람 신청"),
("모바일 배지/QR", "관람객 앱에서 배지 발급·체크인"),
("현장 체크인", "데스크/키오스크(SCR-31)로 입장"),
("리드캡처", "참가업체가 배지 스캔으로 리드 수집(M4)"),
("리드 스코어링", "AI가 리드 우선순위 제안(SCR-32)"),
], item_size=9.6, gap=0.44)
banner(s, 5.45, "승인 워크플로 제출물은 홀매니저 승인 큐 → 검수 상세 → 승인/반려를 거칩니다. 반려 시 사유와 위반 위치가 함께 전달됩니다.",
fill=BLUE_DK, size=11, h=0.5)
# ---- 04 모바일 ----
divider("04", "모바일 앱 사용법", "MOBILE APP",
["현장 체크리스트·검수", "시각화 갤러리", "오프라인·워터마크"])
s = slide(); content_header(s, "04 · 모바일 앱", "현장·조회·승인을 위한 모바일 앱", 4)
text(s, 0.55, 1.4, 12.2, 0.35,
"모바일 앱(Expo)은 조회·승인·현장 시나리오 전용입니다. 설계·배치 편집은 데스크톱 웹에서 수행합니다.",
size=10.5, color=MUTED)
titled_card(s, 0.55, 1.95, 3.95, 3.6, "현장(장치·홀매니저)", BLUE, [
("로그인", "2FA/OTP·아이디 기억·비번찾기"),
("현장 체크리스트", "48px 큰 터치·오프라인·사진 첨부(SCR-M1)"),
("현장 검수", "승인/반려·AI 플래그 확인(SCR-M2)"),
("반입 통행증", "QR 통행증·안전 체크(SCR-M10)"),
("서류·승인 조회", "이동 중 확인(SCR-M12)"),
], item_size=9.6, gap=0.42)
titled_card(s, 4.65, 1.95, 3.95, 3.6, "참가·관람", PURPLE, [
("리드캡처", "배지 스캔으로 방문객 리드 수집(SCR-M4)"),
("관람객 배지/QR", "모바일 배지·체크인(SCR-M5)"),
("길찾기", "실내 지도·wayfinding(SCR-M6)"),
("부스 검색", "플로어플랜·부스 찾기(SCR-M7)"),
("비즈매칭", "미팅 요청·일정(SCR-M8·M9)"),
], item_size=9.6, gap=0.42)
titled_card(s, 8.75, 1.95, 4.03, 3.6, "공통 · 유의", BLUE_DK, [
("홈/대시보드", "역할별 진입(SCR-M3)"),
("시각화 갤러리", "워터마크·고지 항상 표시"),
("알림센터", "마감·승인·반려 푸시(SCR-M11)"),
("오프라인", "현장 통신 불안정 시 임시 저장"),
("보안", "비밀번호 저장 안 함 — 이메일만 기억"),
], item_size=9.6, gap=0.42)
banner(s, 5.85, "설치 앱은 QR 코드 배포로 설치합니다(스토어 불필요). AI 생성 이미지는 앱에서도 워터마크·고지가 항상 표시됩니다.",
fill=BLUE, size=11, h=0.5)
# ---- 05 FAQ·문의 ----
divider("05", "FAQ · 문의", "FAQ & SUPPORT",
["자주 묻는 질문", "오류 대처", "지원 채널"])
s = slide(); content_header(s, "05 · FAQ", "자주 묻는 질문", 5)
faqs = [
("Q. 로그인 후 2차 인증코드를 못 받았어요.", "이메일 인증 방식은 스팸함을 확인하세요. OTP 방식은 인증 앱의 6자리 코드를 입력합니다. 기기 변경 시 마이페이지에서 재설정하거나 관리자에게 OTP 초기화를 요청하세요."),
("Q. AI가 만든 예상 사진을 계약서에 넣어도 되나요?", "안 됩니다. 모든 AI 이미지는 ‘예상 이미지’로 워터마크·고지가 붙으며 계약·인허가·심사 서류로 사용할 수 없습니다."),
("Q. 3안을 합쳐서 쓰고 싶어요.", "배치안 비교에서 여러 안의 구역/블록을 골라 병합할 수 있습니다. 병합 후에는 반드시 규정 검증을 다시 실행한 뒤 확정하세요."),
("Q. 유틸리티 신청 마감을 놓치면?", "인터넷·급배수 등은 현장 추가신청이 어렵습니다. D-25 역산 알림을 활용하고, 부득이한 경우 홀매니저/사무국에 문의하세요."),
("Q. 우리 장치업체를 옥션에 초대할 수 없어요.", "킨텍스 등록업체만 초대·응찰할 수 있습니다. 미등록 업체는 등록 절차 후 이용 가능합니다."),
("Q. 탭을 닫았는데 편집 내용이 사라졌어요.", "탭 세션은 복원되지만 서버에 저장하지 않은 편집 내용 자체는 복원되지 않습니다. 중요한 작업은 저장 후 닫으세요."),
]
for i,(q,a) in enumerate(faqs):
x = 0.55 + (i%2)*6.2
y = 1.55 + (i//2)*1.62
card(s, x, y, 6.05, 1.5)
rect(s, x, y, 0.08, 1.5, fill=(BLUE if i%2==0 else PURPLE), shape=MSO_SHAPE.ROUNDED_RECTANGLE)
text(s, x+0.24, y+0.14, 5.65, 0.5, q, size=10.5, color=INK, bold=True, line_spacing=0.95)
text(s, x+0.24, y+0.66, 5.65, 0.78, a, size=9, color=MUTED, line_spacing=0.98)
s = slide(); content_header(s, "05 · 문의 · 지원", "지원 채널 및 도움말", 5)
titled_card(s, 0.55, 1.6, 6.05, 2.7, "이런 순서로 확인하세요", BLUE, [
"① 화면 안내(툴팁)·본 사용자지침서 해당 절 확인",
"② 오류 배너의 요약 메시지·재시도 버튼 사용",
"③ 마감·승인 관련은 알림센터에서 이력 확인",
"④ 권한·계정 문제는 관리자(테넌트 관리자)에게",
"⑤ 규정·현장 문제는 홀매니저·킨텍스 운영팀에",
], item_size=10, gap=0.44)
titled_card(s, 6.75, 1.6, 6.03, 2.7, "역할별 지원 접점", PURPLE, [
("참가업체", "설계·유틸리티 → 주최자 사무국"),
("장치·공사업체", "등록·옥션 → 킨텍스 등록업체 담당"),
("주최자", "배정·정산 → 킨텍스 행사지원팀"),
("관람객", "등록·배지 → 공개 사이트 문의(SCR-P6)"),
("공통", "계정·권한 → 시스템 관리자"),
], item_size=10, gap=0.44)
banner(s, 4.55, "오류 메시지는 원인 요약만 표시됩니다(내부 정보 비노출). 반복되면 발생 시각·화면명·행사명을 함께 전달하면 빠르게 처리됩니다.",
fill=BLUE_DK, size=11, h=0.6)
text(s, 0.55, 5.5, 12.2, 0.8,
"본 지침서의 화면 번호(SCR-*)는 설계 문서 docs/design.md의 화면 인벤토리(총 80화면)를 기준으로 합니다. 화면 구성은 업데이트로 달라질 수 있습니다.",
size=9.5, color=MUTED, line_spacing=1.1)
return PRS
# =====================================================================
# 2) 운영자지침서
# =====================================================================
def build_ops():
global PRS, BLANK, DOC_FOOTER
PRS = Presentation(); PRS.slide_width = EMU_W; PRS.slide_height = EMU_H
BLANK = PRS.slide_layouts[6]
DOC_FOOTER = "킨텍스 자동전시시스템 · 운영자지침서"
cover("운영자지침서", "Operations Guide",
["킨텍스 자동전시시스템", "운영자 지침서"],
"킨텍스 운영·관리자를 위한 운영·백오피스·장애 대응 안내서",
"아키텍처 → 백오피스 → 배포 → 모니터링 → 백업·보안 → 정기 점검",
[("대상", "테넌트(전시관) 관리자 · 플랫폼 슈퍼관리자 · 운영 담당"),
("근거", "OPS_RUNBOOK · SECURITY · BUILD_DEPLOY 준용")])
toc([
("01", "운영 아키텍처 개요", "서비스 토폴로지 / 의존 관계"),
("02", "관리자 백오피스", "사용자·RBAC·감사·설정 / 마스터·룰셋·테넌트·AiConfig"),
("03", "배포 · CI/CD 운영", "push→자동배포 / health 게이트 / 롤백"),
("04", "모니터링 · 장애 대응", "헬스체크·로그·재기동 / 자주 겪는 이슈"),
("05", "백업 · 보안 점검", "보안 불변 / 워터마크 강제 / 자격증명 보호"),
("06", "정기 점검 체크리스트", "일 · 주 · 월 점검표"),
])
# ---- 01 아키텍처 ----
divider("01", "운영 아키텍처 개요", "OPERATIONS ARCHITECTURE",
["백엔드·워커·프론트", "PostgreSQL·Redis", "webhook 자동배포"])
s = slide(); content_header(s, "01 · 운영 아키텍처", "서비스 토폴로지 (개발 kintex.zioinfo.co.kr)", 1)
def node(x,y,w,h,title,sub,fill,tcol,line=None):
rect(s, x, y, w, h, fill=fill, line=(line or LINE), line_w=1.0, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
text(s, x, y+0.1, w, 0.3, title, size=11, color=tcol, bold=True, align=PP_ALIGN.CENTER)
if sub: text(s, x, y+0.44, w, 0.28, sub, size=8.4, color=MUTED, align=PP_ALIGN.CENTER)
# 프론트/nginx
rect(s, 0.55, 1.55, 12.23, 0.44, fill=BLUE_LT, line=BLUE, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
text(s, 0.7, 1.55, 6, 0.44, "접속 계층 — nginx (kintex.service 도메인 :80/443)", size=10.5, color=BLUE_DK, bold=True, anchor=MSO_ANCHOR.MIDDLE)
node(0.55, 2.15, 3.9, 0.8, "프론트(웹 정적)", "역할별 번들 · SPA 폴백", WHITE, INK, BLUE)
node(4.65, 2.15, 3.9, 0.8, "백엔드 API", "kintex.service · REST + WebSocket", WHITE, INK, BLUE)
node(8.75, 2.15, 4.03, 0.8, "나노바나나 워커", "kintex-nanobanana.service · 큐 소비", PURPLE_LT, PURPLE, PURPLE)
# 데이터
rect(s, 0.55, 3.2, 12.23, 0.44, fill=HALL_BG, line=BLUE_DK, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
text(s, 0.7, 3.2, 6, 0.44, "데이터·큐 계층", size=10.5, color=BLUE_DK, bold=True, anchor=MSO_ANCHOR.MIDDLE)
node(0.55, 3.8, 3.9, 0.8, "PostgreSQL + PostGIS", "kintex_db · 공간 데이터", WHITE, INK, BLUE_DK)
node(4.65, 3.8, 3.9, 0.8, "Redis", "작업 큐·세션·캐시", WHITE, INK, BLUE_DK)
node(8.75, 3.8, 4.03, 0.8, "오브젝트 스토리지", "렌더 이미지·문서", WHITE, INK, BLUE_DK)
# 배포
rect(s, 0.55, 4.85, 12.23, 0.44, fill=PURPLE_LT, line=PURPLE, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
text(s, 0.7, 4.85, 6, 0.44, "배포·연동", size=10.5, color=PURPLE, bold=True, anchor=MSO_ANCHOR.MIDDLE)
node(0.55, 5.45, 3.9, 0.8, "deploy_server (webhook)", "Gitea(zio/kintex) push 수신", WHITE, INK, PURPLE)
node(4.65, 5.45, 3.9, 0.8, "Gemini (G1)", "워커 env only · 미승인시 degraded", WHITE, INK, PURPLE)
node(8.75, 5.45, 4.03, 0.8, "헬스 게이트", "GET /health → data.status=UP", WHITE, INK, PURPLE)
text(s, 0.55, 6.45, 12.2, 0.4,
"의존 백엔드 → PostgreSQL·Redis · 워커 → Redis·Gemini(G1) · 프론트 → nginx → 백엔드. 헬스 판정 = /health UP + 두 서비스 active + Redis PONG + DB 응답.",
size=9.3, color=MUTED)
# ---- 02 백오피스 ----
divider("02", "관리자 백오피스", "ADMIN BACK-OFFICE",
["사용자·RBAC·감사·설정", "마스터데이터·룰셋", "테넌트 온보딩·AiConfig"])
s = slide(); content_header(s, "02 · 관리자 백오피스", "시스템관리 영역 (M18 · §5B)", 2)
text(s, 0.55, 1.4, 12.2, 0.35,
"백오피스(admin.)는 웹 전용입니다. 테넌트 관리자는 자기 전시관에 고정되고, 플랫폼 슈퍼관리자는 테넌트 스위처로 전 전시관을 통제합니다.",
size=10.5, color=MUTED)
titled_card(s, 0.55, 1.95, 3.95, 3.9, "시스템관리 (SCR-A1~A6)", BLUE, [
("사용자 관리", "6역할 + 등록업체·관람객 계정(SCR-A1)"),
("역할·권한 RBAC", "플랫폼·행사 이중 평가(SCR-A2)"),
("공통코드", "홀·부스유형·공종14분류·요금(SCR-A3)"),
("메뉴 관리", "역할별 포털 IA 구동(SCR-A4)"),
("감사로그", "승인·낙찰·룰셋개정·리드접근(SCR-A5)"),
("시스템설정", "요율/규정 룰셋 버전(SCR-A6)"),
("잠금·OTP 초기화", "실패 잠금 해제·OTP 재설정"),
], item_size=9.4, gap=0.42)
titled_card(s, 4.65, 1.95, 3.95, 3.9, "마스터·룰셋 (SCR-A7·A8)", PURPLE, [
("마스터데이터", "홀·요율·요금·부스표준·등록업체(SCR-A7)"),
("규정 룰셋 버전", "높이·통로·바닥하중 규칙 개정(SCR-A8)"),
("룰셋은 데이터", "코드가 아닌 버전 파일로 관리"),
("개정 추적", "리포트에 rulesetVersion·고지 기록"),
("전시관 소유", "홀·요율·규정은 각 전시관이 소유"),
("근거 없는 추정 금지", "미확보 실측값은 온보딩 시 입력"),
], item_size=9.4, gap=0.42)
titled_card(s, 8.75, 1.95, 4.03, 3.9, "테넌트·AI (SCR-A9·A10)", BLUE_DK, [
("테넌트 관리", "전시관 마스터·온보딩(SCR-A9)"),
("테넌트 격리", "tenant_id로 전 데이터 스코프"),
("서브도메인", "kintex/coex 등 컨텍스트 해소"),
("KINTEX = 테넌트#1", "기존 데이터 백필 기준"),
("AiConfig", "AI 플랫폼·모델 설정(SCR-A10)"),
("Claude 기본", "실패 시 Ollama 자동 폴백"),
("크로스 집계", "전 테넌트 BI는 슈퍼관리자만"),
], item_size=9.4, gap=0.42)
# ---- 03 배포 ----
divider("03", "배포 · CI/CD 운영", "DEPLOY & CI/CD",
["push → 자동배포", "health 게이트", "자동·수동 롤백"])
s = slide(); content_header(s, "03 · 배포 · CI/CD", "push → 자동배포 → 헬스 게이트", 3)
steps(s, 0.55, 1.5, 12.23, [
("① push", "scripts/push_kintex.py", BLUE),
("② webhook", "Gitea #47", PURPLE),
("③ 빌드", "bootJar · vite build", BLUE),
("④ 원자 교체", "deploy_kintex.sh", AMBER),
("⑤ health 게이트", "GET /health", GREEN),
])
titled_card(s, 0.55, 2.6, 6.05, 2.7, "정상 배포 경로(자동)", BLUE, [
("git pull", "webhook 수신 후 소스 갱신"),
("Flyway 마이그", "스키마 전진 적용(전진만 권장)"),
("빌드", "프론트 vite build · 백엔드 gradlew bootJar"),
("jar 검증", "KintexApplication.class 존재 확인"),
("원자 교체", "무중단 교체 후 헬스체크"),
("검증", "kintex_smoke_test.py로 라우터 등록 확인"),
], item_size=9.6, gap=0.44)
titled_card(s, 6.75, 2.6, 6.03, 2.7, "금지·주의", RED, [
("동시 빌드 금지", "수동 Jenkins 추가 트리거 금지(jar 교체 깨짐)"),
("서버빌드 신뢰", "로컬 빌드 이슈보다 서버 빌드 결과 신뢰"),
("deploy_server 반영", "kintex 블록 서버 반영·zioinfo-deploy 재시작"),
("‘완료’ 오인", "로그 완료여도 1ms no-op면 블록 부재 의심"),
("빌드 OOM", "공유 8GB — 타 솔루션과 직렬 빌드"),
("운영 배포", "소유자 승인 필수"),
], item_size=9.6, gap=0.44)
banner(s, 5.5, "롤백 deploy_kintex.sh는 헬스 실패 시 이전 jar를 자동 유지·복원합니다. 자동 실패 시 백업 jar 복원 또는 직전 정상 커밋으로 재배포합니다.",
fill=BLUE_DK, size=11, h=0.5)
# ---- 04 모니터링·장애 ----
divider("04", "모니터링 · 장애 대응", "MONITORING & INCIDENT",
["헬스체크 우선", "로그·재기동", "자주 겪는 이슈"])
s = slide(); content_header(s, "04 · 모니터링 · 장애", "헬스체크 · 로그 · 재기동 (런북 요약)", 4)
titled_card(s, 0.55, 1.5, 6.05, 2.55, "점검 순서", BLUE, [
("① 헬스체크", "kintex_smoke_test.py(자동) / /health(수동)"),
("② 서비스 상태", "kintex.service·kintex-nanobanana.service active"),
("③ 의존", "Redis PONG · DB SELECT 1"),
("④ 로그", "journalctl -u kintex.service -n 200"),
("⑤ 재기동", "systemctl restart → /health 재확인"),
], item_size=9.8, gap=0.46)
titled_card(s, 6.75, 1.5, 6.03, 2.55, "재기동 원칙", PURPLE, [
("백엔드", "restart 후 5초 뒤 /health 확인"),
("워커", "이미지 멈춤·큐 적체 시 워커만 재기동"),
("nginx", "정적 이상 시 nginx -t 후 reload"),
("DB/Redis", "공유 인스턴스 — 타 솔루션 영향 확인 후"),
("자동기동", "두 유닛 enable 상태 유지"),
], item_size=9.8, gap=0.46)
# 이슈 표
text(s, 0.55, 4.25, 6, 0.3, "자주 겪는 이슈", size=12, color=INK, bold=True)
rect(s, 0.55, 4.62, 12.23, 0.4, fill=BLUE, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
for x,w,t in [(0.55,4.2,"증상"),(4.75,3.6,"원인 후보"),(8.35,4.43,"조치")]:
text(s, x+0.12, 4.62, w-0.15, 0.4, t, size=10, color=WHITE, bold=True, anchor=MSO_ANCHOR.MIDDLE)
issues = [
("/health 200인데 특정 API 404", "라우터 미배포(구 jar)", "스모크 러너로 라우터 확인 → 재배포"),
("500 relation ... does not exist", "마이그 미적용", "flyway info 확인, 보정 마이그(다음 번호) 추가"),
("이미지 생성 안 됨 / degraded", "G1 미승인·워커 다운", "워커 로그·env 확인, 워커 재기동(키는 워커 env only)"),
("배포 ‘완료’인데 반영 안 됨", "deploy_server 블록 부재", "서버 kintex 블록 반영·zioinfo-deploy 재시작"),
("push 했는데 자동배포 안 됨", "webhook 미설정/시크릿 불일치", "Gitea webhook #47 활성·secret·URL 확인"),
]
ry=5.02; rh=0.4
for i,(sym,cause,act) in enumerate(issues):
y=ry+i*rh
if i%2==0: rect(s, 0.55, y, 12.23, rh, fill=RGBColor(0xFC,0xFC,0xFD))
rect(s, 0.55, y, 12.23, 0.008, fill=LINE)
text(s, 0.67, y, 4.1, rh, sym, size=8.6, color=INK, anchor=MSO_ANCHOR.MIDDLE)
text(s, 4.87, y, 3.5, rh, cause, size=8.6, color=MUTED, anchor=MSO_ANCHOR.MIDDLE)
text(s, 8.35, y, 4.4, rh, act, size=8.6, color=BLUE_DK, anchor=MSO_ANCHOR.MIDDLE)
# ---- 05 백업·보안 ----
divider("05", "백업 · 보안 점검", "BACKUP & SECURITY",
["보안 불변 원칙", "AI 워터마크 강제", "자격증명 보호"])
s = slide(); content_header(s, "05 · 백업 · 보안 점검", "보안 불변 및 배포·리뷰 전 점검", 5)
titled_card(s, 0.55, 1.5, 6.05, 3.35, "보안 불변(위반 불가)", BLUE_DK, [
("외부 API 통제", "온프레미스 우선 · Claude·Gemini만 예외"),
("키는 env only", "코드·DB·커밋·로그·응답 기록 금지"),
("Gemini는 워커만", "백엔드는 키 미취급(큐 발행·콜백만)"),
("자격증명 보호", "IP·SSH·비번·해시 응답/로그 미노출"),
("암호화 저장", "admin 비번 env 주입(AES-256-GCM)·하드코딩 금지"),
("스택트레이스 차단", "오류는 요약 메시지만 — 테이블/컬럼명 미노출"),
("AI 워터마크", "M5 이미지 응답 watermarkRequired 항상 포함"),
], item_size=9.4, gap=0.4)
titled_card(s, 6.75, 1.5, 6.03, 3.35, "배포·리뷰 전 체크리스트", PURPLE, [
"응답/로그/커밋에 IP·SSH·비번·해시·API키 노출 0(grep)",
"Gemini/Claude 키 env only — 코드·DB·커밋 미포함",
"admin 비번 하드코딩 시드 없음(env 재시드)",
"M5 이미지 응답 watermarkRequired:true 포함",
"DB/서버 오류 → 요약 메시지(스택·relation 미노출)",
"운영 스크립트 시크릿 하드코딩 0(마스킹)",
"/api/admin/** ADMIN 게이트·미등록업체 차단 동작",
], item_size=9.4, gap=0.4)
banner(s, 5.05, "백업·스키마 Flyway는 전진(forward)만 — 잘못된 마이그는 되돌리지 말고 보정 마이그를 추가합니다. 운영 DB 임의 롤백 금지.",
fill=RED, size=11, h=0.55)
text(s, 0.55, 5.8, 12.2, 0.8, [
[{"t":"로그에 자격증명·API키가 보이면 ","size":10.5,"color":INK,"bold":True},
{"t":"즉시 보안 이슈로 에스컬레이션합니다(SECURITY §2). 감사로그에도 비밀값을 기재하지 않습니다.","size":10,"color":MUTED}],
], line_spacing=1.1, space_after=4)
# ---- 06 정기 점검 ----
divider("06", "정기 점검 체크리스트", "ROUTINE CHECKLIST",
["일간 점검", "주간 점검", "월간 점검"])
s = slide(); content_header(s, "06 · 정기 점검", "일 · 주 · 월 점검 체크리스트", 6)
titled_card(s, 0.55, 1.5, 3.95, 4.6, "일간 (Daily)", BLUE, [
"스모크 러너 실행·전체 통과 확인",
"/health UP · 두 서비스 active",
"Redis PONG · DB SELECT 1",
"워커 큐 소비·이미지 생성 정상",
"에러 로그 급증·5xx 여부 확인",
"마감 임박 알림(D-25·D-7) 발송 상태",
"감사로그 이상 접근 스캔",
], item_size=9.6, gap=0.44)
titled_card(s, 4.65, 1.5, 3.95, 4.6, "주간 (Weekly)", PURPLE, [
"배포 이력·롤백 발생 여부 검토",
"Flyway 마이그 상태(info) 확인",
"디스크·렌더 스토리지 용량 점검",
"백업 정상 수행·복구 가능성 확인",
"OTP·로그인 실패 잠금 문의 처리",
"룰셋/요율 개정 요청 반영 검토",
"webhook·시크릿 정합 점검",
], item_size=9.6, gap=0.44)
titled_card(s, 8.75, 1.5, 4.03, 4.6, "월간 (Monthly)", BLUE_DK, [
"보안 불변 체크리스트 전수 점검(grep)",
"권한(RBAC)·계정 접근 재검토",
"감사로그 보존·이상 패턴 분석",
"테넌트 격리·크로스 접근 점검",
"AiConfig·모델 폴백 동작 확인",
"의존 서비스(공유 DB/Redis) 용량 추이",
"장애·변경 이력 회고 및 런북 갱신",
], item_size=9.6, gap=0.44)
text(s, 0.55, 6.25, 12.2, 0.4,
"점검 결과는 운영 기록으로 남기고, 이상 발견 시 OPS_RUNBOOK 절차(헬스→로그→재기동→롤백)로 대응합니다. 운영 배포·재구성은 소유자 승인이 필요합니다.",
size=9.3, color=MUTED)
return PRS
# =====================================================================
# 3) 개발자지침서
# =====================================================================
def build_dev():
global PRS, BLANK, DOC_FOOTER
PRS = Presentation(); PRS.slide_width = EMU_W; PRS.slide_height = EMU_H
BLANK = PRS.slide_layouts[6]
DOC_FOOTER = "킨텍스 자동전시시스템 · 개발자지침서"
cover("개발자지침서", "Developer Guide",
["킨텍스 자동전시시스템", "개발자 지침서"],
"개발 참여자를 위한 환경·표준·빌드·보안 가이드",
"환경 구축 → 스택·구조 → 개발 표준 → 빌드·배포 → 하네스 → 보안 코딩",
[("대상", "백엔드 · 프론트 · 모바일 · 워커 개발 참여자"),
("스택", "React(Vite)+TS · Spring Boot 3.x(Java17)+MyBatis · PostgreSQL(PostGIS) · Redis")])
toc([
("01", "개발환경 구축", "clone · env(키 이름) · 로컬 실행"),
("02", "기술 스택 · 구조", "확정 스택 / backend·frontend·mobile·nanobanana"),
("03", "개발 표준", "레이어링·MyBatis·PostGIS / API 봉투·오류코드·토큰"),
("04", "빌드 · 테스트 · 배포", "게이트 / 테스트 / CI/CD 함정"),
("05", "하네스 · 에이전트 워크플로", "planner→designer→dev→qa→devops"),
("06", "보안 코딩 규칙", "보안 불변 · 워터마크 · 자격증명 보호"),
])
# ---- 01 환경 ----
divider("01", "개발환경 구축", "DEV ENVIRONMENT",
["도구·clone", "DB·Redis·워커", "env 키 이름만"])
s = slide(); content_header(s, "01 · 개발환경 구축", "사전 도구 · clone · 로컬 실행", 1)
titled_card(s, 0.55, 1.5, 6.05, 3.0, "사전 요구 도구", BLUE, [
("JDK 17 (LTS)", "Spring Boot 3.x 빌드/실행(gradlew)"),
("Node.js 18+", "Vite 빌드(Node 16 불가 — WISE 함정)"),
("PostgreSQL 15+/16", "kintex_db"),
("PostGIS 3.x", "부스 polygon·트렌치 point·배선 LineString"),
("Redis 6+/7", "작업 큐(RenderJob·서류·알림)"),
("Python 3.11+", "나노바나나 워커 사이드카"),
], item_size=9.6, gap=0.42)
titled_card(s, 6.75, 1.5, 6.03, 3.0, "초기 세팅 흐름", PURPLE, [
("clone", "git clone <gitea>/zio/kintex.git"),
("백엔드", "src/backend → ./gradlew build"),
("프론트", "src/frontend → npm install"),
("워커", "tools/nanobanana → pip install google-genai Pillow"),
("DB 준비", "kintex_db + CREATE EXTENSION postgis"),
("스키마", "Flyway 순번 마이그레이션으로 적용"),
], item_size=9.6, gap=0.42)
banner(s, 4.7, "환경변수는 이름(키)만 규약입니다 — 실제 비밀값·포트는 저장소에 두지 않고 사내 비밀관리·env에서 주입합니다.",
fill=BLUE_DK, size=11, h=0.5)
text(s, 0.55, 5.35, 12.2, 1.05, [
[{"t":"백엔드 env 키(이름만) ","size":10,"color":INK,"bold":True},
{"t":"KINTEX_DB_PASSWORD · KINTEX_JWT_SECRET · REDIS_HOST/REDIS_PORT · RENDER_WORKER_TOKEN · ANTHROPIC_API_KEY · ADMIN_PASSWORD_ENC/ADMIN_KEY_FILE · SMTP_*","size":9.3,"color":MUTED}],
[{"t":"워커 env 키(이름만) ","size":10,"color":INK,"bold":True},
{"t":"GEMINI_API_KEY(워커 전용·G1 승인 대상) · NANOBANANA_MODEL(기본 gemini-3.1-flash-image-preview). 값은 코드·커밋·로그에 기록 금지.","size":9.3,"color":MUTED}],
[{"t":"원격 DB ","size":10,"color":INK,"bold":True},
{"t":"5432 외부 차단 시 SSH 로컬 포워딩으로 jdbc:postgresql://localhost:5432/kintex_db 접속.","size":9.3,"color":MUTED}],
], line_spacing=1.12, space_after=5)
# ---- 02 스택·구조 ----
divider("02", "기술 스택 · 구조", "STACK & STRUCTURE",
["확정 스택(변경 금지)", "패키지 레이어", "프론트·모바일·워커"])
s = slide(); content_header(s, "02 · 기술 스택", "확정 스택 (변경 금지)", 2)
rect(s, 0.55, 1.55, 12.23, 0.44, fill=BLUE, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
for x,w,t in [(0.55,3.0,"레이어"),(3.55,9.23,"표준")]:
text(s, x+0.15, 1.55, w-0.2, 0.44, t, size=10.5, color=WHITE, bold=True, anchor=MSO_ANCHOR.MIDDLE)
stack = [
("프론트(웹)", "React 18/19 + Vite + TypeScript — 역할별 번들 분리(§2-1)"),
("백엔드", "Spring Boot 3.x(Java 17) + MyBatis — REST + WebSocket(STOMP)"),
("DB", "PostgreSQL + PostGIS(부스 polygon·트렌치 point·배선 LineString)"),
("비동기", "Redis 작업 큐(RenderJob·서류·알림)"),
("이미지 생성", "나노바나나 Python 워커 사이드카(tools/nanobanana, google-genai)"),
("AI(텍스트)", "Claude 기본 + 설정형 전환(AiTextRouter/AiConfig) — 실패 시 Ollama 폴백"),
("인증", "행사 단위 RBAC(JWT HS256) + 2차 인증(OTP/TOTP)"),
]
ry=2.05; rh=0.52
for i,(k,v) in enumerate(stack):
y=ry+i*rh
if i%2==0: rect(s, 0.55, y, 12.23, rh, fill=RGBColor(0xFC,0xFC,0xFD))
rect(s, 0.55, y, 12.23, 0.008, fill=LINE)
text(s, 0.7, y, 2.9, rh, k, size=10, color=BLUE_DK, bold=True, anchor=MSO_ANCHOR.MIDDLE)
text(s, 3.6, y, 9.1, rh, v, size=9.6, color=INK, anchor=MSO_ANCHOR.MIDDLE)
banner(s, 5.8, "패키지 루트 com.zioinfo.kintex · DB kintex_db · 나노바나나 호출은 tools/nanobanana 워커만(백엔드는 GEMINI_API_KEY 미취급).",
fill=BLUE_DK, size=10.5, h=0.5)
s = slide(); content_header(s, "02 · 프로젝트 구조", "백엔드 레이어 · 프론트 · 모바일 · 워커", 2)
titled_card(s, 0.55, 1.5, 6.05, 4.0, "백엔드 (com.zioinfo.kintex)", BLUE, [
("config", "Security·Jwt·WebSocket·Redis·@MapperScan"),
("security", "JwtTokenProvider·Filter·KintexPrincipal"),
("common", "response(ApiResponse·PageResponse)·exception·audit"),
("auth", "AuthService·TotpService·mapper·dto"),
("module/m2~m5", "플로어플랜·부스설계·배선·나노바나나"),
("module/m10·m15·m16·m18", "도메인(Phase D)"),
("system", "사용자·권한·공통코드·메뉴·감사·설정"),
("work", "worklog·schedule·message·stats·notice…"),
], item_size=9.2, gap=0.4)
titled_card(s, 6.75, 1.5, 6.03, 4.0, "프론트 · 모바일 · 워커", PURPLE, [
("src/frontend", "pages·components·api·store·hooks·routes"),
("역할별 번들", "organizer·exhibitor·contractor·ops·admin·public"),
("공유 상속", "디자인 시스템·공통 컴포넌트·API 계약 상속"),
("axios", "baseURL=/api · 응답 타입 = 계약 shape 1:1"),
("mobile/", "Expo SDK 51·expo-router·RN 0.74·TS"),
("모바일 저장", "JWT는 expo-secure-store(웹 AsyncStorage)"),
("tools/nanobanana", "큐 소비 데몬 → 콜백(RENDER_WORKER_TOKEN)"),
("룰셋은 데이터", "rulesets/*.json 버전 파일(코드 아님)"),
], item_size=9.2, gap=0.4)
# ---- 03 개발 표준 ----
divider("03", "개발 표준", "DEVELOPMENT STANDARDS",
["레이어링·MyBatis·PostGIS", "API 봉투·오류코드", "디자인 토큰·네이밍"])
s = slide(); content_header(s, "03 · 개발 표준", "레이어링 · API 계약 · 코딩 규약", 3)
titled_card(s, 0.55, 1.5, 6.05, 3.4, "백엔드 레이어링·MyBatis", BLUE, [
("계층 규칙", "controller(검증·RBAC) → service(트랜잭션·룰) → mapper"),
("컨트롤러", "도메인 로직 금지"),
("매퍼", "비즈니스 판단 금지 · @Mapper + XML"),
("PostGIS", "ST_MakePolygon·ST_Area·ST_Distance·<-> KNN은 XML"),
("룰셋", "규정·요율은 버전 파일 · rulesetVersion·disclaimer 기록"),
("네이밍", "Java camel/Pascal · DB SNAKE_CASE · DTO camelCase"),
("마이그레이션", "Flyway 순번 · 번호 충돌 금지(최대+1)"),
], item_size=9.3, gap=0.4)
titled_card(s, 6.75, 1.5, 6.03, 3.4, "API·응답 규약", PURPLE, [
("응답 봉투", "ApiResponse<T> = { success, data, error }"),
("목록", "PageResponse<T> = { items, page, size, total }"),
("오류코드", "VALIDATION 400·FORBIDDEN 403·COMPLIANCE_BLOCKED 422"),
("미구현", "NOT_IMPLEMENTED 501"),
("스코프", "도메인 경로는 {eventId} + 행사 RBAC 가드"),
("프론트 타입", "TS strict · 응답 타입 = 계약 shape 1:1 · any 지양"),
("코드그룹", "boothType·myRole·severity는 COMMON_CODES 단일 출처"),
], item_size=9.3, gap=0.4)
banner(s, 5.1, "디자인 토큰 색·타이포·간격은 design.md §1 토큰만 사용(신규 토큰 추가 금지). 데이터 시각화는 CSS 변수 주입(하드코딩 hex 금지).",
fill=BLUE_DK, size=11, h=0.5)
text(s, 0.55, 5.8, 12.2, 0.7,
"인증 1차 로그인(ID/PW) → verifyToken → 2차 검증(EMAIL/OTP TOTP RFC6238) → access·refresh. 로그인 실패 잠금 + 관리자 해제. /api/system·/api/admin = hasRole(ADMIN).",
size=9.5, color=MUTED, line_spacing=1.1)
# ---- 04 빌드·테스트·배포 ----
divider("04", "빌드 · 테스트 · 배포", "BUILD · TEST · DEPLOY",
["push 전 게이트", "단위·경계면·보안 회귀", "CI/CD 함정"])
s = slide(); content_header(s, "04 · 빌드 · 테스트 · 배포", "게이트 · 테스트 · 파이프라인", 4)
steps(s, 0.55, 1.5, 12.23, [
("① compile/tsc", "변경분 통과", BLUE),
("② 시크릿 차단", ".env·*.key 금지", PURPLE),
("③ 마이그 번호", "충돌 검사", BLUE),
("④ push", "= 배포 트리거", AMBER),
("⑤ health 게이트", "실패 시 롤백", GREEN),
])
titled_card(s, 0.55, 2.6, 6.05, 2.75, "테스트", BLUE, [
("백엔드 단위", "룰 엔진(규정 평가·요율 산식·배선 최단경로)"),
("공간 쿼리", "PostGIS 통합(testcontainers/로컬 PostGIS)"),
("경계면(계약)", "API 응답 shape ↔ 프론트 훅/컴포넌트 교차 대조"),
("보안 회귀", "자격증명·PII·스택트레이스·워터마크·응찰 가드"),
("워커", "키/네트워크 없이도 import·구조 성립(목/degraded)"),
], item_size=9.5, gap=0.44)
titled_card(s, 6.75, 2.6, 6.03, 2.75, "빌드·배포 함정", RED, [
("Node 16 금지", "vite build 불가 — Node 18+ 사용"),
("서버빌드 신뢰", "로컬 rollup/win32 크래시는 서버 빌드 신뢰"),
("동시 빌드 금지", "target/*.jar 교체 깨짐 — 직렬 빌드"),
("자기방어 스크립트", "jar 검증 → 원자 교체 → 헬스 → 실패 시 롤백"),
("커밋 시점", "지시가 있을 때만 · 운영 배포 소유자 승인"),
], item_size=9.5, gap=0.44)
banner(s, 5.6, "Flyway는 전진만 — 잘못된 마이그는 되돌리지 말고 보정 마이그(다음 번호)를 추가합니다. baseline-on-migrate=true.",
fill=AMBER, size=11, h=0.5)
# ---- 05 하네스 ----
divider("05", "하네스 · 에이전트 워크플로", "HARNESS & AGENTS",
["기획→디자인→구현", "점진 QA", "배포 오케스트레이션"])
s = slide(); content_header(s, "05 · 하네스 · 에이전트", "에이전트 파이프라인 개요", 5)
steps(s, 0.55, 1.55, 12.23, [
("기획", "planner", PURPLE),
("설계", "AA/SA/TA/DA/NA", BLUE),
("디자인", "designer", PURPLE),
("구현", "backend·front·db·domain", BLUE),
("검증", "kintex-qa", GREEN),
("배포", "devops-dev", BLUE_DK),
])
titled_card(s, 0.55, 2.65, 6.05, 2.7, "문서 소유권·경유", BLUE, [
("PLANNING.md", "범위 권위 — planner만 수정"),
("design.md", "화면 스펙 — designer 경유"),
("src/", "PLANNING·design 근거로 developer 구현"),
("나노바나나", "tools/nanobanana만 경유(visualizer)"),
("오케스트레이터", "kintex-impl / kintex-mobile 스킬로 조율"),
], item_size=9.6, gap=0.44)
titled_card(s, 6.75, 2.65, 6.03, 2.7, "품질 게이트(점진 QA)", PURPLE, [
("모듈 완성 직후", "경계면·공간 로직·보안 불변·빌드 회귀"),
("경계면", "백엔드 shape ↔ 프론트 호출 교차 비교"),
("보안 불변", "자격증명·PII·스택트레이스 미노출 강제"),
("AI 워터마크", "이미지 워터마크·응찰 게이트 검증"),
("반려 기준", "compileJava·tsc 통과까지 반려"),
], item_size=9.6, gap=0.44)
banner(s, 5.6, "브랜치 main 보호 · Conventional Commits(feat/fix/docs…) · 코드 식별자·커밋 제목은 영어, 설명은 한국어 허용.",
fill=BLUE_DK, size=11, h=0.5)
# ---- 06 보안 코딩 ----
divider("06", "보안 코딩 규칙", "SECURE CODING",
["보안 불변(위반 시 반려)", "AI 워터마크", "자격증명·오류 응답"])
s = slide(); content_header(s, "06 · 보안 코딩 규칙", "보안 불변 (위반 시 QA 반려)", 6)
titled_card(s, 0.55, 1.5, 6.05, 3.9, "지켜야 할 규칙", BLUE_DK, [
("자격증명 미노출", "IP·SSH·비번·해시·API키·OTP 시크릿 미노출"),
("민감 필드 제외", "사용자/업체 응답은 비민감 필드만"),
("스택트레이스 차단", "error.message는 요약만 · 상세는 서버 로그"),
("AI 이미지 워터마크", "watermarkRequired+watermarkText+notice 항상"),
("등록업체 응찰", "미등록 CONTRACTOR NOT_REGISTERED_COMPANY 403"),
("외부 API 금지", "예외 = Claude(env·Ollama 폴백)·Gemini(워커·G1)"),
("암호화 저장", "비밀 AES-256-GCM · 비밀번호 BCrypt 해시"),
], item_size=9.3, gap=0.4)
titled_card(s, 6.75, 1.5, 6.03, 3.9, "코드에서 이렇게", PURPLE, [
("키 로드", "env에서만 — 코드·DB·커밋·로그 기록 금지"),
("Gemini", "워커에서만 로드 · 백엔드는 큐 발행/콜백만"),
("admin 비번", "ADMIN_PASSWORD_ENC 복호 → 기동 시 BCrypt 재시드"),
("오류 핸들러", "GlobalExceptionHandler·DataAccessException로 누출 차단"),
("gitignore", ".env·*.key·*-adminsdk-*.json 커밋 차단"),
("워커 실패", "errorMessage는 요약만(스택 유입 차단)"),
("감사로그", "민감 액션 기록 · 비밀값 미기재"),
], item_size=9.3, gap=0.4)
banner(s, 5.6, "체크(grep) — 응답·로그·커밋에 IP·SSH·비번·해시·API키·워커토큰 노출 0. 위반은 QA 반려 사유입니다.",
fill=RED, size=11, h=0.5)
return PRS
# =====================================================================
# 저장
# =====================================================================
if __name__ == "__main__":
out_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # ...\지침서
targets = [
("사용자지침서.pptx", build_user),
("운영자지침서.pptx", build_ops),
("개발자지침서.pptx", build_dev),
]
for fname, fn in targets:
prs = fn()
path = os.path.join(out_dir, fname)
prs.save(path)
print(f"SAVED: {path} (slides={len(prs.slides._sldIdLst)})")