diff --git a/.playwright-mcp/prototype-01-login.png b/.playwright-mcp/prototype-01-login.png new file mode 100644 index 0000000..73a0a5f Binary files /dev/null and b/.playwright-mcp/prototype-01-login.png differ diff --git a/.playwright-mcp/prototype-05-dashboard.png b/.playwright-mcp/prototype-05-dashboard.png new file mode 100644 index 0000000..774f6cf Binary files /dev/null and b/.playwright-mcp/prototype-05-dashboard.png differ diff --git a/.playwright-mcp/prototype-07-purpose-loading.png b/.playwright-mcp/prototype-07-purpose-loading.png new file mode 100644 index 0000000..0b87f48 Binary files /dev/null and b/.playwright-mcp/prototype-07-purpose-loading.png differ diff --git a/design/uiux/prototype/01-로그인.html b/design/uiux/prototype/01-로그인.html new file mode 100644 index 0000000..53e723a --- /dev/null +++ b/design/uiux/prototype/01-로그인.html @@ -0,0 +1,196 @@ + + + + + + 로그인 - KT AI 이벤트 마케팅 + + + + + +
+
+ +
+
+ celebration +
+

KT AI 이벤트

+

소상공인을 위한 스마트 마케팅

+
+ + +
+
+ + + +
+ +
+ + + +
+ +
+ + +
+ + + + +
+ + +
+
+ 또는 +
+
+ + +
+ + +
+ + +
+

+ 회원가입 시 이용약관개인정보처리방침에 동의하게 됩니다. +

+
+
+
+ + + + + diff --git a/design/uiux/prototype/02-회원가입.html b/design/uiux/prototype/02-회원가입.html new file mode 100644 index 0000000..5889254 --- /dev/null +++ b/design/uiux/prototype/02-회원가입.html @@ -0,0 +1,400 @@ + + + + + + 회원가입 - KT AI 이벤트 마케팅 + + + + + +
+ +
+
+ +

회원가입

+
+
+ + +
+
+

1/3 단계

+
+ +
+ + + + + + + + +
+
+ + + + + diff --git a/design/uiux/prototype/05-대시보드.html b/design/uiux/prototype/05-대시보드.html new file mode 100644 index 0000000..bb33bcb --- /dev/null +++ b/design/uiux/prototype/05-대시보드.html @@ -0,0 +1,235 @@ + + + + + + 대시보드 - KT AI 이벤트 마케팅 + + + + + +
+ + + +
+ +
+

안녕하세요, 사용자님!

+

오늘도 성공적인 이벤트를 준비해보세요

+
+ + +
+
+
+
+
+
+
+ + +
+
+

빠른 시작

+
+
+ + + + +
+
+ + +
+
+

진행 중인 이벤트

+ + 전체보기 + chevron_right + +
+
+ +
+
+ + +
+

최근 활동

+
+
+ +
+
+
+
+ + +
+ + +
+
+ + + + + diff --git a/design/uiux/prototype/07-이벤트목적선택.html b/design/uiux/prototype/07-이벤트목적선택.html new file mode 100644 index 0000000..6fb6465 --- /dev/null +++ b/design/uiux/prototype/07-이벤트목적선택.html @@ -0,0 +1,216 @@ + + + + + + 이벤트 목적 선택 - KT AI 이벤트 마케팅 + + + + + +
+ + + +
+ +
+
+ auto_awesome +
+

이벤트 목적을 선택해주세요

+

AI가 목적에 맞는 최적의 이벤트를 추천해드립니다

+
+ + +
+
+ +
+
+ + +
+ + +
+ + +
+
+
+ info +
+

+ 선택하신 목적에 따라 AI가 업종, 지역, 계절 트렌드를 분석하여 가장 효과적인 이벤트를 추천합니다. +

+
+
+
+
+
+ + +
+
+ + + + + diff --git a/design/uiux/prototype/08-AI이벤트추천.html b/design/uiux/prototype/08-AI이벤트추천.html new file mode 100644 index 0000000..5a159ae --- /dev/null +++ b/design/uiux/prototype/08-AI이벤트추천.html @@ -0,0 +1,473 @@ + + + + + + AI 이벤트 추천 - KT AI 이벤트 마케팅 + + + + + + +
+ + + +
+ +
+

+ insights + AI 트렌드 분석 +

+
+
+
+ store + 업종 트렌드 +
+

음식점업 신년 프로모션 트렌드

+
+
+
+ location_on + 지역 트렌드 +
+

강남구 음식점 할인 이벤트 증가

+
+
+
+ wb_sunny + 시즌 트렌드 +
+

설 연휴 특수 대비 고객 유치 전략

+
+
+
+ + +
+

예산별 추천 이벤트

+

+ 각 예산별 2가지 방식 (온라인 1개, 오프라인 1개)을 추천합니다 +

+
+ + +
+
+ + + +
+
+ + +
+

💰 옵션 1: 저비용 (25~30만원)

+
+
+
+ +
+
+ 🌐 온라인 방식 +

+ SNS 팔로우 이벤트 + edit +

+
+
+ 경품: + 커피 쿠폰 + edit +
+
+
+ 참여 방법: + SNS 팔로우 +
+
+ 예상 참여: + 180명 +
+
+ 예상 비용: + 25만원 +
+
+ 투자대비수익률: + 520% +
+
+
+ +
+
+ +
+
+ 🏪 오프라인 방식 +

+ 전화번호 등록 이벤트 + edit +

+
+
+ 경품: + 커피 쿠폰 + edit +
+
+
+ 참여 방법: + 전화번호 등록 +
+
+ 예상 참여: + 150명 +
+
+ 예상 비용: + 30만원 +
+
+ 투자대비수익률: + 450% +
+
+
+
+
+ + +
+

💰💰 옵션 2: 중비용 (150~180만원)

+
+
+
+ +
+
+ 🌐 온라인 방식 +

+ 리뷰 작성 이벤트 + edit +

+
+
+ 경품: + 5천원 상품권 + edit +
+
+
+ 참여 방법: + 리뷰 작성 +
+
+ 예상 참여: + 250명 +
+
+ 예상 비용: + 150만원 +
+
+ 투자대비수익률: + 380% +
+
+
+ +
+
+ +
+
+ 🏪 오프라인 방식 +

+ 방문 도장 적립 이벤트 + edit +

+
+
+ 경품: + 무료 식사권 + edit +
+
+
+ 참여 방법: + 방문 도장 적립 +
+
+ 예상 참여: + 200명 +
+
+ 예상 비용: + 180만원 +
+
+ 투자대비수익률: + 320% +
+
+
+
+
+ + +
+

💰💰💰 옵션 3: 고비용 (500~600만원)

+
+
+
+ +
+
+ 🌐 온라인 방식 +

+ 인플루언서 협업 이벤트 + edit +

+
+
+ 경품: + 1만원 할인권 + edit +
+
+
+ 참여 방법: + 인플루언서 팔로우 +
+
+ 예상 참여: + 400명 +
+
+ 예상 비용: + 500만원 +
+
+ 투자대비수익률: + 280% +
+
+
+ +
+
+ +
+
+ 🏪 오프라인 방식 +

