diff --git a/design/uiux/prototype/00-메인대시보드.html b/design/uiux/prototype/00-메인대시보드.html new file mode 100644 index 0000000..3bacdd4 --- /dev/null +++ b/design/uiux/prototype/00-메인대시보드.html @@ -0,0 +1,307 @@ + + + + + + 메인 대시보드 - KT AI 이벤트 자동 생성 + + + + + + + + + + + + +
+
+ store + KT 이벤트 도우미 +
+
+ + +
+
+ + +
+

안녕하세요, 정우진님!

+

우진이네 고깃집

+
+ + +
+
+

진행 중인 이벤트

+ 전체보기 > +
+ +
+ +
+
+ + +
+

빠른 실행

+ +
+ +
+
+ add_circle + 새 이벤트
만들기
+
+
+ + +
+
+ event_note + 이벤트
관리
+
+
+ + +
+
+ bar_chart + 통계
보기
+
+
+ + +
+
+ help + 도움말 +
+
+
+
+ + +
+
+
+ lightbulb +
+

AI 활용 팁

+

이벤트 목적을 명확히 입력하면 더 정확한 기획안을 받을 수 있어요!

+
+
+
+
+ +
+ + + +
+ + + + + + + + diff --git a/design/uiux/prototype/01-회원가입.html b/design/uiux/prototype/01-회원가입.html new file mode 100644 index 0000000..0018670 --- /dev/null +++ b/design/uiux/prototype/01-회원가입.html @@ -0,0 +1,407 @@ + + + + + + 회원가입 - KT AI 이벤트 자동 생성 + + + + + + + + + + + + +
+ +

회원가입

+
+
+ + +
+
+ + +
+ store +

KT 이벤트 도우미

+

환영합니다!

+
+ + +
+ + +
+ + + 올바른 이메일 형식이 아닙니다. +
+ + +
+ + + 비밀번호는 8자 이상, 영문과 숫자 조합이어야 합니다. +
+ + +
+ + + 비밀번호가 일치하지 않습니다. +
+ +
+ + +
+ + + 상호명을 입력해주세요. +
+ + +
+ + + 업종을 선택해주세요. +
+ + +
+ + + 위치를 입력해주세요. +
+ + +
+ + + 올바른 전화번호 형식이 아닙니다. +
+ +
+ + +
+ + + + + + + +
+ + + + + +

+ 이미 계정이 있으신가요? + 로그인 +

+ +
+ +
+
+ + + + + + + + diff --git a/design/uiux/prototype/03-이벤트목적선택.html b/design/uiux/prototype/03-이벤트목적선택.html new file mode 100644 index 0000000..b2465ab --- /dev/null +++ b/design/uiux/prototype/03-이벤트목적선택.html @@ -0,0 +1,291 @@ + + + + + + 이벤트 목적 선택 - KT AI 이벤트 자동 생성 + + + + + + + + + + + + +
+ +

이벤트 만들기

+
+
+ + +
+
+ + +
+
+
1
+
목적
+
+
+
2
+
정보
+
+
+
3
+
기획
+
+
+
4
+
이미지
+
+
+
5
+
배포
+
+
+ + +
+

이벤트 목적을 선택하세요

+

AI가 목적에 맞는 이벤트를 기획합니다

+
+ + +
+
+ + +
+
+
+ person_add +
+
+

신규 고객 유치

+

처음 방문하는 고객을 늘립니다

+
+ check_circle +
+
+ + +
+
+
+ replay +
+
+

재방문 유도

+

기존 고객의 재방문을 늘립니다

+
+ check_circle +
+
+ + +
+
+
+ restaurant_menu +
+
+

신메뉴 홍보

+

새로운 메뉴를 알립니다

+
+ check_circle +
+
+ + +
+
+
+ more_horiz +
+
+

기타

+

직접 목적을 입력합니다

+
+ check_circle +
+
+ +
+
+ + +
+ +
+ +
+ + + +
+ + + + + + + + + + diff --git a/design/uiux/prototype/08-이벤트기획안승인.html b/design/uiux/prototype/08-이벤트기획안승인.html new file mode 100644 index 0000000..d942e24 --- /dev/null +++ b/design/uiux/prototype/08-이벤트기획안승인.html @@ -0,0 +1,330 @@ + + + + + + 이벤트 기획안 승인 - KT AI 이벤트 자동 생성 + + + + + + + + + + + + +
+ +

이벤트 기획안

+
+
+ + +
+
+ + +
+
+
1
+
목적
+
+
+
2
+
정보
+
+
+
3
+
기획
+
+
+
4
+
이미지
+
+
+
5
+
배포
+
+
+ + +
+
+ auto_awesome +

AI가 생성한 기획안

+
+

내용을 확인하고 승인해주세요

+
+ + +
+
+
+ + +
+ +

우진이네 여름 특별 할인 이벤트

+
+ +
+ + +
+ +

+ 여름 시즌 신규 고객 유치를 위한 20% 할인 이벤트입니다. 점심 시간대 직장인을 타겟으로 하며, SNS 공유 시 추가 쿠폰을 제공합니다. 시원한 여름 메뉴와 함께 특별한 혜택을 경험하세요! +

+
+ +
+ + +
+ + +
+
+ group +
+
+
타겟 고객
+
20-30대 직장인
+
+
+ + +
+
+ calendar_month +
+
+
이벤트 기간
+
2025.06.01 ~ 2025.06.30 (30일)
+
+
+ + +
+
+ local_offer +
+
+
할인 혜택
+
전 메뉴 20% 할인
+
+
+ + +
+
+ card_giftcard +
+
+
추가 혜택
+
SNS 공유 시 10% 쿠폰 추가 증정
+
+
+ +
+ +
+
+
+ + +
+
+
+ tips_and_updates +
+

AI 제안

+

타겟 고객층에 맞춰 점심 시간대(11:00-14:00)에 집중 홍보하면 더 좋은 효과를 얻을 수 있습니다.

+
+
+
+
+ + +
+
+ + +
+
+ +
+ + + +
+ + + + + + + + diff --git a/design/uiux/prototype/09-AI이미지생성.html b/design/uiux/prototype/09-AI이미지생성.html new file mode 100644 index 0000000..da844dc --- /dev/null +++ b/design/uiux/prototype/09-AI이미지생성.html @@ -0,0 +1,401 @@ + + + + + + AI 이미지 생성 - KT AI 이벤트 자동 생성 + + + + + + + + + + + + + + +
+ +

AI 이미지 생성

+
+
+ + +
+
+ + +
+
+
1
+
목적
+
+
+
2
+
정보
+
+
+
3
+
기획
+
+
+
4
+
이미지
+
+
+
5
+
배포
+
+
+ + +
+
+ image +

AI가 생성한 이미지

+
+

마음에 드는 이미지를 선택하세요 (복수 선택 가능)

+
+ + +
+
+ + +
+
+
+
🔥
+
여름 특별 이벤트
+
20% 할인
+
+
+
+ check +
+
+

