deploy: public header restore + accent white-text sweep + mobile v0.2.0 source (web 0add2cd/mobile 490b723 over f3b800f)
50
.claude/agents/kintex-gap-auditor.md
Normal 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
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
44
.claude/skills/kintex-completeness-orchestrator/SKILL.md
Normal 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이 초기/델타/부분 판별.
|
||||
@ -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
@ -34,3 +34,7 @@ dist/
|
||||
|
||||
# 전시장 CAD 원본(대용량 DWG zip) — 로컬 보존, git 제외
|
||||
docs/assets/floorplans/cad/
|
||||
|
||||
# graphify / screenshot generated outputs (local only)
|
||||
graphify-out/
|
||||
screenshot/
|
||||
|
||||
33
CLAUDE.md
@ -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
@ -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
@ -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회 생성.
|
||||
@ -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) | 상단 헤더 — 로고·검색·알림·설정·사용자 드롭다운·풀스크린·언어 | 대기 |
|
||||
|
||||
@ -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}", "인덱스"), ("V1–V10", "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)
|
||||
@ -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
|
||||
@ -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))
|
||||
@ -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)
|
||||
@ -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)
|
||||
@ -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'}")
|
||||
@ -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))}')
|
||||
@ -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는 "승인 예외"로만 표기.
|
||||
@ -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}")
|
||||
@ -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조⑤ 단서) |
|
||||
@ -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"))
|
||||
@ -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 생성 이미지는 전 슬라이드에서 워터마크·금지 고지 정책 명시.
|
||||
@ -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 등)은 압축 유지.
|
||||
@ -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 고지 존재. 실제 공고 확인 시 배점·목차·투입계획 재정렬만 반영하면 제출 가능.
|
||||
@ -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·미확보 데이터의 확정적 서술 금지. 미확보는 "가정/협의 전제" 라벨 필수.
|
||||
@ -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)
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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`
|
||||
@ -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"로만.
|
||||
106
docs/design.md
@ -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 4–6 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.05–09.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 무수정 |
|
||||
|
||||
@ -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 게이트(소유자 승인).
|
||||
|
||||
@ -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",
|
||||
{
|
||||
|
||||
@ -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} />
|
||||
),
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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)')}
|
||||
/>
|
||||
|
||||
@ -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}>
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
68
mobile/components/LanguageSelector.tsx
Normal 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 },
|
||||
});
|
||||
@ -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,
|
||||
}));
|
||||
}, []);
|
||||
|
||||
|
||||
62
mobile/context/LanguageContext.tsx
Normal 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
@ -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;
|
||||
204
mobile/lib/i18n/locales/en.ts
Normal 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: '8–100 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: '8–100 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',
|
||||
},
|
||||
};
|
||||
204
mobile/lib/i18n/locales/ja.ts
Normal 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: '有効 · Authenticator(OTP)',
|
||||
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: '予約詳細 · キャンセル',
|
||||
},
|
||||
};
|
||||
208
mobile/lib/i18n/locales/ko.ts
Normal 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;
|
||||
203
mobile/lib/i18n/locales/zh.ts
Normal 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: '已启用 · Authenticator(OTP)',
|
||||
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
@ -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
@ -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)';
|
||||
}
|
||||
}
|
||||
86
mobile/package-lock.json
generated
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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 // 연락처(선택)
|
||||
) {
|
||||
}
|
||||
}
|
||||
@ -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): 고득점(score≥80) 리드로 팔로업이 필요한 건. 내가 참가업체(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);
|
||||
|
||||
// ── 쇼케이스(진행 중·다가오는 전시) ────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -12,6 +12,7 @@ public record PublicEventDto(
|
||||
String endDate,
|
||||
String status,
|
||||
String hallLabel,
|
||||
Integer boothCount
|
||||
Integer boothCount,
|
||||
String posterUrl
|
||||
) {
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
}
|
||||
}
|
||||
@ -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) {
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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(값이 없으면 조건 무시) — <script> 미사용.
|
||||
* <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);
|
||||
}
|
||||
@ -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';
|
||||
@ -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;
|
||||
@ -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();
|
||||
@ -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');
|
||||
@ -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, "빈 중괄호는 플레이스홀더로 오탐하지 않음");
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
||||
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 4.8 KiB |
BIN
src/frontend/public/media/hero/hero-1.jpg
Normal file
|
After Width: | Height: | Size: 191 KiB |
BIN
src/frontend/public/media/hero/hero-2.jpg
Normal file
|
After Width: | Height: | Size: 285 KiB |
BIN
src/frontend/public/media/hero/hero-3.jpg
Normal file
|
After Width: | Height: | Size: 294 KiB |
BIN
src/frontend/public/media/hero/hero-4.jpg
Normal file
|
After Width: | Height: | Size: 247 KiB |
BIN
src/frontend/public/media/hero/hero-5.jpg
Normal file
|
After Width: | Height: | Size: 176 KiB |
BIN
src/frontend/public/media/posters/kx-468958-1.jpg
Normal file
|
After Width: | Height: | Size: 35 KiB |