+ VIP 고객 초대 이벤트 + edit +

+
+
+ 경품: + 특별 메뉴 제공 + edit +
+
+
+ 참여 방법: + VIP 초대장 +
+
+ 예상 참여: + 300명 +
+
+ 예상 비용: + 600만원 +
+
+ 투자대비수익률: + 240% +
+
+
+
+
+ + +
+ + +
+
+ + +
+
+ + + + + diff --git a/design/uiux/prototype/12-최종승인.html b/design/uiux/prototype/12-최종승인.html new file mode 100644 index 0000000..cbe1f0f --- /dev/null +++ b/design/uiux/prototype/12-최종승인.html @@ -0,0 +1,296 @@ + + + + + + 최종 승인 - KT AI 이벤트 마케팅 + + + + + +
+ + + +
+ +
+
+ check_circle +
+

이벤트를 확인해주세요

+

모든 정보를 검토한 후 배포하세요

+
+ + +
+
+

SNS 팔로우 이벤트

+ +
+ 배포 대기 + + AI 추천 + +
+ +
+
+
+

이벤트 기간

+

2025.02.01 ~ 2025.02.28

+
+
+

목표 참여자

+

180명

+
+
+

예상 비용

+

250,000원

+
+
+

예상 ROI

+

520%

+
+
+
+
+
+ + +
+

이벤트 상세

+ +
+
+ celebration +
+

이벤트 제목

+

SNS 팔로우 이벤트

+
+ +
+
+ +
+
+ card_giftcard +
+

경품

+

커피 쿠폰

+
+ +
+
+ +
+
+ description +
+

이벤트 설명

+

+ SNS를 팔로우하고 커피 쿠폰을 받으세요!
+ 많은 참여 부탁드립니다. +

+
+ +
+
+ +
+
+ how_to_reg +
+

참여 방법

+

SNS 팔로우

+
+
+
+
+ + +
+

배포 채널

+
+
+ + language + 홈페이지 + + + chat_bubble + 카카오톡 + + + share + Instagram + +
+ +
+
+ + +
+
+
+ + +
+ 약관 보기 +
+
+ + +
+ + + +
+
+ + +
+
+ + + + + diff --git a/design/uiux/prototype/13-이벤트상세.html b/design/uiux/prototype/13-이벤트상세.html new file mode 100644 index 0000000..1d0fba6 --- /dev/null +++ b/design/uiux/prototype/13-이벤트상세.html @@ -0,0 +1,437 @@ + + + + + + 이벤트 상세 - KT AI 이벤트 마케팅 + + + + + +
+ + + +
+ +
+
+

SNS 팔로우 이벤트

+ +
+ +
+ 진행중 + + AI 추천 + +
+ +

+ 2025.01.15 ~ 2025.02.15 +

+
+ + +
+
+

실시간 현황

+
+ fiber_manual_record + 실시간 업데이트 +
+
+ +
+
+
+
+
+
+
+ + +
+

참여 추이

+
+
+
+ + + +
+
+ + +
+
+ show_chart +

참여자 추이 차트

+
+
+
+
+ + +
+

이벤트 정보

+ +
+
+ card_giftcard +
+

경품

+

커피 쿠폰

+
+
+
+ +
+
+ how_to_reg +
+

참여 방법

+

SNS 팔로우

+
+
+
+ +
+
+ attach_money +
+

예상 비용

+

250,000원

+
+
+
+ +
+
+ share +
+

배포 채널

+
+ 홈페이지 + 카카오톡 + Instagram +
+
+
+
+
+ + +
+

빠른 작업

+
+ + + + +
+
+ + +
+
+

최근 참여자