스타일: 모던, 밝은 느낌

+
+
+ + +
+
+
+
🍖
+
우진이네 고깃집
+
여름 시즌 특가
+
+
+
+ check +
+
+

스타일: 따뜻한 색감, 식욕 자극

+
+
+ + +
+
+
+
❄️
+
시원한 여름
+
20% OFF
+
+
+
+ check +
+
+

스타일: 시원한 느낌, 여름 테마

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

+ 💡 여러 이미지를 선택하여 다양한 채널에 활용할 수 있습니다. +

+
+
+
+
+ + +
+ +
+ +
+ + + +
+ + + + + + + + diff --git a/design/uiux/prototype/15-다중채널배포설정.html b/design/uiux/prototype/15-다중채널배포설정.html new file mode 100644 index 0000000..4f21f6e --- /dev/null +++ b/design/uiux/prototype/15-다중채널배포설정.html @@ -0,0 +1,413 @@ + + + + + + 다중 채널 배포 설정 - KT AI 이벤트 자동 생성 + + + + + + + + + + + + +
+ +

배포 설정

+
+
+ + +
+
+ + +
+
+
1
+
목적
+
+
+
2
+
정보
+
+
+
3
+
기획
+
+
+
4
+
이미지
+
+
+
5
+
배포
+
+
+ + +
+

배포 채널 선택

+

이벤트를 배포할 채널을 선택하세요 (복수 선택 가능)

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

배포 일정

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

배포 요약

+
+
+ check_circle + SNS, 문자 메시지 (2개 채널) +
+
+ schedule + 즉시 배포 +
+
+ image + 선택된 이미지 1개 +
+
+
+
+
+ + +
+ +
+ +
+ + + +
+ + + + + + + + diff --git a/design/uiux/prototype/18-이벤트참여신청.html b/design/uiux/prototype/18-이벤트참여신청.html new file mode 100644 index 0000000..5d04682 --- /dev/null +++ b/design/uiux/prototype/18-이벤트참여신청.html @@ -0,0 +1,381 @@ + + + + + + 이벤트 참여 신청 - 우진이네 고깃집 + + + + + + + + + + + + + + +
+
+ store + 우진이네 고깃집 +
+
+ + +
+ + +
+
+
🔥
+

여름 특별 할인

+

전 메뉴 20% 할인!

+
+
+ +
+ + +
+

우진이네 여름 특별 할인 이벤트

+

+ 시원한 여름 메뉴와 함께 특별한 혜택을 경험하세요! +

+
+ + +
+

이벤트 혜택

+ +
+ + +
+
+ calendar_today +
+
+
이벤트 기간
+
2025.06.01 ~ 2025.06.30
+
+
+ + +
+
+ local_offer +
+
+
할인 혜택
+
전 메뉴 20% 할인
+
+
+ + +
+
+ card_giftcard +
+
+
추가 혜택
+
SNS 공유 시 10% 쿠폰 추가
+
+
+ +
+
+ + +
+

참여 신청

+ +
+ + +
+ + + 이름을 입력해주세요. +
+ + +
+ + + 전화번호를 입력해주세요. +
+ + +
+ + +
+ + + + +
+
+ + +
+
+
+

유의사항

+
    +
  • 이벤트 참여는 선착순으로 마감됩니다
  • +
  • 중복 신청은 불가능합니다
  • +
  • 신청 후 문자로 쿠폰을 받으실 수 있습니다
  • +
+
+
+
+ + +
+

이 이벤트를 친구에게 공유하세요!

+
+ + +
+
+ +
+
+ + + + + + + + diff --git a/design/uiux/prototype/21-실시간대시보드.html b/design/uiux/prototype/21-실시간대시보드.html new file mode 100644 index 0000000..06f7ee4 --- /dev/null +++ b/design/uiux/prototype/21-실시간대시보드.html @@ -0,0 +1,538 @@ + + + + + + 실시간 대시보드 - KT AI 이벤트 자동 생성 + + + + + + + + + + + + + + +
+
+

실시간 대시보드

+ +
+ + +
+
+ + +
+ + +
+
+ event +
+
12
+
전체 이벤트
+
+ arrow_upward + +2 이번 달 +
+
+ + +
+
+ play_circle +
+
3
+
진행 중
+
+ trending_up + 진행 중 +
+
+ + +
+
+ group +
+
1,248
+
총 참여자
+
+ arrow_upward + +156 오늘 +
+
+ + +
+
+ trending_up +
+
+23%
+
매출 증가율
+
+ arrow_upward + 전월 대비 +
+
+ +
+ + +
+

참여자 추이

+
+
+ show_chart +
참여자 증가 추이 그래프
+
+
+

+ 최근 7일간 일평균 178명의 고객이 이벤트에 참여했습니다. +

+
+ + +
+
+

진행 중인 이벤트

+ +
+ +
+ +
+
+ + +
+
+
+ auto_awesome +
+

AI 인사이트

+

+ 현재 진행 중인 이벤트의 참여율이 예상보다 15% 높습니다. + 추가 할인 혜택을 제공하면 더 큰 효과를 얻을 수 있습니다. +

+ +
+
+
+
+ +
+ + + +
+ + + + + + + + diff --git a/design/uiux/prototype/25-AI개선안제안.html b/design/uiux/prototype/25-AI개선안제안.html new file mode 100644 index 0000000..e784520 --- /dev/null +++ b/design/uiux/prototype/25-AI개선안제안.html @@ -0,0 +1,467 @@ + + + + + + AI 개선안 제안 - KT AI 이벤트 자동 생성 + + + + + + + + + + + + + + +
+ +

AI 개선안 제안

+
+
+ + +
+
+ + +
+
+ auto_awesome +

AI가 분석한 개선안

+
+

+ 현재 진행 중인 이벤트를 분석하여 성과를 높일 수 있는 개선안을 제안합니다. +

+
+ + +
+ +
+ + +
+ +
+ +
+ + + +
+ + + + + + + + diff --git a/design/uiux/prototype/99-마이페이지.html b/design/uiux/prototype/99-마이페이지.html new file mode 100644 index 0000000..acb71d2 --- /dev/null +++ b/design/uiux/prototype/99-마이페이지.html @@ -0,0 +1,646 @@ + + + + + + 마이페이지 - KT AI 이벤트 자동 생성 + + + + + + + + + + + + + + +
+
+

MY

+ +
+ + +
+
+ + +
+
+

정우진

+

우진이네 고깃집

+

woojin@example.com

