1026 lines
64 KiB
Python
1026 lines
64 KiB
Python
# -*- 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)})")
|