deploy: public header restore + accent white-text sweep + mobile v0.2.0 source (web 0add2cd/mobile 490b723 over f3b800f)

This commit is contained in:
GUARDiA 2026-07-13 00:31:51 +09:00
parent cf6a9a2171
commit 58aa8440d9
218 changed files with 8044 additions and 11632 deletions

View File

@ -0,0 +1,50 @@
---
name: kintex-gap-auditor
description: 킨텍스 앱의 미개발·미완성을 전수 감사하는 에이전트. 메뉴↔라우트↔컴포넌트↔API 4자 체인을 교차 대조해 빈 화면·스텁·"준비중"/TODO·홈/대시보드 폴백·데드링크·미구현 엔드포인트·CRUD 누락·미배선 버튼을 색출하고 우선순위 백로그를 산출한다. "미개발 확인", "완성도 감사", "빈 화면 찾기", "준비중 화면", "홈으로 가는 메뉴", "미구현", 다시 실행·업데이트·보완 포함. general-purpose 타입.
tools: Read, Write, Glob, Grep, Bash
model: opus
---
당신은 킨텍스 AI 전시·행사시스템의 **미개발 감사관**이다. general-purpose 타입 — grep·빌드·서버 조회를 실제 실행한다. 목적은 **고치는 것이 아니라 "무엇이 아직 안 되어 있는지"를 정확·완전하게 확인**하는 것이다(수정은 구현 하네스가 담당).
## 핵심 역할
프론트(`src/frontend`)·백엔드(`src/backend`)를 전수 스캔해 **미개발/미완성 지점**을 근거(파일:라인)와 함께 인벤토리화하고, 심각도·우선순위를 매긴 백로그(`docs/GAP_AUDIT.md`)를 산출한다.
## 감사 방법 (4자 체인 교차 대조 — 핵심)
킨텍스 화면은 **메뉴(AppShell GROUPS) → 라우트(App.tsx) → 컴포넌트(screens/**) → API(backend controller)** 로 이어진다. 각 연결이 끊긴 곳이 미개발이다:
1. **메뉴 인벤토리**: `components/layout/AppShell.tsx``GROUPS`/`NavItem`(`to`·`scoped`) + 공개 네비(PublicShell) 전수 추출.
2. **라우트 인벤토리**: `App.tsx``<Route path>` 전수 추출.
3. **화면 상태**: 각 라우트 컴포넌트가 (a)실제 구현 (b)스텁/빈 (c)"준비중"/TODO/placeholder (d)홈·대시보드로 폴백/리다이렉트 인지 판정.
4. **API 배선**: 화면이 호출하는 엔드포인트가 백엔드에 실제 구현됐는지(컨트롤러 존재·스텁 아님) 대조. 미구현/스텁/501/빈배열 하드코딩 색출.
## 색출 대상 (gap taxonomy — 상세 references/gap-taxonomy.md)
- **빈 화면/스텁**: `return null`·빈 EmptyState 고정·`TODO`·`준비중`·`coming soon`·`구현 예정`·placeholder만 렌더.
- **홈/대시보드 폴백**: 메뉴 클릭이 전용 화면 없이 `/home`·대시보드로 감(참가업체 사례). `EVENT_SCOPED_PATH` 폴백·`Navigate` 리다이렉트.
- **데드링크**: `to`/`href`가 정의된 라우트에 없음, `#`·`javascript:void`·빈 onClick.
- **미배선 버튼/액션**: onClick 없음·`disabled` 고정·`alert('미구현')`·핸들러 빈 함수.
- **미구현 API**: 프론트가 부르는 경로가 백엔드 컨트롤러에 없음, 또는 컨트롤러가 스텁(하드코딩·빈 리스트·`NOT_IMPLEMENTED`).
- **CRUD 누락**: 목록만 있고 생성/수정/삭제 미구현, 상세 없음.
- **메뉴 없는 라우트 / 라우트 없는 메뉴**: 고아 화면·죽은 메뉴.
- **모바일(mobile/)**: 화면은 있으나 API 미배선·mock 고정(선택 스캔).
## 작업 원칙
- **근거 필수**: 모든 지적은 `파일:라인` + 무엇이 왜 미개발인지. 추측 금지 — 실제 코드 확인.
- **오탐 억제**: "구현됐으나 데이터가 비어 보이는 것"(행사스코프 시드 미정렬 [[event-scope-seed-alignment]])과 "코드가 미개발인 것"을 구분. 데이터 이슈는 별도 표기.
- **우선순위**: P0(핵심 업무 흐름 차단)·P1(주요 화면 빈/폴백)·P2(부가·엣지). 역할별 노출도 고려(관리자 전용 등).
- **정본 대조**: `docs/PLANNING.md`(M1~M18 범위)·`docs/design.md`(SCR 인벤토리)와 대조해 "기획엔 있는데 미구현" 을 우선.
## 입력/출력 프로토콜
- 입력: 스캔 범위(전체/특정 영역), 이전 `docs/GAP_AUDIT.md`(있으면 델타 갱신).
- 출력: `docs/GAP_AUDIT.md` — ①요약(총 N건·심각도 분포) ②4자체인 인벤토리(메뉴·라우트·화면상태·API) ③미개발 목록(파일:라인·유형·심각도·정본근거) ④우선순위 백로그(수정 담당 하네스 매핑: 화면=kintex-frontend-dev/구현 오케스트레이터, API=kintex-backend-dev, 데이터=kintex-testdata-dev). 중간 산출 `_workspace/`.
## 에러 핸들링
빌드/서버 조회 실패 시 1회 재시도, 재실패면 해당 항목 "확인 불가"로 표기하고 계속. 상충(코드상 구현인데 런타임 빈)은 삭제 말고 양쪽 근거 병기.
## 협업
- 발견은 `docs/GAP_AUDIT.md`로 공유. **kintex-qa**가 P0/P1 지적을 실동작(경계면·라우트 렌더)으로 재검증(오탐 제거). 수정은 구현 하네스(kintex-impl-orchestrator)로 인계 — 이 에이전트는 **감사·확인만**.
## 재호출 지침
이전 `docs/GAP_AUDIT.md`가 있으면 읽고, 그 사이 배포로 해소된 항목은 ✅ 표기·신규 미개발만 추가(델타 감사). 특정 영역만 요청 시 그 영역만.
## 보안 불변
자격증명·PII·내부IP·스택트레이스 문서 미기재. 서버 조회는 존재/상태만(값 미출력).

24
.claude/settings.json Normal file
View File

@ -0,0 +1,24 @@
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "C:\\Users\\ython\\AppData\\Local\\Python\\pythoncore-3.14-64\\Scripts\\graphify.EXE hook-guard search"
}
]
},
{
"matcher": "Read|Glob",
"hooks": [
{
"type": "command",
"command": "C:\\Users\\ython\\AppData\\Local\\Python\\pythoncore-3.14-64\\Scripts\\graphify.EXE hook-guard read"
}
]
}
]
}
}

View File

@ -0,0 +1,44 @@
---
name: kintex-completeness-orchestrator
description: 킨텍스 앱의 미개발·미완성을 전수 확인하는 감사 하네스. 메뉴↔라우트↔컴포넌트↔API 4자 체인을 교차 대조해 빈 화면·스텁·"준비중"/TODO·홈/대시보드 폴백·데드링크·미배선 버튼·미구현 엔드포인트·CRUD 누락을 색출하고 우선순위 백로그(docs/GAP_AUDIT.md)를 산출한다. "미개발 확인", "완성도 감사", "빈 화면/준비중 화면 찾기", "홈으로 가는 메뉴", "미구현 점검", "개발 안 된 것", 다시 실행·특정 영역만·업데이트·보완 요청 시 이 스킬을 사용하라. (수정·구현은 kintex-impl-orchestrator, UI정렬은 kintex-wise-ui-orchestrator 담당 — 이 하네스는 확인/감사 트랙.)
---
# 킨텍스 미개발 확인 (완성도 감사) 오케스트레이터
킨텍스 앱에서 **아직 개발 안 된/미완성 지점**을 정확·완전하게 확인한다. **실행 모드: 하이브리드** — Phase1 감사(단일 에이전트) → Phase2 검증(단일 QA). 모든 Agent 호출은 `model: "opus"`.
**경계(중복 회피):** 이 하네스는 **확인/감사만** 한다. 수정·구현은 `kintex-impl-orchestrator`, UI 정렬은 `kintex-wise-ui-orchestrator`, 데이터 빈 화면은 `kintex-renewal`/testdata. 감사 결과 백로그를 그 하네스들로 인계한다.
## 에이전트 로스터
| 에이전트 | 역할 |
|---|---|
| **kintex-gap-auditor**(신규) | 4자 체인 교차 대조·미개발 전수 인벤토리·`docs/GAP_AUDIT.md` 산출 |
| **kintex-qa**(재사용) | P0/P1 지적을 실동작(라우트 렌더·경계면·API 200)으로 재검증 — 오탐 제거 |
| **reviewer**(재사용, 선택) | 정본(PLANNING/design) 대조 정합 확인 |
## Phase 0: 컨텍스트
- `docs/GAP_AUDIT.md` 존재 여부로 초기/델타 판별. 특정 영역만 요청이면 범위 한정.
- 최근 배포로 해소된 항목은 델타에서 ✅ 처리.
## Phase 1: 미개발 감사 (kintex-gap-auditor 단일)
메뉴(AppShell GROUPS·PublicShell)·라우트(App.tsx)·화면(screens/**)·API(backend controller)를 전수 스캔·교차 대조. gap taxonomy(references/gap-taxonomy.md)로 분류. **오탐 억제**: "코드 미개발" vs "데이터만 빈(행사스코프 시드 [[event-scope-seed-alignment]])" 구분. `docs/GAP_AUDIT.md`에 요약·인벤토리·미개발목록(파일:라인·유형·심각도)·우선순위 백로그(수정 담당 하네스 매핑) 산출.
## Phase 2: 검증 (kintex-qa 단일)
P0/P1 지적을 **실동작으로 재검증** — 해당 라우트가 정말 빈/폴백인지, API가 정말 미구현(404/스텁)인지 헤드리스/curl/grep로 확인. 오탐은 제거·확정은 CONFIRMED 표기. 결과를 GAP_AUDIT.md에 반영.
## Phase 3: 인계 (오케스트레이터)
확정 백로그를 화면=kintex-frontend-dev/impl, API=kintex-backend-dev, 데이터=kintex-testdata-dev로 매핑해 요약 보고. **수정은 이 하네스가 하지 않는다** — 소유자 승인 후 해당 하네스로 넘긴다.
## 데이터 전달 / 에러
파일 기반(`docs/GAP_AUDIT.md` 정본 + `_workspace/` 중간). 조회 실패는 1회 재시도 후 "확인 불가" 표기. 상충은 양쪽 근거 병기(삭제 금지).
## 보안 불변
자격증명·PII·내부IP·스택트레이스 문서 미기재. 서버 조회는 존재/상태만.
## 테스트 시나리오
- **정상**: "미개발 확인 하네스 실행" → Phase1 감사(GAP_AUDIT.md) → Phase2 QA 재검증(오탐 제거) → Phase3 인계 보고.
- **델타**: "다시 확인" → 기존 GAP_AUDIT.md 대비 해소분 ✅·신규만 추가.
- **부분**: "옥션 화면만 미개발 확인" → 범위 한정 감사.
## 후속 작업
description 후속 키워드("다시 실행"·"특정 영역만"·"업데이트")로 델타/부분 감사 트리거. Phase 0이 초기/델타/부분 판별.

View File

@ -0,0 +1,37 @@
# 미개발 분류 (Gap Taxonomy) — 킨텍스
감사 시 각 지점을 아래 유형으로 분류하고 근거(파일:라인)와 심각도를 붙인다.
## 유형별 탐지 신호 (grep 힌트)
| 유형 | 코드 신호(grep) | 판정 |
|------|----------------|------|
| **빈 화면/스텁** | `return null`(라우트 컴포넌트 최상위)·`준비중`·`준비 중`·`coming soon`·`구현 예정`·`TODO`·`FIXME`·`placeholder`·`더미`·`mock`(고정) | 렌더 내용이 없거나 안내문뿐 |
| **홈/대시보드 폴백** | `EVENT_SCOPED_PATH`·`Navigate to="/home"`·`navigate('/home')`·`navigate('/dashboard')`·메뉴 `scoped` 인데 전용 라우트 없음 | 메뉴 클릭이 전용 화면 없이 홈/대시보드로 감 |
| **데드링크** | `to="#"`·`href="#"`·`href="javascript:`·`to=` 값이 App.tsx 라우트에 없음 | 이동 대상이 없음 |
| **미배선 버튼/액션** | `onClick={() => {}}`·`onClick` 부재(버튼인데)·`disabled` 하드코딩·`alert(`·`console.log('TODO` | 눌러도 동작 없음 |
| **미구현 API** | 프론트 `api.get/post('/api/...')` 경로가 backend `@*Mapping`에 없음·컨트롤러가 `NOT_IMPLEMENTED`·빈 리스트/하드코딩 반환·`return List.of()` 고정 | 서버가 없거나 스텁 |
| **CRUD 누락** | 목록 화면만·`POST/PUT/DELETE` 핸들러 부재·상세 라우트/화면 없음 | 조회만 가능 |
| **고아/죽은** | 라우트 있으나 메뉴/링크 진입 없음 / 메뉴 있으나 라우트 없음 | 접근 불가 또는 죽은 메뉴 |
## 오탐(false positive) 주의 — 미개발 아님
- **데이터만 빈**: 코드는 구현됐는데 행사스코프 시드가 데모 해소행사에 없어 빈 것 → [[event-scope-seed-alignment]]. **미개발 아님, 데이터 이슈로 별도 표기**(담당=testdata).
- **역할 게이트로 미노출**: 권한상 안 보이는 것(관리자 전용 등)은 미개발 아님.
- **degraded 폴백**: API 실패 시 의도적 폴백(그라디언트·안내) 있는 것은 정상 처리.
- **의도적 외부/준비중**: iOS TestFlight "준비중" 등 명시적 로드맵 항목은 "계획됨"으로 표기(미개발과 구분).
## 심각도
- **P0**: 핵심 업무 흐름(로그인 후 주요 도메인 진입·부스/옥션/정산/관람객 핵심 CRUD) 차단.
- **P1**: 주요 화면이 빈/폴백/데드링크. 메뉴에 있으나 동작 안 함.
- **P2**: 부가·엣지·관리자 세부·모바일 mock.
## 정본 대조
`docs/PLANNING.md`(M1~M18·§5B 공통·역할/트랙)·`docs/design.md`(SCR 인벤토리)에 **있는데 미구현**인 것을 최우선. 기획에 없는 것은 "범위 외"로 표기.
## 4자 체인 스캔 순서
1. `AppShell.tsx` GROUPS/NavItem → 메뉴·`to`·`scoped` 목록
2. `App.tsx` `<Route>` → 라우트·컴포넌트 목록
3. 각 컴포넌트 파일 → 렌더 내용 판정(위 신호)
4. 컴포넌트가 부르는 `*Api.ts`/`api.*` 경로 → backend `@*Mapping` 대조
5. 공개 사이트: `PublicShell`·`screens/public/*` 동일 절차
6. (선택) `mobile/app/*` → API 배선 여부

4
.gitignore vendored
View File

@ -34,3 +34,7 @@ dist/
# 전시장 CAD 원본(대용량 DWG zip) — 로컬 보존, git 제외
docs/assets/floorplans/cad/
# graphify / screenshot generated outputs (local only)
graphify-out/
screenshot/

View File

@ -82,6 +82,8 @@ GUARDiA 표준 프레임워크(Spring Boot 3.5 + React 19 + MyBatis + PostgreSQL
| 2026-07-11 | 구현 하네스 초기 구성 — 전문 에이전트 6종 + kintex-impl-orchestrator + 구현 백로그 | 전체 | 범용 에이전트만 존재 → 스택 확정 후 실제 구현 조율 팀 구성 |
| 2026-07-11 | **v2.0 재구성** — 자동전시시스템 확장(PLANNING v2.0). 신규 에이전트 11종(아키텍트 5·공통 1·도메인 5·AI 1) + 오케스트레이터·백로그 v2.0(Phase A 아키텍처→B 공통레이어→C 부스코어→D 도메인→E 배포) | 전체 | 스코프 확장(전시 전기능·경영분석·CMS·관리자·역할분리·공사 옥션·WISE 공통레이어·AI Claude 전환) |
| 2026-07-11 | WISE 웹 기능 레퍼런스 강화 — kintex-frontend-dev에 uiws frontend(pages 공통 업무기능) 차용 원칙, 오케스트레이터에 웹 기능 화면 WISE 패턴·Stitch 디자인 경유·모바일 경계 명시 + 로스터에 kintex-mobile-dev(경계) 추가 | kintex-frontend-dev·kintex-impl-orchestrator | "웹도 wise 기능 참고" 요청 |
| 2026-07-12 | **세션 대량 구현·배포** — visitor 메인(AI 관람 도우미 홈·기능타일·연월 브리핑, 근거=크롤 적재 DB)·3-트랙 관문/서브홈·역할별 랜딩+메뉴 게이트(plan_role_routing)·공개 AI/캘린더 API(V43)·정산 데이터 정렬(V47)·포스터 이미지 빌드내장(V46)·성능 인덱스(V44)·아바타 API(V45). AI 토큰최소화 기획(PLANNING v3.4 §8A)·데이터표준(FK최소화·공통코드·view/mview·배치, data.md)·Open SSO+HR 연동 설계(sso-hr-integration.md). 배포 커밋 214b6df→3077cb0, health 게이트 통과 | 전체 | 소유자 라이브 피드백 연속 반영. **전 지시 로그: `docs/OWNER_FEEDBACK.md`(권위)** |
| 2026-07-12 | **선행 게이트 해소·검증 교훈** — G1 나노바나나 Gemini 라이브·G2 kintex.zioinfo.co.kr:8021 CI/CD 라이브. 배포 전 필수: **Flyway 마이그레이션 라이브 dry-run(BEGIN…ROLLBACK)**·시크릿 fail-fast 프로파일 확인·파일단위 커밋(공유 트리 교차 방지)·health 게이트. Stitch 불안정 시 design.md 스펙 직접 구현(문서화 폴백) | 전체·kintex-devops-dev | 다수 배포 롤백/캐시/교차커밋 사고 후 표준화 |
---
@ -95,6 +97,7 @@ GUARDiA 표준 프레임워크(Spring Boot 3.5 + React 19 + MyBatis + PostgreSQL
| 날짜 | 변경 내용 | 대상 | 사유 |
|------|----------|------|------|
| 2026-07-12 | 초기 구성(analyst·crawler 에이전트 + 오케스트레이터) | 전체 | "크롤링/벤치마킹 하네스 생성" 요청 — COEX 3-트랙 재구성·행사 크롤 정례화 |
| 2026-07-12 | **모바일 앱 벤치마킹**(전시·공연 앱 다운로드·평점 상위순) → `docs/analysis/mobile-benchmark-exhibition-performance.md` + 백로그 12항목. 핵심: 다운로드↔만족도 상반(티켓팅 빅3 저평점) → MMCA·Eventbrite·DICE·Fever 롤모델. **관람객 정보 크롤→DB 적재**(V43 visitor_guide·transport, 포스터 34장 V46) | kintex-benchmark-analyst·kintex-crawler-dev | "모바일은 전시·공연 앱 벤치마킹 다운로드·좋아요순" + "크롤링해서 db 저장" |
---
@ -108,6 +111,7 @@ GUARDiA 표준 프레임워크(Spring Boot 3.5 + React 19 + MyBatis + PostgreSQL
| 날짜 | 변경 내용 | 대상 | 사유 |
|------|----------|------|------|
| 2026-07-12 | 초기 구성(kintex-testdata-dev 신규 + wise-ui/menu-ia/qa 재사용 + 오케스트레이터) | 전체 | 소유자 지시 — 상세→메인 복귀 부재·전면 리뉴얼·테스트 데이터/공통코드/프로그램 채움 |
| 2026-07-12 | **코엑스풍 공개사이트 리뉴얼**(design.md v2.4 §3C-V 비주얼·모션 — 시네마틱 히어로·스크롤 리빌·카운트업·포스터 카드 리프트/줌·오토스크롤, prefers-reduced-motion·CLS 0) + **행사스코프 데이터 정렬**(V47 e-2026-live 폐루프·V49 잔여 보강). ★교훈: 화면이 비는 진짜 원인=시드가 데모 해소행사(workspaces[0]=start_date 최소)에 안 묶임 | kintex-frontend-dev·kintex-testdata-dev·designer | "코엑스처럼 지금 착수"·"정산 데이터 없음"·"모든 테이블 데이터" |
---
@ -122,6 +126,7 @@ GUARDiA 표준 프레임워크(Spring Boot 3.5 + React 19 + MyBatis + PostgreSQL
|------|----------|------|------|
| 2026-07-12 | 초기 구성(에이전트 2 신규 + kintex-qa 재사용 + 오케스트레이터) | 전체 | 소유자 강한 피드백("전부 WISE대로 안 되어 있다" — 시스템관리 메뉴 실종·햄버거/아코디언 부재·캘린더·대시보드·로고) |
| 2026-07-12 | 메뉴 IA 전담 추가 — kintex-menu-ia-dev + kintex-menu-recompose 스킬(표준 카테고리 v1·라우트↔메뉴 정합 절차), 오케스트레이터 Phase 2.5 편입 | agents·skills | "카테고리별 메뉴 재구성 하네스" 요청 — 별도 오케스트레이터 대신 중복 회피 확장 |
| 2026-07-12 | **Nifty 디자인 시스템 전면 정렬** — 카드 Nifty 타입(`.kx-card` subtle 그림자·hover·`--flush`/`__media`/`__header`/`__body`/`__footer` 구조)·타이포(`--fs-micro`/`--fs-nano`·`--fw-*` 토큰·공용 `.kx-page__title`/`.kx-section__title`·font-size 179+weight 415 토큰화)·그리드(`.kx-table` Nifty Advanced header로 6화면 수렴). **캘린더 Nifty**(옅은 격자·연노랑 today·소프트 이벤트칩)·**파란버튼 흰글자**(`--color-on-accent` 다크대응)·favicon 정정. 기준: `docs/DESIGN_SYSTEM_NIFTY.md`·`_workspace/audit_typography_grid.md` | kintex-frontend-dev·kintex-qa | 소유자 반복 강피드백("Nifty 카드/그리드/폰트 안 맞음"·"파란버튼 흰글자"·"달력 Nifty") |
---
@ -138,3 +143,31 @@ GUARDiA 표준 프레임워크(Spring Boot 3.5 + React 19 + MyBatis + PostgreSQL
|------|----------|------|------|
| 2026-07-11 | 초기 구성 — kintex-mobile-dev 신규 + 오케스트레이터(재사용: designer·backend-dev·qa·devops-dev·reviewer). WISE 모바일 레퍼런스·Stitch 디자인 경유 반영 | 전체 | 모바일 앱 트랙 전담 하네스 부재("하네스 생성" + "wise 모바일 참고" + "디자인은 스티치" 요청) |
| 2026-07-11 | **앱 타깃 2개 확정 반영** — 코드베이스 1(mobile/) + 배포 타깃 2(①운영 B2B: 2FA 필수·사내 QR ②관람객 B2C: 스토어 공개·간편가입·게스트). 계정은 단일 통합+가입 트랙 분리(PLANNING v3.1) | kintex-mobile-dev·오케스트레이터 | 소유자 확정 — 회원가입 관람객 포함 질의 → 하이브리드(계정 통합·앱 분리) 채택 |
| 2026-07-12 | **내정보(WISE)+생체인식(expo-local-authentication)+프로필 사진(expo-image-picker)** + **앱 위변조 방지·시큐어코딩 보안 체크리스트**(루트/탈옥·무결성·Hermes+R8·화면캡처·cleartext·권한최소, `docs/security/mobile-security-*.md`) + **다국어(react-i18next)·역할별 랜딩 패리티** 착수 | kintex-mobile-dev·kintex-qa | "모바일 내정보 WISE+생체+사진"·"앱 위변조방지·시큐어코딩·보안체크"·"모바일도 동일 로직"·"웹 4개국어인데 모바일은?" |
## graphify
This project has a knowledge graph at graphify-out/ with god nodes, community structure, and cross-file relationships.
Rules:
- For codebase questions, first run `graphify query "<question>"` when graphify-out/graph.json exists. Use `graphify path "<A>" "<B>"` for relationships and `graphify explain "<concept>"` for focused concepts. These return a scoped subgraph, usually much smaller than GRAPH_REPORT.md or raw grep output.
- If graphify-out/wiki/index.md exists, use it for broad navigation instead of raw source browsing.
- Read graphify-out/GRAPH_REPORT.md only for broad architecture review or when query/path/explain do not surface enough context.
- After modifying code, run `graphify update .` to keep the graph current (AST-only, no API cost).
---
## 하네스: 미개발 확인 (kintex-completeness-orchestrator)
**목표:** 킨텍스 앱의 **미개발·미완성**을 전수 확인. 메뉴↔라우트↔컴포넌트↔API 4자 체인을 교차 대조해 빈 화면·스텁·"준비중"/TODO·홈/대시보드 폴백·데드링크·미배선 버튼·미구현 엔드포인트·CRUD 누락을 색출하고 우선순위 백로그(`docs/GAP_AUDIT.md`) 산출. **확인/감사만** — 수정은 구현 하네스 인계.
**트리거:** 미개발 확인, 완성도 감사, 빈 화면/준비중 화면 찾기, 홈으로 가는 메뉴, 미구현 점검, 개발 안 된 것, 다시 실행·특정 영역만·업데이트 요청 시 `kintex-completeness-orchestrator` 스킬을 사용하라.
**에이전트:** kintex-gap-auditor(신규, 4자체인 감사·인벤토리) + kintex-qa(재사용, P0/P1 실동작 재검증·오탐 제거) + reviewer(선택, 정본 대조).
**경계(중복 회피):** 확인=이 하네스. 수정·구현=kintex-impl-orchestrator, UI정렬=kintex-wise-ui-orchestrator, 데이터 빈 화면=kintex-renewal/testdata. ★오탐 주의: "코드 미개발" vs "데이터만 빈(행사스코프 시드 미정렬)" 구분.
**변경 이력:**
| 날짜 | 변경 내용 | 대상 | 사유 |
|------|----------|------|------|
| 2026-07-12 | 초기 구성(kintex-gap-auditor 신규 + kintex-qa 재사용 + 오케스트레이터 + gap-taxonomy 레퍼런스) | 전체 | "미개발 확인 하네스 생성" 요청 — 4자체인 교차대조 완성도 감사 트랙 부재 |

107
docs/GAP_AUDIT.md Normal file
View File

@ -0,0 +1,107 @@
# KINTEX AI 전시·행사시스템 — 미개발 전수 감사 (GAP_AUDIT)
- 감사일: 2026-07-12 · 감사자: `kintex-gap-auditor`
- 범위: `src/frontend`(85 라우트) + `src/backend`(74 컨트롤러) 4자 체인(메뉴↔라우트↔화면↔API) 교차 대조
- 방법: gap-taxonomy grep 신호 + 실코드 확인. 수정 없음(감사·문서 전용).
---
## ① 요약
| 구분 | 건수 |
|------|------|
| **미개발 총계(코드)** | **9건** |
| P0 (핵심 흐름 차단) | 0 |
| P1 (주요 화면 미배선/백엔드 전무) | 2 |
| P2 (부분·degraded·planned) | 7 |
| 데이터 이슈(별도, 코드 정상) | 행사스코프 시드 정렬 계열(전수 아님) |
| 문서 위생(별도, 미개발 아님) | 4 (stale "미구현" 주석) |
**총평:** 앱 완성도는 매우 높다. 데드 메뉴 0, 빈 onClick/데드링크 0, 최상위 `return null` 라우트 0. 도메인 백엔드(auction·visitor·cms·marketing·logistics·settlement·hallassign·boothsales)는 전부 실구현. **가장 중요한 발견은 "미개발이 많다"가 아니라 "프론트 `sample` 헤더 주석이 Phase D 백엔드 배포 이후 갱신되지 않아 STALE 하다"는 점** — 실제 미개발은 아래 2건(P1)뿐이며 나머지는 graceful degraded/planned.
### 최우선(P0/P1) 목록
1. **[P1] TenantAdminPage** — 백엔드 `/api/admin/tenants`(목록+생성) 실존하나 프론트가 하드코딩 배열·`disabled` 버튼으로 미배선.
2. **[P1] PublicTicketPage(입장권 예매/조회)** — 티켓·재고·결제(PG) 백엔드가 아예 없음(고객 대면 공개 라우트).
---
## ② 4자 체인 인벤토리 (요약)
메뉴 `to` 타깃 ↔ App.tsx 라우트 ↔ 화면 컴포넌트 ↔ 백엔드 매핑. **전 메뉴 항목이 라우트에 정합(데드 메뉴 없음).**
| 메뉴 그룹 | 대표 라우트 | 화면 상태 | 백엔드 컨트롤러 | 판정 |
|-----------|-------------|-----------|-----------------|------|
| 전시운영 | `/exhibitors`,`/schedule`,`/halls`,`/booth-sales`,`/ops/operations`,`:eventId/dashboard` | 실렌더 | Exhibitor·EventCatalog·HallAssign·BoothSales·Ops·Dashboard | ✅ |
| 설계·시공 | `:eventId/halls/:hallId/layout`,`/docs`,`/auctions`,`/contractor/dashboard`,`/logistics` | 실렌더 | Floorplan(m2)·Design(m3)·Utility(m4)·RenderJob(m5)·Document·Auction·Contractor·Logistics | ✅ |
| 관람객·마케팅 | `/visitors`,`/visitors/checkin`,`/leads`,`/campaigns`,`/sponsorship`,`/cms` | 실렌더 | Visitor·Marketing·CmsContent/Media/Microsite | ✅ |
| 정산·분석 | `/settlement`,`/analytics` | 실렌더 | Settlement·Analytics(+Overview) | ✅ |
| 업무공통 | `/work/*`(worklog·schedule·message·notice·meeting·report·approval·opinion·search),`/app-qr` | 실렌더 | work.* 11 컨트롤러 | ✅ |
| 시스템관리(admin) | `/admin`,`/admin/users·roles·depts·companies·menus·programs·role-menus·codes·auth-policy·login-history·audit·error-log·system-health·settings·mail-config·notify-config·**tenants**·masterdata/rulesets·holidays·login-slides` | 실렌더 | system.* + AdminDashboard·AuditLog·Ruleset·**TenantAdmin** | ⚠ tenants만 미배선 |
| 공개사이트 | `/public/*`,`/visitor·/business·/agency`,`/tickets/*` | 실렌더 | PublicSite·PublicGuide·PublicCms·PublicMicrosite·VisitorAssistant | ⚠ `/tickets/*`만 백엔드 전무 |
| 모바일 셀프 | `/m/booths/:id/checklist·/inspection` | 실렌더(웹뷰) | (모바일 앱 `mobile/` 별도 트랙) | ✅ |
---
## ③ 미개발 목록 (근거·유형·심각도·정본)
### P1 — 주요 미배선/백엔드 전무
**G-01 [P1] 테넌트 관리 화면 미배선 (frontend)**
- 파일: `src/frontend/src/screens/admin/TenantAdminPage.tsx:20`(하드코딩 `TENANTS`), `:53`(추가 버튼 `disabled title=notImpl`)
- 유형: 미배선(백엔드 존재, 프론트 sample 고정) — CRUD 누락(목록·생성 미연결)
- 근거: 백엔드 `TenantAdminController`(`/api/admin/tenants` GET list + POST create) **실존**. 화면 주석 "백엔드 미구현"은 **STALE**.
- 정본: PLANNING M18 §1A(테넌트 온보딩), design.md SCR-A9. 기획에 있음.
**G-02 [P1] 입장권 예매/조회 백엔드 전무**
- 파일: `src/frontend/src/screens/public/PublicTicketPage.tsx:5`(주석 "티켓·재고·결제 API 부재 → 샘플"), `:36`(하드코딩 `TICKETS`)
- 라우트: `/tickets/:eventId/purchase`, `/tickets/lookup` (공개, 비로그인 고객 대면)
- 유형: 미구현 API(백엔드 컨트롤러 부재 — grep "ticket" in backend = 0) + PG 미연동
- 정본: PLANNING M10(관람객)·M9(정산) design.md SCR-P7. 기획에 있음.
### P2 — 부분 미개발(정상 degraded/planned)
| ID | 항목 | 근거(파일:라인) | 유형 | 비고 |
|----|------|-----------------|------|------|
| G-03 | CMS AI 자동번역 | `cms/MultilingualCmsPage.tsx:127` · `cmsApi.ts:150` | 백엔드 501 → degraded "준비 중" | 초벌 번역 AI 미배선 |
| G-04 | 마케팅 캠페인 실발송 | `marketing/MarketingController.java:49` | send 엔드포인트 존재하나 실 이메일 발송 미구현(status 전이만) | SMTP 연동 후속 |
| G-05 | 서류 HWP/PDF 생성 | `docs/ReportAuthoringPage.tsx:476-479` · `docs/DocsMilestonePage.tsx:34` | 버튼 `disabled` "준비중" | 파일 큐 연동 후속 |
| G-06 | 회의 녹음/STT 전사 | `work/MeetingPage.tsx:161` | "준비 중" 표기 | 녹음·STT AI 미배선 |
| G-07 | 통합검색 자연어(AI) | `work/SearchPage.tsx:132,144` | "준비 중", 키워드 검색은 동작 | AI 검색 미배선 |
| G-08 | 마이페이지 프로필 수정·알림 서버동기화 | `work/MyPage.tsx:127,192` | "준비 중"(로컬 저장) | 프로필 PUT·알림설정 API 미배선 |
| G-09 | 부스설계 렌더 이력 저장 | `design/BoothDesignStudioPage.tsx:135` | 501 → "큐잉만 동작" | 렌더 이력 영속 후속 |
### 계획됨(미개발 아님)
- AppQr **iOS**`appqr/AppQrCodePage.tsx`: Android APK 배포, iOS는 EAS G3 게이트 명시적 로드맵.
---
## ④ 별도 분류 (미개발 아님)
### 데이터 이슈 (코드 정상 · 담당=kintex-testdata-dev)
- sample-import 12개 화면(analytics·dashboard·ops·floorplan·gallery·marketing×2·schedule·visitor×3)은 **조건부 degraded 폴백**(NETWORK/NOT_FOUND/NOT_IMPLEMENTED 시에만 sample). 실 API가 primary. 빈 화면으로 보이면 코드가 아니라 **행사스코프 시드 미정렬** → testdata 담당.
### 문서 위생 — STALE 주석 (기능 영향 없음 · 담당=frontend-dev 청소)
- `screens/auction/sampleAuction.ts:3` "M15 역경매 엔진 미구현" → 실구현됨
- `screens/visitor/sampleVisitor.ts:3` "M10/M11 백엔드 미구현" → 실구현됨
- `screens/marketing/sampleMarketing.ts:3` "M12 백엔드 미구현" → 실구현됨
- `screens/cms/cms.css:4` "M17 백엔드 미구현 → 전 화면 로컬 샘플" → cmsApi 실배선됨
→ 주석만 갱신(오해 유발). 재감사 오탐 방지 위해 정리 권장.
---
## ⑤ 우선순위 백로그 (수정 담당 매핑)
| 순위 | ID | 작업 | 담당 |
|------|-----|------|------|
| 1 | G-01 | TenantAdminPage → `/api/admin/tenants` GET/POST 배선(하드코딩·disabled 제거) | **kintex-frontend-dev** |
| 2 | G-02 | 티켓 도메인 백엔드(티켓·재고·예매·조회) + PG 어댑터 + 프론트 배선 | **kintex-backend-dev**(+visitor-dev) → frontend-dev |
| 3 | G-04 | 캠페인 실발송(SMTP, 수신동의·상한) | kintex-backend-dev(marketing) |
| 4 | G-03 | CMS AI 자동번역 배선(AiTextRouter) | kintex-ai-dev |
| 5 | G-06/G-07 | 회의 STT·통합검색 자연어 AI | kintex-ai-dev |
| 6 | G-05/G-09 | 서류 HWP/PDF 생성·렌더 이력 영속(파일/렌더 큐) | kintex-backend-dev |
| 7 | G-08 | 마이페이지 프로필 PUT·알림설정 API | kintex-backend-dev + frontend-dev |
| 8 | (위생) | STALE "미구현" 주석 4건 정리 | kintex-frontend-dev |
| — | (데이터) | 행사스코프 시드 정렬(빈 화면 폴백 해소) | kintex-testdata-dev |
---
*P0/P1 지적은 kintex-qa가 실동작(라우트 렌더·경계면)으로 재검증 후 kintex-impl-orchestrator로 인계.*

184
docs/OWNER_FEEDBACK.md Normal file
View File

@ -0,0 +1,184 @@
# 소유자 피드백·지시 로그 (KINTEX AI 전시·행사시스템)
> 이번 세션에서 소유자가 지시한 모든 항목을 전수 기록한다. 상태: ✅완료(배포) · 🚚배포중 · ⏳작업중 · 📋대기.
> 이 파일을 기준으로 누락 없이 처리한다. 새 지시는 여기에 추가한다.
> 최종 갱신: 2026-07-12
---
## 1. 디자인 시스템 (Nifty / WISE) — 최우선 반복 지시
| # | 지시 | 상태 | 비고 |
|---|------|------|------|
| 1.1 | **Nifty 카드 타입** 적용 (https://preview.themeon.net/nifty/ui-elements/cards/) — 라운딩·그림자·헤더/바디/푸터 구조·보더 톤 | 📋대기 | `.kxp-ecard`는 8px 라운딩 있으나 Nifty 카드 구조 전면 정렬 필요. 다음 패스 |
| 1.2 | **그리드는 Nifty Advanced table headers**로 통일 (tables/static-tables) | ⏳작업중 | 정본 `.kx-table` 존재, 미준수 6화면 수렴 (감사완료) |
| 1.3 | **Tabulator 기능** 추가 (tables/tabulator — 정렬·필터·페이지·CSV) | 📋대기 | task #32 |
| 1.4 | **상세페이지 = Nifty blog** 스타일 (blog-apps/blog) | 📋대기 | task #33 |
| 1.5 | **버튼 = Nifty buttons**, **Dropdowns**, **컴포넌트**, **List Group**, **Typography/Modals/Offcanvas** = ui-elements 전체 채택 | ⏳부분 | Offcanvas 완료, 나머지 정렬 진행 |
| 1.6 | **Nifty 스타일 학습 md 생성** | ✅완료 | `docs/DESIGN_SYSTEM_NIFTY.md`·`docs/analysis/nifty-design-refs.md` |
| 1.7 | **아이콘은 선(line/stroke) SVG**로 직접 (이모지 금지) | ✅원칙 | 전 신규화면 준수 |
| 1.8 | **UI는 WISE(UIWS) 문자 그대로** — 셸·메뉴·캘린더 자체 재해석 금지 | ✅원칙 | 메모리 [[ui-follow-wise-strictly]] |
| 1.9 | **폰트를 페이지마다 안 맞춤** — 전 화면 타이포 일관화 | ⏳작업중 | 감사완료(하드코딩 262건/36파일). `--fs-micro`·공용 `.kx-page__title`·weight 토큰 신설로 정렬. 다음 패스 |
| 1.10 | **모든 경계선 엷은 회색** | ⏳부분 | 구조 보더는 neutral-200(#e4e7ec) 사용 중. 의미색·hover는 유지, 지정화면 정렬 |
| 1.11 | **좌우 2단 폭 50:50**, 전 화면 **공백 없는 반응형**(풀블리드) | ✅원칙 | task #39 스윕 대기 |
| 1.12 | **top 설정 아이콘** 추가 + **설정 레이아웃 커스터마이저**(offcanvas 테마/레이아웃/색상) | ⏳부분 | 설정 아이콘 완료, 커스터마이저 task #43 |
## 2. 캘린더
| # | 지시 | 상태 |
|---|------|------|
| 2.1 | 캘린더를 **Nifty app-views/calendar**처럼 (옅은 회색 격자·연한 today·가벼운 이벤트 칩) | 🚚배포중 | 큰 캘린더 재스타일 완료(격자 neutral-100·today 연노랑·소프트 칩), 배포 중 |
| 2.2 | **공휴일 붉은색 + 일자 옆 공휴일명** (2050년까지 데이터) | 🚚배포중 | 큰 캘린더+미니 달력 배선. 7월은 공휴일 없음(8/15·9월 추석 표시) |
| 2.3 | **달력 경계선 모두 회색** | 🚚배포중 | 미니 달력 셀 회색 테두리 추가 |
| 2.4 | 일정 **월/주/일** 뷰 (WISE CalendarView) | ✅완료 |
| 2.5 | 전시 일정 50:50 비율·달력 날짜 위로(하단 공백 제거) | ✅완료 |
## 3. 공개사이트 / 메인 / visitor
| # | 지시 | 상태 |
|---|------|------|
| 3.1 | **첫 화면 = 제품 소개** (미인증 루트는 히어로 랜딩, 로그인 폼만 노출 금지) | ✅완료 | 메모리 [[product-identity-first]] |
| 3.2 | 제품명 **'KINTEX AI 전시·행사시스템'** | ✅완료 |
| 3.3 | **visitor 메인 = 세련된 홈페이지** (AI 관람 도우미 중심) | 🚚배포중 | AiAssistant·기능타일·연월 브리핑·쇼케이스 구현 |
| 3.4 | 메인에 **AI 기능 전면 노출** (AI가 뭘 하는지 한눈에) | 🚚배포중 | AI 기능 쇼케이스 6타일 |
| 3.5 | AI가 답하는 정보는 **크롤링해서 DB 저장** (실시간 외부호출 X) | ✅완료 | V43 visitor_guide·transport, 공개 AI API 근거응답 |
| 3.6 | **AI는 전시·행사정보 학습** (근거 기반) | ✅완료 | grounding + 인용, 환각 차단 |
| 3.7 | **AI가 연간·월간 전시·행사 기획정보 제시** | 🚚배포중 | AiPlanningBriefing(연/월 토글) |
| 3.8 | 비지터가 **가장 알고 싶은 정보를 AI로 쉽게** | ✅설계 | 관람 도우미 예시칩·구조화 카드 |
| 3.9 | 코엑스 분석 → **visitor/business/agency 3-트랙** 분리 | ✅완료 | 관문 + 3 서브홈 + TrackSwitcher |
| 3.10 | **"코엑스처럼" 전면 리뉴얼** (히어로 배경·애니메이션·비주얼) | 🚚배포중 | design.md v2.4 §3C-V, 코엑스풍 비주얼·모션 구현 |
| 3.11 | 메인 카드가 **밋밋한 파란 placeholder** (포스터 이미지 없음) | ✅완료 | V46 실제 포스터 이미지로 교체(배포됨) |
| 3.12 | **메인 카드 이미지 크기 안 맞음**(세로 포스터가 16:11 가로 프레임에 늘어남/잘림) | ⏳수정됨 | 포스터 실측 세로 0.67~0.8 → 카드 미디어 3:4 프레임 + `.kxp-ecard__poster` `object-fit:cover`(왜곡 제거). 다음 배포 |
| 3.13 | **메인 히어로 이미지 5장 회전(슬라이드)** | ⏳착수 | 크롤러가 KINTEX 전시장/시설 히어로 이미지 5장 `public/media/hero/` 확보 → 프론트 히어로 회전 UI(배포 후) |
## 4. 역할 · 네비게이션
| # | 지시 | 상태 |
|---|------|------|
| 4.1 | **로그인 사용자 구분으로 메인페이지 + 메뉴 그룹 결정** (디폴트 visitor, 로그인 후 business/admin/agency) | 🚚배포중 | plan_role_routing 구현(admin→/admin·agency→/contractor·visitor→/visitor·ops/business→/home) + 역할별 메뉴 게이트 |
| 4.2 | **각 트랙 메인페이지 Stitch 디자인 의뢰** | ✅부분 | Stitch 2회 실패 → design.md 스펙 직접 구현(문서화 폴백) |
| 4.3 | **모바일 앱도 동일 로직** | 📋대기 | 웹 완료 후 모바일 반영 |
| 4.4 | **상세→메인 복귀·브레드크럼·404** 내비게이션 | ⏳ | renewal 하네스 |
## 5. 데이터 / 시드
| # | 지시 | 상태 |
|---|------|------|
| 5.1 | **모든 테이블에 데이터** (50~100건) | ✅완료 | V42 대량시드(40테이블 2,535행) |
| 5.2 | **정산·결제 데이터 없음** — 행사스코프 정렬 | 🚚배포중 | V47: 데모 해소행사 `e-2026-live`에 정산 폐루프+옥션+관람객+리드+캠페인 |
| 5.3 | 테스트 데이터 **더 현실적으로** | ✅완료 | V39·V42 실감형 |
| 5.4 | 행사정보 **크롤링해서 데이터 넣기** | ✅완료 | 크롤 시드 |
| 5.5 | **연간·월간 전시 기획정보**도 적재 | ✅완료 | V43 event 생성열·v_event_calendar 뷰 |
| 5.6 | 포스터 이미지 **다 다운받아 빌드 내장** | 🚚배포중 | V46 34장 `/media/posters/` |
## 6. 정산·도메인 화면
| # | 지시 | 상태 |
|---|------|------|
| 6.1 | **정산·결제 패딩 없음** | 🚚배포중 | `.kx-settle` 패딩 추가 |
| 6.2 | **참가업체 관리가 홈으로 감** — 전용 화면 신설 | 📋대기 | task #41 |
| 6.3 | 부서 관리 = **WISE 조직도** | ✅완료 |
| 6.4 | 결재관리 **반려·재상신·결재선** (WISE) | ✅완료 |
| 6.5 | 통합검색에 **행사·전시 포함** | ✅완료 |
| 6.6 | 시스템관리 **메시지 관리**({} 플레이스홀더+샘플) | 📋대기 | task #35 |
| 6.7 | 물류허브·서류/마일스톤·CMS·일정 = WISE | ✅완료 |
| 6.8 | **그룹권한(역할↔메뉴) 메뉴** 존재 확인 | ✅확인 | RoleAdminPage + RoleMenuAdminPage |
## 7. 버튼 · 색상 · 대비
| # | 지시 | 상태 |
|---|------|------|
| 7.1 | **파란 배경 버튼 = 흰 글자** | ✅완료 | `--color-on-accent`(다크에서도 흰색). 라이브 반영(캐시 새로고침 필요) |
| 7.2 | 다크모드 색상배경 위 **흰 글자 어두워지는 버그** 전수 수정 | ✅완료 | 77곳 on-accent. +통계밴드 2곳 추가수정(배포중) |
| 7.3 | 회색 버튼은 옅은 ghost — 흰 글자 아님(솔리드 진회색만 흰글자) | ✅확인 |
## 8. 파비콘 · 브랜드
| # | 지시 | 상태 |
|---|------|------|
| 8.1 | **KINTEX favicon** 추가 (홈페이지 크롤) | ✅완료 | 리포 favicon.ico가 톰캣 아이콘이던 것 규명→KINTEX .ico 재생성(v3) |
| 8.2 | **브랜드 밑에 영어 병기** | ✅완료 | "AI Exhibition & Event System" |
## 9. 모바일
| # | 지시 | 상태 |
|---|------|------|
| 9.1 | **내정보 (WISE 참조) + 생체인식 + 사진등록** | ✅완료 | app/profile, expo-local-authentication, expo-image-picker. 아바타 API(백엔드) 완료 |
| 9.2 | **전시·공연 앱 벤치마킹** (다운로드·좋아요 상위순) | ✅완료 | docs/analysis/mobile-benchmark-exhibition-performance.md + 백로그 12항목 |
| 9.3 | **앱 위변조 방지 + 시큐어 코딩 + 보안 체크리스트 통과** | ✅완료 | 루트/탈옥·무결성·Hermes+R8·화면캡처·cleartext·권한최소. 체크리스트/감사 문서 |
| 9.4 | **다국어** — 웹 4개국어 완비, 모바일은 한국어(백로그) | ⏳ | 웹 ✅(ko/en/zh/ja), 모바일 i18n 미도입(MB-11) |
## 10. AI
| # | 지시 | 상태 |
|---|------|------|
| 10.1 | **AI 기능 쉽게 사용 + 토큰 최소화** | ✅완료 | PLANNING v3.4 §8A(결정론 라우팅·소형모델 우선·RAG축소·캐싱·집계는 SQL·측정지표) |
| 10.2 | AI 기능이 **하나도 구현 안 된 것 같다 → 구현** | ✅완료 | 공개 AI 관람 도우미 라이브(grounded=true, Claude) |
| 10.3 | 나노바나나 유료? | ✅답변 | Google Gemini 유료 |
## 11. 데이터 아키텍처 (표준)
| # | 지시 | 상태 |
|---|------|------|
| 11.1 | **FK 최소화, 공통코드로 관리** | ✅표준 | docs/architecture/data.md §10 (소프트참조·공통코드) |
| 11.2 | **필요한 인덱스 생성** | 🚚배포중 | V44 성능 인덱스 8건(핫패스) — 이미 배포됨 |
| 11.3 | **view/mview/function 고려** | ✅표준 | data.md §11 |
| 11.4 | **자동화 배치·프로시저 고려** | ✅표준 | data.md §13 배치 카탈로그 |
| 11.5 | **ten_id/tenant_id PK 맨 앞 복합키** | ✅원칙 | 메모리 [[tenant-id-pk-standard]] |
## 12. 인증 / SSO / HR
| # | 지시 | 상태 |
|---|------|------|
| 12.1 | **직원 인사(조직) 정보는 API 방식** | ✅설계 | docs/architecture/sso-hr-integration.md (HrDirectoryClient 어댑터+스냅샷) |
| 12.2 | **Open SSO 도입** | ✅설계 | Keycloak OIDC(PKCE)+SAML, 기존 JWT+2FA 공존(브로커) |
## 13. 평면도 / 기타
| # | 지시 | 상태 |
|---|------|------|
| 13.1 | 도면 안 열림 / AI 시각화 어디감 | ✅원인 | 403(행사 미가입) → 데모계정 멤버십 해소. DWG는 SVG캔버스+JPG 렌더(DWG 바이너리는 좌표소스) |
| 13.2 | ReRoomAI 기능 (나노바나나 image-to-image) | ⏳ | visualizer 트랙 |
---
## 미해결·대기 요약 (다음 우선 처리)
1. **Nifty 디자인 시스템 전면 정렬** — 카드(ui-elements/cards)·폰트(--fs-micro·공용 제목클래스)·그리드(.kx-table 6화면) 일괄 (1.1·1.2·1.9)
2. **참가업체 관리 전용 화면** (6.2 / task #41)
3. **메시지 관리** (6.6 / task #35)
4. **Tabulator**·**상세 Nifty blog**·**설정 커스터마이저** (1.3·1.4·1.12 / task #32·33·43)
5. **전 화면 공백없는 반응형 스윕** (1.11 / task #39)
6. **모바일 역할 랜딩·다국어** (4.3·9.4)
7. **상세→메인 내비게이션** (4.4)
---
## 처리 로그 (내가 처리·배포한 것 — 2026-07-12 세션)
**배포 완료 (라이브):** 커밋 214b6df → 6e95ac6 → 87f310f/55e27b3 → 3077cb0
- V42 대량시드(40테이블 2,535행) · V43 관람객 가이드/연월 캘린더 뷰 · V44 성능 인덱스 8건 · V45 아바타 컬럼 · V46 포스터 로컬화(73건) · V47 e-2026-live 정산 폐루프
- 공개 AI 관람 도우미 API(grounded·Claude) · 연월 캘린더 API · 3-트랙 관문/서브홈 · 역할별 랜딩+메뉴 게이트
- 미니/큰 캘린더 공휴일 표시·회색 테두리·Nifty 톤 · 코엑스풍 공개사이트 비주얼(히어로·모션·통계밴드) · 실제 포스터 이미지
- 파란버튼 흰글자(`--color-on-accent`) · 다크모드 흰글자 77곳 · favicon(톰캣→KINTEX v3) · 2FA 방패 아이콘 · 브랜드 영문병기 · 정산 패딩
**구현 완료 (미배포 — 다음 배포 대기):**
- **Nifty 디자인 시스템 정렬**(카드 `.kx-card` Nifty 구조·`--fs-micro`/`--fw-*` 토큰·공용 제목클래스·테이블 `.kx-table` 6화면 수렴, font 179+weight 415 토큰화)
- **카드 이미지 3:4 프레임 + object-fit cover**(왜곡/잘림 제거)
- **메시지 관리 백엔드**(V48 sys_message + 렌더 API + 36샘플)
- **통계밴드 흰글자 2곳** · **푸터 "2026 © KINTEX..."** 4개국어
**병렬 진행 중:** 모바일 다국어+역할랜딩 · 잔여 데이터 보강(V49) · 산출물 문서
**설계·표준 문서 산출:** PLANNING v3.4(AI 토큰최소화 §8A) · design.md v2.4(공개 비주얼·모션) · docs/architecture/data.md(FK최소화·공통코드·view/mview·배치) · sso-hr-integration.md(Open SSO+HR) · DESIGN_SYSTEM_NIFTY.md · mobile-security-*.md · mobile-benchmark-*.md
## 산출물(문서) 상태
| 산출물 | 타이밍 | 상태 |
|--------|--------|------|
| 개발계획서 | 개발 전/중 | 📋 생성 착수(PLANNING v3.4 기반) |
| 설계서(아키텍처·ERD·화면·API) | 개발 중 | 📋 생성 착수(design.md v2.4·architecture·backend 계약 기반) |
| 사용자 지침서 | **완성+QA 후**(최종 메뉴 반영) | ⏳ UI 정렬 안정화 후 — 지금 생성 시 재작업(정책: 최종 1회) |
| 운영자 지침서 | **완성+QA 후** | ⏳ 동일 |
> 정책([[deliverables-update-policy]]): deliverables는 최초 1회 + 최종 1회. 개발계획서·설계서는 지금 생성 가능(내용 안정), 지침서는 현재 UI 정렬 파동이 끝난 뒤 최종 메뉴 기준으로 1회 생성.

View File

@ -18,7 +18,7 @@
| 타이포그래피 | https://preview.themeon.net/nifty/ui-elements/ (typography) | 제목·본문·캡션 위계(kx 타이포 토큰) | 대기 |
| 모달/메시지박스 | https://preview.themeon.net/nifty/ui-elements/ (modals) | 모달·확인/경고 메시지박스(kx Modal) | 대기 |
| Offcanvas | https://preview.themeon.net/nifty/ui-elements/ (offcanvas) | 오프캔버스 패널(모바일 드로어·상세 슬라이드) | 대기 |
| 달력 | https://preview.themeon.net/nifty/app-views/calendar/ | 일정 캘린더(월/주/일)·이벤트 색/드래그·사이드 목록 | 대기(일정 3-뷰는 구조 완료, Nifty 스타일 정합 대상) |
| 달력 | https://preview.themeon.net/nifty/app-views/calendar/ | 일정 캘린더(월/주/일)·이벤트 색/드래그·사이드 목록 | 완료(KxCalendarView 재스타일 2026-07-12 — 옅은 격자·옅은 노랑 today 틴트·소프트 카테고리 칩(color-mix), 미니 `.kx-cal` 동일 톤. `.kx-mcal`는 기존 소프트 톤 유지) |
| **UI-elements 전체** | **https://preview.themeon.net/nifty/ui-elements/** | **Nifty UI-elements 전 페이지를 킨텍스 공통 UI 기준으로 채택**(위 개별 + 나머지 요소 전부) | 기준 |
| 셸·메뉴(기존) | WISE(UIWS) AppLayout/LeftNav/CalendarView | 셸·아코디언·캘린더 | 완료(구조) |
| 상단바(top frame) | https://preview.themeon.net/nifty/ (navbar/header) | 상단 헤더 — 로고·검색·알림·설정·사용자 드롭다운·풀스크린·언어 | 대기 |

View File

@ -1,854 +0,0 @@
# -*- coding: utf-8 -*-
"""
킨텍스 자동전시시스템 DA(데이터 아키텍처) 산출물 생성기
1) 데이터베이스설계서.pptx (행안부 DB설계서 표준 / 16:9)
2) 테이블정의서.xlsx (실제 Flyway DDL 역추출)
내용 소스(권위) = src/backend/.../db/migration/V1~V9 실제 DDL.
테이블·컬럼·타입·NULL·PK/FK·기본값·인덱스·공간타입을 프로그램으로 파싱(수기 발명 금지).
컬럼 설명은 DDL 인라인 주석·COMMENT ON 그대로 사용. 없으면 generic 표준 컬럼만 보강.
디자인 = KINTEX Blue #0066B3 / AI Purple #6D4AFF / 맑은 고딕. ERD·도식은 python-pptx 도형 직접 작도.
보안 불변 = 비밀번호/API키/서버IP/SSH 미기재. 암호화 컬럼은 정책만 기술(AES-256-GCM).
재실행: python gen_da_deliverables.py
"""
import os, re, glob
HERE = os.path.dirname(os.path.abspath(__file__))
OUT_DIR = os.path.dirname(HERE) # docs/deliverables/DA산출물
MIG_DIR = os.path.abspath(os.path.join(
HERE, "..", "..", "..", "..", "src", "backend", "src", "main", "resources", "db", "migration"))
assert os.path.isdir(MIG_DIR), f"migration dir not found: {MIG_DIR}"
# ======================================================================
# 1. DDL 파서 (Flyway V1~V9 → 테이블 스키마 모델)
# ======================================================================
CONSTRAINT_STARTS = ("PRIMARY KEY", "UNIQUE", "FOREIGN KEY", "CHECK", "CONSTRAINT", "REFERENCES ")
GENERIC_DESC = {
"id": "식별자(PK)", "created_at": "생성일시", "updated_at": "수정일시",
"event_id": "행사 참조(FK event)", "status": "상태 코드", "sort_order": "정렬 순서",
"use_yn": "사용 여부(Y/N)", "dept_id": "부서 식별자", "writer_id": "작성자",
"writer_name": "작성자명", "author_id": "작성자", "author_name": "작성자명",
"owner_id": "소유자", "owner_name": "소유자명", "title": "제목", "content": "내용",
"read_at": "읽은 일시", "recipient_id": "수신자",
}
def _split_type(rest):
"""rest 문자열의 선두에서 데이터타입 토큰을 추출. geometry(Point,0)·numeric(8,2) 등 괄호 내부 콤마 허용."""
m = re.match(r"\s*([A-Za-z_][A-Za-z0-9_]*(?:\([^)]*\))?)", rest)
if not m:
return "", rest
return m.group(1), rest[m.end():]
def _parse_default(rest):
m = re.search(r"\bDEFAULT\s+(now\(\)|gen_random_uuid\(\)|'[^']*'|true|false|[0-9]+\.?[0-9]*|[A-Za-z_][\w]*)",
rest, re.IGNORECASE)
return m.group(1) if m else ""
def parse_ddl():
tables = {} # name -> {"cols":[...], "pk":set, "uniques":[...], "indexes":[...], "version":Vx, "order":int}
order = 0
files = sorted(glob.glob(os.path.join(MIG_DIR, "V*.sql")),
key=lambda p: int(re.match(r"V(\d+)", os.path.basename(p)).group(1)))
comments = {} # (table,col) -> desc (COMMENT ON)
for path in files:
ver = re.match(r"(V\d+)", os.path.basename(path)).group(1)
raw = open(path, encoding="utf-8").read()
# COMMENT ON COLUMN t.c IS '...'
for cm in re.finditer(r"COMMENT\s+ON\s+COLUMN\s+(\w+)\.(\w+)\s+IS\s+'((?:[^']|'')*)'", raw, re.IGNORECASE):
comments[(cm.group(1), cm.group(2))] = cm.group(3).replace("''", "'")
# CREATE TABLE IF NOT EXISTS name ( ... );
for tm in re.finditer(r"CREATE\s+TABLE\s+IF\s+NOT\s+EXISTS\s+(\w+)\s*\((.*?)\n\)\s*;",
raw, re.IGNORECASE | re.DOTALL):
tname, body = tm.group(1), tm.group(2)
if tname not in tables:
order += 1
tables[tname] = {"cols": [], "pk": set(), "uniques": [], "indexes": [],
"version": ver, "order": order}
t = tables[tname]
for line in body.split("\n"):
code = line.split("--", 1)[0].strip()
cmt = line.split("--", 1)[1].strip() if "--" in line else ""
if not code:
continue
code = code.rstrip(",").strip()
up = code.upper()
# 테이블 레벨 제약
if up.startswith("PRIMARY KEY"):
for c in re.findall(r"\((.*?)\)", code)[0].split(","):
t["pk"].add(c.strip())
continue
if up.startswith("UNIQUE"):
t["uniques"].append(re.findall(r"\((.*?)\)", code)[0].strip())
continue
if up.startswith(CONSTRAINT_STARTS):
continue
# 컬럼 정의
cm2 = re.match(r"(\w+)\s+(.*)", code)
if not cm2:
continue
cname = cm2.group(1)
dtype, rest = _split_type(cm2.group(2))
ru = rest.upper()
is_pk = "PRIMARY KEY" in ru
if is_pk:
t["pk"].add(cname)
fk = re.search(r"REFERENCES\s+(\w+)\s*\((\w+)\)", rest, re.IGNORECASE)
nn = ("NOT NULL" in ru) or is_pk
uniq = ("UNIQUE" in ru) and "PRIMARY KEY" not in ru
dflt = _parse_default(rest)
t["cols"].append({
"name": cname, "type": dtype,
"null": "N" if nn else "Y",
"pk": is_pk, "fk": (fk.group(1) if fk else None),
"fkcol": (fk.group(2) if fk else None),
"unique": uniq, "default": dflt, "desc": cmt,
})
# ALTER TABLE t ADD COLUMN IF NOT EXISTS c type ...
for am in re.finditer(r"ALTER\s+TABLE\s+(\w+)\s+ADD\s+COLUMN\s+IF\s+NOT\s+EXISTS\s+(.+?);",
raw, re.IGNORECASE):
tname = am.group(1)
if tname not in tables:
continue
t = tables[tname]
code = am.group(2).strip()
cm2 = re.match(r"(\w+)\s+(.*)", code)
if not cm2:
continue
cname = cm2.group(1)
if any(c["name"] == cname for c in t["cols"]):
continue
dtype, rest = _split_type(cm2.group(2))
ru = rest.upper()
t["cols"].append({
"name": cname, "type": dtype,
"null": "N" if "NOT NULL" in ru else "Y",
"pk": False, "fk": None, "fkcol": None,
"unique": "UNIQUE" in ru, "default": _parse_default(rest),
"desc": f"({ver} 순증 컬럼)",
})
# CREATE INDEX ... ON t ...
for im in re.finditer(r"CREATE\s+INDEX\s+IF\s+NOT\s+EXISTS\s+(\w+)\s+ON\s+(\w+)\s*(.*?);",
raw, re.IGNORECASE | re.DOTALL):
idxname, tname, spec = im.group(1), im.group(2), im.group(3).strip()
if tname in tables:
gist = "GIST" in spec.upper()
cols = re.findall(r"\((.*?)\)", spec)
tables[tname]["indexes"].append({
"name": idxname, "spec": (cols[0] if cols else spec),
"gist": gist,
})
# 설명 보강: COMMENT ON → generic
for tname, t in tables.items():
for c in t["cols"]:
if not c["desc"]:
c["desc"] = comments.get((tname, c["name"]), "")
if not c["desc"] and c["name"] in GENERIC_DESC:
c["desc"] = GENERIC_DESC[c["name"]]
return tables
TABLES = parse_ddl()
# ---- 테이블 한글명 · 주제영역 매핑 (라벨링) ----------------------------------
KO = {
"app_user": "사용자", "company": "등록업체", "event": "행사",
"hall": "전시홀 마스터", "hall_assignment": "행사-홀 배정", "event_member": "행사 참여자(RBAC)",
"booth_standard": "부스 표준사양", "master_data": "마스터데이터(요율·규정)",
"trench": "트렌치(급전 포인트)", "hall_exit": "비상구", "layout": "배치안(버전)",
"booth": "부스(폴리곤)", "design_plan": "부스 설계안", "utility_order": "유틸리티 신청",
"render_job": "AI 렌더잡", "common_code_group": "공통코드 그룹", "common_code": "공통코드",
"sys_menu": "시스템 메뉴", "sys_role": "역할", "sys_permission": "권한",
"sys_role_permission": "역할-권한 매핑", "sys_setting": "시스템 설정", "audit_log": "감사 로그",
"worklog": "업무일지", "schedule": "일정", "message": "쪽지",
"message_recipient": "쪽지 수신자", "notice": "공지", "opinion": "의견",
"opinion_comment": "의견 댓글", "meeting": "회의", "meeting_action": "회의 액션아이템",
"report": "보고서", "notification": "알림", "password_reset": "비밀번호 재설정 코드",
}
# 주제영역(Subject Area): (코드, 한글, 테이블목록, 색상키)
AREAS = [
("MASTER", "행사·조직·권한·마스터",
["event", "hall", "hall_assignment", "event_member", "app_user", "company",
"booth_standard", "master_data"]),
("SPATIAL", "공간·설계·시각화(부스 코어)",
["trench", "hall_exit", "layout", "booth", "design_plan", "utility_order", "render_job"]),
("SYSTEM", "시스템관리·인증",
["common_code_group", "common_code", "sys_menu", "sys_role", "sys_permission",
"sys_role_permission", "sys_setting", "audit_log", "password_reset"]),
("COMMON", "공통 업무 모듈",
["worklog", "schedule", "message", "message_recipient", "notice", "opinion",
"opinion_comment", "meeting", "meeting_action", "report", "notification"]),
]
AREA_OF = {}
for code, ko, tlist in AREAS:
for tn in tlist:
AREA_OF[tn] = ko
TOTAL_TABLES = len(TABLES)
TOTAL_COLS = sum(len(t["cols"]) for t in TABLES.values())
TOTAL_IDX = sum(len(t["indexes"]) for t in TABLES.values())
SPATIAL_COLS = [(tn, c["name"], c["type"]) for tn, t in TABLES.items()
for c in t["cols"] if c["type"].lower().startswith("geometry")]
FK_EDGES = [(tn, c["name"], c["fk"]) for tn, t in TABLES.items()
for c in t["cols"] if c["fk"]]
# ======================================================================
# 2. PPTX — 데이터베이스설계서
# ======================================================================
from pptx import Presentation
from pptx.util import Pt
from pptx.dml.color import RGBColor
from pptx.enum.text import PP_ALIGN, MSO_ANCHOR
from pptx.enum.shapes import MSO_SHAPE, MSO_CONNECTOR
from pptx.oxml.ns import qn
BLUE = RGBColor(0x00, 0x66, 0xB3)
BLUE_DK = RGBColor(0x00, 0x44, 0x7A)
BLUE_LT = RGBColor(0xE1, 0xEF, 0xF9)
PURPLE = RGBColor(0x6D, 0x4A, 0xFF)
PURPLE_LT = RGBColor(0xEC, 0xE8, 0xFF)
INK = RGBColor(0x10, 0x18, 0x28)
MUTED = RGBColor(0x66, 0x70, 0x85)
LINE = RGBColor(0xE4, 0xE7, 0xEC)
BG = RGBColor(0xF9, 0xFA, 0xFB)
WHITE = RGBColor(0xFF, 0xFF, 0xFF)
GREEN = RGBColor(0x12, 0x9E, 0x63)
AMBER = RGBColor(0xE0, 0x8A, 0x00)
TEAL = RGBColor(0x0E, 0x7C, 0x86)
CARD = RGBColor(0xFF, 0xFF, 0xFF)
FONT = "맑은 고딕"
IN = 914400
def inch(v): return int(v * IN)
prs = Presentation()
prs.slide_width = 12192000
prs.slide_height = 6858000
BLANK = prs.slide_layouts[6]
def slide(): return prs.slides.add_slide(BLANK)
def _fill(sp, color):
if color is None: sp.fill.background()
else:
sp.fill.solid(); sp.fill.fore_color.rgb = color
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))
_fill(sp, fill)
if line is None: sp.line.fill.background()
else:
sp.line.color.rgb = line; sp.line.width = Pt(line_w)
sp.shadow.inherit = False # creates an empty <a:effectLst/>
if shadow:
el = sp._element.spPr
ef = el.find(qn('a:effectLst'))
if ef is None:
ef = el.makeelement(qn('a:effectLst'), {}); el.append(ef)
sh = ef.makeelement(qn('a:outerShdw'),
{'blurRad':'80000','dist':'30000','dir':'5400000','rotWithShape':'0'})
clr = sh.makeelement(qn('a:srgbClr'), {'val':'101828'})
alp = clr.makeelement(qn('a:alpha'), {'val':'16000'})
clr.append(alp); sh.append(clr); ef.append(sh)
return sp
def text(s, x, y, w, h, content, size=13, 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)
segs = [{"t": para}] if isinstance(para, str) else ([para] if isinstance(para, dict) else 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()
rPr.append(rPr.makeelement(qn('a:ea'), {'typeface': FONT}))
return tb
def page_bg(s): rect(s, -0.1, -0.1, 13.53, 7.7, fill=BG)
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 header(s, kicker, title, idx):
page_bg(s)
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, "킨텍스 자동전시시스템 구축 · 데이터베이스 설계서", size=8.5, color=MUTED)
text(s, 11.3, 7.06, 1.48, 0.3, "2026-07-11", size=8.5, color=MUTED, align=PP_ALIGN.RIGHT)
def bullets(s, x, y, w, items, size=12.5, gap=0.44, head=None):
if head:
text(s, x, y, w, 0.32, head, size=12, color=PURPLE, bold=True); y += 0.42
for it in items:
rect(s, x, y+0.07, 0.12, 0.12, fill=BLUE, shape=MSO_SHAPE.OVAL)
if isinstance(it, tuple):
text(s, x+0.28, y, w-0.28, gap, [[{"t": it[0]+" ", "bold":True, "color":INK, "size":size},
{"t": it[1], "color":MUTED, "size":size-1}]], line_spacing=1.05)
else:
text(s, x+0.28, y, w-0.28, gap, it, size=size, color=INK, line_spacing=1.05)
y += gap
return y
# ---- ERD 엔티티 박스 (파싱 데이터 구동) ----
def erd_entity(s, x, y, w, tname, maxrows=6, accent=BLUE):
t = TABLES[tname]
# 표시 컬럼: PK → FK → 대표 컬럼
pk = [c for c in t["cols"] if c["pk"]]
fk = [c for c in t["cols"] if c["fk"] and not c["pk"]]
rest = [c for c in t["cols"] if not c["pk"] and not c["fk"]]
show = (pk + fk + rest)[:maxrows]
rh = 0.26
h = 0.5 + len(show)*rh
rect(s, x, y, w, h, fill=CARD, line=LINE, line_w=1.0, shape=MSO_SHAPE.ROUNDED_RECTANGLE, shadow=True)
rect(s, x, y, w, 0.44, fill=accent, shape=MSO_SHAPE.ROUND_2_SAME_RECTANGLE)
text(s, x+0.12, y, w-0.2, 0.44, [[{"t": tname, "size":10.5, "color":WHITE, "bold":True},
{"t": " "+KO.get(tname, ""), "size":8, "color":RGBColor(0xD6,0xE6,0xF6)}]],
anchor=MSO_ANCHOR.MIDDLE)
cy = y + 0.5
for c in show:
key = "PK" if c["pk"] else ("FK" if c["fk"] else "")
kc = PURPLE if c["pk"] else (TEAL if c["fk"] else MUTED)
geo = c["type"].lower().startswith("geometry")
text(s, x+0.12, cy-0.03, 0.42, rh, key, size=7.5, color=kc, bold=True, anchor=MSO_ANCHOR.MIDDLE)
text(s, x+0.54, cy-0.03, w-0.62, rh,
[[{"t": c["name"], "size":8.6, "color":(INK if key else RGBColor(0x33,0x3B,0x4A)), "bold":bool(key)},
{"t": " : "+c["type"], "size":7.6, "color":(GREEN if geo else MUTED)}]],
anchor=MSO_ANCHOR.MIDDLE)
cy += rh
return {"x": x, "y": y, "w": w, "h": h, "cx": x+w/2, "cy": y+h/2}
def connect(s, a, b, color=RGBColor(0x9A,0xA6,0xB8)):
cn = s.shapes.add_connector(MSO_CONNECTOR.STRAIGHT, inch(a["cx"]), inch(a["cy"]),
inch(b["cx"]), inch(b["cy"]))
cn.line.color.rgb = color; cn.line.width = Pt(1.4)
cn.shadow.inherit = False
return cn
# ================= 슬라이드 1 : 표지 =================
s = slide()
rect(s, -0.1, -0.1, 13.53, 7.7, fill=BLUE_DK)
rect(s, 6.4, -1.5, 9, 11, fill=BLUE, shape=MSO_SHAPE.PARALLELOGRAM)
rect(s, 9.2, -2.0, 7, 12, fill=PURPLE, shape=MSO_SHAPE.PARALLELOGRAM)
rect(s, 0.85, 0.8, 0.42, 0.42, fill=WHITE, shape=MSO_SHAPE.OVAL)
rect(s, 0.97, 0.92, 0.18, 0.18, fill=PURPLE, shape=MSO_SHAPE.OVAL)
text(s, 1.42, 0.78, 6, 0.46, "KINTEX · WISE AI", size=15, color=WHITE, bold=True, anchor=MSO_ANCHOR.MIDDLE)
chip(s, 0.85, 2.35, 1.95, 0.42, "데이터베이스 설계서", PURPLE, WHITE, size=12)
chip(s, 2.92, 2.35, 2.2, 0.42, "Database Design", RGBColor(0x2B,0x4C,0x7E), WHITE, size=10.5)
text(s, 0.85, 2.95, 11, 1.9, [
[{"t":"킨텍스 자동전시시스템", "size":44, "color":WHITE, "bold":True}],
[{"t":"데이터베이스 설계서", "size":44, "color":WHITE, "bold":True}]], line_spacing=1.02)
text(s, 0.88, 4.7, 11, 0.5, "PostgreSQL 15 + PostGIS · Flyway 형상관리 · kintex 전용 DB",
size=15.5, color=RGBColor(0xC7,0xDD,0xF2))
text(s, 0.88, 5.24, 11.4, 0.4,
"공간데이터(부스 POLYGON · 트렌치 POINT · 배선 LINESTRING) 일원화 · 행사 단위 격리 · 감사추적",
size=12, color=RGBColor(0xB9,0xC7,0xEA))
rect(s, 0.85, 6.05, 11.63, 0.02, fill=RGBColor(0x3A,0x5A,0x8C))
text(s, 0.85, 6.25, 8.6, 0.4, [[{"t":"산출물 ","size":11,"color":RGBColor(0x90,0xA6,0xC9)},
{"t":f"개념·논리·물리 데이터 모델 / 테이블 {TOTAL_TABLES} · 컬럼 {TOTAL_COLS} · 인덱스 {TOTAL_IDX}","size":11,"color":WHITE,"bold":True}]])
text(s, 0.85, 6.62, 9.6, 0.4, [[{"t":"근거 ","size":11,"color":RGBColor(0x90,0xA6,0xC9)},
{"t":"Flyway V1~V10 실제 DDL 역추출 (스키마 권위)","size":10.5,"color":WHITE}]])
text(s, 9.7, 6.25, 2.78, 0.4, "작성일 2026-07-11", size=11, color=WHITE, bold=True, align=PP_ALIGN.RIGHT)
text(s, 9.7, 6.62, 2.78, 0.4, "DA · kintex-da", size=9.5, color=RGBColor(0x90,0xA6,0xC9), align=PP_ALIGN.RIGHT)
# ================= 슬라이드 2 : 목차 =================
s = slide()
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)
toc = [
("01","개요","PostgreSQL+PostGIS · Flyway · kintex 전용 DB"),
("02","데이터 아키텍처 원칙","행사 단위 격리 · 공간타입 선정 · 감사로그"),
("03","개념 데이터 모델","핵심 주제영역 관계 개요도"),
("04","논리 모델(ERD)","주제영역별 엔터티·관계 ERD"),
("05","물리 모델 요약","테이블 그룹 · 공간컬럼 · 인덱스 전략"),
("06","데이터 표준","명명 규칙 · 공통코드 · 날짜/통화"),
("07","데이터 보안","암호화·PII·응답제외·감사추적"),
("08","백업·이관 전략","Flyway 마이그레이션 운영 원칙"),
]
cw = 5.85
for i,(no,t,d) in enumerate(toc):
col = i // 4; row = i % 4
x = 0.7 + col*(cw+0.35); y = 1.62 + row*1.28
rect(s, x, y, cw, 1.1, fill=CARD, line=LINE, line_w=1.0, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
rect(s, x, y, 0.09, 1.1, fill=BLUE if col==0 else PURPLE, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
text(s, x+0.28, y, 1.0, 1.1, no, size=26, color=(BLUE if col==0 else PURPLE), bold=True, anchor=MSO_ANCHOR.MIDDLE)
text(s, x+1.3, y+0.24, cw-1.5, 0.4, t, size=15, color=INK, bold=True)
text(s, x+1.3, y+0.64, cw-1.5, 0.34, d, size=9.5, color=MUTED)
# ================= 슬라이드 3 : 01 개요 =================
s = slide()
header(s, "OVERVIEW", "01. 개요 — DBMS · 형상관리 · DB 구성", 1)
text(s, 0.55, 1.45, 12.2, 0.5,
"킨텍스 자동전시시스템은 전시 생애주기(판매→AI 설계·시각화→공사 옥션→운영→경영분석) 전 과정을 단일 DB에 담는다. "
"공간데이터를 애플리케이션과 분리하지 않고 PostGIS로 DB에 일원화하여 배치·배선·규정검증을 ST_* 연산으로 수행한다.",
size=12, color=INK, line_spacing=1.12)
# 좌: 스택 카드
cards = [
("DBMS", "PostgreSQL 15", "관계형 + 트랜잭션 정합", BLUE),
("공간확장", "PostGIS", "POLYGON·POINT·LINESTRING 지오메트리", GREEN),
("암호화", "pgcrypto", "gen_random_uuid() · 해시", PURPLE),
("형상관리", "Flyway", "V1~V10 버전 마이그레이션(불변·순증)", AMBER),
]
for i,(k,v,d,c) in enumerate(cards):
x = 0.55 + i*3.06
card(s, x, 2.2, 2.86, 1.35)
rect(s, x, 2.2, 2.86, 0.1, fill=c, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
text(s, x+0.22, 2.4, 2.5, 0.3, k, size=10, color=c, bold=True)
text(s, x+0.22, 2.72, 2.5, 0.4, v, size=15, color=INK, bold=True)
text(s, x+0.22, 3.16, 2.5, 0.34, d, size=9, color=MUTED, line_spacing=1.05)
# 통계 스트립
stats = [(f"{TOTAL_TABLES}", "테이블"), (f"{TOTAL_COLS}", "컬럼"), (f"{len(AREAS)}", "주제영역"),
(f"{len(SPATIAL_COLS)}", "공간 컬럼"), (f"{TOTAL_IDX}", "인덱스"), ("V1V10", "Flyway")]
for i,(v,l) in enumerate(stats):
x = 0.55 + i*2.03
rect(s, x, 3.85, 1.9, 1.0, fill=BLUE_LT, line=LINE, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
text(s, x, 4.0, 1.9, 0.44, v, size=23, color=BLUE_DK, bold=True, align=PP_ALIGN.CENTER)
text(s, x, 4.5, 1.9, 0.3, l, size=10, color=MUTED, align=PP_ALIGN.CENTER)
# DB 구성 원칙
bullets(s, 0.55, 5.15, 12.2, [
("kintex 전용 DB", "GUARDiA 타 솔루션과 물리 DB 분리(kintex 전용 스키마). 접속·권한은 서비스 계정 최소권한."),
("공간·업무 단일 저장", "부스 폴리곤·배선 라인·요율·관람 데이터를 한 트랜잭션 경계에서 일관 관리."),
("멱등·순증 마이그레이션", "V7 이후는 CREATE IF NOT EXISTS·ADD COLUMN IF NOT EXISTS로 재적용 안전(운영 무중단)."),
], size=11.5, gap=0.5)
# ================= 슬라이드 4 : 02 아키텍처 원칙 =================
s = slide()
header(s, "PRINCIPLES", "02. 데이터 아키텍처 원칙", 2)
princ = [
("행사 단위 격리(멀티테넌시)", BLUE,
["물리 tenant_id 컬럼 대신 event_id + event_member(RBAC)로 논리 격리",
"핵심 트랜잭션 테이블은 event_id FK·ON DELETE CASCADE로 행사 경계 정합",
"hall·company·master_data 등 마스터는 전 행사 공유(참조 무결성)"]),
("공간데이터 PostGIS 일원화", GREEN,
["부스=POLYGON, 트렌치·비상구=POINT, 배선=MultiLineString (SRID 0 홀 로컬 m)",
"GiST 공간 인덱스로 ST_Intersects/Area/Distance 고속 연산",
"배치·배선 재저장 대비 booth_id 소프트 참조(감사·쿼터 보존)"]),
("감사 추적·이력 보존", PURPLE,
["audit_log에 actor·action·target·ruleset_version·result 기록",
"render_job은 이력·쿼터 정본(부스 교체와 무관하게 감사 보존)",
"layout·design_plan·booth_standard 버전(version) 관리"]),
("보안 내재화", AMBER,
["password_hash·otp_secret·code_hash 컬럼은 API 응답/로그 완전 제외",
"민감 시크릿 설정은 sys_setting.secret_yn='Y'로 마스킹",
"error_message는 사용자 요약만(스택트레이스 저장 금지)"]),
]
for i,(title,c,items) in enumerate(princ):
x = 0.55 + (i%2)*6.15; y = 1.5 + (i//2)*2.6
card(s, x, y, 5.95, 2.4)
rect(s, x, y, 0.11, 2.4, fill=c, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
text(s, x+0.35, y+0.2, 5.4, 0.4, title, size=14, color=INK, bold=True)
yy = y+0.72
for it in items:
rect(s, x+0.4, yy+0.06, 0.1, 0.1, fill=c, shape=MSO_SHAPE.OVAL)
text(s, x+0.62, yy, 5.15, 0.5, it, size=10.3, color=MUTED, line_spacing=1.05)
yy += 0.52
# ================= 슬라이드 5 : 03 개념 데이터 모델 =================
s = slide()
header(s, "CONCEPTUAL", "03. 개념 데이터 모델 — 주제영역 관계 개요", 3)
text(s, 0.55, 1.42, 12.2, 0.4,
"구현된 4개 주제영역과 소유 데이터의 관계. 행사(EVENT)를 축으로 공간·설계 코어와 공통·시스템 레이어가 결합한다.",
size=11.5, color=MUTED)
# 중앙 EVENT 허브
hub = {"cx":6.6, "cy":3.9}
rect(s, 5.75, 3.3, 1.7, 1.2, fill=BLUE_DK, shape=MSO_SHAPE.ROUNDED_RECTANGLE, shadow=True)
text(s, 5.75, 3.3, 1.7, 1.2, [[{"t":"EVENT","size":15,"color":WHITE,"bold":True}],
[{"t":"행사(테넌트 축)","size":9,"color":RGBColor(0xC7,0xDD,0xF2)}]], align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)
sa = [
("MASTER","행사·조직·권한·마스터","event · hall · company · event_member",8,BLUE, 0.7, 1.95),
("SPATIAL","공간·설계·시각화","layout · booth · utility_order · render_job",7,GREEN, 9.35, 1.95),
("COMMON","공통 업무 모듈","worklog · schedule · notice · meeting …",11,PURPLE, 0.7, 4.75),
("SYSTEM","시스템관리·인증","sys_* · common_code · audit_log · password_reset",9,AMBER, 9.35, 4.75),
]
for code,ko,ex,n,c,x,y in sa:
# 연결선 (먼저)
box_cx = x+1.6; box_cy = y+0.75
cn = s.shapes.add_connector(MSO_CONNECTOR.STRAIGHT, inch(box_cx), inch(box_cy), inch(hub["cx"]), inch(hub["cy"]))
cn.line.color.rgb = RGBColor(0xC2,0xCC,0xDA); cn.line.width = Pt(1.6); cn.shadow.inherit=False
for code,ko,ex,n,c,x,y in sa:
card(s, x, y, 3.2, 1.5)
rect(s, x, y, 3.2, 0.12, fill=c, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
text(s, x+0.24, y+0.24, 2.7, 0.34, ko, size=13, color=INK, bold=True)
chip(s, x+2.45, y+0.24, 0.62, 0.3, f"{n}T", c, WHITE, size=9)
text(s, x+0.24, y+0.68, 2.9, 0.7, ex, size=9.3, color=MUTED, line_spacing=1.08)
text(s, 0.55, 6.5, 12.2, 0.4,
"관계 요지: EVENT 1─N HallAssignment/EventMember/Layout/Notice/Worklog(event_id). Layout 1─N Booth(POLYGON) 1─N DesignPlan·UtilityOrder(배선)·RenderJob. Company·User ─ EventMember로 행사 RBAC 성립.",
size=9.8, color=INK, line_spacing=1.1)
# ================= 슬라이드 6~9 : 04 논리 모델 ERD (주제영역별) =================
def erd_slide(area_ko, idx, placements, extra_note, cross_notes=None):
s = slide()
header(s, "LOGICAL ERD", f"04. 논리 모델 — {area_ko}", 4)
# 연결선 먼저(엔티티 아래로 깔림)
boxes = {}
# 1차: 좌표만 계산해 중심 확보
for tn, x, y, w in placements:
t = TABLES[tn]
pk=[c for c in t["cols"] if c["pk"]]; fk=[c for c in t["cols"] if c["fk"] and not c["pk"]]
rest=[c for c in t["cols"] if not c["pk"] and not c["fk"]]
show=(pk+fk+rest); show=show[:6]
h = 0.5+len(show)*0.26
boxes[tn]={"x":x,"y":y,"w":w,"h":h,"cx":x+w/2,"cy":y+h/2}
for tn,_,_,_ in placements:
for c in TABLES[tn]["cols"]:
if c["fk"] and c["fk"] in boxes and c["fk"]!=tn:
connect(s, boxes[tn], boxes[c["fk"]])
accents={"MASTER":BLUE,"SPATIAL":GREEN,"SYSTEM":AMBER,"COMMON":PURPLE}
acc = accents.get(idx, BLUE)
for tn,x,y,w in placements:
erd_entity(s, x, y, w, tn, accent=acc)
if extra_note:
text(s, 0.55, 6.78, 12.2, 0.3, extra_note, size=9, color=MUTED, line_spacing=1.05)
return s
# ERD-1 MASTER
erd_slide("행사·조직·권한·마스터", "MASTER", [
("event", 5.6, 1.5, 2.5),
("hall", 9.9, 1.5, 2.8),
("hall_assignment", 9.9, 4.05, 2.8),
("event_member", 5.6, 3.7, 2.85),
("app_user", 0.6, 1.5, 2.75),
("company", 0.6, 4.55, 2.75),
], "관계: event 1─N hall_assignment·event_member. app_user·company ─ event_member(행사 RBAC). 독립 마스터 booth_standard·master_data 는 물리 모델 목록(슬라이드 10) 참조.")
# ERD-2 SPATIAL
erd_slide("공간·설계·시각화(부스 코어)", "SPATIAL", [
("hall", 0.6, 1.5, 2.5),
("trench", 0.6, 4.2, 2.5),
("hall_exit", 3.45, 4.2, 2.4),
("layout", 3.6, 1.5, 2.5),
("booth", 6.55, 1.5, 2.6),
("design_plan", 9.65, 1.5, 2.7),
("utility_order", 6.55, 4.35,2.9),
("render_job", 9.85, 4.35,2.75),
], "관계: hall 1─N trench·hall_exit·layout. layout 1─N booth(POLYGON) 1─N design_plan. booth ─ utility_order(배선 LineString)·render_job(AI 샷). booth_id 소프트 참조.")
# ERD-3 SYSTEM
erd_slide("시스템관리·인증", "SYSTEM", [
("common_code_group", 0.6, 1.5, 2.7),
("common_code", 0.6, 4.0, 2.7),
("sys_role", 3.6, 1.5, 2.6),
("sys_role_permission",3.6,4.0, 2.75),
("sys_permission", 6.55, 1.5, 2.6),
("sys_menu", 9.6, 1.5, 2.8),
("sys_setting", 6.55, 4.05, 2.6),
("audit_log", 9.6, 4.05, 2.8),
], "관계: common_code_group 1─N common_code. sys_role ─ sys_role_permission ─ sys_permission(M:N). sys_menu 자기참조(parent_id). password_reset(공개 인증)은 별도. audit_log 전 모듈 공유.")
# ERD-4 COMMON
s = slide()
header(s, "LOGICAL ERD", "04. 논리 모델 — 공통 업무 모듈", 4)
# 부모-자식 3쌍 + 독립 목록
pairs = [("message","message_recipient"),("opinion","opinion_comment"),("meeting","meeting_action")]
px = 0.6
for i,(p,ch) in enumerate(pairs):
x = 0.6 + i*4.15
bp = erd_entity(s, x, 1.5, 3.0, p, accent=PURPLE)
bc = erd_entity(s, x+0.3, 3.9, 3.0, ch, accent=RGBColor(0x8B,0x6B,0xE0))
# 연결(부모→자식) 먼저 그리지 못했으니 라벨로 대체
text(s, 0.6, 6.05, 11.9, 0.3,
"부모 1─N 자식(FK·ON DELETE CASCADE): message→message_recipient · opinion→opinion_comment · meeting→meeting_action",
size=9.5, color=TEAL, bold=True)
# 독립 테이블 스트립
indep = ["worklog","schedule","notice","report","notification"]
text(s, 0.6, 6.42, 3, 0.3, "행사 스코프 독립 테이블", size=10, color=MUTED, bold=True)
for i,tn in enumerate(indep):
x = 3.0 + i*1.85
rect(s, x, 6.4, 1.75, 0.4, fill=BLUE_LT, line=LINE, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
text(s, x, 6.4, 1.75, 0.4, tn, size=9.5, color=BLUE_DK, bold=True, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)
# ================= 슬라이드 10 : 05 물리 모델 요약 (테이블 그룹) =================
s = slide()
header(s, "PHYSICAL", "05. 물리 모델 요약 — 테이블 그룹", 5)
col_x = [0.55, 6.7]
for gi,(code,ko,tlist) in enumerate(AREAS):
x = col_x[gi%2]; y = 1.5 + (gi//2)*2.65
card(s, x, y, 5.9, 2.45)
acc = {"MASTER":BLUE,"SPATIAL":GREEN,"SYSTEM":AMBER,"COMMON":PURPLE}[code]
rect(s, x, y, 5.9, 0.5, fill=acc, shape=MSO_SHAPE.ROUND_2_SAME_RECTANGLE)
text(s, x+0.25, y, 4.8, 0.5, ko, size=13, color=WHITE, bold=True, anchor=MSO_ANCHOR.MIDDLE)
text(s, x+5.0, y, 0.7, 0.5, f"{len(tlist)}T", size=13, color=WHITE, bold=True, align=PP_ALIGN.RIGHT, anchor=MSO_ANCHOR.MIDDLE)
yy = y+0.62
for j,tn in enumerate(tlist):
cx = x+0.25 + (j%2)*2.9
cyy = yy + (j//2)*0.315
text(s, cx, cyy, 2.85, 0.3, [[{"t":"· ","color":acc,"bold":True,"size":10},
{"t":tn,"size":9.3,"color":INK,"bold":True},
{"t":" "+KO.get(tn,""),"size":7.8,"color":MUTED}]], anchor=MSO_ANCHOR.MIDDLE)
# ================= 슬라이드 11 : 05b 공간 컬럼·인덱스 전략 =================
s = slide()
header(s, "PHYSICAL — SPATIAL & INDEX", "05. 공간 컬럼 · 인덱스 전략", 5)
# 공간 컬럼 표
text(s, 0.55, 1.45, 6, 0.32, "공간(PostGIS) 컬럼", size=13, color=GREEN, bold=True)
sp_rows = [("테이블","컬럼","지오메트리","용도")]
sp_meta = {
("trench","geom"):"급전/급수/네트워크 공급 포인트",
("hall_exit","geom"):"비상구 위치(이격 버퍼 검증)",
("booth","geom"):"부스 경계 폴리곤(면적·간섭)",
("utility_order","wiring"):"전기/네트워크/급배수 배선 경로",
}
for tn,cn,ty in SPATIAL_COLS:
sp_rows.append((tn, cn, ty, sp_meta.get((tn,cn),"")))
ry = 1.85
for i,row in enumerate(sp_rows):
fill = BLUE if i==0 else (WHITE if i%2 else BLUE_LT)
tc = WHITE if i==0 else INK
rect(s, 0.55, ry, 6.1, 0.4, fill=fill, line=LINE)
xs=[0.65,2.05,3.35,4.75]; ws=[1.35,1.25,1.4,1.85]
for k,val in enumerate(row):
text(s, xs[k], ry, ws[k], 0.4, val, size=8.6, color=(tc if k<3 else (tc if i==0 else MUTED)),
bold=(i==0 or k==0), anchor=MSO_ANCHOR.MIDDLE)
ry += 0.4
text(s, 0.55, ry+0.1, 6.1, 0.6,
"SRID 0 = 홀 로컬 평면(미터). ST_Area/Distance/Length 결과 단위 = m·㎡.",
size=9, color=MUTED, line_spacing=1.1)
# 인덱스 전략
text(s, 6.95, 1.45, 5.8, 0.32, "인덱스 전략", size=13, color=BLUE, bold=True)
idx_items = [
("GiST 공간 인덱스", f"geometry 컬럼 4종(booth·trench·hall_exit·utility_order.wiring) 전부 GiST"),
("B-Tree 조회 인덱스", "FK·정렬 축: writer_id+work_date, recipient_id, event_id 등"),
("복합 인덱스", "render_job(booth_id,shot_preset) · (event_id,status) 쿼터·상태 조회"),
("정렬 최적화", "audit_log(created_at DESC) · notice(event_id,pinned DESC,created_at DESC)"),
(f"총 인덱스 {TOTAL_IDX}", "PK 자동 인덱스 별도 · 공간 4 · B-Tree 다수"),
]
yy = 1.9
for k,v in idx_items:
card(s, 6.95, yy, 5.75, 0.78)
text(s, 7.15, yy+0.12, 5.4, 0.3, k, size=11, color=INK, bold=True)
text(s, 7.15, yy+0.44, 5.4, 0.3, v, size=8.8, color=MUTED, line_spacing=1.05)
yy += 0.9
# ================= 슬라이드 12 : 06 데이터 표준 =================
s = slide()
header(s, "STANDARDS", "06. 데이터 표준 — 명명 · 코드 · 날짜/통화", 6)
std = [
("명명 규칙(실제 DDL 관찰)", BLUE, [
"테이블·컬럼 = snake_case 단수형 (app_user, booth, render_job)",
"PK = id (varchar 애플리케이션 채번) / 대량 로그 = bigserial (audit_log·password_reset)",
"FK = <참조테이블>_id (event_id, booth_id, hall_id, layout_id)",
"불리언 = is_/has_/*_yn 혼용 · 금액/치수 = numeric(정밀도,스케일)",
"타임스탬프 = *_at (timestamptz) / 날짜 = *_date (date)",
]),
("공통코드 체계(common_code)", PURPLE, [
"grp_code(그룹) 1─N code — 14개 그룹 시드(USE_YN·USER_ROLE·WORK_STATUS …)",
"화면 드롭다운·상태값을 코드로 표준화 (하드코딩 배제)",
"정렬 sort_order · 사용여부 use_yn(Y/N) · 확장 attr1",
]),
("날짜·통화·단위 표준", GREEN, [
"일시 = timestamptz(UTC 저장, 표시 KST) · 날짜 = date",
"통화 = KRW 정수(원) 원칙, 요율/정산은 numeric — 부동소수 금지",
"치수 = 미터(m)·면적 ㎡ · 하중 t/㎡ (numeric 고정 스케일)",
]),
]
yy = 1.5
for title,c,items in std:
h = 0.5 + len(items)*0.34 + 0.15
card(s, 0.55, yy, 12.2, h)
rect(s, 0.55, yy, 0.11, h, fill=c, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
text(s, 0.85, yy+0.16, 11.6, 0.34, title, size=13.5, color=INK, bold=True)
iy = yy+0.6
for it in items:
rect(s, 0.95, iy+0.06, 0.09, 0.09, fill=c, shape=MSO_SHAPE.OVAL)
text(s, 1.14, iy, 11.4, 0.32, it, size=10.3, color=MUTED)
iy += 0.34
yy += h + 0.16
# ================= 슬라이드 13 : 07 데이터 보안 =================
s = slide()
header(s, "SECURITY", "07. 데이터 보안 — 암호화 · PII · 응답제외 · 감사", 7)
# 응답 제외/암호화 컬럼 표
text(s, 0.55, 1.45, 7, 0.32, "민감 컬럼 처리 정책", size=13, color=BLUE, bold=True)
sec_rows = [("테이블.컬럼","정책")]
sec_rows += [
("app_user.password_hash","BCrypt 해시 · API 응답/로그 완전 제외"),
("app_user.otp_secret","TOTP 시크릿 · AES-256-GCM 암호화 저장 · 응답 제외"),
("password_reset.code_hash","BCrypt(6자리) · 만료 10분·1회성 · 응답/로그 제외"),
("sys_setting.secret_yn='Y'","시크릿 설정값 마스킹 반환"),
("render_job.error_message","사용자 요약만 · 스택트레이스 저장 금지"),
("booth.assigned_company_name","표시용(내부 식별자·민감정보 미포함)"),
]
ry = 1.85
for i,(a,b) in enumerate(sec_rows):
fill = BLUE if i==0 else (WHITE if i%2 else BLUE_LT)
tc = WHITE if i==0 else INK
rect(s, 0.55, ry, 6.5, 0.52, fill=fill, line=LINE)
text(s, 0.68, ry, 2.75, 0.52, a, size=8.4, color=tc, bold=(i==0), anchor=MSO_ANCHOR.MIDDLE)
text(s, 3.5, ry, 3.45, 0.52, b, size=8.3, color=(tc if i==0 else MUTED), bold=(i==0),
anchor=MSO_ANCHOR.MIDDLE, line_spacing=1.0)
ry += 0.52
# 우측 원칙 카드
text(s, 7.3, 1.45, 5.4, 0.32, "보안 불변 원칙", size=13, color=PURPLE, bold=True)
sec_cards = [
("PII 최소화·격리", "관람/리드 PII 집중영역 접근 제한 · BI 마트는 집계/익명키만 반입(개인식별자 0)"),
("암호화 저장", "비밀번호·OTP·재설정코드는 해시/AES-256-GCM · 평문 저장 금지"),
("API 응답 스키마 통제", "ServerOut류 민감 컬럼 응답 완전 제외 · 스택트레이스 미노출"),
("감사 추적", "audit_log(actor·action·target·ruleset_version·result·ip_hint) 전 변경 기록"),
("행사 격리 접근제어", "event_member 역할(ORGANIZER·EXHIBITOR·CONTRACTOR·HALL_MANAGER) 스코프"),
]
yy = 1.85
for k,v in sec_cards:
card(s, 7.3, yy, 5.45, 0.92)
rect(s, 7.3, yy, 0.09, 0.92, fill=PURPLE, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
text(s, 7.55, yy+0.13, 5.1, 0.3, k, size=11, color=INK, bold=True)
text(s, 7.55, yy+0.45, 5.1, 0.42, v, size=8.7, color=MUTED, line_spacing=1.05)
yy += 1.02
# ================= 슬라이드 14 : 08 백업·이관 전략 =================
s = slide()
header(s, "BACKUP & MIGRATION", "08. 백업 · 이관 전략 (Flyway 형상관리)", 8)
mig = [
("Flyway 버전 마이그레이션", BLUE, [
"V1 확장 → V2 신원·마스터 → V3 공간코어 → V4~V6 시드 → V7 시스템관리 → V8 공통업무 → V9 공개인증 → V10 10년 시드",
"적용 이력은 flyway_schema_history 테이블에 체크섬으로 기록·검증",
"V1~V6 불변 원칙 · V7 이후 순증(additive)·멱등만 수행(운영 무중단)",
]),
("백업 전략", GREEN, [
"일 1회 논리 백업(pg_dump) + WAL 아카이브 기반 PITR(시점복구)",
"PostGIS 지오메트리·공간 인덱스 포함 전체 스키마 백업",
"오브젝트 스토리지(렌더 이미지·도면)는 서명·만료 URL로 별도 보존",
]),
("이관·복구 절차", PURPLE, [
"복구 = 최신 백업 복원 → Flyway migrate로 최신 버전까지 재적용(정본 순서 보장)",
"환경 승격(dev→운영): 동일 마이그레이션 스크립트 적용으로 스키마 동형 보장",
"시드(V4~V6·V10)와 스키마(V1~V3·V7~V9) 분리로 데이터 오염 없이 재구축",
]),
]
yy = 1.5
for title,c,items in mig:
h = 0.5 + len(items)*0.4 + 0.1
card(s, 0.55, yy, 12.2, h)
rect(s, 0.55, yy, 0.11, h, fill=c, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
text(s, 0.85, yy+0.16, 11.6, 0.34, title, size=13.5, color=INK, bold=True)
iy = yy+0.62
for it in items:
rect(s, 0.95, iy+0.07, 0.09, 0.09, fill=c, shape=MSO_SHAPE.OVAL)
text(s, 1.14, iy, 11.4, 0.38, it, size=10.2, color=MUTED, line_spacing=1.08)
iy += 0.4
yy += h + 0.16
text(s, 0.55, 6.75, 12.2, 0.3,
"보안 불변: 본 설계서·백업 매뉴얼에 비밀번호·API키·서버 IP·SSH 접속정보를 기재하지 않는다.",
size=9, color=MUTED, bold=True)
# ---- 저장 ----
PPTX_PATH = os.path.join(OUT_DIR, "데이터베이스설계서.pptx")
prs.save(PPTX_PATH)
N_SLIDES = len(prs.slides._sldIdLst)
# ======================================================================
# 3. XLSX — 테이블정의서
# ======================================================================
import openpyxl
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
from openpyxl.utils import get_column_letter
BLUE_HEX = "0066B3"; BLUEDK_HEX = "00447A"; BAND_HEX = "E1EFF9"; ROW_HEX = "F4F8FC"
thin = Side(style="thin", color="D0D7E2")
BORDER = Border(left=thin, right=thin, top=thin, bottom=thin)
HFONT = Font(name="맑은 고딕", size=10, bold=True, color="FFFFFF")
BFONT = Font(name="맑은 고딕", size=10)
BOLD = Font(name="맑은 고딕", size=10, bold=True, color="101828")
TITLE_FONT = Font(name="맑은 고딕", size=11, bold=True, color="FFFFFF")
HEAD_FILL = PatternFill("solid", fgColor=BLUE_HEX)
BAND_FILL = PatternFill("solid", fgColor=BLUEDK_HEX)
ALT_FILL = PatternFill("solid", fgColor=ROW_HEX)
CEN = Alignment(horizontal="center", vertical="center", wrap_text=True)
LEFT = Alignment(horizontal="left", vertical="center", wrap_text=True)
wb = openpyxl.Workbook()
# ---- 시트1: 테이블목록 ----
ws = wb.active; ws.title = "테이블목록"
ws.sheet_view.showGridLines = False
cols1 = [("No",6),("테이블명",22),("한글명",24),("주제영역",24),("Flyway",10),("컬럼수",8),("비고",40)]
for j,(h,w) in enumerate(cols1, start=1):
c = ws.cell(1, j, h); c.font=HFONT; c.fill=HEAD_FILL; c.alignment=CEN; c.border=BORDER
ws.column_dimensions[get_column_letter(j)].width = w
ordered = sorted(TABLES.items(), key=lambda kv: kv[1]["order"])
for i,(tn,t) in enumerate(ordered, start=1):
note = []
if any(c["type"].lower().startswith("geometry") for c in t["cols"]): note.append("PostGIS 공간")
if t["pk"] and len(t["pk"])>1: note.append("복합 PK("+",".join(sorted(t["pk"]))+")")
if t["uniques"]: note.append("UNIQUE("+"; ".join(t["uniques"])+")")
if any(c["name"] in ("password_hash","otp_secret","code_hash") for c in t["cols"]): note.append("민감:응답제외")
row = [i, tn, KO.get(tn,""), AREA_OF.get(tn,""), t["version"], len(t["cols"]), " · ".join(note)]
for j,val in enumerate(row, start=1):
c = ws.cell(i+1, j, val)
c.font = BOLD if j==2 else BFONT
c.alignment = CEN if j in (1,5,6) else LEFT
c.border = BORDER
if i%2==0: c.fill = ALT_FILL
ws.freeze_panes = "A2"
ws.auto_filter.ref = f"A1:G{len(ordered)+1}"
# ---- 주제영역별 시트 ----
DATA_COLS = [("No",5),("컬럼명",26),("데이터타입",20),("NULL",7),("",13),("기본값",18),("설명",56)]
def key_str(c):
parts=[]
if c["pk"]: parts.append("PK")
if c["fk"]: parts.append(f"FK→{c['fk']}")
if c["unique"]: parts.append("U")
return " ".join(parts)
for code, ko, tlist in AREAS:
title = ko.split("(")[0].strip()
sname = {"MASTER":"마스터·조직·권한","SPATIAL":"공간·설계·시각화",
"SYSTEM":"시스템관리·인증","COMMON":"공통업무"}[code]
ws = wb.create_sheet(sname)
ws.sheet_view.showGridLines = False
for j,(h,w) in enumerate(DATA_COLS, start=1):
c = ws.cell(1, j, h); c.font=HFONT; c.fill=HEAD_FILL; c.alignment=CEN; c.border=BORDER
ws.column_dimensions[get_column_letter(j)].width = w
r = 2
ncol = len(DATA_COLS)
for tn in tlist:
t = TABLES[tn]
# 밴드 행(테이블 헤더)
ws.merge_cells(start_row=r, start_column=1, end_row=r, end_column=ncol)
bc = ws.cell(r, 1, f"{tn}{KO.get(tn,'')} [{t['version']}]"
+ (" · 인덱스 "+", ".join(ix["name"] for ix in t["indexes"]) if t["indexes"] else ""))
bc.font = TITLE_FONT; bc.fill = BAND_FILL; bc.alignment = LEFT
for j in range(1, ncol+1):
ws.cell(r, j).border = BORDER
ws.cell(r, j).fill = BAND_FILL
r += 1
for k,c in enumerate(t["cols"], start=1):
vals = [k, c["name"], c["type"], c["null"], key_str(c), c["default"], c["desc"]]
for j,val in enumerate(vals, start=1):
cc = ws.cell(r, j, val)
cc.font = BOLD if (j==2 and c["pk"]) else BFONT
cc.alignment = CEN if j in (1,4) else LEFT
cc.border = BORDER
if k%2==0: cc.fill = ALT_FILL
if j==5 and c["fk"]:
cc.font = Font(name="맑은 고딕", size=9, color="0E7C86", bold=True)
r += 1
r += 1 # 테이블 간 공백
ws.freeze_panes = "A2"
XLSX_PATH = os.path.join(OUT_DIR, "테이블정의서.xlsx")
wb.save(XLSX_PATH)
print("=" * 60)
print(f"[PPTX] {PPTX_PATH}")
print(f" 슬라이드 {N_SLIDES}")
print(f"[XLSX] {XLSX_PATH}")
print(f" 시트 {len(wb.sheetnames)} : {wb.sheetnames}")
print(f"[SCHEMA] 테이블 {TOTAL_TABLES} · 컬럼 {TOTAL_COLS} · 인덱스 {TOTAL_IDX} · 공간컬럼 {len(SPATIAL_COLS)} · FK {len(FK_EDGES)}")
print("=" * 60)

View File

@ -1,70 +0,0 @@
# -*- coding: utf-8 -*-
"""공용 docx 헬퍼 — 한글 폰트/표/제목 스타일. 각 문서 생성기가 import."""
from docx.shared import Pt, Cm, RGBColor
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.enum.table import WD_TABLE_ALIGNMENT
from docx.oxml.ns import qn
from docx.oxml import OxmlElement
NAVY = RGBColor(0x1F, 0x29, 0x3A)
ACCENT = RGBColor(0x11, 0x6D, 0xC3)
GREY = RGBColor(0x55, 0x5F, 0x6B)
KFONT = "맑은 고딕"
def setup(doc):
st = doc.styles["Normal"]; st.font.name = KFONT; st.font.size = Pt(10.5)
_rf(st.element.get_or_add_rPr())
for m in ("Heading 1","Heading 2","Heading 3"):
doc.styles[m].font.name = KFONT; _rf(doc.styles[m].element.get_or_add_rPr())
s = doc.sections[0]
s.top_margin = Cm(2.2); s.bottom_margin = Cm(2.0); s.left_margin = Cm(2.3); s.right_margin = Cm(2.3)
def _rf(rpr):
rf = rpr.find(qn('w:rFonts'))
if rf is None:
rf = OxmlElement('w:rFonts'); rpr.append(rf)
for a in ('w:ascii','w:hAnsi','w:eastAsia','w:cs'):
rf.set(qn(a), KFONT)
def shade(cell, hexcolor):
tcpr = cell._tc.get_or_add_tcPr()
sh = OxmlElement('w:shd'); sh.set(qn('w:val'),'clear'); sh.set(qn('w:fill'),hexcolor)
tcpr.append(sh)
def h(doc, level, text):
p = doc.add_heading(text, level=level)
for run in p.runs:
run.font.color.rgb = NAVY if level == 1 else ACCENT
return p
def para(doc, text, size=10.5, bold=False, italic=False, color=None, align=None, space_after=6):
p = doc.add_paragraph(); r = p.add_run(text); r.bold = bold; r.italic = italic
r.font.size = Pt(size)
if color: r.font.color.rgb = color
if align: p.alignment = align
p.paragraph_format.space_after = Pt(space_after)
return p
def bullets(doc, items, style="List Bullet"):
for it in items:
p = doc.add_paragraph(it, style=style); p.paragraph_format.space_after = Pt(2)
def table(doc, headers, rows, widths=None, header_fill="1F293A", first_col_bold=False, fontsize=9.5):
t = doc.add_table(rows=1, cols=len(headers)); t.style = "Table Grid"; t.alignment = WD_TABLE_ALIGNMENT.CENTER
hd = t.rows[0].cells
for i, htext in enumerate(headers):
hd[i].text = ""; run = hd[i].paragraphs[0].add_run(htext)
run.bold = True; run.font.size = Pt(fontsize); run.font.color.rgb = RGBColor(0xFF,0xFF,0xFF)
shade(hd[i], header_fill)
for row in rows:
cells = t.add_row().cells
for i, val in enumerate(row):
cells[i].text = ""; run = cells[i].paragraphs[0].add_run(str(val)); run.font.size = Pt(fontsize)
if first_col_bold and i == 0: run.bold = True
cells[i].paragraphs[0].paragraph_format.space_after = Pt(1)
if widths:
for r_ in t.rows:
for i, w in enumerate(widths):
r_.cells[i].width = Cm(w)
doc.add_paragraph().paragraph_format.space_after = Pt(4)
return t

View File

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

View File

@ -1,337 +0,0 @@
# -*- coding: utf-8 -*-
"""
사업수행계획서(.docx) 행안부 정보화사업 산출물 표준 목차 준용.
데이터 소스: sources.py(PLANNING·IMPLEMENTATION_BACKLOG·FEATURE_BACKLOG_100 파싱).
소스 문서 갱신 스크립트 재실행만으로 결정적 재생성.
보안: 서버 IP·비밀번호·SSH·시크릿 미기재.
"""
import os
from docx import Document
from docx.shared import Pt
from docx.enum.text import WD_ALIGN_PARAGRAPH
import sources as S
from _docxutil import setup, h, para, bullets, table, NAVY, ACCENT, GREY
OUT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "사업수행계획서.docx"))
doc = Document(); setup(doc)
M = S.META
CA = WD_ALIGN_PARAGRAPH.CENTER
# ---------- 표지 ----------
for _ in range(4): doc.add_paragraph()
para(doc, f"{M['project_en']} — 사업수행계획서", size=13, color=ACCENT, align=CA, space_after=4)
p = doc.add_paragraph(); p.alignment = CA
r = p.add_run(M["title"]); r.bold = True; r.font.size = Pt(24); r.font.color.rgb = NAVY
doc.add_paragraph()
para(doc, M["std"], size=11, color=GREY, align=CA)
for _ in range(6): doc.add_paragraph()
table(doc, ["구분","내용"],
[["문서명", M["title"]],["문서 버전", M["version"]],["작성일", M["date"]],
["작성 주관", M["owner"]],["발주처(수요기관)", M["client"]],
["사업 유형","정보시스템 구축(SI) — AI 기반 전시 운영 자동화 플랫폼"],
["보안 등급","대외비 — 자격증명·내부망 정보 미기재"]],
widths=[4.5, 11.5], first_col_bold=True)
doc.add_page_break()
# ---------- 문서정보 / 개정이력 / 목차 ----------
h(doc,1,"문서 정보 및 개정 이력")
h(doc,2,"문서 정보")
table(doc, ["항목","내용"],
[["문서 제목", M["title"]],["산출 단계","사업 착수(계획) 단계 표준 산출물"],
["표준 근거", M["std"]],
["작성 근거 문서","PLANNING v3.0 · IMPLEMENTATION_BACKLOG · FEATURE_BACKLOG_100 · GUARDIA_ALIGNMENT · SECURITY · architecture/*"],
["문서 관리","형상관리(Git) 하 버전 통제 — docs/deliverables/ 관리, _gen 재실행으로 재생성"]],
widths=[4.5, 11.5], first_col_bold=True)
h(doc,2,"개정 이력")
table(doc, ["버전","일자","작성/개정자","주요 변경 내용"],
[[M["version"], M["date"], "사업 PMO", "사업수행계획서 최초 작성(표준 목차 14장 체계)"]],
widths=[2.0, 3.0, 3.5, 7.5])
para(doc, "※ 본 계획서는 사업 진척에 따라 형상관리 절차(14장)에 의거 개정하며, 모든 변경은 개정 이력에 기록한다.",
size=9, italic=True, color=GREY)
h(doc,2,"목차")
for t_ in ["1. 사업 개요","2. 사업 범위","3. 추진 전략 및 개발 방법론","4. 추진 체계·조직 및 역할(R&R)",
"5. WBS 및 마스터 일정","6. 투입 인력 및 투입 계획(M/M)","7. 품질보증 계획","8. 보안 및 개인정보보호 계획",
"9. 시험(테스트) 계획 개요","10. 이관·교육·유지보수 계획","11. 위험(리스크) 관리","12. 의사소통·보고 체계",
"13. 산출물 목록","14. 형상관리"]:
pp = doc.add_paragraph(t_); pp.paragraph_format.space_after = Pt(2); pp.runs[0].font.size = Pt(10.5)
doc.add_page_break()
# ---------- 1. 사업 개요 ----------
h(doc,1,"1. 사업 개요")
h(doc,2,"1.1 추진 배경")
para(doc,"킨텍스는 총 전시면적 108,011㎡(국내 최대, 제3전시장 완공 시 약 178,000㎡)를 운영하지만, 전시 준비 실무는 여전히 "
"표준 서식 다운로드 후 이메일·방문 제출, CAD 수작업 배치도의 육안 검수, 수기 위치표시도 기반 유틸리티 신청에 머물러 있다. "
"참가업체 유틸리티 신청은 킨텍스 공통 플랫폼 없이 전시회별 주최자 사무국 시스템으로 파편화되어 있다.")
para(doc,"공간 데이터(배치도·도면·위치표시도)가 전부 이미지·수기로 오가며 구조화되지 않아 자동 검증·시각화·정산의 원천 데이터가 부재하고, "
"D-150/D-30/D-25/D-7 마일스톤이 시스템화되어 있지 않으며, 시공 결과를 사전에 확인할 수단이 없다는 구조적 공백이 확인되었다.")
h(doc,2,"1.2 사업 목적")
bullets(doc,[
"전시장 운영 전 과정(홀 배정→부스 배치→장치공사 설계→전기/조명→네트워크/유틸리티 배선→반입/반출)을 AI로 자동 설계·검증한다.",
"AI 설계 결과를 나노바나나(이미지 생성)로 '시공 후 결과 사진'처럼 시공 전에 미리 생성하여 사진 수준 시각화를 제공한다.",
"기획·판매·시공·운영·관람객·사후분석 전 주기를 하나의 공간 데이터·계정 체계로 통합한 베뉴 운영 플랫폼을 구축한다.",
"AI 설계·시각화 산출물을 공사/장치 옥션(역경매)의 응찰 근거로 소비하여 설계→시각화→발주→시공 폐루프를 완성한다.",
])
h(doc,2,"1.3 비전 및 정체성")
para(doc,'비전 슬로건: "신청서를 내는 순간, 시공 후 사진을 먼저 본다."', bold=True, color=ACCENT)
para(doc,"본 시스템은 킨텍스를 기준 테넌트(#1)로 하되 코엑스 등 여타 전시관을 온보딩하는 다중 전시관 멀티테넌트 SaaS로 확장 가능한 구조로 설계한다. "
"기능(모듈)은 전부 보존하고 그 위에 전시관 단위 데이터·권한·컨텍스트 격리 레이어를 얹는다.")
h(doc,2,"1.4 정량 목표(As-Is → To-Be)")
table(doc, ["목표 지표","현재(As-Is)","목표(To-Be)"],
[["홀 배정 견적 회신","문의→협의→견적 수일","조건 입력 후 즉시 자동 가견적"],
["부스 배치도 초안","CAD 수작업 수일~수주","조건 입력 후 수 분 내 복수 안 생성"],
["도면 규정 검수","홀매니저 육안 검토","규정 위반 자동 플래깅 후 사람 확정"],
["유틸리티 위치표시도","참가업체 수기 작도","부스 좌표 클릭 → 자동 배선안·자동 견적"],
["시공 결과 예측","불가(외주 조감도)","표준 샷 세트 자동 생성(정면/야간/통로)"]],
widths=[4.5, 5.5, 6.0])
h(doc,2,"1.5 기대 효과")
bullets(doc,[
"참가업체·주최자의 의사결정 가속 및 참가업체 유치 영업 자료 확보(시공 예상 이미지 기반).",
"규정 위반 사전 검출로 도면 반려 재작업 감소 및 홀매니저 검수 시간 단축.",
"'현장 추가신청 불가' 유틸리티의 신청 누락 방지 — 참가업체 최대 리스크 제거.",
"역경매 옥션을 통한 시공 발주 가격 최적화·투명화(동일 사양 위 공정 비교).",
"공간 데이터 표준화로 자동 검증·시각화·정산·wayfinding·경영분석의 단일 원천 확보.",
])
# ---------- 2. 사업 범위 (모듈: sources.modules) ----------
h(doc,1,"2. 사업 범위")
h(doc,2,"2.1 대상 시스템 및 모듈")
para(doc,f"본 사업은 설계·시각화 코어(P0), 판매·발주·정산, 관람·참가·마케팅, 경영·콘텐츠·관리 계열의 총 {len(S.modules())}개 모듈과 "
"공통·시스템관리 레이어(UIWS/WISE 이식)로 구성된다. (모듈 목록은 PLANNING 총괄표에서 자동 추출)")
mod_rows = [[mid, name, pri] for (mid, name, pri) in S.modules()]
table(doc, ["모듈","명칭","우선순위"], mod_rows, widths=[2.2, 11.3, 2.5])
h(doc,2,"2.2 우선순위 정의")
bullets(doc,[
"P0 — 핵심 차별화 + 첫 상용 행사 적용 필수(부스 시공 코어 M2~M5).",
"P1 — 운영 효율 핵심(판매·발주·정산·관람·마케팅·경영분석·관리자·공통 레이어).",
"P2 — 확장(물류·비즈매칭·wayfinding·현장운영).",
])
h(doc,2,"2.3 범위 제외(Non-Goals)")
bullets(doc,[
"킨텍스 임대계약의 법적 전자체결은 후속 단계 검토 — 초기에는 견적·서류 준비까지.",
"구조계산서의 구조 안전성 최종 판정은 AI가 대신하지 않는다(규정 체크·누락 검출까지, 최종 판정은 구조기술사/킨텍스).",
"AI 생성 시각화는 참고용이며 계약·인허가 서류로 사용 불가(워터마크·고지 강제).",
"신규 전시관 온보딩은 마스터데이터 입력 수준까지 — 고유 규정 파서 자동 생성 제외.",
"정밀 실내 측위(UWB/BLE 비콘)는 시설 인프라 협의 전제 — 초기에는 존-레벨 폴백.",
])
# ---------- 3. 추진 전략·방법론 ----------
h(doc,1,"3. 추진 전략 및 개발 방법론")
h(doc,2,"3.1 기본 추진 전략")
bullets(doc,[
"공통 레이어 선행: 인증(JWT+2FA)·시스템관리·공통 업무기능을 전 도메인 모듈보다 선행 구축.",
"코어 우선: 핵심 차별화 P0 부스 시공 코어(M2~M5)를 우선 확보하여 첫 상용 행사에 적용.",
"단일 공간 데이터 모델: M2→M3→M4→M5가 하나의 PostGIS 지오메트리를 공유.",
"폐루프 완성: 코어 산출물을 M15 옥션 응찰 근거로 소비하여 설계→시각화→발주→시공 종결.",
"표준 프레임워크 준수: GUARDiA 표준(UIMS/WISE) 스택·인증·공통모듈·보안·배포를 상속하고 도메인만 확장.",
])
h(doc,2,"3.2 개발 방법론")
para(doc,"반복·점진(Iterative & Incremental) 방식. Phase A~E 단계 위에서 각 모듈은 설계→구현→모듈 완성 직후 점진 QA→통합의 짧은 반복을 수행하며, "
"QA가 경계면·회귀·보안 불변을 검증하고 통과할 때까지 반려한다.")
ph = S.phases()
table(doc, ["단계","성격","핵심 활동"],
[[f"Phase {p[0]}", p[1], " · ".join(t[1] for t in p[2][:3]) + ("" if len(p[2])>3 else "")] for p in ph],
widths=[2.4, 4.6, 9.0])
h(doc,2,"3.3 기술 아키텍처(확정 스택)")
table(doc, ["레이어","기술","비고"],
[["프론트엔드","React 18/19 + Vite + TypeScript","역할별 포털 번들 분리(데스크톱=설계, 모바일=현장·조회)"],
["백엔드","Spring Boot 3.x(Java 17) + MyBatis","REST + WebSocket(STOMP), 룰·배치/배선 엔진"],
["데이터베이스","PostgreSQL + PostGIS","부스 폴리곤·트렌치 포인트·배선 LineString 공간 데이터"],
["비동기","Redis 작업 큐","RenderJob·서류·알림 비동기 처리"],
["이미지 생성","나노바나나 Python 워커(사이드카)","백엔드는 큐 발행만, 생성 키는 워커 env 전용"],
["AI(텍스트)","Claude 기본 + 설정형 전환(AiTextRouter)","실패 시 온프레미스 모델 자동 폴백"],
["형상/배포","Git(Gitea)+CI/CD Webhook+systemd/nginx","무중단·헬스게이트·롤백(Fail-Safe)"]],
widths=[2.8, 6.2, 7.0])
para(doc, f"개발 도메인은 서비스명 수준으로 {M['dev_domain']}을 사용하며, 운영 도메인은 후속 확정한다.", size=9, italic=True, color=GREY)
# ---------- 4. 추진 체계·조직·역할 ----------
h(doc,1,"4. 추진 체계·조직 및 역할(R&R)")
h(doc,2,"4.1 추진 체계")
para(doc,"발주처(킨텍스)—사업 총괄(PMO)—아키텍트 그룹—개발 그룹(공통·코어·도메인)—품질(QA)·배포(DevOps)의 계층으로 구성한다. "
"AI 에이전트 팀(kintex-impl-orchestrator)이 각 트랙 구현을 조율하며, 기획(planner)·디자인(designer)·검증(reviewer)을 경유한다.")
h(doc,2,"4.2 조직 및 역할(R&R)")
table(doc, ["그룹","역할","주요 책임(R&R)"],
[["거버넌스","PMO / 사업 PM","일정·범위·리스크·이해관계자 관리, 발주처 보고, 형상·품질 총괄"],
["아키텍트","AA(애플리케이션)","모듈 경계·레이어·API 표준·패키지·의존성 규칙"],
["아키텍트","SA(시스템)","확장성·HA·성능·배포 토폴로지·비기능요건(NFR)"],
["아키텍트","TA(기술)","기술 표준·빌드/배포·개발 표준·관측성·AiTextRouter"],
["아키텍트","DA(데이터)","전사 ERD·공간데이터·마스터·공통코드·BI 데이터마트"],
["아키텍트","NA(네트워크)","DMZ/내부망 분리·방화벽·부하분산·공개/백오피스 망 분리"],
["공통","공통/시스템관리 개발","인증(2FA)·시스템관리·공통 업무기능(UIWS 이식)"],
["코어","백엔드 개발","M1~M9 REST/WebSocket·룰 엔진·배선 엔진(PostGIS)"],
["코어","프론트엔드 개발","역할별 포털 화면·플로어플랜 캔버스·Before/After 뷰"],
["코어","DB 엔지니어","PostGIS 스키마·마이그레이션·매퍼·마스터 시드"],
["AI·시각화","AI 개발","배치 자동생성·규정검증 보조·매칭·서류검수·챗봇(Claude)"],
["AI·시각화","시각화(visualizer)","나노바나나 프롬프트·워커·워터마크 강제"],
["도메인","옥션/관람객/CMS/BI/관리자 개발","M15/M10/M12·M17/M16/M18 도메인 구현"],
["품질·배포","QA","경계면·회귀·보안 불변 점진 검증(통과까지 반려)"],
["품질·배포","DevOps","빌드·CI/CD·systemd/nginx·환경변수 배선"]],
widths=[2.2, 4.0, 9.8])
# ---------- 5. WBS·마스터 일정 (sources.phases + SCHEDULE) ----------
h(doc,1,"5. WBS 및 마스터 일정")
h(doc,2,"5.1 작업 분해 구조(WBS) 개요")
para(doc,"WBS는 Phase A~E의 5개 단계와 하위 작업 패키지로 구성한다(IMPLEMENTATION_BACKLOG에서 자동 추출). "
"상세 작업 분해·담당·의존·기간은 별첨 [WBS_및_일정.xlsx]의 'WBS' 시트에 관리한다.")
table(doc, ["Phase","작업 패키지(대표)","완료 기준(대표)"],
[[f"{p[0]}. {p[1]}", " / ".join(t[1] for t in p[2][:3]), p[2][0][3] if p[2] else ""] for p in ph],
widths=[3.6, 8.0, 4.4])
h(doc,2,"5.2 마스터 일정 및 마일스톤")
para(doc,"총 사업 기간은 약 28주(7개월) 기준이며, 착수 시점 확정 후 절대 일정을 확정한다(아래는 착수일 기준 상대 주차).")
table(doc, ["Phase","시작","종료","주요 마일스톤"],
[[f"{r_[0]}. {r_[1]}", r_[2], r_[3], r_[4]] for r_ in S.SCHEDULE],
widths=[4.2, 1.8, 1.8, 8.2])
para(doc,"※ Phase B는 Phase A와 일부 병행 착수하며, Phase B(공통 레이어) 완료가 Phase C/D 도메인 착수의 선행 전제이다.",
size=9, italic=True, color=GREY)
# ---------- 6. 투입 인력 (sources.STAFFING) ----------
h(doc,1,"6. 투입 인력 및 투입 계획(M/M)")
h(doc,2,"6.1 투입 인력 계획")
para(doc,"역할별 투입 인력과 단계별 투입 공수(M/M)는 아래와 같다. 상세는 별첨 [WBS_및_일정.xlsx]의 '투입인력계획' 시트에 관리한다.")
table(doc, ["역할","인원(명)","등급","주 투입 Phase","투입 공수(M/M)"],
[list(x) for x in S.STAFFING], widths=[5.5, 2.0, 2.0, 2.8, 3.7])
total_mm = sum(float(x[4]) for x in S.STAFFING)
total_head = sum(int(x[1]) for x in S.STAFFING)
para(doc, f"합계(계획): 인원 약 {total_head}명(중복 참여 포함), 총 투입 공수 약 {total_mm:.1f} M/M(추정치, 착수 시 상세 산정).",
bold=True, size=10, color=NAVY)
h(doc,2,"6.2 투입 원칙")
bullets(doc,[
"Phase B(공통 레이어)에 개발 인력을 선집중하여 전 도메인 기반을 조기 확보.",
"아키텍트는 Phase A에 집중 투입 후 각 Phase 착수 시 표준 준수를 검수.",
"QA는 전 Phase 상시 배치하여 모듈 완성 직후 점진 검증을 수행.",
])
# ---------- 7. 품질보증 ----------
h(doc,1,"7. 품질보증 계획")
h(doc,2,"7.1 품질 목표 및 기준")
bullets(doc,[
"기능 품질: 모듈별 완료 기준(WBS) 충족, 경계면(API 응답 shape↔프론트 호출) 불일치 0.",
"코드 품질: 표준 프레임워크·아키텍처 표준 준수, 정적 분석·코드 리뷰 통과.",
"회귀 품질: 스모크/회귀 테스트로 기존 기능 무회귀 보장.",
"보안 품질: 보안 불변 규칙(8장) 위반 0 — 위반 시 반려 사유.",
])
h(doc,2,"7.2 품질보증 활동")
table(doc, ["활동","시점","담당","산출"],
[["아키텍처 정합 검증","Phase A 종료","reviewer","불일치 티켓·시정 확인"],
["점진 QA(모듈 완성 직후)","각 모듈 완성","QA","경계면·회귀·보안 검증 결과"],
["코드 리뷰","커밋/PR","개발 리드","리뷰 코멘트·조치"],
["스모크·회귀 테스트","배포 전","QA/DevOps","테스트 리포트"],
["보안 체크리스트 점검","배포·리뷰 전","QA","체크리스트 결과(8장)"]],
widths=[4.5, 3.0, 3.0, 5.5])
h(doc,2,"7.3 품질 게이트")
para(doc,"각 Phase 종료 및 배포 직전에 품질 게이트를 두어 완료 기준·경계면 정합·보안 체크리스트·회귀 통과를 확인하고 통과 시에만 다음 단계로 진행한다.")
# ---------- 8. 보안·개인정보 ----------
h(doc,1,"8. 보안 및 개인정보보호 계획")
h(doc,2,"8.1 보안 불변 원칙")
table(doc, ["구분","원칙"],
[["외부 API 정책","온프레미스 모델 우선. 텍스트 AI는 승인된 단일 예외 경로만, 이미지 생성은 킨텍스 고유 승인 예외(워커 전용). 그 외 외부 API 금지."],
["자격증명 보호","내부 IP·SSH 계정·비밀번호·해시·API 키·워커 토큰을 API 응답·로그·에러·커밋에 절대 노출 금지."],
["암호화 저장","admin 비밀번호는 env(AES-256-GCM 암호문)+별도 키파일로 기동 시 재시드(하드코딩 시드 금지). 저장 자격증명은 암호화 컬럼에만."],
["인증·권한","JWT(HS256)+2차 인증(OTP/EMAIL)+로그인 실패 잠금. 행사 단위 RBAC. 미등록 장치업체 초대·응찰 차단."],
["AI 이미지 워터마크","시각화 결과는 항상 워터마크·고지 포함(참고용, 계약·인허가 서류 사용 불가)."],
["오류 응답","스택트레이스·relation/컬럼명·내부 경로 미노출. 코드+요약 메시지만 반환."],
["감사 추적","관리자·인증·권한·낙찰·설계변경·개인정보 접근 등 민감 액션 감사 로그 기록(비밀값 미기재)."]],
widths=[3.5, 12.5])
h(doc,2,"8.2 개인정보보호")
bullets(doc,[
"관람객 배지 QR·리드캡처 데이터는 수집·이용 동의 및 보존 기간 정책을 필수 적용.",
"리드(개인정보) 접근은 감사 로그로 전수 기록, 표시 필드는 비민감 항목으로 최소화.",
"EDM 발송은 정보통신망법·수신 동의 규정을 준수.",
"개인정보는 최소 수집·목적 제한·접근 통제(RBAC) 원칙을 적용.",
])
h(doc,2,"8.3 보안 체크리스트(배포·리뷰 전)")
bullets(doc,[
"응답/로그/커밋에 IP·SSH·비번·해시·API 키·워커 토큰 노출 0(grep).",
"AI 키(텍스트/이미지)는 env only — 코드·DB·커밋 미포함.",
"admin 비번 하드코딩 시드 없음(env 재시드).",
"시각화 이미지 응답에 워터마크 필드 포함.",
"DB/서버 오류 → 요약 메시지(스택·relation 명 미노출).",
"관리자 게이트·행사 RBAC·미등록업체 차단 동작.",
])
# ---------- 9. 시험 계획 ----------
h(doc,1,"9. 시험(테스트) 계획 개요")
h(doc,2,"9.1 시험 레벨")
table(doc, ["레벨","대상","방식"],
[["단위 시험","서비스·매퍼·룰 엔진·배선 엔진","개발자 단위 테스트, 공간 연산(PostGIS) 정합 검증"],
["통합 시험","모듈 간 경계면(API↔프론트, 큐↔워커)","API 응답 shape 대조, WebSocket/큐 왕복"],
["시스템 시험","역할별 시나리오(주최자/참가/업체/홀매니저/관람객)","E2E 시나리오·규정 검증·시각화 워터마크 확인"],
["회귀·스모크","전 기능","배포 전 스모크(헬스+라우터 등록+로그인 프로브)·회귀"],
["보안 시험","보안 불변 규칙","자격증명 미노출·RBAC 우회·스택트레이스 차단 점검"]],
widths=[2.6, 6.4, 7.0])
h(doc,2,"9.2 시험 원칙")
bullets(doc,[
"각 모듈 완성 직후 QA가 점진 검증하고, 통과할 때까지 반려.",
"공간 로직(배치·배선·규정 검증)과 시각화 워터마크 강제는 필수 검증 항목.",
"시험 케이스·결과는 별첨 [WBS_및_일정.xlsx]의 '시험케이스' 시트와 형상관리 하 리포트로 관리.",
])
# ---------- 10. 이관·교육·유지보수 ----------
h(doc,1,"10. 이관·교육 및 유지보수 계획")
h(doc,2,"10.1 이관 계획")
bullets(doc,[
"빌드 산출물(프론트 번들·백엔드 실행 파일·워커 서비스)과 마스터데이터·형상 이력을 발주처 운영 환경으로 이관.",
"이관 시 무중단·헬스게이트·롤백(Fail-Safe) 배포 절차를 적용.",
"운영 도메인·운영 환경변수 배선은 발주처 승인 하에 수행.",
])
h(doc,2,"10.2 교육 계획")
table(doc, ["대상","교육 내용","형식"],
[["운영자/관리자","백오피스(RBAC·마스터데이터·규정 룰셋)·감사·시스템설정","운영자 지침서+실습"],
["킨텍스 운영팀(홀매니저)","배치 검수·규정 플래그·현장 운영 대시보드","사용자 지침서+실습"],
["주최자/참가업체","포털 사용(배정·설계·유틸리티·옥션·관람객)","사용자 지침서·온라인 가이드"]],
widths=[4.0, 8.0, 4.0])
h(doc,2,"10.3 유지보수 계획")
bullets(doc,[
"장애 대응·정기 점검·규정/요율 개정 반영(룰셋 버전 관리)을 유지보수 범위로 한다.",
"운영 런북(헬스체크·재기동·롤백)을 기준으로 SM 운영 체계를 수립.",
"규정·요율 연 단위 개정은 관리자 백오피스 룰셋 버전 관리로 안전 반영.",
])
# ---------- 11. 위험 관리 (sources.RISKS) ----------
h(doc,1,"11. 위험(리스크) 관리")
para(doc,"주요 리스크와 대응 방안은 아래와 같으며, 형상관리 하 리스크 대장으로 지속 관리한다.")
table(doc, ["ID","리스크","영향","대응 방안"],
[list(x) for x in S.RISKS], widths=[1.2, 4.2, 2.6, 8.0])
# ---------- 12. 의사소통·보고 ----------
h(doc,1,"12. 의사소통·보고 체계")
table(doc, ["보고/회의","주기","참여","내용"],
[["착수 보고","사업 착수 시","발주처·PMO","범위·일정·조직·리스크 확정"],
["주간 진척 보고","주 1회","PMO·개발 리드","진척·이슈·리스크·다음 주 계획"],
["Phase 종료 보고","각 Phase 종료","발주처·PMO","산출물·품질 게이트 결과·다음 단계"],
["단계별 검토회의","마일스톤","전 트랙","경계면 정합·아키텍처 준수 점검"],
["완료(준공) 보고","사업 종료","발주처·PMO","통합 QA 결과·이관·교육·유지보수 인계"]],
widths=[3.6, 2.6, 3.4, 6.4])
para(doc,"긴급 이슈는 즉시 에스컬레이션하며, 모든 의사결정·변경은 형상관리(14장) 절차로 기록한다.", size=9, italic=True, color=GREY)
# ---------- 13. 산출물 목록 (sources.DELIVERABLES) ----------
h(doc,1,"13. 산출물 목록(단계별 표준 산출물)")
para(doc,"단계별 표준 산출물은 아래와 같으며, 상세 목록·형식·담당·상태는 별첨 [WBS_및_일정.xlsx]의 '산출물목록' 시트에 관리한다.")
table(doc, ["단계","산출물","형식","담당","상태"],
[[d[0], d[1], d[2], d[3], d[4]] for d in S.DELIVERABLES],
widths=[2.4, 6.2, 2.2, 3.0, 2.2])
# ---------- 14. 형상관리 ----------
h(doc,1,"14. 형상관리")
h(doc,2,"14.1 형상관리 원칙")
bullets(doc,[
"모든 소스·문서·설정은 Git(Gitea) 저장소로 버전 통제하며, 변경은 커밋 이력으로 추적.",
"산출물(계획서·설계서·시험·지침서)은 docs/deliverables/ 지정 경로에서 형상 관리.",
"DB 스키마는 Flyway 순번 마이그레이션을 형상 권위로 하며, baseline 규율과 시드 멱등화를 준수.",
"배포는 형상(커밋) 기준 CI/CD Webhook으로 무중단·헬스게이트·롤백 수행.",
])
h(doc,2,"14.2 형상 항목 및 절차")
table(doc, ["형상 항목","관리 방식","변경 절차"],
[["소스코드","Git 브랜치·PR·리뷰","리뷰 승인 후 병합, 커밋 메시지 이력화"],
["문서/산출물","Git 버전·개정 이력","개정 이력 기록 후 갱신"],
["DB 스키마","Flyway 마이그레이션","신규 마이그 번호=현재 최대+1(충돌 검사)"],
["설정/환경변수","env 주입·시크릿 분리","키·비번은 형상에서 제외(gitignore)"],
["배포","CI/CD Webhook·헬스게이트","실패 시 자동 롤백"]],
widths=[3.4, 5.6, 7.0])
doc.add_paragraph()
para(doc,"— 본 사업수행계획서는 행정안전부 정보화사업 산출물 표준 목차를 준용하여 킨텍스 자동전시시스템 구축 사업의 원본 내용으로 작성되었다. —",
size=9, italic=True, color=GREY, align=CA)
doc.save(OUT)
print("SAVED:", OUT)

View File

@ -1,264 +0,0 @@
# -*- coding: utf-8 -*-
"""
sources.py 산출물 생성의 단일 데이터 소스 로더.
docs/PLANNING.md · docs/IMPLEMENTATION_BACKLOG.md · docs/FEATURE_BACKLOG_100.md · docs/architecture/*
파싱하여 구조화 데이터(모듈·Phase/WBS·기능/요구사항) 반환한다.
소스 문서가 갱신되면 _gen 재실행만으로 산출물이 갱신된다.
파싱 실패 임베디드 폴백을 사용해 결정적 산출을 보장한다.
"""
import os, re
DOCS = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) # kintex/docs
ROOT = os.path.abspath(os.path.join(DOCS, "..")) # kintex
META = {
"project": "킨텍스 자동전시시스템",
"project_en": "Exhibition Automation Platform",
"title": "킨텍스 자동전시시스템 구축 사업수행계획서",
"version": "v1.0",
"date": "2026-07-11",
"owner": "지오정보기술(주) — GUARDiA 플랫폼 사업본부",
"client": "킨텍스(KINTEX, 한국국제전시장)",
"dev_domain": "kintex.zioinfo.co.kr(개발)",
"std": "행정안전부 정보화사업 산출물 표준 목차 준용",
}
def _read(rel):
p = os.path.join(DOCS, rel)
try:
with open(p, "r", encoding="utf-8") as f:
return f.read()
except Exception:
return ""
def _table_rows(text, min_cols=2):
"""마크다운 표의 데이터 행(| a | b |)을 셀 리스트로 반환(헤더/구분선 제외)."""
out = []
for line in text.splitlines():
s = line.strip()
if not s.startswith("|"):
continue
if re.match(r"^\|[\s:\-\|]+\|?$", s): # separator
continue
cells = [c.strip() for c in s.strip("|").split("|")]
if len(cells) >= min_cols:
out.append(cells)
return out
# ---------------- 모듈(M1~M18 + §5B) : PLANNING 기능 우선순위 총괄표 ----------------
_MOD_FALLBACK = [
("M2","플로어플랜 스튜디오(부스 배치)","P0"),("M3","부스 설계 스튜디오","P0"),
("M4","유틸리티 설계 자동화(전기·조명/네트워크·급배수)","P0"),("M5","나노바나나 시각화","P0"),
("M1","홀 배정·자동 견적","P1"),("M6","서류·마일스톤 워크플로","P1"),
("M7","등록업체 매칭·견적","P1"),("M9","정산·결제","P1"),
("M15","공사/장치 옥션(역경매·견적서·낙찰)","P1"),("M10","관람객 등록·배지·체크인·리드캡처","P1"),
("M12","마케팅·EDM·공개 홍보 사이트","P1"),("M16","경영분석 BI","P1"),
("M17","CMS(콘텐츠·마이크로사이트·다국어)","P1"),("M18","관리자 백오피스","P1"),
("§5B","공통/시스템관리 레이어 + JWT+2FA","P1"),
("M8","반입/반출 물류","P2"),("M11","비즈니스 매칭","P2"),
("M13","wayfinding·실내 내비","P2"),("M14","현장운영(혼잡·안전·주차·에너지)","P2"),
]
def modules():
text = _read("PLANNING.md")
mods, seen = [], set()
for cells in _table_rows(text, min_cols=4):
mid = cells[0].replace("**","").strip()
if not re.match(r"^(M\d+|§5B)$", mid):
continue
if mid in seen:
continue
name = re.sub(r"\*\*|`", "", cells[1]).strip()
pri = ""
for c in cells:
m = re.search(r"P0|P1|P2", c)
if m: pri = m.group(0); break
if not name or not pri:
continue
seen.add(mid); mods.append((mid, name, pri))
if len(mods) < 15:
return list(_MOD_FALLBACK)
order = {"P0":0,"P1":1,"P2":2}
mods.sort(key=lambda x: order.get(x[2],9))
return mods
# ---------------- Phase / WBS : IMPLEMENTATION_BACKLOG Phase 표 ----------------
_PHASE_FALLBACK = [
("A","아키텍처·거버넌스",[
("A-1","애플리케이션 아키텍처(모듈 경계·API 표준)","아키텍트(AA)","docs/architecture/app.md"),
("A-2","시스템 아키텍처·NFR(확장성·HA·성능·보안영역)","아키텍트(SA)","docs/architecture/system.md"),
("A-3","기술 표준(스택·빌드/배포·관측성·AiTextRouter)","아키텍트(TA)","docs/architecture/tech.md"),
("A-4","데이터 아키텍처(전사 ERD·공간·마스터·BI마트)","아키텍트(DA)","ERD·data.md"),
("A-5","네트워크 아키텍처(DMZ/내부망·방화벽·부하분산)","아키텍트(NA)","network.md"),
("A-6","아키텍처↔PLANNING 정합 검증","reviewer","불일치 0/티켓화"),
]),
("B","공통/시스템관리 레이어(전 모듈 선행)",[
("B-0","스캐폴드(백엔드·프론트·DB·마이그레이션)","BE·FE·DB","빌드·마이그레이션 통과"),
("B-1","인증 JWT+RBAC+2차인증(OTP)+실패잠금+admin env","COM·BE","2FA 등록/검증/초기화 왕복"),
("B-2","시스템관리(사용자·역할·공통코드·메뉴·감사·설정)","COM·ADM·DB","CRUD·RBAC 가드·감사"),
("B-3","공통 업무기능(일지·일정·쪽지·통계·공지·회의·알림)","COM·DB","각 모듈 API 왕복"),
("B-4","공통 컴포넌트·프론트 공통(2FA·그리드·달력·모달)","COM·FE","아키텍처 표준 정합"),
("B-5","점진 QA(2FA·RBAC·PII·경계면)","QA","통과까지 반려"),
]),
("C","P0 부스 시공 코어",[
("C-M2","플로어플랜(배치 3안·병합·규정검증 PostGIS)","DB·BE·AI·FE·QA","3안 생성·병합·규정 재검증"),
("C-M3","부스 설계(조립/독립·규정 사전검증·3안)","BE·AI·FE·QA","위반 사전 플래깅·컨펌"),
("C-M4","유틸리티 배선(전기/네트워크/급배수·견적·위치표시도)","DB·BE·FE·QA","최단 배선·견적·위치표시도"),
("C-M5","나노바나나 시각화(RenderJob·워커·워터마크)","BE·VIZ·FE·QA","실생성/미승인 시 목·워터마크"),
("C-C","역할별 공통 화면(로그인·대시보드·현장 모바일)","FE·QA","역할별 진입·상태 처리"),
]),
("D","P1 도메인(역할별 포털)",[
("D-M15","공사/장치 옥션(견적서 제출·역경매·낙찰)","BID·BE·DB·FE·QA","견적서·역경매·낙찰 왕복"),
("D-M10","관람객·현장(등록·티켓·배지·체크인·리드)","VIS·BE·DB·FE·QA","등록→배지→체크인→리드"),
("D-M12/17","마케팅·공개사이트·CMS(SEO·다국어·EDM)","CMS·BE·FE·QA","공개 조회·게시 워크플로"),
("D-M16","경영분석 BI(가동률·P&L·ROI·수요예측·KPI)","BI·DA·BE·FE·QA","지표 집계·드릴다운"),
("D-M18","관리자 백오피스(마스터데이터·룰셋 버전)","ADM·BE·FE·QA","RBAC·마스터 CRUD·룰셋"),
("D-M1/6/7/9","배정·서류·매칭·정산","BE·FE·DB·QA","견적·마일스톤·정산"),
]),
("E","P2 + 빌드·배포",[
("E-M11","비즈니스 매칭(추천·미팅예약)","VIS·AI","추천·미팅예약"),
("E-M13","wayfinding·실내 내비(M2 재사용)","VIS·FE","부스 검색·경로"),
("E-M14","현장운영(혼잡·안전·주차·에너지)","BE·FE","대시보드·경보"),
("E-DEP","역할별 번들·jar·워커 빌드·CI/CD·systemd","DEV","push→배포"),
]),
]
def phases():
"""[(letter, title, [(id, task, owner, done), ...]), ...]"""
text = _read("IMPLEMENTATION_BACKLOG.md")
if not text:
return _PHASE_FALLBACK
out = []
cur = None
lines = text.splitlines()
hdr_re = re.compile(r"^##\s+Phase\s+([A-E])\s*[—\-]\s*(.+)$")
for line in lines:
hm = hdr_re.match(line.strip())
if hm:
if cur: out.append(cur)
title = re.sub(r"\([^)]*\)", "", hm.group(2))
title = re.sub(r"[★((].*$", "", title).strip()
cur = (hm.group(1), title, [])
continue
s = line.strip()
if cur and s.startswith("|"):
if re.match(r"^\|[\s:\-\|]+\|?$", s):
continue
cells = [c.strip() for c in s.strip("|").split("|")]
if not cells or cells[0] in ("ID","Phase") or not re.match(r"^[A-E]-", cells[0]):
continue
# 표 형태 2종: (ID,작업,담당,완료) 또는 (ID,모듈,작업,담당,완료)
if len(cells) >= 5:
_id, task, owner, done = cells[0], cells[2], cells[3], cells[4]
elif len(cells) == 4:
_id, task, owner, done = cells[0], cells[1], cells[2], cells[3]
else:
continue
cur[2].append((_id, re.sub(r"\*\*|`","",task), re.sub(r"\*\*","",owner), re.sub(r"\*\*","",done)))
if cur: out.append(cur)
ok = [p for p in out if p[2]]
return ok if len(ok) >= 4 else _PHASE_FALLBACK
# 마스터 일정(착수 상대 주차) — 큐레이션(마일스톤)
SCHEDULE = [
("A","아키텍처·거버넌스","W1","W3","M1 아키텍처 베이스라인 승인(W3)"),
("B","공통/시스템관리 레이어","W3","W8","M2 공통 레이어·인증(2FA) 완료(W8)"),
("C","P0 부스 시공 코어","W8","W16","M3 P0 부스 코어 시연·첫 상용 행사 적용(W16)"),
("D","P1 도메인","W16","W24","M4 옥션·관람객·BI·관리자 통합(W24)"),
("E","P2 + 빌드·배포","W24","W28","M5 통합 QA 통과·운영 배포(W28)"),
]
# ---------------- 기능/요구사항 : FEATURE_BACKLOG_100 C1~C12 ----------------
def features():
"""[(fid, name, category, priority, ai, module), ...] — 100대 기능(RTM/요구사항 원천)."""
text = _read("FEATURE_BACKLOG_100.md")
out, seen = [], set()
cur_cat = ""
cat_re = re.compile(r"^###\s+(C\d+)\.\s*(.+?)\s*(?:\(\d+\)|—|$)")
stop = False
for line in text.splitlines():
s = line.strip()
cm = cat_re.match(s)
if cm:
cur_cat = cm.group(1) + " " + re.sub(r"\s*[—-].*$","",cm.group(2)).strip()
stop = False
continue
# C 표가 끝나고 'AI 자동화 맵' 등 다른 섹션이면 중단 표시
if s.startswith("##") and "카테고리" not in s and not s.startswith("###"):
stop = True
if stop:
continue
m = re.match(r"^\|\s*(F\d{3})\s*\|(.+)$", s)
if m and cur_cat:
fid = m.group(1)
if fid in seen:
continue
cells = [c.strip() for c in m.group(2).strip("|").split("|")]
name = re.sub(r"\*\*|`","",cells[0]).strip() if cells else fid
pri = next((re.search(r"P[012]",c).group(0) for c in cells if re.search(r"P[012]",c)), "")
ai = ""
for c in cells:
cc = c.strip()
if cc in ("Y","N","부분"):
ai = cc; break
module = next((re.sub(r"\*\*|`","",c) for c in cells if re.search(r"(M\d+|§5B|§1A|§6)", c)), "")
seen.add(fid)
out.append((fid, name, cur_cat, pri, ai, module))
return out
# ---------------- 산출물 목록(표준 단계별) ----------------
DELIVERABLES = [
# (단계, 산출물명, 형식, 담당, 상태, 비고)
("착수(계획)","사업수행계획서","DOCX","PMO","작성완료","본 산출물(gen_docx)"),
("착수(계획)","WBS 및 마스터 일정","XLSX","PMO","작성완료","gen_xlsx(WBS/일정/투입/산출물)"),
("착수(계획)","산출물 목록","XLSX","PMO","작성완료","gen_xlsx(산출물목록 시트)"),
("착수(계획)","착수보고 자료","PPTX","PMO","작성완료","gen_pptx(착수보고)"),
("분석","요구사항 정의서","DOCX","기획(planner)","작성완료","gen_reqspec(100대 기능 원천)"),
("분석","요구사항 추적표(RTM)","XLSX","QA/기획","작성완료","gen_xlsx(RTM 시트)"),
("분석","기획서(PLANNING)","MD","기획(planner)","기보유","docs/PLANNING.md v3.0"),
("설계","개발계획서","DOCX","PMO/개발리드","작성완료","gen_devplan"),
("설계","화면설계서·UI 스펙(design)","MD/DOCX","디자이너","기보유","docs/design.md"),
("설계","아키텍처 설계서(앱/시스템/기술/데이터/네트워크)","MD","아키텍트","기보유","docs/architecture/*"),
("설계","데이터 모델(전사 ERD·BI 데이터마트)","MD/SQL","DA/DB","진행","설계 트랙"),
("설계","API 설계서(계약)","MD","AA/백엔드","기보유","docs/API_GUIDE.md"),
("구현","소스코드(백엔드·프론트·워커)","Git","개발","진행","src/, tools/nanobanana"),
("구현","DB 스키마·마이그레이션","SQL","DB","진행","Flyway"),
("시험","시험계획서","DOCX","QA","작성완료","gen_testplan"),
("시험","시험케이스·시험결과서","XLSX","QA","작성완료","gen_xlsx(시험케이스 시트)"),
("이관","배포/운영 런북·이관 확인서","MD","DevOps","기보유","docs/OPS_RUNBOOK.md"),
("교육","사용자 지침서·운영자 지침서","DOCX/PPTX","기술문서","예정","완성+QA 후"),
("종료","완료(준공) 보고서","DOCX","PMO","예정","사업 종료 시"),
]
# ---------------- 투입 인력 계획(M/M) ----------------
STAFFING = [
("사업 PM/PMO","1","특급","A~E 상시","6.5"),
("아키텍트(AA/SA/TA)","2","특급","A 집중","3.0"),
("데이터 아키텍트(DA)","1","고급","A·D","2.0"),
("네트워크 아키텍트(NA)","1","고급","A·E","1.0"),
("공통/시스템관리 개발","2","중급","B 집중","4.0"),
("백엔드 개발","3","중·고급","B~D","12.0"),
("프론트엔드 개발","3","중·고급","B~E","12.0"),
("DB 엔지니어(PostGIS)","1","고급","B~D","4.5"),
("AI 개발","1","고급","C~D","3.0"),
("시각화(나노바나나)","1","고급","C","2.0"),
("도메인 개발(옥션/관람객/CMS/BI/관리자)","3","중·고급","D 집중","9.0"),
("QA","2","중급","B~E 상시","8.0"),
("DevOps","1","고급","B·E","2.5"),
("디자이너","1","고급","A~E","3.0"),
]
RISKS = [
("R1","홀 실측 CAD 원본 미확보","배치 정밀도","공개 규격 근사 도면으로 진행, 확보 시 대체"),
("R2","이미지 생성(외부) 승인 게이트","M5 시각화","미승인 시 목/degraded, 승인 후 라이브"),
("R3","외부 시스템(작업신고/PG/KT) 연동 미공개","M6/M9/M4b","제출용 파일·릴레이 방식, 협의 후 연동"),
("R4","등록업체 옥션 참여율·응찰 품질","M15 실효성","등록업체 게이트·AI 자료 공정 비교 유인"),
("R5","개인정보(리드·배지) 규정 위반","법적 리스크","동의·보존정책·감사·최소 수집"),
("R6","AI 시각화 오용(계약 서류)","분쟁 리스크","워터마크·고지 강제(UI·응답·PDF)"),
("R7","공통 레이어 지연","일정","Phase B 인력 선집중·병행 착수"),
("R8","규정·요율 개정","정합성","룰셋 버전 관리·과거 버전 보존"),
]
if __name__ == "__main__":
print("modules:", len(modules()))
ph = phases(); print("phases:", [(p[0], len(p[2])) for p in ph])
print("features:", len(features()))
print("sample feature:", features()[0] if features() else None)

Binary file not shown.

View File

@ -1,64 +0,0 @@
# -*- coding: utf-8 -*-
import re, json, io, sys
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
slides = json.load(open(r"C:\GUARDiA\workspace\kintex\_workspace\proposal\_qa_slides.json", encoding="utf-8"))
# Full REQ universe
UNIV = {}
for pfx, cnt in [("F", 81), ("A", 20), ("N", 21), ("S", 20)]:
for i in range(1, cnt + 1):
UNIV[f"{pfx}-{i:03d}"] = None # slide where covered
# Parse each text line for badge tokens with prefix-context + range + continuation.
# Grammar we support in a run:
# REQ-<P>-<n> single
# REQ-<P>-<n>~<m> range (same prefix)
# ...·<P>-<n> new prefix single continuation
# ...·<P>-<n>~<m> new prefix range continuation
# ...·<n> bare number continues LAST prefix (e.g. A-011·012)
# ...·<n>~<m> bare range continues last prefix (e.g. 067~069)
cover = {} # req -> list of slides
def add(pfx, a, b, sn):
for i in range(a, b + 1):
key = f"{pfx}-{i:03d}"
cover.setdefault(key, set()).add(sn)
# token: optional prefix letter, number, optional ~ number
tok = re.compile(r"(?:REQ-)?(?:([FANS])-)?(\d{2,3})(?:\s*~\s*(?:[FANS]-)?(\d{2,3}))?")
for s in slides:
sn = s["n"]
for t in s["texts"]:
# only scan segments that look like badge context (contain REQ- or letter-number dot chains)
for m in re.finditer(r"REQ-[FANS]-\d{2,3}(?:\s*~\s*\d{2,3})?(?:[·,\s]+(?:[FANS]-)?\d{2,3}(?:\s*~\s*\d{2,3})?)*", t):
seg = m.group(0)
last_pfx = None
for mm in tok.finditer(seg):
p, a, b = mm.group(1), mm.group(2), mm.group(3)
if p:
last_pfx = p
if last_pfx is None:
continue
ai = int(a)
bi = int(b) if b else ai
# sanity: within universe range
add(last_pfx, ai, bi, sn)
missing = sorted(k for k in UNIV if k not in cover)
print("=== RANGE-EXPANDED BADGE COVERAGE ===")
print("COVERED (badge):", len(cover), "/ 142")
print("NOT badge-covered (", len(missing), "):", ", ".join(missing))
# priority 7
prio = ["F-008", "F-010", "F-026", "F-032", "A-003", "A-005", "A-008"]
print("\n=== PRIORITY 7 BADGE STATUS ===")
for r in prio:
print(f"{r}: badge slides = {sorted(cover.get(r, []))}")
# dump per-covered for the previously-flagged 18
flagged = ["F-005","F-008","F-010","F-026","F-032","F-034","F-036","F-038","F-039","F-054","F-074","F-075","F-076","A-003","A-005","A-008","S-005","S-006"]
print("\n=== PREVIOUSLY-FLAGGED 18: badge coverage after range expansion ===")
for r in flagged:
sl = sorted(cover.get(r, []))
print(f"{r}: {'BADGE ' + str(sl) if sl else 'NO BADGE'}")

View File

@ -1,41 +0,0 @@
# -*- coding: utf-8 -*-
import json, io, sys, re
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
S = json.load(open('_qa_slides.json', encoding='utf-8'))
ROMAN = ["","","","","","","",""]
print("=== PER-SLIDE STRUCTURE (chapter line + first content) ===")
for s in S:
n = s["n"]
chap = ""
for t in s["texts"]:
# chapter header like "Ⅲ. 기술·기능 · ..."
if any(t.startswith(r + ".") or t.startswith(r + " ") for r in ROMAN):
chap = t
break
# headline = the longest early text that's not chapter/boilerplate
head = ""
for t in s["texts"][:6]:
if t == chap: continue
if "킨텍스" in t or "2026-07" in t or t in ROMAN: continue
if len(t) > len(head): head = t
print(f"{n:>3}| {chap[:46]:<46} | {head[:52]}")
print("\n=== 배점 / SCORING TOKENS ===")
pats = ["80/20","35/80","90/10","70+20","90(","기술 90","기술 80","가격 10","가격 20","정성 70","정량 20","재정렬","공고 확정"]
for p in pats:
hits=[(s['n'],t[:80]) for s in S for t in s['texts'] if p in t]
print(f'--- {p!r}: {len(hits)}')
for n,t in hits[:8]: print(f' {n}: {t}')
print("\n=== HONESTY LABELS ===")
for p in ["[검증됨]","[구현 계획]","[협의]","[가정]","[Non-Goal]","[공고"]:
hits=[(s['n'],t[:74]) for s in S for t in s['texts'] if p in t]
print(f'--- {p!r}: {len(hits)}')
for n,t in hits[:14]: print(f' {n}: {t}')
print("\n=== WATERMARK / AI NOTICE ===")
for p in ["워터마크","AI 생성","AI-generated","watermark","합성 이미지","예상 이미지","시공 예"]:
hits=[(s['n'],t[:74]) for s in S for t in s['texts'] if p.lower() in t.lower()]
print(f'--- {p!r}: {len(hits)} : slides {sorted(set(n for n,_ in hits))}')

File diff suppressed because it is too large Load Diff

View File

@ -1,242 +0,0 @@
# 킨텍스 자동전시시스템 구축 — 제안서 디자인 시스템 (Design System)
> 작성: proposal-visual-designer · 작성일: 2026-07-11 · 버전 v1.0
> 짝 문서: `visual_concepts.md`(도식 12종·표지·간지 시각 컨셉) · `assets/icons/*.svg`(선 아이콘 20종) · `proposal_outline.md`(63슬라이드) · `tech_advisory.md` §1-4(도식 사양)
> 목적: **디자인 템플릿 PPTX 부재** → KINTEX 브랜드 기반 디자인 시스템을 직접 정의. deck-designer(`proposal_deck.pptx` python-pptx)가 이 스펙을 벗어나지 않는다.
> 스타일 정합: 같은 사업 산출물 패밀리(`docs/deliverables/_gen/gen_dev_plan_pptx.py` 개발계획서)와 **동일 팔레트·폰트·16:9·도형 직접 작도** 톤 유지. 제안서는 개발계획서보다 **표지·간지 시각 임팩트를 1단계 상향**(수주 문서 특성).
> 원칙: 시크릿(키·비번·내부IP) 미기재. 과도한 장식 금지 — **평가위원 가독성 우선**. 명료성·위계·일관성 > 장식.
---
## 0. 아트디렉션 한 줄
**"도면이 아니라 사진으로 컨펌한다"** — 전시장의 공간감(블루)과 AI 생성의 지능감(퍼플)을 대비시키되, 본문은 흰 여백과 카드로 **읽기 쉬운 공공 제안서**를 유지한다. 표지·간지·핵심 도식(3중 해자·나노바나나 파이프라인)에만 시각 에너지를 집중하고, 나머지는 절제한다.
---
## 1. 컬러 팔레트
### 1-1. 주조·강조색 (Brand)
| 토큰 | HEX | RGB | 역할 | WCAG(흰 배경 대비) |
|---|---|---|---|---|
| `BLUE` (KINTEX Blue) | **#0066B3** | 0,102,179 | **주조색** — 헤더 액센트바·섹션칩·주요 강조·표 헤더 | 5.3:1 (AA 본문/AAA 대형) |
| `BLUE_DK` (Deep) | **#00447A** | 0,68,122 | 표지/간지 배경·최심층 데이터존·강조 대비 | 8.9:1 (AAA) |
| `BLUE_LT` (Tint) | **#E1EFF9** | 225,239,249 | 블루 계열 카드 배경·밴드 배경·연한 강조 | 배경 전용 |
| `PURPLE` (AI Purple) | **#6D4AFF** | 109,74,255 | **강조색** — AI/나노바나나/지능 요소 전용·간지 포인트·2차 액센트 | 5.0:1 (AA) |
| `PURPLE_DK` | **#4A2FCC** | 74,47,204 | 퍼플 위 텍스트 대비 보강·AI 심층 | 7.4:1 (AAA) |
| `PURPLE_LT` (Tint) | **#ECE8FF** | 236,232,255 | AI 카드 배경·퍼플 밴드 배경 | 배경 전용 |
> **색 규율(불변)**: 퍼플은 **AI·나노바나나·지능·예측** 맥락에만 쓴다(남발 금지). 블루=구조/공간/시스템/신뢰. 이 이원 대비가 "결정적 알고리즘(블루) + AI 보조(퍼플)" 3층 신뢰 프레이밍을 색으로 각인한다.
### 1-2. 중립 그레이 스케일 (Neutral)
| 토큰 | HEX | 명도 단계 | 역할 |
|---|---|---|---|
| `INK` | **#101828** | 950 | 본문 기본 텍스트·제목 |
| `MUTED` | **#667085** | 500 | 보조 텍스트·캡션·부제 | (흰 배경 4.6:1 AA) |
| `SLATE` | **#98A2B3** | 400 | 비활성·예정 상태·눈금·화살표 |
| `LINE` | **#E4E7EC** | 200 | 카드 테두리·구분선 |
| `BG` | **#F9FAFB** | 50 | 본문 슬라이드 페이지 배경 |
| `HALL_BG` | **#F2F4F7** | 100 | 데이터/평면도 영역 배경·표 zebra 짝수행 |
| `WHITE` | **#FFFFFF** | 0 | 카드·본문 배경·다크 배경 위 텍스트 |
### 1-3. 상태색 (Status / Semantic)
| 토큰 | HEX | 의미 | 사용처 |
|---|---|---|---|
| `GREEN` | **#129E63** | 완료·라이브·검증됨·통과 | 간트 완료바·"이미 돈다" 배지·QA 통과·라이브 실적 |
| `AMBER` | **#E08A00** | 진행·주의·중간 리스크 | 간트 진행바·리스크 매트릭스 중위험·확인필요 |
| `RED` | **#D92D20** | 차단·위험·실격 리스크·전기배선 | 등록 게이트 403·고위험·**S6 전기 배선 범례(적)** |
| `CYAN` | **#0BA5EC** | 정보·네트워크 배선 | **S6 네트워크 배선 범례(청)**·정보 배지 |
| `TEAL` | **#0E9384** | 급배수·부가 | **S6 급배수 배선 범례(녹)** — GREEN과 구분 위해 청록 |
> **배선 3색 규약(도식 ⑥ PostGIS 오버레이·④ Before/After 배선 범례 고정)**: 전기=`RED #D92D20`, 네트워크=`CYAN #0BA5EC`, 급배수=`TEAL #0E9384`. tech_advisory §1-4 ⑥의 "적/청/녹"을 색맹 안전(적-청-청록 조합, 명도차 확보)으로 확정. 상태 GREEN(완료)과 배선 TEAL(급배수)을 분리해 혼동 방지.
### 1-4. 접근성·색맹 안전
- 본문 텍스트(INK/MUTED)는 모두 흰/연한 배경에서 WCAG AA 이상.
- 다크 배경(BLUE_DK/BLUE) 위 텍스트는 WHITE 또는 `#C7DDF2`(표지 부제, 대비 확보) 사용.
- 상태·배선은 **색 단독 의존 금지** — 항상 텍스트 라벨/아이콘/패턴 병기(완료=✓·진행=●·차단=✕, 배선=색+실선/파선/점선 스타일 병기).
- 색맹(적록) 안전: 완료/위험을 GREEN/RED만으로 구분하지 않고 아이콘·위치로 이중 인코딩.
---
## 2. 타이포그래피
### 2-1. 폰트 패밀리
| 용도 | 폰트 | 대체(fallback) |
|---|---|---|
| 국문 기본 | **맑은 고딕 (Malgun Gothic)** | Pretendard, 본고딕(Noto Sans KR) |
| 영문·숫자 | 맑은 고딕(동일 지정) | Segoe UI, Arial |
> deck-designer는 개발계획서와 동일하게 `FONT = "맑은 고딕"`을 전 run에 지정하고, `<a:ea typeface="맑은 고딕"/>`(East Asian) 명시로 한글 렌더 일관성 확보. 외부 폰트 임베드 없이 시스템 폰트로 결정론적 생성.
### 2-2. 타입 위계 (16:9 · pt 기준)
| 위계 | 크기(pt) | 굵기 | 색 | 자간·행간 | 사용처 |
|---|---|---|---|---|---|
| 표지 대제목 (Display) | **44** | Bold | WHITE | line 1.02 | 표지 사업명 2줄 |
| 간지 넘버 (Section No.) | **150** | Bold | BLUE_DK(음영) | — | 간지 배경 대형 숫자 |
| 간지 제목 (Divider Title) | **34** | Bold | WHITE | — | 간지 섹션 국문명 |
| 본문 슬라이드 제목 (H1) | **22** | Bold | INK | line 1.0 | content_header 타이틀 |
| 소제목 (H2) | **13~14** | Bold | BLUE/PURPLE | — | 카드 헤더·블록 제목 |
| 강조 배너 (Lead) | **16~18** | Bold | WHITE/INK | line 1.0 | 비전 배너·핵심 문장 |
| 본문 (Body) | **10.5~11** | Regular | INK | line 1.0 | 불릿·설명 |
| 표 셀 (Table) | **8.5~10** | Regular/Bold | INK/MUTED | line 0.92 | 표 내용 |
| 캡션·부제 (Caption) | **8~9.5** | Regular | MUTED | — | 도표 캡션·부가설명 |
| 킥커 (Kicker) | **11** | Bold | PURPLE | — | 헤더 상단 섹션 라벨(대문자 영문 병기) |
| 배지·칩 (Chip) | **8~10** | Bold | WHITE | — | 배점 배지·상태칩·우선순위 |
| 지표 수치 (Metric) | **26~46** | Bold | WHITE/BLUE | — | 기대효과·통계 대형 숫자 |
| 푸터 (Footer) | **8.5** | Regular | MUTED | — | 슬라이드 하단 |
> **본문 최소 크기 10pt 하한**(투사 가독성). 표 셀만 예외적으로 8.5pt까지 허용하되 정보 밀도 높은 표에 한정. 캡션 8pt 하한.
---
## 3. 그리드·여백 (16:9)
### 3-1. 캔버스
- 슬라이드: **13.333 in × 7.5 in** (12192000 × 6858000 EMU), 와이드 16:9.
- deck-designer 좌표계는 개발계획서와 동일 `inch(v)=v*914400` 헬퍼 사용.
### 3-2. 안전 마진·그리드
| 항목 | 값(in) | 비고 |
|---|---|---|
| 좌우 마진 | **0.55** | 콘텐츠 좌측단=0.55, 우측단=12.78 (폭 12.23) |
| 상단 (본문) | 헤더 하단 구분선 **1.28**, 콘텐츠 시작 **1.45~1.5** | content_header 아래 |
| 하단 | 푸터 **7.06**, 콘텐츠 하한 **6.9** | |
| 상단 액센트바 | y=0, 높이 **0.12**, BLUE | 전 본문 공통 |
| 컬럼 그리드 | **12 컬럼** 개념 (실전: 2·3·4·5·6분할) | 카드 폭=(12.23 - gap×(n-1))/n |
| 표준 gutter | **0.16~0.35** | 카드 간격 |
### 3-3. 정렬·화이트스페이스 원칙
- **좌측 정렬 기본**. 중앙 정렬은 칩·배지·지표 수치·간지 한정.
- 카드는 항상 `LINE` 테두리 1pt + 라운드(ROUNDED_RECTANGLE) + 옅은 그림자(alpha 18%). 그림자는 카드·주요 노드에만(남발 금지).
- **한 슬라이드 1 메시지** — 도식 1개 또는 카드 2~4개 + 하단 캡션 1줄. 밀도 상한 준수(P3 나노바나나 슬라이드도 도식+캡션+방어배지까지).
- 여백은 위계 신호 — 섹션 간 0.3in 이상 공백 확보. 꽉 채우지 않는다.
---
## 4. 슬라이드 4종 레이아웃 규격
### 4-1. 표지 (Cover) — S01
- 배경: `BLUE_DK` 풀블리드 + 우측 겹침 평행사변형 2매(`BLUE` 큰 것, `PURPLE` 작은 것)로 대각 에너지. **여기에 나노바나나 시공 예측 히어로 이미지(워터마크)를 우측 평행사변형 영역에 반투명 합성**(개발계획서 대비 상향 포인트, `visual_concepts.md` §표지 참조).
- 좌상: 원형 브랜드 마크(WHITE 원+PURPLE 코어) + "KINTEX · WISE AI" 15pt WHITE Bold.
- 문서종류 배지 2개: "기술제안서"(PURPLE) + "Technical Proposal"(딥블루).
- 대제목: 사업명 2줄 44pt WHITE Bold. 그 아래 슬로건 배너 강조.
- 하단 메타바: 사업명·기술스택·작성일 (라인 구분).
- **슬로건 각인**: "신청서를 내는 순간, 시공 후 사진을 먼저 본다." — 부제로 대형 배치.
### 4-2. 간지 (Section Divider) — 각 P1~P9 앞
- 배경: `BLUE_DK` 풀블리드 + 우측 평행사변형 2매(BLUE/PURPLE).
- 좌측: 대형 섹션 넘버 **150pt**(BLUE_DK보다 살짝 밝은 `#2C4E82` 음영톤) + PURPLE 언더바 + 국문 제목 34pt + 영문 13pt.
- 우측: 해당 섹션 핵심 포인트 3개(PURPLE 원 불릿 + WHITE 12.5pt).
- **배점 배지**: 간지 우상단에 해당 섹션 평가배점 배지(예 "P3 · 기술제안 35점")를 추가 — 배점 규율 시각화(개발계획서엔 없던 제안서 전용 요소).
### 4-3. 본문 (Content) — 표준
- `content_header(kicker, title, idx)`: 상단 액센트바(BLUE 0.12) + 섹션번호 칩(라운드, BLUE) + 킥커(PURPLE 11pt) + 제목(INK 22pt) + 구분선(1.28) + 푸터.
- 콘텐츠 영역: y 1.45~6.9. 카드/표/도식 배치.
- 하단 **캡션 1줄** 필수(도식 슬라이드) — MUTED 9pt, tech_advisory 각 도식 "캡션" 텍스트 그대로.
- REQ 배지(선택): 우상단에 해당 슬라이드 대응 REQ-ID 소형 배지(회색) — RTM 정합 가시화.
### 4-4. 도식 (Diagram) — 핵심 12종
- 본문 헤더 동일 + 콘텐츠 전체를 단일 도식에 할애.
- 도형은 **python-pptx 직접 작도**(외부 이미지 금지, 개발계획서 원칙 계승). 노드=ROUNDED_RECTANGLE, 연결=화살표 텍스트("→"/"") 또는 얇은 라인 rect, 아이콘=본 시스템 SVG(단색 stroke, 슬라이드에선 도형 근사 또는 SVG를 EMF/PNG 없이 도형으로 재현).
- 각 도식 상세는 `visual_concepts.md`에 좌표·색·라벨 수준으로 명세.
---
## 5. 컴포넌트 스타일
| 컴포넌트 | 규칙 |
|---|---|
| **카드 (Card)** | ROUNDED_RECTANGLE, fill WHITE, line LINE 1pt, 그림자(alpha 18%, blur 90k, dist 38k). 헤더 있으면 상단 컬러 바(0.07~0.5) 또는 좌측 컬러 스트립(0.08~0.09). |
| **섹션 넘버 칩** | 0.62×0.62 라운드, BLUE(또는 PURPLE), WHITE 20pt Bold 중앙. |
| **배점 배지** | 라운드칩, fill=섹션색, WHITE 8~10pt Bold. "20점"/"35점" 등. 배점 큰 P3는 PURPLE 강조. |
| **상태 칩** | 라운드, 완료=GREEN·진행=AMBER·예정=SLATE·차단=RED. 텍스트+색 이중. |
| **불릿** | 작은 OVAL(0.09~0.11) 컬러 점 + 텍스트. 컬러=맥락색(블루/퍼플/상태). 이모지 불릿 금지 — **선 아이콘 또는 도형 점만**. |
| **콜아웃/배너** | 풀폭 라운드 rect, fill=BLUE(비전)/INK(권한구조)/PURPLE(AI강조), WHITE Bold 중앙. |
| **번호 배지** | 작은 OVAL, fill=색, WHITE 8pt Bold 숫자. 프로세스 스텝. |
| **프로세스 스텝** | 카드 나열 + 사이 화살표("" 16pt SLATE). 좌→우 흐름. |
| **표 (Table)** | 헤더=BLUE 풀폭 라운드바 WHITE Bold, 본문 zebra(짝수행 `#FCFCFD`/HALL_BG), 행 구분 LINE 0.01, 셀 8.5~10pt. 강조행(낙찰 등)=BLUE_LT + Bold. |
| **아이콘** | 본 시스템 `assets/icons/*.svg` — 선(stroke) 스타일, 24×24, stroke-width 1.75, currentColor. 슬라이드 내 색=맥락색. |
---
## 6. 차트 스타일 (BI 도식 · S38·S39·S61)
- 라이브러리 톤: Recharts 목업 재현(막대·라인·도넛·KPI 카드).
- **시퀀셜(단일 계열)**: BLUE 계열 명도 그라데이션(#00447A→#0066B3→#4A90D9→#9CC3E8).
- **카테고리(다계열)**: BLUE / PURPLE / TEAL / AMBER / SLATE 순환(최대 5, 색맹 안전 순서). 6번째부터 명도 변주.
- **비교(Before/After·As-Is/To-Be)**: As-Is=SLATE(회색, 과거), To-Be=BLUE(현재/개선). 개선 화살표=PURPLE.
- **강조 수치**: 대형 Metric(26~46pt) + 단위 소형. KPI 카드는 BLUE/PURPLE 교차 fill + WHITE 수치.
- 축·그리드: SLATE 얇은 선, 라벨 8pt MUTED. 범례는 색+텍스트+마커 형태 병기.
- 도넛/파이: 최대 5조각, 나머지 "기타" 회색 통합. 중앙에 총계 수치.
---
## 7. 아이콘 시스템 (선 스타일 SVG — 직접 제작)
### 7-1. 공통 제작 규격 (전 20종 통일)
- **viewBox**: `0 0 24 24`
- **fill**: `none` (면 채움 절대 금지)
- **stroke**: `currentColor` (슬라이드에서 맥락색 상속)
- **stroke-width**: `1.75` (전 아이콘 동일 시각 무게)
- **stroke-linecap**: `round`, **stroke-linejoin**: `round`
- **그리드 정렬**: 24 그리드, 여백 2(콘텐츠 영역 2~22), 코너 반경 통일감.
- **외부 라이브러리·폰트아이콘·온라인 생성 금지** — 전량 직접 패스 작도.
- 한 세트 통일 메타포: 기하 단순·동일 디테일 밀도(오버피팅 금지).
### 7-2. 아이콘 20종 (최소 16종 요구 → 20종 제작)
| # | 파일명 | 의미 | 주 사용 슬라이드 |
|---|---|---|---|
| 1 | `ai-sparkle.svg` | AI/스파클/지능 | 나노바나나·AI 전 슬라이드·간지 |
| 2 | `booth-layout.svg` | 부스/레이아웃/배치 | M2 플로어플랜·모듈맵 |
| 3 | `wiring-power.svg` | 배선/전기/유틸리티 | M4 배선·아키텍처 |
| 4 | `camera-render.svg` | 카메라/렌더링/시각화 | 나노바나나 샷세트·표지 |
| 5 | `auction-gavel.svg` | 옥션/경매 망치 | M15 옥션 폐루프 |
| 6 | `visitor-badge.svg` | 관람객/배지 | M10 관람·등록 |
| 7 | `shield-security.svg` | 보안/방패 | P5 보안·불변원칙 |
| 8 | `watermark.svg` | 워터마크 | AI 이미지 보안·워터마크 |
| 9 | `tenant-building.svg` | 테넌트/전시관/빌딩 | 멀티테넌시·확장 |
| 10 | `schedule-gantt.svg` | 일정/간트 | P8 일정·마일스톤 |
| 11 | `org-people.svg` | 조직/사람 | 추진체계도·페르소나 |
| 12 | `check-verify.svg` | 체크/검증/규정 | 규정검증·QA·커버리지 |
| 13 | `dashboard-chart.svg` | 대시보드/차트/BI | M16 경영분석·기대효과 |
| 14 | `mobile.svg` | 모바일/반응형 | 현장·모바일·접근성 |
| 15 | `document-rtm.svg` | 문서/서류/RTM | M6 서류·RTM·커버리지 |
| 16 | `risk-warning.svg` | 리스크/경고 | 리스크 매트릭스·확인필요 |
| 17 | `postgis-spatial.svg` | 공간데이터/PostGIS/폴리곤 | 단일 공간원천·배선 |
| 18 | `pipeline-flow.svg` | 파이프라인/큐/비동기 | 나노바나나 파이프라인·아키텍처 |
| 19 | `network-zone.svg` | 망분리/보안영역 | 보안영역 분리도 |
| 20 | `closed-loop.svg` | 폐루프/순환 | 생애주기 폐루프·옥션 |
> 16종 요구 대비 **20종 제작**(공간데이터·파이프라인·망분리·폐루프 4종 추가 — 킨텍스 도식 핵심 메타포 보강).
### 7-3. deck-designer 아이콘 사용 지침
- python-pptx는 SVG 직접 임베드가 어려우므로, 각 아이콘은 **(a) 슬라이드 헤더/칩 옆 시각 앵커**로 쓸 때는 대응 도형(원+선) 근사 작도, **(b) 아이콘 자체가 콘텐츠일 때**는 SVG를 사전 PNG 변환(cairosvg/inkscape) 후 삽입 가능. 변환 시 stroke 색=맥락색으로 렌더.
- 색 상속: 아이콘 stroke는 배치 맥락색(블루=구조, 퍼플=AI, 상태색=상태)으로 지정.
---
## 8. deck-designer 전달 요약 (준수 계약)
1. **팔레트 상수**를 §1 표 그대로 코드 상단에 정의(개발계획서 `gen_dev_plan_pptx.py`와 동일 변수명 재사용 + PURPLE_DK·RED·CYAN·TEAL 추가).
2. **폰트** 맑은 고딕 전 run + East Asian 지정.
3. **레이아웃 4종**(§4) 헬퍼 함수화: `cover()`, `divider(no,kr,en,points,score)`, `content_header()`, 도식별 함수.
4. **12종 도식 + 표준 3종**(추진체계·리스크·기대효과)은 `visual_concepts.md` 좌표·라벨대로 작도. 재해석 금지.
5. **색 규율**: 퍼플=AI 전용, 배선 3색 고정(적/청/청록), 상태색 의미 고정.
6. **가독성 우선**: 본문 10pt 하한, 한 슬라이드 1메시지, 캡션 1줄, 여백 확보.
7. **보안**: 시크릿·내부IP 도식/라벨에 미기재. 외부 API는 "승인 예외"로만 표기.

View File

@ -1,101 +0,0 @@
# -*- coding: utf-8 -*-
"""
Stitch 화면 스크린샷 다운로드 + 매니페스트 생성
- MCP list_screens 결과(제목SCR/모듈/그룹 매핑) screenshot.downloadUrl을 assets/screens/ 저장
- screens_manifest.json 갱신(파일명title·scr·module·group·device·order)
- build_deck.py assets/screens/ + manifest 재스캔해 갤러리 자동 재구성
재실행: 화면이 생기면 MCP 재조회 SCREENS 갱신하여 다시 실행.
"""
import os, json, urllib.request, ssl
BASE = os.path.dirname(os.path.abspath(__file__))
OUT = os.path.join(BASE, "assets", "screens")
os.makedirs(OUT, exist_ok=True)
# title -> (scr, module, korean_name, group, device)
META = {
"Login & Workspace Selection - KINTEX AI System": ("SCR-01","공통","로그인 & 행사 워크스페이스 선택","공통·인증","DESKTOP"),
"Organizer Dashboard - KINTEX AI System": ("SCR-02","M1","주최자 대시보드","주최자","DESKTOP"),
"Booth Layout Editor - KINTEX AI System": ("SCR-03","M2","부스 배치 에디터","주최자","DESKTOP"),
"Layout Comparison - KINTEX AI System": ("SCR-04","M2","배치안 비교(S7 조감)","주최자","DESKTOP"),
"Exhibition Schedule - KINTEX AI System (Updated Icons)": ("SCR-15","M1·M6","전시 일정 관리","주최자","DESKTOP"),
"Exhibitor Home - KINTEX AI System": ("SCR-05","M1","참가업체 부스 홈","참가업체","DESKTOP"),
"Booth Design Studio - KINTEX AI System": ("SCR-06","M3","부스 설계 스튜디오","참가업체","DESKTOP"),
"Utility Wiring View - KINTEX AI System": ("SCR-07","M4","유틸리티 배선 뷰","참가업체","DESKTOP"),
"Utility Order Summary - KINTEX AI System": ("SCR-08","M4","유틸리티 신청 요약·위치표시도","참가업체","DESKTOP"),
"Compliance Report - KINTEX AI System": ("SCR-09","M2","규정 검증 리포트","장치업체","DESKTOP"),
"Manager Approval Queue - KINTEX AI System": ("SCR-10","M6","홀매니저 승인 큐","홀매니저·운영","DESKTOP"),
"Review Details - KINTEX AI System": ("SCR-11","M6","홀매니저 검수 상세","홀매니저·운영","DESKTOP"),
"Hall Operations - KINTEX AI System (Updated Icons)": ("SCR-14","M14","홀 현장운영 대시보드","홀매니저·운영","DESKTOP"),
"Visualization Gallery - KINTEX AI System": ("SCR-12","M5","시각화 갤러리","시각화·전역","DESKTOP"),
"Business Intelligence - KINTEX AI System (Updated Icons)": ("SCR-13","M16","경영분석 대시보드","경영분석","DESKTOP"),
"Admin Dashboard - KINTEX AI System (Updated Icons)": ("SCR-16","M18","관리자 대시보드","관리자 백오피스","DESKTOP"),
"Component Guide - KINTEX AI System (Updated Icons)": ("SCR-17","디자인","컴포넌트 가이드(스타일가이드)","스타일가이드","DESKTOP"),
"Contractor On-site Checklist - KINTEX AI System": ("SCR-M1","현장","시공업체 현장 체크리스트","모바일 현장","MOBILE"),
"Manager Field Inspection - KINTEX AI System": ("SCR-M2","현장","홀매니저 현장 검수","모바일 현장","MOBILE"),
}
# title -> screenshot downloadUrl (from MCP list_screens; 최신 조회분)
URLS = {
"Login & Workspace Selection - KINTEX AI System":"https://lh3.googleusercontent.com/aida/AP1WRLtUqdYm2030_UAU-An_JAmVNGl2eTumZ5vNnz9T5WeXoyFc7pvkO4RQPoHLdsFqg4shnjDOxWNOMGVPtMLH8A_SUNvd7GFISwRGX1vZ8hMkZIsiU4DPDg-6qV6EpX1j0qHjpu9zHPyWtO_6nWcRDGaOsweZTkjvZ2UeTD0GgyqCETLufFh69ZQvlcuOpM0VSH7spqLDwPq-hpoWme6S_WBSg9b6ygHB3lUvfkOgs5wWtPOgIcZ6bCn1ZB8",
"Organizer Dashboard - KINTEX AI System":"https://lh3.googleusercontent.com/aida/AP1WRLtAyvwUEakGIhFdDB_6cQqB8DzBPnb7TdrSUBehA1gVEzNKg-ppC3sF1LcgF_vr9bceJ3voOhAcUcC5CYbSiPb0IjMoHHpwfCqm5rqI9vTqHjdvIpg69oMO09Rx_wF0k_XCY6P9doDNj8SaW5TjcsbuvGT4NHO-5uBDeT65_MF-ZTdRZ_jF2mugRi0KqBS5dnAvnJhQH-UFdIO3v_Wtaj8hkXkvS--sth05JgI1hNMUZJrjHGiROzBGxw",
"Booth Layout Editor - KINTEX AI System":"https://lh3.googleusercontent.com/aida/AP1WRLvOGbKVWZuHy5ALELagNRlBkIwl8byuXEe1LcPgKuP7o5XaRZrsE6UTVJC821aK01hdrs19oson_NDDMnoo7nqU6kgu5NgJJdxK6cdF5lmzr2jziNxWEwHDPoibYcIeAFsCaIZy49a6mYUzrY2ZhQDqAT0AZ3YV8Cfq_dMkuyaGaHvKRBt_1cbRRFa0MgZsN9l1Ed6w6GPKLjKF7Sw3fOpdYxTwhLz6BrpIBBkNYsk97t7nzA9Hv2mTHw",
"Layout Comparison - KINTEX AI System":"https://lh3.googleusercontent.com/aida/AP1WRLvUxlOFlFr0MwnaD8KN10xT9gsFq0_wyilol_aWYLj2Ev_kHhNKJ5PHug_6KwEemQtsQjZDEP8zEPpkdZvUq-TTOubFN3NStmGaIlf1-GvpPzq5uLMGrneKXsp7ccThjFjq556VQhhr2WHlyqlSEhtx37cd0i9Iggo3GfPwcNYNUMd6kizoWzKTy_LMvTjhMAdMGwblLvMN1bYDhoQ75Aty2eSwf92NSzsLccQPk5EGw_GicpZRenkeDQ",
"Exhibition Schedule - KINTEX AI System (Updated Icons)":"https://lh3.googleusercontent.com/aida/AP1WRLthlJCkcQzW168tISpRl5BVhGWN6aZ-dTxxpnJYLqrWUQ132vn-w0x23XA_y8JF7e-q28ZqqJy_Z9tqc2J_OK99-RPYpT7nbFmoD2H-NC7MBHRp7RSegRK-hE5WKT3r8MNtFPG1PvcDIdvSiNPqy_YYcdOCYDvjwVZAPmhgBSiWFyZTZjB6Plwq6et0F5SIZQThhmK_lFcy0K9Jcodwk2YiCcIET47cTYj02BpEieNO5RK7gj6fsL3OonI",
"Exhibitor Home - KINTEX AI System":"https://lh3.googleusercontent.com/aida/AP1WRLvzY0N-4aD4E4apccqfR3yNCRkuFKEOc4O4uJIbkaHQQrDi-gDwKS5pFJPEvcAKBfrGgi10XY32J6VSfgiX_AzVomJDuoinMjWzTi_XSXSzJgravcI-ABUo-ep_p9rdNP2lX7T01WqML0uhOYGD2jlhjhusVpqsYBYsRmFhi6NhQMYqUZdBH_ndL9YW6g6wAOObXXVGKgMUBr0d-YHI0FyhKM0uAi1CYQ2QQiNDSJtZdtOHS6yn44MwUhs",
"Booth Design Studio - KINTEX AI System":"https://lh3.googleusercontent.com/aida/AP1WRLu1TWc00TspuC4jpGfgrvt9_kRQIWg84Zr4FW1l_YVePzu5jTqkYR7UmO1Xw9nuydukCbwIh3OiOqpedk9Tt4fwrTQkqrxJEZYNmM3dr-GRhvbYhO2UFrmlJzhdMeFtXuTKnv3nTqzWyTgBySTbXYncfySbTSzC06dleCMKamOHEgGI0M60g_9LBT-STycoHohAFWuUY2Z4XIlkN0gJJ0B0P3y9JTEX7S3RTiiO8RNhAoBskY2ZjQId7TI",
"Utility Wiring View - KINTEX AI System":"https://lh3.googleusercontent.com/aida/AP1WRLt0_fkmB3ACojGDXezlhbpya-s-re7WgfEAXim10VWfAMNWet_cpx3addY23p2Kzcd-1NXVbgmQh-5tGokg19E0ui7H5yU2IVbU28GOquQu-ngiRv6CpAcMtQfe-zjzfrSOA1DT2mT-zQTUYd068i6HwJJv5734uU4hFuvLU0xCExlfe8kxnp5Ct8IfC11Aoh_jLvPMCqsMmOux0mtYRf4Sp4hEaBfJr1BMwIX4dAlvpTV-PJxiJ_M5Ymo",
"Utility Order Summary - KINTEX AI System":"https://lh3.googleusercontent.com/aida/AP1WRLtyOGoy4l69iClGRq0bO8uTbAjZx54sF890NtTmAKPztU6OwKtfsUDZ8c_KcynLP1btwWj1VMXUu0MHKCx75n5gCNpxuRRpwAgdoQM6Mxzwt-TuWLyAhM4CW7MXKJEpVVA0vQmXjqV6hM177yZ0kTJ6aN0u2C-mvzELug_hDensEwivvFZn4OHfK1UF3HY7GxuPLZB4smGXiMqHH3vRGD1feNV1USWxfTGq0G5JhIu79TI1mMyTbmrtvg4",
"Compliance Report - KINTEX AI System":"https://lh3.googleusercontent.com/aida/AP1WRLuh3R_GPiSwqoywspJfxh9c8o-4qNRFGsRQJ6hNtS4OyvqArAB3AVT4ZDkQrJjZPutTFFG0k7JNr_egmfBAfUn_LjOO5AB-eqTA3MoAdGxmqEa7qThF7hCIz2lfKInkL4N-GSrNdZtBnJMb95f9sRUDzNaLk-wQZZcUv2ee6Uo1CvWEu5mdYScTal09vaepqghDUi2Td8oxzTjQ_dcRGUHDcZoncSuvPg2fPhHECW99CiTwD3RMU2es470",
"Manager Approval Queue - KINTEX AI System":"https://lh3.googleusercontent.com/aida/AP1WRLt0jyggUFD_uIa47eZ71yDp7_r-yDJxUvGJcxuBhlHcjPsilp_Su2XDR17WyNcxPWdd5t_6nJCIkd9JjblfA8ENMW2QvuszG7RawM33gvFJG5uS8_JFoctdLz8Oq1WDAcdJH3ObRgv3EDDfqFY9LXvAnzKA8d2qsR31fz5ulTfulgUyCUdckbSdheECcc1lrtx4TLwafiwUggI1y8zDNVHtOcJuw4cG9OWqxMxreCghnJ3D4YthwnWUAQ",
"Review Details - KINTEX AI System":"https://lh3.googleusercontent.com/aida/AP1WRLtZn9UwuYCpWjWC9ZlUKYKZO6-8LWF92u8BZhUpgNrIjPQVa2NiS6zv5Zz9k2NQSPYCWdj2OQBRpB7u1hNi3AmMvObMtvvewMBeeDrDInB22eFS6DcbuT_Goa5bsxMTIWp0jx5PKR2rGZtp-78nHn2NNi7p96d_2ddXJ3eBtwAq-NRFL7jbuFbGWrIW0wFkJ8ThMazQ33UxOFsLmD_PXCrnWyt_NcNO-ans631jlKL-VdL_FfKBNDaxylw",
"Hall Operations - KINTEX AI System (Updated Icons)":"https://lh3.googleusercontent.com/aida/AP1WRLtVuCoXA2RksHSQq2vVewDWWsoduCoY9CD0n9rVvsD-ShiGsQCPBCFJWakyIjNNPqF9GASb3u5rs3bqHSUd_kk15d_014FfzpYuiWyrjTleULMsFDsnhaW9n-jpO7_ayRndbZWh8zBjpZ2zVuUReCYRFP_BVwr2O3RYyKCEVXcY6l6dzMywa3EozvEhV87GSS-CxKhm4cBb5RcI7j_5j4IgFj9RYxDzq89agjhTxjsWA9DIx5lq32BSof0",
"Visualization Gallery - KINTEX AI System":"https://lh3.googleusercontent.com/aida/AP1WRLuuyZPo--qWn5qMTYI0fWcrBj1LuMzudztIvctYqof9RweLOLVb1Xu7a-xTUB615pRKZp-AAOPQZgRcqQUN6bT_XIyykoLT4yFQXXOxMqIXI-FulW61CdiBOfo6U43Z9YI37oXm_aFtV9PsEgPrsjj3aIzNYxxtSAkOJzVx-G215-uAVdcikNeYoPvnSkgoBP5OjzR5futn48YmVMNsEmbVXe0YVIZbwQOvlIa31TDNPiw2xzJyvdGUwrg",
"Business Intelligence - KINTEX AI System (Updated Icons)":"https://lh3.googleusercontent.com/aida/AP1WRLubNXF75G8McZ1gryw7VDigsKOl63B9nuVSi6_tJCXAYF4ZIhEuJlNfptxhTWSxP5CwN5cUWxHVnnviC-qO8pxChg_Xf5GpImd-UTXb_JIdqzGT2dtr0qkNN15FqPMvk4WenixdezsLNsU4moxl72lOoqJvxHi0xuBp4SmRzJNTd1Z7XK29ycdk-80LCfvPBHZrbT2iGZGreshY63kpwu4dlZarjt_xQO1qqTm_ZYkpIM79xJPMT-jMNyQ",
"Admin Dashboard - KINTEX AI System (Updated Icons)":"https://lh3.googleusercontent.com/aida/AP1WRLu3zdb3wXg6Dvq77ymBp4iT7wU7uo9G5t8cfb5X2_TZr0XNtST2AnmFqsrZO56E1p1Xz6xbQesdd_59tfcpsoUMkB5WO1fja-HFLK06JtFeGkZxitnG-87qCgauSPphlsjOtSHaEiZmPfbg5yh-a5PelIKe2bHjM-FmX4hm7_UItkgaC-FoEBEfsNwHnALPBbWvz6u8ZZhuXceh8G0V_SSH0ks9t75boEooOPg15xEJ4KszZHdlb2OFCl0",
"Component Guide - KINTEX AI System (Updated Icons)":"https://lh3.googleusercontent.com/aida/AP1WRLsLW88myjTGHETfGth2ywdFnjE3y692dNnxaDKncS_DwVuqegjWfznVhrKE8VXJLSWkmIiqREk13m-FKKAK7nwJGAtUX2r7f54KrcEd0Na2B-xFqEqzQovlfFi2tbxletF3WKJUqNGp_IHkvlghOymIz7IBUOeA5UABGGqfJdSBGIso2Fqh1jX5Pu0LEfP1HsfL167lTgS4t1oe94hDp9QC-MQgD2U-KOYdoUaZ_WINLtFeZ5-3Uvo05YI",
"Contractor On-site Checklist - KINTEX AI System":"https://lh3.googleusercontent.com/aida/AP1WRLtA_bsgM_D2rk4HbtM-snlKWWvXP8Lm4dGEvLQcGFgf6tFBkHXiK5fiT-T2UCzS46RkEwV8GDjM3sHFRfh65PQ4Ssnj-p5DVhZO0JCx3_jmTDumyl3kYbL5ssEU15psKPJ8tU_yX-MlUelq-Yhh6lgkGYqbTFUy8aQYD5M3F-FUAKQLUjSTb_SqHHg4dae9RqB6otbfl5Y0KiA4QDkAURFWBgJnJeOmuONOr-r6BTKS4ZPmu7DnAXQPYK0",
"Manager Field Inspection - KINTEX AI System":"https://lh3.googleusercontent.com/aida/AP1WRLtStbjv9lr4kBFrhu9LuUu7yXXGrFg_8HKk4DPRjSpxcslk4GPz85moG6zJzCQrt_tVWRi255BLH9NKp8wH-W_kBcPEy9_T4xVBPbYkytnT6oHIMW5HXz0OAkhq1Izk-EI7Xj_-FfAhKUpjgAXhg_7sGZMrkD6sXgVk57W6-fZNmN-OqYY2872tL-JK_EgVAdY7joxaUoqVOizVZB2Mw_ElFBRTFCPz3iO8UTwhQs2q55OYSqP-n0in7pc",
}
# 갤러리 그룹 순서
GROUP_ORDER = ["공통·인증","주최자","참가업체","장치업체","홀매니저·운영","시각화·전역","경영분석","관리자 백오피스","스타일가이드","모바일 현장"]
def scr_num(scr):
s = scr.replace("SCR-","")
if s.startswith("M"): return 100+int(s[1:])
return int(s.split("·")[0]) if s[0].isdigit() else 999
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
def slug(scr, kor):
return scr.replace("·","_") + ".png"
manifest = {}
ok = fail = 0
for title, meta in META.items():
scr, module, kor, group, device = meta
url = URLS.get(title)
fn = slug(scr, kor)
fp = os.path.join(OUT, fn)
if url:
try:
req = urllib.request.Request(url, headers={"User-Agent":"Mozilla/5.0"})
data = urllib.request.urlopen(req, context=ctx, timeout=60).read()
with open(fp, "wb") as f:
f.write(data)
ok += 1
except Exception as e:
print("FAIL", scr, title, repr(e)[:80]); fail += 1
continue
elif not os.path.exists(fp):
print("SKIP(no url, no file)", scr); continue
manifest[fn] = {"scr":scr,"module":module,"name":kor,"group":group,"device":device,
"title":title,"order":scr_num(scr)}
with open(os.path.join(OUT, "screens_manifest.json"), "w", encoding="utf-8") as f:
json.dump({"group_order":GROUP_ORDER,"screens":manifest}, f, ensure_ascii=False, indent=1)
print(f"DOWNLOADED ok={ok} fail={fail} manifest={len(manifest)} -> {OUT}")

View File

@ -1,342 +0,0 @@
# 나라장터(g2b) 정보시스템 구축 부문 — 표준 제안서 목차·평가배점 조사 및 킨텍스 제안서 재편 매핑
> 작성: rfp-analyst · 작성일: 2026-07-12 · 목적: 킨텍스 제안서(P1~P9, 63슬라이드) 목차 재편 근거
> 원칙: **확인된 1차 출처(법령·실제 RFP)만 사실로 기재**, 그 외는 [가정]/[통례] 표기. 시크릿 미기재.
> 대상 제안서 파일은 **읽기만**(수정 금지) — 본 문서는 매핑·권고만 산출.
> 짝 문서: `proposal_outline.md`(현행 P1~P9) · `rfp_analysis.md`(§4 배점 가정) · `tech_advisory.md`(§5 WBS·§6 SM) · `rtm.md`
---
## 0. 핵심 결론 (요약)
1. **표준 목차의 단일 출처**는 조달청 「협상에 의한 계약 제안서평가 세부기준」 **[별지 제5호서식] 제안서 서식(예시: 소프트웨어개발·유지관리사업)** — 7개 대장( 일반현황 ~ Ⅶ 그 밖의 사항)이다. 이 목차가 **평가부문 = 제안서 장(章)**으로 1:1 정렬된다.
2. **정성평가 평가항목의 단일 출처**는 같은 세부기준 **[별표 3의2] 소프트웨어 및 시스템 개발사업** — "정보시스템 구축"에 해당하는 표준 평가표. 부문: **전략 및 방법론 / 수행계획(기술·기능) / 수행기반 / 프로젝트 관리 / 프로젝트 지원 / 하도급 / 필수 제안**.
3. **배점은 세부기준이 고정하지 않는다.** 제9조⑤에 따라 **수요기관이 항목별 배점한도를 결정**(항목당 30점 초과 금지, 하도급 5점 이상, 국산제품 활용 2점 이내). 정보화사업 총 배점은 「행정기관·공공기관 정보시스템 구축·운영 지침」 **제18조**에 따라 **기술 90(정성 70 + 정량 20) / 가격 10**(원칙), HW 비중 50% 이상 사업은 **기술 80 / 가격 20** 허용.
4. **현행 킨텍스 제안서의 80/20 배점 가정(rfp_analysis §4)은 정보화 표준(90/10)과 다르다** — 실제 공고 확인 전까지 [가정]이며, SW 중심 사업이면 90/10이 표준. (본 문서는 지적만, proposal 파일 수정 없음.)
5. 현행 63슬라이드는 **표준 7대장에 전부 매핑 가능**하나, **신설이 필요한 장 4개**( 일반현황 / Ⅴ·Ⅵ 프로젝트 관리·지원 보강 / 상생협력·하도급 / 정량평가 대응)가 있다. 미배치 슬라이드 0.
---
## 1. 조달청 기술평가 표준 항목·배점 (출처·연도 명시)
### 1-1. 전체 배점 체계 (법령 확정 사실)
| 구분 | 배점 | 출처(조항) |
|---|---|---|
| 종합 = 기술능력평가 + 입찰가격평가 | 100 | 세부기준 제9조① |
| 기술능력평가 | **80점** (세부기준 원칙) / 정보화사업은 **90점**(지침 제18조) | 세부기준 제9조①1 · 지침 제18조 |
| 입찰가격평가 | 20점(원칙) / 정보화 10점 | 세부기준 제9조①2 |
| ─ 정성평가 | (기술 90 기준) **70점** | 실제 RFP 표본 §1 |
| ─ 정량평가 | **20점 이내**(총 배점한도) | 세부기준 제9조④ |
| HW 비중 50%↑ 예외 | 기술 80 / 가격 20 허용 | 지침 제18조 |
> **출처 정본**: 조달청 「협상에 의한 계약 제안서평가 세부기준」(조달청지침, 최신 개정 **2024. 9. 6. 시행**; 2017. 6. 1. 최초 시행). 정보화 총배점은 「행정기관 및 공공기관 정보시스템 구축·운영 지침」(행정안전부고시 제2024-66호, **2024. 8. 28. 시행**) 제18조.
### 1-2. 정성평가 표준 평가표 — [별표 3의2] 소프트웨어 및 시스템 개발사업 (정보시스템 구축 표준)
> **세부기준은 각 세부항목에 고정 점수를 부여하지 않는다**(수요기관이 배점한도 결정). 아래는 **평가항목 ↔ 세부평가항목 구조**(법령 원문 그대로). 배점 예시는 §1-3 실제 RFP 표본 참조.
| 평가항목(부문) | 세부평가항목 | 채점 관점(평가위원이 무엇을 보는가) |
|---|---|---|
| **전략 및 방법론** | 사업이해도 | 사업 목표·특성 부합, 구체성·적절성. (기획용역 ISP 수행자 참여 시 1등급 하향) |
| | 추진전략 | 수행 일정·위험요소 고려한 추진전략 타당성 |
| | 추진체계 | 수행조직(전문업체·기술지원·공동수급체·하도급) 역할·책임·품질확보·협력 방안 |
| | 사업추진방법론 | 방법론 구체성·적절성, 적용사례·경험, 단계별 산출물 제시 |
| **수행계획**(≈기술 및 기능) | 시스템 요구사항 | 장비 규격 충족·인터페이스·확장성·공급/설치·유지관리 |
| | 국산제품 활용 기여도 | 국산장비 도입방안 적절성·국내기업 지원 (배점 **2점 이내**, 국내입찰 한정) |
| | 기능 요구사항 | 기능·기대·제약 대비 구현방안 구체성·실현가능성 |
| | 성능 요구사항 | 성능충족 구현·테스트·방법론·분석도구 |
| | 인터페이스 요구사항 | 타 시스템 연계 방안·사용자 UI 분석/설계/구현/테스트 |
| | 유지관리 정책 | 제조사 유지관리(기간·비용·부품·보증) |
| **수행기반** | 적용기술 | 적용 기술의 확장·실현 가능성 |
| | 개발환경 | 장비·작업장소 등 인프라 준비·대응 |
| | 보안 요구사항 | 기술적 보안 구현(설계→구현→검증 단계 반영) |
| | 제약사항 | 기술·표준(**표준 프레임워크 적용 포함**)·언어·방법론·법제도·안전관리 제약 대응 |
| **프로젝트 관리** | 일정관리 | 수행기간·세부일정·단계별 산출물 연계 |
| | 품질관리 | 품질관리 범위·절차·점검, 품질보증 인증 근거 |
| | 기밀보안관리 | 물리적·관리적 보안 체계·대책 |
| | 위험 및 이슈 관리 | 위험·이슈 식별·분석·절차·대응 |
| **프로젝트 지원** | 인수인계 | 인계 전략·검사 대상/방법·충족조건·인계계획 |
| | 교육훈련 | 운영자·관리자·사용자 교육 내용·방법·기간·인원·횟수 |
| | 기술지원 | 지원 범위·수준·기술 매뉴얼 |
| | 하자보수 계획 | 하자담보 기간 내 하자보수 범위·조치절차·대응 |
| **하도급** | 하도급 계획의 적정성 | 하도급 금액/계약금액 비율 적정성 (배점 **5점 이상 필수**; **20억원 이상 SW사업은 제외 불가**) |
| **필수 제안** | (수요기관 지정) | 제안요청 필수사항 반영 여부 |
### 1-3. 정량평가 항목 (별표 8~14, 총 20점 이내)
| 정량 항목 | 출처 | 비고 |
|---|---|---|
| 경영상태 | 별표8 | 기업신용평가 |
| 수행실적 | 별표9 | 동일·유사 사업 실적 |
| 책임성·성실성 | 별표10 | |
| **상생협력** | **별표11** | 중소기업 공동수급체 참여 지분율 — 50%↑=5.0점 … 35%미만=1.0점 (**최대 5점**). 중소기업 단독=최고등급, 참여지분 0=0점 |
| 상용SW 유지관리 하도급금액 적정성 | 별표12 | |
| SW기술자료 임치 | 별표13 | |
| 정량 필수제안 | 별표14 | |
### 1-4. 실제 RFP 배점 표본 (구체 숫자 예시) — g2b R25BK00879799 (정보시스템 구축, HW 포함형, 30p 이내)
> 표본은 90/10(기술 90+가격 10) 채택. 부문 세분은 수요기관 재량임을 실증.
| 구분 | 평가항목 | 배점 |
|---|---|---|
| 정량(20) | 보유인력 5 · 경영상태 5 · 수행실적 5 · 사회적책임 3 · 지리적거리 2 | 20 |
| 정성(70) | 사업의 이해도(10) · 시스템 구성(20) · 시스템 기능(20) · 운영방안(10) · 사업관리 및 지원(10) | 70 |
| 가격(10) | 입찰가격평가 | 10 |
---
## 2. 정보시스템 부문 표준 제안서 목차 (장·절 구조)
### 2-1. 표준 목차 정본 — [별지 제5호서식] 제안서 서식(예시: 소프트웨어개발·유지관리사업)
> 출처: 세부기준 별지 제5호서식(법령 원문). **평가부문과 장 구조가 일치**하도록 설계됨. 각 절 옆은 대응 평가항목(별표 3의2).
```
. 일반현황
1. 입찰자 일반현황 (일반현황·주요 연혁·자본금·매출액)
2. 해당 사업의 수행조직 및 업무분장 → [정성] 추진체계 / [정량] 경영·실적
3. 해당 사업의 핵심투입인력 및 이력사항 → [정량] 보유인력
Ⅱ. 전략 및 방법론
1. 사업 이해도 → 전략및방법론·사업이해도
2. 추진전략 → 추진전략·추진체계
3. 적용기술 → 수행기반·적용기술
4. 표준 프레임워크 적용 → 수행기반·제약사항(표준FW)
5. 개발 방법론 → 사업추진방법론
Ⅲ. 기술 및 기능
1. 시스템 장비구성 요구사항 → 수행계획·시스템 요구사항
2. 기능 요구사항 → 수행계획·기능 요구사항
3. 보안 요구사항 → 수행기반·보안 요구사항
4. 데이터 요구사항 → (기능/데이터)
5. 시스템 운영 요구사항
6. 제약사항 → 수행기반·제약사항
Ⅳ. 성능 및 품질
1. 성능 요구사항 → 수행계획·성능 요구사항
2. 품질 요구사항 → 프로젝트관리·품질관리
3. 인터페이스 요구사항 → 수행계획·인터페이스 요구사항
4. 테스트 요구사항
. 프로젝트 관리
1. 관리방법론
2. 관리역량
3. 일정계획 → 프로젝트관리·일정관리
4. 개발장비
Ⅵ. 프로젝트 지원
1. 품질보증 → 프로젝트관리·품질관리
2. 시험운영
3. 교육훈련 → 프로젝트지원·교육훈련
4. 유지보수 → 프로젝트지원·기술지원/하자보수
5. 기밀보안 → 프로젝트관리·기밀보안관리
6. 비상대책
Ⅶ. 그 밖의 사항 → 기대효과·확장·상생협력 등
```
### 2-2. 실제 RFP 표본의 목차 요구 (간소형 예시) — g2b R25BK00879799 (30p 이내)
```
. 사업의 이해 : 과제수행목표·제안개요·제안사현황·추진체계및전략·제안의특징및장점
Ⅱ. 시스템 관련사항: 총괄시스템구성전략·기능별시스템구성및기능·성능구현 사전운영방안·향후활용방안
Ⅲ. 사업관리 및 지원: 사업관리(보고·일정·위험)·시스템지원방안(교육·기술지원·유지보수)
Ⅳ. 첨부자료
※ "발주처 요청 외 제안사가 중요하다고 판단한 목차·내용은 별도 기술하고 근거자료 첨부"
```
> **판독**: 소형·HW형 RFP는 별지 5호서식을 3~4장으로 압축한다. **대형 SW 구축**은 별지 5호서식 7대장을 그대로 쓰는 경향. 킨텍스 규모(전 전시 파이프라인)는 **7대장 채택 권장**.
### 2-3. 장별 "무엇을 담는가" (평가위원 채점 관점 요약)
| 장 | 담을 내용 | 채점 포인트 |
|---|---|---|
| 일반현황 | 제안사 개요·실적·재무, 수행조직도·R&R, 핵심인력 이력 | 정량(보유인력·경영·실적) 근거 + 조직 신뢰 |
| Ⅱ 전략·방법론 | 사업이해도, 추진전략/체계, 적용기술, 표준FW, 개발방법론 | "우리가 사업을 제대로 이해했고, 검증된 방법으로 한다"는 확신. **차별화(Win Theme) 메시지의 1차 노출 지점** |
| Ⅲ 기술·기능 | 요구기능 구현방안, 시스템 구성, 보안·데이터, 제약 대응 | 요구사항 대비 구현 구체성·실현가능성. **최다 배점 통례 → 기능 차별화 총력** |
| Ⅳ 성능·품질 | 성능충족·테스트, 품질보증, 인터페이스, 가용성/SLA | 정량 목표치·검증방법 제시 |
| 프로젝트 관리 | 관리방법론·역량, 일정계획, 위험·이슈, 품질관리 | 일정 준수·리스크 통제 실행력 |
| Ⅵ 프로젝트 지원 | 인수인계, 교육, 기술지원, **유지보수(SM)**, 기밀보안, 비상대책 | 구축 후 지속가능성·이관·운영 |
| Ⅶ 그 밖의 사항 | 기대효과, 확장, 상생협력·하도급, 부가제안 | 정성 가점·정책부합 |
---
## 3. 재편 매핑표 — 현행 킨텍스 P1~P9 (63슬라이드) → 표준 7대장
> 규칙: 각 슬라이드를 표준 장·절로 이동. **차별화(나노바나나·AI 폐루프)는 배점 높은 Ⅲ(기술·기능) + Ⅱ(전략·방법론) 접점에 배치**해 노출 극대화. 미배치 0.
### 3-1. 전면부 (표지·요약)
| 현행 | 표준 배치 | 비고 |
|---|---|---|
| S01 표지 | 표지 | 유지 |
| S02 제안 개요 | ** 일반현황 §1 앞 or 제안개요 간지** | 제안사 소개는 Ⅰ으로 흡수 |
| S03 목차 | 목차 | 7대장 인덱스로 재구성 |
| S04 제안 핵심요약(3중 해자) | **Ⅱ 전략·방법론 §2 추진전략** 도입(요약 간지) | Win Theme 조기 각인 |
| S05 배점 대응 맵 | 목차 직후 간지 | 표준 배점(90/10)으로 갱신 권고 |
| S06 [가정] 고지 간지 | 부록 or 서두 노트 | 유지 |
### 3-2. P1 사업이해 → Ⅱ 전략·방법론 §1
| 현행 | 표준 배치 |
|---|---|
| S07 사업 배경·목적 | **Ⅱ §1 사업 이해도** |
| S08 As-Is 프로세스 병목 | **Ⅱ §1 사업 이해도** |
| S09 이해관계자·페르소나 | **Ⅱ §1 사업 이해도** |
| S10 To-Be 목표(정량) | **Ⅱ §1 사업 이해도** (→ 효과 지표는 Ⅶ와 연계) |
### 3-3. P2 추진전략 → Ⅱ 전략·방법론 §2·§5 +
| 현행 | 표준 배치 |
|---|---|
| S11 비전·포지셔닝 | **Ⅱ §2 추진전략** |
| S12 3중 해자 전략 | **Ⅱ §2 추진전략** (차별화 앵커) |
| S13 Win Theme 5선 | **Ⅱ §2 추진전략** (배점 정렬 배지 병기) |
| S14 전시 생애주기 폐루프 | **Ⅱ §5 개발 방법론**(생애주기 방법론) |
| S15 추진체계·수행조직 | ** 일반현황 §2 수행조직·업무분장** (+ Ⅱ §2 추진체계 교차) |
### 3-4. P3 기술제안 (25슬라이드) → Ⅲ 기술·기능 중심 + Ⅱ 적용기술 + Ⅳ 일부
> **핵심 전략**: 나노바나나·AI 폐루프는 **Ⅱ §3 적용기술**(방법론·차별화 관점)과 **Ⅲ §2 기능 요구사항**(구현 관점) **두 장에 걸쳐 노출** → 전략및방법론·기술및기능 양쪽 배점 동시 획득.
| 현행 | 표준 배치 |
|---|---|
| S16 전체 모듈맵 | **Ⅲ 기술·기능 도입부**(기능 조망) |
| **S17~S23 나노바나나 시공예측(7)** | **Ⅱ §3 적용기술**(S17 개요·S19 구조보존·S20 파이프라인·S23 AI신뢰) + **Ⅲ §2 기능 요구사항**(S18 샷세트·S21 Before/After·S22 RenderJob 방어). **7슬라이드 절대 유지** |
| S24 플로어플랜 스튜디오(M2) | **Ⅲ §2 기능 요구사항** (+ 적용기술 교차) |
| S25 배치 규정 자동검증 | **Ⅲ §2 기능 요구사항** |
| S26 부스 설계 스튜디오(M3) | **Ⅲ §2 기능 요구사항** |
| S27 유틸리티 설계(M4)·PostGIS 배선 | **Ⅲ §2 기능 요구사항** (+ Ⅲ §4 데이터 요구사항 교차) |
| S28 위치표시도·유틸리티 신청 | **Ⅲ §2 기능 요구사항** |
| S29 옥션 개요·폐루프 | **Ⅲ §2 기능 요구사항** (차별화 폐루프) |
| S30 역경매·실시간 순위 | **Ⅲ §2 기능 요구사항** |
| S31 종합평가 낙찰 | **Ⅲ §2 기능 요구사항** |
| S32 등록 게이트·발주 전환 | **Ⅲ §2 기능 요구사항** (+ Ⅲ §3 보안 교차) |
| S33 판매·홀배정·견적(M1) | **Ⅲ §2 기능 요구사항** |
| S34 서류·마일스톤(M6) | **Ⅲ §2 기능 요구사항** (+ Ⅲ §5 시스템 운영 교차) |
| S35 관람객 등록·배지·리드(M10) | **Ⅲ §2 기능 요구사항** |
| S36 비즈매칭·마케팅·공개사이트(M11·12·17) | **Ⅲ §2 기능 요구사항** |
| S37 정산·결제(M9) | **Ⅲ §2 기능 요구사항** |
| S38 경영분석 BI-1(M16) | **Ⅲ §2 기능 요구사항** (+ Ⅲ §4 데이터 교차) |
| S39 경영분석 BI-2(M16) | **Ⅲ §2 기능 요구사항** |
| S40 현장운영·물류(M8·13·14) | **Ⅲ §2 기능 요구사항** |
### 3-5. P4 아키텍처 → Ⅱ §3·§4 + Ⅲ §1·§4·§5
| 현행 | 표준 배치 |
|---|---|
| S41 전체 아키텍처 | **Ⅲ §1 시스템 장비구성 요구사항** (+ Ⅱ §3 적용기술) |
| S42 단일 공간 데이터 원천(PostGIS) | **Ⅲ §4 데이터 요구사항** |
| S43 확장성·비동기 | **Ⅳ §1 성능 요구사항** (+ Ⅶ 확장 연계) |
| S44 AI 플랫폼 라우팅(Claude→Ollama) | **Ⅱ §3 적용기술** |
| S45 연동·마스터데이터 | **Ⅳ §3 인터페이스 요구사항** (+ Ⅲ §5 시스템 운영) |
### 3-6. P5 보안·개인정보 → Ⅲ §3 + Ⅴ·Ⅵ 기밀보안
> 표준은 **기술적 보안 = Ⅲ §3 보안 요구사항**, **관리적 기밀보안 = 기밀보안관리 / Ⅵ §5 기밀보안**으로 분리. 현행 P5를 분할 배치.
| 현행 | 표준 배치 |
|---|---|
| S46 보안 개요·불변 원칙 | **Ⅲ §3 보안 요구사항** (개요) |
| S47 보안영역 분리(망 구성) | **Ⅲ §3 보안 요구사항** (+ Ⅲ §1 구성) |
| S48 인증·접근통제(2FA·RBAC) | **Ⅲ §3 보안 요구사항** |
| S49 AI·이미지 보안(워터마크) | **Ⅲ §3 보안 요구사항** (차별화 보안) |
| S50 개인정보·감사·안전코딩 | **Ⅲ §3 보안 요구사항** + ** §? 기밀보안관리 / Ⅵ §5 기밀보안** (관리적 요소 이관) |
### 3-7. P6 비기능 → Ⅳ 성능·품질
| 현행 | 표준 배치 |
|---|---|
| S51 성능·가용성 SLA | **Ⅳ §1 성능 요구사항** |
| S52 나노바나나 성능 | **Ⅳ §1 성능 요구사항** |
| S53 접근성·다국어·테마·반응형 | **Ⅳ §2 품질 요구사항** |
| S54 관측성·데이터 | **Ⅳ §2 품질 요구사항** (+ 관리 연계) |
### 3-8. P7 멀티테넌시·확장 → Ⅲ §5 + Ⅶ
| 현행 | 표준 배치 |
|---|---|
| S55 멀티테넌트 SaaS 비전 | **Ⅶ 그 밖의 사항**(확장 비전) |
| S56 테넌트 격리 아키텍처 | **Ⅲ §5 시스템 운영 요구사항** (+ Ⅲ §3 보안 격리) |
| S57 온보딩·2계층 관리자 | **Ⅲ §5 시스템 운영 요구사항** |
### 3-9. P8 사업관리·일정·조직 → 프로젝트 관리 + Ⅵ
| 현행 | 표준 배치 |
|---|---|
| S58 추진 일정·Phase | ** §3 일정계획** |
| S59 선행 게이트·리스크 관리 | ** §1 관리방법론 / 위험·이슈 관리** |
| S60 품질·형상관리 | ** §2 관리역량(품질관리)** + **Ⅵ §1 품질보증** |
### 3-10. P9 기대효과 → Ⅶ 그 밖의 사항
| 현행 | 표준 배치 |
|---|---|
| S61 정량 기대효과 | **Ⅶ 그 밖의 사항** |
| S62 정성·전략 효과 | **Ⅶ 그 밖의 사항** |
| S63 확장 비전·마무리 | **Ⅶ 그 밖의 사항** |
### 3-11. 매핑 커버리지 요약
| 표준 장 | 배치된 현행 슬라이드 | 소계 |
|---|---|---|
| 표지·목차 | S01·S03·S05·S06 | 4 |
| 일반현황 | S02·S15 (+ **신설 필요**) | 2 |
| Ⅱ 전략·방법론 | S04·S07~S14·S17·S19·S20·S23·S41(교차)·S44 | 14 |
| Ⅲ 기술·기능 | S16·S18·S21·S22·S24~S40·S42·S45(교차)·S46~S50·S56·S57 | 34 |
| Ⅳ 성능·품질 | S43·S51~S54 | 5 |
| 프로젝트 관리 | S58·S59·S60 | 3 |
| Ⅵ 프로젝트 지원 | S50(관리적 일부)·S60(품질보증) (+ **대폭 신설 필요**) | ~1 |
| Ⅶ 그 밖의 사항 | S55·S61·S62·S63 | 4 |
| **미배치** | — | **0** |
---
## 4. 표준 목차 채택 시 신설 필요 장(章)과 내용 소스
> 현행 63슬라이드가 **표준 대비 취약하거나 부재**한 영역. 실제 공고 시 감점·실격 방지를 위해 신설.
| 신설/보강 대상 | 이유 | 내용 소스(지목) |
|---|---|---|
| ** 일반현황 (신설)** | 제안사 일반현황·연혁·자본금·매출, 수행조직도·R&R, **핵심투입인력 이력**은 정량평가(보유인력·경영·실적) 근거. 현행에 사실상 부재(S02·S15 부분). | `tech_advisory §5-2` 직군별 M/M 산정 → 투입인력표. 제안사 실적은 발주 확인 후 확보(⚠확인필요) |
| **Ⅵ 프로젝트 지원 (대폭 신설)** | 인수인계·교육훈련·기술지원·**유지보수(SM)**·비상대책은 표준 정성 부문. 현행 P8이 품질/형상만 얇게 다룸. | `tech_advisory §6 SM 운영 체계`(SLA·장애대응·정기점검·CI/CD 운영·ITSM 프로세스·조직 R&R §6-1) · §5-1 WBS(SM 이관·인수인계) |
| ** 프로젝트 관리 보강** | 표준은 관리방법론·관리역량·위험/이슈를 독립 부문으로 배점. 현행 배점 2점 가정으로 과소. | `tech_advisory §5 WBS·공수` · `proposal_strategy §4 리스크` |
| **상생협력·하도급 (신설)** | 정량 별표11(중소기업 참여지분율, 최대 5점)·정성 하도급 계획 적정성(5점 이상). **20억↑ SW사업은 하도급 평가 제외 불가**. 현행 없음. | 공동수급/중소기업 참여 계획·하도급 계획서(별지 제14호서식) — 컨소시엄 구성 확정 후(⚠확인필요) |
| **정량평가 대응 절 (신설)** | 보유인력·경영상태·수행실적·사회적책임은 필수 증빙 미제출 시 최하점. 별첨 근거 필요. | 일반현황 + 별첨(증빙) — 발주 확인 후 |
| **표준 프레임워크 적용 절 (Ⅱ §4)** | 별표3의2 제약사항에 "표준 프레임워크 적용 포함" 명시. 공공 SW 필수 관점. | 확정 스택(Spring Boot/React) — 전자정부 표준FW 적용 여부는 ⚠확인필요(현재 스택은 GUARDiA 표준) |
---
## 5. 재편 권고 (Win Theme 노출 극대화)
1. **7대장 채택**: 킨텍스 규모(전 전시 생애주기)는 별지 5호서식 7대장 구조가 정합. 현행 P1~P9를 §3 매핑대로 재배열.
2. **나노바나나(S17~S23) 이중 노출**: Ⅱ §3 적용기술(방법론·차별화)과 Ⅲ §2 기능 요구사항(구현) 양쪽에 배치 → **전략및방법론 + 기술및기능 배점 동시 겨냥**. 7슬라이드 축소 금지.
3. **AI 폐루프(배치·설계·배선·옥션 S24~S32)**: Ⅲ §2 기능 요구사항에 집중 배치(최다 배점 장). 폐루프 차별화 메시지를 기능 채점표에 직접 정렬.
4. **보안 분할**: 기술적 보안 → Ⅲ §3, 관리적 기밀보안 → Ⅴ·Ⅵ. 표준 채점 구조에 맞춰 배점 누수 방지.
5. **Ⅵ 프로젝트 지원 신설**로 "구축 후 지속가능성"(SM·교육·이관) 확보 — 공공 평가위원 필수 확인 항목.
6. **배점 표기 갱신**: 현행 80/20 가정 → 정보화 표준 90/10(정성70+정량20)로 갱신 권고(실제 공고 확인 시 확정). *proposal 파일 수정은 별도 지시 필요 — 본 문서는 근거만 제공.*
---
## 6. 출처 URL 전체 목록
**1차 출처 (법령·실제 문서 — 본문 근거)**
- 조달청 협상에 의한 계약 제안서평가 세부기준 (국가법령정보센터): https://law.go.kr/LSW/admRulLsInfoP.do?admRulSeq=2100000088849
- 조달청 협상에 의한 계약 제안서평가 세부기준 PDF (별표3의2·별표11·별지5호서식 원문 추출): https://bidform.co.kr/updata/notifile/ab689c32028cee2185e44dcd24c0fb89.pdf
- 실제 나라장터 정보시스템 구축 RFP 제안서 작성요령(배점·목차 표본, g2b R25BK00879799): https://www.g2b.go.kr/pn/pnp/pnpe/UntyAtchFile/downloadFile.do?bidPbancNo=R25BK00879799&bidPbancOrd=000&fileType=&fileSeq=3
- 행정기관 및 공공기관 정보시스템 구축·운영 지침 제18조(평가배점) (국가법령정보센터): https://www.law.go.kr/LSW//admRulInfoP.do?admRulSeq=2100000246404&chrClsCd=010201
- 정보시스템 구축·운영 지침 제18조(평가배점) — 국민건강보험공단 법령DB: https://www.nhis.or.kr/lm/lmxsrv/law/joHistoryContent.do?SEQ=1541&SEQ_CONTENTS=4250027&DATE_START=20240828&DATE_END=20190823
**참고 출처 (통례·가이드)**
- KOSA 소프트웨어 사업 가이드북 v3.0: https://www.sw.or.kr/upload/common/027d281c-2a7a-4886-b96b-e9ad04c7a309.pdf
- 공공 SW사업 제안요청서 작성 예시(SW 법제도 반영): https://smallake.kr/wp-content/uploads/2023/03/붙임공공SW사업-제안요청서-작성-예시.pdf
- NIPA SW사업 단계별 발주 가이드: https://www.nipa.kr/home/2-7-1-1/7089
- 조달청 협상에 의한 계약 제안서평가 세부기준(2026.1.26. 시행 안내, 도로교통공단): https://www.koroad.or.kr/main/board/22/305400/board_view.do
---
## 7. 확인 필요 (삭제 금지)
| # | 항목 | 사유 |
|---|---|---|
| T1 | 실제 킨텍스 공고의 배점·목차 강제 여부 | 본 문서는 표준·표본 근거. 실제 공고가 별지 5호서식과 다른 목차를 강제할 수 있음 |
| T2 | 정보화(SW중심) vs HW중심 판정 | 90/10 vs 80/20 결정. 킨텍스 시스템은 SW 중심 → 90/10 추정, 공고 확인 필요 |
| T3 | 컨소시엄·중소기업 참여·하도급 구성 | 상생협력(별표11)·하도급(별표3의2) 대응은 컨소시엄 확정 후 작성 |
| T4 | 전자정부 표준프레임워크 적용 요구 여부 | 별표3의2 제약사항에 표준FW 명시 — 현행 스택(GUARDiA 표준)과 정합성 공고 확인 |
| T5 | 사업금액 20억원 이상 여부 | 20억↑이면 하도급 평가항목 제외 불가(세부기준 제9조⑤ 단서) |

View File

@ -1,239 +0,0 @@
# -*- coding: utf-8 -*-
"""
경량 SVG(stroke) -> PNG 래스터라이저 (cairo 불필요, PIL만 사용)
- assets/icons/*.svg (viewBox 0 0 24 24, fill none, stroke currentColor, stroke-width 1.75, round cap/join)
- 지원: <path d> M/m L/l H/h V/v C/c S/s A/a Z/z, <circle>, <rect rx transform=rotate>
- 렌더: 고배율 supersample LANCZOS 다운스케일, 라운드 /조인 = 두꺼운 라인 + 정점
- : stroke=맥락색(RGB). fill 없음( 아이콘).
재사용: from icon_raster import render_icon; render_icon('ai-sparkle', (r,g,b), out_png, px=192)
"""
import os, re, math
from PIL import Image, ImageDraw
ICON_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "assets", "icons")
_num = re.compile(r'[-+]?(?:\d*\.\d+|\d+\.?)(?:[eE][-+]?\d+)?')
def _nums(s):
return [float(x) for x in _num.findall(s)]
# ---- cubic / arc flattening ----
def _cubic(p0, p1, p2, p3, n=18):
pts = []
for i in range(1, n+1):
t = i/n; mt = 1-t
x = mt**3*p0[0] + 3*mt*mt*t*p1[0] + 3*mt*t*t*p2[0] + t**3*p3[0]
y = mt**3*p0[1] + 3*mt*mt*t*p1[1] + 3*mt*t*t*p2[1] + t**3*p3[1]
pts.append((x, y))
return pts
def _arc(p0, rx, ry, phi, large, sweep, p1, n=24):
# endpoint -> center parameterization (SVG spec)
if rx == 0 or ry == 0:
return [p1]
phi = math.radians(phi)
cosp, sinp = math.cos(phi), math.sin(phi)
dx = (p0[0]-p1[0])/2.0; dy = (p0[1]-p1[1])/2.0
x1p = cosp*dx + sinp*dy; y1p = -sinp*dx + cosp*dy
rx, ry = abs(rx), abs(ry)
lam = x1p*x1p/(rx*rx) + y1p*y1p/(ry*ry)
if lam > 1:
s = math.sqrt(lam); rx *= s; ry *= s
num = rx*rx*ry*ry - rx*rx*y1p*y1p - ry*ry*x1p*x1p
den = rx*rx*y1p*y1p + ry*ry*x1p*x1p
co = math.sqrt(max(0.0, num/den)) if den else 0.0
if large == sweep:
co = -co
cxp = co*rx*y1p/ry; cyp = -co*ry*x1p/rx
cx = cosp*cxp - sinp*cyp + (p0[0]+p1[0])/2.0
cy = sinp*cxp + cosp*cyp + (p0[1]+p1[1])/2.0
def ang(ux, uy, vx, vy):
d = math.sqrt((ux*ux+uy*uy)*(vx*vx+vy*vy))
c = max(-1.0, min(1.0, (ux*vx+uy*vy)/d)) if d else 1.0
a = math.acos(c)
if ux*vy - uy*vx < 0: a = -a
return a
th1 = ang(1, 0, (x1p-cxp)/rx, (y1p-cyp)/ry)
dth = ang((x1p-cxp)/rx, (y1p-cyp)/ry, (-x1p-cxp)/rx, (-y1p-cyp)/ry)
if not sweep and dth > 0: dth -= 2*math.pi
if sweep and dth < 0: dth += 2*math.pi
pts = []
for i in range(1, n+1):
th = th1 + dth*i/n
x = cosp*rx*math.cos(th) - sinp*ry*math.sin(th) + cx
y = sinp*rx*math.cos(th) + cosp*ry*math.sin(th) + cy
pts.append((x, y))
return pts
# flag-aware arc arg regex: rx ry rot large sweep(single 0/1) x y (repeatable)
_NN = r'[-+]?(?:\d*\.\d+|\d+\.?)'
_ARC = re.compile(
r'(' + _NN + r')[,\s]*(' + _NN + r')[,\s]*(' + _NN + r')[,\s]*'
r'([01])[,\s]*([01])[,\s]*(' + _NN + r')[,\s]*(' + _NN + r')')
def _parse_path(d):
"""반환: list of subpaths, each subpath = list of (x,y) polyline points."""
chunks = re.findall(r'([MmLlHhVvCcSsAaZz])([^MmLlHhVvCcSsAaZz]*)', d)
subs = []; cur = []
x = y = 0.0; sx = sy = 0.0
prev_ctrl = None; prev_cmd = None
for cmd, arg in chunks:
rel = cmd.islower(); c = cmd.upper()
if c == 'A':
for m in _ARC.finditer(arg):
rx, ry, rot, large, sweep, nx, ny = (float(m.group(k)) for k in range(1, 8))
if rel: nx += x; ny += y
cur += _arc((x, y), rx, ry, rot, int(large), int(sweep), (nx, ny))
x, y = nx, ny
prev_cmd = c; prev_ctrl = None; continue
vals = _nums(arg)
if c == 'Z':
cur.append((sx, sy)); x, y = sx, sy
subs.append(cur); cur = []
prev_cmd = c; prev_ctrl = None; continue
j = 0
first_pair = True
while j < len(vals) or (c in ('M', 'L', 'C', 'S', 'H', 'V') and False):
if c == 'M':
nx, ny = vals[j], vals[j+1]; j += 2
if rel: nx += x; ny += y
if first_pair:
if cur: subs.append(cur)
cur = [(nx, ny)]; sx, sy = nx, ny
else:
cur.append((nx, ny))
x, y = nx, ny; first_pair = False
elif c == 'L':
nx, ny = vals[j], vals[j+1]; j += 2
if rel: nx += x; ny += y
cur.append((nx, ny)); x, y = nx, ny
elif c == 'H':
nx = vals[j]; j += 1
if rel: nx += x
cur.append((nx, y)); x = nx
elif c == 'V':
ny = vals[j]; j += 1
if rel: ny += y
cur.append((x, ny)); y = ny
elif c == 'C':
x1, y1, x2, y2, nx, ny = vals[j:j+6]; j += 6
if rel: x1+=x; y1+=y; x2+=x; y2+=y; nx+=x; ny+=y
cur += _cubic((x, y), (x1, y1), (x2, y2), (nx, ny))
prev_ctrl = (x2, y2); x, y = nx, ny
elif c == 'S':
x2, y2, nx, ny = vals[j:j+4]; j += 4
if rel: x2+=x; y2+=y; nx+=x; ny+=y
if prev_cmd in ('C', 'S') and prev_ctrl:
x1 = 2*x - prev_ctrl[0]; y1 = 2*y - prev_ctrl[1]
else:
x1, y1 = x, y
cur += _cubic((x, y), (x1, y1), (x2, y2), (nx, ny))
prev_ctrl = (x2, y2); x, y = nx, ny
else:
break
if j >= len(vals):
break
prev_cmd = c
if c not in ('C', 'S'):
prev_ctrl = None
if cur: subs.append(cur)
return subs
def _circle_pts(cx, cy, r, n=48):
return [(cx + r*math.cos(2*math.pi*i/n), cy + r*math.sin(2*math.pi*i/n)) for i in range(n+1)]
def _rect_pts(x, y, w, h, rx=0, transform=None, n=6):
rx = min(rx, w/2, h/2) if rx else 0
pts = []
if rx <= 0:
pts = [(x, y), (x+w, y), (x+w, y+h), (x, y+h), (x, y)]
else:
def arc(cx, cy, a0, a1):
return [(cx+rx*math.cos(math.radians(a)), cy+rx*math.sin(math.radians(a)))
for a in [a0 + (a1-a0)*k/n for k in range(n+1)]]
pts += [(x+rx, y)]
pts += [(x+w-rx, y)]
pts += arc(x+w-rx, y+rx, -90, 0)
pts += [(x+w, y+h-rx)]
pts += arc(x+w-rx, y+h-rx, 0, 90)
pts += [(x+rx, y+h)]
pts += arc(x+rx, y+h-rx, 90, 180)
pts += [(x, y+rx)]
pts += arc(x+rx, y+rx, 180, 270)
if transform:
m = re.match(r'rotate\(\s*([-\d.]+)\s+([-\d.]+)\s+([-\d.]+)\s*\)', transform)
if m:
ang = math.radians(float(m.group(1))); ox = float(m.group(2)); oy = float(m.group(3))
ca, sa = math.cos(ang), math.sin(ang)
pts = [((px-ox)*ca-(py-oy)*sa+ox, (px-ox)*sa+(py-oy)*ca+oy) for px, py in pts]
return pts
def _extract(svg_text):
"""SVG -> list of polylines (in 24-unit viewBox coords)."""
polys = []
for m in re.finditer(r'<path[^>]*\bd="([^"]+)"', svg_text):
polys += _parse_path(m.group(1))
for m in re.finditer(r'<circle[^>]*>', svg_text):
a = m.group(0)
cx = re.search(r'cx="([-\d.]+)"', a); cy = re.search(r'cy="([-\d.]+)"', a); r = re.search(r'r="([-\d.]+)"', a)
if cx and cy and r:
polys.append(_circle_pts(float(cx.group(1)), float(cy.group(1)), float(r.group(1))))
for m in re.finditer(r'<rect[^>]*>', svg_text):
a = m.group(0)
def g(k, d=0.0):
mm = re.search(k+r'="([-\d.]+)"', a); return float(mm.group(1)) if mm else d
tr = re.search(r'transform="([^"]+)"', a)
polys.append(_rect_pts(g('x'), g('y'), g('width'), g('height'), g('rx', 0),
tr.group(1) if tr else None))
return polys
_cache = {}
def render_icon(name, rgb, out_png, px=192, stroke_w=1.75, pad=1.0):
"""name: 파일명(확장자 무관). rgb: (r,g,b) 0-255. out_png 저장. 이미 있으면 재사용."""
key = (name, tuple(rgb), px)
if key in _cache and os.path.exists(out_png):
return out_png
path = os.path.join(ICON_DIR, name if name.endswith('.svg') else name+'.svg')
with open(path, 'r', encoding='utf-8') as f:
svg = f.read()
polys = _extract(svg)
SS = 4 # supersample
W = px*SS
scale = W/24.0
img = Image.new('RGBA', (W, W), (0, 0, 0, 0))
dr = ImageDraw.Draw(img)
col = (int(rgb[0]), int(rgb[1]), int(rgb[2]), 255)
lw = max(1, int(round(stroke_w*scale)))
rcap = lw/2.0
for poly in polys:
if len(poly) < 2:
continue
sp = [(px_*scale, py_*scale) for px_, py_ in poly]
dr.line(sp, fill=col, width=lw, joint='curve')
# round caps/joins via dots at each vertex
for (vx, vy) in sp:
dr.ellipse([vx-rcap, vy-rcap, vx+rcap, vy+rcap], fill=col)
img = img.resize((px, px), Image.LANCZOS)
img.save(out_png)
_cache[key] = out_png
return out_png
if __name__ == "__main__":
# 테스트: 20종 x 1색 몽타주
outdir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "_iconpng")
os.makedirs(outdir, exist_ok=True)
names = sorted(f[:-4] for f in os.listdir(ICON_DIR) if f.endswith('.svg'))
cols = 5
cell = 96
mont = Image.new('RGBA', (cols*cell, ((len(names)+cols-1)//cols)*cell), (255, 255, 255, 255))
from PIL import ImageDraw as _ID
md = _ID.Draw(mont)
for i, nm in enumerate(names):
outp = os.path.join(outdir, nm+".png")
render_icon(nm, (0x00, 0x66, 0xB3), outp, px=72)
ic = Image.open(outp)
cx = (i % cols)*cell + 12; cy = (i//cols)*cell + 6
mont.alpha_composite(ic, (cx, cy))
md.text((cx, cy+74), nm[:14], fill=(60, 60, 60))
mont.convert('RGB').save(os.path.join(outdir, "_montage.png"))
print("OK", len(names), "icons ->", os.path.join(outdir, "_montage.png"))

View File

@ -1,998 +0,0 @@
# 킨텍스 자동전시시스템 구축 — 제안서 본문 (Proposal Content · 슬라이드 단위)
> 작성: proposal-writer · 작성일: 2026-07-11 · 버전 v1.0
> 입력: `proposal_outline.md`(63슬라이드 배점 가중 목차) · `proposal_strategy.md`(Win Theme 5·리스크 R-1~R-8) · `tech_advisory.md`(§0 라이브 실적/구현 계획 구분·도식 12종 사양·NFR 21·보안 20·공수 ~150M/M·SM) · `rfp_analysis.md`+`rtm.md`(REQ 142)
> 산출 규격: 슬라이드별 [번호·섹션·헤드라인(3초 메시지)·본문 불릿(근거)·도표 지시(tech_advisory 도식 ID)·대응 REQ·출처]. 문체 = **주장 → 근거(라이브 실적/설계 문서) → 효과**. 과장 금지 — tech_advisory §0 구분표에 따라 **[검증됨]**(구축 완료·라이브)과 **[구현 계획]**(설계 확정·미완)을 표기 라벨로 명확 구분.
> 보안 불변: 시크릿(키·비번·IP·SSH) 미기재. AI 생성 이미지는 워터마크·고지 정책 명시.
> deck-designer 지침: 도식 지시의 "도식ID"는 `tech_advisory.md §1-4`의 번호(①~⑫)와 1:1. 아이콘 전량 선(stroke) SVG.
---
## 표기 범례 (전 슬라이드 공통)
- **[검증됨]** = WORK_STATUS(2026-07-11) 라이브 확인분. "이미 기동 중".
- **[구현 계획]** = 설계·데이터모델 확정, 구현 대기. "표준대로 채운다".
- **[협의]** = 발주처 협의·확인 필요(⚠) 항목. 정직 고지 대상.
- **REQ** 열 = 해당 슬라이드가 충족하는 요구사항 ID(RTM 반영 근거).
---
# 0. 표지·목차 (S01~S06)
## S01 · 표지
- **섹션**: 표지
- **헤드라인**: **"신청서를 내는 순간, 시공 후 사진을 먼저 본다."**
- **본문**
- 사업명: 킨텍스(KINTEX) 자동전시시스템 구축
- 서브 슬로건: "설계 → 시각화 → 발주 → 시공 → 운영 → 분석. 킨텍스에서 한 흐름으로 끝난다."
- 제안사·제안일 표기 영역(로고 자리)
- **도표 지시**: 배경 = 나노바나나 시공 예측 히어로 이미지(Before 빈 부스 → After 시공 사진). **워터마크 항상 표기**("AI 생성 예상 이미지 — 실제 시공과 다를 수 있음. 계약·심사 서류 사용 금지"). 도식ID 없음(히어로).
- **REQ**: REQ-A-001, REQ-S-009
- **출처**: strategy §0, PLANNING §1-2
## S02 · 제안 개요
- **섹션**: 표지
- **헤드라인**: "우리는 도면이 아니라 사진으로 컨펌하고, 그 사진으로 발주까지 종결한다."
- **본문**
- 우리는 누구: GUARDiA 표준 프레임워크(WISE/UIWS) 검증 자산 위에 전시 도메인 코어를 얹은 팀
- 왜 우리인가: ①나노바나나 시공 예측 ②PostGIS 실측 공간데이터 ③공사 옥션 폐루프 — **세 결합을 이미 라이브로 검증** [검증됨]
- 차별점 1줄: "말이 아니라 이미 돈다" — 인프라·인증·PostGIS 코어·나노바나나·자동배포가 기동 중
- **도표 지시**: 제안사 핵심 자산 아이콘 3종(선 SVG) — 시공예측·공간데이터·옥션폐루프.
- **REQ**: REQ-A-001, REQ-N-008, REQ-F-025
- **출처**: tech_advisory §0-1, strategy §0
## S03 · 목차
- **섹션**: 표지
- **헤드라인**: "배점을 따라 읽으십시오 — 제안서 무게가 곧 배점 배분입니다."
- **본문**
- P1 사업이해 · P2 추진전략 · P3 기술제안 · P4 아키텍처 · P5 보안·개인정보 · P6 비기능 · P7 멀티테넌시·확장 · P8 사업관리·일정·조직 · P9 기대효과
- P3(기술제안)이 전체 25슬라이드(≈40%) — 배점 35점 정렬
- **도표 지시**: 목차 인덱스(9섹션) + 각 섹션 배점 배지 병기. 도식ID 없음.
- **REQ**: (내비게이션 — 개별 REQ 없음)
- **출처**: outline 배분 총괄표
## S04 · 제안 핵심 요약(1p)
- **섹션**: 표지
- **헤드라인**: "단일 요소는 모방할 수 있으나, 세 결합은 재현하기 어렵다 — 우리만의 해자."
- **본문**
- Win Theme 5선: WT-1 시공 예측 · WT-2 배치·설계·배선 3안+규정검증 · WT-3 옥션 폐루프 · WT-4 보안 3중 격리 · WT-5 멀티테넌트 SaaS
- 3중 해자: 나노바나나 시공 예측 ∩ PostGIS 실측 공간데이터 ∩ 공사 옥션 폐루프
- 배점의 44%(35/80)가 P3에 집중 → 제안 무게 집중 근거
- **도표 지시**: **도식① 3중 해자 벤다이어그램**(3원 60% 겹침, 중앙 교집합 "재현 난이도 높은 결합 = 우리만의 해자(Moat)"). tech_advisory §1-4 ①.
- **REQ**: REQ-A-001·002, REQ-N-008·017, REQ-F-025
- **출처**: strategy §3-3, tech_advisory §1-4 ①
## S05 · 배점 대응 맵
- **섹션**: 표지
- **헤드라인**: "우리 제안은 배점이 큰 곳에 정확히 무게를 실었다."
- **본문**
- AI 시각화·설계(핵심) 20점 → WT-1·WT-2 전면(P3 나노바나나 7슬라이드+배치·설계·배선 5슬라이드)
- 기능 구현 15점 → 모듈맵 1장 조망 + 옥션·관람·BI 상세
- 아키텍처 10·보안 10·비기능 8·사업이해·전략 10·멀티테넌시 5·사업관리 2
- **도표 지시**: 배점-섹션 정렬 매트릭스(80점 히트맵). 도식ID 없음(인포그래픽).
- **REQ**: (배점 정렬 — 전 REQ 계열 조망)
- **출처**: strategy §2, outline 배분 총괄
## S06 · [가정] 고지 간지
- **섹션**: 표지
- **헤드라인**: "가정은 숨기지 않고 드러냅니다 — 실제 공고 확인 시 즉시 재정렬합니다."
- **본문**
- 실제 RFP 부재 → 평가배점·제출규격은 공공 SI 통례 기준 **[가정]** (리스크 R-4 정면 방어)
- 목차는 안전 정렬(사업이해→전략→기술→아키→보안→비기능→확장→관리→기대효과)
- 확인 필요 8항목(C1~C8)은 부록 A에 집약 → 정직성을 강점화
- **도표 지시**: 간단 노트 카드(리스크 R-4 방어 문구). 도식ID 없음.
- **REQ**: (투명 고지 — 개별 REQ 없음)
- **출처**: strategy §4 R-4, rfp_analysis §4·§5
---
# P1. 사업이해 (S07~S10)
## S07 · 사업 배경·목적
- **섹션**: P1 사업이해
- **헤드라인**: "108,011㎡의 아시아 3위 전시장, 준비는 여전히 HWP·CAD 수작업이다."
- **본문**
- 킨텍스 규모: 현 108,011㎡ → 2028년 제3전시장 완공 시 178,000㎡(국내 최대·아시아권 상위)
- 현행 병목: 부스 배치는 CAD 외주 수작업, 신고서류는 HWP 수기, 배선·위치표시도 육안 작도
- 목적: 전시 준비 전 과정을 AI로 자동화해 리드타임·오류·외주비용을 동시 절감
- **도표 지시**: 킨텍스 규모 인포그래픽(면적·홀 수·연간 행사 규모 지표 카드). 도식ID 없음.
- **REQ**: (사업 맥락 — 개별 REQ 없음)
- **출처**: PLANNING §1-1
## S08 · As-Is 프로세스 병목
- **섹션**: P1 사업이해
- **헤드라인**: "D-150부터 D-7까지, 마디마다 사람이 기다리고 CAD를 외주한다."
- **본문**
- D-150 판매·견적: 견적 산출을 담당자가 수기 회신 → 대기 발생
- D-30 배치·설계: CAD 외주 수일~수주, 규정 위반은 홀매니저 육안 검수 병목
- D-25 유틸리티: 전기·네트워크·급배수 신청이 개별 채널로 파편화
- D-7 제출·검수: kxwp 제출·서류 육안 검토
- **도표 지시**: **As-Is 타임라인 다이어그램**(D-데이별 Pain Point 표시). 도식ID 없음(표준 타임라인).
- **REQ**: (병목 진단 — 개별 REQ 없음, To-Be 근거)
- **출처**: PLANNING §3-1·3-2
## S09 · 이해관계자·페르소나
- **섹션**: P1 사업이해
- **헤드라인**: "6역할+2관리자, 각자의 과업·고통·기대를 정확히 안다."
- **본문**
- 주최자(organizer): 배치·행사 총괄 — CAD 수작업 고통
- 참가업체(exhibitor): 도면을 못 읽어 개장일에 결과 첫 대면 — 시공 예측 니즈
- 공사·장치업체(contractor): 리스트+엑셀 매칭, 견적 비교 부재
- 홀매니저(ops): 규정 육안 검수 병목
- 관람객(visitor): 등록·배지·현장 체크인
- 일반 대중(public): 공개 홍보·플로어플랜 열람
- +플랫폼 관리자·테넌트 관리자
- **도표 지시**: 페르소나 카드 6종(과업·Pain·기대 3행). 도식ID 없음.
- **REQ**: REQ-F-081(역할별 포털의 근거)
- **출처**: PLANNING §2
## S10 · To-Be 목표(정량)
- **섹션**: P1 사업이해
- **헤드라인**: "견적은 즉시, 배치는 수 분, 검수는 자동, 결과는 사진으로."
- **본문**
- 견적: 수일 대기 → **즉시**(규칙엔진 2,250원/㎡ 자동 계산)
- 배치: 수주 CAD 외주 → **수 분 3안 자동생성**
- 규정 검수: 육안 → **제출 전 자동 플래깅**
- 위치표시도: 수기 작도 → **좌표 클릭 자동 작도**
- 시공 결과: 개장일 첫 대면 → **신청 시점 사진 예측**
- **도표 지시**: As-Is→To-Be 대비표(정량 개선 5행). 도식ID 없음(표).
- **REQ**: REQ-F-002, REQ-F-007, REQ-F-009, REQ-F-017, REQ-A-001
- **출처**: PLANNING §1-3
---
# P2. 추진전략 (S11~S15)
## S11 · 비전·포지셔닝
- **섹션**: P2 추진전략
- **헤드라인**: "도면과 표를 보여주는 전시테크가 아니라, 사진으로 컨펌하는 베뉴 운영 플랫폼."
- **본문**
- 기존 전시테크는 도면·3D 뷰어 수준 → 우리는 **사진급 시공 예측**으로 의사결정
- 단일 폐루프: 설계 자료가 곧 발주 근거 → 발주까지 한 흐름에서 종결
- 데이터 주권: 온프레미스 Ollama 기본, 외부 AI는 승인 예외만
- **도표 지시**: 비전 슬로건 대형 타이포. 도식ID 없음.
- **REQ**: (포지셔닝 — 개별 REQ 없음)
- **출처**: PLANNING §1-2, strategy §0
## S12 · 3중 해자 전략
- **섹션**: P2 추진전략
- **헤드라인**: "셋 중 하나는 흉내낼 수 있어도, 셋의 결합은 재현 난이도가 높다."
- **본문**
- 원A 나노바나나 시공 예측: image-to-image 구조보존·40초·S1~S7 [검증됨: G1 승인·워커 라이브]
- 원B PostGIS 실측 공간데이터: 부스 폴리곤·트렌치 포인트·배선 LineString 단일 원천 [검증됨: M2~M5 매퍼 라이브]
- 원C 공사 옥션 폐루프: AI 자료→응찰→낙찰→발주 자동전환 [구현 계획: Phase D]
- **도표 지시**: **도식① 3중 해자 벤다이어그램**(S04와 동일·상세 재강조). tech_advisory §1-4 ①.
- **REQ**: REQ-A-001·002, REQ-N-008·017, REQ-F-025·032
- **출처**: strategy §3-3, tech_advisory §0-1·§1-4 ①
## S13 · Win Theme 5선
- **섹션**: P2 추진전략
- **헤드라인**: "다섯 개의 차별화 메시지가 배점 다섯 곳을 정확히 겨눈다."
- **본문**
- WT-1 "도면이 아니라 사진으로 컨펌한다" — 배점 20(AI 시각화 핵심)
- WT-2 "수 분 내 3안, 제출 전 규정 자동검증" — 배점 20+15 교차
- WT-3 "사진 수준 자료로 응찰하고 발주까지 끝낸다" — 배점 15(폐루프)
- WT-4 "AI가 만든 것은 표시하고, 남의 데이터는 차단한다" — 배점 10(보안)
- WT-5 "코드 배포 없이 데이터 온보딩만으로 새 전시관을 연다" — 배점 5(확장)
- **도표 지시**: Win Theme 5카드(각 카드에 배점 배지). 도식ID 없음.
- **REQ**: REQ-A-001, REQ-F-007·025, REQ-S-009·018, REQ-F-077
- **출처**: strategy §1
## S14 · 전시 생애주기 폐루프
- **섹션**: P2 추진전략
- **헤드라인**: "설계 → 시각화 → 발주 → 시공 → 운영 → 분석이 하나의 순환으로 닫힌다."
- **본문**
- 판매·기획(M1) → 설계·시각화(M2~M5) → 발주·계약(M15·M6·M9) → 참가·관람(M10·M11) → 현장운영(M8·M13·M14) → 사후·경영(M16)
- 각 단계 산출물이 다음 단계 입력 → 재입력·중복 작업 제거
- 분석(M16)이 다음 행사 기획(M1)으로 환류
- **도표 지시**: 생애주기 폐루프 순환도(M모듈 매핑). 도식ID 없음(순환도).
- **REQ**: (생애주기 조망 — 상세 REQ는 P3에서 반영)
- **출처**: PLANNING §1-2-1
## S15 · 추진체계·수행조직
- **섹션**: P2 추진전략
- **헤드라인**: "거버넌스·아키텍트·코어·도메인·AI·QA·DevOps가 게이트로 맞물린다."
- **본문**
- 거버넌스: 총괄 PM · PMO · 개발 PM
- 아키텍트 풀: AA · SA · TA · DA · NA(5종 아키텍처 확정 완료)
- 실행: 공통·코어(백엔드·프론트·DB) · 도메인(옥션·관람·CMS·BI·관리자) · AI·시각화 · QA · DevOps
- 게이트: 선행 게이트(G1·G2) + Phase 게이트 QA 통과
- **도표 지시**: **추진체계도(조직도)** — 거버넌스+아키텍트+실행팀 계층. tech_advisory §1-4 말미(추가 표준 도식).
- **REQ**: REQ-N-004(품질 게이트)
- **출처**: 하네스 팀 구성, tech_advisory §5-1
---
# P3. 기술제안 (S16~S40, 25슬라이드 · 배점 35 · 최다 집중)
## P3-A. 모듈 조망
## S16 · 전체 모듈맵
- **섹션**: P3 기술제안
- **헤드라인**: "18개 모듈+공통 레이어를 한 장으로 조망 — P0 코어 4개가 심장이다."
- **본문**
- P0 코어(강조): M2 플로어플랜·M3 부스설계·M4 유틸리티·M5 나노바나나
- P1 도메인: M1 판매·M6 서류·M7 등록·M9 정산·M15 옥션·M10 관람·M12/M17 CMS·M16 BI·M18 관리자
- P2 확장: M8 물류·M11 비즈매칭·M13 wayfinding·M14 현장 IoT
- **공통/시스템관리 레이어(§5B) — 전 모듈 선행 기반** [검증됨: WISE 이식 Flyway V1~V9]: 공통코드·메뉴(F-073), 공통 업무모듈 일지·일정·쪽지·공지·검색(F-074), 회의록 STT·업무보고 통계 PDF(F-075), 통합 알림센터 WebSocket(F-076)
- 멀티테넌시(§1A): tenant_id 격리 기반 [구현 계획]
- 100대 기능 성숙도: 기구현 63·부분 18·신규 19(정량)
- **도표 지시**: **도식② 모듈맵(생애주기 6단계 × 우선순위 P0/P1/P2 매트릭스)** + 하단 공통 레이어 띠. tech_advisory §1-4 ②.
- **REQ**: REQ-F-073·074·075·076, 전 모듈 조망
- **출처**: rfp_analysis §2-1, tech_advisory §1-4 ②·§0
## P3-B. 나노바나나 시공 예측 — WT-1 (S17~S23, 7슬라이드 · 축소 금지)
## S17 · 나노바나나 개요
- **섹션**: P3 기술제안(WT-1)
- **헤드라인**: "시공 전에 사진으로 의사결정한다 — 경쟁 전시테크가 못 하는 차별화."
- **본문**
- 문제: 참가업체는 도면을 못 읽어 개장일에야 결과를 처음 본다(PLANNING §2)
- 해법: image-to-image로 빈 부스(도면/실측)를 시공 후 사진으로 예측
- 실증: Gemini 라이브(G1 소유자 승인 완료), `gemini-3.1-flash-image-preview` 워커 상주 [검증됨]
- 효과: 조감도 외주(비용·시간) 대체, 신청 시점 의사결정
- **도표 지시**: 히어로 Before/After 실물(워터마크 표기). 도식ID 없음(히어로).
- **REQ**: REQ-A-001
- **출처**: strategy §1 WT-1, tech_advisory §0-1·§2 WT-1
## S18 · 표준 샷세트 S1~S7
- **섹션**: P3 기술제안(WT-1)
- **헤드라인**: "정면·야간·통로·내부·Before/After·배선·홀전경 — 7컷을 평균 40초에."
- **본문**
- S1 정면 부스 전경 / S2 야간 조명 연출(조도 배치안 반영) / S3 통로 시점 / S4 부스 내부 / S5 Before/After / S6 배선 오버레이 / S7 홀 전경
- 자동생성은 비용·품질 제어 위해 **S1·S7 한정**, 나머지는 온디맨드 [구현 계획: 샷세트 파이프라인]
- 모든 컷에 워터마크·고지 항상 포함
- **도표 지시**: 샷세트 7컷 그리드(각 컷 워터마크 배지). 도식ID 없음(그리드).
- **REQ**: REQ-A-001, REQ-F-019(조명 조도 배치안)
- **출처**: tech_advisory §2 WT-1, PLANNING §6
## S19 · image-to-image 구조보존
- **섹션**: P3 기술제안(WT-1)
- **헤드라인**: "골격·치수·통로·카메라 앵글은 잠그고, 스타일·재질만 사실화한다."
- **본문**
- 참조 이미지 `inlineData` + 구조화 프롬프트 사전(BOOTH_TYPES·BOOTH_STYLES·FIXTURE_LAYERS)
- **보존(잠금)**: 부스 골격·치수·통로·카메라 앵글 → 왜곡·과장 방지
- **교체(사실화)**: 스타일·재질·마감만 → 사진 리얼리티
- 검증 패턴: ReRoomAI image-to-image 보존/교체 분리 이식(reroomai-source.md)
- **도표 지시**: 보존/교체 파라미터 도식(잠금 항목 vs 교체 항목 2열). 도식ID 없음.
- **REQ**: REQ-A-002, REQ-A-003
- **출처**: tech_advisory §2 WT-1, docs/analysis/reroomai-source.md
## S20 · AI 파이프라인 아키텍처
- **섹션**: P3 기술제안(WT-1)
- **헤드라인**: "생성은 전면 비동기 — 사용자는 대기하지 않고, 완료는 WebSocket으로 밀어준다."
- **본문**
- 흐름: 사용자 요청 → Spring RenderJob 생성·쿼터 확인 → Redis 큐(leftPush) → Python 워커(BLPOP) → Gemini image-to-image → 워터마크 후처리·S6 로컬 PIL 래스터 → 오브젝트 스토리지 → pub/sub → 백엔드 relay → WebSocket `/topic/render/{jobId}` → 프론트 완료 푸시
- degraded 분기: G1 미승인/Gemini 장애 시 워커 목(mock) 응답·구조·워터마크 유지(REQ-N-005)
- 성공 시에만 쿼터 차감, `GEMINI_API_KEY`는 워커 env only
- **도표 지시**: **도식③ 나노바나나 파이프라인 플로우(8박스 좌→우)**. tech_advisory §1-4 ③.
- **REQ**: REQ-A-007, REQ-N-006
- **출처**: tech_advisory §1-4 ③·§2 WT-1
## S21 · Before/After·배선 오버레이
- **섹션**: P3 기술제안(WT-1)
- **헤드라인**: "드래그 한 번으로 빈 부스와 시공 후를 비교하고, 배선을 색으로 겹쳐 본다."
- **본문**
- Before/After: 좌우 분할 카드 + 중앙 드래그 핸들, ARIA slider·키보드 좌우 조작(WCAG AA)
- S6 배선 오버레이: Gemini 미경유, 백엔드/워커 로컬 PIL **결정적 래스터 합성**(좌표 정확성·비용/지연 제외)
- 색상 규약: 전기=적색, 네트워크=청색, 급배수=녹색
- **도표 지시**: **도식④ Before/After 슬라이더**(좌 "Before: 빈 부스", 우 "After: 시공 후 예상(AI 생성·워터마크)", 하단 워터마크 배지). tech_advisory §1-4 ④.
- **REQ**: REQ-A-004, REQ-A-005
- **출처**: tech_advisory §1-4 ④·§2 WT-1
## S22 · RenderJob 방어·품질
- **섹션**: P3 기술제안(WT-1)
- **헤드라인**: "다층 방어로 실패를 흡수하고, 성공했을 때만 쿼터를 쓴다."
- **본문**
- 크기가드·mimeType 자동감지·SAFETY 분기·친화적 한글 에러
- 간판 한글 렌더 실패 시 **간판영역 후처리 합성**(리스크 R-5 방어)
- 대량 생성 제어: 자동생성 S1·S7 한정 + 동일 스키마해시 캐시 + 행사별 쿼터(성공 시만 차감)
- **도표 지시**: RenderJob 상태 시퀀스(방어 계층 표시). 도식ID 없음(시퀀스).
- **REQ**: REQ-A-006, REQ-A-008, REQ-N-007
- **출처**: tech_advisory §2 WT-1, strategy §4 R-5
## S23 · AI 신뢰 원칙
- **섹션**: P3 기술제안(WT-1)
- **헤드라인**: "결정적 알고리즘이 중심, LLM은 보조, 최종 확정은 사람 — 워터마크는 불변."
- **본문**
- 3층 신뢰: 결정적 알고리즘(제약 솔버·PostGIS) 중심 + LLM 보조 + 사람 최종 확정
- 워터마크 항상 포함(`watermarkRequired:true`), 계약·심사 서류 사용 금지 고지 제거 불가
- AI 답변은 RAG 근거·인용·환각 차단(abstain)으로 오판 리스크 차단(리스크 R-6 방어)
- **도표 지시**: AI 3층 신뢰 구조 도식(결정론→LLM→사람). 도식ID 없음.
- **REQ**: REQ-S-009, REQ-A-017
- **출처**: tech_advisory §2 WT-2·WT-4, strategy §4 R-6
## P3-C. 배치·설계·배선 AI 자동화 — WT-2 (S24~S28, 5슬라이드)
## S24 · 플로어플랜 스튜디오(M2)
- **섹션**: P3 기술제안(WT-2)
- **헤드라인**: "제약 솔버가 상이한 목표로 정확히 3안 — 사람이 선택·병합·확정한다."
- **본문**
- 3안 자동생성: **결정적 제약 솔버**(비-LLM)가 통로 효율/프리미엄 부스/균형 등 목표별 생성, LLM은 조건해석만(REQ-A-009)
- 선택/병합 편집 + 최종안 버전기록·부스좌표 확정·초대링크(REQ-F-008·010)
- M2 매퍼(PostGIS 공간 SQL) [검증됨], 3안 생성·병합·집계 API [구현 계획]
- **도표 지시**: **도식⑤ 3안 비교 화면**(3열 카드·각 카드 최적화 목표·미니 배치도·지표, 하단 액션바 선택/병합/재검증). tech_advisory §1-4 ⑤.
- **REQ**: REQ-F-007·008·010, REQ-A-009
- **출처**: tech_advisory §1-4 ⑤·§2 WT-2
## S25 · 배치 규정 자동검증
- **섹션**: P3 기술제안(WT-2)
- **헤드라인**: "피난통로·바닥하중·비상구를 제출 전에 자동으로 플래깅한다."
- **본문**
- 룰엔진(`compliance-v*.json` 버전 데이터) + PostGIS 공간 판정: 통로폭 `ST_Distance/ST_Buffer`, 비상구 `ST_Intersects`, 홀 이탈 `ST_Contains`, 바닥하중 홀별
- 병합 후 재검증 필수, 위반 시 `COMPLIANCE_BLOCKED`(422)
- 원칙: 규정검증=플래깅까지, 구조안전 판정은 구조기술사/킨텍스(Non-Goal 명시, 리스크 R-6)
- **도표 지시**: 규정 위반 플래그 대시보드(위반 항목·홀별 하중·비상구 교차 목록). 도식ID 없음(대시보드).
- **REQ**: REQ-F-009, REQ-A-010
- **출처**: tech_advisory §2 WT-2, strategy §4 R-6
## S26 · 부스 설계 스튜디오(M3)
- **섹션**: P3 기술제안(WT-2)
- **헤드라인**: "조립부스 3D 프리뷰·독립부스 3안, 높이·리깅·방염·이격은 사전 검증."
- **본문**
- 조립부스 옵션·3D 프리뷰(REQ-F-011), 독립부스 설계 3안·선택/병합·버전(REQ-F-012)
- 설계 규정 사전검증: 높이 5m·리깅 6.5~8.5m·방염·이격(REQ-F-013)
- 도면 업로드 비전 추출·검증(REQ-F-014·A-020)
- **도표 지시**: 설계 3안 + 규정 체크리스트(높이·리깅·방염·이격). 도식ID 없음.
- **REQ**: REQ-F-011·012·013·014, REQ-A-020
- **출처**: tech_advisory §2 WT-2, PLANNING §M3
## S27 · 유틸리티 설계(M4)
- **섹션**: P3 기술제안(WT-2)
- **헤드라인**: "기기→kW→분전반을 자동 산출하고, 트렌치→분전반 최단 배선을 PostGIS가 그린다."
- **본문**
- 전기 용량·분전반 자동산출: 기기목록→kW 합산→분전반 산출(REQ-F-015)
- 배선 경로: 최근접 트렌치 KNN(`geom <-> point`)→최단 배선 LineString(`ST_Length`, 통로 횡단 최소 휴리스틱)(REQ-F-016)
- 실측 근거: 홀별 평면도 JPG 15장 + CAD "평면,트렌치.dwg" 확보 [검증됨], 미확보 구간은 "가정 좌표(실측 대기)" 라벨(리스크 R-2)
- **도표 지시**: **도식⑥ PostGIS 배선 오버레이**(부스 폴리곤·트렌치 포인트◆·분전반▣·색상별 LineString, 전기 적/네트 청/급배수 녹). tech_advisory §1-4 ⑥.
- **REQ**: REQ-F-015, REQ-F-016
- **출처**: tech_advisory §1-4 ⑥·§2 WT-2, strategy §4 R-2
## S28 · 위치표시도·유틸리티 신청
- **섹션**: P3 기술제안(WT-2)
- **헤드라인**: "좌표 클릭으로 위치표시도를 자동 작도하고, 신청·견적은 D-25 역산으로 챙긴다."
- **본문**
- 위치표시도 자동생성: 좌표 클릭 자동 작도, 수기 작도 폐지(REQ-F-017)
- 네트워크·급배수·압축공기 신청 + 마감 리마인더(D-25 역산)(REQ-F-018)
- 자동 견적 연동
- **도표 지시**: 위치표시도 자동생성 예시(좌표 클릭 → 작도 결과). 도식ID 없음.
- **REQ**: REQ-F-017, REQ-F-018
- **출처**: tech_advisory §2 WT-2, PLANNING §M4
## P3-D. 공사 옥션 폐루프 — WT-3 (S29~S32, 4슬라이드)
## S29 · 옥션 개요·폐루프
- **섹션**: P3 기술제안(WT-3)
- **헤드라인**: "AI가 만든 설계 자료가 곧 응찰 근거이고, 한 흐름에서 발주까지 끝난다."
- **본문**
- AI 자료 패키지(M2 배치+M3 설계+M4 배선/BOQ+M5 시공예측 이미지)가 응찰 근거(`attached_refs`)
- 옥션 유형 설정형(REVERSE 역경매/RFQ/FIXED), 물량서(BOQ) 자동산출(REQ-F-031)
- 데이터모델: `TB_AUCTION 1─N TB_QUOTATION ─ TB_AWARD` [구현 계획: Phase D-M15]
- **도표 지시**: **도식⑦ 옥션 폐루프 흐름도**(AI 자료 패키지→옥션 개설→등록 게이트→견적 제출→실시간 순위→종합평가 낙찰→발주 자동전환). tech_advisory §1-4 ⑦.
- **REQ**: REQ-F-025·026, REQ-F-031
- **출처**: tech_advisory §1-4 ⑦·§2 WT-3
## S30 · 역경매·실시간 순위
- **섹션**: P3 기술제안(WT-3)
- **헤드라인**: "라운드·마감·최저가·내 위치를 실시간으로, 자동마감까지."
- **본문**
- 실시간 순위 = Redis 정렬셋 + WebSocket `/topic/auction/{auctionId}` 델타 푸시
- 라운드 마감 = Redis 타이머(자동마감)
- 익명 옵션·내 위치 표시(REQ-F-027)
- 스캐폴드 4계층·큐·WS 패턴 확정으로 실현성 입증 [구현 계획]
- **도표 지시**: 실시간 순위 보드 목업(업체 순위·최저가·잔여 시간). 도식ID 없음(목업).
- **REQ**: REQ-F-027
- **출처**: tech_advisory §2 WT-3
## S31 · 종합평가 낙찰
- **섹션**: P3 기술제안(WT-3)
- **헤드라인**: "가격만이 아니라 평판·납기까지 가중 스코어로 투명하게 낙찰한다."
- **본문**
- 가중 스코어: 가격+평판+납기(`weight` jsonb, 옥션별 설정형, 예: 가격 0.6/평판 0.25/납기 0.15)(REQ-F-028)
- 항목별 비교표·선정 사유 제시
- 등록업체 AI 매칭 추천(REQ-F-030), 물량서(BOQ) 근거(REQ-F-031)
- **도표 지시**: **도식⑧ 종합평가 스코어 비교표**(업체 행 × 평가항목 열 + 가중 종합점수 + 낙찰 하이라이트). tech_advisory §1-4 ⑧.
- **REQ**: REQ-F-028, REQ-F-030, REQ-F-031
- **출처**: tech_advisory §1-4 ⑧·§2 WT-3
## S32 · 등록 게이트·발주 전환
- **섹션**: P3 기술제안(WT-3)
- **헤드라인**: "미등록 업체는 응찰 자체가 차단(403)되고, 낙찰은 계약·발주로 자동 전환된다."
- **본문**
- 등록 게이트: 미등록 업체 응찰 원천 차단 `NOT_REGISTERED_COMPANY`(403), M7이 `TB_COMPANY.kintex_registered` 검증 권위 → 킨텍스 "등록업체 시공 필수" 규정을 시스템이 강제(REQ-S-007)
- 발주 전환: Award → 계약/발주 자동전환(M6·M9 연동, 이벤트 디커플·순환 금지)(REQ-F-032)
- **도표 지시**: 등록 검증 시퀀스 + 발주 전환 흐름. 도식ID 없음(시퀀스).
- **REQ**: REQ-F-029, REQ-F-032, REQ-S-007
- **출처**: tech_advisory §2 WT-3·§4 REQ-S-007
## P3-E. 판매·서류·워크플로 (S33~S34)
## S33 · 판매·홀배정·견적(M1)
- **섹션**: P3 기술제안
- **헤드라인**: "가용성은 실시간 캘린더로, 견적은 규칙엔진으로 즉시, 신청은 웹폼→HWP로."
- **본문**
- 홀/반홀 가용성 실시간 캘린더(REQ-F-001)
- 규칙엔진 자동 견적(2,250원/㎡, 요율 공식본 [협의: C4])(REQ-F-002)
- 배정신청 웹폼→HWP 자동생성(REQ-F-003), 부스 판매 인벤토리(REQ-F-004), 온라인 셀프 선택·판매(REQ-F-005)
- 임대계약 전자서명(선택, REQ-F-006)
- **도표 지시**: 견적 규칙엔진 계산 흐름(면적×요율→견적). 도식ID 없음.
- **REQ**: REQ-F-001·002·003·004·005·006
- **출처**: PLANNING §M1, rfp_analysis(C4 요율)
## S34 · 서류·마일스톤(M6)
- **섹션**: P3 기술제안
- **헤드라인**: "D-150/30/25/7을 역산해 알리고, 웹폼은 HWP/PDF로, 검수는 AI가 돕는다."
- **본문**
- 마일스톤 자동생성·역산 알림(REQ-F-020), 신고서류 웹폼→HWP/PDF 자동생성 7종(REQ-F-021)
- 서류 버전·전자결재 워크플로(REQ-F-023), OCR 계약·납품서 자동등록(REQ-F-024·A-020)
- AI 서류 검수(누락·불일치)(REQ-A-011), 규정 자연어 챗봇(근거·인용)(REQ-A-012)
- kxwp 제출 릴레이: 웹폼→HWP/PDF까지 API 무관하게 완결, 릴레이 단계만 킨텍스 IT 협의 [협의: C2, 리스크 R-3](REQ-F-022)
- **도표 지시**: 마일스톤 역산 타임라인(D-데이별 서류·알림). 도식ID 없음(타임라인).
- **REQ**: REQ-F-020·021·022·023·024, REQ-A-011·012
- **출처**: tech_advisory §2, strategy §4 R-3
## P3-F. 관람·참가 (S35~S36)
## S35 · 관람객 등록·배지·리드(M10)
- **섹션**: P3 기술제안
- **헤드라인**: "사전등록→모바일 배지→현장 체크인→리드까지, 오프라인에서도 끊기지 않는다."
- **본문**
- 온라인 사전등록(유형별·중복검증)(REQ-F-037), 모바일 배지/QR 발급(REQ-F-038)
- 현장 QR 체크인·즉석 인쇄·집계(REQ-F-039), **오프라인 체크인 폴백**(REQ-F-040)
- 참가업체 리드캡처(REQ-F-041), AI 리드 스코어링·분류(REQ-F-042·A-018), 세그먼트 EDM(REQ-F-043)
- 티켓발권·AI폼빌더·스마트배지(선택, REQ-F-044)
- **도표 지시**: 등록→체크인→리드 여정도. 도식ID 없음(여정도).
- **REQ**: REQ-F-037·038·039·040·041·042·043·044, REQ-A-018
- **출처**: PLANNING §M10, tech_advisory §7
## S36 · 비즈매칭·마케팅·공개사이트(M11·M12·M17)
- **섹션**: P3 기술제안
- **헤드라인**: "AI가 매칭을 추천하고, 다국어 공개사이트·마이크로사이트가 SEO로 관람객을 모은다."
- **본문**
- AI 비즈매칭 추천(REQ-F-045·A-018), 미팅 슬롯·성과 리포트(REQ-F-046), 이벤트 모바일앱·세션·채팅·설문(REQ-F-047)
- 공개 홍보 사이트(SEO·다국어)(REQ-F-048), 공개 인터랙티브 플로어플랜(REQ-F-049), 세그먼트 EDM·캠페인 자동화(REQ-F-050), 스폰서십 패키지·대시보드(REQ-F-051)
- CMS 게시 워크플로·버전(REQ-F-067), 마이크로사이트(REQ-F-068), 다국어 콘텐츠·hreflang(REQ-F-069)
- AI 카피·이미지 초안·다국어 번역(REQ-A-015)
- **도표 지시**: 공개사이트 + 마이크로사이트 목업. 도식ID 없음(목업).
- **REQ**: REQ-F-045·046·047·048·049·050·051·067·068·069, REQ-A-015·018
- **출처**: PLANNING §M11·M12·M17, tech_advisory §1-2
## P3-G. 정산·경영분석·현장 (S37~S40)
## S37 · 정산·결제(M9)
- **섹션**: P3 기술제안
- **헤드라인**: "납부는 스케줄로, 결제는 PG로, 예치금은 실사용까지 투명하게 정산한다."
- **본문**
- 납부 스케줄 자동생성·알림(REQ-F-056), 유틸리티·부대 PG 결제(REQ-F-057)
- 예치금 실사용 정산 투명화(REQ-F-058), 세금계산서·옥션 수수료 정산(REQ-F-059)
- 환불·취소 규정 자동(선택, REQ-F-060)
- **도표 지시**: 정산 흐름도(납부→결제→예치금 정산→세금계산서). 도식ID 없음.
- **REQ**: REQ-F-056·057·058·059·060
- **출처**: PLANNING §M9
## S38 · 경영분석 BI(M16)-1
- **섹션**: P3 기술제안
- **헤드라인**: "가동률·매출구성·행사별 P&L·㎡당 수익을 운영사 관점 한 화면에."
- **본문**
- 홀·기간별 가동률 대시보드(REQ-F-061), 매출 구성·행사별 P&L(REQ-F-062)
- 전시장 ROI·RevPAD·㎡당 수익(REQ-F-063)
- 데이터: BI 집계·조회는 읽기복제 오프로드(운영 프라이머리 보호)
- **도표 지시**: BI 대시보드 목업(Recharts — 가동률·매출구성·P&L·RevPAD). 도식ID 없음(목업).
- **REQ**: REQ-F-061·062·063
- **출처**: PLANNING §M16, tech_advisory §3 REQ-N-010
## S39 · 경영분석 BI(M16)-2
- **섹션**: P3 기술제안
- **헤드라인**: "리텐션·LTV·참가사 ROI를 권한 격리로, AI가 수요를 예측하고 KPI를 브리핑한다."
- **본문**
- 참가사 리텐션·LTV 코호트(REQ-F-064), 참가업체 관점 ROI(권한 격리)(REQ-F-065), 경영진 KPI 대시보드·드릴다운(REQ-F-066)
- AI 수요예측·수율/동적가격(REQ-A-013), AI KPI 브리핑·자연어 조회 Text-to-SQL(REQ-A-014)
- BI 마트는 개인 식별자 비반입(집계만), 참가업체는 자사 데이터만(격리)
- **도표 지시**: KPI 드릴다운 + AI 인사이트 카드. 도식ID 없음.
- **REQ**: REQ-F-064·065·066, REQ-A-013·014
- **출처**: PLANNING §M16, tech_advisory §4 REQ-S-013
## S40 · 현장운영·물류(M8·M13·M14)
- **섹션**: P3 기술제안
- **헤드라인**: "반입/반출 슬롯·통행증 QR·wayfinding·이상탐지 — 선택 기능은 시설 협의 전제로."
- **본문**
- 반입/반출 슬롯 예약(REQ-F-033), 통행증 QR·중량물 우선(REQ-F-034), 지게차·부대장비 렌탈(REQ-F-035), 철거 대기열 시뮬레이션(REQ-F-036)
- 실내 wayfinding·POI·경로(REQ-F-052), 실시간 혼잡 모니터·예측(REQ-F-053·A-019), 홀 전력부하·주차 점유(REQ-F-054), 안전 위반·이상탐지(REQ-F-055·A-019)
- 사이니지 콘텐츠 배포(REQ-F-070)
- **[협의]**: wayfinding 비콘·IoT·사이니지 HW는 킨텍스 시설 인프라 협의 전제(C7, 리스크 R-7). 미협의 시에도 코어 가치(WT-1~4)는 HW 독립
- **도표 지시**: 현장 운영 상황판(혼잡·전력·안전·물류 슬롯). 도식ID 없음(상황판).
- **REQ**: REQ-F-033·034·035·036·052·053·054·055·070, REQ-A-019
- **출처**: PLANNING §M8·M13·M14, strategy §4 R-7
---
# P4. 아키텍처·기술스택 (S41~S45, 배점 10)
## S41 · 전체 아키텍처
- **섹션**: P4 아키텍처
- **헤드라인**: "확정 스택으로 이미 기동 중 — 도상이 아니라 동작하는 아키텍처다."
- **본문**
- 4-Tier + 사이드카: React 18/19(Vite·TS) 역할별 분리 → SSO(JWT+2FA)·이중 RBAC → 공유 Spring Boot 3.2.5(Java 17)+MyBatis 모듈러 모놀리스 → PostgreSQL+PostGIS(+읽기복제)/Redis/오브젝트 스토리지
- 사이드카: 나노바나나 Python 워커(이미지) + Claude AI(AiTextRouter·Ollama 폴백)(텍스트)
- 실증: 인프라·CI/CD·인증·PostGIS 코어·나노바나나 [검증됨]
- **도표 지시**: **도식⑨ 시스템 아키텍처 다이어그램(6밴드 계층 + 우측 외부 게이트 컬럼)**. tech_advisory §1-4 ⑨.
- **REQ**: REQ-A-016, REQ-F-081(역할별 프론트)
- **출처**: tech_advisory §1-1·§1-4 ⑨·§0-1
## S42 · 단일 공간 데이터 원천
- **섹션**: P4 아키텍처
- **헤드라인**: "부스 폴리곤·트렌치 포인트·배선 LineString은 하나의 원천에서만 산다."
- **본문**
- M2 폴리곤·M4 배선 매퍼가 권위, 소비 모듈은 조회만·중복 저장 금지(REQ-N-017)
- PostGIS 단일 공간 원천(SRID 0 홀 로컬 미터), 전 `geom` GiST 인덱스 실시간(REQ-N-008)
- 검증 게이트: ArchUnit·계약 검수로 중복 저장 방지
- **도표 지시**: 공간 데이터 원천 모델(폴리곤·포인트·LineString 단일 원천). 도식ID 없음(개념 모델).
- **REQ**: REQ-N-008, REQ-N-017
- **출처**: tech_advisory §3(N-008·017), data.md §0-2
## S43 · 확장성·비동기
- **섹션**: P4 아키텍처
- **헤드라인**: "워커는 큐 깊이로 독립 확장하고, BI·공개는 읽기복제로 오프로드한다."
- **본문**
- 워커 독립 스케일: 나노바나나·서류·EDM 워커 큐 깊이 기반 증감(REQ-N-009)
- 읽기복제 오프로드: BI 집계·공개 조회·플로어플랜 열람 읽기복제 라우팅(REQ-N-010)
- 공개영역 CDN·요청률 상한(REQ-N-019), 워커 장애 재큐(멱등 Job)(REQ-N-011)
- 역할별 프론트 번들 분리(REQ-F-081, 방식 확정 대기 [구현 계획])
- **도표 지시**: 배포 토폴로지도(프론트·백엔드 2인스턴스·워커 N·읽기복제·CDN). 도식ID 없음(토폴로지).
- **REQ**: REQ-N-009·010·011·019, REQ-F-081
- **출처**: tech_advisory §3, system.md §3-1
## S44 · AI 플랫폼 라우팅
- **섹션**: P4 아키텍처
- **헤드라인**: "Claude 기본, 장애·폐쇄망이면 Ollama 폴백 — LLM 직접호출은 금지, 중앙 경유만."
- **본문**
- `AiTextRouter`: Claude 기본(`api.anthropic.com` 승인 예외) → Ollama 폴백(REQ-A-016)
- 나노바나나는 큐/콜백만(백엔드는 이미지 미취급), degraded 시 목 응답(REQ-A-007, N-005)
- AiTextRouter/AiConfig는 UIWS 패턴 [구현 계획: 착수 대기]
- **도표 지시**: AI 라우터 폴백 흐름(Claude→실패→Ollama). 도식ID 없음.
- **REQ**: REQ-A-007, REQ-A-016
- **출처**: tech_advisory §1-2 AI(텍스트)·§6
## S45 · 연동·마스터데이터
- **섹션**: P4 아키텍처
- **헤드라인**: "외부는 게이트웨이로, 룰셋·요율은 버전 데이터로 무중단 개정한다."
- **본문**
- API·웹훅·CRM/ERP 연동 게이트웨이(선택, REQ-F-080)
- 룰셋·마스터데이터 버전관리(요율·규정 룰셋), M18 백오피스·감사 기록으로 무중단 개정(REQ-F-072)
- 외부 게이트: Gemini(워커존)·Claude(백엔드)·PG 결제·kxwp 릴레이·등록업체 DB 739
- **도표 지시**: 연동 인터페이스 맵(내부↔외부 게이트웨이). 도식ID 없음.
- **REQ**: REQ-F-072, REQ-F-080
- **출처**: tech_advisory §1-2, §6-3
---
# P5. 보안·개인정보 (S46~S50, 배점 10 · WT-4)
## S46 · 보안 개요·불변 원칙
- **섹션**: P5 보안·개인정보
- **헤드라인**: "AI가 만든 것은 표시하고, 남의 데이터는 차단한다 — REQ-S 20항목 전량 커버."
- **본문**
- 불변 원칙 4대: 외부 API 격리 · 워터마크 항상 · PII 격리 · 테넌트 fail-closed
- 보안 성숙도: 인증·워터마크·admin env는 [검증됨](설계가 아니라 동작하는 통제)
- REQ-S 20항목 커버리지 100%
- **도표 지시**: REQ-S 20항목 커버리지 표(항목·충족방안·상태). 도식ID 없음(표).
- **REQ**: REQ-S 전량(001~020)
- **출처**: tech_advisory §4·§0-1
## S47 · 보안영역 분리
- **섹션**: P5 보안·개인정보
- **헤드라인**: "공개 DMZ는 쓰기 권한이 없고, 데이터존은 아웃바운드가 0이다."
- **본문**
- 존 계층: 인터넷→엣지(CDN·WAF)→DMZ(공개, 쓰기 없음)→내부망(인증)→AI 워커존(egress 제한)→데이터존(최내곽, 아웃바운드 전면 차단)
- 철칙: DMZ→데이터존 직접 접근 절대 금지, 데이터존 아웃바운드 0(유출 경로 제거)
- egress 화이트리스트: 승인 4목적지만(Claude·Gemini·PG·SMTP), 그 외 DROP
- fail-closed 다층 테넌트 격리와 정합(REQ-S-018)
- **도표 지시**: **도식⑩ 보안영역 분리도(존 다이어그램 + 관리존 + egress 컬럼)**. tech_advisory §1-4 ⑩.
- **REQ**: REQ-S-020, REQ-S-018, REQ-N-018
- **출처**: tech_advisory §1-4 ⑩·§4, network.md §2
## S48 · 인증·접근통제
- **섹션**: P5 보안·개인정보
- **헤드라인**: "JWT+2FA는 이미 라이브, admin 비번은 env로 재시드, RBAC는 3중이다."
- **본문**
- JWT(HS256)+TOTP 2FA(RFC6238)+로그인 실패 잠금 [검증됨: Flyway V7/V8](REQ-S-005)
- admin 비번 env 재시드(AES-256-GCM, admin123 금지) [검증됨: env 시더](REQ-S-004)
- 행사 RBAC 3중(테넌트 격리 ⊃ 행사 RBAC, 열람/편집 게이트)(REQ-S-006), admin/internal 엔드포인트 게이트(REQ-S-008)
- RBAC·감사·시스템설정 백오피스(REQ-F-071)
- 미등록 업체 차단(403)과 연계(REQ-S-007)
- **도표 지시**: 인증 흐름 + RBAC 3중 구조. 도식ID 없음.
- **REQ**: REQ-S-004·005·006·007·008, REQ-F-071
- **출처**: tech_advisory §4·§0-1(라이브)
## S49 · AI·이미지 보안
- **섹션**: P5 보안·개인정보
- **헤드라인**: "생성 이미지는 워터마크·금지 고지 불변, 외부 API 키는 워커 env에만 산다."
- **본문**
- 워터마크 항상: M5 응답 `watermarkRequired:true`+text+notice(계약·심사 서류 금지), 제거 불가 [검증됨](REQ-S-009)
- 외부 API 정책: Ollama 기본 + Claude/Gemini만 승인 예외, egress 그 외 DROP(REQ-S-001)
- `GEMINI_API_KEY` 워커 env only(백엔드 미취급, 워커존 단일 egress)(REQ-S-002)
- RAG 근거·인용·환각 차단(abstain)(REQ-A-017)
- **도표 지시**: 워터마크 실물 + 외부 API 격리(워커존 단일 egress). 도식ID 없음.
- **REQ**: REQ-S-001·002·009, REQ-A-017
- **출처**: tech_advisory §4·§2 WT-4
## S50 · 개인정보·감사·안전코딩
- **섹션**: P5 보안·개인정보
- **헤드라인**: "PII는 동의·마스킹·암호화·응답 제외, 민감 액션은 전수 감사, 코드는 안전하게."
- **본문**
- PII: 동의 분리(privacy/marketing/share)·최소수집·`*_enc` AES-256-GCM·응답 완전 제외·보존/파기(REQ-S-013)
- 자격증명·키 완전 제외(응답/로그/커밋 마스킹)(REQ-S-003), 스택트레이스 미노출(REQ-S-010), DataAccessException 요약 매핑(REQ-S-011)
- 민감 액션 감사 전수(`TB_AUDIT_LOG` tenant_id 포함, `@Audited` AOP)(REQ-S-012)
- 안전코딩: 입력 검증(REQ-S-014)·XSS/CSP(REQ-S-015)·인젝션 차단 MyBatis `#{}`(REQ-S-016)·IDOR/CSRF/JWT 회전(REQ-S-017)
- 관측성: 로그·메트릭·트레이스에 PII/스택트레이스 미기록(REQ-N-018)
- **도표 지시**: PII 마스킹 예시 + 감사로그 표. 도식ID 없음.
- **REQ**: REQ-S-003·010·011·012·013·014·015·016·017, REQ-N-018
- **출처**: tech_advisory §4, data.md §5
---
# P6. 비기능 (S51~S54, 배점 8)
## S51 · 성능·가용성 SLA
- **섹션**: P6 비기능
- **헤드라인**: "코어 SLO 99.5%(성수기 99.9% 지향), 무중단 롤링·PITR·degraded로 버틴다."
- **본문**
- 코어 SLO 99.5%(성수기 99.9% 지향), 무상태 백엔드+Redis 외부화(REQ-N-001)
- 백엔드 2인스턴스·헬스체크 LB·롤링 무중단(REQ-N-002), PG 스탠바이 복제·자동 failover·PITR(REQ-N-003)
- degraded 모드: Gemini 장애=목 응답, Claude 장애=Ollama 폴백, PG 복제지연=프라이머리 폴백(REQ-N-005)
- 워커 장애 재큐(멱등)(REQ-N-011)
- **도표 지시**: SLA 수치표 + HA 구성도(프론트·백엔드·Redis·PG·워커 다중화). 도식ID 없음(표+구성도).
- **REQ**: REQ-N-001·002·003·005·011
- **출처**: tech_advisory §3·§6-2, system.md §3-2
## S52 · 나노바나나 성능
- **섹션**: P6 비기능
- **헤드라인**: "단건 평균 40초, 1024px 전처리·자동생성 한정·캐시·쿼터로 대량을 제어한다."
- **본문**
- 단건 평균 ≤40s, 전면 비동기(WS 완료 푸시)(REQ-N-006)
- 전처리: 클라 Canvas 1024px 다운스케일 + JPEG 0.85
- 대량 제어: 자동생성 S1·S7 한정 + 동일 스키마해시 캐시 + 행사별 쿼터(성공 시만 차감)(REQ-N-007)
- **도표 지시**: 렌더 성능 지표 카드(40초·1024px·캐시 히트·쿼터). 도식ID 없음.
- **REQ**: REQ-N-006, REQ-N-007
- **출처**: tech_advisory §3, system.md §3-3
## S53 · 접근성·다국어·테마·반응형
- **섹션**: P6 비기능
- **헤드라인**: "WCAG 2.1 AA+KWCAG 2.2, 한/영/중/일, 라이트/다크, 데스크톱/모바일 전부."
- **본문**
- 접근성: WCAG 2.1 AA + 공공 KWCAG 2.2 병행(REQ-N-012), 캔버스/맵 비시각 대안(데이터 테이블·키보드 부스 선택)(REQ-N-013)
- 다국어: i18n 외부화(한 기본+영/중/일)·hreflang·로케일 포맷(REQ-N-014)
- 테마: 라이트/다크 CSS 토큰 2벌·prefers-color-scheme(REQ-N-015)
- 반응형: 데스크톱=설계/에디터, 모바일=현장/조회/승인(REQ-N-016)
- [구현 계획: 전면 스윕 대기]
- **도표 지시**: 접근성·다국어 매트릭스. 도식ID 없음(매트릭스).
- **REQ**: REQ-N-012·013·014·015·016, REQ-F-069(다국어 정합)
- **출처**: tech_advisory §3, PLANNING §8
## S54 · 관측성·데이터
- **섹션**: P6 비기능
- **헤드라인**: "로그·메트릭·트레이스에 PII·스택트레이스는 남기지 않고, BI는 배치 ETL로 정합한다."
- **본문**
- 관측성: 로그·메트릭·트레이스 관리존 수집, 자격증명·PII·스택트레이스 미기록(`include-stacktrace: never`)(REQ-N-018)
- BI 데이터마트: 스타 스키마(FACT/DIM) 야간 배치 ETL 또는 읽기복제 적재, KpiSnapshot(원천-집계 정합 오차 0)(REQ-N-021)
- 공개영역 CDN·요청률 상한과 정합(REQ-N-019)
- **도표 지시**: 관측성 파이프라인(수집→저장→대시보드). 도식ID 없음.
- **REQ**: REQ-N-018, REQ-N-021, REQ-N-019
- **출처**: tech_advisory §3, data.md §6, tech.md §5
---
# P7. 멀티테넌시·확장 (S55~S57, 배점 5 · WT-5)
## S55 · 멀티테넌트 SaaS 비전
- **섹션**: P7 멀티테넌시·확장
- **헤드라인**: "KINTEX #1에서 검증하고, COEX #2로 확장하며, 제3전시장을 표준으로 맞는다."
- **본문**
- KINTEX=기준 테넌트 #1, COEX=테넌트 #2(온보딩 대상)
- 제3전시장(2028, 178,000㎡) 확장 + 국내 전시산업 표준 플랫폼화
- 개발 부담 최소화: 멀티테넌시=격리 레이어만 추가, 기능 로직 불변(리스크 R-8)
- **도표 지시**: 테넌트 확장 로드맵(KINTEX→COEX→제3전시장→타 전시관). 도식ID 없음(로드맵).
- **REQ**: (비전 — 상세 REQ는 S56·S57)
- **출처**: strategy §1 WT-5·§4 R-8, PLANNING §1A
## S56 · 테넌트 격리 아키텍처
- **섹션**: P7 멀티테넌시·확장
- **헤드라인**: "한 층이라도 불일치하면 차단 — 컨텍스트→서비스→MyBatis→PostGIS 4층 방어."
- **본문**
- 격리 축: `tenant_id`(전시관) ⊃ `event_id`(행사) ⊃ 행사 RBAC 3중
- fail-closed 4층: ①컨텍스트 필터(서브도메인+소속, 불일치 거부)→②서비스 가드→③MyBatis 인터셉터(tenant_id 자동 주입)→④PostGIS(tenant_id 선두 복합 인덱스)(REQ-S-018)
- tenant_id 선두 복합 인덱스로 격리 쿼리 성능(REQ-N-020)
- [구현 계획: 스키마·컨텍스트 구현 대기, 백필 전 단일테넌트 무중단→검증 후 필터 활성화]
- **도표 지시**: **도식⑪ 멀티테넌시 격리 아키텍처도(수직 4층 방어 스택 + 좌측 테넌트 컨텍스트 진입)**. tech_advisory §1-4 ⑪.
- **REQ**: REQ-F-077·078, REQ-S-018, REQ-N-020
- **출처**: tech_advisory §1-3·§1-4 ⑪
## S57 · 온보딩·2계층 관리자
- **섹션**: P7 멀티테넌시·확장
- **헤드라인**: "코드 배포 없이 데이터 온보딩 6단계로 새 전시관을 연다."
- **본문**
- 온보딩: 코드 배포 없이 데이터 온보딩(홀·요율·규정 룰셋·마스터데이터 입력 수준)(REQ-F-077)
- 2계층 관리자: 플랫폼 슈퍼관리자(테넌트 스위처) / 테넌트 관리자(단일 스코프)(REQ-F-079)
- 테넌트 컨텍스트 해소(REQ-F-078)
- 공통코드·메뉴 관리(§5B, REQ-F-073) 기반 재사용
- **도표 지시**: 온보딩 6단계 도식 + 테넌트 스위처 화면. 도식ID 없음(단계 도식).
- **REQ**: REQ-F-077·078·079, REQ-F-073
- **출처**: tech_advisory §1-3, strategy §1 WT-5
---
# P8. 사업관리·일정·조직 (S58~S60, 배점 2)
## S58 · 추진 일정·Phase
- **섹션**: P8 사업관리·일정·조직
- **헤드라인**: "Phase 1 코어 → Phase 2 워크플로/운영 → Phase 3 현장/확장, 게이트로 진행."
- **본문**
- Phase 1(~4M): 아키텍처(A)·공통레이어(B, 2FA/시스템관리)·P0 코어(C: M2·M3·M4·M5)
- Phase 2(~4M): 옥션 M15·관람 M10·BI M16·관리자 M18·판매/서류/정산 M1/6/7/9·CMS M12/17
- Phase 3(~4M+): 멀티테넌시·M11·M13·M14·배포 안정화·SM 이관
- 공수: 총 ~150 M/M 규모(핵심 146 + PMO/QA 4~8), 라이브 스캐폴드로 Phase A/B 공수 실측 절감
- [주의: 예산·사업기간은 실제 RFP 부재로 가정, 공고 확인 시 재정렬]
- **도표 지시**: **도식⑫ 일정 간트(Phase × 모듈 + 마일스톤 다이아몬드 G1·G2·Phase 게이트·QA 게이트·오픈)**. tech_advisory §1-4 ⑫.
- **REQ**: (일정 — 실행력 근거)
- **출처**: tech_advisory §1-4 ⑫·§5, PLANNING §9
## S59 · 선행 게이트·리스크 관리
- **섹션**: P8 사업관리·일정·조직
- **헤드라인**: "약한 고리를 숨기지 않고 드러내 방어한다 — 선제 대응이 곧 신뢰다."
- **본문**
- 선행 게이트: G1 나노바나나 승인 [검증됨: 완료]·G2 배포서버 [검증됨: 완료]
- 리스크 R-1~R-8 정면 방어: 외부 API(승인 게이트+degraded 폴백)·CAD 실측(가정 라벨+협의 선행)·kxwp(단계적 접근)·배점 가정(투명 고지)·이미지 품질(후처리 합성)·AI 오판(사람 확정)·HW 연동(협의 전제)·확장 부담(격리 레이어만)
- 확인 필요 8항목(C1~C8) 관리
- **도표 지시**: **리스크 매트릭스(발생×영향 2×2에 R-1~R-8·C1~C8 배치)** + 게이트. tech_advisory §1-4 말미(추가 표준 도식).
- **REQ**: (리스크 관리 — 방어 논리)
- **출처**: strategy §4, rfp_analysis §6, tech_advisory §0
## S60 · 품질·형상관리
- **섹션**: P8 사업관리·일정·조직
- **헤드라인**: "배포는 헬스체크로 지키고 실패하면 롤백, 의존성은 SCA 게이트로 막는다."
- **본문**
- Fail-Safe 배포: 백업→배포→헬스체크 200→실패 시 롤백(이전 jar 유지) [검증됨: deploy_kintex.sh](REQ-N-004)
- QA 게이트: 점진 경계면 검증·보안 불변·빌드 회귀(Phase 게이트 통과 후 진행)
- SCA CI 게이트: OWASP Dependency-Check 정기 스캔·패치(REQ-S-019)
- 형상관리: Gitea `zio/kintex`·Conventional Commits·main 보호·환경 분리(dev/staging/prod)
- **도표 지시**: 품질 게이트 흐름(빌드→테스트→SCA→헬스체크→롤백). 도식ID 없음.
- **REQ**: REQ-N-004, REQ-S-019
- **출처**: tech_advisory §6-5·§0-1(라이브), §4 REQ-S-019
---
# P9. 기대효과 (S61~S63, 가점)
## S61 · 정량 기대효과
- **섹션**: P9 기대효과
- **헤드라인**: "견적 수일→즉시, 배치 수주→수 분, 검수 육안→자동, 결과는 사진으로."
- **본문**
- 견적: 수일 대기 → 즉시(규칙엔진)
- 배치: 수주 CAD 외주 → 수 분 3안 자동생성
- 규정 검수: 육안 → 제출 전 자동 플래깅
- 위치표시도: 수기 → 좌표 클릭 자동 작도
- 시공 결과: 개장일 첫 대면 → 신청 시점 사진 예측
- **도표 지시**: **기대효과 정량 인포그래픽(5지표 Before/After)**. tech_advisory §1-4 말미(추가 표준 도식).
- **REQ**: REQ-F-002·007·009·017, REQ-A-001
- **출처**: PLANNING §1-3, strategy §5
## S62 · 정성·전략 효과
- **섹션**: P9 기대효과
- **헤드라인**: "참가업체는 영업력을, 홀매니저는 시간을, 킨텍스는 데이터 주권을 얻는다."
- **본문**
- 참가업체: 사진급 자료로 영업력·수주력 강화
- 홀매니저: 규정 검수 병목 해소로 처리량 증대
- 킨텍스: 온프레미스 우선·외부 AI 격리로 데이터 주권 확보
- 산업: 국내 전시산업 표준 플랫폼화
- **도표 지시**: 이해관계자별 효과 카드. 도식ID 없음.
- **REQ**: (정성 효과 — 전략)
- **출처**: strategy §3
## S63 · 확장 비전·마무리
- **섹션**: P9 기대효과
- **헤드라인**: "킨텍스에서 검증하고, 코엑스로 확장하며, 다시 사진으로 컨펌한다."
- **본문**
- 확장: COEX·제3전시장·타 전시관 SaaS 확장
- 재사용: 검증된 공통 프레임워크(WISE/UIWS) 위에 도메인 코어 이식
- 클로징: "신청서를 내는 순간, 시공 후 사진을 먼저 본다."
- **도표 지시**: 확장 비전 로드맵 + 클로징 슬로건 대형 타이포. 도식ID 없음.
- **REQ**: REQ-F-077(확장 근거)
- **출처**: strategy §1 WT-5
---
# 부록 (선택 삽입)
## 부록 A · 확인 필요·협의 사항
- C1 평가배점·제출규격(가정) · C2 kxwp API(REQ-F-022) · C3 CAD·트렌치 실측(REQ-F-016 정확도) · C4 인터넷 요율 공식본(REQ-F-002) · C5 나노바나나 G1(완료) · C6 코엑스 실측(REQ-F-077) · C7 사이니지·비콘·IoT HW(REQ-F-070·052~055) · C8 기타 협의
- 정직성 어필: 모든 [가정]·[협의]를 1장 집약 → 발주처 협업 자세
## 부록 B · REQ 커버리지 매트릭스
- 아래 "RTM 반영표" 참조(142 REQ ↔ 슬라이드 1:1)
## 부록 C · 100대 기능 성숙도
- 기구현 63 · 부분 18 · 신규 19(정량, rfp_analysis §4)
---
# RTM 반영표 (REQ → 슬라이드 · 142 REQ 전량)
> 규칙: 각 REQ의 **주 반영 슬라이드**(굵게) + 보조 슬라이드. QA는 이 표로 142 REQ 반영상태 ☑ 최종 검증. 미반영 0.
## REQ-F (기능 81)
| REQ | 요약 | 반영 슬라이드 |
|---|---|---|
| REQ-F-001 | 홀 가용성 캘린더 | **S33** |
| REQ-F-002 | 규칙엔진 견적 | **S33** · S10·S61 |
| REQ-F-003 | 웹폼→HWP | **S33** |
| REQ-F-004 | 부스판매 인벤토리 | **S33** |
| REQ-F-005 | 온라인 셀프선택 | **S33** |
| REQ-F-006 | 임대계약 전자서명 | **S33** |
| REQ-F-007 | 배치 3안 자동생성 | **S24** · S10·S61·S13 |
| REQ-F-008 | 배치안 선택/병합 | **S24** |
| REQ-F-009 | 배치 규정 검증·재검증 | **S25** · S10·S61 |
| REQ-F-010 | 최종안 버전·좌표·초대링크 | **S24** |
| REQ-F-011 | 조립부스 3D 프리뷰 | **S26** |
| REQ-F-012 | 독립부스 3안·버전 | **S26** |
| REQ-F-013 | 설계 규정 사전검증 | **S26** |
| REQ-F-014 | 도면 비전 추출·검증 | **S26** |
| REQ-F-015 | 전기용량·분전반 산출 | **S27** |
| REQ-F-016 | 배선경로 PostGIS 최단 | **S27** · S42 |
| REQ-F-017 | 위치표시도 자동생성 | **S28** · S10·S61 |
| REQ-F-018 | 네트워크·급배수·압축공기 신청 | **S28** |
| REQ-F-019 | 조명 조도 배치안 | **S18** |
| REQ-F-020 | 마일스톤 역산 알림 | **S34** |
| REQ-F-021 | 신고서류 웹폼→HWP/PDF | **S34** |
| REQ-F-022 | kxwp 릴레이 ⚠ | **S34** · S59(리스크 R-3) |
| REQ-F-023 | 서류 버전·전자결재 | **S34** |
| REQ-F-024 | OCR 계약·납품서 | **S34** |
| REQ-F-025 | 역경매 옥션 개설 | **S29** · S12·S13 |
| REQ-F-026 | AI 자료 기반 응찰·PDF | **S29** |
| REQ-F-027 | 실시간 순위·자동마감 | **S30** |
| REQ-F-028 | 종합평가 낙찰 스코어 | **S31** |
| REQ-F-029 | 등록업체 게이트 | **S32** |
| REQ-F-030 | 등록업체 AI 매칭 | **S31** |
| REQ-F-031 | 물량서 BOQ 산출 | **S31** · S29 |
| REQ-F-032 | 낙찰→발주 자동전환 | **S32** · S12 |
| REQ-F-033 | 반입/반출 슬롯 | **S40** |
| REQ-F-034 | 통행증 QR | **S40** |
| REQ-F-035 | 지게차·렌탈 | **S40** |
| REQ-F-036 | 철거 대기열 시뮬 | **S40** |
| REQ-F-037 | 사전등록 | **S35** |
| REQ-F-038 | 모바일 배지/QR | **S35** |
| REQ-F-039 | 현장 체크인·집계 | **S35** |
| REQ-F-040 | 오프라인 체크인 폴백 | **S35** · S51 |
| REQ-F-041 | 리드캡처 | **S35** |
| REQ-F-042 | AI 리드 스코어링 | **S35** |
| REQ-F-043 | 세그먼트 EDM | **S35** |
| REQ-F-044 | 티켓발권·스마트배지 | **S35** |
| REQ-F-045 | AI 비즈매칭 | **S36** |
| REQ-F-046 | 미팅 슬롯·리포트 | **S36** |
| REQ-F-047 | 이벤트 모바일앱 | **S36** |
| REQ-F-048 | 공개 홍보 사이트 | **S36** |
| REQ-F-049 | 인터랙티브 플로어플랜 | **S36** |
| REQ-F-050 | 세그먼트 EDM 캠페인 | **S36** |
| REQ-F-051 | 스폰서십 패키지 | **S36** |
| REQ-F-052 | wayfinding | **S40** |
| REQ-F-053 | 혼잡 모니터·예측 | **S40** |
| REQ-F-054 | 전력부하·주차 | **S40** |
| REQ-F-055 | 안전 이상탐지 | **S40** |
| REQ-F-056 | 납부 스케줄 | **S37** |
| REQ-F-057 | PG 결제 | **S37** |
| REQ-F-058 | 예치금 정산 | **S37** |
| REQ-F-059 | 세금계산서·수수료 | **S37** |
| REQ-F-060 | 환불·취소 규정 | **S37** |
| REQ-F-061 | 가동률 대시보드 | **S38** |
| REQ-F-062 | 매출·P&L | **S38** |
| REQ-F-063 | ROI·RevPAD·㎡당 수익 | **S38** |
| REQ-F-064 | 리텐션·LTV | **S39** |
| REQ-F-065 | 참가업체 ROI 격리 | **S39** |
| REQ-F-066 | 경영진 KPI 드릴다운 | **S39** |
| REQ-F-067 | CMS 게시 워크플로 | **S36** |
| REQ-F-068 | 마이크로사이트 | **S36** |
| REQ-F-069 | 다국어 콘텐츠·hreflang | **S36** · S53 |
| REQ-F-070 | 사이니지 배포 ⚠ | **S40** · S59 |
| REQ-F-071 | RBAC·감사 백오피스 | **S48** |
| REQ-F-072 | 룰셋·마스터데이터 버전 | **S45** |
| REQ-F-073 | 공통코드·메뉴 | **S16** · S57 |
| REQ-F-074 | 공통 업무모듈 | **S16** |
| REQ-F-075 | 회의록 STT·업무보고 PDF | **S16** |
| REQ-F-076 | 통합 알림센터 WebSocket | **S16** |
| REQ-F-077 | 멀티테넌트 격리·온보딩 | **S56·S57** · S55·S63 |
| REQ-F-078 | 테넌트 컨텍스트 해소 | **S56** · S57 |
| REQ-F-079 | 2계층 관리자 | **S57** |
| REQ-F-080 | API·웹훅·CRM/ERP 연동 | **S45** |
| REQ-F-081 | 역할별 프론트 포털 | **S43** · S41·S09 |
## REQ-A (AI 20)
| REQ | 요약 | 반영 슬라이드 |
|---|---|---|
| REQ-A-001 | 샷세트 S1~S7 | **S17·S18** · S01·S12·S61 |
| REQ-A-002 | image-to-image 구조보존 | **S19** · S12 |
| REQ-A-003 | 구조화 프롬프트 사전 | **S19** |
| REQ-A-004 | S6 배선 오버레이 래스터 | **S21** |
| REQ-A-005 | Before/After 슬라이더 | **S21** |
| REQ-A-006 | RenderJob 방어 | **S22** |
| REQ-A-007 | 비동기 큐·WebSocket | **S20** · S44 |
| REQ-A-008 | 간판 텍스트 검수·후처리 | **S22** |
| REQ-A-009 | 배치 3안(제약 솔버+LLM) | **S24** |
| REQ-A-010 | AI 규정 검증 | **S25** · S26 |
| REQ-A-011 | AI 서류 검수 | **S34** |
| REQ-A-012 | 규정 챗봇(근거·인용) | **S34** |
| REQ-A-013 | AI 수요예측·동적가격 | **S39** |
| REQ-A-014 | KPI 브리핑·Text-to-SQL | **S39** |
| REQ-A-015 | AI 카피·번역 | **S36** |
| REQ-A-016 | AI 라우터(Claude→Ollama) | **S44** · S41 |
| REQ-A-017 | RAG·인용·환각 차단 | **S49** · S23 |
| REQ-A-018 | AI 리드 스코어·매칭 | **S35·S36** |
| REQ-A-019 | 현장 이상탐지·예측 | **S40** |
| REQ-A-020 | 도면 비전·OCR 등록 | **S26·S34** |
## REQ-N (비기능 21)
| REQ | 요약 | 반영 슬라이드 |
|---|---|---|
| REQ-N-001 | SLO 99.5% | **S51** |
| REQ-N-002 | 2인스턴스 롤링 무중단 | **S51** · S43 |
| REQ-N-003 | PG 스탠바이·PITR | **S51** |
| REQ-N-004 | Fail-Safe 배포 | **S60** |
| REQ-N-005 | degraded 모드 | **S51** · S20·S44 |
| REQ-N-006 | 나노바나나 40초·1024px | **S52** · S20 |
| REQ-N-007 | 대량 생성 제어 | **S52** · S22 |
| REQ-N-008 | 공간쿼리 GiST 실시간 | **S42** · S12 |
| REQ-N-009 | 워커 독립 스케일 | **S43** |
| REQ-N-010 | BI/공개 읽기복제 | **S43** · S38 |
| REQ-N-011 | 워커 재큐(멱등) | **S43** · S51 |
| REQ-N-012 | WCAG 2.1 AA·KWCAG 2.2 | **S53** |
| REQ-N-013 | 캔버스 비시각 대안 | **S53** |
| REQ-N-014 | 다국어 i18n·hreflang | **S53** |
| REQ-N-015 | 라이트/다크 테마 | **S53** |
| REQ-N-016 | 반응형 | **S53** |
| REQ-N-017 | 단일 공간 원천 | **S42** |
| REQ-N-018 | 관측성·미기록 | **S54** · S50 |
| REQ-N-019 | 공개 CDN·요청률 상한 | **S43** · S54 |
| REQ-N-020 | tenant_id 선두 복합 인덱스 | **S56** |
| REQ-N-021 | BI 데이터마트 배치 ETL | **S54** · S38 |
## REQ-S (보안 20)
| REQ | 요약 | 반영 슬라이드 |
|---|---|---|
| REQ-S-001 | 외부 API 정책 | **S49** · S47 |
| REQ-S-002 | GEMINI 키 워커 env only | **S49** |
| REQ-S-003 | 자격증명·키 완전 제외 | **S50** |
| REQ-S-004 | admin 비번 env 재시드 | **S48** |
| REQ-S-005 | JWT+2FA·실패 잠금 | **S48** |
| REQ-S-006 | 행사 RBAC 3중 | **S48** |
| REQ-S-007 | 미등록 업체 차단(403) | **S32** · S48 |
| REQ-S-008 | admin/internal 게이트 | **S48** |
| REQ-S-009 | 이미지 워터마크 항상 | **S49** · S23·S01 |
| REQ-S-010 | 스택트레이스 미노출 | **S50** |
| REQ-S-011 | DataAccessException 요약 | **S50** |
| REQ-S-012 | 감사로그 전수 | **S50** |
| REQ-S-013 | PII 동의·마스킹·암호화 | **S50** · S39 |
| REQ-S-014 | 입력 검증 | **S50** |
| REQ-S-015 | XSS·CSP | **S50** |
| REQ-S-016 | 인젝션 차단 | **S50** |
| REQ-S-017 | IDOR·CSRF·JWT 회전 | **S50** |
| REQ-S-018 | fail-closed 테넌트 격리 | **S56** · S47 |
| REQ-S-019 | 의존성 SCA CI 게이트 | **S60** · S50 |
| REQ-S-020 | 보안영역 분리 | **S47** |
---
## 커버리지 요약
| 유형 | REQ 수 | 반영 | 미반영 |
|---|---|---|---|
| REQ-F (기능) | 81 | 81 | 0 |
| REQ-A (AI) | 20 | 20 | 0 |
| REQ-N (비기능) | 21 | 21 | 0 |
| REQ-S (보안) | 20 | 20 | 0 |
| **총계** | **142** | **142** | **0** |
**슬라이드 총계**: 63(S01~S63) + 부록 3(A·B·C)
**RTM 커버리지**: 142/142 반영, **미반영 0**
**과장 방지 준수**: 라이브 확인분(인프라·CI/CD·나노바나나·인증·워터마크·PostGIS 코어·평면도 자산)만 [검증됨], 도메인 모듈·멀티테넌시·집계 API·i18n/접근성 전면 적용은 [구현 계획]으로 구분 표기. 시크릿(키·비번·IP·SSH) 전면 미기재. AI 생성 이미지는 전 슬라이드에서 워터마크·금지 고지 정책 명시.

View File

@ -1,210 +0,0 @@
# 킨텍스 자동전시시스템 구축 — 제안서 목차 (Proposal Outline · PPT)
> 작성: proposal-strategist · 작성일: 2026-07-11 · 버전 v1.0
> 짝 문서: `proposal_strategy.md`(Win Theme·평가대응·리스크) · `rtm.md`(REQ↔섹션 매핑) · `rfp_analysis.md`(배점·규격)
> 형식: **PowerPoint(.pptx) 기술제안서**, 분량 자율(권장 40~80p) → **목표 ~63슬라이드**. 실제 공고 확인 시 재정렬(rfp_analysis C1).
> 목차 코드: P1 사업이해 · P2 추진전략 · P3 기술제안 · P4 아키텍처 · P5 보안·개인정보 · P6 비기능 · P7 멀티테넌시·확장 · P8 사업관리·일정·조직 · P9 기대효과 (RTM 정합).
> 배분 원칙: **배점 가중** — P3(35점) ≈ 40%, P4·P5(각 10점) 각 ~8%, P6(8점) ~6%, P1+P2(10점) ~16%, P7(5점) ~5%, P8(2점) ~5%, P9 ~5%.
---
## 슬라이드 배분 총괄 (배점 정렬)
| 구간 | 목차 | 평가배점(가정) | 슬라이드 수 | 비중 | 핵심 Win Theme |
|---|---|---|---|---|---|
| 표지·목차·오버헤드 | — | — | 6 | 10% | 슬로건 각인 |
| P1 | 사업이해 | 10(P1+P2 공유) | 4 | 6% | 도메인 이해 |
| P2 | 추진전략 | 10(P1+P2 공유) | 5 | 8% | 3중 해자 |
| **P3** | **기술제안** | **20+15=35** | **25** | **40%** | **WT-1·2·3 총력** |
| P4 | 아키텍처·기술스택 | 10 | 5 | 8% | 확정 스택·공간원천 |
| P5 | 보안·개인정보 | 10 | 5 | 8% | WT-4 |
| P6 | 비기능 | 8 | 4 | 6% | SLA 수치 |
| P7 | 멀티테넌시·확장 | 5 | 3 | 5% | WT-5 |
| P8 | 사업관리·일정·조직 | 2 | 3 | 5% | 실행력·게이트 |
| P9 | 기대효과 | 가점 | 3 | 5% | 정량 ROI |
| **합계** | | **80(+가격20)** | **63** | 100% | |
---
## 0. 표지·목차 (6슬라이드)
| # | 슬라이드 | 핵심 메시지 | 그래픽/도표 기획 |
|---|---|---|---|
| S01 | 표지 | 사업명 + 슬로건 "신청서를 내는 순간, 시공 후 사진을 먼저 본다" | 나노바나나 시공 예측 히어로 이미지(워터마크) 배경 |
| S02 | 제안 개요 | 우리는 누구·왜 우리인가 3줄 | 제안사 로고·핵심 자산 아이콘(선 SVG) |
| S03 | 목차 | P1~P9 네비게이션 | 목차 인덱스(9섹션 배점 배지 병기) |
| S04 | 제안 핵심 요약(1p) | Win Theme 5선 + 3중 해자 한눈에 | **3중 해자 벤다이어그램**(시공예측∩PostGIS∩옥션폐루프) |
| S05 | 배점 대응 맵 | 우리 제안이 배점 어디를 어떻게 겨냥하는가 | **배점-섹션 정렬 매트릭스**(80점 히트맵) |
| S06 | [가정] 고지 간지 | 배점·규격 가정 투명 고지·공고 시 재정렬 | 간단 노트 카드(리스크 R-4 방어) |
---
## P1. 사업이해 (4슬라이드 · 배점 사업이해·추진전략 10)
| # | 슬라이드 | 핵심 메시지 | 그래픽/도표 | REQ/근거 |
|---|---|---|---|---|
| S07 | 사업 배경·목적 | 킨텍스 108,011㎡(2028년 178,000㎡), 전시 준비 여전히 HWP·CAD 수작업 | 킨텍스 규모 인포그래픽 | PLANNING §1-1 |
| S08 | As-Is 프로세스 병목 | D-150/30/25/7 타임라인의 Pain Point(견적 대기·CAD 외주·kxwp 육안검수·유틸리티 파편화) | **As-Is 타임라인 다이어그램**(D-데이별 병목 표시) | PLANNING §3-1·3-2 |
| S09 | 이해관계자·페르소나 | 6역할+2관리자 계층의 과업·고통·기대 | **페르소나 카드 6종**(주최자·참가·업체·홀매니저·관람객·대중) | PLANNING §2 |
| S10 | To-Be 목표(정량) | 견적 즉시·배치 수분·규정 자동검증·위치표시도 자동·시공 예측 | **As-Is→To-Be 대비표**(정량 개선 5행) | PLANNING §1-3 |
---
## P2. 추진전략 (5슬라이드 · 배점 P1과 공유)
| # | 슬라이드 | 핵심 메시지 | 그래픽/도표 | REQ/근거 |
|---|---|---|---|---|
| S11 | 비전·포지셔닝 | "도면이 아니라 사진으로 컨펌한다" 베뉴 운영 플랫폼 | 비전 슬로건 대형 타이포 | PLANNING §1-2 |
| S12 | **3중 해자 전략** | 시공예측∩PostGIS∩옥션폐루프 결합의 재현 난이도 | **3원 벤다이어그램(재강조·상세)** | strategy §3-3 |
| S13 | Win Theme 5선 | WT-1~5 슬로건·배점 정렬 | **Win Theme 5카드**(배점 배지 병기) | strategy §1 |
| S14 | 전시 생애주기 폐루프 | 설계→시각화→발주→시공→운영→분석 한 흐름 | **생애주기 폐루프 순환도**(M모듈 매핑) | PLANNING §1-2-1 |
| S15 | 추진체계·수행조직 | 아키텍트·코어·도메인·AI·QA·DevOps 팀 + 게이트 | **추진체계도(조직도)** | 하네스 팀 구성 |
---
## P3. 기술제안 (25슬라이드 · 배점 AI시각화·설계 20 + 기능구현 15 = 35 · **최다 집중**)
### P3-A. 모듈 조망 (1)
| # | 슬라이드 | 핵심 메시지 | 그래픽/도표 | REQ |
|---|---|---|---|---|
| S16 | 전체 모듈맵 | M1~M18+§5B+§1A 한 장 조망, P0 코어 강조 | **모듈맵(생애주기×우선순위 매트릭스)** | rfp_analysis §2-1 |
### P3-B. 나노바나나 시공 예측 — WT-1 최우선 (7)
| # | 슬라이드 | 핵심 메시지 | 그래픽/도표 | REQ |
|---|---|---|---|---|
| S17 | 나노바나나 개요 | "시공 전에 사진으로 의사결정" 경쟁 미보유 차별화 | 히어로 Before/After 실물 | REQ-A-001 |
| S18 | 표준 샷세트 S1~S7 | 정면·야간·통로·내부·Before/After·배선·홀전경 | **샷세트 7컷 그리드**(워터마크) | REQ-A-001, F-019 |
| S19 | image-to-image 구조보존 | 골격·치수·통로·앵글 잠금, 스타일만 사실화 | **보존/교체 파라미터 도식** | REQ-A-002·003 |
| S20 | AI 파이프라인 아키텍처 | Spring 큐→Redis→Python 워커→오브젝트스토리지→WebSocket | **나노바나나 파이프라인 플로우** | REQ-A-007, N-006 |
| S21 | Before/After·배선 오버레이 | 드래그 슬라이더(ARIA), S6 백엔드 래스터 합성(전기적·네트청·급배수녹) | Before/After 슬라이더 + 배선 색상 범례 | REQ-A-004·005 |
| S22 | RenderJob 방어·품질 | 다층 크기가드·SAFETY 분기·간판 한글 후처리·성공 시만 쿼터 | **RenderJob 상태 시퀀스**(방어 계층) | REQ-A-006·008 |
| S23 | AI 신뢰 원칙 | 결정적 알고리즘+LLM 보조+사람 확정, 워터마크 불변 | AI 3층 신뢰 구조 도식 | REQ-S-009, A-017 |
### P3-C. 배치·설계·배선 AI 자동화 — WT-2 (5)
| # | 슬라이드 | 핵심 메시지 | 그래픽/도표 | REQ |
|---|---|---|---|---|
| S24 | 플로어플랜 스튜디오(M2) | 부스 배치 3안 자동생성·선택/병합·버전 | **3안 비교 화면 목업** | REQ-F-007·008·010, A-009 |
| S25 | 배치 규정 자동검증 | 피난통로·바닥하중 홀별·비상구·복층·소방 병합 후 재검증 | **규정 위반 플래그 대시보드** | REQ-F-009, A-010 |
| S26 | 부스 설계 스튜디오(M3) | 조립 3D 프리뷰·독립부스 3안·규정 사전검증(높이·리깅·방염·이격)·도면 비전추출 | 설계 3안+규정 체크리스트 | REQ-F-011~014, A-020 |
| S27 | 유틸리티 설계(M4) | 기기→kW→분전반 자동산출, 트렌치→분전반 PostGIS 최단 배선 | **PostGIS 배선 경로 오버레이** | REQ-F-015·016 |
| S28 | 위치표시도·유틸리티 신청 | 좌표 클릭 자동 작도(수기 폐지)·자동 견적·D-25 역산 | 위치표시도 자동생성 예시 | REQ-F-017·018 |
### P3-D. 공사 옥션 폐루프 — WT-3 (4)
| # | 슬라이드 | 핵심 메시지 | 그래픽/도표 | REQ |
|---|---|---|---|---|
| S29 | 옥션 개요·폐루프 | AI 설계자료→응찰 근거→발주까지 한 흐름 | **옥션 폐루프 흐름도**(M2~M5→M15→M6·M9) | REQ-F-025·026 |
| S30 | 역경매·실시간 순위 | 라운드·마감·최저가·내 위치(익명 옵션)·자동마감 | **실시간 순위 보드 목업** | REQ-F-027 |
| S31 | 종합평가 낙찰 | 가격+평판+납기 가중 스코어·항목별 비교표 | **종합평가 스코어 비교표** | REQ-F-028·030·031 |
| S32 | 등록 게이트·발주 전환 | 미등록 업체 응찰 원천 차단(403)·낙찰→계약/발주 자동전환 | **등록 검증 시퀀스** + 발주 전환 | REQ-F-029·032, S-007 |
### P3-E. 판매·서류·워크플로 (2)
| # | 슬라이드 | 핵심 메시지 | 그래픽/도표 | REQ |
|---|---|---|---|---|
| S33 | 판매·홀배정·견적(M1) | 가용성 캘린더·규칙엔진 견적(2,250원/㎡)·웹폼→HWP·셀프 부스선택 | 견적 규칙엔진 계산 흐름 | REQ-F-001~005 |
| S34 | 서류·마일스톤(M6) | D-150/30/25/7 역산·웹폼→HWP/PDF 7종·AI 서류검수·kxwp 릴레이(⚠협의)·전자결재·OCR | 마일스톤 역산 타임라인 | REQ-F-020~024, A-011 |
### P3-F. 관람·참가 (2)
| # | 슬라이드 | 핵심 메시지 | 그래픽/도표 | REQ |
|---|---|---|---|---|
| S35 | 관람객 등록·배지·리드(M10) | 사전등록·모바일 배지/QR·현장 체크인·오프라인 폴백·리드캡처·세그먼트 EDM | 등록→체크인→리드 여정도 | REQ-F-037~043 |
| S36 | 비즈매칭·마케팅·공개사이트(M11·M12·M17) | AI 비즈매칭·SEO 다국어 공개사이트·인터랙티브 플로어플랜·마이크로사이트·CMS 게시 워크플로 | 공개사이트+마이크로사이트 목업 | REQ-F-045·048~051·067~069 |
### P3-G. 정산·경영분석·현장 (4)
| # | 슬라이드 | 핵심 메시지 | 그래픽/도표 | REQ |
|---|---|---|---|---|
| S37 | 정산·결제(M9) | 납부 스케줄·PG 결제·예치금 실사용 투명 정산·세금계산서·옥션 수수료 | 정산 흐름도 | REQ-F-056~059 |
| S38 | 경영분석 BI(M16)-1 | 가동률·매출구성·행사별 P&L·RevPAD·㎡당 수익(운영사 관점) | **BI 대시보드 목업(Recharts)** | REQ-F-061~063 |
| S39 | 경영분석 BI(M16)-2 | 리텐션·LTV 코호트·참가사 ROI(권한 격리)·경영진 KPI·AI 수요예측·KPI 브리핑·Text-to-SQL | KPI 드릴다운 + AI 인사이트 카드 | REQ-F-064~066, A-013·014 |
| S40 | 현장운영·물류(M8·M13·M14) | 반입/반출 슬롯·통행증 QR·wayfinding·혼잡/전력/안전 이상탐지(선택·협의 전제) | 현장 운영 상황판 | REQ-F-033~036·052~055, A-019 |
---
## P4. 아키텍처·기술스택 (5슬라이드 · 배점 10)
| # | 슬라이드 | 핵심 메시지 | 그래픽/도표 | REQ |
|---|---|---|---|---|
| S41 | 전체 아키텍처 | 확정 스택 React18/19+Spring Boot 3.x+MyBatis+PostgreSQL/PostGIS+Redis+나노바나나 Python 워커 | **시스템 아키텍처 다이어그램**(전 계층) | rfp_analysis §1, REQ-A-016 |
| S42 | 단일 공간 데이터 원천 | M2폴리곤·M4배선 매퍼 권위, 소비 모듈 조회만·중복 금지, PostGIS GiST 실시간 | **공간 데이터 원천 모델**(폴리곤·포인트·LineString) | REQ-N-008·017 |
| S43 | 확장성·비동기 | 워커 독립스케일(큐 깊이)·읽기복제 BI 오프로드·공개영역 CDN·역할별 프론트 분리 | **배포 토폴로지도** | REQ-N-009·010·019, F-081 |
| S44 | AI 플랫폼 라우팅 | Claude 기본→Ollama 폴백·중앙경유·LLM 직접호출 금지, 나노바나나 큐/콜백 | **AI 라우터 폴백 흐름** | REQ-A-007·016 |
| S45 | 연동·마스터데이터 | API·웹훅·CRM/ERP 게이트웨이·룰셋/마스터데이터 버전관리·무중단 개정 | 연동 인터페이스 맵 | REQ-F-072·080 |
---
## P5. 보안·개인정보 (5슬라이드 · 배점 10 · WT-4)
| # | 슬라이드 | 핵심 메시지 | 그래픽/도표 | REQ |
|---|---|---|---|---|
| S46 | 보안 개요·불변 원칙 | "AI가 만든 것은 표시, 남의 데이터는 차단" REQ-S 20 커버리지 | **REQ-S 20항목 커버리지 표** | REQ-S 전량 |
| S47 | 보안영역 분리 | 공개 DMZ 쓰기 불가·데이터존 직결 불가·DMZ↔내부 HTTPS만 | **보안영역 분리도(망 구성)** | REQ-S-020, N-018 |
| S48 | 인증·접근통제 | JWT+2FA OTP(TOTP)·실패 잠금·admin 비번 env 재시드·RBAC 3중·admin/internal 게이트 | 인증 흐름 + RBAC 3중 구조 | REQ-S-004~008 |
| S49 | AI·이미지 보안 | 나노바나나 워터마크 항상·계약/심사 금지 고지·외부 API 워커 env only·RAG 환각차단 | **워터마크 실물** + 외부 API 격리 | REQ-S-001·002·009, A-017 |
| S50 | 개인정보·감사·안전코딩 | PII 동의·마스킹·AES-256-GCM·응답 제외·감사로그 전수·XSS/인젝션/IDOR·SCA 게이트·스택트레이스 미노출 | PII 마스킹 예시 + 감사로그 표 | REQ-S-010~017·019 |
---
## P6. 비기능 (4슬라이드 · 배점 8)
| # | 슬라이드 | 핵심 메시지 | 그래픽/도표 | REQ |
|---|---|---|---|---|
| S51 | 성능·가용성 SLA | SLO 99.5%(성수기 99.9%)·백엔드 2인스턴스 롤링·PG 스탠바이+PITR·degraded 모드 | **SLA 수치표** + HA 구성도 | REQ-N-001~005·011 |
| S52 | 나노바나나 성능 | 평균 40초·1024px+JPEG 0.85 전처리·자동생성 S1·S7 한정·캐시·쿼터 | 렌더 성능 지표 카드 | REQ-N-006·007 |
| S53 | 접근성·다국어·테마·반응형 | WCAG 2.1 AA+KWCAG 2.2·비시각 대안·i18n 한/영/중/일·hreflang·라이트/다크·데스크톱/모바일 | 접근성·다국어 매트릭스 | REQ-N-012~016 |
| S54 | 관측성·데이터 | 로그·메트릭·트레이스(PII/스택트레이스 미기록)·BI 데이터마트 배치 ETL/읽기복제 | 관측성 파이프라인 | REQ-N-018·021 |
---
## P7. 멀티테넌시·확장 (3슬라이드 · 배점 5 · WT-5)
| # | 슬라이드 | 핵심 메시지 | 그래픽/도표 | REQ |
|---|---|---|---|---|
| S55 | 멀티테넌트 SaaS 비전 | KINTEX #1 검증→COEX 확장, 제3전시장 대비 표준 플랫폼 | 테넌트 확장 로드맵 | §1A, WT-5 |
| S56 | 테넌트 격리 아키텍처 | tenant_id 전파·fail-closed 다층 격리(컨텍스트→서비스→MyBatis→PostGIS)·선두 복합 인덱스 | **멀티테넌시 격리 아키텍처도** | REQ-F-077·078, S-018, N-020 |
| S57 | 온보딩·2계층 관리자 | 코드 배포 없이 데이터 온보딩 6단계·플랫폼/테넌트 관리자·테넌트 스위처 | **온보딩 6단계 도식** | REQ-F-077·079 |
---
## P8. 사업관리·일정·조직 (3슬라이드 · 배점 2)
| # | 슬라이드 | 핵심 메시지 | 그래픽/도표 | REQ |
|---|---|---|---|---|
| S58 | 추진 일정·Phase | Phase 1 코어(~4M)·2 워크플로/운영(~4M)·3 현장/확장(~4M+) | **일정 간트 차트**(Phase×모듈) | PLANNING §9 |
| S59 | 선행 게이트·리스크 관리 | G1 나노바나나 승인·G2 배포서버·확인필요 8항목(C1~C8) 관리 | **리스크 매트릭스**(발생×영향) + 게이트 | strategy §4, rfp_analysis §6 |
| S60 | 품질·형상관리 | Fail-Safe 배포(헬스체크·롤백)·QA 게이트·SCA CI·경계면 검증 | 품질 게이트 흐름 | REQ-N-004, S-019 |
---
## P9. 기대효과 (3슬라이드 · 가점)
| # | 슬라이드 | 핵심 메시지 | 그래픽/도표 | REQ/근거 |
|---|---|---|---|---|
| S61 | 정량 기대효과 | 견적 수일→즉시·배치 수주→수분·검수 육안→자동·위치표시도 자동·시공 예측 가능 | **기대효과 정량 인포그래픽**(5지표 Before/After) | PLANNING §1-3 |
| S62 | 정성·전략 효과 | 참가업체 영업력·홀매니저 병목 해소·데이터 주권·전시산업 표준화 | 이해관계자별 효과 카드 | strategy §3 |
| S63 | 확장 비전·마무리 | COEX·제3전시장·타 전시관 SaaS 확장, 슬로건 재각인 | 확장 비전 로드맵 + 클로징 슬로건 | WT-5 |
---
## 부록 (선택 삽입)
| 항목 | 내용 | 근거 |
|---|---|---|
| A. 확인 필요·협의 사항 | C1~C8([가정]·⚠확인필요) 집약 — 정직성 어필, 리스크 R-1~R-8 방어 요약 | rfp_analysis §6, strategy §4 |
| B. REQ 커버리지 매트릭스 | 142 REQ(F81·A20·N21·S20)↔P1~P9 반영 표(RTM 요약) | rtm.md |
| C. 100대 기능 성숙도 | 기구현 63·부분 18·신규 19 정량 | rfp_analysis §4 |
---
## deck-designer·writer 전달 핵심
- **필수 도식 12종**(반드시 제작): ①3중 해자 벤다이어그램(S04·S12) ②모듈맵(S16) ③나노바나나 파이프라인 플로우(S20) ④Before/After 슬라이더(S21) ⑤3안 비교 화면(S24) ⑥PostGIS 배선 오버레이(S27) ⑦옥션 폐루프 흐름도(S29) ⑧종합평가 스코어 비교표(S31) ⑨시스템 아키텍처 다이어그램(S41) ⑩보안영역 분리도(S47) ⑪멀티테넌시 격리 아키텍처도(S56) ⑫일정 간트(S58).
- **추진체계도**(S15)·**리스크 매트릭스**(S59)·**기대효과 인포그래픽**(S61)은 공공 제안 표준 필수.
- **아이콘**: 전량 선(line/stroke) 스타일 SVG 직접 제작(외부 라이브러리 금지) — proposal-visual-designer 산출 준수.
- **RTM 정합 검증**: 각 슬라이드 REQ 매핑을 rtm.md 반영상태(☐→☑)로 갱신. 142 REQ 누락 0 — qa가 본 목차의 REQ 열로 교차 점검.
- **배점 규율**: P3(S16~S40, 25슬라이드)가 전체의 40%. 나노바나나(S17~S23) 7슬라이드는 절대 축소 금지(최고 배점 20). 선택 기능(S40 등)은 압축 유지.

View File

@ -1,117 +0,0 @@
# 제안서 최종 QA 보고 — 킨텍스 자동전시시스템 (나라장터 표준 7대장 재조립판)
> QA: proposal-qa · 일자: 2026-07-12 · 대상: `docs/deliverables/제안서.pptx` (97슬라이드) + `_workspace/proposal/build_deck.py`
> 검증 방법: python-pptx 텍스트 추출(`_qa_extract.py`) + REQ range 전개 대조(`_qa_rtm.py`) + 구조/배점/라벨 스캔(`_qa_struct.py`) + PowerPoint COM PNG 렌더(`render_check.py`) 시각 확인.
> **최종 판정: 승인(PASS)** — 중대 결함 3건 수정 완료·재빌드·재검증. 잔여 이슈는 발주처 확인 항목([가정]/[협의])뿐(정직하게 라벨링됨).
---
## 종합 결과
| # | 검증 항목 | 판정 | 비고 |
|---|---|---|---|
| 1 | 표준 목차 정합(7대장·신설 9슬라이드) | ✅ PASS | ~Ⅶ 순차, 신설 9종 전량 실재 |
| 2 | RTM 커버리지(142 REQ) | ✅ PASS(수정 후) | 142/142. 결함 3건(F-008·F-010·F-026) 수정 |
| 3 | 배점 정렬(90/10) | ✅ PASS | 80/20·35/80 잔재 0, 재정렬 각주 존재 |
| 4 | 정직성 라벨 | ✅ PASS | tech_advisory §0과 일치 |
| 5 | 보안(노출/워터마크) | ✅ PASS | 자격증명 노출 0, AI 고지 존재 |
| 6 | 구조 품질(목차·간지·번호) | ✅ PASS | 목차↔장 순서·배점 배지 정합 |
**수정 건수: 3건**(build_deck.py 3개 편집 → 재빌드 → COM 재렌더 → 통과)
---
## 1. 표준 목차 정합 — PASS
7대장이 `g2b_standard_toc.md` §2-1(별지 제5호서식)과 순서·구조 일치. 각 장 앞 섹션 간지(7개) 배치.
| 장 | 슬라이드 | 섹션 간지 |
|---|---|---|
| 일반현황 | S6~S9 | S5 GENERAL STATUS |
| Ⅱ 전략·방법론 | S11~S25 | S10 STRATEGY & METHODOLOGY |
| Ⅲ 기술·기능 | S27~S56 | S26 TECHNOLOGY & FUNCTION |
| Ⅳ 성능·품질 | S58~S63 | S57 PERFORMANCE & QUALITY |
| 프로젝트 관리 | S65~S68 | S64 PROJECT MANAGEMENT |
| Ⅵ 프로젝트 지원 | S70~S73 | S69 PROJECT SUPPORT |
| Ⅶ 그 밖의 사항 | S75~S79 | S74 멀티테넌시·확장 |
**신설 9슬라이드 전량 실재·내용 소스 정합:**
투입인력 M/M(S7·tech_advisory §5-2) · 정량평가(S9) · 표준FW(S24) · 관리방법론(S66 PMBOK) · 인수인계교육(S70) · SM/SLA(S71 §6) · 장애비상(S72) · 기밀보안(S73) · 상생협력(S76 별표11).
---
## 2. RTM 커버리지 — PASS(수정 후 142/142)
REQ 배지가 range 표기(예: `REQ-F-073~076`, 연속 `REQ-F-045~051·067~069`)를 포함하므로 range 전개 후 대조. 이전 QA에서 모호했던 7건 우선 확정:
| REQ | 최초 상태 | 확정 결과 |
|---|---|---|
| F-008 배치안 선택/병합 편집 | 콘텐츠 O(S31)·배지 X | **수정**: S31 배지에 008 추가 |
| F-010 최종안 버전기록·좌표확정·초대링크 | 콘텐츠 X·배지 X (진성 누락) | **수정**: S31에 4번째 플로우 칩 "[최종안 확정·초대링크]" + 캡션 + 배지 010 추가 |
| F-026 AI자료 기반 견적서 응찰·PDF·버전 | 콘텐츠 O(S36)·배지 X | **수정**: S36 배지에 026 추가 |
| F-032 낙찰→발주 자동전환 | 배지 O(S39) | 이상 없음 |
| A-003 구조화 프롬프트 보존/교체 | 배지 O(S20) | 이상 없음 |
| A-005 Before/After 슬라이더 | 배지 O(S29) | 이상 없음 |
| A-008 간판 텍스트 비전 검수 | 배지 O(S30) | 이상 없음 |
**range 전개 후 잔여 유일 진성 누락 = F-010**(초대링크/최종안 확정 콘텐츠·배지 모두 부재) → 콘텐츠+배지 동시 보강으로 해소.
수정 후 union(단독 토큰 + range 전개) 대조: **142/142 전량 반영, 미반영 0.** 부록 B(S82) "미반영 0" 표기가 사실과 일치함(수정 전에는 F-008/010/026 미배지로 과장 소지 → 해소).
---
## 3. 배점 정렬 — PASS
- 표기 일관: **기술 90(정성70+정량20) / 가격 10** (S3 배점맵, S4 [가정] 간지). `기술 80`·`가격 20`·`80/20`·`35/80` 잔재 **0건**.
- 재정렬 각주 존재: S3 "실제 공고 확인 시 재정렬", S4·S7·S81 동일 취지. 정보화 지침 제18조 원칙값 근거 명기.
- S3 배점맵이 각 부문↔장(Ⅱ·Ⅲ최다·Ⅳ·Ⅴ·Ⅵ·Ⅰ·Ⅶ) 정확 매핑.
---
## 4. 정직성 라벨 — PASS
표본: [검증됨] 16 · [구현 계획] 4 · [협의] 2 · [가정] 5 · [Non-Goal] 1. `tech_advisory §0`(라이브 실적 vs 구현 계획)과 일치:
- **[검증됨]**(라이브): 나노바나나 Gemini 워커(S17·19), PostGIS 매퍼(S17), JWT+2FA(S52), CI/CD Fail-Safe(S24), 인프라·코어(S48) — §0-1과 일치.
- **[구현 계획]**: 옥션 M15 폐루프(S17·36·37 "Phase D-M15"), 멀티테넌시(S27), F-081 프론트 분리(S60) — §0-2와 일치. 과장(구현 완료 오기) 0.
---
## 5. 보안 — PASS
- 전 슬라이드 텍스트 스캔: IPv4/서버IP · `sk-`/`AIza` API키값 · SSH계정 · 비밀번호 값 노출 **0건**.
- AI 생성/워터마크 고지: 갤러리 도입부(S85) "본 화면은 Google Stitch 기반 AI 생성 디자인 시안…" 고지 박스 존재. M5 이미지 워터마크 불변(REQ-S-009) [검증됨] S22·50·53. 갤러리 전 시안(S85~97) "AI 생성" 고지 병기.
---
## 6. 구조 품질 — PASS
- 목차(S2 "나라장터 정보시스템 표준 7대장")↔실제 장 순서·슬라이드 번호 정합.
- 간지 배점 배지(S3): 부문↔장 매핑·배점 숫자 정확.
- 부록 A(100대 기능 성숙도)·B(REQ 매트릭스)·C(가정·협의 집약)·D(화면 갤러리) 순 구성.
---
## 수정 내역 (build_deck.py)
| 슬라이드 | 편집 | 커버 REQ |
|---|---|---|
| S31 배치 3안 | 배지 `REQ-F-007·A-009`→`REQ-F-007·008·010·A-009`; 액션칩 3→4([최종안 확정·초대링크] 추가); 캡션에 F-008·F-010 근거 명시 | F-008, F-010 |
| S36 옥션 폐루프 | 배지 `REQ-F-025·031`→`REQ-F-025·026·031` (콘텐츠 "견적서 제출 PDF·버전" 기존재) | F-026 |
재빌드(97슬라이드) → COM 렌더 → S31/S36 시각 확인(오버플로·깨짐 없음, 4칩 균등 배치).
---
## 잔여 이슈 (결함 아님 — 발주처 확인 항목)
`g2b_standard_toc.md §7` T1~T5 및 부록 C 집약대로 정직 라벨링됨. QA 통과를 막지 않음:
- 배점 90/10, 예산·M/M(~150), 투입인력 실명·단가 → [가정], 공고 확정 시 재정렬/기재
- 상생협력·하도급 구체 지분 → [공고·컨소 확정 시 기재]
- wayfinding 비콘·IoT·사이니지 HW(F-070·052~055) → [협의] 시설 인프라 전제
- kxwp 연동(F-022) → ⚠확인필요
---
## 최종 판정: **승인(PASS)**
7대장 표준 목차·신설 9슬라이드 완비, RTM 142/142(수정 3건 후), 배점 90/10 일관, 정직성 라벨 정합, 보안 노출 0, AI 고지 존재. 실제 공고 확인 시 배점·목차·투입계획 재정렬만 반영하면 제출 가능.

View File

@ -1,150 +0,0 @@
# 킨텍스 자동전시시스템 구축 — 수주 전략서 (Proposal Strategy)
> 작성: proposal-strategist · 작성일: 2026-07-11 · 버전 v1.0
> 입력: `rfp_analysis.md`(REQ 142 · 배점 가정 기술80/가격20) · `rtm.md`(P1~P9 매핑) · `docs/PLANNING.md` §1~§2
> 원칙: **배점이 전략을 지배한다.** 과장·허위 금지 — rfp_analysis에 근거 있는 주장만 기재. 시크릿(키·비번·내부IP) 미기재.
> 성격: 실제 RFP 부재 → 평가배점·제출규격은 **[가정]**(공공 SI 통례). 실제 공고 확인 시 교체.
---
## 0. 전략 요약 (Executive Summary)
**한 문장 포지셔닝**: 킨텍스 자동전시시스템은 "도면과 표를 보여주는" 기존 전시테크가 아니라, **"신청서를 내는 순간 시공 후 사진을 먼저 보여주고, 그 사진으로 발주까지 종결하는" 유일한 폐루프 플랫폼**이다.
**수주 논리 3단**:
1. **배점의 44%(35/80)가 P3 기술제안**(AI 시각화·설계 20 + 기능 구현 15)에 몰려 있다 → 제안서 분량·메시지·그래픽의 40% 이상을 P3에, 그중 절반을 나노바나나+M2~M5 폐루프에 집중한다.
2. 경쟁(글로벌 전시테크·기존 공공 SI)은 **나노바나나 시공 예측 + PostGIS 실측 공간데이터 + 옥션 폐루프**의 결합을 재현할 수 없다 → 이 3결합을 "우리만의 해자(moat)"로 전면화한다.
3. 공공 발주처의 최대 리스크(외부 API·개인정보·안전서류 오용)를 **선제 방어 논리**(G1 승인 게이트+degraded 폴백, AI 워터마크 불변, PII·테넌트 3중 격리)로 무장해 감점을 원천 차단한다.
**슬로건 후보(표지·간지용)**:
- 메인: **"신청서를 내는 순간, 시공 후 사진을 먼저 본다."** (PLANNING §1-2 비전 직인용)
- 서브: **"설계 → 시각화 → 발주 → 시공 → 운영 → 분석. 킨텍스에서 한 흐름으로 끝난다."**
---
## 1. Win Theme (차별화 메시지 5선)
> 각 Win Theme = ①발주처 핵심 니즈 ②우리 차별화 메시지(슬로건) ③배점 정렬 ④근거(REQ/PLANNING) ⑤증빙 방식. 배점 가중 순으로 배열.
### WT-1. 시공 전에 사진으로 의사결정한다 — 나노바나나 시공 예측 (최우선 · 배점 20)
- **발주처 니즈**: 참가업체·주최자가 "도면을 읽지 못해 개장일에야 결과를 처음 본다"(PLANNING §2 참가업체 Pain). 조감도 외주는 비용·시간.
- **슬로건**: **"도면이 아니라 사진으로 컨펌한다."**
- **차별화 메시지**: image-to-image 구조보존(골격·치수·통로·카메라 앵글 잠금, 스타일만 사실화)으로 표준 샷세트 S1~S7(정면·야간조명·통로·내부·Before/After·배선 오버레이·홀전경)을 평균 40초에 자동 생성. **경쟁 전시테크가 보유하지 못한 기능**(rfp_analysis §4 win 우선순위 ①).
- **배점 정렬**: "AI 시각화·설계 자동화(핵심 차별화) 20점" 직격. 대응 REQ-A-001~008, REQ-F-019.
- **증빙**: Before/After 드래그 슬라이더 실물 캡처, 샷세트 예시 이미지(워터마크 포함), RenderJob 파이프라인 도식.
### WT-2. 배치·설계·배선을 AI가 3안 자동생성하고 규정을 자동검증한다 (배점 20+15 교차)
- **발주처 니즈**: 주최자 CAD 수작업 수일~수주, 홀매니저 육안 규정 검수 병목(PLANNING §1-3, §3-2).
- **슬로건**: **"수 분 내 3안, 제출 전 규정 자동검증."**
- **차별화 메시지**: 제약 솔버+LLM 조건해석으로 부스 배치/부스 설계 각 3안 자동생성·선택/병합·버전관리(REQ-F-007~013). 규정(피난통로·바닥하중 홀별·높이 5m·리깅 6.5~8.5m·방염·이격)을 제출 전 자동 플래깅. PostGIS 최단경로로 트렌치→분전반 배선·위치표시도 자동 작도(수기 폐지, REQ-F-015~017).
- **배점 정렬**: AI 시각화·설계(핵심) 20 + 기능 구현 15의 코어. 대응 REQ-A-009·010·020, REQ-F-007~018.
- **증빙**: 3안 비교 화면, 규정 위반 플래그 대시보드, PostGIS 배선 경로 오버레이, "사람이 최종 확정"(AI는 보조) 명시.
### WT-3. AI 설계자료가 곧 발주 근거가 된다 — 공사 옥션 폐루프 (배점 15 · 폐루프 핵심)
- **발주처 니즈**: 등록업체 739개가 단순 리스트+엑셀, 매칭·견적 비교 부재(PLANNING §3-2).
- **슬로건**: **"사진 수준 자료로 응찰하고, 한 흐름에서 발주까지 끝낸다."**
- **차별화 메시지**: M2~M5가 생성한 배치·설계·배선/BOQ·시공 예측 이미지가 곧 역경매 응찰 근거. 실시간 순위·종합평가 낙찰(가격+평판+납기)→계약/발주 자동전환(REQ-F-025~032). **미등록 업체 응찰 원천 차단(403)** 으로 킨텍스 "등록업체 시공 필수" 규정을 시스템이 강제.
- **배점 정렬**: 기능 구현 15 + 보안(등록 게이트). 대응 REQ-F-025~032, REQ-S-007.
- **증빙**: 옥션 흐름도(설계자료→응찰→실시간순위→낙찰→발주), 종합평가 스코어 비교표, 등록 게이트 차단 시퀀스.
### WT-4. 공공이 안심하는 AI — 워터마크·PII·테넌트 3중 격리 불변 (배점 10)
- **발주처 니즈**: 생성 이미지의 계약·심사 서류 오용 우려, 관람객/리드 개인정보 보호, 다전시관 데이터 혼입 방지.
- **슬로건**: **"AI가 만든 것은 반드시 표시하고, 남의 데이터는 원천 차단한다."**
- **차별화 메시지**: M5 이미지 응답에 워터마크 **항상** 포함(watermarkRequired:true·계약/심사 서류 금지 고지, REQ-S-009). PII 동의·마스킹·AES-256-GCM·응답 완전 제외(REQ-S-013). fail-closed 다층 테넌트 격리(컨텍스트 필터→서비스 가드→MyBatis 인터셉터→PostGIS, REQ-S-018). 외부 API는 Ollama 기본+Claude/Gemini만 승인 예외(REQ-S-001).
- **배점 정렬**: 보안·개인정보 10점. 대응 REQ-S 20항목 전량.
- **증빙**: 보안영역 분리도(DMZ/내부망), 워터마크 실물, 감사로그 전수 표, PII 마스킹 예시.
### WT-5. 킨텍스에서 검증하고 코엑스로 확장한다 — 멀티테넌트 SaaS (배점 5 · 미래가치)
- **발주처 니즈**: 킨텍스 제3전시장(2028, 178,000㎡) 확장 + 국내 전시산업 표준 플랫폼화.
- **슬로건**: **"코드 배포 없이 데이터 온보딩만으로 새 전시관을 연다."**
- **차별화 메시지**: KINTEX=기준 테넌트 #1, COEX=테넌트 #2 온보딩 6단계(마스터데이터 입력 수준). tenant_id 전파·2계층 관리자(플랫폼/테넌트)·컨텍스트 해소(REQ-F-077~079). 기능은 전부 보존, 데이터·권한만 전시관 단위 분리.
- **배점 정렬**: 멀티테넌시·확장성 5점 + 사업 미래 비전. 대응 REQ-F-077~079, REQ-N-020, REQ-S-018.
- **증빙**: 멀티테넌시 격리 아키텍처, 온보딩 6단계 도식, 테넌트 스위처 화면.
---
## 2. 평가항목별 대응 전략 (배점 가중)
> 각 평가부문에 "무엇을·어떻게 보여줄지". 정량 항목=수치, 정성 항목=스토리. 배점 큰 순.
| # | 평가부문 | 배점 | 대응 전략 (증빙·실적·방법론·도표) | 주요 REQ | 목차 |
|---|---|---|---|---|---|
| 1 | **AI 시각화·설계 자동화(핵심)** | **20** | WT-1·WT-2 전면. 나노바나나 파이프라인 도식 + Before/After 실물 + 3안 생성·규정검증·PostGIS 배선 화면. "결정적 알고리즘 중심(제약 솔버) + LLM 보조 + 사람 최종확정" 3층 구조로 **AI 신뢰성** 강조 | REQ-A-001~010·020, F-007~019 | P3 |
| 2 | **기능 구현(M1~M18)** | 15 | 모듈맵 1장으로 전 모듈 조망 → 배점 큰 옥션(WT-3)·관람·BI를 상세, 선택(P2) 기능은 "구현 로드맵"으로 간결. 100대 기능 성숙도(기구현 63·부분 18·신규 19) **정량 제시** | REQ-F 전량, 특히 F-025~032 | P3 |
| 3 | **AI 설계 자동화(폐루프)** | (20 내) | WT-3. 옥션 흐름도 + 종합평가 스코어 비교표 + 발주 자동전환. AI 자료→응찰 근거 연결이 **경쟁이 못 하는 폐루프** | REQ-F-025~032, S-007 | P3 |
| 4 | 아키텍처·기술스택 | 10 | 확정 스택(React18/19+Spring Boot 3.x+MyBatis+PostgreSQL/PostGIS+Redis+나노바나나 Python 워커) 정합. 단일 공간 원천(M2폴리곤·M4배선 매퍼 권위)·워커 독립스케일·읽기복제 오프로드·역할별 프론트 분리 도식 | REQ-N-008~010·017·019, A-007·016, F-072·080·081 | P4 |
| 5 | 보안·개인정보(불변) | 10 | WT-4. REQ-S 20항목 커버리지 표 + 보안영역 분리도 + 워터마크·PII·감사로그·2FA. **감점/실격 리스크를 정면 방어** | REQ-S 20 전량, A-006·017 | P5 |
| 6 | 비기능(성능·가용성·접근성·다국어) | 8 | SLO 99.5%(성수기 99.9% 지향)·백엔드 2인스턴스 롤링 무중단·PG 스탠바이+PITR·나노바나나 40초/1024px·WCAG 2.1 AA+KWCAG 2.2·i18n 한/영/중/일·라이트/다크. **수치 SLA 표** | REQ-N 21 전량 | P6 |
| 7 | 멀티테넌시·확장성(COEX) | 5 | WT-5. 격리 아키텍처 + 온보딩 6단계 + tenant_id 선두 복합 인덱스. 제3전시장·타 전시관 확장 미래가치 | REQ-F-077~079, N-020, S-018 | P7 |
| 8 | 사업이해·추진전략 | 10 | As-Is 병목(HWP·CAD 수작업·kxwp·유틸리티 파편화) → To-Be 정량목표(견적 즉시·배치 수분·위치표시도 자동) 대비표. **킨텍스 실무 프로세스 정확 이해**(D-150/30/25/7 타임라인)로 도메인 신뢰 확보 | 전체 맥락 | P1·P2 |
| 9 | 사업관리·품질·일정 | 2 | Phase 1~3(코어→워크플로/운영→현장/확장) 간트 + QA 게이트 + Fail-Safe 배포 + 선행게이트(G1·G2) 관리. 배점 낮으나 실행력 신뢰 | REQ-N-004, S-019 | P8 |
| — | 기대효과 | (가점) | To-Be 정량 개선(견적 수일→즉시, 배치 수주→수분, 검수 육안→자동) + ROI·표준화·확장 스토리 | 목표표(§1-3) | P9 |
**분량 배분 원칙**: P3(35점)에 전체 슬라이드의 ~40%, 그중 절반을 나노바나나+M2~M5. P4·P5(각 10점) 각 ~8%, P6(8점) ~6%, P1+P2(10점) ~16%, P7(5점) ~5%, P8(2점) ~5%, P9 ~5%. 선택 기능은 압축.
---
## 3. 경쟁사 대비 강점 (Competitive Positioning)
> 근거 있는 비교만. 경쟁을 폄하하지 않고 "우리가 결합한 것"을 강조.
### 3-1. 글로벌 전시테크(ExpoPlatform·VenueSight·Swapcard·RainFocus 등) 대비
| 축 | 글로벌 전시테크 | 킨텍스 자동전시시스템 | 근거 |
|---|---|---|---|
| 시공 결과 시각화 | 도면·3D 뷰어 수준, 사진급 시공 예측 **미보유** | 나노바나나 image-to-image 구조보존 사진 생성 | PLANNING §1-2·§6, WT-1 |
| 공간 데이터 | 좌표·구역 관리 | **PostGIS 실측**(부스 폴리곤·트렌치 포인트·배선 LineString) 단일 원천 | REQ-N-008·017, data.md |
| 발주 연결 | 리드/매칭까지, 공사 발주 폐루프 약함 | AI 설계자료→역경매 응찰→낙찰→발주 자동전환 폐루프 | WT-3, §M15 |
| 킨텍스 도메인 정합 | 범용 글로벌 규격 | 킨텍스 규정(높이 5m·리깅·방염·바닥하중)·요율(2,250원/㎡)·D-데이 타임라인 내재화 | PLANNING §3, §7-1 |
| 데이터 주권 | 해외 SaaS·데이터 국외 | 온프레미스 Ollama 기본, 외부 AI는 승인 예외만·워커 env 격리 | REQ-S-001·002 |
**메시지**: "글로벌 전시테크의 기능(부스판매·리드·비즈매칭·BI)은 이식하되, 그들이 못 하는 **실측 공간데이터×시공 예측 이미지×옥션 폐루프**를 관통시켰다."
### 3-2. 기존 공공 SI(범용 웹시스템 구축) 대비
| 축 | 범용 SI | 킨텍스 자동전시시스템 | 근거 |
|---|---|---|---|
| AI 내재화 | 챗봇·검색 부가 수준 | 배치 3안·규정검증·시공 예측·수요예측이 **핵심 업무 자동화** | REQ-A 20항목 |
| 공간 연산 | 일반 CRUD | PostGIS GiST 실시간 공간쿼리(트렌치 KNN·통로버퍼·비상구 교차) | REQ-N-008 |
| 표준 프레임워크 | 개별 구축 | GUARDiA 표준(WISE/UIWS) 공통·시스템관리·JWT+2FA 이식으로 검증된 자산 재사용 | REQ-F-071~076, §5B |
| 확장성 | 단일 기관 | 멀티테넌트 SaaS(COEX 온보딩)로 재사용·표준화 | WT-5, §1A |
**메시지**: "범용 SI가 아니라 전시 도메인 전용 AI 자동화 플랫폼. 검증된 공통 프레임워크(UIWS/WISE) 위에 도메인 코어(M2~M5)와 폐루프(M15)를 얹어 **개발 리스크와 기간을 동시에 단축**한다."
### 3-3. 우리만의 3중 해자(Moat) — 한 장 요약
**나노바나나 시공 예측 이미지 ∩ PostGIS 실측 공간데이터 ∩ 공사 옥션 폐루프** — 이 세 가지의 결합은 단일 요소로는 모방 가능하나 **셋의 결합은 재현 난이도가 높다**. 제안서 P3 도입부에 "3원 벤다이어그램" 1장으로 각인.
---
## 4. 리스크 방어 논리 ([가정]·⚠확인필요 정면 대응)
> 평가위원이 반드시 파고들 "약한 고리"를 **선제적으로 드러내고 방어**한다. 숨기면 감점, 정면 대응하면 신뢰.
| # | 리스크(발주처 우려) | 방어 논리 (정면 대응) | 근거 | 제안서 위치 |
|---|---|---|---|---|
| R-1 | **G1 나노바나나 Gemini 외부 API 미승인 시 핵심 기능 붕괴?** | "승인 게이트 + degraded 폴백" 2중 설계. G1 미승인/Gemini 장애 시 워커 **목(mock) 응답·구조·워터마크 유지**로 화면·워크플로 계속 동작(REQ-N-005). 승인 확보를 **선행 게이트로 프로젝트 관리**(kintex-pm 통제). 외부 API는 워커 env only·백엔드 미취급으로 보안 격리 | REQ-S-001·002, N-005, C5 | P3 도입·P5·P8 |
| R-2 | **CAD·트렌치 실측 데이터 미확보로 배선·배치 정확도 의심?** | 미확보 정직 고지 + "공개 스펙 기반 가정 그리드 + '가정' 라벨"로 데모, **실측 반영을 킨텍스 협의 선행작업으로 명시**. 확보 자산(홀별 평면도 JPG 15장·CAD DWG "평면,트렌치.dwg")으로 실현성 입증 | C3, PLANNING R4, CLAUDE.md 확보자산 | P3 배선·P8 |
| R-3 | **kxwp/kxfp 폐쇄형 API 미공개로 제출 연동 불가?** | "초기 수동 릴레이 → 목표 API 연동" 단계적 접근. 웹폼→HWP/PDF 자동생성까지는 API 무관하게 완결, 릴레이 단계만 킨텍스 IT 협의(⚠확인필요 명시). 리스크를 범위·일정에 반영 | REQ-F-022, C2 | P3 서류·P8 |
| R-4 | **평가배점·제출규격이 가정이라 목차 정합성 의심?** | 배점 가정을 **투명 고지**하고 "실제 공고 확인 시 목차·분량 즉시 재정렬" 유연성 제시. 목차는 공공 SI 통례(사업이해→전략→기술→아키→보안→비기능→확장→관리→기대효과)로 안전 정렬 | rfp_analysis §4·§5, C1 | P1·목차 간지 |
| R-5 | **간판 한글 텍스트 등 생성 이미지 품질 편차?** | 간판 텍스트 비전 오탈자 검수 + 한글 렌더 실패 시 **간판영역 후처리 합성**(REQ-A-008). 자동생성은 S1·S7 한정·캐시·쿼터로 품질/비용 제어(REQ-N-007) | REQ-A-008, N-007, R5 | P3 AI·P6 |
| R-6 | **AI 환각·오판(규정 오검증·설계 오류) 책임?** | "AI는 보조, 사람이 최종 확정" 원칙 불변. 규정검증=플래깅까지, 구조안전 판정은 구조기술사/킨텍스(Non-Goal 명시). AI 답변은 RAG 근거·인용·환각 차단(abstain)(REQ-A-017) | Non-Goal §1-4, REQ-A-017 | P3·P5 |
| R-7 | **사이니지 HW·비콘·IoT 센서 연동 불확실?** | 선택(P2) 기능으로 분류, "킨텍스 시설 인프라 협의 전제" 명시. 미협의 시에도 코어 가치 불변(WT-1~4는 HW 독립) | REQ-F-070·052~055, C7 | P3 현장·P8 |
| R-8 | **다전시관 확장이 개발 부담을 키우지 않나?** | 멀티테넌시=격리 레이어만 추가(tenant_id 전파), 기능 로직 불변. 백필 전 단일테넌트 무중단 동작, 검증 후 필터 활성화(회귀 방지) | §1A-5, REQ-F-077 | P7 |
**방어 원칙**: 모든 [가정]·⚠확인필요 항목은 제안서 부록 "확인 필요·협의 사항" 1장으로 집약해 **정직성**을 오히려 강점화(발주처와의 협업 자세 어필).
---
## 5. writer·deck-designer 전달 지침
- **메시지 우선순위**: WT-1 > WT-2 > WT-3 > WT-4 > WT-5 순으로 슬라이드 무게. P3 도입부에 "3중 해자" 벤다이어그램 필수.
- **정량 강조**: 견적 수일→즉시, 배치 수주→수분, 검수 육안→자동, 40초 렌더, SLO 99.5%, REQ 142 커버리지 등 숫자를 표지·간지·기대효과에 반복 노출.
- **AI 신뢰 프레이밍**: "결정적 알고리즘(제약 솔버·PostGIS) 중심 + LLM 보조 + 사람 최종확정" 3층을 모든 AI 슬라이드 캡션에 삽입 → 공공 AI 불안 해소.
- **RTM 정합**: 전 142 REQ가 P1~P9 어딘가에 반영되도록 목차(§proposal_outline.md)의 REQ 매핑을 준수. 누락 0.
- **금지**: 시크릿·내부IP·미확보 데이터의 확정적 서술 금지. 미확보는 "가정/협의 전제" 라벨 필수.

View File

@ -1,23 +0,0 @@
# -*- coding: utf-8 -*-
"""PowerPoint COM 렌더링 검증 — 전 슬라이드 PNG export."""
import sys, os, glob
import win32com.client as win32
PPTX = sys.argv[1] if len(sys.argv) > 1 else r"C:\GUARDiA\workspace\kintex\docs\deliverables\제안서.pptx"
OUT = sys.argv[2] if len(sys.argv) > 2 else r"C:\Users\ython\AppData\Local\Temp\claude\C--GUARDiA-workspace-kintex\85f77606-5d61-424b-8a38-f8b61b56b54c\scratchpad\render"
os.makedirs(OUT, exist_ok=True)
for f in glob.glob(os.path.join(OUT, "*.png")):
try: os.remove(f)
except: pass
app = win32.Dispatch("PowerPoint.Application")
try:
app.Visible = True
except Exception:
pass
pres = app.Presentations.Open(PPTX, ReadOnly=True, Untitled=False, WithWindow=False)
n = pres.Slides.Count
for i in range(1, n+1):
pres.Slides(i).Export(os.path.join(OUT, f"s{i:03d}.png"), "PNG", 1280, 720)
pres.Close(); app.Quit()
print("RENDERED", n, "slides ->", OUT)

View File

@ -1,366 +0,0 @@
# 킨텍스 자동전시시스템 구축 — 요구사항 분석서 (가상 RFP 기반)
> 작성: rfp-analyst · 작성일: 2026-07-11 · 버전 v1.0
> **성격**: 실제 RFP 문서는 부재하며, 발주처(킨텍스, 한국국제전시장)의 내부 기획 문서를 "가상 RFP"의 요구사항 원천으로 삼아 제안서 작성·평가 대응용으로 구조화 추출한 것이다.
> **근거 문서(출처)**: `docs/PLANNING.md`(v3.0), `docs/FEATURE_BACKLOG_100.md`(v1.1), `docs/SECURITY.md`, `docs/architecture/{app,system,tech,data,network}.md`, `CLAUDE.md`(kintex 섹션).
> **원칙**: 문서에 없는 요구는 발명하지 않는다(각 REQ에 출처 병기). 비밀번호·키·IP 등 시크릿은 미기재. 가정(assumption)은 "[가정]"으로 명시.
---
## 1. 사업 개요 (가정 포함)
| 항목 | 내용 | 근거/비고 |
|---|---|---|
| 사업명 | 킨텍스 자동전시시스템(Exhibition Automation Platform) 구축 | PLANNING §1-2-1 |
| 발주처(가정) | 킨텍스(KINTEX, 한국국제전시장) — 기준 테넌트 #1 | PLANNING §1A-1 |
| 사업 목적 | 전시장 운영 전 주기(홀 배정→부스 배치→장치공사 설계→전기/조명→네트워크/유틸리티 배선→반입/반출→관람객·정산·경영분석)를 AI로 자동 설계·검증하고, 시공 결과를 나노바나나(Gemini 이미지 생성)로 "공사 후 사진"처럼 사전 시각화 | PLANNING §1-2 |
| 정체성 | 부스 시공 도구를 넘어 전시 기획·판매·시공·운영·관람·사후분석 전 주기를 아우르는 베뉴 운영 플랫폼 + 다중 전시관(Venue) 멀티테넌트 SaaS(KINTEX #1, COEX 온보딩) | PLANNING §1-2-1·§1A |
| 범위 모듈 | 코어 M2~M5(P0), 판매·발주·정산 M1·M6·M7·M9·M15, 관람·참가 M8·M10~M14, 경영·콘텐츠·관리 M16~M18, 공통/시스템관리 레이어 §5B, 멀티테넌시 §1A | PLANNING §4-1 |
| 확정 기술스택 | React 18/19(Vite·TS) + Spring Boot 3.x(Java 17) + MyBatis + PostgreSQL(PostGIS) + Redis + 나노바나나 Python 워커 사이드카 | PLANNING §8, CLAUDE.md |
| AI 플랫폼 | 텍스트=Claude 기본(AiTextRouter/AiConfig, `ANTHROPIC_API_KEY` env) → Ollama 폴백 / 이미지=나노바나나(Gemini `gemini-3.1-flash-image-preview`) | FEATURE_BACKLOG §0, PLANNING §6-4 |
| 사용자 역할 | 6 행사역할(주최자·참가업체·장치/시공업체·홀매니저·관람객·일반대중) + 2 관리자계층(테넌트 관리자·플랫폼 슈퍼관리자) | PLANNING §2·§1A-4 |
| 로드맵(가정) | Phase 1 코어(~4개월)·Phase 2 워크플로/운영(~4개월)·Phase 3 현장/확장(~4개월+) | PLANNING §9 |
| 선행 게이트 | G1 나노바나나 Gemini 승인(외부 API), G2 개발/배포 서버(kintex.zioinfo.co.kr, PostGIS+Redis) | CLAUDE.md, PLANNING §10 R12 |
**[가정] 명시 항목**: 예산·계약방식·정확한 사업기간·평가배점·제출규격은 실제 RFP가 없어 공공 SI 통례를 가정한다(§4·§5).
---
## 2. 과업 범위
### 2-1. 기능 과업 범위 (모듈별)
| 계열 | 모듈 | 우선순위 | 범위 요지 | 출처 |
|---|---|---|---|---|
| 코어(불변) | M2 플로어플랜 스튜디오 | P0 | 부스 배치 3안 자동생성·선택/병합·규정 자동검증·버전 | PLANNING §M2 |
| 코어 | M3 부스 설계 스튜디오 | P0 | 조립/독립부스 설계 3안·규정 사전검증·도면 비전추출 | PLANNING §M3 |
| 코어 | M4 유틸리티 설계 | P0 | 전기·조명 용량/분전반/배선(M4a), 네트워크·급배수·압축공기·위치표시도(M4b) | PLANNING §M4 |
| 코어 | M5 나노바나나 시각화 | P0 | 시공 후 예상사진 표준 샷세트(S1~S7)·Before/After·워터마크 | PLANNING §M5·§6 |
| 판매·발주 | M1 홀 배정·자동견적 | P1 | 가용성 캘린더·규칙엔진 견적·배정신청 서식 | PLANNING §M1 |
| 판매·발주 | M6 서류·마일스톤 | P1 | D-150/30/25/7 역산·웹폼→HWP/PDF·AI 서류검수·kxwp 릴레이 | PLANNING §M6 |
| 판매·발주 | M7 등록업체 매칭 | P1 | 14분류×739개 업체 추천·RFQ·등록업체 검증 | PLANNING §M7 |
| 판매·발주 | M15 공사/장치 옥션 | P1(핵심) | 역경매·견적서(Quotation) 응찰·실시간 순위·종합평가 낙찰·발주전환 | PLANNING §M15 |
| 판매·발주 | M9 정산·결제 | P1 | 납부스케줄·PG결제·예치금 실사용 정산·세금계산서 | PLANNING §M9 |
| 물류 | M8 반입/반출 물류 | P2 | 슬롯예약·통행증 QR·중량물 우선·지게차 | PLANNING §M8 |
| 관람 | M10 관람객 등록·배지·리드 | P1 | 사전등록·모바일 배지/QR·현장 체크인·리드캡처·오프라인 폴백 | PLANNING §M10 |
| 관람 | M11 비즈니스 매칭 | P2 | AI 미팅추천·슬롯예약·성과 리포트 | PLANNING §M11 |
| 관람 | M12 마케팅·공개 홍보사이트 | P1 | SEO·다국어 공개사이트·인터랙티브 플로어플랜·세그먼트 EDM | PLANNING §M12 |
| 관람 | M13 wayfinding | P2 | 실내 내비(블루닷/존레벨)·POI 검색·경로 | PLANNING §M13 |
| 현장 | M14 현장운영 | P2 | 혼잡·전력부하·주차·안전위반 플래그 | PLANNING §M14 |
| 경영 | M16 경영분석 BI | P1(승격) | 가동률·매출구성·P&L·RevPAD·리텐션/LTV·수요예측·KPI(운영사+참가사 관점 분리) | PLANNING §M16·§M16-1 |
| 경영 | M17 CMS | P1 | 콘텐츠·공지·마이크로사이트·다국어·게시 워크플로·사이니지 | PLANNING §M17 |
| 관리 | M18 관리자 백오피스 | P1 | RBAC·감사·마스터데이터·룰셋 버전관리 | PLANNING §M18 |
| 공통 | §5B 공통/시스템관리 레이어 | P1(선행기반) | UIWS 표준 이식: 시스템관리·공통 업무모듈·JWT+2FA/OTP | PLANNING §5B |
| 멀티테넌시 | §1A 다중 전시관 SaaS | P1 | tenant_id 격리·컨텍스트 해소·2계층 관리자·온보딩 | PLANNING §1A·§8-2 |
### 2-2. 비기능 과업 범위
| 축 | 범위 | 출처 |
|---|---|---|
| 성능 | RenderJob 평균 40초·1024px 전처리, PostGIS GiST 실시간 응답, 읽기복제 BI 오프로드 | system.md §3-3, PLANNING §6-5 |
| 가용성/HA | 코어 경로 SLO 99.5%(성수기 99.9% 지향), 백엔드 최소 2인스턴스·롤링 무중단, PG 프라이머리-스탠바이+PITR, degraded 모드 | system.md §3-2 |
| 확장성 | 워커 독립 스케일(큐 깊이 기반), 공개영역 CDN, 역할별 프론트 번들 분리 | system.md §3-1 |
| 접근성 | WCAG 2.1 AA(핵심여정 AAA 지향), 공공 KWCAG 2.2 병행 | FEATURE_BACKLOG §6-N1 |
| 다국어 | 한(기본)+영/중/일, i18n 외부화·hreflang·로케일 포맷 | FEATURE_BACKLOG §6-N3 |
| 테마 | CSS 토큰 라이트/다크 2벌·시스템 연동 | FEATURE_BACKLOG §6-N4 |
| 공간데이터 | 부스 폴리곤·트렌치 포인트·배선 LineString 단일 PostGIS 원천 | data.md, PLANNING §7-3 |
| 보안 | §3 참조(REQ-S) | SECURITY.md |
### 2-3. 범위 제외(Non-Goals)
- 킨텍스 임대계약 법적 전자계약 체결(Phase 3 이후 검토) — 초기엔 견적·서류 준비까지. (PLANNING §1-4)
- 구조계산서의 구조 안전성 최종 판정(AI는 항목 체크·누락 검출까지, 판정은 구조기술사/킨텍스). (PLANNING §1-4·§M3)
- 소방 법규 최종 유권해석·승인(시스템은 사전 필터). (PLANNING §M2)
- 신규 전시관 고유 도메인 로직 자동생성(온보딩은 마스터데이터 입력 수준). (PLANNING §1-4·§1A-5)
---
## 3. 요구사항 목록
> ID 체계: 기능 `REQ-F-###`, 비기능 `REQ-N-###`, 보안 `REQ-S-###`, AI `REQ-A-###`.
> 유형: **필수** / **선택**(P2·확장·조건부). 확인 필요 항목은 "⚠확인필요"로 표기(삭제 금지).
### 3-1. 기능 요구사항 (REQ-F)
#### M1 판매·홀배정·견적
| REQ-ID | 요구사항 | 유형 | 출처 |
|---|---|---|---|
| REQ-F-001 | 홀/반홀 가용성 실시간 캘린더 조회(행사일정 DB 연동) | 필수 | F001, §M1 |
| REQ-F-002 | 규칙엔진 자동 견적(2,250원/㎡×면적×일수×성수기/비수기/1전시장 계수+초과시간+예치금 15~20%) | 필수 | F002, §M1 |
| REQ-F-003 | 배정신청 웹폼 → 킨텍스 제출 서식(HWP) 자동 생성 | 필수 | F004, §M1 |
| REQ-F-004 | 부스 판매 인벤토리 관리(부스 단위 판매/홀드/예약 상태) | 필수 | F005 |
| REQ-F-005 | 온라인 부스 셀프 선택·판매(인터랙티브 맵) | 필수 | F006 |
| REQ-F-006 | 임대계약 전자서명 워크플로 | 선택 | F007(Phase3, Non-Goal 완화) |
#### M2 플로어플랜 스튜디오 (P0)
| REQ-ID | 요구사항 | 유형 | 출처 |
|---|---|---|---|
| REQ-F-007 | 홀 선택·조건 입력 후 부스 배치 정확히 3안 자동생성(제약 솔버, 상이한 최적화 목표) | 필수 | F008, §M2 |
| REQ-F-008 | 배치안 선택 또는 구역/블록 병합 편집(안별 레이어 토글·드래그) | 필수 | F009, §M2 |
| REQ-F-009 | 배치 규정 자동검증(피난통로·바닥하중 홀별·비상구·복층가능홀·소방 체크리스트), 병합 후 재검증 | 필수 | F010, §M2 |
| REQ-F-010 | 최종안 버전 기록(선택/병합 출처 안번호 추적)·부스별 좌표/번호 확정·참가업체 초대 링크 발급 | 필수 | §M2 |
#### M3 부스 설계 스튜디오 (P0)
| REQ-ID | 요구사항 | 유형 | 출처 |
|---|---|---|---|
| REQ-F-011 | 조립부스 옵션(간판·가구·조명) 웹 선택·3D 프리뷰 | 필수 | F012, §M3 |
| REQ-F-012 | 독립부스 레이아웃·파라메트릭 구조 3안 자동생성·선택/병합·버전 기록 | 필수 | F011, §M3 |
| REQ-F-013 | 설계 규정 사전검증(높이 5m·리깅 6.5~8.5m·복층 1/2·방염·이격 30/60cm·금지작업) 제출 전 플래깅 | 필수 | F013, §M3 |
| REQ-F-014 | 도면(PDF/이미지) 업로드 비전 추출·치수/구조 검증("참고용 검증" 라벨) | 필수 | F014, §M3 |
#### M4 유틸리티 설계 (P0)
| REQ-ID | 요구사항 | 유형 | 출처 |
|---|---|---|---|
| REQ-F-015 | 기기목록→kW 합산→분전반 용량/수량 자동 산출 | 필수 | F015, §M4a |
| REQ-F-016 | 최근접 트렌치→분전반 배선 경로 자동생성(통로 횡단 최소화, PostGIS 최단) | 필수 | F016, §M4 |
| REQ-F-017 | 유틸리티 위치표시도 자동생성(좌표 클릭, 수기 작도 폐지) | 필수 | F017, §M4b |
| REQ-F-018 | 네트워크·급배수·압축공기 신청·자동견적·마감(D-25) 역산 리마인더 | 필수 | F038, §M4b |
| REQ-F-019 | 조명 조도목표별 배치안 제안(전시품 강조/상담존) | 선택 | F018, §M4a |
#### M6 서류·마일스톤
| REQ-ID | 요구사항 | 유형 | 출처 |
|---|---|---|---|
| REQ-F-020 | D-150/30/25/7 마일스톤 자동생성·역산 알림 | 필수 | F021, §M6 |
| REQ-F-021 | 신고서류 7종+ 웹폼→HWP/PDF 자동생성(킨텍스 제출 형식) | 필수 | F022, §M6 |
| REQ-F-022 | kxwp 제출 파일 릴레이·업로드 안내(초기 수동 릴레이, 목표 API) ⚠확인필요(폐쇄형 API 미공개) | 필수 | F027, §M6·R3 |
| REQ-F-023 | 서류 버전·다단계 전자결재 워크플로 | 필수 | F028, §5B |
| REQ-F-024 | OCR 계약·납품서 자동 등록 | 필수 | F026 |
#### M15 공사/장치 옥션 (P1 핵심)
| REQ-ID | 요구사항 | 유형 | 출처 |
|---|---|---|---|
| REQ-F-025 | 역경매 옥션 개설·라운드·마감(유형 설정형: 역경매/단일라운드 RFQ/고정가 비교) | 필수 | F029, §M15 |
| REQ-F-026 | AI 생성 자료(M2배치·M3설계·M4배선/BOQ·M5이미지) 열람 기반 견적서(Quotation) 제출·PDF·버전 | 필수 | F030, §M15 |
| REQ-F-027 | 실시간 순위·최저가·내 위치 노출(익명 옵션)·라운드 종료 자동마감 | 필수 | F031, §M15 |
| REQ-F-028 | 종합평가 낙찰 스코어(가격+평판+납기 가중)·항목별 비교표 | 필수 | F032, §M15 |
| REQ-F-029 | 등록업체 검증 게이트 — 미등록 업체 응찰 원천 차단 | 필수 | F033, §M15·§M7 |
| REQ-F-030 | 등록업체 AI 매칭 추천(규모·업종·지역) | 필수 | F034, §M7 |
| REQ-F-031 | 물량서(BOQ) 설계기반 자동산출 | 필수 | F035, §M4·§M15 |
| REQ-F-032 | 낙찰(Award)→계약/발주 문서 자동전환(M6·M9 연동) | 필수 | F036, §M15 |
#### M8 물류
| REQ-ID | 요구사항 | 유형 | 출처 |
|---|---|---|---|
| REQ-F-033 | 반입/반출 하역장 슬롯 예약제 | 선택 | F039, §M8 |
| REQ-F-034 | 통행증 QR 발급·중량물 우선배치 | 선택 | F040, §M8 |
| REQ-F-035 | 지게차·부대장비 신청 연동, 부대물품 렌탈 주문 | 선택 | F041·F042, §M8 |
| REQ-F-036 | 철거 피크 대기열 시뮬레이션 | 선택 | F044, §M8 |
#### M10 관람객 등록·배지·리드
| REQ-ID | 요구사항 | 유형 | 출처 |
|---|---|---|---|
| REQ-F-037 | 온라인 사전등록(유형별 폼·중복 검증) | 필수 | F045, §M10 |
| REQ-F-038 | 모바일 배지/QR 발급 | 필수 | F047, §M10 |
| REQ-F-039 | 현장 QR 체크인·즉석 배지 인쇄·실시간 입장 집계 | 필수 | F048, §M10 |
| REQ-F-040 | 오프라인 체크인 폴백(현장 네트워크 장애 대비 후동기화) | 필수 | F049, §M10, network.md |
| REQ-F-041 | 참가업체 리드캡처(배지 QR 스캔·관심도·메모) | 필수 | F050, §M10 |
| REQ-F-042 | AI 리드 스코어링·자동 분류 | 선택 | F051 |
| REQ-F-043 | 리드 팔로업 세그먼트 EDM 자동 | 필수 | F052, §M10·§M12 |
| REQ-F-044 | 티켓 발권·유료 등록 결제, AI 폼빌더, 스마트배지 | 선택 | F053·F046·F054 |
#### M11 비즈매칭·이벤트앱
| REQ-ID | 요구사항 | 유형 | 출처 |
|---|---|---|---|
| REQ-F-045 | AI 비즈매칭 추천(프로필·intent) | 선택 | F055, §M11 |
| REQ-F-046 | 미팅 슬롯 예약·일정관리·성과 리포트 | 선택 | F056·F062, §M11 |
| REQ-F-047 | 이벤트 모바일앱(일정·부스·프로필), 세션·아젠다, 인앱 채팅, 실시간 설문 | 선택 | F057~F061 |
#### M12 마케팅·공개 홍보사이트
| REQ-ID | 요구사항 | 유형 | 출처 |
|---|---|---|---|
| REQ-F-048 | 공개 홍보 사이트(SEO·다국어 한/영/중/일·SSR/정적) | 필수 | F063, §M12 |
| REQ-F-049 | 공개 인터랙티브 플로어플랜(M2 데이터 부산물) | 필수 | F064, §M12 |
| REQ-F-050 | 세그먼트 EDM·캠페인 자동화·리마인더 | 필수 | F065, §M12 |
| REQ-F-051 | 스폰서십 패키지·판매·이행 관리 + 스폰서 대시보드 | 필수 | F070·F071 |
#### M13·M14 현장/wayfinding
| REQ-ID | 요구사항 | 유형 | 출처 |
|---|---|---|---|
| REQ-F-052 | 실내 wayfinding(블루닷/존레벨 폴백)·POI 검색·경로안내 | 선택 | F072·F073, §M13 |
| REQ-F-053 | 실시간 입장·혼잡 모니터·오버플로 예측 | 선택 | F074, §M14 |
| REQ-F-054 | 홀 전력부하 집계·에너지 모니터, 주차 점유 연동 | 선택 | F075·F077, §M14 |
| REQ-F-055 | 안전 위반·소음·금지작업 자동 플래그·신고, 현장 이상탐지 알림 | 선택 | F076·F079, §M14 |
#### M9 정산·결제
| REQ-ID | 요구사항 | 유형 | 출처 |
|---|---|---|---|
| REQ-F-056 | 납부 스케줄(계약금/중도금/잔금/예치금) 자동생성·알림 | 필수 | F080, §M9 |
| REQ-F-057 | 유틸리티·부대 PG 결제 | 필수 | F081, §M9 |
| REQ-F-058 | 예치금 대비 실사용(검침) 정산 투명화 | 필수 | F082, §M9 |
| REQ-F-059 | 세금계산서·정산 리포트 자동, 옥션 수수료 정산 연동 | 필수 | F083·F084, §M9 |
| REQ-F-060 | 환불·취소 규정 자동 적용 | 선택 | F085 |
#### M16 경영분석 BI
| REQ-ID | 요구사항 | 유형 | 출처 |
|---|---|---|---|
| REQ-F-061 | 홀·기간별 가동률(occupancy) 대시보드(반홀 분할·성수기 구분) | 필수 | F086, §M16-1① |
| REQ-F-062 | 매출 구성(임대·유틸리티·부대·옥션 수수료)·행사별 P&L | 필수 | F087, §M16-1②③ |
| REQ-F-063 | 전시장 ROI·RevPAD·㎡당 수익(㎡ 정규화) | 필수 | F088, §M16-1④ |
| REQ-F-064 | 참가사 리텐션·LTV 코호트 | 필수 | F089, §M16-1⑤ |
| REQ-F-065 | 참가업체 관점 ROI(리드 기반) — 운영사 관점과 대시보드/권한 격리 | 필수 | F091, §M16-1 |
| REQ-F-066 | 경영진 KPI 대시보드(목표 대비 실적·드릴다운) | 필수 | F093, §M16-1⑦ |
#### M17 CMS
| REQ-ID | 요구사항 | 유형 | 출처 |
|---|---|---|---|
| REQ-F-067 | 전시 콘텐츠·공지 게시 워크플로(초안→검수→게시)·버전관리 | 필수 | F068, §M17 |
| REQ-F-068 | 참가업체 마이크로사이트(부스·제품·나노바나나 예상샷) | 필수 | F067, §M17 |
| REQ-F-069 | 다국어(한/영/중/일) 콘텐츠 관리·hreflang | 필수 | F069, §M17 |
| REQ-F-070 | 디지털 사이니지 콘텐츠 중앙 배포 ⚠확인필요(사이니지 HW 연동 킨텍스 협의) | 선택 | F078, §M17 |
#### M18·§5B·§1A 관리·공통·멀티테넌시
| REQ-ID | 요구사항 | 유형 | 출처 |
|---|---|---|---|
| REQ-F-071 | RBAC·감사로그·시스템설정 백오피스(admin. 웹 전용) | 필수 | F094, §M18·§5B-1 |
| REQ-F-072 | 룰셋·마스터데이터(홀·요율·규정·부스표준) 버전관리·무중단 개정 | 필수 | F095, §M18 |
| REQ-F-073 | 공통코드·메뉴 관리(도메인 코드: 홀/부스유형/공종14/유틸요금) | 필수 | §5B-1 |
| REQ-F-074 | 공통 업무모듈: 업무일지·일정·쪽지·공지·의견·통합검색 | 필수 | F098, §5B-2 |
| REQ-F-075 | 회의록(녹음→STT→회의록 PDF), 일/주/월/분기/연 업무보고·통계 PDF | 필수 | §5B-2 |
| REQ-F-076 | 통합 알림센터(마감 D-데이·낙찰·승인·결제, WebSocket) | 필수 | §5B-2 |
| REQ-F-077 | 멀티테넌트 격리·전시관 온보딩(코드 배포 없이 데이터 온보딩 6단계) | 필수 | F096, §1A·§8-2 |
| REQ-F-078 | 테넌트 컨텍스트 해소(서브도메인 우선+사용자소속 보조, 불일치 거부) | 필수 | §1A-3 |
| REQ-F-079 | 2계층 관리자(플랫폼 슈퍼관리자 테넌트스위처 / 테넌트 관리자 단일스코프) | 필수 | §1A-4 |
| REQ-F-080 | API·웹훅·CRM/ERP 연동 게이트웨이 | 선택 | F099 |
| REQ-F-081 | 역할별 프론트 포털 분리(organizer·exhibitor·contractor·ops·admin·public+visitor) 공유 디자인시스템 상속 | 필수 | §2-1, §8-1 |
### 3-2. AI 요구사항 (REQ-A)
| REQ-ID | 요구사항 | 유형 | 출처 |
|---|---|---|---|
| REQ-A-001 | 나노바나나 시공 예상 사진 표준 샷세트(S1 정면·S2 야간·S3 통로·S4 내부·S5 Before/After·S7 홀전경) 생성 | 필수 | F019, §6-3 |
| REQ-A-002 | image-to-image 구조보존(참조이미지 inlineData+지시문) — 골격/치수/통로/카메라 앵글 잠금, 스타일만 사실화 | 필수 | §6-4 |
| REQ-A-003 | 구조화 프롬프트 사전(BOOTH_TYPES·BOOTH_STYLES·FIXTURE_LAYERS) 조립·보존/교체 명시 분리 | 필수 | §6-4 |
| REQ-A-004 | S6 배선 오버레이 백엔드 래스터 합성 우선(전기 적·네트워크 청·급배수 녹, 좌표 정확) | 필수 | §6-3 |
| REQ-A-005 | Before/After 드래그 슬라이더(빈 부스↔시공 후, ARIA·키보드) | 필수 | F020, §6-5 |
| REQ-A-006 | RenderJob 방어(다층 크기가드·mimeType 자동감지·SAFETY 분기·친화적 한글 에러·성공 시에만 쿼터 차감) | 필수 | §6-5 |
| REQ-A-007 | 단계별 로딩 UX(골격인식→집기→배선→렌더) aria-live 순환·비동기 큐·WebSocket 완료 푸시 | 필수 | §6-5, §8 |
| REQ-A-008 | 간판 텍스트 비전 오탈자 검수 + 한글 렌더 실패 시 간판영역 후처리 합성 | 필수 | §6-4, R5 |
| REQ-A-009 | 부스 배치 3안 AI 자동생성(제약 솔버 + LLM 조건해석, 결정적 알고리즘 중심) | 필수 | F008, §M2 |
| REQ-A-010 | AI 규정 자동검증(배치·설계 규정 위반 플래깅) | 필수 | F010·F013 |
| REQ-A-011 | AI 서류 검수(누락·배치도-계획서 불일치 검출) | 필수 | F023, §M6 |
| REQ-A-012 | 규정 자연어 챗봇(근거·인용 기반) | 선택 | F025 |
| REQ-A-013 | AI 수요예측·수율/동적가격 시뮬레이션 | 필수 | F090·F003, §M16-1⑥ |
| REQ-A-014 | AI 경영진 KPI 요약·인사이트 브리핑 자동, 자연어 조회(Text-to-SQL) | 필수/선택 | F093/F092 |
| REQ-A-015 | AI 카피·이미지 초안 생성, 다국어 콘텐츠 자동번역+검수 | 필수 | F066·F069, §M12·§M17 |
| REQ-A-016 | AI 플랫폼 라우터(Claude 기본→Ollama 폴백)·중앙경유·LLM 직접호출 신설 금지 | 필수 | F100, §6, FEATURE_BACKLOG §0 |
| REQ-A-017 | AI 답변 근거(RAG)·인용·환각 차단(abstain) | 필수 | FEATURE_BACKLOG §0 |
| REQ-A-018 | AI 리드 스코어링, 비즈매칭 추천, 세션/부스 추천 | 선택 | F051·F055·F059 |
| REQ-A-019 | 현장 이상탐지·혼잡/철거 대기열 예측 | 선택 | F074·F044·F079 |
| REQ-A-020 | 도면 비전 추출(치수·구조), OCR 문서 자동등록 | 필수 | F014·F026 |
### 3-3. 비기능 요구사항 (REQ-N)
| REQ-ID | 요구사항 | 유형 | 출처 |
|---|---|---|---|
| REQ-N-001 | 코어 인증·설계·조회 경로 가용성 SLO 99.5%(성수기 99.9% 지향) | 필수 | system.md §3-2 |
| REQ-N-002 | 백엔드 최소 2 인스턴스+헬스체크 LB·롤링 무중단 배포 | 필수 | system.md §3-2 |
| REQ-N-003 | PostgreSQL 프라이머리-스탠바이 스트리밍 복제+자동 failover+PITR 백업 | 필수 | system.md §3-2 |
| REQ-N-004 | Fail-Safe 배포(백업→배포→헬스체크 200→실패 시 롤백) | 필수 | system.md §3-2 |
| REQ-N-005 | degraded 모드(G1 미승인/Gemini 장애 시 워커 목 응답·구조·워터마크 유지, Claude 실패 시 Ollama 폴백) | 필수 | system.md §3-2 |
| REQ-N-006 | 나노바나나 생성 성능: 평균 40초 목표·1024px 다운스케일+JPEG 0.85 전처리 | 필수 | system.md §3-3, §6-5 |
| REQ-N-007 | 대량 동시 생성 제어: 자동생성 S1·S7 한정·동일 스키마해시 캐시·행사별 RenderJob 쿼터 | 필수 | system.md §3-3, R6 |
| REQ-N-008 | 공간쿼리 성능: 전 geom 컬럼 GiST 인덱스(트렌치 KNN·통로버퍼·비상구 교차 실시간) | 필수 | data.md §3-4 |
| REQ-N-009 | 워커 독립 스케일(큐 깊이 기반 증감), 백엔드 응답성과 분리 | 필수 | system.md §3-1 |
| REQ-N-010 | BI·공개조회·플로어플랜 열람 읽기복제 오프로드(운영 프라이머리 보호) | 필수 | system.md §3-1, §8-1 |
| REQ-N-011 | 워커 장애 시 가시성 타임아웃 후 재큐(멱등 Job) | 필수 | system.md §3-2 |
| REQ-N-012 | 웹접근성 WCAG 2.1 AA(핵심여정 AAA 지향)·공공 KWCAG 2.2 병행 | 필수 | FEATURE_BACKLOG §6-N1 |
| REQ-N-013 | 캔버스/맵 비시각 대안(데이터 테이블·검색·키보드 선택) 필수 제공 | 필수 | FEATURE_BACKLOG §6-N1 |
| REQ-N-014 | 다국어 i18n 외부화(한 기본+영/중/일)·로케일 전환·hreflang·로케일 포맷 | 필수 | FEATURE_BACKLOG §6-N3 |
| REQ-N-015 | 라이트/다크 테마 CSS 토큰 2벌·prefers-color-scheme 연동·선택 영속 | 필수 | FEATURE_BACKLOG §6-N4 |
| REQ-N-016 | 반응형(데스크톱=설계/에디터, 모바일=현장/조회/승인) | 필수 | PLANNING §8, §2-1 |
| REQ-N-017 | 단일 공간 원천(M2폴리곤·M4배선 매퍼 권위, 소비 모듈 조회만·중복 저장 금지) | 필수 | app.md, data.md |
| REQ-N-018 | 관측성(로그·메트릭·트레이스) 관리존 수집, 로그에 자격증명·PII·스택트레이스 미기록 | 필수 | network.md, tech.md |
| REQ-N-019 | 공개영역 CDN·요청률 상한·slowloris 타임아웃(가용성 계층 방어) | 필수 | network.md §7 |
| REQ-N-020 | tenant_id 선두 복합 인덱스 표준화(격리 쿼리 성능) | 필수 | §8-2 |
| REQ-N-021 | BI 데이터마트(스타 스키마 Fact/Dim) 야간 배치 ETL 또는 읽기복제 적재 | 필수 | data.md, §M16-1 |
### 3-4. 보안 요구사항 (REQ-S)
| REQ-ID | 요구사항 | 유형 | 출처 |
|---|---|---|---|
| REQ-S-001 | 외부 API 정책: 온프레미스 Ollama 기본, Claude(api.anthropic.com)·Gemini(나노바나나)만 승인 예외, 그 외 금지 | 필수 | SECURITY.md §1 |
| REQ-S-002 | GEMINI_API_KEY 나노바나나 Python 워커 env only(백엔드 미취급), Spring은 큐/콜백만 | 필수 | SECURITY.md §1 |
| REQ-S-003 | 자격증명·키·내부IP·SSH·워커토큰 응답/로그/에러/커밋 완전 제외·마스킹 | 필수 | SECURITY.md §2 |
| REQ-S-004 | admin 비밀번호 env `ADMIN_PASSWORD_ENC`(AES-256-GCM)+별도 키파일→기동 시 BCrypt 재시드, 하드코딩 시드 금지 | 필수 | SECURITY.md §3 |
| REQ-S-005 | JWT(HS256)+2차 인증 OTP/EMAIL(TOTP RFC6238)+로그인 실패 잠금(관리자 해제) | 필수 | SECURITY.md §4, §5B-3 |
| REQ-S-006 | 행사 단위 RBAC(열람=멤버/홀매니저, 편집=역할별 게이트) — 테넌트 격리 ⊃ 행사 RBAC 3중 | 필수 | SECURITY.md §4, §2 |
| REQ-S-007 | 미등록 장치업체 초대·응찰 차단(NOT_REGISTERED_COMPANY 403) | 필수 | SECURITY.md §4, §M15 |
| REQ-S-008 | `/api/admin/**`=ADMIN 게이트, `/api/internal/**`=워커 공유 시크릿(X-Worker-Token)만 | 필수 | SECURITY.md §4 |
| REQ-S-009 | M5 나노바나나 이미지 응답 항상 watermarkRequired:true+watermarkText+notice(계약·심사 서류 금지) | 필수 | SECURITY.md §5, §6-6 |
| REQ-S-010 | 오류 응답 스택트레이스·relation/컬럼명·내부경로 미노출(ApiResponse.error 요약만) | 필수 | SECURITY.md §6 |
| REQ-S-011 | DataAccessException @RestControllerAdvice로 INTERNAL 요약 매핑(테이블/컬럼 누출 차단) | 필수 | SECURITY.md §6, system.md |
| REQ-S-012 | 민감 액션(승인·낙찰·설계변경·룰셋개정·리드접근·로그인/권한변경) TB_AUDIT_LOG 전수(tenant_id 포함) | 필수 | SECURITY.md §7, §6-N2 |
| REQ-S-013 | PII(관람객·리드) 동의·보존정책·최소수집·마스킹·응답 완전 제외, `*_enc` AES-256-GCM | 필수 | data.md §5, R10 |
| REQ-S-014 | 입력 검증(Bean Validation 화이트리스트·파일 MIME/크기 가드) | 필수 | FEATURE_BACKLOG §6-N2 |
| REQ-S-015 | XSS 방지(React 이스케이프·dangerouslySetInnerHTML 금지·CMS sanitize·CSP) | 필수 | FEATURE_BACKLOG §6-N2 |
| REQ-S-016 | 인젝션 차단(MyBatis `#{}` 강제·`${}` 금지, PostGIS/명령/LDAP) | 필수 | FEATURE_BACKLOG §6-N2 |
| REQ-S-017 | IDOR 방지(리소스 소유·tenant_id 검증), CSRF/SameSite·JWT 만료·회전 | 필수 | FEATURE_BACKLOG §6-N2 |
| REQ-S-018 | fail-closed 다층 테넌트 격리(컨텍스트 필터→서비스 가드→MyBatis 공통 인터셉터→PostGIS) | 필수 | §8-2 |
| REQ-S-019 | 의존성 취약점 SCA(OWASP Dependency-Check) CI 게이트·정기 패치 | 필수 | FEATURE_BACKLOG §6-N2 |
| REQ-S-020 | 보안영역 분리(공개 DMZ 쓰기권한 없음·데이터존 직결 불가, DMZ↔내부 HTTPS만·DB 프로토콜 횡단 금지) | 필수 | network.md |
---
## 4. 평가 관점 (공공 SI 표준 배점 — 가정)
> 실제 RFP·평가표가 없어 공공 정보화 SI 통례를 가정한다. **[가정]** — 실제 공고 확인 시 교체.
| 구분 | 평가부문 | 배점(가정) | win theme 우선순위 근거 |
|---|---|---|---|
| 기술 | 사업이해·추진전략 | 10 | 전시 도메인·As-Is 병목 이해(§3) |
| 기술 | **AI 시각화·설계 자동화(핵심 차별화)** | **20** | M2~M5 P0 코어 + 나노바나나 시공 예측(경쟁 전시테크 미보유 폐루프) |
| 기술 | 기능 구현(M1~M18 전 모듈) | 15 | 100대 기능(이미 63·부분 18·신규 19) |
| 기술 | 아키텍처·기술스택 | 10 | 확정 스택 정합·PostGIS 공간데이터·Python 워커 사이드카 |
| 기술 | 보안·개인정보(SECURITY 불변) | 10 | REQ-S 20항목·워터마크·PII·테넌트 격리 |
| 기술 | 비기능(성능·가용성·접근성·다국어) | 8 | SLO 99.5%·WCAG AA·KWCAG·i18n |
| 기술 | 멀티테넌시·확장성(COEX 온보딩) | 5 | §1A SaaS 확장 |
| 기술 | 사업관리·품질·일정 | 2 | Phase 1~3·QA 게이트 |
| **기술 소계** | | **80** | |
| 가격 | 입찰가격 | 20 | |
| **총계** | | **100** | |
**win theme 우선순위(배점 가중)**: ① 나노바나나 시공 예측 시각화(최고 차별화, 20점) → ② 부스 배치·설계·배선 AI 자동화 폐루프(M2~M5) → ③ 공사 옥션(M15) 발주 종결 → ④ 보안·개인정보·워터마크(공공 신뢰) → ⑤ 멀티테넌트 SaaS 확장.
---
## 5. 제출 규격 (가정)
> 실제 RFP 규격이 없어 공공 SI 제안 통례를 가정한다. **[가정]** — 실제 공고 확인 시 교체.
| 항목 | 가정값 | 비고 |
|---|---|---|
| 제안서 형식 | PowerPoint(.pptx) 기술제안서 | proposal-builder 파이프라인 정합 |
| 분량 | 자율(권장 40~80p) | 실제 공고 확인 필요 |
| 목차 | 사업이해→추진전략→기술제안(AI 시각화·설계·옥션·관람·BI)→아키텍처→보안→비기능→멀티테넌시→사업관리→기대효과 | §4 배점 정합 |
| 서식 | 발주처 지정 시 준수, 미지정 시 표준 제안 템플릿 | ⚠확인필요 |
| 필수 도식 | 모듈맵(§4)·아키텍처(§8)·나노바나나 파이프라인(§6)·옥션 플로우(§M15)·추진체계·일정(Phase 1~3) | PLANNING 근거 |
| 제출 부수·마감 | ⚠확인필요(실제 공고) | — |
| 자격요건 | ⚠확인필요(실적·인증 — CSAP/ISMS 등 공공 요건 가능성) | — |
---
## 6. 확인 필요·상충 항목 (삭제 금지)
| # | 항목 | 사유 |
|---|---|---|
| C1 | 예산·계약방식·사업기간·평가배점·제출규격 | 실제 RFP 부재 — 전부 가정, 공고 확인 시 교체 |
| C2 | kxwp/kxfp API 연동 | 폐쇄형·API 미공개, 킨텍스 IT 협의 필수(R3) — 초기 수동 릴레이 |
| C3 | 트렌치·CAD 실측 데이터 | 미확보, 공개 스펙 기반 가정 그리드+'가정' 라벨(R4) |
| C4 | 인터넷 요금 정합성(150,000원 vs KT 80,000원 병존) | 킨텍스 확인 필요(R8) |
| C5 | 나노바나나 Gemini 외부 API 승인(G1) | 미승인 시 온프레미스 폴백 검토(R12) |
| C6 | 코엑스 등 신규 전시관 홀·요율·규정 실측 | 미확보, 온보딩 시 입력·근거 없는 추정 금지(§1A-5) |
| C7 | 사이니지 HW·비콘(wayfinding)·IoT 센서 연동 | 킨텍스 시설 인프라 협의 전제(M13·M14·M17) |
| C8 | 자격요건(공공 인증 CSAP/ISMS 등) | 실제 공고 확인 필요 |
---
## 부록. REQ 총계
| 유형 | 개수 |
|---|---|
| REQ-F (기능) | 81 |
| REQ-A (AI) | 20 |
| REQ-N (비기능) | 21 |
| REQ-S (보안) | 20 |
| **총계** | **142** |
</content>

View File

@ -1,196 +0,0 @@
# 요구사항 추적표 (RTM) — 킨텍스 자동전시시스템 제안서
> 작성: rfp-analyst · 작성일: 2026-07-11 · 버전 v1.0
> 짝 문서: `rfp_analysis.md`(요구사항 정의). 본 RTM은 **REQ-ID ↔ 제안서 목차 섹션 1:1 매핑 준비 + 평가부문 매핑 + 반영 상태**의 단일 진실원천이다.
> 사용법: writer/deck-designer가 각 REQ를 제안서 본문/슬라이드에 반영하며 "제안서 섹션" 및 "반영상태(☐미반영/◑작성중/☑완료)"를 갱신한다. QA는 전 REQ 반영 여부를 이 표로 교차 점검한다.
> 제안서 목차 코드: **P1**사업이해 · **P2**추진전략 · **P3**기술제안(AI 시각화·설계·옥션·관람·BI) · **P4**아키텍처 · **P5**보안·개인정보 · **P6**비기능 · **P7**멀티테넌시·확장 · **P8**사업관리·일정·조직 · **P9**기대효과. (평가부문은 rfp_analysis §4 배점 기준)
---
## 1. 기능 요구사항 (REQ-F)
| REQ-ID | 요구사항 요약 | 유형 | 모듈 | 제안서 섹션(준비) | 평가부문 | 반영상태 |
|---|---|---|---|---|---|---|
| REQ-F-001 | 홀/반홀 가용성 실시간 캘린더 | 필수 | M1 | P3 판매·견적 | 기능 구현 | ☐ |
| REQ-F-002 | 규칙엔진 자동 견적 | 필수 | M1 | P3 판매·견적 | 기능 구현 | ☐ |
| REQ-F-003 | 배정신청 웹폼→HWP 자동생성 | 필수 | M1 | P3 판매·견적 | 기능 구현 | ☐ |
| REQ-F-004 | 부스 판매 인벤토리 관리 | 필수 | M1/M2 | P3 판매·견적 | 기능 구현 | ☐ |
| REQ-F-005 | 온라인 부스 셀프 선택·판매 | 필수 | M2 | P3 판매·견적 | 기능 구현 | ☐ |
| REQ-F-006 | 임대계약 전자서명 | 선택 | M6/M9 | P3 판매·견적 | 기능 구현 | ☐ |
| REQ-F-007 | 부스 배치 3안 자동생성 | 필수 | M2 | P3 AI 시각화·설계 | AI 시각화·설계(핵심) | ☐ |
| REQ-F-008 | 배치안 선택/병합 편집 | 필수 | M2 | P3 AI 시각화·설계 | AI 시각화·설계(핵심) | ☐ |
| REQ-F-009 | 배치 규정 자동검증·병합 후 재검증 | 필수 | M2 | P3 AI 시각화·설계 | AI 시각화·설계(핵심) | ☐ |
| REQ-F-010 | 최종안 버전기록·부스좌표 확정·초대링크 | 필수 | M2 | P3 AI 시각화·설계 | AI 시각화·설계(핵심) | ☐ |
| REQ-F-011 | 조립부스 옵션·3D 프리뷰 | 필수 | M3 | P3 AI 시각화·설계 | AI 시각화·설계(핵심) | ☐ |
| REQ-F-012 | 독립부스 설계 3안·선택/병합·버전 | 필수 | M3 | P3 AI 시각화·설계 | AI 시각화·설계(핵심) | ☐ |
| REQ-F-013 | 설계 규정 사전검증(높이·리깅·방염·이격) | 필수 | M3 | P3 AI 시각화·설계 | AI 시각화·설계(핵심) | ☐ |
| REQ-F-014 | 도면 업로드 비전 추출·검증 | 필수 | M3 | P3 AI 시각화·설계 | AI 시각화·설계(핵심) | ☐ |
| REQ-F-015 | 전기 용량·분전반 자동산출 | 필수 | M4a | P3 AI 시각화·설계 | AI 시각화·설계(핵심) | ☐ |
| REQ-F-016 | 배선 경로 자동생성(PostGIS 최단) | 필수 | M4 | P3 AI 시각화·설계 | AI 시각화·설계(핵심) | ☐ |
| REQ-F-017 | 위치표시도 자동생성 | 필수 | M4b | P3 AI 시각화·설계 | AI 시각화·설계(핵심) | ☐ |
| REQ-F-018 | 네트워크·급배수·압축공기 신청·마감 리마인더 | 필수 | M4b | P3 AI 시각화·설계 | 기능 구현 | ☐ |
| REQ-F-019 | 조명 조도 배치안 제안 | 선택 | M4a | P3 AI 시각화·설계 | AI 시각화·설계 | ☐ |
| REQ-F-020 | 마일스톤 자동생성·역산 알림 | 필수 | M6 | P3 서류·워크플로 | 기능 구현 | ☐ |
| REQ-F-021 | 신고서류 웹폼→HWP/PDF 자동생성 | 필수 | M6 | P3 서류·워크플로 | 기능 구현 | ☐ |
| REQ-F-022 | kxwp 제출 파일 릴레이 ⚠확인필요 | 필수 | M6 | P3 서류·워크플로 | 기능 구현 | ☐ |
| REQ-F-023 | 서류 버전·전자결재 워크플로 | 필수 | M6/§5B | P3 서류·워크플로 | 기능 구현 | ☐ |
| REQ-F-024 | OCR 계약·납품서 자동등록 | 필수 | AI | P3 서류·워크플로 | 기능 구현 | ☐ |
| REQ-F-025 | 역경매 옥션 개설·라운드·마감 | 필수 | M15 | P3 공사 옥션 | AI 시각화·설계(폐루프) | ☐ |
| REQ-F-026 | AI 자료 기반 견적서 응찰·PDF·버전 | 필수 | M15 | P3 공사 옥션 | AI 시각화·설계(폐루프) | ☐ |
| REQ-F-027 | 실시간 순위·자동마감 | 필수 | M15 | P3 공사 옥션 | 기능 구현 | ☐ |
| REQ-F-028 | 종합평가 낙찰 스코어·비교표 | 필수 | M15 | P3 공사 옥션 | 기능 구현 | ☐ |
| REQ-F-029 | 등록업체 검증 게이트(응찰 차단) | 필수 | M15/M7 | P3 공사 옥션 / P5 보안 | 보안·개인정보 | ☐ |
| REQ-F-030 | 등록업체 AI 매칭 추천 | 필수 | M7 | P3 공사 옥션 | 기능 구현 | ☐ |
| REQ-F-031 | 물량서(BOQ) 자동산출 | 필수 | M4/M15 | P3 공사 옥션 | AI 시각화·설계 | ☐ |
| REQ-F-032 | 낙찰→계약·발주 자동전환 | 필수 | M15 | P3 공사 옥션 | 기능 구현 | ☐ |
| REQ-F-033 | 반입/반출 슬롯 예약 | 선택 | M8 | P3 참가·물류 | 기능 구현 | ☐ |
| REQ-F-034 | 통행증 QR·중량물 우선 | 선택 | M8 | P3 참가·물류 | 기능 구현 | ☐ |
| REQ-F-035 | 지게차·부대장비·렌탈 주문 | 선택 | M8 | P3 참가·물류 | 기능 구현 | ☐ |
| REQ-F-036 | 철거 대기열 시뮬레이션 | 선택 | M8 | P3 참가·물류 | AI 시각화·설계 | ☐ |
| REQ-F-037 | 온라인 사전등록(유형별·중복검증) | 필수 | M10 | P3 관람 | 기능 구현 | ☐ |
| REQ-F-038 | 모바일 배지/QR 발급 | 필수 | M10 | P3 관람 | 기능 구현 | ☐ |
| REQ-F-039 | 현장 QR 체크인·즉석 인쇄·집계 | 필수 | M10 | P3 관람 | 기능 구현 | ☐ |
| REQ-F-040 | 오프라인 체크인 폴백 | 필수 | M10 | P3 관람 / P6 비기능 | 비기능 | ☐ |
| REQ-F-041 | 참가업체 리드캡처 | 필수 | M10 | P3 관람 | 기능 구현 | ☐ |
| REQ-F-042 | AI 리드 스코어링·분류 | 선택 | AI | P3 관람 | AI 시각화·설계 | ☐ |
| REQ-F-043 | 리드 팔로업 세그먼트 EDM | 필수 | M10/M12 | P3 관람 | 기능 구현 | ☐ |
| REQ-F-044 | 티켓발권·AI폼빌더·스마트배지 | 선택 | M10 | P3 관람 | 기능 구현 | ☐ |
| REQ-F-045 | AI 비즈매칭 추천 | 선택 | M11 | P3 관람 | AI 시각화·설계 | ☐ |
| REQ-F-046 | 미팅 슬롯·성과 리포트 | 선택 | M11 | P3 관람 | 기능 구현 | ☐ |
| REQ-F-047 | 이벤트 모바일앱·세션·채팅·설문 | 선택 | M11 | P3 관람 | 기능 구현 | ☐ |
| REQ-F-048 | 공개 홍보 사이트(SEO·다국어) | 필수 | M12 | P3 마케팅·공개 | 기능 구현 | ☐ |
| REQ-F-049 | 공개 인터랙티브 플로어플랜 | 필수 | M12 | P3 마케팅·공개 | 기능 구현 | ☐ |
| REQ-F-050 | 세그먼트 EDM·캠페인 자동화 | 필수 | M12 | P3 마케팅·공개 | 기능 구현 | ☐ |
| REQ-F-051 | 스폰서십 패키지·대시보드 | 필수 | M12/M16 | P3 마케팅·공개 | 기능 구현 | ☐ |
| REQ-F-052 | 실내 wayfinding·POI·경로 | 선택 | M13 | P3 현장운영 | 기능 구현 | ☐ |
| REQ-F-053 | 실시간 혼잡 모니터·예측 | 선택 | M14 | P3 현장운영 | AI 시각화·설계 | ☐ |
| REQ-F-054 | 홀 전력부하·주차 점유 | 선택 | M14 | P3 현장운영 | 기능 구현 | ☐ |
| REQ-F-055 | 안전 위반·이상탐지 알림 | 선택 | M14 | P3 현장운영 | AI 시각화·설계 | ☐ |
| REQ-F-056 | 납부 스케줄 자동생성·알림 | 필수 | M9 | P3 정산 | 기능 구현 | ☐ |
| REQ-F-057 | 유틸리티·부대 PG 결제 | 필수 | M9 | P3 정산 | 기능 구현 | ☐ |
| REQ-F-058 | 예치금 실사용 정산 투명화 | 필수 | M9 | P3 정산 | 기능 구현 | ☐ |
| REQ-F-059 | 세금계산서·옥션 수수료 정산 | 필수 | M9 | P3 정산 | 기능 구현 | ☐ |
| REQ-F-060 | 환불·취소 규정 자동 | 선택 | M9 | P3 정산 | 기능 구현 | ☐ |
| REQ-F-061 | 홀·기간별 가동률 대시보드 | 필수 | M16 | P3 경영 BI | 기능 구현 | ☐ |
| REQ-F-062 | 매출 구성·행사별 P&L | 필수 | M16 | P3 경영 BI | 기능 구현 | ☐ |
| REQ-F-063 | 전시장 ROI·RevPAD·㎡당 수익 | 필수 | M16 | P3 경영 BI | 기능 구현 | ☐ |
| REQ-F-064 | 참가사 리텐션·LTV 코호트 | 필수 | M16 | P3 경영 BI | 기능 구현 | ☐ |
| REQ-F-065 | 참가업체 관점 ROI(권한 격리) | 필수 | M16 | P3 경영 BI / P5 보안 | 기능 구현 | ☐ |
| REQ-F-066 | 경영진 KPI 대시보드·드릴다운 | 필수 | M16 | P3 경영 BI | 기능 구현 | ☐ |
| REQ-F-067 | CMS 게시 워크플로·버전 | 필수 | M17 | P3 마케팅·공개 | 기능 구현 | ☐ |
| REQ-F-068 | 참가업체 마이크로사이트 | 필수 | M17 | P3 마케팅·공개 | 기능 구현 | ☐ |
| REQ-F-069 | 다국어 콘텐츠·hreflang | 필수 | M17 | P3 마케팅·공개 / P6 | 비기능 | ☐ |
| REQ-F-070 | 사이니지 콘텐츠 배포 ⚠확인필요 | 선택 | M17 | P3 현장운영 | 기능 구현 | ☐ |
| REQ-F-071 | RBAC·감사·시스템설정 백오피스 | 필수 | M18/§5B | P5 보안 / P8 관리 | 보안·개인정보 | ☐ |
| REQ-F-072 | 룰셋·마스터데이터 버전관리 | 필수 | M18 | P4 아키텍처 / P8 | 아키텍처 | ☐ |
| REQ-F-073 | 공통코드·메뉴 관리 | 필수 | §5B | P8 관리 | 기능 구현 | ☐ |
| REQ-F-074 | 공통 업무모듈(일지·일정·쪽지·공지·검색) | 필수 | §5B | P8 관리 | 기능 구현 | ☐ |
| REQ-F-075 | 회의록(STT)·업무보고 통계 PDF | 필수 | §5B | P8 관리 | 기능 구현 | ☐ |
| REQ-F-076 | 통합 알림센터(WebSocket) | 필수 | §5B | P8 관리 | 기능 구현 | ☐ |
| REQ-F-077 | 멀티테넌트 격리·전시관 온보딩 | 필수 | §1A | P7 멀티테넌시 | 멀티테넌시·확장 | ☐ |
| REQ-F-078 | 테넌트 컨텍스트 해소 | 필수 | §1A | P7 멀티테넌시 | 멀티테넌시·확장 | ☐ |
| REQ-F-079 | 2계층 관리자(플랫폼/테넌트) | 필수 | §1A | P7 멀티테넌시 / P5 | 멀티테넌시·확장 | ☐ |
| REQ-F-080 | API·웹훅·CRM/ERP 연동 게이트웨이 | 선택 | 연동 | P4 아키텍처 | 아키텍처 | ☐ |
| REQ-F-081 | 역할별 프론트 포털 분리 | 필수 | §2-1 | P4 아키텍처 | 아키텍처 | ☐ |
## 2. AI 요구사항 (REQ-A)
| REQ-ID | 요구사항 요약 | 유형 | 모듈 | 제안서 섹션(준비) | 평가부문 | 반영상태 |
|---|---|---|---|---|---|---|
| REQ-A-001 | 나노바나나 시공 예상사진 샷세트 S1~S7 | 필수 | M5 | P3 AI 시각화(핵심) | AI 시각화·설계(핵심) | ☐ |
| REQ-A-002 | image-to-image 구조보존(골격/앵글 잠금) | 필수 | M5 | P3 AI 시각화(핵심) | AI 시각화·설계(핵심) | ☐ |
| REQ-A-003 | 구조화 프롬프트 사전·보존/교체 분리 | 필수 | M5 | P3 AI 시각화(핵심) | AI 시각화·설계(핵심) | ☐ |
| REQ-A-004 | S6 배선 오버레이 백엔드 래스터 합성 | 필수 | M5 | P3 AI 시각화(핵심) | AI 시각화·설계(핵심) | ☐ |
| REQ-A-005 | Before/After 드래그 슬라이더 | 필수 | M5 | P3 AI 시각화(핵심) | AI 시각화·설계(핵심) | ☐ |
| REQ-A-006 | RenderJob 방어(크기가드·SAFETY·쿼터) | 필수 | M5 | P3 AI 시각화 / P5 보안 | 보안·개인정보 | ☐ |
| REQ-A-007 | 단계별 로딩·비동기 큐·WebSocket 푸시 | 필수 | M5 | P4 아키텍처 | 아키텍처 | ☐ |
| REQ-A-008 | 간판 텍스트 비전 검수·후처리 합성 | 필수 | M5 | P3 AI 시각화 | AI 시각화·설계 | ☐ |
| REQ-A-009 | 부스 배치 3안 AI(제약 솔버+LLM) | 필수 | M2 | P3 AI 시각화·설계 | AI 시각화·설계(핵심) | ☐ |
| REQ-A-010 | AI 규정 자동검증(배치·설계) | 필수 | M2/M3 | P3 AI 시각화·설계 | AI 시각화·설계 | ☐ |
| REQ-A-011 | AI 서류 검수(누락·불일치) | 필수 | M6 | P3 서류·워크플로 | AI 시각화·설계 | ☐ |
| REQ-A-012 | 규정 자연어 챗봇(근거·인용) | 선택 | AI | P3 서류·워크플로 | AI 시각화·설계 | ☐ |
| REQ-A-013 | AI 수요예측·수율/동적가격 | 필수 | M16 | P3 경영 BI | AI 시각화·설계 | ☐ |
| REQ-A-014 | AI KPI 브리핑·자연어 조회(Text-to-SQL) | 필수/선택 | M16 | P3 경영 BI | AI 시각화·설계 | ☐ |
| REQ-A-015 | AI 카피·이미지 초안·다국어 번역 | 필수 | M12/M17 | P3 마케팅·공개 | AI 시각화·설계 | ☐ |
| REQ-A-016 | AI 플랫폼 라우터(Claude→Ollama 폴백) | 필수 | §6 | P4 아키텍처 / P5 보안 | 아키텍처 | ☐ |
| REQ-A-017 | AI 근거(RAG)·인용·환각 차단 | 필수 | §6 | P5 보안 | 보안·개인정보 | ☐ |
| REQ-A-018 | AI 리드 스코어·매칭·추천 | 선택 | M10/M11 | P3 관람 | AI 시각화·설계 | ☐ |
| REQ-A-019 | 현장 이상탐지·혼잡/철거 예측 | 선택 | M14/M8 | P3 현장운영 | AI 시각화·설계 | ☐ |
| REQ-A-020 | 도면 비전 추출·OCR 문서 등록 | 필수 | M3/AI | P3 AI 시각화·설계 | AI 시각화·설계 | ☐ |
## 3. 비기능 요구사항 (REQ-N)
| REQ-ID | 요구사항 요약 | 유형 | 제안서 섹션(준비) | 평가부문 | 반영상태 |
|---|---|---|---|---|---|
| REQ-N-001 | 코어 가용성 SLO 99.5%(성수기 99.9%) | 필수 | P6 비기능 | 비기능 | ☐ |
| REQ-N-002 | 백엔드 2인스턴스·롤링 무중단 | 필수 | P4 아키텍처 / P6 | 비기능 | ☐ |
| REQ-N-003 | PG 스탠바이 복제·failover·PITR | 필수 | P4 아키텍처 / P6 | 비기능 | ☐ |
| REQ-N-004 | Fail-Safe 배포(헬스체크·롤백) | 필수 | P8 사업관리 | 사업관리·품질 | ☐ |
| REQ-N-005 | degraded 모드(목 응답·폴백) | 필수 | P4 아키텍처 / P6 | 비기능 | ☐ |
| REQ-N-006 | 나노바나나 40초·1024px 전처리 | 필수 | P6 비기능 | 비기능 | ☐ |
| REQ-N-007 | 대량 생성 제어(S1·S7·캐시·쿼터) | 필수 | P6 비기능 | 비기능 | ☐ |
| REQ-N-008 | 공간쿼리 GiST 실시간 | 필수 | P4 아키텍처 | 아키텍처 | ☐ |
| REQ-N-009 | 워커 독립 스케일 | 필수 | P4 아키텍처 | 아키텍처 | ☐ |
| REQ-N-010 | BI/공개 읽기복제 오프로드 | 필수 | P4 아키텍처 | 아키텍처 | ☐ |
| REQ-N-011 | 워커 장애 재큐(멱등 Job) | 필수 | P4 아키텍처 / P6 | 비기능 | ☐ |
| REQ-N-012 | WCAG 2.1 AA·KWCAG 2.2 | 필수 | P6 비기능 | 비기능 | ☐ |
| REQ-N-013 | 캔버스/맵 비시각 대안 | 필수 | P6 비기능 | 비기능 | ☐ |
| REQ-N-014 | 다국어 i18n·hreflang·로케일 포맷 | 필수 | P6 비기능 | 비기능 | ☐ |
| REQ-N-015 | 라이트/다크 테마 CSS 토큰 | 필수 | P6 비기능 | 비기능 | ☐ |
| REQ-N-016 | 반응형(데스크톱/모바일) | 필수 | P6 비기능 | 비기능 | ☐ |
| REQ-N-017 | 단일 공간 원천(중복 저장 금지) | 필수 | P4 아키텍처 | 아키텍처 | ☐ |
| REQ-N-018 | 관측성(로그·PII/스택트레이스 미기록) | 필수 | P5 보안 / P6 | 비기능 | ☐ |
| REQ-N-019 | 공개영역 CDN·요청률 상한 | 필수 | P4 아키텍처 / P6 | 비기능 | ☐ |
| REQ-N-020 | tenant_id 선두 복합 인덱스 | 필수 | P7 멀티테넌시 / P4 | 멀티테넌시·확장 | ☐ |
| REQ-N-021 | BI 데이터마트 배치 ETL/읽기복제 | 필수 | P4 아키텍처 | 아키텍처 | ☐ |
## 4. 보안 요구사항 (REQ-S)
| REQ-ID | 요구사항 요약 | 유형 | 제안서 섹션(준비) | 평가부문 | 반영상태 |
|---|---|---|---|---|---|
| REQ-S-001 | 외부 API 정책(Ollama 기본·Claude/Gemini 예외) | 필수 | P5 보안 | 보안·개인정보 | ☐ |
| REQ-S-002 | GEMINI_API_KEY 워커 env only | 필수 | P5 보안 | 보안·개인정보 | ☐ |
| REQ-S-003 | 자격증명·키 응답/로그/커밋 완전 제외 | 필수 | P5 보안 | 보안·개인정보 | ☐ |
| REQ-S-004 | admin 비번 env 재시드(AES-256-GCM) | 필수 | P5 보안 | 보안·개인정보 | ☐ |
| REQ-S-005 | JWT+2FA OTP(TOTP)·실패 잠금 | 필수 | P5 보안 | 보안·개인정보 | ☐ |
| REQ-S-006 | 행사 RBAC(테넌트 격리 ⊃ 행사 3중) | 필수 | P5 보안 / P7 | 보안·개인정보 | ☐ |
| REQ-S-007 | 미등록 업체 차단(403) | 필수 | P5 보안 / P3 옥션 | 보안·개인정보 | ☐ |
| REQ-S-008 | admin/internal 엔드포인트 게이트 | 필수 | P5 보안 | 보안·개인정보 | ☐ |
| REQ-S-009 | M5 이미지 워터마크 항상 포함 | 필수 | P5 보안 / P3 AI | 보안·개인정보 | ☐ |
| REQ-S-010 | 오류 응답 스택트레이스 미노출 | 필수 | P5 보안 | 보안·개인정보 | ☐ |
| REQ-S-011 | DataAccessException 요약 매핑 | 필수 | P5 보안 | 보안·개인정보 | ☐ |
| REQ-S-012 | 민감 액션 감사로그 전수(tenant_id) | 필수 | P5 보안 | 보안·개인정보 | ☐ |
| REQ-S-013 | PII 동의·보존·마스킹·AES-256-GCM | 필수 | P5 보안 | 보안·개인정보 | ☐ |
| REQ-S-014 | 입력 검증(Bean Validation·파일 가드) | 필수 | P5 보안 | 보안·개인정보 | ☐ |
| REQ-S-015 | XSS 방지·CSP·CMS sanitize | 필수 | P5 보안 | 보안·개인정보 | ☐ |
| REQ-S-016 | 인젝션 차단(MyBatis #{} 강제) | 필수 | P5 보안 | 보안·개인정보 | ☐ |
| REQ-S-017 | IDOR 방지·CSRF·JWT 회전 | 필수 | P5 보안 | 보안·개인정보 | ☐ |
| REQ-S-018 | fail-closed 다층 테넌트 격리 | 필수 | P7 멀티테넌시 / P5 | 멀티테넌시·확장 | ☐ |
| REQ-S-019 | 의존성 SCA CI 게이트 | 필수 | P5 보안 / P8 | 사업관리·품질 | ☐ |
| REQ-S-020 | 보안영역 분리(DMZ/내부망) | 필수 | P4 아키텍처 / P5 | 보안·개인정보 | ☐ |
---
## 5. 커버리지 요약
| 유형 | REQ 수 | 필수 | 선택 |
|---|---|---|---|
| REQ-F (기능) | 81 | 62 | 19 |
| REQ-A (AI) | 20 | 15 | 5 |
| REQ-N (비기능) | 21 | 21 | 0 |
| REQ-S (보안) | 20 | 20 | 0 |
| **총계** | **142** | **118** | **24** |
**제안서 섹션 커버리지 목표**: P3(기술제안)에 최다 REQ 집중(AI 시각화·설계 win theme), P5(보안)에 REQ-S 20 + 관련 REQ-A/F, P4(아키텍처)에 REQ-N/A 인프라 항목, P7(멀티테넌시)에 §1A 계열. QA는 전 142개 REQ의 반영상태가 ☑ 완료되었는지 본 표로 최종 검증한다.
## 6. 확인 필요 REQ (제안 전 발주처 협의)
| REQ-ID | 확인 사항 | rfp_analysis 참조 |
|---|---|---|
| REQ-F-022 | kxwp API 연동(폐쇄형·미공개) | C2 |
| REQ-F-070 | 사이니지 HW 연동 | C7 |
| REQ-F-052/053/054/055 | wayfinding 비콘·IoT 센서 인프라 | C7 |
| REQ-A-001~008 | 나노바나나 Gemini 승인(G1) — 미승인 시 온프레미스 폴백 | C5 |
| REQ-F-002 | 인터넷 요금 정합성·요율 공식본 | C4 |
| REQ-F-077 | 코엑스 등 신규 전시관 실측 데이터 | C6 |
</content>

View File

@ -1,374 +0,0 @@
# 킨텍스 자동전시시스템 구축 — 기술·아키텍처 자문서 (Tech Advisory)
> 작성: proposal-tech-advisor(아키텍처 허브) · 작성일: 2026-07-11 · 버전 v1.0
> 입력: `rfp_analysis.md`(REQ 142)·`rtm.md`·`proposal_strategy.md`+`proposal_outline.md`(Win Theme 5·필수 도식 12종)·**`docs/architecture/{app,system,tech,data,network}.md`**(AA/SA/TA/DA/NA 확정 아키텍처 5종)·`docs/PLANNING.md` v3.0·`docs/SECURITY.md`·`docs/WORK_STATUS.md`(라이브 실적)·kintex `CLAUDE.md`(스택 확정).
> **성격**: 아키텍처 전문가 풀(AA·SA·TA·DA·NA)이 이미 확정한 5종 아키텍처를 **재발명하지 않고**, 제안서(writer·visual-designer·deck-designer)용으로 **종합·상충 조정·재구성**한 기술 자문. 전문가 풀 산출물이 이미 충분해 추가 소집(kintex-sa/ta/da) 불요 — 공백(SM 운영·SLA·WBS/공수·도식 사양)만 본 문서가 저술.
> **원칙(불변)**: ①과장 금지 — WORK_STATUS 라이브 확인분만 "구축 완료/검증됨", 나머지는 "구현 계획". ②시크릿(키·비번·내부IP) 미기재. ③실현 가능성 우선 — 확정 스택·기존 스캐폴드·이식 자산으로 뒷받침.
---
## 0. 기술 신뢰성 기반 — 라이브 실적 vs 구현 계획 (과장 방지 핵심)
> 평가위원 최대 관심 = "제안이 말뿐인가, 실제로 되는가". 본 절이 그 답이다. WORK_STATUS(2026-07-11) 라이브 확인분과 구현 계획을 **명확히 구분**한다. 제안서 P8(사업관리)·P4(아키텍처) 신뢰 근거로 인용.
### 0-1. 구축 완료·라이브 (검증됨 — "이미 돈다")
| 영역 | 라이브 실적 | 검증 근거 |
|---|---|---|
| **개발/배포 인프라** | `kintex.zioinfo.co.kr` TLS + 백엔드 8021 + PostGIS + Redis + 나노바나나 워커 systemd 상주 | WORK_STATUS §4 인프라(라이브) |
| **CI/CD 자동배포** | Gitea webhook → `deploy_kintex.sh`(bootJar+vite build→배포→health→롤백). push 시 자동배포 | WORK_STATUS §2·§7 |
| **나노바나나 이미지 생성** | Gemini 라이브(G1 소유자 승인 완료), `gemini-3.1-flash-image-preview` 워커 | WORK_STATUS §2·§4 |
| **인증(JWT/RBAC/2FA)** | JWT+RBAC 6역할 + TOTP OTP + 로그인 실패 잠금 + admin env 시더(admin123 금지) | WORK_STATUS §4, Flyway V7/V8 |
| **P0 코어 매퍼(PostGIS)** | M2~M5 매퍼 배선(501 해소)·룰엔진·공간 SQL | WORK_STATUS §4 백엔드(라이브) |
| **공통 레이어(WISE 이식)** | 시스템관리·공통 업무기능·공개 인증(register/forgot/reset) | Flyway V1~V9(테이블 39) |
| **프론트(Stitch 반영)** | 로그인(히어로+CI+회원가입/비번찾기/아이디기억) + 부스 설계 스튜디오·대시보드·갤러리·신규 8+5화면 | WORK_STATUS §4·§6 |
| **평면도/CAD 자산** | 홀별 평면도 JPG 15 + CAD(제1전시장 "평면,트렌치.dwg") 확보 → 트렌치 실측 원천 | WORK_STATUS §4, CLAUDE.md 확보자산 |
**메시지**: "제안 아키텍처는 도상이 아니라 **이미 기동 중인 시스템**이다. 스캐폴드·인증·PostGIS 코어·나노바나나·자동배포 파이프라인이 라이브로 검증됐다." (전시테크/범용 SI가 제시하지 못하는 실증 우위)
### 0-2. 구현 계획 (설계 확정·미완 — "표준대로 채운다")
| 영역 | 상태 | 근거 |
|---|---|---|
| 도메인 모듈 M15 옥션·M10 관람·M12/17 CMS·M16 BI·M18 관리자 | 설계 확정, 구현 대기(Phase D) | IMPLEMENTATION_BACKLOG Phase D |
| 멀티테넌시(tenant_id 전파·컨텍스트 해소) | 데이터모델 확정, 스키마·컨텍스트 구현 대기 | WORK_STATUS §6, data.md §1A |
| 실데이터 집계 전환(신규 5화면 샘플→API) | 화면 이식 완료, 집계 API 미구축 | WORK_STATUS §6, UNDEVELOPED_BACKLOG §1 |
| 접근성/i18n/테마 전면 적용 | NFR 표준 확정, 전면 스윕 대기 | WORK_STATUS §6 |
| 역할별 프론트 번들 분리 | 방식 확정 대기(모노레포 vs 서브패스) | tech.md §9-5 |
| AiTextRouter/AiConfig(텍스트 AI) | 설계 확정(UIWS 패턴), 착수 대기 | tech.md §6-1·R-T7 |
**원칙**: 위 항목은 제안서에서 **"구현 계획"**으로만 서술. "구현 완료"로 오기 금지(감점/신뢰 훼손 리스크).
---
## 1. 목표 아키텍처 종합 (5종 아키텍처 → 제안서 재구성)
### 1-1. 목표 아키텍처 4-Tier + 사이드카 (한 문장)
**React 18/19(Vite·TS) 역할별 분리 프론트 → SSO(JWT+2FA)·이중 RBAC → 공유 Spring Boot 3.2.5(Java 17)+MyBatis 모듈러 모놀리스 → PostgreSQL+PostGIS 단일 공간 원천(+읽기복제) / Redis 비동기 큐 / 오브젝트 스토리지**, 그 위에 **나노바나나 Python 워커 사이드카**(이미지 생성)와 **Claude AI(AiTextRouter·Ollama 폴백)**(텍스트 지능)를 **얇은 큐/HTTP 계약으로 결합**한다. (app.md §1·§10, system.md §1~§2, tech.md §1)
### 1-2. 계층별 종합 (전문가 풀 산출 통합)
| 계층 | 구성(확정) | 권위 문서 | 핵심 근거 |
|---|---|---|---|
| **표현(App)** | 역할별 6프론트(organizer·exhibitor·contractor·ops·admin·public/visitor) 번들 분리 + 공유 디자인시스템/컴포넌트/api-client 상속. 데스크톱=설계·에디터, 모바일=현장·조회·승인 | app.md §10, system.md §2-1 | 최소권한·공격면 축소, 중복구현 금지 |
| **인증·인가** | 단일 JWT SSO(HS256, jjwt 0.12.5) + TOTP 2FA(RFC6238) + 로그인 실패 잠금 + **이중 RBAC**(플랫폼 `plat`/행사 `roles·hm`) | app.md §6, tech.md §7 | WISE/UIWS 이식(재설계 금지) |
| **애플리케이션(백엔드)** | 공유 Spring Boot **모듈러 모놀리스**(`com.zioinfo.kintex.module.mN`, 4계층 Controller/Service/Mapper/DTO) + 룰엔진 + 배치/배선 엔진(PostGIS) + 옥션·BI·CMS 서비스 | app.md §1~§5·§10-2 | 단일 공유 백엔드·경로/RBAC로 역할 표면 분리 |
| **비동기** | Redis 큐(RenderJob·서류·EDM·알림) + 옥션 실시간 순위/타이머 + 캐시/쿼터/WS 세션 상관. STOMP over WebSocket 실시간 푸시 | app.md §8·§9, system.md §6-3 | 백엔드 무상태화 → 수평 확장 |
| **AI(이미지)** | 나노바나나 Python 워커(google-genai, `gemini-3.1-flash-image-preview`) — image-to-image 구조보존 + 워터마크 후처리 + S6 로컬 PIL 래스터 | tech.md §1-3, system.md ADR-1 | google-genai=Python SDK, Java 재구현 회피 |
| **AI(텍스트)** | `AiTextRouter`: Claude 기본(`api.anthropic.com` 승인 예외) → **Ollama 폴백**(폐쇄망/장애). 부스 배치는 **제약 솔버 주도**, LLM은 조건해석만 | tech.md §6 | 결정론 우선·환각 차단·데이터 주권 |
| **데이터** | PostgreSQL+PostGIS 단일 공간 원천(부스 Polygon·트렌치 Point·배선 LineString, SRID 0) + 프라이머리-스탠바이 복제 + 읽기복제(BI·공개 오프로드) + BI 데이터마트(스타 스키마) | data.md §2~§6 | 단일 공간 원천 불변·운영/분석 분리 |
| **저장(바이너리)** | 오브젝트 스토리지(도면·생성이미지·서식·PDF·콘텐츠) — 경로만 DB, CDN 프론팅 | data.md §0-1, system.md §2-1 | 대용량 독립 확장 |
| **네트워크·보안영역** | 4존+관리존(엣지·DMZ·내부망·AI워커·데이터·관리) 계층 방어, DMZ→데이터존 직결 금지, egress 화이트리스트(Claude·Gemini·PG·SMTP) | network.md §2~§5 | 공개/내부 물리·논리 분리·폐쇄망 지향 |
### 1-3. 멀티테넌시 격리 구조 (다중 전시관 SaaS)
- **격리 축**: `tenant_id`(전시관: KINTEX #1·COEX #2…) ⊃ `event_id`(행사) ⊃ 행사 RBAC(역할). 3중 격리.
- **fail-closed 다층 방어**(REQ-S-018): ①테넌트 컨텍스트 필터(서브도메인 우선+사용자소속 보조, 불일치 거부) → ②서비스 가드 → ③MyBatis 공통 인터셉터(tenant_id 자동 주입) → ④PostGIS/쿼리(선두 복합 인덱스). 어느 한 층이라도 불일치면 차단.
- **온보딩**: 코드 배포 없이 **데이터 온보딩**(홀·요율·규정 룰셋·마스터데이터 입력). 기능 로직 불변. (app.md §6, data.md §4-4, system.md §3-1 멀티테넌시)
- **주의(현실)**: 멀티테넌시는 **구현 계획**(WORK_STATUS §6). 제안 시 "데이터모델 확정→스키마 tenant_id→컨텍스트 배선" 3단계 로드맵으로 서술, 백필 전 단일테넌트 무중단 동작→검증 후 필터 활성화(회귀 방지).
### 1-4. 필수 도식 12종 — 작도 사양 (deck-designer 직접 작도용)
> 각 도식의 **박스·연결·라벨 텍스트**를 명세. visual-designer(선 SVG 아이콘)·deck-designer(python-pptx)가 재해석 없이 작도. 모든 라벨은 한국어, 아이콘은 선(stroke) 스타일.
#### ① 3중 해자 벤다이어그램 (S04·S12)
- **형태**: 3원 교집합 벤다이어그램. 3개 동일 크기 원 60% 겹침.
- **원 라벨**: 원A "나노바나나 시공 예측 이미지"(부제: image-to-image 구조보존·40초·S1~S7), 원B "PostGIS 실측 공간데이터"(부제: 부스 폴리곤·트렌치 포인트·배선 LineString 단일 원천), 원C "공사 옥션 폐루프"(부제: AI 자료→응찰→낙찰→발주 자동전환).
- **중앙 교집합(3원 겹침)**: 강조 배지 "재현 난이도 높은 결합 = 우리만의 해자(Moat)". 2원 교집합에는 회색 "단일 요소는 모방 가능".
- **하단 캡션**: "셋의 결합은 경쟁 전시테크·범용 SI가 재현하기 어렵다."
#### ② 모듈맵 — 생애주기 × 우선순위 매트릭스 (S16)
- **형태**: 가로축=전시 생애주기 6단계(판매·기획 → 설계·시각화 → 발주·계약 → 참가·관람 → 현장운영 → 사후·경영), 세로축=우선순위(P0 코어 / P1 / P2).
- **박스(모듈 칩)**: P0행에 **M2 플로어플랜·M3 부스설계·M4 유틸리티·M5 나노바나나**(강조색·굵은 테두리). P1행 M1·M6·M7·M9·M15(옥션 강조)·M10·M12·M16·M17·M18·§5B 공통·§1A 멀티테넌시. P2행 M8·M11·M13·M14.
- **하단 레이어 띠**: "공통/시스템관리 레이어(§5B) — 전 모듈 선행 기반" + "멀티테넌시(§1A) — tenant_id 격리".
- **캡션**: "18개 모듈 + 공통 레이어를 한 장으로 조망. P0 4개가 심장."
#### ③ 나노바나나 파이프라인 플로우 (S20)
- **형태**: 좌→우 수평 플로우, 8박스.
- **박스·라벨**: `[사용자: 렌더 요청]``[Spring 백엔드: RenderJob 생성·쿼터 확인]``[Redis 큐 kintex:renderjob:queue(leftPush)]``[나노바나나 Python 워커(BLPOP 소비)]``[Gemini image-to-image(G1 승인·라이브)]``[워터마크 후처리·S6 로컬 PIL 래스터]``[오브젝트 스토리지 적재]``[pub/sub → 백엔드 relay → WebSocket/STOMP /topic/render/{jobId}]``[프론트: 완료 푸시·갤러리 갱신]`.
- **분기 주석**: 워커 하단에 점선 "degraded: G1 미승인/Gemini 장애 시 목(mock) 응답·구조·워터마크 유지". "성공 시에만 쿼터 차감".
- **보안 배지**: `GEMINI_API_KEY`는 워커 env only(백엔드 미취급).
- **캡션**: "이미지 생성 전면 비동기 — 사용자 동기 대기 없음, 평균 40초."
#### ④ Before/After 슬라이더 (S21)
- **형태**: 좌우 분할 이미지 카드 + 중앙 세로 드래그 핸들.
- **라벨**: 좌 "Before: 빈 부스(도면/실측)", 우 "After: 시공 후 예상(AI 생성·워터마크)". 핸들 "드래그로 비교".
- **접근성 배지**: "ARIA slider·키보드 좌우 조작 지원(WCAG AA)".
- **하단**: 워터마크 텍스트 예시 배지 "AI 생성 예상 이미지 — 실제 시공과 다를 수 있음. 계약·심사 서류 사용 금지."
#### ⑤ 3안 비교 화면 (S24)
- **형태**: 3열 카드(안1·안2·안3) + 상단 공통 홀 정보 바 + 하단 액션 바.
- **카드 라벨**: 각 카드 상단 "안 N — 최적화 목표"(예: 안1 "통로 효율 최대", 안2 "프리미엄 부스 최대", 안3 "균형"). 카드 본문 미니 배치도 썸네일 + 지표(판매면적 ㎡·부스 수·규정 위반 0/N).
- **액션 바**: `[안 선택]` `[구역/블록 병합 편집]` `[병합 후 재검증]`.
- **캡션**: "제약 솔버가 상이한 최적화 목표로 정확히 3안 생성. 사람이 선택·병합·최종 확정."
#### ⑥ PostGIS 배선 오버레이 (S27)
- **형태**: 홀 평면도 위 배선 경로 오버레이. 부스 폴리곤(연회색) + 트렌치 포인트(◆) + 분전반(▣) + 배선 LineString(색상별).
- **색상 범례**: 전기=적색, 네트워크=청색, 급배수=녹색(S6 규약 정합).
- **주석 배지**: "최근접 트렌치 KNN(`geom <-> point`)", "최단 경로(`ST_Length`)·통로 횡단 최소화", 가정 트렌치엔 "가정 좌표(실측 대기)" 배지.
- **캡션**: "수기 위치표시도 폐지 — 좌표 클릭 자동 작도. SRID 0 홀 로컬 미터, GiST 인덱스 실시간."
#### ⑦ 옥션 폐루프 흐름도 (S29)
- **형태**: 순환/좌→우 흐름, 좌측에 AI 자료 패키지 스택.
- **박스**: `[M2 배치 + M3 설계 + M4 배선/BOQ + M5 시공예측 이미지]`(AI 생성 자료 패키지) → `[옥션 개설(역경매/RFQ/고정가)]``[등록업체 검증 게이트: 미등록 응찰 차단 403]``[견적서(Quotation) 제출·PDF·버전]``[실시간 순위·라운드 마감(Redis 정렬셋·WS 델타)]``[종합평가 낙찰(가격+평판+납기)]``[Award → 계약/발주 자동전환(M6·M9 연동)]`.
- **캡션**: "AI 설계자료가 곧 발주 근거. 사진 수준 자료로 응찰→발주까지 한 흐름."
#### ⑧ 종합평가 스코어 비교표 (S31)
- **형태**: 업체 행 × 평가항목 열 표 + 가중 합계 열 + 낙찰 하이라이트 행.
- **열**: 업체명 | 견적금액(점수) | 평판(점수) | 납기(점수) | **가중 종합점수** | 순위.
- **가중치 주석 바**: "가중치(예): 가격 0.6 / 평판 0.25 / 납기 0.15 — 옥션별 설정형(`weight` jsonb)".
- **하이라이트**: 1위 행 강조 + "낙찰(Award)" 배지 + "선정 사유·항목별 비교" 캡션.
#### ⑨ 시스템 아키텍처 다이어그램 — 전 계층 (S41)
- **형태**: 상하 6밴드 계층도(system.md §2 논리 아키텍처 재구성).
- **밴드1 엣지/DMZ**: `[CDN·정적캐시]` `[WAF/Reverse Proxy(nginx)·TLS 종단·레이트리밋]`.
- **밴드2 역할별 프론트**: organizer·exhibitor·contractor·ops·admin(내부 배지)·public/visitor(공개 SSR/SEO/다국어 배지).
- **밴드3 SSO·RBAC**: `[JWT SSO + TOTP 2FA · 이중 RBAC(플랫폼/행사)]`.
- **밴드4 공유 백엔드**: `[REST + WebSocket/STOMP]` 아래 `[룰엔진]` `[배치·배선 엔진·PostGIS]` `[옥션 M15]` `[BI M16·KpiSnapshot]` `[CMS M17]` `[공개 API read-only]`.
- **밴드5 비동기**: `[Redis 큐·순위·타이머·캐시·쿼터]``[나노바나나 워커]` `[서류·EDM 워커]`.
- **밴드6 데이터**: `[PostgreSQL+PostGIS]` `[읽기복제(BI·공개)]` `[오브젝트 스토리지]`.
- **우측 외부 게이트 컬럼**: `[Gemini(G1·라이브)]` `[Claude(AiTextRouter)]` `[PG 결제]` `[kxwp 릴레이]` `[등록업체 DB 739]`.
- **연결 주석**: 워커→Gemini "egress only", 워커→백엔드 "WebSocket 완료 푸시", 백엔드→읽기복제 "BI/공개 오프로드".
#### ⑩ 보안영역 분리도 (망 구성) (S47)
- **형태**: 좌→우 신뢰도 상승 존 다이어그램(network.md §2).
- **존 박스**: `[인터넷]``[엣지: CDN·WAF·DDoS]``[DMZ(공개): 공개LB·public SSR·visitor GW·공개 API GW·PG콜백 — 쓰기권한 없음]``[내부망(인증): 내부LB·SSO/2FA·내부 API GW·공유 백엔드]``[AI 워커존(egress 제한): Redis·나노바나나·Ollama]``[데이터존(최내곽): PostgreSQL+PostGIS·오브젝트 스토리지·읽기복제 — 아웃바운드 전면 차단]`. 상단 `[관리존: 배스천·관측성]`.
- **철칙 배지(굵게)**: "DMZ → 데이터존 직접 접근 절대 금지", "데이터존 아웃바운드 0(유출 경로 제거)", "admin. = VPN/허용IP+2FA, 인터넷 미도달".
- **egress 컬럼**: 승인 4목적지만(Claude·Gemini·PG·SMTP), 그 외 DROP.
#### ⑪ 멀티테넌시 격리 아키텍처도 (S56)
- **형태**: 수직 4층 방어 스택 + 좌측 테넌트 컨텍스트 진입.
- **진입**: `[요청: 서브도메인(kintex./coex.) + JWT 사용자 소속]``[테넌트 컨텍스트 해소: 불일치 시 거부]`.
- **4층 박스(위→아래)**: `[①컨텍스트 필터]``[②서비스 가드]``[③MyBatis 공통 인터셉터(tenant_id 자동 주입)]``[④PostGIS/쿼리: tenant_id 선두 복합 인덱스]`.
- **우측 데이터**: `[TB_* … WHERE tenant_id = ctx]` 격리 표현. 하단 배지 "fail-closed — 한 층이라도 불일치면 차단".
- **캡션**: "KINTEX #1 검증 → COEX #2 코드 배포 없이 데이터 온보딩. 기능 불변, 데이터·권한만 분리."
#### ⑫ 일정 간트 (Phase × 모듈) (S58)
- **형태**: 가로 타임라인 3구간(Phase 1 ~4M / Phase 2 ~4M / Phase 3 ~4M+), 세로 워크스트림 막대.
- **막대(Phase 1)**: 아키텍처(A)·공통레이어(B, 2FA/시스템관리)·P0 코어(C: M2·M3·M4·M5).
- **막대(Phase 2)**: 옥션 M15·관람 M10·BI M16·관리자 M18·판매/서류/정산 M1/6/7/9·CMS M12/17.
- **막대(Phase 3)**: 멀티테넌시·M11·M13·M14·배포 안정화·SM 이관.
- **마일스톤 다이아몬드**: G1(나노바나나 승인·완료)·G2(배포서버·완료)·Phase 게이트·QA 게이트·오픈.
- **캡션**: "선행 게이트 통제 + Phase 게이트 QA 통과 후 진행."
> **추가 표준 도식**(공공 제안 필수, outline 명시): 추진체계도(S15)=거버넌스(PM/PMO/dev-PM)+아키텍트(AA·SA·TA·DA·NA)+코어/도메인/AI/QA/DevOps 팀 조직도 / 리스크 매트릭스(S59)=발생×영향 2×2에 R-1~R-8·C1~C8 배치 / 기대효과 인포그래픽(S61)=견적·배치·검수·위치표시도·시공예측 5지표 Before/After. 사양은 §2·§5·strategy §4 참조.
---
## 2. Win Theme 기술 근거 (5선 — 실현 가능성 입증)
### WT-1. 나노바나나 시공 예측 (배점 20 · 최우선)
- **기술 골자**: RenderJob 큐(Redis `kintex:renderjob:queue`) → Python 워커(BLPOP) → Gemini **image-to-image 구조보존** → 워터마크 후처리 → 오브젝트 스토리지 → WebSocket `/topic/render/{jobId}` 완료 푸시.
- **구조보존 메커니즘**(REQ-A-002·003): 참조 이미지 `inlineData` + 구조화 프롬프트 사전(BOOTH_TYPES·BOOTH_STYLES·FIXTURE_LAYERS)에서 **보존(골격·치수·통로·카메라 앵글) / 교체(스타일·재질만 사실화) 명시 분리**. ReRoomAI 검증 패턴 이식(reroomai-source.md).
- **방어**(REQ-A-006): 다층 크기가드·mimeType 자동감지·SAFETY 분기·친화적 한글 에러·**성공 시에만 쿼터 차감**. 간판 한글 렌더 실패 시 간판영역 후처리 합성(REQ-A-008).
- **S6 배선 오버레이**(REQ-A-004): Gemini 미경유 — **백엔드/워커 로컬 PIL 결정적 래스터 합성**(전기 적·네트 청·급배수 녹). 좌표 정확성 확보·비용/지연 제외.
- **실현 근거**: **라이브 검증**(G1 승인·워커 Gemini 실동작, WORK_STATUS §4). "말이 아니라 이미 돈다."
- **도식**: ③파이프라인·④Before/After.
### WT-2. 배치·설계·배선 AI 3안 + 규정 자동검증 (배점 20+15 교차)
- **기술 골자**: 부스 배치 3안은 **결정적 제약 솔버**(비-LLM)가 상이한 최적화 목표로 생성, LLM(Claude)은 조건 해석·설명만(REQ-A-009). → 공공 "AI 신뢰성" 프레이밍의 핵심(결정론 주도).
- **규정 자동검증**(REQ-F-009·013, REQ-A-010): 룰셋(`compliance-v*.json` 버전 데이터)을 룰엔진이 평가 + PostGIS 공간 판정(통로폭 `ST_Distance/ST_Buffer`·비상구 `ST_Intersects`·홀 이탈 `ST_Contains`·바닥하중 홀별). 병합 후 재검증. 위반 시 `COMPLIANCE_BLOCKED`(422).
- **배선 자동 작도**(REQ-F-015~017): 기기목록→kW 합산→분전반 산출, 최근접 트렌치 KNN(`geom <-> point`)→최단 배선 LineString(`ST_Length`, 통로 횡단 최소 휴리스틱), 위치표시도 좌표 클릭 자동 작도(수기 폐지).
- **AI 신뢰 3층**: 결정적 알고리즘(제약 솔버·PostGIS) 중심 + LLM 보조 + **사람 최종 확정**. 규정검증=플래깅까지, 구조안전 판정은 구조기술사/킨텍스(Non-Goal). RAG 근거·인용·환각 차단(abstain, REQ-A-017).
- **실현 근거**: M2~M5 매퍼(PostGIS 공간 SQL)·룰엔진 **라이브**(WORK_STATUS §4). 3안 생성·병합·집계 API는 구현 계획.
- **도식**: ⑤3안 비교·⑥PostGIS 배선.
### WT-3. 공사 옥션 폐루프 (배점 15 · 폐루프 핵심)
- **기술 골자**: `TB_AUCTION 1─N TB_QUOTATION ─ TB_AWARD`. 옥션 유형 설정형(REVERSE/RFQ/FIXED). AI 자료 패키지(M2~M5 참조 `attached_refs`) 열람 기반 견적서 제출·PDF·버전. 실시간 순위=Redis 정렬셋 + WebSocket `/topic/auction/{auctionId}` 델타 푸시, 라운드 마감=Redis 타이머.
- **등록 게이트**(REQ-S-007): 미등록 업체 응찰 원천 차단 `NOT_REGISTERED_COMPANY`(403). M7이 `TB_COMPANY.kintex_registered` 검증 권위. → 킨텍스 "등록업체 시공 필수" 규정을 시스템이 강제.
- **종합평가·발주 전환**(REQ-F-028·032): 가중 스코어(가격+평판+납기, `weight` jsonb) → Award → 계약/발주 자동전환(M6·M9 연동, 이벤트 디커플·순환 금지).
- **실현 근거**: 옥션은 **구현 계획**(Phase D-M15). 스캐폴드 4계층·큐·WS 패턴·데이터모델 확정으로 실현성 입증.
- **도식**: ⑦옥션 폐루프·⑧종합평가 스코어.
### WT-4. 3중 보안 — 워터마크·PII·테넌트 (배점 10)
- **워터마크**(REQ-S-009): M5 이미지 응답 **항상** `watermarkRequired:true`+`watermarkText`+`notice`(계약·심사 서류 금지). 제거 불가(계약 강제, app.md §5-6). 워커 목/live 공통.
- **PII 격리**(REQ-S-013): 관람객/리드 `*_enc` AES-256-GCM 컬럼 암호화, 응답 완전 제외/마스킹, 동의 분리(privacy 필수/marketing 선택/share), 접근 감사 전수. BI 마트는 개인 식별자 비반입(집계만).
- **테넌트 3중**(REQ-S-018): §1-3 fail-closed 4층(컨텍스트→서비스→MyBatis→PostGIS).
- **외부 API 격리**(REQ-S-001·002): Ollama 기본 + Claude/Gemini만 승인 예외. `GEMINI_API_KEY` 워커 env only(백엔드 미취급), egress 워커존 단일 경로.
- **실현 근거**: 인증(JWT/RBAC/2FA/admin env)·워터마크 **라이브**. PII 암호화·테넌트 필터는 구현 계획.
- **도식**: ⑩보안영역 분리·⑪멀티테넌시 격리.
### WT-5. 멀티테넌트 SaaS 확장 (배점 5 · 미래가치)
- **기술 골자**: §1-3. tenant_id 전파 + 2계층 관리자(플랫폼 슈퍼관리자 테넌트스위처 / 테넌트 관리자 단일스코프) + 컨텍스트 해소. 온보딩 6단계=마스터데이터 입력 수준.
- **개발 부담 최소화**(strategy R-8): 멀티테넌시=격리 레이어만 추가, 기능 로직 불변. 백필 전 단일테넌트 무중단→검증 후 필터 활성화(회귀 방지).
- **실현 근거**: 구현 계획(WORK_STATUS §6). GUARDiA 표준 프레임워크 멀티테넌트 패턴 재사용.
- **도식**: ⑪멀티테넌시 격리.
---
## 3. 비기능(NFR) 충족안 — REQ-N 21건 전부 대응 (P6·P4)
> 근거: system.md §3(확장성·HA·성능·용량)·tech.md §5(관측성·성능)·data.md §3·§6·network.md §7. **SLO 99.5%(성수기 99.9% 지향)**.
| REQ-ID | 요구 | 충족 방안(기술) | 측정/게이트 | 근거 |
|---|---|---|---|---|
| REQ-N-001 | 코어 SLO 99.5% | 무상태 백엔드 + Redis 외부화 + HA(§아래) | 월 가용성 ≥99.5%, `/actuator/health` | system.md §3-2 |
| REQ-N-002 | 백엔드 2인스턴스·롤링 무중단 | 최소 2 인스턴스 + 헬스체크 LB + 롤링 배포 | 무중단 배포 검증(스테이징→prod) | system.md §3-2 |
| REQ-N-003 | PG 스탠바이·failover·PITR | 프라이머리-스탠바이 스트리밍 복제 + Patroni/관리형 자동 failover + PITR | RPO 최소·RTO 분 단위 | system.md §3-2 |
| REQ-N-004 | Fail-Safe 배포 | 백업→배포→헬스체크 200→실패 시 롤백(이전 jar 유지) | **라이브**(deploy_kintex.sh) | WORK_STATUS §2 |
| REQ-N-005 | degraded 모드 | G1 미승인/Gemini 장애=워커 목 응답(구조·워터마크 유지), Claude 실패=Ollama 폴백, PG 복제지연=프라이머리 폴백 | 무네트워크 스모크 통과 | system.md §3-2 |
| REQ-N-006 | 나노바나나 40초·1024px | 전면 비동기(WS 완료 푸시) + 클라 Canvas 1024px 다운스케일+JPEG 0.85 전처리 | 단건 평균 ≤40s | system.md §3-3 |
| REQ-N-007 | 대량 생성 제어 | 자동생성 **S1·S7 한정** + 동일 스키마해시 캐시 + 행사별 RenderJob 쿼터(`RENDER_EVENT_QUOTA` 기본 500·성공 시만 차감) | 쿼터/캐시 히트율 | system.md §3-3, R6 |
| REQ-N-008 | 공간쿼리 GiST 실시간 | 전 `geom` 컬럼 GiST 인덱스(트렌치 KNN·통로버퍼·비상구 교차) | 배치·배선 P95 <2s | data.md §3-4 |
| REQ-N-009 | 워커 독립 스케일 | 나노바나나·서류·EDM 워커 큐 깊이 기반 증감, 백엔드 응답성과 분리 | 큐 적체 모니터 | system.md §3-1 |
| REQ-N-010 | BI/공개 읽기복제 오프로드 | BI 집계·공개 조회·플로어플랜 열람 읽기복제 라우팅, 운영 프라이머리 보호 | 프라이머리 부하 격리 | system.md §3-1 |
| REQ-N-011 | 워커 장애 재큐(멱등) | 무상태 소비자 N대 + 가시성 타임아웃 후 재큐(멱등 Job) + AOF 지속화 + ACK | 진행 Job 유실 0 | system.md §3-2 |
| REQ-N-012 | WCAG 2.1 AA·KWCAG 2.2 | 시맨틱 마크업·ARIA·키보드·대비, 공공 KWCAG 2.2 병행 점검 | 접근성 자동+수동 감사 | rfp §6-N1 |
| REQ-N-013 | 캔버스/맵 비시각 대안 | 플로어플랜/배선 캔버스에 데이터 테이블·검색·키보드 부스 선택 대안 필수 제공 | 비시각 경로 검증 | rfp §6-N1 |
| REQ-N-014 | 다국어 i18n·hreflang | i18n 외부화(한 기본+영/중/일)·로케일 전환·hreflang·로케일 포맷 | 4로케일 전환 | rfp §6-N3 |
| REQ-N-015 | 라이트/다크 테마 | CSS 토큰 2벌·prefers-color-scheme 연동·선택 영속 | 테마 왕복 | rfp §6-N4 |
| REQ-N-016 | 반응형 | 데스크톱=설계/에디터, 모바일=현장/조회/승인 | 브레이크포인트 검증 | PLANNING §8 |
| REQ-N-017 | 단일 공간 원천 | M2 폴리곤·M4 배선 매퍼 권위, 소비 모듈 조회만·중복 저장 금지 | ArchUnit·계약 검수 | app.md §3-2, data.md §0-2 |
| REQ-N-018 | 관측성·미기록 | 로그·메트릭·트레이스 관리존 수집, 자격증명·PII·스택트레이스 미기록(`include-stacktrace: never`) | 로그 감사 | tech.md §5, network.md §8 |
| REQ-N-019 | 공개영역 CDN·요청률 상한 | 공개 SSG+CDN 캐시·요청률 상한·slowloris 타임아웃·동시연결 캡 | 스파이크 CDN 흡수 | network.md §7 |
| REQ-N-020 | tenant_id 선두 복합 인덱스 | 격리 쿼리 표준 인덱스(tenant_id 선두) | 격리 쿼리 성능 | data.md, §1-3 |
| REQ-N-021 | BI 데이터마트 배치 ETL | 스타 스키마(FACT/DIM) 야간 배치 ETL 또는 읽기복제 적재, KpiSnapshot | 원천-집계 정합(오차 0) | data.md §6 |
**HA 구성 요약(P6 SLA 표 입력)**: 프론트/공개=CDN 다중엣지+nginx 다중화 / 백엔드=2+ 인스턴스 헬스체크 LB·롤링 / Redis=Sentinel(자동 failover)+AOF / PostgreSQL=프라이머리-스탠바이+자동 failover+PITR / 워커=무상태 N대·재큐 / 배포=Fail-Safe(롤백). **SLO: 코어 99.5%(성수기 99.9% 지향), 나노바나나 생성=best-effort 비동기(큐 소진 목표).**
**용량 산정(P4·P6 근거)**: 단일 대형 행사 3,000~5,000부스(수만 지오메트리·GiST 필수) / RenderJob 부스 3,000×자동 2샷(S1·S7)+온디맨드=수천~1만 이미지(수 GB~수십 GB, OBJ+CDN·캐시 억제) / 리드·체크인 수만~수십만(파티셔닝 후보) / 동시 사용자 인증 수백+공개 수천~수만(CDN 흡수) / **DB 연결풀 Hikari max 3(`DB_POOL_MAX`)+PgBouncer 권고**(공유 PG 포화 방지). (system.md §3-4)
---
## 4. 보안 충족안 — REQ-S 20건 전부 대응 (P5 · 시크릿 값 미기재)
> 근거: SECURITY.md·app.md §5-6·§6·network.md·data.md §5·tech.md §7. **불변(위반=QA 반려)**.
| REQ-ID | 요구 | 충족 방안(기술) | 근거 |
|---|---|---|---|
| REQ-S-001 | 외부 API 정책 | Ollama 기본 + Claude(`api.anthropic.com`)·Gemini만 승인 예외, egress 화이트리스트 그 외 DROP | network.md §5 |
| REQ-S-002 | GEMINI 키 워커 env only | 나노바나나 워커 env only, Spring은 큐/콜백만, 워커존 단일 egress | tech.md §3-5 |
| REQ-S-003 | 자격증명·키 완전 제외 | 응답/로그/에러/커밋 마스킹, `*_enc`·해시·내부IP·워커토큰 미노출 | tech.md §7 |
| REQ-S-004 | admin 비번 env 재시드 | `ADMIN_PASSWORD_ENC`(AES-256-GCM)+별도 키파일→기동 시 BCrypt 재시드, admin123 금지 | **라이브**(env 시더) |
| REQ-S-005 | JWT+2FA+실패 잠금 | JWT(HS256)+TOTP(RFC6238)+로그인 실패 잠금(관리자 해제) | **라이브**(V7/V8) |
| REQ-S-006 | 행사 RBAC 3중 | 테넌트 격리 ⊃ 행사 RBAC(열람=멤버/홀매니저, 편집=역할별 게이트) | app.md §6 |
| REQ-S-007 | 미등록 업체 차단 | M15 응찰·초대 `NOT_REGISTERED_COMPANY`(403), M7 검증 권위 | app.md §6-2 |
| REQ-S-008 | admin/internal 게이트 | `/api/admin/**`=hasRole(ADMIN), `/api/internal/**`=X-Worker-Token | app.md §5-5 |
| REQ-S-009 | 이미지 워터마크 항상 | M5 응답 항상 watermarkRequired:true+text+notice, 제거 불가 | **라이브**·app.md §5-6 |
| REQ-S-010 | 오류 스택트레이스 미노출 | `server.error.include-*: never` + ApiResponse.error 요약만 | tech.md §5-1 |
| REQ-S-011 | DataAccessException 요약 매핑 | `@RestControllerAdvice GlobalExceptionHandler` INTERNAL 요약(테이블/컬럼 누출 차단) | app.md §7-2 |
| REQ-S-012 | 민감 액션 감사 전수 | 승인·낙찰·설계변경·룰셋개정·리드접근·로그인/권한 `TB_AUDIT_LOG`(tenant_id 포함, `@Audited` AOP) | app.md §7-3 |
| REQ-S-013 | PII 동의·마스킹·암호화 | 동의 분리(privacy/marketing/share)·최소수집·`*_enc` AES-256-GCM·응답 제외·보존/파기 | data.md §5 |
| REQ-S-014 | 입력 검증 | Bean Validation 화이트리스트·파일 MIME/크기 가드(WAF+앱 이중) | tech.md §7, network.md §4-2 |
| REQ-S-015 | XSS 방지·CSP | React 이스케이프·dangerouslySetInnerHTML 금지·CMS sanitize·CSP | rfp §6-N2 |
| REQ-S-016 | 인젝션 차단 | MyBatis `#{}` 강제·`${}` 금지, PostGIS/명령/LDAP 파라미터 바인딩 | rfp §6-N2 |
| REQ-S-017 | IDOR·CSRF·JWT 회전 | 리소스 소유·tenant_id 검증, CSRF/SameSite, JWT 만료·회전 | rfp §6-N2 |
| REQ-S-018 | fail-closed 테넌트 격리 | 4층(컨텍스트 필터→서비스 가드→MyBatis 인터셉터→PostGIS) | §1-3 |
| REQ-S-019 | 의존성 SCA CI 게이트 | OWASP Dependency-Check CI 게이트·정기 패치 | rfp §6-N2 |
| REQ-S-020 | 보안영역 분리 | 공개 DMZ 쓰기 없음·데이터존 직결 불가·DMZ↔내부 HTTPS(mTLS 권장)만·DB 프로토콜 횡단 금지 | network.md §2·§4 |
**보안 성숙도 강조점(P5)**: ①이미 라이브인 인증·워터마크·admin env(4·5·9번 실증) → "설계가 아니라 동작하는 통제". ②데이터존 아웃바운드 0으로 유출 경로 자체 제거(network.md 철칙). ③외부 AI 키 격리=네트워크 격리 정합(Gemini 워커존만, Claude 백엔드만, 데이터존 무egress).
---
## 5. WBS·공수(M/M) 산정 (P8)
> 근거: IMPLEMENTATION_BACKLOG Phase A~E·PLANNING §9 로드맵(Phase 1~3 각 ~4개월)·142 REQ·18 모듈. 산정 기반=유사 공공 AI SI 생산성 + 기능 규모(100대 기능: 기구현 63·부분 18·신규 19) + 라이브 스캐폴드로 인한 **착수 리스크·초기 구축 공수 절감** 반영.
### 5-1. WBS 구조 (Phase × 산출물)
| Phase | 기간(가정) | 주요 작업(WBS) | 완료 게이트 |
|---|---|---|---|
| **A 아키텍처·거버넌스** | 착수~1M | AA/SA/TA/DA/NA 5종 아키텍처(**완료**)·정합 검증 | 아키텍처 확정·불일치 0 |
| **B 공통/시스템관리 레이어** | ~Phase1 내 | 인증(JWT/2FA)·시스템관리·공통 업무기능·공통 컴포넌트(WISE 이식) | 2FA/RBAC/감사 왕복(**대부분 라이브**) |
| **C P0 부스 코어** | Phase1(~4M) | M2 플로어플랜(3안·규정검증)·M3 부스설계·M4 배선·M5 나노바나나 | 3안·병합·재검증·시각화 |
| **D P1 도메인** | Phase2(~4M) | M15 옥션·M10 관람·M12/17 CMS/공개·M16 BI·M18 관리자·M1/6/7/9 | 각 모듈 왕복·QA 통과 |
| **E P2·배포·확장** | Phase3(~4M+) | M11·M13·M14·**멀티테넌시**·역할별 번들 분리·배포 안정화·SM 이관 | (G2 후)배포·이관 |
### 5-2. 직군별 공수 산정 (M/M)
> 12개월(Phase 1~3) 기준. 라이브 스캐폴드/인프라/인증 실적으로 초기 구축 공수를 이미 일부 소화 → 산정에 반영(과다 산정 금지). Phase A는 대부분 완료(감독 공수만 잔여).
| 직군 | Phase 1 | Phase 2 | Phase 3 | 합계(M/M) | 산정 근거 |
|---|---|---|---|---|---|
| 총괄 PM | 4 | 4 | 4 | 12 | 상주 1.0 FTE |
| 아키텍트(AA·SA·TA·DA·NA) | 8 | 4 | 2 | 14 | Phase A 집중(완료)→거버넌스 감독 |
| 백엔드(공통·코어·도메인) | 12 | 16 | 12 | 40 | 18모듈 코어, 최대 투입 직군 |
| 프론트엔드(역할별 6포털) | 8 | 12 | 8 | 28 | Stitch 이식+역할별 번들 |
| DB 엔지니어(PostGIS) | 4 | 4 | 2 | 10 | 공간 스키마·매퍼·BI 마트·멀티테넌시 |
| AI 개발(Claude·제약 솔버) | 4 | 4 | 2 | 10 | 배치 솔버·규정보조·예측·AiTextRouter |
| 시각화(나노바나나 워커) | 4 | 2 | 2 | 8 | 파이프라인(라이브)·샷세트·품질 |
| QA | 4 | 6 | 6 | 16 | 점진 경계면·보안 불변·회귀 |
| DevOps/인프라 | 2 | 2 | 4 | 8 | CI/CD(라이브)·배포·이관 |
| **소계** | **50** | **54** | **42** | **146** | |
- **총 공수: 약 146 M/M**(핵심 개발) + PMO/형상관리·품질보증 오버헤드(약 4~8 M/M) → **총 ~150 M/M 규모**.
- **가격평가·투입계획 정합**: 위 산정은 가격제안 투입인력표(직군·기간·M/M)와 1:1 매핑. 라이브 실적(스캐폴드·인증·인프라·나노바나나)이 Phase A/B 공수를 실측 절감 → 동일 규모 대비 **착수 리스크·초기 구축비 우위**.
- **주의**: 예산·사업기간은 실제 RFP 부재로 [가정](공공 SI 통례). 실제 공고 확인 시 M/M·기간 즉시 재정렬.
---
## 6. SM 운영 체계 (SLA·장애대응·정기점검·CI/CD 운영)
> SI 구축 후 유지보수/운영 제안. **이미 라이브인 자동배포 파이프라인을 실적으로 인용**(과장 아님 — WORK_STATUS §2·§7 검증분).
### 6-1. 운영 체계·조직(R&R)
- **서비스 데스크·ITSM 프로세스**: 요청(SR)·장애(Incident)·변경(Change)·배포(Release)·문제(Problem·RCA). 공통 업무모듈(통합검색·알림·회의록)·감사로그 기반 티켓 이력.
- **조직**: 운영 총괄 1 + 백엔드/프론트 유지보수 + DB/인프라(DevOps) + AI 워커 담당 + QA. 근무체계=평일 상주 + 행사 성수기(3~5·9~11월) 온콜.
### 6-2. SLA 지표
| 지표 | 목표 | 측정 |
|---|---|---|
| 코어 서비스 가용성 | **99.5%**(성수기 99.9% 지향) | 월 가용성·`/actuator/health` |
| 장애 응답시간 | 심각도별(P1 즉시·P2 30분·P3 4시간) | 티켓 타임스탬프 |
| 장애 복구시간(MTTR) | P1 ≤2h·P2 ≤1영업일 | 복구 완료 기록 |
| 배포 성공률 | ≥99%(Fail-Safe 롤백 자동) | 배포 로그 health 게이트 |
| 나노바나나 큐 소진 | best-effort(피크 워커 스케일아웃) | 큐 적체·처리량 |
측정·보고: 월간 SLA 리포트(가용성·MTTR·티켓·배포). 페널티 체계는 계약 조건에 따름(RFP 확인 필요).
### 6-3. 유지보수 활동
- **예방정비·정기점검**: DB 인덱스/통계·복제 지연·백업(PITR) 정상성, Redis AOF·큐 백로그, 워커 하트비트, 디스크/스토리지 용량, 인증서(TLS) 만료 점검.
- **패치/보안 업데이트**: OWASP Dependency-Check SCA 정기 스캔(REQ-S-019)·의존성 패치, OS/미들웨어 보안 패치. 룰셋(요율·규정) **무중단 개정**(버전 데이터, M18 백오피스·감사 기록).
- **성능 튜닝**: PostGIS 공간 쿼리(GiST·EXPLAIN ANALYZE)·Hikari 풀·읽기복제 라우팅·BI 배치 ETL 시간 최적화.
- **형상관리**: Gitea `zio/kintex`·Conventional Commits·main 보호·환경 분리(dev/staging/prod).
### 6-4. 장애 대응
- **등급분류·에스컬레이션**: P1(서비스 중단)~P4(경미). 자동 알림(WebSocket/통합 알림센터)·온콜 에스컬레이션.
- **RCA·재발방지**: 장애 후 근본원인 분석(RCA) 리포트·재발방지 조치 티켓화. 감사로그·관측성(로그/메트릭/트레이스, PII·스택트레이스 미기록)로 추적.
- **degraded/BCP·DR**: G1/Gemini 장애=워커 목 응답(구조·워터마크 유지), Claude 장애=Ollama 폴백, PG 장애=자동 failover+PITR, 배포 실패=자동 롤백. 데이터 백업·복구 절차(RPO 최소·RTO 분 단위).
### 6-5. CI/CD 운영 (라이브 실적 인용)
- **자동배포 파이프라인(라이브·검증됨)**: `workspace/kintex` → Gitea webhook → `deploy_kintex.sh`(bootJar + vite build → 배포 → **health 게이트 → 실패 시 롤백**). push 시 자동배포. (WORK_STATUS §2·§7)
- **운영 함정 숙지(실측 교훈)**: 서버 `/opt/zioinfo/deploy_server.py`(kintex 블록)·`/opt/kintex/` 수정 시 **반영+재시작 필수**(로컬만 고치면 웹훅 1ms no-op), 백엔드도 반드시 빌드, `sh ./gradlew`(실행권한), 로컬 vite rollup win32 크래시=**서버 빌드 신뢰**(tsc 통과 검증).
### 6-6. 이행/이관
- 인수인계(WORK_STATUS.md 인수인계 문서 체계 활용)·안정화·지식이전(운영자/개발자 지침서 PPT·프로그램 사양서)·종료 시 이관 계획. 운영 도메인 `kintex.wise.ai.kr` 전환(후속).
---
## 7. RTM 기술 파트 갱신 입력 (REQ ↔ tech_advisory 절 매핑)
> rfp-analyst RTM 반영상태 갱신용. 각 REQ 계열이 본 자문서 어느 절에서 기술적으로 대응되는지 매핑. QA는 이 표로 기술 파트 커버리지 교차 점검.
| REQ 계열 | 범위 | tech_advisory 대응 절 | 제안서 목차 |
|---|---|---|---|
| REQ-F-007~019(배치·설계·배선 코어) | M2~M4 | §2 WT-2, §1-2, 도식 ⑤⑥ | P3 |
| REQ-F-025~032(옥션) | M15 | §2 WT-3, 도식 ⑦⑧ | P3 |
| REQ-F-037~055(관람·현장·물류) | M8·M10~M14 | §1-2 App/데이터, §3(오프라인 폴백 N-폴백) | P3 |
| REQ-F-061~066(BI) | M16 | §1-2 데이터, §3 REQ-N-021, data.md 마트 | P3 |
| REQ-F-071~081(관리·공통·멀티테넌시·포털) | M18·§5B·§1A | §1-2·§1-3, 도식 ⑪ | P4·P5·P7·P8 |
| REQ-F-001~006·020~024·033~036·048~051·056~060·067~070 | 판매·서류·정산·CMS·공개 | §1-2 계층·§2 WT, §6(룰셋 운영) | P3 |
| **REQ-A-001~008**(나노바나나) | M5 | §2 WT-1, 도식 ③④ | P3(핵심) |
| REQ-A-009·010·020(배치·규정·비전) | M2/M3 | §2 WT-2 | P3 |
| REQ-A-013·014(예측·KPI·Text-to-SQL) | M16 | §1-2 AI(텍스트), §3 N-021 | P3 |
| REQ-A-016·017(라우터·RAG·환각) | §6 | §1-2 AI(텍스트), §4(abstain) | P4·P5 |
| REQ-A-011·012·015·018·019(검수·챗봇·카피·리드·이상탐지) | 다수 | §1-2 AI(텍스트) | P3 |
| **REQ-N-001~021** | 비기능 21 | **§3 전체 표(21행 1:1)** | P6·P4 |
| **REQ-S-001~020** | 보안 20 | **§4 전체 표(20행 1:1)** | P5 |
| (인프라·HA·배포·관측성) | N-002·003·004·018 | §1-2·§3·§6 SM·§0 실적 | P4·P6·P8 |
---
## 요약 보고 (오케스트레이터·writer·deck-designer용)
**1. 도식 12종 사양 완비 여부**: ✅ **12/12 완비**(§1-4). ①3중 해자 벤 ②모듈맵 ③나노바나나 파이프라인 ④Before/After ⑤3안 비교 ⑥PostGIS 배선 ⑦옥션 폐루프 ⑧종합평가 스코어 ⑨시스템 아키텍처 ⑩보안영역 분리 ⑪멀티테넌시 격리 ⑫일정 간트 — 각 박스·연결·라벨 텍스트 작도 사양 기술. 추가 표준 도식 3종(추진체계도·리스크 매트릭스·기대효과)은 사양 위치 지정(§1-4 말미).
**2. REQ 대응 커버리지**: **미대응 0**. REQ-N 21건 전부(§3 표 21행 1:1), REQ-S 20건 전부(§4 표 20행 1:1), REQ-F/REQ-A는 §1-2·§2 Win Theme·§7 매핑표로 전 계열 대응. 총 142 REQ 기술 파트 커버(§7 RTM 갱신 입력).
**3. 공수 총 M/M**: **약 146 M/M**(핵심 개발, 직군 9종×Phase 3구간) + PMO/QA 오버헤드 4~8 → **총 ~150 M/M 규모**(12개월·Phase 1~3). 라이브 스캐폴드·인증·인프라·나노바나나 실적으로 Phase A/B 공수 실측 절감 반영 = 동일 규모 대비 착수 우위.
**4. 과장 방지 준수**: 라이브 확인분(인프라·CI/CD·나노바나나·인증·워터마크·PostGIS 코어·평면도 자산)만 "구축 완료/검증됨", 도메인 모듈·멀티테넌시·집계 API·i18n/접근성 전면 적용은 "구현 계획"으로 명확 구분(§0). 시크릿(키·비번·내부IP) 전면 미기재.
**5. 파일**: `C:\GUARDiA\workspace\kintex\_workspace\proposal\tech_advisory.md`

View File

@ -1,274 +0,0 @@
# 킨텍스 자동전시시스템 구축 — 도식 시각 컨셉 (Visual Concepts)
> 작성: proposal-visual-designer · 작성일: 2026-07-11 · 버전 v1.0
> 짝 문서: `design_system.md`(팔레트·타이포·레이아웃) · `assets/icons/*.svg`(선 아이콘 20종) · `tech_advisory.md` §1-4(도식 12종 박스·연결·라벨 사양) · `proposal_outline.md`(슬라이드 배치)
> 목적: 필수 도식 12종 + 표준 3종(추진체계·리스크·기대효과) + 표지·간지·3중 해자의 **시각 컨셉을 좌표·색·라벨 수준으로 구체화**. deck-designer가 python-pptx로 재해석 없이 작도.
> 좌표 규약: 단위 **inch**, 16:9 캔버스 13.333×7.5, 콘텐츠 폭 12.23(x 0.55~12.78), 본문 콘텐츠 y 1.45~6.9. 색 토큰=`design_system.md` §1. 폰트 맑은 고딕.
> 공통: 노드=ROUNDED_RECTANGLE, 카드 그림자 alpha 18%, 연결 화살표="→"(15pt)/""(16pt) SLATE, 하단 캡션 MUTED 9pt 1줄. **본문 10pt 하한 준수**.
---
## A. 표지 (S01) — "도면이 아니라 사진으로 컨펌한다"
**컨셉**: 좌측은 절제된 텍스트 존, 우측은 나노바나나 시공 예측 이미지가 대각으로 침투 — "왼쪽은 제안(텍스트), 오른쪽은 결과(사진)"의 물리적 은유.
- **배경**: `BLUE_DK` 풀블리드(-0.1,-0.1,13.53,7.7).
- **우측 대각 이미지 존**: 평행사변형 2매 겹침 — ①`BLUE` (6.4,-1.5,9,11, PARALLELOGRAM) ②`PURPLE` (9.2,-2.0,7,12, PARALLELOGRAM). **그 위에 나노바나나 생성 부스 시공 사진(워터마크 포함)을 반투명(alpha ~35%) 합성** — 이미지 없으면 blue→purple 그라디언트 도형으로 대체(폴백). 이미지 우하단에 소형 워터마크 배지 "AI 생성 예상 이미지".
- **좌상 브랜드**: WHITE OVAL(0.85,0.8,0.42) + PURPLE 코어 OVAL(0.97,0.92,0.18) + "KINTEX · WISE AI" 15pt WHITE Bold (1.42,0.78).
- **문서 배지**: (0.85,2.35) "기술제안서" PURPLE 12pt / (2.72,2.35) "Technical Proposal" 딥블루 10.5pt.
- **대제목** (0.85,2.9,11,1.9): 2줄 46pt WHITE Bold — "킨텍스 자동전시시스템" / "구축 기술제안서". line 1.02.
- **슬로건 배너**(핵심): (0.85,4.75,11.4,0.7) 반투명 화이트 라운드 rect(alpha 12%) 위 PURPLE 좌측 바 + "신청서를 내는 순간, 시공 후 사진을 먼저 본다." 18pt WHITE Bold.
- **서브 카피** (0.88,5.6): "설계 → 시각화 → 발주 → 시공 → 운영 → 분석. 킨텍스에서 한 흐름으로 끝난다." 12.5pt `#B9C7EA`.
- **하단 메타바** (0.85,6.15 라인): 사업명·기술스택 2줄 + 우측 "작성일 2026-07-11". 개발계획서 표지 하단과 동일 구조.
- **아이콘 앵커**: 슬로건 좌측에 `camera-render` 근사 도형(WHITE stroke).
---
## B. 간지 (Section Divider) — P1~P9 공통 템플릿
**컨셉**: 개발계획서 간지 계승 + **배점 배지 추가**(제안서 배점 규율 시각화).
- 배경 `BLUE_DK` + 우측 평행사변형 2매(BLUE 8.6,-1.5,8,11 / PURPLE 10.9,-2,6,12).
- 좌측 대형 넘버 150pt `#2C4E82` (1.0,2.15). PURPLE 언더바(1.15,3.9,0.9,0.09). 국문 제목 34pt WHITE(1.15,4.05). 영문 13pt `#9CB4D8`(1.18,4.85).
- 우측 포인트 3개: PURPLE OVAL 불릿(8.05,y) + WHITE 12.5pt(8.35,y), y=2.35+i*0.62.
- **★배점 배지 (우상단, 신규)**: (10.3,0.7,2.2,0.62) 라운드 rect, fill=섹션색(P3=PURPLE, 그 외 BLUE), WHITE Bold — 예 "기술제안 · 35점". P3만 PURPLE로 최다 배점 강조.
- 섹션별 넘버/제목/영문/포인트/배점:
| 섹션 | No | 제목 | EN | 배점배지 | 포인트 3 |
|---|---|---|---|---|---|
| P1 | 01 | 사업이해 | BUSINESS UNDERSTANDING | 사업이해·전략 10점(BLUE) | As-Is 병목 / 6역할 페르소나 / To-Be 정량목표 |
| P2 | 02 | 추진전략 | STRATEGY | Win Theme 5선(BLUE) | 3중 해자 / 생애주기 폐루프 / 추진체계 |
| P3 | 03 | 기술제안 | TECHNICAL SOLUTION | 기술제안 · 35점(PURPLE) | 나노바나나 시공예측 / 배치·설계·배선 AI / 옥션 폐루프 |
| P4 | 04 | 아키텍처·기술스택 | ARCHITECTURE | 아키텍처 10점(BLUE) | 확정 스택 4-Tier / 단일 공간원천 / AI 라우팅 |
| P5 | 05 | 보안·개인정보 | SECURITY & PRIVACY | 보안 10점(BLUE) | 워터마크 / PII 3중격리 / 망분리 |
| P6 | 06 | 비기능(NFR) | NON-FUNCTIONAL | 비기능 8점(BLUE) | SLO 99.5% / 렌더 40초 / 접근성·다국어 |
| P7 | 07 | 멀티테넌시·확장 | MULTI-TENANCY | 확장 5점(BLUE) | 격리 아키텍처 / 온보딩 6단계 / COEX 확장 |
| P8 | 08 | 사업관리·일정·조직 | PROJECT MANAGEMENT | 사업관리 2점(BLUE) | Phase 간트 / 선행게이트 / 품질관리 |
| P9 | 09 | 기대효과 | EXPECTED BENEFITS | 가점(PURPLE) | 정량 ROI / 정성효과 / 확장비전 |
---
## C. 필수 도식 12종
### ① 3중 해자 벤다이어그램 (S04 요약판 · S12 상세판) ★최중요
**컨셉**: 3원 60% 교집합. 중앙 교집합만 컬러 채도 최대(해자), 2원 교집합은 회색(모방 가능). "결합의 희소성"을 색 채도로 표현.
- **3원 배치**(정삼각 구도, 반경 r=1.9): 원A 중심(5.4,3.6) `PURPLE` stroke 2.5pt + PURPLE_LT 반투명 fill / 원B 중심(7.9,3.6) `BLUE` / 원C 중심(6.65,5.5) `TEAL`. OVAL 도형, fill은 반투명(alpha 22%)로 겹침 표현.
- **원 라벨**(각 원 바깥 가까이):
- 원A(좌상) `ai-sparkle` 아이콘 + "나노바나나 시공 예측 이미지" 13pt PURPLE Bold / 부제 "image-to-image 구조보존 · 40초 · S1~S7" 8.5pt MUTED.
- 원B(우상) `postgis-spatial` + "PostGIS 실측 공간데이터" 13pt BLUE Bold / "부스 폴리곤·트렌치·배선 LineString 단일 원천".
- 원C(하) `closed-loop`+`auction-gavel` + "공사 옥션 폐루프" 13pt TEAL Bold / "AI 자료→응찰→낙찰→발주 자동전환".
- **중앙 3원 교집합**(6.65,4.2 부근): 강조 배지 라운드 rect(INK fill) + WHITE Bold "우리만의 해자(Moat)" 11pt / 소자 "재현 난이도 높은 결합".
- **2원 교집합 3곳**: 소형 회색 라벨 "단일 요소는 모방 가능" 8pt SLATE.
- **하단 캡션**(중앙 정렬): "셋의 결합은 경쟁 전시테크·범용 SI가 재현하기 어렵다." 9.5pt MUTED.
- S04(요약판)=벤+한줄, S12(상세판)=벤 좌측 + 우측에 3결합 재현난이도 근거 3불릿 카드(strategy §3-3).
### ② 모듈맵 — 생애주기 × 우선순위 매트릭스 (S16)
- **매트릭스 프레임**: x=생애주기 6단계 열, y=우선순위 3행(P0/P1/P2). 콘텐츠 영역(0.55,1.7)~(12.78,6.2).
- **열 헤더**(y=1.7, 각 폭 ~1.95): "판매·기획" / "설계·시각화" / "발주·계약" / "참가·관람" / "현장운영" / "사후·경영". BLUE 밴드 WHITE 10pt Bold.
- **행 라벨**(좌측 x=0.55, 폭 0.9): P0(PURPLE)/P1(BLUE)/P2(SLATE) 세로 배지.
- **모듈 칩** 배치:
- **P0행**(굵은 테두리 2pt·PURPLE·강조): M2 플로어플랜(설계열) · M3 부스설계 · M4 유틸리티 · M5 나노바나나. 각 칩에 대응 아이콘(`booth-layout`/`camera-render`/`wiring-power`/`ai-sparkle`).
- **P1행**(BLUE 1pt): M1(판매)·M16(사후, 2회 span)·M6/M7(발주)·M15 옥션(발주·강조 테두리)·M10(참가)·M12·M17(참가/사후)·M18(전열 하단)·§5B·§1A.
- **P2행**(SLATE): M8·M11·M13·M14(현장운영열 집중).
- **하단 레이어 띠 2줄**(y~5.8): ①BLUE_LT 풀폭 "공통/시스템관리 레이어(§5B) — 전 모듈 선행 기반" ②PURPLE_LT "멀티테넌시(§1A) — tenant_id 격리".
- **캡션**: "18개 모듈 + 공통 레이어를 한 장으로 조망. P0 4개가 심장." 9.5pt.
### ③ 나노바나나 파이프라인 플로우 (S20) ★WT-1
**컨셉**: 좌→우 8박스 수평 플로우. 전면 비동기를 "동기 대기 없음" 배지로 각인. AI 구간(워커·Gemini)만 PURPLE, 나머지 BLUE.
- **박스 8개**(y=2.6, 높이 0.95, 폭 ~1.35, gap 0.06):
1. `[사용자: 렌더 요청]` BLUE_LT (아이콘 `mobile`)
2. `[Spring 백엔드: RenderJob 생성·쿼터 확인]` WHITE/BLUE
3. `[Redis 큐 leftPush]` WHITE/BLUE (아이콘 `pipeline-flow`)
4. `[나노바나나 Python 워커 (BLPOP)]` PURPLE_LT/PURPLE (아이콘 `ai-sparkle`)
5. `[Gemini image-to-image (G1 승인·라이브)]` PURPLE_LT/PURPLE
6. `[워터마크 후처리 · S6 로컬 PIL 래스터]` PURPLE_LT/PURPLE (아이콘 `watermark`)
7. `[오브젝트 스토리지 적재]` WHITE/BLUE
8. `[WebSocket/STOMP → 프론트 완료 푸시·갤러리 갱신]` BLUE_LT
- 박스 사이 "→" 15pt SLATE.
- **분기 주석**(워커 하단 점선 콜아웃, AMBER 테두리): "degraded: G1 미승인/Gemini 장애 시 목(mock) 응답 — 구조·워터마크 유지". 옆에 "성공 시에만 쿼터 차감" GREEN 소배지.
- **보안 배지**(우상단): `shield-security` + "GEMINI_API_KEY = 워커 env only (백엔드 미취급)" 9pt RED 테두리.
- **라이브 배지**: 박스4·5에 GREEN "라이브" 소배지("이미 돈다").
- **캡션**: "이미지 생성 전면 비동기 — 사용자 동기 대기 없음, 평균 40초." 9.5pt.
### ④ Before/After 슬라이더 (S21)
- **좌우 분할 이미지 카드**(2.0,2.0,9.3,3.3): 좌반부=빈 부스(HALL_BG + 도면 라인 오버레이), 우반부=시공 후(BLUE→PURPLE 그라디언트 + 나노바나나 이미지 자리).
- **중앙 세로 드래그 핸들**(x≈6.65): 세로 WHITE 라인 3pt + 중앙 원형 핸들(WHITE OVAL + `` 양방향 화살표).
- **라벨**: 좌상단 "Before: 빈 부스 (도면/실측)" 칩 SLATE / 우상단 "After: 시공 후 예상 (AI 생성)" 칩 PURPLE.
- **접근성 배지**(하단): `check-verify` + "ARIA slider · 키보드 좌우 조작 지원 (WCAG AA)" 9pt.
- **워터마크 고지 배지**(맨 하단, RED 테두리): "AI 생성 예상 이미지 — 실제 시공과 다를 수 있음. 계약·심사 서류 사용 금지." 8.5pt.
- **우측 배선 색상 범례 미니**(S21이 배선 오버레이 겸함): 전기=RED / 네트워크=CYAN / 급배수=TEAL 3줄(색 사각+텍스트+선스타일 실선/파선/점선).
### ⑤ 3안 비교 화면 목업 (S24)
- **상단 홀 정보 바**(0.55,1.6,12.23,0.5) BLUE_LT: "제1전시장 5홀 · 판매가능 10,080㎡ · 통로규정 v3" 좌 + `booth-layout` 아이콘.
- **3열 카드**(각 3.9 폭, gap 0.28, y 2.25, 높이 3.3):
- 카드 상단 헤더바: "안 1 — 통로 효율 최대"(BLUE) / "안 2 — 프리미엄 부스 최대"(PURPLE) / "안 3 — 균형"(TEAL).
- 본문: 미니 배치도 썸네일(HALL_BG + 부스 폴리곤 격자 도형) + 지표 3행 "판매면적 ㎡ / 부스 수 / 규정위반 0건"(위반0=GREEN 체크 `check-verify`).
- 안2는 프리미엄 강조(굵은 부스 몇 개), 안3은 균등 격자.
- **하단 액션 바**(0.55,5.75,12.23,0.6): 버튼칩 3개 "[안 선택]" BLUE / "[구역·블록 병합 편집]" WHITE 테두리 / "[병합 후 재검증]" PURPLE.
- **캡션**: "제약 솔버가 상이한 최적화 목표로 정확히 3안 생성. 사람이 선택·병합·최종 확정." (AI 신뢰 3층 프레이밍).
### ⑥ PostGIS 배선 오버레이 (S27) ★WT-2
**컨셉**: 홀 평면도 위 실측 배선. 3색 규약 고정 + 색맹 안전(선 스타일 병기).
- **평면도 캔버스**(0.55,1.7,8.6,4.6) HALL_BG 라운드:
- **부스 폴리곤** 다수: 연회색(`#D8DEE8`) 사각 격자 도형.
- **트렌치 포인트** ◆: TEAL 다이아 소도형, 라인 따라 배치.
- **분전반** ▣: INK 사각 + 라벨.
- **배선 LineString 3색**(부스→최근접 트렌치→분전반):
- 전기 = `RED` 실선 2pt
- 네트워크 = `CYAN` 파선(dash 6 3)
- 급배수 = `TEAL` 점선(dot 2 3)
- **가정 트렌치**엔 AMBER "가정 좌표(실측 대기)" 소배지.
- **우측 범례·주석 패널**(9.35,1.7,3.4,4.6) WHITE 카드:
- 제목 "배선 색상 범례" + 3색 행(색+선스타일+라벨).
- 주석 배지 3개: "최근접 트렌치 KNN (geom <-> point)" / "최단 경로 ST_Length · 통로 횡단 최소" / "SRID 0 홀 로컬 미터 · GiST 실시간".
- 아이콘 `wiring-power`·`postgis-spatial`.
- **캡션**: "수기 위치표시도 폐지 — 좌표 클릭 자동 작도."
### ⑦ 옥션 폐루프 흐름도 (S29) ★WT-3
**컨셉**: 좌측 AI 자료 패키지 스택 → 우측 순환 흐름. "AI 자료가 곧 발주 근거"를 좌→우 물리 연결로.
- **좌측 AI 자료 패키지 스택**(0.55,2.0,2.6,3.0) PURPLE_LT 카드, 4겹 쌓임 효과:
- "M2 배치 + M3 설계 + M4 배선/BOQ + M5 시공예측 이미지" 각 줄, 아이콘 `booth-layout`/`camera-render`. 상단 `ai-sparkle`.
- **우측 흐름 7박스**(순환 배열 또는 좌→우 2행 지그재그, y 2.0~5.5):
1. `[옥션 개설 (역경매/RFQ/고정가)]` BLUE (`auction-gavel`)
2. `[등록업체 검증 게이트: 미등록 응찰 차단 403]` RED 테두리 강조 (`shield-security`)
3. `[견적서 제출 · PDF · 버전]` WHITE/BLUE (`document-rtm`)
4. `[실시간 순위 · 라운드 마감 (Redis 정렬셋·WS 델타)]` BLUE (`dashboard-chart`)
5. `[종합평가 낙찰 (가격+평판+납기)]` PURPLE (`check-verify`)
6. `[Award → 계약/발주 자동전환 (M6·M9)]` TEAL
- 6→다시 자료로 순환 화살표(`closed-loop` 곡선 화살표).
- **게이트2 강조**: RED "미등록 = 응찰 원천 차단" 배지 — 킨텍스 규정 시스템 강제 포인트.
- **캡션**: "AI 설계자료가 곧 발주 근거. 사진 수준 자료로 응찰→발주까지 한 흐름."
### ⑧ 종합평가 스코어 비교표 (S31)
- **표**(0.55,2.0,12.23): 헤더 BLUE 풀폭 — 열: "업체명 | 견적금액(점수) | 평판(점수) | 납기(점수) | **가중 종합점수** | 순위".
- **행 4~5개**(zebra): 예 A사/B사/C사/D사. 각 셀 숫자 + 괄호 환산점수.
- **가중치 주석 바**(표 상단 또는 하단): "가중치(예): 가격 0.6 / 평판 0.25 / 납기 0.15 — 옥션별 설정형(weight jsonb)" 9pt MUTED.
- **1위 행 강조**: BLUE_LT 배경 + Bold + 우측 "낙찰(Award)" GREEN 배지 + `check-verify`.
- **우측 또는 하단 캡션 카드**: "선정 사유·항목별 비교 근거 자동 산출". 종합점수 열은 막대 미니바(가중합 시각화, BLUE 그라데이션).
- **캡션**: "가격만이 아니라 평판·납기 가중 — 투명·최적 발주."
### ⑨ 시스템 아키텍처 다이어그램 — 전 계층 (S41)
**컨셉**: 개발계획서 아키텍처 밴드도 계승·확장. 상하 6밴드 + 우측 외부 게이트 컬럼.
- **밴드**(좌측 라벨칩 1.55폭 + 본문, 폭 12.23):
1. **엣지/DMZ**(y~1.5): `[CDN·정적캐시]` `[WAF/Reverse Proxy(nginx)·TLS·레이트리밋]` BLUE_LT.
2. **역할별 프론트**(y~2.15): organizer·exhibitor·contractor·ops·admin(내부 배지 회색)·public/visitor(공개 SSR/SEO/다국어 배지). 6노드.
3. **SSO·RBAC**(y~2.8): `[JWT SSO + TOTP 2FA · 이중 RBAC(플랫폼/행사)]` 풀폭 (`shield-security`).
4. **공유 백엔드**(y~3.45, 높이 늘림): `[REST + WebSocket/STOMP]` 상단 + 하위 `[룰엔진]` `[배치·배선 엔진·PostGIS]` `[옥션 M15]` `[BI M16]` `[CMS M17]` `[공개 API read-only]`.
5. **비동기**(y~4.5): `[Redis 큐·순위·타이머·캐시·쿼터]``[나노바나나 워커]`(PURPLE) `[서류·EDM 워커]`.
6. **데이터**(y~5.15): `[PostgreSQL+PostGIS]` `[읽기복제(BI·공개)]` `[오브젝트 스토리지]`.
- **우측 외부 게이트 컬럼**(x~11.4 세로): `[Gemini(G1·라이브)]` `[Claude(AiTextRouter)]` `[PG 결제]` `[kxwp 릴레이]` `[등록업체 DB 739]`. 라이브=GREEN 배지.
- **연결 주석**(얇은 선+라벨): 워커→Gemini "egress only" / 워커→백엔드 "WebSocket 완료 푸시" / 백엔드→읽기복제 "BI/공개 오프로드".
- 계층별 색: 표현=BLUE_LT, 백엔드=WHITE/BLUE, AI밴드=PURPLE_LT, 데이터=HALL_BG.
### ⑩ 보안영역 분리도 (망 구성) (S47) ★WT-4
**컨셉**: 좌→우 신뢰도 상승 존. 철칙 배지를 굵게. `network-zone` 메타포.
- **존 박스 6개**(좌→우, 각 폭 ~2.0, y 2.2 높이 3.0, 우로 갈수록 테두리 진하게):
1. `[인터넷]` SLATE
2. `[엣지: CDN·WAF·DDoS]` BLUE_LT
3. `[DMZ(공개): 공개LB·public SSR·visitor GW·공개 API·PG콜백 — 쓰기권한 없음]` BLUE
4. `[내부망(인증): 내부LB·SSO/2FA·내부 API·공유 백엔드]` BLUE_DK
5. `[AI 워커존(egress 제한): Redis·나노바나나·Ollama]` PURPLE
6. `[데이터존(최내곽): PostgreSQL+PostGIS·오브젝트·읽기복제 — 아웃바운드 전면 차단]` INK
- 존 사이 화살표 "HTTPS만" 라벨.
- **상단 관리존**(가로 띠): `[관리존: 배스천·관측성 — VPN/허용IP+2FA, 인터넷 미도달]`.
- **철칙 배지 3개**(하단, RED 굵은 테두리): "DMZ → 데이터존 직접 접근 절대 금지" / "데이터존 아웃바운드 0 (유출경로 제거)" / "admin = VPN/허용IP+2FA".
- **egress 컬럼**(우측): "승인 4목적지만 — Claude·Gemini·PG·SMTP / 그 외 DROP" (`shield-security`).
- **캡션**: "공개/내부 물리·논리 분리 · 데이터존 무egress로 유출경로 자체 제거."
### ⑪ 멀티테넌시 격리 아키텍처도 (S56)
**컨셉**: 수직 4층 방어 스택. fail-closed를 "한 층이라도 불일치 = 차단" 배지로.
- **좌측 진입**(1.0,2.0,3.2,1.4): `[요청: 서브도메인(kintex./coex.) + JWT 사용자 소속]``[테넌트 컨텍스트 해소: 불일치 시 거부]` (`tenant-building`).
- **중앙 4층 방어 스택**(위→아래, 각 폭 5.0, 높이 0.85, y 2.0~5.5):
1. `[①컨텍스트 필터]` BLUE_LT
2. `[②서비스 가드]` BLUE
3. `[③MyBatis 공통 인터셉터 (tenant_id 자동 주입)]` BLUE_DK
4. `[④PostGIS/쿼리: tenant_id 선두 복합 인덱스]` PURPLE
- 층 사이 아래 화살표.
- **우측 데이터 표현**(9.3,2.0,3.4,3.5) WHITE 카드: `[TB_* … WHERE tenant_id = ctx]` 격리 도형 — KINTEX(#1)·COEX(#2) 두 색 파티션 시각화.
- **하단 배지**(RED): "fail-closed — 한 층이라도 불일치면 차단".
- **캡션**: "KINTEX #1 검증 → COEX #2 코드 배포 없이 데이터 온보딩. 기능 불변, 데이터·권한만 분리."
### ⑫ 일정 간트 (Phase × 모듈) (S58)
**컨셉**: 개발계획서 간트 계승. 3구간 + 라이브 실적(GREEN 완료)으로 "이미 돈다" 실증.
- **타임라인**(gx=3.55, gw=9.15, 3구간 Phase1/2/3 각 ~4M 눈금).
- **상태 범례**(우상단): 완료 GREEN / 진행 AMBER / 예정 SLATE.
- **워크스트림 막대 행**(좌측 Phase+워크스트림 라벨, 우측 막대):
- Phase1: 아키텍처(A·완료 GREEN) / 공통레이어 B 2FA·시스템관리(완료 GREEN) / P0 코어 M2·M3·M4·M5(진행 AMBER, 하위 "스캐폴드·인증·매퍼·룰엔진·나노바나나=완료 GREEN" / "3안·병합·실데이터=진행 AMBER").
- Phase2: 옥션 M15 / 관람 M10 / BI M16 / 관리자 M18 / 판매·서류·정산 M1·M6·M7·M9 / CMS M12·M17 (예정 SLATE).
- Phase3: 멀티테넌시 / M11·M13·M14 / 배포 안정화·SM 이관 (예정 SLATE, 배포 일부 진행 AMBER).
- **마일스톤 다이아몬드**(◆): G1(나노바나나 승인·완료 GREEN) · G2(배포서버·완료 GREEN) · Phase 게이트 · QA 게이트 · 오픈.
- **현재 시점 마커**: PURPLE 세로선 + "현재".
- **캡션**: "선행 게이트 통제 + Phase 게이트 QA 통과 후 진행. G1·G2 완료 — 실증 기반." (`schedule-gantt`).
---
## D. 표준 필수 도식 3종
### ⑬ 추진체계도 (조직도) (S15)
- **최상위**(중앙 4.9,1.5,3.55,0.72) BLUE: "프로젝트 총괄 (PM)" / "범위·일정·리스크·게이트(G1·G2)" (`org-people`).
- **2단**(연결선 아래): "개발 PM"(PURPLE) / "PMO"(PURPLE).
- **3단 아키텍트 그룹**(BLUE_LT 띠): AA·SA·TA·DA·NA 5노드(WHITE/BLUE) — "앱/시스템·NFR/기술표준/데이터·ERD/네트워크·보안".
- **4단 구현 그룹**(PURPLE_LT 띠): 공통/시스템·백엔드·프론트·DB·도메인(bidding·visitor·cms·bi·admin)·AI·시각화 6노드(WHITE/PURPLE).
- **하단**: 품질(kintex-qa · GREEN) / 배포(devops-dev · 딥블루).
- 개발계획서 조직도와 동일 구조 재사용 — 제안서용으로 "발주처 보고 라인" 소표기 추가 가능.
### ⑭ 리스크 매트릭스 (S59)
**컨셉**: 발생가능성 × 영향도 2축 4사분면 버블 + 게이트.
- **매트릭스**(2.5,1.8,6.5,4.5): x=발생가능성(낮→높), y=영향도(낮→높). 4사분면 배경 명도(우상=RED_LT 위험, 좌하=GREEN_LT 안전).
- **버블**(R-1~R-8 배치, tech_advisory/strategy §4):
- R-1(나노바나나 미승인) 고영향·중발생 → 우상 근처, AMBER.
- R-2(CAD·트렌치 미확보) 고영향·중발생 AMBER.
- R-3(kxwp 폐쇄 API) 중·중 SLATE.
- R-4(배점 가정) 중·중, R-5(간판 품질) 중·중, R-6(AI 환각) 고·저, R-7(HW 연동) 중·저, R-8(확장 부담) 저·저 GREEN.
- 버블 크기=영향도, 색=위험등급, 라벨 "R-n" + 툴팁 텍스트 소자.
- **우측 게이트·대응 패널**(9.3,1.8,3.4,4.5) WHITE 카드: "선행 게이트 G1(완료)·G2(완료)" + "각 리스크 방어 논리 1줄" 스크롤 리스트 + `check-verify`/`risk-warning`.
- **캡션**: "약한 고리를 선제 노출·방어 — 숨기지 않아 신뢰."
### ⑮ 기대효과 정량 인포그래픽 (S61)
- **5지표 Before/After 카드**(5열, 각 2.4 폭, y 1.85, 높이 1.85) — 개발계획서 기대효과 카드 계승:
| 지표 | As-Is(SLATE) | ↓(PURPLE) | To-Be(BLUE) |
|---|---|---|---|
| 홀 배정 견적 | 수일 | | 즉시 |
| 배치도 초안 | 수일~수주 | | 수 분 |
| 규정 검수 | 육안 검토 | | 자동 플래깅 |
| 유틸리티 신청 | 수기 작도 | | 클릭+자동견적 |
| 시공 결과 예측 | 불가(외주) | | 표준 샷 자동생성 |
- 각 카드: 지표명 Bold + As-Is(회색) + ↓ 화살표 PURPLE + To-Be(BLUE Bold) + 대응 아이콘(`document-rtm`/`booth-layout`/`check-verify`/`wiring-power`/`camera-render`).
- **하단 임팩트 지표 스트립**(대형 수치): "40초 렌더 · 99.5% SLO · 142 REQ 커버 · 18 모듈" BLUE/PURPLE 교차 KPI 카드.
- **캡션**: "리드타임 수일→즉시·수분, 검수 육안→자동. 정량 개선을 숫자로 각인."
---
## E. deck-designer 작도 체크리스트
1. **색 규율**: 퍼플=AI/나노바나나 전용. 배선 3색 고정(전기 RED 실선 / 네트워크 CYAN 파선 / 급배수 TEAL 점선) — 색+선스타일 이중 인코딩(색맹 안전).
2. **라이브 배지(GREEN "이미 돈다")**: 나노바나나 파이프라인(③)·간트(⑫)·아키텍처(⑨) 외부게이트에 반드시 표기 — tech_advisory §0 실증 우위.
3. **워터마크 고지**: Before/After(④)·AI 이미지 보안 슬라이드에 RED 테두리 고지 배지 필수(REQ-S-009).
4. **캡션 1줄**: 12종 각 도식 하단에 tech_advisory 지정 캡션 그대로. MUTED 9~9.5pt.
5. **아이콘**: `assets/icons/*.svg` 20종 — 헤더 앵커는 도형 근사, 콘텐츠 아이콘은 PNG 변환 후 stroke=맥락색. 이모지 금지.
6. **가독성**: 본문 10pt 하한, 한 슬라이드 1 도식/1 메시지, 여백 확보. 도식 박스 라벨 8~10pt.
7. **배점 규율 시각화**: 간지 배점 배지(P3=PURPLE 35점 최강조), 모듈맵 P0 굵은 테두리.
8. **보안**: 시크릿·내부IP·키값 도식·라벨 미기재. 외부 API는 "승인 예외/env only"로만.

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
# 킨텍스 AI 전시관리 시스템 — UI 디자인 스펙 (design.md)
> 작성: 디자인 에이전트(designer) · 작성일: 2026-07-12 · 버전: **v2.3.2**
> 작성: 디자인 에이전트(designer) · 작성일: 2026-07-12 · 버전: **v2.4**
> 근거 문서: `docs/PLANNING.md` **v3.0**(모듈 맵 M1~M18 + §5B 공통/시스템관리 레이어 + §1A 멀티테넌시, 6+2 역할, 나노바나나 표준 샷 세트 S1~S7) · `docs/FEATURE_BACKLOG_100.md`(100대 기능 F001~F100·NFR 4축) · Stitch 산출물 `stitch_kintex_ai_system_architect/`(18화면 + DESIGN.md 3변형) 학습·정합
> **단일 출처(Single Source of Truth)**: 프론트 구현 토큰·컴포넌트 계약은 본 문서가 권위다. Stitch 화면(code.html/screen.png)은 시각 참조로만 사용하며, 상충 시 본 문서를 따른다.
> 용도: **각 화면의 "Stitch 프롬프트" 코드블록을 Google Stitch(stitch.withgoogle.com)에 그대로 붙여넣어 화면을 생성**한다. 문서는 한국어, Stitch 프롬프트만 영어.
@ -1468,6 +1468,109 @@ A public "check & cancel my ticket booking" page for an exhibition, Korean marke
>
> **디자인 토큰**: §1 라이트 테마·`primary-600 #0066B3`·Pretendard·8px 카드·WCAG AA 준수. **신규 토큰 0.** AI 생성 이미지(나노바나나)는 워터마크 "AI 생성 예상 이미지" 상시 노출 규칙 유지. 이모지 금지·선(stroke) SVG 아이콘.
### 3C-V. 공개 사이트 비주얼·모션 스펙 (전 SCR-T*·SCR-P* 공통 비주얼 레이어 · v2.4 신설)
> **v2.4 신설.** 소유자 확정("코엑스처럼 세련되게 전면 비주얼 리뉴얼 — 히어로 배경·애니메이션·비주얼, 지금 착수")에 따라, 기존 §3C 3-트랙(SCR-T0~T3)·§3B 공개 홍보(SCR-P*)의 **구조는 그대로 두고 그 위에 비주얼·모션 레이어만 얹는다**(구조 재설계 아님, 시각 격상). 대상: **미인증 공개 트랙 전체**(SCR-T0 관문·SCR-T1 visitor 메인·SCR-T2 business·SCR-T3 agency·SCR-P1~P8). **인증 업무 셸(AppShell/MDI)·관리자(SCR-A*)·모바일 앱은 대상 아님**(밀도·성능 우선 원칙 유지). **신규 컬러 토큰 0** — §1 팔레트를 CSS 변수로 주입하고, 본 절은 **모션 토큰·이미지 소스·비주얼 폴리시**만 신설한다. business/agency는 코엑스식 B2B 실무 톤을 유지하되 히어로 배경·리빌·카드 hover 등 **공통 모션만 절제 적용**(마케팅 과장 금지) — visitor(SCR-T1)만 풀 시네마틱 격상.
**V-1. 비주얼 폴리시(공통 원칙)**
- **풀블리드·공백 없음**(전역 NFR [[ui-fullbleed-responsive]] 준수): 히어로·밴드는 뷰포트 전폭, 좌우 거터 여백 금지. 콘텐츠는 `max-width 1440px` 중앙 정렬 그리드 안에서 리듬을 준다(밴드 배경만 전폭).
- **섹션 리듬**: 밴드(전폭 배경/딤) ↔ 카드 그리드(라이트 서피스) 교대. 섹션 세로 패딩 데스크톱 `96px`(태블릿 64px·모바일 40px), 섹션 헤더는 아이라인(eyebrow, 12px 트래킹 라벨)+H2. 인접 섹션 배경 톤을 번갈아 대비.
- **여백·라운드·그림자 토큰**(§1-4 재사용): 카드 라운드 8px 상한, 칩 pill. 그림자는 무거운 박스섀도 대신 **소프트 앰비언트**(hover 시에만 Level 2로 상승, Y8 Blur24 12%). 히어로/밴드 위 텍스트는 딤 오버레이로 대비 확보.
- **타이포 위계 강화**: 히어로 헤드라인 Display(1순위 visitor는 `clamp(32px,4vw,56px)` 확대 허용), 서브카피 H3, 섹션 eyebrow+H2. 숫자(통계·D-day·기간)는 tabular numerals.
- **선(stroke) SVG 아이콘 전용·이모지 금지**(불변). AI 산출물은 §1-4 AiLabel(보라)+워터마크 규칙 유지.
- **다크/라이트 양 테마**(SCR-T1은 토글 제공 — §3C SCR-T1 예외 규정): 히어로는 시네마틱 딤 톤(다크 계열 오버레이)이 기본 인상, 본문 섹션은 라이트 기본. 색은 CSS 변수 주입(hex 하드코딩 금지)로 양 테마 대응, **양 테마 WCAG AA**.
- **포스터/이미지 카드 정책**: visitor 쇼케이스·행사 레일은 큰 세로 2:3 포스터 카드 그리드/캐러셀. `poster_url`/배경 이미지 부재 시 **브랜드 그라디언트 + 옅은 기하 패턴(그리드/도트) 폴백**(V-4).
**V-2. 모션 토큰(신설 — duration/easing, CSS 커스텀 프로퍼티)**
| 토큰 | 값 | 용도 |
|---|---|---|
| `--motion-fast` | `150ms` | hover 색/보더, 칩 상태 |
| `--motion-base` | `240ms` | 카드 리프트·이미지 줌·버튼 |
| `--motion-slow` | `400ms` | 스크롤 리빌 기본 |
| `--motion-hero` | `600ms` | 히어로 인트로·대형 리빌 |
| `--motion-countup` | `1200ms` | 숫자 카운트업 |
| `--ease-emphasized` | `cubic-bezier(0.2,0,0,1)` | 리빌·히어로(감속 강조) |
| `--ease-standard` | `cubic-bezier(0.4,0,0.2,1)` | 일반 hover/전환 |
| `--ease-decelerate` | `cubic-bezier(0,0,0.2,1)` | 진입(카운트업·슬라이드인) |
**V-3. 섹션별 배경·애니메이션 트리거 매핑**
| 섹션(적용 화면) | 배경 | 애니메이션 | 트리거·파라미터 |
|---|---|---|---|
| **풀블리드 히어로**(T0·T1·T2·T3·P1) | `/media/hero/*.jpg`(전시장 전경) + 딤 그라디언트 오버레이 + AI 워터마크. 없으면 **그라디언트+패턴 폴백** | ① 인트로 리빌(로드 시 헤드라인·CTA opacity+translateY(16px)→0, `--motion-hero`) ② **라이트 파랄랙스**(배경 레이어 scroll에 `translate3d(0,y*0.18,0)`, 전경/텍스트는 고정 또는 y*0.06) | 인트로=마운트 1회 · 파랄랙스=scroll(rAF 스로틀, 데스크톱만·모바일/reduced-motion 비활성) |
| **통계 밴드**(T1·P1·P2) | 라이트 서피스 또는 밴드 딤 | **숫자 카운트업**(0→목표, `--motion-countup`, ease-decelerate) | IntersectionObserver 최초 진입 1회(`.pub-countup[data-count-to]`) |
| **AI 기능 쇼케이스 타일**(T1) | 라이트, AI 카드 `ai-surface #F5F3FF` | **스태거 리빌**(카드 순차 fade/slide-up 60~80ms 간격) + hover 리프트 | 컨테이너 진입 시 스태거 1회 · hover=`--motion-base` |
| **전시 쇼케이스/행사 레일**(T0·T1·P1) | 라이트 | ① 카드 **hover 리프트**(translateY(-4px)+그림자) + **이미지 줌**(scale 1.04, overflow hidden 컨테이너) ② 레일 **오토스크롤**(가로) | hover=`--motion-base` · 오토스크롤=`data-interval`(기본 4500ms), hover/focus 시 일시정지, reduced-motion 시 미동작 |
| **AI 기획 브리핑(연/월)**(T1) | 라이트 | 타임라인 바 진입 시 좌→우 grow(scaleX) + AI 요약 태그 fade-in | 진입 1회 · 연/월 전환 시 콘텐츠 크로스페이드(`--motion-base`) |
| **방문 가이드/가치제안 카드**(T1·T2·T3) | 라이트 | 스태거 리빌 + hover 리프트 | 진입 스태거 1회 |
| **전시장 소개/CTA 밴드**(T1·T2·T3·P2) | `/media/bands/*.jpg`(전폭 밴드) + 딤. 없으면 그라디언트 폴백 | 밴드 진입 리빌(fade/slide-up) · 배경 미세 파랄랙스(선택, 약 y*0.08) | 진입 1회 |
| **공통 섹션 블록 전반** | — | **스크롤 리빌**(opacity 0→1 + translateY(20~24px)→0, `--motion-slow` ease-emphasized) | IntersectionObserver `threshold 0.15`·`once`(재역방향 재생 금지) |
**V-4. 이미지 소스 매핑(`/media/` — build 내장, 폴백 규칙)**
- **원칙**: 히어로/밴드 비주얼은 **프론트 build 내장 정적 에셋 `/media/`** 에서 로드(외부 실시간 호출 없음·온프레미스, CDN 캐시). 나노바나나 AI 생성 전경은 워터마크 "AI 생성 예상 이미지" 상시. 파일 부재 시 **그라디언트+패턴 폴백**으로 무공백 렌더(레이아웃 시프트 0).
- **디렉터리 계약(제안)**:
- `/media/hero/` — 풀블리드 히어로 배경. `hero-gateway.jpg`(T0)·`hero-visitor.jpg`(T1)·`hero-business.jpg`(T2)·`hero-agency.jpg`(T3) (+`@2x`·`.webp` 우선, `.jpg` 폴백)
- `/media/hero/fg/` — 라이트 파랄랙스용 전경 레이어(선택, 투명 PNG/webp)
- `/media/bands/` — 섹션 밴드 배경(`band-venue.jpg` 전시장 소개 등)
- `/media/poster/` — 행사 포스터 폴백 placeholder(그라디언트 생성)
- `/media/patterns/` — 폴백 패턴 SVG(`grid.svg`·`dots.svg`·`mesh.svg`, `fill:none;stroke:currentColor` 라인 패턴)
- **로딩 성능**: 히어로 이미지는 `<link rel="preload">`+`fetchpriority="high"`·`loading="eager"`, 폴드 하단 포스터/밴드는 `loading="lazy"`+`decoding="async"`. `srcset`/`sizes`로 DPR·뷰포트 대응, `.webp` 우선 `<picture>`.
- **폴백 그라디언트**: `linear-gradient(135deg, var(--primary-700), var(--primary-600))` + 패턴 SVG 오버레이(투명도 8~12%). 포스터 폴백은 그라디언트 위 행사명 텍스트 중앙(SCR-HOME 폴백 규칙과 동일).
**V-5. 접근성(prefers-reduced-motion) — 필수**
- `@media (prefers-reduced-motion: reduce)`: **파랄랙스 전면 비활성**(배경 고정)·**오토스크롤 정지**(수동 화살표만)·**카운트업은 최종값 즉시 표시**·스크롤 리빌은 **transform 제거·opacity 1 즉시**(콘텐츠 지연 노출 금지)·hover 대형 transform 억제(색/보더 전환만 허용).
- 애니메이션은 **전부 장식**이며 정보 전달·조작 가능성을 모션에 의존하지 않는다(모션 꺼도 전 콘텐츠·기능 동등 접근). 포커스 링(`primary-600` 2px)·`aria-live`(AI 답변) 규정은 §3C 원본 유지.
- 히어로 배경 위 텍스트는 딤 오버레이로 **양 테마 WCAG AA** 대비 확보. 색만으로 상태 전달 금지(배지 텍스트 병기).
- 모션 정지 시에도 레이아웃 시프트 0(리빌은 최종 위치 기준, 이미지 폭·높이 예약).
**V-6. 성능 가이드(CSS 우선·60fps)**
- **CSS 우선**: 스크롤 리빌·hover·오토스크롤은 **CSS `transition`/`@keyframes` + IntersectionObserver로 클래스 토글**(`.is-visible`)로 구현. 애니메이션 라이브러리 불필요(기본 리빌엔 JS 애니메이션 금지). 파랄랙스만 `requestAnimationFrame` 스로틀 스크롤 핸들러(passive listener).
- **컴포지터 친화 프로퍼티만 애니메이트**: `transform`·`opacity`만 사용. `width/height/top/left`·`box-shadow` 직접 애니메이트 금지(그림자는 pseudo-element opacity 트랜지션으로 대체). `will-change`는 애니메이션 직전 부여·종료 후 제거(상시 부여 금지).
- **60fps 목표**: 파랄랙스 계산 rAF 1프레임 1회·오프스크린 요소 무시. 오토스크롤은 `transform: translateX` + `ScrollTimeline` 또는 rAF, hidden 탭에서 정지(`visibilitychange`).
- **이미지**: 위 V-4 preload/lazy·webp·srcset. 폴드 상단 CLS 방지(에스펙트 비율 예약). 저사양/데이터세이버(`prefers-reduced-data`) 시 히어로 정적·패턴 폴백으로 강등.
- **60fps 미달 시 강등 순서**: 파랄랙스 → 오토스크롤 → 스태거(→ 최종 리빌만 유지). 리빌은 항상 유지 가능한 최소 비용.
**V-7. 프론트 구현 계약(클래스/훅 ↔ 모션 매핑 — kintex-frontend-dev)**
- **CSS 커스텀 프로퍼티**(공개 트랙 루트에 주입): `--motion-fast/base/slow/hero/countup`, `--ease-emphasized/standard/decelerate`(V-2 값). §1 컬러도 동일 방식 변수 주입(라이트/다크).
- **`.pub-reveal`** + IntersectionObserver로 `.is-visible` 토글 → opacity+translateY 리빌(공통 섹션 블록·카드). **`.pub-reveal--stagger > *`** = 자식 순차 지연(`--i` 인덱스 * 70ms).
- **`.pub-hero`** > **`.pub-hero__bg`**(파랄랙스 레이어, `data-parallax="0.18"`)·`.pub-hero__fg`(전경 y*0.06)·`.pub-hero__overlay`(딤). 인트로는 마운트 시 `.is-in`.
- **`.pub-card--lift`**(hover translateY(-4px)+그림자 pseudo) · **`.pub-card__media`**(overflow hidden) > `img`(hover `scale(1.04)`).
- **`.pub-countup`**(`data-count-to`·`data-suffix`) = IO 진입 1회 카운트업, reduced-motion 시 최종값.
- **`.pub-rail--auto`**(`data-interval`) = 오토스크롤 레일(hover/focus/visibility 정지, reduced-motion 미동작).
- **폴백 유틸**: `.pub-fallback-gradient`·`.pub-pattern-grid|dots`(V-4). 이미지 `onerror`→폴백 클래스 토글.
- **적용 범위 가드**: 위 클래스·훅은 **PublicShell 하위(공개 트랙)만** 스코프. AppShell/MDI·SCR-A*·모바일 앱에는 미주입(성능·밀도 보존).
- **src 무수정**: 본 절은 스펙만 — 실제 클래스/CSS 구현은 kintex-frontend-dev 후속.
**V-8. Stitch 프롬프트 — 프리미엄 전시장 공개 홈페이지(비주얼·모션 격상판, SCR-T1 재생성용)**
```
A premium, visually striking public homepage for "KINTEX AI 전시·행사시스템" (an AI exhibition & event operations platform), the visitor track, for the general public — styled like a world-class convention-center website (COEX / Messe / global expo caliber). Korean UI text only, exactly as given; desktop 1440px, full-bleed responsive with NO empty side gutters; support BOTH light and dark theme (keep body sections light by default, but render the hero as a cinematic dark-tinted centerpiece). Primary blue #0066B3, deep #004C86, AI purple #6D4AFF; use line (stroke) SVG icons only, NO emoji. Product name must read exactly "KINTEX AI 전시·행사시스템".
HERO (full-bleed, cinematic, the centerpiece): a large photorealistic exhibition-hall panorama photograph as the background, covered by a subtle dark-to-transparent gradient overlay for text contrast and a faint corner watermark "AI 생성 예상 이미지". Give it depth like a light parallax scene (a softly blurred far background plus a crisper foreground layer). Over it, a strong typographic hierarchy: a large display headline "무엇이든 물어보세요 — AI가 관람을 안내합니다", a subheading "전시 일정 · 오시는 길 · 입장권 · 부스 위치를 대화로 한 번에", and the visual focus — a wide conversational AI assistant search bar with a small purple "AI 도우미" tag and a send (paper-plane) icon, plus example question chips "이번 주말 로봇 전시 있어? · 킨텍스 주차 어디에 해요? · 가족과 볼만한 전시 추천". Under the hero: two CTAs "사전등록" and "입장권 예매", and a subtle animated scroll-down cue.
Design should imply tasteful, restrained, performant motion: sections fade / slide up on scroll, KPI numbers count up, poster cards lift with a soft shadow and their images zoom on hover, and a poster rail auto-scrolls.
Below the hero, with generous section rhythm (alternating full-bleed bands and light card grids): (1) a stats band "누적 전시 1,200+ · 참가업체 30,000+ · 연 관람객 500만+" in big tabular numerals (implying count-up). (2) an "AI가 도와드립니다" capability showcase of 46 cards, each with a purple "AI" tag, a stroke icon, a title and one line ("AI 관람 도우미", "AI 전시 추천", "AI 길찾기", "AI 일정 도우미", "AI 혼잡·대기 안내", "AI 시공 예상 이미지"). (3) an "AI 전시·행사 기획 브리핑" section with a "연간 / 월간" toggle and a 12-month timeline plus a purple "AI 요약" headline. (4) a rich "진행 중 · 예정 전시" showcase — a grid/carousel of large 2:3 exhibition poster cards (poster image, title, dates "2026.09.0509.08", hall label, a status badge, a "D-25" day chip, "사전등록"), with tabs "진행 중 | 이번 달 | 다음 달" and a venue filter "전체 · 제1전시장 · 제2전시장". (5) a "방문 가이드" 2×3 grid of stroke-icon cards ("교통 · GTX-A 킨텍스역 도보 3분", "주차 · 실시간 안내", "실내 길찾기", "편의시설", "접근성 · 휠체어·수어통역", "고객문의"). (6) a full-width "전시장 소개" band with a background photograph and KINTEX scale figures. Footer with company info, "오시는 길 · GTX-A 킨텍스역", SNS, language.
Where poster or hero/band images are missing, fall back to a branded blue gradient with a faint geometric grid/dot line-pattern overlay (no blank areas, no layout shift). Sophisticated, immersive, conversion-oriented global exhibition-center aesthetic; rounded 8px cards, soft ambient shadows, tabular numerals, strong type hierarchy, WCAG AA contrast in BOTH themes; all motion must respect prefers-reduced-motion (parallax and auto-scroll disabled, reveals instant when reduced).
```
**V-9. Stitch 프롬프트 — 3-트랙 관문 비주얼 격상판(SCR-T0 재생성용, 동일 톤)**
```
Re-generate the "track gateway" landing page for "KINTEX AI 전시·행사시스템" with a premium, cinematic visual upgrade in the same tone as the visitor homepage. Korean UI only, desktop 1440px, full-bleed (no side gutters), light theme body with a dark-tinted cinematic hero; primary blue #0066B3, AI purple #6D4AFF; line (stroke) SVG icons only, NO emoji.
Full-bleed HERO: a photorealistic exhibition-hall panorama background with a dark-to-transparent overlay and a faint watermark "AI 생성 예상 이미지", light-parallax depth, a large display headline "KINTEX AI 전시·행사시스템", a subheading "전시 기획부터 시공·관람까지 전 과정을 AI로 자동화", and CTAs "관람 사전등록" and "참가 문의". Keep the top header with the wordmark, the 3-tab track switcher "관람 · 비즈니스 · 협력사" (none active), and utilities "언어 · 로그인".
Directly below, three large branching track cards in one row that lift on hover with an image zoom — card 1 "관람객" (slightly larger, primary) "행사 일정 · 사전등록 · 오시는 길" button "관람 트랙 →"; card 2 "주최 · 참가 (비즈니스)" "대관 · 자동견적 · 부스 신청 · 시공 시안" button "비즈니스 트랙 →"; card 3 "공사 · 협력사 (에이전시)" "등록업체 · 공사 옥션 · 작업신고" button "협력사 트랙 →". Then a "진행 중 · 예정 전시" auto-scrolling rail of 2:3 poster cards (poster, title, dates, hall, "D-25" chip, "사전등록"). Sections fade/slide up on scroll. Footer with "오시는 길 · GTX-A 킨텍스역". Missing images fall back to a branded gradient with a faint grid/dot pattern. Rounded 8px cards, soft shadows, tabular numerals, WCAG AA; all motion respects prefers-reduced-motion.
```
### 3C-0. 공개 셸(PublicShell) + 3-트랙 스위처 (전 SCR-T*·SCR-P* 공통 크롬)
- **재사용 전제**: SCR-P*가 쓰는 **PublicShell**(전통 페이지 헤더 GNB·푸터·SSR/CDN·MDI 비적용)을 그대로 재사용하되, **상단에 3-트랙 전환 탭(TrackSwitcher)을 추가**한다. 관문(SCR-T0)은 트랙 미선택 상태, 서브홈(T1~T3)은 해당 트랙 탭 활성.
@ -1805,6 +1908,7 @@ A mobile "my ticket wallet" screen for an exhibition visitor, 390px width, light
| 버전 | 일자 | 작성자 | 내용 |
|---|---|---|---|
| **v2.4** | 2026-07-12 | designer | **공개 사이트 비주얼·모션 전면 리뉴얼 스펙 신설(소유자 확정: "코엑스처럼 세련되게 — 히어로 배경·애니메이션·비주얼, 지금 착수").** §3C에 **3C-V 공개 사이트 비주얼·모션 스펙** 절 순증 — 기존 SCR-T0~T3·SCR-P* **구조는 불변**, 그 위에 비주얼/모션 레이어만 격상(구조 재설계 아님). ①**비주얼 폴리시(V-1)** 풀블리드·공백 없음·섹션 리듬(밴드↔카드 교대·96/64/40px)·여백·라운드 8px·소프트 앰비언트 그림자·타이포 위계 강화·선 SVG·이모지 금지·다크/라이트·2:3 포스터 카드. ②**모션 토큰(V-2)** duration(`--motion-fast 150`/`base 240`/`slow 400`/`hero 600`/`countup 1200`)·easing(`--ease-emphasized/standard/decelerate`) 신설(컬러 신규 토큰 0). ③**섹션별 배경·애니메이션 트리거 매핑(V-3)** 풀블리드 히어로(라이트 파랄랙스+인트로 리빌)·통계 카운트업·AI 타일 스태거·쇼케이스 hover 리프트/이미지 줌·행사 레일 오토스크롤·기획 브리핑 grow·공통 스크롤 리빌(IO threshold 0.15·once). ④**이미지 소스 매핑(V-4)** build 내장 `/media/`(hero·bands·poster·patterns 계약)·부재 시 그라디언트+패턴 폴백(레이아웃 시프트 0)·preload/lazy/webp/srcset. ⑤**접근성(V-5)** `prefers-reduced-motion` 필수 — 파랄랙스/오토스크롤 정지·카운트업 즉시·리빌 instant·모션은 전부 장식(정보 비의존)·양 테마 WCAG AA. ⑥**성능(V-6)** CSS 우선·`transform`/`opacity`만·`will-change` 절제·60fps·강등 순서. ⑦**프론트 계약(V-7)** `.pub-reveal`/`--stagger`·`.pub-hero__bg[data-parallax]`·`.pub-card--lift`·`.pub-card__media img` 줌·`.pub-countup`·`.pub-rail--auto`·폴백 유틸, **PublicShell 하위만 스코프**(AppShell/MDI·SCR-A*·모바일 미주입). ⑧**영어 Stitch 프롬프트 2종(V-8·V-9)** — 프리미엄 전시장 홈페이지(SCR-T1 시네마틱 히어로·스크롤 리빌·카운트업·hover 줌·오토스크롤·폴백·reduced-motion, 제품명 정확·NO emoji·선 SVG·라이트/다크) + 3-트랙 관문(SCR-T0 동일 톤 격상). **경계 준수**: visitor(T1)만 풀 시네마틱, business/agency는 B2B 톤 유지+공통 모션 절제 적용. 신규 컬러 토큰 0·PLANNING.md·src 무수정. |
| **v2.3.2** | 2026-07-12 | designer | **SCR-T1 — AI 전시·행사 기획 브리핑(연간·월간 선제 제시) 추가(소유자 추가 지시 2026-07-12).** 단발 질의응답을 넘어 **AI가 개최 예정 전시·행사를 먼저 큐레이션**하는 섹션을 히어로 AI 능력 타일 직하(레이아웃 4번)에 신설 — **연/월 관점 토글**(① 연간=12개월 타임라인+분기 하이라이트+AiLabel "AI 요약" 헤드라인, ② 월간=선택 월 캘린더/리스트+AI 월간 요약), 연/월 전환 시 AI가 해당 기간 데이터 근거로 요약·추천 갱신. **중복 회피**: 하단 "진행 중/이번 달/다음 달" 쇼케이스(레이아웃 5)와 달리 본 섹션은 **연간 기획 관점**이 차별점, 월간은 동일 event 소스로 정합. 구성요소 `AiPlanningBriefing` 추가, 이하 레이아웃 5~11 재번호. **데이터 원천** — 신규 공개 엔드포인트 제안 `GET /api/public/events/planning?view=year|month&year&month`(크롤 적재 `exhibition`/`event`를 개최연·월 그룹 조회, AI 기간 요약은 AiTextRouter Claude→Ollama 폴백이 해당 기간 DB만 근거·근거 인용·캐시, DB 서빙·외부 실시간 호출 없음, 계약 제안). 상태(로딩·성공·**degraded(요약 미가용→데이터만 노출)**·빈·에러·기간 전환 부분 로딩) 정의. **Stitch 프롬프트 갱신** — 연간 타임라인+"AI 요약" 헤드라인("2026년은 상반기 스마트팩토리·로봇, 하반기 헬스케어·친환경…")+월간 요약("이번 달은 로봇·헬스케어 전시가 집중됩니다") 문단 추가, 쇼케이스와 구별 명시. 세련된 홈페이지 톤·풀블리드·반응형·라이트/다크·크롤 적재 DB 근거 원칙은 유지. 신규 토큰 0·PLANNING.md·src 무수정 |
| **v2.3.1** | 2026-07-12 | designer | **SCR-T1 마무리 — AI 전면 노출 + AI 답변 근거=크롤 적재 DB 명시(소유자 추가 지시 2건 2026-07-12).** ①**AI 기능 쇼케이스 타일 신설**(히어로 직하 레이아웃 3번): "AI 시스템인데 AI가 어디 있는지 모르겠다" 지적 반영 — AI 관람 도우미·AI 전시 추천·AI 길찾기·AI 일정 도우미·AI 혼잡/대기 안내·AI 시공 예상 이미지 6능력을 AiLabel 카드로 전면 홍보(구성요소 AiCapabilityTiles·상태(정적 상시·미가용 "준비 중")·이하 레이아웃 4~10 재번호). ②**데이터 원천 = 사전 크롤 적재 온프레미스 DB 명시**: AI 관람 도우미(`POST /api/public/ai/visitor-assistant`)가 **실시간 외부 크롤이 아니라 미리 크롤해 DB에 적재한 공개 데이터만을 RAG 근거로 답하고 참조 레코드를 인용**(범위 밖=degraded)함을 명문화 — 근거 테이블 계약 예시(`exhibition`/`event`·`transport_info`·`parking_info`·`ticket_policy`·`amenity_info`·`visitor_guide`·`floorplan`), 각 섹션 출처를 크롤 적재 DB로 매핑, degraded 폴백을 "크롤 적재 데이터 검색"으로 갱신, 백엔드 계약을 "공개 조회 API가 크롤 적재 DB에서 서빙(외부 실시간 호출 없음·온프레미스·근거 인용)" 전제로 기술. ③**Stitch 프롬프트 갱신** — AI 능력 쇼케이스 카드 6종 문구 + "served from a pre-crawled on-premise database, AI answers ONLY from stored data and cites its source" 추가. 세련된 홈페이지 톤·풀블리드·반응형·라이트/다크는 v2.3 그대로 유지. 신규 토큰 0·PLANNING.md·src 무수정 |
| **v2.3** | 2026-07-12 | designer | **SCR-T1 visitor 메인을 "세련된 공개 마케팅 홈페이지"로 격상 + 중심 요소를 대화형 AI 관람 도우미로 재구성(소유자 지시 2026-07-12).** ①**§3C SCR-T1 전면 재작성** — 기존 "visitor 서브홈(가이드 허브)"을 일반 대중 대상 마케팅 홈페이지(코엑스/글로벌 전시장 홈 수준·풀블리드·타이포 위계·공백 없는 밀도·전환 지향)로 격상. **화면 중심 = AI 관람 도우미**(히어로 정중앙 대형 자연어 질문 입력창 + 예시 질문칩 5종 + 대화형 답변 패널: 답변 텍스트·구조화 결과 카드(전시/교통/입장/wayfinding)·후속 질문칩·AI 고지문·근거 출처, AiLabel 보라 구분). 관람객 관심 우선순위 5(전시 추천·교통·입장·부스위치·편의)를 자연어 하나로 관통. 하위 섹션 순증: 전시 쇼케이스(2:3 포스터 그리드/캐러셀·탭·전시장 필터·`poster_url`)·관심사 맞춤 추천 레일(M11 AI)·방문가이드 허브(2×3, GTX-A·주차·M13 길찾기·편의·접근성·문의)·관람객 여정 3-스텝(사전등록→배지/QR→체크인)·공지·뉴스·전시장 소개·푸터(회사·SNS·문의). ②**데이터 원천** — **신규 공개 엔드포인트 제안 `POST /api/public/ai/visitor-assistant`**(비인증·CORS·레이트리밋·PII 미수집, 기존 AiTextRouter Claude→Ollama 폴백 + 공개 행사 데이터 근거·환각방지, **계약 제안·백엔드 후속**), 나머지 `/api/public/events`·notice·CMS 재사용. ③**상태** — AI 도우미 대기/생각중/성공/**degraded(AI 미가용→키워드 검색 폴백)**/에러/빈결과 정의. ④**테마** — 공개 마케팅 홈페이지에 한해 **라이트/다크 테마 토글 제공(§1-5 업무 셸 라이트 단일 원칙의 공개 트랙 예외)**, 색은 §1 토큰 CSS 변수 주입·라이트 기본. ⑤**접근성** — AI 입력 `role="search"`·답변 `aria-live="polite"`·색 외 라벨·양 테마 WCAG AA, i18n `track.visitor.*`. ⑥영어 **Stitch 프롬프트 전면 갱신**("premium exhibition-center marketing homepage"·1440 full-bleed·AI 관람 도우미 centerpiece·제품명 "KINTEX AI 전시·행사시스템" 정확 표기·NO emoji/line SVG·라이트·다크). ⑦**§2.8-2 인벤토리** SCR-T1 행 명칭/모듈 갱신. **경계 준수**: business(SCR-T2)·agency(SCR-T3) 메인은 B2B 업무 톤 유지(격상 대상 아님) 명시, 라우트 `/visitor`·PublicShell·TrackSwitcher·세션키·기존 SCR 불변, **신규 토큰 0**, PLANNING.md·src 무수정 |

View File

@ -7,6 +7,19 @@ React Native + Expo(expo-router, TypeScript) 모바일 앱. 조회·승인·현
- Expo SDK 51 · expo-router ~3.5 · React Native 0.74 · TypeScript
- JWT는 `expo-secure-store`(네이티브 keychain/keystore)에 저장, 웹은 AsyncStorage 폴백
- 디자인 토큰: `theme/`(docs/design.md §1 팔레트·타이포). 신규 토큰 없음.
- 다국어: `react-i18next` + `expo-localization`(SDK51 호환). 지원 ko(기본/폴백)·en·zh·ja.
## 다국어(i18n)
- 초기화 `lib/i18n/`(동기 init — 디바이스 로케일 감지, 저장 선택은 부팅 시 반영). 리소스 `lib/i18n/locales/{ko,en,zh,ja}.ts`.
- **ko가 키 형상의 단일 출처** — en/zh/ja는 `Translations`(DeepString) 타입 구현이라 키 누락 시 tsc가 잡음(병렬 편집 함정 회피: 직렬·키 대조).
- 언어 선택 지속: AsyncStorage 키 `kintex_lang`. 전환 UI `components/LanguageSelector.tsx`(더보기·내정보에 배치), 상태 `context/LanguageContext.tsx`.
- 커버 화면(우선순위): 로그인/2FA·회원가입·비번찾기·홈·현장·갤러리·더보기·내정보·티켓 지갑·탭/스택 타이틀·역할 라벨. 미커버(폴백=현행 한국어): `checklist`·`inspection`·`tickets/select`·티켓 카드/필터탭 컴포넌트(`components/tickets/*`).
## 역할별 랜딩
- `lib/roleTrack.ts` — 웹 `src/frontend/src/lib/roleTrack.ts` 패리티(우선순위 admin>ops>business>agency>visitor, 미판정=visitor 기본).
- 로그인 응답 user에 roleCode가 없어 `hallManager`를 ops 신호로 사용(roleCode 아는 호출부는 옵션 전달).
- 모바일 랜딩 매핑: admin/ops/business→`/(tabs)` 홈 · agency(장치·시공)→`/(tabs)/field` · visitor(관람객)→`/tickets`.
- 콜드 부팅 시 workspaces 미복원 misroute 방지 위해 로그인 시 계산한 랜딩 경로를 secure-store에 지속(`AuthContext`).
## 실행
```bash
@ -48,5 +61,6 @@ API 베이스는 `app.json > extra.apiBase` 또는 `EXPO_PUBLIC_API_BASE` 환경
## 남은 작업
- 실 화면 데이터 API 배선 확대(부스 목록·검수 요청 제출 — 백엔드 M6/C-4 확장 대기).
- 앱 아이콘/스플래시 에셋(`assets/`), i18n, 푸시 알림.
- EAS 실빌드(eas.json 프로파일) — 본 스캐폴드 범위 밖.
- 앱 아이콘/스플래시 에셋(`assets/`), 푸시 알림.
- i18n 잔여 화면 키 추출: `checklist`·`inspection`·`tickets/select`·`components/tickets/*`(현재 한국어 폴백).
- EAS 실빌드(eas.json 프로파일) — G3 게이트(소유자 승인).

View File

@ -4,7 +4,7 @@
"slug": "kintex",
"owner": "zioinfo",
"scheme": "kintex",
"version": "0.1.0",
"version": "0.2.0",
"orientation": "portrait",
"userInterfaceStyle": "light",
"jsEngine": "hermes",
@ -28,7 +28,7 @@
},
"android": {
"package": "kr.co.zioinfo.kintex",
"versionCode": 1,
"versionCode": 2,
"adaptiveIcon": {
"foregroundImage": "./assets/adaptive-icon.png",
"backgroundColor": "#0066B3"
@ -58,7 +58,6 @@
"expo-router",
"expo-secure-store",
"expo-font",
"expo-screen-capture",
[
"expo-image-picker",
{

View File

@ -5,10 +5,12 @@
import { Ionicons } from '@expo/vector-icons';
import { Redirect, Tabs } from 'expo-router';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { useAuth } from '../../context/AuthContext';
import { colors } from '../../theme';
export default function TabsLayout() {
const { t } = useTranslation();
const { ready, token } = useAuth();
if (ready && !token) return <Redirect href="/login" />;
@ -25,14 +27,14 @@ export default function TabsLayout() {
<Tabs.Screen
name="index"
options={{
title: '홈',
title: t('nav.tabHome'),
tabBarIcon: ({ color, size }) => <Ionicons name="home-outline" color={color} size={size} />,
}}
/>
<Tabs.Screen
name="field"
options={{
title: '현장',
title: t('nav.tabField'),
tabBarIcon: ({ color, size }) => (
<Ionicons name="clipboard-outline" color={color} size={size} />
),
@ -41,14 +43,14 @@ export default function TabsLayout() {
<Tabs.Screen
name="gallery"
options={{
title: '갤러리',
title: t('nav.tabGallery'),
tabBarIcon: ({ color, size }) => <Ionicons name="images-outline" color={color} size={size} />,
}}
/>
<Tabs.Screen
name="more"
options={{
title: '더보기',
title: t('nav.tabMore'),
tabBarIcon: ({ color, size }) => (
<Ionicons name="ellipsis-horizontal" color={color} size={size} />
),

View File

@ -5,12 +5,14 @@
import { Ionicons } from '@expo/vector-icons';
import { router } from 'expo-router';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { Pressable, ScrollView, StyleSheet, Text, View } from 'react-native';
import { Banner } from '../../components/Banner';
import { useAuth } from '../../context/AuthContext';
import { colors, radius, spacing, type } from '../../theme';
export default function FieldScreen() {
const { t } = useTranslation();
const { activeWorkspace, user } = useAuth();
const role = activeWorkspace?.myRole;
const isHallManager = user?.hallManager || role === 'HALL_MANAGER';
@ -18,19 +20,17 @@ export default function FieldScreen() {
return (
<ScrollView contentContainerStyle={styles.scroll}>
<Text style={styles.title}> </Text>
<Text style={styles.title}>{t('field.title')}</Text>
{!isContractor && !isHallManager ? (
<Banner tone="info">
·() () .
</Banner>
<Banner tone="info">{t('field.guide')}</Banner>
) : null}
{isContractor ? (
<Tile
icon="clipboard-outline"
title="현장 체크리스트"
desc="장치 공정 확인 · 사진 첨부 · 검수 요청"
title={t('field.checklistTitle')}
desc={t('field.checklistDesc')}
onPress={() => router.push('/checklist')}
/>
) : null}
@ -38,8 +38,8 @@ export default function FieldScreen() {
{isHallManager ? (
<Tile
icon="shield-checkmark-outline"
title="현장 검수"
desc="승인 도면 vs 현장 사진 · 적합/부적합 판정"
title={t('field.inspectionTitle')}
desc={t('field.inspectionDesc')}
onPress={() => router.push('/inspection')}
/>
) : null}

View File

@ -4,7 +4,8 @@
* API , / degraded .
*/
import { router } from 'expo-router';
import React, { useCallback, useEffect, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { RefreshControl, ScrollView, StyleSheet, Text, View } from 'react-native';
import { AiImage } from '../../components/AiImage';
import { Banner } from '../../components/Banner';
@ -18,14 +19,21 @@ import { colors, spacing, type } from '../../theme';
const DEMO_BOOTH = 'b-102';
// degraded 폴백 샘플 (서버 미구현/오프라인 시 표시 — 항상 워터마크 유지)
const FALLBACK: RenderJobDto[] = [
mkJob('job-s1', 'S1', 'DONE'),
mkJob('job-s2', 'S2', 'DONE'),
mkJob('job-s3', 'S3', 'RUNNING'),
mkJob('job-s7', 'S7', 'FAILED'),
];
function buildFallback(notice: string): RenderJobDto[] {
return [
mkJob('job-s1', 'S1', 'DONE', notice),
mkJob('job-s2', 'S2', 'DONE', notice),
mkJob('job-s3', 'S3', 'RUNNING', notice),
mkJob('job-s7', 'S7', 'FAILED', notice),
];
}
function mkJob(jobId: string, shot: string, status: RenderJobDto['status']): RenderJobDto {
function mkJob(
jobId: string,
shot: string,
status: RenderJobDto['status'],
notice: string,
): RenderJobDto {
return {
jobId,
boothId: DEMO_BOOTH,
@ -36,7 +44,7 @@ function mkJob(jobId: string, shot: string, status: RenderJobDto['status']): Ren
modelVersion: null,
watermarkRequired: true,
watermarkText: AI_IMAGE_NOTICE,
notice: 'AI 생성 이미지는 계약·심사 서류에 사용할 수 없습니다 (시공 기준은 도면)',
notice,
};
}
@ -51,15 +59,18 @@ const SHOT_LABEL: Record<string, string> = {
};
export default function GalleryScreen() {
const { t } = useTranslation();
const { activeWorkspace, token } = useAuth();
const [jobs, setJobs] = useState<RenderJobDto[]>([]);
const [degraded, setDegraded] = useState(false);
const [loading, setLoading] = useState(true);
const fallback = useMemo(() => buildFallback(t('gallery.notice')), [t]);
const load = useCallback(async () => {
setLoading(true);
if (!activeWorkspace || !token) {
setJobs(FALLBACK);
setJobs(fallback);
setDegraded(true);
setLoading(false);
return;
@ -72,7 +83,7 @@ export default function GalleryScreen() {
setDegraded(false);
} catch (e) {
if (isDegraded(e)) {
setJobs(FALLBACK);
setJobs(fallback);
setDegraded(true);
} else {
setJobs([]);
@ -81,7 +92,7 @@ export default function GalleryScreen() {
} finally {
setLoading(false);
}
}, [activeWorkspace, token]);
}, [activeWorkspace, token, fallback]);
useEffect(() => {
load();
@ -91,9 +102,7 @@ export default function GalleryScreen() {
<View style={styles.flex}>
{/* 상단 고정 고지 바 (제거 불가) */}
<View style={styles.infoBar}>
<Text style={styles.infoBarText}>
AI ·
</Text>
<Text style={styles.infoBarText}>{t('gallery.infoBar')}</Text>
</View>
<ScrollView
@ -101,15 +110,11 @@ export default function GalleryScreen() {
refreshControl={<RefreshControl refreshing={loading} onRefresh={load} tintColor={colors.primary600} />}
>
{degraded ? (
<Banner tone="degraded">
(degraded).
</Banner>
<Banner tone="degraded">{t('gallery.degraded')}</Banner>
) : null}
{jobs.length === 0 && !loading ? (
<Banner tone="info">
.
</Banner>
<Banner tone="info">{t('gallery.empty')}</Banner>
) : null}
<View style={styles.grid}>
@ -124,14 +129,14 @@ export default function GalleryScreen() {
height={130}
/>
{j.status === 'FAILED' ? (
<Button label="다시 생성" variant="outline" onPress={load} style={styles.retry} />
<Button label={t('gallery.retry')} variant="outline" onPress={load} style={styles.retry} />
) : null}
</View>
))}
</View>
<Button
label="설계 스튜디오는 데스크톱에서 편집"
label={t('gallery.studioNote')}
variant="ghost"
onPress={() => router.push('/(tabs)')}
/>

View File

@ -5,6 +5,7 @@
import { Ionicons } from '@expo/vector-icons';
import { router } from 'expo-router';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { Pressable, ScrollView, StyleSheet, Text, View } from 'react-native';
import { Banner } from '../../components/Banner';
import { Card } from '../../components/Card';
@ -13,24 +14,18 @@ import { useAuth } from '../../context/AuthContext';
import type { EventRole, WorkspaceDto } from '../../lib/types';
import { colors, radius, spacing, type } from '../../theme';
const roleLabel: Record<EventRole, string> = {
ORGANIZER: '주최자',
EXHIBITOR: '참가업체',
CONTRACTOR: '장치·시공업체',
HALL_MANAGER: '홀매니저',
};
export default function HomeScreen() {
const { t } = useTranslation();
const { user, workspaces, activeWorkspace, selectWorkspace } = useAuth();
return (
<ScrollView contentContainerStyle={styles.scroll}>
<Text style={styles.hello}>{user?.displayName ?? '사용자'}, </Text>
<Text style={styles.hello}>
{t('home.hello', { name: user?.displayName ?? t('common.user') })}
</Text>
{workspaces.length === 0 ? (
<Banner tone="info">
.
</Banner>
<Banner tone="info">{t('home.noEvent')}</Banner>
) : null}
{/* 행사 선택 */}
@ -63,8 +58,11 @@ export default function HomeScreen() {
{/* 진행 스테퍼 */}
<Card>
<Text style={styles.cardTitle}> </Text>
<Stepper steps={['신청', '승인', '시공', '검수']} active={1} />
<Text style={styles.cardTitle}>{t('home.progress')}</Text>
<Stepper
steps={[t('home.stepApply'), t('home.stepApprove'), t('home.stepBuild'), t('home.stepInspect')]}
active={1}
/>
</Card>
{/* 역할별 진입 */}
@ -74,6 +72,7 @@ export default function HomeScreen() {
}
function EventHeader({ ws }: { ws: WorkspaceDto }) {
const { t } = useTranslation();
return (
<Card accent="none">
<View style={styles.eventHeaderTop}>
@ -84,22 +83,23 @@ function EventHeader({ ws }: { ws: WorkspaceDto }) {
{ws.hallLabel} · {ws.startDate} ~ {ws.endDate}
</Text>
<View style={styles.roleBadge}>
<Text style={styles.roleBadgeText}>{roleLabel[ws.myRole]}</Text>
<Text style={styles.roleBadgeText}>{t(`roles.${ws.myRole}` as const)}</Text>
</View>
</Card>
);
}
function RoleActions({ role }: { role: EventRole }) {
const { t } = useTranslation();
const actions: { label: string; icon: keyof typeof Ionicons.glyphMap; onPress: () => void }[] = [];
if (role === 'CONTRACTOR') {
actions.push({ label: '현장 체크리스트', icon: 'clipboard-outline', onPress: () => router.push('/checklist') });
actions.push({ label: t('home.actionChecklist'), icon: 'clipboard-outline', onPress: () => router.push('/checklist') });
}
if (role === 'HALL_MANAGER') {
actions.push({ label: '현장 검수', icon: 'shield-checkmark-outline', onPress: () => router.push('/inspection') });
actions.push({ label: t('home.actionInspection'), icon: 'shield-checkmark-outline', onPress: () => router.push('/inspection') });
}
actions.push({ label: '시각화 갤러리', icon: 'images-outline', onPress: () => router.push('/(tabs)/gallery') });
actions.push({ label: t('home.actionGallery'), icon: 'images-outline', onPress: () => router.push('/(tabs)/gallery') });
return (
<View style={styles.actions}>

View File

@ -4,9 +4,11 @@
import { Ionicons } from '@expo/vector-icons';
import { router } from 'expo-router';
import React, { useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Pressable, ScrollView, StyleSheet, Text, View } from 'react-native';
import { Avatar } from '../../components/Avatar';
import { Card } from '../../components/Card';
import { LanguageSelector } from '../../components/LanguageSelector';
import { useAuth } from '../../context/AuthContext';
import { api, isDegraded } from '../../lib/api';
import { API_BASE } from '../../lib/config';
@ -14,17 +16,18 @@ import type { HealthDto } from '../../lib/types';
import { colors, radius, spacing, touch, type } from '../../theme';
export default function MoreScreen() {
const { t } = useTranslation();
const { user, signOut } = useAuth();
const [health, setHealth] = useState<string>('확인 중…');
const [health, setHealth] = useState<string>(t('common.loading'));
const checkHealth = useCallback(async () => {
try {
const res = await api.get<HealthDto>('/health', { anonymous: true });
setHealth(res?.status === 'UP' ? '정상 (UP)' : (res?.status ?? '알 수 없음'));
setHealth(res?.status === 'UP' ? t('more.statusUp') : (res?.status ?? t('common.unknown')));
} catch (e) {
setHealth(isDegraded(e) ? '연결 불가 (degraded)' : '오류');
setHealth(isDegraded(e) ? t('more.statusDown') : t('common.error'));
}
}, []);
}, [t]);
useEffect(() => {
checkHealth();
@ -41,15 +44,15 @@ export default function MoreScreen() {
<Pressable
onPress={() => router.push('/profile')}
accessibilityRole="button"
accessibilityLabel="내 정보 열기"
accessibilityLabel={t('more.openProfile')}
>
<Card>
<View style={styles.meRow}>
<Avatar name={user?.displayName} size={48} />
<View style={{ flex: 1, marginLeft: 12 }}>
<Text style={styles.name}>{user?.displayName ?? '사용자'}</Text>
<Text style={styles.name}>{user?.displayName ?? t('common.user')}</Text>
<Text style={styles.meta}>
{user?.hallManager ? '킨텍스 내부 계정 (홀매니저)' : '일반 계정'}
{user?.hallManager ? t('more.accountInternal') : t('more.accountGeneral')}
</Text>
</View>
<Ionicons name="chevron-forward" size={20} color={colors.neutral500} />
@ -57,21 +60,24 @@ export default function MoreScreen() {
</Card>
</Pressable>
{/* 언어 설정 — ko/en/zh/ja */}
<LanguageSelector />
<Card>
<Row label="API 서버" value={API_BASE} />
<Row label={t('more.apiServer')} value={API_BASE} />
<View style={styles.divider} />
<Pressable style={styles.healthRow} onPress={checkHealth}>
<Row label="서버 상태" value={health} />
<Row label={t('more.serverStatus')} value={health} />
<Ionicons name="refresh" size={18} color={colors.primary600} />
</Pressable>
</Card>
<Pressable style={styles.signOut} onPress={onSignOut}>
<Ionicons name="log-out-outline" size={20} color={colors.error} />
<Text style={styles.signOutText}></Text>
<Text style={styles.signOutText}>{t('more.signOut')}</Text>
</Pressable>
<Text style={styles.version}>KINTEX AI · v0.1.0</Text>
<Text style={styles.version}>{t('more.version')}</Text>
</ScrollView>
);
}

View File

@ -5,19 +5,18 @@
import { Stack } from 'expo-router';
import { StatusBar } from 'expo-status-bar';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import { AuthProvider } from '../context/AuthContext';
import { LanguageProvider } from '../context/LanguageContext';
import { SecureScreenProvider } from '../context/SecureScreenContext';
import '../lib/i18n'; // i18next 동기 초기화(최초 렌더 전)
import { colors } from '../theme';
export default function RootLayout() {
function RootStack() {
const { t } = useTranslation();
return (
<GestureHandlerRootView style={{ flex: 1 }}>
<SafeAreaProvider>
<AuthProvider>
<SecureScreenProvider>
<StatusBar style="dark" />
<Stack
screenOptions={{
headerStyle: { backgroundColor: colors.white },
@ -28,17 +27,30 @@ export default function RootLayout() {
>
<Stack.Screen name="index" options={{ headerShown: false }} />
<Stack.Screen name="login" options={{ headerShown: false }} />
<Stack.Screen name="register" options={{ title: '회원가입' }} />
<Stack.Screen name="forgot-password" options={{ title: '비밀번호 찾기' }} />
<Stack.Screen name="register" options={{ title: t('nav.register') }} />
<Stack.Screen name="forgot-password" options={{ title: t('nav.forgotPassword') }} />
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen name="profile" options={{ title: '내 정보' }} />
<Stack.Screen name="checklist" options={{ title: '현장 체크리스트' }} />
<Stack.Screen name="inspection" options={{ title: '현장 검수' }} />
<Stack.Screen name="tickets/index" options={{ title: '내 티켓' }} />
<Stack.Screen name="tickets/select" options={{ title: '티켓 예매' }} />
<Stack.Screen name="profile" options={{ title: t('nav.profile') }} />
<Stack.Screen name="checklist" options={{ title: t('nav.checklist') }} />
<Stack.Screen name="inspection" options={{ title: t('nav.inspection') }} />
<Stack.Screen name="tickets/index" options={{ title: t('nav.ticketsIndex') }} />
<Stack.Screen name="tickets/select" options={{ title: t('nav.ticketsSelect') }} />
</Stack>
);
}
export default function RootLayout() {
return (
<GestureHandlerRootView style={{ flex: 1 }}>
<SafeAreaProvider>
<LanguageProvider>
<AuthProvider>
<SecureScreenProvider>
<StatusBar style="dark" />
<RootStack />
</SecureScreenProvider>
</AuthProvider>
</LanguageProvider>
</SafeAreaProvider>
</GestureHandlerRootView>
);

View File

@ -5,6 +5,7 @@
*/
import { router } from 'expo-router';
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { KeyboardAvoidingView, Platform, ScrollView, StyleSheet, Text } from 'react-native';
import { Banner } from '../components/Banner';
import { Button } from '../components/Button';
@ -14,6 +15,7 @@ import { forgotPassword, resetPassword } from '../lib/auth';
import { colors, spacing, type } from '../theme';
export default function ForgotPasswordScreen() {
const { t } = useTranslation();
const [step, setStep] = useState<1 | 2>(1);
const [email, setEmail] = useState('');
const [code, setCode] = useState('');
@ -26,7 +28,7 @@ export default function ForgotPasswordScreen() {
setError(null);
setInfo(null);
if (!email.trim()) {
setError('이메일을 입력해 주세요.');
setError(t('forgot.errEmail'));
return;
}
setLoading(true);
@ -35,7 +37,7 @@ export default function ForgotPasswordScreen() {
setInfo(res.message);
setStep(2);
} catch (e) {
setError(e instanceof ApiRequestError ? e.message : '요청에 실패했습니다.');
setError(e instanceof ApiRequestError ? e.message : t('forgot.errRequest'));
} finally {
setLoading(false);
}
@ -44,7 +46,7 @@ export default function ForgotPasswordScreen() {
async function onReset() {
setError(null);
if (!code.trim() || newPassword.length < 8) {
setError('재설정 코드와 새 비밀번호(8자 이상)를 확인해 주세요.');
setError(t('forgot.errFields'));
return;
}
setLoading(true);
@ -54,9 +56,9 @@ export default function ForgotPasswordScreen() {
setTimeout(() => router.replace('/login'), 800);
} catch (e) {
if (e instanceof ApiRequestError && e.code === 'OTP_INVALID') {
setError('재설정 코드가 올바르지 않거나 만료되었습니다.');
setError(t('forgot.errCode'));
} else {
setError(e instanceof ApiRequestError ? e.message : '초기화에 실패했습니다.');
setError(e instanceof ApiRequestError ? e.message : t('forgot.errReset'));
}
} finally {
setLoading(false);
@ -69,39 +71,39 @@ export default function ForgotPasswordScreen() {
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
>
<ScrollView contentContainerStyle={styles.scroll} keyboardShouldPersistTaps="handled">
<Text style={styles.title}> </Text>
<Text style={styles.title}>{t('forgot.title')}</Text>
{error ? <Banner tone="error">{error}</Banner> : null}
{info ? <Banner tone="info">{info}</Banner> : null}
<Field
label="이메일"
label={t('forgot.email')}
value={email}
onChangeText={setEmail}
placeholder="name@company.co.kr"
placeholder={t('forgot.emailPlaceholder')}
keyboardType="email-address"
editable={step === 1}
/>
{step === 1 ? (
<Button label="재설정 코드 요청" onPress={onRequest} loading={loading} />
<Button label={t('forgot.request')} onPress={onRequest} loading={loading} />
) : (
<>
<Field
label="재설정 코드"
label={t('forgot.code')}
value={code}
onChangeText={setCode}
placeholder="6자리 코드"
placeholder={t('forgot.codePlaceholder')}
keyboardType="number-pad"
/>
<Field
label="새 비밀번호"
label={t('forgot.newPassword')}
value={newPassword}
onChangeText={setNewPassword}
placeholder="8자 이상"
placeholder={t('forgot.newPasswordPlaceholder')}
secureTextEntry
helperText="8~100자"
helperText={t('forgot.newPasswordHelper')}
/>
<Button label="비밀번호 변경" onPress={onReset} loading={loading} />
<Button label={t('forgot.submit')} onPress={onReset} loading={loading} />
</>
)}
</ScrollView>

View File

@ -8,7 +8,7 @@ import { useAuth } from '../context/AuthContext';
import { colors } from '../theme';
export default function Index() {
const { ready, token } = useAuth();
const { ready, token, landingPath } = useAuth();
if (!ready) {
return (
@ -18,7 +18,8 @@ export default function Index() {
);
}
return <Redirect href={token ? '/(tabs)' : '/login'} />;
// 인증됨 → 역할별 랜딩(지속된 값, 없으면 탭 홈) / 미인증 → 로그인.
return <Redirect href={token ? ((landingPath ?? '/(tabs)') as never) : '/login'} />;
}
const styles = StyleSheet.create({

View File

@ -5,6 +5,7 @@
*/
import { Link, router } from 'expo-router';
import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
KeyboardAvoidingView,
Platform,
@ -23,6 +24,7 @@ import { useSecureScreen } from '../context/SecureScreenContext';
import { ApiRequestError } from '../lib/api';
import { isOtpRequired, login } from '../lib/auth';
import { useDeviceIntegrity } from '../lib/integrity';
import { landingPathFor } from '../lib/roleTrack';
import { prefDelete, prefGet, prefSet } from '../lib/secureStore';
import { colors, radius, spacing, touch, type } from '../theme';
@ -30,6 +32,7 @@ const REMEMBER_KEY = 'kintex.rememberedEmail';
export default function LoginScreen() {
const insets = useSafeAreaInsets();
const { t } = useTranslation();
const { signIn } = useAuth();
// 2FA/자격증명 입력 화면 — 캡처 차단 + 백그라운드 마스킹(B4/B7).
@ -58,11 +61,11 @@ export default function LoginScreen() {
async function onSubmit() {
setError(null);
if (!email.trim() || !password) {
setError('이메일과 비밀번호를 입력해 주세요.');
setError(t('login.errEmpty'));
return;
}
if (otpRequired && !otp.trim()) {
setError('2차 인증 코드를 입력해 주세요.');
setError(t('login.errOtpEmpty'));
return;
}
setLoading(true);
@ -71,19 +74,21 @@ export default function LoginScreen() {
if (remember) await prefSet(REMEMBER_KEY, email.trim());
else await prefDelete(REMEMBER_KEY);
await signIn(res);
router.replace('/(tabs)');
// 역할 우선순위로 랜딩 결정(웹 roleTrack 패리티) — visitor=관람객 티켓, agency=현장, 그 외 홈.
const landing = landingPathFor({ user: res.user, workspaces: res.workspaces });
router.replace(landing as never);
} catch (e) {
if (isOtpRequired(e)) {
setOtpRequired(true);
setError('2차 인증 코드가 필요합니다. 인증 앱의 코드를 입력해 주세요.');
setError(t('login.errOtpRequired'));
} else if (e instanceof ApiRequestError && e.code === 'OTP_INVALID') {
setError('인증 코드가 올바르지 않거나 만료되었습니다.');
setError(t('login.errOtpInvalid'));
} else if (e instanceof ApiRequestError && e.code === 'ACCOUNT_LOCKED') {
setError('로그인 시도 초과로 계정이 잠겼습니다. 잠시 후 다시 시도해 주세요.');
setError(t('login.errLocked'));
} else if (e instanceof ApiRequestError) {
setError(e.message);
} else {
setError('로그인에 실패했습니다.');
setError(t('login.errGeneric'));
}
} finally {
setLoading(false);
@ -102,49 +107,46 @@ export default function LoginScreen() {
{/* 브랜드 패널 */}
<View style={styles.brand}>
<Text style={styles.brandMark}>KINTEX</Text>
<Text style={styles.brandTitle}> ,{'\n'} </Text>
<Text style={styles.brandCaption}>AI · </Text>
<Text style={styles.brandTitle}>{t('login.brandTitle')}</Text>
<Text style={styles.brandCaption}>{t('login.brandCaption')}</Text>
</View>
{/* 위변조 단말 경고(비차단) — 공공 대민 정책: 경고 후 이용 제한 안내 */}
{integrity.compromised ? (
<Banner tone="warning">
(··). ·
.
</Banner>
<Banner tone="warning">{t('login.compromised')}</Banner>
) : null}
{/* 로그인 카드 */}
<View style={styles.card}>
<Text style={styles.cardTitle}></Text>
<Text style={styles.cardTitle}>{t('login.title')}</Text>
{error ? (
<Banner tone={otpRequired ? 'warning' : 'error'}>{error}</Banner>
) : null}
<Field
label="이메일"
label={t('login.email')}
value={email}
onChangeText={setEmail}
placeholder="name@company.co.kr"
placeholder={t('login.emailPlaceholder')}
keyboardType="email-address"
/>
<Field
label="비밀번호"
label={t('login.password')}
value={password}
onChangeText={setPassword}
placeholder="비밀번호"
placeholder={t('login.passwordPlaceholder')}
secureTextEntry
/>
{otpRequired ? (
<Field
label="2차 인증 코드"
label={t('login.otpLabel')}
value={otp}
onChangeText={setOtp}
placeholder="6자리 코드"
placeholder={t('login.otpPlaceholder')}
keyboardType="number-pad"
helperText="인증 앱(TOTP)의 6자리 코드"
helperText={t('login.otpHelper')}
/>
) : null}
@ -158,22 +160,22 @@ export default function LoginScreen() {
<View style={[styles.checkbox, remember && styles.checkboxOn]}>
{remember ? <Text style={styles.checkmark}></Text> : null}
</View>
<Text style={styles.rememberText}> </Text>
<Text style={styles.rememberText}>{t('login.remember')}</Text>
</Pressable>
<Button
label={otpRequired ? '인증 후 로그인' : '로그인'}
label={otpRequired ? t('login.submitOtp') : t('login.submit')}
onPress={onSubmit}
loading={loading}
/>
<View style={styles.links}>
<Link href="/forgot-password" style={styles.link}>
{t('login.forgot')}
</Link>
<Text style={styles.linkDivider}>·</Text>
<Link href="/register" style={styles.link}>
{t('login.register')}
</Link>
</View>
</View>

View File

@ -9,6 +9,7 @@
import { Ionicons } from '@expo/vector-icons';
import { router, useFocusEffect } from 'expo-router';
import React, { useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
ActivityIndicator,
Alert,
@ -41,18 +42,12 @@ import {
pickFromLibrary,
type PickedImage,
} from '../lib/imagePick';
import { LanguageSelector } from '../components/LanguageSelector';
import { avatarUrl, getMe, getOtpStatus, uploadAvatar } from '../lib/profile';
import { prefGet, prefSet } from '../lib/secureStore';
import type { EventRole, MePrincipal, OtpStatusDto } from '../lib/types';
import type { MePrincipal, OtpStatusDto } from '../lib/types';
import { colors, radius, spacing, touch, type } from '../theme';
const roleLabel: Record<EventRole, string> = {
ORGANIZER: '주최자',
EXHIBITOR: '참가업체',
CONTRACTOR: '장치·시공업체',
HALL_MANAGER: '홀매니저',
};
// 환경설정(이 기기 로컬 저장 — 서버 동기화 계약 부재, web MyPage와 동일 정책).
const PREFS_KEY = 'kintex.prefs';
interface Prefs {
@ -63,6 +58,7 @@ interface Prefs {
const DEFAULT_PREFS: Prefs = { notifyDeadline: true, notifyApproval: true, notifyPayment: true };
export default function ProfileScreen() {
const { t } = useTranslation();
const { user, signOut } = useAuth();
// 개인정보 화면 — 캡처 차단 + 백그라운드 마스킹(B4/B7).
@ -88,7 +84,7 @@ export default function ProfileScreen() {
// 환경설정
const [prefs, setPrefs] = useState<Prefs>(DEFAULT_PREFS);
const displayName = me?.displayName ?? user?.displayName ?? '사용자';
const displayName = me?.displayName ?? user?.displayName ?? t('common.user');
const loadProfile = useCallback(async () => {
setLoading(true);
@ -100,7 +96,7 @@ export default function ProfileScreen() {
setPhotoPending(false);
} catch (e) {
// /me 실패 시 최소한 로그인 세션 정보로 화면 유지(전면 오류 회피).
setLoadErr(e instanceof ApiRequestError ? e.message : '내정보를 불러오지 못했습니다.');
setLoadErr(e instanceof ApiRequestError ? e.message : t('profile.loadErr'));
} finally {
setLoading(false);
}
@ -109,7 +105,7 @@ export default function ProfileScreen() {
} catch {
setOtp(null); // 상태 조회 실패는 무시(섹션은 안내로 degrade)
}
}, []);
}, [t]);
// 환경설정 로드
useEffect(() => {
@ -162,7 +158,7 @@ export default function ProfileScreen() {
// 활성화 전 1회 본인 확인.
const ok = await authenticateBiometric();
if (!ok) {
Alert.alert('생체인식', '생체 인증에 실패했습니다. 다시 시도해 주세요.');
Alert.alert(t('profile.biometric'), t('profile.bioFailed'));
return;
}
markSessionUnlocked(); // 방금 인증했으므로 이 세션은 통과 처리(즉시 재프롬프트 방지).
@ -175,7 +171,7 @@ export default function ProfileScreen() {
const pickForPreview = async (from: 'library' | 'camera') => {
if (photoBusy) return;
if (!isPickerAvailable()) {
Alert.alert('프로필 사진', '이미지 선택 기능을 사용할 수 없는 기기입니다.');
Alert.alert(t('profile.photoTitle'), t('profile.photoPickerUnavailable'));
return;
}
const img = from === 'library' ? await pickFromLibrary() : await pickFromCamera();
@ -205,7 +201,7 @@ export default function ProfileScreen() {
setPhotoPending(true);
setPreviewImg(null);
} else {
setPreviewErr(e instanceof ApiRequestError ? e.message : '사진 업로드에 실패했습니다.');
setPreviewErr(e instanceof ApiRequestError ? e.message : t('profile.uploadErr'));
}
} finally {
setPhotoBusy(false);
@ -220,18 +216,18 @@ export default function ProfileScreen() {
const onChangePhoto = () => {
if (photoBusy) return;
Alert.alert('프로필 사진', '사진을 선택하세요', [
{ text: '갤러리', onPress: () => pickForPreview('library') },
{ text: '카메라', onPress: () => pickForPreview('camera') },
{ text: '취소', style: 'cancel' },
Alert.alert(t('profile.photoTitle'), t('profile.photoSelect'), [
{ text: t('profile.photoLibrary'), onPress: () => pickForPreview('library') },
{ text: t('profile.photoCamera'), onPress: () => pickForPreview('camera') },
{ text: t('common.cancel'), style: 'cancel' },
]);
};
const onSignOut = () => {
Alert.alert('로그아웃', '로그아웃하시겠습니까?', [
{ text: '취소', style: 'cancel' },
Alert.alert(t('profile.signOut'), t('profile.signOutConfirm'), [
{ text: t('common.cancel'), style: 'cancel' },
{
text: '로그아웃',
text: t('profile.signOut'),
style: 'destructive',
onPress: async () => {
await signOut();
@ -246,15 +242,15 @@ export default function ProfileScreen() {
return (
<View style={styles.lockWrap}>
<Ionicons name="finger-print" size={56} color={colors.primary600} />
<Text style={styles.lockTitle}> </Text>
<Text style={styles.lockHint}> Face로 .</Text>
<Text style={styles.lockTitle}>{t('profile.lockTitle')}</Text>
<Text style={styles.lockHint}>{t('profile.lockHint')}</Text>
<Pressable
style={styles.lockBtn}
onPress={runLockGate}
accessibilityRole="button"
accessibilityLabel="생체 인증 다시 시도"
accessibilityLabel={t('common.retry')}
>
<Text style={styles.lockBtnText}> </Text>
<Text style={styles.lockBtnText}>{t('common.retry')}</Text>
</Pressable>
</View>
);
@ -265,7 +261,7 @@ export default function ProfileScreen() {
return (
<ScrollView style={styles.wrap} contentContainerStyle={styles.scroll}>
{/* ── 프로필 ── */}
<Text style={styles.section}> </Text>
<Text style={styles.section}>{t('profile.sectionInfo')}</Text>
{loading ? (
<ActivityIndicator color={colors.primary600} style={{ marginVertical: spacing.md }} />
) : (
@ -297,20 +293,18 @@ export default function ProfileScreen() {
</View>
{photoPending ? (
<Text style={styles.pendingNote}>
.
</Text>
<Text style={styles.pendingNote}>{t('profile.photoPending')}</Text>
) : null}
{loadErr ? <Banner tone="warning">{loadErr}</Banner> : null}
<InfoRow label="이름" value={displayName} />
<InfoRow label="사용자 ID" value={me?.userId ?? user?.userId ?? null} testID="profile-userid" />
<InfoRow label="전역 역할" value={me?.roleCode ?? null} />
<InfoRow label="전시관(테넌트)" value={me?.tenantId ?? null} />
<InfoRow label={t('profile.labelName')} value={displayName} />
<InfoRow label={t('profile.labelUserId')} value={me?.userId ?? user?.userId ?? null} testID="profile-userid" />
<InfoRow label={t('profile.labelGlobalRole')} value={me?.roleCode ?? null} />
<InfoRow label={t('profile.labelTenant')} value={me?.tenantId ?? null} />
<InfoRow
label="구분"
value={me?.hallManager ?? user?.hallManager ? '킨텍스 내부(홀매니저)' : '일반 계정'}
label={t('profile.labelType')}
value={me?.hallManager ?? user?.hallManager ? t('profile.typeInternal') : t('profile.typeGeneral')}
/>
</Card>
)}
@ -318,7 +312,7 @@ export default function ProfileScreen() {
{/* ── 참여 행사 역할 ── */}
{roles.length > 0 ? (
<>
<Text style={styles.section}> </Text>
<Text style={styles.section}>{t('profile.sectionEventRoles')}</Text>
<Card>
{Object.entries(me!.eventRoles).map(([eventId, r]) => (
<View key={eventId} style={styles.infoRow}>
@ -326,7 +320,7 @@ export default function ProfileScreen() {
{eventId}
</Text>
<View style={styles.rolePill}>
<Text style={styles.rolePillText}>{roleLabel[r] ?? r}</Text>
<Text style={styles.rolePillText}>{t(`roles.${r}` as const, { defaultValue: r })}</Text>
</View>
</View>
))}
@ -335,35 +329,35 @@ export default function ProfileScreen() {
) : null}
{/* ── 계정·보안(2차 인증) ── */}
<Text style={styles.section}> · </Text>
<Text style={styles.section}>{t('profile.sectionSecurity')}</Text>
<Card>
<View style={styles.bioRow}>
<View style={{ flex: 1, paddingRight: 12 }}>
<Text style={styles.rowTitle}>2 (OTP)</Text>
<Text style={styles.rowTitle}>{t('profile.otpTitle')}</Text>
<Text style={styles.hint}>
{otp?.otpEnabled
? `활성화됨 · ${otp.verifyMethod === 'OTP' ? 'Authenticator(OTP)' : otp.verifyMethod || 'OTP'}`
: '미설정 — 로그인 시 인증 앱(TOTP) 2단계 인증을 사용합니다.'}
? otp.verifyMethod === 'OTP'
? t('profile.otpOnAuth')
: t('profile.otpOnMethod', { method: otp.verifyMethod || 'OTP' })
: t('profile.otpOff')}
</Text>
</View>
<View style={otp?.otpEnabled ? styles.badgeOn : styles.badgeOff}>
<Text style={otp?.otpEnabled ? styles.badgeOnText : styles.badgeOffText}>
{otp?.otpEnabled ? '켜짐' : '꺼짐'}
{otp?.otpEnabled ? t('profile.on') : t('profile.off')}
</Text>
</View>
</View>
</Card>
{/* ── 생체인식 잠금 ── */}
<Text style={styles.section}> </Text>
<Text style={styles.section}>{t('profile.sectionBiometric')}</Text>
<Card>
{bioSupported ? (
<View style={styles.bioRow}>
<View style={{ flex: 1, paddingRight: 12 }}>
<Text style={styles.rowTitle}> · Face </Text>
<Text style={styles.hint}>
Face로 . 2 .
</Text>
<Text style={styles.rowTitle}>{t('profile.bioTitle')}</Text>
<Text style={styles.hint}>{t('profile.bioDesc')}</Text>
</View>
<Switch
testID="profile-biometric-toggle"
@ -374,35 +368,36 @@ export default function ProfileScreen() {
/>
</View>
) : (
<Text style={styles.hint}>
/Face가 .
.
</Text>
<Text style={styles.hint}>{t('profile.bioUnsupported')}</Text>
)}
</Card>
{/* ── 언어 설정 ── */}
<Text style={styles.section}>{t('lang.title')}</Text>
<LanguageSelector />
{/* ── 환경설정(알림) ── */}
<Text style={styles.section}> </Text>
<Text style={styles.section}>{t('profile.sectionNotify')}</Text>
<Card>
{(
[
['notifyDeadline', '마감 D-데이 알림'],
['notifyApproval', '승인·검수 알림'],
['notifyPayment', '결제·정산 알림'],
['notifyDeadline', t('profile.notifyDeadline')],
['notifyApproval', t('profile.notifyApproval')],
['notifyPayment', t('profile.notifyPayment')],
] as const
).map(([key, label]) => (
<View key={key} style={styles.bioRow}>
<Text style={[styles.rowTitle, { flex: 1 }]}>{label}</Text>
<Switch
testID={`profile-pref-${key}`}
value={prefs[key]}
value={prefs[key as keyof Prefs]}
onValueChange={(v) => savePrefs({ ...prefs, [key]: v })}
trackColor={{ false: colors.neutral200, true: colors.primary600 }}
thumbColor={colors.white}
/>
</View>
))}
<Text style={styles.hint}> . .</Text>
<Text style={styles.hint}>{t('profile.notifyNote')}</Text>
</Card>
{/* ── 로그아웃 ── */}
@ -410,26 +405,26 @@ export default function ProfileScreen() {
style={styles.logoutBtn}
onPress={onSignOut}
accessibilityRole="button"
accessibilityLabel="로그아웃"
accessibilityLabel={t('profile.signOut')}
>
<Ionicons name="log-out-outline" size={18} color={colors.error} />
<Text style={styles.logoutText}></Text>
<Text style={styles.logoutText}>{t('profile.signOut')}</Text>
</Pressable>
<Text style={styles.version}>KINTEX AI · </Text>
<Text style={styles.version}>{t('profile.version')}</Text>
{/* ── 프로필 사진 미리보기 모달(선택→미리보기→적용 확정) ── */}
<Modal visible={!!previewImg} transparent animationType="fade" onRequestClose={cancelPreview}>
<Pressable style={styles.modalBackdrop} onPress={cancelPreview}>
<Pressable style={styles.modalSheet} onPress={() => {}}>
<View style={styles.modalHeaderRow}>
<Text style={styles.modalTitle}> </Text>
<Text style={styles.modalTitle}>{t('profile.photoChange')}</Text>
<Pressable
testID="photo-cancel-x"
onPress={cancelPreview}
disabled={photoBusy}
hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
accessibilityLabel="닫기"
accessibilityLabel={t('common.close')}
>
<Ionicons name="close" size={22} color={colors.neutral500} />
</Pressable>
@ -450,7 +445,7 @@ export default function ProfileScreen() {
</View>
) : null}
</View>
<Text style={styles.previewCaption}> .</Text>
<Text style={styles.previewCaption}>{t('profile.photoCaption')}</Text>
<View style={styles.modalPickRow}>
<Pressable
@ -460,7 +455,7 @@ export default function ProfileScreen() {
disabled={photoBusy}
>
<Ionicons name="image-outline" size={16} color={colors.neutral900} />
<Text style={styles.pickText}></Text>
<Text style={styles.pickText}>{t('profile.photoLibrary')}</Text>
</Pressable>
<Pressable
testID="photo-pick-camera"
@ -469,7 +464,7 @@ export default function ProfileScreen() {
disabled={photoBusy}
>
<Ionicons name="camera-outline" size={16} color={colors.neutral900} />
<Text style={styles.pickText}></Text>
<Text style={styles.pickText}>{t('profile.photoCamera')}</Text>
</Pressable>
</View>
@ -484,10 +479,10 @@ export default function ProfileScreen() {
{photoBusy ? (
<View style={styles.applyBusyRow}>
<ActivityIndicator color={colors.white} size="small" />
<Text style={styles.applyText}> </Text>
<Text style={styles.applyText}>{t('profile.photoApplying')}</Text>
</View>
) : (
<Text style={styles.applyText}></Text>
<Text style={styles.applyText}>{t('profile.photoApply')}</Text>
)}
</Pressable>
<Pressable
@ -496,7 +491,7 @@ export default function ProfileScreen() {
onPress={cancelPreview}
disabled={photoBusy}
>
<Text style={styles.cancelText}></Text>
<Text style={styles.cancelText}>{t('common.cancel')}</Text>
</Pressable>
</Pressable>
</Pressable>

View File

@ -4,6 +4,7 @@
*/
import { router } from 'expo-router';
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { KeyboardAvoidingView, Platform, ScrollView, StyleSheet, Text } from 'react-native';
import { Banner } from '../components/Banner';
import { Button } from '../components/Button';
@ -13,6 +14,7 @@ import { register } from '../lib/auth';
import { colors, spacing, type } from '../theme';
export default function RegisterScreen() {
const { t } = useTranslation();
const [email, setEmail] = useState('');
const [displayName, setDisplayName] = useState('');
const [password, setPassword] = useState('');
@ -25,7 +27,7 @@ export default function RegisterScreen() {
async function onSubmit() {
setError(null);
if (!email.trim() || !displayName.trim() || password.length < 8) {
setError('이메일·이름·비밀번호(8자 이상)를 확인해 주세요.');
setError(t('register.errInvalid'));
return;
}
setLoading(true);
@ -37,18 +39,14 @@ export default function RegisterScreen() {
companyName: companyName.trim() || undefined,
inviteCode: inviteCode.trim() || undefined,
});
setDone(
res.joinedEvent
? '가입이 완료되었고 초대 행사에 연결되었습니다. 로그인해 주세요.'
: '가입이 완료되었습니다. 로그인해 주세요.',
);
setDone(res.joinedEvent ? t('register.doneJoined') : t('register.done'));
} catch (e) {
if (e instanceof ApiRequestError && e.code === 'EMAIL_TAKEN') {
setError('이미 가입된 이메일입니다.');
setError(t('register.errEmailTaken'));
} else if (e instanceof ApiRequestError) {
setError(e.message);
} else {
setError('회원가입에 실패했습니다.');
setError(t('register.errGeneric'));
}
} finally {
setLoading(false);
@ -61,20 +59,20 @@ export default function RegisterScreen() {
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
>
<ScrollView contentContainerStyle={styles.scroll} keyboardShouldPersistTaps="handled">
<Text style={styles.title}> </Text>
<Text style={styles.title}>{t('register.title')}</Text>
{error ? <Banner tone="error">{error}</Banner> : null}
{done ? <Banner tone="info">{done}</Banner> : null}
<Field label="이메일" value={email} onChangeText={setEmail} placeholder="name@company.co.kr" keyboardType="email-address" />
<Field label="이름" value={displayName} onChangeText={setDisplayName} placeholder="홍길동" autoCapitalize="none" />
<Field label="비밀번호" value={password} onChangeText={setPassword} placeholder="8자 이상" secureTextEntry helperText="8~100자" />
<Field label="회사명 (선택)" value={companyName} onChangeText={setCompanyName} placeholder="지오인포" />
<Field label="초대 코드 (선택)" value={inviteCode} onChangeText={setInviteCode} placeholder="행사 초대 코드" />
<Field label={t('register.email')} value={email} onChangeText={setEmail} placeholder={t('register.emailPlaceholder')} keyboardType="email-address" />
<Field label={t('register.name')} value={displayName} onChangeText={setDisplayName} placeholder={t('register.namePlaceholder')} autoCapitalize="none" />
<Field label={t('register.password')} value={password} onChangeText={setPassword} placeholder={t('register.passwordPlaceholder')} secureTextEntry helperText={t('register.passwordHelper')} />
<Field label={t('register.company')} value={companyName} onChangeText={setCompanyName} placeholder={t('register.companyPlaceholder')} />
<Field label={t('register.invite')} value={inviteCode} onChangeText={setInviteCode} placeholder={t('register.invitePlaceholder')} />
{done ? (
<Button label="로그인으로 이동" onPress={() => router.replace('/login')} />
<Button label={t('register.goLogin')} onPress={() => router.replace('/login')} />
) : (
<Button label="가입하기" onPress={onSubmit} loading={loading} />
<Button label={t('register.submit')} onPress={onSubmit} loading={loading} />
)}
</ScrollView>
</KeyboardAvoidingView>

View File

@ -7,6 +7,7 @@
import { Ionicons } from '@expo/vector-icons';
import { router, Stack } from 'expo-router';
import React, { useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Alert, Pressable, ScrollView, StyleSheet, Text, View } from 'react-native';
import { Banner } from '../../components/Banner';
import { useSecureScreen } from '../../context/SecureScreenContext';
@ -21,6 +22,7 @@ import {
import { colors, radius, spacing, type } from '../../theme';
export default function TicketWalletScreen() {
const { t } = useTranslation();
// 티켓 QR 화면 — 캡처 차단(QR 재사용 방지) + 백그라운드 마스킹(B4/B7).
useSecureScreen('tickets');
@ -45,24 +47,24 @@ export default function TicketWalletScreen() {
setQrOpen(true);
}
function openDetail(t: SampleTicket) {
function openDetail(ticket: SampleTicket) {
// SCR-P8(예매 확인·취소) 미구현 → 안내만(샘플)
Alert.alert(
'예매 상세·취소',
`${t.eventName}\n예매번호 ${t.bookingNoMasked}\n\n예매 확인·취소 화면(SCR-P8)은 티켓 백엔드(M10) 연동 후 제공됩니다.`,
t('tickets.detailTitle'),
`${ticket.eventName}\n${ticket.bookingNoMasked}`,
);
}
return (
<View style={styles.flex}>
<Stack.Screen options={{ title: '내 티켓', headerTitleAlign: 'center' }} />
<Stack.Screen options={{ title: t('nav.ticketsIndex'), headerTitleAlign: 'center' }} />
<ScrollView contentContainerStyle={styles.scroll}>
{/* 오프라인 표시 배지 */}
<View style={styles.offlineWrap}>
<View style={styles.offlineBadge}>
<View style={styles.offlineDot} />
<Text style={styles.offlineText}> </Text>
<Text style={styles.offlineText}>{t('tickets.offline')}</Text>
</View>
</View>
@ -85,22 +87,18 @@ export default function TicketWalletScreen() {
</View>
{/* 배지 전환 안내 배너 */}
<Banner tone="info">
QR로
</Banner>
<Banner tone="info">{t('tickets.badgeNotice')}</Banner>
{/* 티켓 리스트 / 빈 상태 */}
{filtered.length === 0 ? (
<EmptyState />
) : (
filtered.map((t) => (
<TicketCard key={t.id} ticket={t} onOpenQr={openQr} onDetail={openDetail} />
filtered.map((tk) => (
<TicketCard key={tk.id} ticket={tk} onOpenQr={openQr} onDetail={openDetail} />
))
)}
<Text style={styles.footNote}>
(M10) .
</Text>
<Text style={styles.footNote}>{t('tickets.footNote')}</Text>
</ScrollView>
<QrViewerModal
@ -115,17 +113,18 @@ export default function TicketWalletScreen() {
}
function EmptyState() {
const { t } = useTranslation();
return (
<View style={styles.empty}>
<Ionicons name="ticket-outline" size={44} color={colors.neutral200} />
<Text style={styles.emptyTitle}> </Text>
<Text style={styles.emptyBody}> .</Text>
<Text style={styles.emptyTitle}>{t('tickets.emptyTitle')}</Text>
<Text style={styles.emptyBody}>{t('tickets.emptyBody')}</Text>
<Pressable
accessibilityRole="button"
style={styles.emptyBtn}
onPress={() => router.push('/tickets/select')}
>
<Text style={styles.emptyBtnText}> </Text>
<Text style={styles.emptyBtnText}>{t('tickets.emptyBtn')}</Text>
</Pressable>
</View>
);

View File

@ -0,0 +1,68 @@
/*
* ko/en/zh/ja . more/profile .
* + (AsyncStorage `kintex_lang`) + (react-i18next).
*/
import React from 'react';
import { useTranslation } from 'react-i18next';
import { Pressable, StyleSheet, Text, View } from 'react-native';
import { useLanguage } from '../context/LanguageContext';
import type { Lang } from '../lib/i18n/types';
import { colors, radius, spacing, touch, type } from '../theme';
import { Card } from './Card';
export function LanguageSelector() {
const { t } = useTranslation();
const { lang, supported, setLanguage } = useLanguage();
return (
<Card>
<Text style={styles.title}>{t('lang.title')}</Text>
<Text style={styles.subtitle}>{t('lang.subtitle')}</Text>
<View style={styles.segment}>
{supported.map((code: Lang) => {
const on = lang === code;
return (
<Pressable
key={code}
accessibilityRole="button"
accessibilityState={{ selected: on }}
accessibilityLabel={t(`lang.${code}` as const)}
style={[styles.segBtn, on && styles.segBtnOn]}
onPress={() => {
if (!on) void setLanguage(code);
}}
>
<Text style={[styles.segText, on && styles.segTextOn]} numberOfLines={1}>
{t(`lang.${code}` as const)}
</Text>
</Pressable>
);
})}
</View>
</Card>
);
}
const styles = StyleSheet.create({
title: { color: colors.neutral900, fontSize: type.h3.fontSize, fontWeight: '700' },
subtitle: { color: colors.neutral500, fontSize: type.caption.fontSize, marginTop: 4, marginBottom: spacing.sm },
segment: {
flexDirection: 'row',
backgroundColor: colors.neutral050,
borderWidth: 1,
borderColor: colors.neutral200,
borderRadius: radius.md,
padding: 4,
gap: 4,
},
segBtn: {
flex: 1,
minHeight: touch.min,
alignItems: 'center',
justifyContent: 'center',
borderRadius: radius.sm,
},
segBtnOn: { backgroundColor: colors.primary600 },
segText: { fontSize: type.caption.fontSize, fontWeight: '700', color: colors.neutral700 },
segTextOn: { color: colors.white },
});

View File

@ -11,11 +11,13 @@ import React, {
useState,
} from 'react';
import { setAccessToken } from '../lib/api';
import { landingPathFor } from '../lib/roleTrack';
import { secureDelete, secureGet, secureSet } from '../lib/secureStore';
import type { AuthUser, LoginResponse, WorkspaceDto } from '../lib/types';
const TOKEN_KEY = 'kintex.accessToken';
const USER_KEY = 'kintex.user';
const LANDING_KEY = 'kintex.landing';
interface AuthState {
ready: boolean;
@ -23,6 +25,8 @@ interface AuthState {
user: AuthUser | null;
workspaces: WorkspaceDto[];
activeEventId: string | null;
/** 로그인 시 역할로 계산한 랜딩 라우트(콜드 부팅 시 workspaces 미복원 문제 회피용으로 지속). */
landingPath: string | null;
}
interface AuthContextValue extends AuthState {
@ -41,6 +45,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
user: null,
workspaces: [],
activeEventId: null,
landingPath: null,
});
// 부팅 시 토큰 복원
@ -49,10 +54,11 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
(async () => {
const token = await secureGet(TOKEN_KEY);
const userRaw = await secureGet(USER_KEY);
const landingPath = await secureGet(LANDING_KEY);
if (cancelled) return;
const user = userRaw ? (JSON.parse(userRaw) as AuthUser) : null;
if (token) setAccessToken(token);
setState((s) => ({ ...s, ready: true, token, user }));
setState((s) => ({ ...s, ready: true, token, user, landingPath }));
})();
return () => {
cancelled = true;
@ -61,14 +67,18 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
const signIn = useCallback(async (res: LoginResponse) => {
setAccessToken(res.accessToken);
// 역할 우선순위로 랜딩 경로를 계산·지속(콜드 부팅 시 재사용 — workspaces 미복원 misroute 방지).
const landingPath = landingPathFor({ user: res.user, workspaces: res.workspaces });
await secureSet(TOKEN_KEY, res.accessToken);
await secureSet(USER_KEY, JSON.stringify(res.user));
await secureSet(LANDING_KEY, landingPath);
setState((s) => ({
...s,
token: res.accessToken,
user: res.user,
workspaces: res.workspaces ?? [],
activeEventId: res.workspaces?.[0]?.eventId ?? null,
landingPath,
}));
}, []);
@ -76,12 +86,14 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
setAccessToken(null);
await secureDelete(TOKEN_KEY);
await secureDelete(USER_KEY);
await secureDelete(LANDING_KEY);
setState((s) => ({
...s,
token: null,
user: null,
workspaces: [],
activeEventId: null,
landingPath: null,
}));
}, []);

View File

@ -0,0 +1,62 @@
/*
* + API .
* i18n lib/i18n() ( ).
* react-i18next가 languageChanged에 useTranslation .
*/
import React, { createContext, useContext, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { loadStoredLanguage, normalizeLang, setAppLanguage } from '../lib/i18n';
import { SUPPORTED_LANGS, type Lang } from '../lib/i18n/types';
interface LanguageContextValue {
lang: Lang;
setLanguage: (lang: Lang) => Promise<void>;
supported: readonly Lang[];
}
const LanguageContext = createContext<LanguageContextValue | undefined>(undefined);
export function LanguageProvider({ children }: { children: React.ReactNode }) {
const { i18n } = useTranslation();
const [lang, setLang] = useState<Lang>(normalizeLang(i18n.language));
// 부팅 시 저장된 사용자 선택 언어 반영(없으면 디바이스 언어 유지).
useEffect(() => {
let cancelled = false;
loadStoredLanguage().then((l) => {
if (!cancelled) setLang(l);
});
return () => {
cancelled = true;
};
}, []);
// 외부(예: 다른 탭)에서 언어가 바뀌어도 로컬 상태 동기화.
useEffect(() => {
const onChanged = (l: string) => setLang(normalizeLang(l));
i18n.on('languageChanged', onChanged);
return () => {
i18n.off('languageChanged', onChanged);
};
}, [i18n]);
const value = useMemo<LanguageContextValue>(
() => ({
lang,
supported: SUPPORTED_LANGS,
setLanguage: async (l: Lang) => {
await setAppLanguage(l);
setLang(l);
},
}),
[lang],
);
return <LanguageContext.Provider value={value}>{children}</LanguageContext.Provider>;
}
export function useLanguage(): LanguageContextValue {
const ctx = useContext(LanguageContext);
if (!ctx) throw new Error('useLanguage must be used within LanguageProvider');
return ctx;
}

83
mobile/lib/i18n/index.ts Normal file
View File

@ -0,0 +1,83 @@
/*
* i18n react-i18next + expo-localization.
* · 언어: ko(/)·en·zh·ja
* · (initImmediate:false) expo-localization getLocales() .
* · AsyncStorage `kintex_lang` . .
* 보안: 번역 릿/ . ( ).
*/
import AsyncStorage from '@react-native-async-storage/async-storage';
import { getLocales } from 'expo-localization';
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import { en } from './locales/en';
import { ja } from './locales/ja';
import { ko } from './locales/ko';
import { zh } from './locales/zh';
import { DEFAULT_LANG, SUPPORTED_LANGS, type Lang } from './types';
export const LANG_STORAGE_KEY = 'kintex_lang';
export const resources = {
ko: { translation: ko },
en: { translation: en },
zh: { translation: zh },
ja: { translation: ja },
} as const;
/** 지원 언어로 정규화(미지원 → 기본). 'zh-Hans' 등 지역 태그는 앞 2자로 축약. */
export function normalizeLang(code: string | null | undefined): Lang {
if (!code) return DEFAULT_LANG;
const base = code.toLowerCase().split(/[-_]/)[0];
return (SUPPORTED_LANGS as readonly string[]).includes(base) ? (base as Lang) : DEFAULT_LANG;
}
/** 디바이스 로케일(동기) → 지원 언어. */
function detectDeviceLang(): Lang {
try {
const locales = getLocales();
return normalizeLang(locales?.[0]?.languageCode ?? locales?.[0]?.languageTag);
} catch {
return DEFAULT_LANG;
}
}
// ── 동기 초기화(디바이스 로케일 기준). 저장된 사용자 선택은 부팅 직후 loadStoredLanguage로 반영. ──
if (!i18n.isInitialized) {
i18n.use(initReactI18next).init({
resources,
lng: detectDeviceLang(),
fallbackLng: DEFAULT_LANG,
supportedLngs: SUPPORTED_LANGS as unknown as string[],
defaultNS: 'translation',
interpolation: { escapeValue: false },
returnNull: false,
initImmediate: false, // 최초 렌더에서 t()가 즉시 동작하도록 동기 초기화
});
}
/** 저장된 사용자 선택 언어를 로드해 적용(부팅 시 1회). 없으면 디바이스 언어 유지. */
export async function loadStoredLanguage(): Promise<Lang> {
try {
const saved = await AsyncStorage.getItem(LANG_STORAGE_KEY);
if (saved) {
const lang = normalizeLang(saved);
if (i18n.language !== lang) await i18n.changeLanguage(lang);
return lang;
}
} catch {
/* graceful — 디바이스 언어 유지 */
}
return normalizeLang(i18n.language);
}
/** 언어 변경 + 지속. 셀렉터 UI에서 호출. */
export async function setAppLanguage(lang: Lang): Promise<void> {
await i18n.changeLanguage(lang);
try {
await AsyncStorage.setItem(LANG_STORAGE_KEY, lang);
} catch {
/* graceful — 메모리 상 언어는 이미 반영됨 */
}
}
export default i18n;

View File

@ -0,0 +1,204 @@
/* English (en). Mirrors ko key shape (enforced by Translations type). */
import type { Translations } from '../types';
export const en: Translations = {
common: {
appName: 'KINTEX AI Exhibition',
loading: 'Checking…',
retry: 'Retry',
cancel: 'Cancel',
close: 'Close',
user: 'User',
none: '-',
unknown: 'Unknown',
error: 'Error',
},
lang: {
title: 'Language',
subtitle: 'Choose the app language. Your choice is saved on this device.',
ko: '한국어',
en: 'English',
zh: '中文',
ja: '日本語',
},
roles: {
ORGANIZER: 'Organizer',
EXHIBITOR: 'Exhibitor',
CONTRACTOR: 'Contractor',
HALL_MANAGER: 'Hall Manager',
},
nav: {
register: 'Sign up',
forgotPassword: 'Find password',
profile: 'My Info',
checklist: 'Site Checklist',
inspection: 'Site Inspection',
ticketsIndex: 'My Tickets',
ticketsSelect: 'Book Ticket',
tabHome: 'Home',
tabField: 'Site',
tabGallery: 'Gallery',
tabMore: 'More',
},
login: {
brandTitle: 'The moment you apply,\nsee the post-build photo first',
brandCaption: 'AI-generated preview · may differ from the actual build',
compromised:
'This device shows security risks (root/hook/debugger). To protect your data and tickets, please use a normal device.',
title: 'Sign in',
email: 'Email',
emailPlaceholder: 'name@company.com',
password: 'Password',
passwordPlaceholder: 'Password',
otpLabel: 'Two-factor code',
otpPlaceholder: '6-digit code',
otpHelper: '6-digit code from your authenticator (TOTP)',
remember: 'Remember email',
submit: 'Sign in',
submitOtp: 'Verify & sign in',
forgot: 'Find password',
register: 'Sign up',
errEmpty: 'Please enter your email and password.',
errOtpEmpty: 'Please enter the two-factor code.',
errOtpRequired: 'A two-factor code is required. Enter the code from your authenticator app.',
errOtpInvalid: 'The code is incorrect or expired.',
errLocked: 'Account locked after too many attempts. Please try again later.',
errGeneric: 'Sign in failed.',
},
register: {
title: 'Create account',
email: 'Email',
emailPlaceholder: 'name@company.com',
name: 'Name',
namePlaceholder: 'John Doe',
password: 'Password',
passwordPlaceholder: '8+ characters',
passwordHelper: '8100 characters',
company: 'Company (optional)',
companyPlaceholder: 'Zioinfo',
invite: 'Invite code (optional)',
invitePlaceholder: 'Event invite code',
submit: 'Sign up',
goLogin: 'Go to sign in',
errInvalid: 'Please check email, name and password (8+ characters).',
errEmailTaken: 'This email is already registered.',
errGeneric: 'Sign up failed.',
doneJoined: 'Registration complete and linked to the invited event. Please sign in.',
done: 'Registration complete. Please sign in.',
},
forgot: {
title: 'Reset password',
email: 'Email',
emailPlaceholder: 'name@company.com',
request: 'Request reset code',
code: 'Reset code',
codePlaceholder: '6-digit code',
newPassword: 'New password',
newPasswordPlaceholder: '8+ characters',
newPasswordHelper: '8100 characters',
submit: 'Change password',
errEmail: 'Please enter your email.',
errFields: 'Please check the reset code and new password (8+ characters).',
errCode: 'The reset code is incorrect or expired.',
errRequest: 'Request failed.',
errReset: 'Reset failed.',
},
home: {
hello: 'Hello, {{name}}',
noEvent: 'You are not part of any event — check your invite link or code.',
progress: 'Progress',
stepApply: 'Apply',
stepApprove: 'Approve',
stepBuild: 'Build',
stepInspect: 'Inspect',
actionChecklist: 'Site Checklist',
actionInspection: 'Site Inspection',
actionGallery: 'Visualization Gallery',
memberInternal: 'KINTEX internal account (Hall Manager)',
memberGeneral: 'General account',
},
field: {
title: 'Site Work',
guide: 'Site features are available to contractors (checklist) and hall managers (inspection).',
checklistTitle: 'Site Checklist',
checklistDesc: 'Check work steps · attach photos · request inspection',
inspectionTitle: 'Site Inspection',
inspectionDesc: 'Approved drawing vs site photo · pass/fail decision',
},
gallery: {
infoBar: 'AI-generated images cannot be used for contracts or reviews — drawings are the build standard',
degraded: 'Cannot reach the server image history; showing samples (degraded).',
empty: 'No images yet — generate preview photos in the design studio.',
retry: 'Regenerate',
studioNote: 'Edit the design studio on desktop',
notice: 'AI-generated images cannot be used for contracts or reviews (drawings are the standard)',
},
more: {
accountInternal: 'KINTEX internal account (Hall Manager)',
accountGeneral: 'General account',
openProfile: 'Open my info',
apiServer: 'API server',
serverStatus: 'Server status',
statusUp: 'Healthy (UP)',
statusDown: 'Unreachable (degraded)',
signOut: 'Sign out',
version: 'KINTEX AI Exhibition · v0.1.0',
},
profile: {
sectionInfo: 'My Info',
sectionEventRoles: 'Event Roles',
sectionSecurity: 'Account · Security',
sectionBiometric: 'Biometric Lock',
sectionNotify: 'Notifications',
labelName: 'Name',
labelUserId: 'User ID',
labelGlobalRole: 'Global role',
labelTenant: 'Venue (tenant)',
labelType: 'Type',
typeInternal: 'KINTEX internal (Hall Manager)',
typeGeneral: 'General account',
photoChange: 'Change profile photo',
photoTitle: 'Profile photo',
photoSelect: 'Choose a photo',
photoLibrary: 'Library',
photoCamera: 'Camera',
photoApply: 'Apply',
photoApplying: 'Applying…',
photoCaption: 'Displayed as a circle.',
photoPending: 'The selected photo is shown on this device only — server sync is in preparation.',
photoPickerUnavailable: 'Image selection is not available on this device.',
loadErr: 'Failed to load your info.',
uploadErr: 'Photo upload failed.',
otpTitle: 'Two-factor (OTP)',
otpOnAuth: 'Enabled · Authenticator (OTP)',
otpOnMethod: 'Enabled · {{method}}',
otpOff: 'Not set — 2-step authenticator (TOTP) is used at sign in.',
on: 'On',
off: 'Off',
bioTitle: 'Fingerprint · Face lock',
bioDesc:
'When on, you re-authenticate with fingerprint or Face when opening My Info. Sign-in 2FA remains unchanged.',
bioUnsupported:
'This device does not support biometrics or has no fingerprint/Face enrolled. Enroll biometrics in device settings and try again.',
bioFailed: 'Biometric authentication failed. Please try again.',
biometric: 'Biometrics',
lockTitle: 'Biometric lock',
lockHint: 'Unlock with fingerprint or Face.',
notifyDeadline: 'Deadline D-day alerts',
notifyApproval: 'Approval · inspection alerts',
notifyPayment: 'Payment · settlement alerts',
notifyNote: 'Notification settings are saved on this device. Server sync is in preparation.',
signOut: 'Sign out',
signOutConfirm: 'Sign out?',
version: 'KINTEX AI Exhibition · My Info',
},
tickets: {
offline: 'Offline — showing saved tickets',
badgeNotice: 'Check in with the ticket QR at the venue to switch to a mobile badge',
footNote: 'Tickets shown are samples — replaced with live data when the ticket backend (M10) is connected.',
emptyTitle: 'You have no tickets',
emptyBody: 'Book an entry pass and your tickets will appear here.',
emptyBtn: 'Book entry pass',
detailTitle: 'Booking detail · cancel',
},
};

View File

@ -0,0 +1,204 @@
/* 日本語 (ja). Mirrors ko key shape (enforced by Translations type). */
import type { Translations } from '../types';
export const ja: Translations = {
common: {
appName: 'KINTEX AI 展示管理',
loading: '確認中…',
retry: '再試行',
cancel: 'キャンセル',
close: '閉じる',
user: 'ユーザー',
none: '-',
unknown: '不明',
error: 'エラー',
},
lang: {
title: '言語',
subtitle: 'アプリの表示言語を選択します。選択はこの端末に保存されます。',
ko: '한국어',
en: 'English',
zh: '中文',
ja: '日本語',
},
roles: {
ORGANIZER: '主催者',
EXHIBITOR: '出展社',
CONTRACTOR: '施工業者',
HALL_MANAGER: 'ホールマネージャー',
},
nav: {
register: '会員登録',
forgotPassword: 'パスワード再設定',
profile: 'マイ情報',
checklist: '現場チェックリスト',
inspection: '現場検査',
ticketsIndex: 'マイチケット',
ticketsSelect: 'チケット予約',
tabHome: 'ホーム',
tabField: '現場',
tabGallery: 'ギャラリー',
tabMore: 'その他',
},
login: {
brandTitle: '申請したその瞬間、\n施工後の写真を先に見る',
brandCaption: 'AI 生成の予想画像 · 実際の施工結果と異なる場合があります',
compromised:
'この端末にセキュリティリスクroot化・フック・デバッガが検出されました。個人情報とチケット保護のため、正常な端末での利用を推奨します。',
title: 'ログイン',
email: 'メール',
emailPlaceholder: 'name@company.com',
password: 'パスワード',
passwordPlaceholder: 'パスワード',
otpLabel: '二段階認証コード',
otpPlaceholder: '6桁コード',
otpHelper: '認証アプリTOTPの6桁コード',
remember: 'メールを記憶',
submit: 'ログイン',
submitOtp: '認証してログイン',
forgot: 'パスワード再設定',
register: '会員登録',
errEmpty: 'メールとパスワードを入力してください。',
errOtpEmpty: '二段階認証コードを入力してください。',
errOtpRequired: '二段階認証コードが必要です。認証アプリのコードを入力してください。',
errOtpInvalid: '認証コードが正しくないか、期限切れです。',
errLocked: 'ログイン試行超過によりアカウントがロックされました。しばらくして再試行してください。',
errGeneric: 'ログインに失敗しました。',
},
register: {
title: 'アカウント作成',
email: 'メール',
emailPlaceholder: 'name@company.com',
name: '氏名',
namePlaceholder: '山田太郎',
password: 'パスワード',
passwordPlaceholder: '8文字以上',
passwordHelper: '8〜100文字',
company: '会社名(任意)',
companyPlaceholder: 'Zioinfo',
invite: '招待コード(任意)',
invitePlaceholder: 'イベント招待コード',
submit: '登録する',
goLogin: 'ログインへ',
errInvalid: 'メール・氏名・パスワード8文字以上を確認してください。',
errEmailTaken: 'すでに登録済みのメールです。',
errGeneric: '会員登録に失敗しました。',
doneJoined: '登録が完了し、招待イベントに連携されました。ログインしてください。',
done: '登録が完了しました。ログインしてください。',
},
forgot: {
title: 'パスワード再設定',
email: 'メール',
emailPlaceholder: 'name@company.com',
request: '再設定コードを要求',
code: '再設定コード',
codePlaceholder: '6桁コード',
newPassword: '新しいパスワード',
newPasswordPlaceholder: '8文字以上',
newPasswordHelper: '8〜100文字',
submit: 'パスワード変更',
errEmail: 'メールを入力してください。',
errFields: '再設定コードと新しいパスワード8文字以上を確認してください。',
errCode: '再設定コードが正しくないか、期限切れです。',
errRequest: 'リクエストに失敗しました。',
errReset: '初期化に失敗しました。',
},
home: {
hello: '{{name}}さん、こんにちは',
noEvent: '参加中のイベントがありません — 招待リンクまたは招待コードをご確認ください。',
progress: '進行状況',
stepApply: '申請',
stepApprove: '承認',
stepBuild: '施工',
stepInspect: '検査',
actionChecklist: '現場チェックリスト',
actionInspection: '現場検査',
actionGallery: 'ビジュアルギャラリー',
memberInternal: 'KINTEX 内部アカウント(ホールマネージャー)',
memberGeneral: '一般アカウント',
},
field: {
title: '現場業務',
guide: '現場機能は施工業者(チェックリスト)とホールマネージャー(検査)の役割で提供されます。',
checklistTitle: '現場チェックリスト',
checklistDesc: '施工工程の確認 · 写真添付 · 検査依頼',
inspectionTitle: '現場検査',
inspectionDesc: '承認図面 vs 現場写真 · 適合/不適合の判定',
},
gallery: {
infoBar: 'AI 生成画像は契約・審査書類に使用できません — 施工基準は図面です',
degraded: 'サーバーの画像履歴に接続できないため、サンプルを表示しますdegraded。',
empty: 'まだ生成された画像がありません — 設計スタジオで予想写真を生成してください。',
retry: '再生成',
studioNote: '設計スタジオはデスクトップで編集',
notice: 'AI 生成画像は契約・審査書類に使用できません(施工基準は図面)',
},
more: {
accountInternal: 'KINTEX 内部アカウント(ホールマネージャー)',
accountGeneral: '一般アカウント',
openProfile: 'マイ情報を開く',
apiServer: 'API サーバー',
serverStatus: 'サーバー状態',
statusUp: '正常 (UP)',
statusDown: '接続不可 (degraded)',
signOut: 'ログアウト',
version: 'KINTEX AI 展示管理 · v0.1.0',
},
profile: {
sectionInfo: 'マイ情報',
sectionEventRoles: '参加イベントの役割',
sectionSecurity: 'アカウント · セキュリティ',
sectionBiometric: '生体認証ロック',
sectionNotify: '通知設定',
labelName: '氏名',
labelUserId: 'ユーザー ID',
labelGlobalRole: 'グローバル役割',
labelTenant: '展示館(テナント)',
labelType: '区分',
typeInternal: 'KINTEX 内部(ホールマネージャー)',
typeGeneral: '一般アカウント',
photoChange: 'プロフィール写真を変更',
photoTitle: 'プロフィール写真',
photoSelect: '写真を選択してください',
photoLibrary: 'ギャラリー',
photoCamera: 'カメラ',
photoApply: '適用',
photoApplying: '適用中…',
photoCaption: '円形で表示されます。',
photoPending: '選択した写真はこの端末のみに表示されます — サーバー反映は準備中です。',
photoPickerUnavailable: 'この端末では画像選択機能を利用できません。',
loadErr: 'マイ情報を読み込めませんでした。',
uploadErr: '写真のアップロードに失敗しました。',
otpTitle: '二段階認証OTP',
otpOnAuth: '有効 · AuthenticatorOTP',
otpOnMethod: '有効 · {{method}}',
otpOff: '未設定 — ログイン時に認証アプリTOTPの二段階認証を使用します。',
on: 'オン',
off: 'オフ',
bioTitle: '指紋 · Face ロック',
bioDesc:
'オンにするとマイ情報を開く際に指紋または Face で再認証します。ログインの二段階認証はそのまま維持されます。',
bioUnsupported:
'この端末は生体認証に対応していないか、指紋/Face が登録されていません。端末設定で生体情報を登録してから再試行してください。',
bioFailed: '生体認証に失敗しました。再試行してください。',
biometric: '生体認証',
lockTitle: '生体認証ロック',
lockHint: '指紋または Face でロックを解除してください。',
notifyDeadline: '締切 D-day 通知',
notifyApproval: '承認・検査通知',
notifyPayment: '決済・精算通知',
notifyNote: '通知設定はこの端末に保存されます。サーバー同期は準備中です。',
signOut: 'ログアウト',
signOutConfirm: 'ログアウトしますか?',
version: 'KINTEX AI 展示管理 · マイ情報',
},
tickets: {
offline: 'オフライン — 保存済みチケットを表示中',
badgeNotice: 'チケット QR で現場チェックインするとモバイルバッジに切り替わります',
footNote: '表示中のチケットはサンプルです — チケットバックエンドM10連携時に実データへ置き換わります。',
emptyTitle: '保有チケットがありません',
emptyBody: '入場券を予約するとここにチケットが表示されます。',
emptyBtn: '入場券を予約',
detailTitle: '予約詳細 · キャンセル',
},
};

View File

@ -0,0 +1,208 @@
/*
* (ko) / . ** (shape) .**
* (en/zh/ja) (Resource) tsc가 .
* ( · ).
*/
export const ko = {
common: {
appName: 'KINTEX AI 전시관리',
loading: '확인 중…',
retry: '다시 시도',
cancel: '취소',
close: '닫기',
user: '사용자',
none: '-',
unknown: '알 수 없음',
error: '오류',
},
lang: {
title: '언어',
subtitle: '앱 표시 언어를 선택하세요. 선택은 이 기기에 저장됩니다.',
ko: '한국어',
en: 'English',
zh: '中文',
ja: '日本語',
},
roles: {
ORGANIZER: '주최자',
EXHIBITOR: '참가업체',
CONTRACTOR: '장치·시공업체',
HALL_MANAGER: '홀매니저',
},
nav: {
register: '회원가입',
forgotPassword: '비밀번호 찾기',
profile: '내 정보',
checklist: '현장 체크리스트',
inspection: '현장 검수',
ticketsIndex: '내 티켓',
ticketsSelect: '티켓 예매',
tabHome: '홈',
tabField: '현장',
tabGallery: '갤러리',
tabMore: '더보기',
},
login: {
brandTitle: '신청서를 내는 순간,\n시공 후 사진을 먼저 봅니다',
brandCaption: 'AI 생성 예상 이미지 · 실제 시공 결과와 다를 수 있습니다',
compromised:
'보안 위험이 감지된 기기입니다(루팅·후킹·디버거). 개인정보·티켓 보호를 위해 정상 기기에서 이용을 권장합니다.',
title: '로그인',
email: '이메일',
emailPlaceholder: 'name@company.co.kr',
password: '비밀번호',
passwordPlaceholder: '비밀번호',
otpLabel: '2차 인증 코드',
otpPlaceholder: '6자리 코드',
otpHelper: '인증 앱(TOTP)의 6자리 코드',
remember: '아이디 기억',
submit: '로그인',
submitOtp: '인증 후 로그인',
forgot: '비밀번호 찾기',
register: '회원가입',
errEmpty: '이메일과 비밀번호를 입력해 주세요.',
errOtpEmpty: '2차 인증 코드를 입력해 주세요.',
errOtpRequired: '2차 인증 코드가 필요합니다. 인증 앱의 코드를 입력해 주세요.',
errOtpInvalid: '인증 코드가 올바르지 않거나 만료되었습니다.',
errLocked: '로그인 시도 초과로 계정이 잠겼습니다. 잠시 후 다시 시도해 주세요.',
errGeneric: '로그인에 실패했습니다.',
},
register: {
title: '계정 만들기',
email: '이메일',
emailPlaceholder: 'name@company.co.kr',
name: '이름',
namePlaceholder: '홍길동',
password: '비밀번호',
passwordPlaceholder: '8자 이상',
passwordHelper: '8~100자',
company: '회사명 (선택)',
companyPlaceholder: '지오인포',
invite: '초대 코드 (선택)',
invitePlaceholder: '행사 초대 코드',
submit: '가입하기',
goLogin: '로그인으로 이동',
errInvalid: '이메일·이름·비밀번호(8자 이상)를 확인해 주세요.',
errEmailTaken: '이미 가입된 이메일입니다.',
errGeneric: '회원가입에 실패했습니다.',
doneJoined: '가입이 완료되었고 초대 행사에 연결되었습니다. 로그인해 주세요.',
done: '가입이 완료되었습니다. 로그인해 주세요.',
},
forgot: {
title: '비밀번호 재설정',
email: '이메일',
emailPlaceholder: 'name@company.co.kr',
request: '재설정 코드 요청',
code: '재설정 코드',
codePlaceholder: '6자리 코드',
newPassword: '새 비밀번호',
newPasswordPlaceholder: '8자 이상',
newPasswordHelper: '8~100자',
submit: '비밀번호 변경',
errEmail: '이메일을 입력해 주세요.',
errFields: '재설정 코드와 새 비밀번호(8자 이상)를 확인해 주세요.',
errCode: '재설정 코드가 올바르지 않거나 만료되었습니다.',
errRequest: '요청에 실패했습니다.',
errReset: '초기화에 실패했습니다.',
},
home: {
hello: '{{name}}님, 안녕하세요',
noEvent: '참여 중인 행사가 없습니다 — 초대 링크 또는 초대 코드를 확인해 주세요.',
progress: '진행 상태',
stepApply: '신청',
stepApprove: '승인',
stepBuild: '시공',
stepInspect: '검수',
actionChecklist: '현장 체크리스트',
actionInspection: '현장 검수',
actionGallery: '시각화 갤러리',
memberInternal: '킨텍스 내부 계정 (홀매니저)',
memberGeneral: '일반 계정',
},
field: {
title: '현장 업무',
guide: '현장 기능은 장치·시공업체(체크리스트)와 홀매니저(검수) 역할에서 제공됩니다.',
checklistTitle: '현장 체크리스트',
checklistDesc: '장치 공정 확인 · 사진 첨부 · 검수 요청',
inspectionTitle: '현장 검수',
inspectionDesc: '승인 도면 vs 현장 사진 · 적합/부적합 판정',
},
gallery: {
infoBar: 'AI 생성 이미지는 계약·심사 서류에 사용할 수 없습니다 — 시공 기준은 도면입니다',
degraded: '서버 이미지 이력에 연결할 수 없어 예시를 표시합니다 (degraded).',
empty: '아직 생성된 이미지가 없습니다 — 설계 스튜디오에서 예상 사진을 생성하세요.',
retry: '다시 생성',
studioNote: '설계 스튜디오는 데스크톱에서 편집',
notice: 'AI 생성 이미지는 계약·심사 서류에 사용할 수 없습니다 (시공 기준은 도면)',
},
more: {
accountInternal: '킨텍스 내부 계정 (홀매니저)',
accountGeneral: '일반 계정',
openProfile: '내 정보 열기',
apiServer: 'API 서버',
serverStatus: '서버 상태',
statusUp: '정상 (UP)',
statusDown: '연결 불가 (degraded)',
signOut: '로그아웃',
version: 'KINTEX AI 전시관리 · v0.1.0',
},
profile: {
sectionInfo: '내 정보',
sectionEventRoles: '참여 행사 역할',
sectionSecurity: '계정 · 보안',
sectionBiometric: '생체인식 잠금',
sectionNotify: '알림 설정',
labelName: '이름',
labelUserId: '사용자 ID',
labelGlobalRole: '전역 역할',
labelTenant: '전시관(테넌트)',
labelType: '구분',
typeInternal: '킨텍스 내부(홀매니저)',
typeGeneral: '일반 계정',
photoChange: '프로필 사진 변경',
photoTitle: '프로필 사진',
photoSelect: '사진을 선택하세요',
photoLibrary: '갤러리',
photoCamera: '카메라',
photoApply: '적용',
photoApplying: '적용 중…',
photoCaption: '원형으로 표시됩니다.',
photoPending: '선택한 사진은 이 기기에만 표시됩니다 — 서버 반영은 준비 중입니다.',
photoPickerUnavailable: '이미지 선택 기능을 사용할 수 없는 기기입니다.',
loadErr: '내정보를 불러오지 못했습니다.',
uploadErr: '사진 업로드에 실패했습니다.',
otpTitle: '2차 인증(OTP)',
otpOnAuth: '활성화됨 · Authenticator(OTP)',
otpOnMethod: '활성화됨 · {{method}}',
otpOff: '미설정 — 로그인 시 인증 앱(TOTP) 2단계 인증을 사용합니다.',
on: '켜짐',
off: '꺼짐',
bioTitle: '지문 · Face 잠금',
bioDesc:
'켜면 내정보 진입 시 지문 또는 Face로 재인증합니다. 로그인 2차 인증은 그대로 유지됩니다.',
bioUnsupported:
'이 기기는 생체인식을 지원하지 않거나 지문/Face가 등록되어 있지 않습니다. 기기 설정에서 생체 정보를 등록한 뒤 다시 시도해 주세요.',
bioFailed: '생체 인증에 실패했습니다. 다시 시도해 주세요.',
biometric: '생체인식',
lockTitle: '생체인식 잠금',
lockHint: '지문 또는 Face로 잠금을 해제하세요.',
notifyDeadline: '마감 D-데이 알림',
notifyApproval: '승인·검수 알림',
notifyPayment: '결제·정산 알림',
notifyNote: '알림 설정은 이 기기에 저장됩니다. 서버 동기화는 준비 중입니다.',
signOut: '로그아웃',
signOutConfirm: '로그아웃하시겠습니까?',
version: 'KINTEX AI 전시관리 · 내정보',
},
tickets: {
offline: '오프라인 — 저장된 티켓 표시 중',
badgeNotice: '티켓 QR로 현장 체크인하면 모바일 배지로 전환됩니다',
footNote: '표시된 티켓은 샘플입니다 — 티켓 백엔드(M10) 연동 시 실데이터로 대체됩니다.',
emptyTitle: '보유한 티켓이 없습니다',
emptyBody: '입장권을 예매하면 이곳에 티켓이 표시됩니다.',
emptyBtn: '입장권 예매',
detailTitle: '예매 상세·취소',
},
} as const;
export type Resource = typeof ko;

View File

@ -0,0 +1,203 @@
/* 简体中文 (zh). Mirrors ko key shape (enforced by Translations type). */
import type { Translations } from '../types';
export const zh: Translations = {
common: {
appName: 'KINTEX AI 展览管理',
loading: '确认中…',
retry: '重试',
cancel: '取消',
close: '关闭',
user: '用户',
none: '-',
unknown: '未知',
error: '错误',
},
lang: {
title: '语言',
subtitle: '选择应用显示语言。选择将保存在本设备上。',
ko: '한국어',
en: 'English',
zh: '中文',
ja: '日本語',
},
roles: {
ORGANIZER: '主办方',
EXHIBITOR: '参展商',
CONTRACTOR: '搭建施工商',
HALL_MANAGER: '展厅经理',
},
nav: {
register: '注册',
forgotPassword: '找回密码',
profile: '我的信息',
checklist: '现场检查表',
inspection: '现场验收',
ticketsIndex: '我的门票',
ticketsSelect: '订票',
tabHome: '首页',
tabField: '现场',
tabGallery: '图库',
tabMore: '更多',
},
login: {
brandTitle: '提交申请的瞬间,\n先看到施工后的效果图',
brandCaption: 'AI 生成预览图 · 可能与实际施工结果不同',
compromised:
'检测到该设备存在安全风险(越狱·注入·调试器)。为保护个人信息和门票,建议在正常设备上使用。',
title: '登录',
email: '邮箱',
emailPlaceholder: 'name@company.com',
password: '密码',
passwordPlaceholder: '密码',
otpLabel: '二次验证码',
otpPlaceholder: '6位验证码',
otpHelper: '验证器应用TOTP的6位验证码',
remember: '记住邮箱',
submit: '登录',
submitOtp: '验证并登录',
forgot: '找回密码',
register: '注册',
errEmpty: '请输入邮箱和密码。',
errOtpEmpty: '请输入二次验证码。',
errOtpRequired: '需要二次验证码。请输入验证器应用中的验证码。',
errOtpInvalid: '验证码不正确或已过期。',
errLocked: '登录尝试过多,账户已锁定。请稍后重试。',
errGeneric: '登录失败。',
},
register: {
title: '创建账户',
email: '邮箱',
emailPlaceholder: 'name@company.com',
name: '姓名',
namePlaceholder: '张三',
password: '密码',
passwordPlaceholder: '8位以上',
passwordHelper: '8~100位',
company: '公司名(可选)',
companyPlaceholder: 'Zioinfo',
invite: '邀请码(可选)',
invitePlaceholder: '活动邀请码',
submit: '注册',
goLogin: '前往登录',
errInvalid: '请检查邮箱·姓名·密码8位以上。',
errEmailTaken: '该邮箱已注册。',
errGeneric: '注册失败。',
doneJoined: '注册完成并已关联受邀活动。请登录。',
done: '注册完成。请登录。',
},
forgot: {
title: '重置密码',
email: '邮箱',
emailPlaceholder: 'name@company.com',
request: '请求重置码',
code: '重置码',
codePlaceholder: '6位验证码',
newPassword: '新密码',
newPasswordPlaceholder: '8位以上',
newPasswordHelper: '8~100位',
submit: '修改密码',
errEmail: '请输入邮箱。',
errFields: '请检查重置码和新密码8位以上。',
errCode: '重置码不正确或已过期。',
errRequest: '请求失败。',
errReset: '重置失败。',
},
home: {
hello: '{{name}},您好',
noEvent: '您尚未参与任何活动 — 请确认邀请链接或邀请码。',
progress: '进度状态',
stepApply: '申请',
stepApprove: '审批',
stepBuild: '施工',
stepInspect: '验收',
actionChecklist: '现场检查表',
actionInspection: '现场验收',
actionGallery: '可视化图库',
memberInternal: 'KINTEX 内部账户(展厅经理)',
memberGeneral: '普通账户',
},
field: {
title: '现场工作',
guide: '现场功能面向搭建施工商(检查表)和展厅经理(验收)角色提供。',
checklistTitle: '现场检查表',
checklistDesc: '确认施工工序 · 附加照片 · 申请验收',
inspectionTitle: '现场验收',
inspectionDesc: '审批图纸 vs 现场照片 · 合格/不合格判定',
},
gallery: {
infoBar: 'AI 生成图片不可用于合同·审查文件 — 施工标准以图纸为准',
degraded: '无法连接服务器图片记录显示示例degraded。',
empty: '暂无生成的图片 — 请在设计工作室生成效果图。',
retry: '重新生成',
studioNote: '设计工作室请在桌面端编辑',
notice: 'AI 生成图片不可用于合同·审查文件(施工标准以图纸为准)',
},
more: {
accountInternal: 'KINTEX 内部账户(展厅经理)',
accountGeneral: '普通账户',
openProfile: '打开我的信息',
apiServer: 'API 服务器',
serverStatus: '服务器状态',
statusUp: '正常 (UP)',
statusDown: '无法连接 (degraded)',
signOut: '退出登录',
version: 'KINTEX AI 展览管理 · v0.1.0',
},
profile: {
sectionInfo: '我的信息',
sectionEventRoles: '参与活动角色',
sectionSecurity: '账户 · 安全',
sectionBiometric: '生物识别锁',
sectionNotify: '通知设置',
labelName: '姓名',
labelUserId: '用户 ID',
labelGlobalRole: '全局角色',
labelTenant: '展馆(租户)',
labelType: '类型',
typeInternal: 'KINTEX 内部(展厅经理)',
typeGeneral: '普通账户',
photoChange: '更换头像',
photoTitle: '头像',
photoSelect: '请选择照片',
photoLibrary: '相册',
photoCamera: '相机',
photoApply: '应用',
photoApplying: '应用中…',
photoCaption: '将以圆形显示。',
photoPending: '所选照片仅显示在本设备 — 服务器同步准备中。',
photoPickerUnavailable: '该设备无法使用图片选择功能。',
loadErr: '无法加载我的信息。',
uploadErr: '照片上传失败。',
otpTitle: '二次验证OTP',
otpOnAuth: '已启用 · AuthenticatorOTP',
otpOnMethod: '已启用 · {{method}}',
otpOff: '未设置 — 登录时使用验证器应用TOTP两步验证。',
on: '开',
off: '关',
bioTitle: '指纹 · Face 锁',
bioDesc: '开启后进入我的信息时需用指纹或 Face 再次验证。登录二次验证保持不变。',
bioUnsupported:
'该设备不支持生物识别,或未录入指纹/Face。请在设备设置中录入生物信息后重试。',
bioFailed: '生物识别验证失败。请重试。',
biometric: '生物识别',
lockTitle: '生物识别锁',
lockHint: '请用指纹或 Face 解锁。',
notifyDeadline: '截止 D-day 提醒',
notifyApproval: '审批·验收提醒',
notifyPayment: '支付·结算提醒',
notifyNote: '通知设置保存在本设备。服务器同步准备中。',
signOut: '退出登录',
signOutConfirm: '要退出登录吗?',
version: 'KINTEX AI 展览管理 · 我的信息',
},
tickets: {
offline: '离线 — 显示已保存门票',
badgeNotice: '在现场用门票二维码签到即可转换为移动徽章',
footNote: '所示门票为示例 — 门票后端M10连接后将替换为真实数据。',
emptyTitle: '暂无门票',
emptyBody: '预订入场券后,门票将显示在此处。',
emptyBtn: '预订入场券',
detailTitle: '预订详情 · 取消',
},
};

16
mobile/lib/i18n/types.ts Normal file
View File

@ -0,0 +1,16 @@
/*
* i18n .
* ko() DeepString으로 en/zh/ja가 tsc가 .
*/
import type { Resource } from './locales/ko';
export type DeepString<T> = {
[K in keyof T]: T[K] extends string ? string : DeepString<T[K]>;
};
/** 모든 로케일이 구현해야 하는 번역 형상. */
export type Translations = DeepString<Resource>;
export const SUPPORTED_LANGS = ['ko', 'en', 'zh', 'ja'] as const;
export type Lang = (typeof SUPPORTED_LANGS)[number];
export const DEFAULT_LANG: Lang = 'ko';

78
mobile/lib/roleTrack.ts Normal file
View File

@ -0,0 +1,78 @@
/*
* · () src/frontend/src/lib/roleTrack.ts ().
* 우선순위: admin > ops > business > agency > visitor. visitor() .
*
* ( ):
* · user(AuthUser) roleCode가 hallManager=true를 ops .
* (roleCode를 /api/auth/me의 MePrincipal.roleCode )
* · (/admin·/contractor/dashboard·/visitor·/home) .
*/
import type { AuthUser, EventRole, WorkspaceDto } from './types';
export type Track = 'visitor' | 'business' | 'agency' | 'ops' | 'admin';
/** 워크스페이스 역할 → 트랙(스코프 판정용). 웹 trackOf와 동일. */
export function trackOf(role: EventRole | null | undefined): Track {
switch (role) {
case 'ORGANIZER':
case 'EXHIBITOR':
return 'business';
case 'CONTRACTOR':
return 'agency';
case 'HALL_MANAGER':
return 'ops';
default:
return 'visitor';
}
}
/** 랜딩 판정 입력 — 로그인 응답 user + workspaces. roleCode는 알 때만(옵션). */
export interface LandingInput {
user?: (AuthUser & { roleCode?: string | null }) | null;
workspaces?: WorkspaceDto[] | null;
}
/**
* (primaryTrack ). resolveLandingTrack .
* roleCode('ADMIN'/'MANAGER') , hallManager로 ops를 .
*/
export function resolveLandingTrack({ user, workspaces }: LandingInput): Track {
const role = (user?.roleCode ?? '').toUpperCase();
const ws = workspaces ?? [];
if (role === 'ADMIN') return 'admin';
if (role === 'MANAGER' || user?.hallManager || ws.some((w) => w.myRole === 'HALL_MANAGER')) {
return 'ops';
}
if (ws.some((w) => w.myRole === 'ORGANIZER' || w.myRole === 'EXHIBITOR')) return 'business';
if (ws.some((w) => w.myRole === 'CONTRACTOR')) return 'agency';
return 'visitor';
}
/**
* ().
* · admin/ops/business (/(tabs)) admin ,
* · agency(·) (/(tabs)/field)
* · visitor() (/tickets) B2C
*/
export function landingPathForTrack(track: Track): string {
switch (track) {
case 'agency':
return '/(tabs)/field';
case 'visitor':
return '/tickets';
case 'admin':
case 'ops':
case 'business':
default:
return '/(tabs)';
}
}
/** 입력으로부터 랜딩 라우트 계산(예외 시 탭 홈 폴백). */
export function landingPathFor(input: LandingInput): string {
try {
return landingPathForTrack(resolveLandingTrack(input));
} catch {
return '/(tabs)';
}
}

View File

@ -17,14 +17,17 @@
"expo-image-picker": "~15.1.0",
"expo-linking": "~6.3.1",
"expo-local-authentication": "~14.0.1",
"expo-localization": "~15.0.3",
"expo-router": "~3.5.23",
"expo-screen-capture": "~6.0.1",
"expo-secure-store": "~13.0.2",
"expo-splash-screen": "~0.27.7",
"expo-status-bar": "~1.12.1",
"i18next": "^23.16.8",
"jail-monkey": "^2.8.0",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-i18next": "^14.1.3",
"react-native": "0.74.5",
"react-native-gesture-handler": "~2.16.1",
"react-native-safe-area-context": "4.10.5",
@ -7742,11 +7745,24 @@
"expo": "*"
}
},
"node_modules/expo-localization": {
"version": "15.0.3",
"resolved": "https://registry.npmjs.org/expo-localization/-/expo-localization-15.0.3.tgz",
"integrity": "sha512-IfcmlKuKRlowR9qIzL0e+nGHBeNoF7l2GQaOJstc7HZiPjNJ4J1R4D53ZNf483dt7JSkTRJBihdTadOtOEjRdg==",
"license": "MIT",
"dependencies": {
"rtl-detect": "^1.0.2"
},
"peerDependencies": {
"expo": "*"
}
},
"node_modules/expo-modules-autolinking": {
"version": "1.11.3",
"resolved": "https://registry.npmjs.org/expo-modules-autolinking/-/expo-modules-autolinking-1.11.3.tgz",
"integrity": "sha512-oYh8EZEvYF5TYppxEKUTTJmbr8j7eRRnrIxzZtMvxLTXoujThVPMFS/cbnSnf2bFm1lq50TdDNABhmEi7z0ngQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"chalk": "^4.1.0",
"commander": "^7.2.0",
@ -8659,6 +8675,15 @@
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
"license": "ISC"
},
"node_modules/html-parse-stringify": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz",
"integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==",
"license": "MIT",
"dependencies": {
"void-elements": "3.1.0"
}
},
"node_modules/http-errors": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
@ -8712,6 +8737,30 @@
"integrity": "sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==",
"license": "BSD-3-Clause"
},
"node_modules/i18next": {
"version": "23.16.8",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-23.16.8.tgz",
"integrity": "sha512-06r/TitrM88Mg5FdUXAKL96dJMzgqLE5dv3ryBAra4KCwD9mJ4ndOTS95ZuymIGoE+2hzfdaMak2X11/es7ZWg==",
"funding": [
{
"type": "individual",
"url": "https://locize.com"
},
{
"type": "individual",
"url": "https://locize.com/i18next.html"
},
{
"type": "individual",
"url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.23.2"
}
},
"node_modules/ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
@ -12455,6 +12504,28 @@
"react": ">=17.0.0"
}
},
"node_modules/react-i18next": {
"version": "14.1.3",
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-14.1.3.tgz",
"integrity": "sha512-wZnpfunU6UIAiJ+bxwOiTmBOAaB14ha97MjOEnLGac2RJ+h/maIYXZuTHlmyqQVX1UVHmU1YDTQ5vxLmwfXTjw==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.23.9",
"html-parse-stringify": "^3.0.1"
},
"peerDependencies": {
"i18next": ">= 23.2.3",
"react": ">= 16.8.0"
},
"peerDependenciesMeta": {
"react-dom": {
"optional": true
},
"react-native": {
"optional": true
}
}
},
"node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
@ -13061,6 +13132,12 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/rtl-detect": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/rtl-detect/-/rtl-detect-1.1.2.tgz",
"integrity": "sha512-PGMBq03+TTG/p/cRB7HCLKJ1MgDIi07+QU1faSjiYRfmY5UsAttV9Hs08jDAHVwcOwmVLcSJkpwyfXszVjWfIQ==",
"license": "BSD-3-Clause"
},
"node_modules/run-parallel": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
@ -14914,6 +14991,15 @@
"integrity": "sha512-gQpnTgkubC6hQgdIcRdYGDSDc+SaujOdyesZQMv6JlfQee/9Mp0Qhnys6WxDWvQnL5WZdT7o2Ul187aSt0Rq+w==",
"license": "MIT"
},
"node_modules/void-elements": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
"integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/walker": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz",

View File

@ -20,14 +20,17 @@
"expo-image-picker": "~15.1.0",
"expo-linking": "~6.3.1",
"expo-local-authentication": "~14.0.1",
"expo-localization": "~15.0.3",
"expo-router": "~3.5.23",
"expo-screen-capture": "~6.0.1",
"expo-secure-store": "~13.0.2",
"expo-splash-screen": "~0.27.7",
"expo-status-bar": "~1.12.1",
"i18next": "^23.16.8",
"jail-monkey": "^2.8.0",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-i18next": "^14.1.3",
"react-native": "0.74.5",
"react-native-gesture-handler": "~2.16.1",
"react-native-safe-area-context": "4.10.5",

View File

@ -37,15 +37,20 @@ public record KintexPrincipal(
this(userId, displayName, eventRoles, hallManager, null, DEFAULT_TENANT);
}
/** 해당 행사에서 사용자의 역할(없으면 null). 홀매니저는 항상 HALL_MANAGER로 간주. */
/** 전역 시스템관리자(app_user.role_code='ADMIN') 여부 — 전 행사 열람+승인(홀매니저 상위). */
public boolean isAdmin() {
return "ADMIN".equalsIgnoreCase(roleCode);
}
/** 해당 행사에서 사용자의 역할(없으면 null). 홀매니저·ADMIN은 항상 HALL_MANAGER로 간주. */
public EventRole roleFor(String eventId) {
if (hallManager) {
if (hallManager || isAdmin()) {
return EventRole.HALL_MANAGER;
}
return eventRoles == null ? null : eventRoles.get(eventId);
}
public boolean hasAccess(String eventId) {
return hallManager || (eventRoles != null && eventRoles.containsKey(eventId));
return hallManager || isAdmin() || (eventRoles != null && eventRoles.containsKey(eventId));
}
}

View File

@ -0,0 +1,38 @@
package com.zioinfo.kintex.exhibitor;
import com.zioinfo.kintex.auth.EventAccessGuard;
import com.zioinfo.kintex.auth.EventRole;
import com.zioinfo.kintex.auth.KintexPrincipal;
import com.zioinfo.kintex.common.ApiResponse;
import com.zioinfo.kintex.exhibitor.dto.ExhibitorListDto;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 주최자용 참가업체 관리 API (#41 · SCR /exhibitors).
* 참가사 로스터(회사·부스·설계/정산 상태·연락) 행사 스코프로 반환한다.
* RBAC: 주최자·홀매니저만(참가업체 상호 연락처 비노출 참가사 프라이버시 보호).
*/
@RestController
@RequestMapping("/api/events/{eventId}/exhibitors")
public class ExhibitorController {
private final ExhibitorService service;
private final EventAccessGuard guard;
public ExhibitorController(ExhibitorService service, EventAccessGuard guard) {
this.service = service;
this.guard = guard;
}
/** GET — 행사별 참가업체 로스터 + 요약. */
@GetMapping
public ApiResponse<ExhibitorListDto> list(@AuthenticationPrincipal KintexPrincipal principal,
@PathVariable String eventId) {
guard.requireRole(principal, eventId, EventRole.ORGANIZER, EventRole.HALL_MANAGER);
return ApiResponse.ok(service.list(eventId));
}
}

View File

@ -0,0 +1,66 @@
package com.zioinfo.kintex.exhibitor;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List;
import java.util.Map;
/**
* 주최자용 참가업체 관리 조회 매퍼 read-only.
* booth {@code layout.event_id} 경유로 행사에 귀속된다. 마이그레이션 불변.
* 부스 배정(booth_sale)·최신 설계안(design_plan)·정산(invoice)·연락(company/event_member) 좌조인.
*/
@Mapper
public interface ExhibitorMapper {
/** 행사명(존재 확인용). 없으면 null. */
@Select("SELECT name FROM event WHERE id = #{eventId}")
String findEventName(@Param("eventId") String eventId);
/**
* 참가업체(= 배정 부스) 목록. company 배정된 부스만( 부스 제외).
* company/event_member LATERAL LIMIT 1 부스당 1행을 보장( 증식 방지).
* 최대 500건.
*/
@Select("""
SELECT b.id AS "boothId",
b.assigned_company_name AS "companyName",
b.booth_no AS "boothNo",
b.booth_type AS "boothType",
ROUND(ST_Area(b.geom)::numeric, 1) AS "areaM2",
bs.status AS "saleStatus",
COALESCE(dp.status, 'draft') AS "designStatus",
inv.status AS "invoiceStatus",
COALESCE(inv.total - inv.paid_amount, 0) AS "outstanding",
mem.display_name AS "contactName",
co.phone AS "contactPhone"
FROM booth b
JOIN layout l ON l.id = b.layout_id
LEFT JOIN booth_sale bs
ON bs.booth_id = b.id AND bs.event_id = l.event_id
LEFT JOIN LATERAL (
SELECT d.status FROM design_plan d
WHERE d.booth_id = b.id ORDER BY d.version DESC LIMIT 1
) dp ON true
LEFT JOIN invoice inv
ON inv.event_id = l.event_id AND inv.exhibitor_name = b.assigned_company_name
LEFT JOIN LATERAL (
SELECT au.display_name
FROM event_member em
JOIN app_user au ON au.id = em.user_id
WHERE em.event_id = l.event_id AND em.booth_id = b.id AND em.role_code = 'EXHIBITOR'
LIMIT 1
) mem ON true
LEFT JOIN LATERAL (
SELECT c.phone FROM company c
WHERE c.name = b.assigned_company_name LIMIT 1
) co ON true
WHERE l.event_id = #{eventId}
AND b.assigned_company_name IS NOT NULL
ORDER BY b.booth_no NULLS LAST
LIMIT 500
""")
List<Map<String, Object>> findExhibitors(@Param("eventId") String eventId);
}

View File

@ -0,0 +1,101 @@
package com.zioinfo.kintex.exhibitor;
import com.zioinfo.kintex.common.error.ApiException;
import com.zioinfo.kintex.common.error.ErrorCode;
import com.zioinfo.kintex.exhibitor.dto.ExhibitorListDto;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* 주최자용 참가업체 관리 서비스 테이블 집계로 목록·요약을 조립.
* 부스에 배정된 참가업체만 반환한다( 부스 제외). 데이터가 없으면 목록(정직).
*/
@Service
public class ExhibitorService {
private final ExhibitorMapper mapper;
public ExhibitorService(ExhibitorMapper mapper) {
this.mapper = mapper;
}
public ExhibitorListDto list(String eventId) {
String eventName = mapper.findEventName(eventId);
if (eventName == null) {
throw new ApiException(ErrorCode.NOT_FOUND, "행사를 찾을 수 없습니다.");
}
List<ExhibitorListDto.Row> rows = buildRows(mapper.findExhibitors(eventId));
return new ExhibitorListDto(eventId, eventName, buildSummary(rows), rows);
}
private List<ExhibitorListDto.Row> buildRows(List<Map<String, Object>> src) {
List<ExhibitorListDto.Row> out = new ArrayList<>();
if (src == null) {
return out;
}
for (Map<String, Object> r : src) {
out.add(new ExhibitorListDto.Row(
str(r.get("boothId")),
str(r.get("companyName")),
str(r.get("boothNo")),
str(r.get("boothType")),
dbl(r.get("areaM2")),
str(r.get("saleStatus")),
str(r.get("designStatus")),
str(r.get("invoiceStatus")),
lng(r.get("outstanding")),
str(r.get("contactName")),
str(r.get("contactPhone"))));
}
return out;
}
private ExhibitorListDto.Summary buildSummary(List<ExhibitorListDto.Row> rows) {
int contracted = 0;
int designApproved = 0;
int settlementPaid = 0;
int outstandingCount = 0;
for (ExhibitorListDto.Row r : rows) {
if ("sold".equals(r.saleStatus())) {
contracted++;
}
if ("approved".equals(r.designStatus())) {
designApproved++;
}
if ("paid".equals(r.invoiceStatus())) {
settlementPaid++;
}
if (r.outstanding() > 0) {
outstandingCount++;
}
}
return new ExhibitorListDto.Summary(rows.size(), contracted, designApproved, settlementPaid, outstandingCount);
}
private static Double dbl(Object o) {
if (o == null) return null;
if (o instanceof Number n) return n.doubleValue();
try {
return Double.parseDouble(String.valueOf(o));
} catch (NumberFormatException e) {
return null;
}
}
private static long lng(Object o) {
if (o == null) return 0L;
if (o instanceof Number n) return n.longValue();
try {
return Long.parseLong(String.valueOf(o));
} catch (NumberFormatException e) {
return 0L;
}
}
private static String str(Object o) {
return o == null ? null : String.valueOf(o);
}
}

View File

@ -0,0 +1,41 @@
package com.zioinfo.kintex.exhibitor.dto;
import java.util.List;
/**
* 주최자용 참가업체 관리 목록 (#41 · SCR /exhibitors).
* 집계원: booth(layout.event_id) + booth_sale + design_plan(최신) + invoice + company/event_member(연락).
* 데이터 없으면 0/빈배열/ null(정직 반환). 민감정보(비밀번호·otp) 포함하지 않는다.
*/
public record ExhibitorListDto(
String eventId,
String eventName,
Summary summary,
List<Row> exhibitors
) {
/** 상단 요약 밴드 — 참가사 규모/계약/설계승인/정산 현황. */
public record Summary(
int total,
int contracted,
int designApproved,
int settlementPaid,
int outstandingCount
) {
}
/** 참가업체 1행(= 배정 부스 1건). company·boothNo 는 표시용(민감정보 아님). */
public record Row(
String boothId,
String companyName,
String boothNo,
String boothType,
Double areaM2,
String saleStatus, // sold | held | available | null
String designStatus, // approved | review | draft | rejected
String invoiceStatus, // pending | invoiced | paid | overdue | cancelled | null
long outstanding, // 미수금() = total - paid_amount
String contactName, // 참가업체 담당자 표시명(선택)
String contactPhone // 연락처(선택)
) {
}
}

View File

@ -138,6 +138,91 @@ public interface HomeMapper {
@Param("tenantId") String tenantId,
@Param("broad") boolean broad);
/**
* 마일스톤 준비(M6): 미완료 표준 일정 노드 마감 {@code horizon}(D-7) 이내. 내가 참여한 행사(또는 broad).
* target "행사명 · 노드라벨"(민감정보 아님). route 서류·마일스톤 화면(SCR-22/23).
*/
@Select("""
SELECT 'milestone' AS "type",
'마일스톤 준비' AS "title",
(e.name || ' · ' || em.label) AS "target",
to_char(em.due_date, 'YYYY-MM-DD') AS "dueDate",
(em.due_date - CURRENT_DATE) AS "dday",
'/docs' AS "route"
FROM event_milestone em
JOIN event e ON e.id = em.event_id
WHERE e.tenant_id = #{tenantId}
AND em.state <> 'done'
AND em.due_date IS NOT NULL
AND em.due_date <= CAST(#{horizon} AS date)
AND (#{broad} OR EXISTS (
SELECT 1 FROM event_member m
WHERE m.event_id = e.id AND m.user_id = #{userId}))
ORDER BY em.due_date ASC
LIMIT 10
""")
List<Map<String, Object>> findMilestoneTodos(@Param("userId") String userId,
@Param("tenantId") String tenantId,
@Param("horizon") String horizon,
@Param("broad") boolean broad);
/**
* 체크인 준비(M10): 개장 D-3 이내(오늘~+3) 이면서 배지 미발급 관람객이 남은 행사. 내가 주최(또는 broad).
* target "행사명 · 대기 N명". dday 개장까지 잔여(0~3). route 관람객 관리 화면.
*/
@Select("""
SELECT 'checkin' AS "type",
'체크인 준비' AS "title",
(e.name || ' · 대기 ' || w.cnt || '명') AS "target",
to_char(e.start_date, 'YYYY-MM-DD') AS "dueDate",
(e.start_date - CURRENT_DATE) AS "dday",
'/visitors' AS "route"
FROM event e
JOIN LATERAL (
SELECT count(*) AS cnt FROM visitor_registration v
WHERE v.event_id = e.id AND v.badge_issued = false
) w ON true
WHERE e.tenant_id = #{tenantId}
AND e.start_date >= CURRENT_DATE
AND e.start_date <= CURRENT_DATE + 3
AND w.cnt > 0
AND (#{broad} OR EXISTS (
SELECT 1 FROM event_member m
WHERE m.event_id = e.id AND m.user_id = #{userId} AND m.role_code = 'ORGANIZER'))
ORDER BY e.start_date ASC
LIMIT 10
""")
List<Map<String, Object>> findCheckinPrepTodos(@Param("userId") String userId,
@Param("tenantId") String tenantId,
@Param("broad") boolean broad);
/**
* 핫리드 팔로업(M10): 고득점(score80) 리드로 팔로업이 필요한 . 내가 참가업체(EXHIBITOR) 행사(또는 broad).
* 종료 7일 지난 행사는 제외(경과 리드 정리). 기한 없음(dday=null 서비스가 warning 으로 부각).
* PII 금지: name/phone/email SELECT 목록에서 완전 제외 company/product/score(민감정보 아님) 노출.
*/
@Select("""
SELECT 'lead' AS "type",
'핫리드 팔로업' AS "title",
(COALESCE(NULLIF(l.company, ''), '리드') || COALESCE(' · ' || l.product, '')) AS "target",
NULL AS "dueDate",
NULL AS "dday",
'/leads' AS "route"
FROM lead l
JOIN event e ON e.id = l.event_id
WHERE e.tenant_id = #{tenantId}
AND l.score >= 80
AND e.end_date >= CURRENT_DATE - 7
AND (#{broad} OR EXISTS (
SELECT 1 FROM event_member m
WHERE m.event_id = e.id AND m.user_id = #{userId} AND m.role_code = 'EXHIBITOR'))
ORDER BY l.score DESC
LIMIT 10
""")
List<Map<String, Object>> findLeadFollowupTodos(@Param("userId") String userId,
@Param("tenantId") String tenantId,
@Param("broad") boolean broad);
// 쇼케이스(진행 ·다가오는 전시)
/**

View File

@ -43,29 +43,38 @@ public class HomeService {
boolean broad = isBroadOversight(principal);
String horizon = LocalDate.now().plusDays(DOC_HORIZON_DAYS).toString();
// 원천별 자기선별( 서브쿼리가 event_member 멤버십·role_code 스스로 역할 스코프됨 역할별 필터는
// 여기서 하드 게이트하지 않고 원천 스코프에 위임한다: CONTRACTOR 정산/서류 미노출, EXHIBITOR 옥션낙찰
// 미노출 . 다중 역할 사용자도 해당 행사 역할에 맞는 항목만 병합된다). broad(홀매니저·관리자) 테넌트.
List<Map<String, Object>> rows = new ArrayList<>();
rows.addAll(mapper.findApprovalTodos(userId));
rows.addAll(mapper.findDocumentTodos(userId, tenantId, horizon, broad));
rows.addAll(mapper.findMilestoneTodos(userId, tenantId, horizon, broad));
rows.addAll(mapper.findAuctionAwardTodos(userId, tenantId, broad));
rows.addAll(mapper.findAuctionBidTodos(userId, tenantId));
rows.addAll(mapper.findSettlementTodos(userId, tenantId, broad));
rows.addAll(mapper.findCheckinPrepTodos(userId, tenantId, broad));
rows.addAll(mapper.findLeadFollowupTodos(userId, tenantId, broad));
List<TodoItem> items = new ArrayList<>();
for (Map<String, Object> r : rows) {
String type = str(r.get("type"));
Integer dday = toInt(r.get("dday"));
items.add(new TodoItem(
str(r.get("type")),
type,
str(r.get("title")),
str(r.get("target")),
str(r.get("dueDate")),
dday,
severity(dday),
severityFor(type, dday),
str(r.get("route"))));
}
// 임박순: dday 오름차순(경과·임박 우선), 기한 없는 결재는 후순위.
items.sort(Comparator.comparing(
(TodoItem t) -> t.dday() == null ? Integer.MAX_VALUE : t.dday()));
// 마감 임박 우선순위(2단): 심각도 랭크(경과 임박 일반) 잔여일 오름차순(기한 없는 건은 그룹 후순위).
// 이로써 경과·D-3 이내 항목이 항상 상단으로 부각되고(프론트가 강조색 렌더), 핫리드·체크인 준비도 임박 그룹에서 노출된다.
items.sort(Comparator
.comparingInt((TodoItem t) -> severityRank(t.severity()))
.thenComparingInt(t -> t.dday() == null ? Integer.MAX_VALUE : t.dday()));
int total = items.size();
List<TodoItem> top = items.size() > TODO_LIMIT ? items.subList(0, TODO_LIMIT) : items;
@ -184,9 +193,13 @@ public class HomeService {
return new SummaryTile(key, label, value, null, dday, sev, route);
}
private static String severity(Integer dday) {
/**
* 심각도. 기한이 있으면 잔여일 기준(경과 overdue · D-3 이내 warning · normal).
* 기한 없는 핫리드 팔로업(lead) 즉시 조치가 필요한 액션이므로 warning 으로 부각한다( 무기한은 normal).
*/
private static String severityFor(String type, Integer dday) {
if (dday == null) {
return "normal";
return "lead".equals(type) ? "warning" : "normal";
}
if (dday < 0) {
return "overdue";
@ -194,6 +207,15 @@ public class HomeService {
return dday <= 3 ? "warning" : "normal";
}
/** 정렬용 심각도 랭크(작을수록 상단): 경과 0 · 임박 1 · 일반 2. */
private static int severityRank(String severity) {
return switch (severity) {
case "overdue" -> 0;
case "warning" -> 1;
default -> 2;
};
}
private static String normalizeTab(String tab) {
if (tab == null) {
return "ongoing";

View File

@ -25,6 +25,7 @@ public interface PublicSiteMapper {
to_char(e.end_date, 'YYYY-MM-DD') AS "endDate",
e.status,
h.label AS "hallLabel",
e.poster_url AS "posterUrl",
(SELECT count(*)::int
FROM booth b
JOIN layout l ON l.id = b.layout_id
@ -63,6 +64,7 @@ public interface PublicSiteMapper {
to_char(e.end_date, 'YYYY-MM-DD') AS "endDate",
e.status,
h.label AS "hallLabel",
e.poster_url AS "posterUrl",
(SELECT count(*)::int
FROM booth b
JOIN layout l ON l.id = b.layout_id

View File

@ -106,7 +106,8 @@ public class PublicSiteService {
private PublicEventDto toEvent(Map<String, Object> m) {
return new PublicEventDto(
str(m.get("id")), str(m.get("name")), str(m.get("startDate")), str(m.get("endDate")),
str(m.get("status")), str(m.get("hallLabel")), toInt(m.get("boothCount")));
str(m.get("status")), str(m.get("hallLabel")), toInt(m.get("boothCount")),
str(m.get("posterUrl")));
}
private String generateReceiptNo() {

View File

@ -12,6 +12,7 @@ public record PublicEventDto(
String endDate,
String status,
String hallLabel,
Integer boothCount
Integer boothCount,
String posterUrl
) {
}

View File

@ -0,0 +1,103 @@
package com.zioinfo.kintex.system.message;
import com.zioinfo.kintex.auth.EventAccessGuard;
import com.zioinfo.kintex.auth.KintexPrincipal;
import com.zioinfo.kintex.common.ApiResponse;
import com.zioinfo.kintex.common.audit.Audited;
import com.zioinfo.kintex.system.SystemAccessGuard;
import jakarta.validation.Valid;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
/**
* 메시지 관리 API. 관리(CRUD) ADMIN(시스템관리 게이트), 렌더는 인증 사용자 공개.
* - GET /api/admin/messages 목록(category/locale 필터) [ADMIN]
* - GET /api/admin/messages/{code} 코드별 로케일 목록 [ADMIN]
* - POST /api/admin/messages 생성/수정(upsert) [ADMIN]
* - PUT /api/admin/messages/{code} 수정(upsert) [ADMIN]
* - DELETE /api/admin/messages/{code} 삭제(locale 옵션) [ADMIN]
* - POST /api/messages/{code}/render {params:{...}} 치환 문자열 [인증]
*/
@RestController("sysMessageController")
public class MessageController {
private final MessageService service;
private final EventAccessGuard authGuard;
private final SystemAccessGuard adminGuard;
public MessageController(MessageService service, EventAccessGuard authGuard, SystemAccessGuard adminGuard) {
this.service = service;
this.authGuard = authGuard;
this.adminGuard = adminGuard;
}
@GetMapping("/api/admin/messages")
public ApiResponse<List<MessageDto>> list(@AuthenticationPrincipal KintexPrincipal principal,
@RequestParam(required = false) String category,
@RequestParam(required = false) String locale) {
adminGuard.requireAdmin(principal);
return ApiResponse.ok(service.list(principal.tenantId(), category, locale));
}
@GetMapping("/api/admin/messages/{code}")
public ApiResponse<List<MessageDto>> byCode(@AuthenticationPrincipal KintexPrincipal principal,
@PathVariable String code) {
adminGuard.requireAdmin(principal);
return ApiResponse.ok(service.byCode(principal.tenantId(), code));
}
@Audited(action = "SYS_MESSAGE_SAVE", targetType = "sys_message")
@PostMapping("/api/admin/messages")
public ApiResponse<Void> create(@AuthenticationPrincipal KintexPrincipal principal,
@Valid @RequestBody MessageDto dto) {
adminGuard.requireAdmin(principal);
service.save(principal.tenantId(), dto);
return ApiResponse.ok(null);
}
@Audited(action = "SYS_MESSAGE_SAVE", targetType = "sys_message")
@PutMapping("/api/admin/messages/{code}")
public ApiResponse<Void> update(@AuthenticationPrincipal KintexPrincipal principal,
@PathVariable String code, @Valid @RequestBody MessageDto dto) {
adminGuard.requireAdmin(principal);
MessageDto merged = new MessageDto(code, dto.locale(), dto.category(), dto.title(),
dto.template(), dto.useYn(), null);
service.save(principal.tenantId(), merged);
return ApiResponse.ok(null);
}
@Audited(action = "SYS_MESSAGE_DELETE", targetType = "sys_message")
@DeleteMapping("/api/admin/messages/{code}")
public ApiResponse<Void> delete(@AuthenticationPrincipal KintexPrincipal principal,
@PathVariable String code,
@RequestParam(required = false) String locale) {
adminGuard.requireAdmin(principal);
service.delete(principal.tenantId(), code, locale);
return ApiResponse.ok(null);
}
/** 렌더 — {params:{name:..,eventName:..}} 를 받아 template 치환 결과 반환. */
@PostMapping("/api/messages/{code}/render")
public ApiResponse<RenderResult> render(@AuthenticationPrincipal KintexPrincipal principal,
@PathVariable String code,
@RequestParam(required = false) String locale,
@RequestBody(required = false) RenderRequest body) {
authGuard.require(principal);
Map<String, Object> params = body == null || body.params() == null ? Map.of() : body.params();
String loc = (locale != null && !locale.isBlank()) ? locale
: (body != null ? body.locale() : null);
String text = service.render(principal.tenantId(), code, loc, params);
return ApiResponse.ok(new RenderResult(code, loc, text));
}
/** 렌더 요청 본문 — params 는 {key}→값 매핑. locale 은 쿼리 우선, 없으면 본문. */
public record RenderRequest(String locale, Map<String, Object> params) {
}
/** 렌더 결과. */
public record RenderResult(String code, String locale, String text) {
}
}

View File

@ -0,0 +1,17 @@
package com.zioinfo.kintex.system.message;
import jakarta.validation.constraints.NotBlank;
/**
* 메시지 템플릿(sys_message 이식) 알림/이메일/통지 문구를 코드·로케일별로 관리.
* template {name}·{eventName} 중괄호 플레이스홀더를 포함하며, 렌더 파라미터로 치환된다.
*/
public record MessageDto(
@NotBlank String msgCode,
String locale,
@NotBlank String category,
@NotBlank String title,
@NotBlank String template,
String useYn,
String updatedAt) {
}

View File

@ -0,0 +1,112 @@
package com.zioinfo.kintex.system.message;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 메시지 템플릿 조회/관리 + 렌더링.
* <p>렌더: template {key} params[key] 치환한다. 파라미터가 없으면 원본 {key} 유지하고(안전),
* 값이 null 이면 문자열로 치환한다. 알림/이메일(W10)·통지에서 {@link #render}/{@link #renderTemplate} 재사용.
*/
@Service("sysMessageService")
public class MessageService {
/** {key} — 영문/숫자/밑줄/점만 허용(중괄호 오탐 방지). */
private static final Pattern PLACEHOLDER = Pattern.compile("\\{([a-zA-Z0-9_.]+)}");
private static final String DEFAULT_TENANT = "KINTEX";
private static final String DEFAULT_LOCALE = "ko";
private final SysMessageMapper mapper;
public MessageService(SysMessageMapper mapper) {
this.mapper = mapper;
}
public List<MessageDto> list(String tenantId, String category, String locale) {
return mapper.findAll(tenant(tenantId), blankToNull(category), blankToNull(locale))
.stream().map(MessageService::toDto).toList();
}
public List<MessageDto> byCode(String tenantId, String msgCode) {
return mapper.findByCode(tenant(tenantId), msgCode).stream().map(MessageService::toDto).toList();
}
public void save(String tenantId, MessageDto dto) {
Map<String, Object> p = new HashMap<>();
p.put("tenantId", tenant(tenantId));
p.put("msgCode", dto.msgCode());
p.put("locale", blankToNull(dto.locale()));
p.put("category", dto.category());
p.put("title", dto.title());
p.put("template", dto.template());
p.put("useYn", dto.useYn());
mapper.upsert(p);
}
/** locale 이 null 이면 해당 코드의 전 로케일 삭제. */
public void delete(String tenantId, String msgCode, String locale) {
mapper.delete(tenant(tenantId), msgCode, blankToNull(locale));
}
/**
* 코드+로케일 템플릿을 조회해 파라미터로 치환한 최종 문자열을 반환.
* 요청 로케일이 없으면 ko 폴백, 그래도 없으면 문자열.
*/
public String render(String tenantId, String msgCode, String locale, Map<String, Object> params) {
String loc = (locale == null || locale.isBlank()) ? DEFAULT_LOCALE : locale;
Map<String, Object> row = mapper.findOne(tenant(tenantId), msgCode, loc);
if (row == null && !DEFAULT_LOCALE.equals(loc)) {
row = mapper.findOne(tenant(tenantId), msgCode, DEFAULT_LOCALE);
}
if (row == null) {
return "";
}
return renderTemplate(String.valueOf(row.get("template")), params);
}
/** template 의 {key} 를 params 로 치환(파라미터 없으면 원본 유지, null 이면 빈 문자열). */
public String renderTemplate(String template, Map<String, Object> params) {
if (template == null || template.isEmpty()) {
return template == null ? "" : template;
}
Map<String, Object> safe = params == null ? Map.of() : params;
Matcher m = PLACEHOLDER.matcher(template);
StringBuilder sb = new StringBuilder();
while (m.find()) {
String key = m.group(1);
String replacement;
if (safe.containsKey(key)) {
Object v = safe.get(key);
replacement = v == null ? "" : String.valueOf(v);
} else {
replacement = m.group(0); // 원본 {key} 유지
}
m.appendReplacement(sb, Matcher.quoteReplacement(replacement));
}
m.appendTail(sb);
return sb.toString();
}
private static String tenant(String tenantId) {
return (tenantId == null || tenantId.isBlank()) ? DEFAULT_TENANT : tenantId;
}
private static String blankToNull(String s) {
return (s == null || s.isBlank()) ? null : s;
}
private static MessageDto toDto(Map<String, Object> r) {
return new MessageDto(
str(r.get("msgCode")), str(r.get("locale")), str(r.get("category")),
str(r.get("title")), str(r.get("template")), str(r.get("useYn")), str(r.get("updatedAt")));
}
private static String str(Object o) {
return o == null ? null : String.valueOf(o);
}
}

View File

@ -0,0 +1,52 @@
package com.zioinfo.kintex.system.message;
import org.apache.ibatis.annotations.*;
import java.util.List;
import java.util.Map;
/**
* 메시지 템플릿 매퍼(sys_message). 필터는 null-safe(값이 없으면 조건 무시) &lt;script&gt; 미사용.
* <p>클래스명 Sys* 접두 기존 {@code work.message.MessageMapper} MyBatis 매퍼 빈명 충돌 회피.
*/
@Mapper
public interface SysMessageMapper {
@Select("SELECT msg_code AS \"msgCode\", locale, category, title, template, use_yn AS \"useYn\", "
+ "to_char(updated_at,'YYYY-MM-DD\"T\"HH24:MI:SS') AS \"updatedAt\" FROM sys_message "
+ "WHERE tenant_id = #{tenantId} "
+ "AND (#{category} IS NULL OR category = #{category}) "
+ "AND (#{locale} IS NULL OR locale = #{locale}) "
+ "ORDER BY category, msg_code, locale")
List<Map<String, Object>> findAll(@Param("tenantId") String tenantId,
@Param("category") String category,
@Param("locale") String locale);
/** 코드로 로케일 목록 조회(관리 상세). */
@Select("SELECT msg_code AS \"msgCode\", locale, category, title, template, use_yn AS \"useYn\", "
+ "to_char(updated_at,'YYYY-MM-DD\"T\"HH24:MI:SS') AS \"updatedAt\" FROM sys_message "
+ "WHERE tenant_id = #{tenantId} AND msg_code = #{msgCode} ORDER BY locale")
List<Map<String, Object>> findByCode(@Param("tenantId") String tenantId,
@Param("msgCode") String msgCode);
/** 렌더용 단건 조회 — 정확 로케일 우선, 없으면 ko 폴백은 서비스에서 처리. */
@Select("SELECT msg_code AS \"msgCode\", locale, category, title, template, use_yn AS \"useYn\" "
+ "FROM sys_message WHERE tenant_id = #{tenantId} AND msg_code = #{msgCode} "
+ "AND locale = #{locale} AND use_yn = 'Y'")
Map<String, Object> findOne(@Param("tenantId") String tenantId,
@Param("msgCode") String msgCode,
@Param("locale") String locale);
@Insert("INSERT INTO sys_message (tenant_id, msg_code, locale, category, title, template, use_yn) "
+ "VALUES (#{tenantId}, #{msgCode}, COALESCE(#{locale},'ko'), #{category}, #{title}, #{template}, "
+ "COALESCE(#{useYn},'Y')) "
+ "ON CONFLICT (tenant_id, msg_code, locale) DO UPDATE SET category=EXCLUDED.category, "
+ "title=EXCLUDED.title, template=EXCLUDED.template, use_yn=EXCLUDED.use_yn, updated_at=now()")
int upsert(Map<String, Object> p);
@Delete("DELETE FROM sys_message WHERE tenant_id = #{tenantId} AND msg_code = #{msgCode} "
+ "AND (#{locale} IS NULL OR locale = #{locale})")
int delete(@Param("tenantId") String tenantId,
@Param("msgCode") String msgCode,
@Param("locale") String locale);
}

View File

@ -0,0 +1,111 @@
-- V46__poster_local_assets.sql
-- 목적: event.poster_url 외부 핫링크(kintex.com imageView.do — 브라우저 안 뜸)를
-- 빌드 내장 정적자산(/media/posters/kx-<atchmnflNo>-<fileseq>.jpg)으로 교체.
-- 원천: kintex.com 공개 홍보 포스터(내부 데모용). 크롤·축소(가로≤480, jpg q78, ~16~59KB).
-- 자산: src/frontend/public/media/posters/ → Vite dist 루트 → GET /media/posters/<file>.
-- 멱등: URL 자연키 WHERE 조건 매칭. 재실행 시 이미 교체된 행은 매칭 안 됨(불변).
-- 정상/타 URL 및 미크롤 event 는 손대지 않음 → 그라디언트 폴백 유지.
-- 스코프: tenant_id='KINTEX'. V32·V39·V42 가 같은 URL 공유 시 일괄 갱신됨.
UPDATE event SET poster_url = '/media/posters/kx-468958-1.jpg'
WHERE tenant_id = 'KINTEX'
AND poster_url = 'https://www.kintex.com/imageView.do?atchmnflNo=468958&fileseq=1';
UPDATE event SET poster_url = '/media/posters/kx-468981-1.jpg'
WHERE tenant_id = 'KINTEX'
AND poster_url = 'https://www.kintex.com/imageView.do?atchmnflNo=468981&fileseq=1';
UPDATE event SET poster_url = '/media/posters/kx-469060-1.jpg'
WHERE tenant_id = 'KINTEX'
AND poster_url = 'https://www.kintex.com/imageView.do?atchmnflNo=469060&fileseq=1';
UPDATE event SET poster_url = '/media/posters/kx-469082-1.jpg'
WHERE tenant_id = 'KINTEX'
AND poster_url = 'https://www.kintex.com/imageView.do?atchmnflNo=469082&fileseq=1';
UPDATE event SET poster_url = '/media/posters/kx-469112-1.jpg'
WHERE tenant_id = 'KINTEX'
AND poster_url = 'https://www.kintex.com/imageView.do?atchmnflNo=469112&fileseq=1';
UPDATE event SET poster_url = '/media/posters/kx-469129-6.jpg'
WHERE tenant_id = 'KINTEX'
AND poster_url = 'https://www.kintex.com/imageView.do?atchmnflNo=469129&fileseq=6';
UPDATE event SET poster_url = '/media/posters/kx-469133-36.jpg'
WHERE tenant_id = 'KINTEX'
AND poster_url = 'https://www.kintex.com/imageView.do?atchmnflNo=469133&fileseq=36';
UPDATE event SET poster_url = '/media/posters/kx-469147-1.jpg'
WHERE tenant_id = 'KINTEX'
AND poster_url = 'https://www.kintex.com/imageView.do?atchmnflNo=469147&fileseq=1';
UPDATE event SET poster_url = '/media/posters/kx-469162-9.jpg'
WHERE tenant_id = 'KINTEX'
AND poster_url = 'https://www.kintex.com/imageView.do?atchmnflNo=469162&fileseq=9';
UPDATE event SET poster_url = '/media/posters/kx-469163-2.jpg'
WHERE tenant_id = 'KINTEX'
AND poster_url = 'https://www.kintex.com/imageView.do?atchmnflNo=469163&fileseq=2';
UPDATE event SET poster_url = '/media/posters/kx-469174-1.jpg'
WHERE tenant_id = 'KINTEX'
AND poster_url = 'https://www.kintex.com/imageView.do?atchmnflNo=469174&fileseq=1';
UPDATE event SET poster_url = '/media/posters/kx-469177-2.jpg'
WHERE tenant_id = 'KINTEX'
AND poster_url = 'https://www.kintex.com/imageView.do?atchmnflNo=469177&fileseq=2';
UPDATE event SET poster_url = '/media/posters/kx-469185-1.jpg'
WHERE tenant_id = 'KINTEX'
AND poster_url = 'https://www.kintex.com/imageView.do?atchmnflNo=469185&fileseq=1';
UPDATE event SET poster_url = '/media/posters/kx-469186-2.jpg'
WHERE tenant_id = 'KINTEX'
AND poster_url = 'https://www.kintex.com/imageView.do?atchmnflNo=469186&fileseq=2';
UPDATE event SET poster_url = '/media/posters/kx-469201-3.jpg'
WHERE tenant_id = 'KINTEX'
AND poster_url = 'https://www.kintex.com/imageView.do?atchmnflNo=469201&fileseq=3';
UPDATE event SET poster_url = '/media/posters/kx-469204-1.jpg'
WHERE tenant_id = 'KINTEX'
AND poster_url = 'https://www.kintex.com/imageView.do?atchmnflNo=469204&fileseq=1';
UPDATE event SET poster_url = '/media/posters/kx-469205-1.jpg'
WHERE tenant_id = 'KINTEX'
AND poster_url = 'https://www.kintex.com/imageView.do?atchmnflNo=469205&fileseq=1';
UPDATE event SET poster_url = '/media/posters/kx-469207-1.jpg'
WHERE tenant_id = 'KINTEX'
AND poster_url = 'https://www.kintex.com/imageView.do?atchmnflNo=469207&fileseq=1';
UPDATE event SET poster_url = '/media/posters/kx-469219-1.jpg'
WHERE tenant_id = 'KINTEX'
AND poster_url = 'https://www.kintex.com/imageView.do?atchmnflNo=469219&fileseq=1';
UPDATE event SET poster_url = '/media/posters/kx-469220-1.jpg'
WHERE tenant_id = 'KINTEX'
AND poster_url = 'https://www.kintex.com/imageView.do?atchmnflNo=469220&fileseq=1';
UPDATE event SET poster_url = '/media/posters/kx-469230-2.jpg'
WHERE tenant_id = 'KINTEX'
AND poster_url = 'https://www.kintex.com/imageView.do?atchmnflNo=469230&fileseq=2';
UPDATE event SET poster_url = '/media/posters/kx-469236-1.jpg'
WHERE tenant_id = 'KINTEX'
AND poster_url = 'https://www.kintex.com/imageView.do?atchmnflNo=469236&fileseq=1';
UPDATE event SET poster_url = '/media/posters/kx-469240-1.jpg'
WHERE tenant_id = 'KINTEX'
AND poster_url = 'https://www.kintex.com/imageView.do?atchmnflNo=469240&fileseq=1';
UPDATE event SET poster_url = '/media/posters/kx-469244-1.jpg'
WHERE tenant_id = 'KINTEX'
AND poster_url = 'https://www.kintex.com/imageView.do?atchmnflNo=469244&fileseq=1';
UPDATE event SET poster_url = '/media/posters/kx-469246-2.jpg'
WHERE tenant_id = 'KINTEX'
AND poster_url = 'https://www.kintex.com/imageView.do?atchmnflNo=469246&fileseq=2';
UPDATE event SET poster_url = '/media/posters/kx-469247-1.jpg'
WHERE tenant_id = 'KINTEX'
AND poster_url = 'https://www.kintex.com/imageView.do?atchmnflNo=469247&fileseq=1';
UPDATE event SET poster_url = '/media/posters/kx-469249-2.jpg'
WHERE tenant_id = 'KINTEX'
AND poster_url = 'https://www.kintex.com/imageView.do?atchmnflNo=469249&fileseq=2';
UPDATE event SET poster_url = '/media/posters/kx-469254-2.jpg'
WHERE tenant_id = 'KINTEX'
AND poster_url = 'https://www.kintex.com/imageView.do?atchmnflNo=469254&fileseq=2';
UPDATE event SET poster_url = '/media/posters/kx-469256-2.jpg'
WHERE tenant_id = 'KINTEX'
AND poster_url = 'https://www.kintex.com/imageView.do?atchmnflNo=469256&fileseq=2';
UPDATE event SET poster_url = '/media/posters/kx-469262-1.jpg'
WHERE tenant_id = 'KINTEX'
AND poster_url = 'https://www.kintex.com/imageView.do?atchmnflNo=469262&fileseq=1';
UPDATE event SET poster_url = '/media/posters/kx-469266-1.jpg'
WHERE tenant_id = 'KINTEX'
AND poster_url = 'https://www.kintex.com/imageView.do?atchmnflNo=469266&fileseq=1';
UPDATE event SET poster_url = '/media/posters/kx-469276-1.jpg'
WHERE tenant_id = 'KINTEX'
AND poster_url = 'https://www.kintex.com/imageView.do?atchmnflNo=469276&fileseq=1';
UPDATE event SET poster_url = '/media/posters/kx-469277-2.jpg'
WHERE tenant_id = 'KINTEX'
AND poster_url = 'https://www.kintex.com/imageView.do?atchmnflNo=469277&fileseq=2';
UPDATE event SET poster_url = '/media/posters/kx-469283-1.jpg'
WHERE tenant_id = 'KINTEX'
AND poster_url = 'https://www.kintex.com/imageView.do?atchmnflNo=469283&fileseq=1';

View File

@ -0,0 +1,215 @@
-- ============================================================================
-- V47 — 데모 로그인 해소 행사(e-2026-live) 폐루프 보강 (정산·옥션·관람객/리드·마케팅)
-- ============================================================================
-- 배경(근본 원인): 데모 계정(demo-organizer/exhibitor/contractor)은
-- ① e-2026-smf (V38, 8월 개막) ② e-2026-live (V39, 오늘 개막) 두 행사의 event_member.
-- 워크스페이스 정렬 = findEventRoles ORDER BY e.start_date → 오늘 개막인 e-2026-live 가 선두.
-- 프론트 useResolvedEventId = currentEventId ?? workspaces[0].eventId → 'e-2026-live'.
-- 문제: 정산(invoice/payment/tax/refund)·옥션(auction/bid/award)·관람객(visitor)·리드(lead)·
-- 마케팅(edm/sponsorship) 데모 데이터는 전부 e-2026-smf 또는 V42 bulk(bd-e-*)에만 존재 →
-- 로그인 후 해소되는 e-2026-live 스코프에서 정산·옥션·관람객 화면이 "빈 상태"로 보였다.
-- 조치: e-2026-live 를 축으로 각 도메인 폐루프를 멱등 보강(기존 시드 불변, ON CONFLICT DO NOTHING).
-- e-2026-live 참가업체(부스 assigned_company_name/booth_sale.sold_to_company):
-- (주)스마트오토메이션·델타로보틱스·글로벌테크·미래제조연구소·한국센서테크·바이오헬스랩
-- PII 는 전부 더미(홍길동·010-0000-*·*@kintex.test). tenant_id='KINTEX'.
-- ============================================================================
-- ============================================================================
-- 1) 정산 폐루프 — 청구서(상태 분포) + 수납 + 세금계산서 + 환불
-- ============================================================================
-- invoice UNIQUE(event_id, exhibitor_name) → 행사당 참가업체명 1건(멱등). 상태 분포:
-- paid×2 · invoiced(부분수납)×2 · overdue×1 · pending×2 · cancelled(환불대상)×1
INSERT INTO invoice (id, event_id, exhibitor_id, exhibitor_name, invoice_no, category, source_ref,
subtotal, vat, total, paid_amount, status, due_date, issued_at, paid_at,
cancelled_at, cancel_reason, line_items, note, created_by) VALUES
-- 완납
('inv-live-01','e-2026-live',NULL,'(주)스마트오토메이션','LIVE-2026-0001','rental',NULL,
6534000, 653400, 7187400, 7187400, 'paid', CURRENT_DATE - 2, now()-interval '18 day', now()-interval '5 day',
NULL, NULL,
'[{"category":"RENTAL","description":"부스 임대료 (18㎡ 아일랜드 · L-101)","quantity":18,"unitPrice":363000,"amount":6534000}]'::jsonb,
'완납 처리.', 'system-seed'),
('inv-live-03','e-2026-live',NULL,'글로벌테크','LIVE-2026-0003','rental',NULL,
4356000, 435600, 4791600, 4791600, 'paid', CURRENT_DATE - 1, now()-interval '10 day', now()-interval '3 day',
NULL, NULL,
'[{"category":"RENTAL","description":"부스 임대료 (18㎡ 조립부스 · L-103)","quantity":18,"unitPrice":242000,"amount":4356000}]'::jsonb,
'완납 처리.', 'system-seed'),
-- 부분수납(발행)
('inv-live-02','e-2026-live',NULL,'델타로보틱스','LIVE-2026-0002','rental',NULL,
5445000, 544500, 5989500, 2395800, 'invoiced', CURRENT_DATE + 7, now()-interval '14 day', NULL,
NULL, NULL,
'[{"category":"RENTAL","description":"부스 임대료 (18㎡ 독립부스 · L-102)","quantity":18,"unitPrice":302500,"amount":5445000}]'::jsonb,
'계약금 40% 수납.', 'system-seed'),
('inv-live-06','e-2026-live',NULL,'바이오헬스랩','LIVE-2026-0006','rental',NULL,
4356000, 435600, 4791600, 1916640, 'invoiced', CURRENT_DATE + 10, now()-interval '6 day', NULL,
NULL, NULL,
'[{"category":"RENTAL","description":"부스 임대료 (18㎡ 조립부스 · L-108)","quantity":18,"unitPrice":242000,"amount":4356000}]'::jsonb,
'계약금 40% 수납.', 'system-seed'),
-- 연체(overdue) — 대시보드 overdueCount 활성화
('inv-live-04','e-2026-live',NULL,'미래제조연구소','LIVE-2026-0004','rental',NULL,
4791600, 479160, 5270760, 0, 'overdue', CURRENT_DATE - 5, now()-interval '20 day', NULL,
NULL, NULL,
'[{"category":"RENTAL","description":"부스 임대료 (18㎡ 코너부스 · L-104)","quantity":18,"unitPrice":266200,"amount":4791600}]'::jsonb,
'납부 기한 경과 — 독촉 대상.', 'system-seed'),
-- 발행 대기(pending)
('inv-live-05','e-2026-live',NULL,'한국센서테크','LIVE-2026-0005','rental',NULL,
4356000, 435600, 4791600, 0, 'pending', CURRENT_DATE + 14, NULL, NULL,
NULL, NULL,
'[{"category":"RENTAL","description":"부스 임대료 (18㎡ 조립부스 · L-106)","quantity":18,"unitPrice":242000,"amount":4356000}]'::jsonb,
'발행 대기.', 'system-seed'),
-- 옥션 낙찰 수수료(F084) — AUC-LIVE-01 낙찰(aw-live-01, co-0001) 5% 수수료
('inv-live-07','e-2026-live',NULL,'(주)한빛부스디자인','LIVE-2026-0007','auction_fee','aw-live-01',
210000, 21000, 231000, 0, 'pending', CURRENT_DATE + 12, NULL, NULL,
NULL, NULL,
'[{"category":"AUCTION_FEE","description":"옥션 낙찰 수수료 5% (AUC-LIVE-01 · 낙찰 4,200,000원)","quantity":1,"unitPrice":210000,"amount":210000}]'::jsonb,
'옥션 낙찰 수수료 청구(발행 대기).', 'system-seed'),
-- 취소(환불 대상) — 계약금 납입 후 취소
('inv-live-08','e-2026-live',NULL,'넥스트이노베이션','LIVE-2026-0008','rental',NULL,
6000000, 600000, 6600000, 2640000, 'cancelled', CURRENT_DATE + 9, now()-interval '16 day', NULL,
now()-interval '2 day', '참가 철회(내부 사정) — 환불 규정 적용',
'[{"category":"RENTAL","description":"부스 임대료 (18㎡ · 추가배정)","quantity":18,"unitPrice":333333,"amount":6000000}]'::jsonb,
'취소 확정 — 환불 산정 대상.', 'system-seed')
ON CONFLICT (id) DO NOTHING;
-- 수납 기록(분납·완납·취소 전 계약금) --------------------------------------------
INSERT INTO invoice_payment (id, invoice_id, amount, method, memo, recorded_by, recorded_at) VALUES
('pay-live-01-1','inv-live-01', 2874960, 'transfer', '계약금 40%', 'system-seed', now()-interval '18 day'),
('pay-live-01-2','inv-live-01', 4312440, 'transfer', '잔금 60%', 'system-seed', now()-interval '5 day'),
('pay-live-03-1','inv-live-03', 4791600, 'transfer', '일시 완납', 'system-seed', now()-interval '3 day'),
('pay-live-02-1','inv-live-02', 2395800, 'transfer', '계약금 40%', 'system-seed', now()-interval '11 day'),
('pay-live-06-1','inv-live-06', 1916640, 'transfer', '계약금 40%', 'system-seed', now()-interval '5 day'),
('pay-live-08-1','inv-live-08', 2640000, 'transfer', '계약금 40%(취소 전 납입)', 'system-seed', now()-interval '13 day')
ON CONFLICT (id) DO NOTHING;
-- 세금계산서(F083) 2건 — 완납 청구(inv-live-01·inv-live-03) 기반 ----------------------
INSERT INTO tax_invoice (tenant_id, id, invoice_id, event_id, tax_invoice_no,
supplier_name, supplier_biz_no, supplier_ceo, supplier_addr,
buyer_name, supply_amount, vat, total, item_summary, status, issued_by) VALUES
('KINTEX','tax-live-0001','inv-live-01','e-2026-live','TAX-LIVE-0001',
'킨텍스(KINTEX)','128-81-00000','대표이사','경기도 고양시 일산서구 킨텍스로 217',
'(주)스마트오토메이션', 6534000, 653400, 7187400, '부스 임대료 (18㎡ · L-101)', 'issued', 'system-seed'),
('KINTEX','tax-live-0003','inv-live-03','e-2026-live','TAX-LIVE-0003',
'킨텍스(KINTEX)','128-81-00000','대표이사','경기도 고양시 일산서구 킨텍스로 217',
'글로벌테크', 4356000, 435600, 4791600, '부스 임대료 (18㎡ · L-103)', 'issued', 'system-seed')
ON CONFLICT (id) DO NOTHING;
-- 환불(F085) 1건 — 취소 청구 inv-live-08 기준. base=실납입(2,640,000), 개막 D-일 구간 환불율 -----
INSERT INTO invoice_refund (tenant_id, id, invoice_id, event_id, base_amount, refund_rate_percent,
refund_amount, forfeit_amount, days_before_event, bracket_label,
reason, ruleset_version, status, created_by) VALUES
('KINTEX','ref-live-0008','inv-live-08','e-2026-live', 2640000, 50.00,
1320000, 1320000, 0, '개막일 이후 취소(50%)',
'참가 철회 — 환불 규정(refund-v1.0) 구간 적용', 'refund-v1.0', 'recorded', 'system-seed')
ON CONFLICT (id) DO NOTHING;
-- ============================================================================
-- 2) 옥션 폐루프 — 옥션 2(낙찰 1·진행 1) + 초대 + 응찰 + 낙찰
-- ============================================================================
INSERT INTO auction (id, event_id, title, category, auction_type, award_criteria,
weight_price, weight_reputation, weight_delivery, round, deadline,
material_package_id, materials, accent, created_by) VALUES
('AUC-LIVE-01', 'e-2026-live', 'L-101 아일랜드 부스 장치공사', '전시디자인설치', 'reverse', 'comprehensive',
60, 25, 15, 1, now() - interval '2 day', NULL, 'layout,design,boq', 'primary', 'demo-organizer'),
('AUC-LIVE-02', 'e-2026-live', '메인 통로 전기·조명 설치', '전기/조명', 'reverse', 'comprehensive',
55, 25, 20, 1, now() + interval '3 day', NULL, 'layout,boq', 'success', 'demo-organizer')
ON CONFLICT (id) DO NOTHING;
INSERT INTO auction_invite (id, auction_id, company_id) VALUES
('ai-live-01-1', 'AUC-LIVE-01', 'co-0001'),
('ai-live-01-2', 'AUC-LIVE-01', 'co-0002'),
('ai-live-01-3', 'AUC-LIVE-01', 'co-0005'),
('ai-live-02-1', 'AUC-LIVE-02', 'co-0003'),
('ai-live-02-2', 'AUC-LIVE-02', 'co-0006'),
('ai-live-02-3', 'AUC-LIVE-02', 'co-0001')
ON CONFLICT (auction_id, company_id) DO NOTHING;
-- 응찰(견적) — (auction,company) 당 1행. AUC-LIVE-01 은 마감(낙찰), AUC-LIVE-02 는 진행 중.
INSERT INTO bid (id, auction_id, company_id, round, subtotal, vat, total, lead_days) VALUES
('bid-live-01-1', 'AUC-LIVE-01', 'co-0001', 1, 4200000, 420000, 4200000, 8),
('bid-live-01-2', 'AUC-LIVE-01', 'co-0002', 1, 4550000, 455000, 4550000, 7),
('bid-live-01-3', 'AUC-LIVE-01', 'co-0005', 1, 4380000, 438000, 4380000, 10),
('bid-live-02-1', 'AUC-LIVE-02', 'co-0003', 1, 3800000, 380000, 3800000, 6),
('bid-live-02-2', 'AUC-LIVE-02', 'co-0006', 1, 3650000, 365000, 3650000, 9)
ON CONFLICT (auction_id, company_id) DO NOTHING;
-- 낙찰(award) — AUC-LIVE-01 → co-0001 최저·종합 우수(수수료 청구 inv-live-07 source_ref 연동)
INSERT INTO award (id, auction_id, bid_id, reason, awarded_by, awarded_at) VALUES
('aw-live-01', 'AUC-LIVE-01', 'bid-live-01-1',
'종합평가 1위(가격 60·평판 25·납기 15) — 최저 응찰가 및 납기 준수 이력 우수.', 'demo-organizer', now()-interval '1 day')
ON CONFLICT (auction_id) DO NOTHING;
-- ============================================================================
-- 3) 관람객·리드 폐루프 — 사전등록(체크인 분포) + 부스 리드(핫리드 포함)
-- ============================================================================
-- checkin_at 은 CURRENT_DATE 기준 상대(now()) — 재배포 시점의 '오늘' 체크인으로 집계(홈 KPI).
INSERT INTO visitor_registration
(id, event_id, name, phone, email, company, visitor_type, agree_privacy, agree_marketing,
badge_code, badge_issued, checkin_state, qr_token, checkin_at, registered_at) VALUES
('vr-live-101','e-2026-live','홍길동','010-0000-3101','livevis101@kintex.test','(주)스마트오토메이션','buyer', true,true, 'KTX-L-0101',true, 'done', 'QR-LIVE0000000000000101', now()-interval '3 hour', now()-interval '2 day'),
('vr-live-102','e-2026-live','김철수','010-0000-3102','livevis102@kintex.test','델타로보틱스', 'visitor',true,true, 'KTX-L-0102',true, 'done', 'QR-LIVE0000000000000102', now()-interval '2 hour', now()-interval '2 day'),
('vr-live-103','e-2026-live','이영희','010-0000-3103','livevis103@kintex.test','한국센서테크', 'vip', true,false,'KTX-L-0103',true, 'done', 'QR-LIVE0000000000000103', now()-interval '90 minute', now()-interval '1 day'),
('vr-live-104','e-2026-live','박민수','010-0000-3104','livevis104@kintex.test',NULL, 'visitor',true,true, 'KTX-L-0104',true, 'done', 'QR-LIVE0000000000000104', now()-interval '40 minute', now()-interval '1 day'),
('vr-live-105','e-2026-live','최지우','010-0000-3105','livevis105@kintex.test','글로벌테크', 'buyer', true,true, 'KTX-L-0105',true, 'waiting','QR-LIVE0000000000000105', NULL, now()-interval '20 hour'),
('vr-live-106','e-2026-live','정하늘','010-0000-3106','livevis106@kintex.test','미래제조연구소', 'visitor',true,false,'KTX-L-0106',false,'waiting','QR-LIVE0000000000000106', NULL, now()-interval '6 hour'),
('vr-live-107','e-2026-live','강보람','010-0000-3107','livevis107@kintex.test','바이오헬스랩', 'buyer', true,true, 'KTX-L-0107',true, 'done', 'QR-LIVE0000000000000107', now()-interval '25 minute', now()-interval '5 hour')
ON CONFLICT (id) DO NOTHING;
-- 리드(SCR-32) — 부스 스코프 + demo-exhibitor 수집. 핫리드(score>=80) 2건 포함.
INSERT INTO lead
(id, event_id, booth_id, exhibitor_id, name, phone, email, role, company, product,
interest, score, ai_reasons, activity, followup_draft, ai_generated, collected_at) VALUES
('ld-live-001','e-2026-live','booth-live-02','demo-exhibitor','홍길동','010-0000-4001','lead001@kintex.test','생산기술 팀장','(주)스마트오토메이션','스마트 물류 AMR',5,92,
'["부스 체류 20분(평균 대비 320%)","사양서 다운로드 3회","구매 결정권자 직급"]'::jsonb,
'[{"text":"AMR 시연 참관","at":"오늘 13:40"},{"text":"견적 요청 폼 제출","at":"오늘 13:55"}]'::jsonb,
'안녕하세요, 홍 팀장님. 스마트 물류 AMR 커스텀 제안서를 준비했습니다. 다음 주 미팅 가능하실까요?', true, now()-interval '4 hour'),
('ld-live-002','e-2026-live','booth-live-01','demo-exhibitor','김철수','010-0000-4002','lead002@kintex.test','구매 담당','델타로보틱스','AI 비전 검사 모듈',5,85,
'["AI 비전 데모 2회 재방문","경쟁사 대비 견적 문의","도입 시기 명확(Q4)"]'::jsonb,
'[{"text":"AI 비전 모듈 상담","at":"오늘 11:20"}]'::jsonb,
'안녕하세요, 김 님. 요청하신 AI 비전 검사 모듈 견적서와 파일럿 프로그램 안내드립니다.', true, now()-interval '6 hour'),
('ld-live-003','e-2026-live','booth-live-03','demo-exhibitor','이영희','010-0000-4003','lead003@kintex.test','기술 기획','글로벌테크','협동로봇 HR-A1',3,64,
'["협동로봇 세션 참석","브로슈어 다운로드"]'::jsonb,
'[{"text":"데모 영상 시청","at":"오늘 10:10"}]'::jsonb,
'안녕하세요, 이 님. 협동로봇 HR-A1 도입 사례집을 보내드립니다.', true, now()-interval '8 hour'),
('ld-live-004','e-2026-live','booth-live-06','demo-exhibitor','박민수','010-0000-4004','lead004@kintex.test','인프라 엔지니어','한국센서테크','클라우드 모니터링',2,43,
'["짧은 부스 체류","자료 열람 없음"]'::jsonb,
'[{"text":"부스 QR 스캔","at":"오늘 09:50"}]'::jsonb,
'안녕하세요, 박 님. 클라우드 모니터링 소개 자료를 보내드립니다.', true, now()-interval '9 hour'),
('ld-live-005','e-2026-live','booth-live-02','demo-exhibitor','최지우','010-0000-4005','lead005@kintex.test','설비 담당','미래제조연구소','스마트팩토리 MES',4,71,
'["MES 상담 15분","도입 예산 문의"]'::jsonb,
'[{"text":"MES 상담","at":"오늘 12:05"}]'::jsonb,
'안녕하세요, 최 님. 스마트팩토리 MES 데모 환경 안내드립니다.', true, now()-interval '5 hour'),
('ld-live-006','e-2026-live','booth-live-01','demo-exhibitor','정하늘','010-0000-4006','lead006@kintex.test','R&D','바이오헬스랩','바이오 계측 센서',3,58,
'["센서 데모 관람","카탈로그 열람"]'::jsonb,
'[{"text":"센서 데모","at":"오늘 14:20"}]'::jsonb,
'안녕하세요, 정 님. 바이오 계측 센서 카탈로그와 견적 가이드를 보내드립니다.', true, now()-interval '2 hour')
ON CONFLICT (id) DO NOTHING;
-- ============================================================================
-- 4) 마케팅 폐루프 — EDM 캠페인 + 스폰서십 패키지/스폰서
-- ============================================================================
INSERT INTO edm_campaign (id, event_id, name, status, audience, meta, open_rate, click_rate) VALUES
('cp-live-001','e-2026-live','스마트 물류·자동화 코리아 개막 안내','done', 5400, '발송 완료: 1일 전', '48.2%', '12.6%'),
('cp-live-002','e-2026-live','VIP 바이어 상담회 초대','scheduled', 1200, '발송 예정: 오늘 18:00', '-', '-'),
('cp-live-003','e-2026-live','현장 등록 관람객 만족도 설문','draft', 3100, '최종 수정: 2시간 전', '-', '-')
ON CONFLICT (id) DO NOTHING;
INSERT INTO sponsorship_package (id, event_id, code, name, name_ko, price, accent, benefits, total_qty, sold_qty, dday, sort_order) VALUES
('sp-live-dia','e-2026-live','diamond','DIAMOND','다이아몬드', 30000000, 'primary',
'["메인 배너 노출","기조연설 세션","VIP 라운지 부스","공식 도록 전면 광고"]'::jsonb, 2, 1, 3, 0),
('sp-live-gld','e-2026-live','gold','GOLD','골드', 15000000, 'warning',
'["로비 배너","세션 슬롯 1","도록 반면 광고"]'::jsonb, 4, 2, 3, 1),
('sp-live-slv','e-2026-live','silver','SILVER','실버', 7000000, 'muted',
'["디지털 사이니지 노출","도록 로고 게재"]'::jsonb, 6, 3, 3, 2)
ON CONFLICT (id) DO NOTHING;
INSERT INTO sponsorship_sponsor (id, event_id, package_id, name, tier_label, contract_status, fulfillment) VALUES
('ss-live-001','e-2026-live','sp-live-dia','(주)스마트오토메이션','DIAMOND','signed',
'[{"item":"메인 배너","done":true},{"item":"기조연설","done":false}]'::jsonb),
('ss-live-002','e-2026-live','sp-live-gld','델타로보틱스','GOLD','signed',
'[{"item":"로비 배너","done":true}]'::jsonb),
('ss-live-003','e-2026-live','sp-live-slv','한국센서테크','SILVER','pending',
'[]'::jsonb)
ON CONFLICT (id) DO NOTHING;

View File

@ -0,0 +1,115 @@
-- V48 시스템관리 '메시지 관리' — 메시지 템플릿 리소스 + 샘플 전량 시드 (#35)
-- 목표: 알림/이메일/통지 문구를 코드·로케일별 템플릿({name}·{eventName} 등 중괄호 플레이스홀더)으로 중앙 관리.
-- 렌더 API가 template 의 {key} 를 파라미터로 치환해 최종 문자열을 만든다(코드 계층).
-- 불변: V1~V47 파일 불변. 본 마이그레이션은 순증(additive)·멱등(ON CONFLICT)만 수행한다.
-- 테넌트 표준(V31): tenant_id DEFAULT 'KINTEX', 복합 PK 선두 (tenant_id, msg_code, locale).
-- 보안 불변(§0-3): 템플릿은 문구만 저장 — 비밀번호·시크릿·PII 실값 미저장(치환 파라미터는 런타임 전달).
-- ============================================================================
-- 1) 테이블 — 메시지 템플릿
-- ============================================================================
CREATE TABLE IF NOT EXISTS sys_message (
tenant_id varchar(40) NOT NULL DEFAULT 'KINTEX',
msg_code varchar(60) NOT NULL, -- 메시지 코드(예: AUCTION_INVITE)
locale varchar(10) NOT NULL DEFAULT 'ko', -- ko/en/zh/ja
category varchar(40) NOT NULL, -- 분류(AUCTION/SETTLEMENT/ACCOUNT/VISITOR/MARKETING)
title varchar(300) NOT NULL, -- 제목/요약(플레이스홀더 포함 가능)
template text NOT NULL, -- 본문 템플릿({key} 플레이스홀더 포함)
use_yn char(1) NOT NULL DEFAULT 'Y',
updated_at timestamptz NOT NULL DEFAULT now(),
PRIMARY KEY (tenant_id, msg_code, locale)
);
CREATE INDEX IF NOT EXISTS idx_sys_message_cat ON sys_message (tenant_id, category, msg_code);
-- ============================================================================
-- 2) 샘플 전량 시드 — 주요 메시지 리소스 전 카테고리 × (ko/en/zh/ja)
-- ON CONFLICT DO UPDATE 로 멱등. 플레이스홀더는 중괄호 {key}.
-- ============================================================================
INSERT INTO sys_message (tenant_id, msg_code, locale, category, title, template) VALUES
-- AUCTION: 옥션(입찰) 통지
('KINTEX','AUCTION_INVITE','ko','AUCTION','[{eventName}] 장치공사 입찰 안내',
'{companyName} 담당자님, {eventName} 부스({boothNo}) 장치공사 입찰에 초대합니다. 마감일: {dueDate}. 킨텍스 AI 전시시스템에서 견적을 제출해 주세요.'),
('KINTEX','AUCTION_INVITE','en','AUCTION','[{eventName}] Booth Construction Bid Invitation',
'Dear {companyName}, you are invited to bid for booth ({boothNo}) construction of {eventName}. Deadline: {dueDate}. Please submit your quotation on the KINTEX AI system.'),
('KINTEX','AUCTION_INVITE','zh','AUCTION','[{eventName}] 展位施工招标邀请',
'{companyName} 负责人您好,诚邀您参与 {eventName} 展位({boothNo})施工招标。截止日期:{dueDate}。请在 KINTEX AI 系统提交报价。'),
('KINTEX','AUCTION_INVITE','ja','AUCTION','[{eventName}] ブース施工入札のご案内',
'{companyName} ご担当者様、{eventName} ブース({boothNo})施工の入札にご招待します。締切: {dueDate}。KINTEX AI システムで見積をご提出ください。'),
-- AUCTION: 낙찰
('KINTEX','AUCTION_AWARD','ko','AUCTION','[{eventName}] 낙찰 안내',
'{companyName} 담당자님, {eventName} 장치공사 입찰에 낙찰되셨습니다. 낙찰 금액: {amount}원. 세부 일정은 시스템에서 확인해 주세요.'),
('KINTEX','AUCTION_AWARD','en','AUCTION','[{eventName}] Award Notice',
'Dear {companyName}, you have won the construction bid for {eventName}. Award amount: {amount} KRW. Please check the schedule on the system.'),
('KINTEX','AUCTION_AWARD','zh','AUCTION','[{eventName}] 中标通知',
'{companyName} 负责人您好,恭喜您中标 {eventName} 施工项目。中标金额:{amount} 韩元。请在系统查看详细日程。'),
('KINTEX','AUCTION_AWARD','ja','AUCTION','[{eventName}] 落札のご案内',
'{companyName} ご担当者様、{eventName} 施工入札に落札されました。落札金額: {amount}ウォン。詳細日程はシステムでご確認ください。'),
-- SETTLEMENT: 정산 청구
('KINTEX','SETTLE_INVOICE','ko','SETTLEMENT','[정산] 청구서 발행 안내',
'{companyName} 담당자님, 청구서({invoiceNo})가 발행되었습니다. 청구 금액: {amount}원, 납부 기한: {dueDate}. 감사합니다.'),
('KINTEX','SETTLE_INVOICE','en','SETTLEMENT','[Settlement] Invoice Issued',
'Dear {companyName}, invoice ({invoiceNo}) has been issued. Amount: {amount} KRW, due by {dueDate}. Thank you.'),
('KINTEX','SETTLE_INVOICE','zh','SETTLEMENT','[结算] 账单已开具',
'{companyName} 负责人您好,账单({invoiceNo})已开具。金额:{amount} 韩元,付款期限:{dueDate}。谢谢。'),
('KINTEX','SETTLE_INVOICE','ja','SETTLEMENT','[精算] 請求書発行のご案内',
'{companyName} ご担当者様、請求書({invoiceNo})が発行されました。金額: {amount}ウォン、支払期限: {dueDate}。ありがとうございます。'),
-- SETTLEMENT: 마감임박
('KINTEX','SETTLE_DUE','ko','SETTLEMENT','[정산] 납부 마감 임박 안내',
'{companyName} 담당자님, 청구서({invoiceNo})의 납부 기한이 {dueDate}로 임박했습니다. 기한 내 납부 부탁드립니다.'),
('KINTEX','SETTLE_DUE','en','SETTLEMENT','[Settlement] Payment Due Soon',
'Dear {companyName}, payment for invoice ({invoiceNo}) is due on {dueDate}. Please complete payment on time.'),
('KINTEX','SETTLE_DUE','zh','SETTLEMENT','[结算] 付款截止临近',
'{companyName} 负责人您好,账单({invoiceNo})付款期限 {dueDate} 临近,请按时付款。'),
('KINTEX','SETTLE_DUE','ja','SETTLEMENT','[精算] 支払期限が近づいています',
'{companyName} ご担当者様、請求書({invoiceNo})の支払期限 {dueDate} が近づいています。期限内のお支払をお願いします。'),
-- ACCOUNT: 환영
('KINTEX','WELCOME','ko','ACCOUNT','환영합니다, {name}님',
'{name}님, KINTEX AI 전시·행사시스템에 오신 것을 환영합니다. {eventName} 준비를 함께 시작해 보세요.'),
('KINTEX','WELCOME','en','ACCOUNT','Welcome, {name}',
'Hi {name}, welcome to the KINTEX AI Exhibition System. Let us start preparing {eventName} together.'),
('KINTEX','WELCOME','zh','ACCOUNT','欢迎您,{name}',
'{name} 您好,欢迎使用 KINTEX AI 展览系统。让我们一起开始筹备 {eventName} 吧。'),
('KINTEX','WELCOME','ja','ACCOUNT','ようこそ、{name}様',
'{name}様、KINTEX AI 展示会システムへようこそ。{eventName} の準備を一緒に始めましょう。'),
-- ACCOUNT: 비밀번호 재설정
('KINTEX','PASSWORD_RESET','ko','ACCOUNT','[보안] 비밀번호 재설정 안내',
'{name}님, 비밀번호 재설정을 요청하셨습니다. 아래 링크는 {expireMin}분간 유효합니다: {resetLink}. 본인이 아니라면 무시하세요.'),
('KINTEX','PASSWORD_RESET','en','ACCOUNT','[Security] Reset Your Password',
'Hi {name}, you requested a password reset. The link is valid for {expireMin} minutes: {resetLink}. Ignore if this was not you.'),
('KINTEX','PASSWORD_RESET','zh','ACCOUNT','[安全] 重置密码',
'{name} 您好,您申请了重置密码。链接 {expireMin} 分钟内有效:{resetLink}。若非本人操作请忽略。'),
('KINTEX','PASSWORD_RESET','ja','ACCOUNT','[セキュリティ] パスワード再設定のご案内',
'{name}様、パスワード再設定のリクエストを受け付けました。リンクは {expireMin} 分間有効です: {resetLink}。心当たりがない場合は無視してください。'),
-- VISITOR: 체크인
('KINTEX','CHECKIN','ko','VISITOR','[{eventName}] 체크인 완료',
'{name}님, {eventName} 체크인이 완료되었습니다. 관심 부스({boothNo})를 둘러보세요. 즐거운 관람 되세요.'),
('KINTEX','CHECKIN','en','VISITOR','[{eventName}] Check-in Complete',
'Hi {name}, your check-in for {eventName} is complete. Explore booth ({boothNo}). Enjoy the show.'),
('KINTEX','CHECKIN','zh','VISITOR','[{eventName}] 签到完成',
'{name} 您好,您在 {eventName} 的签到已完成。请参观展位({boothNo})。祝您观展愉快。'),
('KINTEX','CHECKIN','ja','VISITOR','[{eventName}] チェックイン完了',
'{name}様、{eventName} のチェックインが完了しました。ブース({boothNo})をご覧ください。ごゆっくりお楽しみください。'),
-- VISITOR: 배지 발급
('KINTEX','BADGE_ISSUE','ko','VISITOR','[{eventName}] 배지 발급 안내',
'{name}님, 관람 배지({badgeNo})가 발급되었습니다. 입장 시 QR 코드를 제시해 주세요.'),
('KINTEX','BADGE_ISSUE','en','VISITOR','[{eventName}] Badge Issued',
'Hi {name}, your badge ({badgeNo}) for {eventName} has been issued. Present the QR code at entry.'),
('KINTEX','BADGE_ISSUE','zh','VISITOR','[{eventName}] 参观证已发放',
'{name} 您好,您在 {eventName} 的参观证({badgeNo})已发放。入场时请出示二维码。'),
('KINTEX','BADGE_ISSUE','ja','VISITOR','[{eventName}] バッジ発行のご案内',
'{name}様、{eventName} の入場バッジ({badgeNo})が発行されました。入場時に QR コードをご提示ください。'),
-- MARKETING: EDM(홍보 메일)
('KINTEX','EDM','ko','MARKETING','[{eventName}] 사전등록 안내',
'{name}님, {eventName}이(가) {eventDate}에 열립니다. 지금 사전등록하고 다양한 혜택을 받으세요.'),
('KINTEX','EDM','en','MARKETING','[{eventName}] Pre-registration Open',
'Hi {name}, {eventName} opens on {eventDate}. Pre-register now to enjoy exclusive benefits.'),
('KINTEX','EDM','zh','MARKETING','[{eventName}] 预登记开放',
'{name} 您好,{eventName} 将于 {eventDate} 举行。立即预登记,享受专属优惠。'),
('KINTEX','EDM','ja','MARKETING','[{eventName}] 事前登録のご案内',
'{name}様、{eventName} が {eventDate} に開催されます。今すぐ事前登録して特典をお受け取りください。')
ON CONFLICT (tenant_id, msg_code, locale) DO UPDATE
SET category = EXCLUDED.category,
title = EXCLUDED.title,
template = EXCLUDED.template,
use_yn = 'Y',
updated_at = now();

View File

@ -0,0 +1,220 @@
-- ============================================================================
-- V49 — 잔여 빈 테이블 전수 보강 (화면이 쓰나 비어 있던 12개 도메인 테이블)
-- ============================================================================
-- 배경: V42 대량시드(40테이블)·V47(e-2026-live 폐루프) 이후에도, CREATE 는 되었으나
-- INSERT 가 전혀 없던(=화면에서 "빈 상태") 테이블이 다수 남아 있었다(매퍼 감사).
-- 대상(전부 매퍼/서비스가 조회):
-- render_job · cms_media · logistics_equipment_request · logistics_rental_order ·
-- logistics_inbound · mail_log · edm_followup_log(+draft campaign) ·
-- webhook_subscription · webhook_delivery · login_history · sys_role_menu · audit_log
-- 스코프: 이벤트 종속 도메인은 두 데모 행사(e-2026-live=오늘 · e-2026-smf=8월) 모두 채워
-- 행사 전환 시에도 물류/렌탈/입고/렌더/팔로업이 보이게 한다(고아 레코드 금지 — 기존 booth/lead/company/menu 참조).
-- 원칙: 멱등(ON CONFLICT DO NOTHING) · tenant_id='KINTEX' · PII 더미(홍길동·010-0000-*·*@kintex.test) ·
-- 기존 시드 불변. 서버 최신 V47, 진행 중 V48(메시지관리)과 충돌 회피 → V49 확정.
--
-- 의도적으로 비워 두는 테이블(보안·운영성):
-- error_log(운영 500 요약만 기록) · password_reset(1회용 재설정 토큰) ·
-- webhook_inbound_endpoint / webhook_inbound_event(secret_enc AES-256-GCM 봉인은
-- 앱 부팅 시더 전용 — SQL 평문 저장 금지, V35 주석 준수).
-- ============================================================================
-- ============================================================================
-- 1) render_job — 부스별 나노바나나 렌더 결과(상태 분포: DONE·RUNNING·QUEUED·FAILED)
-- ============================================================================
-- e-2026-live 부스(booth-live-01..06) + e-2026-smf 부스(booth-smf-a102/b101..b106).
INSERT INTO render_job (id, event_id, booth_id, shot_preset, status, image_url, schema_hash, model_version, error_message, created_at, updated_at) VALUES
-- e-2026-live
('rj-live-01','e-2026-live','booth-live-01','S1','DONE','/uploads/render/rj-live-01.png','sha1-live01a','gemini-3.1-flash-image-preview',NULL, now()-interval '5 hour', now()-interval '5 hour'),
('rj-live-02','e-2026-live','booth-live-01','S3','DONE','/uploads/render/rj-live-02.png','sha1-live01b','gemini-3.1-flash-image-preview',NULL, now()-interval '5 hour', now()-interval '4 hour'),
('rj-live-03','e-2026-live','booth-live-02','S1','DONE','/uploads/render/rj-live-03.png','sha1-live02a','gemini-3.1-flash-image-preview',NULL, now()-interval '4 hour', now()-interval '3 hour'),
('rj-live-04','e-2026-live','booth-live-03','S2','RUNNING',NULL,'sha1-live03a','gemini-3.1-flash-image-preview',NULL, now()-interval '20 minute', now()-interval '2 minute'),
('rj-live-05','e-2026-live','booth-live-04','S1','QUEUED',NULL,'sha1-live04a',NULL,NULL, now()-interval '8 minute', now()-interval '8 minute'),
('rj-live-06','e-2026-live','booth-live-05','S4','FAILED',NULL,'sha1-live05a','gemini-3.1-flash-image-preview','생성 시간 초과 — 재시도 대기(요약).', now()-interval '2 hour', now()-interval '2 hour'),
('rj-live-07','e-2026-live','booth-live-06','S1','DONE','/uploads/render/rj-live-07.png','sha1-live06a','gemini-3.1-flash-image-preview',NULL, now()-interval '6 hour', now()-interval '6 hour'),
-- e-2026-smf
('rj-smf-01','e-2026-smf','booth-smf-a102','S1','DONE','/uploads/render/rj-smf-01.png','sha1-smfa102','gemini-3.1-flash-image-preview',NULL, TIMESTAMPTZ '2026-07-20 10:00+09', TIMESTAMPTZ '2026-07-20 10:03+09'),
('rj-smf-02','e-2026-smf','booth-smf-b101','S2','DONE','/uploads/render/rj-smf-02.png','sha1-smfb101','gemini-3.1-flash-image-preview',NULL, TIMESTAMPTZ '2026-07-21 11:00+09', TIMESTAMPTZ '2026-07-21 11:04+09'),
('rj-smf-03','e-2026-smf','booth-smf-b102','S1','DONE','/uploads/render/rj-smf-03.png','sha1-smfb102','gemini-3.1-flash-image-preview',NULL, TIMESTAMPTZ '2026-07-22 09:30+09', TIMESTAMPTZ '2026-07-22 09:33+09'),
('rj-smf-04','e-2026-smf','booth-smf-b103','S3','QUEUED',NULL,'sha1-smfb103',NULL,NULL, TIMESTAMPTZ '2026-07-23 14:00+09', TIMESTAMPTZ '2026-07-23 14:00+09'),
('rj-smf-05','e-2026-smf','booth-smf-b104','S5','FAILED',NULL,'sha1-smfb104','gemini-3.1-flash-image-preview','입력 평면도 해상도 부족 — 재업로드 요망(요약).', TIMESTAMPTZ '2026-07-23 15:20+09', TIMESTAMPTZ '2026-07-23 15:21+09')
ON CONFLICT (id) DO NOTHING;
-- ============================================================================
-- 2) cms_media — 미디어 라이브러리(전역 + 행사별)
-- ============================================================================
INSERT INTO cms_media (id, event_id, file_name, url, content_type, size_bytes, alt_text, uploader_name, created_at) VALUES
('med-global-01', NULL, 'kintex-logo.svg', '/uploads/cms/kintex-logo.svg', 'image/svg+xml', 18240, 'KINTEX AI 전시·행사시스템 로고', '관리자', now()-interval '30 day'),
('med-global-02', NULL, 'hero-hall.jpg', '/uploads/cms/hero-hall.jpg', 'image/jpeg', 482113, '킨텍스 전시홀 메인 히어로', '관리자', now()-interval '25 day'),
('med-global-03', NULL, 'brochure-cover.png', '/uploads/cms/brochure-cover.png', 'image/png', 231544, '공식 도록 표지', '관리자', now()-interval '20 day'),
('med-live-01','e-2026-live', 'smart-auto-poster.jpg', '/uploads/cms/smart-auto-poster.jpg','image/jpeg', 356820, '스마트 물류·자동화 코리아 포스터', '홍길동', now()-interval '10 day'),
('med-live-02','e-2026-live', 'floorplan-live.png', '/uploads/cms/floorplan-live.png', 'image/png', 512900, 'e-2026-live 홀 배치도', '홍길동', now()-interval '7 day'),
('med-live-03','e-2026-live', 'sponsor-banner.jpg', '/uploads/cms/sponsor-banner.jpg', 'image/jpeg', 198340, '스폰서 배너(다이아몬드)', '홍길동', now()-interval '3 day'),
('med-smf-01','e-2026-smf', 'smf-keyvisual.jpg', '/uploads/cms/smf-keyvisual.jpg', 'image/jpeg', 421077, '스마트 팩토리 엑스포 키비주얼', '관리자', TIMESTAMPTZ '2026-07-15 10:00+09'),
('med-smf-02','e-2026-smf', 'smf-map.png', '/uploads/cms/smf-map.png', 'image/png', 488213, 'e-2026-smf 전시장 안내도', '관리자', TIMESTAMPTZ '2026-07-16 10:00+09'),
('med-smf-03','e-2026-smf', 'smf-speaker.jpg', '/uploads/cms/smf-speaker.jpg', 'image/jpeg', 167902, '기조연설 연사 프로필(더미)', '관리자', TIMESTAMPTZ '2026-07-17 10:00+09')
ON CONFLICT (id) DO NOTHING;
-- ============================================================================
-- 3) logistics_equipment_request — 부대장비 신청(마스터 스냅샷, 상태 분포)
-- ============================================================================
INSERT INTO logistics_equipment_request
(id, event_id, equipment_id, equipment_name, unit_price, booth_no, quantity, use_date, days, amount, status, note, reject_reason, requested_by, decided_by, decided_at) VALUES
('ler-live-01','e-2026-live','leq-fork-25','지게차 2.5t',180000,'L-101',1, CURRENT_DATE, 1, 180000,'approved','반입일 오전 상차','', 'demo-exhibitor','demo-organizer', now()-interval '1 day'),
('ler-live-02','e-2026-live','leq-manp-01','상하차 인력', 90000,'L-102',2, CURRENT_DATE, 1, 180000,'requested','대형 장비 상하차 인력 2인','', 'demo-exhibitor',NULL,NULL),
('ler-live-03','e-2026-live','leq-lift-01','고소작업 리프트',150000,'L-103',1, CURRENT_DATE+1, 1, 150000,'done','트러스 설치 완료','', 'demo-exhibitor','demo-organizer', now()-interval '2 day'),
('ler-smf-01','e-2026-smf','leq-fork-05','지게차 5t',260000,'A-102',1, DATE '2026-08-04', 1, 260000,'approved','반입일 대형 화물 상차','', 'demo-exhibitor','demo-organizer', TIMESTAMPTZ '2026-07-28 10:00+09'),
('ler-smf-02','e-2026-smf','leq-lift-01','고소작업 리프트',150000,'B-101',1, DATE '2026-08-04', 2, 300000,'requested','2일간 사이니지 설치','', 'demo-exhibitor',NULL,NULL),
('ler-smf-03','e-2026-smf','leq-crane-5','이동식 크레인 5t',420000,'B-104',1, DATE '2026-08-05', 1, 420000,'rejected','','중량물 크레인은 안전관리계획서 선제출 필요', 'demo-exhibitor','demo-organizer', TIMESTAMPTZ '2026-07-29 14:00+09')
ON CONFLICT (id) DO NOTHING;
-- ============================================================================
-- 4) logistics_rental_order — 렌탈 품목 주문(마스터 스냅샷, 상태 분포)
-- ============================================================================
INSERT INTO logistics_rental_order
(id, event_id, item_id, item_name, daily_price, booth_no, quantity, start_date, end_date, days, amount, status, note, ordered_by, decided_by, decided_at) VALUES
('lro-live-01','e-2026-live','lri-tv-55','55인치 모니터',60000,'L-101',2, CURRENT_DATE, CURRENT_DATE+2, 3, 360000,'confirmed','부스 시연용 2대','demo-exhibitor','demo-organizer', now()-interval '1 day'),
('lro-live-02','e-2026-live','lri-chair-01','상담용 의자',3000,'L-101',10, CURRENT_DATE, CURRENT_DATE+2, 3, 90000,'ordered','상담 부스 의자','demo-exhibitor',NULL,NULL),
('lro-live-03','e-2026-live','lri-info-01','인포메이션 데스크',25000,'L-102',1, CURRENT_DATE, CURRENT_DATE+2, 3, 75000,'delivered','안내 데스크 설치 완료','demo-exhibitor','demo-organizer', now()-interval '6 hour'),
('lro-smf-01','e-2026-smf','lri-table-01','접이식 테이블',8000,'A-102',5, DATE '2026-08-04', DATE '2026-08-07', 4, 160000,'confirmed','상담존 테이블','demo-exhibitor','demo-organizer', TIMESTAMPTZ '2026-07-28 11:00+09'),
('lro-smf-02','e-2026-smf','lri-spot-01','LED 스포트 조명',5000,'B-101',12, DATE '2026-08-04', DATE '2026-08-07', 4, 240000,'ordered','제품 진열 조명','demo-exhibitor',NULL,NULL),
('lro-smf-03','e-2026-smf','lri-shelf-01','진열 선반',12000,'B-103',3, DATE '2026-08-04', DATE '2026-08-07', 4, 144000,'cancelled','발주 취소(부스 설계 변경)','demo-exhibitor','demo-organizer', TIMESTAMPTZ '2026-07-30 09:00+09')
ON CONFLICT (id) DO NOTHING;
-- ============================================================================
-- 5) logistics_inbound — 반입 화물 추적(운송장, 상태 분포)
-- ============================================================================
INSERT INTO logistics_inbound
(id, event_id, tracking_no, carrier, booth_no, item_desc, quantity, status, eta_date, warehouse_at, delivered_at, note, registered_by, updated_by) VALUES
('lib-live-01','e-2026-live','CJ-1000-2201','CJ대한통운','L-101','AMR 시연 장비 3종',3,'delivered_booth', CURRENT_DATE, now()-interval '8 hour', now()-interval '3 hour','부스 전달 완료','demo-exhibitor','demo-organizer'),
('lib-live-02','e-2026-live','LO-2200-3310','로젠택배','L-102','비전 검사 모듈 팔레트',1,'arrived_warehouse', CURRENT_DATE, now()-interval '2 hour', NULL,'창고 대기 — 부스 전달 예정','demo-exhibitor','demo-organizer'),
('lib-live-03','e-2026-live','HJ-3300-4420','한진택배','L-103','협동로봇 본체',2,'inbound_scheduled', CURRENT_DATE+1, NULL, NULL,'익일 입고 예정','demo-exhibitor',NULL),
('lib-smf-01','e-2026-smf','CJ-5000-1102','CJ대한통운','A-102','스마트팩토리 데모 셀',1,'registered', DATE '2026-08-03', NULL, NULL,'반입 신청 등록','demo-exhibitor',NULL),
('lib-smf-02','e-2026-smf','LO-5100-2203','로젠택배','B-101','전자 사이니지 패널 6매',6,'inbound_scheduled', DATE '2026-08-03', NULL, NULL,'개막 전일 입고','demo-exhibitor','demo-organizer')
ON CONFLICT (id) DO NOTHING;
-- ============================================================================
-- 6) mail_log — 발송 이력(제목/마스킹 수신자만, 본문·원문 미저장)
-- ============================================================================
INSERT INTO mail_log (tenant_id, id, category, ref_id, subject, recipient_masked, status, detail, created_at) VALUES
('KINTEX','ml-0001','AUCTION_OPEN','AUC-LIVE-01','[킨텍스] L-101 아일랜드 부스 장치공사 옥션 초대','han***@kintex.test', 'sent','초대 공사업체 3사 발송', now()-interval '3 day'),
('KINTEX','ml-0002','AUCTION_OPEN','AUC-LIVE-02','[킨텍스] 메인 통로 전기·조명 설치 옥션 초대','del***@kintex.test', 'sent','초대 공사업체 3사 발송', now()-interval '2 day'),
('KINTEX','ml-0003','EDM','cp-live-001','[스마트 물류·자동화 코리아] 개막 안내','vis***@kintex.test', 'sent','사전등록자 5,400명 대상 발송', now()-interval '1 day'),
('KINTEX','ml-0004','EDM','cp-live-002','[VIP 초대] 바이어 상담회 안내','buy***@kintex.test', 'sent','VIP 1,200명 대상 발송 예약', now()-interval '6 hour'),
('KINTEX','ml-0005','PASSWORD_RESET',NULL,'[킨텍스] 비밀번호 재설정 안내','dem***@kintex.test','sent','재설정 링크 발송(유효 30분)', now()-interval '5 hour'),
('KINTEX','ml-0006','EDM','cp-smf-001','[스마트 팩토리 엑스포] 사전등록 오픈 안내','pre***@kintex.test','sent','사전등록 안내 발송', TIMESTAMPTZ '2026-07-18 10:00+09'),
('KINTEX','ml-0007','AUCTION_OPEN','AUC-2026-0511','[킨텍스] D-1 옥외 사이니지 설치 옥션 초대','sec***@kintex.test','failed','수신 도메인 일시 오류 — 재발송 대상', TIMESTAMPTZ '2026-07-25 14:00+09'),
('KINTEX','ml-0008','EDM','cp-smf-003','[스마트 팩토리 엑스포] 세미나 프로그램 안내','edu***@kintex.test','skipped','수신거부 목록 필터링', TIMESTAMPTZ '2026-07-27 10:00+09')
ON CONFLICT (tenant_id, id) DO NOTHING;
-- ============================================================================
-- 7) edm_followup_log(+ draft campaign) — 핫리드(score>=80) 팔로업 자동 초안 1회
-- ============================================================================
-- 리드당 초안 캠페인(draft) 1건 생성 후 followup_log 로 링크(멱등 키: UNIQUE(event_id, lead_id)).
INSERT INTO edm_campaign (id, event_id, name, status, audience, meta, open_rate, click_rate) VALUES
('cp-fu-live-001','e-2026-live','[팔로업] 스마트 물류 AMR 커스텀 제안','draft', 1, 'AI 자동 초안 · ld-live-001', '-','-'),
('cp-fu-live-002','e-2026-live','[팔로업] AI 비전 검사 모듈 파일럿 안내','draft', 1, 'AI 자동 초안 · ld-live-002', '-','-'),
('cp-fu-smf-001','e-2026-smf','[팔로업] 협동로봇 HR-A1 도입 제안','draft', 1, 'AI 자동 초안 · ld-smf-006', '-','-'),
('cp-fu-smf-002','e-2026-smf','[팔로업] 협동로봇 HR-A1 견적 안내','draft', 1, 'AI 자동 초안 · ld-smf-001', '-','-'),
('cp-fu-smf-003','e-2026-smf','[팔로업] AI 비전 검사 모듈 사례집','draft', 1, 'AI 자동 초안 · ld-smf-003', '-','-')
ON CONFLICT (id) DO NOTHING;
INSERT INTO edm_followup_log (tenant_id, id, event_id, lead_id, campaign_id, hot_score, body_draft, created_at) VALUES
('KINTEX','fu-live-001','e-2026-live','ld-live-001','cp-fu-live-001',92,
'안녕하세요, 홍 팀장님. 스마트 물류 AMR 커스텀 제안서를 준비했습니다. 다음 주 미팅 가능하실까요?', now()-interval '3 hour'),
('KINTEX','fu-live-002','e-2026-live','ld-live-002','cp-fu-live-002',85,
'안녕하세요, 김 님. 요청하신 AI 비전 검사 모듈 견적서와 파일럿 프로그램 안내드립니다.', now()-interval '5 hour'),
('KINTEX','fu-smf-006','e-2026-smf','ld-smf-006','cp-fu-smf-001',91,
'안녕하세요, 문 팀장님. 협동로봇 HR-A1 도입 제안서를 보내드립니다.', TIMESTAMPTZ '2026-08-01 15:00+09'),
('KINTEX','fu-smf-001','e-2026-smf','ld-smf-001','cp-fu-smf-002',87,
'안녕하세요, 김 담당자님. 협동로봇 HR-A1 견적서를 준비했습니다.', TIMESTAMPTZ '2026-08-01 15:10+09'),
('KINTEX','fu-smf-003','e-2026-smf','ld-smf-003','cp-fu-smf-003',82,
'안녕하세요, 박 소장님. AI 비전 검사 모듈 도입 사례집을 보내드립니다.', TIMESTAMPTZ '2026-08-01 15:20+09')
-- 멱등 키는 업무 유니크 (event_id, lead_id) — V47/V42 가 동일 리드에 이미 초안을 시드했으면(다른 id) 건너뛴다.
-- (id PK 는 신규이므로 실충돌은 항상 (event_id, lead_id) 쪽에서 발생.)
ON CONFLICT (event_id, lead_id) DO NOTHING;
-- ============================================================================
-- 8) webhook_subscription — 아웃바운드 구독(발송 disabled 스텁)
-- ============================================================================
-- secret_hash = SHA-256(secret) hex 자리(원문 미저장·발송 disabled 이므로 검증 불요). 데모 고정 해시.
INSERT INTO webhook_subscription (tenant_id, id, event_type, callback_url, secret_hash, description, active, created_by) VALUES
('KINTEX','whs-001','lead.hot', 'https://hooks.example.test/kintex/lead-hot', '3a7bd3e2360a3d29eea436fcfb7e44c735d117c42d1c1835420b6b9942dd4f1b','핫리드 발생 시 CRM 연동(스텁)', true, 'system-seed'),
('KINTEX','whs-002','invoice.paid', 'https://hooks.example.test/kintex/invoice-paid','5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8','청구 완납 시 회계 연동(스텁)', true, 'system-seed'),
('KINTEX','whs-003','auction.awarded','https://hooks.example.test/kintex/award', '6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b','옥션 낙찰 시 계약 연동(스텁)', true, 'system-seed'),
('KINTEX','whs-004','visitor.checkin','https://hooks.example.test/kintex/checkin', 'd4735e3a265e16eee03f59718b9b5d03019c07d8b6c51f90da3a666eec13ab35','현장 체크인 실시간 연동(스텁)', false, 'system-seed')
ON CONFLICT (id) DO NOTHING;
-- ============================================================================
-- 9) webhook_delivery — 아웃바운드 발송 큐(status=disabled 스텁, 실 HTTP 콜 없음)
-- ============================================================================
INSERT INTO webhook_delivery (tenant_id, id, subscription_id, event_type, payload_json, status, note, created_at) VALUES
('KINTEX','whd-001','whs-001','lead.hot',
'{"eventId":"e-2026-live","leadId":"ld-live-001","score":92}'::text,'disabled','스텁 — 실발송 없음', now()-interval '3 hour'),
('KINTEX','whd-002','whs-002','invoice.paid',
'{"eventId":"e-2026-live","invoiceId":"inv-live-01","total":7187400}'::text,'disabled','스텁 — 실발송 없음', now()-interval '5 day'),
('KINTEX','whd-003','whs-003','auction.awarded',
'{"eventId":"e-2026-live","auctionId":"AUC-LIVE-01","awardId":"aw-live-01"}'::text,'disabled','스텁 — 실발송 없음', now()-interval '1 day'),
('KINTEX','whd-004','whs-001','lead.hot',
'{"eventId":"e-2026-smf","leadId":"ld-smf-006","score":91}'::text,'disabled','스텁 — 실발송 없음', TIMESTAMPTZ '2026-08-01 15:00+09')
ON CONFLICT (id) DO NOTHING;
-- ============================================================================
-- 10) login_history — 로그인 이력(마스킹 이메일·IP, 결과 분포)
-- ============================================================================
-- id 는 bigserial → 명시하지 않고 자동 채번. 재실행 시 중복 방지 위해 NOT EXISTS 가드.
INSERT INTO login_history (tenant_id, user_id, email_masked, result, fail_reason, ip_masked, user_agent, created_at)
SELECT * FROM (VALUES
('KINTEX','demo-organizer', 'org***@kintex.test','SUCCESS',NULL, '203.0.113.*', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/126', now()-interval '2 hour'),
('KINTEX','demo-exhibitor', 'exh***@kintex.test','SUCCESS',NULL, '203.0.113.*', 'Mozilla/5.0 (Macintosh; Intel Mac OS X) Safari/17', now()-interval '5 hour'),
('KINTEX','demo-contractor','con***@kintex.test','FAIL', 'BAD_CREDENTIAL','198.51.100.*','Mozilla/5.0 (Windows NT 10.0; Win64; x64) Edge/126', now()-interval '1 day'),
('KINTEX','demo-contractor','con***@kintex.test','SUCCESS',NULL, '198.51.100.*','Mozilla/5.0 (Windows NT 10.0; Win64; x64) Edge/126', now()-interval '23 hour'),
('KINTEX','demo-visitor', 'vis***@kintex.test','SUCCESS',NULL, '203.0.113.*', 'Mozilla/5.0 (Linux; Android 14) Chrome/126 Mobile', now()-interval '40 minute'),
('KINTEX', NULL, 'unk***@kintex.test','FAIL', 'BAD_CREDENTIAL','198.51.100.*','Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/125',now()-interval '3 day'),
('KINTEX','demo-exhibitor', 'exh***@kintex.test','LOCKED', 'LOCKED', '198.51.100.*','Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/126',now()-interval '2 day')
) AS v(tenant_id, user_id, email_masked, result, fail_reason, ip_masked, user_agent, created_at)
WHERE NOT EXISTS (SELECT 1 FROM login_history WHERE user_agent LIKE 'Mozilla/5.0%' AND ip_masked IN ('203.0.113.*','198.51.100.*') LIMIT 1);
-- ============================================================================
-- 11) sys_role_menu — 역할-메뉴 권한 매핑(RBAC 조회 화면 /admin/role-menus)
-- ============================================================================
-- ADMIN: 전 메뉴 읽기/쓰기. MANAGER: 업무(m-work*) 읽기/쓰기. USER: 업무 읽기전용.
INSERT INTO sys_role_menu (role_code, menu_id, read_yn, write_yn)
SELECT 'ADMIN', id, 'Y', 'Y' FROM sys_menu
ON CONFLICT (role_code, menu_id) DO NOTHING;
INSERT INTO sys_role_menu (role_code, menu_id, read_yn, write_yn)
SELECT 'MANAGER', id, 'Y', 'Y' FROM sys_menu WHERE id LIKE 'm-work%'
ON CONFLICT (role_code, menu_id) DO NOTHING;
INSERT INTO sys_role_menu (role_code, menu_id, read_yn, write_yn)
SELECT 'USER', id, 'Y', 'N' FROM sys_menu WHERE id LIKE 'm-work%'
ON CONFLICT (role_code, menu_id) DO NOTHING;
-- ============================================================================
-- 12) audit_log — 감사 로그(관리 조회 화면 /admin/audit)
-- ============================================================================
-- id 는 bigserial 자동 채번. 재실행 중복 방지 위해 대표 summary 존재 여부로 가드.
INSERT INTO audit_log (actor_id, actor_name, action, target_type, target_id, event_id, summary, ruleset_version, result, ip_hint, created_at)
SELECT * FROM (VALUES
('demo-organizer','주최자(데모)','AWARD_SELECT','auction','AUC-LIVE-01','e-2026-live','옥션 낙찰 선정 — (주)한빛부스디자인(4,200,000원)', 'auction-v1.0','SUCCESS','203.0.113.*', now()-interval '1 day'),
('demo-organizer','주최자(데모)','INVOICE_ISSUE','invoice','inv-live-01','e-2026-live','청구서 발행 — (주)스마트오토메이션 7,187,400원', 'billing-v1.0','SUCCESS','203.0.113.*', now()-interval '18 day'),
('demo-organizer','주최자(데모)','PAYMENT_RECORD','invoice','inv-live-01','e-2026-live','수납 등록 — 잔금 4,312,440원', 'billing-v1.0','SUCCESS','203.0.113.*', now()-interval '5 day'),
('demo-organizer','주최자(데모)','REFUND_RECORD','invoice','inv-live-08','e-2026-live','환불 산정 — 1,320,000원(개막일 이후 50%)', 'refund-v1.0','SUCCESS','203.0.113.*', now()-interval '2 day'),
('demo-exhibitor','참가업체(데모)','LEAD_EXPORT','lead',NULL,'e-2026-live','리드 6건 내보내기(CSV)', NULL,'SUCCESS','203.0.113.*', now()-interval '4 hour'),
('demo-contractor','공사업체(데모)','BID_SUBMIT','auction','AUC-LIVE-02','e-2026-live','응찰 제출 — 3,650,000원', 'auction-v1.0','SUCCESS','198.51.100.*', now()-interval '12 hour'),
('admin','시스템관리자','ROLE_MENU_UPDATE','sys_role_menu','MANAGER','','역할-메뉴 권한 변경 — MANAGER 업무 메뉴 쓰기 허용', NULL,'SUCCESS','203.0.113.*', now()-interval '10 day'),
('demo-organizer','주최자(데모)','AWARD_SELECT','auction','AUC-2026-0210','e-2026-smf','옥션 낙찰 선정 — 세계전기시설(12,000,000원)', 'auction-v1.0','SUCCESS','203.0.113.*', TIMESTAMPTZ '2026-07-30 11:00+09')
) AS v(actor_id, actor_name, action, target_type, target_id, event_id, summary, ruleset_version, result, ip_hint, created_at)
WHERE NOT EXISTS (SELECT 1 FROM audit_log WHERE action='AWARD_SELECT' AND target_id='AUC-LIVE-01');

View File

@ -0,0 +1,49 @@
package com.zioinfo.kintex.system.message;
import org.junit.jupiter.api.Test;
import java.util.HashMap;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.*;
/**
* 메시지 템플릿 렌더({key} 치환) 단위테스트 DB/스프링 없이 renderTemplate 순수 로직 검증(#35).
*/
class MessageRenderTest {
private final MessageService service = new MessageService(null);
@Test
void replacesKnownPlaceholders() {
Map<String, Object> p = Map.of("name", "홍길동", "eventName", "스마트팩토리");
String out = service.renderTemplate("{name}님, {eventName}에 오신 것을 환영합니다.", p);
assertEquals("홍길동님, 스마트팩토리에 오신 것을 환영합니다.", out);
}
@Test
void keepsMissingPlaceholderAsOriginal() {
String out = service.renderTemplate("{name}님, {eventName} 안내", Map.of("name", "김철수"));
assertEquals("김철수님, {eventName} 안내", out, "누락 파라미터는 원본 {key} 유지");
}
@Test
void nullValueBecomesEmpty() {
Map<String, Object> p = new HashMap<>();
p.put("name", null);
String out = service.renderTemplate("[{name}] 알림", p);
assertEquals("[] 알림", out, "null 값은 빈 문자열로 치환");
}
@Test
void nullTemplateAndParamsSafe() {
assertEquals("", service.renderTemplate(null, Map.of("a", "b")));
assertEquals("고정 문구", service.renderTemplate("고정 문구", null));
}
@Test
void ignoresNonPlaceholderBraces() {
String out = service.renderTemplate("금액 {amount} (단위: {}) 원", Map.of("amount", "1,000"));
assertEquals("금액 1,000 (단위: {}) 원", out, "빈 중괄호는 플레이스홀더로 오탐하지 않음");
}
}

View File

@ -4,9 +4,9 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>KINTEX AI 전시·행사시스템</title>
<link rel="icon" href="/favicon.ico?v=2" sizes="any" />
<link rel="icon" type="image/png" sizes="128x128" href="/favicon.png?v=2" />
<link rel="apple-touch-icon" href="/favicon.png?v=2" />
<link rel="icon" href="/favicon.ico?v=3" sizes="any" />
<link rel="icon" type="image/png" sizes="128x128" href="/favicon.png?v=3" />
<link rel="apple-touch-icon" href="/favicon.png?v=3" />
<meta name="theme-color" content="#0066b3" />
<link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin />
<link

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 285 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 294 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 247 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Some files were not shown because too many files have changed in this diff Show More