+ +
+ + + + + + + + + + + + + + + + + +
+ +
+ +
+ + + +
+ + + + + + + + diff --git a/design/uiux/prototype/common.css b/design/uiux/prototype/common.css new file mode 100644 index 0000000..4e147ba --- /dev/null +++ b/design/uiux/prototype/common.css @@ -0,0 +1,1097 @@ +/* + * KT AI 기반 소상공인 이벤트 자동 생성 서비스 + * 공통 스타일시트 + * + * 스타일 가이드 준수: design/uiux/style-guide.md + * Mobile First 디자인 + */ + +/* ============================================ + 1. CSS 변수 시스템 + ============================================ */ +:root { + /* Primary Colors */ + --color-primary: #E31E24; + --color-primary-hover: #C11A1F; + --color-primary-light: #FFE5E6; + + /* Secondary Colors */ + --color-secondary: #0066FF; + --color-secondary-hover: #0052CC; + --color-secondary-light: #E6F0FF; + + /* Grayscale */ + --color-gray-900: #1A1A1A; + --color-gray-800: #424242; + --color-gray-700: #616161; + --color-gray-600: #757575; + --color-gray-500: #9E9E9E; + --color-gray-400: #BDBDBD; + --color-gray-300: #D9D9D9; + --color-gray-200: #E0E0E0; + --color-gray-100: #F5F5F5; + --color-white: #FFFFFF; + + /* Semantic Colors */ + --color-success: #00C853; + --color-success-light: #E8F5E9; + --color-warning: #FFA000; + --color-warning-light: #FFF8E1; + --color-error: #D32F2F; + --color-error-light: #FFEBEE; + --color-info: #0288D1; + --color-info-light: #E1F5FE; + + /* Typography */ + --font-family: 'Pretendard', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', sans-serif; + --font-size-display: 28px; + --font-size-h1: 24px; + --font-size-h2: 20px; + --font-size-h3: 18px; + --font-size-body-l: 16px; + --font-size-body-m: 14px; + --font-size-body-s: 12px; + --font-size-button: 16px; + + --font-weight-regular: 400; + --font-weight-medium: 500; + --font-weight-semibold: 600; + --font-weight-bold: 700; + + --line-height-tight: 1.3; + --line-height-normal: 1.4; + --line-height-relaxed: 1.5; + --line-height-loose: 1.6; + + /* Spacing */ + --space-xs: 4px; + --space-s: 8px; + --space-m: 16px; + --space-l: 24px; + --space-xl: 32px; + --space-2xl: 48px; + + /* Effects */ + --shadow-button: 0 2px 4px rgba(0, 0, 0, 0.1); + --shadow-card: 0 2px 8px rgba(0, 0, 0, 0.08); + --shadow-modal: 0 8px 24px rgba(0, 0, 0, 0.15); + + --radius-small: 4px; + --radius-medium: 8px; + --radius-large: 12px; + --radius-xlarge: 16px; + + --transition-fast: 100ms ease-out; + --transition-normal: 200ms ease-out; + --transition-slow: 300ms ease-out; + + /* Layout */ + --max-width: 1200px; + --bottom-nav-height: 60px; + --top-bar-height: 56px; + + /* Touch Target */ + --touch-target-min: 44px; +} + +/* ============================================ + 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-l); + font-weight: var(--font-weight-regular); + line-height: var(--line-height-relaxed); + color: var(--color-gray-900); + background-color: var(--color-white); + overflow-x: hidden; +} + +a { + color: inherit; + text-decoration: none; +} + +button { + font-family: inherit; + cursor: pointer; + border: none; + background: none; +} + +img { + max-width: 100%; + height: auto; + display: block; +} + +input, +textarea, +select { + font-family: inherit; + font-size: inherit; + border: none; + outline: none; +} + +/* ============================================ + 3. Typography Classes + ============================================ */ +.display { + font-size: var(--font-size-display); + font-weight: var(--font-weight-bold); + line-height: var(--line-height-tight); + letter-spacing: -0.02em; +} + +.h1 { + font-size: var(--font-size-h1); + font-weight: var(--font-weight-bold); + line-height: var(--line-height-tight); + letter-spacing: -0.01em; +} + +.h2 { + font-size: var(--font-size-h2); + font-weight: var(--font-weight-bold); + line-height: var(--line-height-normal); +} + +.h3 { + font-size: var(--font-size-h3); + font-weight: var(--font-weight-semibold); + line-height: var(--line-height-normal); +} + +.body-l { + font-size: var(--font-size-body-l); + font-weight: var(--font-weight-regular); + line-height: var(--line-height-relaxed); +} + +.body-m { + font-size: var(--font-size-body-m); + font-weight: var(--font-weight-regular); + line-height: var(--line-height-relaxed); +} + +.body-s { + font-size: var(--font-size-body-s); + font-weight: var(--font-weight-regular); + line-height: var(--line-height-relaxed); +} + +.button-text { + font-size: var(--font-size-button); + font-weight: var(--font-weight-semibold); + line-height: var(--line-height-relaxed); +} + +/* Text Utilities */ +.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-right { text-align: right; } + +/* ============================================ + 4. Layout System + ============================================ */ +.container { + width: 100%; + max-width: var(--max-width); + margin: 0 auto; + padding: 0 var(--space-m); +} + +/* Flexbox Utilities */ +.flex { display: flex; } +.flex-col { flex-direction: column; } +.flex-row { flex-direction: row; } +.flex-wrap { flex-wrap: wrap; } +.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-around { justify-content: space-around; } +.justify-end { justify-content: flex-end; } +.flex-1 { flex: 1; } +.gap-xs { gap: var(--space-xs); } +.gap-s { gap: var(--space-s); } +.gap-m { gap: var(--space-m); } +.gap-l { gap: var(--space-l); } +.gap-xl { gap: var(--space-xl); } + +/* Grid System - Mobile First (4 columns) */ +.grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: var(--space-m); +} + +.col-1 { grid-column: span 1; } +.col-2 { grid-column: span 2; } +.col-3 { grid-column: span 3; } +.col-4 { grid-column: span 4; } + +/* Spacing Utilities */ +.m-xs { margin: var(--space-xs); } +.m-s { margin: var(--space-s); } +.m-m { margin: var(--space-m); } +.m-l { margin: var(--space-l); } +.m-xl { margin: var(--space-xl); } + +.mt-xs { margin-top: var(--space-xs); } +.mt-s { margin-top: var(--space-s); } +.mt-m { margin-top: var(--space-m); } +.mt-l { margin-top: var(--space-l); } +.mt-xl { margin-top: var(--space-xl); } + +.mb-xs { margin-bottom: var(--space-xs); } +.mb-s { margin-bottom: var(--space-s); } +.mb-m { margin-bottom: var(--space-m); } +.mb-l { margin-bottom: var(--space-l); } +.mb-xl { margin-bottom: var(--space-xl); } + +.ml-xs { margin-left: var(--space-xs); } +.ml-s { margin-left: var(--space-s); } +.ml-m { margin-left: var(--space-m); } + +.mr-xs { margin-right: var(--space-xs); } +.mr-s { margin-right: var(--space-s); } +.mr-m { margin-right: var(--space-m); } + +.p-xs { padding: var(--space-xs); } +.p-s { padding: var(--space-s); } +.p-m { padding: var(--space-m); } +.p-l { padding: var(--space-l); } +.p-xl { padding: var(--space-xl); } + +.pt-xs { padding-top: var(--space-xs); } +.pt-s { padding-top: var(--space-s); } +.pt-m { padding-top: var(--space-m); } +.pt-l { padding-top: var(--space-l); } + +.pb-xs { padding-bottom: var(--space-xs); } +.pb-s { padding-bottom: var(--space-s); } +.pb-m { padding-bottom: var(--space-m); } +.pb-l { padding-bottom: var(--space-l); } + +.px-m { padding-left: var(--space-m); padding-right: var(--space-m); } +.py-m { padding-top: var(--space-m); padding-bottom: var(--space-m); } + +/* ============================================ + 5. Component Styles + ============================================ */ + +/* Buttons */ +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + min-height: var(--touch-target-min); + padding: 12px var(--space-l); + font-size: var(--font-size-button); + font-weight: var(--font-weight-semibold); + line-height: var(--line-height-relaxed); + border-radius: var(--radius-medium); + transition: all var(--transition-normal); + cursor: pointer; + text-align: center; + white-space: nowrap; + user-select: none; +} + +.btn:focus-visible { + outline: 2px solid var(--color-secondary); + outline-offset: 2px; +} + +.btn-primary { + background-color: var(--color-primary); + color: var(--color-white); + box-shadow: var(--shadow-button); +} + +.btn-primary:hover { + background-color: var(--color-primary-hover); +} + +.btn-primary:active { + transform: translateY(1px); + box-shadow: none; +} + +.btn-primary:disabled { + background-color: var(--color-gray-300); + color: var(--color-gray-600); + cursor: not-allowed; + box-shadow: none; +} + +.btn-secondary { + background-color: var(--color-white); + color: var(--color-primary); + border: 2px solid var(--color-primary); +} + +.btn-secondary:hover { + background-color: var(--color-primary-light); +} + +.btn-secondary:active { + transform: translateY(1px); +} + +.btn-text { + background-color: transparent; + color: var(--color-gray-700); + padding: var(--space-s) var(--space-m); + min-height: auto; +} + +.btn-text:hover { + color: var(--color-primary); + background-color: var(--color-gray-100); +} + +.btn-full { + width: 100%; +} + +.btn-icon { + display: inline-flex; + align-items: center; + gap: var(--space-s); +} + +/* Cards */ +.card { + background-color: var(--color-white); + border: 1px solid var(--color-gray-200); + border-radius: var(--radius-large); + box-shadow: var(--shadow-card); + overflow: hidden; + transition: box-shadow var(--transition-normal); +} + +.card:hover { + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12); +} + +.card-header { + padding: var(--space-m); + border-bottom: 1px solid var(--color-gray-200); + font-weight: var(--font-weight-semibold); +} + +.card-body { + padding: var(--space-m); +} + +.card-footer { + padding: var(--space-m); + border-top: 1px solid var(--color-gray-200); + background-color: var(--color-gray-100); +} + +.card-clickable { + cursor: pointer; +} + +.card-clickable:active { + transform: scale(0.98); +} + +/* Input Fields */ +.input-group { + display: flex; + flex-direction: column; + gap: var(--space-s); + margin-bottom: var(--space-m); +} + +.input-group label { + font-size: var(--font-size-body-m); + font-weight: var(--font-weight-medium); + color: var(--color-gray-900); +} + +.input { + width: 100%; + min-height: 48px; + padding: 12px var(--space-m); + font-size: var(--font-size-body-l); + color: var(--color-gray-900); + background-color: var(--color-white); + border: 1px solid var(--color-gray-300); + border-radius: var(--radius-medium); + transition: all var(--transition-normal); +} + +.input::placeholder { + color: var(--color-gray-500); +} + +.input:focus { + border-color: var(--color-secondary); + box-shadow: 0 0 0 3px var(--color-secondary-light); +} + +.input:disabled { + background-color: var(--color-gray-100); + color: var(--color-gray-600); + cursor: not-allowed; +} + +.input-error { + color: var(--color-error); + font-size: var(--font-size-body-s); + margin-top: var(--space-xs); + display: none; +} + +.input-group.error .input { + border-color: var(--color-error); +} + +.input-group.error .input-error { + display: block; +} + +.textarea { + min-height: 120px; + resize: vertical; + font-family: inherit; +} + +/* Checkbox & Radio */ +.checkbox, +.radio { + display: flex; + align-items: center; + gap: var(--space-s); + cursor: pointer; + min-height: var(--touch-target-min); +} + +.checkbox input, +.radio input { + width: 20px; + height: 20px; + cursor: pointer; +} + +/* Select */ +.select { + appearance: none; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Cpath fill='%23616161' d='M7 10l5 5 5-5z'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right var(--space-m) center; + padding-right: 48px; +} + +/* Bottom Navigation */ +.bottom-nav { + position: fixed; + bottom: 0; + left: 0; + right: 0; + height: var(--bottom-nav-height); + background-color: var(--color-white); + border-top: 1px solid var(--color-gray-200); + display: flex; + justify-content: space-around; + align-items: center; + z-index: 1000; + box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.08); +} + +.nav-item { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 2px; + min-width: 60px; + min-height: var(--touch-target-min); + color: var(--color-gray-600); + transition: color var(--transition-fast); + cursor: pointer; +} + +.nav-item .material-icons { + font-size: 24px; +} + +.nav-item span:not(.material-icons) { + font-size: 11px; + font-weight: var(--font-weight-medium); +} + +.nav-item:hover { + color: var(--color-gray-900); +} + +.nav-item.active { + color: var(--color-primary); +} + +.nav-item:focus-visible { + outline: 2px solid var(--color-secondary); + outline-offset: -2px; +} + +/* Top Bar */ +.top-bar { + position: sticky; + top: 0; + left: 0; + right: 0; + height: var(--top-bar-height); + background-color: var(--color-white); + border-bottom: 1px solid var(--color-gray-200); + display: flex; + align-items: center; + padding: 0 var(--space-m); + z-index: 999; +} + +.top-bar-title { + flex: 1; + text-align: center; + font-size: var(--font-size-h3); + font-weight: var(--font-weight-semibold); +} + +.back-button { + width: var(--touch-target-min); + height: var(--touch-target-min); + display: flex; + align-items: center; + justify-content: center; + color: var(--color-gray-900); + cursor: pointer; +} + +/* Page Container with Bottom Nav */ +.page { + min-height: 100vh; + padding-bottom: calc(var(--bottom-nav-height) + var(--space-m)); +} + +.page-content { + padding: var(--space-m); +} + +/* Stepper */ +.stepper { + display: flex; + align-items: center; + justify-content: space-between; + padding: var(--space-m) 0; +} + +.step { + display: flex; + flex-direction: column; + align-items: center; + flex: 1; + position: relative; +} + +.step-number { + width: 32px; + height: 32px; + border-radius: 50%; + background-color: var(--color-gray-300); + color: var(--color-gray-600); + display: flex; + align-items: center; + justify-content: center; + font-weight: var(--font-weight-semibold); + font-size: var(--font-size-body-m); + margin-bottom: var(--space-xs); + z-index: 1; +} + +.step.active .step-number { + background-color: var(--color-primary); + color: var(--color-white); +} + +.step.completed .step-number { + background-color: var(--color-success); + color: var(--color-white); +} + +.step-label { + font-size: var(--font-size-body-s); + color: var(--color-gray-600); + text-align: center; +} + +.step.active .step-label { + color: var(--color-gray-900); + font-weight: var(--font-weight-semibold); +} + +.step:not(:last-child)::after { + content: ''; + position: absolute; + top: 16px; + left: 50%; + width: 100%; + height: 2px; + background-color: var(--color-gray-300); + z-index: 0; +} + +.step.completed:not(:last-child)::after { + background-color: var(--color-success); +} + +/* AI Loading Components */ +.ai-loading { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: var(--space-2xl); + text-align: center; +} + +.spinner { + width: 48px; + height: 48px; + border: 4px solid var(--color-gray-200); + border-top-color: var(--color-secondary); + border-radius: 50%; + animation: spin 1s linear infinite; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +.ai-loading p { + margin-top: var(--space-m); + color: var(--color-gray-700); +} + +.progress-bar { + width: 100%; + height: 8px; + background-color: var(--color-gray-200); + border-radius: 4px; + overflow: hidden; + margin-top: var(--space-m); +} + +.progress-fill { + height: 100%; + background-color: var(--color-secondary); + transition: width var(--transition-slow); +} + +/* Skeleton Loading */ +.skeleton { + background: linear-gradient( + 90deg, + var(--color-gray-200) 25%, + var(--color-gray-100) 50%, + var(--color-gray-200) 75% + ); + background-size: 200% 100%; + animation: skeleton-loading 1.5s ease-in-out infinite; + border-radius: var(--radius-small); +} + +@keyframes skeleton-loading { + 0% { background-position: 200% 0; } + 100% { background-position: -200% 0; } +} + +.skeleton-text { + height: 16px; + margin-bottom: var(--space-s); +} + +.skeleton-title { + height: 24px; + width: 60%; + margin-bottom: var(--space-m); +} + +.skeleton-card { + height: 120px; +} + +/* Toast */ +.toast { + position: fixed; + bottom: calc(var(--bottom-nav-height) + var(--space-m)); + left: 50%; + transform: translateX(-50%) translateY(100px); + min-width: 280px; + max-width: 90%; + padding: var(--space-m) var(--space-l); + background-color: var(--color-gray-900); + color: var(--color-white); + border-radius: var(--radius-medium); + box-shadow: var(--shadow-modal); + z-index: 2000; + opacity: 0; + transition: all var(--transition-slow); +} + +.toast.show { + opacity: 1; + transform: translateX(-50%) translateY(0); +} + +.toast.success { + background-color: var(--color-success); +} + +.toast.error { + background-color: var(--color-error); +} + +.toast.warning { + background-color: var(--color-warning); +} + +/* Modal */ +.modal-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + z-index: 1500; + opacity: 0; + visibility: hidden; + transition: all var(--transition-slow); +} + +.modal-overlay.show { + opacity: 1; + visibility: visible; +} + +.modal { + background-color: var(--color-white); + border-radius: var(--radius-large); + box-shadow: var(--shadow-modal); + max-width: 90%; + width: 400px; + max-height: 80vh; + overflow: hidden; + transform: scale(0.9); + transition: transform var(--transition-slow); +} + +.modal-overlay.show .modal { + transform: scale(1); +} + +.modal-header { + padding: var(--space-l); + border-bottom: 1px solid var(--color-gray-200); +} + +.modal-title { + font-size: var(--font-size-h2); + font-weight: var(--font-weight-bold); +} + +.modal-body { + padding: var(--space-l); + overflow-y: auto; +} + +.modal-footer { + padding: var(--space-l); + border-top: 1px solid var(--color-gray-200); + display: flex; + gap: var(--space-m); + justify-content: flex-end; +} + +/* Bottom Sheet */ +.bottom-sheet-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.5); + z-index: 1500; + opacity: 0; + visibility: hidden; + transition: all var(--transition-slow); +} + +.bottom-sheet-overlay.show { + opacity: 1; + visibility: visible; +} + +.bottom-sheet { + position: fixed; + bottom: 0; + left: 0; + right: 0; + background-color: var(--color-white); + border-radius: var(--radius-xlarge) var(--radius-xlarge) 0 0; + box-shadow: var(--shadow-modal); + max-height: 80vh; + transform: translateY(100%); + transition: transform var(--transition-slow); + z-index: 1501; +} + +.bottom-sheet-overlay.show .bottom-sheet { + transform: translateY(0); +} + +.bottom-sheet-handle { + width: 40px; + height: 4px; + background-color: var(--color-gray-300); + border-radius: 2px; + margin: var(--space-m) auto; +} + +.bottom-sheet-content { + padding: 0 var(--space-l) var(--space-l); + overflow-y: auto; + max-height: calc(80vh - 60px); +} + +/* Badge */ +.badge { + display: inline-flex; + align-items: center; + padding: 4px var(--space-s); + font-size: var(--font-size-body-s); + font-weight: var(--font-weight-medium); + border-radius: 12px; + line-height: 1; +} + +.badge-primary { + background-color: var(--color-primary-light); + color: var(--color-primary); +} + +.badge-success { + background-color: var(--color-success-light); + color: var(--color-success); +} + +.badge-warning { + background-color: var(--color-warning-light); + color: var(--color-warning); +} + +.badge-error { + background-color: var(--color-error-light); + color: var(--color-error); +} + +.badge-gray { + background-color: var(--color-gray-200); + color: var(--color-gray-700); +} + +/* Divider */ +.divider { + height: 1px; + background-color: var(--color-gray-200); + margin: var(--space-m) 0; +} + +/* ============================================ + 6. Utility Classes + ============================================ */ + +/* Colors */ +.text-primary { color: var(--color-primary); } +.text-secondary { color: var(--color-secondary); } +.text-success { color: var(--color-success); } +.text-warning { color: var(--color-warning); } +.text-error { color: var(--color-error); } +.text-gray-900 { color: var(--color-gray-900); } +.text-gray-700 { color: var(--color-gray-700); } +.text-gray-600 { color: var(--color-gray-600); } +.text-gray-500 { color: var(--color-gray-500); } + +.bg-white { background-color: var(--color-white); } +.bg-gray-100 { background-color: var(--color-gray-100); } +.bg-gray-200 { background-color: var(--color-gray-200); } +.bg-primary-light { background-color: var(--color-primary-light); } +.bg-secondary-light { background-color: var(--color-secondary-light); } + +/* Display */ +.block { display: block; } +.inline-block { display: inline-block; } +.hidden { display: none; } + +/* Position */ +.relative { position: relative; } +.absolute { position: absolute; } +.fixed { position: fixed; } +.sticky { position: sticky; } + +/* Width */ +.w-full { width: 100%; } +.w-auto { width: auto; } + +/* Border Radius */ +.rounded-small { border-radius: var(--radius-small); } +.rounded-medium { border-radius: var(--radius-medium); } +.rounded-large { border-radius: var(--radius-large); } +.rounded-full { border-radius: 9999px; } + +/* Shadow */ +.shadow-card { box-shadow: var(--shadow-card); } +.shadow-button { box-shadow: var(--shadow-button); } +.shadow-modal { box-shadow: var(--shadow-modal); } + +/* ============================================ + 7. Responsive - Tablet (768px+) + ============================================ */ +@media (min-width: 768px) { + :root { + --font-size-display: 32px; + --font-size-h1: 28px; + --font-size-h2: 24px; + --font-size-h3: 20px; + --font-size-body-l: 18px; + } + + .container { + padding: 0 var(--space-l); + } + + /* Grid: 8 columns for tablet */ + .grid { + grid-template-columns: repeat(8, 1fr); + } + + .tablet-col-2 { grid-column: span 2; } + .tablet-col-4 { grid-column: span 4; } + .tablet-col-6 { grid-column: span 6; } + .tablet-col-8 { grid-column: span 8; } + + .page-content { + padding: var(--space-l); + } + + /* Bottom Nav can be hidden on tablet, use sidebar instead */ + .tablet-sidebar-layout { + display: grid; + grid-template-columns: 240px 1fr; + } +} + +/* ============================================ + 8. Responsive - Desktop (1024px+) + ============================================ */ +@media (min-width: 1024px) { + :root { + --font-size-display: 36px; + --font-size-h1: 32px; + --font-size-h2: 28px; + --font-size-h3: 24px; + --font-size-body-l: 20px; + } + + /* Grid: 12 columns for desktop */ + .grid { + grid-template-columns: repeat(12, 1fr); + } + + .desktop-col-3 { grid-column: span 3; } + .desktop-col-4 { grid-column: span 4; } + .desktop-col-6 { grid-column: span 6; } + .desktop-col-8 { grid-column: span 8; } + .desktop-col-12 { grid-column: span 12; } + + .page-content { + padding: var(--space-xl); + } + + /* Hide bottom nav on desktop */ + .desktop-hide-bottom-nav .bottom-nav { + display: none; + } + + .desktop-hide-bottom-nav .page { + padding-bottom: var(--space-m); + } +} + +/* ============================================ + 9. Accessibility + ============================================ */ + +/* Focus styles */ +*:focus-visible { + outline: 2px solid var(--color-secondary); + outline-offset: 2px; +} + +/* Screen reader only */ +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border-width: 0; +} + +/* Reduced motion */ +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + } +} + +/* High contrast mode */ +@media (prefers-contrast: high) { + .btn-primary { + border: 2px solid var(--color-gray-900); + } + + .card { + border: 2px solid var(--color-gray-900); + } +} diff --git a/design/uiux/prototype/common.js b/design/uiux/prototype/common.js new file mode 100644 index 0000000..b7e50ac --- /dev/null +++ b/design/uiux/prototype/common.js @@ -0,0 +1,774 @@ +/** + * KT AI 기반 소상공인 이벤트 자동 생성 서비스 + * 공통 JavaScript + * + * 기능: + * - 네비게이션 관리 + * - 상태 관리 (localStorage) + * - UI 유틸리티 (Toast, Modal, Bottom Sheet) + * - 폼 검증 + * - 애니메이션 헬퍼 + */ + +// ============================================ +// 1. Navigation Manager +// ============================================ +const NavManager = { + /** + * 화면 이동 + * @param {string} pageNumber - 화면 번호 (00, 01, 03 등) + * @param {string} pageName - 화면명 (한글) + */ + navigate: (pageNumber, pageName) => { + window.location.href = `${pageNumber}-${pageName}.html`; + }, + + /** + * 뒤로가기 + */ + back: () => { + window.history.back(); + }, + + /** + * Bottom Navigation 초기화 + * @param {string} activeMenu - 활성 메뉴 ('home' | 'event' | 'analytics' | 'my') + */ + initBottomNav: (activeMenu = 'home') => { + const navItems = document.querySelectorAll('.nav-item'); + + navItems.forEach(item => { + const menu = item.getAttribute('data-nav'); + + // 활성 메뉴 표시 + if (menu === activeMenu) { + item.classList.add('active'); + } + + // 클릭 이벤트 + item.addEventListener('click', (e) => { + e.preventDefault(); + + switch (menu) { + case 'home': + NavManager.navigate('00', '메인대시보드'); + break; + case 'event': + NavManager.navigate('03', '이벤트목적선택'); + break; + case 'analytics': + NavManager.navigate('21', '실시간대시보드'); + break; + case 'my': + NavManager.navigate('99', '마이페이지'); + break; + } + }); + }); + } +}; + +// ============================================ +// 2. State Management +// ============================================ +const AppState = { + /** + * 데이터 저장 + * @param {string} key - 저장 키 + * @param {*} data - 저장할 데이터 + */ + save: (key, data) => { + try { + localStorage.setItem(key, JSON.stringify(data)); + return true; + } catch (error) { + console.error('Failed to save data:', error); + return false; + } + }, + + /** + * 데이터 로드 + * @param {string} key - 로드 키 + * @returns {*} 저장된 데이터 또는 null + */ + load: (key) => { + try { + const data = localStorage.getItem(key); + return data ? JSON.parse(data) : null; + } catch (error) { + console.error('Failed to load data:', error); + return null; + } + }, + + /** + * 데이터 삭제 + * @param {string} key - 삭제 키 + */ + remove: (key) => { + localStorage.removeItem(key); + }, + + /** + * 전체 데이터 삭제 + */ + clear: () => { + localStorage.clear(); + }, + + /** + * 사용자 정보 저장 + */ + saveUser: (userData) => { + AppState.save('user', userData); + }, + + /** + * 사용자 정보 로드 + */ + loadUser: () => { + return AppState.load('user'); + }, + + /** + * 이벤트 데이터 저장 + */ + saveEvent: (eventData) => { + const events = AppState.load('events') || []; + events.push(eventData); + AppState.save('events', events); + }, + + /** + * 이벤트 목록 로드 + */ + loadEvents: () => { + return AppState.load('events') || []; + } +}; + +// ============================================ +// 3. UI Manager +// ============================================ +const UIManager = { + /** + * Toast 메시지 표시 + * @param {string} message - 메시지 내용 + * @param {string} type - 타입 ('success' | 'error' | 'warning' | 'info') + * @param {number} duration - 표시 시간 (ms, 기본 3000) + */ + showToast: (message, type = 'info', duration = 3000) => { + // 기존 Toast 제거 + const existingToast = document.querySelector('.toast'); + if (existingToast) { + existingToast.remove(); + } + + // Toast 생성 + const toast = document.createElement('div'); + toast.className = `toast ${type}`; + toast.textContent = message; + toast.setAttribute('role', 'alert'); + toast.setAttribute('aria-live', 'polite'); + + document.body.appendChild(toast); + + // 애니메이션 + setTimeout(() => { + toast.classList.add('show'); + }, 10); + + // 자동 제거 + setTimeout(() => { + toast.classList.remove('show'); + setTimeout(() => { + toast.remove(); + }, 300); + }, duration); + }, + + /** + * Modal 표시 + * @param {Object} options - Modal 옵션 + * @param {string} options.title - 제목 + * @param {string} options.body - 내용 + * @param {string} options.confirmText - 확인 버튼 텍스트 + * @param {string} options.cancelText - 취소 버튼 텍스트 + * @param {Function} options.onConfirm - 확인 콜백 + * @param {Function} options.onCancel - 취소 콜백 + */ + showModal: (options) => { + const { + title = '알림', + body = '', + confirmText = '확인', + cancelText = '취소', + onConfirm = null, + onCancel = null + } = options; + + // Modal HTML 생성 + const modalHTML = ` + + `; + + // DOM에 추가 + const modalContainer = document.createElement('div'); + modalContainer.innerHTML = modalHTML; + document.body.appendChild(modalContainer.firstElementChild); + + const modalElement = document.getElementById('modal'); + + // 표시 애니메이션 + setTimeout(() => { + modalElement.classList.add('show'); + }, 10); + + // 확인 버튼 + const confirmBtn = document.getElementById('modal-confirm'); + confirmBtn.addEventListener('click', () => { + if (onConfirm) onConfirm(); + UIManager.closeModal(); + }); + + // 취소 버튼 + const cancelBtn = document.getElementById('modal-cancel'); + if (cancelBtn) { + cancelBtn.addEventListener('click', () => { + if (onCancel) onCancel(); + UIManager.closeModal(); + }); + } + + // 배경 클릭으로 닫기 + modalElement.addEventListener('click', (e) => { + if (e.target === modalElement) { + if (onCancel) onCancel(); + UIManager.closeModal(); + } + }); + + // ESC 키로 닫기 + const handleEscape = (e) => { + if (e.key === 'Escape') { + if (onCancel) onCancel(); + UIManager.closeModal(); + document.removeEventListener('keydown', handleEscape); + } + }; + document.addEventListener('keydown', handleEscape); + }, + + /** + * Modal 닫기 + */ + closeModal: () => { + const modal = document.getElementById('modal'); + if (modal) { + modal.classList.remove('show'); + setTimeout(() => { + modal.remove(); + }, 300); + } + }, + + /** + * Bottom Sheet 표시 + * @param {Object} options - Bottom Sheet 옵션 + * @param {string} options.content - HTML 내용 + * @param {Function} options.onClose - 닫기 콜백 + */ + showBottomSheet: (options) => { + const { content = '', onClose = null } = options; + + // Bottom Sheet HTML 생성 + const sheetHTML = ` +
+
+
+
+ ${content} +
+
+
+ `; + + // DOM에 추가 + const sheetContainer = document.createElement('div'); + sheetContainer.innerHTML = sheetHTML; + document.body.appendChild(sheetContainer.firstElementChild); + + const sheetElement = document.getElementById('bottom-sheet'); + + // 표시 애니메이션 + setTimeout(() => { + sheetElement.classList.add('show'); + }, 10); + + // 배경 클릭으로 닫기 + sheetElement.addEventListener('click', (e) => { + if (e.target === sheetElement) { + if (onClose) onClose(); + UIManager.closeBottomSheet(); + } + }); + }, + + /** + * Bottom Sheet 닫기 + */ + closeBottomSheet: () => { + const sheet = document.getElementById('bottom-sheet'); + if (sheet) { + sheet.classList.remove('show'); + setTimeout(() => { + sheet.remove(); + }, 300); + } + }, + + /** + * Loading 표시 + * @param {string} message - 로딩 메시지 + */ + showLoading: (message = 'AI가 처리하고 있습니다...') => { + const loadingHTML = ` + + `; + + const loadingContainer = document.createElement('div'); + loadingContainer.innerHTML = loadingHTML; + document.body.appendChild(loadingContainer.firstElementChild); + }, + + /** + * Loading 숨기기 + */ + hideLoading: () => { + const loading = document.getElementById('loading'); + if (loading) { + loading.remove(); + } + }, + + /** + * Progress Bar 업데이트 + * @param {number} percent - 진행률 (0-100) + */ + updateProgress: (percent) => { + const progressFill = document.querySelector('.progress-fill'); + if (progressFill) { + progressFill.style.width = `${percent}%`; + } + } +}; + +// ============================================ +// 4. Form Validator +// ============================================ +const FormValidator = { + /** + * 이메일 검증 + * @param {string} email - 이메일 주소 + * @returns {boolean} + */ + validateEmail: (email) => { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return emailRegex.test(email); + }, + + /** + * 비밀번호 검증 (8자 이상, 영문+숫자 조합) + * @param {string} password - 비밀번호 + * @returns {boolean} + */ + validatePassword: (password) => { + const passwordRegex = /^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,}$/; + return passwordRegex.test(password); + }, + + /** + * 전화번호 검증 (한국 형식) + * @param {string} phone - 전화번호 + * @returns {boolean} + */ + validatePhone: (phone) => { + const phoneRegex = /^01[0-9]-?[0-9]{3,4}-?[0-9]{4}$/; + return phoneRegex.test(phone); + }, + + /** + * 필수 입력 검증 + * @param {string} value - 입력값 + * @returns {boolean} + */ + validateRequired: (value) => { + return value && value.trim().length > 0; + }, + + /** + * Input 요소에 에러 표시 + * @param {HTMLElement} inputElement - Input 요소 + * @param {string} errorMessage - 에러 메시지 + */ + showError: (inputElement, errorMessage) => { + const inputGroup = inputElement.closest('.input-group'); + if (inputGroup) { + inputGroup.classList.add('error'); + const errorElement = inputGroup.querySelector('.input-error'); + if (errorElement) { + errorElement.textContent = errorMessage; + } + } + }, + + /** + * Input 요소의 에러 제거 + * @param {HTMLElement} inputElement - Input 요소 + */ + clearError: (inputElement) => { + const inputGroup = inputElement.closest('.input-group'); + if (inputGroup) { + inputGroup.classList.remove('error'); + } + }, + + /** + * 폼 전체 검증 + * @param {HTMLFormElement} formElement - Form 요소 + * @returns {boolean} + */ + validateForm: (formElement) => { + let isValid = true; + const inputs = formElement.querySelectorAll('input[required], textarea[required]'); + + inputs.forEach(input => { + FormValidator.clearError(input); + + // 필수 입력 체크 + if (!FormValidator.validateRequired(input.value)) { + FormValidator.showError(input, '필수 입력 항목입니다.'); + isValid = false; + return; + } + + // 타입별 검증 + if (input.type === 'email' && !FormValidator.validateEmail(input.value)) { + FormValidator.showError(input, '올바른 이메일 형식이 아닙니다.'); + isValid = false; + } else if (input.type === 'password' && !FormValidator.validatePassword(input.value)) { + FormValidator.showError(input, '비밀번호는 8자 이상, 영문과 숫자 조합이어야 합니다.'); + isValid = false; + } else if (input.type === 'tel' && !FormValidator.validatePhone(input.value)) { + FormValidator.showError(input, '올바른 전화번호 형식이 아닙니다.'); + isValid = false; + } + }); + + return isValid; + } +}; + +// ============================================ +// 5. Animation Helpers +// ============================================ +const AnimationHelper = { + /** + * Fade In + * @param {HTMLElement} element - 대상 요소 + * @param {number} duration - 지속 시간 (ms) + */ + fadeIn: (element, duration = 300) => { + element.style.opacity = '0'; + element.style.display = 'block'; + + let opacity = 0; + const increment = 50 / duration; + + const timer = setInterval(() => { + opacity += increment; + element.style.opacity = opacity; + + if (opacity >= 1) { + clearInterval(timer); + element.style.opacity = '1'; + } + }, 50); + }, + + /** + * Fade Out + * @param {HTMLElement} element - 대상 요소 + * @param {number} duration - 지속 시간 (ms) + */ + fadeOut: (element, duration = 300) => { + let opacity = 1; + const decrement = 50 / duration; + + const timer = setInterval(() => { + opacity -= decrement; + element.style.opacity = opacity; + + if (opacity <= 0) { + clearInterval(timer); + element.style.opacity = '0'; + element.style.display = 'none'; + } + }, 50); + }, + + /** + * Slide Down + * @param {HTMLElement} element - 대상 요소 + * @param {number} duration - 지속 시간 (ms) + */ + slideDown: (element, duration = 300) => { + element.style.height = '0'; + element.style.overflow = 'hidden'; + element.style.display = 'block'; + + const targetHeight = element.scrollHeight; + let height = 0; + const increment = (targetHeight * 50) / duration; + + const timer = setInterval(() => { + height += increment; + element.style.height = `${height}px`; + + if (height >= targetHeight) { + clearInterval(timer); + element.style.height = 'auto'; + element.style.overflow = 'visible'; + } + }, 50); + } +}; + +// ============================================ +// 6. Utility Functions +// ============================================ +const Utils = { + /** + * 날짜 포맷 (YYYY.MM.DD) + * @param {Date} date - 날짜 객체 + * @returns {string} + */ + formatDate: (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) + * @param {Date} date - 날짜 객체 + * @returns {string} + */ + formatDateISO: (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}`; + }, + + /** + * 숫자 포맷 (천 단위 콤마) + * @param {number} number - 숫자 + * @returns {string} + */ + formatNumber: (number) => { + return number.toLocaleString('ko-KR'); + }, + + /** + * 퍼센트 포맷 + * @param {number} value - 값 + * @param {number} decimals - 소수점 자릿수 + * @returns {string} + */ + formatPercent: (value, decimals = 1) => { + return `${value.toFixed(decimals)}%`; + }, + + /** + * 디바운스 (연속 호출 방지) + * @param {Function} func - 실행할 함수 + * @param {number} wait - 대기 시간 (ms) + * @returns {Function} + */ + debounce: (func, wait = 300) => { + let timeout; + return function executedFunction(...args) { + const later = () => { + clearTimeout(timeout); + func(...args); + }; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + }; + }, + + /** + * 쓰로틀 (일정 시간마다 한 번만 실행) + * @param {Function} func - 실행할 함수 + * @param {number} limit - 제한 시간 (ms) + * @returns {Function} + */ + throttle: (func, limit = 300) => { + let inThrottle; + return function executedFunction(...args) { + if (!inThrottle) { + func(...args); + inThrottle = true; + setTimeout(() => inThrottle = false, limit); + } + }; + }, + + /** + * 랜덤 ID 생성 + * @returns {string} + */ + generateId: () => { + return `id-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; + }, + + /** + * 요소가 뷰포트에 보이는지 확인 + * @param {HTMLElement} element - 대상 요소 + * @returns {boolean} + */ + isInViewport: (element) => { + const rect = element.getBoundingClientRect(); + return ( + rect.top >= 0 && + rect.left >= 0 && + rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && + rect.right <= (window.innerWidth || document.documentElement.clientWidth) + ); + } +}; + +// ============================================ +// 7. 예제 데이터 +// ============================================ +const SampleData = { + // 사용자 정보 + user: { + name: '정우진', + businessName: '우진이네 고깃집', + businessType: '한식당', + location: '서울 강남구', + email: 'woojin@example.com', + phone: '010-1234-5678' + }, + + // 이벤트 예제 + event: { + id: 'evt-001', + name: '우진이네 여름 특별 할인 이벤트', + purpose: '신규 고객 유치', + startDate: '2025.06.01', + endDate: '2025.06.30', + discountRate: 20, + target: '20-30대 직장인', + status: 'in_progress' + }, + + // 이벤트 목록 + events: [ + { + id: 'evt-001', + name: '우진이네 여름 특별 할인 이벤트', + status: 'in_progress', + startDate: '2025.06.01', + participants: 127 + }, + { + id: 'evt-002', + name: '신메뉴 출시 기념 이벤트', + status: 'in_progress', + startDate: '2025.05.15', + participants: 89 + }, + { + id: 'evt-003', + name: '재방문 고객 감사 이벤트', + status: 'completed', + startDate: '2025.04.01', + participants: 254 + } + ], + + // 통계 데이터 + analytics: { + totalEvents: 5, + activeEvents: 2, + completedEvents: 3, + totalParticipants: 127, + salesIncrease: 23, + conversionRate: 15.4 + }, + + // AI 생성 콘텐츠 + aiContent: { + plan: '여름 시즌 신규 고객 유치를 위한 20% 할인 이벤트입니다. 점심 시간대 직장인을 타겟으로 하며, SNS 공유 시 추가 쿠폰을 제공합니다.', + images: [ + { id: 1, url: 'https://via.placeholder.com/400x300?text=여름+특별+이미지1', selected: true }, + { id: 2, url: 'https://via.placeholder.com/400x300?text=여름+특별+이미지2', selected: false }, + { id: 3, url: 'https://via.placeholder.com/400x300?text=여름+특별+이미지3', selected: false } + ], + improvement: '참여율 향상을 위해 쿠폰 사용 기한을 1주일에서 2주일로 연장하고, SNS 공유 혜택을 10%에서 15%로 상향 조정하는 것을 제안합니다.' + } +}; + +// ============================================ +// 8. 초기화 +// ============================================ +document.addEventListener('DOMContentLoaded', () => { + // Back 버튼 초기화 + const backButtons = document.querySelectorAll('.back-button'); + backButtons.forEach(btn => { + btn.addEventListener('click', () => { + NavManager.back(); + }); + }); + + // 예제 데이터 로드 (개발용) + if (!AppState.loadUser()) { + AppState.saveUser(SampleData.user); + } +}); + +// Export for use in other files +if (typeof module !== 'undefined' && module.exports) { + module.exports = { + NavManager, + AppState, + UIManager, + FormValidator, + AnimationHelper, + Utils, + SampleData + }; +}