From eccbeb13370a5a485add4490fd14aca8a500770a Mon Sep 17 00:00:00 2001 From: zio Date: Sat, 11 Jul 2026 18:10:01 +0900 Subject: [PATCH] =?UTF-8?q?feat(v2.0):=20Phase=20A=20=EC=95=84=ED=82=A4?= =?UTF-8?q?=ED=85=8D=EC=B2=98=20+=20WISE=20=EA=B0=9C=EB=B0=9C=EB=AC=B8?= =?UTF-8?q?=EC=84=9C=20+=20PM/PMO=20+=20DB=20=EA=B3=84=EC=B8=B5=20+=20?= =?UTF-8?q?=ED=94=84=EB=A1=A0=ED=8A=B8=20=EC=B0=A9=EC=88=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 거버넌스: kintex-pm·dev-pm·pmo 에이전트 - Phase A 아키텍처 5종: docs/architecture/{app,system,tech,data,network}.md (성문화·NFR·SRID0 공간표준·M16 스타스키마·보안영역) - WISE(UIWS) 참조 개발문서 6종: README·DEVELOPMENT_GUIDE·ENV_SETUP·COMMON_CODES·API_GUIDE·BUILD_DEPLOY - DB 계층: PostGIS 스키마 V1~V6(홀·트렌치 가정그리드·GiST) + MyBatis 매퍼 5종(ST_* 공간쿼리) + Flyway. gradlew build SUCCESS - 프론트 착수: React/Vite 스캐폴드 + design.md 토큰 + SCR-01 로그인·SCR-03 부스 에디터. tsc/vite build EXIT 0 - R-T1 수정: 나노바나나 워커 큐키 kintex:renderjob:queue 통일(백엔드 정합, silent no-op 방지) - 도메인/게이트: G2 해소(dev kintex.zioinfo.kr·prod kintex.wise.ai.kr), CLAUDE.md 로스터 20종 Co-Authored-By: Claude Opus 4.8 (1M context) --- .claude/agents/kintex-dev-pm.md | 18 + .claude/agents/kintex-pm.md | 18 + .claude/agents/kintex-pmo.md | 20 + CLAUDE.md | 4 +- _workspace/02_db_schema.md | 58 + _workspace/02_frontend_progress.md | 32 + docs/API_GUIDE.md | 97 + docs/BUILD_DEPLOY.md | 87 + docs/COMMON_CODES.md | 74 + docs/DEVELOPMENT_GUIDE.md | 130 ++ docs/ENV_SETUP.md | 127 ++ docs/README.md | 75 + docs/architecture/app.md | 401 ++++ docs/architecture/data.md | 696 ++++++ docs/architecture/network.md | 273 +++ docs/architecture/system.md | 355 +++ docs/architecture/tech.md | 324 +++ src/backend/build.gradle | 3 + .../src/main/resources/application.yml | 5 + .../db/migration/V1__enable_extensions.sql | 5 + .../db/migration/V2__identity_and_master.sql | 100 + .../db/migration/V3__spatial_core.sql | 105 + .../db/migration/V4__seed_hall_master.sql | 54 + .../db/migration/V5__seed_trench_grid.sql | 16 + .../db/migration/V6__seed_master_and_demo.sql | 50 + .../resources/mybatis/mapper/BoothMapper.xml | 110 + .../resources/mybatis/mapper/DesignMapper.xml | 52 + .../mybatis/mapper/RenderJobMapper.xml | 59 + .../resources/mybatis/mapper/UserMapper.xml | 39 + .../resources/mybatis/mapper/WiringMapper.xml | 59 + src/frontend/.env.example | 4 + src/frontend/.gitignore | 9 + src/frontend/README.md | 27 + src/frontend/index.html | 17 + src/frontend/package-lock.json | 2005 +++++++++++++++++ src/frontend/package.json | 32 + src/frontend/src/App.tsx | 38 + src/frontend/src/api/client.ts | 98 + src/frontend/src/api/endpoints.ts | 55 + src/frontend/src/api/types.ts | 165 ++ src/frontend/src/api/websocket.ts | 74 + .../src/components/layout/AppShell.tsx | 84 + .../src/components/layout/app-shell.css | 187 ++ src/frontend/src/components/ui/AiImage.tsx | 66 + src/frontend/src/components/ui/Badge.tsx | 61 + src/frontend/src/components/ui/Button.tsx | 30 + src/frontend/src/components/ui/States.tsx | 62 + src/frontend/src/components/ui/ai-image.css | 102 + src/frontend/src/components/ui/badge.css | 70 + src/frontend/src/components/ui/button.css | 69 + src/frontend/src/components/ui/states.css | 38 + src/frontend/src/main.tsx | 26 + .../screens/floorplan/AutoLayoutDialog.tsx | 242 ++ .../floorplan/BoothLayoutEditorPage.tsx | 297 +++ .../src/screens/floorplan/FloorplanCanvas.tsx | 135 ++ .../src/screens/floorplan/ValidationPanel.tsx | 78 + .../src/screens/floorplan/auto-layout.css | 119 + src/frontend/src/screens/floorplan/canvas.css | 68 + src/frontend/src/screens/floorplan/editor.css | 423 ++++ .../src/screens/floorplan/sampleLayout.ts | 66 + .../src/screens/login/InviteDialog.tsx | 125 + src/frontend/src/screens/login/LoginPage.tsx | 196 ++ src/frontend/src/screens/login/login.css | 328 +++ src/frontend/src/store/authStore.ts | 49 + src/frontend/src/styles/global.css | 90 + src/frontend/src/styles/tokens.css | 99 + src/frontend/src/vite-env.d.ts | 9 + src/frontend/tsconfig.app.json | 27 + src/frontend/tsconfig.json | 4 + src/frontend/tsconfig.node.json | 15 + src/frontend/vite.config.d.ts | 2 + src/frontend/vite.config.js | 26 + src/frontend/vite.config.ts | 27 + tools/nanobanana/worker.py | 2 +- 74 files changed, 9189 insertions(+), 3 deletions(-) create mode 100644 .claude/agents/kintex-dev-pm.md create mode 100644 .claude/agents/kintex-pm.md create mode 100644 .claude/agents/kintex-pmo.md create mode 100644 _workspace/02_db_schema.md create mode 100644 _workspace/02_frontend_progress.md create mode 100644 docs/API_GUIDE.md create mode 100644 docs/BUILD_DEPLOY.md create mode 100644 docs/COMMON_CODES.md create mode 100644 docs/DEVELOPMENT_GUIDE.md create mode 100644 docs/ENV_SETUP.md create mode 100644 docs/README.md create mode 100644 docs/architecture/app.md create mode 100644 docs/architecture/data.md create mode 100644 docs/architecture/network.md create mode 100644 docs/architecture/system.md create mode 100644 docs/architecture/tech.md create mode 100644 src/backend/src/main/resources/db/migration/V1__enable_extensions.sql create mode 100644 src/backend/src/main/resources/db/migration/V2__identity_and_master.sql create mode 100644 src/backend/src/main/resources/db/migration/V3__spatial_core.sql create mode 100644 src/backend/src/main/resources/db/migration/V4__seed_hall_master.sql create mode 100644 src/backend/src/main/resources/db/migration/V5__seed_trench_grid.sql create mode 100644 src/backend/src/main/resources/db/migration/V6__seed_master_and_demo.sql create mode 100644 src/backend/src/main/resources/mybatis/mapper/BoothMapper.xml create mode 100644 src/backend/src/main/resources/mybatis/mapper/DesignMapper.xml create mode 100644 src/backend/src/main/resources/mybatis/mapper/RenderJobMapper.xml create mode 100644 src/backend/src/main/resources/mybatis/mapper/UserMapper.xml create mode 100644 src/backend/src/main/resources/mybatis/mapper/WiringMapper.xml create mode 100644 src/frontend/.env.example create mode 100644 src/frontend/.gitignore create mode 100644 src/frontend/README.md create mode 100644 src/frontend/index.html create mode 100644 src/frontend/package-lock.json create mode 100644 src/frontend/package.json create mode 100644 src/frontend/src/App.tsx create mode 100644 src/frontend/src/api/client.ts create mode 100644 src/frontend/src/api/endpoints.ts create mode 100644 src/frontend/src/api/types.ts create mode 100644 src/frontend/src/api/websocket.ts create mode 100644 src/frontend/src/components/layout/AppShell.tsx create mode 100644 src/frontend/src/components/layout/app-shell.css create mode 100644 src/frontend/src/components/ui/AiImage.tsx create mode 100644 src/frontend/src/components/ui/Badge.tsx create mode 100644 src/frontend/src/components/ui/Button.tsx create mode 100644 src/frontend/src/components/ui/States.tsx create mode 100644 src/frontend/src/components/ui/ai-image.css create mode 100644 src/frontend/src/components/ui/badge.css create mode 100644 src/frontend/src/components/ui/button.css create mode 100644 src/frontend/src/components/ui/states.css create mode 100644 src/frontend/src/main.tsx create mode 100644 src/frontend/src/screens/floorplan/AutoLayoutDialog.tsx create mode 100644 src/frontend/src/screens/floorplan/BoothLayoutEditorPage.tsx create mode 100644 src/frontend/src/screens/floorplan/FloorplanCanvas.tsx create mode 100644 src/frontend/src/screens/floorplan/ValidationPanel.tsx create mode 100644 src/frontend/src/screens/floorplan/auto-layout.css create mode 100644 src/frontend/src/screens/floorplan/canvas.css create mode 100644 src/frontend/src/screens/floorplan/editor.css create mode 100644 src/frontend/src/screens/floorplan/sampleLayout.ts create mode 100644 src/frontend/src/screens/login/InviteDialog.tsx create mode 100644 src/frontend/src/screens/login/LoginPage.tsx create mode 100644 src/frontend/src/screens/login/login.css create mode 100644 src/frontend/src/store/authStore.ts create mode 100644 src/frontend/src/styles/global.css create mode 100644 src/frontend/src/styles/tokens.css create mode 100644 src/frontend/src/vite-env.d.ts create mode 100644 src/frontend/tsconfig.app.json create mode 100644 src/frontend/tsconfig.json create mode 100644 src/frontend/tsconfig.node.json create mode 100644 src/frontend/vite.config.d.ts create mode 100644 src/frontend/vite.config.js create mode 100644 src/frontend/vite.config.ts diff --git a/.claude/agents/kintex-dev-pm.md b/.claude/agents/kintex-dev-pm.md new file mode 100644 index 0000000..2083565 --- /dev/null +++ b/.claude/agents/kintex-dev-pm.md @@ -0,0 +1,18 @@ +--- +name: kintex-dev-pm +description: 킨텍스 자동전시시스템 개발 PM. 개발 트랙의 WBS·개발 일정·스프린트·진척(번다운)·개발 리스크·에이전트 팀 조율·품질 게이트(QA 통과)를 관리할 때 사용한다. 다시 실행·업데이트·보완 포함. +tools: Read, Write, Edit, Glob, Grep +model: opus +--- + +당신은 킨텍스 자동전시시스템의 개발 PM이다. **개발 실행의 계획·추적·조율**을 책임진다(총괄은 kintex-pm, 실행 조율은 kintex-impl-orchestrator). + +## 책임 +- **개발 WBS**: IMPLEMENTATION_BACKLOG v2.0(Phase A~E·M2~M18·공통레이어)을 작업분해구조로 정련 — 작업·담당 에이전트·의존·산정·우선순위. +- **일정·스프린트**: Phase별 스프린트 계획·개발 일정·마일스톤 정렬(공통레이어 선행 §5B 준수). +- **진척 추적**: 에이전트 팀 산출물(`_workspace/`·`src/`) 기준 진척·번다운·병목 식별. +- **개발 리스크·품질 게이트**: 기술 리스크·경계면 이슈, kintex-qa 통과를 완료 기준으로 관리. + +## 산출 / 협업 +- `docs/pm/` 또는 `_workspace/`에 개발 WBS·스프린트 계획·진척 보고. kintex-pm(총괄)·kintex-pmo(표준)·오케스트레이터(실행)와 연계. +- 구현은 하지 않는다 — 개발 관리·조율. 재호출: WBS·진척 갱신. diff --git a/.claude/agents/kintex-pm.md b/.claude/agents/kintex-pm.md new file mode 100644 index 0000000..092a5ee --- /dev/null +++ b/.claude/agents/kintex-pm.md @@ -0,0 +1,18 @@ +--- +name: kintex-pm +description: 킨텍스 자동전시시스템 프로젝트 관리자(총괄 PM). 전체 프로젝트의 범위·일정·마일스톤·리스크·이해관계자·의사소통·보고를 총괄 관리하고, 선행 게이트(G1 나노바나나 승인·G2 배포서버)와 Phase 진행을 통제할 때 사용한다. 다시 실행·업데이트·보완 포함. +tools: Read, Write, Edit, Glob, Grep +model: opus +--- + +당신은 킨텍스 자동전시시스템의 총괄 프로젝트 관리자(PM)다. 개별 구현이 아니라 **프로젝트 전체의 성공(범위·일정·품질·리스크)** 을 책임진다. + +## 책임 +- **프로젝트 관리 계획서**: 범위(PLANNING v2.0 M2~M18+공통레이어)·마스터 일정(Phase A~E)·마일스톤·게이트(G1 Gemini 승인·G2 배포서버) 관리. +- **리스크·이슈 관리**: 리스크 대장(PLANNING §10 R1~R12 연계)·이슈 트래킹·완화책·에스컬레이션. +- **이해관계자·의사소통**: 소유자 승인 게이트 관리(외부 API·배포), 진척·의사결정 보고. +- **거버넌스**: kintex-impl-orchestrator(실행)·kintex-dev-pm(개발)·kintex-pmo(관리표준)와 연계 — PM은 계획·통제, 오케스트레이터는 실행. + +## 산출 / 협업 +- `docs/pm/` 또는 `_workspace/`에 프로젝트 관리 계획서·마스터 일정·리스크/이슈 대장·진척 보고. PLANNING(planner)·아키텍처(Phase A)·백로그와 정합. +- 구현은 하지 않는다 — 계획·통제·보고. 재호출: 기존 관리 산출물 갱신(진척·리스크 반영). diff --git a/.claude/agents/kintex-pmo.md b/.claude/agents/kintex-pmo.md new file mode 100644 index 0000000..6bc05c9 --- /dev/null +++ b/.claude/agents/kintex-pmo.md @@ -0,0 +1,20 @@ +--- +name: kintex-pmo +description: 킨텍스 자동전시시스템 PMO(Project Management Office). 프로젝트 관리 표준·방법론, 산출물 목록/형상관리, 품질보증(QA 거버넌스), 변경관리, 통합 진척 보고를 관리할 때 사용한다. 다시 실행·업데이트·보완 포함. +tools: Read, Write, Edit, Glob, Grep +model: opus +--- + +당신은 킨텍스 자동전시시스템의 PMO다. **프로젝트 관리 표준·품질·산출물 거버넌스**를 책임진다. + +## 책임 +- **관리 표준·방법론**: WBS·산정·검토 절차·회의체·보고 양식 표준. PM/개발PM이 이 표준을 사용. +- **산출물 목록(Deliverables Register)**: 정보화사업 표준 산출물(기획·설계·개발·시험·이관 단계별) 목록화·상태 추적 — PLANNING·design·architecture·backlog·API 계약·매뉴얼 등 매핑. +- **형상관리(SCM)**: git 저장소(zio/kintex)·버전·브랜치·릴리즈 형상관리 계획, 문서 버전 일관성. +- **품질보증(QA 거버넌스)**: kintex-qa 활동 표준·품질 게이트·결함 관리 정책. 보안 불변(자격증명·PII·워터마크·RBAC) 준수 감사. +- **변경관리**: 범위·요구 변경 대장(이번 세션처럼 요구 확장 시)·영향분석·승인 절차. +- **통합 진척 보고**: PM/개발PM 데이터 취합 → 경영진·소유자 보고. + +## 산출 / 협업 +- `docs/pmo/` 또는 `_workspace/`에 산출물 목록·형상관리 계획·품질보증 계획·변경관리 대장·통합 진척 보고. kintex-pm·kintex-dev-pm·kintex-qa와 연계. +- 구현은 하지 않는다 — 표준·거버넌스·감사. 재호출: 표준·대장·보고 갱신. diff --git a/CLAUDE.md b/CLAUDE.md index aa6ab3f..322f8c7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -69,9 +69,9 @@ GUARDiA 표준 프레임워크(Spring Boot 3.5 + React 19 + MyBatis + PostgreSQL **트리거:** 킨텍스 구현·부스 배치/설계/배선/시각화·옥션/입찰/견적서·관람객/등록/배지/리드·경영분석/BI·CMS/공개 홍보 사이트·관리자 백오피스·공통기능/시스템관리/2FA·UIWS/WISE 이식·역할별 웹/모바일·AI(Claude)·아키텍처(AA/SA/TA/DA/NA)·src 구현·배포·다시 실행·특정 모듈만 요청 시 `kintex-impl-orchestrator` 스킬을 사용하라. -**에이전트(전문 17 + 범용):** 아키텍트 kintex-aa·sa·ta·da·na / 공통 kintex-common-dev / 코어 kintex-backend-dev·frontend-dev·db-engineer / 도메인 kintex-bidding-dev·visitor-dev·cms-dev·bi-dev·admin-dev / AI·시각화 kintex-ai-dev·visualizer / 품질·배포 kintex-qa·devops-dev + planner·designer·reviewer +**에이전트(전문 20 + 범용):** 거버넌스 kintex-pm·dev-pm·pmo / 아키텍트 kintex-aa·sa·ta·da·na / 공통 kintex-common-dev / 코어 kintex-backend-dev·frontend-dev·db-engineer / 도메인 kintex-bidding-dev·visitor-dev·cms-dev·bi-dev·admin-dev / AI·시각화 kintex-ai-dev·visualizer / 품질·배포 kintex-qa·devops-dev + planner·designer·reviewer -**선행 게이트:** G1 나노바나나(Gemini) 외부 호출 승인(PLANNING R12) · G2 배포 대상 서버·포트(GUARDiA 인프라와 별개 도메인). +**선행 게이트:** **G2 해소** — 개발 `kintex.zioinfo.kr`(등록 완료 → 서버 101.79.17.164, GUARDiA 공유 인프라 재사용) / 운영 `kintex.wise.ai.kr`. nginx vhost·포트는 devops 배정. **잔여: G1** 나노바나나(Gemini) 외부 호출 승인(PLANNING R12) — M5 실호출 전. **확보 자산:** `docs/assets/floorplans/` — 홀별 평면도 JPG 15장 + CAD(제1전시장 "평면,트렌치.dwg" 포함) → PLANNING R4(트렌치·CAD) 해소. 평면도 입력 포맷 = **CAD(DWG) + JPG**. CAD zip은 gitignore(로컬 보존). diff --git a/_workspace/02_db_schema.md b/_workspace/02_db_schema.md new file mode 100644 index 0000000..fdaa362 --- /dev/null +++ b/_workspace/02_db_schema.md @@ -0,0 +1,58 @@ +# 킨텍스 — DB 스키마·매퍼 계약 (Phase C / B-0 DB) + +> 작성: kintex-db-engineer · 근거: PLANNING §7(ERD·마스터)·§8(공간 일원화), `_workspace/01_backend_contracts.md` §8 매퍼 인수 목록 +> 스택: PostgreSQL + PostGIS · Flyway 순번 마이그레이션 · MyBatis 매퍼 XML +> **대조 대상**: kintex-backend-dev(서비스 조립)·kintex-qa. 본 문서가 컬럼·타입·파라미터 계약의 단일 출처. + +## 1. 마이그레이션 (권위 = Flyway, `src/backend/src/main/resources/db/migration/`) + +| 순번 | 파일 | 내용 | +|---|---|---| +| V1 | `V1__enable_extensions.sql` | postgis · pgcrypto extension | +| V2 | `V2__identity_and_master.sql` | app_user · company · event · hall · hall_assignment · event_member · booth_standard · master_data | +| V3 | `V3__spatial_core.sql` | trench(POINT) · hall_exit(POINT) · layout · booth(POLYGON) · design_plan(jsonb) · utility_order(MULTILINESTRING) · render_job + GiST 인덱스 | +| V4 | `V4__seed_hall_master.sql` | 홀 11행(H1~H10·OUTDOOR) 멱등 + 비상구 가정 4개소/홀 | +| V5 | `V5__seed_trench_grid.sql` | 트렌치 **가정 그리드**(6m 격자, is_assumed=true) | +| V6 | `V6__seed_master_and_demo.sql` | 부스 표준·요율/요금/규정 참조·등록업체 샘플·데모 행사(e-2026-smf→H7) | + +- SRID **0** = 홀 로컬 평면 좌표계(미터). `ST_Area`→㎡, `ST_Distance`/`ST_Length`→m. +- 시드는 전부 멱등(ON CONFLICT). 재호출 시 **V7+ 순번으로 이어 추가**(기존 파괴 금지). +- `spring.flyway.baseline-on-migrate=true`. `FLYWAY_ENABLED=false`로 비활성 가능. + +## 2. 보안 (계약 §0-3 유지) +- `app_user.password_hash`(BCrypt)·`otp_secret`(암호화)는 **API 응답 절대 제외**. `findAuthByEmail`만 인증 검증용으로 반환, 그 외 조회는 select 금지. +- 시크릿 하드코딩 없음. 데모 시드에 로그인 사용자 미포함(사용자·비번은 B-1 인증 트랙 admin env 재시드). + +## 3. 매퍼 파라미터·반환 계약 (백엔드 조립 시 준수) + +### UserMapper +- `findAuthByEmail(email)` → `{userId, displayName, passwordHash, hallManager}` | null +- `findEventRoles(userId)` → `[{eventId, roleCode, eventName, startDate, endDate, hallLabel, dday}]` (워크스페이스 카드 + eventRoles 동시 조립 가능) + +### BoothMapper (M2 · PostGIS) +- `findLayout(eventId,hallId,version?)` → `{layoutId, version, name, status, updatedAt}` (version=null→최신) +- `findBooths(layoutId)` → `[{boothId, boothNo, type, polygonJson(GeoJSON), sizeW, sizeD, heightM, floorLoadTPerM2, assignedCompanyName, premium}]` +- `upsertLayout(params)` → layoutId. **params**: `{layoutId?, eventId, hallId, version, name, status}` +- `replaceBooths(layoutId, booths)`. **booths[i]**: `{boothId?, boothNo, type, wkt('POLYGON((x y,...))'), sizeW, sizeD, heightM, floorLoad, assignedCompanyName, premium}` (단일 CTE로 삭제→일괄 삽입, booths 비어있지 않아야 함) +- `sumSalesArea(layoutId)`→Double㎡ · `minAisleWidth(layoutId)`→Double m(10m내 인접쌍 최소이격) · `countExitsBlocked(layoutId,hallId)`→int +- 규정검증 metrics 키(01 계약 §3): `layout.min_aisle_width_m`, `layout.exits_blocked_count`, 부스별 `booth.height_m`/`booth.floor_load` 집계. + +### DesignMapper (M3) +- `findDesign(boothId,version?)` → `{designId, boothId, version, status, specJson(문자열), updatedAt}` — 백엔드가 specJson→DesignSpec 역직렬화 +- `upsertDesign(params)` → designId. **params**: `{designId?, boothId, version, status, specJson(JSON 문자열)}` +- `findEventIdByBooth(boothId)` → eventId (RBAC) + +### WiringMapper (M4 · PostGIS) +- `findNearestTrenches(hallId,boothId,kind)` → `[{trenchId, x, y, distanceM, assumed}]` (KNN `<->` 상위 5) +- `shortestPath(hallId,fromX,fromY,kind)` → `{lineJson(GeoJSON LineString), lengthM, assumed}` — 백엔드가 lineJson→coords 파싱 +- `isAssumedTrench(hallId)` → Boolean (WiringResult.assumedTrench 원천) +- kind: `power|network|plumbing|water|air|gas` → 트렌치 supply_* 컬럼 매핑. + +### RenderJobMapper (M5) +- `insertJob(params)`. **params**: `{jobId, eventId, boothId, shotPreset, status, imageUrl?, schemaHash?, modelVersion?, errorMessage?}` (ON CONFLICT DO NOTHING) +- `updateStatus(jobId,status,imageUrl,errorMessage)` · `findByBooth(boothId,shotPreset?)`→갤러리 목록 · `countSucceededByEvent(eventId)`→int(status='DONE', 쿼터 정본) + +## 4. 남은 TODO (경계·후속) +- **서비스 조립(backend-dev)**: 5개 ServiceImpl이 아직 `notImplemented` 반환. 위 계약대로 매퍼 호출 배선 시 M2~M5·인증 501 해소. +- **CAD 트렌치 실측 추출**: 현재 트렌치·비상구는 **가정 그리드**(is_assumed=true). `docs/assets/floorplans/cad/kintex1_cad_all_halls.zip`의 "평면,트렌치.dwg" 좌표 추출 → V7+ 마이그레이션으로 교체, hall.is_assumed_trench=false 갱신. +- booth 교체(replaceBooths)가 design_plan을 CASCADE 삭제 — 부스 ID 안정화(버전 간 유지)는 backend 배치 저장 로직에서 관리 필요. diff --git a/_workspace/02_frontend_progress.md b/_workspace/02_frontend_progress.md new file mode 100644 index 0000000..880b312 --- /dev/null +++ b/_workspace/02_frontend_progress.md @@ -0,0 +1,32 @@ +# 킨텍스 프론트엔드 — 진행 기록 (kintex-frontend-dev) + +> 작성: kintex-frontend-dev · 2026-07-11 · 근거: `docs/design.md` v1.1 · `_workspace/01_backend_contracts.md` v0.1 · Stitch(SCR-01·SCR-03) +> 범위: IMPLEMENTATION_BACKLOG B-0(프론트 스캐폴드) + C-C 일부(SCR-01) + C-M2 일부(SCR-03) + +## 완료 +- **스캐폴드**: `src/frontend` (React 18 + Vite 5 + TS). 디자인 토큰(design.md §1) → `src/styles/tokens.css` CSS 변수 단일 출처. 라우팅(react-router, 인증 가드), API 클라이언트(JWT 헤더·ApiResponse 봉투·오류코드), WebSocket(STOMP/SockJS, `/topic/render/{jobId}` 구독) 골격. +- **SCR-01** 로그인/워크스페이스 선택: 좌 55% 브랜드 패널 + 우 45% 로그인 카드 → 로그인 후 워크스페이스 카드 그리드. `login`·`workspaces`·`accept-invite` 배선. **미등록 장치업체 초대 차단**(`NOT_REGISTERED_COMPANY` → 차단 모달). +- **SCR-03** 부스 배치 에디터: 3열(좌 도구·중앙 SVG 다크캔버스·우 속성/검증) + 상단 툴바. **SVG 캔버스**(부스 폴리곤·트렌치 그리드·축척·선택·줌). `GET/validate/auto-generate` 배선. **AI 자동배치 1·2·3안 생성→선택/병합** 다이얼로그(S7 조감 RenderJob WebSocket 구독). +- **검증**: `tsc -b` EXIT 0, `vite build` EXIT 0(238 modules, rollup win32 크래시 없음). WCAG AA — 포커스 링·role/aria·switch·reduced-motion. + +## 경계면 이슈 (백엔드/계약 대조 — kintex-qa/backend 확인 요청) + +1. **[중] 위반 오버레이 좌표 부재** — `ComplianceReport.violations[]`에 `pin`·`label`·`measured`는 있으나 **도면 하이라이트 위치(좌표/boothId)가 없다**. design.md §1-4·SCR-03은 "도면 위 같은 색 하이라이트 + 번호 핀으로 목록과 1:1 연결"을 요구. 현재 위반 목록의 "위치 보기"는 캔버스 하이라이트로 연결 불가. **요청: Violation에 `polygon`/`boothId`/`anchor[x,y]` 중 하나 추가**(pin 번호와 캔버스 오버레이 매핑용). + +2. **[중] 홀 치수·트렌치 좌표 계약 부재** — 캔버스가 홀 크기(126×90m)와 트렌치 그리드를 하드코딩/가정 중. `LayoutDto`에 홀 dims·트렌치 좌표가 없다. 계약 §6 render의 `scene.hall.dims_m`엔 있음. **요청: `GET .../layout` 응답에 `hall:{dims_m, trenches?}` 포함** 또는 별도 `GET .../halls/{hallId}` 홀 마스터 엔드포인트(트렌치·기둥·비상구 포함). + +3. **[하] auto-generate → 편집 반영 경로 불명** — `AutoLayoutOption`은 `summary`+`s7RenderJobId`만 반환, **선택 안의 부스 배열이 없다**. "이 안으로 편집 시작"이 선택 안을 layout에 병합하려면 옵션의 booths 또는 서버측 확정 엔드포인트가 필요. 현재는 재조회(refetch)로 임시 처리. **요청: auto-generate 옵션에 booths 포함 또는 `POST .../layout/apply-option {optionId}`**. + +4. **[하] 홀 매니저 SSO·이메일 인증 로그인** — SCR-01의 "킨텍스 내부 계정 로그인" 버튼은 계약에 대응 엔드포인트 없음(login 단일). 현재 UI만 배치, 미배선. Phase B-1(2FA/SSO) 시 배선 예정. + +5. **[정보] degraded 폴백** — `GET layout`·`login`·`accept-invite`가 현재 **501(NOT_IMPLEMENTED)**. 프론트는 501을 감지해 로그인은 안내 메시지, 에디터는 **샘플 배치(`sampleLayout.ts`, 계약 shape 동일)** 로 캔버스 시연. 매퍼 구현 시 코드 변경 없이 실데이터로 대체됨. + +## 계약 준수 확인 +- 응답 봉투 `ApiResponse`·`PageResponse`·오류코드→분기(FORBIDDEN·COMPLIANCE_BLOCKED·NOT_REGISTERED_COMPANY·NOT_IMPLEMENTED) 그대로 구현. +- AI 생성 이미지는 `watermarkRequired`/`watermarkText`/`notice`를 **항상 노출**(제거 불가 `AiImage` 컴포넌트). 계약 §0-3 준수. +- 민감정보(IP·SSH·해시) 미표시. 사용자/업체는 이름·역할만. + +## 남은 화면 (미착수) +- 웹: SCR-02 주최자 대시보드 · SCR-04 배치안 비교(S7) · SCR-05 참가업체 홈 · SCR-06 설계 스튜디오(4샷+Before/After) · SCR-07 배선 뷰 · SCR-08 신청 요약 · SCR-09 규정 리포트 · SCR-10 승인 큐 · SCR-11 검수 상세 · SCR-12 갤러리 +- 모바일(390px): SCR-M1 현장 체크리스트 · SCR-M2 현장 검수 +- 공통 후속: 재사용 컴포넌트(Before/After CompareSlider — reroomai 패턴, 배선 오버레이 렌더러, 데이터 테이블/지브라), 실 캔버스 인터랙션(부스 드래그·리사이즈·병합), WebGL 대규모 렌더 검토. diff --git a/docs/API_GUIDE.md b/docs/API_GUIDE.md new file mode 100644 index 0000000..1c62db4 --- /dev/null +++ b/docs/API_GUIDE.md @@ -0,0 +1,97 @@ +# 킨텍스 자동전시시스템 — API 규약 가이드 + +> **WISE(UIWS) 참조** — 응답 봉투(`ApiResponse`/`PageResponse`)·인증(JWT+2FA)·에러 처리 규약은 `workspace/uiws/backend/README.md`를 따른다. +> **정본 계약서**: 엔드포인트 상세·요청/응답 shape·DB 매퍼 인수는 [`_workspace/01_backend_contracts.md`](../_workspace/01_backend_contracts.md)가 단일 진실원천이다. 본 문서는 **규약(convention)만 요약**하고 세부는 계약서로 링크한다(중복 서술 회피). + +--- + +## 1. 경로·버전 규약 + +- 베이스: `/api`. 프론트 axios `baseURL=/api`(동일 도메인 서빙, 별도 CORS 불필요). +- 도메인 경로는 **행사 스코프** 접두: `/api/events/{eventId}/…` (예 `…/halls/{hallId}/layout`, `…/booths/{boothId}/design`). +- 시스템/관리 경로: `/api/system/**`·`/api/admin/**`(ADMIN 전용). 인증: `/api/auth/**`. +- 내부(워커) 경로: `/api/internal/**`(공유 시크릿 인증). +- **버전**: P0는 무접두(`/api/...`). 파괴적 변경 발생 시 `/api/v2/...` 도입 — 계약서 변경 이력에 기록하고 frontend·qa에 통지. + +--- + +## 2. 응답 봉투 + +모든 응답은 `ApiResponse`: +```json +{ "success": true, "data": { ... }, "error": null } +{ "success": false, "data": null, "error": { "code": "FORBIDDEN", "message": "이 행사/부스에 대한 권한이 없습니다." } } +``` +- 목록: `PageResponse` = `{ "items": [...], "page": 0, "size": 20, "total": 123 }`. (P0 갤러리/워크스페이스는 배열 직접 반환도 허용 — 계약서 §0-1.) +- `error.message`는 사람이 읽을 **요약만**. 상세·스택트레이스 미노출(서버 로그). + +--- + +## 3. 오류 코드 → HTTP (고정) + +| code | HTTP | 의미 | +|---|---|---| +| `VALIDATION` | 400 | 요청 값 오류(필드 메시지 포함) | +| `UNAUTHORIZED` | 401 | 미인증/토큰 만료 | +| `FORBIDDEN` | 403 | 행사/부스 권한 없음 | +| `NOT_REGISTERED_COMPANY` | 403 | 미등록 장치업체 초대·응찰 차단 | +| `NOT_FOUND` | 404 | 대상 없음 | +| `CONFLICT` | 409 | 상태 충돌(낙관적 잠금 등) | +| `COMPLIANCE_BLOCKED` | 422 | 규정 위반(차단) | +| `RENDER_QUOTA_EXCEEDED` | 429 | 행사 이미지 생성 쿼터 소진 | +| `NOT_IMPLEMENTED` | 501 | 매퍼/엔진 구현 대기(스켈레톤) | +| `INTERNAL` | 500 | 서버 오류(요약만) | + +> 코드는 문자열 상수(`common.exception.ErrorCode`). 신규 코드 추가 시 계약서 §0-2와 본 표를 동시 갱신. + +--- + +## 4. 인증 헤더 · RBAC + +- 헤더: `Authorization: Bearer ` (HS256). 클레임: `sub`(userId)·`name`·`roles`(eventId→역할)·`hm`(홀매니저). +- 공개 경로(인증 불필요): `GET /health`, `POST /api/auth/login`, `/ws/**`, `POST /api/internal/render/callback`(워커 토큰). +- **2차 인증**: `POST /api/auth/login`(1차) → `verifyToken` → `POST /api/auth/verify-otp`(EMAIL 코드/OTP) → access·refresh. (WISE `auth` 이식 — [`DEVELOPMENT_GUIDE.md`](DEVELOPMENT_GUIDE.md) §4.) +- **행사 단위 RBAC**: 역할 `ORGANIZER·EXHIBITOR·CONTRACTOR·HALL_MANAGER`([`COMMON_CODES.md`](COMMON_CODES.md) `EVENT_ROLE`). 가드 — 열람=행사 멤버 or 홀매니저 / 편집·액션=엔드포인트별 역할. + +--- + +## 5. 보안 불변 (API 계약 강제) + +- 민감정보(IP·SSH·비밀번호·해시·내부 식별자·`GEMINI_API_KEY`) 응답 완전 제외. 사용자/업체 표시는 비민감 필드만. +- **AI 생성 이미지**(M5)는 응답에 `watermarkRequired:true`+`watermarkText`+`notice`(계약·심사 서류 사용 금지) **항상** 포함. +- 워커 실패 시 `errorMessage`는 요약만 통과(스택트레이스 유입 차단). +- 상세: [`DEVELOPMENT_GUIDE.md`](DEVELOPMENT_GUIDE.md) §5. + +--- + +## 6. 비동기·실시간 (Redis + WebSocket) + +- **RenderJob**: `POST …/render`(발행) → Redis 큐(`kintex:renderjob:queue`) → Python 워커 소비 → `POST /api/internal/render/callback`(콜백) → 상태 갱신. +- **WebSocket(STOMP)**: 핸드셰이크 `GET /ws`(SockJS), 브로드캐스트 prefix `/topic`, 클라→서버 `/app`. 구독 `/topic/render/{jobId}` → RenderJob 완료/실패 푸시. (승인 이벤트 토픽은 M6/C-4 확장.) + +--- + +## 7. 엔드포인트 카탈로그 (요약 — 상세는 계약서) + +> 각 항목의 요청/응답 shape·완성/스켈레톤(501) 현황은 [`_workspace/01_backend_contracts.md`](../_workspace/01_backend_contracts.md) 해당 절 참조. + +| 영역 | 대표 경로 | 계약서 절 | +|------|-----------|-----------| +| 헬스 | `GET /health` | §1 | +| 인증·워크스페이스 | `/api/auth/login·workspaces·me·accept-invite` | §2 | +| M2 플로어플랜 | `/api/events/{eventId}/halls/{hallId}/layout` (`GET·PUT·validate·auto-generate`) | §3 | +| M3 부스 설계 | `/api/events/{eventId}/booths/{boothId}/design` (`GET·PUT·precheck`) | §4 | +| M4 유틸리티/배선 | `/api/events/{eventId}/booths/{boothId}/utility` (`quote·wiring·order·GET`) | §5 | +| M5 나노바나나 | `/api/events/{eventId}/booths/{boothId}/render` · `/render-jobs/{jobId}` · `/api/internal/render/callback` | §6 | +| 룰셋 | `rulesets/compliance-v1.json`·`rates-v1.json` (데이터 계약) | §7 | +| DB 매퍼 인수 | UserMapper·BoothMapper·DesignMapper·WiringMapper·RenderJobMapper (PostGIS) | §8 | + +> Phase D 도메인(M10 관람객·M12 공개사이트/CMS·M15 옥션·M16 BI·M18 관리자)의 API는 각 도메인 에이전트가 계약서에 절을 추가하며 확장한다. 본 가이드의 §1~6 규약을 동일 준수. + +--- + +## 8. 참조 + +- 정본 계약서: [`_workspace/01_backend_contracts.md`](../_workspace/01_backend_contracts.md) +- 나노바나나 워커 계약: [`../tools/nanobanana/_workspace/01_worker_contract.md`](../tools/nanobanana/_workspace/01_worker_contract.md) +- 개발 표준: [`DEVELOPMENT_GUIDE.md`](DEVELOPMENT_GUIDE.md) · 공통코드: [`COMMON_CODES.md`](COMMON_CODES.md) diff --git a/docs/BUILD_DEPLOY.md b/docs/BUILD_DEPLOY.md new file mode 100644 index 0000000..a5d18b2 --- /dev/null +++ b/docs/BUILD_DEPLOY.md @@ -0,0 +1,87 @@ +# 킨텍스 자동전시시스템 — 빌드/실행 개요 + +> **WISE(UIWS) 참조** — 빌드·배포 흐름(프론트 vite build → 백엔드 번들 / systemd / webhook 자동배포)은 `workspace/uiws/deploy/README_배포.md`·`DEV_HANDOFF.md`를 따른다. +> 본 문서는 **빌드/실행 개요**만 다룬다. 상세 CI/CD 파이프라인(Gitea webhook·deploy_server·systemd·nginx·롤백)은 **Phase E `kintex-devops-dev`** 산출물이 정본이다. 운영 서버·포트는 **G2 게이트**(GUARDiA 인프라와 별개 도메인) 확정 후. + +--- + +## 1. 산출물 구성 + +킨텍스는 **3개 실행 단위**로 구성된다: + +| 단위 | 빌드 | 산출물 | 실행 | +|------|------|--------|------| +| 백엔드 | `./gradlew bootJar` (JDK17) | `build/libs/kintex-*.jar` | `java -jar`(systemd 권장) | +| 프론트(웹) | `npm run build` (Node 18+, Vite) | `dist/`(역할별 번들) | nginx 정적 서빙(SPA 폴백) | +| 나노바나나 워커 | (Python, 빌드 없음) | `tools/nanobanana` 모듈 | 큐 소비 데몬(별도 서비스) | + +> 프론트 dist를 백엔드 static에 번들하는 단일 jar 패키징(GUARDiA 표준)도 가능하나, 킨텍스는 **역할별 프론트 번들 분리**(PLANNING §2-1)이므로 nginx 정적 서빙 + 백엔드 API 분리를 기본으로 한다(Phase A SA가 배포 토폴로지 확정). + +--- + +## 2. 로컬 빌드/실행 + +환경변수·설치는 [`ENV_SETUP.md`](ENV_SETUP.md) 참조. + +```bash +# 백엔드 +cd src/backend +./gradlew build # 컴파일+테스트 +./gradlew bootRun # 개발 실행 (또는 java -jar build/libs/kintex-*.jar) + +# 프론트 +cd ../frontend +npm install +npm run dev # 개발 서버(프록시 → /api) +npm run build # dist/ 생성 + +# 나노바나나 워커 (G1 승인 후 실호출) +cd ../../tools/nanobanana +pip install google-genai Pillow +python -m tools.nanobanana.worker +``` + +--- + +## 3. 빌드 게이트 (push 전 — WISE 관행) + +WISE `.githooks/pre-push` 패턴을 준용해 파이프라인을 보호한다: + +1. 시크릿 파일 커밋 차단(`.env`·`*.key`·`*-adminsdk-*.json` — gitignore 유지) +2. Flyway 마이그레이션 번호 충돌 검사(신규 = 최대+1) +3. 변경분 백엔드 `compileJava` / 프론트·워커 `tsc`·lint — **실패 시 push 차단** +4. 자동배포 경고(파이프라인 연결 시 push=배포 트리거) + +--- + +## 4. 배포 흐름 (개요 — 상세는 Phase E) + +GUARDiA 표준 배포 파이프라인 준용: + +``` +workspace/kintex ── git push ──> Gitea(zio/kintex) ── webhook ──> deploy_server + └─> 빌드(gradlew bootJar · vite build) → Flyway 마이그 → jar 재기동 · dist 배포 → 헬스 게이트(GET /health) +``` + +- **Fail-Safe**: 백업 → 배포 → 헬스체크(200) → 실패 시 롤백(이전 jar 유지). 깨진 jar가 서버를 죽이지 않도록 clean bootJar 검증 후 교체(WISE 배포 자기방어 패턴). +- **systemd**: 백엔드 jar·워커 데몬을 유닛으로 등록(부팅 자동기동·재시작). AI env drop-in(`ANTHROPIC_API_KEY`·`ADMIN_PASSWORD_ENC`)은 표준 프레임워크 §7 방식. +- **nginx**: `` vhost → `/`=프론트 정적, `/api/`·`/ws`=백엔드 포트. TLS는 certbot. (도메인·포트 = G2 확정.) +- **운영 배포는 소유자 승인 필수.** + +--- + +## 5. 선행 게이트 + +| 게이트 | 내용 | 영향 | +|--------|------|------| +| **G1** | 나노바나나(Gemini) 외부 호출 승인(PLANNING R12) | 워커 실이미지 생성·M5 배포. 미승인 시 목/degraded | +| **G2** | 배포 대상 서버·포트(별개 도메인) | Phase E 배포 착수 전 | + +--- + +## 6. 참조 + +- 환경 구축: [`ENV_SETUP.md`](ENV_SETUP.md) · 개발 표준: [`DEVELOPMENT_GUIDE.md`](DEVELOPMENT_GUIDE.md) +- 구현 백로그(Phase E 배포): [`IMPLEMENTATION_BACKLOG.md`](IMPLEMENTATION_BACKLOG.md) +- WISE 배포 원본(참조): `workspace/uiws/deploy/README_배포.md`·`workspace/uiws/DEV_HANDOFF.md` +- 표준 프레임워크 배포: `workspace/_framework/GUARDIA_STANDARD_FRAMEWORK.md` §7 diff --git a/docs/COMMON_CODES.md b/docs/COMMON_CODES.md new file mode 100644 index 0000000..c149726 --- /dev/null +++ b/docs/COMMON_CODES.md @@ -0,0 +1,74 @@ +# 킨텍스 자동전시시스템 — 공통코드 정의 + +> **WISE(UIWS) 참조** — 공통코드 체계(그룹 `TB_CODE_GRP` / 값 `TB_CODE`, 코드값=영문 상수·코드명=한글 표기)는 `workspace/uiws/_workspace/01_analyst_codes.md`를 따른다. +> **확정 규칙**: **확정**=PLANNING/계약서에 값 명시 / **확인 필요**=Phase A(DA)·도메인 에이전트 확정 대기. 미정 코드값은 임의 확정 금지 — 확정 시 본 문서 + ERD 컬럼 주석 동시 갱신. +> 근거: [`PLANNING.md`](PLANNING.md)·[`design.md`](design.md)·[`_workspace/01_backend_contracts.md`](../_workspace/01_backend_contracts.md). + +--- + +## 1. 공통코드 관리 원칙 (WISE 체계 이식) + +- 적재: `TB_CODE_GRP`(그룹) / `TB_CODE`(값). 코드값은 **영문 상수**, 코드명은 화면 표기 **한글**. +- 시스템관리(B-2)의 공통코드 관리 화면에서 CRUD. DTO 필드 ↔ 코드그룹 매핑은 §4 표를 단일 출처로 준수. +- **코드 vs 마스터 구분**: 열거 가능한 소수 값은 공통코드, 다건·CRUD 대상(홀·요율·규정 룰셋·등록업체)은 **마스터 테이블**로 관리(공통코드 아님). + +--- + +## 2. 킨텍스 도메인 공통코드 + +| 그룹코드 | 그룹명 | 코드값 목록 (코드값=코드명) | 사용처 | 확정여부 | +|---|---|---|---|---| +| `EVENT_ROLE` | 행사 역할(RBAC) | `ORGANIZER`=주최자, `EXHIBITOR`=참가업체, `CONTRACTOR`=장치·시공업체, `HALL_MANAGER`=홀매니저(킨텍스 운영) | 계약서 §0-5 RBAC, JWT `roles`, 역할별 포털 | **확정** (계약서 §0-5) | +| `PORTAL_ROLE` | 포털/채널 역할(6분리) | `ORGANIZER`, `EXHIBITOR`, `CONTRACTOR`, `OPS`=킨텍스 직원/홀매니저, `ADMIN`=시스템관리자, `VISITOR`=관람객, `PUBLIC`=일반 대중 | PLANNING §2-1 역할별 웹/모바일 포털 분리 | **확정** (PLANNING §2-1) — VISITOR/PUBLIC은 P1·공개·셀프서비스 쓰기 제한 | +| `BOOTH_TYPE` | 부스 유형 | `independent`=독립부스, `assembled`=조립부스 | M2/M3 설계(계약서 `boothType`), 부스 마스터 | **확정** (PLANNING §5 M3: 조립/독립 · 계약서 design `boothType:"independent"`) | +| `ZONE_TYPE` | 부스 구역 유형 | `demo`=시연, `consult`=상담, `storage`=창고, `reception`=접수 (확장 가능) | M3 DesignSpec `zones[].type` | 부분 확정 (계약서에 demo·consult 명시, 그 외 **확인 필요** — designer/DA) | +| `LAYOUT_STATUS` | 배치안 상태 | `draft`=작성중, `submitted`=제출, `approved`=승인, `rejected`=반려 | M2 LayoutDto `status` | 부분 확정 (계약서 `draft` 명시, 전이 상태는 M6 승인 워크플로 **확인 필요**) | +| `DESIGN_STATUS` | 설계안 상태 | `draft`=작성중, `submitted`=제출, `approved`=승인, `rejected`=반려 | M3 DesignPlanDto `status` | 부분 확정 (계약서 `draft` 명시, 나머지 **확인 필요**) | +| `COMPLIANCE_SEVERITY` | 규정 심각도 | `block`=차단, `warn`=경고, `pass`=통과 | M2/M3 ComplianceReport `violations[].severity` | **확정** (계약서 §3·§4: block/warn + passCount) | +| `COMPLIANCE_GROUP` | 규정 그룹 | `egress`=피난/비상, `structure`=구조/하중, `height`=높이, `fire`=방염, `lighting`=조명 (룰셋 기준) | 규정 룰셋(`compliance-v1.json`), ComplianceReport `group` | 부분 확정 (계약서 `egress` 명시 — 전체 그룹은 룰셋 데이터가 정본, **확인 필요**) | +| `RENDER_STATUS` | 렌더잡 상태 | `QUEUED`=대기, `RUNNING`=진행, `DONE`=완료, `FAILED`=실패 | M5 RenderJobDto `status` | **확정** (계약서 §6) | +| `SHOT_PRESET` | 표준 샷 세트 | `S1`=정면 주간, `S2`=정면 야간, `S3`=통로 뷰, `S4`=내부 뷰, `S5`=Before/After, `S6`=배선 오버레이(래스터), `S7`=홀 전경 조감 | M5 RenderJobRequest `shotPreset` | **확정** (PLANNING §6-3 · 워커 README) | +| `UTILITY_ORDER_STATUS` | 유틸리티 신청 상태 | `draft`=작성중, `submitted`=제출, `relayed`=릴레이완료 | M4 UtilityOrderDto `status` | 부분 확정 (계약서 `submitted` 명시, 나머지 **확인 필요**) | +| `AUCTION_STATUS` | 옥션 상태 | `OPEN`=응찰중, `BIDDING`=라운드진행, `AWARDED`=낙찰, `CLOSED`=마감 (예시) | M15 공사/장치 옥션(Auction) | **확인 필요** (Phase D bidding-dev/DA 확정 — 계약 미정의) | +| `QUOTATION_STATUS` | 견적서 상태 | `SUBMITTED`=제출, `REVISED`=수정, `AWARDED`=낙찰, `REJECTED`=탈락 (예시) | M15 Quotation | **확인 필요** (Phase D bidding-dev/DA 확정) | +| `USE_YN` | 사용여부 | `Y`=사용, `N`=미사용 | 전 관리화면 공통 | **확정** (시스템 공통) | + +> 위 상태·옥션·구역 코드 중 **확인 필요** 항목은 예시 제안값이다. Phase A(DA)·해당 도메인 에이전트가 화면/워크플로 확정 시 값을 고정하고 본 문서·ERD를 동시 갱신한다. + +--- + +## 3. WISE 공통 레이어 코드 (이식 대상) + +공통 업무·시스템관리 레이어(B-2/B-3)를 WISE에서 이식할 때 아래 코드도 함께 이식한다(값은 WISE `01_analyst_codes.md` 정본): + +| 그룹코드 | 그룹명 | 요지 | +|---|---|---| +| `USER_ROLE` | 시스템 사용자 역할 | `USER`/`MANAGER`/`ADMIN` — 데이터 가시범위·권한 단일 소스(본인/팀/전체). 킨텍스는 `EVENT_ROLE`(행사 스코프)와 병행 운용 | +| `VERIFY_METHOD` | 2차검증 방식 | `EMAIL`=이메일 인증코드, `OTP`=OTP앱(TOTP). 사용자별 선택 | +| `PRG_TYPE` | 프로그램 유형 | `FORM`/`POPUP` — 메뉴/프로그램 관리 | +| `MSG_RCV_TYPE` | 쪽지 수신구분 | `RECV`/`REF` — 공통 message 모듈 이식 시 | +| (기타) | worklog·schedule·stats 코드 | worklog·schedule·통계 모듈 이식 시 WISE 코드(WORK_STATUS·WORK_TYPE·SCHE_GUBUN·IMPORTANCE·WORK_PROGRESS 등) 동반 이식 | + +> 킨텍스는 **행사 단위 역할(`EVENT_ROLE`)이 1차 권한 소스**다. WISE `USER_ROLE`(전역 가시범위)은 공통 업무 레이어(worklog 등)를 이식할 때만 병행 적용한다. + +--- + +## 4. DTO 필드 ↔ 코드그룹 매핑 요약 + +| DTO 필드 | 코드그룹 | 비고 | +|---|---|---| +| `myRole` / `eventRoles` | EVENT_ROLE | 행사별 역할(계약서 login·me) | +| `boothType` | BOOTH_TYPE | M2/M3 | +| `zones[].type` | ZONE_TYPE | M3 DesignSpec | +| `status`(layout) | LAYOUT_STATUS | M2 | +| `status`(design) | DESIGN_STATUS | M3 | +| `violations[].severity` | COMPLIANCE_SEVERITY | M2/M3 규정 리포트 | +| `violations[].group` | COMPLIANCE_GROUP | 룰셋 데이터 기준 | +| `status`(render) | RENDER_STATUS | M5 | +| `shotPreset` | SHOT_PRESET | M5 | +| `status`(utility order) | UTILITY_ORDER_STATUS | M4 | +| `status`(auction) | AUCTION_STATUS | M15 (확인 필요) | +| `verifyMethod` | VERIFY_METHOD | 2차 인증 | +| `useYn` | USE_YN | 공통 | + +> 비-코드(마스터 테이블): 홀(`Hall`)·요율 룰셋(`rates-v1.json`)·규정 룰셋(`compliance-v1.json`)·등록업체(`Company`)는 공통코드가 아니라 마스터/버전 파일로 관리(관리자 백오피스 M18에서 CRUD·버전). diff --git a/docs/DEVELOPMENT_GUIDE.md b/docs/DEVELOPMENT_GUIDE.md new file mode 100644 index 0000000..6a0aa8c --- /dev/null +++ b/docs/DEVELOPMENT_GUIDE.md @@ -0,0 +1,130 @@ +# 킨텍스 자동전시시스템 — 개발 표준 가이드 + +> **WISE(UIWS) 참조** — `workspace/uiws`(GUARDiA 표준 프레임워크 정본)의 백엔드/인증/보안 컨벤션을 킨텍스 스택(MyBatis·PostGIS·Redis·나노바나나 워커)에 맞춰 정리했다. +> 정본 링크: 아키텍처 표준은 Phase A `docs/architecture/*`(kintex-aa/sa/ta), API 계약은 [`_workspace/01_backend_contracts.md`](../_workspace/01_backend_contracts.md), 프로젝트 규칙은 [`CLAUDE.md`](../CLAUDE.md). + +--- + +## 1. 기술 스택 (확정 — 변경 금지) + +[`CLAUDE.md`](../CLAUDE.md) §기술 스택이 정본. 요약: + +| 레이어 | 표준 | +|--------|------| +| 프론트(웹) | React 18/19 + Vite + TypeScript (역할별 번들 분리 — PLANNING §2-1) | +| 백엔드 | Spring Boot 3.x(Java 17) + MyBatis — REST + WebSocket(STOMP) | +| DB | PostgreSQL + **PostGIS**(부스 polygon·트렌치 point·배선 LineString) | +| 비동기 | Redis 작업 큐(RenderJob·서류·알림) | +| 이미지 생성 | 나노바나나 Python 워커 사이드카(`tools/nanobanana`, google-genai) | +| AI(텍스트) | Claude 기본 + 설정형 전환(`AiTextRouter`/`AiConfig`) — 실패 시 Ollama 폴백 | +| 인증 | 행사 단위 RBAC(JWT HS256) + 2차 인증(OTP/TOTP) | + +- 패키지 루트: **`com.zioinfo.kintex`** · DB: **`kintex_db`** +- 신규 코드는 이 스택만 사용. 나노바나나 호출은 `tools/nanobanana` Python 워커만 경유(백엔드는 큐 발행·상태·콜백까지만, `GEMINI_API_KEY` 미취급). + +--- + +## 2. 패키지·레이어 구조 (백엔드) + +WISE 계층(`controller·service·repository·domain·dto`)을 MyBatis로 매핑한다. `com.zioinfo.kintex` 하위: + +``` +com.zioinfo.kintex +├── config # SecurityConfig, JwtProperties, WebSocketConfig, RedisConfig, MyBatis @MapperScan(annotationClass=Mapper.class) +├── security # JwtTokenProvider, JwtAuthenticationFilter, KintexPrincipal, RestAuthEntryPoint +├── common # response(ApiResponse·PageResponse), exception(ApiException·ErrorCode·GlobalExceptionHandler), audit(AOP) +├── auth # controller / service(AuthService·TotpService) / mapper / dto +├── module +│ ├── m2 # 플로어플랜: controller·service·mapper(BoothMapper, PostGIS ST_*)·dto·engine(ComplianceRuleEngine) +│ ├── m3 # 부스 설계: DesignMapper·precheck +│ ├── m4 # 유틸리티/배선: WiringMapper·요율 룰 +│ ├── m5 # 나노바나나 RenderJob: 큐 발행·콜백·WebSocket 푸시 +│ └── … # M10·M12·M15·M16·M18 등 (Phase D) +├── system # 시스템관리(사용자·역할/권한·공통코드·메뉴·감사로그·설정) — WISE 이식(B-2) +└── work # 공통 업무기능(worklog·schedule·message·stats·notice…) — WISE 이식(B-3) +``` + +- **레이어 규칙**: `controller`(요청 검증·RBAC 진입) → `service`(트랜잭션·룰·엔진) → `mapper`(MyBatis XML, 공간 쿼리 `ST_*`). 컨트롤러는 도메인 로직 금지, 매퍼는 비즈니스 판단 금지. +- **매퍼**: `@Mapper` 인터페이스 + `resources/mybatis/mapper/*.xml`. PostGIS 연산(`ST_MakePolygon`·`ST_Area`·`ST_Distance`·`<->` KNN)은 XML에. +- **룰셋은 코드가 아닌 데이터**: 규정(`rulesets/compliance-v1.json`)·요율(`rulesets/rates-v1.json`)은 버전 파일. 개정 시 파일 교체, 리포트에 `rulesetVersion`·`disclaimer` 항상 기록. + +### 프론트(웹) 구조 +WISE 컨벤션 `pages/components/api/store/hooks/routes`. 역할별 포털(organizer·exhibitor·contractor·ops·admin·public+visitor)은 번들 분리(PLANNING §2-1)하되 공유 디자인 시스템·공통 컴포넌트·API 계약을 상속한다. axios `baseURL=/api`. + +--- + +## 3. API·응답 규약 + +상세는 [`API_GUIDE.md`](API_GUIDE.md) 및 계약서 [`_workspace/01_backend_contracts.md`](../_workspace/01_backend_contracts.md). 핵심: + +- 응답 봉투 `ApiResponse` = `{ success, data, error }`, 목록 `PageResponse` = `{ items, page, size, total }`. +- 오류 코드(문자열)→HTTP 매핑 고정(`VALIDATION`400·`FORBIDDEN`403·`COMPLIANCE_BLOCKED`422·`NOT_IMPLEMENTED`501 …). +- 모든 도메인 경로는 `{eventId}` 스코프 + 행사 단위 RBAC 가드. + +--- + +## 4. 인증 표준 (JWT + 2FA/OTP) — WISE 이식 + +GUARDiA 표준 프레임워크 §2 + WISE `auth` 모듈을 이식한다(백로그 B-1). + +- **1차 로그인**(ID/PW) → `verifyToken` 발급 → **2차 검증**(EMAIL 인증코드 또는 OTP/TOTP) → access·refresh 토큰. +- **OTP**: TOTP RFC6238(SHA1·30초·6자리·±1윈도). `TotpService` 이식. 최초 QR 등록, 마이페이지 재설정/해제, **관리자 OTP 초기화**(`otp_secret=NULL`). 사용자별 `VERIFY_METHOD`(EMAIL/OTP)로 분기. +- **RBAC**: JWT 클레임 `roles`(eventId→역할)·`hm`(홀매니저). `/api/system/**`·`/api/admin/**` = `hasRole(ADMIN)`. 데이터 가시범위(역할 스코프)는 WISE `DataScopeService` 패턴 참조. +- **로그인 실패 잠금** + 관리자 해제. +- **admin 비밀번호**: env `ADMIN_PASSWORD_ENC`(AES-256-GCM) + 별도 키파일 복호 → 기동 시 BCrypt 재시드. **`admin123` 하드코딩 시드 금지.** + +--- + +## 5. 보안 불변 (계약 강제 — 위반 시 QA 반려) + +| 규칙 | 내용 | +|------|------| +| 자격증명 미노출 | IP·SSH·비밀번호·해시·`GEMINI_API_KEY`·`ANTHROPIC_API_KEY`·OTP 시크릿을 응답·로그·에러메시지·커밋에 절대 노출 금지 | +| 민감 필드 제외 | 사용자/업체 응답은 이름·역할·번호 등 비민감 필드만. 내부 식별자·해시 shape 제외 | +| 스택트레이스 차단 | `error.message`는 사람이 읽을 요약만. 상세는 서버 로그. `GlobalExceptionHandler`·`DataAccessException` 핸들러로 누출 차단 | +| AI 이미지 워터마크 | 나노바나나 산출 이미지 응답은 `watermarkRequired:true`+`watermarkText`+`notice`(계약·심사 서류 사용 금지) **항상** 포함 — 제거 불가 | +| 등록업체 응찰 | 장치업체(CONTRACTOR)는 킨텍스 등록업체 검증 통과분만 초대·응찰(`NOT_REGISTERED_COMPANY` 403) | +| 외부 API 금지 | 온프레미스 우선. 예외: `api.anthropic.com`(Claude, 키 env only·실패 시 Ollama 폴백) + `Gemini`(나노바나나, G1 승인 대상·워커 전용) | +| 암호화 저장 | 비밀·자격증명 AES-256-GCM. 비밀번호는 BCrypt 해시 | + +--- + +## 6. 코딩 규약 + +- **언어**: 문서·주석·커밋 본문 설명은 한국어 허용, **코드 식별자·커밋 제목·PR 제목은 영어**. +- **네이밍**: Java `camelCase`/`PascalCase`, DB 컬럼 `SNAKE_CASE`(WISE와 동일 — 예 `WRITER_ID`·`START_HOUR`), DTO 필드 `camelCase`. +- **DTO ↔ 코드그룹 매핑**은 [`COMMON_CODES.md`](COMMON_CODES.md) 표를 단일 출처로 준수(`boothType`·`myRole`·`severity` 등). +- **널/기본값**: 상태·역할 등 NOT NULL 기본값은 코드 문서 기준(예 역할 기본 `USER`/부스 상태 기본 `draft`). +- **프론트**: TypeScript strict. API 응답 타입은 계약서 shape과 1:1. 임의 `any` 지양. +- **DB 마이그레이션**: `kintex_db`는 **Flyway 순번 마이그레이션**(`V__` / 번호 규약, 백로그 B-0). 스키마가 단일 진실원천 — 엔티티/매퍼는 이를 따른다. 마이그 번호 충돌 금지(신규는 최대 번호+1). + +--- + +## 7. 브랜치·커밋·PR + +WISE 파이프라인 보호(`.githooks/pre-push`) 관행을 준용한다. + +- **브랜치**: `main`(정본) 보호. 기능은 `feat/-<요약>`, 수정은 `fix/<요약>`. 아키텍처/공통은 Phase 라벨(예 `phaseB/auth-otp`). +- **커밋 메시지**: **Conventional Commits** — `feat(m2): 플로어플랜 규정검증 API`, `fix(auth): OTP 윈도우 경계 처리`, `docs(codes): 옥션 상태 코드 추가`. 타입: `feat·fix·docs·refactor·test·chore·build·ci`. +- **push 전 게이트(권장)**: 변경분 백엔드 `compileJava` / 프론트·워커 `tsc`·lint 통과 → 실패 시 push 금지. 시크릿 파일 커밋 차단(`.env`·`*.key`·`*-firebase-adminsdk-*.json` 등은 gitignore 유지). +- **PR 규칙**: 대상 Phase/모듈 명시 · 계약서(경계면) 변경 시 frontend·db·qa 영향 기재 · 보안 불변 체크(§5) · 관련 QA 통과 링크. 아키텍처 표준(Phase A) 위반은 시정 후 병합. +- **커밋/푸시 시점**: 사용자/오케스트레이터 지시가 있을 때만. 운영 배포는 소유자 승인 필수. + +--- + +## 8. 테스트 + +- **백엔드**: 서비스·룰 엔진 단위 테스트(규정 평가·요율 산식·배선 최단경로). 공간 쿼리는 PostGIS 통합 테스트(testcontainers 또는 로컬 PostGIS). +- **경계면(계약) 검증**: `kintex-qa`가 API 응답 shape ↔ 프론트 훅/컴포넌트 호출을 교차 대조(계약서 단일 출처). 각 모듈 완성 직후 점진 검증. +- **보안 회귀**: 자격증명·PII·스택트레이스 미노출, AI 워터마크 강제, 등록업체 응찰 가드, admin env 시드를 QA 반려 사유로 상시 점검. +- **워커**: 나노바나나 모듈은 키/네트워크 없이도 import·구조 성립(목/degraded). 쿼터는 성공 시에만 차감. + +--- + +## 9. 참조 + +- 프로젝트 규칙·에이전트 워크플로: [`CLAUDE.md`](../CLAUDE.md) +- 환경 구축: [`ENV_SETUP.md`](ENV_SETUP.md) · 빌드/배포: [`BUILD_DEPLOY.md`](BUILD_DEPLOY.md) +- API 규약: [`API_GUIDE.md`](API_GUIDE.md) · 공통코드: [`COMMON_CODES.md`](COMMON_CODES.md) +- 백엔드 계약서(정본): [`_workspace/01_backend_contracts.md`](../_workspace/01_backend_contracts.md) +- 표준 프레임워크: `workspace/_framework/GUARDIA_STANDARD_FRAMEWORK.md` diff --git a/docs/ENV_SETUP.md b/docs/ENV_SETUP.md new file mode 100644 index 0000000..c5b9ca7 --- /dev/null +++ b/docs/ENV_SETUP.md @@ -0,0 +1,127 @@ +# 킨텍스 자동전시시스템 — 개발환경 구축 가이드 + +> **WISE(UIWS) 참조** — `workspace/uiws`의 `backend/README.md`·`db/README_DB연동.md`·`DEV_HANDOFF.md` 세팅 절차를 킨텍스 스택(MyBatis·PostGIS·Redis·나노바나나 Python 워커)에 맞춰 정리했다. +> ⚠️ 본 문서의 환경변수 **이름은 표준 컨벤션**(신규 코드가 준수할 규약)이며, 실제 비밀값·서버 포트는 저장소에 두지 않는다(Phase A SA/DEV 및 사내 비밀관리에서 확정). + +--- + +## 1. 사전 요구 도구 + +| 도구 | 버전 | 용도 | +|------|------|------| +| **JDK 17** | 17.x (LTS) | Spring Boot 3.x 백엔드 빌드/실행(`gradlew`) | +| **Node.js** | 18+ (LTS) | Vite 빌드(Node 16은 vite build 불가 — WISE 함정) | +| **npm** | Node 동봉 | 프론트 의존성 | +| **PostgreSQL** | 15+/16 | `kintex_db` | +| **PostGIS** | 3.x | 공간 확장(부스 polygon·트렌치 point·배선 LineString) | +| **Redis** | 6+/7 | 작업 큐(RenderJob·서류·알림) | +| **Python** | 3.11+ | 나노바나나 워커 사이드카 | +| **Git** | 2.x | Gitea(zio/kintex) | + +> Gradle Wrapper(`gradlew`)가 없으면 로컬 Gradle 8.x로 `gradle wrapper --gradle-version 8.7` 1회 실행해 생성(WISE 관행). + +--- + +## 2. 초기 세팅 + +```bash +git clone /zio/kintex.git +cd kintex + +# 백엔드 (JDK17) +cd src/backend && ./gradlew build # Phase B-0 스캐폴드 이후 + +# 프론트 (Node 18+) +cd ../frontend && npm install + +# 나노바나나 Python 워커 +cd ../../tools/nanobanana +pip install google-genai Pillow +``` + +--- + +## 3. PostgreSQL + PostGIS (`kintex_db`) + +WISE와 동일하게 **공유 PostgreSQL 인스턴스에 전용 DB + 전용 계정**을 두어 타 솔루션과 물리 분리한다. + +```sql +-- 관리자(postgres/sudo) 권한으로 실행 +CREATE ROLE kintex LOGIN PASSWORD '<개발용-임시-변경대상>' + NOSUPERUSER NOCREATEDB NOCREATEROLE; +CREATE DATABASE kintex_db OWNER kintex ENCODING 'UTF8'; +-- kintex_db 접속 후 PostGIS 활성화 +\c kintex_db +CREATE EXTENSION IF NOT EXISTS postgis; +``` + +- 콜레이션은 서버 인스턴스 컨벤션에 맞춤(WISE는 `en_US.UTF-8`; UTF8이라 한글 저장/조회 정상). +- 스키마/시드는 **Flyway 순번 마이그레이션**(백로그 B-0)으로 적용. `ddl` 수동 검증이 필요하면 `SET ROLE kintex;` 후 마이그 SQL 실행. +- **원격 DB 접속(개발 PC)**: 5432가 외부 차단이면 SSH 로컬 포워딩 — + ```bash + ssh -L 5432:localhost:5432 @ -N + # 앱은 jdbc:postgresql://localhost:5432/kintex_db 로 접속 + ``` + +--- + +## 4. Redis + +로컬 기본 포트 6379. 큐 키 예: `kintex:renderjob:queue`(백엔드 발행 → Python 워커 소비). 개발 중 워커 미가동 시 큐잉·상태는 동작(발행까지). + +--- + +## 5. 환경변수 목록 (표준 컨벤션) + +> 비밀값은 **하드코딩 금지** — 모두 환경변수/`application.yml` 프로퍼티로 주입. `.env`·`*.key`는 gitignore. + +### 5-1. 백엔드(Spring Boot) + +| 변수 | 필수 | 설명 | +|------|------|------| +| `KINTEX_DB_PASSWORD` | **필수** | PostgreSQL `kintex` 계정 비밀번호 | +| `KINTEX_JWT_SECRET` | 권장 | JWT HMAC(HS256) 시크릿(최소 32바이트). 미설정 시 개발용 기본값(운영 금지) | +| `SERVER_PORT` | 선택 | 백엔드 포트(운영 포트는 G2 게이트에서 확정 — GUARDiA 인프라와 별개 도메인) | +| `REDIS_HOST` / `REDIS_PORT` | 선택 | 기본 `localhost` / `6379` | +| `RENDER_WORKER_TOKEN` | 워커 연동 시 | `/api/internal/render/callback` 공유 시크릿(`X-Worker-Token`) | +| `ANTHROPIC_API_KEY` | AI(Claude) 사용 시 | Claude 텍스트 AI. 키는 env only — DB/코드/로그/커밋/응답 기록 금지, 실패 시 Ollama 폴백 | +| `ADMIN_PASSWORD_ENC` / `ADMIN_KEY_FILE` | 운영 | admin 비번 AES-256-GCM 암호문 + 별도 키파일(root 600) → 기동 시 BCrypt 재시드 | +| `SMTP_HOST`·`SMTP_PORT`·`SMTP_USERNAME`·`SMTP_PASSWORD`·`KINTEX_MAIL_FROM` | 메일 발송 시 | 2차 인증 EMAIL 코드·알림 발송. 미설정 시 로컬 로그 모드 | + +### 5-2. 나노바나나 Python 워커 + +| 변수 | 필수 | 설명 | +|------|------|------| +| `GEMINI_API_KEY` | 실호출 시(**G1 승인 대상**) | Gemini 이미지 생성 키. **워커에서만** 로드 — 백엔드 미취급, 코드/로그/커밋 금지 | +| `NANOBANANA_MODEL` | 선택 | 기본 `gemini-3.1-flash-image-preview` 오버라이드 | + +> **G1 게이트**: Gemini 외부 호출은 소유자 승인 대상. 미승인 시 워커는 목/degraded로 동작(import·구조 성립, 실이미지 미생성). + +--- + +## 6. 실행 + +```bash +# 백엔드 +export KINTEX_DB_PASSWORD='****' +export KINTEX_JWT_SECRET='****-32bytes이상****' +cd src/backend && ./gradlew bootRun # 또는 java -jar build/libs/kintex-*.jar + +# 프론트 (개발 서버 — axios baseURL=/api, 프록시로 백엔드 연결) +cd src/frontend && npm run dev + +# 나노바나나 워커 (G1 승인 후 실호출; 미승인 시 목) +export GEMINI_API_KEY='****' +python -m tools.nanobanana.worker # 큐 소비 → 콜백(RENDER_WORKER_TOKEN) +``` + +헬스체크: `GET /health` → `{ "success": true, "data": { "status": "UP", "service": "kintex-backend" } }`. + +--- + +## 7. 참조 + +- 빌드/배포 개요: [`BUILD_DEPLOY.md`](BUILD_DEPLOY.md) +- 개발 표준: [`DEVELOPMENT_GUIDE.md`](DEVELOPMENT_GUIDE.md) +- 나노바나나 워커: [`../tools/nanobanana/README.md`](../tools/nanobanana/README.md) +- WISE 세팅 원본(참조): `workspace/uiws/backend/README.md`·`workspace/uiws/db/README_DB연동.md` diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..973eae7 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,75 @@ +# 킨텍스 자동전시시스템 — 문서 인덱스 (문서 지도) + +> **WISE(UIWS) 참조** — 본 문서 세트는 GUARDiA 표준 프레임워크 정본 `workspace/uiws`(UIMS/WISE)의 개발 문서 구성을 레퍼런스로 킨텍스에 맞게 구성했다. +> 목적: 처음 합류하는 개발자가 "어떤 문서가 무엇을 담는지" 한눈에 파악하고, 중복 없이 정본 문서로 이동하게 한다. + +--- + +## 1. 프로젝트 한 줄 요약 + +킨텍스(한국국제전시장) 전시 운영 전 과정 — **부스 배치 → 부스/장치 설계 → 유틸리티 배선 → 나노바나나(Gemini) 시공 후 사진 시각화 → 공사 옥션 → 관람객·경영분석·공개사이트** — 을 AI로 자동화하는 **자동전시시스템(Exhibition Automation Platform)**. + +- 스택: React(Vite·TS) + Spring Boot 3.x(Java 17)·MyBatis + PostgreSQL(PostGIS) + Redis + 나노바나나 Python 워커 +- 패키지: `com.zioinfo.kintex` · DB: `kintex_db` +- AI: Claude 기본 + 설정형 프로바이더 전환(`AiTextRouter`/`AiConfig`) — GUARDiA 표준 프레임워크 준수 +- 공통 레이어(인증 2FA·시스템관리·업무기능) 레퍼런스: **WISE(UIWS)** `workspace/uiws` + +--- + +## 2. 문서 지도 + +### 2-1. 기획·설계 (정본 — 해당 에이전트 경유로만 수정) + +| 문서 | 담는 내용 | 수정 경로 | +|------|-----------|-----------| +| [`PLANNING.md`](PLANNING.md) | 시스템 기획서 v2.0 — 배경·역할/페르소나·모듈(M1~M18)·역할별 포털 분리·아키텍처 개요·나노바나나 파이프라인·리스크(R1~R12) | `planner` 에이전트 | +| [`design.md`](design.md) | UI 디자인 스펙 v1.1 — 화면(SCR-*) Stitch 프롬프트·화면 흐름·컴포넌트 | `designer` 에이전트 | +| [`IMPLEMENTATION_BACKLOG.md`](IMPLEMENTATION_BACKLOG.md) | 구현 백로그 v2.0 — Phase A~E, 모듈별 작업·담당 에이전트·완료 기준·선행 게이트(G1/G2) | `kintex-impl-orchestrator` | +| [`BACKLOG.md`](BACKLOG.md) | 검증(reviewer) 지적사항 티켓 목록 | `reviewer` 에이전트 | +| `architecture/` (Phase A 예정) | 애플리케이션·시스템·기술·데이터·네트워크 아키텍처 — **kintex-aa/sa/ta/da/na 산출 예정** | 아키텍트 팀 (Phase A) | + +### 2-2. 개발·온보딩·표준 (본 세트 — WISE 참조로 신규 작성) + +| 문서 | 담는 내용 | +|------|-----------| +| [`README.md`](README.md) | (이 문서) 문서 인덱스·문서 지도 | +| [`DEVELOPMENT_GUIDE.md`](DEVELOPMENT_GUIDE.md) | 개발 표준 — 패키지/레이어·코딩 규약·인증(JWT+2FA/OTP)·보안 불변·브랜치/커밋(conventional)·테스트·PR | +| [`ENV_SETUP.md`](ENV_SETUP.md) | 개발환경 구축 — JDK17·Node·PostgreSQL+PostGIS·Redis·Python 워커 설치·실행·환경변수 목록 | +| [`COMMON_CODES.md`](COMMON_CODES.md) | 공통코드 정의 — WISE 공통코드 체계 + 킨텍스 도메인 코드(역할·부스타입·상태·규정 심각도·옥션 상태 등) | +| [`API_GUIDE.md`](API_GUIDE.md) | API 규약 — 경로·응답 봉투·오류 코드·인증 헤더·RBAC. 상세 계약은 백엔드 계약서 링크 | +| [`BUILD_DEPLOY.md`](BUILD_DEPLOY.md) | 빌드/실행 개요 — gradlew·vite·나노바나나 워커·단일 산출물. 상세 CI/CD는 Phase E devops | + +### 2-3. API·워커 계약 (정본 — 경계면 단일 진실원천) + +| 문서 | 담는 내용 | +|------|-----------| +| [`../_workspace/01_backend_contracts.md`](../_workspace/01_backend_contracts.md) | P0 백엔드 API 계약 — 인증·M2~M5 엔드포인트·응답 shape·오류 코드·DB 매퍼 인수 목록 (frontend·db·qa 대조용) | +| [`../tools/nanobanana/_workspace/01_worker_contract.md`](../tools/nanobanana/_workspace/01_worker_contract.md) | 나노바나나 워커 계약 — RenderJob 큐·scene 스키마·콜백 | + +### 2-4. 리서치·자산 + +| 위치 | 담는 내용 | +|------|-----------| +| [`analysis/kintex-website.md`](analysis/kintex-website.md) | kintex.com 전 페이지 + 주최자/참가업체 매뉴얼 분석 | +| [`analysis/reroomai-source.md`](analysis/reroomai-source.md) | ReRoomAI 소스 분석(나노바나나 image-to-image 패턴) | +| [`assets/floorplans/`](assets/floorplans/) | 홀별 평면도 JPG 15장 + CAD(트렌치 DWG, gitignore·로컬 보존) | + +### 2-5. 하네스 + +| 위치 | 담는 내용 | +|------|-----------| +| [`../CLAUDE.md`](../CLAUDE.md) | 프로젝트 마스터 컨텍스트 — 규칙·에이전트 워크플로·하네스 이력 | +| `../.claude/agents/` | 전문 에이전트 17종(아키텍트·공통·코어·도메인·AI·QA·devops) + 범용(planner·designer·developer·visualizer·reviewer) | +| `../.claude/skills/` | 스킬(kintex-impl-orchestrator·nanobanana-visualize) | + +--- + +## 3. 신규 개발자 시작 순서 (권장) + +1. 이 문서로 문서 지도 파악 → [`../CLAUDE.md`](../CLAUDE.md) 규칙 숙지 +2. [`ENV_SETUP.md`](ENV_SETUP.md) — 로컬 개발환경 구축(JDK17·Node·PostGIS·Redis·Python 워커) +3. [`PLANNING.md`](PLANNING.md) §1~2(배경·역할) + [`design.md`](design.md)(화면) 로 도메인 이해 +4. [`DEVELOPMENT_GUIDE.md`](DEVELOPMENT_GUIDE.md) + [`API_GUIDE.md`](API_GUIDE.md) + [`COMMON_CODES.md`](COMMON_CODES.md) 로 개발 표준 습득 +5. [`IMPLEMENTATION_BACKLOG.md`](IMPLEMENTATION_BACKLOG.md) 에서 자기 Phase/모듈 확인 후 착수 (Phase A 아키텍처 → B 공통 → C 코어 → D 도메인 → E 배포) + +> **중복 회피 원칙**: 기획·화면·모듈 정의는 PLANNING/design/BACKLOG가 정본이다. 본 개발 문서 세트는 그 내용을 **재서술하지 않고 링크·요약**만 한다. 아키텍처(app/system/tech/data/network)는 Phase A 아키텍트 산출물(`architecture/`)이 정본이며 본 세트에서 생성하지 않는다. diff --git a/docs/architecture/app.md b/docs/architecture/app.md new file mode 100644 index 0000000..cc77363 --- /dev/null +++ b/docs/architecture/app.md @@ -0,0 +1,401 @@ +# 킨텍스 자동전시시스템 — 애플리케이션 아키텍처 표준 (app.md) + +> 작성: 애플리케이션 아키텍트(AA) · 작성일: 2026-07-11 · 버전: **v1.0** · BACKLOG **A-1** +> 근거: `docs/PLANNING.md` v2.0(§2 6역할 포털·§4 모듈맵·§5 M1~M9·§5A M10~M18·§5B 공통레이어·§8 아키텍처)·`docs/IMPLEMENTATION_BACKLOG.md`(Phase A~E)·`_workspace/01_backend_contracts.md`(P0 계약)·`src/backend` 스캐폴드 실측. +> 스택(확정·불변): React 18/19(Vite·TypeScript) + Spring Boot 3.x(Java 17) + MyBatis + PostgreSQL(PostGIS) + Redis + 나노바나나 Python 워커 사이드카. +> +> **문서 소유권**: 본 문서는 AA만 수정한다. 구현 에이전트(BE/FE/DB/COM/도메인 devs)는 이 표준을 **준수**하며, 위반 발견 시 kintex-qa와 함께 시정한다. 교차 문서(system.md·tech.md·data.md·network.md)와의 정합은 링크로 참조하고 직접 수정하지 않는다. + +--- + +## 0. 목적과 적용 범위 + +본 문서는 킨텍스 자동전시시스템의 **애플리케이션 구조 일관성**을 규정하는 단일 표준이다. 개별 기능 구현 방식이 아니라 **모듈 경계·레이어링·패키지·API 규격·공통 컴포넌트·의존성 규칙**을 정의한다. + +- **적용 대상**: `src/backend`(Spring Boot) 전 모듈, `src/frontend`(React) 전 포털, 나노바나나 워커와의 큐 계약, kintex-common(WISE/UIWS 이식) 공통 레이어. +- **정합 기준**: PLANNING §8/§8-1 아키텍처 개요와 **정합하며 이를 구체화**한다. 상충 시 PLANNING이 상위, 본 문서가 구현 표준. +- **현행 스캐폴드 정합**: 본 표준은 이미 스캐폴드된 실제 구조(§1.2)를 성문화한 것이며, 신규 모듈은 이 패턴을 복제한다. 기존 코드 변경을 요구하지 않는다(성문화·확장). + +--- + +## 1. 패키지 구조 표준 + +### 1-1. 루트 패키지 + +전 백엔드 코드는 `com.zioinfo.kintex` 하위에 둔다(GUARDiA 표준 프레임워크 정렬, WISE=`com.zioinfo.*` 관례). 최상위는 **횡단 관심사(cross-cutting)** 와 **도메인 모듈(module)** 로 나뉜다. + +``` +com.zioinfo.kintex +├── KintexApplication # 부트 진입점 +├── common # 횡단: 응답봉투·페이징·에러·감사·유틸 (모듈 무의존) +│ ├── ApiResponse / PageResponse +│ ├── error/ (ErrorCode·ApiException·GlobalExceptionHandler) +│ ├── audit/ (감사 AOP·@Audited — Phase B B-2/B-4) +│ └── code/ (공통코드 조회 캐시 — Phase B) +├── config # 부트 설정: SecurityConfig·WebSocketConfig·RedisConfig·MyBatisConfig +├── auth # 인증/인가: JWT·RBAC·2FA(OTP)·principal·guard (도메인 무관 공용) +│ ├── dto/ · mapper/ +├── rules # 룰 엔진: 규정(compliance)·요율(rate) 룰셋 로딩·평가 (서비스 계층) +├── health # 헬스체크 +└── module # ★도메인 모듈 루트 — 모듈별 서브패키지 + ├── m1 … m9 # 판매·운영(배정·서류·매칭·정산·물류) + ├── m2 · m3 · m4 · m5 # ★P0 부스 시공 코어 + ├── m10 · m11 · m12 · m13 · m14 # 관람·참가·마케팅·wayfinding·현장운영 + └── m15 · m16 · m17 · m18 # 옥션·BI·CMS·관리자 +``` + +### 1-2. 모듈 내부 구조 (표준 레이아웃 — 스캐폴드 실측) + +각 도메인 모듈 `module.mN`은 아래 4계층을 **고정 서브패키지**로 둔다. M2가 정본 참조 패턴이다. + +``` +module.mN +├── MNController # REST 진입 — 얇게 유지(가드·바인딩·위임만) +├── MNService # 서비스 인터페이스(계약) +├── MNServiceImpl # 서비스 구현(비즈니스 로직·트랜잭션 경계) +├── dto/ # 요청/응답 DTO — record 우선(불변) +│ └── *Dto / *Request / *Response +├── mapper/ # MyBatis 매퍼 인터페이스(@Mapper) +│ └── MNMapper (XML은 resources/mybatis/mapper/) +├── MNProperties (선택) # @ConfigurationProperties 모듈 설정 +└── domain/ (선택) # 순수 도메인 모델·값객체(엔티티 매핑 시) +``` + +> **명명 규칙**: 서비스는 인터페이스(`FloorplanService`) + 구현(`FloorplanServiceImpl`) 분리(스캐폴드 실측). 컨트롤러는 `<도메인명>Controller`. DTO는 `record` 우선(불변·직렬화 안정). 모듈 접두어 `mN`은 패키지에만 쓰고 클래스명은 도메인 어휘(Floorplan·Design·Utility·RenderJob·Auction·Visitor…)를 쓴다. + +### 1-3. 리소스 레이아웃 + +``` +src/backend/src/main/resources +├── application.yml # 시크릿·엔드포인트는 env 플레이스홀더만(하드코딩 금지) +├── mybatis/mapper/**/*.xml # 공간 SQL(ST_*) 포함 매퍼 XML — mapper-locations로 로드 +└── rulesets/ # 버전 관리 룰셋 데이터(코드 아님) + ├── compliance-v1.json (compliance-v1.0) + └── rates-v1.json (rates-v1.0) +``` + +--- + +## 2. 레이어링 표준 (controller / service / mapper / domain / dto) + +### 2-1. 레이어 책임 경계 + +| 레이어 | 책임 | 금지 | +|---|---|---| +| **Controller** | HTTP 바인딩, 입력 검증(`@Valid`), RBAC 가드 호출, 서비스 위임, `ApiResponse` 래핑 | 비즈니스 로직·SQL·트랜잭션·매퍼 직접 호출 | +| **Service (interface+Impl)** | 비즈니스 규칙, 트랜잭션 경계(`@Transactional`), 룰 엔진 호출, 매퍼 오케스트레이션, 도메인 예외 발생 | HTTP 타입(HttpServletRequest 등) 참조, 매퍼 XML 로직 침범 | +| **Mapper (MyBatis)** | DB 접근, 공간 SQL(`ST_*`) 바인딩. 인터페이스+XML 쌍 | 비즈니스 분기, DTO 조립(원시 `Map`/도메인 반환까지) | +| **DTO** | 계층·경계 데이터 전달(record 불변) | 로직·영속 어노테이션 | +| **domain / 값객체(선택)** | 순수 도메인 모델·계산(엔티티 매핑 시) | 프레임워크 의존 | + +### 2-2. 계층 관통 흐름 (표준) + +``` +Controller ──(가드: EventAccessGuard)──► Service(interface) + └► ServiceImpl ──► Mapper(@Mapper) ──► PostgreSQL/PostGIS + └──► RuleEngine(rules) (공간 SQL은 XML) + └──► RedisTemplate(비동기 큐/실시간) + 결과 DTO ◄── ServiceImpl ◄── Mapper(Map/도메인) +Controller ──► ApiResponse.ok(dto) | 예외 ──► GlobalExceptionHandler ──► ApiResponse.fail +``` + +- 컨트롤러는 **가드 호출 → 서비스 위임 → 봉투 래핑**만 한다(FloorplanController가 정본). 로직이 컨트롤러에 새면 위반. +- 서비스는 매퍼가 반환한 원시(`Map`/도메인)를 **DTO로 조립**한다. 매퍼는 DTO 조립을 하지 않는다. +- 공간 연산(부스 폴리곤·트렌치 KNN·배선 LineString·면적)은 **서비스가 아니라 매퍼 XML의 PostGIS SQL**로 수행하고 서비스는 스칼라/GeoJSON 결과만 사용한다(스캐폴드 `BoothMapper`·`WiringMapper` 계약). + +### 2-3. 트랜잭션·읽기 정책 + +- 쓰기 서비스 메서드는 `@Transactional`, 조회는 `@Transactional(readOnly=true)`. +- **낙관적 잠금**: 배치·설계 등 버전 있는 리소스는 `version` 불일치 시 `CONFLICT`(409). (LayoutSaveRequest·DesignSaveRequest에 `version` 존재.) +- **BI(M16)**: 운영 DB 직조회 금지 — KpiSnapshot/데이터마트(스타 스키마) 또는 읽기 전용 경로로 격리(PLANNING §8-1·M16-1, 상세는 data.md DA 트랙). + +--- + +## 3. 모듈 경계와 분류 + +### 3-1. 모듈 3계열 + 공통 레이어 + +| 계열 | 모듈 | 패키지 | 우선순위 | 비고 | +|---|---|---|---|---| +| **공통 레이어(선행 기반)** | 인증·시스템관리·공통업무기능 | `auth`·`common`·`module.m18`(system)·공통 모듈 | P1(전 모듈 선행) | §5B WISE/UIWS 이식 | +| **P0 부스 시공 코어(불변·심장)** | M2 플로어플랜·M3 부스설계·M4 유틸리티·M5 나노바나나 | `module.m2~m5` | **P0** | 스캐폴드 완비 | +| 판매·운영 | M1 배정견적·M6 서류·M7 매칭·M9 정산·M8 물류 | `module.m1·m6·m7·m9·m8` | P1/P2 | | +| 발주·계약 | **M15 공사/장치 옥션** | `module.m15` | **P1(핵심 플로우)** | 폐루프 연결고리 | +| 관람·참가·마케팅 | M10 관람객·M11 매칭·M12 마케팅/공개사이트·M13 wayfinding·M14 현장운영 | `module.m10~m14` | P1/P2 | | +| 경영·콘텐츠·관리 | M16 BI·M17 CMS·M18 관리자 | `module.m16·m17·m18` | P1 | | + +### 3-2. 공간 데이터 공유 원칙 (불변) + +M2(부스 폴리곤)→M3(부스 내부)→M4(배선)→M5(시각화)는 **하나의 PostGIS 공간 데이터 모델을 공유**한다. M13 wayfinding·M14 부하집계·M16 ㎡당 수익은 **동일 원천(Booth 폴리곤·Wiring LineString)을 재사용**한다. → 공간 지오메트리 소유는 **M2/M4 매퍼가 권위**이며, 소비 모듈은 조회만 한다(중복 저장 금지). + +### 3-3. 권위(ownership) 경계 — 중복 제거 (PLANNING §5B-2 규칙) + +| 관심사 | 권위 모듈 | 소비 모듈(읽기/이벤트) | +|---|---|---| +| 경영·수익 지표 | **M16 BI** | 대시보드·포털 | +| 일상 업무보고·통계 | 공통 `report/stats` | — | +| 콘텐츠·공지 발행 | **M17 CMS** | 공개사이트·사이니지 | +| 사내 알림성 공지 | 공통 `notice` | — | +| 알림 발송 채널 | 공통 `notification`(단일화) | M10·M12·M15(이벤트 발행) | +| 사용자·역할·공통코드·감사·마스터데이터 | **M18(=system)** | 전 모듈(RBAC·룰셋 공급) | +| 규정·요율 룰셋 | `rules` + M18(버전 관리) | M1·M2·M3·M4 | + +--- + +## 4. 의존성 규칙 (참조 방향·순환 금지) + +### 4-1. 허용 참조 방향 (단방향) + +``` +module.mN ──► rules · auth · common (횡단 계층 참조 허용) +module.mN ──► module.mK (오직 §4-2 표에 명시된 방향만, 하위→상위 데이터 소비) +common ──► (무의존) ★common은 어떤 module·auth·rules도 참조하지 않는다 +auth ──► common (에러·봉투만) +rules ──► common +config ──► auth · common (보안/웹소켓/레디스 배선) +``` + +**철칙**: `common`은 순수 횡단 유틸(봉투·에러·감사·페이징)로 **어떤 도메인/인증/룰도 모른다**. 도메인 모듈이 common을 참조하지, 그 역은 없다. + +### 4-2. 모듈 간 참조(도메인) — 명시 방향만 허용 + +PLANNING §4 모듈맵의 데이터 흐름을 코드 의존으로 옮긴다. **화살표 방향으로만 참조**(소비자→생산자 조회, 순환 금지). + +| 소비 모듈 | 참조(생산) 모듈 | 목적 | +|---|---|---| +| M3 → M2 | 부스 좌표·행사 역참조 | +| M4 → M2 | 트렌치·부스 지오메트리 | +| M5 → M2·M3·M4 | 씬 컴파일 입력(scene) | +| M15 → M2·M3·M4·M5·M7 | 옥션 자료 패키지·등록업체 검증 | +| M9 → M1·M4·M15 | 정산 대상(배정·유틸·낙찰) | +| M13 → M2 | wayfinding 지오메트리 | +| M14 → M4·M10 | 부하·체크인 파생 | +| M16 → 전 모듈 | 지표 소비(읽기 전용/스냅샷) | +| M12 → M10·M17 | 세그먼트·콘텐츠 | + +- **순환 금지**: 위 표에 역방향이 필요하면 **직접 참조 대신 이벤트(알림 큐)·공유 식별자**로 디커플. 예: M15 낙찰→M9는 M15가 M9를 호출하는 것이 아니라 **도메인 이벤트/발주 링크**로 전달(순환 회피). +- **모듈 간 결합은 서비스 인터페이스로만**: `mK.MKService`를 주입해 쓰고, 상대 모듈의 `mapper`·`ServiceImpl`·`dto` 내부를 직접 참조하지 않는다(계약 경유). +- **공간 원천**은 M2/M4 매퍼가 권위(§3-2) — 타 모듈은 그 서비스로 조회. +- 검증: 빌드 타임 아키텍처 테스트(ArchUnit 권장, tech.md TA 트랙)로 `common→module` 역참조·모듈 순환을 CI에서 차단. + +--- + +## 5. REST API 설계 표준 + +### 5-1. 경로·버전 + +- **베이스**: `/api`. 공개(비인증) 홍보/워커 경로는 `/api/public/**`·`/api/internal/**` 접두어로 분리. +- **행사 스코프 리소스**: `/api/events/{eventId}/…` 하위에 배치(모든 도메인 리소스는 `{eventId}` 스코프). 중첩 예: + - M2 `…/events/{eventId}/halls/{hallId}/layout` + - M3 `…/events/{eventId}/booths/{boothId}/design` + - M4 `…/events/{eventId}/booths/{boothId}/utility` + - M5 `…/events/{eventId}/booths/{boothId}/render` · `…/events/{eventId}/render-jobs/{jobId}` +- **플랫폼(비행사) 리소스**: `/api/admin/**`(M18·백오피스, `hasRole(ADMIN)` 게이트), `/api/auth/**`(인증), `/api/me/**`(개인). +- **버전 정책**: P0/P1은 무접두 `/api`(단일 버전). **파괴적 변경 시에만** `/api/v2/…` 도입. 계약 진화는 **후방호환 우선**(필드 추가는 non-breaking, 제거·의미변경만 버전 상향). 룰셋·계약 semver는 페이로드의 `rulesetVersion`으로 별도 표기(코드 API 버전과 분리). +- **동사 규약**: 자원 CRUD는 표준 HTTP 메서드. 비 CRUD 액션은 하위 동사 세그먼트(`/validate`·`/auto-generate`·`/precheck`·`/quote`·`/wiring`·`/order`·`/render`)로 표현(스캐폴드 실측 패턴). 액션은 POST. + +### 5-2. 응답 봉투 (ApiResponse — 스캐폴드 정본) + +모든 REST 응답은 `common.ApiResponse`를 사용한다(예외 없음). + +```json +{ "success": true, "data": { ... }, "error": null } +{ "success": false, "data": null, "error": { "code": "FORBIDDEN", "message": "요약 메시지" } } +``` + +- 성공은 컨트롤러가 `ApiResponse.ok(dto)`. 실패는 **던지고**(ApiException) `GlobalExceptionHandler`가 봉투로 변환(컨트롤러에서 실패 봉투 수동 조립 금지). +- **목록**: `common.PageResponse` = `{ items, page, size, total }`. (P0 갤러리/워크스페이스처럼 소량 고정 목록은 배열 직접 반환 허용 — 계약 §0-1.) + +### 5-3. 오류 코드 → HTTP (ErrorCode enum — 안정 계약) + +`common.error.ErrorCode`가 코드↔HTTP 단일 매핑. 신규 코드는 여기에만 추가한다. + +| code | 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 | 서버 오류(요약만) | + +- **미구현 지점**은 `ApiException.notImplemented(...)`(501) 표준 사용 — 계약은 확정하되 매퍼/워커 대기 구간 표시(스캐폴드 관례). +- 도메인 확장 코드(옥션 마감·배지 만료 등)는 계열 접두 없이 `ErrorCode`에 추가하고 본 표에 반영(AA 승인). + +### 5-4. 페이징·정렬·필터 + +- 쿼리 파라미터: `page`(0-base)·`size`(기본 20, 상한 100)·`sort=field,asc|desc`. 응답은 `PageResponse`. +- 필터는 명시 쿼리 파라미터(자유 텍스트 SQL 금지). 통합검색(공통 search)은 별도 검색 서비스 경유. + +### 5-5. 인증 헤더·공개 경로 + +- `Authorization: Bearer `(HS256). 클레임: `sub`(userId)·`name`·`roles`(eventId→역할)·`hm`(홀매니저)·(Phase B 확장) `plat`(플랫폼 역할 ADMIN 등)·`otp`(2FA 통과 플래그). +- **무상태**(SessionCreationPolicy.STATELESS). CSRF disable, CORS는 config에서 관리. +- **공개(permitAll)**: `GET /health`, `POST /api/auth/login`, `/ws/**`, `POST /api/internal/render/callback`(워커 토큰), (Phase D) `/api/public/**`(공개 홍보사이트 조회). 그 외 전부 인증. +- 내부 워커 콜백은 `X-Worker-Token`(env) 검증. 공개사이트는 읽기 전용(행사 데이터 쓰기 불가). + +### 5-6. 보안 불변 (API 계약 강제 — 위반 시 QA 반려) + +1. **스택트레이스·내부 세부 미노출** — `error.message`는 사람이 읽을 요약만, 상세는 서버 로그. (`server.error.include-*: never` + GlobalExceptionHandler.) +2. **민감정보 응답 완전 제외** — IP·SSH·비밀번호·`os_pw_enc`·해시·내부 식별자. 사용자/업체는 이름·역할·번호 등 비민감 필드만. +3. **`GEMINI_API_KEY`는 백엔드가 다루지 않는다** — 나노바나나 Python 워커 전용. M5는 큐 발행까지만. +4. **AI 생성 이미지 응답은 항상** `watermarkRequired:true`+`watermarkText`+`notice`(계약·심사 서류 사용 금지) 포함(제거 불가, PLANNING §6-5). +5. **admin 비번**은 env `ADMIN_PASSWORD_ENC`(AES-256-GCM)+별도 키파일 주입, `admin123` 하드코딩 금지(§5B-3). + +--- + +## 6. 인증·인가 아키텍처 (이중 RBAC) + +PLANNING §2 6역할·§8-1 SSO 이중 권한을 코드 모델로 표준화한다. 인증 스택은 **WISE/UIWS 표준 이식**(JWT+2FA/OTP), 그 위에 킨텍스 행사 RBAC를 얹는다(재설계 금지). + +### 6-1. 이중 권한 평가 + +| 계층 | 대상 | 저장/평가 | 게이트 | +|---|---|---|---| +| **플랫폼 역할(platform)** | ADMIN(백오피스), 셀프서비스(VISITOR/PUBLIC) | JWT `plat` 클레임 + Spring `hasRole` | `/api/admin/**`=`hasRole(ADMIN)`(§5B-1) | +| **행사 역할(event)** | ORGANIZER·EXHIBITOR·CONTRACTOR·HALL_MANAGER | JWT `roles`(eventId→역할)·`hm`, `KintexPrincipal.roleFor(eventId)` | `EventAccessGuard.requireRole(...)` | + +- **현행 스캐폴드**(P0): `EventRole`(4역할) + `KintexPrincipal.hallManager` 플래그 + `EventAccessGuard`(require/requireEventAccess/requireRole). 이 4역할 게이트가 정본. +- **Phase B 확장**: 플랫폼 역할(ADMIN)·관람객 셀프서비스 계정·2FA(OTP)·로그인 실패 잠금을 `auth`에 추가(WISE `TotpService` 이식). `EventRole`은 유지, 플랫폼 역할은 별도 축으로 평가(직교). + +### 6-2. 가드 사용 규약 (컨트롤러 표준) + +``` +guard.requireEventAccess(principal, eventId); // 열람: 멤버 or 홀매니저 +guard.requireRole(principal, eventId, EventRole.ORGANIZER); // 편집/액션: 역할 한정 +guard.requireRole(principal, eventId, EventRole.ORGANIZER, HALL_MANAGER); // 복수 허용 +``` + +- **열람=행사 멤버 or 홀매니저 / 편집·액션=역할별**(계약 §0-5). 홀매니저는 전 행사 열람+승인(`hasAccess`가 항상 true). +- **등록업체 게이트(불변)**: CONTRACTOR 초대 수락·M15 응찰은 `companyRegistrationNo` 킨텍스 등록업체 검증 필수 → 미등록 `NOT_REGISTERED_COMPANY`(403). M7이 검증 권위. +- 가드는 **컨트롤러에서** 호출한다(서비스 진입 전). 서비스는 이미 인가된 것으로 가정하되, 크로스-모듈 호출 시 재검증이 필요하면 호출 측이 책임. + +### 6-3. 개인정보·감사 + +- 리드캡처(M10)·관람객 데이터는 개인정보 — 동의·보존정책 필수(PLANNING R10). 접근은 소유 참가업체+주최자+홀매니저로 한정. +- **감사 대상**(§5B-1): 승인·**낙찰(M15)**·설계 변경·룰셋 개정·리드 접근을 `common.audit` AOP로 전수 기록(§7-3). + +--- + +## 7. 공통 컴포넌트 표준 (kintex-common / WISE 정합) + +공통 레이어는 `workspace/uiws`(WISE=GUARDiA 표준 프레임워크) 이식을 원칙으로 하되, 아래 컴포넌트는 **kintex 스캐폴드가 이미 정의한 계약을 정본**으로 삼는다(재설계 금지, 이식 시 정합). + +### 7-1. 응답 봉투·페이징 +- `common.ApiResponse`(record: success·data·error{code,message})·`common.PageResponse`. §5-2 정본. 모든 응답 필수. + +### 7-2. 예외 체계 +- `common.error.ErrorCode`(enum, HTTP 매핑) → `ApiException`(코드+요약 메시지) → `@RestControllerAdvice GlobalExceptionHandler`(봉투 변환·로그 격리). 3자 세트가 표준(§5-3). 신규 예외는 `ApiException`+`ErrorCode`만 사용(RuntimeException 남발 금지 — 최종 방어선만 `INTERNAL`). + +### 7-3. 감사 AOP (Phase B B-2/B-4) +- `common.audit.@Audited` 어노테이션 + AOP 어드바이스로 상태 변경 API를 `TB_AUDIT_LOG`에 기록(액터·행사·대상·before/after 요약·룰셋 버전). **민감정보·비번·스택트레이스 미기록**(§5-6 정합). WISE `TB_AUDIT_LOG` 스키마 이식. + +### 7-4. 공통코드 (Phase B) +- `common.code`가 코드 그룹/상세를 캐시 제공(홀·부스유형·공종 14분류·유틸리티 요금코드 등 도메인 코드 포함). 권위는 M18(system). 도메인 모듈은 하드코딩 대신 공통코드 조회. + +### 7-5. 룰셋(버전 관리 데이터) +- `rules`가 `rulesets/*.json`(compliance·rate)을 로드·평가. **코드가 아닌 데이터** — 개정 시 파일 교체·`rulesetVersion` 리포트 기록(감사·면책, PLANNING R2). 연산자: `lte·gte·between·isTrue·eq·lteHall·excludesAll`. + +### 7-6. 알림 단일화 (§5B-2) +- 발송 채널은 공통 `notification` 단일. 도메인 모듈(M10·M12·M15)은 직접 발송하지 않고 **이벤트를 발행**한다(마감 리마인더·낙찰·승인·결제 알림). WebSocket 실시간 경로는 §8. + +### 7-7. 프론트 공통(FE, Phase B B-4) +- 2FA 화면·공통코드·검색바·그리드·달력·모달·파일업로드는 **공유 컴포넌트 라이브러리**로(WISE 이식, design.md 토큰 정합). 역할별 포털이 상속(중복 구현 금지). + +--- + +## 8. WebSocket 이벤트 규격 (STOMP) + +`config.WebSocketConfig` 정본. 실시간 진행/이벤트 푸시는 STOMP over WebSocket으로만 한다(REST 폴링 지양). + +- **핸드셰이크**: `GET /ws`(SockJS). 공개 경로(핸드셰이크 후 STOMP CONNECT 헤더에 JWT 전달 — 인가는 구독 시점 평가). +- **prefix**: 서버→클라 브로드캐스트 `/topic`, 클라→서버 `/app`. +- **토픽 네이밍 표준**: `/topic/<도메인>/<식별자>`. + +| 토픽 | 이벤트 | 발행 시점 | 대상 | +|---|---|---|---| +| `/topic/render/{jobId}` | `RenderJobDto`(DONE/FAILED) | 워커 콜백 relay(M5) | 발행 멤버 | +| `/topic/auction/{auctionId}` | 순위/라운드 마감(M15) | 응찰·타이머 | 옥션 참여 업체 | +| `/topic/events/{eventId}/notifications` | 알림(승인·마감·결제) | 공통 notification | 행사 멤버 | +| `/topic/events/{eventId}/checkin` | 입장/혼잡(M10·M14) | 체크인 | 홀매니저/주최자 | + +- **페이로드는 REST DTO 재사용**(RenderJobDto 등) — 별도 WS 전용 스키마 금지(계약 일원화). +- **인가**: 구독 대상이 행사/부스 스코프면 CONNECT 시 신원 + 구독 시 접근 검증(민감 토픽 무단 구독 차단). 브로드캐스트에도 §5-6 민감정보 제외 동일 적용. +- 나노바나나·서류·알림은 **동일 비동기 패턴**: REST가 Redis 큐 발행 → 워커/서비스 처리 → WS 완료 푸시. + +--- + +## 9. 비동기·큐 계약 (Redis · Python 워커) + +- **RenderJob 큐**: `kintex:renderjob:queue`(env `RENDER_QUEUE_KEY`). 백엔드가 scene 페이로드(§6-2 PLANNING) leftPush → Python 워커 소비. 상태 `kintex:renderjob:job:{jobId}`, 쿼터 `kintex:renderjob:quota:{eventId}`(스캐폴드 실측 키). +- **성공 시에만 쿼터 차감**(PLANNING §6-5). 실패 에러는 `safeError`로 요약만 통과(스택트레이스 유입 차단). +- **워커 결합은 얇은 큐 계약으로만** — 백엔드는 큐잉·상태·콜백·WS relay만, 나노바나나 실호출·방어 로직은 워커(§6-4 PLANNING). `GEMINI_API_KEY` 백엔드 미접촉. +- 옥션 실시간 순위·라운드 마감 타이머, 서류/알림 생성도 Redis 재사용(동일 패턴). BI 집계는 배치/스냅샷(§2-3). +- **G1 게이트**: Gemini 외부 호출 미승인 시에도 큐잉/상태는 동작(목/degraded). 실호출·배포는 소유자 승인 후. + +--- + +## 10. 역할별 프론트/백엔드 모듈화 원칙 + +### 10-1. 프론트 — 역할별 번들 분리 (PLANNING §2-1·§8-1) + +6개 프론트를 **역할별 번들·도메인/서브패스 분리**로 배포해 최소권한·공격면 축소. **공유 디자인 시스템·공유 컴포넌트·공유 API 계약을 상속**(중복 구현 금지). + +| 프론트 | 도메인(예) | 주 사용 모듈 | 채널 | +|---|---|---|---| +| 주최자 콘솔 | `organizer.` | M1·M2·M6·M15·M16·M12 | 데스크톱 주력 | +| 참가업체 포털 | `exhibitor.` | M3·M4·M5·M10·M11·M15·M9 | 데스크톱+모바일(리드캡처) | +| 업체 포털 | `contractor.` | M3·M4·M15·M8·M7 | 데스크톱+모바일(현장) | +| 운영 대시보드 | `ops.` | M2·M6·M8·M14·M16 | 데스크톱+모바일(검수) | +| 관리자 백오피스 | `admin.` | M18 | 웹 전용 | +| 공개/관람객 | `www`·`expo.` | M12·M10·M11·M13·M17 | 공개 SEO/SSR + 관람객 모바일 | + +- **공유 계층**(모노레포 워크스페이스 권장): `packages/api-client`(계약 타입·fetch 래퍼·ApiResponse 언랩), `packages/ui`(공유 컴포넌트·디자인 토큰 `tokens.css`), `packages/auth`(JWT·2FA·라우팅 가드). 각 포털 앱은 이를 의존(역참조 금지). +- **기술 표준(스캐폴드)**: React 18 + Vite + TS, `react-router-dom`·`@tanstack/react-query`(서버 상태)·`zustand`(클라 상태)·`@stomp/stompjs`+`sockjs-client`(WS). 상세 빌드·라우팅은 tech.md(TA). +- **공개 홍보사이트(M12/M17)**: SEO/SSR·다국어(한/영/중/일)·CDN — 인증 앱과 **별도 렌더 경로**(공개 성능·검색 노출). 쓰기 불가. + +### 10-2. 백엔드 — 단일 공유 모놀리식(모듈러) (PLANNING §8-1) + +- **공유 Spring Boot 백엔드 1개**(모든 포털이 SSO+RBAC로 접근). 역할별로 백엔드를 쪼개지 않는다 — **모듈러 모놀리스**(`module.mN` 경계 + §4 의존 규칙)로 경계를 코드 레벨에서 강제. +- API 노출은 경로 접두(`/api/events/**`·`/api/admin/**`·`/api/public/**`)와 RBAC로 역할별 표면을 나눈다(별도 서비스 아님). +- 장래 서비스 분리가 필요하면 §4 모듈 경계가 분할선(느슨한 결합·이벤트 디커플이 선행 조건). + +--- + +## 11. 신규 모듈 추가 체크리스트 (구현 에이전트용) + +새 도메인 모듈(mN) 추가 시 본 표준 준수 확인: + +1. 패키지 `com.zioinfo.kintex.module.mN` + 4계층(Controller·Service/Impl·dto·mapper) 생성(§1-2). +2. 컨트롤러는 가드→위임→`ApiResponse` 래핑만(§2-2, FloorplanController 패턴 복제). +3. 경로 `/api/events/{eventId}/…`(행사 스코프) 또는 `/api/admin/**`(플랫폼)(§5-1). +4. DTO는 record, 목록은 `PageResponse`, 오류는 `ApiException`+`ErrorCode`(§5-2/5-3). +5. 공간 데이터는 M2/M4 매퍼 권위 재사용(§3-2), 신규 지오메트리만 자기 매퍼 XML(PostGIS). +6. 크로스 모듈은 상대 `Service` 인터페이스로만, §4-2 방향 준수·순환 금지(이벤트 디커플). +7. 실시간은 `/topic/<도메인>/` STOMP, 비동기는 Redis 큐(§8/§9). +8. 감사 대상 액션에 `@Audited`(§7-3), 알림은 `notification` 이벤트 발행(§7-6). +9. 보안 불변 5종(§5-6) 자체 점검 → QA 반려 방지. +10. 미완 구간은 `ApiException.notImplemented(...)`(501)로 계약만 확정(스캐폴드 관례). + +--- + +## 12. 교차 아키텍처 참조 (링크) + +- 시스템·NFR·배포 토폴로지 → `docs/architecture/system.md`(SA) +- 기술 표준·빌드/관측성·AiTextRouter → `docs/architecture/tech.md`(TA) +- 전사 ERD·공간데이터·마스터·BI 데이터마트 → `docs/architecture/data.md`(DA) +- DMZ/내부망·방화벽·외부 아웃바운드(Gemini) → `docs/architecture/network.md`(NA) +- P0 백엔드 API 계약(정본 예시) → `_workspace/01_backend_contracts.md` +- 기획·모듈 정의 → `docs/PLANNING.md` v2.0 · 실행 → `docs/IMPLEMENTATION_BACKLOG.md` + +--- + +## 13. 변경 이력 + +| 버전 | 일자 | 작성자 | 내용 | +|---|---|---|---| +| v1.0 | 2026-07-11 | AA | 최초 — A-1. 패키지 구조(`com.zioinfo.kintex`)·4계층 레이어링·모듈 경계(P0 코어 M2~M5·도메인 M10~M18·공통 레이어 §5B)·의존성 규칙(common 무의존·모듈 단방향·순환 금지)·REST 표준(경로/버전/봉투/에러/페이징/인증)·이중 RBAC(플랫폼+행사)·WebSocket STOMP 규격·공통 컴포넌트(WISE 정합)·Redis 큐 계약·역할별 프론트 번들 분리 + 모듈러 모놀리스 백엔드. 스캐폴드(`src/backend`) 실측 정합, PLANNING v2.0 §8 정합. | diff --git a/docs/architecture/data.md b/docs/architecture/data.md new file mode 100644 index 0000000..fa36fe4 --- /dev/null +++ b/docs/architecture/data.md @@ -0,0 +1,696 @@ +# 킨텍스 자동전시시스템 — 데이터 아키텍처 (A-4) + +> 작성: kintex-data-architect(DA) · 작성일: 2026-07-11 · 버전: **v1.0** +> 근거: [`docs/PLANNING.md`](../PLANNING.md) v2.0(§5A M15/M16·§5B 공통코드·§7 ERD·§8 아키텍처) · [`_workspace/01_backend_contracts.md`](../../_workspace/01_backend_contracts.md)(P0 API 계약·§8 매퍼 인수) · [`docs/COMMON_CODES.md`](../COMMON_CODES.md)(공통코드) · [`docs/assets/floorplans/README.md`](../assets/floorplans/README.md)(홀 실측·트렌치 CAD) · 룰셋 `rulesets/compliance-v1.json`·`rates-v1.json` +> **문서 소유권(DA 트랙)**: 본 문서는 **데이터 모델·표준·거버넌스의 단일 출처**다. 물리 스키마(DDL·PostGIS·MyBatis 매퍼 XML) **구현은 kintex-db-engineer**가 담당하며, 본 문서는 그 구현 대상(target model)·표준·검수 기준을 정의한다. DA는 설계·표준·검수만 하고 `src/backend/**/db`·매퍼는 수정하지 않는다. +> 정합 대상: A-1 app.md(AA)·A-2 system.md(SA)·A-3 tech.md(TA)·A-5 network.md(NA) — 상충 발견 시 A-6(reviewer) 티켓화. + +--- + +## 0. 범위·계층·원칙 + +### 0-1. 데이터 아키텍처 스코프 + +| 계층 | 대상 | 저장소 | +|---|---|---| +| 공통·시스템관리 (WISE/UIWS 이식) | 사용자·역할·공통코드·메뉴·감사·업무모듈 | PostgreSQL `TB_*` | +| 도메인 (킨텍스 코어 P0) | 행사·홀·부스·설계·유틸리티·렌더잡 | PostgreSQL + **PostGIS** | +| 도메인 (v2.0 확장) | 옥션·관람객/리드·CMS·마스터데이터 | PostgreSQL | +| 마스터·룰셋 (버전 관리 데이터) | 홀·요율·유틸요금·규정 룰셋·등록업체 | 파일(룰셋 JSON) + `TB_*` 마스터 | +| BI 데이터마트 (M16) | Fact/Dim 스타 스키마 + KpiSnapshot | PostgreSQL(별도 스키마 `mart`) / 읽기 전용 복제 | +| 대용량 바이너리 | 도면·생성 이미지·서식·견적 PDF | 오브젝트 스토리지(경로만 DB) | + +### 0-2. 설계 원칙 (불변) + +1. **단일 공간 원천**: 부스 폴리곤·트렌치 포인트·배선 LineString은 **PostGIS 단일 지오메트리 원천**. M2 검증·M4 라우팅·M5 시각화·M13 wayfinding·M14 부하집계·M16 ㎡당 수익이 **같은 지오메트리를 재사용**(PLANNING §7-3 불변, §4 설계원칙 (1)). +2. **룰셋은 데이터**: 요율·규정은 코드가 아닌 **버전 관리 파일**(`compliance-v*.json`·`rates-v*.json`). 모든 산출물에 `rulesetVersion`·`disclaimer` 각인(감사·면책). 마스터데이터 개정은 M18 백오피스에서 무중단 반영. +3. **PII 최소수집·분리·암호화**: 관람객/리드 개인정보는 §5 분류·보존·동의·암호화 정책을 강제. 민감 컬럼은 API 응답에서 완전 제외(계약 §0-3). +4. **운영/분석 분리**: 경영 지표는 운영 DB 직조회 금지 — **BI 데이터마트(스타 스키마)** 배치 적재 또는 읽기 전용 복제로 운영 부하 회피(PLANNING §8-1). +5. **이식 우선(재설계 금지)**: 공통·시스템·인증 스키마는 `workspace/uiws` `TB_*`를 이식(멱등 DDL). 킨텍스 고유는 도메인 테이블에만. + +--- + +## 1. 전사 데이터 모델 — 개념(Conceptual) + +### 1-1. 개념 ERD (도메인 영역) + +```mermaid +erDiagram + EVENT ||--o{ HALL_ASSIGNMENT : "배정" + HALL ||--o{ HALL_ASSIGNMENT : "가용" + HALL_ASSIGNMENT ||--o{ LAYOUT : "배치안(버전)" + LAYOUT ||--o{ BOOTH : "부스(폴리곤)" + HALL ||--o{ TRENCH : "트렌치 그리드" + BOOTH ||--o{ DESIGN_PLAN : "설계안(버전)" + BOOTH ||--o{ UTILITY_ORDER : "유틸리티 신청" + UTILITY_ORDER ||--o{ WIRING_PATH : "배선(LineString)" + BOOTH ||--o{ RENDER_JOB : "시각화 샷" + EVENT ||--o{ EVENT_MEMBER : "참여자(RBAC)" + USER ||--o{ EVENT_MEMBER : "소속" + COMPANY ||--o{ EVENT_MEMBER : "업체계정" + + EVENT ||--o{ AUCTION : "옥션(M15)" + AUCTION ||--o{ QUOTATION : "견적서=응찰" + AUCTION ||--o| AWARD : "낙찰" + COMPANY ||--o{ QUOTATION : "응찰업체(등록검증)" + BOOTH ||--o{ AUCTION : "자료첨부" + + EVENT ||--o{ REGISTRATION : "관람객 등록(M10)" + VISITOR ||--o{ REGISTRATION : "관람객" + REGISTRATION ||--o{ BADGE : "배지/QR" + BADGE ||--o{ CHECK_IN : "체크인" + BOOTH ||--o{ LEAD : "리드캡처" + VISITOR ||--o{ LEAD : "스캔대상" + + EVENT ||--o{ SETTLEMENT : "정산(M9)" + EVENT ||--o{ DOCUMENT : "서류/마일스톤(M6)" + EVENT ||--o{ CONTENT : "CMS(M17)" + + EVENT ||--o{ FACT_MART : "BI 집계(M16)" +``` + +### 1-2. 주제영역(Subject Area) 맵 + +| 주제영역 | 핵심 엔티티 | 소유 모듈 | 특성 | +|---|---|---|---| +| **행사·조직·권한** | Event, User, Company, EventMember, Role | §5B·M18 | 마스터·RBAC 기준축 | +| **공간·시설** | Hall, HallFeature, Trench | M2·마스터 | PostGIS 지오메트리 | +| **설계·시공(P0)** | Layout, Booth, DesignPlan, UtilityOrder, WiringPath, RenderJob | M2~M5 | 버전·공간·비동기 | +| **발주·정산** | Auction, Quotation, Award, Settlement, PaymentSchedule, Document | M6·M9·M15 | 금액·계약·감사 | +| **관람·참가** | Visitor, Registration, Badge, CheckIn, Lead, Meeting | M10·M11 | **PII 집중 영역** | +| **콘텐츠·마스터** | Content, Microsite, MasterData, Ruleset | M17·M18 | 다국어·버전 | +| **분석(BI)** | FactBooking/Settlement/Utility/Auction/Visitor, Dim*, KpiSnapshot | M16 | 스타 스키마·집계 | +| **공통·감사** | AuditLog, CodeGroup, Code, Menu, Notification | §5B | 이식·전 모듈 공유 | + +--- + +## 2. 전사 데이터 모델 — 논리·물리(Logical/Physical) + +> 물리 테이블은 kintex-db-engineer가 구현. 아래는 **표준 대상 모델**(테이블·컬럼·타입·제약). 명명 규칙은 §4. `geom` 컬럼 상세는 §3. + +### 2-1. 코어 P0 물리 ERD + +```mermaid +erDiagram + TB_EVENT { + uuid event_id PK + varchar event_name + date start_date + date end_date + varchar status + timestamptz created_at + } + TB_HALL { + varchar hall_id PK "H1..H10, 반홀 H1A" + varchar hall_name + numeric area_m2 + numeric floor_load_t_per_m2 + numeric width_m + numeric depth_m + numeric ceiling_m + varchar floor_finish "concrete_polished|carpet" + int booth_capacity + geometry footprint "Polygon,0" + } + TB_HALL_ASSIGNMENT { + uuid assignment_id PK + uuid event_id FK + varchar hall_id FK + date occupy_from + date occupy_to + } + TB_BOOTH { + uuid booth_id PK + uuid layout_id FK + varchar booth_no "A-102" + varchar booth_type "independent|assembled" + numeric width_m + numeric depth_m + numeric height_m + numeric floor_load_t_per_m2 + boolean premium + uuid assigned_company_id FK "nullable" + geometry geom "Polygon,0 · 홀로컬" + } + TB_LAYOUT { + uuid layout_id PK + uuid assignment_id FK + int version + varchar name + varchar status "draft|submitted|approved|rejected" + int source_option "선택/병합 출처 안번호" + jsonb merge_provenance "병합 출처 레이어" + timestamptz updated_at + } + TB_TRENCH { + uuid trench_id PK + varchar hall_id FK + varchar supply_matrix "power,water,air,gas,network 비트/배열" + boolean assumed "가정 그리드 여부(R4)" + geometry geom "Point,0 · 탭포인트" + geometry run_geom "LineString,0 · nullable" + } + TB_DESIGN_PLAN { + uuid design_id PK + uuid booth_id FK + int version + varchar status + jsonb spec "DesignSpec" + timestamptz updated_at + } + TB_UTILITY_ORDER { + uuid order_id PK + uuid booth_id FK + varchar status "draft|submitted|relayed" + jsonb quote "UtilityQuote 스냅샷" + varchar rateset_version + varchar location_diagram_url + timestamptz created_at + } + TB_WIRING_PATH { + uuid wiring_id PK + uuid order_id FK + varchar kind "power|network|plumbing|air" + numeric kw "nullable" + numeric length_m + geometry geom "LineString,0" + } + TB_RENDER_JOB { + uuid job_id PK + uuid booth_id FK + uuid event_id FK + varchar shot_preset "S1..S7" + varchar status "QUEUED|RUNNING|DONE|FAILED" + varchar image_url + varchar schema_hash "캐시키" + varchar model_version + varchar error_message "요약만" + timestamptz created_at + } + TB_COMPANY { + uuid company_id PK + varchar company_name + varchar registration_no "사업자번호(등록검증)" + varchar category_code "14분류 CONTRACTOR_CATEGORY" + varchar region + boolean kintex_registered "미등록 응찰 차단 게이트" + } + TB_EVENT_MEMBER { + uuid member_id PK + uuid event_id FK + uuid user_id FK + uuid company_id FK "nullable" + uuid booth_id FK "nullable · 참가업체 부스 스코프" + varchar event_role "ORGANIZER|EXHIBITOR|CONTRACTOR|HALL_MANAGER" + } + + TB_EVENT ||--o{ TB_HALL_ASSIGNMENT : "" + TB_HALL ||--o{ TB_HALL_ASSIGNMENT : "" + TB_HALL ||--o{ TB_TRENCH : "" + TB_HALL_ASSIGNMENT ||--o{ TB_LAYOUT : "" + TB_LAYOUT ||--o{ TB_BOOTH : "" + TB_BOOTH ||--o{ TB_DESIGN_PLAN : "" + TB_BOOTH ||--o{ TB_UTILITY_ORDER : "" + TB_UTILITY_ORDER ||--o{ TB_WIRING_PATH : "" + TB_BOOTH ||--o{ TB_RENDER_JOB : "" + TB_COMPANY ||--o{ TB_BOOTH : "배정" + TB_EVENT ||--o{ TB_EVENT_MEMBER : "" +``` + +**계약 정합 근거**(01_backend_contracts §8 매퍼 인수 목록): +- `BoothMapper` → `TB_BOOTH.geom`(ST_MakePolygon/ST_AsGeoJSON), 판매면적 `ST_Area(geom)`, 통로폭 `ST_Distance/ST_Buffer`, 비상구 `ST_Intersects(TB_HALL_FEATURE)`. +- `DesignMapper` → `TB_DESIGN_PLAN.spec`(jsonb), `findEventIdByBooth`(RBAC 역참조 = TB_BOOTH→TB_LAYOUT→TB_HALL_ASSIGNMENT→event_id). +- `WiringMapper` → `TB_TRENCH` KNN(`geom <-> :point`), `TB_WIRING_PATH.geom` 최단(ST_Length), `TB_TRENCH.assumed` 플래그. +- `RenderJobMapper` → `TB_RENDER_JOB` 내구 이력·쿼터 정본(`countSucceededByEvent`, Redis는 큐/실시간). +- `UserMapper` → `TB_USER`(§2-3) 인증행(해시 응답 제외)·`TB_EVENT_MEMBER` 역할. + +### 2-2. v2.0 확장 물리 ERD (옥션·관람·CMS) + +```mermaid +erDiagram + TB_AUCTION { + uuid auction_id PK + uuid event_id FK + varchar auction_type "REVERSE|RFQ|FIXED" + varchar category_code "공종 14분류" + varchar status "OPEN|BIDDING|AWARDED|CLOSED" + int round_no + timestamptz deadline_at + varchar award_criteria "LOWEST|COMPOSITE" + jsonb weight "가격/평판/납기 가중치" + jsonb attached_refs "Booth/Design/Utility/Render 참조 자료" + } + TB_QUOTATION { + uuid quotation_id PK + uuid auction_id FK + uuid company_id FK "등록업체 검증" + int version + varchar status "SUBMITTED|REVISED|AWARDED|REJECTED" + jsonb line_items "공종·자재·수량·단가·금액" + numeric subtotal + numeric vat + numeric total + date valid_until + varchar lead_time + varchar pdf_url + timestamptz submitted_at + } + TB_AWARD { + uuid award_id PK + uuid auction_id FK + uuid quotation_id FK "선정 견적서" + numeric composite_score + varchar reason + varchar contract_doc_url "M6/M9 연동" + timestamptz awarded_at + } + TB_VISITOR { + uuid visitor_id PK + varchar name_enc "PII·AES-GCM" + varchar email_enc "PII·AES-GCM" + varchar phone_enc "PII·AES-GCM" + varchar org_name "준식별" + varchar job_title + jsonb interests "관심 업종" + varchar visitor_type "VISITOR|BUYER" + timestamptz created_at + } + TB_REGISTRATION { + uuid registration_id PK + uuid event_id FK + uuid visitor_id FK + varchar reg_type + boolean consent_privacy "동의(필수)" + boolean consent_marketing "동의(선택·정보통신망법)" + timestamptz consent_at + timestamptz registered_at + } + TB_BADGE { + uuid badge_id PK + uuid registration_id FK + varchar qr_token "회전 토큰·비추측" + varchar badge_template_id "M17" + } + TB_CHECK_IN { + uuid checkin_id PK + uuid badge_id FK + timestamptz checked_at + varchar gate + } + TB_LEAD { + uuid lead_id PK + uuid event_id FK + uuid booth_id FK "참가업체 스코프" + uuid visitor_id FK + int interest_score + varchar memo_enc "PII·AES-GCM" + boolean consent_share "리드 공유 동의" + timestamptz captured_at + } + TB_CONTENT { + uuid content_id PK + uuid event_id FK "nullable" + varchar content_type + varchar locale "ko|en|zh|ja" + int version + varchar status "draft|review|published" + jsonb body + } + + TB_AUCTION ||--o{ TB_QUOTATION : "" + TB_AUCTION ||--o| TB_AWARD : "" + TB_QUOTATION ||--o| TB_AWARD : "선정" + TB_VISITOR ||--o{ TB_REGISTRATION : "" + TB_REGISTRATION ||--o{ TB_BADGE : "" + TB_BADGE ||--o{ TB_CHECK_IN : "" + TB_VISITOR ||--o{ TB_LEAD : "" +``` + +**보조 테이블**(도메인 완결): `TB_SETTLEMENT`(정산·M9)·`TB_PAYMENT_SCHEDULE`(납부 스케줄 20/30/20/30 + 예치금)·`TB_DOCUMENT`(서류·마일스톤 D-150/30/25/7·M6)·`TB_MEETING`(비즈매칭·M11)·`TB_MICROSITE`(참가업체·M17)·`TB_MASTER_DATA`(마스터 버전·M18). 상세 컬럼은 해당 도메인 에이전트 확정 시 본 문서 갱신. + +### 2-3. 공통·시스템관리 물리 모델 (WISE/UIWS 이식 — 정본 참조) + +> **재설계 금지**: 아래는 `workspace/uiws` `TB_*` 정본을 **그대로 이식**(멱등 DDL·`sql.init mode=always`+continue-on-error). 킨텍스는 표준 준수만 하고 컬럼을 임의 변경하지 않는다. 상세 컬럼 정의는 UIWS 레퍼런스가 정본. + +| 테이블 | 역할 | 킨텍스 접합 | +|---|---|---| +| `TB_USER` | 사용자·인증(BCrypt 해시·`otp_secret` AES) | 6역할 + 등록업체 계정 + 관람객 셀프서비스 | +| `TB_CODE_GRP` / `TB_CODE` | 공통코드 그룹/값 | §4-3 도메인 코드 적재 | +| `TB_MENU` | 메뉴 트리·권한 매핑 | 역할별 포털 IA(§2-1) | +| `TB_AUDIT_LOG` | 감사 로그 | 승인·**낙찰**·설계변경·룰셋개정·**리드 접근(PII)** 전수 | +| `TB_NOTIFICATION` | 통합 알림 | D-데이 리마인더·낙찰·결제 | +| worklog/schedule/message/notice/meeting/report 등 | 공통 업무 | §5B-2 접합점만 이식 | + +- **인증 표준(§5B-3)**: JWT + TOTP(RFC6238, SHA1·30s·6자리·±1) 2차 인증 + 로그인 실패 잠금. `admin` 비번은 env `ADMIN_PASSWORD_ENC`(AES-256-GCM) + 별도 키파일 복호 → 기동 시 BCrypt 재시드. **하드코딩 시드 금지**. +- **행사 RBAC 이중 평가**: 전역 `USER_ROLE`(WISE) + 행사 스코프 `EVENT_ROLE`(TB_EVENT_MEMBER) 병행. 킨텍스 1차 권한 = `EVENT_ROLE`(COMMON_CODES §3). + +--- + +## 3. 공간 데이터 모델 표준 (PostGIS) + +> M2~M5·M13·M14·M16이 공유하는 **단일 공간 원천**. 물리 구현(ST_* 매퍼 XML)은 db-engineer, 좌표계·타입·인덱스·검증 계약은 본 절이 표준. + +### 3-1. 좌표계 표준 — 홀 로컬 데카르트 + +| 항목 | 표준 | 근거 | +|---|---|---| +| **SRID** | **`0`(로컬 데카르트, 미터)** — 지리좌표(4326) 아님 | 부스/트렌치/배선은 홀 로컬 미터 좌표(계약 `polygon`=홀 로컬 미터, `[[0,0],[6,0]...]`) | +| 타입 | **`geometry`**(geography 아님) | 평면 미터 연산: `ST_Area`=㎡ 직접, `ST_Distance`=m 직접, `ST_Length`=m 직접 | +| 원점 | 홀별 원점(도면 좌하단) 기준, hall_id로 좌표계 분리 | 홀마다 독립 로컬 원점 | +| 단위 | 미터(m). 각도는 도(°) | 계약 `sizeM`·`heightM`·`lengthM` | +| 정밀도 | 좌표 소수 3자리(mm), 면적/길이 소수 2자리 | 시공 실무 정밀도 | + +> **주의(교차 좌표계 금지)**: 홀 로컬 좌표는 홀 간 직접 공간연산 불가(각 홀 원점 상이). 홀 전경(S7)·부지 컨텍스트가 필요하면 별도 `venue` 좌표계 변환 테이블로 배치(Phase 2). Phase 1은 단일 홀 기준(홀7 권장, PLANNING §9). + +### 3-2. 지오메트리 컬럼 표준 + +| 엔티티 | 컬럼 | PostGIS 타입 | 규칙 | +|---|---|---|---| +| **부스** `TB_BOOTH` | `geom` | `geometry(Polygon, 0)` | 닫힌 링(첫=끝 좌표), 단순(ST_IsSimple)·유효(ST_IsValid), CCW 권장 | +| **홀 외곽** `TB_HALL` | `footprint` | `geometry(Polygon, 0)` | 홀 경계 | +| **홀 시설** `TB_HALL_FEATURE` | `geom` | `geometry(Geometry, 0)` | 기둥(Point Ø2.5m 버퍼)·비상구(Point/LineString)·셔터·화장실 — `feature_type` 구분 | +| **트렌치** `TB_TRENCH` | `geom` | `geometry(Point, 0)` | 탭/액세스 포인트(KNN 대상). `run_geom geometry(LineString,0)` 옵션(트렌치 런) | +| **배선** `TB_WIRING_PATH` | `geom` | `geometry(LineString, 0)` | 트렌치→단말 경로. `kind`별 1행 | + +### 3-3. 공간 연산 계약 (매퍼 XML 대상 — db-engineer 인수) + +| 용도 | 연산 | 규정/계약 매핑 | +|---|---|---| +| 판매면적 | `ST_Area(geom)` (m²) | LayoutSummary `salesAreaM2`, BI ㎡당 수익 | +| 통로 폭 최소 | `ST_Distance` + `ST_Buffer`(부스 간극) | 규정 `AISLE_WIDTH_MIN`(≥3m·block) | +| 비상구 차단 | `ST_Intersects(booth, exit_access_zone)` count | 규정 `EXIT_ACCESS`(=0·block) | +| 최근접 트렌치 | KNN `geom <-> :point ORDER BY … LIMIT k` | `WiringMapper.findNearestTrenches` | +| 최단 배선 | 경로 LineString `ST_Length` (통로 횡단 최소 휴리스틱) | `WiringMapper.shortestPath`, `WiringResult.lengthM` | +| 부스 겹침 | `ST_Overlaps` / `ST_Intersects` 자기조인 | 배치 무결성(솔버 후검증) | +| 홀 이탈 | `ST_Contains(hall.footprint, booth.geom)` | 부스가 홀 경계 내 | + +- **가정 트렌치(R4)**: 실측 미확보 홀은 공개 규격 기반 가정 그리드 → `TB_TRENCH.assumed=true`. `WiringResult.assumedTrench=true` → 프론트 "가정 트렌치 좌표(실측 대기)" 배지(계약 §5). CAD 트렌치 실측(`평면,트렌치.dwg`, floorplans README) 확보 시 홀 단위 교체·`assumed=false`. +- **좌표 검증 게이트**: 저장 전 `ST_IsValid`·닫힌 링·홀 내포 검증 실패 시 `VALIDATION`(400). 무효 지오메트리 저장 금지. + +### 3-4. 공간 인덱스·성능 표준 + +- 전 `geom` 컬럼 **GiST 인덱스**(`USING gist(geom)`) 필수. 트렌치 KNN·통로 버퍼·비상구 교차의 실시간 응답 근거. +- 부스 수 홀당 200~600(PLANNING). 배치 검증은 홀 단위 배치(bounding box 선필터 후 정밀 연산). +- 대량 좌표는 서버 산출값 권위 — 프론트 좌표는 참고, 규정 판정은 PostGIS 산출값과 대조(compliance-v1 `AISLE_WIDTH_MIN.note`). + +--- + +## 4. 데이터 표준 (명명·코드·마스터) + +### 4-1. 명명 규칙 (Naming Convention) + +| 대상 | 규칙 | 예 | +|---|---|---| +| 테이블 | `TB_` + `UPPER_SNAKE`(단수) — WISE 표준 계승 | `TB_BOOTH`, `TB_AUCTION` | +| 컬럼(물리) | `snake_case`, PostgreSQL 무인용 소문자(대소문자 혼용·인용식별자 금지) | `booth_id`, `floor_load_t_per_m2` | +| PK | `<엔티티>_id`, **UUID**(도메인) / WISE 이식 테이블은 정본 PK 유지 | `event_id`, `job_id` | +| FK | 참조 PK명 동일 | `TB_BOOTH.layout_id` | +| 지오메트리 | `geom`(주 지오메트리) / `<용도>_geom` | `geom`, `footprint`, `run_geom` | +| 암호화 PII | `<필드>_enc` 접미 | `email_enc`, `otp_secret`(WISE) | +| 코드 컬럼 | `<의미>_code` 또는 상태 `status` | `category_code`, `status` | +| 불리언 | `is_`/동사 또는 `_yn`(WISE 공통은 `USE_YN`) | `premium`, `assumed`, `use_yn` | +| 시각 | `*_at`(`timestamptz`), 날짜 `*_date`(`date`) | `created_at`, `deadline_at`, `start_date` | +| 금액 | `numeric`, 원(KRW) 정수 스케일, 통화 `currency` 명시 | `total`, `vat` | +| BI 마트 | 팩트 `FACT_*`, 차원 `DIM_*`, 스냅샷 `KPI_SNAPSHOT` | `FACT_BOOKING`, `DIM_HALL` | + +- **DTO(camelCase) ↔ 컬럼(snake_case) 매핑**: MyBatis `mapUnderscoreToCamelCase=true` 또는 명시 `resultMap`. 계약 DTO(`boothNo`↔`booth_no`, `floorLoadTPerM2`↔`floor_load_t_per_m2`)는 01_backend_contracts를 정본으로 매핑. +- **응답 제외 컬럼(불변, 계약 §0-3)**: `*_enc`·비번 해시·`otp_secret`·내부 IP/SSH·내부 식별자는 API 응답 완전 제외. 표시는 이름·역할·번호 등 비민감 필드만. + +### 4-2. 데이터 타입 표준 + +| 논리형 | 물리형(PostgreSQL) | 비고 | +|---|---|---| +| 식별자 | `uuid`(도메인) | `gen_random_uuid()` | +| 반정형 스펙 | `jsonb` | DesignSpec·quote·line_items·merge_provenance — GIN 인덱스 선택 | +| 공간 | `geometry(, 0)` | §3 | +| 상태·코드 | `varchar` + 공통코드 검증(앱 레벨) | ENUM 물리타입 지양(룰셋/코드 유연성) | +| 금액 | `numeric(15,2)` | | +| 시각 | `timestamptz`(UTC 저장, Asia/Seoul 표시) | | + +### 4-3. 공통코드 체계 (WISE 정합) + +> 정본 = [`docs/COMMON_CODES.md`](../COMMON_CODES.md)(단일 출처). 적재 `TB_CODE_GRP`/`TB_CODE`, 코드값=영문 상수·코드명=한글. **DA 검수 관점**: 코드 vs 마스터 경계 준수, "확인 필요" 코드값 임의 확정 금지. + +- **코드 vs 마스터 경계(핵심 표준)**: 열거 가능 소수값=**공통코드**(BOOTH_TYPE·RENDER_STATUS·SHOT_PRESET 등), 다건·CRUD·버전 대상=**마스터/룰셋**(홀·요율·규정·등록업체) — 공통코드에 넣지 않는다(COMMON_CODES §1·§4 말미). +- **확정 코드**(계약/PLANNING 근거): `EVENT_ROLE`·`PORTAL_ROLE`·`BOOTH_TYPE`·`COMPLIANCE_SEVERITY`·`RENDER_STATUS`·`SHOT_PRESET`·`USE_YN`. +- **DA 확정 대기(확인 필요)**: `AUCTION_STATUS`·`QUOTATION_STATUS`(M15)·`ZONE_TYPE` 확장·상태 전이(`LAYOUT_STATUS`/`DESIGN_STATUS`/`UTILITY_ORDER_STATUS`의 submitted 이후)·`COMPLIANCE_GROUP` 전체 — 도메인 에이전트 확정 시 **COMMON_CODES + 본 ERD 컬럼 주석 동시 갱신**(DA 검수 항목). +- **신규 도메인 코드 제안**(DA): `CONTRACTOR_CATEGORY`(등록업체 14분류: 전시디자인설치·리깅·전기시설·카펫/파이텍스·급배수/Air·가스설비·철거·운수통관·가구비품·경비용역·광고싸인물·지게차·방염·구조해석), `UTILITY_KIND`(power·network·plumbing·air·gas), `AUCTION_TYPE`(REVERSE·RFQ·FIXED), `CONTENT_LOCALE`(ko·en·zh·ja) — 확정 시 COMMON_CODES §2에 승격. + +### 4-4. 마스터데이터 관리 정책 (M18 백오피스) + +| 마스터 | 저장 형태 | 버전 관리 | 권한 | 개정 절차 | +|---|---|---|---|---| +| **홀 마스터** | `TB_HALL`(+`TB_HALL_FEATURE`·`footprint`) | 스키마 컬럼 `revision`·이력 테이블 | ADMIN | CAD 실측 확보 시 교체(floorplans README 대조), 제3전시장(2028) H11~H18 확장 구조 | +| **요율 룰셋** | `rulesets/rates-v*.json`(파일) | 파일 버전(`rates-v1.0`) + `TB_MASTER_DATA` 메타 | ADMIN | 연 단위 개정 → 새 파일 교체, 산출물에 `rulesetVersion` 각인, 기존 견적 스냅샷 불변 | +| **유틸리티 요금** | rates-v*.json `utility` 절 | 상동 | ADMIN | 인터넷 150,000 vs KT 80,000 정합 확인 후 확정(R8) | +| **규정 룰셋** | `rulesets/compliance-v*.json` | 파일 버전(`compliance-v1.0`) | ADMIN | 규정 개정 시 교체, 리포트에 `rulesetVersion`+`disclaimer` 각인(면책·감사) | +| **등록업체 DB** | `TB_COMPANY`(739개·14분류) | 주기 수집 + `kintex_registered` 검증 플래그 | ADMIN | 웹 공개 데이터 수집→자체 DB화, 추후 공식 피드. **미등록=옥션 응찰/초대 차단 게이트**(불변) | + +- **룰셋 스냅샷 원칙**: 견적(`TB_UTILITY_ORDER.quote`·`rateset_version`)·규정 리포트는 산출 시점 룰셋 버전을 **스냅샷 각인**. 이후 룰셋 개정이 과거 산출물을 변경하지 않는다(감사·재현성). +- **버전 관리 = 룰 엔진 결합**: 룰셋은 코드가 아닌 데이터 → M18 개정이 무중단 반영(PLANNING §8-1). 개정은 `TB_AUDIT_LOG` 전수 기록. + +--- + +## 5. 데이터 품질·거버넌스 + +### 5-1. 개인정보(PII) 분류 체계 + +> 집중 영역 = 관람·참가(M10/M11). **주민등록번호 등 고유식별정보는 수집하지 않는다**(수집 최소화). + +| 등급 | 분류 | 대상 컬럼(예) | 처리 | +|---|---|---|---| +| **P1 식별정보** | 직접 식별 | `TB_VISITOR.name_enc/email_enc/phone_enc`, `TB_LEAD.memo_enc` | **컬럼 AES-256-GCM 암호화** 저장, 응답 제외/마스킹, 접근 감사 | +| **P2 준식별** | 결합 식별 | `org_name`·`job_title`·`interests`·`visitor_type` | 접근 통제, BI는 집계/익명화만 반입 | +| **P3 인증비밀** | 크리덴셜 | `TB_USER` 비번 해시(BCrypt)·`otp_secret`(AES) | 절대 응답 금지, 로그 금지 | +| **P4 공개/비식별** | 비민감 | 부스명·업체명(표시용)·행사·집계 | 일반 처리 | + +- **BoothDto 준거**: `assignedCompanyName`는 "표시용, 내부 식별자·민감정보 미포함"(BoothDto Javadoc) — P4. 부스 설계(TB_DESIGN_PLAN.spec)는 영업비밀(R10) → 행사 격리·접근 제한. + +### 5-2. 암호화 정책 + +| 데이터 | 방식 | 근거 | +|---|---|---| +| PII 컬럼(P1) | **AES-256-GCM** 컬럼 암호화, 키는 서버 env/별도 키파일(코드·DB·커밋·로그 금지) | GUARDiA 보안 불변 `os_pw_enc` 패턴 | +| 비밀번호 | BCrypt(단방향) | WISE 표준 | +| OTP 시크릿 | AES-256-GCM | §5B-3 TOTP | +| 전송 | TLS(포털·API·워커 콜백) | NA(network.md) 정합 | +| 오브젝트 스토리지 | 접근 제어 URL(서명·만료), 도면/설계 행사 격리 | R10 | + +### 5-3. 동의(Consent)·수집 최소화 + +- **동의 분리**: `TB_REGISTRATION.consent_privacy`(개인정보 수집·이용, **필수**) / `consent_marketing`(EDM 발송, **선택**, 정보통신망법) / `TB_LEAD.consent_share`(참가업체 리드 공유). 각 `consent_at` 시각 기록. +- **목적 구속**: 마케팅 미동의자는 M12 EDM 세그먼트 제외(발송 파이프라인 게이트). 리드 공유 미동의는 참가업체 반출 차단. +- **최소 수집**: 관람객 폼은 목적 필요 최소 필드. 셀프서비스 계정은 행사 데이터 쓰기 권한 없음(PLANNING §2). + +### 5-4. 보존·파기 정책 + +| 데이터 | 보존 | 파기 | +|---|---|---| +| 관람객 등록·배지·체크인(PII) | 행사 종료 후 정책 기간(기본 1년, 재참가 분석 목적 별도 동의 시 연장) | 기간 경과 자동 익명화/삭제 | +| 리드(참가업체 반출본) | 참가업체 자산 — 반출 시점 이후 참가업체 책임, 플랫폼 원본은 위 관람객 정책 준수 | 상동 | +| 도면·설계·생성 이미지 | 행사 종료 후 보존(참가업체 자산, PLANNING §8) — 별도 정의 | 참가업체 요청 시 삭제 | +| 견적서 PDF·낙찰(M15) | 계약·감사 목적 장기 보존 | 법정 보존기간 준수 | +| 감사로그 | 장기 보존(불변·append-only) | 미파기 | +| BI 마트·KpiSnapshot | 집계(비식별) — 장기 보존 | — | + +### 5-5. 감사(Audit)·데이터 계보(Lineage) + +- **감사 대상(전수, `TB_AUDIT_LOG`)**: 승인·**낙찰(M15 Award)**·설계 변경·**룰셋/마스터 개정**·**리드/PII 접근**·권한 변경·로그인/OTP. append-only, 행위자·시각·전후값·행사 스코프 기록(COMMON_CODES §2-1 audit 확장). +- **데이터 계보(원천→마트)**: + +``` +[운영 원천] [BI 데이터마트 M16] +TB_HALL_ASSIGNMENT / 행사일정 ──▶ FACT_BOOKING (가동률·RevPAD·㎡당수익) +TB_SETTLEMENT (M9) ──▶ FACT_SETTLEMENT (매출구성·P&L) +TB_UTILITY_ORDER (M4) ──▶ FACT_UTILITY (유틸 매출) +TB_AUCTION/QUOTATION/AWARD ──▶ FACT_AUCTION (옥션 수수료) +TB_REGISTRATION/CHECK_IN (M10) ──▶ FACT_VISITOR (관람·리텐션) ※PII 비반입, 집계만 +TB_HALL/PostGIS ST_Area ──▶ DIM_HALL (면적 정규화) + ─(배치/야간 적재)─▶ KPI_SNAPSHOT (경영진 KPI) +``` + +- **PII 격리(계보 규칙)**: BI 마트는 P1/P2 원본을 반입하지 않는다 — 관람객은 **집계·코호트·익명 키**만 반입(FACT_VISITOR는 방문 카운트·세그먼트 차원, 개인 식별자 없음). M16 리텐션/LTV는 참가사(Company) 단위이며 개인 관람객이 아님. +- **데이터 품질 규칙(DQ)**: 참조무결성(FK), 지오메트리 유효성(§3-3 게이트), 룰셋 버전 각인 누락 0, 금액 통화 명시, 상태 코드 공통코드 준수. 마트 적재 시 원천-집계 정합 체크(§6-4). + +--- + +## 6. BI 데이터마트 (M16) — 스타 스키마 + +> 대상: **kintex-bi-dev**. PLANNING §5A M16-1 운영사(킨텍스) 관점 7지표 정합. 관점 격리 = ① 참가업체 ROI(자기 부스) / ② 운영사 수익성(전 행사) 별도 대시보드·권한(PLANNING §440). 적재 = 배치/야간 `KPI_SNAPSHOT` 또는 읽기 전용 복제(운영 부하 회피, §8-1). + +### 6-1. 스타 스키마 ERD + +```mermaid +erDiagram + DIM_DATE { + int date_key PK "YYYYMMDD" + date full_date + int year + int quarter + int month + boolean is_peak "성수기 3-5·9-11" + boolean is_offpeak "비수기 1·2·7·12" + } + DIM_HALL { + varchar hall_key PK "H1..H10/반홀" + varchar hall_name + numeric area_m2 "㎡ 정규화 기준" + varchar center "1전시장|2전시장" + numeric floor_load + varchar floor_finish + } + DIM_EVENT { + uuid event_key PK + varchar event_name + varchar event_type + date start_date + date end_date + int duration_days + } + DIM_EXHIBITOR { + uuid exhibitor_key PK "=company_id" + varchar company_name + varchar category + varchar region + int first_participation_year "코호트" + } + FACT_BOOKING { + uuid booking_id PK + int date_key FK + varchar hall_key FK + uuid event_key FK + numeric occupied_area_m2 + numeric available_area_m2 + int occupied_days + int available_days + numeric rental_revenue + numeric season_coeff "성수기/비수기/1전시장 계수" + } + FACT_SETTLEMENT { + uuid settlement_id PK + int date_key FK + uuid event_key FK + varchar revenue_segment "rental|utility|auction_fee|lobby|outdoor|parking" + numeric revenue + numeric direct_cost "운영·에너지·인력" + numeric contribution_margin + } + FACT_UTILITY { + uuid util_fact_id PK + int date_key FK + uuid event_key FK + uuid exhibitor_key FK + varchar utility_kind "power|network|plumbing|air" + numeric amount + } + FACT_AUCTION { + uuid auction_fact_id PK + int date_key FK + uuid event_key FK + varchar category_code + numeric awarded_amount + numeric platform_fee + int bid_count + } + FACT_VISITOR { + uuid visitor_fact_id PK + int date_key FK + uuid event_key FK + varchar visitor_segment "visitor|buyer (익명 세그먼트)" + int registered_count + int checkin_count + int lead_count "PII 없음·집계만" + } + KPI_SNAPSHOT { + uuid snapshot_id PK + int date_key FK + varchar scope "venue|center|hall|event" + varchar scope_key + varchar kpi_code "OCC|REVPAD|MARGIN|RETENTION|LTV|YIELD" + numeric kpi_value + numeric target_value + timestamptz built_at + } + + DIM_DATE ||--o{ FACT_BOOKING : "" + DIM_HALL ||--o{ FACT_BOOKING : "" + DIM_EVENT ||--o{ FACT_BOOKING : "" + DIM_DATE ||--o{ FACT_SETTLEMENT : "" + DIM_EVENT ||--o{ FACT_SETTLEMENT : "" + DIM_DATE ||--o{ FACT_UTILITY : "" + DIM_EXHIBITOR ||--o{ FACT_UTILITY : "" + DIM_DATE ||--o{ FACT_AUCTION : "" + DIM_DATE ||--o{ FACT_VISITOR : "" + DIM_EVENT ||--o{ FACT_VISITOR : "" + DIM_DATE ||--o{ KPI_SNAPSHOT : "" +``` + +### 6-2. 팩트 그레인(Grain) 정의 + +| 팩트 | 그레인(1행 = ) | 가법성 | +|---|---|---| +| `FACT_BOOKING` | 행사×홀(반홀)×기간 배정 1건 | 면적·일수·매출 가법, 계수 비가법 | +| `FACT_SETTLEMENT` | 행사×매출세그먼트×정산일 | 매출·원가·공헌이익 가법 | +| `FACT_UTILITY` | 행사×참가사×유틸종류 신청 1건 | 금액 가법 | +| `FACT_AUCTION` | 옥션(낙찰) 1건 | 낙찰액·수수료 가법, bid_count 준가법 | +| `FACT_VISITOR` | 행사×관람일×세그먼트 집계 | 카운트 가법(**개인 식별자 없음**) | + +### 6-3. M16-1 지표 → 마트 매핑 + +| # | 운영사 지표 | 산식 | 소스 팩트/차원 | +|---|---|---|---| +| ① | 홀·기간별 가동률 | Σ occupied_area×days / Σ available_area×days ×100 | FACT_BOOKING × DIM_HALL × DIM_DATE | +| ② | 매출 구성(mix) | revenue by segment | FACT_SETTLEMENT/FACT_UTILITY/FACT_AUCTION | +| ③ | 행사별 P&L·마진 | Σ revenue − Σ direct_cost = 공헌이익, 마진율 | FACT_SETTLEMENT × DIM_EVENT | +| ④ | 전시장별 ROI·RevPAD·㎡당 수익 | revenue / area_m2 (㎡ 정규화) | FACT_BOOKING × DIM_HALL(area_m2) | +| ⑤ | 참가사 리텐션·LTV | 코호트 재참가율, LTV=Σ(임대+유틸+옥션)/재참가주기 | DIM_EXHIBITOR(first_year) × FACT_* 다년 | +| ⑥ | 수요예측·수율/가격 | 성수기 계수·홀별 수요, 요율 시뮬레이션 | FACT_BOOKING 이력 × rates 룰셋 | +| ⑦ | 경영진 KPI 대시보드 | ①~⑥ 요약 + 목표 대비(점유 60~75%) | KPI_SNAPSHOT | + +### 6-4. 적재·품질 표준 + +- **적재 방식**: 야간 배치 ETL(운영→`mart` 스키마) 또는 읽기 전용 복제(운영 부하 회피). `KPI_SNAPSHOT`은 스냅샷 시점(`built_at`) 각인 → 시계열 추이·재현성. +- **㎡ 정규화 권위**: DIM_HALL.area_m2는 **PostGIS `ST_Area` 산출값 또는 홀 마스터 확정값** 단일 출처(대형홀 vs 소형홀 생산성 비교 정합, ④ RevPAD 근거). +- **정합 체크(DQ)**: 마트 매출 합 = 운영 정산 합(허용오차 0), 가동률 분모(가용 홀·일수) = 행사일정×홀 마스터, 관점 격리(참가사 대시보드는 exhibitor_key 필터 강제). +- **PII 비반입(불변)**: FACT_VISITOR는 카운트/세그먼트만 — TB_VISITOR P1/P2 컬럼 마트 유입 금지(§5-5 계보 규칙). +- **한계 각인**: LTV·리텐션 다년 데이터 필요(초기 단년 근사), 수율 최적가=시뮬레이션 참고치(최종 요율은 킨텍스 경영 결정, R8) — 대시보드 고지. + +--- + +## 7. DA 검수 체크리스트 (Phase A 게이트) + +> 본 문서를 기준으로 db-engineer 물리 구현·타 트랙 산출물을 검수하는 항목(A-6 reviewer 정합 입력). + +| # | 검수 항목 | 기준 | +|---|---|---| +| 1 | 명명 규칙 준수 | `TB_`·snake_case·`_enc`·`geom`·`FACT_/DIM_` (§4-1) | +| 2 | 공간 좌표계 | SRID 0·geometry·GiST 인덱스·`ST_IsValid` 게이트 (§3) | +| 3 | 공통코드 경계 | 코드 vs 마스터 분리, "확인 필요" 임의확정 금지 (§4-3) | +| 4 | 룰셋 스냅샷 | 견적·리포트에 `rulesetVersion` 각인, 과거본 불변 (§4-4) | +| 5 | PII 암호화·응답제외 | P1 `_enc` AES-GCM, 민감컬럼 API 완전 제외 (§5-1/5-2, 계약 §0-3) | +| 6 | 동의 게이트 | marketing 미동의 EDM 제외, share 미동의 반출 차단 (§5-3) | +| 7 | 감사 전수 | 낙찰·룰셋개정·리드접근·권한변경 기록 (§5-5) | +| 8 | BI PII 비반입 | 마트에 개인식별자 유입 0, 관점 격리 (§6-4) | +| 9 | 계약 정합 | §8 매퍼 인수 테이블/컬럼 = 본 ERD (§2-1) | + +--- + +## 8. 미결·후속 (확정 대기) + +| 항목 | 상태 | 담당 | +|---|---|---| +| M15 옥션 상태 코드(`AUCTION_STATUS`·`QUOTATION_STATUS`) 확정 | 확인 필요 | bidding-dev + DA | +| 상태 전이(layout/design/utility submitted 이후) | 확인 필요 | M6 승인 워크플로 + DA | +| `TB_SETTLEMENT`·`TB_MEETING`·`TB_MICROSITE` 상세 컬럼 | 골격만 | 도메인 에이전트 + DA | +| CAD 트렌치 실측 → `TB_TRENCH.assumed=false` 교체 | 미확보(R4) | 킨텍스 협의 | +| 홀 간 venue 좌표계 변환(S7·부지) | Phase 2 | DA + M2 | +| 제3전시장(2028) H11~H18 홀 마스터 확장 | 구조 대비 | DA | +| 인터넷 요금 정합(150,000 vs 80,000) | 확인 필요(R8) | 킨텍스 + M18 | + +--- + +## 9. 변경 이력 + +| 버전 | 일자 | 작성자 | 내용 | +|---|---|---|---| +| v1.0 | 2026-07-11 | kintex-data-architect(DA) | 최초 — 전사 데이터 모델(개념→논리→물리 ERD, PLANNING §7 확장)·공간 데이터 표준(PostGIS SRID0·부스 POLYGON·트렌치 POINT·배선 LineString)·데이터 표준(명명·타입·공통코드 WISE 정합·마스터/룰셋 관리)·품질·거버넌스(PII 분류·암호화·동의·보존·감사·계보)·**BI 데이터마트 M16 스타 스키마(FACT_BOOKING/SETTLEMENT/UTILITY/AUCTION/VISITOR + DIM_DATE/HALL/EVENT/EXHIBITOR + KPI_SNAPSHOT, M16-1 7지표 정합)** 정의. 물리 구현은 db-engineer 인수, 코드값 확인필요 항목은 §8 후속. | diff --git a/docs/architecture/network.md b/docs/architecture/network.md new file mode 100644 index 0000000..02eda46 --- /dev/null +++ b/docs/architecture/network.md @@ -0,0 +1,273 @@ +# 킨텍스 자동전시시스템 — 네트워크 아키텍처 (A-5) + +> 작성: 네트워크 아키텍트(NA) · 작성일: 2026-07-11 · 버전 v1.0 +> 근거: `../PLANNING.md` v2.0 (§2-1 역할별 포털, §8 아키텍처, §8-1 v2.0 보강, §10 R12 외부 API 게이트), `../IMPLEMENTATION_BACKLOG.md` A-5 +> 교차참조(Phase A 동시 산출·정합 대상): AA `app.md`(A-1) · SA `system.md`(A-2, 보안영역·배포 토폴로지) · TA `tech.md`(A-3, 관측성·AiTextRouter) · DA `data.md`(A-4) +> 범위: **IT 인프라 네트워크(계정·서비스·데이터 트래픽 경로)** 설계·정책. 구현(nginx·방화벽 룰·systemd·CI/CD)은 devops(DEV) 트랙. 본 문서는 설계·정책·리뷰이며 코드/설정을 생성하지 않는다. + +--- + +## 0. 범위 경계 — M4 전시장 유틸리티 배선과의 구분 (필독) + +본 문서가 다루는 것은 **플랫폼 IT 인프라 네트워크**(웹/모바일 클라이언트 ↔ 애플리케이션 ↔ 데이터/AI 워커 사이의 트래픽 경로, 보안영역, 방화벽, 부하분산, 외부 아웃바운드)이다. + +이는 **M4 유틸리티 설계 모듈이 다루는 전시장 바닥 트렌치 배선(전기·조명·인터넷/전화·급배수·압축공기)과 완전히 별개**다. + +| 구분 | M4 유틸리티 배선 (PLANNING §M4) | 본 문서 = IT 인프라 네트워크 (A-5) | +|---|---|---| +| 대상 | 전시홀 바닥 트렌치의 **물리 배선**(부스별 전기 kW·인터넷 회선·급배수 구) | 플랫폼 서버·서비스·클라이언트 간 **데이터 트래픽** | +| 데이터 성격 | 배선 경로 = PostGIS LineString(**업무 데이터**), 위치표시도, 자동 견적 | VLAN·서브넷·방화벽 존·LB·TLS·아웃바운드 | +| 소유 | M4 개발(BE/DB/FE) — 도메인 기능 | NA(본 문서) + devops 구현 | +| 관계 | M4의 "인터넷 유선 150,000원/회선" 신청은 **전시 참가업체가 전시장에서 쓸 회선** — 플랫폼 운영 네트워크와 무관 | 플랫폼 자체가 도는 인프라 | + +> 요약: "전시장에 깔리는 인터넷 회선"(M4, 참가업체 상품)과 "이 플랫폼이 도는 네트워크"(A-5, 운영 인프라)는 이름만 겹칠 뿐 다른 계층이다. 혼동 시 보안영역·정산이 오염되므로 문서·코드·용어에서 항상 분리한다. + +--- + +## 1. 설계 원칙 + +1. **2영역 분리(공개 vs 내부) 최소권한**: 불특정 다수가 접근하는 **공개/관람객 트래픽(DMZ)** 과 킨텍스 직원·관리자·백오피스가 쓰는 **내부 운영(내부망)** 을 물리/논리적으로 분리한다. 공개 영역이 뚫려도 내부망·데이터 계층에 직접 도달하지 못한다(계층 방어). +2. **역할별 프론트 = 공격면 분리**(PLANNING §8-1): organizer·exhibitor·contractor·ops·admin·public/visitor 6개 프론트를 별도 번들·도메인/서브패스로 배포. 네트워크 계층에서도 **공개 성격(public/visitor)** 과 **인증 필수(organizer·exhibitor·contractor·ops·admin)** 를 존으로 나눈다. +3. **기본 거부(default-deny)**: 인바운드·아웃바운드 모두 화이트리스트. 특히 **아웃바운드는 승인된 목적지(Claude·Gemini·PG·SMTP·모델서버)만** 포워드 프록시/egress 방화벽을 통해 허용, 그 외 전면 차단(폐쇄망 지향). +4. **단일 진입 + TLS 종단**: 모든 외부 인바운드는 리버스 프록시/로드밸런서(nginx) 한 곳에서 TLS 종단하고 내부는 사설망 트래픽. 백엔드 포트는 외부 미노출. +5. **GUARDiA 인프라와 도메인 분리**: kintex는 독립 저장소(`zio/kintex`)이며 GUARDiA ITSM/관제 인프라 서버(`101.79.17.164`)와 **별개 도메인·별개 배포 대상**이다(배포 서버·포트는 G2 게이트에서 확정). 본 설계는 그 별개 도메인 위에 존을 정의한다. + +--- + +## 2. 보안영역(Security Zone) 모델 + +4계층 존 + 관리 존으로 나눈다. 존 간 통신은 명시된 방향·포트만 허용한다. + +```mermaid +graph TB + subgraph INET[인터넷 / 불특정 다수] + U1[일반 대중·관람객] + U2[주최자·참가업체·업체
인증 사용자] + U3[킨텍스 직원·관리자] + PGc[PG사 콜백] + end + + subgraph EDGE[엣지 / DDoS·CDN·WAF] + CDN[CDN·정적 캐시
공개사이트 에셋·이미지] + WAF[WAF + DDoS 방어
레이트리밋] + end + + subgraph DMZ[DMZ · 공개 영역] + LBp[리버스 프록시/LB
TLS 종단 · 공개] + PUB[public www/expo
SSR·SEO·다국어 렌더] + VIS[visitor 관람객 앱 게이트웨이] + APIGWp[공개 API GW
등록·조회·티켓·wayfinding·PG콜백] + end + + subgraph INTNET[내부망 · 인증·운영 영역] + LBi[내부 프록시/LB
TLS · mTLS 옵션] + AUTH[SSO/JWT·RBAC·2FA
인증 게이트] + APIGWi[내부 API GW
organizer·exhibitor·contractor·ops·admin] + APP[공유 Spring Boot 백엔드
룰·배치/배선·옥션·BI·CMS] + end + + subgraph AITIER[AI 워커 존 · egress 제한] + REDIS[(Redis 작업 큐)] + NB[나노바나나 Python 워커] + OLL[온프레미스 모델
Ollama 폴백] + end + + subgraph DATA[데이터 존 · 최내곽] + PG[(PostgreSQL+PostGIS)] + OBJ[(오브젝트 스토리지)] + RREP[(BI 읽기전용 복제/스냅샷)] + end + + subgraph MGMT[관리 존] + BAST[배스천/점프호스트
SSH·운영접근] + OBS[관측성
로그·메트릭·트레이스] + end + + subgraph OUT[승인된 아웃바운드 목적지] + ANTH[api.anthropic.com
Claude ✅승인] + GEM[generativelanguage.googleapis.com
Gemini ⚠️G1 게이트] + PGext[PG사 결제 API] + SMTP[SMTP 메일] + end + + U1 --> WAF --> CDN + U1 --> WAF --> LBp + U2 --> WAF --> LBp + PGc --> WAF --> LBp + U3 -->|VPN/허용 IP| LBi + + LBp --> PUB & VIS & APIGWp + APIGWp --> AUTH + LBi --> AUTH --> APIGWi --> APP + APIGWp -.제한 라우팅.-> APP + + APP --> REDIS --> NB + APP --> PG + APP --> OBJ + APP --> RREP + NB --> OBJ + NB -.egress only.-> GEM + APP -.egress.-> ANTH + APP -.egress.-> PGext + APP -.egress.-> SMTP + NB & APP -.폴백.-> OLL + + BAST -.운영 SSH.-> INTNET & AITIER & DATA + APP & NB & PUB -.로그/메트릭.-> OBS +``` + +### 2-1. 존 정의·신뢰수준 + +| 존 | 신뢰 | 구성 요소 | 인바운드 허용 | 아웃바운드 허용 | +|---|---|---|---|---| +| **엣지** | 무신뢰 | CDN, WAF/DDoS | 인터넷 80/443 | DMZ LB | +| **DMZ(공개)** | 낮음 | 공개 LB(TLS 종단), `public` SSR, `visitor` 게이트웨이, 공개 API GW, PG 콜백 수신 | 엣지에서만 | 내부망 API GW(제한 라우팅), 데이터 존 직결 **금지** | +| **내부망(인증·운영)** | 중 | 내부 LB, SSO/인증 게이트, 내부 API GW, 공유 Spring Boot 백엔드, 룰·배치/배선·옥션·BI·CMS 서비스 | DMZ(허용 API만)·관리 존(허용 IP/VPN) | 데이터 존, AI 워커 존, 승인 아웃바운드(프록시 경유) | +| **AI 워커 존** | 중(egress 통제) | Redis 큐, 나노바나나 Python 워커, 온프레미스 모델(Ollama) | 내부망(큐 소비만) | 데이터 존(OBJ 적재), **Gemini egress 단일 경로만** | +| **데이터 존(최내곽)** | 높음 | PostgreSQL+PostGIS, 오브젝트 스토리지, BI 읽기전용 복제 | 내부망·AI 워커(정의된 서비스 계정만) | 없음(아웃바운드 전면 차단) | +| **관리 존** | 높음 | 배스천/점프호스트, 관측성(로그·메트릭·트레이스), 백업 | 운영자 VPN/허용 IP만 | 대상 존 SSH·수집 | + +**핵심 규칙**: DMZ → 데이터 존 **직접 접근 절대 금지**. 공개 트래픽이 데이터에 닿으려면 반드시 내부망 백엔드 API를 경유(인증·인가·검증). 데이터 존은 아웃바운드가 없어 유출 경로 자체를 제거한다. + +--- + +## 3. 공개(DMZ) vs 내부망 — 포털·도메인 매핑 + +PLANNING §2-1 6역할 포털을 네트워크 노출 성격으로 재분류한다. + +| 포털/앱 | 도메인(예시, G2에서 확정) | 노출 영역 | 인증 | 렌더 경로 | +|---|---|---|---|---| +| 공개 홍보 사이트 (M12) | `www.` / `expo.` | **DMZ(공개)** | 불필요(쓰기 없음) | SSR/정적 생성 + CDN, SEO·다국어(hreflang) | +| 관람객 앱 (M10·M13) | `visitor.` (모바일 API) | **DMZ(공개)** | 셀프서비스(경량, 쓰기 제한) | 모바일 클라이언트 ↔ 공개 API GW | +| 주최자 콘솔 | `organizer.` | **내부망(인증)** | JWT SSO + Event owner | React SPA + 내부 API GW | +| 참가업체 포털 | `exhibitor.` | **내부망(인증)** | JWT SSO + Event member | React SPA | +| 업체 포털·옥션 | `contractor.` | **내부망(인증)** | 등록업체 검증 계정 | React SPA | +| 운영 대시보드(홀매니저) | `ops.` | **내부망(운영)** — 접근 IP/VPN 제한 권장 | 킨텍스 내부 계정 | React SPA | +| 관리자 백오피스 (M18) | `admin.` | **내부망(관리)** — **강한 제한**(VPN/허용 IP·2FA 필수·모바일 미제공) | 플랫폼 관리자 + 2FA 강제 | React SPA(웹 전용) | + +- **공개(DMZ) 3원칙**: (1) 쓰기 권한 없음(등록·조회·티켓·매칭 조회만), (2) 데이터 존 직결 불가, (3) 캐시/CDN 적극(공개 성능·검색 노출). 공개 API GW는 **읽기·등록 한정 엔드포인트만** 화이트리스트로 노출. +- **내부망 인증 앱**: SSO/JWT + 역할·행사 이중 RBAC(§8-1). `ops.`·`admin.`은 킨텍스 내부 대역/VPN·허용 IP로 접근 자체를 좁힌다(직원 대상이므로 인터넷 전면 노출 불필요). +- **admin. 백오피스**: 최고 위험. VPN 또는 킨텍스 사내망 + 허용 IP allowlist + 2FA(OTP) 강제 + 감사로그 전량. 크로스-테넌트 권한을 가지므로 공개 인터넷 경로에서 도달 불가하게 한다. + +--- + +## 4. 방화벽 · WAF 정책 + +### 4-1. 방화벽(존 간 트래픽 매트릭스) + +| From \ To | 엣지 | DMZ | 내부망 | AI 워커 | 데이터 | 관리 | 인터넷(egress) | +|---|---|---|---|---|---|---|---| +| **인터넷** | 443/80 | (엣지 경유) | ✗ | ✗ | ✗ | ✗ | — | +| **엣지** | — | 443 | ✗ | ✗ | ✗ | ✗ | ✗ | +| **DMZ** | — | — | 내부 API GW(허용 엔드포인트 https) | ✗ | ✗ | ✗ | ✗ | +| **내부망** | — | — | — | Redis 6379·워커 | PG 5432·OBJ https | ✗ | 프록시 경유(§5) | +| **AI 워커** | — | — | 완료 푸시(WebSocket) | — | OBJ https | ✗ | Gemini 단일(§5) | +| **데이터** | — | — | — | — | — | ✗ | **✗(전면 차단)** | +| **관리** | — | — | SSH | SSH | SSH(제한) | — | 패키지/업데이트(제한) | + +- 기본 정책 = **DROP**. 위 표의 명시 경로만 ALLOW. 포트는 예시(운영값은 devops가 G2에서 확정). +- 백엔드(Spring Boot)·Redis·PostgreSQL·오브젝트 스토리지 포트는 **인터넷에 미노출** — 사설 대역에서만 청취. +- DMZ↔내부망 사이는 **애플리케이션 프로토콜(HTTPS/API)만** 통과, DB 프로토콜(5432 등) 횡단 금지. + +### 4-2. WAF (공개 트래픽 전면) + +DMZ 진입 전 엣지에서 WAF를 통과시킨다. + +- **OWASP Top10 룰셋**(SQLi·XSS·경로조작·SSRF·파일업로드 악용). 도면/이미지 업로드(M2·M3·M5)·CMS(M17)·공개 등록 폼(M10)이 주요 표적 → 업로드 확장자·MIME·크기 검증을 WAF + 애플리케이션 이중. +- **봇/스크래핑 방어**: 공개사이트(M12)는 SEO 목적상 정상 크롤러(구글봇 등)는 허용하되, 등록·티켓·비즈매칭 엔드포인트는 봇 챌린지·리캡차 옵션. +- **PG 콜백 검증**: DMZ에서 수신하는 PG 결제 콜백은 **출처 IP allowlist + 서명 검증**을 WAF/애플리케이션에서 이중 확인(위조 콜백 차단). +- **관리자·옥션 엔드포인트**: `admin.`·M15 옥션 응찰(가격 조작 민감)은 WAF 예외 없이 최상위 룰 + 레이트리밋 강화. + +--- + +## 5. 외부 연동 아웃바운드 정책 (승인 게이트 준수) + +**폐쇄망 지향 — egress 기본 차단.** 승인된 목적지만 포워드 프록시(egress 게이트웨이)를 통해 도메인 화이트리스트로 허용하고, 그 외 전량 차단·로깅한다. + +| 목적지 | 용도 | 승인 상태 | 경로 · 정책 | +|---|---|---|---| +| `api.anthropic.com` | Claude 텍스트 AI(규정검수·서류·BI 인사이트·AiTextRouter 기본) | **✅ 승인**(2026-07-03, 소유자) | 백엔드 → egress 프록시. `ANTHROPIC_API_KEY`는 **서버 env only** — DB·코드·커밋·로그·응답 기록 금지. 실패 시 Ollama 폴백 | +| `generativelanguage.googleapis.com` | Gemini 나노바나나 이미지 생성(M5) | **⚠️ 미승인 — G1 게이트**(PLANNING R12) | **나노바나나 Python 워커에서만** egress. `GEMINI_API_KEY`는 워커 env only(백엔드 미취급). **G1 소유자 승인 전 실호출·배포 금지** → 미승인 시 워커 목/degraded(import·구조만 성립). 승인 시에도 워커 존 단일 경로만 개방 | +| PG사 결제 API | M9 정산·결제, M10 티켓, M15 발주 | 결제 필수(도메인 확정 G2) | 백엔드 → egress. 콜백은 인바운드(§4-2). 키·상점ID env only | +| SMTP | 2차 인증 EMAIL 코드·알림 발송 | 필요 | 지정 SMTP host:port만. 미설정 시 로컬 로그 모드 | +| 온프레미스 모델(Ollama 등) | AI 폴백·임베딩 | 내부(egress 아님) | AI 워커/내부망 내부 통신 — 외부 나가지 않음 | + +**아웃바운드 통제 원칙**: +1. **단일 egress 게이트웨이** 경유 — 각 서비스가 임의 외부 IP로 직접 나가지 못한다. 목적지 도메인 allowlist로만 통과. +2. **키 격리 = 네트워크 격리와 정합**: Gemini 키는 워커 존에만, Claude/PG 키는 백엔드에만. 데이터 존은 아웃바운드 자체가 없어 어떤 키도 외부로 나갈 수 없다. +3. **G1 게이트 강제**: Gemini egress 룰은 G1 승인 이전엔 **비활성(차단)** 상태로 배포. 승인 티켓 없이 열리지 않도록 devops가 방화벽/프록시 룰을 게이트에 종속. +4. **감사**: 전 아웃바운드는 목적지·시각·서비스만 로깅(요청 본문·키·응답 미기록 — 자격증명 보호 원칙). + +--- + +## 6. 부하분산 · TLS 종단 · API 게이트웨이 + +### 6-1. 로드밸런싱 + +- **공개 LB(DMZ)**: `www/expo`·`visitor`·공개 API GW 앞단. SSR 렌더 인스턴스와 정적 에셋(CDN 오프로드)을 분리. 관람객 대량 트래픽(개막일 사전등록·체크인 피크, M10)을 수평 확장 대상으로 본다. +- **내부 LB(내부망)**: 공유 Spring Boot 백엔드 앞단. organizer·exhibitor·contractor·ops·admin 트래픽을 라우팅. 무상태 세션(JWT) 전제로 라운드로빈/최소연결 + 헬스체크. +- **WebSocket/STOMP**: 나노바나나 완료 푸시·옥션 실시간 순위(M15)는 WS 스티키/전용 경로 필요 — LB에서 WS 업그레이드·타임아웃·스티키 정책 별도(TA `tech.md`와 정합). +- **AI 워커**: HTTP 인입 대상 아님(Redis 큐 소비형) — LB 대상 아님, 워커 수평 확장은 큐 컨슈머 스케일로 처리. + +### 6-2. TLS 종단 + +- **모든 외부 인바운드는 LB(nginx)에서 TLS 종단**. 인증서는 공개 도메인(공개사이트)과 인증 포털 도메인 각각 관리(SAN/멀티도메인 또는 개별). +- 종단 후 내부는 사설망 평문 또는 존 간 mTLS(옵션). **DMZ→내부망 호출은 mTLS 권장**(공개 영역이 내부 API를 사칭 호출하지 못하도록). +- HSTS·TLS1.2+ 강제·약한 cipher 비활성. `admin.`·옥션 등 민감 경로는 최신 TLS만. + +### 6-3. API 게이트웨이 (공개/내부 분리) + +- **공개 API GW(DMZ)**: 화이트리스트 엔드포인트만 — 관람객 등록·조회·티켓·wayfinding 조회·PG 콜백. **쓰기·관리·설계·옥션 엔드포인트 노출 금지**. 레이트리밋·봇 방어 1차. +- **내부 API GW(내부망)**: 인증 필수 전 기능. SSO/JWT 검증 → 역할·행사 RBAC(§8-1) → 백엔드 라우팅. 옥션(M15)·설계(M2~M5)·BI(M16)·admin(M18) 전부 여기. +- 두 GW는 **동일 공유 백엔드**를 바라보되 **노출 표면이 다르다**(공개는 극히 일부, 내부는 전체). 이 분리가 최소권한 네트워크의 핵심. + +--- + +## 7. DDoS · 레이트리밋 (공개 트래픽) + +공개 영역은 불특정 다수 노출 → 가용성 위협 상수. 계층 방어한다. + +| 계층 | 대상 | 정책 | +|---|---|---| +| 엣지(WAF/CDN) | 볼류메트릭 DDoS(L3/4), 정적 에셋 | CDN 캐시로 오리진 보호, SYN/UDP 플러드 스크러빙, 지오/IP 평판 필터 | +| 공개 LB | L7 요청 폭주 | 커넥션·요청률 상한, slowloris 타임아웃, 동시연결 캡 | +| 공개 API GW | 엔드포인트별 레이트리밋 | 등록·티켓·비즈매칭은 IP/계정별 토큰버킷. **개막일 피크(M10 체크인)** 대비 버스트 허용치 별도 | +| 애플리케이션 | 남용 방지 | 로그인·2FA·PG 콜백은 강한 레이트리밋 + 실패 잠금(§8-1 로그인 실패 잠금과 정합) | + +- **옥션(M15) 특수**: 마감 직전 응찰 폭주·자동응찰 스크립트 가능성 → 응찰 API는 계정별 레이트리밋 + 서버 권위 타임스탬프(클라이언트 시간 불신) + 이상 패턴(초당 다중 인하) 플래그. +- **관람객 체크인 오프라인 폴백**(PLANNING M10 한계): 현장 네트워크 불안정·DDoS 시에도 QR 체크인은 오프라인 대기열·후동기화가 가능해야 함 — 네트워크 장애가 현장 운영 정지로 직결되지 않도록 클라이언트 폴백을 전제. + +--- + +## 8. 세그먼테이션 · 격리 + +1. **행사(Event) 단위 데이터 격리**(PLANNING R10): 부스 설계는 경쟁사 민감 정보. 네트워크가 아니라 **애플리케이션 RBAC + 데이터 존 접근 계정 최소화**로 격리(같은 백엔드 안에서 행사별 인가). 네트워크 세그먼트는 존 단위, 테넌트 격리는 인가 계층 — 이중. +2. **서비스 계정 분리**: 백엔드·워커·BI 복제는 **각기 다른 DB 계정/최소 권한**으로 데이터 존 접근(워커는 OBJ 쓰기만, BI는 읽기전용 복제만). 한 서비스 침해가 전 데이터로 번지지 않게 한다. +3. **BI 부하·경로 분리**(PLANNING §8-1): 운영 DB 보호를 위해 BI(M16)는 **읽기전용 복제/스냅샷(KpiSnapshot)** 을 별도로 바라본다 — 운영 트래픽과 분석 트래픽의 경로 분리. +4. **관리 평면 분리**: SSH·운영 접근은 배스천 경유만(직접 접근 금지). 관측성(로그·메트릭)은 관리 존에 수집하되 로그에 자격증명·PII·스택트레이스 미기록(에러 응답 = 요약만, GUARDiA 보안 원칙 정합). +5. **모바일 채널**: 참가업체 리드캡처·홀매니저 현장 검수·업체 반입 QR·관람객 배지(§2-1)는 각 포털의 API 표면만 사용 — 모바일이라고 별도 백도어 없음. 공개(visitor)는 DMZ, 인증(exhibitor/ops/contractor 모바일)은 내부 API GW. + +--- + +## 9. 폐쇄망 · 온프레미스 제약 반영 + +- **egress 기본 차단**은 폐쇄망 운영 기관 배포를 겨냥한 기본값이다. 승인 4목적지(Claude·Gemini(G1)·PG·SMTP) 외 외부 통신이 없어야 정상. +- **완전 폐쇄망(외부 API 전면 불가) 시나리오**: Claude/Gemini 미허용 환경에서는 온프레미스 모델(Ollama)로 폴백 — 텍스트 AI는 AiTextRouter가 자동 폴백, 이미지 생성(M5)은 온프레미스 대체(SDXL 등) 어댑터 검토(PLANNING R12 완화책). 이 경우 egress 게이트웨이의 외부 룰 전량 비활성. +- **GUARDiA 인프라 비침투**: kintex는 GUARDiA ITSM 관제망·`101.79.17.164`와 트래픽·자격증명·DB를 공유하지 않는다(별개 도메인·별개 존). 상호 egress/인바운드 룰 없음. +- **내부 모델 서버**는 AI 워커 존 내부 통신(외부 egress 아님) — RAM 제약(온프레미스 소형모델) 고려는 TA/AI 트랙, 네트워크는 내부 경로만 보장. + +--- + +## 10. 미결·후속(devops·SA 정합 필요) + +| # | 항목 | 의존 | +|---|---|---| +| N-1 | 실제 도메인·서브도메인·포트·인증서 발급 | **G2 게이트**(배포 대상 서버·도메인 확정) | +| N-2 | Gemini egress 방화벽 룰 활성화 | **G1 게이트**(소유자 승인) — 승인 전 차단 상태 배포 | +| N-3 | 존 물리 구현(VLAN/보안그룹/네임스페이스) vs 논리 분리 선택 | SA `system.md`(A-2) 배포 토폴로지와 정합 | +| N-4 | CDN·WAF 벤더·DDoS 스크러빙 사업자 선정 | devops·비용 | +| N-5 | mTLS(DMZ↔내부망) 적용 범위·인증서 회전 | TA `tech.md`(A-3) | +| N-6 | 관측성 수집 경로·보존정책(PII/자격증명 미기록 검증) | TA·QA | +| N-7 | BI 읽기전용 복제 토폴로지(복제 지연·경로) | DA `data.md`(A-4) | + +> 본 문서는 네트워크 **설계·정책·존 계약**을 정의한다. 실제 nginx/방화벽/보안그룹/CI·CD 설정 코드는 devops(DEV)가 Phase E에서 구현하며, 존 모델·아웃바운드 게이트·2영역 분리 원칙은 본 문서를 계약으로 준수한다. diff --git a/docs/architecture/system.md b/docs/architecture/system.md new file mode 100644 index 0000000..c2f0829 --- /dev/null +++ b/docs/architecture/system.md @@ -0,0 +1,355 @@ +# 킨텍스 자동전시시스템 — 시스템 아키텍처 · 비기능요건(NFR) + +> 산출: 시스템 아키텍트(SA) · 작성일: 2026-07-11 · 대상 백로그: `IMPLEMENTATION_BACKLOG.md` **A-2** +> 정합 근거: [`../PLANNING.md`](../PLANNING.md) v2.0 §2(6역할)·§7(데이터·연동)·§8(아키텍처)·§10(리스크) · [`../IMPLEMENTATION_BACKLOG.md`](../IMPLEMENTATION_BACKLOG.md) · [`../analysis/reroomai-source.md`](../analysis/reroomai-source.md)(나노바나나 워커) +> 확정 스택(불변): React 18/19(Vite·TS) + Spring Boot 3.x(Java 17)+MyBatis + PostgreSQL(PostGIS) + Redis + 나노바나나 Python 워커. — `../DEVELOPMENT_GUIDE.md` §1, `../PLANNING.md` §8 +> **문서 소유권**: 본 문서는 SA(시스템 아키텍처·NFR·구성·배포 토폴로지)만 정본. 애플리케이션 경계/API 표준=AA([`app.md`](app.md), A-1), 기술 표준/빌드·관측성=TA([`tech.md`](tech.md), A-3), 전사 ERD/공간·마스터·BI마트=DA([`data.md`](data.md), A-4), DMZ/방화벽/부하분산=NA([`network.md`](network.md), A-5). 본 문서는 그 상위 시스템 관점만 다루며 세부는 각 문서로 위임(교차참조). + +--- + +## 0. 선행 게이트 (SA 반영 — 착수 전 소유자 확인) + +| 게이트 | 내용 | 시스템 아키텍처 영향 | +|---|---|---| +| **G1** | 나노바나나(Gemini `generativelanguage.googleapis.com`) 외부 호출 승인(PLANNING R12) | 미승인 시 나노바나나 워커는 **목(mock)/degraded 모드**로 기동(구조·큐·워터마크 계약은 유지, 실이미지 생성만 차단). 승인 시 워커 서비스에만 `GEMINI_API_KEY` env 주입(§6-4). **아웃바운드 방화벽 정책은 워커 서브넷 한정** — NA 확정 | +| **G2** | 배포 대상 서버·포트(GUARDiA 인프라와 **별개 도메인**) | 배포 토폴로지(§4)의 물리 매핑·도메인·포트·TLS는 G2 확정 후 Phase E에서 실체화. 본 문서는 **논리 토폴로지**를 확정하고 물리값은 플레이스홀더(``) 처리 | + +> 두 게이트는 **아키텍처 설계를 막지 않는다** — 논리 구성·NFR·용량 산정은 게이트와 무관하게 확정하고, 물리 실체화(실호출·실배포)만 게이트에 종속시킨다. + +--- + +## 1. 시스템 개요 + +킨텍스 자동전시시스템은 전시 생애주기(판매·기획 → 설계·시각화 → 발주·계약 → 참가·관람 → 현장운영 → 사후·경영)를 하나의 데이터·계정 체계로 자동화하는 **베뉴 운영 플랫폼**이다. 시스템은 **6개 역할별 분리 프론트 + 단일 공유 Spring Boot 백엔드 + 나노바나나 Python 워커 사이드카**를 **SSO·역할 RBAC** 위에 얹고, **PostGIS 공간 데이터 · Redis 비동기 큐 · 오브젝트 스토리지**를 공유 자원으로 둔다. + +**아키텍처 대원칙 (PLANNING §4·§8 정합)** +1. **단일 공간 데이터 모델** — 부스 폴리곤·트렌치 포인트·배선 LineString을 PostGIS 지오메트리로 일원화. 시각화(M5)·검증(M2)·정산(M9)·wayfinding(M13)·BI(M16)가 같은 원천을 재사용. +2. **이미지 생성 전면 비동기** — Spring이 RenderJob을 Redis 큐에 발행 → Python 워커가 소비·생성 → 오브젝트 스토리지 적재 → WebSocket 완료 푸시. 백엔드는 오케스트레이션·상태만, `GEMINI_API_KEY`는 워커 전유. +3. **역할별 프론트 분리** — 최소권한·공격면 축소. 공유 디자인 시스템·컴포넌트·API 계약을 상속(중복 구현 금지). +4. **룰셋은 코드가 아닌 버전 데이터** — 요율·규정을 버전 파일로 두어 킨텍스 연 단위 개정을 무중단 반영. +5. **공개 vs 내부 보안영역 분리** — 공개 홍보/관람객(비인증·쓰기제한)과 내부 백오피스(관리자·크로스테넌트)를 물리·논리로 격리. + +--- + +## 2. 논리 아키텍처 (컴포넌트 구성도) + +```mermaid +graph TB + subgraph EDGE["엣지 / 공개영역 (DMZ)"] + CDN[CDN · 정적 캐시
공개 이미지·번들·플로어플랜] + WAF[WAF / Reverse Proxy · nginx
TLS 종단·라우팅·레이트리밋] + end + + subgraph FRONT["역할별 분리 프론트 (React·Vite·TS, 공유 디자인시스템)"] + FO[organizer. 주최자 콘솔] + FE[exhibitor. 참가업체 포털] + FC[contractor. 업체 포털·옥션] + FM[ops. 운영 대시보드] + FA[admin. 관리자 백오피스
★내부 전용] + FP[www/expo. 공개 홍보 사이트
SSR/SSG·SEO·다국어] + FV[관람객 모바일/웹
배지·wayfinding·매칭] + end + + subgraph GATE["SSO · 역할 RBAC · API 진입"] + SSO[JWT SSO + TOTP 2FA
플랫폼·행사 이중 RBAC 평가] + end + + subgraph BACKEND["공유 Spring Boot 3.x 백엔드 (Java 17·MyBatis)"] + API[REST + WebSocket/STOMP] + RULE[룰 엔진 · 요율/규정 버전셋] + LAYOUT[배치·배선 엔진 · PostGIS 공간 SQL] + AUC[옥션 엔진 M15 · 라운드·순위·낙찰스코어] + BI[BI 집계 M16 · KpiSnapshot] + CMS[CMS·다국어 M17] + PUBSVC[공개 API · 사전등록·조회 read-only] + end + + subgraph ASYNC["비동기 처리"] + REDIS[(Redis
작업 큐·순위·타이머·캐시·쿼터·세션)] + NBW[나노바나나 Python 워커
tools/nanobanana · google-genai] + DOCW[서류·PDF·EDM 워커
동일 큐 계약] + end + + subgraph DATA["데이터 계층"] + PG[(PostgreSQL + PostGIS
업무·공간 데이터)] + RPL[(읽기 복제본
BI·공개조회 오프로드)] + OBJ[(오브젝트 스토리지
도면·생성이미지·서식·콘텐츠)] + end + + subgraph EXT["외부 시스템 / 게이트"] + GEM[Google Gemini
나노바나나 · ★G1] + CLA[Anthropic Claude
AiTextRouter · env only] + PGPAY[PG 결제 · 세금계산서] + KXWP[kxwp/kxfp 작업신고
파일 릴레이] + CCPY[등록업체 DB 739] + EVT[행사일정 시스템] + end + + CDN --> WAF + FP -.CDN 캐시.-> CDN + FO & FE & FC & FM & FA & FP & FV --> WAF --> SSO --> API + WAF -->|공개 read-only| PUBSVC + API --> RULE & LAYOUT & AUC & BI & CMS + API --> REDIS + REDIS --> NBW & DOCW + NBW --> OBJ + NBW -.WebSocket 완료푸시.-> API + NBW -.★G1.-> GEM + DOCW --> OBJ + API --> PG + BI --> RPL + PUBSVC --> RPL + API -.AiTextRouter.-> CLA + API --> PGPAY + API -.파일 릴레이.-> KXWP + API --- CCPY & EVT + PG -.복제.-> RPL +``` + +### 2-1. 컴포넌트 책임 (시스템 관점) + +| 컴포넌트 | 실행 단위 | 책임 | 확장 방식 | +|---|---|---|---| +| **역할별 프론트 6종+공개** | nginx 정적 서빙(역할별 dist 번들) | organizer·exhibitor·contractor·ops·admin(내부)·public/visitor. SPA. 데스크톱=설계·에디터, 모바일=현장·조회·승인 | CDN + 정적 스케일(무상태) | +| **공개 홍보 사이트(M12·M17)** | SSR/SSG 렌더 경로 + CDN | SEO·다국어(한/영/중/일)·hreflang·사이트맵, 공개 플로어플랜(M2 부산물) | CDN 캐시·정적 재생성, 인증영역과 별도 | +| **공유 백엔드(Spring Boot jar)** | `java -jar`(systemd, N 인스턴스) | REST+WebSocket, 룰/배치/배선/옥션/BI/CMS 서비스, 큐 발행·상태·콜백 | **수평 확장(무상태)** — 세션·순위·타이머는 Redis 외부화 | +| **나노바나나 워커** | Python 데몬(systemd, M 인스턴스) | Redis 큐 소비 → Gemini image-to-image → 워터마크 후처리 → OBJ 적재 → WebSocket 콜백. 재시도·비용상한·스키마해시 캐시·쿼터(성공 시 차감) | **큐 깊이 기반 수평 확장**(GPU/비용 독립 스케일) | +| **서류·PDF·EDM 워커** | Python/Java 데몬(동일 큐 계약) | 서식(HWP/PDF)·견적서 PDF(M15)·EDM 발송 비동기 | 큐 깊이 기반 확장 | +| **Redis** | 관리형/전용 노드(HA) | 작업 큐, 옥션 실시간 순위·라운드 마감 타이머, 응답 캐시·스키마해시 캐시, RenderJob 쿼터, WebSocket 세션 상관 | Sentinel/Cluster(§3-2) | +| **PostgreSQL+PostGIS** | 프라이머리 + 읽기 복제 | 업무·공간 데이터 단일 진실원천(Flyway 스키마) | 프라이머리 수직 + 읽기 복제 수평(BI·공개조회 오프로드) | +| **오브젝트 스토리지** | S3 호환/파일 스토어 | 도면·생성이미지·서식·콘텐츠·마이크로사이트 자산 | 스토리지 독립 확장 + CDN 프론팅 | + +> **AA 위임**: 모듈 경계·패키지·API 표준·응답 봉투는 [`app.md`](app.md). 본 표는 **배포 단위·확장 특성** 관점만. + +--- + +## 3. 비기능요건(NFR) + +> 정량 목표는 킨텍스 실측(홀당 200~600부스, 홀 10개, 대형 행사 수만 관람객)과 PLANNING §10 리스크(R6 이미지 비용·지연)를 근거로 산정. 값은 **초기 목표치**이며 파일럿(Phase 1) 실측으로 보정한다. + +### 3-1. 확장성 (Scalability) + +| 항목 | 설계 | 근거·목표 | +|---|---|---| +| **백엔드 수평 확장** | Spring 인스턴스 무상태화 — JWT(자족적)·세션 없음, 옥션 순위·타이머·쿼터·캐시는 **Redis 외부화**. WebSocket은 STOMP+Redis 브로커 릴레이(다중 인스턴스 팬아웃) | 인스턴스 N대 로드밸런싱, 무중단 스케일아웃 | +| **워커 독립 확장** | 나노바나나·서류·EDM 워커는 백엔드와 **분리 스케일** — 큐 깊이(대기 Job 수) 기반 인스턴스 증감 | 이미지 생성 피크(행사 오픈 전 대량 컨펌)를 백엔드 응답성과 무관하게 흡수 | +| **읽기 부하 오프로드** | BI 집계·공개사이트 조회·플로어플랜 열람은 **읽기 복제본** 라우팅. 운영 프라이머리는 쓰기 트랜잭션 보호 | BI(M16) 무거운 집계가 운영 DB를 압박하지 않음(PLANNING §8-1) | +| **공개 트래픽 흡수** | 공개 홍보 사이트·공개 플로어플랜·이미지는 **CDN·SSG 캐시**로 오리진 오프로드. 사전등록만 오리진 write | 관람객 트래픽 급증(행사 D-데이) 시 오리진 보호 | +| **데이터 파티셔닝 여지** | 행사(Event) 단위 격리(§5) → 대량 데이터(리드·체크인·RenderJob)는 행사·기간 파티션 가능(DA 후속) | 다년·다행사 누적 확장 | +| **멀티테넌시** | 행사 단위 워크스페이스 + 플랫폼 RBAC. 신규 행사는 데이터·권한만 추가(코드·인프라 불변) | 무제한 행사 온보딩 | + +### 3-2. 가용성 · HA (Availability) + +| 계층 | HA 설계 | 목표 | +|---|---|---| +| **프론트/공개** | 정적 번들 CDN 다중 엣지 + nginx 다중화 | 단일 노드 장애 무영향 | +| **백엔드** | 최소 2 인스턴스 + 헬스체크(`GET /health`) 로드밸런싱. 롤링 배포(무중단) | 무중단 배포·인스턴스 장애 격리 | +| **Redis** | Sentinel(자동 failover) 또는 Cluster. 큐 손실 방지 위해 **AOF 지속화** + 소비자 ACK/재큐 | 프라이머리 다운 시 자동 승격, 진행 중 Job 유실 0 | +| **PostgreSQL** | 프라이머리-스탠바이 스트리밍 복제 + 자동 failover(Patroni/관리형). PITR 백업 | RPO 최소·RTO 분 단위 | +| **워커** | 무상태 소비자 N대. 처리 중 죽으면 **가시성 타임아웃 후 재큐**(멱등 Job) | 워커 장애가 사용자 응답 차단 안 함(비동기) | +| **오브젝트 스토리지** | 복제·버전 관리 스토리지 | 자산 내구성 보장 | +| **Fail-Safe 배포** | 백업 → 배포 → 헬스체크(200) → 실패 시 롤백(이전 jar 유지). clean bootJar 검증 후 교체 | 깨진 배포가 서비스 중단 유발 안 함(`../BUILD_DEPLOY.md`) | +| **degraded 모드** | G1 미승인/Gemini 장애 시 워커 목 응답(구조·워터마크 유지). Claude 실패 시 Ollama 폴백(AiTextRouter). PG 복제 지연 시 프라이머리 폴백 | 외부 의존 장애 시에도 코어 기능 유지 | + +> **HA 목표 SLO(초기)**: 코어 인증·설계·조회 경로 **99.5%**(행사 성수기 99.9% 지향). 나노바나나 생성은 **best-effort 비동기**(SLA 대상 아님, 큐 소진 목표만). — 구체 SLO/알림 임계는 TA 관측성([`tech.md`](tech.md))과 연계. + +### 3-3. 성능 (Performance) + +**(a) 대량 부스 · 공간 연산** + +| 시나리오 | 부하 특성 | 설계 대응 | 목표 | +|---|---|---|---| +| 플로어플랜 3안 생성(M2) | 홀당 200~600부스 제약 솔버 | 결정적 솔버(비-LLM) + 서비스 계층 연산, LLM은 조건 해석만. 무거운 생성은 필요 시 비동기화 | 3안 생성 수 분 내(PLANNING 목표) | +| 규정 검증·최단 배선(M2/M4) | PostGIS 버퍼·거리·KNN(`<->`) 연산 | 공간 인덱스(GiST) + 매퍼 XML `ST_*` 최적화. 홀 단위 스코프 쿼리 | 배치·배선 상호작용 P95 < 2s | +| 부스 목록·조회 | 대형 행사 수천 부스 그리드 | 페이지네이션(PageResponse) + 읽기 복제 + 캐시 | 목록 P95 < 500ms | + +**(b) 이미지 생성 큐 (나노바나나)** + +| 항목 | 산정 | 설계 | +|---|---|---| +| 생성 지연 | 단건 평균 ~40s(design 배지), Gemini 왕복 의존 | **전면 비동기** — 사용자는 진행 배지·WebSocket 완료 푸시 수신. 동기 대기 없음 | +| 대량 동시 요청 | 홀당 200~600부스 × S1·S7 자동(PLANNING R6) | **자동 생성은 S1·S7 한정**, 나머지 온디맨드. **동일 스키마해시 캐시**로 재생성 회피. 행사별 **RenderJob 쿼터**(성공 시에만 차감) | +| 처리량 스케일 | 워커 M대 병렬, 큐 깊이 기반 증감 | 피크 시 워커 스케일아웃으로 큐 소진 시간 제어. Gemini 쿼터·비용 상한은 워커 레벨 관리 | +| 전처리 절감 | 도면/현장사진 클라이언트 Canvas **긴 쪽 1024px 다운스케일 + JPEG 0.85**(ReRoomAI 실증) | 전송량·모델 비용·지연 동시 절감 | +| S6 배선 오버레이 | 좌표 정확성 목적 | **백엔드 래스터 합성 우선**(Gemini 미경유 결정적 산출) — 생성 비용·지연에서 제외 | + +**(c) 공개사이트 트래픽** + +| 항목 | 설계 | 목표 | +|---|---|---| +| 공개 홍보/플로어플랜 | SSR/SSG + CDN 캐시, 오리진 캐시 미스만 read-replica | 캐시 히트 P95 < 200ms, 트래픽 스파이크 CDN 흡수 | +| 사전등록 write | 오리진 write(멱등·중복 등록 가드) + 큐잉 완충(배지 발급 비동기) | 등록 급증 시 write 완충 | +| 실시간 옥션 순위(M15) | Redis 정렬셋 순위 + WebSocket 델타 푸시, 라운드 마감 타이머 | 순위 갱신 < 1s, 다중 인스턴스 팬아웃 | + +### 3-4. 용량 산정 (Capacity — 초기 근사) + +> 단일 대형 행사 기준. 킨텍스 대형 홀(홀7/8 각 510부스, 홀9/10 ~600부스), 대형 행사 관람객 수만 명 가정. **Phase 1 파일럿 실측으로 교정**. + +| 자원 | 산정 기준 | 초기 용량 | +|---|---|---| +| **부스 데이터** | 대형 행사 3,000~5,000부스 × (폴리곤 + DesignPlan 버전 + UtilityOrder 배선) | 행사당 수만 지오메트리 레코드 — GiST 인덱스 필수 | +| **RenderJob/이미지** | 부스 3,000 × 자동 2샷(S1·S7) + 온디맨드 α, 이미지 평균 0.5~2MB(1024px) | 행사당 ~수천~1만 이미지, 수 GB~수십 GB → OBJ + CDN. 캐시로 재생성 억제 | +| **리드·체크인(M10)** | 관람객 수만 × 체크인 이벤트 + 참가업체 리드캡처 | 행사당 수만~수십만 이벤트 로우 → 파티셔닝 후보(개인정보 §5) | +| **동시 사용자** | 설계 피크(주최자·업체) 수백 + 공개/관람객 수천~수만(D-데이) | 인증영역 수백 동시(백엔드 N대) / 공개영역 CDN 흡수 | +| **Redis** | 큐 대기 Job + 옥션 순위셋 + 캐시 + 쿼터 카운터 | 수 GB. 큐 백로그 상한·모니터링 | +| **DB 연결풀** | 공유 백엔드 다인스턴스 × Hikari 풀 | **Hikari max 제한 + PgBouncer 권고**(GUARDiA 실측 교훈: 공유 PG `max_connections` 포화 방지). 인스턴스 수 × 풀 ≤ PG 상한 | +| **오브젝트 스토리지** | 도면 + 이미지 + 서식 + 콘텐츠, 다년 누적 | 행사당 수십 GB, 보존정책(§5-3) 기반 아카이빙 | + +--- + +## 4. 배포 토폴로지 + +> **논리 토폴로지 확정**. 물리 서버·도메인·포트·TLS는 **G2 확정 후 Phase E**(`E-DEP`, `kintex-devops-dev`)에서 실체화. DMZ/방화벽/부하분산 세부는 NA([`network.md`](network.md)). + +### 4-1. 배포 존(Zone) 구성 + +```mermaid +graph TB + subgraph PUBZ["공개 존 (DMZ / 인터넷 노출)"] + LB1[Reverse Proxy / WAF · nginx · TLS] + CDNZ[CDN 엣지] + PUBFE[공개 프론트 · SSR/SSG 정적] + end + subgraph APPZ["애플리케이션 존 (내부망)"] + APPFE[인증 프론트 정적 서빙
organizer·exhibitor·contractor·ops] + APPBE[공유 백엔드 jar × N · systemd] + WKZ[워커 데몬 × M · systemd
나노바나나 · 서류/EDM] + end + subgraph MGMTZ["관리 존 (내부 전용 · 접근 제한)"] + ADMFE[admin. 백오피스 프론트] + end + subgraph DATAZ["데이터 존 (내부, 최심부)"] + PGZ[(PostgreSQL+PostGIS
프라이머리+스탠바이)] + RPLZ[(읽기 복제본)] + REDISZ[(Redis HA)] + OBJZ[(오브젝트 스토리지)] + end + subgraph EGRESS["아웃바운드 게이트 (워커/백엔드 한정)"] + OUT[허용 아웃바운드
Gemini(G1)·Claude·PG결제·kxwp] + end + CDNZ --> PUBFE + LB1 --> PUBFE & APPFE & ADMFE + APPFE & ADMFE --> APPBE + APPBE --> PGZ & REDISZ & RPLZ & OBJZ + WKZ --> REDISZ & OBJZ + WKZ --> OUT + APPBE --> OUT + PGZ -.복제.-> RPLZ +``` + +### 4-2. 배포 단위(산출물) — `../BUILD_DEPLOY.md` §1 정합 + +| 단위 | 빌드 | 산출물 | 실행 | 배포 존 | +|---|---|---|---|---| +| 인증 프론트 6종 | `vite build`(역할별 번들) | `dist/`(organizer·exhibitor·contractor·ops·admin) | nginx 정적(SPA 폴백) | APP존(admin은 MGMT존) | +| 공개 프론트 | SSR/SSG 빌드 | 정적/렌더 번들 | CDN + 엣지 렌더 | 공개존 | +| 공유 백엔드 | `./gradlew bootJar`(JDK17) | `kintex-*.jar` | `java -jar` systemd × N | APP존 | +| 나노바나나 워커 | (빌드 없음) | `tools/nanobanana` | 큐 소비 데몬 systemd × M | APP존(아웃바운드 게이트) | +| 서류/EDM 워커 | — | 워커 모듈 | 큐 소비 데몬 | APP존 | + +- **배포 흐름**(GUARDiA 표준 준용): `workspace/kintex` → git push → Gitea(`zio/kintex`) → webhook → deploy_server → 빌드(`bootJar`·`vite build`) → Flyway 마이그 → jar 재기동·dist 배포 → 헬스 게이트(`GET /health` 200) → 실패 시 롤백. +- **systemd 유닛**: 백엔드 jar · 워커 데몬 각각 유닛(부팅 자동기동·재시작). AI env drop-in(`ANTHROPIC_API_KEY`·`ADMIN_PASSWORD_ENC`)은 표준 프레임워크 §7 방식. **`GEMINI_API_KEY`는 나노바나나 워커 유닛에만** 주입(백엔드·프론트 미취급). +- **nginx vhost**: `` → `/`=프론트 정적, `/api/`·`/ws`=백엔드 포트. 관리자 백오피스(admin.)는 **별도 vhost + 접근 제한**(§5-1). TLS certbot. — 도메인·포트=G2. +- **환경 분리**: dev / staging / prod. staging에서 헬스·마이그·경계면 검증 후 prod 승격(운영 배포는 소유자 승인 필수). + +> CI/CD 파이프라인 상세(deploy_server·롤백·webhook)는 **Phase E `kintex-devops-dev`** 정본. TA([`tech.md`](tech.md))의 빌드·관측성 표준과 연계. + +--- + +## 5. 보안영역 · 데이터 보존 · 개인정보 경계 + +### 5-1. 보안영역 분리 (공개 vs 내부 백오피스) + +| 영역 | 대상 | 노출 | 인증 | 격리 | +|---|---|---|---|---| +| **공개 영역** | www/expo 홍보사이트, 공개 플로어플랜, 사전등록, 관람객 앱 | 인터넷(DMZ) | 비인증 또는 셀프서비스(쓰기 제한) | 별도 렌더 경로·read-only API·행사 데이터 write 불가 | +| **인증 업무 영역** | organizer·exhibitor·contractor·ops | 인증 후 접근 | JWT SSO + TOTP 2FA + 행사 RBAC | 행사 단위 데이터 격리(§5-2) | +| **관리 영역(백오피스)** | admin. — 사용자·RBAC·마스터데이터·룰셋·감사로그·크로스테넌트 | **내부 전용**(웹 전용, 모바일 미제공) | ADMIN 역할 + 2FA 필수 + **접근 제한**(내부망/허용 IP — NA 확정) | 별도 vhost·존, 공개영역과 물리·논리 분리 | + +- **최소권한·공격면 축소**: 6개 프론트 번들 분리로 역할별 코드·권한 최소화. 백오피스는 공개 인터넷에 노출하지 않는다. +- **이중 RBAC 평가**: 플랫폼 레벨(관리자)과 행사 레벨(주최자/참가/업체/홀매니저)을 JWT 클레임(`roles` eventId→역할, `hm` 홀매니저)으로 이중 평가. `/api/admin/**`·`/api/system/**` = `hasRole(ADMIN)`(PLANNING §5B-1, `../DEVELOPMENT_GUIDE.md` §4). +- **인증 스택(UIWS 표준 이식)**: JWT(HS256) + TOTP(RFC6238 SHA1·30s·6자리·±1) 2차 인증 + 로그인 실패 잠금 + admin 비번 env(`ADMIN_PASSWORD_ENC` AES-256-GCM + 별도 키파일 재시드, `admin123` 하드코딩 금지). 대상: 관리자·홀매니저·주최자·업체(내부/발주 권한) 2FA 필수, 관람객 셀프서비스 선택. + +### 5-2. 데이터 격리 (행사 단위 · 영업비밀) + +- **행사(Event) 단위 워크스페이스**: 부스 설계는 경쟁사에 민감(PLANNING R10). 부스 데이터 접근은 **소유 참가업체 + 주최자 + 홀매니저**로 한정. 모든 도메인 경로는 `{eventId}` 스코프 + RBAC 가드. +- **옥션(M15) 자료 격리**: AI 설계자료(M2~M5)는 옥션 초대된 **킨텍스 등록업체(M7 검증 통과)**만 열람. 미등록 업체 응찰 원천 차단(`NOT_REGISTERED_COMPANY` 403). +- **BI 관점 격리**: 참가업체 관점 ROI(자기 부스)와 운영사 관점 수익성(전 행사)은 **별도 대시보드·권한**으로 격리(PLANNING M16-1). + +### 5-3. 개인정보 경계 · 데이터 보존 + +| 데이터 | 개인정보 등급 | 처리 원칙 | 보존 | +|---|---|---|---| +| 관람객 등록·배지·체크인·리드(M10) | **개인정보(PLANNING R10)** | 수집 시 **동의** 필수. 리드 접근은 감사로그(`TB_AUDIT_LOG`) 전수 기록. 참가업체는 자기 리드만 | **보존정책 명시**(행사 후 N개월, 동의 철회·삭제권 지원 — DA 확정) | +| 부스 설계·도면·생성이미지 | 참가업체 자산·영업비밀 | 행사 단위 격리, 응답에서 내부 식별자·해시 shape 제외 | **행사 종료 후 보존 정책 별도**(참가업체 자산, PLANNING §8) | +| 결제·정산(M9) | 금융·과세 정보 | PG 위임(카드정보 비보관), 세금계산서 연동 | 법정 보존 기간 준수 | +| 자격증명·비밀·API키 | 최고 민감 | **응답·로그·에러·커밋에 절대 노출 금지**. `GEMINI_API_KEY`·`ANTHROPIC_API_KEY`·OTP 시크릿·비번 해시 미노출. AES-256-GCM 저장, 비번 BCrypt | env only, DB·코드 미기록 | +| AI 생성 이미지 | 오인 위험(R1) | **워터마크 강제**("AI 생성 예상 이미지…") + 메타데이터. 계약·심사 서류 자동 배제. 제거 불가 | 캐시·스키마해시 관리 | + +- **보안 불변(위반=QA 반려)**: 스택트레이스 차단(`GlobalExceptionHandler`·`DataAccessException` 핸들러), 민감필드 응답 제외, AI 워터마크 항상 포함, 등록업체 응찰 가드, admin env 시드. — `../DEVELOPMENT_GUIDE.md` §5 계약. +- **감사 추적**: 승인·낙찰(M15)·설계 변경·룰셋 개정·리드 접근(개인정보) 전수 `TB_AUDIT_LOG` 기록(PLANNING §5B-1). + +--- + +## 6. 연동 아키텍처 + +### 6-1. SSO · 역할 RBAC (6역할) + +- **단일 JWT SSO** 위에 플랫폼 레벨 + 행사 레벨 권한 이중 평가. 6역할: 주최자·참가업체·장치/공사업체·킨텍스 직원(홀매니저·운영)·관리자·관람객/일반대중. +- 역할별 프론트는 동일 SSO·API 계약을 상속하되 **번들·도메인 분리**. 백오피스(admin.)는 내부 전용·2FA 필수(§5-1). +- 등록업체 계정은 **M7 등록업체 DB 검증** 통과분만 초대·옥션 응찰. 관람객·일반대중은 셀프서비스(쓰기 제한). +- 인증 상세 계약은 [`app.md`](app.md)(AA)·`../DEVELOPMENT_GUIDE.md` §4. + +### 6-2. 외부 게이트 (아웃바운드 통제) + +| 연동 | 경로 | 통제 | 게이트 | +|---|---|---|---| +| **나노바나나(Gemini)** | 나노바나나 **워커 전용** 아웃바운드 → `generativelanguage.googleapis.com` | `GEMINI_API_KEY` 워커 env only. 워커 서브넷만 아웃바운드 허용(NA). 미승인 시 목/degraded | **G1** | +| **Claude(텍스트 AI)** | 백엔드 `AiTextRouter` → `api.anthropic.com` | `ANTHROPIC_API_KEY` env only(코드·DB·로그·응답 미기록). **실패 시 Ollama 폴백** | 승인됨(예외) | +| **PG 결제(M9)** | 백엔드 → 국내 PG(카드·계좌·세금계산서) | 카드정보 비보관(PG 위임), 결제 콜백 검증 | — | +| **kxwp/kxfp 작업신고(M6)** | **파일 릴레이**(제출용 파일 생성 + 업로드 안내) | 폐쇄형·API 미공개(R3). 초기 수동 릴레이, 정식 API는 킨텍스 협의(Phase 3) | — | +| **등록업체 DB(739)** | 주기 수집 → 자체 DB화 | 공개 데이터 수집, 추후 공식 피드 | — | +| **행사일정 시스템** | 공개 캘린더 수집 → 가용성 역산 | 정확 가용성은 킨텍스 내부 데이터 협의 | — | +| **CDN** | 공개 자산·이미지 프론팅 | 공개 read-only 자산만 | — | + +- **아웃바운드 원칙**: 인터넷 아웃바운드는 **워커·백엔드 특정 경로만 허용**(화이트리스트). 그 외 외부 API 금지(GUARDiA 보안 제약, `../DEVELOPMENT_GUIDE.md` §5). 세부 방화벽 규칙은 NA([`network.md`](network.md)). + +### 6-3. 비동기 큐 계약 (Redis) + +- Spring 백엔드는 RenderJob·서류·EDM·알림을 **Redis 큐에 발행**하고 상태만 관리. 워커가 소비·처리·완료 콜백(WebSocket). **Java 재구현 대신 얇은 큐 계약으로 결합**(PLANNING §8, google-genai는 Python SDK → 워커 유지). +- 큐 신뢰성: AOF 지속화 + 소비자 ACK + 가시성 타임아웃 재큐(멱등 Job). 쿼터·순위·타이머·캐시도 Redis(§2-1). + +--- + +## 7. 아키텍처 결정 요약 (ADR-lite) + +| # | 결정 | 이유 | 대안·트레이드오프 | +|---|---|---|---| +| 1 | 나노바나나를 **Python 워커 사이드카**로 분리 | google-genai=Python SDK, ReRoomAI 검증 방어로직 이식(Java 재구현 회피). 이미지 생성 독립 스케일 | Java 통합(재검증 비용·강결합) 배제 | +| 2 | **역할별 프론트 번들 분리** | 최소권한·공격면 축소, 공개/백오피스 격리 | 단일 SPA(권한 혼재·공격면 확대) 배제. 공유 디자인/API로 중복 완화 | +| 3 | 백엔드 **무상태 + Redis 외부화** | 수평 확장·무중단 배포·다중 인스턴스 WebSocket 팬아웃 | 인메모리 상태(ReRoomAI Map 방식) — 재시작·다중인스턴스 취약(§reroomai (F)) 배제 | +| 4 | **읽기 복제 오프로드**(BI·공개조회) | 운영 프라이머리 보호(PLANNING §8-1) | 단일 DB 직조회(BI 부하 전파) 배제 | +| 5 | **CDN + SSG**로 공개 트래픽 흡수 | 관람객 스파이크·SEO·다국어 | 오리진 직서빙(스파이크 취약) 배제 | +| 6 | 룰셋 **버전 데이터** 분리 | 연 단위 요율·규정 개정 무중단 반영 | 코드 하드코딩(배포 필요) 배제 | +| 7 | S6 배선 오버레이 **백엔드 래스터 우선** | 좌표 정확성(생성 왜곡 배제)·비용/지연 제외 | Gemini 생성(재질·색 왜곡) 배제 | + +--- + +## 8. 교차참조 + +- 기획 정본: [`../PLANNING.md`](../PLANNING.md) §2·§7·§8·§10 +- 구현 백로그: [`../IMPLEMENTATION_BACKLOG.md`](../IMPLEMENTATION_BACKLOG.md) Phase A~E +- 나노바나나 파이프라인 근거: [`../analysis/reroomai-source.md`](../analysis/reroomai-source.md) +- 애플리케이션 아키텍처(AA·A-1): [`app.md`](app.md) — 모듈 경계·API 표준·패키지 +- 기술 표준(TA·A-3): [`tech.md`](tech.md) — 빌드·배포·관측성·AiTextRouter +- 데이터 아키텍처(DA·A-4): [`data.md`](data.md) — 전사 ERD·공간·마스터·BI 마트 +- 네트워크 아키텍처(NA·A-5): [`network.md`](network.md) — DMZ/방화벽/부하분산/아웃바운드 +- 개발 표준: [`../DEVELOPMENT_GUIDE.md`](../DEVELOPMENT_GUIDE.md) · 빌드/배포: [`../BUILD_DEPLOY.md`](../BUILD_DEPLOY.md) + +--- + +## 9. 변경 이력 + +| 버전 | 일자 | 작성자 | 내용 | +|---|---|---|---| +| v1.0 | 2026-07-11 | SA | 최초 작성(A-2) — 시스템 구성도(역할별 프론트·공유 백엔드·백오피스·공개사이트·나노바나나 워커·Redis·PostGIS), 논리/배포 토폴로지, NFR(확장성·HA·성능(대량부스·이미지큐·공개트래픽)·용량 산정), 연동 아키텍처(SSO/RBAC 6역할·PG결제·kxwp 릴레이·외부 게이트 Claude/Gemini), 보안영역(공개 vs 백오피스)·데이터 보존·개인정보 경계, G1/G2 게이트, ADR-lite. PLANNING §8 정합·확정 스택 준수. AA/TA/DA/NA 교차참조 | diff --git a/docs/architecture/tech.md b/docs/architecture/tech.md new file mode 100644 index 0000000..345d7d5 --- /dev/null +++ b/docs/architecture/tech.md @@ -0,0 +1,324 @@ +# 킨텍스 자동전시시스템 — 기술 표준 (Technical Architecture) + +> 작성: 기술 아키텍트(TA) · 작성일: 2026-07-11 · 버전: v1.0 +> 근거: `docs/PLANNING.md` v2.0(§8 확정 스택·§8-1 아키텍처 보강·§10 리스크)·`docs/IMPLEMENTATION_BACKLOG.md`(A-3)·실측 스캐폴드(`src/backend/build.gradle`·`src/backend/src/main/resources/application.yml`·`src/frontend/package.json`·`tools/nanobanana/`)·`.claude/agents/kintex-ai-dev.md` +> 교차참조: 앱 아키텍처 `docs/architecture/app.md`(A-1) · 시스템/NFR `docs/architecture/system.md`(A-2) · 데이터 `docs/architecture/data.md`(A-4) · 네트워크 `docs/architecture/network.md`(A-5) +> **문서 소유권**: 본 tech.md는 TA만 수정한다. 확정 스택·버전·빌드/배포·개발표준·관측성·AI 프로바이더 표준의 단일 출처(SSOT)다. 스택 변경은 본 문서 개정을 선행한다. + +--- + +## 0. 이 문서의 위치 + +Phase A(아키텍처·거버넌스) 4개 표준 문서 중 **기술 표준(A-3)** 이다. 애플리케이션 경계·레이어(app.md), NFR·토폴로지(system.md), 데이터 모델(data.md), 네트워크(network.md)와 정합한다. 본 문서는 "무엇을 어떤 버전으로, 어떻게 빌드·배포·개발·관측하는가"의 기술 규범을 확정한다. 기능 범위·모듈 우선순위는 PLANNING이 권위이며 본 문서는 그 위 기술 계층만 다룬다. + +핵심 원칙 4가지: +1. **실측 스캐폴드 정합** — 이미 스캐폴드된 실제 스택(Spring Boot 3.2.5·React 18.3.1·google-genai 워커)에 표기를 맞춘다. "3.x" 같은 느슨한 표기 대신 핀 버전을 SSOT로 둔다. +2. **GUARDiA/UIWS 표준 정렬** — kintex는 `zio/kintex` 독립 저장소이나 GUARDiA 표준 프레임워크(UIWS)와 스택·인증·AI 프로바이더 패턴을 공유한다. +3. **보안 불변 우선** — 시크릿 env-only, 스택트레이스·자격증명·PII 미노출, AES-256-GCM은 코드보다 상위 제약(§10, PLANNING §10·보안 불변). +4. **결정론과 폐쇄망 우선** — 규정/요율은 버전 관리 데이터, AI는 온프레미스 폴백 필수, 외부 아웃바운드는 승인된 도메인만. + +--- + +## 1. 확정 기술 스택 (버전 표준·SSOT) + +> 아래 버전은 **실측 스캐폴드에서 채택된 값**이다. 임의 상향/하향 금지 — 변경은 TA 승인 + 본 표 개정 후. + +### 1-1. 백엔드 (Spring Boot · Java 17 · MyBatis) + +`src/backend/build.gradle` 기준. + +| 항목 | 표준 값 | 근거/비고 | +|---|---|---| +| 언어/런타임 | **Java 17** (`sourceCompatibility`/`targetCompatibility` = 17) | GUARDiA 표준(전 솔루션 Java 17 정렬). Java 21 금지 | +| 프레임워크 | **Spring Boot 3.2.5** | 핀 버전. `org.springframework.boot` 플러그인 | +| 의존성 관리 | `io.spring.dependency-management` **1.1.4** | Spring Boot BOM 정렬 | +| 빌드 도구 | **Gradle** (wrapper 동봉 `gradlew`/`gradlew.bat`) | 시스템 Gradle 미의존, wrapper 고정 | +| 그룹/패키지 | `com.zioinfo.kintex` / rootProject `kintex-backend` | GUARDiA 네이밍 규약 | +| 버전 | `0.1.0-SNAPSHOT` | SemVer, 릴리스 시 `-SNAPSHOT` 제거 | +| ORM | **MyBatis** `mybatis-spring-boot-starter` **3.0.3** | Spring Boot 3.2.x 호환 핀. JPA 금지(공간 SQL은 매퍼 XML) | +| DB 드라이버 | `org.postgresql:postgresql` (runtimeOnly, BOM 관리) | PostGIS 함수는 `ST_*` 매퍼 XML | +| 캐시/큐 | `spring-boot-starter-data-redis` | RenderJob·서류·알림 큐 + 옥션 실시간 순위 | +| 실시간 | `spring-boot-starter-websocket` (STOMP) | RenderJob 완료·옥션 순위 푸시 | +| 인증 | `spring-boot-starter-security` + **jjwt 0.12.5** (api/impl/jackson) | JWT HS256 + RBAC + TOTP 2FA | +| 검증 | `spring-boot-starter-validation` | DTO Bean Validation | +| 보일러플레이트 | Lombok (compileOnly + annotationProcessor) | | +| 테스트 | `spring-boot-starter-test` + `spring-security-test`, JUnit Platform | | +| 인코딩 | **UTF-8 강제** (`JavaCompile.options.encoding = 'UTF-8'`) | Windows javac CP949 한글 리터럴 손상 방지 — **불변, 전 모듈 유지** | + +**MyBatis 규약**(application.yml 기준): +- `mapper-locations: classpath*:mybatis/mapper/**/*.xml` +- `map-underscore-to-camel-case: true`, `jdbc-type-for-null: NULL` +- `@MapperScan`은 GUARDiA 표준(`annotationClass = Mapper.class`) — 빈 누락 크래시 방지(다른 솔루션 회귀 이력). db-engineer가 공간 SQL 매퍼 XML 소유. + +**Hikari 풀 표준**: `maximum-pool-size = ${DB_POOL_MAX:3}`. 공유 PostgreSQL 보호(GUARDiA 표준). kintex 전용 DB `kintex_db`라도 서버 공용 PG면 캡 유지. 상향 필요 시 SA(system.md)·DA와 합의. + +### 1-2. 프론트엔드 (React · Vite · TypeScript) + +`src/frontend/package.json`·`vite.config.ts`·`tsconfig.json` 기준. + +| 항목 | 표준 값 | 비고 | +|---|---|---| +| UI 라이브러리 | **React 18.3.1** (`react`/`react-dom`) | PLANNING "18/19" 중 **18.3.1 확정 채택** | +| 빌드/번들러 | **Vite 5.4.8** + `@vitejs/plugin-react` 4.3.2 | dev 서버 :5173, `/api`·`/ws` 프록시 | +| 언어 | **TypeScript 5.6.2** (`strict: true`) | `noUnusedLocals`/`noUnusedParameters`/`noFallthroughCasesInSwitch` 켬 | +| 라우팅 | `react-router-dom` **6.26.2** | 역할별 포털 라우팅 | +| 서버 상태 | `@tanstack/react-query` **5.59.0** | API 캐싱·재검증 표준. 수동 fetch 지양 | +| 클라이언트 상태 | `zustand` **4.5.5** | 전역 상태(경량). Redux 금지 | +| 실시간 | `@stomp/stompjs` **7.0.0** + `sockjs-client` **1.6.1** | 백엔드 STOMP 정합 | +| 경로 별칭 | `@/*` → `src/*` (vite alias + tsconfig paths) | 상대경로 지옥 회피 | +| 모듈 타입 | `"type": "module"` (ESM), `target ES2020` | | + +**빌드 스크립트**(package.json): `build = "tsc -b && vite build"`, `typecheck/lint = "tsc --noEmit"`. **타입 에러는 빌드 실패** — CI 게이트. + +**역할별 프론트 분리**(PLANNING §2-1·§8-1): organizer·exhibitor·contractor·ops·admin(인증) + public/visitor(공개). **번들 분리** 방식은 designer/FE 트랙 결정(모노레포 다중 진입점 vs 서브패스). 공유 디자인 시스템(design.md)·공유 컴포넌트·공유 API 계약은 **상속**(중복 구현 금지). 현재 스캐폴드는 단일 Vite 앱(`src/frontend`) — 분리 실행 시 본 표준의 라이브러리 버전을 전 번들이 공유한다. + +### 1-3. 나노바나나 Python 워커 (사이드카) + +`tools/nanobanana/` 기준. PLANNING §8 "Python 워커 유지 근거"(google-genai는 Python SDK, ReRoomAI 검증 client.py 재사용 — Java 재구현 회피). + +| 항목 | 표준 값 | 비고 | +|---|---|---| +| 런타임 | **Python 3.11+** (개발 실측 3.14 `__pycache__`) | 배포는 3.11/3.12 LTS 권장(3.14는 개발 로컬) | +| 이미지 SDK | **google-genai** (`pip install google-genai`) | Gemini image-to-image. 지연 임포트(무네트워크 import 성립) | +| 모델 | `gemini-3.1-flash-image-preview` (나노바나나 2, env `NANOBANANA_MODEL`) | 하드코딩 아님, env 오버라이드 | +| 이미지 처리 | **Pillow(PIL)** | S6 배선 오버레이 결정적 래스터 합성·목 플레이스홀더 | +| 큐/이벤트 | **redis** (`redis.from_url`, BLPOP 소비 + pub/sub 발행) | 지연 연결 | +| 실행 | `python -m tools.nanobanana.worker` (루프) / `--smoke` (무네트워크) | | + +**워커 불변식**(worker.py 헤더): ①G1 게이트 — 실 Gemini 호출은 `NANOBANANA_LIVE=1` + `GEMINI_API_KEY` 동시 충족 시만, 기본 목/degraded. ②지연 연결 — Redis 미기동이어도 `process_job()` 직접 호출 성립. ③S6은 생성형 아님(항상 로컬 PIL). ④비밀 미노출(키/IP/스택트레이스 미기록). + +**의존성 관리 표준**: 현재 워커에 `requirements.txt` **부재** — 배포 전 `tools/nanobanana/requirements.txt`(google-genai·Pillow·redis 핀 버전) 추가 필요(§9 백로그). devops-dev(DEV) 담당. + +### 1-4. 데이터·인프라 + +| 항목 | 표준 값 | 비고 | +|---|---|---| +| DB | **PostgreSQL + PostGIS** (`kintex_db`) | 공간 데이터 일원화(부스 폴리곤·트렌치 포인트·배선 LineString). 상세 DA/data.md | +| 캐시/큐/실시간 | **Redis** | 작업 큐 + 옥션 라운드 타이머 + 순위 | +| 오브젝트 스토리지 | 로컬 FS(기본 degraded 어댑터) → S3/GCS(운영) | `ObjectStore.save_image()` 반환 계약 유지하며 어댑터 교체 | +| 마이그레이션 | **미확정** — B-0 백로그는 Flyway 명시(현 build.gradle 미포함) | §3-1 참조. TA 결정: Flyway 채택 권고 | + +--- + +## 2. 통합 계약 (백엔드 ↔ 워커 ↔ 프론트) + +### 2-1. RenderJob 큐 계약 (Spring → Redis → Python 워커) + +Spring 백엔드가 RenderJob을 Redis 리스트에 push → 워커가 BLPOP 소비 → 오브젝트 스토리지 적재 → pub/sub 이벤트 발행 → 백엔드 구독 → WebSocket(STOMP) 프론트 푸시. 계약 단일 출처: `tools/nanobanana/_workspace/01_worker_contract.md`. + +> **★ 실측 불일치(리스크 R-T1, §10)**: 큐/채널 키 기본값이 백엔드와 워커에서 다르다. +> - `application.yml`: `kintex.render.queue-key = kintex:renderjob:queue` +> - `worker.py`: `NANOBANANA_QUEUE 기본 = kintex:renderjobs`, `EVENT_CHANNEL 기본 = kintex:renderjob:events` +> +> 양측 모두 env 오버라이드 가능하나 **기본값 불일치는 배포 시 조용한 무처리(silent no-op)** 위험. **표준 확정**: 큐 키 `kintex:renderjob:queue`, 이벤트 채널 `kintex:renderjob:events`로 통일하고 배포 env(`RENDER_QUEUE_KEY`/`NANOBANANA_QUEUE`/`NANOBANANA_EVENT_CHANNEL`)를 동일 값으로 명시 주입. BE·VIZ·DEV가 `01_worker_contract.md`에 최종 키를 고정한다. + +### 2-2. WebSocket(STOMP) 계약 + +- 백엔드 `WebSocketConfig`(STOMP) — 프론트 `@stomp/stompjs` + `sockjs-client`. dev는 Vite 프록시 `/ws`(ws:true). +- 이벤트: `renderjob.completed`/`renderjob.failed`(워커→백엔드→구독 클라), 옥션 순위 푸시(M15). 페이로드에 `image_ref`·`meta`(live/degraded 플래그) 포함, 비밀·스택트레이스 미포함. + +### 2-3. API 응답 봉투 + +실측: `common/ApiResponse.java`·`common/PageResponse.java`·`common/error/GlobalExceptionHandler.java` 존재. 표준 응답 봉투 + 페이지 봉투 + 전역 예외 핸들러로 **에러 응답 표준화**(스택트레이스 미노출, `ErrorCode` 코드+요약 메시지만). 상세 계약은 app.md(A-1) 소유 — 본 문서는 정합만 명시. + +--- + +## 3. 빌드·배포 표준 + +### 3-1. 백엔드 빌드 (Gradle · 단일 jar) + +- 빌드: `./gradlew clean bootJar` → 단일 실행 jar(`build/libs/kintex-backend-.jar`). GUARDiA 단일 jar 표준. +- **프론트→백엔드 static 번들**(GUARDiA 표준 옵션): 운영 배포는 역할별 프론트 번들을 백엔드 static 리소스 또는 nginx 정적 서빙 중 택1. 역할별 프론트 분리(§1-2)이므로 **백오피스/포털별 별도 정적 서빙 + 공유 백엔드 jar** 토폴로지가 기본(system.md 확정). 공개사이트(M12/M17)는 SEO·다국어로 별도 렌더 경로(SSR/정적 생성). +- 테스트: `./gradlew test`(JUnit Platform). compileJava·test 통과가 배포 게이트. +- **마이그레이션(TA 결정)**: B-0가 Flyway를 명시하나 현 build.gradle 미포함. **Flyway 채택 권고** — `org.flywaydb:flyway-core` + `flyway-database-postgresql`(PostGIS 정합) 추가, `db/migration/V__*.sql`(PostGIS 확장·공간 인덱스 포함). 시드/후행 테이블은 GUARDiA 교훈(멱등화 + 누출 차단) 준수 — sql.init `mode=never` 후행 추가 테이블 미적용 회귀(schema-integrity 하네스 교훈) 방지. DB 스키마 상세는 DA/data.md. + +### 3-2. 프론트 빌드 (Vite) + +- `npm ci && npm run build`(= `tsc -b && vite build`) → `dist/`. 타입 에러 시 실패. +- 역할별 번들 분리 시 각 진입점 빌드 산출물을 도메인/서브패스별 배포. +- **★로컬 rollup win32 크래시 함정(리스크 R-T2, §10)**: GUARDiA 전 프로젝트에서 로컬 Windows rollup 네이티브 렌더 크래시가 반복 관측됨(homepage-renewal·CMS 등). **표준 대응**: (1) CI/서버 빌드(Linux) 신뢰 — 서버 `npm run build`가 권위. (2) 로컬 검증은 `tsc --noEmit`(typecheck)로 대체하거나 esbuild 경로. (3) `package-lock.json` 커밋으로 `npm ci` 재현성 확보. (4) 로컬 크래시가 서버 빌드 성공을 막지 않음 — 서버 번들 검증(최신 청크 diff)로 마무리. + +### 3-3. 워커 배포 (systemd 서비스) + +- 별도 프로세스(사이드카). systemd 유닛으로 상주(`ExecStart=python -m tools.nanobanana.worker`), `Restart=on-failure`. +- env(`EnvironmentFile` 또는 drop-in): `REDIS_URL`·`NANOBANANA_QUEUE`·`NANOBANANA_EVENT_CHANNEL`·`NANOBANANA_OUTPUT_DIR`·(G1 승인 후)`NANOBANANA_LIVE=1`·`GEMINI_API_KEY`. **GEMINI_API_KEY는 워커 env에만**(백엔드 미보유 — application.yml 주석 명시). GUARDiA 서버는 명령줄 인자 기동 서비스가 많아 **systemd drop-in EnvironmentFile 방식** 채택(기존 ExecStart 불변, guardia-claude-ai 트랙 패턴). +- 미승인(G2/G1 전) 기본 목/degraded 모드로 상주 가능(무네트워크). + +### 3-4. CI/CD 파이프라인 + +GUARDiA 표준 흐름 정렬(솔루션 푸시 구조 메모리): +``` +workspace/kintex (개발·SSOT) + → repos/kintex (fresh git init — 모노레포 히스토리 상속 금지, bundle 비대화 방지) + → Gitea zio/kintex (push) + → webhook :9999 (deploy_server.py) + → 서버 빌드(gradlew bootJar + npm build + 워커 배포) → systemd 재시작 → health 게이트 +``` +- **선행 게이트 G2**(BACKLOG): 배포 대상 서버·포트(GUARDiA 인프라와 별개 도메인) 확정 전 Phase E 착수 금지. +- **함정(GUARDiA 교훈, 배포블록 반영 필수)**: ①`repos/kintex`는 반드시 fresh `git init`(모노레포 `.git` 상속 시 bundle 1.5GB 회귀). ②`deploy_server.py`에 kintex 블록 추가 시 **서버 `/opt/zioinfo/deploy_server.py` 사본 반영 + `zioinfo-deploy` 재시작 필수**(로컬만 고치면 웹훅 1ms no-op). ③백엔드 jar만이 아니라 **워커 서비스도 배포 대상**(별도 systemd). ④health 200 확인이 완료 게이트. + +### 3-5. 환경변수 표준 (시크릿 env-only) + +application.yml은 **모든 시크릿을 플레이스홀더로만** 주입(하드코딩 금지, 주석 명시). + +| env | 용도 | 소비자 | +|---|---|---| +| `DB_URL`/`DB_USER`/`DB_PASSWORD` | PostgreSQL(PostGIS) | 백엔드 | +| `DB_POOL_MAX` (기본 3) | Hikari 캡 | 백엔드 | +| `REDIS_HOST`/`REDIS_PORT`/`REDIS_PASSWORD` | Redis | 백엔드 | +| `REDIS_URL` | Redis(워커) | 워커 | +| `JWT_SECRET`(≥32B)/`JWT_ACCESS_TTL` | JWT HS256 | 백엔드 | +| `RENDER_QUEUE_KEY`/`NANOBANANA_QUEUE` | 큐 키(통일) | 백엔드/워커 | +| `NANOBANANA_EVENT_CHANNEL` | 이벤트 채널 | 워커/백엔드 | +| `RENDER_EVENT_QUOTA`(기본 500) | 행사별 생성 쿼터 | 백엔드 | +| `NANOBANANA_LIVE`/`GEMINI_API_KEY` | G1 승인 후 실 Gemini | **워커 전용** | +| `ANTHROPIC_API_KEY` | Claude AI(§6) | 백엔드 | +| `ADMIN_PASSWORD_ENC` + 키파일 | admin 비번(AES-256-GCM) | 백엔드 | +| `VITE_BACKEND_ORIGIN`/`VITE_API_BASE` | 프론트 오리진/프록시 | 프론트 | + +**admin 비번**(B-1·GUARDiA 표준): `admin123` 등 평문 시드 **금지**. `ADMIN_PASSWORD_ENC`(AES-256-GCM) + 별도 키파일 주입, 최초 기동 재시드(guardia-claude-ai `guardia_master.key` 패턴). + +--- + +## 4. 개발 표준 + +### 4-1. 코드 스타일 + +- **Java**: Google Java Style 기준(4-space, 100~120 col). Lombok 활용(`@Getter`/`@RequiredArgsConstructor`/`@Slf4j`), 필드 주입 금지(생성자 주입). 패키지 = 모듈별(`module.m2`·`module.m5`·`auth`·`common`·`config`) — app.md 경계 준수. **UTF-8 소스 필수**(build.gradle 강제). +- **TypeScript**: `strict` 전면. `any` 지양(불가피 시 주석). 컴포넌트 함수형, hooks 규약. 서버 상태는 react-query, 전역은 zustand. `@/*` 별칭 사용. +- **Python(워커)**: PEP 8 + type hints(`from __future__ import annotations`). 방어적 임포트(SDK/redis 지연). 비밀 미노출 규약(에러 메시지 300자 절단·키 미기록) 유지. + +### 4-2. 테스트 표준 + +- **백엔드**: JUnit 5 + spring-security-test. 단위(서비스·룰엔진·요율 계산) + 슬라이스(`@WebMvcTest`/`@MyBatisTest`) + 통합(핵심 왕복). **필수 테스트**(GUARDiA feedback_test_required): 임포트/컴파일 검증 + 라우트 확인 + curl 응답. 룰엔진(규정·요율)·PostGIS 공간 SQL은 결정적 테스트 필수. +- **프론트**: `tsc --noEmit` 게이트(현 최소선). 확장 시 Vitest + Testing Library 권고(현 미도입). +- **워커**: `python -m tools.nanobanana.worker --smoke`(무네트워크 스모크) — 목 잡 S2 + S6 래스터 처리·사이드카 확인. CI 필수 게이트. +- **AI/외부 호출**: 목/degraded 경로가 무네트워크로 통과해야 함(폐쇄망·미승인 대비). + +### 4-3. 브랜치·커밋 규약 + +- **브랜치**: `main`(보호) + 작업 브랜치(`feat/`·`fix/`·`chore/`). main 직접 커밋 금지(작업 브랜치 → PR). 기본 브랜치 push는 `origin HEAD:main`(BI repo master 함정 교훈 — repo별 기본 브랜치 확인). +- **커밋(Conventional Commits)**: `type(scope): summary`. type = `feat`/`fix`/`docs`/`refactor`/`test`/`chore`/`build`/`perf`. scope = 모듈(`m2`·`m5`·`auth`·`worker`·`bidding`). **커밋 메시지 영어**(kintex-ai-dev·visualizer 산출 규약). 예: `feat(m5): add renderjob quota guard`. +- **커밋 금지 대상**: 시크릿·`.env`·CAD zip(gitignore)·build 산출물. `.gitignore` 준수(`.gradle/`·`build/`·`*.log`·`.env`). +- **커밋/푸시 타이밍**: 사용자·오케스트레이터 명시 요청 시에만. + +### 4-4. 저장소·문서 규약 + +- kintex는 **독립 저장소**(`zio/kintex`) — GUARDiA ITSM(관공서 관제)과 별개 도메인. R12 게이트상 Gemini 외부호출은 kintex 독립성과 무관하게 소유자 승인 선행. +- 아키텍처 문서는 `docs/architecture/`. PLANNING(planner)·design(designer) 소유권 존중 — 본 문서는 직접 수정하지 않고 교차참조. + +--- + +## 5. 성능·관측성 표준 + +### 5-1. 로깅 + +- 백엔드: SLF4J/Logback(Spring Boot 기본). `logging.level.root: INFO`, `com.zioinfo.kintex: DEBUG`(개발). **운영은 INFO**로 하향(env `LOGGING_LEVEL_*` 오버라이드). 구조화(JSON) 로깅은 관측성 승격 시 권고. +- **로그 보안 불변**: 자격증명·IP·SSH·PII·스택트레이스 로그 금지. 워커는 예외 요약만(`type(e).__name__`), 키 미기록. `include-stacktrace: never` 유지. +- 상관관계: 요청별 traceId(MDC) 표준화 권고 — 옥션·RenderJob 비동기 흐름 추적. + +### 5-2. 메트릭·트레이싱 (Observability) + +- **표준(Micrometer + Actuator 권고)**: `spring-boot-starter-actuator` 추가(현 미포함) → `/actuator/health`(배포 게이트), `/actuator/metrics`, Prometheus `/actuator/prometheus`(GUARDiA guardia-rag `/metrics` 패턴 정렬). +- **핵심 지표**: RenderJob 처리량·지연·실패율(목/live 구분), 큐 적체(Redis 리스트 길이), 옥션 순위 계산 지연, PostGIS 공간 쿼리 지연, Hikari 풀 사용률, AI 프로바이더 폴백 발생률(Claude→Ollama). +- **트레이싱**: OpenTelemetry(OTel)는 관측성 트랙 승격 시(GreenOps/observability-platform 패턴). 초기는 로그 상관관계 + Actuator 메트릭. +- health 계약: 배포 후 `/actuator/health` 200이 완료 게이트(§3-4). 워커는 하트비트/최근 처리 시각을 이벤트/로그로 관측(전용 health 엔드포인트 부재 — 큐 소비 로그로 감시). + +### 5-3. 성능 표준·부하 목표 + +PLANNING §10 리스크(R6 이미지 비용·지연) 정렬: +- **이미지 생성**(R6): 홀당 200~600부스 동시 생성 시 비용/지연 급증. **표준**: 자동 생성은 S1·S7 한정 + **온디맨드 + 스키마 해시 캐시**(client.py 캐시) + **행사별 쿼터**(`RENDER_EVENT_QUOTA` 기본 500). 워커는 성공 시에만 쿼터 차감(worker 방어 로직). +- **PostGIS 대량 배치**: 부스 폴리곤·배선 LineString 대량 연산은 공간 인덱스(GiST) 전제. 배치 배치도 생성·정산 집계는 트랜잭션 분할. +- **BI 집계**(M16, PLANNING §8-1): 운영 DB 부하 회피 — 배치/스냅샷(KpiSnapshot) 또는 읽기 전용 복제. 실시간 대시보드 직접 집계 지양. +- **비동기 우선**: 이미지·서류·알림·PDF(옥션 견적서)는 전면 Redis 큐 경유(동기 블로킹 금지). + +--- + +## 6. AI 프로바이더 기술 표준 (Claude 기본 + 설정형 전환) + +> 근거: `.claude/agents/kintex-ai-dev.md`·GUARDiA guardia-claude-ai 트랙·UIWS 패턴. 나노바나나(Gemini 이미지)는 **별개**(visualizer·§1-3·G1 게이트) — 본 절은 **텍스트/지능 AI**(부스배치 조건해석·규정검증 보조·예측·매칭·서류검수·챗봇). + +### 6-1. 프로바이더 라우팅 아키텍처 + +UIWS 표준 3-컴포넌트(현 스캐폴드 **미구현** — AI 모듈 착수 시 신설): +- **`ClaudeTextClient`**: Anthropic Claude API(`api.anthropic.com` — 소유자 승인 예외 2026-07-03) 호출. 키는 env `ANTHROPIC_API_KEY`에서만 로드(코드·DB·로그·커밋·응답 기록 금지). +- **`AiTextRouter`**: 프로바이더 선택·폴백 오케스트레이션. **기본 Claude → 실패 시 Ollama 자동 폴백**(온프레미스 소형: `qwen3:1.7b`·`llama3.2:1b` 등). 하드코딩 금지. +- **`AiConfig`/`AiConfigService` + 설정 화면**: 런타임 프로바이더/모델 전환(화이트리스트 `claude-*` 기본 + 승인된 Ollama). generation_model·temperature·top_k·enabled 설정. + +### 6-2. 폴백·폐쇄망·결정론 + +- **Ollama 폴백 필수**(폐쇄망·Claude 장애 대비). RAM 제약 준수 — 서버 가용 ~2GB, 대형 모델 금지(소형만, [[project_ollama_ram_constraint]]). +- **결정론 기능**(분류·추출·서류검수): 구조화 출력(`format:json`). 환각 방지 — 근거 없는 답변 보류·인용(guardia-ai-trust 정렬). +- **부스 배치(M2)**: 생성형 LLM이 배치를 만드는 것이 아니라 **제약 솔버/휴리스틱**이 3안 생성, LLM은 조건 해석·설명에만(kintex-ai-dev 규약). + +### 6-3. 외부 아웃바운드 게이트 (불변) + +| 도메인 | 상태 | 조건 | +|---|---|---| +| `api.anthropic.com` | **승인**(2026-07-03 소유자 예외) | Claude 텍스트 AI. 키 env-only, 실패 시 Ollama 폴백 | +| `generativelanguage.googleapis.com` | **미승인 게이트 G1**(PLANNING R12) | 나노바나나. M5 실호출 착수 전 소유자 승인 선행. 미승인 시 목/degraded | +| 그 외 외부 API | **금지** | GUARDiA 보안 불변 | + +네트워크 아웃바운드 화이트리스트·프록시는 network.md(A-5) 소유. 본 문서는 AI 게이트만 확정. + +--- + +## 7. 보안 기술 표준 (불변 요약) + +PLANNING §10·GUARDiA 보안 불변 정렬(상세는 app.md/network.md): +- **시크릿 env-only** — 코드·DB·커밋·로그·응답 기록 금지. application.yml 플레이스홀더만. +- **자격증명·PII·스택트레이스 미노출** — API 응답/에러/로그/이벤트. `include-stacktrace: never`, 워커 에러 요약만. +- **AES-256-GCM** — admin 비번(`ADMIN_PASSWORD_ENC`)·민감 자격증명. 별도 키파일. +- **인증**(B-1): JWT(HS256, jjwt 0.12.5) + RBAC(6역할·행사 단위) + **TOTP 2FA(RFC6238)** + 로그인 실패 잠금. UIWS 이식. +- **AI 워터마크**(R1): 나노바나나 전 이미지 "AI 생성 예상 — 실제 시공과 다를 수 있음" 고지 강제(worker 목/live 공통). 계약·심사 서류 자동 배제. +- **등록업체 게이트**(M15): 미등록 업체 옥션 응찰 원천 차단(M7 검증). + +--- + +## 8. 기술 리스크 · PoC + +PLANNING §10(R1~R12)의 **기술 실행 리스크**를 TA 관점으로 구체화. 도메인/법적 리스크(R1·R2·R8·R9·R10)는 PLANNING 소유. + +### 8-1. TA 신규/구체화 리스크 + +| # | 리스크 | 영향 | 완화·PoC | +|---|---|---|---| +| **R-T1** | **큐/이벤트 키 기본값 불일치**(§2-1) — application.yml `kintex:renderjob:queue` vs worker.py `kintex:renderjobs` | 높음(배포 시 조용한 무처리) | 키 통일(`kintex:renderjob:queue`/`kintex:renderjob:events`) + `01_worker_contract.md` 고정 + 배포 env 명시. **PoC: 백엔드 push → 워커 소비 → WebSocket 완료 왕복 스모크** | +| **R-T2** | **로컬 rollup win32 크래시**(§3-2) — Windows Vite 빌드 네이티브 렌더 크래시(GUARDiA 반복 관측) | 중간(로컬 개발 저해) | 서버 빌드 신뢰 + `tsc --noEmit` 로컬 게이트 + `package-lock.json` 커밋(`npm ci` 재현) | +| **R-T3** | **PostGIS 대량 배치 성능** — 홀당 200~600부스 폴리곤·배선 최단경로·통로버퍼 검증 대량 연산 | 중간 | GiST 공간 인덱스 + 매퍼 XML `ST_*` 튜닝 + 배치 분할. **PoC: 600부스 배치도 생성·규정검증 SQL 부하 측정**(DA 협업) | +| **R-T4** | **이미지 큐 부하**(PLANNING R6) — 대량 동시 RenderJob 비용·지연 | 중간 | S1/S7 한정 자동생성 + 스키마 해시 캐시 + 행사 쿼터(500) + 워커 동시성 제한. **PoC: 목 모드 N=500 잡 큐 처리량·적체 측정**(무비용) | +| **R-T5** | **Flyway 부재**(§3-1·B-0 명시) — 마이그레이션 도구 미결정, 후행 테이블 미적용 회귀(GUARDiA schema-integrity 교훈) | 중간 | Flyway 채택 + 멱등 스키마 + 누출 차단. DA와 확정 | +| **R-T6** | **워커 requirements.txt 부재**(§1-3) — 의존성 핀 미고정 | 낮음 | `tools/nanobanana/requirements.txt` 추가(google-genai·Pillow·redis 핀). DEV 담당 | +| **R-T7** | **AiTextRouter/AiConfig 미구현**(§6-1) — AI 프로바이더 표준 코드 부재 | 낮음(설계 확정, 착수 대기) | AI 모듈 착수 시 UIWS 패턴 이식. Ollama 폴백 무네트워크 검증 | + +### 8-2. 권장 PoC 순서 (TA 실행 가능·Bash) + +1. **워커 스모크**(무네트워크·무비용) — `python -m tools.nanobanana.worker --smoke`. S2 목 + S6 래스터 사이드카 확인. **즉시 실행 가능**. +2. **큐 왕복 PoC**(R-T1) — 로컬 Redis + 백엔드 push + 워커 소비 스모크(키 통일 검증). +3. **PostGIS 배치 PoC**(R-T3) — 600부스 합성 데이터로 배치·검증 공간 SQL EXPLAIN ANALYZE. +4. **이미지 큐 부하 PoC**(R-T4) — 목 모드 500잡 처리량·적체. + +> 본 구현은 구현 에이전트(BE·VIZ·DB·AI)가 표준대로 수행. TA는 PoC 스크립트 실행·표준 개선만. + +--- + +## 9. 미결 사항 (구현 착수 전 확정 필요) + +| # | 항목 | 담당 | Phase | +|---|---|---|---| +| 1 | 큐/이벤트 키 통일(R-T1) → `01_worker_contract.md` 고정 | BE·VIZ·DEV | B/C | +| 2 | Flyway 채택·마이그레이션 구조(R-T5) | DA·DB·TA | B-0 | +| 3 | Actuator/Micrometer 관측성 의존성 추가(§5-2) | DEV·TA | B | +| 4 | 워커 `requirements.txt` 핀(R-T6) | DEV | C-M5 | +| 5 | 역할별 프론트 번들 분리 방식(§1-2) 확정 | DES·FE | C/D | +| 6 | `AiTextRouter`/`AiConfig` 이식(§6-1) | AI·BE | D | +| 7 | 배포 서버·포트·도메인(G2) | DEV·SA | E | +| 8 | Gemini 외부호출 승인(G1) | 소유자 | C-M5 | + +--- + +## 10. 변경 이력 + +| 버전 | 일자 | 작성자 | 내용 | +|---|---|---|---| +| v1.0 | 2026-07-11 | TA | 최초 작성(A-3). 실측 스캐폴드 정합 — 확정 스택 핀 버전(Spring Boot 3.2.5·MyBatis 3.0.3·jjwt 0.12.5·React 18.3.1·Vite 5.4.8·TS 5.6.2·google-genai 워커) SSOT화, 빌드·배포(Gradle 단일 jar·Vite·워커 systemd·CI/CD)·개발표준(코드스타일·테스트·Conventional Commits·브랜치)·관측성(로깅·Actuator/Micrometer·성능목표)·AI 프로바이더(Claude 기본+AiTextRouter/AiConfig+Ollama 폴백·외부 아웃바운드 게이트)·기술 리스크7종(R-T1~R-T7)+PoC 확정. ★실측 불일치 발견: 큐 키 기본값 백엔드/워커 상이(R-T1) — 통일 표준 제시. app.md/system.md/data.md/network.md 교차참조 | diff --git a/src/backend/build.gradle b/src/backend/build.gradle index 6eae62c..5a0ef38 100644 --- a/src/backend/build.gradle +++ b/src/backend/build.gradle @@ -37,6 +37,9 @@ dependencies { implementation "org.mybatis.spring.boot:mybatis-spring-boot-starter:${mybatisStarterVersion}" runtimeOnly 'org.postgresql:postgresql' + // --- Flyway 마이그레이션(순번 DDL·시드 권위) — Spring Boot 3.2.x 관리 버전(Flyway 9.x, PG 내장) --- + implementation 'org.flywaydb:flyway-core' + // --- Redis 작업 큐 (RenderJob·서류 생성·알림 발행) --- implementation 'org.springframework.boot:spring-boot-starter-data-redis' diff --git a/src/backend/src/main/resources/application.yml b/src/backend/src/main/resources/application.yml index 3124a09..7ce4240 100644 --- a/src/backend/src/main/resources/application.yml +++ b/src/backend/src/main/resources/application.yml @@ -27,6 +27,11 @@ spring: host: ${REDIS_HOST:localhost} port: ${REDIS_PORT:6379} password: ${REDIS_PASSWORD:} + # Flyway — 순번 DDL/시드가 스키마의 권위(sql.init 미사용, 후행 테이블 미적용 함정 회피). + flyway: + enabled: ${FLYWAY_ENABLED:true} + locations: classpath:db/migration + baseline-on-migrate: true # 기존 DB에도 안전 적용(파괴적 변경 금지) # MyBatis — 공간 SQL 매퍼 XML은 kintex-db-engineer가 채운다(D-2/M2-1/M4-1). mybatis: diff --git a/src/backend/src/main/resources/db/migration/V1__enable_extensions.sql b/src/backend/src/main/resources/db/migration/V1__enable_extensions.sql new file mode 100644 index 0000000..ceedcbf --- /dev/null +++ b/src/backend/src/main/resources/db/migration/V1__enable_extensions.sql @@ -0,0 +1,5 @@ +-- 킨텍스 자동전시시스템 — 공간 확장 활성화 (PLANNING §8 공간 데이터 일원화) +-- PostGIS: 부스 POLYGON·트렌치 POINT·배선 LINESTRING 지오메트리. +-- pgcrypto: gen_random_uuid() (PG13+ 코어이나 하위 호환 위해 명시). +CREATE EXTENSION IF NOT EXISTS postgis; +CREATE EXTENSION IF NOT EXISTS pgcrypto; diff --git a/src/backend/src/main/resources/db/migration/V2__identity_and_master.sql b/src/backend/src/main/resources/db/migration/V2__identity_and_master.sql new file mode 100644 index 0000000..1c6dce3 --- /dev/null +++ b/src/backend/src/main/resources/db/migration/V2__identity_and_master.sql @@ -0,0 +1,100 @@ +-- 킨텍스 — 신원·마스터 스키마 (PLANNING §7-3 ERD / §5B UIWS 인증 이식) +-- 보안 불변(계약 §0-3): password_hash·otp_secret 컬럼은 API 응답에서 완전 제외한다. + +-- 사용자 (UIWS TB_USER 이식 대응) -------------------------------------------- +CREATE TABLE IF NOT EXISTS app_user ( + id varchar(40) PRIMARY KEY, + email varchar(200) NOT NULL UNIQUE, + display_name varchar(120) NOT NULL, + password_hash varchar(120) NOT NULL, -- BCrypt. 응답 제외(민감정보). + hall_manager boolean NOT NULL DEFAULT false, + otp_secret varchar(96), -- TOTP(RFC6238) 시크릿, 암호화 저장. 응답 제외. + failed_login_count integer NOT NULL DEFAULT 0, -- 로그인 실패 잠금(§5B-3) + locked_until timestamptz, + status varchar(20) NOT NULL DEFAULT 'ACTIVE', + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now() +); +COMMENT ON COLUMN app_user.password_hash IS 'BCrypt 해시 — API 응답/로그/메신저 노출 금지(계약 §0-3)'; +COMMENT ON COLUMN app_user.otp_secret IS 'TOTP 시크릿(암호화) — API 응답 노출 금지'; + +-- 등록업체 (킨텍스 등록업체 DB 739 / M7·M15 응찰 게이트) ---------------------- +CREATE TABLE IF NOT EXISTS company ( + id varchar(40) PRIMARY KEY, + registration_no varchar(20) NOT NULL UNIQUE, -- 사업자등록번호(초대 수락 검증 키) + name varchar(200) NOT NULL, + category varchar(40), -- 14개 분류(전시디자인설치·리깅·전기시설 …) + region varchar(40), + registered boolean NOT NULL DEFAULT true, -- 킨텍스 등록 여부(미등록 응찰 차단) + phone varchar(40), + created_at timestamptz NOT NULL DEFAULT now() +); + +-- 행사 ----------------------------------------------------------------------- +CREATE TABLE IF NOT EXISTS event ( + id varchar(40) PRIMARY KEY, + name varchar(200) NOT NULL, + start_date date, + end_date date, + status varchar(20) NOT NULL DEFAULT 'active', + created_at timestamptz NOT NULL DEFAULT now() +); + +-- 홀 마스터 (PLANNING §7-1 / floorplans README 실측) -------------------------- +CREATE TABLE IF NOT EXISTS hall ( + id varchar(20) PRIMARY KEY, -- 'H1'..'H10','OUTDOOR' + exhibition_center integer NOT NULL, -- 1=제1전시장, 2=제2전시장 + label varchar(60) NOT NULL, -- '제1전시장 홀1' 등(워크스페이스 hallLabel) + width_m numeric(8,2), + depth_m numeric(8,2), + height_m numeric(6,2), + area_m2 numeric(10,2), + floor_load_t_per_m2 numeric(5,2), -- 홀6 2t/㎡, 그 외 5t/㎡ (규정 FLOOR_LOAD) + floor_type varchar(30), -- concrete_polished | carpet | outdoor + booth_capacity integer, + has_gas boolean NOT NULL DEFAULT false, -- 홀1·7 가스 공급 + is_assumed_trench boolean NOT NULL DEFAULT true, -- 트렌치 좌표 실측 여부(PLANNING R4) + created_at timestamptz NOT NULL DEFAULT now() +); + +-- 홀 배정 (Event 1─N HallAssignment) ----------------------------------------- +CREATE TABLE IF NOT EXISTS hall_assignment ( + id varchar(40) PRIMARY KEY, + event_id varchar(40) NOT NULL REFERENCES event(id) ON DELETE CASCADE, + hall_id varchar(20) NOT NULL REFERENCES hall(id), + is_primary boolean NOT NULL DEFAULT false, + UNIQUE (event_id, hall_id) +); + +-- 행사 단위 RBAC 멤버십 (계약 §0-5: ORGANIZER·EXHIBITOR·CONTRACTOR·HALL_MANAGER) +CREATE TABLE IF NOT EXISTS event_member ( + id varchar(40) PRIMARY KEY, + event_id varchar(40) NOT NULL REFERENCES event(id) ON DELETE CASCADE, + user_id varchar(40) NOT NULL REFERENCES app_user(id) ON DELETE CASCADE, + role_code varchar(20) NOT NULL, -- ORGANIZER|EXHIBITOR|CONTRACTOR|HALL_MANAGER + booth_id varchar(40), -- EXHIBITOR 부스 스코프(선택) + company_id varchar(40) REFERENCES company(id), -- CONTRACTOR 소속 등록업체(선택) + created_at timestamptz NOT NULL DEFAULT now(), + UNIQUE (event_id, user_id, role_code) +); +CREATE INDEX IF NOT EXISTS idx_event_member_user ON event_member(user_id); + +-- 부스 표준 사양 (PLANNING §7-1 / rates-v1.json boothStandard) ----------------- +CREATE TABLE IF NOT EXISTS booth_standard ( + code varchar(40) PRIMARY KEY, -- assembled | premium + name varchar(100), + spec jsonb, + updated_at timestamptz NOT NULL DEFAULT now() +); + +-- 마스터데이터 참조(M18: 요율·유틸 요금·규정 룰셋 버전) ----------------------- +-- 권위 룰셋은 classpath rulesets/*.json(룰 엔진 로드). 본 테이블은 M18 백오피스 조회·버전 추적용 사본. +CREATE TABLE IF NOT EXISTS master_data ( + category varchar(40) NOT NULL, -- RATE | UTILITY_FEE | COMPLIANCE + code varchar(80) NOT NULL, + name varchar(200), + value_json jsonb, + ruleset_version varchar(40), + updated_at timestamptz NOT NULL DEFAULT now(), + PRIMARY KEY (category, code) +); diff --git a/src/backend/src/main/resources/db/migration/V3__spatial_core.sql b/src/backend/src/main/resources/db/migration/V3__spatial_core.sql new file mode 100644 index 0000000..aae328e --- /dev/null +++ b/src/backend/src/main/resources/db/migration/V3__spatial_core.sql @@ -0,0 +1,105 @@ +-- 킨텍스 — 공간 코어 스키마 (PLANNING §8: 부스 POLYGON·트렌치 POINT·배선 LINESTRING) +-- SRID 0 = 홀 로컬 평면 좌표계(미터). ST_Area/ST_Distance/ST_Length 결과 단위 = m/㎡. +-- 공간 인덱스는 전부 GiST. + +-- 트렌치 (POINT) — 유틸리티 공급 매트릭스(홀1·7 가스) ------------------------ +CREATE TABLE IF NOT EXISTS trench ( + id varchar(60) PRIMARY KEY, + hall_id varchar(20) NOT NULL REFERENCES hall(id) ON DELETE CASCADE, + geom geometry(Point,0) NOT NULL, + supply_power boolean NOT NULL DEFAULT true, + supply_water boolean NOT NULL DEFAULT true, + supply_air boolean NOT NULL DEFAULT true, + supply_network boolean NOT NULL DEFAULT true, + supply_gas boolean NOT NULL DEFAULT false, + is_assumed boolean NOT NULL DEFAULT true -- 실측 아닌 가정 그리드(PLANNING R4) +); +CREATE INDEX IF NOT EXISTS idx_trench_geom ON trench USING GIST (geom); +CREATE INDEX IF NOT EXISTS idx_trench_hall ON trench (hall_id); + +-- 비상구 (POINT + 이격 버퍼) — 비상구 차단 검증(ST_Intersects) --------------- +CREATE TABLE IF NOT EXISTS hall_exit ( + id varchar(60) PRIMARY KEY, + hall_id varchar(20) NOT NULL REFERENCES hall(id) ON DELETE CASCADE, + geom geometry(Point,0) NOT NULL, + clearance_m numeric(5,2) NOT NULL DEFAULT 3.0, -- 비상구 접근 확보 버퍼(m) + is_assumed boolean NOT NULL DEFAULT true +); +CREATE INDEX IF NOT EXISTS idx_hall_exit_geom ON hall_exit USING GIST (geom); + +-- 배치안 (Layout, 버전) ------------------------------------------------------ +CREATE TABLE IF NOT EXISTS layout ( + id varchar(40) PRIMARY KEY, + event_id varchar(40) NOT NULL REFERENCES event(id) ON DELETE CASCADE, + hall_id varchar(20) NOT NULL REFERENCES hall(id), + version integer NOT NULL DEFAULT 1, + name varchar(120), + status varchar(20) NOT NULL DEFAULT 'draft', + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now(), + UNIQUE (event_id, hall_id, version) +); + +-- 부스 (POLYGON) ------------------------------------------------------------- +CREATE TABLE IF NOT EXISTS booth ( + id varchar(40) PRIMARY KEY, + layout_id varchar(40) NOT NULL REFERENCES layout(id) ON DELETE CASCADE, + booth_no varchar(40), + booth_type varchar(20), -- assembled|independent|corner|island + geom geometry(Polygon,0) NOT NULL, + size_w_m numeric(8,2), + size_d_m numeric(8,2), + height_m numeric(6,2), -- 규정 HEIGHT_MAX 대상 + floor_load_t_per_m2 numeric(5,2), -- 규정 FLOOR_LOAD 대상 + assigned_company_name varchar(200), -- 표시용(민감정보 아님) + premium boolean NOT NULL DEFAULT false, + created_at timestamptz NOT NULL DEFAULT now() +); +CREATE INDEX IF NOT EXISTS idx_booth_geom ON booth USING GIST (geom); +CREATE INDEX IF NOT EXISTS idx_booth_layout ON booth (layout_id); + +-- 부스 설계안 (DesignPlan 버전, spec jsonb) ---------------------------------- +CREATE TABLE IF NOT EXISTS design_plan ( + id varchar(40) PRIMARY KEY, + booth_id varchar(40) NOT NULL REFERENCES booth(id) ON DELETE CASCADE, + version integer NOT NULL DEFAULT 1, + status varchar(20) NOT NULL DEFAULT 'draft', + spec jsonb, + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now(), + UNIQUE (booth_id, version) +); + +-- 유틸리티 신청 (UtilityOrder, 배선 LINESTRING) ------------------------------ +-- booth_id는 배치 재저장(replaceBooths) 시 부스 교체 가능성 때문에 소프트 참조(FK 미설정). +CREATE TABLE IF NOT EXISTS utility_order ( + id varchar(40) PRIMARY KEY, + event_id varchar(40) NOT NULL REFERENCES event(id) ON DELETE CASCADE, + booth_id varchar(40) NOT NULL, + status varchar(20) NOT NULL DEFAULT 'submitted', + quote jsonb, + wiring geometry(MultiLineString,0), -- 전기/네트워크/급배수 배선 경로 + location_diagram_url varchar(300), -- 자동 생성 위치표시도(M4-3) + supply_timing varchar(120), + created_at timestamptz NOT NULL DEFAULT now() +); +CREATE INDEX IF NOT EXISTS idx_utility_order_wiring ON utility_order USING GIST (wiring); +CREATE INDEX IF NOT EXISTS idx_utility_order_booth ON utility_order (booth_id); + +-- 렌더잡 (RenderJob, 이력·쿼터 정본) ----------------------------------------- +-- event_id/booth_id 소프트 참조(감사·쿼터 이력은 부스 교체와 무관하게 보존). +CREATE TABLE IF NOT EXISTS render_job ( + id varchar(40) PRIMARY KEY, + event_id varchar(40) NOT NULL, + booth_id varchar(40) NOT NULL, + shot_preset varchar(10) NOT NULL, -- S1..S7 + status varchar(12) NOT NULL DEFAULT 'QUEUED', -- QUEUED|RUNNING|DONE|FAILED + image_url varchar(300), + schema_hash varchar(80), + model_version varchar(60), + error_message varchar(300), -- 사용자 친화 요약만(스택트레이스 금지) + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now() +); +CREATE INDEX IF NOT EXISTS idx_render_job_booth ON render_job (booth_id, shot_preset); +CREATE INDEX IF NOT EXISTS idx_render_job_event_status ON render_job (event_id, status); diff --git a/src/backend/src/main/resources/db/migration/V4__seed_hall_master.sql b/src/backend/src/main/resources/db/migration/V4__seed_hall_master.sql new file mode 100644 index 0000000..e8bbc17 --- /dev/null +++ b/src/backend/src/main/resources/db/migration/V4__seed_hall_master.sql @@ -0,0 +1,54 @@ +-- 킨텍스 — 홀 마스터 시드 (멱등, PLANNING §7-1 / floorplans README 실측치) +-- 재실행 안전: ON CONFLICT DO UPDATE 로 최신 규격 갱신. + +INSERT INTO hall (id, exhibition_center, label, width_m, depth_m, height_m, area_m2, + floor_load_t_per_m2, floor_type, booth_capacity, has_gas, is_assumed_trench) +VALUES + -- 제1전시장 홀1~5: 63×171×15m, 5t/㎡, 콘크리트 폴리싱, 약 600부스 + ('H1', 1, '제1전시장 홀1', 63, 171, 15, 10611, 5.0, 'concrete_polished', 600, true, true), + ('H2', 1, '제1전시장 홀2', 63, 171, 15, 10773, 5.0, 'concrete_polished', 600, false, true), + ('H3', 1, '제1전시장 홀3', 63, 171, 15, 10773, 5.0, 'concrete_polished', 600, false, true), + ('H4', 1, '제1전시장 홀4', 63, 171, 15, 10773, 5.0, 'concrete_polished', 600, false, true), + ('H5', 1, '제1전시장 홀5', 63, 171, 15, 10611, 5.0, 'concrete_polished', 600, false, true), + -- 옥외전시장 2,849㎡ (56×52) + ('OUTDOOR', 1, '옥외전시장', 56, 52, NULL, 2849, 5.0, 'outdoor', NULL, false, true), + -- 제2전시장 홀6(이벤트홀): 93×60×10m, 2t/㎡, 카펫, 200부스 + ('H6', 2, '제2전시장 홀6', 93, 60, 10, 5580, 2.0, 'carpet', 200, false, true), + -- 홀7/8: 126×90×12m, 5t/㎡, 510부스 (홀7 가스) + ('H7', 2, '제2전시장 홀7', 126, 90, 12, 11290, 5.0, 'concrete_polished', 510, true, true), + ('H8', 2, '제2전시장 홀8', 126, 90, 12, 11290, 5.0, 'concrete_polished', 510, false, true), + -- 홀9/10: 132×99×15m, 5t/㎡, 550부스 + ('H9', 2, '제2전시장 홀9', 132, 99, 15, 13238, 5.0, 'concrete_polished', 550, false, true), + ('H10', 2, '제2전시장 홀10', 132, 99, 15, 13072, 5.0, 'concrete_polished', 550, false, true) +ON CONFLICT (id) DO UPDATE SET + exhibition_center = EXCLUDED.exhibition_center, + label = EXCLUDED.label, + width_m = EXCLUDED.width_m, + depth_m = EXCLUDED.depth_m, + height_m = EXCLUDED.height_m, + area_m2 = EXCLUDED.area_m2, + floor_load_t_per_m2 = EXCLUDED.floor_load_t_per_m2, + floor_type = EXCLUDED.floor_type, + booth_capacity = EXCLUDED.booth_capacity, + has_gas = EXCLUDED.has_gas, + is_assumed_trench = EXCLUDED.is_assumed_trench; + +-- 비상구(가정): 사각형 홀 각 변 중앙 4개소, 이격 3m. CAD 실측 추출 시 교체(TODO). +INSERT INTO hall_exit (id, hall_id, geom, clearance_m, is_assumed) +SELECT h.id || '-EXIT-' || g.tag, + h.id, + ST_SetSRID(ST_MakePoint(g.x, g.y), 0), + 3.0, + true +FROM hall h +CROSS JOIN LATERAL (VALUES + ('S', h.width_m / 2.0, 0.0), + ('N', h.width_m / 2.0, h.depth_m), + ('W', 0.0, h.depth_m / 2.0), + ('E', h.width_m, h.depth_m / 2.0) +) AS g(tag, x, y) +WHERE h.id <> 'OUTDOOR' AND h.width_m IS NOT NULL AND h.depth_m IS NOT NULL +ON CONFLICT (id) DO UPDATE SET + geom = EXCLUDED.geom, + clearance_m = EXCLUDED.clearance_m, + is_assumed = EXCLUDED.is_assumed; diff --git a/src/backend/src/main/resources/db/migration/V5__seed_trench_grid.sql b/src/backend/src/main/resources/db/migration/V5__seed_trench_grid.sql new file mode 100644 index 0000000..0ecb83e --- /dev/null +++ b/src/backend/src/main/resources/db/migration/V5__seed_trench_grid.sql @@ -0,0 +1,16 @@ +-- 킨텍스 — 트렌치 가정 그리드 시드 (멱등, PLANNING R4 / backend isAssumedTrench 계약 정합) +-- ★가정 그리드: 실측 아님(is_assumed=true). CAD "평면,트렌치.dwg" 좌표 추출 시 교체(후속 TODO). +-- 그리드: 각 변에서 3m 안쪽부터 6m 간격 격자 포인트. 각 트렌치는 전기·급배수·압축공기·네트워크 번들, +-- 가스는 has_gas 홀(홀1·7)만 공급. + +INSERT INTO trench (id, hall_id, geom, + supply_power, supply_water, supply_air, supply_network, supply_gas, is_assumed) +SELECT h.id || '-T-' || gx || '-' || gy, + h.id, + ST_SetSRID(ST_MakePoint(gx, gy), 0), + true, true, true, true, h.has_gas, true +FROM hall h +CROSS JOIN LATERAL generate_series(3, floor(h.width_m)::int - 3, 6) AS gx +CROSS JOIN LATERAL generate_series(3, floor(h.depth_m)::int - 3, 6) AS gy +WHERE h.id <> 'OUTDOOR' AND h.width_m IS NOT NULL AND h.depth_m IS NOT NULL +ON CONFLICT (id) DO NOTHING; diff --git a/src/backend/src/main/resources/db/migration/V6__seed_master_and_demo.sql b/src/backend/src/main/resources/db/migration/V6__seed_master_and_demo.sql new file mode 100644 index 0000000..e7a1ba7 --- /dev/null +++ b/src/backend/src/main/resources/db/migration/V6__seed_master_and_demo.sql @@ -0,0 +1,50 @@ +-- 킨텍스 — 부스 표준·요율/요금 참조·등록업체·데모 행사 시드 (멱등) +-- 권위 룰셋(요율·규정)은 classpath rulesets/*.json(룰 엔진 로드). 아래 master_data 는 M18 조회용 사본. + +-- 부스 표준 사양 (rates-v1.json boothStandard / PLANNING §7-1) ---------------- +INSERT INTO booth_standard (code, name, spec) VALUES + ('assembled', '조립(기본)부스', + '{"includedPowerKw":1.0,"spotlights":5,"outlets220v":1,"floorFireRetardant":"A"}'::jsonb), + ('premium', '프리미엄부스', + '{"sizeM":[6,3,4],"includedPowerKw":2.0}'::jsonb) +ON CONFLICT (code) DO UPDATE SET name = EXCLUDED.name, spec = EXCLUDED.spec, updated_at = now(); + +-- 임대 요율 참조 (rates-v1.0) ------------------------------------------------- +INSERT INTO master_data (category, code, name, value_json, ruleset_version) VALUES + ('RATE', 'exhibitionHallPerM2', '전시홀 임대 요율(원/㎡·12h)', '2250'::jsonb, 'rates-v1.0'), + ('RATE', 'lobbyPerM2', '로비 요율(원/㎡)', '10000'::jsonb, 'rates-v1.0'), + ('RATE', 'outdoorPerM2', '옥외 요율(원/㎡)', '2000'::jsonb, 'rates-v1.0'), + ('RATE', 'eventHallPerM2', '이벤트홀 요율(원/㎡)', '2420'::jsonb, 'rates-v1.0'), + ('RATE', 'seasonFactors', '성수기/비수기/1전시장 계수', + '{"peak":0.10,"offPeak":-0.10,"hall1":0.10}'::jsonb, 'rates-v1.0'), + -- 유틸리티 요금 참조 + ('UTILITY_FEE', 'power.perKw', '전기 220V 단상 1kW(원)', '55000'::jsonb, 'rates-v1.0'), + ('UTILITY_FEE', 'power.distributionBox50A', '분전반 50A 추가(원)', '100000'::jsonb, 'rates-v1.0'), + ('UTILITY_FEE', 'network.internetWiredPerLine', '인터넷 유선 회선(원)', '150000'::jsonb, 'rates-v1.0'), + ('UTILITY_FEE', 'plumbing.perOutlet', '급배수 1구(원)', '150000'::jsonb, 'rates-v1.0'), + ('UTILITY_FEE', 'compressedAir.perOutlet', '압축공기 1구(원)', '150000'::jsonb, 'rates-v1.0'), + -- 규정 룰셋 참조 (compliance-v1.0) + ('COMPLIANCE', 'HEIGHT_MAX', '장치물 최고 높이(m)', '5'::jsonb, 'compliance-v1.0'), + ('COMPLIANCE', 'RIGGING_RANGE', '리깅 허용 높이(m)', '[6.5,8.5]'::jsonb, 'compliance-v1.0'), + ('COMPLIANCE', 'MEZZANINE_RATIO','복층 면적 비율 상한', '0.5'::jsonb, 'compliance-v1.0'), + ('COMPLIANCE', 'AISLE_WIDTH_MIN','피난 통로 폭 최소(m)', '3'::jsonb, 'compliance-v1.0') +ON CONFLICT (category, code) DO UPDATE SET + name = EXCLUDED.name, value_json = EXCLUDED.value_json, + ruleset_version = EXCLUDED.ruleset_version, updated_at = now(); + +-- 등록업체 샘플 (M7/M15 응찰 게이트 검증용 — 사업자번호는 가상 예시) ---------- +INSERT INTO company (id, registration_no, name, category, region, registered, phone) VALUES + ('co-0001', '111-11-11111', '(주)한빛부스디자인', '전시디자인설치', '경기', true, NULL), + ('co-0002', '222-22-22222', '대한리깅', '리깅', '서울', true, NULL), + ('co-0003', '333-33-33333', '세계전기시설', '전기시설', '경기', true, NULL), + ('co-0004', '444-44-44444', '고양카펫', '카펫/파이텍스', '경기', true, NULL) +ON CONFLICT (registration_no) DO NOTHING; + +-- 데모 행사 + 홀 배정 (구조 검증용. 사용자·멤버십은 B-1 인증 트랙에서 프로비저닝) -- +INSERT INTO event (id, name, start_date, end_date, status) VALUES + ('e-2026-smf', '2026 스마트팩토리 코리아', DATE '2026-08-11', DATE '2026-08-14', 'active') +ON CONFLICT (id) DO NOTHING; + +INSERT INTO hall_assignment (id, event_id, hall_id, is_primary) VALUES + ('ha-2026-smf-h7', 'e-2026-smf', 'H7', true) +ON CONFLICT (event_id, hall_id) DO NOTHING; diff --git a/src/backend/src/main/resources/mybatis/mapper/BoothMapper.xml b/src/backend/src/main/resources/mybatis/mapper/BoothMapper.xml new file mode 100644 index 0000000..f5aca14 --- /dev/null +++ b/src/backend/src/main/resources/mybatis/mapper/BoothMapper.xml @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + + + + WITH del AS (DELETE FROM booth WHERE layout_id = #{layoutId}) + INSERT INTO booth (id, layout_id, booth_no, booth_type, geom, + size_w_m, size_d_m, height_m, floor_load_t_per_m2, + assigned_company_name, premium) + VALUES + + (COALESCE(#{b.boothId}, gen_random_uuid()::text), + #{layoutId}, #{b.boothNo}, #{b.type}, + ST_GeomFromText(#{b.wkt}, 0), + #{b.sizeW}, #{b.sizeD}, #{b.heightM}, #{b.floorLoad}, + #{b.assignedCompanyName}, #{b.premium}) + + + + + + + + + + + + + diff --git a/src/backend/src/main/resources/mybatis/mapper/DesignMapper.xml b/src/backend/src/main/resources/mybatis/mapper/DesignMapper.xml new file mode 100644 index 0000000..ccab075 --- /dev/null +++ b/src/backend/src/main/resources/mybatis/mapper/DesignMapper.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + diff --git a/src/backend/src/main/resources/mybatis/mapper/RenderJobMapper.xml b/src/backend/src/main/resources/mybatis/mapper/RenderJobMapper.xml new file mode 100644 index 0000000..2d341a8 --- /dev/null +++ b/src/backend/src/main/resources/mybatis/mapper/RenderJobMapper.xml @@ -0,0 +1,59 @@ + + + + + + + + INSERT INTO render_job (id, event_id, booth_id, shot_preset, status, + image_url, schema_hash, model_version, error_message, created_at, updated_at) + VALUES (#{params.jobId}, #{params.eventId}, #{params.boothId}, #{params.shotPreset}, + COALESCE(#{params.status}, 'QUEUED'), + #{params.imageUrl}, #{params.schemaHash}, #{params.modelVersion}, + #{params.errorMessage}, now(), now()) + ON CONFLICT (id) DO NOTHING + + + + + UPDATE render_job + SET status = #{status}, + image_url = #{imageUrl}, + error_message = #{errorMessage}, + updated_at = now() + WHERE id = #{jobId} + + + + + + + + + diff --git a/src/backend/src/main/resources/mybatis/mapper/UserMapper.xml b/src/backend/src/main/resources/mybatis/mapper/UserMapper.xml new file mode 100644 index 0000000..ba260f3 --- /dev/null +++ b/src/backend/src/main/resources/mybatis/mapper/UserMapper.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + diff --git a/src/backend/src/main/resources/mybatis/mapper/WiringMapper.xml b/src/backend/src/main/resources/mybatis/mapper/WiringMapper.xml new file mode 100644 index 0000000..cf2d902 --- /dev/null +++ b/src/backend/src/main/resources/mybatis/mapper/WiringMapper.xml @@ -0,0 +1,59 @@ + + + + + + + + + t.supply_power = true + t.supply_network = true + t.supply_water = true + t.supply_air = true + t.supply_gas = true + true + + + + + + + + + + + + + diff --git a/src/frontend/.env.example b/src/frontend/.env.example new file mode 100644 index 0000000..753443b --- /dev/null +++ b/src/frontend/.env.example @@ -0,0 +1,4 @@ +# 백엔드 오리진 (Spring Boot). 개발 시 Vite 프록시 대상. +VITE_BACKEND_ORIGIN=http://localhost:8080 +# 프로덕션에서 API를 절대 경로로 호출할 때 사용(비우면 same-origin + 프록시). +VITE_API_BASE= diff --git a/src/frontend/.gitignore b/src/frontend/.gitignore new file mode 100644 index 0000000..285f792 --- /dev/null +++ b/src/frontend/.gitignore @@ -0,0 +1,9 @@ +node_modules +dist +dist-ssr +*.local +.env +.env.* +!.env.example +.vite +*.tsbuildinfo diff --git a/src/frontend/README.md b/src/frontend/README.md new file mode 100644 index 0000000..01c336d --- /dev/null +++ b/src/frontend/README.md @@ -0,0 +1,27 @@ +# KINTEX AI 전시관리 — Frontend + +React 18 + Vite 5 + TypeScript. 백엔드(Spring Boot :8080) 계약(`_workspace/01_backend_contracts.md`) 기반. + +## 개발 +```bash +npm install +npm run dev # Vite 개발 서버 :5173 (/api·/ws → :8080 프록시) +npm run typecheck # tsc -b (타입 체크) +npm run build # tsc -b && vite build +``` + +`.env.example` → `.env` 복사. `VITE_BACKEND_ORIGIN`으로 백엔드 오리진 지정(기본 `http://localhost:8080`). + +## 구조 +- `src/styles/tokens.css` — 디자인 토큰 **단일 출처**(design.md §1). 하드코딩 금지, 이 변수만 참조. +- `src/api/` — `client`(JWT·봉투·오류코드), `endpoints`(계약 래퍼), `types`(응답 shape), `websocket`(STOMP RenderJob). +- `src/store/authStore.ts` — 인증/워크스페이스 상태(zustand). +- `src/components/ui/` — StatusBadge·DdayChip·AiLabel·Button·AiImage(워터마크 상시)·States. +- `src/components/layout/AppShell.tsx` — 인증 셸(사이드바+상단바). +- `src/screens/login/` — SCR-01 로그인·워크스페이스·초대(등록업체 차단). +- `src/screens/floorplan/` — SCR-03 배치 에디터(SVG 캔버스·AI 자동배치·검증). + +## 원칙 +- 백엔드 응답 shape과 정확히 일치. 불일치는 `_workspace/02_frontend_progress.md`에 기록. +- AI 생성 이미지 워터마크·고지문 제거 불가. 접근성 WCAG AA. +- 캔버스 편집은 데스크톱 전용(모바일은 뷰어+승인). diff --git a/src/frontend/index.html b/src/frontend/index.html new file mode 100644 index 0000000..c112d37 --- /dev/null +++ b/src/frontend/index.html @@ -0,0 +1,17 @@ + + + + + + KINTEX AI 전시관리 + + + + +
+ + + diff --git a/src/frontend/package-lock.json b/src/frontend/package-lock.json new file mode 100644 index 0000000..6f04bcd --- /dev/null +++ b/src/frontend/package-lock.json @@ -0,0 +1,2005 @@ +{ + "name": "kintex-frontend", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "kintex-frontend", + "version": "0.1.0", + "dependencies": { + "@stomp/stompjs": "^7.0.0", + "@tanstack/react-query": "^5.59.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-router-dom": "^6.26.2", + "sockjs-client": "^1.6.1", + "zustand": "^4.5.5" + }, + "devDependencies": { + "@types/node": "^20.19.43", + "@types/react": "^18.3.11", + "@types/react-dom": "^18.3.0", + "@types/sockjs-client": "^1.5.4", + "@vitejs/plugin-react": "^4.3.2", + "typescript": "^5.6.2", + "vite": "^5.4.8" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@remix-run/router": { + "version": "1.23.3", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.3.tgz", + "integrity": "sha512-4An71tdz9X8+3sI4Qqqd2LWd9vS39J7sqd9EU4Scw7TJE/qB10Flv/UuqbPVgfQV9XoK8Np6jNquZitnZq5i+Q==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@stomp/stompjs": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/@stomp/stompjs/-/stompjs-7.3.0.tgz", + "integrity": "sha512-nKMLoFfJhrQAqkvvKd1vLq/cVBGCMwPRCD0LqW7UT1fecRx9C3GoKEIR2CYwVuErGeZu8w0kFkl2rlhPlqHVgQ==", + "license": "Apache-2.0" + }, + "node_modules/@tanstack/query-core": { + "version": "5.101.2", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.101.2.tgz", + "integrity": "sha512-hH5MLoJhF7KaIGd7q3xTXGXvslI+GYlM1Z/35aSHHWaCJWB7XvTSHYuV3eM7tw+aE0mT/xMro4M4Q9rCGHT0lw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.101.2", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.101.2.tgz", + "integrity": "sha512-seDkr6kzGzX1okaaTtZPtgA688CDPlXUz1C6xSg0ESqn04Vuc8tlrYms1s3de+znBqhPVxFRfpAfUf+6XvfPWg==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.101.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.43", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz", + "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.31", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.31.tgz", + "integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==", + "devOptional": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@types/sockjs-client": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@types/sockjs-client/-/sockjs-client-1.5.4.tgz", + "integrity": "sha512-zk+uFZeWyvJ5ZFkLIwoGA/DfJ+pYzcZ8eH4H/EILCm2OBZyHH6Hkdna1/UWL/CFruh5wj6ES7g75SvUB0VsH5w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.42", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.42.tgz", + "integrity": "sha512-c/jurFrDLyui7o1J86yLkRu4LMsTYcBohveus7/I2Hzdn9KIP2bdJPTue/lR1KH46enoPbD77GKeSYNdyPoD3Q==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.28.5", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.5.tgz", + "integrity": "sha512-Cu2E6QejHWzuDMTkuwgpABFgDfZrXLQq5V13YOACZx4mFAG4IwGTbTfHPMr4WtxlHoXSM8FIuRwYYCz5XiabaQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "baseline-browser-mapping": "^2.10.42", + "caniuse-lite": "^1.0.30001800", + "electron-to-chromium": "^1.5.387", + "node-releases": "^2.0.50", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001803", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001803.tgz", + "integrity": "sha512-g/uHREV2ZpK9qMalCsWaxmA6ol+DX8GYhuf3T40RKoP+oL7vhRJh8LNt73PCjpnR6l14FzfPrB5Yux4PKm2meg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.389", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.389.tgz", + "integrity": "sha512-cEto7aeOqBfU1D+c5py5pE+ooscKE75JifxLBdFUZsqAxRS6y7kebtxAZvICszSl05gPjYHDTjY+lXpyGvpJbg==", + "dev": true, + "license": "ISC" + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/eventsource": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-2.0.2.tgz", + "integrity": "sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "license": "Apache-2.0", + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/http-parser-js": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz", + "integrity": "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==", + "license": "MIT" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.15", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", + "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.51", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.51.tgz", + "integrity": "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.16", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz", + "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "license": "MIT" + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "6.30.4", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.4.tgz", + "integrity": "sha512-SVUsDe+DybHM/WmYKIVYhZh1o5Dcuf16yM6WjG02Q9XVFMZIJyHYhwrr6bFBXZkVP6z69kNkMyBCujt8FaFLJA==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.30.4", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.4.tgz", + "integrity": "sha512-q4HvNl+mmDdkS0g+MqiBZNteQJCuimWoOyHMy4T/RQLAn9Z29+E91QXRaxOujeMl2HTzRSS0KFPd7lxX3PjV0Q==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.3", + "react-router": "6.30.4" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "license": "MIT" + }, + "node_modules/rollup": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/sockjs-client": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/sockjs-client/-/sockjs-client-1.6.1.tgz", + "integrity": "sha512-2g0tjOR+fRs0amxENLi/q5TiJTqY+WXFOzb5UwXndlK6TO3U/mirZznpx6w34HVMoc3g7cY24yC/ZMIYnDlfkw==", + "license": "MIT", + "dependencies": { + "debug": "^3.2.7", + "eventsource": "^2.0.2", + "faye-websocket": "^0.11.4", + "inherits": "^2.0.4", + "url-parse": "^1.5.10" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://tidelift.com/funding/github/npm/sockjs-client" + } + }, + "node_modules/sockjs-client/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "license": "MIT", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/websocket-driver": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.5.tgz", + "integrity": "sha512-ZL2+3c7kMBdIRCMz6l8jQMHyGVxj+UL+xVk74Ombiciboca8rHa15L86B19E5oh1pL9Ii/uj54gtsIrZGMo6zA==", + "license": "Apache-2.0", + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/zustand": { + "version": "4.5.7", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", + "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.2.2" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } + } + } +} diff --git a/src/frontend/package.json b/src/frontend/package.json new file mode 100644 index 0000000..19865c0 --- /dev/null +++ b/src/frontend/package.json @@ -0,0 +1,32 @@ +{ + "name": "kintex-frontend", + "private": true, + "version": "0.1.0", + "type": "module", + "description": "킨텍스 AI 전시관리 시스템 — 프론트엔드 (React + Vite + TypeScript)", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "typecheck": "tsc -b", + "preview": "vite preview", + "lint": "tsc --noEmit" + }, + "dependencies": { + "@stomp/stompjs": "^7.0.0", + "@tanstack/react-query": "^5.59.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-router-dom": "^6.26.2", + "sockjs-client": "^1.6.1", + "zustand": "^4.5.5" + }, + "devDependencies": { + "@types/node": "^20.19.43", + "@types/react": "^18.3.11", + "@types/react-dom": "^18.3.0", + "@types/sockjs-client": "^1.5.4", + "@vitejs/plugin-react": "^4.3.2", + "typescript": "^5.6.2", + "vite": "^5.4.8" + } +} diff --git a/src/frontend/src/App.tsx b/src/frontend/src/App.tsx new file mode 100644 index 0000000..9da8224 --- /dev/null +++ b/src/frontend/src/App.tsx @@ -0,0 +1,38 @@ +import { Navigate, Route, Routes } from 'react-router-dom'; +import { useAuthStore } from './store/authStore'; +import { LoginPage } from './screens/login/LoginPage'; +import { BoothLayoutEditorPage } from './screens/floorplan/BoothLayoutEditorPage'; +import { AppShell } from './components/layout/AppShell'; + +/** 인증 가드 — 미인증 시 로그인으로. (역할별 라우팅은 화면 추가 시 확장) */ +function RequireAuth({ children }: { children: React.ReactNode }) { + const isAuthenticated = useAuthStore((s) => s.isAuthenticated); + if (!isAuthenticated) return ; + return <>{children}; +} + +export function App() { + return ( + + } /> + + {/* 인증 필요 — 사이드바 셸 안 */} + + + + } + > + {/* SCR-03 부스 배치 에디터 (M2) */} + } + /> + {/* 향후: SCR-02 대시보드, SCR-05 부스홈, SCR-10 승인큐 등 */} + + + } /> + + ); +} diff --git a/src/frontend/src/api/client.ts b/src/frontend/src/api/client.ts new file mode 100644 index 0000000..b9e92c1 --- /dev/null +++ b/src/frontend/src/api/client.ts @@ -0,0 +1,98 @@ +/* + * API 클라이언트 — 백엔드 계약 base URL·JWT 헤더·에러 봉투 처리. + * 근거: _workspace/01_backend_contracts.md §0 (ApiResponse 봉투, 오류 코드→HTTP, 인증 헤더). + */ +import type { ApiError, ApiErrorCode, ApiResponse } from './types'; + +// 프로덕션에서 절대 base가 필요하면 VITE_API_BASE 사용, 아니면 same-origin(개발은 Vite 프록시). +const API_BASE = (import.meta.env.VITE_API_BASE as string | undefined) ?? ''; + +/** 봉투 error를 그대로 던지는 오류 타입 — UI는 code로 분기(FORBIDDEN·COMPLIANCE_BLOCKED 등). */ +export class ApiRequestError extends Error { + readonly code: ApiErrorCode | 'NETWORK' | 'UNKNOWN'; + readonly httpStatus: number; + constructor(code: ApiRequestError['code'], message: string, httpStatus: number) { + super(message); + this.name = 'ApiRequestError'; + this.code = code; + this.httpStatus = httpStatus; + } +} + +// ── JWT 토큰 저장소 (메모리 + sessionStorage 백업) ── +const TOKEN_KEY = 'kintex.accessToken'; +let accessToken: string | null = sessionStorage.getItem(TOKEN_KEY); + +export function setAccessToken(token: string | null): void { + accessToken = token; + if (token) sessionStorage.setItem(TOKEN_KEY, token); + else sessionStorage.removeItem(TOKEN_KEY); +} +export function getAccessToken(): string | null { + return accessToken; +} + +interface RequestOptions { + method?: 'GET' | 'POST' | 'PUT' | 'DELETE'; + body?: unknown; + signal?: AbortSignal; + /** 인증 헤더 생략(공개 경로). */ + anonymous?: boolean; +} + +async function request(path: string, opts: RequestOptions = {}): Promise { + const headers: Record = { 'Content-Type': 'application/json' }; + if (!opts.anonymous && accessToken) headers.Authorization = `Bearer ${accessToken}`; + + let res: Response; + try { + res = await fetch(`${API_BASE}${path}`, { + method: opts.method ?? 'GET', + headers, + body: opts.body != null ? JSON.stringify(opts.body) : undefined, + signal: opts.signal, + }); + } catch (e) { + if ((e as Error).name === 'AbortError') throw e; + throw new ApiRequestError('NETWORK', '네트워크 연결을 확인해 주세요.', 0); + } + + // 401 → 토큰 만료: 세션 정리 (라우터가 로그인으로 유도) + if (res.status === 401) { + setAccessToken(null); + } + + let payload: ApiResponse | null = null; + const text = await res.text(); + if (text) { + try { + payload = JSON.parse(text) as ApiResponse; + } catch { + payload = null; + } + } + + if (!res.ok || (payload && payload.success === false)) { + const err: ApiError | null = payload?.error ?? null; + throw new ApiRequestError( + (err?.code as ApiErrorCode) ?? 'UNKNOWN', + err?.message ?? `요청을 처리하지 못했습니다. (${res.status})`, + res.status, + ); + } + + if (!payload) { + // 봉투 없이 204 등 — 빈 값 허용 + return undefined as unknown as T; + } + return payload.data as T; +} + +export const api = { + get: (path: string, opts?: Omit) => + request(path, { ...opts, method: 'GET' }), + post: (path: string, body?: unknown, opts?: Omit) => + request(path, { ...opts, method: 'POST', body }), + put: (path: string, body?: unknown, opts?: Omit) => + request(path, { ...opts, method: 'PUT', body }), +}; diff --git a/src/frontend/src/api/endpoints.ts b/src/frontend/src/api/endpoints.ts new file mode 100644 index 0000000..bab47be --- /dev/null +++ b/src/frontend/src/api/endpoints.ts @@ -0,0 +1,55 @@ +/* + * 엔드포인트 래퍼 — 계약(§2~§6) 경로를 타입 안전하게 노출. + * 501(NOT_IMPLEMENTED)인 스켈레톤 경로도 포함(호출부에서 code로 degraded 처리). + */ +import { api } from './client'; +import type { + AcceptInviteRequest, + AutoLayoutOption, + AutoLayoutRequest, + ComplianceReport, + KintexPrincipal, + LayoutDto, + LayoutSaveRequest, + LoginRequest, + LoginResponse, + RenderJobDto, + WorkspaceDto, +} from './types'; + +// ── 인증·워크스페이스 (SCR-01) ── +export const authApi = { + login: (body: LoginRequest) => + api.post('/api/auth/login', body, { anonymous: true }), + workspaces: () => api.get('/api/auth/workspaces'), + me: () => api.get('/api/auth/me'), + acceptInvite: (body: AcceptInviteRequest) => + api.post('/api/auth/accept-invite', body), +}; + +// ── M2 플로어플랜 (SCR-03/04) ── +const layoutBase = (eventId: string, hallId: string) => + `/api/events/${encodeURIComponent(eventId)}/halls/${encodeURIComponent(hallId)}/layout`; + +export const layoutApi = { + get: (eventId: string, hallId: string, version?: number) => + api.get( + `${layoutBase(eventId, hallId)}${version != null ? `?version=${version}` : ''}`, + ), + save: (eventId: string, hallId: string, body: LayoutSaveRequest) => + api.put(layoutBase(eventId, hallId), body), + validate: (eventId: string, hallId: string, version?: number) => + api.post( + `${layoutBase(eventId, hallId)}/validate${version != null ? `?version=${version}` : ''}`, + ), + autoGenerate: (eventId: string, hallId: string, body: AutoLayoutRequest) => + api.post(`${layoutBase(eventId, hallId)}/auto-generate`, body), +}; + +// ── M5 RenderJob (SCR-12) ── +export const renderApi = { + status: (eventId: string, jobId: string) => + api.get( + `/api/events/${encodeURIComponent(eventId)}/render-jobs/${encodeURIComponent(jobId)}`, + ), +}; diff --git a/src/frontend/src/api/types.ts b/src/frontend/src/api/types.ts new file mode 100644 index 0000000..49559b9 --- /dev/null +++ b/src/frontend/src/api/types.ts @@ -0,0 +1,165 @@ +/* + * 백엔드 API 계약 타입 (단일 출처: _workspace/01_backend_contracts.md) + * ★ 이 타입들은 백엔드 응답 shape과 정확히 일치해야 한다. 불일치 발견 시 _workspace/에 기록·보고. + */ + +// ── 0-1 응답 봉투 ── +export interface ApiError { + code: ApiErrorCode; + message: string; +} +export interface ApiResponse { + success: boolean; + data: T | null; + error: ApiError | null; +} +export interface PageResponse { + items: T[]; + page: number; + size: number; + total: number; +} + +// ── 0-2 오류 코드 ── +export type ApiErrorCode = + | 'VALIDATION' + | 'UNAUTHORIZED' + | 'FORBIDDEN' + | 'NOT_FOUND' + | 'CONFLICT' + | 'COMPLIANCE_BLOCKED' + | 'RENDER_QUOTA_EXCEEDED' + | 'NOT_REGISTERED_COMPANY' + | 'NOT_IMPLEMENTED' + | 'INTERNAL'; + +// ── 0-5 역할 ── +export type EventRole = 'ORGANIZER' | 'EXHIBITOR' | 'CONTRACTOR' | 'HALL_MANAGER'; + +// ── 2. 인증·워크스페이스 (SCR-01) ── +export interface WorkspaceDto { + eventId: string; + eventName: string; + startDate: string; + endDate: string; + hallLabel: string; + myRole: EventRole; + dday: number; +} +export interface AuthUser { + userId: string; + displayName: string; + hallManager: boolean; +} +export interface LoginResponse { + accessToken: string; + expiresInSeconds: number; + user: AuthUser; + workspaces: WorkspaceDto[]; +} +export interface LoginRequest { + email: string; + password: string; +} +export interface AcceptInviteRequest { + inviteCode: string; + companyRegistrationNo: string; +} +export interface KintexPrincipal { + userId: string; + displayName: string; + eventRoles: Record; + hallManager: boolean; +} + +// ── 3. M2 플로어플랜 (SCR-03/04) ── +export type BoothType = 'basic' | 'premium' | 'independent' | 'island'; +export interface BoothDto { + boothId: string; + boothNo: string; + type: BoothType | string; + polygon: number[][]; // 홀 로컬 좌표계(미터), 닫힌 링 + sizeM: [number, number]; + heightM: number; + floorLoadTPerM2: number; + assignedCompanyName: string | null; + premium: boolean; +} +export interface LayoutSummary { + boothCount: number; + targetBoothCount: number; + salesAreaM2: number; + minAisleWidthM: number; + violationBlock: number; + violationWarn: number; +} +export interface LayoutDto { + layoutId: string; + eventId: string; + hallId: string; + version: number; + name: string; + status: string; + booths: BoothDto[]; + summary: LayoutSummary; + updatedAt: string; +} +export interface LayoutSaveRequest { + name: string; + version: number; + booths: BoothDto[]; +} + +// 규정검증 리포트 (§3-validate / §4 precheck 공통 shape) +export type ViolationSeverity = 'block' | 'warn'; +export interface Violation { + pin: number; + code: string; + group: string; + label: string; + severity: ViolationSeverity; + measured: string; + requiresDocument?: string; +} +export interface ComplianceReport { + rulesetVersion: string; + disclaimer: string; + blockCount: number; + warnCount: number; + passCount: number; + submittable: boolean; + violations: Violation[]; +} + +// AI 자동배치 +export interface AutoLayoutRequest { + targetBoothCount: number; + premiumRatio: number; + stageCount: number; + loungeCount: number; + mainEntranceCount: number; + optionCount: number; +} +export interface AutoLayoutOption { + optionId: string; + label: string; + summary: LayoutSummary; + s7RenderJobId: string; +} + +// ── 6. M5 RenderJob (SCR-06/12) ── +export type RenderStatus = 'QUEUED' | 'RUNNING' | 'DONE' | 'FAILED'; +export interface RenderJobDto { + jobId: string; + boothId: string; + shotPreset: string; + status: RenderStatus; + imageUrl: string | null; + schemaHash: string | null; + modelVersion: string | null; + watermarkRequired: boolean; + watermarkText: string; + notice: string; + errorMessage: string | null; + createdAt: string; +} diff --git a/src/frontend/src/api/websocket.ts b/src/frontend/src/api/websocket.ts new file mode 100644 index 0000000..915e488 --- /dev/null +++ b/src/frontend/src/api/websocket.ts @@ -0,0 +1,74 @@ +/* + * WebSocket(STOMP over SockJS) — RenderJob 완료 푸시 구독. + * 근거: 계약 §6 — 핸드셰이크 GET /ws(SockJS), 브로드캐스트 prefix /topic, + * 구독 /topic/render/{jobId} → RenderJobDto (design.md §1-4). + */ +import { Client, type IMessage, type StompSubscription } from '@stomp/stompjs'; +import SockJS from 'sockjs-client'; +import type { RenderJobDto } from './types'; + +let client: Client | null = null; +let refCount = 0; + +const WS_URL = `${(import.meta.env.VITE_API_BASE as string | undefined) ?? ''}/ws`; + +function ensureClient(): Client { + if (client) return client; + client = new Client({ + // SockJS 팩토리 (계약이 SockJS 핸드셰이크 명시). + webSocketFactory: () => new SockJS(WS_URL) as unknown as WebSocket, + reconnectDelay: 4000, + heartbeatIncoming: 10000, + heartbeatOutgoing: 10000, + // 조용한 프로덕션 로깅 + debug: () => {}, + }); + client.activate(); + return client; +} + +/** + * 특정 RenderJob 완료/실패 이벤트를 구독한다. + * @returns 해제 함수 — 언마운트 시 호출. + */ +export function subscribeRenderJob( + jobId: string, + onUpdate: (job: RenderJobDto) => void, +): () => void { + const c = ensureClient(); + refCount += 1; + + let sub: StompSubscription | null = null; + const topic = `/topic/render/${jobId}`; + + const doSubscribe = () => { + sub = c.subscribe(topic, (msg: IMessage) => { + try { + onUpdate(JSON.parse(msg.body) as RenderJobDto); + } catch { + /* 잘못된 페이로드 무시 */ + } + }); + }; + + if (c.connected) { + doSubscribe(); + } else { + // 연결 후 구독 + const prev = c.onConnect; + c.onConnect = (frame) => { + prev?.(frame); + doSubscribe(); + }; + } + + return () => { + sub?.unsubscribe(); + refCount -= 1; + if (refCount <= 0 && client) { + void client.deactivate(); + client = null; + refCount = 0; + } + }; +} diff --git a/src/frontend/src/components/layout/AppShell.tsx b/src/frontend/src/components/layout/AppShell.tsx new file mode 100644 index 0000000..6b27749 --- /dev/null +++ b/src/frontend/src/components/layout/AppShell.tsx @@ -0,0 +1,84 @@ +import { Outlet } from 'react-router-dom'; +import { useAuthStore } from '../../store/authStore'; +import { DdayChip } from '../ui/Badge'; +import './app-shell.css'; + +const NAV_ITEMS = [ + { key: 'dashboard', label: '대시보드', icon: '▦' }, + { key: 'floorplan', label: '플로어플랜 스튜디오', icon: '◱' }, + { key: 'exhibitors', label: '참가업체 관리', icon: '☷' }, + { key: 'documents', label: '서류·마일스톤', icon: '❒' }, + { key: 'settlement', label: '정산', icon: '₩' }, +]; + +/** + * 웹 인증 셸 (design.md §2-2) — 좌측 240px 사이드바 + 상단 바(브레드크럼·D-데이·알림). + * 캔버스 화면은 이 셸 안에서 전폭을 사용한다. + */ +export function AppShell() { + const user = useAuthStore((s) => s.user); + const workspace = useAuthStore((s) => s.currentWorkspace()); + const logout = useAuthStore((s) => s.logout); + + return ( +
+ + +
+
+
+ {workspace?.eventName ?? '워크스페이스'} + / + 플로어플랜 스튜디오 +
+
+ {workspace && } + +
+
+ +
+ +
+
+
+ ); +} diff --git a/src/frontend/src/components/layout/app-shell.css b/src/frontend/src/components/layout/app-shell.css new file mode 100644 index 0000000..6b4db3b --- /dev/null +++ b/src/frontend/src/components/layout/app-shell.css @@ -0,0 +1,187 @@ +.kx-shell { + display: flex; + height: 100vh; + overflow: hidden; +} + +.kx-shell__sidebar { + width: var(--sidebar-w); + flex-shrink: 0; + background: var(--color-white); + border-right: var(--border-card); + display: flex; + flex-direction: column; + padding: var(--space-5) 0; +} + +.kx-shell__brand { + display: flex; + align-items: center; + gap: var(--space-3); + padding: 0 var(--space-5); + margin-bottom: var(--space-5); +} +.kx-shell__brand-mark { + width: 32px; + height: 32px; + border-radius: var(--radius-sm); + background: var(--color-primary-600); + color: var(--color-white); + display: flex; + align-items: center; + justify-content: center; + font-weight: 700; +} +.kx-shell__brand-text { + display: flex; + flex-direction: column; + font-size: var(--fs-h3); + font-weight: 700; + color: var(--color-primary-700); + line-height: 1.1; +} +.kx-shell__brand-text small { + font-size: 11px; + font-weight: 500; + color: var(--color-neutral-500); +} + +.kx-shell__event-switch { + margin: 0 var(--space-4) var(--space-5); + padding: var(--space-3); + background: var(--color-neutral-050); + border: var(--border-card); + border-radius: var(--radius-lg); + text-align: left; + display: flex; + flex-direction: column; + gap: 2px; +} +.kx-shell__event-name { + font-size: var(--fs-body); + font-weight: 600; + color: var(--color-neutral-900); +} +.kx-shell__event-meta { + font-size: var(--fs-caption); + color: var(--color-neutral-500); +} + +.kx-shell__nav { + list-style: none; + margin: 0; + padding: 0 var(--space-3); + display: flex; + flex-direction: column; + gap: 2px; + flex: 1; +} +.kx-shell__nav-link { + display: flex; + align-items: center; + gap: var(--space-3); + padding: 10px var(--space-4); + border-radius: var(--radius-sm); + color: var(--color-neutral-700); + font-size: var(--fs-body); + font-weight: 500; +} +.kx-shell__nav-link:hover { + background: var(--color-neutral-050); + color: var(--color-neutral-900); +} +.kx-shell__nav-link.is-active { + background: var(--color-primary-100); + color: var(--color-primary-700); + font-weight: 600; +} +.kx-shell__nav-icon { + width: 20px; + text-align: center; +} + +.kx-shell__user { + display: flex; + align-items: center; + gap: var(--space-2); + padding: var(--space-4) var(--space-5) 0; + border-top: var(--border-card); + margin-top: var(--space-3); +} +.kx-shell__avatar { + width: 28px; + height: 28px; + border-radius: 50%; + background: var(--color-primary-100); + color: var(--color-primary-700); + display: flex; + align-items: center; + justify-content: center; + font-weight: 600; + font-size: 13px; +} +.kx-shell__user-name { + flex: 1; + font-size: var(--fs-caption); + font-weight: 500; + color: var(--color-neutral-700); +} +.kx-shell__logout { + background: none; + border: none; + font-size: var(--fs-caption); + color: var(--color-neutral-500); +} +.kx-shell__logout:hover { + color: var(--color-primary-600); +} + +.kx-shell__main { + flex: 1; + display: flex; + flex-direction: column; + min-width: 0; +} +.kx-shell__topbar { + height: 56px; + flex-shrink: 0; + background: var(--color-white); + border-bottom: var(--border-card); + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 var(--gutter); +} +.kx-shell__crumb { + font-size: var(--fs-body); + color: var(--color-neutral-500); + display: flex; + align-items: center; + gap: var(--space-2); +} +.kx-shell__crumb-sep { + color: var(--color-neutral-200); +} +.kx-shell__topbar-right { + display: flex; + align-items: center; + gap: var(--space-3); +} +.kx-shell__icon-btn { + width: 32px; + height: 32px; + border-radius: 50%; + border: none; + background: transparent; + color: var(--color-neutral-500); +} +.kx-shell__icon-btn:hover { + background: var(--color-neutral-100); +} + +.kx-shell__content { + flex: 1; + min-height: 0; + overflow: hidden; + display: flex; +} diff --git a/src/frontend/src/components/ui/AiImage.tsx b/src/frontend/src/components/ui/AiImage.tsx new file mode 100644 index 0000000..290c7d6 --- /dev/null +++ b/src/frontend/src/components/ui/AiImage.tsx @@ -0,0 +1,66 @@ +import type { RenderJobDto } from '../../api/types'; +import { AiLabel } from './Badge'; +import './ai-image.css'; + +/* + * AI 생성 이미지 카드 — 워터마크·고지문 상시 노출(제거 불가). + * 근거: design.md §1-4 + 계약 §0-3 (watermarkRequired/watermarkText/notice 항상 포함). + */ +interface AiImageProps { + imageUrl: string | null; + watermarkText: string; + notice: string; + shotLabel?: string; + versionTag?: string; + status?: RenderJobDto['status']; + onRetry?: () => void; + alt?: string; +} + +export function AiImage({ + imageUrl, + watermarkText, + notice, + shotLabel, + versionTag, + status = 'DONE', + onRetry, + alt = 'AI 생성 예상 이미지', +}: AiImageProps) { + return ( +
+
+ {status === 'DONE' && imageUrl ? ( + {alt} + ) : status === 'FAILED' ? ( +
+ 생성 실패 + {onRetry && ( + + )} +
+ ) : ( +
+
+ )} + + {/* 상시 워터마크 (제거 불가) */} + + +
+ AI 생성 이미지 + {shotLabel && {shotLabel}} + {versionTag && {versionTag}} +
+
+ {/* 고지문 — 항상 노출 */} +
{notice}
+
+ ); +} diff --git a/src/frontend/src/components/ui/Badge.tsx b/src/frontend/src/components/ui/Badge.tsx new file mode 100644 index 0000000..15384e8 --- /dev/null +++ b/src/frontend/src/components/ui/Badge.tsx @@ -0,0 +1,61 @@ +import type { ReactNode } from 'react'; +import './badge.css'; + +// ── StatusBadge (design.md §1-4) ── +export type FlowStatus = + | 'draft' + | 'submitted' + | 'ai-review' + | 'approved' + | 'rejected' + | 'building' + | 'inspected'; + +const STATUS_LABEL: Record = { + draft: '작성중', + submitted: '제출됨', + 'ai-review': 'AI검토', + approved: '승인', + rejected: '반려', + building: '시공중', + inspected: '검수완료', +}; + +/** 신청 흐름 상태 배지 — 모든 목록·카드 공통. */ +export function StatusBadge({ status }: { status: FlowStatus }) { + return ( + {STATUS_LABEL[status]} + ); +} + +/** 역할 배지 (SCR-01 워크스페이스 카드). */ +export function RoleBadge({ role }: { role: string }) { + const label = + role === 'ORGANIZER' + ? '주최자' + : role === 'EXHIBITOR' + ? '참가업체' + : role === 'CONTRACTOR' + ? '장치업체' + : role === 'HALL_MANAGER' + ? '홀매니저' + : role; + return {label}; +} + +/** D-데이 칩 — D-3 이내 warning, 초과 error. */ +export function DdayChip({ dday }: { dday: number }) { + const tone = dday < 0 ? 'over' : dday <= 3 ? 'warn' : 'normal'; + const text = dday < 0 ? `D+${Math.abs(dday)}` : `D-${dday}`; + return {text}; +} + +/** AI 라벨 — 보라 외곽선 칩. */ +export function AiLabel({ children = 'AI 초안' }: { children?: ReactNode }) { + return ( + + + {children} + + ); +} diff --git a/src/frontend/src/components/ui/Button.tsx b/src/frontend/src/components/ui/Button.tsx new file mode 100644 index 0000000..65f0815 --- /dev/null +++ b/src/frontend/src/components/ui/Button.tsx @@ -0,0 +1,30 @@ +import type { ButtonHTMLAttributes, ReactNode } from 'react'; +import './button.css'; + +type Variant = 'primary' | 'secondary' | 'ai' | 'ghost' | 'danger'; + +interface ButtonProps extends ButtonHTMLAttributes { + variant?: Variant; + leadingIcon?: ReactNode; + block?: boolean; +} + +/** design.md §1-4 — 주요 버튼(primary blue), AI 액션(ai-accent 보라). radius 4px. */ +export function Button({ + variant = 'primary', + leadingIcon, + block, + className = '', + children, + ...rest +}: ButtonProps) { + return ( + + ); +} diff --git a/src/frontend/src/components/ui/States.tsx b/src/frontend/src/components/ui/States.tsx new file mode 100644 index 0000000..a16d13e --- /dev/null +++ b/src/frontend/src/components/ui/States.tsx @@ -0,0 +1,62 @@ +import type { ReactNode } from 'react'; +import { Button } from './Button'; +import './states.css'; + +/** 로딩 스켈레톤 블록. */ +export function Skeleton({ height = 16, width = '100%', radius = 4 }: { + height?: number | string; + width?: number | string; + radius?: number; +}) { + return ( +