+ + 전체보기 + chevron_right + +
+ +
+
+ +
+
+
+
+ + +
+
+ + + + + diff --git a/design/uiux/prototype/common.js b/design/uiux/prototype/common.js new file mode 100644 index 0000000..71c8d71 --- /dev/null +++ b/design/uiux/prototype/common.js @@ -0,0 +1,1071 @@ +// ================================================================= +// KT AI 이벤트 마케팅 서비스 - 공통 JavaScript 모듈 +// Version: 1.0.0 +// ================================================================= + +const KTEventApp = (() => { + 'use strict'; + + // ================================================================= + // 1. Utility Functions + // ================================================================= + const Utils = { + /** + * 전화번호 포맷팅 (010-1234-5678) + */ + formatPhoneNumber(phone) { + const cleaned = phone.replace(/\D/g, ''); + const match = cleaned.match(/^(\d{3})(\d{3,4})(\d{4})$/); + if (match) { + return `${match[1]}-${match[2]}-${match[3]}`; + } + return phone; + }, + + /** + * 이메일 유효성 검사 + */ + validateEmail(email) { + const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return re.test(email); + }, + + /** + * 비밀번호 유효성 검사 (8자 이상, 영문+숫자 포함) + */ + validatePassword(password) { + const hasLength = password.length >= 8; + const hasLetter = /[a-zA-Z]/.test(password); + const hasNumber = /\d/.test(password); + return hasLength && hasLetter && hasNumber; + }, + + /** + * 날짜 포맷팅 (YYYY.MM.DD) + */ + formatDate(date) { + if (typeof date === 'string') { + date = new Date(date); + } + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + return `${year}.${month}.${day}`; + }, + + /** + * 날짜 시간 포맷팅 (YYYY.MM.DD HH:MM) + */ + formatDateTime(date) { + if (typeof date === 'string') { + date = new Date(date); + } + const dateStr = this.formatDate(date); + const hours = String(date.getHours()).padStart(2, '0'); + const minutes = String(date.getMinutes()).padStart(2, '0'); + return `${dateStr} ${hours}:${minutes}`; + }, + + /** + * 숫자 포맷팅 (1,234,567) + */ + formatNumber(num) { + return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ','); + }, + + /** + * 금액 포맷팅 (1,234,567원) + */ + formatCurrency(amount) { + return `${this.formatNumber(amount)}원`; + }, + + /** + * 디바운스 함수 + */ + debounce(func, wait) { + let timeout; + return function executedFunction(...args) { + const later = () => { + clearTimeout(timeout); + func(...args); + }; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + }; + }, + + /** + * LocalStorage 저장 + */ + saveToStorage(key, value) { + try { + localStorage.setItem(key, JSON.stringify(value)); + return true; + } catch (e) { + console.error('Storage save failed:', e); + return false; + } + }, + + /** + * LocalStorage 읽기 + */ + getFromStorage(key, defaultValue = null) { + try { + const item = localStorage.getItem(key); + return item ? JSON.parse(item) : defaultValue; + } catch (e) { + console.error('Storage read failed:', e); + return defaultValue; + } + }, + + /** + * LocalStorage 삭제 + */ + removeFromStorage(key) { + try { + localStorage.removeItem(key); + return true; + } catch (e) { + console.error('Storage remove failed:', e); + return false; + } + }, + + /** + * 랜덤 ID 생성 + */ + generateId() { + return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; + } + }; + + // ================================================================= + // 2. Navigation Components + // ================================================================= + const Navigation = { + /** + * 헤더 생성 + */ + createHeader({ title = '', showBack = true, showMenu = true, showProfile = true } = {}) { + const header = document.createElement('header'); + header.className = 'header'; + + const leftDiv = document.createElement('div'); + leftDiv.className = 'header-left'; + + if (showMenu) { + const menuBtn = document.createElement('button'); + menuBtn.className = 'header-icon-btn'; + menuBtn.innerHTML = 'menu'; + menuBtn.setAttribute('aria-label', '메뉴'); + leftDiv.appendChild(menuBtn); + } + + if (showBack) { + const backBtn = document.createElement('button'); + backBtn.className = 'header-icon-btn'; + backBtn.innerHTML = 'arrow_back'; + backBtn.setAttribute('aria-label', '뒤로가기'); + backBtn.addEventListener('click', () => window.history.back()); + leftDiv.appendChild(backBtn); + } + + const titleEl = document.createElement('h1'); + titleEl.className = 'header-title'; + titleEl.textContent = title; + leftDiv.appendChild(titleEl); + + const rightDiv = document.createElement('div'); + rightDiv.className = 'header-right'; + + if (showProfile) { + const profileBtn = document.createElement('button'); + profileBtn.className = 'header-icon-btn'; + profileBtn.innerHTML = 'account_circle'; + profileBtn.setAttribute('aria-label', '프로필'); + rightDiv.appendChild(profileBtn); + } + + header.appendChild(leftDiv); + header.appendChild(rightDiv); + + return header; + }, + + /** + * 하단 네비게이션 생성 + */ + createBottomNav(currentPage = 'home') { + const nav = document.createElement('nav'); + nav.className = 'bottom-nav'; + nav.setAttribute('role', 'navigation'); + nav.setAttribute('aria-label', '주 네비게이션'); + + const navItems = [ + { id: 'home', label: '홈', icon: 'home', href: '05-대시보드.html' }, + { id: 'events', label: '이벤트', icon: 'celebration', href: '06-이벤트목록.html' }, + { id: 'analytics', label: '분석', icon: 'analytics', href: '13-이벤트상세.html' }, + { id: 'profile', label: '프로필', icon: 'person', href: '03-프로필.html' } + ]; + + navItems.forEach(item => { + const link = document.createElement('a'); + link.className = 'bottom-nav-item'; + if (item.id === currentPage) { + link.classList.add('active'); + } + link.href = item.href; + link.innerHTML = ` + ${item.icon} + ${item.label} + `; + nav.appendChild(link); + }); + + return nav; + }, + + /** + * Floating Action Button 생성 + */ + createFAB(icon = 'add', onClick) { + const fab = document.createElement('button'); + fab.className = 'fab'; + fab.innerHTML = `${icon}`; + fab.setAttribute('aria-label', '새 이벤트 생성'); + + if (onClick) { + fab.addEventListener('click', onClick); + } + + return fab; + } + }; + + // ================================================================= + // 3. Form Components + // ================================================================= + const Form = { + /** + * 입력 필드 생성 + */ + createInput({ + type = 'text', + id, + name, + label, + placeholder = '', + required = false, + value = '', + error = '', + hint = '', + disabled = false, + onChange + } = {}) { + const group = document.createElement('div'); + group.className = 'form-group'; + + if (label) { + const labelEl = document.createElement('label'); + labelEl.className = 'form-label'; + if (required) { + labelEl.classList.add('form-label-required'); + } + labelEl.setAttribute('for', id); + labelEl.textContent = label; + group.appendChild(labelEl); + } + + const input = document.createElement('input'); + input.type = type; + input.id = id; + input.name = name || id; + input.className = 'form-input'; + input.placeholder = placeholder; + input.value = value; + input.disabled = disabled; + + if (required) { + input.required = true; + } + + if (onChange) { + input.addEventListener('input', onChange); + } + + group.appendChild(input); + + if (error) { + const errorEl = document.createElement('span'); + errorEl.className = 'form-error'; + errorEl.textContent = error; + group.appendChild(errorEl); + } + + if (hint) { + const hintEl = document.createElement('span'); + hintEl.className = 'form-hint'; + hintEl.textContent = hint; + group.appendChild(hintEl); + } + + return { group, input }; + }, + + /** + * 선택 필드 생성 + */ + createSelect({ + id, + name, + label, + options = [], + required = false, + value = '', + onChange + } = {}) { + const group = document.createElement('div'); + group.className = 'form-group'; + + if (label) { + const labelEl = document.createElement('label'); + labelEl.className = 'form-label'; + if (required) { + labelEl.classList.add('form-label-required'); + } + labelEl.setAttribute('for', id); + labelEl.textContent = label; + group.appendChild(labelEl); + } + + const select = document.createElement('select'); + select.id = id; + select.name = name || id; + select.className = 'form-select'; + + if (required) { + select.required = true; + } + + options.forEach(opt => { + const option = document.createElement('option'); + option.value = opt.value; + option.textContent = opt.label; + if (opt.value === value) { + option.selected = true; + } + select.appendChild(option); + }); + + if (onChange) { + select.addEventListener('change', onChange); + } + + group.appendChild(select); + + return { group, select }; + }, + + /** + * 텍스트 영역 생성 + */ + createTextarea({ + id, + name, + label, + placeholder = '', + required = false, + value = '', + rows = 4, + onChange + } = {}) { + const group = document.createElement('div'); + group.className = 'form-group'; + + if (label) { + const labelEl = document.createElement('label'); + labelEl.className = 'form-label'; + if (required) { + labelEl.classList.add('form-label-required'); + } + labelEl.setAttribute('for', id); + labelEl.textContent = label; + group.appendChild(labelEl); + } + + const textarea = document.createElement('textarea'); + textarea.id = id; + textarea.name = name || id; + textarea.className = 'form-textarea'; + textarea.placeholder = placeholder; + textarea.value = value; + textarea.rows = rows; + + if (required) { + textarea.required = true; + } + + if (onChange) { + textarea.addEventListener('input', onChange); + } + + group.appendChild(textarea); + + return { group, textarea }; + }, + + /** + * 체크박스 생성 + */ + createCheckbox({ + id, + name, + label, + checked = false, + onChange + } = {}) { + const checkDiv = document.createElement('div'); + checkDiv.className = 'form-check'; + + const input = document.createElement('input'); + input.type = 'checkbox'; + input.id = id; + input.name = name || id; + input.className = 'form-check-input'; + input.checked = checked; + + if (onChange) { + input.addEventListener('change', onChange); + } + + const labelEl = document.createElement('label'); + labelEl.className = 'form-check-label'; + labelEl.setAttribute('for', id); + labelEl.textContent = label; + + checkDiv.appendChild(input); + checkDiv.appendChild(labelEl); + + return { checkDiv, input }; + }, + + /** + * 라디오 버튼 생성 + */ + createRadio({ + id, + name, + value, + label, + checked = false, + onChange + } = {}) { + const radioDiv = document.createElement('div'); + radioDiv.className = 'form-check'; + + const input = document.createElement('input'); + input.type = 'radio'; + input.id = id; + input.name = name; + input.value = value; + input.className = 'form-check-input'; + input.checked = checked; + + if (onChange) { + input.addEventListener('change', onChange); + } + + const labelEl = document.createElement('label'); + labelEl.className = 'form-check-label'; + labelEl.setAttribute('for', id); + labelEl.textContent = label; + + radioDiv.appendChild(input); + radioDiv.appendChild(labelEl); + + return { radioDiv, input }; + }, + + /** + * 버튼 생성 + */ + createButton({ + text, + variant = 'primary', + size = 'large', + icon = null, + fullWidth = false, + disabled = false, + onClick + } = {}) { + const button = document.createElement('button'); + button.className = `btn btn-${variant} btn-${size}`; + + if (fullWidth) { + button.classList.add('btn-full'); + } + + button.disabled = disabled; + + if (icon) { + const iconEl = document.createElement('span'); + iconEl.className = 'material-icons'; + iconEl.textContent = icon; + button.appendChild(iconEl); + } + + const textNode = document.createTextNode(text); + button.appendChild(textNode); + + if (onClick) { + button.addEventListener('click', onClick); + } + + return button; + } + }; + + // ================================================================= + // 4. Feedback Components + // ================================================================= + const Feedback = { + /** + * Toast 메시지 표시 + */ + showToast(message, duration = 3000) { + const toast = document.createElement('div'); + toast.className = 'toast'; + toast.textContent = message; + + document.body.appendChild(toast); + + setTimeout(() => { + toast.remove(); + }, duration); + }, + + /** + * Modal 표시 + */ + showModal({ title, content, buttons = [] } = {}) { + const backdrop = document.createElement('div'); + backdrop.className = 'modal-backdrop'; + + const modal = document.createElement('div'); + modal.className = 'modal'; + + if (title) { + const header = document.createElement('div'); + header.className = 'modal-header'; + const titleEl = document.createElement('h2'); + titleEl.className = 'modal-title'; + titleEl.textContent = title; + header.appendChild(titleEl); + modal.appendChild(header); + } + + if (content) { + const body = document.createElement('div'); + body.className = 'modal-body'; + if (typeof content === 'string') { + body.innerHTML = content; + } else { + body.appendChild(content); + } + modal.appendChild(body); + } + + if (buttons.length > 0) { + const footer = document.createElement('div'); + footer.className = 'modal-footer'; + + buttons.forEach(btnConfig => { + const btn = Form.createButton(btnConfig); + footer.appendChild(btn); + }); + + modal.appendChild(footer); + } + + const close = () => { + backdrop.remove(); + }; + + backdrop.addEventListener('click', (e) => { + if (e.target === backdrop) { + close(); + } + }); + + backdrop.appendChild(modal); + document.body.appendChild(backdrop); + + return { backdrop, modal, close }; + }, + + /** + * Bottom Sheet 표시 + */ + showBottomSheet(content) { + const backdrop = document.createElement('div'); + backdrop.className = 'modal-backdrop'; + + const sheet = document.createElement('div'); + sheet.className = 'bottom-sheet'; + + const handle = document.createElement('div'); + handle.className = 'bottom-sheet-handle'; + sheet.appendChild(handle); + + const sheetContent = document.createElement('div'); + sheetContent.className = 'bottom-sheet-content'; + + if (typeof content === 'string') { + sheetContent.innerHTML = content; + } else { + sheetContent.appendChild(content); + } + + sheet.appendChild(sheetContent); + + const close = () => { + backdrop.remove(); + }; + + backdrop.addEventListener('click', (e) => { + if (e.target === backdrop) { + close(); + } + }); + + backdrop.appendChild(sheet); + document.body.appendChild(backdrop); + + return { backdrop, sheet, close }; + }, + + /** + * Loading Spinner 표시 + */ + showSpinner(message = '') { + const container = document.createElement('div'); + container.className = 'spinner-center'; + container.innerHTML = ` +
+
+ ${message ? `

${message}

` : ''} +
+ `; + return container; + }, + + /** + * Progress Bar 생성 + */ + createProgressBar(value = 0) { + const progress = document.createElement('div'); + progress.className = 'progress'; + + const bar = document.createElement('div'); + bar.className = 'progress-bar'; + bar.style.width = `${value}%`; + bar.setAttribute('role', 'progressbar'); + bar.setAttribute('aria-valuenow', value); + bar.setAttribute('aria-valuemin', '0'); + bar.setAttribute('aria-valuemax', '100'); + + progress.appendChild(bar); + + return { + element: progress, + setValue: (newValue) => { + bar.style.width = `${newValue}%`; + bar.setAttribute('aria-valuenow', newValue); + } + }; + } + }; + + // ================================================================= + // 5. Card Components + // ================================================================= + const Cards = { + /** + * 이벤트 카드 생성 + */ + createEventCard({ + id, + title, + status, + startDate, + endDate, + participants, + views, + roi, + onClick + } = {}) { + const card = document.createElement('div'); + card.className = 'card event-card'; + + if (onClick) { + card.classList.add('card-clickable'); + card.addEventListener('click', () => onClick(id)); + } + + const statusBadgeClass = status === '진행중' ? 'badge-active' : + status === '예정' ? 'badge-scheduled' : + 'badge-ended'; + + card.innerHTML = ` +
+

