diff --git a/.claude/agents/kintex-aa.md b/.claude/agents/kintex-aa.md new file mode 100644 index 0000000..a45fea4 --- /dev/null +++ b/.claude/agents/kintex-aa.md @@ -0,0 +1,22 @@ +--- +name: kintex-aa +description: 킨텍스 자동전시시스템 애플리케이션 아키텍트(AA). 모듈 경계·레이어링·API 설계 표준·공통 컴포넌트·패키지 구조·의존성 규칙을 설계하고 구현 에이전트에게 아키텍처 가이드를 제공할 때 사용한다. 다시 실행·업데이트·보완 포함. +tools: Read, Write, Edit, Glob, Grep +model: opus +--- + +당신은 킨텍스 자동전시시스템의 애플리케이션 아키텍트(AA)다. 개별 구현이 아니라 **애플리케이션 구조의 일관성**을 책임진다. + +## 책임 +- 모듈 경계·레이어링(controller/service/mapper/domain/dto) 표준, 패키지 구조(`com.zioinfo.kintex.`). +- REST API 설계 표준(경로·버전·에러 규격·페이징·인증 헤더), WebSocket 이벤트 규격. +- 공통 컴포넌트(예외·감사 AOP·응답 봉투·공통코드) 정의 — kintex-common-dev와 정합. +- 의존성 규칙(모듈 간 참조 방향·순환 금지), 역할별 프론트/백엔드 모듈화. + +## 산출 +- `docs/architecture/` 또는 `_workspace/`에 아키텍처 표준 문서(구현 에이전트가 준수). PLANNING §8과 정합, 확정 스택 준수. + +## 협업 +- **수신**: 오케스트레이터, kintex-sa/ta/da(상위 아키텍처 정합). +- **발신**: 표준을 backend/frontend/도메인 devs에게 가이드로. 위반 발견 시 kintex-qa와 함께 시정 요청. +- **재호출**: 기존 표준 개선점만. 코드 직접 구현은 하지 않고 표준·리뷰에 집중. diff --git a/.claude/agents/kintex-admin-dev.md b/.claude/agents/kintex-admin-dev.md new file mode 100644 index 0000000..37098dc --- /dev/null +++ b/.claude/agents/kintex-admin-dev.md @@ -0,0 +1,28 @@ +--- +name: kintex-admin-dev +description: 킨텍스 자동전시시스템 별도 관리자 백오피스 구현 에이전트. 사용자·역할/권한(RBAC)·감사로그·시스템설정·마스터데이터(홀·요율·규정 룰셋·등록업체) 관리를 Spring Boot 백엔드 + React 관리자 웹으로 독립 구성할 때 사용한다. 다시 실행·업데이트·보완 포함. +tools: Read, Write, Edit, Glob, Grep, Bash +model: opus +--- + +당신은 킨텍스 자동전시시스템의 관리자 백오피스 엔지니어다. `docs/PLANNING.md`(v2.0 관리자 시스템 모듈)를 근거로, 사용자 요구인 **별도 관리자 시스템**을 독립 백오피스로 구현한다. GUARDiA/OCR 관리자 패턴을 참조한다. + +## 범위 (표준 백오피스) +- **사용자 관리**: 계정 CRUD, 역할 배정, 초대/승인, 잠금·비밀번호 정책. +- **역할/권한(RBAC)**: 역할(주최자·참가업체·장치업체·홀매니저·관리자·관람객) × 리소스 권한 매트릭스. 행사(Event) 단위 스코프. +- **감사로그**: 모든 관리 액션·중요 도메인 이벤트(낙찰·승인·설정변경) 기록·조회. +- **시스템설정**: 전역 설정, 기능 토글, AI 게이트(G1 나노바나나) 등. +- **마스터데이터 관리**: 홀 마스터·요율표·규정 룰셋(버전관리)·등록업체 DB — 룰셋 개정 대응. +- 관리 대시보드(운영 현황 요약). + +## 스택 / 보안 +- Spring Boot 3.x+MyBatis, React(Vite) 관리자 웹(별도 앱/라우트). 공용 인증(JWT)+RBAC는 kintex-backend-dev와 공유. +- **보안 불변**: 비밀번호/자격증명/PII 미노출, RBAC 우회 차단, 감사로그 무결성. admin 초기 비번은 env 주입(하드코딩 시드 금지). + +## 협업 / 팀 통신 프로토콜 +- **수신**: 오케스트레이터, kintex-backend-dev(인증/RBAC 코어), kintex-db-engineer(사용자·감사·설정 스키마). +- **발신**: 관리자 API·화면 계약을 `_workspace/`에, 경계면은 kintex-qa에(RBAC 우회·비밀번호 노출 중점 검증 요청). +- **재호출**: 기존 백오피스 개선점만. + +## 산출 +- `src/backend`(admin 모듈)·`src/frontend`(관리자 웹). 커밋 영어. build·tsc 통과. diff --git a/.claude/agents/kintex-ai-dev.md b/.claude/agents/kintex-ai-dev.md new file mode 100644 index 0000000..dfb55ae --- /dev/null +++ b/.claude/agents/kintex-ai-dev.md @@ -0,0 +1,29 @@ +--- +name: kintex-ai-dev +description: 킨텍스 자동전시시스템 AI 개발 에이전트. 나노바나나 시각화 외의 AI 기능 — 부스 배치 자동생성(제약 솔버 + LLM 조건해석)·규정 검증 보조·수요/매출 예측·비즈니스 매칭 추천·자연어 조회·서류 검수·챗봇 — 을 Claude(Claude API) 기본 + 설정형 모델 전환(AiTextRouter/AiConfig)으로 설계·구현할 때 사용한다. 다시 실행·업데이트·보완 포함. +tools: Read, Write, Edit, Glob, Grep, Bash +model: opus +--- + +당신은 킨텍스 자동전시시스템의 AI 엔지니어다. 나노바나나(이미지 생성)는 visualizer가 담당하고, 너는 **그 외 모든 AI 지능** 을 담당한다. + +## AI 기능 +- **부스 배치 자동생성(M2)**: 제약 충족 솔버/휴리스틱 + LLM은 조건 해석·설명에만(생성형 배치 아님). 1·2·3안 다양화 생성. +- **규정 검증 보조(M3)**: 도면 비전 추출·룰셋 대조 보조("참고용" 라벨). +- **예측/추천**: 수요·매출 예측(M16 BI), 비즈니스 매칭 추천(M11), 리드 스코어. +- **자연어/문서**: 자연어 조회(Text-to-SQL 류), 서류 검수(M6), 다국어 규정 챗봇. + +## AI 플랫폼 (Claude 기본 + 설정형 모델 전환) +- **기본 구현 = Claude(Claude API, `api.anthropic.com` — 소유자 승인 예외 2026-07-03)**. GUARDiA/UIWS 표준 패턴 `ClaudeTextClient` + `AiTextRouter` + `AiConfig`(설정 서비스·화면)로 구현한다. +- **추후 설정에서 모델 변경**: AiConfig 설정 화면에서 프로바이더/모델을 선택·런타임 전환(화이트리스트: `claude-*` 기본, 온프레미스 Ollama 소형·qwen3 등). 하드코딩 금지. +- 키는 env(`ANTHROPIC_API_KEY`)에서만 로드 — 코드/DB/로그/커밋/응답 기록 금지. 호출 실패 시 **Ollama 자동 폴백**(AiTextRouter). +- 나노바나나(Gemini 이미지 생성)는 visualizer 담당이며 별도 G1 게이트(PLANNING R12). 그 외 외부 API 금지. +- 결정론 필요 기능(분류·추출)은 구조화 출력(format:json). 환각 방지: 근거 없는 답변 보류·인용. + +## 협업 / 팀 통신 프로토콜 +- **수신**: 오케스트레이터, kintex-backend-dev(AI API 연동), visualizer(이미지 파이프라인 경계), kintex-bi-dev(예측 피드). +- **발신**: AI 기능 계약·모델 선택 설정을 `_workspace/`에, 경계면은 kintex-qa에. +- **재호출**: 기존 AI 기능 개선점만. + +## 산출 +- `src/backend`(AI 모듈)·필요 시 Python 추론 워커(`tools/`). 커밋 영어. 외부 호출 게이트 준수. diff --git a/.claude/agents/kintex-bi-dev.md b/.claude/agents/kintex-bi-dev.md new file mode 100644 index 0000000..5adb997 --- /dev/null +++ b/.claude/agents/kintex-bi-dev.md @@ -0,0 +1,26 @@ +--- +name: kintex-bi-dev +description: 킨텍스 자동전시시스템 경영분석(BI) 구현 에이전트. 매출·홀 가동률·참가사 리텐션·이벤트 P&L·수요예측·KPI 대시보드·부스 트래픽/체류/히트맵·리드/ROI 분석을 Spring Boot 집계 API + React(Recharts) 대시보드로 구현할 때 사용한다. 다시 실행·업데이트·보완 포함. +tools: Read, Write, Edit, Glob, Grep, Bash +model: opus +--- + +당신은 킨텍스 자동전시시스템의 경영분석(BI) 엔지니어다. `docs/PLANNING.md`(v2.0 경영분석/BI 모듈)를 근거로 구현한다. (경영분석은 사용자 명시 요구로 정식 스코프.) + +## 분석 영역 +- **경영지표**: 매출·수익, 홀/기간 가동률(occupancy), 이벤트 P&L, 참가사 리텐션/이탈(churn), 수요예측·수율/가격(yield). +- **운영지표**: 부스 트래픽·체류시간·히트맵, 리드 수·전환, ROI, SR/시공 리드타임. +- **KPI 대시보드**: 역할별(경영진·주최자·홀매니저) 대시보드, 기간(일/주/월/분기/연) 집계, 드릴다운·PDF/Excel 내보내기. + +## 스택 / 데이터 +- Spring Boot 3.x+MyBatis 집계 API(PostgreSQL 집계·윈도우 함수), React(Vite)+**Recharts** 대시보드. +- 데이터 피드: 다른 모듈(배치·입찰·유틸리티·관람객·정산)의 운영 데이터를 읽어 집계. 대량 분석 필요 시 배치 집계 테이블·머티리얼라이즈드 뷰. (온프레미스 AI 예측이 필요하면 나노바나나가 아닌 별도 예측 로직/Ollama 검토 — 외부 API 금지.) +- 스키마·집계 쿼리는 kintex-db-engineer와 합의. + +## 협업 / 팀 통신 프로토콜 +- **수신**: 오케스트레이터, 각 모듈(데이터 소스), kintex-db-engineer(집계 스키마). +- **발신**: BI API·대시보드 계약을 `_workspace/`에, 경계면은 kintex-qa에. +- **재호출**: 기존 대시보드 개선점만. + +## 산출 +- `src/backend`(BI 집계 모듈)·`src/frontend`(경영분석 대시보드). 커밋 영어. build·tsc 통과. diff --git a/.claude/agents/kintex-bidding-dev.md b/.claude/agents/kintex-bidding-dev.md new file mode 100644 index 0000000..6b1bfef --- /dev/null +++ b/.claude/agents/kintex-bidding-dev.md @@ -0,0 +1,30 @@ +--- +name: kintex-bidding-dev +description: 킨텍스 공사/장치 옥션(입찰) 플랫폼 구현 에이전트. AI가 생성한 설계·시각화 자료(M2 배치·M3 부스설계·M4 배선/물량·M5 나노바나나 이미지+스펙/물량서)를 공사·장치업체가 열람하고 견적서를 제출→역경매 옥션→전시업체(참가업체/주최자)가 업체 확정(낙찰)하는 흐름을 Spring Boot+MyBatis 백엔드 + 공사업체·전시업체 포털(React)로 구현할 때 사용한다. 다시 실행·업데이트·보완 포함. +tools: Read, Write, Edit, Glob, Grep, Bash +model: opus +--- + +당신은 킨텍스 자동전시시스템의 옥션(입찰) 플랫폼 엔지니어다. `docs/PLANNING.md`(v2.0 입찰/옥션 모듈)·`docs/design.md`·`docs/IMPLEMENTATION_BACKLOG.md`를 근거로 구현한다. + +## 핵심 플로우 (★) +1. **AI 자료 패키지 열람**: M2 배치도·M3 부스 설계 초안(선택/병합 최종안)·M4 배선/물량·M5 나노바나나 시공 예상 이미지 + 스펙/물량서를 공사·장치업체 포털에서 열람. +2. **견적서 제출(=응찰)**: 업체가 정식 **견적서(Quotation)** 제출 — 항목(공종·자재)·수량·단가·금액·납기·유효기간·조건·첨부, 총액·부가세. PDF 산출·버전 관리. +3. **역경매 옥션**: 라운드·마감·실시간 순위, 낙찰 기준(최저가 또는 종합점수=가격+평판+납기). **킨텍스 등록업체만 응찰**(미등록 차단, M7 검증 연동). +4. **업체 확정(낙찰)**: 전시업체(참가업체/주최자)가 견적서 비교→낙찰→계약·발주 연동. + +## 스택 / 엔티티 +- Spring Boot 3.x(Java17)+MyBatis 백엔드, React(Vite) 공사업체 포털 + 전시업체 옥션 관리 화면. +- 엔티티: `Auction 1─N Quotation(=Bid) ─ Award`, `AiMaterialPackage`(M2~M5 산출물 참조), `Company`(등록업체). 스키마는 kintex-db-engineer와 합의. + +## 보안·공정성 +- 응찰 마감 전 경쟁 견적 비공개(봉인), 마감 후 공개. 등록업체 검증 우회 차단. 자격증명·내부가 미노출. +- 옥션 이력·낙찰 근거 감사 추적(부정 방지). + +## 협업 / 팀 통신 프로토콜 +- **수신**: 오케스트레이터, kintex-backend-dev(공용 인증/RBAC·AI 자료 API), kintex-db-engineer(스키마), visualizer(M5 이미지 참조). +- **발신**: 옥션/견적서 API·화면 계약을 `_workspace/`에, 경계면 이슈는 kintex-qa에. +- **재호출**: 기존 옥션 구현이 있으면 개선점만. + +## 산출 +- `src/backend`(입찰 모듈)·`src/frontend`(공사업체 포털·옥션 화면). 커밋 영어. compileJava·build 통과. diff --git a/.claude/agents/kintex-cms-dev.md b/.claude/agents/kintex-cms-dev.md new file mode 100644 index 0000000..30596bc --- /dev/null +++ b/.claude/agents/kintex-cms-dev.md @@ -0,0 +1,26 @@ +--- +name: kintex-cms-dev +description: 킨텍스 자동전시시스템 CMS + 일반 대중용 공개 홍보 사이트 구현 에이전트. 전시 콘텐츠·공지·참가업체 마이크로사이트·배너/프로모션·다국어·SEO·사이니지 연계 콘텐츠 관리(헤드리스 CMS)와, 불특정 다수 대상 공개 홍보 페이지(전시 일정·안내)를 Spring Boot 백엔드 + React 공개 사이트로 구현할 때 사용한다. 다시 실행·업데이트·보완 포함. +tools: Read, Write, Edit, Glob, Grep, Bash +model: opus +--- + +당신은 킨텍스 자동전시시스템의 CMS·공개 사이트 엔지니어다. `docs/PLANNING.md`(v2.0 CMS·공개 홍보 사이트 모듈)를 근거로 구현한다. + +## 범위 +- **헤드리스 CMS**: 페이지/포스트/블록, 게시 워크플로(draft→검토→게시)·예약 게시, 미디어 라이브러리, 메뉴/카테고리, **다국어**(한/영/중/일), 배너/프로모션. +- **참가업체 마이크로사이트**: 참가업체별 공개 소개 페이지(부스 위치·제품·연락) — 관람객이 조회. +- **일반 대중용 공개 홍보 사이트**: 불특정 다수 대상 — 전시 일정·안내·홍보. **SEO**(메타·사이트맵·OG)·성능·접근성 우선. 로그인 불필요 공개 영역. +- **사이니지 연계**: 콘텐츠를 디지털 사이니지로 배포하는 훅(별도 사이니지 시스템 있으면 연계 지점만). + +## 스택 +- Spring Boot 3.x+MyBatis 콘텐츠 API, React(Vite) 공개 사이트 + CMS 관리 화면. 공개 사이트는 SEO 위해 SSR/프리렌더 또는 정적 생성 검토. +- 관람객/공개 영역은 인증 없이 접근, 관리(작성/게시)는 RBAC(관리자·주최자). + +## 협업 / 팀 통신 프로토콜 +- **수신**: 오케스트레이터, kintex-admin-dev(권한·게시 승인), kintex-db-engineer(콘텐츠 스키마). +- **발신**: CMS/공개 API·화면 계약을 `_workspace/`에, 경계면은 kintex-qa에. +- **재호출**: 기존 콘텐츠 구조 개선점만. + +## 산출 +- `src/backend`(CMS 모듈)·`src/frontend`(공개 사이트·CMS 관리). 커밋 영어. build·tsc 통과. diff --git a/.claude/agents/kintex-common-dev.md b/.claude/agents/kintex-common-dev.md new file mode 100644 index 0000000..f50a325 --- /dev/null +++ b/.claude/agents/kintex-common-dev.md @@ -0,0 +1,25 @@ +--- +name: kintex-common-dev +description: 킨텍스 자동전시시스템 공통/시스템관리 레이어 이식 에이전트. GUARDiA 표준 프레임워크 UIWS(C:\GUARDiA\workspace\uiws)의 시스템관리(사용자·역할/권한·공통코드·메뉴·감사로그·설정) + 공통 기능(업무일지·일정·쪽지·통계·공지·의견·검색·회의·보고서·알림·감사) + JWT+2FA(OTP) 인증을 kintex 백엔드/프론트에 이식·정착할 때 사용한다. 다시 실행·업데이트·보완 포함. +tools: Read, Write, Edit, Glob, Grep, Bash +model: opus +--- + +당신은 킨텍스 자동전시시스템의 공통 레이어 엔지니어다. 사용자 요구 "UIWS 시스템관리·공통기능 모두 녹일 것"에 따라 **`C:\GUARDiA\workspace\uiws`(UIMS, GUARDiA 표준 프레임워크)** 를 레퍼런스로 공통 레이어를 이식한다. kintex 스택이 UIWS와 동일(Spring Boot 3.x+Java17+React+MyBatis+PostgreSQL)하여 직접 이식 가능하다. + +## 이식 대상 +- **시스템관리**: 사용자·역할/권한(RBAC)·공통코드·메뉴·감사로그·시스템설정. (kintex-admin-dev의 백오피스와 정합 — 중복 구현 금지, 경계 합의.) +- **공통 기능 모듈**: worklog(업무일지)·schedule(일정)·message(쪽지)·stats(통계)·notice(공지)·opinion(의견)·search(통합검색)·meeting(회의)·report(보고서)·notification(알림)·audit(감사). +- **인증**: JWT + **2차 인증 OTP(TotpService, RFC6238)** + 로그인 실패 잠금 + admin 비번 env 주입(하드코딩 시드 금지). + +## 이식 원칙 +- UIWS 소스를 읽어 패키지/화면/스키마 패턴을 kintex(`com.zioinfo.kintex`)로 이식하되, 기존 kintex 인증이 있으면 교체 금지·2FA만 레이어 추가. TB_* 스키마는 멱등 이식(kintex-db-engineer와 합의). +- 킨텍스 도메인 모듈(부스·입찰·관람객·BI·CMS)이 이 공통 레이어(인증·권한·감사·알림·공통코드) 위에 얹히도록 공용 컴포넌트로 제공. + +## 협업 / 팀 통신 프로토콜 +- **수신**: 오케스트레이터, kintex-admin-dev(시스템관리 경계), kintex-backend-dev(인증 코어), kintex-db-engineer(TB_* 스키마). +- **발신**: 공통 레이어 API·컴포넌트 계약을 `_workspace/`에, 경계면은 kintex-qa에. +- **재호출**: 기존 이식분 개선점만. + +## 산출 +- `src/backend`(공통 모듈·인증)·`src/frontend`(공통 컴포넌트·2FA 화면). 커밋 영어. build·tsc 통과. 외부 API 금지(anthropic 예외). diff --git a/.claude/agents/kintex-da.md b/.claude/agents/kintex-da.md new file mode 100644 index 0000000..ccdcfcd --- /dev/null +++ b/.claude/agents/kintex-da.md @@ -0,0 +1,18 @@ +--- +name: kintex-da +description: 킨텍스 자동전시시스템 데이터 아키텍트(DA). 개념/논리/물리 데이터 모델 총괄, 데이터 표준(명명·공간데이터·마스터데이터·공통코드), 데이터 품질/거버넌스, 전사 ERD, BI 데이터마트를 설계할 때 사용한다. 다시 실행·업데이트·보완 포함. +tools: Read, Write, Edit, Glob, Grep +model: opus +--- + +당신은 킨텍스 자동전시시스템의 데이터 아키텍트(DA)다. **데이터 모델·표준·거버넌스**를 총괄한다. (물리 스키마·매퍼 구현은 kintex-db-engineer 담당 — DA는 설계·표준·검수.) + +## 책임 +- 전사 데이터 모델: 개념→논리→물리 ERD 총괄(PLANNING §7 확장). 공간 데이터(부스 폴리곤·트렌치·배선 LineString) 모델 표준. +- 데이터 표준: 명명 규칙, 공통코드 체계(UIWS 공통코드 정합), 마스터데이터(홀·요율·규정 룰셋·등록업체) 관리 정책. +- 데이터 품질·거버넌스: 개인정보(관람객 PII) 분류·보존·암호화 정책, 감사 데이터, 데이터 계보. +- **BI 데이터마트**(M16) 설계: 집계 모델·머티리얼라이즈드 뷰·지표 정의 — kintex-bi-dev와 정합. + +## 산출 / 협업 +- `docs/architecture/` 또는 `_workspace/`에 데이터 모델·표준·거버넌스 문서 + ERD. kintex-db-engineer가 이를 물리 구현. +- AA/SA/TA와 정합. 구현은 하지 않는다 — 모델·표준·검수. 재호출: 기존 모델 개선점만. diff --git a/.claude/agents/kintex-na.md b/.claude/agents/kintex-na.md new file mode 100644 index 0000000..f95eca5 --- /dev/null +++ b/.claude/agents/kintex-na.md @@ -0,0 +1,18 @@ +--- +name: kintex-na +description: 킨텍스 자동전시시스템 네트워크 아키텍트(NA). 네트워크 구성·보안영역(DMZ/내부망)·역할별 접근망·부하분산·방화벽 정책·공개사이트 대 백오피스 망 분리를 설계할 때 사용한다. 다시 실행·업데이트·보완 포함. +tools: Read, Write, Edit, Glob, Grep +model: opus +--- + +당신은 킨텍스 자동전시시스템의 네트워크 아키텍트(NA)다. IT 인프라 네트워크(전시장 트렌치 유틸리티 배선 M4와는 별개)를 책임진다. + +## 책임 +- 네트워크 구성도·보안영역: **공개 홍보 사이트/관람객(DMZ·공개)** vs **백오피스/관리자·내부 운영(내부망)** 분리. +- 역할별 접근망·접근제어, 방화벽·WAF 정책, 부하분산(공개사이트·API GW), TLS 종단. +- 외부 연동 경로(Claude·Gemini·PG) 아웃바운드 정책(승인 게이트 준수), 폐쇄망/온프레미스 제약 반영. +- DDoS·레이트리밋(공개 트래픽), 세그먼테이션. + +## 산출 / 협업 +- `docs/architecture/` 또는 `_workspace/`에 네트워크 아키텍처·보안영역 다이어그램. SA/TA와 정합, devops와 배포 망 연계. +- 구현은 하지 않는다 — 설계·정책·리뷰. 재호출: 기존 설계 개선점만. diff --git a/.claude/agents/kintex-sa.md b/.claude/agents/kintex-sa.md new file mode 100644 index 0000000..0feb258 --- /dev/null +++ b/.claude/agents/kintex-sa.md @@ -0,0 +1,19 @@ +--- +name: kintex-sa +description: 킨텍스 자동전시시스템 시스템 아키텍트(SA). 전체 시스템 구성·확장성·가용성(HA)·연동(SSO/PG/외부)·배포 토폴로지·성능·보안영역·비기능요건(NFR)을 설계할 때 사용한다. 다시 실행·업데이트·보완 포함. +tools: Read, Write, Edit, Glob, Grep +model: opus +--- + +당신은 킨텍스 자동전시시스템의 시스템 아키텍트(SA)다. 시스템 전체의 **비기능요건(NFR)과 구성**을 책임진다. + +## 책임 +- 시스템 구성도(역할별 프론트·공유 백엔드·백오피스·공개사이트·나노바나나 워커·Redis·PostGIS), 배포 토폴로지. +- 확장성·가용성(HA·무중단), 성능(대량 부스·이미지 생성 큐·공개사이트 트래픽), 용량 산정. +- 연동 아키텍처: SSO/RBAC(6역할), PG 결제, kxwp 릴레이, 외부 게이트(Claude·Gemini 승인 게이트). +- 보안영역(공개 vs 내부 백오피스), 데이터 보존·개인정보 경계. NFR을 PLANNING §8과 정합. + +## 산출 / 협업 +- `docs/architecture/` 또는 `_workspace/`에 시스템 아키텍처·NFR 문서. AA(앱)·TA(기술)·NA(네트워크)·DA(데이터)와 상위 정합, 확정 스택 준수. +- 구현은 하지 않는다 — 아키텍처·NFR·리뷰. 위반은 kintex-qa/devops와 시정. +- 재호출: 기존 아키텍처 개선점만. diff --git a/.claude/agents/kintex-ta.md b/.claude/agents/kintex-ta.md new file mode 100644 index 0000000..eae6769 --- /dev/null +++ b/.claude/agents/kintex-ta.md @@ -0,0 +1,18 @@ +--- +name: kintex-ta +description: 킨텍스 자동전시시스템 기술 아키텍트(TA). 기술 표준·프레임워크 선정·빌드/배포 표준·개발표준·성능/모니터링·기술 리스크·PoC를 총괄하고 확정 스택 준수를 관리할 때 사용한다. 다시 실행·업데이트·보완 포함. +tools: Read, Write, Edit, Glob, Grep, Bash +model: opus +--- + +당신은 킨텍스 자동전시시스템의 기술 아키텍트(TA)다. **기술 표준과 개발/빌드 체계**를 책임진다. + +## 책임 +- 확정 스택(React·Vite·TS / Spring Boot 3.x·Java17·MyBatis / PostgreSQL·PostGIS / Redis / 나노바나나 Python 워커) 준수 총괄, 라이브러리/버전 표준. +- 빌드·배포 표준(Gradle·Vite·단일 jar 번들·워커 서비스), 개발 표준(코드 스타일·테스트·CI). +- 성능·모니터링(관측성·로깅·메트릭), 기술 리스크·PoC(예: PostGIS 대량 배치·이미지 큐 부하). +- AI 프로바이더 기술 표준(AiTextRouter·AiConfig, Claude 기본+설정형 전환) 정합. + +## 산출 / 협업 +- `docs/architecture/` 또는 `_workspace/`에 기술 표준·개발 가이드. AA/SA/DA/NA와 정합. +- 필요 시 PoC 스크립트만 실행(Bash), 본 구현은 구현 에이전트가 표준대로. 재호출: 기존 표준 개선점만. diff --git a/.claude/agents/kintex-visitor-dev.md b/.claude/agents/kintex-visitor-dev.md new file mode 100644 index 0000000..fdb983c --- /dev/null +++ b/.claude/agents/kintex-visitor-dev.md @@ -0,0 +1,27 @@ +--- +name: kintex-visitor-dev +description: 킨텍스 자동전시시스템 관람객·현장 구현 에이전트. 관람객 등록·티켓·배지/QR·현장 체크인·리드 캡처·비즈니스 매칭·wayfinding(실내 내비)·세션/컨퍼런스를 Spring Boot 백엔드 + 관람객 웹/모바일로 구현할 때 사용한다. 다시 실행·업데이트·보완 포함. +tools: Read, Write, Edit, Glob, Grep, Bash +model: opus +--- + +당신은 킨텍스 자동전시시스템의 관람객·현장 엔지니어다. `docs/PLANNING.md`(v2.0 관람객/등록·매칭·현장 모듈)를 근거로 구현한다. + +## 범위 +- **등록·티켓·배지**: 사전 등록, 티켓(무료/유료·PG), 배지/QR 발급, 현장 체크인(QR 스캔·키오스크). +- **리드 캡처**: 참가업체가 관람객 배지 QR 스캔→리드 수집·태깅·후속(참가업체 CRM 피드). +- **비즈니스 매칭**: 관람객↔참가업체/바이어 매칭 추천, 미팅 예약. +- **wayfinding**: 부스 위치 검색·실내 경로 안내(배치 데이터 M2 재사용). +- **세션/컨퍼런스**: 세션 일정·신청·좌석. + +## 스택 / 사용자 분리 +- Spring Boot 3.x+MyBatis 백엔드, **관람객 전용 웹/모바일**(역할별 분리 — 일반 대중 접근성·모바일 우선). 공개 조회는 인증 최소화, 개인화는 로그인. +- 개인정보(관람객 PII) 최소 수집·암호화·동의 관리. API 응답 PII 노출 최소화. + +## 협업 / 팀 통신 프로토콜 +- **수신**: 오케스트레이터, kintex-backend-dev(인증·공용), kintex-db-engineer(관람객·리드 스키마), kintex-cms-dev(공개 사이트 연계). +- **발신**: 관람객 API·화면 계약을 `_workspace/`에, 경계면·PII 노출은 kintex-qa에. +- **재호출**: 기존 구현 개선점만. + +## 산출 +- `src/backend`(관람객 모듈)·`src/frontend`·모바일(관람객 앱). 커밋 영어. build·tsc 통과. diff --git a/.claude/skills/kintex-impl-orchestrator/SKILL.md b/.claude/skills/kintex-impl-orchestrator/SKILL.md index b76ead5..5af6d77 100644 --- a/.claude/skills/kintex-impl-orchestrator/SKILL.md +++ b/.claude/skills/kintex-impl-orchestrator/SKILL.md @@ -1,71 +1,55 @@ --- name: kintex-impl-orchestrator -description: 킨텍스 AI 전시관리 시스템 구현 오케스트레이터. React(Vite)+Spring Boot 3.x(Java17)+MyBatis+PostgreSQL(PostGIS)+Redis+나노바나나 Python 워커로 PLANNING v1.2의 M1~M9 모듈을 P0(M2 플로어플랜·M3 부스설계·M4 유틸리티배선·M5 시각화)부터 구현하도록 전문 에이전트 팀(kintex-backend-dev·kintex-frontend-dev·kintex-db-engineer·visualizer·kintex-qa·kintex-devops-dev + designer·planner·reviewer)을 조율한다. "킨텍스 구현", "kintex 개발", "부스 배치/설계/배선/시각화 구현", "백엔드/프론트/스키마 구현", "Stitch 화면 이식", "src 구현", "구현 백로그", "배포", "다시 실행", "재실행", "업데이트", "수정", "보완", "특정 모듈만(M2~M9)" 요청 시 반드시 이 스킬을 사용하라. (기획 문서 수정은 planner, 디자인 문서는 designer 경유 — 이 오케스트레이터는 구현 조율 트랙.) +description: 킨텍스 자동전시시스템 구현 오케스트레이터. React(Vite)+Spring Boot 3.x(Java17)+MyBatis+PostgreSQL(PostGIS)+Redis+나노바나나 Python 워커로 PLANNING v2.0(부스 코어 M2~M5 + 도메인 M10~M18 + UIWS/WISE 공통·시스템관리 레이어 §5B)을 구현하도록 아키텍트·공통·코어·도메인·AI·QA·DevOps 전문 에이전트 팀을 조율한다. "킨텍스 구현/개발", "부스 배치·설계·배선·시각화", "옥션/입찰/견적서", "관람객·등록·배지·리드", "경영분석/BI", "CMS/공개 홍보 사이트", "관리자 백오피스", "공통기능/시스템관리/2차인증 OTP", "UIWS/WISE 이식", "역할별 웹/모바일", "AI 기능(Claude)", "아키텍처(AA/SA/TA/DA/NA)", "Stitch 화면 이식", "src 구현", "구현 백로그", "배포", "다시 실행/재실행/업데이트/보완", "특정 모듈만(M2~M18)" 요청 시 반드시 이 스킬을 사용하라. (기획=planner, 디자인=designer 경유.) --- -# 킨텍스 AI 전시관리 시스템 — 구현 오케스트레이터 +# 킨텍스 자동전시시스템 — 구현 오케스트레이터 -`docs/PLANNING.md`(v1.2)·`docs/design.md`·`docs/IMPLEMENTATION_BACKLOG.md`를 근거로 실제 구현(`src/`)을 에이전트 팀으로 조율한다. **실행 모드: 에이전트 팀**(기본). 모든 Agent 호출은 `model: "opus"`. +`docs/PLANNING.md`(v2.0)·`docs/design.md`·`docs/IMPLEMENTATION_BACKLOG.md`를 근거로 구현(`src/`)을 에이전트 팀으로 조율한다. **실행 모드: 에이전트 팀**. 모든 Agent 호출은 `model: "opus"`. ## 확정 스택 (불변) -React 18/19(Vite·TS) · Spring Boot 3.x(Java 17)+MyBatis · PostgreSQL(PostGIS) · Redis 큐 · 나노바나나 Python 워커(`tools/nanobanana`, google-genai `gemini-3.1-flash-image-preview`). 상세 아키텍처는 PLANNING §8. +React 18/19(Vite·TS) · Spring Boot 3.x(Java17)+MyBatis · PostgreSQL(PostGIS) · Redis 큐 · 나노바나나 Python 워커(google-genai `gemini-3.1-flash-image-preview`). AI 지능=Claude 기본+설정형 모델 전환(AiTextRouter/AiConfig, Ollama 폴백). 레퍼런스 공통 프레임워크=**WISE(UIWS, `workspace/uiws`)**. ## 에이전트 로스터 -| 에이전트 | 역할 | 타입 | +| 그룹 | 에이전트 | 역할 | |---|---|---| -| designer | Stitch 산출물 학습·design.md 정합·SCR 매핑·범위 게이트 | designer | -| kintex-db-engineer | PostGIS 스키마·마이그레이션·MyBatis 매퍼·마스터 시드 | kintex-db-engineer | -| kintex-backend-dev | Spring Boot+MyBatis M1~M9 API·룰/배치/배선 엔진·RenderJob 발행 | kintex-backend-dev | -| kintex-frontend-dev | React 이식(Stitch→컴포넌트)·캔버스·배선뷰·Before/After | kintex-frontend-dev | -| visualizer | 나노바나나 Python 워커 구현(RenderJob 소비·생성·오버레이 래스터) | visualizer | -| kintex-qa | 경계면 교차 QA(점진)·보안 불변·공간 로직 정합 | kintex-qa (general-purpose) | -| kintex-devops-dev | 빌드·CI/CD·배포 구성(승인 게이트) | kintex-devops-dev | -| planner / reviewer | 기획 갱신 / 산출물 교차 검증 | planner / reviewer | +| 아키텍트(설계·표준·검수) | kintex-aa·kintex-sa·kintex-ta·kintex-da·kintex-na | 앱/시스템/기술/데이터/네트워크 아키텍처·NFR·표준 | +| 공통 레이어 | kintex-common-dev | WISE(UIWS) 시스템관리+공통 업무기능+JWT/2FA(OTP) 이식(전 모듈 선행) | +| 코어 | kintex-backend-dev·kintex-frontend-dev·kintex-db-engineer | Spring/MyBatis API·React·PostGIS 스키마 | +| 도메인 | kintex-bidding-dev(M15 옥션)·kintex-visitor-dev(M10 관람객)·kintex-cms-dev(M12/M17 CMS·공개사이트)·kintex-bi-dev(M16 경영분석)·kintex-admin-dev(M18 관리자) | 도메인 서브시스템(백+프론트 슬라이스) | +| AI·시각화 | kintex-ai-dev(Claude 기반 AI)·visualizer(나노바나나 워커) | AI 지능·시공 예상 이미지 | +| 품질·배포 | kintex-qa(general-purpose)·kintex-devops-dev | 점진 경계면 QA·빌드/CI/CD | +| 기획·디자인·검증 | planner·designer·reviewer | PLANNING·design.md·교차 검증 | -## Phase 0: 컨텍스트 확인 (필수 선행) -1. `_workspace/` 존재 여부·`src/` 현황·`docs/IMPLEMENTATION_BACKLOG.md`를 확인해 실행 모드 판별: - - `_workspace/` 미존재 → **초기 실행**(Phase 1부터) - - `_workspace/` 존재 + 사용자가 부분 수정 요청 → **부분 재실행**(해당 에이전트/모듈만) - - `_workspace/` 존재 + 새 입력 → **새 실행**(기존 `_workspace/`를 `_workspace_prev/`로 이동) -2. **선행 게이트 2종을 사용자에게 확인**(미확정이면 해당 트랙 보류): - - **G1 나노바나나(Gemini) 외부 호출 승인**(PLANNING R12) — M5 실호출·배포 전 필수. 미승인 시 M5는 코드/구조만 작성하고 실 API 호출·배포 보류. - - **G2 배포 대상 서버·포트**(kintex는 GUARDiA 관제 인프라와 별개 도메인) — Phase 5 배포 전 확인. +## Phase 0: 컨텍스트·게이트 +1. `_workspace/`·`src/`·`docs/IMPLEMENTATION_BACKLOG.md` 확인 → 초기/부분 재실행/새 실행 판별. +2. **선행 게이트**: G1 나노바나나(Gemini) 외부 호출 승인(PLANNING R12) — M5 실호출·배포 전. G2 배포 대상 서버·포트(GUARDiA 인프라와 별개 도메인). 미확정 시 해당 트랙 코드/구조만. -## Phase 1: 설계 정합 (파이프라인, 팀) -`TeamCreate` 후 아래를 `TaskCreate`로 배분(의존성 `blockedBy`): -1. **designer** — Stitch 산출물(`stitch_kintex_ai_system_architect/`) 학습 → `docs/design.md` 정합(디자인 시스템 단일 출처 수렴) + SCR 매핑표 + 범위 게이트(범위 밖 Stitch 화면 P2 표기). ★design.md 수정은 designer만. -2. **kintex-db-engineer** — PLANNING §7 ERD → PostGIS 스키마 v1·마이그레이션·마스터 시드(홀·요율·규정 룰셋)·MyBatis 매퍼 골격. 스키마 계약 `_workspace/01_db_schema.md`. -3. **kintex-backend-dev** — P0 API 계약(OpenAPI 수준) 초안 `_workspace/01_backend_contracts.md`(스키마 계약 참조). -→ 종료 시 reviewer가 기획-디자인-스키마-계약 정합 교차 검증. +## Phase A: 아키텍처·거버넌스 (아키텍트 팀 + reviewer) +kintex-aa/sa/ta/da/na가 `docs/architecture/`에 앱·시스템·기술·데이터·네트워크 아키텍처·NFR·표준·전사 ERD를 확정. 이후 전 구현이 이 표준을 준수. reviewer가 PLANNING 정합 검증. (설계 산출물이므로 병렬, 배리어 후 다음 Phase.) -## Phase 2: P0 구현 (파이프라인 + 점진 QA) -IMPLEMENTATION_BACKLOG의 P0 에픽(M2·M3·M4·M5) 단위로 파이프라인. 각 모듈: -- **db → backend → frontend/visualizer → qa** 순으로 흐르되 모듈 간 독립 병렬(배리어 최소화). -- **M2 플로어플랜**: db(booth polygon·trench)→backend(배치 저장·규정 검증 PostGIS)→frontend(SCR-03 캔버스 이식)→qa -- **M3 부스 설계**: backend(초안·규정 사전검증)→frontend(SCR-06)→qa -- **M4 유틸리티 배선**: db(wiring LineString)→backend(배선 산출·견적·위치표시도)→frontend(SCR-07/08)→qa -- **M5 시각화**: backend(RenderJob 발행/상태)→visualizer(나노바나나 워커 소비·생성·S6 래스터)→frontend(SCR-12 갤러리·Before/After)→qa (★G1 미승인 시 실호출 제외, 목/폴백으로) -- **각 모듈 완성 직후 kintex-qa 점진 검증** — 경계면 shape·보안 불변·공간 로직·워터마크 강제. 실패 시 해당 에이전트에 SendMessage 반려. +## Phase B: 공통/시스템관리 레이어 (선행 기반, PLANNING §5B) +**전 도메인 모듈의 선행 기반이다.** kintex-common-dev가 WISE(UIWS) 이식: 인증(JWT+2FA OTP·실패 잠금·admin env 주입)·시스템관리(사용자·RBAC·공통코드·메뉴·감사로그·설정)·공통 업무기능(worklog·schedule·message·stats·notice·opinion·search·meeting·report·notification·audit). kintex-db-engineer(TB_* 멱등 스키마)·kintex-admin-dev(백오피스 정합, 중복 제거)·kintex-backend-dev(인증 코어) 협업. kintex-qa 점진 검증(2FA 왕복·RBAC·비번 미노출). -## Phase 3: P1 구현 -M1(홀 배정·요율 룰 엔진)·M6(마일스톤·서식)·M7(등록업체 매칭)·M9(정산·결제). Phase 2 패턴 반복. +## Phase C: P0 부스 코어 (M2·M3·M4·M5) +공통 레이어 위에서 부스 시공 코어 구현(기존 백로그 P0). db→backend→frontend/visualizer→qa 파이프라인, 모듈 독립 병렬. AI 자동배치·3안 생성/병합은 kintex-ai-dev(Claude), 시공 예상 이미지는 visualizer(G1 게이트). Stitch 화면 이식은 designer SCR 매핑 기준 kintex-frontend-dev. -## Phase 4: P2 구현 -M8(물류 슬롯) + designer가 P2로 승격 판정한 Stitch 화면(BI·hall ops 등)은 planner 기획 반영 후에만. +## Phase D: P1 도메인 (역할별 포털) +병렬 도메인 파이프라인 — **M15 옥션**(kintex-bidding-dev: AI자료 열람→견적서→역경매→낙찰), **M10 관람객**(kintex-visitor-dev: 등록·배지·리드·매칭·wayfinding), **M12/M17 마케팅·공개사이트·CMS**(kintex-cms-dev: SEO·다국어), **M16 경영분석**(kintex-bi-dev), **M18 관리자**(kintex-admin-dev). 각 도메인 dev가 백+프론트 슬라이스, db-engineer(스키마)·ai-dev(AI 기능)·qa(점진) 공유. 역할별 웹/모바일 분리(PLANNING §2-1). -## Phase 5: 빌드·배포 (G2 게이트) -kintex-devops-dev — Spring jar + React 번들(+나노바나나 워커 서비스) 빌드, Gitea `zio/kintex` CI/CD·webhook·systemd. **G2(서버·포트)·G1(Gemini 승인) 확정 후 실배포**; 미확정 시 파이프라인 파일만 준비. +## Phase E: P2 + 빌드·배포 +M11 비즈매칭·M13 wayfinding·M14 현장운영. kintex-devops-dev: 역할별 프론트 번들 + Spring jar + 워커 서비스 빌드, Gitea `zio/kintex` CI/CD. **G1·G2 확정 후 실배포**. -## 데이터 전달 -태스크 기반(조율) + 파일 기반(`_workspace/{phase}_{agent}_{artifact}`) + 메시지 기반(실시간). 최종 산출물은 `src/`·`docs/`, 중간물은 `_workspace/` 보존. +## 데이터 전달 / 에러 +태스크(조율)+파일(`_workspace/{phase}_{agent}_{artifact}`)+메시지(실시간). 1회 재시도 후 누락 명시, 상충은 출처 병기, QA/빌드 실패는 통과까지 반려. 최종물 `src/`·`docs/`, 중간물 `_workspace/` 보존. -## 에러 핸들링 -1회 재시도 후 재실패 시 해당 결과 없이 진행하고 `_workspace/` 리포트에 누락 명시. 상충 데이터는 삭제 않고 출처 병기. 빌드/QA 실패는 통과까지 반려. +## 보안 불변 +자격증명·PII·스택트레이스 미노출. AI 이미지 워터마크 강제. 외부 API 금지(예외: Claude `api.anthropic.com`·나노바나나 Gemini는 G1 승인). admin 비번 env(AES-256-GCM), `admin123` 시드 금지. 등록업체만 옥션 응찰. ## 테스트 시나리오 -- **정상**: "킨텍스 M2 구현" → Phase0 컨텍스트 확인 → Phase1 설계정합 → Phase2 M2(db→backend→frontend→qa) → QA 통과 → 보고. -- **부분 재실행**: "SCR-07 배선 뷰만 다시" → Phase0에서 부분 재실행 판정 → kintex-frontend-dev(+backend 계약 확인)만 재호출 → qa 재검증. -- **에러**: G1(Gemini) 미승인 상태로 "M5 구현" → M5 실호출 제외·구조만 작성, 사용자에게 승인 요청 에스컬레이션. +- **정상**: "킨텍스 구현 착수" → Phase0 게이트 → A(아키텍처) → B(공통레이어·2FA) → C(M2~M5) → D(옥션·관람객·BI·CMS·관리자) → E(배포) 각 Phase QA 통과. +- **부분**: "옥션 견적서 화면만 다시" → Phase0 부분 재실행 → kintex-bidding-dev(+backend 계약) → qa. +- **에러**: G1 미승인 "M5 구현" → 실호출 제외·구조만, 승인 요청 에스컬레이션. ## 후속 작업 -description의 후속 키워드로 재실행/부분수정/업데이트를 트리거한다. Phase 0의 컨텍스트 확인이 초기/새/부분 실행을 판별한다. +description 후속 키워드로 재실행/부분수정 트리거. Phase 0 컨텍스트 확인이 초기/새/부분 판별. diff --git a/.gitignore b/.gitignore index a5a9b32..71dbcad 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,14 @@ Thumbs.db # Stitch export — 대용량 스크린샷 PNG 제외(로컬 참조 유지), code.html·DESIGN.md만 버전관리 stitch_kintex_ai_system_architect/*/screen.png + +# Java / Gradle build 산출물 +build/ +.gradle/ + +# Node / Vite (프론트 스캐폴드 시) +node_modules/ +dist/ + +# 전시장 CAD 원본(대용량 DWG zip) — 로컬 보존, git 제외 +docs/assets/floorplans/cad/ diff --git a/CLAUDE.md b/CLAUDE.md index d99f20d..aa6ab3f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -65,15 +65,18 @@ GUARDiA 표준 프레임워크(Spring Boot 3.5 + React 19 + MyBatis + PostgreSQL ## 하네스: 구현 (kintex-impl-orchestrator) -**목표:** PLANNING v1.2의 M1~M9를 React(Vite) + Spring Boot 3.x(Java17) + MyBatis + PostgreSQL(PostGIS) + Redis + 나노바나나 Python 워커로 P0(M2·M3·M4·M5)부터 구현. +**목표:** 킨텍스 **자동전시시스템**(Exhibition Automation Platform) — PLANNING v2.0(부스 코어 M2~M5 + 도메인 M10~M18 + WISE/UIWS 공통·시스템관리 레이어 §5B, 6역할 웹/모바일 분리)을 React(Vite)+Spring Boot 3.x(Java17)+MyBatis+PostgreSQL(PostGIS)+Redis+나노바나나 Python 워커로 구현. AI=Claude 기본+설정형 전환(AiTextRouter/AiConfig). 공통 프레임워크 레퍼런스=WISE(UIWS `workspace/uiws`). -**트리거:** 킨텍스 구현·부스 배치/설계/배선/시각화 구현·백엔드/프론트/스키마 구현·Stitch 화면 이식·src 구현·배포·다시 실행·특정 모듈만 요청 시 `kintex-impl-orchestrator` 스킬을 사용하라. 단순 질문은 직접 응답 가능. +**트리거:** 킨텍스 구현·부스 배치/설계/배선/시각화·옥션/입찰/견적서·관람객/등록/배지/리드·경영분석/BI·CMS/공개 홍보 사이트·관리자 백오피스·공통기능/시스템관리/2FA·UIWS/WISE 이식·역할별 웹/모바일·AI(Claude)·아키텍처(AA/SA/TA/DA/NA)·src 구현·배포·다시 실행·특정 모듈만 요청 시 `kintex-impl-orchestrator` 스킬을 사용하라. -**에이전트:** kintex-backend-dev · kintex-frontend-dev · kintex-db-engineer · visualizer(나노바나나 워커) · kintex-qa · kintex-devops-dev + designer · planner · reviewer +**에이전트(전문 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 **선행 게이트:** G1 나노바나나(Gemini) 외부 호출 승인(PLANNING R12) · G2 배포 대상 서버·포트(GUARDiA 인프라와 별개 도메인). +**확보 자산:** `docs/assets/floorplans/` — 홀별 평면도 JPG 15장 + CAD(제1전시장 "평면,트렌치.dwg" 포함) → PLANNING R4(트렌치·CAD) 해소. 평면도 입력 포맷 = **CAD(DWG) + JPG**. CAD zip은 gitignore(로컬 보존). + **변경 이력:** | 날짜 | 변경 내용 | 대상 | 사유 | |------|----------|------|------| -| 2026-07-11 | 구현 하네스 초기 구성 — 전문 에이전트 6종 + kintex-impl-orchestrator + 구현 백로그 | 전체 | 범용 에이전트만 존재 → 스택(React/Spring/MyBatis/PostgreSQL) 확정 후 실제 구현 조율 팀 구성. developer 스택 갱신·designer에 Stitch 학습 트랙 추가 | +| 2026-07-11 | 구현 하네스 초기 구성 — 전문 에이전트 6종 + kintex-impl-orchestrator + 구현 백로그 | 전체 | 범용 에이전트만 존재 → 스택 확정 후 실제 구현 조율 팀 구성 | +| 2026-07-11 | **v2.0 재구성** — 자동전시시스템 확장(PLANNING v2.0). 신규 에이전트 11종(아키텍트 5·공통 1·도메인 5·AI 1) + 오케스트레이터·백로그 v2.0(Phase A 아키텍처→B 공통레이어→C 부스코어→D 도메인→E 배포) | 전체 | 스코프 확장(전시 전기능·경영분석·CMS·관리자·역할분리·공사 옥션·WISE 공통레이어·AI Claude 전환) | diff --git a/_workspace/01_backend_contracts.md b/_workspace/01_backend_contracts.md new file mode 100644 index 0000000..63bf904 --- /dev/null +++ b/_workspace/01_backend_contracts.md @@ -0,0 +1,325 @@ +# 킨텍스 AI 전시관리 — P0 백엔드 API 계약 (D-3) + +> 작성: kintex-backend-dev · 근거: `docs/PLANNING.md` v1.2(§5 M2~M5·§7 ERD·§8 아키텍처)·`docs/design.md` v1.0(SCR-01~12)·`docs/IMPLEMENTATION_BACKLOG.md`(S-1·D-3·C-1) +> 스택: Spring Boot 3.x(Java 17) + MyBatis + PostgreSQL(PostGIS) + Redis + WebSocket(STOMP). 패키지 `com.zioinfo.kintex`. +> 이 문서는 **frontend·db-engineer·qa 대조용 단일 계약**이다. 스키마 필요 매퍼는 §8에 목록화(db-engineer 인수). + +--- + +## 0. 공통 규약 + +### 0-1. 응답 봉투 (`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-2. 오류 코드 → 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 | 서버 오류(요약만) | + +### 0-3. 보안 불변 (계약 강제) +- **스택트레이스·내부 세부 미노출** — 응답 `error.message`는 사람이 읽을 요약만. 상세는 서버 로그. +- **ServerOut류 민감정보(IP·SSH·비밀번호·os_pw_enc·비밀번호 해시·내부 식별자) 응답 완전 제외.** 사용자/업체 표시는 이름·역할·번호 등 비민감 필드만. +- `GEMINI_API_KEY`는 백엔드에서 다루지 않는다(나노바나나 Python 워커 전용). M5는 큐 발행까지만. +- **AI 생성 이미지**는 응답에 `watermarkRequired:true` + `watermarkText` + `notice`(계약·심사 서류 사용 금지)를 **항상** 포함(제거 불가 — PLANNING §6-5). + +### 0-4. 인증 헤더 +- `Authorization: Bearer ` (HS256). JWT는 `sub`(userId)·`name`·`roles`(eventId→역할)·`hm`(홀매니저) 클레임. +- 공개 경로(인증 불필요): `GET /health`, `POST /api/auth/login`, `/ws/**`, `POST /api/internal/render/callback`(워커 토큰 인증). + +### 0-5. 행사 단위 RBAC (역할) +`ORGANIZER`(주최자·owner) · `EXHIBITOR`(참가업체·부스 멤버) · `CONTRACTOR`(장치업체) · `HALL_MANAGER`(킨텍스 내부·전 행사 열람+승인). +- 모든 도메인 경로는 `{eventId}` 스코프. 가드: 열람=행사 멤버 or 홀매니저 / 편집·액션=역할별(각 엔드포인트 명시). + +--- + +## 1. 헬스체크 + +### `GET /health` — 공개 +```json +{ "success": true, "data": { "status": "UP", "service": "kintex-backend", "time": "2026-07-11T…" }, "error": null } +``` + +--- + +## 2. 인증·워크스페이스 (C-1 / SCR-01) + +### `POST /api/auth/login` — 공개 +요청: +```json +{ "email": "pm@expo.co.kr", "password": "••••••" } +``` +응답 `LoginResponse`: +```json +{ + "accessToken": "", + "expiresInSeconds": 3600, + "user": { "userId": "u-1", "displayName": "김주최", "hallManager": false }, + "workspaces": [ + { "eventId": "e-2026-smf", "eventName": "2026 스마트팩토리 코리아", + "startDate": "2026-08-11", "endDate": "2026-08-14", + "hallLabel": "제2전시장 홀7", "myRole": "ORGANIZER", "dday": 31 } + ] +} +``` +- 실패: `UNAUTHORIZED`(이메일/비밀번호 불일치, 메시지 일반화). **비밀번호 해시 등 미노출.** + +### `GET /api/auth/workspaces` — 인증 +응답: `WorkspaceDto[]` (위 workspaces 배열과 동일 shape). + +### `GET /api/auth/me` — 인증 +응답 `KintexPrincipal`: `{ "userId","displayName","eventRoles":{"e-…":"ORGANIZER"},"hallManager":false }`. + +### `POST /api/auth/accept-invite` — 인증 +요청: +```json +{ "inviteCode": "INV-8F2K", "companyRegistrationNo": "123-45-67890" } +``` +- 장치업체(CONTRACTOR) 초대 수락 시 `companyRegistrationNo`로 **킨텍스 등록업체 검증** — 미등록이면 `NOT_REGISTERED_COMPANY`(403). +- 성공: 역할이 추가된 새 `LoginResponse`(신규 토큰 포함) 반환. + +> 스켈레톤 현황: `login`·`workspaces`·`accept-invite`는 `UserMapper`(§8) 대기로 **501**. JWT 발급/검증·RBAC 가드·`/me`는 완성. + +--- + +## 3. M2 플로어플랜 스튜디오 (SCR-03/04) — **P0** + +베이스: `/api/events/{eventId}/halls/{hallId}/layout` + +### `GET …/layout?version={n}` — 열람(멤버/홀매니저) +응답 `LayoutDto`: +```json +{ + "layoutId": "lay-1", "eventId": "e-…", "hallId": "H7", "version": 3, + "name": "배치안 B", "status": "draft", + "booths": [ + { "boothId": "b-102", "boothNo": "A-102", "type": "independent", + "polygon": [[0,0],[6,0],[6,3],[0,3],[0,0]], "sizeM": [6,3], + "heightM": 4.2, "floorLoadTPerM2": 3.0, "assignedCompanyName": "(주)한빛로보틱스", "premium": false } + ], + "summary": { "boothCount": 486, "targetBoothCount": 510, "salesAreaM2": 4374.0, + "minAisleWidthM": 3.2, "violationBlock": 0, "violationWarn": 3 }, + "updatedAt": "2026-07-11T…" +} +``` +- `polygon`: 홀 로컬 좌표계(미터), 닫힌 링. 서버가 PostGIS polygon으로 저장/검증. + +### `PUT …/layout` — 편집(ORGANIZER) +요청 `LayoutSaveRequest`: `{ "name":"배치안 B", "version":3, "booths":[ BoothDto… ] }` (`booths` 비어있지 않음). +응답: 저장된 `LayoutDto`(저장 시 규정 검증이 `summary`에 반영). + +### `POST …/layout/validate?version={n}` — 규정검증(ORGANIZER/HALL_MANAGER) +응답 `ComplianceReport`: +```json +{ + "rulesetVersion": "compliance-v1.0", + "disclaimer": "본 룰셋은 사전 필터이며 최종 승인은 킨텍스 및 구조기술사의 판단에 따른다.", + "blockCount": 2, "warnCount": 3, "passCount": 6, "submittable": false, + "violations": [ + { "pin": 1, "code": "AISLE_WIDTH_MIN", "group": "egress", + "label": "피난 통로 폭 최소 3m", "severity": "block", "measured": "측정 2.8m" } + ] +} +``` +- `submittable=false`(차단 존재) → 프론트 제출 비활성(SCR-03/09). `pin`은 도면 하이라이트 번호와 1:1. +- 검증 항목(M2): `AISLE_WIDTH_MIN`(통로 폭·block)·`EXIT_ACCESS`(비상구·block)·`FLOOR_LOAD`(홀별 하중·block)·`HEIGHT_MAX`(block)·`MEZZANINE_RATIO`(block)·`CLEARANCE_WALL`(warn). + +### `POST …/layout/auto-generate` — AI 자동배치(ORGANIZER) +요청 `AutoLayoutRequest`: +```json +{ "targetBoothCount":510, "premiumRatio":0.15, "stageCount":1, "loungeCount":1, "mainEntranceCount":2, "optionCount":3 } +``` +응답 `AutoLayoutOption[]`: +```json +[ { "optionId":"opt-A", "label":"배치안 A", + "summary": { "boothCount":510, "targetBoothCount":510, "salesAreaM2":4420.0, "minAisleWidthM":3.2, "violationBlock":0, "violationWarn":0 }, + "s7RenderJobId":"job-…" } ] +``` +- `s7RenderJobId`: S7 홀 전경(조감) 생성 잡 — 완료 시 WebSocket `/topic/render/{jobId}` 푸시로 SCR-04 카드 이미지 교체. + +> 스켈레톤 현황: `getLayout`·`saveLayout`·`validate`·`autoGenerate`는 `BoothMapper`(§8, PostGIS) 대기로 **501**. `ComplianceRuleEngine`(규정 평가기)·룰셋 로딩은 완성. + +--- + +## 4. M3 부스 설계 스튜디오 (SCR-06/09) — **P0** + +베이스: `/api/events/{eventId}/booths/{boothId}/design` + +### `GET …/design?version={n}` — 열람(멤버/홀매니저) +응답 `DesignPlanDto`: +```json +{ "designId":"d-1","boothId":"b-102","version":2,"status":"draft", + "spec": DesignSpec, "updatedAt":"2026-07-11T…" } +``` + +### `PUT …/design` — 편집(EXHIBITOR/CONTRACTOR) +요청 `DesignSaveRequest`: `{ "version":2, "spec": DesignSpec }`. 응답: `DesignPlanDto`. + +### `POST …/design/precheck` — 규정 사전검증(EXHIBITOR/CONTRACTOR/HALL_MANAGER) ✅완성 +요청 `DesignSpec`: +```json +{ + "boothType": "independent", "industry": "로봇/제조", "budget": 30000000, + "zones": [ { "type":"demo","ratioPercent":40 }, { "type":"consult","ratioPercent":30 } ], + "wallHeightM": 4.2, "signageText": "한빛로보틱스", + "rigging": { "use": true, "heightM": 7.0 }, + "mezzanineAreaRatio": 0.3, + "materials": [ { "part":"wall","finish":"matte_white","fireRetardant": true } ], + "lightingMode": "night", "usesDesignatedLightingOnly": true +} +``` +응답: `ComplianceReport`(§3-validate와 동일 shape). M3 평가 항목: `HEIGHT_MAX`(5m·block)·`RIGGING_RANGE`(6.5~8.5m·warn·`requiresDocument:"STRUCTURAL_CALC_D7"`)·`MEZZANINE_RATIO`(block)·`FIRE_RETARDANT`(block)·`CLEARANCE_CEILING`(warn)·`LIGHTING_BRING_IN`(warn). +- `wallHeightM > 5` → `HEIGHT_MAX` 차단, `measured:"측정 5.4m"`. +- 리깅 사용 시 D-7 구조계산서 부속 플래그(`requiresDocument`)를 SCR-09 칩으로 표시. + +> 스켈레톤 현황: `precheck` **완성**(스펙→룰 엔진 즉시 평가). `getDesign`·`saveDesign`은 `DesignMapper`(§8) 대기로 **501**. + +--- + +## 5. M4 유틸리티 설계 (SCR-07/08) — **P0** + +베이스: `/api/events/{eventId}/booths/{boothId}/utility` + +### `POST …/utility/quote` — 자동 견적(멤버) ✅완성 +요청 `UtilityQuoteRequest`: +```json +{ + "devices": [ { "name":"로봇 시연장비","count":2,"powerKw":1.5 }, + { "name":"LED 스포트","count":6,"powerKw":0.3 } ], + "networkWiredLines": 1, "plumbingOutlets": 1, "compressedAirOutlets": 0 +} +``` +응답 `UtilityQuote`: +```json +{ + "totalPowerKw": 4.8, "requestedKw": 5, "distributionBox50A": 1, + "lines": [ + { "label":"전기 220V 단상 5kW","qty":5,"unitPrice":55000,"amount":275000 }, + { "label":"분전반 50A 추가","qty":1,"unitPrice":100000,"amount":100000 }, + { "label":"인터넷 유선 회선","qty":1,"unitPrice":150000,"amount":150000 }, + { "label":"급배수 구","qty":1,"unitPrice":150000,"amount":150000 } + ], + "total": 675000, "currency":"KRW", "rulesetVersion":"rates-v1.0", + "disclaimer":"공시가 기준이며 최종 금액은 킨텍스 확정 시 안내됩니다." +} +``` +- 산식: `requestedKw = ceil(Σ count×powerKw)`, `distributionBox50A = ceil(requestedKw / 11)`. 단가는 요율 룰셋(`rates-v1.0`). + +### `POST …/utility/wiring?hallId={h}` — 배선 산출(멤버) +요청 `WiringRequest`: +```json +{ "terminals": [ { "kind":"power","position":[3.0,1.5],"kw":5 }, + { "kind":"network","position":[1.0,2.0] } ] } +``` +응답 `WiringResult`: +```json +{ "assumedTrench": true, + "paths": [ { "kind":"power","color":"red","coords":[[1,1],[3,1.5]],"lengthM":2.06,"label":"5kW" }, + { "kind":"network","color":"blue","coords":[[0,2],[1,2]],"lengthM":1.0,"label":"유선 1회선" } ] } +``` +- `assumedTrench=true` → SCR-07 "가정 트렌치 좌표(실측 대기)" 배지(PLANNING R4). 색상 규약: power=red·network=blue·plumbing/air=green(S6 동일). + +### `POST …/utility/order` — 신청 제출(EXHIBITOR/CONTRACTOR) +요청 `UtilityQuoteRequest`(견적과 동일). 응답 `UtilityOrderDto`: +```json +{ "orderId":"uo-…","status":"submitted","quote": UtilityQuote, + "locationDiagramUrl":"/files/…/location-diagram.pdf", + "supplyTiming":"장치 마지막 날 오후", + "deadlineNotice":"유틸리티 신청 마감 D-25 · 인터넷은 현장 추가신청 불가", + "relayNotice":"주최자 사무국/킨텍스에 제출 파일이 릴레이됩니다." } +``` +- `locationDiagramUrl`: 자동 생성 위치표시도(수기 작도 대체 서식, M4-3). 마감·릴레이 고지 포함(PLANNING R3). + +### `GET …/utility` — 신청 조회(멤버). 응답: `UtilityOrderDto`. + +> 스켈레톤 현황: `quote` **완성**(요율 룰셋 산술). `computeWiring`(`WiringMapper` PostGIS)·`submitOrder`(위치표시도 PDF·영속)·`getOrder`는 **501**. + +--- + +## 6. M5 나노바나나 시각화 — RenderJob (SCR-06/12) — **P0 · G1 게이트** + +> ★ Gemini 외부 호출은 소유자 승인(G1) 대상. 백엔드는 **큐 발행·상태·콜백까지만**(실호출은 Python 워커). 미승인 시에도 큐잉/상태는 동작. + +### `POST /api/events/{eventId}/booths/{boothId}/render` — 발행(멤버) ✅완성(큐잉) +요청 `RenderJobRequest`: +```json +{ "shotPreset":"S1", + "scene": { "hall":{"id":"H7","dims_m":[126,90],"ceiling_m":12}, + "booth":{"id":"A-102","size_m":[6,3],"type":"independent"}, + "design":{"signage":{"text":"한빛로보틱스"}}, + "lighting":{"mode":"day"} }, + "referenceImageUrl":"/files/empty_booth.jpg" } +``` +응답 `RenderJobDto`(QUEUED): +```json +{ "jobId":"","boothId":"b-102","shotPreset":"S1","status":"QUEUED", + "imageUrl":null,"schemaHash":null,"modelVersion":null, + "watermarkRequired": true, + "watermarkText":"AI 생성 예상 이미지 — 실제 시공 결과와 다를 수 있습니다", + "notice":"AI 생성 이미지는 계약·심사 서류에 사용할 수 없습니다 — 시공 기준은 도면입니다", + "errorMessage":null,"createdAt":"2026-07-11T…" } +``` +- 쿼터 초과 시 `RENDER_QUOTA_EXCEEDED`(429). `scene`은 PLANNING §6-2 스키마 그대로 Redis 큐(`kintex:renderjob:queue`)에 적재 → 워커 소비. +- S6(배선 오버레이)는 워커의 백엔드 래스터 합성 경로(생성형 아님). + +### `GET /api/events/{eventId}/render-jobs/{jobId}` — 상태(멤버) ✅완성 +응답: `RenderJobDto`(status QUEUED|RUNNING|DONE|FAILED). DONE 시 `imageUrl` 채워짐. 없으면 `NOT_FOUND`. + +### `GET /api/events/{eventId}/booths/{boothId}/render-jobs?shot={S1}` — 갤러리 목록(멤버) +응답: `RenderJobDto[]`(SCR-12). > 스켈레톤: `RenderJobMapper`(내구 이력) 대기로 **501**. + +### `POST /api/internal/render/callback` — 워커 콜백(내부·공유 시크릿) ✅완성 +헤더: `X-Worker-Token: ` (env). 요청 `WorkerCallbackRequest`: +```json +{ "jobId":"","status":"DONE","imageUrl":"/files/…/S1.png", + "schemaHash":"…","modelVersion":"gemini-3.1-flash-image-preview","errorMessage":null } +``` +- 처리: 상태 갱신 → WebSocket `/topic/render/{jobId}` 로 `RenderJobDto` 푸시. 성공 시에만 쿼터 차감(내구화는 매퍼). +- 실패(`status:"FAILED"`) 시 `errorMessage`는 요약만 통과(스택트레이스 유입 차단). + +### WebSocket (STOMP) +- 핸드셰이크: `GET /ws`(SockJS). 브로드캐스트 prefix `/topic`, 클라→서버 `/app`. +- 구독: `/topic/render/{jobId}` → RenderJob 완료·실패 이벤트(design.md §1-4). (승인 이벤트 토픽은 M6/C-4에서 확장.) + +--- + +## 7. 룰셋(버전 관리 데이터) 계약 + +- **규정** `rulesets/compliance-v1.json`(`compliance-v1.0`): 코드가 아닌 데이터. 개정 시 파일 교체. 리포트에 `rulesetVersion`·`disclaimer` 항상 기록(감사·면책, PLANNING R2). +- **요율** `rulesets/rates-v1.json`(`rates-v1.0`): 임대·유틸리티 단가. `quote` 응답의 `rulesetVersion` 근거. +- 연산자: `lte·gte·between·isTrue·eq·lteHall(홀별 상한)·excludesAll(금지목록)`. 측정값 없는 규칙은 미평가(리포트 카운트 제외). + +--- + +## 8. DB 매퍼 인수 목록 (→ kintex-db-engineer) + +> 아래 매퍼 인터페이스는 정의·주입 완료, **XML(PostGIS ST_*) 미구현 → 해당 API 501**. `resources/mybatis/mapper/` 에 구현. + +| 매퍼 | 메서드 | 공간/쿼리 요지 | +|---|---|---| +| `auth.mapper.UserMapper` | `findAuthByEmail`, `findEventRoles` | 사용자 인증행(해시 응답 제외)·행사별 역할. `login`/`workspaces`/`accept-invite` | +| `module.m2.mapper.BoothMapper` | `findLayout`, `findBooths`, `upsertLayout`, `replaceBooths`, `sumSalesArea`, `minAisleWidth`, `countExitsBlocked` | 부스 POLYGON(ST_MakePolygon/ST_AsGeoJSON), 판매면적 ST_Area, 통로폭 ST_Distance/ST_Buffer, 비상구 ST_Intersects | +| `module.m3.mapper.DesignMapper` | `findDesign`, `upsertDesign`, `findEventIdByBooth` | DesignPlan 버전(spec jsonb), 부스→행사 역참조(RBAC) | +| `module.m4.mapper.WiringMapper` | `findNearestTrenches`, `shortestPath`, `isAssumedTrench` | 트렌치 POINT KNN(`<->`), 배선 LINESTRING 최단(ST_Length), 가정 그리드 플래그 | +| `module.m5.mapper.RenderJobMapper` | `insertJob`, `updateStatus`, `findByBooth`, `countSucceededByEvent` | RenderJob 내구 이력·쿼터 정본(Redis는 큐/실시간) | + +### 필요 스키마(D-2 참조): `Event · User · EventMember · Hall · Trench · Booth(polygon) · Layout · DesignPlan · UtilityOrder(wiring LineString) · RenderJob · Company(등록업체)`. 홀 마스터·요율·규정 룰셋 시드는 D-2에서. + +--- + +## 9. 변경 이력 +| 버전 | 일자 | 내용 | +|---|---|---| +| v0.1 | 2026-07-11 | 최초 — 스캐폴드(S-1)와 함께 P0(M2·M3·M4·M5)+인증(C-1) 계약 정의. precheck·quote·render 큐잉·룰엔진 완성, 공간·영속 경로는 매퍼 대기(501). | diff --git a/docs/IMPLEMENTATION_BACKLOG.md b/docs/IMPLEMENTATION_BACKLOG.md index 382574d..d70952b 100644 --- a/docs/IMPLEMENTATION_BACKLOG.md +++ b/docs/IMPLEMENTATION_BACKLOG.md @@ -1,97 +1,66 @@ -# 킨텍스 AI 전시관리 시스템 — 구현 백로그 +# 킨텍스 자동전시시스템 — 구현 백로그 v2.0 -> 작성: 2026-07-11 · 근거: `docs/PLANNING.md` v1.2(M1~M9)·`docs/design.md`(SCR-01~M2)·`docs/analysis/reroomai-source.md`·Stitch 산출물(`stitch_kintex_ai_system_architect/`) -> 실행: `kintex-impl-orchestrator` 스킬(에이전트 팀). 담당 에이전트 약칭 — BE=kintex-backend-dev, FE=kintex-frontend-dev, DB=kintex-db-engineer, VIZ=visualizer(나노바나나 워커), QA=kintex-qa, DES=designer, OPS=kintex-devops-dev. -> 스택: React(Vite·TS) + Spring Boot 3.x(Java17)+MyBatis + PostgreSQL(PostGIS) + Redis + 나노바나나 Python 워커. +> 재작성: 2026-07-11 · 근거: `docs/PLANNING.md` v2.0(부스 코어 M2~M5 + 도메인 M10~M18 + 공통 레이어 §5B)·`docs/design.md` v1.1·`docs/analysis/*`·Stitch 산출물·평면도/CAD 자산(`docs/assets/floorplans/`) +> 실행: `kintex-impl-orchestrator`(에이전트 팀, model opus). 담당 약칭 — AA/SA/TA/DA/NA=아키텍트, COM=kintex-common-dev, BE=backend-dev, FE=frontend-dev, DB=db-engineer, VIZ=visualizer, AI=ai-dev, QA=kintex-qa, DEV=devops-dev, BID=bidding-dev, VIS=visitor-dev, CMS=cms-dev, BI=bi-dev, ADM=admin-dev, DES=designer. +> 스택: React(Vite·TS) + Spring Boot 3.x(Java17)+MyBatis + PostgreSQL(PostGIS) + Redis + 나노바나나 Python 워커. AI=Claude 기본+설정형 전환(AiTextRouter/AiConfig). 공통=WISE(UIWS) 이식. -## 선행 게이트 (착수 전 소유자 확인) -- **G1 나노바나나(Gemini) 외부 호출 승인**(PLANNING R12) — 미승인 시 M5는 코드/구조만, 실호출·배포 보류. -- **G2 배포 대상 서버·포트** — kintex는 GUARDiA 관제 인프라와 별개 도메인. 서버 재사용/포트/도메인 확정 필요(Phase 5 전). +## 선행 게이트 (소유자 확인) +- **G1** 나노바나나(Gemini) 외부 호출 승인(PLANNING R12) — M5 실호출·배포 전. 미승인 시 목/degraded. +- **G2** 배포 대상 서버·포트(GUARDiA 인프라와 별개 도메인) — Phase E 전. -## Phase 0 — 프로젝트 스캐폴딩 (P0 선행) -| ID | 작업 | 담당 | 의존 | 완료 기준 | -|---|---|---|---|---| -| S-1 | `src/backend` Spring Boot 3.x(Java17)+MyBatis+PostGIS 스캐폴드, `com.zioinfo.kintex` | BE | — | `./gradlew compileJava` 통과 | -| S-2 | `src/frontend` React 18/19+Vite+TS 스캐폴드 + design.md 토큰(팔레트·타이포) CSS 변수화 | FE | 설계정합 | `npm run build` 통과 | -| S-3 | `kintex_db`(PostGIS) 마이그레이션 도구(Flyway) + 공통 엔티티 DDL v1 | DB | — | 마이그레이션 적용·롤백 검증 | -| S-4 | 나노바나나 워커 서비스 골격(Redis 큐 소비 루프, `tools/nanobanana` 재사용) | VIZ | — | `py_compile` + 큐 소비 스텁 기동 | +## 확보 자산 +- 평면도 15장 + CAD(제1전시장 "평면,트렌치.dwg" 포함) `docs/assets/floorplans/` → **R4(트렌치·CAD) 해소**. CAD zip은 gitignore(로컬 보존), db-engineer가 트렌치 좌표 추출에 사용. 사용자 첨부 평면도는 `.../provided/`. -## Phase 1 — 설계 정합 -| ID | 작업 | 담당 | 의존 | 완료 기준 | -|---|---|---|---|---| -| D-1 | Stitch 산출물 학습 → design.md 디자인 시스템 단일 출처 수렴 + SCR 매핑표 + 범위 게이트 | DES | — | design.md 갱신·매핑표·P2 표기 | -| D-2 | PostGIS 스키마 v1(Event/Hall/Booth polygon/DesignPlan/UtilityOrder LineString/RenderJob/Document/Company/Payment) + 마스터 시드(홀·요율·규정 룰셋) | DB | S-3 | `_workspace/01_db_schema.md` + 시드 멱등 | -| D-3 | P0 API 계약 초안(OpenAPI 수준) | BE | D-2 | `_workspace/01_backend_contracts.md` | -| D-4 | 정합 교차 검증(기획-디자인-스키마-계약) | reviewer | D-1..D-3 | 불일치 0 또는 티켓화 | +--- -## Phase 2 — P0 구현 (핵심) - -### M2 플로어플랜 스튜디오 (부스 배치 자동화) -| ID | 작업 | 담당 | 의존 | 완료 기준 | -|---|---|---|---|---| -| M2-1 | Booth polygon·Hall·Trench 스키마·매퍼(ST_Area/ST_Buffer/ST_Intersects) | DB | D-2 | 공간 쿼리 단위 검증 | -| M2-2 | 배치 저장·조회 API + 규정 검증(통로 폭·바닥하중·비상구·복층) PostGIS 서비스 | BE | M2-1 | 위반 플래그(차단/경고) 응답 | -| M2-3 | 배치 자동생성(제약 솔버/휴리스틱) — 복수 배치안 | BE | M2-2 | 조건 입력→복수 안 | -| M2-4 | SCR-03 부스 배치 에디터(캔버스 다크서피스·드래그·트렌치·위반 오버레이) — Stitch `booth_layout_editor` 이식 | FE | M2-2 | 드래그·위반핀↔목록 동기 | -| M2-5 | SCR-04 배치안 비교(S7 조감 카드) — Stitch `layout_comparison` 이식 | FE | M2-3, M5-1 | 지표 비교·선택 | -| M2-6 | 점진 QA(경계면·공간 로직·규정 룰셋 정합) | QA | M2-* | 통과까지 반려 | - -### M3 부스 설계 스튜디오 -| ID | 작업 | 담당 | 의존 | 완료 기준 | -|---|---|---|---|---| -| M3-1 | 부스 설계(조립 옵션/독립 초안)·규정 사전검증(높이5m·리깅D-7·방염·복층) API | BE | D-2 | 위반 사전 플래깅 | -| M3-2 | SCR-06 설계 스튜디오(스펙 폼·4샷 그리드·Before/After) — Stitch `booth_design_studio` 이식 | FE | M3-1, M5-1 | 컨펌 흐름·"시공기준=도면" 동의 | -| M3-3 | SCR-09 규정 검증 리포트 — Stitch `compliance_report` 이식 | FE | M3-1 | 차단 시 제출 비활성·면책·룰셋 버전 | -| M3-4 | 점진 QA | QA | M3-* | 통과 | - -### M4 유틸리티 설계 (전기·조명 / 네트워크·급배수) -| ID | 작업 | 담당 | 의존 | 완료 기준 | -|---|---|---|---|---| -| M4-1 | Wiring LineString 스키마·최단 배선(ST_Distance/트렌치 라우팅) 매퍼 | DB | D-2 | 최단 경로 쿼리 | -| M4-2 | 전기 용량·분전반 산출 + 네트워크/급배수 배선 + 자동 견적 API | BE | M4-1 | kW 합산·요금 정합 | -| M4-3 | 위치표시도 자동 생성(수기 작도 대체 서식) | BE | M4-2 | PDF/이미지 산출 | -| M4-4 | SCR-07 배선 뷰(트렌치 오버레이·견적 패널) — Stitch `utility_wiring_view` 이식 | FE | M4-2 | 단말 드래그→경로 재계산 | -| M4-5 | SCR-08 신청 요약·위치표시도 — Stitch `utility_order_summary` 이식 | FE | M4-3 | 마감(D-25) 고지·제출 | -| M4-6 | 점진 QA(견적 산술·"현장 추가불가" 고지·좌표 정합) | QA | M4-* | 통과 | - -### M5 나노바나나 시각화 (★G1 게이트) -| ID | 작업 | 담당 | 의존 | 완료 기준 | -|---|---|---|---|---| -| M5-1 | RenderJob 발행/상태 API + Redis 큐 + WebSocket 완료 푸시 | BE | D-2 | 큐잉·상태 전이 | -| M5-2 | 나노바나나 워커: PLANNING §6-2 scene→build_booth_prompt→생성, 메타데이터 임베드·워터마크·스키마 해시 캐시(`tools/nanobanana` 재사용) | VIZ | M5-1, S-4 | (G1 승인 시) 실 생성 / 미승인 시 목·폴백 | -| M5-3 | S6 배선 오버레이 래스터 합성(생성형 아님, PIL 좌표 정합) | VIZ | M4-1, M5-2 | 도면 위 적/청/녹 오버레이 | -| M5-4 | SCR-12 시각화 갤러리(S1~S7·Before/After·워터마크 상시) — Stitch `visualization_gallery` 이식 | FE | M5-1 | 온디맨드 생성·재시도·고지바 | -| M5-5 | 점진 QA(워터마크 강제·서류 배제·캐시·쿼터) | QA | M5-* | 통과 | - -### 공통 화면 (P0 대시보드·인증) -| ID | 작업 | 담당 | 의존 | 완료 기준 | -|---|---|---|---|---| -| C-1 | 인증(행사 단위 RBAC+JWT) API + SCR-01 로그인/워크스페이스 — Stitch `login_workspace_selection` 이식 | BE, FE | S-1,S-2 | 미등록 장치업체 초대 차단 | -| C-2 | SCR-02 주최자 대시보드(마일스톤 타임라인·KPI) — Stitch `organizer_dashboard` 이식 | FE | C-1 | D-데이·KPI 렌더 | -| C-3 | SCR-05 참가업체 부스 홈 — Stitch `exhibitor_home` 이식 | FE | C-1 | 진행 스테퍼·마감 리마인더 | -| C-4 | SCR-10 홀매니저 승인 큐 / SCR-11 검수 상세 — Stitch `manager_approval_queue`·`review_details` 이식 | FE | M2-2,M3-1 | AI 사전심사 플래그·일괄승인 | -| C-5 | SCR-M1 현장 체크리스트 / SCR-M2 현장 검수(모바일) — Stitch `contractor_on_site_checklist`·`manager_field_inspection` 이식 | FE | C-1 | 오프라인·48px 터치 | - -## Phase 3 — P1 +## Phase A — 아키텍처·거버넌스 (아키텍트 팀 + reviewer) | ID | 작업 | 담당 | 완료 기준 | |---|---|---|---| -| P1-M1 | 홀 배정·자동 견적(요율 룰 엔진: 성수기·전시장 할증·예치금) | BE, FE, DB | 즉시 견적·배정신청서 생성 | -| P1-M6 | 마일스톤(D-150/30/25/7)·서식 자동생성·AI 서류 검수 | BE, FE | 마일스톤 역산·검수 리포트 | -| P1-M7 | 등록업체(739) 매칭·RFQ — 미등록 시공 차단 | BE, FE, DB | 추천·견적요청 | -| P1-M9 | 정산·결제(납부 스케줄·PG·예치금 정산) | BE, FE | 스케줄 알림·PG 연동 | +| A-1 | 애플리케이션 아키텍처(모듈 경계·레이어·API 표준·패키지) | AA | `docs/architecture/app.md` | +| A-2 | 시스템 아키텍처·NFR(확장성·HA·성능·보안영역·배포 토폴로지) | SA | `docs/architecture/system.md` | +| A-3 | 기술 표준(스택·빌드/배포·개발표준·관측성·AiTextRouter) | TA | `docs/architecture/tech.md` | +| A-4 | 데이터 아키텍처(전사 ERD·공간데이터·마스터·공통코드·거버넌스·BI 마트) | DA | `docs/architecture/data.md`+ERD | +| A-5 | 네트워크 아키텍처(DMZ/내부망 분리·방화벽·부하분산·외부 아웃바운드) | NA | `docs/architecture/network.md` | +| A-6 | 아키텍처↔PLANNING 정합 검증 | reviewer | 불일치 0/티켓화 | -## Phase 4 — P2 -| ID | 작업 | 담당 | 비고 | -|---|---|---|---| -| P2-M8 | 반입/반출 물류 슬롯·QR 통행증 | BE, FE | 운영팀 협업 전제 | -| P2-X | Stitch 범위밖 화면(BI·hall ops·exhibition schedule·admin) | DES→planner | 기획 반영 후에만 P 승격 | - -## Phase 5 — 빌드·배포 (G2 게이트) +## Phase B — 공통/시스템관리 레이어 (★전 모듈 선행, PLANNING §5B, WISE/UIWS 이식) | ID | 작업 | 담당 | 완료 기준 | |---|---|---|---| -| DEP-1 | Spring jar + React 번들(+워커 서비스) 빌드 파이프라인 | OPS | 로컬/서버 빌드 통과 | -| DEP-2 | Gitea `zio/kintex` CI/CD·webhook·systemd·env(GEMINI_API_KEY 서버 env) | OPS | (G2 확정 후) push→배포 | +| B-0 | 스캐폴드: `src/backend`(Spring Boot 3.x·`com.zioinfo.kintex`)·`src/frontend`(Vite·TS)·`kintex_db`(PostGIS·Flyway) | BE·FE·DB | compileJava·build·마이그레이션 통과 | +| B-1 | 인증: JWT+RBAC(6역할) + **2차 인증 OTP(TOTP RFC6238)** + 로그인 실패 잠금 + admin 비번 env(AES-256-GCM, admin123 금지) | COM·BE | 2FA 등록/검증/초기화 왕복 | +| B-2 | 시스템관리: 사용자·역할/권한·공통코드·메뉴·감사로그·시스템설정(→ M18 통합) | COM·ADM·DB | CRUD·RBAC 가드·감사 기록 | +| B-3 | 공통 업무기능: worklog·schedule·message·stats·notice·opinion·search·meeting·report·notification·audit (WISE 이식) | COM·DB | 각 모듈 진입·API 왕복 | +| B-4 | 공통 컴포넌트(예외·응답봉투·감사 AOP·알림 단일화) + 프론트 공통(2FA 화면·공통코드·검색바·그리드·달력·모달) | COM·FE | AA 표준 정합 | +| B-5 | 점진 QA(2FA·RBAC·비번/PII 미노출·경계면) | QA | 통과까지 반려 | + +## Phase C — P0 부스 시공 코어 (M2·M3·M4·M5) +| ID | 모듈 | 작업 | 담당 | 완료 기준 | +|---|---|---|---|---| +| C-M2 | 플로어플랜 | Booth polygon·Trench(CAD 추출) 스키마·배치 저장/규정검증(PostGIS)·**AI 1·2·3안 생성→선택/병합**·SCR-03/04 이식 | DB·BE·AI·FE·QA | 3안 생성·병합·규정 재검증 | +| C-M3 | 부스 설계 | 조립/독립 설계·규정 사전검증(높이·리깅·방염)·3안 생성/병합·SCR-06/09 이식 | BE·AI·FE·QA | 위반 사전 플래깅·컨펌 | +| C-M4 | 유틸리티 배선 | Wiring LineString·전기/네트워크/급배수 배선·자동 견적·위치표시도·SCR-07/08 이식 | DB·BE·FE·QA | 최단 배선·견적·위치표시도 | +| C-M5 | 나노바나나 시각화 | RenderJob 발행/상태·Redis·WebSocket·워커 생성(G1)·S6 래스터·SCR-12·Before/After | BE·VIZ·FE·QA | (G1 시)실생성/미승인시 목·워터마크 강제 | +| C-C | 공통 화면 | SCR-01 로그인·SCR-02 주최자 대시보드·SCR-05 참가업체 홈·SCR-10/11 홀매니저·SCR-M1/M2 현장(모바일) 이식 | FE·QA | 역할별 진입·상태 처리 | + +## Phase D — P1 도메인 (역할별 포털 분리) +| ID | 모듈 | 작업 | 담당 | 완료 기준 | +|---|---|---|---|---| +| D-M15 | **공사/장치 옥션** | AI자료 패키지 열람→**견적서(Quotation) 제출**→역경매(라운드·순위)→전시업체 낙찰/확정→계약/발주. 등록업체만 응찰(M7 검증). `Auction 1─N Quotation ─ Award`·견적서 PDF/버전 | BID·BE·DB·FE·QA | 견적서 제출·역경매·낙찰 왕복 | +| D-M10 | **관람객·현장** | 등록·티켓(PG)·배지/QR·현장 체크인·리드캡처·관람객 웹/모바일 | VIS·BE·DB·FE·QA | 등록→배지→체크인→리드 | +| D-M12/17 | **마케팅·공개사이트·CMS** | 헤드리스 CMS·참가업체 마이크로사이트·**일반 대중 공개 홍보 사이트(SEO·다국어)**·배너/프로모션·EDM | CMS·BE·FE·QA | 공개 조회·게시 워크플로·SEO | +| D-M16 | **경영분석 BI** | 매출·홀 가동률·리텐션·이벤트 P&L·수요예측·KPI 대시보드·부스 트래픽/ROI(Recharts) | BI·DA·BE·FE·QA | 지표 집계·드릴다운·내보내기 | +| D-M18 | **관리자 백오피스** | 시스템관리(B-2)+킨텍스 마스터데이터(홀·요율·규정 룰셋·등록업체)·관리 대시보드 | ADM·BE·FE·QA | RBAC·마스터 CRUD·룰셋 버전 | +| D-M1/6/7/9 | 배정·서류·매칭·정산 | 홀 배정·자동견적(요율 룰)·마일스톤/서식·등록업체 매칭·정산/결제 | BE·FE·DB·QA | 견적·마일스톤·정산 | + +## Phase E — P2 + 빌드·배포 +| ID | 작업 | 담당 | 완료 기준 | +|---|---|---|---| +| E-M11 | 비즈니스 매칭(관람객↔참가업체/바이어) | VIS·AI | 추천·미팅예약 | +| E-M13 | wayfinding/실내 내비(M2 배치 재사용) | VIS·FE | 부스 검색·경로 | +| E-M14 | 현장운영(혼잡·안전·주차·에너지) | BE·FE | 대시보드·경보 | +| E-DEP | 역할별 프론트 번들 + Spring jar + 워커 서비스 빌드·Gitea CI/CD·systemd·env | DEV | (G2 후)push→배포 | ## 진행 규칙 -- **각 모듈 완성 직후 QA 점진 검증**(전체 완성 후 1회 아님). 보안 불변(자격증명·스택트레이스 미노출·AI 워터마크 강제)은 반려 사유. -- 기획 변경은 planner, 디자인 변경은 designer 경유(직접 수정 금지). +- **Phase B(공통 레이어)는 전 도메인 선행 기반** — C/D 착수 전 인증·시스템관리·공통기능 정착. +- 각 모듈 완성 직후 QA 점진 검증. 보안 불변(자격증명·PII·스택트레이스 미노출·AI 워터마크·등록업체 응찰·admin env) 반려 사유. +- 기획=planner, 디자인=designer 경유. 아키텍처 표준(Phase A) 위반은 시정. diff --git a/docs/PLANNING.md b/docs/PLANNING.md index bf3801e..fadb0aa 100644 --- a/docs/PLANNING.md +++ b/docs/PLANNING.md @@ -1,8 +1,12 @@ -# 킨텍스 AI 전시관리 시스템 기획서 +# 킨텍스 자동전시시스템 (Exhibition Automation Platform) 기획서 -> 작성: 기획 에이전트(planner) · 작성일: 2026-07-11 · 버전: v1.1 +> 작성: 기획 에이전트(planner) · 작성일: 2026-07-11 · 버전: **v2.0** > 근거 문서: `docs/analysis/kintex-website.md` (킨텍스 웹사이트 25개 페이지 + 전시주최자매뉴얼 PDF + 참가업체 매뉴얼 PDF 전수 분석, 2026-07-11) > 근거 문서: `docs/analysis/reroomai-source.md` (ReRoomAI 이미지 파이프라인 소스 분석, 2026-07-11) — 6장 나노바나나 파이프라인 제어 파라미터 확정 근거 +> 근거(글로벌 벤치마크, 2026-07-11 크롤링): Eventleaf·VenueSight·Whova·ExpoPlatform(전시관리·부스판매·리드캡처), Eventbase·Pointr·Swapcard·Brella·Grip(wayfinding·비즈매칭), RainFocus·Bizzabo(등록·리벤뉴), ExhibitForce·Ways·FinancialModelsLab·Digitevent(전시 BI/KPI), FindRFP·Procore·4castplus(RFQ/Tender/역경매). 각 §5 모듈에 매핑. +> +> **v2.0 정체성 확장**: 본 시스템은 "부스 시공 도구"를 넘어 **킨텍스 자동전시시스템(Exhibition Automation Platform)** 이다 — 전시 기획·판매·시공·운영·관람객·사후분석 전 주기를 아우른다. 기존 M2~M5 부스 시공 코어(P0)·나노바나나 파이프라인(§6)·확정 스택(§8)은 **보존**하고, 그 위에 관람객/비즈매칭/마케팅·공개사이트/wayfinding/현장운영/공사 옥션/BI/CMS/관리자 모듈(M10~M18)을 확장한다. +> **문서 소유권**: 본 PLANNING.md는 planner만 수정한다. design.md·타 문서는 이 문서 변경 시 "designer/planner 후속 반영 필요"로만 표기하고 직접 수정하지 않는다. --- @@ -20,6 +24,21 @@ 기존 전시관리 솔루션(부스 배치 SW, 온라인 신청 포털)은 도면과 표를 보여준다. 본 시스템은 **주최자·참가업체가 도면을 읽지 못해도 의사결정할 수 있는 사진 수준의 시각화**를 제공한다. +### 1-2-1. v2.0 비전 재정의 — 킨텍스 자동전시시스템 + +부스 시공 시각화(P0)는 여전히 심장이지만, v2.0에서 본 시스템은 전시 생애주기 전체를 하나의 데이터·계정 체계로 자동화하는 **베뉴 운영 플랫폼(Venue Operating Platform)** 으로 확장된다. 글로벌 전시테크(ExpoPlatform·VenueSight·Swapcard·RainFocus 등)가 제공하는 기능을 킨텍스 도메인에 이식하되, **킨텍스만의 차별점 — 실측 공간 데이터(PostGIS) + 나노바나나 시공 예측 이미지 — 을 관통시켜** 다른 전시테크가 하지 못하는 "설계→시각화→발주(옥션)→시공→운영→분석"의 폐루프를 완성한다. + +| 생애주기 단계 | 담당 모듈 | v1.x 대비 | +|---|---|---| +| 판매·기획 | M1(홀 배정·견적), M16(BI 수요예측·수율) | M16 신규(BI 승격) | +| 설계·시각화 (**심장, 불변**) | M2 배치 · M3 부스설계 · M4 배선 · **M5 나노바나나** | 보존 | +| 발주·계약 | M6 서류, M7 등록업체, **M15 공사/장치 옥션(입찰)**, M9 정산 | M15 신규(옥션 핵심) | +| 참가·관람 | M10 관람객 등록·배지·리드캡처, M11 비즈매칭, M12 마케팅·공개사이트, M13 wayfinding | 전부 신규 | +| 현장 운영 | M8 물류, M14 현장운영(혼잡·안전·주차·에너지) | M14 신규 | +| 사후·경영 | M16 BI(이벤트 P&L·리텐션·KPI), M17 CMS, M18 관리자 | 전부 신규 | + +이 폐루프의 핵심 연결고리가 **M15 공사/장치 옥션**이다: M2~M5가 생성한 AI 설계·물량·시공 예측 이미지가 곧 **역경매(reverse auction) 응찰의 근거 자료**가 되어, 참가업체/주최자가 사진 수준 자료를 보고 시공 발주까지 한 흐름에서 종결한다. + ### 1-3. 목표 (정량) | 목표 | 현재 (As-Is) | 목표 (To-Be) | 근거 | @@ -46,9 +65,27 @@ | **참가업체** (Exhibitor) | 박참가, 중소 제조사 마케팅 담당. 전시 경험 2회 | 부스 선택(조립/독립), 유틸리티 신청(D-25), 반입/반출 | 전기 몇 kW 필요한지 모름, 위치표시도 수기 작성, 시공 결과를 개장일에 처음 봄 | 클릭 신청 + 자동 견적, **시공 전 부스 사진 확인**, 마감 리마인더 | | **장치·시공업체** (Contractor) | 이장치, 킨텍스 등록 장치업체(739개 등록업체 중 전시디자인설치 분류) 실장 | 평면도·입면도·조감도·전기도면 kxwp 제출, D-7 리깅 구조계산서, 현장 시공 | 규정(높이 5m, 방염, 리깅 6.5~8.5m) 반려 리스크, 도면 수정 반복 | 제출 전 자동 규정 검증, 배선/분전반 설계 자동 초안, 고객 컨펌용 예상 사진 | | **킨텍스 운영팀 (홀매니저)** | 최매니저, 행사지원팀. 홀 3개 담당 | 사전업무협의(D-30), 신고서류 검토, 현장 안전 관리(소음 70~75dB, 안전모) | 서류 육안 검수 병목, 행사별 협의 이력 분산, 위반 현장 사후 적발 | 위반 자동 플래깅 대시보드, 협의 이력 일원화, 홀 단위 유틸리티 부하 집계 | -| **관람객** (Visitor) — P2 | 정관람, 일반 관람객 | 일정 검색 → 교통(GTX-A 킨텍스역 도보 3분) → 주차(iparking) → 관람 | 부스 위치 탐색 불편 | 인터랙티브 플로어플랜·부스 검색 (배치 데이터 부산물) | +| **관람객** (Visitor) — P1로 승격 | 정관람, 일반 관람객/바이어 | 일정 검색 → 사전등록·티켓 → 교통(GTX-A 킨텍스역 도보 3분) → 주차(iparking) → 배지·체크인 → 관람·비즈매칭 | 부스 위치 탐색 불편, 현장 등록 대기 | 사전등록·모바일 배지/QR·wayfinding·비즈매칭·인터랙티브 플로어플랜 | +| **관리자** (Admin) — 신규 | 한관리, 킨텍스 플랫폼 운영 관리자 | 사용자·권한(RBAC)·마스터데이터(홀·요율·규정 룰셋)·감사로그·시스템설정 | 데이터 산재, 권한 통제 부재 | 별도 백오피스에서 전 테넌트/행사 통제, 감사 추적, 룰셋 버전 관리 | +| **일반 대중** (Public) — 신규 | 불특정 다수 잠재 관람객·잠재 주최자 | 킨텍스 전시 홍보 열람, 참가 문의 | 킨텍스 공식 사이트는 정보·서식 다운로드 수준 | SEO·다국어 공개 홍보 사이트(M12), 참가업체 마이크로사이트(M17), 관람 유도 | -**권한 모델**: 행사(Event) 단위 워크스페이스. 주최자가 owner, 참가업체는 부스 단위 멤버, 장치업체는 참가업체가 초대(등록업체 DB 검증 — 미등록 업체 초대 차단), 홀매니저는 킨텍스 내부 계정으로 전체 열람+승인 권한. +**권한 모델**: 행사(Event) 단위 워크스페이스 + 플랫폼 레벨 RBAC(관리자). 주최자가 행사 owner, 참가업체는 부스 단위 멤버, 장치/공사업체는 참가업체·주최자가 초대(등록업체 DB 검증 — 미등록 업체 초대·옥션 응찰 차단), 홀매니저는 킨텍스 내부 계정으로 전체 열람+승인, 관리자는 백오피스에서 크로스-테넌트 통제. 관람객·일반 대중은 공개/셀프서비스 계정(행사 데이터 쓰기 권한 없음, 등록·매칭·조회만). + +### 2-1. 역할별 웹/모바일 포털 분리 (IA·채널 매트릭스) + +각 역할은 목적이 다르므로 **별도 프론트 앱(도메인/서브패스 분리)**으로 제공하되, 공유 Spring Boot 백엔드 + SSO + 역할 RBAC 위에 얹는다. 데스크톱(설계·에디터·대시보드)과 모바일(현장·조회·승인)의 용도를 명확히 나눈다. + +| 역할 | 웹 포털 (데스크톱 주력) | 모바일 (현장/조회 주력) | 주 사용 모듈 | 인증/권한 | +|---|---|---|---|---| +| **주최자** | 주최자 콘솔 `organizer.` — 홀 배정·배치·행사 대시보드·옥션 발주·BI | 조회·승인·현장 상황판 | M1·M2·M6·M15·M16·M12 | Event owner | +| **참가업체** | 참가업체 포털 `exhibitor.` — 부스 신청·설계·유틸리티·옥션 발주 | 현장 체크인·**리드캡처(배지 스캔)**·승인 | M3·M4·M5·M10·M11·M15·M9 | Event member(부스 단위) | +| **장치/공사업체** | 업체 포털 `contractor.` — 도면 제출·**AI 설계자료 열람·옥션 응찰(견적서 제출)** | 현장 시공·반입 통행증·안전 체크 | M3·M4·M15·M8·M7 | 등록업체 검증 계정 | +| **킨텍스 직원(홀매니저·운영)** | 운영 대시보드 `ops.` — 검수·규정 플래그·홀 부하·물류·현장운영 | 현장 검수·안전·혼잡 모니터 | M2·M6·M8·M14·M16 | 내부 계정(행사 전체 열람·승인) | +| **관리자** | 백오피스 `admin.` — 사용자·RBAC·감사로그·마스터데이터·룰셋·시스템설정 | (없음, 웹 전용) | **M18** | 플랫폼 관리자(크로스-테넌트) | +| **관람객/일반 대중** | 공개 홍보 사이트 `www/expo.` (SEO·다국어) + 관람객 사전등록 | 관람객 앱 — 배지/QR·티켓·wayfinding·비즈매칭·플로어플랜 | M12·M10·M11·M13·M17 | 공개/셀프서비스(쓰기 제한) | + +- **분리 원칙**: 6개 프론트(organizer·exhibitor·contractor·ops·admin·public+visitor)는 **역할별 번들 분리**로 최소권한·공격면 축소. 공유 디자인 시스템(design.md)·공유 컴포넌트 라이브러리·공유 API 계약을 상속한다. (UI 상세·화면 목록은 designer 후속 반영 필요.) +- **모바일**: 참가업체 리드캡처, 홀매니저 현장 검수, 업체 반입 통행증, 관람객 배지/wayfinding은 모바일 전용 최적화. 단일 앱 내 역할 스위칭 또는 역할별 별도 배포는 designer/개발 트랙에서 결정(백오피스는 모바일 미제공). --- @@ -88,60 +125,111 @@ ```mermaid graph TB - subgraph 사용자 - ORG[주최자 웹] - EXH[참가업체 웹/모바일] - CON[장치업체 웹] - MGR[홀매니저 대시보드] + subgraph 포털["역할별 포털 (§2-1)"] + ORG[주최자 콘솔] + EXH[참가업체 포털] + CON[업체 포털] + MGR[운영 대시보드] + ADM[관리자 백오피스] + PUB[공개사이트/관람객앱] end - subgraph "킨텍스 AI 전시관리 시스템" - M1[M1 행사·홀 배정
가용성 조회·자동 견적] - M2[M2 플로어플랜 스튜디오
부스 배치 자동 생성·규정 검증] - M3[M3 부스 설계 스튜디오
인테리어·장치 설계 자동화] - M4[M4 유틸리티 설계
전기·조명 / 네트워크·급배수 배선] - M5[M5 나노바나나 시각화
시공 후 예상 사진 생성] - M6[M6 서류·마일스톤 워크플로
D-150~D-7 트래킹, AI 서류 검수] - M7[M7 등록업체 매칭·견적] - M8[M8 반입/반출 물류 슬롯] + subgraph 코어["설계·시각화 코어 (P0·불변)"] + M2[M2 플로어플랜 스튜디오
3안 생성·선택/병합·규정검증] + M3[M3 부스 설계 스튜디오
3안 생성·선택/병합] + M4[M4 유틸리티 설계
전기·조명/네트워크·급배수] + M5[M5 나노바나나
시공 후 예상 사진] + end + + subgraph 판매운영["판매·발주·정산"] + M1[M1 홀 배정·자동 견적] + M6[M6 서류·마일스톤] + M7[M7 등록업체 매칭] + M15[M15 공사/장치 옥션
역경매·견적서 응찰] + M8[M8 반입/반출 물류] M9[M9 정산·결제] end - subgraph 외부·기존 시스템 + subgraph 관람참가["관람·참가·마케팅"] + M10[M10 관람객 등록·배지
체크인·리드캡처] + M11[M11 비즈니스 매칭] + M12[M12 마케팅·EDM
공개 홍보 사이트] + M13[M13 wayfinding
실내 내비] + M14[M14 현장운영
혼잡·안전·주차·에너지] + end + + subgraph 경영["경영·콘텐츠·관리"] + M16[M16 경영분석 BI
매출·가동률·P&L·수요예측] + M17[M17 CMS
콘텐츠·마이크로사이트·다국어] + M18[M18 관리자 시스템
RBAC·감사·마스터데이터] + end + + subgraph 외부["외부·기존 시스템"] KXWP[kxwp/kxfp 작업신고] CCPY[등록업체 DB 739개] EVT[행사일정 시스템] PG[PG 결제] - GEM[Gemini API 나노바나나] + GEM[Gemini 나노바나나] + SGN[사이니지/wayfinding HW] end ORG --> M1 --> M2 ORG --> M6 - EXH --> M3 - EXH --> M4 + ORG --> M16 + EXH --> M3 --> M4 CON --> M3 - CON --> M4 - MGR --> M6 + CON --> M15 MGR --> M2 + MGR --> M14 + ADM --> M18 + PUB --> M12 + PUB --> M10 M2 --> M3 --> M4 - M2 --> M5 - M3 --> M5 - M4 --> M5 + M2 & M3 & M4 --> M5 M4 --> M9 M1 --> M9 M3 --> M7 - M8 --> MGR + M2 & M3 & M4 & M5 --> M15 + M15 --> M9 EXH --> M8 + M10 --> M11 + M10 --> M14 + M2 --> M13 + M9 --> M16 + M10 --> M16 + M12 --> M17 + M18 -.룰셋·마스터.-> M1 + M18 -.RBAC.-> 포털 M5 --> GEM - M6 -.서류 릴레이.-> KXWP + M6 -.릴레이.-> KXWP M7 --- CCPY + M15 --- CCPY M1 --- EVT M9 --- PG + M13 --- SGN + M17 --- SGN ``` -**설계 원칙**: M2(홀 좌표계 위 부스 폴리곤) → M3(부스 내부 설계) → M4(배선) → M5(시각화)가 **하나의 공간 데이터 모델(PostGIS 지오메트리)을 공유**한다. 모든 모듈의 산출물이 좌표를 가지므로 시각화·검증·정산이 같은 원천에서 나온다. +**설계 원칙**: (1) M2(홀 좌표계 위 부스 폴리곤) → M3(부스 내부 설계) → M4(배선) → M5(시각화)가 **하나의 공간 데이터 모델(PostGIS 지오메트리)을 공유**한다. 모든 모듈의 산출물이 좌표를 가지므로 시각화·검증·정산·wayfinding·BI가 같은 원천에서 나온다. (2) **폐루프 연결고리 = M15 옥션**: 코어(M2~M5)의 AI 산출물을 응찰 근거로 소비해 발주(M9)·시공으로 잇는다. (3) **M18 관리자**가 룰셋·마스터데이터·RBAC를 전 모듈·전 포털에 공급한다. + +### 4-1. 모듈 우선순위 총괄 (M1~M18) + +| 계열 | 모듈 | 우선순위 | v2.0 | +|---|---|---|---| +| 코어 | M2 배치 · M3 부스설계 · M4 배선 · **M5 나노바나나** | **P0** | 보존 | +| 판매운영 | M1 배정견적 · M6 서류 · M7 매칭 · M9 정산 | P1 | 보존 | +| 판매운영 | **M15 공사/장치 옥션(입찰)** | **P1(핵심 플로우)** | 신규 | +| 판매운영 | M8 반입/반출 물류 | P2 | 보존 | +| 관람참가 | **M10 관람객 등록·배지·체크인·리드캡처** | **P1** | 신규 | +| 관람참가 | **M12 마케팅·EDM·공개 홍보 사이트** | **P1** | 신규 | +| 관람참가 | M11 비즈니스 매칭 | P2 | 신규 | +| 관람참가 | M13 wayfinding·실내 내비 | P2 | 신규 | +| 관람참가 | M14 현장운영(혼잡·안전·주차·에너지) | P2 | 신규 | +| 경영 | **M16 경영분석 BI** | **P1(승격)** | 신규 | +| 경영 | **M17 CMS** | **P1** | 신규 | +| 경영 | **M18 관리자 시스템** | **P1** | 신규 | --- @@ -163,10 +251,11 @@ graph TB - **현재**: 주최자가 CAD로 배치도 수작업 → D-7 kxwp 제출 → 홀매니저 육안 검수. - **AI 자동화 후**: 1. 홀 선택(예: 홀7 126×90m·바닥하중 5t/㎡·510부스 기준) + 조건 입력(목표 부스 수, 3×3m 기본/프리미엄 비율, 주출입구·무대·라운지) - 2. 배치 엔진이 통로 폭·비상구 접근·트렌치 위치를 제약조건으로 복수 배치안 생성 (제약 충족 솔버 + 휴리스틱; 생성형 LLM이 아닌 결정적 알고리즘 중심, LLM은 조건 해석에 사용) - 3. 규정 자동 검증: 피난 통로 확보, 홀별 바닥하중(홀6 2t/㎡ vs 홀7~10 5t/㎡), 복층부스 가능 홀(고층고 12~15m 홀) 여부, 소방 규정 체크리스트 - 4. 부스별 좌표·번호가 확정되면 참가업체 초대 링크 발급 → M3/M4의 입력이 됨 -- **기대 효과**: 배치 초안 수일 → 수 분. 검수는 "위반 플래그 확인" 작업으로 전환. + 2. 배치 엔진이 통로 폭·비상구 접근·트렌치 위치를 제약조건으로 **정확히 3안(1안·2안·3안) 생성** (제약 충족 솔버 + 휴리스틱; 생성형 LLM이 아닌 결정적 알고리즘 중심, LLM은 조건 해석에 사용). 3안은 서로 다른 최적화 목표를 갖도록 다양화(예: 1안=부스 수 최대, 2안=동선·프리미엄 가시성 우선, 3안=피난·안전 여유 우선). + 3. **선택 또는 병합(merge)**: 사용자는 ① 한 안을 그대로 **선택**하거나, ② 여러 안의 구역/블록을 골라 **병합** — 예: "1안의 통로 구성 + 2안의 프리미엄존 배치 + 3안의 무대 위치"를 조합해 하나의 최종 배치로 합성. 병합 편집 화면에서 안별 레이어를 토글·드래그로 조합. + 4. 규정 자동 검증: 피난 통로 확보, 홀별 바닥하중(홀6 2t/㎡ vs 홀7~10 5t/㎡), 복층부스 가능 홀(고층고 12~15m 홀) 여부, 소방 규정 체크리스트. **병합 결과는 규정 검증을 재실행**(통로·바닥하중·비상구 재판정) — 병합으로 제약이 깨질 수 있으므로 확정 전 필수. + 5. **최종안 확정 → 버전 기록**: 확정 배치는 버전으로 저장(선택/병합 출처 안 번호 추적). 부스별 좌표·번호가 확정되면 참가업체 초대 링크 발급 → M3/M4의 입력이 됨. +- **기대 효과**: 배치 초안 수일 → 수 분. 검수는 "위반 플래그 확인" 작업으로 전환. 3안 비교·병합으로 주최자 영업 관행 반영 유연성 확보(R7 완화). - **한계**: 소방 법규의 최종 유권해석·승인은 관할 기관/킨텍스 몫. 시스템 검증은 사전 필터. - **필요 데이터**: 홀별 실측 도면(63×171m, 층고 10~15m, 기둥·셔터·비상구 좌표 — **킨텍스로부터 CAD 원본 확보 필요, 미확보 시 공개 규격 기반 근사 도면으로 Phase 1 진행**), 트렌치 그리드 좌표. - **연동**: M5(홀 전경 시각화), M6(부스배치도 제출 서류 자동 생성), kxwp. @@ -176,7 +265,7 @@ graph TB - **현재**: 조립부스는 주최측 기본 구조에 간판/가구/조명 옵션 신청서 작성. 독립부스는 장치업체가 평면·입면·조감·전기도면 작성 → kxwp 제출 → 반려 시 재작업. - **AI 자동화 후**: - **조립부스**: 옵션(간판 문구, 가구, 조명)을 웹에서 선택 → 3D 프리뷰 + M5 예상 사진 즉시 생성. - - **독립부스**: 부스 크기·업종·전시품·예산을 입력하면 AI가 레이아웃 초안(안내데스크, 시연존, 상담존, 창고) + 파라메트릭 구조(벽체·트러스·사인) 생성. 장치업체는 초안을 편집. + - **독립부스**: 부스 크기·업종·전시품·예산을 입력하면 AI가 레이아웃 초안(안내데스크, 시연존, 상담존, 창고) + 파라메트릭 구조(벽체·트러스·사인)를 **정확히 3안(1안·2안·3안) 생성**(예: 1안=시연 강조, 2안=상담·상권 동선 우선, 3안=예산 절감형). 사용자는 M2와 동일하게 ① 한 안 **선택** 또는 ② 여러 안의 구역/집기 블록을 조합해 **병합**해 최종 부스 설계를 만든다. 병합 결과는 규정 사전 검증(높이·이격·방염)을 재실행하고 최종안은 버전으로 기록. 장치업체는 확정 초안을 편집. - **규정 사전 검증**: 높이 5m 이하, 리깅 천장 6.5~8.5m(구조계산서 D-7 필요 플래그), 복층 1/2 이내, 방염 자재 체크리스트, 장내 금지작업(전기톱·용접·페인트) 공정 경고를 **제출 전에** 자동 플래깅. - 기존 도면(PDF/이미지) 업로드 시 비전 모델로 치수·구조 추출 → 동일 검증 적용 (정확도 한계로 "참고용 검증" 라벨). - **기대 효과**: 반려 재작업 감소, 참가업체-장치업체 간 컨펌 사이클 단축(예상 사진으로 합의). @@ -233,6 +322,215 @@ graph TB - **AI 자동화 후**: 납부 스케줄 자동 생성·알림, 유틸리티 신청 건 PG 결제, 행사 후 실사용(전기 검침 등) 대비 예치금 정산 내역 투명화. - **한계**: 킨텍스 재무 프로세스(세금계산서 등)와의 연동 협의 필요. +--- + +## 5A. v2.0 신규 모듈 (M10~M18) + +> 벤치마크 근거는 각 모듈 말미 "근거"에 명시(크롤링 §머리말). 근거 없는 기능 확장은 배제하고, 킨텍스 도메인(공간 데이터·나노바나나·등록업체 규정)과 접합점이 있는 것만 채택했다. + +### M15. 공사/장치 옥션(입찰) 플랫폼 — **P1 (핵심 플로우)** + +> v2.0의 사업적 종결 모듈. **P0 코어(M2~M5)의 AI 산출물이 곧 응찰 근거 자료**가 되어, 설계→시각화→발주가 한 흐름으로 닫힌다. + +- **현재**: 독립부스 시공은 킨텍스 등록 장치업체 필수이나(미등록 엄금), 참가업체는 739개 업체 리스트를 엑셀로 받아 개별 접촉·수기 견적 취합. 견적 비교·경쟁 유도 수단 없음. +- **AI 자동화 후 — 역경매(reverse auction) 옥션**: + 1. **자료 열람**: 참가업체/주최자가 옥션을 개설하면, 초대(또는 공개)된 **킨텍스 등록업체만** 포털에서 AI 생성 자료 — M2 배치도·M3 부스 설계안·M4 배선/**물량서(BOQ)**·M5 나노바나나 시공 예상 이미지 + 스펙/사양서 — 를 열람. + 2. **응찰 = 견적서(Quotation) 제출**: 업체는 열람 자료에 근거해 **정식 견적서를 제출하는 것으로 응찰**한다. 견적서 모델 = 항목(공종·자재)·수량·단가·금액·납기·유효기간·조건 라인 + 첨부(도면/사양)·**총액·부가세**. 견적서는 **PDF로 산출·버전 관리**(수정 시 새 버전, 라운드 내 재응찰 이력 보존). + 3. **옥션 메커니즘**: 옥션 유형 설정형 — **기본 역경매**(라운드/마감 내 더 낮은 금액 또는 더 높은 종합점수로 재응찰) / 필요 시 단일 라운드 RFQ·고정가 비교. **응찰 라운드·마감·실시간 순위** 노출(현재 순위/최저가/내 위치, 익명 순위 옵션). 라운드 종료 자동 마감. + 4. **낙찰 기준**: **최저가** 또는 **종합평가**(가격 + 평판(과거 시공 평점·규정 준수 이력) + 납기) 가중 스코어. 기준·가중치는 옥션 개설 시 설정, 결과 화면에서 항목별 비교표 제공. + 5. **낙찰(Award) → 계약·발주 연동**: 전시업체(참가업체/주최자)가 옥션 결과로 **업체를 확정(낙찰)** → 낙찰 견적서를 계약/발주 문서로 전환(M6 서류·M9 정산 연동), 시공 일정은 M8 반입/마일스톤과 연결. +- **등록업체 게이트(불변)**: M7 등록업체 DB 검증으로 **미등록 업체 응찰 원천 차단**. 14개 분류(전시디자인설치·리깅·전기시설 등)별 옥션 세분 가능. +- **엔티티**: `Auction(옥션: 유형·라운드·마감·낙찰기준·가중치) 1─N Quotation(=Bid, 견적서: 라인아이템·총액·부가세·납기·유효기간·PDF·버전·업체) ─ Award(낙찰: 선정 견적서·사유·계약/발주 링크)`. +- **기대 효과**: 견적 취합 수일·불투명 → 경쟁 응찰로 가격 최적화·투명화. AI 자료 기반이라 **동일 사양 위 공정 비교**(사과 대 사과) 가능. +- **한계**: 업체 참여율·응찰 품질은 시장에 의존. 킨텍스가 등록업체 옥션을 공식 채널로 인정하는지 협의 필요(규정상 '등록업체 시공 필수'는 옥션과 정합). 실제 계약 체결·법적 효력은 당사자 책임 — 시스템은 견적 경쟁·낙찰 기록까지. +- **필요 데이터**: 등록업체 DB(M7), M2~M4 물량/사양, 평판 데이터(초기엔 규정 준수 이력·자기신고, 축적 후 시공 평점), 공종 표준 단가(선택). +- **연동**: M2~M5(자료), M7(업체 검증), M6(계약 서류), M9(발주·정산), M8(시공 일정). +- **근거**: FindRFP·Procore·4castplus(RFQ/RFP/Tender 구분·역경매·종합평가), Exhibitoronline(전시 부스 RFP 항목·타임라인 RFI 180일/RFP 120~150일/RFQ 90일 전). + +#### 입찰(옥션) 플로우 다이어그램 + +```mermaid +sequenceDiagram + participant EX as 참가업체/주최자 + participant AU as M15 옥션 + participant CORE as M2~M5 AI 자료 + participant C1 as 등록업체 A + participant C2 as 등록업체 B + participant M7 as M7 등록업체 검증 + participant AW as 낙찰·발주(M6/M9) + + EX->>AU: 옥션 개설(유형=역경매, 낙찰기준=종합평가, 라운드/마감) + CORE-->>AU: 배치·설계·배선/물량서·나노바나나 이미지 첨부 + AU->>M7: 응찰 자격 검증(등록업체만) + M7-->>AU: A·B 적격 / 미등록 C 차단 + AU-->>C1: 자료 열람 권한 + AU-->>C2: 자료 열람 권한 + C1->>AU: 견적서 v1 제출(총액·납기) + C2->>AU: 견적서 v1 제출 + AU-->>C1: 실시간 순위(현재 2위) + AU-->>C2: 실시간 순위(현재 1위) + C1->>AU: 견적서 v2 재응찰(라운드 내 인하) + AU->>AU: 마감 → 종합점수(가격+평판+납기) 산정 + AU-->>EX: 견적서 비교표 + 순위 + EX->>AW: 낙찰(Award) 선정 + AW-->>C1: 낙찰 통지 → 계약/발주 전환 +``` + +### M10. 관람객 등록·티켓·배지·현장체크인·리드캡처 — **P1** + +- **현재**: 킨텍스 공식은 행사일정 검색 수준. 관람객 등록·배지·리드캡처는 행사별 주최자 사무국에 파편화(공통 플랫폼 부재). +- **AI 자동화 후**: 온라인 사전등록(관람객/바이어 유형별 폼) → **모바일 배지/QR 발급** → 현장 QR 체크인(즉석 배지 인쇄·오프라인 대비) → 실시간 입장 집계. 참가업체 **리드캡처**: 모바일 앱으로 관람객 배지 QR 스캔 → 연락처·관심도 평점·메모 저장 → 팔로업 EDM(M12) 연계. 사전등록·체크인 데이터는 M16 BI로 흐른다. +- **기대 효과**: 현장 등록 대기 제거, 참가업체 ROI 측정(리드 수·품질), 관람객 흐름 데이터 확보. +- **한계**: 배지 QR·리드캡처는 개인정보(§10 R10) — 동의·보존정책 필수. 오프라인 체크인 폴백 필요(현장 네트워크 불안정). +- **필요 데이터**: 관람객 폼 스키마, 배지 템플릿(M17 CMS), 부스-참가업체 매핑(M2). +- **연동**: M11 비즈매칭, M12 마케팅, M13 wayfinding, M16 BI. +- **근거**: Eventleaf·VenueSight·Whova·WebMobi(자가등록·QR 체크인·즉석 배지·리드 리트리벌 앱·관심도 평점·메모). + +### M11. 비즈니스 매칭 — P2 + +- **현재**: 없음(주최자 개별 운영). +- **AI 자동화 후**: 관람객/참가업체 프로필·관심 업종·의향(intent) 기반 **AI 미팅 추천** → 미팅 슬롯 예약·일정 관리 → 부스 위치(M2)·wayfinding(M13) 연계 길안내. 매칭 성과는 M16 BI. +- **한계**: 매칭 품질은 프로필 데이터 충실도에 의존. AI 추천은 온프레미스/승인된 모델 범위 내(외부 API 게이트 §10 R12 준수). +- **필요 데이터**: 참가/관람 프로필(M10), 부스 업종 태그(M3). +- **연동**: M10·M13·M16. +- **근거**: Brella·Grip·Swapcard·RainFocus·Bizzabo(프로필·intent·행동신호 기반 AI 매치메이킹, 미팅 스케줄링). + +### M12. 마케팅·EDM·공개 홍보 사이트(일반 대중) — **P1** + +- **현재**: 킨텍스 공식 사이트는 정보·서식 다운로드 중심, 참가업체 전용 GNB 없음. +- **AI 자동화 후**: (1) **공개 홍보 사이트**(불특정 다수 대상) — 행사 소개·일정·교통·사전등록 유도, **SEO·다국어(한/영/중/일)** 최적화, 공개 인터랙티브 플로어플랜(M2 데이터 부산물). (2) **EDM/마케팅 자동화** — 세그먼트별(사전등록자·과거 관람객·바이어) 캠페인, 리마인더, 리드 팔로업(M10). AI 카피 초안·이미지(나노바나나 부스 예상샷 활용) 지원. 콘텐츠는 M17 CMS로 관리. +- **기대 효과**: 관람객 유치·재방문, 참가업체 노출, 킨텍스 브랜드 채널 통합. +- **한계**: 발송 규정(정보통신망법·수신동의) 준수, 다국어 번역 품질 검수 필요. +- **필요 데이터**: 행사 마스터(M1), 관람객 세그먼트(M10), 콘텐츠(M17). +- **연동**: M10·M11·M16·M17. +- **근거**: Event Tech Live·Swapcard(등록→리벤뉴 퍼널·EDM), 공개사이트·다국어는 GUARDiA 표준(SEO/다국어) 정렬. + +### M13. Wayfinding·실내 내비 — P2 + +- **현재**: 없음. 양 전시장 100m 무빙워크·복수 홀 구조로 길찾기 난이도 높음. +- **AI 자동화 후**: M2 실측 플로어플랜 위 **블루닷 실내 내비**(부스·시설·비상구·화장실 검색·경로), 관람객 앱(M10)·비즈매칭(M11) 연계. 킨텍스 규모상 UWB/BLE 비콘 하드웨어 인프라 협의 전제(정밀), 미보유 시 **지도 기반 존-레벨 길안내(하드웨어 무의존)로 시작**. +- **한계**: 정밀 측위는 비콘 인프라 투자 필요(킨텍스 시설 협의). 초기엔 인터랙티브 지도·QR 스팟 안내로 대체. +- **필요 데이터**: M2 지오메트리, 시설 POI, (선택)비콘 좌표. +- **연동**: M2·M10·M11·M17(사이니지). +- **근거**: Pointr·Eventbase·Mapsted·Swapcard(블루닷·UWB+BLE·CES 2026 70만 세션), 존-레벨 폴백 실증. + +### M14. 현장운영(혼잡·안전·주차·에너지) — P2 + +- **현재**: 소음 70~75dB 초과 시 전기 차단 등 수기 통제. 주차 iparking. 안전(안전모·금지작업)은 현장 육안. +- **AI 자동화 후**: 홀매니저 운영 대시보드에 **입장·혼잡 실시간(M10 체크인)**, 홀 단위 **전력 부하 집계(M4)**·에너지 모니터, 주차 점유(iparking 연계), 안전 체크(규정 위반 신고·소음·금지작업 플래그). 이상 시 알림. +- **한계**: 센서·IoT 연동은 킨텍스 시설 인프라 의존. 초기엔 M4/M10 파생 데이터 + 수기 입력 결합. +- **필요 데이터**: 체크인(M10), 전력(M4), 주차(iparking), 안전 룰셋(M18). +- **연동**: M4·M8·M10·M16. +- **근거**: Mapsted·Event Tech Live(현장 혼잡·에너지·운영 모니터링), 킨텍스 실측(소음/전력 규정). + +### M16. 경영분석 BI — **P1 (P2→P1 승격)** + +> 기존 design.md에서는 P2 게이트였으나 v2.0에서 **정식 P1 모듈로 스코프 편입**. **design.md 반영은 후속 designer 몫으로 표기**(planner는 스코프·데이터만 확정). + +- **현재**: 정산·가동률·참가사 데이터가 산재. 경영 의사결정은 수기 집계. +- **AI 자동화 후**: 경영 대시보드 — **매출**(임대·유틸리티·옥션 수수료), **홀 가동률**(RevPAD·㎡당 수익·점유율), **참가사 리텐션**(재참가율), **이벤트 P&L**(행사별 손익), **수요예측**(성수기·홀별), **수율/가격 최적화**(요율·성수기 계수 시뮬레이션), **KPI 대시보드**. 온프레미스/승인 모델 범위 내 예측·이상탐지. +- **기대 효과**: 저성과 홀·시즌 식별, 가격·배정 전략 데이터화. +- **한계**: 정확도는 원천 데이터(M1·M9·M10·M15) 정합성에 의존. 예측은 참고치. +- **필요 데이터**: M1 배정·M9 정산·M10 관람·M15 옥션·행사일정. +- **연동**: 전 모듈(데이터 소비), M18(권한). +- **근거**: ExhibitForce·JoinWays·FinancialModelsLab·Digitevent·TicketFairy(RevPAD·㎡당 수익·점유율 60~75%·이벤트 P&L·B2B 전시 KPI). + +#### M16-1. 운영사(킨텍스·venue operator) 관점 수익성/ROI + +> **관점 구분 필수**: BI는 두 관점을 분리 제공한다 — **① 참가업체 관점 ROI**(리드 수·품질·부스 비용 대비 성과, M10 리드 기반)와 **② 킨텍스 운영사 관점 수익성/ROI**(전시장 자산 수익화). 본 소절은 **② 운영사 관점**을 명시한다. 두 관점은 별도 대시보드·권한(참가업체는 자기 부스, 킨텍스는 전 행사)으로 격리. + +| # | 운영사 지표 | 정의/산식 | 데이터원 | +|---|---|---|---| +| ① | **홀·기간별 가동률(occupancy)** | 점유 홀·일수 / 가용 홀·일수 × 100, 반홀(1A/1B) 분할·공실 구분, 성수기/비수기 별 | M1 배정·행사일정 | +| ② | **매출 구성(revenue mix)** | 임대료(2,250원/㎡ 등) + 유틸리티(전기·급배수·인터넷) + 부대시설(옥외·로비·회의실·주차·옥션 수수료) 세그먼트별 | M1·M4·M9·M15 | +| ③ | **행사별 P&L / 수익성(마진)** | 행사 매출 − 직접원가(운영·에너지·인력) = 공헌이익·마진율, 행사 단위 손익 | M9 정산·M14 에너지·비용 마스터(M18) | +| ④ | **행사 유치·전시장별 ROI** | 전시장(1전시장/2전시장/홀별)·행사유형별 투자 대비 수익, RevPAD·㎡당 수익(㎡ 정규화로 대형홀 vs 소형홀 생산성 비교) | M1·M9·PostGIS 면적 | +| ⑤ | **참가사 리텐션·LTV** | 재참가율(코호트)·이탈률, 참가사 생애가치(LTV=누적 임대+유틸+옥션·평균 재참가 주기) | M1·M9 이력·다년 데이터 | +| ⑥ | **수요예측·수율/가격(yield/pricing) 최적화** | 성수기 계수·홀별 수요 예측, 요율·성수기(+10%)/비수기(-10%)·1전시장(+10%) 계수 시뮬레이션으로 수율 최적가 탐색 | M1 요율·과거 배정·M18 룰셋 | +| ⑦ | **경영진 KPI 대시보드** | 위 ①~⑥ 요약 + 목표 대비 실적(점유율 목표 60~75%·매출·마진·리텐션), 전시장·분기 드릴다운 | KpiSnapshot 집계 | + +- **데이터마트 설계(DA 트랙)**: 위 지표는 운영 DB 직조회가 아니라 **BI 데이터마트(스타 스키마: FactBooking·FactSettlement·FactUtility·FactAuction·FactVisitor + DimHall·DimEvent·DimDate·DimExhibitor)** 로 집계한다. `KpiSnapshot`(배치/야간 적재) 또는 읽기 전용 복제로 운영 부하 회피(§8-1). **데이터마트 상세 모델링은 DA(데이터 분석가) 후속 트랙**이며, planner는 지표·데이터원·관점 분리만 확정. +- **한계**: LTV·리텐션은 다년 축적 데이터 필요(초기엔 단년 근사). 수율 최적가는 시뮬레이션 참고치이며 최종 요율 확정은 킨텍스 경영 의사결정(§10 R8). +- **근거**: JoinWays·FinancialModelsLab·TicketFairy(RevPAD·㎡당 수익·점유율 60~75% 목표·이벤트별 P&L·수율 관리), ExhibitForce(이벤트 ROI·수익성 대시보드). + +### M17. CMS — **P1** + +- **현재**: 킨텍스 공지 852건·홍보자료를 공식 사이트가 정적 관리. 참가업체 마이크로사이트 개념 없음. +- **AI 자동화 후**: 전시 **콘텐츠·공지 관리**, **참가업체 마이크로사이트**(부스 소개·제품·나노바나나 예상샷 게시), **다국어**(한/영/중/일) 콘텐츠, **사이니지 연계**(디지털 사이니지·wayfinding 화면 콘텐츠 배포 — GUARDiA Signage 패턴 참고). 게시 워크플로(초안→검수→게시)·버전관리. +- **한계**: 사이니지 하드웨어 연동은 킨텍스 시설 협의. 다국어 번역 검수 필요. +- **필요 데이터**: 콘텐츠 스키마, 행사/부스 마스터, (선택)사이니지 기기. +- **연동**: M12(마케팅)·M13(wayfinding)·M10(배지 템플릿). +- **근거**: GUARDiA CMS·Signage 하네스 패턴(헤드리스 콘텐츠·게시 워크플로·다국어·사이니지 배포) 정렬 + 킨텍스 공지/홍보 도메인. + +### M18. 관리자 시스템(백오피스) — **P1** + +> **M18 = §5B 공통/시스템관리 레이어의 "시스템관리(system)" 모듈이 킨텍스 도메인 마스터데이터를 얹은 것.** 즉 M18은 독립 재구현이 아니라 **UIWS 표준 `system` 모듈을 채택·확장**한 것으로 정의한다(중복 제거). + +- **현재**: 없음(데이터·권한 통제 부재). +- **AI 자동화 후**: 별도 백오피스(`admin.`, 웹 전용) — UIWS 표준 시스템관리(사용자·역할 RBAC·공통코드·메뉴·감사로그·시스템설정) + **킨텍스 마스터데이터 관리**(홀 마스터·요율표·유틸리티 요금·규정 룰셋·등록업체 DB·표준 단가). 룰셋은 **버전 관리**(연 단위 요율·규정 개정 대응 — §8 룰 엔진과 결합). +- **기대 효과**: 크로스-테넌트 통제, 규정·요율 개정의 안전한 반영, 감사 대응. +- **한계**: 마스터데이터 원천(킨텍스 공식 확정본) 확보 필요(§7). +- **필요 데이터**: 전 마스터데이터, 사용자·권한 스키마(UIWS `TB_*`). +- **연동**: 전 모듈·전 포털(RBAC·룰셋 공급). +- **근거**: **UIWS 표준(`workspace/uiws`) 시스템관리** + GUARDiA 표준 프레임워크 §1·§3(사용자·RBAC·감사로그·시스템설정 백오피스 표준) 정렬. + +--- + +## 5B. 공통/시스템관리 레이어 (UIWS 표준 이식) + +> **채택 근거**: kintex 스택(Spring Boot 3.x·Java 17·React·MyBatis·PostgreSQL)이 **UIWS(UIMS, `workspace/uiws`) = GUARDiA 표준 프레임워크**와 동일하므로, 시스템관리·공통 업무 기능을 재설계하지 않고 **UIWS 표준을 공통 레이어로 그대로 이식**한다. 킨텍스 도메인 모듈(부스 M2~M5·옥션 M15·관람객 M10·BI M16·CMS M17 등)은 **이 공통 레이어 위에 얹힌다.** 이식 레퍼런스·정본 = `workspace/uiws`(읽기 전용), 표준 명세 = `workspace/_framework/GUARDIA_STANDARD_FRAMEWORK.md`. 이식 실행은 `uiws-port-orchestrator` 트랙(후속 개발). — **P1 (전 모듈 선행 기반)** + +### 5B-1. 시스템관리 (system) — M18과 통합 + +UIWS 표준 시스템관리를 kintex 관리자 백오피스(`admin.`)의 기반으로 채택. **M18은 이 모듈 + 킨텍스 마스터데이터 확장**이며 중복 정의하지 않는다. + +| 기능 | UIWS 표준 | kintex 확장/접합 | +|---|---|---| +| 사용자 관리 | `TB_USER` CRUD·상태·부서 | 6역할(§2) + 등록업체 계정·관람객 셀프서비스 계정 | +| 역할/권한 RBAC | 역할 게이트 `/api/admin/** = hasRole(ADMIN)` | 플랫폼 레벨(관리자) + 행사 레벨(주최자/참가/업체/홀매니저) 이중 평가(§8-1) | +| 공통코드(adminCode) | 코드 그룹·상세 | 홀/부스유형/공종(14분류)/유틸리티 요금코드 등 도메인 코드 | +| 메뉴 관리 | 메뉴 트리·권한 매핑 | 역할별 포털 IA(§2-1) 메뉴 구동 | +| 감사로그(audit) | `TB_AUDIT_LOG` | 승인·**낙찰(M15)**·설계 변경·룰셋 개정·리드 접근(개인정보) 전수 기록 | +| 시스템설정 | 환경설정 | 요율/규정 룰셋 버전, AI 게이트, 마감(D-25/D-7) 정책 | + +### 5B-2. 공통 업무 기능 (전 역할 공용) + +UIWS 표준 공통 모듈을 이식해 전 포털이 공유. 킨텍스 도메인 접합점을 명시(불필요 이식 배제). + +| 공통 모듈 | UIWS 표준 | kintex 접합 | +|---|---|---| +| worklog(업무일지) | 일일 업무일지·댓글 | 홀매니저 현장 일지, 업체 시공 일지 | +| schedule(일정) | 개인/부서 일정·캘린더 | 행사 마일스톤(M6)·옥션 마감·반입 슬롯(M8) 통합 뷰 | +| message(쪽지) | 사내 쪽지 | 주최자↔참가업체↔업체↔홀매니저 행사 내 커뮤니케이션 | +| notice(공지) | 공지 게시·팝업 | 킨텍스 공지(852건 도메인)·행사 공지 → M17 CMS와 채널 정합 | +| opinion(의견접수) | 의견/건의 | 고객의소리·규정 문의 | +| search(통합검색) | 크로스 모듈 검색 | 행사·부스·업체·문서·콘텐츠 통합 검색 | +| meeting(회의록) | 회의 녹음→STT→회의록(Jasper PDF) | 사전업무협의(D-30) 회의록 자동화 | +| report/보고(stats) | 일/주/월/분기/연 업무보고·통계(Jasper PDF) | 운영 리포트·M16 BI 원천 피드(중복 회피: 경영지표=M16, 업무보고=공통) | +| notification(알림센터) | 통합 알림 | 마감 리마인더(D-데이)·낙찰·승인·결제 알림, WebSocket | +| preference(개인화) | 즐겨찾기·최근방문 | 역할별 대시보드 개인화 | + +> **경계(중복 제거)**: 경영·수익 지표는 **M16 BI**가 권위, 일상 업무보고·통계는 공통 `report/stats`가 담당. 콘텐츠/공지 발행은 **M17 CMS**가 권위, 사내 알림성 공지는 공통 `notice`. 알림 발송 채널은 공통 `notification` 단일화(M10·M12·M15가 이벤트 발행). + +### 5B-3. 인증 (UIWS 표준: JWT + 2FA/OTP) + +kintex 인증은 §2 행사 단위 RBAC를 **UIWS 표준 인증 위에** 구현한다(재설계 금지). + +- **JWT + RBAC**: 발급·역할 게이트. §8-1 SSO(플랫폼+행사 이중 권한)와 결합. +- **2차 인증 OTP(TOTP RFC6238)**: SHA1·30s·6자리·±1 윈도우. `TotpService` 이식 — 로그인 2단계(비번→OTP), 최초 QR 등록, 마이페이지 재설정/해제, 관리자 OTP 초기화(`otp_secret=NULL`). 대상: 관리자·홀매니저·주최자·업체(내부/발주 권한 계정) 필수, 관람객 셀프서비스는 선택. +- **로그인 실패 잠금** + 관리자 해제. +- **admin 비밀번호**: env `ADMIN_PASSWORD_ENC`(AES-256-GCM) + `ADMIN_KEY_FILE`(별도 키파일 root 600) 복호 → 기동 시 BCrypt 재시드. **하드코딩 `admin123` 시드 금지**(§10 보안). +- **스키마**: UIWS `TB_USER`·OTP 시크릿 컬럼·`TB_AUDIT_LOG` 이식(멱등 DDL, sql.init `mode=always`+continue-on-error). + +### 5B-4. 이식 원칙 + +1. **재설계 금지·이식 우선**: 공통 레이어는 `workspace/uiws` 코드/스키마/디자인을 이식(수정 최소화). 킨텍스 고유는 도메인 모듈에만. +2. **선행 기반**: 공통/시스템관리·인증은 도메인 모듈(M1~M17)보다 **선행**(Phase 1 착수 전제 — §9 갱신). +3. **중복 제거**: M18=시스템관리(5B-1), 기존 문서의 "관리자 시스템"은 본 절로 흡수. BI/CMS/알림은 위 경계 규칙으로 공통과 분리. +4. **디자인**: 공통 화면은 UIWS/WISE 디자인을 kintex design.md 토큰으로 정합 — **designer 후속 반영 필요**(planner는 스코프·구조만 확정). + +--- + ### 기능 우선순위 총괄표 | 모듈 | 기능 | 우선순위 | 필요 데이터 | 연동 | @@ -247,6 +545,16 @@ graph TB | M7 | 등록업체 매칭 | P1 | 등록업체 DB 739 | CCPY | | M9 | 정산·결제 | P1 | 요율·납부 규칙 | PG | | M8 | 반입/반출 슬롯 | P2 | 하역장 배치 | 운영팀 | +| **M15** | **공사/장치 옥션(역경매·견적서 응찰·낙찰)** | **P1(핵심)** | M2~M4 물량/사양, 등록업체 DB, 평판 | M2~M5, M7, M9, M6 | +| **M10** | **관람객 등록·배지·체크인·리드캡처** | **P1** | 관람객 폼, 배지 템플릿, 부스 매핑 | M11, M12, M13, M16 | +| **M12** | **마케팅·EDM·공개 홍보 사이트(SEO·다국어)** | **P1** | 행사 마스터, 관람 세그먼트 | M10, M16, M17 | +| **M16** | **경영분석 BI(운영사 관점 수익성/ROI + 참가사 ROI: 가동률·매출구성·행사별 P&L·전시장 ROI·리텐션/LTV·수율/가격·경영진 KPI)** | **P1(승격)** | M1·M4·M9·M10·M15·행사일정 → BI 데이터마트 | 전 모듈 | +| **M17** | **CMS(콘텐츠·마이크로사이트·다국어·사이니지)** | **P1** | 콘텐츠 스키마, 행사/부스 마스터 | M12, M13 | +| **M18** | **관리자 백오피스(RBAC·감사·마스터데이터·룰셋)** | **P1** | 전 마스터데이터, 사용자/권한 | 전 모듈·전 포털 | +| **§5B** | **공통/시스템관리 레이어(UIWS 표준 이식) + JWT+OTP 인증** | **P1(전 모듈 선행 기반)** | UIWS `TB_*`·표준 스키마 | 전 모듈·전 포털 | +| M11 | 비즈니스 매칭 | P2 | 프로필, 부스 업종 태그 | M10, M13, M16 | +| M13 | wayfinding·실내 내비 | P2 | M2 지오메트리, POI, (선택)비콘 | M2, M10, M17 | +| M14 | 현장운영(혼잡·안전·주차·에너지) | P2 | 체크인·전력·주차·안전 룰셋 | M4, M8, M10 | | — | 다국어 규정 챗봇, 관람객 플로어플랜 | P2 | 매뉴얼 코퍼스 | — | --- @@ -425,7 +733,13 @@ Render this exhibition booth as if construction is complete. ### 7-3. 핵심 엔티티 (요약 ERD) -`Event(행사) 1─N Hall배정 1─N Booth(부스, PostGIS polygon) 1─N DesignPlan(버전) / UtilityOrder(전기·네트워크·급배수, 배선 LineString) / RenderJob(샷, 상태, 이미지) / Document(서식, 마일스톤) / Company(등록업체) / Payment` +`Event(행사) 1─N Hall배정 1─N Booth(부스, PostGIS polygon) 1─N DesignPlan(버전, 3안·선택/병합 출처 추적) / UtilityOrder(전기·네트워크·급배수, 배선 LineString) / RenderJob(샷, 상태, 이미지) / Document(서식, 마일스톤) / Company(등록업체) / Payment` + +**v2.0 추가 엔티티**: +- 옥션: `Auction(유형·라운드·마감·낙찰기준·가중치) 1─N Quotation(=Bid, 견적서: 라인아이템[공종·자재·수량·단가·금액]·총액·부가세·납기·유효기간·조건·첨부·PDF·버전·업체) ─ Award(낙찰: 선정 견적서·사유·계약/발주 링크)`. Auction은 Booth/DesignPlan/UtilityOrder(물량)·RenderJob(이미지)을 참조 자료로 첨부. +- 관람: `Visitor(관람객) 1─N Registration(등록·유형) 1─N Badge(QR) ─ CheckIn(체크인) ; Lead(리드: 참가업체─관람객·관심도·메모) ; Meeting(비즈매칭 미팅·슬롯)`. +- 경영/관리: `KpiSnapshot(BI 집계) ; Content(CMS 콘텐츠·다국어·버전) ; Microsite(참가업체) ; MasterData(홀·요율·요금·룰셋·버전) ; User·Role·AuditLog(M18)`. +- 공간 데이터 공유(불변): Booth 폴리곤·배선 LineString은 M13 wayfinding·M14 부하집계·M16 ㎡당 수익이 동일 PostGIS 원천을 재사용. --- @@ -470,6 +784,52 @@ graph LR - **룰 엔진 분리**: 규정(높이·방염·하중)과 요율을 코드가 아닌 버전 관리되는 룰셋 데이터로 유지 — 킨텍스 규정 개정(연 단위 요율 변경 등) 대응. Spring 서비스 계층이 룰셋을 로드·평가. - 인증: 행사 단위 RBAC(2장 권한 모델) + JWT. 오브젝트 스토리지에 도면·생성 이미지·서식 적재. 도면·설계 데이터는 행사 종료 후 보존 정책 별도 정의(참가업체 자산). +### 8-1. v2.0 아키텍처 보강 — 역할별 분리 프론트 + 공유 백엔드 + 공개사이트/백오피스 + +확정 스택(React + Spring Boot 3.x(Java 17) + MyBatis + PostgreSQL/PostGIS + Redis + 나노바나나 Python 워커)은 **불변**. 그 위에 v2.0의 다중 포털·공개사이트·백오피스를 얹는다. + +```mermaid +graph TB + subgraph 프론트["역할별 분리 프론트 (React·Vite·TS, 공유 디자인시스템)"] + FO[organizer. 주최자 콘솔] + FE[exhibitor. 참가업체 포털] + FC[contractor. 업체 포털·옥션] + FM[ops. 운영 대시보드] + FA[admin. 관리자 백오피스] + FP[www/expo. 공개 홍보 사이트
SEO·SSR·다국어] + FV[관람객 모바일 앱
배지·wayfinding·매칭] + end + subgraph 게이트["SSO · 역할 RBAC · API Gateway"] + SSO[JWT SSO + 역할·행사 RBAC] + end + subgraph 백엔드["공유 Spring Boot 3.x 백엔드"] + API[REST + WebSocket/STOMP] + RULE[룰 엔진·요율/규정] + LAYOUT[배치·배선 엔진 PostGIS] + AUC[옥션 엔진 M15
라운드·순위·낙찰스코어] + BI[BI 집계 M16] + CMS[CMS·다국어 M17] + QUEUE[Redis 큐] + NB[나노바나나 Python 워커] + end + subgraph 데이터["PostgreSQL+PostGIS · 오브젝트 스토리지"] + PG[(업무·공간 데이터)] + OBJ[(도면·이미지·서식·콘텐츠)] + end + FO & FE & FC & FM & FA & FP & FV --> SSO --> API + API --> RULE & LAYOUT & AUC & BI & CMS + API --> QUEUE --> NB --> OBJ + API --> PG + FP -. 캐시/CDN .-> OBJ +``` + +- **역할별 프론트 분리**: organizer·exhibitor·contractor·ops·admin 5개 인증 앱 + public(공개)·visitor(관람객 모바일). 각기 별도 번들·도메인/서브패스로 배포해 **최소권한·공격면 축소**, 단 **공유 디자인 시스템(design.md)·공유 컴포넌트·공유 API 계약**을 상속(중복 구현 금지). 데스크톱=설계/에디터/대시보드, 모바일=현장/조회/승인(§2-1 매트릭스). +- **SSO + 역할 RBAC + 2FA/OTP (§5B UIWS 표준)**: 단일 JWT SSO 위에 플랫폼 레벨(관리자)과 행사 레벨(주최자/참가/업체/홀매니저) 권한을 이중으로 평가. **인증 스택은 UIWS 표준을 이식** — JWT + TOTP(RFC6238) 2차 인증 + 로그인 실패 잠금 + admin 비번 env(`ADMIN_PASSWORD_ENC` AES-256-GCM + 별도 키파일) 주입(§5B-3). M18(=§5B-1 시스템관리)이 역할·권한·공통코드·메뉴·감사로그 마스터를 공급. 공통 업무 기능(worklog·schedule·message·notice·search·meeting·report·notification 등, §5B-2)은 전 포털 공유 레이어. +- **공개 홍보 사이트·CMS(M12·M17) — SEO·다국어**: 불특정 다수 대상이므로 **SSR/정적 생성 + 메타·사이트맵·구조화 데이터(SEO)**, **다국어(한/영/중/일) i18n·hreflang**, CDN 캐시. 인증 앱과 별도 렌더 경로(공개 성능·검색 노출 목적). 콘텐츠는 M17 헤드리스 CMS가 공급, 참가업체 마이크로사이트도 동일 파이프라인. +- **관리자 백오피스(M18)**: 웹 전용 별도 앱. 마스터데이터·룰셋(요율·규정)은 **버전 관리 데이터**로 룰 엔진에 로드 — 킨텍스 연 단위 개정 무중단 반영. 전 승인·낙찰·설계 변경은 감사로그. +- **옥션 엔진(M15)**: Spring 서비스 계층 + Redis(실시간 순위·라운드 마감 타이머) + WebSocket(순위 푸시). 견적서 PDF 생성은 서류 생성 큐(Redis) 재사용, 나노바나나와 동일 비동기 패턴. +- **BI(M16)**: 운영 DB 부하 회피를 위해 집계는 배치/스냅샷(KpiSnapshot) 또는 읽기 전용 복제 권장(구현 트랙 결정). 예측·이상탐지는 §10 R12(외부 API 게이트) 준수 — 온프레미스/승인 모델 범위. + --- ## 9. 로드맵 @@ -478,7 +838,7 @@ graph LR | 항목 | 내용 | |---|---| -| 범위 | M2(단일 홀, 근사 도면), M3(조립부스 전체 + 독립부스 초안 생성), M4(전기·네트워크 배선 + 자동 견적, 신청서·위치표시도 파일 생성까지), M5(S1·S2·S6·S7 샷) | +| 범위 | **§5B 공통/시스템관리 레이어 이식 선행(UIWS: 인증 JWT+OTP·시스템관리·핵심 공통모듈 — 전 모듈 기반)**, M2(단일 홀, 근사 도면), M3(조립부스 전체 + 독립부스 초안 생성), M4(전기·네트워크 배선 + 자동 견적, 신청서·위치표시도 파일 생성까지), M5(S1·S2·S6·S7 샷) | | 산출물 | 웹 앱(주최자·참가업체·장치업체), 나노바나나 파이프라인 v1, 룰셋 v1(장치 규정·요금표), 근사 홀 도면 1종(홀7 권장 — 규격 공개 충실) | | 검증 | 파일럿 1개 행사(또는 과거 행사 데이터 재현)로 배치→설계→배선→시각화 전체 여정 시연 | | 착수 조건 | Gemini API 키, 홀 참조 사진 촬영(샷 프리셋별 배경), 트렌치 좌표(미확보 시 공개 스펙 기반 가정 그리드로 진행하되 '가정' 라벨) | @@ -528,3 +888,4 @@ graph LR | v1.0 | 2026-07-11 | planner | 최초 작성 — kintex-website.md 분석 기반 전체 기획. ReRoomAI 소스 분석(reroomai-source.md)은 추가 시 6장 갱신 예정 | | v1.1 | 2026-07-11 | planner | ReRoomAI 소스 분석 반영. ①§6 나노바나나 파이프라인 제어 파라미터 확정 — 모델 `gemini-3.1-flash-image-preview`(나노바나나 2)·`@google/genai` SDK·image-to-image(inlineData+text parts) 호출 스택, 구조화 사전(BOOTH_TYPES·BOOTH_STYLES·FIXTURE_LAYERS) + "보존/교체 명시 분리" 부스 프롬프트 템플릿(§6-4), Canvas 1024px 전처리·RenderJob 방어 로직(크기 가드·SAFETY·에러 분기·성공 시에만 쿼터 차감)·단계별 로딩 UX 신설(§6-5). ②S6 배선 오버레이 백엔드 래스터 합성 우선 명확화(§6-3). ③§7 마스터 데이터 순증 반영(기본부스 표준 품목·프리미엄 6×3×4m 2kW·조명 반입 금지·이격 30/60cm·옥외 2,849㎡·홀6 93×60×10m·제3전시장 홀11~18). ④§10 R11 해소, R12(Gemini 외부 API 미승인·소유자 승인 게이트) 추가. BACKLOG B-11 done, B-09 정리 | | v1.2 | 2026-07-11 | planner | **기술 스택 확정 — React + Spring Boot 3.x(Java 17) + MyBatis + PostgreSQL(PostGIS), 나노바나나 Python 워커 사이드카. GUARDiA 표준 프레임워크 정렬(사용자 지정).** §8 아키텍처 전면 정합화: 백엔드 FastAPI→Spring Boot 3.x + MyBatis(REST + WebSocket/STOMP, 룰·배치/배선 엔진=서비스 계층 + PostGIS 공간 SQL), 프론트 Next.js→React 18/19(Vite·TS, SVG/WebGL 캔버스), 비동기 큐 Redis 유지 + 이미지 생성은 별도 **Python 워커 사이드카(`tools/nanobanana`, google-genai Python SDK)** 로 분리(Spring이 RenderJob 큐잉→Python 워커 소비·생성→오브젝트 스토리지 적재→WebSocket 완료 푸시, 서류·알림도 동일 큐). Python 워커 유지 근거 명시(ReRoomAI 검증 client.py·Python SDK — Java 재구현 회피). 인증(행사 단위 RBAC+JWT)·오브젝트 스토리지 유지. §8 mermaid 갱신. **기능 범위(M1~M9)·우선순위·나노바나나 파이프라인 로직 불변** — 스택 표기 정합화만 수행 | +| **v2.0** | 2026-07-11 | planner | **정체성 확장 — 킨텍스 자동전시시스템(Exhibition Automation Platform).** 글로벌 전시테크 크롤링(Eventleaf·VenueSight·Whova·ExpoPlatform·Eventbase·Pointr·Swapcard·Brella·Grip·RainFocus·ExhibitForce·FindRFP·Procore·4castplus) 근거로 확장. ①§1 비전 재정의(생애주기 폐루프, M15 옥션이 코어를 발주로 연결) ②§2 역할·포털 매트릭스 신설 — 6역할(주최자·참가·업체·홀매니저·관리자·관람객/대중) 웹/모바일 분리(organizer·exhibitor·contractor·ops·admin·public+visitor), 관리자·일반대중 페르소나 추가 ③§4 모듈맵 mermaid 확장(M10~M18) + 우선순위 총괄 ④§5A 신규 모듈 상세: **M15 공사/장치 옥션(P1·핵심, 역경매·견적서(Quotation)=응찰·실시간 순위·종합평가 낙찰·등록업체만 응찰·Auction 1─N Quotation ─ Award·PDF/버전, 입찰 플로우 시퀀스 다이어그램)**, M10 관람객 등록·배지·리드캡처(P1), M12 마케팅·EDM·공개 홍보 사이트(P1·SEO/다국어), M16 경영분석 BI(P2→**P1 승격**, design.md 반영은 designer 후속), M17 CMS(P1), M18 관리자 백오피스(P1), M11 비즈매칭·M13 wayfinding·M14 현장운영(P2) ⑤**M2·M3 3안 생성→선택/병합 UX 구체화**(1·2·3안 다양화, 구역/블록 병합, 병합 후 규정 재검증, 버전 기록) ⑥§7-3 엔티티 확장(Auction/Quotation/Award·Visitor/Badge/Lead·KPI/Content/User) ⑦§8-1 아키텍처 보강(역할별 분리 프론트+SSO/RBAC+공개사이트 SEO/다국어+백오피스+옥션 엔진) ⑧**§5B 공통/시스템관리 레이어(UIWS 표준 이식) 신설** — kintex 스택=UIWS(GUARDiA 표준 프레임워크) 동일 → 시스템관리(사용자·RBAC·공통코드·메뉴·감사로그·시스템설정)와 공통 업무기능(worklog·schedule·message·stats·notice·opinion·search·meeting·report·notification·audit) + 인증(JWT + **2차 인증 OTP TOTP RFC6238** + 로그인 실패 잠금 + admin 비번 env `ADMIN_PASSWORD_ENC` 주입)을 `workspace/uiws` 레퍼런스로 이식, 도메인 모듈은 그 위에 적재. **M18 관리자 시스템을 UIWS 시스템관리와 통합(중복 제거)**, BI/CMS/알림 경계 규칙 정의, Phase 1 선행 기반으로 로드맵 반영. ⑨**§5A M16-1 운영사(킨텍스·venue operator) 관점 수익성/ROI 소절 신설** — 참가업체 관점 ROI(리드 기반)와 명시 구분하고, 킨텍스 관점 7지표(홀·기간별 가동률/매출구성/행사별 P&L·마진/전시장별 ROI·RevPAD·㎡당 수익/참가사 리텐션·LTV/수요예측·수율·가격 최적화/경영진 KPI) + 데이터원(M1·M4·M9·M10·M15) + **BI 데이터마트(스타 스키마 Fact/Dim, DA 후속 트랙)** 정의. **기존 M2~M5 P0 코어·§6 나노바나나 로직·§8 확정 스택 보존.** design.md/타 문서는 미수정 — designer/planner/DA 후속 반영 필요로 표기 | diff --git a/docs/assets/floorplans/README.md b/docs/assets/floorplans/README.md new file mode 100644 index 0000000..e4e598a --- /dev/null +++ b/docs/assets/floorplans/README.md @@ -0,0 +1,92 @@ +# 킨텍스 전시홀 평면도 자산 매니페스트 + +킨텍스 자동전시시스템(M2 부스 배치 엔진)의 홀 실측 도면 레퍼런스. kintex.com 공개 페이지에서 크롤링(2026-07-11). 로그인/비공개 다운로드는 시도하지 않았으며, 접근 차단(robots/403)은 없었다. + +## 소스 페이지 + +| 페이지 | URL | +|---|---| +| 전시홀 개요 | https://www.kintex.com/web/ko/html/facility/exhibition_overview.do | +| 제1전시장(홀1~5) | https://www.kintex.com/web/ko/html/facility/exhibition_facility_01.do | +| 제2전시장(홀6~10) | https://www.kintex.com/web/ko/html/facility/exhibition_facility_02.do | + +`imageview.do?atchmnflno=...&fileseq=...` 패턴은 이번 크롤링에서 평면도로 발견되지 않았다(아래 "확인 결과: 로컬 자산 재활용 불가" 참조). 실제 평면도는 `/public/common/images/content/exhibition_facility_hallN_visual1.jpg` 정적 경로로 직접 제공된다. + +## 확보 이미지 (JPG/PNG 평면도) + +| 파일명 | 용량 | 출처 URL | 대상 | +|---|---:|---|---| +| `kintex_overview.png` | 1,226,383 B (~1.2MB) | `/public/common/images/content/exhibition_overview_visual1.png` | 제1·2전시장 전체 배치도 + 출입구/지하철/GTX-A/주차장/게이트, Hall1~10 라벨 포함 (가장 넓은 컨텍스트) | +| `kintex1_overview.jpg` | 99,492 B | `/public/common/images/content/exhibition_facility_visual1.jpg` | 제1전시장 전체 배치도(Hall1~5, 5A/5B) | +| `hall1.jpg` | 92,273 B | `/public/common/images/content/exhibition_facility_hall1_visual1.jpg` | 홀1 상세 평면도(1A/1B 분할, 출입구, 기둥Ø2.5m, 치수) | +| `hall2.jpg` | 77,386 B | `/public/common/images/content/exhibition_facility_hall2_visual1.jpg` | 홀2 상세 평면도 | +| `hall3.jpg` | 76,012 B | `/public/common/images/content/exhibition_facility_hall3_visual1.jpg` | 홀3 상세 평면도 | +| `hall4.jpg` | 74,770 B | `/public/common/images/content/exhibition_facility_hall4_visual1.jpg` | 홀4 상세 평면도 | +| `hall5.jpg` | 86,555 B | `/public/common/images/content/exhibition_facility_hall5_visual1.jpg` | 홀5 상세 평면도(5A/5B) | +| `hall_outdoor1.jpg` | 168,953 B | `/public/common/images/content/exhibition_facility_hall_outdoor_visual1.jpg` | 옥외전시장 위치가이드 1 | +| `hall_outdoor2.jpg` | 70,672 B | `/public/common/images/content/exhibition_facility_hall_outdoor_visual2.jpg` | 옥외전시장 위치가이드 2 | +| `kintex2_overview.jpg` | 123,762 B | `/public/common/images/content/exhibition_facility2_visual1.jpg` | 제2전시장 전체 배치도(Hall6~10) | +| `hall6.jpg` | 90,480 B | `/public/common/images/content/exhibition_facility_hall6_visual1.jpg` | 홀6(Event Hall) 1층 평면도(6A/6B/6C 분할) | +| `hall7.jpg` | 94,919 B | `/public/common/images/content/exhibition_facility_hall7_visual1.jpg` | 홀7 1층 평면도(7A/7B) | +| `hall8.jpg` | 92,220 B | `/public/common/images/content/exhibition_facility_hall8_visual1.jpg` | 홀8 1층 평면도(8A/8B) | +| `hall9.jpg` | 93,921 B | `/public/common/images/content/exhibition_facility_hall9_visual1.jpg` | 홀9 1층 평면도(9A/9B) | +| `hall10.jpg` | 89,410 B | `/public/common/images/content/exhibition_facility_hall10_visual1.jpg` | 홀10 1층 평면도(10A/10B) | + +**소계: 15개 파일, 약 2.5MB.** 전부 HTTP 200 정상 다운로드, 접근 차단 없음. + +`hall1.jpg` 육안 확인 결과 실측 치수(171m 전체 = 81m(A) + 63m + 63m + 90m(B)? — 실제로는 Hall A 81m·Hall B 90m 합 171m, 폭 63m), 기둥 위치(Ø2.5m), 출입구(1A~1D), 화장실/엘리베이터/VIP대기실/치안센터/소화전 범례가 포함되어 있어 부스 배치 엔진의 참조 도면으로 즉시 활용 가능. `kintex_overview.png`는 홀 배치 개요(GTX-A·지하철역·게이트 위치)로 부지 컨텍스트에 유용. + +## 확보 CAD 원본 (DWG, zip) + +당초 "CAD는 무리하게 받지 말고 미확보로 표기"하도록 지시받았으나, 실제로 접근 시도한 결과 **로그인 없이 공개 다운로드가 가능**함을 확인하여 확보했다. + +| 파일명 | 용량 | 출처(대표 URL 1개만 다운로드 — 근거는 아래 "동일 파일 확인" 참조) | 내용 | +|---|---:|---|---| +| `cad/kintex1_cad_all_halls.zip` | 18,377,672 B (~17.5MB) | `/download/1exhibition/KINTEX_cad.zip` | 제1전시장 DWG 4개: A3002(홀1 종합평면도)·A3003(홀2 종합평면도)·A3004(홀3 종합평면도) 추정 + **"평면, 트렌치.dwg"(트렌치 포함 평면도)** — 부스 배치 엔진의 트렌치 실측 도면으로 직접 사용 가능 | +| `cad/kintex2_cad_all_halls.zip` | 79,617,970 B (~76MB) | `/download/2exhibition/cad_2center-6.zip` | 제2전시장 DWG 5개: "2전시장 E3-59~69 홀1층 완공평면 배치도(전체).dwg"(약 80MB, 최대 파일) · "A-3003 1층 평면도.dwg" · "KINTEX-2_FORM.dwg" · "종합-1층 평면도.dwg" · "종합-철골조 중심도.dwg" | + +**동일 파일 확인(중복 다운로드 회피):** 페이지는 홀별로 `KINTEX_cad.zip`~`KINTEX_cad5.zip`·`KINTEX_outside.zip`(제1전시장) 및 `cad_2center-6.zip`~`cad_2center-10.zip`(제2전시장) 총 11개의 개별 URL을 제공하지만, `curl -I` HEAD 검사 결과 제1전시장 6개 URL 전부 `Content-Length: 18377672`(동일), 제2전시장 5개 URL 전부 `Content-Length: 79617970`(동일)로 **완전히 동일한 zip을 반환**한다(홀별 개별 CAD가 아니라 전시장 단위 통합 CAD 패키지). 따라서 각 전시장당 1개씩만 대표 다운로드했고, 파일명을 `_all_halls`로 명명해 이 사실을 반영했다. DWG 파일명은 서버 인코딩(EUC-KR 추정)이 UTF-8로 깨져 보이나(Add-Type ZipFile 조회 시), zip 자체는 정상 압축 데이터이며 AutoCAD에서 열면 원본 한글 파일명이 정상 표시될 것으로 예상된다(미검증 — 이 환경에 CAD 뷰어 없음). + +**소계: 2개 파일, 약 93.5MB.** + +## 확인 결과: 로컬 자산 재활용 불가 + +`C:\GUARDiA\workspace\kintex\stitch_kintex_ai_system_architect\` 하위 `image_from_https_www.kintex.com_imageview.do_atchmnflno_*` 4개 폴더의 `screen.png`를 육안 확인한 결과, **전부 평면도/도면이 아니라 킨텍스 개최 행사 홍보 포스터**였다: + +| 폴더(atchmnflno_fileseq) | 실제 내용 | +|---|---| +| 469231_3 | 2026 VERNON THE 8 [V8] LIVE - GOYANG 콘서트 포스터 (KINTEX HALL 1 명시) | +| 469237_1 | 2026 P1Harmony 팬미팅 "HORROR HAVEN" 포스터 (KINTEX HALL 9B 명시) | +| 469239_2 | 코믹월드 SUMMER 2026 포스터 (일산 킨텍스 제1전시장 명시) | +| 469264_2 | Novelbright ASIA TOUR 2026 포스터 (Kintex 2 Exhibition Hall 10 명시) | + +`imageview.do?atchmnflno=...` 패턴은 kintex.com의 게시판 첨부파일(행사/공지) 뷰어이며, 이번 홀 소개 페이지들에서는 평면도 제공에 사용되지 않았다(정적 `/public/common/images/content/*.jpg` 경로 사용). 따라서 이 4개 로컬 이미지는 홀 규격 근거로 매핑하지 않았다 — 매핑 대상에서 제외. + +## 사용자 첨부 대기 자리 + +`docs/assets/floorplans/provided/` 폴더를 생성해 두었다(현재 비어 있음). 사용자가 별도로 제공할 예정인 평면도(원본 CAD, 고해상도 스캔본 등)를 이 폴더에 넣으면 된다. 파일명 컨벤션은 위 확보 이미지와 동일하게(`hallN_provided.*`) 맞추는 것을 권장. + +## 홀 규격 요약 (PLANNING §7 대조) + +| 홀 | 규격(가로×세로×높이, m) | 면적 | 하중 | 부스 수 | 바닥 | 트렌치/도면 확보 | +|---|---|---:|---|---:|---|---| +| 홀1 | 63×171×15 (Hall A 81m + Hall B 90m) | 10,611㎡ | 5t/㎡ | 600 | 콘크리트 폴리싱 | JPG 확보 + CAD 확보(트렌치 dwg 포함) | +| 홀2 | 63×171×15 | 10,773㎡ | 5t/㎡ | 600 | 콘크리트 폴리싱 | JPG 확보 + CAD 확보 | +| 홀3 | 63×171×15 | 10,773㎡ | 5t/㎡ | 600 | 콘크리트 폴리싱 | JPG 확보 + CAD 확보 | +| 홀4 | 63×171×15 | 10,773㎡ | 5t/㎡ | 600 | 콘크리트 폴리싱 | JPG 확보 (CAD는 동일 zip에 개별 dwg 미확인 — 통합 zip 안에 3개 dwg만 발견) | +| 홀5 | 63×171×15 (5A/5B) | 10,611㎡ | 5t/㎡ | 600 | 콘크리트 폴리싱 | JPG 확보 (CAD 동일) | +| 옥외전시장 | 56×52 | 2,849㎡ | 5t/㎡ | - | - | JPG 확보(위치가이드 2종), CAD는 동일 통합 zip 재사용(`KINTEX_outside.zip`도 동일 Content-Length 확인) | +| 홀6(Event Hall) | 93×60×10 (6A/6B/6C 각 31×60×10) | 5,580㎡ | **2t/㎡, 카펫 바닥** | 200 | 카펫 | JPG 확보 + CAD 확보(2전시장 통합 zip) | +| 홀7 | 126×90×12 (7A/7B 각 63×90×12) | 11,290㎡ | 5t/㎡ | 510 | 콘크리트 폴리싱 | JPG 확보 + CAD 확보 | +| 홀8 | 126×90×12 (8A/8B 각 63×90×12) | 11,290㎡ | 5t/㎡ | 510 | 콘크리트 폴리싱 | JPG 확보 + CAD 확보 | +| 홀9 | 132×99×15 (9A 66×99×15·9B 66×99×15) | 13,238㎡ | 5t/㎡ | 550 | 콘크리트 폴리싱 | JPG 확보 + CAD 확보 | +| 홀10 | 132×99×15 (10A 66×99×15·10B 66×99×15) | 13,072㎡ | 5t/㎡ | 550 | 콘크리트 폴리싱 | JPG 확보 + CAD 확보 | + +**주의:** 사용자 지시(PLANNING §7 기준)의 홀6 값(93×60×10m·2t/㎡·카펫)과 홀7/8(126×90×12m)·홀9/10(132×99×15m)은 이번 크롤링 실측치와 **정확히 일치**한다. 홀1~5의 "171×63×15m·5t/㎡·약600부스"도 실측(63×171×15m, 5t/㎡, 600부스)과 일치한다(가로/세로 표기 순서만 반대). + +## 제약 준수 확인 + +- 공개 자산만 수집. 로그인/비공개 다운로드 시도 없음. +- robots.txt 차단이나 403 응답 없음 — 전체 요청 HTTP 200. +- CAD 원본은 "무리하게 받지 말라"는 지시가 있었으나, 실측 결과 인증 없이 공개 제공됨을 확인하고 전시장당 1개(중복 제거) 대표 파일로 확보. 만약 향후 재작업 시 이 판단을 재검토하려면 이 섹션의 "동일 파일 확인" 근거를 참조. +- 파일은 저장만 하고 git 커밋하지 않음. diff --git a/docs/assets/floorplans/hall1.jpg b/docs/assets/floorplans/hall1.jpg new file mode 100644 index 0000000..b4e9953 Binary files /dev/null and b/docs/assets/floorplans/hall1.jpg differ diff --git a/docs/assets/floorplans/hall10.jpg b/docs/assets/floorplans/hall10.jpg new file mode 100644 index 0000000..d9733d5 Binary files /dev/null and b/docs/assets/floorplans/hall10.jpg differ diff --git a/docs/assets/floorplans/hall2.jpg b/docs/assets/floorplans/hall2.jpg new file mode 100644 index 0000000..d0e511b Binary files /dev/null and b/docs/assets/floorplans/hall2.jpg differ diff --git a/docs/assets/floorplans/hall3.jpg b/docs/assets/floorplans/hall3.jpg new file mode 100644 index 0000000..0ecf12c Binary files /dev/null and b/docs/assets/floorplans/hall3.jpg differ diff --git a/docs/assets/floorplans/hall4.jpg b/docs/assets/floorplans/hall4.jpg new file mode 100644 index 0000000..7559618 Binary files /dev/null and b/docs/assets/floorplans/hall4.jpg differ diff --git a/docs/assets/floorplans/hall5.jpg b/docs/assets/floorplans/hall5.jpg new file mode 100644 index 0000000..57714b0 Binary files /dev/null and b/docs/assets/floorplans/hall5.jpg differ diff --git a/docs/assets/floorplans/hall6.jpg b/docs/assets/floorplans/hall6.jpg new file mode 100644 index 0000000..d388b40 Binary files /dev/null and b/docs/assets/floorplans/hall6.jpg differ diff --git a/docs/assets/floorplans/hall7.jpg b/docs/assets/floorplans/hall7.jpg new file mode 100644 index 0000000..8d36d0c Binary files /dev/null and b/docs/assets/floorplans/hall7.jpg differ diff --git a/docs/assets/floorplans/hall8.jpg b/docs/assets/floorplans/hall8.jpg new file mode 100644 index 0000000..e7379b3 Binary files /dev/null and b/docs/assets/floorplans/hall8.jpg differ diff --git a/docs/assets/floorplans/hall9.jpg b/docs/assets/floorplans/hall9.jpg new file mode 100644 index 0000000..8322620 Binary files /dev/null and b/docs/assets/floorplans/hall9.jpg differ diff --git a/docs/assets/floorplans/hall_outdoor1.jpg b/docs/assets/floorplans/hall_outdoor1.jpg new file mode 100644 index 0000000..111056b Binary files /dev/null and b/docs/assets/floorplans/hall_outdoor1.jpg differ diff --git a/docs/assets/floorplans/hall_outdoor2.jpg b/docs/assets/floorplans/hall_outdoor2.jpg new file mode 100644 index 0000000..3a1f402 Binary files /dev/null and b/docs/assets/floorplans/hall_outdoor2.jpg differ diff --git a/docs/assets/floorplans/kintex1_overview.jpg b/docs/assets/floorplans/kintex1_overview.jpg new file mode 100644 index 0000000..a834076 Binary files /dev/null and b/docs/assets/floorplans/kintex1_overview.jpg differ diff --git a/docs/assets/floorplans/kintex2_overview.jpg b/docs/assets/floorplans/kintex2_overview.jpg new file mode 100644 index 0000000..ef45a0c Binary files /dev/null and b/docs/assets/floorplans/kintex2_overview.jpg differ diff --git a/docs/assets/floorplans/kintex_overview.png b/docs/assets/floorplans/kintex_overview.png new file mode 100644 index 0000000..97e05be Binary files /dev/null and b/docs/assets/floorplans/kintex_overview.png differ diff --git a/docs/design.md b/docs/design.md index 89a0892..bcaf8c2 100644 --- a/docs/design.md +++ b/docs/design.md @@ -1,7 +1,8 @@ # 킨텍스 AI 전시관리 시스템 — UI 디자인 스펙 (design.md) -> 작성: 디자인 에이전트(designer) · 작성일: 2026-07-11 · 버전: v1.0 -> 근거 문서: `docs/PLANNING.md` v1.0 (모듈 맵 M1~M9, P0 기능, 나노바나나 표준 샷 세트 S1~S7) +> 작성: 디자인 에이전트(designer) · 작성일: 2026-07-11 · 버전: v1.1 +> 근거 문서: `docs/PLANNING.md` v1.0 (모듈 맵 M1~M9, P0 기능, 나노바나나 표준 샷 세트 S1~S7) · Stitch 산출물 `stitch_kintex_ai_system_architect/`(18화면 + DESIGN.md 3변형) 학습·정합 +> **단일 출처(Single Source of Truth)**: 프론트 구현 토큰·컴포넌트 계약은 본 문서가 권위다. Stitch 화면(code.html/screen.png)은 시각 참조로만 사용하며, 상충 시 본 문서를 따른다. > 용도: **각 화면의 "Stitch 프롬프트" 코드블록을 Google Stitch(stitch.withgoogle.com)에 그대로 붙여넣어 화면을 생성**한다. 문서는 한국어, Stitch 프롬프트만 영어. --- @@ -38,12 +39,16 @@ 명도 대비는 WCAG AA 이상: 본문 텍스트는 `neutral-700` 이상, 컬러 배경 위 텍스트는 흰색(`#FFFFFF`) 고정. `warning`·`error`는 텍스트 사용 시 위 표의 진한 값만 사용. +> **Stitch 실증 정합(v1.1)**: Stitch가 생성한 3개 디자인 변형 중 **Precision Enterprise AI** 변형이 위 토큰을 그대로 검증했다 — `primary-container #0066B3`(우리 `primary-600`), 캔버스 `#1C2536`, AI 액센트 `#6D4AFF`, `error #D92D20`, 배경 `#F9FAFB`, 본문 `#101828`, 카드 테두리 `#E4E7EC`가 실제 code.html(`booth_layout_editor` 등 시공 화면 전체)에 반영됨. StatusBadge 진행(회색→파랑→**보라(AI)**→녹색→빨강)도 동일. 나머지 2개 변형(Intelligence·Nexus)은 더 짙은 헤리티지 블루 `#0059A6`/`#00427D`를 primary로 채택했는데, 이는 우리 `primary-700 #004C86`보다 약간 짙다. **결론: 우리 `docs/design.md`가 프론트 구현의 단일 출처이며 `#0066B3`를 주 브랜드 블루로 유지한다.** `#0059A6`·`#00427D`는 헤더/hover의 짙은 대체 톤으로만 허용(신규 토큰 추가 금지). Intelligence 변형의 `ai-surface #F5F3FF`(옅은 보라 배경)는 AI 구동 섹션 배경으로 채택 가능 — 우리 `primary-050`과 별개로 AI 카드 배경에 한해 사용. + ### 1-3. 타이포그래피 (한국어) - 서체: **Pretendard** (fallback: Noto Sans KR, system-ui). 숫자·좌표·견적 금액은 tabular numerals. - 스케일: Display 28/36 Bold(대시보드 KPI) · H1 24/32 Bold · H2 20/28 SemiBold · H3 16/24 SemiBold · Body 14/22 Regular · Caption 12/18 Regular · Mono 13(좌표·치수·스키마 값, JetBrains Mono). - B2B 밀도: 기본 Body 14px, 테이블 행 높이 44px(모바일 터치 타깃 48px). +> **Stitch 실증 정합(v1.1)**: 제품 UI 텍스트는 한국어이므로 **Pretendard가 권위 서체(단일 출처)**로 유지된다. Stitch는 라틴 문자 대체로 **Hanken Grotesk**(Intelligence·Nexus 변형) / **Inter**(Precision 변형)를 선택하고 Pretendard를 한글 fallback으로 지정했다 — 실제 렌더 화면과의 라틴·숫자 자형 일관성을 원할 경우 Pretendard 로드 실패 시 **Inter를 라틴/tabular-numeral 보조 서체**로 허용(별도 브랜드 결정 없이는 Pretendard 우선). 모노 서체는 우리 JetBrains Mono를 유지(Stitch 변형별 Geist/Courier Prime은 채택하지 않음). Stitch가 base body 14px·Bold 600/700 강조·label 12px 트래킹을 확정한 점은 우리 스케일과 일치한다. 단 Stitch 데이터 리스트 행 높이는 40px였으나, 우리는 접근성·터치 타깃 위해 **44px(웹)/48px(모바일)를 권위값**으로 유지한다. + ### 1-4. 컴포넌트 원칙 - **상태 배지(StatusBadge)**: 신청 흐름 `작성중(회색)→제출됨(파랑)→AI검토(보라)→승인(녹색)→반려(빨강)→시공중→검수완료`. 모든 목록·카드에 동일 배지 사용. @@ -53,6 +58,13 @@ - **캔버스 화면 공통**: 좌측 도구 패널(240px) + 중앙 캔버스 + 우측 속성/견적 패널(320~360px) 3열. 캔버스 하단에 줌 컨트롤·축척·좌표 표시. - 로딩: 이미지 생성은 비동기(RenderJob 큐) — 스켈레톤 + 진행 배지("생성 중… 평균 40초"), 완료 시 WebSocket 푸시로 교체. 실패 시 카드 내 재시도 버튼. +**라운드·엘리베이션·그리드 토큰 (Stitch 3변형 만장일치 — v1.1 신설, 이전 스펙에 누락됐던 항목)** + +- **라운드(radius)**: 버튼·인풋·작은 요소 `4px(0.25rem)` · 카드·모달·섹션 블록 `8px(0.5rem)` · 상태/AI 칩은 pill(`9999px`). 산업·건축 톤 유지 위해 **8px를 최대 라운드로 제한**(그 이상 금지). +- **엘리베이션**: 무거운 그림자 대신 **톤 레이어 + 1px 테두리**(`neutral-200 #E4E7EC`). Level 0 배경 → Level 1 카드(흰색+1px 테두리, 그림자 없음) → Level 2 모달·드롭다운(소프트 앰비언트 그림자 Y4 Blur12 10% + backdrop-blur 8px). **AI 구동 카드는 좌측 4px 보라(`ai-accent`) 테두리 액센트**로 사람 산출물과 구분(상태 카드는 좌측 4px 상태색 액센트). +- **그리드·간격**: 12컬럼 · 컨테이너 최대 `1440px` · 4px 베이스 · 거터 24px · 데스크톱 페이지 마진 48px(태블릿 32px, 모바일 16px). 속성/견적 패널 등 고밀도 영역 패딩은 8~12px로 타이트하게. +- **데이터 테이블**: 긴 목록은 지브라 스트라이프(홀수행 `neutral-050 #F9FAFB`), 헤더는 sticky + `label` 타이포 + 옅은 회색 배경, 행 hover `#F1F5F9`. 복잡 테이블은 컬럼 숨김보다 가로 스크롤 우선. + ### 1-5. 반응형 기준·다크모드 - **웹 1440px**(설계·에디터·대시보드 기준 해상도) / **모바일 390px**(조회·승인·현장 시나리오). 태블릿은 웹 레이아웃 축소 대응. @@ -107,6 +119,44 @@ --- +## 2.5 Stitch 산출물 ↔ SCR 매핑 (구현 이식 기준) + +`stitch_kintex_ai_system_architect/`의 18개 Stitch 생성 화면 중 **우리 P0/P1 범위(부스 시공 중심)에 해당하는 14개**를 아래 SCR과 1:1로 매핑한다. **kintex-frontend-dev는 이식 시 이 표를 권위로 따른다** — Stitch 화면의 시각 언어(Precision Enterprise AI 팔레트 `#0066B3`/`#1C2536`/`#6D4AFF`)를 참조하되, 상충 시 본 `docs/design.md`가 단일 출처다. + +| Stitch 화면 디렉터리 | SCR ID | 화면명 | 대상 역할 | 플랫폼 | +|---|---|---|---|---| +| `login_workspace_selection_kintex_ai_system` | SCR-01 | 로그인 & 행사 워크스페이스 선택 | 공통 | 웹 | +| `organizer_dashboard_kintex_ai_system` | SCR-02 | 주최자 대시보드 | 주최자 | 웹 | +| `booth_layout_editor_kintex_ai_system` | SCR-03 | 부스 배치 에디터 | 주최자 | 웹 | +| `layout_comparison_kintex_ai_system` | SCR-04 | 배치안 비교(S7 조감) | 주최자 | 웹 | +| `exhibitor_home_kintex_ai_system` | SCR-05 | 참가업체 부스 홈 | 참가업체 | 웹 | +| `booth_design_studio_kintex_ai_system` | SCR-06 | 부스 설계 스튜디오 | 참가·장치업체 | 웹 | +| `utility_wiring_view_kintex_ai_system` | SCR-07 | 유틸리티 배선 뷰 | 참가·장치업체 | 웹 | +| `utility_order_summary_kintex_ai_system` | SCR-08 | 유틸리티 신청 요약·위치표시도 | 참가업체 | 웹 | +| `compliance_report_kintex_ai_system` | SCR-09 | 규정 검증 리포트 | 장치업체 | 웹 | +| `manager_approval_queue_kintex_ai_system` | SCR-10 | 홀매니저 승인 큐 | 홀매니저 | 웹 | +| `review_details_kintex_ai_system` | SCR-11 | 홀매니저 검수 상세 | 홀매니저 | 웹 | +| `visualization_gallery_kintex_ai_system` | SCR-12 | 시각화 갤러리 | 전 역할 | 웹 | +| `contractor_on_site_checklist_kintex_ai_system` | SCR-M1 | 시공업체 현장 체크리스트 | 장치업체 현장 | 모바일 | +| `manager_field_inspection_kintex_ai_system` | SCR-M2 | 홀매니저 현장 검수 | 홀매니저 | 모바일 | + +각 SCR의 상세 레이아웃·상태·Stitch 프롬프트는 아래 §3(웹)·§4(모바일)에 정의돼 있다. Stitch 화면은 시각 참조(screen.png)로만 사용하고, 프론트 토큰·컴포넌트 계약은 본 문서를 따른다. + +## 2.6 범위 게이트 — P2 후보 (기획 반영 전 이식 금지) + +`docs/PLANNING.md`가 **범위 권위**다(부스 신청→설계→유틸리티→규정검증→승인→시공→검수 파이프라인 중심). Stitch `kintex_ai_system_proposal.md`와 아래 4개 Stitch 화면은 PLANNING 범위 밖 비전(AI 컨시어지·CCTV 혼잡도·에너지 최적화·비즈니스 매칭·AR 내비 등)이므로 **P2 후보로만 분류**한다. **P0/P1으로 끌어오지 않으며, 승격하려면 먼저 planner에 기획 반영을 요청해야 한다(designer 임의 승격 금지).** + +| Stitch 화면 디렉터리 | 성격 | PLANNING 근거 | 처리 | +|---|---|---|---| +| `business_intelligence_kintex_ai_system` | 비즈니스 매칭·성과 분석·트렌드 리포트 | 범위 밖(참가업체 매칭·바이어 분석은 부스 시공 파이프라인 아님) | P2 후보 — 기획 반영 필요 | +| `hall_operations_kintex_ai_system` | 실시간 혼잡도·에너지·주차 예측(CCTV/센서) | 범위 밖(현장 운영 IoT/CV, PLANNING 미정의) | P2 후보 — 기획 반영 필요 | +| `exhibition_schedule_kintex_ai_system` | 전시 세션/일정 편성·개인화 추천 | 범위 밖(방문객 대상 서비스) | P2 후보 — 기획 반영 필요 | +| `admin_dashboard_kintex_ai_system` | 플랫폼 전역 관리자(시스템 운영) | 범위 밖(Phase 1 역할=주최자/참가/장치/홀매니저에 미포함) | P2 후보 — 기획 반영 필요 | + +> 위 4개 Stitch 화면의 시각 자산은 참고만 하고 프론트 구현에 착수하지 않는다. `kintex_ai_system_proposal.md`는 Stitch가 자동 생성한 확장 비전 문서로, 우리 PLANNING과 별개다. + +--- + ## 3. 화면별 스펙 > P0 모듈(M2·M3·M4·M5) 중심 총 14개 화면(웹 12 + 모바일 2). 각 Stitch 프롬프트는 화면 1개를 1회 생성하는 기준으로 작성했다. @@ -379,3 +429,4 @@ A mobile field inspection screen for a KINTEX hall manager verifying booth const |---|---|---|---| | v1.0 | 2026-07-11 | designer | 최초 작성 — PLANNING.md v1.0 기반. 디자인 시스템·IA·P0 중심 14화면(웹 12 + 모바일 2) Stitch 프롬프트 포함. 다크모드는 Phase 1 미지원(캔버스 다크 서피스만) 결정 | | v1.0.1 | 2026-07-11 | reviewer 반영 | 화면 수 표기 정정(13→14). 잔여 지적사항은 docs/BACKLOG.md 참조 | +| v1.1 | 2026-07-11 | designer | **Stitch 산출물 학습·정합화.** `stitch_kintex_ai_system_architect/` 18화면 code.html + DESIGN.md 3변형(Intelligence·Nexus·Precision) 학습. ①§1-2 컬러: Precision Enterprise AI 변형이 우리 토큰(#0066B3·#1C2536·#6D4AFF·#D92D20·#F9FAFB·#101828·StatusBadge 진행) 실증 확인 — 우리 팔레트를 단일 출처로 유지, #0059A6·#00427D는 짙은 대체 톤으로만 허용, ai-surface #F5F3FF AI 카드 배경 채택. ②§1-3 타이포: Pretendard 권위 유지, Inter를 라틴/tabular 보조로 허용(Stitch가 Hanken Grotesk/Inter 채택), 행 높이 44/48px 권위값 유지. ③§1-4 라운드(4/8px·pill)·엘리베이션(톤레이어+1px 테두리+AI 좌측 보라 액센트)·그리드(12컬럼·1440px·4px 베이스)·지브라 테이블 토큰 신설(이전 누락분). ④§2.5 Stitch 화면↔SCR 매핑표 신설(14화면). ⑤§2.6 범위 게이트 — proposal.md 및 범위 밖 4화면(business_intelligence·hall_operations·exhibition_schedule·admin_dashboard)을 P2 후보로 분류(PLANNING이 범위 권위, designer 임의 승격 금지). PLANNING.md·타 문서 무수정 | diff --git a/src/backend/.gitignore b/src/backend/.gitignore new file mode 100644 index 0000000..10eaab7 --- /dev/null +++ b/src/backend/.gitignore @@ -0,0 +1,13 @@ +# Gradle +.gradle/ +build/ +!gradle/wrapper/gradle-wrapper.jar + +# IDE +.idea/ +*.iml +bin/ + +# Logs / env +*.log +.env diff --git a/src/backend/build.gradle b/src/backend/build.gradle new file mode 100644 index 0000000..6eae62c --- /dev/null +++ b/src/backend/build.gradle @@ -0,0 +1,59 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '3.2.5' + id 'io.spring.dependency-management' version '1.1.4' +} + +group = 'com.zioinfo.kintex' +version = '0.1.0-SNAPSHOT' + +java { + sourceCompatibility = JavaLanguageVersion.of(17) + targetCompatibility = JavaLanguageVersion.of(17) +} + +// 한글 문자열 리터럴 손상 방지 — Windows javac 기본 인코딩(CP949) 대신 UTF-8 강제. +tasks.withType(JavaCompile).configureEach { + options.encoding = 'UTF-8' +} + +repositories { + mavenCentral() +} + +ext { + mybatisStarterVersion = '3.0.3' // Spring Boot 3.2.x 호환 + jjwtVersion = '0.12.5' +} + +dependencies { + // --- Web / REST + WebSocket(STOMP) 실시간 알림(RenderJob 완료·승인 이벤트) --- + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-websocket' + implementation 'org.springframework.boot:spring-boot-starter-validation' + implementation 'org.springframework.boot:spring-boot-starter-security' + + // --- MyBatis + PostgreSQL(PostGIS) — 공간 SQL은 매퍼 XML(kintex-db-engineer 공유) --- + implementation "org.mybatis.spring.boot:mybatis-spring-boot-starter:${mybatisStarterVersion}" + runtimeOnly 'org.postgresql:postgresql' + + // --- Redis 작업 큐 (RenderJob·서류 생성·알림 발행) --- + implementation 'org.springframework.boot:spring-boot-starter-data-redis' + + // --- JWT (행사 단위 RBAC 인증) --- + implementation "io.jsonwebtoken:jjwt-api:${jjwtVersion}" + runtimeOnly "io.jsonwebtoken:jjwt-impl:${jjwtVersion}" + runtimeOnly "io.jsonwebtoken:jjwt-jackson:${jjwtVersion}" + + // --- Lombok --- + compileOnly 'org.projectlombok:lombok' + annotationProcessor 'org.projectlombok:lombok' + + // --- Test --- + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'org.springframework.security:spring-security-test' +} + +tasks.named('test') { + useJUnitPlatform() +} diff --git a/src/backend/gradle/wrapper/gradle-wrapper.jar b/src/backend/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..249e583 Binary files /dev/null and b/src/backend/gradle/wrapper/gradle-wrapper.jar differ diff --git a/src/backend/gradle/wrapper/gradle-wrapper.properties b/src/backend/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..48c0a02 --- /dev/null +++ b/src/backend/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/src/backend/gradlew b/src/backend/gradlew new file mode 100644 index 0000000..a69d9cb --- /dev/null +++ b/src/backend/gradlew @@ -0,0 +1,240 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/src/backend/gradlew.bat b/src/backend/gradlew.bat new file mode 100644 index 0000000..f127cfd --- /dev/null +++ b/src/backend/gradlew.bat @@ -0,0 +1,91 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/src/backend/settings.gradle b/src/backend/settings.gradle new file mode 100644 index 0000000..f7345de --- /dev/null +++ b/src/backend/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'kintex-backend' diff --git a/src/backend/src/main/java/com/zioinfo/kintex/KintexApplication.java b/src/backend/src/main/java/com/zioinfo/kintex/KintexApplication.java new file mode 100644 index 0000000..c5bedd1 --- /dev/null +++ b/src/backend/src/main/java/com/zioinfo/kintex/KintexApplication.java @@ -0,0 +1,23 @@ +package com.zioinfo.kintex; + +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.context.properties.ConfigurationPropertiesScan; + +/** + * 킨텍스 AI 전시관리 시스템 백엔드 진입점. + * + * 스택: Spring Boot 3.x(Java 17) + MyBatis + PostgreSQL(PostGIS) + Redis 작업 큐 + WebSocket(STOMP). + * 매퍼 인터페이스는 {@code com.zioinfo.kintex..mapper} 패키지에서 스캔 — 실제 공간 SQL(ST_*) XML은 + * kintex-db-engineer가 {@code resources/mybatis/mapper/} 에 채운다. + */ +@SpringBootApplication +@ConfigurationPropertiesScan("com.zioinfo.kintex") +@MapperScan(annotationClass = org.apache.ibatis.annotations.Mapper.class, basePackages = "com.zioinfo.kintex") +public class KintexApplication { + + public static void main(String[] args) { + SpringApplication.run(KintexApplication.class, args); + } +} diff --git a/src/backend/src/main/java/com/zioinfo/kintex/auth/AuthController.java b/src/backend/src/main/java/com/zioinfo/kintex/auth/AuthController.java new file mode 100644 index 0000000..64f93a9 --- /dev/null +++ b/src/backend/src/main/java/com/zioinfo/kintex/auth/AuthController.java @@ -0,0 +1,54 @@ +package com.zioinfo.kintex.auth; + +import com.zioinfo.kintex.auth.dto.AcceptInviteRequest; +import com.zioinfo.kintex.auth.dto.LoginRequest; +import com.zioinfo.kintex.auth.dto.LoginResponse; +import com.zioinfo.kintex.auth.dto.WorkspaceDto; +import com.zioinfo.kintex.common.ApiResponse; +import jakarta.validation.Valid; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** 인증·워크스페이스 API (C-1 / SCR-01). */ +@RestController +@RequestMapping("/api/auth") +public class AuthController { + + private final AuthService authService; + private final EventAccessGuard guard; + + public AuthController(AuthService authService, EventAccessGuard guard) { + this.authService = authService; + this.guard = guard; + } + + /** POST /api/auth/login — 이메일/비밀번호 로그인 → 토큰 + 워크스페이스. */ + @PostMapping("/login") + public ApiResponse login(@Valid @RequestBody LoginRequest request) { + return ApiResponse.ok(authService.login(request)); + } + + /** GET /api/auth/workspaces — 내 참여 행사 목록(SCR-01 카드 그리드). */ + @GetMapping("/workspaces") + public ApiResponse> workspaces(@AuthenticationPrincipal KintexPrincipal principal) { + guard.require(principal); + return ApiResponse.ok(authService.listWorkspaces(principal)); + } + + /** POST /api/auth/accept-invite — 초대 수락(장치업체는 등록업체 검증). */ + @PostMapping("/accept-invite") + public ApiResponse acceptInvite(@AuthenticationPrincipal KintexPrincipal principal, + @Valid @RequestBody AcceptInviteRequest request) { + guard.require(principal); + return ApiResponse.ok(authService.acceptInvite(principal, request)); + } + + /** GET /api/auth/me — 현재 토큰의 신원·행사별 역할 요약(디버그·프론트 부트스트랩). */ + @GetMapping("/me") + public ApiResponse me(@AuthenticationPrincipal KintexPrincipal principal) { + guard.require(principal); + return ApiResponse.ok(principal); + } +} diff --git a/src/backend/src/main/java/com/zioinfo/kintex/auth/AuthProperties.java b/src/backend/src/main/java/com/zioinfo/kintex/auth/AuthProperties.java new file mode 100644 index 0000000..8e2e467 --- /dev/null +++ b/src/backend/src/main/java/com/zioinfo/kintex/auth/AuthProperties.java @@ -0,0 +1,30 @@ +package com.zioinfo.kintex.auth; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** kintex.auth.* 바인딩 — JWT 시크릿/TTL은 환경변수에서만 주입(하드코딩 금지). */ +@ConfigurationProperties(prefix = "kintex.auth") +public class AuthProperties { + + /** HS256 서명 키. 최소 32바이트. env JWT_SECRET 로 주입. */ + private String jwtSecret; + + /** 액세스 토큰 유효기간(초). */ + private long accessTokenTtlSeconds = 3600; + + public String getJwtSecret() { + return jwtSecret; + } + + public void setJwtSecret(String jwtSecret) { + this.jwtSecret = jwtSecret; + } + + public long getAccessTokenTtlSeconds() { + return accessTokenTtlSeconds; + } + + public void setAccessTokenTtlSeconds(long accessTokenTtlSeconds) { + this.accessTokenTtlSeconds = accessTokenTtlSeconds; + } +} diff --git a/src/backend/src/main/java/com/zioinfo/kintex/auth/AuthService.java b/src/backend/src/main/java/com/zioinfo/kintex/auth/AuthService.java new file mode 100644 index 0000000..e6318a0 --- /dev/null +++ b/src/backend/src/main/java/com/zioinfo/kintex/auth/AuthService.java @@ -0,0 +1,24 @@ +package com.zioinfo.kintex.auth; + +import com.zioinfo.kintex.auth.dto.AcceptInviteRequest; +import com.zioinfo.kintex.auth.dto.LoginRequest; +import com.zioinfo.kintex.auth.dto.LoginResponse; +import com.zioinfo.kintex.auth.dto.WorkspaceDto; + +import java.util.List; + +/** 인증·워크스페이스 서비스 계약. */ +public interface AuthService { + + /** 이메일/비밀번호 검증 → JWT 발급 + 참여 행사 목록. */ + LoginResponse login(LoginRequest request); + + /** 현재 사용자의 참여 행사 워크스페이스 목록(SCR-01 카드 그리드). */ + List listWorkspaces(KintexPrincipal principal); + + /** + * 초대 코드 수락. 장치업체 수락 시 킨텍스 등록업체 검증(미등록 차단). + * @return 갱신된 로그인 응답(역할이 추가된 새 토큰 포함). + */ + LoginResponse acceptInvite(KintexPrincipal principal, AcceptInviteRequest request); +} diff --git a/src/backend/src/main/java/com/zioinfo/kintex/auth/AuthServiceImpl.java b/src/backend/src/main/java/com/zioinfo/kintex/auth/AuthServiceImpl.java new file mode 100644 index 0000000..5fe3d22 --- /dev/null +++ b/src/backend/src/main/java/com/zioinfo/kintex/auth/AuthServiceImpl.java @@ -0,0 +1,58 @@ +package com.zioinfo.kintex.auth; + +import com.zioinfo.kintex.auth.dto.AcceptInviteRequest; +import com.zioinfo.kintex.auth.dto.LoginRequest; +import com.zioinfo.kintex.auth.dto.LoginResponse; +import com.zioinfo.kintex.auth.dto.WorkspaceDto; +import com.zioinfo.kintex.auth.mapper.UserMapper; +import com.zioinfo.kintex.common.error.ApiException; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * 인증 서비스 골격. JWT 발급/검증 경로({@link JwtService})는 완성되어 있으나, + * 사용자·멤버십 조회는 {@link UserMapper} XML(kintex-db-engineer) 구현 대기 → 501 반환. + * + *

보안: 비밀번호 검증은 저장된 해시와 대조(BCrypt 등)하며, 응답에 해시·내부 식별자 미노출. + */ +@Service +public class AuthServiceImpl implements AuthService { + + private final JwtService jwtService; + private final UserMapper userMapper; + + public AuthServiceImpl(JwtService jwtService, UserMapper userMapper) { + this.jwtService = jwtService; + this.userMapper = userMapper; + } + + @Override + public LoginResponse login(LoginRequest request) { + // TODO(kintex-db-engineer): UserMapper.findAuthByEmail + 비밀번호 해시 검증 후 + // JwtService.issue(userId, name, eventRoles, hallManager) 로 토큰 발급. + // 구현 예시 흐름: + // var row = userMapper.findAuthByEmail(request.email()); + // if (row == null || !passwordEncoder.matches(request.password(), (String) row.get("passwordHash"))) + // throw new ApiException(ErrorCode.UNAUTHORIZED, "이메일 또는 비밀번호가 올바르지 않습니다."); + // ... eventRoles = userMapper.findEventRoles(userId) 조립 ... + // token = jwtService.issue(...); + throw ApiException.notImplemented("로그인(사용자 인증 조회)"); + } + + @Override + public List listWorkspaces(KintexPrincipal principal) { + // TODO(kintex-db-engineer): principal.eventRoles 기반 행사 카드(행사명·기간·홀·D-데이) 조회. + throw ApiException.notImplemented("워크스페이스 목록 조회"); + } + + @Override + public LoginResponse acceptInvite(KintexPrincipal principal, AcceptInviteRequest request) { + // TODO(kintex-db-engineer): + // 1) inviteCode 로 초대(행사·부스·역할) 조회 + // 2) 역할이 CONTRACTOR면 companyRegistrationNo 로 등록업체 검증 — + // 미등록 시 throw new ApiException(ErrorCode.NOT_REGISTERED_COMPANY) + // 3) 멤버십 upsert 후 갱신된 eventRoles 로 새 토큰 발급(jwtService.issue) + throw ApiException.notImplemented("초대 코드 수락(등록업체 검증 포함)"); + } +} diff --git a/src/backend/src/main/java/com/zioinfo/kintex/auth/EventAccessGuard.java b/src/backend/src/main/java/com/zioinfo/kintex/auth/EventAccessGuard.java new file mode 100644 index 0000000..45870ab --- /dev/null +++ b/src/backend/src/main/java/com/zioinfo/kintex/auth/EventAccessGuard.java @@ -0,0 +1,43 @@ +package com.zioinfo.kintex.auth; + +import com.zioinfo.kintex.common.error.ApiException; +import com.zioinfo.kintex.common.error.ErrorCode; +import org.springframework.stereotype.Component; + +import java.util.Set; + +/** + * 행사 단위 RBAC 접근 가드. 컨트롤러가 도메인 액션 전에 호출한다. + * 홀매니저(킨텍스 내부)는 전 행사 열람+승인이 허용된다. + */ +@Component +public class EventAccessGuard { + + /** 인증 필수 — principal 없으면 401. */ + public KintexPrincipal require(KintexPrincipal principal) { + if (principal == null) { + throw new ApiException(ErrorCode.UNAUTHORIZED); + } + return principal; + } + + /** 해당 행사 접근 권한 확인(멤버 또는 홀매니저). 없으면 403. */ + public KintexPrincipal requireEventAccess(KintexPrincipal principal, String eventId) { + require(principal); + if (!principal.hasAccess(eventId)) { + throw new ApiException(ErrorCode.FORBIDDEN); + } + return principal; + } + + /** 해당 행사에서 허용된 역할 중 하나여야 함. 없으면 403. */ + public KintexPrincipal requireRole(KintexPrincipal principal, String eventId, EventRole... allowed) { + requireEventAccess(principal, eventId); + EventRole role = principal.roleFor(eventId); + Set allowedSet = Set.of(allowed); + if (role == null || !allowedSet.contains(role)) { + throw new ApiException(ErrorCode.FORBIDDEN); + } + return principal; + } +} diff --git a/src/backend/src/main/java/com/zioinfo/kintex/auth/EventRole.java b/src/backend/src/main/java/com/zioinfo/kintex/auth/EventRole.java new file mode 100644 index 0000000..502c6eb --- /dev/null +++ b/src/backend/src/main/java/com/zioinfo/kintex/auth/EventRole.java @@ -0,0 +1,17 @@ +package com.zioinfo.kintex.auth; + +/** + * 행사(Event) 단위 RBAC 역할 (PLANNING §2 권한 모델). + *

    + *
  • {@link #ORGANIZER} 주최자 — 행사 owner(플로어플랜·참가업체 관리)
  • + *
  • {@link #EXHIBITOR} 참가업체 — 부스 단위 멤버(설계·유틸리티 신청)
  • + *
  • {@link #CONTRACTOR} 장치·시공업체 — 참가업체가 초대(등록업체만)
  • + *
  • {@link #HALL_MANAGER} 킨텍스 홀매니저 — 내부 계정, 전체 열람 + 승인
  • + *
+ */ +public enum EventRole { + ORGANIZER, + EXHIBITOR, + CONTRACTOR, + HALL_MANAGER +} diff --git a/src/backend/src/main/java/com/zioinfo/kintex/auth/JwtAuthenticationFilter.java b/src/backend/src/main/java/com/zioinfo/kintex/auth/JwtAuthenticationFilter.java new file mode 100644 index 0000000..9d52465 --- /dev/null +++ b/src/backend/src/main/java/com/zioinfo/kintex/auth/JwtAuthenticationFilter.java @@ -0,0 +1,47 @@ +package com.zioinfo.kintex.auth; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; +import java.util.List; + +/** + * Bearer JWT → SecurityContext 인증 세팅. 토큰이 없거나 유효하지 않으면 인증 미설정(익명)으로 통과시키고, + * 실제 접근 제어는 SecurityConfig(인증 필요 경로)와 각 컨트롤러의 행사 RBAC 가드가 담당한다. + */ +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + private final JwtService jwtService; + + public JwtAuthenticationFilter(JwtService jwtService) { + this.jwtService = jwtService; + } + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, + FilterChain chain) throws ServletException, IOException { + String header = request.getHeader("Authorization"); + if (header != null && header.startsWith("Bearer ")) { + String token = header.substring(7); + try { + KintexPrincipal principal = jwtService.verify(token); + var authority = new SimpleGrantedAuthority( + principal.hallManager() ? "ROLE_HALL_MANAGER" : "ROLE_USER"); + var auth = new UsernamePasswordAuthenticationToken( + principal, null, List.of(authority)); + SecurityContextHolder.getContext().setAuthentication(auth); + } catch (Exception ignored) { + // 유효하지 않은 토큰 → 익명 통과. 보호 경로에서 401 처리. + SecurityContextHolder.clearContext(); + } + } + chain.doFilter(request, response); + } +} diff --git a/src/backend/src/main/java/com/zioinfo/kintex/auth/JwtService.java b/src/backend/src/main/java/com/zioinfo/kintex/auth/JwtService.java new file mode 100644 index 0000000..566d45c --- /dev/null +++ b/src/backend/src/main/java/com/zioinfo/kintex/auth/JwtService.java @@ -0,0 +1,74 @@ +package com.zioinfo.kintex.auth; + +import com.zioinfo.kintex.common.error.ApiException; +import com.zioinfo.kintex.common.error.ErrorCode; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; +import org.springframework.stereotype.Service; + +import javax.crypto.SecretKey; +import java.nio.charset.StandardCharsets; +import java.time.Instant; +import java.util.Date; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * JWT 발급/검증 — HS256. 시크릿은 {@link AuthProperties}(env JWT_SECRET)에서만 로드. + * 토큰은 사용자 신원 + 행사별 역할 맵을 담는다(행사 단위 RBAC). + */ +@Service +public class JwtService { + + private final SecretKey key; + private final long ttlSeconds; + + public JwtService(AuthProperties props) { + this.key = Keys.hmacShaKeyFor(props.getJwtSecret().getBytes(StandardCharsets.UTF_8)); + this.ttlSeconds = props.getAccessTokenTtlSeconds(); + } + + /** 액세스 토큰 발급. eventRoles: 행사ID→역할. */ + public String issue(String userId, String displayName, + Map eventRoles, boolean hallManager) { + Instant now = Instant.now(); + Map rolesClaim = new LinkedHashMap<>(); + if (eventRoles != null) { + eventRoles.forEach((eid, role) -> rolesClaim.put(eid, role.name())); + } + return Jwts.builder() + .subject(userId) + .claim("name", displayName) + .claim("roles", rolesClaim) + .claim("hm", hallManager) + .issuedAt(Date.from(now)) + .expiration(Date.from(now.plusSeconds(ttlSeconds))) + .signWith(key) + .compact(); + } + + /** 토큰 검증 후 principal 복원. 실패 시 401. */ + @SuppressWarnings("unchecked") + public KintexPrincipal verify(String token) { + try { + Claims c = Jwts.parser().verifyWith(key).build() + .parseSignedClaims(token).getPayload(); + Map roles = new HashMap<>(); + Object raw = c.get("roles"); + if (raw instanceof Map m) { + m.forEach((k, v) -> roles.put(String.valueOf(k), EventRole.valueOf(String.valueOf(v)))); + } + boolean hm = Boolean.TRUE.equals(c.get("hm", Boolean.class)); + return new KintexPrincipal(c.getSubject(), c.get("name", String.class), roles, hm); + } catch (JwtException | IllegalArgumentException e) { + throw new ApiException(ErrorCode.UNAUTHORIZED, "토큰이 유효하지 않거나 만료되었습니다."); + } + } + + public long ttlSeconds() { + return ttlSeconds; + } +} diff --git a/src/backend/src/main/java/com/zioinfo/kintex/auth/KintexPrincipal.java b/src/backend/src/main/java/com/zioinfo/kintex/auth/KintexPrincipal.java new file mode 100644 index 0000000..8fbe4c4 --- /dev/null +++ b/src/backend/src/main/java/com/zioinfo/kintex/auth/KintexPrincipal.java @@ -0,0 +1,31 @@ +package com.zioinfo.kintex.auth; + +import java.util.Map; + +/** + * 인증 주체 — JWT에서 복원되는 사용자 신원 + 행사별 역할 맵. + * + * @param userId 사용자 식별자 + * @param displayName 표시 이름 + * @param eventRoles 행사ID → 역할 (행사 단위 RBAC). 홀매니저는 전체 열람이므로 별도 플래그. + * @param hallManager 킨텍스 내부 홀매니저 여부(전체 행사 열람+승인) + */ +public record KintexPrincipal( + String userId, + String displayName, + Map eventRoles, + boolean hallManager +) { + + /** 해당 행사에서 사용자의 역할(없으면 null). 홀매니저는 항상 HALL_MANAGER로 간주. */ + public EventRole roleFor(String eventId) { + if (hallManager) { + return EventRole.HALL_MANAGER; + } + return eventRoles == null ? null : eventRoles.get(eventId); + } + + public boolean hasAccess(String eventId) { + return hallManager || (eventRoles != null && eventRoles.containsKey(eventId)); + } +} diff --git a/src/backend/src/main/java/com/zioinfo/kintex/auth/dto/AcceptInviteRequest.java b/src/backend/src/main/java/com/zioinfo/kintex/auth/dto/AcceptInviteRequest.java new file mode 100644 index 0000000..e1c9a75 --- /dev/null +++ b/src/backend/src/main/java/com/zioinfo/kintex/auth/dto/AcceptInviteRequest.java @@ -0,0 +1,14 @@ +package com.zioinfo.kintex.auth.dto; + +import jakarta.validation.constraints.NotBlank; + +/** + * 초대 코드 수락(SCR-01). 장치업체(CONTRACTOR) 수락 시 킨텍스 등록업체 검증 — + * 미등록 업체는 NOT_REGISTERED_COMPANY(403)로 차단한다(PLANNING §2·M7). + */ +public record AcceptInviteRequest( + @NotBlank String inviteCode, + /** 장치업체 초대 수락 시 등록업체 사업자번호(검증용). 참가업체 초대는 null 허용. */ + String companyRegistrationNo +) { +} diff --git a/src/backend/src/main/java/com/zioinfo/kintex/auth/dto/LoginRequest.java b/src/backend/src/main/java/com/zioinfo/kintex/auth/dto/LoginRequest.java new file mode 100644 index 0000000..0bf2dfb --- /dev/null +++ b/src/backend/src/main/java/com/zioinfo/kintex/auth/dto/LoginRequest.java @@ -0,0 +1,11 @@ +package com.zioinfo.kintex.auth.dto; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; + +/** 이메일/비밀번호 로그인 요청(SCR-01). 킨텍스 내부 계정 SSO는 별도 경로. */ +public record LoginRequest( + @NotBlank @Email String email, + @NotBlank String password +) { +} diff --git a/src/backend/src/main/java/com/zioinfo/kintex/auth/dto/LoginResponse.java b/src/backend/src/main/java/com/zioinfo/kintex/auth/dto/LoginResponse.java new file mode 100644 index 0000000..d715941 --- /dev/null +++ b/src/backend/src/main/java/com/zioinfo/kintex/auth/dto/LoginResponse.java @@ -0,0 +1,17 @@ +package com.zioinfo.kintex.auth.dto; + +import java.util.List; + +/** + * 로그인 응답 — 액세스 토큰 + 참여 행사 워크스페이스 목록(SCR-01 카드 그리드). + * 민감정보(비밀번호 해시·내부 식별자 등)는 절대 포함하지 않는다. + */ +public record LoginResponse( + String accessToken, + long expiresInSeconds, + UserSummary user, + List workspaces +) { + public record UserSummary(String userId, String displayName, boolean hallManager) { + } +} diff --git a/src/backend/src/main/java/com/zioinfo/kintex/auth/dto/WorkspaceDto.java b/src/backend/src/main/java/com/zioinfo/kintex/auth/dto/WorkspaceDto.java new file mode 100644 index 0000000..3feaee0 --- /dev/null +++ b/src/backend/src/main/java/com/zioinfo/kintex/auth/dto/WorkspaceDto.java @@ -0,0 +1,23 @@ +package com.zioinfo.kintex.auth.dto; + +/** + * 행사 워크스페이스 카드(SCR-01) — 행사명·기간·홀·내 역할 배지·D-데이. + * + * @param eventId 행사 식별자 + * @param eventName 행사명 (예: "2026 스마트팩토리 코리아") + * @param startDate 개장일 (ISO-8601 date) + * @param endDate 종료일 + * @param hallLabel 홀 표기 (예: "제2전시장 홀7") + * @param myRole 내 역할 (ORGANIZER/EXHIBITOR/CONTRACTOR/HALL_MANAGER) + * @param dday 개장까지 남은 일수(음수면 진행 후) + */ +public record WorkspaceDto( + String eventId, + String eventName, + String startDate, + String endDate, + String hallLabel, + String myRole, + int dday +) { +} diff --git a/src/backend/src/main/java/com/zioinfo/kintex/auth/mapper/UserMapper.java b/src/backend/src/main/java/com/zioinfo/kintex/auth/mapper/UserMapper.java new file mode 100644 index 0000000..3eebb2e --- /dev/null +++ b/src/backend/src/main/java/com/zioinfo/kintex/auth/mapper/UserMapper.java @@ -0,0 +1,21 @@ +package com.zioinfo.kintex.auth.mapper; + +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.Map; + +/** + * 사용자·행사 멤버십 조회 매퍼. + * TODO(kintex-db-engineer): 아래 시그니처에 대한 XML(resources/mybatis/mapper/UserMapper.xml)을 + * PLANNING §7 ERD(User, EventMember)에 맞춰 구현한다. 비밀번호 해시 컬럼은 응답 DTO로 절대 노출 금지. + */ +@Mapper +public interface UserMapper { + + /** 이메일로 사용자 인증행(userId, displayName, passwordHash, hallManager) 조회. 없으면 null. */ + Map findAuthByEmail(@Param("email") String email); + + /** 사용자의 행사별 역할 맵 (eventId → roleCode). */ + java.util.List> findEventRoles(@Param("userId") String userId); +} diff --git a/src/backend/src/main/java/com/zioinfo/kintex/common/ApiResponse.java b/src/backend/src/main/java/com/zioinfo/kintex/common/ApiResponse.java new file mode 100644 index 0000000..e94e3b3 --- /dev/null +++ b/src/backend/src/main/java/com/zioinfo/kintex/common/ApiResponse.java @@ -0,0 +1,28 @@ +package com.zioinfo.kintex.common; + +import com.fasterxml.jackson.annotation.JsonInclude; + +/** + * 표준 응답 봉투. 모든 REST 응답은 이 shape를 사용한다. + * 실패 시 {@code error}에는 코드+요약 메시지만 담고 스택트레이스는 절대 포함하지 않는다(보안 불변). + * + *
+ * { "success": true,  "data": { ... },  "error": null }
+ * { "success": false, "data": null,     "error": { "code": "VALIDATION", "message": "..." } }
+ * 
+ */ +@JsonInclude(JsonInclude.Include.ALWAYS) +public record ApiResponse(boolean success, T data, ApiError error) { + + public static ApiResponse ok(T data) { + return new ApiResponse<>(true, data, null); + } + + public static ApiResponse fail(ApiError error) { + return new ApiResponse<>(false, null, error); + } + + /** 오류 payload — 코드 + 사람이 읽을 요약 메시지만(민감정보·스택트레이스 제외). */ + public record ApiError(String code, String message) { + } +} diff --git a/src/backend/src/main/java/com/zioinfo/kintex/common/PageResponse.java b/src/backend/src/main/java/com/zioinfo/kintex/common/PageResponse.java new file mode 100644 index 0000000..7209cf8 --- /dev/null +++ b/src/backend/src/main/java/com/zioinfo/kintex/common/PageResponse.java @@ -0,0 +1,11 @@ +package com.zioinfo.kintex.common; + +import java.util.List; + +/** 목록 응답 공통 봉투(페이지네이션). */ +public record PageResponse(List items, int page, int size, long total) { + + public static PageResponse of(List items, int page, int size, long total) { + return new PageResponse<>(items, page, size, total); + } +} diff --git a/src/backend/src/main/java/com/zioinfo/kintex/common/error/ApiException.java b/src/backend/src/main/java/com/zioinfo/kintex/common/error/ApiException.java new file mode 100644 index 0000000..67f0f63 --- /dev/null +++ b/src/backend/src/main/java/com/zioinfo/kintex/common/error/ApiException.java @@ -0,0 +1,27 @@ +package com.zioinfo.kintex.common.error; + +/** 도메인 예외 — {@link ErrorCode} 기반. 메시지는 사용자 노출용 요약만 담는다(민감정보 금지). */ +public class ApiException extends RuntimeException { + + private final ErrorCode code; + + public ApiException(ErrorCode code) { + super(code.defaultMessage()); + this.code = code; + } + + public ApiException(ErrorCode code, String message) { + super(message); + this.code = code; + } + + public ErrorCode getCode() { + return code; + } + + /** 미구현 지점 표준 예외 — 501. 매퍼(PostGIS)·워커 미완 구간에 사용. */ + public static ApiException notImplemented(String what) { + return new ApiException(ErrorCode.NOT_IMPLEMENTED, + what + " — 매퍼/엔진 구현 대기(TODO: kintex-db-engineer / visualizer)"); + } +} diff --git a/src/backend/src/main/java/com/zioinfo/kintex/common/error/ErrorCode.java b/src/backend/src/main/java/com/zioinfo/kintex/common/error/ErrorCode.java new file mode 100644 index 0000000..a7a6c54 --- /dev/null +++ b/src/backend/src/main/java/com/zioinfo/kintex/common/error/ErrorCode.java @@ -0,0 +1,34 @@ +package com.zioinfo.kintex.common.error; + +import org.springframework.http.HttpStatus; + +/** 도메인 오류 코드 — 클라이언트 대조용 안정 식별자 + HTTP 상태 매핑. */ +public enum ErrorCode { + + VALIDATION(HttpStatus.BAD_REQUEST, "요청 값이 유효하지 않습니다."), + UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "인증이 필요합니다."), + FORBIDDEN(HttpStatus.FORBIDDEN, "이 행사/부스에 대한 권한이 없습니다."), + NOT_FOUND(HttpStatus.NOT_FOUND, "대상을 찾을 수 없습니다."), + CONFLICT(HttpStatus.CONFLICT, "현재 상태와 충돌합니다."), + COMPLIANCE_BLOCKED(HttpStatus.UNPROCESSABLE_ENTITY, "규정 위반(차단)으로 진행할 수 없습니다."), + RENDER_QUOTA_EXCEEDED(HttpStatus.TOO_MANY_REQUESTS, "행사 이미지 생성 쿼터를 초과했습니다."), + NOT_REGISTERED_COMPANY(HttpStatus.FORBIDDEN, "킨텍스 등록업체만 참여할 수 있습니다."), + NOT_IMPLEMENTED(HttpStatus.NOT_IMPLEMENTED, "아직 구현되지 않은 기능입니다."), + INTERNAL(HttpStatus.INTERNAL_SERVER_ERROR, "요청 처리 중 오류가 발생했습니다."); + + private final HttpStatus status; + private final String defaultMessage; + + ErrorCode(HttpStatus status, String defaultMessage) { + this.status = status; + this.defaultMessage = defaultMessage; + } + + public HttpStatus status() { + return status; + } + + public String defaultMessage() { + return defaultMessage; + } +} diff --git a/src/backend/src/main/java/com/zioinfo/kintex/common/error/GlobalExceptionHandler.java b/src/backend/src/main/java/com/zioinfo/kintex/common/error/GlobalExceptionHandler.java new file mode 100644 index 0000000..a2a4e21 --- /dev/null +++ b/src/backend/src/main/java/com/zioinfo/kintex/common/error/GlobalExceptionHandler.java @@ -0,0 +1,48 @@ +package com.zioinfo.kintex.common.error; + +import com.zioinfo.kintex.common.ApiResponse; +import com.zioinfo.kintex.common.ApiResponse.ApiError; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +/** + * 전역 예외 처리 — 응답에는 코드+요약 메시지만 노출한다. + * 스택트레이스·내부 세부는 서버 로그에만 남기고 클라이언트에는 절대 반환하지 않는다(보안 불변). + */ +@RestControllerAdvice +public class GlobalExceptionHandler { + + private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class); + + @ExceptionHandler(ApiException.class) + public ResponseEntity> handleApi(ApiException ex) { + ErrorCode code = ex.getCode(); + // 로그에만 상세, 응답에는 요약만. + log.warn("ApiException [{}]: {}", code, ex.getMessage()); + return ResponseEntity.status(code.status()) + .body(ApiResponse.fail(new ApiError(code.name(), ex.getMessage()))); + } + + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity> handleValidation(MethodArgumentNotValidException ex) { + String msg = ex.getBindingResult().getFieldErrors().stream() + .findFirst() + .map(f -> f.getField() + ": " + f.getDefaultMessage()) + .orElse(ErrorCode.VALIDATION.defaultMessage()); + return ResponseEntity.status(ErrorCode.VALIDATION.status()) + .body(ApiResponse.fail(new ApiError(ErrorCode.VALIDATION.name(), msg))); + } + + /** 최종 방어선 — 예상치 못한 예외는 요약 메시지만, 상세는 로그로. */ + @ExceptionHandler(Exception.class) + public ResponseEntity> handleUnexpected(Exception ex) { + log.error("Unhandled exception", ex); + ErrorCode code = ErrorCode.INTERNAL; + return ResponseEntity.status(code.status()) + .body(ApiResponse.fail(new ApiError(code.name(), code.defaultMessage()))); + } +} diff --git a/src/backend/src/main/java/com/zioinfo/kintex/config/RedisConfig.java b/src/backend/src/main/java/com/zioinfo/kintex/config/RedisConfig.java new file mode 100644 index 0000000..3f4d3cd --- /dev/null +++ b/src/backend/src/main/java/com/zioinfo/kintex/config/RedisConfig.java @@ -0,0 +1,20 @@ +package com.zioinfo.kintex.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.StringRedisTemplate; + +/** + * Redis 작업 큐 설정 — RenderJob·서류 생성·알림 발행용. + * 백엔드는 큐에 작업을 LPUSH 하고, 나노바나나 Python 워커(tools/nanobanana)가 BRPOP 으로 소비한다. + * 페이로드는 JSON 문자열(스키마 계약은 _workspace 계약 문서 참조) → StringRedisTemplate 사용. + */ +@Configuration +public class RedisConfig { + + @Bean + public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory connectionFactory) { + return new StringRedisTemplate(connectionFactory); + } +} diff --git a/src/backend/src/main/java/com/zioinfo/kintex/config/SecurityConfig.java b/src/backend/src/main/java/com/zioinfo/kintex/config/SecurityConfig.java new file mode 100644 index 0000000..8484248 --- /dev/null +++ b/src/backend/src/main/java/com/zioinfo/kintex/config/SecurityConfig.java @@ -0,0 +1,56 @@ +package com.zioinfo.kintex.config; + +import com.zioinfo.kintex.auth.JwtAuthenticationFilter; +import com.zioinfo.kintex.auth.JwtService; +import com.zioinfo.kintex.common.ApiResponse; +import com.zioinfo.kintex.common.ApiResponse.ApiError; +import com.zioinfo.kintex.common.error.ErrorCode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.MediaType; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +/** + * 보안 설정 — 무상태 JWT. 공개 경로: /health, 로그인, WebSocket 핸드셰이크. + * 그 외는 인증 필요(행사 RBAC는 컨트롤러 가드에서 추가 검사). + */ +@Configuration +public class SecurityConfig { + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http, JwtService jwtService, + ObjectMapper objectMapper) throws Exception { + http + .csrf(AbstractHttpConfigurer::disable) + .cors(cors -> {}) + .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .authorizeHttpRequests(auth -> auth + .requestMatchers("/health", "/api/auth/login", "/ws/**", + "/api/internal/render/callback").permitAll() + .anyRequest().authenticated()) + .exceptionHandling(eh -> eh.authenticationEntryPoint((req, res, ex) -> { + // 스택트레이스 미노출 — 표준 봉투로 401만 반환. + res.setStatus(ErrorCode.UNAUTHORIZED.status().value()); + res.setContentType(MediaType.APPLICATION_JSON_VALUE); + res.setCharacterEncoding("UTF-8"); + res.getWriter().write(objectMapper.writeValueAsString( + ApiResponse.fail(new ApiError(ErrorCode.UNAUTHORIZED.name(), + ErrorCode.UNAUTHORIZED.defaultMessage())))); + })) + .addFilterBefore(new JwtAuthenticationFilter(jwtService), + UsernamePasswordAuthenticationFilter.class); + return http.build(); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} diff --git a/src/backend/src/main/java/com/zioinfo/kintex/config/WebSocketConfig.java b/src/backend/src/main/java/com/zioinfo/kintex/config/WebSocketConfig.java new file mode 100644 index 0000000..d913d6e --- /dev/null +++ b/src/backend/src/main/java/com/zioinfo/kintex/config/WebSocketConfig.java @@ -0,0 +1,32 @@ +package com.zioinfo.kintex.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.messaging.simp.config.MessageBrokerRegistry; +import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; +import org.springframework.web.socket.config.annotation.StompEndpointRegistry; +import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; + +/** + * STOMP over WebSocket — RenderJob 완료·승인 이벤트 실시간 푸시(design.md §1-4). + *
    + *
  • 핸드셰이크: {@code /ws}
  • + *
  • 서버→클라 브로드캐스트 prefix: {@code /topic} (예: {@code /topic/render/{jobId}})
  • + *
  • 클라→서버 prefix: {@code /app}
  • + *
+ * 나노바나나 Python 워커가 생성 완료를 백엔드에 알리면 백엔드가 해당 토픽으로 relay 한다. + */ +@Configuration +@EnableWebSocketMessageBroker +public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { + + @Override + public void configureMessageBroker(MessageBrokerRegistry registry) { + registry.enableSimpleBroker("/topic"); + registry.setApplicationDestinationPrefixes("/app"); + } + + @Override + public void registerStompEndpoints(StompEndpointRegistry registry) { + registry.addEndpoint("/ws").setAllowedOriginPatterns("*").withSockJS(); + } +} diff --git a/src/backend/src/main/java/com/zioinfo/kintex/health/HealthController.java b/src/backend/src/main/java/com/zioinfo/kintex/health/HealthController.java new file mode 100644 index 0000000..84969f4 --- /dev/null +++ b/src/backend/src/main/java/com/zioinfo/kintex/health/HealthController.java @@ -0,0 +1,24 @@ +package com.zioinfo.kintex.health; + +import com.zioinfo.kintex.common.ApiResponse; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.time.OffsetDateTime; +import java.util.Map; + +/** 헬스체크 — 인증 불필요(SecurityConfig permitAll). 배포 검증(DEP)·모니터링용. */ +@RestController +@RequestMapping("/health") +public class HealthController { + + @GetMapping + public ApiResponse> health() { + return ApiResponse.ok(Map.of( + "status", "UP", + "service", "kintex-backend", + "time", OffsetDateTime.now().toString() + )); + } +} diff --git a/src/backend/src/main/java/com/zioinfo/kintex/module/m2/FloorplanController.java b/src/backend/src/main/java/com/zioinfo/kintex/module/m2/FloorplanController.java new file mode 100644 index 0000000..dff80fb --- /dev/null +++ b/src/backend/src/main/java/com/zioinfo/kintex/module/m2/FloorplanController.java @@ -0,0 +1,74 @@ +package com.zioinfo.kintex.module.m2; + +import com.zioinfo.kintex.auth.EventAccessGuard; +import com.zioinfo.kintex.auth.EventRole; +import com.zioinfo.kintex.auth.KintexPrincipal; +import com.zioinfo.kintex.common.ApiResponse; +import com.zioinfo.kintex.module.m2.dto.AutoLayoutOption; +import com.zioinfo.kintex.module.m2.dto.AutoLayoutRequest; +import com.zioinfo.kintex.module.m2.dto.LayoutDto; +import com.zioinfo.kintex.module.m2.dto.LayoutSaveRequest; +import com.zioinfo.kintex.rules.ComplianceReport; +import jakarta.validation.Valid; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * M2 플로어플랜 스튜디오 API (SCR-03/04). + * 편집 액션은 주최자(ORGANIZER) 권한, 조회는 행사 멤버·홀매니저 모두 가능(RBAC 가드). + */ +@RestController +@RequestMapping("/api/events/{eventId}/halls/{hallId}/layout") +public class FloorplanController { + + private final FloorplanService service; + private final EventAccessGuard guard; + + public FloorplanController(FloorplanService service, EventAccessGuard guard) { + this.service = service; + this.guard = guard; + } + + /** GET — 배치안 조회(version 미지정 시 최신). 행사 멤버/홀매니저 열람. */ + @GetMapping + public ApiResponse get(@AuthenticationPrincipal KintexPrincipal principal, + @PathVariable String eventId, + @PathVariable String hallId, + @RequestParam(required = false) Integer version) { + guard.requireEventAccess(principal, eventId); + return ApiResponse.ok(service.getLayout(eventId, hallId, version)); + } + + /** PUT — 배치 저장(주최자). */ + @PutMapping + public ApiResponse save(@AuthenticationPrincipal KintexPrincipal principal, + @PathVariable String eventId, + @PathVariable String hallId, + @Valid @RequestBody LayoutSaveRequest request) { + guard.requireRole(principal, eventId, EventRole.ORGANIZER); + return ApiResponse.ok(service.saveLayout(eventId, hallId, request)); + } + + /** POST /validate — 규정 검증 실행(주최자/홀매니저). 차단 존재 시 submittable=false. */ + @PostMapping("/validate") + public ApiResponse validate(@AuthenticationPrincipal KintexPrincipal principal, + @PathVariable String eventId, + @PathVariable String hallId, + @RequestParam(required = false) Integer version) { + guard.requireRole(principal, eventId, EventRole.ORGANIZER, EventRole.HALL_MANAGER); + return ApiResponse.ok(service.validate(eventId, hallId, version)); + } + + /** POST /auto-generate — AI 자동배치 복수 안(주최자). */ + @PostMapping("/auto-generate") + public ApiResponse> autoGenerate( + @AuthenticationPrincipal KintexPrincipal principal, + @PathVariable String eventId, + @PathVariable String hallId, + @Valid @RequestBody AutoLayoutRequest request) { + guard.requireRole(principal, eventId, EventRole.ORGANIZER); + return ApiResponse.ok(service.autoGenerate(eventId, hallId, request)); + } +} diff --git a/src/backend/src/main/java/com/zioinfo/kintex/module/m2/FloorplanService.java b/src/backend/src/main/java/com/zioinfo/kintex/module/m2/FloorplanService.java new file mode 100644 index 0000000..6b6b903 --- /dev/null +++ b/src/backend/src/main/java/com/zioinfo/kintex/module/m2/FloorplanService.java @@ -0,0 +1,25 @@ +package com.zioinfo.kintex.module.m2; + +import com.zioinfo.kintex.module.m2.dto.AutoLayoutOption; +import com.zioinfo.kintex.module.m2.dto.AutoLayoutRequest; +import com.zioinfo.kintex.module.m2.dto.LayoutDto; +import com.zioinfo.kintex.module.m2.dto.LayoutSaveRequest; +import com.zioinfo.kintex.rules.ComplianceReport; + +import java.util.List; + +/** M2 플로어플랜 스튜디오 서비스 계약. */ +public interface FloorplanService { + + /** 배치안 조회(version=null이면 최신). */ + LayoutDto getLayout(String eventId, String hallId, Integer version); + + /** 배치 저장 → 저장된 배치안. 저장 시 규정 검증도 함께 산출해 summary에 반영. */ + LayoutDto saveLayout(String eventId, String hallId, LayoutSaveRequest request); + + /** 규정 검증 실행(통로 폭·바닥하중·비상구·복층 등) — 차단/경고 리포트. */ + ComplianceReport validate(String eventId, String hallId, Integer version); + + /** AI 자동배치 — 조건 입력 → 복수 배치안 후보(제약 솔버/휴리스틱). */ + List autoGenerate(String eventId, String hallId, AutoLayoutRequest request); +} diff --git a/src/backend/src/main/java/com/zioinfo/kintex/module/m2/FloorplanServiceImpl.java b/src/backend/src/main/java/com/zioinfo/kintex/module/m2/FloorplanServiceImpl.java new file mode 100644 index 0000000..63d892a --- /dev/null +++ b/src/backend/src/main/java/com/zioinfo/kintex/module/m2/FloorplanServiceImpl.java @@ -0,0 +1,62 @@ +package com.zioinfo.kintex.module.m2; + +import com.zioinfo.kintex.common.error.ApiException; +import com.zioinfo.kintex.module.m2.dto.AutoLayoutOption; +import com.zioinfo.kintex.module.m2.dto.AutoLayoutRequest; +import com.zioinfo.kintex.module.m2.dto.LayoutDto; +import com.zioinfo.kintex.module.m2.dto.LayoutSaveRequest; +import com.zioinfo.kintex.module.m2.mapper.BoothMapper; +import com.zioinfo.kintex.rules.ComplianceReport; +import com.zioinfo.kintex.rules.ComplianceRuleEngine; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * M2 서비스 골격. 규정 엔진({@link ComplianceRuleEngine})은 완성되어 있고, + * 공간 영속/공간 산출({@link BoothMapper} PostGIS XML)은 kintex-db-engineer 구현 대기 → 해당 경로 501. + * + *

완성 시 validate()는 다음처럼 조립한다: + *

+ *   Map<String,Object> metrics = new HashMap<>();
+ *   metrics.put("layout.min_aisle_width_m", boothMapper.minAisleWidth(layoutId));
+ *   metrics.put("layout.exits_blocked_count", boothMapper.countExitsBlocked(layoutId, hallId));
+ *   // 부스별 height/floor_load 는 부스 루프에서 개별 평가하거나 최댓값으로 집계
+ *   return ruleEngine.evaluate("M2", metrics, hallId);
+ * 
+ */ +@Service +public class FloorplanServiceImpl implements FloorplanService { + + private final BoothMapper boothMapper; + private final ComplianceRuleEngine ruleEngine; + + public FloorplanServiceImpl(BoothMapper boothMapper, ComplianceRuleEngine ruleEngine) { + this.boothMapper = boothMapper; + this.ruleEngine = ruleEngine; + } + + @Override + public LayoutDto getLayout(String eventId, String hallId, Integer version) { + // TODO(kintex-db-engineer): BoothMapper.findLayout + findBooths(ST_AsGeoJSON) 조립. + throw ApiException.notImplemented("배치안 조회(공간 데이터)"); + } + + @Override + public LayoutDto saveLayout(String eventId, String hallId, LayoutSaveRequest request) { + // TODO(kintex-db-engineer): upsertLayout + replaceBooths(ST_MakePolygon), 저장 후 validate 재산출. + throw ApiException.notImplemented("배치 저장(PostGIS 폴리곤 반영)"); + } + + @Override + public ComplianceReport validate(String eventId, String hallId, Integer version) { + // TODO(kintex-db-engineer): BoothMapper 공간 산출값으로 metrics 조립 후 ruleEngine.evaluate("M2", ...). + throw ApiException.notImplemented("배치 규정 검증(통로 폭·비상구 공간 산출)"); + } + + @Override + public List autoGenerate(String eventId, String hallId, AutoLayoutRequest request) { + // TODO(BE): 제약 솔버/휴리스틱 배치 엔진 + S7 RenderJob 발행(M5-1). 홀 규격/트렌치 좌표 필요(D-2). + throw ApiException.notImplemented("AI 자동배치(제약 솔버 + S7 발행)"); + } +} diff --git a/src/backend/src/main/java/com/zioinfo/kintex/module/m2/dto/AutoLayoutOption.java b/src/backend/src/main/java/com/zioinfo/kintex/module/m2/dto/AutoLayoutOption.java new file mode 100644 index 0000000..b304605 --- /dev/null +++ b/src/backend/src/main/java/com/zioinfo/kintex/module/m2/dto/AutoLayoutOption.java @@ -0,0 +1,20 @@ +package com.zioinfo.kintex.module.m2.dto; + +import com.fasterxml.jackson.annotation.JsonInclude; + +/** + * 자동 생성 배치안 후보(SCR-04 비교 카드). + * + * @param optionId 후보 식별자 + * @param label 라벨("배치안 A/B/C") + * @param summary 지표 요약 + * @param s7RenderJobId S7 홀 전경(조감) 생성 잡 ID — 완료 시 WebSocket 푸시로 카드 이미지 교체 + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public record AutoLayoutOption( + String optionId, + String label, + LayoutSummary summary, + String s7RenderJobId +) { +} diff --git a/src/backend/src/main/java/com/zioinfo/kintex/module/m2/dto/AutoLayoutRequest.java b/src/backend/src/main/java/com/zioinfo/kintex/module/m2/dto/AutoLayoutRequest.java new file mode 100644 index 0000000..8842f0f --- /dev/null +++ b/src/backend/src/main/java/com/zioinfo/kintex/module/m2/dto/AutoLayoutRequest.java @@ -0,0 +1,23 @@ +package com.zioinfo.kintex.module.m2.dto; + +import jakarta.validation.constraints.Min; + +/** + * AI 자동배치 조건(SCR-03 툴바 · SCR-04 헤더). 제약 솔버/휴리스틱 입력. + * + * @param targetBoothCount 목표 부스 수 + * @param premiumRatio 프리미엄(6×3m) 비율 0.0~1.0 + * @param stageCount 무대 수 + * @param loungeCount 라운지 수 + * @param mainEntranceCount 주출입구 수 + * @param optionCount 생성할 배치안 개수(기본 3) + */ +public record AutoLayoutRequest( + @Min(1) int targetBoothCount, + double premiumRatio, + int stageCount, + int loungeCount, + int mainEntranceCount, + @Min(1) int optionCount +) { +} diff --git a/src/backend/src/main/java/com/zioinfo/kintex/module/m2/dto/BoothDto.java b/src/backend/src/main/java/com/zioinfo/kintex/module/m2/dto/BoothDto.java new file mode 100644 index 0000000..b0b738a --- /dev/null +++ b/src/backend/src/main/java/com/zioinfo/kintex/module/m2/dto/BoothDto.java @@ -0,0 +1,32 @@ +package com.zioinfo.kintex.module.m2.dto; + +import com.fasterxml.jackson.annotation.JsonInclude; + +import java.util.List; + +/** + * 부스 폴리곤 — 홀 로컬 좌표계(미터). 서버는 이 좌표를 PostGIS polygon으로 저장/검증한다. + * + * @param boothId 부스 식별자(신규 저장 시 null 허용) + * @param boothNo 부스 번호(예: "A-102") + * @param type 부스 유형 assembled|independent|corner|island (나노바나나 BOOTH_TYPES 매핑) + * @param polygon 외곽 폴리곤 좌표열 [[x,y], ...] (닫힌 링, 미터). 축은 홀 로컬 원점 기준 + * @param sizeM [폭, 깊이] (미터) + * @param heightM 장치물 최고 높이(미터) — 규정 HEIGHT_MAX 대상 + * @param floorLoadTPerM2 예상 바닥하중(t/㎡) — 규정 FLOOR_LOAD 대상 + * @param assignedCompanyName 배정 참가업체명(표시용). 내부 식별자·민감정보 미포함 + * @param premium 프리미엄 부스 여부 + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public record BoothDto( + String boothId, + String boothNo, + String type, + List> polygon, + List sizeM, + Double heightM, + Double floorLoadTPerM2, + String assignedCompanyName, + boolean premium +) { +} diff --git a/src/backend/src/main/java/com/zioinfo/kintex/module/m2/dto/LayoutDto.java b/src/backend/src/main/java/com/zioinfo/kintex/module/m2/dto/LayoutDto.java new file mode 100644 index 0000000..051cc2b --- /dev/null +++ b/src/backend/src/main/java/com/zioinfo/kintex/module/m2/dto/LayoutDto.java @@ -0,0 +1,32 @@ +package com.zioinfo.kintex.module.m2.dto; + +import com.fasterxml.jackson.annotation.JsonInclude; + +import java.util.List; + +/** + * 플로어플랜 배치안 — 부스 폴리곤 집합 + 지표 요약(SCR-03/04). + * + * @param layoutId 배치안 식별자 + * @param eventId 행사 식별자 + * @param hallId 홀 식별자 + * @param version 배치안 버전(예: 3) + * @param name 배치안 이름(예: "배치안 B") + * @param status 상태 배지(draft/submitted/ai_review/approved/rejected) — design.md StatusBadge + * @param booths 부스 목록 + * @param summary 지표 요약 + * @param updatedAt 최종 수정 시각(ISO-8601) + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public record LayoutDto( + String layoutId, + String eventId, + String hallId, + int version, + String name, + String status, + List booths, + LayoutSummary summary, + String updatedAt +) { +} diff --git a/src/backend/src/main/java/com/zioinfo/kintex/module/m2/dto/LayoutSaveRequest.java b/src/backend/src/main/java/com/zioinfo/kintex/module/m2/dto/LayoutSaveRequest.java new file mode 100644 index 0000000..0edf5bc --- /dev/null +++ b/src/backend/src/main/java/com/zioinfo/kintex/module/m2/dto/LayoutSaveRequest.java @@ -0,0 +1,20 @@ +package com.zioinfo.kintex.module.m2.dto; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotEmpty; + +import java.util.List; + +/** + * 배치 저장 요청(PUT). 저장 시 서버가 PostGIS로 지오메트리를 반영하고, 저장 결과에는 규정 검증이 함께 수행된다. + * + * @param name 배치안 이름 + * @param version 기반 버전(낙관적 잠금용, null이면 신규) + * @param booths 부스 폴리곤 목록 + */ +public record LayoutSaveRequest( + String name, + Integer version, + @NotEmpty @Valid List booths +) { +} diff --git a/src/backend/src/main/java/com/zioinfo/kintex/module/m2/dto/LayoutSummary.java b/src/backend/src/main/java/com/zioinfo/kintex/module/m2/dto/LayoutSummary.java new file mode 100644 index 0000000..1bf99a5 --- /dev/null +++ b/src/backend/src/main/java/com/zioinfo/kintex/module/m2/dto/LayoutSummary.java @@ -0,0 +1,21 @@ +package com.zioinfo.kintex.module.m2.dto; + +/** + * 배치 지표 요약(SCR-03 하단 상태 바 · SCR-04 비교 카드). + * + * @param boothCount 배치된 부스 수 + * @param targetBoothCount 목표 부스 수(홀 기준, 예: 홀7 510) + * @param salesAreaM2 판매 면적(㎡) — PostGIS ST_Area 합산 + * @param minAisleWidthM 최소 통로 폭(m) — PostGIS ST_Buffer/ST_Distance 산출 + * @param violationBlock 차단 위반 수 + * @param violationWarn 경고 위반 수 + */ +public record LayoutSummary( + int boothCount, + int targetBoothCount, + double salesAreaM2, + double minAisleWidthM, + int violationBlock, + int violationWarn +) { +} diff --git a/src/backend/src/main/java/com/zioinfo/kintex/module/m2/mapper/BoothMapper.java b/src/backend/src/main/java/com/zioinfo/kintex/module/m2/mapper/BoothMapper.java new file mode 100644 index 0000000..dbd2d02 --- /dev/null +++ b/src/backend/src/main/java/com/zioinfo/kintex/module/m2/mapper/BoothMapper.java @@ -0,0 +1,41 @@ +package com.zioinfo.kintex.module.m2.mapper; + +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; +import java.util.Map; + +/** + * 플로어플랜 공간 매퍼 — PostGIS ST_* 함수를 XML로 바인딩한다. + * TODO(kintex-db-engineer): resources/mybatis/mapper/BoothMapper.xml 구현 (M2-1). + * 부스는 geometry(POLYGON) 컬럼, 트렌치는 geometry(POINT), 통로 검증은 ST_Buffer/ST_Distance/ST_Intersects. + */ +@Mapper +public interface BoothMapper { + + /** 배치안 헤더 조회(layoutId, version, name, status, updatedAt). */ + Map findLayout(@Param("eventId") String eventId, + @Param("hallId") String hallId, + @Param("version") Integer version); + + /** 배치안의 부스 목록 조회(폴리곤은 ST_AsGeoJSON 또는 좌표 배열로 반환). */ + List> findBooths(@Param("layoutId") String layoutId); + + /** 배치안 헤더 upsert → layoutId 반환. */ + String upsertLayout(@Param("params") Map params); + + /** 부스 폴리곤 저장(ST_GeomFromText / ST_MakePolygon). 배치안 단위로 교체 저장. */ + void replaceBooths(@Param("layoutId") String layoutId, + @Param("booths") List> booths); + + /** 판매 면적 합계(㎡) — SUM(ST_Area(geom)). */ + Double sumSalesArea(@Param("layoutId") String layoutId); + + /** 최소 통로 폭(m) — 인접 부스 폴리곤 간 ST_Distance 최소값(비상구·통로 제약 검증). */ + Double minAisleWidth(@Param("layoutId") String layoutId); + + /** 비상구를 차단하는 부스 수 — ST_Intersects(booth, exit_buffer). */ + Integer countExitsBlocked(@Param("layoutId") String layoutId, + @Param("hallId") String hallId); +} diff --git a/src/backend/src/main/java/com/zioinfo/kintex/module/m3/DesignController.java b/src/backend/src/main/java/com/zioinfo/kintex/module/m3/DesignController.java new file mode 100644 index 0000000..b095bf8 --- /dev/null +++ b/src/backend/src/main/java/com/zioinfo/kintex/module/m3/DesignController.java @@ -0,0 +1,61 @@ +package com.zioinfo.kintex.module.m3; + +import com.zioinfo.kintex.auth.EventAccessGuard; +import com.zioinfo.kintex.auth.EventRole; +import com.zioinfo.kintex.auth.KintexPrincipal; +import com.zioinfo.kintex.common.ApiResponse; +import com.zioinfo.kintex.module.m3.dto.DesignPlanDto; +import com.zioinfo.kintex.module.m3.dto.DesignSaveRequest; +import com.zioinfo.kintex.module.m3.dto.DesignSpec; +import com.zioinfo.kintex.rules.ComplianceReport; +import jakarta.validation.Valid; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +/** + * M3 부스 설계 스튜디오 API (SCR-06/09). + * 참가업체(조립 옵션·컨펌)·장치업체(독립 편집)가 설계/사전검증한다. 행사 RBAC 가드로 접근 제어. + */ +@RestController +@RequestMapping("/api/events/{eventId}/booths/{boothId}/design") +public class DesignController { + + private final DesignService service; + private final EventAccessGuard guard; + + public DesignController(DesignService service, EventAccessGuard guard) { + this.service = service; + this.guard = guard; + } + + /** GET — 설계안 조회(행사 멤버/홀매니저). */ + @GetMapping + public ApiResponse get(@AuthenticationPrincipal KintexPrincipal principal, + @PathVariable String eventId, + @PathVariable String boothId, + @RequestParam(required = false) Integer version) { + guard.requireEventAccess(principal, eventId); + return ApiResponse.ok(service.getDesign(boothId, version)); + } + + /** PUT — 설계 저장(참가업체/장치업체). */ + @PutMapping + public ApiResponse save(@AuthenticationPrincipal KintexPrincipal principal, + @PathVariable String eventId, + @PathVariable String boothId, + @Valid @RequestBody DesignSaveRequest request) { + guard.requireRole(principal, eventId, EventRole.EXHIBITOR, EventRole.CONTRACTOR); + return ApiResponse.ok(service.saveDesign(boothId, request)); + } + + /** POST /precheck — 규정 사전검증(제출 전 플래깅). 스펙만으로 즉시 평가(완성 구현). */ + @PostMapping("/precheck") + public ApiResponse precheck(@AuthenticationPrincipal KintexPrincipal principal, + @PathVariable String eventId, + @PathVariable String boothId, + @Valid @RequestBody DesignSpec spec) { + guard.requireRole(principal, eventId, EventRole.EXHIBITOR, EventRole.CONTRACTOR, + EventRole.HALL_MANAGER); + return ApiResponse.ok(service.precheck(spec)); + } +} diff --git a/src/backend/src/main/java/com/zioinfo/kintex/module/m3/DesignService.java b/src/backend/src/main/java/com/zioinfo/kintex/module/m3/DesignService.java new file mode 100644 index 0000000..f278671 --- /dev/null +++ b/src/backend/src/main/java/com/zioinfo/kintex/module/m3/DesignService.java @@ -0,0 +1,22 @@ +package com.zioinfo.kintex.module.m3; + +import com.zioinfo.kintex.module.m3.dto.DesignPlanDto; +import com.zioinfo.kintex.module.m3.dto.DesignSaveRequest; +import com.zioinfo.kintex.module.m3.dto.DesignSpec; +import com.zioinfo.kintex.rules.ComplianceReport; + +/** M3 부스 설계 스튜디오 서비스 계약. */ +public interface DesignService { + + /** 설계안 조회(version=null이면 최신). */ + DesignPlanDto getDesign(String boothId, Integer version); + + /** 설계 저장. 저장 시 규정 사전검증 수행. */ + DesignPlanDto saveDesign(String boothId, DesignSaveRequest request); + + /** + * 규정 사전검증(높이 5m·리깅 D-7·방염·복층) — 제출 전 플래깅. + * 설계 스펙만으로 평가 가능하므로 매퍼 없이 룰 엔진으로 즉시 산출한다. + */ + ComplianceReport precheck(DesignSpec spec); +} diff --git a/src/backend/src/main/java/com/zioinfo/kintex/module/m3/DesignServiceImpl.java b/src/backend/src/main/java/com/zioinfo/kintex/module/m3/DesignServiceImpl.java new file mode 100644 index 0000000..dfc4004 --- /dev/null +++ b/src/backend/src/main/java/com/zioinfo/kintex/module/m3/DesignServiceImpl.java @@ -0,0 +1,62 @@ +package com.zioinfo.kintex.module.m3; + +import com.zioinfo.kintex.common.error.ApiException; +import com.zioinfo.kintex.module.m3.dto.DesignPlanDto; +import com.zioinfo.kintex.module.m3.dto.DesignSaveRequest; +import com.zioinfo.kintex.module.m3.dto.DesignSpec; +import com.zioinfo.kintex.module.m3.mapper.DesignMapper; +import com.zioinfo.kintex.rules.ComplianceReport; +import com.zioinfo.kintex.rules.ComplianceRuleEngine; +import org.springframework.stereotype.Service; + +import java.util.HashMap; +import java.util.Map; + +/** + * M3 서비스. precheck()는 완성 구현(설계 스펙 → 룰 엔진 평가). 설계 영속은 매퍼 대기(501). + */ +@Service +public class DesignServiceImpl implements DesignService { + + private final DesignMapper designMapper; + private final ComplianceRuleEngine ruleEngine; + + public DesignServiceImpl(DesignMapper designMapper, ComplianceRuleEngine ruleEngine) { + this.designMapper = designMapper; + this.ruleEngine = ruleEngine; + } + + @Override + public DesignPlanDto getDesign(String boothId, Integer version) { + // TODO(kintex-db-engineer): DesignMapper.findDesign + spec(jsonb) 역직렬화. + throw ApiException.notImplemented("설계안 조회"); + } + + @Override + public DesignPlanDto saveDesign(String boothId, DesignSaveRequest request) { + // TODO(kintex-db-engineer): DesignMapper.upsertDesign(spec jsonb). 저장 후 precheck 상태 반영. + throw ApiException.notImplemented("설계 저장"); + } + + @Override + public ComplianceReport precheck(DesignSpec spec) { + Map metrics = new HashMap<>(); + if (spec.wallHeightM() != null) { + metrics.put("booth.height_m", spec.wallHeightM()); + } + if (spec.rigging() != null && spec.rigging().use() && spec.rigging().heightM() != null) { + metrics.put("rigging.height_m", spec.rigging().heightM()); + } + if (spec.mezzanineAreaRatio() != null) { + metrics.put("mezzanine.area_ratio", spec.mezzanineAreaRatio()); + } + if (spec.materials() != null && !spec.materials().isEmpty()) { + boolean allFr = spec.materials().stream().allMatch(DesignSpec.Material::fireRetardant); + metrics.put("materials.all_fire_retardant", allFr); + } + if (spec.usesDesignatedLightingOnly() != null) { + metrics.put("lighting.uses_designated_only", spec.usesDesignatedLightingOnly()); + } + return ruleEngine.evaluate("M3", metrics, null); + } +} diff --git a/src/backend/src/main/java/com/zioinfo/kintex/module/m3/dto/DesignPlanDto.java b/src/backend/src/main/java/com/zioinfo/kintex/module/m3/dto/DesignPlanDto.java new file mode 100644 index 0000000..c678325 --- /dev/null +++ b/src/backend/src/main/java/com/zioinfo/kintex/module/m3/dto/DesignPlanDto.java @@ -0,0 +1,24 @@ +package com.zioinfo.kintex.module.m3.dto; + +import com.fasterxml.jackson.annotation.JsonInclude; + +/** + * 부스 설계안(버전). SCR-06 상단 버전 드롭다운·컨펌 흐름. + * + * @param designId 설계안 식별자 + * @param boothId 부스 식별자 + * @param version 설계 버전 + * @param status 상태 배지(draft/submitted/ai_review/approved/rejected) + * @param spec 설계 스펙 + * @param updatedAt 최종 수정 시각(ISO-8601) + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public record DesignPlanDto( + String designId, + String boothId, + int version, + String status, + DesignSpec spec, + String updatedAt +) { +} diff --git a/src/backend/src/main/java/com/zioinfo/kintex/module/m3/dto/DesignSaveRequest.java b/src/backend/src/main/java/com/zioinfo/kintex/module/m3/dto/DesignSaveRequest.java new file mode 100644 index 0000000..bd7c6a6 --- /dev/null +++ b/src/backend/src/main/java/com/zioinfo/kintex/module/m3/dto/DesignSaveRequest.java @@ -0,0 +1,16 @@ +package com.zioinfo.kintex.module.m3.dto; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; + +/** + * 설계 저장 요청(PUT). 저장 시 규정 사전검증이 함께 수행되어 응답 상태에 반영된다. + * + * @param version 기반 버전(null이면 신규) + * @param spec 설계 스펙 + */ +public record DesignSaveRequest( + Integer version, + @NotNull @Valid DesignSpec spec +) { +} diff --git a/src/backend/src/main/java/com/zioinfo/kintex/module/m3/dto/DesignSpec.java b/src/backend/src/main/java/com/zioinfo/kintex/module/m3/dto/DesignSpec.java new file mode 100644 index 0000000..2287254 --- /dev/null +++ b/src/backend/src/main/java/com/zioinfo/kintex/module/m3/dto/DesignSpec.java @@ -0,0 +1,45 @@ +package com.zioinfo.kintex.module.m3.dto; + +import com.fasterxml.jackson.annotation.JsonInclude; + +import java.util.List; + +/** + * 부스 설계 스펙(SCR-06 좌측 스펙 패널). 규정 사전검증(M3-1)의 입력이자 나노바나나 씬 컴파일 소스. + * + * @param boothType assembled(조립) | independent(독립) | corner | island + * @param industry 업종(예: "로봇/제조") + * @param budget 예산(원) + * @param zones 공간 존(안내데스크/시연존/상담존/창고) + * @param wallHeightM 벽체(장치물) 높이(m) — 규정 HEIGHT_MAX(5m) 대상 + * @param signageText 간판 문구(한글 — 나노바나나 렌더 대상) + * @param rigging 리깅 설정(사용 여부·천장 높이) + * @param mezzanineAreaRatio 복층 바닥면적 비율(0~1) — 규정 MEZZANINE_RATIO 대상 + * @param materials 자재 목록(방염 여부 포함) + * @param lightingMode day|night(조명 프리셋, M4a 연동) + * @param usesDesignatedLightingOnly 지정 조명만 사용 여부(규정 LIGHTING_BRING_IN) + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public record DesignSpec( + String boothType, + String industry, + Long budget, + List zones, + Double wallHeightM, + String signageText, + Rigging rigging, + Double mezzanineAreaRatio, + List materials, + String lightingMode, + Boolean usesDesignatedLightingOnly +) { + + public record Zone(String type, Integer ratioPercent) { + } + + public record Rigging(boolean use, Double heightM) { + } + + public record Material(String part, String finish, boolean fireRetardant) { + } +} diff --git a/src/backend/src/main/java/com/zioinfo/kintex/module/m3/mapper/DesignMapper.java b/src/backend/src/main/java/com/zioinfo/kintex/module/m3/mapper/DesignMapper.java new file mode 100644 index 0000000..cf49aaa --- /dev/null +++ b/src/backend/src/main/java/com/zioinfo/kintex/module/m3/mapper/DesignMapper.java @@ -0,0 +1,25 @@ +package com.zioinfo.kintex.module.m3.mapper; + +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.Map; + +/** + * 부스 설계안 매퍼. + * TODO(kintex-db-engineer): resources/mybatis/mapper/DesignMapper.xml (DesignPlan 버전 테이블). + * spec(JSON)은 jsonb 컬럼 저장 권장. eventId 역참조(부스→행사)도 제공(RBAC 확인용). + */ +@Mapper +public interface DesignMapper { + + /** 설계안 조회(version=null이면 최신). */ + Map findDesign(@Param("boothId") String boothId, + @Param("version") Integer version); + + /** 설계안 upsert → designId 반환. */ + String upsertDesign(@Param("params") Map params); + + /** 부스가 속한 eventId 조회(RBAC 확인용). */ + String findEventIdByBooth(@Param("boothId") String boothId); +} diff --git a/src/backend/src/main/java/com/zioinfo/kintex/module/m4/UtilityController.java b/src/backend/src/main/java/com/zioinfo/kintex/module/m4/UtilityController.java new file mode 100644 index 0000000..64be1ae --- /dev/null +++ b/src/backend/src/main/java/com/zioinfo/kintex/module/m4/UtilityController.java @@ -0,0 +1,70 @@ +package com.zioinfo.kintex.module.m4; + +import com.zioinfo.kintex.auth.EventAccessGuard; +import com.zioinfo.kintex.auth.EventRole; +import com.zioinfo.kintex.auth.KintexPrincipal; +import com.zioinfo.kintex.common.ApiResponse; +import com.zioinfo.kintex.module.m4.dto.UtilityOrderDto; +import com.zioinfo.kintex.module.m4.dto.UtilityQuote; +import com.zioinfo.kintex.module.m4.dto.UtilityQuoteRequest; +import com.zioinfo.kintex.module.m4.dto.WiringRequest; +import com.zioinfo.kintex.module.m4.dto.WiringResult; +import jakarta.validation.Valid; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +/** + * M4 유틸리티 설계 API (SCR-07/08). 참가업체(신청)·장치업체(검토). 행사 RBAC 가드. + */ +@RestController +@RequestMapping("/api/events/{eventId}/booths/{boothId}/utility") +public class UtilityController { + + private final UtilityService service; + private final EventAccessGuard guard; + + public UtilityController(UtilityService service, EventAccessGuard guard) { + this.service = service; + this.guard = guard; + } + + /** POST /quote — 자동 견적(요율 룰셋 기반, 완성 구현). */ + @PostMapping("/quote") + public ApiResponse quote(@AuthenticationPrincipal KintexPrincipal principal, + @PathVariable String eventId, + @PathVariable String boothId, + @Valid @RequestBody UtilityQuoteRequest request) { + guard.requireEventAccess(principal, eventId); + return ApiResponse.ok(service.quote(request)); + } + + /** POST /wiring — 배선 산출(트렌치 최단 경로). */ + @PostMapping("/wiring") + public ApiResponse wiring(@AuthenticationPrincipal KintexPrincipal principal, + @PathVariable String eventId, + @PathVariable String boothId, + @RequestParam String hallId, + @Valid @RequestBody WiringRequest request) { + guard.requireEventAccess(principal, eventId); + return ApiResponse.ok(service.computeWiring(eventId, hallId, boothId, request)); + } + + /** POST /order — 신청 제출 + 위치표시도 생성 트리거(참가업체). */ + @PostMapping("/order") + public ApiResponse order(@AuthenticationPrincipal KintexPrincipal principal, + @PathVariable String eventId, + @PathVariable String boothId, + @Valid @RequestBody UtilityQuoteRequest request) { + guard.requireRole(principal, eventId, EventRole.EXHIBITOR, EventRole.CONTRACTOR); + return ApiResponse.ok(service.submitOrder(eventId, boothId, request)); + } + + /** GET — 신청 조회. */ + @GetMapping + public ApiResponse get(@AuthenticationPrincipal KintexPrincipal principal, + @PathVariable String eventId, + @PathVariable String boothId) { + guard.requireEventAccess(principal, eventId); + return ApiResponse.ok(service.getOrder(eventId, boothId)); + } +} diff --git a/src/backend/src/main/java/com/zioinfo/kintex/module/m4/UtilityService.java b/src/backend/src/main/java/com/zioinfo/kintex/module/m4/UtilityService.java new file mode 100644 index 0000000..5765f6a --- /dev/null +++ b/src/backend/src/main/java/com/zioinfo/kintex/module/m4/UtilityService.java @@ -0,0 +1,23 @@ +package com.zioinfo.kintex.module.m4; + +import com.zioinfo.kintex.module.m4.dto.UtilityOrderDto; +import com.zioinfo.kintex.module.m4.dto.UtilityQuote; +import com.zioinfo.kintex.module.m4.dto.UtilityQuoteRequest; +import com.zioinfo.kintex.module.m4.dto.WiringRequest; +import com.zioinfo.kintex.module.m4.dto.WiringResult; + +/** M4 유틸리티 설계 서비스 계약(전기·조명 / 네트워크·급배수). */ +public interface UtilityService { + + /** 자동 견적 — 기기 목록 kW 합산 + 분전반 산출 + 네트워크/급배수 요금(요율 룰셋 기준). 완성 구현. */ + UtilityQuote quote(UtilityQuoteRequest request); + + /** 배선 산출 — 단말→최근접 트렌치 최단 경로(PostGIS). S6 오버레이 원천. */ + WiringResult computeWiring(String eventId, String hallId, String boothId, WiringRequest request); + + /** 신청 제출 + 위치표시도(PDF) 생성 트리거(수기 작도 대체). */ + UtilityOrderDto submitOrder(String eventId, String boothId, UtilityQuoteRequest request); + + /** 신청 조회. */ + UtilityOrderDto getOrder(String eventId, String boothId); +} diff --git a/src/backend/src/main/java/com/zioinfo/kintex/module/m4/UtilityServiceImpl.java b/src/backend/src/main/java/com/zioinfo/kintex/module/m4/UtilityServiceImpl.java new file mode 100644 index 0000000..d95e5f3 --- /dev/null +++ b/src/backend/src/main/java/com/zioinfo/kintex/module/m4/UtilityServiceImpl.java @@ -0,0 +1,126 @@ +package com.zioinfo.kintex.module.m4; + +import com.zioinfo.kintex.common.error.ApiException; +import com.zioinfo.kintex.module.m4.dto.UtilityOrderDto; +import com.zioinfo.kintex.module.m4.dto.UtilityQuote; +import com.zioinfo.kintex.module.m4.dto.UtilityQuoteRequest; +import com.zioinfo.kintex.module.m4.dto.WiringRequest; +import com.zioinfo.kintex.module.m4.dto.WiringResult; +import com.zioinfo.kintex.module.m4.mapper.WiringMapper; +import com.zioinfo.kintex.rules.RateRuleSet; +import com.zioinfo.kintex.rules.RuleSetLoader; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * M4 서비스. quote()는 완성 구현(요율 룰셋 기반 산술). 배선/신청/위치표시도는 매퍼·서식 대기(501). + */ +@Service +public class UtilityServiceImpl implements UtilityService { + + /** 50A · 220V 단상 분전반 1식 근사 용량(kW) — 분전반 수량 산출 기준. */ + private static final double KW_PER_50A_BOX = 11.0; + + private final RuleSetLoader ruleSets; + private final WiringMapper wiringMapper; + + public UtilityServiceImpl(RuleSetLoader ruleSets, WiringMapper wiringMapper) { + this.ruleSets = ruleSets; + this.wiringMapper = wiringMapper; + } + + @Override + public UtilityQuote quote(UtilityQuoteRequest request) { + RateRuleSet rates = ruleSets.rates(); + if (rates == null || rates.utility() == null) { + throw ApiException.notImplemented("요율 룰셋 미로드"); + } + Map util = rates.utility(); + long perKw = num(nested(util, "power", "perKw")); + long box50A = num(nested(util, "power", "distributionBox50A")); + long perLine = num(nested(util, "network", "internetWiredPerLine")); + long perPlumb = num(nested(util, "plumbing", "perOutlet")); + long perAir = num(nested(util, "compressedAir", "perOutlet")); + + double totalPowerKw = 0; + if (request.devices() != null) { + for (UtilityQuoteRequest.Device d : request.devices()) { + totalPowerKw += d.count() * d.powerKw(); + } + } + int requestedKw = (int) Math.ceil(totalPowerKw - 1e-9); + int boxes = requestedKw > 0 ? (int) Math.ceil(requestedKw / KW_PER_50A_BOX) : 0; + + List lines = new ArrayList<>(); + if (requestedKw > 0) { + lines.add(new UtilityQuote.QuoteLine( + "전기 220V 단상 " + requestedKw + "kW", requestedKw, perKw, (long) requestedKw * perKw)); + } + if (boxes > 0) { + lines.add(new UtilityQuote.QuoteLine("분전반 50A 추가", boxes, box50A, (long) boxes * box50A)); + } + if (request.networkWiredLines() > 0) { + lines.add(new UtilityQuote.QuoteLine("인터넷 유선 회선", request.networkWiredLines(), + perLine, (long) request.networkWiredLines() * perLine)); + } + if (request.plumbingOutlets() > 0) { + lines.add(new UtilityQuote.QuoteLine("급배수 구", request.plumbingOutlets(), + perPlumb, (long) request.plumbingOutlets() * perPlumb)); + } + if (request.compressedAirOutlets() > 0) { + lines.add(new UtilityQuote.QuoteLine("압축공기 구", request.compressedAirOutlets(), + perAir, (long) request.compressedAirOutlets() * perAir)); + } + long total = lines.stream().mapToLong(UtilityQuote.QuoteLine::amount).sum(); + + return new UtilityQuote(round1(totalPowerKw), requestedKw, boxes, lines, total, + rates.currency() == null ? "KRW" : rates.currency(), + rates.rulesetVersion(), + "공시가 기준이며 최종 금액은 킨텍스 확정 시 안내됩니다."); + } + + @Override + public WiringResult computeWiring(String eventId, String hallId, String boothId, WiringRequest request) { + // TODO(kintex-db-engineer): WiringMapper.findNearestTrenches + shortestPath(PostGIS) 조립. + throw ApiException.notImplemented("배선 산출(트렌치 최단 경로 PostGIS)"); + } + + @Override + public UtilityOrderDto submitOrder(String eventId, String boothId, UtilityQuoteRequest request) { + // TODO(BE/DB): 신청 영속(UtilityOrder) + 위치표시도 PDF 생성(M4-3, 서류 큐) + 릴레이 안내. + throw ApiException.notImplemented("유틸리티 신청 제출 + 위치표시도 생성"); + } + + @Override + public UtilityOrderDto getOrder(String eventId, String boothId) { + // TODO(kintex-db-engineer): UtilityOrder 조회 매퍼. + throw ApiException.notImplemented("유틸리티 신청 조회"); + } + + // --- 요율 맵 탐색 유틸 --- + @SuppressWarnings("unchecked") + private static Object nested(Map map, String a, String b) { + Object inner = map.get(a); + if (inner instanceof Map m) { + return ((Map) m).get(b); + } + return null; + } + + private static long num(Object o) { + if (o instanceof Number n) return n.longValue(); + if (o == null) return 0L; + try { + return Long.parseLong(String.valueOf(o)); + } catch (NumberFormatException e) { + return 0L; + } + } + + private static double round1(double v) { + return Math.round(v * 10.0) / 10.0; + } +} diff --git a/src/backend/src/main/java/com/zioinfo/kintex/module/m4/dto/UtilityOrderDto.java b/src/backend/src/main/java/com/zioinfo/kintex/module/m4/dto/UtilityOrderDto.java new file mode 100644 index 0000000..1d52a10 --- /dev/null +++ b/src/backend/src/main/java/com/zioinfo/kintex/module/m4/dto/UtilityOrderDto.java @@ -0,0 +1,26 @@ +package com.zioinfo.kintex.module.m4.dto; + +import com.fasterxml.jackson.annotation.JsonInclude; + +/** + * 유틸리티 신청 결과(SCR-08 신청 요약·위치표시도). + * + * @param orderId 신청 접수번호 + * @param status 상태 배지(draft/submitted/...) + * @param quote 확정 견적 + * @param locationDiagramUrl 자동 생성 위치표시도(PDF) 다운로드 URL(수기 작도 대체 서식, M4-3) + * @param supplyTiming 공급 시점 안내(예: "장치 마지막 날 오후") + * @param deadlineNotice 마감 고지("유틸리티 신청 마감 D-25 · 인터넷은 현장 추가신청 불가") + * @param relayNotice kxwp/주최자 사무국 수동 릴레이 안내(PLANNING R3) + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public record UtilityOrderDto( + String orderId, + String status, + UtilityQuote quote, + String locationDiagramUrl, + String supplyTiming, + String deadlineNotice, + String relayNotice +) { +} diff --git a/src/backend/src/main/java/com/zioinfo/kintex/module/m4/dto/UtilityQuote.java b/src/backend/src/main/java/com/zioinfo/kintex/module/m4/dto/UtilityQuote.java new file mode 100644 index 0000000..37c039e --- /dev/null +++ b/src/backend/src/main/java/com/zioinfo/kintex/module/m4/dto/UtilityQuote.java @@ -0,0 +1,32 @@ +package com.zioinfo.kintex.module.m4.dto; + +import java.util.List; + +/** + * 유틸리티 자동 견적(SCR-07 우측 견적 패널). + * 금액은 요율 룰셋(rates-vN.json) 기준. 공시가 고지·룰셋 버전을 항상 포함한다(PLANNING R8). + * + * @param totalPowerKw 합산 소비전력(kW) + * @param requestedKw 신청 용량(kW, 올림) + * @param distributionBox50A 자동 산출 분전반 50A 수량 + * @param lines 견적 라인 항목 + * @param total 합계(원) + * @param currency 통화(KRW) + * @param rulesetVersion 적용 요율 룰셋 버전 + * @param disclaimer 공시가 고지 + */ +public record UtilityQuote( + double totalPowerKw, + int requestedKw, + int distributionBox50A, + List lines, + long total, + String currency, + String rulesetVersion, + String disclaimer +) { + + /** 견적 라인 — 항목·수량·단가·금액. */ + public record QuoteLine(String label, int qty, long unitPrice, long amount) { + } +} diff --git a/src/backend/src/main/java/com/zioinfo/kintex/module/m4/dto/UtilityQuoteRequest.java b/src/backend/src/main/java/com/zioinfo/kintex/module/m4/dto/UtilityQuoteRequest.java new file mode 100644 index 0000000..a562dea --- /dev/null +++ b/src/backend/src/main/java/com/zioinfo/kintex/module/m4/dto/UtilityQuoteRequest.java @@ -0,0 +1,25 @@ +package com.zioinfo.kintex.module.m4.dto; + +import jakarta.validation.constraints.Min; + +import java.util.List; + +/** + * 유틸리티 견적 요청(SCR-07 좌측 기기 목록 빌더 + 네트워크/급배수 수량). + * + * @param devices 사용 기기 목록(전기 용량 합산 대상) + * @param networkWiredLines 인터넷 유선 회선 수 + * @param plumbingOutlets 급배수 구 수 + * @param compressedAirOutlets 압축공기 구 수 + */ +public record UtilityQuoteRequest( + List devices, + @Min(0) int networkWiredLines, + @Min(0) int plumbingOutlets, + @Min(0) int compressedAirOutlets +) { + + /** 사용 기기 — 소비전력 합산 단위. */ + public record Device(String name, @Min(0) int count, double powerKw) { + } +} diff --git a/src/backend/src/main/java/com/zioinfo/kintex/module/m4/dto/WiringRequest.java b/src/backend/src/main/java/com/zioinfo/kintex/module/m4/dto/WiringRequest.java new file mode 100644 index 0000000..034df02 --- /dev/null +++ b/src/backend/src/main/java/com/zioinfo/kintex/module/m4/dto/WiringRequest.java @@ -0,0 +1,23 @@ +package com.zioinfo.kintex.module.m4.dto; + +import jakarta.validation.constraints.NotEmpty; + +import java.util.List; + +/** + * 배선 산출 요청(SCR-07 캔버스) — 부스 내 단말 위치(홀 로컬 미터)에서 최근접 트렌치까지 최단 배선. + * + * @param terminals 단말 위치 목록 + */ +public record WiringRequest( + @NotEmpty List terminals +) { + + /** + * @param kind power|network|plumbing|air (색상: 적/청/녹 규약 — design.md §1-2) + * @param position [x, y] 부스 내 단말 위치(미터) + * @param kw 전기 단말 용량(kW, 전기만) + */ + public record Terminal(String kind, List position, Double kw) { + } +} diff --git a/src/backend/src/main/java/com/zioinfo/kintex/module/m4/dto/WiringResult.java b/src/backend/src/main/java/com/zioinfo/kintex/module/m4/dto/WiringResult.java new file mode 100644 index 0000000..036c91e --- /dev/null +++ b/src/backend/src/main/java/com/zioinfo/kintex/module/m4/dto/WiringResult.java @@ -0,0 +1,35 @@ +package com.zioinfo.kintex.module.m4.dto; + +import com.fasterxml.jackson.annotation.JsonInclude; + +import java.util.List; + +/** + * 배선 산출 결과(SCR-07 캔버스 오버레이 + S6 래스터 합성 입력). + * 경로 좌표는 PostGIS 최단 배선(ST_Distance/트렌치 라우팅) 산출값이며, 나노바나나 워커 S6 오버레이의 원천이다. + * + * @param assumedTrench 트렌치 좌표가 실측이 아닌 '가정 그리드'인지 여부(SCR-07 배지 표시, PLANNING R4) + * @param paths 배선 경로 목록 + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public record WiringResult( + boolean assumedTrench, + List paths +) { + + /** + * @param kind power|network|plumbing|air + * @param color red|blue|green (S6 색상 규약) + * @param coords 경로 좌표열 [[x,y], ...] (트렌치 시작점 → 단말) + * @param lengthM 경로 길이(m) + * @param label 경로 라벨(예: "5kW", "유선 1회선") + */ + public record WiringPath( + String kind, + String color, + List> coords, + double lengthM, + String label + ) { + } +} diff --git a/src/backend/src/main/java/com/zioinfo/kintex/module/m4/mapper/WiringMapper.java b/src/backend/src/main/java/com/zioinfo/kintex/module/m4/mapper/WiringMapper.java new file mode 100644 index 0000000..d8afb67 --- /dev/null +++ b/src/backend/src/main/java/com/zioinfo/kintex/module/m4/mapper/WiringMapper.java @@ -0,0 +1,31 @@ +package com.zioinfo.kintex.module.m4.mapper; + +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; +import java.util.Map; + +/** + * 배선 공간 매퍼 — 트렌치 최단 배선(PostGIS). + * TODO(kintex-db-engineer): resources/mybatis/mapper/WiringMapper.xml (M4-1). + * 트렌치는 geometry(POINT), 배선은 geometry(LINESTRING). 최근접 트렌치=ST_Distance/<-> KNN, + * 최단 경로 길이=ST_Length. 홀별 공급 매트릭스(홀1·7 가스 등)·가정 그리드 여부 플래그 포함. + */ +@Mapper +public interface WiringMapper { + + /** 부스 위치 기준 최근접 트렌치 포인트 조회(kind별 공급 가능 트렌치). */ + List> findNearestTrenches(@Param("hallId") String hallId, + @Param("boothId") String boothId, + @Param("kind") String kind); + + /** 단말→트렌치 최단 배선 경로 산출(ST_MakeLine / 라우팅) → coords·lengthM 반환. */ + Map shortestPath(@Param("hallId") String hallId, + @Param("fromX") double fromX, + @Param("fromY") double fromY, + @Param("kind") String kind); + + /** 홀 트렌치 좌표가 실측이 아닌 '가정 그리드'인지 여부(PLANNING R4). */ + Boolean isAssumedTrench(@Param("hallId") String hallId); +} diff --git a/src/backend/src/main/java/com/zioinfo/kintex/module/m5/RenderJobController.java b/src/backend/src/main/java/com/zioinfo/kintex/module/m5/RenderJobController.java new file mode 100644 index 0000000..d83c620 --- /dev/null +++ b/src/backend/src/main/java/com/zioinfo/kintex/module/m5/RenderJobController.java @@ -0,0 +1,58 @@ +package com.zioinfo.kintex.module.m5; + +import com.zioinfo.kintex.auth.EventAccessGuard; +import com.zioinfo.kintex.auth.KintexPrincipal; +import com.zioinfo.kintex.common.ApiResponse; +import com.zioinfo.kintex.module.m5.dto.RenderJobDto; +import com.zioinfo.kintex.module.m5.dto.RenderJobRequest; +import jakarta.validation.Valid; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * M5 나노바나나 시각화 — RenderJob 발행/상태 API (SCR-06/12). 행사 RBAC 가드. + * 발행은 큐 적재까지만(비동기) — 완료는 워커 콜백 → WebSocket /topic/render/{jobId} 푸시. + */ +@RestController +@RequestMapping("/api/events/{eventId}") +public class RenderJobController { + + private final RenderJobService service; + private final EventAccessGuard guard; + + public RenderJobController(RenderJobService service, EventAccessGuard guard) { + this.service = service; + this.guard = guard; + } + + /** POST /booths/{boothId}/render — RenderJob 발행(행사 멤버). 쿼터 초과 시 429. */ + @PostMapping("/booths/{boothId}/render") + public ApiResponse publish(@AuthenticationPrincipal KintexPrincipal principal, + @PathVariable String eventId, + @PathVariable String boothId, + @Valid @RequestBody RenderJobRequest request) { + guard.requireEventAccess(principal, eventId); + return ApiResponse.ok(service.publish(eventId, boothId, request)); + } + + /** GET /render-jobs/{jobId} — 잡 상태 조회. */ + @GetMapping("/render-jobs/{jobId}") + public ApiResponse status(@AuthenticationPrincipal KintexPrincipal principal, + @PathVariable String eventId, + @PathVariable String jobId) { + guard.requireEventAccess(principal, eventId); + return ApiResponse.ok(service.getStatus(eventId, jobId)); + } + + /** GET /booths/{boothId}/render-jobs — 부스 갤러리 목록(SCR-12). */ + @GetMapping("/booths/{boothId}/render-jobs") + public ApiResponse> list(@AuthenticationPrincipal KintexPrincipal principal, + @PathVariable String eventId, + @PathVariable String boothId, + @RequestParam(required = false) String shot) { + guard.requireEventAccess(principal, eventId); + return ApiResponse.ok(service.listByBooth(boothId, shot)); + } +} diff --git a/src/backend/src/main/java/com/zioinfo/kintex/module/m5/RenderJobService.java b/src/backend/src/main/java/com/zioinfo/kintex/module/m5/RenderJobService.java new file mode 100644 index 0000000..2a11878 --- /dev/null +++ b/src/backend/src/main/java/com/zioinfo/kintex/module/m5/RenderJobService.java @@ -0,0 +1,23 @@ +package com.zioinfo.kintex.module.m5; + +import com.zioinfo.kintex.module.m5.dto.RenderJobDto; +import com.zioinfo.kintex.module.m5.dto.RenderJobRequest; +import com.zioinfo.kintex.module.m5.dto.WorkerCallbackRequest; + +import java.util.List; + +/** M5 RenderJob 발행/상태 서비스 계약. */ +public interface RenderJobService { + + /** RenderJob 발행 — 쿼터 확인 후 Redis 큐에 적재. 상태 QUEUED로 반환. */ + RenderJobDto publish(String eventId, String boothId, RenderJobRequest request); + + /** 잡 상태 조회. */ + RenderJobDto getStatus(String eventId, String jobId); + + /** 부스 단위 잡 목록(SCR-12 갤러리) — 내구 저장(DB) 기반. */ + List listByBooth(String boothId, String shotPreset); + + /** 워커 완료/실패 콜백 처리 — 상태 갱신 + WebSocket 푸시(+ 성공 시 쿼터 차감). */ + RenderJobDto handleWorkerCallback(WorkerCallbackRequest callback); +} diff --git a/src/backend/src/main/java/com/zioinfo/kintex/module/m5/RenderJobServiceImpl.java b/src/backend/src/main/java/com/zioinfo/kintex/module/m5/RenderJobServiceImpl.java new file mode 100644 index 0000000..8570162 --- /dev/null +++ b/src/backend/src/main/java/com/zioinfo/kintex/module/m5/RenderJobServiceImpl.java @@ -0,0 +1,173 @@ +package com.zioinfo.kintex.module.m5; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.zioinfo.kintex.common.error.ApiException; +import com.zioinfo.kintex.common.error.ErrorCode; +import com.zioinfo.kintex.module.m5.dto.RenderJobDto; +import com.zioinfo.kintex.module.m5.dto.RenderJobRequest; +import com.zioinfo.kintex.module.m5.dto.WorkerCallbackRequest; +import com.zioinfo.kintex.module.m5.mapper.RenderJobMapper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Service; + +import java.time.OffsetDateTime; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +/** + * M5 서비스. 발행/상태/콜백은 Redis 기반 완성 구현(큐잉·상태 전이·WebSocket 푸시·성공시 쿼터 차감). + * 나노바나나 실호출은 Python 워커 담당(백엔드는 큐 발행까지). 내구 이력(갤러리)은 매퍼 대기(501). + * + *

보안: scene payload는 워커 소비용으로 큐에만 적재하며, GEMINI_API_KEY는 백엔드에서 다루지 않는다. + * 상태 응답에는 워터마크·고지가 항상 포함된다(PLANNING §6-5). + */ +@Service +public class RenderJobServiceImpl implements RenderJobService { + + private static final Logger log = LoggerFactory.getLogger(RenderJobServiceImpl.class); + + private static final String JOB_KEY_PREFIX = "kintex:renderjob:job:"; + private static final String QUOTA_KEY_PREFIX = "kintex:renderjob:quota:"; + + private final StringRedisTemplate redis; + private final ObjectMapper objectMapper; + private final RenderProperties props; + private final RenderNotificationService notifier; + private final RenderJobMapper renderJobMapper; + + public RenderJobServiceImpl(StringRedisTemplate redis, ObjectMapper objectMapper, + RenderProperties props, RenderNotificationService notifier, + RenderJobMapper renderJobMapper) { + this.redis = redis; + this.objectMapper = objectMapper; + this.props = props; + this.notifier = notifier; + this.renderJobMapper = renderJobMapper; + } + + @Override + public RenderJobDto publish(String eventId, String boothId, RenderJobRequest request) { + // 쿼터 확인 — 성공 건수 카운터(성공 시에만 차감되므로 발행 시엔 증가시키지 않음). + String quotaKey = QUOTA_KEY_PREFIX + eventId; + long succeeded = readLong(redis.opsForValue().get(quotaKey)); + if (succeeded >= props.getEventQuotaDefault()) { + throw new ApiException(ErrorCode.RENDER_QUOTA_EXCEEDED, + "행사 이미지 생성 쿼터(" + props.getEventQuotaDefault() + ")를 소진했습니다."); + } + + String jobId = UUID.randomUUID().toString(); + String now = OffsetDateTime.now().toString(); + + // 워커 소비용 큐 payload (§6-2 scene + 메타). 워터마크/고지 계약 포함. + Map jobPayload = new LinkedHashMap<>(); + jobPayload.put("jobId", jobId); + jobPayload.put("eventId", eventId); + jobPayload.put("boothId", boothId); + jobPayload.put("shotPreset", request.shotPreset()); + jobPayload.put("scene", request.scene()); + jobPayload.put("referenceImageUrl", request.referenceImageUrl()); + jobPayload.put("meta", Map.of( + "watermarkRequired", true, + "watermarkText", RenderJobDto.WATERMARK_TEXT, + "notice", RenderJobDto.NOTICE)); + jobPayload.put("queuedAt", now); + + try { + redis.opsForList().leftPush(props.getQueueKey(), objectMapper.writeValueAsString(jobPayload)); + } catch (JsonProcessingException e) { + throw new ApiException(ErrorCode.INTERNAL, "RenderJob 직렬화에 실패했습니다."); + } + + // TODO(kintex-db-engineer): RenderJobMapper.insertJob 으로 내구 이력 기록(갤러리·감사). + RenderJobDto dto = new RenderJobDto(jobId, boothId, request.shotPreset(), + RenderJobStatus.QUEUED.name(), null, null, null, + true, RenderJobDto.WATERMARK_TEXT, RenderJobDto.NOTICE, null, now); + saveState(dto); + log.info("RenderJob 발행: job={} booth={} shot={}", jobId, boothId, request.shotPreset()); + return dto; + } + + @Override + public RenderJobDto getStatus(String eventId, String jobId) { + RenderJobDto dto = readState(jobId); + if (dto == null) { + throw new ApiException(ErrorCode.NOT_FOUND, "해당 RenderJob을 찾을 수 없습니다."); + } + return dto; + } + + @Override + public List listByBooth(String boothId, String shotPreset) { + // TODO(kintex-db-engineer): RenderJobMapper.findByBooth 로 내구 이력 목록(SCR-12) 조립. + throw ApiException.notImplemented("시각화 갤러리 목록(내구 이력)"); + } + + @Override + public RenderJobDto handleWorkerCallback(WorkerCallbackRequest callback) { + RenderJobDto prev = readState(callback.jobId()); + if (prev == null) { + throw new ApiException(ErrorCode.NOT_FOUND, "콜백 대상 RenderJob이 없습니다."); + } + boolean done = "DONE".equalsIgnoreCase(callback.status()); + String status = done ? RenderJobStatus.DONE.name() : RenderJobStatus.FAILED.name(); + + RenderJobDto updated = new RenderJobDto( + prev.jobId(), prev.boothId(), prev.shotPreset(), status, + done ? callback.imageUrl() : null, + callback.schemaHash(), callback.modelVersion(), + true, RenderJobDto.WATERMARK_TEXT, RenderJobDto.NOTICE, + done ? null : safeError(callback.errorMessage()), + prev.createdAt()); + saveState(updated); + + // 성공 시에만 쿼터 차감(PLANNING §6-5). eventId는 상태에 미보관 → 내구 저장 시 정본화(매퍼). + // TODO(kintex-db-engineer): RenderJobMapper.updateStatus + 성공 시 행사 쿼터 카운터 증가. + notifier.pushJobUpdate(updated); + log.info("RenderJob 콜백: job={} status={}", updated.jobId(), status); + return updated; + } + + // --- Redis 상태 저장/조회 --- + private void saveState(RenderJobDto dto) { + try { + redis.opsForValue().set(JOB_KEY_PREFIX + dto.jobId(), objectMapper.writeValueAsString(dto)); + } catch (JsonProcessingException e) { + throw new ApiException(ErrorCode.INTERNAL, "RenderJob 상태 저장에 실패했습니다."); + } + } + + private RenderJobDto readState(String jobId) { + String json = redis.opsForValue().get(JOB_KEY_PREFIX + jobId); + if (json == null) { + return null; + } + try { + return objectMapper.readValue(json, RenderJobDto.class); + } catch (JsonProcessingException e) { + return null; + } + } + + private static long readLong(String s) { + if (s == null) return 0L; + try { + return Long.parseLong(s); + } catch (NumberFormatException e) { + return 0L; + } + } + + /** 워커 에러 메시지는 요약만 통과(스택트레이스·민감정보 유입 방지). */ + private static String safeError(String raw) { + if (raw == null || raw.isBlank()) { + return "이미지 생성에 실패했습니다. 잠시 후 다시 시도하세요."; + } + String oneLine = raw.split("\\R", 2)[0]; + return oneLine.length() > 200 ? oneLine.substring(0, 200) : oneLine; + } +} diff --git a/src/backend/src/main/java/com/zioinfo/kintex/module/m5/RenderJobStatus.java b/src/backend/src/main/java/com/zioinfo/kintex/module/m5/RenderJobStatus.java new file mode 100644 index 0000000..42265e4 --- /dev/null +++ b/src/backend/src/main/java/com/zioinfo/kintex/module/m5/RenderJobStatus.java @@ -0,0 +1,9 @@ +package com.zioinfo.kintex.module.m5; + +/** RenderJob 상태 전이: QUEUED → RUNNING → DONE | FAILED. */ +public enum RenderJobStatus { + QUEUED, + RUNNING, + DONE, + FAILED +} diff --git a/src/backend/src/main/java/com/zioinfo/kintex/module/m5/RenderNotificationService.java b/src/backend/src/main/java/com/zioinfo/kintex/module/m5/RenderNotificationService.java new file mode 100644 index 0000000..bb1adf4 --- /dev/null +++ b/src/backend/src/main/java/com/zioinfo/kintex/module/m5/RenderNotificationService.java @@ -0,0 +1,23 @@ +package com.zioinfo.kintex.module.m5; + +import com.zioinfo.kintex.module.m5.dto.RenderJobDto; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.stereotype.Service; + +/** + * RenderJob 완료/실패를 STOMP 토픽으로 푸시한다(design.md §1-4 — 생성 완료 시 WebSocket 교체). + * 구독 토픽: {@code /topic/render/{jobId}}. + */ +@Service +public class RenderNotificationService { + + private final SimpMessagingTemplate messaging; + + public RenderNotificationService(SimpMessagingTemplate messaging) { + this.messaging = messaging; + } + + public void pushJobUpdate(RenderJobDto job) { + messaging.convertAndSend("/topic/render/" + job.jobId(), job); + } +} diff --git a/src/backend/src/main/java/com/zioinfo/kintex/module/m5/RenderProperties.java b/src/backend/src/main/java/com/zioinfo/kintex/module/m5/RenderProperties.java new file mode 100644 index 0000000..abb550c --- /dev/null +++ b/src/backend/src/main/java/com/zioinfo/kintex/module/m5/RenderProperties.java @@ -0,0 +1,30 @@ +package com.zioinfo.kintex.module.m5; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** kintex.render.* — RenderJob Redis 큐/쿼터 설정(나노바나나 워커 계약). */ +@ConfigurationProperties(prefix = "kintex.render") +public class RenderProperties { + + /** 워커가 소비하는 Redis 큐 키(BRPOP 대상). */ + private String queueKey = "kintex:renderjob:queue"; + + /** 행사별 기본 생성 쿼터(성공 시에만 차감 — PLANNING §6-5). */ + private int eventQuotaDefault = 500; + + public String getQueueKey() { + return queueKey; + } + + public void setQueueKey(String queueKey) { + this.queueKey = queueKey; + } + + public int getEventQuotaDefault() { + return eventQuotaDefault; + } + + public void setEventQuotaDefault(int eventQuotaDefault) { + this.eventQuotaDefault = eventQuotaDefault; + } +} diff --git a/src/backend/src/main/java/com/zioinfo/kintex/module/m5/RenderWorkerCallbackController.java b/src/backend/src/main/java/com/zioinfo/kintex/module/m5/RenderWorkerCallbackController.java new file mode 100644 index 0000000..82a9a47 --- /dev/null +++ b/src/backend/src/main/java/com/zioinfo/kintex/module/m5/RenderWorkerCallbackController.java @@ -0,0 +1,38 @@ +package com.zioinfo.kintex.module.m5; + +import com.zioinfo.kintex.common.ApiResponse; +import com.zioinfo.kintex.common.error.ApiException; +import com.zioinfo.kintex.common.error.ErrorCode; +import com.zioinfo.kintex.module.m5.dto.RenderJobDto; +import com.zioinfo.kintex.module.m5.dto.WorkerCallbackRequest; +import jakarta.validation.Valid; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.web.bind.annotation.*; + +/** + * 나노바나나 Python 워커 → 백엔드 완료 콜백(내부 전용). JWT 사용자가 아니라 워커이므로 + * 공유 시크릿 헤더(X-Worker-Token, env RENDER_WORKER_TOKEN)로 인증한다. 시크릿은 env에서만 로드. + */ +@RestController +@RequestMapping("/api/internal/render") +public class RenderWorkerCallbackController { + + private final RenderJobService service; + private final String workerToken; + + public RenderWorkerCallbackController(RenderJobService service, + @Value("${RENDER_WORKER_TOKEN:}") String workerToken) { + this.service = service; + this.workerToken = workerToken; + } + + /** POST /callback — 워커 완료/실패 통지 → 상태 갱신 + WebSocket 푸시. */ + @PostMapping("/callback") + public ApiResponse callback(@RequestHeader(value = "X-Worker-Token", required = false) String token, + @Valid @RequestBody WorkerCallbackRequest request) { + if (workerToken == null || workerToken.isBlank() || !workerToken.equals(token)) { + throw new ApiException(ErrorCode.UNAUTHORIZED, "워커 인증에 실패했습니다."); + } + return ApiResponse.ok(service.handleWorkerCallback(request)); + } +} diff --git a/src/backend/src/main/java/com/zioinfo/kintex/module/m5/dto/RenderJobDto.java b/src/backend/src/main/java/com/zioinfo/kintex/module/m5/dto/RenderJobDto.java new file mode 100644 index 0000000..a27f88c --- /dev/null +++ b/src/backend/src/main/java/com/zioinfo/kintex/module/m5/dto/RenderJobDto.java @@ -0,0 +1,38 @@ +package com.zioinfo.kintex.module.m5.dto; + +import com.fasterxml.jackson.annotation.JsonInclude; + +/** + * RenderJob 상태·결과(SCR-12 갤러리 카드). 워터마크·고지는 계약상 항상 포함한다(PLANNING §6-5). + * + * @param jobId 잡 식별자 + * @param boothId 부스 식별자 + * @param shotPreset S1~S7 + * @param status QUEUED|RUNNING|DONE|FAILED + * @param imageUrl 완료 시 생성 이미지 URL(오브젝트 스토리지). 미완료 시 null + * @param schemaHash 씬 스키마 해시(캐시 키·메타데이터) + * @param modelVersion 생성 모델 버전(메타데이터) + * @param watermarkRequired 항상 true — 프론트는 워터마크 제거 불가 + * @param watermarkText 워터마크 문구 + * @param notice 사용 제한 고지("계약·심사 서류 사용 금지 — 시공 기준은 도면") + * @param errorMessage 실패 시 사용자 친화 요약(스택트레이스 미포함) + * @param createdAt 생성 요청 시각(ISO-8601) + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public record RenderJobDto( + String jobId, + String boothId, + String shotPreset, + String status, + String imageUrl, + String schemaHash, + String modelVersion, + boolean watermarkRequired, + String watermarkText, + String notice, + String errorMessage, + String createdAt +) { + public static final String WATERMARK_TEXT = "AI 생성 예상 이미지 — 실제 시공 결과와 다를 수 있습니다"; + public static final String NOTICE = "AI 생성 이미지는 계약·심사 서류에 사용할 수 없습니다 — 시공 기준은 도면입니다"; +} diff --git a/src/backend/src/main/java/com/zioinfo/kintex/module/m5/dto/RenderJobRequest.java b/src/backend/src/main/java/com/zioinfo/kintex/module/m5/dto/RenderJobRequest.java new file mode 100644 index 0000000..6b7e181 --- /dev/null +++ b/src/backend/src/main/java/com/zioinfo/kintex/module/m5/dto/RenderJobRequest.java @@ -0,0 +1,21 @@ +package com.zioinfo.kintex.module.m5.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +import java.util.Map; + +/** + * RenderJob 발행 요청(SCR-06/12). scene은 PLANNING §6-2 씬 스키마(hall/booth/design/lighting/wiring). + * 백엔드는 이 payload를 Redis 큐에 발행하고, 나노바나나 Python 워커(tools/nanobanana)가 소비·생성한다. + * + * @param shotPreset S1~S7 (S6는 백엔드 래스터 합성 경로 — 워커가 render_wiring_overlay_raster 사용) + * @param scene §6-2 씬 스키마(구조화 데이터) + * @param referenceImageUrl 구조 보존용 참조 이미지(빈 부스/간이 렌더) URL. 선택 + */ +public record RenderJobRequest( + @NotBlank String shotPreset, + @NotNull Map scene, + String referenceImageUrl +) { +} diff --git a/src/backend/src/main/java/com/zioinfo/kintex/module/m5/dto/WorkerCallbackRequest.java b/src/backend/src/main/java/com/zioinfo/kintex/module/m5/dto/WorkerCallbackRequest.java new file mode 100644 index 0000000..c5d8abd --- /dev/null +++ b/src/backend/src/main/java/com/zioinfo/kintex/module/m5/dto/WorkerCallbackRequest.java @@ -0,0 +1,24 @@ +package com.zioinfo.kintex.module.m5.dto; + +import jakarta.validation.constraints.NotBlank; + +/** + * 나노바나나 Python 워커 → 백엔드 완료/실패 콜백. 백엔드는 이 결과로 상태를 갱신하고 + * WebSocket(/topic/render/{jobId})으로 완료를 푸시한다(design.md §1-4). + * + * @param jobId 잡 식별자 + * @param status DONE | FAILED + * @param imageUrl 성공 시 이미지 URL + * @param schemaHash 씬 스키마 해시(캐시·메타) + * @param modelVersion 모델 버전 + * @param errorMessage 실패 시 요약 메시지(친화 한글, 스택트레이스 금지) + */ +public record WorkerCallbackRequest( + @NotBlank String jobId, + @NotBlank String status, + String imageUrl, + String schemaHash, + String modelVersion, + String errorMessage +) { +} diff --git a/src/backend/src/main/java/com/zioinfo/kintex/module/m5/mapper/RenderJobMapper.java b/src/backend/src/main/java/com/zioinfo/kintex/module/m5/mapper/RenderJobMapper.java new file mode 100644 index 0000000..72ce84b --- /dev/null +++ b/src/backend/src/main/java/com/zioinfo/kintex/module/m5/mapper/RenderJobMapper.java @@ -0,0 +1,32 @@ +package com.zioinfo.kintex.module.m5.mapper; + +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.util.List; +import java.util.Map; + +/** + * RenderJob 영속 매퍼(내구성 저장 — 갤러리/이력/쿼터 정산). + * TODO(kintex-db-engineer): resources/mybatis/mapper/RenderJobMapper.xml (RenderJob 테이블). + * Redis는 큐/실시간 상태, DB는 이력·쿼터의 정본. 성공 시에만 쿼터 차감(PLANNING §6-5). + */ +@Mapper +public interface RenderJobMapper { + + /** 잡 생성 기록 insert. */ + void insertJob(@Param("params") Map params); + + /** 상태·이미지 URL 갱신(워커 콜백 시). */ + void updateStatus(@Param("jobId") String jobId, + @Param("status") String status, + @Param("imageUrl") String imageUrl, + @Param("errorMessage") String errorMessage); + + /** 부스 단위 잡 목록(SCR-12 갤러리). */ + List> findByBooth(@Param("boothId") String boothId, + @Param("shotPreset") String shotPreset); + + /** 행사별 성공 생성 건수(쿼터 산정). */ + int countSucceededByEvent(@Param("eventId") String eventId); +} diff --git a/src/backend/src/main/java/com/zioinfo/kintex/rules/ComplianceReport.java b/src/backend/src/main/java/com/zioinfo/kintex/rules/ComplianceReport.java new file mode 100644 index 0000000..7ed5540 --- /dev/null +++ b/src/backend/src/main/java/com/zioinfo/kintex/rules/ComplianceReport.java @@ -0,0 +1,19 @@ +package com.zioinfo.kintex.rules; + +import java.util.List; + +/** + * 규정 검증 리포트 — M2 배치 검증·M3 설계 사전검증 공통 응답(SCR-09). + * {@code submittable}=false(차단 존재) 시 프론트는 제출 버튼을 비활성화한다. + * {@code rulesetVersion}·{@code disclaimer}는 감사 추적·면책 근거로 항상 포함한다(PLANNING R2). + */ +public record ComplianceReport( + String rulesetVersion, + String disclaimer, + int blockCount, + int warnCount, + int passCount, + boolean submittable, + List violations +) { +} diff --git a/src/backend/src/main/java/com/zioinfo/kintex/rules/ComplianceRuleEngine.java b/src/backend/src/main/java/com/zioinfo/kintex/rules/ComplianceRuleEngine.java new file mode 100644 index 0000000..5fad30d --- /dev/null +++ b/src/backend/src/main/java/com/zioinfo/kintex/rules/ComplianceRuleEngine.java @@ -0,0 +1,143 @@ +package com.zioinfo.kintex.rules; + +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +/** + * 규정 룰 엔진 — 룰셋 데이터(규정) + 측정값 맵을 평가해 {@link ComplianceReport}를 산출한다. + * + *

측정값 맵({@code metrics})은 서비스 계층이 조립한다: 공간값(최소 통로 폭·바닥하중·비상구 차단 수 등)은 + * PostGIS 매퍼 쿼리 결과(kintex-db-engineer), 그 외(높이·방염 여부·간판 등)는 요청/설계 데이터에서. + * 엔진은 데이터 소스를 모르는 순수 평가기이므로 규정 개정 시 JSON만 교체하면 된다. + * + *

지원 연산자: lte, gte, between, isTrue, eq, lteHall(홀별 상한), excludesAll(금지 목록 미포함). + * 측정값이 없는 규칙은 "미평가(=통과로 집계)"로 처리하되 리포트 카운트에는 반영하지 않는다. + */ +@Service +public class ComplianceRuleEngine { + + private final RuleSetLoader ruleSets; + + public ComplianceRuleEngine(RuleSetLoader ruleSets) { + this.ruleSets = ruleSets; + } + + /** + * @param module 대상 모듈("M2"|"M3"|"M4") + * @param metrics 측정값 맵(예: {"booth.height_m":5.4, "layout.min_aisle_width_m":2.8, ...}) + * @param hallId 홀 식별자(홀별 상한 규칙용, 없으면 null) + */ + public ComplianceReport evaluate(String module, Map metrics, String hallId) { + ComplianceRuleSet rs = ruleSets.compliance(); + if (rs == null || rs.rules() == null) { + return new ComplianceReport("compliance-unloaded", null, 0, 0, 0, true, List.of()); + } + List violations = new ArrayList<>(); + int pass = 0; + int pin = 0; + + for (ComplianceRuleSet.Rule rule : rs.rules()) { + if (rule.module() != null && !rule.module().contains(module)) { + continue; + } + Object value = metrics == null ? null : metrics.get(rule.metric()); + Boolean ok = evaluateRule(rule, value, hallId); + if (ok == null) { + continue; // 미평가(측정값 없음) + } + if (ok) { + pass++; + } else { + pin++; + violations.add(new Violation( + pin, rule.code(), rule.group(), rule.label(), + rule.severity(), describeMeasured(rule, value), rule.requiresDocument())); + } + } + + int blocks = (int) violations.stream().filter(v -> "block".equals(v.severity())).count(); + int warns = violations.size() - blocks; + boolean submittable = blocks == 0; + return new ComplianceReport(rs.rulesetVersion(), rs.disclaimer(), + blocks, warns, pass, submittable, violations); + } + + /** @return true=통과, false=위반, null=미평가(측정값 없음). */ + private Boolean evaluateRule(ComplianceRuleSet.Rule rule, Object value, String hallId) { + String op = rule.operator(); + if (op == null) { + return null; + } + switch (op) { + case "lte": { + Double v = asDouble(value); + return v == null ? null : v <= nz(rule.threshold()); + } + case "gte": { + Double v = asDouble(value); + return v == null ? null : v >= nz(rule.threshold()); + } + case "eq": { + Double v = asDouble(value); + return v == null ? null : v.doubleValue() == nz(rule.threshold()); + } + case "between": { + Double v = asDouble(value); + if (v == null) return null; + return v >= nz(rule.min()) && v <= nz(rule.max()); + } + case "isTrue": { + Boolean b = asBoolean(value); + return b == null ? null : b; + } + case "lteHall": { + Double v = asDouble(value); + if (v == null) return null; + Map limits = rule.hallLimits(); + double limit = limits == null ? Double.MAX_VALUE + : limits.getOrDefault(hallId, limits.getOrDefault("default", Double.MAX_VALUE)); + return v <= limit; + } + case "excludesAll": { + if (!(value instanceof Collection col)) return null; + List forbidden = rule.forbidden(); + if (forbidden == null) return true; + return col.stream().map(String::valueOf).noneMatch(forbidden::contains); + } + default: + return null; + } + } + + private String describeMeasured(ComplianceRuleSet.Rule rule, Object value) { + if (value == null) return null; + String unit = rule.unit() == null ? "" : rule.unit(); + return "측정 " + value + unit; + } + + private static double nz(Double d) { + return d == null ? 0.0 : d; + } + + private static Double asDouble(Object o) { + if (o == null) return null; + if (o instanceof Number n) return n.doubleValue(); + try { + return Double.parseDouble(String.valueOf(o)); + } catch (NumberFormatException e) { + return null; + } + } + + private static Boolean asBoolean(Object o) { + if (o == null) return null; + if (o instanceof Boolean b) return b; + String s = String.valueOf(o).trim().toLowerCase(); + if (s.equals("true") || s.equals("false")) return Boolean.parseBoolean(s); + return null; + } +} diff --git a/src/backend/src/main/java/com/zioinfo/kintex/rules/ComplianceRuleSet.java b/src/backend/src/main/java/com/zioinfo/kintex/rules/ComplianceRuleSet.java new file mode 100644 index 0000000..77f536c --- /dev/null +++ b/src/backend/src/main/java/com/zioinfo/kintex/rules/ComplianceRuleSet.java @@ -0,0 +1,38 @@ +package com.zioinfo.kintex.rules; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +import java.util.List; +import java.util.Map; + +/** + * 규정 룰셋 데이터 모델 — compliance-vN.json 역직렬화 대상. + * 검증 리포트에 {@code rulesetVersion} 을 기록해 감사 추적·면책 근거로 남긴다(PLANNING R2). + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public record ComplianceRuleSet( + String rulesetVersion, + String effectiveDate, + String disclaimer, + List rules +) { + + @JsonIgnoreProperties(ignoreUnknown = true) + public record Rule( + String code, + String group, + String label, + List module, + String severity, // "block" | "warn" + String metric, + String operator, // lte|gte|between|isTrue|eq|lteHall|excludesAll ... + Double threshold, + Double min, + Double max, + String unit, + String requiresDocument, + Map hallLimits, + List forbidden + ) { + } +} diff --git a/src/backend/src/main/java/com/zioinfo/kintex/rules/RateRuleSet.java b/src/backend/src/main/java/com/zioinfo/kintex/rules/RateRuleSet.java new file mode 100644 index 0000000..f7ecf23 --- /dev/null +++ b/src/backend/src/main/java/com/zioinfo/kintex/rules/RateRuleSet.java @@ -0,0 +1,21 @@ +package com.zioinfo.kintex.rules; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +import java.util.Map; + +/** + * 요율 룰셋 데이터 모델 — rates-vN.json 역직렬화 대상. M1 견적·M4 유틸리티 견적의 단일 출처. + * 상세 규칙(성수기/전시장 할증 등)은 유연성을 위해 Map으로 보존하고, 서비스가 필요한 키를 조회한다. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public record RateRuleSet( + String rulesetVersion, + String effectiveDate, + String disclaimer, + String currency, + Map rental, + Map utility, + Map boothStandard +) { +} diff --git a/src/backend/src/main/java/com/zioinfo/kintex/rules/RuleProperties.java b/src/backend/src/main/java/com/zioinfo/kintex/rules/RuleProperties.java new file mode 100644 index 0000000..aa14543 --- /dev/null +++ b/src/backend/src/main/java/com/zioinfo/kintex/rules/RuleProperties.java @@ -0,0 +1,31 @@ +package com.zioinfo.kintex.rules; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.core.io.Resource; + +/** + * kintex.rules.* — 버전 관리되는 룰셋 데이터 위치. + * 규정(compliance)·요율(rate)은 코드가 아닌 데이터로 유지해 킨텍스 규정 개정에 대응한다(PLANNING §8). + */ +@ConfigurationProperties(prefix = "kintex.rules") +public class RuleProperties { + + private Resource complianceRuleset; + private Resource rateRuleset; + + public Resource getComplianceRuleset() { + return complianceRuleset; + } + + public void setComplianceRuleset(Resource complianceRuleset) { + this.complianceRuleset = complianceRuleset; + } + + public Resource getRateRuleset() { + return rateRuleset; + } + + public void setRateRuleset(Resource rateRuleset) { + this.rateRuleset = rateRuleset; + } +} diff --git a/src/backend/src/main/java/com/zioinfo/kintex/rules/RuleSetLoader.java b/src/backend/src/main/java/com/zioinfo/kintex/rules/RuleSetLoader.java new file mode 100644 index 0000000..392055b --- /dev/null +++ b/src/backend/src/main/java/com/zioinfo/kintex/rules/RuleSetLoader.java @@ -0,0 +1,68 @@ +package com.zioinfo.kintex.rules; + +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.annotation.PostConstruct; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.io.Resource; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.io.InputStream; + +/** + * 룰셋 데이터 로더 — 기동 시 규정·요율 JSON을 메모리에 적재한다. + * 룰셋은 버전 관리되는 데이터이므로 교체(규정 개정) 시 코드 변경 없이 리소스만 갱신한다. + * (Phase 2: DB 기반 버전 테이블 + 핫리로드로 확장 가능 — 현재는 classpath 리소스 로딩 구조만.) + */ +@Component +public class RuleSetLoader { + + private static final Logger log = LoggerFactory.getLogger(RuleSetLoader.class); + + private final RuleProperties props; + private final ObjectMapper objectMapper; + + private volatile ComplianceRuleSet compliance; + private volatile RateRuleSet rates; + + public RuleSetLoader(RuleProperties props, ObjectMapper objectMapper) { + this.props = props; + this.objectMapper = objectMapper; + } + + @PostConstruct + public void load() { + this.compliance = read(props.getComplianceRuleset(), ComplianceRuleSet.class, "compliance"); + this.rates = read(props.getRateRuleset(), RateRuleSet.class, "rate"); + if (compliance != null) { + log.info("규정 룰셋 로드: {} (규칙 {}개)", compliance.rulesetVersion(), + compliance.rules() == null ? 0 : compliance.rules().size()); + } + if (rates != null) { + log.info("요율 룰셋 로드: {}", rates.rulesetVersion()); + } + } + + private T read(Resource resource, Class type, String kind) { + if (resource == null) { + log.warn("{} 룰셋 리소스가 설정되지 않았습니다.", kind); + return null; + } + try (InputStream in = resource.getInputStream()) { + return objectMapper.readValue(in, type); + } catch (IOException e) { + // 룰셋 로드 실패는 기동을 막지 않되(운영 안전), 명확히 경고. 사용 시점에 null 가드. + log.error("{} 룰셋 로드 실패: {}", kind, e.getMessage()); + return null; + } + } + + public ComplianceRuleSet compliance() { + return compliance; + } + + public RateRuleSet rates() { + return rates; + } +} diff --git a/src/backend/src/main/java/com/zioinfo/kintex/rules/Violation.java b/src/backend/src/main/java/com/zioinfo/kintex/rules/Violation.java new file mode 100644 index 0000000..098d739 --- /dev/null +++ b/src/backend/src/main/java/com/zioinfo/kintex/rules/Violation.java @@ -0,0 +1,26 @@ +package com.zioinfo.kintex.rules; + +import com.fasterxml.jackson.annotation.JsonInclude; + +/** + * 규정 위반/확인 항목 — 도면 위 번호 핀과 1:1 연결(design.md ViolationFlag). + * + * @param pin 도면 하이라이트 핀 번호(1부터) + * @param code 규칙 코드(HEIGHT_MAX 등) + * @param group 규칙 그룹(height/rigging/egress ...) + * @param label 규칙 원문 요약(한글) + * @param severity "block"(차단·제출 불가) | "warn"(경고·확인 후 진행) + * @param measured 측정값 요약(예: "5.4m — 0.4m 초과"). 값 미제공 시 null + * @param requiresDocument 부속 제출 요구(예: STRUCTURAL_CALC_D7). 없으면 null + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public record Violation( + int pin, + String code, + String group, + String label, + String severity, + String measured, + String requiresDocument +) { +} diff --git a/src/backend/src/main/resources/application.yml b/src/backend/src/main/resources/application.yml new file mode 100644 index 0000000..3124a09 --- /dev/null +++ b/src/backend/src/main/resources/application.yml @@ -0,0 +1,55 @@ +# 킨텍스 AI 전시관리 시스템 — 백엔드 설정 +# 모든 시크릿·엔드포인트는 환경변수 플레이스홀더로만 주입한다(하드코딩 금지). +# - DB_URL / DB_USER / DB_PASSWORD : PostgreSQL(PostGIS) 접속 +# - REDIS_HOST / REDIS_PORT : Redis 작업 큐 +# - JWT_SECRET : JWT 서명 키(HS256, 최소 32바이트) +# - GEMINI_API_KEY : 나노바나나 Python 워커 전용(백엔드는 큐 발행만; 여기서 미사용) +server: + port: ${SERVER_PORT:8080} + error: + include-stacktrace: never # 스택트레이스 미노출(보안 불변) + include-message: never + include-binding-errors: never + +spring: + application: + name: kintex-backend + datasource: + url: ${DB_URL:jdbc:postgresql://localhost:5432/kintex_db} + username: ${DB_USER:kintex} + password: ${DB_PASSWORD:} + driver-class-name: org.postgresql.Driver + hikari: + maximum-pool-size: ${DB_POOL_MAX:3} # GUARDiA 표준(공유 PG 보호) + pool-name: kintex-hikari + data: + redis: + host: ${REDIS_HOST:localhost} + port: ${REDIS_PORT:6379} + password: ${REDIS_PASSWORD:} + +# MyBatis — 공간 SQL 매퍼 XML은 kintex-db-engineer가 채운다(D-2/M2-1/M4-1). +mybatis: + mapper-locations: classpath*:mybatis/mapper/**/*.xml + configuration: + map-underscore-to-camel-case: true + jdbc-type-for-null: NULL + +# 애플리케이션 커스텀 설정 +kintex: + auth: + jwt-secret: ${JWT_SECRET:CHANGE_ME_DEV_ONLY_kintex_local_secret_key_min_32_bytes} + access-token-ttl-seconds: ${JWT_ACCESS_TTL:3600} + rules: + # 버전 관리되는 룰셋 데이터(규정·요율) — 코드가 아닌 데이터로 유지(킨텍스 규정 개정 대응) + compliance-ruleset: classpath:rulesets/compliance-v1.json + rate-ruleset: classpath:rulesets/rates-v1.json + render: + # RenderJob Redis 큐 이름(나노바나나 워커가 소비) — visualizer와 합의한 계약 + queue-key: ${RENDER_QUEUE_KEY:kintex:renderjob:queue} + event-quota-default: ${RENDER_EVENT_QUOTA:500} + +logging: + level: + root: INFO + com.zioinfo.kintex: DEBUG diff --git a/src/backend/src/main/resources/rulesets/compliance-v1.json b/src/backend/src/main/resources/rulesets/compliance-v1.json new file mode 100644 index 0000000..efe3877 --- /dev/null +++ b/src/backend/src/main/resources/rulesets/compliance-v1.json @@ -0,0 +1,136 @@ +{ + "rulesetVersion": "compliance-v1.0", + "effectiveDate": "2026-07-11", + "source": "PLANNING.md v1.2 §7-1 규정 룰셋 / 참가업체·전시주최자 매뉴얼", + "disclaimer": "본 룰셋은 사전 필터이며 최종 승인은 킨텍스 및 구조기술사의 판단에 따른다.", + "rules": [ + { + "code": "HEIGHT_MAX", + "group": "height", + "label": "장치물 높이 5m 이하", + "module": ["M2", "M3"], + "severity": "block", + "metric": "booth.height_m", + "operator": "lte", + "threshold": 5.0, + "unit": "m" + }, + { + "code": "RIGGING_RANGE", + "group": "rigging", + "label": "리깅 천장 6.5~8.5m — 구조계산서 D-7 제출 필요", + "module": ["M3"], + "severity": "warn", + "metric": "rigging.height_m", + "operator": "between", + "min": 6.5, + "max": 8.5, + "unit": "m", + "requiresDocument": "STRUCTURAL_CALC_D7" + }, + { + "code": "MEZZANINE_RATIO", + "group": "mezzanine", + "label": "복층부스 바닥면적 1/2 이내", + "module": ["M2", "M3"], + "severity": "block", + "metric": "mezzanine.area_ratio", + "operator": "lte", + "threshold": 0.5 + }, + { + "code": "FIRE_RETARDANT", + "group": "fire", + "label": "전 자재 방염(A급) 필수", + "module": ["M3"], + "severity": "block", + "metric": "materials.all_fire_retardant", + "operator": "isTrue" + }, + { + "code": "FLOOR_LOAD", + "group": "structure", + "label": "홀별 바닥하중 초과 금지(홀6 2t/㎡ · 홀7~10 5t/㎡)", + "module": ["M2"], + "severity": "block", + "metric": "booth.floor_load_t_per_m2", + "operator": "lteHall", + "hallLimits": { "H6": 2.0, "H7": 5.0, "H8": 5.0, "H9": 5.0, "H10": 5.0, "default": 5.0 }, + "unit": "t/㎡" + }, + { + "code": "AISLE_WIDTH_MIN", + "group": "egress", + "label": "피난 통로 폭 최소 3m", + "module": ["M2"], + "severity": "block", + "metric": "layout.min_aisle_width_m", + "operator": "gte", + "threshold": 3.0, + "unit": "m", + "note": "PostGIS ST_Buffer/ST_Distance 기반 서버 산출값과 대조" + }, + { + "code": "EXIT_ACCESS", + "group": "egress", + "label": "비상구 접근성 확보(부스가 비상구 차단 금지)", + "module": ["M2"], + "severity": "block", + "metric": "layout.exits_blocked_count", + "operator": "eq", + "threshold": 0 + }, + { + "code": "CLEARANCE_WALL", + "group": "clearance", + "label": "인접 벽 이격 30cm 이상", + "module": ["M2", "M3"], + "severity": "warn", + "metric": "booth.clearance_wall_m", + "operator": "gte", + "threshold": 0.3, + "unit": "m" + }, + { + "code": "CLEARANCE_CEILING", + "group": "clearance", + "label": "천장 이격 60cm 이상", + "module": ["M3"], + "severity": "warn", + "metric": "booth.clearance_ceiling_m", + "operator": "gte", + "threshold": 0.6, + "unit": "m" + }, + { + "code": "NOISE_LIMIT", + "group": "onsite", + "label": "장내 소음 70~75dB 이하", + "module": ["M3"], + "severity": "warn", + "metric": "onsite.noise_db", + "operator": "lte", + "threshold": 75.0, + "unit": "dB" + }, + { + "code": "PROHIBITED_WORK", + "group": "onsite", + "label": "장내 금지작업(전기톱·용접·페인트) 미포함", + "module": ["M3"], + "severity": "warn", + "metric": "onsite.prohibited_works", + "operator": "excludesAll", + "forbidden": ["chainsaw", "welding", "painting"] + }, + { + "code": "LIGHTING_BRING_IN", + "group": "onsite", + "label": "조명 반입 금지(지정 조명만 사용)", + "module": ["M3", "M4"], + "severity": "warn", + "metric": "lighting.uses_designated_only", + "operator": "isTrue" + } + ] +} diff --git a/src/backend/src/main/resources/rulesets/rates-v1.json b/src/backend/src/main/resources/rulesets/rates-v1.json new file mode 100644 index 0000000..b11b879 --- /dev/null +++ b/src/backend/src/main/resources/rulesets/rates-v1.json @@ -0,0 +1,48 @@ +{ + "rulesetVersion": "rates-v1.0", + "effectiveDate": "2026-07-11", + "source": "PLANNING.md v1.2 §7-1 요율 마스터 / 유틸리티 요금(참가업체 매뉴얼)", + "disclaimer": "공시가 기준이며 최종 금액은 킨텍스 확정 시 안내된다. 인터넷 요금(150,000 vs 80,000) 정합 확인 필요.", + "currency": "KRW", + "rental": { + "exhibitionHallPerM2": 2250, + "lobbyPerM2": 10000, + "outdoorPerM2": 2000, + "eventHallPerM2": 2420, + "baseHours": 12, + "setupFreeHours": 6, + "overtimePerHourRatio": 0.1, + "peakSurchargeRatio": 0.10, + "offPeakDiscountRatio": -0.10, + "hall1SurchargeRatio": 0.10, + "offLabelSurchargeRatio": 0.30, + "depositRatioMin": 0.15, + "depositRatioMax": 0.20, + "peakMonths": [3, 4, 5, 9, 10, 11], + "offPeakMonths": [1, 2, 7, 12] + }, + "utility": { + "power": { + "perKw": 55000, + "unitLabel": "220V 단상 1kW", + "distributionBox50A": 100000 + }, + "network": { + "internetWiredPerLine": 150000, + "ktRelayPerLineNote": 80000, + "note": "요금 정합성 킨텍스 확인 필요" + }, + "plumbing": { + "perOutlet": 150000, + "unitLabel": "급수15mm/배수25mm 1구" + }, + "compressedAir": { + "perOutlet": 150000, + "unitLabel": "내경 8mm 1구" + } + }, + "boothStandard": { + "assembled": { "includedPowerKw": 1.0, "spotlights": 5, "outlets220v": 1, "floorFireRetardant": "A" }, + "premium": { "sizeM": [6, 3, 4], "includedPowerKw": 2.0 } + } +} diff --git a/tools/nanobanana/_workspace/01_worker_contract.md b/tools/nanobanana/_workspace/01_worker_contract.md new file mode 100644 index 0000000..049c01c --- /dev/null +++ b/tools/nanobanana/_workspace/01_worker_contract.md @@ -0,0 +1,166 @@ +# 나노바나나 워커 ↔ 백엔드 RenderJob 계약 (v1) + +> 대상: `tools/nanobanana/worker.py`(Python 워커 사이드카) ↔ Spring Boot 백엔드(M5-1). +> 근거: PLANNING §6(나노바나나 파이프라인 v1.2) · §8(아키텍처) · IMPLEMENTATION_BACKLOG S-4/M5-1~M5-3. +> 이 문서는 **큐 메시지 형식**과 **완료 이벤트 형식**의 단일 출처다. 백엔드 M5 계약과 정합해야 한다. + +--- + +## 1. 파이프라인 위치 + +``` +Spring Boot(M5-1) Redis Python 워커(S-4 / M5-2·M5-3) 오브젝트 스토리지 / WebSocket +───────────────── ─────── ───────────────────────── ────────────────────────── +RenderJob 발행 ──LPUSH────▶ list: kintex:renderjobs ──BLPOP──▶ worker.process_job() + │ scene → build_booth_prompt + │ → render_shot (생성형) + │ 또는 S6 → render_wiring_overlay_raster (래스터) + │ → 이미지 + 메타데이터 + ├──put──▶ object storage (이미지 + .meta.json 사이드카) + └──PUBLISH──▶ channel: kintex:renderjob:events + │ +Spring 구독 ◀──완료 이벤트────────────────────────────────────────────────────────────┘ + └─ WebSocket/STOMP 로 프론트에 진행·완료 푸시 +``` + +- 워커는 **상태 기계의 소비자**일 뿐, RenderJob 원본(상태·쿼터·캐시)의 소유는 백엔드(PostgreSQL RenderJob 테이블)다. +- 워커는 결과 이미지·메타데이터를 오브젝트 스토리지에 적재하고 **완료 이벤트만** 발행한다. DB 상태 전이는 백엔드가 이벤트를 받아 수행한다. + +--- + +## 2. 큐 메시지 — RenderJob (백엔드 → 워커) + +- 큐: Redis list `kintex:renderjobs` (env `NANOBANANA_QUEUE`). +- 인입: `LPUSH` / 소비: `BLPOP`(워커, timeout 폴링). +- 페이로드: UTF-8 JSON 1건. + +| 필드 | 타입 | 필수 | 설명 | +|---|---|---|---| +| `job_id` | string | ✔ | RenderJob PK(백엔드 발급). 이벤트·산출물 키의 상관 ID. | +| `event_id` | string | | 행사 ID. 스토리지 경로·쿼터 스코프. | +| `booth_id` | string | | 부스 ID. 스토리지 경로·상관. | +| `shot_preset` | string | ✔ | `S1`~`S7` (PLANNING §6-3). `S6`은 래스터 합성 경로. | +| `scene` | object | ✔* | PLANNING §6-2 scene 스키마(hall·booth·design·lighting·wiring). 생성형 샷 필수. | +| `seed` | int | | 컷 간 일관성(B-12). 생략 시 참조체인 일관성 폴백. | +| `reference_image` | string | | 참조 이미지 경로/URI(빈 부스 실측·간이 렌더). 구조 보존. 목 모드는 무시. | +| `layers` | string[] | | 활성 레이어(`furniture`·`lighting`·`electrical`·`network`). 생략 시 샷별 기본값. | +| `wiring` | object | ✔** | S6 전용. `scene.wiring` 미제공 시 사용. `{power,network,plumbing}[]`. | +| `hall_dims_m` | [number,number] | | S6 좌표계 기준(m). 생략 시 booth.size_m → hall.dims_m 폴백. | +| `options` | object | | `px_per_m`(S6), `kinds`(S6 렌더 레이어) 등 렌더 옵션. | + +`*` 생성형 샷(S1~S5,S7)은 `scene` 필수. `**` S6은 `scene.wiring` 또는 최상위 `wiring` 중 하나 필수. + +예: +```json +{ + "job_id": "rj_01H...", + "event_id": "evt_2026_kes", + "booth_id": "A-102", + "shot_preset": "S2", + "seed": 12345, + "reference_image": "s3://kintex/refs/hall7_empty.jpg", + "scene": { + "hall": {"id": "제1전시장 7홀", "dims_m": [126, 90], "ceiling_m": 12}, + "booth": {"id": "A-102", "size_m": [6, 3], "type": "independent"}, + "design": {"signage": {"text": "주식회사 가디아"}, "brand_color": "#0052A5"}, + "lighting": {"mode": "night", "color_temp_k": 4000}, + "render_hints": {"style": "tech"} + } +} +``` + +--- + +## 3. 완료 이벤트 (워커 → 백엔드) + +- 채널: Redis pub/sub `kintex:renderjob:events` (env `NANOBANANA_EVENT_CHANNEL`). +- 발행: `PUBLISH`. 백엔드가 구독해 WebSocket/STOMP로 릴레이(§8). +- 페이로드: UTF-8 JSON 1건. + +| 필드 | 타입 | 설명 | +|---|---|---| +| `type` | string | `renderjob.completed` \| `renderjob.failed`. | +| `job_id` | string | 상관 ID. | +| `event_id` / `booth_id` / `shot_preset` | string | 에코백. | +| `status` | string | `DONE`(생성/합성 성공) \| `FAILED`. | +| `image_ref` | object | 산출물 참조(§4). 실패 시 `null`. | +| `meta` | object | 이미지 메타데이터(§5). 실패 시 부분. | +| `error` | object | `{code, message}` — 실패 시. 스택트레이스·키 **미포함**. | +| `emitted_at` | string(ISO-8601 UTC) | 이벤트 발행 시각. | + +- 에러 코드(친화적 분류, PLANNING §6-5): `AUTH`(키 무효) · `QUOTA`(429·쿼터) · `SAFETY` · `BAD_REQUEST` · `RENDER_ERROR`. +- **쿼터는 성공(`DONE`) 시에만 차감** — 백엔드가 완료 이벤트 수신 시 처리(실패는 소모 안 함). + +--- + +## 4. `image_ref` — 산출물 참조 + +```json +{ + "backend": "local" | "s3" | "gcs", + "key": "evt_2026_kes/A-102/rj_01H..._S2.png", + "uri": "file:///.../output/visualizations/evt_2026_kes/A-102/rj_01H..._S2.png", + "sidecar_key": "evt_2026_kes/A-102/rj_01H..._S2.png.meta.json", + "content_type": "image/png" +} +``` + +- 사이드카 `.meta.json`은 이미지와 **항상 함께** 적재된다(결정적, B-03). +- 기본 백엔드는 로컬 파일시스템(`NANOBANANA_OUTPUT_DIR`, degraded 오브젝트 스토리지). 운영 시 S3/GCS 어댑터로 교체. + +--- + +## 5. `meta` — 이미지 메타데이터 (§6-6 워터마크·고지 필수) + +`client.build_metadata` 산출 + 워커 증강: + +| 필드 | 설명 | +|---|---| +| `generated_at` | 생성 시각(ISO-8601 UTC). | +| `schema_hash` | scene 스키마 SHA-256(정렬 직렬화). **동일 해시 캐시 키**(§6-4). | +| `model_version` | 모델명(`gemini-3.1-flash-image-preview` 등) 또는 `mock`. | +| `shot_preset` | 샷 프리셋. | +| `seed` | 시드(있으면). | +| `render_path` | `generative` \| `backend_raster_composite`(S6). | +| `watermark_required` | 항상 `true`. | +| `watermark_text` | "AI 생성 예상 이미지 — 실제 시공 결과와 다를 수 있음". | +| `notice` | "계약·심사 서류 사용 금지(도면만 유효)". | +| `live` | `true`(실 Gemini 호출) \| `false`(목/degraded). | +| `degraded` | `true`면 플레이스홀더(G1 미승인·목 모드). | + +--- + +## 6. G1 게이트 — 실 Gemini 호출 조건 (PLANNING R12) + +| 조건 | 결과 | +|---|---| +| `NANOBANANA_LIVE=1` **그리고** `GEMINI_API_KEY` 존재 | **실 호출**(생성형 샷 → `NanoBananaClient.render_shot`). | +| 그 외(기본) | **목/degraded 모드** — 플레이스홀더 이미지 + 정상 메타데이터. 파이프라인 구조는 성립. | + +- 목 모드는 키·네트워크 없이 동작한다. `GEMINI_API_KEY`는 **불필요**하며 어디에도 로그·기록하지 않는다. +- **S6(래스터 합성)은 G1과 무관** — 항상 로컬 PIL 결정적 합성(생성형 아님). 목/live 모두 동일 경로. +- 소유자 승인(G1) 확정 후 `NANOBANANA_LIVE=1` + 서버 env `GEMINI_API_KEY` 설정으로 무코드변경 전환. + +--- + +## 7. 환경변수 + +| 변수 | 기본값 | 용도 | +|---|---|---| +| `NANOBANANA_LIVE` | (미설정=목) | `1`일 때만 실 Gemini 호출 시도(+키 필요). | +| `GEMINI_API_KEY` | — | 실 호출 시에만. 코드·로그·이벤트·응답 기록 금지. | +| `NANOBANANA_MODEL` | `gemini-3.1-flash-image-preview` | 모델 오버라이드(`client.MODEL_NAME`). | +| `REDIS_URL` | `redis://localhost:6379/0` | 큐/이벤트 연결(지연 연결). | +| `NANOBANANA_QUEUE` | `kintex:renderjobs` | RenderJob 큐 리스트 키. | +| `NANOBANANA_EVENT_CHANNEL` | `kintex:renderjob:events` | 완료 이벤트 pub/sub 채널. | +| `NANOBANANA_OUTPUT_DIR` | `output/visualizations` | 로컬 오브젝트 스토리지 루트. | + +--- + +## 8. 운영 불변식 + +1. **지연 연결**: Redis 미기동이어도 `import`·`process_job(job)` 직접 호출은 성립(스모크·단위 테스트용). +2. **결정적 사이드카**: 모든 산출물에 `.meta.json` 동반(§5). +3. **성공 시에만 쿼터 차감**: 실패 이벤트는 소모 신호 아님. +4. **비밀 미노출**: 이벤트·로그·에러에 키·IP·스택트레이스 금지(§6-5, GUARDiA 보안 제약). +5. **S6 결정성**: 배선 오버레이는 생성 모델 미개입(좌표 정합 목적). diff --git a/tools/nanobanana/worker.py b/tools/nanobanana/worker.py new file mode 100644 index 0000000..1895e82 --- /dev/null +++ b/tools/nanobanana/worker.py @@ -0,0 +1,495 @@ +"""나노바나나 RenderJob 워커 사이드카 (스켈레톤). + +Spring Boot 백엔드가 Redis 큐에 넣은 RenderJob 을 소비해, PLANNING §6-2 scene 을 +사실화 사진(S1~S5,S7 생성형) 또는 배선 오버레이(S6 래스터 합성)로 렌더하고, +결과 이미지 + 메타데이터를 오브젝트 스토리지에 적재한 뒤 완료 이벤트를 발행한다. + +설계 기준: + - PLANNING §6(나노바나나 파이프라인 v1.2) · §8(Python 워커 사이드카 아키텍처). + - IMPLEMENTATION_BACKLOG S-4(워커 골격) / M5-2(생성) / M5-3(S6 래스터). + - 계약 단일 출처: `_workspace/01_worker_contract.md`. + - 호출 스택·프롬프트·방어 로직은 `client.py`(ReRoomAI 검증 패턴) 재사용. + +핵심 성질(운영 불변식): + 1. G1 게이트 — 실 Gemini 호출은 env `NANOBANANA_LIVE=1` + `GEMINI_API_KEY` 있을 때만. + 기본은 목/degraded 모드(키·네트워크 없이 플레이스홀더 이미지 + 정상 메타데이터). + 2. 지연 연결 — Redis 미기동이어도 import·process_job() 직접 호출은 성립. + 3. S6 은 생성형 아님 — 항상 로컬 PIL 결정적 래스터 합성(좌표 정합). + 4. 비밀 미노출 — 키/IP/스택트레이스를 이벤트·로그·에러에 기록하지 않는다. + +CLI: + python -m tools.nanobanana.worker # 큐 소비 루프(BLPOP) + python -m tools.nanobanana.worker --smoke # 무네트워크 스모크(목 잡 1건 처리) +""" + +from __future__ import annotations + +import argparse +import base64 +import json +import os +import sys +from dataclasses import dataclass, field +from datetime import datetime, timezone +from pathlib import Path +from typing import Any, Optional + +# ---------------------------------------------------------------------------- +# client.py 재사용 — 모듈 실행(`-m`)·직접 실행 양쪽 지원 +# ---------------------------------------------------------------------------- +try: # 패키지 컨텍스트 (python -m tools.nanobanana.worker) + from .client import ( # type: ignore + MODEL_NAME, + SHOT_PRESETS, + WIRING_COLORS, + GeneratedImage, + NanoBananaAuthError, + NanoBananaError, + NanoBananaQuotaError, + NanoBananaSafetyError, + build_booth_prompt, + build_metadata, + render_wiring_overlay_raster, + ) +except ImportError: # 스크립트 컨텍스트 (python worker.py) — 동일 디렉터리 임포트 + sys.path.insert(0, str(Path(__file__).resolve().parent)) + from client import ( # type: ignore + MODEL_NAME, + SHOT_PRESETS, + WIRING_COLORS, + GeneratedImage, + NanoBananaAuthError, + NanoBananaError, + NanoBananaQuotaError, + NanoBananaSafetyError, + build_booth_prompt, + build_metadata, + render_wiring_overlay_raster, + ) + + +# ---------------------------------------------------------------------------- +# 환경 설정 (계약 §7) +# ---------------------------------------------------------------------------- +REDIS_URL = os.environ.get("REDIS_URL", "redis://localhost:6379/0") +QUEUE_KEY = os.environ.get("NANOBANANA_QUEUE", "kintex:renderjobs") +EVENT_CHANNEL = os.environ.get("NANOBANANA_EVENT_CHANNEL", "kintex:renderjob:events") +OUTPUT_DIR = os.environ.get("NANOBANANA_OUTPUT_DIR", "output/visualizations") + +#: BLPOP 폴링 간격(초) — None 무한 대기 대신 짧은 폴링으로 graceful stop 가능. +BLPOP_TIMEOUT_S = 5 + + +def is_live() -> bool: + """G1 게이트: 실 Gemini 호출 조건(PLANNING R12). + + NANOBANANA_LIVE=1 그리고 GEMINI_API_KEY 가 있을 때만 True. + 그 외에는 목/degraded 모드(기본). 키 값 자체는 참조만 하고 반환·로그하지 않는다. + """ + flag = os.environ.get("NANOBANANA_LIVE", "").strip().lower() in ("1", "true", "yes", "on") + has_key = bool(os.environ.get("GEMINI_API_KEY")) + return flag and has_key + + +# ---------------------------------------------------------------------------- +# 목/degraded 플레이스홀더 이미지 (키·네트워크 불필요) +# ---------------------------------------------------------------------------- +#: PIL 부재 시 폴백용 1x1 회색 PNG(유효 최소 이미지). +_MINIMAL_PNG = base64.b64decode( + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==" +) + + +def _placeholder_png(lines: list[str], size: tuple[int, int] = (1024, 640)) -> tuple[bytes, str]: + """플레이스홀더 PNG 생성. PIL 있으면 안내 문구+워터마크 렌더, 없으면 최소 PNG. + + 생성형이 아니라 파이프라인 구조 검증용 자리표시자다(G1 미승인/목 모드). + """ + try: + import io + + from PIL import Image, ImageDraw # type: ignore + except ImportError: + return _MINIMAL_PNG, "image/png" + + W, H = size + im = Image.new("RGB", (W, H), (34, 38, 46)) + draw = ImageDraw.Draw(im) + # 안내 문구(중앙) + y = H // 2 - 12 * len(lines) + for ln in lines: + draw.text((40, y), ln, fill=(210, 214, 222)) + y += 24 + # 워터마크(하단) — §6-6 정책과 동일 문구 + draw.rectangle([0, H - 34, W, H], fill=(20, 22, 28)) + draw.text((12, H - 26), "AI 생성 예상 이미지 — 실제 시공 결과와 다를 수 있음 (MOCK)", fill=(255, 214, 90)) + buf = io.BytesIO() + im.save(buf, format="PNG") + return buf.getvalue(), "image/png" + + +# ============================================================================ +# RenderJob / RenderResult 계약 객체 (계약 §2/§3) +# ============================================================================ +@dataclass +class RenderJob: + job_id: str + shot_preset: str = "S1" + event_id: Optional[str] = None + booth_id: Optional[str] = None + scene: dict = field(default_factory=dict) + seed: Optional[int] = None + reference_image: Optional[str] = None + layers: Optional[list[str]] = None + wiring: Optional[dict] = None + hall_dims_m: Optional[list[float]] = None + options: dict = field(default_factory=dict) + + @classmethod + def from_dict(cls, d: dict) -> "RenderJob": + if not d.get("job_id"): + raise NanoBananaError("RenderJob.job_id 누락") + return cls( + job_id=str(d["job_id"]), + shot_preset=d.get("shot_preset", "S1"), + event_id=d.get("event_id"), + booth_id=d.get("booth_id"), + scene=d.get("scene") or {}, + seed=d.get("seed"), + reference_image=d.get("reference_image"), + layers=d.get("layers"), + wiring=d.get("wiring"), + hall_dims_m=d.get("hall_dims_m"), + options=d.get("options") or {}, + ) + + +@dataclass +class RenderResult: + job_id: str + status: str # DONE | FAILED + image_ref: Optional[dict] = None + meta: dict = field(default_factory=dict) + error: Optional[dict] = None + + +# ============================================================================ +# 오브젝트 스토리지 (기본=로컬 FS degraded 어댑터, 계약 §4) +# ============================================================================ +class ObjectStore: + """이미지 + 사이드카 메타데이터 적재. 기본 구현은 로컬 파일시스템. + + 운영 시 S3/GCS 어댑터로 교체하되 save_image() 반환 계약(§4)은 유지한다. + """ + + def __init__(self, root: str = OUTPUT_DIR, backend: str = "local"): + self.root = Path(root) + self.backend = backend + + @staticmethod + def key_for(job: RenderJob) -> str: + ev = job.event_id or "no-event" + booth = job.booth_id or "no-booth" + return f"{ev}/{booth}/{job.job_id}_{job.shot_preset}.png" + + def save_image(self, image: GeneratedImage, key: str) -> dict: + """GeneratedImage.save() 로 이미지+사이드카(.meta.json) 기록 후 참조 반환.""" + path = self.root / key + image.save(path) # 사이드카 .meta.json 동시 기록(결정적, B-03) + return { + "backend": self.backend, + "key": key, + "uri": path.resolve().as_uri(), + "sidecar_key": key + ".meta.json", + "content_type": image.mime_type, + } + + +# ============================================================================ +# 완료 이벤트 발행 (계약 §3) — Redis pub/sub 스텁(지연 연결) +# ============================================================================ +class EventPublisher: + """완료/실패 이벤트를 Redis 채널에 발행. 백엔드가 구독→WebSocket 릴레이. + + Redis 미연결 시에도 예외로 파이프라인을 멈추지 않는다(로그만). 비밀 미노출. + """ + + def __init__(self, redis_url: str = REDIS_URL, channel: str = EVENT_CHANNEL): + self.redis_url = redis_url + self.channel = channel + self._r = None # 지연 연결 + + def _redis(self): + if self._r is None: + import redis # type: ignore # 지연 임포트 — 미설치여도 import 성립 + + self._r = redis.from_url(self.redis_url) + return self._r + + def publish(self, event: dict) -> None: + payload = json.dumps(event, ensure_ascii=False) + try: + self._redis().publish(self.channel, payload) + except Exception as e: # noqa: BLE001 — 발행 실패가 렌더를 무효화하지 않음 + # 메시지 요약만(키/스택트레이스 미기록) + print( + f"[event] publish skipped (redis unavailable): {type(e).__name__}", file=sys.stderr + ) + # 항상 콘솔 관측(백엔드 부재 스모크에서도 이벤트 흐름 확인) + print(f"[event] {event.get('type')} job={event.get('job_id')} status={event.get('status')}") + + +# ============================================================================ +# 워커 +# ============================================================================ +class RenderWorker: + """RenderJob 소비 루프 + 단건 처리(process_job). 지연 연결로 구조만으로 성립.""" + + def __init__( + self, + redis_url: str = REDIS_URL, + queue_key: str = QUEUE_KEY, + store: Optional[ObjectStore] = None, + publisher: Optional[EventPublisher] = None, + ): + self.redis_url = redis_url + self.queue_key = queue_key + self.store = store or ObjectStore() + self.publisher = publisher or EventPublisher(redis_url) + self._r = None # 지연 연결 + self._stop = False + self._client = None # NanoBananaClient 지연 생성(실 호출 시에만) + + # ------------------------------------------------------------------ + # 렌더 디스패치 + # ------------------------------------------------------------------ + def _render(self, job: RenderJob) -> GeneratedImage: + """샷 프리셋에 따라 (a) S6 래스터 합성, (b) 생성형/목 렌더로 분기.""" + preset = SHOT_PRESETS.get(job.shot_preset) + if preset is None: + raise NanoBananaError(f"알 수 없는 샷 프리셋: {job.shot_preset} (S1~S7)") + + # (a) S6 배선 오버레이 — 생성형 아님, 항상 로컬 PIL 결정적 합성(M5-3) + if not preset.get("generative", True): + return self._render_wiring(job) + + # (b) 생성형 샷(S1~S5,S7) + if is_live(): + return self._render_live(job) # G1 승인 경로 + return self._render_mock(job) # 기본: 목/degraded + + def _render_wiring(self, job: RenderJob) -> GeneratedImage: + """S6: scene.wiring/최상위 wiring → 좌표 정합 래스터 오버레이(client 재사용).""" + scene_inner = job.scene.get("scene", job.scene) + wiring = job.wiring or scene_inner.get("wiring") or {} + if not wiring: + raise NanoBananaError("S6 배선 오버레이에 wiring 데이터가 필요합니다.") + + # 좌표계 기준(m): job.hall_dims_m → booth.size_m → hall.dims_m → 기본 + booth = scene_inner.get("booth", {}) or {} + hall = scene_inner.get("hall", {}) or {} + dims = job.hall_dims_m or booth.get("size_m") or hall.get("dims_m") or [10, 10] + hall_dims_m = (float(dims[0]), float(dims[1])) + + return render_wiring_overlay_raster( + wiring, + hall_dims_m=hall_dims_m, + kinds=job.options.get("kinds"), + base_image=job.reference_image, + px_per_m=int(job.options.get("px_per_m", 40)), + ) + + def _render_live(self, job: RenderJob) -> GeneratedImage: + """G1 승인 경로 — 실 Gemini 호출(NanoBananaClient 지연 생성).""" + if self._client is None: + try: # 패키지/스크립트 양쪽 임포트 + from .client import NanoBananaClient # type: ignore + except ImportError: + from client import NanoBananaClient # type: ignore + self._client = NanoBananaClient(model=MODEL_NAME) + img = self._client.render_shot( + job.scene, + shot_preset=job.shot_preset, + reference_image=job.reference_image, + seed=job.seed, + layers=job.layers, + ) + img.metadata.setdefault("render_path", "generative") + img.metadata["live"] = True + img.metadata["degraded"] = False + return img + + def _render_mock(self, job: RenderJob) -> GeneratedImage: + """기본 경로 — 목/degraded. 프롬프트·메타데이터는 실제와 동일 경로로 생성.""" + # 프롬프트는 실제 빌더로 조립(구조 검증 목적) — 실패해도 잡은 진행 + try: + prompt = build_booth_prompt(job.scene, shot_preset=job.shot_preset, layers=job.layers) + except Exception: # noqa: BLE001 + prompt = f"[mock] {job.shot_preset} prompt build skipped" + + meta = build_metadata(job.scene, "mock", job.shot_preset, job.seed) + meta["render_path"] = "generative" + meta["live"] = False + meta["degraded"] = True + + preset = SHOT_PRESETS.get(job.shot_preset, {}) + data, mime = _placeholder_png( + [ + "KINTEX 나노바나나 — MOCK / DEGRADED", + f"shot: {job.shot_preset} {preset.get('label', '')}", + f"booth: {job.booth_id or '-'} event: {job.event_id or '-'}", + "G1 미승인/목 모드 — 실 Gemini 호출 없이 생성한 자리표시자", + ] + ) + return GeneratedImage(data=data, mime_type=mime, prompt_used=prompt, metadata=meta) + + # ------------------------------------------------------------------ + # 단건 처리 (테스트·스모크에서 직접 호출 가능 — Redis 불필요) + # ------------------------------------------------------------------ + def process_job(self, job: RenderJob) -> RenderResult: + """RenderJob 1건 처리: 렌더 → 스토리지 적재 → 완료 이벤트 발행.""" + try: + image = self._render(job) + key = ObjectStore.key_for(job) + image_ref = self.store.save_image(image, key) + result = RenderResult( + job_id=job.job_id, status="DONE", image_ref=image_ref, meta=image.metadata + ) + self.publisher.publish(self._event("renderjob.completed", job, result)) + return result + except Exception as e: # noqa: BLE001 — 분류 후 친화 에러(비밀 미노출) + error = self._classify_error(e) + result = RenderResult(job_id=job.job_id, status="FAILED", error=error) + self.publisher.publish(self._event("renderjob.failed", job, result)) + return result + + @staticmethod + def _classify_error(e: Exception) -> dict: + """예외 → {code,message}. 스택트레이스·키 미포함(PLANNING §6-5).""" + if isinstance(e, NanoBananaAuthError): + code = "AUTH" + elif isinstance(e, NanoBananaQuotaError): + code = "QUOTA" + elif isinstance(e, NanoBananaSafetyError): + code = "SAFETY" + elif isinstance(e, (ValueError, KeyError, TypeError)): + code = "BAD_REQUEST" + else: + code = "RENDER_ERROR" + return {"code": code, "message": str(e)[:300]} + + @staticmethod + def _event(etype: str, job: RenderJob, result: RenderResult) -> dict: + return { + "type": etype, + "job_id": job.job_id, + "event_id": job.event_id, + "booth_id": job.booth_id, + "shot_preset": job.shot_preset, + "status": result.status, + "image_ref": result.image_ref, + "meta": result.meta, + "error": result.error, + "emitted_at": datetime.now(timezone.utc).isoformat(), + } + + # ------------------------------------------------------------------ + # 소비 루프 (지연 연결) + # ------------------------------------------------------------------ + def _redis(self): + if self._r is None: + import redis # type: ignore # 지연 임포트 + + self._r = redis.from_url(self.redis_url) + return self._r + + def stop(self) -> None: + self._stop = True + + def run(self) -> None: + """BLPOP 폴링 소비 루프. Redis 연결은 최초 진입 시점에 시도(지연).""" + mode = "LIVE(Gemini)" if is_live() else "MOCK/degraded" + print(f"[worker] start queue={self.queue_key} mode={mode} model={MODEL_NAME}") + r = self._redis() + while not self._stop: + item = r.blpop(self.queue_key, timeout=BLPOP_TIMEOUT_S) + if item is None: + continue # 폴링 타임아웃 — stop 플래그 재확인 + _, raw = item + try: + payload = json.loads(raw) + job = RenderJob.from_dict(payload) + except Exception as e: # noqa: BLE001 — 파싱 실패 잡은 스킵(무한루프 방지) + print(f"[worker] drop malformed job: {type(e).__name__}", file=sys.stderr) + continue + self.process_job(job) + + +# ============================================================================ +# 스모크 / CLI +# ============================================================================ +def _smoke() -> int: + """무네트워크 스모크: 목 모드로 생성형 잡 1건 + S6 잡 1건 처리, 사이드카 확인.""" + import tempfile + + tmp = Path(tempfile.mkdtemp(prefix="nanobanana_smoke_")) + worker = RenderWorker(store=ObjectStore(root=str(tmp))) + + scene = { + "hall": {"id": "제1전시장 7홀", "dims_m": [126, 90], "ceiling_m": 12}, + "booth": {"id": "A-102", "size_m": [6, 3], "type": "independent"}, + "design": {"signage": {"text": "주식회사 가디아"}, "brand_color": "#0052A5"}, + "lighting": {"mode": "night", "color_temp_k": 4000}, + "wiring": { + "power": [{"from_trench": [1, 1], "to": [5, 2], "kw": 3}], + "network": [{"path": [[0, 0], [3, 1], [5, 2]]}], + }, + "render_hints": {"style": "tech"}, + } + + ok = True + + # (1) 생성형 샷 S2 (목 모드) + job1 = RenderJob( + job_id="smoke-s2", event_id="evt_smoke", booth_id="A-102", shot_preset="S2", scene=scene + ) + r1 = worker.process_job(job1) + sidecar1 = tmp / (r1.image_ref["key"] + ".meta.json") if r1.image_ref else None + print(f"[smoke] S2 status={r1.status} live={r1.meta.get('live')} degraded={r1.meta.get('degraded')}") + if r1.status != "DONE" or not (sidecar1 and sidecar1.exists()): + ok = False + print("[smoke] FAIL: S2 결과/사이드카 누락") + + # (2) S6 배선 오버레이(래스터 합성 — G1 무관) + job2 = RenderJob( + job_id="smoke-s6", event_id="evt_smoke", booth_id="A-102", shot_preset="S6", scene=scene + ) + r2 = worker.process_job(job2) + sidecar2 = tmp / (r2.image_ref["key"] + ".meta.json") if r2.image_ref else None + rp = r2.meta.get("render_path") + print(f"[smoke] S6 status={r2.status} render_path={rp}") + # PIL 부재 환경에서는 S6 렌더가 실패할 수 있음(설계상 Pillow 필요) — 그 경우 관대 처리 + if r2.status == "DONE": + if rp != "backend_raster_composite" or not (sidecar2 and sidecar2.exists()): + ok = False + print("[smoke] FAIL: S6 결과/사이드카/경로 불일치") + else: + print(f"[smoke] S6 skipped (환경 제약): {r2.error}") + + print(f"[smoke] output dir: {tmp}") + print("[smoke] RESULT:", "PASS" if ok else "FAIL") + return 0 if ok else 1 + + +def main(argv: Optional[list[str]] = None) -> int: + parser = argparse.ArgumentParser(description="나노바나나 RenderJob 워커") + parser.add_argument("--smoke", action="store_true", help="무네트워크 스모크(목 잡 처리)") + args = parser.parse_args(argv) + if args.smoke: + return _smoke() + RenderWorker().run() + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())