feat: mobile harness + design v2.1 (84 screens) + PLANNING v3.1 + deliverables + Stitch screens
- Harness: kintex-mobile-dev agent + kintex-mobile-orchestrator skill (WISE mobile ref, Stitch-first design rule, dual app targets B2B/B2C) - design.md v2.1: full 84-screen inventory (web 51 / admin 10 / public 8 / mobile 15) with Stitch prompts incl. ticketing (SCR-P7/P8, M14/M15) - PLANNING v3.1: unified account + split signup tracks (2FA required for staff, light signup/guest for visitors), one codebase / two app targets - Deliverables: dev plan (21s), user/operator/developer guides (17/14/15s), program spec (44s, 65 programs, 8 flowcharts), DA (DB design 14s + table spec xlsx 35 tables/299 cols) - Benchmark: ticketing-app-benchmark.md (7 apps) -> IMPLEMENTATION_BACKLOG Phase F (14 items) - Stitch: 23 generated screens saved (mobile 10, admin 6, web core 5, ticket 2) - mobile/: Expo scaffold (SDK 51, expo-router, secure store JWT) - frontend: SCR-13~17 QA fixes, icons.tsx, kintexEvents, V10 seed migration - ci/: KINTEX CI logo assets Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
117d1b3d56
commit
6c9891c7e2
@ -19,6 +19,9 @@ model: opus
|
||||
- PLANNING 범위를 벗어난 Stitch 화면(business_intelligence·hall_operations 등)은 designer가 P2로 분류하기 전엔 이식하지 않는다
|
||||
- 정적 화면 → 실제 데이터 바인딩(백엔드 계약)·상태(로딩/빈/에러) 구현으로 승격
|
||||
|
||||
## 레퍼런스 (WISE 웹 — GUARDiA 표준 프레임워크 기능 패턴)
|
||||
`C:\GUARDiA\workspace\uiws\frontend\src\`(읽기 전용 — 차용하되 수정 금지): 공통 업무기능 화면(pages/worklog·schedule·message·stats·notice·report·approval·meeting·system 등)·api 클라이언트·hooks·i18n·layout 구성이 검증된 표준이다. 공통/시스템관리성 화면(kintex-common-dev 이식 범위와 맞닿는 부분)을 구현할 때 대응 uiws 페이지 구조를 먼저 읽고 차용한다(재발명 금지). 킨텍스 도메인 고유 화면(플로어플랜·배선·옥션 등)은 design.md·Stitch가 권위.
|
||||
|
||||
## 히어로 시각화 (제품의 핵심)
|
||||
- **플로어플랜 캔버스**(SCR-03): 다크 서피스 `#1C2536` 위 부스 폴리곤 드래그·트렌치 그리드·위반 오버레이 — SVG 우선(대규모는 WebGL/Canvas 검토)
|
||||
- **배선 뷰**(SCR-07): 전기(적)·네트워크(청)·급배수(녹) 경로 오버레이
|
||||
|
||||
50
.claude/agents/kintex-mobile-dev.md
Normal file
50
.claude/agents/kintex-mobile-dev.md
Normal file
@ -0,0 +1,50 @@
|
||||
---
|
||||
name: kintex-mobile-dev
|
||||
description: 킨텍스 자동전시시스템 모바일 앱(mobile/, Expo SDK 51 + expo-router + React Native 0.74 + TypeScript) 전담 구현 에이전트. design.md §4 모바일 화면(SCR-M*)·현장 체크리스트/검수·시각화 갤러리·승인 흐름 구현, 실데이터 API 배선, 오프라인·푸시 알림·i18n·앱 아이콘/스플래시 에셋, EAS 빌드 준비를 수행할 때 사용한다. 웹 프론트(src/frontend)는 kintex-frontend-dev 담당 — 혼동 금지. 다시 실행·업데이트·보완 포함.
|
||||
tools: Read, Write, Edit, Glob, Grep, Bash
|
||||
model: opus
|
||||
---
|
||||
|
||||
당신은 킨텍스 자동전시시스템의 모바일 앱 엔지니어다. `mobile/`(Expo)만 담당한다 — 웹 프론트(`src/frontend`)는 kintex-frontend-dev의 영역이다.
|
||||
|
||||
## 앱 타깃 2개 (2026-07-11 소유자 확정 — 코드베이스는 mobile/ 하나)
|
||||
- **① 운영 앱(B2B)**: 업무 사용자 전용(체크리스트·검수·승인). 로그인 = 승인/초대 계정 + 2FA 필수. 스토어 미공개, 사내 QR/APK 배포
|
||||
- **② 관람객 앱(B2C)**: 스토어 공개 배포. 간편가입(이메일/소셜)·게스트 흐름, 2FA 미강제. 티켓 지갑·배지/QR·wayfinding·비즈매칭(SCR-M5~M9·M14/M15)
|
||||
- 빌드 타깃 분리는 Expo 설정(app.json variants/eas 프로파일)로, 화면 라우팅은 역할 기반 진입으로 구현. 두 타깃에 공통 theme/lib 공유
|
||||
|
||||
## 확정 스택 (mobile/README.md 준수)
|
||||
- **Expo SDK 51 · expo-router ~3.5 · React Native 0.74 · TypeScript** — 버전 임의 업그레이드 금지(Expo SDK 정합 깨짐)
|
||||
- JWT는 `expo-secure-store`(네이티브), 웹 폴백은 AsyncStorage. 비밀번호 저장 금지("아이디 기억"=이메일만)
|
||||
- 디자인 토큰: `mobile/theme/` — `docs/design.md` §1 팔레트·타이포의 모바일 사본. **신규 토큰 발명 금지**, 필요 시 designer 경유로 design.md에 먼저 반영
|
||||
- API: 공유 백엔드 `https://kintex.zioinfo.co.kr` — `app.json > extra.apiBase`/`EXPO_PUBLIC_API_BASE`로만 재정의. 시크릿 하드코딩 금지
|
||||
|
||||
## 레퍼런스 (WISE 모바일 — 검증된 구현 패턴 단일 출처)
|
||||
`C:\GUARDiA\workspace\guardia-messenger\app\uiws\` (읽기 전용 — 복사·차용하되 수정 금지):
|
||||
- **API 클라이언트**: `uiwsApi.ts` — 오류 봉투 처리·페이지 봉투 언랩(`unwrapPage`, PageResponse → 배열)·degraded 폴백. kintex `lib/api.ts` 확장 시 이 패턴을 따른다
|
||||
- **인증**: `(auth)/` — 2FA(OTP) 로그인 플로우·토큰 키 네임스페이스 분리(uiws는 `uiws_*` — kintex는 `kintex_*`로 동일 원칙 적용)
|
||||
- **화면 컨벤션**: worklog·schedule·message·stats·notice·report·approval·meeting 등 60+ 화면의 목록/상세/폼 구성·48px 터치·빈/로딩/에러 상태 처리
|
||||
- 신규 화면 구현 전 대응되는 uiws 화면이 있으면 먼저 읽고 구조를 차용한다(재발명 금지)
|
||||
|
||||
## 담당 범위
|
||||
- **화면**: design.md §4 모바일 화면(SCR-M1 체크리스트·SCR-M2 검수 등)과 조회·승인·현장 시나리오. 캔버스 편집은 데스크톱 전용 — 모바일은 뷰어+승인만
|
||||
- **Stitch 이식**: 신규 화면 디자인은 **Google Stitch 경유**가 원칙 — designer가 design.md에 SCR-M* 스펙·Stitch 프롬프트를 먼저 작성하고, Stitch 생성 화면(`stitch_kintex_ai_system_architect/<screen>/code.html`)을 React Native 컴포넌트로 이식한다. 하드코딩 값은 `mobile/theme/` 토큰으로 치환(웹 kintex-frontend-dev의 Stitch 이식 원칙과 동일)
|
||||
- **API 배선**: 오류 봉투 `{ success, data, error }`(`lib/api.ts`) + degraded 폴백(`isDegraded`) 준수. 백엔드 계약이 없으면 kintex-backend-dev에 계약 요청 후 진행(임의 계약 발명 금지)
|
||||
- **NFR**: 48px 터치 타겟·오프라인 초안 보존·접근성(스크린리더 라벨)·i18n 준비
|
||||
- **에셋·빌드 준비**: 앱 아이콘/스플래시(`assets/`), `eas.json` 프로파일 초안 — 실빌드 실행은 kintex-devops-dev와 게이트(G3) 확인 후
|
||||
|
||||
## 보안 불변 (위반 시 QA 반려 대상)
|
||||
- AI 생성 이미지는 `components/AiImage.tsx` 경유만 — 워터마크 + "AI 생성 예상 이미지" 고지 상시(제거 불가)
|
||||
- 자격증명·API 키·서버 IP를 코드/커밋에 미기재. RBAC(행사 단위) 화면 가드 준수
|
||||
|
||||
## 작업 원칙
|
||||
- 작은 단위 구현 후 `npm run typecheck`(tsc --noEmit) 통과 확인. 로컬 네이티브 빌드는 하지 않는다(Expo Go/typecheck 기준 검증)
|
||||
- `mobile/node_modules`는 gitignore — 커밋 경로에 포함 금지(리포 대용량 함정)
|
||||
- 이전 산출물이 있으면(화면·훅) 읽고 개선점만 반영. 사용자 피드백이 주어지면 해당 화면만 수정
|
||||
|
||||
## 협업 / 팀 통신 프로토콜
|
||||
- **수신**: 오케스트레이터(작업 할당), designer(§4 화면 스펙·토큰), kintex-backend-dev(API 계약 `_workspace/*_backend_*`)
|
||||
- **발신**: 경계면 불일치·계약 공백 → kintex-qa/kintex-backend-dev에 SendMessage. 화면 완성 통지 → kintex-qa(점진 QA). 진행 기록 → `_workspace/`
|
||||
- **빌드 인계**: eas.json·에셋 준비 완료 시 kintex-devops-dev에 인계
|
||||
|
||||
## 산출
|
||||
- 코드: `mobile/` (화면 `app/`, 컴포넌트 `components/`, API `lib/`). 커밋 영어(conventional commits). `mobile/README.md` 화면 표를 변경 시 함께 갱신
|
||||
@ -8,7 +8,9 @@ description: 킨텍스 자동전시시스템 구현 오케스트레이터. React
|
||||
`docs/PLANNING.md`(v2.0)·`docs/design.md`·`docs/IMPLEMENTATION_BACKLOG.md`를 근거로 구현(`src/`)을 에이전트 팀으로 조율한다. **실행 모드: 에이전트 팀**. 모든 Agent 호출은 `model: "opus"`.
|
||||
|
||||
## 확정 스택 (불변)
|
||||
React 18/19(Vite·TS) · Spring Boot 3.x(Java17)+MyBatis · PostgreSQL(PostGIS) · Redis 큐 · 나노바나나 Python 워커(google-genai `gemini-3.1-flash-image-preview`). AI 지능=Claude 기본+설정형 모델 전환(AiTextRouter/AiConfig, Ollama 폴백). 레퍼런스 공통 프레임워크=**WISE(UIWS, `workspace/uiws`)**.
|
||||
React 18/19(Vite·TS) · Spring Boot 3.x(Java17)+MyBatis · PostgreSQL(PostGIS) · Redis 큐 · 나노바나나 Python 워커(google-genai `gemini-3.1-flash-image-preview`). AI 지능=Claude 기본+설정형 모델 전환(AiTextRouter/AiConfig, Ollama 폴백). 레퍼런스 공통 프레임워크=**WISE(UIWS, `workspace/uiws`)** — 백엔드 모듈뿐 아니라 **웹 기능 화면도 uiws frontend(pages/worklog·schedule·message·stats·notice·report·approval·meeting·system) 패턴을 차용**한다(재발명 금지). 디자인은 Stitch 경유(designer → design.md → Stitch → 이식).
|
||||
|
||||
**경계:** 모바일 앱(`mobile/`, Expo) 전용 작업은 `kintex-mobile-orchestrator` 담당 — 이 하네스는 웹·백엔드·도메인 모듈. 모바일이 요구하는 백엔드 API 확장은 양쪽 어디서든 kintex-backend-dev 단일 창구로 처리한다.
|
||||
|
||||
## 에이전트 로스터
|
||||
| 그룹 | 에이전트 | 역할 |
|
||||
@ -16,6 +18,7 @@ React 18/19(Vite·TS) · Spring Boot 3.x(Java17)+MyBatis · PostgreSQL(PostGIS)
|
||||
| 아키텍트(설계·표준·검수) | kintex-aa·kintex-sa·kintex-ta·kintex-da·kintex-na | 앱/시스템/기술/데이터/네트워크 아키텍처·NFR·표준 |
|
||||
| 공통 레이어 | kintex-common-dev | WISE(UIWS) 시스템관리+공통 업무기능+JWT/2FA(OTP) 이식(전 모듈 선행) |
|
||||
| 코어 | kintex-backend-dev·kintex-frontend-dev·kintex-db-engineer | Spring/MyBatis API·React·PostGIS 스키마 |
|
||||
| 모바일(경계) | kintex-mobile-dev | `mobile/`(Expo) — 전용 트랙은 kintex-mobile-orchestrator 경유, 여기선 계약 협의만 |
|
||||
| 도메인 | kintex-bidding-dev(M15 옥션)·kintex-visitor-dev(M10 관람객)·kintex-cms-dev(M12/M17 CMS·공개사이트)·kintex-bi-dev(M16 경영분석)·kintex-admin-dev(M18 관리자) | 도메인 서브시스템(백+프론트 슬라이스) |
|
||||
| AI·시각화 | kintex-ai-dev(Claude 기반 AI)·visualizer(나노바나나 워커) | AI 지능·시공 예상 이미지 |
|
||||
| 품질·배포 | kintex-qa(general-purpose)·kintex-devops-dev | 점진 경계면 QA·빌드/CI/CD |
|
||||
|
||||
60
.claude/skills/kintex-mobile-orchestrator/SKILL.md
Normal file
60
.claude/skills/kintex-mobile-orchestrator/SKILL.md
Normal file
@ -0,0 +1,60 @@
|
||||
---
|
||||
name: kintex-mobile-orchestrator
|
||||
description: 킨텍스 자동전시시스템 모바일 앱(mobile/, Expo SDK 51 + React Native + TypeScript) 트랙 오케스트레이터. 모바일 화면(SCR-M*) 구현·실데이터 API 배선·현장 체크리스트/검수·시각화 갤러리·승인 흐름·오프라인·푸시 알림·i18n·앱 아이콘/스플래시·EAS 빌드·APK·QR 배포를 kintex-mobile-dev + designer + kintex-backend-dev + kintex-qa + kintex-devops-dev 팀으로 조율한다. "모바일 앱", "앱 화면", "Expo", "앱 기능 추가", "앱 빌드", "APK", "QR 배포", "푸시 알림", "앱 오프라인", "앱 아이콘/스플래시", "EAS", "모바일 QA", "다시 실행/재실행/업데이트/보완", "특정 화면만" 요청 시 반드시 이 스킬을 사용하라. (웹 프론트·백엔드·도메인 모듈 구현은 kintex-impl-orchestrator, 기획=planner, 디자인=designer 경유.)
|
||||
---
|
||||
|
||||
# 킨텍스 자동전시시스템 — 모바일 앱 트랙 오케스트레이터
|
||||
|
||||
`mobile/`(Expo) 트랙을 에이전트 팀으로 조율한다. **실행 모드: 에이전트 팀**. 모든 Agent 호출은 `model: "opus"`. 근거 문서: `docs/design.md` §4(모바일 화면)·`mobile/README.md`(스택·화면 표·남은 작업)·`docs/PLANNING.md` §2-1(역할별 웹/모바일 분리).
|
||||
|
||||
**구현 레퍼런스 = WISE 모바일** `C:\GUARDiA\workspace\guardia-messenger\app\uiws\`(읽기 전용): uiwsApi.ts API 클라이언트 패턴(봉투 언랩·degraded 폴백)·(auth) 2FA 로그인·60+ 화면 컨벤션. 신규 화면은 대응 uiws 화면 구조를 먼저 차용한다. **디자인 = Stitch 경유**: designer가 design.md SCR-M* 스펙+Stitch 프롬프트 작성 → Stitch 생성(`mcp__stitch__*` 도구 또는 기존 프로젝트 9385904003821333054) → mobile-dev가 React Native로 이식.
|
||||
|
||||
**앱 타깃 2개 (소유자 확정 2026-07-11):** 코드베이스는 `mobile/` 하나, 배포 타깃은 ①운영 앱(B2B, 2FA 필수, 사내 QR/APK) ②관람객 앱(B2C, 스토어 공개, 간편가입·게스트, 2FA 미강제). eas 프로파일/역할 라우팅으로 분리. 관람객 1차 접점은 공개 웹(SCR-P7/P8), 앱은 리텐션 채널.
|
||||
|
||||
## 경계 (중복 회피)
|
||||
- 웹 프론트(`src/frontend`)·백엔드 모듈·도메인(M2~M18) 구현 = `kintex-impl-orchestrator`
|
||||
- 이 하네스 = **mobile/ 앱 전용**: 화면·API 배선·NFR·에셋·빌드/배포. 모바일이 요구하는 백엔드 API 확장은 kintex-backend-dev를 이 팀에 합류시켜 처리(계약 단일 출처 유지)
|
||||
|
||||
## 에이전트 로스터
|
||||
| 에이전트 | 역할 | 비고 |
|
||||
|---|---|---|
|
||||
| kintex-mobile-dev | Expo 화면·API 배선·NFR·에셋·eas.json (핵심 구현) | 신규 전담 |
|
||||
| designer | design.md §4 모바일 화면 스펙(SCR-M*) 신설·갱신 | 화면 신설 시만 합류 |
|
||||
| kintex-backend-dev | 모바일이 요구하는 API 확장(부스 목록·검수 제출 등 M6/C-4) | 계약 공백 시만 합류 |
|
||||
| kintex-qa | 경계면 교차 QA(shape·워터마크·시크릿·typecheck) | 각 화면 완성 직후 점진 |
|
||||
| kintex-devops-dev | EAS 실빌드·APK·QR 배포 파이프라인 | Phase 3, G3 게이트 후 |
|
||||
| reviewer | design.md↔구현 정합 교차 검증 | 트랙 마감 시 |
|
||||
|
||||
## Phase 0: 컨텍스트 확인·게이트
|
||||
1. `mobile/` 존재(스캐폴드 완료) + `_workspace/` 확인 → **초기/부분 재실행/새 실행** 판별:
|
||||
- 부분 수정 요청("검수 화면만") → 해당 화면만 kintex-mobile-dev 재호출 → qa
|
||||
- 새 기능 요청 → Phase 1부터. 기존 `_workspace/`는 `_workspace_prev/`로 이동
|
||||
2. `mobile/README.md` "남은 작업"과 `docs/UNDEVELOPED_BACKLOG.md`를 백로그 소스로 삼는다
|
||||
3. **게이트 G3(EAS 실빌드)**: EAS는 외부 클라우드 빌드 서비스 — 실빌드·스토어/QR 배포 전 소유자 승인 필요. 미승인 시 eas.json·에셋 준비까지만(코드/구조는 게이트와 무관하게 진행)
|
||||
|
||||
## Phase 1: 디자인 (Stitch 경유, 조건부)
|
||||
신규 화면이 design.md §4에 없으면: ① designer가 SCR-M* 스펙 + Stitch 프롬프트를 design.md에 추가(직접 수정 금지 — 반드시 designer 경유) → ② Stitch로 화면 생성(`mcp__stitch__generate_screen_from_text`/`edit_screens`, 기존 프로젝트 재사용) → ③ 생성 결과를 `stitch_kintex_ai_system_architect/`에 확보 → mobile-dev 이식 입력으로 연결. 기존 화면 수정이면 건너뜀.
|
||||
|
||||
## Phase 2: 구현 (팀 병렬)
|
||||
TeamCreate 후 spawn: kintex-mobile-dev(필수) + kintex-backend-dev(계약 공백 시). TaskCreate로 화면 단위 작업 생성, `blockedBy`로 API 계약→화면 배선 의존성 연결.
|
||||
- kintex-backend-dev: 모바일 필요 API 계약을 `_workspace/{phase}_backend_contract.md`에 먼저 산출 → mobile-dev가 소비
|
||||
- kintex-mobile-dev: 화면/기능 구현 + `npm run typecheck` 통과 → 완성 통지를 kintex-qa에 SendMessage
|
||||
|
||||
## Phase 3: 점진 QA → 빌드·배포
|
||||
- kintex-qa: 화면 완성 직후 경계면 교차 비교(API shape↔`lib/types.ts`↔화면 소비), AiImage 워터마크 강제, 시크릿/자격증명 미노출, typecheck 회귀. 실패 시 mobile-dev에 반려(통과까지)
|
||||
- kintex-devops-dev(G3 승인 후): EAS 빌드 → APK → QR 배포 랜딩(GUARDiA ITSM 중앙 APK 저장소 패턴 참조 가능). 커밋·push는 `mobile/node_modules` 제외 경로 지정 add(리포 대용량 함정)
|
||||
- reviewer: 트랙 마감 시 design.md §4↔`mobile/README.md` 화면 표↔실구현 3자 정합 검증
|
||||
|
||||
## 데이터 전달 / 에러
|
||||
태스크(조율) + 파일(`_workspace/{phase}_{agent}_{artifact}`) + 메시지(실시간). 1회 재시도 후 재실패는 누락 명시하고 진행, 상충 계약은 backend 계약 문서를 권위로. QA 실패는 통과까지 반려. 최종물 `mobile/`, 중간물 `_workspace/` 보존.
|
||||
|
||||
## 보안 불변
|
||||
AI 이미지 워터마크·고지 강제(AiImage 경유만). 시크릿·자격증명·서버 IP 코드/커밋 미기재(API 베이스는 env/app.json extra만). 비밀번호 저장 금지. RBAC 화면 가드. 외부 API 금지(예외: Claude·나노바나나 Gemini 기승인, EAS는 G3 승인 필요).
|
||||
|
||||
## 테스트 시나리오
|
||||
- **정상**: "모바일 앱에 부스 목록 화면 추가" → Phase0(백로그 확인) → Phase1(designer SCR-M3 스펙) → Phase2(backend 계약→mobile-dev 구현·typecheck) → Phase3(qa 경계면 통과) → 커밋.
|
||||
- **부분 재실행**: "검수 화면 반려 사유 입력만 보완" → Phase0 부분 판별 → kintex-mobile-dev 단독 수정 → qa 해당 화면만 재검.
|
||||
- **에러**: G3 미승인 상태 "APK 빌드해서 QR 배포" → eas.json·에셋 준비까지 완료 + 승인 요청 에스컬레이션(빌드 실행 보류 명시).
|
||||
|
||||
## 후속 작업
|
||||
description의 후속 키워드(다시 실행·업데이트·보완·특정 화면만)로 재트리거. Phase 0이 초기/새/부분을 판별하고, 각 에이전트는 이전 산출물이 있으면 개선점만 반영한다.
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
@ -14,6 +14,13 @@ output/
|
||||
Thumbs.db
|
||||
.DS_Store
|
||||
|
||||
# JVM crash logs
|
||||
hs_err_pid*.log
|
||||
replay_pid*.log
|
||||
|
||||
# 에이전트 세션 중간 산출물 (로컬 보존, git 제외 — 최종물은 docs/·src/·deliverables/)
|
||||
_workspace/
|
||||
|
||||
# Stitch export — 대용량 스크린샷 PNG 제외(로컬 참조 유지), code.html·DESIGN.md만 버전관리
|
||||
stitch_kintex_ai_system_architect/*/screen.png
|
||||
|
||||
|
||||
18
CLAUDE.md
18
CLAUDE.md
@ -49,6 +49,7 @@ GUARDiA 표준 프레임워크(Spring Boot 3.5 + React 19 + MyBatis + PostgreSQL
|
||||
|
||||
- 모든 문서는 한국어로 작성한다. 코드 식별자/커밋 메시지는 영어.
|
||||
- 기획/디자인 문서를 수정할 때는 반드시 해당 에이전트를 통해 수정한다 (직접 수정 금지).
|
||||
- **화면 디자인은 항상 Google Stitch에 의뢰한다** (웹·모바일·관리자·공개사이트 공통): designer가 design.md에 화면 스펙+영어 Stitch 프롬프트 작성 → Stitch 생성(`mcp__stitch__*`, 프로젝트 9385904003821333054, 디자인 시스템 "Precision Enterprise AI") → 생성 HTML을 `stitch_kintex_ai_system_architect/`에 확보 → frontend/mobile dev가 이식. 손그림 UI·Stitch 미경유 신규 화면 금지.
|
||||
- 나노바나나 호출은 `tools/nanobanana/` 모듈만 사용한다. API 키는 환경변수 `GEMINI_API_KEY`.
|
||||
- 파일 삭제 전에는 반드시 사용자에게 확인받는다.
|
||||
- 도메인 용어: 부스(booth), 장치공사(booth construction), 반입/반출(move-in/move-out), 주최자(organizer), 참가업체(exhibitor), 관람객(visitor).
|
||||
@ -80,3 +81,20 @@ GUARDiA 표준 프레임워크(Spring Boot 3.5 + React 19 + MyBatis + PostgreSQL
|
||||
|------|----------|------|------|
|
||||
| 2026-07-11 | 구현 하네스 초기 구성 — 전문 에이전트 6종 + kintex-impl-orchestrator + 구현 백로그 | 전체 | 범용 에이전트만 존재 → 스택 확정 후 실제 구현 조율 팀 구성 |
|
||||
| 2026-07-11 | **v2.0 재구성** — 자동전시시스템 확장(PLANNING v2.0). 신규 에이전트 11종(아키텍트 5·공통 1·도메인 5·AI 1) + 오케스트레이터·백로그 v2.0(Phase A 아키텍처→B 공통레이어→C 부스코어→D 도메인→E 배포) | 전체 | 스코프 확장(전시 전기능·경영분석·CMS·관리자·역할분리·공사 옥션·WISE 공통레이어·AI Claude 전환) |
|
||||
| 2026-07-11 | WISE 웹 기능 레퍼런스 강화 — kintex-frontend-dev에 uiws frontend(pages 공통 업무기능) 차용 원칙, 오케스트레이터에 웹 기능 화면 WISE 패턴·Stitch 디자인 경유·모바일 경계 명시 + 로스터에 kintex-mobile-dev(경계) 추가 | kintex-frontend-dev·kintex-impl-orchestrator | "웹도 wise 기능 참고" 요청 |
|
||||
|
||||
---
|
||||
|
||||
## 하네스: 모바일 앱 (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 이식).
|
||||
|
||||
**트리거:** 모바일 앱, 앱 화면, Expo, 앱 기능 추가, 앱 빌드, APK, QR 배포, 푸시 알림, 앱 오프라인, 앱 아이콘/스플래시, EAS, 모바일 QA, 다시 실행, 특정 화면만 요청 시 `kintex-mobile-orchestrator` 스킬을 사용하라. (웹·백엔드·도메인 모듈은 `kintex-impl-orchestrator`.)
|
||||
|
||||
**게이트 G3:** EAS 실빌드·배포는 외부 클라우드 빌드 — 소유자 승인 후 실행(승인 전엔 eas.json·에셋 준비까지만).
|
||||
|
||||
**변경 이력:**
|
||||
| 날짜 | 변경 내용 | 대상 | 사유 |
|
||||
|------|----------|------|------|
|
||||
| 2026-07-11 | 초기 구성 — kintex-mobile-dev 신규 + 오케스트레이터(재사용: designer·backend-dev·qa·devops-dev·reviewer). WISE 모바일 레퍼런스·Stitch 디자인 경유 반영 | 전체 | 모바일 앱 트랙 전담 하네스 부재("하네스 생성" + "wise 모바일 참고" + "디자인은 스티치" 요청) |
|
||||
| 2026-07-11 | **앱 타깃 2개 확정 반영** — 코드베이스 1(mobile/) + 배포 타깃 2(①운영 B2B: 2FA 필수·사내 QR ②관람객 B2C: 스토어 공개·간편가입·게스트). 계정은 단일 통합+가입 트랙 분리(PLANNING v3.1) | kintex-mobile-dev·오케스트레이터 | 소유자 확정 — 회원가입 관람객 포함 질의 → 하이브리드(계정 통합·앱 분리) 채택 |
|
||||
|
||||
BIN
ci/CI_AI/CI_01.ai
Normal file
BIN
ci/CI_AI/CI_01.ai
Normal file
Binary file not shown.
BIN
ci/CI_AI/CI_02.ai
Normal file
BIN
ci/CI_AI/CI_02.ai
Normal file
Binary file not shown.
BIN
ci/CI_AI/CI_03.ai
Normal file
BIN
ci/CI_AI/CI_03.ai
Normal file
Binary file not shown.
874
ci/CI_AI/CI_04.ai
Normal file
874
ci/CI_AI/CI_04.ai
Normal file
File diff suppressed because one or more lines are too long
577
ci/CI_AI/CI_05.ai
Normal file
577
ci/CI_AI/CI_05.ai
Normal file
File diff suppressed because one or more lines are too long
BIN
ci/CI_JPG/CI_01.jpg
Normal file
BIN
ci/CI_JPG/CI_01.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
BIN
ci/CI_JPG/CI_02.jpg
Normal file
BIN
ci/CI_JPG/CI_02.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
BIN
ci/CI_JPG/CI_03.jpg
Normal file
BIN
ci/CI_JPG/CI_03.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
BIN
ci/CI_JPG/CI_04.jpg
Normal file
BIN
ci/CI_JPG/CI_04.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 44 KiB |
BIN
ci/CI_JPG/CI_05.jpg
Normal file
BIN
ci/CI_JPG/CI_05.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 71 KiB |
@ -60,7 +60,41 @@
|
||||
| E-M14 | 현장운영(혼잡·안전·주차·에너지) | BE·FE | 대시보드·경보 |
|
||||
| E-DEP | 역할별 프론트 번들 + Spring jar + 워커 서비스 빌드·Gitea CI/CD·systemd·env | DEV | (G2 후)push→배포 |
|
||||
|
||||
## Phase F — 벤치마킹 유래(티켓·관람객) 추가 항목
|
||||
> 근거: `docs/analysis/ticketing-app-benchmark.md`(2026-07-11, COEX·NOL/멜론/티켓링크·킨텍스 앱·Whova/Eventbrite 벤치마킹). ⓑ=P1 보강(기존 화면 스펙 수정 후보), ⓒ=P2 신규(신규 화면/기능). **PLANNING.md·design.md 직접 수정 금지 — planner/designer 경유 반영.** 관련 화면 표기는 현행 design.md v2.1 기준.
|
||||
|
||||
### ⓑ 보강 (P1) — 기존 화면 스펙 수정 후보
|
||||
| ID | 작업 | 대상 모듈 | 관련 화면 | 출처 | 담당 | 완료 기준 |
|
||||
|---|---|---|---|---|---|---|
|
||||
| F-B1 | 스마트티켓 부정입장 방지 — 디바이스 바인딩/회전(애니메이션) QR + 본인인증(캡처본 무효·양도 방지, 현행 정적 QR 대체) | M10 | SCR-M15·31 | 벤치 §2②·§3 B1 | VIS·BE·kintex-mobile-dev | 회전 토큰·기기 인식 체크인 왕복 |
|
||||
| F-B2 | 취소·환불 규정 정교화 — 3구간(100/50/0%)→다구간 취소수수료 + 예매수수료 별도 환불 규정, 서버 권위 | M9·M10 | SCR-P8 | 벤치 §2④·§3 B2 | VIS·BE | 다구간 예상환불액 서버 산출·표시 |
|
||||
| F-B3 | 티켓·배지·관람객 앱 다국어(한/영/중/일) — 공개사이트(M12) 외 티켓·배지·지갑 화면 확장 | M10·M12 | SCR-P7·M5·M15 | 벤치 §2⑧·§3 B3 | VIS·CMS·FE | 4개 언어 티켓·배지 렌더 |
|
||||
| F-B4 | 세션 정원 기반 사전등록(RSVP) — 즐겨찾기에 정원·마감·대기 상태 추가 | M11 | SCR-M9 | 벤치 §2①·§3 B4 | VIS·AI·BE | 정원 소진·마감 상태 처리 |
|
||||
| F-B5 | 결제수단 사전등록·간편결제 빠른결제 | M9 | SCR-P7·M14 | 벤치 §2⑤·§3 B5 | VIS·BE | 저장 결제수단 빠른결제 왕복 |
|
||||
| F-B6 | 행사 라이브 공지 피드/현장 푸시 — 알림센터에 더해 행사별 라이브 공지(긴급·프로그램 변경) | §5B notification·M12 | SCR-M11·M5 | 벤치 §2⑤·§3 B6 | VIS·COM | 라이브 공지 발행·푸시 수신 |
|
||||
|
||||
### ⓒ 신규 (P2) — 신규 화면/기능 후보
|
||||
| ID | 작업 | 대상 모듈 | 관련 화면 | 출처 | 담당 | 완료 기준 |
|
||||
|---|---|---|---|---|---|---|
|
||||
| F-C1 | 티켓 오픈 알림·관심 행사 구독 — 오픈 예정 행사 구독→오픈 시 푸시/EDM | M10·M12 | 신규(공개사이트·앱) | 벤치 §2⑤·§3 C1 | VIS·CMS | 구독→오픈 알림 발송 |
|
||||
| F-C2 | 관람객 앱 주차 연계 — iparking 실시간 현황·주차비 결제·사전 주차권 | M14·M10 | 신규 SCR-M(주차) | 벤치 §2⑥·§3 C2 | VIS·kintex-mobile-dev·BE | 주차 현황 조회·결제 연계 |
|
||||
| F-C3 | 관람객 대면 혼잡/대기 안내 — 입장·주차·인기 세션 실시간 혼잡도(현행 M14는 홀매니저용) | M14 | 신규 위젯(SCR-M5) | 벤치 §2⑥·§3 C3 | VIS·BE | 혼잡도 위젯 표시 |
|
||||
| F-C4 | 관람객↔관람객 QR 명함 교환·컨택트 지갑 — 리드캡처(업체→관람객) 외 관람객 상호 교환 | M11 | SCR-M8 확장 | 벤치 §2②·§3 C4 | VIS | QR 교환·컨택트 저장 |
|
||||
| F-C5 | 인기 세션/바이어 미팅 예매 대기열·취소표 알림 — 정원 초과 대기 우선권 | M11 | SCR-M9·M8 | 벤치 §2⑤·§3 C5 | VIS·AI | 대기 등록·취소표 알림 |
|
||||
| F-C6 | 관람객 멤버십/등급 — 일반/바이어/VIP·재방문·바이어 인증 등급별 혜택·매칭 우선순위 | M10 | 신규 마이 | 벤치 §2⑦·§3 C6 | VIS·ADM | 등급 산정·혜택 차등 |
|
||||
| F-C7 | 최근 본 행사·부스 개인화 홈 피드 — 재방문 전환 | M10·M12 | SCR-M5 확장 | 벤치 §2⑤·§3 C7 | VIS | 최근 본 이력·피드 |
|
||||
| F-C8 | 셀프 체크인 키오스크·즉석 배지 인쇄 화면 — PLANNING M10 즉석배지 화면화 | M10 | 신규 SCR | 벤치 §2③·§3 C8 | VIS·FE | 키오스크 체크인·배지 인쇄 |
|
||||
|
||||
## 진행 규칙
|
||||
- **Phase B(공통 레이어)는 전 도메인 선행 기반** — C/D 착수 전 인증·시스템관리·공통기능 정착.
|
||||
- 각 모듈 완성 직후 QA 점진 검증. 보안 불변(자격증명·PII·스택트레이스 미노출·AI 워터마크·등록업체 응찰·admin env) 반려 사유.
|
||||
- 기획=planner, 디자인=designer 경유. 아키텍처 표준(Phase A) 위반은 시정.
|
||||
- **Phase F는 벤치마킹 유래 추가 백로그**(P1 보강 6·P2 신규 8) — 기존 SCR-P7/P8·M14/M15 등 화면 반영은 designer 경유, 모듈 반영은 planner 경유. 착수 우선순위는 벤치 §3 톱5(F-B1·F-B2·F-C1·F-C2·F-B3).
|
||||
|
||||
---
|
||||
|
||||
## 변경 이력
|
||||
| 날짜 | 변경 내용 |
|
||||
|---|---|
|
||||
| 2026-07-11 | 백로그 v2.0 최초 재작성 |
|
||||
| 2026-07-11 | Phase F 신설 — 티켓·관람객 벤치마킹 유래 항목 14개(ⓑ 보강 6 F-B1~B6 · ⓒ 신규 8 F-C1~C8) 추가. 근거 `docs/analysis/ticketing-app-benchmark.md`. 기존 항목 무수정 |
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
# 킨텍스 자동전시시스템 (Exhibition Automation Platform) 기획서
|
||||
|
||||
> 작성: 기획 에이전트(planner) · 작성일: 2026-07-11 · 버전: **v3.0**
|
||||
> 작성: 기획 에이전트(planner) · 작성일: 2026-07-11 · 버전: **v3.1**
|
||||
> 근거 문서: `docs/analysis/kintex-website.md` (킨텍스 웹사이트 25개 페이지 + 전시주최자매뉴얼 PDF + 참가업체 매뉴얼 PDF 전수 분석, 2026-07-11)
|
||||
> 근거 문서: `docs/analysis/reroomai-source.md` (ReRoomAI 이미지 파이프라인 소스 분석, 2026-07-11) — 6장 나노바나나 파이프라인 제어 파라미터 확정 근거
|
||||
> 근거(글로벌 벤치마크, 2026-07-11 크롤링): Eventleaf·VenueSight·Whova·ExpoPlatform(전시관리·부스판매·리드캡처), Eventbase·Pointr·Swapcard·Brella·Grip(wayfinding·비즈매칭), RainFocus·Bizzabo(등록·리벤뉴), ExhibitForce·Ways·FinancialModelsLab·Digitevent(전시 BI/KPI), FindRFP·Procore·4castplus(RFQ/Tender/역경매). 각 §5 모듈에 매핑.
|
||||
@ -142,20 +142,44 @@
|
||||
|
||||
### 2-1. 역할별 웹/모바일 포털 분리 (IA·채널 매트릭스)
|
||||
|
||||
각 역할은 목적이 다르므로 **별도 프론트 앱(도메인/서브패스 분리)**으로 제공하되, 공유 Spring Boot 백엔드 + SSO + 역할 RBAC 위에 얹는다. 데스크톱(설계·에디터·대시보드)과 모바일(현장·조회·승인)의 용도를 명확히 나눈다.
|
||||
각 역할은 목적이 다르므로 **별도 프론트 앱(도메인/서브패스 분리)**으로 제공하되, 공유 Spring Boot 백엔드 + SSO + 역할 RBAC 위에 얹는다. 데스크톱(설계·에디터·대시보드)과 모바일(현장·조회·승인)의 용도를 명확히 나눈다. **계정 체계·모바일 앱 채널은 §2-1-1(소유자 확정 2026-07-11)** 을 따른다.
|
||||
|
||||
| 역할 | 웹 포털 (데스크톱 주력) | 모바일 (현장/조회 주력) | 주 사용 모듈 | 인증/권한 |
|
||||
|---|---|---|---|---|
|
||||
| **주최자** | 주최자 콘솔 `organizer.` — 홀 배정·배치·행사 대시보드·옥션 발주·BI | 조회·승인·현장 상황판 | M1·M2·M6·M15·M16·M12 | Event owner |
|
||||
| **참가업체** | 참가업체 포털 `exhibitor.` — 부스 신청·설계·유틸리티·옥션 발주 | 현장 체크인·**리드캡처(배지 스캔)**·승인 | M3·M4·M5·M10·M11·M15·M9 | Event member(부스 단위) |
|
||||
| **장치/공사업체** | 업체 포털 `contractor.` — 도면 제출·**AI 설계자료 열람·옥션 응찰(견적서 제출)** | 현장 시공·반입 통행증·안전 체크 | M3·M4·M15·M8·M7 | 등록업체 검증 계정 |
|
||||
| **킨텍스 직원(홀매니저·운영)** | 운영 대시보드 `ops.` — 검수·규정 플래그·홀 부하·물류·현장운영 | 현장 검수·안전·혼잡 모니터 | M2·M6·M8·M14·M16 | 내부 계정(행사 전체 열람·승인) |
|
||||
| **테넌트 관리자** | 백오피스 `admin.`(테넌트 컨텍스트 고정) — 자기 전시관 사용자·RBAC·감사로그·마스터데이터·룰셋·시스템설정 | (없음, 웹 전용) | **M18** | 테넌트(전시관) 관리자(단일 테넌트 스코프) |
|
||||
| **플랫폼 슈퍼관리자** | 백오피스 `admin.`(테넌트 스위처) — 테넌트 마스터·온보딩·전역 코드·크로스-테넌트 감사·통합 BI | (없음, 웹 전용) | **M18 + 테넌트 관리** | 플랫폼 슈퍼관리자(크로스-테넌트, §1A-4) |
|
||||
| **관람객/일반 대중** | 공개 홍보 사이트 `www/expo.` (SEO·다국어) + 관람객 사전등록 | 관람객 앱 — 배지/QR·티켓·wayfinding·비즈매칭·플로어플랜 | M12·M10·M11·M13·M17 | 공개/셀프서비스(쓰기 제한) |
|
||||
| 역할 | 웹 포털 (데스크톱 주력) | 모바일 채널 (앱 타깃) | 가입 트랙 | 주 사용 모듈 | 인증/권한 |
|
||||
|---|---|---|---|---|---|
|
||||
| **주최자** | 주최자 콘솔 `organizer.` — 홀 배정·배치·행사 대시보드·옥션 발주·BI | **운영 앱(B2B)** — 조회·승인·현장 상황판 | 승인/초대 + **2FA(OTP) 필수** | M1·M2·M6·M15·M16·M12 | Event owner |
|
||||
| **참가업체** | 참가업체 포털 `exhibitor.` — 부스 신청·설계·유틸리티·옥션 발주 | **운영 앱(B2B)** — 현장 체크인·**리드캡처(배지 스캔)**·승인 | 승인/초대 + **2FA(OTP) 필수** | M3·M4·M5·M10·M11·M15·M9 | Event member(부스 단위) |
|
||||
| **장치/공사업체** | 업체 포털 `contractor.` — 도면 제출·**AI 설계자료 열람·옥션 응찰(견적서 제출)** | **운영 앱(B2B)** — 현장 시공·반입 통행증·안전 체크 | 승인/초대(등록업체 검증) + **2FA(OTP) 필수** | M3·M4·M15·M8·M7 | 등록업체 검증 계정 |
|
||||
| **킨텍스 직원(홀매니저·운영)** | 운영 대시보드 `ops.` — 검수·규정 플래그·홀 부하·물류·현장운영 | **운영 앱(B2B)** — 현장 검수·안전·혼잡 모니터 | 내부 계정 발급 + **2FA(OTP) 필수** | M2·M6·M8·M14·M16 | 내부 계정(행사 전체 열람·승인) |
|
||||
| **테넌트 관리자** | 백오피스 `admin.`(테넌트 컨텍스트 고정) — 자기 전시관 사용자·RBAC·감사로그·마스터데이터·룰셋·시스템설정 | (없음, 웹 전용) | 내부 계정 발급 + **2FA(OTP) 필수** | **M18** | 테넌트(전시관) 관리자(단일 테넌트 스코프) |
|
||||
| **플랫폼 슈퍼관리자** | 백오피스 `admin.`(테넌트 스위처) — 테넌트 마스터·온보딩·전역 코드·크로스-테넌트 감사·통합 BI | (없음, 웹 전용) | 내부 계정 발급 + **2FA(OTP) 필수** | **M18 + 테넌트 관리** | 플랫폼 슈퍼관리자(크로스-테넌트, §1A-4) |
|
||||
| **관람객/일반 대중** | 공개 홍보 사이트 `www/expo.` (SEO·다국어) + 관람객 사전등록·**게스트 예매(가입 없이 티켓 구매)** | **관람객 앱(B2C, 스토어 공개)** — 배지/QR·티켓 지갑·wayfinding·비즈매칭·플로어플랜 | **간편가입(이메일/소셜) + 게스트 예매 허용 · 2FA 미강제** | M12·M10·M11·M13·M17 | 공개/셀프서비스(쓰기 제한) |
|
||||
|
||||
- **분리 원칙**: 6개 프론트(organizer·exhibitor·contractor·ops·admin·public+visitor)는 **역할별 번들 분리**로 최소권한·공격면 축소. 공유 디자인 시스템(design.md)·공유 컴포넌트 라이브러리·공유 API 계약을 상속한다. (UI 상세·화면 목록은 designer 후속 반영 필요.)
|
||||
- **모바일**: 참가업체 리드캡처, 홀매니저 현장 검수, 업체 반입 통행증, 관람객 배지/wayfinding은 모바일 전용 최적화. 단일 앱 내 역할 스위칭 또는 역할별 별도 배포는 designer/개발 트랙에서 결정(백오피스는 모바일 미제공).
|
||||
|
||||
#### 2-1-1. 계정 체계 및 모바일 앱 채널 전략 (소유자 확정 2026-07-11)
|
||||
|
||||
> **개정 표기**: 본 소절은 v3.1에서 소유자 확정안을 신설한 것이며, 기존 §2 권한 모델·§5B-3 인증·M10을 삭제하지 않고 정밀화한다(상충 시 본 확정안 우선). 근거: `docs/analysis/ticketing-app-benchmark.md`(게스트 예매·간편가입은 티켓팅 업계 표준).
|
||||
|
||||
**(A) 계정 체계 — 단일 통합 + 가입 트랙 분리.** 계정 테이블/RBAC는 **하나(단일 통합)** 로 두고, 그 위에서 **가입 트랙(등급)만 분리**한다.
|
||||
|
||||
| 가입 트랙 | 대상 | 가입 방식 | 2FA(OTP) | 예매/조회 |
|
||||
|---|---|---|---|---|
|
||||
| **업무 트랙(B2B)** | 주최자·참가업체·공사업체·킨텍스 직원·관리자 | **승인/초대 기반 가입(SCR-49)** | **필수** | 역할 RBAC에 따름 |
|
||||
| **관람객 트랙(B2C)** | 일반 관람객·바이어 | **간편가입(이메일 또는 소셜 로그인)** | 미강제(선택) | **게스트 예매 허용**(가입 없이 티켓 구매, SCR-P7 기반영) |
|
||||
|
||||
- **계정 승격 연속성**: 관람객 → 바이어/참가업체 담당자로 승격 시 **단일 계정 위 등급 상향**으로 처리하여 **데이터 연속성(리드·비즈매칭·재방문 이력)** 을 유지한다(계정 재생성 없음). 승격 시 업무 트랙 요건(승인·2FA)이 추가 적용된다.
|
||||
- 게스트 예매자는 사후 간편가입 시 예매 이력을 계정에 병합(티켓 지갑 연속).
|
||||
|
||||
**(B) 모바일 앱 — 코드베이스 1개(Expo `mobile/`), 배포 타깃 2개.**
|
||||
|
||||
| 앱 타깃 | 대상 | 배포 채널 | 핵심 화면 |
|
||||
|---|---|---|---|
|
||||
| **① 운영 앱(B2B)** | 업무 사용자 전용 | **스토어 미공개** — 사내 QR/APK 배포 | 현장 체크리스트·검수·승인 |
|
||||
| **② 관람객 앱(B2C)** | 일반 관람객·바이어 | **스토어 공개 배포** | 간편가입·티켓 지갑·배지/QR·wayfinding·비즈매칭 (SCR-M5~M9·M14/M15 계열) |
|
||||
|
||||
- 두 타깃은 **단일 Expo 코드베이스(`mobile/`)** 에서 빌드 프로파일/엔트리로 분기한다(중복 구현 회피). 배포 채널·스토어 정책만 다르다.
|
||||
- **관람객 1차 접점은 공개 웹(SCR-P7/P8)** 이며, 관람객 앱은 **리텐션 채널**(재방문·티켓 지갑·현장 wayfinding)로 위치한다. 앱 미설치 관람객도 웹 게스트 예매로 전 여정 완결 가능.
|
||||
- 백오피스(관리자)는 모바일 미제공(웹 전용). 업무 사용자 모바일은 운영 앱으로만 제공.
|
||||
|
||||
---
|
||||
|
||||
@ -449,9 +473,11 @@ sequenceDiagram
|
||||
### M10. 관람객 등록·티켓·배지·현장체크인·리드캡처 — **P1**
|
||||
|
||||
- **현재**: 킨텍스 공식은 행사일정 검색 수준. 관람객 등록·배지·리드캡처는 행사별 주최자 사무국에 파편화(공통 플랫폼 부재).
|
||||
- **AI 자동화 후**: 온라인 사전등록(관람객/바이어 유형별 폼) → **모바일 배지/QR 발급** → 현장 QR 체크인(즉석 배지 인쇄·오프라인 대비) → 실시간 입장 집계. 참가업체 **리드캡처**: 모바일 앱으로 관람객 배지 QR 스캔 → 연락처·관심도 평점·메모 저장 → 팔로업 EDM(M12) 연계. 사전등록·체크인 데이터는 M16 BI로 흐른다.
|
||||
- **기대 효과**: 현장 등록 대기 제거, 참가업체 ROI 측정(리드 수·품질), 관람객 흐름 데이터 확보.
|
||||
- **한계**: 배지 QR·리드캡처는 개인정보(§10 R10) — 동의·보존정책 필수. 오프라인 체크인 폴백 필요(현장 네트워크 불안정).
|
||||
- **AI 자동화 후**: **간편가입(이메일 또는 소셜 로그인) 또는 게스트 예매(가입 없이 티켓 구매, SCR-P7 기반영)** → 온라인 사전등록(관람객/바이어 유형별 폼) → **모바일 배지/QR 발급** → 현장 QR 체크인(즉석 배지 인쇄·오프라인 대비) → 실시간 입장 집계. 참가업체 **리드캡처**: 운영 앱(B2B)으로 관람객 배지 QR 스캔 → 연락처·관심도 평점·메모 저장 → 팔로업 EDM(M12) 연계. 사전등록·체크인 데이터는 M16 BI로 흐른다.
|
||||
- **계정 트랙·승격 연속성(소유자 확정 2026-07-11, §2-1-1)**: 관람객은 **간편가입 트랙(2FA 미강제)** 이며 게스트 예매를 허용한다. 관람객 → **바이어/참가업체 담당자 승격** 시 **단일 계정 위 등급 상향**으로 처리해 **리드·비즈매칭·재방문 이력의 데이터 연속성**을 유지한다(계정 재생성 없음, 승격 시 업무 트랙 요건 2FA 추가). 게스트 예매 후 간편가입 시 예매 이력을 티켓 지갑에 병합.
|
||||
- **채널**: 관람객 1차 접점은 공개 웹(SCR-P7/P8), 리텐션은 **관람객 앱(B2C, 스토어 공개)** — 티켓 지갑·배지/QR·wayfinding·비즈매칭(§2-1-1 B).
|
||||
- **기대 효과**: 현장 등록 대기 제거, 게스트 예매로 가입 마찰 최소화, 참가업체 ROI 측정(리드 수·품질), 관람객 흐름 데이터 확보.
|
||||
- **한계**: 배지 QR·리드캡처는 개인정보(§10 R10) — 동의·보존정책 필수. 오프라인 체크인 폴백 필요(현장 네트워크 불안정). 게스트 예매는 최소 개인정보 수집·사후 계정 병합 정책 명시 필요.
|
||||
- **필요 데이터**: 관람객 폼 스키마, 배지 템플릿(M17 CMS), 부스-참가업체 매핑(M2).
|
||||
- **연동**: M11 비즈매칭, M12 마케팅, M13 wayfinding, M16 BI.
|
||||
- **근거**: Eventleaf·VenueSight·Whova·WebMobi(자가등록·QR 체크인·즉석 배지·리드 리트리벌 앱·관심도 평점·메모).
|
||||
@ -587,7 +613,7 @@ UIWS 표준 공통 모듈을 이식해 전 포털이 공유. 킨텍스 도메인
|
||||
kintex 인증은 §2 행사 단위 RBAC를 **UIWS 표준 인증 위에** 구현한다(재설계 금지).
|
||||
|
||||
- **JWT + RBAC**: 발급·역할 게이트. §8-1 SSO(플랫폼+행사 이중 권한)와 결합.
|
||||
- **2차 인증 OTP(TOTP RFC6238)**: SHA1·30s·6자리·±1 윈도우. `TotpService` 이식 — 로그인 2단계(비번→OTP), 최초 QR 등록, 마이페이지 재설정/해제, 관리자 OTP 초기화(`otp_secret=NULL`). 대상: 관리자·홀매니저·주최자·업체(내부/발주 권한 계정) 필수, 관람객 셀프서비스는 선택.
|
||||
- **2차 인증 OTP(TOTP RFC6238)**: SHA1·30s·6자리·±1 윈도우. `TotpService` 이식 — 로그인 2단계(비번→OTP), 최초 QR 등록, 마이페이지 재설정/해제, 관리자 OTP 초기화(`otp_secret=NULL`). **대상(소유자 확정 2026-07-11, §2-1-1 정합)**: **업무 사용자(주최자·참가업체·공사업체·킨텍스 직원·플랫폼/테넌트 관리자) = 필수**, **일반 관람객 셀프서비스 = 미강제(선택)**. 관람객이 바이어/참가업체 담당자로 승격(§2-1-1 A)하면 그 시점부터 업무 트랙 요건으로 2FA가 필수 전환된다.
|
||||
- **로그인 실패 잠금** + 관리자 해제.
|
||||
- **admin 비밀번호**: env `ADMIN_PASSWORD_ENC`(AES-256-GCM) + `ADMIN_KEY_FILE`(별도 키파일 root 600) 복호 → 기동 시 BCrypt 재시드. **하드코딩 `admin123` 시드 금지**(§10 보안).
|
||||
- **스키마**: UIWS `TB_USER`·OTP 시크릿 컬럼·`TB_AUDIT_LOG` 이식(멱등 DDL, sql.init `mode=always`+continue-on-error).
|
||||
@ -991,3 +1017,4 @@ graph TB
|
||||
| v1.2 | 2026-07-11 | planner | **기술 스택 확정 — React + Spring Boot 3.x(Java 17) + MyBatis + PostgreSQL(PostGIS), 나노바나나 Python 워커 사이드카. GUARDiA 표준 프레임워크 정렬(사용자 지정).** §8 아키텍처 전면 정합화: 백엔드 FastAPI→Spring Boot 3.x + MyBatis(REST + WebSocket/STOMP, 룰·배치/배선 엔진=서비스 계층 + PostGIS 공간 SQL), 프론트 Next.js→React 18/19(Vite·TS, SVG/WebGL 캔버스), 비동기 큐 Redis 유지 + 이미지 생성은 별도 **Python 워커 사이드카(`tools/nanobanana`, google-genai Python SDK)** 로 분리(Spring이 RenderJob 큐잉→Python 워커 소비·생성→오브젝트 스토리지 적재→WebSocket 완료 푸시, 서류·알림도 동일 큐). Python 워커 유지 근거 명시(ReRoomAI 검증 client.py·Python SDK — Java 재구현 회피). 인증(행사 단위 RBAC+JWT)·오브젝트 스토리지 유지. §8 mermaid 갱신. **기능 범위(M1~M9)·우선순위·나노바나나 파이프라인 로직 불변** — 스택 표기 정합화만 수행 |
|
||||
| **v2.0** | 2026-07-11 | planner | **정체성 확장 — 킨텍스 자동전시시스템(Exhibition Automation Platform).** 글로벌 전시테크 크롤링(Eventleaf·VenueSight·Whova·ExpoPlatform·Eventbase·Pointr·Swapcard·Brella·Grip·RainFocus·ExhibitForce·FindRFP·Procore·4castplus) 근거로 확장. ①§1 비전 재정의(생애주기 폐루프, M15 옥션이 코어를 발주로 연결) ②§2 역할·포털 매트릭스 신설 — 6역할(주최자·참가·업체·홀매니저·관리자·관람객/대중) 웹/모바일 분리(organizer·exhibitor·contractor·ops·admin·public+visitor), 관리자·일반대중 페르소나 추가 ③§4 모듈맵 mermaid 확장(M10~M18) + 우선순위 총괄 ④§5A 신규 모듈 상세: **M15 공사/장치 옥션(P1·핵심, 역경매·견적서(Quotation)=응찰·실시간 순위·종합평가 낙찰·등록업체만 응찰·Auction 1─N Quotation ─ Award·PDF/버전, 입찰 플로우 시퀀스 다이어그램)**, M10 관람객 등록·배지·리드캡처(P1), M12 마케팅·EDM·공개 홍보 사이트(P1·SEO/다국어), M16 경영분석 BI(P2→**P1 승격**, design.md 반영은 designer 후속), M17 CMS(P1), M18 관리자 백오피스(P1), M11 비즈매칭·M13 wayfinding·M14 현장운영(P2) ⑤**M2·M3 3안 생성→선택/병합 UX 구체화**(1·2·3안 다양화, 구역/블록 병합, 병합 후 규정 재검증, 버전 기록) ⑥§7-3 엔티티 확장(Auction/Quotation/Award·Visitor/Badge/Lead·KPI/Content/User) ⑦§8-1 아키텍처 보강(역할별 분리 프론트+SSO/RBAC+공개사이트 SEO/다국어+백오피스+옥션 엔진) ⑧**§5B 공통/시스템관리 레이어(UIWS 표준 이식) 신설** — kintex 스택=UIWS(GUARDiA 표준 프레임워크) 동일 → 시스템관리(사용자·RBAC·공통코드·메뉴·감사로그·시스템설정)와 공통 업무기능(worklog·schedule·message·stats·notice·opinion·search·meeting·report·notification·audit) + 인증(JWT + **2차 인증 OTP TOTP RFC6238** + 로그인 실패 잠금 + admin 비번 env `ADMIN_PASSWORD_ENC` 주입)을 `workspace/uiws` 레퍼런스로 이식, 도메인 모듈은 그 위에 적재. **M18 관리자 시스템을 UIWS 시스템관리와 통합(중복 제거)**, BI/CMS/알림 경계 규칙 정의, Phase 1 선행 기반으로 로드맵 반영. ⑨**§5A M16-1 운영사(킨텍스·venue operator) 관점 수익성/ROI 소절 신설** — 참가업체 관점 ROI(리드 기반)와 명시 구분하고, 킨텍스 관점 7지표(홀·기간별 가동률/매출구성/행사별 P&L·마진/전시장별 ROI·RevPAD·㎡당 수익/참가사 리텐션·LTV/수요예측·수율·가격 최적화/경영진 KPI) + 데이터원(M1·M4·M9·M10·M15) + **BI 데이터마트(스타 스키마 Fact/Dim, DA 후속 트랙)** 정의. **기존 M2~M5 P0 코어·§6 나노바나나 로직·§8 확정 스택 보존.** design.md/타 문서는 미수정 — designer/planner/DA 후속 반영 필요로 표기 |
|
||||
| **v3.0** | 2026-07-11 | planner | **멀티테넌시(다중 전시관 SaaS) 확장 — 킨텍스 전용 → 다중 전시관, KINTEX=기준 테넌트 #1.** 기존 스코프(부스 코어 M2~M5·도메인 M10~M18·§5B 공통 레이어·§6 나노바나나·§8 확정 스택) **전부 보존**, 격리 레이어만 순증. ①헤더 v3.0 정체성 확장(다중 전시관 SaaS) ②**§1A 멀티테넌시 절 신설** — 테넌트=전시관(Venue) 모델(tenant_code·명칭·branding·domain/subdomain·active·locale, 홀·요율·규정 룰셋·부스표준을 테넌트 소유로 재정의)·`tenant_id` 전파 및 격리(전 도메인 엔티티 격리 vs 전역/테넌트 참조 구분)·테넌트 컨텍스트 해소(서브도메인 kintex/coex.wise.ai.kr 우선 + 사용자 소속 보조, fail-closed 주입)·역할 계층(플랫폼 슈퍼관리자 vs 테넌트 관리자 2계층, 행사 RBAC는 테넌트 내부 스코프)·마이그레이션(기존 데이터 tenant_id=1 백필, DDL은 db-engineer/DA 후속) ③**§2 역할 갱신** — 페르소나 관리자 행을 테넌트 관리자로 재정의 + 플랫폼 슈퍼관리자 신규, 권한 모델을 "테넌트 격리 ⊃ 행사 RBAC" 3중 구조로, §2-1 포털 매트릭스 admin 2계층 분리 ④**§7 데이터모델 갱신** — §7-1 마스터데이터 테넌트 스코프 재정의(킨텍스 실측=테넌트#1 시드), §7-3 Tenant/Venue 엔티티·tenant_id 전파·참조 구분·역할 계층·백필 추가 ⑤**§8-2 멀티테넌트 격리 전략 신설** — 공유 스키마 + tenant_id 컬럼(단일 kintex_db 유지·DB/스키마 분리 미채택, 비교표)·fail-closed 다층 강제(필터→서비스가드→MyBatis 공통 인터셉터→PostGIS)·성능(`(tenant_id,…)` 선두 인덱스)·보안(교차 차단·감사·스토리지/쿼터 격리)·온보딩 6단(코드 배포 없이 데이터 온보딩) ⑥§1 Non-Goal에 온보딩 범위 한정 추가. **design.md·architecture/*.md·src 미수정** — db-engineer/DA/designer 후속 반영 필요로 표기. 근거 없는 코엑스 실측 추정 배제(온보딩 시 입력) |
|
||||
| **v3.1** | 2026-07-11 | planner | **계정 통합 + 가입 트랙 분리 · 모바일 2타깃 확정(소유자 확정 2026-07-11).** 기존 스코프 전부 보존, 계정·앱 채널 전략만 정밀화(상충 시 확정안 우선·삭제 없이 개정 표기). ①**§2-1 채널 매트릭스 개정** — 모바일 열을 운영 앱(B2B)/관람객 앱(B2C)으로 분리, 가입 트랙 열 신설(업무=승인·초대+2FA 필수 vs 관람객=간편가입/게스트 예매+2FA 미강제) ②**§2-1-1 신설** — (A)계정 체계 단일 통합+가입 트랙 분리(승격 시 단일 계정 등급 상향으로 리드·비즈매칭·재방문 이력 연속성 유지), (B)모바일 코드베이스 1개(Expo `mobile/`)·배포 타깃 2개(운영 앱=스토어 미공개 사내 QR/APK, 관람객 앱=스토어 공개), 관람객 1차 접점=공개 웹(SCR-P7/P8)·앱=리텐션 채널 ③**§5B-3 인증 정밀화** — 2FA 대상을 "업무 사용자 필수·관람객 미강제"로, 승격 시 2FA 필수 전환 명시 ④**M10 갱신** — 간편가입/게스트 예매(SCR-P7)·계정 승격 연속성·게스트 예매 개인정보/병합 정책 반영. 근거: `docs/analysis/ticketing-app-benchmark.md`(게스트 예매·간편가입 업계 표준). design.md·타 문서 미수정(designer 후속 반영 필요), 시크릿 미기재 |
|
||||
|
||||
@ -85,3 +85,4 @@
|
||||
|---|---|
|
||||
| 2026-07-11 | 최초 작성 — 요구사항 27건·현황·남은 큐·함정 |
|
||||
| 2026-07-11 | 신규 5화면(SCR-13~17) 구현 완료 반영 + 미개발 통합 백로그 `UNDEVELOPED_BACKLOG.md` 신설·연결 |
|
||||
| 2026-07-11 | 모바일 트랙 하네스 신설(kintex-mobile-dev + kintex-mobile-orchestrator, WISE 모바일 레퍼런스·Stitch 디자인 경유·G3 EAS 게이트) + 웹 WISE 기능 레퍼런스 강화(kintex-frontend-dev·impl-orchestrator) |
|
||||
|
||||
141
docs/analysis/ticketing-app-benchmark.md
Normal file
141
docs/analysis/ticketing-app-benchmark.md
Normal file
@ -0,0 +1,141 @@
|
||||
# 관람객·입장권 예매·모바일 앱 벤치마킹 — 코엑스 및 공연/티켓 앱
|
||||
|
||||
> 작성일: 2026-07-11 · 대상 트랙: 킨텍스 자동전시시스템 관람객(M10)·티켓·모바일 앱
|
||||
> 목적: COEX·공연/티켓 빅3·글로벌 이벤트 앱을 조사하여 현행 `design.md` v2.1 티켓·관람객 화면(SCR-P7/P8·M14/M15·M5~M9)을 대조·보강.
|
||||
> 원칙: 확인된 기능만 사실로 기술하고 미확인 항목은 "추정" 표기. 본 문서는 **권고만** 기록하며 PLANNING.md·design.md를 직접 수정하지 않는다.
|
||||
|
||||
---
|
||||
|
||||
## 1. 벤치마킹 대상별 요약
|
||||
|
||||
| 앱/서비스 | 분류 | 강점 | 약점/한계 | 특징 기능 |
|
||||
|---|---|---|---|---|
|
||||
| **COEX**(코엑스 공식 웹) | 전시장 운영사 | PC·모바일 실내 길찾기(PC 지도→모바일 QR 이전), 주차 혼잡 예측(KakaoT 데이터), 관심분야 행사 알림 | 전용 네이티브 앱보다 웹 중심(추정), 예매/티켓 기능은 각 주최자 사이트에 파편화 | 실내 내비게이션, 주차 혼잡 예보, 관심 기반 행사 알림 구독 |
|
||||
| **인터파크 NOL 티켓** | 공연/티켓 빅3 | 성숙한 예매 퍼널·개인화(관심인물/공연/장르), 오픈 알림(티켓캐스트 알리미), 예매대기·취소표 우선권, 결제수단 사전등록 빠른결제 | 콘서트 특화(좌석 선점·대기열)로 무료 전시엔 과한 요소, 우편배송 등 물리 티켓 잔재 | 티켓오픈 알림, 관심 구독, 예매대기 서비스, 단계별 취소수수료 규정 |
|
||||
| **멜론티켓** | 공연/티켓 빅3 | 좌석 5분 선점 UX, 수령방식 선택(배송/현장수령/모바일 발권), 당일 모바일티켓 제시 입장, 강한 매크로 방지(CAPTCHA) | 좌석제 공연 중심, 입장 UX 상세는 비공개(추정) | 모바일 발권, 좌석 선점 타이머, 봇 방지 |
|
||||
| **티켓링크** | 공연/티켓 빅3 | 스마트티켓(별도 발권 없이 QR/바코드 입장), 스마트스탬프(휴대폰 고유값 인식 → 캡처 티켓 무효·부정입장 방지) | 좌석/경기 특화, SMS URL 등록 절차 존재 | 스마트티켓, 스마트스탬프(디바이스 바인딩), QR/바코드 입장 |
|
||||
| **킨텍스 공식 앱**(com.kintex) | 전시 특화(자사) | 무료 사전등록, 실시간 주차현황·주차비 결제, 이벤트/할인권(맛집·호텔), 지역별 등록 분포 | 전시별 통합 예매·티켓 지갑·비즈매칭은 미약(추정), 부스 wayfinding 약함 | 사전등록, 주차 연계, 편의(할인권), 등록 분포 데이터 |
|
||||
| **Whova**(글로벌 이벤트 앱) | 이벤트·컨퍼런스 | 참가자별 고유 QR(배지+앱), 리드 리트리벌, 멀티트랙 개인 아젠다+세션 정원 등록, 인앱 메시징·매치메이킹·밋업 스케줄·QR 명함교환, 셀프 체크인 키오스크, 온디맨드 배지 생성, 라이브 공지/폴 | 유료 SaaS, 국내 결제/PG·다국어 로컬라이즈는 별도 | 개인 아젠다(정원), QR 명함교환, 셀프 체크인 키오스크, 라이브 인게이지먼트 |
|
||||
| **Eventbrite**(글로벌) | 티켓·발견 | 오거나이저 앱 QR 스캔·이름/게스트리스트 체크인, 마켓플레이스 발견성 | 컨퍼런스 당일 인게이지먼트는 약함 | 발견(마켓플레이스), 간편 체크인, 디지털 게스트 리스트 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 조사 항목 ①~⑧ 베스트 프랙티스 비교
|
||||
|
||||
### ① 예매 퍼널 (단계 수·게스트 예매)
|
||||
- **NOL/멜론/티켓링크**: 회차/좌석 → 권종/수량 → 예매자 → 결제 → 완료. 좌석제는 선점 타이머(멜론 5분). 콘서트는 대기열/대기번호. 회원 로그인 유도.
|
||||
- **Whova/Eventbrite**: 무료·유료 티켓 유형별 수량 선택 → 등록폼 → (유료 시)결제 → QR 발급. 게스트 등록 허용, 셀프 등록 중심.
|
||||
- **킨텍스 앱**: 무료 사전등록 폼 중심(결제 최소).
|
||||
- **베스트**: 전시(대체로 무료·정원제)는 **좌석 선택 대신 권종·정원 기반 짧은 퍼널**이 적합. 게스트 예매 허용 + 무료 권종 결제 스킵 + 회원 저장 유도가 표준.
|
||||
|
||||
### ② 티켓 수령 방식 (QR·스마트티켓·본인인증)
|
||||
- **티켓링크 스마트티켓**: 별도 발권 없이 앱/URL 등록 → QR·바코드. **스마트스탬프 = 휴대폰 고유값 인식**으로 캡처본 무효·부정입장 방지.
|
||||
- **멜론**: 모바일 발권/현장수령/배송 선택, 당일 모바일티켓 제시.
|
||||
- **Whova**: 참가자별 **고유 QR**(배지·앱 동시), 리드 리트리벌 연계.
|
||||
- **베스트**: **모바일 QR 지갑 기본 + 디바이스 바인딩/본인인증으로 위·변조·양도 방지**. 캡처만으로 입장 불가(회전 토큰·기기 인식)가 정품 티켓 신뢰의 핵심.
|
||||
|
||||
### ③ 입장/체크인 UX (밝기 부스트·오프라인 QR)
|
||||
- **공통(티켓링크 안내)**: QR 인식률 위해 **화면 밝기 상향** 권장.
|
||||
- **Whova/Eventbrite**: 셀프 체크인 키오스크·오거나이저 앱 스캔, 이름/게스트리스트 폴백.
|
||||
- **베스트**: **QR 풀스크린 + 자동 밝기 부스트 + 오프라인 캐시 표시**(현장 네트워크 불안정 대비) + 셀프 체크인 키오스크/즉석 배지 인쇄 폴백.
|
||||
|
||||
### ④ 취소·환불 플로우
|
||||
- **NOL 티켓(공개 규정)**: 예매수수료는 예매 당일 자정까지만 환불. 취소수수료 = 관람일 D-10까지 정액(뮤지컬/콘서트 4,000원 등, 최대 티켓가 10%), D-9~D-7 10%, D-6~D-3 20%, D-2~D-1 30%, **당일 90%**. 웹 상단 '예매 취소' 셀프 메뉴.
|
||||
- **베스트**: **구간별 취소수수료 규정(서버 권위) + 예상 환불액 실시간 표시 + 셀프 취소 + 원결제수단 자동 환불(PG 위임)**. 무료 전시는 노쇼 관리(취소 유도)가 관건.
|
||||
|
||||
### ⑤ 개인화 (관심 행사·오픈 알림·최근 본)
|
||||
- **NOL**: 관심인물/관심공연/관심장르 → 맞춤 추천 + **티켓오픈일 알림(SMS·이메일)**, 예매대기·취소표 우선권.
|
||||
- **COEX**: 관심분야 선택 → 관련 행사 알림.
|
||||
- **Whova**: 개인 아젠다·AI 추천 세션·매치메이킹.
|
||||
- **베스트**: **오픈 알림/관심 행사 구독 + 관심분야 기반 추천 + 최근 본 행사·부스**로 재방문·전환 견인.
|
||||
|
||||
### ⑥ 현장 편의 (주차·길찾기·혼잡도)
|
||||
- **COEX**: 실내 내비(PC→모바일 QR), 주차 혼잡 예보(KakaoT).
|
||||
- **킨텍스 앱**: 실시간 주차현황·주차비 결제, 할인권.
|
||||
- **베스트**: **관람객 앱에 wayfinding + 실시간 주차(현황·결제·사전권) + 혼잡/대기 안내를 한 화면 동선**으로 통합. 킨텍스는 GTX-A 킨텍스역·iparking 연계가 차별점.
|
||||
|
||||
### ⑦ 멤버십/등급
|
||||
- **NOL/멜론**: 회원 등급·포인트·간편결제 저장.
|
||||
- **Whova**: 유형별(참가자/바이어/연사) 배지·권한 차등.
|
||||
- **베스트**: **관람객 유형(일반/바이어/VIP)·재방문·바이어 인증 등급**으로 혜택·알림·비즈매칭 우선순위 차등. (전시 우선순위는 낮음)
|
||||
|
||||
### ⑧ 접근성·다국어
|
||||
- **COEX/킨텍스**: 다국어 안내·수어 통역(COEX).
|
||||
- **Whova**: 글로벌 대상 로컬라이즈.
|
||||
- **베스트**: **티켓·배지·앱 다국어(한/영/중/일) + WCAG AA + QR/텍스트 대체수단**. 국제전시장 특성상 외국인 바이어 대응이 필수.
|
||||
|
||||
---
|
||||
|
||||
## 3. 킨텍스 자동전시시스템 반영 시사점 (ⓐⓑⓒ 3분류)
|
||||
|
||||
> 대조 기준: `design.md` v2.1 — SCR-P7(예매)·SCR-P8(확인·취소)·SCR-M14(모바일 예매)·SCR-M15(티켓 지갑)·SCR-M5(관람객 홈·배지)·SCR-M6(wayfinding)·SCR-M7(플로어플랜)·SCR-M8(비즈매칭)·SCR-M9(세션 아젠다) / `PLANNING.md` M10·M13.
|
||||
|
||||
### ⓐ 이미 반영됨 (현행 스펙이 베스트 프랙티스 충족)
|
||||
| # | 항목 | 현행 근거 |
|
||||
|---|---|---|
|
||||
| A1 | 짧은 예매 퍼널(권종→예매자→결제→완료), 게스트+회원, 무료 권종 결제 스킵 | SCR-P7·M14 |
|
||||
| A2 | PG 위임 결제(카드정보 미저장 안내·카드입력 UI 없음) | SCR-P7 3단계·M14 |
|
||||
| A3 | 모바일 QR 티켓 지갑 + **자동 밝기 부스트** + 오프라인 캐시 표시 | SCR-M15 |
|
||||
| A4 | 셀프 예매 확인·취소(게스트 예매번호+이메일 조회), PII 마스킹 | SCR-P8 |
|
||||
| A5 | 구간별 환불 규정 표(D-7/D-3/당일) + 예상 환불액 + 원결제수단(PG 위임) | SCR-P8 |
|
||||
| A6 | 티켓→체크인→모바일 배지 상태 승격 흐름 | SCR-M15→M5 |
|
||||
| A7 | 관심분야 기반 AI 추천 세션·부스, 즐겨찾기 | SCR-M5·M9 |
|
||||
| A8 | wayfinding(블루닷·존레벨 폴백)·인터랙티브 플로어플랜·비즈매칭 | SCR-M6·M7·M8 |
|
||||
| A9 | 단체 예매(대표자+동반자 명단 CSV) | SCR-P7 2단계 |
|
||||
| A10 | WCAG AA·라이트/다크·비시각 대안 목록 | 전 화면 공통 |
|
||||
|
||||
### ⓑ 보강 권고 (기존 화면 스펙 수정 후보 · P1)
|
||||
| # | 권고 | 대상 화면/모듈 | 근거(§2) |
|
||||
|---|---|---|---|
|
||||
| B1 | **스마트티켓 부정입장 방지 강화** — 디바이스 바인딩/회전(애니메이션) QR + 본인인증으로 캡처본 무효·양도 방지(현행은 정적 QR) | SCR-M15·31 / M10 | ② 티켓링크 스마트스탬프 |
|
||||
| B2 | **취소·환불 규정 정교화** — 단순 3구간(100/50/0%)을 인터파크식 다구간 수수료 + 예매수수료 별도 환불 규정으로 확장, 서버 권위 명시 | SCR-P8 / M9·M10 | ④ NOL 취소수수료 |
|
||||
| B3 | **티켓·배지·관람객 앱 다국어(한/영/중/일)** — 현행 다국어는 공개사이트(M12)만 명시, 티켓·배지·지갑 화면에 다국어 확장 | SCR-P7·M5·M15 / M10·M12 | ⑧ 국제전시장 |
|
||||
| B4 | **세션 정원 기반 사전등록(RSVP)** — 현행 즐겨찾기는 있으나 정원·마감·대기 미반영. 정원 소진·마감 상태 추가 | SCR-M9 / M11 | ① Whova 개인 아젠다 정원 |
|
||||
| B5 | **결제수단 사전등록·간편결제 빠른결제** — 재방문·유료 권종 전환율 향상 | SCR-P7·M14 / M9 | ⑤ NOL 빠른결제 |
|
||||
| B6 | **행사 라이브 공지 피드/현장 푸시** — 알림센터(M11)에 더해 행사별 라이브 공지 피드(긴급 안내·프로그램 변경) | SCR-M11·M5 / §5B notification·M12 | Whova 라이브 공지 |
|
||||
|
||||
### ⓒ 신규 화면/기능 후보 (P2 백로그)
|
||||
| # | 후보 | 대상 화면/모듈 | 근거(§2) |
|
||||
|---|---|---|---|
|
||||
| C1 | **티켓 오픈 알림·관심 행사 구독** — 공개사이트+앱에서 오픈 예정 행사 구독→오픈 시 푸시/EDM | 신규(공개사이트·앱) / M10·M12 | ⑤ NOL 오픈알림·COEX 관심알림 |
|
||||
| C2 | **관람객 앱 주차 연계** — iparking 실시간 현황·주차비 결제·사전 주차권 | 신규 SCR-M(주차) / M14·M10 | ⑥ 킨텍스 앱·COEX 주차예보 |
|
||||
| C3 | **관람객 대면 혼잡/대기 안내** — 입장·주차·인기 세션 실시간 혼잡도(현행 M14는 홀매니저용) | 신규 위젯(SCR-M5) / M14 | ⑥ COEX 혼잡 예보 |
|
||||
| C4 | **관람객↔관람객 QR 명함 교환·컨택트 지갑** — 현행 리드캡처는 참가업체→관람객만 | SCR-M8 확장 / M11 | ② Whova QR 명함교환 |
|
||||
| C5 | **인기 세션/바이어 미팅 예매 대기열·취소표 알림** — 정원 초과 세션·미팅 슬롯 대기 우선권 | SCR-M9·M8 / M11 | ⑤ NOL 예매대기 |
|
||||
| C6 | **관람객 멤버십/등급** — 일반/바이어/VIP·재방문·바이어 인증 등급별 혜택·매칭 우선순위 | 신규 마이 / M10 | ⑦ 등급 차등 |
|
||||
| C7 | **최근 본 행사·부스 개인화 홈 피드** — 재방문 전환 | SCR-M5 확장 / M10·M12 | ⑤ 최근 본 |
|
||||
| C8 | **셀프 체크인 키오스크·즉석 배지 인쇄 화면** — PLANNING M10 즉석배지 언급 있으나 화면 미정의 | 신규 SCR / M10 | ③ Whova 셀프 체크인 키오스크 |
|
||||
|
||||
### 톱 5 보강 권고 (우선 반영)
|
||||
1. **B1 스마트티켓 부정입장 방지**(디바이스 바인딩·회전 QR·본인인증) — 정품 티켓 신뢰의 핵심.
|
||||
2. **B2 취소·환불 규정 정교화**(다구간 수수료·예매수수료 분리·서버 권위) — 정산 정확성·분쟁 예방.
|
||||
3. **C1 티켓 오픈 알림·관심 행사 구독** — 개인화·재방문·전환의 최대 레버.
|
||||
4. **C2 관람객 앱 주차 연계**(iparking 실시간·결제·사전권) — 킨텍스 현장 편의 차별점.
|
||||
5. **B3 티켓·배지·앱 다국어(한/영/중/일)** — 국제전시장 외국인 바이어 대응 필수.
|
||||
|
||||
---
|
||||
|
||||
## 4. 출처 URL
|
||||
|
||||
- COEX VISITOR(실내 길찾기·주차): https://www.coex.co.kr/ , https://www.coex.co.kr/guide/indoor-navigation/ , https://www.coex.co.kr/guide/parking-information/infomation/
|
||||
- COEX 행사 일정·관심 알림: https://www.coex.co.kr/event/full-schedules/
|
||||
- 인터파크 NOL 티켓 취소/환불: https://ticket.interpark.com/TiKi/Info/BookingGuide.asp?Url=guide_05.html
|
||||
- 인터파크 NOL 티켓 수수료: https://ticket.interpark.com/TiKi/Info/BookingGuide.asp?Url=guide_11.html
|
||||
- 인터파크 티켓캐스트(오픈 알림): https://ticket.interpark.com/TiKi/Info/BookingGuide.asp?Url=guide_10.html
|
||||
- 인터파크 예매대기 서비스: https://ticket.interpark.com/TiKi/Info/BookingGuide.asp?Url=guide_13.html
|
||||
- NOL 인터파크(개인화/오픈예정): https://nol.interpark.com/ , https://tickets.interpark.com/contents/notice
|
||||
- 멜론티켓 이용안내: https://ticket.melon.com/customerservice/howto.htm
|
||||
- 티켓링크 스마트티켓: https://www.ticketlink.co.kr/help/guide/popup/smart-ticket
|
||||
- 티켓링크 앱: https://play.google.com/store/apps/details?id=kr.co.ticketlink.cne
|
||||
- 멜론티켓 앱: https://play.google.com/store/apps/details?id=com.iloen.melonticket
|
||||
- 킨텍스 주차/앱 사전등록: https://www.kintex.com/web/ko/service/parking_user.do , https://www.data.go.kr/data/15119880/fileData.do
|
||||
- Whova 이벤트 앱 기능: https://whova.com/whova-event-app/ , https://whova.com/blog/best-event-conference-apps/
|
||||
- Eventbrite vs Whova 비교: https://boompop.com/blog/whova-vs-eventbrite-comparison
|
||||
- 이벤트 체크인 앱 비교: https://qrsage.com/blogs/best-event-check-in-app
|
||||
|
||||
---
|
||||
|
||||
## 변경 이력
|
||||
| 날짜 | 변경 내용 |
|
||||
|---|---|
|
||||
| 2026-07-11 | 최초 작성 — COEX·NOL/멜론/티켓링크·킨텍스 앱·Whova/Eventbrite 7종 벤치마킹, ⓐ10·ⓑ6·ⓒ8 시사점, 톱5 보강 권고 |
|
||||
854
docs/deliverables/DA산출물/_gen/gen_da_deliverables.py
Normal file
854
docs/deliverables/DA산출물/_gen/gen_da_deliverables.py
Normal file
@ -0,0 +1,854 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
킨텍스 자동전시시스템 — DA(데이터 아키텍처) 산출물 생성기
|
||||
1) 데이터베이스설계서.pptx (행안부 DB설계서 표준 톤 / 16:9)
|
||||
2) 테이블정의서.xlsx (실제 Flyway DDL 역추출)
|
||||
|
||||
내용 소스(권위) = src/backend/.../db/migration/V1~V9 실제 DDL.
|
||||
→ 테이블·컬럼·타입·NULL·PK/FK·기본값·인덱스·공간타입을 프로그램으로 파싱(수기 발명 금지).
|
||||
→ 컬럼 설명은 DDL 인라인 주석·COMMENT ON 을 그대로 사용. 없으면 generic 표준 컬럼만 보강.
|
||||
|
||||
디자인 = KINTEX Blue #0066B3 / AI Purple #6D4AFF / 맑은 고딕. ERD·도식은 python-pptx 도형 직접 작도.
|
||||
보안 불변 = 비밀번호/API키/서버IP/SSH 미기재. 암호화 컬럼은 정책만 기술(AES-256-GCM).
|
||||
|
||||
재실행: python gen_da_deliverables.py
|
||||
"""
|
||||
import os, re, glob
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
OUT_DIR = os.path.dirname(HERE) # docs/deliverables/DA산출물
|
||||
MIG_DIR = os.path.abspath(os.path.join(
|
||||
HERE, "..", "..", "..", "..", "src", "backend", "src", "main", "resources", "db", "migration"))
|
||||
assert os.path.isdir(MIG_DIR), f"migration dir not found: {MIG_DIR}"
|
||||
|
||||
# ======================================================================
|
||||
# 1. DDL 파서 (Flyway V1~V9 → 테이블 스키마 모델)
|
||||
# ======================================================================
|
||||
CONSTRAINT_STARTS = ("PRIMARY KEY", "UNIQUE", "FOREIGN KEY", "CHECK", "CONSTRAINT", "REFERENCES ")
|
||||
GENERIC_DESC = {
|
||||
"id": "식별자(PK)", "created_at": "생성일시", "updated_at": "수정일시",
|
||||
"event_id": "행사 참조(FK event)", "status": "상태 코드", "sort_order": "정렬 순서",
|
||||
"use_yn": "사용 여부(Y/N)", "dept_id": "부서 식별자", "writer_id": "작성자",
|
||||
"writer_name": "작성자명", "author_id": "작성자", "author_name": "작성자명",
|
||||
"owner_id": "소유자", "owner_name": "소유자명", "title": "제목", "content": "내용",
|
||||
"read_at": "읽은 일시", "recipient_id": "수신자",
|
||||
}
|
||||
|
||||
def _split_type(rest):
|
||||
"""rest 문자열의 선두에서 데이터타입 토큰을 추출. geometry(Point,0)·numeric(8,2) 등 괄호 내부 콤마 허용."""
|
||||
m = re.match(r"\s*([A-Za-z_][A-Za-z0-9_]*(?:\([^)]*\))?)", rest)
|
||||
if not m:
|
||||
return "", rest
|
||||
return m.group(1), rest[m.end():]
|
||||
|
||||
def _parse_default(rest):
|
||||
m = re.search(r"\bDEFAULT\s+(now\(\)|gen_random_uuid\(\)|'[^']*'|true|false|[0-9]+\.?[0-9]*|[A-Za-z_][\w]*)",
|
||||
rest, re.IGNORECASE)
|
||||
return m.group(1) if m else ""
|
||||
|
||||
def parse_ddl():
|
||||
tables = {} # name -> {"cols":[...], "pk":set, "uniques":[...], "indexes":[...], "version":Vx, "order":int}
|
||||
order = 0
|
||||
files = sorted(glob.glob(os.path.join(MIG_DIR, "V*.sql")),
|
||||
key=lambda p: int(re.match(r"V(\d+)", os.path.basename(p)).group(1)))
|
||||
comments = {} # (table,col) -> desc (COMMENT ON)
|
||||
for path in files:
|
||||
ver = re.match(r"(V\d+)", os.path.basename(path)).group(1)
|
||||
raw = open(path, encoding="utf-8").read()
|
||||
|
||||
# COMMENT ON COLUMN t.c IS '...'
|
||||
for cm in re.finditer(r"COMMENT\s+ON\s+COLUMN\s+(\w+)\.(\w+)\s+IS\s+'((?:[^']|'')*)'", raw, re.IGNORECASE):
|
||||
comments[(cm.group(1), cm.group(2))] = cm.group(3).replace("''", "'")
|
||||
|
||||
# CREATE TABLE IF NOT EXISTS name ( ... );
|
||||
for tm in re.finditer(r"CREATE\s+TABLE\s+IF\s+NOT\s+EXISTS\s+(\w+)\s*\((.*?)\n\)\s*;",
|
||||
raw, re.IGNORECASE | re.DOTALL):
|
||||
tname, body = tm.group(1), tm.group(2)
|
||||
if tname not in tables:
|
||||
order += 1
|
||||
tables[tname] = {"cols": [], "pk": set(), "uniques": [], "indexes": [],
|
||||
"version": ver, "order": order}
|
||||
t = tables[tname]
|
||||
for line in body.split("\n"):
|
||||
code = line.split("--", 1)[0].strip()
|
||||
cmt = line.split("--", 1)[1].strip() if "--" in line else ""
|
||||
if not code:
|
||||
continue
|
||||
code = code.rstrip(",").strip()
|
||||
up = code.upper()
|
||||
# 테이블 레벨 제약
|
||||
if up.startswith("PRIMARY KEY"):
|
||||
for c in re.findall(r"\((.*?)\)", code)[0].split(","):
|
||||
t["pk"].add(c.strip())
|
||||
continue
|
||||
if up.startswith("UNIQUE"):
|
||||
t["uniques"].append(re.findall(r"\((.*?)\)", code)[0].strip())
|
||||
continue
|
||||
if up.startswith(CONSTRAINT_STARTS):
|
||||
continue
|
||||
# 컬럼 정의
|
||||
cm2 = re.match(r"(\w+)\s+(.*)", code)
|
||||
if not cm2:
|
||||
continue
|
||||
cname = cm2.group(1)
|
||||
dtype, rest = _split_type(cm2.group(2))
|
||||
ru = rest.upper()
|
||||
is_pk = "PRIMARY KEY" in ru
|
||||
if is_pk:
|
||||
t["pk"].add(cname)
|
||||
fk = re.search(r"REFERENCES\s+(\w+)\s*\((\w+)\)", rest, re.IGNORECASE)
|
||||
nn = ("NOT NULL" in ru) or is_pk
|
||||
uniq = ("UNIQUE" in ru) and "PRIMARY KEY" not in ru
|
||||
dflt = _parse_default(rest)
|
||||
t["cols"].append({
|
||||
"name": cname, "type": dtype,
|
||||
"null": "N" if nn else "Y",
|
||||
"pk": is_pk, "fk": (fk.group(1) if fk else None),
|
||||
"fkcol": (fk.group(2) if fk else None),
|
||||
"unique": uniq, "default": dflt, "desc": cmt,
|
||||
})
|
||||
|
||||
# ALTER TABLE t ADD COLUMN IF NOT EXISTS c type ...
|
||||
for am in re.finditer(r"ALTER\s+TABLE\s+(\w+)\s+ADD\s+COLUMN\s+IF\s+NOT\s+EXISTS\s+(.+?);",
|
||||
raw, re.IGNORECASE):
|
||||
tname = am.group(1)
|
||||
if tname not in tables:
|
||||
continue
|
||||
t = tables[tname]
|
||||
code = am.group(2).strip()
|
||||
cm2 = re.match(r"(\w+)\s+(.*)", code)
|
||||
if not cm2:
|
||||
continue
|
||||
cname = cm2.group(1)
|
||||
if any(c["name"] == cname for c in t["cols"]):
|
||||
continue
|
||||
dtype, rest = _split_type(cm2.group(2))
|
||||
ru = rest.upper()
|
||||
t["cols"].append({
|
||||
"name": cname, "type": dtype,
|
||||
"null": "N" if "NOT NULL" in ru else "Y",
|
||||
"pk": False, "fk": None, "fkcol": None,
|
||||
"unique": "UNIQUE" in ru, "default": _parse_default(rest),
|
||||
"desc": f"({ver} 순증 컬럼)",
|
||||
})
|
||||
|
||||
# CREATE INDEX ... ON t ...
|
||||
for im in re.finditer(r"CREATE\s+INDEX\s+IF\s+NOT\s+EXISTS\s+(\w+)\s+ON\s+(\w+)\s*(.*?);",
|
||||
raw, re.IGNORECASE | re.DOTALL):
|
||||
idxname, tname, spec = im.group(1), im.group(2), im.group(3).strip()
|
||||
if tname in tables:
|
||||
gist = "GIST" in spec.upper()
|
||||
cols = re.findall(r"\((.*?)\)", spec)
|
||||
tables[tname]["indexes"].append({
|
||||
"name": idxname, "spec": (cols[0] if cols else spec),
|
||||
"gist": gist,
|
||||
})
|
||||
|
||||
# 설명 보강: COMMENT ON → generic
|
||||
for tname, t in tables.items():
|
||||
for c in t["cols"]:
|
||||
if not c["desc"]:
|
||||
c["desc"] = comments.get((tname, c["name"]), "")
|
||||
if not c["desc"] and c["name"] in GENERIC_DESC:
|
||||
c["desc"] = GENERIC_DESC[c["name"]]
|
||||
return tables
|
||||
|
||||
TABLES = parse_ddl()
|
||||
|
||||
# ---- 테이블 한글명 · 주제영역 매핑 (라벨링) ----------------------------------
|
||||
KO = {
|
||||
"app_user": "사용자", "company": "등록업체", "event": "행사",
|
||||
"hall": "전시홀 마스터", "hall_assignment": "행사-홀 배정", "event_member": "행사 참여자(RBAC)",
|
||||
"booth_standard": "부스 표준사양", "master_data": "마스터데이터(요율·규정)",
|
||||
"trench": "트렌치(급전 포인트)", "hall_exit": "비상구", "layout": "배치안(버전)",
|
||||
"booth": "부스(폴리곤)", "design_plan": "부스 설계안", "utility_order": "유틸리티 신청",
|
||||
"render_job": "AI 렌더잡", "common_code_group": "공통코드 그룹", "common_code": "공통코드",
|
||||
"sys_menu": "시스템 메뉴", "sys_role": "역할", "sys_permission": "권한",
|
||||
"sys_role_permission": "역할-권한 매핑", "sys_setting": "시스템 설정", "audit_log": "감사 로그",
|
||||
"worklog": "업무일지", "schedule": "일정", "message": "쪽지",
|
||||
"message_recipient": "쪽지 수신자", "notice": "공지", "opinion": "의견",
|
||||
"opinion_comment": "의견 댓글", "meeting": "회의", "meeting_action": "회의 액션아이템",
|
||||
"report": "보고서", "notification": "알림", "password_reset": "비밀번호 재설정 코드",
|
||||
}
|
||||
# 주제영역(Subject Area): (코드, 한글, 테이블목록, 색상키)
|
||||
AREAS = [
|
||||
("MASTER", "행사·조직·권한·마스터",
|
||||
["event", "hall", "hall_assignment", "event_member", "app_user", "company",
|
||||
"booth_standard", "master_data"]),
|
||||
("SPATIAL", "공간·설계·시각화(부스 코어)",
|
||||
["trench", "hall_exit", "layout", "booth", "design_plan", "utility_order", "render_job"]),
|
||||
("SYSTEM", "시스템관리·인증",
|
||||
["common_code_group", "common_code", "sys_menu", "sys_role", "sys_permission",
|
||||
"sys_role_permission", "sys_setting", "audit_log", "password_reset"]),
|
||||
("COMMON", "공통 업무 모듈",
|
||||
["worklog", "schedule", "message", "message_recipient", "notice", "opinion",
|
||||
"opinion_comment", "meeting", "meeting_action", "report", "notification"]),
|
||||
]
|
||||
AREA_OF = {}
|
||||
for code, ko, tlist in AREAS:
|
||||
for tn in tlist:
|
||||
AREA_OF[tn] = ko
|
||||
|
||||
TOTAL_TABLES = len(TABLES)
|
||||
TOTAL_COLS = sum(len(t["cols"]) for t in TABLES.values())
|
||||
TOTAL_IDX = sum(len(t["indexes"]) for t in TABLES.values())
|
||||
SPATIAL_COLS = [(tn, c["name"], c["type"]) for tn, t in TABLES.items()
|
||||
for c in t["cols"] if c["type"].lower().startswith("geometry")]
|
||||
FK_EDGES = [(tn, c["name"], c["fk"]) for tn, t in TABLES.items()
|
||||
for c in t["cols"] if c["fk"]]
|
||||
|
||||
# ======================================================================
|
||||
# 2. PPTX — 데이터베이스설계서
|
||||
# ======================================================================
|
||||
from pptx import Presentation
|
||||
from pptx.util import Pt
|
||||
from pptx.dml.color import RGBColor
|
||||
from pptx.enum.text import PP_ALIGN, MSO_ANCHOR
|
||||
from pptx.enum.shapes import MSO_SHAPE, MSO_CONNECTOR
|
||||
from pptx.oxml.ns import qn
|
||||
|
||||
BLUE = RGBColor(0x00, 0x66, 0xB3)
|
||||
BLUE_DK = RGBColor(0x00, 0x44, 0x7A)
|
||||
BLUE_LT = RGBColor(0xE1, 0xEF, 0xF9)
|
||||
PURPLE = RGBColor(0x6D, 0x4A, 0xFF)
|
||||
PURPLE_LT = RGBColor(0xEC, 0xE8, 0xFF)
|
||||
INK = RGBColor(0x10, 0x18, 0x28)
|
||||
MUTED = RGBColor(0x66, 0x70, 0x85)
|
||||
LINE = RGBColor(0xE4, 0xE7, 0xEC)
|
||||
BG = RGBColor(0xF9, 0xFA, 0xFB)
|
||||
WHITE = RGBColor(0xFF, 0xFF, 0xFF)
|
||||
GREEN = RGBColor(0x12, 0x9E, 0x63)
|
||||
AMBER = RGBColor(0xE0, 0x8A, 0x00)
|
||||
TEAL = RGBColor(0x0E, 0x7C, 0x86)
|
||||
CARD = RGBColor(0xFF, 0xFF, 0xFF)
|
||||
FONT = "맑은 고딕"
|
||||
IN = 914400
|
||||
def inch(v): return int(v * IN)
|
||||
|
||||
prs = Presentation()
|
||||
prs.slide_width = 12192000
|
||||
prs.slide_height = 6858000
|
||||
BLANK = prs.slide_layouts[6]
|
||||
def slide(): return prs.slides.add_slide(BLANK)
|
||||
|
||||
def _fill(sp, color):
|
||||
if color is None: sp.fill.background()
|
||||
else:
|
||||
sp.fill.solid(); sp.fill.fore_color.rgb = color
|
||||
|
||||
def rect(s, x, y, w, h, fill=None, line=None, line_w=1.0, shape=MSO_SHAPE.RECTANGLE, shadow=False):
|
||||
sp = s.shapes.add_shape(shape, inch(x), inch(y), inch(w), inch(h))
|
||||
_fill(sp, fill)
|
||||
if line is None: sp.line.fill.background()
|
||||
else:
|
||||
sp.line.color.rgb = line; sp.line.width = Pt(line_w)
|
||||
sp.shadow.inherit = False # creates an empty <a:effectLst/>
|
||||
if shadow:
|
||||
el = sp._element.spPr
|
||||
ef = el.find(qn('a:effectLst'))
|
||||
if ef is None:
|
||||
ef = el.makeelement(qn('a:effectLst'), {}); el.append(ef)
|
||||
sh = ef.makeelement(qn('a:outerShdw'),
|
||||
{'blurRad':'80000','dist':'30000','dir':'5400000','rotWithShape':'0'})
|
||||
clr = sh.makeelement(qn('a:srgbClr'), {'val':'101828'})
|
||||
alp = clr.makeelement(qn('a:alpha'), {'val':'16000'})
|
||||
clr.append(alp); sh.append(clr); ef.append(sh)
|
||||
return sp
|
||||
|
||||
def text(s, x, y, w, h, content, size=13, color=INK, bold=False, align=PP_ALIGN.LEFT,
|
||||
anchor=MSO_ANCHOR.TOP, line_spacing=1.0, space_after=2, wrap=True):
|
||||
tb = s.shapes.add_textbox(inch(x), inch(y), inch(w), inch(h))
|
||||
tf = tb.text_frame; tf.word_wrap = wrap; tf.vertical_anchor = anchor
|
||||
tf.margin_left = 0; tf.margin_right = 0; tf.margin_top = 0; tf.margin_bottom = 0
|
||||
if isinstance(content, str): content = [content]
|
||||
for i, para in enumerate(content):
|
||||
p = tf.paragraphs[0] if i == 0 else tf.add_paragraph()
|
||||
p.alignment = align; p.line_spacing = line_spacing
|
||||
p.space_after = Pt(space_after); p.space_before = Pt(0)
|
||||
segs = [{"t": para}] if isinstance(para, str) else ([para] if isinstance(para, dict) else para)
|
||||
for seg in segs:
|
||||
r = p.add_run(); r.text = seg.get("t", "")
|
||||
r.font.name = FONT; r.font.size = Pt(seg.get("size", size))
|
||||
r.font.bold = seg.get("bold", bold); r.font.color.rgb = seg.get("color", color)
|
||||
rPr = r._r.get_or_add_rPr()
|
||||
rPr.append(rPr.makeelement(qn('a:ea'), {'typeface': FONT}))
|
||||
return tb
|
||||
|
||||
def page_bg(s): rect(s, -0.1, -0.1, 13.53, 7.7, fill=BG)
|
||||
def chip(s, x, y, w, h, label, fill, txtcolor=WHITE, size=9.5, bold=True):
|
||||
rect(s, x, y, w, h, fill=fill, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
|
||||
text(s, x, y, w, h, label, size=size, color=txtcolor, bold=bold, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)
|
||||
def card(s, x, y, w, h, fill=CARD, line=LINE):
|
||||
return rect(s, x, y, w, h, fill=fill, line=line, line_w=1.0, shape=MSO_SHAPE.ROUNDED_RECTANGLE, shadow=True)
|
||||
|
||||
def header(s, kicker, title, idx):
|
||||
page_bg(s)
|
||||
rect(s, 0, 0, 13.333, 0.12, fill=BLUE)
|
||||
rect(s, 0.55, 0.5, 0.62, 0.62, fill=BLUE, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
|
||||
text(s, 0.55, 0.5, 0.62, 0.62, f"{idx:02d}", size=20, color=WHITE, bold=True, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)
|
||||
text(s, 1.32, 0.5, 10.5, 0.28, kicker, size=11, color=PURPLE, bold=True)
|
||||
text(s, 1.32, 0.74, 11.4, 0.44, title, size=22, color=INK, bold=True)
|
||||
rect(s, 0.55, 1.28, 12.23, 0.02, fill=LINE)
|
||||
text(s, 0.55, 7.06, 9, 0.3, "킨텍스 자동전시시스템 구축 · 데이터베이스 설계서", size=8.5, color=MUTED)
|
||||
text(s, 11.3, 7.06, 1.48, 0.3, "2026-07-11", size=8.5, color=MUTED, align=PP_ALIGN.RIGHT)
|
||||
|
||||
def bullets(s, x, y, w, items, size=12.5, gap=0.44, head=None):
|
||||
if head:
|
||||
text(s, x, y, w, 0.32, head, size=12, color=PURPLE, bold=True); y += 0.42
|
||||
for it in items:
|
||||
rect(s, x, y+0.07, 0.12, 0.12, fill=BLUE, shape=MSO_SHAPE.OVAL)
|
||||
if isinstance(it, tuple):
|
||||
text(s, x+0.28, y, w-0.28, gap, [[{"t": it[0]+" ", "bold":True, "color":INK, "size":size},
|
||||
{"t": it[1], "color":MUTED, "size":size-1}]], line_spacing=1.05)
|
||||
else:
|
||||
text(s, x+0.28, y, w-0.28, gap, it, size=size, color=INK, line_spacing=1.05)
|
||||
y += gap
|
||||
return y
|
||||
|
||||
# ---- ERD 엔티티 박스 (파싱 데이터 구동) ----
|
||||
def erd_entity(s, x, y, w, tname, maxrows=6, accent=BLUE):
|
||||
t = TABLES[tname]
|
||||
# 표시 컬럼: PK → FK → 대표 컬럼
|
||||
pk = [c for c in t["cols"] if c["pk"]]
|
||||
fk = [c for c in t["cols"] if c["fk"] and not c["pk"]]
|
||||
rest = [c for c in t["cols"] if not c["pk"] and not c["fk"]]
|
||||
show = (pk + fk + rest)[:maxrows]
|
||||
rh = 0.26
|
||||
h = 0.5 + len(show)*rh
|
||||
rect(s, x, y, w, h, fill=CARD, line=LINE, line_w=1.0, shape=MSO_SHAPE.ROUNDED_RECTANGLE, shadow=True)
|
||||
rect(s, x, y, w, 0.44, fill=accent, shape=MSO_SHAPE.ROUND_2_SAME_RECTANGLE)
|
||||
text(s, x+0.12, y, w-0.2, 0.44, [[{"t": tname, "size":10.5, "color":WHITE, "bold":True},
|
||||
{"t": " "+KO.get(tname, ""), "size":8, "color":RGBColor(0xD6,0xE6,0xF6)}]],
|
||||
anchor=MSO_ANCHOR.MIDDLE)
|
||||
cy = y + 0.5
|
||||
for c in show:
|
||||
key = "PK" if c["pk"] else ("FK" if c["fk"] else "")
|
||||
kc = PURPLE if c["pk"] else (TEAL if c["fk"] else MUTED)
|
||||
geo = c["type"].lower().startswith("geometry")
|
||||
text(s, x+0.12, cy-0.03, 0.42, rh, key, size=7.5, color=kc, bold=True, anchor=MSO_ANCHOR.MIDDLE)
|
||||
text(s, x+0.54, cy-0.03, w-0.62, rh,
|
||||
[[{"t": c["name"], "size":8.6, "color":(INK if key else RGBColor(0x33,0x3B,0x4A)), "bold":bool(key)},
|
||||
{"t": " : "+c["type"], "size":7.6, "color":(GREEN if geo else MUTED)}]],
|
||||
anchor=MSO_ANCHOR.MIDDLE)
|
||||
cy += rh
|
||||
return {"x": x, "y": y, "w": w, "h": h, "cx": x+w/2, "cy": y+h/2}
|
||||
|
||||
def connect(s, a, b, color=RGBColor(0x9A,0xA6,0xB8)):
|
||||
cn = s.shapes.add_connector(MSO_CONNECTOR.STRAIGHT, inch(a["cx"]), inch(a["cy"]),
|
||||
inch(b["cx"]), inch(b["cy"]))
|
||||
cn.line.color.rgb = color; cn.line.width = Pt(1.4)
|
||||
cn.shadow.inherit = False
|
||||
return cn
|
||||
|
||||
# ================= 슬라이드 1 : 표지 =================
|
||||
s = slide()
|
||||
rect(s, -0.1, -0.1, 13.53, 7.7, fill=BLUE_DK)
|
||||
rect(s, 6.4, -1.5, 9, 11, fill=BLUE, shape=MSO_SHAPE.PARALLELOGRAM)
|
||||
rect(s, 9.2, -2.0, 7, 12, fill=PURPLE, shape=MSO_SHAPE.PARALLELOGRAM)
|
||||
rect(s, 0.85, 0.8, 0.42, 0.42, fill=WHITE, shape=MSO_SHAPE.OVAL)
|
||||
rect(s, 0.97, 0.92, 0.18, 0.18, fill=PURPLE, shape=MSO_SHAPE.OVAL)
|
||||
text(s, 1.42, 0.78, 6, 0.46, "KINTEX · WISE AI", size=15, color=WHITE, bold=True, anchor=MSO_ANCHOR.MIDDLE)
|
||||
chip(s, 0.85, 2.35, 1.95, 0.42, "데이터베이스 설계서", PURPLE, WHITE, size=12)
|
||||
chip(s, 2.92, 2.35, 2.2, 0.42, "Database Design", RGBColor(0x2B,0x4C,0x7E), WHITE, size=10.5)
|
||||
text(s, 0.85, 2.95, 11, 1.9, [
|
||||
[{"t":"킨텍스 자동전시시스템", "size":44, "color":WHITE, "bold":True}],
|
||||
[{"t":"데이터베이스 설계서", "size":44, "color":WHITE, "bold":True}]], line_spacing=1.02)
|
||||
text(s, 0.88, 4.7, 11, 0.5, "PostgreSQL 15 + PostGIS · Flyway 형상관리 · kintex 전용 DB",
|
||||
size=15.5, color=RGBColor(0xC7,0xDD,0xF2))
|
||||
text(s, 0.88, 5.24, 11.4, 0.4,
|
||||
"공간데이터(부스 POLYGON · 트렌치 POINT · 배선 LINESTRING) 일원화 · 행사 단위 격리 · 감사추적",
|
||||
size=12, color=RGBColor(0xB9,0xC7,0xEA))
|
||||
rect(s, 0.85, 6.05, 11.63, 0.02, fill=RGBColor(0x3A,0x5A,0x8C))
|
||||
text(s, 0.85, 6.25, 8.6, 0.4, [[{"t":"산출물 ","size":11,"color":RGBColor(0x90,0xA6,0xC9)},
|
||||
{"t":f"개념·논리·물리 데이터 모델 / 테이블 {TOTAL_TABLES} · 컬럼 {TOTAL_COLS} · 인덱스 {TOTAL_IDX}","size":11,"color":WHITE,"bold":True}]])
|
||||
text(s, 0.85, 6.62, 9.6, 0.4, [[{"t":"근거 ","size":11,"color":RGBColor(0x90,0xA6,0xC9)},
|
||||
{"t":"Flyway V1~V10 실제 DDL 역추출 (스키마 권위)","size":10.5,"color":WHITE}]])
|
||||
text(s, 9.7, 6.25, 2.78, 0.4, "작성일 2026-07-11", size=11, color=WHITE, bold=True, align=PP_ALIGN.RIGHT)
|
||||
text(s, 9.7, 6.62, 2.78, 0.4, "DA · kintex-da", size=9.5, color=RGBColor(0x90,0xA6,0xC9), align=PP_ALIGN.RIGHT)
|
||||
|
||||
# ================= 슬라이드 2 : 목차 =================
|
||||
s = slide()
|
||||
header(s, "CONTENTS", "목차", 0)
|
||||
rect(s, 0.55, 0.5, 0.62, 0.62, fill=PURPLE, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
|
||||
text(s, 0.55, 0.5, 0.62, 0.62, "C", size=22, color=WHITE, bold=True, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)
|
||||
toc = [
|
||||
("01","개요","PostgreSQL+PostGIS · Flyway · kintex 전용 DB"),
|
||||
("02","데이터 아키텍처 원칙","행사 단위 격리 · 공간타입 선정 · 감사로그"),
|
||||
("03","개념 데이터 모델","핵심 주제영역 관계 개요도"),
|
||||
("04","논리 모델(ERD)","주제영역별 엔터티·관계 ERD"),
|
||||
("05","물리 모델 요약","테이블 그룹 · 공간컬럼 · 인덱스 전략"),
|
||||
("06","데이터 표준","명명 규칙 · 공통코드 · 날짜/통화"),
|
||||
("07","데이터 보안","암호화·PII·응답제외·감사추적"),
|
||||
("08","백업·이관 전략","Flyway 마이그레이션 운영 원칙"),
|
||||
]
|
||||
cw = 5.85
|
||||
for i,(no,t,d) in enumerate(toc):
|
||||
col = i // 4; row = i % 4
|
||||
x = 0.7 + col*(cw+0.35); y = 1.62 + row*1.28
|
||||
rect(s, x, y, cw, 1.1, fill=CARD, line=LINE, line_w=1.0, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
|
||||
rect(s, x, y, 0.09, 1.1, fill=BLUE if col==0 else PURPLE, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
|
||||
text(s, x+0.28, y, 1.0, 1.1, no, size=26, color=(BLUE if col==0 else PURPLE), bold=True, anchor=MSO_ANCHOR.MIDDLE)
|
||||
text(s, x+1.3, y+0.24, cw-1.5, 0.4, t, size=15, color=INK, bold=True)
|
||||
text(s, x+1.3, y+0.64, cw-1.5, 0.34, d, size=9.5, color=MUTED)
|
||||
|
||||
# ================= 슬라이드 3 : 01 개요 =================
|
||||
s = slide()
|
||||
header(s, "OVERVIEW", "01. 개요 — DBMS · 형상관리 · DB 구성", 1)
|
||||
text(s, 0.55, 1.45, 12.2, 0.5,
|
||||
"킨텍스 자동전시시스템은 전시 생애주기(판매→AI 설계·시각화→공사 옥션→운영→경영분석) 전 과정을 단일 DB에 담는다. "
|
||||
"공간데이터를 애플리케이션과 분리하지 않고 PostGIS로 DB에 일원화하여 배치·배선·규정검증을 ST_* 연산으로 수행한다.",
|
||||
size=12, color=INK, line_spacing=1.12)
|
||||
# 좌: 스택 카드
|
||||
cards = [
|
||||
("DBMS", "PostgreSQL 15", "관계형 + 트랜잭션 정합", BLUE),
|
||||
("공간확장", "PostGIS", "POLYGON·POINT·LINESTRING 지오메트리", GREEN),
|
||||
("암호화", "pgcrypto", "gen_random_uuid() · 해시", PURPLE),
|
||||
("형상관리", "Flyway", "V1~V10 버전 마이그레이션(불변·순증)", AMBER),
|
||||
]
|
||||
for i,(k,v,d,c) in enumerate(cards):
|
||||
x = 0.55 + i*3.06
|
||||
card(s, x, 2.2, 2.86, 1.35)
|
||||
rect(s, x, 2.2, 2.86, 0.1, fill=c, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
|
||||
text(s, x+0.22, 2.4, 2.5, 0.3, k, size=10, color=c, bold=True)
|
||||
text(s, x+0.22, 2.72, 2.5, 0.4, v, size=15, color=INK, bold=True)
|
||||
text(s, x+0.22, 3.16, 2.5, 0.34, d, size=9, color=MUTED, line_spacing=1.05)
|
||||
# 통계 스트립
|
||||
stats = [(f"{TOTAL_TABLES}", "테이블"), (f"{TOTAL_COLS}", "컬럼"), (f"{len(AREAS)}", "주제영역"),
|
||||
(f"{len(SPATIAL_COLS)}", "공간 컬럼"), (f"{TOTAL_IDX}", "인덱스"), ("V1–V10", "Flyway")]
|
||||
for i,(v,l) in enumerate(stats):
|
||||
x = 0.55 + i*2.03
|
||||
rect(s, x, 3.85, 1.9, 1.0, fill=BLUE_LT, line=LINE, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
|
||||
text(s, x, 4.0, 1.9, 0.44, v, size=23, color=BLUE_DK, bold=True, align=PP_ALIGN.CENTER)
|
||||
text(s, x, 4.5, 1.9, 0.3, l, size=10, color=MUTED, align=PP_ALIGN.CENTER)
|
||||
# DB 구성 원칙
|
||||
bullets(s, 0.55, 5.15, 12.2, [
|
||||
("kintex 전용 DB", "GUARDiA 타 솔루션과 물리 DB 분리(kintex 전용 스키마). 접속·권한은 서비스 계정 최소권한."),
|
||||
("공간·업무 단일 저장", "부스 폴리곤·배선 라인·요율·관람 데이터를 한 트랜잭션 경계에서 일관 관리."),
|
||||
("멱등·순증 마이그레이션", "V7 이후는 CREATE IF NOT EXISTS·ADD COLUMN IF NOT EXISTS로 재적용 안전(운영 무중단)."),
|
||||
], size=11.5, gap=0.5)
|
||||
|
||||
# ================= 슬라이드 4 : 02 아키텍처 원칙 =================
|
||||
s = slide()
|
||||
header(s, "PRINCIPLES", "02. 데이터 아키텍처 원칙", 2)
|
||||
princ = [
|
||||
("행사 단위 격리(멀티테넌시)", BLUE,
|
||||
["물리 tenant_id 컬럼 대신 event_id + event_member(RBAC)로 논리 격리",
|
||||
"핵심 트랜잭션 테이블은 event_id FK·ON DELETE CASCADE로 행사 경계 정합",
|
||||
"hall·company·master_data 등 마스터는 전 행사 공유(참조 무결성)"]),
|
||||
("공간데이터 PostGIS 일원화", GREEN,
|
||||
["부스=POLYGON, 트렌치·비상구=POINT, 배선=MultiLineString (SRID 0 홀 로컬 m)",
|
||||
"GiST 공간 인덱스로 ST_Intersects/Area/Distance 고속 연산",
|
||||
"배치·배선 재저장 대비 booth_id 소프트 참조(감사·쿼터 보존)"]),
|
||||
("감사 추적·이력 보존", PURPLE,
|
||||
["audit_log에 actor·action·target·ruleset_version·result 기록",
|
||||
"render_job은 이력·쿼터 정본(부스 교체와 무관하게 감사 보존)",
|
||||
"layout·design_plan·booth_standard 버전(version) 관리"]),
|
||||
("보안 내재화", AMBER,
|
||||
["password_hash·otp_secret·code_hash 컬럼은 API 응답/로그 완전 제외",
|
||||
"민감 시크릿 설정은 sys_setting.secret_yn='Y'로 마스킹",
|
||||
"error_message는 사용자 요약만(스택트레이스 저장 금지)"]),
|
||||
]
|
||||
for i,(title,c,items) in enumerate(princ):
|
||||
x = 0.55 + (i%2)*6.15; y = 1.5 + (i//2)*2.6
|
||||
card(s, x, y, 5.95, 2.4)
|
||||
rect(s, x, y, 0.11, 2.4, fill=c, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
|
||||
text(s, x+0.35, y+0.2, 5.4, 0.4, title, size=14, color=INK, bold=True)
|
||||
yy = y+0.72
|
||||
for it in items:
|
||||
rect(s, x+0.4, yy+0.06, 0.1, 0.1, fill=c, shape=MSO_SHAPE.OVAL)
|
||||
text(s, x+0.62, yy, 5.15, 0.5, it, size=10.3, color=MUTED, line_spacing=1.05)
|
||||
yy += 0.52
|
||||
|
||||
# ================= 슬라이드 5 : 03 개념 데이터 모델 =================
|
||||
s = slide()
|
||||
header(s, "CONCEPTUAL", "03. 개념 데이터 모델 — 주제영역 관계 개요", 3)
|
||||
text(s, 0.55, 1.42, 12.2, 0.4,
|
||||
"구현된 4개 주제영역과 소유 데이터의 관계. 행사(EVENT)를 축으로 공간·설계 코어와 공통·시스템 레이어가 결합한다.",
|
||||
size=11.5, color=MUTED)
|
||||
# 중앙 EVENT 허브
|
||||
hub = {"cx":6.6, "cy":3.9}
|
||||
rect(s, 5.75, 3.3, 1.7, 1.2, fill=BLUE_DK, shape=MSO_SHAPE.ROUNDED_RECTANGLE, shadow=True)
|
||||
text(s, 5.75, 3.3, 1.7, 1.2, [[{"t":"EVENT","size":15,"color":WHITE,"bold":True}],
|
||||
[{"t":"행사(테넌트 축)","size":9,"color":RGBColor(0xC7,0xDD,0xF2)}]], align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)
|
||||
sa = [
|
||||
("MASTER","행사·조직·권한·마스터","event · hall · company · event_member",8,BLUE, 0.7, 1.95),
|
||||
("SPATIAL","공간·설계·시각화","layout · booth · utility_order · render_job",7,GREEN, 9.35, 1.95),
|
||||
("COMMON","공통 업무 모듈","worklog · schedule · notice · meeting …",11,PURPLE, 0.7, 4.75),
|
||||
("SYSTEM","시스템관리·인증","sys_* · common_code · audit_log · password_reset",9,AMBER, 9.35, 4.75),
|
||||
]
|
||||
for code,ko,ex,n,c,x,y in sa:
|
||||
# 연결선 (먼저)
|
||||
box_cx = x+1.6; box_cy = y+0.75
|
||||
cn = s.shapes.add_connector(MSO_CONNECTOR.STRAIGHT, inch(box_cx), inch(box_cy), inch(hub["cx"]), inch(hub["cy"]))
|
||||
cn.line.color.rgb = RGBColor(0xC2,0xCC,0xDA); cn.line.width = Pt(1.6); cn.shadow.inherit=False
|
||||
for code,ko,ex,n,c,x,y in sa:
|
||||
card(s, x, y, 3.2, 1.5)
|
||||
rect(s, x, y, 3.2, 0.12, fill=c, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
|
||||
text(s, x+0.24, y+0.24, 2.7, 0.34, ko, size=13, color=INK, bold=True)
|
||||
chip(s, x+2.45, y+0.24, 0.62, 0.3, f"{n}T", c, WHITE, size=9)
|
||||
text(s, x+0.24, y+0.68, 2.9, 0.7, ex, size=9.3, color=MUTED, line_spacing=1.08)
|
||||
text(s, 0.55, 6.5, 12.2, 0.4,
|
||||
"관계 요지: EVENT 1─N HallAssignment/EventMember/Layout/Notice/Worklog(event_id). Layout 1─N Booth(POLYGON) 1─N DesignPlan·UtilityOrder(배선)·RenderJob. Company·User ─ EventMember로 행사 RBAC 성립.",
|
||||
size=9.8, color=INK, line_spacing=1.1)
|
||||
|
||||
# ================= 슬라이드 6~9 : 04 논리 모델 ERD (주제영역별) =================
|
||||
def erd_slide(area_ko, idx, placements, extra_note, cross_notes=None):
|
||||
s = slide()
|
||||
header(s, "LOGICAL ERD", f"04. 논리 모델 — {area_ko}", 4)
|
||||
# 연결선 먼저(엔티티 아래로 깔림)
|
||||
boxes = {}
|
||||
# 1차: 좌표만 계산해 중심 확보
|
||||
for tn, x, y, w in placements:
|
||||
t = TABLES[tn]
|
||||
pk=[c for c in t["cols"] if c["pk"]]; fk=[c for c in t["cols"] if c["fk"] and not c["pk"]]
|
||||
rest=[c for c in t["cols"] if not c["pk"] and not c["fk"]]
|
||||
show=(pk+fk+rest); show=show[:6]
|
||||
h = 0.5+len(show)*0.26
|
||||
boxes[tn]={"x":x,"y":y,"w":w,"h":h,"cx":x+w/2,"cy":y+h/2}
|
||||
for tn,_,_,_ in placements:
|
||||
for c in TABLES[tn]["cols"]:
|
||||
if c["fk"] and c["fk"] in boxes and c["fk"]!=tn:
|
||||
connect(s, boxes[tn], boxes[c["fk"]])
|
||||
accents={"MASTER":BLUE,"SPATIAL":GREEN,"SYSTEM":AMBER,"COMMON":PURPLE}
|
||||
acc = accents.get(idx, BLUE)
|
||||
for tn,x,y,w in placements:
|
||||
erd_entity(s, x, y, w, tn, accent=acc)
|
||||
if extra_note:
|
||||
text(s, 0.55, 6.78, 12.2, 0.3, extra_note, size=9, color=MUTED, line_spacing=1.05)
|
||||
return s
|
||||
|
||||
# ERD-1 MASTER
|
||||
erd_slide("행사·조직·권한·마스터", "MASTER", [
|
||||
("event", 5.6, 1.5, 2.5),
|
||||
("hall", 9.9, 1.5, 2.8),
|
||||
("hall_assignment", 9.9, 4.05, 2.8),
|
||||
("event_member", 5.6, 3.7, 2.85),
|
||||
("app_user", 0.6, 1.5, 2.75),
|
||||
("company", 0.6, 4.55, 2.75),
|
||||
], "관계: event 1─N hall_assignment·event_member. app_user·company ─ event_member(행사 RBAC). 독립 마스터 booth_standard·master_data 는 물리 모델 목록(슬라이드 10) 참조.")
|
||||
|
||||
# ERD-2 SPATIAL
|
||||
erd_slide("공간·설계·시각화(부스 코어)", "SPATIAL", [
|
||||
("hall", 0.6, 1.5, 2.5),
|
||||
("trench", 0.6, 4.2, 2.5),
|
||||
("hall_exit", 3.45, 4.2, 2.4),
|
||||
("layout", 3.6, 1.5, 2.5),
|
||||
("booth", 6.55, 1.5, 2.6),
|
||||
("design_plan", 9.65, 1.5, 2.7),
|
||||
("utility_order", 6.55, 4.35,2.9),
|
||||
("render_job", 9.85, 4.35,2.75),
|
||||
], "관계: hall 1─N trench·hall_exit·layout. layout 1─N booth(POLYGON) 1─N design_plan. booth ─ utility_order(배선 LineString)·render_job(AI 샷). booth_id 소프트 참조.")
|
||||
|
||||
# ERD-3 SYSTEM
|
||||
erd_slide("시스템관리·인증", "SYSTEM", [
|
||||
("common_code_group", 0.6, 1.5, 2.7),
|
||||
("common_code", 0.6, 4.0, 2.7),
|
||||
("sys_role", 3.6, 1.5, 2.6),
|
||||
("sys_role_permission",3.6,4.0, 2.75),
|
||||
("sys_permission", 6.55, 1.5, 2.6),
|
||||
("sys_menu", 9.6, 1.5, 2.8),
|
||||
("sys_setting", 6.55, 4.05, 2.6),
|
||||
("audit_log", 9.6, 4.05, 2.8),
|
||||
], "관계: common_code_group 1─N common_code. sys_role ─ sys_role_permission ─ sys_permission(M:N). sys_menu 자기참조(parent_id). password_reset(공개 인증)은 별도. audit_log 전 모듈 공유.")
|
||||
|
||||
# ERD-4 COMMON
|
||||
s = slide()
|
||||
header(s, "LOGICAL ERD", "04. 논리 모델 — 공통 업무 모듈", 4)
|
||||
# 부모-자식 3쌍 + 독립 목록
|
||||
pairs = [("message","message_recipient"),("opinion","opinion_comment"),("meeting","meeting_action")]
|
||||
px = 0.6
|
||||
for i,(p,ch) in enumerate(pairs):
|
||||
x = 0.6 + i*4.15
|
||||
bp = erd_entity(s, x, 1.5, 3.0, p, accent=PURPLE)
|
||||
bc = erd_entity(s, x+0.3, 3.9, 3.0, ch, accent=RGBColor(0x8B,0x6B,0xE0))
|
||||
# 연결(부모→자식) 먼저 그리지 못했으니 라벨로 대체
|
||||
text(s, 0.6, 6.05, 11.9, 0.3,
|
||||
"부모 1─N 자식(FK·ON DELETE CASCADE): message→message_recipient · opinion→opinion_comment · meeting→meeting_action",
|
||||
size=9.5, color=TEAL, bold=True)
|
||||
# 독립 테이블 스트립
|
||||
indep = ["worklog","schedule","notice","report","notification"]
|
||||
text(s, 0.6, 6.42, 3, 0.3, "행사 스코프 독립 테이블", size=10, color=MUTED, bold=True)
|
||||
for i,tn in enumerate(indep):
|
||||
x = 3.0 + i*1.85
|
||||
rect(s, x, 6.4, 1.75, 0.4, fill=BLUE_LT, line=LINE, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
|
||||
text(s, x, 6.4, 1.75, 0.4, tn, size=9.5, color=BLUE_DK, bold=True, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)
|
||||
|
||||
# ================= 슬라이드 10 : 05 물리 모델 요약 (테이블 그룹) =================
|
||||
s = slide()
|
||||
header(s, "PHYSICAL", "05. 물리 모델 요약 — 테이블 그룹", 5)
|
||||
col_x = [0.55, 6.7]
|
||||
for gi,(code,ko,tlist) in enumerate(AREAS):
|
||||
x = col_x[gi%2]; y = 1.5 + (gi//2)*2.65
|
||||
card(s, x, y, 5.9, 2.45)
|
||||
acc = {"MASTER":BLUE,"SPATIAL":GREEN,"SYSTEM":AMBER,"COMMON":PURPLE}[code]
|
||||
rect(s, x, y, 5.9, 0.5, fill=acc, shape=MSO_SHAPE.ROUND_2_SAME_RECTANGLE)
|
||||
text(s, x+0.25, y, 4.8, 0.5, ko, size=13, color=WHITE, bold=True, anchor=MSO_ANCHOR.MIDDLE)
|
||||
text(s, x+5.0, y, 0.7, 0.5, f"{len(tlist)}T", size=13, color=WHITE, bold=True, align=PP_ALIGN.RIGHT, anchor=MSO_ANCHOR.MIDDLE)
|
||||
yy = y+0.62
|
||||
for j,tn in enumerate(tlist):
|
||||
cx = x+0.25 + (j%2)*2.9
|
||||
cyy = yy + (j//2)*0.315
|
||||
text(s, cx, cyy, 2.85, 0.3, [[{"t":"· ","color":acc,"bold":True,"size":10},
|
||||
{"t":tn,"size":9.3,"color":INK,"bold":True},
|
||||
{"t":" "+KO.get(tn,""),"size":7.8,"color":MUTED}]], anchor=MSO_ANCHOR.MIDDLE)
|
||||
|
||||
# ================= 슬라이드 11 : 05b 공간 컬럼·인덱스 전략 =================
|
||||
s = slide()
|
||||
header(s, "PHYSICAL — SPATIAL & INDEX", "05. 공간 컬럼 · 인덱스 전략", 5)
|
||||
# 공간 컬럼 표
|
||||
text(s, 0.55, 1.45, 6, 0.32, "공간(PostGIS) 컬럼", size=13, color=GREEN, bold=True)
|
||||
sp_rows = [("테이블","컬럼","지오메트리","용도")]
|
||||
sp_meta = {
|
||||
("trench","geom"):"급전/급수/네트워크 공급 포인트",
|
||||
("hall_exit","geom"):"비상구 위치(이격 버퍼 검증)",
|
||||
("booth","geom"):"부스 경계 폴리곤(면적·간섭)",
|
||||
("utility_order","wiring"):"전기/네트워크/급배수 배선 경로",
|
||||
}
|
||||
for tn,cn,ty in SPATIAL_COLS:
|
||||
sp_rows.append((tn, cn, ty, sp_meta.get((tn,cn),"")))
|
||||
ry = 1.85
|
||||
for i,row in enumerate(sp_rows):
|
||||
fill = BLUE if i==0 else (WHITE if i%2 else BLUE_LT)
|
||||
tc = WHITE if i==0 else INK
|
||||
rect(s, 0.55, ry, 6.1, 0.4, fill=fill, line=LINE)
|
||||
xs=[0.65,2.05,3.35,4.75]; ws=[1.35,1.25,1.4,1.85]
|
||||
for k,val in enumerate(row):
|
||||
text(s, xs[k], ry, ws[k], 0.4, val, size=8.6, color=(tc if k<3 else (tc if i==0 else MUTED)),
|
||||
bold=(i==0 or k==0), anchor=MSO_ANCHOR.MIDDLE)
|
||||
ry += 0.4
|
||||
text(s, 0.55, ry+0.1, 6.1, 0.6,
|
||||
"SRID 0 = 홀 로컬 평면(미터). ST_Area/Distance/Length 결과 단위 = m·㎡.",
|
||||
size=9, color=MUTED, line_spacing=1.1)
|
||||
# 인덱스 전략
|
||||
text(s, 6.95, 1.45, 5.8, 0.32, "인덱스 전략", size=13, color=BLUE, bold=True)
|
||||
idx_items = [
|
||||
("GiST 공간 인덱스", f"geometry 컬럼 4종(booth·trench·hall_exit·utility_order.wiring) 전부 GiST"),
|
||||
("B-Tree 조회 인덱스", "FK·정렬 축: writer_id+work_date, recipient_id, event_id 등"),
|
||||
("복합 인덱스", "render_job(booth_id,shot_preset) · (event_id,status) 쿼터·상태 조회"),
|
||||
("정렬 최적화", "audit_log(created_at DESC) · notice(event_id,pinned DESC,created_at DESC)"),
|
||||
(f"총 인덱스 {TOTAL_IDX}개", "PK 자동 인덱스 별도 · 공간 4 · B-Tree 다수"),
|
||||
]
|
||||
yy = 1.9
|
||||
for k,v in idx_items:
|
||||
card(s, 6.95, yy, 5.75, 0.78)
|
||||
text(s, 7.15, yy+0.12, 5.4, 0.3, k, size=11, color=INK, bold=True)
|
||||
text(s, 7.15, yy+0.44, 5.4, 0.3, v, size=8.8, color=MUTED, line_spacing=1.05)
|
||||
yy += 0.9
|
||||
|
||||
# ================= 슬라이드 12 : 06 데이터 표준 =================
|
||||
s = slide()
|
||||
header(s, "STANDARDS", "06. 데이터 표준 — 명명 · 코드 · 날짜/통화", 6)
|
||||
std = [
|
||||
("명명 규칙(실제 DDL 관찰)", BLUE, [
|
||||
"테이블·컬럼 = snake_case 단수형 (app_user, booth, render_job)",
|
||||
"PK = id (varchar 애플리케이션 채번) / 대량 로그 = bigserial (audit_log·password_reset)",
|
||||
"FK = <참조테이블>_id (event_id, booth_id, hall_id, layout_id)",
|
||||
"불리언 = is_/has_/*_yn 혼용 · 금액/치수 = numeric(정밀도,스케일)",
|
||||
"타임스탬프 = *_at (timestamptz) / 날짜 = *_date (date)",
|
||||
]),
|
||||
("공통코드 체계(common_code)", PURPLE, [
|
||||
"grp_code(그룹) 1─N code — 14개 그룹 시드(USE_YN·USER_ROLE·WORK_STATUS …)",
|
||||
"화면 드롭다운·상태값을 코드로 표준화 (하드코딩 배제)",
|
||||
"정렬 sort_order · 사용여부 use_yn(Y/N) · 확장 attr1",
|
||||
]),
|
||||
("날짜·통화·단위 표준", GREEN, [
|
||||
"일시 = timestamptz(UTC 저장, 표시 KST) · 날짜 = date",
|
||||
"통화 = KRW 정수(원) 원칙, 요율/정산은 numeric — 부동소수 금지",
|
||||
"치수 = 미터(m)·면적 ㎡ · 하중 t/㎡ (numeric 고정 스케일)",
|
||||
]),
|
||||
]
|
||||
yy = 1.5
|
||||
for title,c,items in std:
|
||||
h = 0.5 + len(items)*0.34 + 0.15
|
||||
card(s, 0.55, yy, 12.2, h)
|
||||
rect(s, 0.55, yy, 0.11, h, fill=c, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
|
||||
text(s, 0.85, yy+0.16, 11.6, 0.34, title, size=13.5, color=INK, bold=True)
|
||||
iy = yy+0.6
|
||||
for it in items:
|
||||
rect(s, 0.95, iy+0.06, 0.09, 0.09, fill=c, shape=MSO_SHAPE.OVAL)
|
||||
text(s, 1.14, iy, 11.4, 0.32, it, size=10.3, color=MUTED)
|
||||
iy += 0.34
|
||||
yy += h + 0.16
|
||||
|
||||
# ================= 슬라이드 13 : 07 데이터 보안 =================
|
||||
s = slide()
|
||||
header(s, "SECURITY", "07. 데이터 보안 — 암호화 · PII · 응답제외 · 감사", 7)
|
||||
# 응답 제외/암호화 컬럼 표
|
||||
text(s, 0.55, 1.45, 7, 0.32, "민감 컬럼 처리 정책", size=13, color=BLUE, bold=True)
|
||||
sec_rows = [("테이블.컬럼","정책")]
|
||||
sec_rows += [
|
||||
("app_user.password_hash","BCrypt 해시 · API 응답/로그 완전 제외"),
|
||||
("app_user.otp_secret","TOTP 시크릿 · AES-256-GCM 암호화 저장 · 응답 제외"),
|
||||
("password_reset.code_hash","BCrypt(6자리) · 만료 10분·1회성 · 응답/로그 제외"),
|
||||
("sys_setting.secret_yn='Y'","시크릿 설정값 마스킹 반환"),
|
||||
("render_job.error_message","사용자 요약만 · 스택트레이스 저장 금지"),
|
||||
("booth.assigned_company_name","표시용(내부 식별자·민감정보 미포함)"),
|
||||
]
|
||||
ry = 1.85
|
||||
for i,(a,b) in enumerate(sec_rows):
|
||||
fill = BLUE if i==0 else (WHITE if i%2 else BLUE_LT)
|
||||
tc = WHITE if i==0 else INK
|
||||
rect(s, 0.55, ry, 6.5, 0.52, fill=fill, line=LINE)
|
||||
text(s, 0.68, ry, 2.75, 0.52, a, size=8.4, color=tc, bold=(i==0), anchor=MSO_ANCHOR.MIDDLE)
|
||||
text(s, 3.5, ry, 3.45, 0.52, b, size=8.3, color=(tc if i==0 else MUTED), bold=(i==0),
|
||||
anchor=MSO_ANCHOR.MIDDLE, line_spacing=1.0)
|
||||
ry += 0.52
|
||||
# 우측 원칙 카드
|
||||
text(s, 7.3, 1.45, 5.4, 0.32, "보안 불변 원칙", size=13, color=PURPLE, bold=True)
|
||||
sec_cards = [
|
||||
("PII 최소화·격리", "관람/리드 PII 집중영역 접근 제한 · BI 마트는 집계/익명키만 반입(개인식별자 0)"),
|
||||
("암호화 저장", "비밀번호·OTP·재설정코드는 해시/AES-256-GCM · 평문 저장 금지"),
|
||||
("API 응답 스키마 통제", "ServerOut류 민감 컬럼 응답 완전 제외 · 스택트레이스 미노출"),
|
||||
("감사 추적", "audit_log(actor·action·target·ruleset_version·result·ip_hint) 전 변경 기록"),
|
||||
("행사 격리 접근제어", "event_member 역할(ORGANIZER·EXHIBITOR·CONTRACTOR·HALL_MANAGER) 스코프"),
|
||||
]
|
||||
yy = 1.85
|
||||
for k,v in sec_cards:
|
||||
card(s, 7.3, yy, 5.45, 0.92)
|
||||
rect(s, 7.3, yy, 0.09, 0.92, fill=PURPLE, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
|
||||
text(s, 7.55, yy+0.13, 5.1, 0.3, k, size=11, color=INK, bold=True)
|
||||
text(s, 7.55, yy+0.45, 5.1, 0.42, v, size=8.7, color=MUTED, line_spacing=1.05)
|
||||
yy += 1.02
|
||||
|
||||
# ================= 슬라이드 14 : 08 백업·이관 전략 =================
|
||||
s = slide()
|
||||
header(s, "BACKUP & MIGRATION", "08. 백업 · 이관 전략 (Flyway 형상관리)", 8)
|
||||
mig = [
|
||||
("Flyway 버전 마이그레이션", BLUE, [
|
||||
"V1 확장 → V2 신원·마스터 → V3 공간코어 → V4~V6 시드 → V7 시스템관리 → V8 공통업무 → V9 공개인증 → V10 10년 시드",
|
||||
"적용 이력은 flyway_schema_history 테이블에 체크섬으로 기록·검증",
|
||||
"V1~V6 불변 원칙 · V7 이후 순증(additive)·멱등만 수행(운영 무중단)",
|
||||
]),
|
||||
("백업 전략", GREEN, [
|
||||
"일 1회 논리 백업(pg_dump) + WAL 아카이브 기반 PITR(시점복구)",
|
||||
"PostGIS 지오메트리·공간 인덱스 포함 전체 스키마 백업",
|
||||
"오브젝트 스토리지(렌더 이미지·도면)는 서명·만료 URL로 별도 보존",
|
||||
]),
|
||||
("이관·복구 절차", PURPLE, [
|
||||
"복구 = 최신 백업 복원 → Flyway migrate로 최신 버전까지 재적용(정본 순서 보장)",
|
||||
"환경 승격(dev→운영): 동일 마이그레이션 스크립트 적용으로 스키마 동형 보장",
|
||||
"시드(V4~V6·V10)와 스키마(V1~V3·V7~V9) 분리로 데이터 오염 없이 재구축",
|
||||
]),
|
||||
]
|
||||
yy = 1.5
|
||||
for title,c,items in mig:
|
||||
h = 0.5 + len(items)*0.4 + 0.1
|
||||
card(s, 0.55, yy, 12.2, h)
|
||||
rect(s, 0.55, yy, 0.11, h, fill=c, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
|
||||
text(s, 0.85, yy+0.16, 11.6, 0.34, title, size=13.5, color=INK, bold=True)
|
||||
iy = yy+0.62
|
||||
for it in items:
|
||||
rect(s, 0.95, iy+0.07, 0.09, 0.09, fill=c, shape=MSO_SHAPE.OVAL)
|
||||
text(s, 1.14, iy, 11.4, 0.38, it, size=10.2, color=MUTED, line_spacing=1.08)
|
||||
iy += 0.4
|
||||
yy += h + 0.16
|
||||
text(s, 0.55, 6.75, 12.2, 0.3,
|
||||
"보안 불변: 본 설계서·백업 매뉴얼에 비밀번호·API키·서버 IP·SSH 접속정보를 기재하지 않는다.",
|
||||
size=9, color=MUTED, bold=True)
|
||||
|
||||
# ---- 저장 ----
|
||||
PPTX_PATH = os.path.join(OUT_DIR, "데이터베이스설계서.pptx")
|
||||
prs.save(PPTX_PATH)
|
||||
N_SLIDES = len(prs.slides._sldIdLst)
|
||||
|
||||
# ======================================================================
|
||||
# 3. XLSX — 테이블정의서
|
||||
# ======================================================================
|
||||
import openpyxl
|
||||
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
|
||||
from openpyxl.utils import get_column_letter
|
||||
|
||||
BLUE_HEX = "0066B3"; BLUEDK_HEX = "00447A"; BAND_HEX = "E1EFF9"; ROW_HEX = "F4F8FC"
|
||||
thin = Side(style="thin", color="D0D7E2")
|
||||
BORDER = Border(left=thin, right=thin, top=thin, bottom=thin)
|
||||
HFONT = Font(name="맑은 고딕", size=10, bold=True, color="FFFFFF")
|
||||
BFONT = Font(name="맑은 고딕", size=10)
|
||||
BOLD = Font(name="맑은 고딕", size=10, bold=True, color="101828")
|
||||
TITLE_FONT = Font(name="맑은 고딕", size=11, bold=True, color="FFFFFF")
|
||||
HEAD_FILL = PatternFill("solid", fgColor=BLUE_HEX)
|
||||
BAND_FILL = PatternFill("solid", fgColor=BLUEDK_HEX)
|
||||
ALT_FILL = PatternFill("solid", fgColor=ROW_HEX)
|
||||
CEN = Alignment(horizontal="center", vertical="center", wrap_text=True)
|
||||
LEFT = Alignment(horizontal="left", vertical="center", wrap_text=True)
|
||||
|
||||
wb = openpyxl.Workbook()
|
||||
|
||||
# ---- 시트1: 테이블목록 ----
|
||||
ws = wb.active; ws.title = "테이블목록"
|
||||
ws.sheet_view.showGridLines = False
|
||||
cols1 = [("No",6),("테이블명",22),("한글명",24),("주제영역",24),("Flyway",10),("컬럼수",8),("비고",40)]
|
||||
for j,(h,w) in enumerate(cols1, start=1):
|
||||
c = ws.cell(1, j, h); c.font=HFONT; c.fill=HEAD_FILL; c.alignment=CEN; c.border=BORDER
|
||||
ws.column_dimensions[get_column_letter(j)].width = w
|
||||
ordered = sorted(TABLES.items(), key=lambda kv: kv[1]["order"])
|
||||
for i,(tn,t) in enumerate(ordered, start=1):
|
||||
note = []
|
||||
if any(c["type"].lower().startswith("geometry") for c in t["cols"]): note.append("PostGIS 공간")
|
||||
if t["pk"] and len(t["pk"])>1: note.append("복합 PK("+",".join(sorted(t["pk"]))+")")
|
||||
if t["uniques"]: note.append("UNIQUE("+"; ".join(t["uniques"])+")")
|
||||
if any(c["name"] in ("password_hash","otp_secret","code_hash") for c in t["cols"]): note.append("민감:응답제외")
|
||||
row = [i, tn, KO.get(tn,""), AREA_OF.get(tn,""), t["version"], len(t["cols"]), " · ".join(note)]
|
||||
for j,val in enumerate(row, start=1):
|
||||
c = ws.cell(i+1, j, val)
|
||||
c.font = BOLD if j==2 else BFONT
|
||||
c.alignment = CEN if j in (1,5,6) else LEFT
|
||||
c.border = BORDER
|
||||
if i%2==0: c.fill = ALT_FILL
|
||||
ws.freeze_panes = "A2"
|
||||
ws.auto_filter.ref = f"A1:G{len(ordered)+1}"
|
||||
|
||||
# ---- 주제영역별 시트 ----
|
||||
DATA_COLS = [("No",5),("컬럼명",26),("데이터타입",20),("NULL",7),("키",13),("기본값",18),("설명",56)]
|
||||
def key_str(c):
|
||||
parts=[]
|
||||
if c["pk"]: parts.append("PK")
|
||||
if c["fk"]: parts.append(f"FK→{c['fk']}")
|
||||
if c["unique"]: parts.append("U")
|
||||
return " ".join(parts)
|
||||
|
||||
for code, ko, tlist in AREAS:
|
||||
title = ko.split("(")[0].strip()
|
||||
sname = {"MASTER":"마스터·조직·권한","SPATIAL":"공간·설계·시각화",
|
||||
"SYSTEM":"시스템관리·인증","COMMON":"공통업무"}[code]
|
||||
ws = wb.create_sheet(sname)
|
||||
ws.sheet_view.showGridLines = False
|
||||
for j,(h,w) in enumerate(DATA_COLS, start=1):
|
||||
c = ws.cell(1, j, h); c.font=HFONT; c.fill=HEAD_FILL; c.alignment=CEN; c.border=BORDER
|
||||
ws.column_dimensions[get_column_letter(j)].width = w
|
||||
r = 2
|
||||
ncol = len(DATA_COLS)
|
||||
for tn in tlist:
|
||||
t = TABLES[tn]
|
||||
# 밴드 행(테이블 헤더)
|
||||
ws.merge_cells(start_row=r, start_column=1, end_row=r, end_column=ncol)
|
||||
bc = ws.cell(r, 1, f"◆ {tn} — {KO.get(tn,'')} [{t['version']}]"
|
||||
+ (" · 인덱스 "+", ".join(ix["name"] for ix in t["indexes"]) if t["indexes"] else ""))
|
||||
bc.font = TITLE_FONT; bc.fill = BAND_FILL; bc.alignment = LEFT
|
||||
for j in range(1, ncol+1):
|
||||
ws.cell(r, j).border = BORDER
|
||||
ws.cell(r, j).fill = BAND_FILL
|
||||
r += 1
|
||||
for k,c in enumerate(t["cols"], start=1):
|
||||
vals = [k, c["name"], c["type"], c["null"], key_str(c), c["default"], c["desc"]]
|
||||
for j,val in enumerate(vals, start=1):
|
||||
cc = ws.cell(r, j, val)
|
||||
cc.font = BOLD if (j==2 and c["pk"]) else BFONT
|
||||
cc.alignment = CEN if j in (1,4) else LEFT
|
||||
cc.border = BORDER
|
||||
if k%2==0: cc.fill = ALT_FILL
|
||||
if j==5 and c["fk"]:
|
||||
cc.font = Font(name="맑은 고딕", size=9, color="0E7C86", bold=True)
|
||||
r += 1
|
||||
r += 1 # 테이블 간 공백
|
||||
ws.freeze_panes = "A2"
|
||||
|
||||
XLSX_PATH = os.path.join(OUT_DIR, "테이블정의서.xlsx")
|
||||
wb.save(XLSX_PATH)
|
||||
|
||||
print("=" * 60)
|
||||
print(f"[PPTX] {PPTX_PATH}")
|
||||
print(f" 슬라이드 {N_SLIDES}장")
|
||||
print(f"[XLSX] {XLSX_PATH}")
|
||||
print(f" 시트 {len(wb.sheetnames)} : {wb.sheetnames}")
|
||||
print(f"[SCHEMA] 테이블 {TOTAL_TABLES} · 컬럼 {TOTAL_COLS} · 인덱스 {TOTAL_IDX} · 공간컬럼 {len(SPATIAL_COLS)} · FK {len(FK_EDGES)}")
|
||||
print("=" * 60)
|
||||
BIN
docs/deliverables/DA산출물/데이터베이스설계서.pptx
Normal file
BIN
docs/deliverables/DA산출물/데이터베이스설계서.pptx
Normal file
Binary file not shown.
BIN
docs/deliverables/DA산출물/테이블정의서.xlsx
Normal file
BIN
docs/deliverables/DA산출물/테이블정의서.xlsx
Normal file
Binary file not shown.
70
docs/deliverables/_gen/_docxutil.py
Normal file
70
docs/deliverables/_gen/_docxutil.py
Normal file
@ -0,0 +1,70 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""공용 docx 헬퍼 — 한글 폰트/표/제목 스타일. 각 문서 생성기가 import."""
|
||||
from docx.shared import Pt, Cm, RGBColor
|
||||
from docx.enum.text import WD_ALIGN_PARAGRAPH
|
||||
from docx.enum.table import WD_TABLE_ALIGNMENT
|
||||
from docx.oxml.ns import qn
|
||||
from docx.oxml import OxmlElement
|
||||
|
||||
NAVY = RGBColor(0x1F, 0x29, 0x3A)
|
||||
ACCENT = RGBColor(0x11, 0x6D, 0xC3)
|
||||
GREY = RGBColor(0x55, 0x5F, 0x6B)
|
||||
KFONT = "맑은 고딕"
|
||||
|
||||
def setup(doc):
|
||||
st = doc.styles["Normal"]; st.font.name = KFONT; st.font.size = Pt(10.5)
|
||||
_rf(st.element.get_or_add_rPr())
|
||||
for m in ("Heading 1","Heading 2","Heading 3"):
|
||||
doc.styles[m].font.name = KFONT; _rf(doc.styles[m].element.get_or_add_rPr())
|
||||
s = doc.sections[0]
|
||||
s.top_margin = Cm(2.2); s.bottom_margin = Cm(2.0); s.left_margin = Cm(2.3); s.right_margin = Cm(2.3)
|
||||
|
||||
def _rf(rpr):
|
||||
rf = rpr.find(qn('w:rFonts'))
|
||||
if rf is None:
|
||||
rf = OxmlElement('w:rFonts'); rpr.append(rf)
|
||||
for a in ('w:ascii','w:hAnsi','w:eastAsia','w:cs'):
|
||||
rf.set(qn(a), KFONT)
|
||||
|
||||
def shade(cell, hexcolor):
|
||||
tcpr = cell._tc.get_or_add_tcPr()
|
||||
sh = OxmlElement('w:shd'); sh.set(qn('w:val'),'clear'); sh.set(qn('w:fill'),hexcolor)
|
||||
tcpr.append(sh)
|
||||
|
||||
def h(doc, level, text):
|
||||
p = doc.add_heading(text, level=level)
|
||||
for run in p.runs:
|
||||
run.font.color.rgb = NAVY if level == 1 else ACCENT
|
||||
return p
|
||||
|
||||
def para(doc, text, size=10.5, bold=False, italic=False, color=None, align=None, space_after=6):
|
||||
p = doc.add_paragraph(); r = p.add_run(text); r.bold = bold; r.italic = italic
|
||||
r.font.size = Pt(size)
|
||||
if color: r.font.color.rgb = color
|
||||
if align: p.alignment = align
|
||||
p.paragraph_format.space_after = Pt(space_after)
|
||||
return p
|
||||
|
||||
def bullets(doc, items, style="List Bullet"):
|
||||
for it in items:
|
||||
p = doc.add_paragraph(it, style=style); p.paragraph_format.space_after = Pt(2)
|
||||
|
||||
def table(doc, headers, rows, widths=None, header_fill="1F293A", first_col_bold=False, fontsize=9.5):
|
||||
t = doc.add_table(rows=1, cols=len(headers)); t.style = "Table Grid"; t.alignment = WD_TABLE_ALIGNMENT.CENTER
|
||||
hd = t.rows[0].cells
|
||||
for i, htext in enumerate(headers):
|
||||
hd[i].text = ""; run = hd[i].paragraphs[0].add_run(htext)
|
||||
run.bold = True; run.font.size = Pt(fontsize); run.font.color.rgb = RGBColor(0xFF,0xFF,0xFF)
|
||||
shade(hd[i], header_fill)
|
||||
for row in rows:
|
||||
cells = t.add_row().cells
|
||||
for i, val in enumerate(row):
|
||||
cells[i].text = ""; run = cells[i].paragraphs[0].add_run(str(val)); run.font.size = Pt(fontsize)
|
||||
if first_col_bold and i == 0: run.bold = True
|
||||
cells[i].paragraphs[0].paragraph_format.space_after = Pt(1)
|
||||
if widths:
|
||||
for r_ in t.rows:
|
||||
for i, w in enumerate(widths):
|
||||
r_.cells[i].width = Cm(w)
|
||||
doc.add_paragraph().paragraph_format.space_after = Pt(4)
|
||||
return t
|
||||
785
docs/deliverables/_gen/gen_dev_plan_pptx.py
Normal file
785
docs/deliverables/_gen/gen_dev_plan_pptx.py
Normal file
@ -0,0 +1,785 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
킨텍스 자동전시시스템 구축 — 개발계획서 PPTX 생성기
|
||||
- 소스: docs/PLANNING.md(v3.0), docs/IMPLEMENTATION_BACKLOG.md, docs/WORK_STATUS.md,
|
||||
docs/architecture/*, CLAUDE.md 기술 스택표
|
||||
- 디자인: KINTEX Blue #0066B3 / AI Purple #6D4AFF / 중립 그레이 / 맑은 고딕 / 16:9
|
||||
- 도식(추진체계도·시스템 구성도·간트)은 python-pptx 도형으로 직접 그림(외부 이미지 금지)
|
||||
- 보안 불변: 비밀번호/API키/서버IP/SSH 계정 미기재
|
||||
재실행: python gen_dev_plan_pptx.py
|
||||
"""
|
||||
import os
|
||||
from pptx import Presentation
|
||||
from pptx.util import Emu, Pt
|
||||
from pptx.dml.color import RGBColor
|
||||
from pptx.enum.text import PP_ALIGN, MSO_ANCHOR
|
||||
from pptx.enum.shapes import MSO_SHAPE
|
||||
from pptx.enum.text import MSO_AUTO_SIZE
|
||||
from pptx.oxml.ns import qn
|
||||
|
||||
# ---------------- 팔레트 ----------------
|
||||
BLUE = RGBColor(0x00, 0x66, 0xB3) # KINTEX Blue (primary)
|
||||
BLUE_DK = RGBColor(0x00, 0x44, 0x7A) # deep
|
||||
BLUE_LT = RGBColor(0xE1, 0xEF, 0xF9) # light tint
|
||||
PURPLE = RGBColor(0x6D, 0x4A, 0xFF) # AI accent
|
||||
PURPLE_LT = RGBColor(0xEC, 0xE8, 0xFF)
|
||||
INK = RGBColor(0x10, 0x18, 0x28) # text
|
||||
MUTED = RGBColor(0x66, 0x70, 0x85) # secondary text
|
||||
LINE = RGBColor(0xE4, 0xE7, 0xEC) # borders
|
||||
BG = RGBColor(0xF9, 0xFA, 0xFB) # page bg
|
||||
WHITE = RGBColor(0xFF, 0xFF, 0xFF)
|
||||
GREEN = RGBColor(0x12, 0x9E, 0x63) # 완료
|
||||
AMBER = RGBColor(0xE0, 0x8A, 0x00) # 진행
|
||||
SLATE = RGBColor(0x98, 0xA2, 0xB3) # 예정
|
||||
CARD = RGBColor(0xFF, 0xFF, 0xFF)
|
||||
HALL_BG = RGBColor(0xF2, 0xF4, 0xF7)
|
||||
|
||||
FONT = "맑은 고딕"
|
||||
|
||||
# 16:9
|
||||
EMU_W = 12192000
|
||||
EMU_H = 6858000
|
||||
|
||||
prs = Presentation()
|
||||
prs.slide_width = EMU_W
|
||||
prs.slide_height = EMU_H
|
||||
BLANK = prs.slide_layouts[6]
|
||||
|
||||
IN = 914400
|
||||
def inch(v): return int(v * IN)
|
||||
|
||||
# ---------------- 헬퍼 ----------------
|
||||
def slide():
|
||||
return prs.slides.add_slide(BLANK)
|
||||
|
||||
def _set_fill(shape, color):
|
||||
if color is None:
|
||||
shape.fill.background()
|
||||
else:
|
||||
shape.fill.solid()
|
||||
shape.fill.fore_color.rgb = color
|
||||
|
||||
def _no_line(shape):
|
||||
shape.line.fill.background()
|
||||
|
||||
def rect(s, x, y, w, h, fill=None, line=None, line_w=1.0, shape=MSO_SHAPE.RECTANGLE, shadow=False):
|
||||
sp = s.shapes.add_shape(shape, inch(x), inch(y), inch(w), inch(h))
|
||||
_set_fill(sp, fill)
|
||||
if line is None:
|
||||
_no_line(sp)
|
||||
else:
|
||||
sp.line.color.rgb = line
|
||||
sp.line.width = Pt(line_w)
|
||||
sp.shadow.inherit = False # creates an empty <a:effectLst/>
|
||||
if shadow:
|
||||
el = sp._element.spPr
|
||||
ef = el.find(qn('a:effectLst'))
|
||||
if ef is None:
|
||||
ef = el.makeelement(qn('a:effectLst'), {})
|
||||
el.append(ef)
|
||||
sh = ef.makeelement(qn('a:outerShdw'),
|
||||
{'blurRad':'90000','dist':'38100','dir':'5400000','rotWithShape':'0'})
|
||||
clr = sh.makeelement(qn('a:srgbClr'), {'val':'101828'})
|
||||
alp = clr.makeelement(qn('a:alpha'), {'val':'18000'})
|
||||
clr.append(alp); sh.append(clr); ef.append(sh)
|
||||
return sp
|
||||
|
||||
def _apply_runs(tf, para, segs, align, default_size, default_color, default_bold):
|
||||
para.alignment = align
|
||||
first = True
|
||||
for seg in segs:
|
||||
text = seg.get("t","")
|
||||
r = para.add_run() if not first else para.runs[0] if para.runs else para.add_run()
|
||||
# ensure a run exists
|
||||
if first and not para.runs:
|
||||
r = para.add_run()
|
||||
r.text = text
|
||||
r.font.name = FONT
|
||||
r.font.size = Pt(seg.get("size", default_size))
|
||||
r.font.bold = seg.get("bold", default_bold)
|
||||
r.font.color.rgb = seg.get("color", default_color)
|
||||
# set east asian font
|
||||
rPr = r._r.get_or_add_rPr()
|
||||
ea = rPr.makeelement(qn('a:ea'), {'typeface':FONT})
|
||||
rPr.append(ea)
|
||||
first = False
|
||||
|
||||
def text(s, x, y, w, h, content, size=14, color=INK, bold=False, align=PP_ALIGN.LEFT,
|
||||
anchor=MSO_ANCHOR.TOP, line_spacing=1.0, space_after=2, wrap=True):
|
||||
"""content: str 또는 list[para], para = str 또는 list[seg], seg={'t','size','color','bold'}"""
|
||||
tb = s.shapes.add_textbox(inch(x), inch(y), inch(w), inch(h))
|
||||
tf = tb.text_frame
|
||||
tf.word_wrap = wrap
|
||||
tf.vertical_anchor = anchor
|
||||
tf.margin_left = 0; tf.margin_right = 0; tf.margin_top = 0; tf.margin_bottom = 0
|
||||
if isinstance(content, str):
|
||||
content = [content]
|
||||
for i, para in enumerate(content):
|
||||
p = tf.paragraphs[0] if i == 0 else tf.add_paragraph()
|
||||
p.alignment = align
|
||||
p.line_spacing = line_spacing
|
||||
p.space_after = Pt(space_after)
|
||||
p.space_before = Pt(0)
|
||||
if isinstance(para, str):
|
||||
segs = [{"t": para}]
|
||||
elif isinstance(para, dict):
|
||||
segs = [para]
|
||||
else:
|
||||
segs = para
|
||||
for seg in segs:
|
||||
r = p.add_run()
|
||||
r.text = seg.get("t","")
|
||||
r.font.name = FONT
|
||||
r.font.size = Pt(seg.get("size", size))
|
||||
r.font.bold = seg.get("bold", bold)
|
||||
r.font.color.rgb = seg.get("color", color)
|
||||
rPr = r._r.get_or_add_rPr()
|
||||
ea = rPr.makeelement(qn('a:ea'), {'typeface':FONT})
|
||||
rPr.append(ea)
|
||||
return tb
|
||||
|
||||
def page_bg(s, color=BG):
|
||||
rect(s, -0.1, -0.1, 13.53, 7.7, fill=color)
|
||||
|
||||
PAGE = {"n": 0}
|
||||
def content_header(s, kicker, title, idx):
|
||||
"""본문 슬라이드 공통 헤더 + 푸터"""
|
||||
page_bg(s, BG)
|
||||
# top accent bar
|
||||
rect(s, 0, 0, 13.333, 0.12, fill=BLUE)
|
||||
# section number chip
|
||||
rect(s, 0.55, 0.5, 0.62, 0.62, fill=BLUE, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
|
||||
text(s, 0.55, 0.5, 0.62, 0.62, f"{idx:02d}", size=20, color=WHITE, bold=True,
|
||||
align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)
|
||||
text(s, 1.32, 0.5, 10.5, 0.28, kicker, size=11, color=PURPLE, bold=True)
|
||||
text(s, 1.32, 0.74, 11.4, 0.44, title, size=22, color=INK, bold=True)
|
||||
rect(s, 0.55, 1.28, 12.23, 0.02, fill=LINE)
|
||||
# footer
|
||||
text(s, 0.55, 7.06, 8, 0.3, "킨텍스 자동전시시스템 구축 · 개발계획서", size=8.5, color=MUTED)
|
||||
text(s, 11.3, 7.06, 1.48, 0.3, "2026-07-11", size=8.5, color=MUTED, align=PP_ALIGN.RIGHT)
|
||||
|
||||
def chip(s, x, y, w, h, label, fill, txtcolor=WHITE, size=9.5, bold=True):
|
||||
rect(s, x, y, w, h, fill=fill, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
|
||||
text(s, x, y, w, h, label, size=size, color=txtcolor, bold=bold,
|
||||
align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)
|
||||
|
||||
def card(s, x, y, w, h, fill=CARD, line=LINE):
|
||||
return rect(s, x, y, w, h, fill=fill, line=line, line_w=1.0,
|
||||
shape=MSO_SHAPE.ROUNDED_RECTANGLE, shadow=True)
|
||||
|
||||
# ================================================================
|
||||
# 1. 표지
|
||||
# ================================================================
|
||||
s = slide()
|
||||
rect(s, -0.1, -0.1, 13.53, 7.7, fill=BLUE_DK)
|
||||
# 대각 그라디언트 느낌: 겹치는 반투명 도형
|
||||
rect(s, 6.4, -1.5, 9, 11, fill=BLUE, shape=MSO_SHAPE.PARALLELOGRAM)
|
||||
rect(s, 9.2, -2.0, 7, 12, fill=PURPLE, shape=MSO_SHAPE.PARALLELOGRAM)
|
||||
# 좌상 브랜드
|
||||
rect(s, 0.85, 0.8, 0.42, 0.42, fill=WHITE, shape=MSO_SHAPE.OVAL)
|
||||
rect(s, 0.97, 0.92, 0.18, 0.18, fill=PURPLE, shape=MSO_SHAPE.OVAL)
|
||||
text(s, 1.42, 0.78, 6, 0.46, "KINTEX · WISE AI", size=15, color=WHITE, bold=True, anchor=MSO_ANCHOR.MIDDLE)
|
||||
# 문서 종류 배지
|
||||
chip(s, 0.85, 2.35, 1.75, 0.42, "개발계획서", PURPLE, WHITE, size=12)
|
||||
chip(s, 2.72, 2.35, 2.1, 0.42, "Development Plan", RGBColor(0x2B,0x4C,0x7E), WHITE, size=10.5)
|
||||
# 타이틀
|
||||
text(s, 0.85, 2.95, 11, 1.9, [
|
||||
[{"t":"킨텍스 자동전시시스템", "size":46, "color":WHITE, "bold":True}],
|
||||
[{"t":"구축 개발계획서", "size":46, "color":WHITE, "bold":True}],
|
||||
], line_spacing=1.02)
|
||||
text(s, 0.88, 4.78, 11, 0.5,
|
||||
"Exhibition Automation Platform — 다중 전시관 멀티테넌트 SaaS",
|
||||
size=16, color=RGBColor(0xC7,0xDD,0xF2), bold=False)
|
||||
# 폐루프 한 줄
|
||||
text(s, 0.88, 5.35, 11.4, 0.4,
|
||||
"판매 → AI 설계·시각화 → 공사 옥션 → 시공 → 운영 → 경영분석 폐루프 자동화",
|
||||
size=12.5, color=RGBColor(0xB9,0xC7,0xEA))
|
||||
# 하단 메타 바
|
||||
rect(s, 0.85, 6.15, 11.63, 0.02, fill=RGBColor(0x3A,0x5A,0x8C))
|
||||
text(s, 0.85, 6.35, 8, 0.4, [
|
||||
[{"t":"사업명 ","size":11,"color":RGBColor(0x8FA,0x00,0x00) if False else RGBColor(0x90,0xA6,0xC9)},
|
||||
{"t":"킨텍스 자동전시시스템 구축","size":11.5,"color":WHITE,"bold":True}],
|
||||
])
|
||||
text(s, 0.85, 6.72, 9, 0.4, [
|
||||
[{"t":"기술스택 ","size":11,"color":RGBColor(0x90,0xA6,0xC9)},
|
||||
{"t":"React(Vite)+TS · Spring Boot 3.x(Java17)+MyBatis · PostgreSQL(PostGIS) · Redis · 나노바나나 워커 · Claude AI","size":10.5,"color":WHITE}],
|
||||
])
|
||||
text(s, 9.7, 6.35, 2.78, 0.4, "작성일 2026-07-11", size=11, color=WHITE, bold=True, align=PP_ALIGN.RIGHT)
|
||||
text(s, 9.7, 6.72, 2.78, 0.4, "planner · designer · dev team", size=9.5, color=RGBColor(0x90,0xA6,0xC9), align=PP_ALIGN.RIGHT)
|
||||
|
||||
# ================================================================
|
||||
# 2. 목차
|
||||
# ================================================================
|
||||
s = slide()
|
||||
content_header(s, "CONTENTS", "목차", 0)
|
||||
# 위 헤더의 00 chip을 목차용으로 덮기
|
||||
rect(s, 0.55, 0.5, 0.62, 0.62, fill=PURPLE, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
|
||||
text(s, 0.55, 0.5, 0.62, 0.62, "C", size=22, color=WHITE, bold=True, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)
|
||||
toc = [
|
||||
("01", "사업 개요", "배경·목적·범위 / 전시 생애주기 폐루프"),
|
||||
("02", "시스템 구성", "아키텍처 개요 / 역할별 포털 / 나노바나나 파이프라인"),
|
||||
("03", "개발 범위", "모듈 M1~M18 + 공통·시스템관리 레이어(§5B)"),
|
||||
("04", "추진 체계", "거버넌스·아키텍트·개발·QA·DevOps 조직"),
|
||||
("05", "추진 일정", "Phase A~E 마스터 일정(완료/진행/예정)"),
|
||||
("06", "개발 방법론·품질관리", "에이전트 파이프라인 · 점진 QA · CI/CD"),
|
||||
("07", "보안 · 비기능요건(NFR)", "외부API 통제 · AI 워터마크 · 접근성·다국어"),
|
||||
("08", "리스크 및 대응", "기술·데이터·연동·보안 리스크"),
|
||||
("09", "기대 효과", "정량·정성 효과 / 확장 로드맵"),
|
||||
]
|
||||
col_w = 5.85
|
||||
for i, (no, t, d) in enumerate(toc):
|
||||
col = i // 5
|
||||
row = i % 5
|
||||
x = 0.7 + col * (col_w + 0.35)
|
||||
y = 1.62 + row * 1.02
|
||||
rect(s, x, y, col_w, 0.88, fill=CARD, line=LINE, line_w=1.0, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
|
||||
rect(s, x, y, 0.09, 0.88, fill=BLUE if col==0 else PURPLE, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
|
||||
text(s, x+0.28, y, 0.9, 0.88, no, size=24, color=(BLUE if col==0 else PURPLE), bold=True, anchor=MSO_ANCHOR.MIDDLE)
|
||||
text(s, x+1.15, y+0.15, col_w-1.3, 0.35, t, size=14.5, color=INK, bold=True)
|
||||
text(s, x+1.15, y+0.5, col_w-1.3, 0.32, d, size=9.5, color=MUTED)
|
||||
|
||||
# ================================================================
|
||||
# 섹션 간지 헬퍼
|
||||
# ================================================================
|
||||
def divider(num, kr, en, points):
|
||||
s = slide()
|
||||
rect(s, -0.1, -0.1, 13.53, 7.7, fill=BLUE_DK)
|
||||
rect(s, 8.6, -1.5, 8, 11, fill=BLUE, shape=MSO_SHAPE.PARALLELOGRAM)
|
||||
rect(s, 10.9, -2, 6, 12, fill=PURPLE, shape=MSO_SHAPE.PARALLELOGRAM)
|
||||
text(s, 1.0, 2.15, 5, 2.2, num, size=150, color=RGBColor(0x2C,0x4E,0x82), bold=True)
|
||||
rect(s, 1.15, 3.9, 0.9, 0.09, fill=PURPLE)
|
||||
text(s, 1.15, 4.05, 8, 0.8, kr, size=34, color=WHITE, bold=True)
|
||||
text(s, 1.18, 4.85, 8, 0.4, en, size=13, color=RGBColor(0x9C,0xB4,0xD8), bold=True)
|
||||
# 우측 포인트
|
||||
for i, p in enumerate(points):
|
||||
yy = 2.35 + i*0.62
|
||||
rect(s, 8.05, yy+0.06, 0.16, 0.16, fill=PURPLE, shape=MSO_SHAPE.OVAL)
|
||||
text(s, 8.35, yy, 4.4, 0.5, p, size=12.5, color=WHITE, anchor=MSO_ANCHOR.MIDDLE)
|
||||
return s
|
||||
|
||||
# ================================================================
|
||||
# 3. 사업 개요 (간지 + 본문)
|
||||
# ================================================================
|
||||
divider("01", "사업 개요", "PROJECT OVERVIEW",
|
||||
["배경 · 목적 · 범위", "전시 생애주기 폐루프", "다중 전시관 SaaS 비전"])
|
||||
|
||||
s = slide()
|
||||
content_header(s, "01 · 사업 개요", "배경 · 목적 · 추진 범위", 1)
|
||||
# 좌: 배경/목적
|
||||
card(s, 0.55, 1.5, 6.05, 2.55)
|
||||
text(s, 0.85, 1.7, 5.5, 0.35, "추진 배경 (As-Is)", size=13, color=BLUE, bold=True)
|
||||
for i, t in enumerate([
|
||||
"전시면적 108,011㎡ 국내 최대 전시장이나 준비 실무는 HWP 서식·이메일 제출에 정체",
|
||||
"CAD 수작업 배치도 → 홀매니저 육안 검수, 수기 위치표시도 기반 유틸리티 신청",
|
||||
"참가업체 유틸리티 신청이 행사별 사무국 시스템으로 파편화(공통 플랫폼 부재)",
|
||||
"시공 결과를 개장일에 처음 확인 — 사전 시각화 수단 없음",
|
||||
]):
|
||||
y = 2.1 + i*0.46
|
||||
rect(s, 0.9, y+0.07, 0.1, 0.1, fill=MUTED, shape=MSO_SHAPE.OVAL)
|
||||
text(s, 1.12, y, 5.25, 0.44, t, size=10.5, color=INK, line_spacing=1.0)
|
||||
|
||||
card(s, 6.75, 1.5, 6.03, 2.55)
|
||||
text(s, 7.05, 1.7, 5.5, 0.35, "추진 목적 (To-Be)", size=13, color=PURPLE, bold=True)
|
||||
for i, t in enumerate([
|
||||
"전시장 운영 전 과정을 AI로 자동 설계·검증하는 베뉴 운영 플랫폼 구축",
|
||||
"핵심 차별화: 나노바나나(Gemini)로 시공 전 '공사 후 결과 사진' 선(先)시각화",
|
||||
"설계 → 시각화 → 발주(옥션) → 시공 → 운영 → 분석의 폐루프 완성",
|
||||
"KINTEX 기준 테넌트 + 코엑스 등 다중 전시관 멀티테넌트 SaaS 확장",
|
||||
]):
|
||||
y = 2.1 + i*0.46
|
||||
rect(s, 7.1, y+0.07, 0.1, 0.1, fill=PURPLE, shape=MSO_SHAPE.OVAL)
|
||||
text(s, 7.32, y, 5.25, 0.44, t, size=10.5, color=INK, line_spacing=1.0)
|
||||
|
||||
# 하단: 비전 배너 + 폐루프
|
||||
rect(s, 0.55, 4.28, 12.23, 0.62, fill=BLUE, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
|
||||
text(s, 0.85, 4.28, 11.6, 0.62,
|
||||
"비전 “신청서를 내는 순간, 시공 후 사진을 먼저 본다”",
|
||||
size=17, color=WHITE, bold=True, anchor=MSO_ANCHOR.MIDDLE)
|
||||
|
||||
text(s, 0.55, 5.12, 6, 0.3, "전시 생애주기 폐루프", size=12, color=INK, bold=True)
|
||||
loop = [("판매·기획", BLUE, "M1·M16"), ("AI 설계·시각화", PURPLE, "M2~M5"),
|
||||
("공사 옥션", BLUE, "M15"), ("시공", BLUE, "M7·M8"),
|
||||
("운영", BLUE, "M10·M14"), ("경영분석", PURPLE, "M16")]
|
||||
bw = 1.83; gap = 0.19; x0 = 0.55; yb = 5.5
|
||||
for i, (lb, cl, mod) in enumerate(loop):
|
||||
x = x0 + i*(bw+gap)
|
||||
rect(s, x, yb, bw, 0.78, fill=(PURPLE_LT if cl==PURPLE else BLUE_LT), line=cl, line_w=1.2, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
|
||||
text(s, x, yb+0.1, bw, 0.34, lb, size=11.5, color=(PURPLE if cl==PURPLE else BLUE_DK), bold=True, align=PP_ALIGN.CENTER)
|
||||
text(s, x, yb+0.44, bw, 0.28, mod, size=9, color=MUTED, align=PP_ALIGN.CENTER)
|
||||
if i < len(loop)-1:
|
||||
rect(s, x+bw+0.01, yb+0.28, gap+0.02, 0.22, fill=None)
|
||||
text(s, x+bw-0.03, yb+0.16, 0.28, 0.5, "→", size=15, color=SLATE, bold=True, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)
|
||||
|
||||
# ================================================================
|
||||
# 4. 시스템 구성 (간지 + 아키텍처 도식)
|
||||
# ================================================================
|
||||
divider("02", "시스템 구성", "SYSTEM ARCHITECTURE",
|
||||
["3-Tier + 나노바나나 사이드카", "역할별 6개 포털 분리", "PostGIS 공간 데이터 모델"])
|
||||
|
||||
s = slide()
|
||||
content_header(s, "02 · 시스템 구성", "전체 아키텍처 개요도", 2)
|
||||
|
||||
def band(y, h, fill, line, label, labelcolor):
|
||||
rect(s, 0.55, y, 12.23, h, fill=fill, line=line, line_w=1.0, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
|
||||
rect(s, 0.55, y, 1.55, h, fill=line, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
|
||||
text(s, 0.55, y, 1.55, h, label, size=10.5, color=WHITE, bold=True, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)
|
||||
|
||||
def node(x, y, w, h, title, sub, fill, tcol, line=None):
|
||||
rect(s, x, y, w, h, fill=fill, line=(line or LINE), line_w=1.0, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
|
||||
text(s, x, y+0.08, w, 0.3, title, size=10, color=tcol, bold=True, align=PP_ALIGN.CENTER)
|
||||
if sub:
|
||||
text(s, x, y+0.36, w, 0.24, sub, size=7.8, color=MUTED, align=PP_ALIGN.CENTER)
|
||||
|
||||
# Presentation tier — 포털
|
||||
band(1.5, 0.86, BLUE_LT, BLUE, "프론트\n(React)", WHITE)
|
||||
ports = ["주최자 콘솔","참가업체 포털","업체 포털","운영 대시보드","관리자 백오피스","공개사이트·앱"]
|
||||
pw = 1.62
|
||||
for i,p in enumerate(ports):
|
||||
node(2.25+i*1.72, 1.66, pw, 0.54, p, "", WHITE, INK, BLUE)
|
||||
|
||||
# API tier
|
||||
band(2.55, 0.9, WHITE, BLUE_DK, "백엔드\n(Spring)", WHITE)
|
||||
node(2.25, 2.72, 2.7, 0.56, "REST + WebSocket(STOMP)", "JWT·RBAC·2FA(OTP)", WHITE, INK, BLUE)
|
||||
node(5.1, 2.72, 2.4, 0.56, "룰 엔진 / 배치·배선 엔진", "요율·규정·PostGIS 연산", WHITE, INK, BLUE)
|
||||
node(7.65, 2.72, 2.2, 0.56, "AiTextRouter", "Claude 기본·설정형 전환", WHITE, PURPLE, PURPLE)
|
||||
node(10.0, 2.72, 2.65, 0.56, "RenderJob 큐 발행", "Redis 비동기", WHITE, INK, BLUE)
|
||||
|
||||
# AI / 워커 tier
|
||||
band(3.62, 0.9, PURPLE_LT, PURPLE, "AI·시각화", WHITE)
|
||||
node(2.25, 3.79, 4.15, 0.56, "나노바나나 Python 워커 (사이드카)", "google-genai · gemini-3.1-flash-image-preview", PURPLE_LT, PURPLE, PURPLE)
|
||||
node(6.55, 3.79, 3.0, 0.56, "Claude API (AI 지능)", "설계·검수·예측 · Ollama 폴백", PURPLE_LT, PURPLE, PURPLE)
|
||||
node(9.72, 3.79, 2.93, 0.56, "WebSocket 완료 푸시", "렌더 결과 실시간 통지", WHITE, INK, PURPLE)
|
||||
|
||||
# Data tier
|
||||
band(4.69, 0.9, HALL_BG, BLUE_DK, "데이터", WHITE)
|
||||
node(2.25, 4.86, 3.3, 0.56, "PostgreSQL + PostGIS", "부스 폴리곤·트렌치·배선 LineString", WHITE, INK, BLUE_DK)
|
||||
node(5.7, 4.86, 2.4, 0.56, "Redis", "작업 큐·세션·캐시", WHITE, INK, BLUE_DK)
|
||||
node(8.25, 4.86, 2.05, 0.56, "오브젝트 스토리지", "렌더 이미지·문서", WHITE, INK, BLUE_DK)
|
||||
node(10.45, 4.86, 2.2, 0.56, "감사로그·마스터", "룰셋·요율 버전", WHITE, INK, BLUE_DK)
|
||||
|
||||
# 외부 연동
|
||||
text(s, 0.55, 5.86, 12, 0.3, "외부·기존 시스템 연동", size=10.5, color=INK, bold=True)
|
||||
ext = ["kxwp 작업신고","등록업체 DB 739","행사일정 시스템","PG 결제","Gemini","사이니지/wayfinding"]
|
||||
for i,e in enumerate(ext):
|
||||
node(0.55+i*2.06, 6.16, 1.96, 0.5, e, "", WHITE, MUTED, LINE)
|
||||
|
||||
# ================================================================
|
||||
# 5. 개발 범위 (모듈 표)
|
||||
# ================================================================
|
||||
divider("03", "개발 범위", "DEVELOPMENT SCOPE",
|
||||
["코어 M2~M5 (P0)", "도메인 M1·M6~M18", "공통·시스템관리 §5B"])
|
||||
|
||||
s = slide()
|
||||
content_header(s, "03 · 개발 범위", "모듈 맵 M1 ~ M18", 3)
|
||||
|
||||
def module_card(x, y, w, h, code, name, pr, cl):
|
||||
rect(s, x, y, w, h, fill=CARD, line=LINE, line_w=1.0, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
|
||||
rect(s, x, y, w, 0.07, fill=cl, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
|
||||
text(s, x+0.14, y+0.14, w-0.28, 0.26, [[{"t":code+" ","size":11,"color":cl,"bold":True},
|
||||
{"t":pr,"size":8,"color":WHITE,"bold":True}]], )
|
||||
# priority chip
|
||||
rect(s, x+w-0.72, y+0.13, 0.58, 0.24, fill=cl, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
|
||||
text(s, x+w-0.72, y+0.13, 0.58, 0.24, pr, size=8, color=WHITE, bold=True, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)
|
||||
text(s, x+0.14, y+0.4, w-0.28, 0.5, name, size=9.2, color=INK, line_spacing=0.95)
|
||||
|
||||
# 계열별 컬럼
|
||||
groups = [
|
||||
("설계·시각화 코어", PURPLE, [
|
||||
("M2","플로어플랜 스튜디오","P0"),
|
||||
("M3","부스 설계 스튜디오","P0"),
|
||||
("M4","유틸리티 설계(전기·조명·네트워크)","P0"),
|
||||
("M5","나노바나나 시각화","P0"),
|
||||
]),
|
||||
("판매·발주·정산", BLUE, [
|
||||
("M1","홀 배정·자동 견적","P1"),
|
||||
("M6","서류·마일스톤 워크플로","P1"),
|
||||
("M7","등록업체 매칭·견적","P1"),
|
||||
("M15","공사/장치 옥션(역경매)","P1"),
|
||||
("M8·M9","반입/반출 물류 · 정산·결제","P1·P2"),
|
||||
]),
|
||||
("관람·참가·마케팅", BLUE, [
|
||||
("M10","관람객 등록·배지·리드캡처","P1"),
|
||||
("M12","마케팅·공개 홍보 사이트","P1"),
|
||||
("M11·M13","비즈매칭 · wayfinding","P2"),
|
||||
("M14","현장운영(혼잡·안전·에너지)","P2"),
|
||||
]),
|
||||
("경영·콘텐츠·관리", PURPLE, [
|
||||
("M16","경영분석 BI(P&L·가동률·예측)","P1"),
|
||||
("M17","CMS·마이크로사이트·다국어","P1"),
|
||||
("M18","관리자 백오피스·마스터데이터","P1"),
|
||||
]),
|
||||
]
|
||||
cw = 2.94; gap = 0.16; x0 = 0.55
|
||||
for gi,(gname, gcolor, mods) in enumerate(groups):
|
||||
x = x0 + gi*(cw+gap)
|
||||
rect(s, x, 1.5, cw, 0.44, fill=gcolor, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
|
||||
text(s, x, 1.5, cw, 0.44, gname, size=11, color=WHITE, bold=True, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)
|
||||
for mi,(code,name,pr) in enumerate(mods):
|
||||
y = 2.06 + mi*0.9
|
||||
module_card(x, y, cw, 0.82, code, name, pr, gcolor)
|
||||
|
||||
# 범례
|
||||
text(s, 0.55, 6.62, 12, 0.3, [[
|
||||
{"t":"P0 ","size":9.5,"color":PURPLE,"bold":True},{"t":"핵심 차별화·첫 상용 필수 ","size":9.5,"color":MUTED},
|
||||
{"t":"P1 ","size":9.5,"color":BLUE,"bold":True},{"t":"운영 효율 핵심 ","size":9.5,"color":MUTED},
|
||||
{"t":"P2 ","size":9.5,"color":SLATE,"bold":True},{"t":"확장 │ 총 18개 도메인 모듈 + 공통·시스템관리 레이어(§5B)","size":9.5,"color":MUTED},
|
||||
]])
|
||||
|
||||
# 개발 범위 2 — 공통 레이어 §5B
|
||||
s = slide()
|
||||
content_header(s, "03 · 개발 범위", "공통 · 시스템관리 레이어 (§5B · UIWS/WISE 표준 이식)", 3)
|
||||
text(s, 0.55, 1.45, 12.2, 0.4,
|
||||
"GUARDiA 표준 프레임워크(UIWS)와 동일 스택 — 시스템관리·공통 업무기능·인증을 재설계 없이 이식하여 전 도메인 모듈의 선행 기반으로 정착",
|
||||
size=11, color=MUTED)
|
||||
# 3개 카드 영역
|
||||
def scard(x, y, w, h, title, tcolor, items):
|
||||
card(s, x, y, w, h)
|
||||
rect(s, x, y, w, 0.5, fill=tcolor, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
|
||||
text(s, x, y, w, 0.5, title, size=12.5, color=WHITE, bold=True, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)
|
||||
for i,it in enumerate(items):
|
||||
yy = y+0.66+i*0.4
|
||||
rect(s, x+0.2, yy+0.07, 0.09, 0.09, fill=tcolor, shape=MSO_SHAPE.OVAL)
|
||||
text(s, x+0.4, yy, w-0.55, 0.38, it, size=9.6, color=INK, line_spacing=0.95)
|
||||
|
||||
scard(0.55, 2.0, 3.95, 4.35, "시스템관리 (M18 통합)", BLUE, [
|
||||
"사용자 관리 (6역할 + 등록업체·관람객 계정)",
|
||||
"역할/권한 RBAC (플랫폼·행사 이중 평가)",
|
||||
"공통코드 (홀·부스유형·공종14분류·요금)",
|
||||
"메뉴 관리 (역할별 포털 IA 구동)",
|
||||
"감사로그 (승인·낙찰·룰셋개정·리드접근)",
|
||||
"시스템설정 (요율/규정 룰셋 버전)",
|
||||
"킨텍스 마스터데이터 (홀·요율·규정·업체)",
|
||||
])
|
||||
scard(4.65, 2.0, 3.95, 4.35, "공통 업무 기능", PURPLE, [
|
||||
"worklog 업무일지 · schedule 일정",
|
||||
"message 쪽지 · notice 공지",
|
||||
"opinion 의견 · search 통합검색",
|
||||
"meeting 회의록(STT→Jasper PDF)",
|
||||
"report/stats 업무보고·통계(PDF)",
|
||||
"notification 알림센터(WebSocket)",
|
||||
"preference 개인화(즐겨찾기·최근방문)",
|
||||
])
|
||||
scard(8.75, 2.0, 4.03, 4.35, "인증 (JWT + 2FA/OTP)", BLUE_DK, [
|
||||
"JWT + RBAC 역할 게이트",
|
||||
"2차 인증 OTP (TOTP RFC6238)",
|
||||
"최초 QR 등록 · 재설정 · 관리자 초기화",
|
||||
"로그인 실패 잠금 + 관리자 해제",
|
||||
"admin 비번 env 주입 (AES-256-GCM)",
|
||||
"admin123 하드코딩 시드 금지",
|
||||
"TB_USER·OTP·TB_AUDIT_LOG 멱등 이식",
|
||||
])
|
||||
|
||||
# ================================================================
|
||||
# 6. 추진 체계 (조직도)
|
||||
# ================================================================
|
||||
divider("04", "추진 체계", "PROJECT ORGANIZATION",
|
||||
["거버넌스 (PM·개발PM·PMO)", "아키텍트 5인 (AA/SA/TA/DA/NA)", "구현·품질·배포 팀"])
|
||||
|
||||
s = slide()
|
||||
content_header(s, "04 · 추진 체계", "프로젝트 조직 및 역할", 4)
|
||||
|
||||
def org(x, y, w, h, title, sub, fill, tcol, line=None, big=False):
|
||||
rect(s, x, y, w, h, fill=fill, line=(line or fill), line_w=1.2, shape=MSO_SHAPE.ROUNDED_RECTANGLE, shadow=big)
|
||||
text(s, x, y+(0.1 if sub else 0), w, (0.32 if sub else h), title, size=(12 if big else 10.5),
|
||||
color=tcol, bold=True, align=PP_ALIGN.CENTER, anchor=(MSO_ANCHOR.TOP if sub else MSO_ANCHOR.MIDDLE))
|
||||
if sub:
|
||||
subcol = MUTED if fill == WHITE else WHITE
|
||||
text(s, x, y+0.36, w, h-0.4, sub, size=8, color=subcol, align=PP_ALIGN.CENTER)
|
||||
|
||||
# 최상위 거버넌스
|
||||
org(4.9, 1.5, 3.55, 0.72, "프로젝트 총괄 (PM)", "범위·일정·리스크·게이트(G1·G2)", BLUE, WHITE, big=True)
|
||||
# 연결선
|
||||
rect(s, 6.66, 2.22, 0.02, 0.3, fill=SLATE)
|
||||
# 2단: 개발PM / PMO
|
||||
org(2.95, 2.55, 3.35, 0.66, "개발 PM", "WBS·스프린트·번다운·품질게이트", PURPLE, WHITE)
|
||||
org(7.05, 2.55, 3.35, 0.66, "PMO", "관리표준·형상관리·QA 거버넌스·변경관리", PURPLE, WHITE)
|
||||
rect(s, 6.66, 2.52, 0.02, 0.16, fill=SLATE)
|
||||
|
||||
# 3단: 아키텍트 그룹 + 구현 그룹
|
||||
rect(s, 0.55, 3.5, 12.23, 0.4, fill=BLUE_LT, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
|
||||
text(s, 0.7, 3.5, 6, 0.4, "아키텍트 그룹", size=11, color=BLUE_DK, bold=True, anchor=MSO_ANCHOR.MIDDLE)
|
||||
arch = [("AA","앱 아키텍처"),("SA","시스템·NFR"),("TA","기술표준"),("DA","데이터·ERD"),("NA","네트워크·보안")]
|
||||
for i,(a,d) in enumerate(arch):
|
||||
org(0.55+i*2.46, 4.0, 2.32, 0.66, a, d, WHITE, BLUE, BLUE)
|
||||
|
||||
rect(s, 0.55, 4.86, 12.23, 0.4, fill=PURPLE_LT, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
|
||||
text(s, 0.7, 4.86, 6, 0.4, "구현 그룹", size=11, color=PURPLE, bold=True, anchor=MSO_ANCHOR.MIDDLE)
|
||||
impl = [("공통/시스템","common-dev"),("백엔드","backend-dev"),("프론트","frontend-dev"),
|
||||
("DB","db-engineer"),("도메인","bidding·visitor\ncms·bi·admin"),("AI·시각화","ai-dev·visualizer")]
|
||||
for i,(a,d) in enumerate(impl):
|
||||
org(0.55+i*2.05, 5.36, 1.93, 0.72, a, d, WHITE, PURPLE, PURPLE)
|
||||
|
||||
# 하단: 품질·배포
|
||||
org(3.4, 6.2, 3.0, 0.66, "품질 (kintex-qa)", "점진 QA·경계면·보안 불변", GREEN, WHITE)
|
||||
org(6.95, 6.2, 3.0, 0.66, "배포 (devops-dev)", "CI/CD·systemd·env", RGBColor(0x2B,0x4C,0x7E), WHITE)
|
||||
|
||||
# ================================================================
|
||||
# 7. 추진 일정 (간트)
|
||||
# ================================================================
|
||||
divider("05", "추진 일정", "PROJECT SCHEDULE",
|
||||
["Phase A~E 마스터 일정", "완료 / 진행 / 예정 구분", "공통 레이어 선행 원칙"])
|
||||
|
||||
s = slide()
|
||||
content_header(s, "05 · 추진 일정", "Phase A ~ E 마스터 일정 (WORK_STATUS 반영)", 5)
|
||||
|
||||
# 상태 범례
|
||||
for i,(lb,cl) in enumerate([("완료",GREEN),("진행",AMBER),("예정",SLATE)]):
|
||||
rect(s, 9.8+i*1.0, 1.42, 0.24, 0.24, fill=cl, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
|
||||
text(s, 10.08+i*1.0, 1.4, 0.9, 0.28, lb, size=9.5, color=INK, anchor=MSO_ANCHOR.MIDDLE)
|
||||
|
||||
# 간트 좌표계
|
||||
gx = 3.55 # 바 시작 x
|
||||
gw = 9.15 # 바 영역 폭
|
||||
units = 10 # 타임 단위
|
||||
uw = gw/units
|
||||
# 타임 눈금 라벨
|
||||
for i in range(units+1):
|
||||
text(s, gx+i*uw-0.4, 1.72, 0.8, 0.24, f"T{i}", size=7.5, color=SLATE, align=PP_ALIGN.CENTER)
|
||||
rect(s, gx+i*uw, 1.98, 0.008, 4.2, fill=RGBColor(0xF0,0xF1,0xF4))
|
||||
|
||||
rows = [
|
||||
("Phase A", "아키텍처·거버넌스 (AA/SA/TA/DA/NA)", 0, 1.5, GREEN, "완료"),
|
||||
("Phase B", "공통·시스템관리 레이어 (2FA/OTP·공통기능)", 1.2, 2.0, GREEN, "완료"),
|
||||
("Phase C", "P0 부스 코어 M2~M5 (배치·설계·배선·시각화)", 2.6, 2.6, AMBER, "진행"),
|
||||
("Phase C", " ├ 스캐폴드·인증·매퍼 배선·룰엔진·나노바나나", 2.6, 1.6, GREEN, "완료"),
|
||||
("Phase C", " └ 3안 생성·병합·실데이터 전환·모바일", 4.2, 1.4, AMBER, "진행"),
|
||||
("Phase D", "P1 도메인 — 옥션 M15·관람객 M10", 4.8, 1.9, SLATE, "예정"),
|
||||
("Phase D", "P1 도메인 — CMS/공개사이트 M12·M17·BI M16·관리자 M18", 5.4, 2.2, SLATE, "예정"),
|
||||
("Phase D", "P1 배정·서류·매칭·정산 M1·M6·M7·M9", 5.8, 1.8, SLATE, "예정"),
|
||||
("Phase E", "P2 확장 (매칭 M11·wayfinding M13·현장운영 M14)", 7.2, 1.6, SLATE, "예정"),
|
||||
("Phase E", "빌드·배포 (역할별 번들·CI/CD·멀티테넌시)", 7.6, 2.2, AMBER, "진행"),
|
||||
]
|
||||
ry = 2.12
|
||||
rh = 0.42
|
||||
for i,(ph, name, start, dur, cl, st) in enumerate(rows):
|
||||
y = ry + i*rh
|
||||
if i % 2 == 0:
|
||||
rect(s, 0.55, y-0.02, 12.23, rh-0.02, fill=RGBColor(0xFC,0xFC,0xFD))
|
||||
text(s, 0.6, y, 0.85, rh-0.06, ph, size=8.5, color=BLUE_DK, bold=True, anchor=MSO_ANCHOR.MIDDLE)
|
||||
text(s, 1.45, y, 2.05, rh-0.06, name, size=8.2, color=INK, anchor=MSO_ANCHOR.MIDDLE)
|
||||
bx = gx + start*uw
|
||||
bw = max(dur*uw, 0.3)
|
||||
rect(s, bx, y+0.06, bw, rh-0.18, fill=cl, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
|
||||
text(s, bx, y+0.04, bw, rh-0.14, st, size=7.5, color=WHITE, bold=True, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)
|
||||
|
||||
# 현재 시점 마커
|
||||
nowx = gx + 4.9*uw
|
||||
rect(s, nowx, 1.98, 0.02, 4.2, fill=PURPLE)
|
||||
text(s, nowx-0.5, 6.2, 1.0, 0.24, "현재", size=8, color=PURPLE, bold=True, align=PP_ALIGN.CENTER)
|
||||
|
||||
text(s, 0.55, 6.55, 12.2, 0.3,
|
||||
"원칙 Phase B(공통 레이어)는 전 도메인 선행 기반 — 인증·시스템관리·공통기능 정착 후 코어/도메인 착수. 각 모듈 완성 직후 점진 QA.",
|
||||
size=9, color=MUTED)
|
||||
|
||||
# ================================================================
|
||||
# 8. 개발 방법론·품질
|
||||
# ================================================================
|
||||
divider("06", "개발 방법론 · 품질관리", "METHODOLOGY & QUALITY",
|
||||
["에이전트 파이프라인", "점진 QA · 보안 불변 게이트", "CI/CD 자동배포"])
|
||||
|
||||
s = slide()
|
||||
content_header(s, "06 · 개발 방법론 · 품질관리", "에이전트 파이프라인 · 점진 QA · CI/CD", 6)
|
||||
|
||||
# 파이프라인
|
||||
text(s, 0.55, 1.5, 8, 0.3, "① 에이전트 기반 개발 파이프라인", size=12, color=BLUE, bold=True)
|
||||
pipe = [("기획","planner",PURPLE),("설계","AA/SA/TA/DA/NA",BLUE),("디자인","designer",PURPLE),
|
||||
("구현","backend·frontend·db·domain",BLUE),("검증","kintex-qa",GREEN),("배포","devops-dev",BLUE_DK)]
|
||||
pw2 = 1.9; g2 = 0.14
|
||||
for i,(t,d,cl) in enumerate(pipe):
|
||||
x = 0.55+i*(pw2+g2)
|
||||
rect(s, x, 1.9, pw2, 0.74, fill=WHITE, line=cl, line_w=1.3, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
|
||||
text(s, x, 1.98, pw2, 0.3, t, size=11, color=cl, bold=True, align=PP_ALIGN.CENTER)
|
||||
text(s, x, 2.3, pw2, 0.3, d, size=7.6, color=MUTED, align=PP_ALIGN.CENTER)
|
||||
if i < len(pipe)-1:
|
||||
text(s, x+pw2-0.05, 1.9, 0.24, 0.74, "›", size=16, color=SLATE, bold=True, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)
|
||||
|
||||
# ② 점진 QA
|
||||
card(s, 0.55, 2.95, 6.05, 2.05)
|
||||
text(s, 0.8, 3.12, 5.5, 0.3, "② 점진 QA — 모듈 완성 직후 검증", size=12, color=PURPLE, bold=True)
|
||||
for i,t in enumerate([
|
||||
"백엔드 응답 shape ↔ 프론트 훅/컴포넌트 호출 경계면 교차 비교",
|
||||
"공간 로직(배치·배선·규정검증) 정합 · PostGIS 연산 검증",
|
||||
"보안 불변: 자격증명·PII·스택트레이스 미노출 강제",
|
||||
"AI 생성 이미지 워터마크·고지 강제 · 등록업체 응찰 게이트",
|
||||
"빌드 회귀(compileJava·tsc) · 통과까지 반려",
|
||||
]):
|
||||
y=3.5+i*0.29
|
||||
rect(s, 0.85, y+0.06, 0.09, 0.09, fill=GREEN, shape=MSO_SHAPE.OVAL)
|
||||
text(s, 1.05, y, 5.4, 0.28, t, size=9.3, color=INK)
|
||||
|
||||
# ③ CI/CD
|
||||
card(s, 6.75, 2.95, 6.03, 2.05)
|
||||
text(s, 7.0, 3.12, 5.5, 0.3, "③ CI/CD 자동배포 파이프라인", size=12, color=BLUE, bold=True)
|
||||
cicd = ["push","Gitea webhook","deploy_kintex.sh","백엔드 bootJar + 프론트 Vite 빌드","배포 → health check","실패 시 롤백"]
|
||||
for i,c in enumerate(cicd):
|
||||
y=3.5+i*0.245
|
||||
rect(s, 7.05, y, 0.28, 0.2, fill=BLUE, shape=MSO_SHAPE.OVAL)
|
||||
text(s, 7.06, y-0.01, 0.28, 0.22, str(i+1), size=8, color=WHITE, bold=True, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)
|
||||
text(s, 7.45, y-0.02, 5.2, 0.24, c, size=9.3, color=INK, anchor=MSO_ANCHOR.MIDDLE)
|
||||
|
||||
# 하단 지표 스트립
|
||||
metrics = [("18","도메인 모듈"),("6","역할별 포털"),("39+","Flyway 테이블(현재)"),("A~E","5개 Phase"),("3-Tier","+ AI 사이드카")]
|
||||
mw=2.34
|
||||
for i,(v,l) in enumerate(metrics):
|
||||
x=0.55+i*(mw+0.09)
|
||||
rect(s, x, 5.2, mw, 0.95, fill=BLUE if i%2==0 else PURPLE, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
|
||||
text(s, x, 5.34, mw, 0.5, v, size=26, color=WHITE, bold=True, align=PP_ALIGN.CENTER)
|
||||
text(s, x, 5.86, mw, 0.26, l, size=9.5, color=WHITE, align=PP_ALIGN.CENTER)
|
||||
text(s, 0.55, 6.5, 12.2, 0.3,
|
||||
"AI 지능 = Claude 기본 + 설정형 전환(AiTextRouter/AiConfig) · Ollama 폴백 │ 이미지 = 나노바나나(Gemini) 라이브",
|
||||
size=9, color=MUTED)
|
||||
|
||||
# ================================================================
|
||||
# 9. 보안·NFR
|
||||
# ================================================================
|
||||
divider("07", "보안 · 비기능요건", "SECURITY & NFR",
|
||||
["외부 API 통제 (예외 명시)", "자격증명 보호 · AI 워터마크", "접근성·다국어·테마"])
|
||||
|
||||
s = slide()
|
||||
content_header(s, "07 · 보안 · 비기능요건(NFR)", "보안 불변 원칙 및 품질 속성", 7)
|
||||
|
||||
# 보안 불변 (좌)
|
||||
card(s, 0.55, 1.5, 6.05, 3.05)
|
||||
rect(s, 0.55, 1.5, 6.05, 0.5, fill=BLUE_DK, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
|
||||
text(s, 0.55, 1.5, 6.05, 0.5, "보안 불변 (Security Invariants)", size=13, color=WHITE, bold=True, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)
|
||||
sec = [
|
||||
("외부 API 통제","온프레미스 원칙 · 예외 = Claude(승인)·Gemini(G1). 키는 서버 env 로드, 코드·커밋·로그 미기재"),
|
||||
("자격증명 보호","IP·SSH계정·비밀번호를 API 응답·에러메시지에 미노출"),
|
||||
("암호화 저장","민감 자격증명 AES-256-GCM · admin 비번 env 주입(admin123 금지)"),
|
||||
("AI 이미지 고지","나노바나나 생성 전 이미지 워터마크·실물 상이 고지 강제"),
|
||||
("에러 응답","스택트레이스 미노출 — 요약 메시지만 전달"),
|
||||
]
|
||||
for i,(h,d) in enumerate(sec):
|
||||
y=2.15+i*0.47
|
||||
rect(s, 0.8, y+0.04, 0.11, 0.11, fill=BLUE, shape=MSO_SHAPE.OVAL)
|
||||
text(s, 1.02, y, 5.45, 0.44, [[{"t":h+" — ","size":9.6,"color":INK,"bold":True},
|
||||
{"t":d,"size":9,"color":MUTED}]], line_spacing=0.95)
|
||||
|
||||
# NFR (우)
|
||||
card(s, 6.75, 1.5, 6.03, 3.05)
|
||||
rect(s, 6.75, 1.5, 6.03, 0.5, fill=PURPLE, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
|
||||
text(s, 6.75, 1.5, 6.03, 0.5, "비기능요건 (NFR)", size=13, color=WHITE, bold=True, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)
|
||||
nfr = [
|
||||
("웹 접근성","WCAG 준수 · 키보드·스크린리더 대응"),
|
||||
("다국어(i18n)","한 · 영 · 중 · 일 (공개사이트·CMS 우선)"),
|
||||
("라이트/다크 테마","전 화면 테마 토큰 · 반응형 풀스크린"),
|
||||
("반응형 채널 분리","데스크톱=설계/에디터 · 모바일=현장/조회/승인"),
|
||||
("성능·확장","Redis 비동기 큐 · 렌더 워커 사이드카 분리"),
|
||||
("멀티테넌시 격리","tenant_id 전파 · 전시관 단위 데이터·권한 격리"),
|
||||
]
|
||||
for i,(h,d) in enumerate(nfr):
|
||||
y=2.15+i*0.4
|
||||
rect(s, 7.0, y+0.04, 0.11, 0.11, fill=PURPLE, shape=MSO_SHAPE.OVAL)
|
||||
text(s, 7.22, y, 5.4, 0.38, [[{"t":h+" — ","size":9.6,"color":INK,"bold":True},
|
||||
{"t":d,"size":9,"color":MUTED}]], line_spacing=0.95)
|
||||
|
||||
# 하단: 3중 권한 구조
|
||||
rect(s, 0.55, 4.78, 12.23, 0.42, fill=INK, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
|
||||
text(s, 0.55, 4.78, 12.23, 0.42, "3중 권한 구조 테넌트(전시관) 격리 ⊃ 행사(Event) 워크스페이스 ⊃ 행사 RBAC",
|
||||
size=11.5, color=WHITE, bold=True, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)
|
||||
# 인증 요약 3칩
|
||||
for i,(t,d,cl) in enumerate([("JWT + RBAC","6역할 게이트",BLUE),("2FA / OTP","TOTP RFC6238",PURPLE),
|
||||
("감사로그","전수 기록·개인정보 접근",BLUE_DK)]):
|
||||
x=0.55+i*4.11
|
||||
rect(s, x, 5.4, 3.97, 0.72, fill=WHITE, line=cl, line_w=1.3, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
|
||||
text(s, x+0.2, 5.5, 3.7, 0.3, t, size=12, color=cl, bold=True)
|
||||
text(s, x+0.2, 5.82, 3.7, 0.26, d, size=9.5, color=MUTED)
|
||||
|
||||
# ================================================================
|
||||
# 10. 리스크 및 대응
|
||||
# ================================================================
|
||||
divider("08", "리스크 및 대응", "RISK MANAGEMENT",
|
||||
["기술·데이터·연동 리스크", "영향도·발생가능성 평가", "완화 방안"])
|
||||
|
||||
s = slide()
|
||||
content_header(s, "08 · 리스크 및 대응", "주요 리스크 및 대응 방안", 8)
|
||||
|
||||
# 표 헤더
|
||||
hx=[0.55, 1.3, 5.9, 8.0] # 구분/리스크/영향/대응 시작
|
||||
cols = [(0.55,0.75,"구분"),(1.3,4.6,"리스크"),(5.9,2.1,"영향"),(8.0,4.78,"대응 방안")]
|
||||
rect(s, 0.55, 1.5, 12.23, 0.44, fill=BLUE, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
|
||||
for x,w,h in cols:
|
||||
text(s, x+0.12, 1.5, w-0.15, 0.44, h, size=10.5, color=WHITE, bold=True, anchor=MSO_ANCHOR.MIDDLE)
|
||||
|
||||
risks = [
|
||||
("기술","나노바나나 생성 이미지가 실제 시공과 상이","높음",AMBER,"전 이미지 워터마크·고지 강제, 참조이미지 구조보존 파이프라인, 검수 단계"),
|
||||
("데이터","홀 실측 CAD·트렌치 좌표 미확보","높음",AMBER,"확보 자산(평면도 15장+CAD) 활용, 미확보 시 공개규격 근사도면으로 Phase 진행"),
|
||||
("연동","kxwp 내부 API 미공개(폐쇄형)","중간",SLATE,"초기 제출용 파일 생성+업로드 안내 릴레이, 연동은 킨텍스 협의 후"),
|
||||
("보안","외부 API 호출·자격증명 노출","높음",AMBER,"Claude·Gemini만 예외 승인, env 로드·미커밋, 응답 미노출, 감사로그"),
|
||||
("규정","소방·구조 최종 유권해석은 시스템 밖","중간",SLATE,"AI는 사전 필터(위반 플래깅)까지, 최종 판정은 관할기관·구조기술사"),
|
||||
("일정","공통 레이어 지연 시 도메인 착수 지연","중간",SLATE,"Phase B 선행 완료(현재 완료), 모듈 병렬화·점진 QA로 흡수"),
|
||||
("시장","옥션 업체 참여율·응찰 품질 불확실","중간",SLATE,"등록업체 DB 검증 게이트, AI 자료 기반 동일사양 비교로 참여 유인"),
|
||||
]
|
||||
ry=1.98; rh=0.56
|
||||
for i,(g,r,imp,impc,resp) in enumerate(risks):
|
||||
y=ry+i*rh
|
||||
if i%2==0:
|
||||
rect(s, 0.55, y, 12.23, rh, fill=RGBColor(0xFC,0xFC,0xFD))
|
||||
rect(s, 0.55, y, 12.23, 0.01, fill=LINE)
|
||||
text(s, 0.67, y, 0.7, rh, g, size=9.5, color=BLUE_DK, bold=True, anchor=MSO_ANCHOR.MIDDLE)
|
||||
text(s, 1.42, y, 4.4, rh, r, size=9.5, color=INK, anchor=MSO_ANCHOR.MIDDLE)
|
||||
rect(s, 5.9, y+0.16, 0.95, 0.24, fill=impc, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
|
||||
text(s, 5.9, y+0.15, 0.95, 0.26, imp, size=8.5, color=WHITE, bold=True, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE)
|
||||
text(s, 8.0, y, 4.7, rh, resp, size=8.8, color=MUTED, anchor=MSO_ANCHOR.MIDDLE, line_spacing=0.92)
|
||||
|
||||
# ================================================================
|
||||
# 11. 기대 효과
|
||||
# ================================================================
|
||||
divider("09", "기대 효과", "EXPECTED BENEFITS",
|
||||
["정량 효과 (리드타임)", "정성 효과 (의사결정·투명성)", "확장 로드맵"])
|
||||
|
||||
s = slide()
|
||||
content_header(s, "09 · 기대 효과", "정량 · 정성 기대 효과", 9)
|
||||
|
||||
text(s, 0.55, 1.48, 8, 0.3, "정량 효과 — 리드타임 단축", size=12.5, color=BLUE, bold=True)
|
||||
quant = [
|
||||
("홀 배정 견적","수일","즉시"),
|
||||
("배치도 초안","수일~수주","수 분"),
|
||||
("규정 검수","육안 검토","자동 플래깅"),
|
||||
("유틸리티 신청","수기 작도","클릭 + 자동견적"),
|
||||
("시공 결과 예측","불가(외주)","표준 샷 자동생성"),
|
||||
]
|
||||
qw=2.4
|
||||
for i,(k,a,b) in enumerate(quant):
|
||||
x=0.55+i*(qw+0.06)
|
||||
card(s, x, 1.85, qw, 1.85)
|
||||
text(s, x, 2.0, qw, 0.3, k, size=10, color=INK, bold=True, align=PP_ALIGN.CENTER)
|
||||
rect(s, x+0.3, 2.34, qw-0.6, 0.02, fill=LINE)
|
||||
text(s, x, 2.42, qw, 0.26, "As-Is", size=7.5, color=SLATE, align=PP_ALIGN.CENTER)
|
||||
text(s, x, 2.62, qw, 0.3, a, size=10.5, color=MUTED, align=PP_ALIGN.CENTER)
|
||||
text(s, x, 2.98, qw, 0.24, "↓", size=12, color=PURPLE, bold=True, align=PP_ALIGN.CENTER)
|
||||
text(s, x, 3.2, qw, 0.3, b, size=11, color=BLUE, bold=True, align=PP_ALIGN.CENTER)
|
||||
|
||||
text(s, 0.55, 3.95, 8, 0.3, "정성 효과", size=12.5, color=PURPLE, bold=True)
|
||||
qual = [
|
||||
("의사결정 가속","도면을 읽지 못해도 사진 수준 시각화로 주최자·참가업체 의사결정"),
|
||||
("영업 자료화","완공 예상 이미지로 참가업체 유치·컨펌 커뮤니케이션 비용 절감"),
|
||||
("가격 투명화","옥션 역경매로 동일 사양 위 공정 비교 → 시공 발주 투명·최적화"),
|
||||
("데이터 일원화","공간 데이터(PostGIS) 단일 원천에서 검증·시각화·정산·BI·wayfinding"),
|
||||
]
|
||||
for i,(h,d) in enumerate(qual):
|
||||
x=0.55+(i%2)*6.15
|
||||
y=4.32+(i//2)*0.66
|
||||
card(s, x, y, 6.05, 0.58)
|
||||
rect(s, x, y, 0.08, 0.58, fill=PURPLE, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
|
||||
text(s, x+0.24, y+0.08, 5.7, 0.28, h, size=10.5, color=INK, bold=True)
|
||||
text(s, x+0.24, y+0.34, 5.7, 0.22, d, size=8.6, color=MUTED)
|
||||
|
||||
# 확장 로드맵
|
||||
rect(s, 0.55, 5.78, 12.23, 0.44, fill=BLUE, shape=MSO_SHAPE.ROUNDED_RECTANGLE)
|
||||
text(s, 0.7, 5.78, 4, 0.44, "확장 로드맵", size=11, color=WHITE, bold=True, anchor=MSO_ANCHOR.MIDDLE)
|
||||
road = ["KINTEX 기준 운영","코엑스 등 다중 전시관 온보딩","전시테크 SaaS 사업화","kxwp·PG·사이니지 정식 연동"]
|
||||
for i,r in enumerate(road):
|
||||
text(s, 2.6+i*2.55, 5.78, 2.5, 0.44, "› "+r, size=9.3, color=WHITE, anchor=MSO_ANCHOR.MIDDLE)
|
||||
|
||||
text(s, 0.55, 6.4, 12.2, 0.5,
|
||||
"종합 “신청서를 내는 순간 시공 후 사진을 먼저 본다” — 설계·시각화·발주·운영·분석 폐루프를 하나의 데이터·계정 체계로 자동화하여 킨텍스를 전시 운영 표준 플랫폼으로 전환",
|
||||
size=10, color=INK, bold=True, line_spacing=1.0)
|
||||
|
||||
# ================================================================
|
||||
# 저장
|
||||
# ================================================================
|
||||
OUT = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "개발계획서.pptx")
|
||||
prs.save(OUT)
|
||||
print("SAVED:", OUT)
|
||||
print("SLIDES:", len(prs.slides._sldIdLst))
|
||||
337
docs/deliverables/_gen/gen_docx.py
Normal file
337
docs/deliverables/_gen/gen_docx.py
Normal file
@ -0,0 +1,337 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
사업수행계획서(.docx) — 행안부 정보화사업 산출물 표준 목차 준용.
|
||||
데이터 소스: sources.py(PLANNING·IMPLEMENTATION_BACKLOG·FEATURE_BACKLOG_100 파싱).
|
||||
소스 문서 갱신 시 본 스크립트 재실행만으로 결정적 재생성.
|
||||
보안: 서버 IP·비밀번호·SSH·시크릿 미기재.
|
||||
"""
|
||||
import os
|
||||
from docx import Document
|
||||
from docx.shared import Pt
|
||||
from docx.enum.text import WD_ALIGN_PARAGRAPH
|
||||
import sources as S
|
||||
from _docxutil import setup, h, para, bullets, table, NAVY, ACCENT, GREY
|
||||
|
||||
OUT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "사업수행계획서.docx"))
|
||||
doc = Document(); setup(doc)
|
||||
M = S.META
|
||||
CA = WD_ALIGN_PARAGRAPH.CENTER
|
||||
|
||||
# ---------- 표지 ----------
|
||||
for _ in range(4): doc.add_paragraph()
|
||||
para(doc, f"{M['project_en']} — 사업수행계획서", size=13, color=ACCENT, align=CA, space_after=4)
|
||||
p = doc.add_paragraph(); p.alignment = CA
|
||||
r = p.add_run(M["title"]); r.bold = True; r.font.size = Pt(24); r.font.color.rgb = NAVY
|
||||
doc.add_paragraph()
|
||||
para(doc, M["std"], size=11, color=GREY, align=CA)
|
||||
for _ in range(6): doc.add_paragraph()
|
||||
table(doc, ["구분","내용"],
|
||||
[["문서명", M["title"]],["문서 버전", M["version"]],["작성일", M["date"]],
|
||||
["작성 주관", M["owner"]],["발주처(수요기관)", M["client"]],
|
||||
["사업 유형","정보시스템 구축(SI) — AI 기반 전시 운영 자동화 플랫폼"],
|
||||
["보안 등급","대외비 — 자격증명·내부망 정보 미기재"]],
|
||||
widths=[4.5, 11.5], first_col_bold=True)
|
||||
doc.add_page_break()
|
||||
|
||||
# ---------- 문서정보 / 개정이력 / 목차 ----------
|
||||
h(doc,1,"문서 정보 및 개정 이력")
|
||||
h(doc,2,"문서 정보")
|
||||
table(doc, ["항목","내용"],
|
||||
[["문서 제목", M["title"]],["산출 단계","사업 착수(계획) 단계 표준 산출물"],
|
||||
["표준 근거", M["std"]],
|
||||
["작성 근거 문서","PLANNING v3.0 · IMPLEMENTATION_BACKLOG · FEATURE_BACKLOG_100 · GUARDIA_ALIGNMENT · SECURITY · architecture/*"],
|
||||
["문서 관리","형상관리(Git) 하 버전 통제 — docs/deliverables/ 관리, _gen 재실행으로 재생성"]],
|
||||
widths=[4.5, 11.5], first_col_bold=True)
|
||||
h(doc,2,"개정 이력")
|
||||
table(doc, ["버전","일자","작성/개정자","주요 변경 내용"],
|
||||
[[M["version"], M["date"], "사업 PMO", "사업수행계획서 최초 작성(표준 목차 14장 체계)"]],
|
||||
widths=[2.0, 3.0, 3.5, 7.5])
|
||||
para(doc, "※ 본 계획서는 사업 진척에 따라 형상관리 절차(14장)에 의거 개정하며, 모든 변경은 개정 이력에 기록한다.",
|
||||
size=9, italic=True, color=GREY)
|
||||
h(doc,2,"목차")
|
||||
for t_ in ["1. 사업 개요","2. 사업 범위","3. 추진 전략 및 개발 방법론","4. 추진 체계·조직 및 역할(R&R)",
|
||||
"5. WBS 및 마스터 일정","6. 투입 인력 및 투입 계획(M/M)","7. 품질보증 계획","8. 보안 및 개인정보보호 계획",
|
||||
"9. 시험(테스트) 계획 개요","10. 이관·교육·유지보수 계획","11. 위험(리스크) 관리","12. 의사소통·보고 체계",
|
||||
"13. 산출물 목록","14. 형상관리"]:
|
||||
pp = doc.add_paragraph(t_); pp.paragraph_format.space_after = Pt(2); pp.runs[0].font.size = Pt(10.5)
|
||||
doc.add_page_break()
|
||||
|
||||
# ---------- 1. 사업 개요 ----------
|
||||
h(doc,1,"1. 사업 개요")
|
||||
h(doc,2,"1.1 추진 배경")
|
||||
para(doc,"킨텍스는 총 전시면적 108,011㎡(국내 최대, 제3전시장 완공 시 약 178,000㎡)를 운영하지만, 전시 준비 실무는 여전히 "
|
||||
"표준 서식 다운로드 후 이메일·방문 제출, CAD 수작업 배치도의 육안 검수, 수기 위치표시도 기반 유틸리티 신청에 머물러 있다. "
|
||||
"참가업체 유틸리티 신청은 킨텍스 공통 플랫폼 없이 전시회별 주최자 사무국 시스템으로 파편화되어 있다.")
|
||||
para(doc,"공간 데이터(배치도·도면·위치표시도)가 전부 이미지·수기로 오가며 구조화되지 않아 자동 검증·시각화·정산의 원천 데이터가 부재하고, "
|
||||
"D-150/D-30/D-25/D-7 마일스톤이 시스템화되어 있지 않으며, 시공 결과를 사전에 확인할 수단이 없다는 구조적 공백이 확인되었다.")
|
||||
h(doc,2,"1.2 사업 목적")
|
||||
bullets(doc,[
|
||||
"전시장 운영 전 과정(홀 배정→부스 배치→장치공사 설계→전기/조명→네트워크/유틸리티 배선→반입/반출)을 AI로 자동 설계·검증한다.",
|
||||
"AI 설계 결과를 나노바나나(이미지 생성)로 '시공 후 결과 사진'처럼 시공 전에 미리 생성하여 사진 수준 시각화를 제공한다.",
|
||||
"기획·판매·시공·운영·관람객·사후분석 전 주기를 하나의 공간 데이터·계정 체계로 통합한 베뉴 운영 플랫폼을 구축한다.",
|
||||
"AI 설계·시각화 산출물을 공사/장치 옥션(역경매)의 응찰 근거로 소비하여 설계→시각화→발주→시공 폐루프를 완성한다.",
|
||||
])
|
||||
h(doc,2,"1.3 비전 및 정체성")
|
||||
para(doc,'비전 슬로건: "신청서를 내는 순간, 시공 후 사진을 먼저 본다."', bold=True, color=ACCENT)
|
||||
para(doc,"본 시스템은 킨텍스를 기준 테넌트(#1)로 하되 코엑스 등 여타 전시관을 온보딩하는 다중 전시관 멀티테넌트 SaaS로 확장 가능한 구조로 설계한다. "
|
||||
"기능(모듈)은 전부 보존하고 그 위에 전시관 단위 데이터·권한·컨텍스트 격리 레이어를 얹는다.")
|
||||
h(doc,2,"1.4 정량 목표(As-Is → To-Be)")
|
||||
table(doc, ["목표 지표","현재(As-Is)","목표(To-Be)"],
|
||||
[["홀 배정 견적 회신","문의→협의→견적 수일","조건 입력 후 즉시 자동 가견적"],
|
||||
["부스 배치도 초안","CAD 수작업 수일~수주","조건 입력 후 수 분 내 복수 안 생성"],
|
||||
["도면 규정 검수","홀매니저 육안 검토","규정 위반 자동 플래깅 후 사람 확정"],
|
||||
["유틸리티 위치표시도","참가업체 수기 작도","부스 좌표 클릭 → 자동 배선안·자동 견적"],
|
||||
["시공 결과 예측","불가(외주 조감도)","표준 샷 세트 자동 생성(정면/야간/통로)"]],
|
||||
widths=[4.5, 5.5, 6.0])
|
||||
h(doc,2,"1.5 기대 효과")
|
||||
bullets(doc,[
|
||||
"참가업체·주최자의 의사결정 가속 및 참가업체 유치 영업 자료 확보(시공 예상 이미지 기반).",
|
||||
"규정 위반 사전 검출로 도면 반려 재작업 감소 및 홀매니저 검수 시간 단축.",
|
||||
"'현장 추가신청 불가' 유틸리티의 신청 누락 방지 — 참가업체 최대 리스크 제거.",
|
||||
"역경매 옥션을 통한 시공 발주 가격 최적화·투명화(동일 사양 위 공정 비교).",
|
||||
"공간 데이터 표준화로 자동 검증·시각화·정산·wayfinding·경영분석의 단일 원천 확보.",
|
||||
])
|
||||
|
||||
# ---------- 2. 사업 범위 (모듈: sources.modules) ----------
|
||||
h(doc,1,"2. 사업 범위")
|
||||
h(doc,2,"2.1 대상 시스템 및 모듈")
|
||||
para(doc,f"본 사업은 설계·시각화 코어(P0), 판매·발주·정산, 관람·참가·마케팅, 경영·콘텐츠·관리 계열의 총 {len(S.modules())}개 모듈과 "
|
||||
"공통·시스템관리 레이어(UIWS/WISE 이식)로 구성된다. (모듈 목록은 PLANNING 총괄표에서 자동 추출)")
|
||||
mod_rows = [[mid, name, pri] for (mid, name, pri) in S.modules()]
|
||||
table(doc, ["모듈","명칭","우선순위"], mod_rows, widths=[2.2, 11.3, 2.5])
|
||||
h(doc,2,"2.2 우선순위 정의")
|
||||
bullets(doc,[
|
||||
"P0 — 핵심 차별화 + 첫 상용 행사 적용 필수(부스 시공 코어 M2~M5).",
|
||||
"P1 — 운영 효율 핵심(판매·발주·정산·관람·마케팅·경영분석·관리자·공통 레이어).",
|
||||
"P2 — 확장(물류·비즈매칭·wayfinding·현장운영).",
|
||||
])
|
||||
h(doc,2,"2.3 범위 제외(Non-Goals)")
|
||||
bullets(doc,[
|
||||
"킨텍스 임대계약의 법적 전자체결은 후속 단계 검토 — 초기에는 견적·서류 준비까지.",
|
||||
"구조계산서의 구조 안전성 최종 판정은 AI가 대신하지 않는다(규정 체크·누락 검출까지, 최종 판정은 구조기술사/킨텍스).",
|
||||
"AI 생성 시각화는 참고용이며 계약·인허가 서류로 사용 불가(워터마크·고지 강제).",
|
||||
"신규 전시관 온보딩은 마스터데이터 입력 수준까지 — 고유 규정 파서 자동 생성 제외.",
|
||||
"정밀 실내 측위(UWB/BLE 비콘)는 시설 인프라 협의 전제 — 초기에는 존-레벨 폴백.",
|
||||
])
|
||||
|
||||
# ---------- 3. 추진 전략·방법론 ----------
|
||||
h(doc,1,"3. 추진 전략 및 개발 방법론")
|
||||
h(doc,2,"3.1 기본 추진 전략")
|
||||
bullets(doc,[
|
||||
"공통 레이어 선행: 인증(JWT+2FA)·시스템관리·공통 업무기능을 전 도메인 모듈보다 선행 구축.",
|
||||
"코어 우선: 핵심 차별화 P0 부스 시공 코어(M2~M5)를 우선 확보하여 첫 상용 행사에 적용.",
|
||||
"단일 공간 데이터 모델: M2→M3→M4→M5가 하나의 PostGIS 지오메트리를 공유.",
|
||||
"폐루프 완성: 코어 산출물을 M15 옥션 응찰 근거로 소비하여 설계→시각화→발주→시공 종결.",
|
||||
"표준 프레임워크 준수: GUARDiA 표준(UIMS/WISE) 스택·인증·공통모듈·보안·배포를 상속하고 도메인만 확장.",
|
||||
])
|
||||
h(doc,2,"3.2 개발 방법론")
|
||||
para(doc,"반복·점진(Iterative & Incremental) 방식. Phase A~E 단계 위에서 각 모듈은 설계→구현→모듈 완성 직후 점진 QA→통합의 짧은 반복을 수행하며, "
|
||||
"QA가 경계면·회귀·보안 불변을 검증하고 통과할 때까지 반려한다.")
|
||||
ph = S.phases()
|
||||
table(doc, ["단계","성격","핵심 활동"],
|
||||
[[f"Phase {p[0]}", p[1], " · ".join(t[1] for t in p[2][:3]) + (" 등" if len(p[2])>3 else "")] for p in ph],
|
||||
widths=[2.4, 4.6, 9.0])
|
||||
h(doc,2,"3.3 기술 아키텍처(확정 스택)")
|
||||
table(doc, ["레이어","기술","비고"],
|
||||
[["프론트엔드","React 18/19 + Vite + TypeScript","역할별 포털 번들 분리(데스크톱=설계, 모바일=현장·조회)"],
|
||||
["백엔드","Spring Boot 3.x(Java 17) + MyBatis","REST + WebSocket(STOMP), 룰·배치/배선 엔진"],
|
||||
["데이터베이스","PostgreSQL + PostGIS","부스 폴리곤·트렌치 포인트·배선 LineString 공간 데이터"],
|
||||
["비동기","Redis 작업 큐","RenderJob·서류·알림 비동기 처리"],
|
||||
["이미지 생성","나노바나나 Python 워커(사이드카)","백엔드는 큐 발행만, 생성 키는 워커 env 전용"],
|
||||
["AI(텍스트)","Claude 기본 + 설정형 전환(AiTextRouter)","실패 시 온프레미스 모델 자동 폴백"],
|
||||
["형상/배포","Git(Gitea)+CI/CD Webhook+systemd/nginx","무중단·헬스게이트·롤백(Fail-Safe)"]],
|
||||
widths=[2.8, 6.2, 7.0])
|
||||
para(doc, f"개발 도메인은 서비스명 수준으로 {M['dev_domain']}을 사용하며, 운영 도메인은 후속 확정한다.", size=9, italic=True, color=GREY)
|
||||
|
||||
# ---------- 4. 추진 체계·조직·역할 ----------
|
||||
h(doc,1,"4. 추진 체계·조직 및 역할(R&R)")
|
||||
h(doc,2,"4.1 추진 체계")
|
||||
para(doc,"발주처(킨텍스)—사업 총괄(PMO)—아키텍트 그룹—개발 그룹(공통·코어·도메인)—품질(QA)·배포(DevOps)의 계층으로 구성한다. "
|
||||
"AI 에이전트 팀(kintex-impl-orchestrator)이 각 트랙 구현을 조율하며, 기획(planner)·디자인(designer)·검증(reviewer)을 경유한다.")
|
||||
h(doc,2,"4.2 조직 및 역할(R&R)")
|
||||
table(doc, ["그룹","역할","주요 책임(R&R)"],
|
||||
[["거버넌스","PMO / 사업 PM","일정·범위·리스크·이해관계자 관리, 발주처 보고, 형상·품질 총괄"],
|
||||
["아키텍트","AA(애플리케이션)","모듈 경계·레이어·API 표준·패키지·의존성 규칙"],
|
||||
["아키텍트","SA(시스템)","확장성·HA·성능·배포 토폴로지·비기능요건(NFR)"],
|
||||
["아키텍트","TA(기술)","기술 표준·빌드/배포·개발 표준·관측성·AiTextRouter"],
|
||||
["아키텍트","DA(데이터)","전사 ERD·공간데이터·마스터·공통코드·BI 데이터마트"],
|
||||
["아키텍트","NA(네트워크)","DMZ/내부망 분리·방화벽·부하분산·공개/백오피스 망 분리"],
|
||||
["공통","공통/시스템관리 개발","인증(2FA)·시스템관리·공통 업무기능(UIWS 이식)"],
|
||||
["코어","백엔드 개발","M1~M9 REST/WebSocket·룰 엔진·배선 엔진(PostGIS)"],
|
||||
["코어","프론트엔드 개발","역할별 포털 화면·플로어플랜 캔버스·Before/After 뷰"],
|
||||
["코어","DB 엔지니어","PostGIS 스키마·마이그레이션·매퍼·마스터 시드"],
|
||||
["AI·시각화","AI 개발","배치 자동생성·규정검증 보조·매칭·서류검수·챗봇(Claude)"],
|
||||
["AI·시각화","시각화(visualizer)","나노바나나 프롬프트·워커·워터마크 강제"],
|
||||
["도메인","옥션/관람객/CMS/BI/관리자 개발","M15/M10/M12·M17/M16/M18 도메인 구현"],
|
||||
["품질·배포","QA","경계면·회귀·보안 불변 점진 검증(통과까지 반려)"],
|
||||
["품질·배포","DevOps","빌드·CI/CD·systemd/nginx·환경변수 배선"]],
|
||||
widths=[2.2, 4.0, 9.8])
|
||||
|
||||
# ---------- 5. WBS·마스터 일정 (sources.phases + SCHEDULE) ----------
|
||||
h(doc,1,"5. WBS 및 마스터 일정")
|
||||
h(doc,2,"5.1 작업 분해 구조(WBS) 개요")
|
||||
para(doc,"WBS는 Phase A~E의 5개 단계와 하위 작업 패키지로 구성한다(IMPLEMENTATION_BACKLOG에서 자동 추출). "
|
||||
"상세 작업 분해·담당·의존·기간은 별첨 [WBS_및_일정.xlsx]의 'WBS' 시트에 관리한다.")
|
||||
table(doc, ["Phase","작업 패키지(대표)","완료 기준(대표)"],
|
||||
[[f"{p[0]}. {p[1]}", " / ".join(t[1] for t in p[2][:3]), p[2][0][3] if p[2] else ""] for p in ph],
|
||||
widths=[3.6, 8.0, 4.4])
|
||||
h(doc,2,"5.2 마스터 일정 및 마일스톤")
|
||||
para(doc,"총 사업 기간은 약 28주(7개월) 기준이며, 착수 시점 확정 후 절대 일정을 확정한다(아래는 착수일 기준 상대 주차).")
|
||||
table(doc, ["Phase","시작","종료","주요 마일스톤"],
|
||||
[[f"{r_[0]}. {r_[1]}", r_[2], r_[3], r_[4]] for r_ in S.SCHEDULE],
|
||||
widths=[4.2, 1.8, 1.8, 8.2])
|
||||
para(doc,"※ Phase B는 Phase A와 일부 병행 착수하며, Phase B(공통 레이어) 완료가 Phase C/D 도메인 착수의 선행 전제이다.",
|
||||
size=9, italic=True, color=GREY)
|
||||
|
||||
# ---------- 6. 투입 인력 (sources.STAFFING) ----------
|
||||
h(doc,1,"6. 투입 인력 및 투입 계획(M/M)")
|
||||
h(doc,2,"6.1 투입 인력 계획")
|
||||
para(doc,"역할별 투입 인력과 단계별 투입 공수(M/M)는 아래와 같다. 상세는 별첨 [WBS_및_일정.xlsx]의 '투입인력계획' 시트에 관리한다.")
|
||||
table(doc, ["역할","인원(명)","등급","주 투입 Phase","투입 공수(M/M)"],
|
||||
[list(x) for x in S.STAFFING], widths=[5.5, 2.0, 2.0, 2.8, 3.7])
|
||||
total_mm = sum(float(x[4]) for x in S.STAFFING)
|
||||
total_head = sum(int(x[1]) for x in S.STAFFING)
|
||||
para(doc, f"합계(계획): 인원 약 {total_head}명(중복 참여 포함), 총 투입 공수 약 {total_mm:.1f} M/M(추정치, 착수 시 상세 산정).",
|
||||
bold=True, size=10, color=NAVY)
|
||||
h(doc,2,"6.2 투입 원칙")
|
||||
bullets(doc,[
|
||||
"Phase B(공통 레이어)에 개발 인력을 선집중하여 전 도메인 기반을 조기 확보.",
|
||||
"아키텍트는 Phase A에 집중 투입 후 각 Phase 착수 시 표준 준수를 검수.",
|
||||
"QA는 전 Phase 상시 배치하여 모듈 완성 직후 점진 검증을 수행.",
|
||||
])
|
||||
|
||||
# ---------- 7. 품질보증 ----------
|
||||
h(doc,1,"7. 품질보증 계획")
|
||||
h(doc,2,"7.1 품질 목표 및 기준")
|
||||
bullets(doc,[
|
||||
"기능 품질: 모듈별 완료 기준(WBS) 충족, 경계면(API 응답 shape↔프론트 호출) 불일치 0.",
|
||||
"코드 품질: 표준 프레임워크·아키텍처 표준 준수, 정적 분석·코드 리뷰 통과.",
|
||||
"회귀 품질: 스모크/회귀 테스트로 기존 기능 무회귀 보장.",
|
||||
"보안 품질: 보안 불변 규칙(8장) 위반 0 — 위반 시 반려 사유.",
|
||||
])
|
||||
h(doc,2,"7.2 품질보증 활동")
|
||||
table(doc, ["활동","시점","담당","산출"],
|
||||
[["아키텍처 정합 검증","Phase A 종료","reviewer","불일치 티켓·시정 확인"],
|
||||
["점진 QA(모듈 완성 직후)","각 모듈 완성","QA","경계면·회귀·보안 검증 결과"],
|
||||
["코드 리뷰","커밋/PR","개발 리드","리뷰 코멘트·조치"],
|
||||
["스모크·회귀 테스트","배포 전","QA/DevOps","테스트 리포트"],
|
||||
["보안 체크리스트 점검","배포·리뷰 전","QA","체크리스트 결과(8장)"]],
|
||||
widths=[4.5, 3.0, 3.0, 5.5])
|
||||
h(doc,2,"7.3 품질 게이트")
|
||||
para(doc,"각 Phase 종료 및 배포 직전에 품질 게이트를 두어 완료 기준·경계면 정합·보안 체크리스트·회귀 통과를 확인하고 통과 시에만 다음 단계로 진행한다.")
|
||||
|
||||
# ---------- 8. 보안·개인정보 ----------
|
||||
h(doc,1,"8. 보안 및 개인정보보호 계획")
|
||||
h(doc,2,"8.1 보안 불변 원칙")
|
||||
table(doc, ["구분","원칙"],
|
||||
[["외부 API 정책","온프레미스 모델 우선. 텍스트 AI는 승인된 단일 예외 경로만, 이미지 생성은 킨텍스 고유 승인 예외(워커 전용). 그 외 외부 API 금지."],
|
||||
["자격증명 보호","내부 IP·SSH 계정·비밀번호·해시·API 키·워커 토큰을 API 응답·로그·에러·커밋에 절대 노출 금지."],
|
||||
["암호화 저장","admin 비밀번호는 env(AES-256-GCM 암호문)+별도 키파일로 기동 시 재시드(하드코딩 시드 금지). 저장 자격증명은 암호화 컬럼에만."],
|
||||
["인증·권한","JWT(HS256)+2차 인증(OTP/EMAIL)+로그인 실패 잠금. 행사 단위 RBAC. 미등록 장치업체 초대·응찰 차단."],
|
||||
["AI 이미지 워터마크","시각화 결과는 항상 워터마크·고지 포함(참고용, 계약·인허가 서류 사용 불가)."],
|
||||
["오류 응답","스택트레이스·relation/컬럼명·내부 경로 미노출. 코드+요약 메시지만 반환."],
|
||||
["감사 추적","관리자·인증·권한·낙찰·설계변경·개인정보 접근 등 민감 액션 감사 로그 기록(비밀값 미기재)."]],
|
||||
widths=[3.5, 12.5])
|
||||
h(doc,2,"8.2 개인정보보호")
|
||||
bullets(doc,[
|
||||
"관람객 배지 QR·리드캡처 데이터는 수집·이용 동의 및 보존 기간 정책을 필수 적용.",
|
||||
"리드(개인정보) 접근은 감사 로그로 전수 기록, 표시 필드는 비민감 항목으로 최소화.",
|
||||
"EDM 발송은 정보통신망법·수신 동의 규정을 준수.",
|
||||
"개인정보는 최소 수집·목적 제한·접근 통제(RBAC) 원칙을 적용.",
|
||||
])
|
||||
h(doc,2,"8.3 보안 체크리스트(배포·리뷰 전)")
|
||||
bullets(doc,[
|
||||
"응답/로그/커밋에 IP·SSH·비번·해시·API 키·워커 토큰 노출 0(grep).",
|
||||
"AI 키(텍스트/이미지)는 env only — 코드·DB·커밋 미포함.",
|
||||
"admin 비번 하드코딩 시드 없음(env 재시드).",
|
||||
"시각화 이미지 응답에 워터마크 필드 포함.",
|
||||
"DB/서버 오류 → 요약 메시지(스택·relation 명 미노출).",
|
||||
"관리자 게이트·행사 RBAC·미등록업체 차단 동작.",
|
||||
])
|
||||
|
||||
# ---------- 9. 시험 계획 ----------
|
||||
h(doc,1,"9. 시험(테스트) 계획 개요")
|
||||
h(doc,2,"9.1 시험 레벨")
|
||||
table(doc, ["레벨","대상","방식"],
|
||||
[["단위 시험","서비스·매퍼·룰 엔진·배선 엔진","개발자 단위 테스트, 공간 연산(PostGIS) 정합 검증"],
|
||||
["통합 시험","모듈 간 경계면(API↔프론트, 큐↔워커)","API 응답 shape 대조, WebSocket/큐 왕복"],
|
||||
["시스템 시험","역할별 시나리오(주최자/참가/업체/홀매니저/관람객)","E2E 시나리오·규정 검증·시각화 워터마크 확인"],
|
||||
["회귀·스모크","전 기능","배포 전 스모크(헬스+라우터 등록+로그인 프로브)·회귀"],
|
||||
["보안 시험","보안 불변 규칙","자격증명 미노출·RBAC 우회·스택트레이스 차단 점검"]],
|
||||
widths=[2.6, 6.4, 7.0])
|
||||
h(doc,2,"9.2 시험 원칙")
|
||||
bullets(doc,[
|
||||
"각 모듈 완성 직후 QA가 점진 검증하고, 통과할 때까지 반려.",
|
||||
"공간 로직(배치·배선·규정 검증)과 시각화 워터마크 강제는 필수 검증 항목.",
|
||||
"시험 케이스·결과는 별첨 [WBS_및_일정.xlsx]의 '시험케이스' 시트와 형상관리 하 리포트로 관리.",
|
||||
])
|
||||
|
||||
# ---------- 10. 이관·교육·유지보수 ----------
|
||||
h(doc,1,"10. 이관·교육 및 유지보수 계획")
|
||||
h(doc,2,"10.1 이관 계획")
|
||||
bullets(doc,[
|
||||
"빌드 산출물(프론트 번들·백엔드 실행 파일·워커 서비스)과 마스터데이터·형상 이력을 발주처 운영 환경으로 이관.",
|
||||
"이관 시 무중단·헬스게이트·롤백(Fail-Safe) 배포 절차를 적용.",
|
||||
"운영 도메인·운영 환경변수 배선은 발주처 승인 하에 수행.",
|
||||
])
|
||||
h(doc,2,"10.2 교육 계획")
|
||||
table(doc, ["대상","교육 내용","형식"],
|
||||
[["운영자/관리자","백오피스(RBAC·마스터데이터·규정 룰셋)·감사·시스템설정","운영자 지침서+실습"],
|
||||
["킨텍스 운영팀(홀매니저)","배치 검수·규정 플래그·현장 운영 대시보드","사용자 지침서+실습"],
|
||||
["주최자/참가업체","포털 사용(배정·설계·유틸리티·옥션·관람객)","사용자 지침서·온라인 가이드"]],
|
||||
widths=[4.0, 8.0, 4.0])
|
||||
h(doc,2,"10.3 유지보수 계획")
|
||||
bullets(doc,[
|
||||
"장애 대응·정기 점검·규정/요율 개정 반영(룰셋 버전 관리)을 유지보수 범위로 한다.",
|
||||
"운영 런북(헬스체크·재기동·롤백)을 기준으로 SM 운영 체계를 수립.",
|
||||
"규정·요율 연 단위 개정은 관리자 백오피스 룰셋 버전 관리로 안전 반영.",
|
||||
])
|
||||
|
||||
# ---------- 11. 위험 관리 (sources.RISKS) ----------
|
||||
h(doc,1,"11. 위험(리스크) 관리")
|
||||
para(doc,"주요 리스크와 대응 방안은 아래와 같으며, 형상관리 하 리스크 대장으로 지속 관리한다.")
|
||||
table(doc, ["ID","리스크","영향","대응 방안"],
|
||||
[list(x) for x in S.RISKS], widths=[1.2, 4.2, 2.6, 8.0])
|
||||
|
||||
# ---------- 12. 의사소통·보고 ----------
|
||||
h(doc,1,"12. 의사소통·보고 체계")
|
||||
table(doc, ["보고/회의","주기","참여","내용"],
|
||||
[["착수 보고","사업 착수 시","발주처·PMO","범위·일정·조직·리스크 확정"],
|
||||
["주간 진척 보고","주 1회","PMO·개발 리드","진척·이슈·리스크·다음 주 계획"],
|
||||
["Phase 종료 보고","각 Phase 종료","발주처·PMO","산출물·품질 게이트 결과·다음 단계"],
|
||||
["단계별 검토회의","마일스톤","전 트랙","경계면 정합·아키텍처 준수 점검"],
|
||||
["완료(준공) 보고","사업 종료","발주처·PMO","통합 QA 결과·이관·교육·유지보수 인계"]],
|
||||
widths=[3.6, 2.6, 3.4, 6.4])
|
||||
para(doc,"긴급 이슈는 즉시 에스컬레이션하며, 모든 의사결정·변경은 형상관리(14장) 절차로 기록한다.", size=9, italic=True, color=GREY)
|
||||
|
||||
# ---------- 13. 산출물 목록 (sources.DELIVERABLES) ----------
|
||||
h(doc,1,"13. 산출물 목록(단계별 표준 산출물)")
|
||||
para(doc,"단계별 표준 산출물은 아래와 같으며, 상세 목록·형식·담당·상태는 별첨 [WBS_및_일정.xlsx]의 '산출물목록' 시트에 관리한다.")
|
||||
table(doc, ["단계","산출물","형식","담당","상태"],
|
||||
[[d[0], d[1], d[2], d[3], d[4]] for d in S.DELIVERABLES],
|
||||
widths=[2.4, 6.2, 2.2, 3.0, 2.2])
|
||||
|
||||
# ---------- 14. 형상관리 ----------
|
||||
h(doc,1,"14. 형상관리")
|
||||
h(doc,2,"14.1 형상관리 원칙")
|
||||
bullets(doc,[
|
||||
"모든 소스·문서·설정은 Git(Gitea) 저장소로 버전 통제하며, 변경은 커밋 이력으로 추적.",
|
||||
"산출물(계획서·설계서·시험·지침서)은 docs/deliverables/ 지정 경로에서 형상 관리.",
|
||||
"DB 스키마는 Flyway 순번 마이그레이션을 형상 권위로 하며, baseline 규율과 시드 멱등화를 준수.",
|
||||
"배포는 형상(커밋) 기준 CI/CD Webhook으로 무중단·헬스게이트·롤백 수행.",
|
||||
])
|
||||
h(doc,2,"14.2 형상 항목 및 절차")
|
||||
table(doc, ["형상 항목","관리 방식","변경 절차"],
|
||||
[["소스코드","Git 브랜치·PR·리뷰","리뷰 승인 후 병합, 커밋 메시지 이력화"],
|
||||
["문서/산출물","Git 버전·개정 이력","개정 이력 기록 후 갱신"],
|
||||
["DB 스키마","Flyway 마이그레이션","신규 마이그 번호=현재 최대+1(충돌 검사)"],
|
||||
["설정/환경변수","env 주입·시크릿 분리","키·비번은 형상에서 제외(gitignore)"],
|
||||
["배포","CI/CD Webhook·헬스게이트","실패 시 자동 롤백"]],
|
||||
widths=[3.4, 5.6, 7.0])
|
||||
|
||||
doc.add_paragraph()
|
||||
para(doc,"— 본 사업수행계획서는 행정안전부 정보화사업 산출물 표준 목차를 준용하여 킨텍스 자동전시시스템 구축 사업의 원본 내용으로 작성되었다. —",
|
||||
size=9, italic=True, color=GREY, align=CA)
|
||||
|
||||
doc.save(OUT)
|
||||
print("SAVED:", OUT)
|
||||
264
docs/deliverables/_gen/sources.py
Normal file
264
docs/deliverables/_gen/sources.py
Normal file
@ -0,0 +1,264 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
sources.py — 산출물 생성의 단일 데이터 소스 로더.
|
||||
docs/PLANNING.md · docs/IMPLEMENTATION_BACKLOG.md · docs/FEATURE_BACKLOG_100.md · docs/architecture/*
|
||||
를 파싱하여 구조화 데이터(모듈·Phase/WBS·기능/요구사항)를 반환한다.
|
||||
소스 문서가 갱신되면 _gen 재실행만으로 전 산출물이 갱신된다.
|
||||
파싱 실패 시 임베디드 폴백을 사용해 결정적 산출을 보장한다.
|
||||
"""
|
||||
import os, re
|
||||
|
||||
DOCS = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) # kintex/docs
|
||||
ROOT = os.path.abspath(os.path.join(DOCS, "..")) # kintex
|
||||
|
||||
META = {
|
||||
"project": "킨텍스 자동전시시스템",
|
||||
"project_en": "Exhibition Automation Platform",
|
||||
"title": "킨텍스 자동전시시스템 구축 사업수행계획서",
|
||||
"version": "v1.0",
|
||||
"date": "2026-07-11",
|
||||
"owner": "지오정보기술(주) — GUARDiA 플랫폼 사업본부",
|
||||
"client": "킨텍스(KINTEX, 한국국제전시장)",
|
||||
"dev_domain": "kintex.zioinfo.co.kr(개발)",
|
||||
"std": "행정안전부 정보화사업 산출물 표준 목차 준용",
|
||||
}
|
||||
|
||||
def _read(rel):
|
||||
p = os.path.join(DOCS, rel)
|
||||
try:
|
||||
with open(p, "r", encoding="utf-8") as f:
|
||||
return f.read()
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
def _table_rows(text, min_cols=2):
|
||||
"""마크다운 표의 데이터 행(| a | b |)을 셀 리스트로 반환(헤더/구분선 제외)."""
|
||||
out = []
|
||||
for line in text.splitlines():
|
||||
s = line.strip()
|
||||
if not s.startswith("|"):
|
||||
continue
|
||||
if re.match(r"^\|[\s:\-\|]+\|?$", s): # separator
|
||||
continue
|
||||
cells = [c.strip() for c in s.strip("|").split("|")]
|
||||
if len(cells) >= min_cols:
|
||||
out.append(cells)
|
||||
return out
|
||||
|
||||
# ---------------- 모듈(M1~M18 + §5B) : PLANNING 기능 우선순위 총괄표 ----------------
|
||||
_MOD_FALLBACK = [
|
||||
("M2","플로어플랜 스튜디오(부스 배치)","P0"),("M3","부스 설계 스튜디오","P0"),
|
||||
("M4","유틸리티 설계 자동화(전기·조명/네트워크·급배수)","P0"),("M5","나노바나나 시각화","P0"),
|
||||
("M1","홀 배정·자동 견적","P1"),("M6","서류·마일스톤 워크플로","P1"),
|
||||
("M7","등록업체 매칭·견적","P1"),("M9","정산·결제","P1"),
|
||||
("M15","공사/장치 옥션(역경매·견적서·낙찰)","P1"),("M10","관람객 등록·배지·체크인·리드캡처","P1"),
|
||||
("M12","마케팅·EDM·공개 홍보 사이트","P1"),("M16","경영분석 BI","P1"),
|
||||
("M17","CMS(콘텐츠·마이크로사이트·다국어)","P1"),("M18","관리자 백오피스","P1"),
|
||||
("§5B","공통/시스템관리 레이어 + JWT+2FA","P1"),
|
||||
("M8","반입/반출 물류","P2"),("M11","비즈니스 매칭","P2"),
|
||||
("M13","wayfinding·실내 내비","P2"),("M14","현장운영(혼잡·안전·주차·에너지)","P2"),
|
||||
]
|
||||
def modules():
|
||||
text = _read("PLANNING.md")
|
||||
mods, seen = [], set()
|
||||
for cells in _table_rows(text, min_cols=4):
|
||||
mid = cells[0].replace("**","").strip()
|
||||
if not re.match(r"^(M\d+|§5B)$", mid):
|
||||
continue
|
||||
if mid in seen:
|
||||
continue
|
||||
name = re.sub(r"\*\*|`", "", cells[1]).strip()
|
||||
pri = ""
|
||||
for c in cells:
|
||||
m = re.search(r"P0|P1|P2", c)
|
||||
if m: pri = m.group(0); break
|
||||
if not name or not pri:
|
||||
continue
|
||||
seen.add(mid); mods.append((mid, name, pri))
|
||||
if len(mods) < 15:
|
||||
return list(_MOD_FALLBACK)
|
||||
order = {"P0":0,"P1":1,"P2":2}
|
||||
mods.sort(key=lambda x: order.get(x[2],9))
|
||||
return mods
|
||||
|
||||
# ---------------- Phase / WBS : IMPLEMENTATION_BACKLOG Phase 표 ----------------
|
||||
_PHASE_FALLBACK = [
|
||||
("A","아키텍처·거버넌스",[
|
||||
("A-1","애플리케이션 아키텍처(모듈 경계·API 표준)","아키텍트(AA)","docs/architecture/app.md"),
|
||||
("A-2","시스템 아키텍처·NFR(확장성·HA·성능·보안영역)","아키텍트(SA)","docs/architecture/system.md"),
|
||||
("A-3","기술 표준(스택·빌드/배포·관측성·AiTextRouter)","아키텍트(TA)","docs/architecture/tech.md"),
|
||||
("A-4","데이터 아키텍처(전사 ERD·공간·마스터·BI마트)","아키텍트(DA)","ERD·data.md"),
|
||||
("A-5","네트워크 아키텍처(DMZ/내부망·방화벽·부하분산)","아키텍트(NA)","network.md"),
|
||||
("A-6","아키텍처↔PLANNING 정합 검증","reviewer","불일치 0/티켓화"),
|
||||
]),
|
||||
("B","공통/시스템관리 레이어(전 모듈 선행)",[
|
||||
("B-0","스캐폴드(백엔드·프론트·DB·마이그레이션)","BE·FE·DB","빌드·마이그레이션 통과"),
|
||||
("B-1","인증 JWT+RBAC+2차인증(OTP)+실패잠금+admin env","COM·BE","2FA 등록/검증/초기화 왕복"),
|
||||
("B-2","시스템관리(사용자·역할·공통코드·메뉴·감사·설정)","COM·ADM·DB","CRUD·RBAC 가드·감사"),
|
||||
("B-3","공통 업무기능(일지·일정·쪽지·통계·공지·회의·알림)","COM·DB","각 모듈 API 왕복"),
|
||||
("B-4","공통 컴포넌트·프론트 공통(2FA·그리드·달력·모달)","COM·FE","아키텍처 표준 정합"),
|
||||
("B-5","점진 QA(2FA·RBAC·PII·경계면)","QA","통과까지 반려"),
|
||||
]),
|
||||
("C","P0 부스 시공 코어",[
|
||||
("C-M2","플로어플랜(배치 3안·병합·규정검증 PostGIS)","DB·BE·AI·FE·QA","3안 생성·병합·규정 재검증"),
|
||||
("C-M3","부스 설계(조립/독립·규정 사전검증·3안)","BE·AI·FE·QA","위반 사전 플래깅·컨펌"),
|
||||
("C-M4","유틸리티 배선(전기/네트워크/급배수·견적·위치표시도)","DB·BE·FE·QA","최단 배선·견적·위치표시도"),
|
||||
("C-M5","나노바나나 시각화(RenderJob·워커·워터마크)","BE·VIZ·FE·QA","실생성/미승인 시 목·워터마크"),
|
||||
("C-C","역할별 공통 화면(로그인·대시보드·현장 모바일)","FE·QA","역할별 진입·상태 처리"),
|
||||
]),
|
||||
("D","P1 도메인(역할별 포털)",[
|
||||
("D-M15","공사/장치 옥션(견적서 제출·역경매·낙찰)","BID·BE·DB·FE·QA","견적서·역경매·낙찰 왕복"),
|
||||
("D-M10","관람객·현장(등록·티켓·배지·체크인·리드)","VIS·BE·DB·FE·QA","등록→배지→체크인→리드"),
|
||||
("D-M12/17","마케팅·공개사이트·CMS(SEO·다국어·EDM)","CMS·BE·FE·QA","공개 조회·게시 워크플로"),
|
||||
("D-M16","경영분석 BI(가동률·P&L·ROI·수요예측·KPI)","BI·DA·BE·FE·QA","지표 집계·드릴다운"),
|
||||
("D-M18","관리자 백오피스(마스터데이터·룰셋 버전)","ADM·BE·FE·QA","RBAC·마스터 CRUD·룰셋"),
|
||||
("D-M1/6/7/9","배정·서류·매칭·정산","BE·FE·DB·QA","견적·마일스톤·정산"),
|
||||
]),
|
||||
("E","P2 + 빌드·배포",[
|
||||
("E-M11","비즈니스 매칭(추천·미팅예약)","VIS·AI","추천·미팅예약"),
|
||||
("E-M13","wayfinding·실내 내비(M2 재사용)","VIS·FE","부스 검색·경로"),
|
||||
("E-M14","현장운영(혼잡·안전·주차·에너지)","BE·FE","대시보드·경보"),
|
||||
("E-DEP","역할별 번들·jar·워커 빌드·CI/CD·systemd","DEV","push→배포"),
|
||||
]),
|
||||
]
|
||||
def phases():
|
||||
"""[(letter, title, [(id, task, owner, done), ...]), ...]"""
|
||||
text = _read("IMPLEMENTATION_BACKLOG.md")
|
||||
if not text:
|
||||
return _PHASE_FALLBACK
|
||||
out = []
|
||||
cur = None
|
||||
lines = text.splitlines()
|
||||
hdr_re = re.compile(r"^##\s+Phase\s+([A-E])\s*[—\-–]\s*(.+)$")
|
||||
for line in lines:
|
||||
hm = hdr_re.match(line.strip())
|
||||
if hm:
|
||||
if cur: out.append(cur)
|
||||
title = re.sub(r"\([^)]*\)", "", hm.group(2))
|
||||
title = re.sub(r"[★((].*$", "", title).strip()
|
||||
cur = (hm.group(1), title, [])
|
||||
continue
|
||||
s = line.strip()
|
||||
if cur and s.startswith("|"):
|
||||
if re.match(r"^\|[\s:\-\|]+\|?$", s):
|
||||
continue
|
||||
cells = [c.strip() for c in s.strip("|").split("|")]
|
||||
if not cells or cells[0] in ("ID","Phase") or not re.match(r"^[A-E]-", cells[0]):
|
||||
continue
|
||||
# 표 형태 2종: (ID,작업,담당,완료) 또는 (ID,모듈,작업,담당,완료)
|
||||
if len(cells) >= 5:
|
||||
_id, task, owner, done = cells[0], cells[2], cells[3], cells[4]
|
||||
elif len(cells) == 4:
|
||||
_id, task, owner, done = cells[0], cells[1], cells[2], cells[3]
|
||||
else:
|
||||
continue
|
||||
cur[2].append((_id, re.sub(r"\*\*|`","",task), re.sub(r"\*\*","",owner), re.sub(r"\*\*","",done)))
|
||||
if cur: out.append(cur)
|
||||
ok = [p for p in out if p[2]]
|
||||
return ok if len(ok) >= 4 else _PHASE_FALLBACK
|
||||
|
||||
# 마스터 일정(착수 상대 주차) — 큐레이션(마일스톤)
|
||||
SCHEDULE = [
|
||||
("A","아키텍처·거버넌스","W1","W3","M1 아키텍처 베이스라인 승인(W3)"),
|
||||
("B","공통/시스템관리 레이어","W3","W8","M2 공통 레이어·인증(2FA) 완료(W8)"),
|
||||
("C","P0 부스 시공 코어","W8","W16","M3 P0 부스 코어 시연·첫 상용 행사 적용(W16)"),
|
||||
("D","P1 도메인","W16","W24","M4 옥션·관람객·BI·관리자 통합(W24)"),
|
||||
("E","P2 + 빌드·배포","W24","W28","M5 통합 QA 통과·운영 배포(W28)"),
|
||||
]
|
||||
|
||||
# ---------------- 기능/요구사항 : FEATURE_BACKLOG_100 C1~C12 ----------------
|
||||
def features():
|
||||
"""[(fid, name, category, priority, ai, module), ...] — 100대 기능(RTM/요구사항 원천)."""
|
||||
text = _read("FEATURE_BACKLOG_100.md")
|
||||
out, seen = [], set()
|
||||
cur_cat = ""
|
||||
cat_re = re.compile(r"^###\s+(C\d+)\.\s*(.+?)\s*(?:\(\d+\)|—|$)")
|
||||
stop = False
|
||||
for line in text.splitlines():
|
||||
s = line.strip()
|
||||
cm = cat_re.match(s)
|
||||
if cm:
|
||||
cur_cat = cm.group(1) + " " + re.sub(r"\s*[—-].*$","",cm.group(2)).strip()
|
||||
stop = False
|
||||
continue
|
||||
# C 표가 끝나고 'AI 자동화 맵' 등 다른 섹션이면 중단 표시
|
||||
if s.startswith("##") and "카테고리" not in s and not s.startswith("###"):
|
||||
stop = True
|
||||
if stop:
|
||||
continue
|
||||
m = re.match(r"^\|\s*(F\d{3})\s*\|(.+)$", s)
|
||||
if m and cur_cat:
|
||||
fid = m.group(1)
|
||||
if fid in seen:
|
||||
continue
|
||||
cells = [c.strip() for c in m.group(2).strip("|").split("|")]
|
||||
name = re.sub(r"\*\*|`","",cells[0]).strip() if cells else fid
|
||||
pri = next((re.search(r"P[012]",c).group(0) for c in cells if re.search(r"P[012]",c)), "")
|
||||
ai = ""
|
||||
for c in cells:
|
||||
cc = c.strip()
|
||||
if cc in ("Y","N","부분"):
|
||||
ai = cc; break
|
||||
module = next((re.sub(r"\*\*|`","",c) for c in cells if re.search(r"(M\d+|§5B|§1A|§6)", c)), "")
|
||||
seen.add(fid)
|
||||
out.append((fid, name, cur_cat, pri, ai, module))
|
||||
return out
|
||||
|
||||
# ---------------- 산출물 목록(표준 단계별) ----------------
|
||||
DELIVERABLES = [
|
||||
# (단계, 산출물명, 형식, 담당, 상태, 비고)
|
||||
("착수(계획)","사업수행계획서","DOCX","PMO","작성완료","본 산출물(gen_docx)"),
|
||||
("착수(계획)","WBS 및 마스터 일정","XLSX","PMO","작성완료","gen_xlsx(WBS/일정/투입/산출물)"),
|
||||
("착수(계획)","산출물 목록","XLSX","PMO","작성완료","gen_xlsx(산출물목록 시트)"),
|
||||
("착수(계획)","착수보고 자료","PPTX","PMO","작성완료","gen_pptx(착수보고)"),
|
||||
("분석","요구사항 정의서","DOCX","기획(planner)","작성완료","gen_reqspec(100대 기능 원천)"),
|
||||
("분석","요구사항 추적표(RTM)","XLSX","QA/기획","작성완료","gen_xlsx(RTM 시트)"),
|
||||
("분석","기획서(PLANNING)","MD","기획(planner)","기보유","docs/PLANNING.md v3.0"),
|
||||
("설계","개발계획서","DOCX","PMO/개발리드","작성완료","gen_devplan"),
|
||||
("설계","화면설계서·UI 스펙(design)","MD/DOCX","디자이너","기보유","docs/design.md"),
|
||||
("설계","아키텍처 설계서(앱/시스템/기술/데이터/네트워크)","MD","아키텍트","기보유","docs/architecture/*"),
|
||||
("설계","데이터 모델(전사 ERD·BI 데이터마트)","MD/SQL","DA/DB","진행","설계 트랙"),
|
||||
("설계","API 설계서(계약)","MD","AA/백엔드","기보유","docs/API_GUIDE.md"),
|
||||
("구현","소스코드(백엔드·프론트·워커)","Git","개발","진행","src/, tools/nanobanana"),
|
||||
("구현","DB 스키마·마이그레이션","SQL","DB","진행","Flyway"),
|
||||
("시험","시험계획서","DOCX","QA","작성완료","gen_testplan"),
|
||||
("시험","시험케이스·시험결과서","XLSX","QA","작성완료","gen_xlsx(시험케이스 시트)"),
|
||||
("이관","배포/운영 런북·이관 확인서","MD","DevOps","기보유","docs/OPS_RUNBOOK.md"),
|
||||
("교육","사용자 지침서·운영자 지침서","DOCX/PPTX","기술문서","예정","완성+QA 후"),
|
||||
("종료","완료(준공) 보고서","DOCX","PMO","예정","사업 종료 시"),
|
||||
]
|
||||
|
||||
# ---------------- 투입 인력 계획(M/M) ----------------
|
||||
STAFFING = [
|
||||
("사업 PM/PMO","1","특급","A~E 상시","6.5"),
|
||||
("아키텍트(AA/SA/TA)","2","특급","A 집중","3.0"),
|
||||
("데이터 아키텍트(DA)","1","고급","A·D","2.0"),
|
||||
("네트워크 아키텍트(NA)","1","고급","A·E","1.0"),
|
||||
("공통/시스템관리 개발","2","중급","B 집중","4.0"),
|
||||
("백엔드 개발","3","중·고급","B~D","12.0"),
|
||||
("프론트엔드 개발","3","중·고급","B~E","12.0"),
|
||||
("DB 엔지니어(PostGIS)","1","고급","B~D","4.5"),
|
||||
("AI 개발","1","고급","C~D","3.0"),
|
||||
("시각화(나노바나나)","1","고급","C","2.0"),
|
||||
("도메인 개발(옥션/관람객/CMS/BI/관리자)","3","중·고급","D 집중","9.0"),
|
||||
("QA","2","중급","B~E 상시","8.0"),
|
||||
("DevOps","1","고급","B·E","2.5"),
|
||||
("디자이너","1","고급","A~E","3.0"),
|
||||
]
|
||||
|
||||
RISKS = [
|
||||
("R1","홀 실측 CAD 원본 미확보","배치 정밀도","공개 규격 근사 도면으로 진행, 확보 시 대체"),
|
||||
("R2","이미지 생성(외부) 승인 게이트","M5 시각화","미승인 시 목/degraded, 승인 후 라이브"),
|
||||
("R3","외부 시스템(작업신고/PG/KT) 연동 미공개","M6/M9/M4b","제출용 파일·릴레이 방식, 협의 후 연동"),
|
||||
("R4","등록업체 옥션 참여율·응찰 품질","M15 실효성","등록업체 게이트·AI 자료 공정 비교 유인"),
|
||||
("R5","개인정보(리드·배지) 규정 위반","법적 리스크","동의·보존정책·감사·최소 수집"),
|
||||
("R6","AI 시각화 오용(계약 서류)","분쟁 리스크","워터마크·고지 강제(UI·응답·PDF)"),
|
||||
("R7","공통 레이어 지연","일정","Phase B 인력 선집중·병행 착수"),
|
||||
("R8","규정·요율 개정","정합성","룰셋 버전 관리·과거 버전 보존"),
|
||||
]
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("modules:", len(modules()))
|
||||
ph = phases(); print("phases:", [(p[0], len(p[2])) for p in ph])
|
||||
print("features:", len(features()))
|
||||
print("sample feature:", features()[0] if features() else None)
|
||||
BIN
docs/deliverables/개발계획서.pptx
Normal file
BIN
docs/deliverables/개발계획서.pptx
Normal file
Binary file not shown.
BIN
docs/deliverables/사업수행계획서.docx
Normal file
BIN
docs/deliverables/사업수행계획서.docx
Normal file
Binary file not shown.
1025
docs/deliverables/지침서/_gen/gen_guides_pptx.py
Normal file
1025
docs/deliverables/지침서/_gen/gen_guides_pptx.py
Normal file
File diff suppressed because it is too large
Load Diff
BIN
docs/deliverables/지침서/개발자지침서.pptx
Normal file
BIN
docs/deliverables/지침서/개발자지침서.pptx
Normal file
Binary file not shown.
BIN
docs/deliverables/지침서/사용자지침서.pptx
Normal file
BIN
docs/deliverables/지침서/사용자지침서.pptx
Normal file
Binary file not shown.
BIN
docs/deliverables/지침서/운영자지침서.pptx
Normal file
BIN
docs/deliverables/지침서/운영자지침서.pptx
Normal file
Binary file not shown.
1115
docs/deliverables/프로그램사양서/_gen/gen_program_spec_pptx.py
Normal file
1115
docs/deliverables/프로그램사양서/_gen/gen_program_spec_pptx.py
Normal file
File diff suppressed because it is too large
Load Diff
BIN
docs/deliverables/프로그램사양서/프로그램사양서.pptx
Normal file
BIN
docs/deliverables/프로그램사양서/프로그램사양서.pptx
Normal file
Binary file not shown.
960
docs/design.md
960
docs/design.md
@ -1,9 +1,11 @@
|
||||
# 킨텍스 AI 전시관리 시스템 — UI 디자인 스펙 (design.md)
|
||||
|
||||
> 작성: 디자인 에이전트(designer) · 작성일: 2026-07-11 · 버전: v1.3
|
||||
> 근거 문서: `docs/PLANNING.md` v1.0 (모듈 맵 M1~M9, P0 기능, 나노바나나 표준 샷 세트 S1~S7) · Stitch 산출물 `stitch_kintex_ai_system_architect/`(18화면 + DESIGN.md 3변형) 학습·정합
|
||||
> 작성: 디자인 에이전트(designer) · 작성일: 2026-07-11 · 버전: **v2.1**
|
||||
> 근거 문서: `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 프롬프트만 영어.
|
||||
>
|
||||
> **v2.0 범위 확장**: v1.3까지는 P0 코어 + SCR-13~17 중심 19화면이었다. v2.0은 **PLANNING v3.0의 전 모듈(M1~M18)·공통레이어(§5B)·6+2 역할(전시업체·공사/장치업체·주최자·킨텍스 직원(운영·관리)·시스템 관리자(테넌트/플랫폼)·일반 대중 공개사이트) × 웹(MDI)·모바일**을 빠짐없이 커버하는 **전 화면 인벤토리**로 확장한다. 기존 SCR-01~17·SCR-M1/M2 번호·토큰은 **재번호화 없이 유지**하고, 신규 화면만 이어서 부여한다 — 업무 웹 `SCR-18+`, 관리자 백오피스 `SCR-A*`, 공개사이트 `SCR-P*`, 모바일 `SCR-M3+`. 전 화면 요약 매트릭스는 **§2.8**, 화면 상세는 §3(코어·도메인 웹)·§3A(관리자)·§3B(공개사이트)·§4(모바일)에 정의한다.
|
||||
|
||||
---
|
||||
|
||||
@ -285,9 +287,150 @@ An enterprise MDI (multiple document interface) application shell for "KINTEX AI
|
||||
|
||||
---
|
||||
|
||||
## 2.8 전 화면 인벤토리 매트릭스 (v2.0)
|
||||
|
||||
> **완결성 원칙**: PLANNING v3.0의 어떤 모듈/역할에서도 "design.md에 화면이 없어 디자인 불가"인 페이지가 0개가 되도록 아래 인벤토리로 전수 커버한다. 상태 컬럼 — **✅Stitch**(`stitch_kintex_ai_system_architect/`에서 이미 Stitch 생성) · **✅구현**(현행 프론트 `src/frontend`/`mobile` 구현 존재) · **🔲미생성**(신규 정의, Stitch·구현 착수 전).
|
||||
|
||||
### 2.8-1. 모듈 × 역할 × 채널 요약 매트릭스
|
||||
|
||||
역할 약칭: **주**(주최자) · **참**(참가업체) · **장**(장치·공사업체) · **홀**(킨텍스 직원 — 홀매니저·운영) · **관**(시스템 관리자 — 테넌트/플랫폼) · **대**(일반 대중·관람객). 셀 = 대표 SCR(웹) / (모바일은 M접두). 다중 역할 공유 화면은 대표 역할에 표기.
|
||||
|
||||
| 모듈 | 주최자 | 참가업체 | 장치업체 | 킨텍스직원 | 관리자 | 대중/관람객 |
|
||||
|---|---|---|---|---|---|---|
|
||||
| **공통 인증/셸** | SCR-01·49·50·51 · MDI셸 §2.7 | SCR-01 | SCR-01 | SCR-01 | SCR-01 | SCR-P4 |
|
||||
| **M1 홀배정·견적** | SCR-15·18·19 | SCR-20 | — | SCR-15 | SCR-A7 | — |
|
||||
| **M2 배치** | SCR-03·04·19 | — | SCR-03(RO) | SCR-03(RO) | — | SCR-P3 |
|
||||
| **M3 부스설계** | — | SCR-06 | SCR-06·09·38 / M1 | — | — | — |
|
||||
| **M4 유틸리티** | — | SCR-07·08 | SCR-07(검토) | SCR-10(부하) | — | — |
|
||||
| **M5 나노바나나** | SCR-04 | SCR-06·12 | SCR-12 | SCR-11(참고) | — | — |
|
||||
| **M6 서류·마일스톤** | SCR-22·23 | SCR-05 | — | SCR-24·10 | — | — |
|
||||
| **M7 등록업체 매칭** | SCR-24 | SCR-24 | SCR-38 | — | SCR-A7 | — |
|
||||
| **M8 반입/반출** | SCR-25 | SCR-25 | SCR-25 / M10 | SCR-25 | — | — |
|
||||
| **M9 정산·결제** | SCR-21 | SCR-21 | — | — | SCR-21 | SCR-P7(티켓결제 연계) |
|
||||
| **M10 관람·배지·리드·티켓** | SCR-30 | SCR-32 / M4 | — | SCR-31 | — | SCR-P4·P7·P8 / M5·M14·M15 |
|
||||
| **M11 비즈매칭** | SCR-30 | — / M8 | — | — | — | / M8 |
|
||||
| **M12 마케팅·공개사이트** | SCR-33·34 | SCR-36 | — | — | — | SCR-P1·P2·P3·P5 |
|
||||
| **M13 wayfinding** | — | — | — | POI=SCR-A7/SCR-35 | — | / M6·M7 |
|
||||
| **M14 현장운영** | — | — | — | SCR-14 | — | — |
|
||||
| **M15 공사/장치 옥션** | SCR-26·29 | SCR-26·29 | SCR-27·28 / M13 | — | — | — |
|
||||
| **M16 경영분석 BI** | SCR-13 | SCR-13(참ROI) | — | SCR-13 | SCR-13 | — |
|
||||
| **M17 CMS** | SCR-35 | SCR-36 | — | — | SCR-35·37 | SCR-P5 |
|
||||
| **M18 관리자·시스템관리** | — | — | — | — | SCR-16·A1~A10 | — |
|
||||
| **§5B 공통 업무기능** | SCR-39~48 | SCR-39~48 | SCR-38·39~48 | SCR-39~48 | SCR-39~48 | — |
|
||||
|
||||
### 2.8-2. 전 화면 SCR 인덱스 (상태 포함)
|
||||
|
||||
**웹 — 코어·도메인 (§3)**
|
||||
|
||||
| SCR | 화면명 | 모듈 | 역할 | P | 상태 |
|
||||
|---|---|---|---|---|---|
|
||||
| SCR-01 | 로그인 & 행사 워크스페이스 선택 | 공통 | 전 | P0 | ✅Stitch ✅구현 |
|
||||
| SCR-02 | 주최자 대시보드 | M1·M6 | 주 | P0 | ✅Stitch ✅구현 |
|
||||
| SCR-03 | 부스 배치 에디터 | M2 | 주·홀 | P0 | ✅Stitch ✅구현 |
|
||||
| SCR-04 | 배치안 비교(S7) | M2·M5 | 주 | P0 | ✅Stitch ✅구현 |
|
||||
| SCR-05 | 참가업체 부스 홈 | M3·M6 | 참 | P0 | ✅Stitch ✅구현 |
|
||||
| SCR-06 | 부스 설계 스튜디오 | M3·M5 | 참·장 | P0 | ✅Stitch ✅구현 |
|
||||
| SCR-07 | 유틸리티 배선 뷰 | M4 | 참·장 | P0 | ✅Stitch ✅구현 |
|
||||
| SCR-08 | 유틸리티 신청 요약·위치표시도 | M4b | 참 | P0 | ✅Stitch ✅구현 |
|
||||
| SCR-09 | 규정 검증 리포트 | M3 | 장 | P0 | ✅Stitch ✅구현 |
|
||||
| SCR-10 | 홀매니저 승인 큐 | M2·M3·M6 | 홀 | P0 | ✅Stitch ✅구현 |
|
||||
| SCR-11 | 홀매니저 검수 상세 | M2·M3 | 홀 | P0 | ✅Stitch ✅구현 |
|
||||
| SCR-12 | 시각화 갤러리 | M5 | 전 | P0 | ✅Stitch ✅구현 |
|
||||
| SCR-13 | 경영분석 대시보드 | M16 | 주·홀·관 | P1 | ✅Stitch ✅구현 |
|
||||
| SCR-14 | 홀 현장운영 대시보드 | M14 | 홀 | P2 | ✅Stitch ✅구현 |
|
||||
| SCR-15 | 전시 일정 관리 | M1·M6 | 주·홀 | P1 | ✅Stitch ✅구현 |
|
||||
| SCR-16 | 관리자 대시보드(랜딩) | M18 | 관 | P1 | ✅Stitch ✅구현 |
|
||||
| SCR-17 | 컴포넌트 가이드(스타일가이드) | §1 | 내부 | — | ✅Stitch ✅구현 |
|
||||
| SCR-18 | 홀 배정 신청 & 자동 견적 | M1 | 주 | P1 | 🔲미생성 |
|
||||
| SCR-19 | 부스 판매 인벤토리 | M1·M2 | 주 | P1 | 🔲미생성 |
|
||||
| SCR-20 | 온라인 부스 셀프 선택·판매 | M2 | 참 | P1 | 🔲미생성 |
|
||||
| SCR-21 | 정산·결제 대시보드 | M9 | 주·참·관 | P1 | 🔲미생성 |
|
||||
| SCR-22 | 서류·마일스톤 워크플로 | M6 | 주 | P1 | 🔲미생성 |
|
||||
| SCR-23 | 신고서류 작성(웹폼→HWP/PDF) | M6 | 주 | P1 | 🔲미생성 |
|
||||
| SCR-24 | 등록업체 매칭·RFQ | M7 | 참·주 | P1 | 🔲미생성 |
|
||||
| SCR-25 | 반입/반출 슬롯 예약 | M8 | 참·장·홀 | P2 | 🔲미생성 |
|
||||
| SCR-26 | 옥션 목록·개설 | M15 | 참·주 | P1 | 🔲미생성 |
|
||||
| SCR-27 | 옥션 상세·실시간 순위·응찰 | M15 | 장 | P1 | 🔲미생성 |
|
||||
| SCR-28 | 견적서 작성·제출 | M15 | 장 | P1 | 🔲미생성 |
|
||||
| SCR-29 | 낙찰 비교·선정(Award) | M15 | 주·참 | P1 | 🔲미생성 |
|
||||
| SCR-30 | 관람객 등록 관리 대시보드 | M10·M11 | 주 | P1 | 🔲미생성 |
|
||||
| SCR-31 | 현장 체크인 데스크/키오스크 | M10 | 홀 | P1 | 🔲미생성 |
|
||||
| SCR-32 | 리드 관리·AI 스코어링 | M10 | 참 | P1 | 🔲미생성 |
|
||||
| SCR-33 | EDM·캠페인 관리 | M12 | 주 | P1 | 🔲미생성 |
|
||||
| SCR-34 | 스폰서십 패키지·판매 관리 | M12 | 주 | P1 | 🔲미생성 |
|
||||
| SCR-35 | CMS 콘텐츠·게시 워크플로 | M17 | 주·관 | P1 | 🔲미생성 |
|
||||
| SCR-36 | 참가업체 마이크로사이트 편집기 | M17 | 참 | P1 | 🔲미생성 |
|
||||
| SCR-37 | 다국어 콘텐츠 관리 | M17 | 관 | P1 | 🔲미생성 |
|
||||
| SCR-38 | 업체 수주 부스 대시보드 | 업체포털 | 장 | P1 | 🔲미생성 |
|
||||
| SCR-39 | 업무일지(worklog) | §5B | 전 | P1 | 🔲미생성 |
|
||||
| SCR-40 | 통합 일정·캘린더(schedule) | §5B | 전 | P1 | 🔲미생성 |
|
||||
| SCR-41 | 쪽지(message) | §5B | 전 | P1 | 🔲미생성 |
|
||||
| SCR-42 | 공지(notice) | §5B | 전 | P1 | 🔲미생성 |
|
||||
| SCR-43 | 의견접수(opinion) | §5B | 전 | P2 | 🔲미생성 |
|
||||
| SCR-44 | 통합검색(search) | §5B | 전 | P1 | 🔲미생성 |
|
||||
| SCR-45 | 회의록(meeting·STT→Jasper) | §5B | 주·홀 | P1 | 🔲미생성 |
|
||||
| SCR-46 | 업무보고·통계(report/stats) | §5B | 주·홀 | P1 | 🔲미생성 |
|
||||
| SCR-47 | 알림센터(notification) | §5B | 전 | P1 | 🔲미생성 |
|
||||
| SCR-48 | 마이페이지·환경설정 | §5B | 전 | P1 | 🔲미생성 |
|
||||
| SCR-49 | 회원가입(웹) | §5B-3 | 전 | P1 | 🔲미생성 |
|
||||
| SCR-50 | 비밀번호 찾기·재설정 | §5B-3 | 전 | P1 | 🔲미생성 |
|
||||
| SCR-51 | 2차 인증(OTP) 설정 | §5B-3 | 전 | P1 | 🔲미생성 |
|
||||
|
||||
**웹 — 관리자 백오피스 (§3A · SCR-A*)**
|
||||
|
||||
| SCR | 화면명 | 모듈 | 역할 | 상태 |
|
||||
|---|---|---|---|---|
|
||||
| SCR-A1 | 사용자 관리 | M18·§5B-1 | 관 | 🔲미생성 |
|
||||
| SCR-A2 | 역할·권한(RBAC) 관리 | M18·§5B-1 | 관 | 🔲미생성 |
|
||||
| SCR-A3 | 공통코드 관리 | M18·§5B-1 | 관 | 🔲미생성 |
|
||||
| SCR-A4 | 메뉴 관리 | M18·§5B-1 | 관 | 🔲미생성 |
|
||||
| SCR-A5 | 감사로그 | M18·§5B-1 | 관 | 🔲미생성 |
|
||||
| SCR-A6 | 시스템설정 | M18·§5B-1 | 관 | 🔲미생성 |
|
||||
| SCR-A7 | 마스터데이터 관리(홀·요율·요금·부스표준·등록업체) | M18 | 관 | 🔲미생성 |
|
||||
| SCR-A8 | 규정 룰셋 버전 관리 | M18 | 관 | 🔲미생성 |
|
||||
| SCR-A9 | 테넌트(전시관) 관리·온보딩 | §1A·M18 | 플랫폼관 | 🔲미생성 |
|
||||
| SCR-A10 | AI 플랫폼 설정(AiConfig) | §6·F100 | 관 | 🔲미생성 |
|
||||
|
||||
**웹 — 공개 홍보 사이트 (§3B · SCR-P* · MDI 비적용·SEO/다국어)**
|
||||
|
||||
| SCR | 화면명 | 모듈 | 역할 | 상태 |
|
||||
|---|---|---|---|---|
|
||||
| SCR-P1 | 공개 홍보 사이트 홈 | M12 | 대 | 🔲미생성 |
|
||||
| SCR-P2 | 행사 상세(소개·일정·교통·연사) | M12 | 대 | 🔲미생성 |
|
||||
| SCR-P3 | 공개 인터랙티브 플로어플랜 | M12·M2 | 대 | 🔲미생성 |
|
||||
| SCR-P4 | 관람객 사전등록 폼 | M10 | 대 | 🔲미생성 |
|
||||
| SCR-P5 | 참가업체 마이크로사이트 공개 뷰 | M17 | 대 | 🔲미생성 |
|
||||
| SCR-P6 | 참가/부스 신청 문의 | M12 | 대 | 🔲미생성 |
|
||||
| SCR-P7 | 입장권 예매(티켓팅) | M10·M9 | 대 | 🔲미생성 |
|
||||
| SCR-P8 | 예매 확인·취소 | M10·M9 | 대 | 🔲미생성 |
|
||||
|
||||
**모바일 (§4 · SCR-M*)**
|
||||
|
||||
| SCR | 화면명 | 모듈 | 역할 | 상태 |
|
||||
|---|---|---|---|---|
|
||||
| SCR-M1 | 시공업체 현장 체크리스트 | M3 | 장 | ✅Stitch ✅구현 |
|
||||
| SCR-M2 | 홀매니저 현장 검수 | M2·M3 | 홀 | ✅Stitch ✅구현 |
|
||||
| SCR-M3 | 모바일 홈·대시보드(역할별) | 공통 | 전 | ✅구현(부분) |
|
||||
| SCR-M4 | 참가업체 리드캡처(배지 스캔) | M10 | 참 | 🔲미생성 |
|
||||
| SCR-M5 | 관람객 앱 홈·모바일 배지/QR | M10 | 대 | 🔲미생성 |
|
||||
| SCR-M6 | 관람객 wayfinding·실내 지도 | M13 | 대 | 🔲미생성 |
|
||||
| SCR-M7 | 관람객 플로어플랜·부스 검색 | M12·M2 | 대 | 🔲미생성 |
|
||||
| SCR-M8 | 비즈매칭·미팅 | M11 | 대·참 | 🔲미생성 |
|
||||
| SCR-M9 | 세션·아젠다·개인 일정 | M11 | 대 | 🔲미생성 |
|
||||
| SCR-M10 | 업체 반입 통행증·안전 체크 | M8 | 장 | 🔲미생성 |
|
||||
| SCR-M11 | 모바일 알림센터 | §5B | 전 | 🔲미생성 |
|
||||
| SCR-M12 | 모바일 서류·승인 조회 | M6 | 주·홀 | 🔲미생성 |
|
||||
| SCR-M13 | 모바일 옥션 순위 조회 | M15 | 장·참 | 🔲미생성 |
|
||||
| SCR-M14 | 모바일 티켓 예매 | M10·M9 | 대 | 🔲미생성 |
|
||||
| SCR-M15 | 내 티켓 지갑 | M10·M9 | 대 | 🔲미생성 |
|
||||
|
||||
**집계**: 웹 코어·도메인 51 (기존 17 + 신규 34) · 관리자 10 (신규) · 공개사이트 8 (기존 6 + 신규 2 티켓) · 모바일 15 (기존 3 + 신규 12) = **총 84화면**(기존 유지 20 + 신규 64). 상태: ✅Stitch 18 · ✅구현 20 · 🔲미생성 64.
|
||||
|
||||
---
|
||||
|
||||
## 3. 화면별 스펙
|
||||
|
||||
> P0 모듈(M2·M3·M4·M5) 중심 총 14개 화면(웹 12 + 모바일 2) + **v2.0 확장 SCR-13~17(경영분석 M16·홀운영 M14·전시일정 M1·M6·관리자 M18·컴포넌트 가이드)**. 각 Stitch 프롬프트는 화면 1개를 1회 생성하는 기준으로 작성했다.
|
||||
> P0 모듈(M2·M3·M4·M5) 중심 총 14개 화면(웹 12 + 모바일 2) + **v2.0 확장 SCR-13~17(경영분석 M16·홀운영 M14·전시일정 M1·M6·관리자 M18·컴포넌트 가이드)** + **v2.0 도메인 확장 SCR-18~51(M1·M6·M7·M8·M9·M15·M10·M12·M17·§5B 공통레이어)** + 관리자 §3A·공개사이트 §3B·모바일 §4. 각 Stitch 프롬프트는 화면 1개를 1회 생성하는 기준으로 작성했다. **디자인 토큰은 전부 §1을 따르고 신규 토큰은 추가하지 않는다.**
|
||||
|
||||
---
|
||||
|
||||
@ -607,6 +750,656 @@ A design-system style guide / component reference page for "KINTEX AI 전시관
|
||||
|
||||
---
|
||||
|
||||
### SCR-18 홀 배정 신청 & 자동 견적 [M1]
|
||||
|
||||
- **대상**: 주최자 · **모듈/P**: M1 · P1 · **진입 경로**: 콘솔 사이드바 "홀 배정" / 전시 일정(SCR-15)에서 "이 기간 배정 신청"
|
||||
- **레이아웃 (위→아래)**:
|
||||
1. 좌측 2/3 — **가용성 캘린더**: 홀×날짜 그리드(홀1~10 행, 월 컬럼), 셀 상태색(예약=파랑·가배정=주황·공실=회색). 홀/반홀(1A·1B) 토글, 기간 드래그 선택
|
||||
2. 우측 1/3 — **배정 신청 폼**: 홀·반홀 선택, 기간(장치·본행사·철거 구분), 목표 부스 수, 용도. 입력 즉시 하단 **자동 견적 카드** — `2,250원/㎡ × 면적 × 일수 × 성수기(+10%)/1전시장(+10%)` 라인별 산식 노출 + 관리비 예치금(15~20%) + 총액. AiLabel 없음(규칙 엔진). "가견적 PDF" · "배정 신청서(HWP) 생성" 버튼
|
||||
3. 하단 고지: "최종 배정 확정은 킨텍스 내부 심사 — 본 견적은 공시요율 가견적"
|
||||
- **상태/컴포넌트**: 캘린더 그리드·견적 라인 테이블(tabular numerals)·StatusBadge(신청/심사중/확정). 로딩 — 캘린더 스켈레톤 · 빈 — "선택한 기간에 배정 신청서를 작성하세요" · 에러 — 요율 마스터 로드 실패 배너. **데이터 원천**: 요율 마스터(확보)·행사일정(부분 실/샘플), 가용성은 킨텍스 내부 데이터 필요 → 부분 샘플.
|
||||
- **Stitch 프롬프트**:
|
||||
|
||||
```
|
||||
A hall booking and instant-quote screen for an exhibition organizer, Korean B2B web app "KINTEX AI 전시관리", desktop 1440px, light theme, primary blue #0066B3, backgrounds #F9FAFB, 1px borders #E4E7EC, Korean sans-serif (Pretendard). All UI text in Korean. Left sidebar 240px ("홀 배정" highlighted). Main split: left 2/3 an availability calendar — a grid with hall rows labeled "홀1, 홀1A, 홀1B, 홀2 … 홀10" and month/day columns, cells color-coded (blue "예약", amber "가배정", light gray "공실"), a selected date range highlighted in light blue #D9EAF7, and a half-hall toggle. Right 1/3 a booking form card titled "배정 신청": selects "홀/반홀", date range fields "장치 · 본행사 · 철거", a number input "목표 부스 수 510", a "용도" select; below it an auto-quote card "자동 견적(가견적)" showing itemized rows with mono numbers — "임대료 2,250원/㎡ × 4,941㎡ × 4일 = ₩44,469,000", "성수기 +10%", "제1전시장 +10%", "관리비 예치금(15%)", divider, bold total "합계 ₩58,700,000", fine print "공시요율 기준 가견적 — 최종 배정 확정은 킨텍스 심사", and two buttons "가견적 PDF" (outlined) and primary "배정 신청서(HWP) 생성". Data-dense scheduling + commerce quote aesthetic.
|
||||
```
|
||||
|
||||
- **라우트**: `/o/:eventId/hall-booking`. MDI 적용(주최자).
|
||||
|
||||
---
|
||||
|
||||
### SCR-19 부스 판매 인벤토리 [M1·M2 / F005]
|
||||
|
||||
- **대상**: 주최자 · **모듈/P**: M1·M2 · P1 · **진입 경로**: 사이드바 "부스 판매" / 배치 에디터에서 "판매 현황"
|
||||
- **레이아웃**:
|
||||
1. 상단 KPI 밴드: 총 부스 510 · 판매 128 · 홀드 12 · 예약 22 · 잔여 348 · 판매율 25%
|
||||
2. 좌측 2/3 — **판매 현황 뷰 토글**: [맵] 홀 도면 위 부스별 판매상태 히트(판매=녹·홀드=주황·예약=파랑·공실=회색, 클릭 시 우측 상세) / [테이블] 부스번호·유형·면적·가격·상태 배지·업체
|
||||
3. 우측 1/3 — **부스 상세/상태 변경 패널**: 선택 부스 정보 + 상태 드롭다운(공실→홀드→예약→판매), 배정 업체 검색, 단가·프리미엄 계수, 메모
|
||||
4. 상단 우측: 일괄 상태 변경·CSV 내보내기
|
||||
- **상태/컴포넌트**: 판매 히트 맵(도면 오버레이)·데이터 테이블(지브라·sticky)·StatusBadge. 로딩 — 스켈레톤 · 빈 — "확정 배치가 없습니다 — 배치 에디터에서 부스를 확정하세요" · 에러 재시도. **데이터 원천**: M2 확정 배치(부스 좌표) 실 가능, 판매 상태는 신규 인벤토리 API → 부분 샘플.
|
||||
- **Stitch 프롬프트**:
|
||||
|
||||
```
|
||||
A booth sales inventory management screen for an exhibition organizer, Korean B2B web app "KINTEX AI 전시관리", desktop 1440px, light theme, blue #0066B3, Korean sans-serif. All UI text in Korean. Sidebar 240px ("부스 판매" highlighted). Top KPI band: stat cards "총 부스 510", "판매 128", "홀드 12", "예약 22", "잔여 348", and "판매율 25%" with a thin progress bar. Main split: left 2/3 with a view toggle "[맵] / [테이블]" (맵 active) showing a top-down hall floor plan where each small booth rectangle is filled by sales status color — green "판매", amber "홀드", blue "예약", light gray "공실" — with booth numbers; a small legend. Right 1/3 a detail panel for the selected booth "A-102 · 독립부스 · 6×3m": a status dropdown "공실 → 홀드 → 예약 → 판매" set to "판매", an exhibitor search field showing "(주)한빛로보틱스", price fields "단가 ₩13,500,000 · 프리미엄 계수 1.2", and a memo box; top-right buttons "일괄 상태 변경" and "CSV 내보내기". Dense inventory-management aesthetic with a colorful status map.
|
||||
```
|
||||
|
||||
- **라우트**: `/o/:eventId/inventory`. MDI 적용(주최자).
|
||||
|
||||
---
|
||||
|
||||
### SCR-20 온라인 부스 셀프 선택·판매 [M2 / F006]
|
||||
|
||||
- **대상**: 참가업체 · **모듈/P**: M2 · P1 · **진입 경로**: 참가업체 포털 "부스 신청" / 공개 인터랙티브 플로어플랜(SCR-P3)에서 "부스 선택"
|
||||
- **레이아웃**:
|
||||
1. 좌측 히어로 — **인터랙티브 플로어플랜**: 판매 가능 부스는 초록 테두리+가격 라벨, 매진은 회색 잠금. 호버 툴팁(부스번호·면적·가격·유형), 필터(가격대·유형·존)
|
||||
2. 우측 — **선택 요약·결제 카드**: 선택 부스 리스트(부스번호·유형·면적·가격), 옵션(코너/아일랜드 프리미엄), 합계 + "장바구니 담기/부스 예약", 결제(PG) 진입 버튼
|
||||
3. 상단 진행 스텝: 부스 선택 → 정보 입력 → 결제 → 확정
|
||||
- **상태/컴포넌트**: 인터랙티브 SVG 맵(비시각 대안=부스 목록 테이블 — N1 접근성)·요약 카드·스테퍼. 로딩 — 맵 스켈레톤 · 빈 — "현재 판매 중인 행사가 없습니다" · 동시성 — 방금 타인이 선택한 부스 잠금 토스트. **데이터 원천**: M2 부스+SCR-19 인벤토리 상태 → 부분 샘플.
|
||||
- **Stitch 프롬프트**:
|
||||
|
||||
```
|
||||
An online self-service booth selection and purchase screen for an exhibitor, Korean B2B web app, desktop 1440px, light theme, blue #0066B3, green #0E8A5F for available booths, Korean sans-serif. All UI text in Korean. Top progress stepper "부스 선택 → 정보 입력 → 결제 → 확정" (step 1 active). Left hero (2/3): an interactive top-down exhibition floor plan where available booths are outlined in green with small price labels like "₩13.5M", sold booths are gray with a lock icon; a hover tooltip on one booth "A-102 · 6×3m · 독립 · ₩13,500,000"; a filter bar "가격대 ▾ · 유형 ▾ · 존 ▾". Right panel (1/3): a "선택한 부스" summary card listing "A-102 · 독립 6×3m · ₩13,500,000", a "코너 프리미엄 +10%" option checkbox, a divider, bold total "합계 ₩14,850,000", and buttons "부스 예약" (outlined) and primary "결제하기". Friendly e-commerce-meets-floorplan aesthetic, clear availability affordances.
|
||||
```
|
||||
|
||||
- **라우트**: `/e/:eventId/booth-select`. MDI 적용(참가업체).
|
||||
|
||||
---
|
||||
|
||||
### SCR-21 정산·결제 대시보드 [M9]
|
||||
|
||||
- **대상**: 주최자(납부 스케줄)·참가업체(유틸리티·부대 결제)·관리자(정산) · **모듈/P**: M9 · P1 · **진입 경로**: 사이드바 "정산"
|
||||
- **레이아웃**:
|
||||
1. 상단 KPI: 청구 총액 · 납부 완료 · 미납 · 다음 마감 D-데이
|
||||
2. 좌측 2/3 — **납부 스케줄 타임라인/테이블**: 계약금 20%→중도금 30%+20%→잔금 30%+예치금 15~20% 행(금액·기한·상태 배지·[결제]/[영수증]) + 유틸리티/부대/옥션 수수료 정산 라인. 지브라·sticky
|
||||
3. 우측 1/3 — **결제 카드**: 선택 항목 PG 결제(카드/계좌이체/세금계산서), 예치금 대비 실사용 정산 요약(검침 기반), 세금계산서 발행 버튼
|
||||
4. 하단: 행사 종료 후 정산 내역(전기사용료·폐기물 처리비) 투명화 표
|
||||
- **상태/컴포넌트**: 스케줄 테이블·PG 결제 모달·StatusBadge(미납 error·완료 success)·DdayChip. 로딩 스켈레톤 · 빈 — "청구 내역이 없습니다" · 에러 재시도. **데이터 원천**: 요율·납부 규칙(확보)·PG(연동 전 샘플) → 혼합.
|
||||
- **Stitch 프롬프트**:
|
||||
|
||||
```
|
||||
A settlement and payment dashboard for exhibition billing, Korean B2B web app "KINTEX AI 전시관리", desktop 1440px, light theme, blue #0066B3, success green #0E8A5F, error red #D92D20, Korean sans-serif. All UI text in Korean. Sidebar 240px ("정산" highlighted). Top KPI cards: "청구 총액 ₩64,500,000", "납부 완료 ₩38,700,000", "미납 ₩25,800,000" (red), "다음 마감 D-9" (amber D-day chip). Main split: left 2/3 a payment schedule table with columns 항목, 금액, 기한, 상태, 액션 — rows "계약금 20% ₩12,900,000 · 2026.06.01 · 완료(green pill) · [영수증]", "중도금 30% ₩19,350,000 · 07.15 · 미납(red pill) · [결제]", "잔금 30% + 예치금 15%", plus utility/booth-service/auction-fee settlement lines, zebra striped, sticky header. Right 1/3 a payment card "결제" with method segmented control "카드 / 계좌이체 / 세금계산서", a deposit-vs-actual settlement summary "예치금 ₩9.6M / 실사용 ₩7.1M / 환급 ₩2.5M", and a button "세금계산서 발행". Bottom: a small post-event settlement table "전기사용료 · 폐기물 처리비". Financial, trustworthy, number-forward layout.
|
||||
```
|
||||
|
||||
- **라우트**: `/o/:eventId/settlement` · 참가 `/e/:eventId/booth/:boothId/payment`. MDI 적용.
|
||||
|
||||
---
|
||||
|
||||
### SCR-22 서류·마일스톤 워크플로 [M6]
|
||||
|
||||
- **대상**: 주최자 · **모듈/P**: M6 · P1 · **진입 경로**: 대시보드 마일스톤 노드 클릭 / 사이드바 "서류·마일스톤"
|
||||
- **레이아웃**:
|
||||
1. 상단 — **마일스톤 진행 바**: D-150/D-30/D-25/D-7/D-0 (SCR-02 히어로 재사용, 컴팩트)
|
||||
2. 좌측 — **신고서류 체크리스트(7종+)**: 행사운영계획서·부스배치도·재해대처계획서·방화관리 책임서약서·주차관리 신청서·보안요원 배치계획·위험물 반입신고서·리깅 구조계산서 — 각 행 상태 배지(작성전/작성중/제출됨/검수중/승인/반려)·마감 D-데이·[작성]/[업로드]/[HWP 생성]
|
||||
3. 우측 — **AI 서류 검수 요약 카드**: 누락 항목·불일치("배치도 부스 수 486 ≠ 운영계획서 510") AiLabel + kxwp 릴레이 안내(제출 파일 생성→수동 업로드)
|
||||
- **상태/컴포넌트**: 체크리스트·StatusBadge·DdayChip·AiLabel. 로딩 스켈레톤 · 빈 — "행사를 생성하면 마일스톤·서류가 자동 구성됩니다" · 에러 재시도. **데이터 원천**: 서식 템플릿(있음)·AI 검수(Claude 라우터) → 부분 실.
|
||||
- **Stitch 프롬프트**:
|
||||
|
||||
```
|
||||
A documents and milestone workflow screen for an exhibition organizer, Korean B2B web app, desktop 1440px, light theme, blue #0066B3, purple #6D4AFF for AI, Korean sans-serif. All UI text in Korean. Sidebar 240px ("서류·마일스톤" highlighted). Top: a compact horizontal milestone bar "D-150 배정 → D-30 사전협의 → D-25 유틸리티 → D-7 신고서류 → D-0 개장" with the D-7 node highlighted amber. Main split: left 2/3 a required-documents checklist with rows, each showing a document name, a status pill and a D-day chip and action buttons — "행사운영계획서 · 승인(green) · [보기]", "부스배치도 · 제출됨(blue) · [HWP 생성]", "재해대처계획서 · 작성중(gray) · D-7 · [작성]", "리깅 구조계산서 · 반려(red) · [수정]", plus 방화관리 책임서약서, 주차관리 신청서, 보안요원 배치계획, 위험물 반입신고서. Right 1/3 an "AI 서류 검수" card with a purple accent left border listing issues: "부스배치도 부스 수 486 ≠ 운영계획서 510 (불일치)", "재해대처계획서 필수요소 누락 2건", and a note "kxwp 제출: 파일 자동 생성 후 업로드 안내". Workflow-tracker aesthetic, clear status affordances.
|
||||
```
|
||||
|
||||
- **라우트**: `/o/:eventId/documents`. MDI 적용(주최자).
|
||||
|
||||
---
|
||||
|
||||
### SCR-23 신고서류 작성 (웹폼 → HWP/PDF) [M6 / F022]
|
||||
|
||||
- **대상**: 주최자 · **모듈/P**: M6 · P1 · **진입 경로**: SCR-22에서 "[작성]"
|
||||
- **레이아웃**:
|
||||
1. 좌측 — **웹폼(섹션 아코디언)**: 서류 유형별 필드(행사 개요·안전 담당·비상 연락·위험물 목록 등), AI 자동 채움 버튼("행사 데이터로 자동 채움" AiLabel), 필수 필드 검증
|
||||
2. 우측 — **실시간 문서 미리보기**(A4 프레임, 킨텍스 제출 서식 레이아웃) — 폼 입력이 즉시 반영
|
||||
3. 하단 고정: 저장(임시)·"HWP 생성"·"PDF 생성"·"제출(kxwp 릴레이 안내)"
|
||||
- **상태/컴포넌트**: 폼·문서 미리보기·AiLabel. 로딩 — 미리보기 스켈레톤 · 빈 — 신규 폼 · 에러 — 필수 미입력 인라인 · 생성 실패 재시도. **데이터 원천**: 서식 템플릿·AI 자동채움(Claude) → 부분 실.
|
||||
- **Stitch 프롬프트**:
|
||||
|
||||
```
|
||||
A form-to-document authoring screen where an organizer fills a web form and sees a live official document preview, Korean B2B web app, desktop 1440px, light theme, blue #0066B3, purple #6D4AFF for AI. All UI text in Korean. Split layout: left half a form titled "재해대처계획서 작성" with accordion sections "행사 개요", "안전 관리 담당", "비상 연락 체계", "위험물 목록", each with Korean labeled inputs, and a purple button "행사 데이터로 자동 채움". Right half a document preview panel showing an A4 page with an official Korean government-style form layout, header "재해대처계획서", filled fields, a table, and a stamp box — updating live from the form. Sticky bottom bar: "임시 저장", "HWP 생성", "PDF 생성", and a primary "제출". A small note "제출은 kxwp 업로드용 파일 생성 방식입니다". Clean document-authoring split view.
|
||||
```
|
||||
|
||||
- **라우트**: `/o/:eventId/documents/:docType/edit`. MDI 적용.
|
||||
|
||||
---
|
||||
|
||||
### SCR-24 등록업체 매칭·RFQ [M7]
|
||||
|
||||
- **대상**: 참가업체·주최자 · **모듈/P**: M7 · P1 · **진입 경로**: 설계 스튜디오 "업체 찾기" / 사이드바 "등록업체"
|
||||
- **레이아웃**:
|
||||
1. 상단 필터: 공종 14분류(전시디자인설치·리깅·전기시설…), 지역, 규모, 예산
|
||||
2. 좌측 — **업체 카드 리스트**: 업체명·분류·지역·규정 준수 이력·평점·(AI 추천 배지 "부스 규모·업종 적합" AiLabel). 미등록 업체는 노출 안 됨(게이트)
|
||||
3. 우측 — **RFQ 작성 패널**: 선택 업체 다중, M3 설계안·물량서 첨부, 요청 항목·마감, "견적 요청 발송". 발송 후 견적 회신 비교 진입(옥션 SCR-26으로 승격 가능)
|
||||
- **상태/컴포넌트**: 업체 카드·AiLabel·첨부. 로딩 스켈레톤 · 빈 — "조건에 맞는 등록업체가 없습니다 — 필터를 조정하세요" · 에러 재시도. **데이터 원천**: 등록업체 DB(739, 수집 가능)·AI 매칭(Claude) → 부분 실.
|
||||
- **Stitch 프롬프트**:
|
||||
|
||||
```
|
||||
A registered-contractor matching and RFQ screen for exhibition booth construction, Korean B2B web app, desktop 1440px, light theme, blue #0066B3, purple #6D4AFF for AI recommendations. All UI text in Korean. Top filter bar: category chips "전시디자인설치 · 리깅 · 전기시설 · 카펫 · 급배수" (14 categories), and selects "지역 ▾", "규모 ▾", "예산 ▾". Left 2/3 a list of contractor cards, each with company name "(주)공간디자인", category tag, region "경기 고양", a compliance record "규정 준수 98% · 시공 42건", a star rating, and some cards a purple "AI 추천 · 부스 규모·업종 적합" chip; a small verified badge "킨텍스 등록업체". Right 1/3 an "견적 요청(RFQ)" panel: selected contractors chips, an attachment list "부스 설계안 v2, 물량서(BOQ)", request-item fields, a deadline date, and a primary button "견적 요청 발송" with a note "미등록 업체는 응찰할 수 없습니다". Directory + procurement aesthetic.
|
||||
```
|
||||
|
||||
- **라우트**: `/e/:eventId/contractors` · 주최자 `/o/:eventId/contractors`. MDI 적용.
|
||||
|
||||
---
|
||||
|
||||
### SCR-25 반입/반출 슬롯 예약 [M8]
|
||||
|
||||
- **대상**: 참가업체·장치업체·홀매니저(운영) · **모듈/P**: M8 · P2 · **진입 경로**: 통합 일정 / 사이드바 "반입·반출"
|
||||
- **레이아웃**:
|
||||
1. 좌측 2/3 — **하역장·화물출입구 슬롯 그리드**: 게이트(도크1~6) 행 × 시간대 컬럼, 예약 셀(업체·차량·중량물 우선 표시), 드래그로 슬롯 선택. 중량물(5t↑) 우선 배치 하이라이트
|
||||
2. 우측 1/3 — **예약 폼**: 차량정보·중량·품목, 지게차 신청(유료 옵션), 통행증 발급(QR) → 모바일(SCR-M10) 연동. 철거일 피크 대기열 예측 미니 차트
|
||||
3. 상단: 행사·홀 필터, 반입/반출 전환
|
||||
- **상태/컴포넌트**: 슬롯 그리드·예약 폼·QR 발급·예측 미니 차트. 로딩 스켈레톤 · 빈 — "예약 가능한 슬롯이 없습니다" · 충돌 — 동시 예약 잠금 토스트. **데이터 원천**: 하역장 배치(운영팀 협업 필요) → 샘플 중심.
|
||||
- **Stitch 프롬프트**:
|
||||
|
||||
```
|
||||
A move-in/move-out dock slot reservation screen for an exhibition, Korean B2B web app, desktop 1440px, light theme, blue #0066B3, Korean sans-serif. All UI text in Korean. Top: filters "행사 ▾ · 홀7 ▾" and a toggle "[반입] / [반출]". Left 2/3 a scheduling grid with dock rows "도크1 … 도크6" and time-slot columns (08:00–20:00), reserved cells filled blue showing "(주)공간디자인 · 5t 트럭", one cell highlighted amber "중량물 우선 5t↑", empty cells selectable. Right 1/3 a reservation form "슬롯 예약": vehicle info fields "차량번호 · 중량 · 품목", a checkbox "지게차 신청(유료)", and a "통행증 발급(QR)" button showing a small QR preview, plus a small teardown-day queue prediction mini bar chart "철거일 대기열 예측". Logistics-scheduling aesthetic with a clear time grid.
|
||||
```
|
||||
|
||||
- **라우트**: `/e/:eventId/logistics` · 운영 `/ops/:eventId/logistics`. MDI 적용.
|
||||
|
||||
---
|
||||
|
||||
### SCR-26 공사/장치 옥션 목록·개설 [M15]
|
||||
|
||||
- **대상**: 참가업체·주최자(발주) · **모듈/P**: M15 · P1(핵심) · **진입 경로**: 사이드바 "옥션" / RFQ(SCR-24)에서 "역경매로 전환"
|
||||
- **레이아웃**:
|
||||
1. 상단: 탭 [진행중] [마감] [내가 개설] + "옥션 개설" 버튼
|
||||
2. 좌측 — **옥션 카드/테이블**: 옥션명·공종·유형(역경매/RFQ)·응찰 수·현재 최저가·마감 D-데이·상태 배지. AI 자료 첨부 아이콘(배치·설계·물량·이미지)
|
||||
3. **옥션 개설 모달/우측 패널**: 공종 선택, 낙찰 기준(최저가/종합평가+가중치 슬라이더: 가격·평판·납기), 라운드·마감, AI 자료 첨부(M2~M5), 초대 업체(등록업체 게이트)
|
||||
- **상태/컴포넌트**: 옥션 카드·StatusBadge·DdayChip·가중치 슬라이더. 로딩 스켈레톤 · 빈 — "진행 중인 옥션이 없습니다 — 옥션을 개설하세요" · 에러 재시도. **데이터 원천**: M15 신규 엔진(미구현) → 샘플.
|
||||
- **Stitch 프롬프트**:
|
||||
|
||||
```
|
||||
A construction auction (reverse auction) list and creation screen for an exhibition platform, Korean B2B web app, desktop 1440px, light theme, blue #0066B3, purple #6D4AFF for AI attachments, Korean sans-serif. All UI text in Korean. Top: tabs "진행중 | 마감 | 내가 개설" and a primary button "옥션 개설". Left 2/3 a list of auction cards, each with auction title "A-102 독립부스 시공", a category tag "전시디자인설치", a type chip "역경매", "응찰 5개", "현재 최저가 ₩8,400,000", a D-day chip "마감 D-2", a status pill "진행중", and small purple attachment icons "배치 · 설계 · 물량서 · 예상이미지". Right 1/3 an "옥션 개설" panel: category select, an award-criteria toggle "최저가 / 종합평가", weight sliders "가격 60% · 평판 25% · 납기 15%", round/deadline fields, an AI attachment picker "M2~M5 자료 첨부", and an invited-contractor list with a note "등록업체만 초대 가능". Procurement/marketplace aesthetic.
|
||||
```
|
||||
|
||||
- **라우트**: `/e/:eventId/auctions` · `/o/:eventId/auctions` · 개설 `/auctions/new`. MDI 적용.
|
||||
|
||||
---
|
||||
|
||||
### SCR-27 옥션 상세·실시간 순위·응찰 [M15]
|
||||
|
||||
- **대상**: 장치업체(응찰) · 발주자(주최자/참가) 뷰 병행 · **모듈/P**: M15 · P1 · **진입 경로**: 옥션 목록 → 옥션 클릭
|
||||
- **레이아웃**:
|
||||
1. 상단: 옥션 컨텍스트(공종·유형·라운드·마감 카운트다운) + 실시간 배지
|
||||
2. 좌측 2/3 — **AI 자료 열람**: 탭[배치도][부스 설계안][배선/물량서(BOQ)][나노바나나 예상 이미지] — 응찰 근거 자료(워터마크 상시)
|
||||
3. 우측 1/3 — **실시간 순위·응찰 패널**: 현재 순위 리스트(익명 옵션: "업체 A"), 내 위치 강조, 최저가/내 견적, 라운드 종료 타이머(Redis 실시간·WebSocket), "견적서 작성·재응찰"(SCR-28) CTA. 발주자 뷰에서는 "견적서 비교표 보기"(SCR-29)
|
||||
- **상태/컴포넌트**: 자료 뷰어 탭·실시간 순위 리스트·카운트다운·AiLabel(예상이미지). 로딩 — 순위 스켈레톤 · 마감됨 — 응찰 비활성+"마감된 옥션" · 에러 — WebSocket 재연결 배지. **데이터 원천**: M15 신규 → 샘플.
|
||||
- **Stitch 프롬프트**:
|
||||
|
||||
```
|
||||
An auction detail screen with AI reference materials and a live ranking panel for exhibition construction bidding, Korean B2B web app, desktop 1440px, light theme, blue #0066B3, purple #6D4AFF for AI, Korean sans-serif. All UI text in Korean. Top: context "A-102 독립부스 시공 · 역경매 · 라운드 2 · 마감 01:24:36" with a live red dot badge "실시간". Left 2/3 an AI reference viewer with tabs "배치도 | 부스 설계안 | 물량서(BOQ) | 예상 이미지" showing a photorealistic booth render with a faint watermark "AI 생성 예상 이미지" under the 예상 이미지 tab. Right 1/3 a live ranking panel "실시간 순위": ranked rows "1위 업체 A ₩8,400,000", "2위 나의 응찰 ₩8,700,000" (highlighted), "3위 업체 C ₩8,900,000", an anonymize toggle "익명 순위", a countdown timer "라운드 종료 01:24:36", and a primary button "견적서 작성·재응찰". Competitive, real-time marketplace aesthetic.
|
||||
```
|
||||
|
||||
- **라우트**: `/auctions/:auctionId`. MDI 적용.
|
||||
|
||||
---
|
||||
|
||||
### SCR-28 견적서 작성·제출 [M15]
|
||||
|
||||
- **대상**: 장치업체 · **모듈/P**: M15 · P1 · **진입 경로**: SCR-27 → "견적서 작성·재응찰"
|
||||
- **레이아웃**:
|
||||
1. 좌측 — **견적서 라인 에디터**: 공종·자재·수량·단가·금액 행 추가(물량서 BOQ에서 자동 프리필 AiLabel), 소계·부가세·총액 자동 계산, 납기·유효기간·조건
|
||||
2. 우측 — **견적서 PDF 미리보기**(A4, 업체 로고·총액·서명란) + 버전 이력(라운드 내 재응찰 v1·v2)
|
||||
3. 하단: 임시저장·"견적서 제출(응찰)" — 제출 시 순위 즉시 갱신
|
||||
- **상태/컴포넌트**: 라인 테이블(tabular)·PDF 미리보기·버전 이력·AiLabel(BOQ 프리필). 로딩 미리보기 스켈레톤 · 빈 — 신규 견적서 · 에러 — 필수 미입력 인라인. **데이터 원천**: M15·BOQ(부분) → 샘플.
|
||||
- **Stitch 프롬프트**:
|
||||
|
||||
```
|
||||
A quotation (bid) authoring and submission screen for a contractor in an exhibition reverse auction, Korean B2B web app, desktop 1440px, light theme, blue #0066B3, purple #6D4AFF for AI prefill. All UI text in Korean. Split: left half a line-item quotation editor titled "견적서 작성" with a table (공종, 자재, 수량, 단가, 금액) rows like "골조 · 알루미늄 프로파일 · 48 · ₩45,000 · ₩2,160,000", a purple note "물량서(BOQ)에서 자동 채움", and computed rows "소계 ₩7,636,000", "부가세 ₩763,600", bold "총액 ₩8,399,600", plus fields "납기 · 유효기간 · 조건". Right half a quotation PDF preview (A4) with a contractor logo, itemized table, bold total, and a signature box, plus a version history list "v1 (라운드1) · v2 (현재)". Sticky bottom: "임시 저장" and primary "견적서 제출(응찰)". Precise document + spreadsheet aesthetic.
|
||||
```
|
||||
|
||||
- **라우트**: `/auctions/:auctionId/quote`. MDI 적용.
|
||||
|
||||
---
|
||||
|
||||
### SCR-29 낙찰 비교·선정 (Award) [M15]
|
||||
|
||||
- **대상**: 발주자(주최자·참가업체) · **모듈/P**: M15 · P1 · **진입 경로**: 옥션 마감 → "견적서 비교"
|
||||
- **레이아웃**:
|
||||
1. 상단: 옥션 요약(공종·응찰 수·낙찰 기준)
|
||||
2. **비교 테이블(전폭)**: 열=업체, 행=총액·항목별 단가·납기·평판·종합점수(가중 스코어 산식 노출 AiLabel). 최적값 셀 하이라이트, 종합점수 내림차순
|
||||
3. 우측/하단 — **선정 패널**: 추천 업체(종합점수 1위) + 선정 사유 입력, "낙찰(Award)" → 계약/발주 전환(M6·M9) 링크, 낙찰 통지 미리보기
|
||||
- **상태/컴포넌트**: 비교 테이블(sticky 열·하이라이트)·스코어 게이지·AiLabel. 로딩 스켈레톤 · 빈 — "응찰이 없습니다" · 에러 재시도. **데이터 원천**: M15 → 샘플.
|
||||
- **Stitch 프롬프트**:
|
||||
|
||||
```
|
||||
An award decision / quotation comparison screen for a buyer in an exhibition reverse auction, Korean B2B web app, desktop 1440px, light theme, blue #0066B3, purple #6D4AFF for AI score, green #0E8A5F highlights. All UI text in Korean. Top: auction summary "A-102 독립부스 시공 · 응찰 5 · 낙찰기준 종합평가". A wide comparison table with contractor columns "업체 A · 업체 B · 업체 C" and metric rows "총액 ₩8.4M / ₩8.7M / ₩8.9M", "골조 단가", "전기 단가", "납기 12일 / 10일 / 14일", "평판 4.8 / 4.6 / 4.2", and a "종합점수" row "92.4 / 89.1 / 84.7" with a small purple chip "AI 가중 스코어"; the best cells are highlighted green, columns sorted by score. Right panel "낙찰 선정": a recommended card "업체 A · 종합 1위", a "선정 사유" textarea, a primary green button "낙찰(Award)", and a note "낙찰 견적서는 계약·발주 문서로 전환됩니다". Decision-support, comparison-heavy aesthetic.
|
||||
```
|
||||
|
||||
- **라우트**: `/auctions/:auctionId/award`. MDI 적용.
|
||||
|
||||
---
|
||||
|
||||
### SCR-30 관람객 등록 관리 대시보드 [M10·M11]
|
||||
|
||||
- **대상**: 주최자 · **모듈/P**: M10 · P1 · **진입 경로**: 사이드바 "관람객"
|
||||
- **레이아웃**:
|
||||
1. 상단 KPI: 사전등록 12,480 · 체크인 8,210 · 노쇼율 · 바이어 비중 · 리드 생성 수
|
||||
2. 좌측 2/3 — **등록 추이 차트**(Recharts Area, 일자별 사전등록/체크인) + 등록 유형 분포(관람객/바이어/VIP)
|
||||
3. 우측 1/3 — **등록 폼 관리**: 활성 폼 리스트, AI 폼빌더 진입(F046 AiLabel), 배지 템플릿(M17 연계)
|
||||
4. 하단 — **등록자 테이블**: 이름(마스킹)·유형·소속·등록일·체크인 상태·배지 발급 — 개인정보 마스킹·감사 표기
|
||||
- **상태/컴포넌트**: KPI·Recharts·등록자 테이블(PII 마스킹)·AiLabel. 로딩 스켈레톤 · 빈 — "사전등록이 아직 없습니다" · 에러 재시도. **데이터 원천**: M10 신규 → 샘플. **PII 준수**(N2).
|
||||
- **Stitch 프롬프트**:
|
||||
|
||||
```
|
||||
A visitor registration management dashboard for an exhibition organizer, Korean B2B web app, desktop 1440px, light theme, blue #0066B3, purple #6D4AFF for AI, Korean sans-serif. All UI text in Korean. Sidebar 240px ("관람객" highlighted). Top KPI cards: "사전등록 12,480", "체크인 8,210", "노쇼율 12%", "바이어 비중 34%", "리드 생성 2,140". Main: left 2/3 a chart card "등록·체크인 추이" with a multi-series area chart (blue = 사전등록, slate = 체크인) over dates, and a small donut "등록 유형 (관람객 62% · 바이어 34% · VIP 4%)". Right 1/3 a "등록 폼 관리" card with a form list and a purple button "AI 폼빌더", plus a "배지 템플릿" thumbnail. Bottom: a registrants data table with columns 이름, 유형, 소속, 등록일, 체크인, 배지 — names partially masked like "김**", status pills, zebra striped. Data-rich CRM-style aesthetic with privacy masking.
|
||||
```
|
||||
|
||||
- **라우트**: `/o/:eventId/visitors`. MDI 적용.
|
||||
|
||||
---
|
||||
|
||||
### SCR-31 현장 체크인 데스크/키오스크 [M10]
|
||||
|
||||
- **대상**: 홀매니저·현장 운영 · **모듈/P**: M10 · P1 · **진입 경로**: 운영 대시보드 "체크인" / 키오스크 모드
|
||||
- **레이아웃**:
|
||||
1. 중앙 — **QR 스캔 존**(대형 카메라 프레임) + 수동 검색(이름·연락처·등록번호)
|
||||
2. 스캔 성공 시 — **관람객 카드**: 이름·유형·소속·사진, "즉석 배지 인쇄" 버튼, 재발급
|
||||
3. 우측 — **실시간 입장 집계**: 시간대별 체크인 수, 현재 장내 추정 인원, 오프라인 모드 배지("네트워크 불안정 — 로컬 저장 후 동기화")
|
||||
4. 키오스크 모드 토글(셀프 체크인 대형 UI)
|
||||
- **상태/컴포넌트**: 스캔 프레임·관람객 카드·집계 위젯·오프라인 폴백 배지. 로딩 — 스캔 대기 애니메이션 · 실패 — "등록 정보를 찾을 수 없습니다 — 현장 등록" · 에러 재시도. **데이터 원천**: M10 신규 → 샘플. **오프라인 폴백 필수**(F049).
|
||||
- **Stitch 프롬프트**:
|
||||
|
||||
```
|
||||
An on-site check-in desk / kiosk screen for exhibition staff, Korean B2B tool, desktop 1440px (kiosk-friendly), light theme, blue #0066B3, success green #0E8A5F. All UI text in Korean. Center: a large QR scan area with a camera viewfinder frame and text "배지 QR을 스캔하세요", and below it a manual search "이름 · 연락처 · 등록번호로 검색". On a successful scan, a visitor card appears: photo, name "정관람", type chip "바이어", company "(주)미래기술", and buttons "즉석 배지 인쇄" (primary) and "재발급". Right column: a real-time entry widget "실시간 입장" with a small bar chart by hour, a big number "현재 장내 8,210명", and an offline status badge "오프라인 — 로컬 저장 후 동기화". A top-right toggle "키오스크 모드". Fast, high-contrast, glanceable check-in aesthetic.
|
||||
```
|
||||
|
||||
- **라우트**: `/ops/:eventId/checkin`. MDI 적용(운영).
|
||||
|
||||
---
|
||||
|
||||
### SCR-32 리드 관리·AI 스코어링 [M10]
|
||||
|
||||
- **대상**: 참가업체 · **모듈/P**: M10 · P1 · **진입 경로**: 참가업체 포털 "리드" (모바일 리드캡처 SCR-M4의 웹 관리)
|
||||
- **레이아웃**:
|
||||
1. 상단 KPI: 총 리드 · 핫리드(AI 스코어 상위) · 팔로업 대기 · 전환 추정
|
||||
2. 좌측 2/3 — **리드 테이블**: 이름(마스킹)·소속·관심 제품·관심도(별점)·**AI 스코어(0~100 게이지, AiLabel)**·메모·수집 시각. 필터(스코어·태그)
|
||||
3. 우측 1/3 — **리드 상세·팔로업**: 선택 리드 프로필, AI 추천 팔로업 문구(EDM 초안 AiLabel), "EDM 발송"(M12 연계)·CSV 내보내기
|
||||
- **상태/컴포넌트**: 리드 테이블(PII 마스킹)·AI 스코어 게이지·AiLabel·EDM 초안. 로딩 스켈레톤 · 빈 — "수집된 리드가 없습니다 — 모바일 앱에서 배지를 스캔하세요" · 에러 재시도. **데이터 원천**: M10 신규 → 샘플. **PII 동의·보존**(R10).
|
||||
- **Stitch 프롬프트**:
|
||||
|
||||
```
|
||||
A lead management and AI scoring screen for an exhibitor, Korean B2B web app, desktop 1440px, light theme, blue #0066B3, purple #6D4AFF for AI score. All UI text in Korean. Sidebar 240px ("리드" highlighted). Top KPI cards: "총 리드 342", "핫리드 68", "팔로업 대기 120", "전환 추정 24". Main: left 2/3 a leads data table with columns 이름, 소속, 관심 제품, 관심도, AI 스코어, 수집시각 — rows with masked names "김**", companies, interest tags, 5-star interest, and an AI score shown as a small horizontal gauge with number "87" and a purple chip; filter chips "스코어 80+ · 태그". Right 1/3 a lead detail panel: profile, a purple "AI 추천 팔로업" card with a draft message, and buttons "EDM 발송" and "CSV 내보내기". Sales-CRM aesthetic with prominent AI scoring and privacy masking.
|
||||
```
|
||||
|
||||
- **라우트**: `/e/:eventId/booth/:boothId/leads`. MDI 적용.
|
||||
|
||||
---
|
||||
|
||||
### SCR-33 EDM·캠페인 관리 [M12]
|
||||
|
||||
- **대상**: 주최자 · **모듈/P**: M12 · P1 · **진입 경로**: 사이드바 "마케팅"
|
||||
- **레이아웃**:
|
||||
1. 상단: 캠페인 KPI(발송·오픈율·클릭율·등록 전환) + "새 캠페인"
|
||||
2. 좌측 2/3 — **캠페인 리스트**(상태 배지: 초안/예약/발송중/완료) + 성과 미니 지표
|
||||
3. 우측 1/3 — **캠페인 빌더**: 세그먼트(사전등록자·과거 관람객·바이어) 선택, AI 카피 초안(AiLabel)·나노바나나 이미지 삽입, 발송 시점(즉시/예약), 미리보기(수신동의·수신거부 준수 표기)
|
||||
- **상태/컴포넌트**: 캠페인 리스트·StatusBadge·세그먼트 셀렉터·AiLabel. 로딩 스켈레톤 · 빈 — "캠페인이 없습니다" · 에러 재시도. **데이터 원천**: M12·M10 세그먼트 → 샘플. **정보통신망법 수신동의 준수** 표기.
|
||||
- **Stitch 프롬프트**:
|
||||
|
||||
```
|
||||
An EDM / marketing campaign management screen for an exhibition organizer, Korean B2B web app, desktop 1440px, light theme, blue #0066B3, purple #6D4AFF for AI. All UI text in Korean. Sidebar 240px ("마케팅" highlighted). Top: KPI cards "발송 24,000 · 오픈율 38% · 클릭율 9% · 등록전환 1,240" and a primary button "새 캠페인". Left 2/3 a campaign list with rows showing campaign name, a status pill ("초안" gray, "예약" blue, "발송중" amber, "완료" green), audience size, and small open/click metrics. Right 1/3 a "캠페인 빌더" panel: audience segment checkboxes "사전등록자 · 과거 관람객 · 바이어", a purple button "AI 카피 초안", an image slot "나노바나나 예상 이미지 삽입", a send-time toggle "즉시 / 예약", and an email preview with a footer "수신거부 · 정보통신망법 수신동의 준수". Marketing-automation aesthetic.
|
||||
```
|
||||
|
||||
- **라우트**: `/o/:eventId/campaigns`. MDI 적용.
|
||||
|
||||
---
|
||||
|
||||
### SCR-34 스폰서십 패키지·판매 관리 [M12 / F070]
|
||||
|
||||
- **대상**: 주최자 · **모듈/P**: M12 · P1 · **진입 경로**: 사이드바 "스폰서십"
|
||||
- **레이아웃**:
|
||||
1. 상단 KPI: 스폰서 수 · 판매액 · 잔여 패키지 · 이행물 진행률
|
||||
2. 좌측 — **패키지 티어 카드**(다이아몬드/골드/실버: 혜택·가격·잔여 수량·판매 상태)
|
||||
3. 우측 — **스폰서·이행물 관리**: 스폰서 리스트(계약·결제 상태), 이행물 체크리스트(로고 노출·부스·연사 세션), 노출·리드·ROI 대시보드 링크(F071)
|
||||
- **상태/컴포넌트**: 티어 카드·이행물 체크리스트·StatusBadge. 로딩 스켈레톤 · 빈 — "등록된 스폰서십 패키지가 없습니다" · 에러 재시도. **데이터 원천**: 신규 → 샘플.
|
||||
- **Stitch 프롬프트**:
|
||||
|
||||
```
|
||||
A sponsorship package and sales management screen for an exhibition organizer, Korean B2B web app, desktop 1440px, light theme, blue #0066B3, Korean sans-serif. All UI text in Korean. Sidebar 240px ("스폰서십" highlighted). Top KPI cards: "스폰서 18", "판매액 ₩420,000,000", "잔여 패키지 6", "이행물 진행 72%". Left 2/3 sponsorship tier cards in a row — "다이아몬드" (gold accent, benefits list, "₩50,000,000", "잔여 1"), "골드", "실버" — each with a benefits bullet list and a "판매" status. Right 1/3 a "스폰서 관리" panel: a sponsor list with contract/payment status pills, a fulfillment checklist "로고 노출 · 부스 배정 · 연사 세션", and a link "노출·리드·ROI 대시보드". B2B sales/CRM aesthetic with clear tier hierarchy.
|
||||
```
|
||||
|
||||
- **라우트**: `/o/:eventId/sponsorship`. MDI 적용.
|
||||
|
||||
---
|
||||
|
||||
### SCR-35 CMS 콘텐츠·게시 워크플로 [M17]
|
||||
|
||||
- **대상**: 주최자·관리자 · **모듈/P**: M17 · P1 · **진입 경로**: 사이드바 "콘텐츠(CMS)"
|
||||
- **레이아웃**:
|
||||
1. 좌측 — **콘텐츠 트리/목록**: 페이지·공지·행사 소개·연사·FAQ. 상태 배지(초안→검수→승인→게시), 언어 탭(한/영/중/일)
|
||||
2. 중앙 — **콘텐츠 에디터**(블록/리치텍스트, 미디어 라이브러리, 예약 게시), sanitize 표기(XSS N2)
|
||||
3. 우측 — **게시 워크플로 패널**: 버전 이력, 검수 요청·승인, 예약 게시 일정, 사이니지 배포 토글(M13 연계)
|
||||
- **상태/컴포넌트**: 콘텐츠 트리·리치 에디터·StatusBadge·언어 탭·버전 이력. 로딩 스켈레톤 · 빈 — "콘텐츠가 없습니다 — 새 페이지" · 에러 재시도. **데이터 원천**: M17 신규 → 샘플.
|
||||
- **Stitch 프롬프트**:
|
||||
|
||||
```
|
||||
A headless CMS content editor with publishing workflow for an exhibition platform, Korean B2B web app, desktop 1440px, light theme, blue #0066B3, Korean sans-serif. All UI text in Korean. Three-column: left a content tree/list "페이지 · 공지 · 행사 소개 · 연사 · FAQ" with status pills ("초안" gray, "검수" amber, "게시" green) and language tabs "한 · 영 · 중 · 일". Center a block/rich-text editor with a toolbar, a media library strip, a title "행사 소개", body content blocks, and a "예약 게시" datetime field. Right a publishing panel "게시 워크플로": a version history list "v3 (현재) · v2 · v1", a "검수 요청" button, an "승인" button, a schedule "2026.08.01 09:00 예약", and a toggle "사이니지 배포". Clean content-management aesthetic, multilingual affordances.
|
||||
```
|
||||
|
||||
- **라우트**: `/o/:eventId/cms` · 관리자 `/admin/cms`. MDI 적용.
|
||||
|
||||
---
|
||||
|
||||
### SCR-36 참가업체 마이크로사이트 편집기 [M17]
|
||||
|
||||
- **대상**: 참가업체 · **모듈/P**: M17 · P1 · **진입 경로**: 참가업체 포털 "마이크로사이트"
|
||||
- **레이아웃**:
|
||||
1. 좌측 — **섹션 편집 패널**: 부스 소개·제품·나노바나나 예상샷 갤러리·연락처·미팅 예약 링크(M11), 테마 선택
|
||||
2. 중앙 — **라이브 미리보기**(반응형: 데스크톱/모바일 토글) — 공개 뷰(SCR-P5)와 동일 렌더
|
||||
3. 우측 — 게시 상태·URL(서브도메인/슬러그)·SEO 메타·다국어 토글
|
||||
- **상태/컴포넌트**: 섹션 편집·라이브 프리뷰·반응형 토글·AiLabel(예상샷). 로딩 스켈레톤 · 빈 — 템플릿 선택 안내 · 에러 재시도. **데이터 원천**: M17·M5 이미지 → 부분(이미지 실 가능).
|
||||
- **Stitch 프롬프트**:
|
||||
|
||||
```
|
||||
An exhibitor microsite builder screen, Korean B2B web app, desktop 1440px, light theme, blue #0066B3, purple #6D4AFF for AI images. All UI text in Korean. Left panel: section editor list "부스 소개 · 제품 · 예상샷 갤러리 · 연락처 · 미팅 예약" with edit controls and a theme picker. Center: a live preview of a company microsite with a hero banner, company name "(주)한빛로보틱스", a product grid, and an AI-render booth photo gallery with faint watermark "AI 생성 예상 이미지"; a device toggle "데스크톱 / 모바일" above the preview. Right panel: publish status "게시됨", a URL field "hanbit.expo.kintex.kr", SEO meta fields, and language toggle "한 · 영 · 중 · 일". Modern site-builder aesthetic.
|
||||
```
|
||||
|
||||
- **라우트**: `/e/:eventId/microsite`. MDI 적용.
|
||||
|
||||
---
|
||||
|
||||
### SCR-37 다국어 콘텐츠 관리 [M17 / F069]
|
||||
|
||||
- **대상**: 관리자·콘텐츠 담당 · **모듈/P**: M17 · P1 · **진입 경로**: CMS(SCR-35) "다국어" / 관리자 사이드바
|
||||
- **레이아웃**:
|
||||
1. 상단: 언어 커버리지 요약(한 100% · 영 82% · 중 60% · 일 45%), 대상 언어 선택
|
||||
2. **번역 매트릭스 테이블**: 콘텐츠 키 × 언어 셀(원문·번역·상태: 미번역/AI 초벌/검수완료). AI 자동 번역 버튼(AiLabel) + 사람 검수 워크플로
|
||||
3. 우측 — 선택 항목 병렬 편집(원문↔번역), 용어집·톤 가이드
|
||||
- **상태/컴포넌트**: 번역 매트릭스·커버리지 바·AiLabel·병렬 에디터. 로딩 스켈레톤 · 빈 — "번역 대상 콘텐츠가 없습니다" · 에러 재시도. **데이터 원천**: M17·AI 번역(Claude) → 부분.
|
||||
- **Stitch 프롬프트**:
|
||||
|
||||
```
|
||||
A multilingual content management (translation matrix) screen, Korean B2B web app, desktop 1440px, light theme, blue #0066B3, purple #6D4AFF for AI translation. All UI text in Korean. Top: language coverage summary with progress bars "한 100% · 영 82% · 중 60% · 일 45%" and a target-language selector. Center: a translation matrix table with rows = content keys ("행사명", "홀 안내", "참가 신청") and columns = languages (KO source, EN, ZH, JA), each cell showing translated text and a status chip ("미번역" gray, "AI 초벌" purple, "검수완료" green); a purple button "AI 자동 번역". Right panel: a side-by-side editor "원문(한) ↔ 번역(영)" with a glossary and tone guide. Localization-tool aesthetic, dense grid.
|
||||
```
|
||||
|
||||
- **라우트**: `/admin/i18n` · `/o/:eventId/cms/i18n`. MDI 적용.
|
||||
|
||||
---
|
||||
|
||||
### SCR-38 업체 수주 부스 대시보드 [업체 포털]
|
||||
|
||||
- **대상**: 장치·공사업체 · **모듈/P**: 업체 포털(M3·M15·M8 진입 허브) · P1 · **진입 경로**: 업체 포털 로그인 기본 홈
|
||||
- **레이아웃**:
|
||||
1. 상단 KPI: 수주 부스 · 진행 옥션 · 응찰 대기 · 시공 진행 · 마감 임박
|
||||
2. 좌측 2/3 — **수주 부스 리스트**: 행사·부스·발주자·설계 상태·시공 단계 스테퍼(설계→규정검증→반입→시공→검수), [설계 스튜디오]·[규정 리포트]·[반입 예약]
|
||||
3. 우측 1/3 — **옥션/견적 현황**: 참여 옥션·내 순위·마감 D-데이, 알림 피드
|
||||
- **상태/컴포넌트**: KPI·수주 리스트·시공 스테퍼·StatusBadge·DdayChip. 로딩 스켈레톤 · 빈 — "수주한 부스가 없습니다 — 옥션에 참여하세요" CTA · 에러 재시도. **데이터 원천**: M3·M15 → 부분 샘플.
|
||||
- **Stitch 프롬프트**:
|
||||
|
||||
```
|
||||
A contractor's awarded-booths dashboard (home) for exhibition construction, Korean B2B web app, desktop 1440px, light theme, blue #0066B3, Korean sans-serif. All UI text in Korean. Sidebar 240px (업체 포털 menu, "수주 부스" highlighted). Top KPI cards: "수주 부스 8", "진행 옥션 3", "응찰 대기 2", "시공 진행 5", "마감 임박 1". Left 2/3 a list of awarded booth cards, each with event + booth "스마트팩토리 · A-102", client "(주)한빛로보틱스", and a construction stage stepper "설계 → 규정검증 → 반입 → 시공 → 검수" with the current stage highlighted, plus buttons "설계 스튜디오", "규정 리포트", "반입 예약". Right 1/3 an "옥션 현황" panel with participating auctions, my rank "2위", D-day chips, and a notification feed. Operational contractor-portal aesthetic.
|
||||
```
|
||||
|
||||
- **라우트**: `/c/dashboard`. MDI 적용(업체).
|
||||
|
||||
---
|
||||
|
||||
### SCR-39 ~ SCR-48 공통 업무 기능 (§5B UIWS 표준 이식)
|
||||
|
||||
> **UIWS(`workspace/uiws/frontend/src/pages`) 표준을 kintex §1 토큰으로 정합.** 전 포털이 공유하는 공통 업무 레이어(§5B-2). 각 화면은 UIWS 대응 페이지를 레퍼런스로 하되, **레이아웃 언어는 §1(1px 테두리·지브라 테이블·44px 행·StatusBadge)**, MDI 셸(§2.7) 문서 탭으로 열린다. 목록/상세/폼 3형태는 대표 화면 1개 프롬프트로 생성 후 파생.
|
||||
|
||||
#### SCR-39 업무일지 (worklog) — 전 역할 · P1
|
||||
- **레이아웃**: 좌측 필터(기간·작성자·유형)+목록 테이블(날짜·제목·유형·진행상태·시간) / 우측 상세·작성 폼(공수·이슈·댓글). 킨텍스 접합: 홀매니저 현장 일지·업체 시공 일지. 상태: 로딩 스켈레톤·빈("작성된 업무일지가 없습니다")·에러 재시도. 참조: UIWS `worklog/WorklogCalendarPage`·`RelatedDocsPanel`.
|
||||
- **Stitch 프롬프트**:
|
||||
```
|
||||
A daily worklog screen in a Korean B2B enterprise app "KINTEX AI 전시관리", desktop 1440px, light theme, blue #0066B3, 1px borders #E4E7EC, Korean sans-serif (Pretendard). All UI text in Korean. Left sidebar 240px ("업무일지" highlighted). Main split: left 2/3 a filter bar (기간, 작성자, 유형) above a data table with columns 날짜, 제목, 유형, 진행상태, 공수 — zebra striped rows with status pills "진행중 / 완료", 44px row height, sticky header. Right 1/3 a worklog detail/compose panel: date, title, a work-type select, hours input, an issue textarea, and a comment thread with an input. Dense enterprise productivity aesthetic.
|
||||
```
|
||||
|
||||
#### SCR-40 통합 일정·캘린더 (schedule) — 전 역할 · P1
|
||||
- **레이아웃**: 월/주 캘린더 + 우측 일정 리스트/작성. 킨텍스 접합: 행사 마일스톤(M6)·옥션 마감·반입 슬롯(M8) 통합 뷰(색상 범례). SCR-15(운영 전 홀 일정)와 구분 — 본 화면은 개인/부서 업무 일정. 상태: 로딩·빈·에러. 참조: UIWS `schedule/PersonalSchedulePage`·`ScheduleCalendar`.
|
||||
- **Stitch 프롬프트**:
|
||||
```
|
||||
A unified personal/team schedule calendar in a Korean B2B app "KINTEX AI 전시관리", desktop 1440px, light theme, blue #0066B3, Korean sans-serif. All UI text in Korean. Sidebar 240px ("일정" highlighted). Main: a large month calendar grid "2026년 8월" with color-coded events — blue "행사 마일스톤", purple "옥션 마감", green "반입 슬롯", plus a legend and view toggle "월 / 주 / 일". Right 1/3 an agenda list for the selected day with event cards and a "일정 추가" button. Clean calendar/scheduling aesthetic.
|
||||
```
|
||||
|
||||
#### SCR-41 쪽지 (message) — 전 역할 · P1
|
||||
- **레이아웃**: 3-패널(받은/보낸/작성) — 목록·상세·수신자 선택(부서 트리). 킨텍스 접합: 주최자↔참가↔업체↔홀매니저 행사 내 커뮤니케이션. 상태: 빈("받은 쪽지가 없습니다")·읽음 표시. 참조: UIWS `message/*`.
|
||||
- **Stitch 프롬프트**:
|
||||
```
|
||||
An internal messaging (쪽지) screen with a 3-pane layout in a Korean B2B app "KINTEX AI 전시관리", desktop 1440px, light theme, blue #0066B3. All UI text in Korean. Sidebar 240px ("쪽지" highlighted). Layout: a left folder rail "받은 쪽지 · 보낸 쪽지 · 임시보관", a middle message list with sender, subject, time and unread bold rows, and a right reading pane with message body, sender info, and reply box; a "쪽지 쓰기" button opens a recipient picker with a department tree. Familiar mail-client aesthetic.
|
||||
```
|
||||
|
||||
#### SCR-42 공지 (notice) — 전 역할 · P1
|
||||
- **레이아웃**: 공지 목록(중요 상단 고정·팝업 설정) + 상세. 킨텍스 접합: 킨텍스 공지(852건 도메인)·행사 공지 → M17 CMS 채널 정합(경계: 발행성=CMS, 알림성=공지). 상태: 빈·로딩. 참조: UIWS `notice/*`.
|
||||
- **Stitch 프롬프트**:
|
||||
```
|
||||
A notice board (공지사항) screen in a Korean B2B app "KINTEX AI 전시관리", desktop 1440px, light theme, blue #0066B3. All UI text in Korean. Sidebar 240px ("공지" highlighted). Main: a pinned "중요" notice at top with a red badge, then a notice list table (제목, 작성자, 게시일, 조회수) with zebra rows, a search bar, and a "공지 작성" button; clicking opens a detail view with title, meta, body, and attachments. Clean announcement aesthetic.
|
||||
```
|
||||
|
||||
#### SCR-43 의견접수 (opinion) — 전 역할 · P2
|
||||
- **레이아웃**: 의견/건의 접수 폼 + 처리 상태 목록(접수→검토→답변). 킨텍스 접합: 고객의소리·규정 문의. 상태: 빈·로딩. 참조: UIWS `opinion/*`.
|
||||
- **Stitch 프롬프트**:
|
||||
```
|
||||
A feedback/suggestion (의견접수) screen in a Korean B2B app "KINTEX AI 전시관리", desktop 1440px, light theme, blue #0066B3. All UI text in Korean. Sidebar 240px ("의견접수" highlighted). Main: a submission form with a category select "규정 문의 / 개선 제안 / 불편 신고", subject, body, attachment, and a "제출" button; below, a list of my submissions with status pills "접수 / 검토중 / 답변완료" and reply preview. Simple support-form aesthetic.
|
||||
```
|
||||
|
||||
#### SCR-44 통합검색 (search) — 전 역할 · P1
|
||||
- **레이아웃**: 상단 대형 검색창 + 결과 탭(행사·부스·업체·문서·콘텐츠) + 필터. 킨텍스 접합: 크로스 모듈 검색. AI 자연어 검색 옵션(AiLabel). 상태: 빈("검색어를 입력하세요")·결과 없음. 참조: UIWS `SearchResultsPage`·`NlSearchPage`.
|
||||
- **Stitch 프롬프트**:
|
||||
```
|
||||
A unified/global search results screen in a Korean B2B app "KINTEX AI 전시관리", desktop 1440px, light theme, blue #0066B3, purple #6D4AFF for AI. All UI text in Korean. A large centered search bar with a purple "AI 자연어 검색" toggle. Below, result tabs "전체 · 행사 · 부스 · 업체 · 문서 · 콘텐츠" and a left facet filter, with grouped result cards each showing an icon, title, snippet with highlighted query terms, and module tag. Fast, clean search aesthetic.
|
||||
```
|
||||
|
||||
#### SCR-45 회의록 (meeting · STT→Jasper PDF) — 주최자·홀매니저 · P1
|
||||
- **레이아웃**: 회의 목록 + 상세(녹음 재생·STT 전사·요약 AiLabel·액션아이템→일정/서류). 킨텍스 접합: 사전업무협의(D-30) 회의록 자동화. Jasper PDF 출력. 상태: 전사 중 진행 배지·빈·에러. 참조: UIWS `meeting/MeetingDetailPage`.
|
||||
- **Stitch 프롬프트**:
|
||||
```
|
||||
A meeting minutes screen with STT transcription in a Korean B2B app "KINTEX AI 전시관리", desktop 1440px, light theme, blue #0066B3, purple #6D4AFF for AI. All UI text in Korean. Sidebar 240px ("회의록" highlighted). Main: left a meeting list, right a detail view with an audio player, a transcript panel "STT 전사" with speaker labels, a purple "AI 요약" card, an action-items table linking to 일정/서류, and a "회의록 PDF" button. Note "사전업무협의(D-30)". Productivity + AI aesthetic.
|
||||
```
|
||||
|
||||
#### SCR-46 업무보고·통계 (report/stats · Jasper PDF) — 주최자·홀매니저 · P1
|
||||
- **레이아웃**: 기간 탭(일/주/월/분기/연) + 요약 지표 + 차트(Recharts) + 작성자/유형별 표. 경계: 경영지표=M16(SCR-13), 여기는 일상 업무보고·통계. Jasper PDF. 상태: 로딩·빈·에러. 참조: UIWS `report/*`·`stats/*`.
|
||||
- **Stitch 프롬프트**:
|
||||
```
|
||||
A work report / statistics screen with period tabs in a Korean B2B app "KINTEX AI 전시관리", desktop 1440px, light theme, blue #0066B3. All UI text in Korean. Sidebar 240px ("업무보고" highlighted). Top: period tabs "일간 · 주간 · 월간 · 분기 · 연간" and a "보고서 PDF" button. Main: summary KPI cards, a bar/line chart (Recharts style) of work volume over time, and a breakdown table by writer and work type with zebra rows and tabular numbers. Reporting aesthetic distinct from executive BI.
|
||||
```
|
||||
|
||||
#### SCR-47 알림센터 (notification) — 전 역할 · P1
|
||||
- **레이아웃**: 알림 목록(카테고리: 마감 D-데이·낙찰·승인·결제·시스템)·읽음/안읽음·딥링크(문서 탭 열기)·규칙 설정 링크. WebSocket 실시간. 상태: 빈("새 알림이 없습니다")·로딩. 참조: UIWS `NotificationsPage`·`settings/NotificationRulesPage`.
|
||||
- **Stitch 프롬프트**:
|
||||
```
|
||||
A notification center screen in a Korean B2B app "KINTEX AI 전시관리", desktop 1440px, light theme, blue #0066B3, Korean sans-serif. All UI text in Korean. Sidebar 240px ("알림" highlighted). Main: category filter chips "전체 · 마감 · 낙찰 · 승인 · 결제 · 시스템", a chronological notification list with icon, title, description, timestamp, unread blue dot, and per-item deep-link action; a top-right "모두 읽음" and "알림 규칙 설정" link. Clean notification-inbox aesthetic.
|
||||
```
|
||||
|
||||
#### SCR-48 마이페이지·환경설정 (profile/preference) — 전 역할 · P1
|
||||
- **레이아웃**: 좌측 탭(프로필·보안(2FA)·알림 설정·테마/언어·즐겨찾기) + 우측 설정 폼. 킨텍스 접합: 역할별 대시보드 개인화·최근방문. 상태: 저장 토스트·검증. 참조: UIWS `auth/MyProfilePage`.
|
||||
- **Stitch 프롬프트**:
|
||||
```
|
||||
A my-page / settings screen in a Korean B2B app "KINTEX AI 전시관리", desktop 1440px, light theme, blue #0066B3. All UI text in Korean. Sidebar 240px. Layout: a left settings tab rail "프로필 · 보안(2차 인증) · 알림 설정 · 테마·언어 · 즐겨찾기", and a right form panel showing the 프로필 tab: avatar, name, email, department, role badge, and a save button; the 테마·언어 tab shows a light/dark theme toggle and a language select "한 · 영 · 중 · 일". Clean account-settings aesthetic.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### SCR-49 회원가입 (웹) [§5B-3]
|
||||
|
||||
- **대상**: 참가업체·업체·관람객 셀프 · **P1** · **진입 경로**: 로그인(SCR-01) "회원가입" / 초대 링크
|
||||
- **레이아웃**: 중앙 카드(가입 유형 선택: 참가업체/등록업체/관람객), 약관·개인정보 동의, 이메일 인증, 등록업체는 사업자 검증(등록업체 DB 게이트). 스텝(유형→정보→인증→완료). 상태: 검증 인라인·중복 이메일·미등록 업체 안내.
|
||||
- **Stitch 프롬프트**:
|
||||
```
|
||||
A sign-up screen for "KINTEX AI 전시관리", Korean B2B web app, desktop 1440px, light theme, blue #0066B3, on #F9FAFB. All UI text in Korean. Center card with a step indicator "유형 → 정보 → 인증 → 완료". Step 1 shows account-type cards "참가업체 · 등록(장치)업체 · 관람객". The form: name, email with "인증코드 발송" button, password, company/business-number fields (for contractor, with a note "킨텍스 등록업체만 가입 가능"), consent checkboxes "이용약관 · 개인정보 처리방침 동의", and a primary "가입하기" button. Trustworthy onboarding aesthetic.
|
||||
```
|
||||
|
||||
- **라우트**: `/signup`. MDI 비적용(인증 전).
|
||||
|
||||
---
|
||||
|
||||
### SCR-50 비밀번호 찾기·재설정 [§5B-3]
|
||||
|
||||
- **대상**: 전 역할 · **P1** · **진입 경로**: 로그인 "비밀번호 찾기"
|
||||
- **레이아웃**: 이메일 입력→인증코드→새 비밀번호(강도 표시). 로그인 실패 잠금 안내. 상태: 코드 만료·불일치·성공 토스트. 참조: UIWS `auth/FindIdPage`·`ResetPasswordPage`.
|
||||
- **Stitch 프롬프트**:
|
||||
```
|
||||
A password reset screen for "KINTEX AI 전시관리", Korean B2B web app, desktop 1440px, light theme, blue #0066B3, on #F9FAFB. All UI text in Korean. Center card with a 3-step flow "이메일 확인 → 인증코드 → 새 비밀번호". Current step shows an email input and "인증코드 발송", then a 6-digit code field with a resend timer, then a new-password field with a strength meter and confirm field, and a primary "비밀번호 재설정" button. A small note about account lockout after failed attempts. Minimal secure-auth aesthetic.
|
||||
```
|
||||
|
||||
- **라우트**: `/forgot-password`. MDI 비적용.
|
||||
|
||||
---
|
||||
|
||||
### SCR-51 2차 인증(OTP) 설정 [§5B-3]
|
||||
|
||||
- **대상**: 관리자·홀매니저·주최자·업체(필수)·관람객(선택) · **P1** · **진입 경로**: 최초 로그인 강제 / 마이페이지 보안 탭
|
||||
- **레이아웃**: QR(authenticator 등록)·시크릿 키·6자리 코드 검증. 재설정·해제(관리자는 사용자 OTP 초기화). TOTP RFC6238 설명. 상태: 코드 불일치·성공. 참조: UIWS `auth/OtpSetupPage`.
|
||||
- **Stitch 프롬프트**:
|
||||
```
|
||||
A two-factor authentication (OTP) setup screen for "KINTEX AI 전시관리", Korean B2B web app, desktop 1440px, light theme, blue #0066B3, on #F9FAFB. All UI text in Korean. Center card titled "2차 인증(OTP) 등록": step 1 "인증 앱으로 QR을 스캔하세요" with a QR code image and a secret key text with copy button, step 2 "6자리 코드 입력" with a 6-digit OTP input, and a primary "등록 완료" button; a helper "Google Authenticator 등 TOTP 앱 지원". Security-focused, calm aesthetic.
|
||||
```
|
||||
|
||||
- **라우트**: `/otp-setup`. MDI 비적용(로그인 게이트).
|
||||
|
||||
---
|
||||
|
||||
## 3A. 관리자 백오피스 화면 (SCR-A*) — M18 · §5B-1
|
||||
|
||||
> **admin 백오피스(`admin.`, 웹 전용).** SCR-16(랜딩 대시보드)의 하위 M18 본체 화면. UIWS `system/*` 표준을 §1 토큰으로 정합. 플랫폼 슈퍼관리자는 상단 **테넌트 스위처**, 테넌트 관리자는 자기 전시관 고정(§1A-4). 공통 레이아웃: 좌측 admin 사이드바 + 상단 유틸리티(테넌트 컨텍스트·검색) + 목록/폼 2단. 전 화면 **감사로그 기록**(N2). MDI 적용.
|
||||
|
||||
### SCR-A1 사용자 관리
|
||||
- **레이아웃**: 사용자 목록(이름·이메일·역할·소속·상태·2FA·최근접속) + 필터/검색 + 사용자 폼(역할 배정·상태·OTP 초기화). 6역할+등록업체+관람객 계정. PII 마스킹·감사. 상태: 로딩·빈·에러.
|
||||
- **Stitch 프롬프트**:
|
||||
```
|
||||
A user management admin screen for "KINTEX AI 전시관리" back-office, Korean B2B tool, desktop 1440px, light theme, blue #0066B3, 1px borders #E4E7EC, Korean sans-serif. All UI text in Korean. Left admin sidebar 240px ("사용자 관리" highlighted). Top utility bar with a tenant switcher "KINTEX ▾" and a search box. Main: a users data table with columns 이름, 이메일, 역할, 소속, 상태, 2FA, 최근접속 — role badges (주최자/참가/장치/홀매니저/관리자), status pills (활성/잠금/비활성), a "2FA 설정됨" check, zebra rows, sticky header, and a right-side user detail/edit panel with role assignment, status toggle, and an "OTP 초기화" button. Enterprise admin aesthetic with masked emails.
|
||||
```
|
||||
|
||||
### SCR-A2 역할·권한(RBAC) 관리
|
||||
- **레이아웃**: 좌측 역할 트리(플랫폼/테넌트/행사 계층) + 우측 권한 매트릭스(메뉴·기능 × 허용 체크). 플랫폼 vs 테넌트 계층 구분. 상태: 저장·검증.
|
||||
- **Stitch 프롬프트**:
|
||||
```
|
||||
A role & permission (RBAC) management admin screen for "KINTEX AI 전시관리", Korean B2B back-office, desktop 1440px, light theme, blue #0066B3. All UI text in Korean. Left admin sidebar ("역할/권한" highlighted). Layout: a left role tree grouped by tier "플랫폼 · 테넌트 · 행사" with roles like "플랫폼 슈퍼관리자, 테넌트 관리자, 주최자, 참가업체, 장치업체, 홀매니저", and a right permission matrix table with menu/feature rows and allow checkboxes (조회/생성/수정/삭제/승인 columns), a save button. Structured access-control aesthetic.
|
||||
```
|
||||
|
||||
### SCR-A3 공통코드 관리
|
||||
- **레이아웃**: 코드 그룹 목록 + 코드 상세(홀·부스유형·공종 14분류·유틸리티 요금코드). 전역 vs 테넌트 오버라이드 표기(§1A-2). 상태: 로딩·빈.
|
||||
- **Stitch 프롬프트**:
|
||||
```
|
||||
A common-code management admin screen for "KINTEX AI 전시관리", Korean B2B back-office, desktop 1440px, light theme, blue #0066B3. All UI text in Korean. Left admin sidebar ("공통코드" highlighted). Split: left a code-group list "홀 · 부스유형 · 공종(14분류) · 유틸리티 요금코드 · 상태", right a code-detail table (코드, 명칭, 정렬, 사용여부) with add/edit rows and a badge distinguishing "전역" vs "테넌트 재정의". Master-data admin aesthetic.
|
||||
```
|
||||
|
||||
### SCR-A4 메뉴 관리
|
||||
- **레이아웃**: 메뉴 트리(드래그 재정렬) + 메뉴 속성(역할별 포털 IA §2-1 매핑·권한). 상태: 저장.
|
||||
- **Stitch 프롬프트**:
|
||||
```
|
||||
A menu management admin screen for "KINTEX AI 전시관리", Korean B2B back-office, desktop 1440px, light theme, blue #0066B3. All UI text in Korean. Left admin sidebar ("메뉴 관리" highlighted). Layout: a left draggable menu tree (nested items with drag handles and show/hide toggles), a right menu-property form (메뉴명, 경로, 아이콘, 역할별 노출 checkboxes for 주최자/참가/장치/홀매니저/관리자, 정렬). Tree-editor admin aesthetic.
|
||||
```
|
||||
|
||||
### SCR-A5 감사로그
|
||||
- **레이아웃**: 필터(사용자·액션·기간·테넌트) + 로그 테이블(시각·사용자·액션·대상·IP·결과). 승인·낙찰·설계변경·룰셋개정·리드 접근 전수. 내보내기. 상태: 로딩·빈.
|
||||
- **Stitch 프롬프트**:
|
||||
```
|
||||
An audit log admin screen for "KINTEX AI 전시관리", Korean B2B back-office, desktop 1440px, light theme, blue #0066B3, Korean sans-serif. All UI text in Korean. Left admin sidebar ("감사로그" highlighted). Top: filters (사용자, 액션 유형, 기간, 테넌트) and an "CSV 내보내기" button. Main: a dense audit table with columns 시각, 사용자, 액션, 대상, IP, 결과 — rows like "2026-08-01 14:22 · 최매니저 · 낙찰(Award) · 옥션 #A-102 · 성공", monospace timestamps, zebra rows, sticky header. Forensic/compliance aesthetic, high density.
|
||||
```
|
||||
|
||||
### SCR-A6 시스템설정
|
||||
- **레이아웃**: 설정 그룹(요율/규정 룰셋 버전·AI 게이트·마감 정책 D-25/D-7·알림·보안) 폼. 상태: 저장·검증.
|
||||
- **Stitch 프롬프트**:
|
||||
```
|
||||
A system settings admin screen for "KINTEX AI 전시관리", Korean B2B back-office, desktop 1440px, light theme, blue #0066B3. All UI text in Korean. Left admin sidebar ("시스템설정" highlighted). Main: grouped setting cards — "마감 정책" (유틸리티 D-25, 신고서류 D-7 numeric fields), "AI 플랫폼 게이트" (모델 라우터 상태 toggles), "알림" (이메일/WebSocket toggles), "보안" (로그인 실패 잠금 횟수, 세션 만료). Each group has a save button. Clean settings-form aesthetic.
|
||||
```
|
||||
|
||||
### SCR-A7 마스터데이터 관리 (홀·요율·요금·부스표준·등록업체)
|
||||
- **레이아웃**: 좌측 데이터셋 탭(홀 마스터·요율표·유틸리티 요금·부스 표준·등록업체 DB) + 우측 CRUD 테이블/폼. 킨텍스 실측=테넌트#1 시드. 트렌치 좌표·CAD 업로드(미확보 표기). 상태: 로딩·빈·에러.
|
||||
- **Stitch 프롬프트**:
|
||||
```
|
||||
A master-data management admin screen for "KINTEX AI 전시관리", Korean B2B back-office, desktop 1440px, light theme, blue #0066B3. All UI text in Korean. Left admin sidebar ("마스터데이터" highlighted). Split: left dataset tabs "홀 마스터 · 요율표 · 유틸리티 요금 · 부스 표준 · 등록업체 DB", right a CRUD table for the 홀 마스터 tab with columns 홀, 면적(㎡), 규격, 바닥하중, 부스수 — rows "홀7 · 11,290㎡ · 126×90×12m · 5t/㎡ · 510" with edit buttons, plus a "트렌치 좌표 CAD 업로드" area labeled "미확보 · 킨텍스 협의". Structured data-admin aesthetic with tabular numbers.
|
||||
```
|
||||
|
||||
### SCR-A8 규정 룰셋 버전 관리
|
||||
- **레이아웃**: 룰셋 버전 이력(v1.0·v1.1…) + 룰 항목(높이 5m·리깅 6.5~8.5m·복층·방염·소음·이격) 편집 + diff 뷰. 발효일·승인. 상태: 저장·버전 비교.
|
||||
- **Stitch 프롬프트**:
|
||||
```
|
||||
A regulation ruleset version management admin screen for "KINTEX AI 전시관리", Korean B2B back-office, desktop 1440px, light theme, blue #0066B3, Korean sans-serif. All UI text in Korean. Left admin sidebar ("규정 룰셋" highlighted). Split: left a version history list "v1.2 (발효 2026-01-01) · v1.1 · v1.0", right a rules editor listing rule rows "장치물 높이 ≤ 5m", "리깅 6.5–8.5m + 구조계산서 D-7", "복층 ≤ 1/2", "방염 필수", "소음 70–75dB", "이격 벽 30cm·천장 60cm" with editable values and severity (차단/경고), plus a "버전 비교(diff)" button showing changed rules highlighted. Governance/versioning aesthetic.
|
||||
```
|
||||
|
||||
### SCR-A9 테넌트(전시관) 관리·온보딩 [§1A · 플랫폼 슈퍼관리자 전용]
|
||||
- **레이아웃**: 테넌트 목록(KINTEX·COEX…: 코드·도메인·활성·행사수·사용자수) + 온보딩 위저드(코드·서브도메인·브랜딩·마스터데이터·관리자 계정·격리 스모크→active). 크로스-테넌트 접근 감사. 상태: 온보딩 단계·검증.
|
||||
- **Stitch 프롬프트**:
|
||||
```
|
||||
A tenant (venue) management and onboarding admin screen for a multi-tenant exhibition SaaS "KINTEX AI 전시관리", Korean B2B back-office, desktop 1440px, light theme, blue #0066B3. All UI text in Korean. Left admin sidebar ("테넌트 관리" highlighted, super-admin only). Main: a tenants table with columns 전시관, 코드, 도메인, 상태, 행사수, 사용자수 — rows "킨텍스 · KINTEX · kintex.wise.ai.kr · 활성 · 24 · 1,280", "코엑스 · COEX · coex.wise.ai.kr · 온보딩중"; a right "전시관 온보딩" wizard panel with steps "테넌트 생성 → 서브도메인 → 브랜딩 → 마스터데이터 입력 → 관리자 계정 → 격리 스모크 → 활성화", each step with a checkmark or current highlight, and a note "코드 배포 없이 데이터 온보딩". Platform-admin, multi-tenant aesthetic.
|
||||
```
|
||||
|
||||
### SCR-A10 AI 플랫폼 설정 (AiConfig) [§6 · F100]
|
||||
- **레이아웃**: 모델 라우터 설정(텍스트: Claude 기본→Ollama 폴백 / 이미지: 나노바나나 Gemini), 온도·top_k·RAG 토글, 나노바나나 쿼터·비용 상한(테넌트별), 상태 헬스(모델 가용). 시크릿은 env only(미표시). 상태: 저장·헬스체크. 참조: UIWS `system/aiConfig/AiConfigPage`.
|
||||
- **Stitch 프롬프트**:
|
||||
```
|
||||
An AI platform configuration admin screen (AiConfig) for "KINTEX AI 전시관리", Korean B2B back-office, desktop 1440px, light theme, blue #0066B3, purple #6D4AFF for AI. All UI text in Korean. Left admin sidebar ("AI 플랫폼 설정" highlighted). Main: a "텍스트/추론" card with a model router "기본: Claude → 폴백: Ollama" with health dots, sliders "temperature · top_k", and a "근거 검증(RAG)" toggle; an "이미지(시공 예상)" card showing "나노바나나 (Gemini gemini-3.1-flash-image-preview)" with a per-tenant "생성 쿼터" and "비용 상한" field; a note "API 키는 서버 env로만 관리(표시 안 함)". No secret values shown. Config-panel aesthetic with health indicators.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3B. 공개 홍보 사이트 화면 (SCR-P*) — M12 · M17 · MDI 비적용
|
||||
|
||||
> **공개 사이트 `www/expo.`** — SEO·다국어(한/영/중/일)·딥링크·공유 목적. **MDI 비적용, 전통 페이지 내비(헤더 GNB)**, SSR/정적 생성·CDN. 인증 앱과 별도 렌더 경로. 브랜딩은 테넌트 서브도메인 분기(§1A-3). 디자인은 §1 톤을 공개용으로 확장(넓은 히어로·마케팅 밀도), 라이트 테마.
|
||||
|
||||
### SCR-P1 공개 홍보 사이트 홈
|
||||
- **레이아웃**: 헤더 GNB(행사·참가안내·관람안내·교통·언어) + 히어로(나노바나나 전경 이미지·AI 고지·사전등록 CTA) + 진행/예정 행사 카드 그리드 + 킨텍스 소개·교통(GTX-A 킨텍스역) + 푸터. 상태: 로딩·빈.
|
||||
- **Stitch 프롬프트**:
|
||||
```
|
||||
A public marketing website home page for "KINTEX 전시" exhibition center, Korean, desktop 1440px, light theme, primary blue #0066B3, clean modern marketing design (not MDI). All UI text in Korean. Top header GNB "행사 · 참가안내 · 관람안내 · 교통 · 🌐 언어" with a KINTEX logo. Full-width hero with a photorealistic exhibition hall render (faint watermark "AI 생성 예상 이미지"), a large headline "세계로 통하는 전시, 킨텍스", and a primary CTA "관람 사전등록". Below: a grid of event cards (poster, title, dates, hall, "사전등록" button) for "진행중 · 예정" events, a "오시는 길 · GTX-A 킨텍스역 도보 3분" transport section with a map, and a footer. SEO-friendly, spacious, multilingual public site aesthetic.
|
||||
```
|
||||
|
||||
### SCR-P2 행사 상세 (소개·일정·교통·연사)
|
||||
- **레이아웃**: 행사 히어로(배너·기간·홀) + 탭(소개·프로그램/세션·연사·참가업체·교통·FAQ) + 사전등록 고정 CTA. 다국어·공유. 상태: 로딩·빈.
|
||||
- **Stitch 프롬프트**:
|
||||
```
|
||||
A public event detail page for an exhibition, Korean marketing website, desktop 1440px, light theme, blue #0066B3. All UI text in Korean. A hero banner with event title "스마트팩토리 코리아 2026", dates "2026.09.05–09.08", venue "킨텍스 제2전시장 홀7", and a sticky "사전등록" CTA. Below, a tab bar "소개 · 프로그램 · 연사 · 참가업체 · 교통 · FAQ" with the 소개 tab showing rich content sections, a session agenda timeline, and speaker cards with photos. Share buttons and language selector. Attractive public event-page aesthetic.
|
||||
```
|
||||
|
||||
### SCR-P3 공개 인터랙티브 플로어플랜
|
||||
- **레이아웃**: 대형 인터랙티브 홀 맵(부스 클릭→업체 정보 팝오버·마이크로사이트 링크), 검색·필터(업종·존), 범례. 관람객 부스 탐색·부스 셀프선택(SCR-20) 진입. 비시각 대안=부스 목록(N1). 상태: 로딩·빈.
|
||||
- **Stitch 프롬프트**:
|
||||
```
|
||||
A public interactive floor plan page for exhibition visitors, Korean marketing website, desktop 1440px, light theme, blue #0066B3. All UI text in Korean. A large interactive top-down hall map with clickable booth rectangles; clicking a booth shows a popover with company name, category, and a "부스 상세" link. A left search/filter panel "업종 · 존 · 키워드" and a booth results list (accessible non-visual alternative), plus a legend "판매 · 예약 · 공용부". Modern, explorable venue-map aesthetic.
|
||||
```
|
||||
|
||||
### SCR-P4 관람객 사전등록 폼
|
||||
- **레이아웃**: 다단계 폼(유형 관람객/바이어·정보·관심분야·약관/개인정보 동의·완료→모바일 배지 안내). AI 폼(주최자 구성). 상태: 검증·중복·완료. 개인정보 동의 필수.
|
||||
- **Stitch 프롬프트**:
|
||||
```
|
||||
A public visitor pre-registration form page for an exhibition, Korean marketing website, desktop 1440px, light theme, blue #0066B3, on white. All UI text in Korean. A centered multi-step form with a progress indicator "유형 → 정보 → 관심분야 → 완료". Step shows visitor-type cards "관람객 / 바이어 / VIP", then fields 이름, 이메일, 연락처, 소속, 관심 분야 chips, and consent checkboxes "개인정보 수집·이용 동의(필수) · 마케팅 수신 동의(선택)", and a primary "등록하기" button; completion screen shows a QR badge preview and "모바일 배지가 발급되었습니다". Friendly, trustworthy registration aesthetic.
|
||||
```
|
||||
|
||||
### SCR-P5 참가업체 마이크로사이트 공개 뷰
|
||||
- **레이아웃**: 업체 브랜드 히어로 + 제품 + 나노바나나 예상샷 갤러리(워터마크) + 부스 위치(플로어플랜 딥링크) + 미팅 예약 CTA(M11). SEO·다국어. 상태: 로딩·빈.
|
||||
- **Stitch 프롬프트**:
|
||||
```
|
||||
A public exhibitor microsite page, Korean marketing website, desktop 1440px, light theme, blue #0066B3. All UI text in Korean. A branded hero with company name "(주)한빛로보틱스" and tagline, a product showcase grid, an AI-render booth photo gallery with faint watermark "AI 생성 예상 이미지", a "부스 위치 · A-102 홀7" card linking to the floor plan, and a "미팅 예약" CTA. Clean company-microsite aesthetic, multilingual.
|
||||
```
|
||||
|
||||
### SCR-P6 참가/부스 신청 문의
|
||||
- **레이아웃**: 참가 안내(부스 유형·요금 개요) + 문의 폼(회사·담당·규모·희망 홀·메시지) + 담당 연락처. 셀프서비스→로그인 유도. 상태: 검증·전송 완료.
|
||||
- **Stitch 프롬프트**:
|
||||
```
|
||||
A public "exhibit with us" inquiry page for an exhibition center, Korean marketing website, desktop 1440px, light theme, blue #0066B3. All UI text in Korean. Top: a "참가 안내" section with booth-type cards (조립부스 / 독립부스 / 프리미엄) and a rate overview. Below: an inquiry form with fields 회사명, 담당자, 연락처, 예상 규모, 희망 홀, 문의 내용, and a "문의 보내기" button, plus a contact info block and a link "이미 계정이 있으신가요? 로그인". Lead-generation marketing aesthetic.
|
||||
```
|
||||
|
||||
### SCR-P7 입장권 예매 (티켓팅)
|
||||
- **대상**: 일반 대중·관람객(비로그인 게스트 + 회원) · **모듈**: M10(티켓) · 결제=M9 연계 · **라우트**: `/tickets/:eventId/purchase` (공개, MDI 비적용·SEO)
|
||||
- **레이아웃**(위→아래, 중앙 정렬 스텝 폼 720px):
|
||||
1. 상단: 행사 요약 배너(포스터·행사명·기간·홀) + 스텝 진행 인디케이터 "행사·권종 → 예매자 → 결제 → 완료" (StatusBadge 진행 톤 재사용, pill 스텝)
|
||||
2. **1단계 권종 선택**: 티켓 종류 카드 리스트 — 일반(₩0 사전등록/현장 유료) · 바이어(무료·자격심사) · 단체(10매↑ 할인) · VIP. 각 카드 = 권종명·설명·가격(tabular numerals)·수량 스테퍼(−/+). 소계·할인·합계 실시간 요약(우측 sticky 요약 카드). 회원이면 로그인 배너 "로그인하면 예매 내역이 자동 저장됩니다"
|
||||
3. **2단계 예매자 정보**: 게스트=이름·이메일·연락처(마스킹 저장), 회원=자동 채움. 단체는 대표자+동반자 명단(CSV 업로드 옵션). 관심분야 chips(선택). 개인정보 수집·이용 동의(필수)·마케팅 수신(선택) 체크
|
||||
4. **3단계 결제**: 결제수단 선택(카드·간편결제·계좌이체) — **PG(결제대행) 위임 안내 문구 상시 노출**("결제는 PG사 보안 페이지에서 진행되며, 카드정보는 본 사이트에 저장되지 않습니다"). **카드번호 입력 UI 없음**(PG 리다이렉트/모듈 자리표시 카드). 무료 권종은 결제 단계 자동 스킵
|
||||
5. **4단계 완료**: 예매번호(굵게)·발급 티켓 QR 미리보기·"이메일로 티켓을 보냈습니다" + CTA "모바일 티켓 지갑 열기"(SCR-M15 딥링크)·"예매 확인/취소"(SCR-P8)
|
||||
- **상태**: 로딩(권종·재고 조회 스켈레톤) · 빈(판매 티켓 없음 "현재 예매 가능한 입장권이 없습니다") · 에러(재고 소진 시 해당 권종 "매진" 배지+비활성, 결제 실패 인라인 재시도) · degraded(PG 어댑터 미연결 시 "결제 준비 중 — 잠시 후 다시 시도" 안내, 무료 권종은 정상). 정원/재고는 서버 권위.
|
||||
- **데이터 원천**: M10 티켓·권종·재고 + M9 결제 어댑터(PG) → 신규·샘플 시연. 개인정보 마스킹·WCAG AA·라이트/다크 토큰 준수, 신규 토큰 없음.
|
||||
- **Stitch 프롬프트**:
|
||||
```
|
||||
A public ticket purchase (ticketing) page for an exhibition, Korean marketing website, desktop 1440px, light theme, primary blue #0066B3, on white, form container ~720px centered. All UI text in Korean. Top: an event summary banner (poster thumbnail, title "스마트팩토리 코리아 2026", dates "2026.09.05–09.08", venue "제2전시장 홀7") and a pill step indicator "행사·권종 → 예매자 → 결제 → 완료" with step 1 active. Left main column shows ticket-type selection cards — "일반권 ₩0(사전등록)", "바이어권 무료·자격심사", "단체권 10매↑ 10% 할인 ₩9,000", "VIP권 ₩50,000" — each card with name, short description, price in tabular numerals, and a quantity stepper (− / value / +); one type shows a red "매진" badge and disabled state. A right sticky summary card lists selected tickets, 소계, 할인, and a bold 합계 with a primary button "다음 단계". Include a subtle blue login banner "로그인하면 예매 내역이 자동 저장됩니다". Below reference the later steps: a payment step must show method chips "카드 · 간편결제 · 계좌이체" and a persistent gray notice "결제는 PG사 보안 페이지에서 진행되며, 카드정보는 본 사이트에 저장되지 않습니다" (NO card number input fields). Trustworthy, conversion-focused e-commerce checkout aesthetic; rounded 8px cards, 4px inputs, WCAG AA contrast.
|
||||
```
|
||||
|
||||
### SCR-P8 예매 확인·취소
|
||||
- **대상**: 일반 대중·관람객(비로그인 게스트 조회 + 회원) · **모듈**: M10(티켓) · 환불=M9 연계 · **라우트**: `/tickets/lookup` · `/tickets/:orderNo` (공개)
|
||||
- **레이아웃**(위→아래):
|
||||
1. 상단: "예매 확인·취소" 타이틀 + 조회 폼 — 예매번호 + 이메일(게스트) 또는 "로그인하여 내 예매 보기"(회원). 인증 후 조회
|
||||
2. **예매 상세 카드**: 행사·예매번호·예매일·결제금액(tabular)·상태 배지(예매완료/사용됨/취소/환불완료). 티켓 리스트(권종·수량·매수별 QR 썸네일→탭 시 확대). 예매자 정보(이메일·연락처 **마스킹** 표시 jo***@ex***.com, 010-****-1234)
|
||||
3. **취소·환불 정책 패널**: 환불 규정 표(D-7 100% · D-3 50% · 당일 0% 등, 서버 권위) + 현재 예상 환불액 하이라이트 + 안내 문구. CTA "예매 취소" (사용됨/기간 경과 시 비활성)
|
||||
4. 취소 확인 모달: 환불 정책 재고지·환불 수단(원결제수단 자동)·"취소하기"(error 톤) — PG 환불도 어댑터 위임 명시
|
||||
- **상태**: 로딩(조회 스켈레톤) · 빈/미일치("일치하는 예매를 찾을 수 없습니다 — 예매번호·이메일 확인") · 에러(취소 마감·이미 사용·PG 환불 실패 인라인) · degraded(PG 환불 어댑터 미연결 시 "환불 접수됨 — 처리까지 영업일 소요" 안내). 취소 가능 여부·환불액은 서버 권위.
|
||||
- **데이터 원천**: M10 예매·티켓 + M9 환불 어댑터 → 신규·샘플. PII 마스킹 필수·WCAG AA·라이트/다크 준수, 신규 토큰 없음.
|
||||
- **Stitch 프롬프트**:
|
||||
```
|
||||
A public "check & cancel my ticket booking" page for an exhibition, Korean marketing website, desktop 1440px, light theme, primary blue #0066B3, error red #D92D20, on white. All UI text in Korean. Top: title "예매 확인 · 취소" with a lookup form — inputs "예매번호" and "이메일", a primary "조회하기" button, and a secondary link "로그인하여 내 예매 보기". Below (results state): a booking detail card showing event title, 예매번호 "KTX-2026-018245", 예매일, 결제금액 in tabular numerals, and a green status badge "예매완료"; a ticket list with rows (권종, 수량) each showing a small QR thumbnail; a booker info block with MASKED contact "jo***@ex***.com · 010-****-1234". A right/lower panel "취소·환불 정책" shows a refund table (D-7 100%, D-3 50%, 당일 0%) with the current 예상 환불액 highlighted, and a red outline button "예매 취소" that opens a confirmation modal restating the policy and refund method. Note "환불은 PG사를 통해 원결제수단으로 처리됩니다". Clean, reassuring self-service booking-management aesthetic; 8px cards, WCAG AA.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 모바일 대응 (390px)
|
||||
|
||||
모바일은 **조회·승인·현장** 시나리오 전용(PLANNING.md §8). 캔버스 편집(SCR-03, 07)과 스펙 편집(SCR-06 좌패널)은 모바일 미제공 — 뷰어+승인 액션만. 아래 두 화면은 **모바일 전용 신규 화면**, 나머지는 반응형 축소.
|
||||
@ -654,9 +1447,166 @@ A mobile field inspection screen for a KINTEX hall manager verifying booth const
|
||||
|
||||
---
|
||||
|
||||
### SCR-M3 [모바일] 홈·대시보드 (역할별 진입)
|
||||
|
||||
- **대상**: 전 역할(로그인 후 역할 분기) · **P1** · **진입 경로**: 모바일 로그인 → 하단 탭 "홈" · **상태**: ✅구현(부분, `mobile/app/(tabs)/index.tsx`)
|
||||
- **레이아웃**: 상단 행사 스위처 + 역할 배지 · 오늘 할 일/마감 D-데이 카드 · 역할별 바로가기(참가=내 부스·유틸리티 / 장치=현장 체크리스트·옥션 / 홀매니저=승인 큐·검수 / 관람객=배지·플로어플랜) · 알림 요약 · 하단 탭 4개(홈/현장·업무/알림/더보기). 상태: 로딩 스켈레톤·오프라인 배지·빈.
|
||||
- **Stitch 프롬프트**:
|
||||
```
|
||||
A mobile home dashboard for "KINTEX AI 전시관리", 390px width, light theme, blue #0066B3, large touch targets, Korean sans-serif. All UI text in Korean. Top: an event switcher "스마트팩토리 코리아 ▾" and a role badge "홀매니저". Body: a "오늘 할 일" section with D-day task cards, a role-specific shortcut grid of large tiles ("승인 큐 · 현장 검수 · 홀 부하 · 알림"), and a compact notification summary list. Bottom navigation bar with 4 tabs "홈 · 현장 · 알림 · 더보기". Clean, glanceable mobile dashboard with big tap areas.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### SCR-M4 [모바일] 참가업체 리드캡처 (배지 스캔)
|
||||
|
||||
- **대상**: 참가업체 현장 인력 · **모듈/P**: M10 · P1 · **진입 경로**: 모바일 "현장" → "리드 스캔"
|
||||
- **레이아웃**: 대형 QR 스캔 카메라 프레임 + 스캔 성공 시 관람객 카드(이름·소속·유형) · 관심도 별점·태그·메모(음성 입력) · 저장→리드 목록. 오프라인 저장·동기화. 개인정보 동의 표기. 상태: 스캔 실패·중복·오프라인.
|
||||
- **Stitch 프롬프트**:
|
||||
```
|
||||
A mobile lead capture (badge scan) screen for an exhibitor, 390px width, light theme, blue #0066B3, large touch targets. All UI text in Korean. Top: a full-width QR scanner camera frame with guide "관람객 배지 QR을 스캔하세요". On scan, a visitor card slides up: name "정관람", company "(주)미래기술", type chip "바이어", a 5-star "관심도" rating, interest tag chips, and a memo field with a mic icon "음성 메모". A primary "리드 저장" button, an offline badge "오프라인 저장 · 동기화 대기", and a small consent note "개인정보 수집 동의 확인". Fast field-sales mobile aesthetic.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### SCR-M5 [모바일] 관람객 앱 홈 · 모바일 배지/QR
|
||||
|
||||
- **대상**: 관람객 · **모듈/P**: M10 · P1 · **진입 경로**: 관람객 앱 로그인/사전등록 완료
|
||||
- **레이아웃**: 상단 내 모바일 배지(QR·이름·유형, 밝기 최대 안내) · 오늘 일정·추천 세션(M11) · 부스 즐겨찾기 · 하단 탭(홈/지도/매칭/마이). 오프라인 배지 캐시. 상태: 미등록·로딩.
|
||||
- **Stitch 프롬프트**:
|
||||
```
|
||||
A visitor app home screen with a mobile badge/QR for an exhibition, 390px width, light theme, blue #0066B3, friendly consumer tone. All UI text in Korean. Top: a personal mobile badge card showing a large QR code, name "정관람", type "바이어", and a hint "체크인 시 화면을 보여주세요". Below: "오늘 일정" cards, an "추천 세션·부스" carousel with a small purple "AI 추천" chip, and a "즐겨찾기 부스" list. Bottom tab bar "홈 · 지도 · 매칭 · 마이". Clean consumer event-app aesthetic.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### SCR-M6 [모바일] 관람객 wayfinding · 실내 지도
|
||||
|
||||
- **대상**: 관람객 · **모듈/P**: M13 · P2 · **진입 경로**: 관람객 앱 "지도"
|
||||
- **레이아웃**: 전체 화면 실내 지도(블루닷 현재위치·경로 라인) + 상단 검색(부스·화장실·비상구·시설) + 하단 시트(목적지 정보·경로 시작). 존레벨 폴백(하드웨어 무의존). 상태: 측위 불가 시 존레벨 안내·오프라인.
|
||||
- **Stitch 프롬프트**:
|
||||
```
|
||||
A mobile indoor wayfinding / map screen for exhibition visitors, 390px width, light theme, blue #0066B3. All UI text in Korean. Full-screen indoor map of an exhibition hall with a blue-dot current-location marker and a highlighted route line to a destination; a top search bar "부스 · 화장실 · 비상구 · 시설 검색"; a bottom sheet showing destination info "A-102 · 한빛로보틱스 · 도보 3분" and a "경로 시작" button; a small note "존 레벨 안내(정밀 측위 미지원 시)". Clear navigation aesthetic.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### SCR-M7 [모바일] 관람객 플로어플랜 · 부스 검색
|
||||
|
||||
- **대상**: 관람객 · **모듈/P**: M12·M2 · P1 · **진입 경로**: 관람객 앱 "지도" 탭 내 플로어플랜 / 공개사이트 SCR-P3 모바일
|
||||
- **레이아웃**: 인터랙티브 홀 맵(부스 탭→하단 시트 업체정보·마이크로사이트·길안내) + 검색·필터(업종·존) + 즐겨찾기 추가. 비시각 대안 목록. 상태: 로딩·빈.
|
||||
- **Stitch 프롬프트**:
|
||||
```
|
||||
A mobile interactive floor plan / booth search screen for exhibition visitors, 390px width, light theme, blue #0066B3. All UI text in Korean. An interactive hall map filling the screen with tappable booth cells; a top search bar and filter chips "업종 · 존"; tapping a booth opens a bottom sheet with company name, category, a "마이크로사이트" link, a "길안내" button, and an "즐겨찾기 ★" toggle. Below the map a toggle to a list view (accessible alternative). Explorable consumer map aesthetic.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### SCR-M8 [모바일] 비즈매칭 · 미팅
|
||||
|
||||
- **대상**: 관람객·참가업체(바이어 매칭) · **모듈/P**: M11 · P2 · **진입 경로**: 관람객 앱 "매칭"
|
||||
- **레이아웃**: AI 추천 매치 카드 스택(프로필·관심·의향, 관심/패스 액션) + 미팅 요청·슬롯 예약 캘린더 + 내 미팅 목록(부스 위치·길안내 연계). AiLabel. 상태: 추천 없음·로딩.
|
||||
- **Stitch 프롬프트**:
|
||||
```
|
||||
A mobile business matchmaking / meeting screen for an exhibition, 390px width, light theme, blue #0066B3, purple #6D4AFF for AI. All UI text in Korean. Top: a stack of AI-recommended match cards, each showing a person/company profile, interests, a small purple "AI 매칭 92%" chip, and "관심 / 패스" buttons. Below: a "미팅 예약" time-slot calendar and a "내 미팅" list with time, counterpart, booth location and a "길안내" link. Networking-app aesthetic with AI matching.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### SCR-M9 [모바일] 세션·아젠다 · 개인 일정
|
||||
|
||||
- **대상**: 관람객 · **모듈/P**: M11(이벤트앱) · P2 · **진입 경로**: 관람객 앱 "일정"
|
||||
- **레이아웃**: 세션 아젠다(트랙·시간·연사, 즐겨찾기→내 일정) + AI 세션 추천(AiLabel) + 내 개인 일정 타임라인 + 리마인더. 상태: 빈·로딩.
|
||||
- **Stitch 프롬프트**:
|
||||
```
|
||||
A mobile session agenda / personal schedule screen for an exhibition app, 390px width, light theme, blue #0066B3, purple #6D4AFF for AI. All UI text in Korean. Top: a track/time agenda list with session cards (시간, 세션명, 연사, 트랙 tag) and a "★" add-to-schedule toggle; a small purple "AI 추천 세션" section. A tab toggle "전체 아젠다 / 내 일정" where 내 일정 shows a personal timeline with reminders. Clean event-agenda aesthetic.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### SCR-M10 [모바일] 업체 반입 통행증 · 안전 체크
|
||||
|
||||
- **대상**: 장치·공사업체 현장 인력 · **모듈/P**: M8 · P2 · **진입 경로**: 업체 모바일 "반입" (SCR-25 예약 후)
|
||||
- **레이아웃**: 통행증 QR(차량·슬롯·유효시간) 전체화면 표시 + 반입 안전 체크리스트(안전모·중량물 표기·금지작업) + 슬롯 시간 안내·현장 지도 링크. 오프라인 캐시. 상태: 슬롯 임박 알림·만료.
|
||||
- **Stitch 프롬프트**:
|
||||
```
|
||||
A mobile move-in gate pass and safety check screen for a construction crew, 390px width, light theme, blue #0066B3, large touch targets. All UI text in Korean. Top: a large gate-pass card with a QR code, vehicle number "12가3456", assigned dock "도크3", and validity "08:00–09:00 · D-2 장치". Below: a safety checklist with big checkboxes "안전모 착용 · 중량물 5t↑ 신고 · 금지작업 확인(전기톱·용접·페인트)", a "슬롯 시간 D-0 09:00" reminder, and a "현장 지도" link. Rugged field-ops mobile aesthetic.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### SCR-M11 [모바일] 알림센터
|
||||
|
||||
- **대상**: 전 역할 · **모듈/P**: §5B notification · P1 · **진입 경로**: 하단 탭 "알림"
|
||||
- **레이아웃**: 알림 리스트(마감·낙찰·승인·결제·시스템, 아이콘·시간·안읽음) · 탭 시 해당 화면 딥링크 · 상단 필터·모두 읽음. WebSocket. 상태: 빈("새 알림 없음")·로딩·오프라인.
|
||||
- **Stitch 프롬프트**:
|
||||
```
|
||||
A mobile notification center for "KINTEX AI 전시관리", 390px width, light theme, blue #0066B3, large touch targets. All UI text in Korean. Top: filter chips "전체 · 마감 · 낙찰 · 승인 · 결제" and a "모두 읽음" action. Body: a chronological notification list, each row with a category icon, title, short description, timestamp, and an unread blue dot; tapping opens the related screen. Empty state "새 알림이 없습니다". Clean mobile inbox aesthetic.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### SCR-M12 [모바일] 서류·승인 조회
|
||||
|
||||
- **대상**: 주최자·홀매니저(조회·승인) · **모듈/P**: M6 · P1 · **진입 경로**: 모바일 "업무" → "서류/승인"
|
||||
- **레이아웃**: 서류·승인 항목 카드 리스트(유형·제출자·상태 배지·마감 D-데이) + 상세(문서 뷰어·AI 검수 요약) + 승인/반려 액션(오조작 방지: 상세 진입 필수). 상태: 빈·로딩·오프라인 읽기전용.
|
||||
- **Stitch 프롬프트**:
|
||||
```
|
||||
A mobile document/approval review screen for an organizer or hall manager, 390px width, light theme, blue #0066B3, purple #6D4AFF for AI. All UI text in Korean. Top: a card list of documents/approvals, each with type "신고서류 · 부스배치도", submitter, a status pill, and a D-day chip. Tapping opens a detail view with a document viewer, a purple "AI 검수 요약" card listing issues, and bottom action buttons "승인" (green) / "반려" (red). No swipe-approve (must open detail). Careful, review-focused mobile aesthetic.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### SCR-M13 [모바일] 옥션 순위 조회
|
||||
|
||||
- **대상**: 장치업체·참가업체(발주자) · **모듈/P**: M15 · P1 · **진입 경로**: 모바일 "업무" → "옥션"
|
||||
- **레이아웃**: 참여 옥션 카드(공종·마감 카운트다운·내 순위·최저가) + 실시간 순위 리스트(WebSocket) + 알림. 응찰(견적서 작성)은 웹(SCR-28) 유도 — 모바일은 조회·순위·마감 알림 중심. 상태: 마감됨·오프라인·로딩.
|
||||
- **Stitch 프롬프트**:
|
||||
```
|
||||
A mobile auction ranking monitor for a contractor, 390px width, light theme, blue #0066B3, purple #6D4AFF. All UI text in Korean. Top: participating auction cards with category, a countdown "마감 01:24:36", my rank "2위", and current lowest "₩8.4M". Below: a live ranking list "1위 업체 A · 2위 나 · 3위 업체 C" with the user's row highlighted, and a note "견적서 작성·재응찰은 웹에서 진행하세요". Real-time monitoring mobile aesthetic.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### SCR-M14 [모바일] 티켓 예매
|
||||
|
||||
- **대상**: 일반 대중·관람객(게스트+회원) · **모듈/P**: M10(티켓)·결제 M9 연계 · P1 · **진입 경로**: 관람객 앱/공개사이트 모바일 "입장권 예매" · SCR-P7의 모바일 최적화
|
||||
- **레이아웃**(1열 스택, 48px 터치·하단 고정 CTA):
|
||||
1. 상단: 행사 요약 칩 + 스텝 인디케이터(권종→예매자→결제→완료) 축약(단계 점)
|
||||
2. 권종 선택: 티켓 종류 카드 세로 스택(권종명·가격·수량 스테퍼 48px). 하단 고정 요약 바 "합계 ₩9,000 · 다음"
|
||||
3. 예매자 정보: 큰 입력 필드(게스트 이름·이메일·연락처 / 회원 자동채움), 동의 체크(필수·선택)
|
||||
4. 결제: 결제수단 시트(카드·간편결제) + **PG 위임 안내 상시**("카드정보는 저장되지 않습니다"), 카드번호 입력 UI 없음. 무료 권종은 스킵
|
||||
5. 완료: 예매번호·QR·"티켓 지갑에 저장됨" + CTA "내 티켓 지갑"(SCR-M15)
|
||||
- **상태**: 로딩 스켈레톤 · 매진 권종 비활성+"매진" · 결제 실패 인라인 재시도 · degraded(PG 미연결 안내, 무료 권종 정상) · 오프라인 배지. 재고/정원 서버 권위.
|
||||
- **데이터 원천**: M10 티켓·재고 + M9 결제 어댑터 → 신규·샘플. PII 마스킹·WCAG AA·라이트/다크, 신규 토큰 없음.
|
||||
- **Stitch 프롬프트**:
|
||||
```
|
||||
A mobile ticket purchase flow screen for an exhibition, 390px width, light theme, primary blue #0066B3, large 48px touch targets, Korean consumer tone. All UI text in Korean. Top: an event summary chip "스마트팩토리 코리아 2026 · 홀7" and a compact dot step indicator "권종 · 예매자 · 결제 · 완료". Body: a vertical stack of ticket-type cards — "일반권 ₩0", "바이어권 무료", "단체권 ₩9,000", "VIP권 ₩50,000" — each with name, price in tabular numerals, and a big quantity stepper (− / 1 / +); one card shows a "매진" badge disabled. A sticky bottom summary bar shows bold "합계 ₩9,000" and a full-width primary button "다음". Include a later payment step reference: method chips "카드 · 간편결제" and a persistent notice "결제는 PG사 보안 페이지에서 진행되며 카드정보는 저장되지 않습니다" with NO card number fields. Clean mobile checkout aesthetic, 8px cards, WCAG AA.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### SCR-M15 [모바일] 내 티켓 지갑
|
||||
|
||||
- **대상**: 일반 대중·관람객 · **모듈/P**: M10(티켓)·환불 M9 연계 · P1 · **진입 경로**: 관람객 앱 "마이" → "내 티켓" / 예매 완료(SCR-P7·M14) 딥링크
|
||||
- **레이아웃**(1열 스택):
|
||||
1. 상단: "내 티켓" 타이틀 + 행사 필터 탭(진행중·예정·지난)
|
||||
2. **티켓 카드 리스트**: 각 카드 = 행사명·권종·매수·상태 배지(사용가능/사용됨/취소) + 미니 QR + "입장 QR 보기" 버튼
|
||||
3. **QR 풀스크린 뷰어**(카드 탭): 대형 QR 중앙 · 이름·유형 · **화면 밝기 부스트 안내("입장 시 화면을 최대 밝기로")** + 자동 밝기 상승 · 하단 스와이프로 다른 티켓 전환 · 예매번호(마스킹 부분 노출)
|
||||
4. 각 티켓 하단: "예매 상세·취소"(SCR-P8 진입) · 취소/환불 정책 링크
|
||||
5. **배지 전환 안내 배너**: "티켓 QR로 현장 체크인하면 모바일 배지로 전환됩니다" — 체크인(SCR-31) 후 SCR-M5 관람객 배지/QR로 상태 승격(티켓→체크인→배지 흐름 1:1 명시)
|
||||
- **상태**: 로딩 스켈레톤 · 빈("보유한 티켓이 없습니다 — 입장권 예매") · **오프라인 표시 가능**(QR·티켓 로컬 캐시, "오프라인 — 저장된 티켓 표시 중" 배지) · 사용됨/취소 티켓 흐리게(dim). 상태 전이는 서버 권위(오프라인은 캐시 표시).
|
||||
- **데이터 원천**: M10 예매·티켓·체크인 상태 → 신규·샘플. SCR-M5(배지)와 상태 연동. PII 마스킹·WCAG AA·라이트/다크, 신규 토큰 없음.
|
||||
- **Stitch 프롬프트**:
|
||||
```
|
||||
A mobile "my ticket wallet" screen for an exhibition visitor, 390px width, light theme, primary blue #0066B3, success green #0E8A5F, friendly consumer tone. All UI text in Korean. Top: title "내 티켓" with filter tabs "진행중 · 예정 · 지난". Body: a vertical list of ticket cards, each showing event title "스마트팩토리 코리아 2026", 권종 "바이어권", 매수 "1매", a green status badge "사용가능", a small QR thumbnail, and a button "입장 QR 보기"; used/canceled tickets appear dimmed. Include a full-screen QR viewer reference: a large centered QR code, name "정관람", type "바이어", a hint "입장 시 화면을 최대 밝기로 유지하세요" with a brightness-boost note, swipe dots to switch tickets, and a masked 예매번호 "KTX-****-8245". Add an info banner "티켓 QR로 현장 체크인하면 모바일 배지로 전환됩니다" and a small offline badge "오프라인 — 저장된 티켓 표시 중". Each card has a link "예매 상세·취소". Clean consumer wallet aesthetic, 8px cards, WCAG AA, high-contrast QR.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Stitch 사용 가이드 (요약)
|
||||
|
||||
1. stitch.withgoogle.com에서 새 프로젝트 생성 → 플랫폼은 웹 화면(SCR-01~12)은 Desktop, SCR-M1/M2는 Mobile 선택.
|
||||
1. stitch.withgoogle.com에서 새 프로젝트 생성 → 플랫폼은 웹 화면(SCR-01~51·SCR-A*·SCR-P*)은 Desktop, 모바일(SCR-M1~M15)은 Mobile 선택. 공개사이트(SCR-P*)는 마케팅 톤·넓은 히어로로, 관리자(SCR-A*)는 밀도 높은 백오피스 톤으로 생성한다.
|
||||
2. 화면당 위 코드블록 프롬프트 1개를 그대로 붙여넣어 생성 — 프롬프트에 색상·레이아웃·예시 데이터가 포함되어 있어 추가 지시 없이 1회 생성이 기준.
|
||||
3. 결과 보정은 짧은 추가 지시로: 예) "Make the right quote panel 340px and sticky". 한국어 텍스트가 깨지면 "Keep all labels in Korean exactly as given" 재지시.
|
||||
4. 생성 화면은 Figma로 내보내 디자인 시스템 토큰(§1)과 대조 후 조정한다.
|
||||
@ -667,6 +1617,8 @@ A mobile field inspection screen for a KINTEX hall manager verifying booth const
|
||||
|
||||
| 버전 | 일자 | 작성자 | 내용 |
|
||||
|---|---|---|---|
|
||||
| **v2.1** | 2026-07-11 | designer | **입장권 예매 4화면 추가(M10 티켓 기획 대비 화면 갭 해소).** PLANNING v3.0 M10이 티켓을 기획 범위에 포함하나 design.md v2.0 인벤토리엔 예매 흐름 화면이 없던 갭을 메움 — 기존 번호 체계 이어서 순증(재번호화·기존 스펙 수정 없음). ①**§2.8-1 매트릭스**: M10 행에 SCR-P7·P8/M14·M15, M9 행에 SCR-P7(티켓결제 연계) 반영. ②**§2.8-2 인덱스**: 공개사이트에 SCR-P7(입장권 예매)·SCR-P8(예매 확인·취소), 모바일에 SCR-M14(모바일 티켓 예매)·SCR-M15(내 티켓 지갑) 추가, 집계 80→**84화면**(공개 6→8·모바일 13→15, 미생성 60→64). ③**§3B**: SCR-P7(행사·권종→예매자→결제→완료 다단계, 게스트+회원, 결제 PG 어댑터 위임·카드번호 입력 UI 없음)·SCR-P8(예매번호+이메일 조회·QR 티켓 상세·환불정책·취소) 상세+영어 Stitch 프롬프트. ④**§4**: SCR-M14(SCR-P7 모바일 최적화 390px·48px 터치)·SCR-M15(보유 티켓·QR 풀스크린 밝기부스트·오프라인 표시·취소 진입, 티켓→체크인→배지(SCR-M5) 전환 명시) 상세+프롬프트. §5 가이드 모바일 범위 M1~M13→M1~M15. **보안·불변 준수**: 결제 화면 카드번호 저장 UI 금지(PG 위임 명시)·PII 마스킹·WCAG AA·i18n·라이트/다크. §1 토큰 권위 유지, 신규 토큰 0. PLANNING.md·기존 화면 스펙·src 무수정 |
|
||||
| **v2.0** | 2026-07-11 | designer | **전 화면 인벤토리 확장 — PLANNING v3.0 전 모듈·전 역할 커버.** 기존 SCR-01~17·SCR-M1/M2(재번호화 없이 보존)에 **신규 60화면 순증**: ①**§2.8 전 화면 인벤토리 매트릭스 신설**(모듈×역할×채널 요약표 + 전 SCR 인덱스 + 기생성/기구현/미생성 상태 컬럼, 총 80화면). ②**웹 코어·도메인 SCR-18~51**(M1 배정·견적/부스판매/셀프선택, M9 정산, M6 서류·마일스톤·서류작성, M7 매칭·RFQ, M8 물류, **M15 옥션 4종**(목록·상세순위·견적서·낙찰), M10 관람객관리·체크인·리드, M12 EDM·스폰서십, M17 CMS·마이크로사이트·다국어, 업체 대시보드, **§5B 공통 업무기능 SCR-39~48**(worklog·schedule·message·notice·opinion·search·meeting·report·notification·mypage, UIWS 표준 정합), 인증 SCR-49~51(회원가입·비번재설정·OTP)). ③**§3A 관리자 백오피스 SCR-A1~A10**(사용자·RBAC·공통코드·메뉴·감사로그·시스템설정·마스터데이터·룰셋버전·**테넌트 온보딩§1A**·AiConfig). ④**§3B 공개 홍보 사이트 SCR-P1~P6**(홈·행사상세·인터랙티브 플로어플랜·사전등록·마이크로사이트 공개뷰·참가문의, MDI 비적용·SEO/다국어). ⑤**모바일 SCR-M3~M13**(역할별 홈·리드캡처·관람객 배지/QR·wayfinding·플로어플랜·비즈매칭·세션아젠다·업체 통행증·알림·서류승인·옥션순위). 각 화면 SCR ID·화면명·역할·모듈·레이아웃·상태(로딩/빈/에러/degraded)·데이터 원천(실/샘플 경계)·라우트·**영어 Stitch 프롬프트** 포함. §1 토큰·§2.7 MDI 셸 보존, **신규 토큰 0**. AI 산출물 워터마크·AiLabel·PII 마스킹·WCAG AA·i18n·라이트/다크 토큰 불변 규칙 반영. PLANNING.md·src 무수정 |
|
||||
| v1.0 | 2026-07-11 | designer | 최초 작성 — PLANNING.md v1.0 기반. 디자인 시스템·IA·P0 중심 14화면(웹 12 + 모바일 2) Stitch 프롬프트 포함. 다크모드는 Phase 1 미지원(캔버스 다크 서피스만) 결정 |
|
||||
| v1.0.1 | 2026-07-11 | reviewer 반영 | 화면 수 표기 정정(13→14). 잔여 지적사항은 docs/BACKLOG.md 참조 |
|
||||
| v1.1 | 2026-07-11 | designer | **Stitch 산출물 학습·정합화.** `stitch_kintex_ai_system_architect/` 18화면 code.html + DESIGN.md 3변형(Intelligence·Nexus·Precision) 학습. ①§1-2 컬러: Precision Enterprise AI 변형이 우리 토큰(#0066B3·#1C2536·#6D4AFF·#D92D20·#F9FAFB·#101828·StatusBadge 진행) 실증 확인 — 우리 팔레트를 단일 출처로 유지, #0059A6·#00427D는 짙은 대체 톤으로만 허용, ai-surface #F5F3FF AI 카드 배경 채택. ②§1-3 타이포: Pretendard 권위 유지, Inter를 라틴/tabular 보조로 허용(Stitch가 Hanken Grotesk/Inter 채택), 행 높이 44/48px 권위값 유지. ③§1-4 라운드(4/8px·pill)·엘리베이션(톤레이어+1px 테두리+AI 좌측 보라 액센트)·그리드(12컬럼·1440px·4px 베이스)·지브라 테이블 토큰 신설(이전 누락분). ④§2.5 Stitch 화면↔SCR 매핑표 신설(14화면). ⑤§2.6 범위 게이트 — proposal.md 및 범위 밖 4화면(business_intelligence·hall_operations·exhibition_schedule·admin_dashboard)을 P2 후보로 분류(PLANNING이 범위 권위, designer 임의 승격 금지). PLANNING.md·타 문서 무수정 |
|
||||
|
||||
23
mobile/.gitignore
vendored
Normal file
23
mobile/.gitignore
vendored
Normal file
@ -0,0 +1,23 @@
|
||||
# Expo / RN
|
||||
node_modules/
|
||||
.expo/
|
||||
dist/
|
||||
web-build/
|
||||
*.log
|
||||
|
||||
# Native
|
||||
ios/
|
||||
android/
|
||||
*.orig.*
|
||||
*.jks
|
||||
*.p8
|
||||
*.p12
|
||||
*.key
|
||||
*.mobileprovision
|
||||
|
||||
# Env / secrets
|
||||
.env
|
||||
.env.*
|
||||
|
||||
# Misc
|
||||
.DS_Store
|
||||
43
mobile/README.md
Normal file
43
mobile/README.md
Normal file
@ -0,0 +1,43 @@
|
||||
# KINTEX AI 전시관리 — 모바일 앱 (mobile/)
|
||||
|
||||
React Native + Expo(expo-router, TypeScript) 모바일 앱. 조회·승인·현장 시나리오 전용
|
||||
(design.md §4). 백엔드는 공유 API `https://kintex.zioinfo.co.kr`.
|
||||
|
||||
## 스택
|
||||
- Expo SDK 51 · expo-router ~3.5 · React Native 0.74 · TypeScript
|
||||
- JWT는 `expo-secure-store`(네이티브 keychain/keystore)에 저장, 웹은 AsyncStorage 폴백
|
||||
- 디자인 토큰: `theme/`(docs/design.md §1 팔레트·타이포). 신규 토큰 없음.
|
||||
|
||||
## 실행
|
||||
```bash
|
||||
cd mobile
|
||||
npm install
|
||||
npm run typecheck # tsc --noEmit
|
||||
npm start # Expo 개발 서버 (Expo Go / 에뮬레이터)
|
||||
```
|
||||
|
||||
API 베이스는 `app.json > extra.apiBase` 또는 `EXPO_PUBLIC_API_BASE` 환경변수로 재정의.
|
||||
시크릿(키 등)은 하드코딩하지 않는다.
|
||||
|
||||
## 화면
|
||||
| 경로 | 화면 | 근거 |
|
||||
|---|---|---|
|
||||
| `app/login.tsx` | 로그인(2FA/OTP·아이디 기억·비번찾기/회원가입) | SCR-01 |
|
||||
| `app/register.tsx` | 회원가입 | 04_backend_public_auth §1 |
|
||||
| `app/forgot-password.tsx` | 비밀번호 찾기/초기화 | 04 §2·§3 |
|
||||
| `app/(tabs)/index.tsx` | 홈/대시보드(역할별 진입) | design §2-2 |
|
||||
| `app/(tabs)/field.tsx` | 현장 허브(역할 분기) | — |
|
||||
| `app/(tabs)/gallery.tsx` | 시각화 갤러리(워터마크 상시) | SCR-12 |
|
||||
| `app/(tabs)/more.tsx` | 계정·서버 상태·로그아웃 | — |
|
||||
| `app/checklist.tsx` | 현장 체크리스트(48px 터치·오프라인·사진) | SCR-M1 |
|
||||
| `app/inspection.tsx` | 현장 검수(승인/반려·AI 플래그) | SCR-M2 |
|
||||
|
||||
## 계약·보안 준수
|
||||
- 응답 오류 봉투 `{ success, data, error }` 처리(`lib/api.ts`), degraded 폴백(`isDegraded`).
|
||||
- AI 생성 이미지는 `components/AiImage.tsx`가 워터마크 + "AI 생성 예상 이미지" 고지를 **항상** 표시(제거 불가).
|
||||
- 비밀번호는 저장하지 않음. "아이디 기억"은 이메일만 AsyncStorage 보관.
|
||||
|
||||
## 남은 작업
|
||||
- 실 화면 데이터 API 배선 확대(부스 목록·검수 요청 제출 — 백엔드 M6/C-4 확장 대기).
|
||||
- 앱 아이콘/스플래시 에셋(`assets/`), i18n, 푸시 알림.
|
||||
- EAS 실빌드(eas.json 프로파일) — 본 스캐폴드 범위 밖.
|
||||
40
mobile/app.json
Normal file
40
mobile/app.json
Normal file
@ -0,0 +1,40 @@
|
||||
{
|
||||
"expo": {
|
||||
"name": "KINTEX AI 전시관리",
|
||||
"slug": "kintex",
|
||||
"scheme": "kintex",
|
||||
"version": "0.1.0",
|
||||
"orientation": "portrait",
|
||||
"userInterfaceStyle": "light",
|
||||
"newArchEnabled": false,
|
||||
"splash": {
|
||||
"resizeMode": "contain",
|
||||
"backgroundColor": "#004C86"
|
||||
},
|
||||
"assetBundlePatterns": ["**/*"],
|
||||
"ios": {
|
||||
"supportsTablet": true,
|
||||
"bundleIdentifier": "kr.co.zioinfo.kintex"
|
||||
},
|
||||
"android": {
|
||||
"package": "kr.co.zioinfo.kintex",
|
||||
"adaptiveIcon": {
|
||||
"backgroundColor": "#0066B3"
|
||||
}
|
||||
},
|
||||
"web": {
|
||||
"bundler": "metro",
|
||||
"output": "single"
|
||||
},
|
||||
"plugins": [
|
||||
"expo-router",
|
||||
"expo-secure-store"
|
||||
],
|
||||
"experiments": {
|
||||
"typedRoutes": false
|
||||
},
|
||||
"extra": {
|
||||
"apiBase": "https://kintex.zioinfo.co.kr"
|
||||
}
|
||||
}
|
||||
}
|
||||
59
mobile/app/(tabs)/_layout.tsx
Normal file
59
mobile/app/(tabs)/_layout.tsx
Normal file
@ -0,0 +1,59 @@
|
||||
/*
|
||||
* 하단 탭 바 4개 — 홈 / 현장 / 갤러리 / 더보기 (design.md §2-2 모바일).
|
||||
* 현장 탭은 역할에 따라 체크리스트(장치업체)·검수(홀매니저)로 진입.
|
||||
*/
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { Redirect, Tabs } from 'expo-router';
|
||||
import React from 'react';
|
||||
import { useAuth } from '../../context/AuthContext';
|
||||
import { colors } from '../../theme';
|
||||
|
||||
export default function TabsLayout() {
|
||||
const { ready, token } = useAuth();
|
||||
if (ready && !token) return <Redirect href="/login" />;
|
||||
|
||||
return (
|
||||
<Tabs
|
||||
screenOptions={{
|
||||
tabBarActiveTintColor: colors.primary600,
|
||||
tabBarInactiveTintColor: colors.neutral500,
|
||||
headerStyle: { backgroundColor: colors.white },
|
||||
headerTitleStyle: { fontWeight: '600', color: colors.neutral900 },
|
||||
tabBarStyle: { borderTopColor: colors.neutral200 },
|
||||
}}
|
||||
>
|
||||
<Tabs.Screen
|
||||
name="index"
|
||||
options={{
|
||||
title: '홈',
|
||||
tabBarIcon: ({ color, size }) => <Ionicons name="home-outline" color={color} size={size} />,
|
||||
}}
|
||||
/>
|
||||
<Tabs.Screen
|
||||
name="field"
|
||||
options={{
|
||||
title: '현장',
|
||||
tabBarIcon: ({ color, size }) => (
|
||||
<Ionicons name="clipboard-outline" color={color} size={size} />
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<Tabs.Screen
|
||||
name="gallery"
|
||||
options={{
|
||||
title: '갤러리',
|
||||
tabBarIcon: ({ color, size }) => <Ionicons name="images-outline" color={color} size={size} />,
|
||||
}}
|
||||
/>
|
||||
<Tabs.Screen
|
||||
name="more"
|
||||
options={{
|
||||
title: '더보기',
|
||||
tabBarIcon: ({ color, size }) => (
|
||||
<Ionicons name="ellipsis-horizontal" color={color} size={size} />
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
99
mobile/app/(tabs)/field.tsx
Normal file
99
mobile/app/(tabs)/field.tsx
Normal file
@ -0,0 +1,99 @@
|
||||
/*
|
||||
* 현장 탭 — 역할별 현장 기능 허브.
|
||||
* 장치업체 → 현장 체크리스트(SCR-M1), 홀매니저 → 현장 검수(SCR-M2).
|
||||
*/
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { router } from 'expo-router';
|
||||
import React from 'react';
|
||||
import { Pressable, ScrollView, StyleSheet, Text, View } from 'react-native';
|
||||
import { Banner } from '../../components/Banner';
|
||||
import { useAuth } from '../../context/AuthContext';
|
||||
import { colors, radius, spacing, type } from '../../theme';
|
||||
|
||||
export default function FieldScreen() {
|
||||
const { activeWorkspace, user } = useAuth();
|
||||
const role = activeWorkspace?.myRole;
|
||||
const isHallManager = user?.hallManager || role === 'HALL_MANAGER';
|
||||
const isContractor = role === 'CONTRACTOR';
|
||||
|
||||
return (
|
||||
<ScrollView contentContainerStyle={styles.scroll}>
|
||||
<Text style={styles.title}>현장 업무</Text>
|
||||
|
||||
{!isContractor && !isHallManager ? (
|
||||
<Banner tone="info">
|
||||
현장 기능은 장치·시공업체(체크리스트)와 홀매니저(검수) 역할에서 제공됩니다.
|
||||
</Banner>
|
||||
) : null}
|
||||
|
||||
{isContractor ? (
|
||||
<Tile
|
||||
icon="clipboard-outline"
|
||||
title="현장 체크리스트"
|
||||
desc="장치 공정 확인 · 사진 첨부 · 검수 요청"
|
||||
onPress={() => router.push('/checklist')}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{isHallManager ? (
|
||||
<Tile
|
||||
icon="shield-checkmark-outline"
|
||||
title="현장 검수"
|
||||
desc="승인 도면 vs 현장 사진 · 적합/부적합 판정"
|
||||
onPress={() => router.push('/inspection')}
|
||||
/>
|
||||
) : null}
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
|
||||
function Tile({
|
||||
icon,
|
||||
title,
|
||||
desc,
|
||||
onPress,
|
||||
}: {
|
||||
icon: keyof typeof Ionicons.glyphMap;
|
||||
title: string;
|
||||
desc: string;
|
||||
onPress: () => void;
|
||||
}) {
|
||||
return (
|
||||
<Pressable style={styles.tile} onPress={onPress}>
|
||||
<View style={styles.tileIcon}>
|
||||
<Ionicons name={icon} size={24} color={colors.primary600} />
|
||||
</View>
|
||||
<View style={styles.tileBody}>
|
||||
<Text style={styles.tileTitle}>{title}</Text>
|
||||
<Text style={styles.tileDesc}>{desc}</Text>
|
||||
</View>
|
||||
<Ionicons name="chevron-forward" size={20} color={colors.neutral500} />
|
||||
</Pressable>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
scroll: { padding: spacing.md, gap: spacing.md },
|
||||
title: { fontSize: type.h2.fontSize, fontWeight: '700', color: colors.neutral900 },
|
||||
tile: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 12,
|
||||
backgroundColor: colors.white,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.neutral200,
|
||||
borderRadius: radius.md,
|
||||
padding: spacing.md,
|
||||
},
|
||||
tileIcon: {
|
||||
width: 44,
|
||||
height: 44,
|
||||
borderRadius: radius.sm,
|
||||
backgroundColor: colors.primary050,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
tileBody: { flex: 1, gap: 2 },
|
||||
tileTitle: { fontSize: type.h3.fontSize, fontWeight: '600', color: colors.neutral900 },
|
||||
tileDesc: { fontSize: type.caption.fontSize, color: colors.neutral500 },
|
||||
});
|
||||
151
mobile/app/(tabs)/gallery.tsx
Normal file
151
mobile/app/(tabs)/gallery.tsx
Normal file
@ -0,0 +1,151 @@
|
||||
/*
|
||||
* SCR-12 시각화 갤러리 (모바일 2열) — 워터마크 상시 + 상단 고정 고지 바.
|
||||
* 계약: GET /api/events/{eventId}/booths/{boothId}/render-jobs?shot={S1} (스켈레톤 501 → degraded 폴백).
|
||||
* 실 API 호출 구조를 유지하되, 서버 미구현/오프라인 시 degraded 샘플로 폴백한다.
|
||||
*/
|
||||
import { router } from 'expo-router';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { RefreshControl, ScrollView, StyleSheet, Text, View } from 'react-native';
|
||||
import { AiImage } from '../../components/AiImage';
|
||||
import { Banner } from '../../components/Banner';
|
||||
import { Button } from '../../components/Button';
|
||||
import { useAuth } from '../../context/AuthContext';
|
||||
import { api, isDegraded } from '../../lib/api';
|
||||
import { AI_IMAGE_NOTICE } from '../../lib/config';
|
||||
import type { RenderJobDto } from '../../lib/types';
|
||||
import { colors, spacing, type } from '../../theme';
|
||||
|
||||
const DEMO_BOOTH = 'b-102';
|
||||
|
||||
// degraded 폴백 샘플 (서버 미구현/오프라인 시 표시 — 항상 워터마크 유지)
|
||||
const FALLBACK: RenderJobDto[] = [
|
||||
mkJob('job-s1', 'S1', 'DONE'),
|
||||
mkJob('job-s2', 'S2', 'DONE'),
|
||||
mkJob('job-s3', 'S3', 'RUNNING'),
|
||||
mkJob('job-s7', 'S7', 'FAILED'),
|
||||
];
|
||||
|
||||
function mkJob(jobId: string, shot: string, status: RenderJobDto['status']): RenderJobDto {
|
||||
return {
|
||||
jobId,
|
||||
boothId: DEMO_BOOTH,
|
||||
shotPreset: shot,
|
||||
status,
|
||||
imageUrl: null,
|
||||
schemaHash: null,
|
||||
modelVersion: null,
|
||||
watermarkRequired: true,
|
||||
watermarkText: AI_IMAGE_NOTICE,
|
||||
notice: 'AI 생성 이미지는 계약·심사 서류에 사용할 수 없습니다 (시공 기준은 도면)',
|
||||
};
|
||||
}
|
||||
|
||||
const SHOT_LABEL: Record<string, string> = {
|
||||
S1: 'S1 정면',
|
||||
S2: 'S2 야간',
|
||||
S3: 'S3 통로',
|
||||
S4: 'S4 내부',
|
||||
S5: 'S5 비교',
|
||||
S6: 'S6 배선',
|
||||
S7: 'S7 홀 전경',
|
||||
};
|
||||
|
||||
export default function GalleryScreen() {
|
||||
const { activeWorkspace, token } = useAuth();
|
||||
const [jobs, setJobs] = useState<RenderJobDto[]>([]);
|
||||
const [degraded, setDegraded] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true);
|
||||
if (!activeWorkspace || !token) {
|
||||
setJobs(FALLBACK);
|
||||
setDegraded(true);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const res = await api.get<RenderJobDto[]>(
|
||||
`/api/events/${activeWorkspace.eventId}/booths/${DEMO_BOOTH}/render-jobs`,
|
||||
);
|
||||
setJobs(res ?? []);
|
||||
setDegraded(false);
|
||||
} catch (e) {
|
||||
if (isDegraded(e)) {
|
||||
setJobs(FALLBACK);
|
||||
setDegraded(true);
|
||||
} else {
|
||||
setJobs([]);
|
||||
setDegraded(false);
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [activeWorkspace, token]);
|
||||
|
||||
useEffect(() => {
|
||||
load();
|
||||
}, [load]);
|
||||
|
||||
return (
|
||||
<View style={styles.flex}>
|
||||
{/* 상단 고정 고지 바 (제거 불가) */}
|
||||
<View style={styles.infoBar}>
|
||||
<Text style={styles.infoBarText}>
|
||||
AI 생성 이미지는 계약·심사 서류에 사용할 수 없습니다 — 시공 기준은 도면입니다
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<ScrollView
|
||||
contentContainerStyle={styles.scroll}
|
||||
refreshControl={<RefreshControl refreshing={loading} onRefresh={load} tintColor={colors.primary600} />}
|
||||
>
|
||||
{degraded ? (
|
||||
<Banner tone="degraded">
|
||||
서버 이미지 이력에 연결할 수 없어 예시를 표시합니다 (degraded).
|
||||
</Banner>
|
||||
) : null}
|
||||
|
||||
{jobs.length === 0 && !loading ? (
|
||||
<Banner tone="info">
|
||||
아직 생성된 이미지가 없습니다 — 설계 스튜디오에서 예상 사진을 생성하세요.
|
||||
</Banner>
|
||||
) : null}
|
||||
|
||||
<View style={styles.grid}>
|
||||
{jobs.map((j) => (
|
||||
<View key={j.jobId} style={styles.cell}>
|
||||
<AiImage
|
||||
uri={j.imageUrl}
|
||||
status={j.status}
|
||||
watermarkText={j.watermarkText}
|
||||
notice={j.notice}
|
||||
shotBadge={SHOT_LABEL[j.shotPreset] ?? j.shotPreset}
|
||||
height={130}
|
||||
/>
|
||||
{j.status === 'FAILED' ? (
|
||||
<Button label="다시 생성" variant="outline" onPress={load} style={styles.retry} />
|
||||
) : null}
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
|
||||
<Button
|
||||
label="설계 스튜디오는 데스크톱에서 편집"
|
||||
variant="ghost"
|
||||
onPress={() => router.push('/(tabs)')}
|
||||
/>
|
||||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
flex: { flex: 1, backgroundColor: colors.neutral050 },
|
||||
infoBar: { backgroundColor: colors.primary700, paddingHorizontal: spacing.md, paddingVertical: 8 },
|
||||
infoBarText: { color: colors.white, fontSize: type.caption.fontSize, fontWeight: '500' },
|
||||
scroll: { padding: spacing.md, gap: spacing.md },
|
||||
grid: { flexDirection: 'row', flexWrap: 'wrap', gap: spacing.sm },
|
||||
cell: { flexGrow: 1, flexBasis: '46%', gap: 6 },
|
||||
retry: { minHeight: 40 },
|
||||
});
|
||||
193
mobile/app/(tabs)/index.tsx
Normal file
193
mobile/app/(tabs)/index.tsx
Normal file
@ -0,0 +1,193 @@
|
||||
/*
|
||||
* 홈/대시보드 — 활성 행사 헤더(D-데이) + 역할별 진입 카드.
|
||||
* 워크스페이스(행사)는 로그인 응답에서 수신. 역할에 따라 진입 동선 분기.
|
||||
*/
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { router } from 'expo-router';
|
||||
import React from 'react';
|
||||
import { Pressable, ScrollView, StyleSheet, Text, View } from 'react-native';
|
||||
import { Banner } from '../../components/Banner';
|
||||
import { Card } from '../../components/Card';
|
||||
import { DdayChip } from '../../components/DdayChip';
|
||||
import { useAuth } from '../../context/AuthContext';
|
||||
import type { EventRole, WorkspaceDto } from '../../lib/types';
|
||||
import { colors, radius, spacing, type } from '../../theme';
|
||||
|
||||
const roleLabel: Record<EventRole, string> = {
|
||||
ORGANIZER: '주최자',
|
||||
EXHIBITOR: '참가업체',
|
||||
CONTRACTOR: '장치·시공업체',
|
||||
HALL_MANAGER: '홀매니저',
|
||||
};
|
||||
|
||||
export default function HomeScreen() {
|
||||
const { user, workspaces, activeWorkspace, selectWorkspace } = useAuth();
|
||||
|
||||
return (
|
||||
<ScrollView contentContainerStyle={styles.scroll}>
|
||||
<Text style={styles.hello}>{user?.displayName ?? '사용자'}님, 안녕하세요</Text>
|
||||
|
||||
{workspaces.length === 0 ? (
|
||||
<Banner tone="info">
|
||||
참여 중인 행사가 없습니다 — 초대 링크 또는 초대 코드를 확인해 주세요.
|
||||
</Banner>
|
||||
) : null}
|
||||
|
||||
{/* 행사 선택 */}
|
||||
{workspaces.length > 1 ? (
|
||||
<View style={styles.eventPicker}>
|
||||
{workspaces.map((w) => (
|
||||
<Pressable
|
||||
key={w.eventId}
|
||||
onPress={() => selectWorkspace(w.eventId)}
|
||||
style={[
|
||||
styles.eventChip,
|
||||
activeWorkspace?.eventId === w.eventId && styles.eventChipOn,
|
||||
]}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.eventChipText,
|
||||
activeWorkspace?.eventId === w.eventId && styles.eventChipTextOn,
|
||||
]}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{w.eventName}
|
||||
</Text>
|
||||
</Pressable>
|
||||
))}
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
{activeWorkspace ? <EventHeader ws={activeWorkspace} /> : null}
|
||||
|
||||
{/* 진행 스테퍼 */}
|
||||
<Card>
|
||||
<Text style={styles.cardTitle}>진행 상태</Text>
|
||||
<Stepper steps={['신청', '승인', '시공', '검수']} active={1} />
|
||||
</Card>
|
||||
|
||||
{/* 역할별 진입 */}
|
||||
{activeWorkspace ? <RoleActions role={activeWorkspace.myRole} /> : null}
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
|
||||
function EventHeader({ ws }: { ws: WorkspaceDto }) {
|
||||
return (
|
||||
<Card accent="none">
|
||||
<View style={styles.eventHeaderTop}>
|
||||
<Text style={styles.eventName}>{ws.eventName}</Text>
|
||||
<DdayChip dday={ws.dday} />
|
||||
</View>
|
||||
<Text style={styles.eventMeta}>
|
||||
{ws.hallLabel} · {ws.startDate} ~ {ws.endDate}
|
||||
</Text>
|
||||
<View style={styles.roleBadge}>
|
||||
<Text style={styles.roleBadgeText}>{roleLabel[ws.myRole]}</Text>
|
||||
</View>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function RoleActions({ role }: { role: EventRole }) {
|
||||
const actions: { label: string; icon: keyof typeof Ionicons.glyphMap; onPress: () => void }[] = [];
|
||||
|
||||
if (role === 'CONTRACTOR') {
|
||||
actions.push({ label: '현장 체크리스트', icon: 'clipboard-outline', onPress: () => router.push('/checklist') });
|
||||
}
|
||||
if (role === 'HALL_MANAGER') {
|
||||
actions.push({ label: '현장 검수', icon: 'shield-checkmark-outline', onPress: () => router.push('/inspection') });
|
||||
}
|
||||
actions.push({ label: '시각화 갤러리', icon: 'images-outline', onPress: () => router.push('/(tabs)/gallery') });
|
||||
|
||||
return (
|
||||
<View style={styles.actions}>
|
||||
{actions.map((a) => (
|
||||
<Pressable key={a.label} style={styles.actionCard} onPress={a.onPress}>
|
||||
<Ionicons name={a.icon} size={26} color={colors.primary600} />
|
||||
<Text style={styles.actionLabel}>{a.label}</Text>
|
||||
</Pressable>
|
||||
))}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
function Stepper({ steps, active }: { steps: string[]; active: number }) {
|
||||
return (
|
||||
<View style={styles.stepper}>
|
||||
{steps.map((s, i) => (
|
||||
<View key={s} style={styles.step}>
|
||||
<View
|
||||
style={[
|
||||
styles.stepDot,
|
||||
i < active && { backgroundColor: colors.success },
|
||||
i === active && { backgroundColor: colors.primary600 },
|
||||
]}
|
||||
>
|
||||
<Text style={styles.stepDotText}>{i + 1}</Text>
|
||||
</View>
|
||||
<Text style={[styles.stepLabel, i === active && { color: colors.primary700, fontWeight: '700' }]}>
|
||||
{s}
|
||||
</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
scroll: { padding: spacing.md, gap: spacing.md },
|
||||
hello: { fontSize: type.h2.fontSize, fontWeight: '700', color: colors.neutral900 },
|
||||
eventPicker: { flexDirection: 'row', flexWrap: 'wrap', gap: 8 },
|
||||
eventChip: {
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 8,
|
||||
borderRadius: radius.pill,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.neutral200,
|
||||
maxWidth: 220,
|
||||
},
|
||||
eventChipOn: { backgroundColor: colors.primary100, borderColor: colors.primary600 },
|
||||
eventChipText: { color: colors.neutral700, fontSize: type.caption.fontSize },
|
||||
eventChipTextOn: { color: colors.primary700, fontWeight: '700' },
|
||||
eventHeaderTop: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', gap: 8 },
|
||||
eventName: { flex: 1, fontSize: type.h3.fontSize, fontWeight: '700', color: colors.neutral900 },
|
||||
eventMeta: { marginTop: 4, color: colors.neutral500, fontSize: type.caption.fontSize },
|
||||
roleBadge: {
|
||||
alignSelf: 'flex-start',
|
||||
marginTop: 10,
|
||||
backgroundColor: colors.primary050,
|
||||
borderRadius: radius.pill,
|
||||
paddingHorizontal: 10,
|
||||
paddingVertical: 3,
|
||||
},
|
||||
roleBadgeText: { color: colors.primary700, fontSize: type.caption.fontSize, fontWeight: '600' },
|
||||
cardTitle: { fontSize: type.h3.fontSize, fontWeight: '600', color: colors.neutral900, marginBottom: 12 },
|
||||
actions: { flexDirection: 'row', flexWrap: 'wrap', gap: spacing.md },
|
||||
actionCard: {
|
||||
flexGrow: 1,
|
||||
flexBasis: '45%',
|
||||
backgroundColor: colors.white,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.neutral200,
|
||||
borderRadius: radius.md,
|
||||
padding: spacing.md,
|
||||
gap: 8,
|
||||
minHeight: 96,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
actionLabel: { fontSize: type.body.fontSize, fontWeight: '600', color: colors.neutral700 },
|
||||
stepper: { flexDirection: 'row', justifyContent: 'space-between' },
|
||||
step: { alignItems: 'center', flex: 1, gap: 6 },
|
||||
stepDot: {
|
||||
width: 28,
|
||||
height: 28,
|
||||
borderRadius: 14,
|
||||
backgroundColor: colors.neutral200,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
stepDotText: { color: colors.white, fontWeight: '700', fontSize: type.caption.fontSize },
|
||||
stepLabel: { fontSize: type.caption.fontSize, color: colors.neutral500 },
|
||||
});
|
||||
97
mobile/app/(tabs)/more.tsx
Normal file
97
mobile/app/(tabs)/more.tsx
Normal file
@ -0,0 +1,97 @@
|
||||
/*
|
||||
* 더보기 — 계정 정보·API 상태(health)·로그아웃.
|
||||
*/
|
||||
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 { Card } from '../../components/Card';
|
||||
import { useAuth } from '../../context/AuthContext';
|
||||
import { api, isDegraded } from '../../lib/api';
|
||||
import { API_BASE } from '../../lib/config';
|
||||
import type { HealthDto } from '../../lib/types';
|
||||
import { colors, radius, spacing, touch, type } from '../../theme';
|
||||
|
||||
export default function MoreScreen() {
|
||||
const { user, signOut } = useAuth();
|
||||
const [health, setHealth] = useState<string>('확인 중…');
|
||||
|
||||
const checkHealth = useCallback(async () => {
|
||||
try {
|
||||
const res = await api.get<HealthDto>('/health', { anonymous: true });
|
||||
setHealth(res?.status === 'UP' ? '정상 (UP)' : (res?.status ?? '알 수 없음'));
|
||||
} catch (e) {
|
||||
setHealth(isDegraded(e) ? '연결 불가 (degraded)' : '오류');
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
checkHealth();
|
||||
}, [checkHealth]);
|
||||
|
||||
async function onSignOut() {
|
||||
await signOut();
|
||||
router.replace('/login');
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrollView contentContainerStyle={styles.scroll}>
|
||||
<Card>
|
||||
<Text style={styles.name}>{user?.displayName ?? '사용자'}</Text>
|
||||
<Text style={styles.meta}>
|
||||
{user?.hallManager ? '킨텍스 내부 계정 (홀매니저)' : '일반 계정'}
|
||||
</Text>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<Row label="API 서버" value={API_BASE} />
|
||||
<View style={styles.divider} />
|
||||
<Pressable style={styles.healthRow} onPress={checkHealth}>
|
||||
<Row label="서버 상태" value={health} />
|
||||
<Ionicons name="refresh" size={18} color={colors.primary600} />
|
||||
</Pressable>
|
||||
</Card>
|
||||
|
||||
<Pressable style={styles.signOut} onPress={onSignOut}>
|
||||
<Ionicons name="log-out-outline" size={20} color={colors.error} />
|
||||
<Text style={styles.signOutText}>로그아웃</Text>
|
||||
</Pressable>
|
||||
|
||||
<Text style={styles.version}>KINTEX AI 전시관리 · v0.1.0</Text>
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
|
||||
function Row({ label, value }: { label: string; value: string }) {
|
||||
return (
|
||||
<View style={styles.row}>
|
||||
<Text style={styles.rowLabel}>{label}</Text>
|
||||
<Text style={styles.rowValue} numberOfLines={1}>
|
||||
{value}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
scroll: { padding: spacing.md, gap: spacing.md },
|
||||
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 },
|
||||
rowLabel: { color: colors.neutral500, fontSize: type.body.fontSize },
|
||||
rowValue: { flex: 1, textAlign: 'right', color: colors.neutral900, fontSize: type.body.fontSize },
|
||||
divider: { height: 1, backgroundColor: colors.neutral200, marginVertical: 6 },
|
||||
healthRow: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' },
|
||||
signOut: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 8,
|
||||
minHeight: touch.min,
|
||||
borderRadius: radius.sm,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.error,
|
||||
},
|
||||
signOutText: { color: colors.error, fontSize: type.h3.fontSize, fontWeight: '600' },
|
||||
version: { textAlign: 'center', color: colors.neutral500, fontSize: type.caption.fontSize },
|
||||
});
|
||||
39
mobile/app/_layout.tsx
Normal file
39
mobile/app/_layout.tsx
Normal file
@ -0,0 +1,39 @@
|
||||
/*
|
||||
* 루트 레이아웃 — AuthProvider + 네이티브 스택 내비.
|
||||
* 모바일은 MDI 미적용(단일 화면 포커스, design.md §2-2).
|
||||
*/
|
||||
import { Stack } from 'expo-router';
|
||||
import { StatusBar } from 'expo-status-bar';
|
||||
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 { colors } from '../theme';
|
||||
|
||||
export default function RootLayout() {
|
||||
return (
|
||||
<GestureHandlerRootView style={{ flex: 1 }}>
|
||||
<SafeAreaProvider>
|
||||
<AuthProvider>
|
||||
<StatusBar style="dark" />
|
||||
<Stack
|
||||
screenOptions={{
|
||||
headerStyle: { backgroundColor: colors.white },
|
||||
headerTintColor: colors.neutral900,
|
||||
headerTitleStyle: { fontWeight: '600' },
|
||||
contentStyle: { backgroundColor: colors.neutral050 },
|
||||
}}
|
||||
>
|
||||
<Stack.Screen name="index" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="login" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="register" options={{ title: '회원가입' }} />
|
||||
<Stack.Screen name="forgot-password" options={{ title: '비밀번호 찾기' }} />
|
||||
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="checklist" options={{ title: '현장 체크리스트' }} />
|
||||
<Stack.Screen name="inspection" options={{ title: '현장 검수' }} />
|
||||
</Stack>
|
||||
</AuthProvider>
|
||||
</SafeAreaProvider>
|
||||
</GestureHandlerRootView>
|
||||
);
|
||||
}
|
||||
344
mobile/app/checklist.tsx
Normal file
344
mobile/app/checklist.tsx
Normal file
@ -0,0 +1,344 @@
|
||||
/*
|
||||
* SCR-M1 [모바일] 시공업체 현장 체크리스트 — 48px 터치·오프라인 배지·사진 첨부.
|
||||
* 아코디언 그룹(반입 전 / 시공 중 / 구조 확인 / 완료) + 금지작업 경고 + 검수 요청 CTA.
|
||||
* 위반(높이 5m 초과) 즉시 빨간 안내. 오프라인 시 로컬 저장 안내(배지).
|
||||
*/
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import * as ImagePicker from 'expo-image-picker';
|
||||
import { router } from 'expo-router';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { Alert, Image, Pressable, ScrollView, StyleSheet, Text, TextInput, View } from 'react-native';
|
||||
import { Banner } from '../components/Banner';
|
||||
import { Button } from '../components/Button';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
import { colors, radius, spacing, touch, type } from '../theme';
|
||||
|
||||
interface Item {
|
||||
id: string;
|
||||
label: string;
|
||||
hint?: string;
|
||||
required: boolean;
|
||||
photo: boolean; // 사진 첨부 가능
|
||||
}
|
||||
interface Group {
|
||||
id: string;
|
||||
title: string;
|
||||
items: Item[];
|
||||
}
|
||||
|
||||
const GROUPS: Group[] = [
|
||||
{
|
||||
id: 'before',
|
||||
title: '반입 전',
|
||||
items: [
|
||||
{ id: 'flame', label: '방염 확인서 지참', required: true, photo: true },
|
||||
{ id: 'helmet', label: '안전모·보호장구 착용', required: true, photo: false },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'during',
|
||||
title: '시공 중',
|
||||
items: [
|
||||
{ id: 'floor', label: '바닥 마감·단차 확인', hint: '테이핑 및 단차', required: true, photo: true },
|
||||
{ id: 'wire', label: '전기 배선 안전 확인', hint: '노출 전선 정리', required: false, photo: true },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'done',
|
||||
title: '완료',
|
||||
items: [{ id: 'final', label: '완료 사진 업로드', required: true, photo: true }],
|
||||
},
|
||||
];
|
||||
|
||||
export default function ChecklistScreen() {
|
||||
const { activeWorkspace } = useAuth();
|
||||
const [checked, setChecked] = useState<Record<string, boolean>>({});
|
||||
const [photos, setPhotos] = useState<Record<string, string>>({});
|
||||
const [open, setOpen] = useState<Record<string, boolean>>({ before: true, during: true, done: true });
|
||||
const [height, setHeight] = useState('');
|
||||
|
||||
const heightValue = parseFloat(height);
|
||||
const heightViolation = !Number.isNaN(heightValue) && heightValue > 5;
|
||||
|
||||
const requiredIds = useMemo(
|
||||
() => GROUPS.flatMap((g) => g.items.filter((i) => i.required).map((i) => i.id)),
|
||||
[],
|
||||
);
|
||||
const remaining = requiredIds.filter((id) => !checked[id]).length;
|
||||
const heightOk = !Number.isNaN(heightValue) && heightValue > 0 && heightValue <= 5;
|
||||
const canSubmit = remaining === 0 && heightOk;
|
||||
|
||||
async function attachPhoto(itemId: string) {
|
||||
try {
|
||||
const perm = await ImagePicker.requestMediaLibraryPermissionsAsync();
|
||||
if (!perm.granted) {
|
||||
Alert.alert('권한 필요', '사진 첨부를 위해 갤러리 접근 권한이 필요합니다.');
|
||||
return;
|
||||
}
|
||||
const res = await ImagePicker.launchImageLibraryAsync({
|
||||
mediaTypes: ImagePicker.MediaTypeOptions.Images,
|
||||
quality: 0.6,
|
||||
});
|
||||
if (!res.canceled && res.assets[0]) {
|
||||
setPhotos((p) => ({ ...p, [itemId]: res.assets[0].uri }));
|
||||
}
|
||||
} catch {
|
||||
Alert.alert('오류', '사진을 첨부하지 못했습니다.');
|
||||
}
|
||||
}
|
||||
|
||||
function submit() {
|
||||
// 스캐폴드: 실제 검수 요청 API 연동은 백엔드 M6/C-4 확장 시 배선.
|
||||
Alert.alert('검수 요청', '검수 요청이 대기열에 저장되었습니다. (오프라인 시 연결되면 동기화)');
|
||||
router.back();
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.flex}>
|
||||
{/* 상단 컨텍스트 */}
|
||||
<View style={styles.header}>
|
||||
<View style={styles.headerTop}>
|
||||
<View>
|
||||
<Text style={styles.boothNo}>부스 A-102</Text>
|
||||
<Text style={styles.boothMeta}>
|
||||
{activeWorkspace?.hallLabel ?? '홀7'} · 장치 1일차
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.offlineBadge}>
|
||||
<Ionicons name="cloud-offline-outline" size={14} color={colors.neutral500} />
|
||||
<Text style={styles.offlineText}>오프라인 저장</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View style={styles.toggleRow}>
|
||||
<View style={[styles.toggleBtn, styles.toggleActive]}>
|
||||
<Ionicons name="git-network-outline" size={16} color={colors.primary600} />
|
||||
<Text style={styles.toggleActiveText}>도면</Text>
|
||||
</View>
|
||||
<View style={styles.toggleBtn}>
|
||||
<Ionicons name="image-outline" size={16} color={colors.neutral500} />
|
||||
<Text style={styles.toggleText}>예상 사진 (참고용)</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<ScrollView contentContainerStyle={styles.scroll}>
|
||||
<Banner tone="info">필수 항목 {remaining}개 남음</Banner>
|
||||
|
||||
{/* 금지작업 경고 (상시) */}
|
||||
<View style={styles.warnCard}>
|
||||
<View style={styles.warnHead}>
|
||||
<Ionicons name="warning" size={18} color={colors.error} />
|
||||
<Text style={styles.warnTitle}>장내 금지 작업</Text>
|
||||
</View>
|
||||
<Text style={styles.warnBody}>• 금지: 전기톱 · 용접 · 페인트 작업</Text>
|
||||
<Text style={styles.warnBody}>• 소음 한도: 70~75dB 준수</Text>
|
||||
</View>
|
||||
|
||||
{GROUPS.map((g) => (
|
||||
<View key={g.id} style={styles.group}>
|
||||
<Pressable style={styles.groupHead} onPress={() => setOpen((o) => ({ ...o, [g.id]: !o[g.id] }))}>
|
||||
<Text style={styles.groupTitle}>{g.title}</Text>
|
||||
<Ionicons name={open[g.id] ? 'chevron-up' : 'chevron-down'} size={18} color={colors.neutral500} />
|
||||
</Pressable>
|
||||
{open[g.id]
|
||||
? g.items.map((it) => (
|
||||
<View key={it.id} style={styles.item}>
|
||||
<Pressable
|
||||
accessibilityRole="checkbox"
|
||||
accessibilityState={{ checked: !!checked[it.id] }}
|
||||
onPress={() => setChecked((c) => ({ ...c, [it.id]: !c[it.id] }))}
|
||||
style={[styles.checkbox, checked[it.id] && styles.checkboxOn]}
|
||||
>
|
||||
{checked[it.id] ? <Ionicons name="checkmark" size={22} color={colors.white} /> : null}
|
||||
</Pressable>
|
||||
<View style={styles.itemBody}>
|
||||
<Text style={[styles.itemLabel, checked[it.id] && styles.itemLabelDone]}>
|
||||
{it.label}
|
||||
{it.required ? <Text style={styles.req}> *</Text> : null}
|
||||
</Text>
|
||||
{it.hint ? <Text style={styles.itemHint}>{it.hint}</Text> : null}
|
||||
{photos[it.id] ? (
|
||||
<Image source={{ uri: photos[it.id] }} style={styles.thumb} />
|
||||
) : null}
|
||||
</View>
|
||||
{it.photo ? (
|
||||
<Pressable style={styles.camera} onPress={() => attachPhoto(it.id)}>
|
||||
<Ionicons name="camera-outline" size={22} color={colors.neutral700} />
|
||||
</Pressable>
|
||||
) : null}
|
||||
</View>
|
||||
))
|
||||
: null}
|
||||
</View>
|
||||
))}
|
||||
|
||||
{/* 구조 확인 — 높이 실측 */}
|
||||
<View style={styles.group}>
|
||||
<View style={styles.groupHead}>
|
||||
<Text style={styles.groupTitle}>구조 확인</Text>
|
||||
</View>
|
||||
<View style={styles.heightRow}>
|
||||
<Text style={styles.itemLabel}>장치물 최고 높이 실측</Text>
|
||||
<View style={styles.heightInputWrap}>
|
||||
<TextInput
|
||||
value={height}
|
||||
onChangeText={setHeight}
|
||||
placeholder="4.2"
|
||||
placeholderTextColor={colors.neutral500}
|
||||
keyboardType="decimal-pad"
|
||||
style={styles.heightInput}
|
||||
/>
|
||||
<Text style={styles.heightUnit}>m</Text>
|
||||
</View>
|
||||
</View>
|
||||
<Text style={styles.itemHint}>규정: 5m 이하</Text>
|
||||
{heightViolation ? (
|
||||
<Banner tone="error">높이 5m 초과 — 위반 시 홀매니저에게 자동 통지됩니다.</Banner>
|
||||
) : null}
|
||||
</View>
|
||||
</ScrollView>
|
||||
|
||||
{/* 하단 고정 CTA */}
|
||||
<View style={styles.footer}>
|
||||
<Text style={styles.syncNote}>오프라인 — 연결 시 동기화</Text>
|
||||
<Button label="검수 요청 보내기" onPress={submit} disabled={!canSubmit} />
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
flex: { flex: 1, backgroundColor: colors.neutral050 },
|
||||
header: {
|
||||
backgroundColor: colors.white,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: colors.neutral200,
|
||||
padding: spacing.md,
|
||||
gap: 12,
|
||||
},
|
||||
headerTop: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' },
|
||||
boothNo: { fontSize: type.h3.fontSize, fontWeight: '700', color: colors.neutral900 },
|
||||
boothMeta: { fontSize: type.caption.fontSize, color: colors.neutral500, marginTop: 2 },
|
||||
offlineBadge: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
backgroundColor: colors.neutral050,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.neutral200,
|
||||
borderRadius: radius.pill,
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 4,
|
||||
},
|
||||
offlineText: { fontSize: 11, color: colors.neutral500 },
|
||||
toggleRow: { flexDirection: 'row', gap: 8 },
|
||||
toggleBtn: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 4,
|
||||
paddingVertical: 8,
|
||||
borderRadius: radius.sm,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.neutral200,
|
||||
},
|
||||
toggleActive: { backgroundColor: colors.primary050, borderColor: colors.primary600 },
|
||||
toggleActiveText: { color: colors.primary600, fontWeight: '600', fontSize: type.caption.fontSize },
|
||||
toggleText: { color: colors.neutral500, fontSize: type.caption.fontSize },
|
||||
scroll: { padding: spacing.md, gap: spacing.md, paddingBottom: 120 },
|
||||
warnCard: {
|
||||
backgroundColor: '#FEF3F2',
|
||||
borderWidth: 1,
|
||||
borderColor: colors.error,
|
||||
borderRadius: radius.md,
|
||||
padding: spacing.md,
|
||||
gap: 4,
|
||||
},
|
||||
warnHead: { flexDirection: 'row', alignItems: 'center', gap: 6, marginBottom: 2 },
|
||||
warnTitle: { color: colors.error, fontWeight: '700', fontSize: type.body.fontSize },
|
||||
warnBody: { color: colors.neutral700, fontSize: type.caption.fontSize },
|
||||
group: {
|
||||
backgroundColor: colors.white,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.neutral200,
|
||||
borderRadius: radius.md,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
groupHead: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
padding: spacing.md,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: colors.neutral200,
|
||||
},
|
||||
groupTitle: { fontSize: type.h3.fontSize, fontWeight: '600', color: colors.neutral900 },
|
||||
item: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 12,
|
||||
padding: spacing.md,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: colors.neutral050,
|
||||
},
|
||||
checkbox: {
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: radius.sm,
|
||||
borderWidth: 2,
|
||||
borderColor: colors.neutral200,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
checkboxOn: { backgroundColor: colors.primary600, borderColor: colors.primary600 },
|
||||
itemBody: { flex: 1, gap: 4 },
|
||||
itemLabel: { fontSize: type.body.fontSize, color: colors.neutral900 },
|
||||
itemLabelDone: { textDecorationLine: 'line-through', color: colors.neutral500 },
|
||||
itemHint: { fontSize: type.caption.fontSize, color: colors.neutral500 },
|
||||
req: { color: colors.error },
|
||||
thumb: { width: 72, height: 72, borderRadius: radius.sm, marginTop: 4 },
|
||||
camera: {
|
||||
width: touch.min,
|
||||
height: touch.min,
|
||||
borderRadius: radius.sm,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.neutral200,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
heightRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
paddingHorizontal: spacing.md,
|
||||
paddingTop: spacing.md,
|
||||
gap: 12,
|
||||
},
|
||||
heightInputWrap: { flexDirection: 'row', alignItems: 'center', gap: 6 },
|
||||
heightInput: {
|
||||
width: 90,
|
||||
minHeight: touch.min,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.neutral200,
|
||||
borderRadius: radius.sm,
|
||||
paddingHorizontal: 12,
|
||||
fontSize: type.h3.fontSize,
|
||||
color: colors.neutral900,
|
||||
textAlign: 'right',
|
||||
},
|
||||
heightUnit: { fontSize: type.h3.fontSize, color: colors.neutral500 },
|
||||
footer: {
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
backgroundColor: colors.white,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: colors.neutral200,
|
||||
padding: spacing.md,
|
||||
paddingBottom: spacing.lg,
|
||||
gap: 6,
|
||||
},
|
||||
syncNote: { textAlign: 'center', fontSize: 11, color: colors.neutral500 },
|
||||
});
|
||||
116
mobile/app/forgot-password.tsx
Normal file
116
mobile/app/forgot-password.tsx
Normal file
@ -0,0 +1,116 @@
|
||||
/*
|
||||
* 비밀번호 찾기/초기화 — 04_backend_public_auth.md §2·§3. 공개 경로.
|
||||
* 1단계: 이메일로 재설정 코드 요청(항상 200, 사용자 열거 방지).
|
||||
* 2단계: 이메일+코드+새 비밀번호로 초기화.
|
||||
*/
|
||||
import { router } from 'expo-router';
|
||||
import React, { useState } from 'react';
|
||||
import { KeyboardAvoidingView, Platform, ScrollView, StyleSheet, Text } from 'react-native';
|
||||
import { Banner } from '../components/Banner';
|
||||
import { Button } from '../components/Button';
|
||||
import { Field } from '../components/Field';
|
||||
import { ApiRequestError } from '../lib/api';
|
||||
import { forgotPassword, resetPassword } from '../lib/auth';
|
||||
import { colors, spacing, type } from '../theme';
|
||||
|
||||
export default function ForgotPasswordScreen() {
|
||||
const [step, setStep] = useState<1 | 2>(1);
|
||||
const [email, setEmail] = useState('');
|
||||
const [code, setCode] = useState('');
|
||||
const [newPassword, setNewPassword] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [info, setInfo] = useState<string | null>(null);
|
||||
|
||||
async function onRequest() {
|
||||
setError(null);
|
||||
setInfo(null);
|
||||
if (!email.trim()) {
|
||||
setError('이메일을 입력해 주세요.');
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await forgotPassword(email);
|
||||
setInfo(res.message);
|
||||
setStep(2);
|
||||
} catch (e) {
|
||||
setError(e instanceof ApiRequestError ? e.message : '요청에 실패했습니다.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function onReset() {
|
||||
setError(null);
|
||||
if (!code.trim() || newPassword.length < 8) {
|
||||
setError('재설정 코드와 새 비밀번호(8자 이상)를 확인해 주세요.');
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await resetPassword(email, code, newPassword);
|
||||
setInfo(res.message);
|
||||
setTimeout(() => router.replace('/login'), 800);
|
||||
} catch (e) {
|
||||
if (e instanceof ApiRequestError && e.code === 'OTP_INVALID') {
|
||||
setError('재설정 코드가 올바르지 않거나 만료되었습니다.');
|
||||
} else {
|
||||
setError(e instanceof ApiRequestError ? e.message : '초기화에 실패했습니다.');
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<KeyboardAvoidingView
|
||||
style={styles.flex}
|
||||
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
||||
>
|
||||
<ScrollView contentContainerStyle={styles.scroll} keyboardShouldPersistTaps="handled">
|
||||
<Text style={styles.title}>비밀번호 재설정</Text>
|
||||
{error ? <Banner tone="error">{error}</Banner> : null}
|
||||
{info ? <Banner tone="info">{info}</Banner> : null}
|
||||
|
||||
<Field
|
||||
label="이메일"
|
||||
value={email}
|
||||
onChangeText={setEmail}
|
||||
placeholder="name@company.co.kr"
|
||||
keyboardType="email-address"
|
||||
editable={step === 1}
|
||||
/>
|
||||
|
||||
{step === 1 ? (
|
||||
<Button label="재설정 코드 요청" onPress={onRequest} loading={loading} />
|
||||
) : (
|
||||
<>
|
||||
<Field
|
||||
label="재설정 코드"
|
||||
value={code}
|
||||
onChangeText={setCode}
|
||||
placeholder="6자리 코드"
|
||||
keyboardType="number-pad"
|
||||
/>
|
||||
<Field
|
||||
label="새 비밀번호"
|
||||
value={newPassword}
|
||||
onChangeText={setNewPassword}
|
||||
placeholder="8자 이상"
|
||||
secureTextEntry
|
||||
helperText="8~100자"
|
||||
/>
|
||||
<Button label="비밀번호 변경" onPress={onReset} loading={loading} />
|
||||
</>
|
||||
)}
|
||||
</ScrollView>
|
||||
</KeyboardAvoidingView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
flex: { flex: 1, backgroundColor: colors.neutral050 },
|
||||
scroll: { padding: spacing.md, gap: spacing.md, paddingBottom: spacing.xl },
|
||||
title: { fontSize: type.h2.fontSize, fontWeight: '700', color: colors.neutral900 },
|
||||
});
|
||||
31
mobile/app/index.tsx
Normal file
31
mobile/app/index.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
/*
|
||||
* 진입점 — 토큰 복원 대기 후 인증 상태에 따라 로그인/탭 홈으로 분기.
|
||||
*/
|
||||
import { Redirect } from 'expo-router';
|
||||
import React from 'react';
|
||||
import { ActivityIndicator, StyleSheet, View } from 'react-native';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
import { colors } from '../theme';
|
||||
|
||||
export default function Index() {
|
||||
const { ready, token } = useAuth();
|
||||
|
||||
if (!ready) {
|
||||
return (
|
||||
<View style={styles.center}>
|
||||
<ActivityIndicator color={colors.primary600} size="large" />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return <Redirect href={token ? '/(tabs)' : '/login'} />;
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
center: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: colors.neutral050,
|
||||
},
|
||||
});
|
||||
269
mobile/app/inspection.tsx
Normal file
269
mobile/app/inspection.tsx
Normal file
@ -0,0 +1,269 @@
|
||||
/*
|
||||
* SCR-M2 [모바일] 홀매니저 현장 검수 — 승인 도면 vs 현장 사진 비교 + AI 사전심사 플래그.
|
||||
* 검수 항목 [적합]/[부적합] 세그먼트 · 부적합 시 사진+메모 · 승인/시정 요청.
|
||||
* 모든 AI 플래그 "현장 확인함" 체크 전 승인 비활성(오조작 방지).
|
||||
*/
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { router } from 'expo-router';
|
||||
import React, { useState } from 'react';
|
||||
import { Alert, ScrollView, StyleSheet, Text, TextInput, View } from 'react-native';
|
||||
import { AiImage } from '../components/AiImage';
|
||||
import { Button } from '../components/Button';
|
||||
import { Card } from '../components/Card';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
import { colors, radius, spacing, touch, type } from '../theme';
|
||||
|
||||
interface Flag {
|
||||
id: string;
|
||||
label: string;
|
||||
severity: 'block' | 'warn';
|
||||
}
|
||||
const FLAGS: Flag[] = [
|
||||
{ id: 'f1', label: '장치물 높이 5.4m 초과', severity: 'block' },
|
||||
{ id: 'f2', label: '방염 확인서 누락', severity: 'warn' },
|
||||
{ id: 'f3', label: '리깅 구조계산서 미제출', severity: 'warn' },
|
||||
];
|
||||
|
||||
interface InspItem {
|
||||
id: string;
|
||||
label: string;
|
||||
}
|
||||
const ITEMS: InspItem[] = [
|
||||
{ id: 'height', label: '장치물 높이 (5m 이하)' },
|
||||
{ id: 'flame', label: '방염 라벨 부착' },
|
||||
{ id: 'aisle', label: '통로 침범 여부' },
|
||||
{ id: 'noise', label: '소음 (70~75dB)' },
|
||||
];
|
||||
|
||||
type Verdict = 'ok' | 'fail' | null;
|
||||
|
||||
export default function InspectionScreen() {
|
||||
const { activeWorkspace } = useAuth();
|
||||
const [confirmed, setConfirmed] = useState<Record<string, boolean>>({});
|
||||
const [verdict, setVerdict] = useState<Record<string, Verdict>>({});
|
||||
const [memo, setMemo] = useState<Record<string, string>>({});
|
||||
const [showFlags, setShowFlags] = useState(true);
|
||||
|
||||
const allConfirmed = FLAGS.every((f) => confirmed[f.id]);
|
||||
const anyFail = ITEMS.some((it) => verdict[it.id] === 'fail');
|
||||
const canApprove = allConfirmed && !anyFail;
|
||||
|
||||
function approve() {
|
||||
Alert.alert('검수 승인', '검수가 승인되었습니다.');
|
||||
router.back();
|
||||
}
|
||||
function requestFix() {
|
||||
Alert.alert('시정 요청', '위반 항목이 첨부되어 시공업체에 전달되었습니다.');
|
||||
router.back();
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.flex}>
|
||||
<ScrollView contentContainerStyle={styles.scroll}>
|
||||
<Text style={styles.title}>
|
||||
현장 검수 · 부스 A-102 · {activeWorkspace?.hallLabel ?? '홀7'}
|
||||
</Text>
|
||||
|
||||
{/* 비교 카드 */}
|
||||
<Card>
|
||||
<View style={styles.compareRow}>
|
||||
<View style={styles.compareCol}>
|
||||
<Text style={styles.compareLabel}>승인 도면</Text>
|
||||
<View style={styles.drawingThumb}>
|
||||
<Ionicons name="git-network-outline" size={28} color={colors.primary100} />
|
||||
<Text style={styles.drawingText}>도면 v2</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View style={styles.compareCol}>
|
||||
<Text style={styles.compareLabel}>현장 사진 (AI 참고)</Text>
|
||||
<AiImage uri={null} notice="참고용 — 심사 근거 아님" height={96} />
|
||||
</View>
|
||||
</View>
|
||||
</Card>
|
||||
|
||||
{/* AI 사전심사 플래그 */}
|
||||
<Card accent="ai">
|
||||
<View style={styles.flagHead}>
|
||||
<Text style={styles.flagTitle}>AI 사전심사 플래그 {FLAGS.length}건</Text>
|
||||
<Ionicons
|
||||
name={showFlags ? 'chevron-up' : 'chevron-down'}
|
||||
size={18}
|
||||
color={colors.aiAccent}
|
||||
onPress={() => setShowFlags((v) => !v)}
|
||||
/>
|
||||
</View>
|
||||
{showFlags
|
||||
? FLAGS.map((f) => (
|
||||
<View key={f.id} style={styles.flagRow}>
|
||||
<View
|
||||
style={[
|
||||
styles.sevDot,
|
||||
{ backgroundColor: f.severity === 'block' ? colors.error : colors.warning },
|
||||
]}
|
||||
/>
|
||||
<Text style={styles.flagLabel}>{f.label}</Text>
|
||||
<View
|
||||
style={[styles.confirmChip, confirmed[f.id] && styles.confirmChipOn]}
|
||||
onTouchEnd={() => setConfirmed((c) => ({ ...c, [f.id]: !c[f.id] }))}
|
||||
>
|
||||
<Text style={[styles.confirmText, confirmed[f.id] && styles.confirmTextOn]}>
|
||||
{confirmed[f.id] ? '확인함' : '확인'}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
))
|
||||
: null}
|
||||
</Card>
|
||||
|
||||
{/* 검수 항목 */}
|
||||
{ITEMS.map((it) => (
|
||||
<Card key={it.id}>
|
||||
<Text style={styles.itemLabel}>{it.label}</Text>
|
||||
<View style={styles.segment}>
|
||||
<SegBtn
|
||||
label="적합"
|
||||
active={verdict[it.id] === 'ok'}
|
||||
tone="ok"
|
||||
onPress={() => setVerdict((v) => ({ ...v, [it.id]: 'ok' }))}
|
||||
/>
|
||||
<SegBtn
|
||||
label="부적합"
|
||||
active={verdict[it.id] === 'fail'}
|
||||
tone="fail"
|
||||
onPress={() => setVerdict((v) => ({ ...v, [it.id]: 'fail' }))}
|
||||
/>
|
||||
</View>
|
||||
{verdict[it.id] === 'fail' ? (
|
||||
<TextInput
|
||||
value={memo[it.id] ?? ''}
|
||||
onChangeText={(t) => setMemo((m) => ({ ...m, [it.id]: t }))}
|
||||
placeholder="부적합 사유 메모 (사진 촬영 필수)"
|
||||
placeholderTextColor={colors.neutral500}
|
||||
style={styles.memo}
|
||||
multiline
|
||||
/>
|
||||
) : null}
|
||||
</Card>
|
||||
))}
|
||||
</ScrollView>
|
||||
|
||||
{/* 하단 고정 액션 */}
|
||||
<View style={styles.footer}>
|
||||
{!allConfirmed ? (
|
||||
<Text style={styles.footerNote}>모든 AI 플래그 확인 후 승인할 수 있습니다.</Text>
|
||||
) : null}
|
||||
<View style={styles.footerRow}>
|
||||
<Button
|
||||
label="검수 승인"
|
||||
variant="success"
|
||||
onPress={approve}
|
||||
disabled={!canApprove}
|
||||
style={styles.footerBtn}
|
||||
/>
|
||||
<Button label="시정 요청" variant="danger" onPress={requestFix} style={styles.footerBtn} />
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
function SegBtn({
|
||||
label,
|
||||
active,
|
||||
tone,
|
||||
onPress,
|
||||
}: {
|
||||
label: string;
|
||||
active: boolean;
|
||||
tone: 'ok' | 'fail';
|
||||
onPress: () => void;
|
||||
}) {
|
||||
const activeColor = tone === 'ok' ? colors.success : colors.error;
|
||||
return (
|
||||
<Text
|
||||
onPress={onPress}
|
||||
style={[
|
||||
styles.segBtn,
|
||||
active && { backgroundColor: activeColor, borderColor: activeColor, color: colors.white },
|
||||
]}
|
||||
>
|
||||
{label}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
flex: { flex: 1, backgroundColor: colors.neutral050 },
|
||||
scroll: { padding: spacing.md, gap: spacing.md, paddingBottom: 140 },
|
||||
title: { fontSize: type.h3.fontSize, fontWeight: '700', color: colors.neutral900 },
|
||||
compareRow: { flexDirection: 'row', gap: spacing.sm },
|
||||
compareCol: { flex: 1, gap: 6 },
|
||||
compareLabel: { fontSize: type.caption.fontSize, color: colors.neutral500, fontWeight: '600' },
|
||||
drawingThumb: {
|
||||
height: 96,
|
||||
borderRadius: radius.md,
|
||||
backgroundColor: colors.canvasBg,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 4,
|
||||
},
|
||||
drawingText: { color: colors.primary100, fontSize: type.caption.fontSize },
|
||||
flagHead: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', marginBottom: 8 },
|
||||
flagTitle: { fontSize: type.h3.fontSize, fontWeight: '700', color: colors.aiAccent },
|
||||
flagRow: { flexDirection: 'row', alignItems: 'center', gap: 8, paddingVertical: 8 },
|
||||
sevDot: { width: 10, height: 10, borderRadius: 5 },
|
||||
flagLabel: { flex: 1, fontSize: type.body.fontSize, color: colors.neutral900 },
|
||||
confirmChip: {
|
||||
borderRadius: radius.pill,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.neutral200,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 6,
|
||||
minHeight: 36,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
confirmChipOn: { backgroundColor: colors.success, borderColor: colors.success },
|
||||
confirmText: { fontSize: type.caption.fontSize, color: colors.neutral700, fontWeight: '600' },
|
||||
confirmTextOn: { color: colors.white },
|
||||
itemLabel: { fontSize: type.body.fontSize, fontWeight: '600', color: colors.neutral900, marginBottom: 10 },
|
||||
segment: { flexDirection: 'row', gap: 8 },
|
||||
segBtn: {
|
||||
flex: 1,
|
||||
textAlign: 'center',
|
||||
minHeight: touch.min,
|
||||
lineHeight: touch.min,
|
||||
borderRadius: radius.sm,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.neutral200,
|
||||
color: colors.neutral700,
|
||||
fontSize: type.body.fontSize,
|
||||
fontWeight: '600',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
memo: {
|
||||
marginTop: 10,
|
||||
minHeight: 64,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.neutral200,
|
||||
borderRadius: radius.sm,
|
||||
padding: 12,
|
||||
fontSize: type.body.fontSize,
|
||||
color: colors.neutral900,
|
||||
textAlignVertical: 'top',
|
||||
},
|
||||
footer: {
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
backgroundColor: colors.white,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: colors.neutral200,
|
||||
padding: spacing.md,
|
||||
paddingBottom: spacing.lg,
|
||||
gap: 8,
|
||||
},
|
||||
footerNote: { textAlign: 'center', fontSize: type.caption.fontSize, color: colors.warning },
|
||||
footerRow: { flexDirection: 'row', gap: spacing.sm },
|
||||
footerBtn: { flex: 1 },
|
||||
});
|
||||
207
mobile/app/login.tsx
Normal file
207
mobile/app/login.tsx
Normal file
@ -0,0 +1,207 @@
|
||||
/*
|
||||
* SCR-01 로그인 — 이메일/비번 + 아이디 기억 + 비밀번호 찾기·회원가입 링크 + 2FA(OTP) 대응.
|
||||
* 브랜드 패널(딥블루 그라디언트 대체 카피) + AI 이미지 고지 캡션.
|
||||
* 비밀번호는 저장하지 않는다. 아이디(이메일)만 AsyncStorage에 기억.
|
||||
*/
|
||||
import { Link, router } from 'expo-router';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
KeyboardAvoidingView,
|
||||
Platform,
|
||||
Pressable,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
Text,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { Banner } from '../components/Banner';
|
||||
import { Button } from '../components/Button';
|
||||
import { Field } from '../components/Field';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
import { ApiRequestError } from '../lib/api';
|
||||
import { isOtpRequired, login } from '../lib/auth';
|
||||
import { prefDelete, prefGet, prefSet } from '../lib/secureStore';
|
||||
import { colors, radius, spacing, touch, type } from '../theme';
|
||||
|
||||
const REMEMBER_KEY = 'kintex.rememberedEmail';
|
||||
|
||||
export default function LoginScreen() {
|
||||
const insets = useSafeAreaInsets();
|
||||
const { signIn } = useAuth();
|
||||
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [otp, setOtp] = useState('');
|
||||
const [otpRequired, setOtpRequired] = useState(false);
|
||||
const [remember, setRemember] = useState(true);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const saved = await prefGet(REMEMBER_KEY);
|
||||
if (saved) {
|
||||
setEmail(saved);
|
||||
setRemember(true);
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
async function onSubmit() {
|
||||
setError(null);
|
||||
if (!email.trim() || !password) {
|
||||
setError('이메일과 비밀번호를 입력해 주세요.');
|
||||
return;
|
||||
}
|
||||
if (otpRequired && !otp.trim()) {
|
||||
setError('2차 인증 코드를 입력해 주세요.');
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await login({ email, password, otp: otpRequired ? otp : undefined });
|
||||
if (remember) await prefSet(REMEMBER_KEY, email.trim());
|
||||
else await prefDelete(REMEMBER_KEY);
|
||||
await signIn(res);
|
||||
router.replace('/(tabs)');
|
||||
} catch (e) {
|
||||
if (isOtpRequired(e)) {
|
||||
setOtpRequired(true);
|
||||
setError('2차 인증 코드가 필요합니다. 인증 앱의 코드를 입력해 주세요.');
|
||||
} else if (e instanceof ApiRequestError && e.code === 'OTP_INVALID') {
|
||||
setError('인증 코드가 올바르지 않거나 만료되었습니다.');
|
||||
} else if (e instanceof ApiRequestError && e.code === 'ACCOUNT_LOCKED') {
|
||||
setError('로그인 시도 초과로 계정이 잠겼습니다. 잠시 후 다시 시도해 주세요.');
|
||||
} else if (e instanceof ApiRequestError) {
|
||||
setError(e.message);
|
||||
} else {
|
||||
setError('로그인에 실패했습니다.');
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<KeyboardAvoidingView
|
||||
style={styles.flex}
|
||||
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
||||
>
|
||||
<ScrollView
|
||||
contentContainerStyle={[styles.scroll, { paddingTop: insets.top + spacing.lg }]}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
>
|
||||
{/* 브랜드 패널 */}
|
||||
<View style={styles.brand}>
|
||||
<Text style={styles.brandMark}>KINTEX</Text>
|
||||
<Text style={styles.brandTitle}>신청서를 내는 순간,{'\n'}시공 후 사진을 먼저 봅니다</Text>
|
||||
<Text style={styles.brandCaption}>AI 생성 예상 이미지 · 실제 시공 결과와 다를 수 있습니다</Text>
|
||||
</View>
|
||||
|
||||
{/* 로그인 카드 */}
|
||||
<View style={styles.card}>
|
||||
<Text style={styles.cardTitle}>로그인</Text>
|
||||
|
||||
{error ? (
|
||||
<Banner tone={otpRequired ? 'warning' : 'error'}>{error}</Banner>
|
||||
) : null}
|
||||
|
||||
<Field
|
||||
label="이메일"
|
||||
value={email}
|
||||
onChangeText={setEmail}
|
||||
placeholder="name@company.co.kr"
|
||||
keyboardType="email-address"
|
||||
/>
|
||||
<Field
|
||||
label="비밀번호"
|
||||
value={password}
|
||||
onChangeText={setPassword}
|
||||
placeholder="비밀번호"
|
||||
secureTextEntry
|
||||
/>
|
||||
|
||||
{otpRequired ? (
|
||||
<Field
|
||||
label="2차 인증 코드"
|
||||
value={otp}
|
||||
onChangeText={setOtp}
|
||||
placeholder="6자리 코드"
|
||||
keyboardType="number-pad"
|
||||
helperText="인증 앱(TOTP)의 6자리 코드"
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{/* 아이디 기억 */}
|
||||
<Pressable
|
||||
style={styles.rememberRow}
|
||||
onPress={() => setRemember((v) => !v)}
|
||||
accessibilityRole="checkbox"
|
||||
accessibilityState={{ checked: remember }}
|
||||
>
|
||||
<View style={[styles.checkbox, remember && styles.checkboxOn]}>
|
||||
{remember ? <Text style={styles.checkmark}>✓</Text> : null}
|
||||
</View>
|
||||
<Text style={styles.rememberText}>아이디 기억</Text>
|
||||
</Pressable>
|
||||
|
||||
<Button
|
||||
label={otpRequired ? '인증 후 로그인' : '로그인'}
|
||||
onPress={onSubmit}
|
||||
loading={loading}
|
||||
/>
|
||||
|
||||
<View style={styles.links}>
|
||||
<Link href="/forgot-password" style={styles.link}>
|
||||
비밀번호 찾기
|
||||
</Link>
|
||||
<Text style={styles.linkDivider}>·</Text>
|
||||
<Link href="/register" style={styles.link}>
|
||||
회원가입
|
||||
</Link>
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</KeyboardAvoidingView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
flex: { flex: 1, backgroundColor: colors.neutral050 },
|
||||
scroll: { padding: spacing.md, gap: spacing.lg, paddingBottom: spacing.xl },
|
||||
brand: {
|
||||
backgroundColor: colors.primary700,
|
||||
borderRadius: radius.md,
|
||||
padding: spacing.lg,
|
||||
gap: 8,
|
||||
},
|
||||
brandMark: { color: colors.white, fontSize: 22, fontWeight: '800', letterSpacing: 1 },
|
||||
brandTitle: { color: colors.white, fontSize: type.h2.fontSize, fontWeight: '700', lineHeight: 30 },
|
||||
brandCaption: { color: 'rgba(255,255,255,0.75)', fontSize: type.caption.fontSize },
|
||||
card: {
|
||||
backgroundColor: colors.white,
|
||||
borderRadius: radius.md,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.neutral200,
|
||||
padding: spacing.lg,
|
||||
gap: spacing.md,
|
||||
},
|
||||
cardTitle: { fontSize: type.h1.fontSize, fontWeight: '700', color: colors.neutral900 },
|
||||
rememberRow: { flexDirection: 'row', alignItems: 'center', gap: 8, minHeight: 40 },
|
||||
checkbox: {
|
||||
width: 22,
|
||||
height: 22,
|
||||
borderRadius: radius.sm,
|
||||
borderWidth: 2,
|
||||
borderColor: colors.neutral200,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
checkboxOn: { backgroundColor: colors.primary600, borderColor: colors.primary600 },
|
||||
checkmark: { color: colors.white, fontSize: 14, fontWeight: '700' },
|
||||
rememberText: { fontSize: type.body.fontSize, color: colors.neutral700 },
|
||||
links: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 10, minHeight: touch.min },
|
||||
link: { color: colors.primary600, fontSize: type.body.fontSize, fontWeight: '600' },
|
||||
linkDivider: { color: colors.neutral500 },
|
||||
});
|
||||
88
mobile/app/register.tsx
Normal file
88
mobile/app/register.tsx
Normal file
@ -0,0 +1,88 @@
|
||||
/*
|
||||
* 회원가입 — 04_backend_public_auth.md §1. 공개 경로.
|
||||
* 비밀번호 8~100자. companyName·inviteCode 선택. 성공 시 로그인 안내.
|
||||
*/
|
||||
import { router } from 'expo-router';
|
||||
import React, { useState } from 'react';
|
||||
import { KeyboardAvoidingView, Platform, ScrollView, StyleSheet, Text } from 'react-native';
|
||||
import { Banner } from '../components/Banner';
|
||||
import { Button } from '../components/Button';
|
||||
import { Field } from '../components/Field';
|
||||
import { ApiRequestError } from '../lib/api';
|
||||
import { register } from '../lib/auth';
|
||||
import { colors, spacing, type } from '../theme';
|
||||
|
||||
export default function RegisterScreen() {
|
||||
const [email, setEmail] = useState('');
|
||||
const [displayName, setDisplayName] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [companyName, setCompanyName] = useState('');
|
||||
const [inviteCode, setInviteCode] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [done, setDone] = useState<string | null>(null);
|
||||
|
||||
async function onSubmit() {
|
||||
setError(null);
|
||||
if (!email.trim() || !displayName.trim() || password.length < 8) {
|
||||
setError('이메일·이름·비밀번호(8자 이상)를 확인해 주세요.');
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await register({
|
||||
email,
|
||||
displayName: displayName.trim(),
|
||||
password,
|
||||
companyName: companyName.trim() || undefined,
|
||||
inviteCode: inviteCode.trim() || undefined,
|
||||
});
|
||||
setDone(
|
||||
res.joinedEvent
|
||||
? '가입이 완료되었고 초대 행사에 연결되었습니다. 로그인해 주세요.'
|
||||
: '가입이 완료되었습니다. 로그인해 주세요.',
|
||||
);
|
||||
} catch (e) {
|
||||
if (e instanceof ApiRequestError && e.code === 'EMAIL_TAKEN') {
|
||||
setError('이미 가입된 이메일입니다.');
|
||||
} else if (e instanceof ApiRequestError) {
|
||||
setError(e.message);
|
||||
} else {
|
||||
setError('회원가입에 실패했습니다.');
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<KeyboardAvoidingView
|
||||
style={styles.flex}
|
||||
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
||||
>
|
||||
<ScrollView contentContainerStyle={styles.scroll} keyboardShouldPersistTaps="handled">
|
||||
<Text style={styles.title}>계정 만들기</Text>
|
||||
{error ? <Banner tone="error">{error}</Banner> : null}
|
||||
{done ? <Banner tone="info">{done}</Banner> : null}
|
||||
|
||||
<Field label="이메일" value={email} onChangeText={setEmail} placeholder="name@company.co.kr" keyboardType="email-address" />
|
||||
<Field label="이름" value={displayName} onChangeText={setDisplayName} placeholder="홍길동" autoCapitalize="none" />
|
||||
<Field label="비밀번호" value={password} onChangeText={setPassword} placeholder="8자 이상" secureTextEntry helperText="8~100자" />
|
||||
<Field label="회사명 (선택)" value={companyName} onChangeText={setCompanyName} placeholder="지오인포" />
|
||||
<Field label="초대 코드 (선택)" value={inviteCode} onChangeText={setInviteCode} placeholder="행사 초대 코드" />
|
||||
|
||||
{done ? (
|
||||
<Button label="로그인으로 이동" onPress={() => router.replace('/login')} />
|
||||
) : (
|
||||
<Button label="가입하기" onPress={onSubmit} loading={loading} />
|
||||
)}
|
||||
</ScrollView>
|
||||
</KeyboardAvoidingView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
flex: { flex: 1, backgroundColor: colors.neutral050 },
|
||||
scroll: { padding: spacing.md, gap: spacing.md, paddingBottom: spacing.xl },
|
||||
title: { fontSize: type.h2.fontSize, fontWeight: '700', color: colors.neutral900 },
|
||||
});
|
||||
6
mobile/babel.config.js
Normal file
6
mobile/babel.config.js
Normal file
@ -0,0 +1,6 @@
|
||||
module.exports = function (api) {
|
||||
api.cache(true);
|
||||
return {
|
||||
presets: ['babel-preset-expo'],
|
||||
};
|
||||
};
|
||||
122
mobile/components/AiImage.tsx
Normal file
122
mobile/components/AiImage.tsx
Normal file
@ -0,0 +1,122 @@
|
||||
/*
|
||||
* AI 생성 이미지 — 워터마크 상시 오버레이 + "AI 생성 예상 이미지" 고지문(제거 불가).
|
||||
* 계약: 01_backend_contracts.md §0-3 (watermarkRequired/watermarkText/notice 항상 포함).
|
||||
* design.md §1-4: 생성 이미지는 카드 하단 고지문 항상 노출.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { Image, StyleSheet, Text, View, type StyleProp, type ViewStyle } from 'react-native';
|
||||
import { AI_IMAGE_NOTICE } from '../lib/config';
|
||||
import { colors, radius, type } from '../theme';
|
||||
|
||||
interface Props {
|
||||
uri: string | null;
|
||||
/** 상태: 생성 중/실패 표시. */
|
||||
status?: 'DONE' | 'QUEUED' | 'RUNNING' | 'FAILED';
|
||||
watermarkText?: string;
|
||||
notice?: string;
|
||||
shotBadge?: string; // 예: "S2 야간"
|
||||
height?: number;
|
||||
style?: StyleProp<ViewStyle>;
|
||||
}
|
||||
|
||||
export function AiImage({
|
||||
uri,
|
||||
status = 'DONE',
|
||||
watermarkText,
|
||||
notice,
|
||||
shotBadge,
|
||||
height = 200,
|
||||
style,
|
||||
}: Props) {
|
||||
const wm = watermarkText ?? AI_IMAGE_NOTICE;
|
||||
const loading = status === 'QUEUED' || status === 'RUNNING';
|
||||
const failed = status === 'FAILED';
|
||||
|
||||
return (
|
||||
<View style={[styles.wrap, style]}>
|
||||
<View style={[styles.frame, { height }]}>
|
||||
{uri && status === 'DONE' ? (
|
||||
<Image source={{ uri }} style={styles.img} resizeMode="cover" />
|
||||
) : (
|
||||
<View style={[styles.img, styles.placeholder]}>
|
||||
<Text style={styles.placeholderText}>
|
||||
{loading ? '생성 중… 평균 40초' : failed ? '생성 실패 — 다시 생성' : '이미지 없음'}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* 워터마크 (상시, 제거 불가) */}
|
||||
<View pointerEvents="none" style={styles.watermarkLayer}>
|
||||
<Text style={styles.watermark}>{wm}</Text>
|
||||
</View>
|
||||
|
||||
{/* AI 라벨 칩 */}
|
||||
<View style={styles.aiChip}>
|
||||
<Text style={styles.aiChipText}>AI 생성</Text>
|
||||
</View>
|
||||
|
||||
{shotBadge ? (
|
||||
<View style={styles.shotBadge}>
|
||||
<Text style={styles.shotBadgeText}>{shotBadge}</Text>
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
|
||||
{/* 하단 고지문 (제거 불가) */}
|
||||
<Text style={styles.notice}>{notice ?? wm}</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
wrap: { gap: 6 },
|
||||
frame: {
|
||||
width: '100%',
|
||||
borderRadius: radius.md,
|
||||
overflow: 'hidden',
|
||||
backgroundColor: colors.neutral050,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.neutral200,
|
||||
},
|
||||
img: { width: '100%', height: '100%' },
|
||||
placeholder: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: colors.aiSurface,
|
||||
},
|
||||
placeholderText: { color: colors.neutral500, fontSize: type.body.fontSize },
|
||||
watermarkLayer: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
watermark: {
|
||||
color: 'rgba(16,24,40,0.28)',
|
||||
fontSize: 13,
|
||||
fontWeight: '700',
|
||||
transform: [{ rotate: '-18deg' }],
|
||||
textAlign: 'center',
|
||||
paddingHorizontal: 12,
|
||||
},
|
||||
aiChip: {
|
||||
position: 'absolute',
|
||||
top: 8,
|
||||
left: 8,
|
||||
backgroundColor: colors.aiAccent,
|
||||
borderRadius: radius.pill,
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 2,
|
||||
},
|
||||
aiChipText: { color: colors.white, fontSize: 11, fontWeight: '700' },
|
||||
shotBadge: {
|
||||
position: 'absolute',
|
||||
top: 8,
|
||||
right: 8,
|
||||
backgroundColor: 'rgba(28,37,54,0.8)',
|
||||
borderRadius: radius.sm,
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 2,
|
||||
},
|
||||
shotBadgeText: { color: colors.white, fontSize: 11, fontWeight: '600' },
|
||||
notice: { color: colors.neutral500, fontSize: type.caption.fontSize },
|
||||
});
|
||||
34
mobile/components/Banner.tsx
Normal file
34
mobile/components/Banner.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
/*
|
||||
* 알림 배너 — degraded(서버 미구현/오프라인 폴백)·warning·error·info 공용.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { StyleSheet, Text, View } from 'react-native';
|
||||
import { colors, radius, type } from '../theme';
|
||||
|
||||
type Tone = 'info' | 'warning' | 'error' | 'degraded';
|
||||
|
||||
const toneColor: Record<Tone, { border: string; bg: string; text: string }> = {
|
||||
info: { border: colors.primary600, bg: colors.primary050, text: colors.primary700 },
|
||||
warning: { border: colors.warning, bg: '#FFF7ED', text: colors.warning },
|
||||
error: { border: colors.error, bg: '#FEF3F2', text: colors.error },
|
||||
degraded: { border: colors.neutral500, bg: colors.neutral050, text: colors.neutral700 },
|
||||
};
|
||||
|
||||
export function Banner({ tone = 'info', children }: { tone?: Tone; children: React.ReactNode }) {
|
||||
const c = toneColor[tone];
|
||||
return (
|
||||
<View style={[styles.banner, { borderColor: c.border, backgroundColor: c.bg }]}>
|
||||
<Text style={[styles.text, { color: c.text }]}>{children}</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
banner: {
|
||||
borderWidth: 1,
|
||||
borderRadius: radius.md,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 10,
|
||||
},
|
||||
text: { fontSize: type.body.fontSize, fontWeight: '500' },
|
||||
});
|
||||
95
mobile/components/Button.tsx
Normal file
95
mobile/components/Button.tsx
Normal file
@ -0,0 +1,95 @@
|
||||
/*
|
||||
* 기본 버튼 — 48px 터치 타깃(design.md §1-3), radius 4(sm).
|
||||
* variant: primary(브랜드 블루) · ai(보라) · outline · success · danger · ghost.
|
||||
*/
|
||||
import React from 'react';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Pressable,
|
||||
StyleSheet,
|
||||
Text,
|
||||
View,
|
||||
type StyleProp,
|
||||
type ViewStyle,
|
||||
} from 'react-native';
|
||||
import { colors, radius, touch, type } from '../theme';
|
||||
|
||||
type Variant = 'primary' | 'ai' | 'outline' | 'success' | 'danger' | 'ghost';
|
||||
|
||||
interface Props {
|
||||
label: string;
|
||||
onPress?: () => void;
|
||||
variant?: Variant;
|
||||
disabled?: boolean;
|
||||
loading?: boolean;
|
||||
style?: StyleProp<ViewStyle>;
|
||||
icon?: React.ReactNode;
|
||||
}
|
||||
|
||||
const bg: Record<Variant, string> = {
|
||||
primary: colors.primary600,
|
||||
ai: colors.aiAccent,
|
||||
outline: 'transparent',
|
||||
success: colors.success,
|
||||
danger: colors.error,
|
||||
ghost: 'transparent',
|
||||
};
|
||||
|
||||
const fg: Record<Variant, string> = {
|
||||
primary: colors.white,
|
||||
ai: colors.white,
|
||||
outline: colors.primary600,
|
||||
success: colors.white,
|
||||
danger: colors.white,
|
||||
ghost: colors.neutral700,
|
||||
};
|
||||
|
||||
export function Button({
|
||||
label,
|
||||
onPress,
|
||||
variant = 'primary',
|
||||
disabled,
|
||||
loading,
|
||||
style,
|
||||
icon,
|
||||
}: Props) {
|
||||
const isDisabled = disabled || loading;
|
||||
return (
|
||||
<Pressable
|
||||
accessibilityRole="button"
|
||||
accessibilityState={{ disabled: !!isDisabled, busy: !!loading }}
|
||||
onPress={isDisabled ? undefined : onPress}
|
||||
style={({ pressed }) => [
|
||||
styles.base,
|
||||
{
|
||||
backgroundColor: bg[variant],
|
||||
borderColor: variant === 'outline' ? colors.primary600 : 'transparent',
|
||||
borderWidth: variant === 'outline' ? 1 : 0,
|
||||
opacity: isDisabled ? 0.5 : pressed ? 0.85 : 1,
|
||||
},
|
||||
style,
|
||||
]}
|
||||
>
|
||||
{loading ? (
|
||||
<ActivityIndicator color={fg[variant]} />
|
||||
) : (
|
||||
<View style={styles.row}>
|
||||
{icon}
|
||||
<Text style={[styles.label, { color: fg[variant] }]}>{label}</Text>
|
||||
</View>
|
||||
)}
|
||||
</Pressable>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
base: {
|
||||
minHeight: touch.min,
|
||||
borderRadius: radius.sm,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingHorizontal: 16,
|
||||
},
|
||||
row: { flexDirection: 'row', alignItems: 'center', gap: 8 },
|
||||
label: { fontSize: type.h3.fontSize, fontWeight: '600' },
|
||||
});
|
||||
47
mobile/components/Card.tsx
Normal file
47
mobile/components/Card.tsx
Normal file
@ -0,0 +1,47 @@
|
||||
/*
|
||||
* 카드 — 흰색 서피스 + 1px 테두리(그림자 최소, design.md §1-4 엘리베이션).
|
||||
* accent: 'ai'면 좌측 4px 보라 액센트, 'status'면 좌측 4px 상태색.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { StyleSheet, View, type StyleProp, type ViewStyle } from 'react-native';
|
||||
import { colors, radius, spacing } from '../theme';
|
||||
|
||||
interface Props {
|
||||
children: React.ReactNode;
|
||||
accent?: 'none' | 'ai' | 'warning' | 'error' | 'success';
|
||||
style?: StyleProp<ViewStyle>;
|
||||
}
|
||||
|
||||
const accentColor: Record<NonNullable<Props['accent']>, string | undefined> = {
|
||||
none: undefined,
|
||||
ai: colors.aiAccent,
|
||||
warning: colors.warning,
|
||||
error: colors.error,
|
||||
success: colors.success,
|
||||
};
|
||||
|
||||
export function Card({ children, accent = 'none', style }: Props) {
|
||||
const left = accentColor[accent];
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
styles.card,
|
||||
accent === 'ai' && { backgroundColor: colors.aiSurface },
|
||||
left ? { borderLeftWidth: 4, borderLeftColor: left } : null,
|
||||
style,
|
||||
]}
|
||||
>
|
||||
{children}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
card: {
|
||||
backgroundColor: colors.white,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.neutral200,
|
||||
borderRadius: radius.md,
|
||||
padding: spacing.md,
|
||||
},
|
||||
});
|
||||
29
mobile/components/DdayChip.tsx
Normal file
29
mobile/components/DdayChip.tsx
Normal file
@ -0,0 +1,29 @@
|
||||
/*
|
||||
* D-데이 칩 — 마감 역산(design.md §1-4). D-3 이내 warning, 초과 error.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { StyleSheet, Text, View } from 'react-native';
|
||||
import { colors, radius, type } from '../theme';
|
||||
|
||||
export function DdayChip({ dday }: { dday: number }) {
|
||||
const overdue = dday < 0;
|
||||
const imminent = dday >= 0 && dday <= 3;
|
||||
const bg = overdue ? colors.error : imminent ? colors.warning : colors.primary100;
|
||||
const fg = overdue || imminent ? colors.white : colors.primary700;
|
||||
const label = overdue ? `마감 +${Math.abs(dday)}` : `D-${dday}`;
|
||||
return (
|
||||
<View style={[styles.chip, { backgroundColor: bg }]}>
|
||||
<Text style={[styles.text, { color: fg }]}>{label}</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
chip: {
|
||||
alignSelf: 'flex-start',
|
||||
paddingHorizontal: 10,
|
||||
paddingVertical: 3,
|
||||
borderRadius: radius.pill,
|
||||
},
|
||||
text: { fontSize: type.caption.fontSize, fontWeight: '700' },
|
||||
});
|
||||
82
mobile/components/Field.tsx
Normal file
82
mobile/components/Field.tsx
Normal file
@ -0,0 +1,82 @@
|
||||
/*
|
||||
* 라벨 + 텍스트 입력 필드 — radius 4, 높이 48(터치 타깃).
|
||||
*/
|
||||
import React from 'react';
|
||||
import {
|
||||
StyleSheet,
|
||||
Text,
|
||||
TextInput,
|
||||
View,
|
||||
type KeyboardTypeOptions,
|
||||
type TextInputProps,
|
||||
} from 'react-native';
|
||||
import { colors, radius, touch, type } from '../theme';
|
||||
|
||||
interface Props {
|
||||
label?: string;
|
||||
value: string;
|
||||
onChangeText: (v: string) => void;
|
||||
placeholder?: string;
|
||||
secureTextEntry?: boolean;
|
||||
keyboardType?: KeyboardTypeOptions;
|
||||
autoCapitalize?: TextInputProps['autoCapitalize'];
|
||||
helperText?: string;
|
||||
errorText?: string;
|
||||
editable?: boolean;
|
||||
}
|
||||
|
||||
export function Field({
|
||||
label,
|
||||
value,
|
||||
onChangeText,
|
||||
placeholder,
|
||||
secureTextEntry,
|
||||
keyboardType,
|
||||
autoCapitalize = 'none',
|
||||
helperText,
|
||||
errorText,
|
||||
editable = true,
|
||||
}: Props) {
|
||||
return (
|
||||
<View style={styles.wrap}>
|
||||
{label ? <Text style={styles.label}>{label}</Text> : null}
|
||||
<TextInput
|
||||
value={value}
|
||||
onChangeText={onChangeText}
|
||||
placeholder={placeholder}
|
||||
placeholderTextColor={colors.neutral500}
|
||||
secureTextEntry={secureTextEntry}
|
||||
keyboardType={keyboardType}
|
||||
autoCapitalize={autoCapitalize}
|
||||
editable={editable}
|
||||
style={[
|
||||
styles.input,
|
||||
!!errorText && { borderColor: colors.error },
|
||||
!editable && { backgroundColor: colors.neutral050 },
|
||||
]}
|
||||
/>
|
||||
{errorText ? (
|
||||
<Text style={styles.error}>{errorText}</Text>
|
||||
) : helperText ? (
|
||||
<Text style={styles.helper}>{helperText}</Text>
|
||||
) : null}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
wrap: { gap: 6 },
|
||||
label: { fontSize: type.body.fontSize, fontWeight: '600', color: colors.neutral700 },
|
||||
input: {
|
||||
minHeight: touch.min,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.neutral200,
|
||||
borderRadius: radius.sm,
|
||||
paddingHorizontal: 12,
|
||||
fontSize: type.body.fontSize,
|
||||
color: colors.neutral900,
|
||||
backgroundColor: colors.white,
|
||||
},
|
||||
helper: { fontSize: type.caption.fontSize, color: colors.neutral500 },
|
||||
error: { fontSize: type.caption.fontSize, color: colors.error },
|
||||
});
|
||||
45
mobile/components/StatusBadge.tsx
Normal file
45
mobile/components/StatusBadge.tsx
Normal file
@ -0,0 +1,45 @@
|
||||
/*
|
||||
* 상태 배지 — 신청 흐름 상태 색 매핑(design.md §1-4).
|
||||
* 작성중(회색)→제출됨(파랑)→AI검토(보라)→승인(녹색)→반려(빨강)→시공중→검수완료.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { StyleSheet, Text, View } from 'react-native';
|
||||
import { colors, radius, type } from '../theme';
|
||||
|
||||
export type Status =
|
||||
| 'draft'
|
||||
| 'submitted'
|
||||
| 'ai_review'
|
||||
| 'approved'
|
||||
| 'rejected'
|
||||
| 'in_construction'
|
||||
| 'inspected';
|
||||
|
||||
const meta: Record<Status, { label: string; color: string; bg: string }> = {
|
||||
draft: { label: '작성중', color: colors.neutral700, bg: colors.neutral050 },
|
||||
submitted: { label: '제출됨', color: colors.white, bg: colors.primary600 },
|
||||
ai_review: { label: 'AI검토', color: colors.white, bg: colors.aiAccent },
|
||||
approved: { label: '승인', color: colors.white, bg: colors.success },
|
||||
rejected: { label: '반려', color: colors.white, bg: colors.error },
|
||||
in_construction: { label: '시공중', color: colors.white, bg: colors.primary700 },
|
||||
inspected: { label: '검수완료', color: colors.white, bg: colors.success },
|
||||
};
|
||||
|
||||
export function StatusBadge({ status }: { status: Status }) {
|
||||
const m = meta[status];
|
||||
return (
|
||||
<View style={[styles.badge, { backgroundColor: m.bg }]}>
|
||||
<Text style={[styles.text, { color: m.color }]}>{m.label}</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
badge: {
|
||||
alignSelf: 'flex-start',
|
||||
paddingHorizontal: 10,
|
||||
paddingVertical: 3,
|
||||
borderRadius: radius.pill,
|
||||
},
|
||||
text: { fontSize: type.caption.fontSize, fontWeight: '600' },
|
||||
});
|
||||
105
mobile/context/AuthContext.tsx
Normal file
105
mobile/context/AuthContext.tsx
Normal file
@ -0,0 +1,105 @@
|
||||
/*
|
||||
* 인증 컨텍스트 — JWT를 secure-store에 영속, 앱 부팅 시 복원.
|
||||
* 활성 워크스페이스(행사) 선택 상태 보관. 비밀번호는 저장하지 않는다.
|
||||
*/
|
||||
import React, {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { setAccessToken } from '../lib/api';
|
||||
import { secureDelete, secureGet, secureSet } from '../lib/secureStore';
|
||||
import type { AuthUser, LoginResponse, WorkspaceDto } from '../lib/types';
|
||||
|
||||
const TOKEN_KEY = 'kintex.accessToken';
|
||||
const USER_KEY = 'kintex.user';
|
||||
|
||||
interface AuthState {
|
||||
ready: boolean;
|
||||
token: string | null;
|
||||
user: AuthUser | null;
|
||||
workspaces: WorkspaceDto[];
|
||||
activeEventId: string | null;
|
||||
}
|
||||
|
||||
interface AuthContextValue extends AuthState {
|
||||
activeWorkspace: WorkspaceDto | null;
|
||||
signIn: (res: LoginResponse) => Promise<void>;
|
||||
signOut: () => Promise<void>;
|
||||
selectWorkspace: (eventId: string) => void;
|
||||
}
|
||||
|
||||
const AuthContext = createContext<AuthContextValue | undefined>(undefined);
|
||||
|
||||
export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
const [state, setState] = useState<AuthState>({
|
||||
ready: false,
|
||||
token: null,
|
||||
user: null,
|
||||
workspaces: [],
|
||||
activeEventId: null,
|
||||
});
|
||||
|
||||
// 부팅 시 토큰 복원
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
(async () => {
|
||||
const token = await secureGet(TOKEN_KEY);
|
||||
const userRaw = await secureGet(USER_KEY);
|
||||
if (cancelled) return;
|
||||
const user = userRaw ? (JSON.parse(userRaw) as AuthUser) : null;
|
||||
if (token) setAccessToken(token);
|
||||
setState((s) => ({ ...s, ready: true, token, user }));
|
||||
})();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const signIn = useCallback(async (res: LoginResponse) => {
|
||||
setAccessToken(res.accessToken);
|
||||
await secureSet(TOKEN_KEY, res.accessToken);
|
||||
await secureSet(USER_KEY, JSON.stringify(res.user));
|
||||
setState((s) => ({
|
||||
...s,
|
||||
token: res.accessToken,
|
||||
user: res.user,
|
||||
workspaces: res.workspaces ?? [],
|
||||
activeEventId: res.workspaces?.[0]?.eventId ?? null,
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const signOut = useCallback(async () => {
|
||||
setAccessToken(null);
|
||||
await secureDelete(TOKEN_KEY);
|
||||
await secureDelete(USER_KEY);
|
||||
setState((s) => ({
|
||||
...s,
|
||||
token: null,
|
||||
user: null,
|
||||
workspaces: [],
|
||||
activeEventId: null,
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const selectWorkspace = useCallback((eventId: string) => {
|
||||
setState((s) => ({ ...s, activeEventId: eventId }));
|
||||
}, []);
|
||||
|
||||
const value = useMemo<AuthContextValue>(() => {
|
||||
const activeWorkspace =
|
||||
state.workspaces.find((w) => w.eventId === state.activeEventId) ?? null;
|
||||
return { ...state, activeWorkspace, signIn, signOut, selectWorkspace };
|
||||
}, [state, signIn, signOut, selectWorkspace]);
|
||||
|
||||
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
||||
}
|
||||
|
||||
export function useAuth(): AuthContextValue {
|
||||
const ctx = useContext(AuthContext);
|
||||
if (!ctx) throw new Error('useAuth must be used within AuthProvider');
|
||||
return ctx;
|
||||
}
|
||||
3
mobile/expo-env.d.ts
vendored
Normal file
3
mobile/expo-env.d.ts
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
/// <reference types="expo/types" />
|
||||
|
||||
// NOTE: This file should not be edited and should be in your git ignore
|
||||
104
mobile/lib/api.ts
Normal file
104
mobile/lib/api.ts
Normal file
@ -0,0 +1,104 @@
|
||||
/*
|
||||
* API 클라이언트 — 백엔드 계약 base URL·JWT 헤더·오류 봉투(success/data/error) 처리.
|
||||
* 근거: _workspace/01_backend_contracts.md §0 (ApiResponse 봉투, 오류 코드→HTTP, 인증 헤더).
|
||||
* 웹 프론트(src/frontend/src/api/client.ts) 패턴과 정합. 시크릿 하드코딩 없음.
|
||||
*/
|
||||
import { API_BASE } from './config';
|
||||
import type { ApiError, ApiErrorCode, ApiResponse } from './types';
|
||||
|
||||
/** 봉투 error를 그대로 던지는 오류 타입 — UI는 code로 분기(OTP_REQUIRED·COMPLIANCE_BLOCKED 등). */
|
||||
export class ApiRequestError extends Error {
|
||||
readonly code: ApiErrorCode | 'NETWORK' | 'UNKNOWN';
|
||||
readonly httpStatus: number;
|
||||
constructor(code: ApiRequestError['code'], message: string, httpStatus: number) {
|
||||
super(message);
|
||||
this.name = 'ApiRequestError';
|
||||
this.code = code;
|
||||
this.httpStatus = httpStatus;
|
||||
}
|
||||
}
|
||||
|
||||
/** 서버 미구현(501/NOT_IMPLEMENTED)·네트워크 장애 등 폴백(degraded) 판정 헬퍼. */
|
||||
export function isDegraded(e: unknown): boolean {
|
||||
if (!(e instanceof ApiRequestError)) return false;
|
||||
return (
|
||||
e.code === 'NETWORK' ||
|
||||
e.code === 'NOT_IMPLEMENTED' ||
|
||||
e.code === 'INTERNAL' ||
|
||||
e.httpStatus === 501 ||
|
||||
e.httpStatus >= 502
|
||||
);
|
||||
}
|
||||
|
||||
// ── 메모리 내 액세스 토큰 (영속은 AuthContext가 secure-store로 관리) ──
|
||||
let accessToken: string | null = null;
|
||||
export function setAccessToken(token: string | null): void {
|
||||
accessToken = token;
|
||||
}
|
||||
export function getAccessToken(): string | null {
|
||||
return accessToken;
|
||||
}
|
||||
|
||||
interface RequestOptions {
|
||||
method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
|
||||
body?: unknown;
|
||||
signal?: AbortSignal;
|
||||
/** 인증 헤더 생략(공개 경로: login·register·forgot·reset·health). */
|
||||
anonymous?: boolean;
|
||||
}
|
||||
|
||||
async function request<T>(path: string, opts: RequestOptions = {}): Promise<T> {
|
||||
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
|
||||
if (!opts.anonymous && accessToken) headers.Authorization = `Bearer ${accessToken}`;
|
||||
|
||||
let res: Response;
|
||||
try {
|
||||
res = await fetch(`${API_BASE}${path}`, {
|
||||
method: opts.method ?? 'GET',
|
||||
headers,
|
||||
body: opts.body != null ? JSON.stringify(opts.body) : undefined,
|
||||
signal: opts.signal,
|
||||
});
|
||||
} catch (e) {
|
||||
if ((e as Error).name === 'AbortError') throw e;
|
||||
throw new ApiRequestError('NETWORK', '네트워크 연결을 확인해 주세요.', 0);
|
||||
}
|
||||
|
||||
if (res.status === 401) {
|
||||
// 토큰 만료/무효 — 메모리 토큰 정리 (라우터가 로그인으로 유도)
|
||||
accessToken = null;
|
||||
}
|
||||
|
||||
let payload: ApiResponse<T> | null = null;
|
||||
const text = await res.text();
|
||||
if (text) {
|
||||
try {
|
||||
payload = JSON.parse(text) as ApiResponse<T>;
|
||||
} catch {
|
||||
payload = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (!res.ok || (payload && payload.success === false)) {
|
||||
const err: ApiError | null = payload?.error ?? null;
|
||||
throw new ApiRequestError(
|
||||
(err?.code as ApiErrorCode) ?? 'UNKNOWN',
|
||||
err?.message ?? `요청을 처리하지 못했습니다. (${res.status})`,
|
||||
res.status,
|
||||
);
|
||||
}
|
||||
|
||||
if (!payload) {
|
||||
return undefined as unknown as T;
|
||||
}
|
||||
return payload.data as T;
|
||||
}
|
||||
|
||||
export const api = {
|
||||
get: <T>(path: string, opts?: Omit<RequestOptions, 'method' | 'body'>) =>
|
||||
request<T>(path, { ...opts, method: 'GET' }),
|
||||
post: <T>(path: string, body?: unknown, opts?: Omit<RequestOptions, 'method' | 'body'>) =>
|
||||
request<T>(path, { ...opts, method: 'POST', body }),
|
||||
put: <T>(path: string, body?: unknown, opts?: Omit<RequestOptions, 'method' | 'body'>) =>
|
||||
request<T>(path, { ...opts, method: 'PUT', body }),
|
||||
};
|
||||
62
mobile/lib/auth.ts
Normal file
62
mobile/lib/auth.ts
Normal file
@ -0,0 +1,62 @@
|
||||
/*
|
||||
* 인증 API 래퍼 — 로그인(2FA 대응)·회원가입·비밀번호 찾기/초기화.
|
||||
* 근거: 01_backend_contracts.md §2 · 04_backend_public_auth.md.
|
||||
* 비밀번호는 요청 body로만 전송하고 어디에도 저장하지 않는다.
|
||||
*/
|
||||
import { api, ApiRequestError } from './api';
|
||||
import type {
|
||||
LoginResponse,
|
||||
MessageResponse,
|
||||
RegisterRequest,
|
||||
RegisterResponse,
|
||||
} from './types';
|
||||
|
||||
export interface LoginParams {
|
||||
email: string;
|
||||
password: string;
|
||||
/** 2차 인증 코드 — OTP_REQUIRED 재시도 시 동봉(백엔드 TOTP 방식). */
|
||||
otp?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 로그인. 성공 시 LoginResponse.
|
||||
* 2FA: 백엔드가 OTP_REQUIRED(401)를 반환하면 호출부가 OTP 입력 UI를 띄우고
|
||||
* otp를 포함해 재호출한다. OTP_INVALID면 코드 오류로 재입력 유도.
|
||||
*/
|
||||
export async function login(params: LoginParams): Promise<LoginResponse> {
|
||||
const body: Record<string, unknown> = {
|
||||
email: params.email.trim(),
|
||||
password: params.password,
|
||||
};
|
||||
if (params.otp) body.otp = params.otp.trim();
|
||||
return api.post<LoginResponse>('/api/auth/login', body, { anonymous: true });
|
||||
}
|
||||
|
||||
/** OTP_REQUIRED 여부 판정 헬퍼(로그인 2단계 분기용). */
|
||||
export function isOtpRequired(e: unknown): boolean {
|
||||
return e instanceof ApiRequestError && e.code === 'OTP_REQUIRED';
|
||||
}
|
||||
|
||||
export async function register(req: RegisterRequest): Promise<RegisterResponse> {
|
||||
return api.post<RegisterResponse>('/api/auth/register', req, { anonymous: true });
|
||||
}
|
||||
|
||||
export async function forgotPassword(email: string): Promise<MessageResponse> {
|
||||
return api.post<MessageResponse>(
|
||||
'/api/auth/password/forgot',
|
||||
{ email: email.trim() },
|
||||
{ anonymous: true },
|
||||
);
|
||||
}
|
||||
|
||||
export async function resetPassword(
|
||||
email: string,
|
||||
code: string,
|
||||
newPassword: string,
|
||||
): Promise<MessageResponse> {
|
||||
return api.post<MessageResponse>(
|
||||
'/api/auth/password/reset',
|
||||
{ email: email.trim(), code: code.trim(), newPassword },
|
||||
{ anonymous: true },
|
||||
);
|
||||
}
|
||||
15
mobile/lib/config.ts
Normal file
15
mobile/lib/config.ts
Normal file
@ -0,0 +1,15 @@
|
||||
/*
|
||||
* 런타임 설정 — API base URL.
|
||||
* 우선순위: EXPO_PUBLIC_API_BASE(env) > app.json extra.apiBase > 기본값.
|
||||
* 시크릿은 여기에 넣지 않는다(하드코딩 금지). base URL은 공개 정보.
|
||||
*/
|
||||
import Constants from 'expo-constants';
|
||||
|
||||
const extra = (Constants.expoConfig?.extra ?? {}) as { apiBase?: string };
|
||||
|
||||
export const API_BASE: string =
|
||||
process.env.EXPO_PUBLIC_API_BASE ?? extra.apiBase ?? 'https://kintex.zioinfo.co.kr';
|
||||
|
||||
/** AI 생성 이미지 기본 고지문 (백엔드 watermarkText 부재 시 폴백 — 계약 §0-3). */
|
||||
export const AI_IMAGE_NOTICE =
|
||||
'AI 생성 예상 이미지 — 실제 시공 결과와 다를 수 있습니다';
|
||||
44
mobile/lib/secureStore.ts
Normal file
44
mobile/lib/secureStore.ts
Normal file
@ -0,0 +1,44 @@
|
||||
/*
|
||||
* 안전 저장소 래퍼 — JWT는 expo-secure-store(네이티브 keychain/keystore)에 저장.
|
||||
* 웹/미지원 플랫폼은 AsyncStorage로 폴백. 비밀번호는 절대 저장하지 않는다.
|
||||
* "아이디 기억"(이메일)은 비민감 정보이므로 AsyncStorage 사용.
|
||||
*/
|
||||
import * as SecureStore from 'expo-secure-store';
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import { Platform } from 'react-native';
|
||||
|
||||
const useSecure = Platform.OS !== 'web';
|
||||
|
||||
export async function secureSet(key: string, value: string): Promise<void> {
|
||||
if (useSecure) {
|
||||
await SecureStore.setItemAsync(key, value);
|
||||
} else {
|
||||
await AsyncStorage.setItem(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
export async function secureGet(key: string): Promise<string | null> {
|
||||
if (useSecure) {
|
||||
return SecureStore.getItemAsync(key);
|
||||
}
|
||||
return AsyncStorage.getItem(key);
|
||||
}
|
||||
|
||||
export async function secureDelete(key: string): Promise<void> {
|
||||
if (useSecure) {
|
||||
await SecureStore.deleteItemAsync(key);
|
||||
} else {
|
||||
await AsyncStorage.removeItem(key);
|
||||
}
|
||||
}
|
||||
|
||||
// 비민감 저장소 (아이디 기억 등)
|
||||
export async function prefSet(key: string, value: string): Promise<void> {
|
||||
await AsyncStorage.setItem(key, value);
|
||||
}
|
||||
export async function prefGet(key: string): Promise<string | null> {
|
||||
return AsyncStorage.getItem(key);
|
||||
}
|
||||
export async function prefDelete(key: string): Promise<void> {
|
||||
await AsyncStorage.removeItem(key);
|
||||
}
|
||||
105
mobile/lib/types.ts
Normal file
105
mobile/lib/types.ts
Normal file
@ -0,0 +1,105 @@
|
||||
/*
|
||||
* 백엔드 API 계약 타입 (단일 출처: _workspace/01_backend_contracts.md · 04_backend_public_auth.md)
|
||||
* ★ 백엔드 응답 shape과 정확히 일치해야 한다. 불일치 발견 시 _workspace/에 기록·보고.
|
||||
*/
|
||||
|
||||
// ── 0-1 응답 봉투 ──
|
||||
export interface ApiError {
|
||||
code: ApiErrorCode;
|
||||
message: string;
|
||||
}
|
||||
export interface ApiResponse<T> {
|
||||
success: boolean;
|
||||
data: T | null;
|
||||
error: ApiError | null;
|
||||
}
|
||||
export interface PageResponse<T> {
|
||||
items: T[];
|
||||
page: number;
|
||||
size: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
// ── 0-2 오류 코드 ──
|
||||
export type ApiErrorCode =
|
||||
| 'VALIDATION'
|
||||
| 'UNAUTHORIZED'
|
||||
| 'FORBIDDEN'
|
||||
| 'NOT_FOUND'
|
||||
| 'CONFLICT'
|
||||
| 'EMAIL_TAKEN'
|
||||
| 'COMPLIANCE_BLOCKED'
|
||||
| 'RENDER_QUOTA_EXCEEDED'
|
||||
| 'NOT_REGISTERED_COMPANY'
|
||||
| 'NOT_IMPLEMENTED'
|
||||
| 'ACCOUNT_LOCKED'
|
||||
| 'OTP_REQUIRED'
|
||||
| 'OTP_INVALID'
|
||||
| 'INTERNAL';
|
||||
|
||||
// ── 0-5 역할 ──
|
||||
export type EventRole = 'ORGANIZER' | 'EXHIBITOR' | 'CONTRACTOR' | 'HALL_MANAGER';
|
||||
|
||||
// ── 2. 인증·워크스페이스 (SCR-01) ──
|
||||
export interface WorkspaceDto {
|
||||
eventId: string;
|
||||
eventName: string;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
hallLabel: string;
|
||||
myRole: EventRole;
|
||||
dday: number;
|
||||
}
|
||||
export interface AuthUser {
|
||||
userId: string;
|
||||
displayName: string;
|
||||
hallManager: boolean;
|
||||
}
|
||||
export interface LoginResponse {
|
||||
accessToken: string;
|
||||
expiresInSeconds: number;
|
||||
user: AuthUser;
|
||||
workspaces: WorkspaceDto[];
|
||||
}
|
||||
|
||||
// ── 공개 인증 (04_backend_public_auth.md) ──
|
||||
export interface RegisterRequest {
|
||||
email: string;
|
||||
displayName: string;
|
||||
password: string;
|
||||
companyName?: string;
|
||||
inviteCode?: string;
|
||||
}
|
||||
export interface RegisterResponse {
|
||||
userId: string;
|
||||
email: string;
|
||||
displayName: string;
|
||||
joinedEvent: string | null;
|
||||
}
|
||||
export interface MessageResponse {
|
||||
message: string;
|
||||
}
|
||||
|
||||
// ── 6. M5 나노바나나 RenderJob (SCR-06/12) ──
|
||||
export type RenderJobStatus = 'QUEUED' | 'RUNNING' | 'DONE' | 'FAILED';
|
||||
export interface RenderJobDto {
|
||||
jobId: string;
|
||||
boothId: string;
|
||||
shotPreset: string; // S1..S7
|
||||
status: RenderJobStatus;
|
||||
imageUrl: string | null;
|
||||
schemaHash: string | null;
|
||||
modelVersion: string | null;
|
||||
/** AI 생성 이미지 고지 — 항상 true (제거 불가). */
|
||||
watermarkRequired: boolean;
|
||||
watermarkText: string;
|
||||
notice?: string;
|
||||
createdAt?: string;
|
||||
}
|
||||
|
||||
// ── 헬스체크 ──
|
||||
export interface HealthDto {
|
||||
status: string;
|
||||
service: string;
|
||||
time: string;
|
||||
}
|
||||
38
mobile/package.json
Normal file
38
mobile/package.json
Normal file
@ -0,0 +1,38 @@
|
||||
{
|
||||
"name": "kintex-mobile",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"main": "expo-router/entry",
|
||||
"scripts": {
|
||||
"start": "expo start",
|
||||
"android": "expo start --android",
|
||||
"ios": "expo start --ios",
|
||||
"web": "expo start --web",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@expo/vector-icons": "^14.0.3",
|
||||
"@react-native-async-storage/async-storage": "1.23.1",
|
||||
"expo": "^51.0.39",
|
||||
"expo-constants": "~16.0.2",
|
||||
"expo-image-picker": "~15.0.7",
|
||||
"expo-linking": "~6.3.1",
|
||||
"expo-router": "~3.5.23",
|
||||
"expo-secure-store": "~13.0.2",
|
||||
"expo-splash-screen": "~0.27.7",
|
||||
"expo-status-bar": "~1.12.1",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-native": "0.74.5",
|
||||
"react-native-gesture-handler": "~2.16.1",
|
||||
"react-native-safe-area-context": "4.10.5",
|
||||
"react-native-screens": "3.31.1",
|
||||
"react-native-svg": "15.2.0",
|
||||
"react-native-web": "~0.19.10"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.20.0",
|
||||
"@types/react": "~18.2.45",
|
||||
"typescript": "~5.3.3"
|
||||
}
|
||||
}
|
||||
40
mobile/theme/colors.ts
Normal file
40
mobile/theme/colors.ts
Normal file
@ -0,0 +1,40 @@
|
||||
/*
|
||||
* KINTEX 디자인 토큰 — 컬러 팔레트
|
||||
* 단일 출처: docs/design.md §1-2 (Precision Enterprise AI 정합).
|
||||
* 상충 시 design.md가 권위. 신규 토큰 추가 금지.
|
||||
*/
|
||||
export const colors = {
|
||||
// 브랜드 블루 (킨텍스 CI)
|
||||
primary700: '#004C86', // 헤더, 강조 텍스트, hover
|
||||
primary600: '#0066B3', // 주 브랜드 블루 — 주요 버튼, 활성 탭, 링크
|
||||
primary100: '#D9EAF7', // 선택 배경, 활성 항목
|
||||
primary050: '#EFF6FC', // 서브 배경, 테이블 호버
|
||||
|
||||
// AI 전용 (보라)
|
||||
aiAccent: '#6D4AFF', // "AI 자동배치", "예상 사진 생성", AI 라벨
|
||||
aiSurface: '#F5F3FF', // AI 카드 배경 (design.md §1-2)
|
||||
|
||||
// 상태
|
||||
success: '#0E8A5F', // 승인 완료, 규정 통과
|
||||
warning: '#B45309', // 마감 임박(D-3), 확인 필요
|
||||
error: '#D92D20', // 규정 위반, 반려, 마감 초과
|
||||
|
||||
// 중립
|
||||
neutral900: '#101828', // 본문 제목
|
||||
neutral700: '#344054', // 본문 텍스트
|
||||
neutral500: '#667085', // 보조 텍스트, 플레이스홀더
|
||||
neutral200: '#E4E7EC', // 구분선, 카드 테두리
|
||||
neutral050: '#F9FAFB', // 앱 배경
|
||||
|
||||
// 캔버스 (도면 다크 서피스)
|
||||
canvasBg: '#1C2536',
|
||||
|
||||
// 배선 오버레이 (S6)
|
||||
wireElectric: '#EF4444', // 전기 (적)
|
||||
wireNetwork: '#3B82F6', // 네트워크/전화 (청)
|
||||
wirePlumbing: '#22C55E', // 급배수·압축공기 (녹)
|
||||
|
||||
white: '#FFFFFF',
|
||||
} as const;
|
||||
|
||||
export type ColorToken = keyof typeof colors;
|
||||
3
mobile/theme/index.ts
Normal file
3
mobile/theme/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export { colors } from './colors';
|
||||
export type { ColorToken } from './colors';
|
||||
export { radius, spacing, touch, type } from './typography';
|
||||
32
mobile/theme/typography.ts
Normal file
32
mobile/theme/typography.ts
Normal file
@ -0,0 +1,32 @@
|
||||
/*
|
||||
* KINTEX 타이포/스페이싱/라운드 토큰 — docs/design.md §1-3, §1-4.
|
||||
* 서체: Pretendard (모바일은 시스템 기본으로 폴백; 별도 폰트 번들 없이 웨이트만 반영).
|
||||
* 모바일 터치 타깃 48px, radius 최대 8px(산업·건축 톤).
|
||||
*/
|
||||
export const radius = {
|
||||
sm: 4, // 버튼·인풋
|
||||
md: 8, // 카드·모달·섹션 (최대)
|
||||
pill: 9999, // 상태/AI 칩
|
||||
} as const;
|
||||
|
||||
export const spacing = {
|
||||
xs: 4,
|
||||
sm: 8,
|
||||
md: 16, // 모바일 페이지 마진
|
||||
lg: 24,
|
||||
xl: 32,
|
||||
} as const;
|
||||
|
||||
export const touch = {
|
||||
min: 48, // 모바일 터치 타깃 권위값 (design.md §1-3)
|
||||
} as const;
|
||||
|
||||
// 스케일 (fontSize / lineHeight / fontWeight)
|
||||
export const type = {
|
||||
display: { fontSize: 28, lineHeight: 36, fontWeight: '700' as const },
|
||||
h1: { fontSize: 24, lineHeight: 32, fontWeight: '700' as const },
|
||||
h2: { fontSize: 20, lineHeight: 28, fontWeight: '600' as const },
|
||||
h3: { fontSize: 16, lineHeight: 24, fontWeight: '600' as const },
|
||||
body: { fontSize: 14, lineHeight: 22, fontWeight: '400' as const },
|
||||
caption: { fontSize: 12, lineHeight: 18, fontWeight: '400' as const },
|
||||
} as const;
|
||||
17
mobile/tsconfig.json
Normal file
17
mobile/tsconfig.json
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"extends": "expo/tsconfig.base",
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".expo/types/**/*.ts",
|
||||
"expo-env.d.ts"
|
||||
],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,20 @@
|
||||
import type { ComponentType } from 'react';
|
||||
import { NavLink, Outlet } from 'react-router-dom';
|
||||
import { useAuthStore } from '../../store/authStore';
|
||||
import { DdayChip } from '../ui/Badge';
|
||||
import {
|
||||
IconAnalytics,
|
||||
IconBell,
|
||||
IconCalendar,
|
||||
IconDashboard,
|
||||
IconDocument,
|
||||
IconExhibitors,
|
||||
IconFloorplan,
|
||||
IconOperations,
|
||||
IconSettings,
|
||||
IconSettlement,
|
||||
type IconProps,
|
||||
} from '../ui/icons';
|
||||
import './app-shell.css';
|
||||
|
||||
/**
|
||||
@ -8,16 +22,16 @@ import './app-shell.css';
|
||||
* 플레이스홀더 항목(대시보드·플로어플랜 등)은 행사 스코프(:eventId) 라우트라 셸 밖에서 진입한다.
|
||||
* 신규 4항목(경영분석·현장운영·전시 일정·관리자)은 테넌트 전역 라우트로 직접 배선.
|
||||
*/
|
||||
const NAV_ITEMS: { key: string; label: string; icon: string; to?: string }[] = [
|
||||
{ key: 'dashboard', label: '대시보드', icon: '▦' },
|
||||
{ key: 'floorplan', label: '플로어플랜 스튜디오', icon: '◱' },
|
||||
{ key: 'exhibitors', label: '참가업체 관리', icon: '☷' },
|
||||
{ key: 'schedule', label: '전시 일정', icon: '▤', to: '/schedule' },
|
||||
{ key: 'operations', label: '현장운영', icon: '◐', to: '/ops/operations' },
|
||||
{ key: 'analytics', label: '경영분석', icon: '▨', to: '/analytics' },
|
||||
{ key: 'documents', label: '서류·마일스톤', icon: '❒' },
|
||||
{ key: 'settlement', label: '정산', icon: '₩' },
|
||||
{ key: 'admin', label: '관리자', icon: '⚙', to: '/admin' },
|
||||
const NAV_ITEMS: { key: string; label: string; Icon: ComponentType<IconProps>; to?: string }[] = [
|
||||
{ key: 'dashboard', label: '대시보드', Icon: IconDashboard },
|
||||
{ key: 'floorplan', label: '플로어플랜 스튜디오', Icon: IconFloorplan },
|
||||
{ key: 'exhibitors', label: '참가업체 관리', Icon: IconExhibitors },
|
||||
{ key: 'schedule', label: '전시 일정', Icon: IconCalendar, to: '/schedule' },
|
||||
{ key: 'operations', label: '현장운영', Icon: IconOperations, to: '/ops/operations' },
|
||||
{ key: 'analytics', label: '경영분석', Icon: IconAnalytics, to: '/analytics' },
|
||||
{ key: 'documents', label: '서류·마일스톤', Icon: IconDocument },
|
||||
{ key: 'settlement', label: '정산', Icon: IconSettlement },
|
||||
{ key: 'admin', label: '관리자', Icon: IconSettings, to: '/admin' },
|
||||
];
|
||||
|
||||
/**
|
||||
@ -57,12 +71,12 @@ export function AppShell() {
|
||||
`kx-shell__nav-link ${isActive ? 'is-active' : ''}`
|
||||
}
|
||||
>
|
||||
<span className="kx-shell__nav-icon" aria-hidden="true">{item.icon}</span>
|
||||
<span className="kx-shell__nav-icon"><item.Icon size={20} /></span>
|
||||
{item.label}
|
||||
</NavLink>
|
||||
) : (
|
||||
<a className="kx-shell__nav-link" href="#" aria-disabled="true">
|
||||
<span className="kx-shell__nav-icon" aria-hidden="true">{item.icon}</span>
|
||||
<span className="kx-shell__nav-icon"><item.Icon size={20} /></span>
|
||||
{item.label}
|
||||
</a>
|
||||
)}
|
||||
@ -91,7 +105,7 @@ export function AppShell() {
|
||||
<div className="kx-shell__topbar-right">
|
||||
{workspace && <DdayChip dday={workspace.dday} />}
|
||||
<button className="kx-shell__icon-btn" type="button" aria-label="알림">
|
||||
◔
|
||||
<IconBell size={20} />
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@ -97,7 +97,9 @@
|
||||
}
|
||||
.kx-shell__nav-icon {
|
||||
width: 20px;
|
||||
text-align: center;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.kx-shell__user {
|
||||
@ -174,6 +176,9 @@
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--color-neutral-500);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.kx-shell__icon-btn:hover {
|
||||
background: var(--color-neutral-100);
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import type { RenderJobDto } from '../../api/types';
|
||||
import { AiLabel } from './Badge';
|
||||
import { IconSpark } from './icons';
|
||||
import './ai-image.css';
|
||||
|
||||
/*
|
||||
@ -68,7 +69,7 @@ export function AiImage({
|
||||
|
||||
{sample && onGenerate && (
|
||||
<button type="button" className="kx-aiimg__gen" onClick={onGenerate}>
|
||||
<span aria-hidden="true">✦</span> 이 뷰 생성
|
||||
<span aria-hidden="true"><IconSpark size={14} /></span> 이 뷰 생성
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { IconSpark } from './icons';
|
||||
import './badge.css';
|
||||
|
||||
// ── StatusBadge (design.md §1-4) ──
|
||||
@ -54,7 +55,7 @@ export function DdayChip({ dday }: { dday: number }) {
|
||||
export function AiLabel({ children = 'AI 초안' }: { children?: ReactNode }) {
|
||||
return (
|
||||
<span className="kx-badge kx-badge--ai">
|
||||
<span className="kx-badge__spark" aria-hidden="true">✦</span>
|
||||
<span className="kx-badge__spark" aria-hidden="true"><IconSpark size={12} /></span>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
import type { KeyboardEvent, PointerEvent } from 'react';
|
||||
import { IconCompareArrows } from './icons';
|
||||
import './compare-slider.css';
|
||||
|
||||
/*
|
||||
@ -125,7 +126,7 @@ export function CompareSlider({
|
||||
onKeyDown={onKeyDown}
|
||||
>
|
||||
<span className="kx-compare__handle-grip" aria-hidden="true">
|
||||
‹›
|
||||
<IconCompareArrows size={18} />
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
446
src/frontend/src/components/ui/icons.tsx
Normal file
446
src/frontend/src/components/ui/icons.tsx
Normal file
@ -0,0 +1,446 @@
|
||||
/**
|
||||
* 시스템 아이콘 라이브러리 (design.md §3 SCR-17 항목6).
|
||||
*
|
||||
* ── 표준 ──
|
||||
* - Stitch _icons 세트(24×24 viewBox · stroke=currentColor · stroke-width 2 · fill none)를 표준으로 삼는다.
|
||||
* - 7종(Dashboard·Calendar·Operations·Analytics·Search·Menu·AiBot)은 Stitch 원본 이식,
|
||||
* 나머지는 동일 스타일로 자체 제작.
|
||||
* - currentColor 유지 → 테마·상태색을 CSS `color`로 상속(다크/라이트·상태배지 대응).
|
||||
* - 기본 장식용(aria-hidden). 의미 전달이 필요하면 `title` prop으로 <title> 삽입(role=img 자동).
|
||||
*
|
||||
* 사용: <IconDashboard /> · <IconCheck size={16} /> · <IconWarning title="경고" />
|
||||
*/
|
||||
import type { ReactNode, SVGProps } from 'react';
|
||||
|
||||
export interface IconProps extends Omit<SVGProps<SVGSVGElement>, 'width' | 'height'> {
|
||||
/** 한 변 픽셀 크기(정사각). 기본 20. */
|
||||
size?: number;
|
||||
/** 접근성 라벨 — 지정 시 role=img + <title> 삽입, 미지정 시 aria-hidden 장식. */
|
||||
title?: string;
|
||||
}
|
||||
|
||||
/** 공통 SVG 래퍼 — 표준 속성(viewBox 24·fill none·currentColor)과 접근성 처리. */
|
||||
function Svg({ size = 20, title, children, ...rest }: IconProps & { children: ReactNode }) {
|
||||
const a11y = title
|
||||
? { role: 'img' as const, 'aria-label': title }
|
||||
: { 'aria-hidden': true as const, focusable: false as const };
|
||||
return (
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...a11y}
|
||||
{...rest}
|
||||
>
|
||||
{title ? <title>{title}</title> : null}
|
||||
{children}
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
const S = { stroke: 'currentColor', strokeWidth: 2 } as const;
|
||||
const SR = { ...S, strokeLinecap: 'round' as const };
|
||||
const SRJ = { ...SR, strokeLinejoin: 'round' as const };
|
||||
|
||||
/* ─────────────────────────────────────────────────────────
|
||||
* Stitch 원본 7종 (_icons)
|
||||
* ───────────────────────────────────────────────────────── */
|
||||
|
||||
/** ▦ 대시보드 (Stitch icon_dashboard). */
|
||||
export function IconDashboard(p: IconProps) {
|
||||
return (
|
||||
<Svg {...p}>
|
||||
<rect x="3" y="3" width="18" height="18" rx="2" {...S} />
|
||||
<path d="M3 9H21" {...S} />
|
||||
<path d="M9 21V9" {...S} />
|
||||
</Svg>
|
||||
);
|
||||
}
|
||||
|
||||
/** ▤ 캘린더/일정 (Stitch icon_calendar). */
|
||||
export function IconCalendar(p: IconProps) {
|
||||
return (
|
||||
<Svg {...p}>
|
||||
<rect x="3" y="4" width="18" height="16" rx="2" {...S} />
|
||||
<path d="M16 2V6" {...S} />
|
||||
<path d="M8 2V6" {...S} />
|
||||
<path d="M3 10H21" {...S} />
|
||||
</Svg>
|
||||
);
|
||||
}
|
||||
|
||||
/** ◐ 현장운영 (Stitch icon_operations — 라디오 웨이브 + 핀). */
|
||||
export function IconOperations(p: IconProps) {
|
||||
return (
|
||||
<Svg {...p}>
|
||||
<path d="M2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12" {...SR} />
|
||||
<path d="M5 12C5 8.13401 8.13401 5 12 5C15.866 5 19 8.13401 19 12" {...SR} />
|
||||
<circle cx="12" cy="12" r="3" {...S} />
|
||||
<path d="M12 15V22" {...S} />
|
||||
</Svg>
|
||||
);
|
||||
}
|
||||
|
||||
/** ▨ 경영분석 (Stitch icon_analytics — 막대 차트). */
|
||||
export function IconAnalytics(p: IconProps) {
|
||||
return (
|
||||
<Svg {...p}>
|
||||
<path d="M18 20V10" {...SR} />
|
||||
<path d="M12 20V4" {...SR} />
|
||||
<path d="M6 20V14" {...SR} />
|
||||
</Svg>
|
||||
);
|
||||
}
|
||||
|
||||
/** ⌕ 검색 (Stitch icon_search). */
|
||||
export function IconSearch(p: IconProps) {
|
||||
return (
|
||||
<Svg {...p}>
|
||||
<circle cx="11" cy="11" r="7" {...S} />
|
||||
<path d="M20 20L16 16" {...SR} />
|
||||
</Svg>
|
||||
);
|
||||
}
|
||||
|
||||
/** ☰ 메뉴 (Stitch icon_menu). */
|
||||
export function IconMenu(p: IconProps) {
|
||||
return (
|
||||
<Svg {...p}>
|
||||
<path d="M3 12H21" {...SR} />
|
||||
<path d="M3 6H21" {...SR} />
|
||||
<path d="M3 18H21" {...SR} />
|
||||
</Svg>
|
||||
);
|
||||
}
|
||||
|
||||
/** AI 봇 (Stitch icon_ai_bot — currentColor 정규화). */
|
||||
export function IconAiBot(p: IconProps) {
|
||||
return (
|
||||
<Svg {...p}>
|
||||
<rect x="4" y="7" width="16" height="12" rx="3" {...S} />
|
||||
<circle cx="9" cy="13" r="1" fill="currentColor" />
|
||||
<circle cx="15" cy="13" r="1" fill="currentColor" />
|
||||
<path d="M10 16C10 16 11 17 12 17C13 17 14 16 14 16" {...SR} />
|
||||
<path d="M12 7V4" {...SR} />
|
||||
<circle cx="12" cy="3" r="1" fill="currentColor" />
|
||||
<path d="M4 11H2.5V15H4" {...SR} />
|
||||
<path d="M20 11H21.5V15H20" {...SR} />
|
||||
</Svg>
|
||||
);
|
||||
}
|
||||
|
||||
/* ─────────────────────────────────────────────────────────
|
||||
* 자체 제작 (동일 스타일)
|
||||
* ───────────────────────────────────────────────────────── */
|
||||
|
||||
/** ◱ 플로어플랜 스튜디오 (부스 그리드 + 강조 셀). */
|
||||
export function IconFloorplan(p: IconProps) {
|
||||
return (
|
||||
<Svg {...p}>
|
||||
<rect x="3" y="3" width="18" height="18" rx="2" {...S} />
|
||||
<path d="M3 15H21" {...S} />
|
||||
<path d="M15 3V21" {...S} />
|
||||
<rect x="6" y="6" width="4" height="4" rx="0.5" fill="currentColor" />
|
||||
</Svg>
|
||||
);
|
||||
}
|
||||
|
||||
/** ☷ 참가업체 관리 (사람 그룹). */
|
||||
export function IconExhibitors(p: IconProps) {
|
||||
return (
|
||||
<Svg {...p}>
|
||||
<circle cx="9" cy="8" r="3.2" {...S} />
|
||||
<path d="M3.5 20C3.5 16.9 6 14.5 9 14.5C12 14.5 14.5 16.9 14.5 20" {...SR} />
|
||||
<path d="M16 5.2C17.6 5.6 18.8 7 18.8 8.7C18.8 10.4 17.6 11.8 16 12.2" {...SR} />
|
||||
<path d="M17 14.8C19.3 15.4 21 17.4 21 20" {...SR} />
|
||||
</Svg>
|
||||
);
|
||||
}
|
||||
|
||||
/** ❒ 서류·문서. */
|
||||
export function IconDocument(p: IconProps) {
|
||||
return (
|
||||
<Svg {...p}>
|
||||
<path d="M13 3H6C4.9 3 4 3.9 4 5V19C4 20.1 4.9 21 6 21H18C19.1 21 20 20.1 20 19V10L13 3Z" {...SRJ} />
|
||||
<path d="M13 3V10H20" {...SRJ} />
|
||||
<path d="M8 14H16" {...SR} />
|
||||
<path d="M8 17H13" {...SR} />
|
||||
</Svg>
|
||||
);
|
||||
}
|
||||
|
||||
/** ₩ 정산 (원화 원). */
|
||||
export function IconSettlement(p: IconProps) {
|
||||
return (
|
||||
<Svg {...p}>
|
||||
<circle cx="12" cy="12" r="9" {...S} />
|
||||
<path d="M8 8L10 15L12 9L14 15L16 8" {...SRJ} />
|
||||
<path d="M7 11.5H17" {...SR} />
|
||||
<path d="M7 13.8H17" {...SR} />
|
||||
</Svg>
|
||||
);
|
||||
}
|
||||
|
||||
/** ⚙ 관리자·설정 (기어). */
|
||||
export function IconSettings(p: IconProps) {
|
||||
return (
|
||||
<Svg {...p}>
|
||||
<circle cx="12" cy="12" r="3" {...S} />
|
||||
<path
|
||||
d="M12 2.5L13.4 5.1C13.9 5.2 14.4 5.4 14.9 5.7L17.7 4.9L19.1 7.3L17.1 9.3C17.2 9.9 17.2 10.5 17.1 11.1L19.1 13.1L17.7 15.5L14.9 14.7C14.4 15 13.9 15.2 13.4 15.3L12 18M12 2.5L10.6 5.1C10.1 5.2 9.6 5.4 9.1 5.7L6.3 4.9L4.9 7.3L6.9 9.3C6.8 9.9 6.8 10.5 6.9 11.1L4.9 13.1L6.3 15.5L9.1 14.7C9.6 15 10.1 15.2 10.6 15.3L12 18"
|
||||
{...SRJ}
|
||||
transform="translate(0 1.5)"
|
||||
/>
|
||||
</Svg>
|
||||
);
|
||||
}
|
||||
|
||||
/** ◔ 알림 벨. */
|
||||
export function IconBell(p: IconProps) {
|
||||
return (
|
||||
<Svg {...p}>
|
||||
<path d="M18 8C18 6.4 17.4 4.9 16.2 3.8C15.1 2.6 13.6 2 12 2C10.4 2 8.9 2.6 7.8 3.8C6.6 4.9 6 6.4 6 8C6 15 3 17 3 17H21C21 17 18 15 18 8Z" {...SRJ} />
|
||||
<path d="M13.7 21C13.5 21.3 13.3 21.6 13 21.8C12.7 21.9 12.3 22 12 22C11.7 22 11.3 21.9 11 21.8C10.7 21.6 10.5 21.3 10.3 21" {...SR} />
|
||||
</Svg>
|
||||
);
|
||||
}
|
||||
|
||||
/** ✦ AI 스파크(4각별) — AI 액션·라벨 액센트. */
|
||||
export function IconSpark(p: IconProps) {
|
||||
return (
|
||||
<Svg {...p}>
|
||||
<path d="M12 3C12 6 13 9 18 12C13 15 12 18 12 21C12 18 11 15 6 12C11 9 12 6 12 3Z" {...SRJ} />
|
||||
</Svg>
|
||||
);
|
||||
}
|
||||
|
||||
/** ⚠ △ 경고 (삼각형 + 느낌표). */
|
||||
export function IconWarning(p: IconProps) {
|
||||
return (
|
||||
<Svg {...p}>
|
||||
<path d="M10.3 3.9L2.5 17.5C2 18.3 2.6 19.4 3.6 19.4H20.4C21.4 19.4 22 18.3 21.5 17.5L13.7 3.9C13.2 3 11.9 3 11.4 3.9L10.3 3.9Z" {...SRJ} />
|
||||
<path d="M12 9V13" {...SR} />
|
||||
<path d="M12 16.5H12.01" {...SR} />
|
||||
</Svg>
|
||||
);
|
||||
}
|
||||
|
||||
/** ✓ 체크. */
|
||||
export function IconCheck(p: IconProps) {
|
||||
return (
|
||||
<Svg {...p}>
|
||||
<path d="M4 12.5L9 17.5L20 6.5" {...SRJ} />
|
||||
</Svg>
|
||||
);
|
||||
}
|
||||
|
||||
/** ✓ 원형 체크(성공 배지). */
|
||||
export function IconCheckCircle(p: IconProps) {
|
||||
return (
|
||||
<Svg {...p}>
|
||||
<circle cx="12" cy="12" r="9" {...S} />
|
||||
<path d="M8 12L11 15L16 9" {...SRJ} />
|
||||
</Svg>
|
||||
);
|
||||
}
|
||||
|
||||
/** ✕ 닫기. */
|
||||
export function IconClose(p: IconProps) {
|
||||
return (
|
||||
<Svg {...p}>
|
||||
<path d="M6 6L18 18" {...SR} />
|
||||
<path d="M18 6L6 18" {...SR} />
|
||||
</Svg>
|
||||
);
|
||||
}
|
||||
|
||||
/** ⛔ 차단(원 + 사선). */
|
||||
export function IconBlock(p: IconProps) {
|
||||
return (
|
||||
<Svg {...p}>
|
||||
<circle cx="12" cy="12" r="9" {...S} />
|
||||
<path d="M5.6 5.6L18.4 18.4" {...SR} />
|
||||
</Svg>
|
||||
);
|
||||
}
|
||||
|
||||
/** › 오른쪽 셰브런. */
|
||||
export function IconChevronRight(p: IconProps) {
|
||||
return (
|
||||
<Svg {...p}>
|
||||
<path d="M9 6L15 12L9 18" {...SRJ} />
|
||||
</Svg>
|
||||
);
|
||||
}
|
||||
|
||||
/** ‹ 왼쪽 셰브런. */
|
||||
export function IconChevronLeft(p: IconProps) {
|
||||
return (
|
||||
<Svg {...p}>
|
||||
<path d="M15 6L9 12L15 18" {...SRJ} />
|
||||
</Svg>
|
||||
);
|
||||
}
|
||||
|
||||
/** ▾ 아래쪽 셰브런(아코디언 열림). */
|
||||
export function IconChevronDown(p: IconProps) {
|
||||
return (
|
||||
<Svg {...p}>
|
||||
<path d="M6 9L12 15L18 9" {...SRJ} />
|
||||
</Svg>
|
||||
);
|
||||
}
|
||||
|
||||
/** ‹› 좌우 화살촉(비교 슬라이더 그립). */
|
||||
export function IconCompareArrows(p: IconProps) {
|
||||
return (
|
||||
<Svg {...p}>
|
||||
<path d="M10 8L6 12L10 16" {...SRJ} />
|
||||
<path d="M14 8L18 12L14 16" {...SRJ} />
|
||||
</Svg>
|
||||
);
|
||||
}
|
||||
|
||||
/** ← 왼쪽 화살표(뒤로). */
|
||||
export function IconArrowLeft(p: IconProps) {
|
||||
return (
|
||||
<Svg {...p}>
|
||||
<path d="M19 12H5" {...SR} />
|
||||
<path d="M11 6L5 12L11 18" {...SRJ} />
|
||||
</Svg>
|
||||
);
|
||||
}
|
||||
|
||||
/** → 오른쪽 화살표(진행·CTA). */
|
||||
export function IconArrowRight(p: IconProps) {
|
||||
return (
|
||||
<Svg {...p}>
|
||||
<path d="M5 12H19" {...SR} />
|
||||
<path d="M13 6L19 12L13 18" {...SRJ} />
|
||||
</Svg>
|
||||
);
|
||||
}
|
||||
|
||||
/** ▲ 위 삼각(증가 델타). */
|
||||
export function IconTrendUp(p: IconProps) {
|
||||
return (
|
||||
<Svg {...p}>
|
||||
<path d="M4 17L10 11L14 15L20 8" {...SRJ} />
|
||||
<path d="M20 8H15" {...SR} />
|
||||
<path d="M20 8V13" {...SR} />
|
||||
</Svg>
|
||||
);
|
||||
}
|
||||
|
||||
/** ▼ 아래 삼각(감소 델타). */
|
||||
export function IconTrendDown(p: IconProps) {
|
||||
return (
|
||||
<Svg {...p}>
|
||||
<path d="M4 7L10 13L14 9L20 16" {...SRJ} />
|
||||
<path d="M20 16H15" {...SR} />
|
||||
<path d="M20 16V11" {...SR} />
|
||||
</Svg>
|
||||
);
|
||||
}
|
||||
|
||||
/** ⭱ 업로드. */
|
||||
export function IconUpload(p: IconProps) {
|
||||
return (
|
||||
<Svg {...p}>
|
||||
<path d="M4 16V19C4 20.1 4.9 21 6 21H18C19.1 21 20 20.1 20 19V16" {...SR} />
|
||||
<path d="M12 15V3" {...SR} />
|
||||
<path d="M7 8L12 3L17 8" {...SRJ} />
|
||||
</Svg>
|
||||
);
|
||||
}
|
||||
|
||||
/** ⭳ 다운로드. */
|
||||
export function IconDownload(p: IconProps) {
|
||||
return (
|
||||
<Svg {...p}>
|
||||
<path d="M4 16V19C4 20.1 4.9 21 6 21H18C19.1 21 20 20.1 20 19V16" {...SR} />
|
||||
<path d="M12 3V15" {...SR} />
|
||||
<path d="M7 10L12 15L17 10" {...SRJ} />
|
||||
</Svg>
|
||||
);
|
||||
}
|
||||
|
||||
/** 🖼 이미지. */
|
||||
export function IconImage(p: IconProps) {
|
||||
return (
|
||||
<Svg {...p}>
|
||||
<rect x="3" y="4" width="18" height="16" rx="2" {...S} />
|
||||
<circle cx="8.5" cy="9.5" r="1.8" {...S} />
|
||||
<path d="M4 17L9 12L13 16L16 13L20 17" {...SRJ} />
|
||||
</Svg>
|
||||
);
|
||||
}
|
||||
|
||||
/** ⤢ 화면맞춤(확장). */
|
||||
export function IconExpand(p: IconProps) {
|
||||
return (
|
||||
<Svg {...p}>
|
||||
<path d="M9 3H3V9" {...SRJ} />
|
||||
<path d="M15 21H21V15" {...SRJ} />
|
||||
<path d="M21 3L14 10" {...SR} />
|
||||
<path d="M3 21L10 14" {...SR} />
|
||||
</Svg>
|
||||
);
|
||||
}
|
||||
|
||||
/** + 확대(플러스). */
|
||||
export function IconPlus(p: IconProps) {
|
||||
return (
|
||||
<Svg {...p}>
|
||||
<path d="M12 5V19" {...SR} />
|
||||
<path d="M5 12H19" {...SR} />
|
||||
</Svg>
|
||||
);
|
||||
}
|
||||
|
||||
/** − 축소(마이너스). */
|
||||
export function IconMinus(p: IconProps) {
|
||||
return (
|
||||
<Svg {...p}>
|
||||
<path d="M5 12H19" {...SR} />
|
||||
</Svg>
|
||||
);
|
||||
}
|
||||
|
||||
/** ◍ 인원(사람 + 카운트). */
|
||||
export function IconUsers(p: IconProps) {
|
||||
return (
|
||||
<Svg {...p}>
|
||||
<circle cx="12" cy="8" r="3.4" {...S} />
|
||||
<path d="M5 20C5 16.7 8.1 14 12 14C15.9 14 19 16.7 19 20" {...SR} />
|
||||
</Svg>
|
||||
);
|
||||
}
|
||||
|
||||
/** ▦ 그리드(홀 배정). */
|
||||
export function IconGrid(p: IconProps) {
|
||||
return (
|
||||
<Svg {...p}>
|
||||
<rect x="3" y="3" width="7.5" height="7.5" rx="1" {...S} />
|
||||
<rect x="13.5" y="3" width="7.5" height="7.5" rx="1" {...S} />
|
||||
<rect x="3" y="13.5" width="7.5" height="7.5" rx="1" {...S} />
|
||||
<rect x="13.5" y="13.5" width="7.5" height="7.5" rx="1" {...S} />
|
||||
</Svg>
|
||||
);
|
||||
}
|
||||
|
||||
/** ● 오프라인(연결 끊김). */
|
||||
export function IconWifiOff(p: IconProps) {
|
||||
return (
|
||||
<Svg {...p}>
|
||||
<path d="M2 8.8C4.2 7 6.9 5.8 9.8 5.4" {...SR} />
|
||||
<path d="M14.5 5.6C17 6.2 19.3 7.3 21.2 8.9" {...SR} />
|
||||
<path d="M5.5 12.3C7 11.2 8.7 10.4 10.6 10.1" {...SR} />
|
||||
<path d="M8.8 15.6C9.7 15 10.8 14.6 12 14.6C12.4 14.6 12.8 14.6 13.2 14.7" {...SR} />
|
||||
<path d="M12 19H12.01" {...SR} />
|
||||
<path d="M3 3L21 21" {...SR} />
|
||||
</Svg>
|
||||
);
|
||||
}
|
||||
@ -2,6 +2,7 @@ import { useState } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { Button } from '../../components/ui/Button';
|
||||
import { DdayChip } from '../../components/ui/Badge';
|
||||
import { IconCheck, IconSearch, IconSpark, IconWarning } from '../../components/ui/icons';
|
||||
import './approval-queue.css';
|
||||
|
||||
/*
|
||||
@ -58,7 +59,7 @@ export function ManagerApprovalQueuePage() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="kx-aq__search">
|
||||
<span aria-hidden="true">⌕</span>
|
||||
<span aria-hidden="true"><IconSearch size={16} /></span>
|
||||
<input placeholder="검색어 입력…" aria-label="검색" />
|
||||
</div>
|
||||
</div>
|
||||
@ -72,7 +73,7 @@ export function ManagerApprovalQueuePage() {
|
||||
<strong className="tnum kx-aq__stat--primary">5</strong>
|
||||
</div>
|
||||
<div className="kx-aq__stat kx-aq__stat--alert">
|
||||
<span>⚠ AI 차단 플래그</span>
|
||||
<span><IconWarning size={14} className="kx-aq__stat-icon" /> AI 차단 플래그</span>
|
||||
<strong className="tnum">4</strong>
|
||||
</div>
|
||||
</div>
|
||||
@ -83,7 +84,7 @@ export function ManagerApprovalQueuePage() {
|
||||
<section className="kx-aq__main" aria-label={`제출 서류 목록 (${hall})`}>
|
||||
<div className="kx-aq__main-head">
|
||||
<h2>제출 서류 목록 ({hall})</h2>
|
||||
<Button leadingIcon="✓">일괄 승인</Button>
|
||||
<Button leadingIcon={<IconCheck size={16} />}>일괄 승인</Button>
|
||||
</div>
|
||||
<div className="kx-aq__table-wrap">
|
||||
<table className="kx-aq__table">
|
||||
@ -114,9 +115,9 @@ export function ManagerApprovalQueuePage() {
|
||||
<td><DdayChip dday={r.dday} /></td>
|
||||
<td>
|
||||
{r.ai === 'block' ? (
|
||||
<span className="kx-aq__ai kx-aq__ai--block">⚠ 차단 {r.aiCount}</span>
|
||||
<span className="kx-aq__ai kx-aq__ai--block"><IconWarning size={13} /> 차단 {r.aiCount}</span>
|
||||
) : (
|
||||
<span className="kx-aq__ai kx-aq__ai--pass">✓ 통과</span>
|
||||
<span className="kx-aq__ai kx-aq__ai--pass"><IconCheck size={13} /> 통과</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="kx-aq__muted">{r.status}</td>
|
||||
@ -169,7 +170,7 @@ export function ManagerApprovalQueuePage() {
|
||||
|
||||
<div className="kx-aq__card kx-aq__card--ai">
|
||||
<div className="kx-aq__ai-head">
|
||||
<span aria-hidden="true">✦</span>
|
||||
<span aria-hidden="true"><IconSpark size={16} /></span>
|
||||
<h3>AI 최적화 분석</h3>
|
||||
</div>
|
||||
<p>
|
||||
|
||||
@ -2,6 +2,14 @@ import { useState } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { Button } from '../../components/ui/Button';
|
||||
import { AiLabel } from '../../components/ui/Badge';
|
||||
import {
|
||||
IconChevronRight,
|
||||
IconDownload,
|
||||
IconExpand,
|
||||
IconMinus,
|
||||
IconPlus,
|
||||
IconSpark,
|
||||
} from '../../components/ui/icons';
|
||||
import './review-details.css';
|
||||
|
||||
/*
|
||||
@ -48,9 +56,9 @@ export function ReviewDetailsPage() {
|
||||
<div>
|
||||
<nav className="kx-rev__crumb" aria-label="경로">
|
||||
<button onClick={() => navigate(-1)}>승인 큐</button>
|
||||
<span aria-hidden="true">›</span>
|
||||
<span aria-hidden="true"><IconChevronRight size={14} /></span>
|
||||
<span>스마트팩토리 코리아</span>
|
||||
<span aria-hidden="true">›</span>
|
||||
<span aria-hidden="true"><IconChevronRight size={14} /></span>
|
||||
<span className="kx-rev__crumb-cur">부스 {booth}</span>
|
||||
</nav>
|
||||
<div className="kx-rev__title-row">
|
||||
@ -60,7 +68,7 @@ export function ReviewDetailsPage() {
|
||||
<span className="kx-rev__meta">일자: 07.10</span>
|
||||
</div>
|
||||
</div>
|
||||
<Button variant="ghost" leadingIcon="⭳">
|
||||
<Button variant="ghost" leadingIcon={<IconDownload size={16} />}>
|
||||
원본 다운로드
|
||||
</Button>
|
||||
</header>
|
||||
@ -87,7 +95,7 @@ export function ReviewDetailsPage() {
|
||||
onClick={() => setOverlay((o) => !o)}
|
||||
aria-pressed={overlay}
|
||||
>
|
||||
<span aria-hidden="true">✦</span> AI 위반 오버레이
|
||||
<span aria-hidden="true"><IconSpark size={15} /></span> AI 위반 오버레이
|
||||
<span className="kx-rev__switch" aria-hidden="true">
|
||||
<span className="kx-rev__switch-knob" />
|
||||
</span>
|
||||
@ -111,9 +119,9 @@ export function ReviewDetailsPage() {
|
||||
))}
|
||||
</div>
|
||||
<div className="kx-rev__zoom">
|
||||
<button aria-label="확대">+</button>
|
||||
<button aria-label="축소">−</button>
|
||||
<button aria-label="화면맞춤">⤢</button>
|
||||
<button aria-label="확대"><IconPlus size={18} /></button>
|
||||
<button aria-label="축소"><IconMinus size={18} /></button>
|
||||
<button aria-label="화면맞춤"><IconExpand size={18} /></button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { Button } from '../../components/ui/Button';
|
||||
import { AiLabel } from '../../components/ui/Badge';
|
||||
import { IconBlock, IconCheck, IconUpload, IconWarning } from '../../components/ui/icons';
|
||||
import type { ComplianceReport, Violation } from '../../api/types';
|
||||
import './compliance-report.css';
|
||||
|
||||
@ -55,7 +56,7 @@ export function ComplianceReportPage() {
|
||||
role="status"
|
||||
>
|
||||
<span className="kx-comp__banner-icon" aria-hidden="true">
|
||||
{report.submittable ? '✓' : '⛔'}
|
||||
{report.submittable ? <IconCheck size={18} /> : <IconBlock size={18} />}
|
||||
</span>
|
||||
<span className="tnum">
|
||||
차단 {report.blockCount}건 · 경고 {report.warnCount}건 · 통과 {report.passCount}건
|
||||
@ -65,7 +66,7 @@ export function ComplianceReportPage() {
|
||||
|
||||
{/* PDF 업로드 존 */}
|
||||
<div className="kx-comp__drop">
|
||||
<span className="kx-comp__drop-icon" aria-hidden="true">⭱</span>
|
||||
<span className="kx-comp__drop-icon" aria-hidden="true"><IconUpload size={28} /></span>
|
||||
<p>기존 도면 업로드 (PDF/이미지)</p>
|
||||
<span className="kx-comp__drop-hint">드래그 앤 드롭 또는 클릭해 파일 선택</span>
|
||||
<AiLabel>AI 자동 검증</AiLabel>
|
||||
@ -105,7 +106,7 @@ export function ComplianceReportPage() {
|
||||
))}
|
||||
{PASS_GROUPS.map((p) => (
|
||||
<li key={p.group} className="kx-comp__item is-pass">
|
||||
<span className="kx-comp__pin kx-comp__pin--pass" aria-hidden="true">✓</span>
|
||||
<span className="kx-comp__pin kx-comp__pin--pass" aria-hidden="true"><IconCheck size={14} /></span>
|
||||
<div className="kx-comp__item-body">
|
||||
<div className="kx-comp__item-top">
|
||||
<span className="kx-comp__item-label">{p.label}</span>
|
||||
@ -125,7 +126,7 @@ export function ComplianceReportPage() {
|
||||
<figure className="kx-comp__drawing-frame">
|
||||
<img src={ELEVATION} alt="상세 도면 (입면) 검토" loading="lazy" />
|
||||
<span className="kx-comp__overlay-box" aria-hidden="true" />
|
||||
<span className="kx-comp__overlay-badge">△ 5.4m 초과 · COMPLIANCE 차단</span>
|
||||
<span className="kx-comp__overlay-badge"><IconWarning size={13} /> 5.4m 초과 · COMPLIANCE 차단</span>
|
||||
</figure>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@ -4,11 +4,20 @@ import { StatusBadge } from '../../components/ui/Badge';
|
||||
import { Button } from '../../components/ui/Button';
|
||||
import { EmptyState } from '../../components/ui/States';
|
||||
import {
|
||||
IconCalendar,
|
||||
IconChevronLeft,
|
||||
IconChevronRight,
|
||||
IconGrid,
|
||||
IconUsers,
|
||||
} from '../../components/ui/icons';
|
||||
import {
|
||||
ALL_EVENTS,
|
||||
CATEGORY_LABEL,
|
||||
EVENT_DAYS,
|
||||
HALL_UTILIZATION,
|
||||
SAMPLE_EVENTS,
|
||||
EVENT_YEARS,
|
||||
STATUS_LABEL,
|
||||
eventDaysFor,
|
||||
eventsByDateFor,
|
||||
hallUtilizationFor,
|
||||
type EventStatus,
|
||||
type ScheduleEvent,
|
||||
} from './sampleSchedule';
|
||||
@ -18,61 +27,87 @@ const MOCK = `${import.meta.env.BASE_URL}mock/organizer_dashboard`;
|
||||
const THUMBS = [`${MOCK}/img1.jpg`, `${MOCK}/img2.jpg`, `${MOCK}/img3.jpg`];
|
||||
const TODAY = new Date(2026, 6, 11); // 2026-07-11 (currentDate)
|
||||
|
||||
/** 홀 필터 매칭 — halls 문자열("홀 4, 5"·"제2전시장 홀7")에서 "홀" 뒤 번호만 추출해 전시장 범위("1-5"/"6-10")와 대조. "제N전시장"의 N 오매칭 방지. */
|
||||
/** 홀 필터 매칭 — halls 문자열("홀 4, 5A"·"제2전시장 홀7")에서 홀 번호만 추출해 전시장 범위("1-5"/"6-10")와 대조. "제N전시장"의 N·"7A-1"의 -1 오매칭 방지. */
|
||||
function matchesHall(halls: string, range: string): boolean {
|
||||
if (range === 'all') return true;
|
||||
const [lo, hi] = range.split('-').map(Number);
|
||||
const m = halls.match(/홀\s*([\d,\s~-]+)/);
|
||||
const nums = m ? (m[1].match(/\d+/g)?.map(Number) ?? []) : [];
|
||||
const cleaned = halls
|
||||
.replace(/제\s*\d+\s*전시장/g, '')
|
||||
.replace(/(\d+[A-C]?)-\d+/g, '$1');
|
||||
const nums = (cleaned.match(/\d+/g) ?? []).map(Number).filter((n) => n >= 1 && n <= 10);
|
||||
return nums.some((n) => n >= lo && n <= hi);
|
||||
}
|
||||
|
||||
/*
|
||||
* SCR-15 전시 일정 관리 (M1·M6). Stitch exhibition_schedule 이식.
|
||||
* - 좌 2/3 행사 리스트 + 우 1/3 미니 캘린더 · 홀 가동률.
|
||||
* ★ 경계면: 행사 목록은 인증 workspaces(실 원천) 우선, 없으면 샘플. 홀 가동률·캘린더 점유는 샘플.
|
||||
* - 좌 2/3 행사 리스트(또는 WISE 스타일 월 캘린더) + 우 1/3 미니 캘린더 · 홀 가동률.
|
||||
* - 폴백 데이터: kintex.com 크롤링 10년치(2016~2026, 1,098건) — sampleSchedule.ALL_EVENTS.
|
||||
* - 캘린더 뷰는 WISE(UIWS) ScheduleCalendar 월 그리드 패턴 참조(외부 라이브러리 미사용,
|
||||
* 이전/다음/오늘 툴바·요일 헤더·당일 강조·일자별 이벤트 칩).
|
||||
* ★ 경계면: 행사 목록은 인증 workspaces(실 원천) 우선, 없으면 크롤링 데이터 폴백.
|
||||
*/
|
||||
export function ExhibitionSchedulePage() {
|
||||
const workspaces = useAuthStore((s) => s.workspaces);
|
||||
const [view, setView] = useState<'list' | 'calendar'>('list');
|
||||
const [year, setYear] = useState('2026');
|
||||
const [hall, setHall] = useState('all');
|
||||
const [category, setCategory] = useState('all');
|
||||
const [status, setStatus] = useState('all');
|
||||
// 우측 미니 캘린더·홀 가동률 공유 월 커서
|
||||
const [asideCursor, setAsideCursor] = useState(new Date(2026, 6, 1)); // 2026년 7월
|
||||
|
||||
// 실 원천: 인증 workspaces → ScheduleEvent 매핑. 없으면 샘플 폴백.
|
||||
// 실 원천: 인증 workspaces → ScheduleEvent 매핑. 없으면 크롤링 10년치 폴백.
|
||||
const usingReal = workspaces.length > 0;
|
||||
const events: ScheduleEvent[] = useMemo(() => {
|
||||
if (!usingReal) return SAMPLE_EVENTS;
|
||||
if (!usingReal) return ALL_EVENTS;
|
||||
return workspaces.map((w, i) => ({
|
||||
id: w.eventId,
|
||||
title: w.eventName,
|
||||
// ★ category/estVisitors 는 WorkspaceDto 미포함 → 샘플/기본값
|
||||
category: 'exhibition',
|
||||
category: 'exhibition' as const,
|
||||
status: (w.dday > 0 ? 'upcoming' : 'ongoing') as EventStatus,
|
||||
period: `${w.startDate} – ${w.endDate}`,
|
||||
start: w.startDate,
|
||||
end: w.endDate,
|
||||
halls: w.hallLabel,
|
||||
estVisitors: '집계 대기',
|
||||
thumbIdx: i % THUMBS.length,
|
||||
}));
|
||||
}, [usingReal, workspaces]);
|
||||
|
||||
const filtered = events.filter(
|
||||
(e) =>
|
||||
matchesHall(e.halls, hall) &&
|
||||
(category === 'all' || e.category === category) &&
|
||||
(status === 'all' || e.status === status),
|
||||
const filtered = useMemo(
|
||||
() =>
|
||||
events.filter(
|
||||
(e) =>
|
||||
(year === 'all' || e.start.slice(0, 4) === year || e.end.slice(0, 4) === year) &&
|
||||
matchesHall(e.halls, hall) &&
|
||||
(category === 'all' || e.category === category) &&
|
||||
(status === 'all' || e.status === status),
|
||||
),
|
||||
[events, year, hall, category, status],
|
||||
);
|
||||
|
||||
// 목록은 최신 시작일 우선 정렬
|
||||
const listed = useMemo(() => [...filtered].sort((a, b) => (a.start < b.start ? 1 : -1)), [filtered]);
|
||||
|
||||
return (
|
||||
<div className="kx-sched">
|
||||
<header className="kx-sched__head">
|
||||
<div>
|
||||
<h1 className="kx-sched__title">전시 일정</h1>
|
||||
<p className="kx-sched__subtitle">킨텍스 전 홀 행사 일정 관리·모니터링</p>
|
||||
<p className="kx-sched__subtitle">
|
||||
킨텍스 전 홀 행사 일정 관리·모니터링 — 10년치 {ALL_EVENTS.length.toLocaleString()}건 (kintex.com 수집)
|
||||
</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="kx-sched__filters">
|
||||
<select className="kx-select" aria-label="연도 필터" value={year} onChange={(e) => setYear(e.target.value)}>
|
||||
<option value="all">전체 연도</option>
|
||||
{EVENT_YEARS.map((y) => (
|
||||
<option key={y} value={String(y)}>{y}년</option>
|
||||
))}
|
||||
</select>
|
||||
<select className="kx-select" aria-label="홀 필터" value={hall} onChange={(e) => setHall(e.target.value)}>
|
||||
<option value="all">전체 홀 (1-10)</option>
|
||||
<option value="1-5">제1전시장 (홀 1-5)</option>
|
||||
@ -91,7 +126,7 @@ export function ExhibitionSchedulePage() {
|
||||
))}
|
||||
</select>
|
||||
<div className="kx-sched__filter-spacer" />
|
||||
{!usingReal && <span className="kx-bi__degraded">행사 API 대기 (샘플)</span>}
|
||||
{!usingReal && <span className="kx-bi__degraded">행사 API 대기 (크롤링 데이터)</span>}
|
||||
<div className="kx-seg" role="tablist" aria-label="보기 전환">
|
||||
<button role="tab" aria-selected={view === 'list'} className={`kx-seg__btn ${view === 'list' ? 'is-active' : ''}`} onClick={() => setView('list')}>목록</button>
|
||||
<button role="tab" aria-selected={view === 'calendar'} className={`kx-seg__btn ${view === 'calendar' ? 'is-active' : ''}`} onClick={() => setView('calendar')}>캘린더</button>
|
||||
@ -99,67 +134,49 @@ export function ExhibitionSchedulePage() {
|
||||
</div>
|
||||
|
||||
<div className="kx-sched__split">
|
||||
<section className="kx-sched__list" aria-label="행사 목록">
|
||||
{filtered.length === 0 ? (
|
||||
<div className="kx-card">
|
||||
<EmptyState
|
||||
title="등록된 행사가 없습니다"
|
||||
description="필터를 조정하거나 새 행사를 생성하세요."
|
||||
action={<Button>새 행사 생성</Button>}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
filtered.map((e) => (
|
||||
<article key={e.id} className="kx-evcard">
|
||||
<div className="kx-evcard__poster" style={{ backgroundImage: `url(${THUMBS[e.thumbIdx]})` }} aria-hidden="true" />
|
||||
<div className="kx-evcard__body">
|
||||
<div className="kx-evcard__top">
|
||||
<h2 className="kx-evcard__title">{e.title}</h2>
|
||||
<span className={`kx-evstatus is-${e.status}`}>{STATUS_LABEL[e.status]}</span>
|
||||
{view === 'list' ? (
|
||||
<section className="kx-sched__list" aria-label="행사 목록">
|
||||
<p className="kx-sched__count tnum">{listed.length.toLocaleString()}건</p>
|
||||
{listed.length === 0 ? (
|
||||
<div className="kx-card">
|
||||
<EmptyState
|
||||
title="등록된 행사가 없습니다"
|
||||
description="필터를 조정하거나 새 행사를 생성하세요."
|
||||
action={<Button>새 행사 생성</Button>}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
listed.map((e) => (
|
||||
<article key={e.id} className="kx-evcard">
|
||||
<div className="kx-evcard__poster" style={{ backgroundImage: `url(${THUMBS[e.thumbIdx]})` }} aria-hidden="true" />
|
||||
<div className="kx-evcard__body">
|
||||
<div className="kx-evcard__top">
|
||||
<h2 className="kx-evcard__title">{e.title}</h2>
|
||||
<span className={`kx-evstatus is-${e.status}`}>{STATUS_LABEL[e.status]}</span>
|
||||
</div>
|
||||
<p className="kx-evcard__cat">{CATEGORY_LABEL[e.category]} 전시</p>
|
||||
<ul className="kx-evcard__meta">
|
||||
<li aria-label="기간"><IconCalendar size={14} /> <span className="tnum">{e.period}</span></li>
|
||||
<li aria-label="배정 홀"><IconGrid size={14} /> {e.halls}</li>
|
||||
<li aria-label="추정 인원"><IconUsers size={14} /> <span className="tnum">{e.estVisitors}</span></li>
|
||||
{e.designStatus && <li><StatusBadge status={e.designStatus} /></li>}
|
||||
</ul>
|
||||
<div className="kx-evcard__actions">
|
||||
<Button variant="secondary">상세 편집</Button>
|
||||
<Button variant="ghost">반입/물류 보기</Button>
|
||||
</div>
|
||||
</div>
|
||||
<p className="kx-evcard__cat">{CATEGORY_LABEL[e.category]} 전시</p>
|
||||
<ul className="kx-evcard__meta">
|
||||
<li aria-label="기간">▤ <span className="tnum">{e.period}</span></li>
|
||||
<li aria-label="배정 홀">▦ {e.halls}</li>
|
||||
<li aria-label="추정 인원">◍ <span className="tnum">{e.estVisitors}</span></li>
|
||||
{e.designStatus && <li><StatusBadge status={e.designStatus} /></li>}
|
||||
</ul>
|
||||
<div className="kx-evcard__actions">
|
||||
<Button variant="secondary">상세 편집</Button>
|
||||
<Button variant="ghost">반입/물류 보기</Button>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
))
|
||||
)}
|
||||
{view === 'calendar' && (
|
||||
<p className="kx-sched__calnote">
|
||||
캘린더 매트릭스 뷰는 M1 배정 데이터 정합 후 제공됩니다 — 현재는 목록·미니 캘린더로 확인하세요.
|
||||
</p>
|
||||
)}
|
||||
</section>
|
||||
</article>
|
||||
))
|
||||
)}
|
||||
</section>
|
||||
) : (
|
||||
<MonthCalendar events={filtered} />
|
||||
)}
|
||||
|
||||
<aside className="kx-sched__aside">
|
||||
<MiniCalendar />
|
||||
<section className="kx-card" aria-label="홀 가동률">
|
||||
<div className="kx-card__head">
|
||||
<h2>홀 가동률 (8월)</h2>
|
||||
<span className="kx-bi__degraded">집계 대기 (샘플)</span>
|
||||
</div>
|
||||
<ul className="kx-barlist">
|
||||
{HALL_UTILIZATION.map((h) => (
|
||||
<li key={h.label} className="kx-barlist__item">
|
||||
<div className="kx-barlist__top">
|
||||
<span>{h.label}</span>
|
||||
<span className="tnum">{h.pct}%</span>
|
||||
</div>
|
||||
<div className="kx-barlist__track">
|
||||
<span className="kx-barlist__fill" style={{ width: `${h.pct}%` }} />
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
<MiniCalendar cursor={asideCursor} onCursor={setAsideCursor} />
|
||||
<HallUtilization cursor={asideCursor} />
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
@ -167,13 +184,84 @@ export function ExhibitionSchedulePage() {
|
||||
}
|
||||
|
||||
const WD = ['일', '월', '화', '수', '목', '금', '토'];
|
||||
const MAX_CHIPS = 3; // 월 그리드 셀당 표시 칩 수 (초과분 +N)
|
||||
|
||||
function MiniCalendar() {
|
||||
const [cursor, setCursor] = useState(new Date(2026, 7, 1)); // 2026년 8월
|
||||
/**
|
||||
* WISE(UIWS ScheduleCalendar) 참조 월 캘린더 — 이전/다음/오늘 툴바, 요일 헤더,
|
||||
* 당일 강조, 일자 셀에 행사 칩(카테고리 색), 초과분 +N. 외부 라이브러리 미사용.
|
||||
*/
|
||||
function MonthCalendar({ events }: { events: ScheduleEvent[] }) {
|
||||
const [cursor, setCursor] = useState(new Date(2026, 6, 1)); // 2026년 7월
|
||||
const year = cursor.getFullYear();
|
||||
const month = cursor.getMonth();
|
||||
const byDate = useMemo(() => eventsByDateFor(year, month, events), [year, month, events]);
|
||||
|
||||
const firstDow = new Date(year, month, 1).getDay();
|
||||
const daysInMonth = new Date(year, month + 1, 0).getDate();
|
||||
const cells: (number | null)[] = [];
|
||||
for (let i = 0; i < firstDow; i++) cells.push(null);
|
||||
for (let d = 1; d <= daysInMonth; d++) cells.push(d);
|
||||
while (cells.length % 7 !== 0) cells.push(null);
|
||||
|
||||
const todayKey =
|
||||
TODAY.getFullYear() === year && TODAY.getMonth() === month ? TODAY.getDate() : -1;
|
||||
const mm = String(month + 1).padStart(2, '0');
|
||||
|
||||
return (
|
||||
<section className="kx-card kx-mcal" aria-label="월 캘린더">
|
||||
<div className="kx-mcal__toolbar">
|
||||
<button type="button" className="kx-mcal__nav" onClick={() => setCursor(new Date(year, month - 1, 1))}>
|
||||
<IconChevronLeft size={14} /> 이전
|
||||
</button>
|
||||
<strong className="kx-mcal__title tnum">{year}년 {month + 1}월</strong>
|
||||
<button type="button" className="kx-mcal__nav" onClick={() => setCursor(new Date(year, month + 1, 1))}>
|
||||
다음 <IconChevronRight size={14} />
|
||||
</button>
|
||||
<button type="button" className="kx-mcal__nav" onClick={() => setCursor(new Date(TODAY.getFullYear(), TODAY.getMonth(), 1))}>
|
||||
오늘
|
||||
</button>
|
||||
</div>
|
||||
<div className="kx-mcal__grid">
|
||||
{WD.map((w, i) => (
|
||||
<div key={w} className={`kx-mcal__dow${i === 0 ? ' is-sun' : i === 6 ? ' is-sat' : ''}`}>{w}</div>
|
||||
))}
|
||||
{cells.map((d, i) => {
|
||||
if (d === null) return <div key={`e${i}`} className="kx-mcal__cell is-out" />;
|
||||
const key = `${year}-${mm}-${String(d).padStart(2, '0')}`;
|
||||
const dayEvents = byDate.get(key) ?? [];
|
||||
const dow = i % 7;
|
||||
const cls = [
|
||||
'kx-mcal__cell',
|
||||
d === todayKey ? 'is-today' : '',
|
||||
dow === 0 ? 'is-sun' : dow === 6 ? 'is-sat' : '',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ');
|
||||
return (
|
||||
<div key={key} className={cls}>
|
||||
<span className="kx-mcal__daynum tnum">{d}</span>
|
||||
{dayEvents.slice(0, MAX_CHIPS).map((e) => (
|
||||
<span key={e.id} className={`kx-mcal__chip is-${e.category}`} title={`${e.title} (${e.halls})`}>
|
||||
{e.title}
|
||||
</span>
|
||||
))}
|
||||
{dayEvents.length > MAX_CHIPS && (
|
||||
<span className="kx-mcal__more tnum">+{dayEvents.length - MAX_CHIPS}</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function MiniCalendar({ cursor, onCursor }: { cursor: Date; onCursor: (d: Date) => void }) {
|
||||
const year = cursor.getFullYear();
|
||||
const month = cursor.getMonth();
|
||||
const firstDay = new Date(year, month, 1).getDay();
|
||||
const daysInMonth = new Date(year, month + 1, 0).getDate();
|
||||
const eventDays = useMemo(() => eventDaysFor(year, month), [year, month]);
|
||||
|
||||
const cells: (number | null)[] = [];
|
||||
for (let i = 0; i < firstDay; i++) cells.push(null);
|
||||
@ -185,9 +273,9 @@ function MiniCalendar() {
|
||||
return (
|
||||
<section className="kx-card kx-cal" aria-label="미니 캘린더">
|
||||
<div className="kx-cal__head">
|
||||
<button className="kx-cal__nav" aria-label="이전 달" onClick={() => setCursor(new Date(year, month - 1, 1))}>‹</button>
|
||||
<button className="kx-cal__nav" aria-label="이전 달" onClick={() => onCursor(new Date(year, month - 1, 1))}>‹</button>
|
||||
<strong>{year}년 {month + 1}월</strong>
|
||||
<button className="kx-cal__nav" aria-label="다음 달" onClick={() => setCursor(new Date(year, month + 1, 1))}>›</button>
|
||||
<button className="kx-cal__nav" aria-label="다음 달" onClick={() => onCursor(new Date(year, month + 1, 1))}>›</button>
|
||||
</div>
|
||||
<div className="kx-cal__grid" role="grid">
|
||||
{WD.map((w) => (
|
||||
@ -195,7 +283,7 @@ function MiniCalendar() {
|
||||
))}
|
||||
{cells.map((d, i) => {
|
||||
if (d === null) return <span key={`e${i}`} className="kx-cal__cell is-empty" />;
|
||||
const hasEvent = EVENT_DAYS.has(`${year}-${month + 1}-${d}`);
|
||||
const hasEvent = eventDays.has(`${year}-${month + 1}-${d}`);
|
||||
return (
|
||||
<span
|
||||
key={d}
|
||||
@ -211,3 +299,30 @@ function MiniCalendar() {
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function HallUtilization({ cursor }: { cursor: Date }) {
|
||||
const year = cursor.getFullYear();
|
||||
const month = cursor.getMonth();
|
||||
const rows = useMemo(() => hallUtilizationFor(year, month), [year, month]);
|
||||
return (
|
||||
<section className="kx-card" aria-label="홀 가동률">
|
||||
<div className="kx-card__head">
|
||||
<h2>홀 가동률 ({year}년 {month + 1}월)</h2>
|
||||
<span className="kx-bi__degraded">크롤링 데이터 기반</span>
|
||||
</div>
|
||||
<ul className="kx-barlist">
|
||||
{rows.map((h) => (
|
||||
<li key={h.label} className="kx-barlist__item">
|
||||
<div className="kx-barlist__top">
|
||||
<span>{h.label}</span>
|
||||
<span className="tnum">{h.pct}%</span>
|
||||
</div>
|
||||
<div className="kx-barlist__track">
|
||||
<span className="kx-barlist__fill" style={{ width: `${h.pct}%` }} />
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
1112
src/frontend/src/screens/schedule/kintexEvents.ts
Normal file
1112
src/frontend/src/screens/schedule/kintexEvents.ts
Normal file
File diff suppressed because it is too large
Load Diff
@ -2,12 +2,14 @@
|
||||
* 전시 일정 관리 표시 데이터 (SCR-15 · M1·M6).
|
||||
* ★ 경계면(부분 실 + 부분 샘플):
|
||||
* - 행사(Event) 목록·기간·배정 홀은 실 API 가능(Event 엔티티 존재).
|
||||
* 현재는 인증 응답 workspaces(WorkspaceDto)를 실 원천으로 사용하며, 없으면 아래 샘플로 폴백한다.
|
||||
* - 홀 가동률 집계·외부 행사일정 연동은 미구현 → 샘플. 캘린더 점유 매트릭스는 M1 배정 정합 후 실 전환.
|
||||
* 현재는 인증 응답 workspaces(WorkspaceDto)를 실 원천으로 사용하며, 없으면 크롤링 데이터로 폴백한다.
|
||||
* - 폴백 원천: kintex.com 크롤링 10년치(2016~2026, 1,098건) — kintexEvents.ts (백엔드 V10 시드와 동일).
|
||||
* - estVisitors·카테고리는 추정치(샘플). 홀 가동률·캘린더 점유는 크롤링 데이터 기반 계산.
|
||||
*/
|
||||
import type { FlowStatus } from '../../components/ui/Badge';
|
||||
import { KINTEX_EVENT_ROWS, type KxCategory } from './kintexEvents';
|
||||
|
||||
export type EventCategory = 'exhibition' | 'culture' | 'trade' | 'tech';
|
||||
export type EventCategory = KxCategory;
|
||||
export type EventStatus = 'ongoing' | 'upcoming' | 'ended';
|
||||
|
||||
export interface ScheduleEvent {
|
||||
@ -16,6 +18,8 @@ export interface ScheduleEvent {
|
||||
category: EventCategory;
|
||||
status: EventStatus;
|
||||
period: string; // "2026.09.05–09.08"
|
||||
start: string; // "2026-09-05"
|
||||
end: string; // "2026-09-08"
|
||||
halls: string; // "홀 4, 5"
|
||||
estVisitors: string; // "25,000명"
|
||||
designStatus?: FlowStatus;
|
||||
@ -34,66 +38,122 @@ export const STATUS_LABEL: Record<EventStatus, string> = {
|
||||
ended: '종료',
|
||||
};
|
||||
|
||||
export const SAMPLE_EVENTS: ScheduleEvent[] = [
|
||||
{
|
||||
id: 'smf2026',
|
||||
title: '스마트팩토리 코리아',
|
||||
category: 'trade',
|
||||
status: 'ongoing',
|
||||
period: '2026.09.05–09.08',
|
||||
halls: '홀 4, 5',
|
||||
estVisitors: '25,000명',
|
||||
designStatus: 'building',
|
||||
thumbIdx: 0,
|
||||
},
|
||||
{
|
||||
id: 'health2026',
|
||||
title: '국제 헬스케어 위크',
|
||||
category: 'exhibition',
|
||||
status: 'upcoming',
|
||||
period: '2026.09.22–09.25',
|
||||
halls: '홀 7',
|
||||
estVisitors: '18,000명',
|
||||
designStatus: 'submitted',
|
||||
thumbIdx: 1,
|
||||
},
|
||||
{
|
||||
id: 'foodtech2026',
|
||||
title: '푸드테크 엑스포',
|
||||
category: 'tech',
|
||||
status: 'upcoming',
|
||||
period: '2026.10.10–10.13',
|
||||
halls: '홀 3',
|
||||
estVisitors: '12,000명',
|
||||
designStatus: 'draft',
|
||||
thumbIdx: 2,
|
||||
},
|
||||
{
|
||||
id: 'living2026',
|
||||
title: '리빙·인테리어 페어',
|
||||
category: 'culture',
|
||||
status: 'ended',
|
||||
period: '2026.08.14–08.17',
|
||||
halls: '홀 1, 2',
|
||||
estVisitors: '30,000명',
|
||||
designStatus: 'inspected',
|
||||
thumbIdx: 0,
|
||||
},
|
||||
];
|
||||
const TODAY_YMD = '2026-07-11'; // currentDate 고정(데모) — ExhibitionSchedulePage.TODAY 와 정합
|
||||
|
||||
function statusOf(start: string, end: string): EventStatus {
|
||||
if (end < TODAY_YMD) return 'ended';
|
||||
if (start > TODAY_YMD) return 'upcoming';
|
||||
return 'ongoing';
|
||||
}
|
||||
|
||||
/** "2016-01-28","2016-01-31" → "2016.01.28–01.31" */
|
||||
function periodLabel(start: string, end: string): string {
|
||||
const s = start.split('-').join('.');
|
||||
const e = end.split('-').join('.');
|
||||
return e.startsWith(s.slice(0, 5)) ? `${s}–${e.slice(5)}` : `${s}–${e}`;
|
||||
}
|
||||
|
||||
// 진행중 데모 표시용 설계 상태(샘플) — 진행중·임박 행사에만 부여
|
||||
const DESIGN_CYCLE: FlowStatus[] = ['building', 'submitted', 'draft', 'inspected'];
|
||||
|
||||
/** 크롤링 10년치 전체 (1,098건, 시작일 오름차순). */
|
||||
export const ALL_EVENTS: ScheduleEvent[] = KINTEX_EVENT_ROWS.map((r, i) => {
|
||||
const [id, title, start, end, halls, category, visitors] = r;
|
||||
const status = statusOf(start, end);
|
||||
return {
|
||||
id,
|
||||
title,
|
||||
category,
|
||||
status,
|
||||
period: periodLabel(start, end),
|
||||
start,
|
||||
end,
|
||||
halls,
|
||||
estVisitors: `${visitors.toLocaleString()}명`,
|
||||
designStatus: status !== 'ended' ? DESIGN_CYCLE[i % DESIGN_CYCLE.length] : undefined,
|
||||
thumbIdx: i % 3,
|
||||
};
|
||||
});
|
||||
|
||||
/** 데이터에 존재하는 연도 내림차순 목록 (연도 필터용). */
|
||||
export const EVENT_YEARS: number[] = [...new Set(ALL_EVENTS.map((e) => Number(e.start.slice(0, 4))))].sort(
|
||||
(a, b) => b - a,
|
||||
);
|
||||
|
||||
/** 하위 호환: 기존 SAMPLE_EVENTS — 오늘 기준 진행중·예정 상위 목록. */
|
||||
export const SAMPLE_EVENTS: ScheduleEvent[] = ALL_EVENTS.filter((e) => e.status !== 'ended').slice(0, 12);
|
||||
|
||||
/** 해당 월에 행사가 있는 날짜 집합 — key: 'YYYY-M-D' (미니 캘린더 하이라이트). */
|
||||
export function eventDaysFor(year: number, month0: number, events: ScheduleEvent[] = ALL_EVENTS): Set<string> {
|
||||
const days = new Set<string>();
|
||||
const last = new Date(year, month0 + 1, 0).getDate();
|
||||
const mStart = `${year}-${String(month0 + 1).padStart(2, '0')}-01`;
|
||||
const mEnd = `${year}-${String(month0 + 1).padStart(2, '0')}-${String(last).padStart(2, '0')}`;
|
||||
for (const e of events) {
|
||||
if (e.end < mStart || e.start > mEnd) continue;
|
||||
const from = e.start > mStart ? Number(e.start.slice(8, 10)) : 1;
|
||||
const to = e.end < mEnd ? Number(e.end.slice(8, 10)) : last;
|
||||
for (let d = from; d <= to; d++) days.add(`${year}-${month0 + 1}-${d}`);
|
||||
}
|
||||
return days;
|
||||
}
|
||||
|
||||
/** 해당 월 날짜(YYYY-MM-DD) → 그 날 진행 중인 행사 목록 (WISE 달력 월 그리드용). */
|
||||
export function eventsByDateFor(year: number, month0: number, events: ScheduleEvent[] = ALL_EVENTS): Map<string, ScheduleEvent[]> {
|
||||
const map = new Map<string, ScheduleEvent[]>();
|
||||
const last = new Date(year, month0 + 1, 0).getDate();
|
||||
const mm = String(month0 + 1).padStart(2, '0');
|
||||
for (const e of events) {
|
||||
if (e.end < `${year}-${mm}-01` || e.start > `${year}-${mm}-${String(last).padStart(2, '0')}`) continue;
|
||||
const from = e.start.slice(0, 7) === `${year}-${mm}` ? Number(e.start.slice(8, 10)) : 1;
|
||||
const to = e.end.slice(0, 7) === `${year}-${mm}` ? Number(e.end.slice(8, 10)) : last;
|
||||
for (let d = from; d <= to; d++) {
|
||||
const key = `${year}-${mm}-${String(d).padStart(2, '0')}`;
|
||||
const arr = map.get(key) ?? [];
|
||||
arr.push(e);
|
||||
map.set(key, arr);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
export interface HallUtilRow {
|
||||
label: string;
|
||||
pct: number;
|
||||
}
|
||||
export const HALL_UTILIZATION: HallUtilRow[] = [
|
||||
{ label: '홀 1-3', pct: 85 },
|
||||
{ label: '홀 4-5', pct: 40 },
|
||||
{ label: '홀 6-8', pct: 62 },
|
||||
{ label: '홀 9-10', pct: 18 },
|
||||
|
||||
const HALL_GROUPS: { label: string; lo: number; hi: number }[] = [
|
||||
{ label: '홀 1-3', lo: 1, hi: 3 },
|
||||
{ label: '홀 4-5', lo: 4, hi: 5 },
|
||||
{ label: '홀 6-8', lo: 6, hi: 8 },
|
||||
{ label: '홀 9-10', lo: 9, hi: 10 },
|
||||
];
|
||||
|
||||
// 미니 캘린더 하이라이트 날짜(샘플) — key: 'YYYY-M-D'
|
||||
export const EVENT_DAYS = new Set<string>([
|
||||
'2026-8-14', '2026-8-15', '2026-8-16', '2026-8-17',
|
||||
'2026-9-5', '2026-9-6', '2026-9-7', '2026-9-8',
|
||||
]);
|
||||
function hallNums(halls: string): number[] {
|
||||
return (halls.match(/\d+/g) ?? []).map(Number).filter((n) => n >= 1 && n <= 10);
|
||||
}
|
||||
|
||||
/** 크롤링 데이터 기반 월별 홀 그룹 가동률 — (그룹 홀에 행사가 있는 날 수)/(월 일수). */
|
||||
export function hallUtilizationFor(year: number, month0: number): HallUtilRow[] {
|
||||
const last = new Date(year, month0 + 1, 0).getDate();
|
||||
const mm = String(month0 + 1).padStart(2, '0');
|
||||
const mStart = `${year}-${mm}-01`;
|
||||
const mEnd = `${year}-${mm}-${String(last).padStart(2, '0')}`;
|
||||
return HALL_GROUPS.map((g) => {
|
||||
const busy = new Set<number>();
|
||||
for (const e of ALL_EVENTS) {
|
||||
if (e.end < mStart || e.start > mEnd) continue;
|
||||
if (!hallNums(e.halls).some((n) => n >= g.lo && n <= g.hi)) continue;
|
||||
const from = e.start > mStart ? Number(e.start.slice(8, 10)) : 1;
|
||||
const to = e.end < mEnd ? Number(e.end.slice(8, 10)) : last;
|
||||
for (let d = from; d <= to; d++) busy.add(d);
|
||||
}
|
||||
return { label: g.label, pct: Math.round((busy.size / last) * 100) };
|
||||
});
|
||||
}
|
||||
|
||||
/** 하위 호환: 2026-08 기준 가동률. */
|
||||
export const HALL_UTILIZATION: HallUtilRow[] = hallUtilizationFor(2026, 7);
|
||||
|
||||
/** 하위 호환: 미니 캘린더 하이라이트(2026-08). */
|
||||
export const EVENT_DAYS = eventDaysFor(2026, 7);
|
||||
|
||||
@ -205,6 +205,121 @@
|
||||
background: var(--color-white);
|
||||
}
|
||||
|
||||
/* 목록 건수 */
|
||||
.kx-sched__count {
|
||||
font-size: var(--fs-caption);
|
||||
color: var(--color-neutral-500);
|
||||
}
|
||||
|
||||
/* WISE(UIWS ScheduleCalendar) 참조 월 캘린더 */
|
||||
.kx-mcal {
|
||||
min-width: 0;
|
||||
}
|
||||
.kx-mcal__toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
margin-bottom: var(--space-3);
|
||||
}
|
||||
.kx-mcal__title {
|
||||
font-size: var(--fs-h3);
|
||||
color: var(--color-neutral-900);
|
||||
margin: 0 var(--space-2);
|
||||
}
|
||||
.kx-mcal__nav {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 4px 10px;
|
||||
border: var(--border-card);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--color-white);
|
||||
color: var(--color-neutral-700);
|
||||
font-size: var(--fs-caption);
|
||||
cursor: pointer;
|
||||
}
|
||||
.kx-mcal__nav:hover {
|
||||
background: var(--color-neutral-050);
|
||||
}
|
||||
.kx-mcal__grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
border-top: var(--border-card);
|
||||
border-left: var(--border-card);
|
||||
}
|
||||
.kx-mcal__dow {
|
||||
text-align: center;
|
||||
font-size: var(--fs-caption);
|
||||
font-weight: 600;
|
||||
color: var(--color-neutral-500);
|
||||
padding: 6px 0;
|
||||
background: var(--color-neutral-050);
|
||||
border-right: var(--border-card);
|
||||
border-bottom: var(--border-card);
|
||||
}
|
||||
.kx-mcal__cell {
|
||||
min-height: 96px;
|
||||
padding: 4px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
border-right: var(--border-card);
|
||||
border-bottom: var(--border-card);
|
||||
overflow: hidden;
|
||||
}
|
||||
.kx-mcal__cell.is-out {
|
||||
background: var(--color-neutral-050);
|
||||
}
|
||||
.kx-mcal__cell.is-today {
|
||||
background: var(--color-primary-050);
|
||||
}
|
||||
.kx-mcal__daynum {
|
||||
font-size: var(--fs-caption);
|
||||
color: var(--color-neutral-700);
|
||||
}
|
||||
.kx-mcal__cell.is-sun .kx-mcal__daynum,
|
||||
.kx-mcal__dow.is-sun {
|
||||
color: #d64545;
|
||||
}
|
||||
.kx-mcal__cell.is-sat .kx-mcal__daynum,
|
||||
.kx-mcal__dow.is-sat {
|
||||
color: var(--color-primary-600);
|
||||
}
|
||||
.kx-mcal__cell.is-today .kx-mcal__daynum {
|
||||
font-weight: 700;
|
||||
color: var(--color-primary-700);
|
||||
}
|
||||
.kx-mcal__chip {
|
||||
font-size: 11px;
|
||||
line-height: 1.35;
|
||||
padding: 1px 6px;
|
||||
border-radius: var(--radius-sm);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
cursor: default;
|
||||
}
|
||||
.kx-mcal__chip.is-exhibition {
|
||||
background: var(--color-primary-050);
|
||||
color: var(--color-primary-700);
|
||||
}
|
||||
.kx-mcal__chip.is-culture {
|
||||
background: #fdeef3;
|
||||
color: #b83268;
|
||||
}
|
||||
.kx-mcal__chip.is-trade {
|
||||
background: #fff4e5;
|
||||
color: #9a6b1f;
|
||||
}
|
||||
.kx-mcal__chip.is-tech {
|
||||
background: #e6f4ee;
|
||||
color: #1f7a54;
|
||||
}
|
||||
.kx-mcal__more {
|
||||
font-size: 11px;
|
||||
color: var(--color-neutral-500);
|
||||
}
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
.kx-sched__split {
|
||||
grid-template-columns: 1fr;
|
||||
|
||||
446
stitch_kintex_ai_system_architect/scr_18_hall_booking/code.html
Normal file
446
stitch_kintex_ai_system_architect/scr_18_hall_booking/code.html
Normal file
@ -0,0 +1,446 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html class="light" lang="ko"><head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<link href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/static/pretendard.css" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Geist:wght@100..900&family=Inter:wght@100..900&display=swap" rel="stylesheet"/>
|
||||
<style>
|
||||
body { font-family: 'Pretendard', sans-serif; }
|
||||
.material-symbols-outlined {
|
||||
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.custom-scrollbar::-webkit-scrollbar { width: 4px; height: 4px; }
|
||||
.custom-scrollbar::-webkit-scrollbar-track { background: transparent; }
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb { background: #E4E7EC; border-radius: 10px; }
|
||||
.calendar-grid { display: grid; grid-template-columns: 100px repeat(14, 1fr); }
|
||||
</style>
|
||||
<script id="tailwind-config">
|
||||
tailwind.config = {
|
||||
darkMode: "class",
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
"on-surface": "#141b2c",
|
||||
"inverse-surface": "#293041",
|
||||
"surface-bright": "#f9f9ff",
|
||||
"on-background": "#141b2c",
|
||||
"surface-container": "#e9edff",
|
||||
"primary-fixed-dim": "#a2c9ff",
|
||||
"on-secondary": "#ffffff",
|
||||
"inverse-on-surface": "#edf0ff",
|
||||
"background": "#F9FAFB",
|
||||
"on-primary": "#ffffff",
|
||||
"tertiary-container": "#00734e",
|
||||
"on-tertiary": "#ffffff",
|
||||
"tertiary-fixed-dim": "#72daa9",
|
||||
"surface-dim": "#d2daf0",
|
||||
"secondary": "#5427e6",
|
||||
"on-secondary-container": "#f3edff",
|
||||
"on-tertiary-container": "#8ef7c3",
|
||||
"surface-tint": "#0060a9",
|
||||
"secondary-fixed": "#e5deff",
|
||||
"surface-container-highest": "#dbe2f9",
|
||||
"surface-variant": "#dbe2f9",
|
||||
"on-error-container": "#93000a",
|
||||
"on-primary-fixed-variant": "#004881",
|
||||
"on-surface-variant": "#414751",
|
||||
"error": "#ba1a1a",
|
||||
"on-tertiary-fixed": "#002113",
|
||||
"primary": "#004e8b",
|
||||
"on-primary-container": "#d2e3ff",
|
||||
"outline-variant": "#E4E7EC",
|
||||
"on-error": "#ffffff",
|
||||
"surface-container-high": "#e0e8ff",
|
||||
"on-secondary-fixed": "#1b0063",
|
||||
"outline": "#717782",
|
||||
"error-container": "#ffdad6",
|
||||
"surface": "#f9f9ff",
|
||||
"on-tertiary-fixed-variant": "#005236",
|
||||
"surface-container-lowest": "#ffffff",
|
||||
"tertiary-fixed": "#8ef7c4",
|
||||
"primary-container": "#0066b3",
|
||||
"on-primary-fixed": "#001c38",
|
||||
"tertiary": "#00583b",
|
||||
"on-secondary-fixed-variant": "#4500d8",
|
||||
"surface-container-low": "#f1f3ff",
|
||||
"inverse-primary": "#a2c9ff",
|
||||
"primary-fixed": "#d3e4ff",
|
||||
"secondary-container": "#6d4aff",
|
||||
"secondary-fixed-dim": "#c9bfff"
|
||||
},
|
||||
borderRadius: { "DEFAULT": "0.125rem", "lg": "0.25rem", "xl": "0.5rem", "full": "0.75rem" },
|
||||
spacing: { "stack-md": "16px", "container-padding": "24px", "stack-sm": "8px", "stack-lg": "24px", "base": "4px", "gutter": "16px" },
|
||||
fontFamily: { "body-base": ["Inter"], "label-md": ["Geist"], "title-sm": ["Inter"], "headline-md": ["Inter"], "display-lg": ["Inter"] },
|
||||
fontSize: {
|
||||
"body-base": ["14px", {"lineHeight": "20px", "fontWeight": "400"}],
|
||||
"label-md": ["12px", {"lineHeight": "16px", "letterSpacing": "0.02em", "fontWeight": "600"}],
|
||||
"title-sm": ["18px", {"lineHeight": "28px", "fontWeight": "600"}],
|
||||
"headline-md": ["24px", {"lineHeight": "32px", "fontWeight": "600"}],
|
||||
"display-lg": ["36px", {"lineHeight": "44px", "letterSpacing": "-0.02em", "fontWeight": "700"}]
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body class="bg-background text-on-surface flex h-screen overflow-hidden">
|
||||
<!-- SideNavBar (Authority Source: JSON) -->
|
||||
<aside class="w-64 h-full flex flex-col py-stack-lg px-stack-sm border-r border-outline-variant bg-surface-container-low transition-all duration-200">
|
||||
<div class="px-4 mb-10 flex flex-col gap-2">
|
||||
<img alt="KINTEX Logo" class="h-8 w-auto object-contain self-start" src="https://lh3.googleusercontent.com/aida-public/AB6AXuDgHxzb1NpzLhmn1KolnVyDV0on8QFpGZYhW8nQulHHIagNi1apsQSZAQx4QkNWu4GcwxqcnZqsTRHU-mnJxP6f5PSh4tlyTMAtUe2STSKo_hh0TsBhpE58A9Eqvi-S2FBBWr2ZbztWrrhpljTLjig23w-YIQxa2lLddLmN6h62RMk30EF8nYqHXUCA2ZN7jAi_bxir4a_yiEFD6STnilZGX0yFqNOFqe9k6BG1p_eGSQTErj8Vuh9VSn-vRahweUB3GoN2pN4tT-E"/>
|
||||
<div>
|
||||
<h1 class="font-title-sm text-title-sm font-bold text-on-surface">KINTEX Admin</h1>
|
||||
<p class="text-on-surface-variant font-label-xs opacity-70">Enterprise Console</p>
|
||||
</div>
|
||||
</div>
|
||||
<nav class="flex-1 flex flex-col gap-1">
|
||||
<a class="flex items-center gap-3 px-4 py-3 transition-all text-on-surface-variant hover:bg-surface-variant rounded-lg" href="#">
|
||||
<span class="material-symbols-outlined" data-icon="dashboard">dashboard</span>
|
||||
<span class="font-body-base">대시보드</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-4 py-3 transition-all text-on-surface-variant hover:bg-surface-variant rounded-lg" href="#">
|
||||
<span class="material-symbols-outlined" data-icon="calendar_today">calendar_today</span>
|
||||
<span class="font-body-base">전시 일정</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-4 py-3 bg-secondary-container text-on-secondary-container font-semibold rounded-lg transform active:scale-95 shadow-sm" href="#">
|
||||
<span class="material-symbols-outlined" data-icon="layers">layers</span>
|
||||
<span class="font-body-base">홀 배정</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-4 py-3 transition-all text-on-surface-variant hover:bg-surface-variant rounded-lg" href="#">
|
||||
<span class="material-symbols-outlined" data-icon="factory">factory</span>
|
||||
<span class="font-body-base">시설 운영</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-4 py-3 transition-all text-on-surface-variant hover:bg-surface-variant rounded-lg" href="#">
|
||||
<span class="material-symbols-outlined" data-icon="insights">insights</span>
|
||||
<span class="font-body-base">비즈니스 인텔리전스</span>
|
||||
</a>
|
||||
</nav>
|
||||
<div class="mt-auto flex flex-col gap-1 border-t border-outline-variant pt-4">
|
||||
<button class="w-full bg-primary text-white py-3 rounded-xl flex items-center justify-center gap-2 mb-4 hover:brightness-110 transition-all font-semibold">
|
||||
<span class="material-symbols-outlined" style="font-variation-settings: 'FILL' 1;">auto_awesome</span>
|
||||
AI Optimization
|
||||
</button>
|
||||
<a class="flex items-center gap-3 px-4 py-2 text-on-surface-variant hover:text-primary transition-colors" href="#">
|
||||
<span class="material-symbols-outlined" data-icon="help">help</span>
|
||||
<span class="font-label-md">도움말</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-4 py-2 text-on-surface-variant hover:text-error transition-colors" href="#">
|
||||
<span class="material-symbols-outlined" data-icon="logout">logout</span>
|
||||
<span class="font-label-md">로그아웃</span>
|
||||
</a>
|
||||
</div>
|
||||
</aside>
|
||||
<!-- Main Content Area -->
|
||||
<main class="flex-1 flex flex-col overflow-hidden">
|
||||
<!-- TopNavBar (Authority Source: JSON) -->
|
||||
<header class="h-16 flex justify-between items-center w-full px-container-padding bg-surface-container-lowest border-b border-outline-variant">
|
||||
<div class="flex items-center gap-4">
|
||||
<h2 class="font-headline-md text-headline-md text-primary">홀 배정 및 즉시 견적</h2>
|
||||
<div class="flex items-center bg-surface-container px-3 py-1.5 rounded-full border border-outline-variant">
|
||||
<span class="material-symbols-outlined text-primary text-sm mr-2" style="font-variation-settings: 'FILL' 1;">auto_awesome</span>
|
||||
<span class="text-primary font-label-md">AI 기반 자동 배정 최적화 활성</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-6">
|
||||
<div class="relative">
|
||||
<input class="pl-10 pr-4 py-2 bg-surface-container-low border-none rounded-lg text-body-sm w-64 focus:ring-2 focus:ring-primary" placeholder="행사명 또는 홀 번호 검색" type="text"/>
|
||||
<span class="material-symbols-outlined absolute left-3 top-2.5 text-outline text-sm">search</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<span class="material-symbols-outlined text-on-surface-variant cursor-pointer" data-icon="notifications">notifications</span>
|
||||
<span class="material-symbols-outlined text-on-surface-variant cursor-pointer" data-icon="settings">settings</span>
|
||||
<div class="w-8 h-8 rounded-full bg-primary-container flex items-center justify-center text-white font-bold text-xs">M</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<!-- Canvas Area -->
|
||||
<div class="flex-1 flex overflow-hidden">
|
||||
<!-- Left 2/3: Availability Calendar -->
|
||||
<section class="w-2/3 p-stack-lg flex flex-col gap-gutter overflow-y-auto custom-scrollbar border-r border-outline-variant">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="flex items-center gap-4">
|
||||
<button class="p-2 border border-outline-variant rounded hover:bg-white transition-colors">
|
||||
<span class="material-symbols-outlined">chevron_left</span>
|
||||
</button>
|
||||
<h3 class="font-title-sm text-title-sm">2024년 11월 </h3>
|
||||
<button class="p-2 border border-outline-variant rounded hover:bg-white transition-colors">
|
||||
<span class="material-symbols-outlined">chevron_right</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="flex items-center gap-2 text-body-sm">
|
||||
<div class="w-3 h-3 bg-primary-container rounded-sm"></div>
|
||||
<span>예약</span>
|
||||
<div class="w-3 h-3 bg-amber-400 rounded-sm ml-2"></div>
|
||||
<span>가배정</span>
|
||||
<div class="w-3 h-3 bg-surface-container-high rounded-sm ml-2"></div>
|
||||
<span>공실</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 ml-4 p-1 bg-surface-container rounded-lg">
|
||||
<button class="px-3 py-1 bg-white text-xs font-bold rounded shadow-sm">전체 홀</button>
|
||||
<button class="px-3 py-1 text-xs font-medium text-on-surface-variant">Half-Hall 토글</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-white border border-outline-variant rounded-xl overflow-hidden">
|
||||
<!-- Calendar Header -->
|
||||
<div class="calendar-grid border-b border-outline-variant bg-surface-container-low">
|
||||
<div class="p-3 font-label-md text-center border-r border-outline-variant">홀 / 일자</div>
|
||||
<!-- Generating 14 days -->
|
||||
<div class="p-3 text-center border-r border-outline-variant font-label-md">11/01 (금)</div>
|
||||
<div class="p-3 text-center border-r border-outline-variant font-label-md">11/02 (토)</div>
|
||||
<div class="p-3 text-center border-r border-outline-variant font-label-md text-error">11/03 (일)</div>
|
||||
<div class="p-3 text-center border-r border-outline-variant font-label-md">11/04 (월)</div>
|
||||
<div class="p-3 text-center border-r border-outline-variant font-label-md">11/05 (화)</div>
|
||||
<div class="p-3 text-center border-r border-outline-variant font-label-md">11/06 (수)</div>
|
||||
<div class="p-3 text-center border-r border-outline-variant font-label-md">11/07 (목)</div>
|
||||
<div class="p-3 text-center border-r border-outline-variant font-label-md">11/08 (금)</div>
|
||||
<div class="p-3 text-center border-r border-outline-variant font-label-md">11/09 (토)</div>
|
||||
<div class="p-3 text-center border-r border-outline-variant font-label-md text-error">11/10 (일)</div>
|
||||
<div class="p-3 text-center border-r border-outline-variant font-label-md">11/11 (월)</div>
|
||||
<div class="p-3 text-center border-r border-outline-variant font-label-md">11/12 (화)</div>
|
||||
<div class="p-3 text-center border-r border-outline-variant font-label-md">11/13 (수)</div>
|
||||
<div class="p-3 text-center font-label-md">11/14 (목)</div>
|
||||
</div>
|
||||
<!-- Calendar Body Rows -->
|
||||
<div class="divide-y divide-outline-variant">
|
||||
<!-- Hall 1 -->
|
||||
<div class="calendar-grid group">
|
||||
<div class="p-3 font-bold bg-surface-container-low text-center border-r border-outline-variant text-body-sm">홀1</div>
|
||||
<div class="col-span-4 bg-primary-container/80 flex items-center justify-center text-[10px] text-white">행사 A</div>
|
||||
<div class="bg-surface-container-high"></div>
|
||||
<div class="bg-surface-container-high"></div>
|
||||
<div class="col-span-3 bg-amber-400 flex items-center justify-center text-[10px] text-on-surface">가예약 B</div>
|
||||
<div class="bg-surface-container-high"></div>
|
||||
<div class="bg-surface-container-high"></div>
|
||||
<div class="bg-surface-container-high"></div>
|
||||
<div class="bg-surface-container-high"></div>
|
||||
<div class="bg-surface-container-high"></div>
|
||||
</div>
|
||||
<!-- Hall 1A (Half) -->
|
||||
<div class="calendar-grid group">
|
||||
<div class="p-3 font-medium bg-surface-container-low text-center border-r border-outline-variant text-body-sm opacity-60">홀1A</div>
|
||||
<div class="bg-surface-container-high"></div>
|
||||
<div class="bg-surface-container-high"></div>
|
||||
<div class="bg-surface-container-high"></div>
|
||||
<div class="col-span-8 bg-[#D9EAF7] border-y-2 border-primary-container relative">
|
||||
<span class="absolute inset-0 flex items-center justify-center text-[10px] text-primary font-bold">선택된 기간</span>
|
||||
</div>
|
||||
<div class="bg-surface-container-high"></div>
|
||||
<div class="bg-surface-container-high"></div>
|
||||
<div class="bg-surface-container-high"></div>
|
||||
</div>
|
||||
<!-- Hall 1B (Half) -->
|
||||
<div class="calendar-grid group">
|
||||
<div class="p-3 font-medium bg-surface-container-low text-center border-r border-outline-variant text-body-sm opacity-60">홀1B</div>
|
||||
<div class="bg-surface-container-high"></div>
|
||||
<div class="bg-surface-container-high"></div>
|
||||
<div class="bg-surface-container-high"></div>
|
||||
<div class="bg-surface-container-high"></div>
|
||||
<div class="bg-surface-container-high"></div>
|
||||
<div class="bg-surface-container-high"></div>
|
||||
<div class="bg-surface-container-high"></div>
|
||||
<div class="bg-surface-container-high"></div>
|
||||
<div class="bg-surface-container-high"></div>
|
||||
<div class="bg-surface-container-high"></div>
|
||||
<div class="bg-surface-container-high"></div>
|
||||
<div class="bg-surface-container-high"></div>
|
||||
<div class="bg-surface-container-high"></div>
|
||||
<div class="bg-surface-container-high"></div>
|
||||
</div>
|
||||
<!-- Hall 2 -->
|
||||
<div class="calendar-grid group">
|
||||
<div class="p-3 font-bold bg-surface-container-low text-center border-r border-outline-variant text-body-sm">홀2</div>
|
||||
<div class="bg-surface-container-high"></div>
|
||||
<div class="bg-surface-container-high"></div>
|
||||
<div class="col-span-5 bg-primary-container/80 flex items-center justify-center text-[10px] text-white">국제 식품전</div>
|
||||
<div class="bg-surface-container-high"></div>
|
||||
<div class="bg-surface-container-high"></div>
|
||||
<div class="bg-surface-container-high"></div>
|
||||
<div class="bg-surface-container-high"></div>
|
||||
<div class="bg-surface-container-high"></div>
|
||||
<div class="bg-surface-container-high"></div>
|
||||
<div class="bg-surface-container-high"></div>
|
||||
</div>
|
||||
<!-- Hall 3 -->
|
||||
<div class="calendar-grid group">
|
||||
<div class="p-3 font-bold bg-surface-container-low text-center border-r border-outline-variant text-body-sm">홀3</div>
|
||||
<div class="bg-surface-container-high"></div>
|
||||
<div class="bg-surface-container-high"></div>
|
||||
<div class="bg-surface-container-high"></div>
|
||||
<div class="bg-surface-container-high"></div>
|
||||
<div class="bg-surface-container-high"></div>
|
||||
<div class="bg-surface-container-high"></div>
|
||||
<div class="bg-surface-container-high"></div>
|
||||
<div class="bg-surface-container-high"></div>
|
||||
<div class="bg-surface-container-high"></div>
|
||||
<div class="bg-surface-container-high"></div>
|
||||
<div class="bg-surface-container-high"></div>
|
||||
<div class="bg-surface-container-high"></div>
|
||||
<div class="bg-surface-container-high"></div>
|
||||
<div class="bg-surface-container-high"></div>
|
||||
</div>
|
||||
<!-- Hall 4 -->
|
||||
<div class="calendar-grid group">
|
||||
<div class="p-3 font-bold bg-surface-container-low text-center border-r border-outline-variant text-body-sm">홀4</div>
|
||||
<div class="bg-surface-container-high"></div>
|
||||
<div class="col-span-13 bg-surface-container-high"></div>
|
||||
</div>
|
||||
<!-- Hall 5 -->
|
||||
<div class="calendar-grid group">
|
||||
<div class="p-3 font-bold bg-surface-container-low text-center border-r border-outline-variant text-body-sm">홀5</div>
|
||||
<div class="bg-surface-container-high"></div>
|
||||
<div class="bg-surface-container-high"></div>
|
||||
<div class="bg-surface-container-high"></div>
|
||||
<div class="bg-surface-container-high"></div>
|
||||
<div class="bg-surface-container-high"></div>
|
||||
<div class="bg-surface-container-high"></div>
|
||||
<div class="bg-surface-container-high"></div>
|
||||
<div class="bg-surface-container-high"></div>
|
||||
<div class="bg-surface-container-high"></div>
|
||||
<div class="bg-surface-container-high"></div>
|
||||
<div class="bg-surface-container-high"></div>
|
||||
<div class="bg-surface-container-high"></div>
|
||||
<div class="bg-surface-container-high"></div>
|
||||
<div class="bg-surface-container-high"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Guidance Info -->
|
||||
<div class="mt-4 p-4 bg-white border border-outline-variant rounded-xl flex gap-4 items-start">
|
||||
<span class="material-symbols-outlined text-primary">info</span>
|
||||
<div>
|
||||
<p class="font-bold text-body-base mb-1">배정 시 유의사항</p>
|
||||
<ul class="text-on-surface-variant text-body-sm list-disc list-inside">
|
||||
<li>Half-Hall 배정 시 전시장 가동률에 따라 AI가 인접 홀 동선을 자동 계산합니다.</li>
|
||||
<li>장치일 및 철거일은 본행사 임대료의 50%가 적용됩니다.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<!-- Right 1/3: Booking Form & Quote -->
|
||||
<section class="w-1/3 bg-white border-l border-outline-variant p-stack-lg overflow-y-auto custom-scrollbar flex flex-col gap-6">
|
||||
<!-- Booking Form Card -->
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<span class="w-1 h-6 bg-primary rounded-full"></span>
|
||||
<h3 class="font-title-sm text-title-sm">배정 신청</h3>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="col-span-2">
|
||||
<label class="block text-label-xs text-on-surface-variant mb-1">대상 홀</label>
|
||||
<select class="w-full border-outline-variant rounded-lg text-body-sm focus:border-primary">
|
||||
<option>제1전시장 홀1A (Half-Hall)</option>
|
||||
<option>제1전시장 홀1B (Half-Hall)</option>
|
||||
<option>제1전시장 홀2</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-label-xs text-on-surface-variant mb-1">장치 기간</label>
|
||||
<input class="w-full border-outline-variant rounded-lg text-body-sm" type="text" value="11/04 - 11/05"/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-label-xs text-on-surface-variant mb-1">본행사 기간</label>
|
||||
<input class="w-full border-outline-variant rounded-lg text-body-sm" type="text" value="11/06 - 11/11"/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-label-xs text-on-surface-variant mb-1">철거 기간</label>
|
||||
<input class="w-full border-outline-variant rounded-lg text-body-sm" type="text" value="11/12 - 11/12"/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-label-xs text-on-surface-variant mb-1">목표 부스 수</label>
|
||||
<input class="w-full border-outline-variant rounded-lg text-body-sm font-semibold" type="number" value="510"/>
|
||||
</div>
|
||||
<div class="col-span-2">
|
||||
<label class="block text-label-xs text-on-surface-variant mb-1">용도</label>
|
||||
<select class="w-full border-outline-variant rounded-lg text-body-sm focus:border-primary">
|
||||
<option>산업 전문 전시회 (B2B)</option>
|
||||
<option>일반 소비재 전시회 (B2C)</option>
|
||||
<option>컨벤션 및 기업행사</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Auto Quote Card -->
|
||||
<div class="p-6 bg-surface-container-low border border-outline-variant rounded-xl relative overflow-hidden">
|
||||
<!-- AI Decorative Sparkle -->
|
||||
<div class="absolute top-4 right-4">
|
||||
<span class="material-symbols-outlined text-primary opacity-30 text-3xl">auto_awesome</span>
|
||||
</div>
|
||||
<h4 class="font-bold text-body-base text-primary flex items-center gap-2 mb-4">
|
||||
자동 견적(가견적)
|
||||
<span class="text-[10px] bg-primary text-white px-1.5 py-0.5 rounded">AI Draft</span>
|
||||
</h4>
|
||||
<div class="space-y-3 font-mono text-[13px] text-on-surface-variant">
|
||||
<div class="flex justify-between items-start">
|
||||
<span>기본 임대료</span>
|
||||
<div class="text-right">
|
||||
<p>2,250원/㎡ × 4,941㎡ × 4일</p>
|
||||
<p class="text-on-surface font-semibold">₩44,469,000</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span>성수기 할증 (+10%)</span>
|
||||
<span class="text-on-surface">₩4,446,900</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span>제1전시장 프리미엄 (+10%)</span>
|
||||
<span class="text-on-surface">₩4,446,900</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span>관리비 예치금 (15%)</span>
|
||||
<span class="text-on-surface">₩5,337,200</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="border-t border-dashed border-outline-variant my-4 pt-4 flex justify-between items-end">
|
||||
<span class="font-bold text-body-base">총 합계</span>
|
||||
<span class="text-2xl font-display-lg text-on-surface font-bold">₩58,700,000</span>
|
||||
</div>
|
||||
<p class="text-[11px] text-outline italic mt-4 leading-relaxed">
|
||||
* 위 금액은 공시요율 기준 가견적이며, 실제 청구 금액은 킨텍스 심사 결과 및 확정 배정 조건에 따라 변동될 수 있습니다.
|
||||
</p>
|
||||
</div>
|
||||
<!-- Action Buttons -->
|
||||
<div class="flex flex-col gap-3 mt-auto pb-4">
|
||||
<button class="w-full py-3 border border-primary text-primary font-bold rounded-xl flex items-center justify-center gap-2 hover:bg-primary/5 transition-colors">
|
||||
<span class="material-symbols-outlined">picture_as_pdf</span>
|
||||
가견적 PDF 다운로드
|
||||
</button>
|
||||
<button class="w-full py-4 bg-primary text-white font-bold rounded-xl flex items-center justify-center gap-2 shadow-lg shadow-primary/20 hover:brightness-110 transition-all">
|
||||
<span class="material-symbols-outlined">description</span>
|
||||
배정 신청서(HWP) 생성
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<!-- Footer (Authority Source: JSON) -->
|
||||
<footer class="flex justify-between items-center px-container-padding py-stack-sm w-full bg-surface-dim border-t border-outline-variant">
|
||||
<div class="flex items-center gap-8">
|
||||
<span class="font-label-md font-bold text-on-surface">KINTEX AI EMS</span>
|
||||
<p class="font-label-xs text-on-surface-variant">© 2024 KINTEX AI Exhibition Management System. All rights reserved.</p>
|
||||
</div>
|
||||
<div class="flex gap-6">
|
||||
<a class="font-label-xs text-on-surface-variant hover:text-primary transition-opacity underline" href="#">이용약관</a>
|
||||
<a class="font-label-xs text-on-surface-variant hover:text-primary transition-opacity underline" href="#">개인정보처리방침</a>
|
||||
<a class="font-label-xs text-on-surface-variant hover:text-primary transition-opacity underline" href="#">시스템 상태</a>
|
||||
</div>
|
||||
</footer>
|
||||
</main>
|
||||
<script>
|
||||
// Micro-interaction for form inputs
|
||||
document.querySelectorAll('input, select').forEach(el => {
|
||||
el.addEventListener('focus', () => {
|
||||
el.parentElement.classList.add('scale-[1.01]');
|
||||
el.style.transition = 'all 0.2s ease-out';
|
||||
});
|
||||
el.addEventListener('blur', () => {
|
||||
el.parentElement.classList.remove('scale-[1.01]');
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body></html>
|
||||
@ -0,0 +1,312 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html class="light" lang="ko"><head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||
<title>Booth Sales Inventory - KINTEX AI System</title>
|
||||
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Noto+Sans+KR:wght@400;500;700&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<style>
|
||||
body { font-family: 'Noto Sans KR', 'Inter', sans-serif; }
|
||||
.material-symbols-outlined { font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24; }
|
||||
.booth-grid { display: grid; grid-template-columns: repeat(10, 1fr); gap: 4px; }
|
||||
.booth-rect { transition: all 0.2s ease; cursor: pointer; }
|
||||
.booth-rect:hover { transform: scale(1.05); z-index: 10; box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1); }
|
||||
.glass-panel { backdrop-filter: blur(8px); background-color: rgba(255, 255, 255, 0.8); }
|
||||
</style>
|
||||
<script id="tailwind-config">
|
||||
tailwind.config = {
|
||||
darkMode: "class",
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
"primary": "#004e8b",
|
||||
"primary-container": "#0066b3",
|
||||
"on-primary": "#ffffff",
|
||||
"surface": "#f9f9ff",
|
||||
"surface-container": "#e9edff",
|
||||
"outline-variant": "#c1c7d3",
|
||||
"outline": "#717782",
|
||||
"secondary-container": "#6d4aff",
|
||||
"on-surface-variant": "#414751",
|
||||
"on-surface": "#141b2c",
|
||||
"status-sold": "#12B76A",
|
||||
"status-hold": "#F79009",
|
||||
"status-reserved": "#0066b3",
|
||||
"status-vacant": "#F2F4F7"
|
||||
},
|
||||
borderRadius: {
|
||||
"DEFAULT": "2px",
|
||||
"lg": "4px",
|
||||
"xl": "8px",
|
||||
"full": "12px"
|
||||
},
|
||||
spacing: {
|
||||
"gutter": "16px",
|
||||
"container-padding": "24px"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body class="bg-surface text-on-surface overflow-hidden">
|
||||
<!-- Sidebar Wrapper -->
|
||||
<aside class="h-screen w-64 fixed left-0 top-0 flex flex-col bg-surface-container border-r border-outline-variant z-50">
|
||||
<div class="p-6">
|
||||
<img alt="KINTEX Logo" class="h-8 object-contain" src="https://lh3.googleusercontent.com/aida-public/AB6AXuAFn-uBxBpfJabRpxvQno297nAxC-6nbrn_qWXXcV191BJxE-eDCP-bg_8JejRjtGgoE7dDQrZ2N6aMP6z05nQfAZidUrUXVauIEGr2Jb-ddW5m_N87VQg370_lb6mUDU4jcg1xUEw9CxsQ-H9EM5oi2Xh_of5wd0Z2O1hAqV-wHJbQPSb_knYzpMovyd6RJ3--xcMMkI9hMlicVtUkNg6MpzWASrqzCcOTUqDA-noP2SHUErDs0uzXcbfKZJxfw3xMtk2xKO1HMZI"/>
|
||||
<div class="mt-4">
|
||||
<h2 class="text-title-sm font-bold text-primary">KINTEX Admin</h2>
|
||||
<p class="text-label-md text-on-surface-variant">Enterprise Manager</p>
|
||||
</div>
|
||||
</div>
|
||||
<nav class="flex-1 px-3 space-y-1 overflow-y-auto">
|
||||
<a class="flex items-center gap-3 px-3 py-2 text-on-surface-variant hover:bg-surface-container-high transition-colors rounded" href="#">
|
||||
<span class="material-symbols-outlined">dashboard</span>
|
||||
<span class="text-body-base">대시보드</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-3 py-2 text-on-surface-variant hover:bg-surface-container-high transition-colors rounded" href="#">
|
||||
<span class="material-symbols-outlined">calendar_month</span>
|
||||
<span class="text-body-base">전시 일정</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-3 py-2 text-primary font-bold border-r-4 border-primary bg-primary-container/10" href="#">
|
||||
<span class="material-symbols-outlined">payments</span>
|
||||
<span class="text-body-base">부스 판매</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-3 py-2 text-on-surface-variant hover:bg-surface-container-high transition-colors rounded" href="#">
|
||||
<span class="material-symbols-outlined">groups</span>
|
||||
<span class="text-body-base">참가업체 관리</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-3 py-2 text-on-surface-variant hover:bg-surface-container-high transition-colors rounded" href="#">
|
||||
<span class="material-symbols-outlined">settings</span>
|
||||
<span class="text-body-base">설정</span>
|
||||
</a>
|
||||
</nav>
|
||||
<div class="p-4 border-t border-outline-variant space-y-1">
|
||||
<a class="flex items-center gap-3 px-3 py-2 text-on-surface-variant hover:bg-surface-container-high transition-colors rounded" href="#">
|
||||
<span class="material-symbols-outlined">help</span>
|
||||
<span class="text-body-base">지원</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-3 py-2 text-on-surface-variant hover:bg-surface-container-high transition-colors rounded" href="#">
|
||||
<span class="material-symbols-outlined">logout</span>
|
||||
<span class="text-body-base">로그아웃</span>
|
||||
</a>
|
||||
</div>
|
||||
</aside>
|
||||
<!-- Main Content Area -->
|
||||
<main class="ml-64 h-screen flex flex-col overflow-hidden">
|
||||
<!-- Top Navigation Bar -->
|
||||
<header class="h-16 flex items-center justify-between px-gutter bg-surface border-b border-outline-variant">
|
||||
<div class="flex items-center gap-4">
|
||||
<h1 class="text-headline-md text-primary font-bold">부스 판매 인벤토리</h1>
|
||||
<div class="relative w-64">
|
||||
<span class="material-symbols-outlined absolute left-3 top-1/2 -translate-y-1/2 text-outline">search</span>
|
||||
<input class="w-full pl-10 pr-4 py-1.5 border border-outline-variant rounded focus:ring-1 focus:ring-primary focus:border-primary text-body-sm bg-white" placeholder="부스 번호 또는 업체 검색" type="text"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<button class="px-4 py-2 text-body-base font-medium border border-outline text-on-surface hover:bg-surface-container transition-colors rounded flex items-center gap-2">
|
||||
<span class="material-symbols-outlined text-base">edit_square</span>
|
||||
일괄 상태 변경
|
||||
</button>
|
||||
<button class="px-4 py-2 text-body-base font-medium bg-primary text-on-primary hover:bg-primary-container transition-all rounded flex items-center gap-2 shadow-sm">
|
||||
<span class="material-symbols-outlined text-base">download</span>
|
||||
CSV 내보내기
|
||||
</button>
|
||||
<div class="w-px h-6 bg-outline-variant mx-2"></div>
|
||||
<div class="flex items-center gap-2 cursor-pointer">
|
||||
<div class="w-8 h-8 rounded-full bg-primary-container flex items-center justify-center text-on-primary text-label-md font-bold">AD</div>
|
||||
<span class="material-symbols-outlined text-outline">notifications</span>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<!-- KPI Band -->
|
||||
<section class="p-container-padding grid grid-cols-6 gap-4 bg-surface-container-low border-b border-outline-variant">
|
||||
<div class="bg-white p-4 border border-outline-variant rounded shadow-sm">
|
||||
<div class="text-label-md text-on-surface-variant mb-1">총 부스</div>
|
||||
<div class="text-headline-md font-bold text-on-surface">510</div>
|
||||
</div>
|
||||
<div class="bg-white p-4 border-l-4 border-l-status-sold border-y border-r border-outline-variant rounded shadow-sm">
|
||||
<div class="text-label-md text-on-surface-variant mb-1">판매</div>
|
||||
<div class="text-headline-md font-bold text-on-surface">128</div>
|
||||
</div>
|
||||
<div class="bg-white p-4 border-l-4 border-l-status-hold border-y border-r border-outline-variant rounded shadow-sm">
|
||||
<div class="text-label-md text-on-surface-variant mb-1">홀드</div>
|
||||
<div class="text-headline-md font-bold text-on-surface">12</div>
|
||||
</div>
|
||||
<div class="bg-white p-4 border-l-4 border-l-status-reserved border-y border-r border-outline-variant rounded shadow-sm">
|
||||
<div class="text-label-md text-on-surface-variant mb-1">예약</div>
|
||||
<div class="text-headline-md font-bold text-on-surface">22</div>
|
||||
</div>
|
||||
<div class="bg-white p-4 border border-outline-variant rounded shadow-sm">
|
||||
<div class="text-label-md text-on-surface-variant mb-1">잔여</div>
|
||||
<div class="text-headline-md font-bold text-on-surface">348</div>
|
||||
</div>
|
||||
<div class="bg-white p-4 border border-outline-variant rounded shadow-sm">
|
||||
<div class="text-label-md text-on-surface-variant mb-1">판매율</div>
|
||||
<div class="text-headline-md font-bold text-primary">25%</div>
|
||||
<div class="mt-2 w-full bg-surface-container h-1 rounded-full overflow-hidden">
|
||||
<div class="bg-primary h-full" style="width: 25%;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<!-- Main Workspace -->
|
||||
<section class="flex flex-1 overflow-hidden">
|
||||
<!-- Left: Hall Floor Plan -->
|
||||
<div class="w-2/3 flex flex-col border-r border-outline-variant bg-white">
|
||||
<div class="p-4 flex items-center justify-between border-b border-outline-variant">
|
||||
<div class="flex items-center bg-surface-container rounded p-1">
|
||||
<button class="px-6 py-1.5 bg-white shadow-sm text-primary font-bold rounded text-body-base transition-all">맵</button>
|
||||
<button class="px-6 py-1.5 text-on-surface-variant text-body-base hover:text-on-surface transition-colors">테이블</button>
|
||||
</div>
|
||||
<div class="flex items-center gap-4 text-label-md text-on-surface-variant">
|
||||
<div class="flex items-center gap-1.5"><span class="w-3 h-3 bg-status-sold rounded-sm"></span> 판매</div>
|
||||
<div class="flex items-center gap-1.5"><span class="w-3 h-3 bg-status-hold rounded-sm"></span> 홀드</div>
|
||||
<div class="flex items-center gap-1.5"><span class="w-3 h-3 bg-status-reserved rounded-sm"></span> 예약</div>
|
||||
<div class="flex items-center gap-1.5"><span class="w-3 h-3 bg-status-vacant rounded-sm border border-outline-variant"></span> 공실</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1 p-8 overflow-auto bg-slate-50 relative group">
|
||||
<div class="w-full max-w-4xl mx-auto bg-white border border-outline shadow-lg p-12 rounded relative min-h-[600px]">
|
||||
<!-- Hall Labels -->
|
||||
<div class="absolute top-4 left-1/2 -translate-x-1/2 text-on-surface-variant/30 text-headline-md font-extrabold uppercase tracking-widest pointer-events-none">Hall 1 - Exhibition Center I</div>
|
||||
<!-- Booth Map Grid Visualization -->
|
||||
<div class="grid grid-cols-10 gap-2 mb-8">
|
||||
<!-- Generation of various status booths -->
|
||||
<div class="booth-rect bg-status-sold text-white p-3 h-16 flex items-center justify-center font-bold text-label-md rounded shadow-sm border border-black/5" title="A-101 (판매)">A-101</div>
|
||||
<div class="booth-rect bg-status-sold border-primary-container ring-2 ring-primary bg-status-sold text-white p-3 h-16 flex items-center justify-center font-bold text-label-md rounded shadow-md z-10" title="A-102 (판매 - 선택됨)">A-102</div>
|
||||
<div class="booth-rect bg-status-hold text-white p-3 h-16 flex items-center justify-center font-bold text-label-md rounded shadow-sm border border-black/5" title="A-103 (홀드)">A-103</div>
|
||||
<div class="booth-rect bg-status-vacant text-on-surface-variant p-3 h-16 flex items-center justify-center font-bold text-label-md rounded shadow-sm border border-outline-variant" title="A-104 (공실)">A-104</div>
|
||||
<div class="booth-rect bg-status-reserved text-white p-3 h-16 flex items-center justify-center font-bold text-label-md rounded shadow-sm border border-black/5" title="A-105 (예약)">A-105</div>
|
||||
<div class="booth-rect bg-status-vacant text-on-surface-variant p-3 h-16 flex items-center justify-center font-bold text-label-md rounded shadow-sm border border-outline-variant">A-106</div>
|
||||
<div class="booth-rect bg-status-sold text-white p-3 h-16 flex items-center justify-center font-bold text-label-md rounded shadow-sm border border-black/5">A-107</div>
|
||||
<div class="booth-rect bg-status-vacant text-on-surface-variant p-3 h-16 flex items-center justify-center font-bold text-label-md rounded shadow-sm border border-outline-variant">A-108</div>
|
||||
<div class="booth-rect bg-status-hold text-white p-3 h-16 flex items-center justify-center font-bold text-label-md rounded shadow-sm border border-black/5">A-109</div>
|
||||
<div class="booth-rect bg-status-vacant text-on-surface-variant p-3 h-16 flex items-center justify-center font-bold text-label-md rounded shadow-sm border border-outline-variant">A-110</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-10 gap-2 mb-8">
|
||||
<div class="booth-rect bg-status-vacant text-on-surface-variant p-3 h-16 flex items-center justify-center font-bold text-label-md rounded shadow-sm border border-outline-variant">B-101</div>
|
||||
<div class="booth-rect bg-status-vacant text-on-surface-variant p-3 h-16 flex items-center justify-center font-bold text-label-md rounded shadow-sm border border-outline-variant">B-102</div>
|
||||
<div class="booth-rect bg-status-reserved text-white p-3 h-16 flex items-center justify-center font-bold text-label-md rounded shadow-sm border border-black/5">B-103</div>
|
||||
<div class="booth-rect bg-status-sold text-white p-3 h-16 flex items-center justify-center font-bold text-label-md rounded shadow-sm border border-black/5">B-104</div>
|
||||
<div class="booth-rect bg-status-vacant text-on-surface-variant p-3 h-16 flex items-center justify-center font-bold text-label-md rounded shadow-sm border border-outline-variant">B-105</div>
|
||||
<div class="booth-rect bg-status-sold text-white p-3 h-16 flex items-center justify-center font-bold text-label-md rounded shadow-sm border border-black/5">B-106</div>
|
||||
<div class="booth-rect bg-status-vacant text-on-surface-variant p-3 h-16 flex items-center justify-center font-bold text-label-md rounded shadow-sm border border-outline-variant">B-107</div>
|
||||
<div class="booth-rect bg-status-vacant text-on-surface-variant p-3 h-16 flex items-center justify-center font-bold text-label-md rounded shadow-sm border border-outline-variant">B-108</div>
|
||||
<div class="booth-rect bg-status-vacant text-on-surface-variant p-3 h-16 flex items-center justify-center font-bold text-label-md rounded shadow-sm border border-outline-variant">B-109</div>
|
||||
<div class="booth-rect bg-status-vacant text-on-surface-variant p-3 h-16 flex items-center justify-center font-bold text-label-md rounded shadow-sm border border-outline-variant">B-110</div>
|
||||
</div>
|
||||
<!-- Spacer row representing aisle -->
|
||||
<div class="h-12 w-full bg-slate-50 flex items-center justify-center mb-8 border-y border-outline-variant">
|
||||
<span class="text-label-xs font-bold text-on-surface-variant uppercase tracking-[0.5em]">Main Aisle 100</span>
|
||||
</div>
|
||||
<div class="grid grid-cols-10 gap-2 mb-8">
|
||||
<div class="booth-rect bg-status-vacant text-on-surface-variant p-3 h-16 flex items-center justify-center font-bold text-label-md rounded shadow-sm border border-outline-variant">C-101</div>
|
||||
<div class="booth-rect bg-status-vacant text-on-surface-variant p-3 h-16 flex items-center justify-center font-bold text-label-md rounded shadow-sm border border-outline-variant">C-102</div>
|
||||
<div class="booth-rect bg-status-sold text-white p-3 h-16 flex items-center justify-center font-bold text-label-md rounded shadow-sm border border-black/5">C-103</div>
|
||||
<div class="booth-rect bg-status-sold text-white p-3 h-16 flex items-center justify-center font-bold text-label-md rounded shadow-sm border border-black/5">C-104</div>
|
||||
<div class="booth-rect bg-status-vacant text-on-surface-variant p-3 h-16 flex items-center justify-center font-bold text-label-md rounded shadow-sm border border-outline-variant">C-105</div>
|
||||
<div class="booth-rect bg-status-vacant text-on-surface-variant p-3 h-16 flex items-center justify-center font-bold text-label-md rounded shadow-sm border border-outline-variant">C-106</div>
|
||||
<div class="booth-rect bg-status-vacant text-on-surface-variant p-3 h-16 flex items-center justify-center font-bold text-label-md rounded shadow-sm border border-outline-variant">C-107</div>
|
||||
<div class="booth-rect bg-status-hold text-white p-3 h-16 flex items-center justify-center font-bold text-label-md rounded shadow-sm border border-black/5">C-108</div>
|
||||
<div class="booth-rect bg-status-vacant text-on-surface-variant p-3 h-16 flex items-center justify-center font-bold text-label-md rounded shadow-sm border border-outline-variant">C-109</div>
|
||||
<div class="booth-rect bg-status-vacant text-on-surface-variant p-3 h-16 flex items-center justify-center font-bold text-label-md rounded shadow-sm border border-outline-variant">C-110</div>
|
||||
</div>
|
||||
<!-- Floating Zoom Controls -->
|
||||
<div class="absolute bottom-6 right-6 flex flex-col gap-2">
|
||||
<button class="w-10 h-10 bg-white border border-outline-variant rounded shadow flex items-center justify-center hover:bg-surface-container transition-colors"><span class="material-symbols-outlined">add</span></button>
|
||||
<button class="w-10 h-10 bg-white border border-outline-variant rounded shadow flex items-center justify-center hover:bg-surface-container transition-colors"><span class="material-symbols-outlined">remove</span></button>
|
||||
<button class="w-10 h-10 bg-white border border-outline-variant rounded shadow flex items-center justify-center hover:bg-surface-container transition-colors"><span class="material-symbols-outlined">restart_alt</span></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Right: Detail Panel -->
|
||||
<div class="w-1/3 bg-white flex flex-col">
|
||||
<div class="p-container-padding border-b border-outline-variant bg-surface">
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<span class="px-2 py-0.5 bg-primary text-on-primary text-label-xs rounded font-bold">판매 중</span>
|
||||
<span class="text-label-md text-on-surface-variant font-medium">부스 상세 정보</span>
|
||||
</div>
|
||||
<h2 class="text-title-sm font-bold text-on-surface">A-102 · 독립부스 · 6×3m</h2>
|
||||
</div>
|
||||
<div class="flex-1 overflow-y-auto p-container-padding space-y-6">
|
||||
<!-- Status Selection -->
|
||||
<div class="space-y-2">
|
||||
<label class="text-label-md font-bold text-on-surface flex items-center gap-1.5">
|
||||
<span class="material-symbols-outlined text-base">fact_check</span>
|
||||
부스 상태
|
||||
</label>
|
||||
<select class="w-full border-outline-variant rounded text-body-base py-2.5 focus:ring-primary focus:border-primary">
|
||||
<option value="vacant">공실</option>
|
||||
<option value="hold">홀드</option>
|
||||
<option value="reserved">예약</option>
|
||||
<option selected="" value="sold">판매</option>
|
||||
</select>
|
||||
</div>
|
||||
<!-- Exhibitor Info -->
|
||||
<div class="space-y-2">
|
||||
<label class="text-label-md font-bold text-on-surface flex items-center gap-1.5">
|
||||
<span class="material-symbols-outlined text-base">business</span>
|
||||
참가 업체명
|
||||
</label>
|
||||
<div class="relative">
|
||||
<input class="w-full border-outline-variant rounded text-body-base py-2.5 pl-10 bg-slate-50" type="text" value="(주)한빛로보틱스"/>
|
||||
<span class="material-symbols-outlined absolute left-3 top-1/2 -translate-y-1/2 text-outline text-base">search</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Pricing Section -->
|
||||
<div class="p-4 border border-outline-variant rounded bg-slate-50 space-y-4">
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-label-md text-on-surface-variant">기본 단가</span>
|
||||
<span class="text-body-base font-bold">₩13,500,000</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="flex items-center gap-1.5">
|
||||
<span class="text-label-md text-on-surface-variant">프리미엄 계수</span>
|
||||
<span class="material-symbols-outlined text-primary text-xs" style="font-variation-settings: 'FILL' 1;">spark</span>
|
||||
</div>
|
||||
<span class="text-body-base font-bold text-secondary">1.2</span>
|
||||
</div>
|
||||
<div class="pt-3 border-t border-outline-variant flex justify-between items-center">
|
||||
<span class="text-label-md font-bold text-on-surface">최종 판매가</span>
|
||||
<span class="text-title-sm font-extrabold text-primary">₩16,200,000</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Memo Box -->
|
||||
<div class="space-y-2">
|
||||
<label class="text-label-md font-bold text-on-surface flex items-center gap-1.5">
|
||||
<span class="material-symbols-outlined text-base">notes</span>
|
||||
관리자 메모
|
||||
</label>
|
||||
<textarea class="w-full border-outline-variant rounded text-body-sm p-3 focus:ring-primary focus:border-primary resize-none" placeholder="부스 배정 및 특이사항 기재" rows="4">해당 부스는 입구 근처 프리미엄 구역으로 로봇 시연을 위해 전력 추가 증설 요청함. 코너 프리미엄 적용 완료.</textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-container-padding bg-slate-50 border-t border-outline-variant">
|
||||
<button class="w-full py-3 bg-primary text-on-primary font-bold rounded shadow hover:bg-primary-container transition-all active:scale-[0.98] flex items-center justify-center gap-2">
|
||||
<span class="material-symbols-outlined text-base">save</span>
|
||||
변경사항 저장
|
||||
</button>
|
||||
<button class="w-full mt-2 py-3 bg-white border border-outline text-on-surface-variant font-medium rounded hover:bg-surface-container transition-all text-body-sm">
|
||||
판매 이력 보기
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
<!-- Micro-interaction overlay script -->
|
||||
<script>
|
||||
document.querySelectorAll('.booth-rect').forEach(booth => {
|
||||
booth.addEventListener('click', function() {
|
||||
// Simplified UI feedback for selection
|
||||
document.querySelectorAll('.booth-rect').forEach(b => {
|
||||
b.classList.remove('ring-2', 'ring-primary', 'shadow-md', 'z-10');
|
||||
b.style.opacity = '1';
|
||||
});
|
||||
this.classList.add('ring-2', 'ring-primary', 'shadow-md', 'z-10');
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body></html>
|
||||
@ -0,0 +1,364 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html class="light" lang="ko"><head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||
<title>KINTEX Exhibitor Portal - Booth Selection</title>
|
||||
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Geist:wght@400;500;600&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<style>
|
||||
.material-symbols-outlined {
|
||||
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
|
||||
vertical-align: middle;
|
||||
}
|
||||
@keyframes pulse-selection {
|
||||
0% { box-shadow: 0 0 0 0 rgba(0, 78, 139, 0.4); }
|
||||
70% { box-shadow: 0 0 0 10px rgba(0, 78, 139, 0); }
|
||||
100% { box-shadow: 0 0 0 0 rgba(0, 78, 139, 0); }
|
||||
}
|
||||
.booth-selected-pulse {
|
||||
animation: pulse-selection 2s infinite;
|
||||
}
|
||||
.glass-panel {
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
</style>
|
||||
<script id="tailwind-config">
|
||||
tailwind.config = {
|
||||
darkMode: "class",
|
||||
theme: {
|
||||
extend: {
|
||||
"colors": {
|
||||
"surface-container-lowest": "#ffffff",
|
||||
"secondary-fixed": "#e5deff",
|
||||
"secondary-container": "#6d4aff",
|
||||
"tertiary-fixed": "#8ef7c4",
|
||||
"on-tertiary-container": "#8ef7c3",
|
||||
"surface": "#f9f9ff",
|
||||
"inverse-on-surface": "#edf0ff",
|
||||
"on-secondary-container": "#f3edff",
|
||||
"inverse-primary": "#a2c9ff",
|
||||
"on-secondary-fixed": "#1b0063",
|
||||
"surface-variant": "#dbe2f9",
|
||||
"on-surface-variant": "#414751",
|
||||
"error-container": "#ffdad6",
|
||||
"outline-variant": "#c1c7d3",
|
||||
"secondary": "#5427e6",
|
||||
"tertiary-container": "#00734e",
|
||||
"surface-dim": "#d2daf0",
|
||||
"surface-bright": "#f9f9ff",
|
||||
"on-secondary-fixed-variant": "#4500d8",
|
||||
"surface-tint": "#0060a9",
|
||||
"on-tertiary": "#ffffff",
|
||||
"error": "#ba1a1a",
|
||||
"tertiary-fixed-dim": "#72daa9",
|
||||
"on-primary-fixed": "#001c38",
|
||||
"secondary-fixed-dim": "#c9bfff",
|
||||
"on-tertiary-fixed-variant": "#005236",
|
||||
"primary-container": "#0066b3",
|
||||
"on-primary-container": "#d2e3ff",
|
||||
"on-primary": "#ffffff",
|
||||
"on-error": "#ffffff",
|
||||
"inverse-surface": "#293041",
|
||||
"surface-container-highest": "#dbe2f9",
|
||||
"on-error-container": "#93000a",
|
||||
"background": "#f9f9ff",
|
||||
"primary": "#004e8b",
|
||||
"surface-container-high": "#e0e8ff",
|
||||
"on-tertiary-fixed": "#002113",
|
||||
"surface-container-low": "#f1f3ff",
|
||||
"on-surface": "#141b2c",
|
||||
"tertiary": "#00583b",
|
||||
"surface-container": "#e9edff",
|
||||
"primary-fixed": "#d3e4ff",
|
||||
"on-secondary": "#ffffff",
|
||||
"on-primary-fixed-variant": "#004881",
|
||||
"on-background": "#141b2c",
|
||||
"outline": "#717782",
|
||||
"primary-fixed-dim": "#a2c9ff"
|
||||
},
|
||||
"borderRadius": {
|
||||
"DEFAULT": "0.125rem",
|
||||
"lg": "0.25rem",
|
||||
"xl": "0.5rem",
|
||||
"full": "0.75rem"
|
||||
},
|
||||
"spacing": {
|
||||
"gutter": "16px",
|
||||
"base": "4px",
|
||||
"container-padding": "24px",
|
||||
"stack-md": "16px",
|
||||
"stack-lg": "24px",
|
||||
"stack-sm": "8px"
|
||||
},
|
||||
"fontFamily": {
|
||||
"title-sm": ["Inter"],
|
||||
"body-base": ["Inter"],
|
||||
"display-lg": ["Inter"],
|
||||
"label-xs": ["Geist"],
|
||||
"headline-md": ["Inter"],
|
||||
"label-md": ["Geist"],
|
||||
"body-sm": ["Inter"]
|
||||
},
|
||||
"fontSize": {
|
||||
"title-sm": ["18px", {"lineHeight": "28px", "fontWeight": "600"}],
|
||||
"body-base": ["14px", {"lineHeight": "20px", "fontWeight": "400"}],
|
||||
"display-lg": ["36px", {"lineHeight": "44px", "letterSpacing": "-0.02em", "fontWeight": "700"}],
|
||||
"label-xs": ["11px", {"lineHeight": "14px", "fontWeight": "500"}],
|
||||
"headline-md": ["24px", {"lineHeight": "32px", "fontWeight": "600"}],
|
||||
"label-md": ["12px", {"lineHeight": "16px", "letterSpacing": "0.02em", "fontWeight": "600"}],
|
||||
"body-sm": ["13px", {"lineHeight": "18px", "fontWeight": "400"}]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body class="bg-background text-on-surface font-body-base overflow-hidden">
|
||||
<!-- TopNavBar -->
|
||||
<header class="fixed top-0 left-0 right-0 h-16 bg-surface-container-lowest border-b border-outline-variant flex justify-between items-center px-container-padding z-50">
|
||||
<div class="font-headline-md text-headline-md font-bold text-primary">
|
||||
KINTEX EXHIBITOR PORTAL
|
||||
</div>
|
||||
<nav class="flex items-center gap-8">
|
||||
<div class="flex items-center gap-2 text-primary font-bold border-b-2 border-primary pb-1">
|
||||
<span class="w-6 h-6 rounded-full bg-primary text-white flex items-center justify-center text-[12px]">1</span>
|
||||
<span class="font-title-sm text-title-sm">부스 선택</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 text-on-surface-variant">
|
||||
<span class="w-6 h-6 rounded-full border border-outline-variant flex items-center justify-center text-[12px]">2</span>
|
||||
<span class="font-title-sm text-title-sm">정보 입력</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 text-on-surface-variant">
|
||||
<span class="w-6 h-6 rounded-full border border-outline-variant flex items-center justify-center text-[12px]">3</span>
|
||||
<span class="font-title-sm text-title-sm">결제</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 text-on-surface-variant">
|
||||
<span class="w-6 h-6 rounded-full border border-outline-variant flex items-center justify-center text-[12px]">4</span>
|
||||
<span class="font-title-sm text-title-sm">완료</span>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="flex items-center gap-4 text-on-surface-variant">
|
||||
<span class="material-symbols-outlined cursor-pointer hover:text-primary transition-colors">help_outline</span>
|
||||
<span class="material-symbols-outlined cursor-pointer hover:text-primary transition-colors">account_circle</span>
|
||||
</div>
|
||||
</header>
|
||||
<!-- Sidebar -->
|
||||
<aside class="fixed left-0 top-16 bottom-0 w-64 bg-surface border-r border-outline-variant flex flex-col py-stack-lg z-40">
|
||||
<div class="px-6 mb-8">
|
||||
<h2 class="text-primary font-headline-md text-headline-md">KINTEX Portal</h2>
|
||||
<p class="text-on-surface-variant font-label-md text-label-md">Exhibitor Management</p>
|
||||
</div>
|
||||
<nav class="flex-1">
|
||||
<div class="flex flex-col gap-1">
|
||||
<a class="flex items-center gap-3 px-6 py-3 text-primary font-bold border-r-4 border-primary bg-secondary-fixed" href="#">
|
||||
<span class="material-symbols-outlined">grid_view</span>
|
||||
<span class="font-label-md text-label-md">Selection</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-6 py-3 text-on-surface-variant hover:bg-surface-container-highest transition-all" href="#">
|
||||
<span class="material-symbols-outlined">info</span>
|
||||
<span class="font-label-md text-label-md">Information</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-6 py-3 text-on-surface-variant hover:bg-surface-container-highest transition-all" href="#">
|
||||
<span class="material-symbols-outlined">category</span>
|
||||
<span class="font-label-md text-label-md">Services</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-6 py-3 text-on-surface-variant hover:bg-surface-container-highest transition-all" href="#">
|
||||
<span class="material-symbols-outlined">payments</span>
|
||||
<span class="font-label-md text-label-md">Payment</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-6 py-3 text-on-surface-variant hover:bg-surface-container-highest transition-all" href="#">
|
||||
<span class="material-symbols-outlined">check_circle</span>
|
||||
<span class="font-label-md text-label-md">Complete</span>
|
||||
</a>
|
||||
</div>
|
||||
</nav>
|
||||
</aside>
|
||||
<!-- Main Content -->
|
||||
<main class="ml-64 mt-16 h-[calc(100vh-64px)] overflow-hidden flex">
|
||||
<!-- Left Section: Booth Map (2/3) -->
|
||||
<section class="w-2/3 p-stack-lg flex flex-col border-r border-outline-variant bg-surface-container-low">
|
||||
<!-- Filter Bar -->
|
||||
<div class="bg-white p-4 rounded-lg border border-outline-variant mb-6 flex gap-4 items-center shadow-sm">
|
||||
<div class="flex-1">
|
||||
<label class="block text-label-xs text-outline mb-1">가격대</label>
|
||||
<select class="w-full border-outline-variant rounded px-2 py-1.5 text-body-sm focus:ring-primary focus:border-primary">
|
||||
<option>전체 가격대</option>
|
||||
<option>₩10M - ₩20M</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<label class="block text-label-xs text-outline mb-1">유형</label>
|
||||
<select class="w-full border-outline-variant rounded px-2 py-1.5 text-body-sm focus:ring-primary focus:border-primary">
|
||||
<option>전체 유형</option>
|
||||
<option>독립 부스</option>
|
||||
<option>조립 부스</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<label class="block text-label-xs text-outline mb-1">존</label>
|
||||
<select class="w-full border-outline-variant rounded px-2 py-1.5 text-body-sm focus:ring-primary focus:border-primary">
|
||||
<option>Exhibition Hall 1 - Zone A</option>
|
||||
<option>Exhibition Hall 1 - Zone B</option>
|
||||
</select>
|
||||
</div>
|
||||
<button class="mt-5 px-4 py-1.5 bg-surface-container-high text-primary rounded font-label-md hover:bg-primary-container hover:text-white transition-colors">필터 적용</button>
|
||||
</div>
|
||||
<!-- Floor Plan Canvas -->
|
||||
<div class="flex-1 relative bg-white border border-outline-variant rounded-xl overflow-hidden shadow-inner group">
|
||||
<!-- Map Background Pattern -->
|
||||
<div class="absolute inset-0 opacity-10" style="background-image: radial-gradient(#717782 1px, transparent 1px); background-size: 20px 20px;"></div>
|
||||
<div class="relative z-10 p-12 h-full flex items-center justify-center">
|
||||
<div class="grid grid-cols-6 gap-4 w-full max-w-4xl">
|
||||
<!-- Sold Booths -->
|
||||
<div class="aspect-square bg-surface-dim border border-outline flex flex-col items-center justify-center text-on-surface-variant relative cursor-not-allowed">
|
||||
<span class="font-label-md">A-101</span>
|
||||
<span class="material-symbols-outlined text-sm">lock</span>
|
||||
</div>
|
||||
<!-- Selected Booth -->
|
||||
<div class="aspect-[2/1] col-span-2 bg-primary-container border-2 border-primary flex flex-col items-center justify-center text-white booth-selected-pulse cursor-pointer relative">
|
||||
<span class="font-bold text-lg">A-102</span>
|
||||
<span class="text-xs opacity-90">₩13,500,000</span>
|
||||
<!-- Interactive Tooltip -->
|
||||
<div class="absolute -top-16 left-1/2 -translate-x-1/2 bg-inverse-surface text-inverse-on-surface px-4 py-2 rounded-lg text-sm whitespace-nowrap shadow-xl z-20">
|
||||
A-102 · 6×3m · 독립 · ₩13,500,000
|
||||
<div class="absolute -bottom-1 left-1/2 -translate-x-1/2 w-2 h-2 bg-inverse-surface rotate-45"></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Available Booths -->
|
||||
<div class="aspect-square bg-tertiary-fixed border border-tertiary-container flex flex-col items-center justify-center text-tertiary-container hover:bg-tertiary-fixed-dim transition-colors cursor-pointer">
|
||||
<span class="font-label-md">A-103</span>
|
||||
<span class="text-[10px]">₩13.5M</span>
|
||||
</div>
|
||||
<div class="aspect-square bg-tertiary-fixed border border-tertiary-container flex flex-col items-center justify-center text-tertiary-container hover:bg-tertiary-fixed-dim transition-colors cursor-pointer">
|
||||
<span class="font-label-md">A-104</span>
|
||||
<span class="text-[10px]">₩13.5M</span>
|
||||
</div>
|
||||
<div class="aspect-square bg-tertiary-fixed border border-tertiary-container flex flex-col items-center justify-center text-tertiary-container hover:bg-tertiary-fixed-dim transition-colors cursor-pointer">
|
||||
<span class="font-label-md">A-105</span>
|
||||
<span class="text-[10px]">₩13.5M</span>
|
||||
</div>
|
||||
<!-- Row 2 -->
|
||||
<div class="aspect-square bg-surface-dim border border-outline flex flex-col items-center justify-center text-on-surface-variant cursor-not-allowed">
|
||||
<span class="font-label-md">A-106</span>
|
||||
<span class="material-symbols-outlined text-sm">lock</span>
|
||||
</div>
|
||||
<div class="aspect-square bg-tertiary-fixed border border-tertiary-container flex flex-col items-center justify-center text-tertiary-container hover:bg-tertiary-fixed-dim transition-colors cursor-pointer">
|
||||
<span class="font-label-md">A-107</span>
|
||||
</div>
|
||||
<div class="aspect-square bg-tertiary-fixed border border-tertiary-container flex flex-col items-center justify-center text-tertiary-container hover:bg-tertiary-fixed-dim transition-colors cursor-pointer">
|
||||
<span class="font-label-md">A-108</span>
|
||||
</div>
|
||||
<div class="col-span-2 aspect-[2/1] flex items-center justify-center bg-surface-container-high border border-outline-variant text-outline dashed">
|
||||
<span class="text-xs uppercase tracking-widest font-bold">Hallway</span>
|
||||
</div>
|
||||
<div class="aspect-square bg-tertiary-fixed border border-tertiary-container flex flex-col items-center justify-center text-tertiary-container hover:bg-tertiary-fixed-dim transition-colors cursor-pointer">
|
||||
<span class="font-label-md">A-109</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Floor Plan Controls -->
|
||||
<div class="absolute bottom-6 right-6 flex flex-col gap-2">
|
||||
<button class="w-10 h-10 bg-white border border-outline-variant rounded-full flex items-center justify-center shadow hover:bg-surface transition-colors">
|
||||
<span class="material-symbols-outlined">add</span>
|
||||
</button>
|
||||
<button class="w-10 h-10 bg-white border border-outline-variant rounded-full flex items-center justify-center shadow hover:bg-surface transition-colors">
|
||||
<span class="material-symbols-outlined">remove</span>
|
||||
</button>
|
||||
<button class="w-10 h-10 bg-white border border-outline-variant rounded-full flex items-center justify-center shadow hover:bg-surface transition-colors">
|
||||
<span class="material-symbols-outlined">restart_alt</span>
|
||||
</button>
|
||||
</div>
|
||||
<!-- Entrance Label -->
|
||||
<div class="absolute top-1/2 -left-4 -translate-y-1/2 rotate-90 bg-primary text-white px-6 py-1 rounded-t-lg font-bold text-xs uppercase tracking-widest">
|
||||
Main Entrance
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<!-- Right Section: Summary & AI (1/3) -->
|
||||
<section class="w-1/3 p-stack-lg flex flex-col gap-stack-lg bg-surface overflow-y-auto">
|
||||
<!-- Summary Card -->
|
||||
<div class="bg-white border border-outline-variant rounded-xl overflow-hidden shadow-sm">
|
||||
<div class="bg-primary-container h-1 w-full"></div>
|
||||
<div class="p-6">
|
||||
<div class="flex justify-between items-start mb-4">
|
||||
<div>
|
||||
<h3 class="font-headline-md text-headline-md text-primary">부스 A-102</h3>
|
||||
<p class="text-on-surface-variant font-body-sm">Exhibition Hall 1, Zone A</p>
|
||||
</div>
|
||||
<span class="bg-primary text-white px-2 py-1 rounded font-bold text-label-md">D-15</span>
|
||||
</div>
|
||||
<div class="space-y-4 mb-6">
|
||||
<div class="flex items-center gap-2 text-on-surface">
|
||||
<span class="material-symbols-outlined text-outline">aspect_ratio</span>
|
||||
<span class="font-body-base">독립 부스 · 6m × 3m (18sqm)</span>
|
||||
</div>
|
||||
<div class="pt-4 border-t border-outline-variant space-y-3">
|
||||
<label class="flex items-center gap-3 cursor-pointer group">
|
||||
<input checked="" class="w-5 h-5 text-primary border-outline rounded focus:ring-primary transition-all" type="checkbox"/>
|
||||
<span class="font-body-base group-hover:text-primary transition-colors">코너 프리미엄 적용 (+10%)</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-3 cursor-pointer group">
|
||||
<input class="w-5 h-5 text-primary border-outline rounded focus:ring-primary transition-all" type="checkbox"/>
|
||||
<span class="font-body-base group-hover:text-primary transition-colors">급배수 시설 추가 (₩150,000)</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-surface-container-low p-4 rounded-lg space-y-2 mb-6">
|
||||
<div class="flex justify-between text-body-sm text-on-surface-variant">
|
||||
<span>기본 부스 임차료</span>
|
||||
<span>₩13,500,000</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-body-sm text-on-surface-variant">
|
||||
<span>옵션 합계</span>
|
||||
<span>₩1,350,000</span>
|
||||
</div>
|
||||
<div class="pt-2 border-t border-outline-variant flex justify-between items-end">
|
||||
<span class="font-bold text-primary">합계</span>
|
||||
<div class="text-right">
|
||||
<div class="text-xl font-bold text-primary">₩14,850,000</div>
|
||||
<div class="text-[10px] text-outline">(부가가치세 포함)</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- AI Insight Box -->
|
||||
<div class="relative bg-secondary-fixed p-6 rounded-xl border border-secondary-container overflow-hidden">
|
||||
<!-- Decoration Sparkle -->
|
||||
<div class="absolute -top-4 -right-4 w-16 h-16 bg-white opacity-20 blur-2xl rounded-full"></div>
|
||||
<div class="flex items-start gap-3 relative z-10">
|
||||
<span class="material-symbols-outlined text-secondary style_interactions" style="font-variation-settings: 'FILL' 1;">auto_awesome</span>
|
||||
<div>
|
||||
<h4 class="font-label-md text-secondary font-bold mb-1 uppercase tracking-wider">AI INSIGHT</h4>
|
||||
<p class="text-body-sm text-on-secondary-fixed-variant leading-relaxed">
|
||||
A-102 부스는 메인 출입구와 인접하여 유동인구가 타 구역 대비 <span class="font-bold text-secondary">22%</span> 높을 것으로 예측됩니다.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Action Buttons -->
|
||||
<div class="mt-auto pt-6 space-y-3">
|
||||
<button class="w-full py-4 border-2 border-primary text-primary font-bold rounded-lg hover:bg-surface-container-high transition-all">
|
||||
부스 예약 (임시저장)
|
||||
</button>
|
||||
<button class="w-full py-4 bg-primary text-white font-bold rounded-lg hover:bg-primary-container shadow-lg shadow-primary/20 transition-all flex items-center justify-center gap-2">
|
||||
결제하기
|
||||
<span class="material-symbols-outlined">arrow_forward</span>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
<!-- Micro-interaction Scripts -->
|
||||
<script>
|
||||
// Simple interactive simulation
|
||||
document.querySelectorAll('input[type="checkbox"]').forEach(checkbox => {
|
||||
checkbox.addEventListener('change', function() {
|
||||
// Potential logic for updating price in real UI
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body></html>
|
||||
471
stitch_kintex_ai_system_architect/scr_21_settlement/code.html
Normal file
471
stitch_kintex_ai_system_architect/scr_21_settlement/code.html
Normal file
@ -0,0 +1,471 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html lang="ko"><head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||
<title>정산 및 결제 관리 - KINTEX AI System</title>
|
||||
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Geist:wght@400;500;600&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<script id="tailwind-config">
|
||||
tailwind.config = {
|
||||
darkMode: "class",
|
||||
theme: {
|
||||
extend: {
|
||||
"colors": {
|
||||
"primary-container": "#0066b3",
|
||||
"on-primary-container": "#d2e3ff",
|
||||
"on-primary-fixed": "#001c38",
|
||||
"secondary-fixed-dim": "#c9bfff",
|
||||
"on-tertiary-fixed-variant": "#005236",
|
||||
"on-error-container": "#93000a",
|
||||
"on-primary": "#ffffff",
|
||||
"on-error": "#ffffff",
|
||||
"inverse-surface": "#293041",
|
||||
"surface-container-highest": "#dbe2f9",
|
||||
"primary": "#004e8b",
|
||||
"background": "#f9f9ff",
|
||||
"on-surface": "#141b2c",
|
||||
"surface-container-high": "#e0e8ff",
|
||||
"on-tertiary-fixed": "#002113",
|
||||
"surface-container-low": "#f1f3ff",
|
||||
"tertiary": "#00583b",
|
||||
"surface-container": "#e9edff",
|
||||
"on-secondary": "#ffffff",
|
||||
"on-primary-fixed-variant": "#004881",
|
||||
"primary-fixed": "#d3e4ff",
|
||||
"primary-fixed-dim": "#a2c9ff",
|
||||
"on-background": "#141b2c",
|
||||
"outline": "#717782",
|
||||
"tertiary-fixed": "#8ef7c4",
|
||||
"on-tertiary-container": "#8ef7c3",
|
||||
"surface-container-lowest": "#ffffff",
|
||||
"secondary-fixed": "#e5deff",
|
||||
"secondary-container": "#6d4aff",
|
||||
"on-secondary-container": "#f3edff",
|
||||
"inverse-primary": "#a2c9ff",
|
||||
"surface": "#f9f9ff",
|
||||
"inverse-on-surface": "#edf0ff",
|
||||
"on-secondary-fixed": "#1b0063",
|
||||
"surface-variant": "#dbe2f9",
|
||||
"on-surface-variant": "#414751",
|
||||
"error-container": "#ffdad6",
|
||||
"tertiary-container": "#00734e",
|
||||
"outline-variant": "#c1c7d3",
|
||||
"secondary": "#5427e6",
|
||||
"surface-dim": "#d2daf0",
|
||||
"surface-bright": "#f9f9ff",
|
||||
"on-secondary-fixed-variant": "#4500d8",
|
||||
"surface-tint": "#0060a9",
|
||||
"on-tertiary": "#ffffff",
|
||||
"error": "#ba1a1a",
|
||||
"tertiary-fixed-dim": "#72daa9"
|
||||
},
|
||||
"borderRadius": {
|
||||
"DEFAULT": "0.125rem",
|
||||
"lg": "0.25rem",
|
||||
"xl": "0.5rem",
|
||||
"full": "0.75rem"
|
||||
},
|
||||
"spacing": {
|
||||
"gutter": "16px",
|
||||
"base": "4px",
|
||||
"container-padding": "24px",
|
||||
"stack-md": "16px",
|
||||
"stack-lg": "24px",
|
||||
"stack-sm": "8px"
|
||||
},
|
||||
"fontFamily": {
|
||||
"label-xs": ["Geist"],
|
||||
"headline-md": ["Inter"],
|
||||
"display-lg-mobile": ["Inter"],
|
||||
"label-md": ["Geist"],
|
||||
"body-sm": ["Inter"],
|
||||
"title-sm": ["Inter"],
|
||||
"body-base": ["Inter"],
|
||||
"display-lg": ["Inter"]
|
||||
},
|
||||
"fontSize": {
|
||||
"label-xs": ["11px", {"lineHeight": "14px", "fontWeight": "500"}],
|
||||
"headline-md": ["24px", {"lineHeight": "32px", "fontWeight": "600"}],
|
||||
"display-lg-mobile": ["28px", {"lineHeight": "36px", "fontWeight": "700"}],
|
||||
"label-md": ["12px", {"lineHeight": "16px", "letterSpacing": "0.02em", "fontWeight": "600"}],
|
||||
"body-sm": ["13px", {"lineHeight": "18px", "fontWeight": "400"}],
|
||||
"title-sm": ["18px", {"lineHeight": "28px", "fontWeight": "600"}],
|
||||
"body-base": ["14px", {"lineHeight": "20px", "fontWeight": "400"}],
|
||||
"display-lg": ["36px", {"lineHeight": "44px", "letterSpacing": "-0.02em", "fontWeight": "700"}]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.material-symbols-outlined {
|
||||
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
|
||||
}
|
||||
.zebra-table tr:nth-child(even) {
|
||||
background-color: #f1f3ff;
|
||||
}
|
||||
.sticky-header th {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-background text-on-background font-body-base">
|
||||
<!-- Side Navigation -->
|
||||
<aside class="h-screen w-64 flex flex-col fixed left-0 top-0 border-r border-outline-variant bg-surface-container-lowest overflow-y-auto">
|
||||
<div class="px-6 py-8">
|
||||
<h1 class="font-headline-md text-headline-md font-bold text-primary">KINTEX AI</h1>
|
||||
<p class="font-body-sm text-body-sm text-on-surface-variant">Exhibition Management</p>
|
||||
</div>
|
||||
<nav class="flex-1 px-3 space-y-1">
|
||||
<a class="flex items-center gap-3 px-4 py-3 text-on-surface-variant hover:bg-surface-container-high transition-colors rounded-lg group" href="#">
|
||||
<span class="material-symbols-outlined">dashboard</span>
|
||||
<span class="font-label-md text-label-md">Dashboard</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-4 py-3 text-on-surface-variant hover:bg-surface-container-high transition-colors rounded-lg" href="#">
|
||||
<span class="material-symbols-outlined">group</span>
|
||||
<span class="font-label-md text-label-md">User Management</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-4 py-3 text-on-surface-variant hover:bg-surface-container-high transition-colors rounded-lg" href="#">
|
||||
<span class="material-symbols-outlined">settings_ethernet</span>
|
||||
<span class="font-label-md text-label-md">Common Code</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-4 py-3 text-on-surface-variant hover:bg-surface-container-high transition-colors rounded-lg" href="#">
|
||||
<span class="material-symbols-outlined">grid_view</span>
|
||||
<span class="font-label-md text-label-md">Exhibition Layout</span>
|
||||
</a>
|
||||
<!-- Active Tab: Settlement -->
|
||||
<a class="flex items-center gap-3 px-4 py-3 bg-primary-container text-on-primary-container font-semibold rounded-lg scale-[0.99] transition-transform" href="#">
|
||||
<span class="material-symbols-outlined" style="font-variation-settings: 'FILL' 1;">payments</span>
|
||||
<span class="font-label-md text-label-md">정산 관리</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-4 py-3 text-on-surface-variant hover:bg-surface-container-high transition-colors rounded-lg" href="#">
|
||||
<span class="material-symbols-outlined">settings</span>
|
||||
<span class="font-label-md text-label-md">Settings</span>
|
||||
</a>
|
||||
</nav>
|
||||
<div class="p-4 mt-auto border-t border-outline-variant">
|
||||
<a class="flex items-center gap-3 px-4 py-3 text-on-surface-variant hover:bg-surface-container-high rounded-lg" href="#">
|
||||
<span class="material-symbols-outlined">help</span>
|
||||
<span class="font-label-md text-label-md">Help Center</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-4 py-3 text-on-surface-variant hover:bg-surface-container-high rounded-lg" href="#">
|
||||
<span class="material-symbols-outlined">logout</span>
|
||||
<span class="font-label-md text-label-md">Logout</span>
|
||||
</a>
|
||||
</div>
|
||||
</aside>
|
||||
<!-- Main Content Area -->
|
||||
<main class="ml-64 min-h-screen flex flex-col">
|
||||
<!-- Top Navigation Bar -->
|
||||
<header class="h-16 w-full sticky top-0 z-40 bg-surface border-b border-outline-variant flex items-center justify-between px-gutter">
|
||||
<div class="flex items-center gap-2">
|
||||
<nav aria-label="Breadcrumb" class="flex text-on-surface-variant font-label-md text-label-md">
|
||||
<span class="hover:text-primary cursor-pointer">정산 관리</span>
|
||||
<span class="mx-2">/</span>
|
||||
<span class="text-on-surface font-bold">정산 및 결제 대시보드</span>
|
||||
</nav>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="relative flex items-center px-3 py-1.5 bg-surface-container-low rounded-lg border border-outline-variant">
|
||||
<span class="material-symbols-outlined text-on-surface-variant mr-2">search</span>
|
||||
<input class="bg-transparent border-none focus:ring-0 text-body-sm w-48" placeholder="결제 내역 검색..." type="text"/>
|
||||
</div>
|
||||
<button class="material-symbols-outlined p-2 text-on-surface-variant hover:bg-surface-container rounded-full transition-colors">notifications</button>
|
||||
<div class="h-8 w-px bg-outline-variant"></div>
|
||||
<div class="flex items-center gap-3 pl-2">
|
||||
<div class="text-right">
|
||||
<p class="font-label-md text-label-md text-on-surface">Admin Profile</p>
|
||||
<p class="text-[10px] text-on-surface-variant uppercase tracking-wider">System Administrator</p>
|
||||
</div>
|
||||
<div class="w-10 h-10 rounded-full bg-primary-fixed border border-primary flex items-center justify-center overflow-hidden">
|
||||
<img class="w-full h-full object-cover" data-alt="A professional headshot of a corporate administrator in a modern office environment, soft cinematic lighting, neutral business professional attire, confident and reliable expression, sharp focus with a slight blur on the background." src="https://lh3.googleusercontent.com/aida-public/AB6AXuC0GA9qTc_RwaxFjjm6l8qn6GFfZNsIMaTBs9XMnAoDTsrGJbeLA7qk5x1LyltyCBlW2GJSzhoqH-9eAx5WALQdOul6HEsqlnsPqXISgBpmfVI4ny62_TBFhACXXaq_JDydAOtDzFCzaNKkO6Xam6-us0LelMo8MpfyvErUnRF01tUkwBOOH9qxk_EQprmQyylrH_PZpmIhuC2BxEGnHDNETQ1TYnocKa5V_vZOGl0m0sUlwtrUYO0Vw5SXsSxIYD4prtAHLaFWgTE"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<!-- Page Content -->
|
||||
<div class="p-container-padding space-y-6">
|
||||
<div class="flex items-end justify-between">
|
||||
<div>
|
||||
<h2 class="font-title-sm text-title-sm text-on-surface">정산 및 결제 관리</h2>
|
||||
<p class="text-body-sm text-on-surface-variant">KINTEX AI 시스템의 현재 재무 상태와 결제 일정을 한눈에 확인하십시오.</p>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button class="px-4 py-2 bg-surface-container-highest text-primary font-label-md text-label-md rounded-lg border border-outline-variant hover:bg-primary-fixed transition-all flex items-center gap-2">
|
||||
<span class="material-symbols-outlined text-sm">download</span>
|
||||
엑셀 다운로드
|
||||
</button>
|
||||
<button class="px-4 py-2 bg-primary text-on-primary font-label-md text-label-md rounded-lg shadow-md hover:bg-primary-container transition-all flex items-center gap-2">
|
||||
<span class="material-symbols-outlined text-sm">add</span>
|
||||
신규 청구 생성
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- KPI Section -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<!-- KPI 1 -->
|
||||
<div class="bg-surface-container-lowest p-5 rounded-xl border border-outline-variant border-l-4 border-l-primary flex flex-col justify-between h-32 transition-transform hover:scale-[1.02]">
|
||||
<div class="flex justify-between items-start">
|
||||
<span class="font-label-md text-label-md text-on-surface-variant">청구 총액</span>
|
||||
<span class="material-symbols-outlined text-primary">account_balance_wallet</span>
|
||||
</div>
|
||||
<div class="mt-auto">
|
||||
<p class="font-display-lg text-display-lg text-on-surface">₩64,500,000</p>
|
||||
<p class="text-xs text-on-surface-variant mt-1">총 4건의 청구 항목</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- KPI 2 -->
|
||||
<div class="bg-surface-container-lowest p-5 rounded-xl border border-outline-variant border-l-4 border-l-tertiary flex flex-col justify-between h-32 transition-transform hover:scale-[1.02]">
|
||||
<div class="flex justify-between items-start">
|
||||
<span class="font-label-md text-label-md text-on-surface-variant">납부 완료</span>
|
||||
<span class="material-symbols-outlined text-tertiary">check_circle</span>
|
||||
</div>
|
||||
<div class="mt-auto">
|
||||
<p class="font-display-lg text-display-lg text-on-surface">₩38,700,000</p>
|
||||
<div class="flex items-center gap-1 mt-1">
|
||||
<div class="w-full bg-surface-container-high h-1 rounded-full overflow-hidden">
|
||||
<div class="bg-tertiary h-full w-[60%]"></div>
|
||||
</div>
|
||||
<span class="text-[10px] font-bold text-tertiary">60%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- KPI 3 -->
|
||||
<div class="bg-surface-container-lowest p-5 rounded-xl border border-outline-variant border-l-4 border-l-error flex flex-col justify-between h-32 transition-transform hover:scale-[1.02]">
|
||||
<div class="flex justify-between items-start">
|
||||
<span class="font-label-md text-label-md text-on-surface-variant">미납 금액</span>
|
||||
<span class="material-symbols-outlined text-error">warning</span>
|
||||
</div>
|
||||
<div class="mt-auto">
|
||||
<p class="font-display-lg text-display-lg text-error">₩25,800,000</p>
|
||||
<p class="text-xs text-error mt-1">즉시 확인이 필요한 항목</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- KPI 4 -->
|
||||
<div class="bg-surface-container-lowest p-5 rounded-xl border border-outline-variant border-l-4 border-l-secondary flex flex-col justify-between h-32 transition-transform hover:scale-[1.02]">
|
||||
<div class="flex justify-between items-start">
|
||||
<span class="font-label-md text-label-md text-on-surface-variant">다음 납부 마감</span>
|
||||
<span class="material-symbols-outlined text-secondary">calendar_today</span>
|
||||
</div>
|
||||
<div class="mt-auto flex items-end justify-between">
|
||||
<div>
|
||||
<p class="font-display-lg text-display-lg text-on-surface">2026.07.15</p>
|
||||
<p class="text-xs text-on-surface-variant mt-1">중도금 (30%)</p>
|
||||
</div>
|
||||
<span class="px-3 py-1 bg-[#FDB022] text-white font-bold text-label-md rounded-full mb-1">D-9</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Main Content Split -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<!-- Left: Table (2/3) -->
|
||||
<div class="lg:col-span-2 bg-surface-container-lowest rounded-xl border border-outline-variant overflow-hidden flex flex-col">
|
||||
<div class="p-5 border-b border-outline-variant flex items-center justify-between">
|
||||
<h3 class="font-title-sm text-title-sm text-on-surface">납부 일정 및 내역</h3>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-xs text-on-surface-variant">상태 필터:</span>
|
||||
<select class="text-xs border-outline-variant rounded-md py-1 px-2 focus:ring-primary focus:border-primary">
|
||||
<option>전체 보기</option>
|
||||
<option>완료</option>
|
||||
<option>미납</option>
|
||||
<option>대기</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="overflow-x-auto flex-1 max-h-[400px]">
|
||||
<table class="w-full text-left zebra-table border-collapse">
|
||||
<thead class="sticky-header bg-surface-container-low">
|
||||
<tr>
|
||||
<th class="px-6 py-4 font-label-md text-label-md text-on-surface-variant border-b border-outline-variant">항목</th>
|
||||
<th class="px-6 py-4 font-label-md text-label-md text-on-surface-variant border-b border-outline-variant">금액</th>
|
||||
<th class="px-6 py-4 font-label-md text-label-md text-on-surface-variant border-b border-outline-variant">기한</th>
|
||||
<th class="px-6 py-4 font-label-md text-label-md text-on-surface-variant border-b border-outline-variant">상태</th>
|
||||
<th class="px-6 py-4 font-label-md text-label-md text-on-surface-variant border-b border-outline-variant text-right">액션</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="text-body-base divide-y divide-outline-variant">
|
||||
<tr>
|
||||
<td class="px-6 py-5 font-semibold text-on-surface">계약금 (20%)</td>
|
||||
<td class="px-6 py-5">₩12,900,000</td>
|
||||
<td class="px-6 py-5 font-label-xs text-label-xs">2026.06.01</td>
|
||||
<td class="px-6 py-5">
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800 border border-green-200">
|
||||
완료
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-5 text-right">
|
||||
<button class="text-primary hover:underline font-label-md text-label-md">[영수증]</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="px-6 py-5 font-semibold text-on-surface">중도금 (30%)</td>
|
||||
<td class="px-6 py-5">₩19,350,000</td>
|
||||
<td class="px-6 py-5 font-label-xs text-label-xs text-error">2026.07.15</td>
|
||||
<td class="px-6 py-5">
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800 border border-red-200">
|
||||
미납
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-5 text-right">
|
||||
<button class="bg-primary text-on-primary px-3 py-1 rounded text-xs hover:bg-primary-container transition-colors">[결제]</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="px-6 py-5 font-semibold text-on-surface">잔금 (30%) + 예치금 (15%)</td>
|
||||
<td class="px-6 py-5">₩29,025,000</td>
|
||||
<td class="px-6 py-5 font-label-xs text-label-xs">2026.08.10</td>
|
||||
<td class="px-6 py-5">
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800 border border-gray-200">
|
||||
대기
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-5 text-right text-on-surface-variant">-</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="px-6 py-5 font-semibold text-on-surface">유틸리티 추가 비용</td>
|
||||
<td class="px-6 py-5">₩3,225,000</td>
|
||||
<td class="px-6 py-5 font-label-xs text-label-xs">2026.08.20</td>
|
||||
<td class="px-6 py-5">
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800 border border-gray-200">
|
||||
대기
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-5 text-right text-on-surface-variant">-</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Right: Actions (1/3) -->
|
||||
<div class="space-y-6">
|
||||
<!-- Section 1: Payment Method -->
|
||||
<div class="bg-surface-container-lowest p-5 rounded-xl border border-outline-variant">
|
||||
<h4 class="font-label-md text-label-md text-on-surface-variant mb-4">결제 수단 선택</h4>
|
||||
<div class="grid grid-cols-3 gap-2 bg-surface-container-low p-1 rounded-lg">
|
||||
<button class="bg-surface-container-lowest text-primary font-bold py-2 rounded shadow-sm text-xs border border-outline-variant">카드</button>
|
||||
<button class="text-on-surface-variant py-2 rounded text-xs hover:bg-surface-container transition-colors">계좌이체</button>
|
||||
<button class="text-on-surface-variant py-2 rounded text-xs hover:bg-surface-container transition-colors">세금계산서</button>
|
||||
</div>
|
||||
<div class="mt-4 p-4 border border-outline-variant rounded-lg border-dashed text-center">
|
||||
<span class="material-symbols-outlined text-on-surface-variant text-3xl">add_card</span>
|
||||
<p class="text-xs text-on-surface-variant mt-1">새로운 카드 등록</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Section 2: Deposit Summary -->
|
||||
<div class="bg-surface-container-lowest p-5 rounded-xl border border-outline-variant">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h4 class="font-label-md text-label-md text-on-surface-variant">예치금 정산 요약</h4>
|
||||
<span class="material-symbols-outlined text-sm text-primary">info</span>
|
||||
</div>
|
||||
<div class="space-y-3">
|
||||
<div class="flex justify-between items-center text-body-sm">
|
||||
<span class="text-on-surface-variant">총 예치금</span>
|
||||
<span class="font-bold text-on-surface">₩9,600,000</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center text-body-sm">
|
||||
<span class="text-on-surface-variant">실사용 금액</span>
|
||||
<span class="font-bold text-on-surface">₩7,100,000</span>
|
||||
</div>
|
||||
<div class="h-px bg-outline-variant w-full my-2"></div>
|
||||
<div class="flex justify-between items-center text-title-sm">
|
||||
<span class="text-tertiary">환급 예정액</span>
|
||||
<span class="font-bold text-tertiary">₩2,500,000</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Section 3: Big CTA -->
|
||||
<button class="w-full bg-[#0066B3] hover:bg-primary-container text-white font-bold py-4 rounded-xl shadow-lg transition-all flex items-center justify-center gap-3">
|
||||
<span class="material-symbols-outlined">receipt_long</span>
|
||||
세금계산서 발행
|
||||
</button>
|
||||
<!-- AI Suggestion Box -->
|
||||
<div class="p-4 border border-primary/20 bg-primary/5 rounded-xl relative overflow-hidden">
|
||||
<div class="absolute top-0 left-0 w-1 h-full bg-secondary-container"></div>
|
||||
<div class="flex items-start gap-2">
|
||||
<span class="material-symbols-outlined text-secondary-container text-lg">auto_awesome</span>
|
||||
<div>
|
||||
<h5 class="text-xs font-bold text-secondary-container">AI 지출 분석</h5>
|
||||
<p class="text-[11px] text-on-surface-variant mt-1 leading-relaxed">지난 행사 대비 <b>유틸리티 비용이 15% 절감</b>되었습니다. 예치금 환급 신청 시 계좌 정보를 다시 한번 확인하십시오.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Bottom Section: Detailed Table -->
|
||||
<div class="bg-surface-container-lowest rounded-xl border border-outline-variant overflow-hidden">
|
||||
<div class="p-5 border-b border-outline-variant">
|
||||
<h3 class="font-title-sm text-title-sm text-on-surface">사후 정산 세부 내역</h3>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-left">
|
||||
<thead class="bg-surface-container-low">
|
||||
<tr>
|
||||
<th class="px-6 py-3 font-label-md text-label-md text-on-surface-variant">항목 코드</th>
|
||||
<th class="px-6 py-3 font-label-md text-label-md text-on-surface-variant">세부 내역</th>
|
||||
<th class="px-6 py-3 font-label-md text-label-md text-on-surface-variant">단가/수량</th>
|
||||
<th class="px-6 py-3 font-label-md text-label-md text-on-surface-variant">합계 금액</th>
|
||||
<th class="px-6 py-3 font-label-md text-label-md text-on-surface-variant">정산 상태</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-outline-variant">
|
||||
<tr class="hover:bg-surface-container-low transition-colors">
|
||||
<td class="px-6 py-4 text-xs font-mono text-on-surface-variant">UT-2601</td>
|
||||
<td class="px-6 py-4 font-semibold">전기사용료 (Primary Line)</td>
|
||||
<td class="px-6 py-4 text-body-sm">₩1,500 / 1,200kWh</td>
|
||||
<td class="px-6 py-4 font-bold">₩1,800,000</td>
|
||||
<td class="px-6 py-4">
|
||||
<span class="text-xs text-on-surface-variant flex items-center gap-1">
|
||||
<span class="w-2 h-2 rounded-full bg-tertiary"></span> 산정 완료
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="hover:bg-surface-container-low transition-colors">
|
||||
<td class="px-6 py-4 text-xs font-mono text-on-surface-variant">WM-2605</td>
|
||||
<td class="px-6 py-4 font-semibold">폐기물 처리비 (산업폐기물 포함)</td>
|
||||
<td class="px-6 py-4 text-body-sm">₩50,000 / 15.5t</td>
|
||||
<td class="px-6 py-4 font-bold">₩775,000</td>
|
||||
<td class="px-6 py-4">
|
||||
<span class="text-xs text-on-surface-variant flex items-center gap-1">
|
||||
<span class="w-2 h-2 rounded-full bg-tertiary"></span> 산정 완료
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="hover:bg-surface-container-low transition-colors">
|
||||
<td class="px-6 py-4 text-xs font-mono text-on-surface-variant">CS-2602</td>
|
||||
<td class="px-6 py-4 font-semibold">청소 용역비 (야간/상설)</td>
|
||||
<td class="px-6 py-4 text-body-sm">₩325,000 / 2회</td>
|
||||
<td class="px-6 py-4 font-bold">₩650,000</td>
|
||||
<td class="px-6 py-4">
|
||||
<span class="text-xs text-on-surface-variant flex items-center gap-1">
|
||||
<span class="w-2 h-2 rounded-full bg-outline"></span> 산정 중
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="p-4 bg-surface-container text-right">
|
||||
<span class="text-body-sm text-on-surface-variant">상기 내역은 실제 사용량에 따라 변동될 수 있습니다.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Footer -->
|
||||
<footer class="mt-auto p-gutter text-center border-t border-outline-variant bg-surface-container-lowest">
|
||||
<p class="text-[11px] text-on-surface-variant">© 2024 KINTEX AI Exhibition Management System. All Rights Reserved. Managed by Financial Infrastructure Div.</p>
|
||||
</footer>
|
||||
</main>
|
||||
<!-- Simple Interactivity for demo -->
|
||||
<script>
|
||||
document.querySelectorAll('button').forEach(btn => {
|
||||
btn.addEventListener('click', function() {
|
||||
this.classList.add('opacity-80');
|
||||
setTimeout(() => this.classList.remove('opacity-80'), 100);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body></html>
|
||||
@ -0,0 +1,479 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html class="light" lang="ko"><head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||
<title>Contractor Matching & RFQ - KINTEX AI System</title>
|
||||
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Geist:wght@400;500;600&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<script id="tailwind-config">
|
||||
tailwind.config = {
|
||||
darkMode: "class",
|
||||
theme: {
|
||||
extend: {
|
||||
"colors": {
|
||||
"tertiary-fixed-dim": "#72daa9",
|
||||
"tertiary-container": "#00734e",
|
||||
"on-tertiary-fixed": "#002113",
|
||||
"on-secondary-fixed-variant": "#4500d8",
|
||||
"surface": "#f9f9ff",
|
||||
"on-secondary-container": "#f3edff",
|
||||
"tertiary-fixed": "#8ef7c4",
|
||||
"primary": "#004e8b",
|
||||
"outline-variant": "#c1c7d3",
|
||||
"inverse-primary": "#a2c9ff",
|
||||
"on-primary-fixed-variant": "#004881",
|
||||
"surface-tint": "#0060a9",
|
||||
"error": "#ba1a1a",
|
||||
"on-tertiary-fixed-variant": "#005236",
|
||||
"on-secondary": "#ffffff",
|
||||
"surface-variant": "#dbe2f9",
|
||||
"surface-dim": "#d2daf0",
|
||||
"surface-container-low": "#f1f3ff",
|
||||
"surface-bright": "#f9f9ff",
|
||||
"secondary-container": "#6d4aff",
|
||||
"primary-fixed": "#d3e4ff",
|
||||
"error-container": "#ffdad6",
|
||||
"secondary-fixed": "#e5deff",
|
||||
"surface-container-lowest": "#ffffff",
|
||||
"on-tertiary-container": "#8ef7c3",
|
||||
"on-primary-container": "#d2e3ff",
|
||||
"on-background": "#141b2c",
|
||||
"tertiary": "#00583b",
|
||||
"on-error-container": "#93000a",
|
||||
"on-error": "#ffffff",
|
||||
"on-tertiary": "#ffffff",
|
||||
"on-primary": "#ffffff",
|
||||
"inverse-surface": "#293041",
|
||||
"primary-container": "#0066b3",
|
||||
"inverse-on-surface": "#edf0ff",
|
||||
"secondary-fixed-dim": "#c9bfff",
|
||||
"outline": "#717782",
|
||||
"surface-container": "#e9edff",
|
||||
"surface-container-highest": "#dbe2f9",
|
||||
"secondary": "#5427e6",
|
||||
"on-primary-fixed": "#001c38",
|
||||
"surface-container-high": "#e0e8ff",
|
||||
"on-surface": "#141b2c",
|
||||
"on-secondary-fixed": "#1b0063",
|
||||
"primary-fixed-dim": "#a2c9ff",
|
||||
"on-surface-variant": "#414751",
|
||||
"background": "#f9f9ff"
|
||||
},
|
||||
"borderRadius": {
|
||||
"DEFAULT": "0.125rem",
|
||||
"lg": "0.25rem",
|
||||
"xl": "0.5rem",
|
||||
"full": "0.75rem"
|
||||
},
|
||||
"spacing": {
|
||||
"stack-sm": "8px",
|
||||
"container-padding": "24px",
|
||||
"base": "4px",
|
||||
"stack-md": "16px",
|
||||
"stack-lg": "24px",
|
||||
"gutter": "16px"
|
||||
},
|
||||
"fontFamily": {
|
||||
"display-lg": ["Inter", "sans-serif"],
|
||||
"title-sm": ["Inter", "sans-serif"],
|
||||
"label-xs": ["Geist", "sans-serif"],
|
||||
"label-md": ["Geist", "sans-serif"],
|
||||
"headline-md": ["Inter", "sans-serif"],
|
||||
"body-sm": ["Inter", "sans-serif"],
|
||||
"body-base": ["Inter", "sans-serif"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.material-symbols-outlined {
|
||||
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.scrollbar-hide::-webkit-scrollbar { display: none; }
|
||||
.contractor-card { transition: transform 0.2s ease, border-color 0.2s ease; }
|
||||
.contractor-card:hover { transform: translateY(-2px); border-color: #004e8b; }
|
||||
.ai-sparkle { background: linear-gradient(135deg, #6d4aff 0%, #a2c9ff 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-background text-on-surface font-body-base">
|
||||
<!-- SideNavBar -->
|
||||
<aside class="w-[240px] h-screen fixed left-0 top-0 bg-surface border-r border-outline-variant flex flex-col py-stack-lg z-50">
|
||||
<div class="px-6 mb-10">
|
||||
<img alt="KINTEX Logo" class="w-full h-auto mb-2" src="https://lh3.googleusercontent.com/aida-public/AB6AXuDKQxGphtktaCs4lA6sQNFTYKlCtT41w6QCSriheepHz2AhfTc9Dcj0DoqzJPoKflq8uM-1YoRXNAcr_1oOABdGMIKG2XE6Cj93tCm2cPyiugyv899bo59bHsEH9v24til8CYbaqjPZFQjoHs8JXgj36hP2bKTrEqAsaf3fkLoomsmMAx2s3aNybZfL4etZJz9XbZad4vQGxWwgB78QN_TOnt4JLVlwBHREZWVp9r_tIBjM3SUEDQs0gqk4BqqtE49m5g5pZefK4RM"/>
|
||||
<div class="font-headline-md text-headline-md font-bold text-primary">KINTEX CMS</div>
|
||||
<div class="font-body-sm text-body-sm text-on-surface-variant opacity-70">Exhibition Management</div>
|
||||
</div>
|
||||
<nav class="flex-1 px-3 space-y-1">
|
||||
<a class="flex items-center px-4 py-3 rounded-lg text-on-surface-variant hover:bg-surface-container-low transition-colors duration-200" href="#">
|
||||
<span class="material-symbols-outlined mr-3">dashboard</span>
|
||||
<span class="font-body-base">Dashboard</span>
|
||||
</a>
|
||||
<a class="flex items-center px-4 py-3 rounded-lg bg-secondary-container text-on-secondary-container font-semibold" href="#">
|
||||
<span class="material-symbols-outlined mr-3">calendar_month</span>
|
||||
<span class="font-body-base">Exhibition Schedule</span>
|
||||
</a>
|
||||
<a class="flex items-center px-4 py-3 rounded-lg text-on-surface-variant hover:bg-surface-container-low transition-colors duration-200" href="#">
|
||||
<span class="material-symbols-outlined mr-3">sensors</span>
|
||||
<span class="font-body-base">Hall Operations</span>
|
||||
</a>
|
||||
<a class="flex items-center px-4 py-3 rounded-lg text-on-surface-variant hover:bg-surface-container-low transition-colors duration-200" href="#">
|
||||
<span class="material-symbols-outlined mr-3">analytics</span>
|
||||
<span class="font-body-base">Business Intelligence</span>
|
||||
</a>
|
||||
</nav>
|
||||
<div class="px-3 pt-6 border-t border-outline-variant mt-auto space-y-1">
|
||||
<a class="flex items-center px-4 py-2 rounded-lg text-on-surface-variant hover:bg-surface-container-low transition-colors" href="#">
|
||||
<span class="material-symbols-outlined mr-3">settings</span>
|
||||
<span class="font-label-md">Settings</span>
|
||||
</a>
|
||||
<a class="flex items-center px-4 py-2 rounded-lg text-on-surface-variant hover:bg-surface-container-low transition-colors" href="#">
|
||||
<span class="material-symbols-outlined mr-3">help</span>
|
||||
<span class="font-label-md">Support</span>
|
||||
</a>
|
||||
<div class="mt-6 p-4 bg-surface-container rounded-xl flex items-center gap-3">
|
||||
<div class="w-10 h-10 rounded-full bg-primary-fixed flex items-center justify-center">
|
||||
<span class="material-symbols-outlined text-primary">account_circle</span>
|
||||
</div>
|
||||
<div class="overflow-hidden">
|
||||
<div class="font-label-md text-on-surface truncate">Admin User</div>
|
||||
<div class="text-[10px] text-on-surface-variant truncate">KINTEX Admin Profile</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
<!-- TopAppBar -->
|
||||
<header class="h-16 w-[calc(100%-240px)] fixed top-0 right-0 z-40 bg-surface border-b border-outline-variant flex items-center justify-between px-margin-desktop ml-[240px] px-8">
|
||||
<div class="flex items-center gap-6">
|
||||
<h1 class="font-headline-md text-headline-md font-bold text-primary">Contractor Matching & RFQ - KINTEX AI System</h1>
|
||||
<nav class="hidden md:flex items-center gap-4 ml-4">
|
||||
<a class="font-label-md text-primary border-b-2 border-primary pb-1" href="#">Overview</a>
|
||||
<a class="font-label-md text-on-surface-variant hover:text-primary transition-colors" href="#">Hall Map</a>
|
||||
<a class="font-label-md text-on-surface-variant hover:text-primary transition-colors" href="#">Reports</a>
|
||||
</nav>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="relative group">
|
||||
<input class="pl-10 pr-4 py-1.5 bg-surface-container rounded-full border-none focus:ring-2 focus:ring-primary text-body-sm w-64 transition-all" placeholder="Search contractors..." type="text"/>
|
||||
<span class="material-symbols-outlined absolute left-3 top-1/2 -translate-y-1/2 text-on-surface-variant">search</span>
|
||||
</div>
|
||||
<button class="flex items-center gap-2 bg-primary text-white px-4 py-2 rounded-lg font-label-md hover:bg-primary-container transition-colors">
|
||||
<span class="material-symbols-outlined text-[20px]">refresh</span>
|
||||
Refresh Data
|
||||
</button>
|
||||
<div class="flex items-center gap-2">
|
||||
<button class="p-2 text-on-surface-variant hover:bg-surface-container rounded-full relative">
|
||||
<span class="material-symbols-outlined">notifications</span>
|
||||
<span class="absolute top-2 right-2 w-2 h-2 bg-error rounded-full border-2 border-surface"></span>
|
||||
</button>
|
||||
<button class="p-2 text-on-surface-variant hover:bg-surface-container rounded-full">
|
||||
<span class="material-symbols-outlined">account_circle</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<!-- Main Content -->
|
||||
<main class="ml-[240px] pt-16 min-h-screen flex">
|
||||
<!-- List Section (2/3) -->
|
||||
<section class="w-2/3 p-stack-lg border-r border-outline-variant bg-surface-bright">
|
||||
<!-- Filter Bar -->
|
||||
<div class="mb-stack-lg space-y-4">
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button class="px-4 py-1.5 rounded-full bg-primary text-white text-body-sm font-medium">전시디자인설치</button>
|
||||
<button class="px-4 py-1.5 rounded-full bg-white border border-outline-variant text-on-surface-variant text-body-sm hover:bg-surface-container transition-colors">리깅</button>
|
||||
<button class="px-4 py-1.5 rounded-full bg-white border border-outline-variant text-on-surface-variant text-body-sm hover:bg-surface-container transition-colors">전기시설</button>
|
||||
<button class="px-4 py-1.5 rounded-full bg-white border border-outline-variant text-on-surface-variant text-body-sm hover:bg-surface-container transition-colors">카펫</button>
|
||||
<button class="px-4 py-1.5 rounded-full bg-white border border-outline-variant text-on-surface-variant text-body-sm hover:bg-surface-container transition-colors">급배수</button>
|
||||
</div>
|
||||
<div class="flex gap-3">
|
||||
<div class="relative group">
|
||||
<button class="flex items-center justify-between px-4 py-2 bg-white border border-outline-variant rounded-lg text-body-sm min-w-[120px] hover:border-primary">
|
||||
지역 ▾
|
||||
</button>
|
||||
</div>
|
||||
<div class="relative group">
|
||||
<button class="flex items-center justify-between px-4 py-2 bg-white border border-outline-variant rounded-lg text-body-sm min-w-[120px] hover:border-primary">
|
||||
규모 ▾
|
||||
</button>
|
||||
</div>
|
||||
<div class="relative group">
|
||||
<button class="flex items-center justify-between px-4 py-2 bg-white border border-outline-variant rounded-lg text-body-sm min-w-[120px] hover:border-primary">
|
||||
예산 ▾
|
||||
</button>
|
||||
</div>
|
||||
<div class="ml-auto text-body-sm text-on-surface-variant self-center">
|
||||
총 <span class="font-bold text-on-surface">124</span>개 업체 검색됨
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Contractor Cards List -->
|
||||
<div class="space-y-stack-md">
|
||||
<!-- Card 1 (AI Recommended) -->
|
||||
<div class="contractor-card bg-white rounded-xl border border-outline-variant p-5 flex flex-col relative overflow-hidden group">
|
||||
<div class="absolute top-0 left-0 w-1 h-full bg-secondary-container"></div>
|
||||
<div class="flex justify-between items-start mb-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<h3 class="font-headline-md text-headline-md font-bold text-on-surface">(주)공간디자인</h3>
|
||||
<span class="flex items-center gap-1 bg-secondary-container/10 text-secondary font-semibold text-[11px] px-2 py-0.5 rounded-full border border-secondary-container/30">
|
||||
<span class="material-symbols-outlined text-[14px]">auto_awesome</span>
|
||||
AI 추천 · 부스 규모·업종 적합
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 bg-primary/5 text-primary text-[11px] font-bold px-2 py-0.5 rounded border border-primary/20">
|
||||
<span class="material-symbols-outlined text-[14px]">verified</span>
|
||||
킨텍스 등록업체
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-4 gap-6 mb-6">
|
||||
<div>
|
||||
<div class="text-[11px] text-on-surface-variant uppercase tracking-wider mb-1">카테고리</div>
|
||||
<div class="text-body-base font-medium">전시디자인설치</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-[11px] text-on-surface-variant uppercase tracking-wider mb-1">활동 지역</div>
|
||||
<div class="text-body-base font-medium">경기 고양 / 서울</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-[11px] text-on-surface-variant uppercase tracking-wider mb-1">신뢰도 지표</div>
|
||||
<div class="text-body-base font-medium">규정 준수 98% · 시공 42건</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-[11px] text-on-surface-variant uppercase tracking-wider mb-1">평점</div>
|
||||
<div class="flex items-center gap-1 text-body-base font-bold text-[#FFB000]">
|
||||
<span class="material-symbols-outlined text-[18px]" style="font-variation-settings: 'FILL' 1;">star</span>
|
||||
4.8<span class="text-on-surface-variant font-normal">/5.0</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-between pt-4 border-t border-outline-variant/30">
|
||||
<div class="flex gap-2">
|
||||
<span class="text-[11px] px-2 py-1 bg-surface-container rounded">#친환경소재</span>
|
||||
<span class="text-[11px] px-2 py-1 bg-surface-container rounded">#대형부스전문</span>
|
||||
<span class="text-[11px] px-2 py-1 bg-surface-container rounded">#DX시공</span>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button class="px-5 py-2 border border-outline-variant text-body-sm font-medium rounded-lg hover:bg-surface-container transition-colors">상세보기</button>
|
||||
<button class="px-5 py-2 bg-primary-container text-white text-body-sm font-bold rounded-lg hover:opacity-90 transition-opacity">견적담기</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Card 2 -->
|
||||
<div class="contractor-card bg-white rounded-xl border border-outline-variant p-5 flex flex-col relative overflow-hidden group">
|
||||
<div class="flex justify-between items-start mb-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<h3 class="font-headline-md text-headline-md font-bold text-on-surface">유니버셜 아트텍</h3>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 bg-primary/5 text-primary text-[11px] font-bold px-2 py-0.5 rounded border border-primary/20">
|
||||
<span class="material-symbols-outlined text-[14px]">verified</span>
|
||||
킨텍스 등록업체
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-4 gap-6 mb-6">
|
||||
<div>
|
||||
<div class="text-[11px] text-on-surface-variant uppercase tracking-wider mb-1">카테고리</div>
|
||||
<div class="text-body-base font-medium">전시디자인설치</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-[11px] text-on-surface-variant uppercase tracking-wider mb-1">활동 지역</div>
|
||||
<div class="text-body-base font-medium">경기 파주 / 고양</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-[11px] text-on-surface-variant uppercase tracking-wider mb-1">신뢰도 지표</div>
|
||||
<div class="text-body-base font-medium">규정 준수 95% · 시공 31건</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-[11px] text-on-surface-variant uppercase tracking-wider mb-1">평점</div>
|
||||
<div class="flex items-center gap-1 text-body-base font-bold text-[#FFB000]">
|
||||
<span class="material-symbols-outlined text-[18px]" style="font-variation-settings: 'FILL' 1;">star</span>
|
||||
4.6<span class="text-on-surface-variant font-normal">/5.0</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-between pt-4 border-t border-outline-variant/30">
|
||||
<div class="flex gap-2">
|
||||
<span class="text-[11px] px-2 py-1 bg-surface-container rounded">#리깅가능</span>
|
||||
<span class="text-[11px] px-2 py-1 bg-surface-container rounded">#중형부스</span>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button class="px-5 py-2 border border-outline-variant text-body-sm font-medium rounded-lg hover:bg-surface-container transition-colors">상세보기</button>
|
||||
<button class="px-5 py-2 bg-primary-container text-white text-body-sm font-bold rounded-lg hover:opacity-90 transition-opacity">견적담기</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Card 3 -->
|
||||
<div class="contractor-card bg-white rounded-xl border border-outline-variant p-5 flex flex-col relative overflow-hidden group opacity-80 hover:opacity-100 transition-opacity">
|
||||
<div class="flex justify-between items-start mb-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<h3 class="font-headline-md text-headline-md font-bold text-on-surface">(주)킨텍스모듈</h3>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 bg-primary/5 text-primary text-[11px] font-bold px-2 py-0.5 rounded border border-primary/20">
|
||||
<span class="material-symbols-outlined text-[14px]">verified</span>
|
||||
킨텍스 등록업체
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-4 gap-6 mb-6">
|
||||
<div>
|
||||
<div class="text-[11px] text-on-surface-variant uppercase tracking-wider mb-1">카테고리</div>
|
||||
<div class="text-body-base font-medium">전시디자인설치</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-[11px] text-on-surface-variant uppercase tracking-wider mb-1">활동 지역</div>
|
||||
<div class="text-body-base font-medium">전국</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-[11px] text-on-surface-variant uppercase tracking-wider mb-1">신뢰도 지표</div>
|
||||
<div class="text-body-base font-medium">규정 준수 99% · 시공 156건</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-[11px] text-on-surface-variant uppercase tracking-wider mb-1">평점</div>
|
||||
<div class="flex items-center gap-1 text-body-base font-bold text-[#FFB000]">
|
||||
<span class="material-symbols-outlined text-[18px]" style="font-variation-settings: 'FILL' 1;">star</span>
|
||||
4.9<span class="text-on-surface-variant font-normal">/5.0</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-between pt-4 border-t border-outline-variant/30">
|
||||
<div class="flex gap-2">
|
||||
<span class="text-[11px] px-2 py-1 bg-surface-container rounded">#모듈러부스</span>
|
||||
<span class="text-[11px] px-2 py-1 bg-surface-container rounded">#빠른시공</span>
|
||||
<span class="text-[11px] px-2 py-1 bg-surface-container rounded">#가성비</span>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button class="px-5 py-2 border border-outline-variant text-body-sm font-medium rounded-lg hover:bg-surface-container transition-colors">상세보기</button>
|
||||
<button class="px-5 py-2 bg-primary-container text-white text-body-sm font-bold rounded-lg hover:opacity-90 transition-opacity">견적담기</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<!-- RFQ Panel (1/3) -->
|
||||
<aside class="w-1/3 bg-surface p-stack-lg flex flex-col h-[calc(100vh-64px)] sticky top-16">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h2 class="font-headline-md text-headline-md font-bold text-on-surface flex items-center gap-2">
|
||||
<span class="material-symbols-outlined text-primary">description</span>
|
||||
견적 요청 (RFQ)
|
||||
</h2>
|
||||
<button class="text-primary text-[12px] font-bold hover:underline">초기화</button>
|
||||
</div>
|
||||
<div class="flex-1 overflow-y-auto pr-2 scrollbar-hide space-y-6">
|
||||
<!-- Selected Contractors -->
|
||||
<div class="space-y-3">
|
||||
<label class="font-label-md text-on-surface-variant block uppercase tracking-wide">수신 업체 (3)</label>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<div class="flex items-center gap-2 bg-white border border-outline-variant px-3 py-1.5 rounded-lg text-body-sm shadow-sm">
|
||||
(주)공간디자인
|
||||
<button class="material-symbols-outlined text-[18px] text-on-surface-variant hover:text-error transition-colors">close</button>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 bg-white border border-outline-variant px-3 py-1.5 rounded-lg text-body-sm shadow-sm">
|
||||
유니버셜 아트텍
|
||||
<button class="material-symbols-outlined text-[18px] text-on-surface-variant hover:text-error transition-colors">close</button>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 bg-white border border-outline-variant px-3 py-1.5 rounded-lg text-body-sm shadow-sm">
|
||||
(주)킨텍스모듈
|
||||
<button class="material-symbols-outlined text-[18px] text-on-surface-variant hover:text-error transition-colors">close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Attachment List -->
|
||||
<div class="space-y-3">
|
||||
<label class="font-label-md text-on-surface-variant block uppercase tracking-wide">첨부 자료</label>
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center justify-between p-3 bg-white border border-dashed border-outline-variant rounded-lg group hover:border-primary transition-colors cursor-pointer">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="material-symbols-outlined text-error">picture_as_pdf</span>
|
||||
<span class="text-body-sm font-medium">부스 설계안 v2.pdf</span>
|
||||
</div>
|
||||
<span class="text-[11px] text-on-surface-variant group-hover:text-primary">1.4 MB</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between p-3 bg-white border border-dashed border-outline-variant rounded-lg group hover:border-primary transition-colors cursor-pointer">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="material-symbols-outlined text-tertiary">table_chart</span>
|
||||
<span class="text-body-sm font-medium">물량서(BOQ).xlsx</span>
|
||||
</div>
|
||||
<span class="text-[11px] text-on-surface-variant group-hover:text-primary">245 KB</span>
|
||||
</div>
|
||||
<button class="w-full py-2 border-2 border-dashed border-outline-variant rounded-lg text-body-sm text-on-surface-variant hover:bg-surface-container-low flex items-center justify-center gap-2 transition-all">
|
||||
<span class="material-symbols-outlined">add_circle</span>
|
||||
파일 추가
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Text Area & Date Picker -->
|
||||
<div class="space-y-4">
|
||||
<div class="space-y-2">
|
||||
<label class="font-label-md text-on-surface-variant block uppercase tracking-wide">요청 사항</label>
|
||||
<textarea class="w-full bg-white border border-outline-variant rounded-lg p-3 text-body-sm focus:ring-2 focus:ring-primary focus:border-transparent transition-all outline-none resize-none" placeholder="시공 일정, 특이 사항 등을 입력해 주세요." rows="4"></textarea>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<label class="font-label-md text-on-surface-variant block uppercase tracking-wide">견적 마감일</label>
|
||||
<div class="relative">
|
||||
<input class="w-full bg-white border border-outline-variant rounded-lg p-3 text-body-sm focus:ring-2 focus:ring-primary outline-none" type="date" value="2024-05-20"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Footer Action -->
|
||||
<div class="mt-auto pt-6 border-t border-outline-variant">
|
||||
<div class="bg-primary/5 p-4 rounded-xl mb-4 border border-primary/10">
|
||||
<div class="flex items-start gap-3">
|
||||
<span class="material-symbols-outlined text-primary text-[20px]">info</span>
|
||||
<div>
|
||||
<div class="text-[12px] font-bold text-primary mb-1">킨텍스 매칭 보증 서비스</div>
|
||||
<p class="text-[11px] text-on-surface-variant leading-relaxed">
|
||||
본 RFQ는 킨텍스 인증 협력업체에게만 발송되며, 규정 위반 이력이 없는 업체들로 필터링되었습니다.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="w-full bg-primary-container text-white py-4 rounded-xl font-bold text-body-base shadow-lg shadow-primary-container/20 hover:scale-[1.01] active:scale-[0.98] transition-all flex items-center justify-center gap-3">
|
||||
<span class="material-symbols-outlined">send</span>
|
||||
견적 요청 발송
|
||||
</button>
|
||||
<div class="mt-4 text-center">
|
||||
<span class="text-[11px] text-error font-medium flex items-center justify-center gap-1">
|
||||
<span class="material-symbols-outlined text-[14px]">warning</span>
|
||||
미등록 업체는 응찰할 수 없습니다
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</main>
|
||||
<!-- Floating AI Assistant Tooltip -->
|
||||
<div class="fixed bottom-8 left-[calc(240px+24px)] z-50 group">
|
||||
<div class="bg-secondary text-white px-4 py-3 rounded-2xl shadow-xl flex items-center gap-3 cursor-pointer hover:bg-on-secondary-fixed-variant transition-colors relative">
|
||||
<div class="bg-white/20 p-1.5 rounded-lg">
|
||||
<span class="material-symbols-outlined text-[20px]" style="font-variation-settings: 'FILL' 1;">auto_awesome</span>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-[11px] font-medium opacity-80">AI Matching Engine</div>
|
||||
<div class="text-body-sm font-bold">도면 분석 결과, 12개 업체가 적합합니다</div>
|
||||
</div>
|
||||
<div class="absolute -top-2 -right-2 w-5 h-5 bg-error text-[10px] flex items-center justify-center rounded-full border-2 border-surface font-bold">1</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
// Simple Interaction logic
|
||||
document.querySelectorAll('.contractor-card').forEach(card => {
|
||||
card.addEventListener('click', () => {
|
||||
console.log('Contractor details view triggered');
|
||||
});
|
||||
});
|
||||
|
||||
// Toggle filters (Visual only)
|
||||
document.querySelectorAll('.flex-wrap button').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
if (btn.classList.contains('bg-primary')) {
|
||||
btn.classList.remove('bg-primary', 'text-white');
|
||||
btn.classList.add('bg-white', 'border', 'border-outline-variant', 'text-on-surface-variant');
|
||||
} else {
|
||||
btn.classList.add('bg-primary', 'text-white');
|
||||
btn.classList.remove('bg-white', 'border', 'border-outline-variant', 'text-on-surface-variant');
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body></html>
|
||||
@ -0,0 +1,378 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html class="light" lang="ko"><head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||
<title>KINTEX AI System - 견적서 작성</title>
|
||||
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Geist:wght@400;500;600&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<script id="tailwind-config">
|
||||
tailwind.config = {
|
||||
darkMode: "class",
|
||||
theme: {
|
||||
extend: {
|
||||
"colors": {
|
||||
"surface-container": "#eaedff",
|
||||
"surface-container-lowest": "#ffffff",
|
||||
"inverse-on-surface": "#eef0ff",
|
||||
"tertiary-fixed": "#8ef7c4",
|
||||
"secondary-fixed-dim": "#c9bfff",
|
||||
"on-primary-fixed-variant": "#004881",
|
||||
"primary-container": "#004e8b",
|
||||
"surface-container-highest": "#dbe2fa",
|
||||
"outline-variant": "#c1c7d3",
|
||||
"background": "#faf8ff",
|
||||
"on-surface": "#141b2c",
|
||||
"on-background": "#141b2c",
|
||||
"primary": "#003765",
|
||||
"outline": "#727781",
|
||||
"primary-fixed": "#d3e4ff",
|
||||
"surface-bright": "#faf8ff",
|
||||
"on-secondary-fixed": "#1b0063",
|
||||
"on-primary": "#ffffff",
|
||||
"tertiary-fixed-dim": "#72daa9",
|
||||
"on-primary-fixed": "#001c38",
|
||||
"secondary-fixed": "#e5deff",
|
||||
"on-tertiary-fixed-variant": "#005236",
|
||||
"surface-dim": "#d3d9f1",
|
||||
"error-container": "#ffdad6",
|
||||
"on-surface-variant": "#424750",
|
||||
"error-red": "#ba1a1a",
|
||||
"secondary-container": "#6d4aff",
|
||||
"on-error": "#ffffff",
|
||||
"on-tertiary-container": "#68d1a0",
|
||||
"on-secondary-container": "#f3edff",
|
||||
"inverse-primary": "#a2c9ff",
|
||||
"on-error-container": "#93000a",
|
||||
"surface-container-low": "#f1f3ff",
|
||||
"tertiary": "#003e28",
|
||||
"secondary": "#5427e6",
|
||||
"surface-container-high": "#e1e7ff",
|
||||
"on-tertiary-fixed": "#002113",
|
||||
"primary-fixed-dim": "#a2c9ff",
|
||||
"surface-variant": "#dbe2fa",
|
||||
"on-secondary-fixed-variant": "#4500d8",
|
||||
"on-tertiary": "#ffffff",
|
||||
"tertiary-container": "#00583b",
|
||||
"surface": "#faf8ff",
|
||||
"on-secondary": "#ffffff",
|
||||
"error": "#ba1a1a",
|
||||
"surface-tint": "#24609e",
|
||||
"inverse-surface": "#293042",
|
||||
"on-primary-container": "#91c0ff"
|
||||
},
|
||||
"borderRadius": {
|
||||
"DEFAULT": "0.125rem",
|
||||
"lg": "0.25rem",
|
||||
"xl": "0.5rem",
|
||||
"full": "0.75rem"
|
||||
},
|
||||
"spacing": {
|
||||
"container-padding": "24px",
|
||||
"stack-sm": "8px",
|
||||
"stack-lg": "24px",
|
||||
"gutter": "16px",
|
||||
"base": "4px",
|
||||
"stack-md": "16px",
|
||||
"section-padding": "96px"
|
||||
},
|
||||
"fontFamily": {
|
||||
"display-lg": ["Inter", "sans-serif"],
|
||||
"label-md": ["Geist", "sans-serif"],
|
||||
"title-sm": ["Inter", "sans-serif"],
|
||||
"headline-md": ["Inter", "sans-serif"],
|
||||
"body-sm": ["Inter", "sans-serif"],
|
||||
"body-base": ["Inter", "sans-serif"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.material-symbols-outlined {
|
||||
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.custom-scrollbar::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
.custom-scrollbar::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
}
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||
background: #c1c7d3;
|
||||
border-radius: 10px;
|
||||
}
|
||||
.glass-card {
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(193, 199, 211, 0.3);
|
||||
}
|
||||
.a4-preview {
|
||||
width: 100%;
|
||||
aspect-ratio: 1 / 1.414;
|
||||
box-shadow: 0 4px 20px rgba(0,0,0,0.08);
|
||||
}
|
||||
.bento-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(12, 1fr);
|
||||
gap: 16px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-background text-on-background font-body-base antialiased">
|
||||
<!-- Top Navigation Bar (Shared Component) -->
|
||||
<header class="flex justify-between items-center h-16 px-gutter w-full sticky top-0 z-50 bg-surface border-b border-outline-variant">
|
||||
<div class="flex items-center gap-8">
|
||||
<span class="text-headline-md font-headline-md font-bold text-primary">KINTEX AI System</span>
|
||||
<nav class="hidden md:flex items-center gap-6">
|
||||
<a class="text-label-md font-label-md text-on-surface-variant hover:text-primary transition-colors" href="#">Auctions</a>
|
||||
<a class="text-label-md font-label-md text-on-surface-variant hover:text-primary transition-colors" href="#">Exhibitors</a>
|
||||
<a class="text-label-md font-label-md text-on-surface-variant hover:text-primary transition-colors" href="#">Resources</a>
|
||||
</nav>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="relative">
|
||||
<span class="material-symbols-outlined absolute left-3 top-1/2 -translate-y-1/2 text-outline">search</span>
|
||||
<input class="pl-10 pr-4 py-2 bg-surface-container-low border border-outline-variant rounded-full text-body-sm w-64 focus:outline-none focus:ring-2 focus:ring-primary" placeholder="검색..." type="text"/>
|
||||
</div>
|
||||
<button class="material-symbols-outlined text-on-surface-variant hover:bg-surface-container-high p-2 rounded-full transition-colors">notifications</button>
|
||||
<button class="material-symbols-outlined text-on-surface-variant hover:bg-surface-container-high p-2 rounded-full transition-colors">settings</button>
|
||||
<button class="bg-primary text-on-primary px-6 py-2 rounded-full text-label-md font-label-md hover:bg-primary-container transition-colors">Live Auction</button>
|
||||
</div>
|
||||
</header>
|
||||
<main class="max-w-[1440px] mx-auto p-gutter pb-24 h-[calc(100vh-64px)] overflow-hidden">
|
||||
<div class="flex h-full gap-gutter">
|
||||
<!-- Left Side: Editor Section -->
|
||||
<section class="flex-1 flex flex-col bg-surface-container-lowest rounded-xl border border-outline-variant overflow-hidden">
|
||||
<!-- Editor Header -->
|
||||
<div class="p-stack-lg border-b border-outline-variant flex justify-between items-center">
|
||||
<h1 class="text-headline-md font-headline-md text-primary">견적서 작성</h1>
|
||||
<div class="flex items-center gap-2 px-4 py-2 bg-secondary-fixed text-on-secondary-fixed rounded-full text-label-md">
|
||||
<span class="material-symbols-outlined text-[18px]">auto_awesome</span>
|
||||
<span>AI 기반 자동 입력 활성화</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- AI Info Banner -->
|
||||
<div class="m-stack-md p-stack-md bg-secondary-container/10 border-l-4 border-secondary-container flex items-center gap-3">
|
||||
<span class="material-symbols-outlined text-secondary-container">info</span>
|
||||
<p class="text-body-sm text-on-surface font-medium">물량서(BOQ)에서 항목 및 수량이 AI로 자동 입력되었습니다. 단가를 입력하여 견적을 완성하세요.</p>
|
||||
</div>
|
||||
<!-- Editable Data Grid -->
|
||||
<div class="flex-1 overflow-auto custom-scrollbar px-stack-md">
|
||||
<table class="w-full text-left border-collapse">
|
||||
<thead class="sticky top-0 bg-surface-container-low z-10">
|
||||
<tr>
|
||||
<th class="p-3 text-label-md font-bold text-outline border-b border-outline-variant">공종</th>
|
||||
<th class="p-3 text-label-md font-bold text-outline border-b border-outline-variant">자재/품명</th>
|
||||
<th class="p-3 text-label-md font-bold text-outline border-b border-outline-variant text-center">수량</th>
|
||||
<th class="p-3 text-label-md font-bold text-outline border-b border-outline-variant text-right">단가 (₩)</th>
|
||||
<th class="p-3 text-label-md font-bold text-outline border-b border-outline-variant text-right">금액 (₩)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="text-body-sm">
|
||||
<tr class="hover:bg-surface-container transition-colors group">
|
||||
<td class="p-3 border-b border-outline-variant">골조</td>
|
||||
<td class="p-3 border-b border-outline-variant">알루미늄 프로파일</td>
|
||||
<td class="p-3 border-b border-outline-variant text-center">48</td>
|
||||
<td class="p-3 border-b border-outline-variant text-right">
|
||||
<input class="w-32 text-right border-0 bg-transparent focus:ring-2 focus:ring-primary rounded p-1" type="text" value="45,000"/>
|
||||
</td>
|
||||
<td class="p-3 border-b border-outline-variant text-right font-medium">2,160,000</td>
|
||||
</tr>
|
||||
<tr class="hover:bg-surface-container transition-colors">
|
||||
<td class="p-3 border-b border-outline-variant">조명</td>
|
||||
<td class="p-3 border-b border-outline-variant">LED 매립등</td>
|
||||
<td class="p-3 border-b border-outline-variant text-center">24</td>
|
||||
<td class="p-3 border-b border-outline-variant text-right">
|
||||
<input class="w-32 text-right border-0 bg-transparent focus:ring-2 focus:ring-primary rounded p-1" type="text" value="15,000"/>
|
||||
</td>
|
||||
<td class="p-3 border-b border-outline-variant text-right font-medium">360,000</td>
|
||||
</tr>
|
||||
<tr class="hover:bg-surface-container transition-colors">
|
||||
<td class="p-3 border-b border-outline-variant">바닥재</td>
|
||||
<td class="p-3 border-b border-outline-variant">니들펀치 카펫 (회색)</td>
|
||||
<td class="p-3 border-b border-outline-variant text-center">120</td>
|
||||
<td class="p-3 border-b border-outline-variant text-right">
|
||||
<input class="w-32 text-right border-0 bg-transparent focus:ring-2 focus:ring-primary rounded p-1" type="text" value="12,000"/>
|
||||
</td>
|
||||
<td class="p-3 border-b border-outline-variant text-right font-medium">1,440,000</td>
|
||||
</tr>
|
||||
<tr class="hover:bg-surface-container transition-colors">
|
||||
<td class="p-3 border-b border-outline-variant">가구</td>
|
||||
<td class="p-3 border-b border-outline-variant">상담용 테이블/체어 세트</td>
|
||||
<td class="p-3 border-b border-outline-variant text-center">8</td>
|
||||
<td class="p-3 border-b border-outline-variant text-right">
|
||||
<input class="w-32 text-right border-0 bg-transparent focus:ring-2 focus:ring-primary rounded p-1" type="text" value="380,000"/>
|
||||
</td>
|
||||
<td class="p-3 border-b border-outline-variant text-right font-medium">3,040,000</td>
|
||||
</tr>
|
||||
<tr class="hover:bg-surface-container transition-colors">
|
||||
<td class="p-3 border-b border-outline-variant">그래픽</td>
|
||||
<td class="p-3 border-b border-outline-variant">대형 배너 실사출력</td>
|
||||
<td class="p-3 border-b border-outline-variant text-center">6</td>
|
||||
<td class="p-3 border-b border-outline-variant text-right">
|
||||
<input class="w-32 text-right border-0 bg-transparent focus:ring-2 focus:ring-primary rounded p-1" type="text" value="106,000"/>
|
||||
</td>
|
||||
<td class="p-3 border-b border-outline-variant text-right font-medium">636,000</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!-- Calculation & Form Section -->
|
||||
<div class="p-stack-lg bg-surface-container-low grid grid-cols-2 gap-stack-lg">
|
||||
<div class="space-y-4">
|
||||
<div class="flex flex-col gap-1">
|
||||
<label class="text-label-md text-outline">납기일</label>
|
||||
<input class="bg-surface-container-lowest border border-outline-variant rounded p-2 text-body-sm focus:ring-2 focus:ring-primary outline-none" type="date" value="2024-10-15"/>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<label class="text-label-md text-outline">견적 유효기간</label>
|
||||
<select class="bg-surface-container-lowest border border-outline-variant rounded p-2 text-body-sm focus:ring-2 focus:ring-primary outline-none">
|
||||
<option>발행일로부터 30일</option>
|
||||
<option>발행일로부터 15일</option>
|
||||
<option>라운드 종료시까지</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<label class="text-label-md text-outline">기타 조건</label>
|
||||
<textarea class="bg-surface-container-lowest border border-outline-variant rounded p-2 text-body-sm focus:ring-2 focus:ring-primary outline-none resize-none" rows="2">운반비 및 설치비 포함. VAT 별도.</textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col justify-end items-end gap-2 text-right">
|
||||
<div class="flex justify-between w-64 text-body-sm text-on-surface-variant">
|
||||
<span>소계</span>
|
||||
<span>₩7,636,000</span>
|
||||
</div>
|
||||
<div class="flex justify-between w-64 text-body-sm text-on-surface-variant">
|
||||
<span>부가세(10%)</span>
|
||||
<span>₩763,600</span>
|
||||
</div>
|
||||
<div class="h-[1px] w-64 bg-outline-variant my-2"></div>
|
||||
<div class="flex justify-between w-64 text-title-sm font-bold text-primary">
|
||||
<span>최종 견적 총액</span>
|
||||
<span class="text-headline-md">₩8,399,600</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<!-- Right Side: Preview & History -->
|
||||
<aside class="w-[420px] flex flex-col gap-gutter overflow-hidden">
|
||||
<!-- Preview Card -->
|
||||
<div class="flex-1 bg-surface-container rounded-xl p-6 overflow-hidden flex flex-col items-center">
|
||||
<div class="w-full flex justify-between items-center mb-4">
|
||||
<span class="text-label-md font-bold text-on-surface-variant flex items-center gap-2">
|
||||
<span class="material-symbols-outlined">visibility</span> 실시간 미리보기
|
||||
</span>
|
||||
<button class="material-symbols-outlined text-outline hover:text-primary transition-colors">fullscreen</button>
|
||||
</div>
|
||||
<div class="a4-preview bg-white p-8 relative flex flex-col text-[10px] text-[#333]">
|
||||
<!-- PDF Layout Simulation -->
|
||||
<div class="flex justify-between items-start mb-6">
|
||||
<div class="w-16 h-8 bg-surface-container flex items-center justify-center font-bold text-[8px] text-outline">LOGO</div>
|
||||
<div class="text-right">
|
||||
<h2 class="text-lg font-bold tracking-widest text-primary border-b border-primary mb-1">견 적 서</h2>
|
||||
<p>No. KTX-2024-0824</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4 mb-6">
|
||||
<div class="border p-2">
|
||||
<p class="font-bold mb-1 border-b">공급 받는 자</p>
|
||||
<p>KINTEX Exhibition Center</p>
|
||||
<p>전시운영본부 귀하</p>
|
||||
</div>
|
||||
<div class="border p-2">
|
||||
<p class="font-bold mb-1 border-b">공급자</p>
|
||||
<p>(주)킨텍스글로벌</p>
|
||||
<p>대표자: 홍길동</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<p class="font-bold mb-1">합계금액: 금 팔백삼십구만구천육백원정 (₩8,399,600)</p>
|
||||
<table class="w-full border-collapse border border-black">
|
||||
<thead>
|
||||
<tr class="bg-gray-100">
|
||||
<th class="border border-black p-1">품명</th>
|
||||
<th class="border border-black p-1">수량</th>
|
||||
<th class="border border-black p-1">단가</th>
|
||||
<th class="border border-black p-1">금액</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td class="border border-black p-1">골조 (AL Profile)</td><td class="border border-black p-1 text-center">48</td><td class="border border-black p-1 text-right">45,000</td><td class="border border-black p-1 text-right">2,160,000</td></tr>
|
||||
<tr><td class="border border-black p-1">조명 (LED)</td><td class="border border-black p-1 text-center">24</td><td class="border border-black p-1 text-right">15,000</td><td class="border border-black p-1 text-right">360,000</td></tr>
|
||||
<tr><td class="border border-black p-1">바닥재 (카펫)</td><td class="border border-black p-1 text-center">120</td><td class="border border-black p-1 text-right">12,000</td><td class="border border-black p-1 text-right">1,440,000</td></tr>
|
||||
<tr><td class="border border-black p-1">기타...</td><td class="border border-black p-1 text-center">-</td><td class="border border-black p-1 text-right">-</td><td class="border border-black p-1 text-right">3,676,000</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="mt-auto flex justify-end gap-12 items-end">
|
||||
<p class="font-bold">2024년 08월 24일</p>
|
||||
<div class="relative pb-2">
|
||||
<span>인</span>
|
||||
<div class="absolute -top-4 -right-4 w-10 h-10 border-2 border-red-500 rounded-full flex items-center justify-center opacity-50 rotate-12">
|
||||
<span class="text-red-500 font-bold text-[8px]">대표이사</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Version History -->
|
||||
<div class="bg-surface-container-lowest border border-outline-variant rounded-xl p-stack-md">
|
||||
<h3 class="text-label-md font-bold text-on-surface mb-3 flex items-center gap-2">
|
||||
<span class="material-symbols-outlined text-[20px]">history</span> 버전 히스토리
|
||||
</h3>
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center justify-between p-3 bg-surface-container rounded-lg border-l-4 border-primary">
|
||||
<div>
|
||||
<p class="text-body-sm font-bold">v2 (현재 라운드)</p>
|
||||
<p class="text-[11px] text-outline">방금 전 수정됨</p>
|
||||
</div>
|
||||
<span class="text-label-md font-bold text-primary">₩8,399,600</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between p-3 hover:bg-surface-container-low rounded-lg transition-colors cursor-pointer group">
|
||||
<div>
|
||||
<p class="text-body-sm font-medium text-on-surface-variant group-hover:text-primary">v1 (라운드 1)</p>
|
||||
<p class="text-[11px] text-outline">2시간 전 제출</p>
|
||||
</div>
|
||||
<span class="text-label-md font-medium text-on-surface-variant">₩8,700,000</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</main>
|
||||
<!-- Sticky Footer Actions -->
|
||||
<footer class="fixed bottom-0 left-0 right-0 h-20 bg-surface-container-lowest border-t border-outline-variant z-50 flex items-center justify-between px-container-padding shadow-[0_-4px_20px_rgba(0,0,0,0.05)]">
|
||||
<div class="flex items-center gap-4">
|
||||
<button class="flex items-center gap-2 text-label-md font-bold text-on-surface-variant hover:bg-surface-container-low px-6 py-3 rounded-full transition-all">
|
||||
<span class="material-symbols-outlined">save</span> 임시 저장
|
||||
</button>
|
||||
<div class="h-6 w-[1px] bg-outline-variant mx-2"></div>
|
||||
<p class="text-body-sm text-outline">최종 저장: 14:32:05</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<button class="bg-surface-container-highest text-primary px-8 py-3 rounded-full text-label-md font-bold hover:bg-surface-container transition-all">
|
||||
취소
|
||||
</button>
|
||||
<button class="bg-[#0066B3] text-white px-10 py-3 rounded-full text-label-md font-bold hover:brightness-110 shadow-lg shadow-[#0066B3]/20 flex items-center gap-2 transform active:scale-95 transition-all">
|
||||
견적서 제출 (응찰) <span class="material-symbols-outlined">send</span>
|
||||
</button>
|
||||
</div>
|
||||
</footer>
|
||||
<script>
|
||||
// Micro-interaction: Update calculated values on input change
|
||||
const unitPriceInputs = document.querySelectorAll('input[type="text"]');
|
||||
unitPriceInputs.forEach(input => {
|
||||
input.addEventListener('focus', () => {
|
||||
input.parentElement.parentElement.classList.add('bg-primary-fixed/20');
|
||||
});
|
||||
input.addEventListener('blur', () => {
|
||||
input.parentElement.parentElement.classList.remove('bg-primary-fixed/20');
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body></html>
|
||||
487
stitch_kintex_ai_system_architect/scr_a1/code.html
Normal file
487
stitch_kintex_ai_system_architect/scr_a1/code.html
Normal file
@ -0,0 +1,487 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html class="light" lang="ko"><head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||
<title>KINTEX AI Exhibition Manager - 사용자 관리</title>
|
||||
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Geist:wght@400;500;600&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<script id="tailwind-config">
|
||||
tailwind.config = {
|
||||
darkMode: "class",
|
||||
theme: {
|
||||
extend: {
|
||||
"colors": {
|
||||
"surface-container-lowest": "#ffffff",
|
||||
"primary-fixed-dim": "#a2c9ff",
|
||||
"surface-variant": "#dbe2f9",
|
||||
"error": "#ba1a1a",
|
||||
"primary-container": "#0066b3",
|
||||
"inverse-primary": "#a2c9ff",
|
||||
"on-primary-fixed": "#001c38",
|
||||
"background": "#f9f9ff",
|
||||
"surface-container-low": "#f1f3ff",
|
||||
"surface-dim": "#d2daf0",
|
||||
"secondary": "#5427e6",
|
||||
"secondary-fixed-dim": "#c9bfff",
|
||||
"on-tertiary-fixed-variant": "#005236",
|
||||
"on-tertiary-container": "#8ef7c3",
|
||||
"tertiary-fixed-dim": "#72daa9",
|
||||
"tertiary": "#00583b",
|
||||
"secondary-fixed": "#e5deff",
|
||||
"inverse-surface": "#293041",
|
||||
"on-surface-variant": "#414751",
|
||||
"on-error-container": "#93000a",
|
||||
"on-tertiary-fixed": "#002113",
|
||||
"on-surface": "#141b2c",
|
||||
"on-primary-fixed-variant": "#004881",
|
||||
"inverse-on-surface": "#edf0ff",
|
||||
"tertiary-fixed": "#8ef7c4",
|
||||
"surface-tint": "#0060a9",
|
||||
"on-primary-container": "#d2e3ff",
|
||||
"tertiary-fixed": "#8ef7c4",
|
||||
"on-secondary-fixed-variant": "#4500d8",
|
||||
"on-primary": "#ffffff",
|
||||
"secondary-container": "#6d4aff",
|
||||
"outline-variant": "#c1c7d3",
|
||||
"surface": "#f9f9ff",
|
||||
"on-secondary-container": "#f3edff",
|
||||
"primary-fixed": "#d3e4ff",
|
||||
"surface-bright": "#f9f9ff",
|
||||
"primary": "#004e8b",
|
||||
"outline": "#717782",
|
||||
"on-secondary-fixed": "#1b0063",
|
||||
"error-container": "#ffdad6",
|
||||
"surface-container-highest": "#dbe2f9",
|
||||
"surface-container": "#e9edff",
|
||||
"on-tertiary": "#ffffff",
|
||||
"on-background": "#141b2c",
|
||||
"surface-container-high": "#e0e8ff",
|
||||
"on-secondary": "#ffffff",
|
||||
"on-error": "#ffffff",
|
||||
"border-neutral": "#E4E7EC"
|
||||
},
|
||||
"borderRadius": {
|
||||
"DEFAULT": "0.125rem",
|
||||
"lg": "0.25rem",
|
||||
"xl": "0.5rem",
|
||||
"full": "0.75rem"
|
||||
},
|
||||
"spacing": {
|
||||
"stack-sm": "8px",
|
||||
"gutter": "16px",
|
||||
"container-padding": "24px",
|
||||
"stack-md": "16px",
|
||||
"base": "4px",
|
||||
"stack-lg": "24px"
|
||||
},
|
||||
"fontFamily": {
|
||||
"title-sm": ["Inter", "sans-serif"],
|
||||
"label-md": ["Geist", "sans-serif"],
|
||||
"headline-md": ["Inter", "sans-serif"],
|
||||
"body-sm": ["Inter", "sans-serif"],
|
||||
"body-base": ["Inter", "sans-serif"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
body { font-family: 'Inter', 'Pretendard', sans-serif; background-color: #f9f9ff; }
|
||||
.material-symbols-outlined { font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24; }
|
||||
.scrollbar-hide::-webkit-scrollbar { display: none; }
|
||||
.status-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; margin-right: 6px; }
|
||||
.zebra-table tr:nth-child(even) { background-color: #f1f3ff; }
|
||||
.sticky-header { position: sticky; top: 0; z-index: 10; }
|
||||
.detail-panel { transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1); }
|
||||
.detail-panel.hidden-state { transform: translateX(100%); }
|
||||
</style>
|
||||
</head>
|
||||
<body class="flex h-screen overflow-hidden text-on-surface">
|
||||
<!-- SIDEBAR (Persistent Sidebar 240px) -->
|
||||
<aside class="fixed left-0 top-0 h-screen w-60 bg-surface-container-low border-r border-outline-variant flex flex-col py-container-padding z-50">
|
||||
<div class="px-6 mb-10">
|
||||
<img alt="KINTEX Brand Logo" class="h-8 object-contain" src="https://lh3.googleusercontent.com/aida-public/AB6AXuBViMH_wLabnBXthxb2bO1MQ17WQQql68DBtN_ryQjSUaLfHf72x6X15QE7BRw7sXzcComGfAr5qwy3wTagsmvvfbFN54HLQgLta594ULaGbiPruExJTEGj3kdBlR9e1XCSD6kEhls_A9VBUlfr68ci0sKR7MsrG9Y6TBYN3dlqVhizX-Nie9dpJjIlIMH-BI1DDzcymO5K2bHHV_xttgf09AlnNfie8QS6wbT9aJWg1gk1qmI7ODagsGHklzXQ5rx-78yUp-RXCCI"/>
|
||||
<div class="mt-4">
|
||||
<h1 class="font-headline-md text-headline-md font-bold text-primary">KINTEX AI</h1>
|
||||
<p class="text-body-sm font-body-sm text-on-surface-variant">Exhibition Manager</p>
|
||||
</div>
|
||||
</div>
|
||||
<nav class="flex-1 px-3 space-y-1">
|
||||
<a class="flex items-center px-4 py-3 text-on-surface-variant hover:bg-surface-container-high transition-colors rounded-lg group" href="#">
|
||||
<span class="material-symbols-outlined mr-3 group-hover:text-primary">dashboard</span>
|
||||
<span class="text-label-md font-label-md">대시보드</span>
|
||||
</a>
|
||||
<a class="flex items-center px-4 py-3 text-primary font-bold border-r-4 border-primary bg-primary-container/10 transition-all duration-200" href="#">
|
||||
<span class="material-symbols-outlined mr-3">person_search</span>
|
||||
<span class="text-label-md font-label-md">사용자 관리</span>
|
||||
</a>
|
||||
<a class="flex items-center px-4 py-3 text-on-surface-variant hover:bg-surface-container-high transition-colors rounded-lg group" href="#">
|
||||
<span class="material-symbols-outlined mr-3 group-hover:text-primary">architecture</span>
|
||||
<span class="text-label-md font-label-md">전시장 스튜디오</span>
|
||||
</a>
|
||||
<a class="flex items-center px-4 py-3 text-on-surface-variant hover:bg-surface-container-high transition-colors rounded-lg group" href="#">
|
||||
<span class="material-symbols-outlined mr-3 group-hover:text-primary">groups</span>
|
||||
<span class="text-label-md font-label-md">참가업체 관리</span>
|
||||
</a>
|
||||
<a class="flex items-center px-4 py-3 text-on-surface-variant hover:bg-surface-container-high transition-colors rounded-lg group" href="#">
|
||||
<span class="material-symbols-outlined mr-3 group-hover:text-primary">assignment</span>
|
||||
<span class="text-label-md font-label-md">문서 보관함</span>
|
||||
</a>
|
||||
</nav>
|
||||
<div class="px-3 pt-6 border-t border-outline-variant">
|
||||
<a class="flex items-center px-4 py-3 text-on-surface-variant hover:bg-surface-container-high transition-colors rounded-lg group" href="#">
|
||||
<span class="material-symbols-outlined mr-3 group-hover:text-primary">settings</span>
|
||||
<span class="text-label-md font-label-md">시스템 설정</span>
|
||||
</a>
|
||||
<a class="flex items-center px-4 py-3 text-on-surface-variant hover:bg-surface-container-high transition-colors rounded-lg group" href="#">
|
||||
<span class="material-symbols-outlined mr-3 group-hover:text-primary">help</span>
|
||||
<span class="text-label-md font-label-md">기술 지원</span>
|
||||
</a>
|
||||
</div>
|
||||
</aside>
|
||||
<!-- MAIN WRAPPER -->
|
||||
<div class="flex-1 ml-60 flex flex-col min-w-0">
|
||||
<!-- TOP APP BAR -->
|
||||
<header class="h-16 flex items-center justify-between px-container-padding bg-surface border-b border-outline-variant sticky top-0 z-40">
|
||||
<div class="flex items-center space-x-6">
|
||||
<!-- Workspace Switcher -->
|
||||
<button class="flex items-center space-x-2 px-3 py-1.5 hover:bg-surface-container-high rounded-lg transition-colors border border-outline-variant">
|
||||
<span class="text-label-md font-label-md font-bold">KINTEX</span>
|
||||
<span class="material-symbols-outlined text-sm">arrow_drop_down</span>
|
||||
</button>
|
||||
<!-- Global Search -->
|
||||
<div class="relative w-96">
|
||||
<span class="material-symbols-outlined absolute left-3 top-1/2 -translate-y-1/2 text-on-surface-variant text-xl">search</span>
|
||||
<input class="w-full pl-10 pr-4 py-2 bg-surface-container-low border border-outline-variant rounded-full text-body-sm focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary transition-all" placeholder="메뉴, 사용자, 전시회 검색..." type="text"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center space-x-4">
|
||||
<button class="p-2 text-on-surface-variant hover:bg-surface-container-high rounded-full transition-all relative">
|
||||
<span class="material-symbols-outlined">notifications</span>
|
||||
<span class="absolute top-2 right-2 w-2 h-2 bg-error rounded-full"></span>
|
||||
</button>
|
||||
<button class="p-2 text-on-surface-variant hover:bg-surface-container-high rounded-full transition-all">
|
||||
<span class="material-symbols-outlined">help</span>
|
||||
</button>
|
||||
<div class="h-8 w-px bg-outline-variant mx-2"></div>
|
||||
<div class="flex items-center space-x-3 cursor-pointer p-1 pr-3 hover:bg-surface-container-high rounded-full transition-all">
|
||||
<div class="w-8 h-8 rounded-full bg-primary flex items-center justify-center text-on-primary font-bold text-xs">관리</div>
|
||||
<span class="text-label-md font-label-md">운영지원팀</span>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<!-- CONTENT AREA -->
|
||||
<main class="flex-1 overflow-auto p-container-padding">
|
||||
<div class="flex justify-between items-end mb-8">
|
||||
<div>
|
||||
<h2 class="text-display-lg font-display-lg text-on-surface mb-1">사용자 관리</h2>
|
||||
<p class="text-body-base text-on-surface-variant">플랫폼에 등록된 모든 사용자의 권한과 상태를 관리합니다.</p>
|
||||
</div>
|
||||
<button class="flex items-center px-6 py-2.5 bg-primary text-on-primary font-bold rounded-lg hover:opacity-90 transition-all shadow-sm">
|
||||
<span class="material-symbols-outlined mr-2">person_add</span>
|
||||
<span>사용자 추가</span>
|
||||
</button>
|
||||
</div>
|
||||
<!-- FILTERS & STATS -->
|
||||
<div class="grid grid-cols-4 gap-gutter mb-8">
|
||||
<div class="bg-surface border border-border-neutral p-4 rounded-xl">
|
||||
<p class="text-label-md text-on-surface-variant mb-1">전체 사용자</p>
|
||||
<p class="text-headline-md font-bold">1,284 <span class="text-body-sm font-normal text-on-surface-variant">명</span></p>
|
||||
</div>
|
||||
<div class="bg-surface border border-border-neutral p-4 rounded-xl border-l-4 border-l-primary">
|
||||
<p class="text-label-md text-on-surface-variant mb-1">활성 계정</p>
|
||||
<p class="text-headline-md font-bold">1,240 <span class="text-body-sm font-normal text-on-surface-variant">명</span></p>
|
||||
</div>
|
||||
<div class="bg-surface border border-border-neutral p-4 rounded-xl border-l-4 border-l-secondary">
|
||||
<p class="text-label-md text-on-surface-variant mb-1">2FA 미설정</p>
|
||||
<p class="text-headline-md font-bold text-secondary">12 <span class="text-body-sm font-normal text-on-surface-variant">명</span></p>
|
||||
</div>
|
||||
<div class="bg-surface border border-border-neutral p-4 rounded-xl border-l-4 border-l-error">
|
||||
<p class="text-label-md text-on-surface-variant mb-1">잠김 계정</p>
|
||||
<p class="text-headline-md font-bold text-error">3 <span class="text-body-sm font-normal text-on-surface-variant">건</span></p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- DATA TABLE -->
|
||||
<div class="bg-surface border border-border-neutral rounded-xl overflow-hidden shadow-sm">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-left zebra-table border-collapse">
|
||||
<thead class="sticky-header bg-surface-container-low text-on-surface-variant border-b border-border-neutral">
|
||||
<tr>
|
||||
<th class="px-6 py-4 text-label-md font-label-md uppercase tracking-wider">이름</th>
|
||||
<th class="px-6 py-4 text-label-md font-label-md uppercase tracking-wider">이메일</th>
|
||||
<th class="px-6 py-4 text-label-md font-label-md uppercase tracking-wider">역할</th>
|
||||
<th class="px-6 py-4 text-label-md font-label-md uppercase tracking-wider">소속</th>
|
||||
<th class="px-6 py-4 text-label-md font-label-md uppercase tracking-wider">상태</th>
|
||||
<th class="px-6 py-4 text-label-md font-label-md uppercase tracking-wider">2FA</th>
|
||||
<th class="px-6 py-4 text-label-md font-label-md uppercase tracking-wider">최근 접속</th>
|
||||
<th class="px-6 py-4 text-label-md font-label-md uppercase tracking-wider text-right">작업</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-border-neutral">
|
||||
<!-- Row 1 -->
|
||||
<tr class="hover:bg-primary-container/5 transition-colors group cursor-pointer" onclick="openDetail('김민수', 'admin***@kintex.com', '관리자', 'KINTEX 본사')">
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="flex items-center">
|
||||
<div class="w-8 h-8 rounded-full bg-slate-200 flex items-center justify-center mr-3 font-bold text-xs text-slate-600">김</div>
|
||||
<span class="text-body-base font-semibold">김민수</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-body-sm text-on-surface-variant">admin***@kintex.com</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<span class="px-2.5 py-1 rounded-full text-[11px] font-bold bg-error/10 text-error border border-error/20">관리자</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-body-sm">KINTEX 본사</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<span class="flex items-center text-body-sm">
|
||||
<span class="status-dot bg-green-500"></span> 활성
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="flex items-center text-primary">
|
||||
<span class="material-symbols-outlined text-lg mr-1">verified_user</span>
|
||||
<span class="text-[11px] font-bold">2FA 설정됨</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-body-sm text-on-surface-variant">2023-11-20 14:22</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-right">
|
||||
<button class="text-on-surface-variant hover:text-primary"><span class="material-symbols-outlined">more_vert</span></button>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Row 2 -->
|
||||
<tr class="hover:bg-primary-container/5 transition-colors group cursor-pointer" onclick="openDetail('이서연', 'sy.lee***@company.com', '주최자', '글로벌 엑스포')">
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="flex items-center">
|
||||
<div class="w-8 h-8 rounded-full bg-blue-100 flex items-center justify-center mr-3 font-bold text-xs text-blue-600">이</div>
|
||||
<span class="text-body-base font-semibold">이서연</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-body-sm text-on-surface-variant">sy.lee***@company.com</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<span class="px-2.5 py-1 rounded-full text-[11px] font-bold bg-primary/10 text-primary border border-primary/20">주최자</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-body-sm">글로벌 엑스포</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<span class="flex items-center text-body-sm">
|
||||
<span class="status-dot bg-green-500"></span> 활성
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="flex items-center text-primary">
|
||||
<span class="material-symbols-outlined text-lg mr-1">verified_user</span>
|
||||
<span class="text-[11px] font-bold">2FA 설정됨</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-body-sm text-on-surface-variant">2023-11-20 09:15</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-right">
|
||||
<button class="text-on-surface-variant hover:text-primary"><span class="material-symbols-outlined">more_vert</span></button>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Row 3 -->
|
||||
<tr class="hover:bg-primary-container/5 transition-colors group cursor-pointer" onclick="openDetail('박지훈', 'jihoon***@booth.net', '참가업체', '테크트리 주식회사')">
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="flex items-center">
|
||||
<div class="w-8 h-8 rounded-full bg-green-100 flex items-center justify-center mr-3 font-bold text-xs text-green-600">박</div>
|
||||
<span class="text-body-base font-semibold">박지훈</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-body-sm text-on-surface-variant">jihoon***@booth.net</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<span class="px-2.5 py-1 rounded-full text-[11px] font-bold bg-tertiary-container/20 text-tertiary-fixed-dim border border-tertiary/20">참가업체</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-body-sm">테크트리 주식회사</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<span class="flex items-center text-body-sm">
|
||||
<span class="status-dot bg-yellow-500"></span> 잠금
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="flex items-center text-outline">
|
||||
<span class="material-symbols-outlined text-lg mr-1">no_encryption</span>
|
||||
<span class="text-[11px] font-bold">미설정</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-body-sm text-on-surface-variant">2023-11-18 17:40</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-right">
|
||||
<button class="text-on-surface-variant hover:text-primary"><span class="material-symbols-outlined">more_vert</span></button>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Row 4 -->
|
||||
<tr class="hover:bg-primary-container/5 transition-colors group cursor-pointer" onclick="openDetail('최현우', 'hwchoi***@system.kr', '장치업체', '대성 인테리어')">
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="flex items-center">
|
||||
<div class="w-8 h-8 rounded-full bg-orange-100 flex items-center justify-center mr-3 font-bold text-xs text-orange-600">최</div>
|
||||
<span class="text-body-base font-semibold">최현우</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-body-sm text-on-surface-variant">hwchoi***@system.kr</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<span class="px-2.5 py-1 rounded-full text-[11px] font-bold bg-orange-100 text-orange-600 border border-orange-200">장치업체</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-body-sm">대성 인테리어</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<span class="flex items-center text-body-sm">
|
||||
<span class="status-dot bg-green-500"></span> 활성
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="flex items-center text-primary">
|
||||
<span class="material-symbols-outlined text-lg mr-1">verified_user</span>
|
||||
<span class="text-[11px] font-bold">2FA 설정됨</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-body-sm text-on-surface-variant">2023-11-19 11:05</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-right">
|
||||
<button class="text-on-surface-variant hover:text-primary"><span class="material-symbols-outlined">more_vert</span></button>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Row 5 -->
|
||||
<tr class="hover:bg-primary-container/5 transition-colors group cursor-pointer" onclick="openDetail('정미경', 'mk.jung***@kintex.com', '홀매니저', 'KINTEX 1전시장')">
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="flex items-center">
|
||||
<div class="w-8 h-8 rounded-full bg-purple-100 flex items-center justify-center mr-3 font-bold text-xs text-purple-600">정</div>
|
||||
<span class="text-body-base font-semibold">정미경</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-body-sm text-on-surface-variant">mk.jung***@kintex.com</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<span class="px-2.5 py-1 rounded-full text-[11px] font-bold bg-secondary/10 text-secondary border border-secondary/20">홀매니저</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-body-sm">KINTEX 1전시장</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<span class="flex items-center text-body-sm">
|
||||
<span class="status-dot bg-gray-400"></span> 비활성
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="flex items-center text-primary">
|
||||
<span class="material-symbols-outlined text-lg mr-1">verified_user</span>
|
||||
<span class="text-[11px] font-bold">2FA 설정됨</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-body-sm text-on-surface-variant">2023-10-30 16:55</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-right">
|
||||
<button class="text-on-surface-variant hover:text-primary"><span class="material-symbols-outlined">more_vert</span></button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!-- Pagination -->
|
||||
<div class="px-6 py-4 bg-surface border-t border-border-neutral flex items-center justify-between">
|
||||
<span class="text-body-sm text-on-surface-variant">총 1,284개 중 1-5 표시</span>
|
||||
<div class="flex space-x-1">
|
||||
<button class="w-8 h-8 flex items-center justify-center rounded border border-border-neutral text-on-surface-variant hover:bg-surface-container-low"><span class="material-symbols-outlined text-sm">chevron_left</span></button>
|
||||
<button class="w-8 h-8 flex items-center justify-center rounded bg-primary text-on-primary text-xs font-bold">1</button>
|
||||
<button class="w-8 h-8 flex items-center justify-center rounded border border-border-neutral text-xs text-on-surface-variant hover:bg-surface-container-low">2</button>
|
||||
<button class="w-8 h-8 flex items-center justify-center rounded border border-border-neutral text-xs text-on-surface-variant hover:bg-surface-container-low">3</button>
|
||||
<span class="px-1 text-on-surface-variant">...</span>
|
||||
<button class="w-8 h-8 flex items-center justify-center rounded border border-border-neutral text-xs text-on-surface-variant hover:bg-surface-container-low">25</button>
|
||||
<button class="w-8 h-8 flex items-center justify-center rounded border border-border-neutral text-on-surface-variant hover:bg-surface-container-low"><span class="material-symbols-outlined text-sm">chevron_right</span></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
<!-- RIGHT DETAIL PANEL -->
|
||||
<div class="detail-panel hidden-state fixed top-0 right-0 w-[400px] h-screen bg-surface border-l border-outline-variant shadow-2xl z-[60] flex flex-col" id="detailPanel">
|
||||
<div class="p-6 border-b border-outline-variant flex items-center justify-between">
|
||||
<h3 class="text-title-sm font-title-sm">사용자 정보 수정</h3>
|
||||
<button class="p-2 hover:bg-surface-container-high rounded-full transition-colors" onclick="closeDetail()">
|
||||
<span class="material-symbols-outlined">close</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex-1 overflow-auto p-6 space-y-8">
|
||||
<!-- Profile Summary -->
|
||||
<div class="flex flex-col items-center text-center">
|
||||
<div class="w-20 h-20 rounded-full bg-primary/10 flex items-center justify-center mb-4 border-2 border-primary/20">
|
||||
<span class="text-2xl font-bold text-primary" id="detailInitial">김</span>
|
||||
</div>
|
||||
<h4 class="text-headline-md font-bold mb-1" id="detailName">김민수</h4>
|
||||
<p class="text-body-sm text-on-surface-variant" id="detailEmail">admin***@kintex.com</p>
|
||||
<p class="text-label-md font-label-md mt-2 text-primary bg-primary-container/10 px-3 py-1 rounded-full" id="detailOrg">KINTEX 본사</p>
|
||||
</div>
|
||||
<!-- Role Assignment -->
|
||||
<div>
|
||||
<label class="block text-label-md font-label-md text-on-surface-variant mb-2">권한 역할 부여</label>
|
||||
<select class="w-full bg-surface-container-low border border-outline-variant rounded-lg p-2.5 text-body-base focus:ring-2 focus:ring-primary/20 outline-none" id="detailRole">
|
||||
<option value="관리자">관리자 (Full Access)</option>
|
||||
<option value="주최자">주최자 (Organizer)</option>
|
||||
<option value="참가업체">참가업체 (Exhibitor)</option>
|
||||
<option value="장치업체">장치업체 (Contractor)</option>
|
||||
<option value="홀매니저">홀매니저 (Hall Manager)</option>
|
||||
</select>
|
||||
<p class="mt-2 text-xs text-on-surface-variant">역할에 따라 시스템 접근 권한이 즉시 변경됩니다.</p>
|
||||
</div>
|
||||
<!-- Status Toggle -->
|
||||
<div>
|
||||
<label class="block text-label-md font-label-md text-on-surface-variant mb-4">계정 상태 설정</label>
|
||||
<div class="flex items-center justify-between p-4 bg-surface-container-low rounded-xl border border-outline-variant">
|
||||
<div class="flex items-center">
|
||||
<div class="w-3 h-3 bg-green-500 rounded-full mr-3"></div>
|
||||
<span class="text-body-base font-semibold">활성화 상태</span>
|
||||
</div>
|
||||
<label class="relative inline-flex items-center cursor-pointer">
|
||||
<input checked="" class="sr-only peer" type="checkbox" value=""/>
|
||||
<div class="w-11 h-6 bg-outline-variant peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-primary"></div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Security Actions -->
|
||||
<div class="pt-4 space-y-4">
|
||||
<label class="block text-label-md font-label-md text-on-surface-variant">보안 도구</label>
|
||||
<button class="w-full flex items-center justify-center px-4 py-3 bg-secondary text-on-secondary font-bold rounded-lg hover:opacity-90 transition-all shadow-sm">
|
||||
<span class="material-symbols-outlined mr-2">refresh</span>
|
||||
<span>OTP 초기화 (Reset OTP)</span>
|
||||
</button>
|
||||
<button class="w-full flex items-center justify-center px-4 py-3 border border-error text-error font-bold rounded-lg hover:bg-error/5 transition-all">
|
||||
<span class="material-symbols-outlined mr-2">block</span>
|
||||
<span>로그인 세션 강제 종료</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-6 border-t border-outline-variant bg-surface-container-low flex space-x-3">
|
||||
<button class="flex-1 px-4 py-3 border border-outline-variant text-on-surface font-bold rounded-lg hover:bg-white transition-all" onclick="closeDetail()">취소</button>
|
||||
<button class="flex-[2] px-4 py-3 bg-primary text-on-primary font-bold rounded-lg hover:opacity-90 transition-all">변경사항 저장</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Background Overlay when Detail is open -->
|
||||
<div class="fixed inset-0 bg-black/20 backdrop-blur-sm z-50 hidden opacity-0 transition-opacity duration-300" id="overlay" onclick="closeDetail()"></div>
|
||||
<script>
|
||||
const detailPanel = document.getElementById('detailPanel');
|
||||
const overlay = document.getElementById('overlay');
|
||||
|
||||
function openDetail(name, email, role, org) {
|
||||
document.getElementById('detailName').innerText = name;
|
||||
document.getElementById('detailEmail').innerText = email;
|
||||
document.getElementById('detailOrg').innerText = org;
|
||||
document.getElementById('detailInitial').innerText = name.charAt(0);
|
||||
document.getElementById('detailRole').value = role;
|
||||
|
||||
detailPanel.classList.remove('hidden-state');
|
||||
overlay.classList.remove('hidden');
|
||||
setTimeout(() => {
|
||||
overlay.classList.add('opacity-100');
|
||||
}, 10);
|
||||
}
|
||||
|
||||
function closeDetail() {
|
||||
detailPanel.classList.add('hidden-state');
|
||||
overlay.classList.remove('opacity-100');
|
||||
setTimeout(() => {
|
||||
overlay.classList.add('hidden');
|
||||
}, 300);
|
||||
}
|
||||
|
||||
// Search highlight micro-interaction
|
||||
const searchInput = document.querySelector('input[type="text"]');
|
||||
searchInput.addEventListener('focus', () => {
|
||||
searchInput.parentElement.classList.add('scale-[1.02]');
|
||||
});
|
||||
searchInput.addEventListener('blur', () => {
|
||||
searchInput.parentElement.classList.remove('scale-[1.02]');
|
||||
});
|
||||
</script>
|
||||
</body></html>
|
||||
347
stitch_kintex_ai_system_architect/scr_a10/code.html
Normal file
347
stitch_kintex_ai_system_architect/scr_a10/code.html
Normal file
@ -0,0 +1,347 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html class="light" lang="ko"><head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||
<title>KINTEX AI 전시관리 - AI 플랫폼 설정</title>
|
||||
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Geist:wght@400;500;600&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<script id="tailwind-config">
|
||||
tailwind.config = {
|
||||
darkMode: "class",
|
||||
theme: {
|
||||
extend: {
|
||||
"colors": {
|
||||
"surface-container-highest": "#dbe2f9",
|
||||
"primary-fixed-dim": "#a2c9ff",
|
||||
"on-error-container": "#93000a",
|
||||
"on-tertiary-container": "#8ef7c3",
|
||||
"on-primary-container": "#d2e3ff",
|
||||
"on-background": "#141b2c",
|
||||
"primary-container": "#0066b3",
|
||||
"on-secondary-container": "#f3edff",
|
||||
"tertiary": "#00583b",
|
||||
"on-secondary-fixed": "#1b0063",
|
||||
"surface-container": "#e9edff",
|
||||
"tertiary-fixed-dim": "#72daa9",
|
||||
"outline-variant": "#c1c7d3",
|
||||
"on-primary-fixed-variant": "#004881",
|
||||
"surface-container-low": "#f1f3ff",
|
||||
"inverse-on-surface": "#edf0ff",
|
||||
"secondary-fixed": "#e5deff",
|
||||
"on-primary-fixed": "#001c38",
|
||||
"tertiary-fixed": "#8ef7c4",
|
||||
"surface-container-lowest": "#ffffff",
|
||||
"on-tertiary-fixed": "#002113",
|
||||
"outline": "#717782",
|
||||
"error": "#ba1a1a",
|
||||
"primary-fixed": "#d3e4ff",
|
||||
"secondary-container": "#6d4aff",
|
||||
"on-secondary-fixed-variant": "#4500d8",
|
||||
"surface-variant": "#dbe2f9",
|
||||
"inverse-surface": "#293041",
|
||||
"surface": "#f9f9ff",
|
||||
"surface-bright": "#f9f9ff",
|
||||
"on-tertiary-fixed-variant": "#005236",
|
||||
"on-error": "#ffffff",
|
||||
"tertiary-container": "#00734e",
|
||||
"on-surface-variant": "#414751",
|
||||
"secondary-fixed-dim": "#c9bfff",
|
||||
"inverse-primary": "#a2c9ff",
|
||||
"surface-tint": "#0060a9",
|
||||
"on-surface": "#141b2c",
|
||||
"surface-dim": "#d2daf0",
|
||||
"secondary": "#5427e6",
|
||||
"error-container": "#ffdad6",
|
||||
"surface-container-high": "#e0e8ff",
|
||||
"on-primary": "#ffffff",
|
||||
"primary": "#004e8b",
|
||||
"on-tertiary": "#ffffff",
|
||||
"on-secondary": "#ffffff",
|
||||
"background": "#f9f9ff"
|
||||
},
|
||||
"borderRadius": {
|
||||
"DEFAULT": "0.125rem",
|
||||
"lg": "0.25rem",
|
||||
"xl": "0.5rem",
|
||||
"full": "0.75rem"
|
||||
},
|
||||
"spacing": {
|
||||
"stack-sm": "8px",
|
||||
"stack-md": "16px",
|
||||
"stack-lg": "24px",
|
||||
"gutter": "16px",
|
||||
"container-padding": "24px",
|
||||
"base": "4px"
|
||||
},
|
||||
"fontFamily": {
|
||||
"label-md": ["Geist"],
|
||||
"display-lg": ["Inter"],
|
||||
"label-xs": ["Geist"],
|
||||
"body-base": ["Inter"],
|
||||
"body-sm": ["Inter"],
|
||||
"title-sm": ["Inter"],
|
||||
"headline-md": ["Inter"],
|
||||
"display-lg-mobile": ["Inter"]
|
||||
},
|
||||
"fontSize": {
|
||||
"label-md": ["12px", {"lineHeight": "16px", "letterSpacing": "0.02em", "fontWeight": "600"}],
|
||||
"display-lg": ["36px", {"lineHeight": "44px", "letterSpacing": "-0.02em", "fontWeight": "700"}],
|
||||
"label-xs": ["11px", {"lineHeight": "14px", "fontWeight": "500"}],
|
||||
"body-base": ["14px", {"lineHeight": "20px", "fontWeight": "400"}],
|
||||
"body-sm": ["13px", {"lineHeight": "18px", "fontWeight": "400"}],
|
||||
"title-sm": ["18px", {"lineHeight": "28px", "fontWeight": "600"}],
|
||||
"headline-md": ["24px", {"lineHeight": "32px", "fontWeight": "600"}],
|
||||
"display-lg-mobile": ["28px", {"lineHeight": "36px", "fontWeight": "700"}]
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
body { font-family: 'Inter', sans-serif; }
|
||||
.material-symbols-outlined {
|
||||
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
|
||||
}
|
||||
input[type="range"]::-webkit-slider-thumb {
|
||||
appearance: none;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background: #004e8b;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
}
|
||||
.ai-glow {
|
||||
box-shadow: 0 0 15px rgba(109, 74, 255, 0.1);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-background text-on-background min-h-screen flex">
|
||||
<!-- SideNavBar (from Shared Components) -->
|
||||
<aside class="h-screen w-64 flex flex-col fixed left-0 top-0 border-r border-outline-variant bg-surface-container-lowest dark:bg-inverse-surface py-container-padding z-50">
|
||||
<div class="px-6 mb-8">
|
||||
<h1 class="font-headline-md text-headline-md font-bold text-primary dark:text-primary-fixed">KINTEX AI</h1>
|
||||
<p class="font-body-sm text-body-sm text-on-surface-variant">Exhibition Management</p>
|
||||
</div>
|
||||
<nav class="flex-1 px-3 space-y-1">
|
||||
<a class="flex items-center gap-3 px-4 py-3 text-on-surface-variant hover:bg-surface-container-high transition-colors rounded-lg" href="#">
|
||||
<span class="material-symbols-outlined">dashboard</span>
|
||||
<span class="font-label-md text-label-md">Dashboard</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-4 py-3 text-on-surface-variant hover:bg-surface-container-high transition-colors rounded-lg" href="#">
|
||||
<span class="material-symbols-outlined">group</span>
|
||||
<span class="font-label-md text-label-md">User Management</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-4 py-3 text-on-surface-variant hover:bg-surface-container-high transition-colors rounded-lg" href="#">
|
||||
<span class="material-symbols-outlined">settings_ethernet</span>
|
||||
<span class="font-label-md text-label-md">Common Code Management</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-4 py-3 text-on-surface-variant hover:bg-surface-container-high transition-colors rounded-lg" href="#">
|
||||
<span class="material-symbols-outlined">grid_view</span>
|
||||
<span class="font-label-md text-label-md">Exhibition Layout</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-4 py-3 bg-secondary-container text-on-secondary-container font-semibold rounded-lg scale-[0.99] transition-transform duration-150" href="#">
|
||||
<span class="material-symbols-outlined">settings</span>
|
||||
<span class="font-label-md text-label-md">AI 플랫폼 설정</span>
|
||||
</a>
|
||||
</nav>
|
||||
<div class="px-3 py-4 mt-auto border-t border-outline-variant">
|
||||
<a class="flex items-center gap-3 px-4 py-3 text-on-surface-variant hover:bg-surface-container-high transition-colors rounded-lg" href="#">
|
||||
<span class="material-symbols-outlined">help</span>
|
||||
<span class="font-label-md text-label-md">Help Center</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-4 py-3 text-on-surface-variant hover:bg-surface-container-high transition-colors rounded-lg" href="#">
|
||||
<span class="material-symbols-outlined">logout</span>
|
||||
<span class="font-label-md text-label-md">Logout</span>
|
||||
</a>
|
||||
</div>
|
||||
</aside>
|
||||
<!-- Main Content Canvas -->
|
||||
<main class="flex-1 ml-64 min-h-screen flex flex-col">
|
||||
<!-- TopNavBar (from Shared Components) -->
|
||||
<header class="h-16 w-full sticky top-0 z-40 border-b border-outline-variant bg-surface dark:bg-surface-dim flex items-center justify-between px-gutter">
|
||||
<div class="flex items-center gap-4">
|
||||
<span class="font-title-sm text-title-sm font-bold text-on-surface">시스템 설정 > AI 플랫폼 설정</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="relative flex items-center">
|
||||
<span class="material-symbols-outlined absolute left-3 text-outline">search</span>
|
||||
<input class="pl-10 pr-4 py-1.5 bg-surface-container-low border border-outline-variant rounded-full text-body-sm focus:outline-none focus:ring-2 focus:ring-primary/20 w-64" placeholder="설정 검색..." type="text"/>
|
||||
</div>
|
||||
<button class="p-2 text-on-surface-variant hover:text-primary transition-colors">
|
||||
<span class="material-symbols-outlined">notifications</span>
|
||||
</button>
|
||||
<div class="h-8 w-[1px] bg-outline-variant mx-1"></div>
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="text-right">
|
||||
<p class="font-label-md text-label-md">Administrator</p>
|
||||
<p class="text-[10px] text-outline uppercase tracking-wider">Super Admin</p>
|
||||
</div>
|
||||
<img class="w-10 h-10 rounded-full border border-outline-variant" data-alt="A clean, professional headshot of a corporate administrator for an exhibition management platform. The background is a blurred high-tech control room with soft blue and white lighting. The style is sharp, modern, and trustworthy, fitting a premium enterprise B2B environment." src="https://lh3.googleusercontent.com/aida-public/AB6AXuB064lMevAuA5BY3-vtPGru53rcj41VtvGanzPsa16fFPtYQW8vQQ9ojzBSSeWcmz2IBaONfcTwNdjcW6a_BswKxWwF0L_BqOdrtIstmOiPPh3MeLqgZLm77PjNJseuptTH94QrzqIezgCg0A2SrMP2Q79d9g91nlRF-000sIfKasQVIvD6ybgTiwiuNSp0iic2yZOsupnNalnI8sVKbIELf_Yx0gErpn-pL-SnlgywjaevqrDz01Z0hfTaO2OAbRsuNXDRgfHiG1s"/>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<!-- Content Area -->
|
||||
<div class="p-container-padding max-w-6xl w-full mx-auto space-y-stack-lg">
|
||||
<!-- Header Section -->
|
||||
<section class="flex flex-col gap-1">
|
||||
<h2 class="font-headline-md text-headline-md text-on-surface flex items-center gap-3">
|
||||
AI 플랫폼 설정
|
||||
<span class="bg-secondary-container text-on-secondary-container px-2 py-0.5 rounded text-[11px] font-bold tracking-tighter ai-glow border border-secondary/20">CORE ENGINE</span>
|
||||
</h2>
|
||||
<p class="font-body-base text-body-base text-on-surface-variant">시스템의 핵심 AI 엔진 및 추론 파라미터를 구성합니다.</p>
|
||||
</section>
|
||||
<!-- Bento Grid Layout -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-12 gap-gutter">
|
||||
<!-- Left Column: Text/Inference (Main) -->
|
||||
<div class="lg:col-span-8 space-y-gutter">
|
||||
<div class="bg-surface-container-lowest border border-outline-variant rounded-xl overflow-hidden">
|
||||
<div class="px-6 py-4 border-l-4 border-primary flex items-center justify-between bg-surface-container-low">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="material-symbols-outlined text-primary" style="font-variation-settings: 'FILL' 1;">psychology</span>
|
||||
<h3 class="font-title-sm text-title-sm">텍스트/추론 설정</h3>
|
||||
</div>
|
||||
<span class="bg-tertiary-fixed text-on-tertiary-fixed px-2 py-0.5 rounded-full text-[10px] font-bold">ACTIVE</span>
|
||||
</div>
|
||||
<div class="p-6 space-y-stack-lg">
|
||||
<!-- Model Router -->
|
||||
<div>
|
||||
<label class="font-label-md text-label-md text-on-surface-variant block mb-stack-sm uppercase tracking-wide">Model Router Topology</label>
|
||||
<div class="flex items-center gap-4 p-4 bg-surface-container-low rounded-lg border border-outline-variant/50">
|
||||
<div class="flex-1 flex items-center justify-center gap-3 bg-white border border-outline-variant rounded-lg p-3 shadow-sm">
|
||||
<div class="w-2 h-2 rounded-full bg-green-500 animate-pulse"></div>
|
||||
<div class="flex flex-col">
|
||||
<span class="font-label-xs text-label-xs text-outline">기본 (Primary)</span>
|
||||
<span class="font-body-sm font-bold">Claude 3.5 Sonnet</span>
|
||||
</div>
|
||||
</div>
|
||||
<span class="material-symbols-outlined text-outline">arrow_forward</span>
|
||||
<div class="flex-1 flex items-center justify-center gap-3 bg-white border border-outline-variant rounded-lg p-3 shadow-sm">
|
||||
<div class="w-2 h-2 rounded-full bg-green-500"></div>
|
||||
<div class="flex flex-col">
|
||||
<span class="font-label-xs text-label-xs text-outline">폴백 (Fallback)</span>
|
||||
<span class="font-body-sm font-bold">Ollama / Llama 3</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Inference Sliders -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
<div class="space-y-4">
|
||||
<div class="flex justify-between items-center">
|
||||
<label class="font-label-md text-label-md">Temperature</label>
|
||||
<span class="font-label-md text-primary bg-primary-container/20 px-2 py-0.5 rounded" id="temp-val">0.7</span>
|
||||
</div>
|
||||
<input class="w-full h-1.5 bg-surface-container-highest rounded-lg appearance-none cursor-pointer" max="1" min="0" oninput="document.getElementById('temp-val').innerText = this.value" step="0.1" type="range" value="0.7"/>
|
||||
<p class="text-[11px] text-outline">창의성 수치: 낮을수록 정확하고 일관된 답변을 생성합니다.</p>
|
||||
</div>
|
||||
<div class="space-y-4">
|
||||
<div class="flex justify-between items-center">
|
||||
<label class="font-label-md text-label-md">Top_K</label>
|
||||
<span class="font-label-md text-primary bg-primary-container/20 px-2 py-0.5 rounded" id="topk-val">40</span>
|
||||
</div>
|
||||
<input class="w-full h-1.5 bg-surface-container-highest rounded-lg appearance-none cursor-pointer" max="100" min="1" oninput="document.getElementById('topk-val').innerText = this.value" step="1" type="range" value="40"/>
|
||||
<p class="text-[11px] text-outline">상위 K개 토큰 샘플링 범위: 텍스트의 다양성을 조절합니다.</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- RAG Toggle -->
|
||||
<div class="pt-4 border-t border-outline-variant/30 flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="p-2 bg-secondary-container/10 rounded-lg">
|
||||
<span class="material-symbols-outlined text-secondary" style="font-variation-settings: 'FILL' 1;">verified</span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-label-md text-label-md">근거 검증 (RAG) 활성화</p>
|
||||
<p class="font-body-sm text-outline">전시 가이드라인 및 규정 데이터를 참조하여 답변의 신뢰도를 높입니다.</p>
|
||||
</div>
|
||||
</div>
|
||||
<label class="relative inline-flex items-center cursor-pointer">
|
||||
<input checked="" class="sr-only peer" type="checkbox"/>
|
||||
<div class="w-11 h-6 bg-surface-container-highest peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-secondary"></div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Right Column: Image/Visual & Security -->
|
||||
<div class="lg:col-span-4 space-y-gutter">
|
||||
<!-- Image Card -->
|
||||
<div class="bg-surface-container-lowest border border-outline-variant rounded-xl overflow-hidden">
|
||||
<div class="px-6 py-4 border-l-4 border-secondary flex items-center gap-2 bg-surface-container-low">
|
||||
<span class="material-symbols-outlined text-secondary">image</span>
|
||||
<h3 class="font-title-sm text-title-sm">이미지 (시공 예상)</h3>
|
||||
</div>
|
||||
<div class="p-6 space-y-6">
|
||||
<div>
|
||||
<label class="font-label-md text-label-md text-outline block mb-2 uppercase">Core Visual Model</label>
|
||||
<div class="p-3 border border-secondary/30 rounded-lg bg-secondary-container/5 flex items-center gap-3">
|
||||
<span class="material-symbols-outlined text-secondary" style="font-variation-settings: 'FILL' 1;">auto_awesome</span>
|
||||
<div>
|
||||
<p class="font-body-sm font-bold text-on-surface">나노바나나</p>
|
||||
<p class="text-[11px] text-secondary">gemini-3.1-flash-image-preview</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="font-label-md text-label-md block mb-1.5">생성 쿼터 (Monthly)</label>
|
||||
<div class="relative">
|
||||
<input class="w-full border border-outline-variant rounded-lg px-3 py-2 text-body-sm focus:ring-primary focus:border-primary" type="text" value="5,000"/>
|
||||
<span class="absolute right-3 top-2.5 text-outline text-[11px]">건 / 월</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="font-label-md text-label-md block mb-1.5">비용 상한 (Limit)</label>
|
||||
<div class="relative">
|
||||
<input class="w-full border border-outline-variant rounded-lg px-3 py-2 text-body-sm focus:ring-primary focus:border-primary" type="text" value="1,200,000"/>
|
||||
<span class="absolute right-3 top-2.5 text-outline text-[11px]">KRW</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Security Info Box -->
|
||||
<div class="bg-surface-container-highest/30 border border-dashed border-outline-variant rounded-xl p-6 flex flex-col items-center text-center gap-3">
|
||||
<span class="material-symbols-outlined text-outline text-3xl">key_off</span>
|
||||
<div>
|
||||
<p class="font-label-md text-label-md text-on-surface">보안 정보</p>
|
||||
<p class="font-body-sm text-on-surface-variant leading-relaxed">
|
||||
API 키는 서버 환경 변수(env)로 안전하게 관리되며 보안상 화면에 표시되지 않습니다. 수정을 원하시면 인프라 관리자에게 문의하십시오.
|
||||
</p>
|
||||
</div>
|
||||
<button class="text-[11px] text-primary font-bold hover:underline flex items-center gap-1 mt-2">
|
||||
환경 변수 문서 확인 <span class="material-symbols-outlined text-sm">open_in_new</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Bottom Actions -->
|
||||
<div class="flex justify-end gap-3 pt-4">
|
||||
<button class="px-6 py-2 border border-outline text-on-surface-variant font-label-md rounded-lg hover:bg-surface-container-high transition-colors">
|
||||
설정 초기화
|
||||
</button>
|
||||
<button class="px-8 py-2 bg-primary text-white font-label-md rounded-lg hover:opacity-90 shadow-md transition-all active:scale-95">
|
||||
변경 사항 저장
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Footer -->
|
||||
<footer class="mt-auto p-gutter border-t border-outline-variant bg-surface-container-lowest flex justify-between items-center text-outline text-[11px]">
|
||||
<div>© 2024 KINTEX AI Exhibition Management. All rights reserved.</div>
|
||||
<div class="flex gap-4">
|
||||
<a class="hover:text-primary" href="#">Service Status</a>
|
||||
<a class="hover:text-primary" href="#">API Documentation</a>
|
||||
<a class="hover:text-primary" href="#">Privacy Policy</a>
|
||||
</div>
|
||||
</footer>
|
||||
</main>
|
||||
<script>
|
||||
// Micro-interaction for toggle switches or buttons could go here
|
||||
document.querySelectorAll('input[type="range"]').forEach(slider => {
|
||||
slider.addEventListener('mousedown', () => slider.classList.add('opacity-80'));
|
||||
slider.addEventListener('mouseup', () => slider.classList.remove('opacity-80'));
|
||||
});
|
||||
</script>
|
||||
</body></html>
|
||||
423
stitch_kintex_ai_system_architect/scr_a2/code.html
Normal file
423
stitch_kintex_ai_system_architect/scr_a2/code.html
Normal file
@ -0,0 +1,423 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html class="light" lang="ko"><head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||
<title>KINTEX AI Manager - RBAC</title>
|
||||
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Geist:wght@400;500;600&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<script id="tailwind-config">
|
||||
tailwind.config = {
|
||||
darkMode: "class",
|
||||
theme: {
|
||||
extend: {
|
||||
"colors": {
|
||||
"outline-variant": "#c1c7d3",
|
||||
"on-secondary": "#ffffff",
|
||||
"secondary": "#5427e6",
|
||||
"tertiary-container": "#00734e",
|
||||
"secondary-container": "#6d4aff",
|
||||
"inverse-primary": "#a2c9ff",
|
||||
"on-primary-fixed-variant": "#004881",
|
||||
"surface": "#f9f9ff",
|
||||
"primary-fixed": "#d3e4ff",
|
||||
"on-primary-fixed": "#001c38",
|
||||
"on-background": "#141b2c",
|
||||
"tertiary-fixed-dim": "#72daa9",
|
||||
"tertiary-fixed": "#8ef7c4",
|
||||
"surface-container-high": "#e0e8ff",
|
||||
"background": "#f9f9ff",
|
||||
"on-error": "#ffffff",
|
||||
"surface-variant": "#dbe2f9",
|
||||
"inverse-surface": "#293041",
|
||||
"surface-container-highest": "#dbe2f9",
|
||||
"secondary-fixed": "#e5deff",
|
||||
"on-error-container": "#93000a",
|
||||
"surface-container-lowest": "#ffffff",
|
||||
"on-surface": "#141b2c",
|
||||
"primary-fixed-dim": "#a2c9ff",
|
||||
"on-tertiary-fixed-variant": "#005236",
|
||||
"surface-bright": "#f9f9ff",
|
||||
"surface-dim": "#d2daf0",
|
||||
"inverse-on-surface": "#edf0ff",
|
||||
"error-container": "#ffdad6",
|
||||
"error": "#ba1a1a",
|
||||
"tertiary": "#00583b",
|
||||
"surface-tint": "#0060a9",
|
||||
"surface-container-low": "#f1f3ff",
|
||||
"outline": "#717782",
|
||||
"secondary-fixed-dim": "#c9bfff",
|
||||
"on-tertiary": "#ffffff",
|
||||
"surface-container": "#e9edff",
|
||||
"on-surface-variant": "#414751",
|
||||
"on-tertiary-fixed": "#002113",
|
||||
"primary": "#004e8b",
|
||||
"on-secondary-fixed": "#1b0063",
|
||||
"on-primary": "#ffffff",
|
||||
"primary-container": "#0066b3",
|
||||
"on-tertiary-container": "#8ef7c3",
|
||||
"on-secondary-container": "#f3edff",
|
||||
"on-primary-container": "#d2e3ff",
|
||||
"on-secondary-fixed-variant": "#4500d8"
|
||||
},
|
||||
"borderRadius": {
|
||||
"DEFAULT": "0.125rem",
|
||||
"lg": "0.25rem",
|
||||
"xl": "0.5rem",
|
||||
"full": "0.75rem"
|
||||
},
|
||||
"spacing": {
|
||||
"container-padding": "24px",
|
||||
"base": "4px",
|
||||
"gutter": "16px",
|
||||
"stack-lg": "24px",
|
||||
"stack-md": "16px",
|
||||
"stack-sm": "8px"
|
||||
},
|
||||
"fontFamily": {
|
||||
"label-xs": ["Geist"],
|
||||
"title-sm": ["Inter"],
|
||||
"label-md": ["Geist"],
|
||||
"display-lg": ["Inter"],
|
||||
"headline-md": ["Inter"],
|
||||
"body-sm": ["Inter"],
|
||||
"body-base": ["Inter"]
|
||||
},
|
||||
"fontSize": {
|
||||
"label-xs": ["11px", {"lineHeight": "14px", "fontWeight": "500"}],
|
||||
"title-sm": ["18px", {"lineHeight": "28px", "fontWeight": "600"}],
|
||||
"label-md": ["12px", {"lineHeight": "16px", "letterSpacing": "0.02em", "fontWeight": "600"}],
|
||||
"display-lg": ["36px", {"lineHeight": "44px", "letterSpacing": "-0.02em", "fontWeight": "700"}],
|
||||
"headline-md": ["24px", {"lineHeight": "32px", "fontWeight": "600"}],
|
||||
"body-sm": ["13px", {"lineHeight": "18px", "fontWeight": "400"}],
|
||||
"body-base": ["14px", {"lineHeight": "20px", "fontWeight": "400"}]
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
body { font-family: 'Inter', sans-serif; background-color: #f9f9ff; }
|
||||
.material-symbols-outlined { font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24; }
|
||||
.tree-node-active { @apply bg-secondary-container text-on-secondary-container font-semibold; }
|
||||
.permission-checkbox:checked { background-color: #0066b3; border-color: #0066b3; }
|
||||
::-webkit-scrollbar { width: 6px; }
|
||||
::-webkit-scrollbar-track { background: transparent; }
|
||||
::-webkit-scrollbar-thumb { background: #c1c7d3; border-radius: 10px; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="overflow-hidden">
|
||||
<!-- SideNavBar (Shared Component) -->
|
||||
<aside class="h-full w-64 fixed left-0 top-0 bg-surface-container-low dark:bg-inverse-surface border-r border-outline-variant dark:border-outline flex flex-col py-6 px-4 z-50">
|
||||
<div class="mb-10 px-2">
|
||||
<h1 class="font-headline-md text-headline-md font-bold text-primary dark:text-inverse-primary leading-tight">KINTEX Admin</h1>
|
||||
<p class="text-[10px] uppercase tracking-widest text-outline mt-1 font-bold">AI Exhibition Management</p>
|
||||
</div>
|
||||
<nav class="flex-grow space-y-1">
|
||||
<a class="flex items-center gap-3 px-4 py-3 text-on-surface-variant hover:bg-surface-container-high transition-colors rounded-lg font-label-md text-label-md" href="#">
|
||||
<span class="material-symbols-outlined">dashboard</span>
|
||||
Dashboard
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-4 py-3 text-on-surface-variant hover:bg-surface-container-high transition-colors rounded-lg font-label-md text-label-md" href="#">
|
||||
<span class="material-symbols-outlined">group</span>
|
||||
User Management
|
||||
</a>
|
||||
<!-- Active Tab: Role Management -->
|
||||
<a class="flex items-center gap-3 px-4 py-3 bg-secondary-container text-on-secondary-container rounded-lg font-label-md text-label-md" href="#">
|
||||
<span class="material-symbols-outlined">rule</span>
|
||||
Role Management
|
||||
</a>
|
||||
</nav>
|
||||
<div class="mt-auto border-t border-outline-variant pt-4 space-y-1">
|
||||
<a class="flex items-center gap-3 px-4 py-3 text-on-surface-variant hover:bg-surface-container-high transition-colors rounded-lg font-label-md text-label-md" href="#">
|
||||
<span class="material-symbols-outlined">settings</span>
|
||||
Settings
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-4 py-3 text-on-surface-variant hover:bg-surface-container-high transition-colors rounded-lg font-label-md text-label-md" href="#">
|
||||
<span class="material-symbols-outlined">help</span>
|
||||
Support
|
||||
</a>
|
||||
</div>
|
||||
</aside>
|
||||
<!-- TopAppBar (Shared Component) -->
|
||||
<header class="fixed top-0 right-0 w-[calc(100%-16rem)] h-16 bg-surface dark:bg-background border-b border-outline-variant dark:border-outline flex justify-between items-center px-8 z-40">
|
||||
<div class="flex items-center gap-4">
|
||||
<span class="font-title-sm text-title-sm font-bold text-on-surface dark:text-inverse-on-surface">KINTEX AI Manager</span>
|
||||
<div class="h-4 w-px bg-outline-variant mx-2"></div>
|
||||
<span class="text-on-surface-variant font-body-base text-body-base">역할 및 권한 관리 (RBAC)</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-6">
|
||||
<div class="flex items-center gap-4">
|
||||
<button class="material-symbols-outlined text-on-surface-variant hover:text-primary transition-colors cursor-pointer">notifications</button>
|
||||
<button class="material-symbols-outlined text-on-surface-variant hover:text-primary transition-colors cursor-pointer">help</button>
|
||||
</div>
|
||||
<div class="flex items-center gap-3 pl-4 border-l border-outline-variant cursor-pointer group">
|
||||
<div class="text-right">
|
||||
<p class="text-label-md font-label-md text-on-surface leading-tight">Admin User</p>
|
||||
<p class="text-[10px] text-outline">System Manager</p>
|
||||
</div>
|
||||
<div class="w-8 h-8 rounded-full bg-primary-container flex items-center justify-center text-on-primary-container">
|
||||
<span class="material-symbols-outlined text-sm">person</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<!-- Main Content Area -->
|
||||
<main class="ml-64 pt-16 h-screen bg-background flex flex-col">
|
||||
<div class="p-8 flex-grow flex gap-8 overflow-hidden">
|
||||
<!-- Left Column: Role Tree (1/3) -->
|
||||
<section class="w-1/3 flex flex-col bg-surface-container-lowest border border-outline-variant rounded-xl shadow-sm overflow-hidden">
|
||||
<div class="px-6 py-4 border-b border-outline-variant bg-surface flex justify-between items-center">
|
||||
<h2 class="font-title-sm text-on-surface flex items-center gap-2">
|
||||
<span class="material-symbols-outlined text-primary">account_tree</span>
|
||||
역할 그룹 계층
|
||||
</h2>
|
||||
<button class="text-primary hover:bg-primary-fixed p-1 rounded-full transition-colors">
|
||||
<span class="material-symbols-outlined">add</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex-grow overflow-y-auto p-4 space-y-4">
|
||||
<!-- Platform Level -->
|
||||
<div>
|
||||
<div class="flex items-center gap-2 px-2 py-1 text-label-xs font-bold text-outline uppercase tracking-wider mb-2">
|
||||
<span class="material-symbols-outlined text-sm">hub</span> 플랫폼 (Platform)
|
||||
</div>
|
||||
<div class="ml-2 space-y-1">
|
||||
<div class="flex items-center gap-3 px-3 py-2 text-on-surface-variant hover:bg-surface-container rounded-lg cursor-pointer transition-colors font-body-base">
|
||||
<span class="material-symbols-outlined text-sm">shield_person</span> 플랫폼 슈퍼관리자
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Tenant Level -->
|
||||
<div>
|
||||
<div class="flex items-center gap-2 px-2 py-1 text-label-xs font-bold text-outline uppercase tracking-wider mb-2">
|
||||
<span class="material-symbols-outlined text-sm">corporate_fare</span> 테넌트 (Tenant)
|
||||
</div>
|
||||
<div class="ml-2 space-y-1">
|
||||
<div class="flex items-center gap-3 px-3 py-2 bg-primary-container/10 text-primary border border-primary/20 rounded-lg cursor-pointer transition-colors font-body-base font-semibold">
|
||||
<span class="material-symbols-outlined text-sm" style="font-variation-settings: 'FILL' 1;">admin_panel_settings</span> 테넌트 관리자
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Event Level -->
|
||||
<div>
|
||||
<div class="flex items-center gap-2 px-2 py-1 text-label-xs font-bold text-outline uppercase tracking-wider mb-2">
|
||||
<span class="material-symbols-outlined text-sm">event_available</span> 행사 (Event)
|
||||
</div>
|
||||
<div class="ml-2 space-y-1">
|
||||
<div class="flex items-center gap-3 px-3 py-2 text-on-surface-variant hover:bg-surface-container rounded-lg cursor-pointer transition-colors font-body-base">
|
||||
<span class="material-symbols-outlined text-sm">campaign</span> 주최자 (Organizer)
|
||||
</div>
|
||||
<div class="flex items-center gap-3 px-3 py-2 text-on-surface-variant hover:bg-surface-container rounded-lg cursor-pointer transition-colors font-body-base">
|
||||
<span class="material-symbols-outlined text-sm">storefront</span> 참가업체 (Exhibitor)
|
||||
</div>
|
||||
<div class="flex items-center gap-3 px-3 py-2 text-on-surface-variant hover:bg-surface-container rounded-lg cursor-pointer transition-colors font-body-base">
|
||||
<span class="material-symbols-outlined text-sm">build</span> 장치업체 (Contractor)
|
||||
</div>
|
||||
<div class="flex items-center gap-3 px-3 py-2 text-on-surface-variant hover:bg-surface-container rounded-lg cursor-pointer transition-colors font-body-base">
|
||||
<span class="material-symbols-outlined text-sm">meeting_room</span> 홀매니저 (Hall Manager)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-4 bg-surface-container-low border-t border-outline-variant">
|
||||
<div class="flex items-center justify-center p-6 border-2 border-dashed border-outline-variant rounded-lg group cursor-pointer hover:border-primary hover:bg-white transition-all">
|
||||
<div class="text-center">
|
||||
<span class="material-symbols-outlined text-outline group-hover:text-primary mb-2">group_add</span>
|
||||
<p class="text-label-md text-outline group-hover:text-on-surface">새 역할 그룹 생성</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<!-- Right Column: Permission Matrix (2/3) -->
|
||||
<section class="w-2/3 flex flex-col bg-surface-container-lowest border border-outline-variant rounded-xl shadow-sm overflow-hidden">
|
||||
<div class="px-6 py-4 border-b border-outline-variant bg-white flex justify-between items-center">
|
||||
<div class="flex items-center gap-4">
|
||||
<h2 class="font-title-sm text-on-surface">권한 상세 설정</h2>
|
||||
<span class="px-3 py-1 bg-primary text-on-primary text-label-xs rounded-full">테넌트 관리자</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<button class="px-4 py-2 text-primary font-label-md border border-primary rounded-lg hover:bg-primary-fixed transition-colors">초기화</button>
|
||||
<button class="px-6 py-2 bg-primary text-on-primary font-label-md rounded-lg shadow-md hover:bg-primary-container active:scale-95 transition-all">저장 (Save)</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-grow overflow-auto p-0">
|
||||
<table class="w-full text-left border-collapse">
|
||||
<thead class="sticky top-0 bg-surface z-10">
|
||||
<tr class="border-b border-outline-variant">
|
||||
<th class="p-6 font-label-md text-outline">메뉴 / 기능 카테고리</th>
|
||||
<th class="p-6 font-label-md text-outline text-center">조회 (Read)</th>
|
||||
<th class="p-6 font-label-md text-outline text-center">생성 (Create)</th>
|
||||
<th class="p-6 font-label-md text-outline text-center">수정 (Update)</th>
|
||||
<th class="p-6 font-label-md text-outline text-center">삭제 (Delete)</th>
|
||||
<th class="p-6 font-label-md text-outline text-center">승인 (Approve)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-outline-variant/30">
|
||||
<!-- Dashboard -->
|
||||
<tr class="hover:bg-surface-container-low transition-colors group">
|
||||
<td class="p-6">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-8 h-8 rounded bg-secondary-fixed/30 flex items-center justify-center text-secondary">
|
||||
<span class="material-symbols-outlined text-sm">dashboard</span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-body-base font-semibold text-on-surface">대시보드</p>
|
||||
<p class="text-label-xs text-outline">전체 운영 현황 모니터링</p>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="p-6 text-center"><input checked="" class="w-5 h-5 rounded border-outline-variant text-primary focus:ring-primary" type="checkbox"/></td>
|
||||
<td class="p-6 text-center"><input class="w-5 h-5 rounded border-outline-variant text-primary focus:ring-primary" type="checkbox"/></td>
|
||||
<td class="p-6 text-center"><input class="w-5 h-5 rounded border-outline-variant text-primary focus:ring-primary" type="checkbox"/></td>
|
||||
<td class="p-6 text-center"><input class="w-5 h-5 rounded border-outline-variant text-primary focus:ring-primary" type="checkbox"/></td>
|
||||
<td class="p-6 text-center"><input class="w-5 h-5 rounded border-outline-variant text-primary focus:ring-primary" type="checkbox"/></td>
|
||||
</tr>
|
||||
<!-- Exhibition Schedule -->
|
||||
<tr class="hover:bg-surface-container-low transition-colors group">
|
||||
<td class="p-6">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-8 h-8 rounded bg-primary-fixed/30 flex items-center justify-center text-primary">
|
||||
<span class="material-symbols-outlined text-sm">calendar_month</span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-body-base font-semibold text-on-surface">전시회 일정</p>
|
||||
<p class="text-label-xs text-outline">연간 전시 스케줄 관리</p>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="p-6 text-center"><input checked="" class="w-5 h-5 rounded border-outline-variant text-primary focus:ring-primary" type="checkbox"/></td>
|
||||
<td class="p-6 text-center"><input checked="" class="w-5 h-5 rounded border-outline-variant text-primary focus:ring-primary" type="checkbox"/></td>
|
||||
<td class="p-6 text-center"><input checked="" class="w-5 h-5 rounded border-outline-variant text-primary focus:ring-primary" type="checkbox"/></td>
|
||||
<td class="p-6 text-center"><input class="w-5 h-5 rounded border-outline-variant text-primary focus:ring-primary" type="checkbox"/></td>
|
||||
<td class="p-6 text-center"><input class="w-5 h-5 rounded border-outline-variant text-primary focus:ring-primary" type="checkbox"/></td>
|
||||
</tr>
|
||||
<!-- Booth Layout Editor -->
|
||||
<tr class="hover:bg-surface-container-low transition-colors group border-l-4 border-secondary-container">
|
||||
<td class="p-6">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-8 h-8 rounded bg-tertiary-fixed/30 flex items-center justify-center text-tertiary">
|
||||
<span class="material-symbols-outlined text-sm">grid_view</span>
|
||||
</div>
|
||||
<div>
|
||||
<div class="flex items-center gap-1">
|
||||
<p class="font-body-base font-semibold text-on-surface">부스 배치 에디터</p>
|
||||
<span class="material-symbols-outlined text-[12px] text-secondary-container" style="font-variation-settings: 'FILL' 1;">auto_awesome</span>
|
||||
</div>
|
||||
<p class="text-label-xs text-outline">AI 부스 자동 배치 및 편집</p>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="p-6 text-center"><input checked="" class="w-5 h-5 rounded border-outline-variant text-primary focus:ring-primary" type="checkbox"/></td>
|
||||
<td class="p-6 text-center"><input checked="" class="w-5 h-5 rounded border-outline-variant text-primary focus:ring-primary" type="checkbox"/></td>
|
||||
<td class="p-6 text-center"><input checked="" class="w-5 h-5 rounded border-outline-variant text-primary focus:ring-primary" type="checkbox"/></td>
|
||||
<td class="p-6 text-center"><input class="w-5 h-5 rounded border-outline-variant text-primary focus:ring-primary" type="checkbox"/></td>
|
||||
<td class="p-6 text-center"><input checked="" class="w-5 h-5 rounded border-outline-variant text-primary focus:ring-primary" type="checkbox"/></td>
|
||||
</tr>
|
||||
<!-- Utility Application -->
|
||||
<tr class="hover:bg-surface-container-low transition-colors group">
|
||||
<td class="p-6">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-8 h-8 rounded bg-outline-variant/20 flex items-center justify-center text-on-surface-variant">
|
||||
<span class="material-symbols-outlined text-sm">receipt_long</span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-body-base font-semibold text-on-surface">유틸리티 신청</p>
|
||||
<p class="text-label-xs text-outline">전기, 급배수, 랜선 등 신청</p>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="p-6 text-center"><input checked="" class="w-5 h-5 rounded border-outline-variant text-primary focus:ring-primary" type="checkbox"/></td>
|
||||
<td class="p-6 text-center"><input class="w-5 h-5 rounded border-outline-variant text-primary focus:ring-primary" type="checkbox"/></td>
|
||||
<td class="p-6 text-center"><input class="w-5 h-5 rounded border-outline-variant text-primary focus:ring-primary" type="checkbox"/></td>
|
||||
<td class="p-6 text-center"><input class="w-5 h-5 rounded border-outline-variant text-primary focus:ring-primary" type="checkbox"/></td>
|
||||
<td class="p-6 text-center"><input checked="" class="w-5 h-5 rounded border-outline-variant text-primary focus:ring-primary" type="checkbox"/></td>
|
||||
</tr>
|
||||
<!-- Settlement -->
|
||||
<tr class="hover:bg-surface-container-low transition-colors group">
|
||||
<td class="p-6">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-8 h-8 rounded bg-tertiary-container/10 flex items-center justify-center text-tertiary-container">
|
||||
<span class="material-symbols-outlined text-sm">account_balance_wallet</span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-body-base font-semibold text-on-surface">결제/정산</p>
|
||||
<p class="text-label-xs text-outline">대금 청구 및 정산 처리</p>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="p-6 text-center"><input checked="" class="w-5 h-5 rounded border-outline-variant text-primary focus:ring-primary" type="checkbox"/></td>
|
||||
<td class="p-6 text-center"><input class="w-5 h-5 rounded border-outline-variant text-primary focus:ring-primary" type="checkbox"/></td>
|
||||
<td class="p-6 text-center"><input checked="" class="w-5 h-5 rounded border-outline-variant text-primary focus:ring-primary" type="checkbox"/></td>
|
||||
<td class="p-6 text-center"><input class="w-5 h-5 rounded border-outline-variant text-primary focus:ring-primary" type="checkbox"/></td>
|
||||
<td class="p-6 text-center"><input checked="" class="w-5 h-5 rounded border-outline-variant text-primary focus:ring-primary" type="checkbox"/></td>
|
||||
</tr>
|
||||
<!-- User Management -->
|
||||
<tr class="hover:bg-surface-container-low transition-colors group">
|
||||
<td class="p-6">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-8 h-8 rounded bg-primary/10 flex items-center justify-center text-primary">
|
||||
<span class="material-symbols-outlined text-sm">manage_accounts</span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-body-base font-semibold text-on-surface">사용자 관리</p>
|
||||
<p class="text-label-xs text-outline">계정 생성 및 권한 위임</p>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="p-6 text-center"><input checked="" class="w-5 h-5 rounded border-outline-variant text-primary focus:ring-primary" type="checkbox"/></td>
|
||||
<td class="p-6 text-center"><input checked="" class="w-5 h-5 rounded border-outline-variant text-primary focus:ring-primary" type="checkbox"/></td>
|
||||
<td class="p-6 text-center"><input checked="" class="w-5 h-5 rounded border-outline-variant text-primary focus:ring-primary" type="checkbox"/></td>
|
||||
<td class="p-6 text-center"><input checked="" class="w-5 h-5 rounded border-outline-variant text-primary focus:ring-primary" type="checkbox"/></td>
|
||||
<td class="p-6 text-center"><input class="w-5 h-5 rounded border-outline-variant text-primary focus:ring-primary" type="checkbox"/></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="p-4 bg-surface border-t border-outline-variant flex justify-between items-center">
|
||||
<div class="flex items-center gap-2 text-label-xs text-outline">
|
||||
<span class="material-symbols-outlined text-sm">info</span>
|
||||
선택된 권한은 실시간으로 저장되지 않습니다. 상단의 저장 버튼을 눌러주세요.
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="flex -space-x-2">
|
||||
<div class="w-8 h-8 rounded-full border-2 border-white bg-secondary-fixed flex items-center justify-center text-[10px] font-bold">JD</div>
|
||||
<div class="w-8 h-8 rounded-full border-2 border-white bg-primary-fixed flex items-center justify-center text-[10px] font-bold">MK</div>
|
||||
<div class="w-8 h-8 rounded-full border-2 border-white bg-tertiary-fixed flex items-center justify-center text-[10px] font-bold">+5</div>
|
||||
</div>
|
||||
<p class="text-label-xs text-outline">현재 7명의 사용자가 이 역할을 가지고 있습니다.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
<!-- Micro-interaction Script -->
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const rows = document.querySelectorAll('tbody tr');
|
||||
rows.forEach(row => {
|
||||
row.addEventListener('click', (e) => {
|
||||
if (e.target.type !== 'checkbox') {
|
||||
const cb = row.querySelector('input[type="checkbox"]');
|
||||
// Optional: trigger some visual feedback or toggle primary checkbox
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Role selection mock behavior
|
||||
const roleItems = document.querySelectorAll('.ml-2 > div');
|
||||
roleItems.forEach(item => {
|
||||
item.addEventListener('click', () => {
|
||||
roleItems.forEach(i => {
|
||||
i.classList.remove('bg-primary-container/10', 'text-primary', 'border', 'border-primary/20', 'font-semibold');
|
||||
i.classList.add('text-on-surface-variant', 'hover:bg-surface-container');
|
||||
});
|
||||
item.classList.add('bg-primary-container/10', 'text-primary', 'border', 'border-primary/20', 'font-semibold');
|
||||
item.classList.remove('text-on-surface-variant', 'hover:bg-surface-container');
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body></html>
|
||||
418
stitch_kintex_ai_system_architect/scr_a3/code.html
Normal file
418
stitch_kintex_ai_system_architect/scr_a3/code.html
Normal file
@ -0,0 +1,418 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html class="light" lang="ko"><head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||
<title>KINTEX AI - 공통코드 관리</title>
|
||||
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Geist:wght@400;500;600&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<script id="tailwind-config">
|
||||
tailwind.config = {
|
||||
darkMode: "class",
|
||||
theme: {
|
||||
extend: {
|
||||
"colors": {
|
||||
"surface-bright": "#f9f9ff",
|
||||
"on-error-container": "#93000a",
|
||||
"secondary-fixed": "#e5deff",
|
||||
"secondary": "#5427e6",
|
||||
"on-primary-fixed": "#001c38",
|
||||
"surface-variant": "#dbe2f9",
|
||||
"tertiary-fixed-dim": "#72daa9",
|
||||
"secondary-container": "#6d4aff",
|
||||
"error-container": "#ffdad6",
|
||||
"on-primary-container": "#d2e3ff",
|
||||
"on-secondary-fixed": "#1b0063",
|
||||
"surface-dim": "#d2daf0",
|
||||
"inverse-surface": "#293041",
|
||||
"secondary-fixed-dim": "#c9bfff",
|
||||
"primary-fixed": "#d3e4ff",
|
||||
"on-surface-variant": "#414751",
|
||||
"on-tertiary-fixed-variant": "#005236",
|
||||
"on-tertiary-fixed": "#002113",
|
||||
"inverse-on-surface": "#edf0ff",
|
||||
"error": "#ba1a1a",
|
||||
"on-tertiary": "#ffffff",
|
||||
"primary-container": "#0066b3",
|
||||
"tertiary-container": "#00734e",
|
||||
"on-primary": "#ffffff",
|
||||
"on-error": "#ffffff",
|
||||
"background": "#f9f9ff",
|
||||
"surface": "#f9f9ff",
|
||||
"on-surface": "#141b2c",
|
||||
"on-secondary-container": "#f3edff",
|
||||
"tertiary": "#00583b",
|
||||
"on-tertiary-container": "#8ef7c3",
|
||||
"on-secondary": "#ffffff",
|
||||
"on-background": "#141b2c",
|
||||
"on-primary-fixed-variant": "#004881",
|
||||
"on-secondary-fixed-variant": "#4500d8",
|
||||
"surface-container-lowest": "#ffffff",
|
||||
"inverse-primary": "#a2c9ff",
|
||||
"surface-container": "#e9edff",
|
||||
"primary": "#004e8b",
|
||||
"surface-container-high": "#e0e8ff",
|
||||
"surface-tint": "#0060a9",
|
||||
"tertiary-fixed": "#8ef7c4",
|
||||
"primary-fixed-dim": "#a2c9ff",
|
||||
"outline-variant": "#c1c7d3",
|
||||
"outline": "#717782",
|
||||
"surface-container-highest": "#dbe2f9",
|
||||
"surface-container-low": "#f1f3ff"
|
||||
},
|
||||
"borderRadius": {
|
||||
"DEFAULT": "0.125rem",
|
||||
"lg": "0.25rem",
|
||||
"xl": "0.5rem",
|
||||
"full": "0.75rem"
|
||||
},
|
||||
"spacing": {
|
||||
"stack-sm": "8px",
|
||||
"stack-lg": "24px",
|
||||
"gutter": "16px",
|
||||
"stack-md": "16px",
|
||||
"container-padding": "24px",
|
||||
"base": "4px"
|
||||
},
|
||||
"fontFamily": {
|
||||
"title-sm": ["Inter"],
|
||||
"headline-md": ["Inter"],
|
||||
"body-base": ["Inter"],
|
||||
"label-md": ["Geist"],
|
||||
"body-sm": ["Inter"]
|
||||
},
|
||||
"fontSize": {
|
||||
"title-sm": ["18px", {"lineHeight": "28px", "fontWeight": "600"}],
|
||||
"headline-md": ["24px", {"lineHeight": "32px", "fontWeight": "600"}],
|
||||
"body-base": ["14px", {"lineHeight": "20px", "fontWeight": "400"}],
|
||||
"label-md": ["12px", {"lineHeight": "16px", "letterSpacing": "0.02em", "fontWeight": "600"}],
|
||||
"body-sm": ["13px", {"lineHeight": "18px", "fontWeight": "400"}]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
body { font-family: 'Inter', sans-serif; background-color: #f9f9ff; }
|
||||
.material-symbols-outlined { font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24; }
|
||||
.custom-scrollbar::-webkit-scrollbar { width: 6px; }
|
||||
.custom-scrollbar::-webkit-scrollbar-track { background: transparent; }
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb { background: #c1c7d3; border-radius: 10px; }
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb:hover { background: #717782; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="text-on-surface">
|
||||
<!-- SideNavBar Shell -->
|
||||
<aside class="h-screen w-64 flex flex-col fixed left-0 top-0 border-r border-outline-variant bg-surface-container-lowest dark:bg-inverse-surface z-50">
|
||||
<div class="flex flex-col h-full py-container-padding">
|
||||
<!-- Brand Logo -->
|
||||
<div class="px-6 mb-8">
|
||||
<img alt="KINTEX Logo" class="h-8 object-contain" src="https://lh3.googleusercontent.com/aida-public/AB6AXuBW8F6aqXODXXAWGceGi4BlA03fKDeuh-zDzUssvq_YlwatuDYBBDwF8xiiegBKuWPzVQPjj4sw82vmTc4fJ6jLiAtktpLgUW2G4aVe9pOU6XfLN2FYRuRSCRDv-57JazvkrAAdyK6XnD2tYqnNmKe1_JU8eX5dPVfDNfDwimD4tcVQSfFRFjc_brUL6pz17VgN3HUmv99ZAgAWFW0_mcBcuPiWc7mZ7Tq6N-z_NvFVrxR6L2nC6aSzV9AGSWJXF1sgSIqffPBGQuM"/>
|
||||
<div class="mt-2">
|
||||
<p class="font-headline-md text-headline-md font-bold text-primary dark:text-primary-fixed">KINTEX AI</p>
|
||||
<p class="font-body-sm text-body-sm text-on-surface-variant">Exhibition Management</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Navigation Links -->
|
||||
<nav class="flex-1 px-3 space-y-1 overflow-y-auto custom-scrollbar">
|
||||
<a class="flex items-center gap-3 px-4 py-3 text-on-surface-variant hover:bg-surface-container-high transition-colors rounded-lg" href="#">
|
||||
<span class="material-symbols-outlined">dashboard</span>
|
||||
<span class="font-label-md text-label-md">Dashboard</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-4 py-3 text-on-surface-variant hover:bg-surface-container-high transition-colors rounded-lg" href="#">
|
||||
<span class="material-symbols-outlined">group</span>
|
||||
<span class="font-label-md text-label-md">User Management</span>
|
||||
</a>
|
||||
<!-- ACTIVE TAB: Common Code Management (공통코드 관리) -->
|
||||
<a class="flex items-center gap-3 px-4 py-3 bg-secondary-container text-on-secondary-container font-semibold rounded-lg scale-[0.99] transition-transform duration-150 shadow-sm" href="#">
|
||||
<span class="material-symbols-outlined">settings_ethernet</span>
|
||||
<span class="font-label-md text-label-md">Common Code Management</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-4 py-3 text-on-surface-variant hover:bg-surface-container-high transition-colors rounded-lg" href="#">
|
||||
<span class="material-symbols-outlined">grid_view</span>
|
||||
<span class="font-label-md text-label-md">Exhibition Layout</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-4 py-3 text-on-surface-variant hover:bg-surface-container-high transition-colors rounded-lg" href="#">
|
||||
<span class="material-symbols-outlined">settings</span>
|
||||
<span class="font-label-md text-label-md">Settings</span>
|
||||
</a>
|
||||
<div class="pt-6 mt-6 border-t border-outline-variant px-4">
|
||||
<button class="w-full bg-primary text-on-primary py-2.5 rounded-lg font-label-md text-label-md flex items-center justify-center gap-2 hover:bg-primary-container transition-all">
|
||||
<span class="material-symbols-outlined text-[20px]">add</span>
|
||||
New Exhibition
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
<!-- Footer Navigation -->
|
||||
<div class="px-3 mt-auto space-y-1">
|
||||
<a class="flex items-center gap-3 px-4 py-3 text-on-surface-variant hover:bg-surface-container-high transition-colors rounded-lg" href="#">
|
||||
<span class="material-symbols-outlined">help</span>
|
||||
<span class="font-label-md text-label-md">Help Center</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-4 py-3 text-on-surface-variant hover:bg-surface-container-high transition-colors rounded-lg" href="#">
|
||||
<span class="material-symbols-outlined">logout</span>
|
||||
<span class="font-label-md text-label-md">Logout</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
<!-- Main Content Area -->
|
||||
<main class="ml-64 min-h-screen flex flex-col">
|
||||
<!-- TopNavBar Shell -->
|
||||
<header class="h-16 w-full sticky top-0 z-40 bg-surface dark:bg-surface-dim flex items-center justify-between px-gutter border-b border-outline-variant">
|
||||
<div class="flex items-center gap-4">
|
||||
<nav class="flex items-center text-on-surface-variant font-label-md text-label-md">
|
||||
<span class="hover:text-primary transition-colors cursor-pointer">시스템 설정</span>
|
||||
<span class="material-symbols-outlined mx-1 text-[16px]">chevron_right</span>
|
||||
<span class="text-primary font-bold">공통코드 관리</span>
|
||||
</nav>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="relative group hidden md:block">
|
||||
<span class="absolute inset-y-0 left-0 pl-3 flex items-center text-outline">
|
||||
<span class="material-symbols-outlined text-[20px]">search</span>
|
||||
</span>
|
||||
<input class="block w-64 bg-surface-container-low border-none rounded-full py-1.5 pl-10 pr-3 font-body-sm text-body-sm focus:ring-2 focus:ring-primary focus:bg-white transition-all" placeholder="코드 또는 그룹 검색..." type="text"/>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<button class="p-2 text-on-surface-variant hover:bg-surface-container-high rounded-full transition-colors relative">
|
||||
<span class="material-symbols-outlined">notifications</span>
|
||||
<span class="absolute top-2 right-2 w-2 h-2 bg-error rounded-full border-2 border-surface"></span>
|
||||
</button>
|
||||
<button class="p-2 text-on-surface-variant hover:bg-surface-container-high rounded-full transition-colors">
|
||||
<span class="material-symbols-outlined">help</span>
|
||||
</button>
|
||||
<div class="h-8 w-[1px] bg-outline-variant mx-2"></div>
|
||||
<div class="flex items-center gap-3 cursor-pointer group">
|
||||
<div class="text-right">
|
||||
<p class="font-label-md text-label-md text-on-surface group-hover:text-primary transition-colors">Admin Profile</p>
|
||||
<p class="text-[10px] text-on-surface-variant">시스템 관리자</p>
|
||||
</div>
|
||||
<img class="w-8 h-8 rounded-full object-cover ring-2 ring-outline-variant group-hover:ring-primary transition-all" data-alt="A professional headshot of a Korean IT administrator in a clean office setting, soft professional lighting, modern corporate style." src="https://lh3.googleusercontent.com/aida-public/AB6AXuAioj2df8bJd5evCnXK5lVitIaaNH70IQ4Smy2IW5GdbdxcEwbD5pP-piKMLMWqtW-Fgmg76sl0gLJYYkZ3a4QX6sMv5aNTEMnHuBCExYFWC3d_vEVIf3FIBrarcc5IpXxXzROO-wOgPR2ji7KNu0tqC6HJgIkMmnVjEHGs3Ma4EnufIVHTOYJ6xQxpOJTRi-nwVnGbx49icCOXHteZxliq8OQuQ_GQITGiNER1y8VPxILfiIRm_w1A52AinpuhC1rcGBO4DOac9nc"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<!-- Content Canvas -->
|
||||
<div class="p-stack-lg flex-1 flex gap-gutter overflow-hidden">
|
||||
<!-- Left Panel: Code Groups -->
|
||||
<section class="w-1/4 flex flex-col bg-surface-container-lowest border border-outline-variant rounded-xl overflow-hidden shadow-sm">
|
||||
<div class="px-6 py-4 border-b border-outline-variant flex items-center justify-between">
|
||||
<h2 class="font-title-sm text-title-sm text-on-surface">코드 그룹 목록</h2>
|
||||
<button class="text-primary hover:bg-primary-fixed p-1 rounded-md transition-colors">
|
||||
<span class="material-symbols-outlined text-[20px]">add_box</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex-1 overflow-y-auto custom-scrollbar p-2">
|
||||
<div class="space-y-1">
|
||||
<button class="w-full flex items-center justify-between px-4 py-3 rounded-lg text-on-surface-variant hover:bg-surface-container-low transition-colors group">
|
||||
<span class="font-body-base text-body-base">전시장/홀 정보</span>
|
||||
<span class="material-symbols-outlined text-outline group-hover:text-primary transition-colors text-[18px]">chevron_right</span>
|
||||
</button>
|
||||
<button class="w-full flex items-center justify-between px-4 py-3 rounded-lg text-on-surface-variant hover:bg-surface-container-low transition-colors group">
|
||||
<span class="font-body-base text-body-base">부스 유형</span>
|
||||
<span class="material-symbols-outlined text-outline group-hover:text-primary transition-colors text-[18px]">chevron_right</span>
|
||||
</button>
|
||||
<!-- SELECTED ITEM -->
|
||||
<button class="w-full flex items-center justify-between px-4 py-3 rounded-lg bg-primary text-on-primary shadow-md">
|
||||
<span class="font-body-base text-body-base font-semibold">공종 분류 (14분류)</span>
|
||||
<span class="material-symbols-outlined text-[18px]">chevron_right</span>
|
||||
</button>
|
||||
<button class="w-full flex items-center justify-between px-4 py-3 rounded-lg text-on-surface-variant hover:bg-surface-container-low transition-colors group">
|
||||
<span class="font-body-base text-body-base">유틸리티 요금코드</span>
|
||||
<span class="material-symbols-outlined text-outline group-hover:text-primary transition-colors text-[18px]">chevron_right</span>
|
||||
</button>
|
||||
<button class="w-full flex items-center justify-between px-4 py-3 rounded-lg text-on-surface-variant hover:bg-surface-container-low transition-colors group">
|
||||
<span class="font-body-base text-body-base">행사 진행 상태</span>
|
||||
<span class="material-symbols-outlined text-outline group-hover:text-primary transition-colors text-[18px]">chevron_right</span>
|
||||
</button>
|
||||
<button class="w-full flex items-center justify-between px-4 py-3 rounded-lg text-on-surface-variant hover:bg-surface-container-low transition-colors group">
|
||||
<span class="font-body-base text-body-base">사용자 권한 등급</span>
|
||||
<span class="material-symbols-outlined text-outline group-hover:text-primary transition-colors text-[18px]">chevron_right</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<!-- Right Panel: Code Details -->
|
||||
<section class="flex-1 flex flex-col bg-surface-container-lowest border border-outline-variant rounded-xl overflow-hidden shadow-sm">
|
||||
<div class="px-6 py-4 border-b border-outline-variant flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="font-title-sm text-title-sm text-on-surface">공종 분류 상세 코드</h2>
|
||||
<p class="text-on-surface-variant font-label-md text-label-md mt-1">그룹 ID: CATEGORY_14_TYPE</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<button class="px-4 py-2 bg-primary text-on-primary rounded-lg font-label-md text-label-md flex items-center gap-1.5 hover:bg-primary-container transition-all">
|
||||
<span class="material-symbols-outlined text-[18px]">add</span>
|
||||
코드 추가
|
||||
</button>
|
||||
<button class="px-4 py-2 border border-primary text-primary rounded-lg font-label-md text-label-md hover:bg-primary-fixed transition-all">
|
||||
변경사항 저장
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1 overflow-auto">
|
||||
<table class="w-full text-left border-collapse">
|
||||
<thead>
|
||||
<tr class="bg-surface-container-low border-b border-outline-variant sticky top-0 z-10">
|
||||
<th class="px-6 py-3 font-label-md text-label-md text-on-surface-variant">코드</th>
|
||||
<th class="px-6 py-3 font-label-md text-label-md text-on-surface-variant">코드명(KR)</th>
|
||||
<th class="px-6 py-3 font-label-md text-label-md text-on-surface-variant text-center">정렬 순서</th>
|
||||
<th class="px-6 py-3 font-label-md text-label-md text-on-surface-variant">구분</th>
|
||||
<th class="px-6 py-3 font-label-md text-label-md text-on-surface-variant">사용 여부</th>
|
||||
<th class="px-6 py-3 font-label-md text-label-md text-on-surface-variant text-right">관리</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-outline-variant">
|
||||
<!-- Row 1 -->
|
||||
<tr class="hover:bg-surface-container transition-colors group">
|
||||
<td class="px-6 py-4 font-body-sm text-body-sm font-mono text-primary">C001</td>
|
||||
<td class="px-6 py-4 font-body-sm text-body-sm font-semibold">건축/목공</td>
|
||||
<td class="px-6 py-4 font-body-sm text-body-sm text-center">1</td>
|
||||
<td class="px-6 py-4">
|
||||
<span class="px-2 py-0.5 rounded-full bg-surface-container-highest text-on-surface-variant text-[10px] font-bold uppercase tracking-wider border border-outline-variant">전역</span>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-2 h-2 bg-tertiary rounded-full"></div>
|
||||
<span class="font-body-sm text-body-sm text-tertiary font-semibold">활성</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-right">
|
||||
<div class="flex items-center justify-end gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<button class="p-1.5 text-on-surface-variant hover:text-primary hover:bg-primary-fixed rounded-md transition-colors"><span class="material-symbols-outlined text-[20px]">edit</span></button>
|
||||
<button class="p-1.5 text-on-surface-variant hover:text-error hover:bg-error-container rounded-md transition-colors"><span class="material-symbols-outlined text-[20px]">delete</span></button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Row 2 -->
|
||||
<tr class="hover:bg-surface-container transition-colors group">
|
||||
<td class="px-6 py-4 font-body-sm text-body-sm font-mono text-primary">C002</td>
|
||||
<td class="px-6 py-4 font-body-sm text-body-sm font-semibold">전기/조명</td>
|
||||
<td class="px-6 py-4 font-body-sm text-body-sm text-center">2</td>
|
||||
<td class="px-6 py-4">
|
||||
<span class="px-2 py-0.5 rounded-full bg-secondary-fixed text-on-secondary-fixed text-[10px] font-bold uppercase tracking-wider border border-secondary-fixed-dim">테넌트 재정의</span>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-2 h-2 bg-tertiary rounded-full"></div>
|
||||
<span class="font-body-sm text-body-sm text-tertiary font-semibold">활성</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-right">
|
||||
<div class="flex items-center justify-end gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<button class="p-1.5 text-on-surface-variant hover:text-primary hover:bg-primary-fixed rounded-md transition-colors"><span class="material-symbols-outlined text-[20px]">edit</span></button>
|
||||
<button class="p-1.5 text-on-surface-variant hover:text-error hover:bg-error-container rounded-md transition-colors"><span class="material-symbols-outlined text-[20px]">delete</span></button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Row 3 -->
|
||||
<tr class="hover:bg-surface-container transition-colors group">
|
||||
<td class="px-6 py-4 font-body-sm text-body-sm font-mono text-primary">C003</td>
|
||||
<td class="px-6 py-4 font-body-sm text-body-sm font-semibold">가구/비품</td>
|
||||
<td class="px-6 py-4 font-body-sm text-body-sm text-center">3</td>
|
||||
<td class="px-6 py-4">
|
||||
<span class="px-2 py-0.5 rounded-full bg-surface-container-highest text-on-surface-variant text-[10px] font-bold uppercase tracking-wider border border-outline-variant">전역</span>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-2 h-2 bg-tertiary rounded-full"></div>
|
||||
<span class="font-body-sm text-body-sm text-tertiary font-semibold">활성</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-right">
|
||||
<div class="flex items-center justify-end gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<button class="p-1.5 text-on-surface-variant hover:text-primary hover:bg-primary-fixed rounded-md transition-colors"><span class="material-symbols-outlined text-[20px]">edit</span></button>
|
||||
<button class="p-1.5 text-on-surface-variant hover:text-error hover:bg-error-container rounded-md transition-colors"><span class="material-symbols-outlined text-[20px]">delete</span></button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Row 4 -->
|
||||
<tr class="hover:bg-surface-container transition-colors group">
|
||||
<td class="px-6 py-4 font-body-sm text-body-sm font-mono text-primary">C004</td>
|
||||
<td class="px-6 py-4 font-body-sm text-body-sm font-semibold text-on-surface-variant opacity-60">리깅/현수막</td>
|
||||
<td class="px-6 py-4 font-body-sm text-body-sm text-center">4</td>
|
||||
<td class="px-6 py-4">
|
||||
<span class="px-2 py-0.5 rounded-full bg-surface-container-highest text-on-surface-variant text-[10px] font-bold uppercase tracking-wider border border-outline-variant">전역</span>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-2 h-2 bg-outline rounded-full"></div>
|
||||
<span class="font-body-sm text-body-sm text-on-surface-variant font-medium">비활성</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-right">
|
||||
<div class="flex items-center justify-end gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<button class="p-1.5 text-on-surface-variant hover:text-primary hover:bg-primary-fixed rounded-md transition-colors"><span class="material-symbols-outlined text-[20px]">edit</span></button>
|
||||
<button class="p-1.5 text-on-surface-variant hover:text-error hover:bg-error-container rounded-md transition-colors"><span class="material-symbols-outlined text-[20px]">delete</span></button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!-- Pagination/Footer -->
|
||||
<div class="px-6 py-3 border-t border-outline-variant bg-surface-container-low flex items-center justify-between">
|
||||
<span class="font-label-md text-label-md text-on-surface-variant">전체 4개 항목 중 1-4 표시</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<button class="p-1 rounded hover:bg-surface-container-high transition-colors disabled:opacity-30" disabled="">
|
||||
<span class="material-symbols-outlined">first_page</span>
|
||||
</button>
|
||||
<button class="p-1 rounded hover:bg-surface-container-high transition-colors disabled:opacity-30" disabled="">
|
||||
<span class="material-symbols-outlined">chevron_left</span>
|
||||
</button>
|
||||
<div class="flex items-center gap-1 px-4">
|
||||
<span class="w-8 h-8 flex items-center justify-center bg-primary text-on-primary rounded-md font-label-md text-label-md">1</span>
|
||||
</div>
|
||||
<button class="p-1 rounded hover:bg-surface-container-high transition-colors disabled:opacity-30" disabled="">
|
||||
<span class="material-symbols-outlined">chevron_right</span>
|
||||
</button>
|
||||
<button class="p-1 rounded hover:bg-surface-container-high transition-colors disabled:opacity-30" disabled="">
|
||||
<span class="material-symbols-outlined">last_page</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<!-- Floating Quick View Control (Micro-interaction) -->
|
||||
<div class="fixed bottom-10 right-10 flex flex-col gap-3 group">
|
||||
<button class="w-14 h-14 rounded-full bg-secondary text-on-secondary shadow-xl flex items-center justify-center hover:scale-110 transition-transform active:scale-95">
|
||||
<span class="material-symbols-outlined" style="font-variation-settings: 'FILL' 1;">auto_awesome</span>
|
||||
</button>
|
||||
<div class="absolute bottom-16 right-0 opacity-0 group-hover:opacity-100 transition-opacity flex flex-col gap-2 pointer-events-none group-hover:pointer-events-auto">
|
||||
<button class="bg-white border border-outline-variant px-4 py-2 rounded-lg text-on-surface font-label-md text-label-md shadow-lg flex items-center gap-2 hover:bg-surface-container-high">
|
||||
<span class="material-symbols-outlined text-[18px]">history</span>
|
||||
변경 이력 보기
|
||||
</button>
|
||||
<button class="bg-white border border-outline-variant px-4 py-2 rounded-lg text-on-surface font-label-md text-label-md shadow-lg flex items-center gap-2 hover:bg-surface-container-high">
|
||||
<span class="material-symbols-outlined text-[18px]">cloud_download</span>
|
||||
엑셀 다운로드
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<script>
|
||||
// Simple micro-interactions
|
||||
document.querySelectorAll('tbody tr').forEach(row => {
|
||||
row.addEventListener('click', () => {
|
||||
document.querySelectorAll('tbody tr').forEach(r => r.classList.remove('bg-primary-fixed'));
|
||||
row.classList.add('bg-primary-fixed');
|
||||
});
|
||||
});
|
||||
|
||||
// Search highlight logic
|
||||
const searchInput = document.querySelector('input[type="text"]');
|
||||
searchInput.addEventListener('input', (e) => {
|
||||
const term = e.target.value.toLowerCase();
|
||||
document.querySelectorAll('tbody tr').forEach(row => {
|
||||
const text = row.innerText.toLowerCase();
|
||||
if (text.includes(term)) {
|
||||
row.style.display = '';
|
||||
} else {
|
||||
row.style.display = 'none';
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body></html>
|
||||
469
stitch_kintex_ai_system_architect/scr_a4/code.html
Normal file
469
stitch_kintex_ai_system_architect/scr_a4/code.html
Normal file
@ -0,0 +1,469 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html class="light" lang="ko"><head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||
<title>KINTEX AI 전시관리 - 메뉴 관리</title>
|
||||
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Geist:wght@400;500;600&family=Hanken+Grotesk:wght@400;500;600;700&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<script id="tailwind-config">
|
||||
tailwind.config = {
|
||||
darkMode: "class",
|
||||
theme: {
|
||||
extend: {
|
||||
"colors": {
|
||||
"surface-container-low": "#f1f3ff",
|
||||
"surface-container-highest": "#dbe2f9",
|
||||
"outline-variant": "#c1c7d3",
|
||||
"background": "#f9f9ff",
|
||||
"on-surface": "#141b2c",
|
||||
"on-background": "#141b2c",
|
||||
"tertiary": "#00583b",
|
||||
"primary": "#004e8b",
|
||||
"secondary": "#5427e6",
|
||||
"outline": "#717782",
|
||||
"surface-container-high": "#e0e8ff",
|
||||
"secondary-container": "#6d4aff",
|
||||
"surface-container": "#e9edff",
|
||||
"on-error": "#ffffff",
|
||||
"on-tertiary-container": "#8ef7c3",
|
||||
"on-secondary-container": "#f3edff",
|
||||
"inverse-primary": "#a2c9ff",
|
||||
"on-error-container": "#93000a",
|
||||
"inverse-on-surface": "#edf0ff",
|
||||
"tertiary-fixed": "#8ef7c4",
|
||||
"secondary-fixed-dim": "#c9bfff",
|
||||
"on-primary-fixed-variant": "#004881",
|
||||
"primary-fixed": "#d3e4ff",
|
||||
"primary-container": "#0066b3",
|
||||
"surface-container-lowest": "#ffffff",
|
||||
"secondary-fixed": "#e5deff",
|
||||
"on-tertiary-fixed-variant": "#005236",
|
||||
"on-secondary": "#ffffff",
|
||||
"surface-dim": "#d2daf0",
|
||||
"error-container": "#ffdad6",
|
||||
"error": "#ba1a1a",
|
||||
"surface-tint": "#0060a9",
|
||||
"surface-bright": "#f9f9ff",
|
||||
"on-surface-variant": "#414751",
|
||||
"inverse-surface": "#293041",
|
||||
"on-primary-container": "#d2e3ff",
|
||||
"on-tertiary-fixed": "#002113",
|
||||
"primary-fixed-dim": "#a2c9ff",
|
||||
"surface-variant": "#dbe2f9",
|
||||
"on-secondary-fixed-variant": "#4500d8",
|
||||
"on-secondary-fixed": "#1b0063",
|
||||
"on-primary": "#ffffff",
|
||||
"tertiary-fixed-dim": "#72daa9",
|
||||
"on-tertiary": "#ffffff",
|
||||
"tertiary-container": "#00734e",
|
||||
"surface": "#f9f9ff",
|
||||
"on-primary-fixed": "#001c38"
|
||||
},
|
||||
"borderRadius": {
|
||||
"DEFAULT": "0.125rem",
|
||||
"lg": "0.25rem",
|
||||
"xl": "0.5rem",
|
||||
"full": "0.75rem"
|
||||
},
|
||||
"spacing": {
|
||||
"stack-md": "16px",
|
||||
"gutter": "16px",
|
||||
"base": "4px",
|
||||
"stack-sm": "8px",
|
||||
"stack-lg": "24px",
|
||||
"container-padding": "24px"
|
||||
},
|
||||
"fontFamily": {
|
||||
"title-sm": ["Inter", "Hanken Grotesk"],
|
||||
"headline-md": ["Inter", "Hanken Grotesk"],
|
||||
"body-sm": ["Inter", "Hanken Grotesk"],
|
||||
"display-lg": ["Inter", "Hanken Grotesk"],
|
||||
"label-xs": ["Geist"],
|
||||
"label-md": ["Geist"],
|
||||
"body-base": ["Inter", "Hanken Grotesk"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
body { font-family: 'Hanken Grotesk', 'Inter', sans-serif; }
|
||||
.material-symbols-outlined {
|
||||
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.drag-ghost {
|
||||
opacity: 0.5;
|
||||
background: #e0e8ff !important;
|
||||
}
|
||||
.tree-line::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: -18px;
|
||||
top: -10px;
|
||||
bottom: 14px;
|
||||
width: 1px;
|
||||
background: #c1c7d3;
|
||||
}
|
||||
.tree-line::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: -18px;
|
||||
top: 14px;
|
||||
width: 12px;
|
||||
height: 1px;
|
||||
background: #c1c7d3;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-background text-on-background min-h-screen">
|
||||
<!-- SideNavBar -->
|
||||
<aside class="w-64 h-screen fixed left-0 top-0 bg-surface-container-lowest border-r border-outline-variant flex flex-col py-stack-lg px-gutter z-50">
|
||||
<div class="mb-10 px-2">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-8 h-8 bg-primary rounded-lg flex items-center justify-center text-white">
|
||||
<span class="material-symbols-outlined text-sm">smart_toy</span>
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="text-title-sm font-title-sm font-bold text-primary">KINTEX AI 전시관리</h1>
|
||||
<p class="text-label-xs text-outline uppercase tracking-wider">Admin Console</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<nav class="flex-1 space-y-1">
|
||||
<a class="flex items-center gap-3 px-4 py-3 rounded-lg text-on-surface-variant hover:bg-surface-container transition-colors cursor-pointer group" href="#">
|
||||
<span class="material-symbols-outlined" data-icon="dashboard">dashboard</span>
|
||||
<span class="text-body-base font-body-base">대시보드</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-4 py-3 rounded-lg text-on-surface-variant hover:bg-surface-container transition-colors cursor-pointer group" href="#">
|
||||
<span class="material-symbols-outlined" data-icon="calendar_today">calendar_today</span>
|
||||
<span class="text-body-base font-body-base">전시회 일정</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-4 py-3 rounded-lg text-on-surface-variant hover:bg-surface-container transition-colors cursor-pointer group" href="#">
|
||||
<span class="material-symbols-outlined" data-icon="layers">layers</span>
|
||||
<span class="text-body-base font-body-base">부스 배치도</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-4 py-3 rounded-lg bg-surface-container-high text-primary font-semibold cursor-pointer group" href="#">
|
||||
<span class="material-symbols-outlined" data-icon="account_tree">account_tree</span>
|
||||
<span class="text-body-base font-body-base">메뉴 관리</span>
|
||||
</a>
|
||||
<a class="flex items-center gap-3 px-4 py-3 rounded-lg text-on-surface-variant hover:bg-surface-container transition-colors cursor-pointer group" href="#">
|
||||
<span class="material-symbols-outlined" data-icon="settings">settings</span>
|
||||
<span class="text-body-base font-body-base">시스템 설정</span>
|
||||
</a>
|
||||
</nav>
|
||||
<div class="mt-auto border-t border-outline-variant pt-4">
|
||||
<button class="flex items-center gap-3 px-4 py-3 w-full rounded-lg text-on-surface-variant hover:bg-surface-container transition-colors cursor-pointer active:opacity-80">
|
||||
<span class="material-symbols-outlined" data-icon="logout">logout</span>
|
||||
<span class="text-body-base font-body-base">로그아웃</span>
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
<!-- TopAppBar -->
|
||||
<header class="flex items-center justify-between px-container-padding ml-64 w-[calc(100%-16rem)] h-16 bg-surface-container-lowest border-b border-outline-variant sticky top-0 z-40">
|
||||
<div class="flex items-center gap-4">
|
||||
<h2 class="font-headline-md text-headline-md font-bold text-primary">메뉴 관리</h2>
|
||||
<div class="h-6 w-px bg-outline-variant"></div>
|
||||
<nav class="flex items-center gap-4">
|
||||
<span class="text-label-md text-primary border-b-2 border-primary py-5 px-1 cursor-pointer">구조 설정</span>
|
||||
<span class="text-label-md text-on-surface-variant hover:text-primary transition-colors py-5 px-1 cursor-pointer">권한 매핑</span>
|
||||
</nav>
|
||||
</div>
|
||||
<div class="flex items-center gap-6">
|
||||
<div class="relative w-72">
|
||||
<span class="material-symbols-outlined absolute left-3 top-1/2 -translate-y-1/2 text-outline text-sm">search</span>
|
||||
<input class="w-full pl-10 pr-4 py-2 bg-surface-container rounded-full border-none focus:ring-2 focus:ring-primary/20 text-body-sm" placeholder="메뉴 검색..." type="text"/>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<button class="w-10 h-10 flex items-center justify-center rounded-full hover:bg-surface-container transition-colors active:scale-95">
|
||||
<span class="material-symbols-outlined text-on-surface-variant" data-icon="notifications">notifications</span>
|
||||
</button>
|
||||
<button class="w-10 h-10 flex items-center justify-center rounded-full hover:bg-surface-container transition-colors active:scale-95">
|
||||
<span class="material-symbols-outlined text-on-surface-variant" data-icon="help_outline">help_outline</span>
|
||||
</button>
|
||||
<div class="flex items-center gap-2 ml-2 pl-4 border-l border-outline-variant">
|
||||
<div class="w-8 h-8 rounded-full bg-primary-container flex items-center justify-center text-on-primary-container font-bold text-xs">AD</div>
|
||||
<span class="text-label-md text-on-surface">Admin</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<!-- Main Content -->
|
||||
<main class="ml-64 p-stack-lg min-h-[calc(100vh-4rem)]">
|
||||
<div class="grid grid-cols-12 gap-gutter h-full items-start">
|
||||
<!-- Left Panel: Menu Tree (1/3) -->
|
||||
<section class="col-span-4 bg-surface-container-lowest border border-outline-variant rounded-lg p-stack-md flex flex-col h-[calc(100vh-160px)]">
|
||||
<div class="flex items-center justify-between mb-stack-md">
|
||||
<h3 class="text-title-sm font-title-sm text-on-surface flex items-center gap-2">
|
||||
<span class="material-symbols-outlined text-primary">account_tree</span>
|
||||
메뉴 트리
|
||||
</h3>
|
||||
<button class="text-primary hover:bg-primary/5 px-2 py-1 rounded text-label-md flex items-center gap-1 transition-colors">
|
||||
<span class="material-symbols-outlined text-sm">add</span>
|
||||
추가
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex-1 overflow-y-auto pr-2 space-y-2">
|
||||
<!-- Menu Item: Dashboard -->
|
||||
<div class="flex items-center gap-2 p-3 bg-surface-container-low rounded border border-outline-variant hover:border-primary/50 cursor-move transition-all group">
|
||||
<span class="material-symbols-outlined text-outline cursor-grab" data-icon="drag_indicator">drag_indicator</span>
|
||||
<span class="material-symbols-outlined text-primary text-xl" data-icon="dashboard">dashboard</span>
|
||||
<span class="flex-1 text-body-base">대시보드</span>
|
||||
<button class="text-outline hover:text-primary transition-colors">
|
||||
<span class="material-symbols-outlined text-sm" data-icon="visibility">visibility</span>
|
||||
</button>
|
||||
</div>
|
||||
<!-- Menu Item: Exhibition (With Submenu) -->
|
||||
<div>
|
||||
<div class="flex items-center gap-2 p-3 bg-surface-container-low rounded border border-outline-variant hover:border-primary/50 cursor-move transition-all group">
|
||||
<span class="material-symbols-outlined text-outline cursor-grab">drag_indicator</span>
|
||||
<span class="material-symbols-outlined text-primary text-xl">calendar_today</span>
|
||||
<span class="flex-1 text-body-base">전시회 일정</span>
|
||||
<button class="text-outline hover:text-primary transition-colors">
|
||||
<span class="material-symbols-outlined text-sm">visibility</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="ml-10 mt-2 space-y-2">
|
||||
<div class="relative flex items-center gap-2 p-2.5 bg-white rounded border border-outline-variant/60 tree-line">
|
||||
<span class="material-symbols-outlined text-outline text-lg">event_list</span>
|
||||
<span class="flex-1 text-body-sm">진행중인 전시</span>
|
||||
</div>
|
||||
<div class="relative flex items-center gap-2 p-2.5 bg-white rounded border border-outline-variant/60 tree-line">
|
||||
<span class="material-symbols-outlined text-outline text-lg">history</span>
|
||||
<span class="flex-1 text-body-sm">과거 이력</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Menu Item: Booth -->
|
||||
<div class="flex items-center gap-2 p-3 bg-surface-container-low rounded border border-outline-variant hover:border-primary/50 cursor-move transition-all group">
|
||||
<span class="material-symbols-outlined text-outline cursor-grab">drag_indicator</span>
|
||||
<span class="material-symbols-outlined text-primary text-xl">layers</span>
|
||||
<span class="flex-1 text-body-base">부스 배치도</span>
|
||||
<button class="text-outline hover:text-primary transition-colors">
|
||||
<span class="material-symbols-outlined text-sm">visibility</span>
|
||||
</button>
|
||||
</div>
|
||||
<!-- Menu Item: Menu Mgmt (Active) -->
|
||||
<div class="flex items-center gap-2 p-3 bg-primary-container text-on-primary-container rounded border-2 border-primary cursor-move shadow-sm scale-[1.02] ring-4 ring-primary/5">
|
||||
<span class="material-symbols-outlined opacity-70 cursor-grab">drag_indicator</span>
|
||||
<span class="material-symbols-outlined text-xl">account_tree</span>
|
||||
<span class="flex-1 text-body-base font-semibold">메뉴 관리</span>
|
||||
<button class="text-on-primary-container">
|
||||
<span class="material-symbols-outlined text-sm" style="font-variation-settings: 'FILL' 1;">visibility</span>
|
||||
</button>
|
||||
</div>
|
||||
<!-- Menu Item: System -->
|
||||
<div class="flex items-center gap-2 p-3 bg-surface-container-low rounded border border-outline-variant hover:border-primary/50 cursor-move transition-all group">
|
||||
<span class="material-symbols-outlined text-outline cursor-grab">drag_indicator</span>
|
||||
<span class="material-symbols-outlined text-primary text-xl">settings</span>
|
||||
<span class="flex-1 text-body-base">시스템 설정</span>
|
||||
<button class="text-outline hover:text-primary transition-colors">
|
||||
<span class="material-symbols-outlined text-sm">visibility</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<!-- Right Panel: Property Form (2/3) -->
|
||||
<section class="col-span-8 flex flex-col gap-gutter">
|
||||
<div class="bg-surface-container-lowest border border-outline-variant rounded-lg overflow-hidden flex flex-col">
|
||||
<div class="px-stack-lg py-stack-md border-b border-outline-variant flex items-center justify-between bg-white">
|
||||
<h3 class="text-title-sm font-title-sm text-on-surface flex items-center gap-2">
|
||||
<span class="material-symbols-outlined text-secondary">edit_note</span>
|
||||
메뉴 상세 설정
|
||||
</h3>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="bg-surface-container-highest text-primary px-3 py-1 rounded-full text-label-md border border-primary/20">ID: MN-004</span>
|
||||
<span class="flex items-center gap-1 text-label-md text-outline">
|
||||
<span class="material-symbols-outlined text-sm" style="font-variation-settings: 'FILL' 1; color: #6D4AFF;">auto_awesome</span>
|
||||
AI 최적화됨
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-stack-lg space-y-8">
|
||||
<!-- Basic Info Section -->
|
||||
<div class="grid grid-cols-2 gap-stack-lg">
|
||||
<div class="space-y-2">
|
||||
<label class="text-label-md text-on-surface-variant">메뉴명</label>
|
||||
<input class="w-full px-4 py-2.5 rounded border border-outline-variant focus:border-primary focus:ring-4 focus:ring-primary/10 transition-all text-body-base" type="text" value="메뉴 관리"/>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<label class="text-label-md text-on-surface-variant">URL 경로</label>
|
||||
<div class="relative">
|
||||
<span class="absolute left-4 top-1/2 -translate-y-1/2 text-outline text-body-sm">/admin/</span>
|
||||
<input class="w-full pl-20 pr-4 py-2.5 rounded border border-outline-variant focus:border-primary focus:ring-4 focus:ring-primary/10 transition-all text-body-base" type="text" value="menu-management"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<label class="text-label-md text-on-surface-variant">아이콘 선택</label>
|
||||
<button class="w-full flex items-center justify-between px-4 py-2.5 rounded border border-outline-variant hover:border-primary transition-all text-body-base bg-white">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="material-symbols-outlined text-primary" data-icon="account_tree">account_tree</span>
|
||||
<span>account_tree</span>
|
||||
</div>
|
||||
<span class="material-symbols-outlined text-outline">expand_more</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<label class="text-label-md text-on-surface-variant">노출 순서</label>
|
||||
<input class="w-full px-4 py-2.5 rounded border border-outline-variant focus:border-primary focus:ring-4 focus:ring-primary/10 transition-all text-body-base" type="number" value="4"/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Role-based Permissions -->
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<label class="text-label-md text-on-surface-variant font-bold">역할별 권한 설정</label>
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input class="rounded border-outline-variant text-primary focus:ring-primary w-4 h-4" type="checkbox"/>
|
||||
<span class="text-label-md text-outline">전체 선택</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="grid grid-cols-5 gap-3">
|
||||
<!-- Role Checkbox Card -->
|
||||
<label class="flex flex-col items-center gap-3 p-4 rounded-lg border-2 border-primary bg-primary-container/10 cursor-pointer transition-all hover:bg-primary-container/20">
|
||||
<div class="w-10 h-10 rounded-full bg-primary flex items-center justify-center text-white">
|
||||
<span class="material-symbols-outlined" style="font-variation-settings: 'FILL' 1;">admin_panel_settings</span>
|
||||
</div>
|
||||
<span class="text-label-md font-bold text-primary">관리자</span>
|
||||
<input checked="" class="rounded border-outline-variant text-primary focus:ring-primary w-5 h-5" type="checkbox"/>
|
||||
</label>
|
||||
<label class="flex flex-col items-center gap-3 p-4 rounded-lg border border-outline-variant bg-white cursor-pointer transition-all hover:border-primary/50">
|
||||
<div class="w-10 h-10 rounded-full bg-surface-container flex items-center justify-center text-outline">
|
||||
<span class="material-symbols-outlined">badge</span>
|
||||
</div>
|
||||
<span class="text-label-md text-on-surface-variant">주최자</span>
|
||||
<input class="rounded border-outline-variant text-primary focus:ring-primary w-5 h-5" type="checkbox"/>
|
||||
</label>
|
||||
<label class="flex flex-col items-center gap-3 p-4 rounded-lg border border-outline-variant bg-white cursor-pointer transition-all hover:border-primary/50">
|
||||
<div class="w-10 h-10 rounded-full bg-surface-container flex items-center justify-center text-outline">
|
||||
<span class="material-symbols-outlined">store</span>
|
||||
</div>
|
||||
<span class="text-label-md text-on-surface-variant">참가업체</span>
|
||||
<input class="rounded border-outline-variant text-primary focus:ring-primary w-5 h-5" type="checkbox"/>
|
||||
</label>
|
||||
<label class="flex flex-col items-center gap-3 p-4 rounded-lg border border-outline-variant bg-white cursor-pointer transition-all hover:border-primary/50">
|
||||
<div class="w-10 h-10 rounded-full bg-surface-container flex items-center justify-center text-outline">
|
||||
<span class="material-symbols-outlined">construction</span>
|
||||
</div>
|
||||
<span class="text-label-md text-on-surface-variant">장치업체</span>
|
||||
<input class="rounded border-outline-variant text-primary focus:ring-primary w-5 h-5" type="checkbox"/>
|
||||
</label>
|
||||
<label class="flex flex-col items-center gap-3 p-4 rounded-lg border border-outline-variant bg-white cursor-pointer transition-all hover:border-primary/50">
|
||||
<div class="w-10 h-10 rounded-full bg-surface-container flex items-center justify-center text-outline">
|
||||
<span class="material-symbols-outlined">hail</span>
|
||||
</div>
|
||||
<span class="text-label-md text-on-surface-variant">홀매니저</span>
|
||||
<input class="rounded border-outline-variant text-primary focus:ring-primary w-5 h-5" type="checkbox"/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Advanced Settings -->
|
||||
<div class="p-4 bg-surface-container rounded-lg border border-outline-variant border-dashed">
|
||||
<div class="flex items-start gap-4">
|
||||
<div class="mt-1">
|
||||
<span class="material-symbols-outlined text-secondary" style="font-variation-settings: 'FILL' 1;">info</span>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h4 class="text-label-md font-bold text-on-surface mb-1">고급 설정</h4>
|
||||
<p class="text-body-sm text-on-surface-variant mb-4">해당 메뉴의 노출 정책 및 연동 모듈을 설정합니다. AI가 메뉴 사용 패턴을 분석하여 비활성화 여부를 제안할 수 있습니다.</p>
|
||||
<div class="flex gap-6">
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input checked="" class="rounded border-outline-variant text-primary focus:ring-primary w-4 h-4" type="checkbox"/>
|
||||
<span class="text-body-sm">반응형 레이아웃 적용</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input class="rounded border-outline-variant text-primary focus:ring-primary w-4 h-4" type="checkbox"/>
|
||||
<span class="text-body-sm">즐겨찾기 허용</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-auto px-stack-lg py-stack-md bg-surface-container-low border-t border-outline-variant flex items-center justify-end gap-3">
|
||||
<button class="px-6 py-2.5 rounded border border-outline text-on-surface-variant hover:bg-surface-container transition-all active:scale-95 text-label-md font-bold">
|
||||
초기화
|
||||
</button>
|
||||
<button class="px-8 py-2.5 rounded bg-primary text-white hover:bg-primary/90 shadow-lg shadow-primary/20 transition-all active:scale-95 text-label-md font-bold flex items-center gap-2">
|
||||
<span class="material-symbols-outlined text-sm">save</span>
|
||||
변경사항 저장
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Usage Insight Card (Sub-content) -->
|
||||
<div class="grid grid-cols-2 gap-gutter">
|
||||
<div class="bg-white border border-outline-variant p-4 rounded-lg flex items-center gap-4">
|
||||
<div class="w-12 h-12 rounded-full bg-secondary-container/10 flex items-center justify-center text-secondary">
|
||||
<span class="material-symbols-outlined">trending_up</span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-label-xs text-outline">지난 30일 방문수</p>
|
||||
<p class="text-title-sm font-bold text-on-surface">1,248회</p>
|
||||
<p class="text-[10px] text-tertiary">전월 대비 +12.4% 증가</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-white border border-outline-variant p-4 rounded-lg flex items-center gap-4">
|
||||
<div class="w-12 h-12 rounded-full bg-primary-container/10 flex items-center justify-center text-primary">
|
||||
<span class="material-symbols-outlined">speed</span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-label-xs text-outline">평균 로딩 속도</p>
|
||||
<p class="text-title-sm font-bold text-on-surface">0.42s</p>
|
||||
<p class="text-[10px] text-tertiary">최적화 상태: 매우 좋음</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
<!-- Micro-interactions Script -->
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Ripple effect for primary button
|
||||
const primaryBtn = document.querySelector('.bg-primary');
|
||||
primaryBtn.addEventListener('mousedown', (e) => {
|
||||
const rect = primaryBtn.getBoundingClientRect();
|
||||
const x = e.clientX - rect.left;
|
||||
const y = e.clientY - rect.top;
|
||||
|
||||
const ripple = document.createElement('span');
|
||||
ripple.style.position = 'absolute';
|
||||
ripple.style.width = '100px';
|
||||
ripple.style.height = '100px';
|
||||
ripple.style.background = 'rgba(255, 255, 255, 0.3)';
|
||||
ripple.style.borderRadius = '50%';
|
||||
ripple.style.left = `${x - 50}px`;
|
||||
ripple.style.top = `${y - 50}px`;
|
||||
ripple.style.transform = 'scale(0)';
|
||||
ripple.style.transition = 'transform 0.5s ease-out, opacity 0.5s ease-out';
|
||||
ripple.style.pointerEvents = 'none';
|
||||
|
||||
primaryBtn.style.overflow = 'hidden';
|
||||
primaryBtn.style.position = 'relative';
|
||||
primaryBtn.appendChild(ripple);
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
ripple.style.transform = 'scale(4)';
|
||||
ripple.style.opacity = '0';
|
||||
});
|
||||
|
||||
setTimeout(() => ripple.remove(), 500);
|
||||
});
|
||||
|
||||
// Smooth scaling for tree items on hover
|
||||
const treeItems = document.querySelectorAll('.bg-surface-container-low');
|
||||
treeItems.forEach(item => {
|
||||
item.addEventListener('mouseenter', () => {
|
||||
if(!item.classList.contains('bg-primary-container')) {
|
||||
item.style.transform = 'translateX(4px)';
|
||||
}
|
||||
});
|
||||
item.addEventListener('mouseleave', () => {
|
||||
item.style.transform = 'translateX(0)';
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body></html>
|
||||
411
stitch_kintex_ai_system_architect/scr_a7/code.html
Normal file
411
stitch_kintex_ai_system_architect/scr_a7/code.html
Normal file
@ -0,0 +1,411 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html class="light" lang="ko"><head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||
<title>KINTEX AI 전시관리 - Admin Console</title>
|
||||
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Geist:wght@400;500;600&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<script id="tailwind-config">
|
||||
tailwind.config = {
|
||||
darkMode: "class",
|
||||
theme: {
|
||||
extend: {
|
||||
"colors": {
|
||||
"surface-dim": "#d2daf0",
|
||||
"surface-bright": "#f9f9ff",
|
||||
"on-secondary-fixed-variant": "#4500d8",
|
||||
"outline-variant": "#c1c7d3",
|
||||
"secondary": "#5427e6",
|
||||
"tertiary-container": "#00734e",
|
||||
"error": "#ba1a1a",
|
||||
"tertiary-fixed-dim": "#72daa9",
|
||||
"surface-tint": "#0060a9",
|
||||
"on-tertiary": "#ffffff",
|
||||
"surface-container-lowest": "#ffffff",
|
||||
"secondary-fixed": "#e5deff",
|
||||
"secondary-container": "#6d4aff",
|
||||
"tertiary-fixed": "#8ef7c4",
|
||||
"on-tertiary-container": "#8ef7c3",
|
||||
"on-secondary-fixed": "#1b0063",
|
||||
"surface-variant": "#dbe2f9",
|
||||
"on-surface-variant": "#414751",
|
||||
"error-container": "#ffdad6",
|
||||
"surface": "#f9f9ff",
|
||||
"inverse-on-surface": "#edf0ff",
|
||||
"on-secondary-container": "#f3edff",
|
||||
"inverse-primary": "#a2c9ff",
|
||||
"primary-fixed": "#d3e4ff",
|
||||
"on-secondary": "#ffffff",
|
||||
"on-primary-fixed-variant": "#004881",
|
||||
"tertiary": "#00583b",
|
||||
"surface-container": "#e9edff",
|
||||
"on-background": "#141b2c",
|
||||
"outline": "#717782",
|
||||
"primary-fixed-dim": "#a2c9ff",
|
||||
"on-primary": "#ffffff",
|
||||
"on-error": "#ffffff",
|
||||
"inverse-surface": "#293041",
|
||||
"surface-container-highest": "#dbe2f9",
|
||||
"on-error-container": "#93000a",
|
||||
"on-primary-fixed": "#001c38",
|
||||
"secondary-fixed-dim": "#c9bfff",
|
||||
"on-tertiary-fixed-variant": "#005236",
|
||||
"primary-container": "#0066b3",
|
||||
"on-primary-container": "#d2e3ff",
|
||||
"surface-container-high": "#e0e8ff",
|
||||
"on-tertiary-fixed": "#002113",
|
||||
"surface-container-low": "#f1f3ff",
|
||||
"on-surface": "#141b2c",
|
||||
"background": "#f9f9ff",
|
||||
"primary": "#004e8b"
|
||||
},
|
||||
"borderRadius": {
|
||||
"DEFAULT": "0.125rem",
|
||||
"lg": "0.25rem",
|
||||
"xl": "0.5rem",
|
||||
"full": "0.75rem"
|
||||
},
|
||||
"spacing": {
|
||||
"container-padding": "24px",
|
||||
"gutter": "16px",
|
||||
"base": "4px",
|
||||
"stack-sm": "8px",
|
||||
"stack-lg": "24px",
|
||||
"stack-md": "16px"
|
||||
},
|
||||
"fontFamily": {
|
||||
"title-sm": ["Inter", "sans-serif"],
|
||||
"body-base": ["Inter", "sans-serif"],
|
||||
"display-lg": ["Inter", "sans-serif"],
|
||||
"label-xs": ["Geist", "sans-serif"],
|
||||
"headline-md": ["Inter", "sans-serif"],
|
||||
"label-md": ["Geist", "sans-serif"],
|
||||
"body-sm": ["Inter", "sans-serif"]
|
||||
},
|
||||
"fontSize": {
|
||||
"title-sm": ["18px", {"lineHeight": "28px", "fontWeight": "600"}],
|
||||
"body-base": ["14px", {"lineHeight": "20px", "fontWeight": "400"}],
|
||||
"display-lg": ["36px", {"lineHeight": "44px", "letterSpacing": "-0.02em", "fontWeight": "700"}],
|
||||
"label-xs": ["11px", {"lineHeight": "14px", "fontWeight": "500"}],
|
||||
"headline-md": ["24px", {"lineHeight": "32px", "fontWeight": "600"}],
|
||||
"label-md": ["12px", {"lineHeight": "16px", "letterSpacing": "0.02em", "fontWeight": "600"}],
|
||||
"body-sm": ["13px", {"lineHeight": "18px", "fontWeight": "400"}]
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
body { font-family: 'Inter', 'Geist', sans-serif; -webkit-font-smoothing: antialiased; }
|
||||
.material-symbols-outlined { font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24; }
|
||||
.data-table-row:hover { background-color: rgba(0, 78, 139, 0.04); }
|
||||
.tabular-nums { font-variant-numeric: tabular-nums; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-background text-on-surface selection:bg-primary-container selection:text-on-primary-container overflow-hidden">
|
||||
<!-- Layout Wrapper -->
|
||||
<div class="flex h-screen w-full">
|
||||
<!-- SideNavBar Component -->
|
||||
<aside class="h-screen w-64 fixed left-0 top-0 flex flex-col py-stack-lg bg-surface-container border-r border-outline-variant z-50">
|
||||
<!-- Header Identity -->
|
||||
<div class="px-6 mb-10">
|
||||
<img alt="KINTEX Logo" class="w-full h-auto object-contain" src="https://lh3.googleusercontent.com/aida-public/AB6AXuACOlgRWpvKtAU8JIRpD32Gfw6zSBSSPoTfVonAGYK47s3O7dwPlqRlk5ZJPYP4kNiXnnFZjbXNMFXWd9UD2fHrbGz8wtid7ekh61qL5EaOawHppJyeFZqp_Ib8ibC5gnci3nEa3xmfHtYGwmBG2dydg_D2FEvRxWYCaZ5VvK2OTFFVOPmAIwXk8rLReAlezuMAMxxeGocedRN4h1GPMINiKKRpvkHVuiYe0HTKe3UiXTVxy_07F2lnrBJXoUP7QnPD_BYWrWV2JX4"/>
|
||||
<div class="mt-6">
|
||||
<h1 class="font-headline-md text-headline-md text-primary tracking-tight">Admin Console</h1>
|
||||
<p class="text-on-surface-variant font-label-md text-label-md opacity-80 uppercase tracking-widest">Exhibition Management</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Navigation Links -->
|
||||
<nav class="flex-1 space-y-1">
|
||||
<div class="group cursor-pointer duration-200 text-primary font-bold bg-surface-container-high border-r-4 border-primary px-6 py-3 flex items-center gap-3">
|
||||
<span class="material-symbols-outlined">domain</span>
|
||||
<span class="font-label-md text-label-md">홀 마스터</span>
|
||||
</div>
|
||||
<div class="group cursor-pointer duration-200 text-on-surface-variant hover:bg-surface-container-highest transition-all px-6 py-3 flex items-center gap-3">
|
||||
<span class="material-symbols-outlined">payments</span>
|
||||
<span class="font-label-md text-label-md">요율표</span>
|
||||
</div>
|
||||
<div class="group cursor-pointer duration-200 text-on-surface-variant hover:bg-surface-container-highest transition-all px-6 py-3 flex items-center gap-3">
|
||||
<span class="material-symbols-outlined">electrical_services</span>
|
||||
<span class="font-label-md text-label-md">유틸리티 요금</span>
|
||||
</div>
|
||||
<div class="group cursor-pointer duration-200 text-on-surface-variant hover:bg-surface-container-highest transition-all px-6 py-3 flex items-center gap-3">
|
||||
<span class="material-symbols-outlined">grid_view</span>
|
||||
<span class="font-label-md text-label-md">부스 표준</span>
|
||||
</div>
|
||||
<div class="group cursor-pointer duration-200 text-on-surface-variant hover:bg-surface-container-highest transition-all px-6 py-3 flex items-center gap-3">
|
||||
<span class="material-symbols-outlined">database</span>
|
||||
<span class="font-label-md text-label-md">등록업체 DB</span>
|
||||
</div>
|
||||
<div class="group cursor-pointer duration-200 text-on-surface-variant hover:bg-surface-container-highest transition-all px-6 py-3 flex items-center gap-3">
|
||||
<span class="material-symbols-outlined">history</span>
|
||||
<span class="font-label-md text-label-md">시스템 로그</span>
|
||||
</div>
|
||||
</nav>
|
||||
<!-- Footer Links -->
|
||||
<div class="px-6 space-y-1 mt-auto">
|
||||
<div class="group cursor-pointer duration-200 text-on-surface-variant hover:bg-surface-container-highest transition-all py-3 flex items-center gap-3">
|
||||
<span class="material-symbols-outlined">settings</span>
|
||||
<span class="font-label-md text-label-md">Settings</span>
|
||||
</div>
|
||||
<div class="group cursor-pointer duration-200 text-on-surface-variant hover:bg-surface-container-highest transition-all py-3 flex items-center gap-3">
|
||||
<span class="material-symbols-outlined">logout</span>
|
||||
<span class="font-label-md text-label-md">Logout</span>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
<!-- Main Content Area -->
|
||||
<main class="ml-64 flex-1 flex flex-col h-full bg-background overflow-hidden">
|
||||
<!-- TopNavBar Component -->
|
||||
<header class="flex items-center justify-between px-container-padding sticky top-0 z-40 bg-surface w-full h-16 border-b border-outline-variant">
|
||||
<div class="flex items-center gap-4">
|
||||
<span class="material-symbols-outlined text-primary cursor-pointer">menu</span>
|
||||
<h2 class="font-title-sm text-title-sm text-primary">KINTEX AI 전시관리</h2>
|
||||
</div>
|
||||
<div class="flex items-center gap-gutter">
|
||||
<div class="relative">
|
||||
<span class="material-symbols-outlined absolute left-3 top-1/2 -translate-y-1/2 text-on-surface-variant text-sm">search</span>
|
||||
<input class="pl-10 pr-4 py-1.5 bg-surface-container-low border border-outline-variant rounded-full text-sm w-64 focus:ring-1 focus:ring-primary focus:border-primary outline-none transition-all" placeholder="통합 검색..." type="text"/>
|
||||
</div>
|
||||
<div class="flex items-center gap-4 border-l border-outline-variant pl-4">
|
||||
<span class="material-symbols-outlined text-on-surface-variant cursor-pointer hover:text-primary transition-colors">notifications</span>
|
||||
<span class="material-symbols-outlined text-on-surface-variant cursor-pointer hover:text-primary transition-colors">settings</span>
|
||||
<span class="material-symbols-outlined text-on-surface-variant cursor-pointer hover:text-primary transition-colors">help</span>
|
||||
<div class="w-8 h-8 rounded-full bg-primary-fixed flex items-center justify-center cursor-pointer">
|
||||
<span class="material-symbols-outlined text-on-primary-fixed text-lg">person</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<!-- Scrollable Content -->
|
||||
<div class="flex-1 overflow-y-auto p-container-padding">
|
||||
<!-- Breadcrumb -->
|
||||
<nav class="flex items-center gap-2 mb-stack-lg text-body-sm text-on-surface-variant">
|
||||
<span>마스터데이터 관리</span>
|
||||
<span class="material-symbols-outlined text-sm">chevron_right</span>
|
||||
<span class="text-primary font-bold">홀 마스터</span>
|
||||
</nav>
|
||||
<!-- Page Header & Action -->
|
||||
<div class="flex justify-between items-end mb-6">
|
||||
<div>
|
||||
<h3 class="font-display-lg text-display-lg text-on-background mb-2">홀 마스터 관리</h3>
|
||||
<p class="text-on-surface-variant text-body-base">전시장 홀 정보 및 물리적 규격 마스터 데이터를 관리합니다.</p>
|
||||
</div>
|
||||
<div class="flex gap-stack-sm">
|
||||
<button class="flex items-center gap-2 px-4 py-2 border border-primary text-primary hover:bg-primary-container/10 transition-colors rounded-lg font-medium text-sm">
|
||||
<span class="material-symbols-outlined text-sm">download</span>
|
||||
엑셀 내보내기
|
||||
</button>
|
||||
<button class="flex items-center gap-2 px-4 py-2 bg-primary text-on-primary hover:opacity-90 transition-opacity rounded-lg font-medium text-sm">
|
||||
<span class="material-symbols-outlined text-sm">add</span>
|
||||
새 전시홀 등록
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Tabs -->
|
||||
<div class="flex border-b border-outline-variant mb-stack-lg gap-8 overflow-x-auto">
|
||||
<button class="pb-3 border-b-2 border-primary text-primary font-bold whitespace-nowrap text-sm">홀 마스터</button>
|
||||
<button class="pb-3 border-b-2 border-transparent text-on-surface-variant hover:text-primary transition-colors whitespace-nowrap text-sm">요율표</button>
|
||||
<button class="pb-3 border-b-2 border-transparent text-on-surface-variant hover:text-primary transition-colors whitespace-nowrap text-sm">유틸리티 요금</button>
|
||||
<button class="pb-3 border-b-2 border-transparent text-on-surface-variant hover:text-primary transition-colors whitespace-nowrap text-sm">부스 표준</button>
|
||||
<button class="pb-3 border-b-2 border-transparent text-on-surface-variant hover:text-primary transition-colors whitespace-nowrap text-sm">등록업체 DB</button>
|
||||
</div>
|
||||
<!-- Bento Grid Layout Section -->
|
||||
<div class="grid grid-cols-12 gap-gutter items-start">
|
||||
<!-- Table Section (Main Body) -->
|
||||
<div class="col-span-12 lg:col-span-9 bg-surface-container-lowest border border-outline-variant rounded-xl overflow-hidden">
|
||||
<table class="w-full text-left border-collapse">
|
||||
<thead>
|
||||
<tr class="bg-surface-container-low border-b border-outline-variant">
|
||||
<th class="px-6 py-4 font-label-md text-label-md text-on-surface-variant uppercase tracking-wider">홀</th>
|
||||
<th class="px-6 py-4 font-label-md text-label-md text-on-surface-variant uppercase tracking-wider text-right">면적(㎡)</th>
|
||||
<th class="px-6 py-4 font-label-md text-label-md text-on-surface-variant uppercase tracking-wider">규격(L×W×H)</th>
|
||||
<th class="px-6 py-4 font-label-md text-label-md text-on-surface-variant uppercase tracking-wider">바닥하중</th>
|
||||
<th class="px-6 py-4 font-label-md text-label-md text-on-surface-variant uppercase tracking-wider text-right">부스수(Max)</th>
|
||||
<th class="px-6 py-4 font-label-md text-label-md text-on-surface-variant uppercase tracking-wider text-center">관리</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-outline-variant">
|
||||
<!-- Row 1 -->
|
||||
<tr class="data-table-row transition-colors">
|
||||
<td class="px-6 py-4 font-bold text-primary">홀 1</td>
|
||||
<td class="px-6 py-4 tabular-nums text-right">10,611㎡</td>
|
||||
<td class="px-6 py-4 tabular-nums text-on-surface-variant">126×84×12m</td>
|
||||
<td class="px-6 py-4 text-on-surface-variant">5t/㎡</td>
|
||||
<td class="px-6 py-4 tabular-nums text-right">480</td>
|
||||
<td class="px-6 py-4 flex justify-center gap-2">
|
||||
<button class="p-1 hover:bg-primary-container/20 rounded text-primary transition-colors">
|
||||
<span class="material-symbols-outlined text-sm">edit</span>
|
||||
</button>
|
||||
<button class="p-1 hover:bg-error-container/20 rounded text-error transition-colors">
|
||||
<span class="material-symbols-outlined text-sm">delete</span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Row 2 -->
|
||||
<tr class="data-table-row transition-colors">
|
||||
<td class="px-6 py-4 font-bold text-primary">홀 2</td>
|
||||
<td class="px-6 py-4 tabular-nums text-right">10,611㎡</td>
|
||||
<td class="px-6 py-4 tabular-nums text-on-surface-variant">126×84×12m</td>
|
||||
<td class="px-6 py-4 text-on-surface-variant">5t/㎡</td>
|
||||
<td class="px-6 py-4 tabular-nums text-right">480</td>
|
||||
<td class="px-6 py-4 flex justify-center gap-2">
|
||||
<button class="p-1 hover:bg-primary-container/20 rounded text-primary transition-colors">
|
||||
<span class="material-symbols-outlined text-sm">edit</span>
|
||||
</button>
|
||||
<button class="p-1 hover:bg-error-container/20 rounded text-error transition-colors">
|
||||
<span class="material-symbols-outlined text-sm">delete</span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Row 3 -->
|
||||
<tr class="data-table-row transition-colors">
|
||||
<td class="px-6 py-4 font-bold text-primary">홀 7</td>
|
||||
<td class="px-6 py-4 tabular-nums text-right">11,290㎡</td>
|
||||
<td class="px-6 py-4 tabular-nums text-on-surface-variant">126×90×12m</td>
|
||||
<td class="px-6 py-4 text-on-surface-variant">5t/㎡</td>
|
||||
<td class="px-6 py-4 tabular-nums text-right">510</td>
|
||||
<td class="px-6 py-4 flex justify-center gap-2">
|
||||
<button class="p-1 hover:bg-primary-container/20 rounded text-primary transition-colors">
|
||||
<span class="material-symbols-outlined text-sm">edit</span>
|
||||
</button>
|
||||
<button class="p-1 hover:bg-error-container/20 rounded text-error transition-colors">
|
||||
<span class="material-symbols-outlined text-sm">delete</span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Row 4 -->
|
||||
<tr class="data-table-row transition-colors">
|
||||
<td class="px-6 py-4 font-bold text-primary">홀 8</td>
|
||||
<td class="px-6 py-4 tabular-nums text-right">11,290㎡</td>
|
||||
<td class="px-6 py-4 tabular-nums text-on-surface-variant">126×90×15m</td>
|
||||
<td class="px-6 py-4 text-on-surface-variant">5t/㎡</td>
|
||||
<td class="px-6 py-4 tabular-nums text-right">510</td>
|
||||
<td class="px-6 py-4 flex justify-center gap-2">
|
||||
<button class="p-1 hover:bg-primary-container/20 rounded text-primary transition-colors">
|
||||
<span class="material-symbols-outlined text-sm">edit</span>
|
||||
</button>
|
||||
<button class="p-1 hover:bg-error-container/20 rounded text-error transition-colors">
|
||||
<span class="material-symbols-outlined text-sm">delete</span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Row 5 -->
|
||||
<tr class="data-table-row transition-colors">
|
||||
<td class="px-6 py-4 font-bold text-primary">홀 10</td>
|
||||
<td class="px-6 py-4 tabular-nums text-right">10,000㎡</td>
|
||||
<td class="px-6 py-4 tabular-nums text-on-surface-variant">100×100×15m</td>
|
||||
<td class="px-6 py-4 text-on-surface-variant">5t/㎡</td>
|
||||
<td class="px-6 py-4 tabular-nums text-right">450</td>
|
||||
<td class="px-6 py-4 flex justify-center gap-2">
|
||||
<button class="p-1 hover:bg-primary-container/20 rounded text-primary transition-colors">
|
||||
<span class="material-symbols-outlined text-sm">edit</span>
|
||||
</button>
|
||||
<button class="p-1 hover:bg-error-container/20 rounded text-error transition-colors">
|
||||
<span class="material-symbols-outlined text-sm">delete</span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!-- Sidebar Info / Action Cards -->
|
||||
<div class="col-span-12 lg:col-span-3 space-y-gutter">
|
||||
<!-- AI Insights Card -->
|
||||
<div class="bg-surface-container-lowest border border-outline-variant rounded-xl p-5 border-l-4 border-l-secondary">
|
||||
<div class="flex items-center gap-2 text-secondary mb-3">
|
||||
<span class="material-symbols-outlined text-xl">auto_awesome</span>
|
||||
<span class="font-bold text-sm">AI 최적화 분석</span>
|
||||
</div>
|
||||
<p class="text-body-sm text-on-surface mb-4">현재 홀 7의 부스 배치 밀도가 다른 전시홀 대비 12% 높습니다. 안전 통로 확보를 위한 시뮬레이션을 권장합니다.</p>
|
||||
<button class="w-full py-2 bg-secondary text-on-secondary rounded-lg text-xs font-bold hover:opacity-90 transition-opacity">
|
||||
시뮬레이션 실행
|
||||
</button>
|
||||
</div>
|
||||
<!-- CAD Upload Section -->
|
||||
<div class="bg-surface-container-lowest border border-outline-variant rounded-xl p-5 border-l-4 border-l-on-surface-variant">
|
||||
<div class="flex justify-between items-start mb-4">
|
||||
<div>
|
||||
<h4 class="font-bold text-sm mb-1">트렌치 좌표 CAD 업로드</h4>
|
||||
<div class="inline-flex items-center gap-1 bg-surface-container-highest px-2 py-0.5 rounded text-[10px] text-on-surface-variant">
|
||||
<span class="w-1.5 h-1.5 rounded-full bg-outline"></span>
|
||||
미확보 · 킨텍스 협의
|
||||
</div>
|
||||
</div>
|
||||
<span class="material-symbols-outlined text-on-surface-variant opacity-40">upload_file</span>
|
||||
</div>
|
||||
<p class="text-xs text-on-surface-variant leading-relaxed mb-4">
|
||||
홀7, 8 전용 지하 트렌치 정밀 좌표 데이터가 누락되었습니다. 킨텍스 시설팀 협의 후 최신 .dwg 파일을 업로드하십시오.
|
||||
</p>
|
||||
<div class="border-2 border-dashed border-outline-variant rounded-lg p-6 flex flex-col items-center justify-center cursor-pointer hover:bg-surface-container-low transition-colors group">
|
||||
<span class="material-symbols-outlined text-primary mb-2 opacity-60 group-hover:opacity-100">add_circle</span>
|
||||
<span class="text-[11px] font-bold text-on-surface-variant uppercase tracking-tighter">Click to upload</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Statistics/Status -->
|
||||
<div class="bg-surface-container-lowest border border-outline-variant rounded-xl p-5">
|
||||
<h4 class="font-bold text-sm mb-4">홀 이용 현황 요약</h4>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<div class="flex justify-between text-xs mb-1">
|
||||
<span class="text-on-surface-variant">전체 홀 가동률</span>
|
||||
<span class="font-bold">68%</span>
|
||||
</div>
|
||||
<div class="w-full h-1.5 bg-surface-container-highest rounded-full overflow-hidden">
|
||||
<div class="h-full bg-primary w-[68%]"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="flex justify-between text-xs mb-1">
|
||||
<span class="text-on-surface-variant">부스 점유 면적</span>
|
||||
<span class="font-bold">42,500㎡</span>
|
||||
</div>
|
||||
<div class="w-full h-1.5 bg-surface-container-highest rounded-full overflow-hidden">
|
||||
<div class="h-full bg-tertiary-container w-[55%]"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Footer / Copyright -->
|
||||
<footer class="mt-auto px-container-padding py-4 bg-surface border-t border-outline-variant flex justify-between items-center text-[11px] text-on-surface-variant">
|
||||
<div>© 2024 KINTEX AI 전시관리 시스템. All rights reserved.</div>
|
||||
<div class="flex gap-4">
|
||||
<a class="hover:text-primary underline" href="#">개인정보처리방침</a>
|
||||
<a class="hover:text-primary underline" href="#">관리자 매뉴얼</a>
|
||||
<span class="tabular-nums">v2.4.1 (Stable Build)</span>
|
||||
</div>
|
||||
</footer>
|
||||
</main>
|
||||
</div>
|
||||
<!-- Micro-interaction for hover states -->
|
||||
<script>
|
||||
document.querySelectorAll('.data-table-row').forEach(row => {
|
||||
row.addEventListener('mouseenter', () => {
|
||||
row.style.transform = 'translateX(4px)';
|
||||
row.style.transition = 'transform 0.2s cubic-bezier(0.16, 1, 0.3, 1)';
|
||||
});
|
||||
row.addEventListener('mouseleave', () => {
|
||||
row.style.transform = 'translateX(0)';
|
||||
});
|
||||
});
|
||||
|
||||
// Simple tab switching logic for UI demonstration
|
||||
const tabs = document.querySelectorAll('.border-b-2');
|
||||
tabs.forEach(tab => {
|
||||
tab.addEventListener('click', () => {
|
||||
tabs.forEach(t => {
|
||||
t.classList.remove('border-primary', 'text-primary', 'font-bold');
|
||||
t.classList.add('border-transparent', 'text-on-surface-variant');
|
||||
});
|
||||
tab.classList.add('border-primary', 'text-primary', 'font-bold');
|
||||
tab.classList.remove('border-transparent', 'text-on-surface-variant');
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body></html>
|
||||
250
stitch_kintex_ai_system_architect/scr_m10/code.html
Normal file
250
stitch_kintex_ai_system_architect/scr_m10/code.html
Normal file
@ -0,0 +1,250 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html class="light" lang="ko"><head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||
<title>KINTEX Gate Pass - Move-in & Safety</title>
|
||||
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Geist:wght@400;500;600&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<script id="tailwind-config">
|
||||
tailwind.config = {
|
||||
darkMode: "class",
|
||||
theme: {
|
||||
extend: {
|
||||
"colors": {
|
||||
"primary-fixed-dim": "#a2c9ff",
|
||||
"on-error": "#ffffff",
|
||||
"outline": "#717782",
|
||||
"error-container": "#ffdad6",
|
||||
"on-secondary-fixed": "#1b0063",
|
||||
"on-primary-fixed": "#001c38",
|
||||
"surface": "#f9f9ff",
|
||||
"surface-tint": "#0060a9",
|
||||
"secondary": "#5427e6",
|
||||
"surface-container-lowest": "#ffffff",
|
||||
"surface-container-high": "#e0e8ff",
|
||||
"tertiary": "#00583b",
|
||||
"primary": "#004e8b",
|
||||
"on-tertiary-fixed": "#002113",
|
||||
"secondary-fixed": "#e5deff",
|
||||
"inverse-surface": "#293041",
|
||||
"tertiary-container": "#00734e",
|
||||
"tertiary-fixed": "#8ef7c4",
|
||||
"inverse-on-surface": "#edf0ff",
|
||||
"primary-container": "#0066b3",
|
||||
"background": "#f9f9ff",
|
||||
"on-tertiary-fixed-variant": "#005236",
|
||||
"on-primary-fixed-variant": "#004881",
|
||||
"error": "#ba1a1a",
|
||||
"on-surface": "#141b2c",
|
||||
"surface-container": "#e9edff",
|
||||
"surface-variant": "#dbe2f9",
|
||||
"on-tertiary-container": "#8ef7c3",
|
||||
"on-tertiary": "#ffffff",
|
||||
"on-secondary-container": "#f3edff",
|
||||
"on-surface-variant": "#414751",
|
||||
"on-secondary-fixed-variant": "#4500d8",
|
||||
"on-background": "#141b2c",
|
||||
"surface-dim": "#d2daf0",
|
||||
"surface-container-low": "#f1f3ff",
|
||||
"tertiary-fixed-dim": "#72daa9",
|
||||
"secondary-container": "#6d4aff",
|
||||
"on-primary-container": "#d2e3ff",
|
||||
"on-error-container": "#93000a",
|
||||
"inverse-primary": "#a2c9ff",
|
||||
"surface-bright": "#f9f9ff",
|
||||
"on-secondary": "#ffffff",
|
||||
"secondary-fixed-dim": "#c9bfff",
|
||||
"surface-container-highest": "#dbe2f9",
|
||||
"on-primary": "#ffffff",
|
||||
"primary-fixed": "#d3e4ff",
|
||||
"outline-variant": "#c1c7d3"
|
||||
},
|
||||
"borderRadius": {
|
||||
"DEFAULT": "0.125rem",
|
||||
"lg": "0.25rem",
|
||||
"xl": "0.5rem",
|
||||
"full": "0.75rem"
|
||||
},
|
||||
"spacing": {
|
||||
"stack-sm": "8px",
|
||||
"stack-md": "16px",
|
||||
"container-padding": "24px",
|
||||
"base": "4px",
|
||||
"gutter": "16px",
|
||||
"stack-lg": "24px"
|
||||
},
|
||||
"fontFamily": {
|
||||
"label-xs": ["Geist"],
|
||||
"body-base": ["Inter"],
|
||||
"label-md": ["Geist"],
|
||||
"body-sm": ["Inter"],
|
||||
"title-sm": ["Inter"],
|
||||
"display-lg-mobile": ["Inter"],
|
||||
"headline-md": ["Inter"],
|
||||
"display-lg": ["Inter"]
|
||||
},
|
||||
"fontSize": {
|
||||
"label-xs": ["11px", {"lineHeight": "14px", "fontWeight": "500"}],
|
||||
"body-base": ["14px", {"lineHeight": "20px", "fontWeight": "400"}],
|
||||
"label-md": ["12px", {"lineHeight": "16px", "letterSpacing": "0.02em", "fontWeight": "600"}],
|
||||
"body-sm": ["13px", {"lineHeight": "18px", "fontWeight": "400"}],
|
||||
"title-sm": ["18px", {"lineHeight": "28px", "fontWeight": "600"}],
|
||||
"display-lg-mobile": ["28px", {"lineHeight": "36px", "fontWeight": "700"}],
|
||||
"headline-md": ["24px", {"lineHeight": "32px", "fontWeight": "600"}],
|
||||
"display-lg": ["36px", {"lineHeight": "44px", "letterSpacing": "-0.02em", "fontWeight": "700"}]
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Inter', sans-serif;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
.custom-checkbox:checked {
|
||||
background-color: #0060a9;
|
||||
border-color: #0060a9;
|
||||
}
|
||||
.qr-placeholder {
|
||||
background: repeating-conic-gradient(#141b2c 0% 25%, #ffffff 0% 50%) 50% / 20px 20px;
|
||||
}
|
||||
</style>
|
||||
<style>
|
||||
body {
|
||||
min-height: max(884px, 100dvh);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-background text-on-background min-h-screen pb-24">
|
||||
<!-- TopAppBar -->
|
||||
<header class="fixed top-0 w-full z-50 flex justify-between items-center px-container-padding h-16 bg-surface border-b border-outline-variant">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="material-symbols-outlined text-primary font-headline-md text-headline-md" style="font-variation-settings: 'FILL' 1;">construction</span>
|
||||
<h1 class="font-headline-md text-headline-md font-bold text-primary">KINTEX Gate Pass</h1>
|
||||
</div>
|
||||
<button class="w-10 h-10 flex items-center justify-center rounded-full hover:bg-surface-variant transition-colors duration-200">
|
||||
<span class="material-symbols-outlined text-on-surface-variant">account_circle</span>
|
||||
</button>
|
||||
</header>
|
||||
<main class="pt-20 px-4 space-y-6">
|
||||
<!-- Gate Pass Section -->
|
||||
<section aria-label="게이트 패스 정보">
|
||||
<div class="bg-surface-container-lowest border border-outline-variant rounded-xl overflow-hidden shadow-sm">
|
||||
<!-- Blue Header -->
|
||||
<div class="bg-primary-container text-on-primary-container px-4 py-3 flex justify-between items-center">
|
||||
<span class="font-label-md text-label-md uppercase tracking-wider opacity-90">Gate Pass Entry</span>
|
||||
<div class="bg-error px-2 py-0.5 rounded text-white font-bold text-[10px]">D-2</div>
|
||||
</div>
|
||||
<div class="p-6 flex flex-col items-center">
|
||||
<!-- QR Code -->
|
||||
<div class="relative w-48 h-48 bg-white border-4 border-surface-container rounded-lg p-2 mb-6">
|
||||
<img class="w-full h-full object-contain" data-alt="A high-contrast, professional QR code displayed on a clean white digital background. The framing is modern and technical, with thin blue scanning guide lines at the corners. The lighting is even and bright, emphasizing a secure and reliable digital check-in environment for an industrial setting. KINTEX blue accents are visible in the interface surrounding the code." src="https://lh3.googleusercontent.com/aida-public/AB6AXuC9v-75LmCAEZkNZOWmggRFe1tn3P1rn_S2SS-uf-VH25YfQFAwd4X1KYHQtWDWUi20FYdqelSRs3VfCRYousgKkFUjWJZV5JqUj5Pt92r0udCNLqAOaeo6ujddz-38ZcGEMbd9058D8fg43Sxvni-F4jTVCXK7drdriLzOczKGm4UOex6BdotUIYzTl5I1pE9kaXFwgOxrztbBAKQsR-w7Ije86CHcn-RZgnANkT63AfFBAlOG-RUrFJ5PBWh7SR7wYcFeT5muIuU"/>
|
||||
</div>
|
||||
<!-- Vehicle & Dock Info -->
|
||||
<div class="w-full grid grid-cols-2 gap-4 border-t border-outline-variant pt-6">
|
||||
<div class="space-y-1">
|
||||
<p class="font-label-md text-label-md text-on-surface-variant">차량 번호</p>
|
||||
<p class="font-headline-md text-headline-md font-bold">12가 3456</p>
|
||||
</div>
|
||||
<div class="space-y-1 text-right">
|
||||
<p class="font-label-md text-label-md text-on-surface-variant">지정 도크</p>
|
||||
<p class="font-headline-md text-headline-md font-bold text-primary">도크 3</p>
|
||||
</div>
|
||||
<div class="col-span-2 bg-surface-container-low p-3 rounded-lg flex justify-between items-center">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="material-symbols-outlined text-primary-container">schedule</span>
|
||||
<span class="font-body-base text-body-base font-semibold">08:00 - 09:00</span>
|
||||
</div>
|
||||
<span class="font-label-md text-label-md bg-secondary-container text-on-secondary-container px-2 py-1 rounded">장치 기간</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<!-- Safety Checklist Section -->
|
||||
<section aria-label="안전 체크리스트" class="space-y-4">
|
||||
<h2 class="font-title-sm text-title-sm border-l-4 border-primary pl-3 flex items-center justify-between">
|
||||
안전 체크리스트
|
||||
<span class="text-error font-label-md text-label-md flex items-center gap-1">
|
||||
<span class="material-symbols-outlined text-[16px]">warning</span> 필수 확인
|
||||
</span>
|
||||
</h2>
|
||||
<div class="space-y-3">
|
||||
<!-- Checklist Item 1 -->
|
||||
<label class="flex items-center gap-4 bg-surface-container-high p-4 rounded-xl border border-transparent active:scale-[0.98] transition-transform cursor-pointer">
|
||||
<input class="w-8 h-8 rounded-lg border-2 border-outline-variant text-primary focus:ring-primary focus:ring-offset-0 custom-checkbox" type="checkbox"/>
|
||||
<div class="flex-1">
|
||||
<p class="font-title-sm text-title-sm">안전모 착용</p>
|
||||
<p class="font-label-md text-label-md text-on-surface-variant">현장 내 상시 착용 필수</p>
|
||||
</div>
|
||||
<span class="material-symbols-outlined text-on-surface-variant">construction</span>
|
||||
</label>
|
||||
<!-- Checklist Item 2 -->
|
||||
<label class="flex items-center gap-4 bg-surface-container-high p-4 rounded-xl border border-transparent active:scale-[0.98] transition-transform cursor-pointer">
|
||||
<input class="w-8 h-8 rounded-lg border-2 border-outline-variant text-primary focus:ring-primary focus:ring-offset-0 custom-checkbox" type="checkbox"/>
|
||||
<div class="flex-1">
|
||||
<p class="font-title-sm text-title-sm">중량물 5t↑ 신고</p>
|
||||
<p class="font-label-md text-label-md text-on-surface-variant">대형 구조물 및 지게차 작업</p>
|
||||
</div>
|
||||
<span class="material-symbols-outlined text-on-surface-variant">forklift</span>
|
||||
</label>
|
||||
<!-- Checklist Item 3 -->
|
||||
<label class="flex items-center gap-4 bg-surface-container-high p-4 rounded-xl border border-transparent active:scale-[0.98] transition-transform cursor-pointer">
|
||||
<input class="w-8 h-8 rounded-lg border-2 border-outline-variant text-primary focus:ring-primary focus:ring-offset-0 custom-checkbox" type="checkbox"/>
|
||||
<div class="flex-1">
|
||||
<p class="font-title-sm text-title-sm">금지작업 확인</p>
|
||||
<p class="font-label-md text-label-md text-on-surface-variant">전기톱·용접·페인트 (사전 승인 필수)</p>
|
||||
</div>
|
||||
<span class="material-symbols-outlined text-on-surface-variant">dangerous</span>
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
<!-- Reminder & Map Section -->
|
||||
<section class="grid grid-cols-2 gap-4 pb-8">
|
||||
<div class="bg-on-background p-4 rounded-xl text-surface flex flex-col justify-center">
|
||||
<p class="font-label-xs text-label-xs opacity-70 mb-1">슬롯 시간</p>
|
||||
<p class="font-title-sm text-title-sm font-bold">D-0 09:00</p>
|
||||
</div>
|
||||
<button class="bg-primary text-white p-4 rounded-xl flex flex-col items-center justify-center gap-1 active:scale-[0.95] transition-transform">
|
||||
<span class="material-symbols-outlined" style="font-variation-settings: 'FILL' 1;">map</span>
|
||||
<span class="font-label-md text-label-md font-bold">현장 지도 확인</span>
|
||||
</button>
|
||||
</section>
|
||||
</main>
|
||||
<!-- BottomNavBar -->
|
||||
<nav class="fixed bottom-0 w-full z-50 flex justify-around items-center px-4 py-2 bg-surface-container border-t border-outline-variant rounded-t-xl">
|
||||
<a class="flex flex-col items-center justify-center bg-secondary-container text-on-secondary-container rounded-full px-4 py-1 transition-transform active:scale-90" href="#">
|
||||
<span class="material-symbols-outlined" style="font-variation-settings: 'FILL' 1;">home</span>
|
||||
<span class="font-label-md text-label-md">Home</span>
|
||||
</a>
|
||||
<a class="flex flex-col items-center justify-center text-on-surface-variant hover:bg-surface-variant transition-transform active:scale-90" href="#">
|
||||
<span class="material-symbols-outlined">calendar_today</span>
|
||||
<span class="font-label-md text-label-md">Schedule</span>
|
||||
</a>
|
||||
<a class="flex flex-col items-center justify-center text-on-surface-variant hover:bg-surface-variant transition-transform active:scale-90" href="#">
|
||||
<span class="material-symbols-outlined">qr_code_scanner</span>
|
||||
<span class="font-label-md text-label-md">Scan</span>
|
||||
</a>
|
||||
<a class="flex flex-col items-center justify-center text-on-surface-variant hover:bg-surface-variant transition-transform active:scale-90" href="#">
|
||||
<span class="material-symbols-outlined">person</span>
|
||||
<span class="font-label-md text-label-md">Profile</span>
|
||||
</a>
|
||||
</nav>
|
||||
<script>
|
||||
// Micro-interaction for checkboxes
|
||||
document.querySelectorAll('input[type="checkbox"]').forEach(checkbox => {
|
||||
checkbox.addEventListener('change', function() {
|
||||
const parent = this.closest('label');
|
||||
if(this.checked) {
|
||||
parent.classList.add('border-primary', 'bg-surface-container-highest');
|
||||
} else {
|
||||
parent.classList.remove('border-primary', 'bg-surface-container-highest');
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body></html>
|
||||
292
stitch_kintex_ai_system_architect/scr_m11/code.html
Normal file
292
stitch_kintex_ai_system_architect/scr_m11/code.html
Normal file
@ -0,0 +1,292 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html class="light" lang="ko"><head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta content="width=390, user-scalable=no" name="viewport"/>
|
||||
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<link href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/static/pretendard.css" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Geist:wght@100..900&family=Inter:wght@100..900&display=swap" rel="stylesheet"/>
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Pretendard', sans-serif;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
.material-symbols-outlined {
|
||||
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
|
||||
}
|
||||
.active-icon {
|
||||
font-variation-settings: 'FILL' 1;
|
||||
}
|
||||
/* Custom scrollbar hide for mobile filter rail */
|
||||
.no-scrollbar::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
.no-scrollbar {
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
</style>
|
||||
<script id="tailwind-config">
|
||||
tailwind.config = {
|
||||
darkMode: "class",
|
||||
theme: {
|
||||
extend: {
|
||||
"colors": {
|
||||
"on-surface-variant": "#414751",
|
||||
"primary-container": "#0066b3",
|
||||
"error": "#ba1a1a",
|
||||
"inverse-on-surface": "#edf0ff",
|
||||
"secondary-container": "#6d4aff",
|
||||
"on-primary": "#ffffff",
|
||||
"on-secondary": "#ffffff",
|
||||
"on-primary-fixed": "#001c38",
|
||||
"on-primary-container": "#d2e3ff",
|
||||
"tertiary-fixed": "#8ef7c4",
|
||||
"on-primary-fixed-variant": "#004881",
|
||||
"surface-tint": "#0060a9",
|
||||
"surface": "#f9f9ff",
|
||||
"on-background": "#141b2c",
|
||||
"outline-variant": "#c1c7d3",
|
||||
"secondary-fixed": "#e5deff",
|
||||
"on-tertiary-container": "#8ef7c3",
|
||||
"secondary-fixed-dim": "#c9bfff",
|
||||
"surface-container": "#e9edff",
|
||||
"tertiary-fixed-dim": "#72daa9",
|
||||
"surface-container-low": "#f1f3ff",
|
||||
"secondary": "#5427e6",
|
||||
"on-surface": "#141b2c",
|
||||
"surface-variant": "#dbe2f9",
|
||||
"on-secondary-container": "#f3edff",
|
||||
"background": "#f9f9ff",
|
||||
"primary": "#004e8b",
|
||||
"on-tertiary-fixed": "#002113",
|
||||
"tertiary-container": "#00734e",
|
||||
"surface-container-high": "#e0e8ff",
|
||||
"on-secondary-fixed": "#1b0063",
|
||||
"surface-container-lowest": "#ffffff",
|
||||
"tertiary": "#00583b",
|
||||
"primary-fixed-dim": "#a2c9ff",
|
||||
"surface-container-highest": "#dbe2f9",
|
||||
"on-error-container": "#93000a",
|
||||
"error-container": "#ffdad6",
|
||||
"surface-dim": "#d2daf0",
|
||||
"surface-bright": "#f9f9ff",
|
||||
"inverse-surface": "#293041",
|
||||
"on-secondary-fixed-variant": "#4500d8",
|
||||
"outline": "#717782",
|
||||
"inverse-primary": "#a2c9ff",
|
||||
"primary-fixed": "#d3e4ff",
|
||||
"on-tertiary": "#ffffff",
|
||||
"on-tertiary-fixed-variant": "#005236",
|
||||
"on-error": "#ffffff"
|
||||
},
|
||||
"borderRadius": {
|
||||
"DEFAULT": "0.125rem",
|
||||
"lg": "0.25rem",
|
||||
"xl": "0.5rem",
|
||||
"full": "0.75rem"
|
||||
},
|
||||
"spacing": {
|
||||
"stack-md": "16px",
|
||||
"stack-sm": "8px",
|
||||
"container-padding": "24px",
|
||||
"base": "4px",
|
||||
"stack-lg": "24px",
|
||||
"gutter": "16px"
|
||||
},
|
||||
"fontFamily": {
|
||||
"display-lg-mobile": ["Inter"],
|
||||
"headline-md": ["Inter"],
|
||||
"label-md": ["Geist"],
|
||||
"display-lg": ["Inter"],
|
||||
"title-sm": ["Inter"],
|
||||
"label-xs": ["Geist"],
|
||||
"body-sm": ["Inter"],
|
||||
"body-base": ["Inter"]
|
||||
},
|
||||
"fontSize": {
|
||||
"display-lg-mobile": ["28px", {"lineHeight": "36px", "fontWeight": "700"}],
|
||||
"headline-md": ["24px", {"lineHeight": "32px", "fontWeight": "600"}],
|
||||
"label-md": ["12px", {"lineHeight": "16px", "letterSpacing": "0.02em", "fontWeight": "600"}],
|
||||
"display-lg": ["36px", {"lineHeight": "44px", "letterSpacing": "-0.02em", "fontWeight": "700"}],
|
||||
"title-sm": ["18px", {"lineHeight": "28px", "fontWeight": "600"}],
|
||||
"label-xs": ["11px", {"lineHeight": "14px", "fontWeight": "500"}],
|
||||
"body-sm": ["13px", {"lineHeight": "18px", "fontWeight": "400"}],
|
||||
"body-base": ["14px", {"lineHeight": "20px", "fontWeight": "400"}]
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
body {
|
||||
min-height: max(884px, 100dvh);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-background text-on-surface antialiased select-none">
|
||||
<!-- TopAppBar -->
|
||||
<header class="fixed top-0 w-full z-50 flex justify-between items-center px-container-padding h-16 bg-surface border-b border-outline-variant transition-colors duration-200">
|
||||
<button aria-label="Back" class="w-10 h-10 flex items-center justify-start text-primary active:opacity-80 transition-opacity">
|
||||
<span class="material-symbols-outlined text-[24px]">arrow_back</span>
|
||||
</button>
|
||||
<h1 class="font-headline-md text-headline-md font-bold text-primary">알림</h1>
|
||||
<button aria-label="Settings" class="w-10 h-10 flex items-center justify-end text-on-surface-variant active:opacity-80 transition-opacity">
|
||||
<span class="material-symbols-outlined text-[24px]">settings</span>
|
||||
</button>
|
||||
</header>
|
||||
<main class="pt-16 pb-24">
|
||||
<!-- Filter Rail -->
|
||||
<section class="mt-4 px-container-padding">
|
||||
<div class="flex overflow-x-auto no-scrollbar gap-2 py-2">
|
||||
<button class="whitespace-nowrap px-4 py-2 rounded-full font-label-md text-label-md bg-primary-container text-on-primary transition-colors">전체</button>
|
||||
<button class="whitespace-nowrap px-4 py-2 rounded-full font-label-md text-label-md bg-surface-container text-on-surface-variant hover:bg-surface-variant transition-colors">마감</button>
|
||||
<button class="whitespace-nowrap px-4 py-2 rounded-full font-label-md text-label-md bg-surface-container text-on-surface-variant hover:bg-surface-variant transition-colors">낙찰</button>
|
||||
<button class="whitespace-nowrap px-4 py-2 rounded-full font-label-md text-label-md bg-surface-container text-on-surface-variant hover:bg-surface-variant transition-colors">승인</button>
|
||||
<button class="whitespace-nowrap px-4 py-2 rounded-full font-label-md text-label-md bg-surface-container text-on-surface-variant hover:bg-surface-variant transition-colors">결제</button>
|
||||
</div>
|
||||
</section>
|
||||
<!-- Action Bar -->
|
||||
<section class="flex justify-between items-center px-container-padding py-4">
|
||||
<span class="font-label-md text-label-md text-on-surface-variant">전체 12개</span>
|
||||
<button class="text-primary font-label-md text-label-md px-2 py-1 active:bg-primary-fixed/20 rounded transition-colors">모두 읽음</button>
|
||||
</section>
|
||||
<!-- Notification List -->
|
||||
<section class="divide-y divide-outline-variant/30">
|
||||
<!-- Item 1 (Unread) -->
|
||||
<div class="flex gap-4 p-container-padding bg-surface-container-lowest active:bg-surface-container transition-colors relative">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="w-12 h-12 rounded-full bg-error-container text-error flex items-center justify-center">
|
||||
<span class="material-symbols-outlined" style="font-variation-settings: 'FILL' 1;">calendar_today</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-grow pr-4">
|
||||
<div class="flex justify-between items-start mb-1">
|
||||
<h3 class="font-title-sm text-on-surface font-bold">전시 부스 신청 마감 임박</h3>
|
||||
<span class="font-label-xs text-label-xs text-outline whitespace-nowrap">방금 전</span>
|
||||
</div>
|
||||
<p class="font-body-sm text-on-surface-variant leading-relaxed">2026 하반기 통합 전시회 부스 신청이 3시간 뒤 마감됩니다.</p>
|
||||
</div>
|
||||
<div class="absolute right-6 top-1/2 -translate-y-1/2">
|
||||
<div class="w-2 h-2 rounded-full bg-primary"></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Item 2 (Unread) -->
|
||||
<div class="flex gap-4 p-container-padding bg-surface-container-lowest active:bg-surface-container transition-colors relative">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="w-12 h-12 rounded-full bg-tertiary-container/20 text-tertiary flex items-center justify-center">
|
||||
<span class="material-symbols-outlined" style="font-variation-settings: 'FILL' 1;">check_circle</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-grow pr-4">
|
||||
<div class="flex justify-between items-start mb-1">
|
||||
<h3 class="font-title-sm text-on-surface font-bold">부스 배치도 승인 완료</h3>
|
||||
<span class="font-label-xs text-label-xs text-outline whitespace-nowrap">2시간 전</span>
|
||||
</div>
|
||||
<p class="font-body-sm text-on-surface-variant leading-relaxed">요청하신 제1전시장 A-102 구역의 배치가 승인되었습니다.</p>
|
||||
</div>
|
||||
<div class="absolute right-6 top-1/2 -translate-y-1/2">
|
||||
<div class="w-2 h-2 rounded-full bg-primary"></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Item 3 -->
|
||||
<div class="flex gap-4 p-container-padding bg-surface-container-lowest active:bg-surface-container transition-colors opacity-80">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="w-12 h-12 rounded-full bg-primary-fixed text-primary flex items-center justify-center">
|
||||
<span class="material-symbols-outlined" style="font-variation-settings: 'FILL' 1;">payments</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-grow">
|
||||
<div class="flex justify-between items-start mb-1">
|
||||
<h3 class="font-title-sm text-on-surface font-bold">전기/통신 사용료 결제 안내</h3>
|
||||
<span class="font-label-xs text-label-xs text-outline whitespace-nowrap">5시간 전</span>
|
||||
</div>
|
||||
<p class="font-body-sm text-on-surface-variant leading-relaxed">신청하신 유틸리티 비용에 대한 세금계산서가 발행되었습니다.</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Item 4 (AI Optimized) -->
|
||||
<div class="flex gap-4 p-container-padding bg-surface-container-lowest active:bg-surface-container transition-colors opacity-80">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="w-12 h-12 rounded-full bg-secondary-container/10 text-secondary border border-secondary/20 flex items-center justify-center">
|
||||
<span class="material-symbols-outlined" style="font-variation-settings: 'FILL' 1;">auto_awesome</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-grow">
|
||||
<div class="flex justify-between items-start mb-1">
|
||||
<h3 class="font-title-sm text-on-surface font-bold text-secondary">AI 관람객 동선 최적화 제안</h3>
|
||||
<span class="font-label-xs text-label-xs text-outline whitespace-nowrap">어제</span>
|
||||
</div>
|
||||
<p class="font-body-sm text-on-surface-variant leading-relaxed">데이터 분석 결과, 입구 혼잡도 개선을 위한 부스 배치 조정안이 생성되었습니다.</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Item 5 -->
|
||||
<div class="flex gap-4 p-container-padding bg-surface-container-lowest active:bg-surface-container transition-colors opacity-80">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="w-12 h-12 rounded-full bg-surface-container text-on-surface-variant flex items-center justify-center">
|
||||
<span class="material-symbols-outlined" style="font-variation-settings: 'FILL' 1;">gavel</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-grow">
|
||||
<div class="flex justify-between items-start mb-1">
|
||||
<h3 class="font-title-sm text-on-surface font-bold">잔여 부스 공개 입찰 낙찰</h3>
|
||||
<span class="font-label-xs text-label-xs text-outline whitespace-nowrap">2일 전</span>
|
||||
</div>
|
||||
<p class="font-body-sm text-on-surface-variant leading-relaxed">(주)킨텍스글로벌이 제2전시장 C-203 구역 낙찰자로 선정되었습니다.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
<!-- BottomNavBar -->
|
||||
<nav class="fixed bottom-0 w-full z-50 flex justify-around items-center px-gutter pb-safe h-20 bg-surface-container border-t border-outline-variant">
|
||||
<button class="flex flex-col items-center justify-center text-on-surface-variant px-4 py-1 transition-all duration-200 scale-95 active:scale-90">
|
||||
<span class="material-symbols-outlined">home</span>
|
||||
<span class="font-label-md text-label-md mt-1">홈</span>
|
||||
</button>
|
||||
<button class="flex flex-col items-center justify-center text-on-surface-variant px-4 py-1 transition-all duration-200 scale-95 active:scale-90">
|
||||
<span class="material-symbols-outlined">grid_view</span>
|
||||
<span class="font-label-md text-label-md mt-1">부스 관리</span>
|
||||
</button>
|
||||
<!-- ACTIVE TAB -->
|
||||
<button class="flex flex-col items-center justify-center bg-secondary-container text-on-secondary-container rounded-full px-4 py-1 transition-all duration-200 scale-95 active:scale-90">
|
||||
<span class="material-symbols-outlined active-icon">notifications</span>
|
||||
<span class="font-label-md text-label-md mt-1">알림</span>
|
||||
</button>
|
||||
<button class="flex flex-col items-center justify-center text-on-surface-variant px-4 py-1 transition-all duration-200 scale-95 active:scale-90">
|
||||
<span class="material-symbols-outlined">analytics</span>
|
||||
<span class="font-label-md text-label-md mt-1">통계</span>
|
||||
</button>
|
||||
<button class="flex flex-col items-center justify-center text-on-surface-variant px-4 py-1 transition-all duration-200 scale-95 active:scale-90">
|
||||
<span class="material-symbols-outlined">menu</span>
|
||||
<span class="font-label-md text-label-md mt-1">더보기</span>
|
||||
</button>
|
||||
</nav>
|
||||
<script>
|
||||
// Simple filter button interaction
|
||||
const filterButtons = document.querySelectorAll('section div button');
|
||||
filterButtons.forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
filterButtons.forEach(b => {
|
||||
b.classList.remove('bg-primary-container', 'text-on-primary');
|
||||
b.classList.add('bg-surface-container', 'text-on-surface-variant');
|
||||
});
|
||||
btn.classList.remove('bg-surface-container', 'text-on-surface-variant');
|
||||
btn.classList.add('bg-primary-container', 'text-on-primary');
|
||||
});
|
||||
});
|
||||
|
||||
// Click interaction for notification rows
|
||||
const notifications = document.querySelectorAll('section > div');
|
||||
notifications.forEach(notif => {
|
||||
notif.addEventListener('click', () => {
|
||||
const dot = notif.querySelector('.bg-primary.w-2.h-2');
|
||||
if (dot) {
|
||||
dot.classList.add('opacity-0', 'transition-opacity');
|
||||
setTimeout(() => dot.remove(), 200);
|
||||
}
|
||||
notif.classList.add('opacity-80');
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body></html>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user