From ca4dff559caa39a584afde0a3f1eede9157e14f0 Mon Sep 17 00:00:00 2001 From: cherry2250 Date: Fri, 24 Oct 2025 10:37:11 +0900 Subject: [PATCH] =?UTF-8?q?7=EA=B0=9C=20=EB=A7=88=EC=9D=B4=ED=81=AC?= =?UTF-8?q?=EB=A1=9C=EC=84=9C=EB=B9=84=EC=8A=A4=20=EB=B0=98=EC=98=81?= =?UTF-8?q?=ED=95=98=EC=97=AC=20=ED=94=84=EB=A1=A0=ED=8A=B8=EC=97=94?= =?UTF-8?q?=EB=93=9C=20=EC=84=A4=EA=B3=84=EC=84=9C=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?(ia.md,=20api-mapping.md)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit πŸ€– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- claude/frontend-design.md | 77 ++ design/frontend/api-mapping.md | 1158 ++++++++++++++++++++++++ design/frontend/ia.md | 614 +++++++++++++ design/frontend/style-guide.md | 1554 ++++++++++++++++++++++++++++++++ design/frontend/uiux-design.md | 502 +++++++++++ 5 files changed, 3905 insertions(+) create mode 100644 claude/frontend-design.md create mode 100644 design/frontend/api-mapping.md create mode 100644 design/frontend/ia.md create mode 100644 design/frontend/style-guide.md create mode 100644 design/frontend/uiux-design.md diff --git a/claude/frontend-design.md b/claude/frontend-design.md new file mode 100644 index 0000000..7c08c05 --- /dev/null +++ b/claude/frontend-design.md @@ -0,0 +1,77 @@ +# ν”„λ‘ νŠΈμ—”λ“œμ„€κ³„κ°€μ΄λ“œ + +[μš”μ²­μ‚¬ν•­] +- <섀계원칙>을 μ€€μš©ν•˜μ—¬ 섀계 +- <μ„€κ³„μˆœμ„œ>에 따라 섀계 +- [결과파일] μ•ˆλ‚΄μ— 따라 파일 μž‘μ„± + +[κ°€μ΄λ“œ] +<섀계원칙> +- κΈ°μˆ μŠ€νƒ: TypeScript 5.5 + React 18.3 + Vite 5.4 +- ν”„λ‘œν† νƒ€μž…κ³Ό λ™μΌν•˜κ²Œ 섀계 +- 각 λ°±μ—”λ“œμ„œλΉ„μŠ€ APIλͺ…μ„Έμ„œμ™€ λ°˜λ“œμ‹œ 일치 +- λͺ¨λ°”일, νƒœλΈ”λ¦Ώ, μ›Ή ν™”λ©΄ 크기에 맞게 λ°˜μ‘ν˜•μœΌλ‘œ λ””μžμΈ + +<μ„€κ³„μˆœμ„œ> +- μ€€λΉ„: + - ν”„λ‘œν† νƒ€μž… 뢄석: '../{μ‹œμŠ€ν…œ}/design/uiux/prototype'λ””λ ‰ν† λ¦¬μ˜ λͺ¨λ“  νŒŒμΌμ„ 'design/prototype' λ””λ ‰ν† λ¦¬λ‘œ λ³΅μ‚¬ν•˜κ³  뢄석 및 이해 + - API 뢄석: "[λ°±μ—”λ“œμ‹œμŠ€ν…œ]"μ„Ήμ…˜μ˜ 정보λ₯Ό μ΄μš©ν•˜μ—¬ APIλͺ…μ„Έμ„œλ₯Ό 'design/api'에 λ‹€μš΄λ‘œλ“œν•˜μ—¬ 뢄석 및 이해 + - ν™”λ©΄μš”κ΅¬μ‚¬ν•­ 뢄석: "[μš”κ΅¬μ‚¬ν•­]" μ„Ήμ…˜μ„ 읽어 ν™”λ©΄ μš”κ΅¬μ‚¬ν•­ 이해 + +- 섀계: + - 1. **UI/UX 섀계** + - 1.1 UIν”„λ ˆμž„μ›Œν¬ 선택: MUI, Ant Design, Chakra UI, Mantine, React Bootstrap λ“± + - 1.2 ν™”λ©΄λͺ©λ‘ μ •μ˜ + - 1.3 ν™”λ©΄ κ°„ μ‚¬μš©μž ν”Œλ‘œμš° μ •μ˜ + - 1.4 화면별 상세 섀계: + - 1.4.1 상세기λŠ₯ + - 1.4.2 UI κ΅¬μ„±μš”μ†Œ + - 1.4.3 μΈν„°λž™μ…˜ + - 1.5 ν™”λ©΄κ°„ μ „ν™˜ 및 λ„€λΉ„κ²Œμ΄μ…˜ + - 1.6 λ°˜μ‘ν˜• 섀계 μ „λž΅ + - 1.7 μ ‘κ·Όμ„± 보μž₯ λ°©μ•ˆ + - 1.8 μ„±λŠ₯ μ΅œμ ν™” λ°©μ•ˆ + + - 2. **μŠ€νƒ€μΌκ°€μ΄λ“œ μž‘μ„±**: + APIλͺ…μ„Έμ„œ 뢄석 결과와 μ„ νƒν•œ UIν”„λ ˆμž„μ›Œν¬ νŠΉμ„±μ„ 반영 + - 2.1 λΈŒλžœλ“œ 아이덴티티: λ””μžμΈ 컨셉 λ“± + - 2.2 λ””μžμΈ 원칙 + - 2.3 컬러 μ‹œμŠ€ν…œ + - 2.4 νƒ€μ΄ν¬κ·Έλž˜ν”Ό + - 2.5 간격 μ‹œμŠ€ν…œ + - 2.6 μ»΄ν¬λ„ŒνŠΈ μŠ€νƒ€μΌ + - 2.7 λ°˜μ‘ν˜• 브레이크포인트 + - 2.8 λŒ€μƒ μ„œλΉ„μŠ€ νŠΉν™” μ»΄ν¬λ„ŒνŠΈ + - 2.9 μΈν„°λž™μ…˜ νŒ¨ν„΄ + + - 3. **정보 μ•„ν‚€ν…μ²˜ 섀계** + - 3.1 μ‚¬μ΄νŠΈλ§΅: νŽ˜μ΄μ§€ ꡬ쑰 및 λ„€λΉ„κ²Œμ΄μ…˜ 흐름 + - 3.2 ν”„λ‘œμ νŠΈ ꡬ쑰 섀계: νŒ¨ν‚€μ§€μ™€ νŒŒμΌκΉŒμ§€ 섀계 + + - 4. **APIλ§€ν•‘μ„€κ³„μ„œ** + - 4.1 API경둜 λ§€ν•‘ + public/runtime-env.jsνŒŒμΌμ„ 읽어 APIκ·Έλ£Ήκ³Ό '"[λ°±μ—”λ“œμ‹œμŠ€ν…œ]"μ„Ήμ…˜μ— μ •μ˜λœ 각 μ„œλΉ„μŠ€λ³„ HOSTλ₯Ό μ§€μ • + μ˜ˆμ‹œ) + ``` + window.__runtime_config__ = { + API_GROUP: "/api/${version:v1}", + USER_HOST: "http://localhost:8081", + ORDER_HOST: "http://localhost:8082" + } + ``` + + - 4.2 **API와 ν™”λ©΄ 상세기λŠ₯ λ§€μΉ­**: '1.4.1 상세기λŠ₯'κ³Ό API λ§€ν•‘ + - ν™”λ©΄, κΈ°λŠ₯, λ°±μ—”λ“œ μ„œλΉ„μŠ€, API경둜, μš”μ²­λ°μ΄ν„° ꡬ쑰, 응닡데이터 ꡬ쑰 λͺ…μ‹œ + - API μš”μ²­λ°μ΄νƒ€μ™€ API 응닡데이터 μ˜ˆμ‹œ + +[참고자료] +- ν”„λ‘œν† νƒ€μž…: design/prototype/* +- APIλͺ…μ„Έμ„œ: design/api/*.json + +[결과파일] +- UI/UXμ„€κ³„μ„œ: design/frontend/uiux-design.md +- μŠ€νƒ€μΌκ°€μ΄λ“œ: design/frontend/style-guide.md +- μ •λ³΄μ•„ν‚€ν…μ²˜: design/frontend/ia.md +- APIλ§€ν•‘μ„€κ³„μ„œ: design/frontend/api-mapping.md + + diff --git a/design/frontend/api-mapping.md b/design/frontend/api-mapping.md new file mode 100644 index 0000000..3fd7015 --- /dev/null +++ b/design/frontend/api-mapping.md @@ -0,0 +1,1158 @@ +# API λ§€ν•‘ μ„€κ³„μ„œ + +## λͺ©μ°¨ +1. [API 경둜 λ§€ν•‘](#1-api-경둜-λ§€ν•‘) +2. [화면별 API λ§€ν•‘](#2-화면별-api-λ§€ν•‘) +3. [API 호좜 μ˜ˆμ‹œ](#3-api-호좜-μ˜ˆμ‹œ) + +--- + +## 1. API 경둜 λ§€ν•‘ + +### 1.1 Runtime ν™˜κ²½ λ³€μˆ˜ μ„€μ • + +**public/runtime-env.js** +```javascript +/** + * λŸ°νƒ€μž„ ν™˜κ²½ λ³€μˆ˜ μ„€μ • + * - λΉŒλ“œ μ‹œμ μ΄ μ•„λ‹Œ μ‹€ν–‰ μ‹œμ μ— ν™˜κ²½λ³„ API 호슀트 μ„€μ • + * - 도컀 μ»¨ν…Œμ΄λ„ˆ ν™˜κ²½μ—μ„œ ν™˜κ²½λ³€μˆ˜ μ£Όμž… κ°€λŠ₯ + */ +window.__runtime_config__ = { + // API κ·Έλ£Ή 경둜 (버전 포함) + API_GROUP: "/api/v1", + + // 7개 λ§ˆμ΄ν¬λ‘œμ„œλΉ„μŠ€ 호슀트 + USER_HOST: process.env.NEXT_PUBLIC_USER_HOST || "http://localhost:8081", + EVENT_HOST: process.env.NEXT_PUBLIC_EVENT_HOST || "http://localhost:8080", + CONTENT_HOST: process.env.NEXT_PUBLIC_CONTENT_HOST || "http://localhost:8082", + AI_HOST: process.env.NEXT_PUBLIC_AI_HOST || "http://localhost:8083", + PARTICIPATION_HOST: process.env.NEXT_PUBLIC_PARTICIPATION_HOST || "http://localhost:8084", + DISTRIBUTION_HOST: process.env.NEXT_PUBLIC_DISTRIBUTION_HOST || "http://localhost:8085", + ANALYTICS_HOST: process.env.NEXT_PUBLIC_ANALYTICS_HOST || "http://localhost:8086", +}; +``` + +### 1.2 API ν΄λΌμ΄μ–ΈνŠΈ μ΄ˆκΈ°ν™” + +**src/lib/api/client.ts** +```typescript +import axios, { AxiosInstance } from 'axios'; + +// λŸ°νƒ€μž„ ν™˜κ²½ λ³€μˆ˜ νƒ€μž… μ •μ˜ +declare global { + interface Window { + __runtime_config__: { + API_GROUP: string; + USER_HOST: string; + EVENT_HOST: string; + CONTENT_HOST: string; + AI_HOST: string; + PARTICIPATION_HOST: string; + DISTRIBUTION_HOST: string; + ANALYTICS_HOST: string; + }; + } +} + +const config = typeof window !== 'undefined' ? window.__runtime_config__ : { + API_GROUP: '/api/v1', + USER_HOST: process.env.NEXT_PUBLIC_USER_HOST || 'http://localhost:8081', + EVENT_HOST: process.env.NEXT_PUBLIC_EVENT_HOST || 'http://localhost:8080', + CONTENT_HOST: process.env.NEXT_PUBLIC_CONTENT_HOST || 'http://localhost:8082', + AI_HOST: process.env.NEXT_PUBLIC_AI_HOST || 'http://localhost:8083', + PARTICIPATION_HOST: process.env.NEXT_PUBLIC_PARTICIPATION_HOST || 'http://localhost:8084', + DISTRIBUTION_HOST: process.env.NEXT_PUBLIC_DISTRIBUTION_HOST || 'http://localhost:8085', + ANALYTICS_HOST: process.env.NEXT_PUBLIC_ANALYTICS_HOST || 'http://localhost:8086', +}; + +// JWT 토큰 κ°€μ Έμ˜€κΈ° 헬퍼 +const getAuthToken = (): string | null => { + if (typeof window === 'undefined') return null; + return localStorage.getItem('accessToken'); +}; + +// Request Interceptor (JWT 토큰 μžλ™ μΆ”κ°€) +const authInterceptor = (instance: AxiosInstance) => { + instance.interceptors.request.use( + (config) => { + const token = getAuthToken(); + if (token) { + config.headers.Authorization = `Bearer ${token}`; + } + return config; + }, + (error) => Promise.reject(error) + ); + + // Response Interceptor (401 처리) + instance.interceptors.response.use( + (response) => response, + (error) => { + if (error.response?.status === 401) { + // 토큰 만료 λ˜λŠ” 인증 μ‹€νŒ¨ + localStorage.removeItem('accessToken'); + window.location.href = '/login'; + } + return Promise.reject(error); + } + ); +}; + +// 1. User Service API Client (인증, ν”„λ‘œν•„ 관리) +export const userClient = axios.create({ + baseURL: `${config.USER_HOST}`, + timeout: 10000, + headers: { + 'Content-Type': 'application/json', + }, +}); +authInterceptor(userClient); + +// 2. Event Service API Client (이벀트 생λͺ…μ£ΌκΈ° 관리) +export const eventClient = axios.create({ + baseURL: `${config.EVENT_HOST}`, + timeout: 30000, // Job 폴링 κ³ λ € + headers: { + 'Content-Type': 'application/json', + }, +}); +authInterceptor(eventClient); + +// 3. Content Service API Client (이미지 생성 및 νŽΈμ§‘) +export const contentClient = axios.create({ + baseURL: `${config.CONTENT_HOST}`, + timeout: 30000, // 이미지 생성 Job 폴링 + headers: { + 'Content-Type': 'application/json', + }, +}); +authInterceptor(contentClient); + +// 4. AI Service API Client (AI μΆ”μ²œ 생성) +export const aiClient = axios.create({ + baseURL: `${config.AI_HOST}`, + timeout: 30000, // AI 생성 Job 폴링 + headers: { + 'Content-Type': 'application/json', + }, +}); +authInterceptor(aiClient); + +// 5. Participation Service API Client (μ°Έμ—¬μž/λ‹Ήμ²¨μž 관리) +export const participationClient = axios.create({ + baseURL: `${config.PARTICIPATION_HOST}`, + timeout: 10000, + headers: { + 'Content-Type': 'application/json', + }, +}); +authInterceptor(participationClient); + +// 6. Distribution Service API Client (닀쀑 채널 배포) +export const distributionClient = axios.create({ + baseURL: `${config.DISTRIBUTION_HOST}`, + timeout: 20000, // 닀쀑 채널 배포 μ‹œκ°„ κ³ λ € + headers: { + 'Content-Type': 'application/json', + }, +}); +authInterceptor(distributionClient); + +// 7. Analytics Service API Client (μ„±κ³Ό 뢄석 및 λŒ€μ‹œλ³΄λ“œ) +export const analyticsClient = axios.create({ + baseURL: `${config.ANALYTICS_HOST}`, + timeout: 10000, + headers: { + 'Content-Type': 'application/json', + }, +}); +authInterceptor(analyticsClient); +``` + +### 1.3 ν™˜κ²½λ³„ μ„€μ • + +| ν™˜κ²½ | User Service | Event Service | Content Service | AI Service | Participation Service | Distribution Service | Analytics Service | +|------|-------------|---------------|----------------|-----------|---------------------|---------------------|-------------------| +| **둜컬 개발** | http://localhost:8081 | http://localhost:8080 | http://localhost:8082 | http://localhost:8083 | http://localhost:8084 | http://localhost:8085 | http://localhost:8086 | +| **개발 μ„œλ²„** | https://dev-api.kt-event-marketing.com/user/v1 | https://dev-api.kt-event-marketing.com/event/v1 | https://dev-api.kt-event-marketing.com/content/v1 | https://dev-api.kt-event-marketing.com/ai/v1 | https://dev-api.kt-event-marketing.com/participation/v1 | https://dev-api.kt-event-marketing.com/distribution/v1 | https://dev-api.kt-event-marketing.com/analytics/v1 | +| **ν”„λ‘œλ•μ…˜** | https://api.kt-event-marketing.com/user/v1 | https://api.kt-event-marketing.com/event/v1 | https://api.kt-event-marketing.com/content/v1 | https://api.kt-event-marketing.com/ai/v1 | https://api.kt-event-marketing.com/participation/v1 | https://api.kt-event-marketing.com/distribution/v1 | https://api.kt-event-marketing.com/analytics/v1 | + +--- + +## 2. 화면별 API λ§€ν•‘ + +### 2.1 인증 μ˜μ—­ + +#### AUTH-01: 둜그인 ν™”λ©΄ + +| κΈ°λŠ₯ | λ°±μ—”λ“œ μ„œλΉ„μŠ€ | API 경둜 | HTTP λ©”μ„œλ“œ | 인증 ν•„μš” | +|-----|------------|---------|-----------|---------| +| 둜그인 | User Service | `/users/login` | POST | ❌ | + +**μš”μ²­ 데이터** +```typescript +interface LoginRequest { + phoneNumber: string; // "010XXXXXXXX" ν˜•μ‹ + password: string; // μ΅œμ†Œ 8자 +} +``` + +**응닡 데이터** +```typescript +interface LoginResponse { + token: string; // JWT 토큰 (7일 만료) + userId: number; + userName: string; + role: 'OWNER' | 'ADMIN'; + email: string; +} +``` + +#### AUTH-02: νšŒμ›κ°€μž… ν™”λ©΄ + +| κΈ°λŠ₯ | λ°±μ—”λ“œ μ„œλΉ„μŠ€ | API 경둜 | HTTP λ©”μ„œλ“œ | 인증 ν•„μš” | +|-----|------------|---------|-----------|---------| +| νšŒμ›κ°€μž… | User Service | `/users/register` | POST | ❌ | + +**μš”μ²­ 데이터** +```typescript +interface RegisterRequest { + name: string; // 2-50자 + phoneNumber: string; // "010XXXXXXXX" + email: string; // 이메일 ν˜•μ‹ + password: string; // 8자 이상, 영문/숫자/특수문자 + storeName: string; // 2-100자 + industry: string; // μ—…μ’… (예: μŒμ‹μ , 카페) + address: string; // 5-200자 + businessHours?: string; // μ˜μ—…μ‹œκ°„ (선택) +} +``` + +**응닡 데이터** +```typescript +interface RegisterResponse { + token: string; // JWT 토큰 (μžλ™ 둜그인) + userId: number; + userName: string; + storeId: number; + storeName: string; +} +``` + +#### AUTH-03: ν”„λ‘œν•„ 관리 ν™”λ©΄ + +| κΈ°λŠ₯ | λ°±μ—”λ“œ μ„œλΉ„μŠ€ | API 경둜 | HTTP λ©”μ„œλ“œ | 인증 ν•„μš” | +|-----|------------|---------|-----------|---------| +| ν”„λ‘œν•„ 쑰회 | User Service | `/users/profile` | GET | βœ… | +| ν”„λ‘œν•„ μˆ˜μ • | User Service | `/users/profile` | PUT | βœ… | +| λΉ„λ°€λ²ˆν˜Έ λ³€κ²½ | User Service | `/users/password` | PUT | βœ… | + +**ν”„λ‘œν•„ 쑰회 응닡** +```typescript +interface ProfileResponse { + userId: number; + userName: string; + phoneNumber: string; + email: string; + role: 'OWNER' | 'ADMIN'; + storeId: number; + storeName: string; + industry: string; + address: string; + businessHours?: string; + createdAt: string; // ISO 8601 + lastLoginAt: string; // ISO 8601 +} +``` + +**ν”„λ‘œν•„ μˆ˜μ • μš”μ²­** +```typescript +interface UpdateProfileRequest { + name?: string; + phoneNumber?: string; + email?: string; + storeName?: string; + industry?: string; + address?: string; + businessHours?: string; +} +``` + +**λΉ„λ°€λ²ˆν˜Έ λ³€κ²½ μš”μ²­** +```typescript +interface ChangePasswordRequest { + currentPassword: string; + newPassword: string; // 8자 이상, 영문/숫자/특수문자 +} +``` + +#### AUTH-04: λ‘œκ·Έμ•„μ›ƒ + +| κΈ°λŠ₯ | λ°±μ—”λ“œ μ„œλΉ„μŠ€ | API 경둜 | HTTP λ©”μ„œλ“œ | 인증 ν•„μš” | +|-----|------------|---------|-----------|---------| +| λ‘œκ·Έμ•„μ›ƒ | User Service | `/users/logout` | POST | βœ… | + +**응닡 데이터** +```typescript +interface LogoutResponse { + success: boolean; + message: string; // "μ•ˆμ „ν•˜κ²Œ λ‘œκ·Έμ•„μ›ƒλ˜μ—ˆμŠ΅λ‹ˆλ‹€" +} +``` + +--- + +### 2.2 λŒ€μ‹œλ³΄λ“œ μ˜μ—­ + +#### DASH-01: 메인 λŒ€μ‹œλ³΄λ“œ + +| κΈ°λŠ₯ | λ°±μ—”λ“œ μ„œλΉ„μŠ€ | API 경둜 | HTTP λ©”μ„œλ“œ | 인증 ν•„μš” | +|-----|------------|---------|-----------|---------| +| 이벀트 μš”μ•½ 쑰회 | Event Service | `/events?status=PUBLISHED&page=0&size=5` | GET | βœ… | +| 졜근 이벀트 쑰회 | Event Service | `/events?sort=createdAt&order=desc&page=0&size=3` | GET | βœ… | + +**이벀트 λͺ©λ‘ 응닡** +```typescript +interface EventListResponse { + content: EventSummary[]; + page: PageInfo; +} + +interface EventSummary { + eventId: string; // UUID + eventName: string; + objective: string; // "μ‹ κ·œ 고객 유치", "재방문 μœ λ„" λ“± + status: 'DRAFT' | 'PUBLISHED' | 'ENDED'; + startDate: string; // "YYYY-MM-DD" + endDate: string; // "YYYY-MM-DD" + thumbnailUrl?: string; + createdAt: string; // ISO 8601 +} + +interface PageInfo { + page: number; // ν˜„μž¬ νŽ˜μ΄μ§€ (0λΆ€ν„° μ‹œμž‘) + size: number; // νŽ˜μ΄μ§€ 크기 + totalElements: number; // 전체 μš”μ†Œ 수 + totalPages: number; // 전체 νŽ˜μ΄μ§€ 수 +} +``` + +#### DASH-02: 이벀트 λͺ©λ‘ ν™”λ©΄ + +| κΈ°λŠ₯ | λ°±μ—”λ“œ μ„œλΉ„μŠ€ | API 경둜 | HTTP λ©”μ„œλ“œ | 인증 ν•„μš” | +|-----|------------|---------|-----------|---------| +| 전체 이벀트 쑰회 | Event Service | `/events` | GET | βœ… | +| μƒνƒœλ³„ 필터링 | Event Service | `/events?status={status}` | GET | βœ… | +| 검색 | Event Service | `/events?search={keyword}` | GET | βœ… | + +**Query Parameters** +- `status`: DRAFT, PUBLISHED, ENDED +- `objective`: 이벀트 λͺ©μ  ν•„ν„° +- `search`: 이벀트λͺ… 검색 +- `page`: νŽ˜μ΄μ§€ 번호 (κΈ°λ³Έ 0) +- `size`: νŽ˜μ΄μ§€ 크기 (κΈ°λ³Έ 20, μ΅œλŒ€ 100) +- `sort`: μ •λ ¬ κΈ°μ€€ (createdAt, startDate, endDate) +- `order`: μ •λ ¬ μˆœμ„œ (asc, desc) + +--- + +### 2.3 이벀트 생성 ν”Œλ‘œμš° (Funnel) + +#### EVENT-01: λͺ©μ  선택 (Step 1) + +| κΈ°λŠ₯ | λ°±μ—”λ“œ μ„œλΉ„μŠ€ | API 경둜 | HTTP λ©”μ„œλ“œ | 인증 ν•„μš” | +|-----|------------|---------|-----------|---------| +| λͺ©μ  선택 및 이벀트 생성 | Event Service | `/events/objectives` | POST | βœ… | + +**μš”μ²­ 데이터** +```typescript +interface SelectObjectiveRequest { + objective: string; // "μ‹ κ·œ 고객 유치", "재방문 μœ λ„", "맀좜 μ¦λŒ€", "λΈŒλžœλ“œ 인지도 ν–₯상" +} +``` + +**응닡 데이터** +```typescript +interface EventCreatedResponse { + eventId: string; // UUID (μƒμ„±λœ 이벀트 ID) + status: 'DRAFT'; // 항상 DRAFT + objective: string; + createdAt: string; // ISO 8601 +} +``` + +#### EVENT-02: AI μΆ”μ²œ 확인 (Step 2) + +| κΈ°λŠ₯ | λ°±μ—”λ“œ μ„œλΉ„μŠ€ | API 경둜 | HTTP λ©”μ„œλ“œ | 인증 ν•„μš” | +|-----|------------|---------|-----------|---------| +| AI μΆ”μ²œ μš”μ²­ | Event Service | `/events/{eventId}/ai-recommendations` | POST | βœ… | +| Job μƒνƒœ 폴링 | Event Service | `/jobs/{jobId}` | GET | βœ… | +| AI μΆ”μ²œ 선택 | Event Service | `/events/{eventId}/recommendations` | PUT | βœ… | + +**AI μΆ”μ²œ μš”μ²­** +```typescript +interface AiRecommendationRequest { + storeInfo: { + storeId: string; + storeName: string; + category: string; + description?: string; + }; +} +``` + +**Job μ ‘μˆ˜ 응닡 (202 Accepted)** +```typescript +interface JobAcceptedResponse { + jobId: string; // UUID + status: 'PENDING'; + message: string; // "AI μΆ”μ²œ 생성 μš”μ²­μ΄ μ ‘μˆ˜λ˜μ—ˆμŠ΅λ‹ˆλ‹€..." +} +``` + +**Job μƒνƒœ 쑰회 응닡** +```typescript +interface JobStatusResponse { + jobId: string; + jobType: 'AI_RECOMMENDATION' | 'IMAGE_GENERATION'; + status: 'PENDING' | 'PROCESSING' | 'COMPLETED' | 'FAILED'; + progress: number; // 0-100 (%) + resultKey?: string; // Redis κ²°κ³Ό ν‚€ (COMPLETED μ‹œ) + errorMessage?: string; // μ—λŸ¬ λ©”μ‹œμ§€ (FAILED μ‹œ) + createdAt: string; + completedAt?: string; +} +``` + +**AI μΆ”μ²œ 선택 μš”μ²­** +```typescript +interface SelectRecommendationRequest { + recommendationId: string; // μ„ νƒν•œ μΆ”μ²œ ID + customizations?: { + eventName?: string; + description?: string; + startDate?: string; // "YYYY-MM-DD" + endDate?: string; // "YYYY-MM-DD" + discountRate?: number; + }; +} +``` + +#### EVENT-03: μ½˜ν…μΈ  미리보기 (Step 3) + +| κΈ°λŠ₯ | λ°±μ—”λ“œ μ„œλΉ„μŠ€ | API 경둜 | HTTP λ©”μ„œλ“œ | 인증 ν•„μš” | +|-----|------------|---------|-----------|---------| +| 이미지 생성 μš”μ²­ | Event Service | `/events/{eventId}/images` | POST | βœ… | +| Job μƒνƒœ 폴링 | Event Service | `/jobs/{jobId}` | GET | βœ… | +| 이미지 선택 | Event Service | `/events/{eventId}/images/{imageId}/select` | PUT | βœ… | + +**이미지 생성 μš”μ²­** +```typescript +interface ImageGenerationRequest { + eventInfo: { + eventName: string; + description: string; + promotionType: string; // "할인", "증정", "쿠폰" λ“± + }; + imageCount?: number; // 1-5, κΈ°λ³Έ 3 +} +``` + +#### EVENT-04: μ½˜ν…μΈ  νŽΈμ§‘ (Step 4) + +| κΈ°λŠ₯ | λ°±μ—”λ“œ μ„œλΉ„μŠ€ | API 경둜 | HTTP λ©”μ„œλ“œ | 인증 ν•„μš” | +|-----|------------|---------|-----------|---------| +| 이미지 νŽΈμ§‘ | Event Service | `/events/{eventId}/images/{imageId}/edit` | PUT | βœ… | + +**이미지 νŽΈμ§‘ μš”μ²­** +```typescript +interface ImageEditRequest { + editType: 'TEXT_OVERLAY' | 'COLOR_ADJUST' | 'CROP' | 'FILTER'; + parameters: { + // TEXT_OVERLAY μ˜ˆμ‹œ + text?: string; + fontSize?: number; + color?: string; // HEX μ½”λ“œ + position?: 'center' | 'top' | 'bottom'; + + // COLOR_ADJUST μ˜ˆμ‹œ + brightness?: number; // -100 ~ 100 + contrast?: number; // -100 ~ 100 + saturation?: number; // -100 ~ 100 + }; +} +``` + +**이미지 νŽΈμ§‘ 응닡** +```typescript +interface ImageEditResponse { + imageId: string; + imageUrl: string; // νŽΈμ§‘λœ 이미지 URL + editedAt: string; +} +``` + +#### EVENT-05: 배포 채널 선택 (Step 5) + +| κΈ°λŠ₯ | λ°±μ—”λ“œ μ„œλΉ„μŠ€ | API 경둜 | HTTP λ©”μ„œλ“œ | 인증 ν•„μš” | +|-----|------------|---------|-----------|---------| +| 배포 채널 선택 | Event Service | `/events/{eventId}/channels` | PUT | βœ… | + +**배포 채널 선택 μš”μ²­** +```typescript +interface SelectChannelsRequest { + channels: ('WEBSITE' | 'KAKAO' | 'INSTAGRAM' | 'FACEBOOK' | 'NAVER_BLOG')[]; + // μ΅œμ†Œ 1개 이상 선택 +} +``` + +#### EVENT-06: μ΅œμ’… 승인 (Step 6) + +| κΈ°λŠ₯ | λ°±μ—”λ“œ μ„œλΉ„μŠ€ | API 경둜 | HTTP λ©”μ„œλ“œ | 인증 ν•„μš” | +|-----|------------|---------|-----------|---------| +| 이벀트 배포 | Event Service | `/events/{eventId}/publish` | POST | βœ… | + +**이벀트 배포 응닡** +```typescript +interface EventPublishedResponse { + eventId: string; + status: 'PUBLISHED'; + publishedAt: string; + channels: string[]; // 배포된 채널 λͺ©λ‘ + distributionResults: DistributionResult[]; +} + +interface DistributionResult { + channel: string; // "WEBSITE", "KAKAO" λ“± + success: boolean; + url?: string; // 배포된 URL + message: string; +} +``` + +--- + +### 2.4 이벀트 관리 μ˜μ—­ + +#### MANAGE-01: 이벀트 상세 ν™”λ©΄ + +| κΈ°λŠ₯ | λ°±μ—”λ“œ μ„œλΉ„μŠ€ | API 경둜 | HTTP λ©”μ„œλ“œ | 인증 ν•„μš” | +|-----|------------|---------|-----------|---------| +| 이벀트 상세 쑰회 | Event Service | `/events/{eventId}` | GET | βœ… | +| 이벀트 μˆ˜μ • | Event Service | `/events/{eventId}` | PUT | βœ… | +| 이벀트 μ‚­μ œ | Event Service | `/events/{eventId}` | DELETE | βœ… | +| 이벀트 μ‘°κΈ° μ’…λ£Œ | Event Service | `/events/{eventId}/end` | POST | βœ… | + +**이벀트 상세 응닡** +```typescript +interface EventDetailResponse { + eventId: string; + userId: string; + storeId: string; + eventName: string; + objective: string; + description: string; + targetAudience: string; // "20-30λŒ€ μ—¬μ„±" λ“± + promotionType: string; // "할인", "증정", "쿠폰" + discountRate?: number; // ν• μΈμœ¨ (%) + startDate: string; + endDate: string; + status: 'DRAFT' | 'PUBLISHED' | 'ENDED'; + selectedImageId?: string; + selectedImageUrl?: string; + generatedImages?: GeneratedImage[]; + channels?: string[]; + aiRecommendations?: AiRecommendation[]; + createdAt: string; + updatedAt: string; +} + +interface GeneratedImage { + imageId: string; + imageUrl: string; + isSelected: boolean; + createdAt: string; +} + +interface AiRecommendation { + recommendationId: string; + eventName: string; + description: string; + promotionType: string; + targetAudience: string; + isSelected: boolean; +} +``` + +**이벀트 μˆ˜μ • μš”μ²­** +```typescript +interface UpdateEventRequest { + eventName?: string; + description?: string; + startDate?: string; + endDate?: string; + discountRate?: number; +} +``` + +**이벀트 μ‘°κΈ° μ’…λ£Œ μš”μ²­** +```typescript +interface EndEventRequest { + reason: string; // "λͺ©ν‘œ λ‹¬μ„±μœΌλ‘œ μ‘°κΈ° μ’…λ£Œ" λ“± +} +``` + +#### MANAGE-02: μ°Έμ—¬μž λͺ©λ‘ ν™”λ©΄ + +| κΈ°λŠ₯ | λ°±μ—”λ“œ μ„œλΉ„μŠ€ | API 경둜 | HTTP λ©”μ„œλ“œ | 인증 ν•„μš” | +|-----|------------|---------|-----------|---------| +| μ°Έμ—¬μž λͺ©λ‘ 쑰회 | Participation Service | `/events/{eventId}/participants` | GET | βœ… | +| μ°Έμ—¬μž 상세 쑰회 | Participation Service | `/events/{eventId}/participants/{participantId}` | GET | βœ… | + +**μ°Έμ—¬μž λͺ©λ‘ 응닡** +```typescript +interface ParticipantListResponse { + success: boolean; + message: string; + data: { + participants: ParticipantInfo[]; + pagination: Pagination; + }; +} + +interface ParticipantInfo { + participantId: string; + eventId: string; + name: string; + phoneNumber: string; // "010-1234-5678" ν˜•μ‹ + email?: string; + participatedAt: string; // ISO 8601 + storeVisited: boolean; + bonusEntries: number; // λ³΄λ„ˆμŠ€ 응λͺ¨κΆŒ (λ§€μž₯ λ°©λ¬Έ μ‹œ +1) + isWinner: boolean; +} + +interface Pagination { + currentPage: number; + pageSize: number; + totalElements: number; + totalPages: number; + hasNext: boolean; + hasPrevious: boolean; +} +``` + +**Query Parameters** +- `page`: νŽ˜μ΄μ§€ 번호 (κΈ°λ³Έ 0) +- `size`: νŽ˜μ΄μ§€ 크기 (κΈ°λ³Έ 20, μ΅œλŒ€ 100) +- `storeVisited`: λ§€μž₯ λ°©λ¬Έ μ—¬λΆ€ ν•„ν„° (true/false) + +#### MANAGE-03: 고객 μ°Έμ—¬ ν™”λ©΄ (Public) + +| κΈ°λŠ₯ | λ°±μ—”λ“œ μ„œλΉ„μŠ€ | API 경둜 | HTTP λ©”μ„œλ“œ | 인증 ν•„μš” | +|-----|------------|---------|-----------|---------| +| 이벀트 μ°Έμ—¬ | Participation Service | `/events/{eventId}/participate` | POST | ❌ | +| 이벀트 정보 쑰회 | Event Service | `/events/{eventId}` | GET | ❌ | + +**μ°Έμ—¬ μš”μ²­** +```typescript +interface ParticipationRequest { + name: string; // 2-50자 + phoneNumber: string; // "010-XXXX-XXXX" ν˜•μ‹ + email?: string; + agreeMarketing: boolean; // λ§ˆμΌ€νŒ… 정보 μˆ˜μ‹  λ™μ˜ + agreePrivacy: boolean; // κ°œμΈμ •λ³΄ μˆ˜μ§‘ λ™μ˜ (ν•„μˆ˜) + storeVisited: boolean; // λ§€μž₯ λ°©λ¬Έ μ—¬λΆ€ (λ³΄λ„ˆμŠ€ 응λͺ¨κΆŒ) +} +``` + +**μ°Έμ—¬ 응닡** +```typescript +interface ParticipationResponse { + success: boolean; + message: string; // "이벀트 μ°Έμ—¬κ°€ μ™„λ£Œλ˜μ—ˆμŠ΅λ‹ˆλ‹€" + data: ParticipantInfo; +} +``` + +#### MANAGE-04: λ‹Ήμ²¨μž 좔첨 ν™”λ©΄ + +| κΈ°λŠ₯ | λ°±μ—”λ“œ μ„œλΉ„μŠ€ | API 경둜 | HTTP λ©”μ„œλ“œ | 인증 ν•„μš” | +|-----|------------|---------|-----------|---------| +| λ‹Ήμ²¨μž 좔첨 | Participation Service | `/events/{eventId}/draw-winners` | POST | βœ… | +| λ‹Ήμ²¨μž λͺ©λ‘ 쑰회 | Participation Service | `/events/{eventId}/winners` | GET | βœ… | + +**λ‹Ήμ²¨μž 좔첨 μš”μ²­** +```typescript +interface DrawWinnersRequest { + winnerCount: number; // λ‹Ήμ²¨μž 수 (μ΅œμ†Œ 1) + applyStoreVisitBonus?: boolean; // λ§€μž₯ λ°©λ¬Έ λ³΄λ„ˆμŠ€ 적용 (κΈ°λ³Έ true) +} +``` + +**λ‹Ήμ²¨μž 좔첨 응닡** +```typescript +interface DrawWinnersResponse { + success: boolean; + message: string; + data: { + eventId: string; + totalParticipants: number; + winnerCount: number; + drawnAt: string; + winners: WinnerSummary[]; + }; +} + +interface WinnerSummary { + participantId: string; + name: string; + phoneNumber: string; + rank: number; // 당첨 μˆœμœ„ (1λ“±, 2λ“±, ...) +} +``` + +**λ‹Ήμ²¨μž λͺ©λ‘ 응닡** +```typescript +interface WinnerListResponse { + success: boolean; + message: string; + data: { + eventId: string; + drawnAt: string; + totalWinners: number; + winners: WinnerInfo[]; + pagination: Pagination; + }; +} + +interface WinnerInfo { + participantId: string; + name: string; + phoneNumber: string; + email?: string; + rank: number; + wonAt: string; +} +``` + +#### MANAGE-05: μ„±κ³Ό 뢄석 ν™”λ©΄ + +| κΈ°λŠ₯ | λ°±μ—”λ“œ μ„œλΉ„μŠ€ | API 경둜 | HTTP λ©”μ„œλ“œ | 인증 ν•„μš” | +|-----|------------|---------|-----------|---------| +| 이벀트 상세 쑰회 | Event Service | `/events/{eventId}` | GET | βœ… | +| μ°Έμ—¬μž 톡계 | Participation Service | `/events/{eventId}/participants` | GET | βœ… | + +**μ„±κ³Ό λΆ„μ„μš© 데이터 μ‘°ν•©** +- 이벀트 κΈ°λ³Έ 정보 (Event Service) +- μ°Έμ—¬μž 수, λ§€μž₯ 방문율 (Participation Service) +- λ‹Ήμ²¨μž 수 (Participation Service) + +**계산 μ§€ν‘œ** +```typescript +interface EventAnalytics { + // 이벀트 정보 + eventName: string; + eventPeriod: { start: string; end: string }; + status: string; + + // μ°Έμ—¬ μ§€ν‘œ + totalParticipants: number; + storeVisitCount: number; + storeVisitRate: number; // (λ§€μž₯ λ°©λ¬Έ / 전체 μ°Έμ—¬) * 100 + + // 당첨 μ§€ν‘œ + totalWinners: number; + winnerDrawnDate?: string; + + // 채널별 배포 ν˜„ν™© + distributionChannels: string[]; + + // μ‹œκ°„λŒ€λ³„ μ°Έμ—¬ 좔이 (ν”„λ‘ νŠΈμ—”λ“œμ—μ„œ 계산) + participationTrend: { + date: string; + count: number; + }[]; +} +``` + +--- + +## 3. API 호좜 μ˜ˆμ‹œ + +### 3.1 둜그인 ν”Œλ‘œμš° + +```typescript +// src/lib/api/auth.ts +import { userClient } from './client'; + +export const authApi = { + // 둜그인 + async login(phoneNumber: string, password: string) { + const response = await userClient.post('/users/login', { + phoneNumber, + password, + }); + + // JWT 토큰 μ €μž₯ + localStorage.setItem('accessToken', response.data.token); + + return response.data; + }, + + // λ‘œκ·Έμ•„μ›ƒ + async logout() { + const response = await userClient.post('/users/logout'); + + // 토큰 μ‚­μ œ + localStorage.removeItem('accessToken'); + + return response.data; + }, + + // νšŒμ›κ°€μž… + async register(data: RegisterRequest) { + const response = await userClient.post('/users/register', data); + + // μžλ™ 둜그인 (JWT 토큰 μ €μž₯) + localStorage.setItem('accessToken', response.data.token); + + return response.data; + }, +}; +``` + +**호좜 μ˜ˆμ‹œ** +```typescript +// μ»΄ν¬λ„ŒνŠΈμ—μ„œ μ‚¬μš© +const handleLogin = async () => { + try { + const result = await authApi.login('01012345678', 'Password123!'); + console.log('둜그인 성곡:', result.userName); + router.push('/'); // λŒ€μ‹œλ³΄λ“œλ‘œ 이동 + } catch (error) { + console.error('둜그인 μ‹€νŒ¨:', error); + toast.error('λ‘œκ·ΈμΈμ— μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€'); + } +}; +``` + +### 3.2 이벀트 생성 ν”Œλ‘œμš° + +```typescript +// src/lib/api/events.ts +import { eventClient } from './client'; + +export const eventApi = { + // Step 1: λͺ©μ  선택 + async selectObjective(objective: string) { + const response = await eventClient.post('/events/objectives', { + objective, + }); + return response.data; // { eventId, status, objective, createdAt } + }, + + // Step 2: AI μΆ”μ²œ μš”μ²­ + async requestAiRecommendations(eventId: string, storeInfo: any) { + const response = await eventClient.post( + `/events/${eventId}/ai-recommendations`, + { storeInfo } + ); + return response.data; // { jobId, status, message } + }, + + // Job μƒνƒœ 폴링 + async getJobStatus(jobId: string) { + const response = await eventClient.get(`/jobs/${jobId}`); + return response.data; + }, + + // AI μΆ”μ²œ 선택 + async selectRecommendation( + eventId: string, + recommendationId: string, + customizations?: any + ) { + const response = await eventClient.put( + `/events/${eventId}/recommendations`, + { recommendationId, customizations } + ); + return response.data; + }, + + // Step 3: 이미지 생성 μš”μ²­ + async requestImageGeneration(eventId: string, eventInfo: any) { + const response = await eventClient.post(`/events/${eventId}/images`, { + eventInfo, + imageCount: 3, + }); + return response.data; // { jobId, status, message } + }, + + // 이미지 선택 + async selectImage(eventId: string, imageId: string) { + const response = await eventClient.put( + `/events/${eventId}/images/${imageId}/select` + ); + return response.data; + }, + + // Step 4: 이미지 νŽΈμ§‘ + async editImage(eventId: string, imageId: string, editRequest: any) { + const response = await eventClient.put( + `/events/${eventId}/images/${imageId}/edit`, + editRequest + ); + return response.data; + }, + + // Step 5: 배포 채널 선택 + async selectChannels(eventId: string, channels: string[]) { + const response = await eventClient.put(`/events/${eventId}/channels`, { + channels, + }); + return response.data; + }, + + // Step 6: μ΅œμ’… 배포 + async publishEvent(eventId: string) { + const response = await eventClient.post(`/events/${eventId}/publish`); + return response.data; + }, +}; +``` + +**Funnel μ»΄ν¬λ„ŒνŠΈμ—μ„œ μ‚¬μš© μ˜ˆμ‹œ** +```typescript +// Step 2: AI μΆ”μ²œ μ»΄ν¬λ„ŒνŠΈ +const RecommendationStep = ({ eventId, onNext }) => { + const [jobId, setJobId] = useState(null); + const [recommendations, setRecommendations] = useState([]); + + // AI μΆ”μ²œ μš”μ²­ + const requestRecommendations = async () => { + const storeInfo = { /* User Serviceμ—μ„œ μ‘°νšŒν•œ λ§€μž₯ 정보 */ }; + const result = await eventApi.requestAiRecommendations(eventId, storeInfo); + setJobId(result.jobId); + + // Job 폴링 μ‹œμž‘ + pollJobStatus(result.jobId); + }; + + // Job μƒνƒœ 폴링 (3초 간격) + const pollJobStatus = async (jobId: string) => { + const interval = setInterval(async () => { + const status = await eventApi.getJobStatus(jobId); + + if (status.status === 'COMPLETED') { + clearInterval(interval); + // Redisμ—μ„œ κ²°κ³Ό 쑰회 (resultKey μ‚¬μš©) + fetchRecommendations(status.resultKey); + } else if (status.status === 'FAILED') { + clearInterval(interval); + toast.error('AI μΆ”μ²œ 생성에 μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€'); + } + }, 3000); + }; + + // μΆ”μ²œ 선택 및 λ‹€μŒ 단계 + const handleSelectRecommendation = async (recommendationId: string) => { + await eventApi.selectRecommendation(eventId, recommendationId); + onNext(); // Step 3으둜 이동 + }; + + return ( +
+ {/* AI μΆ”μ²œ UI */} + {recommendations.map(rec => ( + handleSelectRecommendation(rec.recommendationId)} + /> + ))} +
+ ); +}; +``` + +### 3.3 μ°Έμ—¬μž 관리 + +```typescript +// src/lib/api/participants.ts +import { participationClient } from './client'; + +export const participantApi = { + // μ°Έμ—¬μž λͺ©λ‘ 쑰회 + async getParticipants( + eventId: string, + page: number = 0, + size: number = 20, + storeVisited?: boolean + ) { + const params = new URLSearchParams({ + page: page.toString(), + size: size.toString(), + }); + + if (storeVisited !== undefined) { + params.append('storeVisited', storeVisited.toString()); + } + + const response = await participationClient.get( + `/events/${eventId}/participants?${params}` + ); + return response.data.data; + }, + + // μ°Έμ—¬μž 상세 쑰회 + async getParticipantDetail(eventId: string, participantId: string) { + const response = await participationClient.get( + `/events/${eventId}/participants/${participantId}` + ); + return response.data.data; + }, + + // λ‹Ήμ²¨μž 좔첨 + async drawWinners( + eventId: string, + winnerCount: number, + applyStoreVisitBonus: boolean = true + ) { + const response = await participationClient.post( + `/events/${eventId}/draw-winners`, + { winnerCount, applyStoreVisitBonus } + ); + return response.data.data; + }, + + // λ‹Ήμ²¨μž λͺ©λ‘ 쑰회 + async getWinners(eventId: string, page: number = 0, size: number = 20) { + const response = await participationClient.get( + `/events/${eventId}/winners?page=${page}&size=${size}` + ); + return response.data.data; + }, + + // 고객 μ°Έμ—¬ (인증 λΆˆν•„μš”) + async participate(eventId: string, data: ParticipationRequest) { + const response = await participationClient.post( + `/events/${eventId}/participate`, + data + ); + return response.data.data; + }, +}; +``` + +**React Queryλ₯Ό μ‚¬μš©ν•œ 데이터 패칭 μ˜ˆμ‹œ** +```typescript +// src/hooks/useParticipants.ts +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { participantApi } from '@/lib/api/participants'; + +export const useParticipants = (eventId: string, page: number = 0) => { + return useQuery({ + queryKey: ['participants', eventId, page], + queryFn: () => participantApi.getParticipants(eventId, page), + enabled: !!eventId, + }); +}; + +export const useDrawWinners = (eventId: string) => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: ({ winnerCount }: { winnerCount: number }) => + participantApi.drawWinners(eventId, winnerCount), + onSuccess: () => { + // μ°Έμ—¬μž λͺ©λ‘ κ°±μ‹  + queryClient.invalidateQueries({ queryKey: ['participants', eventId] }); + // λ‹Ήμ²¨μž λͺ©λ‘ κ°±μ‹  + queryClient.invalidateQueries({ queryKey: ['winners', eventId] }); + }, + }); +}; +``` + +### 3.4 μ—λŸ¬ 처리 + +```typescript +// src/lib/api/errorHandler.ts +import { AxiosError } from 'axios'; + +export interface ApiError { + code: string; + message: string; + timestamp: string; + details?: string[]; +} + +export const handleApiError = (error: unknown): string => { + if (error instanceof AxiosError) { + const apiError = error.response?.data as ApiError; + + // μ—λŸ¬ μ½”λ“œλ³„ 처리 + switch (apiError.code) { + case 'USER_001': + return '이미 κ°€μž…λœ μ „ν™”λ²ˆν˜Έμž…λ‹ˆλ‹€'; + case 'AUTH_001': + return 'μ „ν™”λ²ˆν˜Έ λ˜λŠ” λΉ„λ°€λ²ˆν˜Έλ₯Ό ν™•μΈν•΄μ£Όμ„Έμš”'; + case 'DUPLICATE_PARTICIPATION': + return '이미 μ°Έμ—¬ν•˜μ‹  μ΄λ²€νŠΈμž…λ‹ˆλ‹€'; + case 'INVALID_WINNER_COUNT': + return 'λ‹Ήμ²¨μž μˆ˜κ°€ μ°Έμ—¬μž μˆ˜λ³΄λ‹€ λ§ŽμŠ΅λ‹ˆλ‹€'; + default: + return apiError.message || 'μš”μ²­ 처리 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€'; + } + } + + return 'μ•Œ 수 μ—†λŠ” 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€'; +}; +``` + +--- + +## 4. μš”μ•½ + +### 4.1 μ£Όμš” API μ—”λ“œν¬μΈνŠΈ μš”μ•½ + +| μ„œλΉ„μŠ€ | μ£Όμš” κΈ°λŠ₯ | μ—”λ“œν¬μΈνŠΈ 수 | +|--------|---------|------------| +| **User Service** | 인증, ν”„λ‘œν•„ 관리 | 6개 | +| **Event Service** | 이벀트 생성/관리, Job 폴링 | 14개 | +| **Participation Service** | μ°Έμ—¬μž/λ‹Ήμ²¨μž 관리 | 5개 | + +### 4.2 비동기 μž‘μ—… 처리 + +**AI μΆ”μ²œ 생성 & 이미지 생성** +- μš”μ²­ μ‹œ `202 Accepted` 응닡과 ν•¨κ»˜ `jobId` λ°˜ν™˜ +- `/jobs/{jobId}`둜 3초 간격 폴링 +- `status: COMPLETED` μ‹œ `resultKey`둜 Redis κ²°κ³Ό 쑰회 +- `status: FAILED` μ‹œ μ—λŸ¬ λ©”μ‹œμ§€ ν‘œμ‹œ + +### 4.3 인증 처리 + +**JWT 토큰 관리** +- 둜그인/νšŒμ›κ°€μž… 성곡 μ‹œ `localStorage`에 μ €μž₯ +- λͺ¨λ“  API μš”μ²­ μ‹œ `Authorization: Bearer {token}` 헀더 μžλ™ μΆ”κ°€ +- 401 응닡 μ‹œ μžλ™ λ‘œκ·Έμ•„μ›ƒ 및 둜그인 νŽ˜μ΄μ§€ λ¦¬λ‹€μ΄λ ‰νŠΈ + +### 4.4 데이터 캐싱 μ „λž΅ + +**React Query 캐싱 ν‚€** +```typescript +['user', 'profile'] // ν”„λ‘œν•„ 정보 +['events', { status, page }] // 이벀트 λͺ©λ‘ +['event', eventId] // 이벀트 상세 +['participants', eventId, page] // μ°Έμ—¬μž λͺ©λ‘ +['winners', eventId] // λ‹Ήμ²¨μž λͺ©λ‘ +['job', jobId] // Job μƒνƒœ +``` + +--- + +**λ¬Έμ„œ 버전**: 1.0 +**μž‘μ„±μΌ**: 2025-01-24 +**μž‘μ„±μž**: Frontend Design Team diff --git a/design/frontend/ia.md b/design/frontend/ia.md new file mode 100644 index 0000000..f776684 --- /dev/null +++ b/design/frontend/ia.md @@ -0,0 +1,614 @@ +# 정보 μ•„ν‚€ν…μ²˜ μ„€κ³„μ„œ + +## λͺ©μ°¨ +1. [μ‚¬μ΄νŠΈλ§΅](#1-μ‚¬μ΄νŠΈλ§΅) +2. [ν”„λ‘œμ νŠΈ ꡬ쑰 섀계](#2-ν”„λ‘œμ νŠΈ-ꡬ쑰-섀계) + +--- + +## 1. μ‚¬μ΄νŠΈλ§΅ + +### 1.1 전체 μ‚¬μ΄νŠΈ ꡬ쑰 + +``` +KT 이벀트 λ§ˆμΌ€νŒ… ν”Œλž«νΌ +β”‚ +β”œβ”€β”€ πŸ” 인증 μ˜μ—­ (Public) +β”‚ β”œβ”€β”€ /login - 둜그인 +β”‚ β”œβ”€β”€ /register - νšŒμ›κ°€μž… +β”‚ └── /profile - ν”„λ‘œν•„ 관리 +β”‚ +β”œβ”€β”€ 🏠 λŒ€μ‹œλ³΄λ“œ μ˜μ—­ (Private) +β”‚ β”œβ”€β”€ / - 메인 λŒ€μ‹œλ³΄λ“œ +β”‚ β”‚ β”œβ”€β”€ 진행쀑 이벀트 ν˜„ν™© +β”‚ β”‚ β”œβ”€β”€ 졜근 μ„±κ³Ό μš”μ•½ +β”‚ β”‚ └── λΉ λ₯Έ μž‘μ—… λ²„νŠΌ +β”‚ β”‚ +β”‚ └── /events - 이벀트 λͺ©λ‘ +β”‚ β”œβ”€β”€ 전체 이벀트 쑰회 +β”‚ β”œβ”€β”€ μƒνƒœλ³„ 필터링 (진행쀑/μ˜ˆμ •/μ’…λ£Œ) +β”‚ └── 검색 κΈ°λŠ₯ +β”‚ +β”œβ”€β”€ ✨ 이벀트 생성 ν”Œλ‘œμš° (Private) +β”‚ └── /events/create - 이벀트 생성 Funnel +β”‚ β”œβ”€β”€ Step 1: /events/create?step=objective - λͺ©μ  선택 +β”‚ β”œβ”€β”€ Step 2: /events/create?step=recommendation - AI μΆ”μ²œ 확인 +β”‚ β”œβ”€β”€ Step 3: /events/create?step=content - μ½˜ν…μΈ  미리보기 +β”‚ β”œβ”€β”€ Step 4: /events/create?step=edit - μ½˜ν…μΈ  νŽΈμ§‘ +β”‚ β”œβ”€β”€ Step 5: /events/create?step=channels - 배포 채널 선택 +β”‚ └── Step 6: /events/create?step=publish - μ΅œμ’… 승인 +β”‚ +└── πŸ“Š 이벀트 관리 μ˜μ—­ (Private) + └── /events/[eventId] + β”œβ”€β”€ /events/[eventId] - 이벀트 상세 + β”œβ”€β”€ /events/[eventId]/participants - μ°Έμ—¬μž λͺ©λ‘ + β”œβ”€β”€ /events/[eventId]/participate - 고객 μ°Έμ—¬ ν™”λ©΄ (Public) + β”œβ”€β”€ /events/[eventId]/draw - λ‹Ήμ²¨μž 좔첨 + └── /events/[eventId]/analytics - μ„±κ³Ό 뢄석 +``` + +### 1.2 λ„€λΉ„κ²Œμ΄μ…˜ 흐름 + +#### Bottom Navigation (인증 ν›„ λͺ¨λ“  ν™”λ©΄) +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ ν™ˆ 이벀트 뢄석 ν”„λ‘œν•„ β”‚ +β”‚ / /events /analytics /profile β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +#### μ£Όμš” μ‚¬μš©μž μ—¬μ • + +**μ—¬μ • 1: μ‹ κ·œ 이벀트 생성** +``` +/ (λŒ€μ‹œλ³΄λ“œ) + β†’ [+ μƒˆ 이벀트 생성] λ²„νŠΌ 클릭 + β†’ /events/create?step=objective (λͺ©μ  선택) + β†’ /events/create?step=recommendation (AI μΆ”μ²œ) + β†’ /events/create?step=content (미리보기) + β†’ /events/create?step=edit (νŽΈμ§‘) + β†’ /events/create?step=channels (채널 선택) + β†’ /events/create?step=publish (승인) + β†’ /events/[eventId] (생성 μ™„λ£Œ, 상세 νŽ˜μ΄μ§€) +``` + +**μ—¬μ • 2: 이벀트 관리 및 뢄석** +``` +/events (이벀트 λͺ©λ‘) + β†’ [이벀트 μΉ΄λ“œ] 클릭 + β†’ /events/[eventId] (상세) + β”œβ”€β”€ [μ°Έμ—¬μž 관리] β†’ /events/[eventId]/participants + β”œβ”€β”€ [λ‹Ήμ²¨μž 좔첨] β†’ /events/[eventId]/draw + └── [μ„±κ³Ό 뢄석] β†’ /events/[eventId]/analytics +``` + +**μ—¬μ • 3: 고객 μ°Έμ—¬** +``` +QR μ½”λ“œ μŠ€μΊ” λ˜λŠ” 링크 접속 + β†’ /events/[eventId]/participate (μ°Έμ—¬ ν™”λ©΄) + β†’ μ°Έμ—¬ 정보 μž…λ ₯ + β†’ μ°Έμ—¬ μ™„λ£Œ +``` + +### 1.3 μ ‘κ·Ό κΆŒν•œ μ„€μ • + +| νŽ˜μ΄μ§€ 경둜 | μ ‘κ·Ό κΆŒν•œ | 인증 ν•„μš” | μ„€λͺ… | +|------------|----------|---------|------| +| `/login` | Public | ❌ | 둜그인 νŽ˜μ΄μ§€ | +| `/register` | Public | ❌ | νšŒμ›κ°€μž… νŽ˜μ΄μ§€ | +| `/` | Private | βœ… | 메인 λŒ€μ‹œλ³΄λ“œ | +| `/events` | Private | βœ… | 이벀트 λͺ©λ‘ | +| `/events/create` | Private | βœ… | 이벀트 생성 ν”Œλ‘œμš° | +| `/events/[eventId]` | Private | βœ… | 이벀트 상세 | +| `/events/[eventId]/participants` | Private | βœ… | μ°Έμ—¬μž 관리 | +| `/events/[eventId]/draw` | Private | βœ… | λ‹Ήμ²¨μž 좔첨 | +| `/events/[eventId]/analytics` | Private | βœ… | μ„±κ³Ό 뢄석 | +| `/events/[eventId]/participate` | **Public** | ❌ | 고객 μ°Έμ—¬ ν™”λ©΄ | +| `/profile` | Private | βœ… | ν”„λ‘œν•„ 관리 | + +--- + +## 2. ν”„λ‘œμ νŠΈ ꡬ쑰 섀계 + +### 2.1 Next.js 14 App Router 기반 디렉토리 ꡬ쑰 + +``` +fe-kt-event-marketing/ +β”‚ +β”œβ”€β”€ πŸ“ app/ # Next.js 14 App Router +β”‚ β”œβ”€β”€ (auth)/ # 인증 λ ˆμ΄μ•„μ›ƒ κ·Έλ£Ή +β”‚ β”‚ β”œβ”€β”€ login/ +β”‚ β”‚ β”‚ └── page.tsx # 둜그인 νŽ˜μ΄μ§€ +β”‚ β”‚ β”œβ”€β”€ register/ +β”‚ β”‚ β”‚ └── page.tsx # νšŒμ›κ°€μž… νŽ˜μ΄μ§€ +β”‚ β”‚ └── layout.tsx # 인증 μ „μš© λ ˆμ΄μ•„μ›ƒ +β”‚ β”‚ +β”‚ β”œβ”€β”€ (main)/ # 메인 λ ˆμ΄μ•„μ›ƒ κ·Έλ£Ή (인증 ν•„μš”) +β”‚ β”‚ β”œβ”€β”€ page.tsx # λŒ€μ‹œλ³΄λ“œ (/) +β”‚ β”‚ β”œβ”€β”€ events/ +β”‚ β”‚ β”‚ β”œβ”€β”€ page.tsx # 이벀트 λͺ©λ‘ +β”‚ β”‚ β”‚ β”œβ”€β”€ create/ +β”‚ β”‚ β”‚ β”‚ └── page.tsx # 이벀트 생성 Funnel +β”‚ β”‚ β”‚ └── [eventId]/ +β”‚ β”‚ β”‚ β”œβ”€β”€ page.tsx # 이벀트 상세 +β”‚ β”‚ β”‚ β”œβ”€β”€ participants/ +β”‚ β”‚ β”‚ β”‚ └── page.tsx # μ°Έμ—¬μž λͺ©λ‘ +β”‚ β”‚ β”‚ β”œβ”€β”€ draw/ +β”‚ β”‚ β”‚ β”‚ └── page.tsx # λ‹Ήμ²¨μž 좔첨 +β”‚ β”‚ β”‚ └── analytics/ +β”‚ β”‚ β”‚ └── page.tsx # μ„±κ³Ό 뢄석 +β”‚ β”‚ β”œβ”€β”€ profile/ +β”‚ β”‚ β”‚ └── page.tsx # ν”„λ‘œν•„ 관리 +β”‚ β”‚ └── layout.tsx # 메인 λ ˆμ΄μ•„μ›ƒ (Bottom Nav 포함) +β”‚ β”‚ +β”‚ β”œβ”€β”€ events/ # Public 이벀트 경둜 +β”‚ β”‚ └── [eventId]/ +β”‚ β”‚ └── participate/ +β”‚ β”‚ └── page.tsx # 고객 μ°Έμ—¬ ν™”λ©΄ (인증 λΆˆν•„μš”) +β”‚ β”‚ +β”‚ β”œβ”€β”€ api/ # API Routes (μ„œλ²„ μ»΄ν¬λ„ŒνŠΈ) +β”‚ β”‚ └── auth/ +β”‚ β”‚ └── [...nextauth]/ +β”‚ β”‚ └── route.ts # NextAuth API 라우트 +β”‚ β”‚ +β”‚ β”œβ”€β”€ layout.tsx # 루트 λ ˆμ΄μ•„μ›ƒ +β”‚ β”œβ”€β”€ error.tsx # μ „μ—­ μ—λŸ¬ 핸듀링 +β”‚ β”œβ”€β”€ loading.tsx # μ „μ—­ λ‘œλ”© μƒνƒœ +β”‚ └── not-found.tsx # 404 νŽ˜μ΄μ§€ +β”‚ +β”œβ”€β”€ πŸ“ src/ +β”‚ β”œβ”€β”€ components/ # React μ»΄ν¬λ„ŒνŠΈ +β”‚ β”‚ β”œβ”€β”€ common/ # 곡톡 μ»΄ν¬λ„ŒνŠΈ +β”‚ β”‚ β”‚ β”œβ”€β”€ Button/ +β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ Button.tsx +β”‚ β”‚ β”‚ β”‚ └── Button.test.tsx +β”‚ β”‚ β”‚ β”œβ”€β”€ Card/ +β”‚ β”‚ β”‚ β”‚ └── Card.tsx +β”‚ β”‚ β”‚ β”œβ”€β”€ Input/ +β”‚ β”‚ β”‚ β”‚ └── Input.tsx +β”‚ β”‚ β”‚ β”œβ”€β”€ Layout/ +β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ BottomNavigation.tsx +β”‚ β”‚ β”‚ β”‚ β”œβ”€β”€ Header.tsx +β”‚ β”‚ β”‚ β”‚ └── Container.tsx +β”‚ β”‚ β”‚ └── Loading/ +β”‚ β”‚ β”‚ └── Loading.tsx +β”‚ β”‚ β”‚ +β”‚ β”‚ β”œβ”€β”€ auth/ # 인증 κ΄€λ ¨ μ»΄ν¬λ„ŒνŠΈ +β”‚ β”‚ β”‚ β”œβ”€β”€ LoginForm.tsx +β”‚ β”‚ β”‚ β”œβ”€β”€ RegisterForm.tsx +β”‚ β”‚ β”‚ └── ProfileForm.tsx +β”‚ β”‚ β”‚ +β”‚ β”‚ β”œβ”€β”€ dashboard/ # λŒ€μ‹œλ³΄λ“œ μ»΄ν¬λ„ŒνŠΈ +β”‚ β”‚ β”‚ β”œβ”€β”€ EventSummaryCard.tsx +β”‚ β”‚ β”‚ β”œβ”€β”€ PerformanceChart.tsx +β”‚ β”‚ β”‚ └── QuickActions.tsx +β”‚ β”‚ β”‚ +β”‚ β”‚ β”œβ”€β”€ event/ # 이벀트 κ΄€λ ¨ μ»΄ν¬λ„ŒνŠΈ +β”‚ β”‚ β”‚ β”œβ”€β”€ EventCard.tsx +β”‚ β”‚ β”‚ β”œβ”€β”€ EventList.tsx +β”‚ β”‚ β”‚ β”œβ”€β”€ EventDetail.tsx +β”‚ β”‚ β”‚ β”œβ”€β”€ ParticipantTable.tsx +β”‚ β”‚ β”‚ └── WinnerDrawModal.tsx +β”‚ β”‚ β”‚ +β”‚ β”‚ └── funnel/ # Funnel κ΄€λ ¨ μ»΄ν¬λ„ŒνŠΈ +β”‚ β”‚ β”œβ”€β”€ ObjectiveStep.tsx +β”‚ β”‚ β”œβ”€β”€ RecommendationStep.tsx +β”‚ β”‚ β”œβ”€β”€ ContentStep.tsx +β”‚ β”‚ β”œβ”€β”€ EditStep.tsx +β”‚ β”‚ β”œβ”€β”€ ChannelsStep.tsx +β”‚ β”‚ β”œβ”€β”€ PublishStep.tsx +β”‚ β”‚ └── FunnelLayout.tsx +β”‚ β”‚ +β”‚ β”œβ”€β”€ hooks/ # Custom React Hooks +β”‚ β”‚ β”œβ”€β”€ useAuth.ts # 인증 μƒνƒœ 관리 +β”‚ β”‚ β”œβ”€β”€ useEvent.ts # 이벀트 CRUD +β”‚ β”‚ β”œβ”€β”€ useEventList.ts # 이벀트 λͺ©λ‘ +β”‚ β”‚ β”œβ”€β”€ useParticipants.ts # μ°Έμ—¬μž 관리 +β”‚ β”‚ β”œβ”€β”€ useJobPolling.ts # Job μƒνƒœ 폴링 +β”‚ β”‚ └── useToast.ts # Toast μ•Œλ¦Ό +β”‚ β”‚ +β”‚ β”œβ”€β”€ lib/ # 라이브러리 및 μœ ν‹Έλ¦¬ν‹° +β”‚ β”‚ β”œβ”€β”€ api/ # API ν΄λΌμ΄μ–ΈνŠΈ +β”‚ β”‚ β”‚ β”œβ”€β”€ client.ts # Axios μΈμŠ€ν„΄μŠ€ +β”‚ β”‚ β”‚ β”œβ”€β”€ auth.ts # 인증 API +β”‚ β”‚ β”‚ β”œβ”€β”€ events.ts # 이벀트 API +β”‚ β”‚ β”‚ └── participants.ts # μ°Έμ—¬ API +β”‚ β”‚ β”‚ +β”‚ β”‚ β”œβ”€β”€ utils/ # μœ ν‹Έλ¦¬ν‹° ν•¨μˆ˜ +β”‚ β”‚ β”‚ β”œβ”€β”€ format.ts # λ‚ μ§œ/숫자 ν¬λ§·νŒ… +β”‚ β”‚ β”‚ β”œβ”€β”€ validation.ts # 폼 검증 헬퍼 +β”‚ β”‚ β”‚ └── storage.ts # localStorage 헬퍼 +β”‚ β”‚ β”‚ +β”‚ β”‚ └── constants/ # μƒμˆ˜ μ •μ˜ +β”‚ β”‚ β”œβ”€β”€ api.ts # API μ—”λ“œν¬μΈνŠΈ +β”‚ β”‚ β”œβ”€β”€ routes.ts # 라우트 경둜 +β”‚ β”‚ └── event.ts # 이벀트 μƒμˆ˜ +β”‚ β”‚ +β”‚ β”œβ”€β”€ store/ # μƒνƒœ 관리 (Zustand) +β”‚ β”‚ β”œβ”€β”€ authStore.ts # 인증 μƒνƒœ +β”‚ β”‚ β”œβ”€β”€ funnelStore.ts # Funnel μž„μ‹œ μ €μž₯ +β”‚ β”‚ └── uiStore.ts # UI μƒνƒœ (Toast, Modal λ“±) +β”‚ β”‚ +β”‚ β”œβ”€β”€ styles/ # μŠ€νƒ€μΌ +β”‚ β”‚ β”œβ”€β”€ globals.css # μ „μ—­ μŠ€νƒ€μΌ +β”‚ β”‚ └── theme.ts # MUI ν…Œλ§ˆ μ„€μ • +β”‚ β”‚ +β”‚ └── types/ # TypeScript νƒ€μž… μ •μ˜ +β”‚ β”œβ”€β”€ auth.ts # 인증 κ΄€λ ¨ νƒ€μž… +β”‚ β”œβ”€β”€ event.ts # 이벀트 νƒ€μž… +β”‚ β”œβ”€β”€ participant.ts # μ°Έμ—¬μž νƒ€μž… +β”‚ └── api.ts # API 응닡 νƒ€μž… +β”‚ +β”œβ”€β”€ πŸ“ public/ # 정적 파일 +β”‚ β”œβ”€β”€ images/ # 이미지 파일 +β”‚ β”œβ”€β”€ icons/ # μ•„μ΄μ½˜ 파일 +β”‚ └── runtime-env.js # λŸ°νƒ€μž„ ν™˜κ²½ λ³€μˆ˜ +β”‚ +β”œβ”€β”€ πŸ“ tests/ # ν…ŒμŠ€νŠΈ 파일 +β”‚ β”œβ”€β”€ unit/ # λ‹¨μœ„ ν…ŒμŠ€νŠΈ +β”‚ β”œβ”€β”€ integration/ # 톡합 ν…ŒμŠ€νŠΈ +β”‚ └── e2e/ # E2E ν…ŒμŠ€νŠΈ (Playwright) +β”‚ +β”œβ”€β”€ .env.local # 둜컬 ν™˜κ²½ λ³€μˆ˜ +β”œβ”€β”€ .env.development # 개발 ν™˜κ²½ λ³€μˆ˜ +β”œβ”€β”€ .env.production # ν”„λ‘œλ•μ…˜ ν™˜κ²½ λ³€μˆ˜ +β”œβ”€β”€ next.config.js # Next.js μ„€μ • +β”œβ”€β”€ tsconfig.json # TypeScript μ„€μ • +β”œβ”€β”€ package.json # μ˜μ‘΄μ„± 관리 +└── README.md # ν”„λ‘œμ νŠΈ λ¬Έμ„œ +``` + +### 2.2 μ£Όμš” 파일 μ„€λͺ… + +#### 2.2.1 λ ˆμ΄μ•„μ›ƒ ꡬ쑰 + +**app/layout.tsx** (루트 λ ˆμ΄μ•„μ›ƒ) +```typescript +// React Query Provider, MUI ThemeProvider, Toast Provider μ„€μ • +export default function RootLayout({ children }) { + return ( + + + + + + {children} + + + + + ); +} +``` + +**app/(main)/layout.tsx** (메인 λ ˆμ΄μ•„μ›ƒ) +```typescript +// Bottom Navigation 포함 λ ˆμ΄μ•„μ›ƒ +// 인증 체크 미듀웨어 +export default function MainLayout({ children }) { + return ( + <> +
+ {children} + + + ); +} +``` + +**app/(auth)/layout.tsx** (인증 λ ˆμ΄μ•„μ›ƒ) +```typescript +// 둜고 μ€‘μ‹¬μ˜ μ‹¬ν”Œν•œ λ ˆμ΄μ•„μ›ƒ +export default function AuthLayout({ children }) { + return ( + + + {children} + + ); +} +``` + +#### 2.2.2 Funnel κ΅¬ν˜„ + +**app/(main)/events/create/page.tsx** +```typescript +'use client'; + +import { useFunnel } from '@use-funnel/next'; +import { ObjectiveStep, RecommendationStep, ... } from '@/components/funnel'; + +export default function EventCreatePage() { + const [Funnel, state, setStep] = useFunnel({ + id: 'event-creation', + initial: 'objective', + }); + + return ( + + + setStep('recommendation')} /> + + + setStep('content')} + onBack={() => setStep('objective')} + /> + + {/* ... λ‚˜λ¨Έμ§€ Stepλ“€ */} + + ); +} +``` + +#### 2.2.3 API ν΄λΌμ΄μ–ΈνŠΈ + +**src/lib/api/client.ts** +```typescript +import axios from 'axios'; + +// runtime-env.jsμ—μ„œ ν™˜κ²½ λ³€μˆ˜ λ‘œλ“œ +declare global { + interface Window { + __runtime_config__: { + API_GROUP: string; + USER_HOST: string; + EVENT_HOST: string; + CONTENT_HOST: string; + AI_HOST: string; + PARTICIPATION_HOST: string; + DISTRIBUTION_HOST: string; + ANALYTICS_HOST: string; + }; + } +} + +const config = window.__runtime_config__; + +// 1. User Service API Client (인증, ν”„λ‘œν•„) +export const userClient = axios.create({ + baseURL: `${config.USER_HOST}${config.API_GROUP}`, + timeout: 10000, +}); + +// 2. Event Service API Client (이벀트 생λͺ…μ£ΌκΈ° 관리) +export const eventClient = axios.create({ + baseURL: `${config.EVENT_HOST}${config.API_GROUP}`, + timeout: 30000, // AI/이미지 생성 Job 폴링 κ³ λ € +}); + +// 3. Content Service API Client (이미지 생성 및 νŽΈμ§‘) +export const contentClient = axios.create({ + baseURL: `${config.CONTENT_HOST}${config.API_GROUP}`, + timeout: 30000, // 이미지 생성 Job 폴링 +}); + +// 4. AI Service API Client (AI μΆ”μ²œ 생성) +export const aiClient = axios.create({ + baseURL: `${config.AI_HOST}${config.API_GROUP}`, + timeout: 30000, // AI 생성 Job 폴링 +}); + +// 5. Participation Service API Client (μ°Έμ—¬μž/λ‹Ήμ²¨μž 관리) +export const participationClient = axios.create({ + baseURL: `${config.PARTICIPATION_HOST}${config.API_GROUP}`, + timeout: 10000, +}); + +// 6. Distribution Service API Client (닀쀑 채널 배포) +export const distributionClient = axios.create({ + baseURL: `${config.DISTRIBUTION_HOST}${config.API_GROUP}`, + timeout: 20000, // 닀쀑 채널 배포 μ‹œκ°„ κ³ λ € +}); + +// 7. Analytics Service API Client (μ„±κ³Ό 뢄석 및 λŒ€μ‹œλ³΄λ“œ) +export const analyticsClient = axios.create({ + baseURL: `${config.ANALYTICS_HOST}${config.API_GROUP}`, + timeout: 10000, +}); +``` + +### 2.3 μ»΄ν¬λ„ŒνŠΈ μž¬μ‚¬μš© μ „λž΅ + +#### Atomic Design 기반 μ»΄ν¬λ„ŒνŠΈ 계측 + +``` +Atoms (μ›μž) + └── Button, Input, Icon, Typography + ↓ +Molecules (λΆ„μž) + └── SearchBar, EventCard, ParticipantRow + ↓ +Organisms (유기체) + └── EventList, ParticipantTable, Dashboard + ↓ +Templates (ν…œν”Œλ¦Ώ) + └── MainLayout, AuthLayout, FunnelLayout + ↓ +Pages (νŽ˜μ΄μ§€) + └── HomePage, EventListPage, EventCreatePage +``` + +#### MUI μ»΄ν¬λ„ŒνŠΈ μ»€μŠ€ν„°λ§ˆμ΄μ§• + +```typescript +// src/components/common/Button/Button.tsx +import { Button as MuiButton, ButtonProps } from '@mui/material'; + +export const Button: React.FC = (props) => { + return ( + + ); +}; +``` + +### 2.4 μƒνƒœ 관리 μ „λž΅ + +| μƒνƒœ μœ ν˜• | 관리 도ꡬ | μ‚¬μš© 사둀 | +|---------|---------|---------| +| μ„œλ²„ μƒνƒœ | React Query v5 | API 데이터 캐싱, μžλ™ μž¬κ²€μ¦ | +| μ „μ—­ ν΄λΌμ΄μ–ΈνŠΈ μƒνƒœ | Zustand | 인증 μƒνƒœ, UI μƒνƒœ (Toast, Modal) | +| 둜컬 μƒνƒœ | useState | μ»΄ν¬λ„ŒνŠΈ λ‚΄λΆ€ μƒνƒœ | +| 폼 μƒνƒœ | React Hook Form | 폼 μž…λ ₯, 검증 | +| Funnel μƒνƒœ | @use-funnel/next | Step μ „ν™˜, 데이터 μž„μ‹œ μ €μž₯ | + +### 2.5 λΌμš°νŒ… 및 미듀웨어 + +**middleware.ts** (Next.js Middleware) +```typescript +import { NextResponse } from 'next/server'; +import type { NextRequest } from 'next/server'; + +export function middleware(request: NextRequest) { + const token = request.cookies.get('accessToken'); + const { pathname } = request.nextUrl; + + // 인증이 ν•„μš”ν•œ 경둜 + const protectedRoutes = ['/', '/events', '/profile']; + const isProtectedRoute = protectedRoutes.some(route => + pathname.startsWith(route) + ); + + // μΈμ¦λ˜μ§€ μ•Šμ€ μ‚¬μš©μžκ°€ 보호된 경둜 μ ‘κ·Ό μ‹œ 둜그인으둜 λ¦¬λ‹€μ΄λ ‰νŠΈ + if (isProtectedRoute && !token) { + return NextResponse.redirect(new URL('/login', request.url)); + } + + // 인증된 μ‚¬μš©μžκ°€ 둜그인/νšŒμ›κ°€μž… μ ‘κ·Ό μ‹œ λŒ€μ‹œλ³΄λ“œλ‘œ λ¦¬λ‹€μ΄λ ‰νŠΈ + if ((pathname === '/login' || pathname === '/register') && token) { + return NextResponse.redirect(new URL('/', request.url)); + } + + return NextResponse.next(); +} + +export const config = { + matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'], +}; +``` + +### 2.6 ν™˜κ²½ λ³€μˆ˜ 관리 + +**.env.local** +``` +# 둜컬 개발 ν™˜κ²½ +NEXT_PUBLIC_USER_HOST=http://localhost:8081 +NEXT_PUBLIC_EVENT_HOST=http://localhost:8080 +NEXT_PUBLIC_CONTENT_HOST=http://localhost:8082 +NEXT_PUBLIC_AI_HOST=http://localhost:8083 +NEXT_PUBLIC_PARTICIPATION_HOST=http://localhost:8084 +NEXT_PUBLIC_DISTRIBUTION_HOST=http://localhost:8085 +NEXT_PUBLIC_ANALYTICS_HOST=http://localhost:8086 +NEXT_PUBLIC_API_VERSION=v1 +``` + +**.env.production** +``` +# ν”„λ‘œλ•μ…˜ ν™˜κ²½ +NEXT_PUBLIC_USER_HOST=https://api.kt-event-marketing.com/user/v1 +NEXT_PUBLIC_EVENT_HOST=https://api.kt-event-marketing.com/event/v1 +NEXT_PUBLIC_CONTENT_HOST=https://api.kt-event-marketing.com/content/v1 +NEXT_PUBLIC_AI_HOST=https://api.kt-event-marketing.com/ai/v1 +NEXT_PUBLIC_PARTICIPATION_HOST=https://api.kt-event-marketing.com/participation/v1 +NEXT_PUBLIC_DISTRIBUTION_HOST=https://api.kt-event-marketing.com/distribution/v1 +NEXT_PUBLIC_ANALYTICS_HOST=https://api.kt-event-marketing.com/analytics/v1 +NEXT_PUBLIC_API_VERSION=v1 +``` + +**public/runtime-env.js** +```javascript +window.__runtime_config__ = { + API_GROUP: "/api/v1", + + // 7개 λ§ˆμ΄ν¬λ‘œμ„œλΉ„μŠ€ 호슀트 + USER_HOST: process.env.NEXT_PUBLIC_USER_HOST || "http://localhost:8081", + EVENT_HOST: process.env.NEXT_PUBLIC_EVENT_HOST || "http://localhost:8080", + CONTENT_HOST: process.env.NEXT_PUBLIC_CONTENT_HOST || "http://localhost:8082", + AI_HOST: process.env.NEXT_PUBLIC_AI_HOST || "http://localhost:8083", + PARTICIPATION_HOST: process.env.NEXT_PUBLIC_PARTICIPATION_HOST || "http://localhost:8084", + DISTRIBUTION_HOST: process.env.NEXT_PUBLIC_DISTRIBUTION_HOST || "http://localhost:8085", + ANALYTICS_HOST: process.env.NEXT_PUBLIC_ANALYTICS_HOST || "http://localhost:8086", +}; +``` + +--- + +## 3. 기술 μŠ€νƒ 정리 + +| μΉ΄ν…Œκ³ λ¦¬ | 기술 | 버전 | μš©λ„ | +|---------|-----|------|-----| +| **Framework** | Next.js | 14.x | App Router, SSR, SSG | +| **Library** | React | 18.x | UI 라이브러리 | +| **Language** | TypeScript | 5.x | 정적 νƒ€μž… 검사 | +| **UI Framework** | Material-UI (MUI) | 6.x | UI μ»΄ν¬λ„ŒνŠΈ | +| **State (Server)** | React Query | 5.x | μ„œλ²„ μƒνƒœ 관리 | +| **State (Client)** | Zustand | 4.x | μ „μ—­ μƒνƒœ 관리 | +| **Form** | React Hook Form | 7.x | 폼 관리 | +| **Validation** | Zod | 3.x | μŠ€ν‚€λ§ˆ 검증 | +| **Funnel** | @use-funnel/next | 1.x | Funnel μƒνƒœ 관리 | +| **HTTP Client** | Axios | 1.x | API 톡신 | +| **Charts** | Chart.js | 4.x | 데이터 μ‹œκ°ν™” | +| **Testing** | Jest, Playwright | - | λ‹¨μœ„/E2E ν…ŒμŠ€νŠΈ | + +--- + +## 4. 배포 및 ν™˜κ²½ μ„€μ • + +### 4.1 개발 ν™˜κ²½ +```bash +npm run dev # 개발 μ„œλ²„ μ‹€ν–‰ (localhost:3000) +npm run build # ν”„λ‘œλ•μ…˜ λΉŒλ“œ +npm run start # ν”„λ‘œλ•μ…˜ μ„œλ²„ μ‹€ν–‰ +npm run lint # ESLint 검사 +npm run test # Jest λ‹¨μœ„ ν…ŒμŠ€νŠΈ +npm run test:e2e # Playwright E2E ν…ŒμŠ€νŠΈ +``` + +### 4.2 λΉŒλ“œ μ΅œμ ν™” +- **Code Splitting**: 동적 import둜 λ²ˆλ“€ 크기 μ΅œμ ν™” +- **Image Optimization**: Next.js Image μ»΄ν¬λ„ŒνŠΈ μ‚¬μš© +- **Font Optimization**: next/font둜 μ›Ήν°νŠΈ μ΅œμ ν™” +- **Tree Shaking**: μ‚¬μš©ν•˜μ§€ μ•ŠλŠ” μ½”λ“œ 제거 + +### 4.3 μ„±λŠ₯ λͺ¨λ‹ˆν„°λ§ +- **Lighthouse**: μ„±λŠ₯, μ ‘κ·Όμ„±, SEO 점수 +- **Web Vitals**: LCP, FID, CLS μΈ‘μ • +- **Bundle Analyzer**: λ²ˆλ“€ 크기 뢄석 + +--- + +## 5. μ ‘κ·Όμ„± 및 λ°˜μ‘ν˜• 섀계 + +### 5.1 λ°˜μ‘ν˜• 브레이크포인트 +```typescript +// src/styles/theme.ts +const breakpoints = { + xs: 0, // Mobile: 320px ~ 599px + sm: 600, // Tablet: 600px ~ 1023px + md: 1024, // Desktop: 1024px ~ 1439px + lg: 1440, // Large Desktop: 1440px+ +}; +``` + +### 5.2 μ ‘κ·Όμ„± μ€€μˆ˜ (WCAG 2.1 AA) +- **ν‚€λ³΄λ“œ λ„€λΉ„κ²Œμ΄μ…˜**: Tab, Enter, Space 지원 +- **슀크린 리더**: aria-label, role 속성 +- **색상 λŒ€λΉ„**: μ΅œμ†Œ 4.5:1 λΉ„μœ¨ +- **포컀슀 ν‘œμ‹œ**: λͺ…ν™•ν•œ focus μƒνƒœ +- **λŒ€μ²΄ ν…μŠ€νŠΈ**: λͺ¨λ“  이미지에 alt 속성 + +--- + +**λ¬Έμ„œ 버전**: 1.0 +**μž‘μ„±μΌ**: 2025-01-24 +**μž‘μ„±μž**: Frontend Design Team diff --git a/design/frontend/style-guide.md b/design/frontend/style-guide.md new file mode 100644 index 0000000..b1f46d5 --- /dev/null +++ b/design/frontend/style-guide.md @@ -0,0 +1,1554 @@ +# KT AI 기반 μ†Œμƒκ³΅μΈ 이벀트 μžλ™ 생성 μ„œλΉ„μŠ€ - μŠ€νƒ€μΌ κ°€μ΄λ“œ + +## λ¬Έμ„œ 정보 + +- μž‘μ„±μΌ: 2025-10-17 +- 버전: 1.0 +- 기반 섀계: UI/UX μ„€κ³„μ„œ v1.0 +- 섀계 원칙: Mobile First, μ ‘κ·Όμ„± μš°μ„ , 일관성 + +--- + +## 1. λΈŒλžœλ“œ 아이덴티티 + +### 1.1 λΈŒλžœλ“œ λΉ„μ „ + +**"AI둜 κ°„νŽΈν•˜κ²Œ, μ„±κ³΅μœΌλ‘œ ν™•μ‹€ν•˜κ²Œ"** + +μ†Œμƒκ³΅μΈμ΄ μ „λ¬Έ λ§ˆμΌ€ν„° 없이도 AI의 λ„μ›€μœΌλ‘œ 효과적인 이벀트λ₯Ό κΈ°νšν•˜κ³  μ‹€ν–‰ν•  수 μžˆλ„λ‘ λ•λŠ” ν˜μ‹ μ μΈ μ„œλΉ„μŠ€ + +### 1.2 λ””μžμΈ μ² ν•™ + +#### ν˜μ‹ μ„± (Innovation) + +- μ΅œμ‹  AI κΈ°μˆ μ„ ν™œμš©ν•œ μžλ™ν™”λœ 이벀트 생성 +- λ³΅μž‘ν•œ λ§ˆμΌ€νŒ… ν”„λ‘œμ„ΈμŠ€λ₯Ό λ‹¨μˆœν™” +- 3λΆ„ λ§Œμ— μ™„μ„±λ˜λŠ” 이벀트 μ½˜ν…μΈ  + +#### μ‹ λ’°μ„± (Trust) + +- KT λΈŒλžœλ“œμ˜ μ•ˆμ •μ„±κ³Ό 신뒰감 +- λͺ…ν™•ν•œ ν”„λ‘œμ„ΈμŠ€μ™€ 투λͺ…ν•œ κ²°κ³Ό 제곡 +- μ‹€μ‹œκ°„ μ„±κ³Ό λͺ¨λ‹ˆν„°λ§ + +#### μΉœκ·Όν•¨ (Approachability) + +- μ†Œμƒκ³΅μΈ λˆˆλ†’μ΄μ— 맞좘 μ‰¬μš΄ μΈν„°νŽ˜μ΄μŠ€ +- μ΄ˆλ³΄μžλ„ 이해할 수 μžˆλŠ” λͺ…ν™•ν•œ μ•ˆλ‚΄ +- λ”°λœ»ν•˜κ³  μΉœκ·Όν•œ ν†€μ•€λ§€λ„ˆ + +### 1.3 λΈŒλžœλ“œ 컬러 + +**Primary Color: KT Red** + +- 정체성, λΈŒλžœλ“œ λŒ€ν‘œ 색상 +- 행동 μœ λ„(CTA), κ°•μ‘° μš”μ†Œ +- KT λΈŒλžœλ“œ 헀리티지 κ³„μŠΉ + +**Secondary Color: AI Blue** + +- AI κΈ°λŠ₯, 기술적 신뒰감 +- 정보 전달, μ•ˆλ‚΄ μš”μ†Œ +- ν˜μ‹ κ³Ό λ―Έλž˜μ§€ν–₯μ„± + +--- + +## 2. λ””μžμΈ 원칙 + +### 2.1 λͺ…ν™•μ„± (Clarity) + +**μ‚¬μš©μžκ°€ 무엇을 ν•΄μ•Ό ν•˜λŠ”μ§€ 항상 λͺ…ν™•ν•΄μ•Ό ν•©λ‹ˆλ‹€** + +- 직관적인 μ•„μ΄μ½˜κ³Ό λ ˆμ΄λΈ” μ‚¬μš© +- λͺ…ν™•ν•œ μ‹œκ°μ  계측 ꡬ쑰 +- ν˜„μž¬ μœ„μΉ˜μ™€ λ‹€μŒ 단계λ₯Ό 항상 ν‘œμ‹œ + +μ˜ˆμ‹œ: + +``` +βœ“ 쒋은 예: "μƒˆ 이벀트 λ§Œλ“€κΈ°" + 큰 CTA λ²„νŠΌ +βœ— λ‚˜μœ 예: "μ‹œμž‘ν•˜κΈ°" (무엇을 μ‹œμž‘ν•˜λŠ”μ§€ λΆˆλΆ„λͺ…) +``` + +### 2.2 νš¨μœ¨μ„± (Efficiency) + +**μ΅œμ†Œν•œμ˜ λ‹¨κ³„λ‘œ λͺ©ν‘œλ₯Ό 달성할 수 μžˆμ–΄μ•Ό ν•©λ‹ˆλ‹€** + +- λΆˆν•„μš”ν•œ μŠ€ν… 제거 +- μžλ™ μ™„μ„± 및 μΆ”μ²œ κΈ°λŠ₯ ν™œμš© +- AIκ°€ λŒ€λΆ€λΆ„μ˜ μž‘μ—… μžλ™ν™” + +λͺ©ν‘œ: + +- 이벀트 기획: 10초 이내 +- μ½˜ν…μΈ  생성: 3λΆ„ 이내 +- 배포 μ„€μ •: 1λΆ„ 이내 + +### 2.3 μ‹ λ’°μ„± (Trust) + +**AI 처리 κ³Όμ •κ³Ό κ²°κ³Όλ₯Ό 투λͺ…ν•˜κ²Œ λ³΄μ—¬μ€λ‹ˆλ‹€** + +- AI 처리 μ‹œκ°„ λͺ…μ‹œ (예: "AIκ°€ λΆ„μ„μ€‘μž…λ‹ˆλ‹€ μ•½ 3초 μ†Œμš”") +- 쀑간 단계 κ²°κ³Ό 확인 κ°€λŠ₯ +- μ–Έμ œλ“  이전 λ‹¨κ³„λ‘œ λŒμ•„κ°ˆ 수 있음 + +### 2.4 μΉœκ·Όν•¨ (Approachability) + +**μ΄ˆλ³΄μžλ„ μ‰½κ²Œ μ‚¬μš©ν•  수 μžˆμ–΄μ•Ό ν•©λ‹ˆλ‹€** + +- μ „λ¬Έ μš©μ–΄ μ΅œμ†Œν™” +- μΉœκ·Όν•œ ν†€μ˜ μ•ˆλ‚΄ 문ꡬ +- 도움말과 μ˜ˆμ‹œ 제곡 + +### 2.5 일관성 (Consistency) + +**λͺ¨λ“  ν™”λ©΄μ—μ„œ λ™μΌν•œ νŒ¨ν„΄μ„ μœ μ§€ν•©λ‹ˆλ‹€** + +- μ»΄ν¬λ„ŒνŠΈ μž¬μ‚¬μš© +- μΌκ΄€λœ 색상과 νƒ€μ΄ν¬κ·Έλž˜ν”Ό +- 예츑 κ°€λŠ₯ν•œ μΈν„°λž™μ…˜ + +--- + +## 3. 색상 μ‹œμŠ€ν…œ + +### 3.1 Primary Color (μ£Ό 색상) + +#### KT Red - λΈŒλžœλ“œ 정체성 + +``` +Main: #E31E24 // CTA λ²„νŠΌ, μ£Όμš” μ•‘μ…˜, ν™œμ„±ν™” μƒνƒœ +Light: #FF4D52 // ν˜Έλ²„ μƒνƒœ, λ°°κ²½ κ°•μ‘° +Dark: #C71820 // 눌림 μƒνƒœ, μ§„ν•œ κ°•μ‘° + +μ‚¬μš© μ˜ˆμ‹œ: +- Primary Button λ°°κ²½ +- ν™œμ„±ν™”λœ λ„€λΉ„κ²Œμ΄μ…˜ μ•„μ΄ν…œ +- μ€‘μš”ν•œ μ•Œλ¦Ό λ°°μ§€ +- 링크 ν…μŠ€νŠΈ +``` + +#### 색상 μ ‘κ·Όμ„± + +- White λ°°κ²½ λŒ€λΉ„: 7.2:1 (WCAG AAA) +- Gray-100 λ°°κ²½ λŒ€λΉ„: 6.8:1 (WCAG AAA) + +### 3.2 Secondary Color (보쑰 색상) + +#### AI Blue - ν˜μ‹ κ³Ό μ‹ λ’° + +``` +Main: #0066FF // AI κΈ°λŠ₯ κ°•μ‘°, 정보 μ•„μ΄μ½˜ +Light: #4D94FF // AI λ‘œλ”© λ°°κ²½, μ•ˆλ‚΄ μ˜μ—­ +Dark: #004DBF // AI ν”„λ‘œμ„ΈμŠ€ μ™„λ£Œ + +μ‚¬μš© μ˜ˆμ‹œ: +- AI 처리 쀑 μƒνƒœ ν‘œμ‹œ +- 정보 제곡 μ˜μ—­ +- νŠΈλ Œλ“œ 뢄석 차트 +- 도움말 μ•„μ΄μ½˜ +``` + +### 3.3 Grayscale (νšŒμƒ‰μ‘°) + +``` +Black (Gray-900): #1A1A1A // μ£Όμš” 제λͺ©, λ³Έλ¬Έ ν…μŠ€νŠΈ +Gray-700: #4A4A4A // 보쑰 ν…μŠ€νŠΈ, μ•„μ΄μ½˜ +Gray-500: #9E9E9E // λΉ„ν™œμ„±ν™” ν…μŠ€νŠΈ, ν”Œλ ˆμ΄μŠ€ν™€λ” +Gray-300: #D9D9D9 // ꡬ뢄선, λΉ„ν™œμ„±ν™” ν…Œλ‘λ¦¬ +Gray-100: #F5F5F5 // λ°°κ²½, λΉ„ν™œμ„±ν™” λ²„νŠΌ +White (Gray-50): #FFFFFF // μΉ΄λ“œ λ°°κ²½, κΈ°λ³Έ λ°°κ²½ + +색상 λŒ€λΉ„ (White λ°°κ²½ κΈ°μ€€): +- Gray-900: 14.2:1 (AAA) +- Gray-700: 8.5:1 (AAA) +- Gray-500: 4.6:1 (AA) +``` + +### 3.4 Semantic Colors (의미 색상) + +``` +Success (성곡): #00C853 // μ™„λ£Œ, 승인, 성곡 λ©”μ‹œμ§€ +Warning (κ²½κ³ ): #FFA000 // 주의, λŒ€κΈ° 쀑, 확인 ν•„μš” +Error (였λ₯˜): #D32F2F // 였λ₯˜, κ±°λΆ€, μ‚­μ œ 확인 +Info (정보): #0288D1 // μ•ˆλ‚΄, 팁, μΆ”κ°€ 정보 + +μ‚¬μš© μ˜ˆμ‹œ: +- Success: "μ΄λ²€νŠΈκ°€ μ„±κ³΅μ μœΌλ‘œ λ°°ν¬λ˜μ—ˆμŠ΅λ‹ˆλ‹€" +- Warning: "AI μ²˜λ¦¬κ°€ ν‰μ†Œλ³΄λ‹€ 였래 걸리고 μžˆμŠ΅λ‹ˆλ‹€" +- Error: "이미지 생성에 μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€" +- Info: "νŠΈλ Œλ“œ 뢄석 κ²°κ³Όλ₯Ό ν™•μΈν•˜μ„Έμš”" +``` + +### 3.5 Gradient (κ·ΈλΌλ°μ΄μ…˜) + +#### AI Feature Gradient + +``` +Primary Gradient: + background: linear-gradient(135deg, #E31E24 0%, #FF4D52 100%); + μ‚¬μš©: AI κΈ°λŠ₯ κ°•μ‘° μΉ΄λ“œ, 프리미엄 κΈ°λŠ₯ + +Secondary Gradient: + background: linear-gradient(135deg, #0066FF 0%, #4D94FF 100%); + μ‚¬μš©: AI 처리 쀑 λ°°κ²½, 정보 κ°•μ‘° μ˜μ—­ +``` + +--- + +## 4. νƒ€μ΄ν¬κ·Έλž˜ν”Ό μ‹œμŠ€ν…œ + +### 4.1 Font Family + +**Primary: Pretendard** + +```css +font-family: "Pretendard", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", + "Helvetica Neue", system-ui, sans-serif; +``` + +**선택 이유:** + +- ν•œκΈ€ 가독성이 뛰어남 +- λ‹€μ–‘ν•œ Font Weight 지원 (100~900) +- λͺ¨λ˜ν•˜κ³  κΉ”λ”ν•œ λ””μžμΈ +- Variable Font μ§€μ›μœΌλ‘œ μ„±λŠ₯ μ΅œμ ν™” + +### 4.2 Type Scale (Mobile First) + +``` +Display (메인 타이틀) +- Size: 28px +- Weight: 700 (Bold) +- Line Height: 1.3 (36px) +- Letter Spacing: -0.5px +- μ‚¬μš©: 메인 λŒ€μ‹œλ³΄λ“œ 타이틀, λžœλ”© ν™”λ©΄ + +H1 (ν™”λ©΄ 제λͺ©) +- Size: 24px +- Weight: 700 (Bold) +- Line Height: 1.3 (31px) +- Letter Spacing: -0.3px +- μ‚¬μš©: 각 ν™”λ©΄μ˜ 메인 제λͺ© + +H2 (μ„Ήμ…˜ 제λͺ©) +- Size: 20px +- Weight: 700 (Bold) +- Line Height: 1.4 (28px) +- Letter Spacing: -0.2px +- μ‚¬μš©: μΉ΄λ“œ κ·Έλ£Ή, μ„Ήμ…˜ ꡬ뢄 + +H3 (μΉ΄λ“œ 제λͺ©) +- Size: 18px +- Weight: 600 (SemiBold) +- Line Height: 1.4 (25px) +- Letter Spacing: 0px +- μ‚¬μš©: μΉ΄λ“œ 제λͺ©, λͺ¨λ‹¬ 제λͺ© + +Body-Large (큰 λ³Έλ¬Έ) +- Size: 16px +- Weight: 400 (Regular) +- Line Height: 1.5 (24px) +- Letter Spacing: 0px +- μ‚¬μš©: μž…λ ₯ ν•„λ“œ, μ€‘μš”ν•œ λ³Έλ¬Έ + +Body-Medium (κΈ°λ³Έ λ³Έλ¬Έ) +- Size: 14px +- Weight: 400 (Regular) +- Line Height: 1.5 (21px) +- Letter Spacing: 0px +- μ‚¬μš©: 일반 λ³Έλ¬Έ, μ„€λͺ… ν…μŠ€νŠΈ + +Body-Small (μž‘μ€ λ³Έλ¬Έ) +- Size: 12px +- Weight: 400 (Regular) +- Line Height: 1.5 (18px) +- Letter Spacing: 0px +- μ‚¬μš©: μΊ‘μ…˜, 보쑰 정보, 메타 데이터 + +Button (λ²„νŠΌ λ ˆμ΄λΈ”) +- Size: 16px +- Weight: 600 (SemiBold) +- Line Height: 1.5 (24px) +- Letter Spacing: 0px +- μ‚¬μš©: λͺ¨λ“  λ²„νŠΌ ν…μŠ€νŠΈ +``` + +### 4.3 Font Weights + +``` +Regular (400): 일반 λ³Έλ¬Έ, μ„€λͺ… +Medium (500): κ°•μ‘°ν•˜κ³  싢은 λ³Έλ¬Έ +SemiBold (600): λ²„νŠΌ, μ€‘μš” 정보 +Bold (700): 제λͺ©, ν—€λ”© +``` + +### 4.4 Responsive Typography + +**Tablet (768px~)** + +``` +Display: 32px (+4px) +H1: 28px (+4px) +H2: 22px (+2px) +H3: 20px (+2px) +Body-L: 18px (+2px) +Body-M: 16px (+2px) +Body-S: 14px (+2px) +Button: 16px (μœ μ§€) +``` + +**Desktop (1024px~)** + +``` +Display: 36px (+8px) +H1: 32px (+8px) +H2: 24px (+4px) +H3: 20px (+2px) +Body-L: 18px (+2px) +Body-M: 16px (+2px) +Body-S: 14px (+2px) +Button: 16px (μœ μ§€) +``` + +--- + +## 5. 간격 μ‹œμŠ€ν…œ (Spacing) + +### 5.1 Base Unit + +**4px Grid System** - λͺ¨λ“  간격은 4의 배수 + +### 5.2 Spacing Scale + +``` +XS (Extra Small): 4px // 맀우 μž‘μ€ μš”μ†Œ κ°„ (μ•„μ΄μ½˜-ν…μŠ€νŠΈ) +S (Small): 8px // κ΄€λ ¨ μš”μ†Œ κ°„ (λ ˆμ΄λΈ”-μž…λ ₯, λ²„νŠΌ λ‚΄λΆ€) +M (Medium): 16px // μ„Ήμ…˜ λ‚΄ μš”μ†Œ κ°„ (μΉ΄λ“œ λ‚΄ ν•­λͺ©) +L (Large): 24px // μΉ΄λ“œ λ‚΄λΆ€ νŒ¨λ”©, μ„Ήμ…˜ 제λͺ© ν•˜λ‹¨ +XL (Extra Large): 32px // μ„Ήμ…˜ κ°„ 간격 +2XL (2X Large): 48px // ν™”λ©΄ μƒν•˜λ‹¨ μ—¬λ°± +``` + +### 5.3 Component Spacing + +#### Button + +``` +Padding (μ„Έλ‘œ x κ°€λ‘œ): +- Large: 16px x 24px (높이 48px) +- Medium: 12px x 20px (높이 44px) +- Small: 8px x 16px (높이 36px) + +Button κ°„ 간격: 12px (S + XS) +``` + +#### Card + +``` +λ‚΄λΆ€ νŒ¨λ”©: 24px (L) +μΉ΄λ“œ κ°„ 간격: 16px (M) +``` + +#### Input Field + +``` +λ‚΄λΆ€ νŒ¨λ”©: 16px (M) +λ ˆμ΄λΈ”-μž…λ ₯ 간격: 8px (S) +μž…λ ₯ ν•„λ“œ κ°„ 간격: 16px (M) +``` + +#### Screen Margins + +``` +Mobile: 20px (μ–‘μͺ½) +Tablet: 40px (μ–‘μͺ½) +Desktop: 80px (μ–‘μͺ½, μ΅œλŒ€ 1200px container) +``` + +### 5.4 Touch Target + +**WCAG 2.1 AA μ€€μˆ˜** + +``` +μ΅œμ†Œ ν„°μΉ˜ μ˜μ—­: 44 x 44px +ꢌμž₯ ν„°μΉ˜ μ˜μ—­: 48 x 48px + +적용 λŒ€μƒ: +- λͺ¨λ“  λ²„νŠΌ +- νƒ­ ν•­λͺ© +- μ²΄ν¬λ°•μŠ€, λΌλ””μ˜€ λ²„νŠΌ +- 링크가 μžˆλŠ” μΉ΄λ“œ +``` + +--- + +## 6. μ»΄ν¬λ„ŒνŠΈ μŠ€νƒ€μΌ + +### 6.1 Button (λ²„νŠΌ) + +#### Primary Button + +``` +λ°°κ²½: #E31E24 (KT Red) +ν…μŠ€νŠΈ: #FFFFFF (White) +λ‘₯κ·Ό λͺ¨μ„œλ¦¬: 8px +그림자: 0 2px 4px rgba(227, 30, 36, 0.2) + +μƒνƒœλ³„: +- Default: λ°°κ²½ #E31E24 +- Hover: λ°°κ²½ #FF4D52 (10% 밝게) +- Pressed: λ°°κ²½ #C71820 (10% μ–΄λ‘‘κ²Œ) +- Disabled: λ°°κ²½ #D9D9D9, ν…μŠ€νŠΈ #9E9E9E +``` + +#### Secondary Button + +``` +λ°°κ²½: #FFFFFF (White) +ν…μŠ€νŠΈ: #E31E24 (KT Red) +ν…Œλ‘λ¦¬: 2px solid #E31E24 +λ‘₯κ·Ό λͺ¨μ„œλ¦¬: 8px + +μƒνƒœλ³„: +- Default: ν…Œλ‘λ¦¬ #E31E24 +- Hover: λ°°κ²½ #FFF5F5 (5% Red tint) +- Pressed: λ°°κ²½ #FFEBEB (10% Red tint) +- Disabled: ν…Œλ‘λ¦¬ #D9D9D9, ν…μŠ€νŠΈ #9E9E9E +``` + +#### Text Button + +``` +λ°°κ²½: μ—†μŒ +ν…μŠ€νŠΈ: #E31E24 (KT Red) +λ‘₯κ·Ό λͺ¨μ„œλ¦¬: 8px + +μƒνƒœλ³„: +- Default: ν…μŠ€νŠΈ #E31E24 +- Hover: λ°°κ²½ #FFF5F5 +- Pressed: λ°°κ²½ #FFEBEB +- Disabled: ν…μŠ€νŠΈ #9E9E9E +``` + +#### Button Sizes + +``` +Large: +- 높이: 48px +- νŒ¨λ”©: 16px x 24px +- 폰트: Button (16px SemiBold) +- μ‚¬μš©: μ£Όμš” CTA + +Medium: +- 높이: 44px +- νŒ¨λ”©: 12px x 20px +- 폰트: Body-M (14px SemiBold) +- μ‚¬μš©: 일반 μ•‘μ…˜ + +Small: +- 높이: 36px +- νŒ¨λ”©: 8px x 16px +- 폰트: Body-S (12px SemiBold) +- μ‚¬μš©: 보쑰 μ•‘μ…˜ +``` + +### 6.2 Card (μΉ΄λ“œ) + +#### Default Card + +``` +λ°°κ²½: #FFFFFF (White) +ν…Œλ‘λ¦¬: 1px solid #E0E0E0 +λ‘₯κ·Ό λͺ¨μ„œλ¦¬: 12px +그림자: 0 2px 8px rgba(0, 0, 0, 0.08) +λ‚΄λΆ€ νŒ¨λ”©: 24px + +μƒνƒœλ³„: +- Default: ν…Œλ‘λ¦¬ #E0E0E0 +- Hover: ν…Œλ‘λ¦¬ #E31E24, 그림자 0 4px 12px rgba(227, 30, 36, 0.12) +- Selected: ν…Œλ‘λ¦¬ 2px solid #E31E24 +``` + +#### Event Card (이벀트 μΉ΄λ“œ) + +``` +λ°°κ²½: #FFFFFF +λ‘₯κ·Ό λͺ¨μ„œλ¦¬: 12px +그림자: 0 2px 8px rgba(0, 0, 0, 0.08) + +ꡬ쑰: +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ [이미지 썸넀일 16:9] β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ H3 제λͺ© β”‚ +β”‚ Body-S 메타 정보 β”‚ +β”‚ [μƒνƒœ λ°°μ§€] [CTA λ²„νŠΌ] β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +#### Selection Card (μ„ νƒν˜• μΉ΄λ“œ) + +``` +μ‚¬μš©: μ˜΅μ…˜ 선택 ν™”λ©΄ (09-μ½˜ν…μΈ λ―Έλ¦¬λ³΄κΈ°.html) +νŠΉμ§•: μΉ΄λ“œ 전체가 선택 κ°€λŠ₯ν•œ μΈν„°λž™ν‹°λΈŒ μ˜μ—­ + +λ°°κ²½: #FFFFFF +ν…Œλ‘λ¦¬: 3px solid transparent +λ‘₯κ·Ό λͺ¨μ„œλ¦¬: 12px +그림자: 0 2px 8px rgba(0, 0, 0, 0.08) +λ‚΄λΆ€ νŒ¨λ”©: 24px +μ»€μ„œ: pointer + +μƒνƒœλ³„: +- Default: + - ν…Œλ‘λ¦¬ transparent + - 그림자 0 2px 8px rgba(0, 0, 0, 0.08) + +- Hover: + - transform: translateY(-2px) + - 그림자 0 8px 16px rgba(0, 0, 0, 0.1) + +- Selected: + - ν…Œλ‘λ¦¬ 3px solid #E31E24 + - 그림자 0 4px 12px rgba(227, 30, 36, 0.2) + - 우츑 상단 체크 λ°°μ§€ ν‘œμ‹œ + +ꡬ쑰: +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ [βœ“] β”‚ ← 선택 λ°°μ§€ (쑰건뢀) +β”‚ [미리보기 이미지 1:1] β”‚ +β”‚ β”‚ +β”‚ H3 μ˜΅μ…˜ 제λͺ© β”‚ +β”‚ Body-S μ„€λͺ… β”‚ +β”‚ β”‚ +β”‚ [크게보기] β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + +Selected Badge (체크 λ°°μ§€): +- μœ„μΉ˜: 우츑 상단 (absolute) +- 크기: 32 x 32px +- λ°°κ²½: #E31E24 +- μ•„μ΄μ½˜: check (White, 20px) +- λ‘₯κ·Ό λͺ¨μ„œλ¦¬: 50% (μ›ν˜•) +- Display: none (κΈ°λ³Έ), flex (선택 μ‹œ) +- z-index: 10 + +Image Preview: +- λΉ„μœ¨: 1:1 (aspect-ratio) +- λ°°κ²½: #F5F5F5 (ν”Œλ ˆμ΄μŠ€ν™€λ”) +- λ‘₯κ·Ό λͺ¨μ„œλ¦¬: 12px +- object-fit: cover + +Radio Button: +- Display: none (μˆ¨κΉ€) +- κΈ°λŠ₯: μœ μ§€ (폼 제좜용) +- μ ‘κ·Όμ„±: ν‚€λ³΄λ“œ λ„€λΉ„κ²Œμ΄μ…˜ 지원 + +μ „ν™˜ μ• λ‹ˆλ©”μ΄μ…˜: +- Duration: 0.3s +- Easing: ease + +μ£Όμ˜μ‚¬ν•­: +- μΉ΄λ“œ λ‚΄λΆ€ λ²„νŠΌ 클릭 μ‹œ 이벀트 버블링 λ°©μ§€ ν•„μš” + - event.stopPropagation() μ‚¬μš© +- μΉ΄λ“œ 클릭과 보쑰 μ•‘μ…˜ λ²„νŠΌ 클릭 ꡬ뢄 +- ν‚€λ³΄λ“œ μ ‘κ·Όμ„±: Enter/Space둜 선택 κ°€λŠ₯ +``` + +#### Stat Card (μ§€ν‘œ μΉ΄λ“œ) + +``` +λ°°κ²½: Gradient λ˜λŠ” Solid +λ‘₯κ·Ό λͺ¨μ„œλ¦¬: 16px +λ‚΄λΆ€ νŒ¨λ”©: 24px + +ꡬ쑰: +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ [μ•„μ΄μ½˜] Body-S λ ˆμ΄λΈ” β”‚ +β”‚ Display 수치 β”‚ +β”‚ Body-S λ³€ν™”μœ¨ +32% ↑ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +### 6.3 Input Field (μž…λ ₯ ν•„λ“œ) + +#### Text Input + +``` +λ°°κ²½: #FFFFFF (White) +ν…Œλ‘λ¦¬: 1px solid #D9D9D9 +λ‘₯κ·Ό λͺ¨μ„œλ¦¬: 8px +높이: 48px +λ‚΄λΆ€ νŒ¨λ”©: 16px +폰트: Body-L (16px Regular) + +μƒνƒœλ³„: +- Default: ν…Œλ‘λ¦¬ #D9D9D9 +- Focus: ν…Œλ‘λ¦¬ 2px solid #0066FF, 그림자 0 0 0 4px rgba(0, 102, 255, 0.1) +- Error: ν…Œλ‘λ¦¬ 2px solid #D32F2F, 그림자 0 0 0 4px rgba(211, 47, 47, 0.1) +- Disabled: λ°°κ²½ #F5F5F5, ν…Œλ‘λ¦¬ #E0E0E0, ν…μŠ€νŠΈ #9E9E9E +- Filled: λ°°κ²½ μœ μ§€, ν…μŠ€νŠΈ #1A1A1A +``` + +#### Textarea + +``` +λ°°κ²½: #FFFFFF +ν…Œλ‘λ¦¬: 1px solid #D9D9D9 +λ‘₯κ·Ό λͺ¨μ„œλ¦¬: 8px +μ΅œμ†Œ 높이: 120px (μ•½ 5쀄) +λ‚΄λΆ€ νŒ¨λ”©: 16px +폰트: Body-M (14px Regular) +Line Height: 1.5 + +Resize: vertical (μ„Έλ‘œ λ°©ν–₯만) +``` + +#### Editable Field (인라인 νŽΈμ§‘) + +``` +μ‚¬μš©: 08-AIμ΄λ²€νŠΈμΆ”μ²œ.html (제λͺ©, κ²½ν’ˆ νŽΈμ§‘) +νŠΉμ§•: contenteditable을 ν™œμš©ν•œ 인라인 νŽΈμ§‘ + +λ°°κ²½: 투λͺ… (κΈ°λ³Έ), #F5F5F5 (hover), #FFFFFF (focus) +ν…Œλ‘λ¦¬: 1px dashed #D9D9D9 +λ‘₯κ·Ό λͺ¨μ„œλ¦¬: 4px +λ‚΄λΆ€ νŒ¨λ”©: 4px 8px +폰트: λ¬Έλ§₯에 따름 (제λͺ©: H3, κ²½ν’ˆ: Body) +μ»€μ„œ: text + +μƒνƒœλ³„: +- Default: + - ν…Œλ‘λ¦¬: 1px dashed #D9D9D9 + - λ°°κ²½: 투λͺ… + +- Hover: + - ν…Œλ‘λ¦¬: 1px dashed #E31E24 + - λ°°κ²½: #F5F5F5 + +- Focus: + - outline: none + - ν…Œλ‘λ¦¬: 1px solid #E31E24 + - λ°°κ²½: #FFFFFF + +μ „ν™˜ μ• λ‹ˆλ©”μ΄μ…˜: +- Duration: 200ms (Fast) +- Easing: ease-out + +μ ‘κ·Όμ„±: +- contenteditable="true" +- role="textbox" +- aria-label="νŽΈμ§‘ κ°€λŠ₯ν•œ ν•„λ“œλͺ…" + +μ£Όμ˜μ‚¬ν•­: +- 빈 κ°’ λ°©μ§€ (μ΅œμ†Œ 1자 이상) +- Enter ν‚€λ‘œ νŽΈμ§‘ μ™„λ£Œ (blur) +- maxLength μ†μ„±μœΌλ‘œ 길이 μ œν•œ +``` + +#### Placeholder + +``` +색상: #9E9E9E (Gray-500) +폰트: Body-L (16px Regular) +μŠ€νƒ€μΌ: italic (선택적) +``` + +### 6.4 Checkbox & Radio + +#### Checkbox + +``` +크기: 24 x 24px (ν„°μΉ˜ μ˜μ—­ 44px) +λ‘₯κ·Ό λͺ¨μ„œλ¦¬: 4px +ν…Œλ‘λ¦¬: 2px solid #D9D9D9 + +μƒνƒœλ³„: +- Unchecked: λ°°κ²½ White, ν…Œλ‘λ¦¬ #D9D9D9 +- Checked: λ°°κ²½ #E31E24, 체크마크 White +- Disabled: λ°°κ²½ #F5F5F5, ν…Œλ‘λ¦¬ #E0E0E0 +``` + +#### Radio Button + +``` +크기: 24 x 24px (ν„°μΉ˜ μ˜μ—­ 44px) +λ‘₯κ·Ό λͺ¨μ„œλ¦¬: 50% (μ›ν˜•) +ν…Œλ‘λ¦¬: 2px solid #D9D9D9 + +μƒνƒœλ³„: +- Unselected: λ°°κ²½ White, ν…Œλ‘λ¦¬ #D9D9D9 +- Selected: ν…Œλ‘λ¦¬ #E31E24, λ‚΄λΆ€ 점 12px #E31E24 +- Disabled: λ°°κ²½ #F5F5F5, ν…Œλ‘λ¦¬ #E0E0E0 +``` + +### 6.5 Budget Navigation (μ˜ˆμ‚° 선택 νƒ­) + +``` +μ‚¬μš©: 08-AIμ΄λ²€νŠΈμΆ”μ²œ.html +νŠΉμ§•: Sticky λ„€λΉ„κ²Œμ΄μ…˜μœΌλ‘œ μ˜ˆμ‚° μ„Ήμ…˜ 이동 + +λ°°κ²½: #F5F5F5 (배경색) +μœ„μΉ˜: sticky, top 56px (Header μ•„λž˜) +z-index: 10 +νŒ¨λ”©: 16px 0 + +ꡬ쑰: +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ [πŸ’° μ €λΉ„μš©] [πŸ’°πŸ’° μ€‘λΉ„μš©] [πŸ’°πŸ’°πŸ’° κ³ λΉ„μš©] β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + +λ²„νŠΌ: +- 크기: Medium (44px 높이) +- 간격: 8px +- flex-1 (κ· λ“± λΆ„λ°°) + +μƒνƒœλ³„: +- Default: + - λ°°κ²½: White + - ν…μŠ€νŠΈ: #4A4A4A + - ν…Œλ‘λ¦¬: 1px solid #E0E0E0 + +- Active: + - λ°°κ²½: #E31E24 + - ν…μŠ€νŠΈ: White + - ν…Œλ‘λ¦¬: none + - 그림자: 0 2px 4px rgba(227, 30, 36, 0.2) + +- Hover (λΉ„ν™œμ„±ν™” νƒ­): + - λ°°κ²½: #FFF5F5 + - ν…Œλ‘λ¦¬: 1px solid #E31E24 + +μƒν˜Έμž‘μš©: +- 클릭 μ‹œ: ν•΄λ‹Ή μ˜ˆμ‚° μ„Ήμ…˜μœΌλ‘œ smooth scroll +- Scroll μ‹œ: ν˜„μž¬ λ³΄μ΄λŠ” μ„Ήμ…˜μ— 맞좰 Active μƒνƒœ λ³€κ²½ +``` + +### 6.6 Option Card (이벀트 μ˜΅μ…˜ μΉ΄λ“œ) + +``` +μ‚¬μš©: 08-AIμ΄λ²€νŠΈμΆ”μ²œ.html +νŠΉμ§•: 온라인/μ˜€ν”„λΌμΈ λ°°μ§€ + Editable Field + Radio 선택 + +λ°°κ²½: #FFFFFF +ν…Œλ‘λ¦¬: 1px solid #E0E0E0 +λ‘₯κ·Ό λͺ¨μ„œλ¦¬: 12px +그림자: 0 2px 8px rgba(0, 0, 0, 0.08) +λ‚΄λΆ€ νŒ¨λ”©: 24px + +ꡬ쑰: +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ [온라인] λ°°μ§€ [β—―] β”‚ +β”‚ β”‚ +β”‚ 제λͺ© (editable) β”‚ +β”‚ κ²½ν’ˆλͺ… (editable) β”‚ +β”‚ β”‚ +β”‚ μ°Έμ—¬ 방법: ... β”‚ +β”‚ β”‚ +β”‚ πŸ“Š μ˜ˆμƒ μ°Έμ—¬μž: 200λͺ… β”‚ +β”‚ πŸ’° μ˜ˆμƒ λΉ„μš©: 30λ§Œμ› β”‚ +β”‚ πŸ“ˆ μ˜ˆμƒ ROI: 180% β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + +온라인/μ˜€ν”„λΌμΈ λ°°μ§€: +- 크기: νŒ¨λ”© 4px 12px +- 폰트: Body-S (12px SemiBold) +- λ‘₯κ·Ό λͺ¨μ„œλ¦¬: 12px (pill) +- 온라인: λ°°κ²½ #E3F2FD (Blue), ν…μŠ€νŠΈ #0066FF +- μ˜€ν”„λΌμΈ: λ°°κ²½ #FCE4EC (Pink), ν…μŠ€νŠΈ #E31E24 + +Radio λ²„νŠΌ: +- μœ„μΉ˜: 우츑 상단 +- 크기: 24 x 24px +- Display: visible (κΈ°λ³Έ λ³΄μž„) + +Editable Field: +- μŠ€νƒ€μΌ: 인라인 νŽΈμ§‘ (상세 λ‚΄μš©μ€ 6.3 μ°Έμ‘°) +- Hover: 점선 ν…Œλ‘λ¦¬λ‘œ νŽΈμ§‘ κ°€λŠ₯ ν‘œμ‹œ +- Focus: μ‹€μ„  ν…Œλ‘λ¦¬λ‘œ λ³€κ²½ + +톡계 정보: +- 폰트: Body-S (12px Regular) +- 색상: #4A4A4A (Secondary) +- μ•„μ΄μ½˜: Material Icons (16px) +``` + +### 6.7 Bottom Navigation + +``` +λ°°κ²½: #FFFFFF (White) +높이: 60px +그림자: 0 -2px 8px rgba(0, 0, 0, 0.08) +μ•„μ΄ν…œ 개수: 4개 (ν™ˆ, 이벀트, 뢄석, 마이) + +μ•„μ΄ν…œ ꡬ쑰: +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ [μ•„μ΄μ½˜] β”‚ +β”‚ λ ˆμ΄λΈ” β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + +μ•„μ΄μ½˜: +- 크기: 24 x 24px +- μŠ€νƒ€μΌ: Outlined (κΈ°λ³Έ), Filled (ν™œμ„±ν™”) + +λ ˆμ΄λΈ”: +- 폰트: Body-S (12px Regular) + +색상: +- λΉ„ν™œμ„±ν™”: μ•„μ΄μ½˜/ν…μŠ€νŠΈ #9E9E9E +- ν™œμ„±ν™”: μ•„μ΄μ½˜/ν…μŠ€νŠΈ #E31E24 +- λ°°κ²½: ν™œμ„±ν™” μ‹œ 투λͺ… + +간격: +- μ•„μ΄μ½˜-λ ˆμ΄λΈ”: 4px (XS) +``` + +### 6.8 Stepper (단계 ν‘œμ‹œ) + +#### Progress Stepper (AI 이벀트 생성) + +``` +전체 높이: 48px +λ°°κ²½: #F5F5F5 +μ§„ν–‰λ₯  λ°”: #E31E24 +λ‘₯κ·Ό λͺ¨μ„œλ¦¬: 24px + +ꡬ쑰: +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘ 3/7 단계 β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + +ν…μŠ€νŠΈ: +- 폰트: Body-M (14px SemiBold) +- 색상: #1A1A1A +``` + +#### Step Indicator (단계별 ν‘œμ‹œ) + +``` +β”Œβ”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β” +β”‚ βœ“ │─ β”‚ 2 │─ β”‚ 3 β”‚ +β””β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”˜ +μ™„λ£Œ 진행쀑 λŒ€κΈ° + +원 크기: 32px +μ„  λ‘κ»˜: 2px +간격: 8px (μ„  길이 κ°€λ³€) + +색상: +- μ™„λ£Œ: λ°°κ²½ #00C853, μ•„μ΄μ½˜ White +- 진행쀑: λ°°κ²½ #E31E24, 숫자 White +- λŒ€κΈ°: λ°°κ²½ #F5F5F5, ν…Œλ‘λ¦¬ #D9D9D9, 숫자 #9E9E9E +``` + +--- + +## 7. λ°˜μ‘ν˜• 브레이크포인트 + +### 7.1 Breakpoints + +``` +Mobile (κΈ°λ³Έ): +- Range: 320px ~ 767px +- Container: 100% - 40px (μ–‘μͺ½ 20px λ§ˆμ§„) +- Columns: 4 columns +- Gutter: 16px +- λ ˆμ΄μ•„μ›ƒ: 1μ—΄ μŠ€νƒ + +Tablet: +- Range: 768px ~ 1023px +- Container: 100% - 80px (μ–‘μͺ½ 40px λ§ˆμ§„) +- Columns: 8 columns +- Gutter: 24px +- λ ˆμ΄μ•„μ›ƒ: 2μ—΄ κ·Έλ¦¬λ“œ + +Desktop: +- Range: 1024px 이상 +- Container: Max 1200px (쀑앙 μ •λ ¬) +- Columns: 12 columns +- Gutter: 32px +- λ ˆμ΄μ•„μ›ƒ: 3μ—΄ κ·Έλ¦¬λ“œ + μ‚¬μ΄λ“œλ°” +``` + +### 7.2 CSS Media Queries + +```css +/* Mobile First - κΈ°λ³Έ μŠ€νƒ€μΌ */ +.component { + /* 320px ~ 767px */ +} + +/* Tablet */ +@media (min-width: 768px) { + .component { + /* Tablet μŠ€νƒ€μΌ */ + } +} + +/* Desktop */ +@media (min-width: 1024px) { + .component { + /* Desktop μŠ€νƒ€μΌ */ + } +} +``` + +### 7.3 Grid System + +#### Mobile Grid (4 Columns) + +``` +β”Œβ”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β” +β”‚ 1 β”‚ 2 β”‚ 3 β”‚ 4 β”‚ +β””β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”˜ + +μ‚¬μš© 예: +- Full Width: span 4 +- Half Width: span 2 +``` + +#### Tablet Grid (8 Columns) + +``` +β”Œβ”€β”€β”¬β”€β”€β”¬β”€β”€β”¬β”€β”€β”¬β”€β”€β”¬β”€β”€β”¬β”€β”€β”¬β”€β”€β” +β”‚ 1β”‚ 2β”‚ 3β”‚ 4β”‚ 5β”‚ 6β”‚ 7β”‚ 8β”‚ +β””β”€β”€β”΄β”€β”€β”΄β”€β”€β”΄β”€β”€β”΄β”€β”€β”΄β”€β”€β”΄β”€β”€β”΄β”€β”€β”˜ + +μ‚¬μš© 예: +- Full Width: span 8 +- Half Width: span 4 (2개 μΉ΄λ“œ) +- 1/3 Width: span 2-3 (3개 μΉ΄λ“œ) +``` + +#### Desktop Grid (12 Columns) + +``` +β”Œβ”¬β”¬β”¬β”¬β”¬β”¬β”¬β”¬β”¬β”¬β”¬β”¬β” +β”‚β”‚β”‚β”‚β”‚β”‚β”‚β”‚β”‚β”‚β”‚β”‚β”‚ +β””β”΄β”΄β”΄β”΄β”΄β”΄β”΄β”΄β”΄β”΄β”΄β”΄β”˜ + +μ‚¬μš© 예: +- Main Content: span 8-9 (μ™Όμͺ½) +- Sidebar: span 3-4 (였λ₯Έμͺ½) +- 3 Column Cards: span 4 each +``` + +--- + +## 8. μ„œλΉ„μŠ€ νŠΉν™” μ»΄ν¬λ„ŒνŠΈ + +### 8.1 AI 처리 μƒνƒœ μ»΄ν¬λ„ŒνŠΈ + +#### Loading Skeleton + +``` +λ°°κ²½: #F5F5F5 +μ• λ‹ˆλ©”μ΄μ…˜: Shimmer (μ’Œβ†’μš° λ°˜μ§μž„) +Duration: 1.5s infinite + +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ β–“β–“β–“β–“β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘ β”‚ ← 제λͺ© +β”‚ β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘ β”‚ ← λ³Έλ¬Έ +β”‚ β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘ β”‚ +β”‚ β–“β–“β–“β–‘β–‘β–‘ β–“β–“β–“β–‘β–‘β–‘ β–“β–“β–“β–‘β–‘ β”‚ ← λ²„νŠΌλ“€ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + +μ‚¬μš©: AI 이미지 생성, νŠΈλ Œλ“œ 뢄석 λ“± +``` + +#### Progress Indicator (AI 단계별) + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ πŸ€– AIκ°€ νŠΈλ Œλ“œλ₯Ό λΆ„μ„μ€‘μž…λ‹ˆλ‹€ β”‚ +β”‚ β”‚ +β”‚ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘ 35% β”‚ +β”‚ β”‚ +β”‚ μ˜ˆμƒ μ‹œκ°„: μ•½ 2초 λ‚¨μŒ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + +λ°°κ²½: White +ν…Œλ‘λ¦¬: 1px solid #E0E0E0 +μ§„ν–‰λ°”: #0066FF (AI Blue) +ν…μŠ€νŠΈ: Body-M #4A4A4A +``` + +#### Spinner (κ°„λ‹¨ν•œ λ‘œλ”©) + +``` +크기: 32px +λ‘κ»˜: 3px +색상: #E31E24 (Primary) λ˜λŠ” #0066FF (AI Blue) +μ• λ‹ˆλ©”μ΄μ…˜: rotate 360deg, 0.8s linear infinite + +μ‚¬μš©: λ²„νŠΌ λ‚΄λΆ€, μž‘μ€ μ•‘μ…˜ λ‘œλ”© +``` + +### 8.2 AI κ²°κ³Ό μΉ΄λ“œ + +#### AI 생성 μ˜΅μ…˜ μΉ΄λ“œ (μ„ νƒν˜•) + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ β—― [이미지 미리보기] β”‚ +β”‚ β”‚ +β”‚ H3 μ˜΅μ…˜ 제λͺ© β”‚ +β”‚ Body-S μ„€λͺ… β”‚ +β”‚ [μž¬μƒμ„± λ²„νŠΌ] β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + +μƒνƒœλ³„: +- 비선택: ν…Œλ‘λ¦¬ 1px #E0E0E0 +- 선택: ν…Œλ‘λ¦¬ 2px #E31E24, λ°°κ²½ #FFF5F5 +- ν˜Έλ²„: 그림자 0 4px 12px rgba(0,0,0,0.1) + +λΌλ””μ˜€ λ²„νŠΌ: +- μœ„μΉ˜: μ’Œμƒλ‹¨ λ˜λŠ” μš°μƒλ‹¨ +- 크기: 24px +``` + +#### AI μΆ”μ²œ λ°°μ§€ + +``` +λ°°κ²½: linear-gradient(135deg, #0066FF 0%, #4D94FF 100%) +ν…μŠ€νŠΈ: White +폰트: Body-S (12px SemiBold) +λ‘₯κ·Ό λͺ¨μ„œλ¦¬: 12px (pill ν˜•νƒœ) +νŒ¨λ”©: 4px 12px +μœ„μΉ˜: μΉ΄λ“œ μ’Œμƒλ‹¨ μ ˆλŒ€ 배치 + +ν…μŠ€νŠΈ: "AI μΆ”μ²œ" λ˜λŠ” "인기" +``` + +### 8.3 Real-time Dashboard Components + +#### KPI Card (핡심 μ§€ν‘œ) + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ πŸ“Š μ°Έμ—¬μž 수 β”‚ +β”‚ β”‚ +β”‚ 152λͺ… β”‚ +β”‚ +32% ↑ μ „μ£Ό λŒ€λΉ„ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + +λ°°κ²½: White +λ‘₯κ·Ό λͺ¨μ„œλ¦¬: 16px +그림자: 0 2px 8px rgba(0,0,0,0.08) +νŒ¨λ”©: 24px + +ꡬ쑰: +- μ•„μ΄μ½˜ + λ ˆμ΄λΈ”: Body-S #4A4A4A +- 수치: Display (28px Bold) #1A1A1A +- λ³€ν™”μœ¨: Body-S, 증가 #00C853, κ°μ†Œ #D32F2F +``` + +#### Chart Container + +``` +λ°°κ²½: White +λ‘₯κ·Ό λͺ¨μ„œλ¦¬: 12px +그림자: 0 2px 8px rgba(0,0,0,0.08) +νŒ¨λ”©: 24px + +차트 색상: +- Primary Line: #E31E24 +- Secondary Line: #0066FF +- Grid: #F5F5F5 +- Axis Label: #9E9E9E +``` + +--- + +## 9. μΈν„°λž™μ…˜ νŒ¨ν„΄ + +### 9.1 Bottom Sheet + +``` +λ°°κ²½: White +λ‘₯κ·Ό λͺ¨μ„œλ¦¬: 24px 24px 0 0 (μƒλ‹¨λ§Œ) +μ΅œλŒ€ 높이: 80vh +Handle: 40px x 4px, #D9D9D9, 상단 쀑앙 +그림자: 0 -4px 12px rgba(0,0,0,0.15) + +μ• λ‹ˆλ©”μ΄μ…˜: +- Open: transform translateY(100% β†’ 0), 300ms ease-out +- Close: transform translateY(0 β†’ 100%), 250ms ease-in + +Backdrop: +- λ°°κ²½: rgba(0,0,0,0.5) +- 클릭 μ‹œ: Bottom Sheet λ‹«νž˜ +``` + +### 9.2 Toast (μ•Œλ¦Ό) + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ βœ“ μ΄λ²€νŠΈκ°€ μ„±κ³΅μ μœΌλ‘œ μ €μž₯λ˜μ—ˆμŠ΅λ‹ˆλ‹€ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + +λ°°κ²½: #1A1A1A (90% opacity) +ν…μŠ€νŠΈ: White +λ‘₯κ·Ό λͺ¨μ„œλ¦¬: 8px +νŒ¨λ”©: 16px 24px +μœ„μΉ˜: ν•˜λ‹¨ 쀑앙, Bottom Navigation μœ„ 80px +μžλ™ λ‹«νž˜: 3초 + +μ• λ‹ˆλ©”μ΄μ…˜: +- Show: opacity 0β†’1, translateY(20pxβ†’0), 200ms +- Hide: opacity 1β†’0, 200ms + +νƒ€μž…λ³„ μ•„μ΄μ½˜: +- Success: βœ“ (#00C853) +- Error: βœ• (#D32F2F) +- Info: β“˜ (#0288D1) +``` + +### 9.3 Modal (λͺ¨λ‹¬ λ‹€μ΄μ–Όλ‘œκ·Έ) + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ H2 제λͺ© [βœ•] β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ β”‚ +β”‚ Body-M λ³Έλ¬Έ λ‚΄μš© β”‚ +β”‚ β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ [μ·¨μ†Œ λ²„νŠΌ] [확인 λ²„νŠΌ] β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + +λ°°κ²½: White +λ‘₯κ·Ό λͺ¨μ„œλ¦¬: 16px +μ΅œλŒ€ λ„ˆλΉ„: 400px (Mobile), 480px (Tablet+) +νŒ¨λ”©: 24px +그림자: 0 8px 24px rgba(0,0,0,0.2) + +Backdrop: +- λ°°κ²½: rgba(0,0,0,0.6) +- 클릭 μ‹œ: λͺ¨λ‹¬ λ‹«νž˜ (확인 ν•„μš”ν•œ 경우 μ œμ™Έ) + +μ• λ‹ˆλ©”μ΄μ…˜: +- Show: opacity 0β†’1, scale(0.95β†’1), 250ms ease-out +- Hide: opacity 1β†’0, scale(1β†’0.95), 200ms ease-in +``` + +### 9.4 Pull to Refresh + +``` +μƒνƒœ ν‘œμ‹œ: +1. Pull Down (λ‹ΉκΈ°κΈ°): + - μ•„μ΄μ½˜: ↓ νšŒμ „ + - ν…μŠ€νŠΈ: "λ‹Ήκ²¨μ„œ μƒˆλ‘œκ³ μΉ¨" + +2. Release to Refresh (λ†“μ•„μ„œ μƒˆλ‘œκ³ μΉ¨): + - μ•„μ΄μ½˜: ↻ νšŒμ „ + - ν…μŠ€νŠΈ: "λ†“μ•„μ„œ μƒˆλ‘œκ³ μΉ¨" + +3. Refreshing (μƒˆλ‘œκ³ μΉ¨ 쀑): + - Spinner ν‘œμ‹œ + - ν…μŠ€νŠΈ: "μƒˆλ‘œκ³ μΉ¨ 쀑..." + +색상: +- μ•„μ΄μ½˜/ν…μŠ€νŠΈ: #9E9E9E +- Spinner: #E31E24 + +μœ„μΉ˜: ν™”λ©΄ μ΅œμƒλ‹¨ +높이: 60px +``` + +### 9.5 Swipe Actions (μΉ΄λ“œ μŠ€μ™€μ΄ν”„) + +``` +쒌츑 μŠ€μ™€μ΄ν”„ (μ‚­μ œ): +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ β—€ μ‚­μ œ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +λ°°κ²½: #D32F2F +μ•„μ΄μ½˜: πŸ—‘οΈ White +ν…μŠ€νŠΈ: White Body-M + +우츑 μŠ€μ™€μ΄ν”„ (νŽΈμ§‘): +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ νŽΈμ§‘ β–Ά β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +λ°°κ²½: #0066FF +μ•„μ΄μ½˜: ✏️ White +ν…μŠ€νŠΈ: White Body-M + +Threshold: 30% λ„ˆλΉ„ 이상 μŠ€μ™€μ΄ν”„ μ‹œ μ•‘μ…˜ 트리거 +``` + +### 9.6 Drag and Drop + +``` +λ“œλž˜κ·Έ 쀑 μƒνƒœ: +- 원본 μΉ΄λ“œ: opacity 0.5 +- λ“œλž˜κ·Έ μΉ΄λ“œ: 그림자 0 8px 16px rgba(0,0,0,0.2), scale(1.05) +- Drop Zone: λ°°κ²½ #F5F5F5, ν…Œλ‘λ¦¬ 2px dashed #E31E24 + +λ“œλ‘­ κ°€λŠ₯ μ˜μ—­: +- λ°°κ²½: #FFF5F5 (light red tint) +- ν…Œλ‘λ¦¬: 2px dashed #E31E24 +``` + +--- + +## 10. μ• λ‹ˆλ©”μ΄μ…˜ κ°€μ΄λ“œλΌμΈ + +### 10.1 Duration (지속 μ‹œκ°„) + +``` +μ¦‰μ‹œ (Instant): 0ms // 색상 λ³€ν™” +맀우 빠름 (Very Fast): 100ms // Hover 효과 +빠름 (Fast): 200ms // Toast λ“±μž₯ +일반 (Normal): 300ms // Modal, Bottom Sheet +느림 (Slow): 500ms // Page Transition +``` + +### 10.2 Easing (가속도) + +``` +Ease-Out (감속): +- cubic-bezier(0, 0, 0.2, 1) +- μ‚¬μš©: μš”μ†Œ λ“±μž₯ (Modal, Sheet, Toast) + +Ease-In (가속): +- cubic-bezier(0.4, 0, 1, 1) +- μ‚¬μš©: μš”μ†Œ 퇴μž₯ + +Ease-In-Out (가속+감속): +- cubic-bezier(0.4, 0, 0.2, 1) +- μ‚¬μš©: μ „ν™˜ 효과 (Tab μ „ν™˜) + +Linear (일정): +- linear +- μ‚¬μš©: λ¬΄ν•œ νšŒμ „ (Spinner, Loading) +``` + +### 10.3 μ£Όμš” μ• λ‹ˆλ©”μ΄μ…˜ + +#### Page Transition + +``` +μ§„μž…: +- opacity: 0 β†’ 1 +- transform: translateX(20px) β†’ translateX(0) +- duration: 300ms +- easing: ease-out + +퇴μž₯: +- opacity: 1 β†’ 0 +- transform: translateX(0) β†’ translateX(-20px) +- duration: 250ms +- easing: ease-in +``` + +#### Card Hover + +``` +transform: translateY(0) β†’ translateY(-4px) +box-shadow: 증가 (0 2px 8px β†’ 0 8px 16px) +duration: 200ms +easing: ease-out +``` + +#### Button Press + +``` +transform: scale(1) β†’ scale(0.95) +duration: 100ms +easing: ease-out +``` + +--- + +## 11. μ•„μ΄μ½˜ μ‹œμŠ€ν…œ + +### 11.1 Icon Style + +**Material Icons Outlined** (κΈ°λ³Έ) +**Material Icons Filled** (ν™œμ„±ν™” μƒνƒœ) + +``` +μŠ€νƒ€μΌ νŠΉμ§•: +- Outlined: μ„  λ‘κ»˜ 2px, μ‹¬ν”Œν•˜κ³  깔끔 +- Filled: ν™œμ„±ν™” μ‹œ 직관적 ꡬ뢄 +``` + +### 11.2 Icon Sizes + +``` +Small: 16 x 16px // ν…μŠ€νŠΈ λ‚΄ 인라인 +Medium: 24 x 24px // λ²„νŠΌ, λ„€λΉ„κ²Œμ΄μ…˜ (κΈ°λ³Έ) +Large: 32 x 32px // 헀더 μ•‘μ…˜ +XLarge: 48 x 48px // Empty State 일러슀트 +``` + +### 11.3 Icon Colors + +``` +Default: #4A4A4A (Gray-700) +Active: #E31E24 (Primary Red) +Disabled: #9E9E9E (Gray-500) +On Color: #FFFFFF (White) - 색상 λ²„νŠΌ μœ„ + +AI Feature: #0066FF (AI Blue) +Success: #00C853 +Warning: #FFA000 +Error: #D32F2F +``` + +### 11.4 μ£Όμš” μ•„μ΄μ½˜ λ§€ν•‘ + +``` +ν™ˆ: home +이벀트: event / campaign +뢄석: analytics / bar_chart +마이: person / account_circle +μΆ”κ°€: add / add_circle +μ•Œλ¦Ό: notifications +μ„€μ •: settings +검색: search +ν•„ν„°: filter_list +μ •λ ¬: sort +곡유: share +λ‹€μš΄λ‘œλ“œ: download +μ—…λ‘œλ“œ: upload +νŽΈμ§‘: edit +μ‚­μ œ: delete +λ‹«κΈ°: close +λ’€λ‘œ: arrow_back +μ•žμœΌλ‘œ: arrow_forward +체크: check / check_circle +였λ₯˜: error / cancel +정보: info +κ²½κ³ : warning +도움말: help / help_outline + +AI κ΄€λ ¨: +νŠΈλ Œλ“œ: trending_up +κ²½ν’ˆ: card_giftcard +이미지: image / photo +μ˜μƒ: videocam +SNS: share / language +QR: qr_code +달λ ₯: calendar_today +μ‹œκ°„: schedule +μœ„μΉ˜: place +μ°Έμ—¬μž: group / people +``` + +--- + +## 12. μ ‘κ·Όμ„± κ°€μ΄λ“œλΌμΈ + +### 12.1 색상 λŒ€λΉ„ (WCAG 2.1 AA) + +``` +ν…μŠ€νŠΈ: +- 일반 ν…μŠ€νŠΈ (14px+): 4.5:1 이상 +- 큰 ν…μŠ€νŠΈ (18px+ or 14px+ Bold): 3:1 이상 + +UI μš”μ†Œ: +- λ²„νŠΌ, μž…λ ₯ ν•„λ“œ, μ•„μ΄μ½˜: 3:1 이상 + +κ²€μ¦λœ μ‘°ν•©: +βœ“ #1A1A1A (Black) on #FFFFFF (White): 14.2:1 +βœ“ #4A4A4A (Gray-700) on #FFFFFF: 8.5:1 +βœ“ #E31E24 (KT Red) on #FFFFFF: 7.2:1 +βœ“ #0066FF (AI Blue) on #FFFFFF: 7.8:1 +βœ“ #FFFFFF (White) on #E31E24 (Red): 7.2:1 +βœ— #9E9E9E (Gray-500) on #FFFFFF: 4.6:1 (μž‘μ€ ν…μŠ€νŠΈ 뢀적합) +``` + +### 12.2 ν‚€λ³΄λ“œ λ„€λΉ„κ²Œμ΄μ…˜ + +``` +Tab Order: +- 논리적 μˆœμ„œ (μƒβ†’ν•˜, μ’Œβ†’μš°) +- λͺ¨λ“  μΈν„°λž™ν‹°λΈŒ μš”μ†Œ μ ‘κ·Ό κ°€λŠ₯ + +Focus Indicator: +- ν…Œλ‘λ¦¬: 2px solid #0066FF +- Offset: 2px +- λ‘₯κ·Ό λͺ¨μ„œλ¦¬: λ²„νŠΌκ³Ό 동일 + +Focus Trap: +- Modal, Bottom Sheet μ—΄λ¦Ό μ‹œ λ‚΄λΆ€λ§Œ νƒ­ 이동 +- ESC ν‚€λ‘œ λ‹«κΈ° κ°€λŠ₯ +``` + +### 12.3 슀크린 리더 + +``` +ARIA Labels: +- λͺ¨λ“  λ²„νŠΌμ— λͺ…ν™•ν•œ λ ˆμ΄λΈ” +- μ•„μ΄μ½˜ λ²„νŠΌ: aria-label="μ„€μ • μ—΄κΈ°" +- 이미지: alt="AIκ°€ μƒμ„±ν•œ 홍보 이미지" + +Role & State: +- role="button", role="dialog" +- aria-expanded="true/false" +- aria-selected="true/false" +- aria-disabled="true/false" + +Live Regions: +- Toast: aria-live="polite" +- Error: aria-live="assertive" +``` + +### 12.4 ν„°μΉ˜ μ˜μ—­ + +``` +μ΅œμ†Œ 크기: 44 x 44px (WCAG 2.1 AA) +ꢌμž₯ 크기: 48 x 48px + +μΆ©λΆ„ν•œ 간격: +- 인접 ν„°μΉ˜ μš”μ†Œ κ°„: 8px 이상 +``` + +### 12.5 λŒ€μ•ˆ 제곡 + +``` +μƒ‰μƒλ§ŒμœΌλ‘œ 정보 전달 κΈˆμ§€: +βœ— λ‚˜μœ 예: λΉ¨κ°„μƒ‰λ§ŒμœΌλ‘œ 였λ₯˜ ν‘œμ‹œ +βœ“ 쒋은 예: 빨간색 + βœ• μ•„μ΄μ½˜ + "였λ₯˜" ν…μŠ€νŠΈ + +λ“œλž˜κ·Έ μ•€ λ“œλ‘­ λŒ€μ•ˆ: +- λ²„νŠΌ ν΄λ¦­μœΌλ‘œλ„ μˆœμ„œ λ³€κ²½ κ°€λŠ₯ +- ν‚€λ³΄λ“œλ‘œ μˆœμ„œ μ‘°μ • κ°€λŠ₯ +``` + +--- + +## 13. μ„±λŠ₯ μ΅œμ ν™” + +### 13.1 이미지 μ΅œμ ν™” + +``` +포맷: +- 사진: WebP (fallback: JPG) +- 일러슀트: SVG λ˜λŠ” PNG +- μ•„μ΄μ½˜: SVG Sprite + +μ••μΆ•: +- JPG: Quality 80-85% +- PNG: TinyPNG λ˜λŠ” ImageOptim + +Lazy Loading: +- 슀크둀 μ‹œ λ‘œλ“œ (Intersection Observer) +- μ€‘μš” 이미지: eager loading +``` + +### 13.2 폰트 μ΅œμ ν™” + +``` +Font Loading: +- font-display: swap +- Preload μ£Όμš” 폰트 + +Subsetting: +- ν•œκΈ€: 자주 μ“°λŠ” κΈ€μžλ§Œ (Subset) +- Variable Font μ‚¬μš© (Pretendard Variable) + +WOFF2 μš°μ„ : +@font-face { + font-family: 'Pretendard'; + src: url('/fonts/Pretendard-Variable.woff2') format('woff2-variations'); + font-display: swap; +} +``` + +### 13.3 μ• λ‹ˆλ©”μ΄μ…˜ μ„±λŠ₯ + +``` +GPU 가속 μ‚¬μš©: +- transform: translate3d(), scale3d() +- opacity +- filter + +ν”Όν•΄μ•Ό ν•  속성: +- width, height (Reflow λ°œμƒ) +- top, left (Reflow λ°œμƒ) +- background-position + +Will-Change μ‚¬μš©: +will-change: transform, opacity; +(μ• λ‹ˆλ©”μ΄μ…˜ μ§μ „μ—λ§Œ 적용) +``` + +--- + +## 14. 닀크 λͺ¨λ“œ (ν–₯ν›„ 지원) + +### 14.1 색상 λ§€ν•‘ (참고용) + +``` +Light Mode β†’ Dark Mode + +λ°°κ²½: +#FFFFFF β†’ #121212 +#F5F5F5 β†’ #1E1E1E + +ν…μŠ€νŠΈ: +#1A1A1A β†’ #E0E0E0 +#4A4A4A β†’ #B0B0B0 +#9E9E9E β†’ #707070 + +μΉ΄λ“œ: +#FFFFFF β†’ #1E1E1E +border #E0E0E0 β†’ #2C2C2C + +Primary (μœ μ§€): +#E31E24 β†’ #E31E24 (동일) + +Secondary: +#0066FF β†’ #4D94FF (밝게 μ‘°μ •) +``` + +--- + +## 15. λ³€κ²½ 이λ ₯ + +### Version 1.1 (2025-10-21) + +- ν”„λ‘œν† νƒ€μž… 기반 μ»΄ν¬λ„ŒνŠΈ μ—…λ°μ΄νŠΈ +- Editable Field μ»΄ν¬λ„ŒνŠΈ μΆ”κ°€ (인라인 νŽΈμ§‘) +- Budget Navigation μ»΄ν¬λ„ŒνŠΈ μΆ”κ°€ (Sticky νƒ­ λ„€λΉ„κ²Œμ΄μ…˜) +- Option Card μ»΄ν¬λ„ŒνŠΈ μΆ”κ°€ (온라인/μ˜€ν”„λΌμΈ λ°°μ§€) +- Selection Card 세뢀사항 보완 (이미지 λΉ„μœ¨, z-index) +- 이벀트 버블링 λ°©μ§€ κ°€μ΄λ“œ μΆ”κ°€ + +### Version 1.0 (2025-10-17) + +- μ΄ˆμ•ˆ μž‘μ„± +- λΈŒλžœλ“œ 아이덴티티 μ •μ˜ +- λ””μžμΈ 원칙 5κ°€μ§€ 수립 +- 색상 μ‹œμŠ€ν…œ (Primary/Secondary/Grayscale/Semantic) μ •μ˜ +- νƒ€μ΄ν¬κ·Έλž˜ν”Ό μ‹œμŠ€ν…œ (Pretendard, 8단계 μŠ€μΌ€μΌ) μ •μ˜ +- 간격 μ‹œμŠ€ν…œ (4px 기반, 6단계) μ •μ˜ +- μ»΄ν¬λ„ŒνŠΈ μŠ€νƒ€μΌ (Button/Card/Input/Navigation) μ •μ˜ +- λ°˜μ‘ν˜• 브레이크포인트 (Mobile/Tablet/Desktop) μ •μ˜ +- AI νŠΉν™” μ»΄ν¬λ„ŒνŠΈ (λ‘œλ”©/κ²°κ³Ό/Stepper) μ •μ˜ +- μΈν„°λž™μ…˜ νŒ¨ν„΄ (BottomSheet/Toast/Modal/Swipe) μ •μ˜ +- μ• λ‹ˆλ©”μ΄μ…˜ κ°€μ΄λ“œλΌμΈ μ •μ˜ +- μ•„μ΄μ½˜ μ‹œμŠ€ν…œ (Material Icons) μ •μ˜ +- μ ‘κ·Όμ„± κ°€μ΄λ“œλΌμΈ (WCAG 2.1 AA) μ •μ˜ +- μ„±λŠ₯ μ΅œμ ν™” κ°€μ΄λ“œ μ •μ˜ + +--- + +## 16. μ°Έκ³  자료 + +- UI/UX μ„€κ³„μ„œ: design/uiux/uiux.md +- μœ μ €μŠ€ν† λ¦¬: design/userstory.md +- KT 사μž₯λ‹˜Easy: https://product.kt.com/wDic/soho/marketing.do?itemCode=sajangeasy +- wwit.design λ‹·μŠ¬λž˜μ‹œλŒ€μ‹œ: https://wwit.design/2023/09/30/dotslashdash/ +- Material Design Icons: https://fonts.google.com/icons +- WCAG 2.1 Guidelines: https://www.w3.org/WAI/WCAG21/quickref/ +- Pretendard Font: https://github.com/orioncactus/pretendard + +--- + +**λ¬Έμ„œ 끝** diff --git a/design/frontend/uiux-design.md b/design/frontend/uiux-design.md new file mode 100644 index 0000000..982a465 --- /dev/null +++ b/design/frontend/uiux-design.md @@ -0,0 +1,502 @@ +# KT AI 기반 μ†Œμƒκ³΅μΈ 이벀트 μžλ™ 생성 μ„œλΉ„μŠ€ - ν”„λ‘ νŠΈμ—”λ“œ UI/UX μ„€κ³„μ„œ + +## λ¬Έμ„œ 정보 +- **μž‘μ„±μΌ**: 2025-10-24 +- **버전**: 1.0 +- **기술 μŠ€νƒ**: Next.js 14 + React 18 + TypeScript 5 +- **기반 λ¬Έμ„œ**: design/uiux/uiux-design.md, ν”„λ‘œν† νƒ€μž… 뢄석 +- **ν”„λ‘œν† νƒ€μž…**: design/uiux/prototype/\*.html + +--- + +## 1. UI/UX 섀계 + +### 1.1 UI ν”„λ ˆμž„μ›Œν¬ 선택 + +**μ„ νƒν•œ ν”„λ ˆμž„μ›Œν¬**: Material-UI (MUI) v6 + +**선택 이유**: +- React 18κ³Ό Next.js 14μ™€μ˜ μ™„λ²½ν•œ ν˜Έν™˜μ„± +- TypeScript κΈ°λ³Έ 지원 +- λͺ¨λ°”일 μš°μ„  λ°˜μ‘ν˜• λ””μžμΈ (Mobile First) +- μ ‘κ·Όμ„± (WCAG 2.1 AA) κΈ°λ³Έ μ€€μˆ˜ +- ν’λΆ€ν•œ μ»΄ν¬λ„ŒνŠΈ 라이브러리 +- KT λΈŒλžœλ“œ 컬러 μ»€μŠ€ν„°λ§ˆμ΄μ§• 용이 + +**λŒ€μ•ˆ κ³ λ €**: +- Ant Design: ν•œκ΅­μ–΄ 지원 μš°μˆ˜ν•˜λ‚˜ 파일 크기가 큼 +- Chakra UI: κ²½λŸ‰μ΄λ‚˜ κΈ°μ—… λ””μžμΈ μ‹œμŠ€ν…œμ— 뢀적합 +- Tailwind CSS + HeadlessUI: μ»€μŠ€ν„°λ§ˆμ΄μ§•μ€ μš°μˆ˜ν•˜λ‚˜ 개발 속도 느림 + +**μ΅œμ’… κ²°μ •**: MUI의 μ»΄ν¬λ„ŒνŠΈ 완성도와 κΈ°μ—…μš© UI νŒ¨ν„΄ μ§€μ›μœΌλ‘œ 선택 + +--- + +### 1.2 ν™”λ©΄ λͺ©λ‘ μ •μ˜ + +#### 인증 μ˜μ—­ (4개) +| ν™”λ©΄ ID | ν™”λ©΄λͺ… | URL | μ„€λͺ… | +|---------|--------|-----|------| +| AUTH-01 | 둜그인 | `/login` | μ „ν™”λ²ˆν˜Έ/λΉ„λ°€λ²ˆν˜Έ 둜그인 | +| AUTH-02 | νšŒμ›κ°€μž… | `/register` | μ†Œμƒκ³΅μΈ νšŒμ›κ°€μž… (λ§€μž₯ 정보 포함) | +| AUTH-03 | ν”„λ‘œν•„ | `/profile` | μ‚¬μš©μž 및 λ§€μž₯ 정보 관리 | +| AUTH-04 | λ‘œκ·Έμ•„μ›ƒ 확인 | Modal | λ‘œκ·Έμ•„μ›ƒ 확인 λ‹€μ΄μ–Όλ‘œκ·Έ | + +#### λŒ€μ‹œλ³΄λ“œ μ˜μ—­ (2개) +| ν™”λ©΄ ID | ν™”λ©΄λͺ… | URL | μ„€λͺ… | +|---------|--------|-----|------| +| DASH-01 | λŒ€μ‹œλ³΄λ“œ | `/` | KPI μš”μ•½, λΉ λ₯Έ μ‹œμž‘, μ§„ν–‰ 쀑 이벀트 | +| DASH-02 | 이벀트 λͺ©λ‘ | `/events` | 전체 이벀트 λͺ©λ‘ (ν•„ν„°, 검색, νŽ˜μ΄μ§•) | + +#### 이벀트 생성 ν”Œλ‘œμš° (6개) +| ν™”λ©΄ ID | ν™”λ©΄λͺ… | URL | μ„€λͺ… | Funnel Step | +|---------|--------|-----|------|-------------| +| EVENT-01 | 이벀트 λͺ©μ  선택 | `/events/create/objective` | 4κ°€μ§€ λͺ©μ  선택 | Step 1 | +| EVENT-02 | AI 이벀트 μΆ”μ²œ | `/events/create/recommendation` | AI νŠΈλ Œλ“œ 뢄석 및 μΆ”μ²œ | Step 2 | +| EVENT-03 | μ½˜ν…μΈ  미리보기 | `/events/create/content` | SNS 이미지 μŠ€νƒ€μΌ 선택 | Step 3 | +| EVENT-04 | μ½˜ν…μΈ  νŽΈμ§‘ | `/events/create/edit` | ν…μŠ€νŠΈ 및 이미지 νŽΈμ§‘ | Step 4 | +| EVENT-05 | 배포 채널 선택 | `/events/create/channels` | 배포 채널 선택 | Step 5 | +| EVENT-06 | μ΅œμ’… 승인 | `/events/create/publish` | μ΅œμ’… κ²€ν†  및 배포 | Step 6 | + +#### 이벀트 관리 및 λͺ¨λ‹ˆν„°λ§ (5개) +| ν™”λ©΄ ID | ν™”λ©΄λͺ… | URL | μ„€λͺ… | +|---------|--------|-----|------| +| MANAGE-01 | 이벀트 상세 | `/events/[id]` | 이벀트 상세 정보 및 μ‹€μ‹œκ°„ KPI | +| MANAGE-02 | μ°Έμ—¬μž λͺ©λ‘ | `/events/[id]/participants` | μ°Έμ—¬μž 관리 및 필터링 | +| MANAGE-03 | 이벀트 μ°Έμ—¬ (고객용) | `/participate/[id]` | 고객 이벀트 μ°Έμ—¬ ν™”λ©΄ | +| MANAGE-04 | λ‹Ήμ²¨μž 좔첨 | `/events/[id]/draw` | λ‹Ήμ²¨μž 좔첨 및 관리 | +| MANAGE-05 | μ„±κ³Ό 뢄석 | `/analytics` | μ‹€μ‹œκ°„ λŒ€μ‹œλ³΄λ“œ 및 μ„±κ³Ό 뢄석 | + +**총 17개 ν™”λ©΄ (λͺ¨λ‹¬ 포함)** + +--- + +### 1.3 ν™”λ©΄ κ°„ μ‚¬μš©μž ν”Œλ‘œμš° + +``` +[둜그인] β†’ [λŒ€μ‹œλ³΄λ“œ] ←→ [Bottom Navigation] + ↓ + β”Œβ”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β” + ↓ ↓ + [이벀트 λͺ©λ‘] [μ„±κ³Ό 뢄석] + ↓ ↓ + [이벀트 상세] β†’ [μ°Έμ—¬μž λͺ©λ‘] β†’ [λ‹Ήμ²¨μž 좔첨] + +[λŒ€μ‹œλ³΄λ“œ] β†’ [FAB: μƒˆ 이벀트] β†’ [이벀트 생성 Funnel] + ↓ + (Objective β†’ AIμΆ”μ²œ β†’ μ½˜ν…μΈ  β†’ νŽΈμ§‘ β†’ 채널 β†’ 승인) + ↓ + [이벀트 상세] + +[Bottom Navigation] +- ν™ˆ: λŒ€μ‹œλ³΄λ“œ +- 이벀트: 이벀트 λͺ©λ‘ +- 뢄석: μ„±κ³Ό 뢄석 +- ν”„λ‘œν•„: ν”„λ‘œν•„ +``` + +--- + +### 1.4 화면별 상세 섀계 + +#### 1.4.1 AUTH-01: 둜그인 + +**상세 κΈ°λŠ₯**: +- μ „ν™”λ²ˆν˜Έ (010XXXXXXXX) μž…λ ₯ +- λΉ„λ°€λ²ˆν˜Έ μž…λ ₯ +- "둜그인 μœ μ§€" μ²΄ν¬λ°•μŠ€ +- 둜그인 λ²„νŠΌ (API: POST /users/login) +- νšŒμ›κ°€μž… 링크 +- λΉ„λ°€λ²ˆν˜Έ μ°ΎκΈ° 링크 (ν–₯ν›„ κ΅¬ν˜„) + +**UI κ΅¬μ„±μš”μ†Œ**: +- Logo (KT 둜고) +- TextField: phoneNumber (pattern 검증) +- TextField: password (type="password") +- Checkbox: "둜그인 μœ μ§€" +- Button: "둜그인" (variant="contained", color="primary") +- Link: "νšŒμ›κ°€μž…", "λΉ„λ°€λ²ˆν˜Έ μ°ΎκΈ°" + +**μΈν„°λž™μ…˜**: +- μ „ν™”λ²ˆν˜Έ μž…λ ₯ μ‹œ μžλ™ ν•˜μ΄ν”ˆ 제거 +- λΉ„λ°€λ²ˆν˜Έ 8자 이상 검증 +- Enter ν‚€λ‘œ 둜그인 +- 둜그인 성곡 μ‹œ "/" λ¦¬λ‹€μ΄λ ‰νŠΈ +- 둜그인 μ‹€νŒ¨ μ‹œ μ—λŸ¬ Toast ν‘œμ‹œ + +--- + +#### 1.4.2 EVENT-01 ~ EVENT-06: 이벀트 생성 Funnel + +**Funnel κ΅¬ν˜„**: `@use-funnel/next` μ‚¬μš© + +**Funnel 섀계**: +```typescript +const steps = [ + 'objective', // λͺ©μ  선택 + 'recommendation', // AI μΆ”μ²œ + 'content', // μ½˜ν…μΈ  미리보기 + 'edit', // μ½˜ν…μΈ  νŽΈμ§‘ + 'channels', // 배포 채널 + 'publish' // μ΅œμ’… 승인 +] as const; + +// useFunnel μ‚¬μš© +const [Funnel, state, setStep] = useFunnel({ + id: 'event-creation', + initial: 'objective' +}); +``` + +**Step 1: Objective (λͺ©μ  선택)** + +*상세 κΈ°λŠ₯*: +- 4개 λͺ©μ  쀑 1개 선택 (Radio) + 1. μ‹ κ·œ 고객 유치 + 2. 재방문 μœ λ„ + 3. 맀좜 μ¦λŒ€ + 4. 인지도 ν–₯상 +- 선택 ν›„ λ‹€μŒ λ²„νŠΌ ν™œμ„±ν™” +- API: POST /events/objectives + +*UI κ΅¬μ„±μš”μ†Œ*: +- Stepper: 1/6 μ§„ν–‰ ν‘œμ‹œ +- OptionCard: 4개 (μ•„μ΄μ½˜ + 제λͺ© + μ„€λͺ…) +- Button: "λ‹€μŒ" (ν•˜λ‹¨ κ³ μ •) + +*μΈν„°λž™μ…˜*: +- μΉ΄λ“œ 클릭 μ‹œ Radio 선택 +- 선택 μ‹œ μΉ΄λ“œ κ°•μ‘° (border: KT Red) +- λ‹€μŒ λ²„νŠΌ 클릭 β†’ API 호좜 β†’ eventId μ €μž₯ β†’ Step 2 이동 + +--- + +**Step 2: AI Recommendation (AI μΆ”μ²œ)** + +*상세 κΈ°λŠ₯*: +- AI νŠΈλ Œλ“œ 뢄석 κ²°κ³Ό ν‘œμ‹œ (μ—…μ’…, μ§€μ—­, μ‹œμ¦Œ) +- μ˜ˆμ‚°λ³„ μΆ”μ²œ 이벀트 (μ €/쀑/κ³ ) + - 각 μ˜ˆμ‚°λ‹Ή 온라인/μ˜€ν”„λΌμΈ 2κ°€μ§€ 방식 제곡 + - 총 6개 μ˜΅μ…˜ 쀑 1개 선택 +- 이벀트λͺ…, κ²½ν’ˆ 인라인 νŽΈμ§‘ κ°€λŠ₯ +- μ˜ˆμ‚° λ„€λΉ„κ²Œμ΄μ…˜ (Sticky) +- API: POST /events/{eventId}/ai-recommendations + - Job λ°œν–‰ β†’ 폴링 (GET /jobs/{jobId}) + +*UI κ΅¬μ„±μš”μ†Œ*: +- TrendAnalysis Card: 3개 (μ—…μ’…/μ§€μ—­/μ‹œμ¦Œ) +- BudgetNavigation: 3개 λ²„νŠΌ (Sticky) +- RecommendationCard: 6개 (온라인/μ˜€ν”„λΌμΈ λ°°μ§€, νŽΈμ§‘ κ°€λŠ₯ 제λͺ©/κ²½ν’ˆ) +- LoadingIndicator: AI 처리 쀑 (5초 μ˜ˆμƒ) +- Button: "λ‹€μŒ" + +*μΈν„°λž™μ…˜*: +- μ˜ˆμ‚° λ„€λΉ„κ²Œμ΄μ…˜ 클릭 β†’ smooth scroll +- 이벀트λͺ…/κ²½ν’ˆ hover β†’ 점선 ν…Œλ‘λ¦¬ (νŽΈμ§‘ κ°€λŠ₯ ν‘œμ‹œ) +- 클릭 β†’ 인라인 νŽΈμ§‘ (TextField μ „ν™˜) +- Enter/Blur β†’ μ €μž₯ +- AI Job 폴링 (5초 간격) +- μ™„λ£Œ μ‹œ μΆ”μ²œ κ²°κ³Ό ν‘œμ‹œ +- 1개 선택 β†’ PUT /events/{eventId}/recommendations β†’ Step 3 이동 + +--- + +**Step 3: Content (μ½˜ν…μΈ  미리보기)** + +*상세 κΈ°λŠ₯*: +- 5κ°€μ§€ SNS 이미지 μŠ€νƒ€μΌ 선택 + 1. μ‹¬ν”Œ + 2. λͺ¨λ˜ + 3. 귀여움 + 4. κ³ κΈ‰μŠ€λŸ¬μ›€ + 5. νŠΈλ Œλ”” +- 각 μŠ€νƒ€μΌ 미리보기 이미지 +- "크게보기" λ²„νŠΌ β†’ 전체화면 λͺ¨λ‹¬ +- API: POST /events/{eventId}/images + - Job λ°œν–‰ β†’ 폴링 (GET /jobs/{jobId}) + +*UI κ΅¬μ„±μš”μ†Œ*: +- StyleCard: 5개 (이미지 + 제λͺ© + μ„€λͺ… + "크게보기") +- SelectBadge: 우츑 상단 (선택 μ‹œλ§Œ ν‘œμ‹œ) +- FullscreenModal: 이미지 ν™•λŒ€ +- LoadingIndicator: 이미지 생성 쀑 (5초 μ˜ˆμƒ) +- Button: "λ‹€μŒ" + +*μΈν„°λž™μ…˜*: +- μΉ΄λ“œ 클릭 β†’ Radio 선택 (μˆ¨κΉ€) +- 선택 μ‹œ ν…Œλ‘λ¦¬ κ°•μ‘° + 체크 λ°°μ§€ +- "크게보기" β†’ 전체화면 λͺ¨λ‹¬ (이벀트 버블링 λ°©μ§€) +- Job 폴링 (3초 간격) +- μ™„λ£Œ μ‹œ 이미지 URL ν‘œμ‹œ +- 1개 선택 β†’ PUT /events/{eventId}/images/{imageId}/select β†’ Step 4 이동 + +--- + +**Step 4: Edit (μ½˜ν…μΈ  νŽΈμ§‘)** + +*상세 κΈ°λŠ₯*: +- ν…μŠ€νŠΈ νŽΈμ§‘ (제λͺ©, κ²½ν’ˆ, μ°Έμ—¬ μ•ˆλ‚΄) +- μ‹€μ‹œκ°„ 미리보기 +- API: PUT /events/{eventId}/images/{imageId}/edit + +*UI κ΅¬μ„±μš”μ†Œ*: +- TextField: 제λͺ© +- TextField: κ²½ν’ˆ +- TextField: μ°Έμ—¬ μ•ˆλ‚΄ (multiline) +- PreviewCard: μ‹€μ‹œκ°„ 미리보기 (우츑/ν•˜λ‹¨) +- Button: "λ‹€μŒ", "μ €μž₯" + +*μΈν„°λž™μ…˜*: +- μž…λ ₯ μ‹œ debounce (300ms) ν›„ 미리보기 μ—…λ°μ΄νŠΈ +- μ €μž₯ λ²„νŠΌ β†’ μž„μ‹œ μ €μž₯ +- λ‹€μŒ λ²„νŠΌ β†’ API 호좜 β†’ Step 5 이동 + +--- + +**Step 5: Channels (배포 채널 선택)** + +*상세 κΈ°λŠ₯*: +- 볡수 채널 선택 (Checkbox) + - μš°λ¦¬λ™λ„€TV + - λ§κ³ λΉ„μ¦ˆ + - SNS (Instagram, Naver, Kakao) + - QRμ½”λ“œ +- 채널별 μ˜΅μ…˜ (쑰건뢀 ν‘œμ‹œ) +- API: PUT /events/{eventId}/channels + +*UI κ΅¬μ„±μš”μ†Œ*: +- Checkbox: 각 채널 +- ConditionalOptions: 각 채널 (펼침/μ ‘νž˜) +- Button: "λ‹€μŒ" + +*μΈν„°λž™μ…˜*: +- μ²΄ν¬λ°•μŠ€ 선택 β†’ ν•΄λ‹Ή μ˜΅μ…˜ 펼침 +- μ΅œμ†Œ 1개 선택 ν•„μˆ˜ +- λ‹€μŒ λ²„νŠΌ β†’ API 호좜 β†’ Step 6 이동 + +--- + +**Step 6: Publish (μ΅œμ’… 승인)** + +*상세 κΈ°λŠ₯*: +- 이벀트 μš”μ•½ ν‘œμ‹œ +- 이미지 미리보기 +- 배포 채널 λͺ©λ‘ +- 일정 μ„€μ • (μ‹œμž‘μΌ, μ’…λ£ŒμΌ) +- μ΅œμ’… 승인 및 배포 +- API: POST /events/{eventId}/publish + +*UI κ΅¬μ„±μš”μ†Œ*: +- SummaryCard: 이벀트 정보 +- ImagePreview: μ΅œμ’… 이미지 +- ChannelBadges: μ„ νƒλœ 채널 +- DatePicker: μ‹œμž‘μΌ, μ’…λ£ŒμΌ +- Button: "μˆ˜μ •" (이전 단계), "승인 및 배포" + +*μΈν„°λž™μ…˜*: +- μˆ˜μ • λ²„νŠΌ β†’ Funnel 이전 단계 이동 +- 승인 λ²„νŠΌ β†’ Confirm λ‹€μ΄μ–Όλ‘œκ·Έ β†’ API 호좜 β†’ 배포 μ™„λ£Œ Toast β†’ `/events/[id]` 이동 + +--- + +#### 1.4.3 MANAGE-01: 이벀트 상세 + +**상세 κΈ°λŠ₯**: +- 이벀트 헀더 (이미지, 제λͺ©, μƒνƒœ, κΈ°κ°„) +- μ‹€μ‹œκ°„ KPI (4개) + - μ°Έμ—¬μž 수 + - 쑰회수 + - ROI + - μ „ν™˜μœ¨ +- λΉ λ₯Έ μ•‘μ…˜ (4개 λ²„νŠΌ) + - μ°Έμ—¬μž λͺ©λ‘ + - λ‹Ήμ²¨μž 관리 + - μ„±κ³Ό 뢄석 + - 이벀트 μˆ˜μ • +- μ°Έμ—¬ 좔이 차트 (Line Chart) +- API: GET /events/{eventId} + +**UI κ΅¬μ„±μš”μ†Œ**: +- EventHeader: 이미지 + 정보 +- KPICard: 4개 (Grid) +- ActionButton: 4개 (Grid) +- LineChart: μ°Έμ—¬ 좔이 (Chart.js) + +**μΈν„°λž™μ…˜**: +- μ‹€μ‹œκ°„ μ—…λ°μ΄νŠΈ (5λΆ„λ§ˆλ‹€ μžλ™ κ°±μ‹ ) +- Pull to Refresh (λͺ¨λ°”일) +- μ•‘μ…˜ λ²„νŠΌ 클릭 β†’ ν•΄λ‹Ή νŽ˜μ΄μ§€ 이동 + +--- + +#### 1.4.4 MANAGE-03: 이벀트 μ°Έμ—¬ (고객용) + +**상세 κΈ°λŠ₯**: +- 이벀트 이미지 +- 이벀트 정보 (제λͺ©, κ²½ν’ˆ, μ°Έμ—¬ 방법) +- μ°Έμ—¬ 폼 (이름, μ „ν™”λ²ˆν˜Έ) +- κ°œμΈμ •λ³΄ λ™μ˜ (ν•„μˆ˜) +- λ§ˆμΌ€νŒ… μˆ˜μ‹  λ™μ˜ (선택) +- API: POST /events/{eventId}/participate + +**UI κ΅¬μ„±μš”μ†Œ**: +- EventBanner: 이미지 +- InfoCard: 이벀트 정보 +- TextField: 이름, μ „ν™”λ²ˆν˜Έ +- Checkbox: κ°œμΈμ •λ³΄ λ™μ˜, λ§ˆμΌ€νŒ… λ™μ˜ +- Button: "μ°Έμ—¬ν•˜κΈ°" + +**μΈν„°λž™μ…˜**: +- μ „ν™”λ²ˆν˜Έ 검증 (010-XXXX-XXXX) +- 쀑볡 μ°Έμ—¬ λ°©μ§€ +- κ°œμΈμ •λ³΄ λ™μ˜ ν•„μˆ˜ +- μ°Έμ—¬ 성곡 μ‹œ 응λͺ¨ 번호 λͺ¨λ‹¬ ν‘œμ‹œ + +--- + +### 1.5 ν™”λ©΄ κ°„ μ „ν™˜ 및 λ„€λΉ„κ²Œμ΄μ…˜ + +**Bottom Navigation** (4개 νƒ­): +- ν™ˆ (`/`): DASH-01 λŒ€μ‹œλ³΄λ“œ +- 이벀트 (`/events`): DASH-02 이벀트 λͺ©λ‘ +- 뢄석 (`/analytics`): MANAGE-05 μ„±κ³Ό 뢄석 +- ν”„λ‘œν•„ (`/profile`): AUTH-03 ν”„λ‘œν•„ + +**Header Navigation**: +- 둜고 (클릭 μ‹œ ν™ˆ 이동) +- λ’€λ‘œκ°€κΈ° λ²„νŠΌ (쑰건뢀 ν‘œμ‹œ) +- ν”„λ‘œν•„ μ•„μ΄μ½˜ (둜그인 μ‹œλ§Œ ν‘œμ‹œ) + +**FAB (Floating Action Button)**: +- μœ„μΉ˜: 우츑 ν•˜λ‹¨ κ³ μ • +- κΈ°λŠ₯: μƒˆ 이벀트 생성 (`/events/create/objective`) +- ν‘œμ‹œ 쑰건: λŒ€μ‹œλ³΄λ“œ, 이벀트 λͺ©λ‘ ν™”λ©΄μ—μ„œλ§Œ + +--- + +### 1.6 λ°˜μ‘ν˜• 섀계 μ „λž΅ + +**Breakpoints**: +- Mobile: 320px ~ 767px (κΈ°λ³Έ) +- Tablet: 768px ~ 1023px +- Desktop: 1024px 이상 + +**λ ˆμ΄μ•„μ›ƒ λ³€ν™”**: + +| ν™”λ©΄ | Mobile | Tablet | Desktop | +|------|--------|--------|---------| +| λŒ€μ‹œλ³΄λ“œ | KPI μ„Έλ‘œ μŠ€νƒ | KPI 2x2 Grid | KPI 4μ—΄ + μ‚¬μ΄λ“œλ°” | +| 이벀트 λͺ©λ‘ | 1μ—΄ | 2μ—΄ | 3μ—΄ | +| 이벀트 생성 | μ„Έλ‘œ μŠ€νƒ | μ„Έλ‘œ μŠ€νƒ | νŽΈμ§‘ \| 미리보기 (Split) | +| μ„±κ³Ό 뢄석 | 차트 μ„Έλ‘œ μŠ€νƒ | 차트 2x1 Grid | 차트 2x2 Grid | + +**λ°˜μ‘ν˜• μ»΄ν¬λ„ŒνŠΈ**: +- `Box` with `sx` prop (MUI) +- `Grid` with `xs/md/lg` props +- `useMediaQuery` hook +- `Container` with `maxWidth` prop + +--- + +### 1.7 μ ‘κ·Όμ„± 보μž₯ λ°©μ•ˆ + +**WCAG 2.1 AA μ€€μˆ˜**: +- 색상 λŒ€λΉ„: 4.5:1 (ν…μŠ€νŠΈ), 3:1 (UI μš”μ†Œ) +- ν„°μΉ˜ μ˜μ—­: μ΅œμ†Œ 44x44px +- ν‚€λ³΄λ“œ λ„€λΉ„κ²Œμ΄μ…˜: Tab, Enter, Escape 지원 +- Focus Indicator: λͺ…ν™•ν•œ 포컀슀 ν‘œμ‹œ + +**슀크린 리더 지원**: +- ARIA Labels: λͺ¨λ“  λ²„νŠΌ, 링크, 폼 ν•„λ“œ +- ARIA Roles: μ μ ˆν•œ μ—­ν•  μ§€μ • +- Live Regions: 동적 μ½˜ν…μΈ  μ—…λ°μ΄νŠΈ μ•Œλ¦Ό + +**λŒ€μ•ˆ 제곡**: +- 색상 μ™Έ ν‘œν˜„: μ•„μ΄μ½˜ + ν…μŠ€νŠΈ μ‘°ν•© +- 이미지 alt ν…μŠ€νŠΈ +- 폼 검증 μ—λŸ¬ λ©”μ‹œμ§€ + +--- + +### 1.8 μ„±λŠ₯ μ΅œμ ν™” λ°©μ•ˆ + +**Next.js μ΅œμ ν™”**: +- App Router μ‚¬μš© (Next.js 14) +- Server Components (κΈ°λ³Έ) +- Client Components (μΈν„°λž™μ…˜ ν•„μš” μ‹œλ§Œ) +- Image μ»΄ν¬λ„ŒνŠΈ (μžλ™ μ΅œμ ν™”) +- Font μ΅œμ ν™” (next/font) + +**Code Splitting**: +- νŽ˜μ΄μ§€λ³„ μžλ™ μ½”λ“œ λΆ„ν•  +- Dynamic Import (Chart.js, 큰 라이브러리) +- Lazy Loading (이미지, μ»΄ν¬λ„ŒνŠΈ) + +**캐싱 μ „λž΅**: +- React Query (μ„œλ²„ μƒνƒœ 관리) + - staleTime: 5λΆ„ (λŒ€μ‹œλ³΄λ“œ) + - cacheTime: 30λΆ„ + - refetchOnWindowFocus: true +- SWR (λŒ€μ•ˆ) +- Redux (ν΄λΌμ΄μ–ΈνŠΈ μƒνƒœ 관리, ν•„μš” μ‹œ) + +**이미지 μ΅œμ ν™”**: +- WebP 포맷 (fallback: JPG) +- μ••μΆ•: Quality 80-85% +- Lazy Loading +- Responsive Images (srcset) + +**폰트 μ΅œμ ν™”**: +- next/font μ‚¬μš© +- Font Display: swap +- Preload: μ£Όμš” 폰트 +- Subset: ν•œκΈ€ + 영문 + 숫자 + +**μ• λ‹ˆλ©”μ΄μ…˜ μ΅œμ ν™”**: +- GPU 가속: transform, opacity +- Framer Motion (선택적 μ‚¬μš©) +- CSS Transitions (κΈ°λ³Έ) + +--- + +## 2. μ°Έμ‘° λ¬Έμ„œ + +- **ν”„λ‘œν† νƒ€μž…**: design/uiux/prototype/*.html +- **UI/UX μ„€κ³„μ„œ**: design/uiux/uiux-design.md +- **μŠ€νƒ€μΌ κ°€μ΄λ“œ**: design/uiux/style-guide.md (β†’ design/frontend/style-guide.md) +- **정보 μ•„ν‚€ν…μ²˜**: design/frontend/ia.md +- **API λ§€ν•‘ μ„€κ³„μ„œ**: design/frontend/api-mapping.md + +--- + +## 3. 기술 μŠ€νƒ μš”μ•½ + +| ν•­λͺ© | 기술 | +|------|------| +| ν”„λ ˆμž„μ›Œν¬ | Next.js 14 (App Router) | +| 라이브러리 | React 18 | +| μ–Έμ–΄ | TypeScript 5 | +| UI ν”„λ ˆμž„μ›Œν¬ | Material-UI (MUI) v6 | +| Funnel 관리 | @use-funnel/next | +| μƒνƒœ 관리 | React Query v5 (μ„œλ²„ μƒνƒœ), Zustand (ν΄λΌμ΄μ–ΈνŠΈ μƒνƒœ) | +| 폼 관리 | React Hook Form v7 + Zod (검증) | +| 차트 | Chart.js v4 + react-chartjs-2 | +| λ‚ μ§œ | dayjs | +| HTTP ν΄λΌμ΄μ–ΈνŠΈ | Axios | +| μŠ€νƒ€μΌλ§ | MUI sx prop, Emotion (CSS-in-JS) | + +--- + +## 4. λ³€κ²½ 이λ ₯ + +### Version 1.0 (2025-10-24) +- μ΄ˆμ•ˆ μž‘μ„± +- Next.js 14 기반 섀계 +- @use-funnel/next κ²€ν†  및 적용 +- MUI v6 선택 +- λ°˜μ‘ν˜• λ””μžμΈ μ „λž΅ 수립 +- μ ‘κ·Όμ„± 및 μ„±λŠ₯ μ΅œμ ν™” λ°©μ•ˆ 수립 + +--- + +**λ¬Έμ„œ 끝**