diff --git a/CLAUDE.md b/CLAUDE.md index 1d5b7f8..02fca7b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -85,6 +85,46 @@ GUARDiA 표준 프레임워크(Spring Boot 3.5 + React 19 + MyBatis + PostgreSQL --- +## 하네스: 벤치마킹·크롤링 (kintex-benchmark-orchestrator) + +**목표:** 레퍼런스 사이트(COEX 등) 크롤·분석·벤치마크 백로그 산출(kintex-benchmark-analyst) + 외부 행사 데이터 크롤 적재(kintex-crawler-dev). 적용은 planner/renewal/wise-ui 하네스 연계. + +**트리거:** 벤치마킹, 사이트 분석해서 재구성, 경쟁사 비교, 크롤링(행사정보 수집·갱신), 데이터 수집, 다시 실행, 특정 사이트만 요청 시 `kintex-benchmark-orchestrator` 스킬을 사용하라. + +**변경 이력:** +| 날짜 | 변경 내용 | 대상 | 사유 | +|------|----------|------|------| +| 2026-07-12 | 초기 구성(analyst·crawler 에이전트 + 오케스트레이터) | 전체 | "크롤링/벤치마킹 하네스 생성" 요청 — COEX 3-트랙 재구성·행사 크롤 정례화 | + +--- + +## 하네스: 전면 리뉴얼 (kintex-renewal-orchestrator) + +**목표:** 내비게이션(브레드크럼·상세→메인 복귀·404)·전 화면 WISE 정렬·마스터/테스트 데이터 전면 생성(공통코드·프로그램·부서·데모 폐루프)을 총괄 리뉴얼. UI 정렬은 wise-ui 하네스 에이전트 재사용 + kintex-testdata-dev 신규. + +**트리거:** 리뉴얼, 전면 개편, 상세에서 메인 못 감/내비 문제, 테스트 데이터 생성, 데이터 채워/화면 비어 있음, 공통코드·프로그램 채움, 다시 실행, 특정 영역만 요청 시 `kintex-renewal-orchestrator` 스킬을 사용하라. + +**변경 이력:** +| 날짜 | 변경 내용 | 대상 | 사유 | +|------|----------|------|------| +| 2026-07-12 | 초기 구성(kintex-testdata-dev 신규 + wise-ui/menu-ia/qa 재사용 + 오케스트레이터) | 전체 | 소유자 지시 — 상세→메인 복귀 부재·전면 리뉴얼·테스트 데이터/공통코드/프로그램 채움 | + +--- + +## 하네스: WISE UI 정렬 (kintex-wise-ui-orchestrator) + +**목표:** 킨텍스 UI(셸·아코디언 메뉴 IA·캘린더·차트·전 화면)를 WISE(UIWS `C:\GUARDiA\workspace\uiws\frontend`) 컨벤션에 **문자 그대로** 정렬(자체 재해석 금지 — 소유자 원칙 2026-07-12). 감사(kintex-wise-ui-auditor)→정렬 이식(kintex-wise-ui-dev)→QA(kintex-qa 재사용). + +**트리거:** WISE처럼/WISE대로, UI 정렬, 메뉴 재구성, 아코디언/햄버거 메뉴, 셸·사이드바·푸터 수정, 캘린더 WISE, 대시보드 구성, 차트, 로고 교체, 반응형 깨짐, 다시 실행, 특정 화면만 요청 시 `kintex-wise-ui-orchestrator` 스킬을 사용하라. + +**변경 이력:** +| 날짜 | 변경 내용 | 대상 | 사유 | +|------|----------|------|------| +| 2026-07-12 | 초기 구성(에이전트 2 신규 + kintex-qa 재사용 + 오케스트레이터) | 전체 | 소유자 강한 피드백("전부 WISE대로 안 되어 있다" — 시스템관리 메뉴 실종·햄버거/아코디언 부재·캘린더·대시보드·로고) | +| 2026-07-12 | 메뉴 IA 전담 추가 — kintex-menu-ia-dev + kintex-menu-recompose 스킬(표준 카테고리 v1·라우트↔메뉴 정합 절차), 오케스트레이터 Phase 2.5 편입 | agents·skills | "카테고리별 메뉴 재구성 하네스" 요청 — 별도 오케스트레이터 대신 중복 회피 확장 | + +--- + ## 하네스: 모바일 앱 (kintex-mobile-orchestrator) **목표:** `mobile/`(Expo SDK 51 + expo-router + React Native 0.74 + TS) 모바일 앱 트랙 — 화면(SCR-M*)·실데이터 API 배선·오프라인/푸시/i18n·에셋·EAS 빌드·APK·QR 배포. **레퍼런스 = WISE 모바일**(`workspace/guardia-messenger/app/uiws/` — uiwsApi 봉투 언랩·2FA 로그인·화면 컨벤션), **디자인 = Stitch 경유**(designer → design.md SCR-M* → Stitch 생성 → RN 이식). diff --git a/docs/PLANNING.md b/docs/PLANNING.md index 90d46bb..fe48e22 100644 --- a/docs/PLANNING.md +++ b/docs/PLANNING.md @@ -1,6 +1,6 @@ # 킨텍스 AI 전시·행사시스템 (KINTEX AI Exhibition & Event System) 기획서 -> 작성: 기획 에이전트(planner) · 작성일: 2026-07-11 · 최종 갱신: 2026-07-12 · 버전: **v3.3** +> 작성: 기획 에이전트(planner) · 작성일: 2026-07-11 · 최종 갱신: 2026-07-12 · 버전: **v3.4** > **제품 공식 명칭(소유자 확정 2026-07-12): "KINTEX AI 전시·행사시스템"** — 기존 "자동전시시스템(Exhibition Automation Platform)"을 대체한다. 전시(exhibition)뿐 아니라 행사·이벤트(event) 전반을 포괄한다는 의미를 명칭에 반영. 본 명칭이 정본이며, 이하 본문·3-트랙 기획·타 문서 후속 반영 시 이 명칭을 사용한다(기존 "자동전시시스템" 표기는 문맥 보존을 위해 삭제하지 않고 잔존하나, 신규 표기는 정본 명칭을 따른다). > 근거 문서: `docs/analysis/kintex-website.md` (킨텍스 웹사이트 25개 페이지 + 전시주최자매뉴얼 PDF + 참가업체 매뉴얼 PDF 전수 분석, 2026-07-11) > 근거 문서: `docs/analysis/coex-website.md` (코엑스 VISITOR/BUSINESS/CYBER 3-사이트 IA 분석 + 킨텍스 대비표, 2026-07-12) — §2-2 대외 접점 3-트랙 재구성 확정 근거 @@ -10,6 +10,7 @@ > **v2.0 정체성 확장**: 본 시스템은 "부스 시공 도구"를 넘어 **킨텍스 자동전시시스템(Exhibition Automation Platform)** 이다 — 전시 기획·판매·시공·운영·관람객·사후분석 전 주기를 아우른다. 기존 M2~M5 부스 시공 코어(P0)·나노바나나 파이프라인(§6)·확정 스택(§8)은 **보존**하고, 그 위에 관람객/비즈매칭/마케팅·공개사이트/wayfinding/현장운영/공사 옥션/BI/CMS/관리자 모듈(M10~M18)을 확장한다. > **v3.0 정체성 확장 — 다중 전시관 SaaS**: 본 시스템은 **킨텍스 전용에서 다중 전시관(Venue) 멀티테넌트 SaaS**로 확장한다. 킨텍스(KINTEX)는 **기준 테넌트(테넌트 #1)** 로 유지되고, 코엑스(COEX) 등 여타 전시관을 테넌트로 온보딩한다. 기능(M1~M18·§5B·§6 나노바나나·§8 확정 스택)은 **전부 보존**하고, 그 위에 **테넌트 격리 레이어(tenant_id 전파·컨텍스트 해소·역할 계층)** 만 얹는다 — 기능은 그대로, 데이터·권한·컨텍스트가 전시관 단위로 분리된다. 상세는 **§1A**, 격리 전략은 **§8-2**. > **v3.2 대외 접점 3-트랙 재구성**: 코엑스(COEX) IA 분석(3-사이트 분리: VISITOR/BUSINESS/CYBER)을 근거로, 대외 접점을 **visitor(관람객) / business(비즈니스=주최자·참가업체) / agency(에이전시=공사·장치·협력사)** 3개 트랙으로 분리·재편한다. 기존 6역할·7페르소나(§2)·포털 매트릭스(§2-1)·모듈(M1~M18)·화면(SCR-*)은 **전부 보존**하고, 그 위에 **트랙 셸(track shell) — 랜딩 3-분기 + 트랙별 서브홈 + 트랙-스코프 로그인 후 홈** IA 레이어만 얹는다. 상세는 **§2-2**(내부 역할 ops·admin은 대외 트랙 외부에 그대로 유지). +> **v3.4 AI 사용성 & 토큰 최소화**: 소유자 지시(2026-07-12) — "AI 기능을 쉽게 사용하도록 기획 + 토큰을 최소한으로 사용하는 방식 채용". 산재해 있던 AI 규약을 **§8A**로 명문화한다 — ① AI 사용성 표준(전 화면 인라인 진입점·예시 질문 칩·원탭 액션·다음 명령 제시·구조화 카드+근거 인용·대화 히스토리·다국어/모바일/음성) ② 토큰 최소화 6원칙(결정론 우선 라우팅으로 LLM 우회·소형모델 우선 티어링·RAG 발췌로 프롬프트 축소·캐싱·출력 상한/구조화·집계는 SQL) ③ 측정지표·목표 ④ design/개발 계약 포인트. 기존 스코프(§5B 공통 AI·§6 나노바나나·§8 스택·§8-2 격리)·화면·라우트 보존, 원칙·계약·지표만 순증. M10 관람객 AI 도우미·M16 BI·§5B 공통 AI 보조에 §8A 링크. 이미 진행 중(visitor-assistant DB 근거 응답·AiTextRouter Claude→Ollama 폴백)과 정합(재발명 아님·표준화). src·design.md 미수정(designer/backend 후속). > **v3.3 로그인 사용자 구분 → 메인페이지·메뉴 그룹 결정 규칙**: 소유자 확정(2026-07-12) — **"로그인한 사용자의 사용자 구분으로 메인페이지가 결정되고, 좌측 메뉴 그룹도 결정된다."** §2-2의 3-트랙(visitor/business/agency) + 내부 역할(ops/admin) 위에, **미인증 기본 진입 = visitor 공개 관람 랜딩**, **인증 후 역할별 전용 메인**(business 메인·agency 메인·visitor 메인은 신규 화면 — designer Stitch 의뢰, admin/내부 = 관리 메인 또는 비즈니스 메인+시스템관리)과 **트랙별 좌측 메뉴 그룹 노출/숨김**을 결정하는 규칙을 **§2-3**에 확정한다. 기존 §2·§2-2·화면(SCR-*)·라우트는 보존(안 B 정합), 결정 규칙·트랙 메인 화면 목록만 순증. src·design.md 미수정(frontend·designer 후속). > **문서 소유권**: 본 PLANNING.md는 planner만 수정한다. design.md·타 문서는 이 문서 변경 시 "designer/planner 후속 반영 필요"로만 표기하고 직접 수정하지 않는다. @@ -654,6 +655,7 @@ sequenceDiagram - **현재**: 킨텍스 공식은 행사일정 검색 수준. 관람객 등록·배지·리드캡처는 행사별 주최자 사무국에 파편화(공통 플랫폼 부재). - **AI 자동화 후**: **간편가입(이메일 또는 소셜 로그인) 또는 게스트 예매(가입 없이 티켓 구매, SCR-P7 기반영)** → 온라인 사전등록(관람객/바이어 유형별 폼) → **모바일 배지/QR 발급** → 현장 QR 체크인(즉석 배지 인쇄·오프라인 대비) → 실시간 입장 집계. 참가업체 **리드캡처**: 운영 앱(B2B)으로 관람객 배지 QR 스캔 → 연락처·관심도 평점·메모 저장 → 팔로업 EDM(M12) 연계. 사전등록·체크인 데이터는 M16 BI로 흐른다. - **계정 트랙·승격 연속성(소유자 확정 2026-07-11, §2-1-1)**: 관람객은 **간편가입 트랙(2FA 미강제)** 이며 게스트 예매를 허용한다. 관람객 → **바이어/참가업체 담당자 승격** 시 **단일 계정 위 등급 상향**으로 처리해 **리드·비즈매칭·재방문 이력의 데이터 연속성**을 유지한다(계정 재생성 없음, 승격 시 업무 트랙 요건 2FA 추가). 게스트 예매 후 간편가입 시 예매 이력을 티켓 지갑에 병합. +- **관람객 AI 도우미(§8A 정합)**: visitor 메인의 AI 도우미는 **§8A-1 사용성 표준**(인라인 진입점·예시 질문 칩·구조화 카드+근거 인용)과 **§8A-2 토큰 최소화**(일정·교통·주차·부스 위치 등 사실조회는 **DB 직접 응답=LLM 미호출 P1**, 종합 필요 시에만 소형모델 우선 P2·RAG 발췌 P3)를 따른다. 이미 DB 근거로 답하는 구조를 §8A 계약(`/ai/ask`)으로 표준화. - **채널**: 관람객 1차 접점은 공개 웹(SCR-P7/P8), 리텐션은 **관람객 앱(B2C, 스토어 공개)** — 티켓 지갑·배지/QR·wayfinding·비즈매칭(§2-1-1 B). - **기대 효과**: 현장 등록 대기 제거, 게스트 예매로 가입 마찰 최소화, 참가업체 ROI 측정(리드 수·품질), 관람객 흐름 데이터 확보. - **한계**: 배지 QR·리드캡처는 개인정보(§10 R10) — 동의·보존정책 필수. 오프라인 체크인 폴백 필요(현장 네트워크 불안정). 게스트 예매는 최소 개인정보 수집·사후 계정 병합 정책 명시 필요. @@ -724,6 +726,7 @@ sequenceDiagram | ⑥ | **수요예측·수율/가격(yield/pricing) 최적화** | 성수기 계수·홀별 수요 예측, 요율·성수기(+10%)/비수기(-10%)·1전시장(+10%) 계수 시뮬레이션으로 수율 최적가 탐색 | M1 요율·과거 배정·M18 룰셋 | | ⑦ | **경영진 KPI 대시보드** | 위 ①~⑥ 요약 + 목표 대비 실적(점유율 목표 60~75%·매출·마진·리텐션), 전시장·분기 드릴다운 | KpiSnapshot 집계 | +- **BI AI(§8A 정합·토큰 최소화 핵심)**: 모든 **집계·랭킹·추이 수치는 데이터마트 SQL이 산출(§8A-2 P6)**, LLM은 그 수치에 대한 **자연어 설명·해석만** 담당(수치 계산에 LLM 금지). 자연어 질의(예 "지난 분기 홀7 가동률은?")도 **SQL 집계 → 카드 렌더**를 우선하고, LLM은 설명·후속 제안에만 사용. 동일 기간/스코프 요약은 캐시 재사용(§8A-2 P4). - **데이터마트 설계(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·수익성 대시보드). @@ -787,6 +790,8 @@ UIWS 표준 공통 모듈을 이식해 전 포털이 공유. 킨텍스 도메인 > **경계(중복 제거)**: 경영·수익 지표는 **M16 BI**가 권위, 일상 업무보고·통계는 공통 `report/stats`가 담당. 콘텐츠/공지 발행은 **M17 CMS**가 권위, 사내 알림성 공지는 공통 `notice`. 알림 발송 채널은 공통 `notification` 단일화(M10·M12·M15가 이벤트 발행). +> **공통 AI 보조(§8A 정합)**: 전 업무 화면의 AI 보조(search·meeting 액션아이템·report 요약·초안 생성 등)는 **§8A 단일 AI 계약(`/ai/ask`)·사용성 표준·토큰 최소화 원칙**을 공유한다 — 사실조회는 결정론/DB 직답(LLM 미호출), 종합은 소형모델 우선·Claude 승급(`AiTextRouter`)·RAG 발췌·캐시·구조화 출력. 신규 LLM 직접 호출 경로 신설 금지(중앙 `/ai/ask` 경유). + ### 5B-3. 인증 (UIWS 표준: JWT + 2FA/OTP) kintex 인증은 §2 행사 단위 RBAC를 **UIWS 표준 인증 위에** 구현한다(재설계 금지). @@ -1138,6 +1143,71 @@ graph TB --- +## 8A. AI 사용성 & 토큰 최소화 아키텍처 (v3.4, 소유자 지시 2026-07-12) + +> **소유자 지시**: "기획자는 AI 기능을 **쉽게 사용**할 수 있도록 기획하고, **토큰을 최소한으로 사용**하는 방식을 채용하라." 본 절은 전 모듈에 걸친 **AI 사용성 표준(§8A-1)** 과 **토큰 최소화 설계 원칙(§8A-2)** 을 명문화한다. **재발명이 아니라 이미 진행 중인 패턴의 표준화**다 — visitor 관람객 AI 도우미가 이미 **DB 근거로 답하고**, 공통 AI 계층이 **`AiTextRouter`로 Claude→Ollama(소형) 폴백**하며, 크롤 적재 DB(행사 데이터 등)를 근거로 삼는 구조가 존재한다. 본 절은 이를 **원칙·계약·측정지표로 고정**해 전 모듈(M10 관람객 AI 도우미·M16 BI·§5B 공통 AI 보조)이 동일 규약을 따르게 한다. +> **보존·경계**: §5B 공통 레이어·§8 확정 스택·§8-2 테넌트 격리·§6 나노바나나(이미지 생성은 별개 파이프라인·본 절의 "토큰"은 **텍스트 LLM** 대상)를 삭제·재작성하지 않는다. AI 게이트(외부 API 금지, `api.anthropic.com` 예외 — §10)·온프레미스 소형모델 RAM 제약([[project_ollama_ram_constraint]])과 정합한다. + +### 8A-1. AI 사용성 표준 — "어디서나 한 번에 물어본다" + +전 화면에 **동일한 AI 진입 규약**을 심어, 사용자가 메뉴를 뒤지지 않고 자연어로 즉시 도움을 받게 한다. 관람객(visitor 메인 AI 도우미)과 업무 사용자(업무 화면 AI 보조)가 **같은 컴포넌트·같은 로직**을 공유한다. + +| # | 사용성 요소 | 내용 | 적용 범위 | +|---|---|---|---| +| U1 | **인라인 AI 진입점** | 전 화면 공통 위치(헤더/우하단)에 "AI에게 물어보기" 버튼 + 자연어 질문창(공통 컴포넌트 `AiAssistant`). 화면 컨텍스트(현재 행사·부스·화면 ID)를 자동 첨부 | 웹·모바일 전 화면 | +| U2 | **예시 질문 칩(prompt chips)** | 자주 묻는 질문을 원클릭 칩으로 노출(화면·역할별 큐레이션). 예: 관람객 "오늘 열리는 전시는?"·"주차 어디?"·"부스 어떻게 찾아?" / 참가업체 "내 유틸리티 신청 마감일?"·"이 도면 규정 위반 있나?" | 화면·역할별 | +| U3 | **원탭 AI 액션** | 목록/상세에서 버튼 한 번으로 요약·추천·초안 생성. 예: 서류 요약, 옥션 응찰 비교 요약, EDM 카피 초안(M12), 회의록 액션아이템 추출(§5B meeting) | 목록·상세 화면 | +| U4 | **다음 명령 제시(proactive)** | 결과 하단에 후속 액션·후속 질문 칩 제시("이 배치안으로 시각화 생성?"·"이 업체에 옥션 초대?"). 프로액티브 제안은 **규칙 기반 우선**(LLM 미사용, §8A-2 P1) | 결과 카드 | +| U5 | **구조화 카드 + 근거 인용** | 답변은 자유 서술 대신 **구조화 카드**(핵심값·표·링크) + **근거 인용**(출처 레코드/문서 링크·"근거: 행사마스터 #123"). 근거 없으면 **"모름" 폴백**(환각 차단, GUARDiA AI-trust 정합) | 전 답변 | +| U6 | **대화 히스토리·후속 질문** | 세션 내 멀티턴 히스토리 + 후속 질문 칩. 히스토리는 **요약 압축**해 컨텍스트 재주입 최소화(§8A-2 P4) | 도우미 패널 | +| U7 | **접근성·다국어·모바일·음성** | 동일 로직을 웹·모바일 공유(§2-3 채널 규칙 정합), 다국어(한/영/중/일) UI, 스크린리더 대응, **음성 입력 옵션**(온디바이스 STT 고려·서버 부하·토큰 무관) | 전 채널 | + +- **관람객 vs 업무 사용자 공통화**: 두 페르소나가 **단일 `AiAssistant` 컴포넌트 + 단일 `/ai/ask` 계약**을 공유하고, 노출 위치·예시 칩·허용 액션만 역할/트랙(§2-3)으로 스코프한다 → 구현 중복 회피·UX 일관성. +- **신뢰성 우선**: 사용성(쉽게)은 신뢰성(근거·모름 폴백)과 함께 간다 — "빠르고 그럴듯한 환각"보다 "근거 있는 절제된 답"을 표준으로 한다(U5). + +### 8A-2. 토큰 최소화 설계 원칙 (핵심 설계 원칙 — 비용 효율) + +> **대원칙: "LLM은 최후에 최소로."** 답할 수 있는 것은 DB/규칙/집계로 먼저 답하고, 자연어 종합이 실제 필요한 부분에만, 가장 작은 모델로, 가장 짧은 컨텍스트로 호출한다. 아래 6원칙은 **`/ai/ask` 오케스트레이션의 결정 순서**이기도 하다. + +| 원칙 | 내용 | 계약/구현 지점 | +|---|---|---| +| **P1. 결정론 우선 라우팅** | 의도 분류(키워드·규칙·화면 컨텍스트)로 **LLM 없이 답할 수 있으면 LLM을 호출하지 않는다.** 연/월 일정·교통·주차·마감일·요금·부스 위치·통계 수치 등 **사실 조회형은 DB/뷰가 직접 응답**(카드 렌더). LLM은 **요약·추천·자연어 종합·다중 근거 통합**이 실제 필요할 때만 | `IntentRouter`(규칙 테이블) → `DirectAnswer`(DB) vs `LlmAnswer` 분기 | +| **P2. 소형모델 우선 티어링** | 기본 온프레미스 **소형(Ollama `qwen3:1.7b`/`llama3.2:1b`)** 로 처리 → 난도·실패 시에만 **Claude로 에스컬레이션**(`AiTextRouter` 폴백 체인 역방향 활용: 저비용 우선 → 고비용 승급). 난도 판정 = 컨텍스트 길이·요구 추론 깊이·소형모델 신뢰도 | `AiTextRouter`(기존)·`AiConfig` 티어 정책. RAM 제약([[project_ollama_ram_constraint]]) 정합 | +| **P3. RAG 그라운딩으로 프롬프트 축소** | 크롤 적재/도메인 DB에서 **필요 레코드만 발췌**(top_k 제한·컬럼 프로젝션)해 **짧은 컨텍스트**로 전달. **전체 문서·전체 테이블 주입 금지.** 근거 스니펫만 넣어 프롬프트 길이·환각 동시 억제 | RAG 검색 `top_k`(기본 3~5)·발췌 요약·근거 스니펫 | +| **P4. 캐싱** | ① **응답 캐시**: 동일 의도+동일 파라미터 질의 결과 캐시(TTL, 예 일정·요금 24h) ② **시스템 프롬프트 캐시**(프롬프트 프리픽스 재사용) ③ **동일 기간/스코프 요약 재사용**(M16 BI·기간 요약). 히스토리는 요약본만 재주입 | Redis 응답 캐시(키=의도+파라미터+tenant+locale), 프롬프트캐시 | +| **P5. 출력 상한·구조화** | `max_tokens` 상한 강제, **구조화(JSON/카드) 출력**으로 장문 방지. **스트리밍은 UX 체감용일 뿐 토큰량은 불변**임을 명시(비용 절감 아님) | `max_tokens`·`response_format`(structured), 스트리밍=표시 옵션 | +| **P6. 집계는 SQL로** | 통계·집계·랭킹·추이는 **LLM이 아니라 DB 집계(뷰/데이터마트)** 로 산출. LLM은 그 수치에 대한 **자연어 설명·해석만** 담당(수치 계산에 LLM 금지 — 환각·비용 이중 방지) | M16 BI 데이터마트(§M16-1)·SQL 집계 → LLM은 설명만 | + +**오케스트레이션 흐름(요약)**: `사용자 질문 → IntentRouter(P1) → [사실조회면] DB/뷰 카드 응답(LLM 0토큰) / [종합필요면] RAG 발췌(P3) → 캐시 조회(P4) → 소형모델(P2) → (난도↑) Claude 승급 → max_tokens·구조화 출력(P5) → 근거 인용 카드(U5)`. 통계는 항상 SQL 선처리(P6). + +### 8A-3. 측정지표 및 목표 (비용 가시성) + +> 토큰 최소화는 **측정되어야 관리된다.** 아래 지표를 `ai_usage_log`(질의별 토큰·모델·경로·캐시히트·지연)로 적재하고 M16 BI/관리자 대시보드에 노출한다. 목표치는 초기 가설이며 파일럿 후 보정(근거 없는 확정 금지). + +| 지표 | 정의 | 목표(초기 가설) | +|---|---|---| +| **LLM 우회율(deflection)** | 전 질의 중 P1 결정론/DB로 **LLM 없이** 처리된 비율 | **≥ 40%** (일정·요금·위치·통계형 다수) | +| **소형모델 처리율** | LLM 호출 중 소형(Ollama)로 완결(Claude 미승급) 비율 | **≥ 70%** | +| **캐시 적중률** | 캐시(P4)로 응답한 비율 | **≥ 30%**(반복 질의 화면) | +| **호출당 평균 입력 토큰** | LLM 호출 1건당 평균 프롬프트 토큰(RAG 발췌·히스토리 요약 후) | **≤ 1,500** | +| **호출당 평균 출력 토큰** | `max_tokens` 상한 하 평균 출력 | **≤ 400** | +| **근거 인용률** | LLM 답변 중 근거 링크·"모름" 폴백을 포함한 비율(U5) | **≥ 95%** | + +- **비용 귀속**: `ai_usage_log`는 `tenant_id`(§8-2)·모듈·역할을 태깅해 **테넌트/모듈별 비용 배분**을 가능케 한다(플랫폼 슈퍼관리자 크로스-테넌트 집계). + +### 8A-4. design/개발 에이전트 계약 포인트 + +> planner는 원칙·계약을 확정한다. **화면·컴포넌트 상세는 designer(Stitch 경유), 라우팅·캐시·모델 티어 구현은 backend/ai-dev 후속.** design.md·src 미수정. + +1. **단일 AI 계약** `POST /ai/ask` — 입력: `{ question, contextRef(화면·행사·부스), locale, history(요약) }`, 출력: `{ answerCard(구조화), citations[], followups[], route(direct|small|escalated), usage(tokens·cached) }`. 전 화면·전 채널(웹·모바일)이 이 단일 계약을 사용(U1·U7). +2. **라우팅 계약**: `IntentRouter`는 **규칙 테이블(공통코드)** 로 관리(코드 배포 없이 의도·직답 매핑 추가). 사실조회 의도는 **DB 리졸버 매핑**(일정·요금·위치·통계). 미매핑만 LLM 경로. +3. **캐시 계약**: 캐시 키 = `hash(intent + params + tenant_id + locale)`, TTL은 의도별 정책(공통코드). 무효화 = 원천 데이터 변경 이벤트(§5B notification 버스 재사용). +4. **모델 티어 계약**: `AiConfig`(§5B, 기존 AiTextRouter/AiConfig 패턴)에 **티어 정책**(기본 소형·승급 임계·모델명) 설정형 노출 — 하드코딩 금지, 관리자 조정 가능. 실패 시 폴백(소형↔Claude 상호). +5. **UI 컴포넌트 계약**: 공통 `AiAssistant`(진입점·질문창·칩·카드·히스토리) + `AiActionButton`(원탭 액션). 예시 칩·허용 액션은 **화면/역할 메타데이터**로 주입(designer가 화면별 큐레이션, Stitch 프롬프트에 반영). +6. **모듈 링크**: M10(§5A 관람객 AI 도우미)·M16(§M16-1 BI 자연어 설명)·§5B 공통 AI 보조가 본 절 원칙을 **동일 계약으로** 준수. 나노바나나(§6) 이미지 생성은 본 절 토큰 원칙 대상 아님(별도 RenderJob 쿼터·캐시). + +--- + ## 9. 로드맵 ### Phase 1 — 설계·시각화 코어 (MVP, ~4개월) @@ -1191,6 +1261,7 @@ graph TB | 버전 | 일자 | 작성자 | 내용 | |---|---|---|---| +| **v3.4** | 2026-07-12 | planner | **AI 사용성 & 토큰 최소화 아키텍처 신설(소유자 지시 2026-07-12).** 기존 스코프·화면·라우트 전부 보존, AI 원칙·계약·지표만 순증. ①헤더 v3.4 라인 + 버전 상향 ②**§8A 신설**(§8-2 뒤·§9 앞) — (8A-1) AI 사용성 표준 U1~U7(인라인 진입점 `AiAssistant`·예시 질문 칩·원탭 AI 액션·다음 명령 제시(규칙 우선)·구조화 카드+근거 인용·"모름" 폴백·대화 히스토리 요약·접근성/다국어/모바일/음성, 관람객·업무 사용자 단일 컴포넌트 공유) (8A-2) 토큰 최소화 6원칙 P1~P6(결정론 우선 라우팅으로 사실조회 LLM 미호출·소형모델 우선 티어링 AiTextRouter 저비용→승급·RAG 발췌 top_k 제한·캐싱 응답/프롬프트/기간요약·max_tokens 상한+구조화 출력(스트리밍=UX용 토큰 불변)·집계는 SQL LLM은 설명만) + 오케스트레이션 흐름 (8A-3) 측정지표·목표(LLM 우회율≥40%·소형모델 처리율≥70%·캐시 적중률≥30%·평균 입력≤1500/출력≤400·근거 인용률≥95%, `ai_usage_log` 적재·tenant/모듈 비용 귀속) (8A-4) design/개발 계약 6종(단일 `/ai/ask`·규칙테이블 IntentRouter·캐시 키·AiConfig 티어·공통 `AiAssistant`/`AiActionButton`·모듈 링크) ③**M10·M16-1·§5B-2에 §8A 정합 링크** 순증(관람객 AI 도우미 DB 직답·BI 집계 SQL/LLM 설명만·공통 AI 보조 중앙 `/ai/ask` 경유). 이미 진행 중(visitor-assistant DB 근거·AiTextRouter 폴백)과 정합·표준화(재발명 아님). 외부 API 게이트·RAM 제약 정합. **src·design.md 미수정** — designer(AiAssistant/칩 Stitch)·backend/ai-dev(라우팅·캐시·티어) 후속 | | 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)·우선순위·나노바나나 파이프라인 로직 불변** — 스택 표기 정합화만 수행 | diff --git a/docs/analysis/mobile-benchmark-exhibition-performance.md b/docs/analysis/mobile-benchmark-exhibition-performance.md new file mode 100644 index 0000000..19c1657 --- /dev/null +++ b/docs/analysis/mobile-benchmark-exhibition-performance.md @@ -0,0 +1,131 @@ +# 전시·공연 모바일 앱 벤치마킹 (인기순) — 킨텍스 관람객(B2C) 앱 + +> 작성일: 2026-07-12 · 대상 트랙: KINTEX AI 전시·행사시스템 관람객(M10)·모바일 앱(`mobile/`, Expo) +> 소유자 지시: "모바일은 전시·공연 앱 벤치마킹 => 다운로드·좋아요가 가장 많은 순으로" +> 원칙: 스토어에서 확인된 수치만 사실로 기재하고 미확인·추정은 명시. 본 문서는 **분석·권고**만 기록하며 PLANNING.md·design.md를 직접 수정하지 않는다(적용은 kintex-mobile-orchestrator/designer 경유). +> 보완 관계: 기존 `docs/analysis/ticketing-app-benchmark.md`(예매·티켓 기능 대조)의 **상위 문서** — 여기서는 앱 인기순 랭킹·IA/UX·패턴 카탈로그를 다루고 기능 세부 대조는 티켓 문서를 참조한다. + +--- + +## 0. 방법론 · 데이터 신뢰도 주의 + +- 수집: Google Play / App Store(KR) 공개 페이지 + 공개 보도. 크롤은 WebFetch/WebSearch만 사용. +- **Google Play 설치수**는 봇 차단으로 자동 수집 시 헤더만 노출되어 다수 **추정(범위)**로 표기했다. App Store는 평점·평가수가 안정적으로 노출되어 실값을 기재했다. +- ★**중대 해석 주의**: 한국 티켓팅 앱의 App Store 평점은 **1.3~1.8**로 매우 낮다. 이는 품질이 아니라 **티켓팅 실패·대기열 좌절에 따른 별점 폭탄(review bomb) 아티팩트**다 — 설치수·실사용은 오히려 최상위다. 따라서 "좋아요(평점) 순"과 "다운로드 순"은 **상반**하며, 아래 §1에 **두 개의 랭킹**을 분리 제시한다. + +--- + +## 1. 인기순 랭킹표 (두 축) + +### 1-A. 다운로드(설치) 기준 — "가장 많이 쓰는" + +| 순위 | 앱 | 분류 | 플랫폼 | 설치수(Android) | 평점 | 평가/리뷰수 | 출처·비고 | +|---|---|---|---|---|---|---|---| +| 1 | **NOL(야놀자)** | 여가·공연·전시 슈퍼앱 | GP·AS | **1,000만+** (GP 단독) | — | — | outstanding.kr 보도(GP 누적 1천만). 공연·전시 예매 포함 슈퍼앱 | +| 2 | **NOL 티켓(구 인터파크티켓)** | 공연·전시·티켓 빅3 | GP·AS | 100만~500만+ *(추정)* | iOS 1.8 | iOS 2,100 | AS id440487844. 국내 공연·전시 예매 1위권. 평점=별점폭탄 | +| 3 | **Fever** (글로벌) | 이벤트·전시 발견·티켓 | GP·AS | **14,000,000** | GP 4.70 | 140,000 | appbrain/GP. 몰입형 전시(반고흐 등) 강세 | +| 4 | **Eventbrite** (글로벌) | 이벤트 발견·티켓 | GP·AS | 1,000만+ | GP 4.9 | 대량 | GP/SimilarWeb | +| 5 | **DICE** (글로벌) | 공연·라이브 티켓 | GP·AS | 월 1,000만+ 사용 | iOS 4.8 | iOS 97,000 | GP fm.dice. 양도(waitlist)·반환매 강점 | +| 6 | **티켓링크** | 공연·스포츠 티켓 빅3 | GP·AS | 50만~100만+ *(추정)* | — | — | GP kr.co.ticketlink.cne. 스마트티켓·스마트스탬프 | +| 7 | **멜론티켓** | 공연 티켓 빅3 | GP·AS | 50만~100만+ *(추정)* | iOS 1.8 | iOS 426 | AS id1103635576. 좌석 5분 선점 | +| 8 | **예스24 티켓** | 공연 티켓 | GP·AS | 10만~50만+ *(추정)* | iOS 1.3 | iOS 773 | AS id937042887 | +| 9 | **아트맵 ARTMAP** | 전시 큐레이션·발견 | GP·AS | 회원 14만+ (앱+웹+SNS) | 정성 긍정 | — | art-map.co.kr. 취향 기반 전시 추천·좋아요/다녀옴 | +| 10 | **국립현대미술관 전시안내(MMCA)** | 미술관 관람 | GP·AS | 5만~10만+ *(추정)* | iOS 4.2 | iOS 20 | AS id1350753069. 전시예약·QR관람권·오디오가이드·실내길찾기 | +| — | 서울시립미술관 도슨팅 / 국립중앙박물관 가이드 | 미술관/박물관 | GP·AS | 소~중규모 *(추정)* | — | — | 도슨트·오디오가이드 중심 | +| — | KOPIS(공연예술통합전산망) | 공연 통계·정보 공공 | 모바일웹 중심 | 전용앱 미약 | — | — | kopis.or.kr/mob. 예매 아닌 정보·통계 허브 | +| — | Whova (글로벌 컨퍼런스) | B2B 이벤트 앱 | GP·AS | 100만+ | 높음 | — | 기존 티켓 벤치마크 §1 참조(개인 아젠다·QR 명함·셀프 체크인) | + +### 1-B. 좋아요(평점·만족도) 기준 — "가장 사랑받는" + +| 순위 | 앱 | 평점 | 근거 | 킨텍스 시사 | +|---|---|---|---|---| +| 1 | **Eventbrite** | GP 4.9 | 발견성·간편 등록·게스트리스트 체크인 | 무료·정원제 전시 등록 UX가 콘서트 예매보다 관람객 앱에 적합 | +| 2 | **DICE** | iOS 4.8 (97K) | 무발권 QR·정품 양도(waitlist)·반환매 | 티켓 신뢰(재판매·양도) UX 우수 | +| 3 | **Fever** | GP 4.70 (140K) | 몰입형 전시 발견·큐레이션·추천 | 전시 "발견" 홈피드·추천이 만족도 핵심 | +| 4 | **국립현대미술관(MMCA)** | iOS 4.2 | 전시예약+QR관람권+오디오가이드+실내길찾기 **한 앱** 통합 | 킨텍스 관람객 앱과 가장 근접한 국내 롤모델 | +| 5 | **아트맵 ARTMAP** | 정성 긍정 | 취향 추천·좋아요/다녀옴 컬렉션 | 개인화 발견·"찜/방문" 데이터 루프 | +| 하위 | 국내 티켓팅 빅3(NOL 티켓·멜론·예스24) | 1.3~1.8 | **별점 폭탄 아티팩트**(티켓팅 좌절) | 설치는 최상위이나 만족도 낮음 → 킨텍스는 대기열 스트레스 낮은 무료 전시라 **만족도 우위 확보 가능** | + +**요지**: 킨텍스 관람객 앱의 벤치마크 최적 조합 = **MMCA(국내 전시 통합 UX) + Eventbrite/DICE(신뢰·발견·양도) + Fever/아트맵(개인화 발견) + Whova(현장 인게이지먼트·비즈매칭)**. 국내 티켓팅 빅3는 예매 퍼널·부정입장 방지 기법만 차용(만족도 반면교사). + +--- + +## 2. 상위 앱별 IA · 기능 · UX 분석 + +### 2-1. 국립현대미술관 전시안내 (MMCA) — 국내 전시 앱 롤모델 (iOS 4.2) +- **홈 IA**: 전시추천 → 전시예약 → QR관람권 → 오디오가이드(작품감상) → 실내 길찾기. **관람 여정 전체를 한 앱**에 담은 국내 유일급. +- **차용가치 최상**: (a) **전시 사전예약→QR 관람권 발권→현장 QR 입장** 폐루프, (b) **작품 단위 오디오가이드**(전시장 비콘/번호 입력), (c) **관내 실내 길찾기**. 킨텍스 SCR-M5/M6/M14/M15와 1:1 대응. +- **약점**: 4관 한정·평가 표본 적음(20). 추천·소셜·다국어 얕음. + +### 2-2. Fever (글로벌, 14M·4.70) — 전시 "발견" 엔진 +- **홈 IA**: 위치기반 **발견 피드**(오늘/이번주/인기/카테고리 타일) 중심. 리스트가 아니라 **큐레이션 카드**. +- **UX 강점**: 몰입형 전시(디지털 아트) 강세, **관심·위치 기반 추천**, 원탭 예약, 캘린더 리마인더. +- **차용**: 관람객 홈을 "행사 리스트"가 아닌 **개인화 발견 피드**(추천 존·근처·마감임박·AI추천 칩)로. + +### 2-3. DICE (글로벌, iOS 4.8·97K) — 티켓 신뢰 UX +- **강점**: **무발권 QR**(입장 직전 활성화), **정품 재판매/양도 waitlist**(암표 차단), 리마인더, 라인업 상세. +- **차용**: 티켓 **양도·환불 대기열**(정원 소진 세션/티켓의 대기·취소표 알림), 입장 직전 QR 활성화(캡처 무효). + +### 2-4. Eventbrite (글로벌, GP 4.9) — 무료·게스트 등록 표준 +- **강점**: 마켓플레이스 **발견성**, 무료/유료 티켓 유형 선택→간편 등록→QR, 오거나이저 앱 게스트리스트 체크인. +- **차용**: 전시(무료·정원제)에 맞는 **짧은 등록 퍼널 + 게스트 등록 + 셀프 체크인**. + +### 2-5. 아트맵 ARTMAP (전시 큐레이션, 회원 14만+) +- **강점**: **취향 기반 전시 추천**, **좋아요·다녀옴** 버튼으로 개인 컬렉션·취향 분석, 근처 갤러리, 일 400+ 전시 정보. +- **차용**: **찜(좋아요)·다녀옴 데이터 루프 → 개인화 추천** (킨텍스 즐겨찾기를 취향학습으로 승격). + +### 2-6. 국내 티켓팅 빅3 (NOL 티켓·멜론·티켓링크·예스24) — 예매·입장 기법 (반면교사) +- **예매 퍼널**: 회차/좌석→권종→예매자→결제→완료. 좌석 선점 타이머(멜론 5분)·대기열·매크로 방지(CAPTCHA). +- **입장/부정방지**: 티켓링크 **스마트티켓**(무발권 QR/바코드)+**스마트스탬프**(단말 고유값 인식 → 캡처본 무효). QR 인식 위해 밝기 상향. +- **개인화**: 관심인물/공연/장르, **티켓오픈 알림**, 예매대기·취소표 우선권. +- **차용**: 부정입장 방지(디바이스 바인딩·회전 QR)·오픈 알림·다구간 환불 규정 — 상세는 `ticketing-app-benchmark.md` §2·§3. +- **주의**: 낮은 평점=티켓팅 스트레스. 킨텍스 무료 전시는 이 스트레스가 없어 **만족도 우위**가 기본값. + +### 2-7. Whova (글로벌 B2B 컨퍼런스) — 현장 인게이지먼트 +- **강점**: 참가자 고유 QR(배지+앱), **개인 아젠다(세션 정원)**, **QR 명함교환·매치메이킹·밋업**, 셀프 체크인 키오스크, 라이브 공지/폴. +- **차용**: 비즈매칭(SCR-M8)·세션 아젠다 정원(SCR-M9)·QR 명함교환·라이브 공지 피드. + +--- + +## 3. 패턴 카탈로그 (전시·공연 앱 공통 베스트 프랙티스) + +| 패턴 | 정의 | 대표 앱 | 킨텍스 대응 | +|---|---|---|---| +| **P-A 발견 피드 홈** | 리스트가 아닌 위치·취향 기반 큐레이션 카드 홈 | Fever·아트맵·NOL | SCR-M5 홈을 발견 피드로 | +| **P-B 관람 폐루프** | 예약→QR 관람권→현장 QR 입장→배지 승격 | MMCA·Whova | SCR-M14→M15→체크인→M5 (이미 정의) | +| **P-C 무발권/회전 QR** | 입장 직전 활성화·디바이스 바인딩으로 캡처 무효 | DICE·티켓링크 | SCR-M15 강화(현재 정적 QR) | +| **P-D 오픈/관심 알림 구독** | 오픈 예정·관심 행사 구독→푸시/EDM | NOL·COEX | 신규(공개사이트+앱) | +| **P-E 취향 학습(좋아요/다녀옴)** | 찜·방문 데이터→개인화 추천 | 아트맵·Fever | SCR-M5·M7 즐겨찾기→추천 승격 | +| **P-F 개인 아젠다·정원 RSVP** | 세션 즐겨찾기 + 정원·마감·대기 | Whova | SCR-M9 정원 상태 추가 | +| **P-G QR 명함교환·컨택트 지갑** | 관람객↔관람객/참가업체 상호 QR | Whova | SCR-M8 확장 | +| **P-H 오디오가이드/도슨트** | 작품·부스 단위 음성 해설 | MMCA·서울시립·박물관 | 신규(킨텍스 부스/전시 해설) | +| **P-I 실내 wayfinding** | 블루닷·경로·존레벨 폴백 | MMCA·COEX | SCR-M6(이미 정의) | +| **P-J 현장 편의 통합** | 주차 실시간·결제·혼잡/대기 한 동선 | 킨텍스 공식앱·COEX | 신규(주차·혼잡 위젯) | +| **P-K 셀프 체크인/즉석 배지** | 키오스크 셀프 등록·현장 배지 인쇄 | Whova·Eventbrite | 신규 화면 | +| **P-L 다국어·접근성** | 한/영/중/일 + WCAG AA + QR 대체 | Whova·COEX | 전 화면(강화) | +| **P-M 티켓 양도·취소표 대기** | 정품 재판매·대기열 알림(암표 차단) | DICE·NOL | 신규(정원 세션·티켓) | + +--- + +## 4. 출처 URL +- NOL(야놀자) 1천만 다운로드: https://outstanding.kr/ (보도), GP `com.cultsotry.yanolja.nativeapp` +- NOL 티켓(인터파크): AS https://apps.apple.com/kr/app/id440487844 · GP `com.interpark.app.ticket` +- 멜론티켓: AS https://apps.apple.com/kr/app/id1103635576 · GP `com.iloen.melonticket` +- 예스24 티켓: AS https://apps.apple.com/kr/app/id937042887 +- 티켓링크: GP `kr.co.ticketlink.cne` · 스마트티켓 https://www.ticketlink.co.kr/help/guide/popup/smart-ticket +- 아트맵 ARTMAP: AS id1446240686 · GP `com.artmap.app` · https://art-map.co.kr/ +- 국립현대미술관 전시안내: AS https://apps.apple.com/kr/app/id1350753069 · GP `kr.go.mmca.hdexhibit` +- 서울시립미술관 도슨팅: GP `seoul.museum.art.act` · 국립중앙박물관: GP `or.kr.nationalmuseum` +- KOPIS: https://www.kopis.or.kr/mob/main/main.do +- Fever: GP `com.feverup.fever` · appbrain fever-events-tickets +- DICE: GP `fm.dice` · https://dice.fm/ +- Eventbrite: GP `com.eventbrite.attendee` · SimilarWeb 통계 +- Whova: https://whova.com/whova-event-app/ +- (기능 세부 대조) `docs/analysis/ticketing-app-benchmark.md` + +--- + +## 변경 이력 +| 날짜 | 변경 내용 | +|---|---| +| 2026-07-12 | 최초 작성 — 전시·공연·티켓·미술관 앱 인기순 랭킹(다운로드/평점 2축), MMCA·Fever·DICE·Eventbrite·아트맵·티켓팅빅3·Whova IA/UX 분석, 패턴 카탈로그 13종(P-A~P-M). 별점폭탄 아티팩트 해석 주의 명기 | diff --git a/docs/architecture/data.md b/docs/architecture/data.md index fa36fe4..a0d0632 100644 --- a/docs/architecture/data.md +++ b/docs/architecture/data.md @@ -689,8 +689,193 @@ erDiagram --- +## 10. FK 최소화·공통코드 관리 표준 (소유자 지시 2026-07-12) + +> 소유자 원칙: **"FK는 최소화, 공통코드로 관리"**. 본 절은 §4·§5-5(DQ) 위에 **참조무결성·범주값 관리 방식의 단일 정책**을 확정한다. 이미 적용된 마이그레이션(V1~V43)은 **불변** — 본 절은 표준·감사·백로그이며 파괴적 재작성을 지시하지 않는다. 구현(신규 멱등 마이그레이션)은 kintex-db-engineer. + +### 10-1. 물리 FK 제약 최소화 정책 + +- **기본값 = 물리 FK 미설정**: 신규 테넌트/도메인 테이블은 DB `FOREIGN KEY`/`REFERENCES`를 **두지 않는다**. 참조무결성은 **애플리케이션 레이어(서비스·매퍼 검증) + 명명 규약(`*_id` 소프트 참조)**로 보장(§4-1 FK 명명 유지, 물리 제약만 생략). +- **근거(4)**: ① **멀티테넌트 복합키 마찰** — 테넌트 루트(`event`·`hall`·`app_user`)는 `PRIMARY KEY (tenant_id, id)`로 전환됨(V31). 단일 `id` 참조 FK는 `UNIQUE(id)` 보조제약을 강제하고, V31이 실제로 **전 자식 FK를 드롭→복합PK 전환→UNIQUE(id)로 재생성**하는 동적 스윕을 수행해야 했다(마찰 실증). ② **MyBatis** — 조인·삭제 순서를 앱이 제어. ③ **마이그레이션·시드 순서 자유** — 멱등 `ON CONFLICT` 시드가 부모 선삽입에 묶이지 않음. ④ **성능·재배치** — 부스 replaceBooths·부스 교체 시 자식 재지정이 잦음(V3 utility_order·render_job는 이미 소프트 참조 채택 — 정본 사례). +- **예외 화이트리스트(FK 유지 허용)**: 아래 **강한 무결성이 필수이고 테넌트 복합키가 아닌 전역 시스템/RBAC 구성**만 물리 FK를 허용한다. 그 외 신규 FK 신설 **금지**. + +| # | 자식 → 부모 | 마이그레이션 | 유지 사유 | +|---|---|---|---| +| W1 | `common_code.grp_code` → `common_code_group` | V7 | 코드값 고아 방지(공통코드 정합의 근간)·전역·정적 | +| W2 | `sys_menu.parent_id` → `sys_menu`(self) | V7 | 메뉴 트리 순환/고아 방지·전역 | +| W3 | `sys_role_permission`(role_code→`sys_role`, perm_code→`sys_permission`) | V7 | RBAC 권한 매핑 무결성(보안 임계)·전역 | +| W4 | `sys_role_menu`(role_code→`sys_role`, menu_id→`sys_menu`) | V37 | RBAC 메뉴 매핑 무결성·전역 | + +### 10-2. 공통코드로 범주값 관리 정책 + +- **범주형 컬럼 = 공통코드**: 상태·유형·카테고리·모드·구분·심각도 등 **열거 가능 소수값**은 자유문자열/DB enum/전용 참조테이블이 아닌 **공통코드(`common_code_group`/`common_code`)로 관리**. 컬럼엔 코드값(영문 상수)만 저장, 표시명은 조인/캐시(§4-3, COMMON_CODES 정본). DB `ENUM` 물리타입은 지양(§4-2 — 룰셋/코드 유연성). +- **정본 화면 = W12 공통코드 관리**(CommonCodeAdminPage): 그룹/상세 CRUD의 단일 관리 지점. 신규 범주 컬럼 도입 시 **먼저 공통코드 그룹을 정의**하고 컬럼 주석에 `-- 공통코드(GRP)` 표기(V37 `partner_type` 사례). +- **코드 vs 마스터 경계(불변, §4-3)**: 다건·CRUD·버전 대상(홀·요율·규정 룰셋·등록업체)은 공통코드가 아니라 마스터/룰셋. 범주 컬럼만 공통코드로. + +### 10-3. 테넌트 표준 정합 + +- 공통코드도 **테넌트 스코프 원칙 준수**([[tenant-id-pk-standard]]). 현행 `common_code_group`/`common_code`는 전역(테넌트 미부여) 이식본 — 킨텍스 단일 테넌트 운영 중엔 전역 공유가 유효하나, 멀티테넌트 확장 시 **테넌트별 코드 오버라이드**가 필요하면 `(tenant_id, grp_code, code)` 확장을 백로그로 둔다(§10-7 B4, 지금은 순증 시드만). +- 소프트 참조 인덱스는 `(tenant_id, *_id)` 복합 선두로 생성(§10-4). + +### 10-4. 소프트 참조 무결성 보완책 (명문화) + +물리 FK를 생략하는 대신 아래를 강제한다: +1. **앱 레이어 검증**: 부모 존재 확인은 서비스/매퍼에서 수행(삽입 전 조회 또는 조인 검증). 낙찰·정산 등 임계 트랜잭션은 명시적 존재검증 필수. +2. **삭제 시 고아 방지 규약**: 부모 삭제는 서비스가 자식 선삭제/무효화(soft-delete `use_yn='N'` 우선). 물리 CASCADE에 의존하지 않는다. +3. **논리참조 인덱스**: 조회·조인·고아 스캔 가속을 위해 소프트 FK 컬럼에 `(tenant_id, _id)` 인덱스(§10-7 B2). +4. **고아 검증 쿼리(DQ)**: 야간/배포 후 `LEFT JOIN ... WHERE parent.id IS NULL` 고아 스캔을 운영 점검 쿼리로 상비(마트 적재 전 DQ 게이트, §6-4). §5-5 DQ의 "참조무결성(FK)"은 **소프트 참조 무결성(앱+검증쿼리)**으로 해석 갱신. + +--- + +## 11. 뷰·구체화뷰·함수 사용 지침 (소유자 지시 2026-07-12) + +> 파생·집계·재사용 계산은 **결정론적 SQL 객체**로 캡슐화해 상태 백필·중복 로직·토큰 낭비를 줄인다. **실사용 근거 없는 선제 생성 금지**(남용 방지). + +### 11-1. VIEW (`v_*`) — 실시간 파생·경량 조인·상태 산출 + +- 용도: 저장 없이 **결정론적 파생**(생명주기 상태·경량 조인·표시용 코드명 조인). 상태/구분 컬럼 백필 대신 **뷰 산출 권장**. +- 정본 사례(V43): `v_event_calendar`(날짜로 `lifecycle` UPCOMING/ONGOING/ENDED 산출 — 별도 상태 컬럼 불필요), `v_event_monthly_summary`(월별 건수 집계). 이 패턴을 신규 파생에 재사용. +- 규약: `CREATE OR REPLACE VIEW`, `tenant_id` 컬럼 노출·필터 유지, 하부 테이블 인덱스에 의존(뷰 자체 인덱스 불가), 민감 컬럼(§5-1 P1/P3) 미노출·마스킹만. + +### 11-2. MATERIALIZED VIEW (`mv_*`) — 무겁고 자주 조회·실시간성 낮은 집계 + +- 용도: **BI 대시보드 KPI·홀 가동률·리드/ROI·월/연 통계** 등 비용 큰 집계로 실시간성이 덜 중요한 것(§6 마트 KPI_SNAPSHOT과 정합 — mview는 경량 대체/보조). +- **새로고침 전략 명시 필수**: 주기(야간 배치)·`REFRESH MATERIALIZED VIEW CONCURRENTLY`(무중단·유니크 인덱스 전제)·mview 자체 인덱스(`tenant_id` + 조회 키) 생성. +- 남용 판단 기준: + +| 상황 | 선택 | +|---|---| +| 실시간성 필수·경량 | VIEW | +| 실시간성 낮음·집계 무거움·반복 조회 | MATERIALIZED VIEW | +| 경영 KPI 시계열·스냅샷 재현성 | KPI_SNAPSHOT 테이블(§6) | +| 1회성·희소 조회 | 뷰/mview 생성 안 함(온디맨드 쿼리) | + +### 11-3. FUNCTION (`fn_*`) — 재사용 계산 로직 캡슐화 + +- 용도: 여러 쿼리·화면 공유 계산(기간→분기 산출, 요율 계산 보조, 거리/트래블타임 보조). **부수효과 없는 순수/`IMMUTABLE`·`STABLE` 우선**, PL/pgSQL은 꼭 필요할 때만(SQL 함수 우선). +- 규약: `CREATE OR REPLACE FUNCTION fn_*`, `tenant_id` 파라미터화, 룰셋 의존 계산(요율)은 스냅샷 버전 인자(§4-4)로 재현성 보장. + +### 11-4. STORED PROCEDURE (`sp_*`) — 집합연산·다단계 트랜잭션 + +- 용도: **대량 집합 처리·다단계 트랜잭션·정기 롤업**은 앱 루프(행 단위 왕복) 대신 **DB 프로시저(PL/pgSQL) 세트기반 처리**. 예: 월마감 집계, `REFRESH MATERIALIZED VIEW`, 대량 상태전이, 정산 롤업. +- **경계(남용 금지)**: **비즈니스 로직 대부분은 앱(Service) 유지**. DB 프로시저는 **성능이 결정적일 때만**(대량·세트기반이 앱 루프 대비 확연히 유리). 검증·권한·감사·룰셋 판정 등 도메인 규칙을 프로시저로 이관하지 않는다. +- 순수/부수효과 구분: 반환값만 있는 재사용 계산 = `fn_*`(`IMMUTABLE`/`STABLE`), **부수효과(쓰기·다단계 커밋) 있는 것만 `PROCEDURE sp_*`**(`CALL`). 명명 `fn_*`/`sp_*`, 멱등 `CREATE OR REPLACE`, `tenant_id` 파라미터·스코프 준수. + +### 11-5. 공통 규약 + +- 명명 `v_*`·`mv_*`·`fn_*`·`sp_*`. 마이그레이션 멱등(`CREATE OR REPLACE` / mview는 `IF NOT EXISTS` 가드). 테넌트 스코프 유지. 실사용 근거(화면/API/AI 답변) 있는 것만 생성. + +--- + +## 13. 자동화 배치 카탈로그 (대상·주기·멱등 — 데이터 표준 관점) + +> **무엇을 배치로 돌릴지**(대상·주기·멱등·잠금)만 데이터 표준에서 정의한다. **실행 프레임워크**(Spring `@Scheduled`/Quartz/cron·분산락)는 아키텍처(A-1 app.md/A-3 tech.md, kintex-sa/ta) 표준 영역 — 본 카탈로그를 그쪽으로 링크. 구현 인계: **스케줄러=backend-dev, 프로시저/mview=db-engineer**. + +| # | 배치 작업 | 데이터 대상 | 주기(권고) | 멱등성 | 재시도·잠금 요건 | +|---|---|---|---|---|---| +| J1 | **mview 새로고침** | `mv_hall_occupancy`·`mv_lead_roi_summary`·KPI 집계(§6) | 야간 1회(+수요 트리거) | `REFRESH ... CONCURRENTLY` 자연 멱등 | 단일 실행락(중복 REFRESH 방지)·실패 시 다음 주기 | +| J2 | **KPI 스냅샷 적재** | `KPI_SNAPSHOT`(§6-4, `built_at` 각인) | 야간 배치 | 스냅샷 키(date_key,scope) UPSERT | 원천-집계 정합 DQ 통과 후 커밋 | +| J3 | **마감임박 알림·해야할일 자동생성** | D-데이(문서/마일스톤 D-150/30/25/7)·옥션 deadline·납부 스케줄 | 일 1회(+정시) | 발송/생성 대상 dedup 키(대상+일자) | 이미 발송분 스킵(중복 통지 금지)·실패 재큐 | +| J4 | **EDM/옥션 통지 발송** | 캠페인·옥션 통지(SMTP, [[smtp-email-approval]]) | 예약시각·이벤트 트리거 | mail_log 상태(sent/skipped)로 재발송 차단 | 마케팅 미동의 세그먼트 제외 게이트(§5-3)·발송락 | +| J5 | **세션/토큰·만료 데이터 정리** | 만료 비번재설정 토큰·잠금 해제·로그인이력 보존기간 | 시간별/일별 | 조건부 삭제(자연 멱등) | — | +| J6 | **소프트 참조 고아 검출(DQ)** | §10-4 논리참조 무결성(event/hall/company 참조 고아) | 야간(마트 적재 전) | 읽기 전용 스캔 | 고아 발견 시 리포트·차단(마트 적재 게이트) | +| J7 | **PII 보존·파기** | 관람객 등록·배지·체크인(행사종료+1년, §5-4) | 일 1회 | 기간 경과분 익명화/삭제(재실행 안전) | 동의 연장분 제외·감사로그 기록 | +| J8 | **행사 crawl 갱신** | 외부 행사정보(kintex-crawler 트랙) | 정기(일/주) | 소스키 UPSERT | 소스 실패 격리·부분성공 허용 | +| J9 | **정산 롤업** | `sp_settlement_rollup`(정산→FACT_SETTLEMENT) | 마감 주기 | 재실행 시 기간 재계산(UPSERT) | 정산 확정 상태만 대상·실행락 | + +- **공통 요건**: 모든 배치는 ① **멱등**(재실행 안전 — UPSERT/조건부/dedup), ② **중복실행 방지 잠금**(분산락 — 실행수단은 아키텍처), ③ **실패 격리·재시도**(부분성공 허용·다음 주기 복구), ④ **감사**(J4·J7 등 통지·파기는 `audit_log` 기록), ⑤ **테넌트 스코프**(배치도 tenant 루프/필터). +- 세트기반 롤업(J2·J9)·대량 상태전이는 §11-4 `sp_*` 프로시저 후보. 알림/발송(J3·J4)의 **판정·세그먼트 규칙은 앱(Service)** — 프로시저는 대량 적재만. + +--- + +## 12. 감사표 + kintex-db-engineer 인계 백로그 + +> 대조 대상: `src/backend/src/main/resources/db/migration/` V1~V43 전수. **파괴적 재작성 금지** — FK 드롭은 권고(별도 승인 후), 즉시 실행 백로그 = 공통코드 시드·논리참조 인덱스·뷰(순증 멱등). 신규 마이그레이션 번호는 **V43 이후 여유 번호(V44~) 권고**(진행 중 V43·신규분과 충돌 회피 — 실제 번호는 db-engineer가 병합 시점 최댓값+1로 확정). + +### 12-1. 감사표 A — 현행 물리 FK 목록 (유지/제거 권고) + +현행 물리 FK 약 40건. **유지=화이트리스트 4그룹(§10-1)**, 그 외는 소프트 참조 **제거 권고**(우선순위 P1: 테넌트 루트 참조 / P3: 아그리게잇 내부 — 무해·비복제). + +| 분류 | 자식 → 부모(FK) | 마이그레이션 | 판정 | +|---|---|---|---| +| **유지(W1~W4)** | common_code.grp_code→common_code_group; sys_menu.parent_id(self); sys_role_permission(role/perm); sys_role_menu(role/menu) | V7·V37 | **유지** | +| **P1 제거권고** | hall_assignment.event_id→event, hall_id→hall; event_member.event_id→event, user_id→app_user, company_id→company | V2 | 소프트 참조(테넌트 루트·V31 마찰) | +| **P1 제거권고** | trench.hall_id→hall; hall_exit.hall_id→hall; layout.event_id→event, hall_id→hall; utility_order.event_id→event | V3 | 소프트 참조(hall/event 루트) | +| **P1 제거권고** | doc/milestone 3× event_id→event | V14 | 소프트 참조 | +| **P1 제거권고** | dock_reservation.event_id→event, hall_id→hall, dock_id→dock; booth_sale.event_id→event, hall_id→hall | V15·V27 | 소프트 참조 | +| **P1 제거권고** | auction.event_id→event; auction_invite.company_id→company; bid.company_id→company; company_reputation.company_id→company | V16 | 소프트 참조(company/event 루트) | +| **P1 제거권고** | visitor_registration.event_id→event; lead.event_id→event; campaign 3× event_id→event; sponsorship.package_id; settlement.event_id→event; logistics 3× event_id→event | V17·V18·V24·V33 | 소프트 참조 | +| **P3 잔류허용(무해·비복제)** | booth.layout_id→layout; design_plan.booth_id→booth; auction_invite/bid/award.auction_id→auction, award.bid_id→bid; content_version.content_id→cms_content; payment_schedule/refund/tax.invoice_id→invoice; approval_line/history.approval_id→approval; message_recipient/opinion_reply/meeting_attendee | V3·V8·V16·V19·V24·V29·V34 | 아그리게잇 내부 CASCADE — 유지해도 무해, **신규 테이블엔 미복제**(소프트 우선) | + +> **주의**: P1 제거는 **자동 실행 대상 아님** — 기존 FK 드롭은 테넌트 복합키 정합·데이터 검증 후 **소유자 승인 별도 DROP 마이그레이션**으로만. 표준의 실질 효력은 **신규 테이블에 FK 미신설 + P1 패턴 미복제**에 있다. + +### 12-2. 감사표 B — 범주형 컬럼 → 공통코드 그룹 매핑 (전환 후보) + +자유문자열/주석 열거 범주 컬럼 ≈ **34개**. 기존 시드 그룹(V7·V37)과 정합, 신규 그룹(★)은 V44 순증 시드 대상. 코드값=현행 저장값 유지(스키마 불변). + +| 그룹코드 | 컬럼(테이블.컬럼) | 마이그 | 코드값(현행) | 상태 | +|---|---|---|---|---| +| `USER_STATUS`★ | app_user.status | V2 | ACTIVE·(LOCKED·INACTIVE) | 신규 | +| `CONTRACTOR_CATEGORY`★ | company.category, auction.category | V2·V16 | 14분류(전시디자인설치·리깅·전기시설·카펫/파이텍스·급배수Air·가스·철거·운수통관·가구비품·경비·광고싸인·지게차·방염·구조해석) | 신규(COMMON_CODES §2 승격) | +| `HALL_FLOOR_TYPE`★ | hall.floor_type | V2 | concrete_polished·carpet·outdoor | 신규 | +| `MASTER_DATA_CATEGORY`★ | master_data.category | V2 | RATE·UTILITY_FEE·COMPLIANCE | 신규 | +| `EVENT_STATUS`★ | event.status | V2 | active·cancelled·(closed) | 신규 | +| `EVENT_CATEGORY`★ | event.category | V23 | (행사 분류 — 값 확인 필요) | 신규 | +| `LAYOUT_STATUS` | layout.status | V3 | draft·submitted·approved·rejected | COMMON_CODES 등재(시드화 필요) | +| `BOOTH_TYPE` | booth.booth_type | V3 | assembled·independent·corner·island | 등재(corner·island 추가) | +| `DESIGN_STATUS` | design_plan.status | V3 | draft·submitted·approved·rejected | 등재(시드화) | +| `UTILITY_ORDER_STATUS` | utility_order.status | V3 | draft·submitted·relayed | 등재(시드화) | +| `RENDER_STATUS` | render_job.status | V3 | QUEUED·RUNNING·DONE·FAILED | 등재(시드화) | +| `SHOT_PRESET` | render_job.shot_preset | V3 | S1~S7 | 등재(시드화) | +| `MILESTONE_TYPE`★ | milestone.milestone_type | V14 | assignment·pre_review·utility·documents·opening | 신규 | +| `DOC_TYPE`★ | document.doc_type | V14 | operation_plan·booth_layout·disaster_plan… | 신규 | +| `APPROVAL_STATUS`★ | document.status, approval.status | V14·V29 | pending·draft·submitted·approved·rejected / DRAFT… | 신규 | +| `DOCK_RESV_STATUS`★ | dock_reservation.status | V15 | (예약 상태) | 신규 | +| `AUCTION_TYPE` | auction.auction_type | V16 | reverse·rfq | 등재(COMMON_CODES §2, 소문자 정합) | +| `AWARD_CRITERIA`★ | auction.award_criteria | V16 | lowest·comprehensive | 신규 | +| `QUOTATION_STATUS` | bid.status | V16 | submitted·revised·awarded·rejected | 등재(확인 필요 → 확정) | +| `VISITOR_TYPE` | visitor_registration.visitor_type | V17 | visitor·buyer·vip | 등재(vip 추가) | +| `CHECKIN_STATE`★ | visitor_registration.checkin_state | V17 | done·waiting·cancelled | 신규 | +| `CAMPAIGN_STATUS`★ | campaign.status | V18 | draft·scheduled·sending·done | 신규 | +| `SPONSOR_CONTRACT_STATUS`★ | sponsorship.contract_status | V18 | signed·pending | 신규 | +| `CONTENT_TYPE`★ | cms_content.content_type | V19 | PAGE·POST·NOTICE·BLOCK | 신규 | +| `CONTENT_STATUS`★ | cms_content.status | V19 | draft·review·approved·published | 신규 | +| `TRANS_STATUS`★ | content_i18n.trans_status | V19 | none·ai·reviewed | 신규 | +| `INQUIRY_TYPE`★ | public inquiry.inquiry_type | V20 | shell·raw·premium·general | 신규 | +| `INQUIRY_STATUS`★ | public inquiry.status | V20 | received·in_review·closed | 신규 | +| `HALL_ASSIGN_STATUS`★ | hall_assignment.status | V25 | assigned·… | 신규 | +| `TENANT_STATUS`★ | tenant.status | V26 | active·onboarding·suspended | 신규 | +| `BOOTH_SALE_STATUS`★ | booth_sale.status | V27 | available·held·sold·blocked | 신규 | +| `INVOICE_CATEGORY`★ | invoice.category | V28 | rental·utility·auction_fee… | 신규 | +| `SETTLEMENT_STATUS`★ | settlement.status | V24 | pending·invoiced·paid·overdue | 신규 | +| `PAYMENT_METHOD`★ | payment_schedule.method | V24 | manual·transfer·card | 신규 | +| `APPROVAL_LINE_STATUS`★ | approval_line.line_status | V29 | WAIT·approved·rejected | 신규 | +| `EQUIP_CATEGORY`★·`RENTAL_STATUS`★·`RENTAL_ITEM_CATEGORY`★·`ORDER_STATUS`★·`FREIGHT_STATUS`★ | V33 logistics 5종 | V33 | forklift…/requested…/furniture…/ordered…/registered… | 신규 | +| `REFUND_STATUS`★·`TAX_INVOICE_STATUS`★ | refund.status·tax_invoice.status | V34 | recorded·settled / issued·void | 신규 | +| `WEBHOOK_EVENT_TYPE`★·`WEBHOOK_STATUS`★ | webhook 등 | V35 | lead.hot…/disabled·queued·sent·failed | 신규 | +| `MAIL_CATEGORY`★·`MAIL_STATUS`★ | mail_log.category·status | V36 | AUCTION_OPEN·EDM·PASSWORD_RESET / sent·failed·skipped | 신규 | +| `VISITOR_GUIDE_CAT`★ | visitor_guide.category | V43 | TRANSPORT·PARKING·ADMISSION·FACILITY·ACCESS·OVERVIEW | 신규 | +| `TRANSPORT_MODE`★ | (관람객 교통 안내 세부) | V43 | (지하철·버스·자가용·KTX 등 — 확인 필요) | 신규 | + +> 이미 시드된 그룹(V7·V37): USE_YN·USER_ROLE·VERIFY_METHOD·PRG_TYPE·MSG_RCV_TYPE·WORK_*·SCHE_GUBUN·IMPORTANCE·NOTICE_TYPE·OPINION_STATUS·NOTI_TYPE·REPORT_TYPE·PARTNER_TYPE — 재시드 불필요. **"확인 필요" 코드값(EVENT_CATEGORY·QUOTATION_STATUS·TRANSPORT_MODE 등)은 도메인 에이전트 확정 후 값 고정**(COMMON_CODES §2 규칙). + +### 12-3. db-engineer 인계 백로그 (신규 멱등 마이그레이션 — 번호 V44~ 권고) + +| ID | 백로그 | 형태 | 우선순위 | +|---|---|---|---| +| **B1** | 감사표 B ★ 신규 그룹 + 상세 공통코드 **순증 시드**(`common_code_group`/`common_code`, `ON CONFLICT DO UPDATE`) + 기존 등재 그룹(LAYOUT_STATUS·DESIGN_STATUS·RENDER_STATUS·SHOT_PRESET 등) 시드화. 컬럼 스키마 불변(코드값=현행 저장값). "확인 필요" 값은 확정분만. | V44(멱등 시드) | **P1** | +| **B2** | 소프트 참조 **논리참조 인덱스**: P1 소프트 참조 컬럼에 `(tenant_id, _id)` 인덱스(`CREATE INDEX IF NOT EXISTS`). 예: event 참조 자식들의 `(tenant_id, event_id)`, hall 참조의 `(tenant_id, hall_id)`. 이미 있는 idx(idx_lead_event 등)는 스킵. | V45(멱등 인덱스) | **P2** | +| **B3** | 뷰/mview/함수 **순증 후보**(§11): ⓐ `v_booth_display`(부스+코드명 조인·표시용), ⓑ `mv_hall_occupancy`(홀·기간 가동률 — REFRESH CONCURRENTLY·`(tenant_id,hall_key)` 유니크 인덱스), ⓒ `mv_lead_roi_summary`(리드/ROI 월별), ⓓ `fn_period_to_quarter(date)`·`fn_utility_fee(...,ruleset_version)`(재사용 계산). **실사용 화면/AI 근거 확인 후** 생성. | V46(뷰/함수, CREATE OR REPLACE) | P3 | +| **B4** | (멀티테넌트 확장 시) 공통코드 테넌트 오버라이드 `(tenant_id, grp_code, code)` 확장 — 현재 단일 테넌트라 **보류**(구조 대비만). | 후속 | P4 | +| **B5** | (소유자 승인 후) P1 FK **DROP 마이그레이션** — 테넌트 복합키 정합·고아 검증 통과 후에만. 자동 실행 금지. | 승인 대기 | P4 | +| **B6** | **프로시저 후보**(§11-4·§13): `sp_settlement_rollup`(J9)·`sp_kpi_snapshot_build`(J2)·`sp_refresh_marts`(J1 mview 일괄 REFRESH). 성능 결정적일 때만·CREATE OR REPLACE. | V46(뷰와 동반) | P3 | +| **B7** | **자동화 배치**(§13 J1~J9) — 대상·주기·멱등은 본 표 확정. 실행 프레임워크(스케줄러·분산락)는 **아키텍처(kintex-sa/ta) 인계**, 스케줄러 배선 backend-dev. | 인계(아키텍처+backend) | P2 | + +--- + ## 9. 변경 이력 | 버전 | 일자 | 작성자 | 내용 | |---|---|---|---| +| v1.1 | 2026-07-12 | kintex-data-architect(DA) | **§10 FK 최소화·공통코드 관리 표준**(소유자 지시) — 물리 FK 기본 미설정+화이트리스트 4그룹(W1~W4 RBAC/공통코드 구성), 범주값 공통코드화 정책, 테넌트 스코프 정합, 소프트 참조 무결성 보완책 / **§11 뷰·구체화뷰·함수·프로시저 지침**(`v_*` 실시간 파생·`mv_*` 무거운 집계+REFRESH 전략·`fn_*` 재사용 계산·`sp_*` 집합연산/다단계 트랜잭션·남용 판단 기준) / **§13 자동화 배치 카탈로그**(J1~J9 대상·주기·멱등·잠금 — 실행 프레임워크는 아키텍처 인계) / **§12 감사표 A(현행 FK 약 40건 유지4/제거권고 P1·잔류P3)+B(범주 컬럼 34개→공통코드 그룹 매핑)+db-engineer 백로그 B1~B7(V44 시드·V45 인덱스·V46 뷰·프로시저·배치 인계, 번호 V44~ 권고)**. 기존 마이그레이션 불변·비파괴. | | v1.0 | 2026-07-11 | kintex-data-architect(DA) | 최초 — 전사 데이터 모델(개념→논리→물리 ERD, PLANNING §7 확장)·공간 데이터 표준(PostGIS SRID0·부스 POLYGON·트렌치 POINT·배선 LineString)·데이터 표준(명명·타입·공통코드 WISE 정합·마스터/룰셋 관리)·품질·거버넌스(PII 분류·암호화·동의·보존·감사·계보)·**BI 데이터마트 M16 스타 스키마(FACT_BOOKING/SETTLEMENT/UTILITY/AUCTION/VISITOR + DIM_DATE/HALL/EVENT/EXHIBITOR + KPI_SNAPSHOT, M16-1 7지표 정합)** 정의. 물리 구현은 db-engineer 인수, 코드값 확인필요 항목은 §8 후속. | diff --git a/docs/architecture/sso-hr-integration.md b/docs/architecture/sso-hr-integration.md new file mode 100644 index 0000000..592bbb4 --- /dev/null +++ b/docs/architecture/sso-hr-integration.md @@ -0,0 +1,373 @@ +# 킨텍스 AI 전시·행사시스템 — Open SSO · 인사(조직) API 연동 아키텍처 + +> 산출: 시스템 아키텍트(SA) · 작성일: 2026-07-12 · 대상: 소유자 지시("직원 인사(조직)정보는 API 방식, Open SSO 도입") +> 정합 근거: [`system.md`](system.md)(SA §5·§6 인증/연동), [`app.md`](app.md)(AA 인증 계약), [`network.md`](network.md)(NA 존/아웃바운드), [`data.md`](data.md)(DA 마스터/소프트참조), [`../PLANNING.md`](../PLANNING.md) §2(6역할)·§5B(공통·시스템관리)·§8, `../DEVELOPMENT_GUIDE.md` §4·§5 +> **본 문서는 설계·계약·이행계획만 정본. src·기존 인증코드 미수정(파괴적 구현 금지).** 실제 구현은 인계 백로그(§9)로 각 dev 트랙에 위임. +> 확정 스택(불변): React 18/19(Vite·TS) + Spring Boot 3.x(Java 17)+MyBatis + PostgreSQL(PostGIS) + Redis. 신규 IdP(Keycloak)는 **온프레미스 인프라 구성요소**로 추가(스택 위반 아님). + +--- + +## 0. 요지 (Executive Summary) + +| 항목 | 결정 | 근거 | +|---|---|---| +| **인증 프로토콜** | **OIDC(우선) + SAML 2.0(옵션)** — Authorization Code + PKCE | 표준·웹/모바일 공통·JWKS 검증. SAML은 고객사 레거시 IdP 대비 폴백 | +| **IdP** | **Keycloak(자체 호스팅, 온프레미스)** | 오픈소스·OIDC/SAML/그룹·클레임 매핑·2FA·back-channel 로그아웃 내장. 외부 인터넷 API 아님(내부망 IdP) | +| **기존 JWT+2FA 관계** | **삭제·교체 금지. 공존(coexist)** — SSO를 1차 인증, 앱 JWT를 세션토큰으로 **브로커 발급**. 로컬 로그인은 폴백으로 유지 | W12 자산(app_user·event_member·TotpService) 보존, 무중단 이행 | +| **2FA** | IdP가 OTP 제공 시 **위임**(앱 OTP 강제 해제), 미제공 시 앱 TOTP 유지 | 중복 2FA 방지·기존 `TwoFactorService` 재사용 | +| **인사·조직** | 로컬 마스터 유지가 아니라 **`HrDirectoryClient` 어댑터로 외부 HR API 조달** + 로컬 **읽기 캐시 스냅샷**(HR 미가용 폴백) | HR 정본화, dept/sys_user는 캐시로 전환(멱등 upsert 배치) | +| **3자 매핑** | **SSO subject ↔ HR 사번(empNo) ↔ 로컬 user(app_user.id)** 매핑 테이블 신설 | 계정·조직·권한 정합 단일 키 체계 | +| **이행** | **coexist → pilot(파일럿 부서) → cutover(SSO 우선) → 로컬로그인 축소** 4단계 + 롤백 게이트 | 파괴 없는 점진 전환 | + +**보안 정합(불변):** Open SSO(자체 IdP)·HR API(고객 내부망 인사시스템)는 **온프레미스/내부 통합 → 외부 인터넷 API 금지 규칙에 해당하지 않음**(Claude/Gemini 예외와 별개 범주). 단, 토큰·클라이언트 시크릿·HR 자격증명은 **env/시크릿 스토어 only**, 응답·로그·커밋 미노출, 마스킹 유지. + +--- + +## 1. 현행(As-Is) 인증·조직 기준선 + +설계는 기존 코드를 **읽어 확인한 실제 계약** 위에 얹는다(교체 없음). + +| 자산 | 실체 | 본 설계에서의 처리 | +|---|---|---| +| `app_user`(V2) | id·email·display_name·password_hash(BCrypt)·hall_manager·otp_secret·failed_login_count·locked_until·status | **보존.** 로컬 계정 정본 → 이행 후 "SSO 매핑 대상 + 로컬 폴백" | +| `event_member`(V2) | 행사 RBAC(ORGANIZER·EXHIBITOR·CONTRACTOR·HALL_MANAGER, booth/company 스코프) | **보존.** IdP는 행사 역할을 모름 → 행사 역할은 계속 로컬 권위 | +| `app_user.role_code`(V31) | 전역 역할(rc 클레임) | IdP 그룹/클레임 → 전역 역할 매핑 소스로 확장 | +| `dept`(V37) | 부서 트리(tenant_id·id·parent_id·sort_order·use_yn) | **HR 정본 시 읽기 캐시로 전환**(스냅샷 upsert) | +| `company`(V2/V37) | 등록업체(M7 응찰 게이트) | 불변(HR 무관 — 외부 파트너) | +| `JwtService` | HS256, 클레임 sub·name·roles(eventId→role)·hm·tid·rc | **불변.** SSO 성공 후 이 서비스로 **동일 형태 앱 JWT를 브로커 발급**(다운스트림 RBAC 무변경) | +| `TwoFactorService`/`TotpService`/`LoginAttemptService` | 로컬 2FA·잠금·챌린지 | IdP 2FA 위임 시 우회, 로컬 폴백 시 유지 | +| `JwtAuthenticationFilter` | Bearer 앱JWT → SecurityContext | **불변.** SSO 도입해도 백엔드 보호경로는 계속 앱 JWT 검증(핵심 무중단 포인트) | + +> **설계 핵심 원칙:** SSO는 **로그인 게이트웨이만 교체**한다. 로그인 성공의 산출물은 여전히 "기존 형태의 앱 JWT"이므로, 84개 화면·전 RBAC 가드·행사 스코프 로직은 **한 줄도 바뀌지 않는다**. HR API는 **조직 마스터의 데이터 출처만 교체**한다(dept 트리 소비자 무변경). + +--- + +## 2. 설계 A — Open SSO 도입 (OIDC / Keycloak) + +### 2-1. 채택 결정 (ADR) + +| # | 결정 | 이유 | 대안·트레이드오프 | +|---|---|---|---| +| A1 | **OIDC 우선**(SAML 옵션) | 웹+모바일(expo-auth-session) 동일 표준, JSON/JWKS, PKCE로 공개 클라이언트 안전 | SAML-only(모바일·SPA 부적합) 배제. 고객 레거시가 SAML뿐이면 Keycloak가 SAML IdP↔OIDC 브로커로 흡수 | +| A2 | **Keycloak 자체 호스팅** | 오픈소스·온프레미스·그룹/클레임 매핑·OTP·Identity Brokering(외부 AD/LDAP/SAML 연합)·back-channel 로그아웃 | 상용 IdP(비용·외부 SaaS=보안 위반) 배제. 경량 대안(자체 OIDC 구현)은 표준 준수·유지비 열위 | +| A3 | **브로커드 세션토큰**(IdP 토큰 → 앱 JWT 교환) | 다운스트림 RBAC·행사 스코프·tid/rc 클레임 무변경, 무상태 유지 | IdP 액세스토큰 직접 신뢰(리소스서버 모드)는 **행사 RBAC 클레임 부재**로 전 가드 재작성 필요 → 이행기엔 배제(§2-6 장기 옵션으로만) | +| A4 | **로컬 로그인 폴백 유지** | IdP 장애 시 관리자·핵심 운영 지속(가용성 NFR) | IdP 단일 의존(장애 시 전면 로그인 불가) 배제 | + +**IdP 후보 근거:** Keycloak(1순위, 위 A2) / 대안 Authentik·ZITADEL(경량·최신 UX이나 조직 내 운영 실적·AD 연합 성숙도에서 Keycloak 우위) / Gluu·Ory Hydra(각각 무거움·인증 파트 분리 필요). **킨텍스 온프레미스·AD 연합·SAML 흡수** 요건에서 Keycloak 채택. + +### 2-2. 토폴로지 (존 배치) + +Keycloak는 **내부 애플리케이션 존**에 배치하고, 브라우저 리다이렉트를 위해 **리버스 프록시(nginx)의 별도 vhost**(`auth.`)로만 외부 노출한다. HR API·AD 연합은 IdP↔내부망 구간. + +```mermaid +graph TB + subgraph EDGE["엣지 / DMZ (nginx TLS 종단)"] + RP[리버스 프록시
app.· auth.· api. vhost] + end + subgraph APPZ["애플리케이션 존 (내부망)"] + FE[역할별 프론트 SPA + 공개/관람객] + BE[공유 백엔드 Spring Boot × N
OIDC RP + 앱JWT 브로커] + KC[Keycloak IdP
realm=KINTEX · OIDC/SAML · OTP] + end + subgraph DATAZ["데이터 존 (최심부)"] + PG[(PostgreSQL+PostGIS
app_user·매핑·조직 캐시)] + KCDB[(Keycloak DB
별도 스키마/DB)] + RED[(Redis
state·nonce·세션상관)] + end + subgraph HRZONE["고객 내부망 (연동 대상)"] + AD[AD / LDAP
직원 계정] + HRAPI[HR 인사시스템 API
조직·직원 마스터] + end + FE -->|1 로그인 리다이렉트| RP --> KC + KC -.연합(선택).-> AD + FE -->|2 code+PKCE 콜백| RP --> BE + BE -->|3 code→token 교환·JWKS 검증| KC + BE -->|4 앱 JWT 발급| FE + BE --> PG + BE -.HrDirectoryClient(배치·조회).-> HRAPI + KC --> KCDB + BE --> RED +``` + +**배치 규칙** +- Keycloak DB는 앱 PG와 **별도 데이터베이스/스키마**(계정 데이터 격리·백업 주기 분리). +- `auth.` vhost는 IdP UI/토큰 엔드포인트만 프록시. 백오피스(admin.)와 동일하게 **접근 제한 정책**은 NA 확정. +- 아웃바운드: Keycloak→AD/LDAP, 백엔드→HR API는 **내부망 구간**(인터넷 egress 아님). NA egress 화이트리스트에 인터넷 목적지 추가 없음. + +### 2-3. 로그인 시퀀스 (Authorization Code + PKCE → 앱 JWT 브로커) + +```mermaid +sequenceDiagram + participant U as 사용자(브라우저/앱) + participant FE as 프론트(SPA/Expo) + participant BE as 백엔드(OIDC RP) + participant KC as Keycloak(IdP) + U->>FE: 로그인 클릭 + FE->>FE: PKCE code_verifier/challenge, state, nonce 생성 + FE->>KC: /authorize (code, PKCE challenge, redirect_uri) + KC->>U: 로그인 폼(+IdP OTP if 위임) + U->>KC: 자격증명(+OTP) + KC->>FE: redirect_uri?code=...&state=... + FE->>BE: POST /api/auth/oidc/callback (code, code_verifier, state) + BE->>KC: token 교환(code + code_verifier + client_secret) + KC->>BE: id_token + access_token (JWT) + BE->>KC: JWKS 공개키(캐시) — id_token 서명·iss·aud·nonce·exp 검증 + BE->>BE: subject(sub)로 매핑 조회 → 로컬 user 해석/JIT 프로비저닝(§2-5) + BE->>BE: event_member 행사역할·hall_manager·tid·rc 조립 + BE->>FE: 앱 JWT(JwtService.issue) + 워크스페이스(기존 LoginResponse 형태) + FE->>FE: 기존 저장키에 앱 JWT 저장 — 이후 전 API는 기존 그대로 +``` + +**핵심:** 5~7단계 산출물 = **현행 `LoginResponse`와 100% 동일**. `/api/auth/oidc/callback`만 신설, 나머지 전 경로 불변. + +### 2-4. 토큰 갱신·로그아웃 시퀀스 + +| 흐름 | 설계 | +|---|---| +| **앱 JWT 갱신** | 현행 TTL 정책 유지. 만료 시 프론트가 **silent OIDC 재인증**(prompt=none, IdP 세션 유효 시 무마찰) → 백엔드가 앱 JWT 재브로커. 리프레시 토큰은 백엔드가 보관(HttpOnly·서버측), 프론트 미노출 | +| **IdP 세션 만료** | silent 재인증 실패 → 로그인 화면. 앱 JWT 짧게, IdP 세션이 마스터 수명 | +| **로그아웃** | 프론트 로컬 토큰 파기 + `/api/auth/oidc/logout` → Keycloak end-session(`id_token_hint`). **back-channel logout**: Keycloak → 백엔드 `/api/auth/oidc/backchannel-logout`(로그아웃 토큰 검증) → Redis 세션상관 무효화(다중 인스턴스 팬아웃) | +| **단일 로그아웃(SLO)** | Keycloak realm SSO 로그아웃으로 연동 클라이언트 전파(웹·모바일·타 GUARDiA 연계 시) | + +### 2-5. 계정 매핑 · JIT 프로비저닝 (SSO subject ↔ 로컬 user) + +```mermaid +graph LR + SUB[OIDC sub
=IdP subject] --> MAP[user_identity 매핑] + EMP[HR empNo
=사번] --> MAP + LU[app_user.id
=로컬 user] --> MAP + MAP --> RESOLVE{로컬 user 존재?} + RESOLVE -->|Yes| LOGIN[역할 조립 → 앱 JWT] + RESOLVE -->|No| JIT[JIT 프로비저닝
app_user upsert + 매핑 생성] + JIT --> LOGIN +``` + +- **매핑 키:** IdP `sub`(불변 식별자) 우선. 보조 매칭키 = 이메일 + HR 사번(empNo, IdP 커스텀 클레임). **이메일 단독 매칭 금지**(재사용·변경 위험) — sub↔empNo 확정 후 email은 표시용. +- **JIT 프로비저닝:** 첫 SSO 로그인 시 매핑 없으면 `app_user`를 **password_hash 없이(SSO 전용 플래그)** 생성 + `user_identity` 매핑 생성. 로컬 폴백 비번은 미설정(SSO 전용 계정). +- **충돌 규칙:** 동일 이메일에 기존 로컬 계정 존재 → **자동 병합 금지, 관리자 수동 링크**(계정 탈취 방지). `link_status`(UNLINKED/LINKED/CONFLICT). +- **역할 소스:** 전역 역할(rc)·hall_manager = IdP 그룹/클레임 매핑(§2-7). **행사 역할(event_member)은 로컬 권위 유지**(IdP는 행사를 모름). + +### 2-6. 기존 JWT+2FA 공존안 (핵심) + +| 관심사 | 공존 규칙 | +|---|---| +| **1차 인증** | Keycloak(OIDC). 성공 후 백엔드가 **기존 `JwtService.issue()`로 앱 JWT 브로커** — 다운스트림 무변경 | +| **로컬 로그인** | `/api/auth/login`·`/login/secure` **유지**(폴백). 관리자·핵심 운영은 IdP 장애 시 로컬 폴백 허용. 일반 사용자는 이행 단계별로 SSO 우선/로컬 차단(§8) | +| **2FA** | IdP realm이 OTP 요구 시 → 앱 `requiresOtp` 위임(앱 OTP 강제 해제, 이중 2FA 방지). IdP OTP 미구성 계정·로컬 폴백 로그인 → **기존 `TwoFactorService` 그대로** | +| **admin 비번** | `ADMIN_PASSWORD_ENC` env 시드 정책 **유지**(IdP 장애 시 최후 관리자 접근) | +| **토큰 신뢰 모드** | 이행기 = **브로커 모드**(앱 JWT). 장기 옵션 = 리소스서버 모드(IdP 토큰 직접, 커스텀 클레임 매퍼로 행사역할 주입) — **행사 RBAC 재검증 부담으로 이행 완료 후 별도 과제**(A3) | + +**공존 상태 판정 로직(개념):** +``` +로그인 요청 + ├─ SSO 경로(/oidc/callback): IdP 검증 성공 → 매핑 해석 → 앱 JWT (2FA는 IdP 위임) + └─ 로컬 경로(/login, /login/secure): 기존 TwoFactorService (단계별 허용범위 §8) +다운스트림(전 API): 기존 JwtAuthenticationFilter가 앱 JWT만 검증 — 경로 무관 동일 +``` + +### 2-7. 멀티테넌트·역할 매핑 + +- **tenant:** realm=단일(`KINTEX`) 또는 realm 그룹 클레임 `tenant`. 앱 `tid` 클레임 = IdP `tenant` 클레임(부재 시 기준 테넌트 `KINTEX` 폴백, 현행 `normalizeTenant` 규칙 준수). +- **역할 매핑표(IdP 그룹/클레임 → 앱 역할):** + +| IdP 그룹/클레임 | 앱 전역 역할(rc) | hall_manager | 트랙 | +|---|---|---|---| +| `/kintex/admin` | ADMIN | - | 내부 백오피스 | +| `/kintex/manager` | MANAGER | - | 내부 운영 | +| `/kintex/hall-manager` | (rc null) | true | 내부 운영(홀) | +| `/kintex/staff` | STAFF | - | 내부 직원 | +| (매핑 없음·외부) | null | false | 행사역할은 event_member로 | + +- **6역할 정합:** 주최자·참가업체·장치/공사업체는 대개 **외부**(HR 대상 아님) → SSO 계정이라도 행사 역할은 `event_member` 초대로만 부여. 관람객/일반대중은 셀프서비스(SSO 대상 아님·별도 트랙 §8 note). + +### 2-8. 웹 + 모바일 플로우 + +| 클라이언트 | 라이브러리 | 리다이렉트 URI | 특이사항 | +|---|---|---|---| +| **웹 SPA(역할별 6종)** | `oidc-client-ts` 또는 백엔드 BFF 콜백 | `https://./auth/callback` | PKCE. 역할별 번들마다 클라이언트 등록(또는 와일드카드 redirect + 역할 라우팅) | +| **모바일(Expo)** | `expo-auth-session`(PKCE 기본) | `kintexai://auth/callback` (앱스킴) + `https:///auth/native-callback`(유니버설 링크 폴백) | 공개 클라이언트(시크릿 없음)=PKCE 필수. B2B 앱=SSO+IdP OTP, B2C 관람객 앱=SSO 대상 아님(셀프서비스 유지) | + +- **클라이언트 등록(Keycloak):** `kintex-web`(confidential, BFF 콜백) / `kintex-mobile`(public, PKCE). redirect_uri·web_origins(CORS)·post_logout_redirect 화이트리스트 등록. +- **모바일 레퍼런스:** WISE 모바일(`workspace/guardia-messenger/app/uiws`) 2FA 로그인 화면 컨벤션 위에 expo-auth-session 브라우저 플로우 삽입(디자인은 Stitch 경유 — MEMORY 준수). + +--- + +## 3. 설계 B — 인사(조직) API 연동 (HrDirectoryClient) + +### 3-1. 채택 결정 (ADR) + +| # | 결정 | 이유 | +|---|---|---| +| B1 | **어댑터 패턴 `HrDirectoryClient` 인터페이스 + 고객 HR 구현** | 고객사별 HR API 상이 → 인터페이스로 격리, 구현만 교체. Mock 구현으로 개발·폴백 | +| B2 | **로컬 읽기 캐시 스냅샷 유지**(HR 미가용 폴백) | HR 장애·야간 배치 실패에도 조직도·직원조회 지속(가용성). HR=정본, 로컬 dept/직원=파생 캐시 | +| B3 | **증분 동기화(배치) + 실시간 조회 혼합** | 조직 트리·직원 마스터는 배치 스냅샷(멱등 upsert), 로그인 순간 신원 확인은 실시간 조회(선택) | +| B4 | **PII 최소 수집·마스킹** | 직원 개인정보 경계(SA §5-3). 필요 필드만 수집, 연락처·주민식별 미수집/마스킹 | + +### 3-2. 어댑터 계약 (인터페이스 · DTO) + +**인터페이스(개념 시그니처 — backend-dev 구현):** + +| 메서드 | 목적 | 동기성 | +|---|---|---| +| `List fetchOrgTree(tenantId, sinceRev?)` | 조직(부서) 트리 — 전체 또는 변경분 | 배치 | +| `Page fetchEmployees(tenantId, sinceRev?, page)` | 직원 마스터 — 전체/증분 페이지 | 배치 | +| `Optional findByEmpNo(tenantId, empNo)` | 로그인·매핑 시 단건 신원 확인 | 실시간(선택) | +| `SyncCursor currentRevision(tenantId)` | 증분 동기화 커서(변경 기준점) | 배치 | + +**표준 DTO(계약·PII 최소):** + +``` +HrDept { empDeptCode, deptName, parentDeptCode, sortOrder, useYn, revision } +HrEmployee { + empNo, // 사번 = 매핑 정본 키 + name, // 표시명 + email, // SSO sub 보조 매칭·표시 + deptCode, // 소속 부서(HrDept.empDeptCode 참조) + positionCode, // 직급(공통코드 POSITION 매핑) + employmentStatus, // 재직상태 ACTIVE|LEAVE|RETIRED + revision // 증분 동기화 기준 + // 미수집: 주민번호·개인 연락처·급여 등 (PII 최소) +} +``` + +- **원격/캐시 이원화:** 서비스 계층은 항상 `HrDirectoryClient`를 부르되, HR 실패 시 `HrSnapshotRepository`(로컬 캐시)로 폴백(`degraded=true` 표기). 소비자(조직도·직원선택 컴포넌트)는 출처를 모름. + +### 3-3. 동기화 전략 + +```mermaid +sequenceDiagram + participant SCH as 스케줄러(배치) + participant HC as HrDirectoryClient + participant HR as 고객 HR API + participant SNAP as 로컬 스냅샷(hr_dept_snapshot·hr_emp_snapshot) + participant DEPT as dept(파생 캐시) + SCH->>HC: fetchOrgTree(sinceRev) / fetchEmployees(sinceRev) + HC->>HR: GET 조직·직원 변경분 + alt HR 정상 + HR->>HC: 변경 레코드 + HC->>SNAP: 멱등 upsert(revision 기준) + SNAP->>DEPT: dept 트리 파생 반영(use_yn·parent 매핑) + else HR 미가용 + HC->>SNAP: 기존 스냅샷 유지(degraded) + Note over DEPT: 조직도·직원조회는 마지막 스냅샷으로 계속 서비스 + end +``` + +- **주기:** 조직 트리·직원 = 야간 전체 리컨실 + 주간(수시) 증분. 로그인 시점 신원은 `findByEmpNo` 실시간(선택, 실패 시 스냅샷). +- **멱등:** `revision`/`empNo`/`empDeptCode` 기준 upsert. 삭제는 **소프트**(useYn='N', employmentStatus=RETIRED) — 물리삭제 금지(감사·과거 참조 보존). +- **정본 전환:** HR 정본화 후 `dept`·직원정보는 **읽기 전용 캐시**. 백오피스 조직 편집 UI는 "HR 마스터 편집 안내"로 전환(직접 수정 차단), 단 로컬 전용 부서(외부 파트너 그룹핑 등)는 `source='LOCAL'` 플래그로 공존 허용. + +### 3-4. 기존 dept/sys_user 매핑 + +| 현행 | HR 연동 후 | +|---|---| +| `dept`(로컬 편집) | `source` 컬럼 추가 → `HR`(캐시, 읽기전용) / `LOCAL`(로컬 전용, 편집가능) 공존. HR 파생행은 배치만 갱신 | +| `sys_user`(=app_user 조회) | 직원 신원(name·dept·position)은 **HR 스냅샷 조인**으로 표시, `app_user`는 로그인/권한 최소필드만 | +| 조직도 화면(WISE 트리) | 데이터 출처만 HR 스냅샷 → 컴포넌트 무변경 | + +--- + +## 4. 3자 매핑 통합 규칙 (SSO subject ↔ HR 사번 ↔ 로컬 user) + +| 키 | 소유 | 역할 | +|---|---|---| +| `sub`(OIDC subject) | Keycloak | 로그인 신원 불변 식별자 | +| `empNo`(사번) | HR API | 직원·조직 정본 키(직급·부서·재직상태) | +| `app_user.id` | 로컬 | 앱 계정·권한(event_member·hall_manager·tid) 앵커 | + +- **연결 테이블 `user_identity`**(§5): (tenant_id, app_user_id) ↔ (idp_sub, emp_no) + link_status. +- **로그인 해석 순서:** `sub` → user_identity 조회 → app_user 해석 → (선택) empNo로 HR 스냅샷 조인(부서·직급 최신화) → 역할 조립 → 앱 JWT. +- **불일치 처리:** sub 있으나 empNo 없음(외부 SSO 사용자) 허용 / empNo 있으나 sub 없음(미로그인 직원) 허용(사전 프로비저닝) / 이메일 충돌 = CONFLICT(관리자 수동 링크). + +--- + +## 5. 데이터 모델 (신설 — DA 정합, 소프트참조 표준) + +> 신규 테이블만. **기존 테이블 변경 최소**(dept에 `source` 컬럼 순증만). 테넌트 표준(V31: tenant_id DEFAULT 'KINTEX' 복합 PK 선두)·소프트참조(FK 남발 금지, 애플리케이션 조인) 준수. 실제 DDL은 db-engineer 인계(§9). + +| 테이블 | 목적 | 핵심 컬럼(개념) | +|---|---|---| +| `user_identity` | 3자 매핑 | (tenant_id, app_user_id) PK, idp_sub UNIQUE, emp_no, link_status, provider, linked_at | +| `hr_dept_snapshot` | HR 조직 캐시 | (tenant_id, emp_dept_code) PK, dept_name, parent_dept_code, sort_order, use_yn, revision, synced_at | +| `hr_emp_snapshot` | HR 직원 캐시 | (tenant_id, emp_no) PK, name, email, dept_code, position_code, employment_status, revision, synced_at | +| `hr_sync_log` | 배치 감사 | (tenant_id, id) PK, kind(DEPT/EMP), rows_upserted, result, degraded, started_at, finished_at | +| `dept`(순증) | 출처 구분 | `source varchar(10) DEFAULT 'LOCAL'`('HR'|'LOCAL') 컬럼 추가(멱등 ALTER) | + +- **PII 경계:** hr_emp_snapshot는 name·email·dept·position·status만. 연락처·식별번호 미보관. email은 표시·매칭용(마스킹은 로그·감사 계층). +- **소프트참조:** emp_no·emp_dept_code·idp_sub는 **소프트 참조**(FK 미설정) — HR/IdP 외부 키를 물리 FK로 묶지 않음(배치 순서·정합 유연성). 공통코드(POSITION·EMPLOYMENT_STATUS)는 common_code 순증. + +--- + +## 6. NFR 정합 (SA system.md §3) + +| NFR | SSO/HR 반영 | +|---|---| +| **확장성** | 백엔드 무상태 유지(OIDC state/nonce는 Redis 외부화). Keycloak는 클러스터 가능. HR 배치는 백엔드와 분리 스케줄(응답성 무영향) | +| **가용성/HA** | **로컬 로그인 폴백**(IdP 장애)·**HR 스냅샷 폴백**(HR 장애) 이중 degraded 모드로 코어 기능 유지. Keycloak 최소 2노드 + DB HA | +| **성능** | JWKS 공개키 캐시(매 요청 IdP 미호출). 로그인만 IdP 왕복, 이후 앱 JWT 로컬 검증(현행 성능 무변경). HR은 배치 오프라인 | +| **보안영역** | Keycloak=내부 애플리케이션 존, `auth.` vhost만 노출. HR/AD=내부망 구간(인터넷 egress 무증가). 토큰·시크릿 env only | +| **용량** | user_identity·hr_*_snapshot는 직원 규모(수백~수천) 소량. hr_sync_log 보존정책(N개월) | + +--- + +## 7. 보안 · 개인정보 (불변 준수) + +- **온프레미스 판정:** Keycloak(자체 IdP)·HR API(고객 내부 인사시스템)는 내부망 통합 → **외부 인터넷 API 금지 규칙 비해당**. NA egress 화이트리스트에 인터넷 목적지 신규 추가 없음. +- **시크릿:** OIDC client_secret·HR API 자격증명·리프레시 토큰 = **env/시크릿 스토어 only**(systemd EnvironmentFile drop-in, `ADMIN_PASSWORD_ENC` 방식 준용). DB·코드·로그·커밋·API응답 미기록. +- **토큰 노출 금지:** id_token/access_token/refresh_token은 응답 본문·로그 미노출. 프론트에는 앱 JWT만(리프레시는 서버 보관). +- **감사:** 계정 링크/언링크·JIT 프로비저닝·HR 배치·권한 매핑 변경 = `TB_AUDIT_LOG` 전수. 로그인 성공/실패는 기존 `login_history`(이메일/IP 마스킹) 재사용. +- **PII 최소화:** HR 수집 필드 최소, 삭제권·재직상태 반영(RETIRED 소프트), 보존정책 DA 확정. +- **검증:** id_token 서명(JWKS)·iss·aud·exp·nonce, code 교환 시 code_verifier(PKCE), state(CSRF), back-channel logout token 검증 필수. + +--- + +## 8. 이행 계획 (coexist → pilot → cutover → 롤백) + +| 단계 | 범위 | 로컬 로그인 | 완료 기준(게이트) | 롤백 | +|---|---|---|---|---| +| **0. 준비** | Keycloak 배치(realm/클라이언트)·매핑 테이블·HrDirectoryClient(Mock)·백엔드 `/oidc/*` 엔드포인트(피처플래그 `sso.enabled=false`) | 전면 허용(현행) | staging OIDC 왕복·앱 JWT 브로커·회귀 0 | 플래그 off = 완전 현행 | +| **1. coexist** | SSO 로그인 **옵션** 노출(로그인 화면 "SSO로 로그인" 병행). JIT 프로비저닝·수동 링크 UI | 전면 허용 | 파일럿 계정 SSO 로그인·역할 정합·2FA 위임 검증 | SSO 버튼 숨김 | +| **2. pilot** | **1개 부서(내부 직원)** SSO 우선 + HR 배치 실연동(스냅샷→dept). 로컬은 폴백만 | 파일럿 외 허용, 파일럿은 폴백 | HR 스냅샷 정합·조직도 HR 출처·degraded 폴백 동작 | 파일럿 부서 로컬 복귀 | +| **3. cutover** | 전 내부 직원 SSO 필수(로컬은 관리자·비상만). HR 정본화(dept 읽기전용) | 관리자·비상만 | 전 직원 SSO·HR 정본·이중 2FA 없음·감사 완비 | IdP 장애 시 로컬 폴백 자동 | +| **4. 정착** | 로컬 비번 로그인 축소(SSO 전용 계정 비번 미발급). 리소스서버 모드 전환은 별도 과제(A3) | 최소(비상 관리자) | 운영 안정·SLO 충족 | — | + +- **무중단 원칙:** 각 단계는 **피처플래그**로 제어, 언제든 이전 단계 복귀. 다운스트림(앱 JWT 소비)은 전 단계 불변 → 화면·API 회귀 0이 롤백 안전판. +- **관람객/외부(B2C):** SSO 이행 대상 아님 — 셀프서비스(간편가입·게스트) 트랙 유지. 본 이행은 **내부 직원·조직** 한정. + +--- + +## 9. 인계 백로그 (트랙별) + +> 본 문서는 설계 정본. 아래는 각 dev 트랙 착수 항목. **모두 피처플래그 뒤에서 순증 구현**(기존 인증코드 미파괴). + +| 트랙 | 항목 | 산출 | +|---|---|---| +| **backend-dev** | ① OIDC RP: `/api/auth/oidc/authorize-info·callback·logout·backchannel-logout`, JWKS 캐시·id_token 검증 ② 콜백 성공 후 **기존 `JwtService.issue()` 재사용**한 앱 JWT 브로커 ③ `HrDirectoryClient` 인터페이스 + Mock 구현 + 고객 어댑터 스텁 ④ 매핑 해석·JIT 프로비저닝·수동 링크 서비스 | 신규 패키지 `auth/oidc`, `hr/` (기존 `auth`·`security` 불변) | +| **common-dev(인증 레이어)** | ① 로그인 화면 SSO 버튼(피처플래그)·expo-auth-session(모바일) ② `TwoFactorService.requiresOtp` IdP 위임 분기(설정형) ③ 계정 링크/언링크 마이페이지·관리자 링크 UI | 인증 공통 레이어 | +| **db-engineer** | ① `user_identity`·`hr_dept_snapshot`·`hr_emp_snapshot`·`hr_sync_log` DDL(V38+, 멱등·테넌트 표준·소프트참조) ② `dept.source` 순증 ALTER ③ common_code POSITION·EMPLOYMENT_STATUS 순증 | Flyway 마이그레이션 | +| **mobile** | expo-auth-session PKCE 플로우 + B2B 앱 SSO/IdP OTP, WISE 모바일 로그인 컨벤션 위 삽입, 디자인 Stitch 경유 | `mobile/` 로그인 | +| **devops** | ① Keycloak 배치(systemd/컨테이너·별도 DB·realm import) ② `auth.` nginx vhost·TLS·접근제한 ③ client_secret·HR 자격증명 env drop-in(시크릿 스토어) ④ HR 배치 스케줄 | 인프라(G2 연계) | +| **planner(인계)** | PLANNING §5B/§8에 **Open SSO·HR API 연동** 반영(6역할·인증 스택·조직 마스터 출처 갱신). 관람객 SSO 제외 명시 | PLANNING 개정 | +| **NA/DA(정합)** | NA: `auth.` vhost·IdP↔AD/HR 내부망 구간 존 반영 / DA: hr_*_snapshot·user_identity ERD·PII 보존정책 편입 | network.md·data.md 갱신 | + +--- + +## 10. 리스크 + +| # | 리스크 | 완화 | +|---|---|---| +| R1 | IdP 단일 장애 → 전면 로그인 불가 | 로컬 로그인 폴백 유지(§2-6)·Keycloak HA·앱 JWT 수명으로 순간 장애 흡수 | +| R2 | 이메일 재사용·계정 병합 오류 → 권한 탈취 | sub↔empNo 확정 매핑, 이메일 단독 병합 금지, CONFLICT 수동 링크·감사 | +| R3 | HR API 스펙 상이·미제공 | `HrDirectoryClient` 어댑터로 격리, Mock·스냅샷 폴백으로 개발·운영 지속 | +| R4 | 이중 2FA(IdP+앱) 마찰 | IdP OTP 위임 시 앱 OTP 강제 해제(설정형 분기) | +| R5 | 행사 RBAC 클레임 누락(리소스서버 직접 신뢰 시) | 이행기 브로커 모드 고정(앱 JWT에 event_member 조립). 직접 신뢰는 정착 후 별도 과제 | +| R6 | HR 정본 전환 후 로컬 조직 편집 상실 | `dept.source=LOCAL` 공존 허용(외부 파트너 그룹핑 등 로컬 전용 유지) | +| R7 | 토큰/시크릿 노출 | env only·서버측 리프레시 보관·응답/로그 미노출·JWKS 검증 | + +--- + +## 11. 변경 이력 + +| 버전 | 일자 | 작성자 | 내용 | +|---|---|---|---| +| v1.0 | 2026-07-12 | SA | 최초 작성 — Open SSO(OIDC/Keycloak, PKCE, 브로커드 앱JWT 공존, 로컬 폴백, 2FA 위임, 6역할/멀티테넌트 매핑, 웹+모바일) + 인사·조직 API 연동(HrDirectoryClient 어댑터·스냅샷 폴백·증분 동기화·PII 최소) + 3자 매핑(sub↔empNo↔app_user) + 데이터 모델·NFR·보안·이행(coexist→pilot→cutover→롤백)·인계 백로그·리스크. 기존 W12 인증(app_user·event_member·TotpService·JwtService) 미수정 공존 설계. system.md/app.md/network.md/data.md 교차참조 | diff --git a/docs/architecture/system.md b/docs/architecture/system.md index c2f0829..0d39657 100644 --- a/docs/architecture/system.md +++ b/docs/architecture/system.md @@ -299,6 +299,7 @@ graph TB - 역할별 프론트는 동일 SSO·API 계약을 상속하되 **번들·도메인 분리**. 백오피스(admin.)는 내부 전용·2FA 필수(§5-1). - 등록업체 계정은 **M7 등록업체 DB 검증** 통과분만 초대·옥션 응찰. 관람객·일반대중은 셀프서비스(쓰기 제한). - 인증 상세 계약은 [`app.md`](app.md)(AA)·`../DEVELOPMENT_GUIDE.md` §4. +- **Open SSO(OIDC/Keycloak) 도입 + 인사·조직 API 연동**은 별도 정본 [`sso-hr-integration.md`](sso-hr-integration.md)(SA) — 기존 JWT+2FA를 삭제 없이 **공존(SSO 1차 인증→앱 JWT 브로커, 로컬 폴백)**, 직원·조직은 `HrDirectoryClient` 어댑터로 조달(로컬 스냅샷 폴백). 관람객/외부는 SSO 제외(셀프서비스 유지). ### 6-2. 외부 게이트 (아웃바운드 통제) @@ -339,6 +340,7 @@ graph TB - 기획 정본: [`../PLANNING.md`](../PLANNING.md) §2·§7·§8·§10 - 구현 백로그: [`../IMPLEMENTATION_BACKLOG.md`](../IMPLEMENTATION_BACKLOG.md) Phase A~E +- **Open SSO · 인사(조직) API 연동(SA)**: [`sso-hr-integration.md`](sso-hr-integration.md) — OIDC/Keycloak·HrDirectoryClient·3자 매핑·이행(coexist→cutover) - 나노바나나 파이프라인 근거: [`../analysis/reroomai-source.md`](../analysis/reroomai-source.md) - 애플리케이션 아키텍처(AA·A-1): [`app.md`](app.md) — 모듈 경계·API 표준·패키지 - 기술 표준(TA·A-3): [`tech.md`](tech.md) — 빌드·배포·관측성·AiTextRouter diff --git a/docs/design.md b/docs/design.md index a41e791..12f08bb 100644 --- a/docs/design.md +++ b/docs/design.md @@ -1,6 +1,6 @@ # 킨텍스 AI 전시관리 시스템 — UI 디자인 스펙 (design.md) -> 작성: 디자인 에이전트(designer) · 작성일: 2026-07-12 · 버전: **v2.2** +> 작성: 디자인 에이전트(designer) · 작성일: 2026-07-12 · 버전: **v2.3.2** > 근거 문서: `docs/PLANNING.md` **v3.0**(모듈 맵 M1~M18 + §5B 공통/시스템관리 레이어 + §1A 멀티테넌시, 6+2 역할, 나노바나나 표준 샷 세트 S1~S7) · `docs/FEATURE_BACKLOG_100.md`(100대 기능 F001~F100·NFR 4축) · Stitch 산출물 `stitch_kintex_ai_system_architect/`(18화면 + DESIGN.md 3변형) 학습·정합 > **단일 출처(Single Source of Truth)**: 프론트 구현 토큰·컴포넌트 계약은 본 문서가 권위다. Stitch 화면(code.html/screen.png)은 시각 참조로만 사용하며, 상충 시 본 문서를 따른다. > 용도: **각 화면의 "Stitch 프롬프트" 코드블록을 Google Stitch(stitch.withgoogle.com)에 그대로 붙여넣어 화면을 생성**한다. 문서는 한국어, Stitch 프롬프트만 영어. @@ -418,7 +418,7 @@ An enterprise MDI (multiple document interface) application shell for "KINTEX AI | SCR | 화면명 | 모듈 | 역할 | 라우트 | 상태 | |---|---|---|---|---|---| | SCR-T0 | 트랙 관문 랜딩(3-트랙 분기) | 공통·M12 | 대 | `/` | 🔲미생성 | -| SCR-T1 | visitor 서브홈(가이드 허브) | M10·M13·M12 | 대 | `/visitor` | 🔲미생성 | +| SCR-T1 | visitor 메인(공개 홈·AI 관람 도우미) | M10·M11·M13·M12·AI | 대 | `/visitor` | 🔲미생성 | | SCR-T2 | business 서브홈(비즈니스 진입) | M1·M3·M4·M6 | 대 | `/business` | 🔲미생성 | | SCR-T3 | agency 서브홈(등록업체·옥션) | M15·안전경영 | 협·대 | `/agency` | 🔲미생성 | @@ -1494,23 +1494,56 @@ A public "check & cancel my ticket booking" page for an exhibition, Korean marke A public landing "track gateway" home page for the product "KINTEX AI 전시·행사시스템" (an AI exhibition & event operations platform), Korean, desktop 1440px, light theme, primary blue #0066B3, purple #6D4AFF for AI accents, clean modern marketing design (not an MDI app shell). All UI text in Korean; use line (stroke) SVG icons only, NO emoji. Top: a public header with a "KINTEX AI 전시·행사시스템" wordmark on the left, a 3-tab track switcher "관람 · 비즈니스 · 협력사" (none active on this gateway), and right utilities "언어(한/영/중/일) · 로그인". Full-bleed hero with a photorealistic exhibition-hall render as background (with a faint watermark "AI 생성 예상 이미지" and a dim overlay for text contrast), a large headline "KINTEX AI 전시·행사시스템", a subheading "전시 기획부터 시공·관람까지 전 과정을 AI로 자동화", and two CTAs "관람 사전등록" and "참가 문의". Directly below the hero: three large branching track cards in one row — card 1 "관람객" (slightly larger, primary) with a stroke icon, text "행사 일정 · 사전등록 · 오시는 길" and a button "관람 트랙 →"; card 2 "주최 · 참가 (비즈니스)" with text "대관 · 자동견적 · 부스 신청 · 시공 시안" and a button "비즈니스 트랙 →"; card 3 "공사 · 협력사 (에이전시)" with text "등록업체 · 공사 옥션 · 작업신고" and a button "협력사 트랙 →". Below: a horizontal scrolling rail of event poster cards (poster, title, dates, hall, a "D-25" day chip, "사전등록" button) titled "진행 중 · 예정 전시". Footer with company info and "오시는 길 · GTX-A 킨텍스역". Spacious, multilingual, conversion-oriented gateway aesthetic; rounded 8px cards, WCAG AA contrast. ``` -### SCR-T1 visitor 서브홈 (관람객 공개 홈 · 가이드 허브) [visitor · 공개 · `/visitor`] +### SCR-T1 visitor 메인 (관람객 공개 홈 · AI 관람 도우미 중심 · 세련된 마케팅 홈페이지) [visitor · 공개 · `/visitor`] -- **대상**: 일반 대중·관람객(게스트+회원) · **모듈/P**: M10(사전등록·티켓)·M13(실내 길찾기)·M12(공지) · P1 · **진입 경로**: 관문 SCR-T0 "관람 트랙" · TrackSwitcher "관람" · 직접 `/visitor` -- **레이아웃**(위→아래, PublicShell·SCR-P1 확장): - 1. **PublicShell 헤더 + TrackSwitcher**("관람" 활성) - 2. **관람 히어로 + 주 CTA**: 관람객 톤 히어로 + "사전등록"·"입장권 예매"(→ SCR-P4·P7) 이중 CTA + 맞춤 알림 유도("관심 분야 알림 받기") - 3. **행사 일정 그리드**(SCR-P1 행사 카드 재사용): 진행 중·예정 행사 카드 그리드(포스터·행사명·기간·홀·상태 배지·"사전등록")+전시장 세그먼트 필터(전체/홀, 동적 옵션) - 4. **방문 가이드 허브**(코엑스 가이드 차용 — 2×3 카드): ① 교통(GTX-A 킨텍스역 도보 3분·광역버스) ② 주차(iparking 실시간·요금·게이트) ③ **실내 길찾기**(M13 wayfinding 딥링크) ④ 편의시설(식음·수유실·물품보관·ATM) ⑤ 접근성·편의(휠체어·수어통역 고지) ⑥ 고객문의(FAQ·연락처). 각 카드 선 SVG 아이콘+1줄 설명+링크 - 5. **공지·알림마당**: 최근 공지 5건 리스트(§5B notice 재사용)+더보기 - 6. **PublicShell 푸터** -- **구성요소**: HeroBanner·EventGrid(P1 카드)·GuideHubCard×6·NoticeList·LanguageSelector·CTA(사전등록·티켓·알림) -- **데이터 원천**: 행사 그리드 = `/api/public/events`(SCR-P1 자산 재사용) · 사전등록/티켓 CTA = SCR-P4/P7 딥링크 · 실내 길찾기 = M13 wayfinding(SCR-M9 계열) 딥링크 · 공지 = §5B notice `/work/notice` 공개 서브셋 → 실 API 재사용, 가이드 정적 콘텐츠(교통·주차·편의 안내)는 CMS/정적. **SEO·다국어 적용, MDI 비적용** -- **상태**: 로딩(행사·공지 스켈레톤) · 빈(행사 없음 "예정된 전시가 없습니다"·공지 없음) · 에러(영역별 재시도, 가이드 허브는 정적이라 상시 노출) -- **접근성**: 가이드 카드 링크 텍스트 명시, 접근성 안내 카드 자체가 WCAG AA·수어/휠체어 정보 제공, 색 외 라벨 병기. **i18n 네임스페이스 `track.visitor.*`**(+ 공유 `track.shell.*`) +> **v2.3 격상.** 기존 v2.2 "visitor 서브홈(가이드 허브)"을 **일반 대중 대상 세련된 공개 마케팅 홈페이지**로 격상하고, **화면 중심 요소를 대화형 "AI 관람 도우미"로 재구성**한다(소유자 지시 2026-07-12: "비지터가 가장 알고 싶어하는 정보를 AI로 쉽게"). business(SCR-T2)·agency(SCR-T3) 메인은 여전히 **B2B 업무 진입 톤**이며, 본 화면만 코엑스/글로벌 전시장 홈페이지 수준의 마케팅 톤(풀블리드 히어로·타이포 위계·공백 없는 밀도·전환 지향)으로 차별화한다. 기존 §3C 공개 셸(PublicShell)·TrackSwitcher·라우트(`/visitor`)·데이터 재사용 원칙·신규 토큰 0은 유지한다. + +- **대상**: 일반 대중·관람객(게스트+회원, 미인증 기본 랜딩) · **모듈/P**: M10(사전등록·티켓)·M11(비즈매칭·세션)·M13(실내 길찾기)·M12(공지·홍보)·**AI 라우터(Claude→Ollama 폴백, 공개 행사 데이터)** · P1 · **진입 경로**: 미인증 기본(§2-3 순서 5 `resolveLandingTrack→visitor`)·관문 SCR-T0 "관람 트랙"·TrackSwitcher "관람"·직접 `/visitor`·검색/광고/QR +- **화면 목표(관람객 관심 우선순위)**: ① 지금/곧 어떤 전시 + 관심사 맞춤 추천 ② 가는 길·주차·소요시간 ③ 입장 방법(사전등록·입장권·무료/유료) ④ 부스 위치·볼거리·이벤트(wayfinding) ⑤ 편의시설·접근성. 이 5가지를 **AI 관람 도우미 하나의 자연어 입력으로 관통**시키고, 스크롤 하위 섹션은 동일 정보를 시각적으로 재확인시킨다. +- **레이아웃**(위→아래, PublicShell·풀블리드 마케팅 밀도·반응형 풀스크린): + 1. **PublicShell 헤더 + TrackSwitcher**("관람" 활성·`aria-current`) + 우측 유틸(언어 한/영/중/일·**라이트/다크 테마 토글**·`로그인`·인증 시 `내 홈`) + 2. **풀블리드 히어로 + AI 관람 도우미(중심 요소)**: + - 배경 = 대형 전시장 전경 비주얼(나노바나나 AI 생성, 워터마크 "AI 생성 예상 이미지" 상시·텍스트 대비용 딤 오버레이). 헤드라인 "**무엇이든 물어보세요 — AI가 관람을 안내합니다**"(H1/Display) + 서브카피 "전시 일정·오시는 길·입장권·부스 위치를 대화로 한 번에". + - **AI 관람 도우미 대화 입력창(히어로 정중앙·대형)**: 폭 넓은 자연어 질문 인풋(placeholder 로테이션 "이번 주말 로봇 전시 있어?" → "킨텍스 주차 어디에 해요?" → "가족과 볼만한 전시 추천") + 우측 전송 버튼(선 SVG send 아이콘) + 좌측 AiLabel(보라 `ai-accent` "AI 도우미"). 인풋 하단 **예시 질문칩(자주 묻는 것)** 5개: `이번 주말 로봇 전시 있어?` · `킨텍스 주차 어디에 해요?` · `가족과 볼만한 전시 추천` · `사전등록 어떻게 해요?` · `GTX-A로 얼마나 걸려요?`(칩 클릭=즉시 질의). + - **AI 답변 패널(질의 시 인풋 아래 확장·대화형)**: 답변 텍스트 + **구조화 결과 카드**(매칭 전시=포스터·행사명·기간·홀·D-day·"사전등록"/교통=경로·소요시간·주차 카드/입장=사전등록·입장권 CTA/부스·wayfinding 딥링크) + **후속 질문칩**("무료인가요?"·"길찾기 열기"·"티켓 예매") + 하단 고지문 "AI 생성 답변 — 정확한 정보는 공식 안내를 확인하세요"(제거 불가) + 근거 출처 링크. AI 산출물은 §1-4 AiLabel(보라)로 구분. + - 보조 CTA(인풋 하단·항상 노출): "사전등록"·"입장권 예매"(→ SCR-P4·P7). + 3. **AI 기능 쇼케이스 타일(히어로 직하·AI 전면 노출 — 소유자 지시 2026-07-12)**: "이 시스템의 AI가 해주는 것"을 관람객이 한눈에 알도록 **AI 능력 카드 그리드**(4~6타일, 각 카드 좌상단 AiLabel 보라·선 SVG 아이콘·제목·1줄 설명·바로가기). 대표 타일: ① **AI 관람 도우미**(자연어 질문·위 히어로 입력창으로 스크롤/포커스) ② **AI 전시 추천**(관심사 맞춤 큐레이션→§4 추천 레일) ③ **AI 길찾기**(실내 wayfinding·최적 동선, M13 딥링크) ④ **AI 일정 도우미**(관심 세션·동선 기반 관람 코스 자동 구성, M11) ⑤ **AI 혼잡·대기 안내**(시간대별 관람 추천, degraded 시 정적 안내) ⑥ **AI 시공 예상 이미지**(나노바나나 — 참가·주최 대상 능력 홍보, "비즈니스 트랙에서" 안내). AI가 시스템 전면에 있음을 시각적으로 각인시키는 것이 목적(밀도 유지·공백 금지). + 4. **AI 전시·행사 기획 브리핑(연간·월간 선제 제시 — 소유자 지시 2026-07-12)**: 단발 질의응답을 넘어 **AI가 개최 예정 전시·행사를 먼저 큐레이션**하는 섹션. 상단에 **연/월 관점 토글** — ① `연간(올해)` = 12개월 **연간 타임라인**(월별 레인 위 전시·행사 바/도트, 카테고리 색·규모 표시, 성수기 하이라이트) + 좌상단 **AiLabel 보라 "AI 요약"** 헤드라인(예: "2026년은 상반기 스마트팩토리·로봇, 하반기 헬스케어·친환경 전시가 축을 이룹니다") + 분기별 핵심 3~5건 카드 ② `월간(월 선택)` = 선택 월 캘린더/리스트(해당 월 전시·행사 카드·개막일·홀·D-day) + **AI 월간 요약**(예: "이번 달은 로봇·헬스케어 전시가 집중됩니다 · 주말 방문 추천"). 연/월 전환·이전/다음 이동 시 AI가 해당 기간 데이터를 근거로 요약·추천 갱신. **중복 회피**: 하단 "진행 중/이번 달/다음 달" 쇼케이스(레이아웃 5)와 달리 본 섹션은 **연간 기획 관점(연 타임라인 + 기간 요약)**이 차별점이며, 월간 뷰는 쇼케이스 카드 데이터와 정합(동일 event 소스, 표현만 캘린더/요약). + 5. **진행 중·예정 전시 쇼케이스**(SCR-HOME 쇼케이스 레일 패턴 재사용·시각적으로 풍부): 세로 2:3 포스터 카드 그리드/캐러셀 8~12장(포스터·행사명·기간·`hall_label`·상태 배지·D-day·"사전등록") + 탭 `진행 중 | 이번 달 | 다음 달` + 전시장 세그먼트 필터(전체/제1·제2전시장, 동적 옵션). `poster_url` 부재 시 그라디언트+행사명 폴백. + 6. **관심사 맞춤 추천 레일**(M11·AI): "당신의 관심사에 맞는 전시"(관심 분야 칩 선택→AI 추천, AiLabel) — 미선택 시 인기·추천 큐레이션. 관심 분야 알림 유도("관심 분야 알림 받기"). + 7. **방문 가이드 허브**(코엑스 가이드 차용 — 2×3 아이콘 카드, 선 SVG): ① 교통(GTX-A 킨텍스역 도보 3분·광역버스·소요시간) ② 주차(iparking 실시간·요금·게이트) ③ **실내 길찾기**(M13 wayfinding 딥링크) ④ 편의시설(식음·수유실·물품보관·ATM) ⑤ 접근성·편의(휠체어·수어통역 고지) ⑥ 고객문의(FAQ·연락처). + 8. **관람객 여정 안내(3-스텝)**: `사전등록 → 배지/QR 발급 → 현장 체크인` 가로 스텝(각 1줄 설명·해당 딥링크 SCR-P4·M5·SCR-31), 티켓→체크인→배지 흐름 시각화. + 9. **공지·뉴스**: 최근 공지/보도 5건 리스트(§5B notice 공개 서브셋 재사용)+더보기. + 10. **전시장 소개**: 킨텍스 규모·홀 규격·부대시설 요약 밴드(+공개 홍보 SCR-P2 링크). + 11. **PublicShell 푸터**: 회사정보·오시는 길(GTX-A 킨텍스역)·SNS·고객문의·정책·언어. +- **구성요소**: AiVisitorAssistant(대화 입력·예시칩·답변 패널·AiLabel)·AiCapabilityTiles(AI 기능 쇼케이스 4~6타일)·**AiPlanningBriefing(연/월 토글·연간 타임라인·월간 캘린더/리스트·AI 요약 태그)**·HeroBanner(AI 워터마크)·EventShowcaseRail(포스터 카드)·RecommendRail(관심사·AI)·GuideHubCard×6·JourneyStepper(3-스텝)·NoticeList·VenueIntroBand·TrackSwitcher·LanguageSelector·ThemeToggle·CTA(사전등록·티켓·알림) +- **데이터 원천 (★근거 = 사전 크롤 적재 온프레미스 DB — 실시간 외부 크롤 아님, 소유자 지시 2026-07-12)**: + - **AI 관람 도우미** = **신규 공개 엔드포인트 제안 `POST /api/public/ai/visitor-assistant`**(질의 body·비인증·CORS·레이트리밋·PII 미수집; 응답 = 답변 텍스트 + 구조화 결과 참조(eventId·가이드 키·CTA 딥링크) + **근거 출처(DB 레코드 id·출처 라벨)**). 백엔드는 **기존 AiTextRouter(Claude→Ollama 폴백)** 가 **미리 크롤해 DB에 적재한 공개 데이터만을 컨텍스트/근거로 RAG 답변**한다(실시간 외부 호출 없음·온프레미스·근거 인용·환각 방지 §1-4). 답변 근거 DB 테이블(예시 계약): 전시=`exhibition`/`event`(크롤 34건+, `poster_url`·기간·홀·카테고리), 교통·오시는 길=`transport_info`(경로·수단·소요시간), 주차=`parking_info`(위치·요금·게이트·실시간 여부 플래그), 입장·티켓=`ticket_policy`/M10, 편의시설=`amenity_info`, 방문 가이드/FAQ=`visitor_guide`, 부스·wayfinding=`booth`/`floorplan`. **AI는 이 테이블 밖 지식으로 답하지 않으며(범위 밖=degraded 안내), 각 답변에 참조 DB 레코드를 인용**. **계약은 제안 — 백엔드 후속(공개 조회 API가 크롤 적재 DB에서 서빙).** + - **AI 전시·행사 기획 브리핑(연간·월간)** = **신규 공개 엔드포인트 제안 `GET /api/public/events/planning?view=year|month&year=YYYY&month=MM`** — **크롤 적재 `exhibition`/`event` 테이블을 개최연·월 기준 그룹 조회**(기간·`poster_url`·카테고리·홀). **AI 기간 요약**(연간/월간 헤드라인)은 **AiTextRouter(Claude→Ollama 폴백)** 가 **해당 기간 DB 데이터만을 근거로 요약·추천**(외부 실시간 호출 없음·근거 인용·§1-4 AiLabel). 요약은 캐시(기간별)로 서빙. **계약은 제안 — 백엔드 후속(DB 서빙).** 월간 뷰는 쇼케이스(레이아웃 5)와 동일 event 소스로 정합. + - 전시 쇼케이스·추천 = `/api/public/events` ← **크롤 적재 `exhibition`/`event` 테이블**(`poster_url`, SCR-P1/HOME 자산 재사용) · AI 기능 타일 = 정적 구성(AI 능력 메타)+각 기능 딥링크 · 사전등록/티켓 = SCR-P4/P7 딥링크(M10 `ticket_policy`) · 실내 길찾기 = M13 wayfinding(SCR-M6/M9 계열, `floorplan`) 딥링크 · 방문 가이드 = **크롤 적재 `visitor_guide`/`transport_info`/`parking_info`/`amenity_info`**(정적 폴백) · 공지·뉴스 = §5B notice `/work/notice` 공개 서브셋(+크롤 보도자료) · 전시장 소개 = CMS/정적(M17). **모든 공개 조회는 DB 서빙(외부 실시간 크롤 없음)·SEO·다국어 적용, MDI 비적용** +- **상태**: + - **AI 도우미** — 대기(placeholder 로테이션·예시칩) · 생각 중(타이핑 인디케이터·"답변 생성 중…") · 성공(답변+결과 카드+**근거 DB 출처 인용**) · **degraded(AI 라우터 미가용 → "AI 도우미 점검 중 — 크롤 적재 데이터 검색으로 안내" 폴백: 질의어로 DB(`exhibition`/`visitor_guide` 등) 직접 검색+가이드 링크 노출)** · 에러(재시도) · 빈 결과("해당 정보를 찾지 못했어요 — 전체 일정/가이드 보기", DB 근거 범위 밖 질의는 "공식 안내에서 확인" 유도) + - **AI 기능 타일** — 정적 상시 노출(로딩 없음) · 각 타일 대상 기능 미가용 시 해당 타일 "준비 중" 배지(예: 나노바나나 시공 이미지=비즈니스 트랙 안내) + - **AI 기획 브리핑** — 로딩(타임라인/캘린더 스켈레톤·"AI 요약 생성 중…") · 성공(기간 데이터+AI 요약 태그) · **degraded(AI 요약 미가용 → 요약 태그 숨김, 기간 전시·행사 데이터는 DB로 정상 노출)** · 빈(해당 연/월 개최 없음 "이 기간 예정된 전시가 없습니다") · 에러(재시도) · 연/월 전환 시 부분 로딩 + - **쇼케이스/추천** — 로딩 스켈레톤 · 빈("예정된 전시가 없습니다") · 에러(영역별 재시도) · 관심사 미선택(인기 큐레이션) + - **공지/가이드** — 로딩 스켈레톤 · 빈 · 가이드 허브·전시장 소개는 크롤 적재 DB(`visitor_guide` 등)/정적이라 상시 노출 +- **접근성**: AI 입력창 `role="search"`+명시 라벨·예시칩 버튼 명확 텍스트, 답변 패널 `aria-live="polite"`(스트리밍 답변 스크린리더 고지), 답변 내 결과 카드 링크 텍스트 명시, AI 라벨 색 외 텍스트 병기, 히어로 배경 위 헤드라인 대비 확보(딤 오버레이), 가이드·접근성 카드 자체가 WCAG AA·수어/휠체어 정보 제공, 테마 토글 키보드 조작·포커스 링 `primary-600` 2px. 색만으로 상태 전달 금지. **라이트/다크 양 테마 모두 WCAG AA**. **i18n 네임스페이스 `track.visitor.*`**(+ 공유 `track.shell.*`) +- **테마 주의**: 본 공개 마케팅 홈페이지는 소유자 지시로 **라이트/다크 테마 토글을 제공**한다(§1-5의 업무 셸 라이트 단일 원칙에 대한 **공개 트랙 마케팅 예외**). 색은 §1 토큰을 CSS 변수로 주입(하드코딩 hex 금지)해 양 테마 대응, 라이트가 기본. - **Stitch 프롬프트**: ``` -A public visitor sub-home / visit-guide hub page for the product "KINTEX AI 전시·행사시스템", visitor track, Korean, desktop 1440px, light theme, primary blue #0066B3, clean marketing design (not MDI). All UI text in Korean; line (stroke) SVG icons only, NO emoji. Top: public header with "KINTEX AI 전시·행사시스템" wordmark, a 3-tab track switcher "관람 · 비즈니스 · 협력사" with "관람" active (underlined, aria-current), and right utilities "언어 · 로그인". A visitor-toned hero with headline "전시 관람, 여기서 시작하세요", dual CTAs "사전등록" and "입장권 예매", and a subtle "관심 분야 알림 받기" prompt. Below: an event schedule grid of cards (poster, title, dates "2026.09.05–09.08", hall, a status badge, "사전등록" button) with a venue segment filter "전체 · 제1전시장 · 제2전시장". Then a "방문 가이드" hub section as a 2×3 grid of cards, each with a stroke icon, title and one-line text and a link: "교통 · GTX-A 킨텍스역 도보 3분", "주차 · 실시간 주차 안내", "실내 길찾기", "편의시설 · 식음·수유실·물품보관", "접근성 · 휠체어·수어통역 안내", "고객문의 · FAQ·연락처". Then a "공지사항" list of 5 recent notices with a "더보기" link. Footer. Helpful, welcoming, accessible public visitor-guide aesthetic; rounded 8px cards, WCAG AA contrast. +A premium exhibition-center marketing homepage for the product "KINTEX AI 전시·행사시스템" (an AI exhibition & event operations platform), visitor track, for the general public. Korean, desktop 1440px, full-bleed responsive layout, modern conversion-oriented marketing aesthetic (NOT an MDI business app shell). Primary blue #0066B3, purple #6D4AFF for AI accents; support BOTH light and dark theme (show LIGHT as default). All UI text in Korean exactly as given; use line (stroke) SVG icons only, NO emoji. Product name must read exactly "KINTEX AI 전시·행사시스템". + +Top: a clean public header with the "KINTEX AI 전시·행사시스템" wordmark on the left, a 3-tab track switcher "관람 · 비즈니스 · 협력사" with "관람" active (underlined, aria-current), and right utilities "언어(한/영/중/일) · 라이트/다크 토글 · 로그인". + +HERO (full-bleed, the centerpiece): a large photorealistic exhibition-hall panorama as background (with a faint watermark "AI 생성 예상 이미지" and a dim overlay for text contrast). Centered over it: a big headline "무엇이든 물어보세요 — AI가 관람을 안내합니다" and a subheading "전시 일정 · 오시는 길 · 입장권 · 부스 위치를 대화로 한 번에". The visual focus is a large conversational AI ASSISTANT search bar in the center: a wide natural-language question input with a rotating placeholder "이번 주말 로봇 전시 있어?", a small purple "AI 도우미" label on the left and a send (paper-plane) icon button on the right. Directly below the input, a row of example question chips: "이번 주말 로봇 전시 있어?", "킨텍스 주차 어디에 해요?", "가족과 볼만한 전시 추천", "사전등록 어떻게 해요?", "GTX-A로 얼마나 걸려요?". Show one AI answer panel expanded below the input in a chat style: an answer paragraph with a purple "AI" tag, structured result cards (a matched exhibition poster card with title/dates/hall/"사전등록", a transport card "GTX-A 킨텍스역 도보 3분 · 소요 약 40분", an entry card "무료 · 사전등록 권장"), follow-up chips "무료인가요? · 길찾기 열기 · 티켓 예매", and a small disclaimer "AI 생성 답변 — 정확한 정보는 공식 안내를 확인하세요". Under the hero, two secondary CTAs "사전등록" and "입장권 예매". + +Immediately below the hero, prominently, an "AI가 도와드립니다" capability showcase — a row/grid of 4 to 6 cards that advertise what THIS system's AI does, each with a small purple "AI" tag, a stroke SVG icon, a title and one line: "AI 관람 도우미 · 무엇이든 물어보세요", "AI 전시 추천 · 관심사 맞춤 큐레이션", "AI 길찾기 · 실내 최적 동선", "AI 일정 도우미 · 나만의 관람 코스", "AI 혼잡·대기 안내 · 여유로운 시간 추천", "AI 시공 예상 이미지 · 부스 미리보기". This section must make it obvious at a glance that the platform is AI-powered. + +Next, an "AI 전시·행사 기획 브리핑" section where the AI proactively curates upcoming exhibitions by period. It has a view toggle at top: "연간 (올해)" and "월간" (with a month picker "2026년 3월 ◂ ▸"). In the "연간" view, show a 12-month annual timeline (a horizontal row of month lanes Jan–Dec with colored event bars/dots grouped by category and highlighted peak seasons), with a prominent purple "AI 요약" headline card reading "2026년은 상반기 스마트팩토리·로봇, 하반기 헬스케어·친환경 전시가 축을 이룹니다" and 3–5 quarterly highlight cards. Below that, a compact "월간" summary example with a purple "AI 요약" tag "이번 달은 로봇·헬스케어 전시가 집중됩니다 · 주말 방문 추천" and a small calendar/list of that month's exhibitions (title, opening date, hall, D-day). Make it clearly AI-curated and forward-looking, distinct from the "진행 중/이번 달/다음 달" showcase below. + +Scroll sections below: (1) "진행 중 · 예정 전시" showcase — a rich grid/carousel of 2:3 poster cards (poster image, title, dates "2026.09.05–09.08", hall label, a status badge, a "D-25" day chip, "사전등록"), with tabs "진행 중 | 이번 달 | 다음 달" and a venue filter "전체 · 제1전시장 · 제2전시장". (2) "당신의 관심사에 맞는 전시" recommendation rail with interest chips and a small purple "AI 추천" tag. (3) "방문 가이드" hub as a 2×3 grid of stroke-icon cards: "교통 · GTX-A 킨텍스역 도보 3분", "주차 · 실시간 주차 안내", "실내 길찾기", "편의시설 · 식음·수유실·물품보관", "접근성 · 휠체어·수어통역 안내", "고객문의 · FAQ·연락처". (4) a 3-step visitor journey "사전등록 → 배지/QR 발급 → 현장 체크인". (5) a "공지 · 뉴스" list of 5 recent items with "더보기". (6) a "전시장 소개" band with KINTEX scale/hall figures. Footer with company info, "오시는 길 · GTX-A 킨텍스역", SNS links, 고객문의, language. + +All exhibition, transport, parking, entry and amenity data shown here is served from a pre-crawled on-premise database (not live external calls); the AI assistant answers ONLY from this stored data and cites its source. Spacious yet dense (no large empty gaps), sophisticated global exhibition-center homepage feel, rounded 8px cards, tabular numerals for dates, WCAG AA contrast in both themes. ``` ### SCR-T2 business 서브홈 (비즈니스 안내·진입 홈) [business · 공개 · `/business`] @@ -1772,6 +1805,9 @@ A mobile "my ticket wallet" screen for an exhibition visitor, 390px width, light | 버전 | 일자 | 작성자 | 내용 | |---|---|---|---| +| **v2.3.2** | 2026-07-12 | designer | **SCR-T1 — AI 전시·행사 기획 브리핑(연간·월간 선제 제시) 추가(소유자 추가 지시 2026-07-12).** 단발 질의응답을 넘어 **AI가 개최 예정 전시·행사를 먼저 큐레이션**하는 섹션을 히어로 AI 능력 타일 직하(레이아웃 4번)에 신설 — **연/월 관점 토글**(① 연간=12개월 타임라인+분기 하이라이트+AiLabel "AI 요약" 헤드라인, ② 월간=선택 월 캘린더/리스트+AI 월간 요약), 연/월 전환 시 AI가 해당 기간 데이터 근거로 요약·추천 갱신. **중복 회피**: 하단 "진행 중/이번 달/다음 달" 쇼케이스(레이아웃 5)와 달리 본 섹션은 **연간 기획 관점**이 차별점, 월간은 동일 event 소스로 정합. 구성요소 `AiPlanningBriefing` 추가, 이하 레이아웃 5~11 재번호. **데이터 원천** — 신규 공개 엔드포인트 제안 `GET /api/public/events/planning?view=year|month&year&month`(크롤 적재 `exhibition`/`event`를 개최연·월 그룹 조회, AI 기간 요약은 AiTextRouter Claude→Ollama 폴백이 해당 기간 DB만 근거·근거 인용·캐시, DB 서빙·외부 실시간 호출 없음, 계약 제안). 상태(로딩·성공·**degraded(요약 미가용→데이터만 노출)**·빈·에러·기간 전환 부분 로딩) 정의. **Stitch 프롬프트 갱신** — 연간 타임라인+"AI 요약" 헤드라인("2026년은 상반기 스마트팩토리·로봇, 하반기 헬스케어·친환경…")+월간 요약("이번 달은 로봇·헬스케어 전시가 집중됩니다") 문단 추가, 쇼케이스와 구별 명시. 세련된 홈페이지 톤·풀블리드·반응형·라이트/다크·크롤 적재 DB 근거 원칙은 유지. 신규 토큰 0·PLANNING.md·src 무수정 | +| **v2.3.1** | 2026-07-12 | designer | **SCR-T1 마무리 — AI 전면 노출 + AI 답변 근거=크롤 적재 DB 명시(소유자 추가 지시 2건 2026-07-12).** ①**AI 기능 쇼케이스 타일 신설**(히어로 직하 레이아웃 3번): "AI 시스템인데 AI가 어디 있는지 모르겠다" 지적 반영 — AI 관람 도우미·AI 전시 추천·AI 길찾기·AI 일정 도우미·AI 혼잡/대기 안내·AI 시공 예상 이미지 6능력을 AiLabel 카드로 전면 홍보(구성요소 AiCapabilityTiles·상태(정적 상시·미가용 "준비 중")·이하 레이아웃 4~10 재번호). ②**데이터 원천 = 사전 크롤 적재 온프레미스 DB 명시**: AI 관람 도우미(`POST /api/public/ai/visitor-assistant`)가 **실시간 외부 크롤이 아니라 미리 크롤해 DB에 적재한 공개 데이터만을 RAG 근거로 답하고 참조 레코드를 인용**(범위 밖=degraded)함을 명문화 — 근거 테이블 계약 예시(`exhibition`/`event`·`transport_info`·`parking_info`·`ticket_policy`·`amenity_info`·`visitor_guide`·`floorplan`), 각 섹션 출처를 크롤 적재 DB로 매핑, degraded 폴백을 "크롤 적재 데이터 검색"으로 갱신, 백엔드 계약을 "공개 조회 API가 크롤 적재 DB에서 서빙(외부 실시간 호출 없음·온프레미스·근거 인용)" 전제로 기술. ③**Stitch 프롬프트 갱신** — AI 능력 쇼케이스 카드 6종 문구 + "served from a pre-crawled on-premise database, AI answers ONLY from stored data and cites its source" 추가. 세련된 홈페이지 톤·풀블리드·반응형·라이트/다크는 v2.3 그대로 유지. 신규 토큰 0·PLANNING.md·src 무수정 | +| **v2.3** | 2026-07-12 | designer | **SCR-T1 visitor 메인을 "세련된 공개 마케팅 홈페이지"로 격상 + 중심 요소를 대화형 AI 관람 도우미로 재구성(소유자 지시 2026-07-12).** ①**§3C SCR-T1 전면 재작성** — 기존 "visitor 서브홈(가이드 허브)"을 일반 대중 대상 마케팅 홈페이지(코엑스/글로벌 전시장 홈 수준·풀블리드·타이포 위계·공백 없는 밀도·전환 지향)로 격상. **화면 중심 = AI 관람 도우미**(히어로 정중앙 대형 자연어 질문 입력창 + 예시 질문칩 5종 + 대화형 답변 패널: 답변 텍스트·구조화 결과 카드(전시/교통/입장/wayfinding)·후속 질문칩·AI 고지문·근거 출처, AiLabel 보라 구분). 관람객 관심 우선순위 5(전시 추천·교통·입장·부스위치·편의)를 자연어 하나로 관통. 하위 섹션 순증: 전시 쇼케이스(2:3 포스터 그리드/캐러셀·탭·전시장 필터·`poster_url`)·관심사 맞춤 추천 레일(M11 AI)·방문가이드 허브(2×3, GTX-A·주차·M13 길찾기·편의·접근성·문의)·관람객 여정 3-스텝(사전등록→배지/QR→체크인)·공지·뉴스·전시장 소개·푸터(회사·SNS·문의). ②**데이터 원천** — **신규 공개 엔드포인트 제안 `POST /api/public/ai/visitor-assistant`**(비인증·CORS·레이트리밋·PII 미수집, 기존 AiTextRouter Claude→Ollama 폴백 + 공개 행사 데이터 근거·환각방지, **계약 제안·백엔드 후속**), 나머지 `/api/public/events`·notice·CMS 재사용. ③**상태** — AI 도우미 대기/생각중/성공/**degraded(AI 미가용→키워드 검색 폴백)**/에러/빈결과 정의. ④**테마** — 공개 마케팅 홈페이지에 한해 **라이트/다크 테마 토글 제공(§1-5 업무 셸 라이트 단일 원칙의 공개 트랙 예외)**, 색은 §1 토큰 CSS 변수 주입·라이트 기본. ⑤**접근성** — AI 입력 `role="search"`·답변 `aria-live="polite"`·색 외 라벨·양 테마 WCAG AA, i18n `track.visitor.*`. ⑥영어 **Stitch 프롬프트 전면 갱신**("premium exhibition-center marketing homepage"·1440 full-bleed·AI 관람 도우미 centerpiece·제품명 "KINTEX AI 전시·행사시스템" 정확 표기·NO emoji/line SVG·라이트·다크). ⑦**§2.8-2 인벤토리** SCR-T1 행 명칭/모듈 갱신. **경계 준수**: business(SCR-T2)·agency(SCR-T3) 메인은 B2B 업무 톤 유지(격상 대상 아님) 명시, 라우트 `/visitor`·PublicShell·TrackSwitcher·세션키·기존 SCR 불변, **신규 토큰 0**, PLANNING.md·src 무수정 | | **v2.2** | 2026-07-12 | designer | **3-트랙(visitor/business/agency) 대외 접점 재편 — 신규 4화면 SCR-T0~T3 순증(planner 지시서 `_workspace/plan_3track.md`·PLANNING v3.2 §2-2·`docs/analysis/coex-website.md` 근거).** 코엑스 3-사이트 IA(VISITOR·BUSINESS·CYBER)를 킨텍스 단일 도메인 위 3-트랙 얕은 진입 레이어로 정형화. ①**§3C 신설** — 3C-0 공개 셸(PublicShell) 재사용+상단 3-트랙 스위처(TrackSwitcher, `role=tablist`·`관람/비즈니스/협력사`·워드마크 `/` 복귀·언어·로그인·`내 홈`) 사양, SCR-T0 관문 랜딩(루트 `/`·제품 히어로 "KINTEX AI 전시·행사시스템"+3-트랙 대형 분기 카드+행사 하이라이트 레일, 관람 1순위), SCR-T1 visitor 서브홈(`/visitor`·행사 그리드+사전등록/티켓 CTA+2×3 방문 가이드 허브(교통 GTX-A·주차·M13 실내 길찾기·편의·접근성·문의)+공지), SCR-T2 business 서브홈(`/business`·주최자/참가 2-분기 가치제안+임대 절차 스텝+요금·서류+로그인/문의 CTA→SCR-01→SCR-02/05, 온라인 신청 일원화 강조), SCR-T3 agency 서브홈(`/agency`·등록업체 안내 739·14분류·미등록 시공 엄금 고지+진행 중 M15 공사 옥션 역경매 리스트+작업신고·규정+등록업체 로그인→SCR-27/28, 옥션 차별점 강조) — 각 화면 레이아웃·구성요소·데이터 원천(기존 API 재사용 매핑: login-slides·`/api/public/events`·notice·SCR-P6·M15 공개 서브셋)·상태(로딩/빈/에러/degraded)·접근성·i18n 네임스페이스(`track.gateway/visitor/business/agency/shell.*`)+영어 Stitch 프롬프트(Precision Enterprise AI·1440·NO emoji·선 SVG·WCAG AA). ②**SCR-HOME 관계 명시**: 관문 SCR-T0(`/`)=미인증 전용 공개 랜딩, 인증 세션 시 `/home`(SCR-HOME) 우선 리다이렉트(딥링크·SEO·게스트 티켓 조회 예외). ③**§2.8-2 인벤토리**에 SCR-T* 표 추가(집계 85→**89화면**, 미생성 2→6: SCR-HOME·M3+T0~T3). **경계 준수**: 기존 SCR·라우트·MDI 셸(§2.7)·티켓 공개 라우트(SCR-P7/P8)·세션키 불변, 신규 라우트는 얕은 트랙 진입 4개만, ops/admin 트랙 외부 유지, §1A 멀티테넌시 정합. §1 토큰 권위·**신규 토큰 0**, 기존 화면 스펙·PLANNING.md·src 무수정 | | **v2.1.2** | 2026-07-12 | designer | **로그인 후 메인 홈(SCR-HOME) 신설 + 로그인 랜딩 흐름 변경 + 셸 하단 상태바(ShellFooter) 신설(소유자 지시 2026-07-12).** ①**§3 SCR-HOME 신설**(라우트 `/home`, AppShell 셸 내부 핀 홈 탭) — 인사·요약 KPI 스트립 4타일(진행 전시·오늘 체크인·D-임박·미결 결재, 기존 실 API 재사용), 행사소개 배너 캐러셀(**SCR-01 login-slides 자산/API 재사용**·자동슬라이드·좌우 내비), 현재·예정 전시 일정 캘린더(**WISE `CalendarView` 패턴 참조** — 월 그리드 6주×7열·요일 헤더·오늘 강조·단일일 색점칩/기간 spanning bar·"+N" 팝업 + **전시장 세그먼트 필터(동적 옵션·하드코딩 금지 — `전체`+홀 마스터 센터 구분 distinct, 3관·4관 확장 대비)** 기본값 전체, `/api/events/calendar`+`/api/public/events`+홀 마스터 옵션 API 재사용), **"진행 중·다가오는 전시" 쇼케이스(확정판 — 배너 아래 가로 스크롤 포스터 카드 레일 8~12장·세로 2:3 포스터+행사명/기간/hall_label/상태배지/D-day·좌우 화살표·탭 `진행 중|이번 달|다음 달`·poster_url 부재 시 그라디언트+행사명 폴백, 데이터=event 테이블 poster_url V32 크롤 34건)**, 우측 컬럼 4위젯 스택(**내 할 일(역할 기반, 최상단) — 주최자=미결 결재/서류 마감 D-n/홀 배정 대기/옥션 낙찰 대기·참가업체=신고서류/설계 확정/견적·청구·공사업체=응찰 마감/시공 일정·관리자=승인 대기/점검, 행=상태점+작업명+대상+D-day 클릭 이동·건수 배지·0건 "모두 완료"+CTA, 데이터=§5B approval·M6·M15·M9 집계 조합 — 신규 통합 API `GET /api/home/todos` 계약 제안 명시**·공지 최근 5건+더보기(§5B notice `/work/notice`)·**빠른 작업 2×3 바로가기 그리드(역할별)**·**최근 활동 타임라인 5건(알림 피드 재사용)**), 내 행사 워크스페이스 카드 그리드(참여 행사·카드 클릭→역할별 대시보드·빈 상태 초대코드 안내) + **KPI 4타일 역할별 구성 명시(주최자/참가업체/공사업체/관리자 예시 세트)** + **밀도 원칙 4항(소유자 추가 지시)**: 대형 공백 금지·12컬럼 컴팩트(섹션 간 24px 표준/32px 상한)·배너 높이 220~260px 제한·좌우 컬럼 세로 균형·전 빈 상태 안내+행동 버튼(CTA) 조합. **본 판이 SCR-HOME 스펙 확정판** — 레이아웃·상태(영역별 로딩/빈/에러)·데이터 원천 매핑·접근성(WCAG AA·i18n `home.`·이모지 금지 선SVG·`prefers-reduced-motion`)+영어 Stitch 프롬프트(Precision Enterprise AI·1440·footer 포함). ②**SCR-01 흐름 수정**: 로그인 성공→`/home` 랜딩, "행사 워크스페이스 선택" phase 제거(SCR-HOME 섹션으로 흡수), SCR-01은 순수 로그인 화면으로 축소. ③**§2-2 ShellFooter 신설**(약 36px, 전 셸 화면 공통): 좌측 저작권 "© 2026 KINTEX. All rights reserved.", 우측 시스템 상태 점(정상/점검·색외 텍스트 병기)·앱 버전·고객지원 링크, `role="contentinfo"`·라이트/다크 토큰·WCAG AA·i18n `shell.footer.*`, 모바일 미적용(더보기 이관). §2.7-1 셸 레이아웃 다이어그램에 하단 36px 행 추가. ④**§2-1 사이트맵·§2.8-2 인벤토리**에 SCR-HOME 반영(집계 84→**85화면**, 미생성 0→2: SCR-HOME+SCR-M3). §1 토큰 권위 유지·**신규 토큰 0**, 기존 화면 스펙·PLANNING.md·src 무수정 | | **v2.1.1** | 2026-07-12 | designer | **Stitch 잔여 39화면 전량 확보(64/64) 상태 반영 + 이전 확보 25화면 상태 정합 + 집계 갱신.** 2026-07-12 Stitch 수확 재실행으로 미확보 39화면이 전량 확보됨 — §2.8-2 인덱스 상태 컬럼만 `🔲미생성`→`✅Stitch` 교체(웹코어 SCR-22·23·25·26·27·29·30·32~51, 관리자 SCR-A5·A6·A8·A9, 공개 SCR-P1~P7, 모바일 SCR-M15). 추가로 이전 세션 확보분 25화면(디스크 code.html 존재 검증: 웹코어 SCR-18·19·20·21·24·28·31, 관리자 SCR-A1~A4·A7·A10, 공개 SCR-P8, 모바일 SCR-M4~M14)도 동일 규칙으로 정합화 — 총 64화면 상태 교체. §2.8-2 집계 문장 갱신: ✅Stitch 18→**83** · 🔲미생성 64→**0**(Stitch 미확보는 SCR-M3 1건뿐). 화면 스펙·프롬프트·구조 무수정. 수확 상세 `_workspace/stitch_gen/harvest_report.md` | diff --git a/docs/security/mobile-security-audit.md b/docs/security/mobile-security-audit.md new file mode 100644 index 0000000..0f1880c --- /dev/null +++ b/docs/security/mobile-security-audit.md @@ -0,0 +1,138 @@ +# KINTEX AI 모바일 앱 + 백엔드 보안 감사 결과 + +> 감사일: 2026-07-12 · 감사자: 통합 QA(reviewer) · 방식: 정적 소스 감사(코드 미수정, 읽기전용) +> 체크리스트: `mobile-security-checklist.md` · 개선 백로그: 본 문서 §하단 +> 증거는 `파일:라인` 표기. `mobile/`는 다른 에이전트 편집 중이라 스냅샷 기준. + +## 종합 판정 + +| 영역 | PASS | FAIL | N-A | 총평 | +|------|:----:|:----:|:---:|------| +| 1. 위변조 방지 | 1 | 5 | 0 | **최대 약점** — 루트/디버거/무결성 탐지·난독화 선언 부재 | +| 2. 데이터 보호 | 5 | 2 | 0 | SecureStore·로그 청결 우수 / FLAG_SECURE·백그라운드 마스킹 부재 | +| 3. 통신 보안 | 2 | 2 | 0 | HTTPS 강제 / cleartext 차단 선언·피닝 부재 | +| 4. 인증·세션 | 4 | 1 | 0 | 2FA·잠금·RBAC 견고 / 생체 로컬잠금 부재 | +| 5. 시큐어 코딩 | 7 | 0 | 0 | **모범** — SQLi 가드·CSPRNG·스택트레이스 차단 완비 | +| 6. 빌드·배포 | 4 | 2 | 0 | env 시크릿·오류응답 청결 / 권한 최소화·CORS 명시 보강 | + +전반: **백엔드·시큐어코딩은 공공 기준 대비 견고**. **모바일 클라이언트 측 위변조 방지(anti-tampering)와 화면보호가 최대 갭** — 공공 대민 앱 기준(KISA 위변조 항목군)에서 필수 보강 대상. + +--- + +## 1. 앱 위변조 방지 + +| ID | 판정 | 위험도 | 증거 / 근거 | +|----|------|--------|-------------| +| AT-1 루트/탈옥 탐지 | **FAIL** | High | package.json 의존성에 `jail-monkey`·`freerasp`·root 탐지 라이브러리 없음(mobile/package.json 13~33). 코드 내 탐지 로직 0건. | +| AT-2 서명/무결성 | **FAIL** | High | Play Integrity/App Attest·서명 지문 검증 코드 없음. 리패키징 탐지 부재. | +| AT-3 코드 난독화 | **FAIL** | Med | app.json에 `jsEngine: "hermes"` 미선언(mobile/app.json 2~53) — RN 0.74 기본 Hermes지만 명시·R8 설정 없음. `expo-build-properties`(R8/ProGuard) 미설치. | +| AT-4 디버거/에뮬레이터 | **FAIL** | Med | Frida/디버거/에뮬레이터 탐지 없음. | +| AT-5 디버그 비활성 | **부분 PASS** | Low | eas.json production=`app-bundle`(비디버그 기본, eas.json 18~22). 단 `android:debuggable=false` 명시·`__DEV__` 스트립 확인 필요. | +| AT-6 개발자모드/USB | **FAIL** | Med | 개발자옵션·USB 디버깅 감지 없음. | + +## 2. 데이터 보호 + +| ID | 판정 | 위험도 | 증거 / 근거 | +|----|------|--------|-------------| +| DP-1 토큰 안전저장 | **PASS** | — | JWT를 expo-secure-store(Keychain/Keystore)에 저장(mobile/lib/secureStore.ts 10~25, mobile/context/AuthContext.tsx 62~66). web만 AsyncStorage 폴백(비네이티브). | +| DP-2 하드코딩 비밀 0 | **PASS** | — | 시크릿/키/토큰 grep 0건(mobile 전체). config.ts는 base URL만(공개, mobile/lib/config.ts 10~11). | +| DP-3 비밀번호 미저장 | **PASS** | — | 비밀번호는 요청 body로만 전송, 저장 로직 없음(mobile/lib/auth.ts 26~33, login.tsx 주석 4). | +| DP-4 화면캡처/오버레이 | **FAIL** | Med | 2FA/티켓QR 등 민감화면에 FLAG_SECURE 미적용(grep 0건). `expo-screen-capture` 미설치. | +| DP-5 백그라운드 스냅샷 | **FAIL** | Med | 앱 전환 시 민감화면 마스킹 로직 없음. | +| DP-6 클립보드 보호 | **PASS(경미)** | Low | 클립보드 강제복사 코드 없음. 티켓/개인정보 필드 클립보드 제어는 별도 검토 권장. | +| DP-7 로그 민감정보 0 | **PASS** | — | `console.log/warn/error` grep 0건(mobile 전체). 토큰·비밀번호 로깅 없음. | + +## 3. 통신 보안 + +| ID | 판정 | 위험도 | 증거 / 근거 | +|----|------|--------|-------------| +| NW-1 TLS 강제 | **부분 PASS** | Med | base URL `https://kintex.zioinfo.co.kr`(mobile/app.json 45, config.ts 11). cleartext `http://` grep 0건. 단 Android `usesCleartextTraffic=false`·networkSecurityConfig 미선언(기본 SDK34 false지만 명시 권장). | +| NW-2 인증서 검증 | **PASS** | — | 표준 fetch(트러스트스토어 검증), 검증 우회 코드 없음(mobile/lib/api.ts 56~61). | +| NW-3 인증서 피닝 | **FAIL** | Med | 인증서/공개키 피닝 미적용. 공공 대민 앱 권고사항. | +| NW-4 API 키 미노출 | **PASS** | — | 통신에 클라이언트 API 키 없음. Authorization은 런타임 발급 JWT만(mobile/lib/api.ts 52). | + +## 4. 인증·세션 + +| ID | 판정 | 위험도 | 증거 / 근거 | +|----|------|--------|-------------| +| AU-1 2FA(TOTP) | **PASS** | — | OTP_REQUIRED→재제출 흐름(mobile/lib/auth.ts 26~38, login.tsx 62~84). 서버 TotpService(HS 라이브러리 codeVerifier, backend security/TotpService.java 46~57). | +| AU-2 생체·로컬잠금 | **FAIL** | Med | `expo-local-authentication` 미설치, 로컬 재잠금 없음(진행 중 내정보 트랙과 연계). | +| AU-3 세션 만료·재발급 | **PASS** | — | 401 시 메모리 토큰 정리·재로그인 유도(mobile/lib/api.ts 67~70). 서버 TTL env(backend application.yml 67, JwtService 만료 66). | +| AU-4 로그인 실패 잠금 | **PASS** | — | 임계 초과 시 locked_until 설정, 계정열거 방지 일반화 메시지(backend security/LoginAttemptService.java 34~57). 클라 ACCOUNT_LOCKED 처리(login.tsx 74~75). | +| AU-5 서버측 RBAC | **PASS** | — | 행사 단위 RBAC를 JWT roles 클레임+컨트롤러 가드로 서버 강제(backend JwtService.java 47~69, SecurityConfig.java 48). | + +## 5. 시큐어 코딩 + +| ID | 판정 | 위험도 | 증거 / 근거 | +|----|------|--------|-------------| +| SC-1 입력검증 | **PASS** | — | 클라 검증(login.tsx 53~60, register.tsx 27~30) + 서버 @Valid(GlobalExceptionHandler MethodArgumentNotValid 39~47). | +| SC-2 SQLi 방지 | **PASS** | — | MyBatis 동적 `${}`는 NlQueryReadMapper.xml `${sql}` 단 1곳, 결정론적 화이트리스트 가드 통과분만 실행(backend NlQuerySqlGuard.java: 단일SELECT·테이블 화이트리스트·주석/다중문/PII/LIMIT 검증 56~130). 그 외 전부 `#{}` 바인딩. | +| SC-3 안전한 난수 | **PASS** | — | OTP IV SecureRandom(backend SecretCipher.java 31,44), OTP secret은 totp SecretGenerator(CSPRNG), 웹훅 HmacSHA256. | +| SC-4 웹뷰 안전설정 | **N-A→PASS** | — | WebView 미사용(mobile grep 0건). 공격면 없음. | +| SC-5 딥링크 검증 | **부분 PASS** | Low | scheme `kintex`(app.json 6). expo-router 라우팅. 딥링크 파라미터 명시 검증 로직 추가 권장(현재 인증가드로 보호). | +| SC-6 스택트레이스 미노출 | **PASS** | — | 서버 include-stacktrace/message: never(application.yml 13~14), GlobalExceptionHandler 요약만 반환·스택 미저장(51~62), 401 봉투(SecurityConfig 49~57). 클라 요약 메시지만 노출(api.ts 82~89). | +| SC-7 암호화 저장 | **PASS** | — | OTP secret AES-256-GCM 봉인(backend SecretCipher.java seal/open 39~77, `enc:v1:` 포맷). | + +## 6. 빌드·배포 + +| ID | 판정 | 위험도 | 증거 / 근거 | +|----|------|--------|-------------| +| BD-1 릴리스 로그/소스맵 | **PASS(경미)** | Low | console 로그 0건. 소스맵 배포 제외·`__DEV__` 스트립은 빌드시 확인 권장. | +| BD-2 최소 권한 | **부분 PASS** | Med | app.json에 명시 권한 선언 없음(기본 Expo 권한만). expo-image-picker 사용(package.json 19) → 카메라/사진 권한 발생. `blockedPermissions`로 불필요 권한 제거 권장. | +| BD-3 서드파티 취약점 | **미검증** | Med | `npm audit` 미실행(감사 범위상 코드 실행 배제). 의존성 버전은 SDK51 정렬 최신(package.json). CI에 audit 게이트 권장. | +| BD-4 서버 오류응답 | **PASS** | — | IP/SSH/비밀번호/스택트레이스 응답 미노출(GlobalExceptionHandler, ServerOut 스키마 규칙 준수). error_log 요약 1줄만(51~59). | +| BD-5 CORS/헤더 | **부분 PASS** | Med | `.cors(cors -> {})`에 CorsConfigurationSource 빈 미정의(backend SecurityConfig.java 35) → 명시 허용출처 없음(와일드카드 `*`도 없음). 운영 도메인 명시 권장. | +| BD-6 시크릿 주입 | **PASS(주의)** | Med | JWT_SECRET·KINTEX_OTP_ENC_KEY·DB/SMTP/ADMIN 비번 전부 env 주입(application.yml 27,43,66,76,83). **주의**: JWT_SECRET 개발 폴백 `CHANGE_ME_DEV_ONLY...`(66), OTP 키 미주입 시 개발 파생키 경고(SecretCipher 88) → **운영 env 주입 필수**. GEMINI_API_KEY는 워커 전용, 백엔드 미사용(9). | + +--- + +## 추가 관찰(비차단, 운영 하드닝) + +- **로그 레벨**: `logging.level.com.zioinfo.kintex: DEBUG`(application.yml 106) — 운영 프로파일에서 INFO로 하향 권장(민감 파라미터 verbose 노출 위험). +- **JWT 저장 위치**: 웹 타깃만 AsyncStorage 폴백(secureStore.ts 10) — 관람객 B2C 웹뷰/PWA 배포 시 XSS 토큰 탈취면 존재. 네이티브 앱은 무관. +- **웹훅 인증**: `/api/webhooks/in/**` permitAll이나 토큰+HMAC-SHA256 이중검증·상수시간 비교(WebhookSignature.java `MessageDigest.isEqual` 58) — 양호. + +--- + +## 개선 백로그 (FAIL/취약 → 구현 작업 전환) + +> 담당: 위변조·SecureStore·화면보호·network config = **kintex-mobile-dev** / 백엔드 = **kintex-backend-dev** / EAS·빌드설정 = **kintex-devops-dev** +> 코드 편집은 각 담당 에이전트가 수행(본 QA는 문서·감사만). SDK51 호환 주의 병기. + +### P0 (High · 공공 대민 필수) + +| # | 항목 | 담당 | 권장 구현 | SDK51 주의 | +|---|------|------|-----------|-----------| +| B1 | AT-1 루트/탈옥 탐지 | mobile-dev | `jail-monkey`(isJailBroken/canMockLocation) 또는 `freerasp-react-native` 도입 → 루팅 단말 경고·민감기능 제한. config plugin으로 prebuild 반영 | jail-monkey는 config plugin 필요(bare/prebuild). Expo 관리형은 `expo prebuild` 또는 dev-client 필요 — devops와 협의 | +| B2 | AT-2 앱 무결성 검증 | mobile-dev + backend-dev | Android **Play Integrity API**·iOS **App Attest** 토큰을 로그인/민감 API에 첨부 → backend에서 검증. 서명 지문 확인 | Expo 관리형은 네이티브 모듈 → dev-client/prebuild. 서버 검증 엔드포인트 신설 | +| B3 | AT-3 난독화 선언 | devops-dev | app.json `android.jsEngine:"hermes"`(또는 최상위) 명시 + `expo-build-properties`로 `android.enableProguardInReleaseBuilds:true`·`enableShrinkResources:true` | expo-build-properties 플러그인 추가·prebuild 필요. Hermes는 RN0.74 기본이나 명시로 회귀 방지 | +| B4 | DP-4 화면캡처 방지 | mobile-dev | `expo-screen-capture` `preventScreenCaptureAsync()`를 2FA·티켓QR·개인정보 화면 focus 시 적용 | expo-screen-capture SDK51 호환. iOS는 캡처 감지만·Android는 FLAG_SECURE 차단 | + +### P1 (Med) + +| # | 항목 | 담당 | 권장 구현 | SDK51 주의 | +|---|------|------|-----------|-----------| +| B5 | NW-1 cleartext 차단 명시 | devops-dev | `expo-build-properties`로 `android.usesCleartextTraffic:false` 명시 + iOS ATS 유지 확인 | prebuild 반영 | +| B6 | NW-3 인증서 피닝 | mobile-dev + devops-dev | `react-native-ssl-pinning` 또는 build-properties networkSecurityConfig `pin-set`(SPKI 해시). 백엔드 인증서 갱신 로테이션 절차 병행 | 피닝 만료/로테이션 운영 리스크 — 백업 핀 2개 이상 | +| B7 | DP-5 백그라운드 마스킹 | mobile-dev | AppState 'inactive/background' 시 오버레이 블러/스플래시 표시(민감 화면) | RN AppState + 조건부 렌더 | +| B8 | AU-2 생체 로컬잠금 | mobile-dev | `expo-local-authentication`으로 앱 재진입·민감작업 시 생체 재인증(내정보 트랙과 통합) | SDK51 호환. 폴백(PIN) 설계 | +| B9 | AT-4/AT-6 디버거·개발자모드 탐지 | mobile-dev | freerasp의 debugger/emulator/hooking 탐지 콜백 활용(B1과 통합) | B1과 동일 도입 경로 | +| B10 | BD-2 권한 최소화 | devops-dev | app.json `android.permissions` 명시 + `blockedPermissions`로 불필요 권한 제거. image-picker 권한 문자열(카메라/사진) 최소화 | image-picker는 권한 필요 — 사용처만 요청 | +| B11 | BD-5 CORS 명시 | backend-dev | `CorsConfigurationSource` 빈으로 운영 도메인(`kintex.zioinfo.co.kr`·`kintex.wise.ai.kr`)만 allowedOrigins 명시, 와일드카드 금지 | Spring Security 6 lambda DSL | +| B12 | BD-6 운영 시크릿 강제 | devops-dev + backend-dev | 운영 배포 시 JWT_SECRET·KINTEX_OTP_ENC_KEY env 주입 검증(부재 시 기동 실패 fail-fast 검토), DEBUG 로그 INFO 하향 | 개발 폴백 값 운영 유입 차단 | +| B13 | BD-3 의존성 CVE | devops-dev | CI에 `npm audit --audit-level=high`(mobile) + gradle 의존성 스캔 게이트 추가 | 정기 스캔 | + +### P2 (Low · 하드닝) + +| # | 항목 | 담당 | 권장 구현 | +|---|------|------|-----------| +| B14 | AT-5 debuggable 검증 | devops-dev | 릴리스 빌드 `android:debuggable=false`·소스맵 미배포·`__DEV__` 스트립 CI 검증 | +| B15 | SC-5 딥링크 검증 | mobile-dev | `kintex://` 딥링크 진입 파라미터 스키마 검증·화이트리스트 라우팅 | +| B16 | DP-6 클립보드 정책 | mobile-dev | 티켓QR·개인정보 필드 `contextMenuHidden`·민감 복사 자동클리어 | + +--- + +## 재검증 게이트(구현 후) +1. `mobile/`: jail-monkey/freerasp 도입·screen-capture 적용·build-properties(hermes/R8/cleartext) 반영 → 본 문서 AT-*/DP-4/NW-1 재판정. +2. `backend`: CorsConfigurationSource 빈·운영 로그레벨·시크릿 fail-fast → BD-5/6 재판정. +3. 빌드 회귀: `mobile tsc --noEmit` EXIT 0, `./gradlew build` 통과 확인. diff --git a/docs/security/mobile-security-checklist.md b/docs/security/mobile-security-checklist.md new file mode 100644 index 0000000..39dd77d --- /dev/null +++ b/docs/security/mobile-security-checklist.md @@ -0,0 +1,85 @@ +# KINTEX AI 모바일 앱 + 백엔드 보안 체크리스트 + +> 대상: `mobile/`(Expo SDK 51 · expo-router · RN 0.74) + `src/backend`(Spring Boot 3.x · MyBatis · PostgreSQL) +> 기준 표준: **OWASP MASVS v2 / MSTG**, **행안부·KISA "모바일 대민 서비스 앱 소스코드 검증 가이드"**, **전자정부 모바일 표준보안 요구사항** +> 킨텍스는 공공 전시장(한국국제전시장) 대상 서비스 — 공공기관 대민 앱 기준을 함께 적용한다. +> 판정 결과는 `mobile-security-audit.md`, 개선 작업은 동 문서 하단 백로그 참조. + +## 표준 매핑 표기 +- MASVS-STORAGE / -CRYPTO / -AUTH / -NETWORK / -PLATFORM / -CODE / -RESILIENCE +- KISA: 대민 앱 검증 가이드 항목군(위변조·데이터보호·통신·인증·시큐어코딩·서버) + +--- + +## 1. 앱 위변조 방지 (Anti-Tampering) — MASVS-RESILIENCE / KISA 위변조 + +| ID | 항목 | 기준 | 근거 표준 | +|----|------|------|-----------| +| AT-1 | 루트/탈옥 탐지 | 루팅(Android)·탈옥(iOS) 단말에서 실행 시 경고·기능 제한·차단 | MASVS-RESILIENCE-1, KISA 위변조-1 | +| AT-2 | 앱 서명/무결성 검증 | 서명 인증서 지문 확인, 리패키징(재서명) 탐지, Play Integrity/App Attest 검토 | MASVS-RESILIENCE-2, KISA 위변조-2 | +| AT-3 | 코드 난독화 | JS 번들 Hermes 바이트코드 컴파일, 네이티브 R8/ProGuard 난독화·최적화 활성 | MASVS-RESILIENCE-3 | +| AT-4 | 디버거/후킹 탐지 | 디버거 attach·Frida/Xposed 후킹·에뮬레이터 실행 탐지 | MASVS-RESILIENCE-4 | +| AT-5 | 디버그 비활성 | 릴리스 빌드 `android:debuggable=false`, JS `__DEV__` 코드 제거 | MASVS-RESILIENCE, KISA 위변조-3 | +| AT-6 | 개발자모드/USB 디버깅 대응 | 민감 작업 시 개발자옵션·USB 디버깅 활성 감지·경고 | KISA 위변조-4 | + +## 2. 데이터 보호 (Data at Rest) — MASVS-STORAGE / KISA 데이터보호 + +| ID | 항목 | 기준 | 근거 표준 | +|----|------|------|-----------| +| DP-1 | 토큰 안전저장 | JWT·세션 토큰은 Keychain(iOS)/Keystore(Android) = expo-secure-store, 평문 AsyncStorage 금지 | MASVS-STORAGE-1, KISA 데이터-1 | +| DP-2 | 하드코딩 비밀 0 | 소스·설정에 API 키·비밀번호·토큰 하드코딩 없음(grep 0건) | MASVS-STORAGE-2, KISA 데이터-2 | +| DP-3 | 비밀번호 미저장 | 로그인 비밀번호를 단말에 어떤 형태로도 저장하지 않음 | MASVS-STORAGE-1 | +| DP-4 | 화면캡처/오버레이 방지 | 민감 화면(2FA·티켓QR·개인정보) FLAG_SECURE로 캡처·녹화·오버레이 차단 | MASVS-PLATFORM-3, KISA 데이터-4 | +| DP-5 | 백그라운드 스냅샷 | 앱 전환 시 민감 화면 마스킹(태스크 스위처 스냅샷 노출 방지) | MASVS-STORAGE-7 | +| DP-6 | 클립보드 보호 | 비밀번호·토큰·개인정보 필드 클립보드 복사 최소화·자동삭제 | MASVS-STORAGE-8 | +| DP-7 | 로그 내 민감정보 0 | 토큰·비밀번호·개인정보를 로그(console/logcat)에 남기지 않음 | MASVS-STORAGE-3, KISA 데이터-3 | + +## 3. 통신 보안 (Data in Transit) — MASVS-NETWORK / KISA 통신 + +| ID | 항목 | 기준 | 근거 표준 | +|----|------|------|-----------| +| NW-1 | TLS 강제 | 모든 API가 HTTPS. 평문 HTTP(cleartext) 차단 — Android networkSecurityConfig(`cleartextTrafficPermitted=false`), iOS ATS 유지 | MASVS-NETWORK-1, KISA 통신-1 | +| NW-2 | 인증서 검증 | 서버 인증서·호스트명 검증(기본 트러스트스토어). 검증 우회 코드 없음 | MASVS-NETWORK-2 | +| NW-3 | 인증서 피닝 | 공공/민감 앱 권고 — 서버 인증서·공개키 피닝 검토·적용 | MASVS-NETWORK-4, KISA 통신-2 | +| NW-4 | API 키 미노출 | 통신에 클라이언트 하드코딩 API 키 사용 안 함(base URL만 공개) | MASVS-STORAGE-2 | + +## 4. 인증·세션 — MASVS-AUTH / KISA 인증 + +| ID | 항목 | 기준 | 근거 표준 | +|----|------|------|-----------| +| AU-1 | 2차 인증(2FA) | TOTP 기반 OTP 로그인 흐름(OTP_REQUIRED→재제출) 지원 | MASVS-AUTH-1, KISA 인증-1 | +| AU-2 | 생체·로컬 잠금 | 앱 로컬 생체(Face/Touch/지문) 재잠금 옵션(민감 진입) | MASVS-AUTH-2 | +| AU-3 | 세션 만료·재발급 | 액세스 토큰 TTL, 만료 시 401 처리·재로그인 유도 | MASVS-AUTH-3 | +| AU-4 | 로그인 실패 잠금 | 연속 실패 임계 초과 시 계정 잠금(서버), 계정 열거 방지 일반화 메시지 | MASVS-AUTH-6, KISA 인증-2 | +| AU-5 | 서버측 권한검증 | 행사 단위 RBAC를 서버에서 강제(클라이언트 신뢰 금지) | MASVS-AUTH-4 | + +## 5. 시큐어 코딩 — MASVS-CODE / KISA 시큐어코딩 + +| ID | 항목 | 기준 | 근거 표준 | +|----|------|------|-----------| +| SC-1 | 입력검증 | 클라이언트·서버 이중 입력검증(이메일·비밀번호 길이·형식) | MASVS-CODE-4, KISA SC-1 | +| SC-2 | SQL 인젝션 방지 | MyBatis 파라미터 바인딩(`#{}`), 동적 `${}` 미사용 또는 화이트리스트 가드 | KISA SC-2 | +| SC-3 | 안전한 난수 | 토큰·OTP·IV 생성에 CSPRNG(SecureRandom) 사용 | MASVS-CRYPTO-3 | +| SC-4 | 웹뷰 안전설정 | (있으면) JS브리지 최소화·파일접근 차단·신뢰 URL만 로드 | MASVS-PLATFORM-2 | +| SC-5 | 딥링크 검증 | 커스텀 스킴(`kintex://`) 파라미터 검증, 미검증 라우팅 금지 | MASVS-PLATFORM-1 | +| SC-6 | 예외/스택트레이스 미노출 | 서버·클라이언트 모두 스택트레이스·내부 세부 미노출, 요약 메시지만 | MASVS-CODE-5, KISA 서버-4 | +| SC-7 | 암호화 저장 | 민감 시크릿(OTP secret 등) AES-256-GCM 저장 | MASVS-CRYPTO-1 | + +## 6. 빌드·배포 — MASVS-RESILIENCE / KISA 서버·배포 + +| ID | 항목 | 기준 | 근거 표준 | +|----|------|------|-----------| +| BD-1 | 릴리스 로그/소스맵 | 릴리스 빌드에 디버그 로그·소스맵 미포함 | MASVS-RESILIENCE-3 | +| BD-2 | 최소 권한 | app.json 권한 최소화, 불필요 권한 제거(blockedPermissions) | MASVS-PLATFORM-1, KISA 서버-1 | +| BD-3 | 서드파티 취약점 | package.json 의존성 CVE 스캔(npm audit), 최신 유지 | MASVS-CODE-3 | +| BD-4 | 서버 오류응답 | 백엔드 오류 응답에 IP·SSH·비밀번호·스택트레이스 미노출 | KISA 서버-3·4 | +| BD-5 | CORS/헤더 | CORS 허용출처 명시(와일드카드 금지), 보안 헤더 | KISA 서버-2 | +| BD-6 | 시크릿 주입 | JWT_SECRET·암호화 키·DB/SMTP 비번은 env 주입, 코드/커밋 미포함 | MASVS-STORAGE-2 | + +--- + +## 판정 기호 +- **PASS** — 기준 충족(증거 있음) +- **FAIL** — 기준 미충족(구현 필요) +- **N-A** — 현재 미적용 대상(해당 기능 부재) +- 위험도: **High**(공공/민감 필수) · **Med** · **Low** diff --git a/mobile/README.md b/mobile/README.md index 1ed133c..87b0eca 100644 --- a/mobile/README.md +++ b/mobile/README.md @@ -28,7 +28,8 @@ API 베이스는 `app.json > extra.apiBase` 또는 `EXPO_PUBLIC_API_BASE` 환경 | `app/(tabs)/index.tsx` | 홈/대시보드(역할별 진입) | design §2-2 | | `app/(tabs)/field.tsx` | 현장 허브(역할 분기) | — | | `app/(tabs)/gallery.tsx` | 시각화 갤러리(워터마크 상시) | SCR-12 | -| `app/(tabs)/more.tsx` | 계정·서버 상태·로그아웃 | — | +| `app/(tabs)/more.tsx` | 계정(내정보 진입)·서버 상태·로그아웃 | — | +| `app/profile.tsx` | 내정보(WISE mypage 이식·생체인식 잠금·프로필 사진 등록) | WISE `uiws/mypage.tsx` | | `app/checklist.tsx` | 현장 체크리스트(48px 터치·오프라인·사진) | SCR-M1 | | `app/inspection.tsx` | 현장 검수(승인/반려·AI 플래그) | SCR-M2 | @@ -37,6 +38,14 @@ API 베이스는 `app.json > extra.apiBase` 또는 `EXPO_PUBLIC_API_BASE` 환경 - AI 생성 이미지는 `components/AiImage.tsx`가 워터마크 + "AI 생성 예상 이미지" 고지를 **항상** 표시(제거 불가). - 비밀번호는 저장하지 않음. "아이디 기억"은 이메일만 AsyncStorage 보관. +## 보안 하드닝(위변조 방지 · 화면보호) +근거: `docs/security/mobile-security-audit.md`(B1~B13) · 상세: `_workspace/08_mobile_security_hardening.md`. 모두 방어적(네이티브 미지원 기기 graceful). +- **위변조 탐지(B1/B9)**: `lib/integrity.ts`(jail-monkey) — 루팅·후킹·디버거·개발자모드·USB 디버깅 탐지. 로그인 시 비차단 경고(공공 대민 정책). +- **화면캡처 방지(B4)+백그라운드 마스킹(B7)**: `context/SecureScreenContext.tsx`의 `useSecureScreen(key)` — 2FA 로그인·티켓 QR·내정보에서 캡처 차단(FLAG_SECURE)+태스크 스위처 마스킹. +- **난독화(B3)·cleartext 차단(B5)·권한 최소화(B10)**: `app.json`(Hermes·expo-build-properties R8/ProGuard·usesCleartextTraffic:false·iOS ATS·android.permissions/blockedPermissions). +- **앱 무결성 토큰(B2 전송부)**: `lib/api.ts`가 `X-Integrity-Token` 조건부 첨부(발급기 등록 시). 서버 검증은 backend-dev 인계. +- **한계**: 인증서 피닝(B6)은 회귀 위험으로 미도입 — 커스텀 config plugin 경로 문서화(`_workspace/08`). EAS 실빌드는 devops·G3 게이트. + ## 남은 작업 - 실 화면 데이터 API 배선 확대(부스 목록·검수 요청 제출 — 백엔드 M6/C-4 확장 대기). - 앱 아이콘/스플래시 에셋(`assets/`), i18n, 푸시 알림. diff --git a/mobile/app.json b/mobile/app.json index a871cd3..c4032f3 100644 --- a/mobile/app.json +++ b/mobile/app.json @@ -7,6 +7,7 @@ "version": "0.1.0", "orientation": "portrait", "userInterfaceStyle": "light", + "jsEngine": "hermes", "icon": "./assets/icon.png", "splash": { "image": "./assets/splash.png", @@ -18,7 +19,12 @@ ], "ios": { "supportsTablet": true, - "bundleIdentifier": "kr.co.zioinfo.kintex" + "bundleIdentifier": "kr.co.zioinfo.kintex", + "infoPlist": { + "NSAppTransportSecurity": { + "NSAllowsArbitraryLoads": false + } + } }, "android": { "package": "kr.co.zioinfo.kintex", @@ -26,7 +32,22 @@ "adaptiveIcon": { "foregroundImage": "./assets/adaptive-icon.png", "backgroundColor": "#0066B3" - } + }, + "permissions": [ + "android.permission.CAMERA", + "android.permission.USE_BIOMETRIC", + "android.permission.USE_FINGERPRINT", + "android.permission.READ_MEDIA_IMAGES" + ], + "blockedPermissions": [ + "android.permission.RECORD_AUDIO", + "android.permission.READ_MEDIA_VIDEO", + "android.permission.ACCESS_MEDIA_LOCATION", + "android.permission.READ_EXTERNAL_STORAGE", + "android.permission.WRITE_EXTERNAL_STORAGE", + "android.permission.ACCESS_COARSE_LOCATION", + "android.permission.ACCESS_FINE_LOCATION" + ] }, "web": { "bundler": "metro", @@ -36,7 +57,32 @@ "plugins": [ "expo-router", "expo-secure-store", - "expo-font" + "expo-font", + "expo-screen-capture", + [ + "expo-image-picker", + { + "photosPermission": "프로필 사진 등록을 위해 사진 보관함 접근이 필요합니다.", + "cameraPermission": "프로필 사진 촬영을 위해 카메라 접근이 필요합니다." + } + ], + [ + "expo-local-authentication", + { + "faceIDPermission": "생체인식 잠금 해제를 위해 Face ID 사용이 필요합니다." + } + ], + [ + "expo-build-properties", + { + "android": { + "enableProguardInReleaseBuilds": true, + "enableShrinkResources": true, + "usesCleartextTraffic": false + }, + "ios": {} + } + ] ], "experiments": { "typedRoutes": false diff --git a/mobile/app/(tabs)/more.tsx b/mobile/app/(tabs)/more.tsx index 6e0bc40..8e48349 100644 --- a/mobile/app/(tabs)/more.tsx +++ b/mobile/app/(tabs)/more.tsx @@ -5,6 +5,7 @@ import { Ionicons } from '@expo/vector-icons'; import { router } from 'expo-router'; import React, { useCallback, useEffect, useState } from 'react'; import { Pressable, ScrollView, StyleSheet, Text, View } from 'react-native'; +import { Avatar } from '../../components/Avatar'; import { Card } from '../../components/Card'; import { useAuth } from '../../context/AuthContext'; import { api, isDegraded } from '../../lib/api'; @@ -36,12 +37,25 @@ export default function MoreScreen() { return ( - - {user?.displayName ?? '사용자'} - - {user?.hallManager ? '킨텍스 내부 계정 (홀매니저)' : '일반 계정'} - - + {/* 내 정보 진입 — 프로필/생체인식 잠금/사진 등록 (SCR-M 내정보) */} + router.push('/profile')} + accessibilityRole="button" + accessibilityLabel="내 정보 열기" + > + + + + + {user?.displayName ?? '사용자'} + + {user?.hallManager ? '킨텍스 내부 계정 (홀매니저)' : '일반 계정'} + + + + + + @@ -75,6 +89,7 @@ function Row({ label, value }: { label: string; value: string }) { const styles = StyleSheet.create({ scroll: { padding: spacing.md, gap: spacing.md }, + meRow: { flexDirection: 'row', alignItems: 'center' }, name: { fontSize: type.h3.fontSize, fontWeight: '700', color: colors.neutral900 }, meta: { marginTop: 4, color: colors.neutral500, fontSize: type.caption.fontSize }, row: { flexDirection: 'row', justifyContent: 'space-between', gap: 12, paddingVertical: 4 }, diff --git a/mobile/app/_layout.tsx b/mobile/app/_layout.tsx index 449ae49..9e3a782 100644 --- a/mobile/app/_layout.tsx +++ b/mobile/app/_layout.tsx @@ -8,6 +8,7 @@ import React from 'react'; import { GestureHandlerRootView } from 'react-native-gesture-handler'; import { SafeAreaProvider } from 'react-native-safe-area-context'; import { AuthProvider } from '../context/AuthContext'; +import { SecureScreenProvider } from '../context/SecureScreenContext'; import { colors } from '../theme'; export default function RootLayout() { @@ -15,6 +16,7 @@ export default function RootLayout() { + + + diff --git a/mobile/app/login.tsx b/mobile/app/login.tsx index 41d9e06..14f3427 100644 --- a/mobile/app/login.tsx +++ b/mobile/app/login.tsx @@ -19,8 +19,10 @@ import { Banner } from '../components/Banner'; import { Button } from '../components/Button'; import { Field } from '../components/Field'; import { useAuth } from '../context/AuthContext'; +import { useSecureScreen } from '../context/SecureScreenContext'; import { ApiRequestError } from '../lib/api'; import { isOtpRequired, login } from '../lib/auth'; +import { useDeviceIntegrity } from '../lib/integrity'; import { prefDelete, prefGet, prefSet } from '../lib/secureStore'; import { colors, radius, spacing, touch, type } from '../theme'; @@ -30,6 +32,11 @@ export default function LoginScreen() { const insets = useSafeAreaInsets(); const { signIn } = useAuth(); + // 2FA/자격증명 입력 화면 — 캡처 차단 + 백그라운드 마스킹(B4/B7). + useSecureScreen('login'); + // 위변조(루팅·후킹·디버거) 단말 경고(B1/B9) — 비차단 고지. + const integrity = useDeviceIntegrity(); + const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const [otp, setOtp] = useState(''); @@ -99,6 +106,14 @@ export default function LoginScreen() { AI 생성 예상 이미지 · 실제 시공 결과와 다를 수 있습니다 + {/* 위변조 단말 경고(비차단) — 공공 대민 정책: 경고 후 이용 제한 안내 */} + {integrity.compromised ? ( + + 보안 위험이 감지된 기기입니다(루팅·후킹·디버거). 개인정보·티켓 보호를 위해 정상 기기에서 + 이용을 권장합니다. + + ) : null} + {/* 로그인 카드 */} 로그인 diff --git a/mobile/app/profile.tsx b/mobile/app/profile.tsx new file mode 100644 index 0000000..3b715b7 --- /dev/null +++ b/mobile/app/profile.tsx @@ -0,0 +1,688 @@ +/* + * SCR-M(내정보/프로필) — WISE 모바일 mypage 이식(구조·컴포넌트·플로우 문자 그대로 차용). + * 레퍼런스: guardia-messenger/app/uiws/mypage.tsx. + * · 프로필 헤드(아바타+카메라 배지 → 사진 선택→미리보기→적용 시에만 업로드) + * · 정보 행(InfoRow) · 계정/보안(2FA 상태) · 생체인식 잠금 토글 · 환경설정 · 로그아웃 + * 추가: ① 생체인식 로컬 잠금(진입 게이트) ② 프로필 사진 등록(갤러리/카메라). + * 보안: 새 시크릿·토큰·자격증명을 console로 출력하지 않는다. 생체 원본은 OS에만. + */ +import { Ionicons } from '@expo/vector-icons'; +import { router, useFocusEffect } from 'expo-router'; +import React, { useCallback, useEffect, useState } from 'react'; +import { + ActivityIndicator, + Alert, + Image, + Modal, + Pressable, + ScrollView, + StyleSheet, + Switch, + Text, + View, +} from 'react-native'; +import { Avatar } from '../components/Avatar'; +import { Banner } from '../components/Banner'; +import { Card } from '../components/Card'; +import { useAuth } from '../context/AuthContext'; +import { useSecureScreen } from '../context/SecureScreenContext'; +import { ApiRequestError, isDegraded } from '../lib/api'; +import { + authenticateBiometric, + isBiometricEnabled, + isBiometricSupported, + isSessionUnlocked, + markSessionUnlocked, + setBiometricEnabled, +} from '../lib/biometric'; +import { + isPickerAvailable, + pickFromCamera, + pickFromLibrary, + type PickedImage, +} from '../lib/imagePick'; +import { avatarUrl, getMe, getOtpStatus, uploadAvatar } from '../lib/profile'; +import { prefGet, prefSet } from '../lib/secureStore'; +import type { EventRole, MePrincipal, OtpStatusDto } from '../lib/types'; +import { colors, radius, spacing, touch, type } from '../theme'; + +const roleLabel: Record = { + ORGANIZER: '주최자', + EXHIBITOR: '참가업체', + CONTRACTOR: '장치·시공업체', + HALL_MANAGER: '홀매니저', +}; + +// 환경설정(이 기기 로컬 저장 — 서버 동기화 계약 부재, web MyPage와 동일 정책). +const PREFS_KEY = 'kintex.prefs'; +interface Prefs { + notifyDeadline: boolean; + notifyApproval: boolean; + notifyPayment: boolean; +} +const DEFAULT_PREFS: Prefs = { notifyDeadline: true, notifyApproval: true, notifyPayment: true }; + +export default function ProfileScreen() { + const { user, signOut } = useAuth(); + + // 개인정보 화면 — 캡처 차단 + 백그라운드 마스킹(B4/B7). + useSecureScreen('profile'); + + const [me, setMe] = useState(null); + const [loading, setLoading] = useState(false); + const [loadErr, setLoadErr] = useState(''); + const [otp, setOtp] = useState(null); + + // 프로필 사진 — 선택 → 미리보기(적용/취소) → 적용 시에만 업로드(WISE 불변식). + const [photoUrl, setPhotoUrl] = useState(null); + const [photoBusy, setPhotoBusy] = useState(false); + const [previewImg, setPreviewImg] = useState(null); + const [previewErr, setPreviewErr] = useState(''); + const [photoPending, setPhotoPending] = useState(false); // 서버 미반영(degrade) 로컬 프리뷰 상태 + + // 생체인식 + const [bioSupported, setBioSupported] = useState(false); + const [bioEnabled, setBioEnabled] = useState(false); + const [locked, setLocked] = useState(false); // 잠금 게이트 통과 전 콘텐츠 차단 + + // 환경설정 + const [prefs, setPrefs] = useState(DEFAULT_PREFS); + + const displayName = me?.displayName ?? user?.displayName ?? '사용자'; + + const loadProfile = useCallback(async () => { + setLoading(true); + setLoadErr(''); + try { + const p = await getMe(); + setMe(p); + setPhotoUrl(avatarUrl(p.userId)); + setPhotoPending(false); + } catch (e) { + // /me 실패 시 최소한 로그인 세션 정보로 화면 유지(전면 오류 회피). + setLoadErr(e instanceof ApiRequestError ? e.message : '내정보를 불러오지 못했습니다.'); + } finally { + setLoading(false); + } + try { + setOtp(await getOtpStatus()); + } catch { + setOtp(null); // 상태 조회 실패는 무시(섹션은 안내로 degrade) + } + }, []); + + // 환경설정 로드 + useEffect(() => { + prefGet(PREFS_KEY) + .then((raw) => { + if (raw) setPrefs({ ...DEFAULT_PREFS, ...(JSON.parse(raw) as Partial) }); + }) + .catch(() => setPrefs(DEFAULT_PREFS)); + }, []); + + // 생체 지원/토글 상태 + useEffect(() => { + isBiometricSupported().then(setBioSupported).catch(() => setBioSupported(false)); + isBiometricEnabled().then(setBioEnabled).catch(() => setBioEnabled(false)); + }, []); + + // 진입 게이트 — 생체 잠금 활성 + 세션 미해제 시 인증 요구. + const runLockGate = useCallback(async () => { + const enabled = await isBiometricEnabled(); + if (!enabled || isSessionUnlocked()) { + setLocked(false); + return; + } + setLocked(true); + const ok = await authenticateBiometric(); + if (ok) { + markSessionUnlocked(); + setLocked(false); + } + }, []); + + useFocusEffect( + useCallback(() => { + runLockGate(); + loadProfile(); + }, [runLockGate, loadProfile]), + ); + + const savePrefs = async (next: Prefs) => { + setPrefs(next); + try { + await prefSet(PREFS_KEY, JSON.stringify(next)); + } catch { + /* graceful */ + } + }; + + const toggleBiometric = async (next: boolean) => { + if (next) { + // 활성화 전 1회 본인 확인. + const ok = await authenticateBiometric(); + if (!ok) { + Alert.alert('생체인식', '생체 인증에 실패했습니다. 다시 시도해 주세요.'); + return; + } + markSessionUnlocked(); // 방금 인증했으므로 이 세션은 통과 처리(즉시 재프롬프트 방지). + } + await setBiometricEnabled(next); + setBioEnabled(next); + }; + + // ── 사진 선택(미리보기용) — 업로드 호출 없음 ── + const pickForPreview = async (from: 'library' | 'camera') => { + if (photoBusy) return; + if (!isPickerAvailable()) { + Alert.alert('프로필 사진', '이미지 선택 기능을 사용할 수 없는 기기입니다.'); + return; + } + const img = from === 'library' ? await pickFromLibrary() : await pickFromCamera(); + if (!img) { + // 권한 거부/취소/미설치: 모달이 열려있으면 유지, 아니면 무동작 + 권한 안내. + return; + } + setPreviewImg(img); + setPreviewErr(''); + }; + + // ── 적용 — 이 시점에만 1회 업로드. 미구현/네트워크면 로컬 프리뷰로 degrade ── + const applyPhoto = async () => { + if (!previewImg || photoBusy) return; + setPhotoBusy(true); + setPreviewErr(''); + try { + const res = await uploadAvatar(previewImg); + const base = res.photoUrl || (me ? avatarUrl(me.userId) : null); + setPhotoUrl(base ? `${base}${base.includes('?') ? '&' : '?'}_t=${Date.now()}` : previewImg.uri); + setPhotoPending(false); + setPreviewImg(null); + } catch (e) { + if (isDegraded(e)) { + // 백엔드 아바타 업로드 미구현 — 로컬 프리뷰 유지 + 대기 안내(파괴적 실패 아님). + setPhotoUrl(previewImg.uri); + setPhotoPending(true); + setPreviewImg(null); + } else { + setPreviewErr(e instanceof ApiRequestError ? e.message : '사진 업로드에 실패했습니다.'); + } + } finally { + setPhotoBusy(false); + } + }; + + const cancelPreview = () => { + if (photoBusy) return; + setPreviewImg(null); + setPreviewErr(''); + }; + + const onChangePhoto = () => { + if (photoBusy) return; + Alert.alert('프로필 사진', '사진을 선택하세요', [ + { text: '갤러리', onPress: () => pickForPreview('library') }, + { text: '카메라', onPress: () => pickForPreview('camera') }, + { text: '취소', style: 'cancel' }, + ]); + }; + + const onSignOut = () => { + Alert.alert('로그아웃', '로그아웃하시겠습니까?', [ + { text: '취소', style: 'cancel' }, + { + text: '로그아웃', + style: 'destructive', + onPress: async () => { + await signOut(); + router.replace('/login'); + }, + }, + ]); + }; + + // 생체 잠금 게이트 — 미통과 시 콘텐츠 차단 화면. + if (locked) { + return ( + + + 생체인식 잠금 + 지문 또는 Face로 잠금을 해제하세요. + + 다시 시도 + + + ); + } + + const roles = me?.eventRoles ? Object.values(me.eventRoles) : []; + + return ( + + {/* ── 프로필 ── */} + 내 정보 + {loading ? ( + + ) : ( + + + + + + {photoBusy ? ( + + ) : ( + + )} + + + + {displayName} + + {me?.userId ?? user?.userId ?? '-'} + {me?.roleCode ? ` · ${me.roleCode}` : ''} + + + + + {photoPending ? ( + + 선택한 사진은 이 기기에만 표시됩니다 — 서버 반영은 준비 중입니다. + + ) : null} + + {loadErr ? {loadErr} : null} + + + + + + + + )} + + {/* ── 참여 행사 역할 ── */} + {roles.length > 0 ? ( + <> + 참여 행사 역할 + + {Object.entries(me!.eventRoles).map(([eventId, r]) => ( + + + {eventId} + + + {roleLabel[r] ?? r} + + + ))} + + + ) : null} + + {/* ── 계정·보안(2차 인증) ── */} + 계정 · 보안 + + + + 2차 인증(OTP) + + {otp?.otpEnabled + ? `활성화됨 · ${otp.verifyMethod === 'OTP' ? 'Authenticator(OTP)' : otp.verifyMethod || 'OTP'}` + : '미설정 — 로그인 시 인증 앱(TOTP) 2단계 인증을 사용합니다.'} + + + + + {otp?.otpEnabled ? '켜짐' : '꺼짐'} + + + + + + {/* ── 생체인식 잠금 ── */} + 생체인식 잠금 + + {bioSupported ? ( + + + 지문 · Face 잠금 + + 켜면 내정보 진입 시 지문 또는 Face로 재인증합니다. 로그인 2차 인증은 그대로 유지됩니다. + + + + + ) : ( + + 이 기기는 생체인식을 지원하지 않거나 지문/Face가 등록되어 있지 않습니다. 기기 설정에서 생체 + 정보를 등록한 뒤 다시 시도해 주세요. + + )} + + + {/* ── 환경설정(알림) ── */} + 알림 설정 + + {( + [ + ['notifyDeadline', '마감 D-데이 알림'], + ['notifyApproval', '승인·검수 알림'], + ['notifyPayment', '결제·정산 알림'], + ] as const + ).map(([key, label]) => ( + + {label} + savePrefs({ ...prefs, [key]: v })} + trackColor={{ false: colors.neutral200, true: colors.primary600 }} + thumbColor={colors.white} + /> + + ))} + 알림 설정은 이 기기에 저장됩니다. 서버 동기화는 준비 중입니다. + + + {/* ── 로그아웃 ── */} + + + 로그아웃 + + + KINTEX AI 전시관리 · 내정보 + + {/* ── 프로필 사진 미리보기 모달(선택→미리보기→적용 확정) ── */} + + + {}}> + + 프로필 사진 변경 + + + + + + + {previewImg ? ( + + ) : null} + {photoBusy ? ( + + + + ) : null} + + 원형으로 표시됩니다. + + + pickForPreview('library')} + disabled={photoBusy} + > + + 갤러리 + + pickForPreview('camera')} + disabled={photoBusy} + > + + 카메라 + + + + {previewErr ? {previewErr} : null} + + + {photoBusy ? ( + + + 적용 중… + + ) : ( + 적용 + )} + + + 취소 + + + + + + ); +} + +function InfoRow({ label, value, testID }: { label: string; value: string | null; testID?: string }) { + return ( + + {label} + + {value && value.length ? value : '-'} + + + ); +} + +const styles = StyleSheet.create({ + wrap: { flex: 1, backgroundColor: colors.neutral050 }, + scroll: { padding: spacing.md, paddingBottom: spacing.xl }, + section: { + color: colors.neutral900, + fontSize: type.h3.fontSize, + fontWeight: '800', + marginTop: spacing.md, + marginBottom: spacing.sm, + }, + + profileHead: { flexDirection: 'row', alignItems: 'center', marginBottom: spacing.sm }, + photoEdit: { + position: 'absolute', + right: -2, + bottom: -2, + width: 22, + height: 22, + borderRadius: 11, + backgroundColor: colors.primary600, + alignItems: 'center', + justifyContent: 'center', + borderWidth: 2, + borderColor: colors.white, + }, + profileName: { color: colors.neutral900, fontSize: type.h2.fontSize, fontWeight: '800' }, + profileId: { color: colors.neutral500, fontSize: type.caption.fontSize, marginTop: 3 }, + pendingNote: { color: colors.warning, fontSize: type.caption.fontSize, marginBottom: spacing.sm }, + + infoRow: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + paddingVertical: 9, + borderTopWidth: 1, + borderTopColor: colors.neutral200, + }, + infoLabel: { color: colors.neutral500, fontSize: type.caption.fontSize, fontWeight: '700' }, + infoValue: { + color: colors.neutral900, + fontSize: type.body.fontSize, + fontWeight: '600', + flexShrink: 1, + textAlign: 'right', + marginLeft: 12, + }, + rolePill: { + backgroundColor: colors.primary100, + borderRadius: radius.pill, + paddingHorizontal: 10, + paddingVertical: 4, + }, + rolePillText: { color: colors.primary700, fontSize: type.caption.fontSize, fontWeight: '700' }, + + bioRow: { flexDirection: 'row', alignItems: 'center', paddingVertical: 4 }, + rowTitle: { color: colors.neutral900, fontSize: type.body.fontSize, fontWeight: '700', marginBottom: 4 }, + hint: { color: colors.neutral500, fontSize: type.caption.fontSize, lineHeight: 18, marginTop: 4 }, + + badgeOn: { backgroundColor: '#E7F5EF', borderRadius: radius.pill, paddingHorizontal: 10, paddingVertical: 4 }, + badgeOnText: { color: colors.success, fontSize: type.caption.fontSize, fontWeight: '800' }, + badgeOff: { backgroundColor: colors.neutral200, borderRadius: radius.pill, paddingHorizontal: 10, paddingVertical: 4 }, + badgeOffText: { color: colors.neutral700, fontSize: type.caption.fontSize, fontWeight: '800' }, + + logoutBtn: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + gap: 8, + marginTop: spacing.lg, + minHeight: touch.min, + borderRadius: radius.sm, + borderWidth: 1, + borderColor: colors.error, + backgroundColor: colors.white, + }, + logoutText: { color: colors.error, fontSize: type.h3.fontSize, fontWeight: '700' }, + version: { textAlign: 'center', color: colors.neutral500, fontSize: type.caption.fontSize, marginTop: spacing.md }, + + // 생체 잠금 게이트 + lockWrap: { + flex: 1, + alignItems: 'center', + justifyContent: 'center', + gap: spacing.sm, + padding: spacing.lg, + backgroundColor: colors.neutral050, + }, + lockTitle: { color: colors.neutral900, fontSize: type.h2.fontSize, fontWeight: '800', marginTop: spacing.sm }, + lockHint: { color: colors.neutral500, fontSize: type.body.fontSize, textAlign: 'center' }, + lockBtn: { + marginTop: spacing.md, + minHeight: touch.min, + paddingHorizontal: spacing.lg, + borderRadius: radius.sm, + backgroundColor: colors.primary600, + alignItems: 'center', + justifyContent: 'center', + }, + lockBtnText: { color: colors.white, fontSize: type.h3.fontSize, fontWeight: '700' }, + + // 사진 미리보기 모달 + modalBackdrop: { flex: 1, backgroundColor: 'rgba(0,0,0,0.55)', justifyContent: 'flex-end' }, + modalSheet: { + backgroundColor: colors.white, + borderTopLeftRadius: radius.md, + borderTopRightRadius: radius.md, + borderWidth: 1, + borderColor: colors.neutral200, + padding: spacing.lg, + paddingBottom: spacing.xl, + }, + modalHeaderRow: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }, + modalTitle: { color: colors.neutral900, fontSize: type.h3.fontSize, fontWeight: '800' }, + previewWrap: { + width: 120, + height: 120, + borderRadius: 60, + alignSelf: 'center', + marginVertical: spacing.md, + overflow: 'hidden', + borderWidth: 1, + borderColor: colors.neutral200, + backgroundColor: colors.primary050, + }, + previewImg: { width: 120, height: 120, borderRadius: 60 }, + previewOverlay: { + ...StyleSheet.absoluteFillObject, + alignItems: 'center', + justifyContent: 'center', + backgroundColor: 'rgba(0,0,0,0.35)', + }, + previewCaption: { color: colors.neutral500, fontSize: type.caption.fontSize, textAlign: 'center', marginTop: -4 }, + modalPickRow: { flexDirection: 'row', gap: 10, marginTop: spacing.md }, + pickBtn: { + flex: 1, + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + gap: 8, + minHeight: touch.min, + borderRadius: radius.sm, + borderWidth: 1, + borderColor: colors.neutral200, + backgroundColor: colors.white, + }, + pickText: { color: colors.neutral900, fontSize: type.body.fontSize, fontWeight: '700' }, + applyBtn: { + marginTop: spacing.md, + minHeight: touch.min, + borderRadius: radius.sm, + backgroundColor: colors.primary600, + alignItems: 'center', + justifyContent: 'center', + }, + applyBusyRow: { flexDirection: 'row', alignItems: 'center', gap: 8 }, + applyText: { color: colors.white, fontSize: type.h3.fontSize, fontWeight: '800' }, + cancelBtn: { + marginTop: spacing.sm, + minHeight: touch.min, + borderRadius: radius.sm, + borderWidth: 1, + borderColor: colors.neutral200, + alignItems: 'center', + justifyContent: 'center', + backgroundColor: colors.white, + }, + cancelText: { color: colors.neutral700, fontSize: type.h3.fontSize, fontWeight: '700' }, + err: { color: colors.error, fontSize: type.caption.fontSize, marginTop: spacing.sm }, + disabled: { opacity: 0.6 }, +}); diff --git a/mobile/app/tickets/index.tsx b/mobile/app/tickets/index.tsx index cb1767b..64ca15e 100644 --- a/mobile/app/tickets/index.tsx +++ b/mobile/app/tickets/index.tsx @@ -9,6 +9,7 @@ import { router, Stack } from 'expo-router'; import React, { useMemo, useState } from 'react'; import { Alert, Pressable, ScrollView, StyleSheet, Text, View } from 'react-native'; import { Banner } from '../../components/Banner'; +import { useSecureScreen } from '../../context/SecureScreenContext'; import { QrViewerModal } from '../../components/tickets/QrViewerModal'; import { TicketCard } from '../../components/tickets/TicketCard'; import { @@ -20,6 +21,9 @@ import { import { colors, radius, spacing, type } from '../../theme'; export default function TicketWalletScreen() { + // 티켓 QR 화면 — 캡처 차단(QR 재사용 방지) + 백그라운드 마스킹(B4/B7). + useSecureScreen('tickets'); + const [filter, setFilter] = useState('active'); const [qrOpen, setQrOpen] = useState(false); const [qrIndex, setQrIndex] = useState(0); diff --git a/mobile/assets/adaptive-icon.png b/mobile/assets/adaptive-icon.png new file mode 100644 index 0000000..1a160ad Binary files /dev/null and b/mobile/assets/adaptive-icon.png differ diff --git a/mobile/assets/favicon.png b/mobile/assets/favicon.png new file mode 100644 index 0000000..a3b7d9a Binary files /dev/null and b/mobile/assets/favicon.png differ diff --git a/mobile/assets/icon.png b/mobile/assets/icon.png new file mode 100644 index 0000000..5303496 Binary files /dev/null and b/mobile/assets/icon.png differ diff --git a/mobile/assets/splash.png b/mobile/assets/splash.png new file mode 100644 index 0000000..94eb737 Binary files /dev/null and b/mobile/assets/splash.png differ diff --git a/mobile/components/Avatar.tsx b/mobile/components/Avatar.tsx new file mode 100644 index 0000000..5f3c3e5 --- /dev/null +++ b/mobile/components/Avatar.tsx @@ -0,0 +1,72 @@ +/* + * 공통 Avatar — 프로필 사진(photoUrl) 표시, 실패/부재 시 이름 이니셜 폴백. WISE 모바일 Avatar 이식. + * 레퍼런스: guardia-messenger/app/uiws/_lib/components/Avatar.tsx. + * 사진 서빙 엔드포인트는 인증 필요 — 는 api 인터셉터를 거치지 않으므로 Bearer 헤더를 직접 부여. + * 색은 theme 토큰만 사용(신규 토큰 발명 금지). + */ +import React, { useEffect, useState } from 'react'; +import { Image, StyleSheet, Text, View } from 'react-native'; +import { getAccessToken } from '../lib/api'; +import { colors } from '../theme'; + +interface AvatarProps { + photoUrl?: string | null; + name?: string | null; + size?: number; + testID?: string; +} + +function initials(name?: string | null): string { + if (!name) return '?'; + const trimmed = name.trim(); + if (!trimmed) return '?'; + const isAscii = /^[\x00-\x7F]+$/.test(trimmed); + if (isAscii) { + const parts = trimmed.split(/\s+/); + return (parts.length >= 2 ? parts[0][0] + parts[1][0] : trimmed.slice(0, 2)).toUpperCase(); + } + return trimmed.slice(0, 1); +} + +export function Avatar({ photoUrl, name, size = 48, testID }: AvatarProps) { + const [failed, setFailed] = useState(false); + const token = getAccessToken(); + // photoUrl/token 변화 시 실패 상태 초기화 후 재시도(업로드·캐시버스터 반영). + useEffect(() => { + setFailed(false); + }, [photoUrl, token]); + + const dim = { width: size, height: size, borderRadius: size / 2 }; + const showImage = !!photoUrl && !!token && !failed; + + if (showImage) { + return ( + setFailed(true)} + accessibilityIgnoresInvertColors + /> + ); + } + return ( + + + {initials(name)} + + + ); +} + +const styles = StyleSheet.create({ + fallback: { alignItems: 'center', justifyContent: 'center', borderWidth: 1 }, + initials: { fontWeight: '800' }, +}); diff --git a/mobile/context/SecureScreenContext.tsx b/mobile/context/SecureScreenContext.tsx new file mode 100644 index 0000000..1935b37 --- /dev/null +++ b/mobile/context/SecureScreenContext.tsx @@ -0,0 +1,101 @@ +/* + * 민감화면 보호 컨텍스트 — B4(화면캡처 차단) + B7(백그라운드 스냅샷 마스킹) 통합. + * 근거: docs/security/mobile-security-audit.md 백로그 B4·B7, checklist DP-4·DP-5. + * + * 동작: + * · 민감화면이 focus되면 useSecureScreen 훅이 등록(activeCount++) + preventCapture. + * · 앱이 background/inactive(태스크 스위처·홈 전환)로 가고 activeCount>0이면 + * 루트에 브랜드 오버레이를 덮어 스냅샷에 민감 정보가 남지 않게 마스킹. + * · 이탈/포그라운드 복귀 시 자동 해제. + * 방어적: 캡처 모듈 부재여도 오버레이(B7)는 순수 RN(AppState)로 동작. + */ +import { useFocusEffect } from 'expo-router'; +import React, { + createContext, + useCallback, + useContext, + useEffect, + useMemo, + useRef, + useState, +} from 'react'; +import { AppState, type AppStateStatus, StyleSheet, Text, View } from 'react-native'; +import { allowCapture, preventCapture } from '../lib/screenCapture'; +import { colors, spacing, type } from '../theme'; + +interface SecureScreenContextValue { + register: () => void; + unregister: () => void; +} + +const SecureScreenContext = createContext(undefined); + +export function SecureScreenProvider({ children }: { children: React.ReactNode }) { + const [activeCount, setActiveCount] = useState(0); + const [appState, setAppState] = useState(AppState.currentState); + + const register = useCallback(() => setActiveCount((n) => n + 1), []); + const unregister = useCallback(() => setActiveCount((n) => Math.max(0, n - 1)), []); + + useEffect(() => { + const sub = AppState.addEventListener('change', (next) => setAppState(next)); + return () => sub.remove(); + }, []); + + const value = useMemo(() => ({ register, unregister }), [register, unregister]); + + // 민감화면 활성 + 비활성 상태(태스크 스위처/백그라운드) → 마스킹. + const masked = activeCount > 0 && appState !== 'active'; + + return ( + + {children} + {masked ? ( + + KINTEX + AI 전시·행사시스템 + + ) : null} + + ); +} + +/** + * 민감화면 보호 훅 — 화면 컴포넌트 본문에서 1회 호출. + * @param key 캡처 스택 구분용 고유 키(화면별 상수). + * focus 시 캡처 차단 + 백그라운드 마스킹 등록, blur 시 해제. + */ +export function useSecureScreen(key: string): void { + const ctx = useContext(SecureScreenContext); + const registered = useRef(false); + + useFocusEffect( + useCallback(() => { + preventCapture(key); + if (ctx && !registered.current) { + ctx.register(); + registered.current = true; + } + return () => { + allowCapture(key); + if (ctx && registered.current) { + ctx.unregister(); + registered.current = false; + } + }; + // ctx는 프로바이더 수명 내 안정적 — key 변경 시에만 재바인딩 + }, [ctx, key]), + ); +} + +const styles = StyleSheet.create({ + mask: { + ...StyleSheet.absoluteFillObject, + backgroundColor: colors.primary700, + alignItems: 'center', + justifyContent: 'center', + gap: spacing.xs, + }, + markTop: { color: colors.white, fontSize: 30, fontWeight: '800', letterSpacing: 2 }, + markSub: { color: 'rgba(255,255,255,0.8)', fontSize: type.body.fontSize, fontWeight: '600' }, +}); diff --git a/mobile/lib/api.ts b/mobile/lib/api.ts index ebdfd07..cec7ad7 100644 --- a/mobile/lib/api.ts +++ b/mobile/lib/api.ts @@ -4,6 +4,7 @@ * 웹 프론트(src/frontend/src/api/client.ts) 패턴과 정합. 시크릿 하드코딩 없음. */ import { API_BASE } from './config'; +import { getAttestationToken } from './integrity'; import type { ApiError, ApiErrorCode, ApiResponse } from './types'; /** 봉투 error를 그대로 던지는 오류 타입 — UI는 code로 분기(OTP_REQUIRED·COMPLIANCE_BLOCKED 등). */ @@ -51,6 +52,13 @@ async function request(path: string, opts: RequestOptions = {}): Promise { const headers: Record = { 'Content-Type': 'application/json' }; if (!opts.anonymous && accessToken) headers.Authorization = `Bearer ${accessToken}`; + // B2 앱 무결성 토큰(Play Integrity/App Attest) — 발급기 등록 시에만 첨부(미등록 시 null → 무해). + // 서버 검증 엔드포인트는 backend-dev 인계(_workspace/08_mobile_security_hardening.md). + if (!opts.anonymous) { + const attestation = await getAttestationToken(); + if (attestation) headers['X-Integrity-Token'] = attestation; + } + let res: Response; try { res = await fetch(`${API_BASE}${path}`, { diff --git a/mobile/lib/biometric.ts b/mobile/lib/biometric.ts new file mode 100644 index 0000000..6b1b0db --- /dev/null +++ b/mobile/lib/biometric.ts @@ -0,0 +1,92 @@ +/* + * 생체인식(지문·Face) 로컬 잠금 레이어 — WISE 모바일 패턴 이식. + * 레퍼런스: guardia-messenger/app/uiws/_lib/constants/biometric.ts (문자 그대로 차용). + * + * 성격: 2FA 로그인을 대체하지 않는다 — 이미 로그인된 세션에 대한 "로컬 재인증/잠금" 레이어. + * 방어적 동적 require: expo-local-authentication 미설치 기기에서도 graceful(미지원 취급). + * OS만 생체 데이터를 다루며, 앱은 성공 불린(boolean)만 받는다(생체 원본 미보관). + * pref 키는 kintex 전용 접두어로 분리(WISE `wise_*`와 격리 — CLAUDE.md 토큰키 분리 원칙). + */ +import AsyncStorage from '@react-native-async-storage/async-storage'; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +let LocalAuth: any = null; +try { + // eslint-disable-next-line @typescript-eslint/no-var-requires + LocalAuth = require('expo-local-authentication'); +} catch { + LocalAuth = null; +} + +const PREF_KEY = 'kintex.biometric'; // '1' | '0' + +/** 기기가 생체인증 하드웨어 + OS 등록(지문/Face)을 모두 지원하는지. */ +export async function isBiometricSupported(): Promise { + if (!LocalAuth) return false; + try { + const hasHw = await LocalAuth.hasHardwareAsync(); + const enrolled = await LocalAuth.isEnrolledAsync(); + return !!(hasHw && enrolled); + } catch { + return false; + } +} + +/** 하드웨어만 보유(OS 미등록 포함) — "생체 등록 유도" 안내 노출 판정용. */ +export async function isBiometricHardwareAvailable(): Promise { + if (!LocalAuth) return false; + try { + return !!(await LocalAuth.hasHardwareAsync()); + } catch { + return false; + } +} + +/* + * 세션(콜드 스타트) 단위 잠금 통과 플래그 — 모듈 전역. + * 콜드 스타트(JS 번들 재로드) 시 자연 초기화 → 재실행마다 잠금 1회. + * 포커스마다 반복 프롬프트를 막고, 인증 직후 무한 재프롬프트를 방지한다. + */ +let sessionUnlocked = false; +export function isSessionUnlocked(): boolean { + return sessionUnlocked; +} +export function markSessionUnlocked(): void { + sessionUnlocked = true; +} +export function resetSessionUnlocked(): void { + sessionUnlocked = false; +} + +/** 사용자 토글(pref) 조회 — 기본 비활성('0'/미설정). */ +export async function isBiometricEnabled(): Promise { + try { + return (await AsyncStorage.getItem(PREF_KEY)) === '1'; + } catch { + return false; + } +} + +/** 사용자 토글(pref) 설정. */ +export async function setBiometricEnabled(on: boolean): Promise { + try { + await AsyncStorage.setItem(PREF_KEY, on ? '1' : '0'); + } catch { + /* graceful */ + } +} + +/** 생체 인증 프롬프트. 성공 시 true. 미지원/취소/실패 시 false(생체 원본 미노출). */ +export async function authenticateBiometric(): Promise { + if (!LocalAuth) return false; + try { + const res = await LocalAuth.authenticateAsync({ + promptMessage: 'KINTEX 생체인식 잠금 해제', + cancelLabel: '취소', + disableDeviceFallback: false, + }); + return !!res?.success; + } catch { + return false; + } +} diff --git a/mobile/lib/imagePick.ts b/mobile/lib/imagePick.ts new file mode 100644 index 0000000..f540494 --- /dev/null +++ b/mobile/lib/imagePick.ts @@ -0,0 +1,99 @@ +/* + * 프로필 사진 선택 — expo-image-picker 래퍼(갤러리/카메라, 1:1 크롭). WISE 모바일 패턴 이식. + * 레퍼런스: guardia-messenger/app/uiws/_lib/constants/imagePick.ts. + * 방어적 동적 require: 미설치 시 graceful(null 반환). 권한 요청·거부 처리 포함. + */ + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +let ImagePicker: any = null; +try { + // eslint-disable-next-line @typescript-eslint/no-var-requires + ImagePicker = require('expo-image-picker'); +} catch { + ImagePicker = null; +} + +export interface PickedImage { + uri: string; + fileName: string; + mimeType: string; + /** base64 원본(백엔드 멀티파트 미지원 시 대체 계약용). quality 절충. */ + base64?: string | null; +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function toPicked(result: any): PickedImage | null { + if (!result || result.canceled) return null; + const asset = result.assets?.[0]; + if (!asset?.uri) return null; + const uri: string = asset.uri; + const ext = (uri.split('.').pop() || 'jpg').split('?')[0].toLowerCase(); + const mime = asset.mimeType || (ext === 'png' ? 'image/png' : 'image/jpeg'); + return { + uri, + fileName: asset.fileName || `profile.${ext}`, + mimeType: mime, + base64: asset.base64 ?? null, + }; +} + +export function isPickerAvailable(): boolean { + return !!ImagePicker; +} + +/** 갤러리에서 1:1 크롭 선택. 권한 거부/취소/미설치 시 null. */ +export async function pickFromLibrary(): Promise { + if (!ImagePicker) return null; + try { + const perm = await ImagePicker.requestMediaLibraryPermissionsAsync(); + if (!perm?.granted) return null; + const result = await ImagePicker.launchImageLibraryAsync({ + mediaTypes: ImagePicker.MediaTypeOptions?.Images, + allowsEditing: true, + aspect: [1, 1], + quality: 0.7, + base64: true, + }); + return toPicked(result); + } catch { + return null; + } +} + +/** 카메라 촬영(1:1 크롭). 권한 거부/취소/미설치 시 null. */ +export async function pickFromCamera(): Promise { + if (!ImagePicker) return null; + try { + const perm = await ImagePicker.requestCameraPermissionsAsync(); + if (!perm?.granted) return null; + const result = await ImagePicker.launchCameraAsync({ + allowsEditing: true, + aspect: [1, 1], + quality: 0.7, + base64: true, + }); + return toPicked(result); + } catch { + return null; + } +} + +/** 권한 상태 조회 — 거부 시 안내 문구 분기용(요청 없이 현 상태만). */ +export async function permissionState(): Promise<{ media: boolean; camera: boolean }> { + if (!ImagePicker) return { media: false, camera: false }; + try { + const m = await ImagePicker.getMediaLibraryPermissionsAsync(); + const c = await ImagePicker.getCameraPermissionsAsync(); + return { media: !!m?.granted, camera: !!c?.granted }; + } catch { + return { media: false, camera: false }; + } +} + +/** PickedImage → multipart FormData(field "file"). RN 멀티파트: { uri, name, type }. */ +export function toPhotoFormData(img: PickedImage): FormData { + const fd = new FormData(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + fd.append('file', { uri: img.uri, name: img.fileName, type: img.mimeType } as any); + return fd; +} diff --git a/mobile/lib/integrity.ts b/mobile/lib/integrity.ts new file mode 100644 index 0000000..9a75189 --- /dev/null +++ b/mobile/lib/integrity.ts @@ -0,0 +1,143 @@ +/* + * 기기 무결성/위변조 탐지 레이어 (B1 루트·탈옥 / B9 디버거·개발자모드·USB / B2 앱무결성 토큰 전송부). + * 근거: docs/security/mobile-security-audit.md 백로그 B1·B2·B9, checklist AT-1/AT-4/AT-6/AT-2. + * + * 성격(공공 대민 정책): 탐지 시 "경고 후 제한" — 하드 차단이 아니라 사용자 고지. + * 오탐(에뮬레이터 개발기기·루팅 오검출)으로 정상 이용자를 잠그지 않는다(graceful). + * 도입 경로: jail-monkey는 네이티브 모듈 → Expo Go/웹/미설치 기기에서는 방어적 동적 require로 + * "탐지 불가(안전 가정)" 취급. dev-client/prebuild(EAS)에서 실제 동작. (biometric.ts 동일 패턴) + * 시크릿 미보관: 앱은 불린/토큰만 다루고 원본 서명·키를 저장하지 않는다. + */ +import { useEffect, useState } from 'react'; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +let JailMonkey: any = null; +try { + // eslint-disable-next-line @typescript-eslint/no-var-requires + JailMonkey = require('jail-monkey'); + // 일부 번들러는 default 래핑 → 안전 정규화 + if (JailMonkey && JailMonkey.default) JailMonkey = JailMonkey.default; +} catch { + JailMonkey = null; +} + +export interface IntegrityResult { + /** 네이티브 탐지 모듈 사용 가능 여부(불가 시 아래 값은 모두 안전 기본값 false). */ + available: boolean; + /** 루팅(Android)·탈옥(iOS) 단말. */ + jailBroken: boolean; + /** 디버거 attach(Android). */ + debugger: boolean; + /** 후킹(Frida/Xposed 등, Android). */ + hooking: boolean; + /** USB 디버깅(ADB) 활성(Android). */ + adbEnabled: boolean; + /** 개발자 옵션 활성(Android). */ + devMode: boolean; + /** 위치 모의(mock) 허용. */ + mockLocation: boolean; + /** 종합 위변조 신호(위 항목 중 하나라도 참). */ + compromised: boolean; +} + +const SAFE: IntegrityResult = { + available: false, + jailBroken: false, + debugger: false, + hooking: false, + adbEnabled: false, + devMode: false, + mockLocation: false, + compromised: false, +}; + +/** 동기 불린 호출을 방어적으로 실행(예외/미구현 시 false). */ +function safeBool(fn: (() => boolean) | undefined): boolean { + if (typeof fn !== 'function') return false; + try { + return !!fn(); + } catch { + return false; + } +} + +/** Promise 반환 API를 방어적으로 실행(예외/미구현 시 false). */ +async function safeBoolAsync(fn: (() => Promise) | undefined): Promise { + if (typeof fn !== 'function') return false; + try { + return !!(await fn()); + } catch { + return false; + } +} + +/** + * 기기 무결성 점검. 네이티브 모듈이 없으면 SAFE(모두 false)로 graceful 반환. + * 결과는 UI에서 "경고 배너" 등으로 활용(차단 아님). + */ +export async function checkDeviceIntegrity(): Promise { + if (!JailMonkey) return { ...SAFE }; + try { + const jailBroken = safeBool(JailMonkey.isJailBroken); + const hooking = safeBool(JailMonkey.hookDetected); + const adbEnabled = safeBool(JailMonkey.AdbEnabled); + const mockLocation = safeBool(JailMonkey.canMockLocation); + const debugger_ = await safeBoolAsync(JailMonkey.isDebuggedMode); + const devMode = await safeBoolAsync(JailMonkey.isDevelopmentSettingsMode); + const compromised = jailBroken || hooking || debugger_; + return { + available: true, + jailBroken, + debugger: debugger_, + hooking, + adbEnabled, + devMode, + mockLocation, + compromised, + }; + } catch { + return { ...SAFE }; + } +} + +/** + * React 훅 — 마운트 시 1회 무결성 점검(비차단). 로그인 등 진입 화면에서 경고 노출용. + */ +export function useDeviceIntegrity(): IntegrityResult { + const [result, setResult] = useState(SAFE); + useEffect(() => { + let cancelled = false; + checkDeviceIntegrity().then((r) => { + if (!cancelled) setResult(r); + }); + return () => { + cancelled = true; + }; + }, []); + return result; +} + +// ── B2 앱 무결성 토큰(전송부만) ─────────────────────────────────────────────── +/* + * Play Integrity(Android)·App Attest(iOS) 토큰은 각 플랫폼 네이티브 모듈로 "발급"한다. + * 본 앱은 발급기(provider)를 주입받아 "전송"만 담당한다(api.ts가 헤더로 첨부). + * 서버 검증 엔드포인트는 backend-dev 인계(_workspace/08_mobile_security_hardening.md). + * provider 미등록 시 getAttestationToken()은 null → 헤더 미첨부(무해). + */ +type AttestationProvider = () => Promise; +let attestationProvider: AttestationProvider | null = null; + +/** 네이티브 무결성 토큰 발급기 등록(추후 Play Integrity/App Attest 모듈 연동 시). */ +export function setAttestationProvider(provider: AttestationProvider | null): void { + attestationProvider = provider; +} + +/** 등록된 발급기로 무결성 토큰 조회. 미등록/실패 시 null(graceful). */ +export async function getAttestationToken(): Promise { + if (!attestationProvider) return null; + try { + return await attestationProvider(); + } catch { + return null; + } +} diff --git a/mobile/lib/profile.ts b/mobile/lib/profile.ts new file mode 100644 index 0000000..d4eda6f --- /dev/null +++ b/mobile/lib/profile.ts @@ -0,0 +1,81 @@ +/* + * 내정보(프로필) API 래퍼 — 조회·OTP 상태·아바타 업로드. + * 근거: /api/auth/me(KintexPrincipal)·/api/auth/otp/status(OtpStatusResponse). + * ★ 아바타 업로드/서빙 엔드포인트는 백엔드 미구현 — 제안 계약으로 배선하되 미구현(404/501/네트워크) + * 시 degrade(로컬 프리뷰 유지). 인계 노트: _workspace/mobile_myinfo_avatar_contract.md. + */ +import { API_BASE } from './config'; +import { ApiRequestError, api, getAccessToken } from './api'; +import type { + ApiResponse, + AvatarUploadResponse, + MePrincipal, + OtpStatusDto, +} from './types'; +import type { PickedImage } from './imagePick'; +import { toPhotoFormData } from './imagePick'; + +/** 내 신원·행사별 역할(GET /api/auth/me). */ +export async function getMe(): Promise { + return api.get('/api/auth/me'); +} + +/** 본인 OTP(2차 인증) 상태(GET /api/auth/otp/status). */ +export async function getOtpStatus(): Promise { + return api.get('/api/auth/otp/status'); +} + +/** + * 아바타 서빙 URL(제안 계약: GET /api/auth/users/{userId}/photo). + * 는 axios/fetch 인터셉터를 거치지 않으므로 Avatar 컴포넌트에서 Bearer 헤더를 직접 부여한다. + */ +export function avatarUrl(userId: string): string { + return `${API_BASE}/api/auth/users/${encodeURIComponent(userId)}/photo`; +} + +/** + * 아바타 업로드(제안 계약: POST /api/auth/profile/photo, multipart field "file"). + * 성공 시 photoUrl. 미구현/네트워크 오류는 ApiRequestError로 전파 → 호출부가 degrade 처리. + * 멀티파트는 JSON api 클라이언트를 쓰지 않고 fetch 직접 호출(Content-Type 자동 boundary). + */ +export async function uploadAvatar(img: PickedImage): Promise { + const token = getAccessToken(); + const headers: Record = {}; + if (token) headers.Authorization = `Bearer ${token}`; + // Content-Type은 지정하지 않는다 — RN이 multipart boundary를 자동 설정한다. + + let res: Response; + try { + res = await fetch(`${API_BASE}/api/auth/profile/photo`, { + method: 'POST', + headers, + body: toPhotoFormData(img), + }); + } catch { + throw new ApiRequestError('NETWORK', '네트워크 연결을 확인해 주세요.', 0); + } + + let payload: ApiResponse | null = null; + const text = await res.text(); + if (text) { + try { + payload = JSON.parse(text) as ApiResponse; + } catch { + payload = null; + } + } + + if (!res.ok || (payload && payload.success === false)) { + const err = payload?.error ?? null; + throw new ApiRequestError( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (err?.code as any) ?? (res.status === 404 ? 'NOT_IMPLEMENTED' : 'UNKNOWN'), + err?.message ?? `프로필 사진 업로드를 처리하지 못했습니다. (${res.status})`, + res.status, + ); + } + + if (payload?.data?.photoUrl) return payload.data; + // 봉투는 성공이나 data 부재 — 표준 서빙 URL로 폴백. + return { photoUrl: '' }; +} diff --git a/mobile/lib/screenCapture.ts b/mobile/lib/screenCapture.ts new file mode 100644 index 0000000..9482062 --- /dev/null +++ b/mobile/lib/screenCapture.ts @@ -0,0 +1,44 @@ +/* + * 화면캡처/녹화 차단 래퍼 (B4 · checklist DP-4 / MASVS-PLATFORM-3). + * 근거: docs/security/mobile-security-audit.md 백로그 B4. + * + * expo-screen-capture: + * · Android: FLAG_SECURE 적용 → 스크린샷·화면녹화·최근앱 스냅샷 차단 + * · iOS: 캡처 자체 차단은 불가(OS 제약) → 캡처 "감지"만 지원(별도), 여기서는 Android FLAG_SECURE 목적 + * 방어적 동적 require: 미설치/Expo Go/웹에서 graceful no-op(회귀 방지). + * key 인자로 화면별 스택 관리 → 여러 민감화면 동시/중첩 진입 시 안전 해제. + */ + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +let ScreenCapture: any = null; +try { + // eslint-disable-next-line @typescript-eslint/no-var-requires + ScreenCapture = require('expo-screen-capture'); +} catch { + ScreenCapture = null; +} + +/** 이 기기/빌드에서 캡처 차단 기능을 쓸 수 있는지. */ +export function isScreenCaptureGuardAvailable(): boolean { + return typeof ScreenCapture?.preventScreenCaptureAsync === 'function'; +} + +/** 캡처/녹화 차단 활성(진입 시). key로 화면 스택 구분. */ +export async function preventCapture(key: string): Promise { + if (!isScreenCaptureGuardAvailable()) return; + try { + await ScreenCapture.preventScreenCaptureAsync(key); + } catch { + /* graceful */ + } +} + +/** 캡처/녹화 차단 해제(이탈 시). 진입 시와 동일 key 필요. */ +export async function allowCapture(key: string): Promise { + if (typeof ScreenCapture?.allowScreenCaptureAsync !== 'function') return; + try { + await ScreenCapture.allowScreenCaptureAsync(key); + } catch { + /* graceful */ + } +} diff --git a/mobile/lib/types.ts b/mobile/lib/types.ts index eda20b6..58e4851 100644 --- a/mobile/lib/types.ts +++ b/mobile/lib/types.ts @@ -103,3 +103,25 @@ export interface HealthDto { service: string; time: string; } + +// ── 내정보(SCR-M 프로필) ── +// GET /api/auth/me → KintexPrincipal(백엔드 record). 이메일·부서·전화는 현 계약 미포함(백엔드 인계 대상). +export interface MePrincipal { + userId: string; + displayName: string; + eventRoles: Record; + hallManager: boolean; + roleCode: string | null; + tenantId: string | null; +} + +// GET /api/auth/otp/status → { otpEnabled, verifyMethod }. +export interface OtpStatusDto { + otpEnabled: boolean; + verifyMethod: string; +} + +/** 아바타 업로드 응답(제안 계약 — 백엔드 미구현 시 degrade). */ +export interface AvatarUploadResponse { + photoUrl: string; +} diff --git a/mobile/package-lock.json b/mobile/package-lock.json index 13e9dbf..7ffbd4a 100644 --- a/mobile/package-lock.json +++ b/mobile/package-lock.json @@ -11,14 +11,18 @@ "@expo/vector-icons": "^14.0.3", "@react-native-async-storage/async-storage": "1.23.1", "expo": "^51.0.39", + "expo-build-properties": "~0.12.5", "expo-constants": "~16.0.2", "expo-font": "~12.0.10", "expo-image-picker": "~15.1.0", "expo-linking": "~6.3.1", + "expo-local-authentication": "~14.0.1", "expo-router": "~3.5.23", + "expo-screen-capture": "~6.0.1", "expo-secure-store": "~13.0.2", "expo-splash-screen": "~0.27.7", "expo-status-bar": "~1.12.1", + "jail-monkey": "^2.8.0", "react": "18.2.0", "react-dom": "18.2.0", "react-native": "0.74.5", @@ -7624,6 +7628,31 @@ "expo": "*" } }, + "node_modules/expo-build-properties": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/expo-build-properties/-/expo-build-properties-0.12.5.tgz", + "integrity": "sha512-donC1le0PYfLKCPKRMGQoixuWuwDWCngzXSoQXUPsgHTDHQUKr8aw+lcWkTwZcItgNovcnk784I0dyfYDcxybA==", + "license": "MIT", + "dependencies": { + "ajv": "^8.11.0", + "semver": "^7.6.0" + }, + "peerDependencies": { + "expo": "*" + } + }, + "node_modules/expo-build-properties/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/expo-constants": { "version": "16.0.2", "resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-16.0.2.tgz", @@ -7701,6 +7730,18 @@ "invariant": "^2.2.4" } }, + "node_modules/expo-local-authentication": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/expo-local-authentication/-/expo-local-authentication-14.0.1.tgz", + "integrity": "sha512-kAwUD1wEqj1fhwQgIHlP4H/JV9AcX+NO3BJwhPM2HuCFS0kgx2wvcHisnKBSTRyl8u5Jt4odzMyQkDJystwUTg==", + "license": "MIT", + "dependencies": { + "invariant": "^2.2.4" + }, + "peerDependencies": { + "expo": "*" + } + }, "node_modules/expo-modules-autolinking": { "version": "1.11.3", "resolved": "https://registry.npmjs.org/expo-modules-autolinking/-/expo-modules-autolinking-1.11.3.tgz", @@ -7802,6 +7843,15 @@ } } }, + "node_modules/expo-screen-capture": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/expo-screen-capture/-/expo-screen-capture-6.0.1.tgz", + "integrity": "sha512-rJnwcKxZ6anfEx2xFeRo9MAVp2JgI4S+TuhUurHRf9rildPVorS2JvLey3i5xZPs7CxEe2xcijTmr5WT5Lw4dA==", + "license": "MIT", + "peerDependencies": { + "expo": "*" + } + }, "node_modules/expo-secure-store": { "version": "13.0.2", "resolved": "https://registry.npmjs.org/expo-secure-store/-/expo-secure-store-13.0.2.tgz", @@ -9438,6 +9488,12 @@ "@pkgjs/parseargs": "^0.11.0" } }, + "node_modules/jail-monkey": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/jail-monkey/-/jail-monkey-2.8.5.tgz", + "integrity": "sha512-c91Bsfl18VyCRaoJAxB87ZC27lX23BL0EbHajFQu+h5Sa8OU/yeo33Lu8BplHPejdqxBwL31R//N/zYAb1XOSA==", + "license": "MIT" + }, "node_modules/jest-environment-node": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", diff --git a/mobile/package.json b/mobile/package.json index 2c96c49..e2e64c4 100644 --- a/mobile/package.json +++ b/mobile/package.json @@ -14,13 +14,18 @@ "@expo/vector-icons": "^14.0.3", "@react-native-async-storage/async-storage": "1.23.1", "expo": "^51.0.39", + "expo-build-properties": "~0.12.5", "expo-constants": "~16.0.2", - "expo-image-picker": "~15.0.7", + "expo-font": "~12.0.10", + "expo-image-picker": "~15.1.0", "expo-linking": "~6.3.1", + "expo-local-authentication": "~14.0.1", "expo-router": "~3.5.23", + "expo-screen-capture": "~6.0.1", "expo-secure-store": "~13.0.2", "expo-splash-screen": "~0.27.7", "expo-status-bar": "~1.12.1", + "jail-monkey": "^2.8.0", "react": "18.2.0", "react-dom": "18.2.0", "react-native": "0.74.5", 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 index 64f93a9..2783e17 100644 --- a/src/backend/src/main/java/com/zioinfo/kintex/auth/AuthController.java +++ b/src/backend/src/main/java/com/zioinfo/kintex/auth/AuthController.java @@ -3,6 +3,7 @@ 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.MeResponse; import com.zioinfo.kintex.auth.dto.WorkspaceDto; import com.zioinfo.kintex.common.ApiResponse; import jakarta.validation.Valid; @@ -45,10 +46,13 @@ public class AuthController { return ApiResponse.ok(authService.acceptInvite(principal, request)); } - /** GET /api/auth/me — 현재 토큰의 신원·행사별 역할 요약(디버그·프론트 부트스트랩). */ + /** + * GET /api/auth/me — 현재 토큰의 신원·행사별 역할 + 프로필(email·phone·deptId·photoUrl) 순증. + * 기존 principal 필드의 상위집합이라 웹 부트스트랩 회귀 0(계약: {@link MeResponse}). + */ @GetMapping("/me") - public ApiResponse me(@AuthenticationPrincipal KintexPrincipal principal) { + public ApiResponse me(@AuthenticationPrincipal KintexPrincipal principal) { guard.require(principal); - return ApiResponse.ok(principal); + return ApiResponse.ok(authService.me(principal)); } } 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 index e6318a0..5d56c5b 100644 --- a/src/backend/src/main/java/com/zioinfo/kintex/auth/AuthService.java +++ b/src/backend/src/main/java/com/zioinfo/kintex/auth/AuthService.java @@ -3,6 +3,7 @@ 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.MeResponse; import com.zioinfo.kintex.auth.dto.WorkspaceDto; import java.util.List; @@ -13,6 +14,9 @@ public interface AuthService { /** 이메일/비밀번호 검증 → JWT 발급 + 참여 행사 목록. */ LoginResponse login(LoginRequest request); + /** 현재 사용자 신원 + 프로필(email·phone·deptId·photoUrl) 순증. 웹/모바일 부트스트랩. */ + MeResponse me(KintexPrincipal principal); + /** 현재 사용자의 참여 행사 워크스페이스 목록(SCR-01 카드 그리드). */ List listWorkspaces(KintexPrincipal principal); 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 index d653238..7e38c6b 100644 --- a/src/backend/src/main/java/com/zioinfo/kintex/auth/AuthServiceImpl.java +++ b/src/backend/src/main/java/com/zioinfo/kintex/auth/AuthServiceImpl.java @@ -3,8 +3,10 @@ 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.MeResponse; import com.zioinfo.kintex.auth.dto.WorkspaceDto; import com.zioinfo.kintex.auth.mapper.UserMapper; +import com.zioinfo.kintex.auth.profile.ProfilePhotoService; import com.zioinfo.kintex.common.error.ApiException; import com.zioinfo.kintex.common.error.ErrorCode; import com.zioinfo.kintex.security.TwoFactorService; @@ -48,6 +50,20 @@ public class AuthServiceImpl implements AuthService { return secured.login(); } + @Override + public MeResponse me(KintexPrincipal principal) { + Map p = userMapper.findProfile(principal.userId()); + String email = p == null ? null : str(p.get("email")); + String phone = p == null ? null : str(p.get("phone")); + String deptId = p == null ? null : str(p.get("deptId")); + boolean hasPhoto = p != null && Boolean.TRUE.equals(bool(p.get("hasPhoto"))); + String photoUrl = hasPhoto ? ProfilePhotoService.serveUrl(principal.userId()) : null; + return new MeResponse( + principal.userId(), principal.displayName(), principal.eventRoles(), + principal.hallManager(), principal.roleCode(), principal.tenantId(), + email, phone, deptId, photoUrl); + } + @Override public List listWorkspaces(KintexPrincipal principal) { return loadWorkspaces(principal.userId()); diff --git a/src/backend/src/main/java/com/zioinfo/kintex/auth/dto/MeResponse.java b/src/backend/src/main/java/com/zioinfo/kintex/auth/dto/MeResponse.java new file mode 100644 index 0000000..516816b --- /dev/null +++ b/src/backend/src/main/java/com/zioinfo/kintex/auth/dto/MeResponse.java @@ -0,0 +1,27 @@ +package com.zioinfo.kintex.auth.dto; + +import com.zioinfo.kintex.auth.EventRole; + +import java.util.Map; + +/** + * {@code GET /api/auth/me} 응답 — 기존 {@code KintexPrincipal} 필드의 상위집합(순증). + * 웹은 기존 필드(userId·displayName·eventRoles·hallManager·roleCode·tenantId)를 그대로 읽고(회귀 0), + * 모바일 {@code MePrincipal} 은 email·phone·deptId·photoUrl 순증 필드를 표시에 사용한다. + * 민감 컬럼(password_hash·otp_secret)은 절대 포함하지 않는다(계약 §0-3). + * + * @param photoUrl 프로필 사진이 있으면 서빙 URL, 없으면 null(모바일 이니셜 폴백). + */ +public record MeResponse( + String userId, + String displayName, + Map eventRoles, + boolean hallManager, + String roleCode, + String tenantId, + String email, + String phone, + String deptId, + String photoUrl +) { +} diff --git a/src/backend/src/main/java/com/zioinfo/kintex/auth/integrity/AppIntegrityController.java b/src/backend/src/main/java/com/zioinfo/kintex/auth/integrity/AppIntegrityController.java new file mode 100644 index 0000000..042dcc6 --- /dev/null +++ b/src/backend/src/main/java/com/zioinfo/kintex/auth/integrity/AppIntegrityController.java @@ -0,0 +1,38 @@ +package com.zioinfo.kintex.auth.integrity; + +import com.zioinfo.kintex.auth.EventAccessGuard; +import com.zioinfo.kintex.auth.KintexPrincipal; +import com.zioinfo.kintex.auth.integrity.dto.AppIntegrityRequest; +import com.zioinfo.kintex.auth.integrity.dto.AppIntegrityResponse; +import com.zioinfo.kintex.common.ApiResponse; +import jakarta.validation.Valid; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * B2 — 앱 무결성 검증 엔드포인트(골격). 인증 필요(SecurityConfig anyRequest().authenticated()). + * {@code POST /api/auth/app-integrity/verify} — 모바일이 Play Integrity/App Attest 토큰을 첨부. + * 온프레미스 외부 API 금지로 실검증은 스텁(mode=STUB) — {@link AppIntegrityService} 참조. + */ +@RestController +@RequestMapping("/api/auth/app-integrity") +public class AppIntegrityController { + + private final AppIntegrityService service; + private final EventAccessGuard guard; + + public AppIntegrityController(AppIntegrityService service, EventAccessGuard guard) { + this.service = service; + this.guard = guard; + } + + @PostMapping("/verify") + public ApiResponse verify(@AuthenticationPrincipal KintexPrincipal principal, + @Valid @RequestBody AppIntegrityRequest req) { + guard.require(principal); + return ApiResponse.ok(service.verify(principal.userId(), req)); + } +} diff --git a/src/backend/src/main/java/com/zioinfo/kintex/auth/integrity/AppIntegrityService.java b/src/backend/src/main/java/com/zioinfo/kintex/auth/integrity/AppIntegrityService.java new file mode 100644 index 0000000..8bca7f6 --- /dev/null +++ b/src/backend/src/main/java/com/zioinfo/kintex/auth/integrity/AppIntegrityService.java @@ -0,0 +1,46 @@ +package com.zioinfo.kintex.auth.integrity; + +import com.zioinfo.kintex.auth.integrity.dto.AppIntegrityRequest; +import com.zioinfo.kintex.auth.integrity.dto.AppIntegrityResponse; +import com.zioinfo.kintex.common.error.ApiException; +import com.zioinfo.kintex.common.error.ErrorCode; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +import java.util.Locale; +import java.util.Set; + +/** + * B2 — 앱 무결성(위변조 방지) 서버 검증 골격. + * + *

실제 Play Integrity(Google)/App Attest(Apple) 토큰 검증은 각 플랫폼 서버로의 외부 호출이 + * 필요하다. 온프레미스 외부 API 금지 제약(보안 불변) 하에서는 여기서 실검증을 수행하지 않는다 — + * 요청 형식만 검증하고 {@code mode=STUB, attested=false} 로 안전하게 반환한다. + * 모바일은 이 결과로 로그인/기능을 차단하지 않는다(감사 문서 B2: 인계 골격). + * + *

운영에서 검증기를 붙일 때 진입점(TODO): (1) Android — Google Play Integrity API로 토큰 복호·검증 + * (앱 패키지/서명 지문/nonce 대조), (2) iOS — App Attest 공개키·nonce·receipt 검증. 검증 결과만 + * boolean 으로 반환하고 토큰 원문은 저장/로그 금지(계약 §0-3). + */ +@Service +public class AppIntegrityService { + + private static final Logger log = LoggerFactory.getLogger(AppIntegrityService.class); + private static final Set PLATFORMS = Set.of("ANDROID", "IOS"); + private static final int MAX_TOKEN_LEN = 8192; // 무결성 토큰 길이 상한(방어적) + + public AppIntegrityResponse verify(String userId, AppIntegrityRequest req) { + String platform = req.platform() == null ? "" : req.platform().trim().toUpperCase(Locale.ROOT); + if (!PLATFORMS.contains(platform)) { + throw new ApiException(ErrorCode.VALIDATION, "platform 은 ANDROID 또는 IOS 여야 합니다."); + } + if (req.token() == null || req.token().isBlank() || req.token().length() > MAX_TOKEN_LEN) { + throw new ApiException(ErrorCode.VALIDATION, "무결성 토큰이 올바르지 않습니다."); + } + // 토큰 원문은 로깅하지 않는다 — 수신 사실만 기록(사용자 식별자도 마스킹된 감사에 위임). + log.info("[integrity] attestation received platform={} (stub verifier, no external call)", platform); + return new AppIntegrityResponse(false, "STUB", + "무결성 검증기 미구성(온프레미스) — 서버 검증 골격만 동작합니다. 접근은 차단하지 않습니다."); + } +} diff --git a/src/backend/src/main/java/com/zioinfo/kintex/auth/integrity/dto/AppIntegrityRequest.java b/src/backend/src/main/java/com/zioinfo/kintex/auth/integrity/dto/AppIntegrityRequest.java new file mode 100644 index 0000000..c9c8aa8 --- /dev/null +++ b/src/backend/src/main/java/com/zioinfo/kintex/auth/integrity/dto/AppIntegrityRequest.java @@ -0,0 +1,17 @@ +package com.zioinfo.kintex.auth.integrity.dto; + +import jakarta.validation.constraints.NotBlank; + +/** + * 앱 무결성(위변조 방지) 검증 요청 — 모바일이 Play Integrity(Android)/App Attest(iOS) 토큰을 첨부. + * + * @param platform ANDROID | IOS + * @param token 플랫폼 발급 무결성 토큰(불투명). 서버 로그에 원문 저장/노출 금지. + * @param nonce (선택) 재생공격 방지용 서버발급 nonce 에코. + */ +public record AppIntegrityRequest( + @NotBlank String platform, + @NotBlank String token, + String nonce +) { +} diff --git a/src/backend/src/main/java/com/zioinfo/kintex/auth/integrity/dto/AppIntegrityResponse.java b/src/backend/src/main/java/com/zioinfo/kintex/auth/integrity/dto/AppIntegrityResponse.java new file mode 100644 index 0000000..5f2a45c --- /dev/null +++ b/src/backend/src/main/java/com/zioinfo/kintex/auth/integrity/dto/AppIntegrityResponse.java @@ -0,0 +1,11 @@ +package com.zioinfo.kintex.auth.integrity.dto; + +/** + * 앱 무결성 검증 결과. + * + * @param attested 무결성 검증 통과 여부. {@code mode=STUB} 에서는 항상 false(실검증 미구성). + * @param mode STUB(서버 검증기 미구성·온프레미스 폴백) | LIVE(플랫폼 검증 구성 완료). + * @param reason 판정 사유(사용자/클라 안내용 요약, 민감정보 없음). + */ +public record AppIntegrityResponse(boolean attested, String mode, String reason) { +} 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 index 369411d..aed8bdc 100644 --- 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 @@ -24,4 +24,15 @@ public interface UserMapper { * 장치업체(CONTRACTOR) 초대 수락 게이트(계약: NOT_REGISTERED_COMPANY). 기존 company 테이블 read-only. */ Boolean findCompanyRegistered(@Param("registrationNo") String registrationNo); + + /** 내정보 표시용 프로필(email·phone·deptId + 사진 유무). password_hash·otp_secret 절대 select 금지. */ + Map findProfile(@Param("userId") String userId); + + /** 프로필 사진 메타(photoPath·photoContentType) 조회 — 서빙 엔드포인트용. 없으면 null. */ + Map findPhotoMeta(@Param("userId") String userId); + + /** 프로필 사진 메타 갱신(본인만 호출됨 — 서비스에서 principal.userId 강제). 영향 행수 반환. */ + int updatePhoto(@Param("userId") String userId, + @Param("photoPath") String photoPath, + @Param("photoContentType") String photoContentType); } diff --git a/src/backend/src/main/java/com/zioinfo/kintex/auth/profile/ProfilePhotoController.java b/src/backend/src/main/java/com/zioinfo/kintex/auth/profile/ProfilePhotoController.java new file mode 100644 index 0000000..2e0aa04 --- /dev/null +++ b/src/backend/src/main/java/com/zioinfo/kintex/auth/profile/ProfilePhotoController.java @@ -0,0 +1,70 @@ +package com.zioinfo.kintex.auth.profile; + +import com.zioinfo.kintex.auth.EventAccessGuard; +import com.zioinfo.kintex.auth.KintexPrincipal; +import com.zioinfo.kintex.auth.profile.dto.PhotoBase64Request; +import com.zioinfo.kintex.auth.profile.dto.PhotoUploadResponse; +import com.zioinfo.kintex.common.ApiResponse; +import com.zioinfo.kintex.common.audit.Audited; +import jakarta.validation.Valid; +import org.springframework.http.CacheControl; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import java.time.Duration; + +/** + * 프로필 사진 API — 모바일 내정보(SCR-M) 아바타 계약(단일 창구). + *

    + *
  • 업로드: {@code POST /api/auth/profile/photo} (본인 · multipart {@code file} 또는 base64 JSON) → {@code { photoUrl }}
  • + *
  • 서빙: {@code GET /api/auth/users/{userId}/photo} (인증 · 없으면 404) — 바이트 스트림 + 캐시 헤더
  • + *
+ * 인증 경로는 SecurityConfig 의 {@code anyRequest().authenticated()} 로 보호된다(별도 permitAll 없음). + */ +@RestController +@RequestMapping("/api/auth") +public class ProfilePhotoController { + + private final ProfilePhotoService service; + private final EventAccessGuard guard; + + public ProfilePhotoController(ProfilePhotoService service, EventAccessGuard guard) { + this.service = service; + this.guard = guard; + } + + /** 업로드(멀티파트) — 본인 사진만. userId 는 항상 principal 에서 온다(경로/폼 파라미터 미수용). */ + @Audited(action = "PROFILE_PHOTO_UPLOAD", targetType = "app_user") + @PostMapping(value = "/profile/photo", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + public ApiResponse upload(@AuthenticationPrincipal KintexPrincipal principal, + @RequestPart("file") MultipartFile file) { + guard.require(principal); + String url = service.upload(principal.userId(), file); + return ApiResponse.ok(new PhotoUploadResponse(url)); + } + + /** 업로드(base64 JSON 대체안) — 멀티파트 곤란 단말용. */ + @Audited(action = "PROFILE_PHOTO_UPLOAD", targetType = "app_user") + @PostMapping(value = "/profile/photo", consumes = MediaType.APPLICATION_JSON_VALUE) + public ApiResponse uploadBase64(@AuthenticationPrincipal KintexPrincipal principal, + @Valid @RequestBody PhotoBase64Request req) { + guard.require(principal); + String url = service.uploadBase64(principal.userId(), req.base64(), req.mimeType()); + return ApiResponse.ok(new PhotoUploadResponse(url)); + } + + /** 서빙 — 인증 사용자면 조회 가능(멤버 목록·아바타 표시). 없으면 404(이니셜 폴백). */ + @GetMapping("/users/{userId}/photo") + public ResponseEntity photo(@AuthenticationPrincipal KintexPrincipal principal, + @PathVariable String userId) { + guard.require(principal); + ProfilePhotoService.StoredPhoto photo = service.load(userId); + return ResponseEntity.ok() + .contentType(MediaType.parseMediaType(photo.contentType())) + .cacheControl(CacheControl.maxAge(Duration.ofHours(1)).cachePrivate()) + .body(photo.bytes()); + } +} diff --git a/src/backend/src/main/java/com/zioinfo/kintex/auth/profile/ProfilePhotoService.java b/src/backend/src/main/java/com/zioinfo/kintex/auth/profile/ProfilePhotoService.java new file mode 100644 index 0000000..5485480 --- /dev/null +++ b/src/backend/src/main/java/com/zioinfo/kintex/auth/profile/ProfilePhotoService.java @@ -0,0 +1,221 @@ +package com.zioinfo.kintex.auth.profile; + +import com.zioinfo.kintex.auth.mapper.UserMapper; +import com.zioinfo.kintex.common.error.ApiException; +import com.zioinfo.kintex.common.error.ErrorCode; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.util.Arrays; +import java.util.Locale; +import java.util.Map; +import java.util.UUID; + +/** + * 프로필 사진 업로드/서빙 서비스 — 모바일 내정보 아바타 계약. + * + *

저장 컨벤션은 로그인 슬라이드와 동일(파일시스템 {@code {kintex.upload.dir}/avatars/} + 서버 UUID 파일명). + * 단, 서빙은 정적 /uploads 가 아니라 인증 엔드포인트({@code GET /api/auth/users/{id}/photo})로 바이트 스트림한다 + * (RN {@code } 가 Authorization 헤더를 직접 부여하는 계약). photo_path 는 서버 UUID 만 저장 — + * 원본 파일명·클라이언트 경로는 신뢰하지 않으므로 경로 순회는 구조적으로 불가능하다. + * + *

검증(다층): ①콘텐츠타입 화이트리스트 ②크기 상한 ③매직바이트(실제 파일 시그니처) — 확장자/헤더 위조로 + * SVG·실행형(스크립트) 업로드를 차단한다. + */ +@Service +public class ProfilePhotoService { + + private static final Logger log = LoggerFactory.getLogger(ProfilePhotoService.class); + + /** 아바타 크기 상한(5MB) — 모바일은 1:1 크롭·quality 0.7로 축소 전송. */ + private static final long MAX_IMAGE_BYTES = 5L * 1024 * 1024; + + /** 허용 MIME → 확장자. SVG·gif 등은 화이트리스트 밖(실행형/애니 차단). */ + private static final Map ALLOWED_TYPES = Map.of( + "image/png", "png", + "image/jpeg", "jpg", + "image/webp", "webp"); + + private final UserMapper userMapper; + private final Path avatarDir; + + public ProfilePhotoService(UserMapper userMapper, + @Value("${kintex.upload.dir:./data/uploads}") String uploadDir) { + this.userMapper = userMapper; + this.avatarDir = Paths.get(uploadDir).toAbsolutePath().normalize().resolve("avatars"); + } + + /** 서빙 URL(캐시버스터는 모바일이 부착). {@code GET /api/auth/users/{userId}/photo} 와 동일 경로. */ + public static String serveUrl(String userId) { + return "/api/auth/users/" + userId + "/photo"; + } + + // ── 업로드(멀티파트) ────────────────────────────────────────────── + + /** 본인 프로필 사진 업로드 → 서빙 URL 반환. userId 는 principal 에서만 온다(본인 강제). */ + public String upload(String userId, MultipartFile file) { + if (file == null || file.isEmpty()) { + throw new ApiException(ErrorCode.VALIDATION, "이미지 파일을 첨부해 주세요."); + } + byte[] bytes; + try { + bytes = file.getBytes(); + } catch (IOException e) { + log.warn("avatar read failed: {}", e.getMessage()); + throw new ApiException(ErrorCode.VALIDATION, "이미지를 읽을 수 없습니다."); + } + return storeBytes(userId, bytes, file.getContentType()); + } + + // ── 업로드(base64 JSON 대체안) ─────────────────────────────────── + + /** base64 페이로드 업로드(멀티파트 곤란 단말 대체). data-URL prefix 는 서비스에서 제거한다. */ + public String uploadBase64(String userId, String base64, String mimeType) { + if (base64 == null || base64.isBlank()) { + throw new ApiException(ErrorCode.VALIDATION, "이미지 데이터를 전달해 주세요."); + } + String payload = base64; + int comma = payload.indexOf(','); // "data:image/png;base64,...." 형태 허용 + if (payload.startsWith("data:") && comma > 0) { + payload = payload.substring(comma + 1); + } + byte[] bytes; + try { + bytes = java.util.Base64.getDecoder().decode(payload.trim()); + } catch (IllegalArgumentException e) { + throw new ApiException(ErrorCode.VALIDATION, "이미지 데이터 형식이 올바르지 않습니다."); + } + return storeBytes(userId, bytes, mimeType); + } + + private String storeBytes(String userId, byte[] bytes, String declaredType) { + if (bytes.length == 0) { + throw new ApiException(ErrorCode.VALIDATION, "빈 이미지입니다."); + } + if (bytes.length > MAX_IMAGE_BYTES) { + throw new ApiException(ErrorCode.VALIDATION, "이미지는 5MB 이하만 업로드할 수 있습니다."); + } + // 매직바이트로 실제 유형 확정(선언 타입은 참고만) — SVG/스크립트/위조 헤더 차단. + String detected = detectImageType(bytes); + if (detected == null) { + throw new ApiException(ErrorCode.VALIDATION, "PNG/JPG/WebP 이미지만 업로드할 수 있습니다."); + } + // 선언 타입이 있으면 화이트리스트 여부만 확인(불일치는 매직바이트를 신뢰). + String normalizedDeclared = declaredType == null ? null : declaredType.toLowerCase(Locale.ROOT).trim(); + if (normalizedDeclared != null && !normalizedDeclared.isBlank() + && !ALLOWED_TYPES.containsKey(normalizedDeclared)) { + throw new ApiException(ErrorCode.VALIDATION, "PNG/JPG/WebP 이미지만 업로드할 수 있습니다."); + } + + String contentType = mimeFor(detected); + String fileName = UUID.randomUUID() + "." + detected; + try { + Files.createDirectories(avatarDir); + Path target = avatarDir.resolve(fileName).normalize(); + if (!target.startsWith(avatarDir)) { // 파일명이 UUID라 사실상 불변식이나 방어적 확인 + throw new ApiException(ErrorCode.VALIDATION, "잘못된 파일 경로입니다."); + } + Files.write(target, bytes); + // 이전 사진 정리(교체) — 실패는 무해(로그만). + Map prev = userMapper.findPhotoMeta(userId); + userMapper.updatePhoto(userId, fileName, contentType); + if (prev != null) { + removeQuietly(str(prev.get("photoPath"))); + } + } catch (IOException e) { + log.error("avatar store failed for user (masked): {}", e.getMessage()); + throw new ApiException(ErrorCode.INTERNAL, "이미지 저장에 실패했습니다."); + } + return serveUrl(userId); + } + + // ── 서빙 ───────────────────────────────────────────────────────── + + /** 사진 바이트 + Content-Type. 없으면 404(모바일 이니셜 폴백). */ + public StoredPhoto load(String userId) { + Map meta = userMapper.findPhotoMeta(userId); + if (meta == null) { + throw new ApiException(ErrorCode.NOT_FOUND, "등록된 프로필 사진이 없습니다."); + } + String fileName = str(meta.get("photoPath")); + if (fileName == null || fileName.isBlank() || !isSafeName(fileName)) { + throw new ApiException(ErrorCode.NOT_FOUND, "등록된 프로필 사진이 없습니다."); + } + Path target = avatarDir.resolve(fileName).normalize(); + if (!target.startsWith(avatarDir) || !Files.isRegularFile(target)) { + throw new ApiException(ErrorCode.NOT_FOUND, "등록된 프로필 사진이 없습니다."); + } + try { + byte[] data = Files.readAllBytes(target); + String ct = str(meta.get("photoContentType")); + return new StoredPhoto(data, ct == null || ct.isBlank() ? "application/octet-stream" : ct); + } catch (IOException e) { + log.warn("avatar load failed: {}", e.getMessage()); + throw new ApiException(ErrorCode.NOT_FOUND, "등록된 프로필 사진이 없습니다."); + } + } + + // ── 검증 헬퍼 ──────────────────────────────────────────────────── + + /** 매직바이트로 png/jpg/webp 판별. 아니면 null(SVG·스크립트·위조 헤더 차단). 순수 정적 헬퍼. */ + public static String detectImageType(byte[] b) { + if (b.length >= 8 && (b[0] & 0xFF) == 0x89 && b[1] == 0x50 && b[2] == 0x4E && b[3] == 0x47 + && b[4] == 0x0D && b[5] == 0x0A && b[6] == 0x1A && b[7] == 0x0A) { + return "png"; + } + if (b.length >= 3 && (b[0] & 0xFF) == 0xFF && (b[1] & 0xFF) == 0xD8 && (b[2] & 0xFF) == 0xFF) { + return "jpg"; + } + // WEBP: "RIFF"...."WEBP" + if (b.length >= 12 && b[0] == 'R' && b[1] == 'I' && b[2] == 'F' && b[3] == 'F' + && b[8] == 'W' && b[9] == 'E' && b[10] == 'B' && b[11] == 'P') { + return "webp"; + } + return null; + } + + private static String mimeFor(String ext) { + return switch (ext) { + case "png" -> "image/png"; + case "webp" -> "image/webp"; + default -> "image/jpeg"; + }; + } + + private static boolean isSafeName(String name) { + return !name.contains("/") && !name.contains("\\") && !name.contains(".."); + } + + private void removeQuietly(String fileName) { + if (fileName == null || fileName.isBlank() || !isSafeName(fileName)) { + return; + } + try { + Files.deleteIfExists(avatarDir.resolve(fileName)); + } catch (IOException e) { + log.warn("avatar cleanup failed: {}", e.getMessage()); + } + } + + private static String str(Object o) { + return o == null ? null : String.valueOf(o); + } + + /** 서빙 바이트 컨테이너. */ + public record StoredPhoto(byte[] bytes, String contentType) { + public StoredPhoto { + bytes = bytes == null ? new byte[0] : Arrays.copyOf(bytes, bytes.length); + } + public byte[] bytes() { + return Arrays.copyOf(bytes, bytes.length); + } + } +} diff --git a/src/backend/src/main/java/com/zioinfo/kintex/auth/profile/dto/PhotoBase64Request.java b/src/backend/src/main/java/com/zioinfo/kintex/auth/profile/dto/PhotoBase64Request.java new file mode 100644 index 0000000..558b826 --- /dev/null +++ b/src/backend/src/main/java/com/zioinfo/kintex/auth/profile/dto/PhotoBase64Request.java @@ -0,0 +1,10 @@ +package com.zioinfo.kintex.auth.profile.dto; + +import jakarta.validation.constraints.NotBlank; + +/** 아바타 업로드(base64 JSON 대체안) — 멀티파트 곤란 단말용. data-URL prefix 허용. */ +public record PhotoBase64Request( + @NotBlank String base64, + String mimeType +) { +} diff --git a/src/backend/src/main/java/com/zioinfo/kintex/auth/profile/dto/PhotoUploadResponse.java b/src/backend/src/main/java/com/zioinfo/kintex/auth/profile/dto/PhotoUploadResponse.java new file mode 100644 index 0000000..a0609e6 --- /dev/null +++ b/src/backend/src/main/java/com/zioinfo/kintex/auth/profile/dto/PhotoUploadResponse.java @@ -0,0 +1,5 @@ +package com.zioinfo.kintex.auth.profile.dto; + +/** 아바타 업로드 응답 — 서빙 URL. 모바일은 캐시버스터(?_t=...)를 부착해 사용한다. */ +public record PhotoUploadResponse(String photoUrl) { +} diff --git a/src/backend/src/main/java/com/zioinfo/kintex/config/SecretStartupValidator.java b/src/backend/src/main/java/com/zioinfo/kintex/config/SecretStartupValidator.java new file mode 100644 index 0000000..0346870 --- /dev/null +++ b/src/backend/src/main/java/com/zioinfo/kintex/config/SecretStartupValidator.java @@ -0,0 +1,82 @@ +package com.zioinfo.kintex.config; + +import com.zioinfo.kintex.auth.AuthProperties; +import com.zioinfo.kintex.security.SecurityPolicyProperties; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.core.env.Environment; +import org.springframework.stereotype.Component; + +import java.util.Arrays; + +/** + * B12 — 운영 시크릿 fail-fast. 운영 프로파일에서 JWT 서명 키/OTP 암호화 키가 개발 폴백이거나 + * 미주입이면 컨텍스트 초기화 단계에서 기동을 중단한다(개발 폴백 값의 운영 유입 차단). + * 개발 프로파일에서는 경고만 남긴다(로컬 편의 유지). + * + *

보안 불변(계약 §0-3): 시크릿 은 로그·예외 메시지에 절대 담지 않는다 — 항목명과 조치만 노출한다. + * 운영 판정: 활성 프로파일에 {@code prod} 포함 또는 {@code kintex.security.enforce-secrets=true}. + */ +@Component +public class SecretStartupValidator implements InitializingBean { + + private static final Logger log = LoggerFactory.getLogger(SecretStartupValidator.class); + + /** application.yml 개발 폴백 접두어(env 미주입 시 주입되는 값). */ + private static final String DEV_JWT_PREFIX = "CHANGE_ME_DEV_ONLY"; + + private final Environment env; + private final AuthProperties authProperties; + private final SecurityPolicyProperties securityProperties; + + public SecretStartupValidator(Environment env, AuthProperties authProperties, + SecurityPolicyProperties securityProperties) { + this.env = env; + this.authProperties = authProperties; + this.securityProperties = securityProperties; + } + + @Override + public void afterPropertiesSet() { + boolean production = isProduction(); + + boolean jwtWeak = isBlank(authProperties.getJwtSecret()) + || authProperties.getJwtSecret().startsWith(DEV_JWT_PREFIX); + boolean otpKeyMissing = isBlank(securityProperties.getOtp().getEncKey()); + + if (production) { + if (jwtWeak) { + throw new IllegalStateException( + "운영 기동 중단(fail-fast): JWT_SECRET 이 미주입 또는 개발 폴백값입니다. " + + "최소 32바이트 시크릿을 환경변수 JWT_SECRET 로 주입하세요."); + } + if (otpKeyMissing) { + throw new IllegalStateException( + "운영 기동 중단(fail-fast): KINTEX_OTP_ENC_KEY(OTP AES-256-GCM 키)가 미주입입니다. " + + "base64 32바이트 키를 환경변수 KINTEX_OTP_ENC_KEY 로 주입하세요."); + } + log.info("[security] 운영 시크릿 검증 통과 — JWT_SECRET·KINTEX_OTP_ENC_KEY 주입 확인."); + } else { + if (jwtWeak) { + log.warn("[security] JWT_SECRET 이 개발 폴백/미주입 상태입니다 — 운영 배포 전 env 주입 필수(운영 프로파일은 fail-fast)."); + } + if (otpKeyMissing) { + log.warn("[security] KINTEX_OTP_ENC_KEY 미주입 — 개발 파생키 사용 중(운영 프로파일은 fail-fast)."); + } + } + } + + /** 활성 프로파일에 prod 포함 또는 명시 강제 플래그. */ + private boolean isProduction() { + if (Boolean.parseBoolean(env.getProperty("kintex.security.enforce-secrets", "false"))) { + return true; + } + return Arrays.stream(env.getActiveProfiles()) + .anyMatch(p -> p != null && p.toLowerCase().contains("prod")); + } + + private static boolean isBlank(String s) { + return s == null || s.isBlank(); + } +} 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 index c90e1a7..0579eae 100644 --- a/src/backend/src/main/java/com/zioinfo/kintex/config/SecurityConfig.java +++ b/src/backend/src/main/java/com/zioinfo/kintex/config/SecurityConfig.java @@ -8,6 +8,7 @@ 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.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.MediaType; @@ -18,6 +19,12 @@ 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; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; + +import java.util.ArrayList; +import java.util.List; /** * 보안 설정 — 무상태 JWT. 공개 경로: /health, 로그인, WebSocket 핸드셰이크. @@ -26,13 +33,21 @@ import org.springframework.security.web.authentication.UsernamePasswordAuthentic @Configuration public class SecurityConfig { + /** + * CORS 허용 오리진 화이트리스트(env {@code CORS_ALLOWED_ORIGINS}, 콤마구분). 와일드카드('*') 금지 — + * 값에 '*'가 있으면 무시하고 로컬 개발 오리진만 허용한다(운영 오리진은 반드시 명시 주입). + */ + @Value("${kintex.cors.allowed-origins:http://localhost:5173,http://localhost:3000}") + private List allowedOrigins; + @Bean public SecurityFilterChain filterChain(HttpSecurity http, JwtService jwtService, TenantResolver tenantResolver, - ObjectMapper objectMapper) throws Exception { + ObjectMapper objectMapper, + CorsConfigurationSource corsConfigurationSource) throws Exception { http .csrf(AbstractHttpConfigurer::disable) - .cors(cors -> {}) + .cors(cors -> cors.configurationSource(corsConfigurationSource)) .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(auth -> auth .requestMatchers("/health", "/api/auth/login", "/ws/**", @@ -63,6 +78,39 @@ public class SecurityConfig { return http.build(); } + /** + * B11 — CORS 명시(화이트리스트). 와일드카드('*') 오리진은 제거하고 명시 오리진만 허용한다. + * 인증은 Bearer JWT(쿠키 미사용)이므로 {@code allowCredentials=false} — 자격증명 쿠키 반사 위험을 원천 차단한다. + * 허용 헤더는 Authorization·Content-Type·테넌트 헤더로 최소화한다. + */ + @Bean + public CorsConfigurationSource corsConfigurationSource() { + List origins = new ArrayList<>(); + if (allowedOrigins != null) { + for (String o : allowedOrigins) { + if (o == null) { + continue; + } + String v = o.trim(); + // 와일드카드는 금지(운영 오리진 명시 강제) — '*' 단독/포함 값은 무시한다. + if (!v.isEmpty() && !v.contains("*")) { + origins.add(v); + } + } + } + CorsConfiguration cfg = new CorsConfiguration(); + cfg.setAllowedOrigins(origins); + cfg.setAllowedMethods(List.of("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS")); + cfg.setAllowedHeaders(List.of("Authorization", "Content-Type", "X-Tenant-Id", "X-Requested-With")); + cfg.setExposedHeaders(List.of("Content-Disposition")); + cfg.setAllowCredentials(false); + cfg.setMaxAge(3600L); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", cfg); + return source; + } + @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); diff --git a/src/backend/src/main/java/com/zioinfo/kintex/publicsite/PublicGuideController.java b/src/backend/src/main/java/com/zioinfo/kintex/publicsite/PublicGuideController.java new file mode 100644 index 0000000..e48dded --- /dev/null +++ b/src/backend/src/main/java/com/zioinfo/kintex/publicsite/PublicGuideController.java @@ -0,0 +1,61 @@ +package com.zioinfo.kintex.publicsite; + +import com.zioinfo.kintex.common.ApiResponse; +import com.zioinfo.kintex.publicsite.dto.PublicGuideDtos.MonthlySummaryDto; +import com.zioinfo.kintex.publicsite.dto.PublicGuideDtos.PublicCalendarEventDto; +import com.zioinfo.kintex.publicsite.dto.PublicGuideDtos.TransportInfoDto; +import com.zioinfo.kintex.publicsite.dto.PublicGuideDtos.VisitorGuideDto; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +/** + * 공개 관람객 안내 API (V43 근거 — 비인증 공개, {@code /api/public/**} permitAll). + *

연/월 전시 캘린더 + 방문가이드 + 교통 — 외부 실시간 호출 없이 적재 DB 서빙. + *

기존 {@code PublicSiteController} 의 {@code /api/public/events}(카탈로그)와는 별도 리터럴 경로로 충돌 없음: + * 이 컨트롤러는 캘린더 뷰(lifecycle·연/월) 기반의 {@code /events/calendar}·{@code /events/monthly-summary} 를 신설한다. + */ +@RestController +@RequestMapping("/api/public") +public class PublicGuideController { + + private final PublicGuideService service; + + public PublicGuideController(PublicGuideService service) { + this.service = service; + } + + /** + * GET /api/public/events/calendar?year=&month=&lifecycle= — 연/월/상태 캘린더(v_event_calendar). + *

필터 없으면 전체(시작일 오름차순, 상한 {@code limit}). lifecycle ∈ UPCOMING|ONGOING|ENDED. + */ + @GetMapping("/events/calendar") + public ApiResponse> calendar( + @RequestParam(required = false) Integer year, + @RequestParam(required = false) Integer month, + @RequestParam(required = false) String lifecycle, + @RequestParam(required = false, defaultValue = "300") int limit) { + return ApiResponse.ok(service.calendar(year, month, lifecycle, limit)); + } + + /** GET /api/public/events/monthly-summary?year= — 월별 전시 건수 집계(v_event_monthly_summary). */ + @GetMapping("/events/monthly-summary") + public ApiResponse> monthlySummary(@RequestParam(required = false) Integer year) { + return ApiResponse.ok(service.monthlySummary(year)); + } + + /** GET /api/public/visitor-guide?category= — 방문가이드(교통/주차/입장/편의/접근성/개요). */ + @GetMapping("/visitor-guide") + public ApiResponse> visitorGuide(@RequestParam(required = false) String category) { + return ApiResponse.ok(service.guides(category)); + } + + /** GET /api/public/transport?mode= — 교통수단 구조화(SUBWAY/GTX/BUS/CAR/AIRPORT/KTX). */ + @GetMapping("/transport") + public ApiResponse> transport(@RequestParam(required = false) String mode) { + return ApiResponse.ok(service.transport(mode)); + } +} diff --git a/src/backend/src/main/java/com/zioinfo/kintex/publicsite/PublicGuideMapper.java b/src/backend/src/main/java/com/zioinfo/kintex/publicsite/PublicGuideMapper.java new file mode 100644 index 0000000..b9c0420 --- /dev/null +++ b/src/backend/src/main/java/com/zioinfo/kintex/publicsite/PublicGuideMapper.java @@ -0,0 +1,145 @@ +package com.zioinfo.kintex.publicsite; + +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; + +import java.util.List; +import java.util.Map; + +/** + * 공개 관람객 안내 매퍼 (V43 근거) — 전 조회 read-only, tenant_id='KINTEX' 고정. + *

근거: v_event_calendar / v_event_monthly_summary / visitor_guide / transport_info. + *

★ Map 반환 @Select 는 camelCase 별칭에 반드시 쌍따옴표({@code AS "x"})를 사용한다(PG lower-fold 방지). + *

★ 외부 실시간 호출 없이 DB 만 서빙 — AI 관람 도우미의 유일한 근거 소스. + */ +@Mapper +public interface PublicGuideMapper { + + // ── 연/월 캘린더 (v_event_calendar) ────────────────────────────────────── + /** + * 연/월/생명주기 필터 캘린더 조회 — 시작일 오름차순(타임라인). 필터는 모두 선택. + * lifecycle ∈ UPCOMING|ONGOING|ENDED (null 이면 전체). + */ + @Select(""" + + """) + List> findCalendar(@Param("year") Integer year, + @Param("month") Integer month, + @Param("lifecycle") String lifecycle, + @Param("limit") int limit); + + /** + * 행사명 부분검색 — AI 도우미 이름 매칭용. 미래 행사 우선(가까운 순) → 과거 최신순. + * tokens 는 이름 ILIKE OR 결합(빈 리스트면 호출자가 스킵). + */ + @Select(""" + + """) + List> searchEventsByName(@Param("tokens") List tokens, + @Param("limit") int limit); + + // ── 월별 집계 (v_event_monthly_summary) ───────────────────────────────── + /** 연도(선택) 월별 전시 건수 집계 — 연/월 오름차순. */ + @Select(""" + + """) + List> findMonthlySummary(@Param("year") Integer year); + + // ── 방문가이드 (visitor_guide) ─────────────────────────────────────────── + /** 카테고리(선택) 방문가이드 — use_yn='Y' 만. 카테고리·표시순 정렬. */ + @Select(""" + + """) + List> findGuides(@Param("category") String category); + + // ── 교통수단 (transport_info) ──────────────────────────────────────────── + /** 교통수단(선택 mode) 구조화 목록 — 표시순 정렬. */ + @Select(""" + + """) + List> findTransport(@Param("mode") String mode); +} diff --git a/src/backend/src/main/java/com/zioinfo/kintex/publicsite/PublicGuideService.java b/src/backend/src/main/java/com/zioinfo/kintex/publicsite/PublicGuideService.java new file mode 100644 index 0000000..aa3e89e --- /dev/null +++ b/src/backend/src/main/java/com/zioinfo/kintex/publicsite/PublicGuideService.java @@ -0,0 +1,134 @@ +package com.zioinfo.kintex.publicsite; + +import com.zioinfo.kintex.publicsite.dto.PublicGuideDtos.MonthlySummaryDto; +import com.zioinfo.kintex.publicsite.dto.PublicGuideDtos.PublicCalendarEventDto; +import com.zioinfo.kintex.publicsite.dto.PublicGuideDtos.TransportInfoDto; +import com.zioinfo.kintex.publicsite.dto.PublicGuideDtos.VisitorGuideDto; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * 공개 관람객 안내 서비스 — 연/월 전시 캘린더 + 방문가이드 + 교통(V43 근거, DB 서빙). + *

전 경로 비인증 공개. 외부 실시간 호출 없이 적재 DB 만 조회한다. + *

AI 관람 도우미({@code VisitorAssistantService})가 근거 수집에 이 서비스를 재사용한다. + */ +@Service +public class PublicGuideService { + + /** 캘린더 조회 상한(대량 데모 데이터 보호). */ + static final int CALENDAR_MAX = 500; + private static final int CALENDAR_DEFAULT = 300; + + private static final Set LIFECYCLES = Set.of("UPCOMING", "ONGOING", "ENDED"); + + private final PublicGuideMapper mapper; + + public PublicGuideService(PublicGuideMapper mapper) { + this.mapper = mapper; + } + + // ── 연/월 캘린더 ─────────────────────────────────────────────────────────── + @Transactional(readOnly = true) + public List calendar(Integer year, Integer month, String lifecycle) { + return calendar(year, month, lifecycle, CALENDAR_DEFAULT); + } + + @Transactional(readOnly = true) + public List calendar(Integer year, Integer month, String lifecycle, int limit) { + String lc = normalizeLifecycle(lifecycle); + int cap = Math.min(Math.max(limit, 1), CALENDAR_MAX); + return mapper.findCalendar(validYear(year), validMonth(month), lc, cap) + .stream().map(PublicGuideService::toCalendarEvent).toList(); + } + + /** 행사명 토큰 부분검색(AI 도우미 근거 수집용). tokens 비면 빈 목록. */ + @Transactional(readOnly = true) + public List searchEventsByName(List tokens, int limit) { + if (tokens == null || tokens.isEmpty()) { + return List.of(); + } + int cap = Math.min(Math.max(limit, 1), CALENDAR_MAX); + return mapper.searchEventsByName(tokens, cap) + .stream().map(PublicGuideService::toCalendarEvent).toList(); + } + + // ── 월별 집계 ────────────────────────────────────────────────────────────── + @Transactional(readOnly = true) + public List monthlySummary(Integer year) { + return mapper.findMonthlySummary(validYear(year)).stream() + .map(m -> new MonthlySummaryDto(toInt(m.get("eventYear")), toInt(m.get("eventMonth")), + toInt(m.get("eventCount")))) + .toList(); + } + + // ── 방문가이드 ───────────────────────────────────────────────────────────── + @Transactional(readOnly = true) + public List guides(String category) { + return mapper.findGuides(nullIfBlank(category)).stream() + .map(PublicGuideService::toGuide).toList(); + } + + // ── 교통 ────────────────────────────────────────────────────────────────── + @Transactional(readOnly = true) + public List transport(String mode) { + return mapper.findTransport(nullIfBlank(mode)).stream() + .map(PublicGuideService::toTransport).toList(); + } + + // ── row → DTO ─────────────────────────────────────────────────────────── + static PublicCalendarEventDto toCalendarEvent(Map m) { + return new PublicCalendarEventDto( + str(m.get("id")), str(m.get("name")), str(m.get("category")), + str(m.get("hallLabel")), str(m.get("posterUrl")), + str(m.get("startDate")), str(m.get("endDate")), + toInt(m.get("eventYear")), toInt(m.get("eventMonth")), str(m.get("lifecycle"))); + } + + static VisitorGuideDto toGuide(Map m) { + return new VisitorGuideDto( + str(m.get("id")), str(m.get("category")), str(m.get("title")), + str(m.get("body")), str(m.get("summary")), + toInt(m.get("displayOrder")), str(m.get("sourceUrl"))); + } + + static TransportInfoDto toTransport(Map m) { + return new TransportInfoDto( + str(m.get("id")), str(m.get("mode")), str(m.get("lineName")), str(m.get("route")), + str(m.get("fromPoint")), str(m.get("travelTime")), str(m.get("note")), + toInt(m.get("displayOrder")), str(m.get("sourceUrl"))); + } + + // ── helpers ────────────────────────────────────────────────────────────── + private static String normalizeLifecycle(String lifecycle) { + String v = nullIfBlank(lifecycle); + if (v == null) { + return null; + } + String up = v.toUpperCase(java.util.Locale.ROOT); + return LIFECYCLES.contains(up) ? up : null; // 알 수 없는 값은 무시(전체 조회) + } + + private static Integer validYear(Integer year) { + return (year != null && year >= 2000 && year <= 2100) ? year : null; + } + + private static Integer validMonth(Integer month) { + return (month != null && month >= 1 && month <= 12) ? month : null; + } + + private static String nullIfBlank(String s) { + return (s == null || s.isBlank()) ? null : s.trim(); + } + + private static String str(Object o) { + return o == null ? null : o.toString(); + } + + private static Integer toInt(Object o) { + return (o instanceof Number n) ? n.intValue() : null; + } +} diff --git a/src/backend/src/main/java/com/zioinfo/kintex/publicsite/assistant/VisitorAssistantController.java b/src/backend/src/main/java/com/zioinfo/kintex/publicsite/assistant/VisitorAssistantController.java new file mode 100644 index 0000000..e44f77f --- /dev/null +++ b/src/backend/src/main/java/com/zioinfo/kintex/publicsite/assistant/VisitorAssistantController.java @@ -0,0 +1,32 @@ +package com.zioinfo.kintex.publicsite.assistant; + +import com.zioinfo.kintex.common.ApiResponse; +import com.zioinfo.kintex.publicsite.assistant.dto.VisitorAssistantDtos.AssistantRequest; +import com.zioinfo.kintex.publicsite.assistant.dto.VisitorAssistantDtos.AssistantResult; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * 공개 AI 관람 도우미 API (비인증, {@code /api/public/**} permitAll). + * + *

POST /api/public/ai/visitor-assistant — 자연어 질문을 적재 DB(V43: 전시 캘린더·방문가이드·교통) 근거로 + * 답한다. 응답은 답변 + 근거 카드 + 인용 출처 + degraded 플래그를 정직하게 담는다(환각 방지). + * AI(Claude→Ollama) 미가용 시 검색 요약으로 자동 폴백한다. 외부 실시간 크롤 호출은 하지 않는다. + */ +@RestController +@RequestMapping("/api/public/ai") +public class VisitorAssistantController { + + private final VisitorAssistantService service; + + public VisitorAssistantController(VisitorAssistantService service) { + this.service = service; + } + + @PostMapping("/visitor-assistant") + public ApiResponse visitorAssistant(@RequestBody(required = false) AssistantRequest req) { + return ApiResponse.ok(service.ask(req == null ? null : req.question())); + } +} diff --git a/src/backend/src/main/java/com/zioinfo/kintex/publicsite/assistant/VisitorAssistantService.java b/src/backend/src/main/java/com/zioinfo/kintex/publicsite/assistant/VisitorAssistantService.java new file mode 100644 index 0000000..9e147c3 --- /dev/null +++ b/src/backend/src/main/java/com/zioinfo/kintex/publicsite/assistant/VisitorAssistantService.java @@ -0,0 +1,377 @@ +package com.zioinfo.kintex.publicsite.assistant; + +import com.zioinfo.kintex.ai.AiTextRouter; +import com.zioinfo.kintex.ai.AiTextRouter.AiResult; +import com.zioinfo.kintex.common.error.ApiException; +import com.zioinfo.kintex.common.error.ErrorCode; +import com.zioinfo.kintex.publicsite.PublicGuideService; +import com.zioinfo.kintex.publicsite.assistant.dto.VisitorAssistantDtos.AssistantResult; +import com.zioinfo.kintex.publicsite.assistant.dto.VisitorAssistantDtos.Citation; +import com.zioinfo.kintex.publicsite.dto.PublicGuideDtos.PublicCalendarEventDto; +import com.zioinfo.kintex.publicsite.dto.PublicGuideDtos.TransportInfoDto; +import com.zioinfo.kintex.publicsite.dto.PublicGuideDtos.VisitorGuideDto; +import org.springframework.stereotype.Service; + +import java.time.YearMonth; +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * 공개 AI 관람 도우미 — 자연어 질문을 적재 DB(V43) 근거만으로 답한다(RAG-style grounding · 비인증). + * + *

파이프라인: 질문 → 의도 감지(교통/주차/입장/편의/접근성/개요 + 전시 일정) → + * {@link PublicGuideService} 로 관련 레코드 조회(방문가이드·교통·연/월 캘린더) → + * 키워드 스코어링으로 상위 근거 선별 → 근거 있으면 {@link AiTextRouter}(Claude→Ollama)로 + * 선별된 레코드만 근거로 한국어 답변 생성 → 응답에 근거 카드 + 인용 출처 동봉. + * + *

환각 방지(핵심): + *

    + *
  • 근거 0건 → AI 호출 없이 "찾지 못했습니다" 정직 응답(grounded=false, provider=none).
  • + *
  • 근거는 있으나 AI 미가용 → 지어내지 않고 찾은 레코드 요약 반환(degraded=true, provider="search").
  • + *
  • 프롬프트에 "제공된 DB 근거만 사용, 없으면 모른다"는 grounding 지시. 외부 실시간 크롤 호출 금지.
  • + *
+ */ +@Service +public class VisitorAssistantService { + + private static final int MAX_QUESTION = 500; + private static final int TOP_GUIDE = 4; + private static final int TOP_TRANSPORT = 4; + private static final int MAX_EVENTS = 12; + private static final int MAX_TOKENS = 700; + + private static final Pattern YEAR = Pattern.compile("(20\\d{2})\\s*년?"); + private static final Pattern MONTH = Pattern.compile("(1[0-2]|0?[1-9])\\s*월"); + + /** 전시/행사 일정 의도 신호. */ + private static final String[] EVENT_INTENT = { + "전시", "행사", "일정", "언제", "열리", "열려", "열림", "개최", "스케줄", "볼거리", "관람", + "이번달", "이번 달", "다음달", "다음 달", "올해", "내년", "연간", "몇월", "몇 월", + "event", "exhibition", "show", "expo", "fair", "calendar" + }; + + /** 행사명 검색에서 제외할 일반/질문/날짜 어휘(모든 행사에 매칭되는 잡음 차단). */ + private static final Set NAME_STOPWORDS = Set.of( + "전시", "행사", "일정", "스케줄", "관람", "볼거리", "언제", "얼마", "어디", "어떻게", "무엇", "뭐가", + "있나요", "있어", "알려줘", "알려주세요", "해주세요", "하나요", "인가요", "개최", "열리", "열려", + "이번", "다음", "올해", "내년", "연간", "이번달", "다음달", "지금", "오늘", "내일", "요즘", "최근", + "event", "exhibition", "show", "expo", "fair", "calendar", "kintex", "킨텍스", "전시장", "전시회"); + + private final PublicGuideService guideService; + private final AiTextRouter router; + + public VisitorAssistantService(PublicGuideService guideService, AiTextRouter router) { + this.guideService = guideService; + this.router = router; + } + + /** 스코어링용 근거 항목. */ + private record Scored(T item, int score) { + } + + public AssistantResult ask(String questionRaw) { + if (questionRaw == null || questionRaw.isBlank()) { + throw new ApiException(ErrorCode.VALIDATION, "질문을 입력해 주세요."); + } + String question = questionRaw.strip(); + if (question.length() > MAX_QUESTION) { + question = question.substring(0, MAX_QUESTION); + } + List tokens = tokenize(question); + + // ── 근거 수집(DB 서빙만) ────────────────────────────────────────────── + List guides = scoreGuides(guideService.guides(null), tokens, TOP_GUIDE); + List transports = scoreTransport(guideService.transport(null), tokens, TOP_TRANSPORT); + List events = collectEvents(question, tokens); + + boolean grounded = !guides.isEmpty() || !transports.isEmpty() || !events.isEmpty(); + List citations = buildCitations(events, transports, guides); + + // 근거 없음 → AI 호출 없이 정직 응답(환각 차단). + if (!grounded) { + return new AssistantResult(question, + "제공된 안내 정보에서 관련 내용을 찾지 못했습니다. " + + "교통·주차·입장·편의시설·접근성 또는 전시 일정(연도/월/행사명) 관련으로 다시 질문해 주세요.", + List.of(), List.of(), List.of(), List.of(), false, "none", true); + } + + AiResult ai = router.generate(buildPrompt(question, events, transports, guides), MAX_TOKENS); + if (ai.usable()) { + return new AssistantResult(question, ai.text().trim(), events, transports, guides, + citations, true, ai.provider(), false); + } + // AI 미가용 → 지어내지 말고 찾은 레코드를 요약(근거 기반·정직). + return new AssistantResult(question, summarize(events, transports, guides), events, transports, guides, + citations, true, "search", true); + } + + // ── 이벤트 근거 수집 ───────────────────────────────────────────────────── + private List collectEvents(String question, List tokens) { + if (!hasEventIntent(question)) { + return List.of(); + } + String q = question.toLowerCase(Locale.ROOT).replace(" ", ""); + Integer year = firstInt(YEAR, question); + Integer month = firstInt(MONTH, question); + String lifecycle = parseLifecycle(question); + + // 상대 표현(이번달/다음달/올해/내년) → 연/월 산출(명시 값이 우선). + YearMonth now = YearMonth.now(); + if (month == null && q.contains("다음달")) { + YearMonth next = now.plusMonths(1); + year = (year != null) ? year : next.getYear(); + month = next.getMonthValue(); + } else if (month == null && q.contains("이번달")) { + year = (year != null) ? year : now.getYear(); + month = now.getMonthValue(); + } else if (year == null && q.contains("내년")) { + year = now.getYear() + 1; + } else if (year == null && q.contains("올해")) { + year = now.getYear(); + } + + LinkedHashSet merged = new LinkedHashSet<>(); + if (year != null || month != null || lifecycle != null) { + merged.addAll(guideService.calendar(year, month, lifecycle, MAX_EVENTS)); + } else { + // 시점 미지정 일반 일정 질문 → 예정(다가오는) 행사 우선. + merged.addAll(guideService.calendar(null, null, "UPCOMING", MAX_EVENTS)); + } + // 행사명 토큰 검색(특정 전시 지목 대응). + List nameTokens = nameSearchTokens(tokens); + if (!nameTokens.isEmpty()) { + merged.addAll(guideService.searchEventsByName(nameTokens, MAX_EVENTS)); + } + return merged.stream().limit(MAX_EVENTS).toList(); + } + + private boolean hasEventIntent(String question) { + String q = question.toLowerCase(Locale.ROOT); + for (String kw : EVENT_INTENT) { + if (q.contains(kw)) { + return true; + } + } + return YEAR.matcher(question).find() || MONTH.matcher(question).find(); + } + + private List nameSearchTokens(List tokens) { + List out = new ArrayList<>(); + for (String t : tokens) { + if (t.length() >= 2 && !NAME_STOPWORDS.contains(t) && !t.matches(".*\\d.*") && out.size() < 5) { + out.add(t); + } + } + return out; + } + + private String parseLifecycle(String question) { + if (question.contains("예정") || question.contains("다가오")) { + return "UPCOMING"; + } + if (question.contains("진행") || question.contains("진행중") || question.contains("지금")) { + return "ONGOING"; + } + if (question.contains("종료") || question.contains("끝난") || question.contains("지난")) { + return "ENDED"; + } + return null; + } + + // ── 키워드 스코어링 ────────────────────────────────────────────────────── + private List scoreGuides(List corpus, List tokens, int topN) { + List> scored = new ArrayList<>(); + for (VisitorGuideDto g : corpus) { + String hay = norm(String.join(" ", + safe(g.category()), safe(g.title()), safe(g.summary()), safe(g.body()))); + int s = score(hay, tokens); + if (s > 0) { + scored.add(new Scored<>(g, s)); + } + } + return topItems(scored, topN); + } + + private List scoreTransport(List corpus, List tokens, int topN) { + List> scored = new ArrayList<>(); + for (TransportInfoDto t : corpus) { + String hay = norm(String.join(" ", + safe(t.mode()), safe(t.lineName()), safe(t.route()), safe(t.fromPoint()), + safe(t.travelTime()), safe(t.note()))); + int s = score(hay, tokens); + if (s > 0) { + scored.add(new Scored<>(t, s)); + } + } + return topItems(scored, topN); + } + + private static List topItems(List> scored, int topN) { + scored.sort((a, b) -> Integer.compare(b.score(), a.score())); + List out = new ArrayList<>(); + for (int i = 0; i < Math.min(topN, scored.size()); i++) { + out.add(scored.get(i).item()); + } + return out; + } + + private static int score(String haystack, List tokens) { + int s = 0; + for (String tok : tokens) { + if (tok.length() >= 2 && haystack.contains(tok)) { + s++; + } + } + return s; + } + + // ── 프롬프트 · 요약 ────────────────────────────────────────────────────── + private String buildPrompt(String question, List events, + List transports, List guides) { + StringBuilder ev = new StringBuilder(); + for (PublicCalendarEventDto e : events) { + ev.append("- [EVENT ").append(safe(e.id())).append("] ").append(safe(e.name())); + if (e.startDate() != null) { + ev.append(" | 기간=").append(safe(e.startDate())); + if (e.endDate() != null) { + ev.append("~").append(safe(e.endDate())); + } + } + if (e.hallLabel() != null) { + ev.append(" | 장소=").append(safe(e.hallLabel())); + } + if (e.lifecycle() != null) { + ev.append(" | 상태=").append(lifecycleKo(e.lifecycle())); + } + ev.append('\n'); + } + StringBuilder tr = new StringBuilder(); + for (TransportInfoDto t : transports) { + tr.append("- [TRANSPORT ").append(safe(t.id())).append("] ").append(safe(t.lineName())); + if (t.route() != null) { + tr.append(" | 경로=").append(safe(t.route())); + } + if (t.travelTime() != null) { + tr.append(" | 시간=").append(safe(t.travelTime())); + } + tr.append('\n'); + } + StringBuilder gd = new StringBuilder(); + for (VisitorGuideDto g : guides) { + gd.append("- [GUIDE ").append(safe(g.id())).append("] ").append(safe(g.title())) + .append(" | ").append(safe(g.summary() != null ? g.summary() : g.body())) + .append('\n'); + } + + StringBuilder p = new StringBuilder(); + p.append("당신은 킨텍스(KINTEX) 전시장 공개 관람 안내 도우미다. ") + .append("아래 'DB 근거'만 사용해 관람객 질문에 한국어로 친절하고 간결하게 답하라.\n\n"); + if (ev.length() > 0) { + p.append("[전시·행사 일정]\n").append(ev).append('\n'); + } + if (tr.length() > 0) { + p.append("[교통]\n").append(tr).append('\n'); + } + if (gd.length() > 0) { + p.append("[방문가이드]\n").append(gd).append('\n'); + } + p.append("규칙(엄수):\n") + .append("1) 위 'DB 근거'에 있는 내용만 사용하라. 근거 밖의 날짜·요금·노선·시설을 지어내지 마라.\n") + .append("2) 근거로 답할 수 없으면 '제공된 안내 정보에서 확인되지 않습니다'라고 정직하게 답하라.\n") + .append("3) 요금·시간·주소 등 수치는 근거에 적힌 그대로만 인용하라.\n") + .append("4) 정확한 입장 조건·주차 운영은 행사별 공지 확인이 필요함을 필요 시 덧붙여라.\n") + .append("5) 마크다운 없이 자연스러운 문장으로 답하라.\n\n") + .append("질문: ").append(question); + return p.toString(); + } + + /** AI 미가용 시 근거 레코드 요약(지어내기 없음). */ + private String summarize(List events, + List transports, List guides) { + StringBuilder sb = new StringBuilder("관련 안내 정보입니다(AI 요약 미가용 — 원문 기준):\n"); + for (PublicCalendarEventDto e : events) { + sb.append("· ").append(safe(e.name())); + if (e.startDate() != null) { + sb.append(" (").append(safe(e.startDate())); + if (e.endDate() != null) { + sb.append("~").append(safe(e.endDate())); + } + sb.append(", ").append(lifecycleKo(e.lifecycle())).append(")"); + } + sb.append('\n'); + } + for (TransportInfoDto t : transports) { + sb.append("· ").append(safe(t.lineName())); + if (t.route() != null) { + sb.append(" — ").append(safe(t.route())); + } + sb.append('\n'); + } + for (VisitorGuideDto g : guides) { + sb.append("· ").append(safe(g.title())); + String detail = g.summary() != null ? g.summary() : g.body(); + if (detail != null && !detail.isBlank()) { + sb.append(" — ").append(detail); + } + sb.append('\n'); + } + return sb.toString().trim(); + } + + private List buildCitations(List events, + List transports, List guides) { + List cites = new ArrayList<>(); + for (PublicCalendarEventDto e : events) { + cites.add(new Citation("EVENT", e.id(), e.name(), null)); + } + for (TransportInfoDto t : transports) { + cites.add(new Citation("TRANSPORT", t.id(), t.lineName(), t.sourceUrl())); + } + for (VisitorGuideDto g : guides) { + cites.add(new Citation("GUIDE", g.id(), g.title(), g.sourceUrl())); + } + return cites; + } + + private static String lifecycleKo(String lifecycle) { + if (lifecycle == null) { + return ""; + } + return switch (lifecycle) { + case "UPCOMING" -> "예정"; + case "ONGOING" -> "진행중"; + case "ENDED" -> "종료"; + default -> lifecycle; + }; + } + + // ── 토큰화 helpers ────────────────────────────────────────────────────── + private static List tokenize(String question) { + String[] raw = norm(question).split("[\\s,·.!?()\\[\\]/]+"); + List out = new ArrayList<>(); + for (String r : raw) { + if (r.length() >= 2) { + out.add(r); + } + } + return out; + } + + private static Integer firstInt(Pattern p, String s) { + Matcher m = p.matcher(s); + return m.find() ? Integer.valueOf(m.group(1)) : null; + } + + private static String norm(String s) { + return s == null ? "" : s.toLowerCase(Locale.ROOT); + } + + private static String safe(String s) { + return s == null ? "" : s; + } +} diff --git a/src/backend/src/main/java/com/zioinfo/kintex/publicsite/assistant/dto/VisitorAssistantDtos.java b/src/backend/src/main/java/com/zioinfo/kintex/publicsite/assistant/dto/VisitorAssistantDtos.java new file mode 100644 index 0000000..f0fefdc --- /dev/null +++ b/src/backend/src/main/java/com/zioinfo/kintex/publicsite/assistant/dto/VisitorAssistantDtos.java @@ -0,0 +1,57 @@ +package com.zioinfo.kintex.publicsite.assistant.dto; + +import com.zioinfo.kintex.publicsite.dto.PublicGuideDtos.PublicCalendarEventDto; +import com.zioinfo.kintex.publicsite.dto.PublicGuideDtos.TransportInfoDto; +import com.zioinfo.kintex.publicsite.dto.PublicGuideDtos.VisitorGuideDto; + +import java.util.List; + +/** + * 공개 AI 관람 도우미 요청·응답 DTO (비인증). 응답은 답변 + 근거 카드 + 인용 출처 + degraded 플래그를 + * 정직하게 담는다(환각 방지·투명성). 답변은 조회된 DB 레코드만을 근거로 생성한다. + */ +public final class VisitorAssistantDtos { + + private VisitorAssistantDtos() { + } + + /** 관람 도우미 질문 요청. */ + public record AssistantRequest(String question) { + } + + /** + * 인용 출처(근거 레코드) — 프론트가 "출처 보기" 링크로 노출. + * + * @param type 근거 종류 — EVENT | TRANSPORT | GUIDE + * @param id 참조 레코드 id + * @param title 표시명(행사명 / 교통수단 / 가이드 제목) + * @param sourceUrl 공개 출처 URL(있을 때만) + */ + public record Citation(String type, String id, String title, String sourceUrl) { + } + + /** + * 관람 도우미 응답. + * + * @param question 질문 에코 + * @param answer 한국어 답변(근거 기반) — grounded=false 면 "찾지 못했습니다" 정직 응답 + * @param events 근거 행사 카드(연/월/상태) + * @param transport 근거 교통 카드 + * @param guide 근거 방문가이드 카드 + * @param citations 인용 출처(레코드 id·source_url) + * @param grounded DB 근거를 실제로 찾았는지(false 면 답변 불가·환각 차단) + * @param provider 응답 출처 — claude | ollama | search(AI 미가용 검색요약) | none(근거 없음) + * @param degraded AI 미가용으로 검색 요약/폴백으로 강등되었는지 + */ + public record AssistantResult( + String question, + String answer, + List events, + List transport, + List guide, + List citations, + boolean grounded, + String provider, + boolean degraded) { + } +} diff --git a/src/backend/src/main/java/com/zioinfo/kintex/publicsite/dto/PublicGuideDtos.java b/src/backend/src/main/java/com/zioinfo/kintex/publicsite/dto/PublicGuideDtos.java new file mode 100644 index 0000000..c9152c8 --- /dev/null +++ b/src/backend/src/main/java/com/zioinfo/kintex/publicsite/dto/PublicGuideDtos.java @@ -0,0 +1,60 @@ +package com.zioinfo.kintex.publicsite.dto; + +/** + * 공개 관람객 안내 DTO 모음 (V43 근거 — visitor_guide · transport_info · v_event_calendar · v_event_monthly_summary). + *

전 항목 비인증 공개 조회용. 내부 식별자·PII·원가는 담지 않는다. 근거 출처(source_url)는 투명성 위해 노출한다. + */ +public final class PublicGuideDtos { + + private PublicGuideDtos() { + } + + /** + * 연/월 캘린더 행사 카드 (v_event_calendar 근거). + * + * @param lifecycle 생명주기 — UPCOMING(예정) | ONGOING(진행) | ENDED(종료) | UNKNOWN. 날짜로 뷰가 산출. + */ + public record PublicCalendarEventDto( + String id, + String name, + String category, + String hallLabel, + String posterUrl, + String startDate, + String endDate, + Integer eventYear, + Integer eventMonth, + String lifecycle) { + } + + /** 월별 전시 건수 요약 (v_event_monthly_summary 근거) — 연간 기획 요약. */ + public record MonthlySummaryDto( + Integer eventYear, + Integer eventMonth, + Integer eventCount) { + } + + /** 관람객 방문가이드 항목 (visitor_guide 근거) — 카테고리형 안내. */ + public record VisitorGuideDto( + String id, + String category, + String title, + String body, + String summary, + Integer displayOrder, + String sourceUrl) { + } + + /** 교통수단 구조화 항목 (transport_info 근거). */ + public record TransportInfoDto( + String id, + String mode, + String lineName, + String route, + String fromPoint, + String travelTime, + String note, + Integer displayOrder, + String sourceUrl) { + } +} diff --git a/src/backend/src/main/resources/application-prod.yml b/src/backend/src/main/resources/application-prod.yml new file mode 100644 index 0000000..805f720 --- /dev/null +++ b/src/backend/src/main/resources/application-prod.yml @@ -0,0 +1,15 @@ +# 킨텍스 백엔드 — 운영(prod) 프로파일 오버레이. +# 활성화: SPRING_PROFILES_ACTIVE=prod. 기본(dev) application.yml 위에 얹힌다. +# +# 이 프로파일에서는 SecretStartupValidator 가 JWT_SECRET·KINTEX_OTP_ENC_KEY 미주입/개발폴백을 +# 기동 실패(fail-fast)로 처리한다(B12). 시크릿 값은 env 주입만 — 파일/로그/코드에 기록 금지. +kintex: + security: + # SecretStartupValidator 운영 강제(활성 프로파일에 prod 없이도 명시 강제 가능). + enforce-secrets: true + +logging: + level: + root: INFO + # 운영은 DEBUG → INFO 하향(민감 파라미터 verbose 노출 위험 완화, 추가 관찰 항목). + com.zioinfo.kintex: ${LOG_LEVEL_KINTEX:INFO} diff --git a/src/backend/src/main/resources/application.yml b/src/backend/src/main/resources/application.yml index baf91b8..8610f96 100644 --- a/src/backend/src/main/resources/application.yml +++ b/src/backend/src/main/resources/application.yml @@ -92,8 +92,12 @@ kintex: # 버전 관리되는 룰셋 데이터(규정·요율) — 코드가 아닌 데이터로 유지(킨텍스 규정 개정 대응) compliance-ruleset: classpath:rulesets/compliance-v1.json rate-ruleset: classpath:rulesets/rates-v1.json + # B11 CORS — 허용 오리진 화이트리스트(콤마구분, env 주입). 와일드카드('*') 금지(코드에서 필터링). + # 운영은 CORS_ALLOWED_ORIGINS 로 운영 도메인만 명시 주입한다. 기본값은 로컬 개발 오리진. + cors: + allowed-origins: ${CORS_ALLOWED_ORIGINS:http://localhost:5173,http://localhost:3000} upload: - # 관리자 업로드 파일 저장 루트(로그인 슬라이드 이미지 등) — /uploads/** 로 정적 서빙 + # 관리자 업로드 파일 저장 루트(로그인 슬라이드 이미지·프로필 아바타 등) — 아바타는 인증 서빙(정적 아님) dir: ${UPLOAD_DIR:./data/uploads} render: # RenderJob Redis 큐 이름(나노바나나 워커가 소비) — visualizer와 합의한 계약 diff --git a/src/backend/src/main/resources/db/migration/V42__bulk_demo_seed.sql b/src/backend/src/main/resources/db/migration/V42__bulk_demo_seed.sql new file mode 100644 index 0000000..f521d6e --- /dev/null +++ b/src/backend/src/main/resources/db/migration/V42__bulk_demo_seed.sql @@ -0,0 +1,2705 @@ +-- ============================================================================ +-- V42 전 도메인 대량 데모 시드 (테이블당 50~100건) +-- 담당: kintex-testdata-dev · 생성기: _workspace/gen_bulk_seed.py (결정적) · 작성: 2026-07-12 +-- ---------------------------------------------------------------------------- +-- 원칙(불변): +-- • V1~V41 파일 불변. 본 파일은 순증(additive)만 — 전부 순수 INSERT ... ON CONFLICT DO NOTHING. +-- 기존 시드/실데이터 UPDATE·DELETE 절대 금지(말미 company 백필만 IS NULL 가드 멱등). +-- • 테넌트 표준 = 'KINTEX'. 모든 신규 id 접두어 'bd-'(V1~V41 및 V38/V39 데모와 충돌 없음). +-- • PII(이름/전화/이메일)는 전부 가짜 더미(@kintex.test · 010-0000-xxxx). 실존 인물 금지. +-- • 날짜는 CURRENT_DATE/now() 상대값(배포 시점 기준 항상 유효). +-- • FK 무결성: company→event→layout→booth→(booth_sale/design_plan) / auction→bid→award / +-- invoice→(payment/tax/refund) 부모 선삽입. 자식은 본 파일 내 부모만 참조(고아 금지). +-- ============================================================================ + +-- 1. 회사(company) 80 — 공사업체 30 + 참가업체 50 혼합. registration_no 유니크 멱등키. +INSERT INTO company (id, registration_no, name, category, region, registered, phone) VALUES + ('bd-co-001', '701-81-10001', '테크테크', '구조물 임대', '경기', true, '02-0000-7001'), + ('bd-co-002', '702-81-10002', '로보틱스테크', '전기/조명', '인천', true, '02-0000-7002'), + ('bd-co-003', '703-81-10003', '(주)인더스트리테크', '영상/음향', '부산', true, '02-0000-7003'), + ('bd-co-004', '704-81-10004', '시스템즈테크', '네트워크', '대구', true, '02-0000-7004'), + ('bd-co-005', '705-81-10005', '(주)솔루션테크', '리깅', '대전', true, '02-0000-7005'), + ('bd-co-006', '706-81-10006', '이노베이션테크', '전시디자인설치', '광주', true, '02-0000-7006'), + ('bd-co-007', '707-81-10007', '오토메이션테크', '구조물 임대', '울산', true, '02-0000-7007'), + ('bd-co-008', '708-81-10008', '(주)디자인테크', '전기/조명', '세종', true, '02-0000-7008'), + ('bd-co-009', '709-81-10009', '엔지니어링테크', '영상/음향', '강원', true, '02-0000-7009'), + ('bd-co-010', '710-81-10010', '(주)일렉트릭테크', '네트워크', '서울', true, '02-0000-7010'), + ('bd-co-011', '711-81-10011', '네트웍스테크', '리깅', '경기', true, '02-0000-7011'), + ('bd-co-012', '712-81-10012', '미디어테크', '전시디자인설치', '인천', true, '02-0000-7012'), + ('bd-co-013', '713-81-10013', '(주)디스플레이테크', '구조물 임대', '부산', true, '02-0000-7013'), + ('bd-co-014', '714-81-10014', '컨스트럭션테크', '전기/조명', '대구', true, '02-0000-7014'), + ('bd-co-015', '715-81-10015', '(주)부스테크', '영상/음향', '대전', true, '02-0000-7015'), + ('bd-co-016', '716-81-10016', '전기시설테크', '네트워크', '광주', true, '02-0000-7016'), + ('bd-co-017', '717-81-10017', '구조테크', '리깅', '울산', false, '02-0000-7017'), + ('bd-co-018', '718-81-10018', '(주)비전테크', '전시디자인설치', '세종', true, '02-0000-7018'), + ('bd-co-019', '719-81-10019', '센서텍테크', '구조물 임대', '강원', true, '02-0000-7019'), + ('bd-co-020', '720-81-10020', '(주)머티리얼테크', '전기/조명', '서울', true, '02-0000-7020'), + ('bd-co-021', '721-81-10021', '로지스테크', '영상/음향', '경기', true, '02-0000-7021'), + ('bd-co-022', '722-81-10022', '바이오테크', '네트워크', '인천', true, '02-0000-7022'), + ('bd-co-023', '723-81-10023', '(주)헬스랩테크', '리깅', '부산', true, '02-0000-7023'), + ('bd-co-024', '724-81-10024', '에너지테크', '전시디자인설치', '대구', true, '02-0000-7024'), + ('bd-co-025', '725-81-10025', '(주)스마트테크', '구조물 임대', '대전', true, '02-0000-7025'), + ('bd-co-026', '726-81-10026', '글로벌테크', '전기/조명', '광주', true, '02-0000-7026'), + ('bd-co-027', '727-81-10027', '넥스트테크', '영상/음향', '울산', true, '02-0000-7027'), + ('bd-co-028', '728-81-10028', '(주)미래테크', '네트워크', '세종', true, '02-0000-7028'), + ('bd-co-029', '729-81-10029', '한빛테크', '리깅', '강원', true, '02-0000-7029'), + ('bd-co-030', '730-81-10030', '(주)대한테크', '전시디자인설치', '서울', true, '02-0000-7030'), + ('bd-co-031', '731-81-10031', '테크시스템즈', '물류/유통', '경기', true, '02-0000-7031'), + ('bd-co-032', '732-81-10032', '로보틱스시스템즈', 'ICT/소프트웨어', '인천', true, '02-0000-7032'), + ('bd-co-033', '733-81-10033', '(주)인더스트리시스템즈', '바이오/헬스', '부산', true, '02-0000-7033'), + ('bd-co-034', '734-81-10034', '시스템즈시스템즈', '기타', '대구', false, '02-0000-7034'), + ('bd-co-035', '735-81-10035', '(주)솔루션시스템즈', '제조/자동화', '대전', true, '02-0000-7035'), + ('bd-co-036', '736-81-10036', '이노베이션시스템즈', '로봇', '광주', true, '02-0000-7036'), + ('bd-co-037', '737-81-10037', '오토메이션시스템즈', '전기/전자', '울산', true, '02-0000-7037'), + ('bd-co-038', '738-81-10038', '(주)디자인시스템즈', '물류/유통', '세종', true, '02-0000-7038'), + ('bd-co-039', '739-81-10039', '엔지니어링시스템즈', 'ICT/소프트웨어', '강원', true, '02-0000-7039'), + ('bd-co-040', '740-81-10040', '(주)일렉트릭시스템즈', '바이오/헬스', '서울', true, '02-0000-7040'), + ('bd-co-041', '741-81-10041', '네트웍스시스템즈', '기타', '경기', true, '02-0000-7041'), + ('bd-co-042', '742-81-10042', '미디어시스템즈', '제조/자동화', '인천', true, '02-0000-7042'), + ('bd-co-043', '743-81-10043', '(주)디스플레이시스템즈', '로봇', '부산', true, '02-0000-7043'), + ('bd-co-044', '744-81-10044', '컨스트럭션시스템즈', '전기/전자', '대구', true, '02-0000-7044'), + ('bd-co-045', '745-81-10045', '(주)부스시스템즈', '물류/유통', '대전', true, '02-0000-7045'), + ('bd-co-046', '746-81-10046', '전기시설시스템즈', 'ICT/소프트웨어', '광주', true, '02-0000-7046'), + ('bd-co-047', '747-81-10047', '구조시스템즈', '바이오/헬스', '울산', true, '02-0000-7047'), + ('bd-co-048', '748-81-10048', '(주)비전시스템즈', '기타', '세종', true, '02-0000-7048'), + ('bd-co-049', '749-81-10049', '센서텍시스템즈', '제조/자동화', '강원', true, '02-0000-7049'), + ('bd-co-050', '750-81-10050', '(주)머티리얼시스템즈', '로봇', '서울', true, '02-0000-7050'), + ('bd-co-051', '751-81-10051', '로지스시스템즈', '전기/전자', '경기', false, '02-0000-7051'), + ('bd-co-052', '752-81-10052', '바이오시스템즈', '물류/유통', '인천', true, '02-0000-7052'), + ('bd-co-053', '753-81-10053', '(주)헬스랩시스템즈', 'ICT/소프트웨어', '부산', true, '02-0000-7053'), + ('bd-co-054', '754-81-10054', '에너지시스템즈', '바이오/헬스', '대구', true, '02-0000-7054'), + ('bd-co-055', '755-81-10055', '(주)스마트시스템즈', '기타', '대전', true, '02-0000-7055'), + ('bd-co-056', '756-81-10056', '글로벌시스템즈', '제조/자동화', '광주', true, '02-0000-7056'), + ('bd-co-057', '757-81-10057', '넥스트시스템즈', '로봇', '울산', true, '02-0000-7057'), + ('bd-co-058', '758-81-10058', '(주)미래시스템즈', '전기/전자', '세종', true, '02-0000-7058'), + ('bd-co-059', '759-81-10059', '한빛시스템즈', '물류/유통', '강원', true, '02-0000-7059'), + ('bd-co-060', '760-81-10060', '(주)대한시스템즈', 'ICT/소프트웨어', '서울', true, '02-0000-7060'), + ('bd-co-061', '761-81-10061', '테크인더스트리', '바이오/헬스', '경기', true, '02-0000-7061'), + ('bd-co-062', '762-81-10062', '로보틱스인더스트리', '기타', '인천', true, '02-0000-7062'), + ('bd-co-063', '763-81-10063', '(주)인더스트리인더스트리', '제조/자동화', '부산', true, '02-0000-7063'), + ('bd-co-064', '764-81-10064', '시스템즈인더스트리', '로봇', '대구', true, '02-0000-7064'), + ('bd-co-065', '765-81-10065', '(주)솔루션인더스트리', '전기/전자', '대전', true, '02-0000-7065'), + ('bd-co-066', '766-81-10066', '이노베이션인더스트리', '물류/유통', '광주', true, '02-0000-7066'), + ('bd-co-067', '767-81-10067', '오토메이션인더스트리', 'ICT/소프트웨어', '울산', true, '02-0000-7067'), + ('bd-co-068', '768-81-10068', '(주)디자인인더스트리', '바이오/헬스', '세종', false, '02-0000-7068'), + ('bd-co-069', '769-81-10069', '엔지니어링인더스트리', '기타', '강원', true, '02-0000-7069'), + ('bd-co-070', '770-81-10070', '(주)일렉트릭인더스트리', '제조/자동화', '서울', true, '02-0000-7070'), + ('bd-co-071', '771-81-10071', '네트웍스인더스트리', '로봇', '경기', true, '02-0000-7071'), + ('bd-co-072', '772-81-10072', '미디어인더스트리', '전기/전자', '인천', true, '02-0000-7072'), + ('bd-co-073', '773-81-10073', '(주)디스플레이인더스트리', '물류/유통', '부산', true, '02-0000-7073'), + ('bd-co-074', '774-81-10074', '컨스트럭션인더스트리', 'ICT/소프트웨어', '대구', true, '02-0000-7074'), + ('bd-co-075', '775-81-10075', '(주)부스인더스트리', '바이오/헬스', '대전', true, '02-0000-7075'), + ('bd-co-076', '776-81-10076', '전기시설인더스트리', '기타', '광주', true, '02-0000-7076'), + ('bd-co-077', '777-81-10077', '구조인더스트리', '제조/자동화', '울산', true, '02-0000-7077'), + ('bd-co-078', '778-81-10078', '(주)비전인더스트리', '로봇', '세종', true, '02-0000-7078'), + ('bd-co-079', '779-81-10079', '센서텍인더스트리', '전기/전자', '강원', true, '02-0000-7079'), + ('bd-co-080', '780-81-10080', '(주)머티리얼인더스트리', '물류/유통', '서울', true, '02-0000-7080') +ON CONFLICT (registration_no) DO NOTHING; + +INSERT INTO company_reputation (company_id, rating, jobs_done, claim_rate) VALUES + ('bd-co-001', 3.5, 0, 0.0), + ('bd-co-002', 4.8, 7, 0.3), + ('bd-co-003', 4.6, 14, 0.6), + ('bd-co-004', 4.4, 21, 0.9), + ('bd-co-005', 4.2, 28, 1.2), + ('bd-co-006', 4.0, 35, 1.5), + ('bd-co-007', 3.8, 2, 1.8), + ('bd-co-008', 3.6, 9, 2.1), + ('bd-co-009', 4.9, 16, 2.4), + ('bd-co-010', 4.7, 23, 2.7), + ('bd-co-011', 4.5, 30, 3.0), + ('bd-co-012', 4.3, 37, 3.3), + ('bd-co-013', 4.1, 4, 3.6), + ('bd-co-014', 3.9, 11, 3.9), + ('bd-co-015', 3.7, 18, 4.2), + ('bd-co-016', 3.5, 25, 4.5), + ('bd-co-017', 4.8, 32, 4.8), + ('bd-co-018', 4.6, 39, 0.1), + ('bd-co-019', 4.4, 6, 0.4), + ('bd-co-020', 4.2, 13, 0.7), + ('bd-co-021', 4.0, 20, 1.0), + ('bd-co-022', 3.8, 27, 1.3), + ('bd-co-023', 3.6, 34, 1.6), + ('bd-co-024', 4.9, 1, 1.9), + ('bd-co-025', 4.7, 8, 2.2), + ('bd-co-026', 4.5, 15, 2.5), + ('bd-co-027', 4.3, 22, 2.8), + ('bd-co-028', 4.1, 29, 3.1), + ('bd-co-029', 3.9, 36, 3.4), + ('bd-co-030', 3.7, 3, 3.7), + ('bd-co-031', 3.5, 10, 4.0), + ('bd-co-032', 4.8, 17, 4.3), + ('bd-co-033', 4.6, 24, 4.6), + ('bd-co-034', 4.4, 31, 4.9), + ('bd-co-035', 4.2, 38, 0.2), + ('bd-co-036', 4.0, 5, 0.5), + ('bd-co-037', 3.8, 12, 0.8), + ('bd-co-038', 3.6, 19, 1.1), + ('bd-co-039', 4.9, 26, 1.4), + ('bd-co-040', 4.7, 33, 1.7), + ('bd-co-041', 4.5, 0, 2.0), + ('bd-co-042', 4.3, 7, 2.3), + ('bd-co-043', 4.1, 14, 2.6), + ('bd-co-044', 3.9, 21, 2.9), + ('bd-co-045', 3.7, 28, 3.2), + ('bd-co-046', 3.5, 35, 3.5), + ('bd-co-047', 4.8, 2, 3.8), + ('bd-co-048', 4.6, 9, 4.1), + ('bd-co-049', 4.4, 16, 4.4), + ('bd-co-050', 4.2, 23, 4.7), + ('bd-co-051', 4.0, 30, 0.0), + ('bd-co-052', 3.8, 37, 0.3), + ('bd-co-053', 3.6, 4, 0.6), + ('bd-co-054', 4.9, 11, 0.9), + ('bd-co-055', 4.7, 18, 1.2), + ('bd-co-056', 4.5, 25, 1.5), + ('bd-co-057', 4.3, 32, 1.8), + ('bd-co-058', 4.1, 39, 2.1), + ('bd-co-059', 3.9, 6, 2.4), + ('bd-co-060', 3.7, 13, 2.7), + ('bd-co-061', 3.5, 20, 3.0), + ('bd-co-062', 4.8, 27, 3.3), + ('bd-co-063', 4.6, 34, 3.6), + ('bd-co-064', 4.4, 1, 3.9), + ('bd-co-065', 4.2, 8, 4.2), + ('bd-co-066', 4.0, 15, 4.5), + ('bd-co-067', 3.8, 22, 4.8), + ('bd-co-068', 3.6, 29, 0.1), + ('bd-co-069', 4.9, 36, 0.4), + ('bd-co-070', 4.7, 3, 0.7), + ('bd-co-071', 4.5, 10, 1.0), + ('bd-co-072', 4.3, 17, 1.3), + ('bd-co-073', 4.1, 24, 1.6), + ('bd-co-074', 3.9, 31, 1.9), + ('bd-co-075', 3.7, 38, 2.2), + ('bd-co-076', 3.5, 5, 2.5), + ('bd-co-077', 4.8, 12, 2.8), + ('bd-co-078', 4.6, 19, 3.1), + ('bd-co-079', 4.4, 26, 3.4), + ('bd-co-080', 4.2, 33, 3.7) +ON CONFLICT (company_id) DO NOTHING; + +-- 2. 행사(event) 60 — 진행중(active)·예정(upcoming)·종료(ended) 혼합. 상대일 분산. +INSERT INTO event (id, name, start_date, end_date, status, tenant_id, category, hall_label, poster_url) VALUES + ('bd-e-001', '2025 국제로봇 코리아 (BD001)', CURRENT_DATE + -1, CURRENT_DATE + 3, 'active', 'KINTEX', '컨퍼런스', '제1전시장 홀2', 'https://www.kintex.com/imageView.do?atchmnflNo=469254&fileseq=2'), + ('bd-e-002', '2026 미래모빌리티 코리아 (BD002)', CURRENT_DATE + -2, CURRENT_DATE + 3, 'active', 'KINTEX', '박람회', '제1전시장 홀3', 'https://www.kintex.com/imageView.do?atchmnflNo=469283&fileseq=1'), + ('bd-e-003', '2024 첨단제조 코리아 (BD003)', CURRENT_DATE + -3, CURRENT_DATE + 0, 'active', 'KINTEX', '무역전시', '제1전시장 홀4', NULL), + ('bd-e-004', '2025 물류자동화 코리아 (BD004)', CURRENT_DATE + 0, CURRENT_DATE + 4, 'active', 'KINTEX', 'B2B상담회', '제1전시장 홀5', NULL), + ('bd-e-005', '2026 바이오헬스 코리아 (BD005)', CURRENT_DATE + -1, CURRENT_DATE + 4, 'active', 'KINTEX', '전시', '제2전시장 홀6', 'https://www.kintex.com/imageView.do?atchmnflNo=469205&fileseq=1'), + ('bd-e-006', '2024 ICT융합 코리아 (BD006)', CURRENT_DATE + -2, CURRENT_DATE + 1, 'active', 'KINTEX', '컨퍼런스', '제2전시장 홀7', 'https://www.kintex.com/imageView.do?atchmnflNo=469254&fileseq=2'), + ('bd-e-007', '2025 전기전자 코리아 (BD007)', CURRENT_DATE + -3, CURRENT_DATE + 1, 'active', 'KINTEX', '박람회', '제2전시장 홀8', 'https://www.kintex.com/imageView.do?atchmnflNo=469283&fileseq=1'), + ('bd-e-008', '2026 에너지테크 코리아 (BD008)', CURRENT_DATE + 0, CURRENT_DATE + 5, 'active', 'KINTEX', '무역전시', '제2전시장 홀9', NULL), + ('bd-e-009', '2024 디스플레이 코리아 (BD009)', CURRENT_DATE + -1, CURRENT_DATE + 2, 'active', 'KINTEX', 'B2B상담회', '제2전시장 홀10', NULL), + ('bd-e-010', '2025 반도체장비 코리아 (BD010)', CURRENT_DATE + -2, CURRENT_DATE + 2, 'active', 'KINTEX', '전시', '제1전시장 홀1', 'https://www.kintex.com/imageView.do?atchmnflNo=469205&fileseq=1'), + ('bd-e-011', '2026 친환경산업 코리아 (BD011)', CURRENT_DATE + -3, CURRENT_DATE + 2, 'active', 'KINTEX', '컨퍼런스', '제1전시장 홀2', 'https://www.kintex.com/imageView.do?atchmnflNo=469254&fileseq=2'), + ('bd-e-012', '2024 K-뷰티 코리아 (BD012)', CURRENT_DATE + 0, CURRENT_DATE + 3, 'active', 'KINTEX', '박람회', '제1전시장 홀3', 'https://www.kintex.com/imageView.do?atchmnflNo=469283&fileseq=1'), + ('bd-e-013', '2025 푸드테크 코리아 (BD013)', CURRENT_DATE + 62, CURRENT_DATE + 65, 'upcoming', 'KINTEX', '무역전시', '제1전시장 홀4', NULL), + ('bd-e-014', '2026 조선해양 코리아 (BD014)', CURRENT_DATE + 66, CURRENT_DATE + 70, 'upcoming', 'KINTEX', 'B2B상담회', '제1전시장 홀5', NULL), + ('bd-e-015', '2024 스마트팩토리 코리아 (BD015)', CURRENT_DATE + 70, CURRENT_DATE + 75, 'upcoming', 'KINTEX', '전시', '제2전시장 홀6', 'https://www.kintex.com/imageView.do?atchmnflNo=469205&fileseq=1'), + ('bd-e-016', '2025 국제로봇 코리아 (BD016)', CURRENT_DATE + 74, CURRENT_DATE + 76, 'upcoming', 'KINTEX', '컨퍼런스', '제2전시장 홀7', 'https://www.kintex.com/imageView.do?atchmnflNo=469254&fileseq=2'), + ('bd-e-017', '2026 미래모빌리티 코리아 (BD017)', CURRENT_DATE + 78, CURRENT_DATE + 81, 'upcoming', 'KINTEX', '박람회', '제2전시장 홀8', 'https://www.kintex.com/imageView.do?atchmnflNo=469283&fileseq=1'), + ('bd-e-018', '2024 첨단제조 코리아 (BD018)', CURRENT_DATE + 82, CURRENT_DATE + 86, 'upcoming', 'KINTEX', '무역전시', '제2전시장 홀9', NULL), + ('bd-e-019', '2025 물류자동화 코리아 (BD019)', CURRENT_DATE + 86, CURRENT_DATE + 91, 'upcoming', 'KINTEX', 'B2B상담회', '제2전시장 홀10', NULL), + ('bd-e-020', '2026 바이오헬스 코리아 (BD020)', CURRENT_DATE + 90, CURRENT_DATE + 92, 'upcoming', 'KINTEX', '전시', '제1전시장 홀1', 'https://www.kintex.com/imageView.do?atchmnflNo=469205&fileseq=1'), + ('bd-e-021', '2024 ICT융합 코리아 (BD021)', CURRENT_DATE + 94, CURRENT_DATE + 97, 'upcoming', 'KINTEX', '컨퍼런스', '제1전시장 홀2', 'https://www.kintex.com/imageView.do?atchmnflNo=469254&fileseq=2'), + ('bd-e-022', '2025 전기전자 코리아 (BD022)', CURRENT_DATE + 98, CURRENT_DATE + 102, 'upcoming', 'KINTEX', '박람회', '제1전시장 홀3', 'https://www.kintex.com/imageView.do?atchmnflNo=469283&fileseq=1'), + ('bd-e-023', '2026 에너지테크 코리아 (BD023)', CURRENT_DATE + 102, CURRENT_DATE + 107, 'upcoming', 'KINTEX', '무역전시', '제1전시장 홀4', NULL), + ('bd-e-024', '2024 디스플레이 코리아 (BD024)', CURRENT_DATE + 106, CURRENT_DATE + 108, 'upcoming', 'KINTEX', 'B2B상담회', '제1전시장 홀5', NULL), + ('bd-e-025', '2025 반도체장비 코리아 (BD025)', CURRENT_DATE + 110, CURRENT_DATE + 113, 'upcoming', 'KINTEX', '전시', '제2전시장 홀6', 'https://www.kintex.com/imageView.do?atchmnflNo=469205&fileseq=1'), + ('bd-e-026', '2026 친환경산업 코리아 (BD026)', CURRENT_DATE + 114, CURRENT_DATE + 118, 'upcoming', 'KINTEX', '컨퍼런스', '제2전시장 홀7', 'https://www.kintex.com/imageView.do?atchmnflNo=469254&fileseq=2'), + ('bd-e-027', '2024 K-뷰티 코리아 (BD027)', CURRENT_DATE + 118, CURRENT_DATE + 123, 'upcoming', 'KINTEX', '박람회', '제2전시장 홀8', 'https://www.kintex.com/imageView.do?atchmnflNo=469283&fileseq=1'), + ('bd-e-028', '2025 푸드테크 코리아 (BD028)', CURRENT_DATE + 122, CURRENT_DATE + 124, 'upcoming', 'KINTEX', '무역전시', '제2전시장 홀9', NULL), + ('bd-e-029', '2026 조선해양 코리아 (BD029)', CURRENT_DATE + 126, CURRENT_DATE + 129, 'upcoming', 'KINTEX', 'B2B상담회', '제2전시장 홀10', NULL), + ('bd-e-030', '2024 스마트팩토리 코리아 (BD030)', CURRENT_DATE + 130, CURRENT_DATE + 134, 'upcoming', 'KINTEX', '전시', '제1전시장 홀1', 'https://www.kintex.com/imageView.do?atchmnflNo=469205&fileseq=1'), + ('bd-e-031', '2025 국제로봇 코리아 (BD031)', CURRENT_DATE + 134, CURRENT_DATE + 139, 'upcoming', 'KINTEX', '컨퍼런스', '제1전시장 홀2', 'https://www.kintex.com/imageView.do?atchmnflNo=469254&fileseq=2'), + ('bd-e-032', '2026 미래모빌리티 코리아 (BD032)', CURRENT_DATE + 138, CURRENT_DATE + 140, 'upcoming', 'KINTEX', '박람회', '제1전시장 홀3', 'https://www.kintex.com/imageView.do?atchmnflNo=469283&fileseq=1'), + ('bd-e-033', '2024 첨단제조 코리아 (BD033)', CURRENT_DATE + 142, CURRENT_DATE + 145, 'upcoming', 'KINTEX', '무역전시', '제1전시장 홀4', NULL), + ('bd-e-034', '2025 물류자동화 코리아 (BD034)', CURRENT_DATE + 146, CURRENT_DATE + 150, 'upcoming', 'KINTEX', 'B2B상담회', '제1전시장 홀5', NULL), + ('bd-e-035', '2026 바이오헬스 코리아 (BD035)', CURRENT_DATE + 150, CURRENT_DATE + 155, 'upcoming', 'KINTEX', '전시', '제2전시장 홀6', 'https://www.kintex.com/imageView.do?atchmnflNo=469205&fileseq=1'), + ('bd-e-036', '2024 ICT융합 코리아 (BD036)', CURRENT_DATE + 154, CURRENT_DATE + 156, 'upcoming', 'KINTEX', '컨퍼런스', '제2전시장 홀7', 'https://www.kintex.com/imageView.do?atchmnflNo=469254&fileseq=2'), + ('bd-e-037', '2025 전기전자 코리아 (BD037)', CURRENT_DATE + -14, CURRENT_DATE + -11, 'ended', 'KINTEX', '박람회', '제2전시장 홀8', 'https://www.kintex.com/imageView.do?atchmnflNo=469283&fileseq=1'), + ('bd-e-038', '2026 에너지테크 코리아 (BD038)', CURRENT_DATE + -21, CURRENT_DATE + -17, 'ended', 'KINTEX', '무역전시', '제2전시장 홀9', NULL), + ('bd-e-039', '2024 디스플레이 코리아 (BD039)', CURRENT_DATE + -28, CURRENT_DATE + -23, 'ended', 'KINTEX', 'B2B상담회', '제2전시장 홀10', NULL), + ('bd-e-040', '2025 반도체장비 코리아 (BD040)', CURRENT_DATE + -31, CURRENT_DATE + -29, 'ended', 'KINTEX', '전시', '제1전시장 홀1', 'https://www.kintex.com/imageView.do?atchmnflNo=469205&fileseq=1'), + ('bd-e-041', '2026 친환경산업 코리아 (BD041)', CURRENT_DATE + -38, CURRENT_DATE + -35, 'ended', 'KINTEX', '컨퍼런스', '제1전시장 홀2', 'https://www.kintex.com/imageView.do?atchmnflNo=469254&fileseq=2'), + ('bd-e-042', '2024 K-뷰티 코리아 (BD042)', CURRENT_DATE + -45, CURRENT_DATE + -41, 'ended', 'KINTEX', '박람회', '제1전시장 홀3', 'https://www.kintex.com/imageView.do?atchmnflNo=469283&fileseq=1'), + ('bd-e-043', '2025 푸드테크 코리아 (BD043)', CURRENT_DATE + -52, CURRENT_DATE + -47, 'ended', 'KINTEX', '무역전시', '제1전시장 홀4', NULL), + ('bd-e-044', '2026 조선해양 코리아 (BD044)', CURRENT_DATE + -55, CURRENT_DATE + -53, 'ended', 'KINTEX', 'B2B상담회', '제1전시장 홀5', NULL), + ('bd-e-045', '2024 스마트팩토리 코리아 (BD045)', CURRENT_DATE + -62, CURRENT_DATE + -59, 'ended', 'KINTEX', '전시', '제2전시장 홀6', 'https://www.kintex.com/imageView.do?atchmnflNo=469205&fileseq=1'), + ('bd-e-046', '2025 국제로봇 코리아 (BD046)', CURRENT_DATE + -69, CURRENT_DATE + -65, 'ended', 'KINTEX', '컨퍼런스', '제2전시장 홀7', 'https://www.kintex.com/imageView.do?atchmnflNo=469254&fileseq=2'), + ('bd-e-047', '2026 미래모빌리티 코리아 (BD047)', CURRENT_DATE + -76, CURRENT_DATE + -71, 'ended', 'KINTEX', '박람회', '제2전시장 홀8', 'https://www.kintex.com/imageView.do?atchmnflNo=469283&fileseq=1'), + ('bd-e-048', '2024 첨단제조 코리아 (BD048)', CURRENT_DATE + -79, CURRENT_DATE + -77, 'ended', 'KINTEX', '무역전시', '제2전시장 홀9', NULL), + ('bd-e-049', '2025 물류자동화 코리아 (BD049)', CURRENT_DATE + -86, CURRENT_DATE + -83, 'ended', 'KINTEX', 'B2B상담회', '제2전시장 홀10', NULL), + ('bd-e-050', '2026 바이오헬스 코리아 (BD050)', CURRENT_DATE + -93, CURRENT_DATE + -89, 'ended', 'KINTEX', '전시', '제1전시장 홀1', 'https://www.kintex.com/imageView.do?atchmnflNo=469205&fileseq=1'), + ('bd-e-051', '2024 ICT융합 코리아 (BD051)', CURRENT_DATE + -100, CURRENT_DATE + -95, 'ended', 'KINTEX', '컨퍼런스', '제1전시장 홀2', 'https://www.kintex.com/imageView.do?atchmnflNo=469254&fileseq=2'), + ('bd-e-052', '2025 전기전자 코리아 (BD052)', CURRENT_DATE + -103, CURRENT_DATE + -101, 'ended', 'KINTEX', '박람회', '제1전시장 홀3', 'https://www.kintex.com/imageView.do?atchmnflNo=469283&fileseq=1'), + ('bd-e-053', '2026 에너지테크 코리아 (BD053)', CURRENT_DATE + -110, CURRENT_DATE + -107, 'ended', 'KINTEX', '무역전시', '제1전시장 홀4', NULL), + ('bd-e-054', '2024 디스플레이 코리아 (BD054)', CURRENT_DATE + -117, CURRENT_DATE + -113, 'ended', 'KINTEX', 'B2B상담회', '제1전시장 홀5', NULL), + ('bd-e-055', '2025 반도체장비 코리아 (BD055)', CURRENT_DATE + -124, CURRENT_DATE + -119, 'ended', 'KINTEX', '전시', '제2전시장 홀6', 'https://www.kintex.com/imageView.do?atchmnflNo=469205&fileseq=1'), + ('bd-e-056', '2026 친환경산업 코리아 (BD056)', CURRENT_DATE + -127, CURRENT_DATE + -125, 'ended', 'KINTEX', '컨퍼런스', '제2전시장 홀7', 'https://www.kintex.com/imageView.do?atchmnflNo=469254&fileseq=2'), + ('bd-e-057', '2024 K-뷰티 코리아 (BD057)', CURRENT_DATE + -134, CURRENT_DATE + -131, 'ended', 'KINTEX', '박람회', '제2전시장 홀8', 'https://www.kintex.com/imageView.do?atchmnflNo=469283&fileseq=1'), + ('bd-e-058', '2025 푸드테크 코리아 (BD058)', CURRENT_DATE + -141, CURRENT_DATE + -137, 'ended', 'KINTEX', '무역전시', '제2전시장 홀9', NULL), + ('bd-e-059', '2026 조선해양 코리아 (BD059)', CURRENT_DATE + -148, CURRENT_DATE + -143, 'ended', 'KINTEX', 'B2B상담회', '제2전시장 홀10', NULL), + ('bd-e-060', '2024 스마트팩토리 코리아 (BD060)', CURRENT_DATE + -151, CURRENT_DATE + -149, 'ended', 'KINTEX', '전시', '제1전시장 홀1', 'https://www.kintex.com/imageView.do?atchmnflNo=469205&fileseq=1') +ON CONFLICT (id) DO NOTHING; + +-- 2-1. 홀 배정(hall_assignment) 60 — 행사당 대표 홀 1. +INSERT INTO hall_assignment (id, event_id, hall_id, is_primary) VALUES + ('bd-ha-001', 'bd-e-001', 'H2', true), + ('bd-ha-002', 'bd-e-002', 'H3', true), + ('bd-ha-003', 'bd-e-003', 'H4', true), + ('bd-ha-004', 'bd-e-004', 'H5', true), + ('bd-ha-005', 'bd-e-005', 'H6', true), + ('bd-ha-006', 'bd-e-006', 'H7', true), + ('bd-ha-007', 'bd-e-007', 'H8', true), + ('bd-ha-008', 'bd-e-008', 'H9', true), + ('bd-ha-009', 'bd-e-009', 'H10', true), + ('bd-ha-010', 'bd-e-010', 'H1', true), + ('bd-ha-011', 'bd-e-011', 'H2', true), + ('bd-ha-012', 'bd-e-012', 'H3', true), + ('bd-ha-013', 'bd-e-013', 'H4', true), + ('bd-ha-014', 'bd-e-014', 'H5', true), + ('bd-ha-015', 'bd-e-015', 'H6', true), + ('bd-ha-016', 'bd-e-016', 'H7', true), + ('bd-ha-017', 'bd-e-017', 'H8', true), + ('bd-ha-018', 'bd-e-018', 'H9', true), + ('bd-ha-019', 'bd-e-019', 'H10', true), + ('bd-ha-020', 'bd-e-020', 'H1', true), + ('bd-ha-021', 'bd-e-021', 'H2', true), + ('bd-ha-022', 'bd-e-022', 'H3', true), + ('bd-ha-023', 'bd-e-023', 'H4', true), + ('bd-ha-024', 'bd-e-024', 'H5', true), + ('bd-ha-025', 'bd-e-025', 'H6', true), + ('bd-ha-026', 'bd-e-026', 'H7', true), + ('bd-ha-027', 'bd-e-027', 'H8', true), + ('bd-ha-028', 'bd-e-028', 'H9', true), + ('bd-ha-029', 'bd-e-029', 'H10', true), + ('bd-ha-030', 'bd-e-030', 'H1', true), + ('bd-ha-031', 'bd-e-031', 'H2', true), + ('bd-ha-032', 'bd-e-032', 'H3', true), + ('bd-ha-033', 'bd-e-033', 'H4', true), + ('bd-ha-034', 'bd-e-034', 'H5', true), + ('bd-ha-035', 'bd-e-035', 'H6', true), + ('bd-ha-036', 'bd-e-036', 'H7', true), + ('bd-ha-037', 'bd-e-037', 'H8', true), + ('bd-ha-038', 'bd-e-038', 'H9', true), + ('bd-ha-039', 'bd-e-039', 'H10', true), + ('bd-ha-040', 'bd-e-040', 'H1', true), + ('bd-ha-041', 'bd-e-041', 'H2', true), + ('bd-ha-042', 'bd-e-042', 'H3', true), + ('bd-ha-043', 'bd-e-043', 'H4', true), + ('bd-ha-044', 'bd-e-044', 'H5', true), + ('bd-ha-045', 'bd-e-045', 'H6', true), + ('bd-ha-046', 'bd-e-046', 'H7', true), + ('bd-ha-047', 'bd-e-047', 'H8', true), + ('bd-ha-048', 'bd-e-048', 'H9', true), + ('bd-ha-049', 'bd-e-049', 'H10', true), + ('bd-ha-050', 'bd-e-050', 'H1', true), + ('bd-ha-051', 'bd-e-051', 'H2', true), + ('bd-ha-052', 'bd-e-052', 'H3', true), + ('bd-ha-053', 'bd-e-053', 'H4', true), + ('bd-ha-054', 'bd-e-054', 'H5', true), + ('bd-ha-055', 'bd-e-055', 'H6', true), + ('bd-ha-056', 'bd-e-056', 'H7', true), + ('bd-ha-057', 'bd-e-057', 'H8', true), + ('bd-ha-058', 'bd-e-058', 'H9', true), + ('bd-ha-059', 'bd-e-059', 'H10', true), + ('bd-ha-060', 'bd-e-060', 'H1', true) +ON CONFLICT (event_id, hall_id) DO NOTHING; + +-- 3. 배치(layout) 25 — 앞선 25개 행사에 부스 호스팅용 배치. SRID 0 로컬 평면(m). +INSERT INTO layout (id, event_id, hall_id, version, name, status) VALUES + ('bd-ly-001', 'bd-e-001', 'H2', 1, 'BD 배치 1', 'confirmed'), + ('bd-ly-002', 'bd-e-002', 'H3', 1, 'BD 배치 2', 'confirmed'), + ('bd-ly-003', 'bd-e-003', 'H4', 1, 'BD 배치 3', 'confirmed'), + ('bd-ly-004', 'bd-e-004', 'H5', 1, 'BD 배치 4', 'confirmed'), + ('bd-ly-005', 'bd-e-005', 'H6', 1, 'BD 배치 5', 'confirmed'), + ('bd-ly-006', 'bd-e-006', 'H7', 1, 'BD 배치 6', 'confirmed'), + ('bd-ly-007', 'bd-e-007', 'H8', 1, 'BD 배치 7', 'confirmed'), + ('bd-ly-008', 'bd-e-008', 'H9', 1, 'BD 배치 8', 'confirmed'), + ('bd-ly-009', 'bd-e-009', 'H10', 1, 'BD 배치 9', 'confirmed'), + ('bd-ly-010', 'bd-e-010', 'H1', 1, 'BD 배치 10', 'confirmed'), + ('bd-ly-011', 'bd-e-011', 'H2', 1, 'BD 배치 11', 'confirmed'), + ('bd-ly-012', 'bd-e-012', 'H3', 1, 'BD 배치 12', 'confirmed'), + ('bd-ly-013', 'bd-e-013', 'H4', 1, 'BD 배치 13', 'confirmed'), + ('bd-ly-014', 'bd-e-014', 'H5', 1, 'BD 배치 14', 'confirmed'), + ('bd-ly-015', 'bd-e-015', 'H6', 1, 'BD 배치 15', 'confirmed'), + ('bd-ly-016', 'bd-e-016', 'H7', 1, 'BD 배치 16', 'confirmed'), + ('bd-ly-017', 'bd-e-017', 'H8', 1, 'BD 배치 17', 'confirmed'), + ('bd-ly-018', 'bd-e-018', 'H9', 1, 'BD 배치 18', 'confirmed'), + ('bd-ly-019', 'bd-e-019', 'H10', 1, 'BD 배치 19', 'confirmed'), + ('bd-ly-020', 'bd-e-020', 'H1', 1, 'BD 배치 20', 'confirmed'), + ('bd-ly-021', 'bd-e-021', 'H2', 1, 'BD 배치 21', 'confirmed'), + ('bd-ly-022', 'bd-e-022', 'H3', 1, 'BD 배치 22', 'confirmed'), + ('bd-ly-023', 'bd-e-023', 'H4', 1, 'BD 배치 23', 'confirmed'), + ('bd-ly-024', 'bd-e-024', 'H5', 1, 'BD 배치 24', 'confirmed'), + ('bd-ly-025', 'bd-e-025', 'H6', 1, 'BD 배치 25', 'confirmed') +ON CONFLICT (event_id, hall_id, version) DO NOTHING; + +-- 3-1. 부스(booth) 100 — 배치당 4부스 6m×3m(18㎡). POLYGON 그리드. +INSERT INTO booth (id, layout_id, booth_no, booth_type, geom, size_w_m, size_d_m, height_m, floor_load_t_per_m2, assigned_company_name, premium) VALUES + ('bd-booth-001', 'bd-ly-001', 'BD-011', 'assembled', ST_GeomFromText('POLYGON((0 0,6 0,6 3,0 3,0 0))',0), 6, 3, 3.0, 1.0, NULL, false), + ('bd-booth-002', 'bd-ly-002', 'BD-021', 'independent', ST_GeomFromText('POLYGON((0 0,6 0,6 3,0 3,0 0))',0), 6, 3, 3.5, 1.0, '로보틱스테크', true), + ('bd-booth-003', 'bd-ly-003', 'BD-031', 'corner', ST_GeomFromText('POLYGON((0 0,6 0,6 3,0 3,0 0))',0), 6, 3, 4.0, 1.0, '(주)인더스트리테크', false), + ('bd-booth-004', 'bd-ly-004', 'BD-041', 'island', ST_GeomFromText('POLYGON((0 0,6 0,6 3,0 3,0 0))',0), 6, 3, 3.0, 1.0, NULL, true), + ('bd-booth-005', 'bd-ly-005', 'BD-051', 'assembled', ST_GeomFromText('POLYGON((0 0,6 0,6 3,0 3,0 0))',0), 6, 3, 3.5, 1.0, '(주)솔루션테크', false), + ('bd-booth-006', 'bd-ly-006', 'BD-061', 'independent', ST_GeomFromText('POLYGON((0 0,6 0,6 3,0 3,0 0))',0), 6, 3, 4.0, 1.0, '이노베이션테크', true), + ('bd-booth-007', 'bd-ly-007', 'BD-071', 'corner', ST_GeomFromText('POLYGON((0 0,6 0,6 3,0 3,0 0))',0), 6, 3, 3.0, 1.0, NULL, false), + ('bd-booth-008', 'bd-ly-008', 'BD-081', 'island', ST_GeomFromText('POLYGON((0 0,6 0,6 3,0 3,0 0))',0), 6, 3, 3.5, 1.0, '(주)디자인테크', true), + ('bd-booth-009', 'bd-ly-009', 'BD-091', 'assembled', ST_GeomFromText('POLYGON((0 0,6 0,6 3,0 3,0 0))',0), 6, 3, 4.0, 1.0, '엔지니어링테크', false), + ('bd-booth-010', 'bd-ly-010', 'BD-101', 'independent', ST_GeomFromText('POLYGON((0 0,6 0,6 3,0 3,0 0))',0), 6, 3, 3.0, 1.0, NULL, true), + ('bd-booth-011', 'bd-ly-011', 'BD-111', 'corner', ST_GeomFromText('POLYGON((0 0,6 0,6 3,0 3,0 0))',0), 6, 3, 3.5, 1.0, '네트웍스테크', false), + ('bd-booth-012', 'bd-ly-012', 'BD-121', 'island', ST_GeomFromText('POLYGON((0 0,6 0,6 3,0 3,0 0))',0), 6, 3, 4.0, 1.0, '미디어테크', true), + ('bd-booth-013', 'bd-ly-013', 'BD-131', 'assembled', ST_GeomFromText('POLYGON((0 0,6 0,6 3,0 3,0 0))',0), 6, 3, 3.0, 1.0, NULL, false), + ('bd-booth-014', 'bd-ly-014', 'BD-141', 'independent', ST_GeomFromText('POLYGON((0 0,6 0,6 3,0 3,0 0))',0), 6, 3, 3.5, 1.0, '컨스트럭션테크', true), + ('bd-booth-015', 'bd-ly-015', 'BD-151', 'corner', ST_GeomFromText('POLYGON((0 0,6 0,6 3,0 3,0 0))',0), 6, 3, 4.0, 1.0, '(주)부스테크', false), + ('bd-booth-016', 'bd-ly-016', 'BD-161', 'island', ST_GeomFromText('POLYGON((0 0,6 0,6 3,0 3,0 0))',0), 6, 3, 3.0, 1.0, NULL, true), + ('bd-booth-017', 'bd-ly-017', 'BD-171', 'assembled', ST_GeomFromText('POLYGON((0 0,6 0,6 3,0 3,0 0))',0), 6, 3, 3.5, 1.0, '구조테크', false), + ('bd-booth-018', 'bd-ly-018', 'BD-181', 'independent', ST_GeomFromText('POLYGON((0 0,6 0,6 3,0 3,0 0))',0), 6, 3, 4.0, 1.0, '(주)비전테크', true), + ('bd-booth-019', 'bd-ly-019', 'BD-191', 'corner', ST_GeomFromText('POLYGON((0 0,6 0,6 3,0 3,0 0))',0), 6, 3, 3.0, 1.0, NULL, false), + ('bd-booth-020', 'bd-ly-020', 'BD-201', 'island', ST_GeomFromText('POLYGON((0 0,6 0,6 3,0 3,0 0))',0), 6, 3, 3.5, 1.0, '(주)머티리얼테크', true), + ('bd-booth-021', 'bd-ly-021', 'BD-211', 'assembled', ST_GeomFromText('POLYGON((0 0,6 0,6 3,0 3,0 0))',0), 6, 3, 4.0, 1.0, '로지스테크', false), + ('bd-booth-022', 'bd-ly-022', 'BD-221', 'independent', ST_GeomFromText('POLYGON((0 0,6 0,6 3,0 3,0 0))',0), 6, 3, 3.0, 1.0, NULL, true), + ('bd-booth-023', 'bd-ly-023', 'BD-231', 'corner', ST_GeomFromText('POLYGON((0 0,6 0,6 3,0 3,0 0))',0), 6, 3, 3.5, 1.0, '(주)헬스랩테크', false), + ('bd-booth-024', 'bd-ly-024', 'BD-241', 'island', ST_GeomFromText('POLYGON((0 0,6 0,6 3,0 3,0 0))',0), 6, 3, 4.0, 1.0, '에너지테크', true), + ('bd-booth-025', 'bd-ly-025', 'BD-251', 'assembled', ST_GeomFromText('POLYGON((0 0,6 0,6 3,0 3,0 0))',0), 6, 3, 3.0, 1.0, NULL, false), + ('bd-booth-026', 'bd-ly-001', 'BD-012', 'independent', ST_GeomFromText('POLYGON((7 0,13 0,13 3,7 3,7 0))',0), 6, 3, 3.5, 1.0, '글로벌테크', true), + ('bd-booth-027', 'bd-ly-002', 'BD-022', 'corner', ST_GeomFromText('POLYGON((7 0,13 0,13 3,7 3,7 0))',0), 6, 3, 4.0, 1.0, '넥스트테크', false), + ('bd-booth-028', 'bd-ly-003', 'BD-032', 'island', ST_GeomFromText('POLYGON((7 0,13 0,13 3,7 3,7 0))',0), 6, 3, 3.0, 1.0, NULL, true), + ('bd-booth-029', 'bd-ly-004', 'BD-042', 'assembled', ST_GeomFromText('POLYGON((7 0,13 0,13 3,7 3,7 0))',0), 6, 3, 3.5, 1.0, '한빛테크', false), + ('bd-booth-030', 'bd-ly-005', 'BD-052', 'independent', ST_GeomFromText('POLYGON((7 0,13 0,13 3,7 3,7 0))',0), 6, 3, 4.0, 1.0, '(주)대한테크', true), + ('bd-booth-031', 'bd-ly-006', 'BD-062', 'corner', ST_GeomFromText('POLYGON((7 0,13 0,13 3,7 3,7 0))',0), 6, 3, 3.0, 1.0, NULL, false), + ('bd-booth-032', 'bd-ly-007', 'BD-072', 'island', ST_GeomFromText('POLYGON((7 0,13 0,13 3,7 3,7 0))',0), 6, 3, 3.5, 1.0, '로보틱스시스템즈', true), + ('bd-booth-033', 'bd-ly-008', 'BD-082', 'assembled', ST_GeomFromText('POLYGON((7 0,13 0,13 3,7 3,7 0))',0), 6, 3, 4.0, 1.0, '(주)인더스트리시스템즈', false), + ('bd-booth-034', 'bd-ly-009', 'BD-092', 'independent', ST_GeomFromText('POLYGON((7 0,13 0,13 3,7 3,7 0))',0), 6, 3, 3.0, 1.0, NULL, true), + ('bd-booth-035', 'bd-ly-010', 'BD-102', 'corner', ST_GeomFromText('POLYGON((7 0,13 0,13 3,7 3,7 0))',0), 6, 3, 3.5, 1.0, '(주)솔루션시스템즈', false), + ('bd-booth-036', 'bd-ly-011', 'BD-112', 'island', ST_GeomFromText('POLYGON((7 0,13 0,13 3,7 3,7 0))',0), 6, 3, 4.0, 1.0, '이노베이션시스템즈', true), + ('bd-booth-037', 'bd-ly-012', 'BD-122', 'assembled', ST_GeomFromText('POLYGON((7 0,13 0,13 3,7 3,7 0))',0), 6, 3, 3.0, 1.0, NULL, false), + ('bd-booth-038', 'bd-ly-013', 'BD-132', 'independent', ST_GeomFromText('POLYGON((7 0,13 0,13 3,7 3,7 0))',0), 6, 3, 3.5, 1.0, '(주)디자인시스템즈', true), + ('bd-booth-039', 'bd-ly-014', 'BD-142', 'corner', ST_GeomFromText('POLYGON((7 0,13 0,13 3,7 3,7 0))',0), 6, 3, 4.0, 1.0, '엔지니어링시스템즈', false), + ('bd-booth-040', 'bd-ly-015', 'BD-152', 'island', ST_GeomFromText('POLYGON((7 0,13 0,13 3,7 3,7 0))',0), 6, 3, 3.0, 1.0, NULL, true), + ('bd-booth-041', 'bd-ly-016', 'BD-162', 'assembled', ST_GeomFromText('POLYGON((7 0,13 0,13 3,7 3,7 0))',0), 6, 3, 3.5, 1.0, '네트웍스시스템즈', false), + ('bd-booth-042', 'bd-ly-017', 'BD-172', 'independent', ST_GeomFromText('POLYGON((7 0,13 0,13 3,7 3,7 0))',0), 6, 3, 4.0, 1.0, '미디어시스템즈', true), + ('bd-booth-043', 'bd-ly-018', 'BD-182', 'corner', ST_GeomFromText('POLYGON((7 0,13 0,13 3,7 3,7 0))',0), 6, 3, 3.0, 1.0, NULL, false), + ('bd-booth-044', 'bd-ly-019', 'BD-192', 'island', ST_GeomFromText('POLYGON((7 0,13 0,13 3,7 3,7 0))',0), 6, 3, 3.5, 1.0, '컨스트럭션시스템즈', true), + ('bd-booth-045', 'bd-ly-020', 'BD-202', 'assembled', ST_GeomFromText('POLYGON((7 0,13 0,13 3,7 3,7 0))',0), 6, 3, 4.0, 1.0, '(주)부스시스템즈', false), + ('bd-booth-046', 'bd-ly-021', 'BD-212', 'independent', ST_GeomFromText('POLYGON((7 0,13 0,13 3,7 3,7 0))',0), 6, 3, 3.0, 1.0, NULL, true), + ('bd-booth-047', 'bd-ly-022', 'BD-222', 'corner', ST_GeomFromText('POLYGON((7 0,13 0,13 3,7 3,7 0))',0), 6, 3, 3.5, 1.0, '구조시스템즈', false), + ('bd-booth-048', 'bd-ly-023', 'BD-232', 'island', ST_GeomFromText('POLYGON((7 0,13 0,13 3,7 3,7 0))',0), 6, 3, 4.0, 1.0, '(주)비전시스템즈', true), + ('bd-booth-049', 'bd-ly-024', 'BD-242', 'assembled', ST_GeomFromText('POLYGON((7 0,13 0,13 3,7 3,7 0))',0), 6, 3, 3.0, 1.0, NULL, false), + ('bd-booth-050', 'bd-ly-025', 'BD-252', 'independent', ST_GeomFromText('POLYGON((7 0,13 0,13 3,7 3,7 0))',0), 6, 3, 3.5, 1.0, '(주)머티리얼시스템즈', true), + ('bd-booth-051', 'bd-ly-001', 'BD-013', 'corner', ST_GeomFromText('POLYGON((0 4,6 4,6 7,0 7,0 4))',0), 6, 3, 4.0, 1.0, '로지스시스템즈', false), + ('bd-booth-052', 'bd-ly-002', 'BD-023', 'island', ST_GeomFromText('POLYGON((0 4,6 4,6 7,0 7,0 4))',0), 6, 3, 3.0, 1.0, NULL, true), + ('bd-booth-053', 'bd-ly-003', 'BD-033', 'assembled', ST_GeomFromText('POLYGON((0 4,6 4,6 7,0 7,0 4))',0), 6, 3, 3.5, 1.0, '(주)헬스랩시스템즈', false), + ('bd-booth-054', 'bd-ly-004', 'BD-043', 'independent', ST_GeomFromText('POLYGON((0 4,6 4,6 7,0 7,0 4))',0), 6, 3, 4.0, 1.0, '에너지시스템즈', true), + ('bd-booth-055', 'bd-ly-005', 'BD-053', 'corner', ST_GeomFromText('POLYGON((0 4,6 4,6 7,0 7,0 4))',0), 6, 3, 3.0, 1.0, NULL, false), + ('bd-booth-056', 'bd-ly-006', 'BD-063', 'island', ST_GeomFromText('POLYGON((0 4,6 4,6 7,0 7,0 4))',0), 6, 3, 3.5, 1.0, '글로벌시스템즈', true), + ('bd-booth-057', 'bd-ly-007', 'BD-073', 'assembled', ST_GeomFromText('POLYGON((0 4,6 4,6 7,0 7,0 4))',0), 6, 3, 4.0, 1.0, '넥스트시스템즈', false), + ('bd-booth-058', 'bd-ly-008', 'BD-083', 'independent', ST_GeomFromText('POLYGON((0 4,6 4,6 7,0 7,0 4))',0), 6, 3, 3.0, 1.0, NULL, true), + ('bd-booth-059', 'bd-ly-009', 'BD-093', 'corner', ST_GeomFromText('POLYGON((0 4,6 4,6 7,0 7,0 4))',0), 6, 3, 3.5, 1.0, '한빛시스템즈', false), + ('bd-booth-060', 'bd-ly-010', 'BD-103', 'island', ST_GeomFromText('POLYGON((0 4,6 4,6 7,0 7,0 4))',0), 6, 3, 4.0, 1.0, '(주)대한시스템즈', true), + ('bd-booth-061', 'bd-ly-011', 'BD-113', 'assembled', ST_GeomFromText('POLYGON((0 4,6 4,6 7,0 7,0 4))',0), 6, 3, 3.0, 1.0, NULL, false), + ('bd-booth-062', 'bd-ly-012', 'BD-123', 'independent', ST_GeomFromText('POLYGON((0 4,6 4,6 7,0 7,0 4))',0), 6, 3, 3.5, 1.0, '로보틱스인더스트리', true), + ('bd-booth-063', 'bd-ly-013', 'BD-133', 'corner', ST_GeomFromText('POLYGON((0 4,6 4,6 7,0 7,0 4))',0), 6, 3, 4.0, 1.0, '(주)인더스트리인더스트리', false), + ('bd-booth-064', 'bd-ly-014', 'BD-143', 'island', ST_GeomFromText('POLYGON((0 4,6 4,6 7,0 7,0 4))',0), 6, 3, 3.0, 1.0, NULL, true), + ('bd-booth-065', 'bd-ly-015', 'BD-153', 'assembled', ST_GeomFromText('POLYGON((0 4,6 4,6 7,0 7,0 4))',0), 6, 3, 3.5, 1.0, '(주)솔루션인더스트리', false), + ('bd-booth-066', 'bd-ly-016', 'BD-163', 'independent', ST_GeomFromText('POLYGON((0 4,6 4,6 7,0 7,0 4))',0), 6, 3, 4.0, 1.0, '이노베이션인더스트리', true), + ('bd-booth-067', 'bd-ly-017', 'BD-173', 'corner', ST_GeomFromText('POLYGON((0 4,6 4,6 7,0 7,0 4))',0), 6, 3, 3.0, 1.0, NULL, false), + ('bd-booth-068', 'bd-ly-018', 'BD-183', 'island', ST_GeomFromText('POLYGON((0 4,6 4,6 7,0 7,0 4))',0), 6, 3, 3.5, 1.0, '(주)디자인인더스트리', true), + ('bd-booth-069', 'bd-ly-019', 'BD-193', 'assembled', ST_GeomFromText('POLYGON((0 4,6 4,6 7,0 7,0 4))',0), 6, 3, 4.0, 1.0, '엔지니어링인더스트리', false), + ('bd-booth-070', 'bd-ly-020', 'BD-203', 'independent', ST_GeomFromText('POLYGON((0 4,6 4,6 7,0 7,0 4))',0), 6, 3, 3.0, 1.0, NULL, true), + ('bd-booth-071', 'bd-ly-021', 'BD-213', 'corner', ST_GeomFromText('POLYGON((0 4,6 4,6 7,0 7,0 4))',0), 6, 3, 3.5, 1.0, '네트웍스인더스트리', false), + ('bd-booth-072', 'bd-ly-022', 'BD-223', 'island', ST_GeomFromText('POLYGON((0 4,6 4,6 7,0 7,0 4))',0), 6, 3, 4.0, 1.0, '미디어인더스트리', true), + ('bd-booth-073', 'bd-ly-023', 'BD-233', 'assembled', ST_GeomFromText('POLYGON((0 4,6 4,6 7,0 7,0 4))',0), 6, 3, 3.0, 1.0, NULL, false), + ('bd-booth-074', 'bd-ly-024', 'BD-243', 'independent', ST_GeomFromText('POLYGON((0 4,6 4,6 7,0 7,0 4))',0), 6, 3, 3.5, 1.0, '컨스트럭션인더스트리', true), + ('bd-booth-075', 'bd-ly-025', 'BD-253', 'corner', ST_GeomFromText('POLYGON((0 4,6 4,6 7,0 7,0 4))',0), 6, 3, 4.0, 1.0, '(주)부스인더스트리', false), + ('bd-booth-076', 'bd-ly-001', 'BD-014', 'island', ST_GeomFromText('POLYGON((7 4,13 4,13 7,7 7,7 4))',0), 6, 3, 3.0, 1.0, NULL, true), + ('bd-booth-077', 'bd-ly-002', 'BD-024', 'assembled', ST_GeomFromText('POLYGON((7 4,13 4,13 7,7 7,7 4))',0), 6, 3, 3.5, 1.0, '구조인더스트리', false), + ('bd-booth-078', 'bd-ly-003', 'BD-034', 'independent', ST_GeomFromText('POLYGON((7 4,13 4,13 7,7 7,7 4))',0), 6, 3, 4.0, 1.0, '(주)비전인더스트리', true), + ('bd-booth-079', 'bd-ly-004', 'BD-044', 'corner', ST_GeomFromText('POLYGON((7 4,13 4,13 7,7 7,7 4))',0), 6, 3, 3.0, 1.0, NULL, false), + ('bd-booth-080', 'bd-ly-005', 'BD-054', 'island', ST_GeomFromText('POLYGON((7 4,13 4,13 7,7 7,7 4))',0), 6, 3, 3.5, 1.0, '(주)머티리얼인더스트리', true), + ('bd-booth-081', 'bd-ly-006', 'BD-064', 'assembled', ST_GeomFromText('POLYGON((7 4,13 4,13 7,7 7,7 4))',0), 6, 3, 4.0, 1.0, '테크테크', false), + ('bd-booth-082', 'bd-ly-007', 'BD-074', 'independent', ST_GeomFromText('POLYGON((7 4,13 4,13 7,7 7,7 4))',0), 6, 3, 3.0, 1.0, NULL, true), + ('bd-booth-083', 'bd-ly-008', 'BD-084', 'corner', ST_GeomFromText('POLYGON((7 4,13 4,13 7,7 7,7 4))',0), 6, 3, 3.5, 1.0, '(주)인더스트리테크', false), + ('bd-booth-084', 'bd-ly-009', 'BD-094', 'island', ST_GeomFromText('POLYGON((7 4,13 4,13 7,7 7,7 4))',0), 6, 3, 4.0, 1.0, '시스템즈테크', true), + ('bd-booth-085', 'bd-ly-010', 'BD-104', 'assembled', ST_GeomFromText('POLYGON((7 4,13 4,13 7,7 7,7 4))',0), 6, 3, 3.0, 1.0, NULL, false), + ('bd-booth-086', 'bd-ly-011', 'BD-114', 'independent', ST_GeomFromText('POLYGON((7 4,13 4,13 7,7 7,7 4))',0), 6, 3, 3.5, 1.0, '이노베이션테크', true), + ('bd-booth-087', 'bd-ly-012', 'BD-124', 'corner', ST_GeomFromText('POLYGON((7 4,13 4,13 7,7 7,7 4))',0), 6, 3, 4.0, 1.0, '오토메이션테크', false), + ('bd-booth-088', 'bd-ly-013', 'BD-134', 'island', ST_GeomFromText('POLYGON((7 4,13 4,13 7,7 7,7 4))',0), 6, 3, 3.0, 1.0, NULL, true), + ('bd-booth-089', 'bd-ly-014', 'BD-144', 'assembled', ST_GeomFromText('POLYGON((7 4,13 4,13 7,7 7,7 4))',0), 6, 3, 3.5, 1.0, '엔지니어링테크', false), + ('bd-booth-090', 'bd-ly-015', 'BD-154', 'independent', ST_GeomFromText('POLYGON((7 4,13 4,13 7,7 7,7 4))',0), 6, 3, 4.0, 1.0, '(주)일렉트릭테크', true), + ('bd-booth-091', 'bd-ly-016', 'BD-164', 'corner', ST_GeomFromText('POLYGON((7 4,13 4,13 7,7 7,7 4))',0), 6, 3, 3.0, 1.0, NULL, false), + ('bd-booth-092', 'bd-ly-017', 'BD-174', 'island', ST_GeomFromText('POLYGON((7 4,13 4,13 7,7 7,7 4))',0), 6, 3, 3.5, 1.0, '미디어테크', true), + ('bd-booth-093', 'bd-ly-018', 'BD-184', 'assembled', ST_GeomFromText('POLYGON((7 4,13 4,13 7,7 7,7 4))',0), 6, 3, 4.0, 1.0, '(주)디스플레이테크', false), + ('bd-booth-094', 'bd-ly-019', 'BD-194', 'independent', ST_GeomFromText('POLYGON((7 4,13 4,13 7,7 7,7 4))',0), 6, 3, 3.0, 1.0, NULL, true), + ('bd-booth-095', 'bd-ly-020', 'BD-204', 'corner', ST_GeomFromText('POLYGON((7 4,13 4,13 7,7 7,7 4))',0), 6, 3, 3.5, 1.0, '(주)부스테크', false), + ('bd-booth-096', 'bd-ly-021', 'BD-214', 'island', ST_GeomFromText('POLYGON((7 4,13 4,13 7,7 7,7 4))',0), 6, 3, 4.0, 1.0, '전기시설테크', true), + ('bd-booth-097', 'bd-ly-022', 'BD-224', 'assembled', ST_GeomFromText('POLYGON((7 4,13 4,13 7,7 7,7 4))',0), 6, 3, 3.0, 1.0, NULL, false), + ('bd-booth-098', 'bd-ly-023', 'BD-234', 'independent', ST_GeomFromText('POLYGON((7 4,13 4,13 7,7 7,7 4))',0), 6, 3, 3.5, 1.0, '(주)비전테크', true), + ('bd-booth-099', 'bd-ly-024', 'BD-244', 'corner', ST_GeomFromText('POLYGON((7 4,13 4,13 7,7 7,7 4))',0), 6, 3, 4.0, 1.0, '센서텍테크', false), + ('bd-booth-100', 'bd-ly-025', 'BD-254', 'island', ST_GeomFromText('POLYGON((7 4,13 4,13 7,7 7,7 4))',0), 6, 3, 3.0, 1.0, NULL, true) +ON CONFLICT (id) DO NOTHING; + +-- 3-2. 부스 판매(booth_sale) 100 — 상태 분포 sold/available/held/blocked. +INSERT INTO booth_sale (tenant_id, id, event_id, hall_id, booth_id, booth_no, status, unit_price, area_m2, held_by, held_until, sold_to_company, sold_by, sold_at, blocked_reason, created_by) VALUES + ('KINTEX', 'bd-bs-001', 'bd-e-001', 'H2', 'bd-booth-001', 'BD-011', 'sold', 4356000, 18, NULL, NULL, '테크테크', 'demo-organizer', now() - interval '1 day', NULL, 'system-seed'), + ('KINTEX', 'bd-bs-002', 'bd-e-002', 'H3', 'bd-booth-002', 'BD-021', 'sold', 4446000, 18, NULL, NULL, '로보틱스테크', 'demo-organizer', now() - interval '2 day', NULL, 'system-seed'), + ('KINTEX', 'bd-bs-003', 'bd-e-003', 'H4', 'bd-booth-003', 'BD-031', 'sold', 4536000, 18, NULL, NULL, '(주)인더스트리테크', 'demo-organizer', now() - interval '3 day', NULL, 'system-seed'), + ('KINTEX', 'bd-bs-004', 'bd-e-004', 'H5', 'bd-booth-004', 'BD-041', 'sold', 4626000, 18, NULL, NULL, '시스템즈테크', 'demo-organizer', now() - interval '4 day', NULL, 'system-seed'), + ('KINTEX', 'bd-bs-005', 'bd-e-005', 'H6', 'bd-booth-005', 'BD-051', 'sold', 4716000, 18, NULL, NULL, '(주)솔루션테크', 'demo-organizer', now() - interval '5 day', NULL, 'system-seed'), + ('KINTEX', 'bd-bs-006', 'bd-e-006', 'H7', 'bd-booth-006', 'BD-061', 'sold', 4806000, 18, NULL, NULL, '이노베이션테크', 'demo-organizer', now() - interval '6 day', NULL, 'system-seed'), + ('KINTEX', 'bd-bs-007', 'bd-e-007', 'H8', 'bd-booth-007', 'BD-071', 'sold', 4896000, 18, NULL, NULL, '오토메이션테크', 'demo-organizer', now() - interval '7 day', NULL, 'system-seed'), + ('KINTEX', 'bd-bs-008', 'bd-e-008', 'H9', 'bd-booth-008', 'BD-081', 'sold', 4986000, 18, NULL, NULL, '(주)디자인테크', 'demo-organizer', now() - interval '8 day', NULL, 'system-seed'), + ('KINTEX', 'bd-bs-009', 'bd-e-009', 'H10', 'bd-booth-009', 'BD-091', 'sold', 5076000, 18, NULL, NULL, '엔지니어링테크', 'demo-organizer', now() - interval '9 day', NULL, 'system-seed'), + ('KINTEX', 'bd-bs-010', 'bd-e-010', 'H1', 'bd-booth-010', 'BD-101', 'sold', 5166000, 18, NULL, NULL, '(주)일렉트릭테크', 'demo-organizer', now() - interval '10 day', NULL, 'system-seed'), + ('KINTEX', 'bd-bs-011', 'bd-e-011', 'H2', 'bd-booth-011', 'BD-111', 'sold', 5256000, 18, NULL, NULL, '네트웍스테크', 'demo-organizer', now() - interval '11 day', NULL, 'system-seed'), + ('KINTEX', 'bd-bs-012', 'bd-e-012', 'H3', 'bd-booth-012', 'BD-121', 'sold', 5346000, 18, NULL, NULL, '미디어테크', 'demo-organizer', now() - interval '12 day', NULL, 'system-seed'), + ('KINTEX', 'bd-bs-013', 'bd-e-013', 'H4', 'bd-booth-013', 'BD-131', 'sold', 5436000, 18, NULL, NULL, '(주)디스플레이테크', 'demo-organizer', now() - interval '13 day', NULL, 'system-seed'), + ('KINTEX', 'bd-bs-014', 'bd-e-014', 'H5', 'bd-booth-014', 'BD-141', 'sold', 5526000, 18, NULL, NULL, '컨스트럭션테크', 'demo-organizer', now() - interval '14 day', NULL, 'system-seed'), + ('KINTEX', 'bd-bs-015', 'bd-e-015', 'H6', 'bd-booth-015', 'BD-151', 'sold', 5616000, 18, NULL, NULL, '(주)부스테크', 'demo-organizer', now() - interval '15 day', NULL, 'system-seed'), + ('KINTEX', 'bd-bs-016', 'bd-e-016', 'H7', 'bd-booth-016', 'BD-161', 'sold', 5706000, 18, NULL, NULL, '전기시설테크', 'demo-organizer', now() - interval '16 day', NULL, 'system-seed'), + ('KINTEX', 'bd-bs-017', 'bd-e-017', 'H8', 'bd-booth-017', 'BD-171', 'sold', 5796000, 18, NULL, NULL, '구조테크', 'demo-organizer', now() - interval '17 day', NULL, 'system-seed'), + ('KINTEX', 'bd-bs-018', 'bd-e-018', 'H9', 'bd-booth-018', 'BD-181', 'sold', 5886000, 18, NULL, NULL, '(주)비전테크', 'demo-organizer', now() - interval '18 day', NULL, 'system-seed'), + ('KINTEX', 'bd-bs-019', 'bd-e-019', 'H10', 'bd-booth-019', 'BD-191', 'sold', 5976000, 18, NULL, NULL, '센서텍테크', 'demo-organizer', now() - interval '19 day', NULL, 'system-seed'), + ('KINTEX', 'bd-bs-020', 'bd-e-020', 'H1', 'bd-booth-020', 'BD-201', 'sold', 6066000, 18, NULL, NULL, '(주)머티리얼테크', 'demo-organizer', now() - interval '20 day', NULL, 'system-seed'), + ('KINTEX', 'bd-bs-021', 'bd-e-021', 'H2', 'bd-booth-021', 'BD-211', 'sold', 4356000, 18, NULL, NULL, '로지스테크', 'demo-organizer', now() - interval '21 day', NULL, 'system-seed'), + ('KINTEX', 'bd-bs-022', 'bd-e-022', 'H3', 'bd-booth-022', 'BD-221', 'sold', 4446000, 18, NULL, NULL, '바이오테크', 'demo-organizer', now() - interval '22 day', NULL, 'system-seed'), + ('KINTEX', 'bd-bs-023', 'bd-e-023', 'H4', 'bd-booth-023', 'BD-231', 'sold', 4536000, 18, NULL, NULL, '(주)헬스랩테크', 'demo-organizer', now() - interval '23 day', NULL, 'system-seed'), + ('KINTEX', 'bd-bs-024', 'bd-e-024', 'H5', 'bd-booth-024', 'BD-241', 'sold', 4626000, 18, NULL, NULL, '에너지테크', 'demo-organizer', now() - interval '24 day', NULL, 'system-seed'), + ('KINTEX', 'bd-bs-025', 'bd-e-025', 'H6', 'bd-booth-025', 'BD-251', 'sold', 4716000, 18, NULL, NULL, '(주)스마트테크', 'demo-organizer', now() - interval '25 day', NULL, 'system-seed'), + ('KINTEX', 'bd-bs-026', 'bd-e-001', 'H2', 'bd-booth-026', 'BD-012', 'sold', 4806000, 18, NULL, NULL, '글로벌테크', 'demo-organizer', now() - interval '1 day', NULL, 'system-seed'), + ('KINTEX', 'bd-bs-027', 'bd-e-002', 'H3', 'bd-booth-027', 'BD-022', 'sold', 4896000, 18, NULL, NULL, '넥스트테크', 'demo-organizer', now() - interval '2 day', NULL, 'system-seed'), + ('KINTEX', 'bd-bs-028', 'bd-e-003', 'H4', 'bd-booth-028', 'BD-032', 'sold', 4986000, 18, NULL, NULL, '(주)미래테크', 'demo-organizer', now() - interval '3 day', NULL, 'system-seed'), + ('KINTEX', 'bd-bs-029', 'bd-e-004', 'H5', 'bd-booth-029', 'BD-042', 'sold', 5076000, 18, NULL, NULL, '한빛테크', 'demo-organizer', now() - interval '4 day', NULL, 'system-seed'), + ('KINTEX', 'bd-bs-030', 'bd-e-005', 'H6', 'bd-booth-030', 'BD-052', 'sold', 5166000, 18, NULL, NULL, '(주)대한테크', 'demo-organizer', now() - interval '5 day', NULL, 'system-seed'), + ('KINTEX', 'bd-bs-031', 'bd-e-006', 'H7', 'bd-booth-031', 'BD-062', 'sold', 5256000, 18, NULL, NULL, '테크시스템즈', 'demo-organizer', now() - interval '6 day', NULL, 'system-seed'), + ('KINTEX', 'bd-bs-032', 'bd-e-007', 'H8', 'bd-booth-032', 'BD-072', 'sold', 5346000, 18, NULL, NULL, '로보틱스시스템즈', 'demo-organizer', now() - interval '7 day', NULL, 'system-seed'), + ('KINTEX', 'bd-bs-033', 'bd-e-008', 'H9', 'bd-booth-033', 'BD-082', 'sold', 5436000, 18, NULL, NULL, '(주)인더스트리시스템즈', 'demo-organizer', now() - interval '8 day', NULL, 'system-seed'), + ('KINTEX', 'bd-bs-034', 'bd-e-009', 'H10', 'bd-booth-034', 'BD-092', 'sold', 5526000, 18, NULL, NULL, '시스템즈시스템즈', 'demo-organizer', now() - interval '9 day', NULL, 'system-seed'), + ('KINTEX', 'bd-bs-035', 'bd-e-010', 'H1', 'bd-booth-035', 'BD-102', 'sold', 5616000, 18, NULL, NULL, '(주)솔루션시스템즈', 'demo-organizer', now() - interval '10 day', NULL, 'system-seed'), + ('KINTEX', 'bd-bs-036', 'bd-e-011', 'H2', 'bd-booth-036', 'BD-112', 'sold', 5706000, 18, NULL, NULL, '이노베이션시스템즈', 'demo-organizer', now() - interval '11 day', NULL, 'system-seed'), + ('KINTEX', 'bd-bs-037', 'bd-e-012', 'H3', 'bd-booth-037', 'BD-122', 'sold', 5796000, 18, NULL, NULL, '오토메이션시스템즈', 'demo-organizer', now() - interval '12 day', NULL, 'system-seed'), + ('KINTEX', 'bd-bs-038', 'bd-e-013', 'H4', 'bd-booth-038', 'BD-132', 'sold', 5886000, 18, NULL, NULL, '(주)디자인시스템즈', 'demo-organizer', now() - interval '13 day', NULL, 'system-seed'), + ('KINTEX', 'bd-bs-039', 'bd-e-014', 'H5', 'bd-booth-039', 'BD-142', 'sold', 5976000, 18, NULL, NULL, '엔지니어링시스템즈', 'demo-organizer', now() - interval '14 day', NULL, 'system-seed'), + ('KINTEX', 'bd-bs-040', 'bd-e-015', 'H6', 'bd-booth-040', 'BD-152', 'sold', 6066000, 18, NULL, NULL, '(주)일렉트릭시스템즈', 'demo-organizer', now() - interval '15 day', NULL, 'system-seed'), + ('KINTEX', 'bd-bs-041', 'bd-e-016', 'H7', 'bd-booth-041', 'BD-162', 'sold', 4356000, 18, NULL, NULL, '네트웍스시스템즈', 'demo-organizer', now() - interval '16 day', NULL, 'system-seed'), + ('KINTEX', 'bd-bs-042', 'bd-e-017', 'H8', 'bd-booth-042', 'BD-172', 'sold', 4446000, 18, NULL, NULL, '미디어시스템즈', 'demo-organizer', now() - interval '17 day', NULL, 'system-seed'), + ('KINTEX', 'bd-bs-043', 'bd-e-018', 'H9', 'bd-booth-043', 'BD-182', 'sold', 4536000, 18, NULL, NULL, '(주)디스플레이시스템즈', 'demo-organizer', now() - interval '18 day', NULL, 'system-seed'), + ('KINTEX', 'bd-bs-044', 'bd-e-019', 'H10', 'bd-booth-044', 'BD-192', 'sold', 4626000, 18, NULL, NULL, '컨스트럭션시스템즈', 'demo-organizer', now() - interval '19 day', NULL, 'system-seed'), + ('KINTEX', 'bd-bs-045', 'bd-e-020', 'H1', 'bd-booth-045', 'BD-202', 'sold', 4716000, 18, NULL, NULL, '(주)부스시스템즈', 'demo-organizer', now() - interval '20 day', NULL, 'system-seed'), + ('KINTEX', 'bd-bs-046', 'bd-e-021', 'H2', 'bd-booth-046', 'BD-212', 'available', NULL, 18, NULL, NULL, NULL, NULL, NULL, NULL, 'system-seed'), + ('KINTEX', 'bd-bs-047', 'bd-e-022', 'H3', 'bd-booth-047', 'BD-222', 'available', NULL, 18, NULL, NULL, NULL, NULL, NULL, NULL, 'system-seed'), + ('KINTEX', 'bd-bs-048', 'bd-e-023', 'H4', 'bd-booth-048', 'BD-232', 'available', NULL, 18, NULL, NULL, NULL, NULL, NULL, NULL, 'system-seed'), + ('KINTEX', 'bd-bs-049', 'bd-e-024', 'H5', 'bd-booth-049', 'BD-242', 'available', NULL, 18, NULL, NULL, NULL, NULL, NULL, NULL, 'system-seed'), + ('KINTEX', 'bd-bs-050', 'bd-e-025', 'H6', 'bd-booth-050', 'BD-252', 'available', NULL, 18, NULL, NULL, NULL, NULL, NULL, NULL, 'system-seed'), + ('KINTEX', 'bd-bs-051', 'bd-e-001', 'H2', 'bd-booth-051', 'BD-013', 'available', NULL, 18, NULL, NULL, NULL, NULL, NULL, NULL, 'system-seed'), + ('KINTEX', 'bd-bs-052', 'bd-e-002', 'H3', 'bd-booth-052', 'BD-023', 'available', NULL, 18, NULL, NULL, NULL, NULL, NULL, NULL, 'system-seed'), + ('KINTEX', 'bd-bs-053', 'bd-e-003', 'H4', 'bd-booth-053', 'BD-033', 'available', NULL, 18, NULL, NULL, NULL, NULL, NULL, NULL, 'system-seed'), + ('KINTEX', 'bd-bs-054', 'bd-e-004', 'H5', 'bd-booth-054', 'BD-043', 'available', NULL, 18, NULL, NULL, NULL, NULL, NULL, NULL, 'system-seed'), + ('KINTEX', 'bd-bs-055', 'bd-e-005', 'H6', 'bd-booth-055', 'BD-053', 'available', NULL, 18, NULL, NULL, NULL, NULL, NULL, NULL, 'system-seed'), + ('KINTEX', 'bd-bs-056', 'bd-e-006', 'H7', 'bd-booth-056', 'BD-063', 'available', NULL, 18, NULL, NULL, NULL, NULL, NULL, NULL, 'system-seed'), + ('KINTEX', 'bd-bs-057', 'bd-e-007', 'H8', 'bd-booth-057', 'BD-073', 'available', NULL, 18, NULL, NULL, NULL, NULL, NULL, NULL, 'system-seed'), + ('KINTEX', 'bd-bs-058', 'bd-e-008', 'H9', 'bd-booth-058', 'BD-083', 'available', NULL, 18, NULL, NULL, NULL, NULL, NULL, NULL, 'system-seed'), + ('KINTEX', 'bd-bs-059', 'bd-e-009', 'H10', 'bd-booth-059', 'BD-093', 'available', NULL, 18, NULL, NULL, NULL, NULL, NULL, NULL, 'system-seed'), + ('KINTEX', 'bd-bs-060', 'bd-e-010', 'H1', 'bd-booth-060', 'BD-103', 'available', NULL, 18, NULL, NULL, NULL, NULL, NULL, NULL, 'system-seed'), + ('KINTEX', 'bd-bs-061', 'bd-e-011', 'H2', 'bd-booth-061', 'BD-113', 'available', NULL, 18, NULL, NULL, NULL, NULL, NULL, NULL, 'system-seed'), + ('KINTEX', 'bd-bs-062', 'bd-e-012', 'H3', 'bd-booth-062', 'BD-123', 'available', NULL, 18, NULL, NULL, NULL, NULL, NULL, NULL, 'system-seed'), + ('KINTEX', 'bd-bs-063', 'bd-e-013', 'H4', 'bd-booth-063', 'BD-133', 'available', NULL, 18, NULL, NULL, NULL, NULL, NULL, NULL, 'system-seed'), + ('KINTEX', 'bd-bs-064', 'bd-e-014', 'H5', 'bd-booth-064', 'BD-143', 'available', NULL, 18, NULL, NULL, NULL, NULL, NULL, NULL, 'system-seed'), + ('KINTEX', 'bd-bs-065', 'bd-e-015', 'H6', 'bd-booth-065', 'BD-153', 'available', NULL, 18, NULL, NULL, NULL, NULL, NULL, NULL, 'system-seed'), + ('KINTEX', 'bd-bs-066', 'bd-e-016', 'H7', 'bd-booth-066', 'BD-163', 'available', NULL, 18, NULL, NULL, NULL, NULL, NULL, NULL, 'system-seed'), + ('KINTEX', 'bd-bs-067', 'bd-e-017', 'H8', 'bd-booth-067', 'BD-173', 'available', NULL, 18, NULL, NULL, NULL, NULL, NULL, NULL, 'system-seed'), + ('KINTEX', 'bd-bs-068', 'bd-e-018', 'H9', 'bd-booth-068', 'BD-183', 'available', NULL, 18, NULL, NULL, NULL, NULL, NULL, NULL, 'system-seed'), + ('KINTEX', 'bd-bs-069', 'bd-e-019', 'H10', 'bd-booth-069', 'BD-193', 'available', NULL, 18, NULL, NULL, NULL, NULL, NULL, NULL, 'system-seed'), + ('KINTEX', 'bd-bs-070', 'bd-e-020', 'H1', 'bd-booth-070', 'BD-203', 'available', NULL, 18, NULL, NULL, NULL, NULL, NULL, NULL, 'system-seed'), + ('KINTEX', 'bd-bs-071', 'bd-e-021', 'H2', 'bd-booth-071', 'BD-213', 'available', NULL, 18, NULL, NULL, NULL, NULL, NULL, NULL, 'system-seed'), + ('KINTEX', 'bd-bs-072', 'bd-e-022', 'H3', 'bd-booth-072', 'BD-223', 'available', NULL, 18, NULL, NULL, NULL, NULL, NULL, NULL, 'system-seed'), + ('KINTEX', 'bd-bs-073', 'bd-e-023', 'H4', 'bd-booth-073', 'BD-233', 'available', NULL, 18, NULL, NULL, NULL, NULL, NULL, NULL, 'system-seed'), + ('KINTEX', 'bd-bs-074', 'bd-e-024', 'H5', 'bd-booth-074', 'BD-243', 'available', NULL, 18, NULL, NULL, NULL, NULL, NULL, NULL, 'system-seed'), + ('KINTEX', 'bd-bs-075', 'bd-e-025', 'H6', 'bd-booth-075', 'BD-253', 'available', NULL, 18, NULL, NULL, NULL, NULL, NULL, NULL, 'system-seed'), + ('KINTEX', 'bd-bs-076', 'bd-e-001', 'H2', 'bd-booth-076', 'BD-014', 'available', NULL, 18, NULL, NULL, NULL, NULL, NULL, NULL, 'system-seed'), + ('KINTEX', 'bd-bs-077', 'bd-e-002', 'H3', 'bd-booth-077', 'BD-024', 'available', NULL, 18, NULL, NULL, NULL, NULL, NULL, NULL, 'system-seed'), + ('KINTEX', 'bd-bs-078', 'bd-e-003', 'H4', 'bd-booth-078', 'BD-034', 'available', NULL, 18, NULL, NULL, NULL, NULL, NULL, NULL, 'system-seed'), + ('KINTEX', 'bd-bs-079', 'bd-e-004', 'H5', 'bd-booth-079', 'BD-044', 'available', NULL, 18, NULL, NULL, NULL, NULL, NULL, NULL, 'system-seed'), + ('KINTEX', 'bd-bs-080', 'bd-e-005', 'H6', 'bd-booth-080', 'BD-054', 'available', NULL, 18, NULL, NULL, NULL, NULL, NULL, NULL, 'system-seed'), + ('KINTEX', 'bd-bs-081', 'bd-e-006', 'H7', 'bd-booth-081', 'BD-064', 'held', NULL, 18, 'demo-exhibitor', now() + interval '3 hour', NULL, NULL, NULL, NULL, 'system-seed'), + ('KINTEX', 'bd-bs-082', 'bd-e-007', 'H8', 'bd-booth-082', 'BD-074', 'held', NULL, 18, 'demo-exhibitor', now() + interval '1 hour', NULL, NULL, NULL, NULL, 'system-seed'), + ('KINTEX', 'bd-bs-083', 'bd-e-008', 'H9', 'bd-booth-083', 'BD-084', 'held', NULL, 18, 'demo-exhibitor', now() + interval '2 hour', NULL, NULL, NULL, NULL, 'system-seed'), + ('KINTEX', 'bd-bs-084', 'bd-e-009', 'H10', 'bd-booth-084', 'BD-094', 'held', NULL, 18, 'demo-exhibitor', now() + interval '3 hour', NULL, NULL, NULL, NULL, 'system-seed'), + ('KINTEX', 'bd-bs-085', 'bd-e-010', 'H1', 'bd-booth-085', 'BD-104', 'held', NULL, 18, 'demo-exhibitor', now() + interval '1 hour', NULL, NULL, NULL, NULL, 'system-seed'), + ('KINTEX', 'bd-bs-086', 'bd-e-011', 'H2', 'bd-booth-086', 'BD-114', 'held', NULL, 18, 'demo-exhibitor', now() + interval '2 hour', NULL, NULL, NULL, NULL, 'system-seed'), + ('KINTEX', 'bd-bs-087', 'bd-e-012', 'H3', 'bd-booth-087', 'BD-124', 'held', NULL, 18, 'demo-exhibitor', now() + interval '3 hour', NULL, NULL, NULL, NULL, 'system-seed'), + ('KINTEX', 'bd-bs-088', 'bd-e-013', 'H4', 'bd-booth-088', 'BD-134', 'held', NULL, 18, 'demo-exhibitor', now() + interval '1 hour', NULL, NULL, NULL, NULL, 'system-seed'), + ('KINTEX', 'bd-bs-089', 'bd-e-014', 'H5', 'bd-booth-089', 'BD-144', 'held', NULL, 18, 'demo-exhibitor', now() + interval '2 hour', NULL, NULL, NULL, NULL, 'system-seed'), + ('KINTEX', 'bd-bs-090', 'bd-e-015', 'H6', 'bd-booth-090', 'BD-154', 'held', NULL, 18, 'demo-exhibitor', now() + interval '3 hour', NULL, NULL, NULL, NULL, 'system-seed'), + ('KINTEX', 'bd-bs-091', 'bd-e-016', 'H7', 'bd-booth-091', 'BD-164', 'blocked', NULL, 18, NULL, NULL, NULL, NULL, NULL, '소방 통로 확보 구역', 'system-seed'), + ('KINTEX', 'bd-bs-092', 'bd-e-017', 'H8', 'bd-booth-092', 'BD-174', 'blocked', NULL, 18, NULL, NULL, NULL, NULL, NULL, '소방 통로 확보 구역', 'system-seed'), + ('KINTEX', 'bd-bs-093', 'bd-e-018', 'H9', 'bd-booth-093', 'BD-184', 'blocked', NULL, 18, NULL, NULL, NULL, NULL, NULL, '소방 통로 확보 구역', 'system-seed'), + ('KINTEX', 'bd-bs-094', 'bd-e-019', 'H10', 'bd-booth-094', 'BD-194', 'blocked', NULL, 18, NULL, NULL, NULL, NULL, NULL, '소방 통로 확보 구역', 'system-seed'), + ('KINTEX', 'bd-bs-095', 'bd-e-020', 'H1', 'bd-booth-095', 'BD-204', 'blocked', NULL, 18, NULL, NULL, NULL, NULL, NULL, '소방 통로 확보 구역', 'system-seed'), + ('KINTEX', 'bd-bs-096', 'bd-e-021', 'H2', 'bd-booth-096', 'BD-214', 'blocked', NULL, 18, NULL, NULL, NULL, NULL, NULL, '소방 통로 확보 구역', 'system-seed'), + ('KINTEX', 'bd-bs-097', 'bd-e-022', 'H3', 'bd-booth-097', 'BD-224', 'blocked', NULL, 18, NULL, NULL, NULL, NULL, NULL, '소방 통로 확보 구역', 'system-seed'), + ('KINTEX', 'bd-bs-098', 'bd-e-023', 'H4', 'bd-booth-098', 'BD-234', 'blocked', NULL, 18, NULL, NULL, NULL, NULL, NULL, '소방 통로 확보 구역', 'system-seed'), + ('KINTEX', 'bd-bs-099', 'bd-e-024', 'H5', 'bd-booth-099', 'BD-244', 'blocked', NULL, 18, NULL, NULL, NULL, NULL, NULL, '소방 통로 확보 구역', 'system-seed'), + ('KINTEX', 'bd-bs-100', 'bd-e-025', 'H6', 'bd-booth-100', 'BD-254', 'blocked', NULL, 18, NULL, NULL, NULL, NULL, NULL, '소방 통로 확보 구역', 'system-seed') +ON CONFLICT (id) DO NOTHING; + +-- 3-3. 설계안(design_plan) 60 — 부스 앞 60개, 상태 분포. +INSERT INTO design_plan (id, booth_id, version, status, spec) VALUES + ('bd-dp-001', 'bd-booth-001', 1, 'approved', '{"theme":"assembled","height":3.0}'::jsonb), + ('bd-dp-002', 'bd-booth-002', 1, 'approved', '{"theme":"independent","height":3.5}'::jsonb), + ('bd-dp-003', 'bd-booth-003', 1, 'review', '{"theme":"corner","height":4.0}'::jsonb), + ('bd-dp-004', 'bd-booth-004', 1, 'draft', '{"theme":"island","height":3.0}'::jsonb), + ('bd-dp-005', 'bd-booth-005', 1, 'approved', '{"theme":"assembled","height":3.5}'::jsonb), + ('bd-dp-006', 'bd-booth-006', 1, 'approved', '{"theme":"independent","height":4.0}'::jsonb), + ('bd-dp-007', 'bd-booth-007', 1, 'review', '{"theme":"corner","height":3.0}'::jsonb), + ('bd-dp-008', 'bd-booth-008', 1, 'draft', '{"theme":"island","height":3.5}'::jsonb), + ('bd-dp-009', 'bd-booth-009', 1, 'approved', '{"theme":"assembled","height":4.0}'::jsonb), + ('bd-dp-010', 'bd-booth-010', 1, 'approved', '{"theme":"independent","height":3.0}'::jsonb), + ('bd-dp-011', 'bd-booth-011', 1, 'review', '{"theme":"corner","height":3.5}'::jsonb), + ('bd-dp-012', 'bd-booth-012', 1, 'draft', '{"theme":"island","height":4.0}'::jsonb), + ('bd-dp-013', 'bd-booth-013', 1, 'approved', '{"theme":"assembled","height":3.0}'::jsonb), + ('bd-dp-014', 'bd-booth-014', 1, 'approved', '{"theme":"independent","height":3.5}'::jsonb), + ('bd-dp-015', 'bd-booth-015', 1, 'review', '{"theme":"corner","height":4.0}'::jsonb), + ('bd-dp-016', 'bd-booth-016', 1, 'draft', '{"theme":"island","height":3.0}'::jsonb), + ('bd-dp-017', 'bd-booth-017', 1, 'approved', '{"theme":"assembled","height":3.5}'::jsonb), + ('bd-dp-018', 'bd-booth-018', 1, 'approved', '{"theme":"independent","height":4.0}'::jsonb), + ('bd-dp-019', 'bd-booth-019', 1, 'review', '{"theme":"corner","height":3.0}'::jsonb), + ('bd-dp-020', 'bd-booth-020', 1, 'draft', '{"theme":"island","height":3.5}'::jsonb), + ('bd-dp-021', 'bd-booth-021', 1, 'approved', '{"theme":"assembled","height":4.0}'::jsonb), + ('bd-dp-022', 'bd-booth-022', 1, 'approved', '{"theme":"independent","height":3.0}'::jsonb), + ('bd-dp-023', 'bd-booth-023', 1, 'review', '{"theme":"corner","height":3.5}'::jsonb), + ('bd-dp-024', 'bd-booth-024', 1, 'draft', '{"theme":"island","height":4.0}'::jsonb), + ('bd-dp-025', 'bd-booth-025', 1, 'approved', '{"theme":"assembled","height":3.0}'::jsonb), + ('bd-dp-026', 'bd-booth-026', 1, 'approved', '{"theme":"independent","height":3.5}'::jsonb), + ('bd-dp-027', 'bd-booth-027', 1, 'review', '{"theme":"corner","height":4.0}'::jsonb), + ('bd-dp-028', 'bd-booth-028', 1, 'draft', '{"theme":"island","height":3.0}'::jsonb), + ('bd-dp-029', 'bd-booth-029', 1, 'approved', '{"theme":"assembled","height":3.5}'::jsonb), + ('bd-dp-030', 'bd-booth-030', 1, 'approved', '{"theme":"independent","height":4.0}'::jsonb), + ('bd-dp-031', 'bd-booth-031', 1, 'review', '{"theme":"corner","height":3.0}'::jsonb), + ('bd-dp-032', 'bd-booth-032', 1, 'draft', '{"theme":"island","height":3.5}'::jsonb), + ('bd-dp-033', 'bd-booth-033', 1, 'approved', '{"theme":"assembled","height":4.0}'::jsonb), + ('bd-dp-034', 'bd-booth-034', 1, 'approved', '{"theme":"independent","height":3.0}'::jsonb), + ('bd-dp-035', 'bd-booth-035', 1, 'review', '{"theme":"corner","height":3.5}'::jsonb), + ('bd-dp-036', 'bd-booth-036', 1, 'draft', '{"theme":"island","height":4.0}'::jsonb), + ('bd-dp-037', 'bd-booth-037', 1, 'approved', '{"theme":"assembled","height":3.0}'::jsonb), + ('bd-dp-038', 'bd-booth-038', 1, 'approved', '{"theme":"independent","height":3.5}'::jsonb), + ('bd-dp-039', 'bd-booth-039', 1, 'review', '{"theme":"corner","height":4.0}'::jsonb), + ('bd-dp-040', 'bd-booth-040', 1, 'draft', '{"theme":"island","height":3.0}'::jsonb), + ('bd-dp-041', 'bd-booth-041', 1, 'approved', '{"theme":"assembled","height":3.5}'::jsonb), + ('bd-dp-042', 'bd-booth-042', 1, 'approved', '{"theme":"independent","height":4.0}'::jsonb), + ('bd-dp-043', 'bd-booth-043', 1, 'review', '{"theme":"corner","height":3.0}'::jsonb), + ('bd-dp-044', 'bd-booth-044', 1, 'draft', '{"theme":"island","height":3.5}'::jsonb), + ('bd-dp-045', 'bd-booth-045', 1, 'approved', '{"theme":"assembled","height":4.0}'::jsonb), + ('bd-dp-046', 'bd-booth-046', 1, 'approved', '{"theme":"independent","height":3.0}'::jsonb), + ('bd-dp-047', 'bd-booth-047', 1, 'review', '{"theme":"corner","height":3.5}'::jsonb), + ('bd-dp-048', 'bd-booth-048', 1, 'draft', '{"theme":"island","height":4.0}'::jsonb), + ('bd-dp-049', 'bd-booth-049', 1, 'approved', '{"theme":"assembled","height":3.0}'::jsonb), + ('bd-dp-050', 'bd-booth-050', 1, 'approved', '{"theme":"independent","height":3.5}'::jsonb), + ('bd-dp-051', 'bd-booth-051', 1, 'review', '{"theme":"corner","height":4.0}'::jsonb), + ('bd-dp-052', 'bd-booth-052', 1, 'draft', '{"theme":"island","height":3.0}'::jsonb), + ('bd-dp-053', 'bd-booth-053', 1, 'approved', '{"theme":"assembled","height":3.5}'::jsonb), + ('bd-dp-054', 'bd-booth-054', 1, 'approved', '{"theme":"independent","height":4.0}'::jsonb), + ('bd-dp-055', 'bd-booth-055', 1, 'review', '{"theme":"corner","height":3.0}'::jsonb), + ('bd-dp-056', 'bd-booth-056', 1, 'draft', '{"theme":"island","height":3.5}'::jsonb), + ('bd-dp-057', 'bd-booth-057', 1, 'approved', '{"theme":"assembled","height":4.0}'::jsonb), + ('bd-dp-058', 'bd-booth-058', 1, 'approved', '{"theme":"independent","height":3.0}'::jsonb), + ('bd-dp-059', 'bd-booth-059', 1, 'review', '{"theme":"corner","height":3.5}'::jsonb), + ('bd-dp-060', 'bd-booth-060', 1, 'draft', '{"theme":"island","height":4.0}'::jsonb) +ON CONFLICT (booth_id, version) DO NOTHING; + +-- 4. 관람객 사전등록(visitor_registration) 100 — 체크인 분포. PII 가짜, 마스킹 전제. +INSERT INTO visitor_registration (id, event_id, name, phone, email, company, visitor_type, agree_privacy, agree_marketing, badge_code, badge_issued, checkin_state, qr_token, checkin_at, registered_at) VALUES + ('bd-vr-001', 'bd-e-002', '이민준', '010-0000-3001', 'bdvisitor1@kintex.test', '로보틱스테크', 'buyer', true, false, 'KTX-BD-0001', false, 'waiting', 'QR-BD0000000000000000000001', NULL, now() - interval '2 day'), + ('bd-vr-002', 'bd-e-003', '박수아', '010-0000-3002', 'bdvisitor2@kintex.test', '(주)인더스트리테크', 'vip', true, true, 'KTX-BD-0002', true, 'cancelled', 'QR-BD0000000000000000000002', NULL, now() - interval '3 day'), + ('bd-vr-003', 'bd-e-004', '최건우', '010-0000-3003', 'bdvisitor3@kintex.test', '시스템즈테크', 'visitor', true, false, 'KTX-BD-0003', true, 'done', 'QR-BD0000000000000000000003', now() - interval '3 hour', now() - interval '4 day'), + ('bd-vr-004', 'bd-e-005', '정유나', '010-0000-3004', 'bdvisitor4@kintex.test', NULL, 'buyer', true, true, 'KTX-BD-0004', true, 'waiting', 'QR-BD0000000000000000000004', NULL, now() - interval '5 day'), + ('bd-vr-005', 'bd-e-006', '강예준', '010-0000-3005', 'bdvisitor5@kintex.test', '이노베이션테크', 'vip', true, false, 'KTX-BD-0005', false, 'cancelled', 'QR-BD0000000000000000000005', NULL, now() - interval '6 day'), + ('bd-vr-006', 'bd-e-007', '조지유', '010-0000-3006', 'bdvisitor6@kintex.test', '오토메이션테크', 'visitor', true, true, 'KTX-BD-0006', true, 'done', 'QR-BD0000000000000000000006', now() - interval '6 hour', now() - interval '7 day'), + ('bd-vr-007', 'bd-e-008', '윤예은', '010-0000-3007', 'bdvisitor7@kintex.test', '(주)디자인테크', 'buyer', true, false, 'KTX-BD-0007', false, 'waiting', 'QR-BD0000000000000000000007', NULL, now() - interval '8 day'), + ('bd-vr-008', 'bd-e-009', '장준서', '010-0000-3008', 'bdvisitor8@kintex.test', NULL, 'vip', true, true, 'KTX-BD-0008', true, 'cancelled', 'QR-BD0000000000000000000008', NULL, now() - interval '9 day'), + ('bd-vr-009', 'bd-e-010', '임하준', '010-0000-3009', 'bdvisitor9@kintex.test', '(주)일렉트릭테크', 'visitor', true, false, 'KTX-BD-0009', true, 'done', 'QR-BD0000000000000000000009', now() - interval '9 hour', now() - interval '10 day'), + ('bd-vr-010', 'bd-e-011', '한서윤', '010-0000-3010', 'bdvisitor10@kintex.test', '네트웍스테크', 'buyer', true, true, 'KTX-BD-0010', true, 'waiting', 'QR-BD0000000000000000000010', NULL, now() - interval '1 day'), + ('bd-vr-011', 'bd-e-012', '오예린', '010-0000-3011', 'bdvisitor11@kintex.test', '미디어테크', 'vip', true, false, 'KTX-BD-0011', false, 'cancelled', 'QR-BD0000000000000000000011', NULL, now() - interval '2 day'), + ('bd-vr-012', 'bd-e-013', '서도현', '010-0000-3012', 'bdvisitor12@kintex.test', NULL, 'visitor', true, true, 'KTX-BD-0012', true, 'done', 'QR-BD0000000000000000000012', now() - interval '12 hour', now() - interval '3 day'), + ('bd-vr-013', 'bd-e-014', '신도윤', '010-0000-3013', 'bdvisitor13@kintex.test', '컨스트럭션테크', 'buyer', true, false, 'KTX-BD-0013', false, 'waiting', 'QR-BD0000000000000000000013', NULL, now() - interval '4 day'), + ('bd-vr-014', 'bd-e-015', '권서연', '010-0000-3014', 'bdvisitor14@kintex.test', '(주)부스테크', 'vip', true, true, 'KTX-BD-0014', true, 'cancelled', 'QR-BD0000000000000000000014', NULL, now() - interval '5 day'), + ('bd-vr-015', 'bd-e-016', '황지민', '010-0000-3015', 'bdvisitor15@kintex.test', '전기시설테크', 'visitor', true, false, 'KTX-BD-0015', true, 'done', 'QR-BD0000000000000000000015', now() - interval '15 hour', now() - interval '6 day'), + ('bd-vr-016', 'bd-e-017', '안우진', '010-0000-3016', 'bdvisitor16@kintex.test', NULL, 'buyer', true, true, 'KTX-BD-0016', true, 'waiting', 'QR-BD0000000000000000000016', NULL, now() - interval '7 day'), + ('bd-vr-017', 'bd-e-018', '송소율', '010-0000-3017', 'bdvisitor17@kintex.test', '(주)비전테크', 'vip', true, false, 'KTX-BD-0017', false, 'cancelled', 'QR-BD0000000000000000000017', NULL, now() - interval '8 day'), + ('bd-vr-018', 'bd-e-019', '류유준', '010-0000-3018', 'bdvisitor18@kintex.test', '센서텍테크', 'visitor', true, true, 'KTX-BD-0018', true, 'done', 'QR-BD0000000000000000000018', now() - interval '18 hour', now() - interval '9 day'), + ('bd-vr-019', 'bd-e-020', '전채원', '010-0000-3019', 'bdvisitor19@kintex.test', '(주)머티리얼테크', 'buyer', true, false, 'KTX-BD-0019', false, 'waiting', 'QR-BD0000000000000000000019', NULL, now() - interval '10 day'), + ('bd-vr-020', 'bd-e-021', '김현우', '010-0000-3020', 'bdvisitor20@kintex.test', NULL, 'vip', true, true, 'KTX-BD-0020', true, 'cancelled', 'QR-BD0000000000000000000020', NULL, now() - interval '1 day'), + ('bd-vr-021', 'bd-e-022', '이민재', '010-0000-3021', 'bdvisitor21@kintex.test', '바이오테크', 'visitor', true, false, 'KTX-BD-0021', true, 'done', 'QR-BD0000000000000000000021', now() - interval '21 hour', now() - interval '2 day'), + ('bd-vr-022', 'bd-e-023', '박지호', '010-0000-3022', 'bdvisitor22@kintex.test', '(주)헬스랩테크', 'buyer', true, true, 'KTX-BD-0022', true, 'waiting', 'QR-BD0000000000000000000022', NULL, now() - interval '3 day'), + ('bd-vr-023', 'bd-e-024', '최하윤', '010-0000-3023', 'bdvisitor23@kintex.test', '에너지테크', 'vip', true, false, 'KTX-BD-0023', false, 'cancelled', 'QR-BD0000000000000000000023', NULL, now() - interval '4 day'), + ('bd-vr-024', 'bd-e-025', '정수빈', '010-0000-3024', 'bdvisitor24@kintex.test', NULL, 'visitor', true, true, 'KTX-BD-0024', true, 'done', 'QR-BD0000000000000000000024', now() - interval '24 hour', now() - interval '5 day'), + ('bd-vr-025', 'bd-e-026', '강지훈', '010-0000-3025', 'bdvisitor25@kintex.test', '글로벌테크', 'buyer', true, false, 'KTX-BD-0025', false, 'waiting', 'QR-BD0000000000000000000025', NULL, now() - interval '6 day'), + ('bd-vr-026', 'bd-e-027', '조시우', '010-0000-3026', 'bdvisitor26@kintex.test', '넥스트테크', 'vip', true, true, 'KTX-BD-0026', true, 'cancelled', 'QR-BD0000000000000000000026', NULL, now() - interval '7 day'), + ('bd-vr-027', 'bd-e-028', '윤지우', '010-0000-3027', 'bdvisitor27@kintex.test', '(주)미래테크', 'visitor', true, false, 'KTX-BD-0027', true, 'done', 'QR-BD0000000000000000000027', now() - interval '27 hour', now() - interval '8 day'), + ('bd-vr-028', 'bd-e-029', '장다은', '010-0000-3028', 'bdvisitor28@kintex.test', NULL, 'buyer', true, true, 'KTX-BD-0028', true, 'waiting', 'QR-BD0000000000000000000028', NULL, now() - interval '9 day'), + ('bd-vr-029', 'bd-e-030', '임선우', '010-0000-3029', 'bdvisitor29@kintex.test', '(주)대한테크', 'vip', true, false, 'KTX-BD-0029', false, 'cancelled', 'QR-BD0000000000000000000029', NULL, now() - interval '10 day'), + ('bd-vr-030', 'bd-e-031', '한서준', '010-0000-3030', 'bdvisitor30@kintex.test', '테크시스템즈', 'visitor', true, true, 'KTX-BD-0030', true, 'done', 'QR-BD0000000000000000000030', now() - interval '30 hour', now() - interval '1 day'), + ('bd-vr-031', 'bd-e-032', '오민준', '010-0000-3031', 'bdvisitor31@kintex.test', '로보틱스시스템즈', 'buyer', true, false, 'KTX-BD-0031', false, 'waiting', 'QR-BD0000000000000000000031', NULL, now() - interval '2 day'), + ('bd-vr-032', 'bd-e-033', '서수아', '010-0000-3032', 'bdvisitor32@kintex.test', NULL, 'vip', true, true, 'KTX-BD-0032', true, 'cancelled', 'QR-BD0000000000000000000032', NULL, now() - interval '3 day'), + ('bd-vr-033', 'bd-e-034', '신건우', '010-0000-3033', 'bdvisitor33@kintex.test', '시스템즈시스템즈', 'visitor', true, false, 'KTX-BD-0033', true, 'done', 'QR-BD0000000000000000000033', now() - interval '33 hour', now() - interval '4 day'), + ('bd-vr-034', 'bd-e-035', '권유나', '010-0000-3034', 'bdvisitor34@kintex.test', '(주)솔루션시스템즈', 'buyer', true, true, 'KTX-BD-0034', true, 'waiting', 'QR-BD0000000000000000000034', NULL, now() - interval '5 day'), + ('bd-vr-035', 'bd-e-036', '황예준', '010-0000-3035', 'bdvisitor35@kintex.test', '이노베이션시스템즈', 'vip', true, false, 'KTX-BD-0035', false, 'cancelled', 'QR-BD0000000000000000000035', NULL, now() - interval '6 day'), + ('bd-vr-036', 'bd-e-037', '안지유', '010-0000-3036', 'bdvisitor36@kintex.test', NULL, 'visitor', true, true, 'KTX-BD-0036', true, 'done', 'QR-BD0000000000000000000036', now() - interval '36 hour', now() - interval '7 day'), + ('bd-vr-037', 'bd-e-038', '송예은', '010-0000-3037', 'bdvisitor37@kintex.test', '(주)디자인시스템즈', 'buyer', true, false, 'KTX-BD-0037', false, 'waiting', 'QR-BD0000000000000000000037', NULL, now() - interval '8 day'), + ('bd-vr-038', 'bd-e-039', '류준서', '010-0000-3038', 'bdvisitor38@kintex.test', '엔지니어링시스템즈', 'vip', true, true, 'KTX-BD-0038', true, 'cancelled', 'QR-BD0000000000000000000038', NULL, now() - interval '9 day'), + ('bd-vr-039', 'bd-e-040', '전하준', '010-0000-3039', 'bdvisitor39@kintex.test', '(주)일렉트릭시스템즈', 'visitor', true, false, 'KTX-BD-0039', true, 'done', 'QR-BD0000000000000000000039', now() - interval '39 hour', now() - interval '10 day'), + ('bd-vr-040', 'bd-e-041', '김서윤', '010-0000-3040', 'bdvisitor40@kintex.test', NULL, 'buyer', true, true, 'KTX-BD-0040', true, 'waiting', 'QR-BD0000000000000000000040', NULL, now() - interval '1 day'), + ('bd-vr-041', 'bd-e-042', '이예린', '010-0000-3041', 'bdvisitor41@kintex.test', '미디어시스템즈', 'vip', true, false, 'KTX-BD-0041', false, 'cancelled', 'QR-BD0000000000000000000041', NULL, now() - interval '2 day'), + ('bd-vr-042', 'bd-e-043', '박도현', '010-0000-3042', 'bdvisitor42@kintex.test', '(주)디스플레이시스템즈', 'visitor', true, true, 'KTX-BD-0042', true, 'done', 'QR-BD0000000000000000000042', now() - interval '42 hour', now() - interval '3 day'), + ('bd-vr-043', 'bd-e-044', '최도윤', '010-0000-3043', 'bdvisitor43@kintex.test', '컨스트럭션시스템즈', 'buyer', true, false, 'KTX-BD-0043', false, 'waiting', 'QR-BD0000000000000000000043', NULL, now() - interval '4 day'), + ('bd-vr-044', 'bd-e-045', '정서연', '010-0000-3044', 'bdvisitor44@kintex.test', NULL, 'vip', true, true, 'KTX-BD-0044', true, 'cancelled', 'QR-BD0000000000000000000044', NULL, now() - interval '5 day'), + ('bd-vr-045', 'bd-e-046', '강지민', '010-0000-3045', 'bdvisitor45@kintex.test', '전기시설시스템즈', 'visitor', true, false, 'KTX-BD-0045', true, 'done', 'QR-BD0000000000000000000045', now() - interval '45 hour', now() - interval '6 day'), + ('bd-vr-046', 'bd-e-047', '조우진', '010-0000-3046', 'bdvisitor46@kintex.test', '구조시스템즈', 'buyer', true, true, 'KTX-BD-0046', true, 'waiting', 'QR-BD0000000000000000000046', NULL, now() - interval '7 day'), + ('bd-vr-047', 'bd-e-048', '윤소율', '010-0000-3047', 'bdvisitor47@kintex.test', '(주)비전시스템즈', 'vip', true, false, 'KTX-BD-0047', false, 'cancelled', 'QR-BD0000000000000000000047', NULL, now() - interval '8 day'), + ('bd-vr-048', 'bd-e-049', '장유준', '010-0000-3048', 'bdvisitor48@kintex.test', NULL, 'visitor', true, true, 'KTX-BD-0048', true, 'done', 'QR-BD0000000000000000000048', now() - interval '0 hour', now() - interval '9 day'), + ('bd-vr-049', 'bd-e-050', '임채원', '010-0000-3049', 'bdvisitor49@kintex.test', '(주)머티리얼시스템즈', 'buyer', true, false, 'KTX-BD-0049', false, 'waiting', 'QR-BD0000000000000000000049', NULL, now() - interval '10 day'), + ('bd-vr-050', 'bd-e-051', '한현우', '010-0000-3050', 'bdvisitor50@kintex.test', '로지스시스템즈', 'vip', true, true, 'KTX-BD-0050', true, 'cancelled', 'QR-BD0000000000000000000050', NULL, now() - interval '1 day'), + ('bd-vr-051', 'bd-e-052', '오민재', '010-0000-3051', 'bdvisitor51@kintex.test', '바이오시스템즈', 'visitor', true, false, 'KTX-BD-0051', true, 'done', 'QR-BD0000000000000000000051', now() - interval '3 hour', now() - interval '2 day'), + ('bd-vr-052', 'bd-e-053', '서지호', '010-0000-3052', 'bdvisitor52@kintex.test', NULL, 'buyer', true, true, 'KTX-BD-0052', true, 'waiting', 'QR-BD0000000000000000000052', NULL, now() - interval '3 day'), + ('bd-vr-053', 'bd-e-054', '신하윤', '010-0000-3053', 'bdvisitor53@kintex.test', '에너지시스템즈', 'vip', true, false, 'KTX-BD-0053', false, 'cancelled', 'QR-BD0000000000000000000053', NULL, now() - interval '4 day'), + ('bd-vr-054', 'bd-e-055', '권수빈', '010-0000-3054', 'bdvisitor54@kintex.test', '(주)스마트시스템즈', 'visitor', true, true, 'KTX-BD-0054', true, 'done', 'QR-BD0000000000000000000054', now() - interval '6 hour', now() - interval '5 day'), + ('bd-vr-055', 'bd-e-056', '황지훈', '010-0000-3055', 'bdvisitor55@kintex.test', '글로벌시스템즈', 'buyer', true, false, 'KTX-BD-0055', false, 'waiting', 'QR-BD0000000000000000000055', NULL, now() - interval '6 day'), + ('bd-vr-056', 'bd-e-057', '안시우', '010-0000-3056', 'bdvisitor56@kintex.test', NULL, 'vip', true, true, 'KTX-BD-0056', true, 'cancelled', 'QR-BD0000000000000000000056', NULL, now() - interval '7 day'), + ('bd-vr-057', 'bd-e-058', '송지우', '010-0000-3057', 'bdvisitor57@kintex.test', '(주)미래시스템즈', 'visitor', true, false, 'KTX-BD-0057', true, 'done', 'QR-BD0000000000000000000057', now() - interval '9 hour', now() - interval '8 day'), + ('bd-vr-058', 'bd-e-059', '류다은', '010-0000-3058', 'bdvisitor58@kintex.test', '한빛시스템즈', 'buyer', true, true, 'KTX-BD-0058', true, 'waiting', 'QR-BD0000000000000000000058', NULL, now() - interval '9 day'), + ('bd-vr-059', 'bd-e-060', '전선우', '010-0000-3059', 'bdvisitor59@kintex.test', '(주)대한시스템즈', 'vip', true, false, 'KTX-BD-0059', false, 'cancelled', 'QR-BD0000000000000000000059', NULL, now() - interval '10 day'), + ('bd-vr-060', 'bd-e-001', '김서준', '010-0000-3060', 'bdvisitor60@kintex.test', NULL, 'visitor', true, true, 'KTX-BD-0060', true, 'done', 'QR-BD0000000000000000000060', now() - interval '12 hour', now() - interval '1 day'), + ('bd-vr-061', 'bd-e-002', '이민준', '010-0000-3061', 'bdvisitor61@kintex.test', '로보틱스인더스트리', 'buyer', true, false, 'KTX-BD-0061', false, 'waiting', 'QR-BD0000000000000000000061', NULL, now() - interval '2 day'), + ('bd-vr-062', 'bd-e-003', '박수아', '010-0000-3062', 'bdvisitor62@kintex.test', '(주)인더스트리인더스트리', 'vip', true, true, 'KTX-BD-0062', true, 'cancelled', 'QR-BD0000000000000000000062', NULL, now() - interval '3 day'), + ('bd-vr-063', 'bd-e-004', '최건우', '010-0000-3063', 'bdvisitor63@kintex.test', '시스템즈인더스트리', 'visitor', true, false, 'KTX-BD-0063', true, 'done', 'QR-BD0000000000000000000063', now() - interval '15 hour', now() - interval '4 day'), + ('bd-vr-064', 'bd-e-005', '정유나', '010-0000-3064', 'bdvisitor64@kintex.test', NULL, 'buyer', true, true, 'KTX-BD-0064', true, 'waiting', 'QR-BD0000000000000000000064', NULL, now() - interval '5 day'), + ('bd-vr-065', 'bd-e-006', '강예준', '010-0000-3065', 'bdvisitor65@kintex.test', '이노베이션인더스트리', 'vip', true, false, 'KTX-BD-0065', false, 'cancelled', 'QR-BD0000000000000000000065', NULL, now() - interval '6 day'), + ('bd-vr-066', 'bd-e-007', '조지유', '010-0000-3066', 'bdvisitor66@kintex.test', '오토메이션인더스트리', 'visitor', true, true, 'KTX-BD-0066', true, 'done', 'QR-BD0000000000000000000066', now() - interval '18 hour', now() - interval '7 day'), + ('bd-vr-067', 'bd-e-008', '윤예은', '010-0000-3067', 'bdvisitor67@kintex.test', '(주)디자인인더스트리', 'buyer', true, false, 'KTX-BD-0067', false, 'waiting', 'QR-BD0000000000000000000067', NULL, now() - interval '8 day'), + ('bd-vr-068', 'bd-e-009', '장준서', '010-0000-3068', 'bdvisitor68@kintex.test', NULL, 'vip', true, true, 'KTX-BD-0068', true, 'cancelled', 'QR-BD0000000000000000000068', NULL, now() - interval '9 day'), + ('bd-vr-069', 'bd-e-010', '임하준', '010-0000-3069', 'bdvisitor69@kintex.test', '(주)일렉트릭인더스트리', 'visitor', true, false, 'KTX-BD-0069', true, 'done', 'QR-BD0000000000000000000069', now() - interval '21 hour', now() - interval '10 day'), + ('bd-vr-070', 'bd-e-011', '한서윤', '010-0000-3070', 'bdvisitor70@kintex.test', '네트웍스인더스트리', 'buyer', true, true, 'KTX-BD-0070', true, 'waiting', 'QR-BD0000000000000000000070', NULL, now() - interval '1 day'), + ('bd-vr-071', 'bd-e-012', '오예린', '010-0000-3071', 'bdvisitor71@kintex.test', '미디어인더스트리', 'vip', true, false, 'KTX-BD-0071', false, 'cancelled', 'QR-BD0000000000000000000071', NULL, now() - interval '2 day'), + ('bd-vr-072', 'bd-e-013', '서도현', '010-0000-3072', 'bdvisitor72@kintex.test', NULL, 'visitor', true, true, 'KTX-BD-0072', true, 'done', 'QR-BD0000000000000000000072', now() - interval '24 hour', now() - interval '3 day'), + ('bd-vr-073', 'bd-e-014', '신도윤', '010-0000-3073', 'bdvisitor73@kintex.test', '컨스트럭션인더스트리', 'buyer', true, false, 'KTX-BD-0073', false, 'waiting', 'QR-BD0000000000000000000073', NULL, now() - interval '4 day'), + ('bd-vr-074', 'bd-e-015', '권서연', '010-0000-3074', 'bdvisitor74@kintex.test', '(주)부스인더스트리', 'vip', true, true, 'KTX-BD-0074', true, 'cancelled', 'QR-BD0000000000000000000074', NULL, now() - interval '5 day'), + ('bd-vr-075', 'bd-e-016', '황지민', '010-0000-3075', 'bdvisitor75@kintex.test', '전기시설인더스트리', 'visitor', true, false, 'KTX-BD-0075', true, 'done', 'QR-BD0000000000000000000075', now() - interval '27 hour', now() - interval '6 day'), + ('bd-vr-076', 'bd-e-017', '안우진', '010-0000-3076', 'bdvisitor76@kintex.test', NULL, 'buyer', true, true, 'KTX-BD-0076', true, 'waiting', 'QR-BD0000000000000000000076', NULL, now() - interval '7 day'), + ('bd-vr-077', 'bd-e-018', '송소율', '010-0000-3077', 'bdvisitor77@kintex.test', '(주)비전인더스트리', 'vip', true, false, 'KTX-BD-0077', false, 'cancelled', 'QR-BD0000000000000000000077', NULL, now() - interval '8 day'), + ('bd-vr-078', 'bd-e-019', '류유준', '010-0000-3078', 'bdvisitor78@kintex.test', '센서텍인더스트리', 'visitor', true, true, 'KTX-BD-0078', true, 'done', 'QR-BD0000000000000000000078', now() - interval '30 hour', now() - interval '9 day'), + ('bd-vr-079', 'bd-e-020', '전채원', '010-0000-3079', 'bdvisitor79@kintex.test', '(주)머티리얼인더스트리', 'buyer', true, false, 'KTX-BD-0079', false, 'waiting', 'QR-BD0000000000000000000079', NULL, now() - interval '10 day'), + ('bd-vr-080', 'bd-e-021', '김현우', '010-0000-3080', 'bdvisitor80@kintex.test', NULL, 'vip', true, true, 'KTX-BD-0080', true, 'cancelled', 'QR-BD0000000000000000000080', NULL, now() - interval '1 day'), + ('bd-vr-081', 'bd-e-022', '이민재', '010-0000-3081', 'bdvisitor81@kintex.test', '로보틱스테크', 'visitor', true, false, 'KTX-BD-0081', true, 'done', 'QR-BD0000000000000000000081', now() - interval '33 hour', now() - interval '2 day'), + ('bd-vr-082', 'bd-e-023', '박지호', '010-0000-3082', 'bdvisitor82@kintex.test', '(주)인더스트리테크', 'buyer', true, true, 'KTX-BD-0082', true, 'waiting', 'QR-BD0000000000000000000082', NULL, now() - interval '3 day'), + ('bd-vr-083', 'bd-e-024', '최하윤', '010-0000-3083', 'bdvisitor83@kintex.test', '시스템즈테크', 'vip', true, false, 'KTX-BD-0083', false, 'cancelled', 'QR-BD0000000000000000000083', NULL, now() - interval '4 day'), + ('bd-vr-084', 'bd-e-025', '정수빈', '010-0000-3084', 'bdvisitor84@kintex.test', NULL, 'visitor', true, true, 'KTX-BD-0084', true, 'done', 'QR-BD0000000000000000000084', now() - interval '36 hour', now() - interval '5 day'), + ('bd-vr-085', 'bd-e-026', '강지훈', '010-0000-3085', 'bdvisitor85@kintex.test', '이노베이션테크', 'buyer', true, false, 'KTX-BD-0085', false, 'waiting', 'QR-BD0000000000000000000085', NULL, now() - interval '6 day'), + ('bd-vr-086', 'bd-e-027', '조시우', '010-0000-3086', 'bdvisitor86@kintex.test', '오토메이션테크', 'vip', true, true, 'KTX-BD-0086', true, 'cancelled', 'QR-BD0000000000000000000086', NULL, now() - interval '7 day'), + ('bd-vr-087', 'bd-e-028', '윤지우', '010-0000-3087', 'bdvisitor87@kintex.test', '(주)디자인테크', 'visitor', true, false, 'KTX-BD-0087', true, 'done', 'QR-BD0000000000000000000087', now() - interval '39 hour', now() - interval '8 day'), + ('bd-vr-088', 'bd-e-029', '장다은', '010-0000-3088', 'bdvisitor88@kintex.test', NULL, 'buyer', true, true, 'KTX-BD-0088', true, 'waiting', 'QR-BD0000000000000000000088', NULL, now() - interval '9 day'), + ('bd-vr-089', 'bd-e-030', '임선우', '010-0000-3089', 'bdvisitor89@kintex.test', '(주)일렉트릭테크', 'vip', true, false, 'KTX-BD-0089', false, 'cancelled', 'QR-BD0000000000000000000089', NULL, now() - interval '10 day'), + ('bd-vr-090', 'bd-e-031', '한서준', '010-0000-3090', 'bdvisitor90@kintex.test', '네트웍스테크', 'visitor', true, true, 'KTX-BD-0090', true, 'done', 'QR-BD0000000000000000000090', now() - interval '42 hour', now() - interval '1 day'), + ('bd-vr-091', 'bd-e-032', '오민준', '010-0000-3091', 'bdvisitor91@kintex.test', '미디어테크', 'buyer', true, false, 'KTX-BD-0091', false, 'waiting', 'QR-BD0000000000000000000091', NULL, now() - interval '2 day'), + ('bd-vr-092', 'bd-e-033', '서수아', '010-0000-3092', 'bdvisitor92@kintex.test', NULL, 'vip', true, true, 'KTX-BD-0092', true, 'cancelled', 'QR-BD0000000000000000000092', NULL, now() - interval '3 day'), + ('bd-vr-093', 'bd-e-034', '신건우', '010-0000-3093', 'bdvisitor93@kintex.test', '컨스트럭션테크', 'visitor', true, false, 'KTX-BD-0093', true, 'done', 'QR-BD0000000000000000000093', now() - interval '45 hour', now() - interval '4 day'), + ('bd-vr-094', 'bd-e-035', '권유나', '010-0000-3094', 'bdvisitor94@kintex.test', '(주)부스테크', 'buyer', true, true, 'KTX-BD-0094', true, 'waiting', 'QR-BD0000000000000000000094', NULL, now() - interval '5 day'), + ('bd-vr-095', 'bd-e-036', '황예준', '010-0000-3095', 'bdvisitor95@kintex.test', '전기시설테크', 'vip', true, false, 'KTX-BD-0095', false, 'cancelled', 'QR-BD0000000000000000000095', NULL, now() - interval '6 day'), + ('bd-vr-096', 'bd-e-037', '안지유', '010-0000-3096', 'bdvisitor96@kintex.test', NULL, 'visitor', true, true, 'KTX-BD-0096', true, 'done', 'QR-BD0000000000000000000096', now() - interval '0 hour', now() - interval '7 day'), + ('bd-vr-097', 'bd-e-038', '송예은', '010-0000-3097', 'bdvisitor97@kintex.test', '(주)비전테크', 'buyer', true, false, 'KTX-BD-0097', false, 'waiting', 'QR-BD0000000000000000000097', NULL, now() - interval '8 day'), + ('bd-vr-098', 'bd-e-039', '류준서', '010-0000-3098', 'bdvisitor98@kintex.test', '센서텍테크', 'vip', true, true, 'KTX-BD-0098', true, 'cancelled', 'QR-BD0000000000000000000098', NULL, now() - interval '9 day'), + ('bd-vr-099', 'bd-e-040', '전하준', '010-0000-3099', 'bdvisitor99@kintex.test', '(주)머티리얼테크', 'visitor', true, false, 'KTX-BD-0099', true, 'done', 'QR-BD0000000000000000000099', now() - interval '3 hour', now() - interval '10 day'), + ('bd-vr-100', 'bd-e-041', '김서윤', '010-0000-3100', 'bdvisitor100@kintex.test', NULL, 'buyer', true, true, 'KTX-BD-0100', true, 'waiting', 'QR-BD0000000000000000000100', NULL, now() - interval '1 day') +ON CONFLICT (id) DO NOTHING; + +-- 5. 리드(lead) 80 — 스코어 분포(핫≥80 다수). 일부 부스 스코프. +INSERT INTO lead (id, event_id, booth_id, exhibitor_id, name, phone, email, role, company, product, interest, score, ai_reasons, activity, followup_draft, ai_generated, collected_at) VALUES + ('bd-ld-001', 'bd-e-002', 'bd-booth-002', NULL, '최건우', '010-0000-4001', 'bdlead1@kintex.test', '기술 기획', '로보틱스테크', '데모 제품 1', 2, 26, '["부스 체류 시간 평균 대비 높음","사양서 다운로드","구매 결정권자 직급"]'::jsonb, '[{"text":"부스 QR 스캔","at":"오늘 13:20"},{"text":"제품 시연 참관","at":"오늘 13:35"}]'::jsonb, '안녕하세요. 관심 제품 견적서를 준비했습니다. 미팅 가능하실까요?', true, now() - interval '2 hour'), + ('bd-ld-002', 'bd-e-003', NULL, NULL, '조지유', '010-0000-4002', 'bdlead2@kintex.test', '연구원', '(주)인더스트리테크', '데모 제품 2', 2, 27, '["부스 체류 시간 평균 대비 높음","사양서 다운로드","구매 결정권자 직급"]'::jsonb, '[{"text":"부스 QR 스캔","at":"오늘 13:20"},{"text":"제품 시연 참관","at":"오늘 13:35"}]'::jsonb, '안녕하세요. 관심 제품 견적서를 준비했습니다. 미팅 가능하실까요?', true, now() - interval '3 hour'), + ('bd-ld-003', 'bd-e-004', 'bd-booth-004', NULL, '임하준', '010-0000-4003', 'bdlead3@kintex.test', '설비 담당', '시스템즈테크', '데모 제품 3', 2, 28, '["부스 체류 시간 평균 대비 높음","사양서 다운로드","구매 결정권자 직급"]'::jsonb, '[{"text":"부스 QR 스캔","at":"오늘 13:20"},{"text":"제품 시연 참관","at":"오늘 13:35"}]'::jsonb, '안녕하세요. 관심 제품 견적서를 준비했습니다. 미팅 가능하실까요?', true, now() - interval '4 hour'), + ('bd-ld-004', 'bd-e-005', NULL, NULL, '서도현', '010-0000-4004', 'bdlead4@kintex.test', '마케팅', '(주)솔루션테크', '데모 제품 4', 5, 84, '["부스 체류 시간 평균 대비 높음","사양서 다운로드","구매 결정권자 직급"]'::jsonb, '[{"text":"부스 QR 스캔","at":"오늘 13:20"},{"text":"제품 시연 참관","at":"오늘 13:35"}]'::jsonb, '안녕하세요. 관심 제품 견적서를 준비했습니다. 미팅 가능하실까요?', true, now() - interval '5 hour'), + ('bd-ld-005', 'bd-e-006', 'bd-booth-006', NULL, '황지민', '010-0000-4005', 'bdlead5@kintex.test', '구매 담당', '이노베이션테크', '데모 제품 5', 2, 30, '["부스 체류 시간 평균 대비 높음","사양서 다운로드","구매 결정권자 직급"]'::jsonb, '[{"text":"부스 QR 스캔","at":"오늘 13:20"},{"text":"제품 시연 참관","at":"오늘 13:35"}]'::jsonb, '안녕하세요. 관심 제품 견적서를 준비했습니다. 미팅 가능하실까요?', true, now() - interval '6 hour'), + ('bd-ld-006', 'bd-e-007', NULL, NULL, '류유준', '010-0000-4006', 'bdlead6@kintex.test', '기술 기획', '오토메이션테크', '데모 제품 6', 2, 31, '["부스 체류 시간 평균 대비 높음","사양서 다운로드","구매 결정권자 직급"]'::jsonb, '[{"text":"부스 QR 스캔","at":"오늘 13:20"},{"text":"제품 시연 참관","at":"오늘 13:35"}]'::jsonb, '안녕하세요. 관심 제품 견적서를 준비했습니다. 미팅 가능하실까요?', true, now() - interval '7 hour'), + ('bd-ld-007', 'bd-e-008', 'bd-booth-008', NULL, '이민재', '010-0000-4007', 'bdlead7@kintex.test', '연구원', '(주)디자인테크', '데모 제품 7', 2, 32, '["부스 체류 시간 평균 대비 높음","사양서 다운로드","구매 결정권자 직급"]'::jsonb, '[{"text":"부스 QR 스캔","at":"오늘 13:20"},{"text":"제품 시연 참관","at":"오늘 13:35"}]'::jsonb, '안녕하세요. 관심 제품 견적서를 준비했습니다. 미팅 가능하실까요?', true, now() - interval '8 hour'), + ('bd-ld-008', 'bd-e-009', NULL, NULL, '정수빈', '010-0000-4008', 'bdlead8@kintex.test', '설비 담당', '엔지니어링테크', '데모 제품 8', 5, 88, '["부스 체류 시간 평균 대비 높음","사양서 다운로드","구매 결정권자 직급"]'::jsonb, '[{"text":"부스 QR 스캔","at":"오늘 13:20"},{"text":"제품 시연 참관","at":"오늘 13:35"}]'::jsonb, '안녕하세요. 관심 제품 견적서를 준비했습니다. 미팅 가능하실까요?', true, now() - interval '9 hour'), + ('bd-ld-009', 'bd-e-010', 'bd-booth-010', NULL, '윤지우', '010-0000-4009', 'bdlead9@kintex.test', '마케팅', '(주)일렉트릭테크', '데모 제품 9', 2, 34, '["부스 체류 시간 평균 대비 높음","사양서 다운로드","구매 결정권자 직급"]'::jsonb, '[{"text":"부스 QR 스캔","at":"오늘 13:20"},{"text":"제품 시연 참관","at":"오늘 13:35"}]'::jsonb, '안녕하세요. 관심 제품 견적서를 준비했습니다. 미팅 가능하실까요?', true, now() - interval '10 hour'), + ('bd-ld-010', 'bd-e-011', NULL, NULL, '한서준', '010-0000-4010', 'bdlead10@kintex.test', '구매 담당', '네트웍스테크', '데모 제품 10', 2, 35, '["부스 체류 시간 평균 대비 높음","사양서 다운로드","구매 결정권자 직급"]'::jsonb, '[{"text":"부스 QR 스캔","at":"오늘 13:20"},{"text":"제품 시연 참관","at":"오늘 13:35"}]'::jsonb, '안녕하세요. 관심 제품 견적서를 준비했습니다. 미팅 가능하실까요?', true, now() - interval '11 hour'), + ('bd-ld-011', 'bd-e-012', 'bd-booth-012', NULL, '신건우', '010-0000-4011', 'bdlead11@kintex.test', '기술 기획', '미디어테크', '데모 제품 11', 2, 36, '["부스 체류 시간 평균 대비 높음","사양서 다운로드","구매 결정권자 직급"]'::jsonb, '[{"text":"부스 QR 스캔","at":"오늘 13:20"},{"text":"제품 시연 참관","at":"오늘 13:35"}]'::jsonb, '안녕하세요. 관심 제품 견적서를 준비했습니다. 미팅 가능하실까요?', true, now() - interval '12 hour'), + ('bd-ld-012', 'bd-e-013', NULL, NULL, '안지유', '010-0000-4012', 'bdlead12@kintex.test', '연구원', '(주)디스플레이테크', '데모 제품 12', 5, 92, '["부스 체류 시간 평균 대비 높음","사양서 다운로드","구매 결정권자 직급"]'::jsonb, '[{"text":"부스 QR 스캔","at":"오늘 13:20"},{"text":"제품 시연 참관","at":"오늘 13:35"}]'::jsonb, '안녕하세요. 관심 제품 견적서를 준비했습니다. 미팅 가능하실까요?', true, now() - interval '13 hour'), + ('bd-ld-013', 'bd-e-014', 'bd-booth-014', NULL, '전하준', '010-0000-4013', 'bdlead13@kintex.test', '설비 담당', '컨스트럭션테크', '데모 제품 13', 2, 38, '["부스 체류 시간 평균 대비 높음","사양서 다운로드","구매 결정권자 직급"]'::jsonb, '[{"text":"부스 QR 스캔","at":"오늘 13:20"},{"text":"제품 시연 참관","at":"오늘 13:35"}]'::jsonb, '안녕하세요. 관심 제품 견적서를 준비했습니다. 미팅 가능하실까요?', true, now() - interval '14 hour'), + ('bd-ld-014', 'bd-e-015', NULL, NULL, '박도현', '010-0000-4014', 'bdlead14@kintex.test', '마케팅', '(주)부스테크', '데모 제품 14', 2, 39, '["부스 체류 시간 평균 대비 높음","사양서 다운로드","구매 결정권자 직급"]'::jsonb, '[{"text":"부스 QR 스캔","at":"오늘 13:20"},{"text":"제품 시연 참관","at":"오늘 13:35"}]'::jsonb, '안녕하세요. 관심 제품 견적서를 준비했습니다. 미팅 가능하실까요?', true, now() - interval '15 hour'), + ('bd-ld-015', 'bd-e-016', 'bd-booth-016', NULL, '강지민', '010-0000-4015', 'bdlead15@kintex.test', '구매 담당', '전기시설테크', '데모 제품 15', 2, 40, '["부스 체류 시간 평균 대비 높음","사양서 다운로드","구매 결정권자 직급"]'::jsonb, '[{"text":"부스 QR 스캔","at":"오늘 13:20"},{"text":"제품 시연 참관","at":"오늘 13:35"}]'::jsonb, '안녕하세요. 관심 제품 견적서를 준비했습니다. 미팅 가능하실까요?', true, now() - interval '16 hour'), + ('bd-ld-016', 'bd-e-017', NULL, NULL, '장유준', '010-0000-4016', 'bdlead16@kintex.test', '기술 기획', '구조테크', '데모 제품 16', 5, 96, '["부스 체류 시간 평균 대비 높음","사양서 다운로드","구매 결정권자 직급"]'::jsonb, '[{"text":"부스 QR 스캔","at":"오늘 13:20"},{"text":"제품 시연 참관","at":"오늘 13:35"}]'::jsonb, '안녕하세요. 관심 제품 견적서를 준비했습니다. 미팅 가능하실까요?', true, now() - interval '17 hour'), + ('bd-ld-017', 'bd-e-018', 'bd-booth-018', NULL, '오민재', '010-0000-4017', 'bdlead17@kintex.test', '연구원', '(주)비전테크', '데모 제품 17', 2, 42, '["부스 체류 시간 평균 대비 높음","사양서 다운로드","구매 결정권자 직급"]'::jsonb, '[{"text":"부스 QR 스캔","at":"오늘 13:20"},{"text":"제품 시연 참관","at":"오늘 13:35"}]'::jsonb, '안녕하세요. 관심 제품 견적서를 준비했습니다. 미팅 가능하실까요?', true, now() - interval '18 hour'), + ('bd-ld-018', 'bd-e-019', NULL, NULL, '권수빈', '010-0000-4018', 'bdlead18@kintex.test', '설비 담당', '센서텍테크', '데모 제품 18', 2, 43, '["부스 체류 시간 평균 대비 높음","사양서 다운로드","구매 결정권자 직급"]'::jsonb, '[{"text":"부스 QR 스캔","at":"오늘 13:20"},{"text":"제품 시연 참관","at":"오늘 13:35"}]'::jsonb, '안녕하세요. 관심 제품 견적서를 준비했습니다. 미팅 가능하실까요?', true, now() - interval '19 hour'), + ('bd-ld-019', 'bd-e-020', 'bd-booth-020', NULL, '송지우', '010-0000-4019', 'bdlead19@kintex.test', '마케팅', '(주)머티리얼테크', '데모 제품 19', 2, 44, '["부스 체류 시간 평균 대비 높음","사양서 다운로드","구매 결정권자 직급"]'::jsonb, '[{"text":"부스 QR 스캔","at":"오늘 13:20"},{"text":"제품 시연 참관","at":"오늘 13:35"}]'::jsonb, '안녕하세요. 관심 제품 견적서를 준비했습니다. 미팅 가능하실까요?', true, now() - interval '20 hour'), + ('bd-ld-020', 'bd-e-021', NULL, NULL, '김서준', '010-0000-4020', 'bdlead20@kintex.test', '구매 담당', '로지스테크', '데모 제품 20', 5, 80, '["부스 체류 시간 평균 대비 높음","사양서 다운로드","구매 결정권자 직급"]'::jsonb, '[{"text":"부스 QR 스캔","at":"오늘 13:20"},{"text":"제품 시연 참관","at":"오늘 13:35"}]'::jsonb, '안녕하세요. 관심 제품 견적서를 준비했습니다. 미팅 가능하실까요?', true, now() - interval '1 hour'), + ('bd-ld-021', 'bd-e-022', 'bd-booth-022', NULL, '최건우', '010-0000-4021', 'bdlead21@kintex.test', '기술 기획', '바이오테크', '데모 제품 21', 2, 46, '["부스 체류 시간 평균 대비 높음","사양서 다운로드","구매 결정권자 직급"]'::jsonb, '[{"text":"부스 QR 스캔","at":"오늘 13:20"},{"text":"제품 시연 참관","at":"오늘 13:35"}]'::jsonb, '안녕하세요. 관심 제품 견적서를 준비했습니다. 미팅 가능하실까요?', true, now() - interval '2 hour'), + ('bd-ld-022', 'bd-e-023', NULL, NULL, '조지유', '010-0000-4022', 'bdlead22@kintex.test', '연구원', '(주)헬스랩테크', '데모 제품 22', 2, 47, '["부스 체류 시간 평균 대비 높음","사양서 다운로드","구매 결정권자 직급"]'::jsonb, '[{"text":"부스 QR 스캔","at":"오늘 13:20"},{"text":"제품 시연 참관","at":"오늘 13:35"}]'::jsonb, '안녕하세요. 관심 제품 견적서를 준비했습니다. 미팅 가능하실까요?', true, now() - interval '3 hour'), + ('bd-ld-023', 'bd-e-024', 'bd-booth-024', NULL, '임하준', '010-0000-4023', 'bdlead23@kintex.test', '설비 담당', '에너지테크', '데모 제품 23', 2, 48, '["부스 체류 시간 평균 대비 높음","사양서 다운로드","구매 결정권자 직급"]'::jsonb, '[{"text":"부스 QR 스캔","at":"오늘 13:20"},{"text":"제품 시연 참관","at":"오늘 13:35"}]'::jsonb, '안녕하세요. 관심 제품 견적서를 준비했습니다. 미팅 가능하실까요?', true, now() - interval '4 hour'), + ('bd-ld-024', 'bd-e-025', NULL, NULL, '서도현', '010-0000-4024', 'bdlead24@kintex.test', '마케팅', '(주)스마트테크', '데모 제품 24', 5, 84, '["부스 체류 시간 평균 대비 높음","사양서 다운로드","구매 결정권자 직급"]'::jsonb, '[{"text":"부스 QR 스캔","at":"오늘 13:20"},{"text":"제품 시연 참관","at":"오늘 13:35"}]'::jsonb, '안녕하세요. 관심 제품 견적서를 준비했습니다. 미팅 가능하실까요?', true, now() - interval '5 hour'), + ('bd-ld-025', 'bd-e-026', 'bd-booth-026', NULL, '황지민', '010-0000-4025', 'bdlead25@kintex.test', '구매 담당', '글로벌테크', '데모 제품 25', 3, 50, '["부스 체류 시간 평균 대비 높음","사양서 다운로드","구매 결정권자 직급"]'::jsonb, '[{"text":"부스 QR 스캔","at":"오늘 13:20"},{"text":"제품 시연 참관","at":"오늘 13:35"}]'::jsonb, '안녕하세요. 관심 제품 견적서를 준비했습니다. 미팅 가능하실까요?', true, now() - interval '6 hour'), + ('bd-ld-026', 'bd-e-027', NULL, NULL, '류유준', '010-0000-4026', 'bdlead26@kintex.test', '기술 기획', '넥스트테크', '데모 제품 26', 3, 51, '["부스 체류 시간 평균 대비 높음","사양서 다운로드","구매 결정권자 직급"]'::jsonb, '[{"text":"부스 QR 스캔","at":"오늘 13:20"},{"text":"제품 시연 참관","at":"오늘 13:35"}]'::jsonb, '안녕하세요. 관심 제품 견적서를 준비했습니다. 미팅 가능하실까요?', true, now() - interval '7 hour'), + ('bd-ld-027', 'bd-e-028', 'bd-booth-028', NULL, '이민재', '010-0000-4027', 'bdlead27@kintex.test', '연구원', '(주)미래테크', '데모 제품 27', 3, 52, '["부스 체류 시간 평균 대비 높음","사양서 다운로드","구매 결정권자 직급"]'::jsonb, '[{"text":"부스 QR 스캔","at":"오늘 13:20"},{"text":"제품 시연 참관","at":"오늘 13:35"}]'::jsonb, '안녕하세요. 관심 제품 견적서를 준비했습니다. 미팅 가능하실까요?', true, now() - interval '8 hour'), + ('bd-ld-028', 'bd-e-029', NULL, NULL, '정수빈', '010-0000-4028', 'bdlead28@kintex.test', '설비 담당', '한빛테크', '데모 제품 28', 5, 88, '["부스 체류 시간 평균 대비 높음","사양서 다운로드","구매 결정권자 직급"]'::jsonb, '[{"text":"부스 QR 스캔","at":"오늘 13:20"},{"text":"제품 시연 참관","at":"오늘 13:35"}]'::jsonb, '안녕하세요. 관심 제품 견적서를 준비했습니다. 미팅 가능하실까요?', true, now() - interval '9 hour'), + ('bd-ld-029', 'bd-e-030', 'bd-booth-030', NULL, '윤지우', '010-0000-4029', 'bdlead29@kintex.test', '마케팅', '(주)대한테크', '데모 제품 29', 3, 54, '["부스 체류 시간 평균 대비 높음","사양서 다운로드","구매 결정권자 직급"]'::jsonb, '[{"text":"부스 QR 스캔","at":"오늘 13:20"},{"text":"제품 시연 참관","at":"오늘 13:35"}]'::jsonb, '안녕하세요. 관심 제품 견적서를 준비했습니다. 미팅 가능하실까요?', true, now() - interval '10 hour'), + ('bd-ld-030', 'bd-e-031', NULL, NULL, '한서준', '010-0000-4030', 'bdlead30@kintex.test', '구매 담당', '테크시스템즈', '데모 제품 30', 3, 55, '["부스 체류 시간 평균 대비 높음","사양서 다운로드","구매 결정권자 직급"]'::jsonb, '[{"text":"부스 QR 스캔","at":"오늘 13:20"},{"text":"제품 시연 참관","at":"오늘 13:35"}]'::jsonb, '안녕하세요. 관심 제품 견적서를 준비했습니다. 미팅 가능하실까요?', true, now() - interval '11 hour'), + ('bd-ld-031', 'bd-e-032', 'bd-booth-032', NULL, '신건우', '010-0000-4031', 'bdlead31@kintex.test', '기술 기획', '로보틱스시스템즈', '데모 제품 31', 3, 56, '["부스 체류 시간 평균 대비 높음","사양서 다운로드","구매 결정권자 직급"]'::jsonb, '[{"text":"부스 QR 스캔","at":"오늘 13:20"},{"text":"제품 시연 참관","at":"오늘 13:35"}]'::jsonb, '안녕하세요. 관심 제품 견적서를 준비했습니다. 미팅 가능하실까요?', true, now() - interval '12 hour'), + ('bd-ld-032', 'bd-e-033', NULL, NULL, '안지유', '010-0000-4032', 'bdlead32@kintex.test', '연구원', '(주)인더스트리시스템즈', '데모 제품 32', 5, 92, '["부스 체류 시간 평균 대비 높음","사양서 다운로드","구매 결정권자 직급"]'::jsonb, '[{"text":"부스 QR 스캔","at":"오늘 13:20"},{"text":"제품 시연 참관","at":"오늘 13:35"}]'::jsonb, '안녕하세요. 관심 제품 견적서를 준비했습니다. 미팅 가능하실까요?', true, now() - interval '13 hour'), + ('bd-ld-033', 'bd-e-034', 'bd-booth-034', NULL, '전하준', '010-0000-4033', 'bdlead33@kintex.test', '설비 담당', '시스템즈시스템즈', '데모 제품 33', 3, 58, '["부스 체류 시간 평균 대비 높음","사양서 다운로드","구매 결정권자 직급"]'::jsonb, '[{"text":"부스 QR 스캔","at":"오늘 13:20"},{"text":"제품 시연 참관","at":"오늘 13:35"}]'::jsonb, '안녕하세요. 관심 제품 견적서를 준비했습니다. 미팅 가능하실까요?', true, now() - interval '14 hour'), + ('bd-ld-034', 'bd-e-035', NULL, NULL, '박도현', '010-0000-4034', 'bdlead34@kintex.test', '마케팅', '(주)솔루션시스템즈', '데모 제품 34', 3, 59, '["부스 체류 시간 평균 대비 높음","사양서 다운로드","구매 결정권자 직급"]'::jsonb, '[{"text":"부스 QR 스캔","at":"오늘 13:20"},{"text":"제품 시연 참관","at":"오늘 13:35"}]'::jsonb, '안녕하세요. 관심 제품 견적서를 준비했습니다. 미팅 가능하실까요?', true, now() - interval '15 hour'), + ('bd-ld-035', 'bd-e-036', 'bd-booth-036', NULL, '강지민', '010-0000-4035', 'bdlead35@kintex.test', '구매 담당', '이노베이션시스템즈', '데모 제품 35', 3, 60, '["부스 체류 시간 평균 대비 높음","사양서 다운로드","구매 결정권자 직급"]'::jsonb, '[{"text":"부스 QR 스캔","at":"오늘 13:20"},{"text":"제품 시연 참관","at":"오늘 13:35"}]'::jsonb, '안녕하세요. 관심 제품 견적서를 준비했습니다. 미팅 가능하실까요?', true, now() - interval '16 hour'), + ('bd-ld-036', 'bd-e-037', NULL, NULL, '장유준', '010-0000-4036', 'bdlead36@kintex.test', '기술 기획', '오토메이션시스템즈', '데모 제품 36', 5, 96, '["부스 체류 시간 평균 대비 높음","사양서 다운로드","구매 결정권자 직급"]'::jsonb, '[{"text":"부스 QR 스캔","at":"오늘 13:20"},{"text":"제품 시연 참관","at":"오늘 13:35"}]'::jsonb, '안녕하세요. 관심 제품 견적서를 준비했습니다. 미팅 가능하실까요?', true, now() - interval '17 hour'), + ('bd-ld-037', 'bd-e-038', 'bd-booth-038', NULL, '오민재', '010-0000-4037', 'bdlead37@kintex.test', '연구원', '(주)디자인시스템즈', '데모 제품 37', 3, 62, '["부스 체류 시간 평균 대비 높음","사양서 다운로드","구매 결정권자 직급"]'::jsonb, '[{"text":"부스 QR 스캔","at":"오늘 13:20"},{"text":"제품 시연 참관","at":"오늘 13:35"}]'::jsonb, '안녕하세요. 관심 제품 견적서를 준비했습니다. 미팅 가능하실까요?', true, now() - interval '18 hour'), + ('bd-ld-038', 'bd-e-039', NULL, NULL, '권수빈', '010-0000-4038', 'bdlead38@kintex.test', '설비 담당', '엔지니어링시스템즈', '데모 제품 38', 3, 63, '["부스 체류 시간 평균 대비 높음","사양서 다운로드","구매 결정권자 직급"]'::jsonb, '[{"text":"부스 QR 스캔","at":"오늘 13:20"},{"text":"제품 시연 참관","at":"오늘 13:35"}]'::jsonb, '안녕하세요. 관심 제품 견적서를 준비했습니다. 미팅 가능하실까요?', true, now() - interval '19 hour'), + ('bd-ld-039', 'bd-e-040', 'bd-booth-040', NULL, '송지우', '010-0000-4039', 'bdlead39@kintex.test', '마케팅', '(주)일렉트릭시스템즈', '데모 제품 39', 3, 64, '["부스 체류 시간 평균 대비 높음","사양서 다운로드","구매 결정권자 직급"]'::jsonb, '[{"text":"부스 QR 스캔","at":"오늘 13:20"},{"text":"제품 시연 참관","at":"오늘 13:35"}]'::jsonb, '안녕하세요. 관심 제품 견적서를 준비했습니다. 미팅 가능하실까요?', true, now() - interval '20 hour'), + ('bd-ld-040', 'bd-e-041', NULL, NULL, '김서준', '010-0000-4040', 'bdlead40@kintex.test', '구매 담당', '네트웍스시스템즈', '데모 제품 40', 5, 80, '["부스 체류 시간 평균 대비 높음","사양서 다운로드","구매 결정권자 직급"]'::jsonb, '[{"text":"부스 QR 스캔","at":"오늘 13:20"},{"text":"제품 시연 참관","at":"오늘 13:35"}]'::jsonb, '안녕하세요. 관심 제품 견적서를 준비했습니다. 미팅 가능하실까요?', true, now() - interval '1 hour'), + ('bd-ld-041', 'bd-e-042', 'bd-booth-042', NULL, '최건우', '010-0000-4041', 'bdlead41@kintex.test', '기술 기획', '미디어시스템즈', '데모 제품 41', 3, 66, '["부스 체류 시간 평균 대비 높음","사양서 다운로드","구매 결정권자 직급"]'::jsonb, '[{"text":"부스 QR 스캔","at":"오늘 13:20"},{"text":"제품 시연 참관","at":"오늘 13:35"}]'::jsonb, '안녕하세요. 관심 제품 견적서를 준비했습니다. 미팅 가능하실까요?', true, now() - interval '2 hour'), + ('bd-ld-042', 'bd-e-043', NULL, NULL, '조지유', '010-0000-4042', 'bdlead42@kintex.test', '연구원', '(주)디스플레이시스템즈', '데모 제품 42', 3, 67, '["부스 체류 시간 평균 대비 높음","사양서 다운로드","구매 결정권자 직급"]'::jsonb, '[{"text":"부스 QR 스캔","at":"오늘 13:20"},{"text":"제품 시연 참관","at":"오늘 13:35"}]'::jsonb, '안녕하세요. 관심 제품 견적서를 준비했습니다. 미팅 가능하실까요?', true, now() - interval '3 hour'), + ('bd-ld-043', 'bd-e-044', 'bd-booth-044', NULL, '임하준', '010-0000-4043', 'bdlead43@kintex.test', '설비 담당', '컨스트럭션시스템즈', '데모 제품 43', 3, 68, '["부스 체류 시간 평균 대비 높음","사양서 다운로드","구매 결정권자 직급"]'::jsonb, '[{"text":"부스 QR 스캔","at":"오늘 13:20"},{"text":"제품 시연 참관","at":"오늘 13:35"}]'::jsonb, '안녕하세요. 관심 제품 견적서를 준비했습니다. 미팅 가능하실까요?', true, now() - interval '4 hour'), + ('bd-ld-044', 'bd-e-045', NULL, NULL, '서도현', '010-0000-4044', 'bdlead44@kintex.test', '마케팅', '(주)부스시스템즈', '데모 제품 44', 5, 84, '["부스 체류 시간 평균 대비 높음","사양서 다운로드","구매 결정권자 직급"]'::jsonb, '[{"text":"부스 QR 스캔","at":"오늘 13:20"},{"text":"제품 시연 참관","at":"오늘 13:35"}]'::jsonb, '안녕하세요. 관심 제품 견적서를 준비했습니다. 미팅 가능하실까요?', true, now() - interval '5 hour'), + ('bd-ld-045', 'bd-e-046', 'bd-booth-046', NULL, '황지민', '010-0000-4045', 'bdlead45@kintex.test', '구매 담당', '전기시설시스템즈', '데모 제품 45', 3, 70, '["부스 체류 시간 평균 대비 높음","사양서 다운로드","구매 결정권자 직급"]'::jsonb, '[{"text":"부스 QR 스캔","at":"오늘 13:20"},{"text":"제품 시연 참관","at":"오늘 13:35"}]'::jsonb, '안녕하세요. 관심 제품 견적서를 준비했습니다. 미팅 가능하실까요?', true, now() - interval '6 hour'), + ('bd-ld-046', 'bd-e-047', NULL, NULL, '류유준', '010-0000-4046', 'bdlead46@kintex.test', '기술 기획', '구조시스템즈', '데모 제품 46', 3, 71, '["부스 체류 시간 평균 대비 높음","사양서 다운로드","구매 결정권자 직급"]'::jsonb, '[{"text":"부스 QR 스캔","at":"오늘 13:20"},{"text":"제품 시연 참관","at":"오늘 13:35"}]'::jsonb, '안녕하세요. 관심 제품 견적서를 준비했습니다. 미팅 가능하실까요?', true, now() - interval '7 hour'), + ('bd-ld-047', 'bd-e-048', 'bd-booth-048', NULL, '이민재', '010-0000-4047', 'bdlead47@kintex.test', '연구원', '(주)비전시스템즈', '데모 제품 47', 3, 72, '["부스 체류 시간 평균 대비 높음","사양서 다운로드","구매 결정권자 직급"]'::jsonb, '[{"text":"부스 QR 스캔","at":"오늘 13:20"},{"text":"제품 시연 참관","at":"오늘 13:35"}]'::jsonb, '안녕하세요. 관심 제품 견적서를 준비했습니다. 미팅 가능하실까요?', true, now() - interval '8 hour'), + ('bd-ld-048', 'bd-e-049', NULL, NULL, '정수빈', '010-0000-4048', 'bdlead48@kintex.test', '설비 담당', '센서텍시스템즈', '데모 제품 48', 5, 88, '["부스 체류 시간 평균 대비 높음","사양서 다운로드","구매 결정권자 직급"]'::jsonb, '[{"text":"부스 QR 스캔","at":"오늘 13:20"},{"text":"제품 시연 참관","at":"오늘 13:35"}]'::jsonb, '안녕하세요. 관심 제품 견적서를 준비했습니다. 미팅 가능하실까요?', true, now() - interval '9 hour'), + ('bd-ld-049', 'bd-e-050', 'bd-booth-050', NULL, '윤지우', '010-0000-4049', 'bdlead49@kintex.test', '마케팅', '(주)머티리얼시스템즈', '데모 제품 49', 3, 74, '["부스 체류 시간 평균 대비 높음","사양서 다운로드","구매 결정권자 직급"]'::jsonb, '[{"text":"부스 QR 스캔","at":"오늘 13:20"},{"text":"제품 시연 참관","at":"오늘 13:35"}]'::jsonb, '안녕하세요. 관심 제품 견적서를 준비했습니다. 미팅 가능하실까요?', true, now() - interval '10 hour'), + ('bd-ld-050', 'bd-e-051', NULL, NULL, '한서준', '010-0000-4050', 'bdlead50@kintex.test', '구매 담당', '로지스시스템즈', '데모 제품 50', 3, 75, '["부스 체류 시간 평균 대비 높음","사양서 다운로드","구매 결정권자 직급"]'::jsonb, '[{"text":"부스 QR 스캔","at":"오늘 13:20"},{"text":"제품 시연 참관","at":"오늘 13:35"}]'::jsonb, '안녕하세요. 관심 제품 견적서를 준비했습니다. 미팅 가능하실까요?', true, now() - interval '11 hour'), + ('bd-ld-051', 'bd-e-052', 'bd-booth-052', NULL, '신건우', '010-0000-4051', 'bdlead51@kintex.test', '기술 기획', '바이오시스템즈', '데모 제품 51', 3, 76, '["부스 체류 시간 평균 대비 높음","사양서 다운로드","구매 결정권자 직급"]'::jsonb, '[{"text":"부스 QR 스캔","at":"오늘 13:20"},{"text":"제품 시연 참관","at":"오늘 13:35"}]'::jsonb, '안녕하세요. 관심 제품 견적서를 준비했습니다. 미팅 가능하실까요?', true, now() - interval '12 hour'), + ('bd-ld-052', 'bd-e-053', NULL, NULL, '안지유', '010-0000-4052', 'bdlead52@kintex.test', '연구원', '(주)헬스랩시스템즈', '데모 제품 52', 5, 92, '["부스 체류 시간 평균 대비 높음","사양서 다운로드","구매 결정권자 직급"]'::jsonb, '[{"text":"부스 QR 스캔","at":"오늘 13:20"},{"text":"제품 시연 참관","at":"오늘 13:35"}]'::jsonb, '안녕하세요. 관심 제품 견적서를 준비했습니다. 미팅 가능하실까요?', true, now() - interval '13 hour'), + ('bd-ld-053', 'bd-e-054', 'bd-booth-054', NULL, '전하준', '010-0000-4053', 'bdlead53@kintex.test', '설비 담당', '에너지시스템즈', '데모 제품 53', 3, 78, '["부스 체류 시간 평균 대비 높음","사양서 다운로드","구매 결정권자 직급"]'::jsonb, '[{"text":"부스 QR 스캔","at":"오늘 13:20"},{"text":"제품 시연 참관","at":"오늘 13:35"}]'::jsonb, '안녕하세요. 관심 제품 견적서를 준비했습니다. 미팅 가능하실까요?', true, now() - interval '14 hour'), + ('bd-ld-054', 'bd-e-055', NULL, NULL, '박도현', '010-0000-4054', 'bdlead54@kintex.test', '마케팅', '(주)스마트시스템즈', '데모 제품 54', 3, 79, '["부스 체류 시간 평균 대비 높음","사양서 다운로드","구매 결정권자 직급"]'::jsonb, '[{"text":"부스 QR 스캔","at":"오늘 13:20"},{"text":"제품 시연 참관","at":"오늘 13:35"}]'::jsonb, '안녕하세요. 관심 제품 견적서를 준비했습니다. 미팅 가능하실까요?', true, now() - interval '15 hour'), + ('bd-ld-055', 'bd-e-056', 'bd-booth-056', NULL, '강지민', '010-0000-4055', 'bdlead55@kintex.test', '구매 담당', '글로벌시스템즈', '데모 제품 55', 2, 25, '["부스 체류 시간 평균 대비 높음","사양서 다운로드","구매 결정권자 직급"]'::jsonb, '[{"text":"부스 QR 스캔","at":"오늘 13:20"},{"text":"제품 시연 참관","at":"오늘 13:35"}]'::jsonb, '안녕하세요. 관심 제품 견적서를 준비했습니다. 미팅 가능하실까요?', true, now() - interval '16 hour'), + ('bd-ld-056', 'bd-e-057', NULL, NULL, '장유준', '010-0000-4056', 'bdlead56@kintex.test', '기술 기획', '넥스트시스템즈', '데모 제품 56', 5, 96, '["부스 체류 시간 평균 대비 높음","사양서 다운로드","구매 결정권자 직급"]'::jsonb, '[{"text":"부스 QR 스캔","at":"오늘 13:20"},{"text":"제품 시연 참관","at":"오늘 13:35"}]'::jsonb, '안녕하세요. 관심 제품 견적서를 준비했습니다. 미팅 가능하실까요?', true, now() - interval '17 hour'), + ('bd-ld-057', 'bd-e-058', 'bd-booth-058', NULL, '오민재', '010-0000-4057', 'bdlead57@kintex.test', '연구원', '(주)미래시스템즈', '데모 제품 57', 2, 27, '["부스 체류 시간 평균 대비 높음","사양서 다운로드","구매 결정권자 직급"]'::jsonb, '[{"text":"부스 QR 스캔","at":"오늘 13:20"},{"text":"제품 시연 참관","at":"오늘 13:35"}]'::jsonb, '안녕하세요. 관심 제품 견적서를 준비했습니다. 미팅 가능하실까요?', true, now() - interval '18 hour'), + ('bd-ld-058', 'bd-e-059', NULL, NULL, '권수빈', '010-0000-4058', 'bdlead58@kintex.test', '설비 담당', '한빛시스템즈', '데모 제품 58', 2, 28, '["부스 체류 시간 평균 대비 높음","사양서 다운로드","구매 결정권자 직급"]'::jsonb, '[{"text":"부스 QR 스캔","at":"오늘 13:20"},{"text":"제품 시연 참관","at":"오늘 13:35"}]'::jsonb, '안녕하세요. 관심 제품 견적서를 준비했습니다. 미팅 가능하실까요?', true, now() - interval '19 hour'), + ('bd-ld-059', 'bd-e-060', 'bd-booth-060', NULL, '송지우', '010-0000-4059', 'bdlead59@kintex.test', '마케팅', '(주)대한시스템즈', '데모 제품 59', 2, 29, '["부스 체류 시간 평균 대비 높음","사양서 다운로드","구매 결정권자 직급"]'::jsonb, '[{"text":"부스 QR 스캔","at":"오늘 13:20"},{"text":"제품 시연 참관","at":"오늘 13:35"}]'::jsonb, '안녕하세요. 관심 제품 견적서를 준비했습니다. 미팅 가능하실까요?', true, now() - interval '20 hour'), + ('bd-ld-060', 'bd-e-001', NULL, NULL, '김서준', '010-0000-4060', 'bdlead60@kintex.test', '구매 담당', '테크인더스트리', '데모 제품 60', 5, 80, '["부스 체류 시간 평균 대비 높음","사양서 다운로드","구매 결정권자 직급"]'::jsonb, '[{"text":"부스 QR 스캔","at":"오늘 13:20"},{"text":"제품 시연 참관","at":"오늘 13:35"}]'::jsonb, '안녕하세요. 관심 제품 견적서를 준비했습니다. 미팅 가능하실까요?', true, now() - interval '1 hour'), + ('bd-ld-061', 'bd-e-002', 'bd-booth-062', NULL, '최건우', '010-0000-4061', 'bdlead61@kintex.test', '기술 기획', '로보틱스인더스트리', '데모 제품 61', 2, 31, '["부스 체류 시간 평균 대비 높음","사양서 다운로드","구매 결정권자 직급"]'::jsonb, '[{"text":"부스 QR 스캔","at":"오늘 13:20"},{"text":"제품 시연 참관","at":"오늘 13:35"}]'::jsonb, '안녕하세요. 관심 제품 견적서를 준비했습니다. 미팅 가능하실까요?', true, now() - interval '2 hour'), + ('bd-ld-062', 'bd-e-003', NULL, NULL, '조지유', '010-0000-4062', 'bdlead62@kintex.test', '연구원', '(주)인더스트리인더스트리', '데모 제품 62', 2, 32, '["부스 체류 시간 평균 대비 높음","사양서 다운로드","구매 결정권자 직급"]'::jsonb, '[{"text":"부스 QR 스캔","at":"오늘 13:20"},{"text":"제품 시연 참관","at":"오늘 13:35"}]'::jsonb, '안녕하세요. 관심 제품 견적서를 준비했습니다. 미팅 가능하실까요?', true, now() - interval '3 hour'), + ('bd-ld-063', 'bd-e-004', 'bd-booth-064', NULL, '임하준', '010-0000-4063', 'bdlead63@kintex.test', '설비 담당', '시스템즈인더스트리', '데모 제품 63', 2, 33, '["부스 체류 시간 평균 대비 높음","사양서 다운로드","구매 결정권자 직급"]'::jsonb, '[{"text":"부스 QR 스캔","at":"오늘 13:20"},{"text":"제품 시연 참관","at":"오늘 13:35"}]'::jsonb, '안녕하세요. 관심 제품 견적서를 준비했습니다. 미팅 가능하실까요?', true, now() - interval '4 hour'), + ('bd-ld-064', 'bd-e-005', NULL, NULL, '서도현', '010-0000-4064', 'bdlead64@kintex.test', '마케팅', '(주)솔루션인더스트리', '데모 제품 64', 5, 84, '["부스 체류 시간 평균 대비 높음","사양서 다운로드","구매 결정권자 직급"]'::jsonb, '[{"text":"부스 QR 스캔","at":"오늘 13:20"},{"text":"제품 시연 참관","at":"오늘 13:35"}]'::jsonb, '안녕하세요. 관심 제품 견적서를 준비했습니다. 미팅 가능하실까요?', true, now() - interval '5 hour'), + ('bd-ld-065', 'bd-e-006', 'bd-booth-066', NULL, '황지민', '010-0000-4065', 'bdlead65@kintex.test', '구매 담당', '이노베이션인더스트리', '데모 제품 65', 2, 35, '["부스 체류 시간 평균 대비 높음","사양서 다운로드","구매 결정권자 직급"]'::jsonb, '[{"text":"부스 QR 스캔","at":"오늘 13:20"},{"text":"제품 시연 참관","at":"오늘 13:35"}]'::jsonb, '안녕하세요. 관심 제품 견적서를 준비했습니다. 미팅 가능하실까요?', true, now() - interval '6 hour'), + ('bd-ld-066', 'bd-e-007', NULL, NULL, '류유준', '010-0000-4066', 'bdlead66@kintex.test', '기술 기획', '오토메이션인더스트리', '데모 제품 66', 2, 36, '["부스 체류 시간 평균 대비 높음","사양서 다운로드","구매 결정권자 직급"]'::jsonb, '[{"text":"부스 QR 스캔","at":"오늘 13:20"},{"text":"제품 시연 참관","at":"오늘 13:35"}]'::jsonb, '안녕하세요. 관심 제품 견적서를 준비했습니다. 미팅 가능하실까요?', true, now() - interval '7 hour'), + ('bd-ld-067', 'bd-e-008', 'bd-booth-068', NULL, '이민재', '010-0000-4067', 'bdlead67@kintex.test', '연구원', '(주)디자인인더스트리', '데모 제품 67', 2, 37, '["부스 체류 시간 평균 대비 높음","사양서 다운로드","구매 결정권자 직급"]'::jsonb, '[{"text":"부스 QR 스캔","at":"오늘 13:20"},{"text":"제품 시연 참관","at":"오늘 13:35"}]'::jsonb, '안녕하세요. 관심 제품 견적서를 준비했습니다. 미팅 가능하실까요?', true, now() - interval '8 hour'), + ('bd-ld-068', 'bd-e-009', NULL, NULL, '정수빈', '010-0000-4068', 'bdlead68@kintex.test', '설비 담당', '엔지니어링인더스트리', '데모 제품 68', 5, 88, '["부스 체류 시간 평균 대비 높음","사양서 다운로드","구매 결정권자 직급"]'::jsonb, '[{"text":"부스 QR 스캔","at":"오늘 13:20"},{"text":"제품 시연 참관","at":"오늘 13:35"}]'::jsonb, '안녕하세요. 관심 제품 견적서를 준비했습니다. 미팅 가능하실까요?', true, now() - interval '9 hour'), + ('bd-ld-069', 'bd-e-010', 'bd-booth-070', NULL, '윤지우', '010-0000-4069', 'bdlead69@kintex.test', '마케팅', '(주)일렉트릭인더스트리', '데모 제품 69', 2, 39, '["부스 체류 시간 평균 대비 높음","사양서 다운로드","구매 결정권자 직급"]'::jsonb, '[{"text":"부스 QR 스캔","at":"오늘 13:20"},{"text":"제품 시연 참관","at":"오늘 13:35"}]'::jsonb, '안녕하세요. 관심 제품 견적서를 준비했습니다. 미팅 가능하실까요?', true, now() - interval '10 hour'), + ('bd-ld-070', 'bd-e-011', NULL, NULL, '한서준', '010-0000-4070', 'bdlead70@kintex.test', '구매 담당', '네트웍스인더스트리', '데모 제품 70', 2, 40, '["부스 체류 시간 평균 대비 높음","사양서 다운로드","구매 결정권자 직급"]'::jsonb, '[{"text":"부스 QR 스캔","at":"오늘 13:20"},{"text":"제품 시연 참관","at":"오늘 13:35"}]'::jsonb, '안녕하세요. 관심 제품 견적서를 준비했습니다. 미팅 가능하실까요?', true, now() - interval '11 hour'), + ('bd-ld-071', 'bd-e-012', 'bd-booth-072', NULL, '신건우', '010-0000-4071', 'bdlead71@kintex.test', '기술 기획', '미디어인더스트리', '데모 제품 71', 2, 41, '["부스 체류 시간 평균 대비 높음","사양서 다운로드","구매 결정권자 직급"]'::jsonb, '[{"text":"부스 QR 스캔","at":"오늘 13:20"},{"text":"제품 시연 참관","at":"오늘 13:35"}]'::jsonb, '안녕하세요. 관심 제품 견적서를 준비했습니다. 미팅 가능하실까요?', true, now() - interval '12 hour'), + ('bd-ld-072', 'bd-e-013', NULL, NULL, '안지유', '010-0000-4072', 'bdlead72@kintex.test', '연구원', '(주)디스플레이인더스트리', '데모 제품 72', 5, 92, '["부스 체류 시간 평균 대비 높음","사양서 다운로드","구매 결정권자 직급"]'::jsonb, '[{"text":"부스 QR 스캔","at":"오늘 13:20"},{"text":"제품 시연 참관","at":"오늘 13:35"}]'::jsonb, '안녕하세요. 관심 제품 견적서를 준비했습니다. 미팅 가능하실까요?', true, now() - interval '13 hour'), + ('bd-ld-073', 'bd-e-014', 'bd-booth-074', NULL, '전하준', '010-0000-4073', 'bdlead73@kintex.test', '설비 담당', '컨스트럭션인더스트리', '데모 제품 73', 2, 43, '["부스 체류 시간 평균 대비 높음","사양서 다운로드","구매 결정권자 직급"]'::jsonb, '[{"text":"부스 QR 스캔","at":"오늘 13:20"},{"text":"제품 시연 참관","at":"오늘 13:35"}]'::jsonb, '안녕하세요. 관심 제품 견적서를 준비했습니다. 미팅 가능하실까요?', true, now() - interval '14 hour'), + ('bd-ld-074', 'bd-e-015', NULL, NULL, '박도현', '010-0000-4074', 'bdlead74@kintex.test', '마케팅', '(주)부스인더스트리', '데모 제품 74', 2, 44, '["부스 체류 시간 평균 대비 높음","사양서 다운로드","구매 결정권자 직급"]'::jsonb, '[{"text":"부스 QR 스캔","at":"오늘 13:20"},{"text":"제품 시연 참관","at":"오늘 13:35"}]'::jsonb, '안녕하세요. 관심 제품 견적서를 준비했습니다. 미팅 가능하실까요?', true, now() - interval '15 hour'), + ('bd-ld-075', 'bd-e-016', 'bd-booth-076', NULL, '강지민', '010-0000-4075', 'bdlead75@kintex.test', '구매 담당', '전기시설인더스트리', '데모 제품 75', 2, 45, '["부스 체류 시간 평균 대비 높음","사양서 다운로드","구매 결정권자 직급"]'::jsonb, '[{"text":"부스 QR 스캔","at":"오늘 13:20"},{"text":"제품 시연 참관","at":"오늘 13:35"}]'::jsonb, '안녕하세요. 관심 제품 견적서를 준비했습니다. 미팅 가능하실까요?', true, now() - interval '16 hour'), + ('bd-ld-076', 'bd-e-017', NULL, NULL, '장유준', '010-0000-4076', 'bdlead76@kintex.test', '기술 기획', '구조인더스트리', '데모 제품 76', 5, 96, '["부스 체류 시간 평균 대비 높음","사양서 다운로드","구매 결정권자 직급"]'::jsonb, '[{"text":"부스 QR 스캔","at":"오늘 13:20"},{"text":"제품 시연 참관","at":"오늘 13:35"}]'::jsonb, '안녕하세요. 관심 제품 견적서를 준비했습니다. 미팅 가능하실까요?', true, now() - interval '17 hour'), + ('bd-ld-077', 'bd-e-018', 'bd-booth-078', NULL, '오민재', '010-0000-4077', 'bdlead77@kintex.test', '연구원', '(주)비전인더스트리', '데모 제품 77', 2, 47, '["부스 체류 시간 평균 대비 높음","사양서 다운로드","구매 결정권자 직급"]'::jsonb, '[{"text":"부스 QR 스캔","at":"오늘 13:20"},{"text":"제품 시연 참관","at":"오늘 13:35"}]'::jsonb, '안녕하세요. 관심 제품 견적서를 준비했습니다. 미팅 가능하실까요?', true, now() - interval '18 hour'), + ('bd-ld-078', 'bd-e-019', NULL, NULL, '권수빈', '010-0000-4078', 'bdlead78@kintex.test', '설비 담당', '센서텍인더스트리', '데모 제품 78', 2, 48, '["부스 체류 시간 평균 대비 높음","사양서 다운로드","구매 결정권자 직급"]'::jsonb, '[{"text":"부스 QR 스캔","at":"오늘 13:20"},{"text":"제품 시연 참관","at":"오늘 13:35"}]'::jsonb, '안녕하세요. 관심 제품 견적서를 준비했습니다. 미팅 가능하실까요?', true, now() - interval '19 hour'), + ('bd-ld-079', 'bd-e-020', 'bd-booth-080', NULL, '송지우', '010-0000-4079', 'bdlead79@kintex.test', '마케팅', '(주)머티리얼인더스트리', '데모 제품 79', 2, 49, '["부스 체류 시간 평균 대비 높음","사양서 다운로드","구매 결정권자 직급"]'::jsonb, '[{"text":"부스 QR 스캔","at":"오늘 13:20"},{"text":"제품 시연 참관","at":"오늘 13:35"}]'::jsonb, '안녕하세요. 관심 제품 견적서를 준비했습니다. 미팅 가능하실까요?', true, now() - interval '20 hour'), + ('bd-ld-080', 'bd-e-021', NULL, NULL, '김서준', '010-0000-4080', 'bdlead80@kintex.test', '구매 담당', '테크테크', '데모 제품 80', 5, 80, '["부스 체류 시간 평균 대비 높음","사양서 다운로드","구매 결정권자 직급"]'::jsonb, '[{"text":"부스 QR 스캔","at":"오늘 13:20"},{"text":"제품 시연 참관","at":"오늘 13:35"}]'::jsonb, '안녕하세요. 관심 제품 견적서를 준비했습니다. 미팅 가능하실까요?', true, now() - interval '1 hour') +ON CONFLICT (id) DO NOTHING; + +-- 6. 옥션(auction) 50 — 진행/마감 혼합. 등록 공사업체만 초대. +INSERT INTO auction (id, event_id, title, category, auction_type, award_criteria, weight_price, weight_reputation, weight_delivery, round, deadline, material_package_id, materials, accent, created_by) VALUES + ('bd-auc-001', 'bd-e-002', 'BD001 구조물 임대 공사', '구조물 임대', 'reverse', 'comprehensive', 60, 25, 15, 1, now() + interval '2 day', NULL, 'layout,boq', 'primary', 'demo-organizer'), + ('bd-auc-002', 'bd-e-003', 'BD002 전기/조명 공사', '전기/조명', 'reverse', 'comprehensive', 60, 25, 15, 1, now() - interval '3 day', NULL, 'layout,boq', 'primary', 'demo-organizer'), + ('bd-auc-003', 'bd-e-004', 'BD003 영상/음향 공사', '영상/음향', 'reverse', 'lowest', 100, 0, 0, 1, now() + interval '4 day', NULL, 'layout,boq', 'primary', 'demo-organizer'), + ('bd-auc-004', 'bd-e-005', 'BD004 네트워크 공사', '네트워크', 'reverse', 'comprehensive', 60, 25, 15, 1, now() - interval '5 day', NULL, 'layout,boq', 'primary', 'demo-organizer'), + ('bd-auc-005', 'bd-e-006', 'BD005 리깅 공사', '리깅', 'reverse', 'comprehensive', 60, 25, 15, 1, now() + interval '6 day', NULL, 'layout,boq', 'primary', 'demo-organizer'), + ('bd-auc-006', 'bd-e-007', 'BD006 전시디자인설치 공사', '전시디자인설치', 'reverse', 'lowest', 100, 0, 0, 1, now() - interval '7 day', NULL, 'layout,boq', 'primary', 'demo-organizer'), + ('bd-auc-007', 'bd-e-008', 'BD007 구조물 임대 공사', '구조물 임대', 'reverse', 'comprehensive', 60, 25, 15, 1, now() + interval '1 day', NULL, 'layout,boq', 'primary', 'demo-organizer'), + ('bd-auc-008', 'bd-e-009', 'BD008 전기/조명 공사', '전기/조명', 'reverse', 'comprehensive', 60, 25, 15, 1, now() - interval '9 day', NULL, 'layout,boq', 'primary', 'demo-organizer'), + ('bd-auc-009', 'bd-e-010', 'BD009 영상/음향 공사', '영상/음향', 'reverse', 'lowest', 100, 0, 0, 1, now() + interval '3 day', NULL, 'layout,boq', 'primary', 'demo-organizer'), + ('bd-auc-010', 'bd-e-011', 'BD010 네트워크 공사', '네트워크', 'reverse', 'comprehensive', 60, 25, 15, 1, now() - interval '1 day', NULL, 'layout,boq', 'primary', 'demo-organizer'), + ('bd-auc-011', 'bd-e-012', 'BD011 리깅 공사', '리깅', 'reverse', 'comprehensive', 60, 25, 15, 1, now() + interval '5 day', NULL, 'layout,boq', 'primary', 'demo-organizer'), + ('bd-auc-012', 'bd-e-013', 'BD012 전시디자인설치 공사', '전시디자인설치', 'reverse', 'lowest', 100, 0, 0, 1, now() - interval '3 day', NULL, 'layout,boq', 'primary', 'demo-organizer'), + ('bd-auc-013', 'bd-e-014', 'BD013 구조물 임대 공사', '구조물 임대', 'reverse', 'comprehensive', 60, 25, 15, 1, now() + interval '7 day', NULL, 'layout,boq', 'primary', 'demo-organizer'), + ('bd-auc-014', 'bd-e-015', 'BD014 전기/조명 공사', '전기/조명', 'reverse', 'comprehensive', 60, 25, 15, 1, now() - interval '5 day', NULL, 'layout,boq', 'primary', 'demo-organizer'), + ('bd-auc-015', 'bd-e-016', 'BD015 영상/음향 공사', '영상/음향', 'reverse', 'lowest', 100, 0, 0, 1, now() + interval '2 day', NULL, 'layout,boq', 'primary', 'demo-organizer'), + ('bd-auc-016', 'bd-e-017', 'BD016 네트워크 공사', '네트워크', 'reverse', 'comprehensive', 60, 25, 15, 1, now() - interval '7 day', NULL, 'layout,boq', 'primary', 'demo-organizer'), + ('bd-auc-017', 'bd-e-018', 'BD017 리깅 공사', '리깅', 'reverse', 'comprehensive', 60, 25, 15, 1, now() + interval '4 day', NULL, 'layout,boq', 'primary', 'demo-organizer'), + ('bd-auc-018', 'bd-e-019', 'BD018 전시디자인설치 공사', '전시디자인설치', 'reverse', 'lowest', 100, 0, 0, 1, now() - interval '9 day', NULL, 'layout,boq', 'primary', 'demo-organizer'), + ('bd-auc-019', 'bd-e-020', 'BD019 구조물 임대 공사', '구조물 임대', 'reverse', 'comprehensive', 60, 25, 15, 1, now() + interval '6 day', NULL, 'layout,boq', 'primary', 'demo-organizer'), + ('bd-auc-020', 'bd-e-021', 'BD020 전기/조명 공사', '전기/조명', 'reverse', 'comprehensive', 60, 25, 15, 1, now() - interval '1 day', NULL, 'layout,boq', 'primary', 'demo-organizer'), + ('bd-auc-021', 'bd-e-022', 'BD021 영상/음향 공사', '영상/음향', 'reverse', 'lowest', 100, 0, 0, 1, now() + interval '1 day', NULL, 'layout,boq', 'primary', 'demo-organizer'), + ('bd-auc-022', 'bd-e-023', 'BD022 네트워크 공사', '네트워크', 'reverse', 'comprehensive', 60, 25, 15, 1, now() - interval '3 day', NULL, 'layout,boq', 'primary', 'demo-organizer'), + ('bd-auc-023', 'bd-e-024', 'BD023 리깅 공사', '리깅', 'reverse', 'comprehensive', 60, 25, 15, 1, now() + interval '3 day', NULL, 'layout,boq', 'primary', 'demo-organizer'), + ('bd-auc-024', 'bd-e-025', 'BD024 전시디자인설치 공사', '전시디자인설치', 'reverse', 'lowest', 100, 0, 0, 1, now() - interval '5 day', NULL, 'layout,boq', 'primary', 'demo-organizer'), + ('bd-auc-025', 'bd-e-026', 'BD025 구조물 임대 공사', '구조물 임대', 'reverse', 'comprehensive', 60, 25, 15, 1, now() + interval '5 day', NULL, 'layout,boq', 'primary', 'demo-organizer'), + ('bd-auc-026', 'bd-e-027', 'BD026 전기/조명 공사', '전기/조명', 'reverse', 'comprehensive', 60, 25, 15, 1, now() - interval '7 day', NULL, 'layout,boq', 'primary', 'demo-organizer'), + ('bd-auc-027', 'bd-e-028', 'BD027 영상/음향 공사', '영상/음향', 'reverse', 'lowest', 100, 0, 0, 1, now() + interval '7 day', NULL, 'layout,boq', 'primary', 'demo-organizer'), + ('bd-auc-028', 'bd-e-029', 'BD028 네트워크 공사', '네트워크', 'reverse', 'comprehensive', 60, 25, 15, 1, now() - interval '9 day', NULL, 'layout,boq', 'primary', 'demo-organizer'), + ('bd-auc-029', 'bd-e-030', 'BD029 리깅 공사', '리깅', 'reverse', 'comprehensive', 60, 25, 15, 1, now() + interval '2 day', NULL, 'layout,boq', 'primary', 'demo-organizer'), + ('bd-auc-030', 'bd-e-031', 'BD030 전시디자인설치 공사', '전시디자인설치', 'reverse', 'lowest', 100, 0, 0, 1, now() - interval '1 day', NULL, 'layout,boq', 'primary', 'demo-organizer'), + ('bd-auc-031', 'bd-e-032', 'BD031 구조물 임대 공사', '구조물 임대', 'reverse', 'comprehensive', 60, 25, 15, 1, now() + interval '4 day', NULL, 'layout,boq', 'primary', 'demo-organizer'), + ('bd-auc-032', 'bd-e-033', 'BD032 전기/조명 공사', '전기/조명', 'reverse', 'comprehensive', 60, 25, 15, 1, now() - interval '3 day', NULL, 'layout,boq', 'primary', 'demo-organizer'), + ('bd-auc-033', 'bd-e-034', 'BD033 영상/음향 공사', '영상/음향', 'reverse', 'lowest', 100, 0, 0, 1, now() + interval '6 day', NULL, 'layout,boq', 'primary', 'demo-organizer'), + ('bd-auc-034', 'bd-e-035', 'BD034 네트워크 공사', '네트워크', 'reverse', 'comprehensive', 60, 25, 15, 1, now() - interval '5 day', NULL, 'layout,boq', 'primary', 'demo-organizer'), + ('bd-auc-035', 'bd-e-036', 'BD035 리깅 공사', '리깅', 'reverse', 'comprehensive', 60, 25, 15, 1, now() + interval '1 day', NULL, 'layout,boq', 'primary', 'demo-organizer'), + ('bd-auc-036', 'bd-e-037', 'BD036 전시디자인설치 공사', '전시디자인설치', 'reverse', 'lowest', 100, 0, 0, 1, now() - interval '7 day', NULL, 'layout,boq', 'primary', 'demo-organizer'), + ('bd-auc-037', 'bd-e-038', 'BD037 구조물 임대 공사', '구조물 임대', 'reverse', 'comprehensive', 60, 25, 15, 1, now() + interval '3 day', NULL, 'layout,boq', 'primary', 'demo-organizer'), + ('bd-auc-038', 'bd-e-039', 'BD038 전기/조명 공사', '전기/조명', 'reverse', 'comprehensive', 60, 25, 15, 1, now() - interval '9 day', NULL, 'layout,boq', 'primary', 'demo-organizer'), + ('bd-auc-039', 'bd-e-040', 'BD039 영상/음향 공사', '영상/음향', 'reverse', 'lowest', 100, 0, 0, 1, now() + interval '5 day', NULL, 'layout,boq', 'primary', 'demo-organizer'), + ('bd-auc-040', 'bd-e-041', 'BD040 네트워크 공사', '네트워크', 'reverse', 'comprehensive', 60, 25, 15, 1, now() - interval '1 day', NULL, 'layout,boq', 'primary', 'demo-organizer'), + ('bd-auc-041', 'bd-e-042', 'BD041 리깅 공사', '리깅', 'reverse', 'comprehensive', 60, 25, 15, 1, now() + interval '7 day', NULL, 'layout,boq', 'primary', 'demo-organizer'), + ('bd-auc-042', 'bd-e-043', 'BD042 전시디자인설치 공사', '전시디자인설치', 'reverse', 'lowest', 100, 0, 0, 1, now() - interval '3 day', NULL, 'layout,boq', 'primary', 'demo-organizer'), + ('bd-auc-043', 'bd-e-044', 'BD043 구조물 임대 공사', '구조물 임대', 'reverse', 'comprehensive', 60, 25, 15, 1, now() + interval '2 day', NULL, 'layout,boq', 'primary', 'demo-organizer'), + ('bd-auc-044', 'bd-e-045', 'BD044 전기/조명 공사', '전기/조명', 'reverse', 'comprehensive', 60, 25, 15, 1, now() - interval '5 day', NULL, 'layout,boq', 'primary', 'demo-organizer'), + ('bd-auc-045', 'bd-e-046', 'BD045 영상/음향 공사', '영상/음향', 'reverse', 'lowest', 100, 0, 0, 1, now() + interval '4 day', NULL, 'layout,boq', 'primary', 'demo-organizer'), + ('bd-auc-046', 'bd-e-047', 'BD046 네트워크 공사', '네트워크', 'reverse', 'comprehensive', 60, 25, 15, 1, now() - interval '7 day', NULL, 'layout,boq', 'primary', 'demo-organizer'), + ('bd-auc-047', 'bd-e-048', 'BD047 리깅 공사', '리깅', 'reverse', 'comprehensive', 60, 25, 15, 1, now() + interval '6 day', NULL, 'layout,boq', 'primary', 'demo-organizer'), + ('bd-auc-048', 'bd-e-049', 'BD048 전시디자인설치 공사', '전시디자인설치', 'reverse', 'lowest', 100, 0, 0, 1, now() - interval '9 day', NULL, 'layout,boq', 'primary', 'demo-organizer'), + ('bd-auc-049', 'bd-e-050', 'BD049 구조물 임대 공사', '구조물 임대', 'reverse', 'comprehensive', 60, 25, 15, 1, now() + interval '1 day', NULL, 'layout,boq', 'primary', 'demo-organizer'), + ('bd-auc-050', 'bd-e-051', 'BD050 전기/조명 공사', '전기/조명', 'reverse', 'comprehensive', 60, 25, 15, 1, now() - interval '1 day', NULL, 'layout,boq', 'primary', 'demo-organizer') +ON CONFLICT (id) DO NOTHING; + +-- 6-1. 옥션 초대(auction_invite) + 응찰(bid) 100 — (auction,company) 유니크. +INSERT INTO auction_invite (id, auction_id, company_id) VALUES + ('bd-ai-0001', 'bd-auc-001', 'bd-co-001'), + ('bd-ai-0002', 'bd-auc-001', 'bd-co-002'), + ('bd-ai-0003', 'bd-auc-002', 'bd-co-004'), + ('bd-ai-0004', 'bd-auc-002', 'bd-co-005'), + ('bd-ai-0005', 'bd-auc-002', 'bd-co-006'), + ('bd-ai-0006', 'bd-auc-003', 'bd-co-007'), + ('bd-ai-0007', 'bd-auc-003', 'bd-co-008'), + ('bd-ai-0008', 'bd-auc-004', 'bd-co-010'), + ('bd-ai-0009', 'bd-auc-004', 'bd-co-011'), + ('bd-ai-0010', 'bd-auc-004', 'bd-co-012'), + ('bd-ai-0011', 'bd-auc-005', 'bd-co-013'), + ('bd-ai-0012', 'bd-auc-005', 'bd-co-014'), + ('bd-ai-0013', 'bd-auc-006', 'bd-co-016'), + ('bd-ai-0014', 'bd-auc-006', 'bd-co-017'), + ('bd-ai-0015', 'bd-auc-006', 'bd-co-018'), + ('bd-ai-0016', 'bd-auc-007', 'bd-co-019'), + ('bd-ai-0017', 'bd-auc-007', 'bd-co-020'), + ('bd-ai-0018', 'bd-auc-008', 'bd-co-022'), + ('bd-ai-0019', 'bd-auc-008', 'bd-co-023'), + ('bd-ai-0020', 'bd-auc-008', 'bd-co-024'), + ('bd-ai-0021', 'bd-auc-009', 'bd-co-025'), + ('bd-ai-0022', 'bd-auc-009', 'bd-co-026'), + ('bd-ai-0023', 'bd-auc-010', 'bd-co-028'), + ('bd-ai-0024', 'bd-auc-010', 'bd-co-029'), + ('bd-ai-0025', 'bd-auc-010', 'bd-co-030'), + ('bd-ai-0026', 'bd-auc-011', 'bd-co-001'), + ('bd-ai-0027', 'bd-auc-011', 'bd-co-002'), + ('bd-ai-0028', 'bd-auc-012', 'bd-co-004'), + ('bd-ai-0029', 'bd-auc-012', 'bd-co-005'), + ('bd-ai-0030', 'bd-auc-012', 'bd-co-006'), + ('bd-ai-0031', 'bd-auc-013', 'bd-co-007'), + ('bd-ai-0032', 'bd-auc-013', 'bd-co-008'), + ('bd-ai-0033', 'bd-auc-014', 'bd-co-010'), + ('bd-ai-0034', 'bd-auc-014', 'bd-co-011'), + ('bd-ai-0035', 'bd-auc-014', 'bd-co-012'), + ('bd-ai-0036', 'bd-auc-015', 'bd-co-013'), + ('bd-ai-0037', 'bd-auc-015', 'bd-co-014'), + ('bd-ai-0038', 'bd-auc-016', 'bd-co-016'), + ('bd-ai-0039', 'bd-auc-016', 'bd-co-017'), + ('bd-ai-0040', 'bd-auc-016', 'bd-co-018'), + ('bd-ai-0041', 'bd-auc-017', 'bd-co-019'), + ('bd-ai-0042', 'bd-auc-017', 'bd-co-020'), + ('bd-ai-0043', 'bd-auc-018', 'bd-co-022'), + ('bd-ai-0044', 'bd-auc-018', 'bd-co-023'), + ('bd-ai-0045', 'bd-auc-018', 'bd-co-024'), + ('bd-ai-0046', 'bd-auc-019', 'bd-co-025'), + ('bd-ai-0047', 'bd-auc-019', 'bd-co-026'), + ('bd-ai-0048', 'bd-auc-020', 'bd-co-028'), + ('bd-ai-0049', 'bd-auc-020', 'bd-co-029'), + ('bd-ai-0050', 'bd-auc-020', 'bd-co-030'), + ('bd-ai-0051', 'bd-auc-021', 'bd-co-001'), + ('bd-ai-0052', 'bd-auc-021', 'bd-co-002'), + ('bd-ai-0053', 'bd-auc-022', 'bd-co-004'), + ('bd-ai-0054', 'bd-auc-022', 'bd-co-005'), + ('bd-ai-0055', 'bd-auc-022', 'bd-co-006'), + ('bd-ai-0056', 'bd-auc-023', 'bd-co-007'), + ('bd-ai-0057', 'bd-auc-023', 'bd-co-008'), + ('bd-ai-0058', 'bd-auc-024', 'bd-co-010'), + ('bd-ai-0059', 'bd-auc-024', 'bd-co-011'), + ('bd-ai-0060', 'bd-auc-024', 'bd-co-012'), + ('bd-ai-0061', 'bd-auc-025', 'bd-co-013'), + ('bd-ai-0062', 'bd-auc-025', 'bd-co-014'), + ('bd-ai-0063', 'bd-auc-026', 'bd-co-016'), + ('bd-ai-0064', 'bd-auc-026', 'bd-co-017'), + ('bd-ai-0065', 'bd-auc-026', 'bd-co-018'), + ('bd-ai-0066', 'bd-auc-027', 'bd-co-019'), + ('bd-ai-0067', 'bd-auc-027', 'bd-co-020'), + ('bd-ai-0068', 'bd-auc-028', 'bd-co-022'), + ('bd-ai-0069', 'bd-auc-028', 'bd-co-023'), + ('bd-ai-0070', 'bd-auc-028', 'bd-co-024'), + ('bd-ai-0071', 'bd-auc-029', 'bd-co-025'), + ('bd-ai-0072', 'bd-auc-029', 'bd-co-026'), + ('bd-ai-0073', 'bd-auc-030', 'bd-co-028'), + ('bd-ai-0074', 'bd-auc-030', 'bd-co-029'), + ('bd-ai-0075', 'bd-auc-030', 'bd-co-030'), + ('bd-ai-0076', 'bd-auc-031', 'bd-co-001'), + ('bd-ai-0077', 'bd-auc-031', 'bd-co-002'), + ('bd-ai-0078', 'bd-auc-032', 'bd-co-004'), + ('bd-ai-0079', 'bd-auc-032', 'bd-co-005'), + ('bd-ai-0080', 'bd-auc-032', 'bd-co-006'), + ('bd-ai-0081', 'bd-auc-033', 'bd-co-007'), + ('bd-ai-0082', 'bd-auc-033', 'bd-co-008'), + ('bd-ai-0083', 'bd-auc-034', 'bd-co-010'), + ('bd-ai-0084', 'bd-auc-034', 'bd-co-011'), + ('bd-ai-0085', 'bd-auc-034', 'bd-co-012'), + ('bd-ai-0086', 'bd-auc-035', 'bd-co-013'), + ('bd-ai-0087', 'bd-auc-035', 'bd-co-014'), + ('bd-ai-0088', 'bd-auc-036', 'bd-co-016'), + ('bd-ai-0089', 'bd-auc-036', 'bd-co-017'), + ('bd-ai-0090', 'bd-auc-036', 'bd-co-018'), + ('bd-ai-0091', 'bd-auc-037', 'bd-co-019'), + ('bd-ai-0092', 'bd-auc-037', 'bd-co-020'), + ('bd-ai-0093', 'bd-auc-038', 'bd-co-022'), + ('bd-ai-0094', 'bd-auc-038', 'bd-co-023'), + ('bd-ai-0095', 'bd-auc-038', 'bd-co-024'), + ('bd-ai-0096', 'bd-auc-039', 'bd-co-025'), + ('bd-ai-0097', 'bd-auc-039', 'bd-co-026'), + ('bd-ai-0098', 'bd-auc-040', 'bd-co-028'), + ('bd-ai-0099', 'bd-auc-040', 'bd-co-029'), + ('bd-ai-0100', 'bd-auc-040', 'bd-co-030'), + ('bd-ai-0101', 'bd-auc-041', 'bd-co-001'), + ('bd-ai-0102', 'bd-auc-041', 'bd-co-002'), + ('bd-ai-0103', 'bd-auc-042', 'bd-co-004'), + ('bd-ai-0104', 'bd-auc-042', 'bd-co-005'), + ('bd-ai-0105', 'bd-auc-042', 'bd-co-006'), + ('bd-ai-0106', 'bd-auc-043', 'bd-co-007'), + ('bd-ai-0107', 'bd-auc-043', 'bd-co-008'), + ('bd-ai-0108', 'bd-auc-044', 'bd-co-010'), + ('bd-ai-0109', 'bd-auc-044', 'bd-co-011'), + ('bd-ai-0110', 'bd-auc-044', 'bd-co-012'), + ('bd-ai-0111', 'bd-auc-045', 'bd-co-013'), + ('bd-ai-0112', 'bd-auc-045', 'bd-co-014'), + ('bd-ai-0113', 'bd-auc-046', 'bd-co-016'), + ('bd-ai-0114', 'bd-auc-046', 'bd-co-017'), + ('bd-ai-0115', 'bd-auc-046', 'bd-co-018'), + ('bd-ai-0116', 'bd-auc-047', 'bd-co-019'), + ('bd-ai-0117', 'bd-auc-047', 'bd-co-020'), + ('bd-ai-0118', 'bd-auc-048', 'bd-co-022'), + ('bd-ai-0119', 'bd-auc-048', 'bd-co-023'), + ('bd-ai-0120', 'bd-auc-048', 'bd-co-024'), + ('bd-ai-0121', 'bd-auc-049', 'bd-co-025'), + ('bd-ai-0122', 'bd-auc-049', 'bd-co-026'), + ('bd-ai-0123', 'bd-auc-050', 'bd-co-028'), + ('bd-ai-0124', 'bd-auc-050', 'bd-co-029'), + ('bd-ai-0125', 'bd-auc-050', 'bd-co-030') +ON CONFLICT (auction_id, company_id) DO NOTHING; + +INSERT INTO bid (id, auction_id, company_id, round, subtotal, vat, total, lead_days) VALUES + ('bd-bid-0001', 'bd-auc-001', 'bd-co-001', 1, 4000000, 400000, 4000000, 7), + ('bd-bid-0002', 'bd-auc-001', 'bd-co-002', 1, 4300000, 430000, 4300000, 9), + ('bd-bid-0003', 'bd-auc-002', 'bd-co-004', 1, 4100000, 410000, 4100000, 7), + ('bd-bid-0004', 'bd-auc-002', 'bd-co-005', 1, 4400000, 440000, 4400000, 9), + ('bd-bid-0005', 'bd-auc-002', 'bd-co-006', 1, 4700000, 470000, 4700000, 11), + ('bd-bid-0006', 'bd-auc-003', 'bd-co-007', 1, 4200000, 420000, 4200000, 7), + ('bd-bid-0007', 'bd-auc-003', 'bd-co-008', 1, 4500000, 450000, 4500000, 9), + ('bd-bid-0008', 'bd-auc-004', 'bd-co-010', 1, 4300000, 430000, 4300000, 7), + ('bd-bid-0009', 'bd-auc-004', 'bd-co-011', 1, 4600000, 460000, 4600000, 9), + ('bd-bid-0010', 'bd-auc-004', 'bd-co-012', 1, 4900000, 490000, 4900000, 11), + ('bd-bid-0011', 'bd-auc-005', 'bd-co-013', 1, 4400000, 440000, 4400000, 7), + ('bd-bid-0012', 'bd-auc-005', 'bd-co-014', 1, 4700000, 470000, 4700000, 9), + ('bd-bid-0013', 'bd-auc-006', 'bd-co-016', 1, 4500000, 450000, 4500000, 7), + ('bd-bid-0014', 'bd-auc-006', 'bd-co-017', 1, 4800000, 480000, 4800000, 9), + ('bd-bid-0015', 'bd-auc-006', 'bd-co-018', 1, 5100000, 510000, 5100000, 11), + ('bd-bid-0016', 'bd-auc-007', 'bd-co-019', 1, 4600000, 460000, 4600000, 7), + ('bd-bid-0017', 'bd-auc-007', 'bd-co-020', 1, 4900000, 490000, 4900000, 9), + ('bd-bid-0018', 'bd-auc-008', 'bd-co-022', 1, 4700000, 470000, 4700000, 7), + ('bd-bid-0019', 'bd-auc-008', 'bd-co-023', 1, 5000000, 500000, 5000000, 9), + ('bd-bid-0020', 'bd-auc-008', 'bd-co-024', 1, 5300000, 530000, 5300000, 11), + ('bd-bid-0021', 'bd-auc-009', 'bd-co-025', 1, 4800000, 480000, 4800000, 7), + ('bd-bid-0022', 'bd-auc-009', 'bd-co-026', 1, 5100000, 510000, 5100000, 9), + ('bd-bid-0023', 'bd-auc-010', 'bd-co-028', 1, 4900000, 490000, 4900000, 7), + ('bd-bid-0024', 'bd-auc-010', 'bd-co-029', 1, 5200000, 520000, 5200000, 9), + ('bd-bid-0025', 'bd-auc-010', 'bd-co-030', 1, 5500000, 550000, 5500000, 11), + ('bd-bid-0026', 'bd-auc-011', 'bd-co-001', 1, 5000000, 500000, 5000000, 7), + ('bd-bid-0027', 'bd-auc-011', 'bd-co-002', 1, 5300000, 530000, 5300000, 9), + ('bd-bid-0028', 'bd-auc-012', 'bd-co-004', 1, 5100000, 510000, 5100000, 7), + ('bd-bid-0029', 'bd-auc-012', 'bd-co-005', 1, 5400000, 540000, 5400000, 9), + ('bd-bid-0030', 'bd-auc-012', 'bd-co-006', 1, 5700000, 570000, 5700000, 11), + ('bd-bid-0031', 'bd-auc-013', 'bd-co-007', 1, 5200000, 520000, 5200000, 7), + ('bd-bid-0032', 'bd-auc-013', 'bd-co-008', 1, 5500000, 550000, 5500000, 9), + ('bd-bid-0033', 'bd-auc-014', 'bd-co-010', 1, 5300000, 530000, 5300000, 7), + ('bd-bid-0034', 'bd-auc-014', 'bd-co-011', 1, 5600000, 560000, 5600000, 9), + ('bd-bid-0035', 'bd-auc-014', 'bd-co-012', 1, 5900000, 590000, 5900000, 11), + ('bd-bid-0036', 'bd-auc-015', 'bd-co-013', 1, 5400000, 540000, 5400000, 7), + ('bd-bid-0037', 'bd-auc-015', 'bd-co-014', 1, 5700000, 570000, 5700000, 9), + ('bd-bid-0038', 'bd-auc-016', 'bd-co-016', 1, 5500000, 550000, 5500000, 7), + ('bd-bid-0039', 'bd-auc-016', 'bd-co-017', 1, 5800000, 580000, 5800000, 9), + ('bd-bid-0040', 'bd-auc-016', 'bd-co-018', 1, 6100000, 610000, 6100000, 11), + ('bd-bid-0041', 'bd-auc-017', 'bd-co-019', 1, 5600000, 560000, 5600000, 7), + ('bd-bid-0042', 'bd-auc-017', 'bd-co-020', 1, 5900000, 590000, 5900000, 9), + ('bd-bid-0043', 'bd-auc-018', 'bd-co-022', 1, 5700000, 570000, 5700000, 7), + ('bd-bid-0044', 'bd-auc-018', 'bd-co-023', 1, 6000000, 600000, 6000000, 9), + ('bd-bid-0045', 'bd-auc-018', 'bd-co-024', 1, 6300000, 630000, 6300000, 11), + ('bd-bid-0046', 'bd-auc-019', 'bd-co-025', 1, 5800000, 580000, 5800000, 7), + ('bd-bid-0047', 'bd-auc-019', 'bd-co-026', 1, 6100000, 610000, 6100000, 9), + ('bd-bid-0048', 'bd-auc-020', 'bd-co-028', 1, 5900000, 590000, 5900000, 7), + ('bd-bid-0049', 'bd-auc-020', 'bd-co-029', 1, 6200000, 620000, 6200000, 9), + ('bd-bid-0050', 'bd-auc-020', 'bd-co-030', 1, 6500000, 650000, 6500000, 11), + ('bd-bid-0051', 'bd-auc-021', 'bd-co-001', 1, 6000000, 600000, 6000000, 7), + ('bd-bid-0052', 'bd-auc-021', 'bd-co-002', 1, 6300000, 630000, 6300000, 9), + ('bd-bid-0053', 'bd-auc-022', 'bd-co-004', 1, 6100000, 610000, 6100000, 7), + ('bd-bid-0054', 'bd-auc-022', 'bd-co-005', 1, 6400000, 640000, 6400000, 9), + ('bd-bid-0055', 'bd-auc-022', 'bd-co-006', 1, 6700000, 670000, 6700000, 11), + ('bd-bid-0056', 'bd-auc-023', 'bd-co-007', 1, 6200000, 620000, 6200000, 7), + ('bd-bid-0057', 'bd-auc-023', 'bd-co-008', 1, 6500000, 650000, 6500000, 9), + ('bd-bid-0058', 'bd-auc-024', 'bd-co-010', 1, 6300000, 630000, 6300000, 7), + ('bd-bid-0059', 'bd-auc-024', 'bd-co-011', 1, 6600000, 660000, 6600000, 9), + ('bd-bid-0060', 'bd-auc-024', 'bd-co-012', 1, 6900000, 690000, 6900000, 11), + ('bd-bid-0061', 'bd-auc-025', 'bd-co-013', 1, 6400000, 640000, 6400000, 7), + ('bd-bid-0062', 'bd-auc-025', 'bd-co-014', 1, 6700000, 670000, 6700000, 9), + ('bd-bid-0063', 'bd-auc-026', 'bd-co-016', 1, 6500000, 650000, 6500000, 7), + ('bd-bid-0064', 'bd-auc-026', 'bd-co-017', 1, 6800000, 680000, 6800000, 9), + ('bd-bid-0065', 'bd-auc-026', 'bd-co-018', 1, 7100000, 710000, 7100000, 11), + ('bd-bid-0066', 'bd-auc-027', 'bd-co-019', 1, 6600000, 660000, 6600000, 7), + ('bd-bid-0067', 'bd-auc-027', 'bd-co-020', 1, 6900000, 690000, 6900000, 9), + ('bd-bid-0068', 'bd-auc-028', 'bd-co-022', 1, 6700000, 670000, 6700000, 7), + ('bd-bid-0069', 'bd-auc-028', 'bd-co-023', 1, 7000000, 700000, 7000000, 9), + ('bd-bid-0070', 'bd-auc-028', 'bd-co-024', 1, 7300000, 730000, 7300000, 11), + ('bd-bid-0071', 'bd-auc-029', 'bd-co-025', 1, 6800000, 680000, 6800000, 7), + ('bd-bid-0072', 'bd-auc-029', 'bd-co-026', 1, 7100000, 710000, 7100000, 9), + ('bd-bid-0073', 'bd-auc-030', 'bd-co-028', 1, 6900000, 690000, 6900000, 7), + ('bd-bid-0074', 'bd-auc-030', 'bd-co-029', 1, 7200000, 720000, 7200000, 9), + ('bd-bid-0075', 'bd-auc-030', 'bd-co-030', 1, 7500000, 750000, 7500000, 11), + ('bd-bid-0076', 'bd-auc-031', 'bd-co-001', 1, 7000000, 700000, 7000000, 7), + ('bd-bid-0077', 'bd-auc-031', 'bd-co-002', 1, 7300000, 730000, 7300000, 9), + ('bd-bid-0078', 'bd-auc-032', 'bd-co-004', 1, 7100000, 710000, 7100000, 7), + ('bd-bid-0079', 'bd-auc-032', 'bd-co-005', 1, 7400000, 740000, 7400000, 9), + ('bd-bid-0080', 'bd-auc-032', 'bd-co-006', 1, 7700000, 770000, 7700000, 11), + ('bd-bid-0081', 'bd-auc-033', 'bd-co-007', 1, 7200000, 720000, 7200000, 7), + ('bd-bid-0082', 'bd-auc-033', 'bd-co-008', 1, 7500000, 750000, 7500000, 9), + ('bd-bid-0083', 'bd-auc-034', 'bd-co-010', 1, 7300000, 730000, 7300000, 7), + ('bd-bid-0084', 'bd-auc-034', 'bd-co-011', 1, 7600000, 760000, 7600000, 9), + ('bd-bid-0085', 'bd-auc-034', 'bd-co-012', 1, 7900000, 790000, 7900000, 11), + ('bd-bid-0086', 'bd-auc-035', 'bd-co-013', 1, 7400000, 740000, 7400000, 7), + ('bd-bid-0087', 'bd-auc-035', 'bd-co-014', 1, 7700000, 770000, 7700000, 9), + ('bd-bid-0088', 'bd-auc-036', 'bd-co-016', 1, 7500000, 750000, 7500000, 7), + ('bd-bid-0089', 'bd-auc-036', 'bd-co-017', 1, 7800000, 780000, 7800000, 9), + ('bd-bid-0090', 'bd-auc-036', 'bd-co-018', 1, 8100000, 810000, 8100000, 11), + ('bd-bid-0091', 'bd-auc-037', 'bd-co-019', 1, 7600000, 760000, 7600000, 7), + ('bd-bid-0092', 'bd-auc-037', 'bd-co-020', 1, 7900000, 790000, 7900000, 9), + ('bd-bid-0093', 'bd-auc-038', 'bd-co-022', 1, 7700000, 770000, 7700000, 7), + ('bd-bid-0094', 'bd-auc-038', 'bd-co-023', 1, 8000000, 800000, 8000000, 9), + ('bd-bid-0095', 'bd-auc-038', 'bd-co-024', 1, 8300000, 830000, 8300000, 11), + ('bd-bid-0096', 'bd-auc-039', 'bd-co-025', 1, 7800000, 780000, 7800000, 7), + ('bd-bid-0097', 'bd-auc-039', 'bd-co-026', 1, 8100000, 810000, 8100000, 9), + ('bd-bid-0098', 'bd-auc-040', 'bd-co-028', 1, 7900000, 790000, 7900000, 7), + ('bd-bid-0099', 'bd-auc-040', 'bd-co-029', 1, 8200000, 820000, 8200000, 9), + ('bd-bid-0100', 'bd-auc-040', 'bd-co-030', 1, 8500000, 850000, 8500000, 11) +ON CONFLICT (auction_id, company_id) DO NOTHING; + +INSERT INTO award (id, auction_id, bid_id, reason, awarded_by) VALUES + ('bd-aw-002', 'bd-auc-002', 'bd-bid-0003', '최저가 + 종합평가 우수(데모 자동 낙찰).', 'system-seed'), + ('bd-aw-004', 'bd-auc-004', 'bd-bid-0008', '최저가 + 종합평가 우수(데모 자동 낙찰).', 'system-seed'), + ('bd-aw-006', 'bd-auc-006', 'bd-bid-0013', '최저가 + 종합평가 우수(데모 자동 낙찰).', 'system-seed'), + ('bd-aw-008', 'bd-auc-008', 'bd-bid-0018', '최저가 + 종합평가 우수(데모 자동 낙찰).', 'system-seed'), + ('bd-aw-010', 'bd-auc-010', 'bd-bid-0023', '최저가 + 종합평가 우수(데모 자동 낙찰).', 'system-seed'), + ('bd-aw-012', 'bd-auc-012', 'bd-bid-0028', '최저가 + 종합평가 우수(데모 자동 낙찰).', 'system-seed'), + ('bd-aw-014', 'bd-auc-014', 'bd-bid-0033', '최저가 + 종합평가 우수(데모 자동 낙찰).', 'system-seed'), + ('bd-aw-016', 'bd-auc-016', 'bd-bid-0038', '최저가 + 종합평가 우수(데모 자동 낙찰).', 'system-seed'), + ('bd-aw-018', 'bd-auc-018', 'bd-bid-0043', '최저가 + 종합평가 우수(데모 자동 낙찰).', 'system-seed'), + ('bd-aw-020', 'bd-auc-020', 'bd-bid-0048', '최저가 + 종합평가 우수(데모 자동 낙찰).', 'system-seed'), + ('bd-aw-022', 'bd-auc-022', 'bd-bid-0053', '최저가 + 종합평가 우수(데모 자동 낙찰).', 'system-seed'), + ('bd-aw-024', 'bd-auc-024', 'bd-bid-0058', '최저가 + 종합평가 우수(데모 자동 낙찰).', 'system-seed'), + ('bd-aw-026', 'bd-auc-026', 'bd-bid-0063', '최저가 + 종합평가 우수(데모 자동 낙찰).', 'system-seed'), + ('bd-aw-028', 'bd-auc-028', 'bd-bid-0068', '최저가 + 종합평가 우수(데모 자동 낙찰).', 'system-seed'), + ('bd-aw-030', 'bd-auc-030', 'bd-bid-0073', '최저가 + 종합평가 우수(데모 자동 낙찰).', 'system-seed'), + ('bd-aw-032', 'bd-auc-032', 'bd-bid-0078', '최저가 + 종합평가 우수(데모 자동 낙찰).', 'system-seed'), + ('bd-aw-034', 'bd-auc-034', 'bd-bid-0083', '최저가 + 종합평가 우수(데모 자동 낙찰).', 'system-seed'), + ('bd-aw-036', 'bd-auc-036', 'bd-bid-0088', '최저가 + 종합평가 우수(데모 자동 낙찰).', 'system-seed'), + ('bd-aw-038', 'bd-auc-038', 'bd-bid-0093', '최저가 + 종합평가 우수(데모 자동 낙찰).', 'system-seed'), + ('bd-aw-040', 'bd-auc-040', 'bd-bid-0098', '최저가 + 종합평가 우수(데모 자동 낙찰).', 'system-seed') +ON CONFLICT (auction_id) DO NOTHING; + +-- 7. 청구서(invoice) 80 — 상태 분포. (event_id, exhibitor_name) 유니크 → 청구당 고유 회사. +INSERT INTO invoice (id, event_id, exhibitor_id, exhibitor_name, invoice_no, category, source_ref, subtotal, vat, total, paid_amount, status, due_date, issued_at, paid_at, cancelled_at, cancel_reason, line_items, note, created_by) VALUES + ('bd-inv-001', 'bd-e-001', 'bd-co-001', '테크테크', 'BD-INV-0001', 'rental', NULL, 5000000, 500000, 5500000, 5500000, 'paid', CURRENT_DATE + -5, now() - interval '3 day', now() - interval '1 day', NULL, NULL, '[{"category":"RENTAL","description":"부스 임대료","quantity":54,"unitPrice":100000,"amount":5400000}]'::jsonb, '대량 데모 청구.', 'system-seed'), + ('bd-inv-002', 'bd-e-002', 'bd-co-002', '로보틱스테크', 'BD-INV-0002', 'rental', NULL, 5400000, 540000, 5940000, 5940000, 'paid', CURRENT_DATE + -4, now() - interval '4 day', now() - interval '2 day', NULL, NULL, '[{"category":"RENTAL","description":"부스 임대료","quantity":54,"unitPrice":100000,"amount":5400000}]'::jsonb, '대량 데모 청구.', 'system-seed'), + ('bd-inv-003', 'bd-e-003', 'bd-co-003', '(주)인더스트리테크', 'BD-INV-0003', 'rental', NULL, 5800000, 580000, 6380000, 6380000, 'paid', CURRENT_DATE + -3, now() - interval '5 day', now() - interval '3 day', NULL, NULL, '[{"category":"RENTAL","description":"부스 임대료","quantity":54,"unitPrice":100000,"amount":5400000}]'::jsonb, '대량 데모 청구.', 'system-seed'), + ('bd-inv-004', 'bd-e-004', 'bd-co-004', '시스템즈테크', 'BD-INV-0004', 'rental', NULL, 6200000, 620000, 6820000, 6820000, 'paid', CURRENT_DATE + -2, now() - interval '6 day', now() - interval '4 day', NULL, NULL, '[{"category":"RENTAL","description":"부스 임대료","quantity":54,"unitPrice":100000,"amount":5400000}]'::jsonb, '대량 데모 청구.', 'system-seed'), + ('bd-inv-005', 'bd-e-005', 'bd-co-005', '(주)솔루션테크', 'BD-INV-0005', 'rental', NULL, 6600000, 660000, 7260000, 7260000, 'paid', CURRENT_DATE + -1, now() - interval '7 day', now() - interval '5 day', NULL, NULL, '[{"category":"RENTAL","description":"부스 임대료","quantity":54,"unitPrice":100000,"amount":5400000}]'::jsonb, '대량 데모 청구.', 'system-seed'), + ('bd-inv-006', 'bd-e-006', 'bd-co-006', '이노베이션테크', 'BD-INV-0006', 'rental', NULL, 7000000, 700000, 7700000, 7700000, 'paid', CURRENT_DATE + 0, now() - interval '8 day', now() - interval '6 day', NULL, NULL, '[{"category":"RENTAL","description":"부스 임대료","quantity":54,"unitPrice":100000,"amount":5400000}]'::jsonb, '대량 데모 청구.', 'system-seed'), + ('bd-inv-007', 'bd-e-007', 'bd-co-007', '오토메이션테크', 'BD-INV-0007', 'rental', NULL, 7400000, 740000, 8140000, 8140000, 'paid', CURRENT_DATE + 1, now() - interval '9 day', now() - interval '1 day', NULL, NULL, '[{"category":"RENTAL","description":"부스 임대료","quantity":54,"unitPrice":100000,"amount":5400000}]'::jsonb, '대량 데모 청구.', 'system-seed'), + ('bd-inv-008', 'bd-e-008', 'bd-co-008', '(주)디자인테크', 'BD-INV-0008', 'rental', NULL, 7800000, 780000, 8580000, 8580000, 'paid', CURRENT_DATE + 2, now() - interval '10 day', now() - interval '2 day', NULL, NULL, '[{"category":"RENTAL","description":"부스 임대료","quantity":54,"unitPrice":100000,"amount":5400000}]'::jsonb, '대량 데모 청구.', 'system-seed'), + ('bd-inv-009', 'bd-e-009', 'bd-co-009', '엔지니어링테크', 'BD-INV-0009', 'rental', NULL, 8200000, 820000, 9020000, 9020000, 'paid', CURRENT_DATE + 3, now() - interval '11 day', now() - interval '3 day', NULL, NULL, '[{"category":"RENTAL","description":"부스 임대료","quantity":54,"unitPrice":100000,"amount":5400000}]'::jsonb, '대량 데모 청구.', 'system-seed'), + ('bd-inv-010', 'bd-e-010', 'bd-co-010', '(주)일렉트릭테크', 'BD-INV-0010', 'rental', NULL, 8600000, 860000, 9460000, 9460000, 'paid', CURRENT_DATE + 4, now() - interval '12 day', now() - interval '4 day', NULL, NULL, '[{"category":"RENTAL","description":"부스 임대료","quantity":54,"unitPrice":100000,"amount":5400000}]'::jsonb, '대량 데모 청구.', 'system-seed'), + ('bd-inv-011', 'bd-e-011', 'bd-co-011', '네트웍스테크', 'BD-INV-0011', 'rental', NULL, 9000000, 900000, 9900000, 9900000, 'paid', CURRENT_DATE + 5, now() - interval '13 day', now() - interval '5 day', NULL, NULL, '[{"category":"RENTAL","description":"부스 임대료","quantity":54,"unitPrice":100000,"amount":5400000}]'::jsonb, '대량 데모 청구.', 'system-seed'), + ('bd-inv-012', 'bd-e-012', 'bd-co-012', '미디어테크', 'BD-INV-0012', 'rental', NULL, 9400000, 940000, 10340000, 10340000, 'paid', CURRENT_DATE + 6, now() - interval '14 day', now() - interval '6 day', NULL, NULL, '[{"category":"RENTAL","description":"부스 임대료","quantity":54,"unitPrice":100000,"amount":5400000}]'::jsonb, '대량 데모 청구.', 'system-seed'), + ('bd-inv-013', 'bd-e-013', 'bd-co-013', '(주)디스플레이테크', 'BD-INV-0013', 'rental', NULL, 9800000, 980000, 10780000, 10780000, 'paid', CURRENT_DATE + 7, now() - interval '15 day', now() - interval '1 day', NULL, NULL, '[{"category":"RENTAL","description":"부스 임대료","quantity":54,"unitPrice":100000,"amount":5400000}]'::jsonb, '대량 데모 청구.', 'system-seed'), + ('bd-inv-014', 'bd-e-014', 'bd-co-014', '컨스트럭션테크', 'BD-INV-0014', 'rental', NULL, 10200000, 1020000, 11220000, 11220000, 'paid', CURRENT_DATE + 8, now() - interval '16 day', now() - interval '2 day', NULL, NULL, '[{"category":"RENTAL","description":"부스 임대료","quantity":54,"unitPrice":100000,"amount":5400000}]'::jsonb, '대량 데모 청구.', 'system-seed'), + ('bd-inv-015', 'bd-e-015', 'bd-co-015', '(주)부스테크', 'BD-INV-0015', 'rental', NULL, 10600000, 1060000, 11660000, 11660000, 'paid', CURRENT_DATE + 9, now() - interval '17 day', now() - interval '3 day', NULL, NULL, '[{"category":"RENTAL","description":"부스 임대료","quantity":54,"unitPrice":100000,"amount":5400000}]'::jsonb, '대량 데모 청구.', 'system-seed'), + ('bd-inv-016', 'bd-e-016', 'bd-co-016', '전기시설테크', 'BD-INV-0016', 'rental', NULL, 11000000, 1100000, 12100000, 12100000, 'paid', CURRENT_DATE + 10, now() - interval '18 day', now() - interval '4 day', NULL, NULL, '[{"category":"RENTAL","description":"부스 임대료","quantity":54,"unitPrice":100000,"amount":5400000}]'::jsonb, '대량 데모 청구.', 'system-seed'), + ('bd-inv-017', 'bd-e-017', 'bd-co-017', '구조테크', 'BD-INV-0017', 'rental', NULL, 11400000, 1140000, 12540000, 12540000, 'paid', CURRENT_DATE + 11, now() - interval '19 day', now() - interval '5 day', NULL, NULL, '[{"category":"RENTAL","description":"부스 임대료","quantity":54,"unitPrice":100000,"amount":5400000}]'::jsonb, '대량 데모 청구.', 'system-seed'), + ('bd-inv-018', 'bd-e-018', 'bd-co-018', '(주)비전테크', 'BD-INV-0018', 'rental', NULL, 11800000, 1180000, 12980000, 12980000, 'paid', CURRENT_DATE + 12, now() - interval '20 day', now() - interval '6 day', NULL, NULL, '[{"category":"RENTAL","description":"부스 임대료","quantity":54,"unitPrice":100000,"amount":5400000}]'::jsonb, '대량 데모 청구.', 'system-seed'), + ('bd-inv-019', 'bd-e-019', 'bd-co-019', '센서텍테크', 'BD-INV-0019', 'rental', NULL, 12200000, 1220000, 13420000, 13420000, 'paid', CURRENT_DATE + 13, now() - interval '21 day', now() - interval '1 day', NULL, NULL, '[{"category":"RENTAL","description":"부스 임대료","quantity":54,"unitPrice":100000,"amount":5400000}]'::jsonb, '대량 데모 청구.', 'system-seed'), + ('bd-inv-020', 'bd-e-020', 'bd-co-020', '(주)머티리얼테크', 'BD-INV-0020', 'rental', NULL, 12600000, 1260000, 13860000, 13860000, 'paid', CURRENT_DATE + 14, now() - interval '22 day', now() - interval '2 day', NULL, NULL, '[{"category":"RENTAL","description":"부스 임대료","quantity":54,"unitPrice":100000,"amount":5400000}]'::jsonb, '대량 데모 청구.', 'system-seed'), + ('bd-inv-021', 'bd-e-021', 'bd-co-021', '로지스테크', 'BD-INV-0021', 'rental', NULL, 13000000, 1300000, 14300000, 14300000, 'paid', CURRENT_DATE + -5, now() - interval '23 day', now() - interval '3 day', NULL, NULL, '[{"category":"RENTAL","description":"부스 임대료","quantity":54,"unitPrice":100000,"amount":5400000}]'::jsonb, '대량 데모 청구.', 'system-seed'), + ('bd-inv-022', 'bd-e-022', 'bd-co-022', '바이오테크', 'BD-INV-0022', 'rental', NULL, 13400000, 1340000, 14740000, 14740000, 'paid', CURRENT_DATE + -4, now() - interval '24 day', now() - interval '4 day', NULL, NULL, '[{"category":"RENTAL","description":"부스 임대료","quantity":54,"unitPrice":100000,"amount":5400000}]'::jsonb, '대량 데모 청구.', 'system-seed'), + ('bd-inv-023', 'bd-e-023', 'bd-co-023', '(주)헬스랩테크', 'BD-INV-0023', 'rental', NULL, 13800000, 1380000, 15180000, 15180000, 'paid', CURRENT_DATE + -3, now() - interval '25 day', now() - interval '5 day', NULL, NULL, '[{"category":"RENTAL","description":"부스 임대료","quantity":54,"unitPrice":100000,"amount":5400000}]'::jsonb, '대량 데모 청구.', 'system-seed'), + ('bd-inv-024', 'bd-e-024', 'bd-co-024', '에너지테크', 'BD-INV-0024', 'rental', NULL, 14200000, 1420000, 15620000, 15620000, 'paid', CURRENT_DATE + -2, now() - interval '26 day', now() - interval '6 day', NULL, NULL, '[{"category":"RENTAL","description":"부스 임대료","quantity":54,"unitPrice":100000,"amount":5400000}]'::jsonb, '대량 데모 청구.', 'system-seed'), + ('bd-inv-025', 'bd-e-025', 'bd-co-025', '(주)스마트테크', 'BD-INV-0025', 'rental', NULL, 14600000, 1460000, 16060000, 6424000, 'invoiced', CURRENT_DATE + -1, now() - interval '27 day', NULL, NULL, NULL, '[{"category":"RENTAL","description":"부스 임대료","quantity":54,"unitPrice":100000,"amount":5400000}]'::jsonb, '대량 데모 청구.', 'system-seed'), + ('bd-inv-026', 'bd-e-026', 'bd-co-026', '글로벌테크', 'BD-INV-0026', 'rental', NULL, 15000000, 1500000, 16500000, 6600000, 'invoiced', CURRENT_DATE + 0, now() - interval '3 day', NULL, NULL, NULL, '[{"category":"RENTAL","description":"부스 임대료","quantity":54,"unitPrice":100000,"amount":5400000}]'::jsonb, '대량 데모 청구.', 'system-seed'), + ('bd-inv-027', 'bd-e-027', 'bd-co-027', '넥스트테크', 'BD-INV-0027', 'rental', NULL, 15400000, 1540000, 16940000, 6776000, 'invoiced', CURRENT_DATE + 1, now() - interval '4 day', NULL, NULL, NULL, '[{"category":"RENTAL","description":"부스 임대료","quantity":54,"unitPrice":100000,"amount":5400000}]'::jsonb, '대량 데모 청구.', 'system-seed'), + ('bd-inv-028', 'bd-e-028', 'bd-co-028', '(주)미래테크', 'BD-INV-0028', 'rental', NULL, 15800000, 1580000, 17380000, 6952000, 'invoiced', CURRENT_DATE + 2, now() - interval '5 day', NULL, NULL, NULL, '[{"category":"RENTAL","description":"부스 임대료","quantity":54,"unitPrice":100000,"amount":5400000}]'::jsonb, '대량 데모 청구.', 'system-seed'), + ('bd-inv-029', 'bd-e-029', 'bd-co-029', '한빛테크', 'BD-INV-0029', 'rental', NULL, 16200000, 1620000, 17820000, 7128000, 'invoiced', CURRENT_DATE + 3, now() - interval '6 day', NULL, NULL, NULL, '[{"category":"RENTAL","description":"부스 임대료","quantity":54,"unitPrice":100000,"amount":5400000}]'::jsonb, '대량 데모 청구.', 'system-seed'), + ('bd-inv-030', 'bd-e-030', 'bd-co-030', '(주)대한테크', 'BD-INV-0030', 'rental', NULL, 16600000, 1660000, 18260000, 7304000, 'invoiced', CURRENT_DATE + 4, now() - interval '7 day', NULL, NULL, NULL, '[{"category":"RENTAL","description":"부스 임대료","quantity":54,"unitPrice":100000,"amount":5400000}]'::jsonb, '대량 데모 청구.', 'system-seed'), + ('bd-inv-031', 'bd-e-031', 'bd-co-031', '테크시스템즈', 'BD-INV-0031', 'rental', NULL, 5000000, 500000, 5500000, 2200000, 'invoiced', CURRENT_DATE + 5, now() - interval '8 day', NULL, NULL, NULL, '[{"category":"RENTAL","description":"부스 임대료","quantity":54,"unitPrice":100000,"amount":5400000}]'::jsonb, '대량 데모 청구.', 'system-seed'), + ('bd-inv-032', 'bd-e-032', 'bd-co-032', '로보틱스시스템즈', 'BD-INV-0032', 'rental', NULL, 5400000, 540000, 5940000, 2376000, 'invoiced', CURRENT_DATE + 6, now() - interval '9 day', NULL, NULL, NULL, '[{"category":"RENTAL","description":"부스 임대료","quantity":54,"unitPrice":100000,"amount":5400000}]'::jsonb, '대량 데모 청구.', 'system-seed'), + ('bd-inv-033', 'bd-e-033', 'bd-co-033', '(주)인더스트리시스템즈', 'BD-INV-0033', 'rental', NULL, 5800000, 580000, 6380000, 2552000, 'invoiced', CURRENT_DATE + 7, now() - interval '10 day', NULL, NULL, NULL, '[{"category":"RENTAL","description":"부스 임대료","quantity":54,"unitPrice":100000,"amount":5400000}]'::jsonb, '대량 데모 청구.', 'system-seed'), + ('bd-inv-034', 'bd-e-034', 'bd-co-034', '시스템즈시스템즈', 'BD-INV-0034', 'rental', NULL, 6200000, 620000, 6820000, 2728000, 'invoiced', CURRENT_DATE + 8, now() - interval '11 day', NULL, NULL, NULL, '[{"category":"RENTAL","description":"부스 임대료","quantity":54,"unitPrice":100000,"amount":5400000}]'::jsonb, '대량 데모 청구.', 'system-seed'), + ('bd-inv-035', 'bd-e-035', 'bd-co-035', '(주)솔루션시스템즈', 'BD-INV-0035', 'rental', NULL, 6600000, 660000, 7260000, 2904000, 'invoiced', CURRENT_DATE + 9, now() - interval '12 day', NULL, NULL, NULL, '[{"category":"RENTAL","description":"부스 임대료","quantity":54,"unitPrice":100000,"amount":5400000}]'::jsonb, '대량 데모 청구.', 'system-seed'), + ('bd-inv-036', 'bd-e-036', 'bd-co-036', '이노베이션시스템즈', 'BD-INV-0036', 'rental', NULL, 7000000, 700000, 7700000, 3080000, 'invoiced', CURRENT_DATE + 10, now() - interval '13 day', NULL, NULL, NULL, '[{"category":"RENTAL","description":"부스 임대료","quantity":54,"unitPrice":100000,"amount":5400000}]'::jsonb, '대량 데모 청구.', 'system-seed'), + ('bd-inv-037', 'bd-e-037', 'bd-co-037', '오토메이션시스템즈', 'BD-INV-0037', 'rental', NULL, 7400000, 740000, 8140000, 3256000, 'invoiced', CURRENT_DATE + 11, now() - interval '14 day', NULL, NULL, NULL, '[{"category":"RENTAL","description":"부스 임대료","quantity":54,"unitPrice":100000,"amount":5400000}]'::jsonb, '대량 데모 청구.', 'system-seed'), + ('bd-inv-038', 'bd-e-038', 'bd-co-038', '(주)디자인시스템즈', 'BD-INV-0038', 'rental', NULL, 7800000, 780000, 8580000, 3432000, 'invoiced', CURRENT_DATE + 12, now() - interval '15 day', NULL, NULL, NULL, '[{"category":"RENTAL","description":"부스 임대료","quantity":54,"unitPrice":100000,"amount":5400000}]'::jsonb, '대량 데모 청구.', 'system-seed'), + ('bd-inv-039', 'bd-e-039', 'bd-co-039', '엔지니어링시스템즈', 'BD-INV-0039', 'rental', NULL, 8200000, 820000, 9020000, 3608000, 'invoiced', CURRENT_DATE + 13, now() - interval '16 day', NULL, NULL, NULL, '[{"category":"RENTAL","description":"부스 임대료","quantity":54,"unitPrice":100000,"amount":5400000}]'::jsonb, '대량 데모 청구.', 'system-seed'), + ('bd-inv-040', 'bd-e-040', 'bd-co-040', '(주)일렉트릭시스템즈', 'BD-INV-0040', 'rental', NULL, 8600000, 860000, 9460000, 3784000, 'invoiced', CURRENT_DATE + 14, now() - interval '17 day', NULL, NULL, NULL, '[{"category":"RENTAL","description":"부스 임대료","quantity":54,"unitPrice":100000,"amount":5400000}]'::jsonb, '대량 데모 청구.', 'system-seed'), + ('bd-inv-041', 'bd-e-001', 'bd-co-041', '네트웍스시스템즈', 'BD-INV-0041', 'rental', NULL, 9000000, 900000, 9900000, 0, 'overdue', CURRENT_DATE + -5, now() - interval '18 day', NULL, NULL, NULL, '[{"category":"RENTAL","description":"부스 임대료","quantity":54,"unitPrice":100000,"amount":5400000}]'::jsonb, '대량 데모 청구.', 'system-seed'), + ('bd-inv-042', 'bd-e-002', 'bd-co-042', '미디어시스템즈', 'BD-INV-0042', 'rental', NULL, 9400000, 940000, 10340000, 0, 'overdue', CURRENT_DATE + -4, now() - interval '19 day', NULL, NULL, NULL, '[{"category":"RENTAL","description":"부스 임대료","quantity":54,"unitPrice":100000,"amount":5400000}]'::jsonb, '대량 데모 청구.', 'system-seed'), + ('bd-inv-043', 'bd-e-003', 'bd-co-043', '(주)디스플레이시스템즈', 'BD-INV-0043', 'rental', NULL, 9800000, 980000, 10780000, 0, 'overdue', CURRENT_DATE + -3, now() - interval '20 day', NULL, NULL, NULL, '[{"category":"RENTAL","description":"부스 임대료","quantity":54,"unitPrice":100000,"amount":5400000}]'::jsonb, '대량 데모 청구.', 'system-seed'), + ('bd-inv-044', 'bd-e-004', 'bd-co-044', '컨스트럭션시스템즈', 'BD-INV-0044', 'rental', NULL, 10200000, 1020000, 11220000, 0, 'overdue', CURRENT_DATE + -2, now() - interval '21 day', NULL, NULL, NULL, '[{"category":"RENTAL","description":"부스 임대료","quantity":54,"unitPrice":100000,"amount":5400000}]'::jsonb, '대량 데모 청구.', 'system-seed'), + ('bd-inv-045', 'bd-e-005', 'bd-co-045', '(주)부스시스템즈', 'BD-INV-0045', 'rental', NULL, 10600000, 1060000, 11660000, 0, 'overdue', CURRENT_DATE + -1, now() - interval '22 day', NULL, NULL, NULL, '[{"category":"RENTAL","description":"부스 임대료","quantity":54,"unitPrice":100000,"amount":5400000}]'::jsonb, '대량 데모 청구.', 'system-seed'), + ('bd-inv-046', 'bd-e-006', 'bd-co-046', '전기시설시스템즈', 'BD-INV-0046', 'rental', NULL, 11000000, 1100000, 12100000, 0, 'overdue', CURRENT_DATE + 0, now() - interval '23 day', NULL, NULL, NULL, '[{"category":"RENTAL","description":"부스 임대료","quantity":54,"unitPrice":100000,"amount":5400000}]'::jsonb, '대량 데모 청구.', 'system-seed'), + ('bd-inv-047', 'bd-e-007', 'bd-co-047', '구조시스템즈', 'BD-INV-0047', 'rental', NULL, 11400000, 1140000, 12540000, 0, 'overdue', CURRENT_DATE + 1, now() - interval '24 day', NULL, NULL, NULL, '[{"category":"RENTAL","description":"부스 임대료","quantity":54,"unitPrice":100000,"amount":5400000}]'::jsonb, '대량 데모 청구.', 'system-seed'), + ('bd-inv-048', 'bd-e-008', 'bd-co-048', '(주)비전시스템즈', 'BD-INV-0048', 'rental', NULL, 11800000, 1180000, 12980000, 0, 'overdue', CURRENT_DATE + 2, now() - interval '25 day', NULL, NULL, NULL, '[{"category":"RENTAL","description":"부스 임대료","quantity":54,"unitPrice":100000,"amount":5400000}]'::jsonb, '대량 데모 청구.', 'system-seed'), + ('bd-inv-049', 'bd-e-009', 'bd-co-049', '센서텍시스템즈', 'BD-INV-0049', 'rental', NULL, 12200000, 1220000, 13420000, 0, 'overdue', CURRENT_DATE + 3, now() - interval '26 day', NULL, NULL, NULL, '[{"category":"RENTAL","description":"부스 임대료","quantity":54,"unitPrice":100000,"amount":5400000}]'::jsonb, '대량 데모 청구.', 'system-seed'), + ('bd-inv-050', 'bd-e-010', 'bd-co-050', '(주)머티리얼시스템즈', 'BD-INV-0050', 'rental', NULL, 12600000, 1260000, 13860000, 0, 'overdue', CURRENT_DATE + 4, now() - interval '27 day', NULL, NULL, NULL, '[{"category":"RENTAL","description":"부스 임대료","quantity":54,"unitPrice":100000,"amount":5400000}]'::jsonb, '대량 데모 청구.', 'system-seed'), + ('bd-inv-051', 'bd-e-011', 'bd-co-051', '로지스시스템즈', 'BD-INV-0051', 'rental', NULL, 13000000, 1300000, 14300000, 0, 'overdue', CURRENT_DATE + 5, now() - interval '3 day', NULL, NULL, NULL, '[{"category":"RENTAL","description":"부스 임대료","quantity":54,"unitPrice":100000,"amount":5400000}]'::jsonb, '대량 데모 청구.', 'system-seed'), + ('bd-inv-052', 'bd-e-012', 'bd-co-052', '바이오시스템즈', 'BD-INV-0052', 'rental', NULL, 13400000, 1340000, 14740000, 0, 'overdue', CURRENT_DATE + 6, now() - interval '4 day', NULL, NULL, NULL, '[{"category":"RENTAL","description":"부스 임대료","quantity":54,"unitPrice":100000,"amount":5400000}]'::jsonb, '대량 데모 청구.', 'system-seed'), + ('bd-inv-053', 'bd-e-013', 'bd-co-053', '(주)헬스랩시스템즈', 'BD-INV-0053', 'rental', NULL, 13800000, 1380000, 15180000, 0, 'pending', CURRENT_DATE + 7, NULL, NULL, NULL, NULL, '[{"category":"RENTAL","description":"부스 임대료","quantity":54,"unitPrice":100000,"amount":5400000}]'::jsonb, '대량 데모 청구.', 'system-seed'), + ('bd-inv-054', 'bd-e-014', 'bd-co-054', '에너지시스템즈', 'BD-INV-0054', 'rental', NULL, 14200000, 1420000, 15620000, 0, 'pending', CURRENT_DATE + 8, NULL, NULL, NULL, NULL, '[{"category":"RENTAL","description":"부스 임대료","quantity":54,"unitPrice":100000,"amount":5400000}]'::jsonb, '대량 데모 청구.', 'system-seed'), + ('bd-inv-055', 'bd-e-015', 'bd-co-055', '(주)스마트시스템즈', 'BD-INV-0055', 'rental', NULL, 14600000, 1460000, 16060000, 0, 'pending', CURRENT_DATE + 9, NULL, NULL, NULL, NULL, '[{"category":"RENTAL","description":"부스 임대료","quantity":54,"unitPrice":100000,"amount":5400000}]'::jsonb, '대량 데모 청구.', 'system-seed'), + ('bd-inv-056', 'bd-e-016', 'bd-co-056', '글로벌시스템즈', 'BD-INV-0056', 'rental', NULL, 15000000, 1500000, 16500000, 0, 'pending', CURRENT_DATE + 10, NULL, NULL, NULL, NULL, '[{"category":"RENTAL","description":"부스 임대료","quantity":54,"unitPrice":100000,"amount":5400000}]'::jsonb, '대량 데모 청구.', 'system-seed'), + ('bd-inv-057', 'bd-e-017', 'bd-co-057', '넥스트시스템즈', 'BD-INV-0057', 'rental', NULL, 15400000, 1540000, 16940000, 0, 'pending', CURRENT_DATE + 11, NULL, NULL, NULL, NULL, '[{"category":"RENTAL","description":"부스 임대료","quantity":54,"unitPrice":100000,"amount":5400000}]'::jsonb, '대량 데모 청구.', 'system-seed'), + ('bd-inv-058', 'bd-e-018', 'bd-co-058', '(주)미래시스템즈', 'BD-INV-0058', 'rental', NULL, 15800000, 1580000, 17380000, 0, 'pending', CURRENT_DATE + 12, NULL, NULL, NULL, NULL, '[{"category":"RENTAL","description":"부스 임대료","quantity":54,"unitPrice":100000,"amount":5400000}]'::jsonb, '대량 데모 청구.', 'system-seed'), + ('bd-inv-059', 'bd-e-019', 'bd-co-059', '한빛시스템즈', 'BD-INV-0059', 'rental', NULL, 16200000, 1620000, 17820000, 0, 'pending', CURRENT_DATE + 13, NULL, NULL, NULL, NULL, '[{"category":"RENTAL","description":"부스 임대료","quantity":54,"unitPrice":100000,"amount":5400000}]'::jsonb, '대량 데모 청구.', 'system-seed'), + ('bd-inv-060', 'bd-e-020', 'bd-co-060', '(주)대한시스템즈', 'BD-INV-0060', 'rental', NULL, 16600000, 1660000, 18260000, 0, 'pending', CURRENT_DATE + 14, NULL, NULL, NULL, NULL, '[{"category":"RENTAL","description":"부스 임대료","quantity":54,"unitPrice":100000,"amount":5400000}]'::jsonb, '대량 데모 청구.', 'system-seed'), + ('bd-inv-061', 'bd-e-021', 'bd-co-061', '테크인더스트리', 'BD-INV-0061', 'rental', NULL, 5000000, 500000, 5500000, 0, 'pending', CURRENT_DATE + -5, NULL, NULL, NULL, NULL, '[{"category":"RENTAL","description":"부스 임대료","quantity":54,"unitPrice":100000,"amount":5400000}]'::jsonb, '대량 데모 청구.', 'system-seed'), + ('bd-inv-062', 'bd-e-022', 'bd-co-062', '로보틱스인더스트리', 'BD-INV-0062', 'rental', NULL, 5400000, 540000, 5940000, 0, 'pending', CURRENT_DATE + -4, NULL, NULL, NULL, NULL, '[{"category":"RENTAL","description":"부스 임대료","quantity":54,"unitPrice":100000,"amount":5400000}]'::jsonb, '대량 데모 청구.', 'system-seed'), + ('bd-inv-063', 'bd-e-023', 'bd-co-063', '(주)인더스트리인더스트리', 'BD-INV-0063', 'rental', NULL, 5800000, 580000, 6380000, 0, 'pending', CURRENT_DATE + -3, NULL, NULL, NULL, NULL, '[{"category":"RENTAL","description":"부스 임대료","quantity":54,"unitPrice":100000,"amount":5400000}]'::jsonb, '대량 데모 청구.', 'system-seed'), + ('bd-inv-064', 'bd-e-024', 'bd-co-064', '시스템즈인더스트리', 'BD-INV-0064', 'rental', NULL, 6200000, 620000, 6820000, 0, 'pending', CURRENT_DATE + -2, NULL, NULL, NULL, NULL, '[{"category":"RENTAL","description":"부스 임대료","quantity":54,"unitPrice":100000,"amount":5400000}]'::jsonb, '대량 데모 청구.', 'system-seed'), + ('bd-inv-065', 'bd-e-025', 'bd-co-065', '(주)솔루션인더스트리', 'BD-INV-0065', 'rental', NULL, 6600000, 660000, 7260000, 2904000, 'cancelled', CURRENT_DATE + -1, now() - interval '17 day', NULL, now() - interval '5 day', '참가 철회(내부 사정) — 환불 규정 적용', '[{"category":"RENTAL","description":"부스 임대료","quantity":54,"unitPrice":100000,"amount":5400000}]'::jsonb, '대량 데모 청구.', 'system-seed'), + ('bd-inv-066', 'bd-e-026', 'bd-co-066', '이노베이션인더스트리', 'BD-INV-0066', 'rental', NULL, 7000000, 700000, 7700000, 3080000, 'cancelled', CURRENT_DATE + 0, now() - interval '18 day', NULL, now() - interval '1 day', '참가 철회(내부 사정) — 환불 규정 적용', '[{"category":"RENTAL","description":"부스 임대료","quantity":54,"unitPrice":100000,"amount":5400000}]'::jsonb, '대량 데모 청구.', 'system-seed'), + ('bd-inv-067', 'bd-e-027', 'bd-co-067', '오토메이션인더스트리', 'BD-INV-0067', 'rental', NULL, 7400000, 740000, 8140000, 3256000, 'cancelled', CURRENT_DATE + 1, now() - interval '19 day', NULL, now() - interval '2 day', '참가 철회(내부 사정) — 환불 규정 적용', '[{"category":"RENTAL","description":"부스 임대료","quantity":54,"unitPrice":100000,"amount":5400000}]'::jsonb, '대량 데모 청구.', 'system-seed'), + ('bd-inv-068', 'bd-e-028', 'bd-co-068', '(주)디자인인더스트리', 'BD-INV-0068', 'rental', NULL, 7800000, 780000, 8580000, 3432000, 'cancelled', CURRENT_DATE + 2, now() - interval '20 day', NULL, now() - interval '3 day', '참가 철회(내부 사정) — 환불 규정 적용', '[{"category":"RENTAL","description":"부스 임대료","quantity":54,"unitPrice":100000,"amount":5400000}]'::jsonb, '대량 데모 청구.', 'system-seed'), + ('bd-inv-069', 'bd-e-029', 'bd-co-069', '엔지니어링인더스트리', 'BD-INV-0069', 'rental', NULL, 8200000, 820000, 9020000, 3608000, 'cancelled', CURRENT_DATE + 3, now() - interval '21 day', NULL, now() - interval '4 day', '참가 철회(내부 사정) — 환불 규정 적용', '[{"category":"RENTAL","description":"부스 임대료","quantity":54,"unitPrice":100000,"amount":5400000}]'::jsonb, '대량 데모 청구.', 'system-seed'), + ('bd-inv-070', 'bd-e-030', 'bd-co-070', '(주)일렉트릭인더스트리', 'BD-INV-0070', 'rental', NULL, 8600000, 860000, 9460000, 3784000, 'cancelled', CURRENT_DATE + 4, now() - interval '22 day', NULL, now() - interval '5 day', '참가 철회(내부 사정) — 환불 규정 적용', '[{"category":"RENTAL","description":"부스 임대료","quantity":54,"unitPrice":100000,"amount":5400000}]'::jsonb, '대량 데모 청구.', 'system-seed'), + ('bd-inv-071', 'bd-e-031', 'bd-co-071', '네트웍스인더스트리', 'BD-INV-0071', 'rental', NULL, 9000000, 900000, 9900000, 3960000, 'cancelled', CURRENT_DATE + 5, now() - interval '23 day', NULL, now() - interval '1 day', '참가 철회(내부 사정) — 환불 규정 적용', '[{"category":"RENTAL","description":"부스 임대료","quantity":54,"unitPrice":100000,"amount":5400000}]'::jsonb, '대량 데모 청구.', 'system-seed'), + ('bd-inv-072', 'bd-e-032', 'bd-co-072', '미디어인더스트리', 'BD-INV-0072', 'rental', NULL, 9400000, 940000, 10340000, 4136000, 'cancelled', CURRENT_DATE + 6, now() - interval '24 day', NULL, now() - interval '2 day', '참가 철회(내부 사정) — 환불 규정 적용', '[{"category":"RENTAL","description":"부스 임대료","quantity":54,"unitPrice":100000,"amount":5400000}]'::jsonb, '대량 데모 청구.', 'system-seed'), + ('bd-inv-073', 'bd-e-033', 'bd-co-073', '(주)디스플레이인더스트리', 'BD-INV-0073', 'rental', NULL, 9800000, 980000, 10780000, 4312000, 'cancelled', CURRENT_DATE + 7, now() - interval '25 day', NULL, now() - interval '3 day', '참가 철회(내부 사정) — 환불 규정 적용', '[{"category":"RENTAL","description":"부스 임대료","quantity":54,"unitPrice":100000,"amount":5400000}]'::jsonb, '대량 데모 청구.', 'system-seed'), + ('bd-inv-074', 'bd-e-034', 'bd-co-074', '컨스트럭션인더스트리', 'BD-INV-0074', 'rental', NULL, 10200000, 1020000, 11220000, 4488000, 'cancelled', CURRENT_DATE + 8, now() - interval '26 day', NULL, now() - interval '4 day', '참가 철회(내부 사정) — 환불 규정 적용', '[{"category":"RENTAL","description":"부스 임대료","quantity":54,"unitPrice":100000,"amount":5400000}]'::jsonb, '대량 데모 청구.', 'system-seed'), + ('bd-inv-075', 'bd-e-035', 'bd-co-075', '(주)부스인더스트리', 'BD-INV-0075', 'rental', NULL, 10600000, 1060000, 11660000, 4664000, 'cancelled', CURRENT_DATE + 9, now() - interval '27 day', NULL, now() - interval '5 day', '참가 철회(내부 사정) — 환불 규정 적용', '[{"category":"RENTAL","description":"부스 임대료","quantity":54,"unitPrice":100000,"amount":5400000}]'::jsonb, '대량 데모 청구.', 'system-seed'), + ('bd-inv-076', 'bd-e-036', 'bd-co-076', '전기시설인더스트리', 'BD-INV-0076', 'rental', NULL, 11000000, 1100000, 12100000, 4840000, 'cancelled', CURRENT_DATE + 10, now() - interval '3 day', NULL, now() - interval '1 day', '참가 철회(내부 사정) — 환불 규정 적용', '[{"category":"RENTAL","description":"부스 임대료","quantity":54,"unitPrice":100000,"amount":5400000}]'::jsonb, '대량 데모 청구.', 'system-seed'), + ('bd-inv-077', 'bd-e-037', 'bd-co-077', '구조인더스트리', 'BD-INV-0077', 'rental', NULL, 11400000, 1140000, 12540000, 5016000, 'cancelled', CURRENT_DATE + 11, now() - interval '4 day', NULL, now() - interval '2 day', '참가 철회(내부 사정) — 환불 규정 적용', '[{"category":"RENTAL","description":"부스 임대료","quantity":54,"unitPrice":100000,"amount":5400000}]'::jsonb, '대량 데모 청구.', 'system-seed'), + ('bd-inv-078', 'bd-e-038', 'bd-co-078', '(주)비전인더스트리', 'BD-INV-0078', 'rental', NULL, 11800000, 1180000, 12980000, 5192000, 'cancelled', CURRENT_DATE + 12, now() - interval '5 day', NULL, now() - interval '3 day', '참가 철회(내부 사정) — 환불 규정 적용', '[{"category":"RENTAL","description":"부스 임대료","quantity":54,"unitPrice":100000,"amount":5400000}]'::jsonb, '대량 데모 청구.', 'system-seed'), + ('bd-inv-079', 'bd-e-039', 'bd-co-079', '센서텍인더스트리', 'BD-INV-0079', 'rental', NULL, 12200000, 1220000, 13420000, 5368000, 'cancelled', CURRENT_DATE + 13, now() - interval '6 day', NULL, now() - interval '4 day', '참가 철회(내부 사정) — 환불 규정 적용', '[{"category":"RENTAL","description":"부스 임대료","quantity":54,"unitPrice":100000,"amount":5400000}]'::jsonb, '대량 데모 청구.', 'system-seed'), + ('bd-inv-080', 'bd-e-040', 'bd-co-080', '(주)머티리얼인더스트리', 'BD-INV-0080', 'rental', NULL, 12600000, 1260000, 13860000, 5544000, 'cancelled', CURRENT_DATE + 14, now() - interval '7 day', NULL, now() - interval '5 day', '참가 철회(내부 사정) — 환불 규정 적용', '[{"category":"RENTAL","description":"부스 임대료","quantity":54,"unitPrice":100000,"amount":5400000}]'::jsonb, '대량 데모 청구.', 'system-seed') +ON CONFLICT (event_id, exhibitor_name) DO NOTHING; + +INSERT INTO invoice_payment (id, invoice_id, amount, method, memo, recorded_by, recorded_at) VALUES + ('bd-pay-0001', 'bd-inv-001', 5500000, 'transfer', '완납', 'system-seed', now() - interval '1 day'), + ('bd-pay-0002', 'bd-inv-002', 5940000, 'transfer', '완납', 'system-seed', now() - interval '2 day'), + ('bd-pay-0003', 'bd-inv-003', 6380000, 'transfer', '완납', 'system-seed', now() - interval '3 day'), + ('bd-pay-0004', 'bd-inv-004', 6820000, 'transfer', '완납', 'system-seed', now() - interval '4 day'), + ('bd-pay-0005', 'bd-inv-005', 7260000, 'transfer', '완납', 'system-seed', now() - interval '5 day'), + ('bd-pay-0006', 'bd-inv-006', 7700000, 'transfer', '완납', 'system-seed', now() - interval '6 day'), + ('bd-pay-0007', 'bd-inv-007', 8140000, 'transfer', '완납', 'system-seed', now() - interval '7 day'), + ('bd-pay-0008', 'bd-inv-008', 8580000, 'transfer', '완납', 'system-seed', now() - interval '8 day'), + ('bd-pay-0009', 'bd-inv-009', 9020000, 'transfer', '완납', 'system-seed', now() - interval '9 day'), + ('bd-pay-0010', 'bd-inv-010', 9460000, 'transfer', '완납', 'system-seed', now() - interval '10 day'), + ('bd-pay-0011', 'bd-inv-011', 9900000, 'transfer', '완납', 'system-seed', now() - interval '1 day'), + ('bd-pay-0012', 'bd-inv-012', 10340000, 'transfer', '완납', 'system-seed', now() - interval '2 day'), + ('bd-pay-0013', 'bd-inv-013', 10780000, 'transfer', '완납', 'system-seed', now() - interval '3 day'), + ('bd-pay-0014', 'bd-inv-014', 11220000, 'transfer', '완납', 'system-seed', now() - interval '4 day'), + ('bd-pay-0015', 'bd-inv-015', 11660000, 'transfer', '완납', 'system-seed', now() - interval '5 day'), + ('bd-pay-0016', 'bd-inv-016', 12100000, 'transfer', '완납', 'system-seed', now() - interval '6 day'), + ('bd-pay-0017', 'bd-inv-017', 12540000, 'transfer', '완납', 'system-seed', now() - interval '7 day'), + ('bd-pay-0018', 'bd-inv-018', 12980000, 'transfer', '완납', 'system-seed', now() - interval '8 day'), + ('bd-pay-0019', 'bd-inv-019', 13420000, 'transfer', '완납', 'system-seed', now() - interval '9 day'), + ('bd-pay-0020', 'bd-inv-020', 13860000, 'transfer', '완납', 'system-seed', now() - interval '10 day'), + ('bd-pay-0021', 'bd-inv-021', 14300000, 'transfer', '완납', 'system-seed', now() - interval '1 day'), + ('bd-pay-0022', 'bd-inv-022', 14740000, 'transfer', '완납', 'system-seed', now() - interval '2 day'), + ('bd-pay-0023', 'bd-inv-023', 15180000, 'transfer', '완납', 'system-seed', now() - interval '3 day'), + ('bd-pay-0024', 'bd-inv-024', 15620000, 'transfer', '완납', 'system-seed', now() - interval '4 day'), + ('bd-pay-0025', 'bd-inv-025', 6424000, 'transfer', '계약금', 'system-seed', now() - interval '5 day'), + ('bd-pay-0026', 'bd-inv-026', 6600000, 'transfer', '계약금', 'system-seed', now() - interval '6 day'), + ('bd-pay-0027', 'bd-inv-027', 6776000, 'transfer', '계약금', 'system-seed', now() - interval '7 day'), + ('bd-pay-0028', 'bd-inv-028', 6952000, 'transfer', '계약금', 'system-seed', now() - interval '8 day'), + ('bd-pay-0029', 'bd-inv-029', 7128000, 'transfer', '계약금', 'system-seed', now() - interval '9 day'), + ('bd-pay-0030', 'bd-inv-030', 7304000, 'transfer', '계약금', 'system-seed', now() - interval '10 day'), + ('bd-pay-0031', 'bd-inv-031', 2200000, 'transfer', '계약금', 'system-seed', now() - interval '1 day'), + ('bd-pay-0032', 'bd-inv-032', 2376000, 'transfer', '계약금', 'system-seed', now() - interval '2 day'), + ('bd-pay-0033', 'bd-inv-033', 2552000, 'transfer', '계약금', 'system-seed', now() - interval '3 day'), + ('bd-pay-0034', 'bd-inv-034', 2728000, 'transfer', '계약금', 'system-seed', now() - interval '4 day'), + ('bd-pay-0035', 'bd-inv-035', 2904000, 'transfer', '계약금', 'system-seed', now() - interval '5 day'), + ('bd-pay-0036', 'bd-inv-036', 3080000, 'transfer', '계약금', 'system-seed', now() - interval '6 day'), + ('bd-pay-0037', 'bd-inv-037', 3256000, 'transfer', '계약금', 'system-seed', now() - interval '7 day'), + ('bd-pay-0038', 'bd-inv-038', 3432000, 'transfer', '계약금', 'system-seed', now() - interval '8 day'), + ('bd-pay-0039', 'bd-inv-039', 3608000, 'transfer', '계약금', 'system-seed', now() - interval '9 day'), + ('bd-pay-0040', 'bd-inv-040', 3784000, 'transfer', '계약금', 'system-seed', now() - interval '10 day'), + ('bd-pay-0041', 'bd-inv-065', 2904000, 'transfer', '계약금', 'system-seed', now() - interval '5 day'), + ('bd-pay-0042', 'bd-inv-066', 3080000, 'transfer', '계약금', 'system-seed', now() - interval '6 day'), + ('bd-pay-0043', 'bd-inv-067', 3256000, 'transfer', '계약금', 'system-seed', now() - interval '7 day'), + ('bd-pay-0044', 'bd-inv-068', 3432000, 'transfer', '계약금', 'system-seed', now() - interval '8 day'), + ('bd-pay-0045', 'bd-inv-069', 3608000, 'transfer', '계약금', 'system-seed', now() - interval '9 day'), + ('bd-pay-0046', 'bd-inv-070', 3784000, 'transfer', '계약금', 'system-seed', now() - interval '10 day'), + ('bd-pay-0047', 'bd-inv-071', 3960000, 'transfer', '계약금', 'system-seed', now() - interval '1 day'), + ('bd-pay-0048', 'bd-inv-072', 4136000, 'transfer', '계약금', 'system-seed', now() - interval '2 day'), + ('bd-pay-0049', 'bd-inv-073', 4312000, 'transfer', '계약금', 'system-seed', now() - interval '3 day'), + ('bd-pay-0050', 'bd-inv-074', 4488000, 'transfer', '계약금', 'system-seed', now() - interval '4 day'), + ('bd-pay-0051', 'bd-inv-075', 4664000, 'transfer', '계약금', 'system-seed', now() - interval '5 day'), + ('bd-pay-0052', 'bd-inv-076', 4840000, 'transfer', '계약금', 'system-seed', now() - interval '6 day'), + ('bd-pay-0053', 'bd-inv-077', 5016000, 'transfer', '계약금', 'system-seed', now() - interval '7 day'), + ('bd-pay-0054', 'bd-inv-078', 5192000, 'transfer', '계약금', 'system-seed', now() - interval '8 day'), + ('bd-pay-0055', 'bd-inv-079', 5368000, 'transfer', '계약금', 'system-seed', now() - interval '9 day'), + ('bd-pay-0056', 'bd-inv-080', 5544000, 'transfer', '계약금', 'system-seed', now() - interval '10 day') +ON CONFLICT (id) DO NOTHING; + +INSERT INTO tax_invoice (tenant_id, id, invoice_id, event_id, tax_invoice_no, supplier_name, supplier_biz_no, supplier_ceo, supplier_addr, buyer_name, supply_amount, vat, total, item_summary, status, issued_by) VALUES + ('KINTEX', 'bd-tax-0001', 'bd-inv-001', 'bd-e-001', 'BD-TAX-0001', '킨텍스(KINTEX)', '128-81-00000', '대표이사', '경기도 고양시 일산서구 킨텍스로 217', '테크테크', 5000000, 500000, 5500000, '부스 임대료 외', 'issued', 'system-seed'), + ('KINTEX', 'bd-tax-0002', 'bd-inv-002', 'bd-e-002', 'BD-TAX-0002', '킨텍스(KINTEX)', '128-81-00000', '대표이사', '경기도 고양시 일산서구 킨텍스로 217', '로보틱스테크', 5400000, 540000, 5940000, '부스 임대료 외', 'issued', 'system-seed'), + ('KINTEX', 'bd-tax-0003', 'bd-inv-003', 'bd-e-003', 'BD-TAX-0003', '킨텍스(KINTEX)', '128-81-00000', '대표이사', '경기도 고양시 일산서구 킨텍스로 217', '(주)인더스트리테크', 5800000, 580000, 6380000, '부스 임대료 외', 'issued', 'system-seed'), + ('KINTEX', 'bd-tax-0004', 'bd-inv-004', 'bd-e-004', 'BD-TAX-0004', '킨텍스(KINTEX)', '128-81-00000', '대표이사', '경기도 고양시 일산서구 킨텍스로 217', '시스템즈테크', 6200000, 620000, 6820000, '부스 임대료 외', 'issued', 'system-seed'), + ('KINTEX', 'bd-tax-0005', 'bd-inv-005', 'bd-e-005', 'BD-TAX-0005', '킨텍스(KINTEX)', '128-81-00000', '대표이사', '경기도 고양시 일산서구 킨텍스로 217', '(주)솔루션테크', 6600000, 660000, 7260000, '부스 임대료 외', 'issued', 'system-seed'), + ('KINTEX', 'bd-tax-0006', 'bd-inv-006', 'bd-e-006', 'BD-TAX-0006', '킨텍스(KINTEX)', '128-81-00000', '대표이사', '경기도 고양시 일산서구 킨텍스로 217', '이노베이션테크', 7000000, 700000, 7700000, '부스 임대료 외', 'issued', 'system-seed'), + ('KINTEX', 'bd-tax-0007', 'bd-inv-007', 'bd-e-007', 'BD-TAX-0007', '킨텍스(KINTEX)', '128-81-00000', '대표이사', '경기도 고양시 일산서구 킨텍스로 217', '오토메이션테크', 7400000, 740000, 8140000, '부스 임대료 외', 'issued', 'system-seed'), + ('KINTEX', 'bd-tax-0008', 'bd-inv-008', 'bd-e-008', 'BD-TAX-0008', '킨텍스(KINTEX)', '128-81-00000', '대표이사', '경기도 고양시 일산서구 킨텍스로 217', '(주)디자인테크', 7800000, 780000, 8580000, '부스 임대료 외', 'issued', 'system-seed'), + ('KINTEX', 'bd-tax-0009', 'bd-inv-009', 'bd-e-009', 'BD-TAX-0009', '킨텍스(KINTEX)', '128-81-00000', '대표이사', '경기도 고양시 일산서구 킨텍스로 217', '엔지니어링테크', 8200000, 820000, 9020000, '부스 임대료 외', 'issued', 'system-seed'), + ('KINTEX', 'bd-tax-0010', 'bd-inv-010', 'bd-e-010', 'BD-TAX-0010', '킨텍스(KINTEX)', '128-81-00000', '대표이사', '경기도 고양시 일산서구 킨텍스로 217', '(주)일렉트릭테크', 8600000, 860000, 9460000, '부스 임대료 외', 'issued', 'system-seed'), + ('KINTEX', 'bd-tax-0011', 'bd-inv-011', 'bd-e-011', 'BD-TAX-0011', '킨텍스(KINTEX)', '128-81-00000', '대표이사', '경기도 고양시 일산서구 킨텍스로 217', '네트웍스테크', 9000000, 900000, 9900000, '부스 임대료 외', 'issued', 'system-seed'), + ('KINTEX', 'bd-tax-0012', 'bd-inv-012', 'bd-e-012', 'BD-TAX-0012', '킨텍스(KINTEX)', '128-81-00000', '대표이사', '경기도 고양시 일산서구 킨텍스로 217', '미디어테크', 9400000, 940000, 10340000, '부스 임대료 외', 'issued', 'system-seed'), + ('KINTEX', 'bd-tax-0013', 'bd-inv-013', 'bd-e-013', 'BD-TAX-0013', '킨텍스(KINTEX)', '128-81-00000', '대표이사', '경기도 고양시 일산서구 킨텍스로 217', '(주)디스플레이테크', 9800000, 980000, 10780000, '부스 임대료 외', 'issued', 'system-seed'), + ('KINTEX', 'bd-tax-0014', 'bd-inv-014', 'bd-e-014', 'BD-TAX-0014', '킨텍스(KINTEX)', '128-81-00000', '대표이사', '경기도 고양시 일산서구 킨텍스로 217', '컨스트럭션테크', 10200000, 1020000, 11220000, '부스 임대료 외', 'issued', 'system-seed'), + ('KINTEX', 'bd-tax-0015', 'bd-inv-015', 'bd-e-015', 'BD-TAX-0015', '킨텍스(KINTEX)', '128-81-00000', '대표이사', '경기도 고양시 일산서구 킨텍스로 217', '(주)부스테크', 10600000, 1060000, 11660000, '부스 임대료 외', 'issued', 'system-seed'), + ('KINTEX', 'bd-tax-0016', 'bd-inv-016', 'bd-e-016', 'BD-TAX-0016', '킨텍스(KINTEX)', '128-81-00000', '대표이사', '경기도 고양시 일산서구 킨텍스로 217', '전기시설테크', 11000000, 1100000, 12100000, '부스 임대료 외', 'issued', 'system-seed'), + ('KINTEX', 'bd-tax-0017', 'bd-inv-017', 'bd-e-017', 'BD-TAX-0017', '킨텍스(KINTEX)', '128-81-00000', '대표이사', '경기도 고양시 일산서구 킨텍스로 217', '구조테크', 11400000, 1140000, 12540000, '부스 임대료 외', 'issued', 'system-seed'), + ('KINTEX', 'bd-tax-0018', 'bd-inv-018', 'bd-e-018', 'BD-TAX-0018', '킨텍스(KINTEX)', '128-81-00000', '대표이사', '경기도 고양시 일산서구 킨텍스로 217', '(주)비전테크', 11800000, 1180000, 12980000, '부스 임대료 외', 'issued', 'system-seed'), + ('KINTEX', 'bd-tax-0019', 'bd-inv-019', 'bd-e-019', 'BD-TAX-0019', '킨텍스(KINTEX)', '128-81-00000', '대표이사', '경기도 고양시 일산서구 킨텍스로 217', '센서텍테크', 12200000, 1220000, 13420000, '부스 임대료 외', 'issued', 'system-seed'), + ('KINTEX', 'bd-tax-0020', 'bd-inv-020', 'bd-e-020', 'BD-TAX-0020', '킨텍스(KINTEX)', '128-81-00000', '대표이사', '경기도 고양시 일산서구 킨텍스로 217', '(주)머티리얼테크', 12600000, 1260000, 13860000, '부스 임대료 외', 'issued', 'system-seed'), + ('KINTEX', 'bd-tax-0021', 'bd-inv-021', 'bd-e-021', 'BD-TAX-0021', '킨텍스(KINTEX)', '128-81-00000', '대표이사', '경기도 고양시 일산서구 킨텍스로 217', '로지스테크', 13000000, 1300000, 14300000, '부스 임대료 외', 'issued', 'system-seed'), + ('KINTEX', 'bd-tax-0022', 'bd-inv-022', 'bd-e-022', 'BD-TAX-0022', '킨텍스(KINTEX)', '128-81-00000', '대표이사', '경기도 고양시 일산서구 킨텍스로 217', '바이오테크', 13400000, 1340000, 14740000, '부스 임대료 외', 'issued', 'system-seed'), + ('KINTEX', 'bd-tax-0023', 'bd-inv-023', 'bd-e-023', 'BD-TAX-0023', '킨텍스(KINTEX)', '128-81-00000', '대표이사', '경기도 고양시 일산서구 킨텍스로 217', '(주)헬스랩테크', 13800000, 1380000, 15180000, '부스 임대료 외', 'issued', 'system-seed'), + ('KINTEX', 'bd-tax-0024', 'bd-inv-024', 'bd-e-024', 'BD-TAX-0024', '킨텍스(KINTEX)', '128-81-00000', '대표이사', '경기도 고양시 일산서구 킨텍스로 217', '에너지테크', 14200000, 1420000, 15620000, '부스 임대료 외', 'issued', 'system-seed'), + ('KINTEX', 'bd-tax-0025', 'bd-inv-025', 'bd-e-025', 'BD-TAX-0025', '킨텍스(KINTEX)', '128-81-00000', '대표이사', '경기도 고양시 일산서구 킨텍스로 217', '(주)스마트테크', 14600000, 1460000, 16060000, '부스 임대료 외', 'issued', 'system-seed'), + ('KINTEX', 'bd-tax-0026', 'bd-inv-026', 'bd-e-026', 'BD-TAX-0026', '킨텍스(KINTEX)', '128-81-00000', '대표이사', '경기도 고양시 일산서구 킨텍스로 217', '글로벌테크', 15000000, 1500000, 16500000, '부스 임대료 외', 'issued', 'system-seed'), + ('KINTEX', 'bd-tax-0027', 'bd-inv-027', 'bd-e-027', 'BD-TAX-0027', '킨텍스(KINTEX)', '128-81-00000', '대표이사', '경기도 고양시 일산서구 킨텍스로 217', '넥스트테크', 15400000, 1540000, 16940000, '부스 임대료 외', 'issued', 'system-seed'), + ('KINTEX', 'bd-tax-0028', 'bd-inv-028', 'bd-e-028', 'BD-TAX-0028', '킨텍스(KINTEX)', '128-81-00000', '대표이사', '경기도 고양시 일산서구 킨텍스로 217', '(주)미래테크', 15800000, 1580000, 17380000, '부스 임대료 외', 'issued', 'system-seed'), + ('KINTEX', 'bd-tax-0029', 'bd-inv-029', 'bd-e-029', 'BD-TAX-0029', '킨텍스(KINTEX)', '128-81-00000', '대표이사', '경기도 고양시 일산서구 킨텍스로 217', '한빛테크', 16200000, 1620000, 17820000, '부스 임대료 외', 'issued', 'system-seed'), + ('KINTEX', 'bd-tax-0030', 'bd-inv-030', 'bd-e-030', 'BD-TAX-0030', '킨텍스(KINTEX)', '128-81-00000', '대표이사', '경기도 고양시 일산서구 킨텍스로 217', '(주)대한테크', 16600000, 1660000, 18260000, '부스 임대료 외', 'issued', 'system-seed'), + ('KINTEX', 'bd-tax-0031', 'bd-inv-031', 'bd-e-031', 'BD-TAX-0031', '킨텍스(KINTEX)', '128-81-00000', '대표이사', '경기도 고양시 일산서구 킨텍스로 217', '테크시스템즈', 5000000, 500000, 5500000, '부스 임대료 외', 'issued', 'system-seed'), + ('KINTEX', 'bd-tax-0032', 'bd-inv-032', 'bd-e-032', 'BD-TAX-0032', '킨텍스(KINTEX)', '128-81-00000', '대표이사', '경기도 고양시 일산서구 킨텍스로 217', '로보틱스시스템즈', 5400000, 540000, 5940000, '부스 임대료 외', 'issued', 'system-seed'), + ('KINTEX', 'bd-tax-0033', 'bd-inv-033', 'bd-e-033', 'BD-TAX-0033', '킨텍스(KINTEX)', '128-81-00000', '대표이사', '경기도 고양시 일산서구 킨텍스로 217', '(주)인더스트리시스템즈', 5800000, 580000, 6380000, '부스 임대료 외', 'issued', 'system-seed'), + ('KINTEX', 'bd-tax-0034', 'bd-inv-034', 'bd-e-034', 'BD-TAX-0034', '킨텍스(KINTEX)', '128-81-00000', '대표이사', '경기도 고양시 일산서구 킨텍스로 217', '시스템즈시스템즈', 6200000, 620000, 6820000, '부스 임대료 외', 'issued', 'system-seed'), + ('KINTEX', 'bd-tax-0035', 'bd-inv-035', 'bd-e-035', 'BD-TAX-0035', '킨텍스(KINTEX)', '128-81-00000', '대표이사', '경기도 고양시 일산서구 킨텍스로 217', '(주)솔루션시스템즈', 6600000, 660000, 7260000, '부스 임대료 외', 'issued', 'system-seed'), + ('KINTEX', 'bd-tax-0036', 'bd-inv-036', 'bd-e-036', 'BD-TAX-0036', '킨텍스(KINTEX)', '128-81-00000', '대표이사', '경기도 고양시 일산서구 킨텍스로 217', '이노베이션시스템즈', 7000000, 700000, 7700000, '부스 임대료 외', 'issued', 'system-seed'), + ('KINTEX', 'bd-tax-0037', 'bd-inv-037', 'bd-e-037', 'BD-TAX-0037', '킨텍스(KINTEX)', '128-81-00000', '대표이사', '경기도 고양시 일산서구 킨텍스로 217', '오토메이션시스템즈', 7400000, 740000, 8140000, '부스 임대료 외', 'issued', 'system-seed'), + ('KINTEX', 'bd-tax-0038', 'bd-inv-038', 'bd-e-038', 'BD-TAX-0038', '킨텍스(KINTEX)', '128-81-00000', '대표이사', '경기도 고양시 일산서구 킨텍스로 217', '(주)디자인시스템즈', 7800000, 780000, 8580000, '부스 임대료 외', 'issued', 'system-seed'), + ('KINTEX', 'bd-tax-0039', 'bd-inv-039', 'bd-e-039', 'BD-TAX-0039', '킨텍스(KINTEX)', '128-81-00000', '대표이사', '경기도 고양시 일산서구 킨텍스로 217', '엔지니어링시스템즈', 8200000, 820000, 9020000, '부스 임대료 외', 'issued', 'system-seed'), + ('KINTEX', 'bd-tax-0040', 'bd-inv-040', 'bd-e-040', 'BD-TAX-0040', '킨텍스(KINTEX)', '128-81-00000', '대표이사', '경기도 고양시 일산서구 킨텍스로 217', '(주)일렉트릭시스템즈', 8600000, 860000, 9460000, '부스 임대료 외', 'issued', 'system-seed'), + ('KINTEX', 'bd-tax-0041', 'bd-inv-041', 'bd-e-001', 'BD-TAX-0041', '킨텍스(KINTEX)', '128-81-00000', '대표이사', '경기도 고양시 일산서구 킨텍스로 217', '네트웍스시스템즈', 9000000, 900000, 9900000, '부스 임대료 외', 'issued', 'system-seed'), + ('KINTEX', 'bd-tax-0042', 'bd-inv-042', 'bd-e-002', 'BD-TAX-0042', '킨텍스(KINTEX)', '128-81-00000', '대표이사', '경기도 고양시 일산서구 킨텍스로 217', '미디어시스템즈', 9400000, 940000, 10340000, '부스 임대료 외', 'issued', 'system-seed'), + ('KINTEX', 'bd-tax-0043', 'bd-inv-043', 'bd-e-003', 'BD-TAX-0043', '킨텍스(KINTEX)', '128-81-00000', '대표이사', '경기도 고양시 일산서구 킨텍스로 217', '(주)디스플레이시스템즈', 9800000, 980000, 10780000, '부스 임대료 외', 'issued', 'system-seed'), + ('KINTEX', 'bd-tax-0044', 'bd-inv-044', 'bd-e-004', 'BD-TAX-0044', '킨텍스(KINTEX)', '128-81-00000', '대표이사', '경기도 고양시 일산서구 킨텍스로 217', '컨스트럭션시스템즈', 10200000, 1020000, 11220000, '부스 임대료 외', 'issued', 'system-seed'), + ('KINTEX', 'bd-tax-0045', 'bd-inv-045', 'bd-e-005', 'BD-TAX-0045', '킨텍스(KINTEX)', '128-81-00000', '대표이사', '경기도 고양시 일산서구 킨텍스로 217', '(주)부스시스템즈', 10600000, 1060000, 11660000, '부스 임대료 외', 'issued', 'system-seed'), + ('KINTEX', 'bd-tax-0046', 'bd-inv-046', 'bd-e-006', 'BD-TAX-0046', '킨텍스(KINTEX)', '128-81-00000', '대표이사', '경기도 고양시 일산서구 킨텍스로 217', '전기시설시스템즈', 11000000, 1100000, 12100000, '부스 임대료 외', 'issued', 'system-seed'), + ('KINTEX', 'bd-tax-0047', 'bd-inv-047', 'bd-e-007', 'BD-TAX-0047', '킨텍스(KINTEX)', '128-81-00000', '대표이사', '경기도 고양시 일산서구 킨텍스로 217', '구조시스템즈', 11400000, 1140000, 12540000, '부스 임대료 외', 'issued', 'system-seed'), + ('KINTEX', 'bd-tax-0048', 'bd-inv-048', 'bd-e-008', 'BD-TAX-0048', '킨텍스(KINTEX)', '128-81-00000', '대표이사', '경기도 고양시 일산서구 킨텍스로 217', '(주)비전시스템즈', 11800000, 1180000, 12980000, '부스 임대료 외', 'issued', 'system-seed'), + ('KINTEX', 'bd-tax-0049', 'bd-inv-049', 'bd-e-009', 'BD-TAX-0049', '킨텍스(KINTEX)', '128-81-00000', '대표이사', '경기도 고양시 일산서구 킨텍스로 217', '센서텍시스템즈', 12200000, 1220000, 13420000, '부스 임대료 외', 'issued', 'system-seed'), + ('KINTEX', 'bd-tax-0050', 'bd-inv-050', 'bd-e-010', 'BD-TAX-0050', '킨텍스(KINTEX)', '128-81-00000', '대표이사', '경기도 고양시 일산서구 킨텍스로 217', '(주)머티리얼시스템즈', 12600000, 1260000, 13860000, '부스 임대료 외', 'issued', 'system-seed'), + ('KINTEX', 'bd-tax-0051', 'bd-inv-051', 'bd-e-011', 'BD-TAX-0051', '킨텍스(KINTEX)', '128-81-00000', '대표이사', '경기도 고양시 일산서구 킨텍스로 217', '로지스시스템즈', 13000000, 1300000, 14300000, '부스 임대료 외', 'issued', 'system-seed'), + ('KINTEX', 'bd-tax-0052', 'bd-inv-052', 'bd-e-012', 'BD-TAX-0052', '킨텍스(KINTEX)', '128-81-00000', '대표이사', '경기도 고양시 일산서구 킨텍스로 217', '바이오시스템즈', 13400000, 1340000, 14740000, '부스 임대료 외', 'issued', 'system-seed') +ON CONFLICT (id) DO NOTHING; + +INSERT INTO invoice_refund (tenant_id, id, invoice_id, event_id, base_amount, refund_rate_percent, refund_amount, forfeit_amount, days_before_event, bracket_label, reason, ruleset_version, status, created_by) VALUES + ('KINTEX', 'bd-ref-0001', 'bd-inv-065', 'bd-e-025', 2904000, 50.0, 1452000, 1452000, 4, '개막 30~15일 전(50%)', '참가 철회 — 환불 규정 적용', 'refund-v1.0', 'recorded', 'system-seed'), + ('KINTEX', 'bd-ref-0002', 'bd-inv-066', 'bd-e-026', 3080000, 50.0, 1540000, 1540000, 5, '개막 30~15일 전(50%)', '참가 철회 — 환불 규정 적용', 'refund-v1.0', 'recorded', 'system-seed'), + ('KINTEX', 'bd-ref-0003', 'bd-inv-067', 'bd-e-027', 3256000, 50.0, 1628000, 1628000, 6, '개막 30~15일 전(50%)', '참가 철회 — 환불 규정 적용', 'refund-v1.0', 'recorded', 'system-seed'), + ('KINTEX', 'bd-ref-0004', 'bd-inv-068', 'bd-e-028', 3432000, 50.0, 1716000, 1716000, 7, '개막 30~15일 전(50%)', '참가 철회 — 환불 규정 적용', 'refund-v1.0', 'recorded', 'system-seed'), + ('KINTEX', 'bd-ref-0005', 'bd-inv-069', 'bd-e-029', 3608000, 50.0, 1804000, 1804000, 8, '개막 30~15일 전(50%)', '참가 철회 — 환불 규정 적용', 'refund-v1.0', 'recorded', 'system-seed'), + ('KINTEX', 'bd-ref-0006', 'bd-inv-070', 'bd-e-030', 3784000, 50.0, 1892000, 1892000, 9, '개막 30~15일 전(50%)', '참가 철회 — 환불 규정 적용', 'refund-v1.0', 'recorded', 'system-seed'), + ('KINTEX', 'bd-ref-0007', 'bd-inv-071', 'bd-e-031', 3960000, 50.0, 1980000, 1980000, 10, '개막 30~15일 전(50%)', '참가 철회 — 환불 규정 적용', 'refund-v1.0', 'recorded', 'system-seed'), + ('KINTEX', 'bd-ref-0008', 'bd-inv-072', 'bd-e-032', 4136000, 50.0, 2068000, 2068000, 11, '개막 30~15일 전(50%)', '참가 철회 — 환불 규정 적용', 'refund-v1.0', 'recorded', 'system-seed'), + ('KINTEX', 'bd-ref-0009', 'bd-inv-073', 'bd-e-033', 4312000, 50.0, 2156000, 2156000, 12, '개막 30~15일 전(50%)', '참가 철회 — 환불 규정 적용', 'refund-v1.0', 'recorded', 'system-seed'), + ('KINTEX', 'bd-ref-0010', 'bd-inv-074', 'bd-e-034', 4488000, 50.0, 2244000, 2244000, 13, '개막 30~15일 전(50%)', '참가 철회 — 환불 규정 적용', 'refund-v1.0', 'recorded', 'system-seed'), + ('KINTEX', 'bd-ref-0011', 'bd-inv-075', 'bd-e-035', 4664000, 50.0, 2332000, 2332000, 14, '개막 30~15일 전(50%)', '참가 철회 — 환불 규정 적용', 'refund-v1.0', 'recorded', 'system-seed'), + ('KINTEX', 'bd-ref-0012', 'bd-inv-076', 'bd-e-036', 4840000, 50.0, 2420000, 2420000, 15, '개막 30~15일 전(50%)', '참가 철회 — 환불 규정 적용', 'refund-v1.0', 'recorded', 'system-seed'), + ('KINTEX', 'bd-ref-0013', 'bd-inv-077', 'bd-e-037', 5016000, 50.0, 2508000, 2508000, 16, '개막 30~15일 전(50%)', '참가 철회 — 환불 규정 적용', 'refund-v1.0', 'recorded', 'system-seed'), + ('KINTEX', 'bd-ref-0014', 'bd-inv-078', 'bd-e-038', 5192000, 50.0, 2596000, 2596000, 17, '개막 30~15일 전(50%)', '참가 철회 — 환불 규정 적용', 'refund-v1.0', 'recorded', 'system-seed'), + ('KINTEX', 'bd-ref-0015', 'bd-inv-079', 'bd-e-039', 5368000, 50.0, 2684000, 2684000, 18, '개막 30~15일 전(50%)', '참가 철회 — 환불 규정 적용', 'refund-v1.0', 'recorded', 'system-seed'), + ('KINTEX', 'bd-ref-0016', 'bd-inv-080', 'bd-e-040', 5544000, 50.0, 2772000, 2772000, 19, '개막 30~15일 전(50%)', '참가 철회 — 환불 규정 적용', 'refund-v1.0', 'recorded', 'system-seed') +ON CONFLICT (id) DO NOTHING; + +-- 8. 전자결재(approval) 60 + 결재선(approval_line) + 이력(approval_history). +INSERT INTO approval (tenant_id, id, event_id, doc_type, title, content, ref_type, ref_id, drafter_id, drafter_name, status, cur_step, version, submitted_at, completed_at) VALUES + ('KINTEX', 'bd-ap-001', 'bd-e-002', 'GENERAL', 'BD 결재 문서 1', '대량 데모 결재 본문 1.', 'DOCUMENT', NULL, 'demo-exhibitor', '데모 참가업체(김참가)', 'SUBMITTED', 1, 1, now() - interval '2 day', NULL), + ('KINTEX', 'bd-ap-002', 'bd-e-003', 'GENERAL', 'BD 결재 문서 2', '대량 데모 결재 본문 2.', 'DOCUMENT', NULL, 'demo-contractor', '데모 공사업체(이시공)', 'IN_PROGRESS', 2, 1, now() - interval '3 day', NULL), + ('KINTEX', 'bd-ap-003', 'bd-e-004', 'GENERAL', 'BD 결재 문서 3', '대량 데모 결재 본문 3.', 'DOCUMENT', NULL, 'demo-organizer', '데모 주최자(홍길동)', 'APPROVED', 1, 1, now() - interval '4 day', now() - interval '1 day'), + ('KINTEX', 'bd-ap-004', 'bd-e-005', 'GENERAL', 'BD 결재 문서 4', '대량 데모 결재 본문 4.', 'DOCUMENT', NULL, 'demo-exhibitor', '데모 참가업체(김참가)', 'REJECTED', 1, 1, now() - interval '5 day', now() - interval '2 day'), + ('KINTEX', 'bd-ap-005', 'bd-e-006', 'GENERAL', 'BD 결재 문서 5', '대량 데모 결재 본문 5.', 'DOCUMENT', NULL, 'demo-contractor', '데모 공사업체(이시공)', 'DRAFT', 0, 1, NULL, NULL), + ('KINTEX', 'bd-ap-006', 'bd-e-007', 'GENERAL', 'BD 결재 문서 6', '대량 데모 결재 본문 6.', 'DOCUMENT', NULL, 'demo-organizer', '데모 주최자(홍길동)', 'SUBMITTED', 1, 1, now() - interval '2 day', NULL), + ('KINTEX', 'bd-ap-007', 'bd-e-008', 'GENERAL', 'BD 결재 문서 7', '대량 데모 결재 본문 7.', 'DOCUMENT', NULL, 'demo-exhibitor', '데모 참가업체(김참가)', 'IN_PROGRESS', 2, 1, now() - interval '3 day', NULL), + ('KINTEX', 'bd-ap-008', 'bd-e-009', 'GENERAL', 'BD 결재 문서 8', '대량 데모 결재 본문 8.', 'DOCUMENT', NULL, 'demo-contractor', '데모 공사업체(이시공)', 'APPROVED', 1, 1, now() - interval '4 day', now() - interval '3 day'), + ('KINTEX', 'bd-ap-009', 'bd-e-010', 'GENERAL', 'BD 결재 문서 9', '대량 데모 결재 본문 9.', 'DOCUMENT', NULL, 'demo-organizer', '데모 주최자(홍길동)', 'REJECTED', 1, 1, now() - interval '5 day', now() - interval '1 day'), + ('KINTEX', 'bd-ap-010', 'bd-e-011', 'GENERAL', 'BD 결재 문서 10', '대량 데모 결재 본문 10.', 'DOCUMENT', NULL, 'demo-exhibitor', '데모 참가업체(김참가)', 'DRAFT', 0, 1, NULL, NULL), + ('KINTEX', 'bd-ap-011', 'bd-e-012', 'GENERAL', 'BD 결재 문서 11', '대량 데모 결재 본문 11.', 'DOCUMENT', NULL, 'demo-contractor', '데모 공사업체(이시공)', 'SUBMITTED', 1, 1, now() - interval '2 day', NULL), + ('KINTEX', 'bd-ap-012', 'bd-e-013', 'GENERAL', 'BD 결재 문서 12', '대량 데모 결재 본문 12.', 'DOCUMENT', NULL, 'demo-organizer', '데모 주최자(홍길동)', 'IN_PROGRESS', 2, 1, now() - interval '3 day', NULL), + ('KINTEX', 'bd-ap-013', 'bd-e-014', 'GENERAL', 'BD 결재 문서 13', '대량 데모 결재 본문 13.', 'DOCUMENT', NULL, 'demo-exhibitor', '데모 참가업체(김참가)', 'APPROVED', 1, 1, now() - interval '4 day', now() - interval '2 day'), + ('KINTEX', 'bd-ap-014', 'bd-e-015', 'GENERAL', 'BD 결재 문서 14', '대량 데모 결재 본문 14.', 'DOCUMENT', NULL, 'demo-contractor', '데모 공사업체(이시공)', 'REJECTED', 1, 1, now() - interval '5 day', now() - interval '3 day'), + ('KINTEX', 'bd-ap-015', 'bd-e-016', 'GENERAL', 'BD 결재 문서 15', '대량 데모 결재 본문 15.', 'DOCUMENT', NULL, 'demo-organizer', '데모 주최자(홍길동)', 'DRAFT', 0, 1, NULL, NULL), + ('KINTEX', 'bd-ap-016', 'bd-e-017', 'GENERAL', 'BD 결재 문서 16', '대량 데모 결재 본문 16.', 'DOCUMENT', NULL, 'demo-exhibitor', '데모 참가업체(김참가)', 'SUBMITTED', 1, 1, now() - interval '2 day', NULL), + ('KINTEX', 'bd-ap-017', 'bd-e-018', 'GENERAL', 'BD 결재 문서 17', '대량 데모 결재 본문 17.', 'DOCUMENT', NULL, 'demo-contractor', '데모 공사업체(이시공)', 'IN_PROGRESS', 2, 1, now() - interval '3 day', NULL), + ('KINTEX', 'bd-ap-018', 'bd-e-019', 'GENERAL', 'BD 결재 문서 18', '대량 데모 결재 본문 18.', 'DOCUMENT', NULL, 'demo-organizer', '데모 주최자(홍길동)', 'APPROVED', 1, 1, now() - interval '4 day', now() - interval '1 day'), + ('KINTEX', 'bd-ap-019', 'bd-e-020', 'GENERAL', 'BD 결재 문서 19', '대량 데모 결재 본문 19.', 'DOCUMENT', NULL, 'demo-exhibitor', '데모 참가업체(김참가)', 'REJECTED', 1, 1, now() - interval '5 day', now() - interval '2 day'), + ('KINTEX', 'bd-ap-020', 'bd-e-021', 'GENERAL', 'BD 결재 문서 20', '대량 데모 결재 본문 20.', 'DOCUMENT', NULL, 'demo-contractor', '데모 공사업체(이시공)', 'DRAFT', 0, 1, NULL, NULL), + ('KINTEX', 'bd-ap-021', 'bd-e-022', 'GENERAL', 'BD 결재 문서 21', '대량 데모 결재 본문 21.', 'DOCUMENT', NULL, 'demo-organizer', '데모 주최자(홍길동)', 'SUBMITTED', 1, 1, now() - interval '2 day', NULL), + ('KINTEX', 'bd-ap-022', 'bd-e-023', 'GENERAL', 'BD 결재 문서 22', '대량 데모 결재 본문 22.', 'DOCUMENT', NULL, 'demo-exhibitor', '데모 참가업체(김참가)', 'IN_PROGRESS', 2, 1, now() - interval '3 day', NULL), + ('KINTEX', 'bd-ap-023', 'bd-e-024', 'GENERAL', 'BD 결재 문서 23', '대량 데모 결재 본문 23.', 'DOCUMENT', NULL, 'demo-contractor', '데모 공사업체(이시공)', 'APPROVED', 1, 1, now() - interval '4 day', now() - interval '3 day'), + ('KINTEX', 'bd-ap-024', 'bd-e-025', 'GENERAL', 'BD 결재 문서 24', '대량 데모 결재 본문 24.', 'DOCUMENT', NULL, 'demo-organizer', '데모 주최자(홍길동)', 'REJECTED', 1, 1, now() - interval '5 day', now() - interval '1 day'), + ('KINTEX', 'bd-ap-025', 'bd-e-026', 'GENERAL', 'BD 결재 문서 25', '대량 데모 결재 본문 25.', 'DOCUMENT', NULL, 'demo-exhibitor', '데모 참가업체(김참가)', 'DRAFT', 0, 1, NULL, NULL), + ('KINTEX', 'bd-ap-026', 'bd-e-027', 'GENERAL', 'BD 결재 문서 26', '대량 데모 결재 본문 26.', 'DOCUMENT', NULL, 'demo-contractor', '데모 공사업체(이시공)', 'SUBMITTED', 1, 1, now() - interval '2 day', NULL), + ('KINTEX', 'bd-ap-027', 'bd-e-028', 'GENERAL', 'BD 결재 문서 27', '대량 데모 결재 본문 27.', 'DOCUMENT', NULL, 'demo-organizer', '데모 주최자(홍길동)', 'IN_PROGRESS', 2, 1, now() - interval '3 day', NULL), + ('KINTEX', 'bd-ap-028', 'bd-e-029', 'GENERAL', 'BD 결재 문서 28', '대량 데모 결재 본문 28.', 'DOCUMENT', NULL, 'demo-exhibitor', '데모 참가업체(김참가)', 'APPROVED', 1, 1, now() - interval '4 day', now() - interval '2 day'), + ('KINTEX', 'bd-ap-029', 'bd-e-030', 'GENERAL', 'BD 결재 문서 29', '대량 데모 결재 본문 29.', 'DOCUMENT', NULL, 'demo-contractor', '데모 공사업체(이시공)', 'REJECTED', 1, 1, now() - interval '5 day', now() - interval '3 day'), + ('KINTEX', 'bd-ap-030', 'bd-e-031', 'GENERAL', 'BD 결재 문서 30', '대량 데모 결재 본문 30.', 'DOCUMENT', NULL, 'demo-organizer', '데모 주최자(홍길동)', 'DRAFT', 0, 1, NULL, NULL), + ('KINTEX', 'bd-ap-031', 'bd-e-032', 'GENERAL', 'BD 결재 문서 31', '대량 데모 결재 본문 31.', 'DOCUMENT', NULL, 'demo-exhibitor', '데모 참가업체(김참가)', 'SUBMITTED', 1, 1, now() - interval '2 day', NULL), + ('KINTEX', 'bd-ap-032', 'bd-e-033', 'GENERAL', 'BD 결재 문서 32', '대량 데모 결재 본문 32.', 'DOCUMENT', NULL, 'demo-contractor', '데모 공사업체(이시공)', 'IN_PROGRESS', 2, 1, now() - interval '3 day', NULL), + ('KINTEX', 'bd-ap-033', 'bd-e-034', 'GENERAL', 'BD 결재 문서 33', '대량 데모 결재 본문 33.', 'DOCUMENT', NULL, 'demo-organizer', '데모 주최자(홍길동)', 'APPROVED', 1, 1, now() - interval '4 day', now() - interval '1 day'), + ('KINTEX', 'bd-ap-034', 'bd-e-035', 'GENERAL', 'BD 결재 문서 34', '대량 데모 결재 본문 34.', 'DOCUMENT', NULL, 'demo-exhibitor', '데모 참가업체(김참가)', 'REJECTED', 1, 1, now() - interval '5 day', now() - interval '2 day'), + ('KINTEX', 'bd-ap-035', 'bd-e-036', 'GENERAL', 'BD 결재 문서 35', '대량 데모 결재 본문 35.', 'DOCUMENT', NULL, 'demo-contractor', '데모 공사업체(이시공)', 'DRAFT', 0, 1, NULL, NULL), + ('KINTEX', 'bd-ap-036', 'bd-e-037', 'GENERAL', 'BD 결재 문서 36', '대량 데모 결재 본문 36.', 'DOCUMENT', NULL, 'demo-organizer', '데모 주최자(홍길동)', 'SUBMITTED', 1, 1, now() - interval '2 day', NULL), + ('KINTEX', 'bd-ap-037', 'bd-e-038', 'GENERAL', 'BD 결재 문서 37', '대량 데모 결재 본문 37.', 'DOCUMENT', NULL, 'demo-exhibitor', '데모 참가업체(김참가)', 'IN_PROGRESS', 2, 1, now() - interval '3 day', NULL), + ('KINTEX', 'bd-ap-038', 'bd-e-039', 'GENERAL', 'BD 결재 문서 38', '대량 데모 결재 본문 38.', 'DOCUMENT', NULL, 'demo-contractor', '데모 공사업체(이시공)', 'APPROVED', 1, 1, now() - interval '4 day', now() - interval '3 day'), + ('KINTEX', 'bd-ap-039', 'bd-e-040', 'GENERAL', 'BD 결재 문서 39', '대량 데모 결재 본문 39.', 'DOCUMENT', NULL, 'demo-organizer', '데모 주최자(홍길동)', 'REJECTED', 1, 1, now() - interval '5 day', now() - interval '1 day'), + ('KINTEX', 'bd-ap-040', 'bd-e-041', 'GENERAL', 'BD 결재 문서 40', '대량 데모 결재 본문 40.', 'DOCUMENT', NULL, 'demo-exhibitor', '데모 참가업체(김참가)', 'DRAFT', 0, 1, NULL, NULL), + ('KINTEX', 'bd-ap-041', 'bd-e-042', 'GENERAL', 'BD 결재 문서 41', '대량 데모 결재 본문 41.', 'DOCUMENT', NULL, 'demo-contractor', '데모 공사업체(이시공)', 'SUBMITTED', 1, 1, now() - interval '2 day', NULL), + ('KINTEX', 'bd-ap-042', 'bd-e-043', 'GENERAL', 'BD 결재 문서 42', '대량 데모 결재 본문 42.', 'DOCUMENT', NULL, 'demo-organizer', '데모 주최자(홍길동)', 'IN_PROGRESS', 2, 1, now() - interval '3 day', NULL), + ('KINTEX', 'bd-ap-043', 'bd-e-044', 'GENERAL', 'BD 결재 문서 43', '대량 데모 결재 본문 43.', 'DOCUMENT', NULL, 'demo-exhibitor', '데모 참가업체(김참가)', 'APPROVED', 1, 1, now() - interval '4 day', now() - interval '2 day'), + ('KINTEX', 'bd-ap-044', 'bd-e-045', 'GENERAL', 'BD 결재 문서 44', '대량 데모 결재 본문 44.', 'DOCUMENT', NULL, 'demo-contractor', '데모 공사업체(이시공)', 'REJECTED', 1, 1, now() - interval '5 day', now() - interval '3 day'), + ('KINTEX', 'bd-ap-045', 'bd-e-046', 'GENERAL', 'BD 결재 문서 45', '대량 데모 결재 본문 45.', 'DOCUMENT', NULL, 'demo-organizer', '데모 주최자(홍길동)', 'DRAFT', 0, 1, NULL, NULL), + ('KINTEX', 'bd-ap-046', 'bd-e-047', 'GENERAL', 'BD 결재 문서 46', '대량 데모 결재 본문 46.', 'DOCUMENT', NULL, 'demo-exhibitor', '데모 참가업체(김참가)', 'SUBMITTED', 1, 1, now() - interval '2 day', NULL), + ('KINTEX', 'bd-ap-047', 'bd-e-048', 'GENERAL', 'BD 결재 문서 47', '대량 데모 결재 본문 47.', 'DOCUMENT', NULL, 'demo-contractor', '데모 공사업체(이시공)', 'IN_PROGRESS', 2, 1, now() - interval '3 day', NULL), + ('KINTEX', 'bd-ap-048', 'bd-e-049', 'GENERAL', 'BD 결재 문서 48', '대량 데모 결재 본문 48.', 'DOCUMENT', NULL, 'demo-organizer', '데모 주최자(홍길동)', 'APPROVED', 1, 1, now() - interval '4 day', now() - interval '1 day'), + ('KINTEX', 'bd-ap-049', 'bd-e-050', 'GENERAL', 'BD 결재 문서 49', '대량 데모 결재 본문 49.', 'DOCUMENT', NULL, 'demo-exhibitor', '데모 참가업체(김참가)', 'REJECTED', 1, 1, now() - interval '5 day', now() - interval '2 day'), + ('KINTEX', 'bd-ap-050', 'bd-e-051', 'GENERAL', 'BD 결재 문서 50', '대량 데모 결재 본문 50.', 'DOCUMENT', NULL, 'demo-contractor', '데모 공사업체(이시공)', 'DRAFT', 0, 1, NULL, NULL), + ('KINTEX', 'bd-ap-051', 'bd-e-052', 'GENERAL', 'BD 결재 문서 51', '대량 데모 결재 본문 51.', 'DOCUMENT', NULL, 'demo-organizer', '데모 주최자(홍길동)', 'SUBMITTED', 1, 1, now() - interval '2 day', NULL), + ('KINTEX', 'bd-ap-052', 'bd-e-053', 'GENERAL', 'BD 결재 문서 52', '대량 데모 결재 본문 52.', 'DOCUMENT', NULL, 'demo-exhibitor', '데모 참가업체(김참가)', 'IN_PROGRESS', 2, 1, now() - interval '3 day', NULL), + ('KINTEX', 'bd-ap-053', 'bd-e-054', 'GENERAL', 'BD 결재 문서 53', '대량 데모 결재 본문 53.', 'DOCUMENT', NULL, 'demo-contractor', '데모 공사업체(이시공)', 'APPROVED', 1, 1, now() - interval '4 day', now() - interval '3 day'), + ('KINTEX', 'bd-ap-054', 'bd-e-055', 'GENERAL', 'BD 결재 문서 54', '대량 데모 결재 본문 54.', 'DOCUMENT', NULL, 'demo-organizer', '데모 주최자(홍길동)', 'REJECTED', 1, 1, now() - interval '5 day', now() - interval '1 day'), + ('KINTEX', 'bd-ap-055', 'bd-e-056', 'GENERAL', 'BD 결재 문서 55', '대량 데모 결재 본문 55.', 'DOCUMENT', NULL, 'demo-exhibitor', '데모 참가업체(김참가)', 'DRAFT', 0, 1, NULL, NULL), + ('KINTEX', 'bd-ap-056', 'bd-e-057', 'GENERAL', 'BD 결재 문서 56', '대량 데모 결재 본문 56.', 'DOCUMENT', NULL, 'demo-contractor', '데모 공사업체(이시공)', 'SUBMITTED', 1, 1, now() - interval '2 day', NULL), + ('KINTEX', 'bd-ap-057', 'bd-e-058', 'GENERAL', 'BD 결재 문서 57', '대량 데모 결재 본문 57.', 'DOCUMENT', NULL, 'demo-organizer', '데모 주최자(홍길동)', 'IN_PROGRESS', 2, 1, now() - interval '3 day', NULL), + ('KINTEX', 'bd-ap-058', 'bd-e-059', 'GENERAL', 'BD 결재 문서 58', '대량 데모 결재 본문 58.', 'DOCUMENT', NULL, 'demo-exhibitor', '데모 참가업체(김참가)', 'APPROVED', 1, 1, now() - interval '4 day', now() - interval '2 day'), + ('KINTEX', 'bd-ap-059', 'bd-e-060', 'GENERAL', 'BD 결재 문서 59', '대량 데모 결재 본문 59.', 'DOCUMENT', NULL, 'demo-contractor', '데모 공사업체(이시공)', 'REJECTED', 1, 1, now() - interval '5 day', now() - interval '3 day'), + ('KINTEX', 'bd-ap-060', 'bd-e-001', 'GENERAL', 'BD 결재 문서 60', '대량 데모 결재 본문 60.', 'DOCUMENT', NULL, 'demo-organizer', '데모 주최자(홍길동)', 'DRAFT', 0, 1, NULL, NULL) +ON CONFLICT (id) DO NOTHING; + +INSERT INTO approval_line (tenant_id, id, approval_id, step_no, approver_id, approver_name, line_status, acted_at, comment) VALUES + ('KINTEX', 'bd-al-0001', 'bd-ap-001', 1, 'demo-organizer', '데모 주최자(홍길동)', 'CURRENT', NULL, NULL), + ('KINTEX', 'bd-al-0002', 'bd-ap-002', 1, 'demo-organizer', '데모 주최자(홍길동)', 'APPROVED', now() - interval '3 day', NULL), + ('KINTEX', 'bd-al-0003', 'bd-ap-002', 2, 'demo-organizer', '데모 주최자(홍길동)', 'CURRENT', NULL, NULL), + ('KINTEX', 'bd-al-0004', 'bd-ap-003', 1, 'demo-organizer', '데모 주최자(홍길동)', 'APPROVED', now() - interval '1 day', NULL), + ('KINTEX', 'bd-al-0005', 'bd-ap-004', 1, 'demo-organizer', '데모 주최자(홍길동)', 'REJECTED', now() - interval '2 day', NULL), + ('KINTEX', 'bd-al-0006', 'bd-ap-005', 1, 'demo-organizer', '데모 주최자(홍길동)', 'WAIT', NULL, NULL), + ('KINTEX', 'bd-al-0007', 'bd-ap-006', 1, 'demo-organizer', '데모 주최자(홍길동)', 'CURRENT', NULL, NULL), + ('KINTEX', 'bd-al-0008', 'bd-ap-007', 1, 'demo-organizer', '데모 주최자(홍길동)', 'APPROVED', now() - interval '2 day', NULL), + ('KINTEX', 'bd-al-0009', 'bd-ap-007', 2, 'demo-organizer', '데모 주최자(홍길동)', 'CURRENT', NULL, NULL), + ('KINTEX', 'bd-al-0010', 'bd-ap-008', 1, 'demo-organizer', '데모 주최자(홍길동)', 'APPROVED', now() - interval '3 day', NULL), + ('KINTEX', 'bd-al-0011', 'bd-ap-009', 1, 'demo-organizer', '데모 주최자(홍길동)', 'REJECTED', now() - interval '1 day', NULL), + ('KINTEX', 'bd-al-0012', 'bd-ap-010', 1, 'demo-organizer', '데모 주최자(홍길동)', 'WAIT', NULL, NULL), + ('KINTEX', 'bd-al-0013', 'bd-ap-011', 1, 'demo-organizer', '데모 주최자(홍길동)', 'CURRENT', NULL, NULL), + ('KINTEX', 'bd-al-0014', 'bd-ap-012', 1, 'demo-organizer', '데모 주최자(홍길동)', 'APPROVED', now() - interval '1 day', NULL), + ('KINTEX', 'bd-al-0015', 'bd-ap-012', 2, 'demo-organizer', '데모 주최자(홍길동)', 'CURRENT', NULL, NULL), + ('KINTEX', 'bd-al-0016', 'bd-ap-013', 1, 'demo-organizer', '데모 주최자(홍길동)', 'APPROVED', now() - interval '2 day', NULL), + ('KINTEX', 'bd-al-0017', 'bd-ap-014', 1, 'demo-organizer', '데모 주최자(홍길동)', 'REJECTED', now() - interval '3 day', NULL), + ('KINTEX', 'bd-al-0018', 'bd-ap-015', 1, 'demo-organizer', '데모 주최자(홍길동)', 'WAIT', NULL, NULL), + ('KINTEX', 'bd-al-0019', 'bd-ap-016', 1, 'demo-organizer', '데모 주최자(홍길동)', 'CURRENT', NULL, NULL), + ('KINTEX', 'bd-al-0020', 'bd-ap-017', 1, 'demo-organizer', '데모 주최자(홍길동)', 'APPROVED', now() - interval '3 day', NULL), + ('KINTEX', 'bd-al-0021', 'bd-ap-017', 2, 'demo-organizer', '데모 주최자(홍길동)', 'CURRENT', NULL, NULL), + ('KINTEX', 'bd-al-0022', 'bd-ap-018', 1, 'demo-organizer', '데모 주최자(홍길동)', 'APPROVED', now() - interval '1 day', NULL), + ('KINTEX', 'bd-al-0023', 'bd-ap-019', 1, 'demo-organizer', '데모 주최자(홍길동)', 'REJECTED', now() - interval '2 day', NULL), + ('KINTEX', 'bd-al-0024', 'bd-ap-020', 1, 'demo-organizer', '데모 주최자(홍길동)', 'WAIT', NULL, NULL), + ('KINTEX', 'bd-al-0025', 'bd-ap-021', 1, 'demo-organizer', '데모 주최자(홍길동)', 'CURRENT', NULL, NULL), + ('KINTEX', 'bd-al-0026', 'bd-ap-022', 1, 'demo-organizer', '데모 주최자(홍길동)', 'APPROVED', now() - interval '2 day', NULL), + ('KINTEX', 'bd-al-0027', 'bd-ap-022', 2, 'demo-organizer', '데모 주최자(홍길동)', 'CURRENT', NULL, NULL), + ('KINTEX', 'bd-al-0028', 'bd-ap-023', 1, 'demo-organizer', '데모 주최자(홍길동)', 'APPROVED', now() - interval '3 day', NULL), + ('KINTEX', 'bd-al-0029', 'bd-ap-024', 1, 'demo-organizer', '데모 주최자(홍길동)', 'REJECTED', now() - interval '1 day', NULL), + ('KINTEX', 'bd-al-0030', 'bd-ap-025', 1, 'demo-organizer', '데모 주최자(홍길동)', 'WAIT', NULL, NULL), + ('KINTEX', 'bd-al-0031', 'bd-ap-026', 1, 'demo-organizer', '데모 주최자(홍길동)', 'CURRENT', NULL, NULL), + ('KINTEX', 'bd-al-0032', 'bd-ap-027', 1, 'demo-organizer', '데모 주최자(홍길동)', 'APPROVED', now() - interval '1 day', NULL), + ('KINTEX', 'bd-al-0033', 'bd-ap-027', 2, 'demo-organizer', '데모 주최자(홍길동)', 'CURRENT', NULL, NULL), + ('KINTEX', 'bd-al-0034', 'bd-ap-028', 1, 'demo-organizer', '데모 주최자(홍길동)', 'APPROVED', now() - interval '2 day', NULL), + ('KINTEX', 'bd-al-0035', 'bd-ap-029', 1, 'demo-organizer', '데모 주최자(홍길동)', 'REJECTED', now() - interval '3 day', NULL), + ('KINTEX', 'bd-al-0036', 'bd-ap-030', 1, 'demo-organizer', '데모 주최자(홍길동)', 'WAIT', NULL, NULL), + ('KINTEX', 'bd-al-0037', 'bd-ap-031', 1, 'demo-organizer', '데모 주최자(홍길동)', 'CURRENT', NULL, NULL), + ('KINTEX', 'bd-al-0038', 'bd-ap-032', 1, 'demo-organizer', '데모 주최자(홍길동)', 'APPROVED', now() - interval '3 day', NULL), + ('KINTEX', 'bd-al-0039', 'bd-ap-032', 2, 'demo-organizer', '데모 주최자(홍길동)', 'CURRENT', NULL, NULL), + ('KINTEX', 'bd-al-0040', 'bd-ap-033', 1, 'demo-organizer', '데모 주최자(홍길동)', 'APPROVED', now() - interval '1 day', NULL), + ('KINTEX', 'bd-al-0041', 'bd-ap-034', 1, 'demo-organizer', '데모 주최자(홍길동)', 'REJECTED', now() - interval '2 day', NULL), + ('KINTEX', 'bd-al-0042', 'bd-ap-035', 1, 'demo-organizer', '데모 주최자(홍길동)', 'WAIT', NULL, NULL), + ('KINTEX', 'bd-al-0043', 'bd-ap-036', 1, 'demo-organizer', '데모 주최자(홍길동)', 'CURRENT', NULL, NULL), + ('KINTEX', 'bd-al-0044', 'bd-ap-037', 1, 'demo-organizer', '데모 주최자(홍길동)', 'APPROVED', now() - interval '2 day', NULL), + ('KINTEX', 'bd-al-0045', 'bd-ap-037', 2, 'demo-organizer', '데모 주최자(홍길동)', 'CURRENT', NULL, NULL), + ('KINTEX', 'bd-al-0046', 'bd-ap-038', 1, 'demo-organizer', '데모 주최자(홍길동)', 'APPROVED', now() - interval '3 day', NULL), + ('KINTEX', 'bd-al-0047', 'bd-ap-039', 1, 'demo-organizer', '데모 주최자(홍길동)', 'REJECTED', now() - interval '1 day', NULL), + ('KINTEX', 'bd-al-0048', 'bd-ap-040', 1, 'demo-organizer', '데모 주최자(홍길동)', 'WAIT', NULL, NULL), + ('KINTEX', 'bd-al-0049', 'bd-ap-041', 1, 'demo-organizer', '데모 주최자(홍길동)', 'CURRENT', NULL, NULL), + ('KINTEX', 'bd-al-0050', 'bd-ap-042', 1, 'demo-organizer', '데모 주최자(홍길동)', 'APPROVED', now() - interval '1 day', NULL), + ('KINTEX', 'bd-al-0051', 'bd-ap-042', 2, 'demo-organizer', '데모 주최자(홍길동)', 'CURRENT', NULL, NULL), + ('KINTEX', 'bd-al-0052', 'bd-ap-043', 1, 'demo-organizer', '데모 주최자(홍길동)', 'APPROVED', now() - interval '2 day', NULL), + ('KINTEX', 'bd-al-0053', 'bd-ap-044', 1, 'demo-organizer', '데모 주최자(홍길동)', 'REJECTED', now() - interval '3 day', NULL), + ('KINTEX', 'bd-al-0054', 'bd-ap-045', 1, 'demo-organizer', '데모 주최자(홍길동)', 'WAIT', NULL, NULL), + ('KINTEX', 'bd-al-0055', 'bd-ap-046', 1, 'demo-organizer', '데모 주최자(홍길동)', 'CURRENT', NULL, NULL), + ('KINTEX', 'bd-al-0056', 'bd-ap-047', 1, 'demo-organizer', '데모 주최자(홍길동)', 'APPROVED', now() - interval '3 day', NULL), + ('KINTEX', 'bd-al-0057', 'bd-ap-047', 2, 'demo-organizer', '데모 주최자(홍길동)', 'CURRENT', NULL, NULL), + ('KINTEX', 'bd-al-0058', 'bd-ap-048', 1, 'demo-organizer', '데모 주최자(홍길동)', 'APPROVED', now() - interval '1 day', NULL), + ('KINTEX', 'bd-al-0059', 'bd-ap-049', 1, 'demo-organizer', '데모 주최자(홍길동)', 'REJECTED', now() - interval '2 day', NULL), + ('KINTEX', 'bd-al-0060', 'bd-ap-050', 1, 'demo-organizer', '데모 주최자(홍길동)', 'WAIT', NULL, NULL), + ('KINTEX', 'bd-al-0061', 'bd-ap-051', 1, 'demo-organizer', '데모 주최자(홍길동)', 'CURRENT', NULL, NULL), + ('KINTEX', 'bd-al-0062', 'bd-ap-052', 1, 'demo-organizer', '데모 주최자(홍길동)', 'APPROVED', now() - interval '2 day', NULL), + ('KINTEX', 'bd-al-0063', 'bd-ap-052', 2, 'demo-organizer', '데모 주최자(홍길동)', 'CURRENT', NULL, NULL), + ('KINTEX', 'bd-al-0064', 'bd-ap-053', 1, 'demo-organizer', '데모 주최자(홍길동)', 'APPROVED', now() - interval '3 day', NULL), + ('KINTEX', 'bd-al-0065', 'bd-ap-054', 1, 'demo-organizer', '데모 주최자(홍길동)', 'REJECTED', now() - interval '1 day', NULL), + ('KINTEX', 'bd-al-0066', 'bd-ap-055', 1, 'demo-organizer', '데모 주최자(홍길동)', 'WAIT', NULL, NULL), + ('KINTEX', 'bd-al-0067', 'bd-ap-056', 1, 'demo-organizer', '데모 주최자(홍길동)', 'CURRENT', NULL, NULL), + ('KINTEX', 'bd-al-0068', 'bd-ap-057', 1, 'demo-organizer', '데모 주최자(홍길동)', 'APPROVED', now() - interval '1 day', NULL), + ('KINTEX', 'bd-al-0069', 'bd-ap-057', 2, 'demo-organizer', '데모 주최자(홍길동)', 'CURRENT', NULL, NULL), + ('KINTEX', 'bd-al-0070', 'bd-ap-058', 1, 'demo-organizer', '데모 주최자(홍길동)', 'APPROVED', now() - interval '2 day', NULL), + ('KINTEX', 'bd-al-0071', 'bd-ap-059', 1, 'demo-organizer', '데모 주최자(홍길동)', 'REJECTED', now() - interval '3 day', NULL), + ('KINTEX', 'bd-al-0072', 'bd-ap-060', 1, 'demo-organizer', '데모 주최자(홍길동)', 'WAIT', NULL, NULL) +ON CONFLICT (tenant_id, id) DO NOTHING; + +INSERT INTO approval_history (tenant_id, id, approval_id, step_no, actor_id, actor_name, action, comment, acted_at) VALUES + ('KINTEX', 'bd-ah-0001', 'bd-ap-001', 1, 'demo-exhibitor', '데모 참가업체(김참가)', 'SUBMIT', '상신', now() - interval '2 day'), + ('KINTEX', 'bd-ah-0002', 'bd-ap-002', 1, 'demo-contractor', '데모 공사업체(이시공)', 'SUBMIT', '상신', now() - interval '3 day'), + ('KINTEX', 'bd-ah-0003', 'bd-ap-003', 1, 'demo-organizer', '데모 주최자(홍길동)', 'SUBMIT', '상신', now() - interval '4 day'), + ('KINTEX', 'bd-ah-0004', 'bd-ap-003', 1, 'demo-organizer', '데모 주최자(홍길동)', 'APPROVE', '처리', now() - interval '1 day'), + ('KINTEX', 'bd-ah-0005', 'bd-ap-004', 1, 'demo-exhibitor', '데모 참가업체(김참가)', 'SUBMIT', '상신', now() - interval '5 day'), + ('KINTEX', 'bd-ah-0006', 'bd-ap-004', 1, 'demo-organizer', '데모 주최자(홍길동)', 'REJECT', '처리', now() - interval '2 day'), + ('KINTEX', 'bd-ah-0007', 'bd-ap-006', 1, 'demo-organizer', '데모 주최자(홍길동)', 'SUBMIT', '상신', now() - interval '2 day'), + ('KINTEX', 'bd-ah-0008', 'bd-ap-007', 1, 'demo-exhibitor', '데모 참가업체(김참가)', 'SUBMIT', '상신', now() - interval '3 day'), + ('KINTEX', 'bd-ah-0009', 'bd-ap-008', 1, 'demo-contractor', '데모 공사업체(이시공)', 'SUBMIT', '상신', now() - interval '4 day'), + ('KINTEX', 'bd-ah-0010', 'bd-ap-008', 1, 'demo-organizer', '데모 주최자(홍길동)', 'APPROVE', '처리', now() - interval '3 day'), + ('KINTEX', 'bd-ah-0011', 'bd-ap-009', 1, 'demo-organizer', '데모 주최자(홍길동)', 'SUBMIT', '상신', now() - interval '5 day'), + ('KINTEX', 'bd-ah-0012', 'bd-ap-009', 1, 'demo-organizer', '데모 주최자(홍길동)', 'REJECT', '처리', now() - interval '1 day'), + ('KINTEX', 'bd-ah-0013', 'bd-ap-011', 1, 'demo-contractor', '데모 공사업체(이시공)', 'SUBMIT', '상신', now() - interval '2 day'), + ('KINTEX', 'bd-ah-0014', 'bd-ap-012', 1, 'demo-organizer', '데모 주최자(홍길동)', 'SUBMIT', '상신', now() - interval '3 day'), + ('KINTEX', 'bd-ah-0015', 'bd-ap-013', 1, 'demo-exhibitor', '데모 참가업체(김참가)', 'SUBMIT', '상신', now() - interval '4 day'), + ('KINTEX', 'bd-ah-0016', 'bd-ap-013', 1, 'demo-organizer', '데모 주최자(홍길동)', 'APPROVE', '처리', now() - interval '2 day'), + ('KINTEX', 'bd-ah-0017', 'bd-ap-014', 1, 'demo-contractor', '데모 공사업체(이시공)', 'SUBMIT', '상신', now() - interval '5 day'), + ('KINTEX', 'bd-ah-0018', 'bd-ap-014', 1, 'demo-organizer', '데모 주최자(홍길동)', 'REJECT', '처리', now() - interval '3 day'), + ('KINTEX', 'bd-ah-0019', 'bd-ap-016', 1, 'demo-exhibitor', '데모 참가업체(김참가)', 'SUBMIT', '상신', now() - interval '2 day'), + ('KINTEX', 'bd-ah-0020', 'bd-ap-017', 1, 'demo-contractor', '데모 공사업체(이시공)', 'SUBMIT', '상신', now() - interval '3 day'), + ('KINTEX', 'bd-ah-0021', 'bd-ap-018', 1, 'demo-organizer', '데모 주최자(홍길동)', 'SUBMIT', '상신', now() - interval '4 day'), + ('KINTEX', 'bd-ah-0022', 'bd-ap-018', 1, 'demo-organizer', '데모 주최자(홍길동)', 'APPROVE', '처리', now() - interval '1 day'), + ('KINTEX', 'bd-ah-0023', 'bd-ap-019', 1, 'demo-exhibitor', '데모 참가업체(김참가)', 'SUBMIT', '상신', now() - interval '5 day'), + ('KINTEX', 'bd-ah-0024', 'bd-ap-019', 1, 'demo-organizer', '데모 주최자(홍길동)', 'REJECT', '처리', now() - interval '2 day'), + ('KINTEX', 'bd-ah-0025', 'bd-ap-021', 1, 'demo-organizer', '데모 주최자(홍길동)', 'SUBMIT', '상신', now() - interval '2 day'), + ('KINTEX', 'bd-ah-0026', 'bd-ap-022', 1, 'demo-exhibitor', '데모 참가업체(김참가)', 'SUBMIT', '상신', now() - interval '3 day'), + ('KINTEX', 'bd-ah-0027', 'bd-ap-023', 1, 'demo-contractor', '데모 공사업체(이시공)', 'SUBMIT', '상신', now() - interval '4 day'), + ('KINTEX', 'bd-ah-0028', 'bd-ap-023', 1, 'demo-organizer', '데모 주최자(홍길동)', 'APPROVE', '처리', now() - interval '3 day'), + ('KINTEX', 'bd-ah-0029', 'bd-ap-024', 1, 'demo-organizer', '데모 주최자(홍길동)', 'SUBMIT', '상신', now() - interval '5 day'), + ('KINTEX', 'bd-ah-0030', 'bd-ap-024', 1, 'demo-organizer', '데모 주최자(홍길동)', 'REJECT', '처리', now() - interval '1 day'), + ('KINTEX', 'bd-ah-0031', 'bd-ap-026', 1, 'demo-contractor', '데모 공사업체(이시공)', 'SUBMIT', '상신', now() - interval '2 day'), + ('KINTEX', 'bd-ah-0032', 'bd-ap-027', 1, 'demo-organizer', '데모 주최자(홍길동)', 'SUBMIT', '상신', now() - interval '3 day'), + ('KINTEX', 'bd-ah-0033', 'bd-ap-028', 1, 'demo-exhibitor', '데모 참가업체(김참가)', 'SUBMIT', '상신', now() - interval '4 day'), + ('KINTEX', 'bd-ah-0034', 'bd-ap-028', 1, 'demo-organizer', '데모 주최자(홍길동)', 'APPROVE', '처리', now() - interval '2 day'), + ('KINTEX', 'bd-ah-0035', 'bd-ap-029', 1, 'demo-contractor', '데모 공사업체(이시공)', 'SUBMIT', '상신', now() - interval '5 day'), + ('KINTEX', 'bd-ah-0036', 'bd-ap-029', 1, 'demo-organizer', '데모 주최자(홍길동)', 'REJECT', '처리', now() - interval '3 day'), + ('KINTEX', 'bd-ah-0037', 'bd-ap-031', 1, 'demo-exhibitor', '데모 참가업체(김참가)', 'SUBMIT', '상신', now() - interval '2 day'), + ('KINTEX', 'bd-ah-0038', 'bd-ap-032', 1, 'demo-contractor', '데모 공사업체(이시공)', 'SUBMIT', '상신', now() - interval '3 day'), + ('KINTEX', 'bd-ah-0039', 'bd-ap-033', 1, 'demo-organizer', '데모 주최자(홍길동)', 'SUBMIT', '상신', now() - interval '4 day'), + ('KINTEX', 'bd-ah-0040', 'bd-ap-033', 1, 'demo-organizer', '데모 주최자(홍길동)', 'APPROVE', '처리', now() - interval '1 day'), + ('KINTEX', 'bd-ah-0041', 'bd-ap-034', 1, 'demo-exhibitor', '데모 참가업체(김참가)', 'SUBMIT', '상신', now() - interval '5 day'), + ('KINTEX', 'bd-ah-0042', 'bd-ap-034', 1, 'demo-organizer', '데모 주최자(홍길동)', 'REJECT', '처리', now() - interval '2 day'), + ('KINTEX', 'bd-ah-0043', 'bd-ap-036', 1, 'demo-organizer', '데모 주최자(홍길동)', 'SUBMIT', '상신', now() - interval '2 day'), + ('KINTEX', 'bd-ah-0044', 'bd-ap-037', 1, 'demo-exhibitor', '데모 참가업체(김참가)', 'SUBMIT', '상신', now() - interval '3 day'), + ('KINTEX', 'bd-ah-0045', 'bd-ap-038', 1, 'demo-contractor', '데모 공사업체(이시공)', 'SUBMIT', '상신', now() - interval '4 day'), + ('KINTEX', 'bd-ah-0046', 'bd-ap-038', 1, 'demo-organizer', '데모 주최자(홍길동)', 'APPROVE', '처리', now() - interval '3 day'), + ('KINTEX', 'bd-ah-0047', 'bd-ap-039', 1, 'demo-organizer', '데모 주최자(홍길동)', 'SUBMIT', '상신', now() - interval '5 day'), + ('KINTEX', 'bd-ah-0048', 'bd-ap-039', 1, 'demo-organizer', '데모 주최자(홍길동)', 'REJECT', '처리', now() - interval '1 day'), + ('KINTEX', 'bd-ah-0049', 'bd-ap-041', 1, 'demo-contractor', '데모 공사업체(이시공)', 'SUBMIT', '상신', now() - interval '2 day'), + ('KINTEX', 'bd-ah-0050', 'bd-ap-042', 1, 'demo-organizer', '데모 주최자(홍길동)', 'SUBMIT', '상신', now() - interval '3 day'), + ('KINTEX', 'bd-ah-0051', 'bd-ap-043', 1, 'demo-exhibitor', '데모 참가업체(김참가)', 'SUBMIT', '상신', now() - interval '4 day'), + ('KINTEX', 'bd-ah-0052', 'bd-ap-043', 1, 'demo-organizer', '데모 주최자(홍길동)', 'APPROVE', '처리', now() - interval '2 day'), + ('KINTEX', 'bd-ah-0053', 'bd-ap-044', 1, 'demo-contractor', '데모 공사업체(이시공)', 'SUBMIT', '상신', now() - interval '5 day'), + ('KINTEX', 'bd-ah-0054', 'bd-ap-044', 1, 'demo-organizer', '데모 주최자(홍길동)', 'REJECT', '처리', now() - interval '3 day'), + ('KINTEX', 'bd-ah-0055', 'bd-ap-046', 1, 'demo-exhibitor', '데모 참가업체(김참가)', 'SUBMIT', '상신', now() - interval '2 day'), + ('KINTEX', 'bd-ah-0056', 'bd-ap-047', 1, 'demo-contractor', '데모 공사업체(이시공)', 'SUBMIT', '상신', now() - interval '3 day'), + ('KINTEX', 'bd-ah-0057', 'bd-ap-048', 1, 'demo-organizer', '데모 주최자(홍길동)', 'SUBMIT', '상신', now() - interval '4 day'), + ('KINTEX', 'bd-ah-0058', 'bd-ap-048', 1, 'demo-organizer', '데모 주최자(홍길동)', 'APPROVE', '처리', now() - interval '1 day'), + ('KINTEX', 'bd-ah-0059', 'bd-ap-049', 1, 'demo-exhibitor', '데모 참가업체(김참가)', 'SUBMIT', '상신', now() - interval '5 day'), + ('KINTEX', 'bd-ah-0060', 'bd-ap-049', 1, 'demo-organizer', '데모 주최자(홍길동)', 'REJECT', '처리', now() - interval '2 day'), + ('KINTEX', 'bd-ah-0061', 'bd-ap-051', 1, 'demo-organizer', '데모 주최자(홍길동)', 'SUBMIT', '상신', now() - interval '2 day'), + ('KINTEX', 'bd-ah-0062', 'bd-ap-052', 1, 'demo-exhibitor', '데모 참가업체(김참가)', 'SUBMIT', '상신', now() - interval '3 day'), + ('KINTEX', 'bd-ah-0063', 'bd-ap-053', 1, 'demo-contractor', '데모 공사업체(이시공)', 'SUBMIT', '상신', now() - interval '4 day'), + ('KINTEX', 'bd-ah-0064', 'bd-ap-053', 1, 'demo-organizer', '데모 주최자(홍길동)', 'APPROVE', '처리', now() - interval '3 day'), + ('KINTEX', 'bd-ah-0065', 'bd-ap-054', 1, 'demo-organizer', '데모 주최자(홍길동)', 'SUBMIT', '상신', now() - interval '5 day'), + ('KINTEX', 'bd-ah-0066', 'bd-ap-054', 1, 'demo-organizer', '데모 주최자(홍길동)', 'REJECT', '처리', now() - interval '1 day'), + ('KINTEX', 'bd-ah-0067', 'bd-ap-056', 1, 'demo-contractor', '데모 공사업체(이시공)', 'SUBMIT', '상신', now() - interval '2 day'), + ('KINTEX', 'bd-ah-0068', 'bd-ap-057', 1, 'demo-organizer', '데모 주최자(홍길동)', 'SUBMIT', '상신', now() - interval '3 day'), + ('KINTEX', 'bd-ah-0069', 'bd-ap-058', 1, 'demo-exhibitor', '데모 참가업체(김참가)', 'SUBMIT', '상신', now() - interval '4 day'), + ('KINTEX', 'bd-ah-0070', 'bd-ap-058', 1, 'demo-organizer', '데모 주최자(홍길동)', 'APPROVE', '처리', now() - interval '2 day'), + ('KINTEX', 'bd-ah-0071', 'bd-ap-059', 1, 'demo-contractor', '데모 공사업체(이시공)', 'SUBMIT', '상신', now() - interval '5 day'), + ('KINTEX', 'bd-ah-0072', 'bd-ap-059', 1, 'demo-organizer', '데모 주최자(홍길동)', 'REJECT', '처리', now() - interval '3 day') +ON CONFLICT (tenant_id, id) DO NOTHING; + +-- 9. 공지(notice) 60 — 행사/전역 혼합. +INSERT INTO notice (id, event_id, category, title, content, pinned, author_id, author_name, view_count, published_at) VALUES + ('bd-nt-001', 'bd-e-002', 'EVENT', '[BD] 데모 공지 1', '대량 데모 공지 본문 1.', false, 'demo-organizer', '데모 주최자(홍길동)', 13, now() - interval '2 day'), + ('bd-nt-002', 'bd-e-003', 'GENERAL', '[BD] 데모 공지 2', '대량 데모 공지 본문 2.', false, 'demo-organizer', '데모 주최자(홍길동)', 26, now() - interval '3 day'), + ('bd-nt-003', 'bd-e-004', 'SYSTEM', '[BD] 데모 공지 3', '대량 데모 공지 본문 3.', false, 'demo-organizer', '데모 주최자(홍길동)', 39, now() - interval '4 day'), + ('bd-nt-004', NULL, 'EVENT', '[BD] 데모 공지 4', '대량 데모 공지 본문 4.', false, 'demo-organizer', '데모 주최자(홍길동)', 52, now() - interval '5 day'), + ('bd-nt-005', 'bd-e-006', 'GENERAL', '[BD] 데모 공지 5', '대량 데모 공지 본문 5.', false, 'demo-organizer', '데모 주최자(홍길동)', 65, now() - interval '6 day'), + ('bd-nt-006', 'bd-e-007', 'SYSTEM', '[BD] 데모 공지 6', '대량 데모 공지 본문 6.', false, 'demo-organizer', '데모 주최자(홍길동)', 78, now() - interval '7 day'), + ('bd-nt-007', 'bd-e-008', 'EVENT', '[BD] 데모 공지 7', '대량 데모 공지 본문 7.', true, 'demo-organizer', '데모 주최자(홍길동)', 91, now() - interval '8 day'), + ('bd-nt-008', NULL, 'GENERAL', '[BD] 데모 공지 8', '대량 데모 공지 본문 8.', false, 'demo-organizer', '데모 주최자(홍길동)', 104, now() - interval '9 day'), + ('bd-nt-009', 'bd-e-010', 'SYSTEM', '[BD] 데모 공지 9', '대량 데모 공지 본문 9.', false, 'demo-organizer', '데모 주최자(홍길동)', 117, now() - interval '10 day'), + ('bd-nt-010', 'bd-e-011', 'EVENT', '[BD] 데모 공지 10', '대량 데모 공지 본문 10.', false, 'demo-organizer', '데모 주최자(홍길동)', 130, now() - interval '11 day'), + ('bd-nt-011', 'bd-e-012', 'GENERAL', '[BD] 데모 공지 11', '대량 데모 공지 본문 11.', false, 'demo-organizer', '데모 주최자(홍길동)', 143, now() - interval '12 day'), + ('bd-nt-012', NULL, 'SYSTEM', '[BD] 데모 공지 12', '대량 데모 공지 본문 12.', false, 'demo-organizer', '데모 주최자(홍길동)', 156, now() - interval '13 day'), + ('bd-nt-013', 'bd-e-014', 'EVENT', '[BD] 데모 공지 13', '대량 데모 공지 본문 13.', false, 'demo-organizer', '데모 주최자(홍길동)', 169, now() - interval '14 day'), + ('bd-nt-014', 'bd-e-015', 'GENERAL', '[BD] 데모 공지 14', '대량 데모 공지 본문 14.', true, 'demo-organizer', '데모 주최자(홍길동)', 182, now() - interval '15 day'), + ('bd-nt-015', 'bd-e-016', 'SYSTEM', '[BD] 데모 공지 15', '대량 데모 공지 본문 15.', false, 'demo-organizer', '데모 주최자(홍길동)', 195, now() - interval '16 day'), + ('bd-nt-016', NULL, 'EVENT', '[BD] 데모 공지 16', '대량 데모 공지 본문 16.', false, 'demo-organizer', '데모 주최자(홍길동)', 208, now() - interval '17 day'), + ('bd-nt-017', 'bd-e-018', 'GENERAL', '[BD] 데모 공지 17', '대량 데모 공지 본문 17.', false, 'demo-organizer', '데모 주최자(홍길동)', 221, now() - interval '18 day'), + ('bd-nt-018', 'bd-e-019', 'SYSTEM', '[BD] 데모 공지 18', '대량 데모 공지 본문 18.', false, 'demo-organizer', '데모 주최자(홍길동)', 234, now() - interval '19 day'), + ('bd-nt-019', 'bd-e-020', 'EVENT', '[BD] 데모 공지 19', '대량 데모 공지 본문 19.', false, 'demo-organizer', '데모 주최자(홍길동)', 247, now() - interval '20 day'), + ('bd-nt-020', NULL, 'GENERAL', '[BD] 데모 공지 20', '대량 데모 공지 본문 20.', false, 'demo-organizer', '데모 주최자(홍길동)', 260, now() - interval '1 day'), + ('bd-nt-021', 'bd-e-022', 'SYSTEM', '[BD] 데모 공지 21', '대량 데모 공지 본문 21.', true, 'demo-organizer', '데모 주최자(홍길동)', 273, now() - interval '2 day'), + ('bd-nt-022', 'bd-e-023', 'EVENT', '[BD] 데모 공지 22', '대량 데모 공지 본문 22.', false, 'demo-organizer', '데모 주최자(홍길동)', 286, now() - interval '3 day'), + ('bd-nt-023', 'bd-e-024', 'GENERAL', '[BD] 데모 공지 23', '대량 데모 공지 본문 23.', false, 'demo-organizer', '데모 주최자(홍길동)', 299, now() - interval '4 day'), + ('bd-nt-024', NULL, 'SYSTEM', '[BD] 데모 공지 24', '대량 데모 공지 본문 24.', false, 'demo-organizer', '데모 주최자(홍길동)', 312, now() - interval '5 day'), + ('bd-nt-025', 'bd-e-026', 'EVENT', '[BD] 데모 공지 25', '대량 데모 공지 본문 25.', false, 'demo-organizer', '데모 주최자(홍길동)', 325, now() - interval '6 day'), + ('bd-nt-026', 'bd-e-027', 'GENERAL', '[BD] 데모 공지 26', '대량 데모 공지 본문 26.', false, 'demo-organizer', '데모 주최자(홍길동)', 338, now() - interval '7 day'), + ('bd-nt-027', 'bd-e-028', 'SYSTEM', '[BD] 데모 공지 27', '대량 데모 공지 본문 27.', false, 'demo-organizer', '데모 주최자(홍길동)', 351, now() - interval '8 day'), + ('bd-nt-028', NULL, 'EVENT', '[BD] 데모 공지 28', '대량 데모 공지 본문 28.', true, 'demo-organizer', '데모 주최자(홍길동)', 364, now() - interval '9 day'), + ('bd-nt-029', 'bd-e-030', 'GENERAL', '[BD] 데모 공지 29', '대량 데모 공지 본문 29.', false, 'demo-organizer', '데모 주최자(홍길동)', 377, now() - interval '10 day'), + ('bd-nt-030', 'bd-e-031', 'SYSTEM', '[BD] 데모 공지 30', '대량 데모 공지 본문 30.', false, 'demo-organizer', '데모 주최자(홍길동)', 390, now() - interval '11 day'), + ('bd-nt-031', 'bd-e-032', 'EVENT', '[BD] 데모 공지 31', '대량 데모 공지 본문 31.', false, 'demo-organizer', '데모 주최자(홍길동)', 403, now() - interval '12 day'), + ('bd-nt-032', NULL, 'GENERAL', '[BD] 데모 공지 32', '대량 데모 공지 본문 32.', false, 'demo-organizer', '데모 주최자(홍길동)', 416, now() - interval '13 day'), + ('bd-nt-033', 'bd-e-034', 'SYSTEM', '[BD] 데모 공지 33', '대량 데모 공지 본문 33.', false, 'demo-organizer', '데모 주최자(홍길동)', 429, now() - interval '14 day'), + ('bd-nt-034', 'bd-e-035', 'EVENT', '[BD] 데모 공지 34', '대량 데모 공지 본문 34.', false, 'demo-organizer', '데모 주최자(홍길동)', 442, now() - interval '15 day'), + ('bd-nt-035', 'bd-e-036', 'GENERAL', '[BD] 데모 공지 35', '대량 데모 공지 본문 35.', true, 'demo-organizer', '데모 주최자(홍길동)', 455, now() - interval '16 day'), + ('bd-nt-036', NULL, 'SYSTEM', '[BD] 데모 공지 36', '대량 데모 공지 본문 36.', false, 'demo-organizer', '데모 주최자(홍길동)', 468, now() - interval '17 day'), + ('bd-nt-037', 'bd-e-038', 'EVENT', '[BD] 데모 공지 37', '대량 데모 공지 본문 37.', false, 'demo-organizer', '데모 주최자(홍길동)', 481, now() - interval '18 day'), + ('bd-nt-038', 'bd-e-039', 'GENERAL', '[BD] 데모 공지 38', '대량 데모 공지 본문 38.', false, 'demo-organizer', '데모 주최자(홍길동)', 494, now() - interval '19 day'), + ('bd-nt-039', 'bd-e-040', 'SYSTEM', '[BD] 데모 공지 39', '대량 데모 공지 본문 39.', false, 'demo-organizer', '데모 주최자(홍길동)', 7, now() - interval '20 day'), + ('bd-nt-040', NULL, 'EVENT', '[BD] 데모 공지 40', '대량 데모 공지 본문 40.', false, 'demo-organizer', '데모 주최자(홍길동)', 20, now() - interval '1 day'), + ('bd-nt-041', 'bd-e-042', 'GENERAL', '[BD] 데모 공지 41', '대량 데모 공지 본문 41.', false, 'demo-organizer', '데모 주최자(홍길동)', 33, now() - interval '2 day'), + ('bd-nt-042', 'bd-e-043', 'SYSTEM', '[BD] 데모 공지 42', '대량 데모 공지 본문 42.', true, 'demo-organizer', '데모 주최자(홍길동)', 46, now() - interval '3 day'), + ('bd-nt-043', 'bd-e-044', 'EVENT', '[BD] 데모 공지 43', '대량 데모 공지 본문 43.', false, 'demo-organizer', '데모 주최자(홍길동)', 59, now() - interval '4 day'), + ('bd-nt-044', NULL, 'GENERAL', '[BD] 데모 공지 44', '대량 데모 공지 본문 44.', false, 'demo-organizer', '데모 주최자(홍길동)', 72, now() - interval '5 day'), + ('bd-nt-045', 'bd-e-046', 'SYSTEM', '[BD] 데모 공지 45', '대량 데모 공지 본문 45.', false, 'demo-organizer', '데모 주최자(홍길동)', 85, now() - interval '6 day'), + ('bd-nt-046', 'bd-e-047', 'EVENT', '[BD] 데모 공지 46', '대량 데모 공지 본문 46.', false, 'demo-organizer', '데모 주최자(홍길동)', 98, now() - interval '7 day'), + ('bd-nt-047', 'bd-e-048', 'GENERAL', '[BD] 데모 공지 47', '대량 데모 공지 본문 47.', false, 'demo-organizer', '데모 주최자(홍길동)', 111, now() - interval '8 day'), + ('bd-nt-048', NULL, 'SYSTEM', '[BD] 데모 공지 48', '대량 데모 공지 본문 48.', false, 'demo-organizer', '데모 주최자(홍길동)', 124, now() - interval '9 day'), + ('bd-nt-049', 'bd-e-050', 'EVENT', '[BD] 데모 공지 49', '대량 데모 공지 본문 49.', true, 'demo-organizer', '데모 주최자(홍길동)', 137, now() - interval '10 day'), + ('bd-nt-050', 'bd-e-051', 'GENERAL', '[BD] 데모 공지 50', '대량 데모 공지 본문 50.', false, 'demo-organizer', '데모 주최자(홍길동)', 150, now() - interval '11 day'), + ('bd-nt-051', 'bd-e-052', 'SYSTEM', '[BD] 데모 공지 51', '대량 데모 공지 본문 51.', false, 'demo-organizer', '데모 주최자(홍길동)', 163, now() - interval '12 day'), + ('bd-nt-052', NULL, 'EVENT', '[BD] 데모 공지 52', '대량 데모 공지 본문 52.', false, 'demo-organizer', '데모 주최자(홍길동)', 176, now() - interval '13 day'), + ('bd-nt-053', 'bd-e-054', 'GENERAL', '[BD] 데모 공지 53', '대량 데모 공지 본문 53.', false, 'demo-organizer', '데모 주최자(홍길동)', 189, now() - interval '14 day'), + ('bd-nt-054', 'bd-e-055', 'SYSTEM', '[BD] 데모 공지 54', '대량 데모 공지 본문 54.', false, 'demo-organizer', '데모 주최자(홍길동)', 202, now() - interval '15 day'), + ('bd-nt-055', 'bd-e-056', 'EVENT', '[BD] 데모 공지 55', '대량 데모 공지 본문 55.', false, 'demo-organizer', '데모 주최자(홍길동)', 215, now() - interval '16 day'), + ('bd-nt-056', NULL, 'GENERAL', '[BD] 데모 공지 56', '대량 데모 공지 본문 56.', true, 'demo-organizer', '데모 주최자(홍길동)', 228, now() - interval '17 day'), + ('bd-nt-057', 'bd-e-058', 'SYSTEM', '[BD] 데모 공지 57', '대량 데모 공지 본문 57.', false, 'demo-organizer', '데모 주최자(홍길동)', 241, now() - interval '18 day'), + ('bd-nt-058', 'bd-e-059', 'EVENT', '[BD] 데모 공지 58', '대량 데모 공지 본문 58.', false, 'demo-organizer', '데모 주최자(홍길동)', 254, now() - interval '19 day'), + ('bd-nt-059', 'bd-e-060', 'GENERAL', '[BD] 데모 공지 59', '대량 데모 공지 본문 59.', false, 'demo-organizer', '데모 주최자(홍길동)', 267, now() - interval '20 day'), + ('bd-nt-060', NULL, 'SYSTEM', '[BD] 데모 공지 60', '대량 데모 공지 본문 60.', false, 'demo-organizer', '데모 주최자(홍길동)', 280, now() - interval '1 day') +ON CONFLICT (id) DO NOTHING; + +-- 10. 알림(notification) 80 — 데모 계정 수신. +INSERT INTO notification (id, recipient_id, event_id, noti_type, title, message, link, read_at) VALUES + ('bd-no-001', 'demo-exhibitor', 'bd-e-002', 'APPROVAL', 'BD 알림 1', '대량 데모 알림 메시지 1.', '/notifications', NULL), + ('bd-no-002', 'demo-contractor', 'bd-e-003', 'DEADLINE', 'BD 알림 2', '대량 데모 알림 메시지 2.', '/notifications', NULL), + ('bd-no-003', 'demo-visitor', 'bd-e-004', 'MESSAGE', 'BD 알림 3', '대량 데모 알림 메시지 3.', '/notifications', now() - interval '4 day'), + ('bd-no-004', 'demo-organizer', 'bd-e-005', 'SYSTEM', 'BD 알림 4', '대량 데모 알림 메시지 4.', '/notifications', NULL), + ('bd-no-005', 'demo-exhibitor', 'bd-e-006', 'APPROVAL', 'BD 알림 5', '대량 데모 알림 메시지 5.', '/notifications', NULL), + ('bd-no-006', 'demo-contractor', 'bd-e-007', 'DEADLINE', 'BD 알림 6', '대량 데모 알림 메시지 6.', '/notifications', now() - interval '2 day'), + ('bd-no-007', 'demo-visitor', 'bd-e-008', 'MESSAGE', 'BD 알림 7', '대량 데모 알림 메시지 7.', '/notifications', NULL), + ('bd-no-008', 'demo-organizer', 'bd-e-009', 'SYSTEM', 'BD 알림 8', '대량 데모 알림 메시지 8.', '/notifications', NULL), + ('bd-no-009', 'demo-exhibitor', 'bd-e-010', 'APPROVAL', 'BD 알림 9', '대량 데모 알림 메시지 9.', '/notifications', now() - interval '5 day'), + ('bd-no-010', 'demo-contractor', 'bd-e-011', 'DEADLINE', 'BD 알림 10', '대량 데모 알림 메시지 10.', '/notifications', NULL), + ('bd-no-011', 'demo-visitor', 'bd-e-012', 'MESSAGE', 'BD 알림 11', '대량 데모 알림 메시지 11.', '/notifications', NULL), + ('bd-no-012', 'demo-organizer', 'bd-e-013', 'SYSTEM', 'BD 알림 12', '대량 데모 알림 메시지 12.', '/notifications', now() - interval '3 day'), + ('bd-no-013', 'demo-exhibitor', 'bd-e-014', 'APPROVAL', 'BD 알림 13', '대량 데모 알림 메시지 13.', '/notifications', NULL), + ('bd-no-014', 'demo-contractor', 'bd-e-015', 'DEADLINE', 'BD 알림 14', '대량 데모 알림 메시지 14.', '/notifications', NULL), + ('bd-no-015', 'demo-visitor', 'bd-e-016', 'MESSAGE', 'BD 알림 15', '대량 데모 알림 메시지 15.', '/notifications', now() - interval '1 day'), + ('bd-no-016', 'demo-organizer', 'bd-e-017', 'SYSTEM', 'BD 알림 16', '대량 데모 알림 메시지 16.', '/notifications', NULL), + ('bd-no-017', 'demo-exhibitor', 'bd-e-018', 'APPROVAL', 'BD 알림 17', '대량 데모 알림 메시지 17.', '/notifications', NULL), + ('bd-no-018', 'demo-contractor', 'bd-e-019', 'DEADLINE', 'BD 알림 18', '대량 데모 알림 메시지 18.', '/notifications', now() - interval '4 day'), + ('bd-no-019', 'demo-visitor', 'bd-e-020', 'MESSAGE', 'BD 알림 19', '대량 데모 알림 메시지 19.', '/notifications', NULL), + ('bd-no-020', 'demo-organizer', 'bd-e-021', 'SYSTEM', 'BD 알림 20', '대량 데모 알림 메시지 20.', '/notifications', NULL), + ('bd-no-021', 'demo-exhibitor', 'bd-e-022', 'APPROVAL', 'BD 알림 21', '대량 데모 알림 메시지 21.', '/notifications', now() - interval '2 day'), + ('bd-no-022', 'demo-contractor', 'bd-e-023', 'DEADLINE', 'BD 알림 22', '대량 데모 알림 메시지 22.', '/notifications', NULL), + ('bd-no-023', 'demo-visitor', 'bd-e-024', 'MESSAGE', 'BD 알림 23', '대량 데모 알림 메시지 23.', '/notifications', NULL), + ('bd-no-024', 'demo-organizer', 'bd-e-025', 'SYSTEM', 'BD 알림 24', '대량 데모 알림 메시지 24.', '/notifications', now() - interval '5 day'), + ('bd-no-025', 'demo-exhibitor', 'bd-e-026', 'APPROVAL', 'BD 알림 25', '대량 데모 알림 메시지 25.', '/notifications', NULL), + ('bd-no-026', 'demo-contractor', 'bd-e-027', 'DEADLINE', 'BD 알림 26', '대량 데모 알림 메시지 26.', '/notifications', NULL), + ('bd-no-027', 'demo-visitor', 'bd-e-028', 'MESSAGE', 'BD 알림 27', '대량 데모 알림 메시지 27.', '/notifications', now() - interval '3 day'), + ('bd-no-028', 'demo-organizer', 'bd-e-029', 'SYSTEM', 'BD 알림 28', '대량 데모 알림 메시지 28.', '/notifications', NULL), + ('bd-no-029', 'demo-exhibitor', 'bd-e-030', 'APPROVAL', 'BD 알림 29', '대량 데모 알림 메시지 29.', '/notifications', NULL), + ('bd-no-030', 'demo-contractor', 'bd-e-031', 'DEADLINE', 'BD 알림 30', '대량 데모 알림 메시지 30.', '/notifications', now() - interval '1 day'), + ('bd-no-031', 'demo-visitor', 'bd-e-032', 'MESSAGE', 'BD 알림 31', '대량 데모 알림 메시지 31.', '/notifications', NULL), + ('bd-no-032', 'demo-organizer', 'bd-e-033', 'SYSTEM', 'BD 알림 32', '대량 데모 알림 메시지 32.', '/notifications', NULL), + ('bd-no-033', 'demo-exhibitor', 'bd-e-034', 'APPROVAL', 'BD 알림 33', '대량 데모 알림 메시지 33.', '/notifications', now() - interval '4 day'), + ('bd-no-034', 'demo-contractor', 'bd-e-035', 'DEADLINE', 'BD 알림 34', '대량 데모 알림 메시지 34.', '/notifications', NULL), + ('bd-no-035', 'demo-visitor', 'bd-e-036', 'MESSAGE', 'BD 알림 35', '대량 데모 알림 메시지 35.', '/notifications', NULL), + ('bd-no-036', 'demo-organizer', 'bd-e-037', 'SYSTEM', 'BD 알림 36', '대량 데모 알림 메시지 36.', '/notifications', now() - interval '2 day'), + ('bd-no-037', 'demo-exhibitor', 'bd-e-038', 'APPROVAL', 'BD 알림 37', '대량 데모 알림 메시지 37.', '/notifications', NULL), + ('bd-no-038', 'demo-contractor', 'bd-e-039', 'DEADLINE', 'BD 알림 38', '대량 데모 알림 메시지 38.', '/notifications', NULL), + ('bd-no-039', 'demo-visitor', 'bd-e-040', 'MESSAGE', 'BD 알림 39', '대량 데모 알림 메시지 39.', '/notifications', now() - interval '5 day'), + ('bd-no-040', 'demo-organizer', 'bd-e-041', 'SYSTEM', 'BD 알림 40', '대량 데모 알림 메시지 40.', '/notifications', NULL), + ('bd-no-041', 'demo-exhibitor', 'bd-e-042', 'APPROVAL', 'BD 알림 41', '대량 데모 알림 메시지 41.', '/notifications', NULL), + ('bd-no-042', 'demo-contractor', 'bd-e-043', 'DEADLINE', 'BD 알림 42', '대량 데모 알림 메시지 42.', '/notifications', now() - interval '3 day'), + ('bd-no-043', 'demo-visitor', 'bd-e-044', 'MESSAGE', 'BD 알림 43', '대량 데모 알림 메시지 43.', '/notifications', NULL), + ('bd-no-044', 'demo-organizer', 'bd-e-045', 'SYSTEM', 'BD 알림 44', '대량 데모 알림 메시지 44.', '/notifications', NULL), + ('bd-no-045', 'demo-exhibitor', 'bd-e-046', 'APPROVAL', 'BD 알림 45', '대량 데모 알림 메시지 45.', '/notifications', now() - interval '1 day'), + ('bd-no-046', 'demo-contractor', 'bd-e-047', 'DEADLINE', 'BD 알림 46', '대량 데모 알림 메시지 46.', '/notifications', NULL), + ('bd-no-047', 'demo-visitor', 'bd-e-048', 'MESSAGE', 'BD 알림 47', '대량 데모 알림 메시지 47.', '/notifications', NULL), + ('bd-no-048', 'demo-organizer', 'bd-e-049', 'SYSTEM', 'BD 알림 48', '대량 데모 알림 메시지 48.', '/notifications', now() - interval '4 day'), + ('bd-no-049', 'demo-exhibitor', 'bd-e-050', 'APPROVAL', 'BD 알림 49', '대량 데모 알림 메시지 49.', '/notifications', NULL), + ('bd-no-050', 'demo-contractor', 'bd-e-051', 'DEADLINE', 'BD 알림 50', '대량 데모 알림 메시지 50.', '/notifications', NULL), + ('bd-no-051', 'demo-visitor', 'bd-e-052', 'MESSAGE', 'BD 알림 51', '대량 데모 알림 메시지 51.', '/notifications', now() - interval '2 day'), + ('bd-no-052', 'demo-organizer', 'bd-e-053', 'SYSTEM', 'BD 알림 52', '대량 데모 알림 메시지 52.', '/notifications', NULL), + ('bd-no-053', 'demo-exhibitor', 'bd-e-054', 'APPROVAL', 'BD 알림 53', '대량 데모 알림 메시지 53.', '/notifications', NULL), + ('bd-no-054', 'demo-contractor', 'bd-e-055', 'DEADLINE', 'BD 알림 54', '대량 데모 알림 메시지 54.', '/notifications', now() - interval '5 day'), + ('bd-no-055', 'demo-visitor', 'bd-e-056', 'MESSAGE', 'BD 알림 55', '대량 데모 알림 메시지 55.', '/notifications', NULL), + ('bd-no-056', 'demo-organizer', 'bd-e-057', 'SYSTEM', 'BD 알림 56', '대량 데모 알림 메시지 56.', '/notifications', NULL), + ('bd-no-057', 'demo-exhibitor', 'bd-e-058', 'APPROVAL', 'BD 알림 57', '대량 데모 알림 메시지 57.', '/notifications', now() - interval '3 day'), + ('bd-no-058', 'demo-contractor', 'bd-e-059', 'DEADLINE', 'BD 알림 58', '대량 데모 알림 메시지 58.', '/notifications', NULL), + ('bd-no-059', 'demo-visitor', 'bd-e-060', 'MESSAGE', 'BD 알림 59', '대량 데모 알림 메시지 59.', '/notifications', NULL), + ('bd-no-060', 'demo-organizer', 'bd-e-001', 'SYSTEM', 'BD 알림 60', '대량 데모 알림 메시지 60.', '/notifications', now() - interval '1 day'), + ('bd-no-061', 'demo-exhibitor', 'bd-e-002', 'APPROVAL', 'BD 알림 61', '대량 데모 알림 메시지 61.', '/notifications', NULL), + ('bd-no-062', 'demo-contractor', 'bd-e-003', 'DEADLINE', 'BD 알림 62', '대량 데모 알림 메시지 62.', '/notifications', NULL), + ('bd-no-063', 'demo-visitor', 'bd-e-004', 'MESSAGE', 'BD 알림 63', '대량 데모 알림 메시지 63.', '/notifications', now() - interval '4 day'), + ('bd-no-064', 'demo-organizer', 'bd-e-005', 'SYSTEM', 'BD 알림 64', '대량 데모 알림 메시지 64.', '/notifications', NULL), + ('bd-no-065', 'demo-exhibitor', 'bd-e-006', 'APPROVAL', 'BD 알림 65', '대량 데모 알림 메시지 65.', '/notifications', NULL), + ('bd-no-066', 'demo-contractor', 'bd-e-007', 'DEADLINE', 'BD 알림 66', '대량 데모 알림 메시지 66.', '/notifications', now() - interval '2 day'), + ('bd-no-067', 'demo-visitor', 'bd-e-008', 'MESSAGE', 'BD 알림 67', '대량 데모 알림 메시지 67.', '/notifications', NULL), + ('bd-no-068', 'demo-organizer', 'bd-e-009', 'SYSTEM', 'BD 알림 68', '대량 데모 알림 메시지 68.', '/notifications', NULL), + ('bd-no-069', 'demo-exhibitor', 'bd-e-010', 'APPROVAL', 'BD 알림 69', '대량 데모 알림 메시지 69.', '/notifications', now() - interval '5 day'), + ('bd-no-070', 'demo-contractor', 'bd-e-011', 'DEADLINE', 'BD 알림 70', '대량 데모 알림 메시지 70.', '/notifications', NULL), + ('bd-no-071', 'demo-visitor', 'bd-e-012', 'MESSAGE', 'BD 알림 71', '대량 데모 알림 메시지 71.', '/notifications', NULL), + ('bd-no-072', 'demo-organizer', 'bd-e-013', 'SYSTEM', 'BD 알림 72', '대량 데모 알림 메시지 72.', '/notifications', now() - interval '3 day'), + ('bd-no-073', 'demo-exhibitor', 'bd-e-014', 'APPROVAL', 'BD 알림 73', '대량 데모 알림 메시지 73.', '/notifications', NULL), + ('bd-no-074', 'demo-contractor', 'bd-e-015', 'DEADLINE', 'BD 알림 74', '대량 데모 알림 메시지 74.', '/notifications', NULL), + ('bd-no-075', 'demo-visitor', 'bd-e-016', 'MESSAGE', 'BD 알림 75', '대량 데모 알림 메시지 75.', '/notifications', now() - interval '1 day'), + ('bd-no-076', 'demo-organizer', 'bd-e-017', 'SYSTEM', 'BD 알림 76', '대량 데모 알림 메시지 76.', '/notifications', NULL), + ('bd-no-077', 'demo-exhibitor', 'bd-e-018', 'APPROVAL', 'BD 알림 77', '대량 데모 알림 메시지 77.', '/notifications', NULL), + ('bd-no-078', 'demo-contractor', 'bd-e-019', 'DEADLINE', 'BD 알림 78', '대량 데모 알림 메시지 78.', '/notifications', now() - interval '4 day'), + ('bd-no-079', 'demo-visitor', 'bd-e-020', 'MESSAGE', 'BD 알림 79', '대량 데모 알림 메시지 79.', '/notifications', NULL), + ('bd-no-080', 'demo-organizer', 'bd-e-021', 'SYSTEM', 'BD 알림 80', '대량 데모 알림 메시지 80.', '/notifications', NULL) +ON CONFLICT (id) DO NOTHING; + +-- 11. 업무일지(worklog) 80. +INSERT INTO worklog (id, event_id, writer_id, writer_name, dept_id, work_date, title, work_type, progress, status, content, hours) VALUES + ('bd-wl-001', 'bd-e-002', 'demo-exhibitor', '데모 참가업체(김참가)', 'd-sales', CURRENT_DATE - 1, 'BD 업무일지 1', 'DESIGN', 'P25', 'DOING', '대량 데모 업무 내용 1.', 1.5), + ('bd-wl-002', 'bd-e-003', 'demo-contractor', '데모 공사업체(이시공)', 'd-mkt', CURRENT_DATE - 2, 'BD 업무일지 2', 'SALES', 'P50', 'DONE', '대량 데모 업무 내용 2.', 2.0), + ('bd-wl-003', 'bd-e-004', 'demo-organizer', '데모 주최자(홍길동)', 'd-fin', CURRENT_DATE - 3, 'BD 업무일지 3', 'SUPPORT', 'P75', 'TODO', '대량 데모 업무 내용 3.', 2.5), + ('bd-wl-004', 'bd-e-005', 'demo-exhibitor', '데모 참가업체(김참가)', 'd-it-ops', CURRENT_DATE - 4, 'BD 업무일지 4', 'OPERATION', 'P100', 'DOING', '대량 데모 업무 내용 4.', 3.0), + ('bd-wl-005', 'bd-e-006', 'demo-contractor', '데모 공사업체(이시공)', 'd-hall', CURRENT_DATE - 5, 'BD 업무일지 5', 'DESIGN', 'P0', 'DONE', '대량 데모 업무 내용 5.', 3.5), + ('bd-wl-006', 'bd-e-007', 'demo-organizer', '데모 주최자(홍길동)', 'd-sales', CURRENT_DATE - 6, 'BD 업무일지 6', 'SALES', 'P25', 'TODO', '대량 데모 업무 내용 6.', 4.0), + ('bd-wl-007', 'bd-e-008', 'demo-exhibitor', '데모 참가업체(김참가)', 'd-mkt', CURRENT_DATE - 7, 'BD 업무일지 7', 'SUPPORT', 'P50', 'DOING', '대량 데모 업무 내용 7.', 4.5), + ('bd-wl-008', 'bd-e-009', 'demo-contractor', '데모 공사업체(이시공)', 'd-fin', CURRENT_DATE - 8, 'BD 업무일지 8', 'OPERATION', 'P75', 'DONE', '대량 데모 업무 내용 8.', 1.0), + ('bd-wl-009', 'bd-e-010', 'demo-organizer', '데모 주최자(홍길동)', 'd-it-ops', CURRENT_DATE - 9, 'BD 업무일지 9', 'DESIGN', 'P100', 'TODO', '대량 데모 업무 내용 9.', 1.5), + ('bd-wl-010', 'bd-e-011', 'demo-exhibitor', '데모 참가업체(김참가)', 'd-hall', CURRENT_DATE - 10, 'BD 업무일지 10', 'SALES', 'P0', 'DOING', '대량 데모 업무 내용 10.', 2.0), + ('bd-wl-011', 'bd-e-012', 'demo-contractor', '데모 공사업체(이시공)', 'd-sales', CURRENT_DATE - 11, 'BD 업무일지 11', 'SUPPORT', 'P25', 'DONE', '대량 데모 업무 내용 11.', 2.5), + ('bd-wl-012', 'bd-e-013', 'demo-organizer', '데모 주최자(홍길동)', 'd-mkt', CURRENT_DATE - 12, 'BD 업무일지 12', 'OPERATION', 'P50', 'TODO', '대량 데모 업무 내용 12.', 3.0), + ('bd-wl-013', 'bd-e-014', 'demo-exhibitor', '데모 참가업체(김참가)', 'd-fin', CURRENT_DATE - 13, 'BD 업무일지 13', 'DESIGN', 'P75', 'DOING', '대량 데모 업무 내용 13.', 3.5), + ('bd-wl-014', 'bd-e-015', 'demo-contractor', '데모 공사업체(이시공)', 'd-it-ops', CURRENT_DATE - 14, 'BD 업무일지 14', 'SALES', 'P100', 'DONE', '대량 데모 업무 내용 14.', 4.0), + ('bd-wl-015', 'bd-e-016', 'demo-organizer', '데모 주최자(홍길동)', 'd-hall', CURRENT_DATE - 15, 'BD 업무일지 15', 'SUPPORT', 'P0', 'TODO', '대량 데모 업무 내용 15.', 4.5), + ('bd-wl-016', 'bd-e-017', 'demo-exhibitor', '데모 참가업체(김참가)', 'd-sales', CURRENT_DATE - 16, 'BD 업무일지 16', 'OPERATION', 'P25', 'DOING', '대량 데모 업무 내용 16.', 1.0), + ('bd-wl-017', 'bd-e-018', 'demo-contractor', '데모 공사업체(이시공)', 'd-mkt', CURRENT_DATE - 17, 'BD 업무일지 17', 'DESIGN', 'P50', 'DONE', '대량 데모 업무 내용 17.', 1.5), + ('bd-wl-018', 'bd-e-019', 'demo-organizer', '데모 주최자(홍길동)', 'd-fin', CURRENT_DATE - 18, 'BD 업무일지 18', 'SALES', 'P75', 'TODO', '대량 데모 업무 내용 18.', 2.0), + ('bd-wl-019', 'bd-e-020', 'demo-exhibitor', '데모 참가업체(김참가)', 'd-it-ops', CURRENT_DATE - 19, 'BD 업무일지 19', 'SUPPORT', 'P100', 'DOING', '대량 데모 업무 내용 19.', 2.5), + ('bd-wl-020', 'bd-e-021', 'demo-contractor', '데모 공사업체(이시공)', 'd-hall', CURRENT_DATE - 20, 'BD 업무일지 20', 'OPERATION', 'P0', 'DONE', '대량 데모 업무 내용 20.', 3.0), + ('bd-wl-021', 'bd-e-022', 'demo-organizer', '데모 주최자(홍길동)', 'd-sales', CURRENT_DATE - 21, 'BD 업무일지 21', 'DESIGN', 'P25', 'TODO', '대량 데모 업무 내용 21.', 3.5), + ('bd-wl-022', 'bd-e-023', 'demo-exhibitor', '데모 참가업체(김참가)', 'd-mkt', CURRENT_DATE - 22, 'BD 업무일지 22', 'SALES', 'P50', 'DOING', '대량 데모 업무 내용 22.', 4.0), + ('bd-wl-023', 'bd-e-024', 'demo-contractor', '데모 공사업체(이시공)', 'd-fin', CURRENT_DATE - 23, 'BD 업무일지 23', 'SUPPORT', 'P75', 'DONE', '대량 데모 업무 내용 23.', 4.5), + ('bd-wl-024', 'bd-e-025', 'demo-organizer', '데모 주최자(홍길동)', 'd-it-ops', CURRENT_DATE - 24, 'BD 업무일지 24', 'OPERATION', 'P100', 'TODO', '대량 데모 업무 내용 24.', 1.0), + ('bd-wl-025', 'bd-e-026', 'demo-exhibitor', '데모 참가업체(김참가)', 'd-hall', CURRENT_DATE - 25, 'BD 업무일지 25', 'DESIGN', 'P0', 'DOING', '대량 데모 업무 내용 25.', 1.5), + ('bd-wl-026', 'bd-e-027', 'demo-contractor', '데모 공사업체(이시공)', 'd-sales', CURRENT_DATE - 26, 'BD 업무일지 26', 'SALES', 'P25', 'DONE', '대량 데모 업무 내용 26.', 2.0), + ('bd-wl-027', 'bd-e-028', 'demo-organizer', '데모 주최자(홍길동)', 'd-mkt', CURRENT_DATE - 27, 'BD 업무일지 27', 'SUPPORT', 'P50', 'TODO', '대량 데모 업무 내용 27.', 2.5), + ('bd-wl-028', 'bd-e-029', 'demo-exhibitor', '데모 참가업체(김참가)', 'd-fin', CURRENT_DATE - 28, 'BD 업무일지 28', 'OPERATION', 'P75', 'DOING', '대량 데모 업무 내용 28.', 3.0), + ('bd-wl-029', 'bd-e-030', 'demo-contractor', '데모 공사업체(이시공)', 'd-it-ops', CURRENT_DATE - 29, 'BD 업무일지 29', 'DESIGN', 'P100', 'DONE', '대량 데모 업무 내용 29.', 3.5), + ('bd-wl-030', 'bd-e-031', 'demo-organizer', '데모 주최자(홍길동)', 'd-hall', CURRENT_DATE - 0, 'BD 업무일지 30', 'SALES', 'P0', 'TODO', '대량 데모 업무 내용 30.', 4.0), + ('bd-wl-031', 'bd-e-032', 'demo-exhibitor', '데모 참가업체(김참가)', 'd-sales', CURRENT_DATE - 1, 'BD 업무일지 31', 'SUPPORT', 'P25', 'DOING', '대량 데모 업무 내용 31.', 4.5), + ('bd-wl-032', 'bd-e-033', 'demo-contractor', '데모 공사업체(이시공)', 'd-mkt', CURRENT_DATE - 2, 'BD 업무일지 32', 'OPERATION', 'P50', 'DONE', '대량 데모 업무 내용 32.', 1.0), + ('bd-wl-033', 'bd-e-034', 'demo-organizer', '데모 주최자(홍길동)', 'd-fin', CURRENT_DATE - 3, 'BD 업무일지 33', 'DESIGN', 'P75', 'TODO', '대량 데모 업무 내용 33.', 1.5), + ('bd-wl-034', 'bd-e-035', 'demo-exhibitor', '데모 참가업체(김참가)', 'd-it-ops', CURRENT_DATE - 4, 'BD 업무일지 34', 'SALES', 'P100', 'DOING', '대량 데모 업무 내용 34.', 2.0), + ('bd-wl-035', 'bd-e-036', 'demo-contractor', '데모 공사업체(이시공)', 'd-hall', CURRENT_DATE - 5, 'BD 업무일지 35', 'SUPPORT', 'P0', 'DONE', '대량 데모 업무 내용 35.', 2.5), + ('bd-wl-036', 'bd-e-037', 'demo-organizer', '데모 주최자(홍길동)', 'd-sales', CURRENT_DATE - 6, 'BD 업무일지 36', 'OPERATION', 'P25', 'TODO', '대량 데모 업무 내용 36.', 3.0), + ('bd-wl-037', 'bd-e-038', 'demo-exhibitor', '데모 참가업체(김참가)', 'd-mkt', CURRENT_DATE - 7, 'BD 업무일지 37', 'DESIGN', 'P50', 'DOING', '대량 데모 업무 내용 37.', 3.5), + ('bd-wl-038', 'bd-e-039', 'demo-contractor', '데모 공사업체(이시공)', 'd-fin', CURRENT_DATE - 8, 'BD 업무일지 38', 'SALES', 'P75', 'DONE', '대량 데모 업무 내용 38.', 4.0), + ('bd-wl-039', 'bd-e-040', 'demo-organizer', '데모 주최자(홍길동)', 'd-it-ops', CURRENT_DATE - 9, 'BD 업무일지 39', 'SUPPORT', 'P100', 'TODO', '대량 데모 업무 내용 39.', 4.5), + ('bd-wl-040', 'bd-e-041', 'demo-exhibitor', '데모 참가업체(김참가)', 'd-hall', CURRENT_DATE - 10, 'BD 업무일지 40', 'OPERATION', 'P0', 'DOING', '대량 데모 업무 내용 40.', 1.0), + ('bd-wl-041', 'bd-e-042', 'demo-contractor', '데모 공사업체(이시공)', 'd-sales', CURRENT_DATE - 11, 'BD 업무일지 41', 'DESIGN', 'P25', 'DONE', '대량 데모 업무 내용 41.', 1.5), + ('bd-wl-042', 'bd-e-043', 'demo-organizer', '데모 주최자(홍길동)', 'd-mkt', CURRENT_DATE - 12, 'BD 업무일지 42', 'SALES', 'P50', 'TODO', '대량 데모 업무 내용 42.', 2.0), + ('bd-wl-043', 'bd-e-044', 'demo-exhibitor', '데모 참가업체(김참가)', 'd-fin', CURRENT_DATE - 13, 'BD 업무일지 43', 'SUPPORT', 'P75', 'DOING', '대량 데모 업무 내용 43.', 2.5), + ('bd-wl-044', 'bd-e-045', 'demo-contractor', '데모 공사업체(이시공)', 'd-it-ops', CURRENT_DATE - 14, 'BD 업무일지 44', 'OPERATION', 'P100', 'DONE', '대량 데모 업무 내용 44.', 3.0), + ('bd-wl-045', 'bd-e-046', 'demo-organizer', '데모 주최자(홍길동)', 'd-hall', CURRENT_DATE - 15, 'BD 업무일지 45', 'DESIGN', 'P0', 'TODO', '대량 데모 업무 내용 45.', 3.5), + ('bd-wl-046', 'bd-e-047', 'demo-exhibitor', '데모 참가업체(김참가)', 'd-sales', CURRENT_DATE - 16, 'BD 업무일지 46', 'SALES', 'P25', 'DOING', '대량 데모 업무 내용 46.', 4.0), + ('bd-wl-047', 'bd-e-048', 'demo-contractor', '데모 공사업체(이시공)', 'd-mkt', CURRENT_DATE - 17, 'BD 업무일지 47', 'SUPPORT', 'P50', 'DONE', '대량 데모 업무 내용 47.', 4.5), + ('bd-wl-048', 'bd-e-049', 'demo-organizer', '데모 주최자(홍길동)', 'd-fin', CURRENT_DATE - 18, 'BD 업무일지 48', 'OPERATION', 'P75', 'TODO', '대량 데모 업무 내용 48.', 1.0), + ('bd-wl-049', 'bd-e-050', 'demo-exhibitor', '데모 참가업체(김참가)', 'd-it-ops', CURRENT_DATE - 19, 'BD 업무일지 49', 'DESIGN', 'P100', 'DOING', '대량 데모 업무 내용 49.', 1.5), + ('bd-wl-050', 'bd-e-051', 'demo-contractor', '데모 공사업체(이시공)', 'd-hall', CURRENT_DATE - 20, 'BD 업무일지 50', 'SALES', 'P0', 'DONE', '대량 데모 업무 내용 50.', 2.0), + ('bd-wl-051', 'bd-e-052', 'demo-organizer', '데모 주최자(홍길동)', 'd-sales', CURRENT_DATE - 21, 'BD 업무일지 51', 'SUPPORT', 'P25', 'TODO', '대량 데모 업무 내용 51.', 2.5), + ('bd-wl-052', 'bd-e-053', 'demo-exhibitor', '데모 참가업체(김참가)', 'd-mkt', CURRENT_DATE - 22, 'BD 업무일지 52', 'OPERATION', 'P50', 'DOING', '대량 데모 업무 내용 52.', 3.0), + ('bd-wl-053', 'bd-e-054', 'demo-contractor', '데모 공사업체(이시공)', 'd-fin', CURRENT_DATE - 23, 'BD 업무일지 53', 'DESIGN', 'P75', 'DONE', '대량 데모 업무 내용 53.', 3.5), + ('bd-wl-054', 'bd-e-055', 'demo-organizer', '데모 주최자(홍길동)', 'd-it-ops', CURRENT_DATE - 24, 'BD 업무일지 54', 'SALES', 'P100', 'TODO', '대량 데모 업무 내용 54.', 4.0), + ('bd-wl-055', 'bd-e-056', 'demo-exhibitor', '데모 참가업체(김참가)', 'd-hall', CURRENT_DATE - 25, 'BD 업무일지 55', 'SUPPORT', 'P0', 'DOING', '대량 데모 업무 내용 55.', 4.5), + ('bd-wl-056', 'bd-e-057', 'demo-contractor', '데모 공사업체(이시공)', 'd-sales', CURRENT_DATE - 26, 'BD 업무일지 56', 'OPERATION', 'P25', 'DONE', '대량 데모 업무 내용 56.', 1.0), + ('bd-wl-057', 'bd-e-058', 'demo-organizer', '데모 주최자(홍길동)', 'd-mkt', CURRENT_DATE - 27, 'BD 업무일지 57', 'DESIGN', 'P50', 'TODO', '대량 데모 업무 내용 57.', 1.5), + ('bd-wl-058', 'bd-e-059', 'demo-exhibitor', '데모 참가업체(김참가)', 'd-fin', CURRENT_DATE - 28, 'BD 업무일지 58', 'SALES', 'P75', 'DOING', '대량 데모 업무 내용 58.', 2.0), + ('bd-wl-059', 'bd-e-060', 'demo-contractor', '데모 공사업체(이시공)', 'd-it-ops', CURRENT_DATE - 29, 'BD 업무일지 59', 'SUPPORT', 'P100', 'DONE', '대량 데모 업무 내용 59.', 2.5), + ('bd-wl-060', 'bd-e-001', 'demo-organizer', '데모 주최자(홍길동)', 'd-hall', CURRENT_DATE - 0, 'BD 업무일지 60', 'OPERATION', 'P0', 'TODO', '대량 데모 업무 내용 60.', 3.0), + ('bd-wl-061', 'bd-e-002', 'demo-exhibitor', '데모 참가업체(김참가)', 'd-sales', CURRENT_DATE - 1, 'BD 업무일지 61', 'DESIGN', 'P25', 'DOING', '대량 데모 업무 내용 61.', 3.5), + ('bd-wl-062', 'bd-e-003', 'demo-contractor', '데모 공사업체(이시공)', 'd-mkt', CURRENT_DATE - 2, 'BD 업무일지 62', 'SALES', 'P50', 'DONE', '대량 데모 업무 내용 62.', 4.0), + ('bd-wl-063', 'bd-e-004', 'demo-organizer', '데모 주최자(홍길동)', 'd-fin', CURRENT_DATE - 3, 'BD 업무일지 63', 'SUPPORT', 'P75', 'TODO', '대량 데모 업무 내용 63.', 4.5), + ('bd-wl-064', 'bd-e-005', 'demo-exhibitor', '데모 참가업체(김참가)', 'd-it-ops', CURRENT_DATE - 4, 'BD 업무일지 64', 'OPERATION', 'P100', 'DOING', '대량 데모 업무 내용 64.', 1.0), + ('bd-wl-065', 'bd-e-006', 'demo-contractor', '데모 공사업체(이시공)', 'd-hall', CURRENT_DATE - 5, 'BD 업무일지 65', 'DESIGN', 'P0', 'DONE', '대량 데모 업무 내용 65.', 1.5), + ('bd-wl-066', 'bd-e-007', 'demo-organizer', '데모 주최자(홍길동)', 'd-sales', CURRENT_DATE - 6, 'BD 업무일지 66', 'SALES', 'P25', 'TODO', '대량 데모 업무 내용 66.', 2.0), + ('bd-wl-067', 'bd-e-008', 'demo-exhibitor', '데모 참가업체(김참가)', 'd-mkt', CURRENT_DATE - 7, 'BD 업무일지 67', 'SUPPORT', 'P50', 'DOING', '대량 데모 업무 내용 67.', 2.5), + ('bd-wl-068', 'bd-e-009', 'demo-contractor', '데모 공사업체(이시공)', 'd-fin', CURRENT_DATE - 8, 'BD 업무일지 68', 'OPERATION', 'P75', 'DONE', '대량 데모 업무 내용 68.', 3.0), + ('bd-wl-069', 'bd-e-010', 'demo-organizer', '데모 주최자(홍길동)', 'd-it-ops', CURRENT_DATE - 9, 'BD 업무일지 69', 'DESIGN', 'P100', 'TODO', '대량 데모 업무 내용 69.', 3.5), + ('bd-wl-070', 'bd-e-011', 'demo-exhibitor', '데모 참가업체(김참가)', 'd-hall', CURRENT_DATE - 10, 'BD 업무일지 70', 'SALES', 'P0', 'DOING', '대량 데모 업무 내용 70.', 4.0), + ('bd-wl-071', 'bd-e-012', 'demo-contractor', '데모 공사업체(이시공)', 'd-sales', CURRENT_DATE - 11, 'BD 업무일지 71', 'SUPPORT', 'P25', 'DONE', '대량 데모 업무 내용 71.', 4.5), + ('bd-wl-072', 'bd-e-013', 'demo-organizer', '데모 주최자(홍길동)', 'd-mkt', CURRENT_DATE - 12, 'BD 업무일지 72', 'OPERATION', 'P50', 'TODO', '대량 데모 업무 내용 72.', 1.0), + ('bd-wl-073', 'bd-e-014', 'demo-exhibitor', '데모 참가업체(김참가)', 'd-fin', CURRENT_DATE - 13, 'BD 업무일지 73', 'DESIGN', 'P75', 'DOING', '대량 데모 업무 내용 73.', 1.5), + ('bd-wl-074', 'bd-e-015', 'demo-contractor', '데모 공사업체(이시공)', 'd-it-ops', CURRENT_DATE - 14, 'BD 업무일지 74', 'SALES', 'P100', 'DONE', '대량 데모 업무 내용 74.', 2.0), + ('bd-wl-075', 'bd-e-016', 'demo-organizer', '데모 주최자(홍길동)', 'd-hall', CURRENT_DATE - 15, 'BD 업무일지 75', 'SUPPORT', 'P0', 'TODO', '대량 데모 업무 내용 75.', 2.5), + ('bd-wl-076', 'bd-e-017', 'demo-exhibitor', '데모 참가업체(김참가)', 'd-sales', CURRENT_DATE - 16, 'BD 업무일지 76', 'OPERATION', 'P25', 'DOING', '대량 데모 업무 내용 76.', 3.0), + ('bd-wl-077', 'bd-e-018', 'demo-contractor', '데모 공사업체(이시공)', 'd-mkt', CURRENT_DATE - 17, 'BD 업무일지 77', 'DESIGN', 'P50', 'DONE', '대량 데모 업무 내용 77.', 3.5), + ('bd-wl-078', 'bd-e-019', 'demo-organizer', '데모 주최자(홍길동)', 'd-fin', CURRENT_DATE - 18, 'BD 업무일지 78', 'SALES', 'P75', 'TODO', '대량 데모 업무 내용 78.', 4.0), + ('bd-wl-079', 'bd-e-020', 'demo-exhibitor', '데모 참가업체(김참가)', 'd-it-ops', CURRENT_DATE - 19, 'BD 업무일지 79', 'SUPPORT', 'P100', 'DOING', '대량 데모 업무 내용 79.', 4.5), + ('bd-wl-080', 'bd-e-021', 'demo-contractor', '데모 공사업체(이시공)', 'd-hall', CURRENT_DATE - 20, 'BD 업무일지 80', 'OPERATION', 'P0', 'DONE', '대량 데모 업무 내용 80.', 1.0) +ON CONFLICT (id) DO NOTHING; + +-- 12. 일정(schedule) 60. +INSERT INTO schedule (id, event_id, owner_id, owner_name, dept_id, title, schedule_type, importance, start_at, end_at, all_day, location, content) VALUES + ('bd-sc-001', 'bd-e-002', 'demo-exhibitor', '데모 참가업체(김참가)', 'd-sales', 'BD 일정 1', 'TEAM', 'NORMAL', now() + interval '-4 day', now() + interval '-4 day' + interval '2 hour', false, '킨텍스 회의실', '대량 데모 일정 1.'), + ('bd-sc-002', 'bd-e-003', 'demo-contractor', '데모 공사업체(이시공)', 'd-mkt', 'BD 일정 2', 'EVENT', 'HIGH', now() + interval '-3 day', now() + interval '-3 day' + interval '2 hour', false, '킨텍스 회의실', '대량 데모 일정 2.'), + ('bd-sc-003', 'bd-e-004', 'demo-organizer', '데모 주최자(홍길동)', 'd-fin', 'BD 일정 3', 'PERSONAL', 'URGENT', now() + interval '-2 day', now() + interval '-2 day' + interval '2 hour', false, '킨텍스 회의실', '대량 데모 일정 3.'), + ('bd-sc-004', 'bd-e-005', 'demo-exhibitor', '데모 참가업체(김참가)', 'd-it-ops', 'BD 일정 4', 'TEAM', 'LOW', now() + interval '-1 day', now() + interval '-1 day' + interval '2 hour', false, '킨텍스 회의실', '대량 데모 일정 4.'), + ('bd-sc-005', 'bd-e-006', 'demo-contractor', '데모 공사업체(이시공)', 'd-hall', 'BD 일정 5', 'EVENT', 'NORMAL', now() + interval '0 day', now() + interval '0 day' + interval '2 hour', false, '킨텍스 회의실', '대량 데모 일정 5.'), + ('bd-sc-006', 'bd-e-007', 'demo-organizer', '데모 주최자(홍길동)', 'd-sales', 'BD 일정 6', 'PERSONAL', 'HIGH', now() + interval '1 day', now() + interval '1 day' + interval '2 hour', false, '킨텍스 회의실', '대량 데모 일정 6.'), + ('bd-sc-007', 'bd-e-008', 'demo-exhibitor', '데모 참가업체(김참가)', 'd-mkt', 'BD 일정 7', 'TEAM', 'URGENT', now() + interval '2 day', now() + interval '2 day' + interval '2 hour', false, '킨텍스 회의실', '대량 데모 일정 7.'), + ('bd-sc-008', 'bd-e-009', 'demo-contractor', '데모 공사업체(이시공)', 'd-fin', 'BD 일정 8', 'EVENT', 'LOW', now() + interval '3 day', now() + interval '3 day' + interval '2 hour', false, '킨텍스 회의실', '대량 데모 일정 8.'), + ('bd-sc-009', 'bd-e-010', 'demo-organizer', '데모 주최자(홍길동)', 'd-it-ops', 'BD 일정 9', 'PERSONAL', 'NORMAL', now() + interval '4 day', now() + interval '4 day' + interval '2 hour', false, '킨텍스 회의실', '대량 데모 일정 9.'), + ('bd-sc-010', 'bd-e-011', 'demo-exhibitor', '데모 참가업체(김참가)', 'd-hall', 'BD 일정 10', 'TEAM', 'HIGH', now() + interval '5 day', now() + interval '5 day' + interval '2 hour', false, '킨텍스 회의실', '대량 데모 일정 10.'), + ('bd-sc-011', 'bd-e-012', 'demo-contractor', '데모 공사업체(이시공)', 'd-sales', 'BD 일정 11', 'EVENT', 'URGENT', now() + interval '6 day', now() + interval '6 day' + interval '2 hour', false, '킨텍스 회의실', '대량 데모 일정 11.'), + ('bd-sc-012', 'bd-e-013', 'demo-organizer', '데모 주최자(홍길동)', 'd-mkt', 'BD 일정 12', 'PERSONAL', 'LOW', now() + interval '7 day', now() + interval '7 day' + interval '2 hour', false, '킨텍스 회의실', '대량 데모 일정 12.'), + ('bd-sc-013', 'bd-e-014', 'demo-exhibitor', '데모 참가업체(김참가)', 'd-fin', 'BD 일정 13', 'TEAM', 'NORMAL', now() + interval '8 day', now() + interval '8 day' + interval '2 hour', false, '킨텍스 회의실', '대량 데모 일정 13.'), + ('bd-sc-014', 'bd-e-015', 'demo-contractor', '데모 공사업체(이시공)', 'd-it-ops', 'BD 일정 14', 'EVENT', 'HIGH', now() + interval '9 day', now() + interval '9 day' + interval '2 hour', false, '킨텍스 회의실', '대량 데모 일정 14.'), + ('bd-sc-015', 'bd-e-016', 'demo-organizer', '데모 주최자(홍길동)', 'd-hall', 'BD 일정 15', 'PERSONAL', 'URGENT', now() + interval '10 day', now() + interval '10 day' + interval '2 hour', false, '킨텍스 회의실', '대량 데모 일정 15.'), + ('bd-sc-016', 'bd-e-017', 'demo-exhibitor', '데모 참가업체(김참가)', 'd-sales', 'BD 일정 16', 'TEAM', 'LOW', now() + interval '11 day', now() + interval '11 day' + interval '2 hour', false, '킨텍스 회의실', '대량 데모 일정 16.'), + ('bd-sc-017', 'bd-e-018', 'demo-contractor', '데모 공사업체(이시공)', 'd-mkt', 'BD 일정 17', 'EVENT', 'NORMAL', now() + interval '12 day', now() + interval '12 day' + interval '2 hour', false, '킨텍스 회의실', '대량 데모 일정 17.'), + ('bd-sc-018', 'bd-e-019', 'demo-organizer', '데모 주최자(홍길동)', 'd-fin', 'BD 일정 18', 'PERSONAL', 'HIGH', now() + interval '13 day', now() + interval '13 day' + interval '2 hour', false, '킨텍스 회의실', '대량 데모 일정 18.'), + ('bd-sc-019', 'bd-e-020', 'demo-exhibitor', '데모 참가업체(김참가)', 'd-it-ops', 'BD 일정 19', 'TEAM', 'URGENT', now() + interval '14 day', now() + interval '14 day' + interval '2 hour', false, '킨텍스 회의실', '대량 데모 일정 19.'), + ('bd-sc-020', 'bd-e-021', 'demo-contractor', '데모 공사업체(이시공)', 'd-hall', 'BD 일정 20', 'EVENT', 'LOW', now() + interval '-5 day', now() + interval '-5 day' + interval '2 hour', false, '킨텍스 회의실', '대량 데모 일정 20.'), + ('bd-sc-021', 'bd-e-022', 'demo-organizer', '데모 주최자(홍길동)', 'd-sales', 'BD 일정 21', 'PERSONAL', 'NORMAL', now() + interval '-4 day', now() + interval '-4 day' + interval '2 hour', false, '킨텍스 회의실', '대량 데모 일정 21.'), + ('bd-sc-022', 'bd-e-023', 'demo-exhibitor', '데모 참가업체(김참가)', 'd-mkt', 'BD 일정 22', 'TEAM', 'HIGH', now() + interval '-3 day', now() + interval '-3 day' + interval '2 hour', false, '킨텍스 회의실', '대량 데모 일정 22.'), + ('bd-sc-023', 'bd-e-024', 'demo-contractor', '데모 공사업체(이시공)', 'd-fin', 'BD 일정 23', 'EVENT', 'URGENT', now() + interval '-2 day', now() + interval '-2 day' + interval '2 hour', false, '킨텍스 회의실', '대량 데모 일정 23.'), + ('bd-sc-024', 'bd-e-025', 'demo-organizer', '데모 주최자(홍길동)', 'd-it-ops', 'BD 일정 24', 'PERSONAL', 'LOW', now() + interval '-1 day', now() + interval '-1 day' + interval '2 hour', false, '킨텍스 회의실', '대량 데모 일정 24.'), + ('bd-sc-025', 'bd-e-026', 'demo-exhibitor', '데모 참가업체(김참가)', 'd-hall', 'BD 일정 25', 'TEAM', 'NORMAL', now() + interval '0 day', now() + interval '0 day' + interval '2 hour', false, '킨텍스 회의실', '대량 데모 일정 25.'), + ('bd-sc-026', 'bd-e-027', 'demo-contractor', '데모 공사업체(이시공)', 'd-sales', 'BD 일정 26', 'EVENT', 'HIGH', now() + interval '1 day', now() + interval '1 day' + interval '2 hour', false, '킨텍스 회의실', '대량 데모 일정 26.'), + ('bd-sc-027', 'bd-e-028', 'demo-organizer', '데모 주최자(홍길동)', 'd-mkt', 'BD 일정 27', 'PERSONAL', 'URGENT', now() + interval '2 day', now() + interval '2 day' + interval '2 hour', false, '킨텍스 회의실', '대량 데모 일정 27.'), + ('bd-sc-028', 'bd-e-029', 'demo-exhibitor', '데모 참가업체(김참가)', 'd-fin', 'BD 일정 28', 'TEAM', 'LOW', now() + interval '3 day', now() + interval '3 day' + interval '2 hour', false, '킨텍스 회의실', '대량 데모 일정 28.'), + ('bd-sc-029', 'bd-e-030', 'demo-contractor', '데모 공사업체(이시공)', 'd-it-ops', 'BD 일정 29', 'EVENT', 'NORMAL', now() + interval '4 day', now() + interval '4 day' + interval '2 hour', false, '킨텍스 회의실', '대량 데모 일정 29.'), + ('bd-sc-030', 'bd-e-031', 'demo-organizer', '데모 주최자(홍길동)', 'd-hall', 'BD 일정 30', 'PERSONAL', 'HIGH', now() + interval '5 day', now() + interval '5 day' + interval '2 hour', false, '킨텍스 회의실', '대량 데모 일정 30.'), + ('bd-sc-031', 'bd-e-032', 'demo-exhibitor', '데모 참가업체(김참가)', 'd-sales', 'BD 일정 31', 'TEAM', 'URGENT', now() + interval '6 day', now() + interval '6 day' + interval '2 hour', false, '킨텍스 회의실', '대량 데모 일정 31.'), + ('bd-sc-032', 'bd-e-033', 'demo-contractor', '데모 공사업체(이시공)', 'd-mkt', 'BD 일정 32', 'EVENT', 'LOW', now() + interval '7 day', now() + interval '7 day' + interval '2 hour', false, '킨텍스 회의실', '대량 데모 일정 32.'), + ('bd-sc-033', 'bd-e-034', 'demo-organizer', '데모 주최자(홍길동)', 'd-fin', 'BD 일정 33', 'PERSONAL', 'NORMAL', now() + interval '8 day', now() + interval '8 day' + interval '2 hour', false, '킨텍스 회의실', '대량 데모 일정 33.'), + ('bd-sc-034', 'bd-e-035', 'demo-exhibitor', '데모 참가업체(김참가)', 'd-it-ops', 'BD 일정 34', 'TEAM', 'HIGH', now() + interval '9 day', now() + interval '9 day' + interval '2 hour', false, '킨텍스 회의실', '대량 데모 일정 34.'), + ('bd-sc-035', 'bd-e-036', 'demo-contractor', '데모 공사업체(이시공)', 'd-hall', 'BD 일정 35', 'EVENT', 'URGENT', now() + interval '10 day', now() + interval '10 day' + interval '2 hour', false, '킨텍스 회의실', '대량 데모 일정 35.'), + ('bd-sc-036', 'bd-e-037', 'demo-organizer', '데모 주최자(홍길동)', 'd-sales', 'BD 일정 36', 'PERSONAL', 'LOW', now() + interval '11 day', now() + interval '11 day' + interval '2 hour', false, '킨텍스 회의실', '대량 데모 일정 36.'), + ('bd-sc-037', 'bd-e-038', 'demo-exhibitor', '데모 참가업체(김참가)', 'd-mkt', 'BD 일정 37', 'TEAM', 'NORMAL', now() + interval '12 day', now() + interval '12 day' + interval '2 hour', false, '킨텍스 회의실', '대량 데모 일정 37.'), + ('bd-sc-038', 'bd-e-039', 'demo-contractor', '데모 공사업체(이시공)', 'd-fin', 'BD 일정 38', 'EVENT', 'HIGH', now() + interval '13 day', now() + interval '13 day' + interval '2 hour', false, '킨텍스 회의실', '대량 데모 일정 38.'), + ('bd-sc-039', 'bd-e-040', 'demo-organizer', '데모 주최자(홍길동)', 'd-it-ops', 'BD 일정 39', 'PERSONAL', 'URGENT', now() + interval '14 day', now() + interval '14 day' + interval '2 hour', false, '킨텍스 회의실', '대량 데모 일정 39.'), + ('bd-sc-040', 'bd-e-041', 'demo-exhibitor', '데모 참가업체(김참가)', 'd-hall', 'BD 일정 40', 'TEAM', 'LOW', now() + interval '-5 day', now() + interval '-5 day' + interval '2 hour', false, '킨텍스 회의실', '대량 데모 일정 40.'), + ('bd-sc-041', 'bd-e-042', 'demo-contractor', '데모 공사업체(이시공)', 'd-sales', 'BD 일정 41', 'EVENT', 'NORMAL', now() + interval '-4 day', now() + interval '-4 day' + interval '2 hour', false, '킨텍스 회의실', '대량 데모 일정 41.'), + ('bd-sc-042', 'bd-e-043', 'demo-organizer', '데모 주최자(홍길동)', 'd-mkt', 'BD 일정 42', 'PERSONAL', 'HIGH', now() + interval '-3 day', now() + interval '-3 day' + interval '2 hour', false, '킨텍스 회의실', '대량 데모 일정 42.'), + ('bd-sc-043', 'bd-e-044', 'demo-exhibitor', '데모 참가업체(김참가)', 'd-fin', 'BD 일정 43', 'TEAM', 'URGENT', now() + interval '-2 day', now() + interval '-2 day' + interval '2 hour', false, '킨텍스 회의실', '대량 데모 일정 43.'), + ('bd-sc-044', 'bd-e-045', 'demo-contractor', '데모 공사업체(이시공)', 'd-it-ops', 'BD 일정 44', 'EVENT', 'LOW', now() + interval '-1 day', now() + interval '-1 day' + interval '2 hour', false, '킨텍스 회의실', '대량 데모 일정 44.'), + ('bd-sc-045', 'bd-e-046', 'demo-organizer', '데모 주최자(홍길동)', 'd-hall', 'BD 일정 45', 'PERSONAL', 'NORMAL', now() + interval '0 day', now() + interval '0 day' + interval '2 hour', false, '킨텍스 회의실', '대량 데모 일정 45.'), + ('bd-sc-046', 'bd-e-047', 'demo-exhibitor', '데모 참가업체(김참가)', 'd-sales', 'BD 일정 46', 'TEAM', 'HIGH', now() + interval '1 day', now() + interval '1 day' + interval '2 hour', false, '킨텍스 회의실', '대량 데모 일정 46.'), + ('bd-sc-047', 'bd-e-048', 'demo-contractor', '데모 공사업체(이시공)', 'd-mkt', 'BD 일정 47', 'EVENT', 'URGENT', now() + interval '2 day', now() + interval '2 day' + interval '2 hour', false, '킨텍스 회의실', '대량 데모 일정 47.'), + ('bd-sc-048', 'bd-e-049', 'demo-organizer', '데모 주최자(홍길동)', 'd-fin', 'BD 일정 48', 'PERSONAL', 'LOW', now() + interval '3 day', now() + interval '3 day' + interval '2 hour', false, '킨텍스 회의실', '대량 데모 일정 48.'), + ('bd-sc-049', 'bd-e-050', 'demo-exhibitor', '데모 참가업체(김참가)', 'd-it-ops', 'BD 일정 49', 'TEAM', 'NORMAL', now() + interval '4 day', now() + interval '4 day' + interval '2 hour', false, '킨텍스 회의실', '대량 데모 일정 49.'), + ('bd-sc-050', 'bd-e-051', 'demo-contractor', '데모 공사업체(이시공)', 'd-hall', 'BD 일정 50', 'EVENT', 'HIGH', now() + interval '5 day', now() + interval '5 day' + interval '2 hour', false, '킨텍스 회의실', '대량 데모 일정 50.'), + ('bd-sc-051', 'bd-e-052', 'demo-organizer', '데모 주최자(홍길동)', 'd-sales', 'BD 일정 51', 'PERSONAL', 'URGENT', now() + interval '6 day', now() + interval '6 day' + interval '2 hour', false, '킨텍스 회의실', '대량 데모 일정 51.'), + ('bd-sc-052', 'bd-e-053', 'demo-exhibitor', '데모 참가업체(김참가)', 'd-mkt', 'BD 일정 52', 'TEAM', 'LOW', now() + interval '7 day', now() + interval '7 day' + interval '2 hour', false, '킨텍스 회의실', '대량 데모 일정 52.'), + ('bd-sc-053', 'bd-e-054', 'demo-contractor', '데모 공사업체(이시공)', 'd-fin', 'BD 일정 53', 'EVENT', 'NORMAL', now() + interval '8 day', now() + interval '8 day' + interval '2 hour', false, '킨텍스 회의실', '대량 데모 일정 53.'), + ('bd-sc-054', 'bd-e-055', 'demo-organizer', '데모 주최자(홍길동)', 'd-it-ops', 'BD 일정 54', 'PERSONAL', 'HIGH', now() + interval '9 day', now() + interval '9 day' + interval '2 hour', false, '킨텍스 회의실', '대량 데모 일정 54.'), + ('bd-sc-055', 'bd-e-056', 'demo-exhibitor', '데모 참가업체(김참가)', 'd-hall', 'BD 일정 55', 'TEAM', 'URGENT', now() + interval '10 day', now() + interval '10 day' + interval '2 hour', false, '킨텍스 회의실', '대량 데모 일정 55.'), + ('bd-sc-056', 'bd-e-057', 'demo-contractor', '데모 공사업체(이시공)', 'd-sales', 'BD 일정 56', 'EVENT', 'LOW', now() + interval '11 day', now() + interval '11 day' + interval '2 hour', false, '킨텍스 회의실', '대량 데모 일정 56.'), + ('bd-sc-057', 'bd-e-058', 'demo-organizer', '데모 주최자(홍길동)', 'd-mkt', 'BD 일정 57', 'PERSONAL', 'NORMAL', now() + interval '12 day', now() + interval '12 day' + interval '2 hour', false, '킨텍스 회의실', '대량 데모 일정 57.'), + ('bd-sc-058', 'bd-e-059', 'demo-exhibitor', '데모 참가업체(김참가)', 'd-fin', 'BD 일정 58', 'TEAM', 'HIGH', now() + interval '13 day', now() + interval '13 day' + interval '2 hour', false, '킨텍스 회의실', '대량 데모 일정 58.'), + ('bd-sc-059', 'bd-e-060', 'demo-contractor', '데모 공사업체(이시공)', 'd-it-ops', 'BD 일정 59', 'EVENT', 'URGENT', now() + interval '14 day', now() + interval '14 day' + interval '2 hour', false, '킨텍스 회의실', '대량 데모 일정 59.'), + ('bd-sc-060', 'bd-e-001', 'demo-organizer', '데모 주최자(홍길동)', 'd-hall', 'BD 일정 60', 'PERSONAL', 'LOW', now() + interval '-5 day', now() + interval '-5 day' + interval '2 hour', false, '킨텍스 회의실', '대량 데모 일정 60.') +ON CONFLICT (id) DO NOTHING; + +-- 13. 쪽지(message) 60 + 수신(message_recipient). +INSERT INTO message (id, sender_id, sender_name, title, content) VALUES + ('bd-msg-001', 'demo-exhibitor', '데모 참가업체(김참가)', 'BD 쪽지 1', '대량 데모 쪽지 본문 1.'), + ('bd-msg-002', 'demo-contractor', '데모 공사업체(이시공)', 'BD 쪽지 2', '대량 데모 쪽지 본문 2.'), + ('bd-msg-003', 'demo-visitor', '데모 관람객(박관람)', 'BD 쪽지 3', '대량 데모 쪽지 본문 3.'), + ('bd-msg-004', 'demo-organizer', '데모 주최자(홍길동)', 'BD 쪽지 4', '대량 데모 쪽지 본문 4.'), + ('bd-msg-005', 'demo-exhibitor', '데모 참가업체(김참가)', 'BD 쪽지 5', '대량 데모 쪽지 본문 5.'), + ('bd-msg-006', 'demo-contractor', '데모 공사업체(이시공)', 'BD 쪽지 6', '대량 데모 쪽지 본문 6.'), + ('bd-msg-007', 'demo-visitor', '데모 관람객(박관람)', 'BD 쪽지 7', '대량 데모 쪽지 본문 7.'), + ('bd-msg-008', 'demo-organizer', '데모 주최자(홍길동)', 'BD 쪽지 8', '대량 데모 쪽지 본문 8.'), + ('bd-msg-009', 'demo-exhibitor', '데모 참가업체(김참가)', 'BD 쪽지 9', '대량 데모 쪽지 본문 9.'), + ('bd-msg-010', 'demo-contractor', '데모 공사업체(이시공)', 'BD 쪽지 10', '대량 데모 쪽지 본문 10.'), + ('bd-msg-011', 'demo-visitor', '데모 관람객(박관람)', 'BD 쪽지 11', '대량 데모 쪽지 본문 11.'), + ('bd-msg-012', 'demo-organizer', '데모 주최자(홍길동)', 'BD 쪽지 12', '대량 데모 쪽지 본문 12.'), + ('bd-msg-013', 'demo-exhibitor', '데모 참가업체(김참가)', 'BD 쪽지 13', '대량 데모 쪽지 본문 13.'), + ('bd-msg-014', 'demo-contractor', '데모 공사업체(이시공)', 'BD 쪽지 14', '대량 데모 쪽지 본문 14.'), + ('bd-msg-015', 'demo-visitor', '데모 관람객(박관람)', 'BD 쪽지 15', '대량 데모 쪽지 본문 15.'), + ('bd-msg-016', 'demo-organizer', '데모 주최자(홍길동)', 'BD 쪽지 16', '대량 데모 쪽지 본문 16.'), + ('bd-msg-017', 'demo-exhibitor', '데모 참가업체(김참가)', 'BD 쪽지 17', '대량 데모 쪽지 본문 17.'), + ('bd-msg-018', 'demo-contractor', '데모 공사업체(이시공)', 'BD 쪽지 18', '대량 데모 쪽지 본문 18.'), + ('bd-msg-019', 'demo-visitor', '데모 관람객(박관람)', 'BD 쪽지 19', '대량 데모 쪽지 본문 19.'), + ('bd-msg-020', 'demo-organizer', '데모 주최자(홍길동)', 'BD 쪽지 20', '대량 데모 쪽지 본문 20.'), + ('bd-msg-021', 'demo-exhibitor', '데모 참가업체(김참가)', 'BD 쪽지 21', '대량 데모 쪽지 본문 21.'), + ('bd-msg-022', 'demo-contractor', '데모 공사업체(이시공)', 'BD 쪽지 22', '대량 데모 쪽지 본문 22.'), + ('bd-msg-023', 'demo-visitor', '데모 관람객(박관람)', 'BD 쪽지 23', '대량 데모 쪽지 본문 23.'), + ('bd-msg-024', 'demo-organizer', '데모 주최자(홍길동)', 'BD 쪽지 24', '대량 데모 쪽지 본문 24.'), + ('bd-msg-025', 'demo-exhibitor', '데모 참가업체(김참가)', 'BD 쪽지 25', '대량 데모 쪽지 본문 25.'), + ('bd-msg-026', 'demo-contractor', '데모 공사업체(이시공)', 'BD 쪽지 26', '대량 데모 쪽지 본문 26.'), + ('bd-msg-027', 'demo-visitor', '데모 관람객(박관람)', 'BD 쪽지 27', '대량 데모 쪽지 본문 27.'), + ('bd-msg-028', 'demo-organizer', '데모 주최자(홍길동)', 'BD 쪽지 28', '대량 데모 쪽지 본문 28.'), + ('bd-msg-029', 'demo-exhibitor', '데모 참가업체(김참가)', 'BD 쪽지 29', '대량 데모 쪽지 본문 29.'), + ('bd-msg-030', 'demo-contractor', '데모 공사업체(이시공)', 'BD 쪽지 30', '대량 데모 쪽지 본문 30.'), + ('bd-msg-031', 'demo-visitor', '데모 관람객(박관람)', 'BD 쪽지 31', '대량 데모 쪽지 본문 31.'), + ('bd-msg-032', 'demo-organizer', '데모 주최자(홍길동)', 'BD 쪽지 32', '대량 데모 쪽지 본문 32.'), + ('bd-msg-033', 'demo-exhibitor', '데모 참가업체(김참가)', 'BD 쪽지 33', '대량 데모 쪽지 본문 33.'), + ('bd-msg-034', 'demo-contractor', '데모 공사업체(이시공)', 'BD 쪽지 34', '대량 데모 쪽지 본문 34.'), + ('bd-msg-035', 'demo-visitor', '데모 관람객(박관람)', 'BD 쪽지 35', '대량 데모 쪽지 본문 35.'), + ('bd-msg-036', 'demo-organizer', '데모 주최자(홍길동)', 'BD 쪽지 36', '대량 데모 쪽지 본문 36.'), + ('bd-msg-037', 'demo-exhibitor', '데모 참가업체(김참가)', 'BD 쪽지 37', '대량 데모 쪽지 본문 37.'), + ('bd-msg-038', 'demo-contractor', '데모 공사업체(이시공)', 'BD 쪽지 38', '대량 데모 쪽지 본문 38.'), + ('bd-msg-039', 'demo-visitor', '데모 관람객(박관람)', 'BD 쪽지 39', '대량 데모 쪽지 본문 39.'), + ('bd-msg-040', 'demo-organizer', '데모 주최자(홍길동)', 'BD 쪽지 40', '대량 데모 쪽지 본문 40.'), + ('bd-msg-041', 'demo-exhibitor', '데모 참가업체(김참가)', 'BD 쪽지 41', '대량 데모 쪽지 본문 41.'), + ('bd-msg-042', 'demo-contractor', '데모 공사업체(이시공)', 'BD 쪽지 42', '대량 데모 쪽지 본문 42.'), + ('bd-msg-043', 'demo-visitor', '데모 관람객(박관람)', 'BD 쪽지 43', '대량 데모 쪽지 본문 43.'), + ('bd-msg-044', 'demo-organizer', '데모 주최자(홍길동)', 'BD 쪽지 44', '대량 데모 쪽지 본문 44.'), + ('bd-msg-045', 'demo-exhibitor', '데모 참가업체(김참가)', 'BD 쪽지 45', '대량 데모 쪽지 본문 45.'), + ('bd-msg-046', 'demo-contractor', '데모 공사업체(이시공)', 'BD 쪽지 46', '대량 데모 쪽지 본문 46.'), + ('bd-msg-047', 'demo-visitor', '데모 관람객(박관람)', 'BD 쪽지 47', '대량 데모 쪽지 본문 47.'), + ('bd-msg-048', 'demo-organizer', '데모 주최자(홍길동)', 'BD 쪽지 48', '대량 데모 쪽지 본문 48.'), + ('bd-msg-049', 'demo-exhibitor', '데모 참가업체(김참가)', 'BD 쪽지 49', '대량 데모 쪽지 본문 49.'), + ('bd-msg-050', 'demo-contractor', '데모 공사업체(이시공)', 'BD 쪽지 50', '대량 데모 쪽지 본문 50.'), + ('bd-msg-051', 'demo-visitor', '데모 관람객(박관람)', 'BD 쪽지 51', '대량 데모 쪽지 본문 51.'), + ('bd-msg-052', 'demo-organizer', '데모 주최자(홍길동)', 'BD 쪽지 52', '대량 데모 쪽지 본문 52.'), + ('bd-msg-053', 'demo-exhibitor', '데모 참가업체(김참가)', 'BD 쪽지 53', '대량 데모 쪽지 본문 53.'), + ('bd-msg-054', 'demo-contractor', '데모 공사업체(이시공)', 'BD 쪽지 54', '대량 데모 쪽지 본문 54.'), + ('bd-msg-055', 'demo-visitor', '데모 관람객(박관람)', 'BD 쪽지 55', '대량 데모 쪽지 본문 55.'), + ('bd-msg-056', 'demo-organizer', '데모 주최자(홍길동)', 'BD 쪽지 56', '대량 데모 쪽지 본문 56.'), + ('bd-msg-057', 'demo-exhibitor', '데모 참가업체(김참가)', 'BD 쪽지 57', '대량 데모 쪽지 본문 57.'), + ('bd-msg-058', 'demo-contractor', '데모 공사업체(이시공)', 'BD 쪽지 58', '대량 데모 쪽지 본문 58.'), + ('bd-msg-059', 'demo-visitor', '데모 관람객(박관람)', 'BD 쪽지 59', '대량 데모 쪽지 본문 59.'), + ('bd-msg-060', 'demo-organizer', '데모 주최자(홍길동)', 'BD 쪽지 60', '대량 데모 쪽지 본문 60.') +ON CONFLICT (id) DO NOTHING; + +INSERT INTO message_recipient (id, message_id, recipient_id, recv_type, read_at, deleted) VALUES + ('bd-mr-001', 'bd-msg-001', 'demo-contractor', 'RECV', NULL, false), + ('bd-mr-002', 'bd-msg-002', 'demo-visitor', 'RECV', now() - interval '3 day', false), + ('bd-mr-003', 'bd-msg-003', 'demo-organizer', 'RECV', NULL, false), + ('bd-mr-004', 'bd-msg-004', 'demo-exhibitor', 'RECV', now() - interval '1 day', false), + ('bd-mr-005', 'bd-msg-005', 'demo-contractor', 'RECV', NULL, false), + ('bd-mr-006', 'bd-msg-006', 'demo-visitor', 'RECV', now() - interval '3 day', false), + ('bd-mr-007', 'bd-msg-007', 'demo-organizer', 'RECV', NULL, false), + ('bd-mr-008', 'bd-msg-008', 'demo-exhibitor', 'RECV', now() - interval '1 day', false), + ('bd-mr-009', 'bd-msg-009', 'demo-contractor', 'RECV', NULL, false), + ('bd-mr-010', 'bd-msg-010', 'demo-visitor', 'RECV', now() - interval '3 day', false), + ('bd-mr-011', 'bd-msg-011', 'demo-organizer', 'RECV', NULL, false), + ('bd-mr-012', 'bd-msg-012', 'demo-exhibitor', 'RECV', now() - interval '1 day', false), + ('bd-mr-013', 'bd-msg-013', 'demo-contractor', 'RECV', NULL, false), + ('bd-mr-014', 'bd-msg-014', 'demo-visitor', 'RECV', now() - interval '3 day', false), + ('bd-mr-015', 'bd-msg-015', 'demo-organizer', 'RECV', NULL, false), + ('bd-mr-016', 'bd-msg-016', 'demo-exhibitor', 'RECV', now() - interval '1 day', false), + ('bd-mr-017', 'bd-msg-017', 'demo-contractor', 'RECV', NULL, false), + ('bd-mr-018', 'bd-msg-018', 'demo-visitor', 'RECV', now() - interval '3 day', false), + ('bd-mr-019', 'bd-msg-019', 'demo-organizer', 'RECV', NULL, false), + ('bd-mr-020', 'bd-msg-020', 'demo-exhibitor', 'RECV', now() - interval '1 day', false), + ('bd-mr-021', 'bd-msg-021', 'demo-contractor', 'RECV', NULL, false), + ('bd-mr-022', 'bd-msg-022', 'demo-visitor', 'RECV', now() - interval '3 day', false), + ('bd-mr-023', 'bd-msg-023', 'demo-organizer', 'RECV', NULL, false), + ('bd-mr-024', 'bd-msg-024', 'demo-exhibitor', 'RECV', now() - interval '1 day', false), + ('bd-mr-025', 'bd-msg-025', 'demo-contractor', 'RECV', NULL, false), + ('bd-mr-026', 'bd-msg-026', 'demo-visitor', 'RECV', now() - interval '3 day', false), + ('bd-mr-027', 'bd-msg-027', 'demo-organizer', 'RECV', NULL, false), + ('bd-mr-028', 'bd-msg-028', 'demo-exhibitor', 'RECV', now() - interval '1 day', false), + ('bd-mr-029', 'bd-msg-029', 'demo-contractor', 'RECV', NULL, false), + ('bd-mr-030', 'bd-msg-030', 'demo-visitor', 'RECV', now() - interval '3 day', false), + ('bd-mr-031', 'bd-msg-031', 'demo-organizer', 'RECV', NULL, false), + ('bd-mr-032', 'bd-msg-032', 'demo-exhibitor', 'RECV', now() - interval '1 day', false), + ('bd-mr-033', 'bd-msg-033', 'demo-contractor', 'RECV', NULL, false), + ('bd-mr-034', 'bd-msg-034', 'demo-visitor', 'RECV', now() - interval '3 day', false), + ('bd-mr-035', 'bd-msg-035', 'demo-organizer', 'RECV', NULL, false), + ('bd-mr-036', 'bd-msg-036', 'demo-exhibitor', 'RECV', now() - interval '1 day', false), + ('bd-mr-037', 'bd-msg-037', 'demo-contractor', 'RECV', NULL, false), + ('bd-mr-038', 'bd-msg-038', 'demo-visitor', 'RECV', now() - interval '3 day', false), + ('bd-mr-039', 'bd-msg-039', 'demo-organizer', 'RECV', NULL, false), + ('bd-mr-040', 'bd-msg-040', 'demo-exhibitor', 'RECV', now() - interval '1 day', false), + ('bd-mr-041', 'bd-msg-041', 'demo-contractor', 'RECV', NULL, false), + ('bd-mr-042', 'bd-msg-042', 'demo-visitor', 'RECV', now() - interval '3 day', false), + ('bd-mr-043', 'bd-msg-043', 'demo-organizer', 'RECV', NULL, false), + ('bd-mr-044', 'bd-msg-044', 'demo-exhibitor', 'RECV', now() - interval '1 day', false), + ('bd-mr-045', 'bd-msg-045', 'demo-contractor', 'RECV', NULL, false), + ('bd-mr-046', 'bd-msg-046', 'demo-visitor', 'RECV', now() - interval '3 day', false), + ('bd-mr-047', 'bd-msg-047', 'demo-organizer', 'RECV', NULL, false), + ('bd-mr-048', 'bd-msg-048', 'demo-exhibitor', 'RECV', now() - interval '1 day', false), + ('bd-mr-049', 'bd-msg-049', 'demo-contractor', 'RECV', NULL, false), + ('bd-mr-050', 'bd-msg-050', 'demo-visitor', 'RECV', now() - interval '3 day', false), + ('bd-mr-051', 'bd-msg-051', 'demo-organizer', 'RECV', NULL, false), + ('bd-mr-052', 'bd-msg-052', 'demo-exhibitor', 'RECV', now() - interval '1 day', false), + ('bd-mr-053', 'bd-msg-053', 'demo-contractor', 'RECV', NULL, false), + ('bd-mr-054', 'bd-msg-054', 'demo-visitor', 'RECV', now() - interval '3 day', false), + ('bd-mr-055', 'bd-msg-055', 'demo-organizer', 'RECV', NULL, false), + ('bd-mr-056', 'bd-msg-056', 'demo-exhibitor', 'RECV', now() - interval '1 day', false), + ('bd-mr-057', 'bd-msg-057', 'demo-contractor', 'RECV', NULL, false), + ('bd-mr-058', 'bd-msg-058', 'demo-visitor', 'RECV', now() - interval '3 day', false), + ('bd-mr-059', 'bd-msg-059', 'demo-organizer', 'RECV', NULL, false), + ('bd-mr-060', 'bd-msg-060', 'demo-exhibitor', 'RECV', now() - interval '1 day', false) +ON CONFLICT (id) DO NOTHING; + +-- 14. 회의(meeting) 50 + 액션아이템(meeting_action). +INSERT INTO meeting (id, event_id, title, location, meeting_at, organizer_id, organizer_name, content, minutes) VALUES + ('bd-mtg-001', 'bd-e-002', 'BD 회의 1', '킨텍스 회의실', now() - interval '-6 day', 'demo-exhibitor', '데모 참가업체(김참가)', '대량 데모 회의 안건 1.', '대량 데모 회의록 1.'), + ('bd-mtg-002', 'bd-e-003', 'BD 회의 2', '킨텍스 회의실', now() - interval '-5 day', 'demo-contractor', '데모 공사업체(이시공)', '대량 데모 회의 안건 2.', '대량 데모 회의록 2.'), + ('bd-mtg-003', 'bd-e-004', 'BD 회의 3', '킨텍스 회의실', now() - interval '-4 day', 'demo-organizer', '데모 주최자(홍길동)', '대량 데모 회의 안건 3.', '대량 데모 회의록 3.'), + ('bd-mtg-004', 'bd-e-005', 'BD 회의 4', '킨텍스 회의실', now() - interval '-3 day', 'demo-exhibitor', '데모 참가업체(김참가)', '대량 데모 회의 안건 4.', '대량 데모 회의록 4.'), + ('bd-mtg-005', 'bd-e-006', 'BD 회의 5', '킨텍스 회의실', now() - interval '-2 day', 'demo-contractor', '데모 공사업체(이시공)', '대량 데모 회의 안건 5.', '대량 데모 회의록 5.'), + ('bd-mtg-006', 'bd-e-007', 'BD 회의 6', '킨텍스 회의실', now() - interval '-1 day', 'demo-organizer', '데모 주최자(홍길동)', '대량 데모 회의 안건 6.', '대량 데모 회의록 6.'), + ('bd-mtg-007', 'bd-e-008', 'BD 회의 7', '킨텍스 회의실', now() - interval '0 day', 'demo-exhibitor', '데모 참가업체(김참가)', '대량 데모 회의 안건 7.', '대량 데모 회의록 7.'), + ('bd-mtg-008', 'bd-e-009', 'BD 회의 8', '킨텍스 회의실', now() - interval '1 day', 'demo-contractor', '데모 공사업체(이시공)', '대량 데모 회의 안건 8.', '대량 데모 회의록 8.'), + ('bd-mtg-009', 'bd-e-010', 'BD 회의 9', '킨텍스 회의실', now() - interval '2 day', 'demo-organizer', '데모 주최자(홍길동)', '대량 데모 회의 안건 9.', '대량 데모 회의록 9.'), + ('bd-mtg-010', 'bd-e-011', 'BD 회의 10', '킨텍스 회의실', now() - interval '3 day', 'demo-exhibitor', '데모 참가업체(김참가)', '대량 데모 회의 안건 10.', '대량 데모 회의록 10.'), + ('bd-mtg-011', 'bd-e-012', 'BD 회의 11', '킨텍스 회의실', now() - interval '4 day', 'demo-contractor', '데모 공사업체(이시공)', '대량 데모 회의 안건 11.', '대량 데모 회의록 11.'), + ('bd-mtg-012', 'bd-e-013', 'BD 회의 12', '킨텍스 회의실', now() - interval '5 day', 'demo-organizer', '데모 주최자(홍길동)', '대량 데모 회의 안건 12.', '대량 데모 회의록 12.'), + ('bd-mtg-013', 'bd-e-014', 'BD 회의 13', '킨텍스 회의실', now() - interval '6 day', 'demo-exhibitor', '데모 참가업체(김참가)', '대량 데모 회의 안건 13.', '대량 데모 회의록 13.'), + ('bd-mtg-014', 'bd-e-015', 'BD 회의 14', '킨텍스 회의실', now() - interval '7 day', 'demo-contractor', '데모 공사업체(이시공)', '대량 데모 회의 안건 14.', '대량 데모 회의록 14.'), + ('bd-mtg-015', 'bd-e-016', 'BD 회의 15', '킨텍스 회의실', now() - interval '-7 day', 'demo-organizer', '데모 주최자(홍길동)', '대량 데모 회의 안건 15.', '대량 데모 회의록 15.'), + ('bd-mtg-016', 'bd-e-017', 'BD 회의 16', '킨텍스 회의실', now() - interval '-6 day', 'demo-exhibitor', '데모 참가업체(김참가)', '대량 데모 회의 안건 16.', '대량 데모 회의록 16.'), + ('bd-mtg-017', 'bd-e-018', 'BD 회의 17', '킨텍스 회의실', now() - interval '-5 day', 'demo-contractor', '데모 공사업체(이시공)', '대량 데모 회의 안건 17.', '대량 데모 회의록 17.'), + ('bd-mtg-018', 'bd-e-019', 'BD 회의 18', '킨텍스 회의실', now() - interval '-4 day', 'demo-organizer', '데모 주최자(홍길동)', '대량 데모 회의 안건 18.', '대량 데모 회의록 18.'), + ('bd-mtg-019', 'bd-e-020', 'BD 회의 19', '킨텍스 회의실', now() - interval '-3 day', 'demo-exhibitor', '데모 참가업체(김참가)', '대량 데모 회의 안건 19.', '대량 데모 회의록 19.'), + ('bd-mtg-020', 'bd-e-021', 'BD 회의 20', '킨텍스 회의실', now() - interval '-2 day', 'demo-contractor', '데모 공사업체(이시공)', '대량 데모 회의 안건 20.', '대량 데모 회의록 20.'), + ('bd-mtg-021', 'bd-e-022', 'BD 회의 21', '킨텍스 회의실', now() - interval '-1 day', 'demo-organizer', '데모 주최자(홍길동)', '대량 데모 회의 안건 21.', '대량 데모 회의록 21.'), + ('bd-mtg-022', 'bd-e-023', 'BD 회의 22', '킨텍스 회의실', now() - interval '0 day', 'demo-exhibitor', '데모 참가업체(김참가)', '대량 데모 회의 안건 22.', '대량 데모 회의록 22.'), + ('bd-mtg-023', 'bd-e-024', 'BD 회의 23', '킨텍스 회의실', now() - interval '1 day', 'demo-contractor', '데모 공사업체(이시공)', '대량 데모 회의 안건 23.', '대량 데모 회의록 23.'), + ('bd-mtg-024', 'bd-e-025', 'BD 회의 24', '킨텍스 회의실', now() - interval '2 day', 'demo-organizer', '데모 주최자(홍길동)', '대량 데모 회의 안건 24.', '대량 데모 회의록 24.'), + ('bd-mtg-025', 'bd-e-026', 'BD 회의 25', '킨텍스 회의실', now() - interval '3 day', 'demo-exhibitor', '데모 참가업체(김참가)', '대량 데모 회의 안건 25.', '대량 데모 회의록 25.'), + ('bd-mtg-026', 'bd-e-027', 'BD 회의 26', '킨텍스 회의실', now() - interval '4 day', 'demo-contractor', '데모 공사업체(이시공)', '대량 데모 회의 안건 26.', '대량 데모 회의록 26.'), + ('bd-mtg-027', 'bd-e-028', 'BD 회의 27', '킨텍스 회의실', now() - interval '5 day', 'demo-organizer', '데모 주최자(홍길동)', '대량 데모 회의 안건 27.', '대량 데모 회의록 27.'), + ('bd-mtg-028', 'bd-e-029', 'BD 회의 28', '킨텍스 회의실', now() - interval '6 day', 'demo-exhibitor', '데모 참가업체(김참가)', '대량 데모 회의 안건 28.', '대량 데모 회의록 28.'), + ('bd-mtg-029', 'bd-e-030', 'BD 회의 29', '킨텍스 회의실', now() - interval '7 day', 'demo-contractor', '데모 공사업체(이시공)', '대량 데모 회의 안건 29.', '대량 데모 회의록 29.'), + ('bd-mtg-030', 'bd-e-031', 'BD 회의 30', '킨텍스 회의실', now() - interval '-7 day', 'demo-organizer', '데모 주최자(홍길동)', '대량 데모 회의 안건 30.', '대량 데모 회의록 30.'), + ('bd-mtg-031', 'bd-e-032', 'BD 회의 31', '킨텍스 회의실', now() - interval '-6 day', 'demo-exhibitor', '데모 참가업체(김참가)', '대량 데모 회의 안건 31.', '대량 데모 회의록 31.'), + ('bd-mtg-032', 'bd-e-033', 'BD 회의 32', '킨텍스 회의실', now() - interval '-5 day', 'demo-contractor', '데모 공사업체(이시공)', '대량 데모 회의 안건 32.', '대량 데모 회의록 32.'), + ('bd-mtg-033', 'bd-e-034', 'BD 회의 33', '킨텍스 회의실', now() - interval '-4 day', 'demo-organizer', '데모 주최자(홍길동)', '대량 데모 회의 안건 33.', '대량 데모 회의록 33.'), + ('bd-mtg-034', 'bd-e-035', 'BD 회의 34', '킨텍스 회의실', now() - interval '-3 day', 'demo-exhibitor', '데모 참가업체(김참가)', '대량 데모 회의 안건 34.', '대량 데모 회의록 34.'), + ('bd-mtg-035', 'bd-e-036', 'BD 회의 35', '킨텍스 회의실', now() - interval '-2 day', 'demo-contractor', '데모 공사업체(이시공)', '대량 데모 회의 안건 35.', '대량 데모 회의록 35.'), + ('bd-mtg-036', 'bd-e-037', 'BD 회의 36', '킨텍스 회의실', now() - interval '-1 day', 'demo-organizer', '데모 주최자(홍길동)', '대량 데모 회의 안건 36.', '대량 데모 회의록 36.'), + ('bd-mtg-037', 'bd-e-038', 'BD 회의 37', '킨텍스 회의실', now() - interval '0 day', 'demo-exhibitor', '데모 참가업체(김참가)', '대량 데모 회의 안건 37.', '대량 데모 회의록 37.'), + ('bd-mtg-038', 'bd-e-039', 'BD 회의 38', '킨텍스 회의실', now() - interval '1 day', 'demo-contractor', '데모 공사업체(이시공)', '대량 데모 회의 안건 38.', '대량 데모 회의록 38.'), + ('bd-mtg-039', 'bd-e-040', 'BD 회의 39', '킨텍스 회의실', now() - interval '2 day', 'demo-organizer', '데모 주최자(홍길동)', '대량 데모 회의 안건 39.', '대량 데모 회의록 39.'), + ('bd-mtg-040', 'bd-e-041', 'BD 회의 40', '킨텍스 회의실', now() - interval '3 day', 'demo-exhibitor', '데모 참가업체(김참가)', '대량 데모 회의 안건 40.', '대량 데모 회의록 40.'), + ('bd-mtg-041', 'bd-e-042', 'BD 회의 41', '킨텍스 회의실', now() - interval '4 day', 'demo-contractor', '데모 공사업체(이시공)', '대량 데모 회의 안건 41.', '대량 데모 회의록 41.'), + ('bd-mtg-042', 'bd-e-043', 'BD 회의 42', '킨텍스 회의실', now() - interval '5 day', 'demo-organizer', '데모 주최자(홍길동)', '대량 데모 회의 안건 42.', '대량 데모 회의록 42.'), + ('bd-mtg-043', 'bd-e-044', 'BD 회의 43', '킨텍스 회의실', now() - interval '6 day', 'demo-exhibitor', '데모 참가업체(김참가)', '대량 데모 회의 안건 43.', '대량 데모 회의록 43.'), + ('bd-mtg-044', 'bd-e-045', 'BD 회의 44', '킨텍스 회의실', now() - interval '7 day', 'demo-contractor', '데모 공사업체(이시공)', '대량 데모 회의 안건 44.', '대량 데모 회의록 44.'), + ('bd-mtg-045', 'bd-e-046', 'BD 회의 45', '킨텍스 회의실', now() - interval '-7 day', 'demo-organizer', '데모 주최자(홍길동)', '대량 데모 회의 안건 45.', '대량 데모 회의록 45.'), + ('bd-mtg-046', 'bd-e-047', 'BD 회의 46', '킨텍스 회의실', now() - interval '-6 day', 'demo-exhibitor', '데모 참가업체(김참가)', '대량 데모 회의 안건 46.', '대량 데모 회의록 46.'), + ('bd-mtg-047', 'bd-e-048', 'BD 회의 47', '킨텍스 회의실', now() - interval '-5 day', 'demo-contractor', '데모 공사업체(이시공)', '대량 데모 회의 안건 47.', '대량 데모 회의록 47.'), + ('bd-mtg-048', 'bd-e-049', 'BD 회의 48', '킨텍스 회의실', now() - interval '-4 day', 'demo-organizer', '데모 주최자(홍길동)', '대량 데모 회의 안건 48.', '대량 데모 회의록 48.'), + ('bd-mtg-049', 'bd-e-050', 'BD 회의 49', '킨텍스 회의실', now() - interval '-3 day', 'demo-exhibitor', '데모 참가업체(김참가)', '대량 데모 회의 안건 49.', '대량 데모 회의록 49.'), + ('bd-mtg-050', 'bd-e-051', 'BD 회의 50', '킨텍스 회의실', now() - interval '-2 day', 'demo-contractor', '데모 공사업체(이시공)', '대량 데모 회의 안건 50.', '대량 데모 회의록 50.') +ON CONFLICT (id) DO NOTHING; + +INSERT INTO meeting_action (id, meeting_id, seq, action_item, assignee_id, due_date, status) VALUES + ('bd-ma-001', 'bd-mtg-001', 1, '액션아이템 1', 'demo-contractor', CURRENT_DATE + 2, 'TODO'), + ('bd-ma-002', 'bd-mtg-002', 1, '액션아이템 2', 'demo-organizer', CURRENT_DATE + 3, 'TODO'), + ('bd-ma-003', 'bd-mtg-003', 1, '액션아이템 3', 'demo-exhibitor', CURRENT_DATE + 4, 'TODO'), + ('bd-ma-004', 'bd-mtg-004', 1, '액션아이템 4', 'demo-contractor', CURRENT_DATE + 5, 'TODO'), + ('bd-ma-005', 'bd-mtg-005', 1, '액션아이템 5', 'demo-organizer', CURRENT_DATE + 6, 'TODO'), + ('bd-ma-006', 'bd-mtg-006', 1, '액션아이템 6', 'demo-exhibitor', CURRENT_DATE + 7, 'TODO'), + ('bd-ma-007', 'bd-mtg-007', 1, '액션아이템 7', 'demo-contractor', CURRENT_DATE + 8, 'TODO'), + ('bd-ma-008', 'bd-mtg-008', 1, '액션아이템 8', 'demo-organizer', CURRENT_DATE + 9, 'TODO'), + ('bd-ma-009', 'bd-mtg-009', 1, '액션아이템 9', 'demo-exhibitor', CURRENT_DATE + 10, 'TODO'), + ('bd-ma-010', 'bd-mtg-010', 1, '액션아이템 10', 'demo-contractor', CURRENT_DATE + 1, 'TODO'), + ('bd-ma-011', 'bd-mtg-011', 1, '액션아이템 11', 'demo-organizer', CURRENT_DATE + 2, 'TODO'), + ('bd-ma-012', 'bd-mtg-012', 1, '액션아이템 12', 'demo-exhibitor', CURRENT_DATE + 3, 'TODO'), + ('bd-ma-013', 'bd-mtg-013', 1, '액션아이템 13', 'demo-contractor', CURRENT_DATE + 4, 'TODO'), + ('bd-ma-014', 'bd-mtg-014', 1, '액션아이템 14', 'demo-organizer', CURRENT_DATE + 5, 'TODO'), + ('bd-ma-015', 'bd-mtg-015', 1, '액션아이템 15', 'demo-exhibitor', CURRENT_DATE + 6, 'TODO'), + ('bd-ma-016', 'bd-mtg-016', 1, '액션아이템 16', 'demo-contractor', CURRENT_DATE + 7, 'TODO'), + ('bd-ma-017', 'bd-mtg-017', 1, '액션아이템 17', 'demo-organizer', CURRENT_DATE + 8, 'TODO'), + ('bd-ma-018', 'bd-mtg-018', 1, '액션아이템 18', 'demo-exhibitor', CURRENT_DATE + 9, 'TODO'), + ('bd-ma-019', 'bd-mtg-019', 1, '액션아이템 19', 'demo-contractor', CURRENT_DATE + 10, 'TODO'), + ('bd-ma-020', 'bd-mtg-020', 1, '액션아이템 20', 'demo-organizer', CURRENT_DATE + 1, 'TODO'), + ('bd-ma-021', 'bd-mtg-021', 1, '액션아이템 21', 'demo-exhibitor', CURRENT_DATE + 2, 'TODO'), + ('bd-ma-022', 'bd-mtg-022', 1, '액션아이템 22', 'demo-contractor', CURRENT_DATE + 3, 'TODO'), + ('bd-ma-023', 'bd-mtg-023', 1, '액션아이템 23', 'demo-organizer', CURRENT_DATE + 4, 'TODO'), + ('bd-ma-024', 'bd-mtg-024', 1, '액션아이템 24', 'demo-exhibitor', CURRENT_DATE + 5, 'TODO'), + ('bd-ma-025', 'bd-mtg-025', 1, '액션아이템 25', 'demo-contractor', CURRENT_DATE + 6, 'TODO'), + ('bd-ma-026', 'bd-mtg-026', 1, '액션아이템 26', 'demo-organizer', CURRENT_DATE + 7, 'TODO'), + ('bd-ma-027', 'bd-mtg-027', 1, '액션아이템 27', 'demo-exhibitor', CURRENT_DATE + 8, 'TODO'), + ('bd-ma-028', 'bd-mtg-028', 1, '액션아이템 28', 'demo-contractor', CURRENT_DATE + 9, 'TODO'), + ('bd-ma-029', 'bd-mtg-029', 1, '액션아이템 29', 'demo-organizer', CURRENT_DATE + 10, 'TODO'), + ('bd-ma-030', 'bd-mtg-030', 1, '액션아이템 30', 'demo-exhibitor', CURRENT_DATE + 1, 'TODO'), + ('bd-ma-031', 'bd-mtg-031', 1, '액션아이템 31', 'demo-contractor', CURRENT_DATE + 2, 'TODO'), + ('bd-ma-032', 'bd-mtg-032', 1, '액션아이템 32', 'demo-organizer', CURRENT_DATE + 3, 'TODO'), + ('bd-ma-033', 'bd-mtg-033', 1, '액션아이템 33', 'demo-exhibitor', CURRENT_DATE + 4, 'TODO'), + ('bd-ma-034', 'bd-mtg-034', 1, '액션아이템 34', 'demo-contractor', CURRENT_DATE + 5, 'TODO'), + ('bd-ma-035', 'bd-mtg-035', 1, '액션아이템 35', 'demo-organizer', CURRENT_DATE + 6, 'TODO'), + ('bd-ma-036', 'bd-mtg-036', 1, '액션아이템 36', 'demo-exhibitor', CURRENT_DATE + 7, 'TODO'), + ('bd-ma-037', 'bd-mtg-037', 1, '액션아이템 37', 'demo-contractor', CURRENT_DATE + 8, 'TODO'), + ('bd-ma-038', 'bd-mtg-038', 1, '액션아이템 38', 'demo-organizer', CURRENT_DATE + 9, 'TODO'), + ('bd-ma-039', 'bd-mtg-039', 1, '액션아이템 39', 'demo-exhibitor', CURRENT_DATE + 10, 'TODO'), + ('bd-ma-040', 'bd-mtg-040', 1, '액션아이템 40', 'demo-contractor', CURRENT_DATE + 1, 'TODO'), + ('bd-ma-041', 'bd-mtg-041', 1, '액션아이템 41', 'demo-organizer', CURRENT_DATE + 2, 'TODO'), + ('bd-ma-042', 'bd-mtg-042', 1, '액션아이템 42', 'demo-exhibitor', CURRENT_DATE + 3, 'TODO'), + ('bd-ma-043', 'bd-mtg-043', 1, '액션아이템 43', 'demo-contractor', CURRENT_DATE + 4, 'TODO'), + ('bd-ma-044', 'bd-mtg-044', 1, '액션아이템 44', 'demo-organizer', CURRENT_DATE + 5, 'TODO'), + ('bd-ma-045', 'bd-mtg-045', 1, '액션아이템 45', 'demo-exhibitor', CURRENT_DATE + 6, 'TODO'), + ('bd-ma-046', 'bd-mtg-046', 1, '액션아이템 46', 'demo-contractor', CURRENT_DATE + 7, 'TODO'), + ('bd-ma-047', 'bd-mtg-047', 1, '액션아이템 47', 'demo-organizer', CURRENT_DATE + 8, 'TODO'), + ('bd-ma-048', 'bd-mtg-048', 1, '액션아이템 48', 'demo-exhibitor', CURRENT_DATE + 9, 'TODO'), + ('bd-ma-049', 'bd-mtg-049', 1, '액션아이템 49', 'demo-contractor', CURRENT_DATE + 10, 'TODO'), + ('bd-ma-050', 'bd-mtg-050', 1, '액션아이템 50', 'demo-organizer', CURRENT_DATE + 1, 'TODO') +ON CONFLICT (id) DO NOTHING; + +-- 15. 보고서(report) 50. +INSERT INTO report (id, event_id, report_type, period_from, period_to, title, content, author_id, author_name) VALUES + ('bd-rpt-001', 'bd-e-002', 'WEEKLY', CURRENT_DATE - 8, CURRENT_DATE - 1, 'BD 보고서 1', '대량 데모 보고 내용 1.', 'demo-exhibitor', '데모 참가업체(김참가)'), + ('bd-rpt-002', 'bd-e-003', 'MONTHLY', CURRENT_DATE - 9, CURRENT_DATE - 2, 'BD 보고서 2', '대량 데모 보고 내용 2.', 'demo-contractor', '데모 공사업체(이시공)'), + ('bd-rpt-003', 'bd-e-004', 'DAILY', CURRENT_DATE - 10, CURRENT_DATE - 3, 'BD 보고서 3', '대량 데모 보고 내용 3.', 'demo-organizer', '데모 주최자(홍길동)'), + ('bd-rpt-004', 'bd-e-005', 'WEEKLY', CURRENT_DATE - 11, CURRENT_DATE - 4, 'BD 보고서 4', '대량 데모 보고 내용 4.', 'demo-exhibitor', '데모 참가업체(김참가)'), + ('bd-rpt-005', 'bd-e-006', 'MONTHLY', CURRENT_DATE - 12, CURRENT_DATE - 5, 'BD 보고서 5', '대량 데모 보고 내용 5.', 'demo-contractor', '데모 공사업체(이시공)'), + ('bd-rpt-006', 'bd-e-007', 'DAILY', CURRENT_DATE - 13, CURRENT_DATE - 6, 'BD 보고서 6', '대량 데모 보고 내용 6.', 'demo-organizer', '데모 주최자(홍길동)'), + ('bd-rpt-007', 'bd-e-008', 'WEEKLY', CURRENT_DATE - 14, CURRENT_DATE - 7, 'BD 보고서 7', '대량 데모 보고 내용 7.', 'demo-exhibitor', '데모 참가업체(김참가)'), + ('bd-rpt-008', 'bd-e-009', 'MONTHLY', CURRENT_DATE - 15, CURRENT_DATE - 8, 'BD 보고서 8', '대량 데모 보고 내용 8.', 'demo-contractor', '데모 공사업체(이시공)'), + ('bd-rpt-009', 'bd-e-010', 'DAILY', CURRENT_DATE - 16, CURRENT_DATE - 9, 'BD 보고서 9', '대량 데모 보고 내용 9.', 'demo-organizer', '데모 주최자(홍길동)'), + ('bd-rpt-010', 'bd-e-011', 'WEEKLY', CURRENT_DATE - 17, CURRENT_DATE - 10, 'BD 보고서 10', '대량 데모 보고 내용 10.', 'demo-exhibitor', '데모 참가업체(김참가)'), + ('bd-rpt-011', 'bd-e-012', 'MONTHLY', CURRENT_DATE - 18, CURRENT_DATE - 11, 'BD 보고서 11', '대량 데모 보고 내용 11.', 'demo-contractor', '데모 공사업체(이시공)'), + ('bd-rpt-012', 'bd-e-013', 'DAILY', CURRENT_DATE - 19, CURRENT_DATE - 12, 'BD 보고서 12', '대량 데모 보고 내용 12.', 'demo-organizer', '데모 주최자(홍길동)'), + ('bd-rpt-013', 'bd-e-014', 'WEEKLY', CURRENT_DATE - 20, CURRENT_DATE - 13, 'BD 보고서 13', '대량 데모 보고 내용 13.', 'demo-exhibitor', '데모 참가업체(김참가)'), + ('bd-rpt-014', 'bd-e-015', 'MONTHLY', CURRENT_DATE - 21, CURRENT_DATE - 14, 'BD 보고서 14', '대량 데모 보고 내용 14.', 'demo-contractor', '데모 공사업체(이시공)'), + ('bd-rpt-015', 'bd-e-016', 'DAILY', CURRENT_DATE - 22, CURRENT_DATE - 15, 'BD 보고서 15', '대량 데모 보고 내용 15.', 'demo-organizer', '데모 주최자(홍길동)'), + ('bd-rpt-016', 'bd-e-017', 'WEEKLY', CURRENT_DATE - 23, CURRENT_DATE - 16, 'BD 보고서 16', '대량 데모 보고 내용 16.', 'demo-exhibitor', '데모 참가업체(김참가)'), + ('bd-rpt-017', 'bd-e-018', 'MONTHLY', CURRENT_DATE - 24, CURRENT_DATE - 17, 'BD 보고서 17', '대량 데모 보고 내용 17.', 'demo-contractor', '데모 공사업체(이시공)'), + ('bd-rpt-018', 'bd-e-019', 'DAILY', CURRENT_DATE - 25, CURRENT_DATE - 18, 'BD 보고서 18', '대량 데모 보고 내용 18.', 'demo-organizer', '데모 주최자(홍길동)'), + ('bd-rpt-019', 'bd-e-020', 'WEEKLY', CURRENT_DATE - 26, CURRENT_DATE - 19, 'BD 보고서 19', '대량 데모 보고 내용 19.', 'demo-exhibitor', '데모 참가업체(김참가)'), + ('bd-rpt-020', 'bd-e-021', 'MONTHLY', CURRENT_DATE - 27, CURRENT_DATE - 20, 'BD 보고서 20', '대량 데모 보고 내용 20.', 'demo-contractor', '데모 공사업체(이시공)'), + ('bd-rpt-021', 'bd-e-022', 'DAILY', CURRENT_DATE - 28, CURRENT_DATE - 21, 'BD 보고서 21', '대량 데모 보고 내용 21.', 'demo-organizer', '데모 주최자(홍길동)'), + ('bd-rpt-022', 'bd-e-023', 'WEEKLY', CURRENT_DATE - 29, CURRENT_DATE - 22, 'BD 보고서 22', '대량 데모 보고 내용 22.', 'demo-exhibitor', '데모 참가업체(김참가)'), + ('bd-rpt-023', 'bd-e-024', 'MONTHLY', CURRENT_DATE - 30, CURRENT_DATE - 23, 'BD 보고서 23', '대량 데모 보고 내용 23.', 'demo-contractor', '데모 공사업체(이시공)'), + ('bd-rpt-024', 'bd-e-025', 'DAILY', CURRENT_DATE - 31, CURRENT_DATE - 24, 'BD 보고서 24', '대량 데모 보고 내용 24.', 'demo-organizer', '데모 주최자(홍길동)'), + ('bd-rpt-025', 'bd-e-026', 'WEEKLY', CURRENT_DATE - 32, CURRENT_DATE - 25, 'BD 보고서 25', '대량 데모 보고 내용 25.', 'demo-exhibitor', '데모 참가업체(김참가)'), + ('bd-rpt-026', 'bd-e-027', 'MONTHLY', CURRENT_DATE - 33, CURRENT_DATE - 26, 'BD 보고서 26', '대량 데모 보고 내용 26.', 'demo-contractor', '데모 공사업체(이시공)'), + ('bd-rpt-027', 'bd-e-028', 'DAILY', CURRENT_DATE - 34, CURRENT_DATE - 27, 'BD 보고서 27', '대량 데모 보고 내용 27.', 'demo-organizer', '데모 주최자(홍길동)'), + ('bd-rpt-028', 'bd-e-029', 'WEEKLY', CURRENT_DATE - 35, CURRENT_DATE - 28, 'BD 보고서 28', '대량 데모 보고 내용 28.', 'demo-exhibitor', '데모 참가업체(김참가)'), + ('bd-rpt-029', 'bd-e-030', 'MONTHLY', CURRENT_DATE - 36, CURRENT_DATE - 29, 'BD 보고서 29', '대량 데모 보고 내용 29.', 'demo-contractor', '데모 공사업체(이시공)'), + ('bd-rpt-030', 'bd-e-031', 'DAILY', CURRENT_DATE - 7, CURRENT_DATE - 0, 'BD 보고서 30', '대량 데모 보고 내용 30.', 'demo-organizer', '데모 주최자(홍길동)'), + ('bd-rpt-031', 'bd-e-032', 'WEEKLY', CURRENT_DATE - 8, CURRENT_DATE - 1, 'BD 보고서 31', '대량 데모 보고 내용 31.', 'demo-exhibitor', '데모 참가업체(김참가)'), + ('bd-rpt-032', 'bd-e-033', 'MONTHLY', CURRENT_DATE - 9, CURRENT_DATE - 2, 'BD 보고서 32', '대량 데모 보고 내용 32.', 'demo-contractor', '데모 공사업체(이시공)'), + ('bd-rpt-033', 'bd-e-034', 'DAILY', CURRENT_DATE - 10, CURRENT_DATE - 3, 'BD 보고서 33', '대량 데모 보고 내용 33.', 'demo-organizer', '데모 주최자(홍길동)'), + ('bd-rpt-034', 'bd-e-035', 'WEEKLY', CURRENT_DATE - 11, CURRENT_DATE - 4, 'BD 보고서 34', '대량 데모 보고 내용 34.', 'demo-exhibitor', '데모 참가업체(김참가)'), + ('bd-rpt-035', 'bd-e-036', 'MONTHLY', CURRENT_DATE - 12, CURRENT_DATE - 5, 'BD 보고서 35', '대량 데모 보고 내용 35.', 'demo-contractor', '데모 공사업체(이시공)'), + ('bd-rpt-036', 'bd-e-037', 'DAILY', CURRENT_DATE - 13, CURRENT_DATE - 6, 'BD 보고서 36', '대량 데모 보고 내용 36.', 'demo-organizer', '데모 주최자(홍길동)'), + ('bd-rpt-037', 'bd-e-038', 'WEEKLY', CURRENT_DATE - 14, CURRENT_DATE - 7, 'BD 보고서 37', '대량 데모 보고 내용 37.', 'demo-exhibitor', '데모 참가업체(김참가)'), + ('bd-rpt-038', 'bd-e-039', 'MONTHLY', CURRENT_DATE - 15, CURRENT_DATE - 8, 'BD 보고서 38', '대량 데모 보고 내용 38.', 'demo-contractor', '데모 공사업체(이시공)'), + ('bd-rpt-039', 'bd-e-040', 'DAILY', CURRENT_DATE - 16, CURRENT_DATE - 9, 'BD 보고서 39', '대량 데모 보고 내용 39.', 'demo-organizer', '데모 주최자(홍길동)'), + ('bd-rpt-040', 'bd-e-041', 'WEEKLY', CURRENT_DATE - 17, CURRENT_DATE - 10, 'BD 보고서 40', '대량 데모 보고 내용 40.', 'demo-exhibitor', '데모 참가업체(김참가)'), + ('bd-rpt-041', 'bd-e-042', 'MONTHLY', CURRENT_DATE - 18, CURRENT_DATE - 11, 'BD 보고서 41', '대량 데모 보고 내용 41.', 'demo-contractor', '데모 공사업체(이시공)'), + ('bd-rpt-042', 'bd-e-043', 'DAILY', CURRENT_DATE - 19, CURRENT_DATE - 12, 'BD 보고서 42', '대량 데모 보고 내용 42.', 'demo-organizer', '데모 주최자(홍길동)'), + ('bd-rpt-043', 'bd-e-044', 'WEEKLY', CURRENT_DATE - 20, CURRENT_DATE - 13, 'BD 보고서 43', '대량 데모 보고 내용 43.', 'demo-exhibitor', '데모 참가업체(김참가)'), + ('bd-rpt-044', 'bd-e-045', 'MONTHLY', CURRENT_DATE - 21, CURRENT_DATE - 14, 'BD 보고서 44', '대량 데모 보고 내용 44.', 'demo-contractor', '데모 공사업체(이시공)'), + ('bd-rpt-045', 'bd-e-046', 'DAILY', CURRENT_DATE - 22, CURRENT_DATE - 15, 'BD 보고서 45', '대량 데모 보고 내용 45.', 'demo-organizer', '데모 주최자(홍길동)'), + ('bd-rpt-046', 'bd-e-047', 'WEEKLY', CURRENT_DATE - 23, CURRENT_DATE - 16, 'BD 보고서 46', '대량 데모 보고 내용 46.', 'demo-exhibitor', '데모 참가업체(김참가)'), + ('bd-rpt-047', 'bd-e-048', 'MONTHLY', CURRENT_DATE - 24, CURRENT_DATE - 17, 'BD 보고서 47', '대량 데모 보고 내용 47.', 'demo-contractor', '데모 공사업체(이시공)'), + ('bd-rpt-048', 'bd-e-049', 'DAILY', CURRENT_DATE - 25, CURRENT_DATE - 18, 'BD 보고서 48', '대량 데모 보고 내용 48.', 'demo-organizer', '데모 주최자(홍길동)'), + ('bd-rpt-049', 'bd-e-050', 'WEEKLY', CURRENT_DATE - 26, CURRENT_DATE - 19, 'BD 보고서 49', '대량 데모 보고 내용 49.', 'demo-exhibitor', '데모 참가업체(김참가)'), + ('bd-rpt-050', 'bd-e-051', 'MONTHLY', CURRENT_DATE - 27, CURRENT_DATE - 20, 'BD 보고서 50', '대량 데모 보고 내용 50.', 'demo-contractor', '데모 공사업체(이시공)') +ON CONFLICT (id) DO NOTHING; + +-- 16. 의견(opinion) 50 + 댓글(opinion_comment). +INSERT INTO opinion (id, event_id, category, title, content, status, author_id, author_name, secret_yn) VALUES + ('bd-op-001', 'bd-e-002', '개선제안', 'BD 의견 1', '대량 데모 의견 본문 1.', 'ANSWERED', 'demo-exhibitor', '데모 참가업체(김참가)', 'N'), + ('bd-op-002', 'bd-e-003', '개선제안', 'BD 의견 2', '대량 데모 의견 본문 2.', 'CLOSED', 'demo-contractor', '데모 공사업체(이시공)', 'N'), + ('bd-op-003', NULL, '개선제안', 'BD 의견 3', '대량 데모 의견 본문 3.', 'OPEN', 'demo-visitor', '데모 관람객(박관람)', 'N'), + ('bd-op-004', 'bd-e-005', '개선제안', 'BD 의견 4', '대량 데모 의견 본문 4.', 'ANSWERED', 'demo-organizer', '데모 주최자(홍길동)', 'N'), + ('bd-op-005', 'bd-e-006', '개선제안', 'BD 의견 5', '대량 데모 의견 본문 5.', 'CLOSED', 'demo-exhibitor', '데모 참가업체(김참가)', 'N'), + ('bd-op-006', NULL, '개선제안', 'BD 의견 6', '대량 데모 의견 본문 6.', 'OPEN', 'demo-contractor', '데모 공사업체(이시공)', 'N'), + ('bd-op-007', 'bd-e-008', '개선제안', 'BD 의견 7', '대량 데모 의견 본문 7.', 'ANSWERED', 'demo-visitor', '데모 관람객(박관람)', 'N'), + ('bd-op-008', 'bd-e-009', '개선제안', 'BD 의견 8', '대량 데모 의견 본문 8.', 'CLOSED', 'demo-organizer', '데모 주최자(홍길동)', 'N'), + ('bd-op-009', NULL, '개선제안', 'BD 의견 9', '대량 데모 의견 본문 9.', 'OPEN', 'demo-exhibitor', '데모 참가업체(김참가)', 'N'), + ('bd-op-010', 'bd-e-011', '개선제안', 'BD 의견 10', '대량 데모 의견 본문 10.', 'ANSWERED', 'demo-contractor', '데모 공사업체(이시공)', 'N'), + ('bd-op-011', 'bd-e-012', '개선제안', 'BD 의견 11', '대량 데모 의견 본문 11.', 'CLOSED', 'demo-visitor', '데모 관람객(박관람)', 'N'), + ('bd-op-012', NULL, '개선제안', 'BD 의견 12', '대량 데모 의견 본문 12.', 'OPEN', 'demo-organizer', '데모 주최자(홍길동)', 'N'), + ('bd-op-013', 'bd-e-014', '개선제안', 'BD 의견 13', '대량 데모 의견 본문 13.', 'ANSWERED', 'demo-exhibitor', '데모 참가업체(김참가)', 'N'), + ('bd-op-014', 'bd-e-015', '개선제안', 'BD 의견 14', '대량 데모 의견 본문 14.', 'CLOSED', 'demo-contractor', '데모 공사업체(이시공)', 'N'), + ('bd-op-015', NULL, '개선제안', 'BD 의견 15', '대량 데모 의견 본문 15.', 'OPEN', 'demo-visitor', '데모 관람객(박관람)', 'N'), + ('bd-op-016', 'bd-e-017', '개선제안', 'BD 의견 16', '대량 데모 의견 본문 16.', 'ANSWERED', 'demo-organizer', '데모 주최자(홍길동)', 'N'), + ('bd-op-017', 'bd-e-018', '개선제안', 'BD 의견 17', '대량 데모 의견 본문 17.', 'CLOSED', 'demo-exhibitor', '데모 참가업체(김참가)', 'N'), + ('bd-op-018', NULL, '개선제안', 'BD 의견 18', '대량 데모 의견 본문 18.', 'OPEN', 'demo-contractor', '데모 공사업체(이시공)', 'N'), + ('bd-op-019', 'bd-e-020', '개선제안', 'BD 의견 19', '대량 데모 의견 본문 19.', 'ANSWERED', 'demo-visitor', '데모 관람객(박관람)', 'N'), + ('bd-op-020', 'bd-e-021', '개선제안', 'BD 의견 20', '대량 데모 의견 본문 20.', 'CLOSED', 'demo-organizer', '데모 주최자(홍길동)', 'N'), + ('bd-op-021', NULL, '개선제안', 'BD 의견 21', '대량 데모 의견 본문 21.', 'OPEN', 'demo-exhibitor', '데모 참가업체(김참가)', 'N'), + ('bd-op-022', 'bd-e-023', '개선제안', 'BD 의견 22', '대량 데모 의견 본문 22.', 'ANSWERED', 'demo-contractor', '데모 공사업체(이시공)', 'N'), + ('bd-op-023', 'bd-e-024', '개선제안', 'BD 의견 23', '대량 데모 의견 본문 23.', 'CLOSED', 'demo-visitor', '데모 관람객(박관람)', 'N'), + ('bd-op-024', NULL, '개선제안', 'BD 의견 24', '대량 데모 의견 본문 24.', 'OPEN', 'demo-organizer', '데모 주최자(홍길동)', 'N'), + ('bd-op-025', 'bd-e-026', '개선제안', 'BD 의견 25', '대량 데모 의견 본문 25.', 'ANSWERED', 'demo-exhibitor', '데모 참가업체(김참가)', 'N'), + ('bd-op-026', 'bd-e-027', '개선제안', 'BD 의견 26', '대량 데모 의견 본문 26.', 'CLOSED', 'demo-contractor', '데모 공사업체(이시공)', 'N'), + ('bd-op-027', NULL, '개선제안', 'BD 의견 27', '대량 데모 의견 본문 27.', 'OPEN', 'demo-visitor', '데모 관람객(박관람)', 'N'), + ('bd-op-028', 'bd-e-029', '개선제안', 'BD 의견 28', '대량 데모 의견 본문 28.', 'ANSWERED', 'demo-organizer', '데모 주최자(홍길동)', 'N'), + ('bd-op-029', 'bd-e-030', '개선제안', 'BD 의견 29', '대량 데모 의견 본문 29.', 'CLOSED', 'demo-exhibitor', '데모 참가업체(김참가)', 'N'), + ('bd-op-030', NULL, '개선제안', 'BD 의견 30', '대량 데모 의견 본문 30.', 'OPEN', 'demo-contractor', '데모 공사업체(이시공)', 'N'), + ('bd-op-031', 'bd-e-032', '개선제안', 'BD 의견 31', '대량 데모 의견 본문 31.', 'ANSWERED', 'demo-visitor', '데모 관람객(박관람)', 'N'), + ('bd-op-032', 'bd-e-033', '개선제안', 'BD 의견 32', '대량 데모 의견 본문 32.', 'CLOSED', 'demo-organizer', '데모 주최자(홍길동)', 'N'), + ('bd-op-033', NULL, '개선제안', 'BD 의견 33', '대량 데모 의견 본문 33.', 'OPEN', 'demo-exhibitor', '데모 참가업체(김참가)', 'N'), + ('bd-op-034', 'bd-e-035', '개선제안', 'BD 의견 34', '대량 데모 의견 본문 34.', 'ANSWERED', 'demo-contractor', '데모 공사업체(이시공)', 'N'), + ('bd-op-035', 'bd-e-036', '개선제안', 'BD 의견 35', '대량 데모 의견 본문 35.', 'CLOSED', 'demo-visitor', '데모 관람객(박관람)', 'N'), + ('bd-op-036', NULL, '개선제안', 'BD 의견 36', '대량 데모 의견 본문 36.', 'OPEN', 'demo-organizer', '데모 주최자(홍길동)', 'N'), + ('bd-op-037', 'bd-e-038', '개선제안', 'BD 의견 37', '대량 데모 의견 본문 37.', 'ANSWERED', 'demo-exhibitor', '데모 참가업체(김참가)', 'N'), + ('bd-op-038', 'bd-e-039', '개선제안', 'BD 의견 38', '대량 데모 의견 본문 38.', 'CLOSED', 'demo-contractor', '데모 공사업체(이시공)', 'N'), + ('bd-op-039', NULL, '개선제안', 'BD 의견 39', '대량 데모 의견 본문 39.', 'OPEN', 'demo-visitor', '데모 관람객(박관람)', 'N'), + ('bd-op-040', 'bd-e-041', '개선제안', 'BD 의견 40', '대량 데모 의견 본문 40.', 'ANSWERED', 'demo-organizer', '데모 주최자(홍길동)', 'N'), + ('bd-op-041', 'bd-e-042', '개선제안', 'BD 의견 41', '대량 데모 의견 본문 41.', 'CLOSED', 'demo-exhibitor', '데모 참가업체(김참가)', 'N'), + ('bd-op-042', NULL, '개선제안', 'BD 의견 42', '대량 데모 의견 본문 42.', 'OPEN', 'demo-contractor', '데모 공사업체(이시공)', 'N'), + ('bd-op-043', 'bd-e-044', '개선제안', 'BD 의견 43', '대량 데모 의견 본문 43.', 'ANSWERED', 'demo-visitor', '데모 관람객(박관람)', 'N'), + ('bd-op-044', 'bd-e-045', '개선제안', 'BD 의견 44', '대량 데모 의견 본문 44.', 'CLOSED', 'demo-organizer', '데모 주최자(홍길동)', 'N'), + ('bd-op-045', NULL, '개선제안', 'BD 의견 45', '대량 데모 의견 본문 45.', 'OPEN', 'demo-exhibitor', '데모 참가업체(김참가)', 'N'), + ('bd-op-046', 'bd-e-047', '개선제안', 'BD 의견 46', '대량 데모 의견 본문 46.', 'ANSWERED', 'demo-contractor', '데모 공사업체(이시공)', 'N'), + ('bd-op-047', 'bd-e-048', '개선제안', 'BD 의견 47', '대량 데모 의견 본문 47.', 'CLOSED', 'demo-visitor', '데모 관람객(박관람)', 'N'), + ('bd-op-048', NULL, '개선제안', 'BD 의견 48', '대량 데모 의견 본문 48.', 'OPEN', 'demo-organizer', '데모 주최자(홍길동)', 'N'), + ('bd-op-049', 'bd-e-050', '개선제안', 'BD 의견 49', '대량 데모 의견 본문 49.', 'ANSWERED', 'demo-exhibitor', '데모 참가업체(김참가)', 'N'), + ('bd-op-050', 'bd-e-051', '개선제안', 'BD 의견 50', '대량 데모 의견 본문 50.', 'CLOSED', 'demo-contractor', '데모 공사업체(이시공)', 'N') +ON CONFLICT (id) DO NOTHING; + +INSERT INTO opinion_comment (id, opinion_id, author_id, author_name, content) VALUES + ('bd-oc-002', 'bd-op-002', 'demo-organizer', '데모 주최자(홍길동)', '데모 답변 2.'), + ('bd-oc-004', 'bd-op-004', 'demo-organizer', '데모 주최자(홍길동)', '데모 답변 4.'), + ('bd-oc-006', 'bd-op-006', 'demo-organizer', '데모 주최자(홍길동)', '데모 답변 6.'), + ('bd-oc-008', 'bd-op-008', 'demo-organizer', '데모 주최자(홍길동)', '데모 답변 8.'), + ('bd-oc-010', 'bd-op-010', 'demo-organizer', '데모 주최자(홍길동)', '데모 답변 10.'), + ('bd-oc-012', 'bd-op-012', 'demo-organizer', '데모 주최자(홍길동)', '데모 답변 12.'), + ('bd-oc-014', 'bd-op-014', 'demo-organizer', '데모 주최자(홍길동)', '데모 답변 14.'), + ('bd-oc-016', 'bd-op-016', 'demo-organizer', '데모 주최자(홍길동)', '데모 답변 16.'), + ('bd-oc-018', 'bd-op-018', 'demo-organizer', '데모 주최자(홍길동)', '데모 답변 18.'), + ('bd-oc-020', 'bd-op-020', 'demo-organizer', '데모 주최자(홍길동)', '데모 답변 20.'), + ('bd-oc-022', 'bd-op-022', 'demo-organizer', '데모 주최자(홍길동)', '데모 답변 22.'), + ('bd-oc-024', 'bd-op-024', 'demo-organizer', '데모 주최자(홍길동)', '데모 답변 24.'), + ('bd-oc-026', 'bd-op-026', 'demo-organizer', '데모 주최자(홍길동)', '데모 답변 26.'), + ('bd-oc-028', 'bd-op-028', 'demo-organizer', '데모 주최자(홍길동)', '데모 답변 28.'), + ('bd-oc-030', 'bd-op-030', 'demo-organizer', '데모 주최자(홍길동)', '데모 답변 30.'), + ('bd-oc-032', 'bd-op-032', 'demo-organizer', '데모 주최자(홍길동)', '데모 답변 32.'), + ('bd-oc-034', 'bd-op-034', 'demo-organizer', '데모 주최자(홍길동)', '데모 답변 34.'), + ('bd-oc-036', 'bd-op-036', 'demo-organizer', '데모 주최자(홍길동)', '데모 답변 36.'), + ('bd-oc-038', 'bd-op-038', 'demo-organizer', '데모 주최자(홍길동)', '데모 답변 38.'), + ('bd-oc-040', 'bd-op-040', 'demo-organizer', '데모 주최자(홍길동)', '데모 답변 40.'), + ('bd-oc-042', 'bd-op-042', 'demo-organizer', '데모 주최자(홍길동)', '데모 답변 42.'), + ('bd-oc-044', 'bd-op-044', 'demo-organizer', '데모 주최자(홍길동)', '데모 답변 44.'), + ('bd-oc-046', 'bd-op-046', 'demo-organizer', '데모 주최자(홍길동)', '데모 답변 46.'), + ('bd-oc-048', 'bd-op-048', 'demo-organizer', '데모 주최자(홍길동)', '데모 답변 48.'), + ('bd-oc-050', 'bd-op-050', 'demo-organizer', '데모 주최자(홍길동)', '데모 답변 50.') +ON CONFLICT (id) DO NOTHING; + +-- 17. CMS 콘텐츠(cms_content) 60 — NOTICE/PAGE/POST 혼합, published 다수. +INSERT INTO cms_content (id, event_id, content_type, title, body, status, lang, signage, mailing, author_name, published_at) VALUES + ('bd-cms-001', 'bd-e-002', 'PAGE', 'BD 콘텐츠 1', '대량 데모 콘텐츠 본문 1.', 'published', 'ko', false, false, '주최 사무국', now() - interval '2 day'), + ('bd-cms-002', 'bd-e-003', 'POST', 'BD 콘텐츠 2', '대량 데모 콘텐츠 본문 2.', 'draft', 'ko', false, false, '주최 사무국', NULL), + ('bd-cms-003', NULL, 'BLOCK', 'BD 콘텐츠 3', '대량 데모 콘텐츠 본문 3.', 'review', 'ko', false, false, '주최 사무국', NULL), + ('bd-cms-004', 'bd-e-005', 'NOTICE', 'BD 콘텐츠 4', '대량 데모 콘텐츠 본문 4.', 'approved', 'ko', false, false, '주최 사무국', NULL), + ('bd-cms-005', 'bd-e-006', 'PAGE', 'BD 콘텐츠 5', '대량 데모 콘텐츠 본문 5.', 'published', 'ko', true, false, '주최 사무국', now() - interval '6 day'), + ('bd-cms-006', NULL, 'POST', 'BD 콘텐츠 6', '대량 데모 콘텐츠 본문 6.', 'published', 'ko', false, false, '주최 사무국', now() - interval '7 day'), + ('bd-cms-007', 'bd-e-008', 'BLOCK', 'BD 콘텐츠 7', '대량 데모 콘텐츠 본문 7.', 'draft', 'ko', false, true, '주최 사무국', NULL), + ('bd-cms-008', 'bd-e-009', 'NOTICE', 'BD 콘텐츠 8', '대량 데모 콘텐츠 본문 8.', 'review', 'ko', false, false, '주최 사무국', NULL), + ('bd-cms-009', NULL, 'PAGE', 'BD 콘텐츠 9', '대량 데모 콘텐츠 본문 9.', 'approved', 'ko', false, false, '주최 사무국', NULL), + ('bd-cms-010', 'bd-e-011', 'POST', 'BD 콘텐츠 10', '대량 데모 콘텐츠 본문 10.', 'published', 'ko', true, false, '주최 사무국', now() - interval '11 day'), + ('bd-cms-011', 'bd-e-012', 'BLOCK', 'BD 콘텐츠 11', '대량 데모 콘텐츠 본문 11.', 'published', 'ko', false, false, '주최 사무국', now() - interval '12 day'), + ('bd-cms-012', NULL, 'NOTICE', 'BD 콘텐츠 12', '대량 데모 콘텐츠 본문 12.', 'draft', 'ko', false, false, '주최 사무국', NULL), + ('bd-cms-013', 'bd-e-014', 'PAGE', 'BD 콘텐츠 13', '대량 데모 콘텐츠 본문 13.', 'review', 'ko', false, false, '주최 사무국', NULL), + ('bd-cms-014', 'bd-e-015', 'POST', 'BD 콘텐츠 14', '대량 데모 콘텐츠 본문 14.', 'approved', 'ko', false, true, '주최 사무국', NULL), + ('bd-cms-015', NULL, 'BLOCK', 'BD 콘텐츠 15', '대량 데모 콘텐츠 본문 15.', 'published', 'ko', true, false, '주최 사무국', now() - interval '16 day'), + ('bd-cms-016', 'bd-e-017', 'NOTICE', 'BD 콘텐츠 16', '대량 데모 콘텐츠 본문 16.', 'published', 'ko', false, false, '주최 사무국', now() - interval '17 day'), + ('bd-cms-017', 'bd-e-018', 'PAGE', 'BD 콘텐츠 17', '대량 데모 콘텐츠 본문 17.', 'draft', 'ko', false, false, '주최 사무국', NULL), + ('bd-cms-018', NULL, 'POST', 'BD 콘텐츠 18', '대량 데모 콘텐츠 본문 18.', 'review', 'ko', false, false, '주최 사무국', NULL), + ('bd-cms-019', 'bd-e-020', 'BLOCK', 'BD 콘텐츠 19', '대량 데모 콘텐츠 본문 19.', 'approved', 'ko', false, false, '주최 사무국', NULL), + ('bd-cms-020', 'bd-e-021', 'NOTICE', 'BD 콘텐츠 20', '대량 데모 콘텐츠 본문 20.', 'published', 'ko', true, false, '주최 사무국', now() - interval '1 day'), + ('bd-cms-021', NULL, 'PAGE', 'BD 콘텐츠 21', '대량 데모 콘텐츠 본문 21.', 'published', 'ko', false, true, '주최 사무국', now() - interval '2 day'), + ('bd-cms-022', 'bd-e-023', 'POST', 'BD 콘텐츠 22', '대량 데모 콘텐츠 본문 22.', 'draft', 'ko', false, false, '주최 사무국', NULL), + ('bd-cms-023', 'bd-e-024', 'BLOCK', 'BD 콘텐츠 23', '대량 데모 콘텐츠 본문 23.', 'review', 'ko', false, false, '주최 사무국', NULL), + ('bd-cms-024', NULL, 'NOTICE', 'BD 콘텐츠 24', '대량 데모 콘텐츠 본문 24.', 'approved', 'ko', false, false, '주최 사무국', NULL), + ('bd-cms-025', 'bd-e-026', 'PAGE', 'BD 콘텐츠 25', '대량 데모 콘텐츠 본문 25.', 'published', 'ko', true, false, '주최 사무국', now() - interval '6 day'), + ('bd-cms-026', 'bd-e-027', 'POST', 'BD 콘텐츠 26', '대량 데모 콘텐츠 본문 26.', 'published', 'ko', false, false, '주최 사무국', now() - interval '7 day'), + ('bd-cms-027', NULL, 'BLOCK', 'BD 콘텐츠 27', '대량 데모 콘텐츠 본문 27.', 'draft', 'ko', false, false, '주최 사무국', NULL), + ('bd-cms-028', 'bd-e-029', 'NOTICE', 'BD 콘텐츠 28', '대량 데모 콘텐츠 본문 28.', 'review', 'ko', false, true, '주최 사무국', NULL), + ('bd-cms-029', 'bd-e-030', 'PAGE', 'BD 콘텐츠 29', '대량 데모 콘텐츠 본문 29.', 'approved', 'ko', false, false, '주최 사무국', NULL), + ('bd-cms-030', NULL, 'POST', 'BD 콘텐츠 30', '대량 데모 콘텐츠 본문 30.', 'published', 'ko', true, false, '주최 사무국', now() - interval '11 day'), + ('bd-cms-031', 'bd-e-032', 'BLOCK', 'BD 콘텐츠 31', '대량 데모 콘텐츠 본문 31.', 'published', 'ko', false, false, '주최 사무국', now() - interval '12 day'), + ('bd-cms-032', 'bd-e-033', 'NOTICE', 'BD 콘텐츠 32', '대량 데모 콘텐츠 본문 32.', 'draft', 'ko', false, false, '주최 사무국', NULL), + ('bd-cms-033', NULL, 'PAGE', 'BD 콘텐츠 33', '대량 데모 콘텐츠 본문 33.', 'review', 'ko', false, false, '주최 사무국', NULL), + ('bd-cms-034', 'bd-e-035', 'POST', 'BD 콘텐츠 34', '대량 데모 콘텐츠 본문 34.', 'approved', 'ko', false, false, '주최 사무국', NULL), + ('bd-cms-035', 'bd-e-036', 'BLOCK', 'BD 콘텐츠 35', '대량 데모 콘텐츠 본문 35.', 'published', 'ko', true, true, '주최 사무국', now() - interval '16 day'), + ('bd-cms-036', NULL, 'NOTICE', 'BD 콘텐츠 36', '대량 데모 콘텐츠 본문 36.', 'published', 'ko', false, false, '주최 사무국', now() - interval '17 day'), + ('bd-cms-037', 'bd-e-038', 'PAGE', 'BD 콘텐츠 37', '대량 데모 콘텐츠 본문 37.', 'draft', 'ko', false, false, '주최 사무국', NULL), + ('bd-cms-038', 'bd-e-039', 'POST', 'BD 콘텐츠 38', '대량 데모 콘텐츠 본문 38.', 'review', 'ko', false, false, '주최 사무국', NULL), + ('bd-cms-039', NULL, 'BLOCK', 'BD 콘텐츠 39', '대량 데모 콘텐츠 본문 39.', 'approved', 'ko', false, false, '주최 사무국', NULL), + ('bd-cms-040', 'bd-e-041', 'NOTICE', 'BD 콘텐츠 40', '대량 데모 콘텐츠 본문 40.', 'published', 'ko', true, false, '주최 사무국', now() - interval '1 day'), + ('bd-cms-041', 'bd-e-042', 'PAGE', 'BD 콘텐츠 41', '대량 데모 콘텐츠 본문 41.', 'published', 'ko', false, false, '주최 사무국', now() - interval '2 day'), + ('bd-cms-042', NULL, 'POST', 'BD 콘텐츠 42', '대량 데모 콘텐츠 본문 42.', 'draft', 'ko', false, true, '주최 사무국', NULL), + ('bd-cms-043', 'bd-e-044', 'BLOCK', 'BD 콘텐츠 43', '대량 데모 콘텐츠 본문 43.', 'review', 'ko', false, false, '주최 사무국', NULL), + ('bd-cms-044', 'bd-e-045', 'NOTICE', 'BD 콘텐츠 44', '대량 데모 콘텐츠 본문 44.', 'approved', 'ko', false, false, '주최 사무국', NULL), + ('bd-cms-045', NULL, 'PAGE', 'BD 콘텐츠 45', '대량 데모 콘텐츠 본문 45.', 'published', 'ko', true, false, '주최 사무국', now() - interval '6 day'), + ('bd-cms-046', 'bd-e-047', 'POST', 'BD 콘텐츠 46', '대량 데모 콘텐츠 본문 46.', 'published', 'ko', false, false, '주최 사무국', now() - interval '7 day'), + ('bd-cms-047', 'bd-e-048', 'BLOCK', 'BD 콘텐츠 47', '대량 데모 콘텐츠 본문 47.', 'draft', 'ko', false, false, '주최 사무국', NULL), + ('bd-cms-048', NULL, 'NOTICE', 'BD 콘텐츠 48', '대량 데모 콘텐츠 본문 48.', 'review', 'ko', false, false, '주최 사무국', NULL), + ('bd-cms-049', 'bd-e-050', 'PAGE', 'BD 콘텐츠 49', '대량 데모 콘텐츠 본문 49.', 'approved', 'ko', false, true, '주최 사무국', NULL), + ('bd-cms-050', 'bd-e-051', 'POST', 'BD 콘텐츠 50', '대량 데모 콘텐츠 본문 50.', 'published', 'ko', true, false, '주최 사무국', now() - interval '11 day'), + ('bd-cms-051', NULL, 'BLOCK', 'BD 콘텐츠 51', '대량 데모 콘텐츠 본문 51.', 'published', 'ko', false, false, '주최 사무국', now() - interval '12 day'), + ('bd-cms-052', 'bd-e-053', 'NOTICE', 'BD 콘텐츠 52', '대량 데모 콘텐츠 본문 52.', 'draft', 'ko', false, false, '주최 사무국', NULL), + ('bd-cms-053', 'bd-e-054', 'PAGE', 'BD 콘텐츠 53', '대량 데모 콘텐츠 본문 53.', 'review', 'ko', false, false, '주최 사무국', NULL), + ('bd-cms-054', NULL, 'POST', 'BD 콘텐츠 54', '대량 데모 콘텐츠 본문 54.', 'approved', 'ko', false, false, '주최 사무국', NULL), + ('bd-cms-055', 'bd-e-056', 'BLOCK', 'BD 콘텐츠 55', '대량 데모 콘텐츠 본문 55.', 'published', 'ko', true, false, '주최 사무국', now() - interval '16 day'), + ('bd-cms-056', 'bd-e-057', 'NOTICE', 'BD 콘텐츠 56', '대량 데모 콘텐츠 본문 56.', 'published', 'ko', false, true, '주최 사무국', now() - interval '17 day'), + ('bd-cms-057', NULL, 'PAGE', 'BD 콘텐츠 57', '대량 데모 콘텐츠 본문 57.', 'draft', 'ko', false, false, '주최 사무국', NULL), + ('bd-cms-058', 'bd-e-059', 'POST', 'BD 콘텐츠 58', '대량 데모 콘텐츠 본문 58.', 'review', 'ko', false, false, '주최 사무국', NULL), + ('bd-cms-059', 'bd-e-060', 'BLOCK', 'BD 콘텐츠 59', '대량 데모 콘텐츠 본문 59.', 'approved', 'ko', false, false, '주최 사무국', NULL), + ('bd-cms-060', NULL, 'NOTICE', 'BD 콘텐츠 60', '대량 데모 콘텐츠 본문 60.', 'published', 'ko', true, false, '주최 사무국', now() - interval '1 day') +ON CONFLICT (id) DO NOTHING; + +-- 18. 필수 서류(required_document) — 12개 행사 × 6 doc_type = 72. 유니크(event_id, doc_type). +INSERT INTO required_document (id, event_id, doc_type, name, status, due_date, sort_order) VALUES + ('bd-doc-001', 'bd-e-001', 'operation_plan', '행사운영계획서', 'approved', NULL, 0), + ('bd-doc-002', 'bd-e-001', 'booth_layout', '부스배치도', 'submitted', CURRENT_DATE + 0, 1), + ('bd-doc-003', 'bd-e-001', 'disaster_plan', '재해대처계획서', 'draft', CURRENT_DATE + 2, 2), + ('bd-doc-004', 'bd-e-001', 'rigging_calc', '리깅 구조계산서', 'pending', CURRENT_DATE + 4, 3), + ('bd-doc-005', 'bd-e-001', 'fire_safety', '방화관리 책임서약서', 'rejected', NULL, 4), + ('bd-doc-006', 'bd-e-001', 'elec_safety', '전기안전 점검확인서', 'approved', NULL, 5), + ('bd-doc-007', 'bd-e-002', 'operation_plan', '행사운영계획서', 'submitted', CURRENT_DATE + -2, 0), + ('bd-doc-008', 'bd-e-002', 'booth_layout', '부스배치도', 'draft', CURRENT_DATE + 0, 1), + ('bd-doc-009', 'bd-e-002', 'disaster_plan', '재해대처계획서', 'pending', CURRENT_DATE + 2, 2), + ('bd-doc-010', 'bd-e-002', 'rigging_calc', '리깅 구조계산서', 'rejected', NULL, 3), + ('bd-doc-011', 'bd-e-002', 'fire_safety', '방화관리 책임서약서', 'approved', NULL, 4), + ('bd-doc-012', 'bd-e-002', 'elec_safety', '전기안전 점검확인서', 'submitted', CURRENT_DATE + 8, 5), + ('bd-doc-013', 'bd-e-003', 'operation_plan', '행사운영계획서', 'draft', CURRENT_DATE + -2, 0), + ('bd-doc-014', 'bd-e-003', 'booth_layout', '부스배치도', 'pending', CURRENT_DATE + 0, 1), + ('bd-doc-015', 'bd-e-003', 'disaster_plan', '재해대처계획서', 'rejected', NULL, 2), + ('bd-doc-016', 'bd-e-003', 'rigging_calc', '리깅 구조계산서', 'approved', NULL, 3), + ('bd-doc-017', 'bd-e-003', 'fire_safety', '방화관리 책임서약서', 'submitted', CURRENT_DATE + 6, 4), + ('bd-doc-018', 'bd-e-003', 'elec_safety', '전기안전 점검확인서', 'draft', CURRENT_DATE + 8, 5), + ('bd-doc-019', 'bd-e-004', 'operation_plan', '행사운영계획서', 'pending', CURRENT_DATE + -2, 0), + ('bd-doc-020', 'bd-e-004', 'booth_layout', '부스배치도', 'rejected', NULL, 1), + ('bd-doc-021', 'bd-e-004', 'disaster_plan', '재해대처계획서', 'approved', NULL, 2), + ('bd-doc-022', 'bd-e-004', 'rigging_calc', '리깅 구조계산서', 'submitted', CURRENT_DATE + 4, 3), + ('bd-doc-023', 'bd-e-004', 'fire_safety', '방화관리 책임서약서', 'draft', CURRENT_DATE + 6, 4), + ('bd-doc-024', 'bd-e-004', 'elec_safety', '전기안전 점검확인서', 'pending', CURRENT_DATE + 8, 5), + ('bd-doc-025', 'bd-e-005', 'operation_plan', '행사운영계획서', 'rejected', NULL, 0), + ('bd-doc-026', 'bd-e-005', 'booth_layout', '부스배치도', 'approved', NULL, 1), + ('bd-doc-027', 'bd-e-005', 'disaster_plan', '재해대처계획서', 'submitted', CURRENT_DATE + 2, 2), + ('bd-doc-028', 'bd-e-005', 'rigging_calc', '리깅 구조계산서', 'draft', CURRENT_DATE + 4, 3), + ('bd-doc-029', 'bd-e-005', 'fire_safety', '방화관리 책임서약서', 'pending', CURRENT_DATE + 6, 4), + ('bd-doc-030', 'bd-e-005', 'elec_safety', '전기안전 점검확인서', 'rejected', NULL, 5), + ('bd-doc-031', 'bd-e-006', 'operation_plan', '행사운영계획서', 'approved', NULL, 0), + ('bd-doc-032', 'bd-e-006', 'booth_layout', '부스배치도', 'submitted', CURRENT_DATE + 0, 1), + ('bd-doc-033', 'bd-e-006', 'disaster_plan', '재해대처계획서', 'draft', CURRENT_DATE + 2, 2), + ('bd-doc-034', 'bd-e-006', 'rigging_calc', '리깅 구조계산서', 'pending', CURRENT_DATE + 4, 3), + ('bd-doc-035', 'bd-e-006', 'fire_safety', '방화관리 책임서약서', 'rejected', NULL, 4), + ('bd-doc-036', 'bd-e-006', 'elec_safety', '전기안전 점검확인서', 'approved', NULL, 5), + ('bd-doc-037', 'bd-e-007', 'operation_plan', '행사운영계획서', 'submitted', CURRENT_DATE + -2, 0), + ('bd-doc-038', 'bd-e-007', 'booth_layout', '부스배치도', 'draft', CURRENT_DATE + 0, 1), + ('bd-doc-039', 'bd-e-007', 'disaster_plan', '재해대처계획서', 'pending', CURRENT_DATE + 2, 2), + ('bd-doc-040', 'bd-e-007', 'rigging_calc', '리깅 구조계산서', 'rejected', NULL, 3), + ('bd-doc-041', 'bd-e-007', 'fire_safety', '방화관리 책임서약서', 'approved', NULL, 4), + ('bd-doc-042', 'bd-e-007', 'elec_safety', '전기안전 점검확인서', 'submitted', CURRENT_DATE + 8, 5), + ('bd-doc-043', 'bd-e-008', 'operation_plan', '행사운영계획서', 'draft', CURRENT_DATE + -2, 0), + ('bd-doc-044', 'bd-e-008', 'booth_layout', '부스배치도', 'pending', CURRENT_DATE + 0, 1), + ('bd-doc-045', 'bd-e-008', 'disaster_plan', '재해대처계획서', 'rejected', NULL, 2), + ('bd-doc-046', 'bd-e-008', 'rigging_calc', '리깅 구조계산서', 'approved', NULL, 3), + ('bd-doc-047', 'bd-e-008', 'fire_safety', '방화관리 책임서약서', 'submitted', CURRENT_DATE + 6, 4), + ('bd-doc-048', 'bd-e-008', 'elec_safety', '전기안전 점검확인서', 'draft', CURRENT_DATE + 8, 5), + ('bd-doc-049', 'bd-e-009', 'operation_plan', '행사운영계획서', 'pending', CURRENT_DATE + -2, 0), + ('bd-doc-050', 'bd-e-009', 'booth_layout', '부스배치도', 'rejected', NULL, 1), + ('bd-doc-051', 'bd-e-009', 'disaster_plan', '재해대처계획서', 'approved', NULL, 2), + ('bd-doc-052', 'bd-e-009', 'rigging_calc', '리깅 구조계산서', 'submitted', CURRENT_DATE + 4, 3), + ('bd-doc-053', 'bd-e-009', 'fire_safety', '방화관리 책임서약서', 'draft', CURRENT_DATE + 6, 4), + ('bd-doc-054', 'bd-e-009', 'elec_safety', '전기안전 점검확인서', 'pending', CURRENT_DATE + 8, 5), + ('bd-doc-055', 'bd-e-010', 'operation_plan', '행사운영계획서', 'rejected', NULL, 0), + ('bd-doc-056', 'bd-e-010', 'booth_layout', '부스배치도', 'approved', NULL, 1), + ('bd-doc-057', 'bd-e-010', 'disaster_plan', '재해대처계획서', 'submitted', CURRENT_DATE + 2, 2), + ('bd-doc-058', 'bd-e-010', 'rigging_calc', '리깅 구조계산서', 'draft', CURRENT_DATE + 4, 3), + ('bd-doc-059', 'bd-e-010', 'fire_safety', '방화관리 책임서약서', 'pending', CURRENT_DATE + 6, 4), + ('bd-doc-060', 'bd-e-010', 'elec_safety', '전기안전 점검확인서', 'rejected', NULL, 5), + ('bd-doc-061', 'bd-e-011', 'operation_plan', '행사운영계획서', 'approved', NULL, 0), + ('bd-doc-062', 'bd-e-011', 'booth_layout', '부스배치도', 'submitted', CURRENT_DATE + 0, 1), + ('bd-doc-063', 'bd-e-011', 'disaster_plan', '재해대처계획서', 'draft', CURRENT_DATE + 2, 2), + ('bd-doc-064', 'bd-e-011', 'rigging_calc', '리깅 구조계산서', 'pending', CURRENT_DATE + 4, 3), + ('bd-doc-065', 'bd-e-011', 'fire_safety', '방화관리 책임서약서', 'rejected', NULL, 4), + ('bd-doc-066', 'bd-e-011', 'elec_safety', '전기안전 점검확인서', 'approved', NULL, 5), + ('bd-doc-067', 'bd-e-012', 'operation_plan', '행사운영계획서', 'submitted', CURRENT_DATE + -2, 0), + ('bd-doc-068', 'bd-e-012', 'booth_layout', '부스배치도', 'draft', CURRENT_DATE + 0, 1), + ('bd-doc-069', 'bd-e-012', 'disaster_plan', '재해대처계획서', 'pending', CURRENT_DATE + 2, 2), + ('bd-doc-070', 'bd-e-012', 'rigging_calc', '리깅 구조계산서', 'rejected', NULL, 3), + ('bd-doc-071', 'bd-e-012', 'fire_safety', '방화관리 책임서약서', 'approved', NULL, 4), + ('bd-doc-072', 'bd-e-012', 'elec_safety', '전기안전 점검확인서', 'submitted', CURRENT_DATE + 8, 5) +ON CONFLICT (event_id, doc_type) DO NOTHING; + +-- 19. 마일스톤(event_milestone) — 12개 행사 × 5 = 60. 유니크(event_id, milestone_type). +INSERT INTO event_milestone (id, event_id, milestone_type, label, due_date, state, sort_order) VALUES + ('bd-ms-001', 'bd-e-001', 'assignment', 'D-150 배정', CURRENT_DATE - 80, 'done', 0), + ('bd-ms-002', 'bd-e-001', 'pre_review', 'D-30 사전협의', CURRENT_DATE - 60, 'done', 1), + ('bd-ms-003', 'bd-e-001', 'utility', 'D-25 유틸리티', CURRENT_DATE - 40, 'done', 2), + ('bd-ms-004', 'bd-e-001', 'documents', 'D-7 신고서류', CURRENT_DATE - 20, 'active', 3), + ('bd-ms-005', 'bd-e-001', 'opening', 'D-0 개장', CURRENT_DATE - 0, 'todo', 4), + ('bd-ms-006', 'bd-e-002', 'assignment', 'D-150 배정', CURRENT_DATE - 80, 'done', 0), + ('bd-ms-007', 'bd-e-002', 'pre_review', 'D-30 사전협의', CURRENT_DATE - 60, 'done', 1), + ('bd-ms-008', 'bd-e-002', 'utility', 'D-25 유틸리티', CURRENT_DATE - 40, 'done', 2), + ('bd-ms-009', 'bd-e-002', 'documents', 'D-7 신고서류', CURRENT_DATE - 20, 'active', 3), + ('bd-ms-010', 'bd-e-002', 'opening', 'D-0 개장', CURRENT_DATE - 0, 'todo', 4), + ('bd-ms-011', 'bd-e-003', 'assignment', 'D-150 배정', CURRENT_DATE - 80, 'done', 0), + ('bd-ms-012', 'bd-e-003', 'pre_review', 'D-30 사전협의', CURRENT_DATE - 60, 'done', 1), + ('bd-ms-013', 'bd-e-003', 'utility', 'D-25 유틸리티', CURRENT_DATE - 40, 'done', 2), + ('bd-ms-014', 'bd-e-003', 'documents', 'D-7 신고서류', CURRENT_DATE - 20, 'active', 3), + ('bd-ms-015', 'bd-e-003', 'opening', 'D-0 개장', CURRENT_DATE - 0, 'todo', 4), + ('bd-ms-016', 'bd-e-004', 'assignment', 'D-150 배정', CURRENT_DATE - 80, 'done', 0), + ('bd-ms-017', 'bd-e-004', 'pre_review', 'D-30 사전협의', CURRENT_DATE - 60, 'done', 1), + ('bd-ms-018', 'bd-e-004', 'utility', 'D-25 유틸리티', CURRENT_DATE - 40, 'done', 2), + ('bd-ms-019', 'bd-e-004', 'documents', 'D-7 신고서류', CURRENT_DATE - 20, 'active', 3), + ('bd-ms-020', 'bd-e-004', 'opening', 'D-0 개장', CURRENT_DATE - 0, 'todo', 4), + ('bd-ms-021', 'bd-e-005', 'assignment', 'D-150 배정', CURRENT_DATE - 80, 'done', 0), + ('bd-ms-022', 'bd-e-005', 'pre_review', 'D-30 사전협의', CURRENT_DATE - 60, 'done', 1), + ('bd-ms-023', 'bd-e-005', 'utility', 'D-25 유틸리티', CURRENT_DATE - 40, 'done', 2), + ('bd-ms-024', 'bd-e-005', 'documents', 'D-7 신고서류', CURRENT_DATE - 20, 'active', 3), + ('bd-ms-025', 'bd-e-005', 'opening', 'D-0 개장', CURRENT_DATE - 0, 'todo', 4), + ('bd-ms-026', 'bd-e-006', 'assignment', 'D-150 배정', CURRENT_DATE - 80, 'done', 0), + ('bd-ms-027', 'bd-e-006', 'pre_review', 'D-30 사전협의', CURRENT_DATE - 60, 'done', 1), + ('bd-ms-028', 'bd-e-006', 'utility', 'D-25 유틸리티', CURRENT_DATE - 40, 'done', 2), + ('bd-ms-029', 'bd-e-006', 'documents', 'D-7 신고서류', CURRENT_DATE - 20, 'active', 3), + ('bd-ms-030', 'bd-e-006', 'opening', 'D-0 개장', CURRENT_DATE - 0, 'todo', 4), + ('bd-ms-031', 'bd-e-007', 'assignment', 'D-150 배정', CURRENT_DATE - 80, 'done', 0), + ('bd-ms-032', 'bd-e-007', 'pre_review', 'D-30 사전협의', CURRENT_DATE - 60, 'done', 1), + ('bd-ms-033', 'bd-e-007', 'utility', 'D-25 유틸리티', CURRENT_DATE - 40, 'done', 2), + ('bd-ms-034', 'bd-e-007', 'documents', 'D-7 신고서류', CURRENT_DATE - 20, 'active', 3), + ('bd-ms-035', 'bd-e-007', 'opening', 'D-0 개장', CURRENT_DATE - 0, 'todo', 4), + ('bd-ms-036', 'bd-e-008', 'assignment', 'D-150 배정', CURRENT_DATE - 80, 'done', 0), + ('bd-ms-037', 'bd-e-008', 'pre_review', 'D-30 사전협의', CURRENT_DATE - 60, 'done', 1), + ('bd-ms-038', 'bd-e-008', 'utility', 'D-25 유틸리티', CURRENT_DATE - 40, 'done', 2), + ('bd-ms-039', 'bd-e-008', 'documents', 'D-7 신고서류', CURRENT_DATE - 20, 'active', 3), + ('bd-ms-040', 'bd-e-008', 'opening', 'D-0 개장', CURRENT_DATE - 0, 'todo', 4), + ('bd-ms-041', 'bd-e-009', 'assignment', 'D-150 배정', CURRENT_DATE - 80, 'done', 0), + ('bd-ms-042', 'bd-e-009', 'pre_review', 'D-30 사전협의', CURRENT_DATE - 60, 'done', 1), + ('bd-ms-043', 'bd-e-009', 'utility', 'D-25 유틸리티', CURRENT_DATE - 40, 'done', 2), + ('bd-ms-044', 'bd-e-009', 'documents', 'D-7 신고서류', CURRENT_DATE - 20, 'active', 3), + ('bd-ms-045', 'bd-e-009', 'opening', 'D-0 개장', CURRENT_DATE - 0, 'todo', 4), + ('bd-ms-046', 'bd-e-010', 'assignment', 'D-150 배정', CURRENT_DATE - 80, 'done', 0), + ('bd-ms-047', 'bd-e-010', 'pre_review', 'D-30 사전협의', CURRENT_DATE - 60, 'done', 1), + ('bd-ms-048', 'bd-e-010', 'utility', 'D-25 유틸리티', CURRENT_DATE - 40, 'done', 2), + ('bd-ms-049', 'bd-e-010', 'documents', 'D-7 신고서류', CURRENT_DATE - 20, 'active', 3), + ('bd-ms-050', 'bd-e-010', 'opening', 'D-0 개장', CURRENT_DATE - 0, 'todo', 4), + ('bd-ms-051', 'bd-e-011', 'assignment', 'D-150 배정', CURRENT_DATE - 80, 'done', 0), + ('bd-ms-052', 'bd-e-011', 'pre_review', 'D-30 사전협의', CURRENT_DATE - 60, 'done', 1), + ('bd-ms-053', 'bd-e-011', 'utility', 'D-25 유틸리티', CURRENT_DATE - 40, 'done', 2), + ('bd-ms-054', 'bd-e-011', 'documents', 'D-7 신고서류', CURRENT_DATE - 20, 'active', 3), + ('bd-ms-055', 'bd-e-011', 'opening', 'D-0 개장', CURRENT_DATE - 0, 'todo', 4), + ('bd-ms-056', 'bd-e-012', 'assignment', 'D-150 배정', CURRENT_DATE - 80, 'done', 0), + ('bd-ms-057', 'bd-e-012', 'pre_review', 'D-30 사전협의', CURRENT_DATE - 60, 'done', 1), + ('bd-ms-058', 'bd-e-012', 'utility', 'D-25 유틸리티', CURRENT_DATE - 40, 'done', 2), + ('bd-ms-059', 'bd-e-012', 'documents', 'D-7 신고서류', CURRENT_DATE - 20, 'active', 3), + ('bd-ms-060', 'bd-e-012', 'opening', 'D-0 개장', CURRENT_DATE - 0, 'todo', 4) +ON CONFLICT (event_id, milestone_type) DO NOTHING; + +-- 20. EDM 캠페인(edm_campaign) 50. +INSERT INTO edm_campaign (id, event_id, name, status, audience, meta, open_rate, click_rate) VALUES + ('bd-cp-001', 'bd-e-002', 'BD 캠페인 1', 'scheduled', 100, '대량 데모 캠페인', '21%', '3%'), + ('bd-cp-002', 'bd-e-003', 'BD 캠페인 2', 'sending', 200, '대량 데모 캠페인', '22%', '4%'), + ('bd-cp-003', 'bd-e-004', 'BD 캠페인 3', 'done', 300, '대량 데모 캠페인', '23%', '5%'), + ('bd-cp-004', 'bd-e-005', 'BD 캠페인 4', 'draft', 400, '대량 데모 캠페인', '24%', '6%'), + ('bd-cp-005', 'bd-e-006', 'BD 캠페인 5', 'scheduled', 500, '대량 데모 캠페인', '25%', '7%'), + ('bd-cp-006', 'bd-e-007', 'BD 캠페인 6', 'sending', 600, '대량 데모 캠페인', '26%', '8%'), + ('bd-cp-007', 'bd-e-008', 'BD 캠페인 7', 'done', 700, '대량 데모 캠페인', '27%', '9%'), + ('bd-cp-008', 'bd-e-009', 'BD 캠페인 8', 'draft', 800, '대량 데모 캠페인', '28%', '10%'), + ('bd-cp-009', 'bd-e-010', 'BD 캠페인 9', 'scheduled', 900, '대량 데모 캠페인', '29%', '11%'), + ('bd-cp-010', 'bd-e-011', 'BD 캠페인 10', 'sending', 1000, '대량 데모 캠페인', '30%', '2%'), + ('bd-cp-011', 'bd-e-012', 'BD 캠페인 11', 'done', 1100, '대량 데모 캠페인', '31%', '3%'), + ('bd-cp-012', 'bd-e-013', 'BD 캠페인 12', 'draft', 1200, '대량 데모 캠페인', '32%', '4%'), + ('bd-cp-013', 'bd-e-014', 'BD 캠페인 13', 'scheduled', 1300, '대량 데모 캠페인', '33%', '5%'), + ('bd-cp-014', 'bd-e-015', 'BD 캠페인 14', 'sending', 1400, '대량 데모 캠페인', '34%', '6%'), + ('bd-cp-015', 'bd-e-016', 'BD 캠페인 15', 'done', 1500, '대량 데모 캠페인', '35%', '7%'), + ('bd-cp-016', 'bd-e-017', 'BD 캠페인 16', 'draft', 1600, '대량 데모 캠페인', '36%', '8%'), + ('bd-cp-017', 'bd-e-018', 'BD 캠페인 17', 'scheduled', 1700, '대량 데모 캠페인', '37%', '9%'), + ('bd-cp-018', 'bd-e-019', 'BD 캠페인 18', 'sending', 1800, '대량 데모 캠페인', '38%', '10%'), + ('bd-cp-019', 'bd-e-020', 'BD 캠페인 19', 'done', 1900, '대량 데모 캠페인', '39%', '11%'), + ('bd-cp-020', 'bd-e-021', 'BD 캠페인 20', 'draft', 2000, '대량 데모 캠페인', '40%', '2%'), + ('bd-cp-021', 'bd-e-022', 'BD 캠페인 21', 'scheduled', 2100, '대량 데모 캠페인', '41%', '3%'), + ('bd-cp-022', 'bd-e-023', 'BD 캠페인 22', 'sending', 2200, '대량 데모 캠페인', '42%', '4%'), + ('bd-cp-023', 'bd-e-024', 'BD 캠페인 23', 'done', 2300, '대량 데모 캠페인', '43%', '5%'), + ('bd-cp-024', 'bd-e-025', 'BD 캠페인 24', 'draft', 2400, '대량 데모 캠페인', '44%', '6%'), + ('bd-cp-025', 'bd-e-026', 'BD 캠페인 25', 'scheduled', 2500, '대량 데모 캠페인', '45%', '7%'), + ('bd-cp-026', 'bd-e-027', 'BD 캠페인 26', 'sending', 2600, '대량 데모 캠페인', '46%', '8%'), + ('bd-cp-027', 'bd-e-028', 'BD 캠페인 27', 'done', 2700, '대량 데모 캠페인', '47%', '9%'), + ('bd-cp-028', 'bd-e-029', 'BD 캠페인 28', 'draft', 2800, '대량 데모 캠페인', '48%', '10%'), + ('bd-cp-029', 'bd-e-030', 'BD 캠페인 29', 'scheduled', 2900, '대량 데모 캠페인', '49%', '11%'), + ('bd-cp-030', 'bd-e-031', 'BD 캠페인 30', 'sending', 3000, '대량 데모 캠페인', '20%', '2%'), + ('bd-cp-031', 'bd-e-032', 'BD 캠페인 31', 'done', 3100, '대량 데모 캠페인', '21%', '3%'), + ('bd-cp-032', 'bd-e-033', 'BD 캠페인 32', 'draft', 3200, '대량 데모 캠페인', '22%', '4%'), + ('bd-cp-033', 'bd-e-034', 'BD 캠페인 33', 'scheduled', 3300, '대량 데모 캠페인', '23%', '5%'), + ('bd-cp-034', 'bd-e-035', 'BD 캠페인 34', 'sending', 3400, '대량 데모 캠페인', '24%', '6%'), + ('bd-cp-035', 'bd-e-036', 'BD 캠페인 35', 'done', 3500, '대량 데모 캠페인', '25%', '7%'), + ('bd-cp-036', 'bd-e-037', 'BD 캠페인 36', 'draft', 3600, '대량 데모 캠페인', '26%', '8%'), + ('bd-cp-037', 'bd-e-038', 'BD 캠페인 37', 'scheduled', 3700, '대량 데모 캠페인', '27%', '9%'), + ('bd-cp-038', 'bd-e-039', 'BD 캠페인 38', 'sending', 3800, '대량 데모 캠페인', '28%', '10%'), + ('bd-cp-039', 'bd-e-040', 'BD 캠페인 39', 'done', 3900, '대량 데모 캠페인', '29%', '11%'), + ('bd-cp-040', 'bd-e-041', 'BD 캠페인 40', 'draft', 4000, '대량 데모 캠페인', '30%', '2%'), + ('bd-cp-041', 'bd-e-042', 'BD 캠페인 41', 'scheduled', 4100, '대량 데모 캠페인', '31%', '3%'), + ('bd-cp-042', 'bd-e-043', 'BD 캠페인 42', 'sending', 4200, '대량 데모 캠페인', '32%', '4%'), + ('bd-cp-043', 'bd-e-044', 'BD 캠페인 43', 'done', 4300, '대량 데모 캠페인', '33%', '5%'), + ('bd-cp-044', 'bd-e-045', 'BD 캠페인 44', 'draft', 4400, '대량 데모 캠페인', '34%', '6%'), + ('bd-cp-045', 'bd-e-046', 'BD 캠페인 45', 'scheduled', 4500, '대량 데모 캠페인', '35%', '7%'), + ('bd-cp-046', 'bd-e-047', 'BD 캠페인 46', 'sending', 4600, '대량 데모 캠페인', '36%', '8%'), + ('bd-cp-047', 'bd-e-048', 'BD 캠페인 47', 'done', 4700, '대량 데모 캠페인', '37%', '9%'), + ('bd-cp-048', 'bd-e-049', 'BD 캠페인 48', 'draft', 4800, '대량 데모 캠페인', '38%', '10%'), + ('bd-cp-049', 'bd-e-050', 'BD 캠페인 49', 'scheduled', 4900, '대량 데모 캠페인', '39%', '11%'), + ('bd-cp-050', 'bd-e-051', 'BD 캠페인 50', 'sending', 5000, '대량 데모 캠페인', '40%', '2%') +ON CONFLICT (id) DO NOTHING; + +-- 21. 스폰서십 패키지(sponsorship_package) 50. +INSERT INTO sponsorship_package (id, event_id, code, name, name_ko, price, accent, benefits, total_qty, sold_qty, dday, sort_order) VALUES + ('bd-sp-001', 'bd-e-002', 'gold', 'GOLD', '골드', 30000000, '#B8860B', '["로고 노출","부스 우선 배정"]'::jsonb, 3, 1, 1, 1), + ('bd-sp-002', 'bd-e-003', 'silver', 'SILVER', '실버', 15000000, '#B8860B', '["로고 노출","부스 우선 배정"]'::jsonb, 3, 2, 2, 2), + ('bd-sp-003', 'bd-e-004', 'diamond', 'DIAMOND', '다이아몬드', 50000000, '#B8860B', '["로고 노출","부스 우선 배정"]'::jsonb, 3, 0, 3, 0), + ('bd-sp-004', 'bd-e-005', 'gold', 'GOLD', '골드', 30000000, '#B8860B', '["로고 노출","부스 우선 배정"]'::jsonb, 3, 1, 4, 1), + ('bd-sp-005', 'bd-e-006', 'silver', 'SILVER', '실버', 15000000, '#B8860B', '["로고 노출","부스 우선 배정"]'::jsonb, 3, 2, 5, 2), + ('bd-sp-006', 'bd-e-007', 'diamond', 'DIAMOND', '다이아몬드', 50000000, '#B8860B', '["로고 노출","부스 우선 배정"]'::jsonb, 3, 0, 6, 0), + ('bd-sp-007', 'bd-e-008', 'gold', 'GOLD', '골드', 30000000, '#B8860B', '["로고 노출","부스 우선 배정"]'::jsonb, 3, 1, 7, 1), + ('bd-sp-008', 'bd-e-009', 'silver', 'SILVER', '실버', 15000000, '#B8860B', '["로고 노출","부스 우선 배정"]'::jsonb, 3, 2, 8, 2), + ('bd-sp-009', 'bd-e-010', 'diamond', 'DIAMOND', '다이아몬드', 50000000, '#B8860B', '["로고 노출","부스 우선 배정"]'::jsonb, 3, 0, 9, 0), + ('bd-sp-010', 'bd-e-011', 'gold', 'GOLD', '골드', 30000000, '#B8860B', '["로고 노출","부스 우선 배정"]'::jsonb, 3, 1, 10, 1), + ('bd-sp-011', 'bd-e-012', 'silver', 'SILVER', '실버', 15000000, '#B8860B', '["로고 노출","부스 우선 배정"]'::jsonb, 3, 2, 11, 2), + ('bd-sp-012', 'bd-e-013', 'diamond', 'DIAMOND', '다이아몬드', 50000000, '#B8860B', '["로고 노출","부스 우선 배정"]'::jsonb, 3, 0, 12, 0), + ('bd-sp-013', 'bd-e-014', 'gold', 'GOLD', '골드', 30000000, '#B8860B', '["로고 노출","부스 우선 배정"]'::jsonb, 3, 1, 13, 1), + ('bd-sp-014', 'bd-e-015', 'silver', 'SILVER', '실버', 15000000, '#B8860B', '["로고 노출","부스 우선 배정"]'::jsonb, 3, 2, 14, 2), + ('bd-sp-015', 'bd-e-016', 'diamond', 'DIAMOND', '다이아몬드', 50000000, '#B8860B', '["로고 노출","부스 우선 배정"]'::jsonb, 3, 0, 15, 0), + ('bd-sp-016', 'bd-e-017', 'gold', 'GOLD', '골드', 30000000, '#B8860B', '["로고 노출","부스 우선 배정"]'::jsonb, 3, 1, 16, 1), + ('bd-sp-017', 'bd-e-018', 'silver', 'SILVER', '실버', 15000000, '#B8860B', '["로고 노출","부스 우선 배정"]'::jsonb, 3, 2, 17, 2), + ('bd-sp-018', 'bd-e-019', 'diamond', 'DIAMOND', '다이아몬드', 50000000, '#B8860B', '["로고 노출","부스 우선 배정"]'::jsonb, 3, 0, 18, 0), + ('bd-sp-019', 'bd-e-020', 'gold', 'GOLD', '골드', 30000000, '#B8860B', '["로고 노출","부스 우선 배정"]'::jsonb, 3, 1, 19, 1), + ('bd-sp-020', 'bd-e-021', 'silver', 'SILVER', '실버', 15000000, '#B8860B', '["로고 노출","부스 우선 배정"]'::jsonb, 3, 2, 0, 2), + ('bd-sp-021', 'bd-e-022', 'diamond', 'DIAMOND', '다이아몬드', 50000000, '#B8860B', '["로고 노출","부스 우선 배정"]'::jsonb, 3, 0, 1, 0), + ('bd-sp-022', 'bd-e-023', 'gold', 'GOLD', '골드', 30000000, '#B8860B', '["로고 노출","부스 우선 배정"]'::jsonb, 3, 1, 2, 1), + ('bd-sp-023', 'bd-e-024', 'silver', 'SILVER', '실버', 15000000, '#B8860B', '["로고 노출","부스 우선 배정"]'::jsonb, 3, 2, 3, 2), + ('bd-sp-024', 'bd-e-025', 'diamond', 'DIAMOND', '다이아몬드', 50000000, '#B8860B', '["로고 노출","부스 우선 배정"]'::jsonb, 3, 0, 4, 0), + ('bd-sp-025', 'bd-e-026', 'gold', 'GOLD', '골드', 30000000, '#B8860B', '["로고 노출","부스 우선 배정"]'::jsonb, 3, 1, 5, 1), + ('bd-sp-026', 'bd-e-027', 'silver', 'SILVER', '실버', 15000000, '#B8860B', '["로고 노출","부스 우선 배정"]'::jsonb, 3, 2, 6, 2), + ('bd-sp-027', 'bd-e-028', 'diamond', 'DIAMOND', '다이아몬드', 50000000, '#B8860B', '["로고 노출","부스 우선 배정"]'::jsonb, 3, 0, 7, 0), + ('bd-sp-028', 'bd-e-029', 'gold', 'GOLD', '골드', 30000000, '#B8860B', '["로고 노출","부스 우선 배정"]'::jsonb, 3, 1, 8, 1), + ('bd-sp-029', 'bd-e-030', 'silver', 'SILVER', '실버', 15000000, '#B8860B', '["로고 노출","부스 우선 배정"]'::jsonb, 3, 2, 9, 2), + ('bd-sp-030', 'bd-e-031', 'diamond', 'DIAMOND', '다이아몬드', 50000000, '#B8860B', '["로고 노출","부스 우선 배정"]'::jsonb, 3, 0, 10, 0), + ('bd-sp-031', 'bd-e-032', 'gold', 'GOLD', '골드', 30000000, '#B8860B', '["로고 노출","부스 우선 배정"]'::jsonb, 3, 1, 11, 1), + ('bd-sp-032', 'bd-e-033', 'silver', 'SILVER', '실버', 15000000, '#B8860B', '["로고 노출","부스 우선 배정"]'::jsonb, 3, 2, 12, 2), + ('bd-sp-033', 'bd-e-034', 'diamond', 'DIAMOND', '다이아몬드', 50000000, '#B8860B', '["로고 노출","부스 우선 배정"]'::jsonb, 3, 0, 13, 0), + ('bd-sp-034', 'bd-e-035', 'gold', 'GOLD', '골드', 30000000, '#B8860B', '["로고 노출","부스 우선 배정"]'::jsonb, 3, 1, 14, 1), + ('bd-sp-035', 'bd-e-036', 'silver', 'SILVER', '실버', 15000000, '#B8860B', '["로고 노출","부스 우선 배정"]'::jsonb, 3, 2, 15, 2), + ('bd-sp-036', 'bd-e-037', 'diamond', 'DIAMOND', '다이아몬드', 50000000, '#B8860B', '["로고 노출","부스 우선 배정"]'::jsonb, 3, 0, 16, 0), + ('bd-sp-037', 'bd-e-038', 'gold', 'GOLD', '골드', 30000000, '#B8860B', '["로고 노출","부스 우선 배정"]'::jsonb, 3, 1, 17, 1), + ('bd-sp-038', 'bd-e-039', 'silver', 'SILVER', '실버', 15000000, '#B8860B', '["로고 노출","부스 우선 배정"]'::jsonb, 3, 2, 18, 2), + ('bd-sp-039', 'bd-e-040', 'diamond', 'DIAMOND', '다이아몬드', 50000000, '#B8860B', '["로고 노출","부스 우선 배정"]'::jsonb, 3, 0, 19, 0), + ('bd-sp-040', 'bd-e-041', 'gold', 'GOLD', '골드', 30000000, '#B8860B', '["로고 노출","부스 우선 배정"]'::jsonb, 3, 1, 0, 1), + ('bd-sp-041', 'bd-e-042', 'silver', 'SILVER', '실버', 15000000, '#B8860B', '["로고 노출","부스 우선 배정"]'::jsonb, 3, 2, 1, 2), + ('bd-sp-042', 'bd-e-043', 'diamond', 'DIAMOND', '다이아몬드', 50000000, '#B8860B', '["로고 노출","부스 우선 배정"]'::jsonb, 3, 0, 2, 0), + ('bd-sp-043', 'bd-e-044', 'gold', 'GOLD', '골드', 30000000, '#B8860B', '["로고 노출","부스 우선 배정"]'::jsonb, 3, 1, 3, 1), + ('bd-sp-044', 'bd-e-045', 'silver', 'SILVER', '실버', 15000000, '#B8860B', '["로고 노출","부스 우선 배정"]'::jsonb, 3, 2, 4, 2), + ('bd-sp-045', 'bd-e-046', 'diamond', 'DIAMOND', '다이아몬드', 50000000, '#B8860B', '["로고 노출","부스 우선 배정"]'::jsonb, 3, 0, 5, 0), + ('bd-sp-046', 'bd-e-047', 'gold', 'GOLD', '골드', 30000000, '#B8860B', '["로고 노출","부스 우선 배정"]'::jsonb, 3, 1, 6, 1), + ('bd-sp-047', 'bd-e-048', 'silver', 'SILVER', '실버', 15000000, '#B8860B', '["로고 노출","부스 우선 배정"]'::jsonb, 3, 2, 7, 2), + ('bd-sp-048', 'bd-e-049', 'diamond', 'DIAMOND', '다이아몬드', 50000000, '#B8860B', '["로고 노출","부스 우선 배정"]'::jsonb, 3, 0, 8, 0), + ('bd-sp-049', 'bd-e-050', 'gold', 'GOLD', '골드', 30000000, '#B8860B', '["로고 노출","부스 우선 배정"]'::jsonb, 3, 1, 9, 1), + ('bd-sp-050', 'bd-e-051', 'silver', 'SILVER', '실버', 15000000, '#B8860B', '["로고 노출","부스 우선 배정"]'::jsonb, 3, 2, 10, 2) +ON CONFLICT (id) DO NOTHING; + +INSERT INTO sponsorship_sponsor (id, event_id, package_id, name, tier_label, contract_status, fulfillment) VALUES + ('bd-ss-001', 'bd-e-002', 'bd-sp-001', '로보틱스테크', '골드 패키지', 'pending', '[{"id":"f1","label":"로고 노출","done":true}]'::jsonb), + ('bd-ss-002', 'bd-e-003', 'bd-sp-002', '(주)인더스트리테크', '실버 패키지', 'signed', '[{"id":"f1","label":"로고 노출","done":true}]'::jsonb), + ('bd-ss-003', 'bd-e-004', 'bd-sp-003', '시스템즈테크', '다이아몬드 패키지', 'pending', '[{"id":"f1","label":"로고 노출","done":true}]'::jsonb), + ('bd-ss-004', 'bd-e-005', 'bd-sp-004', '(주)솔루션테크', '골드 패키지', 'signed', '[{"id":"f1","label":"로고 노출","done":true}]'::jsonb), + ('bd-ss-005', 'bd-e-006', 'bd-sp-005', '이노베이션테크', '실버 패키지', 'pending', '[{"id":"f1","label":"로고 노출","done":true}]'::jsonb), + ('bd-ss-006', 'bd-e-007', 'bd-sp-006', '오토메이션테크', '다이아몬드 패키지', 'signed', '[{"id":"f1","label":"로고 노출","done":true}]'::jsonb), + ('bd-ss-007', 'bd-e-008', 'bd-sp-007', '(주)디자인테크', '골드 패키지', 'pending', '[{"id":"f1","label":"로고 노출","done":true}]'::jsonb), + ('bd-ss-008', 'bd-e-009', 'bd-sp-008', '엔지니어링테크', '실버 패키지', 'signed', '[{"id":"f1","label":"로고 노출","done":true}]'::jsonb), + ('bd-ss-009', 'bd-e-010', 'bd-sp-009', '(주)일렉트릭테크', '다이아몬드 패키지', 'pending', '[{"id":"f1","label":"로고 노출","done":true}]'::jsonb), + ('bd-ss-010', 'bd-e-011', 'bd-sp-010', '네트웍스테크', '골드 패키지', 'signed', '[{"id":"f1","label":"로고 노출","done":true}]'::jsonb), + ('bd-ss-011', 'bd-e-012', 'bd-sp-011', '미디어테크', '실버 패키지', 'pending', '[{"id":"f1","label":"로고 노출","done":true}]'::jsonb), + ('bd-ss-012', 'bd-e-013', 'bd-sp-012', '(주)디스플레이테크', '다이아몬드 패키지', 'signed', '[{"id":"f1","label":"로고 노출","done":true}]'::jsonb), + ('bd-ss-013', 'bd-e-014', 'bd-sp-013', '컨스트럭션테크', '골드 패키지', 'pending', '[{"id":"f1","label":"로고 노출","done":true}]'::jsonb), + ('bd-ss-014', 'bd-e-015', 'bd-sp-014', '(주)부스테크', '실버 패키지', 'signed', '[{"id":"f1","label":"로고 노출","done":true}]'::jsonb), + ('bd-ss-015', 'bd-e-016', 'bd-sp-015', '전기시설테크', '다이아몬드 패키지', 'pending', '[{"id":"f1","label":"로고 노출","done":true}]'::jsonb), + ('bd-ss-016', 'bd-e-017', 'bd-sp-016', '구조테크', '골드 패키지', 'signed', '[{"id":"f1","label":"로고 노출","done":true}]'::jsonb), + ('bd-ss-017', 'bd-e-018', 'bd-sp-017', '(주)비전테크', '실버 패키지', 'pending', '[{"id":"f1","label":"로고 노출","done":true}]'::jsonb), + ('bd-ss-018', 'bd-e-019', 'bd-sp-018', '센서텍테크', '다이아몬드 패키지', 'signed', '[{"id":"f1","label":"로고 노출","done":true}]'::jsonb), + ('bd-ss-019', 'bd-e-020', 'bd-sp-019', '(주)머티리얼테크', '골드 패키지', 'pending', '[{"id":"f1","label":"로고 노출","done":true}]'::jsonb), + ('bd-ss-020', 'bd-e-021', 'bd-sp-020', '로지스테크', '실버 패키지', 'signed', '[{"id":"f1","label":"로고 노출","done":true}]'::jsonb), + ('bd-ss-021', 'bd-e-022', 'bd-sp-021', '바이오테크', '다이아몬드 패키지', 'pending', '[{"id":"f1","label":"로고 노출","done":true}]'::jsonb), + ('bd-ss-022', 'bd-e-023', 'bd-sp-022', '(주)헬스랩테크', '골드 패키지', 'signed', '[{"id":"f1","label":"로고 노출","done":true}]'::jsonb), + ('bd-ss-023', 'bd-e-024', 'bd-sp-023', '에너지테크', '실버 패키지', 'pending', '[{"id":"f1","label":"로고 노출","done":true}]'::jsonb), + ('bd-ss-024', 'bd-e-025', 'bd-sp-024', '(주)스마트테크', '다이아몬드 패키지', 'signed', '[{"id":"f1","label":"로고 노출","done":true}]'::jsonb), + ('bd-ss-025', 'bd-e-026', 'bd-sp-025', '글로벌테크', '골드 패키지', 'pending', '[{"id":"f1","label":"로고 노출","done":true}]'::jsonb), + ('bd-ss-026', 'bd-e-027', 'bd-sp-026', '넥스트테크', '실버 패키지', 'signed', '[{"id":"f1","label":"로고 노출","done":true}]'::jsonb), + ('bd-ss-027', 'bd-e-028', 'bd-sp-027', '(주)미래테크', '다이아몬드 패키지', 'pending', '[{"id":"f1","label":"로고 노출","done":true}]'::jsonb), + ('bd-ss-028', 'bd-e-029', 'bd-sp-028', '한빛테크', '골드 패키지', 'signed', '[{"id":"f1","label":"로고 노출","done":true}]'::jsonb), + ('bd-ss-029', 'bd-e-030', 'bd-sp-029', '(주)대한테크', '실버 패키지', 'pending', '[{"id":"f1","label":"로고 노출","done":true}]'::jsonb), + ('bd-ss-030', 'bd-e-031', 'bd-sp-030', '테크시스템즈', '다이아몬드 패키지', 'signed', '[{"id":"f1","label":"로고 노출","done":true}]'::jsonb), + ('bd-ss-031', 'bd-e-032', 'bd-sp-031', '로보틱스시스템즈', '골드 패키지', 'pending', '[{"id":"f1","label":"로고 노출","done":true}]'::jsonb), + ('bd-ss-032', 'bd-e-033', 'bd-sp-032', '(주)인더스트리시스템즈', '실버 패키지', 'signed', '[{"id":"f1","label":"로고 노출","done":true}]'::jsonb), + ('bd-ss-033', 'bd-e-034', 'bd-sp-033', '시스템즈시스템즈', '다이아몬드 패키지', 'pending', '[{"id":"f1","label":"로고 노출","done":true}]'::jsonb), + ('bd-ss-034', 'bd-e-035', 'bd-sp-034', '(주)솔루션시스템즈', '골드 패키지', 'signed', '[{"id":"f1","label":"로고 노출","done":true}]'::jsonb), + ('bd-ss-035', 'bd-e-036', 'bd-sp-035', '이노베이션시스템즈', '실버 패키지', 'pending', '[{"id":"f1","label":"로고 노출","done":true}]'::jsonb), + ('bd-ss-036', 'bd-e-037', 'bd-sp-036', '오토메이션시스템즈', '다이아몬드 패키지', 'signed', '[{"id":"f1","label":"로고 노출","done":true}]'::jsonb), + ('bd-ss-037', 'bd-e-038', 'bd-sp-037', '(주)디자인시스템즈', '골드 패키지', 'pending', '[{"id":"f1","label":"로고 노출","done":true}]'::jsonb), + ('bd-ss-038', 'bd-e-039', 'bd-sp-038', '엔지니어링시스템즈', '실버 패키지', 'signed', '[{"id":"f1","label":"로고 노출","done":true}]'::jsonb), + ('bd-ss-039', 'bd-e-040', 'bd-sp-039', '(주)일렉트릭시스템즈', '다이아몬드 패키지', 'pending', '[{"id":"f1","label":"로고 노출","done":true}]'::jsonb), + ('bd-ss-040', 'bd-e-041', 'bd-sp-040', '네트웍스시스템즈', '골드 패키지', 'signed', '[{"id":"f1","label":"로고 노출","done":true}]'::jsonb), + ('bd-ss-041', 'bd-e-042', 'bd-sp-041', '미디어시스템즈', '실버 패키지', 'pending', '[{"id":"f1","label":"로고 노출","done":true}]'::jsonb), + ('bd-ss-042', 'bd-e-043', 'bd-sp-042', '(주)디스플레이시스템즈', '다이아몬드 패키지', 'signed', '[{"id":"f1","label":"로고 노출","done":true}]'::jsonb), + ('bd-ss-043', 'bd-e-044', 'bd-sp-043', '컨스트럭션시스템즈', '골드 패키지', 'pending', '[{"id":"f1","label":"로고 노출","done":true}]'::jsonb), + ('bd-ss-044', 'bd-e-045', 'bd-sp-044', '(주)부스시스템즈', '실버 패키지', 'signed', '[{"id":"f1","label":"로고 노출","done":true}]'::jsonb), + ('bd-ss-045', 'bd-e-046', 'bd-sp-045', '전기시설시스템즈', '다이아몬드 패키지', 'pending', '[{"id":"f1","label":"로고 노출","done":true}]'::jsonb), + ('bd-ss-046', 'bd-e-047', 'bd-sp-046', '구조시스템즈', '골드 패키지', 'signed', '[{"id":"f1","label":"로고 노출","done":true}]'::jsonb), + ('bd-ss-047', 'bd-e-048', 'bd-sp-047', '(주)비전시스템즈', '실버 패키지', 'pending', '[{"id":"f1","label":"로고 노출","done":true}]'::jsonb), + ('bd-ss-048', 'bd-e-049', 'bd-sp-048', '센서텍시스템즈', '다이아몬드 패키지', 'signed', '[{"id":"f1","label":"로고 노출","done":true}]'::jsonb), + ('bd-ss-049', 'bd-e-050', 'bd-sp-049', '(주)머티리얼시스템즈', '골드 패키지', 'pending', '[{"id":"f1","label":"로고 노출","done":true}]'::jsonb), + ('bd-ss-050', 'bd-e-051', 'bd-sp-050', '로지스시스템즈', '실버 패키지', 'signed', '[{"id":"f1","label":"로고 노출","done":true}]'::jsonb) +ON CONFLICT (id) DO NOTHING; + +-- 22. 참가 문의(exhibit_inquiry) 60 — 공개 접수(PII 응답 미노출). receipt_no 유니크. +INSERT INTO exhibit_inquiry (id, receipt_no, inquiry_type, company, contact_name, email, phone, scale, hall_pref, message, agree_privacy, agree_marketing, status, created_at) VALUES + ('bd-inq-001', 'KTX-INQ-BD-0001', 'raw', '로보틱스테크', '강예준', 'bdinq1@kintex.test', '010-0000-5001', '3개 부스', '제1전시장 홀2', '대량 데모 참가 문의 1.', true, false, 'in_review', now() - interval '2 day'), + ('bd-inq-002', 'KTX-INQ-BD-0002', 'premium', '(주)인더스트리테크', '한서윤', 'bdinq2@kintex.test', '010-0000-5002', '4개 부스', '제1전시장 홀3', '대량 데모 참가 문의 2.', true, false, 'closed', now() - interval '3 day'), + ('bd-inq-003', 'KTX-INQ-BD-0003', 'general', '시스템즈테크', '황지민', 'bdinq3@kintex.test', '010-0000-5003', '5개 부스', '제1전시장 홀4', '대량 데모 참가 문의 3.', true, true, 'received', now() - interval '4 day'), + ('bd-inq-004', 'KTX-INQ-BD-0004', 'shell', '(주)솔루션테크', '김현우', 'bdinq4@kintex.test', '010-0000-5004', '6개 부스', '제1전시장 홀5', '대량 데모 참가 문의 4.', true, false, 'in_review', now() - interval '5 day'), + ('bd-inq-005', 'KTX-INQ-BD-0005', 'raw', '이노베이션테크', '강지훈', 'bdinq5@kintex.test', '010-0000-5005', '7개 부스', '제2전시장 홀6', '대량 데모 참가 문의 5.', true, false, 'closed', now() - interval '6 day'), + ('bd-inq-006', 'KTX-INQ-BD-0006', 'premium', '오토메이션테크', '한서준', 'bdinq6@kintex.test', '010-0000-5006', '2개 부스', '제2전시장 홀7', '대량 데모 참가 문의 6.', true, true, 'received', now() - interval '7 day'), + ('bd-inq-007', 'KTX-INQ-BD-0007', 'general', '(주)디자인테크', '황예준', 'bdinq7@kintex.test', '010-0000-5007', '3개 부스', '제2전시장 홀8', '대량 데모 참가 문의 7.', true, false, 'in_review', now() - interval '8 day'), + ('bd-inq-008', 'KTX-INQ-BD-0008', 'shell', '엔지니어링테크', '김서윤', 'bdinq8@kintex.test', '010-0000-5008', '4개 부스', '제2전시장 홀9', '대량 데모 참가 문의 8.', true, false, 'closed', now() - interval '9 day'), + ('bd-inq-009', 'KTX-INQ-BD-0009', 'raw', '(주)일렉트릭테크', '강지민', 'bdinq9@kintex.test', '010-0000-5009', '5개 부스', '제2전시장 홀10', '대량 데모 참가 문의 9.', true, true, 'received', now() - interval '10 day'), + ('bd-inq-010', 'KTX-INQ-BD-0010', 'premium', '네트웍스테크', '한현우', 'bdinq10@kintex.test', '010-0000-5010', '6개 부스', '제1전시장 홀1', '대량 데모 참가 문의 10.', true, false, 'in_review', now() - interval '11 day'), + ('bd-inq-011', 'KTX-INQ-BD-0011', 'general', '미디어테크', '황지훈', 'bdinq11@kintex.test', '010-0000-5011', '7개 부스', '제1전시장 홀2', '대량 데모 참가 문의 11.', true, false, 'closed', now() - interval '12 day'), + ('bd-inq-012', 'KTX-INQ-BD-0012', 'shell', '(주)디스플레이테크', '김서준', 'bdinq12@kintex.test', '010-0000-5012', '2개 부스', '제1전시장 홀3', '대량 데모 참가 문의 12.', true, true, 'received', now() - interval '13 day'), + ('bd-inq-013', 'KTX-INQ-BD-0013', 'raw', '컨스트럭션테크', '강예준', 'bdinq13@kintex.test', '010-0000-5013', '3개 부스', '제1전시장 홀4', '대량 데모 참가 문의 13.', true, false, 'in_review', now() - interval '14 day'), + ('bd-inq-014', 'KTX-INQ-BD-0014', 'premium', '(주)부스테크', '한서윤', 'bdinq14@kintex.test', '010-0000-5014', '4개 부스', '제1전시장 홀5', '대량 데모 참가 문의 14.', true, false, 'closed', now() - interval '15 day'), + ('bd-inq-015', 'KTX-INQ-BD-0015', 'general', '전기시설테크', '황지민', 'bdinq15@kintex.test', '010-0000-5015', '5개 부스', '제2전시장 홀6', '대량 데모 참가 문의 15.', true, true, 'received', now() - interval '16 day'), + ('bd-inq-016', 'KTX-INQ-BD-0016', 'shell', '구조테크', '김현우', 'bdinq16@kintex.test', '010-0000-5016', '6개 부스', '제2전시장 홀7', '대량 데모 참가 문의 16.', true, false, 'in_review', now() - interval '17 day'), + ('bd-inq-017', 'KTX-INQ-BD-0017', 'raw', '(주)비전테크', '강지훈', 'bdinq17@kintex.test', '010-0000-5017', '7개 부스', '제2전시장 홀8', '대량 데모 참가 문의 17.', true, false, 'closed', now() - interval '18 day'), + ('bd-inq-018', 'KTX-INQ-BD-0018', 'premium', '센서텍테크', '한서준', 'bdinq18@kintex.test', '010-0000-5018', '2개 부스', '제2전시장 홀9', '대량 데모 참가 문의 18.', true, true, 'received', now() - interval '19 day'), + ('bd-inq-019', 'KTX-INQ-BD-0019', 'general', '(주)머티리얼테크', '황예준', 'bdinq19@kintex.test', '010-0000-5019', '3개 부스', '제2전시장 홀10', '대량 데모 참가 문의 19.', true, false, 'in_review', now() - interval '20 day'), + ('bd-inq-020', 'KTX-INQ-BD-0020', 'shell', '로지스테크', '김서윤', 'bdinq20@kintex.test', '010-0000-5020', '4개 부스', '제1전시장 홀1', '대량 데모 참가 문의 20.', true, false, 'closed', now() - interval '21 day'), + ('bd-inq-021', 'KTX-INQ-BD-0021', 'raw', '바이오테크', '강지민', 'bdinq21@kintex.test', '010-0000-5021', '5개 부스', '제1전시장 홀2', '대량 데모 참가 문의 21.', true, true, 'received', now() - interval '22 day'), + ('bd-inq-022', 'KTX-INQ-BD-0022', 'premium', '(주)헬스랩테크', '한현우', 'bdinq22@kintex.test', '010-0000-5022', '6개 부스', '제1전시장 홀3', '대량 데모 참가 문의 22.', true, false, 'in_review', now() - interval '23 day'), + ('bd-inq-023', 'KTX-INQ-BD-0023', 'general', '에너지테크', '황지훈', 'bdinq23@kintex.test', '010-0000-5023', '7개 부스', '제1전시장 홀4', '대량 데모 참가 문의 23.', true, false, 'closed', now() - interval '24 day'), + ('bd-inq-024', 'KTX-INQ-BD-0024', 'shell', '(주)스마트테크', '김서준', 'bdinq24@kintex.test', '010-0000-5024', '2개 부스', '제1전시장 홀5', '대량 데모 참가 문의 24.', true, true, 'received', now() - interval '25 day'), + ('bd-inq-025', 'KTX-INQ-BD-0025', 'raw', '글로벌테크', '강예준', 'bdinq25@kintex.test', '010-0000-5025', '3개 부스', '제2전시장 홀6', '대량 데모 참가 문의 25.', true, false, 'in_review', now() - interval '26 day'), + ('bd-inq-026', 'KTX-INQ-BD-0026', 'premium', '넥스트테크', '한서윤', 'bdinq26@kintex.test', '010-0000-5026', '4개 부스', '제2전시장 홀7', '대량 데모 참가 문의 26.', true, false, 'closed', now() - interval '27 day'), + ('bd-inq-027', 'KTX-INQ-BD-0027', 'general', '(주)미래테크', '황지민', 'bdinq27@kintex.test', '010-0000-5027', '5개 부스', '제2전시장 홀8', '대량 데모 참가 문의 27.', true, true, 'received', now() - interval '28 day'), + ('bd-inq-028', 'KTX-INQ-BD-0028', 'shell', '한빛테크', '김현우', 'bdinq28@kintex.test', '010-0000-5028', '6개 부스', '제2전시장 홀9', '대량 데모 참가 문의 28.', true, false, 'in_review', now() - interval '29 day'), + ('bd-inq-029', 'KTX-INQ-BD-0029', 'raw', '(주)대한테크', '강지훈', 'bdinq29@kintex.test', '010-0000-5029', '7개 부스', '제2전시장 홀10', '대량 데모 참가 문의 29.', true, false, 'closed', now() - interval '30 day'), + ('bd-inq-030', 'KTX-INQ-BD-0030', 'premium', '테크시스템즈', '한서준', 'bdinq30@kintex.test', '010-0000-5030', '2개 부스', '제1전시장 홀1', '대량 데모 참가 문의 30.', true, true, 'received', now() - interval '1 day'), + ('bd-inq-031', 'KTX-INQ-BD-0031', 'general', '로보틱스시스템즈', '황예준', 'bdinq31@kintex.test', '010-0000-5031', '3개 부스', '제1전시장 홀2', '대량 데모 참가 문의 31.', true, false, 'in_review', now() - interval '2 day'), + ('bd-inq-032', 'KTX-INQ-BD-0032', 'shell', '(주)인더스트리시스템즈', '김서윤', 'bdinq32@kintex.test', '010-0000-5032', '4개 부스', '제1전시장 홀3', '대량 데모 참가 문의 32.', true, false, 'closed', now() - interval '3 day'), + ('bd-inq-033', 'KTX-INQ-BD-0033', 'raw', '시스템즈시스템즈', '강지민', 'bdinq33@kintex.test', '010-0000-5033', '5개 부스', '제1전시장 홀4', '대량 데모 참가 문의 33.', true, true, 'received', now() - interval '4 day'), + ('bd-inq-034', 'KTX-INQ-BD-0034', 'premium', '(주)솔루션시스템즈', '한현우', 'bdinq34@kintex.test', '010-0000-5034', '6개 부스', '제1전시장 홀5', '대량 데모 참가 문의 34.', true, false, 'in_review', now() - interval '5 day'), + ('bd-inq-035', 'KTX-INQ-BD-0035', 'general', '이노베이션시스템즈', '황지훈', 'bdinq35@kintex.test', '010-0000-5035', '7개 부스', '제2전시장 홀6', '대량 데모 참가 문의 35.', true, false, 'closed', now() - interval '6 day'), + ('bd-inq-036', 'KTX-INQ-BD-0036', 'shell', '오토메이션시스템즈', '김서준', 'bdinq36@kintex.test', '010-0000-5036', '2개 부스', '제2전시장 홀7', '대량 데모 참가 문의 36.', true, true, 'received', now() - interval '7 day'), + ('bd-inq-037', 'KTX-INQ-BD-0037', 'raw', '(주)디자인시스템즈', '강예준', 'bdinq37@kintex.test', '010-0000-5037', '3개 부스', '제2전시장 홀8', '대량 데모 참가 문의 37.', true, false, 'in_review', now() - interval '8 day'), + ('bd-inq-038', 'KTX-INQ-BD-0038', 'premium', '엔지니어링시스템즈', '한서윤', 'bdinq38@kintex.test', '010-0000-5038', '4개 부스', '제2전시장 홀9', '대량 데모 참가 문의 38.', true, false, 'closed', now() - interval '9 day'), + ('bd-inq-039', 'KTX-INQ-BD-0039', 'general', '(주)일렉트릭시스템즈', '황지민', 'bdinq39@kintex.test', '010-0000-5039', '5개 부스', '제2전시장 홀10', '대량 데모 참가 문의 39.', true, true, 'received', now() - interval '10 day'), + ('bd-inq-040', 'KTX-INQ-BD-0040', 'shell', '네트웍스시스템즈', '김현우', 'bdinq40@kintex.test', '010-0000-5040', '6개 부스', '제1전시장 홀1', '대량 데모 참가 문의 40.', true, false, 'in_review', now() - interval '11 day'), + ('bd-inq-041', 'KTX-INQ-BD-0041', 'raw', '미디어시스템즈', '강지훈', 'bdinq41@kintex.test', '010-0000-5041', '7개 부스', '제1전시장 홀2', '대량 데모 참가 문의 41.', true, false, 'closed', now() - interval '12 day'), + ('bd-inq-042', 'KTX-INQ-BD-0042', 'premium', '(주)디스플레이시스템즈', '한서준', 'bdinq42@kintex.test', '010-0000-5042', '2개 부스', '제1전시장 홀3', '대량 데모 참가 문의 42.', true, true, 'received', now() - interval '13 day'), + ('bd-inq-043', 'KTX-INQ-BD-0043', 'general', '컨스트럭션시스템즈', '황예준', 'bdinq43@kintex.test', '010-0000-5043', '3개 부스', '제1전시장 홀4', '대량 데모 참가 문의 43.', true, false, 'in_review', now() - interval '14 day'), + ('bd-inq-044', 'KTX-INQ-BD-0044', 'shell', '(주)부스시스템즈', '김서윤', 'bdinq44@kintex.test', '010-0000-5044', '4개 부스', '제1전시장 홀5', '대량 데모 참가 문의 44.', true, false, 'closed', now() - interval '15 day'), + ('bd-inq-045', 'KTX-INQ-BD-0045', 'raw', '전기시설시스템즈', '강지민', 'bdinq45@kintex.test', '010-0000-5045', '5개 부스', '제2전시장 홀6', '대량 데모 참가 문의 45.', true, true, 'received', now() - interval '16 day'), + ('bd-inq-046', 'KTX-INQ-BD-0046', 'premium', '구조시스템즈', '한현우', 'bdinq46@kintex.test', '010-0000-5046', '6개 부스', '제2전시장 홀7', '대량 데모 참가 문의 46.', true, false, 'in_review', now() - interval '17 day'), + ('bd-inq-047', 'KTX-INQ-BD-0047', 'general', '(주)비전시스템즈', '황지훈', 'bdinq47@kintex.test', '010-0000-5047', '7개 부스', '제2전시장 홀8', '대량 데모 참가 문의 47.', true, false, 'closed', now() - interval '18 day'), + ('bd-inq-048', 'KTX-INQ-BD-0048', 'shell', '센서텍시스템즈', '김서준', 'bdinq48@kintex.test', '010-0000-5048', '2개 부스', '제2전시장 홀9', '대량 데모 참가 문의 48.', true, true, 'received', now() - interval '19 day'), + ('bd-inq-049', 'KTX-INQ-BD-0049', 'raw', '(주)머티리얼시스템즈', '강예준', 'bdinq49@kintex.test', '010-0000-5049', '3개 부스', '제2전시장 홀10', '대량 데모 참가 문의 49.', true, false, 'in_review', now() - interval '20 day'), + ('bd-inq-050', 'KTX-INQ-BD-0050', 'premium', '로지스시스템즈', '한서윤', 'bdinq50@kintex.test', '010-0000-5050', '4개 부스', '제1전시장 홀1', '대량 데모 참가 문의 50.', true, false, 'closed', now() - interval '21 day'), + ('bd-inq-051', 'KTX-INQ-BD-0051', 'general', '바이오시스템즈', '황지민', 'bdinq51@kintex.test', '010-0000-5051', '5개 부스', '제1전시장 홀2', '대량 데모 참가 문의 51.', true, true, 'received', now() - interval '22 day'), + ('bd-inq-052', 'KTX-INQ-BD-0052', 'shell', '(주)헬스랩시스템즈', '김현우', 'bdinq52@kintex.test', '010-0000-5052', '6개 부스', '제1전시장 홀3', '대량 데모 참가 문의 52.', true, false, 'in_review', now() - interval '23 day'), + ('bd-inq-053', 'KTX-INQ-BD-0053', 'raw', '에너지시스템즈', '강지훈', 'bdinq53@kintex.test', '010-0000-5053', '7개 부스', '제1전시장 홀4', '대량 데모 참가 문의 53.', true, false, 'closed', now() - interval '24 day'), + ('bd-inq-054', 'KTX-INQ-BD-0054', 'premium', '(주)스마트시스템즈', '한서준', 'bdinq54@kintex.test', '010-0000-5054', '2개 부스', '제1전시장 홀5', '대량 데모 참가 문의 54.', true, true, 'received', now() - interval '25 day'), + ('bd-inq-055', 'KTX-INQ-BD-0055', 'general', '글로벌시스템즈', '황예준', 'bdinq55@kintex.test', '010-0000-5055', '3개 부스', '제2전시장 홀6', '대량 데모 참가 문의 55.', true, false, 'in_review', now() - interval '26 day'), + ('bd-inq-056', 'KTX-INQ-BD-0056', 'shell', '넥스트시스템즈', '김서윤', 'bdinq56@kintex.test', '010-0000-5056', '4개 부스', '제2전시장 홀7', '대량 데모 참가 문의 56.', true, false, 'closed', now() - interval '27 day'), + ('bd-inq-057', 'KTX-INQ-BD-0057', 'raw', '(주)미래시스템즈', '강지민', 'bdinq57@kintex.test', '010-0000-5057', '5개 부스', '제2전시장 홀8', '대량 데모 참가 문의 57.', true, true, 'received', now() - interval '28 day'), + ('bd-inq-058', 'KTX-INQ-BD-0058', 'premium', '한빛시스템즈', '한현우', 'bdinq58@kintex.test', '010-0000-5058', '6개 부스', '제2전시장 홀9', '대량 데모 참가 문의 58.', true, false, 'in_review', now() - interval '29 day'), + ('bd-inq-059', 'KTX-INQ-BD-0059', 'general', '(주)대한시스템즈', '황지훈', 'bdinq59@kintex.test', '010-0000-5059', '7개 부스', '제2전시장 홀10', '대량 데모 참가 문의 59.', true, false, 'closed', now() - interval '30 day'), + ('bd-inq-060', 'KTX-INQ-BD-0060', 'shell', '테크인더스트리', '김서준', 'bdinq60@kintex.test', '010-0000-5060', '2개 부스', '제1전시장 홀1', '대량 데모 참가 문의 60.', true, true, 'received', now() - interval '1 day') +ON CONFLICT (id) DO NOTHING; + +-- 23. 도크 예약(dock_reservation) 60 — 기존 dock-h7-1~6 재사용, H7 행사(bd 중 H7) 참조. +INSERT INTO dock_reservation (id, event_id, dock_id, direction, reserve_date, start_hour, span, company_name, vehicle_no, vehicle_weight, item, forklift, tone) VALUES + ('bd-dr-001', 'bd-e-016', 'dock-h7-2', 'out', CURRENT_DATE + -2, 9, 2, '로보틱스테크', '11가 1001', '2.5t', '전시 자재', false, 'booked'), + ('bd-dr-002', 'bd-e-026', 'dock-h7-3', 'in', CURRENT_DATE + -1, 10, 2, '(주)인더스트리테크', '12가 1002', '5t', '전시 자재', false, 'booked'), + ('bd-dr-003', 'bd-e-036', 'dock-h7-4', 'out', CURRENT_DATE + 0, 11, 2, '시스템즈테크', '13가 1003', '11t 이상', '전시 자재', true, 'booked'), + ('bd-dr-004', 'bd-e-046', 'dock-h7-5', 'in', CURRENT_DATE + 1, 12, 2, '(주)솔루션테크', '14가 1004', '1t', '전시 자재', false, 'booked'), + ('bd-dr-005', 'bd-e-056', 'dock-h7-6', 'out', CURRENT_DATE + 2, 13, 2, '이노베이션테크', '15가 1005', '2.5t', '전시 자재', false, 'booked'), + ('bd-dr-006', 'bd-e-006', 'dock-h7-1', 'in', CURRENT_DATE + 3, 14, 2, '오토메이션테크', '16가 1006', '5t', '전시 자재', true, 'booked'), + ('bd-dr-007', 'bd-e-016', 'dock-h7-2', 'out', CURRENT_DATE + 4, 15, 2, '(주)디자인테크', '17가 1007', '11t 이상', '전시 자재', false, 'priority'), + ('bd-dr-008', 'bd-e-026', 'dock-h7-3', 'in', CURRENT_DATE + 5, 16, 2, '엔지니어링테크', '18가 1008', '1t', '전시 자재', false, 'booked'), + ('bd-dr-009', 'bd-e-036', 'dock-h7-4', 'out', CURRENT_DATE + 6, 17, 2, '(주)일렉트릭테크', '19가 1009', '2.5t', '전시 자재', true, 'booked'), + ('bd-dr-010', 'bd-e-046', 'dock-h7-5', 'in', CURRENT_DATE + -3, 8, 2, '네트웍스테크', '20가 1010', '5t', '전시 자재', false, 'booked'), + ('bd-dr-011', 'bd-e-056', 'dock-h7-6', 'out', CURRENT_DATE + -2, 9, 2, '미디어테크', '21가 1011', '11t 이상', '전시 자재', false, 'booked'), + ('bd-dr-012', 'bd-e-006', 'dock-h7-1', 'in', CURRENT_DATE + -1, 10, 2, '(주)디스플레이테크', '22가 1012', '1t', '전시 자재', true, 'booked'), + ('bd-dr-013', 'bd-e-016', 'dock-h7-2', 'out', CURRENT_DATE + 0, 11, 2, '컨스트럭션테크', '23가 1013', '2.5t', '전시 자재', false, 'booked'), + ('bd-dr-014', 'bd-e-026', 'dock-h7-3', 'in', CURRENT_DATE + 1, 12, 2, '(주)부스테크', '24가 1014', '5t', '전시 자재', false, 'priority'), + ('bd-dr-015', 'bd-e-036', 'dock-h7-4', 'out', CURRENT_DATE + 2, 13, 2, '전기시설테크', '25가 1015', '11t 이상', '전시 자재', true, 'booked'), + ('bd-dr-016', 'bd-e-046', 'dock-h7-5', 'in', CURRENT_DATE + 3, 14, 2, '구조테크', '26가 1016', '1t', '전시 자재', false, 'booked'), + ('bd-dr-017', 'bd-e-056', 'dock-h7-6', 'out', CURRENT_DATE + 4, 15, 2, '(주)비전테크', '27가 1017', '2.5t', '전시 자재', false, 'booked'), + ('bd-dr-018', 'bd-e-006', 'dock-h7-1', 'in', CURRENT_DATE + 5, 16, 2, '센서텍테크', '28가 1018', '5t', '전시 자재', true, 'booked'), + ('bd-dr-019', 'bd-e-016', 'dock-h7-2', 'out', CURRENT_DATE + 6, 17, 2, '(주)머티리얼테크', '29가 1019', '11t 이상', '전시 자재', false, 'booked'), + ('bd-dr-020', 'bd-e-026', 'dock-h7-3', 'in', CURRENT_DATE + -3, 8, 2, '로지스테크', '30가 1020', '1t', '전시 자재', false, 'booked'), + ('bd-dr-021', 'bd-e-036', 'dock-h7-4', 'out', CURRENT_DATE + -2, 9, 2, '바이오테크', '31가 1021', '2.5t', '전시 자재', true, 'priority'), + ('bd-dr-022', 'bd-e-046', 'dock-h7-5', 'in', CURRENT_DATE + -1, 10, 2, '(주)헬스랩테크', '32가 1022', '5t', '전시 자재', false, 'booked'), + ('bd-dr-023', 'bd-e-056', 'dock-h7-6', 'out', CURRENT_DATE + 0, 11, 2, '에너지테크', '33가 1023', '11t 이상', '전시 자재', false, 'booked'), + ('bd-dr-024', 'bd-e-006', 'dock-h7-1', 'in', CURRENT_DATE + 1, 12, 2, '(주)스마트테크', '34가 1024', '1t', '전시 자재', true, 'booked'), + ('bd-dr-025', 'bd-e-016', 'dock-h7-2', 'out', CURRENT_DATE + 2, 13, 2, '글로벌테크', '35가 1025', '2.5t', '전시 자재', false, 'booked'), + ('bd-dr-026', 'bd-e-026', 'dock-h7-3', 'in', CURRENT_DATE + 3, 14, 2, '넥스트테크', '36가 1026', '5t', '전시 자재', false, 'booked'), + ('bd-dr-027', 'bd-e-036', 'dock-h7-4', 'out', CURRENT_DATE + 4, 15, 2, '(주)미래테크', '37가 1027', '11t 이상', '전시 자재', true, 'booked'), + ('bd-dr-028', 'bd-e-046', 'dock-h7-5', 'in', CURRENT_DATE + 5, 16, 2, '한빛테크', '38가 1028', '1t', '전시 자재', false, 'priority'), + ('bd-dr-029', 'bd-e-056', 'dock-h7-6', 'out', CURRENT_DATE + 6, 17, 2, '(주)대한테크', '39가 1029', '2.5t', '전시 자재', false, 'booked'), + ('bd-dr-030', 'bd-e-006', 'dock-h7-1', 'in', CURRENT_DATE + -3, 8, 2, '테크시스템즈', '40가 1030', '5t', '전시 자재', true, 'booked'), + ('bd-dr-031', 'bd-e-016', 'dock-h7-2', 'out', CURRENT_DATE + -2, 9, 2, '로보틱스시스템즈', '41가 1031', '11t 이상', '전시 자재', false, 'booked'), + ('bd-dr-032', 'bd-e-026', 'dock-h7-3', 'in', CURRENT_DATE + -1, 10, 2, '(주)인더스트리시스템즈', '42가 1032', '1t', '전시 자재', false, 'booked'), + ('bd-dr-033', 'bd-e-036', 'dock-h7-4', 'out', CURRENT_DATE + 0, 11, 2, '시스템즈시스템즈', '43가 1033', '2.5t', '전시 자재', true, 'booked'), + ('bd-dr-034', 'bd-e-046', 'dock-h7-5', 'in', CURRENT_DATE + 1, 12, 2, '(주)솔루션시스템즈', '44가 1034', '5t', '전시 자재', false, 'booked'), + ('bd-dr-035', 'bd-e-056', 'dock-h7-6', 'out', CURRENT_DATE + 2, 13, 2, '이노베이션시스템즈', '45가 1035', '11t 이상', '전시 자재', false, 'priority'), + ('bd-dr-036', 'bd-e-006', 'dock-h7-1', 'in', CURRENT_DATE + 3, 14, 2, '오토메이션시스템즈', '46가 1036', '1t', '전시 자재', true, 'booked'), + ('bd-dr-037', 'bd-e-016', 'dock-h7-2', 'out', CURRENT_DATE + 4, 15, 2, '(주)디자인시스템즈', '47가 1037', '2.5t', '전시 자재', false, 'booked'), + ('bd-dr-038', 'bd-e-026', 'dock-h7-3', 'in', CURRENT_DATE + 5, 16, 2, '엔지니어링시스템즈', '48가 1038', '5t', '전시 자재', false, 'booked'), + ('bd-dr-039', 'bd-e-036', 'dock-h7-4', 'out', CURRENT_DATE + 6, 17, 2, '(주)일렉트릭시스템즈', '49가 1039', '11t 이상', '전시 자재', true, 'booked'), + ('bd-dr-040', 'bd-e-046', 'dock-h7-5', 'in', CURRENT_DATE + -3, 8, 2, '네트웍스시스템즈', '50가 1040', '1t', '전시 자재', false, 'booked'), + ('bd-dr-041', 'bd-e-056', 'dock-h7-6', 'out', CURRENT_DATE + -2, 9, 2, '미디어시스템즈', '51가 1041', '2.5t', '전시 자재', false, 'booked'), + ('bd-dr-042', 'bd-e-006', 'dock-h7-1', 'in', CURRENT_DATE + -1, 10, 2, '(주)디스플레이시스템즈', '52가 1042', '5t', '전시 자재', true, 'priority'), + ('bd-dr-043', 'bd-e-016', 'dock-h7-2', 'out', CURRENT_DATE + 0, 11, 2, '컨스트럭션시스템즈', '53가 1043', '11t 이상', '전시 자재', false, 'booked'), + ('bd-dr-044', 'bd-e-026', 'dock-h7-3', 'in', CURRENT_DATE + 1, 12, 2, '(주)부스시스템즈', '54가 1044', '1t', '전시 자재', false, 'booked'), + ('bd-dr-045', 'bd-e-036', 'dock-h7-4', 'out', CURRENT_DATE + 2, 13, 2, '전기시설시스템즈', '55가 1045', '2.5t', '전시 자재', true, 'booked'), + ('bd-dr-046', 'bd-e-046', 'dock-h7-5', 'in', CURRENT_DATE + 3, 14, 2, '구조시스템즈', '56가 1046', '5t', '전시 자재', false, 'booked'), + ('bd-dr-047', 'bd-e-056', 'dock-h7-6', 'out', CURRENT_DATE + 4, 15, 2, '(주)비전시스템즈', '57가 1047', '11t 이상', '전시 자재', false, 'booked'), + ('bd-dr-048', 'bd-e-006', 'dock-h7-1', 'in', CURRENT_DATE + 5, 16, 2, '센서텍시스템즈', '58가 1048', '1t', '전시 자재', true, 'booked'), + ('bd-dr-049', 'bd-e-016', 'dock-h7-2', 'out', CURRENT_DATE + 6, 17, 2, '(주)머티리얼시스템즈', '59가 1049', '2.5t', '전시 자재', false, 'priority'), + ('bd-dr-050', 'bd-e-026', 'dock-h7-3', 'in', CURRENT_DATE + -3, 8, 2, '로지스시스템즈', '60가 1050', '5t', '전시 자재', false, 'booked'), + ('bd-dr-051', 'bd-e-036', 'dock-h7-4', 'out', CURRENT_DATE + -2, 9, 2, '바이오시스템즈', '61가 1051', '11t 이상', '전시 자재', true, 'booked'), + ('bd-dr-052', 'bd-e-046', 'dock-h7-5', 'in', CURRENT_DATE + -1, 10, 2, '(주)헬스랩시스템즈', '62가 1052', '1t', '전시 자재', false, 'booked'), + ('bd-dr-053', 'bd-e-056', 'dock-h7-6', 'out', CURRENT_DATE + 0, 11, 2, '에너지시스템즈', '63가 1053', '2.5t', '전시 자재', false, 'booked'), + ('bd-dr-054', 'bd-e-006', 'dock-h7-1', 'in', CURRENT_DATE + 1, 12, 2, '(주)스마트시스템즈', '64가 1054', '5t', '전시 자재', true, 'booked'), + ('bd-dr-055', 'bd-e-016', 'dock-h7-2', 'out', CURRENT_DATE + 2, 13, 2, '글로벌시스템즈', '65가 1055', '11t 이상', '전시 자재', false, 'booked'), + ('bd-dr-056', 'bd-e-026', 'dock-h7-3', 'in', CURRENT_DATE + 3, 14, 2, '넥스트시스템즈', '66가 1056', '1t', '전시 자재', false, 'priority'), + ('bd-dr-057', 'bd-e-036', 'dock-h7-4', 'out', CURRENT_DATE + 4, 15, 2, '(주)미래시스템즈', '67가 1057', '2.5t', '전시 자재', true, 'booked'), + ('bd-dr-058', 'bd-e-046', 'dock-h7-5', 'in', CURRENT_DATE + 5, 16, 2, '한빛시스템즈', '68가 1058', '5t', '전시 자재', false, 'booked'), + ('bd-dr-059', 'bd-e-056', 'dock-h7-6', 'out', CURRENT_DATE + 6, 17, 2, '(주)대한시스템즈', '69가 1059', '11t 이상', '전시 자재', false, 'booked'), + ('bd-dr-060', 'bd-e-006', 'dock-h7-1', 'in', CURRENT_DATE + -3, 8, 2, '테크인더스트리', '70가 1060', '1t', '전시 자재', true, 'booked') +ON CONFLICT (id) DO NOTHING; + +-- 말미: 신규 company 파생 컬럼 백필(V37 name→company_name 등, IS NULL 가드 · 멱등). +UPDATE company SET company_name = name WHERE company_name IS NULL AND name IS NOT NULL; +UPDATE company SET biz_no = registration_no WHERE biz_no IS NULL AND registration_no IS NOT NULL; +UPDATE company SET contact_tel = phone WHERE contact_tel IS NULL AND phone IS NOT NULL; + +-- ============================================================================ +-- 끝. 테이블별 건수는 _workspace/impl_bulk_seed.md 참조. +-- ============================================================================ diff --git a/src/backend/src/main/resources/db/migration/V43__visitor_guide_and_event_calendar.sql b/src/backend/src/main/resources/db/migration/V43__visitor_guide_and_event_calendar.sql new file mode 100644 index 0000000..36da351 --- /dev/null +++ b/src/backend/src/main/resources/db/migration/V43__visitor_guide_and_event_calendar.sql @@ -0,0 +1,185 @@ +-- V43 관람객 방문가이드 + 연간/월간 전시 캘린더 (소유자 지시 2026-07-12) +-- 목적: AI 관람 도우미가 실시간 외부호출 없이 DB 근거로 답하도록 관람객 관심정보를 멱등 적재. +-- (1) visitor_guide — 교통·주차·입장·편의시설·접근성 안내(카테고리형, 공개 조회용) +-- (2) transport_info — 교통수단 구조화(지하철·GTX·버스·자가용·공항·KTX) +-- (3) 연/월 그룹 조회 지원 — event 파생컬럼(event_year·event_month) + 인덱스 + 캘린더 뷰 +-- 원천: kintex.com 공개 페이지 크롤 (_workspace/crawl_visitor_guide_raw.md). 자격증명/PII 없음. +-- 불변: V1~V42 파일 불변. 순증(additive)·멱등만 수행(단일 트랜잭션 · Flyway 기본). +-- 테넌트 표준(V31): 신규 테넌트 소유 테이블은 tenant_id DEFAULT 'KINTEX' + PRIMARY KEY (tenant_id, id) 선두. + +-- ============================================================================ +-- 1) 관람객 방문가이드 — 카테고리형 안내 (공개 조회 · 민감정보 없음) +-- category: TRANSPORT | PARKING | ADMISSION | FACILITY | ACCESS | OVERVIEW +-- ============================================================================ +CREATE TABLE IF NOT EXISTS visitor_guide ( + tenant_id varchar(40) NOT NULL DEFAULT 'KINTEX', + id varchar(60) NOT NULL, + category varchar(20) NOT NULL, -- TRANSPORT|PARKING|ADMISSION|FACILITY|ACCESS|OVERVIEW + title varchar(200) NOT NULL, + body text NOT NULL, -- 안내 본문(마크다운/평문) + summary varchar(400), -- AI 요약 답변용 한 줄 요약 + display_order integer NOT NULL DEFAULT 0, + source_url varchar(300), -- 출처(공개 페이지) + use_yn char(1) NOT NULL DEFAULT 'Y', + updated_at timestamptz NOT NULL DEFAULT now(), + PRIMARY KEY (tenant_id, id) +); +CREATE INDEX IF NOT EXISTS idx_visitor_guide_cat + ON visitor_guide (tenant_id, category, display_order); +COMMENT ON TABLE visitor_guide IS + '관람객 방문가이드(교통/주차/입장/편의/접근성) — AI 관람도우미 근거 데이터. 공개 조회용(PII 없음).'; + +-- ── 시드(멱등: ON CONFLICT DO UPDATE 로 최신 원천 리프레시) ────────────────── +INSERT INTO visitor_guide (id, category, title, body, summary, display_order, source_url) VALUES +-- 교통 ----------------------------------------------------------------------- +('vg-transport-subway', 'TRANSPORT', '지하철 · GTX', + '수도권 전철 3호선 대화역에서 도보/셔틀 연계로 접근합니다. GTX-A 킨텍스역(2024 개통)이 전시장과 직접 연결되어 서울역·수서 방면에서 빠르게 도착할 수 있습니다.', + '3호선 대화역 또는 GTX-A 킨텍스역 이용', 10, + 'https://www.kintex.com/web/ko/html/service/parking_transportation.do'), +('vg-transport-bus', 'TRANSPORT', '버스', + '시내버스 33-1, 55, 83, 89, 97, 98, 062(마을), N007(마을). 직행·광역 1100, 6701, M7646, M7731. 공항버스 5600(김포공항~인천공항 T2 연계). 예) 83번 평일 배차 9~10분.', + '시내·광역·공항버스 다수 정차(83·1100·M7646·5600 등)', 20, + 'https://www.kintex.com/web/ko/html/service/parking_transportation.do'), +('vg-transport-car', 'TRANSPORT', '자가용 · 주소', + '경기도 고양시 일산서구 킨텍스로 217-60 (우편번호 10390). 자유로·수도권제1순환고속도로 이용. 내비게이션 "킨텍스" 검색. 대표전화 031-995-8114.', + '주소 고양시 일산서구 킨텍스로 217-60, 자유로 이용', 30, + 'https://www.kintex.com/web/ko/html/service/parking_transportation.do'), +('vg-transport-ktx-air', 'TRANSPORT', 'KTX · 공항', + 'KTX는 서울역·행신역(경부/전라/중앙선) 이용 후 지하철/버스 환승. 항공은 김포공항에서 5600 공항버스, 인천공항에서 리무진/철도 연계.', + 'KTX 서울·행신역, 김포/인천공항 버스 연계', 40, + 'https://www.kintex.com/web/ko/html/service/parking_transportation.do'), +-- 주차 ----------------------------------------------------------------------- +('vg-parking-fee', 'PARKING', '주차 요금', + '전용주차(2.5t 미만 승용/소형): 기본 20분 1,240원, 추가 10분당 620원, 일 최대 19,000원. 대형(2.5t 이상)/버스: 기본 20분 2,480원, 추가 10분당 1,240원, 일 최대 38,000원. 임시주차장: 10분당 620원(최대 6,500원).', + '승용 기본 20분 1,240원·일 최대 19,000원', 10, + 'https://www.kintex.com/web/ko/service/parking_user.do'), +('vg-parking-lot', 'PARKING', '주차장 위치 · 운영', + '행사 시 임시주차장 T2(대화동 2600-7)·T3(2707-1)·T4(2720) 운영(08:00~18:30). 운영 규모는 행사 규모에 따라 변동됩니다. 대규모 행사 시 대중교통 이용을 권장합니다.', + '임시주차장 T2/T3/T4 08:00~18:30(행사별 변동)', 20, + 'https://www.kintex.com/web/ko/service/parking_user.do'), +('vg-parking-discount', 'PARKING', '주차 할인', + '국가유공자·장애인·친환경(전기/수소)차량 50% 감면, 다일권 할인 적용. 감면은 현장 증빙 확인 후 적용됩니다.', + '유공/장애인/친환경차 50% 감면', 30, + 'https://www.kintex.com/web/ko/service/parking_user.do'), +-- 입장 안내 ------------------------------------------------------------------- +('vg-admission-prereg', 'ADMISSION', '사전등록 · 입장권', + '대부분의 전시는 온라인 무료 사전등록 후 QR/배지를 발급받아 현장에서 빠르게 입장합니다. 일부 전문 전시는 유료 또는 바이어 인증이 필요합니다. 정확한 입장 조건은 각 행사 공지를 확인하세요.', + '온라인 무료 사전등록→QR/배지 발급→현장 입장', 10, + 'https://www.kintex.com'), +('vg-admission-checkin', 'ADMISSION', '현장 체크인 · 운영시간', + '현장 안내데스크 또는 무인 키오스크에서 사전등록 QR을 스캔해 배지를 수령합니다. 전시장 일반 운영시간은 09:00~18:00이며 관람 시간은 통상 10:00~17:00(행사별 공지)입니다.', + '현장 QR 스캔 체크인·관람 10:00~17:00(행사별)', 20, + 'https://www.kintex.com'), +-- 편의시설 ------------------------------------------------------------------- +('vg-facility-food', 'FACILITY', '식음시설 · 편의점', + '전문식당: (1전시장) 소담·본도시락·본우리반상·교동DF푸드코트·일산열무 / (2전시장) 명동칼국수·가쯔레쯔·예당·나주곰탕·IL MAZZIO·파나시아 푸드몰. 카페: 공차·뚜레쥬르·디저트39·망고식스 등. 편의점: 세븐일레븐(24시간), CU 1~5호점(08:00~18:00).', + '전시장별 전문식당·카페·편의점(세븐일레븐 24시간)', 10, + 'https://www.kintex.com/web/ko/service/facilities.do?gubun=A05001'), +('vg-facility-support', 'FACILITY', '지원시설', + 'ATM(현금인출기), 정보데스크(각 홀 부근), 의무실, 동전 보관함, 치안센터를 운영합니다. 분실물·응급 상황은 인근 정보데스크 또는 치안센터를 이용하세요.', + 'ATM·정보데스크·의무실·보관함·치안센터', 20, + 'https://www.kintex.com/web/ko/service/facilities.do?gubun=A05001'), +-- 접근성 --------------------------------------------------------------------- +('vg-access-facility', 'ACCESS', '접근성 · 유아 · 장애인', + '다층 이동 엘리베이터, 휠체어 접근 동선(식음시설·복도), 장애인 전용 주차구역, 수유실(2전시장 2층)을 제공합니다. 휠체어 대여 및 상세 접근 안내는 정보데스크에 문의하세요.', + '엘리베이터·휠체어 동선·장애인 주차·수유실(2전시장 2층)', 10, + 'https://www.kintex.com/web/ko/service/facilities.do?gubun=A05001'), +-- 전시장 개요 ----------------------------------------------------------------- +('vg-overview-venue', 'OVERVIEW', '전시장 개요', + '제1전시장 5개 홀(1~5, 실내 53,541㎡+옥외), 제2전시장 5개 홀(6~10, 54,470㎡). 총 약 108,011㎡로 국내 최대 규모입니다. 6홀은 이벤트홀, 그 외 홀은 이동식 칸막이로 분할/통합됩니다.', + '국내 최대 약 108,011㎡, 제1(5홀)·제2(5홀)전시장', 10, + 'https://www.kintex.com/web/ko/html/facility/exhibition_overview.do') +ON CONFLICT (tenant_id, id) DO UPDATE + SET category = EXCLUDED.category, title = EXCLUDED.title, body = EXCLUDED.body, + summary = EXCLUDED.summary, display_order = EXCLUDED.display_order, + source_url = EXCLUDED.source_url, updated_at = now(); + +-- ============================================================================ +-- 2) 교통수단 구조화 — 지도/경로 UI 및 AI 상세 답변용 +-- mode: SUBWAY | GTX | BUS | CAR | AIRPORT | KTX +-- ============================================================================ +CREATE TABLE IF NOT EXISTS transport_info ( + tenant_id varchar(40) NOT NULL DEFAULT 'KINTEX', + id varchar(60) NOT NULL, + mode varchar(20) NOT NULL, -- SUBWAY|GTX|BUS|CAR|AIRPORT|KTX + line_name varchar(120), -- 3호선 대화역 / GTX-A / 버스 83 등 + route varchar(400), -- 경로/정차/노선 설명 + from_point varchar(120), -- 출발 기준점(서울역, 김포공항 등) + travel_time varchar(60), -- 소요/배차 시간 + note varchar(300), + display_order integer NOT NULL DEFAULT 0, + source_url varchar(300), + updated_at timestamptz NOT NULL DEFAULT now(), + PRIMARY KEY (tenant_id, id) +); +CREATE INDEX IF NOT EXISTS idx_transport_info_mode + ON transport_info (tenant_id, mode, display_order); + +INSERT INTO transport_info (id, mode, line_name, route, from_point, travel_time, note, display_order, source_url) VALUES +('ti-subway-3', 'SUBWAY', '수도권 전철 3호선 대화역', '대화역 하차 후 도보/셔틀 연계', '서울(고속터미널/종로3가 환승)', '역~전시장 셔틀/도보', '3호선 서쪽 종착부', 10, + 'https://www.kintex.com/web/ko/html/service/parking_transportation.do'), +('ti-gtx-a', 'GTX', 'GTX-A 킨텍스역', '킨텍스역 하차, 전시장 직접 연결', '서울역·수서', '수서~킨텍스 약 20분대', '2024 개통, 최속 접근', 20, + 'https://www.kintex.com/web/ko/html/service/parking_transportation.do'), +('ti-bus-city', 'BUS', '시내버스', '33-1, 55, 83, 89, 97, 98, 062마을, N007마을', '고양/일산 일대', '83번 평일 배차 9~10분', '킨텍스 정류장 정차', 30, + 'https://www.kintex.com/web/ko/html/service/parking_transportation.do'), +('ti-bus-express', 'BUS', '직행·광역버스', '1100(직행), 6701(직행), M7646(광역), M7731(광역)', '서울 도심/강남권', '노선별 상이', '광역 급행 연계', 40, + 'https://www.kintex.com/web/ko/html/service/parking_transportation.do'), +('ti-airport', 'AIRPORT', '공항버스 5600', '김포공항~인천공항 T2 연계', '김포/인천공항', '노선 시간표 참조', '공항 이용객 연계', 50, + 'https://www.kintex.com/web/ko/html/service/parking_transportation.do'), +('ti-ktx', 'KTX', 'KTX 서울역·행신역', '서울역·행신역 하차 후 지하철/버스 환승', '전국(경부/전라/중앙선)', '환승 필요', '고속철 연계', 60, + 'https://www.kintex.com/web/ko/html/service/parking_transportation.do'), +('ti-car', 'CAR', '자가용', '자유로·수도권제1순환고속도로 이용, 내비 "킨텍스" 검색', '수도권', '-', '킨텍스로 217-60(우 10390)', 70, + 'https://www.kintex.com/web/ko/html/service/parking_transportation.do') +ON CONFLICT (tenant_id, id) DO UPDATE + SET mode = EXCLUDED.mode, line_name = EXCLUDED.line_name, route = EXCLUDED.route, + from_point = EXCLUDED.from_point, travel_time = EXCLUDED.travel_time, + note = EXCLUDED.note, display_order = EXCLUDED.display_order, + source_url = EXCLUDED.source_url, updated_at = now(); + +-- ============================================================================ +-- 3) 연간/월간 전시 캘린더 지원 — event 파생컬럼 + 인덱스 + 뷰 +-- 이벤트 데이터는 V10(10년 시드)·V32(2026 하반기 크롤)·V42(대량 데모)로 이미 적재됨. +-- 본 절은 "올해 주요 전시", "이번 달/다음 달 일정"을 연/월로 그룹 조회하기 위한 파생/인덱스만 순증. +-- ============================================================================ +-- 3-1) 파생 컬럼(STORED 생성열) — start_date 로부터 자동 산출·동기 유지(향후 시드 자동 반영). +ALTER TABLE event ADD COLUMN IF NOT EXISTS event_year integer + GENERATED ALWAYS AS (EXTRACT(YEAR FROM start_date)::integer) STORED; +ALTER TABLE event ADD COLUMN IF NOT EXISTS event_month integer + GENERATED ALWAYS AS (EXTRACT(MONTH FROM start_date)::integer) STORED; +COMMENT ON COLUMN event.event_year IS '개최연(start_date 파생·STORED) — 연간 그룹 조회 키.'; +COMMENT ON COLUMN event.event_month IS '개최월(start_date 파생·STORED) — 월간 그룹 조회 키.'; + +-- 3-2) 연/월 그룹 조회 가속 인덱스(tenant_id 선두 복합). +CREATE INDEX IF NOT EXISTS idx_event_calendar + ON event (tenant_id, event_year, event_month, start_date); + +-- 3-3) 캘린더 뷰 — 생명주기 상태(예정/진행/종료)를 날짜로 산출(별도 컬럼 백필 불필요). +CREATE OR REPLACE VIEW v_event_calendar AS +SELECT e.tenant_id, + e.id, + e.name, + e.category, + e.hall_label, + e.poster_url, + e.start_date, + e.end_date, + e.event_year, + e.event_month, + CASE + WHEN e.start_date IS NULL THEN 'UNKNOWN' + WHEN CURRENT_DATE < e.start_date THEN 'UPCOMING' -- 예정 + WHEN CURRENT_DATE > COALESCE(e.end_date, e.start_date) THEN 'ENDED' -- 종료 + ELSE 'ONGOING' -- 진행중 + END AS lifecycle + FROM event e + WHERE e.status <> 'cancelled'; +COMMENT ON VIEW v_event_calendar IS + '연/월 캘린더 조회 뷰 — lifecycle(UPCOMING/ONGOING/ENDED)을 날짜로 산출. AI 연간/월간 일정 답변 근거.'; + +-- 3-4) 월별 집계 뷰 — "그 달에 몇 개 전시" 요약. +CREATE OR REPLACE VIEW v_event_monthly_summary AS +SELECT tenant_id, event_year, event_month, count(*) AS event_count + FROM event + WHERE start_date IS NOT NULL AND status <> 'cancelled' + GROUP BY tenant_id, event_year, event_month; +COMMENT ON VIEW v_event_monthly_summary IS '월별 전시 건수 집계 — 연간 기획 요약(월별 개최 수) 근거.'; diff --git a/src/backend/src/main/resources/db/migration/V44__performance_indexes.sql b/src/backend/src/main/resources/db/migration/V44__performance_indexes.sql new file mode 100644 index 0000000..a4f019c --- /dev/null +++ b/src/backend/src/main/resources/db/migration/V44__performance_indexes.sql @@ -0,0 +1,76 @@ +-- V44: 소프트 참조·핫 조회 경로 성능 인덱스 (멱등 · 순수 인덱스 · 스키마 불변) +-- 배경: 데이터 표준이 물리 FK 최소화 → PostgreSQL 은 FK/소프트참조 컬럼에 인덱스를 자동 생성하지 않는다. +-- 조인/필터/정렬에 실제로 쓰이는 소프트 참조(*_id)·상태(공통코드값)·기간 컬럼 중, 기존 인덱스· +-- PK((tenant_id,id))·UNIQUE 제약 자동 인덱스로 커버되지 않는 누락분만 실사용 쿼리 근거로 추가한다. +-- 원칙: 전부 CREATE INDEX IF NOT EXISTS(멱등) · 단일 트랜잭션 · 순증 · 중복/PK커버 조합 제외. +-- 소유: 성능 인덱스 마이그레이션의 단일 소유 파일(DA 감사 트랙은 별도 인덱스 파일을 만들지 않음). +-- 신규 테이블/컬럼 없음 — V43 다음 순번. + +-- ───────────────────────────────────────────────────────────────────────────── +-- 1) hall_assignment(event_id, is_primary) — 최고 빈도 핫패스 +-- 근거: 이벤트 목록의 "대표 홀" LATERAL 서브쿼리 — +-- DashboardMapper.listEvents / EventCatalogMapper.list / HomeMapper / +-- MarketingMapper / OpsMapper.myEvents 의 +-- "SELECT hall_id FROM hall_assignment ha WHERE ha.event_id = e.id ORDER BY is_primary DESC LIMIT 1", +-- PublicSiteMapper "LEFT JOIN hall_assignment ha ON ha.event_id = e.id AND ha.is_primary = true", +-- HallAssignMapper "WHERE ha.event_id = #{eventId} ORDER BY ha.is_primary DESC". +-- 기존 idx_hall_assignment_hall_period 는 (hall_id, occupy_start, occupy_end) 로 event_id 미커버. +CREATE INDEX IF NOT EXISTS idx_hall_assignment_event + ON hall_assignment (event_id, is_primary DESC); + +-- ───────────────────────────────────────────────────────────────────────────── +-- 2) event_member(company_id, event_id) — 업체 스코프 조인 +-- 근거: AuctionMapper 초대 입찰자 조회 +-- "JOIN event_member em ON em.company_id = ai.company_id AND em.event_id = (SELECT event_id FROM auction ...)", +-- OpsMapper.summary awardedBooths "EXISTS(SELECT 1 FROM event_member m WHERE m.company_id = b.company_id ...)". +-- 기존 idx_event_member_user(user_id) · UNIQUE(event_id,user_id,role_code) 는 company_id 선두 미커버. +CREATE INDEX IF NOT EXISTS idx_event_member_company + ON event_member (company_id, event_id); + +-- ───────────────────────────────────────────────────────────────────────────── +-- 3) bid(company_id) — 계약사(입찰업체) 내 입찰 조회 +-- 근거: AuctionMapper.contractorMyBids "FROM bid b JOIN auction a ON a.id = b.auction_id WHERE b.company_id = #{companyId} ORDER BY a.deadline". +-- 기존 idx_bid_auction(auction_id) · UNIQUE(auction_id,company_id) 는 company_id 선두 미커버. +CREATE INDEX IF NOT EXISTS idx_bid_company + ON bid (company_id); + +-- ───────────────────────────────────────────────────────────────────────────── +-- 4) tax_invoice(invoice_id) — 정산 세금계산서 역참조 +-- 근거: SettlementMapper.getTaxInvoiceByInvoice "FROM tax_invoice WHERE invoice_id = #{invoiceId}". +-- 기존 인덱스는 (event_id) 만 존재 → invoice_id(소프트 FK) 미커버. +CREATE INDEX IF NOT EXISTS idx_tax_invoice_invoice + ON tax_invoice (invoice_id); + +-- ───────────────────────────────────────────────────────────────────────────── +-- 5) message(sender_id, created_at) — 쪽지 보낸함 +-- 근거: MessageMapper.listSent "FROM message m WHERE m.sender_id = #{userId} ORDER BY m.created_at DESC LIMIT/OFFSET". +-- message 테이블엔 인덱스 없음(수신함은 message_recipient(recipient_id,read_at) 로 커버). +CREATE INDEX IF NOT EXISTS idx_message_sender + ON message (sender_id, created_at DESC); + +-- ───────────────────────────────────────────────────────────────────────────── +-- 6) cms_content 공개 게시 목록 — 상태(공통코드값) 필터 + 타입 + 게시일 정렬 +-- 근거: CmsMapper.listPublished(공개 홍보사이트) +-- "WHERE status = 'published' AND content_type = #{type} ORDER BY published_at DESC NULLS LAST". +-- 기존 idx_cms_content_status(status) 는 content_type/published_at 정렬 미커버. 게시분만 부분 인덱스로 경량화. +CREATE INDEX IF NOT EXISTS idx_cms_content_published + ON cms_content (content_type, published_at DESC) + WHERE status = 'published'; + +-- ───────────────────────────────────────────────────────────────────────────── +-- 7) visitor_registration(event_id, checkin_state) — 체크인 상태(공통코드값) 필터 +-- 근거: VisitorMapper.checkinHourly +-- "WHERE event_id = #{eventId} AND checkin_state = 'done' AND checkin_at IS NOT NULL ORDER BY to_char(checkin_at,'HH24')". +-- 대용량(관람객 등록) 테이블에서 (event_id) 만으로는 상태 서브셋 필터 비용 큼. 기존 (event_id,registered_at) 와 별개. +CREATE INDEX IF NOT EXISTS idx_visitor_reg_checkin_state + ON visitor_registration (event_id, checkin_state); + +-- ───────────────────────────────────────────────────────────────────────────── +-- 8) lead 팔로업 스윕 — hot 리드(score>=80) 전역 조회(이벤트 무관 배치) +-- 근거: FollowupMapper.findUnprocessedHotLeads +-- "FROM lead l LEFT JOIN edm_followup_log f ... WHERE l.score >= 80 AND f.id IS NULL ORDER BY l.score DESC, l.id". +-- 기존 idx_lead_score 는 (event_id, score DESC) — event_id 선두라 전역 score 범위스캔에 부적합 → 부분 인덱스. +-- (조인측 edm_followup_log 는 UNIQUE(event_id,lead_id) 자동 인덱스로 이미 커버.) +CREATE INDEX IF NOT EXISTS idx_lead_hot_followup + ON lead (score DESC) + WHERE score >= 80; diff --git a/src/backend/src/main/resources/db/migration/V45__user_profile_photo.sql b/src/backend/src/main/resources/db/migration/V45__user_profile_photo.sql new file mode 100644 index 0000000..d9a758c --- /dev/null +++ b/src/backend/src/main/resources/db/migration/V45__user_profile_photo.sql @@ -0,0 +1,12 @@ +-- 킨텍스 — 사용자 프로필 사진 + 연락처 컬럼(순증, 멱등). 모바일 내정보 아바타 계약 충족. +-- V1~V44 불변. 본 마이그레이션은 additive만 수행한다(파괴적 변경 없음). +-- 보안 불변(계약 §0-3): photo_path 는 서버 생성 UUID 파일명만 저장(원본 파일명 미신뢰). +-- password_hash·otp_secret 등 민감 컬럼과 무관 — 프로필 사진 메타데이터만 추가한다. + +ALTER TABLE app_user ADD COLUMN IF NOT EXISTS photo_path varchar(200); -- {uploadDir}/avatars/ 하위 UUID 파일명(경로 아님) +ALTER TABLE app_user ADD COLUMN IF NOT EXISTS photo_content_type varchar(40); -- image/png|jpeg|webp (서빙 Content-Type) +ALTER TABLE app_user ADD COLUMN IF NOT EXISTS photo_updated_at timestamptz; -- 마지막 업로드 시각(캐시 무효화 참조) +ALTER TABLE app_user ADD COLUMN IF NOT EXISTS phone varchar(40); -- 내정보 표시용 연락처(선택, /me 순증) + +COMMENT ON COLUMN app_user.photo_path IS '프로필 사진 저장 파일명(서버 UUID) — 원본 파일명 미저장, 경로 순회 불가'; +COMMENT ON COLUMN app_user.photo_content_type IS '프로필 사진 MIME(화이트리스트: image/png|image/jpeg|image/webp)'; diff --git a/src/backend/src/main/resources/mybatis/mapper/UserMapper.xml b/src/backend/src/main/resources/mybatis/mapper/UserMapper.xml index f380de4..84e0029 100644 --- a/src/backend/src/main/resources/mybatis/mapper/UserMapper.xml +++ b/src/backend/src/main/resources/mybatis/mapper/UserMapper.xml @@ -43,4 +43,35 @@ WHERE registration_no = #{registrationNo} + + + + + + + + + UPDATE app_user + SET photo_path = #{photoPath}, + photo_content_type = #{photoContentType}, + photo_updated_at = now(), + updated_at = now() + WHERE id = #{userId} + + diff --git a/src/backend/src/test/java/com/zioinfo/kintex/auth/ProfilePhotoServiceTest.java b/src/backend/src/test/java/com/zioinfo/kintex/auth/ProfilePhotoServiceTest.java new file mode 100644 index 0000000..63087b3 --- /dev/null +++ b/src/backend/src/test/java/com/zioinfo/kintex/auth/ProfilePhotoServiceTest.java @@ -0,0 +1,93 @@ +package com.zioinfo.kintex.auth; + +import com.zioinfo.kintex.auth.mapper.UserMapper; +import com.zioinfo.kintex.auth.profile.ProfilePhotoService; +import com.zioinfo.kintex.common.error.ApiException; +import com.zioinfo.kintex.common.error.ErrorCode; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.nio.file.Path; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * 프로필 사진 서비스 — 매직바이트 판별 + base64 저장/검증 단위테스트. + * DB(UserMapper)는 목으로 대체하고 파일 저장은 임시 디렉터리로 격리한다. + */ +class ProfilePhotoServiceTest { + + // ── 매직바이트 판별(SVG/위조 헤더 차단) ───────────────────────── + + @Test + void detectsPng() { + byte[] png = {(byte) 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0, 0}; + assertEquals("png", ProfilePhotoService.detectImageType(png)); + } + + @Test + void detectsJpeg() { + byte[] jpg = {(byte) 0xFF, (byte) 0xD8, (byte) 0xFF, (byte) 0xE0, 0, 0}; + assertEquals("jpg", ProfilePhotoService.detectImageType(jpg)); + } + + @Test + void detectsWebp() { + byte[] webp = {'R', 'I', 'F', 'F', 0, 0, 0, 0, 'W', 'E', 'B', 'P'}; + assertEquals("webp", ProfilePhotoService.detectImageType(webp)); + } + + @Test + void rejectsSvgAndText() { + byte[] svg = "".getBytes(); + assertNull(ProfilePhotoService.detectImageType(svg), "SVG(실행형)는 이미지로 판별되면 안 됨"); + byte[] script = "".getBytes(); + assertNull(ProfilePhotoService.detectImageType(script)); + } + + // ── base64 업로드 저장 ────────────────────────────────────────── + + @Test + void base64PngRoundTrip(@TempDir Path tmp) { + UserMapper mapper = mock(UserMapper.class); + when(mapper.findPhotoMeta("u1")).thenReturn(null); + ProfilePhotoService svc = new ProfilePhotoService(mapper, tmp.toString()); + + byte[] png = {(byte) 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 1, 2, 3}; + String b64 = java.util.Base64.getEncoder().encodeToString(png); + + String url = svc.uploadBase64("u1", b64, "image/png"); + assertEquals("/api/auth/users/u1/photo", url); + } + + @Test + void base64RejectsNonImage(@TempDir Path tmp) { + UserMapper mapper = mock(UserMapper.class); + ProfilePhotoService svc = new ProfilePhotoService(mapper, tmp.toString()); + String b64 = java.util.Base64.getEncoder().encodeToString("not-an-image".getBytes()); + ApiException ex = assertThrows(ApiException.class, () -> svc.uploadBase64("u1", b64, "image/png")); + assertEquals(ErrorCode.VALIDATION, ex.getCode()); + } + + @Test + void loadReturns404WhenNoPhoto(@TempDir Path tmp) { + UserMapper mapper = mock(UserMapper.class); + when(mapper.findPhotoMeta("u1")).thenReturn(null); + ProfilePhotoService svc = new ProfilePhotoService(mapper, tmp.toString()); + ApiException ex = assertThrows(ApiException.class, () -> svc.load("u1")); + assertEquals(ErrorCode.NOT_FOUND, ex.getCode()); + } + + @Test + void serveUrlBuildsExpectedPath() { + assertEquals("/api/auth/users/abc/photo", ProfilePhotoService.serveUrl("abc")); + assertNotNull(ProfilePhotoService.serveUrl("x")); + assertTrue(ProfilePhotoService.serveUrl("x").startsWith("/api/auth/")); + } +} diff --git a/src/backend/src/test/java/com/zioinfo/kintex/config/SecretStartupValidatorTest.java b/src/backend/src/test/java/com/zioinfo/kintex/config/SecretStartupValidatorTest.java new file mode 100644 index 0000000..3196911 --- /dev/null +++ b/src/backend/src/test/java/com/zioinfo/kintex/config/SecretStartupValidatorTest.java @@ -0,0 +1,64 @@ +package com.zioinfo.kintex.config; + +import com.zioinfo.kintex.auth.AuthProperties; +import com.zioinfo.kintex.security.SecurityPolicyProperties; +import org.junit.jupiter.api.Test; +import org.springframework.mock.env.MockEnvironment; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * B12 — 운영 시크릿 fail-fast 검증기 단위테스트. + * 시크릿 값 자체는 예외 메시지에 담기지 않음(항목명·조치만)을 함께 확인한다. + */ +class SecretStartupValidatorTest { + + private AuthProperties auth(String secret) { + AuthProperties p = new AuthProperties(); + p.setJwtSecret(secret); + return p; + } + + private SecurityPolicyProperties security(String otpKey) { + SecurityPolicyProperties p = new SecurityPolicyProperties(); + p.getOtp().setEncKey(otpKey); + return p; + } + + @Test + void prodFailsFastOnDevJwtFallback() { + MockEnvironment env = new MockEnvironment().withProperty("kintex.security.enforce-secrets", "true"); + SecretStartupValidator v = new SecretStartupValidator( + env, auth("CHANGE_ME_DEV_ONLY_kintex_local_secret_key_min_32_bytes"), + security("base64key32byteslongxxxxxxxxxxxxx")); + IllegalStateException ex = assertThrows(IllegalStateException.class, v::afterPropertiesSet); + // 시크릿 값이 메시지에 노출되지 않아야 함(항목명만). + org.junit.jupiter.api.Assertions.assertFalse(ex.getMessage().contains("CHANGE_ME_DEV_ONLY_kintex")); + } + + @Test + void prodFailsFastOnMissingOtpKey() { + MockEnvironment env = new MockEnvironment().withProperty("kintex.security.enforce-secrets", "true"); + SecretStartupValidator v = new SecretStartupValidator( + env, auth("a-strong-production-secret-of-at-least-32-bytes!!"), security("")); + assertThrows(IllegalStateException.class, v::afterPropertiesSet); + } + + @Test + void prodPassesWithStrongSecrets() { + MockEnvironment env = new MockEnvironment().withProperty("kintex.security.enforce-secrets", "true"); + SecretStartupValidator v = new SecretStartupValidator( + env, auth("a-strong-production-secret-of-at-least-32-bytes!!"), + security("base64key32byteslongxxxxxxxxxxxxx")); + assertDoesNotThrow(v::afterPropertiesSet); + } + + @Test + void devOnlyWarnsNeverThrows() { + MockEnvironment env = new MockEnvironment(); // no prod profile, no enforce + SecretStartupValidator v = new SecretStartupValidator( + env, auth("CHANGE_ME_DEV_ONLY_kintex_local_secret_key_min_32_bytes"), security("")); + assertDoesNotThrow(v::afterPropertiesSet); + } +} diff --git a/src/frontend/index.html b/src/frontend/index.html index b26bfff..4e55889 100644 --- a/src/frontend/index.html +++ b/src/frontend/index.html @@ -4,6 +4,10 @@ KINTEX AI 전시·행사시스템 + + + + {children}; } -/** 루트/미매칭 분기 — 인증=홈, 미인증=공개 소개 랜딩(제품 아이덴티티, 2026-07-12 소유자 지적). */ +/** + * 루트/미매칭 분기 — 인증=홈 리다이렉트, 미인증=SCR-T0 3-트랙 공개 관문 직접 렌더 + * (design.md §3C-0: 미인증 `/`는 대외 유입 관문. 2026-07-12 소유자 지적 반영). + */ function RootGate() { const isAuthenticated = useAuthStore((s) => s.isAuthenticated); - return ; + if (isAuthenticated) return ; + return ; } export function App() { @@ -246,6 +254,11 @@ export function App() { } /> + {/* SCR-T1~T3 트랙 서브홈 — 비로그인 공개, PublicShell+TrackSwitcher */} + } /> + } /> + } /> + {/* SCR-P1~P7 공개 홍보 사이트 — 비로그인, 자체 PublicShell */} } /> } /> diff --git a/src/frontend/src/api/auctionApi.ts b/src/frontend/src/api/auctionApi.ts new file mode 100644 index 0000000..711301e --- /dev/null +++ b/src/frontend/src/api/auctionApi.ts @@ -0,0 +1,61 @@ +/* + * M15 공사/장치 옥션 API 클라이언트 — 자체 모듈. + * ★ 공유 api/endpoints.ts 를 수정하지 않기 위해 옥션 엔드포인트를 여기에 캡슐화한다. + * 공용 client.ts 의 api(get/post) 만 재사용하여 JWT·에러 봉투 처리를 공유한다. + * 실시간 순위: 현재 WebSocket 채널이 없어 폴링 계약으로 구현(refetchInterval). + * TODO(M5 WS 재사용): STOMP /topic/auction/{id} 브로드캐스트 도입 시 폴링 제거. + */ +import { api } from './client'; +import type { + AuctionDetail, + AuctionSummary, + AwardRequest, + AwardResult, + AwardView, + BidRequest, + BidResult, + CreateAuctionRequest, +} from './auctionTypes'; + +/** 실시간 순위 폴링 주기(ms). WebSocket 도입 전 계약. */ +export const AUCTION_POLL_MS = 15_000; + +export const auctionApi = { + /** GET /api/auctions — 옥션 목록(카드). */ + list: (params?: { eventId?: string; page?: number; size?: number }) => { + const q = new URLSearchParams(); + if (params?.eventId) q.set('eventId', params.eventId); + if (params?.page != null) q.set('page', String(params.page)); + if (params?.size != null) q.set('size', String(params.size)); + const qs = q.toString(); + return api.get(`/api/auctions${qs ? `?${qs}` : ''}`); + }, + + /** POST /api/auctions — 옥션 개설(발주자). */ + create: (body: CreateAuctionRequest) => api.post('/api/auctions', body), + + /** GET /api/auctions/{id} — 상세·봉인 순위. */ + detail: (id: string) => api.get(`/api/auctions/${encodeURIComponent(id)}`), + + /** POST /api/auctions/{id}/bids — 응찰(견적서 제출). 서버가 소계/부가세/총액 재계산. */ + bid: (id: string, body: BidRequest) => + api.post(`/api/auctions/${encodeURIComponent(id)}/bids`, body), + + /** POST /api/auctions/{id}/close — 라운드 마감 처리(발주자). */ + close: (id: string) => api.post(`/api/auctions/${encodeURIComponent(id)}/close`), + + /** GET /api/auctions/{id}/award-view — 마감 후 발주자 전용 견적 비교. */ + awardView: (id: string) => api.get(`/api/auctions/${encodeURIComponent(id)}/award-view`), + + /** POST /api/auctions/{id}/award — 낙찰(발주자·사유 필수). */ + award: (id: string, body: AwardRequest) => + api.post(`/api/auctions/${encodeURIComponent(id)}/award`, body), +}; + +/** react-query 키 팩토리 — 옥션 도메인. */ +export const auctionKeys = { + all: ['auction'] as const, + list: (eventId?: string) => ['auction', 'list', eventId ?? 'all'] as const, + detail: (id: string) => ['auction', 'detail', id] as const, + awardView: (id: string) => ['auction', 'awardView', id] as const, +}; diff --git a/src/frontend/src/api/auctionTypes.ts b/src/frontend/src/api/auctionTypes.ts new file mode 100644 index 0000000..f66c361 --- /dev/null +++ b/src/frontend/src/api/auctionTypes.ts @@ -0,0 +1,161 @@ +/* + * M15 공사/장치 옥션(역경매) API 타입 — 백엔드 com.zioinfo.kintex.auction.dto.AuctionDtos 와 정합. + * ★ 자체 모듈: 공유 api/types.ts 를 수정하지 않기 위해 옥션 전용 타입을 여기에 둔다. + * 봉인 입찰: 응찰자 뷰(RankRow)는 마감 전 타사 금액을 price=null·priceMasked=true 로 마스킹. + */ + +export type AuctionTypeLabel = '역경매' | 'RFQ'; +export type AuctionStatusLabel = '진행중' | '마감' | '정산중'; +export type AwardCriteria = 'lowest' | 'comprehensive'; +export type MaterialKind = 'layout' | 'design' | 'boq' | 'aiimage'; + +export interface Weights { + price: number; + reputation: number; + delivery: number; +} + +export interface AuctionSummary { + id: string; + eventId: string; + title: string; + category: string; + type: AuctionTypeLabel; + status: AuctionStatusLabel; + round: number; + dday: number; + bidderCount: number; + lowestPrice: number | null; + awardedCompany: string | null; + finalPrice: number | null; + materials: string[]; + accent: string; +} + +export interface MaterialPackage { + layoutUrl: string | null; + designUrl: string | null; + boqUrl: string | null; + aiImageUrl: string | null; +} + +export interface RankRow { + rank: number; + alias: string; + isMe: boolean; + isLowest: boolean; + priceMasked: boolean; + price: number | null; +} + +export interface AuctionDetail { + id: string; + eventId: string; + title: string; + category: string; + type: AuctionTypeLabel; + status: AuctionStatusLabel; + round: number; + dday: number; + deadline: string; + bidderCount: number; + lowestPrice: number | null; + awardCriteria: AwardCriteria; + weights: Weights; + materials: string[]; + materialPackage: MaterialPackage; + sealed: boolean; + myRank: number | null; + myPrice: number | null; + ranking: RankRow[]; + canBid: boolean; + canViewAward: boolean; + awardedCompany: string | null; + finalPrice: number | null; +} + +/** 견적서 라인아이템 — 금액=수량×단가는 서버가 재계산(클라 값 불신). */ +export interface QuoteLine { + trade: string; + material: string; + qty: number; + unitPrice: number; +} + +export interface BidRequest { + total?: number; + leadDays?: number; + validUntil?: string; + terms?: string; + lines?: QuoteLine[]; +} + +export interface BidResult { + id: string; + round: number; + subtotal: number; + vat: number; + total: number; + grandTotal: number; + version: number; + myRank: number | null; + lowestPrice: number | null; +} + +export interface CreateAuctionRequest { + eventId: string; + title: string; + category?: string; + type?: AuctionTypeLabel | 'reverse' | 'rfq'; + awardCriteria?: AwardCriteria; + weights?: Weights; + round?: number; + deadline: string; + materialPackageId?: string; + materials?: string[]; + invitedCompanyIds?: string[]; +} + +export interface Quote { + bidId: string; + companyId: string; + alias: string; + subtitle: string; + recommended: boolean; + totalPrice: number; + priceDeltaPct: number; + isLowestPrice: boolean; + leadDays: number; + isShortestLead: boolean; + reputation: number; + score: number; +} + +export interface RiskBar { + label: string; + pct: number; + tone: string; +} + +export interface AwardResult { + id: string; + bidId: string; + companyName: string; + awardedAt: string; +} + +export interface AwardView { + auctionId: string; + auctionTitle: string; + bidderCount: number; + awardCriteria: AwardCriteria; + weights: Weights; + quotes: Quote[]; + riskBars: RiskBar[]; + awarded: AwardResult | null; +} + +export interface AwardRequest { + bidId: string; + reason: string; +} diff --git a/src/frontend/src/i18n/locales/en.json b/src/frontend/src/i18n/locales/en.json index a273e4b..9dcd52a 100644 --- a/src/frontend/src/i18n/locales/en.json +++ b/src/frontend/src/i18n/locales/en.json @@ -1135,6 +1135,33 @@ "citations": "Cited regulations", "notFound": "Not found in regulations", "degraded": "Regulation summary (AI unavailable)" + }, + "assistant": { + "label": "AI Guide", + "inputAria": "Ask the visit assistant", + "sendAria": "Send question", + "chipsLabel": "Frequently asked", + "chip1": "Any robotics expo this weekend?", + "chip2": "Where do I park at KINTEX?", + "chip3": "Recommend a family-friendly exhibition", + "chip4": "How do I pre-register?", + "chip5": "How long via GTX-A?", + "thinking": "Generating answer…", + "error": "Could not get an answer. Please try again.", + "retry": "Try again", + "degraded": "AI guide in maintenance — showing stored-data search results", + "providerAi": "AI answer", + "notFound": "Couldn't find that — please check the full schedule and guide.", + "cardEventsTitle": "Related exhibitions", + "cardTransportTitle": "Getting here", + "cardGuideTitle": "Visit info", + "followupLabel": "Follow-up questions", + "followup1": "Is it free?", + "followup2": "Where to park?", + "followup3": "How to pre-register", + "citations": "Sources", + "register": "Pre-register", + "disclaimer": "AI-generated answer — verify with the official notice." } }, "analytics": { @@ -2140,5 +2167,364 @@ "cancel": "Cancel", "saving": "Saving…", "save": "Save" + }, + "track": { + "shell": { + "wordmark": "KINTEX AI Exhibition & Event System", + "tablistAria": "Select track", + "tab": { + "visitor": "Visitor", + "business": "Business", + "agency": "Agency" + }, + "login": "Log in", + "myHome": "My Home" + }, + "gateway": { + "heroAria": "Product hero", + "aiWatermark": "AI-generated preview image", + "eyebrow": "AI Exhibition & Event Platform", + "title": "KINTEX AI Exhibition & Event System", + "subtitle": "Automating the entire journey from planning to construction and visiting with AI.", + "ctaPreReg": "Visitor Pre-registration", + "ctaInquiry": "Exhibitor Inquiry", + "tracksAria": "Track selection", + "tracksTitle": "What brings you here?", + "tracksSub": "Choose the track that fits your purpose.", + "card": { + "visitor": { + "label": "Visitor", + "title": "Visit an Event", + "desc": "Schedules, pre-registration, and directions", + "cta": "Visitor Track" + }, + "business": { + "label": "Organizer / Exhibitor", + "title": "Host or Exhibit", + "desc": "Rental, auto-quotes, booth applications, construction previews", + "cta": "Business Track" + }, + "agency": { + "label": "Construction Partner", + "title": "Join Construction", + "desc": "Registered vendors, construction auctions, work reports", + "cta": "Agency Track" + } + }, + "eventsAria": "Current and upcoming exhibitions", + "eventsTitle": "Current & Upcoming Exhibitions", + "eventsSub": "Explore ongoing and upcoming exhibitions.", + "eventsMore": "View all schedules" + }, + "visitor": { + "heroAria": "Visitor hero", + "eyebrow": "Visitor Guide", + "title": "Start Your Visit Here", + "subtitle": "Pre-register for faster entry and get alerts for topics you care about.", + "ctaPreReg": "Pre-register", + "ctaTicket": "Buy Tickets", + "eventsAria": "Event schedule", + "eventsTitle": "Event Schedule", + "eventsSub": "Check current and upcoming exhibitions and pre-register.", + "guideAria": "Visit guide", + "guideTitle": "Visit Guide", + "guideSub": "Transport, parking, amenities, and accessibility in one place.", + "guide": { + "transport": { + "title": "Transport", + "desc": "3 min walk from GTX-A KINTEX Stn · intercity buses", + "link": "Directions" + }, + "parking": { + "title": "Parking", + "desc": "Real-time availability, rates, and gates", + "link": "Parking info" + }, + "wayfinding": { + "title": "Indoor Wayfinding", + "desc": "Indoor routes to halls and booths", + "link": "Open wayfinding" + }, + "amenities": { + "title": "Amenities", + "desc": "Dining, nursing room, storage, ATM", + "link": "View amenities" + }, + "accessibility": { + "title": "Accessibility", + "desc": "Wheelchair access, sign-language guidance", + "link": "Accessibility info" + }, + "inquiry": { + "title": "Support", + "desc": "FAQ and contact info", + "link": "Contact us" + } + }, + "noticeAria": "Notices", + "noticeTitle": "Notices", + "noticeSub": "Check the latest notices and alerts.", + "noticeEmpty": "No notices yet.", + "aiWatermark": "AI-generated preview image", + "hero": { + "title": "Ask anything — AI guides your visit", + "subtitle": "Schedules, directions, tickets and booth locations — all in one chat" + }, + "aiTiles": { + "title": "How our AI helps", + "sub": "AI supports every moment of your visit.", + "soon": "Coming soon", + "assistant": { + "title": "AI Visit Assistant", + "desc": "Ask anything" + }, + "recommend": { + "title": "AI Recommendations", + "desc": "Curated to your interests" + }, + "wayfinding": { + "title": "AI Wayfinding", + "desc": "Best indoor route" + }, + "schedule": { + "title": "AI Itinerary", + "desc": "Your own visit course" + }, + "crowd": { + "title": "AI Crowd & Wait", + "desc": "Best low-crowd times" + }, + "render": { + "title": "AI Booth Preview", + "desc": "Booth preview (Business track)" + } + }, + "showcaseTitle": "Ongoing & upcoming", + "showcaseSub": "See what's on now and coming up.", + "recommendTitle": "Exhibitions for your interests", + "recommendSub": "Pick your interests and AI curates for you.", + "recommendTag": "AI picks", + "interests": { + "robot": "Robotics", + "smartfactory": "Smart factory", + "healthcare": "Healthcare", + "eco": "Eco & energy", + "mobility": "Mobility", + "content": "Content & culture" + }, + "guideMore": "Learn more", + "journeyTitle": "How your visit works", + "journeySub": "From pre-registration to check-in in three simple steps.", + "journey": { + "register": { + "title": "Pre-register", + "desc": "Register your visit online.", + "link": "Browse exhibitions" + }, + "badge": { + "title": "Badge & QR", + "desc": "Get your mobile badge and entry QR.", + "link": "Find my ticket" + }, + "checkin": { + "title": "On-site check-in", + "desc": "Scan your QR at the gate and walk in.", + "link": "Sign in" + } + }, + "venueTitle": "About KINTEX", + "venueSub": "Korea's largest exhibition & convention center.", + "venue": { + "halls": { + "label": "Exhibition halls", + "value": "10" + }, + "area": { + "label": "Exhibition area", + "value": "108,556㎡" + }, + "parking": { + "label": "Parking", + "value": "7,500" + }, + "access": { + "label": "Access", + "value": "GTX-A 3-min walk" + } + }, + "briefing": { + "title": "AI Exhibition Briefing", + "sub": "AI curates this year's and this month's upcoming exhibitions for you.", + "viewYear": "Yearly ({{year}})", + "viewMonth": "Monthly", + "aiSummary": "AI summary", + "loading": "Generating AI summary…", + "empty": "No exhibitions scheduled for this period.", + "yearSummary": "{{count}} exhibitions & events are planned in {{year}}, concentrated around {{peak}}.", + "monthSummary": "{{count}} exhibitions this month · weekend visits recommended.", + "monthLabel": "month {{month}}", + "eventsCount": "{{count}} events", + "prevMonth": "Previous month", + "nextMonth": "Next month" + } + }, + "business": { + "heroAria": "Business hero", + "eyebrow": "For Organizers & Exhibitors", + "title": "Host or Exhibit — All Applications Online", + "subtitle": "Handle everything online — from rental to booth application and construction.", + "ctaApply": "Apply / Inquire", + "ctaLogin": "Log in", + "ctaInquiry": "Inquire", + "valueAria": "Value propositions", + "valueTitle": "Choose Your Role", + "valueSub": "Features for organizers and exhibitors.", + "value": { + "organizer": { + "title": "Organizer · Hall Rental", + "desc": "From rental to hall assignment and floor plans in one place.", + "item": { + "rental": "Rental process", + "quote": "Auto quotation", + "floorplan": "Hall assignment & floor plan", + "rate": "Rates & facility specs" + }, + "cta": "Rental inquiry · Log in" + }, + "exhibitor": { + "title": "Exhibitor · Booth Application", + "desc": "From booth and utility applications to AI construction previews.", + "item": { + "apply": "Booth application", + "utility": "Utility application", + "compliance": "Compliance pre-check", + "render": "AI construction preview" + }, + "cta": "Apply · Log in" + } + }, + "processAria": "Rental process", + "processTitle": "Rental Process", + "processSub": "From inquiry to construction and inspection at a glance.", + "process": { + "inquiry": { + "title": "Inquiry", + "desc": "Rental consultation" + }, + "quote": { + "title": "Estimate", + "desc": "Automated estimate" + }, + "contract": { + "title": "Contract", + "desc": "Sign the contract" + }, + "assign": { + "title": "Hall Assignment", + "desc": "Hall and zone assignment" + }, + "booth": { + "title": "Booth Application", + "desc": "Exhibitor booths and utilities" + }, + "inspect": { + "title": "Construction & Inspection", + "desc": "On-site inspection after construction" + } + }, + "rateAria": "Rates and documents", + "rateTitle": "Rates & Documents", + "rateSub": "Rates by booth type and key documents.", + "rate": { + "shell": { + "label": "Shell Scheme", + "price": "Standard", + "desc": "Basic booth with basic power included" + }, + "space": { + "label": "Space Only", + "price": "Space", + "desc": "Custom build, structural calc required" + }, + "premium": { + "label": "Premium", + "price": "Premium", + "desc": "Premium location with extra utilities" + } + }, + "doc": { + "assign": { + "title": "Assignment Application", + "desc": "Hall assignment application form" + }, + "flame": { + "title": "Flame-retardant Certificate", + "desc": "Flame-retardant treatment certificate" + }, + "structure": { + "title": "Structural Calculation", + "desc": "Structural review for space-only booths and rigging" + } + }, + "ctaBandAria": "Log in or inquire", + "ctaBandText": "Already have an account?" + }, + "agency": { + "heroAria": "Agency hero", + "eyebrow": "For Construction & Booth Partners", + "title": "Join Exhibition Construction as a KINTEX Registered Vendor", + "subtitle": "Win projects transparently through construction auctions (reverse auction) — a KINTEX-exclusive approach.", + "ctaLogin": "Vendor Log in", + "ctaRegister": "Apply to Register", + "ctaAuctionLogin": "Log in to bid on auctions", + "ctaWorkReport": "Submit Work Report", + "registryAria": "Registered vendor info", + "registryTitle": "Registered Vendors", + "registrySub": "Operated with 739 registered vendors across 14 categories.", + "gateTitle": "Unregistered vendors cannot participate in construction (self-construction prohibited)", + "gateBody": "Exhibition construction must go through KINTEX-registered vendors. Review the registration process and apply.", + "reg": { + "apply": { + "title": "Apply", + "desc": "Submit registration form" + }, + "review": { + "title": "Review", + "desc": "Qualification and track-record review" + }, + "register": { + "title": "Register", + "desc": "Listed as an official vendor" + }, + "eligible": { + "title": "Eligibility", + "desc": "Bid on auctions and win projects" + } + }, + "auctionAria": "Construction auctions", + "auctionTitle": "Ongoing Construction Auctions (Reverse Auction)", + "auctionSub": "Compete transparently within the budget range.", + "auctionEmptyTitle": "Auction listings are available after logging in", + "auctionEmptyBody": "After logging in as a registered vendor, view ongoing auction listings, budgets, deadlines, and bid status.", + "safetyAria": "Work reports and safety rules", + "safetyTitle": "Work Reports & Safety Rules", + "safetySub": "Review required procedures and rules before on-site work.", + "safety": { + "report": { + "title": "Work Report Procedure", + "desc": "Online work report before on-site work" + }, + "prohibited": { + "title": "Prohibited Works & Noise", + "desc": "Prohibited works, noise limits, rigging structural calc" + }, + "docs": { + "title": "Required Documents", + "desc": "Flame-retardant certificate, insurance" + } + }, + "ctaBandAria": "Vendor log in", + "ctaBandText": "Already a registered vendor?" + } } -} \ No newline at end of file +} diff --git a/src/frontend/src/i18n/locales/ja.json b/src/frontend/src/i18n/locales/ja.json index c72fce2..17ed7f2 100644 --- a/src/frontend/src/i18n/locales/ja.json +++ b/src/frontend/src/i18n/locales/ja.json @@ -1135,6 +1135,33 @@ "citations": "根拠規定", "notFound": "規定で見つかりませんでした", "degraded": "規定原文の要約(AI利用不可)" + }, + "assistant": { + "label": "AI ガイド", + "inputAria": "来場アシスタントに質問", + "sendAria": "質問を送信", + "chipsLabel": "よくある質問", + "chip1": "今週末にロボット展はある?", + "chip2": "KINTEXの駐車場はどこ?", + "chip3": "家族向けの展示を教えて", + "chip4": "事前登録の方法は?", + "chip5": "GTX-Aでどのくらい?", + "thinking": "回答を生成中…", + "error": "回答を取得できませんでした。再度お試しください。", + "retry": "再試行", + "degraded": "AIガイド点検中 — 蓄積データ検索でご案内します", + "providerAi": "AI 回答", + "notFound": "該当情報が見つかりませんでした — 全日程・ガイドをご確認ください。", + "cardEventsTitle": "関連展示", + "cardTransportTitle": "アクセス", + "cardGuideTitle": "来場案内", + "followupLabel": "続けて質問", + "followup1": "無料ですか?", + "followup2": "駐車場はどこ?", + "followup3": "事前登録の方法", + "citations": "出典", + "register": "事前登録", + "disclaimer": "AI生成の回答 — 正確な情報は公式案内をご確認ください。" } }, "analytics": { @@ -2140,5 +2167,364 @@ "cancel": "キャンセル", "saving": "保存中…", "save": "保存" + }, + "track": { + "shell": { + "wordmark": "KINTEX AI 展示·イベントシステム", + "tablistAria": "トラック選択", + "tab": { + "visitor": "観覧", + "business": "ビジネス", + "agency": "協力会社" + }, + "login": "ログイン", + "myHome": "マイホーム" + }, + "gateway": { + "heroAria": "製品ヒーロー", + "aiWatermark": "AI生成の予想画像", + "eyebrow": "AI展示·イベント運営プラットフォーム", + "title": "KINTEX AI 展示·イベントシステム", + "subtitle": "展示企画から施工·観覧まで、全工程をAIで自動化します。", + "ctaPreReg": "観覧事前登録", + "ctaInquiry": "出展のお問い合わせ", + "tracksAria": "トラック分岐", + "tracksTitle": "どのような目的でお越しですか?", + "tracksSub": "目的に合ったトラックをお選びください。", + "card": { + "visitor": { + "label": "観覧者", + "title": "イベント観覧", + "desc": "イベント日程·事前登録·アクセス", + "cta": "観覧トラック" + }, + "business": { + "label": "主催·出展", + "title": "開催·出展", + "desc": "貸館·自動見積·ブース申請·施工案", + "cta": "ビジネストラック" + }, + "agency": { + "label": "工事·協力会社", + "title": "展示施工に参加", + "desc": "登録業者·工事オークション·作業申告", + "cta": "協力会社トラック" + } + }, + "eventsAria": "開催中·予定の展示", + "eventsTitle": "開催中 · 予定の展示", + "eventsSub": "開催中および今後の展示をご確認ください。", + "eventsMore": "全日程を見る" + }, + "visitor": { + "heroAria": "観覧案内ヒーロー", + "eyebrow": "観覧者ご案内", + "title": "展示観覧はここから", + "subtitle": "事前登録で入場がスムーズに。関心分野の通知も受け取れます。", + "ctaPreReg": "事前登録", + "ctaTicket": "チケット予約", + "eventsAria": "イベント日程", + "eventsTitle": "イベント日程", + "eventsSub": "開催中·予定の展示を確認して事前登録。", + "guideAria": "訪問ガイド", + "guideTitle": "訪問ガイド", + "guideSub": "交通·駐車·施設·アクセシビリティを一か所で。", + "guide": { + "transport": { + "title": "交通", + "desc": "GTX-A キンテックス駅 徒歩3分·広域バス", + "link": "アクセス" + }, + "parking": { + "title": "駐車", + "desc": "リアルタイム空き·料金·ゲート", + "link": "駐車案内" + }, + "wayfinding": { + "title": "屋内ナビ", + "desc": "ホール·ブースまでの屋内経路", + "link": "ナビを開く" + }, + "amenities": { + "title": "施設", + "desc": "飲食·授乳室·手荷物·ATM", + "link": "施設を見る" + }, + "accessibility": { + "title": "アクセシビリティ", + "desc": "車いす·手話通訳案内", + "link": "アクセシビリティ案内" + }, + "inquiry": { + "title": "お問い合わせ", + "desc": "FAQ·連絡先", + "link": "問い合わせ" + } + }, + "noticeAria": "お知らせ", + "noticeTitle": "お知らせ", + "noticeSub": "最近のお知らせをご確認ください。", + "noticeEmpty": "お知らせはありません。", + "aiWatermark": "AI生成の予想画像", + "hero": { + "title": "何でも聞いてください — AIが観覧をご案内", + "subtitle": "展示日程・アクセス・入場券・ブース位置を対話でまとめて" + }, + "aiTiles": { + "title": "AIがお手伝いします", + "sub": "観覧のあらゆる瞬間をAIがサポートします。", + "soon": "準備中", + "assistant": { + "title": "AI 観覧アシスタント", + "desc": "何でも聞いてください" + }, + "recommend": { + "title": "AI 展示レコメンド", + "desc": "興味に合わせたキュレーション" + }, + "wayfinding": { + "title": "AI 道案内", + "desc": "屋内の最適ルート" + }, + "schedule": { + "title": "AI 日程アシスタント", + "desc": "自分だけの観覧コース" + }, + "crowd": { + "title": "AI 混雑・待ち時間案内", + "desc": "空いている時間帯を提案" + }, + "render": { + "title": "AI 施工予想画像", + "desc": "ブースプレビュー(ビジネス)" + } + }, + "showcaseTitle": "開催中・開催予定", + "showcaseSub": "開催中と開催予定の展示を一目で。", + "recommendTitle": "あなたの興味に合う展示", + "recommendSub": "関心分野を選ぶとAIがおすすめします。", + "recommendTag": "AI おすすめ", + "interests": { + "robot": "ロボット・自動化", + "smartfactory": "スマートファクトリー", + "healthcare": "ヘルスケア", + "eco": "環境・エネルギー", + "mobility": "モビリティ", + "content": "コンテンツ・文化" + }, + "guideMore": "詳しく見る", + "journeyTitle": "観覧の流れ", + "journeySub": "事前登録から入場まで、かんたん3ステップ。", + "journey": { + "register": { + "title": "事前登録", + "desc": "オンラインで来場情報を登録。", + "link": "展示を見る" + }, + "badge": { + "title": "バッジ・QR発行", + "desc": "モバイルバッジと入場QRを受け取り。", + "link": "入場券照会" + }, + "checkin": { + "title": "現地チェックイン", + "desc": "入口でQRをスキャンして入場。", + "link": "ログイン" + } + }, + "venueTitle": "KINTEX会場のご案内", + "venueSub": "国内最大級の展示・コンベンションセンター。", + "venue": { + "halls": { + "label": "展示ホール", + "value": "10" + }, + "area": { + "label": "展示面積", + "value": "108,556㎡" + }, + "parking": { + "label": "駐車", + "value": "7,500台" + }, + "access": { + "label": "アクセス", + "value": "GTX-A 徒歩3分" + } + }, + "briefing": { + "title": "AI 展示・イベント企画ブリーフィング", + "sub": "今年と今月の開催予定展示をAIが先にまとめます。", + "viewYear": "年間({{year}})", + "viewMonth": "月間", + "aiSummary": "AI 要約", + "loading": "AI要約を生成中…", + "empty": "この期間に予定された展示はありません。", + "yearSummary": "{{year}}年は計{{count}}件の展示・イベントが予定され、{{peak}}に集中します。", + "monthSummary": "今月は{{count}}件の展示が開催されます · 週末の来場がおすすめ。", + "monthLabel": "{{month}}月", + "eventsCount": "{{count}}件", + "prevMonth": "前の月", + "nextMonth": "次の月" + } + }, + "business": { + "heroAria": "ビジネス案内ヒーロー", + "eyebrow": "主催者·出展者ご案内", + "title": "開催·出展、オンライン申請を一元化", + "subtitle": "貸館からブース申請·施工まで、すべてオンラインで。", + "ctaApply": "申請·問い合わせ", + "ctaLogin": "ログイン", + "ctaInquiry": "問い合わせ", + "valueAria": "提供価値", + "valueTitle": "役割に合った入口", + "valueSub": "主催者·出展者向けの機能をご案内します。", + "value": { + "organizer": { + "title": "主催者 · ホール貸館", + "desc": "貸館手続きからホール割当·フロアプランまで一括。", + "item": { + "rental": "貸館手続き", + "quote": "自動見積", + "floorplan": "ホール割当·フロアプラン", + "rate": "料金·施設規格" + }, + "cta": "貸館問い合わせ·ログイン" + }, + "exhibitor": { + "title": "出展者 · ブース申請", + "desc": "ブース·ユーティリティ申請からAI施工案まで。", + "item": { + "apply": "ブース申請", + "utility": "ユーティリティ申請", + "compliance": "規定事前検証", + "render": "AI施工案" + }, + "cta": "出展申請·ログイン" + } + }, + "processAria": "貸館プロセス", + "processTitle": "貸館プロセス", + "processSub": "問い合わせから施工·検収まで一目で。", + "process": { + "inquiry": { + "title": "問い合わせ", + "desc": "貸館相談受付" + }, + "quote": { + "title": "仮見積", + "desc": "自動見積算出" + }, + "contract": { + "title": "契約", + "desc": "契約締結" + }, + "assign": { + "title": "ホール割当", + "desc": "ホール·区画割当" + }, + "booth": { + "title": "ブース申請", + "desc": "出展ブース·ユーティリティ" + }, + "inspect": { + "title": "施工·検収", + "desc": "施工後の現場検収" + } + }, + "rateAria": "料金·書類", + "rateTitle": "料金 · 書類のご案内", + "rateSub": "ブースタイプ別の料率と主な書類。", + "rate": { + "shell": { + "label": "組立ブース", + "price": "基本型", + "desc": "基本ブース·基本電気込み" + }, + "space": { + "label": "独立ブース", + "price": "スペース型", + "desc": "自由施工·構造計算書必要" + }, + "premium": { + "label": "プレミアム", + "price": "特化型", + "desc": "プレミアム位置·追加ユーティリティ" + } + }, + "doc": { + "assign": { + "title": "割当申請書", + "desc": "ホール割当申請書式" + }, + "flame": { + "title": "防炎確認書", + "desc": "防炎処理確認書類" + }, + "structure": { + "title": "構造計算書", + "desc": "独立ブース·リギング構造検討" + } + }, + "ctaBandAria": "ログイン·問い合わせ", + "ctaBandText": "すでにアカウントをお持ちですか?" + }, + "agency": { + "heroAria": "協力会社案内ヒーロー", + "eyebrow": "工事·装置·協力会社ご案内", + "title": "KINTEX公式登録業者として展示施工に参加", + "subtitle": "工事オークション(逆オークション)で透明に受注。KINTEXならではの方式です。", + "ctaLogin": "登録業者ログイン", + "ctaRegister": "登録申請", + "ctaAuctionLogin": "ログインして応札", + "ctaWorkReport": "作業申告する", + "registryAria": "登録業者案内", + "registryTitle": "登録業者のご案内", + "registrySub": "登録業者739社·14分類で運営されます。", + "gateTitle": "未登録業者は展示施工に参加できません(自主施工不可)", + "gateBody": "展示施工は必ずKINTEX公式登録業者を通す必要があります。登録手続きをご確認のうえ申請してください。", + "reg": { + "apply": { + "title": "申請", + "desc": "登録申請書提出" + }, + "review": { + "title": "資格審査", + "desc": "資格·実績審査" + }, + "register": { + "title": "登録", + "desc": "公式登録業者に登載" + }, + "eligible": { + "title": "受注資格", + "desc": "オークション応札·受注可能" + } + }, + "auctionAria": "工事オークション", + "auctionTitle": "進行中の工事オークション(逆オークション)", + "auctionSub": "予算範囲内で透明に競争応札。", + "auctionEmptyTitle": "オークション公告はログイン後にご確認いただけます", + "auctionEmptyBody": "登録業者としてログインすると、進行中の工事オークション公告·予算·締切·応札状況を確認し応札できます。", + "safetyAria": "作業申告·安全規定", + "safetyTitle": "作業申告 · 安全規定", + "safetySub": "現場作業前に必須の手続きと規定をご確認ください。", + "safety": { + "report": { + "title": "作業申告手続き", + "desc": "現場作業前のオンライン作業申告" + }, + "prohibited": { + "title": "禁止作業·騒音", + "desc": "禁止作業·騒音限度·リギング構造計算書" + }, + "docs": { + "title": "必須書類", + "desc": "防炎確認書·保険証書" + } + }, + "ctaBandAria": "登録業者ログイン", + "ctaBandText": "すでに登録業者ですか?" + } } -} \ No newline at end of file +} diff --git a/src/frontend/src/i18n/locales/ko.json b/src/frontend/src/i18n/locales/ko.json index ef66105..c0dd147 100644 --- a/src/frontend/src/i18n/locales/ko.json +++ b/src/frontend/src/i18n/locales/ko.json @@ -1135,6 +1135,33 @@ "citations": "근거 규정", "notFound": "규정에서 찾지 못함", "degraded": "규정 원문 요약(AI 미가용)" + }, + "assistant": { + "label": "AI 도우미", + "inputAria": "관람 도우미에게 질문하기", + "sendAria": "질문 보내기", + "chipsLabel": "자주 묻는 질문", + "chip1": "이번 주말 로봇 전시 있어?", + "chip2": "킨텍스 주차 어디에 해요?", + "chip3": "가족과 볼만한 전시 추천", + "chip4": "사전등록 어떻게 해요?", + "chip5": "GTX-A로 얼마나 걸려요?", + "thinking": "답변 생성 중…", + "error": "답변을 가져오지 못했습니다. 다시 시도해 주세요.", + "retry": "다시 시도", + "degraded": "AI 도우미 점검 중 — 적재 데이터 검색으로 안내합니다", + "providerAi": "AI 답변", + "notFound": "해당 정보를 찾지 못했어요 — 전체 일정·가이드를 확인해 보세요.", + "cardEventsTitle": "관련 전시", + "cardTransportTitle": "오시는 길", + "cardGuideTitle": "방문 안내", + "followupLabel": "이어서 물어보기", + "followup1": "무료인가요?", + "followup2": "주차 어디에 해요?", + "followup3": "사전등록 방법", + "citations": "근거 출처", + "register": "사전등록", + "disclaimer": "AI 생성 답변 — 정확한 정보는 공식 안내를 확인하세요." } }, "analytics": { @@ -2140,5 +2167,364 @@ "cancel": "취소", "saving": "저장 중…", "save": "저장" + }, + "track": { + "shell": { + "wordmark": "KINTEX AI 전시·행사시스템", + "tablistAria": "트랙 선택", + "tab": { + "visitor": "관람", + "business": "비즈니스", + "agency": "협력사" + }, + "login": "로그인", + "myHome": "내 홈" + }, + "gateway": { + "heroAria": "제품 소개 히어로", + "aiWatermark": "AI 생성 예상 이미지", + "eyebrow": "AI 전시·행사 운영 플랫폼", + "title": "KINTEX AI 전시·행사시스템", + "subtitle": "전시 기획부터 시공·관람까지 전 과정을 AI로 자동화합니다.", + "ctaPreReg": "관람 사전등록", + "ctaInquiry": "참가 문의", + "tracksAria": "트랙 분기", + "tracksTitle": "어떤 목적으로 방문하셨나요?", + "tracksSub": "목적에 맞는 트랙을 선택하세요.", + "card": { + "visitor": { + "label": "관람객", + "title": "행사 관람", + "desc": "행사 일정·사전등록·오시는 길", + "cta": "관람 트랙" + }, + "business": { + "label": "주최·참가", + "title": "전시 개최·참가", + "desc": "대관·자동견적·부스 신청·시공 시안", + "cta": "비즈니스 트랙" + }, + "agency": { + "label": "공사·협력사", + "title": "전시 시공 참여", + "desc": "등록업체·공사 옥션·작업신고", + "cta": "협력사 트랙" + } + }, + "eventsAria": "진행 중·예정 전시", + "eventsTitle": "진행 중 · 예정 전시", + "eventsSub": "지금 열리는 전시와 곧 열릴 전시를 확인하세요.", + "eventsMore": "전체 일정 보기" + }, + "visitor": { + "heroAria": "관람 안내 히어로", + "eyebrow": "관람객 안내", + "title": "전시 관람, 여기서 시작하세요", + "subtitle": "사전등록하고 더 빠르게 입장하세요. 관심 분야 알림도 받아보세요.", + "ctaPreReg": "사전등록", + "ctaTicket": "입장권 예매", + "eventsAria": "행사 일정", + "eventsTitle": "행사 일정", + "eventsSub": "진행 중·예정 전시를 확인하고 사전등록하세요.", + "guideAria": "방문 가이드", + "guideTitle": "방문 가이드", + "guideSub": "교통·주차·편의시설·접근성 정보를 한 곳에서.", + "guide": { + "transport": { + "title": "교통", + "desc": "GTX-A 킨텍스역 도보 3분·광역버스", + "link": "오시는 길" + }, + "parking": { + "title": "주차", + "desc": "실시간 주차 안내·요금·게이트", + "link": "주차 안내" + }, + "wayfinding": { + "title": "실내 길찾기", + "desc": "홀·부스까지 실내 경로 안내", + "link": "길찾기 열기" + }, + "amenities": { + "title": "편의시설", + "desc": "식음·수유실·물품보관·ATM", + "link": "편의시설 보기" + }, + "accessibility": { + "title": "접근성·편의", + "desc": "휠체어·수어통역 안내", + "link": "접근성 안내" + }, + "inquiry": { + "title": "고객문의", + "desc": "FAQ·연락처 안내", + "link": "문의하기" + } + }, + "noticeAria": "공지사항", + "noticeTitle": "공지사항", + "noticeSub": "최근 공지·알림을 확인하세요.", + "noticeEmpty": "등록된 공지가 없습니다.", + "aiWatermark": "AI 생성 예상 이미지", + "hero": { + "title": "무엇이든 물어보세요 — AI가 관람을 안내합니다", + "subtitle": "전시 일정·오시는 길·입장권·부스 위치를 대화로 한 번에" + }, + "aiTiles": { + "title": "AI가 도와드립니다", + "sub": "관람의 모든 순간을 AI가 함께합니다.", + "soon": "준비 중", + "assistant": { + "title": "AI 관람 도우미", + "desc": "무엇이든 물어보세요" + }, + "recommend": { + "title": "AI 전시 추천", + "desc": "관심사 맞춤 큐레이션" + }, + "wayfinding": { + "title": "AI 길찾기", + "desc": "실내 최적 동선" + }, + "schedule": { + "title": "AI 일정 도우미", + "desc": "나만의 관람 코스" + }, + "crowd": { + "title": "AI 혼잡·대기 안내", + "desc": "여유로운 시간 추천" + }, + "render": { + "title": "AI 시공 예상 이미지", + "desc": "부스 미리보기 (비즈니스 트랙)" + } + }, + "showcaseTitle": "진행 중 · 예정 전시", + "showcaseSub": "지금 열리는 전시와 곧 열릴 전시를 한눈에.", + "recommendTitle": "당신의 관심사에 맞는 전시", + "recommendSub": "관심 분야를 선택하면 AI가 맞춤 전시를 추천합니다.", + "recommendTag": "AI 추천", + "interests": { + "robot": "로봇·자동화", + "smartfactory": "스마트팩토리", + "healthcare": "헬스케어", + "eco": "친환경·에너지", + "mobility": "모빌리티", + "content": "콘텐츠·문화" + }, + "guideMore": "자세히 보기", + "journeyTitle": "관람은 이렇게 진행됩니다", + "journeySub": "사전등록부터 현장 입장까지 3단계로 간편하게.", + "journey": { + "register": { + "title": "사전등록", + "desc": "온라인으로 관람 정보를 등록하세요.", + "link": "전시 보기" + }, + "badge": { + "title": "배지·QR 발급", + "desc": "모바일 배지와 입장 QR을 받으세요.", + "link": "입장권 조회" + }, + "checkin": { + "title": "현장 체크인", + "desc": "입구에서 QR을 스캔하고 바로 입장하세요.", + "link": "로그인" + } + }, + "venueTitle": "킨텍스 전시장 소개", + "venueSub": "국내 최대 규모의 전시·컨벤션 센터.", + "venue": { + "halls": { + "label": "전시홀", + "value": "10개" + }, + "area": { + "label": "전시면적", + "value": "108,556㎡" + }, + "parking": { + "label": "주차", + "value": "7,500대" + }, + "access": { + "label": "교통", + "value": "GTX-A 도보 3분" + } + }, + "briefing": { + "title": "AI 전시·행사 기획 브리핑", + "sub": "올 한 해와 이번 달, AI가 개최 예정 전시를 먼저 정리해 드립니다.", + "viewYear": "연간 ({{year}})", + "viewMonth": "월간", + "aiSummary": "AI 요약", + "loading": "AI 요약 생성 중…", + "empty": "이 기간에 예정된 전시가 없습니다.", + "yearSummary": "{{year}}년에는 총 {{count}}건의 전시·행사가 예정되어 있으며, {{peak}}에 집중됩니다.", + "monthSummary": "이번 달은 {{count}}건의 전시가 열립니다 · 주말 방문을 추천합니다.", + "monthLabel": "{{month}}월", + "eventsCount": "{{count}}건", + "prevMonth": "이전 달", + "nextMonth": "다음 달" + } + }, + "business": { + "heroAria": "비즈니스 안내 히어로", + "eyebrow": "주최자·참가업체 안내", + "title": "전시 개최·참가, 온라인 신청 일원화", + "subtitle": "대관부터 부스 신청·시공까지 모든 절차를 온라인에서 처리하세요.", + "ctaApply": "신청·문의", + "ctaLogin": "로그인", + "ctaInquiry": "문의하기", + "valueAria": "가치 제안", + "valueTitle": "역할에 맞는 진입", + "valueSub": "주최자와 참가업체를 위한 기능을 안내합니다.", + "value": { + "organizer": { + "title": "주최자 · 홀 임대", + "desc": "대관 절차부터 홀 배정·플로어플랜까지 한 번에.", + "item": { + "rental": "대관 절차 안내", + "quote": "자동견적", + "floorplan": "홀 배정·플로어플랜", + "rate": "요금·시설 규격" + }, + "cta": "대관 문의·로그인" + }, + "exhibitor": { + "title": "참가업체 · 부스 신청", + "desc": "부스 신청·유틸리티부터 AI 시공 시안까지.", + "item": { + "apply": "부스 신청", + "utility": "유틸리티 신청", + "compliance": "규정 사전검증", + "render": "AI 시공 시안" + }, + "cta": "참가 신청·로그인" + } + }, + "processAria": "임대 절차", + "processTitle": "임대 절차", + "processSub": "문의부터 시공·검수까지 한눈에.", + "process": { + "inquiry": { + "title": "문의", + "desc": "대관 상담 접수" + }, + "quote": { + "title": "가견적", + "desc": "자동견적 산출" + }, + "contract": { + "title": "계약", + "desc": "계약 체결" + }, + "assign": { + "title": "홀 배정", + "desc": "홀·구획 배정" + }, + "booth": { + "title": "부스 신청", + "desc": "참가업체 부스·유틸리티" + }, + "inspect": { + "title": "시공·검수", + "desc": "시공 후 현장 검수" + } + }, + "rateAria": "요금·서류", + "rateTitle": "요금 · 서류 안내", + "rateSub": "부스 유형별 요율과 주요 서류를 안내합니다.", + "rate": { + "shell": { + "label": "조립부스", + "price": "기본형", + "desc": "기본 부스·기본 전기 포함" + }, + "space": { + "label": "독립부스", + "price": "공간형", + "desc": "자유 시공·구조계산서 필요" + }, + "premium": { + "label": "프리미엄", + "price": "특화형", + "desc": "프리미엄 위치·추가 유틸리티" + } + }, + "doc": { + "assign": { + "title": "배정신청서", + "desc": "홀 배정 신청 서식" + }, + "flame": { + "title": "방염 확인서", + "desc": "방염 처리 확인 서류" + }, + "structure": { + "title": "구조계산서", + "desc": "독립부스·리깅 구조 검토" + } + }, + "ctaBandAria": "로그인·문의", + "ctaBandText": "이미 계정이 있으신가요?" + }, + "agency": { + "heroAria": "협력사 안내 히어로", + "eyebrow": "공사·장치·협력업체 안내", + "title": "킨텍스 공식 등록업체로 전시 시공에 참여하세요", + "subtitle": "공사 옥션(역경매)으로 투명하게 수주하세요. 킨텍스만의 차별화된 방식입니다.", + "ctaLogin": "등록업체 로그인", + "ctaRegister": "등록 신청", + "ctaAuctionLogin": "로그인하고 옥션 응찰", + "ctaWorkReport": "작업신고 하기", + "registryAria": "등록업체 안내", + "registryTitle": "등록업체 안내", + "registrySub": "등록업체 739개사 · 14개 분류로 운영됩니다.", + "gateTitle": "미등록 업체는 전시 시공에 참여할 수 없습니다 (자체시공 불가)", + "gateBody": "전시 시공은 반드시 킨텍스 공식 등록업체를 통해야 합니다. 등록 절차를 확인하고 신청하세요.", + "reg": { + "apply": { + "title": "신청", + "desc": "등록 신청서 제출" + }, + "review": { + "title": "자격심사", + "desc": "자격·실적 심사" + }, + "register": { + "title": "등록", + "desc": "공식 등록업체 등재" + }, + "eligible": { + "title": "수주 자격", + "desc": "옥션 응찰·수주 가능" + } + }, + "auctionAria": "공사 옥션", + "auctionTitle": "진행 중 공사 옥션 (역경매)", + "auctionSub": "예산 범위 안에서 투명하게 경쟁 응찰하세요.", + "auctionEmptyTitle": "공개 옥션 공고는 로그인 후 확인하실 수 있습니다", + "auctionEmptyBody": "등록업체 로그인 시 진행 중인 공사 옥션 공고·예산·마감·응찰 현황을 확인하고 응찰할 수 있습니다.", + "safetyAria": "작업신고·안전 규정", + "safetyTitle": "작업신고 · 안전 규정", + "safetySub": "현장 작업 전 필수 절차와 규정을 확인하세요.", + "safety": { + "report": { + "title": "작업신고 절차", + "desc": "현장 작업 전 온라인 작업신고" + }, + "prohibited": { + "title": "금지작업·소음", + "desc": "금지작업·소음 한도·리깅 구조계산서" + }, + "docs": { + "title": "필수 서류", + "desc": "방염 확인서·보험 증서" + } + }, + "ctaBandAria": "등록업체 로그인", + "ctaBandText": "이미 등록된 업체이신가요?" + } } -} \ No newline at end of file +} diff --git a/src/frontend/src/i18n/locales/zh.json b/src/frontend/src/i18n/locales/zh.json index 3aabea7..be19c52 100644 --- a/src/frontend/src/i18n/locales/zh.json +++ b/src/frontend/src/i18n/locales/zh.json @@ -1135,6 +1135,33 @@ "citations": "依据规定", "notFound": "在规定中未找到", "degraded": "规定原文摘要(AI 不可用)" + }, + "assistant": { + "label": "AI 助手", + "inputAria": "向参观助手提问", + "sendAria": "发送问题", + "chipsLabel": "常见问题", + "chip1": "这周末有机器人展吗?", + "chip2": "KINTEX 停车在哪里?", + "chip3": "推荐适合家庭参观的展览", + "chip4": "如何预先登记?", + "chip5": "乘 GTX-A 需要多久?", + "thinking": "正在生成回答…", + "error": "无法获取回答,请重试。", + "retry": "重试", + "degraded": "AI 助手维护中 — 以已存数据检索结果作答", + "providerAi": "AI 回答", + "notFound": "未找到相关信息 — 请查看完整日程与指南。", + "cardEventsTitle": "相关展览", + "cardTransportTitle": "交通路线", + "cardGuideTitle": "参观须知", + "followupLabel": "继续提问", + "followup1": "是免费的吗?", + "followup2": "在哪里停车?", + "followup3": "如何预先登记", + "citations": "来源", + "register": "预先登记", + "disclaimer": "AI 生成的回答 — 准确信息请以官方通知为准。" } }, "analytics": { @@ -2140,5 +2167,364 @@ "cancel": "取消", "saving": "保存中…", "save": "保存" + }, + "track": { + "shell": { + "wordmark": "KINTEX AI 展览·活动系统", + "tablistAria": "选择通道", + "tab": { + "visitor": "参观", + "business": "商务", + "agency": "合作商" + }, + "login": "登录", + "myHome": "我的主页" + }, + "gateway": { + "heroAria": "产品介绍", + "aiWatermark": "AI 生成预览图", + "eyebrow": "AI 展览·活动运营平台", + "title": "KINTEX AI 展览·活动系统", + "subtitle": "从展览策划到施工·参观,全流程 AI 自动化。", + "ctaPreReg": "参观预登记", + "ctaInquiry": "参展咨询", + "tracksAria": "通道分流", + "tracksTitle": "您此次访问的目的是?", + "tracksSub": "请选择符合您目的的通道。", + "card": { + "visitor": { + "label": "参观者", + "title": "参观活动", + "desc": "活动日程·预登记·交通指引", + "cta": "参观通道" + }, + "business": { + "label": "主办·参展", + "title": "举办·参展", + "desc": "租馆·自动报价·展位申请·施工方案", + "cta": "商务通道" + }, + "agency": { + "label": "施工·合作商", + "title": "参与展览施工", + "desc": "注册企业·施工竞标·作业申报", + "cta": "合作商通道" + } + }, + "eventsAria": "进行中·预定展览", + "eventsTitle": "进行中 · 预定展览", + "eventsSub": "查看正在举办及即将举办的展览。", + "eventsMore": "查看全部日程" + }, + "visitor": { + "heroAria": "参观介绍", + "eyebrow": "参观者指南", + "title": "参观从这里开始", + "subtitle": "预登记可更快入场,还可接收关注领域的通知。", + "ctaPreReg": "预登记", + "ctaTicket": "购票", + "eventsAria": "活动日程", + "eventsTitle": "活动日程", + "eventsSub": "查看进行中·预定展览并预登记。", + "guideAria": "参观指南", + "guideTitle": "参观指南", + "guideSub": "交通·停车·便利设施·无障碍信息一站式了解。", + "guide": { + "transport": { + "title": "交通", + "desc": "GTX-A KINTEX站步行3分钟·城际巴士", + "link": "交通指引" + }, + "parking": { + "title": "停车", + "desc": "实时车位·费用·出入口", + "link": "停车指南" + }, + "wayfinding": { + "title": "室内导航", + "desc": "至展厅·展位的室内路线", + "link": "打开导航" + }, + "amenities": { + "title": "便利设施", + "desc": "餐饮·母婴室·寄存·ATM", + "link": "查看设施" + }, + "accessibility": { + "title": "无障碍", + "desc": "轮椅·手语翻译指引", + "link": "无障碍指南" + }, + "inquiry": { + "title": "客户咨询", + "desc": "FAQ·联系方式", + "link": "联系我们" + } + }, + "noticeAria": "公告", + "noticeTitle": "公告", + "noticeSub": "查看最新公告·通知。", + "noticeEmpty": "暂无公告。", + "aiWatermark": "AI 生成的预览图", + "hero": { + "title": "尽管提问 — AI 为您导览", + "subtitle": "展会日程、交通、门票与展位,一次对话全搞定" + }, + "aiTiles": { + "title": "AI 为您服务", + "sub": "AI 陪伴您参观的每一刻。", + "soon": "敬请期待", + "assistant": { + "title": "AI 参观助手", + "desc": "尽管提问" + }, + "recommend": { + "title": "AI 展会推荐", + "desc": "按兴趣精选" + }, + "wayfinding": { + "title": "AI 室内导航", + "desc": "室内最优路线" + }, + "schedule": { + "title": "AI 行程助手", + "desc": "专属参观路线" + }, + "crowd": { + "title": "AI 拥挤·等候提示", + "desc": "推荐空闲时段" + }, + "render": { + "title": "AI 施工预览图", + "desc": "展位预览(商务通道)" + } + }, + "showcaseTitle": "进行中·即将举办", + "showcaseSub": "一览当前与即将举办的展会。", + "recommendTitle": "为您兴趣定制的展会", + "recommendSub": "选择兴趣领域,AI 为您推荐。", + "recommendTag": "AI 推荐", + "interests": { + "robot": "机器人·自动化", + "smartfactory": "智能工厂", + "healthcare": "医疗健康", + "eco": "环保·能源", + "mobility": "出行", + "content": "内容·文化" + }, + "guideMore": "查看详情", + "journeyTitle": "参观流程", + "journeySub": "从预先登记到现场入场,三步搞定。", + "journey": { + "register": { + "title": "预先登记", + "desc": "在线登记参观信息。", + "link": "浏览展会" + }, + "badge": { + "title": "领取胸卡·二维码", + "desc": "获取手机胸卡与入场二维码。", + "link": "查询门票" + }, + "checkin": { + "title": "现场签到", + "desc": "在入口扫码即可入场。", + "link": "登录" + } + }, + "venueTitle": "KINTEX 展馆介绍", + "venueSub": "韩国最大的展览会议中心。", + "venue": { + "halls": { + "label": "展厅", + "value": "10 个" + }, + "area": { + "label": "展览面积", + "value": "108,556㎡" + }, + "parking": { + "label": "停车", + "value": "7,500 辆" + }, + "access": { + "label": "交通", + "value": "GTX-A 步行3分" + } + }, + "briefing": { + "title": "AI 展会企划简报", + "sub": "AI 提前为您梳理全年与本月的展会。", + "viewYear": "全年({{year}})", + "viewMonth": "月度", + "aiSummary": "AI 摘要", + "loading": "正在生成 AI 摘要…", + "empty": "该期间暂无展会安排。", + "yearSummary": "{{year}}年共安排 {{count}} 场展会,集中在{{peak}}。", + "monthSummary": "本月共 {{count}} 场展会 · 建议周末参观。", + "monthLabel": "{{month}}月", + "eventsCount": "{{count}} 场", + "prevMonth": "上个月", + "nextMonth": "下个月" + } + }, + "business": { + "heroAria": "商务介绍", + "eyebrow": "主办方·参展商指南", + "title": "举办·参展,在线申请一站式", + "subtitle": "从租馆到展位申请·施工,全流程在线办理。", + "ctaApply": "申请·咨询", + "ctaLogin": "登录", + "ctaInquiry": "咨询", + "valueAria": "价值主张", + "valueTitle": "按角色进入", + "valueSub": "为主办方与参展商提供的功能。", + "value": { + "organizer": { + "title": "主办方 · 租馆", + "desc": "从租馆流程到展厅分配·平面图,一站搞定。", + "item": { + "rental": "租馆流程", + "quote": "自动报价", + "floorplan": "展厅分配·平面图", + "rate": "费用·设施规格" + }, + "cta": "租馆咨询·登录" + }, + "exhibitor": { + "title": "参展商 · 展位申请", + "desc": "从展位·水电申请到 AI 施工方案。", + "item": { + "apply": "展位申请", + "utility": "水电等申请", + "compliance": "规定预检", + "render": "AI 施工方案" + }, + "cta": "参展申请·登录" + } + }, + "processAria": "租赁流程", + "processTitle": "租赁流程", + "processSub": "从咨询到施工·验收一目了然。", + "process": { + "inquiry": { + "title": "咨询", + "desc": "受理租馆咨询" + }, + "quote": { + "title": "初步报价", + "desc": "自动报价" + }, + "contract": { + "title": "签约", + "desc": "签订合同" + }, + "assign": { + "title": "展厅分配", + "desc": "展厅·分区分配" + }, + "booth": { + "title": "展位申请", + "desc": "参展商展位·水电" + }, + "inspect": { + "title": "施工·验收", + "desc": "施工后现场验收" + } + }, + "rateAria": "费用·文件", + "rateTitle": "费用 · 文件指南", + "rateSub": "各展位类型的费率及主要文件。", + "rate": { + "shell": { + "label": "标准展位", + "price": "基本型", + "desc": "含基本展位·基础用电" + }, + "space": { + "label": "光地展位", + "price": "空间型", + "desc": "自由施工·需结构计算书" + }, + "premium": { + "label": "高级", + "price": "特化型", + "desc": "优质位置·附加水电" + } + }, + "doc": { + "assign": { + "title": "分配申请书", + "desc": "展厅分配申请表" + }, + "flame": { + "title": "阻燃确认书", + "desc": "阻燃处理确认文件" + }, + "structure": { + "title": "结构计算书", + "desc": "光地展位·吊挂结构审查" + } + }, + "ctaBandAria": "登录·咨询", + "ctaBandText": "已经拥有账号?" + }, + "agency": { + "heroAria": "合作商介绍", + "eyebrow": "施工·装置·合作商指南", + "title": "成为 KINTEX 官方注册企业,参与展览施工", + "subtitle": "通过施工竞标(反向竞价)透明中标 — KINTEX 独有方式。", + "ctaLogin": "注册企业登录", + "ctaRegister": "注册申请", + "ctaAuctionLogin": "登录参与竞标", + "ctaWorkReport": "提交作业申报", + "registryAria": "注册企业指南", + "registryTitle": "注册企业指南", + "registrySub": "由 739 家注册企业·14 个分类运营。", + "gateTitle": "未注册企业不得参与展览施工(不可自行施工)", + "gateBody": "展览施工必须通过 KINTEX 官方注册企业。请查看注册流程并申请。", + "reg": { + "apply": { + "title": "申请", + "desc": "提交注册申请" + }, + "review": { + "title": "资格审查", + "desc": "资格·业绩审查" + }, + "register": { + "title": "注册", + "desc": "登记为官方注册企业" + }, + "eligible": { + "title": "中标资格", + "desc": "可参与竞标·中标" + } + }, + "auctionAria": "施工竞标", + "auctionTitle": "进行中的施工竞标(反向竞价)", + "auctionSub": "在预算范围内透明竞标。", + "auctionEmptyTitle": "竞标公告需登录后查看", + "auctionEmptyBody": "以注册企业登录后,可查看进行中的施工竞标公告·预算·截止·应标情况并参与竞标。", + "safetyAria": "作业申报·安全规定", + "safetyTitle": "作业申报 · 安全规定", + "safetySub": "现场作业前请确认必要流程与规定。", + "safety": { + "report": { + "title": "作业申报流程", + "desc": "现场作业前在线申报" + }, + "prohibited": { + "title": "禁止作业·噪音", + "desc": "禁止作业·噪音限值·吊挂结构计算书" + }, + "docs": { + "title": "必需文件", + "desc": "阻燃确认书·保险单" + } + }, + "ctaBandAria": "注册企业登录", + "ctaBandText": "已是注册企业?" + } } -} \ No newline at end of file +} diff --git a/src/frontend/src/screens/public/AiAssistant.tsx b/src/frontend/src/screens/public/AiAssistant.tsx new file mode 100644 index 0000000..41ebd3d --- /dev/null +++ b/src/frontend/src/screens/public/AiAssistant.tsx @@ -0,0 +1,303 @@ +/* + * 공용 AI 관람 도우미 (SCR-T1 히어로 중심 요소) — 재사용 컴포넌트. + * PLANNING §8A 계약 정합: 자연어 입력(role=search) + 예시 질문칩 + 대화형 답변 패널 + * (답변·구조화 카드(전시/교통/가이드)·근거 인용·후속칩·AI 고지문·degraded 안내). + * 데이터: publicApi.askVisitorAssistant → POST /api/public/ai/visitor-assistant(크롤 적재 DB 근거). + * 상태: 대기(placeholder 로테이션)·생각중·성공·degraded(provider=search|none)·에러·빈결과(grounded=false). + * 이모지 금지·선 SVG·kx 토큰만·AiLabel(보라) 구분·aria-live=polite. + */ +import { useCallback, useEffect, useRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { AiLabel } from '../../components/ui/Badge'; +import { publicApi, type AssistantResult } from './publicApi'; +import { formatRange, dday, errorMessage } from './publicFormat'; +import { ddayClass } from './trackShared'; +import { + IconSend, + IconCalendar, + IconPin, + IconTrain, + IconInfo, + IconLink, + IconArrowRight, +} from './publicIcons'; + +/** 예시/후속 질문칩 i18n 키. */ +const EXAMPLE_KEYS = ['chip1', 'chip2', 'chip3', 'chip4', 'chip5'] as const; +const FOLLOWUP_KEYS = ['followup1', 'followup2', 'followup3'] as const; + +type Phase = 'idle' | 'thinking' | 'done' | 'error'; + +export function AiAssistant() { + const { t } = useTranslation(); + const [question, setQuestion] = useState(''); + const [phase, setPhase] = useState('idle'); + const [result, setResult] = useState(null); + const [error, setError] = useState(''); + const [phIdx, setPhIdx] = useState(0); + const inputRef = useRef(null); + const reqSeq = useRef(0); + + // 대기 중 placeholder 로테이션(입력 비어 있을 때만). + useEffect(() => { + if (phase !== 'idle' || question) return; + const id = window.setInterval(() => setPhIdx((i) => (i + 1) % EXAMPLE_KEYS.length), 3800); + return () => window.clearInterval(id); + }, [phase, question]); + + const ask = useCallback( + (q: string) => { + const query = q.trim(); + if (!query) return; + const seq = ++reqSeq.current; + setQuestion(query); + setPhase('thinking'); + setError(''); + publicApi + .askVisitorAssistant(query) + .then((res) => { + if (seq !== reqSeq.current) return; + setResult(res); + setPhase('done'); + }) + .catch((e) => { + if (seq !== reqSeq.current) return; + setError(errorMessage(e)); + setPhase('error'); + }); + }, + [], + ); + + const onSubmit = (e: React.FormEvent) => { + e.preventDefault(); + ask(question); + }; + + const placeholder = t(`ai.assistant.${EXAMPLE_KEYS[phIdx]}`); + + return ( +

+
+ + {t('ai.assistant.label')} + + setQuestion(e.target.value)} + placeholder={placeholder} + aria-label={t('ai.assistant.inputAria')} + enterKeyHint="search" + /> + +
+ + {/* 예시 질문칩 */} +
+ {EXAMPLE_KEYS.map((k) => ( + + ))} +
+ + {/* 대화형 답변 패널 */} +
+ {phase === 'thinking' && ( +
+ + + + + + {t('ai.assistant.thinking')} +
+ )} + + {phase === 'error' && ( +
+

{error || t('ai.assistant.error')}

+ +
+ )} + + {phase === 'done' && result && } +
+
+ ); +} + +function AnswerCard({ + result, + onFollowup, +}: { + result: AssistantResult; + onFollowup: (q: string) => void; +}) { + const { t } = useTranslation(); + const { answer, events, transport, guide, citations, grounded, degraded } = result; + const hasCards = events.length > 0 || transport.length > 0 || guide.length > 0; + + return ( +
+
+ {t('ai.assistant.providerAi')} + {degraded && {t('ai.assistant.degraded')}} +
+ + {answer ? ( +

{answer}

+ ) : ( +

{t('ai.assistant.notFound')}

+ )} + + {!grounded && !answer &&

{t('ai.assistant.notFound')}

} + + {hasCards && ( +
+ {events.length > 0 && ( +
+

{t('ai.assistant.cardEventsTitle')}

+
+ {events.slice(0, 4).map((ev) => { + const d = dday(ev.startDate); + const ended = ev.lifecycle === 'ENDED'; + return ( +
+
+ {ev.posterUrl ? ( + + ) : ( + {ev.name.slice(0, 2)} + )} +
+
+
{ev.name}
+
+ + {formatRange(ev.startDate, ev.endDate)} + + {ev.hallLabel && ( + + {ev.hallLabel} + + )} +
+
+ {!ended && d >= 0 && ( + + {d === 0 ? t('home.ddayZero') : t('home.dday', { n: d })} + + )} +
+ ); + })} +
+
+ )} + + {transport.length > 0 && ( +
+

{t('ai.assistant.cardTransportTitle')}

+
+ {transport.slice(0, 3).map((tr) => ( +
+ + + +
+
{tr.lineName || tr.mode}
+
+ {[tr.route, tr.travelTime].filter(Boolean).join(' · ')} +
+
+
+ ))} +
+
+ )} + + {guide.length > 0 && ( +
+

{t('ai.assistant.cardGuideTitle')}

+
+ {guide.slice(0, 3).map((g) => ( +
+ + + +
+
{g.title}
+ {g.summary &&
{g.summary}
} +
+
+ ))} +
+
+ )} +
+ )} + + {/* 후속 질문칩 */} +
+ {FOLLOWUP_KEYS.map((k) => ( + + ))} + + {t('ai.assistant.register')} + +
+ + {/* 근거 출처 인용 */} + {citations.length > 0 && ( +
+ {t('ai.assistant.citations')} + {citations.slice(0, 6).map((c, i) => + c.sourceUrl ? ( + + {c.title} + + ) : ( + + {c.title} + + ), + )} +
+ )} + + {/* AI 고지문 — 제거 불가·상시 노출 */} +

{t('ai.assistant.disclaimer')}

+
+ ); +} diff --git a/src/frontend/src/screens/public/AiPlanningBriefing.tsx b/src/frontend/src/screens/public/AiPlanningBriefing.tsx new file mode 100644 index 0000000..446dd60 --- /dev/null +++ b/src/frontend/src/screens/public/AiPlanningBriefing.tsx @@ -0,0 +1,253 @@ +/* + * AI 전시·행사 기획 브리핑 (SCR-T1 레이아웃 4) — 연/월 관점 토글. + * 연간 = 12개월 타임라인(월별 건수 바) + "AI 요약" 헤드라인, 월간 = 선택 월 전시 리스트 + AI 월간 요약. + * 데이터: publicApi.listMonthlySummary(연간 근거) + listCalendar(월간 근거) — 크롤 적재 DB 서빙. + * AI 요약 문구는 assistant 미가용 시 데이터 기반 정적 폴백(degraded). 이모지 금지·선 SVG·kx 토큰. + */ +import { useEffect, useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { AiLabel } from '../../components/ui/Badge'; +import { publicApi, type MonthlySummary, type PublicCalendarEvent } from './publicApi'; +import { formatRange, dday, errorMessage } from './publicFormat'; +import { ddayClass } from './trackShared'; +import { IconChevronRight, IconCalendar, IconPin } from './publicIcons'; + +type View = 'year' | 'month'; +const NOW = new Date(); + +export function AiPlanningBriefing() { + const { t } = useTranslation(); + const [view, setView] = useState('year'); + const [year] = useState(NOW.getFullYear()); + const [month, setMonth] = useState(NOW.getMonth() + 1); + + const [summary, setSummary] = useState([]); + const [sumLoading, setSumLoading] = useState(true); + const [sumErr, setSumErr] = useState(''); + + const [monthEvents, setMonthEvents] = useState([]); + const [mLoading, setMLoading] = useState(false); + const [mErr, setMErr] = useState(''); + + // 연간 월별 요약 로드(1회). + useEffect(() => { + let alive = true; + setSumLoading(true); + publicApi + .listMonthlySummary(year) + .then((list) => alive && (setSummary(list), setSumErr(''))) + .catch((e) => alive && setSumErr(errorMessage(e))) + .finally(() => alive && setSumLoading(false)); + return () => { + alive = false; + }; + }, [year]); + + // 월간 전시 로드(월 변경 시). + useEffect(() => { + if (view !== 'month') return; + let alive = true; + setMLoading(true); + publicApi + .listCalendar({ year, month, limit: 40 }) + .then((list) => alive && (setMonthEvents(list), setMErr(''))) + .catch((e) => alive && setMErr(errorMessage(e))) + .finally(() => alive && setMLoading(false)); + return () => { + alive = false; + }; + }, [view, year, month]); + + const byMonth = useMemo(() => { + const m = new Map(); + summary.forEach((s) => { + if (s.eventMonth != null) m.set(s.eventMonth, (m.get(s.eventMonth) ?? 0) + (s.eventCount ?? 0)); + }); + return m; + }, [summary]); + + const maxCount = useMemo(() => Math.max(1, ...Array.from(byMonth.values())), [byMonth]); + const totalCount = useMemo( + () => Array.from(byMonth.values()).reduce((a, b) => a + b, 0), + [byMonth], + ); + const peakMonth = useMemo(() => { + let best = 0; + let bestC = -1; + byMonth.forEach((c, mo) => { + if (c > bestC) { + bestC = c; + best = mo; + } + }); + return best; + }, [byMonth]); + const monthCount = byMonth.get(month) ?? monthEvents.length; + + return ( +
+
+
+
+

{t('track.visitor.briefing.title')}

+

{t('track.visitor.briefing.sub')}

+
+
+ + +
+
+ + {view === 'year' ? ( +
+ {/* AI 요약 헤드라인 */} +
+ {t('track.visitor.briefing.aiSummary')} +

+ {sumLoading + ? t('track.visitor.briefing.loading') + : sumErr || totalCount === 0 + ? t('track.visitor.briefing.empty') + : t('track.visitor.briefing.yearSummary', { + year, + count: totalCount, + peak: t('track.visitor.briefing.monthLabel', { month: peakMonth || month }), + })} +

+
+ + {/* 12개월 타임라인 */} + {sumLoading ? ( +
+ {Array.from({ length: 12 }).map((_, i) => ( +
+ + {i + 1} +
+ ))} +
+ ) : ( +
+ {Array.from({ length: 12 }).map((_, i) => { + const mo = i + 1; + const c = byMonth.get(mo) ?? 0; + const h = Math.max(6, Math.round((c / maxCount) * 100)); + const isPeak = c > 0 && c === maxCount; + return ( + + ); + })} +
+ )} +
+ ) : ( +
+
+ + + {year}. {String(month).padStart(2, '0')} + + +
+ +
+ {t('track.visitor.briefing.aiSummary')} +

+ {mLoading + ? t('track.visitor.briefing.loading') + : monthCount === 0 + ? t('track.visitor.briefing.empty') + : t('track.visitor.briefing.monthSummary', { count: monthCount })} +

+
+ + {mLoading ? ( +

+ {t('home.loading')} +

+ ) : mErr ? ( +

+ {mErr} +

+ ) : monthEvents.length === 0 ? ( +

{t('track.visitor.briefing.empty')}

+ ) : ( +
+ {monthEvents.map((ev) => { + const d = dday(ev.startDate); + const ended = ev.lifecycle === 'ENDED'; + return ( +
+
+
{ev.name}
+
+ + {formatRange(ev.startDate, ev.endDate)} + + {ev.hallLabel && ( + + {ev.hallLabel} + + )} + {ev.category && {ev.category}} +
+
+ {!ended && d >= 0 && ( + + {d === 0 ? t('home.ddayZero') : t('home.dday', { n: d })} + + )} +
+ ); + })} +
+ )} +
+ )} +
+
+ ); +} diff --git a/src/frontend/src/screens/public/VisitorTrackPage.tsx b/src/frontend/src/screens/public/VisitorTrackPage.tsx index daf4507..9e7dbd8 100644 --- a/src/frontend/src/screens/public/VisitorTrackPage.tsx +++ b/src/frontend/src/screens/public/VisitorTrackPage.tsx @@ -1,15 +1,23 @@ /* - * SCR-T1 visitor 서브홈 (§3C) — `/visitor`, 미인증 공개 · track="visitor". - * 관람 히어로 + 행사 일정 그리드 + 방문 가이드 허브(2×3) + 공지·알림마당. - * 데이터: /api/public/events(그리드) · /api/public/cms/NOTICE(공지) · 가이드=정적. + * SCR-T1 visitor 메인 (§3C v2.3.2) — `/visitor`, 미인증 공개 · track="visitor". + * "세련된 AI 관람 도우미 홈페이지"로 격상: 히어로 AI 도우미(중심) → AI 기능 쇼케이스 → + * AI 연/월 기획 브리핑 → 전시 쇼케이스 → 관심사 추천 → 방문가이드 허브 → 여정 3스텝 → + * 공지 → 전시장 소개 → 푸터(PublicShell). + * 데이터(라이브): AI 도우미(POST /ai/visitor-assistant)·기획(calendar+monthly-summary)· + * 방문가이드/교통(visitor-guide+transport)·쇼케이스(/events)·공지(/cms/NOTICE). + * ※ Stitch 생성 2회 실패 → design.md 스펙+기존 컨벤션 재조합 직접 구현(신규 토큰 0). + * Stitch 확보 시 히어로/타일/브리핑 마크업 교체 지점 = 각 섹션 주석 "STITCH-REPLACE". */ import { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Link } from 'react-router-dom'; import { PublicShell } from './PublicShell'; import { usePublicEvents, EventGrid } from './trackShared'; -import { publicApi, type PublicNotice } from './publicApi'; +import { AiAssistant } from './AiAssistant'; +import { AiPlanningBriefing } from './AiPlanningBriefing'; +import { publicApi, type PublicNotice, type VisitorGuide, type TransportInfo } from './publicApi'; import { errorMessage } from './publicFormat'; +import { AiLabel } from '../../components/ui/Badge'; import { IconTrain, IconParking, @@ -19,13 +27,31 @@ import { IconChat, IconChevronRight, IconArrowRight, - IconBell, + IconSparkles, + IconTarget, + IconCalendar, + IconRobot, + IconCloud, + IconQr, + IconCheckCircle, + IconTicket, + IconVenue, } from './publicIcons'; import type { ComponentType, SVGProps } from 'react'; type Ico = ComponentType>; -/** 방문 가이드 허브 카드(2×3). link 가 내부 경로면 Link, 아니면 앵커. */ +/** AI 기능 쇼케이스 타일(4~6) — 정적 능력 메타. `soon`=준비 중 배지. */ +const AI_TILES: { key: string; icon: Ico; to: string; soon?: boolean }[] = [ + { key: 'assistant', icon: IconSparkles, to: '#hero' }, + { key: 'recommend', icon: IconTarget, to: '#recommend' }, + { key: 'wayfinding', icon: IconMap, to: '/login' }, + { key: 'schedule', icon: IconCalendar, to: '#briefing' }, + { key: 'crowd', icon: IconCloud, to: '#briefing', soon: true }, + { key: 'render', icon: IconRobot, to: '/business', soon: true }, +]; + +/** 방문 가이드 허브(2×3) 정적 폴백 — 라이브 visitor-guide 부재 시. */ const GUIDE_CARDS: { key: string; icon: Ico; to: string }[] = [ { key: 'transport', icon: IconTrain, to: '#transport' }, { key: 'parking', icon: IconParking, to: '#transport' }, @@ -35,28 +61,55 @@ const GUIDE_CARDS: { key: string; icon: Ico; to: string }[] = [ { key: 'inquiry', icon: IconChat, to: '/public/exhibit-inquiry' }, ]; -function GuideLink({ to, children }: { to: string; children: React.ReactNode }) { +/** 관심사 추천 칩(정적 카테고리 라벨). */ +const INTEREST_KEYS = ['robot', 'smartfactory', 'healthcare', 'eco', 'mobility', 'content'] as const; + +/** 관람객 여정 3-스텝. */ +const JOURNEY: { key: string; icon: Ico; to: string }[] = [ + { key: 'register', icon: IconTicket, to: '#showcase' }, + { key: 'badge', icon: IconQr, to: '/tickets/lookup' }, + { key: 'checkin', icon: IconCheckCircle, to: '/login' }, +]; + +function LinkOrAnchor({ to, className, children }: { to: string; className: string; children: React.ReactNode }) { if (to.startsWith('#')) { return ( - + {children} ); } return ( - + {children} ); } +/** 방문가이드 카테고리 → 아이콘 휴리스틱(라이브 카테고리 텍스트 매칭). */ +function guideIcon(cat: string | null, title: string | null): Ico { + const s = `${cat ?? ''} ${title ?? ''}`.toLowerCase(); + if (/교통|transport|gtx|지하철|버스|subway|bus/.test(s)) return IconTrain; + if (/주차|park/.test(s)) return IconParking; + if (/길찾기|wayfind|지도|map|동선/.test(s)) return IconMap; + if (/접근|access|휠체어|수어/.test(s)) return IconShield; + if (/문의|faq|inquir|연락/.test(s)) return IconChat; + return IconInfo; +} + export function VisitorTrackPage() { const { t } = useTranslation(); - const events = usePublicEvents(8); + const events = usePublicEvents(12); + const [notices, setNotices] = useState([]); const [noticeErr, setNoticeErr] = useState(''); const [noticeLoading, setNoticeLoading] = useState(true); + const [guides, setGuides] = useState([]); + const [transport, setTransport] = useState([]); + + const [interest, setInterest] = useState(''); + useEffect(() => { let alive = true; setNoticeLoading(true); @@ -65,55 +118,129 @@ export function VisitorTrackPage() { .then((list) => alive && (setNotices(list), setNoticeErr(''))) .catch((e) => alive && setNoticeErr(errorMessage(e))) .finally(() => alive && setNoticeLoading(false)); + + // 방문가이드·교통(라이브) — 실패해도 정적 폴백으로 화면 유지(빈 화면 금지). + publicApi + .listVisitorGuide() + .then((list) => alive && setGuides(list)) + .catch(() => alive && setGuides([])); + publicApi + .listTransport() + .then((list) => alive && setTransport(list)) + .catch(() => alive && setTransport([])); + return () => { alive = false; }; }, []); + const useLiveGuides = guides.length > 0; + return ( - {/* 관람 히어로 + 이중 CTA */} -
-
-
-
-
- - {t('track.visitor.eyebrow')} - -

{t('track.visitor.title')}

-

{t('track.visitor.subtitle')}

-
- - {t('track.visitor.ctaPreReg')} - - - {t('track.visitor.ctaTicket')} - -
+ {/* ── 1. 풀블리드 히어로 + AI 관람 도우미(중심) ── STITCH-REPLACE:hero ── */} +
+
+
+ + {t('track.visitor.aiWatermark')} + +
+

{t('track.visitor.hero.title')}

+

{t('track.visitor.hero.subtitle')}

+ + + +
+ + {t('track.visitor.ctaPreReg')} + + + {t('track.visitor.ctaTicket')} +
- {/* 행사 일정 그리드 */} -
+ {/* ── 2. AI 기능 쇼케이스 타일 ── STITCH-REPLACE:tiles ── */} +
-

{t('track.visitor.eventsTitle')}

-

{t('track.visitor.eventsSub')}

+

{t('track.visitor.aiTiles.title')}

+

{t('track.visitor.aiTiles.sub')}

+
+
+
+ {AI_TILES.map(({ key, icon: Icon, to, soon }) => ( + + + {t('ai.assistant.label')} + {soon && {t('track.visitor.aiTiles.soon')}} + + + + + {t(`track.visitor.aiTiles.${key}.title`)} + {t(`track.visitor.aiTiles.${key}.desc`)} + + + + + ))} +
+
+
+ + {/* ── 3. AI 전시·행사 기획 브리핑(연/월) ── */} + + + {/* ── 4. 진행 중·예정 전시 쇼케이스 ── */} +
+
+
+
+

{t('track.visitor.showcaseTitle')}

+

{t('track.visitor.showcaseSub')}

- {/* 방문 가이드 허브 2×3 */} -
+ {/* ── 5. 관심사 맞춤 추천 레일(AI) ── */} +
+
+
+
+

+ {t('track.visitor.recommendTitle')}{' '} + + {t('track.visitor.recommendTag')} + +

+

{t('track.visitor.recommendSub')}

+
+
+
+ {INTEREST_KEYS.map((k) => ( + + ))} +
+ +
+
+ + {/* ── 6. 방문 가이드 허브(2×3, 라이브 우선·정적 폴백) ── */} +
@@ -121,26 +248,102 @@ export function VisitorTrackPage() {

{t('track.visitor.guideSub')}

-
- {GUIDE_CARDS.map(({ key, icon: Icon, to }) => ( -
- - - -
-
{t(`track.visitor.guide.${key}.title`)}
-

{t(`track.visitor.guide.${key}.desc`)}

- - {t(`track.visitor.guide.${key}.link`)} - + + {/* 교통 요약 스트립(라이브 transport) — 없으면 자동 숨김 */} + {transport.length > 0 && ( +
+ {transport.slice(0, 5).map((tr) => ( +
+ + + +
+
{tr.lineName || tr.mode}
+
+ {[tr.route, tr.travelTime].filter(Boolean).join(' · ')} +
+
-
- ))} + ))} +
+ )} + +
+ {useLiveGuides + ? guides.slice(0, 6).map((g) => { + const Icon = guideIcon(g.category, g.title); + return ( + + ); + }) + : GUIDE_CARDS.map(({ key, icon: Icon, to }) => ( +
+ + + +
+
{t(`track.visitor.guide.${key}.title`)}
+

{t(`track.visitor.guide.${key}.desc`)}

+ + {t(`track.visitor.guide.${key}.link`)} + +
+
+ ))}
- {/* 공지·알림마당 */} + {/* ── 7. 관람객 여정 3-스텝 ── */} +
+
+
+
+

{t('track.visitor.journeyTitle')}

+

{t('track.visitor.journeySub')}

+
+
+
    + {JOURNEY.map(({ key, icon: Icon, to }, i) => ( +
  1. + + {i + 1} + + + + +
    +
    {t(`track.visitor.journey.${key}.title`)}
    +

    {t(`track.visitor.journey.${key}.desc`)}

    + + {t(`track.visitor.journey.${key}.link`)} + +
    + {i < JOURNEY.length - 1 && ( + + + + )} +
  2. + ))} +
+
+
+ + {/* ── 8. 공지·뉴스 ── */}
@@ -175,6 +378,31 @@ export function VisitorTrackPage() { )}
+ + {/* ── 9. 전시장 소개 밴드 ── */} +
+
+
+
+ + + +
+

{t('track.visitor.venueTitle')}

+

{t('track.visitor.venueSub')}

+
+
+
+ {(['halls', 'area', 'parking', 'access'] as const).map((k) => ( +
+
{t(`track.visitor.venue.${k}.label`)}
+
{t(`track.visitor.venue.${k}.value`)}
+
+ ))} +
+
+
+
); } diff --git a/src/frontend/src/screens/public/public.css b/src/frontend/src/screens/public/public.css index a95bf5f..befbdf7 100644 --- a/src/frontend/src/screens/public/public.css +++ b/src/frontend/src/screens/public/public.css @@ -2792,10 +2792,10 @@ /* =================================================================== * §3C 3-트랙 관문·서브홈 (SCR-T0~T3) — 상단 스위처 + 트랙 페이지 헬퍼 - * 신규 토큰 0 (기존 --color-*/--radius-*/--kxp-maxw 재사용). WCAG AA. + * 신규 토큰 0 (기존 color·radius·kxp-maxw 변수 재사용). WCAG AA. * =================================================================== */ -/* -- 상단 3-트랙 스위처 바 (전 SCR-T*/SCR-P* 공통 크롬) -- */ +/* -- 상단 3-트랙 스위처 바 (전 SCR-T·SCR-P 공통 크롬) -- */ .kxp-topbar { position: sticky; top: 0; @@ -3210,3 +3210,794 @@ font-weight: 800; color: var(--color-primary-800, #003a66); } + +/* ═══════════════════════════════════════════════════════════════════════════ + SCR-T1 visitor 메인 v2.3.2 — AI 관람 도우미 홈페이지 격상 (신규 토큰 0) + ═══════════════════════════════════════════════════════════════════════════ */ + +/* ── 1. 풀블리드 히어로 + AI 도우미 ── */ +.kxp-vhero { + position: relative; + isolation: isolate; + padding: 56px 20px 64px; + overflow: hidden; +} +.kxp-vhero__bg { + position: absolute; + inset: 0; + z-index: -2; + background: + radial-gradient(1200px 600px at 80% -10%, rgba(109, 74, 255, 0.35) 0%, transparent 60%), + linear-gradient(135deg, #0b2a4a 0%, #0066b3 60%, #2f8fd6 120%); +} +.kxp-vhero__scrim { + position: absolute; + inset: 0; + z-index: -1; + background: linear-gradient(180deg, rgba(11, 20, 32, 0.35) 0%, rgba(11, 20, 32, 0.55) 100%); +} +.kxp-vhero__wm { + position: absolute; + right: 16px; + bottom: 12px; + font-size: 11px; + letter-spacing: 0.02em; + color: rgba(255, 255, 255, 0.55); + z-index: 0; + pointer-events: none; +} +.kxp-vhero__inner { + max-width: 860px; + margin: 0 auto; + text-align: center; + color: var(--color-white); +} +.kxp-vhero__title { + font-size: clamp(28px, 5.2vw, 46px); + font-weight: 800; + line-height: 1.18; + letter-spacing: -0.025em; + margin: 8px 0 12px; + color: var(--color-white); +} +.kxp-vhero__lead { + font-size: clamp(15px, 2.2vw, 19px); + color: rgba(255, 255, 255, 0.9); + margin: 0 0 28px; +} +.kxp-vhero__cta { + display: flex; + gap: 12px; + justify-content: center; + flex-wrap: wrap; + margin-top: 24px; +} +@media (min-width: 768px) { + .kxp-vhero { + padding: 88px 24px 96px; + } +} + +/* ── AI 관람 도우미(공용) ── */ +.kxp-ai { + max-width: 760px; + margin: 0 auto; + text-align: left; +} +.kxp-ai__bar { + display: flex; + align-items: center; + gap: 8px; + background: var(--color-white); + border: 1px solid var(--color-neutral-200); + border-radius: 999px; + padding: 8px 8px 8px 14px; + box-shadow: var(--shadow-level2); +} +.kxp-ai__badge { + flex: none; + display: inline-flex; +} +.kxp-ai__input { + flex: 1 1 auto; + min-width: 0; + border: 0; + outline: none; + background: transparent; + font-size: 16px; + color: var(--color-neutral-900); + padding: 8px 4px; +} +.kxp-ai__input::placeholder { + color: var(--color-neutral-500); +} +.kxp-ai__send { + flex: none; + display: inline-flex; + align-items: center; + justify-content: center; + width: 44px; + height: 44px; + border: 0; + border-radius: 999px; + background: var(--color-ai-accent); + color: var(--color-white); + cursor: pointer; + transition: background 0.15s ease; +} +.kxp-ai__send:hover:not(:disabled) { + background: var(--color-ai-accent-strong); +} +.kxp-ai__send:disabled { + opacity: 0.5; + cursor: not-allowed; +} +.kxp-ai__chips { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-top: 14px; + justify-content: center; +} +.kxp-ai__chip { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 7px 13px; + border-radius: 999px; + border: 1px solid rgba(255, 255, 255, 0.5); + background: rgba(255, 255, 255, 0.14); + color: var(--color-white); + font-size: 13px; + font-weight: 600; + cursor: pointer; + transition: background 0.15s ease; +} +.kxp-ai__chip:hover { + background: rgba(255, 255, 255, 0.26); +} +.kxp-ai__chip--sm { + padding: 5px 11px; + font-size: 12px; + border-color: var(--color-neutral-200); + background: var(--color-neutral-050); + color: var(--color-primary-700); +} +.kxp-ai__chip--sm:hover { + background: var(--color-primary-050); +} + +/* 답변 패널 — 라이트 카드(대비 확보) */ +.kxp-ai__panel { + margin-top: 16px; +} +.kxp-ai__thinking { + display: inline-flex; + align-items: center; + gap: 10px; + padding: 12px 16px; + border-radius: var(--radius-lg); + background: var(--color-white); + color: var(--color-neutral-700); + font-size: 14px; + font-weight: 600; + box-shadow: var(--shadow-level2); +} +.kxp-ai__dots { + display: inline-flex; + gap: 4px; +} +.kxp-ai__dots i { + width: 6px; + height: 6px; + border-radius: 50%; + background: var(--color-ai-accent); + animation: kxpDot 1s infinite ease-in-out; +} +.kxp-ai__dots i:nth-child(2) { + animation-delay: 0.15s; +} +.kxp-ai__dots i:nth-child(3) { + animation-delay: 0.3s; +} +@keyframes kxpDot { + 0%, 60%, 100% { opacity: 0.3; transform: translateY(0); } + 30% { opacity: 1; transform: translateY(-3px); } +} +.kxp-ai__answer { + text-align: left; + background: var(--color-white); + border: var(--border-card); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-level2); + padding: 18px 20px; + color: var(--color-neutral-800, #182230); +} +.kxp-ai__answer--error { + display: flex; + flex-direction: column; + gap: 12px; + align-items: flex-start; +} +.kxp-ai__answerhead { + display: flex; + align-items: center; + gap: 10px; + flex-wrap: wrap; + margin-bottom: 8px; +} +.kxp-ai__degraded { + font-size: 12px; + font-weight: 600; + color: var(--color-warning); + background: var(--color-warning-bg); + padding: 3px 9px; + border-radius: 999px; +} +.kxp-ai__text { + font-size: 15px; + line-height: 1.65; + color: var(--color-neutral-900); + margin: 0 0 12px; + white-space: pre-line; +} +.kxp-ai__muted { + color: var(--color-neutral-500); +} +.kxp-ai__cards { + display: grid; + gap: 14px; + margin: 6px 0 14px; +} +.kxp-ai__group { + border-top: 1px dashed var(--color-neutral-200); + padding-top: 12px; +} +.kxp-ai__grouptitle { + font-size: 13px; + font-weight: 700; + color: var(--color-neutral-700); + margin: 0 0 10px; +} +.kxp-ai__grouprows { + display: grid; + gap: 8px; +} +.kxp-ai__evcard { + display: flex; + align-items: center; + gap: 12px; + padding: 8px; + border: var(--border-card); + border-radius: var(--radius-lg); +} +.kxp-ai__evposter { + flex: none; + width: 44px; + height: 60px; + border-radius: 6px; + overflow: hidden; + background: linear-gradient(135deg, #0066b3 0%, #004c86 100%); + display: flex; + align-items: center; + justify-content: center; +} +.kxp-ai__evposter img { + width: 100%; + height: 100%; + object-fit: cover; +} +.kxp-ai__evfallback { + color: var(--color-white); + font-weight: 800; + font-size: 15px; +} +.kxp-ai__evbody { + flex: 1 1 auto; + min-width: 0; +} +.kxp-ai__evname { + font-weight: 700; + font-size: 14px; + color: var(--color-neutral-900); + margin-bottom: 4px; +} +.kxp-ai__evmeta, +.kxp-ai__infodesc { + display: flex; + flex-wrap: wrap; + gap: 10px 14px; + font-size: 12.5px; + color: var(--color-neutral-500); + font-variant-numeric: tabular-nums; +} +.kxp-ai__evmeta span { + display: inline-flex; + align-items: center; + gap: 5px; +} +.kxp-ai__inforow { + display: flex; + align-items: flex-start; + gap: 10px; +} +.kxp-ai__ic { + flex: none; + display: inline-flex; + align-items: center; + justify-content: center; + width: 34px; + height: 34px; + border-radius: 8px; + background: var(--color-primary-050); + color: var(--color-primary-700); +} +.kxp-ai__infotitle { + font-weight: 700; + font-size: 14px; + color: var(--color-neutral-900); +} +.kxp-ai__followups { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin: 6px 0 12px; +} +.kxp-ai__cites { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px; + padding-top: 10px; + border-top: 1px solid var(--color-neutral-100); +} +.kxp-ai__citelabel { + font-size: 12px; + font-weight: 700; + color: var(--color-neutral-500); +} +.kxp-ai__cite { + display: inline-flex; + align-items: center; + gap: 4px; + font-size: 12px; + color: var(--color-primary-700); + background: var(--color-primary-050); + padding: 3px 9px; + border-radius: 999px; +} +a.kxp-ai__cite:hover { + text-decoration: underline; +} +.kxp-ai__disclaimer { + margin: 12px 0 0; + font-size: 11.5px; + color: var(--color-neutral-500); +} + +/* ── 2. AI 기능 쇼케이스 타일 ── */ +.kxp-aitiles { + display: grid; + grid-template-columns: 1fr; + gap: 14px; +} +@media (min-width: 640px) { + .kxp-aitiles { + grid-template-columns: repeat(2, 1fr); + } +} +@media (min-width: 1024px) { + .kxp-aitiles { + grid-template-columns: repeat(3, 1fr); + } +} +.kxp-aitile { + position: relative; + display: flex; + flex-direction: column; + gap: 8px; + padding: 20px; + background: var(--color-ai-surface); + border: 1px solid var(--color-neutral-200); + border-radius: var(--radius-lg); + transition: transform 0.15s ease, box-shadow 0.15s ease; +} +.kxp-aitile:hover { + transform: translateY(-2px); + box-shadow: var(--shadow-level2); +} +.kxp-aitile__top { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; +} +.kxp-aitile__soon { + font-size: 11px; + font-weight: 700; + color: var(--color-neutral-500); + background: var(--color-neutral-100); + padding: 2px 8px; + border-radius: 999px; +} +.kxp-aitile__ic { + display: inline-flex; + align-items: center; + justify-content: center; + width: 44px; + height: 44px; + border-radius: 10px; + background: var(--color-white); + color: var(--color-ai-accent); + margin-top: 4px; +} +.kxp-aitile__title { + font-weight: 800; + font-size: 16px; + color: var(--color-neutral-900); +} +.kxp-aitile__desc { + font-size: 13.5px; + color: var(--color-neutral-500); + line-height: 1.5; +} +.kxp-aitile__go { + position: absolute; + top: 18px; + right: 18px; + color: var(--color-ai-accent); + opacity: 0; + transition: opacity 0.15s ease; +} +.kxp-aitile:hover .kxp-aitile__go { + opacity: 1; +} + +/* ── 3. AI 기획 브리핑 ── */ +.kxp-briefing__head { + align-items: center; +} +.kxp-briefing__toggle { + display: inline-flex; + gap: 4px; + padding: 4px; + background: var(--color-neutral-100); + border-radius: 999px; +} +.kxp-seg { + border: 0; + background: transparent; + padding: 8px 18px; + border-radius: 999px; + font-size: 14px; + font-weight: 700; + color: var(--color-neutral-500); + cursor: pointer; +} +.kxp-seg.is-active { + background: var(--color-white); + color: var(--color-primary-700); + box-shadow: var(--shadow-level2); +} +.kxp-briefing__ai { + display: flex; + align-items: center; + gap: 12px; + flex-wrap: wrap; + padding: 14px 16px; + background: var(--color-ai-surface); + border: 1px solid var(--color-neutral-200); + border-radius: var(--radius-lg); + margin-bottom: 18px; +} +.kxp-briefing__aitext { + margin: 0; + font-size: 15px; + font-weight: 600; + color: var(--color-neutral-900); + line-height: 1.55; +} +.kxp-briefing__timeline { + display: grid; + grid-template-columns: repeat(12, 1fr); + gap: 6px; + align-items: end; + height: 180px; + padding: 12px; + background: var(--color-white); + border: var(--border-card); + border-radius: var(--radius-lg); +} +.kxp-briefing__lane { + display: flex; + flex-direction: column; + align-items: center; + justify-content: flex-end; + gap: 6px; + height: 100%; + border: 0; + background: transparent; + cursor: pointer; +} +.kxp-briefing__count { + font-size: 11px; + font-weight: 700; + color: var(--color-neutral-500); + font-variant-numeric: tabular-nums; +} +.kxp-briefing__bar { + width: 60%; + min-height: 6px; + border-radius: 6px 6px 0 0; + background: var(--color-primary-600); + transition: background 0.15s ease; +} +.kxp-briefing__lane:hover .kxp-briefing__bar { + background: var(--color-primary-700); +} +.kxp-briefing__lane.is-peak .kxp-briefing__bar { + background: var(--color-ai-accent); +} +.kxp-briefing__mo { + font-size: 11px; + color: var(--color-neutral-500); + font-variant-numeric: tabular-nums; +} +.kxp-briefing__timeline--skel .kxp-briefing__bar { + background: var(--color-neutral-100); +} +.kxp-briefing__nav { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 14px; +} +.kxp-briefing__navbtn { + display: inline-flex; + align-items: center; + justify-content: center; + width: 36px; + height: 36px; + border: var(--border-card); + border-radius: 8px; + background: var(--color-white); + color: var(--color-neutral-700); + cursor: pointer; +} +.kxp-briefing__navbtn:hover { + border-color: var(--color-primary-600); + color: var(--color-primary-600); +} +.kxp-briefing__navlabel { + font-size: 18px; + font-weight: 800; + color: var(--color-neutral-900); + font-variant-numeric: tabular-nums; +} +.kxp-briefing__list { + display: grid; + gap: 8px; +} +.kxp-briefing__row { + display: flex; + align-items: center; + gap: 12px; + padding: 14px 16px; + background: var(--color-white); + border: var(--border-card); + border-radius: var(--radius-lg); +} +.kxp-briefing__rowmain { + flex: 1 1 auto; + min-width: 0; +} +.kxp-briefing__rowname { + font-weight: 700; + font-size: 15px; + color: var(--color-neutral-900); + margin-bottom: 5px; +} +.kxp-briefing__rowmeta { + display: flex; + flex-wrap: wrap; + gap: 8px 14px; + align-items: center; + font-size: 13px; + color: var(--color-neutral-500); + font-variant-numeric: tabular-nums; +} +.kxp-briefing__rowmeta span { + display: inline-flex; + align-items: center; + gap: 5px; +} + +/* ── 5. 관심사 추천 칩 ── */ +.kxp-vinterests { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-bottom: 24px; +} +.kxp-vinterests .kxp-ai__chip { + border-color: var(--color-neutral-200); + background: var(--color-white); + color: var(--color-neutral-700); +} +.kxp-vinterests .kxp-ai__chip.is-active { + border-color: var(--color-ai-accent); + background: var(--color-ai-surface); + color: var(--color-ai-accent); +} +.kxp-inlineai { + display: inline-flex; + vertical-align: middle; +} + +/* ── 6. 교통 스트립 ── */ +.kxp-transtrip { + display: grid; + grid-template-columns: 1fr; + gap: 10px; + margin-bottom: 20px; +} +@media (min-width: 640px) { + .kxp-transtrip { + grid-template-columns: repeat(2, 1fr); + } +} +@media (min-width: 1024px) { + .kxp-transtrip { + grid-template-columns: repeat(3, 1fr); + } +} +.kxp-transtrip__item { + display: flex; + align-items: flex-start; + gap: 10px; + padding: 12px 14px; + background: var(--color-primary-050); + border-radius: var(--radius-lg); +} +.kxp-transtrip__ic { + flex: none; + display: inline-flex; + color: var(--color-primary-700); +} +.kxp-transtrip__name { + font-weight: 700; + font-size: 14px; + color: var(--color-neutral-900); +} +.kxp-transtrip__desc { + font-size: 12.5px; + color: var(--color-neutral-500); + font-variant-numeric: tabular-nums; +} + +/* ── 7. 여정 3-스텝 ── */ +.kxp-journey { + list-style: none; + margin: 0; + padding: 0; + display: grid; + grid-template-columns: 1fr; + gap: 14px; +} +@media (min-width: 768px) { + .kxp-journey { + grid-template-columns: repeat(3, 1fr); + } +} +.kxp-journey__step { + position: relative; + display: flex; + flex-direction: column; + gap: 8px; + padding: 22px; + background: var(--color-white); + border: var(--border-card); + border-radius: var(--radius-lg); +} +.kxp-journey__num { + display: inline-flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + border-radius: 50%; + background: var(--color-primary-600); + color: var(--color-white); + font-weight: 800; + font-size: 14px; +} +.kxp-journey__ic { + display: inline-flex; + color: var(--color-primary-700); +} +.kxp-journey__title { + font-weight: 800; + font-size: 16px; + color: var(--color-neutral-900); +} +.kxp-journey__desc { + font-size: 13.5px; + color: var(--color-neutral-500); + line-height: 1.5; + margin: 0; +} +.kxp-journey__arrow { + display: none; +} +@media (min-width: 768px) { + .kxp-journey__arrow { + display: inline-flex; + position: absolute; + right: -17px; + top: 50%; + transform: translateY(-50%); + z-index: 1; + color: var(--color-neutral-300, #d0d5dd); + } +} + +/* ── 9. 전시장 소개 밴드 ── */ +.kxp-venue { + display: grid; + grid-template-columns: 1fr; + gap: 24px; + align-items: center; +} +@media (min-width: 900px) { + .kxp-venue { + grid-template-columns: 1fr 1.4fr; + } +} +.kxp-venue__intro { + display: flex; + gap: 14px; + align-items: flex-start; +} +.kxp-venue__ic { + flex: none; + display: inline-flex; + align-items: center; + justify-content: center; + width: 52px; + height: 52px; + border-radius: 12px; + background: var(--color-primary-050); + color: var(--color-primary-700); +} +.kxp-venue__stats { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 12px; + margin: 0; +} +@media (min-width: 640px) { + .kxp-venue__stats { + grid-template-columns: repeat(4, 1fr); + } +} +.kxp-venue__stat { + padding: 16px; + background: var(--color-white); + border: var(--border-card); + border-radius: var(--radius-lg); + text-align: center; +} +.kxp-venue__stat dt { + font-size: 12.5px; + color: var(--color-neutral-500); + margin-bottom: 6px; +} +.kxp-venue__stat dd { + margin: 0; + font-size: 20px; + font-weight: 800; + color: var(--color-primary-700); +} +.kxp-tabnum { + font-variant-numeric: tabular-nums; +} diff --git a/src/frontend/src/screens/public/publicApi.ts b/src/frontend/src/screens/public/publicApi.ts index 310a3a5..5f34edb 100644 --- a/src/frontend/src/screens/public/publicApi.ts +++ b/src/frontend/src/screens/public/publicApi.ts @@ -49,6 +49,76 @@ export interface PublicNotice { createdAt?: string | null; } +// ── 관람 안내 · AI 도우미 (백엔드 PublicGuideDtos / VisitorAssistantDtos, V43 라이브) ── +/** 연/월 캘린더 행사 카드 (v_event_calendar). lifecycle ∈ UPCOMING|ONGOING|ENDED|UNKNOWN. */ +export interface PublicCalendarEvent { + id: string; + name: string; + category: string | null; + hallLabel: string | null; + posterUrl: string | null; + startDate: string | null; + endDate: string | null; + eventYear: number | null; + eventMonth: number | null; + lifecycle: string | null; +} + +/** 월별 전시 건수 요약 (v_event_monthly_summary) — 연간 기획 요약 근거. */ +export interface MonthlySummary { + eventYear: number | null; + eventMonth: number | null; + eventCount: number | null; +} + +/** 관람객 방문가이드 항목 (visitor_guide). */ +export interface VisitorGuide { + id: string; + category: string | null; + title: string | null; + body: string | null; + summary: string | null; + displayOrder: number | null; + sourceUrl: string | null; +} + +/** 교통수단 구조화 항목 (transport_info). */ +export interface TransportInfo { + id: string; + mode: string | null; + lineName: string | null; + route: string | null; + fromPoint: string | null; + travelTime: string | null; + note: string | null; + displayOrder: number | null; + sourceUrl: string | null; +} + +/** AI 관람 도우미 인용 출처 — type ∈ EVENT|TRANSPORT|GUIDE. */ +export interface AssistantCitation { + type: string; + id: string; + title: string; + sourceUrl: string | null; +} + +/** + * AI 관람 도우미 응답 (POST /api/public/ai/visitor-assistant). + * grounded=false → DB 근거를 못 찾음(정직한 "찾지 못함"). provider ∈ claude|ollama|search|none. + */ +export interface AssistantResult { + question: string; + answer: string; + events: PublicCalendarEvent[]; + transport: TransportInfo[]; + guide: VisitorGuide[]; + citations: AssistantCitation[]; + grounded: boolean; + provider: string; + degraded: boolean; +} + // ── 문의 접수 (백엔드 InquiryRequest / InquiryReceiptDto) ──────────────────── export interface InquiryPayload { inquiryType?: string; @@ -134,4 +204,36 @@ export const publicApi = { /** M17 공개 공지(게시분) — 최근순. 백엔드 미배포 시 에러(빈/안내로 처리). */ listNotices: (limit = 5) => api.get(`/api/public/cms/NOTICE${qs({ limit: String(limit) })}`, opt), + + // ── 관람 안내 · AI 도우미 (V43 라이브) ────────────────────────────────────── + /** AI 관람 도우미 — 자연어 질문을 크롤 적재 DB 근거로 답변(비인증). */ + askVisitorAssistant: (question: string) => + api.post('/api/public/ai/visitor-assistant', { question }, opt), + + /** 연/월/상태 캘린더(v_event_calendar). lifecycle ∈ UPCOMING|ONGOING|ENDED. */ + listCalendar: (params: { year?: number; month?: number; lifecycle?: string; limit?: number } = {}) => + api.get( + `/api/public/events/calendar${qs({ + year: params.year != null ? String(params.year) : undefined, + month: params.month != null ? String(params.month) : undefined, + lifecycle: params.lifecycle, + limit: params.limit != null ? String(params.limit) : undefined, + })}`, + opt, + ), + + /** 월별 전시 건수 집계(v_event_monthly_summary) — 연간 기획 요약. */ + listMonthlySummary: (year?: number) => + api.get( + `/api/public/events/monthly-summary${qs({ year: year != null ? String(year) : undefined })}`, + opt, + ), + + /** 방문가이드(교통/주차/입장/편의/접근성/개요). */ + listVisitorGuide: (category?: string) => + api.get(`/api/public/visitor-guide${qs({ category })}`, opt), + + /** 교통수단 구조화(SUBWAY/GTX/BUS/CAR/AIRPORT/KTX). */ + listTransport: (mode?: string) => + api.get(`/api/public/transport${qs({ mode })}`, opt), };