${title}

+ ${status} +
+

+ ${Utils.formatDate(startDate)} ~ ${Utils.formatDate(endDate)} +

+
+
+
참여자
+
${Utils.formatNumber(participants)}
+
+
+
조회수
+
${Utils.formatNumber(views)}
+
+
+
ROI
+
${roi}%
+
+
+ `; + + return card; + }, + + /** + * KPI 카드 생성 + */ + createKPICard({ + icon, + iconType = 'primary', + label, + value, + onClick + } = {}) { + const card = document.createElement('div'); + card.className = 'card kpi-card'; + + if (onClick) { + card.classList.add('card-clickable'); + card.addEventListener('click', onClick); + } + + card.innerHTML = ` +
+ ${icon} +
+
+
${label}
+
${value}
+
+ `; + + return card; + }, + + /** + * 옵션 선택 카드 생성 + */ + createOptionCard({ + id, + title, + description, + content, + selected = false, + onSelect + } = {}) { + const card = document.createElement('div'); + card.className = 'card option-card'; + + if (selected) { + card.classList.add('selected'); + } + + card.innerHTML = ` + +

${title}

+ ${description ? `

${description}

` : ''} + ${content || ''} + `; + + card.addEventListener('click', () => { + document.querySelectorAll('.option-card').forEach(c => c.classList.remove('selected')); + card.classList.add('selected'); + const radio = card.querySelector('input[type="radio"]'); + radio.checked = true; + + if (onSelect) { + onSelect(id); + } + }); + + return card; + } + }; + + // ================================================================= + // 6. Session Management + // ================================================================= + const Session = { + /** + * 사용자 정보 저장 + */ + saveUser(user) { + return Utils.saveToStorage('kt_event_user', user); + }, + + /** + * 사용자 정보 가져오기 + */ + getUser() { + return Utils.getFromStorage('kt_event_user'); + }, + + /** + * 로그인 여부 확인 + */ + isLoggedIn() { + return this.getUser() !== null; + }, + + /** + * 로그아웃 + */ + logout() { + Utils.removeFromStorage('kt_event_user'); + window.location.href = '01-로그인.html'; + }, + + /** + * 로그인 필요 페이지 보호 + */ + requireAuth() { + if (!this.isLoggedIn()) { + window.location.href = '01-로그인.html'; + } + } + }; + + // ================================================================= + // 7. Mock Data + // ================================================================= + const MockData = { + /** + * 예제 이벤트 데이터 + */ + getEvents() { + return [ + { + id: 'evt001', + title: 'SNS 팔로우 이벤트', + status: '진행중', + purpose: '신규 고객 유치', + startDate: '2025-01-15', + endDate: '2025-02-15', + participants: 142, + views: 856, + roi: 520, + budget: '저비용', + channel: '온라인', + prize: '커피 쿠폰' + }, + { + id: 'evt002', + title: '설 맞이 할인 이벤트', + status: '예정', + purpose: '매출 증대', + startDate: '2025-02-01', + endDate: '2025-02-10', + participants: 0, + views: 0, + roi: 0, + budget: '중비용', + channel: '오프라인', + prize: '10% 할인권' + }, + { + id: 'evt003', + title: '고객 만족도 조사', + status: '종료', + purpose: '고객 유지', + startDate: '2024-12-01', + endDate: '2024-12-31', + participants: 287, + views: 1234, + roi: 340, + budget: '저비용', + channel: '온라인', + prize: '포인트 적립' + } + ]; + }, + + /** + * 예제 사용자 데이터 + */ + getDefaultUser() { + return { + id: 'user001', + name: '홍길동', + email: 'hong@example.com', + phone: '010-1234-5678', + businessName: '홍길동 고깃집', + businessType: '음식점', + joinDate: '2025-01-01' + }; + }, + + /** + * 이벤트 목적 옵션 + */ + getEventPurposes() { + return [ + { + id: 'new_customers', + title: '신규 고객 유치', + description: '새로운 고객을 매장으로 끌어들이고 싶어요', + icon: 'person_add' + }, + { + id: 'sales', + title: '매출 증대', + description: '단기간에 매출을 올리고 싶어요', + icon: 'trending_up' + }, + { + id: 'retention', + title: '고객 유지', + description: '기존 고객이 계속 방문하도록 하고 싶어요', + icon: 'favorite' + }, + { + id: 'awareness', + title: '브랜드 인지도', + description: '우리 가게를 더 많은 사람에게 알리고 싶어요', + icon: 'campaign' + } + ]; + }, + + /** + * AI 추천 이벤트 데이터 + */ + getAIRecommendations() { + return { + trends: { + industry: '음식점업 신년 프로모션 트렌드', + location: '강남구 음식점 할인 이벤트 증가', + season: '설 연휴 특수 대비 고객 유치 전략' + }, + recommendations: [ + // 저비용 + { + budget: '저비용', + type: '온라인', + title: 'SNS 팔로우 이벤트', + prize: '커피 쿠폰', + method: 'SNS 팔로우', + participants: 180, + cost: 250000, + roi: 520 + }, + { + budget: '저비용', + type: '오프라인', + title: '전화번호 등록 이벤트', + prize: '커피 쿠폰', + method: '전화번호 등록', + participants: 150, + cost: 300000, + roi: 450 + }, + // 중비용 + { + budget: '중비용', + type: '온라인', + title: '리뷰 작성 이벤트', + prize: '5천원 상품권', + method: '리뷰 작성', + participants: 250, + cost: 1500000, + roi: 380 + }, + { + budget: '중비용', + type: '오프라인', + title: '방문 도장 적립 이벤트', + prize: '무료 식사권', + method: '방문 도장 적립', + participants: 200, + cost: 1800000, + roi: 320 + }, + // 고비용 + { + budget: '고비용', + type: '온라인', + title: '인플루언서 협업 이벤트', + prize: '1만원 할인권', + method: '인플루언서 팔로우', + participants: 400, + cost: 5000000, + roi: 280 + }, + { + budget: '고비용', + type: '오프라인', + title: 'VIP 고객 초대 이벤트', + prize: '특별 메뉴 제공', + method: 'VIP 초대장', + participants: 300, + cost: 6000000, + roi: 240 + } + ] + }; + } + }; + + // ================================================================= + // Public API + // ================================================================= + return { + Utils, + Navigation, + Form, + Feedback, + Cards, + Session, + MockData + }; +})(); + +// Material Icons 폰트 로드 +if (!document.querySelector('link[href*="material-icons"]')) { + const link = document.createElement('link'); + link.rel = 'stylesheet'; + link.href = 'https://fonts.googleapis.com/icon?family=Material+Icons'; + document.head.appendChild(link); +} + +// Pretendard 폰트 로드 +if (!document.querySelector('link[href*="pretendard"]')) { + const link = document.createElement('link'); + link.rel = 'stylesheet'; + link.href = 'https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/static/pretendard.min.css'; + document.head.appendChild(link); +} diff --git a/design/uiux/prototype/styles.css b/design/uiux/prototype/styles.css new file mode 100644 index 0000000..0be54e8 --- /dev/null +++ b/design/uiux/prototype/styles.css @@ -0,0 +1,973 @@ +/* ================================================================= + KT AI 이벤트 마케팅 서비스 - 공통 스타일시트 + Version: 1.0.0 + Mobile First Design Philosophy + ================================================================= */ + +/* ================================================================= + 1. CSS Variables - Design System + ================================================================= */ +:root { + /* Brand Colors */ + --color-kt-red: #E31E24; + --color-ai-blue: #0066FF; + + /* Grayscale */ + --color-gray-50: #F9FAFB; + --color-gray-100: #F3F4F6; + --color-gray-200: #E5E7EB; + --color-gray-300: #D1D5DB; + --color-gray-400: #9CA3AF; + --color-gray-500: #6B7280; + --color-gray-600: #4B5563; + --color-gray-700: #374151; + --color-gray-800: #1F2937; + --color-gray-900: #111827; + + /* Semantic Colors */ + --color-success: #10B981; + --color-warning: #F59E0B; + --color-error: #EF4444; + --color-info: #3B82F6; + + /* Background */ + --color-bg-primary: #FFFFFF; + --color-bg-secondary: #F9FAFB; + --color-bg-tertiary: #F3F4F6; + + /* Text */ + --color-text-primary: #111827; + --color-text-secondary: #4B5563; + --color-text-tertiary: #9CA3AF; + --color-text-inverse: #FFFFFF; + + /* Gradients */ + --gradient-primary: linear-gradient(135deg, #E31E24 0%, #B71419 100%); + --gradient-ai: linear-gradient(135deg, #0066FF 0%, #0052CC 100%); + + /* Typography Scale */ + --font-family: 'Pretendard', -apple-system, BlinkMacSystemFont, system-ui, sans-serif; + --font-size-display: 28px; + --font-size-title-large: 24px; + --font-size-title: 20px; + --font-size-headline: 18px; + --font-size-body-large: 16px; + --font-size-body: 14px; + --font-size-body-small: 13px; + --font-size-caption: 12px; + + --line-height-display: 1.2; + --line-height-title: 1.3; + --line-height-body: 1.5; + --line-height-caption: 1.4; + + --font-weight-bold: 700; + --font-weight-semibold: 600; + --font-weight-medium: 500; + --font-weight-regular: 400; + + /* Spacing System (4px grid) */ + --spacing-xs: 4px; + --spacing-sm: 8px; + --spacing-md: 16px; + --spacing-lg: 24px; + --spacing-xl: 32px; + --spacing-2xl: 48px; + + /* Shadows */ + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05); + --shadow-md: 0 4px 6px rgba(0, 0, 0, 0.07); + --shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1); + --shadow-xl: 0 20px 25px rgba(0, 0, 0, 0.15); + + /* Border Radius */ + --radius-sm: 4px; + --radius-md: 8px; + --radius-lg: 12px; + --radius-xl: 16px; + --radius-full: 9999px; + + /* Z-Index */ + --z-dropdown: 1000; + --z-sticky: 1020; + --z-fixed: 1030; + --z-modal-backdrop: 1040; + --z-modal: 1050; + --z-popover: 1060; + --z-tooltip: 1070; + + /* Animation */ + --duration-instant: 0ms; + --duration-fast: 150ms; + --duration-normal: 300ms; + --duration-slow: 500ms; + + --ease-in: cubic-bezier(0.4, 0, 1, 1); + --ease-out: cubic-bezier(0, 0, 0.2, 1); + --ease-in-out: cubic-bezier(0.4, 0, 0.2, 1); +} + +/* Tablet Responsive Variables */ +@media (min-width: 768px) { + :root { + --font-size-display: 32px; + --font-size-title-large: 28px; + --font-size-title: 24px; + --font-size-headline: 20px; + } +} + +/* Desktop Responsive Variables */ +@media (min-width: 1024px) { + :root { + --font-size-display: 36px; + --font-size-title-large: 32px; + --font-size-title: 28px; + --font-size-headline: 22px; + } +} + +/* ================================================================= + 2. Reset & Base Styles + ================================================================= */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +html { + font-size: 16px; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +body { + font-family: var(--font-family); + font-size: var(--font-size-body); + line-height: var(--line-height-body); + color: var(--color-text-primary); + background-color: var(--color-bg-primary); + overflow-x: hidden; +} + +/* ================================================================= + 3. Typography + ================================================================= */ +.text-display { + font-size: var(--font-size-display); + line-height: var(--line-height-display); + font-weight: var(--font-weight-bold); +} + +.text-title-large { + font-size: var(--font-size-title-large); + line-height: var(--line-height-title); + font-weight: var(--font-weight-bold); +} + +.text-title { + font-size: var(--font-size-title); + line-height: var(--line-height-title); + font-weight: var(--font-weight-semibold); +} + +.text-headline { + font-size: var(--font-size-headline); + line-height: var(--line-height-title); + font-weight: var(--font-weight-semibold); +} + +.text-body-large { + font-size: var(--font-size-body-large); + line-height: var(--line-height-body); + font-weight: var(--font-weight-regular); +} + +.text-body { + font-size: var(--font-size-body); + line-height: var(--line-height-body); + font-weight: var(--font-weight-regular); +} + +.text-body-small { + font-size: var(--font-size-body-small); + line-height: var(--line-height-body); + font-weight: var(--font-weight-regular); +} + +.text-caption { + font-size: var(--font-size-caption); + line-height: var(--line-height-caption); + font-weight: var(--font-weight-regular); +} + +.text-primary { color: var(--color-text-primary); } +.text-secondary { color: var(--color-text-secondary); } +.text-tertiary { color: var(--color-text-tertiary); } +.text-inverse { color: var(--color-text-inverse); } +.text-kt-red { color: var(--color-kt-red); } +.text-ai-blue { color: var(--color-ai-blue); } +.text-success { color: var(--color-success); } +.text-warning { color: var(--color-warning); } +.text-error { color: var(--color-error); } + +.text-bold { font-weight: var(--font-weight-bold); } +.text-semibold { font-weight: var(--font-weight-semibold); } +.text-medium { font-weight: var(--font-weight-medium); } + +.text-center { text-align: center; } +.text-left { text-align: left; } +.text-right { text-align: right; } + +/* ================================================================= + 4. Layout Utilities + ================================================================= */ +.container { + width: 100%; + max-width: 1200px; + margin: 0 auto; + padding: 0 var(--spacing-md); +} + +.page { + min-height: 100vh; + background-color: var(--color-bg-secondary); + padding-bottom: 76px; /* Bottom nav height + spacing */ +} + +.page-with-header { + padding-top: 56px; /* Header height */ +} + +/* ================================================================= + 5. Button Components + ================================================================= */ +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: var(--spacing-sm); + border: none; + border-radius: var(--radius-md); + font-family: var(--font-family); + font-weight: var(--font-weight-semibold); + cursor: pointer; + transition: all var(--duration-fast) var(--ease-out); + text-decoration: none; + user-select: none; +} + +.btn:disabled { + opacity: 0.4; + cursor: not-allowed; +} + +/* Button Sizes */ +.btn-large { + height: 48px; + padding: 0 var(--spacing-lg); + font-size: var(--font-size-body-large); +} + +.btn-medium { + height: 44px; + padding: 0 var(--spacing-md); + font-size: var(--font-size-body); +} + +.btn-small { + height: 36px; + padding: 0 var(--spacing-md); + font-size: var(--font-size-body-small); +} + +/* Button Variants */ +.btn-primary { + background: var(--gradient-primary); + color: var(--color-text-inverse); + box-shadow: var(--shadow-sm); +} + +.btn-primary:hover:not(:disabled) { + box-shadow: var(--shadow-md); + transform: translateY(-1px); +} + +.btn-primary:active:not(:disabled) { + transform: translateY(0); + box-shadow: var(--shadow-sm); +} + +.btn-secondary { + background-color: var(--color-bg-primary); + color: var(--color-kt-red); + border: 1px solid var(--color-gray-300); +} + +.btn-secondary:hover:not(:disabled) { + background-color: var(--color-gray-50); + border-color: var(--color-kt-red); +} + +.btn-text { + background-color: transparent; + color: var(--color-kt-red); +} + +.btn-text:hover:not(:disabled) { + background-color: rgba(227, 30, 36, 0.08); +} + +.btn-full { + width: 100%; +} + +/* ================================================================= + 6. Card Components + ================================================================= */ +.card { + background-color: var(--color-bg-primary); + border-radius: var(--radius-lg); + padding: var(--spacing-lg); + box-shadow: var(--shadow-sm); + transition: box-shadow var(--duration-fast) var(--ease-out); +} + +.card:hover { + box-shadow: var(--shadow-md); +} + +.card-clickable { + cursor: pointer; +} + +.card-clickable:active { + transform: scale(0.98); +} + +.event-card { + display: flex; + flex-direction: column; + gap: var(--spacing-md); +} + +.event-card-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: var(--spacing-sm); +} + +.event-card-badge { + display: inline-flex; + align-items: center; + padding: 4px 12px; + border-radius: var(--radius-full); + font-size: var(--font-size-caption); + font-weight: var(--font-weight-medium); +} + +.badge-active { + background-color: rgba(16, 185, 129, 0.1); + color: var(--color-success); +} + +.badge-scheduled { + background-color: rgba(59, 130, 246, 0.1); + color: var(--color-info); +} + +.badge-ended { + background-color: rgba(156, 163, 175, 0.1); + color: var(--color-gray-500); +} + +.event-card-stats { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: var(--spacing-md); + padding-top: var(--spacing-md); + border-top: 1px solid var(--color-gray-200); +} + +.stat-item { + text-align: center; +} + +.stat-label { + font-size: var(--font-size-caption); + color: var(--color-text-tertiary); + margin-bottom: 4px; +} + +.stat-value { + font-size: var(--font-size-headline); + font-weight: var(--font-weight-bold); + color: var(--color-text-primary); +} + +/* KPI Card */ +.kpi-card { + display: flex; + align-items: center; + gap: var(--spacing-md); + padding: var(--spacing-md); +} + +.kpi-icon { + width: 48px; + height: 48px; + display: flex; + align-items: center; + justify-content: center; + border-radius: var(--radius-md); + font-size: 24px; +} + +.kpi-icon-primary { + background: rgba(227, 30, 36, 0.1); + color: var(--color-kt-red); +} + +.kpi-icon-ai { + background: rgba(0, 102, 255, 0.1); + color: var(--color-ai-blue); +} + +.kpi-icon-success { + background: rgba(16, 185, 129, 0.1); + color: var(--color-success); +} + +.kpi-content { + flex: 1; +} + +.kpi-label { + font-size: var(--font-size-body-small); + color: var(--color-text-secondary); + margin-bottom: 4px; +} + +.kpi-value { + font-size: var(--font-size-title); + font-weight: var(--font-weight-bold); + color: var(--color-text-primary); +} + +/* Option Card for Selection */ +.option-card { + position: relative; + border: 2px solid var(--color-gray-200); + transition: all var(--duration-fast) var(--ease-out); +} + +.option-card:hover { + border-color: var(--color-kt-red); +} + +.option-card.selected { + border-color: var(--color-kt-red); + background-color: rgba(227, 30, 36, 0.02); +} + +.option-card-radio { + position: absolute; + top: var(--spacing-md); + right: var(--spacing-md); +} + +/* ================================================================= + 7. Form Components + ================================================================= */ +.form-group { + margin-bottom: var(--spacing-lg); +} + +.form-label { + display: block; + font-size: var(--font-size-body); + font-weight: var(--font-weight-medium); + color: var(--color-text-primary); + margin-bottom: var(--spacing-sm); +} + +.form-label-required::after { + content: " *"; + color: var(--color-error); +} + +.form-input, +.form-select, +.form-textarea { + width: 100%; + padding: 12px var(--spacing-md); + border: 1px solid var(--color-gray-300); + border-radius: var(--radius-md); + font-family: var(--font-family); + font-size: var(--font-size-body); + color: var(--color-text-primary); + background-color: var(--color-bg-primary); + transition: all var(--duration-fast) var(--ease-out); +} + +.form-input::placeholder, +.form-textarea::placeholder { + color: var(--color-text-tertiary); +} + +.form-input:focus, +.form-select:focus, +.form-textarea:focus { + outline: none; + border-color: var(--color-kt-red); + box-shadow: 0 0 0 3px rgba(227, 30, 36, 0.1); +} + +.form-input:disabled, +.form-select:disabled, +.form-textarea:disabled { + background-color: var(--color-gray-100); + cursor: not-allowed; +} + +.form-textarea { + min-height: 100px; + resize: vertical; +} + +.form-error { + display: block; + margin-top: var(--spacing-sm); + font-size: var(--font-size-body-small); + color: var(--color-error); +} + +.form-hint { + display: block; + margin-top: var(--spacing-sm); + font-size: var(--font-size-body-small); + color: var(--color-text-tertiary); +} + +/* Checkbox & Radio */ +.form-check { + display: flex; + align-items: center; + gap: var(--spacing-sm); + cursor: pointer; +} + +.form-check-input { + width: 20px; + height: 20px; + cursor: pointer; +} + +.form-check-label { + font-size: var(--font-size-body); + color: var(--color-text-primary); + cursor: pointer; +} + +/* ================================================================= + 8. Navigation Components + ================================================================= */ +.header { + position: fixed; + top: 0; + left: 0; + right: 0; + height: 56px; + background-color: var(--color-bg-primary); + border-bottom: 1px solid var(--color-gray-200); + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 var(--spacing-md); + z-index: var(--z-sticky); +} + +.header-left, +.header-right { + display: flex; + align-items: center; + gap: var(--spacing-sm); +} + +.header-title { + font-size: var(--font-size-headline); + font-weight: var(--font-weight-semibold); + color: var(--color-text-primary); +} + +.header-icon-btn { + width: 40px; + height: 40px; + display: flex; + align-items: center; + justify-content: center; + border: none; + background: transparent; + color: var(--color-text-primary); + cursor: pointer; + border-radius: var(--radius-md); + transition: background-color var(--duration-fast) var(--ease-out); +} + +.header-icon-btn:hover { + background-color: var(--color-gray-100); +} + +.bottom-nav { + position: fixed; + bottom: 0; + left: 0; + right: 0; + height: 60px; + background-color: var(--color-bg-primary); + border-top: 1px solid var(--color-gray-200); + display: flex; + align-items: center; + justify-content: space-around; + padding: 0 var(--spacing-sm); + z-index: var(--z-sticky); +} + +.bottom-nav-item { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 4px; + padding: var(--spacing-sm); + color: var(--color-text-tertiary); + text-decoration: none; + font-size: var(--font-size-caption); + transition: color var(--duration-fast) var(--ease-out); + cursor: pointer; +} + +.bottom-nav-item:hover { + color: var(--color-text-secondary); +} + +.bottom-nav-item.active { + color: var(--color-kt-red); +} + +.bottom-nav-icon { + font-size: 24px; +} + +/* FAB (Floating Action Button) */ +.fab { + position: fixed; + bottom: 76px; /* Bottom nav height + spacing */ + right: var(--spacing-md); + width: 56px; + height: 56px; + background: var(--gradient-primary); + color: var(--color-text-inverse); + border: none; + border-radius: var(--radius-full); + box-shadow: var(--shadow-lg); + display: flex; + align-items: center; + justify-content: center; + font-size: 24px; + cursor: pointer; + transition: all var(--duration-fast) var(--ease-out); + z-index: var(--z-fixed); +} + +.fab:hover { + box-shadow: var(--shadow-xl); + transform: scale(1.05); +} + +.fab:active { + transform: scale(0.95); +} + +/* ================================================================= + 9. Feedback Components + ================================================================= */ +/* Toast */ +.toast { + position: fixed; + bottom: 92px; /* Bottom nav + spacing */ + left: 50%; + transform: translateX(-50%); + padding: var(--spacing-md) var(--spacing-lg); + background-color: var(--color-gray-900); + color: var(--color-text-inverse); + border-radius: var(--radius-md); + box-shadow: var(--shadow-lg); + font-size: var(--font-size-body); + z-index: var(--z-tooltip); + animation: slideUp var(--duration-normal) var(--ease-out); +} + +@keyframes slideUp { + from { + opacity: 0; + transform: translate(-50%, 20px); + } + to { + opacity: 1; + transform: translate(-50%, 0); + } +} + +/* Modal */ +.modal-backdrop { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.5); + z-index: var(--z-modal-backdrop); + animation: fadeIn var(--duration-normal) var(--ease-out); +} + +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +.modal { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background-color: var(--color-bg-primary); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-xl); + z-index: var(--z-modal); + max-width: 90%; + max-height: 90vh; + overflow-y: auto; + animation: scaleIn var(--duration-normal) var(--ease-out); +} + +@keyframes scaleIn { + from { + opacity: 0; + transform: translate(-50%, -50%) scale(0.95); + } + to { + opacity: 1; + transform: translate(-50%, -50%) scale(1); + } +} + +.modal-header { + padding: var(--spacing-lg); + border-bottom: 1px solid var(--color-gray-200); +} + +.modal-title { + font-size: var(--font-size-title); + font-weight: var(--font-weight-semibold); +} + +.modal-body { + padding: var(--spacing-lg); +} + +.modal-footer { + padding: var(--spacing-lg); + border-top: 1px solid var(--color-gray-200); + display: flex; + gap: var(--spacing-sm); + justify-content: flex-end; +} + +/* Bottom Sheet */ +.bottom-sheet { + position: fixed; + bottom: 0; + left: 0; + right: 0; + background-color: var(--color-bg-primary); + border-radius: var(--radius-xl) var(--radius-xl) 0 0; + box-shadow: var(--shadow-xl); + z-index: var(--z-modal); + max-height: 80vh; + overflow-y: auto; + animation: slideUpSheet var(--duration-normal) var(--ease-out); +} + +@keyframes slideUpSheet { + from { + transform: translateY(100%); + } + to { + transform: translateY(0); + } +} + +.bottom-sheet-handle { + width: 40px; + height: 4px; + background-color: var(--color-gray-300); + border-radius: var(--radius-full); + margin: var(--spacing-sm) auto; +} + +.bottom-sheet-content { + padding: var(--spacing-lg); +} + +/* Spinner */ +.spinner { + width: 40px; + height: 40px; + border: 4px solid var(--color-gray-200); + border-top-color: var(--color-kt-red); + border-radius: var(--radius-full); + animation: spin 1s linear infinite; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +.spinner-center { + display: flex; + align-items: center; + justify-content: center; + padding: var(--spacing-2xl); +} + +/* Progress Bar */ +.progress { + width: 100%; + height: 8px; + background-color: var(--color-gray-200); + border-radius: var(--radius-full); + overflow: hidden; +} + +.progress-bar { + height: 100%; + background: var(--gradient-primary); + border-radius: var(--radius-full); + transition: width var(--duration-normal) var(--ease-out); +} + +/* ================================================================= + 10. Utility Classes + ================================================================= */ +/* Spacing */ +.m-0 { margin: 0; } +.mt-xs { margin-top: var(--spacing-xs); } +.mt-sm { margin-top: var(--spacing-sm); } +.mt-md { margin-top: var(--spacing-md); } +.mt-lg { margin-top: var(--spacing-lg); } +.mt-xl { margin-top: var(--spacing-xl); } +.mt-2xl { margin-top: var(--spacing-2xl); } + +.mb-xs { margin-bottom: var(--spacing-xs); } +.mb-sm { margin-bottom: var(--spacing-sm); } +.mb-md { margin-bottom: var(--spacing-md); } +.mb-lg { margin-bottom: var(--spacing-lg); } +.mb-xl { margin-bottom: var(--spacing-xl); } +.mb-2xl { margin-bottom: var(--spacing-2xl); } + +.p-0 { padding: 0; } +.p-xs { padding: var(--spacing-xs); } +.p-sm { padding: var(--spacing-sm); } +.p-md { padding: var(--spacing-md); } +.p-lg { padding: var(--spacing-lg); } +.p-xl { padding: var(--spacing-xl); } +.p-2xl { padding: var(--spacing-2xl); } + +.gap-xs { gap: var(--spacing-xs); } +.gap-sm { gap: var(--spacing-sm); } +.gap-md { gap: var(--spacing-md); } +.gap-lg { gap: var(--spacing-lg); } + +/* Flexbox */ +.flex { display: flex; } +.flex-col { flex-direction: column; } +.flex-row { flex-direction: row; } +.items-center { align-items: center; } +.items-start { align-items: flex-start; } +.items-end { align-items: flex-end; } +.justify-center { justify-content: center; } +.justify-between { justify-content: space-between; } +.justify-end { justify-content: flex-end; } +.flex-1 { flex: 1; } +.flex-wrap { flex-wrap: wrap; } + +/* Grid */ +.grid { display: grid; } +.grid-cols-2 { grid-template-columns: repeat(2, 1fr); } +.grid-cols-3 { grid-template-columns: repeat(3, 1fr); } +.grid-cols-4 { grid-template-columns: repeat(4, 1fr); } + +/* Display */ +.hidden { display: none; } +.block { display: block; } +.inline-block { display: inline-block; } + +/* Width */ +.w-full { width: 100%; } +.w-auto { width: auto; } + +/* Background */ +.bg-primary { background-color: var(--color-bg-primary); } +.bg-secondary { background-color: var(--color-bg-secondary); } +.bg-tertiary { background-color: var(--color-bg-tertiary); } + +/* Border */ +.border { border: 1px solid var(--color-gray-200); } +.border-t { border-top: 1px solid var(--color-gray-200); } +.border-b { border-bottom: 1px solid var(--color-gray-200); } +.border-none { border: none; } + +.rounded-sm { border-radius: var(--radius-sm); } +.rounded-md { border-radius: var(--radius-md); } +.rounded-lg { border-radius: var(--radius-lg); } +.rounded-xl { border-radius: var(--radius-xl); } +.rounded-full { border-radius: var(--radius-full); } + +/* Shadow */ +.shadow-sm { box-shadow: var(--shadow-sm); } +.shadow-md { box-shadow: var(--shadow-md); } +.shadow-lg { box-shadow: var(--shadow-lg); } +.shadow-xl { box-shadow: var(--shadow-xl); } +.shadow-none { box-shadow: none; } + +/* Cursor */ +.cursor-pointer { cursor: pointer; } +.cursor-not-allowed { cursor: not-allowed; } + +/* ================================================================= + 11. Responsive Grid System + ================================================================= */ +@media (min-width: 768px) { + .container { + padding: 0 var(--spacing-lg); + } + + .tablet\:grid-cols-2 { grid-template-columns: repeat(2, 1fr); } + .tablet\:grid-cols-3 { grid-template-columns: repeat(3, 1fr); } + .tablet\:grid-cols-4 { grid-template-columns: repeat(4, 1fr); } +} + +@media (min-width: 1024px) { + .container { + padding: 0 var(--spacing-xl); + } + + .desktop\:grid-cols-2 { grid-template-columns: repeat(2, 1fr); } + .desktop\:grid-cols-3 { grid-template-columns: repeat(3, 1fr); } + .desktop\:grid-cols-4 { grid-template-columns: repeat(4, 1fr); } + .desktop\:grid-cols-5 { grid-template-columns: repeat(5, 1fr); } +}