솔루션명 수정

This commit is contained in:
zio 2026-07-13 04:23:07 +09:00
parent 277c895b47
commit 37aeebdd28
44 changed files with 1400 additions and 94 deletions

View File

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

24
.claude/settings.json Normal file
View File

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

View File

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

View File

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

View File

@ -144,3 +144,30 @@ GUARDiA 표준 프레임워크(Spring Boot 3.5 + React 19 + MyBatis + PostgreSQL
| 2026-07-11 | 초기 구성 — kintex-mobile-dev 신규 + 오케스트레이터(재사용: designer·backend-dev·qa·devops-dev·reviewer). WISE 모바일 레퍼런스·Stitch 디자인 경유 반영 | 전체 | 모바일 앱 트랙 전담 하네스 부재("하네스 생성" + "wise 모바일 참고" + "디자인은 스티치" 요청) |
| 2026-07-11 | **앱 타깃 2개 확정 반영** — 코드베이스 1(mobile/) + 배포 타깃 2(①운영 B2B: 2FA 필수·사내 QR ②관람객 B2C: 스토어 공개·간편가입·게스트). 계정은 단일 통합+가입 트랙 분리(PLANNING v3.1) | kintex-mobile-dev·오케스트레이터 | 소유자 확정 — 회원가입 관람객 포함 질의 → 하이브리드(계정 통합·앱 분리) 채택 |
| 2026-07-12 | **내정보(WISE)+생체인식(expo-local-authentication)+프로필 사진(expo-image-picker)** + **앱 위변조 방지·시큐어코딩 보안 체크리스트**(루트/탈옥·무결성·Hermes+R8·화면캡처·cleartext·권한최소, `docs/security/mobile-security-*.md`) + **다국어(react-i18next)·역할별 랜딩 패리티** 착수 | kintex-mobile-dev·kintex-qa | "모바일 내정보 WISE+생체+사진"·"앱 위변조방지·시큐어코딩·보안체크"·"모바일도 동일 로직"·"웹 4개국어인데 모바일은?" |
## graphify
This project has a knowledge graph at graphify-out/ with god nodes, community structure, and cross-file relationships.
Rules:
- For codebase questions, first run `graphify query "<question>"` when graphify-out/graph.json exists. Use `graphify path "<A>" "<B>"` for relationships and `graphify explain "<concept>"` for focused concepts. These return a scoped subgraph, usually much smaller than GRAPH_REPORT.md or raw grep output.
- If graphify-out/wiki/index.md exists, use it for broad navigation instead of raw source browsing.
- Read graphify-out/GRAPH_REPORT.md only for broad architecture review or when query/path/explain do not surface enough context.
- After modifying code, run `graphify update .` to keep the graph current (AST-only, no API cost).
---
## 하네스: 미개발 확인 (kintex-completeness-orchestrator)
**목표:** 킨텍스 앱의 **미개발·미완성**을 전수 확인. 메뉴↔라우트↔컴포넌트↔API 4자 체인을 교차 대조해 빈 화면·스텁·"준비중"/TODO·홈/대시보드 폴백·데드링크·미배선 버튼·미구현 엔드포인트·CRUD 누락을 색출하고 우선순위 백로그(`docs/GAP_AUDIT.md`) 산출. **확인/감사만** — 수정은 구현 하네스 인계.
**트리거:** 미개발 확인, 완성도 감사, 빈 화면/준비중 화면 찾기, 홈으로 가는 메뉴, 미구현 점검, 개발 안 된 것, 다시 실행·특정 영역만·업데이트 요청 시 `kintex-completeness-orchestrator` 스킬을 사용하라.
**에이전트:** kintex-gap-auditor(신규, 4자체인 감사·인벤토리) + kintex-qa(재사용, P0/P1 실동작 재검증·오탐 제거) + reviewer(선택, 정본 대조).
**경계(중복 회피):** 확인=이 하네스. 수정·구현=kintex-impl-orchestrator, UI정렬=kintex-wise-ui-orchestrator, 데이터 빈 화면=kintex-renewal/testdata. ★오탐 주의: "코드 미개발" vs "데이터만 빈(행사스코프 시드 미정렬)" 구분.
**변경 이력:**
| 날짜 | 변경 내용 | 대상 | 사유 |
|------|----------|------|------|
| 2026-07-12 | 초기 구성(kintex-gap-auditor 신규 + kintex-qa 재사용 + 오케스트레이터 + gap-taxonomy 레퍼런스) | 전체 | "미개발 확인 하네스 생성" 요청 — 4자체인 교차대조 완성도 감사 트랙 부재 |

107
docs/GAP_AUDIT.md Normal file
View File

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

View File

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

View File

@ -0,0 +1,278 @@
# KINTEX AI 전시·행사시스템 — 개발계획서
> - 문서 종류: 개발계획서(Development Plan)
> - 프로젝트: **KINTEX AI 전시·행사시스템** (KINTEX AI Exhibition & Event System)
> - 작성일: 2026-07-12 · 버전: v1.0
> - 근거 문서: `docs/PLANNING.md`(v3.4) · `docs/IMPLEMENTATION_BACKLOG.md`(v2.0) · `docs/BUILD_DEPLOY.md` · `docs/architecture/*` · 실제 구현 스키마(Flyway V1~V49)
> - 산출물 정책: 개발계획서·설계서는 개발 착수 시점 1회 작성(본 문서). 사용자/운영자 지침서는 UI 정렬 안정화 후 별도 산출.
> - 보안: 본 문서에는 자격증명·비밀번호·SSH·내부 IP·시크릿을 기재하지 않는다.
---
## 목차
1. 사업 개요
2. 목표 및 기대효과
3. 개발 범위(모듈)
4. 기술 스택
5. 시스템 아키텍처 개요
6. 개발 방법론 및 조직(에이전트 트랙)
7. 개발 단계(Phase)와 WBS
8. 마일스톤 및 일정
9. 선행 게이트 및 전제조건
10. 리스크 관리
11. 품질 관리 방침
12. 보안 방침
13. 배포(CI/CD) 계획
---
## 1. 사업 개요
### 1-1. 배경
킨텍스(KINTEX, 한국국제전시장)는 총 전시면적 108,011㎡(2028년 제3전시장 완공 시 178,000㎡)를 운영하나, 전시 준비 실무는 **HWP 서식 다운로드 → 이메일/방문 제출**, **CAD 수작업 배치도 → 홀매니저 육안 검수**, **수기 위치표시도 기반 유틸리티 신청**에 머물러 있다. 온라인 작업신고(kxwp.kintex.com)는 서류 업로드 창구 수준이며, 참가업체 유틸리티 신청은 킨텍스 공통 플랫폼 없이 전시회별 주최자 사무국 시스템으로 파편화되어 있다.
### 1-2. 비전
**"신청서를 내는 순간, 시공 후 사진을 먼저 본다."**
전시장 운영 전 과정(홀 배정 → 부스 배치 → 장치공사 설계 → 전기/조명 → 네트워크/유틸리티 배선 → 반입/반출)을 AI로 자동 설계·검증하고, 그 결과를 **나노바나나(Gemini 이미지 생성)로 '공사 후 결과 사진'처럼 시공 전에 미리 생성**해 보여주는 것이 핵심 차별화다.
### 1-3. 정체성 확장(제품 진화)
- **v2.0 자동전시시스템**: 부스 시공 시각화(P0 코어 M2~M5)를 심장으로 두되, 전시 기획·판매·시공·운영·관람객·사후분석 전 주기를 자동화하는 베뉴 운영 플랫폼으로 확장(도메인 모듈 M10~M18 신설).
- **v3.0 다중 전시관 SaaS**: KINTEX를 기준 테넌트(#1)로 두고 코엑스(COEX) 등을 테넌트로 온보딩하는 멀티테넌트 SaaS. 기능은 보존하고 `tenant_id` 격리 레이어만 순증.
- **v3.2 대외 접점 3-트랙**: visitor(관람객) / business(주최자·참가업체) / agency(공사·장치·협력사) 3-트랙 IA 재구성.
- **v3.4 AI 사용성 & 토큰 최소화**: 전 화면 인라인 AI 진입점 표준화 + LLM 토큰 최소화 6원칙(§8A).
> 근거: PLANNING §1·§1-2·§1A·§2-2·§8A.
---
## 2. 목표 및 기대효과
| 목표(To-Be) | 현재(As-Is) | 개선 |
|---|---|---|
| 홀 배정 견적 | 문의→협의→견적 수일 | 규칙 엔진 즉시 자동 견적(2,250원/㎡ × 성수기/전시장 계수) |
| 부스 배치도 초안 | CAD 수작업 수일~수주 | 조건 입력 후 수 분 내 복수 안(1·2·3안) 생성·선택/병합 |
| 도면 규정 검수 | 홀매니저 육안 | 규정 위반 자동 플래깅(높이 5m·방염·리깅 등) 후 사람 확정 |
| 유틸리티 위치표시도 | 참가업체 수기 작도 | 부스 좌표 클릭 → 자동 배선안 + 자동 견적 |
| 시공 결과 예측 | 불가(외주 조감도) | 표준 샷 세트 자동 생성(정면/야간/통로/배선 오버레이) |
> 근거: PLANNING §1-3 목표(정량).
---
## 3. 개발 범위(모듈)
### 3-1. 코어(P0, 불변) — 설계·시각화
- **M2 플로어플랜 스튜디오** — 부스 배치 자동 생성(제약 솔버 중심), 3안 생성 → 선택/병합, 규정 자동 검증(통로·바닥하중·비상구).
- **M3 부스 설계 스튜디오** — 조립/독립 부스 설계 3안, 규정 사전 검증(높이 5m·리깅 6.5~8.5m·방염·이격).
- **M4 유틸리티 설계** — (a) 전기·조명, (b) 네트워크·급배수·압축공기. 트렌치 최단 배선 자동 산출, 위치표시도 자동 생성, 자동 견적.
- **M5 나노바나나 시각화** — M2~M4 구조화 데이터를 프롬프트로 컴파일해 "시공 후 사진" 표준 샷 세트(S1~S7) 생성.
### 3-2. 판매·발주·정산(P1)
- **M1** 홀 배정·자동 견적 / **M6** 서류·마일스톤 워크플로 / **M7** 등록업체 매칭·견적 / **M9** 정산·결제
- **M15 공사/장치 옥션(핵심 플로우)** — AI 설계자료를 근거로 등록업체가 견적서(Quotation) 제출 → 역경매(라운드·실시간 순위) → 낙찰(Award) → 계약/발주 연동. 폐루프의 연결고리.
- **M8** 반입/반출 물류 슬롯(P2)
### 3-3. 관람·참가·마케팅
- **M10 관람객 등록·티켓·배지·체크인·리드캡처(P1)** / **M12 마케팅·EDM·공개 홍보 사이트(P1, SEO·다국어)**
- **M11** 비즈니스 매칭 / **M13** wayfinding·실내 내비 / **M14** 현장운영(혼잡·안전·주차·에너지) (P2)
### 3-4. 경영·콘텐츠·관리(P1)
- **M16 경영분석 BI** — 운영사(킨텍스) 관점 수익성/ROI(가동률·매출구성·행사별 P&L·전시장 ROI·리텐션/LTV·수율·경영진 KPI) + 참가사 관점 ROI.
- **M17 CMS** — 콘텐츠·공지·참가업체 마이크로사이트·다국어·사이니지 연계.
- **M18 관리자 백오피스** — §5B 시스템관리 + 킨텍스 마스터데이터(홀·요율·규정 룰셋·등록업체).
### 3-5. 공통/시스템관리 레이어(§5B, P1·전 모듈 선행 기반)
UIWS(GUARDiA 표준 프레임워크) 이식: 인증(JWT+2FA/OTP·로그인 실패 잠금·admin 비번 env 주입), 시스템관리(사용자·역할/권한 RBAC·공통코드·메뉴·감사로그·시스템설정), 공통 업무기능(worklog·schedule·message·notice·opinion·search·meeting·report·notification 등).
> 근거: PLANNING §5(M1~M9)·§5A(M10~M18)·§5B(공통 레이어)·기능 우선순위 총괄표.
---
## 4. 기술 스택
| 레이어 | 기술 | 비고 |
|---|---|---|
| 프론트엔드 | **React 18/19 + Vite + TypeScript** | 반응형(데스크톱=설계/에디터, 모바일=조회·승인·현장), 역할별 번들 분리 |
| 백엔드 | **Spring Boot 3.x(Java 17) + MyBatis** | REST + WebSocket(STOMP), 룰 엔진·배치/배선 엔진(서비스 계층) |
| DB | **PostgreSQL + PostGIS** | 부스 폴리곤·트렌치 포인트·배선 LineString 공간 데이터, Flyway 마이그레이션 |
| 비동기 | **Redis 작업 큐** | RenderJob·서류·알림 |
| 이미지 생성 | **나노바나나 Python 워커 사이드카**(`tools/nanobanana`) | google-genai + `gemini-3.1-flash-image-preview`, Spring이 큐로 트리거 |
| 인증 | **JWT + RBAC + TOTP 2FA** | 행사 단위 RBAC, UIWS 표준 이식 |
| AI(텍스트) | **Claude 기본 + 설정형 전환**(AiTextRouter/AiConfig) | Claude→Ollama 소형모델 폴백, 토큰 최소화 라우팅 |
| 모바일 | Expo SDK 51 + React Native + TypeScript(`mobile/`) | 코드베이스 1개·배포 타깃 2개(B2B 운영/B2C 관람객) |
- GUARDiA 표준 프레임워크(Spring Boot + React + MyBatis + PostgreSQL) 정렬.
- 나노바나나 image-to-image 파이프라인은 Python SDK(google-genai) 검증 자산을 재사용하기 위해 **Python 워커 사이드카로 유지**하고, Spring은 큐·오케스트레이션·상태 관리만 담당.
> 근거: PLANNING §8 아키텍처, CLAUDE.md 기술 스택 확정표.
---
## 5. 시스템 아키텍처 개요
- **역할별 분리 프론트 + 공유 백엔드**: organizer·exhibitor·contractor·ops·admin 5개 인증 앱 + public(공개 사이트)·visitor(관람객 앱). 공유 디자인 시스템·공유 컴포넌트·공유 API 계약 상속(최소권한·공격면 축소).
- **공유 Spring Boot 백엔드**: REST + WebSocket, 룰 엔진(요율/규정), 배치·배선 엔진(PostGIS), 옥션 엔진(M15), BI 집계(M16), CMS(M17), Redis 큐, 나노바나나 워커.
- **데이터**: PostgreSQL+PostGIS(업무·공간 데이터) + 오브젝트 스토리지(도면·생성 이미지·서식·콘텐츠).
- **멀티테넌시**: 공유 스키마 + `tenant_id` 컬럼(단일 DB) 논리 격리, fail-closed 다층 방어(필터→서비스 가드→MyBatis 강제 바인딩→PostGIS).
- **AI 사용성·토큰 최소화**: 단일 `/ai/ask` 계약, 결정론 우선 라우팅(사실조회는 LLM 미호출), 소형모델 우선 티어링, RAG 발췌·캐시·구조화 출력.
> 상세는 설계서(`02_설계서.md`) 참조. 근거: PLANNING §8·§8-1·§8-2·§8A.
---
## 6. 개발 방법론 및 조직(에이전트 트랙)
- **방법론**: 하네스 기반 에이전트 오케스트레이션(`kintex-impl-orchestrator`). Phase A(아키텍처) → B(공통 레이어) → C(부스 코어) → D(도메인) → E(배포)의 단계적 진행, 각 모듈 완성 직후 QA 점진 검증.
- **문서 워크플로(CLAUDE.md)**: planner(기획, PLANNING.md) → designer(디자인, design.md·Stitch 경유) → developer(구현, src/) → visualizer(나노바나나) → reviewer(교차 검증).
- **개발 조직(에이전트 트랙)**:
| 구분 | 에이전트 | 역할 |
|---|---|---|
| 거버넌스 | kintex-pm·dev-pm·pmo | 범위·일정·게이트·품질 게이트·산출물 관리 |
| 아키텍트 | kintex-aa·sa·ta·da·na | 애플리케이션·시스템·기술·데이터·네트워크 아키텍처 |
| 공통 | kintex-common-dev | UIWS 공통/시스템관리·2FA 이식 |
| 코어 | kintex-backend-dev·frontend-dev·db-engineer | Spring/MyBatis·React·PostGIS 스키마 |
| 도메인 | kintex-bidding-dev·visitor-dev·cms-dev·bi-dev·admin-dev | M15·M10·M12/M17·M16·M18 |
| AI·시각화 | kintex-ai-dev·visualizer | Claude AI 기능·나노바나나 |
| 품질·배포 | kintex-qa·devops-dev | 경계면 QA·CI/CD |
| 모바일 | kintex-mobile-dev | Expo 앱 트랙 |
> 근거: CLAUDE.md 하네스(kintex-impl-orchestrator) 로스터, IMPLEMENTATION_BACKLOG 담당 약칭.
---
## 7. 개발 단계(Phase)와 WBS
### Phase A — 아키텍처·거버넌스
| ID | 작업 | 산출물 |
|---|---|---|
| A-1 | 애플리케이션 아키텍처(모듈 경계·레이어·API 표준·패키지) | architecture/app.md |
| A-2 | 시스템 아키텍처·NFR(확장성·HA·성능·보안영역·배포 토폴로지) | architecture/system.md |
| A-3 | 기술 표준(스택·빌드/배포·개발표준·관측성·AiTextRouter) | architecture/tech.md |
| A-4 | 데이터 아키텍처(전사 ERD·공간데이터·마스터·공통코드·BI 마트) | architecture/data.md |
| A-5 | 네트워크 아키텍처(DMZ/내부망·방화벽·부하분산) | architecture/network.md |
| A-6 | 아키텍처↔PLANNING 정합 검증 | 불일치 0/티켓화 |
### Phase B — 공통/시스템관리 레이어(★전 모듈 선행)
| ID | 작업 | 완료 기준 |
|---|---|---|
| B-0 | 스캐폴드(Spring Boot·Vite·PostGIS·Flyway) | 빌드·마이그레이션 통과 |
| B-1 | 인증 JWT+RBAC(6역할) + 2FA(TOTP) + 로그인 실패 잠금 + admin 비번 env | 2FA 등록/검증/초기화 왕복 |
| B-2 | 시스템관리(사용자·역할/권한·공통코드·메뉴·감사로그·설정) | CRUD·RBAC 가드·감사 기록 |
| B-3 | 공통 업무기능(worklog·schedule·message·notice·opinion·search·meeting·report·notification) | 각 모듈 API 왕복 |
| B-4 | 공통 컴포넌트(예외·응답봉투·감사 AOP·알림 단일화) + 프론트 공통 | AA 표준 정합 |
| B-5 | 점진 QA(2FA·RBAC·비번/PII 미노출·경계면) | 통과까지 반려 |
### Phase C — P0 부스 시공 코어
| ID | 모듈 | 완료 기준 |
|---|---|---|
| C-M2 | 플로어플랜(Booth polygon·Trench·PostGIS 규정검증·3안 생성/병합) | 3안 생성·병합·규정 재검증 |
| C-M3 | 부스 설계(조립/독립·규정 사전검증·3안/병합) | 위반 사전 플래깅·컨펌 |
| C-M4 | 유틸리티 배선(Wiring LineString·자동 견적·위치표시도) | 최단 배선·견적·위치표시도 |
| C-M5 | 나노바나나(RenderJob·Redis·WebSocket·워커·S6 래스터·Before/After) | (G1 시)실생성/미승인시 목·워터마크 강제 |
| C-C | 공통 화면(로그인·주최자·참가업체·홀매니저·현장 모바일) | 역할별 진입·상태 처리 |
### Phase D — P1 도메인(역할별 포털 분리)
| ID | 모듈 | 완료 기준 |
|---|---|---|
| D-M15 | 공사/장치 옥션(견적서 제출→역경매→낙찰, 등록업체만 응찰) | 견적서·역경매·낙찰 왕복 |
| D-M10 | 관람객·현장(등록·티켓·배지/QR·체크인·리드캡처) | 등록→배지→체크인→리드 |
| D-M12/17 | 마케팅·공개사이트·CMS(SEO·다국어·게시 워크플로) | 공개 조회·게시·SEO |
| D-M16 | 경영분석 BI(가동률·P&L·리텐션·KPI·Recharts) | 지표 집계·드릴다운·내보내기 |
| D-M18 | 관리자 백오피스(시스템관리+마스터데이터·룰셋 버전) | RBAC·마스터 CRUD·룰셋 버전 |
| D-M1/6/7/9 | 배정·서류·매칭·정산 | 견적·마일스톤·정산 |
### Phase E — P2 + 빌드·배포
M11 비즈매칭 / M13 wayfinding / M14 현장운영 + 역할별 프론트 번들·Spring jar·워커 서비스 빌드·Gitea CI/CD·systemd·env(G2 후).
### Phase F — 벤치마킹 유래 추가(티켓·관람객)
스마트티켓 부정입장 방지(회전 QR)·환불 규정 정교화·티켓/배지 다국어·세션 정원 RSVP·간편결제·라이브 공지(ⓑ 보강 6) + 티켓 오픈 알림·주차 연계·혼잡 안내·QR 명함 교환·대기열·멤버십·개인화 홈·셀프 체크인 키오스크(ⓒ 신규 8).
> 근거: IMPLEMENTATION_BACKLOG v2.0 Phase A~F 전문.
---
## 8. 마일스톤 및 일정(로드맵)
| Phase | 기간(목표) | 범위 요약 |
|---|---|---|
| Phase 1 — 설계·시각화 코어(MVP) | ~4개월 | §5B 공통 레이어 선행 + M2(단일 홀 근사 도면)·M3·M4·M5(S1·S2·S6·S7) |
| Phase 2 — 워크플로·운영 통합 | ~4개월 | M1·M6·M7·M9 + 홀매니저 대시보드 + 전 홀(10개) 도면 확장 |
| Phase 3 — 현장·확장 | ~4개월+ | M8·kxwp 정식 API·정밀 조도·다국어 규정 챗봇·관람객 플로어플랜 |
- 각 Phase 종료 시 reviewer 에이전트 교차 검증(기획-디자인-구현 정합성).
- Phase 1 착수 조건: Gemini API 키·홀 참조 사진·트렌치 좌표(미확보 시 공개 스펙 기반 '가정' 그리드).
> 근거: PLANNING §9 로드맵.
---
## 9. 선행 게이트 및 전제조건
| 게이트 | 내용 | 영향 |
|---|---|---|
| **G1** | 나노바나나(Gemini) 외부 호출 소유자 승인(PLANNING R12) | M5 실호출·배포 전. 미승인 시 목/degraded |
| **G2** | 배포 대상 서버·포트(개발 `kintex.zioinfo.co.kr` 포트 8021, 운영 별개 도메인) | Phase E 배포 착수 전 |
| **G3** | 모바일 EAS 클라우드 빌드 소유자 승인 | 앱 실빌드·QR 배포 전 |
전제조건(킨텍스 협의): CAD 도면·트렌치 실측 제공, kxwp 연동 논의, 요금 정합성 확인(인터넷 150,000 vs KT 80,000). 확보 자산: 홀별 평면도 JPG 15장 + CAD(제1전시장 트렌치 DWG) → PLANNING R4 부분 해소.
> 근거: IMPLEMENTATION_BACKLOG 선행 게이트·확보 자산, CLAUDE.md 하네스 게이트.
---
## 10. 리스크 관리
| # | 리스크 | 영향 | 완화 |
|---|---|---|---|
| R1 | AI 생성 이미지가 실제 시공과 다름(계약 오인 분쟁) | 높음 | 전 이미지 워터마크·고지, 계약/심사 서류 자동 배제, "시공 기준은 도면" 동의 |
| R2 | 도면 심사 책임(자동 통과 ≠ 승인) | 높음 | 시스템을 '사전 필터'로 정의, 최종 승인 주체 명시, 면책·룰셋 버전 기록 |
| R3 | kxwp 연동 불확실성(폐쇄형·API 미공개) | 중간 | 파일 자동 생성+수동 업로드 릴레이로 독립 가치 확보, 병행 협의 |
| R4 | 트렌치·CAD 실측 미확보 | 높음 | '가정' 그리드 라벨링 + 킨텍스 데이터 제공을 Phase 2 전제로 계약화 |
| R6 | 이미지 생성 비용·지연(부스 수백 개) | 중간 | 자동 생성 S1·S7 한정, 온디맨드+스키마 해시 캐시, 행사별 쿼터 |
| R8 | 요금·규정 공개값과 실계약가 차이 | 중간 | "공시가 기준, 최종가는 킨텍스 확정" 고지, 룰셋 버전 교체 절차 |
| R10 | 부스 설계 영업비밀 민감성 | 중간 | 행사 단위 격리, 소유 참가업체+주최자+홀매니저로 접근 한정 |
| R12 | 나노바나나(Gemini) 외부 API 승인 게이트 | 높음 | G1 승인 선행, 미승인 시 온프레미스 이미지 생성 폴백 검토 |
> 근거: PLANNING §10 리스크 및 제약(R1~R12).
---
## 11. 품질 관리 방침
- **점진 QA**: 각 모듈 완성 직후 kintex-qa가 백엔드 응답 shape ↔ 프론트 호출 경계면 불일치를 교차 검증. 공간 로직(배치·배선·규정) 정합, 빌드 회귀 확인.
- **품질 게이트(반려 사유)**: 자격증명·PII·스택트레이스 미노출, AI 이미지 워터마크 강제, 등록업체 응찰 게이트, admin 비번 env 주입.
- **빌드 게이트(push 전)**: 시크릿 커밋 차단, Flyway 번호 충돌 검사, 변경분 compileJava/tsc·lint 실패 시 push 차단.
- **정합성 검증**: reviewer 에이전트가 기획-디자인-구현 3자 정합을 Phase 종료 시 교차 검증.
> 근거: IMPLEMENTATION_BACKLOG 진행 규칙·B-5, BUILD_DEPLOY §3.
---
## 12. 보안 방침
- **인증·인가**: JWT + 행사 단위 RBAC(ORGANIZER·EXHIBITOR·CONTRACTOR·HALL_MANAGER) + 플랫폼/테넌트 관리자 계층 + TOTP 2차 인증(업무 사용자 필수, 관람객 미강제).
- **자격증명 보호**: `password_hash`(BCrypt)·`otp_secret`(TOTP)는 API 응답에서 완전 제외. admin 비밀번호는 env(`ADMIN_PASSWORD_ENC` AES-256-GCM + 별도 키파일) 주입, 하드코딩 `admin123` 시드 금지.
- **테넌트 격리**: 전 계층 `tenant_id` 강제, 크로스-테넌트 접근은 플랫폼 슈퍼관리자 전용 API로만(감사).
- **외부 API**: 온프레미스 원칙. 예외 승인 = Claude API(`api.anthropic.com`), 나노바나나(Gemini)는 G1 승인 게이트. 키는 서버 env로만 로드(코드·DB·커밋·로그·응답 기록 금지).
- **에러 응답**: 스택트레이스 미노출 — 요약 메시지만 전달. 감사로그(승인·낙찰·설계 변경·룰셋 개정·리드 접근)에 전수 기록.
> 근거: PLANNING §5B-3·§8-2·§10, CLAUDE.md 보안 제약, 실제 스키마(app_user 컬럼 코멘트).
---
## 13. 배포(CI/CD) 계획
- **실행 단위 3개**: 백엔드(`./gradlew bootJar` → jar, systemd) / 프론트(Vite `npm run build` → dist, nginx 정적 서빙, 역할별 번들 분리) / 나노바나나 워커(Python 큐 소비 데몬).
- **파이프라인**: `workspace/kintex → git push → Gitea(zio/kintex) → webhook → deploy_server`. 빌드(bootJar·vite build) → Flyway 마이그레이션 → jar 재기동·dist 배포 → 헬스 게이트(`GET /health`).
- **Fail-Safe**: 백업 → 배포 → 헬스체크(200) → 실패 시 롤백(이전 jar 유지). clean bootJar 검증 후 교체.
- **인프라**: systemd 유닛(백엔드·워커), nginx vhost(`/`=프론트 정적, `/api/`·`/ws`=백엔드), TLS certbot, AI env drop-in.
- **개발 도메인**: `kintex.zioinfo.co.kr`(포트 8021, PostGIS+Redis+Flyway). 운영 배포는 소유자 승인 필수(G2).
> 근거: BUILD_DEPLOY §1·§4·§5, CLAUDE.md 하네스(kintex-impl-orchestrator) G2 게이트.
---
> 후속 산출: 본 개발계획서와 함께 `02_설계서.md`를 산출한다. **사용자지침서·운영자지침서는 UI 정렬 안정화 이후 별도 산출**(본 산출 범위 아님 — deliverables 갱신 정책상 완성+QA 통과 후 최신 메뉴 반영).

Binary file not shown.

View File

@ -0,0 +1,420 @@
# KINTEX AI 전시·행사시스템 — 설계서
> - 문서 종류: 설계서(System Design Document)
> - 프로젝트: **KINTEX AI 전시·행사시스템** (KINTEX AI Exhibition & Event System)
> - 작성일: 2026-07-12 · 버전: v1.0
> - 근거 문서: `docs/PLANNING.md`(v3.4) · `docs/design.md`(v2.4) · `docs/architecture/{app,system,tech,data,network,sso-hr-integration}.md` · `docs/COMMON_CODES.md` · **실제 구현 스키마(Flyway `V1~V49`)** · 실제 컨트롤러(REST 계약)
> - 보안: 본 문서에는 자격증명·비밀번호·SSH·내부 IP·시크릿을 기재하지 않는다.
> - 비고: 본 설계서는 구현된 스키마·API 계약을 정본으로 삼고, 계획(설계 완료·미구현) 항목은 그 취지를 명시한다.
---
## 목차
1. 시스템 아키텍처
2. 애플리케이션 아키텍처(레이어·모듈·의존성)
3. 데이터 설계
4. API 설계
5. 화면 설계
6. AI 설계(§8A)
7. 인증·인가 설계
8. 네트워크·보안영역 설계
9. 비기능 요구사항(NFR)
---
## 1. 시스템 아키텍처
### 1-1. 전체 구성
**6개 역할별 분리 프론트(React·Vite·TS) + 단일 공유 Spring Boot 백엔드 + 나노바나나 Python 워커 사이드카**를 SSO·역할 RBAC 위에 얹고, PostGIS·Redis 큐·오브젝트 스토리지를 공유한다.
```
[엣지/DMZ] CDN · WAF/nginx(TLS 종단)
[역할별 프론트] organizer · exhibitor · contractor · ops · admin(내부) · public/visitor
│ (SSO · 역할 RBAC 게이트)
[공유 Spring Boot 백엔드] REST + WebSocket(STOMP)
│ 룰 엔진(요율/규정) · 배치·배선 엔진(PostGIS) · 옥션 엔진(M15) · BI 집계(M16) · CMS(M17) · 공개 API
[비동기] Redis 큐 → 나노바나나 워커(google-genai) · 서류/PDF/EDM 워커 → 오브젝트 스토리지
│ (완료 시 WebSocket 푸시)
[데이터] PostgreSQL + PostGIS (프라이머리 + 읽기 복제) · 오브젝트 스토리지
```
### 1-2. 대원칙(5)
1. **단일 공간 데이터 모델** — 부스 폴리곤·트렌치 포인트·배선 경로를 PostGIS로 일원화(설계·시각화·검증·정산·wayfinding·BI가 동일 원천 재사용).
2. **이미지 생성 전면 비동기** — Spring이 RenderJob을 Redis 큐에 발행 → Python 워커가 Gemini로 생성 → 오브젝트 스토리지 적재 → WebSocket 완료 푸시. `GEMINI_API_KEY`는 워커 전유(백엔드 미취급).
3. **역할별 프론트 분리** — 최소권한·공격면 축소, 공유 디자인 시스템·컴포넌트·API 계약 상속.
4. **룰셋 = 버전 관리 데이터** — 규정(높이·방염·하중)·요율을 코드가 아닌 `rulesets/*.json` + master_data로 관리(연 단위 개정 무중단 반영).
5. **공개 vs 내부 백오피스 보안영역 분리**(§8).
### 1-3. 배포 토폴로지(존)
- **공개 존(DMZ)**: Reverse Proxy/WAF·nginx·TLS, CDN, 공개 프론트 SSR/SSG, 공개 API GW(화이트리스트 엔드포인트만), PG 콜백.
- **애플리케이션 존(내부망)**: 인증 프론트 정적 서빙(organizer·exhibitor·contractor·ops), 공유 백엔드 jar × N(systemd, 무상태 수평 확장), 워커 데몬 × M.
- **관리 존(내부 전용)**: admin 백오피스(웹 전용, VPN/허용 IP + 2FA 강제).
- **데이터 존(최내곽)**: PostgreSQL+PostGIS(프라이머리+스탠바이)·읽기 복제·Redis HA·오브젝트 스토리지. **아웃바운드 없음(유출 경로 제거)**.
> 물리 서버·도메인·포트는 G2 게이트 확정 후 Phase E에서 실체화(개발 도메인은 `kintex.zioinfo.co.kr`, 포트 8021). 근거: architecture/system.md·network.md.
---
## 2. 애플리케이션 아키텍처(레이어·모듈·의존성)
### 2-1. 패키지 구조
루트 `com.zioinfo.kintex`. 횡단 계층 + 도메인 모듈:
- `common`(응답봉투·페이징·에러·감사 AOP·공통코드 캐시 — **무의존**), `config`(Security·WebSocket·Redis·MyBatis), `auth`(JWT·RBAC·2FA/OTP·가드), `rules`(규정·요율 룰셋 로딩·평가), `system`(시스템관리), `work`(공통 업무), `module`(도메인 m2~m5 등).
- 리소스: `application.yml`(env 플레이스홀더), `mybatis/mapper/**/*.xml`(공간 SQL `ST_*`), `rulesets/`(compliance·rates JSON = 데이터).
### 2-2. 레이어링 표준
| 계층 | 책임 | 금지 |
|---|---|---|
| Controller | HTTP 바인딩·`@Valid`·RBAC 가드 호출·서비스 위임·`ApiResponse` 래핑 | 비즈니스 로직·SQL·트랜잭션·매퍼 직접 호출 |
| Service(interface+Impl) | 비즈니스 규칙·`@Transactional` 경계·룰 엔진 호출·매퍼 오케스트레이션·DTO 조립 | HTTP 타입 참조 |
| Mapper(MyBatis) | DB 접근·공간 SQL(ST_*) 바인딩 | 비즈니스 분기·DTO 조립 |
| DTO(record) / domain | 불변 전송 객체 / 순수 값객체(프레임워크 무의존) | — |
- 공간 연산은 매퍼 XML의 PostGIS SQL로 수행(서비스는 스칼라/GeoJSON 결과만 사용). 조회 `readOnly=true`, 버전 리소스는 낙관적 잠금(불일치=409).
### 2-3. 의존성 규칙(단방향·순환 금지)
- `module.mN``rules·auth·common`(횡단 허용). `common`**무의존**.
- **모듈 간 명시 방향만**: M3→M2, M4→M2, M5→M2·M3·M4, M15→M2·M3·M4·M5·M7, M9→M1·M4·M15, M13→M2, M14→M4·M10, M16→전 모듈(읽기), M12→M10·M17.
- 역방향 필요 시 직접 참조 금지 → **도메인 이벤트·공유 식별자로 디커플**(예: M15 낙찰→M9는 발주 링크). 모듈 간 결합은 서비스 인터페이스로만. 검증 = ArchUnit 빌드타임 아키텍처 테스트(역참조·순환 CI 차단).
- 백엔드는 **단일 공유 모듈러 모놀리스**(역할별로 쪼개지 않음) — 모듈 경계 + 의존 규칙으로 코드 레벨 강제, 노출 표면은 경로 접두 + RBAC로 분리.
> 근거: architecture/app.md.
---
## 3. 데이터 설계
### 3-1. 데이터 표준(명명·타입)
- 테이블/컬럼: PostgreSQL 무인용 소문자 `snake_case`, PK `<엔티티>_id`(도메인은 UUID), WISE 이식 테이블은 정본 PK 유지.
- 지오메트리 `geom`/`<용도>_geom`, 암호화 PII `<필드>_enc`, 코드 컬럼 `status`/`<의미>_code`, 시각 `*_at`(timestamptz, UTC 저장/Asia/Seoul 표시), 금액 `numeric`(KRW).
- **DTO(camelCase) ↔ 컬럼(snake_case)**: MyBatis `map-underscore-to-camel-case=true`.
- **응답 제외(불변)**: `*_enc`·`password_hash`·`otp_secret`·내부 IP/SSH·내부 식별자는 API 응답 완전 제외.
### 3-2. FK 최소화 원칙 (소유자 지시 2026-07-12)
- **원칙: "FK는 최소화, 공통코드로 관리."** 신규 도메인/테넌트 테이블은 물리 `FOREIGN KEY`를 두지 않고, 참조무결성은 **애플리케이션 레이어 검증 + `*_id` 소프트 참조 명명**으로 보장.
- **근거**: ① 멀티테넌트 복합 PK(`(tenant_id, id)`) 전환 시 단일 id FK가 `UNIQUE(id)` 보조제약을 강제하는 마찰 ② MyBatis가 조인·삭제 순서 제어 ③ 멱등 `ON CONFLICT` 시드 순서 자유 ④ 부스 재배치(replaceBooths) 시 자식 재지정 빈번(V3 `utility_order`·`render_job`이 이미 소프트 참조 = 정본 사례).
- **물리 FK 예외 화이트리스트(그 외 신규 FK 신설 금지)**: (W1) `common_code.grp_code`, (W2) `sys_menu.parent_id`(self), (W3) `sys_role_permission`, (W4) `sys_role_menu` — 전역 시스템/RBAC 무결성 필수분만.
- **소프트 참조 보완**: 임계 트랜잭션(낙찰·정산) 명시적 부모 존재 검증, 삭제는 서비스가 자식 선처리(soft-delete `use_yn='N'` 우선), 논리참조 인덱스 `(tenant_id, <ref>_id)`, 야간 고아 스캔(DQ 게이트).
### 3-3. 공통코드 표준
- 범주형 컬럼(상태·유형·심각도 등 열거 가능 소수값)은 자유문자열/DB enum이 아닌 `common_code_group`/`common_code`로 관리. 컬럼엔 **코드값(영문 상수)만 저장**, 표시명(한글)은 조인/캐시. **DB `ENUM` 물리타입 지양**.
- **코드 vs 마스터 경계**: 열거 가능 소수값=공통코드(BOOTH_TYPE·RENDER_STATUS·SHOT_PRESET 등), 다건·CRUD·버전 대상=마스터/룰셋(홀·요율·규정 룰셋·등록업체). 정본 = `docs/COMMON_CODES.md`.
- 확정 코드그룹 예: `EVENT_ROLE`(ORGANIZER·EXHIBITOR·CONTRACTOR·HALL_MANAGER), `PORTAL_ROLE`, `BOOTH_TYPE`(independent·assembled), `COMPLIANCE_SEVERITY`(block·warn·pass), `RENDER_STATUS`(QUEUED·RUNNING·DONE·FAILED), `SHOT_PRESET`(S1~S7), `VERIFY_METHOD`(EMAIL·OTP).
### 3-4. 공간 데이터 표준
- **SRID `0`**(홀 로컬 평면 데카르트 좌표, 단위 미터). 지리좌표(4326) 아님, `geometry`(geography 아님).
- 부스 = `Polygon`, 트렌치 = `Point`(+ 배선 run은 LineString), 배선 = `LineString`/`MultiLineString`. 전 `geom` **GiST 인덱스 필수**.
- 저장 전 `ST_IsValid`·닫힌 링·홀 내포 검증(실패 시 400). 미실측 트렌치는 `is_assumed=true` 플래그(PLANNING R4).
- 파생 연산: 최단 배선(라우팅), 통로 폭 검증(버퍼), 면적 정산(ST_Area)이 모두 SQL 수준에서 수행.
### 3-5. 핵심 엔티티(구현 스키마 기준)
**신원·마스터(V2)**
- `app_user`(id·email·display_name·`password_hash`(BCrypt, 응답 제외)·hall_manager·`otp_secret`(응답 제외)·failed_login_count·locked_until·status) — V7에서 otp_enabled·verify_method·role_code·dept_id·last_login_at 순증, V26에서 tenant_id 순증.
- `company`(id·registration_no(사업자번호, 초대 검증 키)·name·category(14분류)·region·**registered**(미등록 응찰 차단)).
- `event`·`hall`(exhibition_center·label·width/depth/height_m·area_m2·floor_load_t_per_m2·floor_type·booth_capacity·has_gas·is_assumed_trench)·`hall_assignment`.
- `event_member`(event_id·user_id·**role_code**(ORGANIZER·EXHIBITOR·CONTRACTOR·HALL_MANAGER)·booth_id·company_id) — 행사 단위 RBAC 멤버십.
- `booth_standard`(assembled·premium spec jsonb)·`master_data`(RATE·UTILITY_FEE·COMPLIANCE + ruleset_version).
**공간 코어(V3, P0)**
- `trench`(hall_id·geom(Point)·supply_power/water/air/network/gas·is_assumed).
- `hall_exit`(geom(Point)·clearance_m — 비상구 이격 버퍼).
- `layout`(event_id·hall_id·version·status, UNIQUE(event_id,hall_id,version)).
- `booth`(layout_id·booth_no·booth_type·geom(Polygon)·size_w/d_m·height_m·floor_load_t_per_m2·premium).
- `design_plan`(booth_id·version·status·spec jsonb, UNIQUE(booth_id,version)).
- `utility_order`(event_id·booth_id(소프트 참조)·quote jsonb·wiring(MultiLineString)·location_diagram_url).
- `render_job`(event_id·booth_id(소프트)·shot_preset(S1~S7)·status·image_url·schema_hash·model_version·error_message(요약만)).
**옥션 M15(V16)**
- `auction`(event_id·category·auction_type(reverse/rfq)·award_criteria(lowest/comprehensive)·weight_price/reputation/delivery·round·deadline·material_package_id·materials).
- `auction_invite`(등록업체만, UNIQUE(auction_id,company_id) — 미등록 원천 차단).
- `bid`(=Quotation: subtotal·vat·total·lead_days·valid_until·terms·`lines` jsonb·version·status, UNIQUE(auction_id,company_id) — 재응찰 시 버전 증가).
- `award`(옥션당 1건, bid_id·reason·awarded_by — 감사 추적)·`company_reputation`(rating·jobs_done·claim_rate).
- 봉인 입찰: 마감 전 경쟁 견적 비공개(서비스 레이어 강제 마스킹), 마감 후 발주자 전체 공개.
**관람·발주·정산·콘텐츠(V14~V24 등)**
- M6: `event_milestone`·`required_document`·`document_review_issue`(V14). M8: `dock`·`dock_reservation`(V15), `logistics_*`(V33).
- M10: `visitor_registration`·`lead`(V17). M12: `edm_campaign`·`sponsorship_package`·`sponsorship_sponsor`(V18). 공개: `exhibit_inquiry`(V20)·`visitor_guide`·`transport_info`(V43).
- M17 CMS: `cms_content`·`cms_translation`·`microsite`(V19), `cms_content_version`·`cms_media`(V21).
- M9: `invoice`·`invoice_payment`(V24), `invoice_refund`·`tax_invoice`(V34). M1 판매: `booth_sale`(V27). 결재: `approval`·`approval_line`·`approval_history`(V29).
**공통·시스템관리(V7·V8·V37)**
- `common_code_group`/`common_code`·`sys_menu`·`sys_role`/`sys_permission`/`sys_role_permission`·`sys_setting`·`audit_log`(actor·action·target·summary·ruleset_version·result·ip_hint)(V7).
- 공통 업무(V8): `worklog`·`schedule`·`message`/`message_recipient`·`notice`·`opinion`/`opinion_comment`·`meeting`/`meeting_action`·`report`·`notification`.
- 시스템 기초(V37): `dept`·`sys_program`·`sys_role_menu`·`sys_auth_policy`·`login_history`·`error_log`. 기타: `login_slide`(V11)·`holiday`(V41)·`sys_message`(V48)·`mail_log`(V36)·`webhook_*`(V35)·`ai_config`(V30).
**멀티테넌시(V26·V31)**
- `tenant`(id(slug: kintex·coex)·name·domain·status). KINTEX = 테넌트 #1 시드.
- 핵심 테이블 `tenant_id` 순증(`NOT NULL DEFAULT 'kintex'` 백필, 회귀 0) + `(tenant_id, …)` 선두 복합 인덱스. V31에서 테넌트 루트(event·hall·app_user)를 `PRIMARY KEY (tenant_id, id)` 복합 PK로 전환.
### 3-6. 마이그레이션 요약(Flyway V1~V49)
| 버전 | 주제 | 주요 산출 |
|---|---|---|
| V1 | 확장 | PostGIS·pgcrypto 등 extension |
| V2 | 신원·마스터 | app_user·company·event·hall·hall_assignment·event_member·booth_standard·master_data |
| V3 | 공간 코어(P0) | trench·hall_exit·layout·booth·design_plan·utility_order·render_job(+GiST) |
| V4·V5·V6 | 마스터 시드 | 홀 마스터·트렌치 그리드·마스터/데모 시드 |
| V7 | 시스템관리·보안 | common_code(_group)·sys_menu·sys_role/permission·sys_setting·audit_log |
| V8 | 공통 업무 | worklog·schedule·message·notice·opinion·meeting·report·notification |
| V9 | 공개 인증 | password_reset |
| V10 | 시드 | 10년치 행사 시드 |
| V11·V12·V13 | 부가 | login_slide·TOTP 2FA·카탈로그/대시보드 인덱스 |
| V14 | M6 | event_milestone·required_document·document_review_issue |
| V15 | M8 | dock·dock_reservation |
| V16 | M15 옥션 | auction·auction_invite·bid·award·company_reputation(+시드) |
| V17 | M10 | visitor_registration·lead |
| V18 | M12 | edm_campaign·sponsorship_package·sponsorship_sponsor |
| V19 | M17 CMS | cms_content·cms_translation·microsite |
| V20 | 공개 사이트 | exhibit_inquiry |
| V21 | 확장 | cms_content_version·cms_media(체크인·리드·CMS 확장) |
| V23 | 일정 보강 | schedule 확장 |
| V24 | M9 정산 | invoice·invoice_payment |
| V25 | M1 | 홀 배정 확장 |
| V26 | 멀티테넌시 1단계 | tenant + 핵심 테이블 tenant_id 순증·복합 인덱스 |
| V27 | 부스 판매 | booth_sale |
| V28 | 옥션 부가 | BOQ·옥션 수수료 |
| V29 | 결재 | approval·approval_line·approval_history |
| V30 | AI | ai_config |
| V31 | 테넌트 표준 | 테넌트 루트 복합 PK((tenant_id, id)) 전환 |
| V32 | 시드 | 2026 하반기 킨텍스 행사 |
| V33 | M8 확장 | logistics_equipment/request·rental_item/order·inbound |
| V34 | M9 확장 | invoice_refund·tax_invoice |
| V35 | 자동화 | webhook_subscription·delivery·inbound·edm_followup_log |
| V36 | 메일 | mail_log |
| V37 | 시스템 기초 | dept·sys_program·sys_role_menu·sys_auth_policy·login_history·error_log |
| V38·V39 | 데모 | full·realism 데모 시드 |
| V41·V42 | 마스터/데모 | holiday(2050)·대량 데모 시드 |
| V43 | 관람 가이드 | visitor_guide·transport_info + `v_event_calendar`·`v_event_monthly_summary`(뷰) |
| V44 | 성능 | 성능 인덱스 |
| V45·V46·V47 | 부가/데모 | 프로필 사진·포스터 로컬 에셋·라이브 행사 폐루프 시드 |
| V48 | 메시지 | sys_message(템플릿) |
| V49 | 데모 | 빈 테이블 데모 시드 |
> 멱등(`IF NOT EXISTS`·`ON CONFLICT`) + 비파괴 순증 원칙. V22·V40은 결번.
### 3-7. VIEW/MVIEW/배치 전략
- **`v_*` VIEW**: 실시간 파생·경량 조인·상태 산출(예 `v_event_calendar` — 날짜→UPCOMING/ONGOING/ENDED 산출로 별도 상태 컬럼 불필요). tenant_id 필터·민감 컬럼 미노출.
- **`mv_*` MATERIALIZED VIEW**: 무겁고 실시간성 낮은 집계(BI KPI·가동률·리드/ROI). 새로고침 전략 명시(야간 배치·`REFRESH ... CONCURRENTLY`·mview 인덱스).
- **BI 데이터마트(M16)**: 스타 스키마 `FACT_BOOKING/SETTLEMENT/UTILITY/AUCTION/VISITOR` + `DIM_DATE/HALL/EVENT/EXHIBITOR` + `KPI_SNAPSHOT`(built_at 각인). 별도 `mart` 스키마, 야간 ETL 또는 읽기 복제. **운영 DB 직조회 금지**.
- **배치 카탈로그(J1~J9)**: mview 새로고침·KPI 스냅샷·마감 알림/할일·EDM/옥션 통지·세션 만료 정리·고아 검출(DQ)·PII 보존/파기(행사종료+1년)·행사 crawl·정산 롤업. 공통 요건: 멱등·분산락·실패 격리·감사·테넌트 스코프.
> 근거: architecture/data.md.
---
## 4. API 설계
### 4-1. 표준
- 베이스 `/api`. 공개=`/api/public/**`, 워커=`/api/internal/**`, 행사 스코프=`/api/events/{eventId}/…`, 플랫폼 관리=`/api/admin/**`(hasRole ADMIN), 인증=`/api/auth/**`. 비-CRUD 액션은 하위 동사 세그먼트(POST).
- 응답 봉투 `ApiResponse<T>`{success·data·error{code,message}}, 목록 `PageResponse<T>`{items·page·size·total}.
- **ErrorCode→HTTP**: VALIDATION 400 · UNAUTHORIZED 401 · FORBIDDEN 403 · NOT_FOUND 404 · CONFLICT 409 · **COMPLIANCE_BLOCKED 422** · **RENDER_QUOTA_EXCEEDED 429** · **NOT_REGISTERED_COMPANY 403** · NOT_IMPLEMENTED 501 · INTERNAL 500.
### 4-2. 도메인별 주요 엔드포인트(실제 컨트롤러 기준)
| 도메인 | 베이스 경로 | 컨트롤러 |
|---|---|---|
| 인증(로그인·2FA·비번찾기·프로필사진·앱무결성) | `/api/auth`, `/api/auth/app-integrity` | AuthController·PublicAuthController·TwoFactorController·ProfilePhotoController·AppIntegrityController |
| M2 플로어플랜 | `/api/events/{eventId}/halls/{hallId}/layout` | FloorplanController |
| M3 부스 설계 | `/api/events/{eventId}/booths/{boothId}/design` | DesignController |
| M4 유틸리티 | `/api/events/{eventId}/booths/{boothId}/utility` | UtilityController |
| M5 렌더잡 | `/api/events/{eventId}`(render), `/api/internal/render`(워커 콜백) | RenderJobController·RenderWorkerCallbackController |
| M15 옥션 | `/api/auctions`, `/api/contractor` | AuctionController·ContractorController |
| M1 홀 배정·부스 판매 | `/api/halls`, `/api/booth-sales`, `/api/events`(catalog) | HallAssignController·BoothSalesController·EventCatalogController |
| M6 서류·마일스톤 | `/api/events/{eventId}`(document) | DocumentController |
| M8 물류 | `/api/events/{eventId}`, `/api/events/{eventId}/logistics` | LogisticsController·LogisticsExtController |
| M9 정산 | `/api/settlement` | SettlementController |
| M10 관람객·리드 | `/api/visitors` 등(VisitorController) | VisitorController |
| M12 마케팅 | `/api/events/{eventId}`(marketing) | MarketingController |
| M16 BI·분석 | `/api/analytics`, `/api/events/{eventId}/analytics`, `/api/events/{eventId}/dashboard`, `/api/admin/dashboard` | AnalyticsController·AnalyticsOverviewController·DashboardController·AdminDashboardController |
| M17 CMS·마이크로사이트 | `/api/cms/contents`, `/api/cms/media`, `/api/exhibitors/{exhibitorId}/microsite` | CmsContentController·CmsMediaController·MicrositeController |
| 공개 사이트·가이드·AI 도우미 | `/api/public`, `/api/public/cms`, `/api/public/microsites`, `/api/public/ai` | PublicSiteController·PublicGuideController·PublicCmsController·PublicMicrositeController·VisitorAssistantController |
| 현장 운영 | `/api/events/{eventId}/ops` | OpsController |
| 공통 업무(§5B) | `/api/work/{worklogs,schedules,messages,notices,opinions,search,meetings,reports,stats,approvals,notifications}` | Worklog·Schedule·Message·Notice·Opinion·Search·Meeting·Report·Stats·Approval·Notification Controller |
| 시스템관리(M18) | `/api/admin/{users,roles,roles/{role}/menus,programs,depts,companies,settings,audit,login-history,error-log,mail-config,notify-config,system-health,rulesets,auth-policy}` | SysUser·Role·RoleMenu·Program·Dept·Company·Setting·AuditLog·LoginHistory·ErrorLog·MailConfig·NotifyConfig·SystemHealth·AdminRuleset·AuthPolicy Controller |
| 테넌트 | `/api/admin/tenants` | TenantAdminController |
| AI 설정·NL 질의 | `/api/admin/ai`, `/api/ai` | AiConfigController·NlQueryController |
| 웹훅 | `/api/webhooks/in`, `/api/admin/webhooks` | WebhookInbound·WebhookAdmin Controller |
| 헬스 | `/health`, `/api/home` | HealthController·HomeController |
### 4-3. WebSocket(STOMP)·큐 계약
- `GET /ws`(SockJS), prefix `/topic`(서버→클라)·`/app`(클라→서버). 토픽 `/topic/render/{jobId}`·`/topic/auction/{auctionId}`·`/topic/events/{eventId}/notifications`·checkin. 페이로드는 REST DTO 재사용.
- Redis 큐: RenderJob 큐 `kintex:renderjob:queue`, 상태 `...:job:{jobId}`, 쿼터 `...:quota:{eventId}`. **성공 시에만 쿼터 차감**. G1 미승인 시에도 큐잉/상태는 동작(목/degraded).
> 근거: architecture/app.md, 실제 controller 매핑.
---
## 5. 화면 설계
### 5-1. 화면 총괄
design.md v2.4 집계 **총 89화면** — 웹 코어·도메인 52(SCR-01~51, SCR-HOME) + 관리자 10(SCR-A1~A10) + 공개사이트 8(SCR-P1~P8) + 3-트랙 관문 4(SCR-T0~T3) + 모바일 15(SCR-M1~M15). 역할 약칭: 주=주최자·참=참가업체·장=장치/공사업체·홀=킨텍스 직원·관=관리자·대=일반 대중/관람객.
### 5-2. 웹 코어·도메인(대표)
| SCR | 화면 | 모듈 | 역할 |
|---|---|---|---|
| SCR-01 | 로그인 & 행사 워크스페이스 선택 | 공통 | 전 |
| SCR-HOME | 로그인 후 메인 홈(랜딩 대시보드) | 공통·§5B·M1·M12 | 전 |
| SCR-02 | 주최자 대시보드(D-데이 마일스톤 타임라인) | M1·M6 | 주 |
| SCR-03 | 부스 배치 에디터(DnD+AI 자동배치+규정 오버레이) | M2 | 주 |
| SCR-04 | 배치안 비교(S7 홀 전경 조감) | M2·M5 | 주 |
| SCR-05 | 참가업체 부스 홈 | M3·M6 | 참 |
| SCR-06 | 부스 설계 스튜디오(스펙→나노바나나 4샷+before/after) | M3·M5 | 참·장 |
| SCR-07 | 유틸리티 배선 뷰(트렌치 오버레이+자동 견적) | M4 | 참·장 |
| SCR-08 | 유틸리티 신청 요약·위치표시도 | M4b | 참 |
| SCR-09 | 장치업체 규정 검증 리포트 | M3 | 장 |
| SCR-10/11 | 홀매니저 승인 큐 / 검수 상세 | M2·M3·M6 | 홀 |
| SCR-12 | 시각화 갤러리(S1~S7) | M5 | 전 |
| SCR-13 | 경영분석 대시보드 | M16 | 주·홀·관 |
| SCR-18~29 | 홀배정·부스판매·정산·서류·매칭·물류·옥션(개설·응찰·견적서·낙찰) | M1·M6·M7·M8·M9·M15 | 역할별 |
| SCR-30~37 | 관람객 등록·체크인·리드·EDM·스폰서십·CMS·마이크로사이트·다국어 | M10·M12·M17 | 역할별 |
| SCR-38 | 업체 수주 부스 대시보드 | 업체포털 | 장 |
| SCR-39~48 | §5B 공통 업무(업무일지·일정·쪽지·공지·의견·검색·회의록·업무보고·알림·마이페이지) | §5B | 전 |
| SCR-49~51 | 회원가입·비밀번호 재설정·2차 인증(OTP) 설정 | §5B-3 | 전 |
### 5-3. 관리자 백오피스(SCR-A*, M18·§5B-1, MDI 적용)
SCR-A1 사용자 · A2 역할·권한(RBAC) · A3 공통코드 · A4 메뉴 · A5 감사로그 · A6 시스템설정 · A7 마스터데이터(홀·요율·요금·부스표준·등록업체) · A8 규정 룰셋 버전 · A9 테넌트 온보딩(플랫폼 슈퍼관리자) · A10 AI 플랫폼 설정(AiConfig).
### 5-4. 공개 홍보 사이트(SCR-P*, MDI 비적용·SEO/다국어)
SCR-P1 홈 · P2 행사 상세 · P3 공개 인터랙티브 플로어플랜 · P4 관람객 사전등록 · P5 마이크로사이트 공개 뷰 · P6 참가/부스 문의 · P7 입장권 예매(티켓팅) · P8 예매 확인·취소.
### 5-5. 3-트랙 IA(§3C, SCR-T*)
코엑스 3-사이트 IA를 킨텍스 단일 도메인 위에 **visitor / business / agency** 3-트랙 얕은 진입 레이어로 정형화. 신규 라우트 4개(`/`=SCR-T0 관문, `/visitor`=T1, `/business`=T2, `/agency`=T3)만 추가하고 기존 딥라우트·MDI·티켓 공개 라우트는 불변(회귀 0).
- **SCR-T0 관문**(`/`, 미인증): 제품 히어로 + 3-트랙 대형 분기 카드(관람 1순위) + 행사 하이라이트 레일.
- **SCR-T1 visitor**: 세련된 마케팅 홈페이지로 격상, 중심 = 대화형 AI 관람 도우미(히어로 정중앙 자연어 입력창 + 예시 질문칩). 라이트/다크 토글(공개 예외).
- **SCR-T2 business**: B2B 실무 톤, 주최자(홀 임대)/참가업체(부스 신청) 2-분기 가치 제안 + 임대 절차 스텝 → 로그인 CTA.
- **SCR-T3 agency**: 옥션/역경매 차별점 강조, 등록업체 안내(739개사·14분류·미등록 시공 엄금) + 진행 중 공사 옥션 공고 리스트 → 등록업체 로그인 CTA.
- **공개 셸**: PublicShell + 상단 3-트랙 전환 탭(TrackSwitcher), 좌측 "KINTEX AI 전시·행사시스템" 워드마크, 우측 언어(한/영/중/일)·로그인. 서브도메인 테넌트 컨텍스트 하 해당 테넌트 콘텐츠만 노출.
### 5-6. 역할 → 랜딩·메뉴 결정 규칙(웹·모바일 공통)
- 미인증 기본 진입 = visitor 공개 관람 랜딩(SCR-T0). 인증 후 역할 매핑 트랙의 전용 메인으로 랜딩:
- primaryTrack 우선순위: 내부-admin > 내부-ops > business > agency > visitor.
- 랜딩: ADMIN→관리 메인(SCR-16) / MANAGER·HALL_MANAGER→운영 메인 / ORGANIZER·EXHIBITOR→비즈니스 메인(SCR-T2) / CONTRACTOR→에이전시 메인(SCR-T3) / VISITOR→관람객 메인(SCR-T1).
- 좌측 메뉴 그룹(GROUPS: ops·design·visitor·finance·work·system)은 트랙 필터로 노출/숨김. `system` 그룹은 ADMIN만.
- 모바일 하단 탭바는 역할별 4~5탭 구성.
### 5-7. 디자인 시스템(요약)
- **브랜드**: 킨텍스 CI 블루 계열 B2B 실무 톤. AI 생성물은 항상 "AI 생성/초안" 라벨.
- **컬러 토큰(kx)**: `primary-600 #0066B3`(주 브랜드), `primary-700 #004C86`, `ai-accent #6D4AFF`(AI 전용), success `#0E8A5F`·warning `#B45309`·error `#D92D20`, `canvas-bg #1C2536`(에디터 다크 서피스), 배선 전기 `#EF4444`/네트워크 `#3B82F6`/급배수 `#22C55E`. WCAG AA 이상.
- **타이포**: Pretendard(숫자·좌표 tabular), Display 28 / H1 24 / Body 14, 테이블 행 44px(모바일 48).
- **컴포넌트**: StatusBadge(작성중→제출→AI검토→승인→반려→시공→검수), DdayChip(D-3 warning), ViolationFlag(차단 빨강/경고 주황 + 도면 위 번호 핀 1:1), AI 라벨(보라 외곽선), 생성 이미지 상시 고지문(제거 불가). 라운드 버튼 4px/카드 8px(상한)/칩 pill. 12컬럼·컨테이너 max 1440px.
- **MDI 셸**: 좌측 사이드바(240px)+상단 문서 탭바+DocumentHost, ShellFooter 36px. 메뉴 클릭=탭 열기(라우트 이동 아님), 비활성 탭 상태 보존, 최대 12탭, 세션 `localStorage`(`kintex.mdi.{role}.{eventId}`). MDI 적용=관리자·주최자·참가·장치·홀매니저 / 비적용=공개(P·T)·모바일.
- **캘린더**: WISE(UIWS `CalendarView`) 패턴 문자 그대로 — 월 6주×7열, 기간 이벤트=가로 spanning bar, "+N" 팝업, 전시장 세그먼트 필터는 홀 마스터 센터 distinct 동적 옵션(하드코딩 금지).
- **반응형·다크모드**: 웹 1440px(설계·에디터·대시보드)/모바일 390px(조회·승인·현장). 다크모드 Phase 1 미지원(에디터 캔버스만 다크), 공개 마케팅 T1은 예외적 토글. 전 화면 풀블리드·공백 없는 반응형(전역 NFR).
### 5-8. 모바일(SCR-M*, Expo/RN 390px)
조회·승인·현장 전용(캔버스 편집 미제공, 뷰어+승인 액션만). 하단 탭 4개. SCR-M1 시공업체 현장 체크리스트 · M2 홀매니저 현장 검수 · M3 역할별 홈 · M4 리드캡처(배지 스캔) · M5 관람객 홈·배지/QR · M6 wayfinding · M7 플로어플랜·부스 검색 · M8 비즈매칭 · M9 세션·아젠다 · M10 반입 통행증·안전 · M11 알림센터 · M12 서류·승인 조회 · M13 옥션 순위 · M14 티켓 예매 · M15 내 티켓 지갑.
### 5-9. Stitch 연동
전 화면 Stitch 경유(프로젝트 `9385904003821333054`, 디자인 시스템 "Precision Enterprise AI"). 생성 화면 = `stitch_kintex_ai_system_architect/`(각 디렉터리 `code.html`·`screen.png`). design.md가 토큰·컴포넌트 권위(상충 시 design.md 우선). 신규 정의 SCR-HOME·SCR-T0~T3는 미생성(designer Stitch 의뢰 대상).
> 근거: design.md v2.4 §1~§4, PLANNING §2-3.
---
## 6. AI 설계(§8A — 사용성 & 토큰 최소화)
### 6-1. 사용성 표준(U1~U7)
전 화면 공통 인라인 AI 진입점(`AiAssistant`) + 예시 질문 칩 + 원탭 액션 + 다음 명령 제시(규칙 기반 우선) + **구조화 카드 + 근거 인용**(출처 레코드 링크, 근거 없으면 "모름" 폴백) + 대화 히스토리(요약 압축) + 접근성/다국어/모바일/음성. 관람객·업무 사용자가 단일 `AiAssistant` + 단일 `/ai/ask` 계약 공유, 노출 위치·칩·허용 액션만 역할/트랙으로 스코프.
### 6-2. 토큰 최소화 6원칙(P1~P6)
- **P1 결정론 우선 라우팅**: 사실 조회형(일정·교통·주차·마감일·요금·부스 위치·통계)은 DB/뷰가 직접 응답(LLM 미호출). LLM은 요약·추천·자연어 종합에만.
- **P2 소형모델 우선 티어링**: 온프레미스 소형(Ollama qwen3:1.7b/llama3.2:1b) → 난도·실패 시 Claude 승급(AiTextRouter).
- **P3 RAG 발췌**: top_k 제한·컬럼 프로젝션으로 짧은 컨텍스트만(전체 문서/테이블 주입 금지).
- **P4 캐싱**: 응답 캐시(의도+파라미터+tenant+locale, TTL)·프롬프트 프리픽스·기간 요약 재사용.
- **P5 출력 상한·구조화**: max_tokens 상한 + JSON/카드 구조화.
- **P6 집계는 SQL로**: 통계·랭킹·추이는 데이터마트 SQL이 산출, LLM은 설명·해석만.
### 6-3. 계약·측정
- **단일 계약** `POST /ai/ask`: 입력 `{question, contextRef, locale, history}`, 출력 `{answerCard, citations[], followups[], route(direct|small|escalated), usage(tokens·cached)}`.
- IntentRouter는 규칙 테이블(공통코드)로 관리, 사실조회 의도는 DB 리졸버 매핑, 미매핑만 LLM 경로. 캐시 키 `hash(intent+params+tenant_id+locale)`.
- AI 프로바이더: Claude 기본(`api.anthropic.com`, 키 env only) + AiTextRouter(실패 시 Ollama 폴백) + AiConfig 설정 화면(하드코딩 금지). 나노바나나(Gemini)는 별도 이미지 파이프라인(본 절 토큰 원칙 대상 아님, G1 게이트).
- **측정지표**(`ai_usage_log` 적재, tenant/모듈 비용 귀속): LLM 우회율 ≥40%·소형모델 처리율 ≥70%·캐시 적중률 ≥30%·평균 입력 ≤1500/출력 ≤400·근거 인용률 ≥95%(초기 가설).
> 근거: PLANNING §8A. 현행 visitor-assistant DB 근거 응답·AiTextRouter 폴백과 정합(표준화).
---
## 7. 인증·인가 설계
### 7-1. 현행(구현) — JWT + RBAC + 2FA
- **JWT(HS256)**: 클레임 sub·name·roles(eventId→역할)·hm(홀매니저)·plat(플랫폼 역할)·otp. STATELESS·CSRF disable. 공개(permitAll): `GET /health`·`POST /api/auth/login`·`/ws/**`·`/api/internal/render/callback`·(Phase D)`/api/public/**`.
- **이중 RBAC**: 플랫폼 역할(JWT `plat`+`hasRole`) × 행사 역할(JWT `roles`/`hm`+`EventAccessGuard.requireRole`). 열람=행사 멤버 or 홀매니저, 편집·액션=역할별. **등록업체 게이트(불변)**: CONTRACTOR 응찰은 등록업체 검증 필수(미등록=NOT_REGISTERED_COMPANY 403).
- **2차 인증(TOTP RFC6238)**: SHA1·30s·6자리·±1 윈도우. 최초 QR 등록, 마이페이지 재설정/해제, 관리자 OTP 초기화. 대상 = 업무 사용자 필수, 일반 관람객 미강제(승격 시 필수 전환).
- **로그인 실패 잠금**(failed_login_count·locked_until) + 관리자 해제. **admin 비밀번호** = env `ADMIN_PASSWORD_ENC`(AES-256-GCM) + 별도 키파일 주입, 기동 시 BCrypt 재시드(`admin123` 하드코딩 금지).
- 민감 컬럼(`password_hash`·`otp_secret`)은 API 응답에서 완전 제외.
### 7-2. 계정 체계(단일 통합 + 가입 트랙 분리)
- 계정/RBAC는 단일 통합, 가입 트랙만 분리: 업무 트랙(B2B, 승인/초대+2FA 필수) vs 관람객 트랙(B2C, 간편가입/게스트 예매·2FA 미강제). 관람객→바이어/참가 승격은 단일 계정 등급 상향(리드·비즈매칭·재방문 이력 연속성 유지).
### 7-3. Open SSO / HR 연동 (계획 — 설계 완료·미구현)
- **프로토콜**: OIDC(Authorization Code + PKCE) 우선 + SAML 2.0 옵션. **IdP = Keycloak 자체 호스팅(온프레미스, 내부망 IdP — 외부 API 금지 비해당)**.
- **기존 JWT+2FA와 공존(교체 아님)**: SSO는 로그인 게이트웨이만 교체, 성공 후 백엔드가 기존 `JwtService.issue()`로 동일 형태 앱 JWT를 브로커 발급 → 전 화면·RBAC·행사 스코프 불변. `/api/auth/oidc/callback`만 신설, 로컬 로그인 폴백 유지(IdP 장애 대비).
- **2FA**: IdP realm이 OTP 제공 시 위임(이중 2FA 방지), 미구성·로컬 폴백은 기존 TotpService 유지.
- **HR 연동**: 어댑터 `HrDirectoryClient`(+Mock)로 외부 HR API를 정본 조달 + 로컬 읽기 캐시 스냅샷(미가용 시 degraded). PII 최소(주민번호·연락처·급여 미수집), 삭제는 소프트. 전역 역할·hall_manager는 IdP 그룹/클레임 매핑, **행사 역할(event_member)은 로컬 권위 유지**.
- **3자 매핑**: SSO subject(`sub`) ↔ HR 사번(`empNo`) ↔ 로컬 `app_user.id`(연결 테이블 `user_identity`). 이메일 단독 매칭 금지, 충돌 시 자동 병합 금지·관리자 수동 링크. JIT 프로비저닝(첫 SSO 로그인 시 password_hash 없이 생성).
- **이행**: 피처플래그(`sso.enabled`) 4단계(준비→coexist→pilot→cutover→정착), 각 단계 롤백 게이트. 신설 모델(`user_identity`·`hr_dept_snapshot`·`hr_emp_snapshot`·`hr_sync_log`·`dept.source`)은 DA/backend 인계 백로그(미구현).
> 근거: architecture/sso-hr-integration.md, PLANNING §5B-3, 실제 스키마.
---
## 8. 네트워크·보안영역 설계
### 8-1. 존 모델(4계층 + 관리 존)
| 존 | 구성 | 인바운드 | 아웃바운드 |
|---|---|---|---|
| 엣지(무신뢰) | CDN·WAF/DDoS | 인터넷 80/443 | DMZ LB |
| DMZ(공개) | 공개 LB(TLS 종단)·public SSR·visitor 게이트웨이·공개 API GW·PG 콜백 | 엣지에서만 | 내부망 API GW(제한), **데이터 존 직결 금지** |
| 내부망(인증·운영) | 내부 LB·SSO 게이트·내부 API GW·공유 백엔드 | DMZ(허용 API)·관리 존(IP/VPN) | 데이터 존·AI 워커·승인 아웃바운드 |
| AI 워커 존 | Redis 큐·나노바나나 워커·Ollama | 내부망(큐 소비만) | 데이터 존(OBJ), Gemini egress 단일 경로만 |
| 데이터 존(최내곽) | PostgreSQL+PostGIS·오브젝트 스토리지·BI 읽기 복제 | 내부망·AI 워커(서비스 계정만) | **없음(전면 차단)** |
| 관리 존 | 배스천·관측성·백업 | 운영자 VPN/허용 IP | 대상 존 SSH·수집 |
- **핵심 규칙**: DMZ → 데이터 존 직접 접근 절대 금지(내부망 백엔드 API 경유). 데이터 존 아웃바운드 없음.
### 8-2. 공개 vs 내부 백오피스 분리
- DMZ: 공개 홍보 사이트(`www.`/`expo.`, SSR·CDN·SEO·다국어·비인증·쓰기 없음), 관람객 앱(`visitor.`·쓰기 제한).
- 내부망(인증): `organizer.`·`exhibitor.`·`contractor.`(등록업체 검증). 내부망(운영): `ops.`(IP/VPN 제한). 관리: `admin.`(M18) = VPN/허용 IP allowlist + 2FA 강제 + 감사로그 전량 + 웹 전용.
- 공개(DMZ) 3원칙: 쓰기 없음 · 데이터 존 직결 불가 · 캐시/CDN 적극.
### 8-3. 방화벽·LB·TLS·아웃바운드
- 기본 정책 = DROP(default-deny). DB 포트(5432)·Redis·OBJ 인터넷 미노출. DMZ↔내부망은 HTTPS/API만.
- WAF: OWASP Top10, 업로드 확장자·MIME·크기 이중 검증, PG 콜백 IP allowlist+서명 검증, admin·옥션 레이트리밋 강화.
- LB: 공개 LB(DMZ) / 내부 LB(백엔드, 무상태 JWT 라운드로빈+헬스체크) / WebSocket 스티키. TLS는 LB 종단(HSTS·TLS1.2+), 존 간 mTLS 옵션.
- **아웃바운드 게이트(승인 4목적지)**: `api.anthropic.com`(승인, 실패 시 Ollama 폴백)·`generativelanguage.googleapis.com`(G1 미승인, 워커에서만)·PG 결제·SMTP. 그 외 전량 차단. 키 격리=네트워크 격리(Gemini 키=워커 존, Claude/PG 키=백엔드, 데이터 존 무아웃바운드).
### 8-4. 보안 불변(위반 = QA 반려)
① 스택트레이스 미노출(요약만) ② 민감정보(자격증명·PII) 응답 완전 제외 ③ `GEMINI_API_KEY` 백엔드 미취급(M5는 큐 발행까지만) ④ AI 이미지 항상 워터마크·고지 강제 ⑤ admin 비번 env 주입 ⑥ 크로스-테넌트 접근은 플랫폼 슈퍼관리자 전용 API(감사).
> 근거: architecture/network.md·app.md, CLAUDE.md 보안 제약.
---
## 9. 비기능 요구사항(NFR)
### 9-1. 성능·용량
- 플로어플랜 3안 생성 수 분 내, 배치·배선 상호작용 P95 < 2s, 부스 목록 P95 < 500ms, 이미지 단건 평균 ~40s(비동기·SLA 대상 아님), 공개 캐시 히트 P95 < 200ms, 옥션 순위 갱신 < 1s.
- 대형 행사 3,000~5,000부스(홀당 200~600). **Hikari max = `${DB_POOL_MAX:3}` + PgBouncer 권고**(공유 PG 포화 방지).
### 9-2. 가용성
- 코어 인증·설계·조회 경로 HA SLO 99.5%(성수기 99.9% 지향). 백엔드 무상태 수평 확장(세션·순위·타이머·쿼터·캐시 Redis 외부화), Redis HA(Sentinel/Cluster+AOF), PG 프라이머리+읽기 복제(BI·공개조회 오프로드).
### 9-3. 기술 표준(핀 버전)
- 백엔드: Java 17 · Spring Boot 3.2.5 · Gradle · **MyBatis 3.0.3**(JPA 금지, `@MapperScan(annotationClass=Mapper.class)`) · jjwt 0.12.5 · UTF-8 강제.
- 프론트: React 18.3.1 · Vite 5.4.8 · TypeScript 5.6.2(strict) · react-router-dom 6 · @tanstack/react-query 5 · zustand 4(Redux 금지). 빌드 `tsc -b && vite build`(타입 에러=빌드 실패).
- 워커: Python 3.11+ · google-genai · `gemini-3.1-flash-image-preview`. 실 호출은 `NANOBANANA_LIVE=1`+`GEMINI_API_KEY` 동시 충족 시만(기본 목/degraded).
- 데이터: PostgreSQL+PostGIS(`kintex_db`)·Redis·오브젝트 스토리지·Flyway.
- 관측성: Actuator+Micrometer 권고(`/actuator/health` 배포 게이트·`/metrics`). 로그 보안 불변(자격증명·IP·PII·스택트레이스 금지, `include-stacktrace:never`).
### 9-4. 접근성·국제화
- WCAG AA 이상, 차트 색 외 패턴/라벨 병기, KPI·표 aria-label, 키보드 포커스 링 `primary-600` 2px, 전 아이콘 선(stroke) SVG(이모지 금지), i18n 키 분리(한/영/중/일 로케일 숫자/통화/날짜 포맷), `prefers-reduced-motion` 준수.
> 근거: architecture/system.md·tech.md, design.md 전역 NFR.
---
> **후속 산출 명시**: 본 설계서와 개발계획서(`01_개발계획서.md`)는 개발 착수 시점 산출물이다. **사용자지침서·운영자지침서는 UI 정렬 안정화 이후 별도 산출**한다(deliverables 갱신 정책상 완성+QA 통과 후 최신 메뉴를 반영해야 하므로 이번 범위에서 제외).

Binary file not shown.

View File

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

View File

@ -499,6 +499,140 @@ export function IconShieldCheck(p: IconProps) {
);
}
/** ⚡ 번개 — 빠른 작업·즉시 실행. */
export function IconBolt(p: IconProps) {
return (
<Svg {...p}>
<path d="M13 2L4 14H11L10 22L20 9H13L13 2Z" {...SRJ} />
</Svg>
);
}
/** ⧗ 활동·최근(시계). */
export function IconClock(p: IconProps) {
return (
<Svg {...p}>
<circle cx="12" cy="12" r="9" {...S} />
<path d="M12 7V12L15 14.5" {...SRJ} />
</Svg>
);
}
/** ✉ 메시지·쪽지(봉투). */
export function IconMail(p: IconProps) {
return (
<Svg {...p}>
<rect x="3" y="5" width="18" height="14" rx="2" {...S} />
<path d="M4 7L12 13L20 7" {...SRJ} />
</Svg>
);
}
/** 🗨 의견·댓글(말풍선). */
export function IconChat(p: IconProps) {
return (
<Svg {...p}>
<path d="M21 11.5C21 15.6 16.97 19 12 19C10.9 19 9.85 18.83 8.9 18.53L4 20L5.4 16.1C4.53 14.77 4 13.2 4 11.5C4 7.4 8.03 4 12 4C16.97 4 21 7.4 21 11.5Z" {...SRJ} />
</Svg>
);
}
/** ⇄ 승인·결재(체크리스트). */
export function IconApproval(p: IconProps) {
return (
<Svg {...p}>
<path d="M9 5H7C5.9 5 5 5.9 5 7V19C5 20.1 5.9 21 7 21H17C18.1 21 19 20.1 19 19V7C19 5.9 18.1 5 17 5H15" {...SRJ} />
<rect x="9" y="3" width="6" height="4" rx="1" {...SRJ} />
<path d="M9 13L11 15L15 11" {...SRJ} />
</Svg>
);
}
/** ✎ 업무일지·기록(펜). */
export function IconEdit(p: IconProps) {
return (
<Svg {...p}>
<path d="M4 20H8L18.5 9.5C19.3 8.7 19.3 7.4 18.5 6.6L17.4 5.5C16.6 4.7 15.3 4.7 14.5 5.5L4 16V20Z" {...SRJ} />
<path d="M13.5 6.5L17.5 10.5" {...SR} />
</Svg>
);
}
/** ⚑ 마일스톤·깃발. */
export function IconFlag(p: IconProps) {
return (
<Svg {...p}>
<path d="M5 21V4" {...SR} />
<path d="M5 4H18L15 8L18 12H5" {...SRJ} />
</Svg>
);
}
/** ⛟ 물류·트럭. */
export function IconTruck(p: IconProps) {
return (
<Svg {...p}>
<path d="M3 6C3 5.4 3.4 5 4 5H14C14.6 5 15 5.4 15 6V16H3V6Z" {...SRJ} />
<path d="M15 9H18.5L21 12V16H15V9Z" {...SRJ} />
<circle cx="7" cy="18" r="2" {...S} />
<circle cx="17" cy="18" r="2" {...S} />
</Svg>
);
}
/** ◈ 태그·라벨. */
export function IconTag(p: IconProps) {
return (
<Svg {...p}>
<path d="M3 3H11L21 13L13 21L3 11V3Z" {...SRJ} />
<circle cx="7.5" cy="7.5" r="1.4" fill="currentColor" />
</Svg>
);
}
/** ◫ 목록·리스트. */
export function IconList(p: IconProps) {
return (
<Svg {...p}>
<path d="M8 6H21" {...SR} />
<path d="M8 12H21" {...SR} />
<path d="M8 18H21" {...SR} />
<circle cx="4" cy="6" r="1" fill="currentColor" />
<circle cx="4" cy="12" r="1" fill="currentColor" />
<circle cx="4" cy="18" r="1" fill="currentColor" />
</Svg>
);
}
/** ◔ 파이·상태분포. */
export function IconPie(p: IconProps) {
return (
<Svg {...p}>
<path d="M12 3V12H21C21 7 17 3 12 3Z" {...SRJ} />
<circle cx="12" cy="12" r="9" {...S} />
</Svg>
);
}
/** ⚲ 방문·핀(위치). */
export function IconMapPin(p: IconProps) {
return (
<Svg {...p}>
<path d="M12 22C12 22 19 15.5 19 9.5C19 5.9 15.9 3 12 3C8.1 3 5 5.9 5 9.5C5 15.5 12 22 12 22Z" {...SRJ} />
<circle cx="12" cy="9.5" r="2.5" {...S} />
</Svg>
);
}
/** ★ 별(하이라이트·추천). */
export function IconStar(p: IconProps) {
return (
<Svg {...p}>
<path d="M12 3L14.7 8.5L20.5 9.3L16.3 13.4L17.3 19.2L12 16.5L6.7 19.2L7.7 13.4L3.5 9.3L9.3 8.5L12 3Z" {...SRJ} />
</Svg>
);
}
/** ● 오프라인(연결 끊김). */
export function IconWifiOff(p: IconProps) {
return (

View File

@ -13,6 +13,7 @@ import {
} from 'recharts';
import { CHART } from '../chartColors';
import { EmptyState, ErrorState, Skeleton } from '../../components/ui/States';
import { IconClock, IconOperations } from '../../components/ui/icons';
import { adminApi } from '../../api/endpoints';
import { ApiRequestError } from '../../api/client';
import { StatusPill } from '../work/workShared';
@ -106,7 +107,7 @@ export function AdminDashboardPage() {
<div className="kx-admin__split">
<section className="kx-card kx-admin__chart-card" aria-label={t('admin.visitorTrend')}>
<div className="kx-card__head">
<h2>{t('admin.hourlyVisitors')}</h2>
<h2><IconClock className="kx-title-ic" size={18} />{t('admin.hourlyVisitors')}</h2>
</div>
<div className="kx-admin__chart">
{data.visitorTrend.length === 0 ? (
@ -130,7 +131,7 @@ export function AdminDashboardPage() {
<aside className="kx-admin__live" aria-label={t('admin.liveEvents')}>
<div className="kx-card__head">
<h2>{t('admin.liveEvents')}</h2>
<h2><IconOperations className="kx-title-ic" size={18} />{t('admin.liveEvents')}</h2>
</div>
{data.liveEvents.length === 0 ? (
<EmptyState title={t('admin.noLiveEvents')} />

View File

@ -5,6 +5,7 @@ import { ApiRequestError } from '../../api/client';
import type { LoginSlideDto } from '../../api/types';
import { Button } from '../../components/ui/Button';
import { EmptyState, ErrorState, Skeleton } from '../../components/ui/States';
import { IconImage, IconPlus } from '../../components/ui/icons';
import './admin.css';
/**
@ -125,7 +126,7 @@ export function LoginSlideAdminPage() {
{/* 새 슬라이드 추가 */}
<section className="kx-card kx-slideadm__form-card" aria-label={t('slide.addAria')}>
<div className="kx-card__head">
<h2>{t('slide.addTitle')}</h2>
<h2><IconPlus className="kx-title-ic" size={18} />{t('slide.addTitle')}</h2>
</div>
<form className="kx-slideadm__form" onSubmit={handleCreate}>
<div className="kx-slideadm__form-grid">
@ -190,6 +191,7 @@ export function LoginSlideAdminPage() {
<section className="kx-card kx-slideadm__list-card" aria-label={t('slide.listAria')}>
<div className="kx-card__head">
<h2>
<IconImage className="kx-title-ic" size={18} />
{t('slide.registered')}{' '}
{slides && (
<span className="kx-slideadm__count tnum">

View File

@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next';
import { Button } from '../../components/ui/Button';
import { AiLabel } from '../../components/ui/Badge';
import { EmptyState } from '../../components/ui/States';
import { IconAiBot } from '../../components/ui/icons';
import { ApiRequestError } from '../../api/client';
import { aiConsoleApi } from './aiConsoleApi';
import type { NlQueryResult, RulesChatResult } from './aiConsoleApi';
@ -26,7 +27,7 @@ export function AiConsolePanel() {
<section className="kx-card kx-aicon" aria-label={t('ai.console.title')}>
<div className="kx-card__head">
<div>
<h2>{t('ai.console.title')}</h2>
<h2><IconAiBot className="kx-title-ic" size={18} />{t('ai.console.title')}</h2>
<p className="kx-card__hint">{t('ai.console.subtitle')}</p>
</div>
<AiLabel>{t('ai.console.badge')}</AiLabel>

View File

@ -13,6 +13,7 @@ import {
import { AiLabel } from '../../components/ui/Badge';
import { Button } from '../../components/ui/Button';
import { EmptyState, ErrorState, Skeleton } from '../../components/ui/States';
import { IconAnalytics, IconPie, IconSettlement } from '../../components/ui/icons';
import { CHART } from '../chartColors';
import { analyticsApi } from '../../api/endpoints';
import { ApiRequestError } from '../../api/client';
@ -131,7 +132,7 @@ export function AnalyticsDashboardPage() {
<div className="kx-bi__split">
<section className="kx-card kx-bi__trend" aria-label={t('analytics.trendAria')}>
<div className="kx-card__head">
<h2>{t('analytics.trendTitle')}</h2>
<h2><IconAnalytics className="kx-title-ic" size={18} />{t('analytics.trendTitle')}</h2>
<AiLabel>{t('analytics.monthlyAgg')}</AiLabel>
</div>
<p className="kx-card__hint">{t('analytics.trendHint')}</p>
@ -167,7 +168,7 @@ export function AnalyticsDashboardPage() {
<section className="kx-card kx-bi__sectors" aria-label={t('analytics.sectorsAria')}>
<div className="kx-card__head">
<h2>{t('analytics.sectorsTitle')}</h2>
<h2><IconPie className="kx-title-ic" size={18} />{t('analytics.sectorsTitle')}</h2>
</div>
{data.sectors.length === 0 ? (
<EmptyState
@ -195,7 +196,7 @@ export function AnalyticsDashboardPage() {
{/* 성과 테이블(전폭) */}
<section className="kx-card kx-bi__table-card" aria-label={t('analytics.perfTable')}>
<div className="kx-card__head">
<h2>{isOperator ? t('analytics.pnlTitle') : t('analytics.exhibitorPerfTitle')}</h2>
<h2><IconSettlement className="kx-title-ic" size={18} />{isOperator ? t('analytics.pnlTitle') : t('analytics.exhibitorPerfTitle')}</h2>
{degraded && <span className="kx-bi__degraded">{t('analytics.sampleTag')}</span>}
</div>
<div className="kx-table-scroll">

View File

@ -14,6 +14,7 @@ import {
} from 'recharts';
import { AiLabel } from '../../components/ui/Badge';
import { EmptyState, Skeleton } from '../../components/ui/States';
import { IconAnalytics, IconGrid, IconSpark, IconTrendUp } from '../../components/ui/icons';
import { CHART } from '../chartColors';
import { ApiRequestError } from '../../api/client';
import {
@ -91,7 +92,7 @@ function OverviewBody({ data }: { data: AnalyticsOverview }) {
{/* 행사 간 매출 비교 */}
<section className="kx-card" aria-label={t('global.evComp')}>
<div className="kx-card__head">
<h2>{t('global.evComp')}</h2>
<h2><IconAnalytics className="kx-title-ic" size={18} />{t('global.evComp')}</h2>
<AiLabel>{t('global.top8')}</AiLabel>
</div>
<p className="kx-card__hint">{t('global.evCompHint')}</p>
@ -133,7 +134,7 @@ function OverviewBody({ data }: { data: AnalyticsOverview }) {
{/* 참가사 리텐션 */}
<section className="kx-card" aria-label={t('global.retention')}>
<div className="kx-card__head">
<h2>{t('global.retention')}</h2>
<h2><IconTrendUp className="kx-title-ic" size={18} />{t('global.retention')}</h2>
</div>
<p className="kx-card__hint">
{t('global.retentionCum')}{' '}
@ -163,7 +164,7 @@ function OverviewBody({ data }: { data: AnalyticsOverview }) {
{/* 홀 가동률 추이 */}
<section className="kx-card" aria-label={t('global.occupancy')}>
<div className="kx-card__head">
<h2>{t('global.occupancy')}</h2>
<h2><IconGrid className="kx-title-ic" size={18} />{t('global.occupancy')}</h2>
</div>
<p className="kx-card__hint">{t('global.occupancyHint')}</p>
{data.hallOccupancy.length === 0 ? (
@ -186,7 +187,7 @@ function OverviewBody({ data }: { data: AnalyticsOverview }) {
{/* 수요예측(추세) */}
<section className="kx-card" aria-label={t('global.forecast')}>
<div className="kx-card__head">
<h2>{t('global.forecastTitle')}</h2>
<h2><IconSpark className="kx-title-ic" size={18} />{t('global.forecastTitle')}</h2>
<AiLabel>{t('global.trendProjection')}</AiLabel>
</div>
<p className="kx-card__hint">{t('global.forecastHint')}</p>

View File

@ -10,7 +10,7 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { Button } from '../../components/ui/Button';
import { AiLabel } from '../../components/ui/Badge';
import { EmptyState, ErrorState, Skeleton } from '../../components/ui/States';
import { IconAiBot, IconCheckCircle, IconDownload, IconWarning } from '../../components/ui/icons';
import { IconAiBot, IconCheckCircle, IconDownload, IconList, IconWarning } from '../../components/ui/icons';
import { auctionApi, formatWon, type Quote } from './auctionApi';
import { errMessage, useToast } from './aucShared';
import './auction.css';
@ -133,7 +133,7 @@ export function AwardComparePage() {
{/* 비교 매트릭스 */}
<section className="kx-card" aria-label="견적 비교표">
<div className="kx-card__head">
<h2> </h2>
<h2><IconList className="kx-title-ic" size={18} /> </h2>
<AiLabel>AI </AiLabel>
</div>
<div className="kx-table-scroll">
@ -229,7 +229,7 @@ export function AwardComparePage() {
{/* AI 리스크 분석 */}
<section className="kx-card" aria-label="AI 리스크 분석">
<div className="kx-card__head">
<h2>AI · </h2>
<h2><IconAiBot className="kx-title-ic" size={18} />AI · </h2>
<AiLabel>AI </AiLabel>
</div>
<div className="kx-risk">

View File

@ -10,9 +10,12 @@ import { Button } from '../../components/ui/Button';
import { DdayChip } from '../../components/ui/Badge';
import { EmptyState, ErrorState, Skeleton } from '../../components/ui/States';
import {
IconBell,
IconCheckCircle,
IconDocument,
IconGrid,
IconList,
IconTag,
IconUsers,
IconWarning,
} from '../../components/ui/icons';
@ -78,7 +81,7 @@ export function ContractorBoothDashboardPage() {
{/* 좌 — 수주 부스 리스트 */}
<section className="kx-cbd__col" aria-label="수주 부스 리스트">
<div className="kx-card__head" style={{ marginBottom: 0 }}>
<h2 style={{ fontSize: 'var(--fs-h2)' }}> </h2>
<h2 style={{ fontSize: 'var(--fs-h2)' }}><IconList className="kx-title-ic" size={18} /> </h2>
</div>
{data.awardedBooths.length === 0 ? (
@ -101,7 +104,7 @@ export function ContractorBoothDashboardPage() {
<aside className="kx-cbd__col" aria-label="옥션·알림 위젯">
<div className="kx-cbd-widget">
<div className="kx-card__head">
<h2 style={{ fontSize: 'var(--fs-h3)' }}> </h2>
<h2 style={{ fontSize: 'var(--fs-h3)' }}><IconTag className="kx-title-ic" size={18} /> </h2>
</div>
<div className="kx-cbd-widget__rows">
{data.myAuctionBids.length === 0 ? (
@ -130,7 +133,7 @@ export function ContractorBoothDashboardPage() {
<div className="kx-cbd-widget">
<div className="kx-card__head">
<h2 style={{ fontSize: 'var(--fs-h3)' }}> </h2>
<h2 style={{ fontSize: 'var(--fs-h3)' }}><IconBell className="kx-title-ic" size={18} /> </h2>
</div>
<div className="kx-feed">
{data.feed.length === 0 ? (

View File

@ -5,7 +5,7 @@ import { useAuthStore } from '../../store/authStore';
import { AiLabel } from '../../components/ui/Badge';
import { AiImage } from '../../components/ui/AiImage';
import { EmptyState, ErrorState, Skeleton } from '../../components/ui/States';
import { IconCheck } from '../../components/ui/icons';
import { IconBell, IconCheck, IconExhibitors, IconImage } from '../../components/ui/icons';
import { dashboardApi } from '../../api/endpoints';
import { ApiRequestError } from '../../api/client';
import type { DashboardData } from '../../api/types';
@ -133,7 +133,7 @@ export function OrganizerDashboardPage() {
<div className="kx-dash__split">
<section className="kx-dash__table-wrap" aria-label={t('dashboard.exhibitorStatus')}>
<div className="kx-dash__section-head">
<h2>{t('dashboard.exhibitorStatus')}</h2>
<h2><IconExhibitors className="kx-title-ic" size={18} />{t('dashboard.exhibitorStatus')}</h2>
{degraded && <span className="kx-dash__degraded">{t('dashboard.sampleTag')}</span>}
</div>
{data.exhibitors.length === 0 ? (
@ -167,7 +167,7 @@ export function OrganizerDashboardPage() {
<aside className="kx-dash__aside">
<section className="kx-feed" aria-label={t('dashboard.feedAria')}>
<h2 className="kx-feed__title">{t('dashboard.feedTitle')}</h2>
<h2 className="kx-feed__title"><IconBell className="kx-title-ic" size={18} />{t('dashboard.feedTitle')}</h2>
{data.feed.length === 0 ? (
<EmptyState title={t('dashboard.feedEmpty')} />
) : (
@ -185,7 +185,7 @@ export function OrganizerDashboardPage() {
<section className="kx-dash__s7" aria-label={t('dashboard.aerialAria')}>
<div className="kx-dash__s7-head">
<h2>{t('dashboard.aerialTitle')}</h2>
<h2><IconImage className="kx-title-ic" size={18} />{t('dashboard.aerialTitle')}</h2>
<AiLabel>S7</AiLabel>
</div>
<AiImage

View File

@ -186,7 +186,7 @@ export function DocsMilestonePage({ onOpenAuthoring }: { onOpenAuthoring?: (docT
<section className="kx-card kx-doc__list" aria-label="신고서류 체크리스트">
<div className="kx-doc__list-head">
<div>
<h2> </h2>
<h2><IconDocument className="kx-title-ic" size={18} /> </h2>
<p> </p>
</div>
<ChecklistProgress docs={docs} />

View File

@ -11,7 +11,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query';
import { AiLabel } from '../../components/ui/Badge';
import { Button } from '../../components/ui/Button';
import { ErrorState, Skeleton } from '../../components/ui/States';
import { IconChevronDown, IconSpark, IconWarning } from '../../components/ui/icons';
import { IconChevronDown, IconDocument, IconSpark, IconWarning } from '../../components/ui/icons';
import { ApiRequestError } from '../../api/client';
import { useResolvedEventId } from '../../hooks/useResolvedEventId';
import { docsApi, type DocAction } from './docsApi';
@ -389,7 +389,7 @@ export function ReportAuthoringPage({ docType = '재해대처계획서' }: { doc
<div className="kx-rep__preview" aria-label="문서 미리보기">
<article className="kx-rep__a4">
<div className="kx-rep__a4-title">
<h2>{docType}</h2>
<h2><IconDocument className="kx-title-ic" size={18} />{docType}</h2>
</div>
<table className="kx-rep__table">
<tbody>

View File

@ -12,7 +12,7 @@ import { useTranslation } from 'react-i18next';
import { Button } from '../../components/ui/Button';
import { EmptyState, ErrorState, Skeleton } from '../../components/ui/States';
import { Offcanvas } from '../../components/ui/Offcanvas';
import { IconWarning } from '../../components/ui/icons';
import { IconGrid, IconSettlement, IconWarning } from '../../components/ui/icons';
import { ApiRequestError } from '../../api/client';
import {
hallAssignApi,
@ -204,7 +204,7 @@ export function HallAssignmentPage() {
{/* ── 배정 보드 ── */}
<section className="kx-card">
<div className="kx-card__head">
<h2>{t('hallassign.board')}</h2>
<h2><IconGrid className="kx-title-ic" size={18} />{t('hallassign.board')}</h2>
<span className="kx-card__hint">{t('hallassign.boardHint')}</span>
</div>
{assignQ.isLoading || hallsQ.isLoading ? (
@ -519,7 +519,7 @@ function QuotePanel({ halls }: { halls: Hall[] }) {
return (
<section className="kx-card">
<div className="kx-card__head">
<h2>{t('hallassign.autoQuote')}</h2>
<h2><IconSettlement className="kx-title-ic" size={18} />{t('hallassign.autoQuote')}</h2>
<span className="kx-card__hint">{t('hallassign.quoteHint')}</span>
</div>
<div className="kx-hall__quoteform">

View File

@ -12,12 +12,16 @@ import { EmptyState, ErrorState, Skeleton } from '../../components/ui/States';
import {
IconAiBot,
IconBell,
IconBolt,
IconCalendar,
IconCheckCircle,
IconChevronLeft,
IconChevronRight,
IconClock,
IconDocument,
IconExhibitors,
IconFloorplan,
IconGrid,
IconSearch,
IconSettlement,
IconSpark,
@ -479,7 +483,10 @@ function ShowcaseRail() {
return (
<section className="kx-home__showcase" aria-label={t('home.showcase.title')}>
<div className="kx-home__section-head">
<h2 className="kx-home__section-title">{t('home.showcase.title')}</h2>
<h2 className="kx-home__section-title">
<IconGrid className="kx-title-ic" size={18} />
{t('home.showcase.title')}
</h2>
<div className="kx-seg" role="tablist" aria-label={t('home.showcase.title')}>
{SHOWCASE_TABS.map((k) => (
<button
@ -687,7 +694,10 @@ function HomeCalendar() {
>
<IconChevronLeft size={16} />
</button>
<strong className="kx-home__cal-title tnum">{monthTitle}</strong>
<strong className="kx-home__cal-title tnum">
<IconCalendar className="kx-title-ic" size={16} />
{monthTitle}
</strong>
<button
type="button"
className="kx-home__cal-nav"
@ -812,7 +822,10 @@ function HomeCalendar() {
{/* 진행 중·예정 전시 리스트 */}
<div className="kx-home__cal-list">
<h3 className="kx-home__cal-list-title">{t('home.calendar.listTitle')}</h3>
<h3 className="kx-home__cal-list-title">
<IconGrid className="kx-title-ic" size={16} />
{t('home.calendar.listTitle')}
</h3>
{listed.length === 0 ? (
<EmptyState
title={t('home.calendar.listEmpty')}
@ -928,7 +941,10 @@ function TodoWidget() {
return (
<section className="kx-card kx-home__widget" aria-label={t('home.todo.title')}>
<div className="kx-home__widget-head">
<h2>{t('home.todo.title')}</h2>
<h2>
<IconCheckCircle className="kx-title-ic" size={18} />
{t('home.todo.title')}
</h2>
{data && data.total > 0 && <span className="kx-home__count-badge tnum">{data.total}</span>}
</div>
@ -1007,7 +1023,10 @@ function NoticeWidget() {
return (
<section className="kx-card kx-home__widget" aria-label={t('home.notice.title')}>
<div className="kx-home__widget-head">
<h2>{t('home.notice.title')}</h2>
<h2>
<IconBell className="kx-title-ic" size={18} />
{t('home.notice.title')}
</h2>
</div>
{query.isLoading ? (
<div className="kx-home__widget-body">
@ -1065,7 +1084,10 @@ function QuickActions() {
return (
<section className="kx-card kx-home__widget" aria-label={t('home.quick.title')}>
<div className="kx-home__widget-head">
<h2>{t('home.quick.title')}</h2>
<h2>
<IconBolt className="kx-title-ic" size={18} />
{t('home.quick.title')}
</h2>
</div>
<div className="kx-home__quick-grid">
{actions.map((a) => (
@ -1107,7 +1129,10 @@ function ActivityWidget() {
return (
<section className="kx-card kx-home__widget" aria-label={t('home.activity.title')}>
<div className="kx-home__widget-head">
<h2>{t('home.activity.title')}</h2>
<h2>
<IconClock className="kx-title-ic" size={18} />
{t('home.activity.title')}
</h2>
</div>
{query.isLoading ? (
<div className="kx-home__widget-body">
@ -1166,7 +1191,10 @@ function WorkspaceGrid() {
return (
<section className="kx-home__workspace" aria-label={t('home.workspace.title')}>
<div className="kx-home__section-head">
<h2 className="kx-home__section-title">{t('home.workspace.title')}</h2>
<h2 className="kx-home__section-title">
<IconGrid className="kx-title-ic" size={18} />
{t('home.workspace.title')}
</h2>
</div>
{workspaces.length === 0 ? (
<div className="kx-card kx-home__ws-empty">

View File

@ -4,7 +4,7 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { AiLabel } from '../../components/ui/Badge';
import { Button } from '../../components/ui/Button';
import { EmptyState, ErrorState, Skeleton } from '../../components/ui/States';
import { IconImage, IconPlus, IconSpark } from '../../components/ui/icons';
import { IconImage, IconMail, IconPlus, IconSpark } from '../../components/ui/icons';
import { ApiRequestError } from '../../api/client';
import { useResolvedEventId } from '../../hooks/useResolvedEventId';
import { CAMPAIGN_STATUS_LABEL, CAMPAIGNS, SEGMENTS, type CampaignStatus } from './sampleMarketing';
@ -201,7 +201,7 @@ export function EdmCampaignPage() {
<aside className="kx-mkt__builder" aria-label="캠페인 빌더">
<div className="kx-card">
<div className="kx-card__head">
<h2> </h2>
<h2><IconMail className="kx-title-ic" size={18} /> </h2>
<AiLabel>AI </AiLabel>
</div>

View File

@ -3,7 +3,7 @@ import { useQuery } from '@tanstack/react-query';
import { Button } from '../../components/ui/Button';
import { DdayChip } from '../../components/ui/Badge';
import { EmptyState, ErrorState, Skeleton } from '../../components/ui/States';
import { IconArrowRight, IconCheck, IconPlus } from '../../components/ui/icons';
import { IconArrowRight, IconCheck, IconCheckCircle, IconPlus, IconStar } from '../../components/ui/icons';
import { ApiRequestError } from '../../api/client';
import { useResolvedEventId } from '../../hooks/useResolvedEventId';
import {
@ -116,7 +116,7 @@ export function SponsorshipPage() {
<aside className="kx-mkt__sponsor-col" aria-label="스폰서 관리">
<div className="kx-card">
<div className="kx-card__head">
<h2> </h2>
<h2><IconStar className="kx-title-ic" size={18} /> </h2>
</div>
<ul className="kx-mkt__sponsors">
{sponsors.map((s) => (
@ -147,7 +147,7 @@ export function SponsorshipPage() {
{active && (
<div className="kx-card">
<div className="kx-card__head">
<h2>{active.name} </h2>
<h2><IconCheckCircle className="kx-title-ic" size={18} />{active.name} </h2>
</div>
<ul className="kx-mkt__fulfillment">
{active.fulfillment.map((f) => (

View File

@ -11,7 +11,7 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
import { AiLabel } from '../../components/ui/Badge';
import { EmptyState, ErrorState, Skeleton } from '../../components/ui/States';
import { IconSpark } from '../../components/ui/icons';
import { IconSpark, IconTruck } from '../../components/ui/icons';
import { ApiRequestError } from '../../api/client';
import { useResolvedEventId } from '../../hooks/useResolvedEventId';
import { logisticsApi, type Dock, type GridReservation } from './logisticsApi';
@ -274,7 +274,7 @@ function DockScheduleTab() {
{/* 우: 예약 폼 + AI */}
<aside className="kx-dock__aside">
<section className="kx-card">
<span className="kx-dock__panel-title"> </span>
<span className="kx-dock__panel-title"><IconTruck className="kx-title-ic" size={18} /> </span>
<div className="kx-dock__form">
{selected && (
<p className="kx-dock__slot-note">

View File

@ -7,6 +7,7 @@ import { useEffect, useMemo, useState } from 'react';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
import { EmptyState, ErrorState, Skeleton } from '../../components/ui/States';
import { IconList, IconTag, IconTruck } from '../../components/ui/icons';
import { ApiRequestError } from '../../api/client';
import {
logisticsExtApi,
@ -105,7 +106,7 @@ export function EquipmentTab({ eventId }: { eventId: string | null }) {
return (
<div className="kx-logi__split">
<section className="kx-card">
<span className="kx-dock__panel-title">{t('logistics.equipment.formTitle')}</span>
<span className="kx-dock__panel-title"><IconTruck className="kx-title-ic" size={18} />{t('logistics.equipment.formTitle')}</span>
<div className="kx-dock__form">
<label className="kx-dock__field">
<span className="kx-dock__label">{t('logistics.equipment.item')}</span>
@ -148,7 +149,7 @@ export function EquipmentTab({ eventId }: { eventId: string | null }) {
</section>
<section className="kx-card kx-logi__list">
<span className="kx-dock__panel-title">{t('logistics.equipment.listTitle')}</span>
<span className="kx-dock__panel-title"><IconList className="kx-title-ic" size={18} />{t('logistics.equipment.listTitle')}</span>
{rows.length === 0 ? (
<EmptyState title={t('logistics.equipment.empty')} description={t('logistics.equipment.emptyDesc')} />
) : (
@ -256,7 +257,7 @@ export function RentalTab({ eventId }: { eventId: string | null }) {
return (
<div className="kx-logi__split">
<section className="kx-card">
<span className="kx-dock__panel-title">{t('logistics.rental.formTitle')}</span>
<span className="kx-dock__panel-title"><IconTag className="kx-title-ic" size={18} />{t('logistics.rental.formTitle')}</span>
<div className="kx-dock__form">
<label className="kx-dock__field">
<span className="kx-dock__label">{t('logistics.rental.item')}</span>
@ -299,7 +300,7 @@ export function RentalTab({ eventId }: { eventId: string | null }) {
</section>
<section className="kx-card kx-logi__list">
<span className="kx-dock__panel-title">{t('logistics.rental.listTitle')}</span>
<span className="kx-dock__panel-title"><IconList className="kx-title-ic" size={18} />{t('logistics.rental.listTitle')}</span>
{rows.length === 0 ? (
<EmptyState title={t('logistics.rental.empty')} description={t('logistics.rental.emptyDesc')} />
) : (
@ -402,7 +403,7 @@ export function InboundTab({ eventId }: { eventId: string | null }) {
return (
<div className="kx-logi__split">
<section className="kx-card">
<span className="kx-dock__panel-title">{t('logistics.inbound.formTitle')}</span>
<span className="kx-dock__panel-title"><IconTruck className="kx-title-ic" size={18} />{t('logistics.inbound.formTitle')}</span>
<div className="kx-dock__form">
<label className="kx-dock__field">
<span className="kx-dock__label">{t('logistics.inbound.trackingNo')}</span>
@ -440,7 +441,7 @@ export function InboundTab({ eventId }: { eventId: string | null }) {
</section>
<section className="kx-card kx-logi__list">
<span className="kx-dock__panel-title">{t('logistics.inbound.listTitle')}</span>
<span className="kx-dock__panel-title"><IconList className="kx-title-ic" size={18} />{t('logistics.inbound.listTitle')}</span>
{rows.length === 0 ? (
<EmptyState title={t('logistics.inbound.empty')} description={t('logistics.inbound.emptyDesc')} />
) : (

View File

@ -2,6 +2,7 @@ import { useState } from 'react';
import { useQuery } from '@tanstack/react-query';
import { Trans, useTranslation } from 'react-i18next';
import { EmptyState, ErrorState, Skeleton } from '../../components/ui/States';
import { IconBolt, IconClock, IconGrid, IconOperations } from '../../components/ui/icons';
import { opsApiLocal, type OpsDataExt } from './opsApi';
import { ApiRequestError } from '../../api/client';
import { useResolvedEventId } from '../../hooks/useResolvedEventId';
@ -87,7 +88,7 @@ export function HallOperationsPage() {
{/* 히어로: 홀 혼잡(체크인 파생) */}
<section className="kx-card kx-ops__heat" aria-label={t('ops.hallStatus')}>
<div className="kx-card__head">
<h2>{t('ops.hallStatus')}</h2>
<h2><IconGrid className="kx-title-ic" size={18} />{t('ops.hallStatus')}</h2>
</div>
<span className="kx-ops__sensor-badge">
{hasPeople ? t('ops.checkinDerived') : t('ops.sensorNoneHalls')}
@ -130,7 +131,7 @@ export function HallOperationsPage() {
<aside className="kx-ops__aside">
<section className="kx-card" aria-label={t('ops.powerLoad')}>
<div className="kx-card__head">
<h2>{t('ops.powerLoad')}</h2>
<h2><IconBolt className="kx-title-ic" size={18} />{t('ops.powerLoad')}</h2>
{powerTotal != null && (
<span className="kx-ops__total"><Trans i18nKey="ops.totalKw" values={{ v: formatKw(powerTotal) }} components={[<strong className="tnum" key="s" />]} /></span>
)}
@ -161,7 +162,7 @@ export function HallOperationsPage() {
<section className="kx-card" aria-label={t('ops.hourlyCheckin')}>
<div className="kx-card__head">
<h2>{t('ops.hourlyCheckin')}</h2>
<h2><IconClock className="kx-title-ic" size={18} />{t('ops.hourlyCheckin')}</h2>
</div>
<span className="kx-ops__sensor-badge">{t('ops.hourlyAgg')}</span>
{series.length === 0 ? (
@ -190,7 +191,7 @@ export function HallOperationsPage() {
{/* 하단: 시설 센서(미연동) — 정직한 빈 상태 */}
<section className="kx-card kx-ops__parking" aria-label={t('ops.facilitySensor')}>
<div className="kx-card__head">
<h2>{t('ops.facilitySensorTitle')}</h2>
<h2><IconOperations className="kx-title-ic" size={18} />{t('ops.facilitySensorTitle')}</h2>
</div>
{!hasHvac && data.lighting.length === 0 && data.parking.length === 0 ? (
<EmptyState

View File

@ -14,6 +14,8 @@ import {
IconInfo,
IconCheck,
IconArrowRight,
IconBuyer,
IconTicket,
} from './publicIcons';
const REG_STEPS = ['apply', 'review', 'register', 'eligible'];
@ -62,7 +64,7 @@ export function AgencyTrackPage() {
<div className="kxp-wrap">
<div className="kxp-section__head">
<div>
<h2 className="kxp-section__title">{t('track.agency.registryTitle')}</h2>
<h2 className="kxp-section__title"><IconBuyer className="kx-title-ic" width={22} height={22} />{t('track.agency.registryTitle')}</h2>
<p className="kxp-section__sub">{t('track.agency.registrySub')}</p>
</div>
</div>
@ -99,7 +101,7 @@ export function AgencyTrackPage() {
<div className="kxp-wrap">
<div className="kxp-section__head">
<div>
<h2 className="kxp-section__title">{t('track.agency.auctionTitle')}</h2>
<h2 className="kxp-section__title"><IconTicket className="kx-title-ic" width={22} height={22} />{t('track.agency.auctionTitle')}</h2>
<p className="kxp-section__sub">{t('track.agency.auctionSub')}</p>
</div>
</div>
@ -128,7 +130,7 @@ export function AgencyTrackPage() {
<div className="kxp-wrap">
<div className="kxp-section__head">
<div>
<h2 className="kxp-section__title">{t('track.agency.safetyTitle')}</h2>
<h2 className="kxp-section__title"><IconShield className="kx-title-ic" width={22} height={22} />{t('track.agency.safetyTitle')}</h2>
<p className="kxp-section__sub">{t('track.agency.safetySub')}</p>
</div>
</div>

View File

@ -10,7 +10,7 @@ import { AiLabel } from '../../components/ui/Badge';
import { publicApi, type MonthlySummary, type PublicCalendarEvent } from './publicApi';
import { formatRange, dday, errorMessage } from './publicFormat';
import { ddayClass } from './trackShared';
import { IconChevronRight, IconCalendar, IconPin } from './publicIcons';
import { IconChevronRight, IconCalendar, IconPin, IconSparkles } from './publicIcons';
type View = 'year' | 'month';
const NOW = new Date();
@ -89,7 +89,7 @@ export function AiPlanningBriefing() {
<div className="kxp-wrap">
<div className="kxp-section__head kxp-briefing__head">
<div>
<h2 className="kxp-section__title">{t('track.visitor.briefing.title')}</h2>
<h2 className="kxp-section__title"><IconSparkles className="kx-title-ic" width={22} height={22} />{t('track.visitor.briefing.title')}</h2>
<p className="kxp-section__sub">{t('track.visitor.briefing.sub')}</p>
</div>
<div className="kxp-briefing__toggle" role="tablist" aria-label={t('track.visitor.briefing.title')}>

View File

@ -11,8 +11,10 @@ import {
IconVenue,
IconBuyer,
IconCheck,
IconCheckCircle,
IconSparkles,
IconArrowRight,
IconTicket,
} from './publicIcons';
/** 주최자·참가 가치 항목. ai=true 면 보라색 AI 칩 표기. */
@ -74,7 +76,7 @@ export function BusinessTrackPage() {
<div className="kxp-wrap">
<div className="kxp-section__head">
<div>
<h2 className="kxp-section__title">{t('track.business.valueTitle')}</h2>
<h2 className="kxp-section__title"><IconCheckCircle className="kx-title-ic" width={22} height={22} />{t('track.business.valueTitle')}</h2>
<p className="kxp-section__sub">{t('track.business.valueSub')}</p>
</div>
</div>
@ -116,7 +118,7 @@ export function BusinessTrackPage() {
<div className="kxp-wrap">
<div className="kxp-section__head">
<div>
<h2 className="kxp-section__title">{t('track.business.processTitle')}</h2>
<h2 className="kxp-section__title"><IconArrowRight className="kx-title-ic" width={22} height={22} />{t('track.business.processTitle')}</h2>
<p className="kxp-section__sub">{t('track.business.processSub')}</p>
</div>
</div>
@ -137,7 +139,7 @@ export function BusinessTrackPage() {
<div className="kxp-wrap">
<div className="kxp-section__head">
<div>
<h2 className="kxp-section__title">{t('track.business.rateTitle')}</h2>
<h2 className="kxp-section__title"><IconTicket className="kx-title-ic" width={22} height={22} />{t('track.business.rateTitle')}</h2>
<p className="kxp-section__sub">{t('track.business.rateSub')}</p>
</div>
</div>

View File

@ -37,6 +37,7 @@ import {
IconCheckCircle,
IconTicket,
IconVenue,
IconBell,
} from './publicIcons';
import type { ComponentType, SVGProps } from 'react';
@ -204,7 +205,7 @@ export function VisitorTrackPage() {
<div className="kxp-wrap">
<div className="kxp-section__head pub-reveal">
<div>
<h2 className="kxp-section__title">{t('track.visitor.aiTiles.title')}</h2>
<h2 className="kxp-section__title"><IconSparkles className="kx-title-ic" width={22} height={22} />{t('track.visitor.aiTiles.title')}</h2>
<p className="kxp-section__sub">{t('track.visitor.aiTiles.sub')}</p>
</div>
</div>
@ -237,7 +238,7 @@ export function VisitorTrackPage() {
<div className="kxp-wrap">
<div className="kxp-section__head pub-reveal">
<div>
<h2 className="kxp-section__title">{t('track.visitor.showcaseTitle')}</h2>
<h2 className="kxp-section__title"><IconVenue className="kx-title-ic" width={22} height={22} />{t('track.visitor.showcaseTitle')}</h2>
<p className="kxp-section__sub">{t('track.visitor.showcaseSub')}</p>
</div>
</div>
@ -251,6 +252,7 @@ export function VisitorTrackPage() {
<div className="kxp-section__head pub-reveal">
<div>
<h2 className="kxp-section__title">
<IconTarget className="kx-title-ic" width={22} height={22} />
{t('track.visitor.recommendTitle')}{' '}
<span className="kxp-inlineai">
<AiLabel>{t('track.visitor.recommendTag')}</AiLabel>
@ -281,7 +283,7 @@ export function VisitorTrackPage() {
<div className="kxp-wrap">
<div className="kxp-section__head pub-reveal">
<div>
<h2 className="kxp-section__title">{t('track.visitor.guideTitle')}</h2>
<h2 className="kxp-section__title"><IconMap className="kx-title-ic" width={22} height={22} />{t('track.visitor.guideTitle')}</h2>
<p className="kxp-section__sub">{t('track.visitor.guideSub')}</p>
</div>
</div>
@ -349,7 +351,7 @@ export function VisitorTrackPage() {
<div className="kxp-wrap">
<div className="kxp-section__head pub-reveal">
<div>
<h2 className="kxp-section__title">{t('track.visitor.journeyTitle')}</h2>
<h2 className="kxp-section__title"><IconArrowRight className="kx-title-ic" width={22} height={22} />{t('track.visitor.journeyTitle')}</h2>
<p className="kxp-section__sub">{t('track.visitor.journeySub')}</p>
</div>
</div>
@ -386,7 +388,7 @@ export function VisitorTrackPage() {
<div className="kxp-wrap">
<div className="kxp-section__head pub-reveal">
<div>
<h2 className="kxp-section__title">{t('track.visitor.noticeTitle')}</h2>
<h2 className="kxp-section__title"><IconBell className="kx-title-ic" width={22} height={22} />{t('track.visitor.noticeTitle')}</h2>
<p className="kxp-section__sub">{t('track.visitor.noticeSub')}</p>
</div>
</div>
@ -426,7 +428,7 @@ export function VisitorTrackPage() {
<IconVenue width={28} height={28} />
</span>
<div>
<h2 className="kxp-section__title">{t('track.visitor.venueTitle')}</h2>
<h2 className="kxp-section__title"><IconVenue className="kx-title-ic" width={22} height={22} />{t('track.visitor.venueTitle')}</h2>
<p className="kxp-section__sub">{t('track.visitor.venueSub')}</p>
</div>
</div>

View File

@ -445,7 +445,7 @@ function HallAvailability({ cursor }: { cursor: Date }) {
return (
<section className="kx-card" aria-label={t('schedule.availTitle')}>
<div className="kx-card__head">
<h2>{t('schedule.availMonth', { y: year, m: month + 1 })}</h2>
<h2><IconCalendar className="kx-title-ic" size={18} />{t('schedule.availMonth', { y: year, m: month + 1 })}</h2>
{data?.degraded && <span className="kx-bi__degraded">{t('schedule.offlineSample')}</span>}
</div>
{query.isLoading ? (
@ -501,7 +501,7 @@ function HallUtilization({ cursor }: { cursor: Date }) {
return (
<section className="kx-card" aria-label={t('schedule.hallUtil')}>
<div className="kx-card__head">
<h2>{t('schedule.hallUtilMonth', { y: year, m: month + 1 })}</h2>
<h2><IconGrid className="kx-title-ic" size={18} />{t('schedule.hallUtilMonth', { y: year, m: month + 1 })}</h2>
{degraded && <span className="kx-bi__degraded">{t('schedule.offlineSample')}</span>}
</div>
<ul className="kx-barlist">

View File

@ -5,6 +5,7 @@ import type { TFunction } from 'i18next';
import { Cell, Pie, PieChart, ResponsiveContainer, Tooltip } from 'recharts';
import { Button } from '../../components/ui/Button';
import { EmptyState, ErrorState, Skeleton } from '../../components/ui/States';
import { IconAnalytics, IconDocument, IconPie } from '../../components/ui/icons';
import { CHART } from '../chartColors';
import { useResolvedEventId } from '../../hooks/useResolvedEventId';
import { useAuthStore } from '../../store/authStore';
@ -136,7 +137,7 @@ export function SettlementDashboardPage() {
{/* 상태 분포 도넛 + 범례 */}
<section className="kx-card kx-settle__dist" aria-label={t('settlement.distTitle')}>
<div className="kx-card__head">
<h2>{t('settlement.distTitle')}</h2>
<h2><IconPie className="kx-title-ic" size={18} />{t('settlement.distTitle')}</h2>
</div>
<StatusDistribution buckets={dash.statusDistribution} />
<p className="kx-settle__paynote">{dash.paymentNote}</p>
@ -147,7 +148,7 @@ export function SettlementDashboardPage() {
{/* 청구서 목록 */}
<section className="kx-card kx-settle__list-card" aria-label={t('settlement.listTitle')}>
<div className="kx-card__head kx-settle__list-head">
<h2>{t('settlement.invoices')}</h2>
<h2><IconDocument className="kx-title-ic" size={18} />{t('settlement.invoices')}</h2>
<div className="kx-seg" role="tablist" aria-label={t('settlement.statusFilter')}>
{STATUS_FILTER_VALUES.map((v) => (
<button
@ -322,7 +323,7 @@ function ReportSection({ eventId }: { eventId: string }) {
return (
<section className="kx-card kx-settle__report" aria-label={t('settlement.reportTitle')}>
<div className="kx-card__head kx-settle__report-head">
<h2>{t('settlement.reportTitle')}</h2>
<h2><IconAnalytics className="kx-title-ic" size={18} />{t('settlement.reportTitle')}</h2>
<div className="kx-settle__report-actions">
<Button variant="secondary" onClick={() => setOpen((v) => !v)}>
{open ? t('settlement.reportHide') : t('settlement.reportShow')}

View File

@ -11,7 +11,7 @@ import {
} from 'recharts';
import { Button } from '../../components/ui/Button';
import { EmptyState, ErrorState, Skeleton } from '../../components/ui/States';
import { IconSearch, IconWifiOff, IconCheckCircle, IconGrid, IconUsers } from '../../components/ui/icons';
import { IconSearch, IconWifiOff, IconCheckCircle, IconClock, IconGrid, IconUsers } from '../../components/ui/icons';
import { ApiRequestError } from '../../api/client';
import { useResolvedEventId } from '../../hooks/useResolvedEventId';
import { CHART } from '../chartColors';
@ -317,7 +317,7 @@ export function CheckinDeskPage() {
</div>
<div className="kx-card kx-checkin__hours">
<div className="kx-card__head"><h2> </h2></div>
<div className="kx-card__head"><h2><IconClock className="kx-title-ic" size={18} /> </h2></div>
{chartData.length === 0 ? (
<EmptyState title="집계 없음" description="체크인이 시작되면 시간대별 추이가 표시됩니다." />
) : (

View File

@ -3,7 +3,7 @@ import { useQuery } from '@tanstack/react-query';
import { AiLabel } from '../../components/ui/Badge';
import { Button } from '../../components/ui/Button';
import { EmptyState, ErrorState, Skeleton } from '../../components/ui/States';
import { IconDownload, IconSpark } from '../../components/ui/icons';
import { IconClock, IconDownload, IconSpark } from '../../components/ui/icons';
import { ApiRequestError } from '../../api/client';
import { useResolvedEventId } from '../../hooks/useResolvedEventId';
import { useParams } from 'react-router-dom';
@ -326,7 +326,7 @@ function LeadDetail({
<div className="kx-card">
<div className="kx-card__head">
<h2> </h2>
<h2><IconClock className="kx-title-ic" size={18} /> </h2>
</div>
<ul className="kx-vis__activity">
{lead.activity.map((a, i) => (

View File

@ -14,7 +14,7 @@ import {
import { AiLabel } from '../../components/ui/Badge';
import { Button } from '../../components/ui/Button';
import { EmptyState, ErrorState, Skeleton } from '../../components/ui/States';
import { IconDownload, IconSpark } from '../../components/ui/icons';
import { IconDocument, IconDownload, IconSpark, IconTag, IconTrendUp, IconUsers } from '../../components/ui/icons';
import { ApiRequestError } from '../../api/client';
import { useResolvedEventId } from '../../hooks/useResolvedEventId';
import { CHART } from '../chartColors';
@ -116,7 +116,7 @@ export function VisitorRegistrationDashboardPage() {
<div className="kx-vis__split">
<section className="kx-card kx-vis__trend" aria-label="등록·체크인 추이">
<div className="kx-card__head">
<h2> · </h2>
<h2><IconTrendUp className="kx-title-ic" size={18} /> · </h2>
<ul className="kx-vis__legend" aria-hidden="true">
<li>
<span className="kx-dot" style={{ background: CHART.primary600 }} />
@ -184,7 +184,7 @@ export function VisitorRegistrationDashboardPage() {
<aside className="kx-vis__aside">
<section className="kx-card" aria-label="등록 폼 관리">
<div className="kx-card__head">
<h2> </h2>
<h2><IconDocument className="kx-title-ic" size={18} /> </h2>
</div>
<ul className="kx-vis__forms">
{summary.forms.map((f) => (
@ -210,7 +210,7 @@ export function VisitorRegistrationDashboardPage() {
<section className="kx-card" aria-label="배지 템플릿">
<div className="kx-card__head">
<h2> 릿</h2>
<h2><IconTag className="kx-title-ic" size={18} /> 릿</h2>
</div>
<div className="kx-vis__badge-preview">
<div className="kx-badgecard">
@ -235,7 +235,7 @@ export function VisitorRegistrationDashboardPage() {
{/* 등록자 명단 */}
<section className="kx-card kx-vis__table-card" aria-label="전체 신청자 명단">
<div className="kx-card__head">
<h2> </h2>
<h2><IconUsers className="kx-title-ic" size={18} /> </h2>
<span className="kx-vis__pii-note"> · </span>
</div>
<div className="kx-table-scroll">

View File

@ -12,7 +12,7 @@ import { useAuthStore } from '../../store/authStore';
import { Button } from '../../components/ui/Button';
import { RoleBadge } from '../../components/ui/Badge';
import { ErrorState, Skeleton } from '../../components/ui/States';
import { IconCheckCircle } from '../../components/ui/icons';
import { IconBell, IconCheckCircle, IconSettings, IconShieldCheck, IconUser } from '../../components/ui/icons';
import { useToast } from './workShared';
import './work.css';
@ -99,7 +99,7 @@ export function MyPage() {
{tab === 'profile' && (
<>
<div className="kx-card__head">
<h2></h2>
<h2><IconUser className="kx-title-ic" size={18} /></h2>
</div>
{meQ.isLoading ? (
<Skeleton height={80} />
@ -133,7 +133,7 @@ export function MyPage() {
{tab === 'security' && (
<>
<div className="kx-card__head">
<h2>2 (OTP)</h2>
<h2><IconShieldCheck className="kx-title-ic" size={18} />2 (OTP)</h2>
</div>
{otpQ.isLoading ? (
<Skeleton height={60} />
@ -169,7 +169,7 @@ export function MyPage() {
{tab === 'notify' && (
<>
<div className="kx-card__head">
<h2> </h2>
<h2><IconBell className="kx-title-ic" size={18} /> </h2>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
{(
@ -199,7 +199,7 @@ export function MyPage() {
{tab === 'theme' && (
<>
<div className="kx-card__head">
<h2>·</h2>
<h2><IconSettings className="kx-title-ic" size={18} />·</h2>
</div>
<div className="kx-formgrid">
<label className="kx-field">

View File

@ -10,6 +10,7 @@ import type { OpinionDto, OpinionSaveRequest } from '../../api/types';
import { useAuthStore } from '../../store/authStore';
import { Button } from '../../components/ui/Button';
import { EmptyState, ErrorState, Skeleton } from '../../components/ui/States';
import { IconChat, IconList } from '../../components/ui/icons';
import { StatusPill, errMessage, fmtDateTime, useToast } from './workShared';
import './work.css';
@ -85,7 +86,7 @@ export function OpinionPage() {
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
<section className="kx-card" aria-label="의견 접수">
<div className="kx-card__head">
<h2> </h2>
<h2><IconChat className="kx-title-ic" size={18} /> </h2>
</div>
<div className="kx-formgrid">
<label className="kx-field">
@ -136,7 +137,7 @@ export function OpinionPage() {
<section className="kx-card" aria-label="접수 목록">
<div className="kx-card__head">
<h2> </h2>
<h2><IconList className="kx-title-ic" size={18} /> </h2>
<select
className="kx-select"
value={statusFilter}

View File

@ -20,7 +20,7 @@ import { reportApi, workStatsApi } from '../../api/endpoints';
import { Button } from '../../components/ui/Button';
import { CHART } from '../chartColors';
import { EmptyState, ErrorState, Skeleton } from '../../components/ui/States';
import { IconDownload } from '../../components/ui/icons';
import { IconAnalytics, IconDocument, IconDownload, IconUsers } from '../../components/ui/icons';
import { errMessage, fmtDate } from './workShared';
import './work.css';
@ -133,7 +133,7 @@ export function ReportPage() {
{/* 유형별 차트 */}
<section className="kx-card" aria-label="유형별 업무량">
<div className="kx-card__head">
<h2> </h2>
<h2><IconAnalytics className="kx-title-ic" size={18} /> </h2>
</div>
{statsQ.isLoading ? (
<Skeleton height={260} />
@ -165,7 +165,7 @@ export function ReportPage() {
<div className="kx-split" style={{ gridTemplateColumns: '1fr 1fr' }}>
<section className="kx-card" aria-label="작성자별 집계">
<div className="kx-card__head">
<h2> </h2>
<h2><IconUsers className="kx-title-ic" size={18} /> </h2>
</div>
<div className="kx-table-scroll">
<table className="kx-list-table">
@ -207,7 +207,7 @@ export function ReportPage() {
<section className="kx-card" aria-label="최근 보고서">
<div className="kx-card__head">
<h2> </h2>
<h2><IconDocument className="kx-title-ic" size={18} /> </h2>
</div>
<div className="kx-table-scroll">
<table className="kx-list-table">

View File

@ -16,6 +16,7 @@ import {
IconDocument,
IconSearch,
IconSpark,
IconTag,
} from '../../components/ui/icons';
import { fmtDateTime, errMessage } from './workShared';
import './work.css';
@ -157,7 +158,7 @@ export function SearchPage() {
{/* 파셋 */}
<aside className="kx-card" aria-label="검색 필터">
<div className="kx-card__head">
<h2></h2>
<h2><IconTag className="kx-title-ic" size={18} /></h2>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
{tabs.map((t) => (

View File

@ -174,6 +174,39 @@ textarea {
font-variant-numeric: tabular-nums;
}
/*
* 중간 타이틀(섹션/카드 제목) 선행 아이콘 헬퍼 (2026-07 전수 스윕)
* - 타이틀 요소의 자식 <svg> 삽입 텍스트 컨텍스트/flex 컨텍스트 모두 대응.
* · 일반(블록/인라인) 제목: vertical-align 캡하이트 중앙 정렬 + margin 간격.
* · flex 제목(display:flex): align-items 중앙 정렬, flex:none 축소 방지.
* - 색은 토큰만(--color-primary-600 기본 · --muted neutral-500).
* - 페이지 대제목(h1) 대상 아님 중간 타이틀에만 부착한다.
* */
.kx-title-ic {
display: inline-block;
flex: none;
vertical-align: -0.16em;
margin-right: 0.42em;
color: var(--color-primary-600);
}
.kx-title-ic--muted {
color: var(--color-neutral-500);
}
/* flex 컨텍스트(제목 요소 자체가 flex)에서는 margin 대신 부모 gap 간격을 준다
* 그런 제목은 이미 gap 가지므로 아이콘 margin 이중 간격이 되지 않도록 소거. */
.kx-card__header .kx-title-ic,
.kx-card__head .kx-title-ic,
.kx-dock__panel-title .kx-title-ic {
margin-right: 0;
}
.kx-card__header h2,
.kx-card__header h3,
.kx-card__head h2 {
display: inline-flex;
align-items: center;
gap: var(--space-2);
}
.mono {
font-family: var(--font-mono);
font-size: var(--fs-mono);