15-19번 화면 CSS 변수 추가 및 스타일 수정

- 15-배포채널선택.html: CSS 변수 및 기본 스타일 추가
- 16-배포진행상태.html: CSS 변수 및 기본 스타일 추가
- 17-오프라인자료다운로드.html: CSS 변수 및 기본 스타일 추가
- 18-이벤트참여.html: CSS 변수 및 기본 스타일 추가
- 19-참여완료.html: CSS 변수 및 기본 스타일 추가

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
cherry2250 2025-10-21 09:38:44 +09:00
parent c855bddc0a
commit f1ad9a79ea
85 changed files with 36184 additions and 7364 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

View File

@ -6,241 +6,409 @@
<meta name="description" content="KT AI 기반 소상공인 이벤트 자동 생성 서비스 - 로그인">
<title>로그인 - KT 이벤트 마케팅</title>
<!-- Styles -->
<link rel="stylesheet" href="css/common.css">
<!-- Pretendard Font -->
<link rel="stylesheet" as="style" crossorigin
href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/variable/pretendardvariable.min.css">
<!-- Fonts -->
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/static/pretendard.min.css">
<!-- Stylesheets -->
<link rel="stylesheet" href="css/variables.css">
<link rel="stylesheet" href="css/reset.css">
<link rel="stylesheet" href="css/layout.css">
<link rel="stylesheet" href="css/common.css">
<link rel="stylesheet" href="css/components/buttons.css">
<link rel="stylesheet" href="css/components/inputs.css">
<link rel="stylesheet" href="css/components/loaders.css">
<style>
/* Login Page Specific Styles */
.login-page {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #FFF5F5 0%, #FFFFFF 100%);
padding: var(--spacing-l);
}
.login-container {
width: 100%;
max-width: 400px;
background-color: var(--color-white);
border-radius: var(--radius-l);
box-shadow: var(--shadow-lg);
padding: var(--spacing-2xl) var(--spacing-l);
animation: fadeIn var(--transition-normal) var(--ease-out);
}
.login-header {
text-align: center;
margin-bottom: var(--spacing-2xl);
}
.login-logo {
width: 120px;
height: 120px;
margin: 0 auto var(--spacing-l);
background: var(--gradient-primary);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 48px;
}
.login-title {
font-size: var(--font-size-h1);
font-weight: var(--font-weight-bold);
color: var(--color-black);
margin-bottom: var(--spacing-xs);
}
.login-subtitle {
font-size: var(--font-size-body-medium);
color: var(--color-gray-500);
}
.login-form {
margin-bottom: var(--spacing-l);
}
.login-options {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: var(--spacing-l);
}
.login-link {
font-size: var(--font-size-body-small);
color: var(--color-gray-500);
text-decoration: none;
transition: color var(--transition-fast) var(--ease-out);
}
.login-link:hover {
color: var(--color-primary);
text-decoration: underline;
}
.login-divider {
text-align: center;
margin: var(--spacing-l) 0;
position: relative;
}
.login-divider::before {
content: '';
position: absolute;
top: 50%;
left: 0;
right: 0;
height: 1px;
background-color: var(--color-gray-300);
z-index: 0;
}
.login-divider span {
position: relative;
background-color: var(--color-white);
padding: 0 var(--spacing-m);
font-size: var(--font-size-body-small);
color: var(--color-gray-500);
z-index: 1;
}
.login-signup {
text-align: center;
font-size: var(--font-size-body-medium);
color: var(--color-gray-700);
}
.login-signup a {
color: var(--color-primary);
font-weight: var(--font-weight-semibold);
text-decoration: none;
}
.login-signup a:hover {
text-decoration: underline;
}
.password-toggle {
position: absolute;
right: var(--spacing-m);
top: 50%;
transform: translateY(-50%);
width: 24px;
height: 24px;
background: none;
border: none;
color: var(--color-gray-500);
cursor: pointer;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
}
.password-toggle:hover {
color: var(--color-primary);
}
/* Tablet and Desktop */
@media (min-width: 768px) {
.login-container {
padding: var(--spacing-2xl);
}
}
@media (min-width: 1024px) {
.login-page {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0;
padding: 0;
}
.login-hero {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: var(--gradient-primary);
color: var(--color-white);
padding: var(--spacing-2xl);
}
.login-hero__title {
font-size: 36px;
font-weight: var(--font-weight-bold);
margin-bottom: var(--spacing-m);
text-align: center;
}
.login-hero__subtitle {
font-size: var(--font-size-body-large);
text-align: center;
opacity: 0.9;
}
.login-wrapper {
display: flex;
align-items: center;
justify-content: center;
padding: var(--spacing-2xl);
}
.login-container {
box-shadow: none;
max-width: 480px;
}
}
</style>
</head>
<body>
<!-- Skip Link -->
<a href="#main-content" class="skip-link">본문으로 건너뛰기</a>
<div class="login-page">
<!-- Hero Section (Desktop Only) -->
<div class="login-hero hidden-mobile hidden-tablet">
<div>
<h1 class="login-hero__title">AI로 간편하게,<br>성공으로 확실하게</h1>
<p class="login-hero__subtitle">소상공인을 위한 스마트 이벤트 마케팅 솔루션</p>
</div>
</div>
<!-- App Wrapper -->
<div class="app-wrapper">
<!-- Main Content -->
<main id="main-content" class="app-content">
<div class="container" style="max-width: 480px; margin: 0 auto; padding: 60px 20px 40px;">
<!-- Logo & Title -->
<div style="text-align: center; margin-bottom: 48px;">
<div style="width: 80px; height: 80px; background: linear-gradient(135deg, var(--color-primary-main), #C71F24); border-radius: 20px; display: flex; align-items: center; justify-content: center; margin: 0 auto 24px;">
<span class="material-icons" style="font-size: 48px; color: white;">campaign</span>
<!-- Login Form Section -->
<div class="login-wrapper">
<div class="login-container">
<div class="login-header">
<div class="login-logo" aria-label="KT 이벤트 마케팅">
🎯
</div>
<h1 class="h1" style="margin-bottom: 8px;">KT 이벤트 마케팅</h1>
<p class="body-m text-muted">AI로 쉽고 빠르게, 효과적인 이벤트</p>
<h1 class="login-title">KT 이벤트 마케팅</h1>
<p class="login-subtitle">소상공인 이벤트 자동 생성 서비스</p>
</div>
<!-- Login Form -->
<form id="loginForm" novalidate>
<!-- Email -->
<form class="login-form" id="loginForm" novalidate>
<div class="form-group">
<label for="email" class="form-label required">이메일</label>
<label for="email" class="form-label form-label--required">이메일</label>
<input
type="email"
id="email"
name="email"
class="form-input"
class="input"
placeholder="example@email.com"
required
autocomplete="email"
>
</div>
<!-- Password -->
<div class="form-group">
<label for="password" class="form-label required">비밀번호</label>
<input
type="password"
id="password"
name="password"
class="form-input"
placeholder="비밀번호를 입력하세요"
required
minlength="8"
autocomplete="current-password"
>
</div>
<!-- Remember Me & Find Password -->
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px;">
<div class="form-check">
<div class="form-group">
<label for="password" class="form-label form-label--required">비밀번호</label>
<div style="position: relative;">
<input
type="checkbox"
id="remember"
name="remember"
type="password"
id="password"
name="password"
class="input"
placeholder="비밀번호를 입력하세요"
autocomplete="current-password"
required
>
<label for="remember" style="flex: 1; cursor: pointer;">
<span class="body-m">로그인 상태 유지</span>
</label>
<button
type="button"
class="password-toggle"
id="passwordToggle"
aria-label="비밀번호 보기/숨기기"
>
<span id="passwordIcon">👁</span>
</button>
</div>
<button type="button" class="btn btn-text btn-sm" style="color: var(--color-gray-600);">
비밀번호 찾기
</button>
</div>
<!-- Submit Button -->
<button
type="submit"
id="loginBtn"
class="btn btn-primary btn-lg btn-block"
style="margin-bottom: 16px;"
>
로그인
</button>
<div class="login-options">
<div class="checkbox-wrapper">
<input type="checkbox" id="autoLogin" name="autoLogin" class="checkbox">
<label for="autoLogin" class="checkbox-label">자동 로그인</label>
</div>
<a href="#" class="login-link" id="forgotPassword">비밀번호 찾기</a>
</div>
<!-- KT Auth Button -->
<button
type="button"
id="ktAuthBtn"
class="btn btn-secondary btn-lg btn-block"
style="margin-bottom: 32px; display: flex; align-items: center; justify-content: center; gap: 8px;"
>
<span class="material-icons" style="font-size: 20px;">smartphone</span>
KT 통합 인증으로 로그인
<button type="submit" class="btn btn-primary btn-large btn-block" id="loginButton">
로그인
</button>
</form>
<!-- Sign Up Link -->
<div style="text-align: center; padding: 24px 0; border-top: 1px solid var(--color-gray-200);">
<p class="body-m text-muted" style="margin-bottom: 12px;">아직 계정이 없으신가요?</p>
<button
type="button"
class="btn btn-text"
style="color: var(--color-primary-main); font-weight: 600;"
onclick="window.location.href='01-회원가입.html'"
>
회원가입하기
</button>
<div class="login-divider">
<span>또는</span>
</div>
<!-- Footer Info -->
<div style="text-align: center; margin-top: 40px;">
<p class="body-s text-muted">
로그인 시
<a href="#" style="text-decoration: underline; color: var(--color-gray-600);">서비스 이용약관</a>
<a href="#" style="text-decoration: underline; color: var(--color-gray-600);">개인정보처리방침</a>에 동의하게 됩니다
</p>
<div class="login-signup">
계정이 없으신가요? <a href="01-회원가입.html">회원가입</a>
</div>
</div>
</main>
</div>
</div>
<!-- Scripts -->
<script src="js/common.js"></script>
<script src="js/form.js"></script>
<script>
(function() {
'use strict';
const form = document.getElementById('loginForm');
const emailInput = document.getElementById('email');
const { FormValidator, Toast } = window.KTEvent;
// Password Toggle
const passwordInput = document.getElementById('password');
const loginBtn = document.getElementById('loginBtn');
const ktAuthBtn = document.getElementById('ktAuthBtn');
const passwordToggle = document.getElementById('passwordToggle');
const passwordIcon = document.getElementById('passwordIcon');
// 폼 검증 및 버튼 활성화
function checkFormValidity() {
const isEmailValid = FormValidator.isValidEmail(emailInput.value);
const isPasswordValid = passwordInput.value.length >= 8;
passwordToggle.addEventListener('click', () => {
if (passwordInput.type === 'password') {
passwordInput.type = 'text';
passwordIcon.textContent = '👁️‍🗨️';
} else {
passwordInput.type = 'password';
passwordIcon.textContent = '👁';
}
});
loginBtn.disabled = !(isEmailValid && isPasswordValid);
// Form Validation
const loginForm = document.getElementById('loginForm');
const validator = new FormValidator(loginForm, {
validateOnBlur: true,
validateOnInput: true,
scrollToError: true
});
// Add validation rules
validator
.addField('email', {
required: true,
email: true,
messages: {
required: '이메일을 입력해주세요.',
email: '올바른 이메일 형식이 아닙니다.'
}
})
.addField('password', {
required: true,
minLength: 6,
messages: {
required: '비밀번호를 입력해주세요.',
minLength: '비밀번호는 최소 6자 이상이어야 합니다.'
}
});
// Override onSubmit
validator.onSubmit = function() {
const values = this.getValues();
const loginButton = document.getElementById('loginButton');
// Show loading state
loginButton.disabled = true;
loginButton.classList.add('btn-loading');
// Simulate API call
setTimeout(() => {
// Mock authentication
if (values.email === 'demo@kt.com' && values.password === 'demo1234') {
// Save to localStorage
window.KTEvent.storage.set('user', {
email: values.email,
autoLogin: values.autoLogin,
loginAt: new Date().toISOString()
});
Toast.show('로그인에 성공했습니다!', {
type: 'success',
duration: 2000
});
// Redirect to home
setTimeout(() => {
window.location.href = '03-홈화면.html';
}, 1000);
} else {
// Login failed
loginButton.disabled = false;
loginButton.classList.remove('btn-loading');
Toast.show('이메일 또는 비밀번호가 일치하지 않습니다.', {
type: 'error',
duration: 3000
});
}
}, 1500);
};
// Forgot Password
document.getElementById('forgotPassword').addEventListener('click', (e) => {
e.preventDefault();
Toast.show('비밀번호 찾기 기능은 준비 중입니다.', {
type: 'info',
duration: 2000
});
});
// Auto-fill demo credentials (for prototype testing)
if (window.location.search.includes('demo=true')) {
document.getElementById('email').value = 'demo@kt.com';
document.getElementById('password').value = 'demo1234';
}
// 입력 필드 이벤트 리스너
[emailInput, passwordInput].forEach(input => {
input.addEventListener('input', checkFormValidity);
input.addEventListener('change', checkFormValidity);
});
// 이메일 로그인
form.addEventListener('submit', function(e) {
e.preventDefault();
const email = emailInput.value.trim();
const password = passwordInput.value;
if (!FormValidator.isValidEmail(email)) {
Toast.error('올바른 이메일 형식을 입력하세요.');
return;
}
if (password.length < 8) {
Toast.error('비밀번호는 8자 이상이어야 합니다.');
return;
}
Loading.show('로그인 중...');
// 시뮬레이션: 실제로는 서버 API 호출
// Check if already logged in
const user = window.KTEvent.storage.get('user');
if (user && user.autoLogin) {
Toast.show('자동 로그인 중...', {
type: 'info',
duration: 1500
});
setTimeout(() => {
Loading.hide();
// 사용자 정보 저장
window.AppState.user = {
id: Utils.generateId(),
email: email,
name: email.split('@')[0],
authType: 'email',
loginAt: new Date().toISOString()
};
window.AppState.save();
Toast.success('로그인되었습니다.');
// 홈 화면으로 이동
setTimeout(() => {
window.location.href = '21.5-홈.html';
}, 500);
window.location.href = '03-홈화면.html';
}, 1500);
});
// KT 통합 인증 로그인
ktAuthBtn.addEventListener('click', function() {
Loading.show('KT 통합 인증 연결 중...');
// 시뮬레이션: 실제로는 KT 인증 API 호출
setTimeout(() => {
Loading.hide();
// 사용자 정보 저장
window.AppState.user = {
id: Utils.generateId(),
phone: '010-1234-5678',
name: '홍길동',
authType: 'kt',
loginAt: new Date().toISOString()
};
window.AppState.save();
Toast.success('KT 통합 인증으로 로그인되었습니다.');
// 홈 화면으로 이동
setTimeout(() => {
window.location.href = '21.5-홈.html';
}, 500);
}, 2000);
});
// 초기 상태 확인
checkFormValidity();
console.log('로그인 페이지 로드 완료');
}
})();
</script>
<style>
/* 로그인 페이지 특화 스타일 */
body {
background: linear-gradient(180deg, #FFF5F5 0%, #FFFFFF 50%);
min-height: 100vh;
}
.app-wrapper {
background: transparent;
}
.btn-block {
width: 100%;
}
</style>
</body>
</html>

View File

@ -6,136 +6,157 @@
<meta name="description" content="KT AI 기반 소상공인 이벤트 자동 생성 서비스 - 회원가입">
<title>회원가입 - KT 이벤트 마케팅</title>
<!-- Styles -->
<link rel="stylesheet" href="css/common.css">
<!-- Pretendard Font -->
<link rel="stylesheet" as="style" crossorigin
href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/variable/pretendardvariable.min.css">
<!-- Fonts -->
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/static/pretendard.min.css">
<!-- Stylesheets -->
<link rel="stylesheet" href="css/variables.css">
<link rel="stylesheet" href="css/reset.css">
<link rel="stylesheet" href="css/layout.css">
<link rel="stylesheet" href="css/common.css">
<link rel="stylesheet" href="css/components/buttons.css">
<link rel="stylesheet" href="css/components/inputs.css">
<link rel="stylesheet" href="css/components/navigation.css">
<link rel="stylesheet" href="css/components/modals.css">
</head>
<body>
<!-- Skip Link -->
<a href="#main-content" class="skip-link">본문으로 건너뛰기</a>
<!-- App Wrapper -->
<div class="app-wrapper">
<!-- Top App Bar -->
<div class="page">
<!-- App Bar -->
<header class="app-bar">
<button class="app-bar__back" aria-label="뒤로가기">
<span class="material-icons">arrow_back</span>
</button>
<h1 class="app-bar__title">회원가입</h1>
<div class="app-bar__container">
<div class="app-bar__left">
<svg class="app-bar__back" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path d="M19 12H5M12 19l-7-7 7-7" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>
<h1 class="app-bar__title">회원가입</h1>
<div class="app-bar__right"></div>
</div>
</header>
<!-- Main Content -->
<main id="main-content" class="app-content">
<div class="container" style="max-width: 500px; margin: 0 auto; padding-top: 32px;">
<!-- Welcome Section -->
<div style="text-align: center; margin-bottom: 48px;">
<h2 class="display" style="margin-bottom: 8px;">KT 이벤트 마케팅</h2>
<p class="body-l text-muted">환영합니다 🎉</p>
<main class="page-main">
<div class="container" style="max-width: 500px; padding-top: var(--spacing-2xl); padding-bottom: var(--spacing-2xl);">
<div class="text-center mb-2xl">
<div style="font-size: 48px; margin-bottom: var(--spacing-m);">🎉</div>
<h2 class="h2 mb-s">KT 이벤트 마케팅</h2>
<p class="body-medium text-gray-500">환영합니다</p>
</div>
<!-- Registration Form -->
<form id="registrationForm" novalidate>
<form id="signupForm" novalidate>
<!-- 이름 -->
<div class="form-group">
<label for="name" class="form-label required">이름</label>
<label for="name" class="form-label form-label--required">이름</label>
<input
type="text"
id="name"
name="name"
class="form-input"
placeholder="홍길동"
required
minlength="2"
class="input"
placeholder="2자 이상 입력"
autocomplete="name"
required
>
</div>
<!-- 전화번호 -->
<div class="form-group">
<label for="phone" class="form-label required">전화번호</label>
<label for="phone" class="form-label form-label--required">전화번호</label>
<input
type="tel"
id="phone"
name="phone"
class="form-input"
placeholder="010-1234-5678"
required
maxlength="13"
class="input"
placeholder="010-XXXX-XXXX"
autocomplete="tel"
required
>
</div>
<!-- 이메일 -->
<div class="form-group">
<label for="email" class="form-label required">이메일</label>
<label for="email" class="form-label form-label--required">이메일</label>
<input
type="email"
id="email"
name="email"
class="form-input"
placeholder="example@domain.com"
required
class="input"
placeholder="example@email.com"
autocomplete="email"
required
>
</div>
<!-- KT 본인 인증 (선택) -->
<div class="card" style="margin-bottom: 24px; padding: 16px;">
<div class="form-check">
<input
type="checkbox"
id="ktAuth"
name="ktAuth"
>
<label for="ktAuth" style="flex: 1; cursor: pointer;">
<div class="body-m" style="font-weight: 600; margin-bottom: 4px;">
KT 본인 인증 (선택)
</div>
<div class="body-s text-muted">
인증 시 추가 혜택이 제공됩니다
</div>
</label>
</div>
</div>
<!-- 개인정보 수집 동의 (필수) -->
<!-- 비밀번호 -->
<div class="form-group">
<div class="form-check">
<label for="password" class="form-label form-label--required">비밀번호</label>
<div style="position: relative;">
<input
type="checkbox"
id="privacyAgree"
name="privacyAgree"
type="password"
id="password"
name="password"
class="input"
placeholder="비밀번호 입력"
autocomplete="new-password"
required
>
<label for="privacyAgree" style="flex: 1; cursor: pointer;">
<span class="body-m">개인정보 수집 및 이용 동의 (필수)</span>
<button type="button" class="btn-text btn-sm" style="padding: 0; margin-left: 8px;" onclick="showPrivacyModal()">
자세히보기
</button>
<button
type="button"
class="password-toggle"
data-target="password"
aria-label="비밀번호 보기/숨기기"
style="position: absolute; right: 16px; top: 50%; transform: translateY(-50%); background: none; border: none; cursor: pointer; padding: 0; display: flex; align-items: center;"
>
<span style="font-size: 20px;">👁</span>
</button>
</div>
<span class="form-helper">최소 8자, 영문/숫자/특수문자 조합</span>
</div>
<!-- 비밀번호 확인 -->
<div class="form-group">
<label for="passwordConfirm" class="form-label form-label--required">비밀번호 확인</label>
<div style="position: relative;">
<input
type="password"
id="passwordConfirm"
name="passwordConfirm"
class="input"
placeholder="비밀번호 재입력"
autocomplete="new-password"
required
>
<button
type="button"
class="password-toggle"
data-target="passwordConfirm"
aria-label="비밀번호 확인 보기/숨기기"
style="position: absolute; right: 16px; top: 50%; transform: translateY(-50%); background: none; border: none; cursor: pointer; padding: 0; display: flex; align-items: center;"
>
<span style="font-size: 20px;">👁</span>
</button>
</div>
</div>
<!-- 개인정보 수집 동의 -->
<div class="form-group">
<div class="checkbox-wrapper">
<input type="checkbox" id="agreePrivacy" name="agreePrivacy" class="checkbox" required>
<label for="agreePrivacy" class="checkbox-label">
개인정보 수집 동의 (필수) <a href="#" id="viewPrivacy" style="color: var(--color-primary); text-decoration: underline;">자세히보기</a>
</label>
</div>
</div>
<!-- Submit Button -->
<button
type="submit"
id="submitBtn"
class="btn btn-primary btn-lg btn-block"
style="margin-top: 32px;"
disabled
>
다음 단계
<!-- 제출 버튼 -->
<button type="submit" class="btn btn-primary btn-large btn-block mt-xl">
매장 정보 등록
</button>
</form>
<!-- Additional Info -->
<div style="text-align: center; margin-top: 24px;">
<p class="body-s text-muted">
이미 계정이 있으신가요?
<a href="#" class="text-primary" style="text-decoration: none; font-weight: 600;">로그인</a>
<div class="text-center mt-l">
<p class="body-small text-gray-500">
이미 계정이 있으신가요? <a href="00-로그인.html" style="color: var(--color-primary); font-weight: var(--font-weight-semibold);">로그인</a>
</p>
</div>
</div>
@ -144,180 +165,182 @@
<!-- Scripts -->
<script src="js/common.js"></script>
<script src="js/navigation.js"></script>
<script src="js/modal.js"></script>
<script src="js/form.js"></script>
<script>
(function() {
'use strict';
const form = document.getElementById('registrationForm');
const submitBtn = document.getElementById('submitBtn');
const nameInput = document.getElementById('name');
const phoneInput = document.getElementById('phone');
const emailInput = document.getElementById('email');
const ktAuthCheckbox = document.getElementById('ktAuth');
const privacyCheckbox = document.getElementById('privacyAgree');
const { FormValidator, Toast, Dialog } = window.KTEvent;
// 폼 검증 및 버튼 활성화 체크
function checkFormValidity() {
const isNameValid = nameInput.value.trim().length >= 2;
const isPhoneValid = FormValidator.isValidPhone(phoneInput.value);
const isEmailValid = FormValidator.isValidEmail(emailInput.value);
const isPrivacyAgreed = privacyCheckbox.checked;
// Password Toggle
document.querySelectorAll('.password-toggle').forEach(button => {
button.addEventListener('click', () => {
const targetId = button.getAttribute('data-target');
const input = document.getElementById(targetId);
const icon = button.querySelector('span');
const isFormValid = isNameValid && isPhoneValid && isEmailValid && isPrivacyAgreed;
submitBtn.disabled = !isFormValid;
}
// 입력 필드 이벤트 리스너
[nameInput, phoneInput, emailInput].forEach(input => {
input.addEventListener('input', checkFormValidity);
input.addEventListener('blur', function() {
if (this.value) {
validateField(this);
if (input.type === 'password') {
input.type = 'text';
icon.textContent = '👁️‍🗨️';
} else {
input.type = 'password';
icon.textContent = '👁';
}
});
});
privacyCheckbox.addEventListener('change', checkFormValidity);
// Phone Number Formatting
const phoneInput = document.getElementById('phone');
phoneInput.addEventListener('input', (e) => {
let value = e.target.value.replace(/[^0-9]/g, '');
if (value.length > 11) value = value.slice(0, 11);
// 개별 필드 검증
function validateField(input) {
let isValid = true;
let errorMessage = '';
switch(input.id) {
case 'name':
if (input.value.trim().length < 2) {
isValid = false;
errorMessage = '이름은 2자 이상 입력해주세요.';
}
break;
case 'phone':
if (!FormValidator.isValidPhone(input.value)) {
isValid = false;
errorMessage = '올바른 전화번호 형식을 입력하세요. (010-XXXX-XXXX)';
}
break;
case 'email':
if (!FormValidator.isValidEmail(input.value)) {
isValid = false;
errorMessage = '올바른 이메일 형식을 입력하세요.';
}
break;
if (value.length >= 7) {
value = value.replace(/(\d{3})(\d{4})(\d{0,4})/, '$1-$2-$3');
} else if (value.length >= 3) {
value = value.replace(/(\d{3})(\d{0,4})/, '$1-$2');
}
if (isValid) {
FormValidator.clearError(input);
} else {
FormValidator.showError(input, errorMessage);
}
return isValid;
}
// KT 본인 인증 체크박스 변경 시
ktAuthCheckbox.addEventListener('change', function() {
if (this.checked) {
// KT 인증 시뮬레이션
simulateKTAuth();
}
e.target.value = value;
});
// KT 인증 시뮬레이션
function simulateKTAuth() {
// 실제로는 KT 인증 시스템 연동
Loading.show('KT 본인 인증 중...');
// Form Validation
const signupForm = document.getElementById('signupForm');
const validator = new FormValidator(signupForm, {
validateOnBlur: true,
validateOnInput: true,
scrollToError: true
});
// Password strength validation
const validatePasswordStrength = (password) => {
if (password.length < 8) return false;
const hasLetter = /[a-zA-Z]/.test(password);
const hasNumber = /[0-9]/.test(password);
const hasSpecial = /[!@#$%^&*(),.?":{}|<>]/.test(password);
return hasLetter && hasNumber && hasSpecial;
};
// Add validation rules
validator
.addField('name', {
required: true,
minLength: 2,
messages: {
required: '이름을 입력해주세요.',
minLength: '이름을 2자 이상 입력해주세요.'
}
})
.addField('phone', {
required: true,
phone: true,
messages: {
required: '전화번호를 입력해주세요.',
phone: '올바른 전화번호 형식이 아닙니다. (010-XXXX-XXXX)'
}
})
.addField('email', {
required: true,
email: true,
messages: {
required: '이메일을 입력해주세요.',
email: '올바른 이메일 형식이 아닙니다.'
}
})
.addField('password', {
required: true,
custom: (value) => validatePasswordStrength(value),
messages: {
required: '비밀번호를 입력해주세요.',
custom: '최소 8자 이상, 영문/숫자/특수문자를 조합해주세요.'
}
})
.addField('passwordConfirm', {
required: true,
custom: (value) => value === document.getElementById('password').value,
messages: {
required: '비밀번호 확인을 입력해주세요.',
custom: '비밀번호가 일치하지 않습니다.'
}
})
.addField('agreePrivacy', {
required: true,
custom: (value, field) => field.checked,
messages: {
required: '개인정보 수집 동의가 필요합니다.',
custom: '개인정보 수집 동의가 필요합니다.'
}
});
// Override onSubmit
validator.onSubmit = function() {
const values = this.getValues();
const submitButton = signupForm.querySelector('button[type="submit"]');
// Show loading state
submitButton.disabled = true;
submitButton.classList.add('btn-loading');
// Simulate API call
setTimeout(() => {
Loading.hide();
Toast.success('KT 본인 인증이 완료되었습니다. 추가 혜택이 적용됩니다!');
}, 2000);
}
// Mock duplicate check
const existingEmails = ['test@example.com', 'demo@kt.com'];
const existingPhones = ['010-1234-5678'];
// 폼 제출
form.addEventListener('submit', function(e) {
e.preventDefault();
if (existingEmails.includes(values.email)) {
submitButton.disabled = false;
submitButton.classList.remove('btn-loading');
validator.setError('email', '이미 가입된 이메일입니다.');
return;
}
// 모든 필드 검증
const isNameValid = validateField(nameInput);
const isPhoneValid = validateField(phoneInput);
const isEmailValid = validateField(emailInput);
if (existingPhones.includes(values.phone)) {
submitButton.disabled = false;
submitButton.classList.remove('btn-loading');
validator.setError('phone', '이미 가입된 전화번호입니다.');
return;
}
if (!isNameValid || !isPhoneValid || !isEmailValid) {
Toast.error('입력 내용을 확인해주세요.');
return;
}
if (!privacyCheckbox.checked) {
Toast.error('개인정보 수집 및 이용에 동의해주세요.');
return;
}
// 중복 가입 체크 시뮬레이션
Loading.show('가입 정보 확인 중...');
setTimeout(() => {
Loading.hide();
// 사용자 정보 저장
const userData = {
id: Utils.generateId(),
name: nameInput.value.trim(),
phone: phoneInput.value,
email: emailInput.value,
ktAuth: ktAuthCheckbox.checked,
// Save user data
window.KTEvent.storage.set('signupData', {
name: values.name,
phone: values.phone,
email: values.email,
createdAt: new Date().toISOString()
};
});
window.AppState.user = userData;
window.AppState.save();
Toast.show('회원가입이 완료되었습니다!', {
type: 'success',
duration: 2000
});
// 성공 메시지
Toast.success('회원가입이 완료되었습니다!');
// 다음 화면으로 이동 (매장정보등록)
// Redirect to store info registration
setTimeout(() => {
window.location.href = '02-매장정보등록.html';
}, 1000);
}, 1500);
});
// 개인정보 처리방침 모달
window.showPrivacyModal = function() {
Modal.alert(
'개인정보 수집 및 이용 동의',
`
<div class="body-m" style="max-height: 300px; overflow-y: auto;">
<h3 class="h3" style="margin-bottom: 12px;">수집하는 개인정보 항목</h3>
<ul style="margin-left: 20px; margin-bottom: 16px;">
<li>이름</li>
<li>전화번호</li>
<li>이메일 주소</li>
</ul>
<h3 class="h3" style="margin-bottom: 12px;">개인정보의 수집 및 이용 목적</h3>
<ul style="margin-left: 20px; margin-bottom: 16px;">
<li>회원 가입 및 관리</li>
<li>서비스 제공 및 계약 이행</li>
<li>고객 문의 응대</li>
</ul>
<h3 class="h3" style="margin-bottom: 12px;">개인정보의 보유 및 이용 기간</h3>
<p style="margin-bottom: 16px;">
회원 탈퇴 시까지 보유하며, 관계 법령에 따라 일정 기간 보관할 수 있습니다.
</p>
<p class="body-s text-muted">
귀하는 개인정보 수집 및 이용에 동의하지 않을 권리가 있으나, 동의하지 않을 경우 서비스 이용이 제한될 수 있습니다.
</p>
</div>
`
);
};
console.log('회원가입 페이지 로드 완료');
// View Privacy Policy
document.getElementById('viewPrivacy').addEventListener('click', async (e) => {
e.preventDefault();
const result = await Dialog.confirm({
title: '개인정보 수집 및 이용 동의',
message: 'KT 이벤트 마케팅 서비스는 다음과 같이 개인정보를 수집 및 이용합니다.\n\n1. 수집 항목: 이름, 전화번호, 이메일, 매장 정보\n2. 수집 목적: 서비스 제공, 이벤트 생성 및 관리\n3. 보유 기간: 회원 탈퇴 시까지\n\n자세한 내용은 개인정보처리방침을 확인해주세요.',
confirmText: '동의',
cancelText: '닫기'
});
if (result) {
document.getElementById('agreePrivacy').checked = true;
}
});
})();
</script>
</body>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,470 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="KT AI 기반 소상공인 이벤트 자동 생성 서비스 - 홈">
<title>홈 - KT 이벤트 마케팅</title>
<!-- Pretendard Font -->
<link rel="stylesheet" as="style" crossorigin
href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/variable/pretendardvariable.min.css">
<!-- Stylesheets -->
<link rel="stylesheet" href="css/variables.css">
<link rel="stylesheet" href="css/reset.css">
<link rel="stylesheet" href="css/layout.css">
<link rel="stylesheet" href="css/common.css">
<link rel="stylesheet" href="css/components/buttons.css">
<link rel="stylesheet" href="css/components/cards.css">
<link rel="stylesheet" href="css/components/navigation.css">
<style>
/* Home Screen Specific Styles */
.home-header {
background: linear-gradient(135deg, var(--color-primary) 0%, var(--color-primary-dark) 100%);
color: var(--color-white);
padding: var(--spacing-xl) var(--spacing-l) var(--spacing-2xl);
}
.home-header__greeting {
font-size: var(--font-size-h2);
font-weight: var(--font-weight-bold);
margin-bottom: var(--spacing-xs);
}
.home-header__subtitle {
font-size: var(--font-size-body-medium);
opacity: 0.9;
margin-bottom: var(--spacing-xl);
}
.quick-actions {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: var(--spacing-m);
}
.quick-action-card {
background-color: rgba(255, 255, 255, 0.2);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: var(--radius-m);
padding: var(--spacing-l);
text-align: center;
transition: all var(--transition-fast) var(--ease-out);
cursor: pointer;
}
.quick-action-card:hover {
background-color: rgba(255, 255, 255, 0.3);
transform: translateY(-2px);
}
.quick-action-card__icon {
font-size: 32px;
margin-bottom: var(--spacing-s);
}
.quick-action-card__title {
font-size: var(--font-size-body-medium);
font-weight: var(--font-weight-semibold);
}
.stats-section {
padding: var(--spacing-l);
background-color: var(--color-white);
}
.stats-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: var(--spacing-m);
margin-bottom: var(--spacing-xl);
}
.stat-card--compact {
background-color: var(--color-white);
border: 1px solid var(--color-gray-300);
border-radius: var(--radius-m);
padding: var(--spacing-m);
text-align: center;
}
.stat-card--compact__value {
font-size: var(--font-size-h1);
font-weight: var(--font-weight-bold);
color: var(--color-primary);
margin-bottom: var(--spacing-xs);
}
.stat-card--compact__label {
font-size: var(--font-size-body-small);
color: var(--color-gray-600);
}
.section-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: var(--spacing-l);
}
.section-header__title {
font-size: var(--font-size-h3);
font-weight: var(--font-weight-semibold);
color: var(--color-black);
}
.section-header__link {
font-size: var(--font-size-body-small);
color: var(--color-primary);
text-decoration: none;
display: flex;
align-items: center;
gap: var(--spacing-xs);
}
.section-header__link:hover {
text-decoration: underline;
}
.events-list {
display: flex;
flex-direction: column;
gap: var(--spacing-m);
}
.empty-state {
text-align: center;
padding: var(--spacing-2xl);
background-color: var(--color-gray-100);
border-radius: var(--radius-m);
margin-bottom: var(--spacing-xl);
}
.empty-state__icon {
font-size: 48px;
margin-bottom: var(--spacing-m);
}
.empty-state__title {
font-size: var(--font-size-h3);
font-weight: var(--font-weight-semibold);
color: var(--color-black);
margin-bottom: var(--spacing-s);
}
.empty-state__description {
font-size: var(--font-size-body-medium);
color: var(--color-gray-600);
margin-bottom: var(--spacing-l);
}
.ai-insights {
background: linear-gradient(135deg, #F0F7FF 0%, #E3F2FD 100%);
border-radius: var(--radius-m);
padding: var(--spacing-l);
margin-bottom: var(--spacing-xl);
}
.ai-insights__header {
display: flex;
align-items: center;
gap: var(--spacing-s);
margin-bottom: var(--spacing-m);
}
.ai-insights__icon {
font-size: 24px;
}
.ai-insights__title {
font-size: var(--font-size-h3);
font-weight: var(--font-weight-semibold);
color: var(--color-secondary);
}
.ai-insights__content {
font-size: var(--font-size-body-medium);
color: var(--color-gray-700);
line-height: 1.6;
}
@media (min-width: 768px) {
.quick-actions {
grid-template-columns: repeat(4, 1fr);
}
.stats-grid {
grid-template-columns: repeat(3, 1fr);
}
}
</style>
</head>
<body>
<div class="page">
<!-- Home Header -->
<header class="home-header">
<div class="home-header__greeting" id="greeting">안녕하세요!</div>
<div class="home-header__subtitle">AI로 간편하게 이벤트를 만들어보세요</div>
<!-- Quick Actions -->
<div class="quick-actions">
<div class="quick-action-card" onclick="location.href='04-이벤트목적선택.html'">
<div class="quick-action-card__icon"></div>
<div class="quick-action-card__title">새 이벤트</div>
</div>
<div class="quick-action-card" onclick="location.href='25-이벤트목록.html'">
<div class="quick-action-card__icon">📋</div>
<div class="quick-action-card__title">이벤트 목록</div>
</div>
<div class="quick-action-card" onclick="location.href='21-실시간대시보드.html'">
<div class="quick-action-card__icon">📊</div>
<div class="quick-action-card__title">실시간 현황</div>
</div>
<div class="quick-action-card" onclick="location.href='26-마이페이지.html'">
<div class="quick-action-card__icon">⚙️</div>
<div class="quick-action-card__title">설정</div>
</div>
</div>
</header>
<!-- Main Content -->
<main class="page-main" style="padding-bottom: var(--bottom-nav-height);">
<div class="stats-section">
<!-- Stats Grid -->
<div class="stats-grid">
<div class="stat-card--compact">
<div class="stat-card--compact__value" id="totalEvents">0</div>
<div class="stat-card--compact__label">진행 중인<br>이벤트</div>
</div>
<div class="stat-card--compact">
<div class="stat-card--compact__value" id="totalParticipants">0</div>
<div class="stat-card--compact__label">총 참여자</div>
</div>
<div class="stat-card--compact">
<div class="stat-card--compact__value" id="conversionRate">0%</div>
<div class="stat-card--compact__label">전환율</div>
</div>
</div>
<!-- AI Insights -->
<div class="ai-insights">
<div class="ai-insights__header">
<span class="ai-insights__icon">🤖</span>
<h3 class="ai-insights__title">AI 인사이트</h3>
</div>
<div class="ai-insights__content" id="aiInsight">
이벤트를 생성하시면 AI가 맞춤형 인사이트를 제공해드립니다.
</div>
</div>
<!-- Recent Events Section -->
<div class="section-header">
<h2 class="section-header__title">최근 이벤트</h2>
<a href="25-이벤트목록.html" class="section-header__link">
전체보기
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor">
<path d="M6 12l4-4-4-4" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</a>
</div>
<!-- Empty State -->
<div class="empty-state" id="emptyState">
<div class="empty-state__icon">🎯</div>
<h3 class="empty-state__title">첫 이벤트를 만들어보세요!</h3>
<p class="empty-state__description">
AI가 업종과 목적에 맞는<br>
최적의 이벤트를 추천해드립니다
</p>
<button
class="btn btn-primary btn-large"
onclick="location.href='04-이벤트목적선택.html'"
>
AI 이벤트 만들기
</button>
</div>
<!-- Recent Events List (Hidden initially) -->
<div class="events-list" id="eventsList" style="display: none;">
<!-- Events will be dynamically added here -->
</div>
</div>
</main>
<!-- Bottom Navigation -->
<nav class="bottom-nav">
<a href="03-홈화면.html" class="bottom-nav__item bottom-nav__item--active">
<svg class="bottom-nav__icon" width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
<path d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z"/>
</svg>
<span class="bottom-nav__label"></span>
</a>
<a href="25-이벤트목록.html" class="bottom-nav__item">
<svg class="bottom-nav__icon" width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
<path d="M19 3h-4.18C14.4 1.84 13.3 1 12 1c-1.3 0-2.4.84-2.82 2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-7 0c.55 0 1 .45 1 1s-.45 1-1 1-1-.45-1-1 .45-1 1-1zm2 14H7v-2h7v2zm3-4H7v-2h10v2zm0-4H7V7h10v2z"/>
</svg>
<span class="bottom-nav__label">이벤트</span>
</a>
<a href="04-이벤트목적선택.html" class="bottom-nav__item bottom-nav__item--primary">
<svg class="bottom-nav__icon" width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
<circle cx="12" cy="12" r="10"/>
<path d="M12 6v12M6 12h12" stroke="white" stroke-width="2" stroke-linecap="round"/>
</svg>
<span class="bottom-nav__label">만들기</span>
</a>
<a href="21-실시간대시보드.html" class="bottom-nav__item">
<svg class="bottom-nav__icon" width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
<path d="M3 13h8V3H3v10zm0 8h8v-6H3v6zm10 0h8V11h-8v10zm0-18v6h8V3h-8z"/>
</svg>
<span class="bottom-nav__label">분석</span>
</a>
<a href="26-마이페이지.html" class="bottom-nav__item">
<svg class="bottom-nav__icon" width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/>
</svg>
<span class="bottom-nav__label">MY</span>
</a>
</nav>
</div>
<!-- Scripts -->
<script src="js/common.js"></script>
<script src="js/navigation.js"></script>
<script>
(function() {
'use strict';
const { storage, createElement } = window.KTEvent;
// Get user and store data
const user = storage.get('user');
const storeData = storage.get('storeData');
const signupData = storage.get('signupData');
// Update greeting with user name
const greetingEl = document.getElementById('greeting');
if (signupData && signupData.name) {
greetingEl.textContent = `${signupData.name}님, 안녕하세요!`;
} else if (user && user.email) {
const userName = user.email.split('@')[0];
greetingEl.textContent = `${userName}님, 안녕하세요!`;
}
// Load events from storage
const events = storage.get('events', []);
// Update stats
document.getElementById('totalEvents').textContent = events.length;
// Calculate total participants
const totalParticipants = events.reduce((sum, event) => {
return sum + (event.participants || 0);
}, 0);
document.getElementById('totalParticipants').textContent = totalParticipants.toLocaleString();
// Calculate conversion rate
const totalViews = events.reduce((sum, event) => sum + (event.views || 0), 0);
const conversionRate = totalViews > 0
? ((totalParticipants / totalViews) * 100).toFixed(1)
: 0;
document.getElementById('conversionRate').textContent = `${conversionRate}%`;
// Update AI Insights
const aiInsightEl = document.getElementById('aiInsight');
if (events.length > 0) {
const recentEvent = events[0];
const insights = [
`최근 이벤트의 참여율이 ${conversionRate}%입니다. 업계 평균(2.5%)보다 우수해요! 🎉`,
`${storeData?.businessType === 'restaurant' ? '음식점' : '매장'}의 경우, 오후 2-5시 이벤트 참여가 가장 활발합니다. 이 시간대를 활용해보세요! ⏰`,
`SNS 공유를 통한 참여가 65%를 차지하고 있습니다. 공유 인센티브를 강화해보는 건 어떨까요? 📱`
];
const randomInsight = insights[Math.floor(Math.random() * insights.length)];
aiInsightEl.textContent = randomInsight;
} else if (storeData) {
const businessTypeInsights = {
'restaurant': '음식점은 "1+1 할인" 이벤트가 가장 효과적입니다. 런치타임이나 저녁시간대를 타겟팅해보세요!',
'cafe': '카페는 "음료 무료 업그레이드" 이벤트가 인기입니다. SNS 인증샷 이벤트도 추천드려요!',
'retail': '소매점은 "첫 구매 할인" 이벤트로 신규 고객 유치에 집중해보세요.',
'beauty': '뷰티샵은 "친구 추천" 이벤트가 효과적입니다. 재방문 쿠폰을 활용해보세요!',
'fitness': '헬스장은 "무료 체험권" 이벤트가 가장 효과적입니다. 그룹 할인도 고려해보세요!',
'education': '학원은 "첫 달 할인" 이벤트가 인기입니다. 상담 예약 이벤트도 추천드려요!',
'service': '서비스업은 "첫 이용 할인" 이벤트로 신규 고객을 유치해보세요.',
'other': '업종에 맞는 맞춤형 이벤트를 AI가 추천해드립니다. 지금 시작해보세요!'
};
const insight = businessTypeInsights[storeData.businessType] || businessTypeInsights['other'];
aiInsightEl.textContent = `🤖 ${insight}`;
}
// Render events or empty state
const emptyState = document.getElementById('emptyState');
const eventsList = document.getElementById('eventsList');
if (events.length === 0) {
emptyState.style.display = 'block';
eventsList.style.display = 'none';
} else {
emptyState.style.display = 'none';
eventsList.style.display = 'flex';
// Render recent events (max 3)
const recentEvents = events.slice(0, 3);
recentEvents.forEach(event => {
const eventCard = createElement('div', 'event-card');
const statusBadge = event.status === 'active' ?
'<span class="badge badge-success">진행중</span>' :
event.status === 'scheduled' ?
'<span class="badge badge-warning">예정</span>' :
'<span class="badge badge-secondary">종료</span>';
eventCard.innerHTML = `
<div class="event-card__image" style="background: linear-gradient(135deg, ${event.color || '#E31E24'} 0%, ${event.colorDark || '#C71820'} 100%); display: flex; align-items: center; justify-content: center; color: white; font-size: 48px;">
${event.icon || '🎉'}
</div>
<div class="event-card__content">
<div class="event-card__header">
<h3 class="event-card__title">${event.title || '이벤트 제목'}</h3>
${statusBadge}
</div>
<p class="event-card__description">${event.description || '이벤트 설명'}</p>
<div class="event-card__meta">
<span class="event-card__date">
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M11 1v2H5V1H3v2H2c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2h-1V1h-2zm3 14H2V8h12v7zm0-9H2V5h12v1z"/>
</svg>
${event.startDate ? new Date(event.startDate).toLocaleDateString('ko-KR') : '날짜 미정'}
</span>
<span class="event-card__participants">
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M8 8c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/>
</svg>
${event.participants || 0}명 참여
</span>
</div>
</div>
`;
eventCard.style.cursor = 'pointer';
eventCard.addEventListener('click', () => {
// Navigate to event detail page (to be implemented)
console.log('Navigate to event:', event.id);
});
eventsList.appendChild(eventCard);
});
}
// Auto-redirect check (commented out for prototype)
// if (!user) {
// window.location.href = '00-로그인.html';
// } else if (!storeData) {
// window.location.href = '02-매장정보등록.html';
// }
})();
</script>
</body>
</html>

View File

@ -0,0 +1,516 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="KT AI 기반 소상공인 이벤트 자동 생성 서비스 - 이벤트 목적 선택">
<title>이벤트 목적 선택 - KT 이벤트 마케팅</title>
<!-- Pretendard Font -->
<link rel="stylesheet" as="style" crossorigin
href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/variable/pretendardvariable.min.css">
<!-- Stylesheets -->
<link rel="stylesheet" href="css/variables.css">
<link rel="stylesheet" href="css/reset.css">
<link rel="stylesheet" href="css/layout.css">
<link rel="stylesheet" href="css/common.css">
<link rel="stylesheet" href="css/components/buttons.css">
<link rel="stylesheet" href="css/components/cards.css">
<link rel="stylesheet" href="css/components/navigation.css">
<style>
/* Event Purpose Selection Specific Styles */
.purpose-selection {
padding: var(--spacing-l);
padding-bottom: calc(var(--bottom-nav-height) + var(--spacing-l));
max-width: 600px;
margin: 0 auto;
}
.intro-section {
text-align: center;
margin-bottom: var(--spacing-2xl);
}
.intro-section__icon {
font-size: 64px;
margin-bottom: var(--spacing-m);
}
.intro-section__title {
font-size: var(--font-size-h1);
font-weight: var(--font-weight-bold);
color: var(--color-black);
margin-bottom: var(--spacing-s);
}
.intro-section__subtitle {
font-size: var(--font-size-body-medium);
color: var(--color-gray-600);
line-height: 1.6;
}
.purpose-options {
display: flex;
flex-direction: column;
gap: var(--spacing-m);
margin-bottom: var(--spacing-xl);
}
.purpose-card {
position: relative;
background-color: var(--color-white);
border: 2px solid var(--color-gray-300);
border-radius: var(--radius-m);
padding: var(--spacing-l);
cursor: pointer;
transition: all var(--transition-fast) var(--ease-out);
display: flex;
align-items: flex-start;
gap: var(--spacing-m);
}
.purpose-card:hover {
border-color: var(--color-primary);
box-shadow: var(--shadow-md);
transform: translateY(-2px);
}
.purpose-card--selected {
border-color: var(--color-primary);
border-width: 2px;
background-color: #FFF5F5;
box-shadow: var(--shadow-primary);
}
.purpose-card__radio {
appearance: none;
width: 24px;
height: 24px;
border: 2px solid var(--color-gray-400);
border-radius: 50%;
margin-top: 4px;
cursor: pointer;
position: relative;
flex-shrink: 0;
}
.purpose-card--selected .purpose-card__radio {
border-color: var(--color-primary);
background-color: var(--color-primary);
}
.purpose-card--selected .purpose-card__radio::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 8px;
height: 8px;
background-color: var(--color-white);
border-radius: 50%;
}
.purpose-card__content {
flex: 1;
}
.purpose-card__header {
display: flex;
align-items: center;
gap: var(--spacing-s);
margin-bottom: var(--spacing-s);
}
.purpose-card__icon {
font-size: 24px;
}
.purpose-card__title {
font-size: var(--font-size-h3);
font-weight: var(--font-weight-semibold);
color: var(--color-black);
}
.purpose-card__description {
font-size: var(--font-size-body-medium);
color: var(--color-gray-600);
line-height: 1.6;
margin-bottom: var(--spacing-m);
}
.purpose-card__tag {
display: inline-flex;
align-items: center;
padding: 4px 12px;
background-color: var(--color-secondary-light);
color: var(--color-secondary);
font-size: var(--font-size-body-small);
font-weight: var(--font-weight-semibold);
border-radius: var(--radius-s);
}
.purpose-card__examples {
font-size: var(--font-size-body-small);
color: var(--color-gray-500);
margin-top: var(--spacing-s);
}
.ai-tip {
background: linear-gradient(135deg, #F0F7FF 0%, #E3F2FD 100%);
border-radius: var(--radius-m);
padding: var(--spacing-l);
margin-bottom: var(--spacing-xl);
}
.ai-tip__header {
display: flex;
align-items: center;
gap: var(--spacing-s);
margin-bottom: var(--spacing-m);
}
.ai-tip__icon {
font-size: 20px;
}
.ai-tip__title {
font-size: var(--font-size-body-large);
font-weight: var(--font-weight-semibold);
color: var(--color-secondary);
}
.ai-tip__content {
font-size: var(--font-size-body-medium);
color: var(--color-gray-700);
line-height: 1.6;
}
.cta-section {
position: fixed;
bottom: var(--bottom-nav-height);
left: 0;
right: 0;
background-color: var(--color-white);
padding: var(--spacing-m) var(--spacing-l);
border-top: 1px solid var(--color-gray-300);
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.08);
}
.cta-section__inner {
max-width: 600px;
margin: 0 auto;
}
@media (min-width: 768px) {
.purpose-options {
display: grid;
grid-template-columns: repeat(2, 1fr);
}
}
</style>
</head>
<body>
<div class="page">
<!-- App Bar -->
<header class="app-bar">
<div class="app-bar__container">
<div class="app-bar__left">
<svg class="app-bar__back" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path d="M19 12H5M12 19l-7-7 7-7" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>
<h1 class="app-bar__title">새 이벤트 만들기</h1>
<div class="app-bar__right"></div>
</div>
</header>
<!-- Main Content -->
<main class="page-main">
<div class="purpose-selection">
<!-- Intro Section -->
<div class="intro-section">
<div class="intro-section__icon">🎯</div>
<h2 class="intro-section__title">이벤트 목적을 선택하세요</h2>
<p class="intro-section__subtitle">
목적에 맞는 최적의 이벤트를<br>
AI가 추천해드립니다
</p>
</div>
<!-- AI Tip -->
<div class="ai-tip">
<div class="ai-tip__header">
<span class="ai-tip__icon">💡</span>
<span class="ai-tip__title">AI TIP</span>
</div>
<div class="ai-tip__content" id="aiTip">
선택하신 목적에 맞는 최신 트렌드와 성공 사례를 분석하여 맞춤형 이벤트를 제안해드립니다.
</div>
</div>
<!-- Purpose Options -->
<div class="purpose-options" id="purposeOptions">
<label class="purpose-card" data-purpose="new-customers">
<input type="radio" name="purpose" value="new-customers" class="purpose-card__radio">
<div class="purpose-card__content">
<div class="purpose-card__header">
<span class="purpose-card__icon">👥</span>
<h3 class="purpose-card__title">신규 고객 유치</h3>
</div>
<p class="purpose-card__description">
새로운 고객을 끌어들이고 매장 인지도를 높이고 싶어요
</p>
<span class="purpose-card__tag">AI 추천</span>
<div class="purpose-card__examples">
예: 첫 방문 할인, 친구 추천 이벤트
</div>
</div>
</label>
<label class="purpose-card" data-purpose="repeat-visits">
<input type="radio" name="purpose" value="repeat-visits" class="purpose-card__radio">
<div class="purpose-card__content">
<div class="purpose-card__header">
<span class="purpose-card__icon">🔄</span>
<h3 class="purpose-card__title">재방문 유도</h3>
</div>
<p class="purpose-card__description">
기존 고객의 재방문을 늘리고 충성도를 높이고 싶어요
</p>
<div class="purpose-card__examples">
예: 스탬프 적립, 재방문 쿠폰
</div>
</div>
</label>
<label class="purpose-card" data-purpose="sales-boost">
<input type="radio" name="purpose" value="sales-boost" class="purpose-card__radio">
<div class="purpose-card__content">
<div class="purpose-card__header">
<span class="purpose-card__icon">💰</span>
<h3 class="purpose-card__title">매출 증대</h3>
</div>
<p class="purpose-card__description">
객단가를 올리고 매출을 늘리고 싶어요
</p>
<div class="purpose-card__examples">
예: 세트 할인, 한정 특가 이벤트
</div>
</div>
</label>
<label class="purpose-card" data-purpose="brand-awareness">
<input type="radio" name="purpose" value="brand-awareness" class="purpose-card__radio">
<div class="purpose-card__content">
<div class="purpose-card__header">
<span class="purpose-card__icon"></span>
<h3 class="purpose-card__title">브랜드 인지도</h3>
</div>
<p class="purpose-card__description">
매장을 더 많은 사람에게 알리고 SNS에서 화제가 되고 싶어요
</p>
<div class="purpose-card__examples">
예: SNS 인증샷, 바이럴 이벤트
</div>
</div>
</label>
<label class="purpose-card" data-purpose="engagement">
<input type="radio" name="purpose" value="engagement" class="purpose-card__radio">
<div class="purpose-card__content">
<div class="purpose-card__header">
<span class="purpose-card__icon">🎉</span>
<h3 class="purpose-card__title">고객 참여 활성화</h3>
</div>
<p class="purpose-card__description">
고객과 더 많이 소통하고 커뮤니티를 만들고 싶어요
</p>
<div class="purpose-card__examples">
예: 리뷰 이벤트, 고객 투표
</div>
</div>
</label>
<label class="purpose-card" data-purpose="seasonal">
<input type="radio" name="purpose" value="seasonal" class="purpose-card__radio">
<div class="purpose-card__content">
<div class="purpose-card__header">
<span class="purpose-card__icon">🎊</span>
<h3 class="purpose-card__title">시즌/기념일 마케팅</h3>
</div>
<p class="purpose-card__description">
계절이나 기념일에 맞는 특별한 이벤트를 하고 싶어요
</p>
<div class="purpose-card__examples">
예: 크리스마스, 창립기념일 이벤트
</div>
</div>
</label>
</div>
</div>
</main>
<!-- CTA Section -->
<div class="cta-section">
<div class="cta-section__inner">
<button
type="button"
id="nextButton"
class="btn btn-primary btn-large btn-block"
disabled
>
AI 트렌드 분석 시작
</button>
</div>
</div>
<!-- Bottom Navigation -->
<nav class="bottom-nav">
<a href="03-홈화면.html" class="bottom-nav__item">
<svg class="bottom-nav__icon" width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
<path d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z"/>
</svg>
<span class="bottom-nav__label"></span>
</a>
<a href="25-이벤트목록.html" class="bottom-nav__item">
<svg class="bottom-nav__icon" width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
<path d="M19 3h-4.18C14.4 1.84 13.3 1 12 1c-1.3 0-2.4.84-2.82 2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-7 0c.55 0 1 .45 1 1s-.45 1-1 1-1-.45-1-1 .45-1 1-1zm2 14H7v-2h7v2zm3-4H7v-2h10v2zm0-4H7V7h10v2z"/>
</svg>
<span class="bottom-nav__label">이벤트</span>
</a>
<a href="04-이벤트목적선택.html" class="bottom-nav__item bottom-nav__item--active bottom-nav__item--primary">
<svg class="bottom-nav__icon" width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
<circle cx="12" cy="12" r="10"/>
<path d="M12 6v12M6 12h12" stroke="white" stroke-width="2" stroke-linecap="round"/>
</svg>
<span class="bottom-nav__label">만들기</span>
</a>
<a href="21-실시간대시보드.html" class="bottom-nav__item">
<svg class="bottom-nav__icon" width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
<path d="M3 13h8V3H3v10zm0 8h8v-6H3v6zm10 0h8V11h-8v10zm0-18v6h8V3h-8z"/>
</svg>
<span class="bottom-nav__label">분석</span>
</a>
<a href="26-마이페이지.html" class="bottom-nav__item">
<svg class="bottom-nav__icon" width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/>
</svg>
<span class="bottom-nav__label">MY</span>
</a>
</nav>
</div>
<!-- Scripts -->
<script src="js/common.js"></script>
<script src="js/navigation.js"></script>
<script>
(function() {
'use strict';
const { storage, addClass, removeClass } = window.KTEvent;
const purposeCards = document.querySelectorAll('.purpose-card');
const purposeOptions = document.querySelectorAll('input[name="purpose"]');
const nextButton = document.getElementById('nextButton');
const aiTip = document.getElementById('aiTip');
let selectedPurpose = null;
// Purpose-specific AI tips
const aiTips = {
'new-customers': '신규 고객 유치는 SNS 광고와 친구 추천이 가장 효과적입니다. 첫 방문 혜택을 강조하면 참여율이 평균 42% 증가합니다.',
'repeat-visits': '재방문 유도는 스탬프 적립과 회원 등급제가 효과적입니다. 3회 방문 고객의 재방문율은 첫 방문 대비 3.2배 높습니다.',
'sales-boost': '매출 증대는 세트 할인과 한정 수량 특가가 효과적입니다. 긴급성을 강조하면 평균 객단가가 28% 증가합니다.',
'brand-awareness': 'SNS 인증샷 이벤트는 바이럴 효과가 가장 높습니다. 해시태그를 활용하면 도달 범위가 평균 5배 증가합니다.',
'engagement': '고객 참여는 리뷰 이벤트와 투표가 효과적입니다. 참여형 이벤트는 브랜드 충성도를 47% 향상시킵니다.',
'seasonal': '시즌 이벤트는 2주 전부터 준비하면 효과가 가장 좋습니다. 연휴 직전 참여율이 평균 대비 65% 높습니다.'
};
// Get store data for personalized tips
const storeData = storage.get('storeData');
// Handle purpose card selection
purposeCards.forEach(card => {
card.addEventListener('click', () => {
// Remove selected class from all cards
purposeCards.forEach(c => removeClass(c, 'purpose-card--selected'));
// Add selected class to clicked card
addClass(card, 'purpose-card--selected');
// Get the selected purpose
const purpose = card.getAttribute('data-purpose');
selectedPurpose = purpose;
// Update radio button
const radio = card.querySelector('input[type="radio"]');
radio.checked = true;
// Update AI tip
if (aiTips[purpose]) {
aiTip.textContent = aiTips[purpose];
}
// Enable next button
nextButton.disabled = false;
});
});
// Handle radio button change (for keyboard navigation)
purposeOptions.forEach(option => {
option.addEventListener('change', () => {
const purpose = option.value;
const card = option.closest('.purpose-card');
purposeCards.forEach(c => removeClass(c, 'purpose-card--selected'));
addClass(card, 'purpose-card--selected');
selectedPurpose = purpose;
if (aiTips[purpose]) {
aiTip.textContent = aiTips[purpose];
}
nextButton.disabled = false;
});
});
// Handle next button click
nextButton.addEventListener('click', () => {
if (!selectedPurpose) return;
// Show loading state
nextButton.disabled = true;
nextButton.classList.add('btn-loading');
nextButton.textContent = 'AI 분석 중...';
// Save selected purpose
const eventDraft = storage.get('eventDraft', {});
eventDraft.purpose = selectedPurpose;
eventDraft.purposeSelectedAt = new Date().toISOString();
storage.set('eventDraft', eventDraft);
// Simulate AI analysis
setTimeout(() => {
window.location.href = '05-AI트렌드분석결과.html';
}, 1500);
});
// Auto-select "new-customers" if it's recommended
if (storeData && storeData.businessType) {
// Auto-select and highlight AI recommended option
const recommendedCard = document.querySelector('[data-purpose="new-customers"]');
if (recommendedCard) {
setTimeout(() => {
recommendedCard.scrollIntoView({ behavior: 'smooth', block: 'center' });
}, 500);
}
}
})();
</script>
</body>
</html>

View File

@ -0,0 +1,680 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="KT AI 기반 소상공인 이벤트 자동 생성 서비스 - AI 트렌드 분석 결과">
<title>AI 트렌드 분석 결과 - KT 이벤트 마케팅</title>
<!-- Pretendard Font -->
<link rel="stylesheet" as="style" crossorigin
href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/variable/pretendardvariable.min.css">
<!-- Stylesheets -->
<link rel="stylesheet" href="css/variables.css">
<link rel="stylesheet" href="css/reset.css">
<link rel="stylesheet" href="css/layout.css">
<link rel="stylesheet" href="css/common.css">
<link rel="stylesheet" href="css/components/buttons.css">
<link rel="stylesheet" href="css/components/cards.css">
<link rel="stylesheet" href="css/components/navigation.css">
<link rel="stylesheet" href="css/components/loaders.css">
<style>
/* AI Trend Analysis Specific Styles */
.trend-analysis {
padding-bottom: calc(var(--bottom-nav-height) + 80px);
}
.analysis-loading {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 60vh;
padding: var(--spacing-2xl);
}
.analysis-loading__icon {
font-size: 80px;
margin-bottom: var(--spacing-l);
animation: pulse 2s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.1); opacity: 0.8; }
}
.analysis-loading__title {
font-size: var(--font-size-h2);
font-weight: var(--font-weight-bold);
color: var(--color-black);
margin-bottom: var(--spacing-s);
text-align: center;
}
.analysis-loading__subtitle {
font-size: var(--font-size-body-medium);
color: var(--color-gray-600);
text-align: center;
margin-bottom: var(--spacing-xl);
}
.analysis-results {
display: none;
padding: var(--spacing-l);
max-width: 800px;
margin: 0 auto;
animation: fadeIn var(--transition-normal) var(--ease-out);
}
.results-header {
text-align: center;
margin-bottom: var(--spacing-2xl);
padding: var(--spacing-xl) 0;
background: linear-gradient(135deg, var(--color-secondary-light) 0%, #E3F2FD 100%);
border-radius: var(--radius-m);
}
.results-header__icon {
font-size: 56px;
margin-bottom: var(--spacing-m);
}
.results-header__title {
font-size: var(--font-size-h1);
font-weight: var(--font-weight-bold);
color: var(--color-secondary);
margin-bottom: var(--spacing-s);
}
.results-header__subtitle {
font-size: var(--font-size-body-large);
color: var(--color-gray-700);
}
.section-title {
font-size: var(--font-size-h3);
font-weight: var(--font-weight-semibold);
color: var(--color-black);
margin-bottom: var(--spacing-l);
display: flex;
align-items: center;
gap: var(--spacing-s);
}
.section-title__icon {
font-size: 24px;
}
.insight-cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: var(--spacing-m);
margin-bottom: var(--spacing-2xl);
}
.insight-card {
background-color: var(--color-white);
border: 1px solid var(--color-gray-300);
border-radius: var(--radius-m);
padding: var(--spacing-l);
}
.insight-card__header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: var(--spacing-m);
}
.insight-card__title {
font-size: var(--font-size-body-large);
font-weight: var(--font-weight-semibold);
color: var(--color-gray-700);
}
.insight-card__icon {
font-size: 24px;
}
.insight-card__value {
font-size: var(--font-size-display);
font-weight: var(--font-weight-bold);
color: var(--color-primary);
margin-bottom: var(--spacing-xs);
}
.insight-card__description {
font-size: var(--font-size-body-small);
color: var(--color-gray-600);
}
.trend-list {
display: flex;
flex-direction: column;
gap: var(--spacing-m);
margin-bottom: var(--spacing-2xl);
}
.trend-item {
background-color: var(--color-white);
border: 1px solid var(--color-gray-300);
border-radius: var(--radius-m);
padding: var(--spacing-l);
}
.trend-item__header {
display: flex;
align-items: flex-start;
gap: var(--spacing-m);
margin-bottom: var(--spacing-m);
}
.trend-item__rank {
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
background: linear-gradient(135deg, var(--color-primary) 0%, var(--color-primary-dark) 100%);
color: var(--color-white);
font-size: var(--font-size-h3);
font-weight: var(--font-weight-bold);
border-radius: var(--radius-s);
flex-shrink: 0;
}
.trend-item__content {
flex: 1;
}
.trend-item__title {
font-size: var(--font-size-h3);
font-weight: var(--font-weight-semibold);
color: var(--color-black);
margin-bottom: var(--spacing-s);
}
.trend-item__description {
font-size: var(--font-size-body-medium);
color: var(--color-gray-600);
line-height: 1.6;
margin-bottom: var(--spacing-m);
}
.trend-item__stats {
display: flex;
gap: var(--spacing-l);
flex-wrap: wrap;
}
.trend-stat {
display: flex;
align-items: center;
gap: var(--spacing-xs);
font-size: var(--font-size-body-small);
color: var(--color-gray-600);
}
.trend-stat__label {
color: var(--color-gray-500);
}
.trend-stat__value {
font-weight: var(--font-weight-semibold);
color: var(--color-primary);
}
.ai-recommendation {
background: linear-gradient(135deg, #FFF5F5 0%, #FFE5E5 100%);
border: 2px solid var(--color-primary-light);
border-radius: var(--radius-m);
padding: var(--spacing-xl);
margin-bottom: var(--spacing-2xl);
}
.ai-recommendation__header {
display: flex;
align-items: center;
gap: var(--spacing-m);
margin-bottom: var(--spacing-l);
}
.ai-recommendation__icon {
font-size: 32px;
}
.ai-recommendation__title {
font-size: var(--font-size-h2);
font-weight: var(--font-weight-bold);
color: var(--color-primary);
}
.ai-recommendation__content {
font-size: var(--font-size-body-large);
color: var(--color-gray-800);
line-height: 1.8;
margin-bottom: var(--spacing-l);
}
.ai-recommendation__tags {
display: flex;
gap: var(--spacing-s);
flex-wrap: wrap;
}
.ai-recommendation__tag {
padding: 6px 12px;
background-color: var(--color-white);
color: var(--color-primary);
font-size: var(--font-size-body-small);
font-weight: var(--font-weight-semibold);
border-radius: var(--radius-s);
border: 1px solid var(--color-primary);
}
.cta-section {
position: fixed;
bottom: var(--bottom-nav-height);
left: 0;
right: 0;
background-color: var(--color-white);
padding: var(--spacing-m) var(--spacing-l);
border-top: 1px solid var(--color-gray-300);
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.08);
}
.cta-section__inner {
max-width: 800px;
margin: 0 auto;
}
</style>
</head>
<body>
<div class="page">
<!-- App Bar -->
<header class="app-bar">
<div class="app-bar__container">
<div class="app-bar__left">
<svg class="app-bar__back" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path d="M19 12H5M12 19l-7-7 7-7" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>
<h1 class="app-bar__title">AI 트렌드 분석</h1>
<div class="app-bar__right"></div>
</div>
</header>
<!-- Main Content -->
<main class="page-main trend-analysis">
<!-- Loading State -->
<div class="analysis-loading" id="loadingState">
<div class="analysis-loading__icon">🤖</div>
<h2 class="analysis-loading__title">AI가 트렌드를 분석하고 있어요</h2>
<p class="analysis-loading__subtitle">
최신 데이터를 기반으로<br>
업종별 성공 사례를 분석 중입니다
</p>
<div class="ai-progress">
<div class="ai-progress__bar">
<div class="ai-progress__fill" id="progressBar"></div>
</div>
<div class="ai-progress__text" id="progressText">데이터 수집 중...</div>
</div>
</div>
<!-- Analysis Results -->
<div class="analysis-results" id="analysisResults">
<!-- Results Header -->
<div class="results-header">
<div class="results-header__icon"></div>
<h2 class="results-header__title">분석이 완료되었습니다!</h2>
<p class="results-header__subtitle" id="resultsSubtitle">신규 고객 유치를 위한 트렌드를 분석했어요</p>
</div>
<!-- Key Insights -->
<h3 class="section-title">
<span class="section-title__icon">📊</span>
<span>핵심 인사이트</span>
</h3>
<div class="insight-cards">
<div class="insight-card">
<div class="insight-card__header">
<span class="insight-card__title">평균 참여율</span>
<span class="insight-card__icon">📈</span>
</div>
<div class="insight-card__value" id="insightEngagement">42%</div>
<div class="insight-card__description">업계 평균 대비 +18%p 높은 수치</div>
</div>
<div class="insight-card">
<div class="insight-card__header">
<span class="insight-card__title">최적 기간</span>
<span class="insight-card__icon">📅</span>
</div>
<div class="insight-card__value" id="insightDuration">7일</div>
<div class="insight-card__description">참여율이 가장 높은 이벤트 기간</div>
</div>
<div class="insight-card">
<div class="insight-card__header">
<span class="insight-card__title">ROI</span>
<span class="insight-card__icon">💰</span>
</div>
<div class="insight-card__value" id="insightROI">3.2배</div>
<div class="insight-card__description">평균 투자 대비 수익률</div>
</div>
</div>
<!-- Top Trends -->
<h3 class="section-title">
<span class="section-title__icon">🔥</span>
<span>인기 이벤트 유형 TOP 3</span>
</h3>
<div class="trend-list" id="trendList">
<!-- Trends will be dynamically added -->
</div>
<!-- AI Recommendation -->
<div class="ai-recommendation">
<div class="ai-recommendation__header">
<span class="ai-recommendation__icon">🤖</span>
<h3 class="ai-recommendation__title">AI 추천</h3>
</div>
<p class="ai-recommendation__content" id="aiRecommendation">
현재 트렌드를 분석한 결과, "친구 초대 이벤트"가 가장 효과적입니다. SNS 공유를 통한 바이럴 효과가 뛰어나며, 신규 고객 유치율이 평균 3.2배 높습니다.
</p>
<div class="ai-recommendation__tags">
<span class="ai-recommendation__tag">#신규고객유치</span>
<span class="ai-recommendation__tag">#바이럴효과</span>
<span class="ai-recommendation__tag">#SNS마케팅</span>
</div>
</div>
</div>
</main>
<!-- CTA Section -->
<div class="cta-section" style="display: none;" id="ctaSection">
<div class="cta-section__inner">
<button
type="button"
id="nextButton"
class="btn btn-primary btn-large btn-block"
>
이벤트 상품 추천 받기
</button>
</div>
</div>
<!-- Bottom Navigation -->
<nav class="bottom-nav">
<a href="03-홈화면.html" class="bottom-nav__item">
<svg class="bottom-nav__icon" width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
<path d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z"/>
</svg>
<span class="bottom-nav__label"></span>
</a>
<a href="25-이벤트목록.html" class="bottom-nav__item">
<svg class="bottom-nav__icon" width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
<path d="M19 3h-4.18C14.4 1.84 13.3 1 12 1c-1.3 0-2.4.84-2.82 2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-7 0c.55 0 1 .45 1 1s-.45 1-1 1-1-.45-1-1 .45-1 1-1zm2 14H7v-2h7v2zm3-4H7v-2h10v2zm0-4H7V7h10v2z"/>
</svg>
<span class="bottom-nav__label">이벤트</span>
</a>
<a href="04-이벤트목적선택.html" class="bottom-nav__item bottom-nav__item--active bottom-nav__item--primary">
<svg class="bottom-nav__icon" width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
<circle cx="12" cy="12" r="10"/>
<path d="M12 6v12M6 12h12" stroke="white" stroke-width="2" stroke-linecap="round"/>
</svg>
<span class="bottom-nav__label">만들기</span>
</a>
<a href="21-실시간대시보드.html" class="bottom-nav__item">
<svg class="bottom-nav__icon" width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
<path d="M3 13h8V3H3v10zm0 8h8v-6H3v6zm10 0h8V11h-8v10zm0-18v6h8V3h-8z"/>
</svg>
<span class="bottom-nav__label">분석</span>
</a>
<a href="26-마이페이지.html" class="bottom-nav__item">
<svg class="bottom-nav__icon" width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/>
</svg>
<span class="bottom-nav__label">MY</span>
</a>
</nav>
</div>
<!-- Scripts -->
<script src="js/common.js"></script>
<script src="js/navigation.js"></script>
<script>
(function() {
'use strict';
const { storage, createElement } = window.KTEvent;
// Get event draft data
const eventDraft = storage.get('eventDraft', {});
const storeData = storage.get('storeData', {});
const purpose = eventDraft.purpose || 'new-customers';
// Purpose-specific data
const trendData = {
'new-customers': {
subtitle: '신규 고객 유치를 위한 트렌드를 분석했어요',
insights: {
engagement: '42%',
duration: '7일',
roi: '3.2배'
},
trends: [
{
rank: 1,
title: '친구 초대 이벤트',
description: 'SNS 공유를 통한 바이럴 효과가 뛰어나며, 참여자의 평균 68%가 친구와 함께 방문합니다.',
engagementRate: '68%',
avgParticipants: '1,200명',
roi: '3.5배'
},
{
rank: 2,
title: '첫 방문 할인',
description: '신규 고객 대상 할인으로 방문 장벽을 낮추고, 재방문율도 42%로 높은 편입니다.',
engagementRate: '52%',
avgParticipants: '980명',
roi: '2.8배'
},
{
rank: 3,
title: 'SNS 인증샷 이벤트',
description: '해시태그를 활용한 바이럴 마케팅으로 브랜드 노출도가 평균 5배 증가합니다.',
engagementRate: '45%',
avgParticipants: '850명',
roi: '3.1배'
}
],
recommendation: '현재 트렌드를 분석한 결과, "친구 초대 이벤트"가 가장 효과적입니다. SNS 공유를 통한 바이럴 효과가 뛰어나며, 신규 고객 유치율이 평균 3.2배 높습니다.',
tags: ['#신규고객유치', '#바이럴효과', '#SNS마케팅']
},
'repeat-visits': {
subtitle: '재방문 유도를 위한 트렌드를 분석했어요',
insights: {
engagement: '58%',
duration: '30일',
roi: '4.1배'
},
trends: [
{
rank: 1,
title: '스탬프 적립 이벤트',
description: '방문할 때마다 스탬프를 적립하여 혜택을 제공하는 방식으로, 재방문율이 75%에 달합니다.',
engagementRate: '75%',
avgParticipants: '2,100명',
roi: '4.5배'
},
{
rank: 2,
title: '회원 등급제',
description: '방문 횟수에 따라 등급이 올라가며, 고객 충성도가 3.8배 증가합니다.',
engagementRate: '62%',
avgParticipants: '1,850명',
roi: '4.2배'
},
{
rank: 3,
title: '재방문 쿠폰',
description: '첫 방문 후 재방문 시 사용 가능한 쿠폰으로, 1주일 내 재방문율이 48%입니다.',
engagementRate: '48%',
avgParticipants: '1,650명',
roi: '3.6배'
}
],
recommendation: '스탬프 적립 이벤트가 재방문 유도에 가장 효과적입니다. 고객의 재방문율이 75%에 달하며, 장기적인 고객 관계 형성에 유리합니다.',
tags: ['#재방문유도', '#고객충성도', '#리워드프로그램']
},
'sales-boost': {
subtitle: '매출 증대를 위한 트렌드를 분석했어요',
insights: {
engagement: '51%',
duration: '3일',
roi: '5.3배'
},
trends: [
{
rank: 1,
title: '세트 할인',
description: '여러 상품을 묶어서 할인하는 방식으로, 평균 객단가가 38% 증가합니다.',
engagementRate: '65%',
avgParticipants: '1,450명',
roi: '5.8배'
},
{
rank: 2,
title: '한정 수량 특가',
description: '수량을 제한하여 긴급성을 강조하면, 구매 전환율이 52% 증가합니다.',
engagementRate: '58%',
avgParticipants: '1,320명',
roi: '5.2배'
},
{
rank: 3,
title: '타임 세일',
description: '시간대별 특가로 고객 집중도를 높이며, 매출이 평균 45% 증가합니다.',
engagementRate: '51%',
avgParticipants: '1,280명',
roi: '4.9배'
}
],
recommendation: '세트 할인 이벤트가 매출 증대에 가장 효과적입니다. 평균 객단가가 38% 증가하며, ROI가 5.8배로 가장 높습니다.',
tags: ['#매출증대', '#객단가상승', '#구매전환']
}
};
const currentData = trendData[purpose] || trendData['new-customers'];
// Loading animation
const loadingState = document.getElementById('loadingState');
const analysisResults = document.getElementById('analysisResults');
const ctaSection = document.getElementById('ctaSection');
const progressBar = document.getElementById('progressBar');
const progressText = document.getElementById('progressText');
const progressSteps = [
{ progress: 20, text: '데이터 수집 중...' },
{ progress: 40, text: '업종별 트렌드 분석 중...' },
{ progress: 60, text: '성공 사례 분석 중...' },
{ progress: 80, text: '맞춤형 인사이트 생성 중...' },
{ progress: 100, text: '분석 완료!' }
];
let currentStep = 0;
function updateProgress() {
if (currentStep >= progressSteps.length) {
// Show results
setTimeout(() => {
loadingState.style.display = 'none';
analysisResults.style.display = 'block';
ctaSection.style.display = 'block';
}, 500);
return;
}
const step = progressSteps[currentStep];
progressBar.style.width = step.progress + '%';
progressText.textContent = step.text;
currentStep++;
setTimeout(updateProgress, 800);
}
// Start loading animation
setTimeout(updateProgress, 500);
// Populate results
document.getElementById('resultsSubtitle').textContent = currentData.subtitle;
document.getElementById('insightEngagement').textContent = currentData.insights.engagement;
document.getElementById('insightDuration').textContent = currentData.insights.duration;
document.getElementById('insightROI').textContent = currentData.insights.roi;
document.getElementById('aiRecommendation').textContent = currentData.recommendation;
// Populate trends
const trendList = document.getElementById('trendList');
currentData.trends.forEach(trend => {
const trendItem = createElement('div', 'trend-item');
trendItem.innerHTML = `
<div class="trend-item__header">
<div class="trend-item__rank">${trend.rank}</div>
<div class="trend-item__content">
<h4 class="trend-item__title">${trend.title}</h4>
<p class="trend-item__description">${trend.description}</p>
<div class="trend-item__stats">
<div class="trend-stat">
<span class="trend-stat__label">참여율:</span>
<span class="trend-stat__value">${trend.engagementRate}</span>
</div>
<div class="trend-stat">
<span class="trend-stat__label">평균 참여자:</span>
<span class="trend-stat__value">${trend.avgParticipants}</span>
</div>
<div class="trend-stat">
<span class="trend-stat__label">ROI:</span>
<span class="trend-stat__value">${trend.roi}</span>
</div>
</div>
</div>
</div>
`;
trendList.appendChild(trendItem);
});
// Populate tags
const tagsContainer = document.querySelector('.ai-recommendation__tags');
tagsContainer.innerHTML = '';
currentData.tags.forEach(tag => {
const tagEl = createElement('span', 'ai-recommendation__tag');
tagEl.textContent = tag;
tagsContainer.appendChild(tagEl);
});
// Handle next button
document.getElementById('nextButton').addEventListener('click', () => {
// Save trend analysis to event draft
eventDraft.trendAnalysis = {
purpose,
insights: currentData.insights,
topTrend: currentData.trends[0].title,
recommendation: currentData.recommendation,
analyzedAt: new Date().toISOString()
};
storage.set('eventDraft', eventDraft);
window.location.href = '06-AI이벤트상품추천.html';
});
})();
</script>
</body>
</html>

View File

@ -0,0 +1,602 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="KT AI 기반 소상공인 이벤트 자동 생성 서비스 - AI 이벤트 상품 추천">
<title>AI 이벤트 상품 추천 - KT 이벤트 마케팅</title>
<!-- Pretendard Font -->
<link rel="stylesheet" as="style" crossorigin
href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/variable/pretendardvariable.min.css">
<!-- Stylesheets -->
<link rel="stylesheet" href="css/variables.css">
<link rel="stylesheet" href="css/reset.css">
<link rel="stylesheet" href="css/layout.css">
<link rel="stylesheet" href="css/common.css">
<link rel="stylesheet" href="css/components/buttons.css">
<link rel="stylesheet" href="css/components/cards.css">
<link rel="stylesheet" href="css/components/navigation.css">
<style>
/* AI Prize Recommendation Specific Styles */
.prize-recommendation {
padding: var(--spacing-l);
padding-bottom: calc(var(--bottom-nav-height) + 80px);
max-width: 800px;
margin: 0 auto;
}
.intro-section {
text-align: center;
margin-bottom: var(--spacing-2xl);
}
.intro-section__icon {
font-size: 64px;
margin-bottom: var(--spacing-m);
}
.intro-section__title {
font-size: var(--font-size-h1);
font-weight: var(--font-weight-bold);
color: var(--color-black);
margin-bottom: var(--spacing-s);
}
.intro-section__subtitle {
font-size: var(--font-size-body-medium);
color: var(--color-gray-600);
line-height: 1.6;
}
.section-title {
font-size: var(--font-size-h3);
font-weight: var(--font-weight-semibold);
color: var(--color-black);
margin-bottom: var(--spacing-l);
display: flex;
align-items: center;
gap: var(--spacing-s);
}
.section-title__icon {
font-size: 24px;
}
.section-subtitle {
font-size: var(--font-size-body-medium);
color: var(--color-gray-600);
margin-top: calc(-1 * var(--spacing-m));
margin-bottom: var(--spacing-l);
}
.prize-options {
display: flex;
flex-direction: column;
gap: var(--spacing-m);
margin-bottom: var(--spacing-2xl);
}
.prize-card {
position: relative;
background-color: var(--color-white);
border: 2px solid var(--color-gray-300);
border-radius: var(--radius-m);
padding: var(--spacing-l);
cursor: pointer;
transition: all var(--transition-fast) var(--ease-out);
}
.prize-card:hover {
border-color: var(--color-primary);
box-shadow: var(--shadow-md);
transform: translateY(-2px);
}
.prize-card--selected {
border-color: var(--color-primary);
background-color: #FFF5F5;
box-shadow: var(--shadow-primary);
}
.prize-card--recommended {
border-color: var(--color-secondary);
background: linear-gradient(135deg, #F0F7FF 0%, #FFFFFF 100%);
}
.prize-card__badge {
position: absolute;
top: -12px;
right: var(--spacing-l);
padding: 6px 16px;
background: linear-gradient(135deg, var(--color-secondary) 0%, var(--color-secondary-dark) 100%);
color: var(--color-white);
font-size: var(--font-size-body-small);
font-weight: var(--font-weight-semibold);
border-radius: var(--radius-s);
box-shadow: var(--shadow-md);
}
.prize-card__header {
display: flex;
align-items: flex-start;
gap: var(--spacing-m);
margin-bottom: var(--spacing-m);
}
.prize-card__icon {
font-size: 48px;
flex-shrink: 0;
}
.prize-card__content {
flex: 1;
}
.prize-card__title {
font-size: var(--font-size-h2);
font-weight: var(--font-weight-bold);
color: var(--color-black);
margin-bottom: var(--spacing-xs);
}
.prize-card__type {
font-size: var(--font-size-body-small);
color: var(--color-gray-600);
margin-bottom: var(--spacing-m);
}
.prize-card__description {
font-size: var(--font-size-body-medium);
color: var(--color-gray-700);
line-height: 1.6;
margin-bottom: var(--spacing-m);
}
.prize-card__stats {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: var(--spacing-m);
padding: var(--spacing-m);
background-color: rgba(255, 255, 255, 0.8);
border-radius: var(--radius-s);
}
.prize-stat {
text-align: center;
}
.prize-stat__value {
font-size: var(--font-size-h3);
font-weight: var(--font-weight-bold);
color: var(--color-primary);
margin-bottom: var(--spacing-xs);
}
.prize-stat__label {
font-size: var(--font-size-body-small);
color: var(--color-gray-600);
}
.prize-card__pros {
margin-top: var(--spacing-m);
padding: var(--spacing-m);
background-color: rgba(0, 200, 83, 0.05);
border-left: 3px solid var(--color-success);
border-radius: var(--radius-s);
}
.prize-card__pros-title {
font-size: var(--font-size-body-small);
font-weight: var(--font-weight-semibold);
color: var(--color-success);
margin-bottom: var(--spacing-s);
}
.prize-card__pros-list {
list-style: none;
padding: 0;
margin: 0;
}
.prize-card__pros-list li {
font-size: var(--font-size-body-small);
color: var(--color-gray-700);
padding-left: var(--spacing-m);
position: relative;
margin-bottom: var(--spacing-xs);
}
.prize-card__pros-list li::before {
content: '✓';
position: absolute;
left: 0;
color: var(--color-success);
font-weight: var(--font-weight-bold);
}
.custom-prize-section {
background-color: var(--color-gray-100);
border-radius: var(--radius-m);
padding: var(--spacing-l);
margin-bottom: var(--spacing-2xl);
}
.custom-prize-section__title {
font-size: var(--font-size-h3);
font-weight: var(--font-weight-semibold);
color: var(--color-black);
margin-bottom: var(--spacing-m);
display: flex;
align-items: center;
gap: var(--spacing-s);
}
.custom-prize-input {
width: 100%;
padding: var(--spacing-m);
border: 1px solid var(--color-gray-300);
border-radius: var(--radius-s);
font-size: var(--font-size-body-medium);
transition: all var(--transition-fast) var(--ease-out);
}
.custom-prize-input:focus {
outline: none;
border-color: var(--color-secondary);
box-shadow: 0 0 0 4px rgba(0, 102, 255, 0.1);
}
.cta-section {
position: fixed;
bottom: var(--bottom-nav-height);
left: 0;
right: 0;
background-color: var(--color-white);
padding: var(--spacing-m) var(--spacing-l);
border-top: 1px solid var(--color-gray-300);
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.08);
}
.cta-section__inner {
max-width: 800px;
margin: 0 auto;
}
</style>
</head>
<body>
<div class="page">
<!-- App Bar -->
<header class="app-bar">
<div class="app-bar__container">
<div class="app-bar__left">
<svg class="app-bar__back" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path d="M19 12H5M12 19l-7-7 7-7" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>
<h1 class="app-bar__title">AI 상품 추천</h1>
<div class="app-bar__right"></div>
</div>
</header>
<!-- Main Content -->
<main class="page-main">
<div class="prize-recommendation">
<!-- Intro Section -->
<div class="intro-section">
<div class="intro-section__icon">🎁</div>
<h2 class="intro-section__title">어떤 혜택을 제공하시겠어요?</h2>
<p class="intro-section__subtitle">
AI가 분석한 결과를 바탕으로<br>
가장 효과적인 이벤트 상품을 추천해드립니다
</p>
</div>
<!-- Prize Options -->
<h3 class="section-title">
<span class="section-title__icon"></span>
<span>AI 추천 혜택</span>
</h3>
<p class="section-subtitle">트렌드 분석을 바탕으로 가장 효과적인 혜택을 선별했어요</p>
<div class="prize-options" id="prizeOptions">
<!-- AI Recommended -->
<label class="prize-card prize-card--recommended" data-prize="friend-invite">
<span class="prize-card__badge">🤖 AI 추천</span>
<div class="prize-card__header">
<span class="prize-card__icon">👥</span>
<div class="prize-card__content">
<h4 class="prize-card__title">친구 초대 특전</h4>
<p class="prize-card__type">추천형 · 바이럴 효과</p>
</div>
<input type="radio" name="prize" value="friend-invite" class="prize-card__radio" style="display: none;">
</div>
<p class="prize-card__description">
친구를 초대한 고객과 초대받은 친구 모두에게 혜택을 제공하는 방식입니다. SNS 공유를 통한 바이럴 효과가 뛰어나며, 신규 고객 유치에 가장 효과적입니다.
</p>
<div class="prize-card__stats">
<div class="prize-stat">
<div class="prize-stat__value">68%</div>
<div class="prize-stat__label">참여율</div>
</div>
<div class="prize-stat">
<div class="prize-stat__value">3.5배</div>
<div class="prize-stat__label">ROI</div>
</div>
<div class="prize-stat">
<div class="prize-stat__value">1,200명</div>
<div class="prize-stat__label">평균 참여자</div>
</div>
</div>
<div class="prize-card__pros">
<div class="prize-card__pros-title">✓ 주요 장점</div>
<ul class="prize-card__pros-list">
<li>바이럴 효과로 빠른 확산 가능</li>
<li>신규 고객 유치율 평균 3.2배</li>
<li>고객 획득 비용 40% 절감</li>
</ul>
</div>
</label>
<!-- Option 2 -->
<label class="prize-card" data-prize="discount">
<div class="prize-card__header">
<span class="prize-card__icon">💰</span>
<div class="prize-card__content">
<h4 class="prize-card__title">할인 쿠폰</h4>
<p class="prize-card__type">즉시 혜택형 · 전환율 높음</p>
</div>
<input type="radio" name="prize" value="discount" class="prize-card__radio" style="display: none;">
</div>
<p class="prize-card__description">
즉시 사용 가능한 할인 쿠폰을 제공합니다. 구매 전환율이 높으며, 객단가 상승 효과도 기대할 수 있습니다.
</p>
<div class="prize-card__stats">
<div class="prize-stat">
<div class="prize-stat__value">52%</div>
<div class="prize-stat__label">참여율</div>
</div>
<div class="prize-stat">
<div class="prize-stat__value">2.8배</div>
<div class="prize-stat__label">ROI</div>
</div>
<div class="prize-stat">
<div class="prize-stat__value">980명</div>
<div class="prize-stat__label">평균 참여자</div>
</div>
</div>
<div class="prize-card__pros">
<div class="prize-card__pros-title">✓ 주요 장점</div>
<ul class="prize-card__pros-list">
<li>즉시 구매 전환 유도</li>
<li>객단가 평균 28% 상승</li>
<li>설정 및 관리 간편</li>
</ul>
</div>
</label>
<!-- Option 3 -->
<label class="prize-card" data-prize="freebie">
<div class="prize-card__header">
<span class="prize-card__icon">🎉</span>
<div class="prize-card__content">
<h4 class="prize-card__title">무료 증정</h4>
<p class="prize-card__type">체험형 · 고객 만족도 높음</p>
</div>
<input type="radio" name="prize" value="freebie" class="prize-card__radio" style="display: none;">
</div>
<p class="prize-card__description">
인기 상품이나 신메뉴를 무료로 체험할 수 있는 기회를 제공합니다. 고객 만족도가 높고 재방문율 증가에 효과적입니다.
</p>
<div class="prize-card__stats">
<div class="prize-stat">
<div class="prize-stat__value">48%</div>
<div class="prize-stat__label">참여율</div>
</div>
<div class="prize-stat">
<div class="prize-stat__value">3.1배</div>
<div class="prize-stat__label">ROI</div>
</div>
<div class="prize-stat">
<div class="prize-stat__value">850명</div>
<div class="prize-stat__label">평균 참여자</div>
</div>
</div>
<div class="prize-card__pros">
<div class="prize-card__pros-title">✓ 주요 장점</div>
<ul class="prize-card__pros-list">
<li>고객 만족도 매우 높음</li>
<li>재방문율 평균 42% 증가</li>
<li>긍정적 브랜드 이미지 형성</li>
</ul>
</div>
</label>
</div>
<!-- Custom Prize Section -->
<div class="custom-prize-section">
<h3 class="custom-prize-section__title">
<span>✏️</span>
<span>직접 입력하기</span>
</h3>
<input
type="text"
id="customPrize"
class="custom-prize-input"
placeholder="예: 아메리카노 1잔 무료, 10% 할인권, 케이크 증정"
maxlength="100"
>
<span class="form-helper mt-s">원하는 혜택을 직접 입력할 수도 있어요</span>
</div>
</div>
</main>
<!-- CTA Section -->
<div class="cta-section">
<div class="cta-section__inner">
<button
type="button"
id="nextButton"
class="btn btn-primary btn-large btn-block"
disabled
>
참여 방법 설계하기
</button>
</div>
</div>
<!-- Bottom Navigation -->
<nav class="bottom-nav">
<a href="03-홈화면.html" class="bottom-nav__item">
<svg class="bottom-nav__icon" width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
<path d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z"/>
</svg>
<span class="bottom-nav__label"></span>
</a>
<a href="25-이벤트목록.html" class="bottom-nav__item">
<svg class="bottom-nav__icon" width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
<path d="M19 3h-4.18C14.4 1.84 13.3 1 12 1c-1.3 0-2.4.84-2.82 2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-7 0c.55 0 1 .45 1 1s-.45 1-1 1-1-.45-1-1 .45-1 1-1zm2 14H7v-2h7v2zm3-4H7v-2h10v2zm0-4H7V7h10v2z"/>
</svg>
<span class="bottom-nav__label">이벤트</span>
</a>
<a href="04-이벤트목적선택.html" class="bottom-nav__item bottom-nav__item--active bottom-nav__item--primary">
<svg class="bottom-nav__icon" width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
<circle cx="12" cy="12" r="10"/>
<path d="M12 6v12M6 12h12" stroke="white" stroke-width="2" stroke-linecap="round"/>
</svg>
<span class="bottom-nav__label">만들기</span>
</a>
<a href="21-실시간대시보드.html" class="bottom-nav__item">
<svg class="bottom-nav__icon" width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
<path d="M3 13h8V3H3v10zm0 8h8v-6H3v6zm10 0h8V11h-8v10zm0-18v6h8V3h-8z"/>
</svg>
<span class="bottom-nav__label">분석</span>
</a>
<a href="26-마이페이지.html" class="bottom-nav__item">
<svg class="bottom-nav__icon" width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/>
</svg>
<span class="bottom-nav__label">MY</span>
</a>
</nav>
</div>
<!-- Scripts -->
<script src="js/common.js"></script>
<script src="js/navigation.js"></script>
<script>
(function() {
'use strict';
const { storage, addClass, removeClass } = window.KTEvent;
const prizeCards = document.querySelectorAll('.prize-card');
const prizeOptions = document.querySelectorAll('input[name="prize"]');
const customPrizeInput = document.getElementById('customPrize');
const nextButton = document.getElementById('nextButton');
let selectedPrize = null;
let customPrize = null;
// Handle prize card selection
prizeCards.forEach(card => {
card.addEventListener('click', () => {
// Clear custom input
customPrizeInput.value = '';
customPrize = null;
// Remove selected class from all cards
prizeCards.forEach(c => removeClass(c, 'prize-card--selected'));
// Add selected class to clicked card
addClass(card, 'prize-card--selected');
// Get the selected prize
const prize = card.getAttribute('data-prize');
selectedPrize = prize;
// Update radio button
const radio = card.querySelector('input[type="radio"]');
radio.checked = true;
// Enable next button
nextButton.disabled = false;
});
});
// Handle radio button change (for keyboard navigation)
prizeOptions.forEach(option => {
option.addEventListener('change', () => {
const prize = option.value;
const card = option.closest('.prize-card');
customPrizeInput.value = '';
customPrize = null;
prizeCards.forEach(c => removeClass(c, 'prize-card--selected'));
addClass(card, 'prize-card--selected');
selectedPrize = prize;
nextButton.disabled = false;
});
});
// Handle custom prize input
customPrizeInput.addEventListener('input', (e) => {
const value = e.target.value.trim();
if (value) {
// Clear card selections
prizeCards.forEach(c => removeClass(c, 'prize-card--selected'));
prizeOptions.forEach(o => o.checked = false);
selectedPrize = null;
customPrize = value;
// Enable next button
nextButton.disabled = false;
} else {
customPrize = null;
if (!selectedPrize) {
nextButton.disabled = true;
}
}
});
// Handle next button click
nextButton.addEventListener('click', () => {
if (!selectedPrize && !customPrize) return;
// Show loading state
nextButton.disabled = true;
nextButton.classList.add('btn-loading');
// Save selected prize
const eventDraft = storage.get('eventDraft', {});
eventDraft.prize = {
type: selectedPrize,
custom: customPrize,
selectedAt: new Date().toISOString()
};
storage.set('eventDraft', eventDraft);
// Navigate to next screen
setTimeout(() => {
window.location.href = '07-AI참여방법설계.html';
}, 800);
});
// Auto-select AI recommended option
setTimeout(() => {
const recommendedCard = document.querySelector('[data-prize="friend-invite"]');
if (recommendedCard) {
recommendedCard.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}, 500);
})();
</script>
</body>
</html>

View File

@ -0,0 +1,859 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI 참여 방법 설계 - KT AI 이벤트 플랫폼</title>
<style>
/* CSS Reset */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
/* Design System Variables */
:root {
/* Colors */
--color-primary: #E31E24;
--color-primary-hover: #C71820;
--color-primary-light: #FF4D52;
--color-secondary: #0066FF;
--color-secondary-hover: #004DBF;
--color-secondary-light: #4D94FF;
--color-success: #00C853;
--color-warning: #FFA000;
--color-error: #D32F2F;
--color-info: #0288D1;
--color-gray-50: #F8F9FA;
--color-gray-100: #F1F3F5;
--color-gray-200: #E9ECEF;
--color-gray-300: #DEE2E6;
--color-gray-400: #CED4DA;
--color-gray-500: #ADB5BD;
--color-gray-600: #868E96;
--color-gray-700: #495057;
--color-gray-800: #343A40;
--color-gray-900: #212529;
--color-white: #FFFFFF;
--color-black: #000000;
/* Typography */
--font-family: 'Pretendard Variable', -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
/* Spacing (4px grid) */
--spacing-xs: 4px;
--spacing-s: 8px;
--spacing-m: 16px;
--spacing-l: 24px;
--spacing-xl: 32px;
--spacing-2xl: 48px;
/* Border Radius */
--radius-s: 4px;
--radius-m: 8px;
--radius-l: 12px;
--radius-xl: 16px;
--radius-full: 9999px;
/* Shadows */
--shadow-s: 0 1px 2px rgba(0, 0, 0, 0.05);
--shadow-m: 0 4px 6px rgba(0, 0, 0, 0.07);
--shadow-l: 0 10px 15px rgba(0, 0, 0, 0.1);
--shadow-xl: 0 20px 25px rgba(0, 0, 0, 0.15);
/* Transitions */
--transition-fast: 150ms ease-in-out;
--transition-base: 250ms ease-in-out;
--transition-slow: 350ms ease-in-out;
/* Z-index */
--z-header: 100;
--z-modal: 200;
--z-toast: 300;
}
/* Base Styles */
body {
font-family: var(--font-family);
font-size: 16px;
line-height: 1.5;
color: var(--color-gray-900);
background: linear-gradient(135deg, #F8F9FF 0%, #FFFFFF 100%);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
min-height: 100vh;
}
/* Header */
.header {
background: linear-gradient(135deg, var(--color-secondary) 0%, var(--color-secondary-hover) 100%);
color: var(--color-white);
padding: var(--spacing-xl) var(--spacing-m);
box-shadow: var(--shadow-l);
}
.header__back {
background: none;
border: none;
color: var(--color-white);
font-size: 24px;
padding: 0;
margin-bottom: var(--spacing-m);
cursor: pointer;
transition: opacity var(--transition-fast);
}
.header__back:hover {
opacity: 0.8;
}
.header__title {
font-size: 24px;
font-weight: 700;
margin-bottom: var(--spacing-xs);
}
.header__subtitle {
font-size: 15px;
opacity: 0.9;
}
/* Main */
.main {
padding: var(--spacing-l) var(--spacing-m);
padding-bottom: var(--spacing-2xl);
}
/* AI Tip */
.ai-tip {
background: linear-gradient(135deg, #FFF9E6 0%, #FFFFFF 100%);
border-left: 4px solid #FFB800;
padding: var(--spacing-m);
border-radius: var(--radius-m);
margin-bottom: var(--spacing-l);
}
.ai-tip__header {
display: flex;
align-items: center;
gap: var(--spacing-s);
margin-bottom: var(--spacing-s);
}
.ai-tip__icon {
font-size: 20px;
}
.ai-tip__title {
font-size: 15px;
font-weight: 600;
color: var(--color-gray-900);
}
.ai-tip__content {
font-size: 14px;
color: var(--color-gray-700);
line-height: 1.6;
}
/* Methods Grid */
.methods-grid {
display: grid;
gap: var(--spacing-m);
margin-bottom: var(--spacing-l);
}
/* Method Card */
.method-card {
background: var(--color-white);
border: 2px solid var(--color-gray-200);
border-radius: var(--radius-l);
padding: var(--spacing-m);
cursor: pointer;
transition: all var(--transition-base);
position: relative;
}
.method-card:hover {
border-color: var(--color-secondary);
box-shadow: var(--shadow-m);
transform: translateY(-2px);
}
.method-card--selected {
border-color: var(--color-secondary);
background: linear-gradient(135deg, #F0F7FF 0%, #FFFFFF 100%);
box-shadow: var(--shadow-l);
}
.method-card--recommended {
border-color: var(--color-secondary);
background: linear-gradient(135deg, #F0F7FF 0%, #FFFFFF 100%);
}
.method-card__badge {
position: absolute;
top: -12px;
right: var(--spacing-l);
background: linear-gradient(135deg, var(--color-secondary) 0%, var(--color-secondary-hover) 100%);
color: var(--color-white);
padding: var(--spacing-xs) var(--spacing-m);
border-radius: var(--radius-s);
font-size: 12px;
font-weight: 600;
box-shadow: var(--shadow-m);
}
.method-card__header {
display: flex;
align-items: flex-start;
gap: var(--spacing-m);
margin-bottom: var(--spacing-m);
}
.method-card__radio {
width: 20px;
height: 20px;
margin-top: 2px;
cursor: pointer;
flex-shrink: 0;
}
.method-card__icon {
width: 48px;
height: 48px;
background: linear-gradient(135deg, var(--color-secondary-light) 0%, var(--color-secondary) 100%);
border-radius: var(--radius-m);
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
flex-shrink: 0;
}
.method-card__info {
flex: 1;
}
.method-card__title {
font-size: 16px;
font-weight: 600;
color: var(--color-gray-900);
margin-bottom: var(--spacing-xs);
}
.method-card__type {
display: inline-block;
font-size: 12px;
color: var(--color-secondary);
background: var(--color-secondary-light);
padding: 2px var(--spacing-s);
border-radius: var(--radius-s);
margin-bottom: var(--spacing-s);
}
.method-card__description {
font-size: 14px;
color: var(--color-gray-600);
line-height: 1.5;
margin-bottom: var(--spacing-m);
}
/* Stats */
.method-card__stats {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: var(--spacing-s);
margin-bottom: var(--spacing-m);
padding-top: var(--spacing-m);
border-top: 1px solid var(--color-gray-200);
}
.method-stat {
text-align: center;
}
.method-stat__value {
font-size: 15px;
font-weight: 700;
color: var(--color-secondary);
margin-bottom: var(--spacing-xs);
}
.method-stat__label {
font-size: 11px;
color: var(--color-gray-600);
}
/* Features */
.method-card__features {
background: linear-gradient(135deg, #F0FFF4 0%, #FFFFFF 100%);
border-left: 3px solid var(--color-success);
padding: var(--spacing-s);
border-radius: var(--radius-s);
}
.method-card__features-title {
font-size: 12px;
font-weight: 600;
color: var(--color-gray-900);
margin-bottom: var(--spacing-xs);
}
.method-card__features-list {
list-style: none;
padding: 0;
margin: 0;
}
.method-card__features-list li {
font-size: 12px;
color: var(--color-gray-700);
padding-left: var(--spacing-m);
position: relative;
margin-bottom: var(--spacing-xs);
}
.method-card__features-list li:before {
content: '✓';
position: absolute;
left: 0;
color: var(--color-success);
font-weight: 700;
}
.method-card__features-list li:last-child {
margin-bottom: 0;
}
/* Custom Method */
.custom-method {
background: var(--color-white);
border: 2px dashed var(--color-gray-200);
border-radius: var(--radius-l);
padding: var(--spacing-m);
margin-bottom: var(--spacing-l);
}
.custom-method__label {
display: block;
font-size: 15px;
font-weight: 600;
color: var(--color-gray-900);
margin-bottom: var(--spacing-s);
}
.custom-method__input {
width: 100%;
padding: var(--spacing-m);
border: 1px solid var(--color-gray-200);
border-radius: var(--radius-m);
font-size: 15px;
font-family: var(--font-family);
transition: all var(--transition-base);
}
.custom-method__input:focus {
outline: none;
border-color: var(--color-secondary);
box-shadow: 0 0 0 3px rgba(0, 102, 255, 0.1);
}
.custom-method__hint {
font-size: 12px;
color: var(--color-gray-500);
margin-top: var(--spacing-xs);
}
/* Next Button */
.next-button {
width: 100%;
padding: var(--spacing-m);
background: linear-gradient(135deg, var(--color-secondary) 0%, var(--color-secondary-hover) 100%);
color: var(--color-white);
border: none;
border-radius: var(--radius-m);
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all var(--transition-base);
box-shadow: var(--shadow-m);
}
.next-button:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: var(--shadow-l);
}
.next-button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.next-button--loading {
position: relative;
color: transparent;
}
.next-button--loading:after {
content: '';
position: absolute;
width: 20px;
height: 20px;
top: 50%;
left: 50%;
margin-left: -10px;
margin-top: -10px;
border: 3px solid rgba(255, 255, 255, 0.3);
border-radius: 50%;
border-top-color: var(--color-white);
animation: spinner 0.6s linear infinite;
}
@keyframes spinner {
to { transform: rotate(360deg); }
}
/* Responsive */
@media (min-width: 768px) {
.header {
padding: var(--spacing-2xl) var(--spacing-xl);
}
.main {
max-width: 720px;
margin: 0 auto;
padding: var(--spacing-xl);
}
.methods-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (min-width: 1024px) {
.main {
max-width: 960px;
}
}
</style>
</head>
<body>
<!-- Header -->
<header class="header">
<button class="header__back" onclick="history.back()"></button>
<h1 class="header__title">AI 참여 방법 설계</h1>
<p class="header__subtitle">고객이 이벤트에 참여하는 방법을 선택해 주세요</p>
</header>
<!-- Main -->
<main class="main">
<!-- AI Tip -->
<div class="ai-tip">
<div class="ai-tip__header">
<span class="ai-tip__icon">💡</span>
<h2 class="ai-tip__title">AI 추천</h2>
</div>
<p class="ai-tip__content">
<strong id="aiTipText">SNS 공유하기</strong>는 바이럴 효과가 뛰어나 신규 고객 유치에 가장 효과적입니다.
평균 2.3배의 참여자 증가 효과가 있으며, 설정도 간단합니다.
</p>
</div>
<!-- Methods Grid -->
<div class="methods-grid">
<!-- SNS Share - AI Recommended -->
<div class="method-card method-card--recommended" data-method="sns-share">
<div class="method-card__badge">🤖 AI 추천</div>
<div class="method-card__header">
<input type="radio" name="method" value="sns-share" class="method-card__radio">
<div class="method-card__icon">📱</div>
<div class="method-card__info">
<h3 class="method-card__title">SNS 공유하기</h3>
<span class="method-card__type">바이럴형 · 확산 효과 우수</span>
</div>
</div>
<p class="method-card__description">
고객이 이벤트를 SNS에 공유하면 참여 완료. 인스타그램, 페이스북 등 주요 SNS 플랫폼 지원
</p>
<div class="method-card__stats">
<div class="method-stat">
<div class="method-stat__value">75%</div>
<div class="method-stat__label">참여율</div>
</div>
<div class="method-stat">
<div class="method-stat__value">2.3배</div>
<div class="method-stat__label">바이럴 계수</div>
</div>
<div class="method-stat">
<div class="method-stat__value">쉬움</div>
<div class="method-stat__label">난이도</div>
</div>
</div>
<div class="method-card__features">
<h4 class="method-card__features-title">주요 장점</h4>
<ul class="method-card__features-list">
<li>자연스러운 바이럴 효과로 신규 고객 유입</li>
<li>추가 비용 없이 홍보 효과 극대화</li>
<li>간편한 참여로 높은 전환율</li>
</ul>
</div>
</div>
<!-- QR Code Scan -->
<div class="method-card" data-method="qr-scan">
<div class="method-card__header">
<input type="radio" name="method" value="qr-scan" class="method-card__radio">
<div class="method-card__icon">📷</div>
<div class="method-card__info">
<h3 class="method-card__title">QR 코드 스캔</h3>
<span class="method-card__type">오프라인형 · 매장 방문 유도</span>
</div>
</div>
<p class="method-card__description">
매장 내 QR 코드를 스캔하여 참여. 포스터, 테이블 스티커 등 다양한 형태로 활용 가능
</p>
<div class="method-card__stats">
<div class="method-stat">
<div class="method-stat__value">68%</div>
<div class="method-stat__label">참여율</div>
</div>
<div class="method-stat">
<div class="method-stat__value">1.2배</div>
<div class="method-stat__label">바이럴 계수</div>
</div>
<div class="method-stat">
<div class="method-stat__value">쉬움</div>
<div class="method-stat__label">난이도</div>
</div>
</div>
<div class="method-card__features">
<h4 class="method-card__features-title">주요 장점</h4>
<ul class="method-card__features-list">
<li>오프라인 매장 방문 고객 타겟팅</li>
<li>즉각적인 참여와 혜택 제공</li>
<li>누구나 쉽게 참여 가능</li>
</ul>
</div>
</div>
<!-- Coupon Download -->
<div class="method-card" data-method="coupon-download">
<div class="method-card__header">
<input type="radio" name="method" value="coupon-download" class="method-card__radio">
<div class="method-card__icon">🎫</div>
<div class="method-card__info">
<h3 class="method-card__title">쿠폰 다운로드</h3>
<span class="method-card__type">즉시 혜택형 · 전환율 높음</span>
</div>
</div>
<p class="method-card__description">
간단한 정보 입력 후 쿠폰 다운로드. 모바일 쿠폰으로 즉시 사용 가능
</p>
<div class="method-card__stats">
<div class="method-stat">
<div class="method-stat__value">82%</div>
<div class="method-stat__label">참여율</div>
</div>
<div class="method-stat">
<div class="method-stat__value">1.1배</div>
<div class="method-stat__label">바이럴 계수</div>
</div>
<div class="method-stat">
<div class="method-stat__value">쉬움</div>
<div class="method-stat__label">난이도</div>
</div>
</div>
<div class="method-card__features">
<h4 class="method-card__features-title">주요 장점</h4>
<ul class="method-card__features-list">
<li>즉시 혜택으로 높은 전환율</li>
<li>고객 정보 수집 용이</li>
<li>재방문 유도 효과</li>
</ul>
</div>
</div>
<!-- In-store Visit -->
<div class="method-card" data-method="store-visit">
<div class="method-card__header">
<input type="radio" name="method" value="store-visit" class="method-card__radio">
<div class="method-card__icon">🏪</div>
<div class="method-card__info">
<h3 class="method-card__title">매장 방문 인증</h3>
<span class="method-card__type">오프라인형 · 방문 유도</span>
</div>
</div>
<p class="method-card__description">
매장 방문 시 스탬프나 인증 사진 업로드로 참여. 실제 방문 고객 확보에 효과적
</p>
<div class="method-card__stats">
<div class="method-stat">
<div class="method-stat__value">58%</div>
<div class="method-stat__label">참여율</div>
</div>
<div class="method-stat">
<div class="method-stat__value">1.0배</div>
<div class="method-stat__label">바이럴 계수</div>
</div>
<div class="method-stat">
<div class="method-stat__value">보통</div>
<div class="method-stat__label">난이도</div>
</div>
</div>
<div class="method-card__features">
<h4 class="method-card__features-title">주요 장점</h4>
<ul class="method-card__features-list">
<li>확실한 매장 방문 고객 확보</li>
<li>직접적인 매출 증대 효과</li>
<li>단골 고객 형성에 유리</li>
</ul>
</div>
</div>
<!-- Survey Participation -->
<div class="method-card" data-method="survey">
<div class="method-card__header">
<input type="radio" name="method" value="survey" class="method-card__radio">
<div class="method-card__icon">📝</div>
<div class="method-card__info">
<h3 class="method-card__title">설문 참여</h3>
<span class="method-card__type">데이터 수집형 · 인사이트 확보</span>
</div>
</div>
<p class="method-card__description">
간단한 설문 작성 후 참여 완료. 고객 니즈 파악과 맞춤형 마케팅에 활용
</p>
<div class="method-card__stats">
<div class="method-stat">
<div class="method-stat__value">45%</div>
<div class="method-stat__label">참여율</div>
</div>
<div class="method-stat">
<div class="method-stat__value">0.8배</div>
<div class="method-stat__label">바이럴 계수</div>
</div>
<div class="method-stat">
<div class="method-stat__value">보통</div>
<div class="method-stat__label">난이도</div>
</div>
</div>
<div class="method-card__features">
<h4 class="method-card__features-title">주요 장점</h4>
<ul class="method-card__features-list">
<li>고객 선호도 및 니즈 파악</li>
<li>데이터 기반 의사결정 가능</li>
<li>타겟 마케팅 최적화</li>
</ul>
</div>
</div>
<!-- Friend Invite -->
<div class="method-card" data-method="friend-invite">
<div class="method-card__header">
<input type="radio" name="method" value="friend-invite" class="method-card__radio">
<div class="method-card__icon">👥</div>
<div class="method-card__info">
<h3 class="method-card__title">친구 초대</h3>
<span class="method-card__type">추천형 · 신규 고객 확보</span>
</div>
</div>
<p class="method-card__description">
친구를 초대하고 함께 혜택 받기. 추천인과 피추천인 모두 혜택 제공
</p>
<div class="method-card__stats">
<div class="method-stat">
<div class="method-stat__value">62%</div>
<div class="method-stat__label">참여율</div>
</div>
<div class="method-stat">
<div class="method-stat__value">3.5배</div>
<div class="method-stat__label">바이럴 계수</div>
</div>
<div class="method-stat">
<div class="method-stat__value">보통</div>
<div class="method-stat__label">난이도</div>
</div>
</div>
<div class="method-card__features">
<h4 class="method-card__features-title">주요 장점</h4>
<ul class="method-card__features-list">
<li>검증된 신규 고객 확보</li>
<li>강력한 추천 효과</li>
<li>Win-Win 혜택 구조</li>
</ul>
</div>
</div>
</div>
<!-- Custom Method Input -->
<div class="custom-method">
<label for="customMethod" class="custom-method__label">
💭 직접 입력하기
</label>
<input
type="text"
id="customMethod"
class="custom-method__input"
placeholder="예: 리뷰 작성하기, 인스타그램 팔로우, 해시태그 이벤트"
maxlength="100"
>
<p class="custom-method__hint">
원하는 참여 방법을 직접 입력할 수 있어요 (최대 100자)
</p>
</div>
<!-- Next Button -->
<button class="next-button" id="nextButton" disabled>
홍보 문구 생성하기
</button>
</main>
<script>
// Utils
const $ = (selector) => document.querySelector(selector);
const $$ = (selector) => document.querySelectorAll(selector);
const addClass = (el, className) => el?.classList.add(className);
const removeClass = (el, className) => el?.classList.remove(className);
// LocalStorage wrapper
const storage = {
get: (key) => {
try {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : null;
} catch (e) {
console.error('Storage get error:', e);
return null;
}
},
set: (key, value) => {
try {
localStorage.setItem(key, JSON.stringify(value));
} catch (e) {
console.error('Storage set error:', e);
}
}
};
// Elements
const methodCards = $$('.method-card');
const methodRadios = $$('input[name="method"]');
const customMethodInput = $('#customMethod');
const nextButton = $('#nextButton');
const aiTipText = $('#aiTipText');
// State
let selectedMethod = null;
let customMethod = null;
// AI Tips for different methods
const aiTips = {
'sns-share': 'SNS 공유하기는 바이럴 효과가 뛰어나 신규 고객 유치에 가장 효과적입니다. 평균 2.3배의 참여자 증가 효과가 있으며, 설정도 간단합니다.',
'qr-scan': 'QR 코드 스캔은 오프라인 매장 방문 고객을 타겟팅하기에 적합합니다. 즉각적인 참여와 혜택 제공이 가능합니다.',
'coupon-download': '쿠폰 다운로드는 즉시 혜택 제공으로 가장 높은 전환율을 보입니다. 고객 정보 수집과 재방문 유도에도 효과적입니다.',
'store-visit': '매장 방문 인증은 실제 방문 고객을 확보하여 직접적인 매출 증대에 효과적입니다. 단골 고객 형성에도 유리합니다.',
'survey': '설문 참여는 고객 니즈를 파악하고 데이터 기반 의사결정을 할 수 있는 기회를 제공합니다.',
'friend-invite': '친구 초대는 강력한 바이럴 효과와 함께 검증된 신규 고객을 확보할 수 있습니다.'
};
const methodNames = {
'sns-share': 'SNS 공유하기',
'qr-scan': 'QR 코드 스캔',
'coupon-download': '쿠폰 다운로드',
'store-visit': '매장 방문 인증',
'survey': '설문 참여',
'friend-invite': '친구 초대'
};
// Handle card click
methodCards.forEach(card => {
card.addEventListener('click', () => {
// Clear custom input
customMethodInput.value = '';
customMethod = null;
// Remove selected class from all cards
methodCards.forEach(c => removeClass(c, 'method-card--selected'));
// Add selected class to clicked card
addClass(card, 'method-card--selected');
const method = card.getAttribute('data-method');
selectedMethod = method;
// Update radio button
const radio = card.querySelector('input[type="radio"]');
radio.checked = true;
// Update AI tip
if (aiTips[method]) {
aiTipText.textContent = methodNames[method] || '';
}
nextButton.disabled = false;
});
});
// Handle custom method input
customMethodInput.addEventListener('input', (e) => {
const value = e.target.value.trim();
if (value) {
// Clear card selection
methodCards.forEach(c => removeClass(c, 'method-card--selected'));
methodRadios.forEach(r => r.checked = false);
selectedMethod = null;
customMethod = value;
nextButton.disabled = false;
} else {
customMethod = null;
if (!selectedMethod) {
nextButton.disabled = true;
}
}
});
// Handle next button
nextButton.addEventListener('click', () => {
// Get event draft
const eventDraft = storage.get('eventDraft') || {};
// Save participation method
eventDraft.participationMethod = {
type: selectedMethod,
custom: customMethod,
selectedAt: new Date().toISOString()
};
storage.set('eventDraft', eventDraft);
// Show loading state
addClass(nextButton, 'next-button--loading');
nextButton.disabled = true;
// Navigate to next screen
setTimeout(() => {
window.location.href = '08-AI홍보문구생성.html';
}, 800);
});
// Auto-scroll to recommended card on load
window.addEventListener('load', () => {
const recommendedCard = document.querySelector('.method-card--recommended');
if (recommendedCard) {
setTimeout(() => {
recommendedCard.scrollIntoView({ behavior: 'smooth', block: 'center' });
}, 300);
}
});
</script>
</body>
</html>

View File

@ -0,0 +1,931 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI 홍보 문구 생성 - KT 이벤트 마케팅</title>
<style>
/* CSS Variables */
:root {
/* Colors */
--color-primary: #E31E24;
--color-primary-dark: #C41E24;
--color-primary-light: #FFE9EA;
--color-secondary: #0066FF;
--color-secondary-dark: #0052CC;
--color-secondary-light: #E0EFFF;
--color-success: #00C853;
--color-success-light: #E8F5E9;
--color-error: #FF3B30;
--color-error-light: #FFE9E9;
--color-warning: #FFB800;
--color-white: #FFFFFF;
--color-black: #000000;
/* Text Colors */
--color-text-primary: #1A1A1A;
--color-text-secondary: #666666;
--color-text-tertiary: #999999;
/* Border Colors */
--color-border-light: #E0E0E0;
--color-border: #CCCCCC;
--color-border-dark: #999999;
/* Background Colors */
--color-surface: #F5F5F5;
--color-surface-light: #FAFAFA;
/* Spacing */
--spacing-xs: 4px;
--spacing-s: 8px;
--spacing-m: 16px;
--spacing-l: 24px;
--spacing-xl: 32px;
--spacing-2xl: 48px;
/* Font Sizes */
--font-size-xs: 12px;
--font-size-s: 14px;
--font-size-m: 16px;
--font-size-l: 18px;
--font-size-xl: 20px;
--font-size-2xl: 24px;
/* Font Weights */
--font-weight-regular: 400;
--font-weight-medium: 500;
--font-weight-semibold: 600;
--font-weight-bold: 700;
/* Border Radius */
--radius-s: 4px;
--radius-m: 8px;
--radius-l: 12px;
--radius-xl: 16px;
--radius-full: 9999px;
/* Shadows */
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.1);
--shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.15);
/* Transitions */
--transition-fast: 150ms ease-in-out;
--transition-normal: 300ms ease-in-out;
--transition-slow: 500ms ease-in-out;
/* Font Family */
--font-family: -apple-system, BlinkMacSystemFont, 'Pretendard Variable', 'Pretendard', 'Segoe UI', 'Roboto', sans-serif;
}
/* Reset */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: var(--font-family);
font-size: var(--font-size-m);
color: var(--color-text-primary);
background: #FFFFFF;
line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* Layout */
.copy-container {
min-height: 100vh;
background: linear-gradient(135deg, #F8F9FF 0%, #FFFFFF 100%);
padding-bottom: var(--spacing-2xl);
}
/* Header */
.copy-header {
background: linear-gradient(135deg, var(--color-secondary) 0%, var(--color-secondary-dark) 100%);
color: var(--color-white);
padding: var(--spacing-xl) var(--spacing-m);
box-shadow: var(--shadow-lg);
}
.copy-header__back {
background: none;
border: none;
color: var(--color-white);
font-size: var(--font-size-xl);
padding: 0;
margin-bottom: var(--spacing-m);
cursor: pointer;
}
.copy-header__title {
font-size: var(--font-size-2xl);
font-weight: var(--font-weight-bold);
margin-bottom: var(--spacing-xs);
}
.copy-header__subtitle {
font-size: var(--font-size-m);
opacity: 0.9;
}
/* Loading State */
.loading-state {
padding: var(--spacing-2xl) var(--spacing-m);
text-align: center;
}
.loading-animation {
width: 80px;
height: 80px;
margin: 0 auto var(--spacing-l);
position: relative;
}
.loading-animation__circle {
position: absolute;
width: 100%;
height: 100%;
border: 4px solid rgba(0, 102, 255, 0.1);
border-radius: 50%;
border-top-color: var(--color-secondary);
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.loading-animation__icon {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 32px;
}
.loading-state__title {
font-size: var(--font-size-xl);
font-weight: var(--font-weight-bold);
color: var(--color-text-primary);
margin-bottom: var(--spacing-s);
}
.loading-state__text {
font-size: var(--font-size-m);
color: var(--color-text-secondary);
margin-bottom: var(--spacing-l);
}
.loading-state__progress {
max-width: 400px;
margin: 0 auto;
background: var(--color-white);
border-radius: var(--radius-l);
padding: var(--spacing-m);
}
.progress-bar {
width: 100%;
height: 8px;
background: var(--color-border-light);
border-radius: var(--radius-s);
overflow: hidden;
margin-bottom: var(--spacing-s);
}
.progress-bar__fill {
height: 100%;
background: linear-gradient(90deg, var(--color-secondary) 0%, var(--color-secondary-light) 100%);
border-radius: var(--radius-s);
transition: width var(--transition-slow);
}
.progress-text {
font-size: var(--font-size-s);
color: var(--color-text-secondary);
text-align: center;
}
/* Copy Results */
.copy-results {
display: none;
padding: var(--spacing-l) var(--spacing-m);
}
.copy-results--visible {
display: block;
}
/* AI Tip */
.ai-tip {
background: linear-gradient(135deg, #FFF9E6 0%, #FFFFFF 100%);
border-left: 4px solid #FFB800;
padding: var(--spacing-m);
border-radius: var(--radius-m);
margin-bottom: var(--spacing-l);
}
.ai-tip__header {
display: flex;
align-items: center;
gap: var(--spacing-s);
margin-bottom: var(--spacing-s);
}
.ai-tip__icon {
font-size: var(--font-size-xl);
}
.ai-tip__title {
font-size: var(--font-size-m);
font-weight: var(--font-weight-semibold);
color: var(--color-text-primary);
}
.ai-tip__content {
font-size: var(--font-size-s);
color: var(--color-text-secondary);
line-height: 1.6;
}
/* Copy Variations */
.copy-variations {
display: grid;
gap: var(--spacing-m);
margin-bottom: var(--spacing-l);
}
/* Copy Card */
.copy-card {
background: var(--color-white);
border: 2px solid var(--color-border-light);
border-radius: var(--radius-l);
padding: var(--spacing-m);
cursor: pointer;
transition: all var(--transition-normal);
position: relative;
}
.copy-card:hover {
border-color: var(--color-secondary);
box-shadow: var(--shadow-md);
}
.copy-card--selected {
border-color: var(--color-secondary);
background: linear-gradient(135deg, #F0F7FF 0%, #FFFFFF 100%);
box-shadow: var(--shadow-lg);
}
.copy-card--recommended {
border-color: var(--color-secondary);
background: linear-gradient(135deg, #F0F7FF 0%, #FFFFFF 100%);
}
.copy-card__badge {
position: absolute;
top: -12px;
right: var(--spacing-l);
background: linear-gradient(135deg, var(--color-secondary) 0%, var(--color-secondary-dark) 100%);
color: var(--color-white);
padding: var(--spacing-xs) var(--spacing-m);
border-radius: var(--radius-s);
font-size: var(--font-size-xs);
font-weight: var(--font-weight-semibold);
box-shadow: var(--shadow-md);
}
.copy-card__header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: var(--spacing-m);
}
.copy-card__label {
font-size: var(--font-size-s);
font-weight: var(--font-weight-semibold);
color: var(--color-text-secondary);
}
.copy-card__radio {
width: 20px;
height: 20px;
cursor: pointer;
}
.copy-card__headline {
font-size: var(--font-size-xl);
font-weight: var(--font-weight-bold);
color: var(--color-text-primary);
margin-bottom: var(--spacing-m);
line-height: 1.4;
}
.copy-card__body {
font-size: var(--font-size-m);
color: var(--color-text-secondary);
line-height: 1.6;
margin-bottom: var(--spacing-m);
}
.copy-card__cta {
display: inline-block;
background: linear-gradient(135deg, var(--color-primary) 0%, var(--color-primary-dark) 100%);
color: var(--color-white);
padding: var(--spacing-s) var(--spacing-l);
border-radius: var(--radius-m);
font-size: var(--font-size-m);
font-weight: var(--font-weight-semibold);
margin-bottom: var(--spacing-m);
}
.copy-card__hashtags {
display: flex;
flex-wrap: wrap;
gap: var(--spacing-xs);
margin-bottom: var(--spacing-m);
}
.hashtag {
background: var(--color-secondary-light);
color: var(--color-secondary);
padding: var(--spacing-xs) var(--spacing-s);
border-radius: var(--radius-s);
font-size: var(--font-size-xs);
font-weight: var(--font-weight-medium);
}
.copy-card__actions {
display: flex;
gap: var(--spacing-s);
padding-top: var(--spacing-m);
border-top: 1px solid var(--color-border-light);
}
.copy-card__action-btn {
flex: 1;
padding: var(--spacing-s);
background: var(--color-white);
border: 1px solid var(--color-border-light);
border-radius: var(--radius-m);
font-size: var(--font-size-s);
font-weight: var(--font-weight-medium);
color: var(--color-text-secondary);
cursor: pointer;
transition: all var(--transition-normal);
}
.copy-card__action-btn:hover {
border-color: var(--color-secondary);
color: var(--color-secondary);
}
/* Edit Modal */
.edit-modal {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 1000;
padding: var(--spacing-m);
}
.edit-modal--active {
display: flex;
align-items: center;
justify-content: center;
}
.edit-modal__content {
background: var(--color-white);
border-radius: var(--radius-l);
padding: var(--spacing-l);
max-width: 600px;
width: 100%;
max-height: 90vh;
overflow-y: auto;
}
.edit-modal__header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--spacing-l);
}
.edit-modal__title {
font-size: var(--font-size-xl);
font-weight: var(--font-weight-bold);
color: var(--color-text-primary);
}
.edit-modal__close {
background: none;
border: none;
font-size: var(--font-size-2xl);
color: var(--color-text-secondary);
cursor: pointer;
padding: 0;
width: 32px;
height: 32px;
}
.edit-modal__form {
display: grid;
gap: var(--spacing-m);
}
.form-group {
display: grid;
gap: var(--spacing-xs);
}
.form-group__label {
font-size: var(--font-size-s);
font-weight: var(--font-weight-semibold);
color: var(--color-text-primary);
}
.form-group__input,
.form-group__textarea {
width: 100%;
padding: var(--spacing-m);
border: 1px solid var(--color-border-light);
border-radius: var(--radius-m);
font-size: var(--font-size-m);
font-family: var(--font-family);
transition: all var(--transition-normal);
}
.form-group__textarea {
resize: vertical;
min-height: 100px;
}
.form-group__input:focus,
.form-group__textarea:focus {
outline: none;
border-color: var(--color-secondary);
box-shadow: 0 0 0 3px rgba(0, 102, 255, 0.1);
}
.edit-modal__actions {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--spacing-s);
margin-top: var(--spacing-l);
}
.modal-btn {
padding: var(--spacing-m);
border: none;
border-radius: var(--radius-m);
font-size: var(--font-size-m);
font-weight: var(--font-weight-semibold);
cursor: pointer;
transition: all var(--transition-normal);
}
.modal-btn--cancel {
background: var(--color-white);
border: 1px solid var(--color-border-light);
color: var(--color-text-secondary);
}
.modal-btn--save {
background: linear-gradient(135deg, var(--color-secondary) 0%, var(--color-secondary-dark) 100%);
color: var(--color-white);
}
/* Next Button */
.next-button {
width: 100%;
padding: var(--spacing-m);
background: linear-gradient(135deg, var(--color-secondary) 0%, var(--color-secondary-dark) 100%);
color: var(--color-white);
border: none;
border-radius: var(--radius-m);
font-size: var(--font-size-m);
font-weight: var(--font-weight-semibold);
cursor: pointer;
transition: all var(--transition-normal);
box-shadow: var(--shadow-md);
}
.next-button:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: var(--shadow-lg);
}
.next-button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.next-button--loading {
position: relative;
color: transparent;
}
.next-button--loading:after {
content: '';
position: absolute;
width: 20px;
height: 20px;
top: 50%;
left: 50%;
margin-left: -10px;
margin-top: -10px;
border: 3px solid rgba(255, 255, 255, 0.3);
border-radius: 50%;
border-top-color: var(--color-white);
animation: spinner 0.6s linear infinite;
}
@keyframes spinner {
to { transform: rotate(360deg); }
}
/* Tablet */
@media (min-width: 768px) {
.copy-header {
padding: var(--spacing-2xl) var(--spacing-xl);
}
.copy-results {
max-width: 720px;
margin: 0 auto;
padding: var(--spacing-xl);
}
}
/* Desktop */
@media (min-width: 1024px) {
.copy-results {
max-width: 960px;
}
}
</style>
</head>
<body>
<div class="copy-container">
<!-- Header -->
<header class="copy-header">
<button class="copy-header__back" onclick="history.back()">
</button>
<h1 class="copy-header__title">AI 홍보 문구 생성</h1>
<p class="copy-header__subtitle">AI가 맞춤형 홍보 문구를 생성해 드려요</p>
</header>
<!-- Loading State -->
<div class="loading-state" id="loadingState">
<div class="loading-animation">
<div class="loading-animation__circle"></div>
<div class="loading-animation__icon"></div>
</div>
<h2 class="loading-state__title">AI가 홍보 문구를 생성하고 있어요</h2>
<p class="loading-state__text" id="loadingText">이벤트 정보를 분석하고 있어요...</p>
<div class="loading-state__progress">
<div class="progress-bar">
<div class="progress-bar__fill" id="progressBar" style="width: 0%"></div>
</div>
<p class="progress-text" id="progressText">0%</p>
</div>
</div>
<!-- Copy Results -->
<div class="copy-results" id="copyResults">
<!-- AI Tip -->
<div class="ai-tip">
<div class="ai-tip__header">
<span class="ai-tip__icon">💡</span>
<h2 class="ai-tip__title">AI 추천</h2>
</div>
<p class="ai-tip__content">
<strong>버전 1</strong>은 감성적인 톤으로 고객의 공감을 이끌어내기에 가장 적합합니다.
SNS 공유 시 참여율이 평균 35% 높게 나타났어요.
</p>
</div>
<!-- Copy Variations -->
<div class="copy-variations" id="copyVariations">
<!-- Copy cards will be inserted here -->
</div>
<!-- Next Button -->
<button class="next-button" id="nextButton" disabled>
이벤트 기획안 확인하기
</button>
</div>
</div>
<!-- Edit Modal -->
<div class="edit-modal" id="editModal">
<div class="edit-modal__content">
<div class="edit-modal__header">
<h2 class="edit-modal__title">홍보 문구 수정</h2>
<button class="edit-modal__close" id="closeModal">×</button>
</div>
<form class="edit-modal__form" id="editForm">
<div class="form-group">
<label class="form-group__label">헤드라인</label>
<input
type="text"
class="form-group__input"
id="editHeadline"
maxlength="100"
>
</div>
<div class="form-group">
<label class="form-group__label">본문</label>
<textarea
class="form-group__textarea"
id="editBody"
maxlength="300"
></textarea>
</div>
<div class="form-group">
<label class="form-group__label">행동 유도 문구 (CTA)</label>
<input
type="text"
class="form-group__input"
id="editCTA"
maxlength="50"
>
</div>
<div class="form-group">
<label class="form-group__label">해시태그 (쉼표로 구분)</label>
<input
type="text"
class="form-group__input"
id="editHashtags"
placeholder="이벤트, 할인, 초대"
>
</div>
</form>
<div class="edit-modal__actions">
<button class="modal-btn modal-btn--cancel" id="cancelEdit">취소</button>
<button class="modal-btn modal-btn--save" id="saveEdit">저장</button>
</div>
</div>
</div>
<script>
// Utility functions
const $ = (selector) => document.querySelector(selector);
const $$ = (selector) => document.querySelectorAll(selector);
const addClass = (el, className) => el?.classList.add(className);
const removeClass = (el, className) => el?.classList.remove(className);
const storage = {
get: (key) => {
try {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : null;
} catch (e) {
console.error('Storage get error:', e);
return null;
}
},
set: (key, value) => {
try {
localStorage.setItem(key, JSON.stringify(value));
} catch (e) {
console.error('Storage set error:', e);
}
}
};
// Elements
const loadingState = $('#loadingState');
const copyResults = $('#copyResults');
const loadingText = $('#loadingText');
const progressBar = $('#progressBar');
const progressText = $('#progressText');
const copyVariations = $('#copyVariations');
const nextButton = $('#nextButton');
const editModal = $('#editModal');
const closeModal = $('#closeModal');
const cancelEdit = $('#cancelEdit');
const saveEdit = $('#saveEdit');
// Edit form elements
const editHeadline = $('#editHeadline');
const editBody = $('#editBody');
const editCTA = $('#editCTA');
const editHashtags = $('#editHashtags');
// State
let selectedCopy = null;
let currentEditIndex = null;
const copyOptions = [
{
version: '버전 1',
recommended: true,
headline: '🎉 친구 초대하고 함께 받는 특별한 혜택!',
body: '맛있는 순간을 소중한 사람과 함께하세요. 친구를 초대하면 둘 다 특별한 선물을 드려요. 지금 바로 참여하고 행복을 나누세요!',
cta: '지금 친구 초대하고 혜택 받기',
hashtags: ['친구초대이벤트', '함께할인', '소상공인응원', 'SNS이벤트']
},
{
version: '버전 2',
recommended: false,
headline: '💝 지금 초대하면 특별 혜택 2배!',
body: '단 3일간! 친구를 초대하고 최대 50% 할인 쿠폰을 받아가세요. 초대한 친구도 동일한 혜택을 받을 수 있어요.',
cta: '3일 한정 특가 혜택 받기',
hashtags: ['한정이벤트', '친구초대', '특별할인', '이벤트']
},
{
version: '버전 3',
recommended: false,
headline: '🎁 우리 가게 단골이 되어주세요!',
body: '매일 찾아주시는 고객님들을 위한 특별한 이벤트를 준비했어요. 친구와 함께 방문하면 더 큰 혜택이 기다립니다.',
cta: '단골 고객 혜택 확인하기',
hashtags: ['단골이벤트', '매장방문', '친구와함께', '로컬맛집']
}
];
// Loading animation
let currentStep = 0;
const progressSteps = [
{ progress: 20, text: '이벤트 정보를 분석하고 있어요...' },
{ progress: 40, text: '타겟 고객 성향을 파악하고 있어요...' },
{ progress: 60, text: '감성적인 키워드를 추출하고 있어요...' },
{ progress: 80, text: '다양한 문구를 생성하고 있어요...' },
{ progress: 100, text: '최적의 문구를 선별하고 있어요...' }
];
function updateProgress() {
if (currentStep >= progressSteps.length) {
// Show results
loadingState.style.display = 'none';
addClass(copyResults, 'copy-results--visible');
renderCopyVariations();
return;
}
const step = progressSteps[currentStep];
progressBar.style.width = step.progress + '%';
progressText.textContent = step.progress + '%';
loadingText.textContent = step.text;
currentStep++;
setTimeout(updateProgress, 800);
}
// Render copy variations
function renderCopyVariations() {
copyVariations.innerHTML = copyOptions.map((copy, index) => `
<div class="copy-card ${copy.recommended ? 'copy-card--recommended' : ''}" data-index="${index}">
${copy.recommended ? '<div class="copy-card__badge">🤖 AI 추천</div>' : ''}
<div class="copy-card__header">
<span class="copy-card__label">${copy.version}</span>
<input type="radio" name="copy" value="${index}" class="copy-card__radio">
</div>
<h3 class="copy-card__headline">${copy.headline}</h3>
<p class="copy-card__body">${copy.body}</p>
<div class="copy-card__cta">${copy.cta}</div>
<div class="copy-card__hashtags">
${copy.hashtags.map(tag => `<span class="hashtag">#${tag}</span>`).join('')}
</div>
<div class="copy-card__actions">
<button class="copy-card__action-btn" onclick="editCopy(${index})">✏️ 수정</button>
<button class="copy-card__action-btn" onclick="copyCopy(${index})">📋 복사</button>
</div>
</div>
`).join('');
// Add click handlers
const cards = document.querySelectorAll('.copy-card');
cards.forEach(card => {
card.addEventListener('click', (e) => {
// Don't trigger if clicking action buttons
if (e.target.closest('.copy-card__action-btn')) return;
const index = parseInt(card.getAttribute('data-index'));
selectCopy(index);
});
});
}
// Select copy
function selectCopy(index) {
selectedCopy = copyOptions[index];
// Update UI
const cards = document.querySelectorAll('.copy-card');
cards.forEach(card => removeClass(card, 'copy-card--selected'));
addClass(cards[index], 'copy-card--selected');
// Update radio
const radio = cards[index].querySelector('.copy-card__radio');
radio.checked = true;
nextButton.disabled = false;
}
// Edit copy
window.editCopy = function(index) {
currentEditIndex = index;
const copy = copyOptions[index];
editHeadline.value = copy.headline;
editBody.value = copy.body;
editCTA.value = copy.cta;
editHashtags.value = copy.hashtags.join(', ');
addClass(editModal, 'edit-modal--active');
};
// Copy to clipboard
window.copyCopy = function(index) {
const copy = copyOptions[index];
const text = `${copy.headline}\n\n${copy.body}\n\n${copy.cta}\n\n${copy.hashtags.map(t => '#' + t).join(' ')}`;
navigator.clipboard.writeText(text).then(() => {
alert('홍보 문구가 클립보드에 복사되었습니다.');
}).catch(() => {
alert('복사에 실패했습니다. 다시 시도해 주세요.');
});
};
// Close modal
closeModal.addEventListener('click', () => {
removeClass(editModal, 'edit-modal--active');
});
cancelEdit.addEventListener('click', () => {
removeClass(editModal, 'edit-modal--active');
});
// Save edit
saveEdit.addEventListener('click', () => {
if (currentEditIndex !== null) {
copyOptions[currentEditIndex] = {
...copyOptions[currentEditIndex],
headline: editHeadline.value,
body: editBody.value,
cta: editCTA.value,
hashtags: editHashtags.value.split(',').map(t => t.trim()).filter(t => t)
};
renderCopyVariations();
// Re-select if it was selected
if (selectedCopy && selectedCopy.version === copyOptions[currentEditIndex].version) {
selectCopy(currentEditIndex);
}
}
removeClass(editModal, 'edit-modal--active');
});
// Close modal on background click
editModal.addEventListener('click', (e) => {
if (e.target === editModal) {
removeClass(editModal, 'edit-modal--active');
}
});
// Handle next button
nextButton.addEventListener('click', () => {
if (!selectedCopy) return;
// Get event draft
const eventDraft = storage.get('eventDraft') || {};
// Save promotional copy
eventDraft.promotionalCopy = {
...selectedCopy,
selectedAt: new Date().toISOString()
};
storage.set('eventDraft', eventDraft);
// Show loading state
addClass(nextButton, 'next-button--loading');
nextButton.disabled = true;
// Navigate to next screen
setTimeout(() => {
window.location.href = '09-AI이미지생성.html';
}, 800);
});
// Start loading animation on page load
window.addEventListener('load', () => {
setTimeout(updateProgress, 500);
});
</script>
</body>
</html>

View File

@ -1,390 +1,560 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI 이미지 생성 - KT AI 이벤트</title>
<link rel="stylesheet" href="css/common.css">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Pretendard:wght@400;500;600;700&display=swap" rel="stylesheet">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI 이미지 생성 - KT 이벤트 마케팅</title>
<style>
/* CSS Variables */
:root {
/* Colors */
--color-primary: #E31E24;
--color-primary-dark: #B71C1C;
--color-primary-light: #FFEBEE;
--color-secondary: #0066FF;
--color-secondary-dark: #0052CC;
--color-secondary-light: #E3F2FD;
--color-success: #4CAF50;
--color-success-light: #E8F5E9;
--color-warning: #FF9800;
--color-warning-light: #FFF3E0;
--color-error: #F44336;
--color-error-light: #FFEBEE;
--color-info: #2196F3;
--color-info-light: #E3F2FD;
/* Grayscale */
--color-white: #FFFFFF;
--color-gray-50: #FAFAFA;
--color-gray-100: #F5F5F5;
--color-gray-200: #EEEEEE;
--color-gray-300: #E0E0E0;
--color-gray-400: #BDBDBD;
--color-gray-500: #9E9E9E;
--color-gray-600: #757575;
--color-gray-700: #616161;
--color-gray-800: #424242;
--color-gray-900: #212121;
--color-black: #000000;
/* Text Colors */
--color-text-primary: #212121;
--color-text-secondary: #757575;
--color-text-tertiary: #9E9E9E;
--color-text-disabled: #BDBDBD;
--color-text-inverse: #FFFFFF;
/* Border Colors */
--color-border-light: #E0E0E0;
--color-border-medium: #BDBDBD;
--color-border-dark: #757575;
/* Background Colors */
--color-bg-primary: #FFFFFF;
--color-bg-secondary: #F5F5F5;
--color-bg-tertiary: #FAFAFA;
/* Spacing (4px base unit) */
--spacing-xs: 4px;
--spacing-s: 8px;
--spacing-m: 16px;
--spacing-l: 24px;
--spacing-xl: 32px;
--spacing-2xl: 48px;
--spacing-3xl: 64px;
/* Typography */
--font-family-primary: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
--font-size-xs: 12px;
--font-size-s: 14px;
--font-size-m: 16px;
--font-size-l: 18px;
--font-size-xl: 20px;
--font-size-2xl: 24px;
--font-size-3xl: 32px;
--font-size-4xl: 40px;
--font-weight-regular: 400;
--font-weight-medium: 500;
--font-weight-semibold: 600;
--font-weight-bold: 700;
--line-height-tight: 1.2;
--line-height-normal: 1.5;
--line-height-relaxed: 1.75;
/* Border Radius */
--radius-s: 4px;
--radius-m: 8px;
--radius-l: 12px;
--radius-xl: 16px;
--radius-2xl: 24px;
--radius-full: 9999px;
/* Shadows */
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
--shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
/* Transitions */
--transition-fast: 150ms ease-in-out;
--transition-normal: 250ms ease-in-out;
--transition-slow: 350ms ease-in-out;
/* Z-index */
--z-dropdown: 1000;
--z-sticky: 1020;
--z-fixed: 1030;
--z-modal-backdrop: 1040;
--z-modal: 1050;
--z-popover: 1060;
--z-tooltip: 1070;
}
/* Reset & Base Styles */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: var(--font-family-primary);
font-size: var(--font-size-m);
line-height: var(--line-height-normal);
color: var(--color-text-primary);
background: var(--color-bg-secondary);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
button {
font-family: inherit;
cursor: pointer;
}
/* Layout */
.image-container {
min-height: 100vh;
background: linear-gradient(135deg, #F8F9FF 0%, #FFFFFF 100%);
padding-bottom: var(--spacing-2xl);
}
/* Header */
.image-header {
background: linear-gradient(135deg, var(--color-secondary) 0%, var(--color-secondary-dark) 100%);
color: var(--color-white);
padding: var(--spacing-xl) var(--spacing-m);
box-shadow: var(--shadow-lg);
}
.image-header__back {
background: none;
border: none;
color: var(--color-white);
font-size: var(--font-size-xl);
padding: 0;
margin-bottom: var(--spacing-m);
cursor: pointer;
}
.image-header__title {
font-size: var(--font-size-2xl);
font-weight: var(--font-weight-bold);
margin-bottom: var(--spacing-xs);
}
.image-header__subtitle {
font-size: var(--font-size-m);
opacity: 0.9;
}
/* Content */
.image-content {
padding: var(--spacing-l) var(--spacing-m);
}
/* Loading State */
.loading-state {
background: var(--color-white);
border-radius: var(--radius-l);
padding: var(--spacing-2xl);
text-align: center;
box-shadow: var(--shadow-md);
}
.loading-state__icon {
font-size: 64px;
margin-bottom: var(--spacing-m);
animation: pulse 2s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.loading-state__title {
font-size: var(--font-size-l);
font-weight: var(--font-weight-bold);
color: var(--color-text-primary);
margin-bottom: var(--spacing-s);
}
.loading-state__text {
font-size: var(--font-size-s);
color: var(--color-text-secondary);
margin-bottom: var(--spacing-l);
}
.progress-bar {
width: 100%;
height: 8px;
background: var(--color-gray-200);
border-radius: var(--radius-full);
overflow: hidden;
margin-bottom: var(--spacing-s);
}
.progress-bar__fill {
height: 100%;
background: linear-gradient(90deg, var(--color-secondary), var(--color-primary));
border-radius: var(--radius-full);
transition: width 0.3s ease;
}
.progress-text {
font-size: var(--font-size-s);
color: var(--color-text-secondary);
}
/* Results */
.image-results {
display: none;
}
.image-results--visible {
display: block;
}
.image-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: var(--spacing-m);
margin-bottom: var(--spacing-l);
}
.image-item {
position: relative;
aspect-ratio: 1;
background: linear-gradient(135deg, #E0E7FF 0%, #F0F7FF 100%);
border-radius: var(--radius-l);
overflow: hidden;
cursor: pointer;
border: 3px solid transparent;
transition: all var(--transition-normal);
}
.image-item:hover {
transform: translateY(-4px);
box-shadow: var(--shadow-lg);
}
.image-item--selected {
border-color: var(--color-secondary);
box-shadow: var(--shadow-xl);
}
.image-item__preview {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
font-size: 64px;
}
.image-item__checkbox {
position: absolute;
top: var(--spacing-s);
right: var(--spacing-s);
width: 24px;
height: 24px;
background: var(--color-white);
border: 2px solid var(--color-border-light);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: var(--font-size-xs);
color: var(--color-white);
transition: all var(--transition-normal);
}
.image-item--selected .image-item__checkbox {
background: var(--color-secondary);
border-color: var(--color-secondary);
}
/* Next Button */
.next-button {
width: 100%;
padding: var(--spacing-m);
background: linear-gradient(135deg, var(--color-secondary) 0%, var(--color-secondary-dark) 100%);
color: var(--color-white);
border: none;
border-radius: var(--radius-m);
font-size: var(--font-size-m);
font-weight: var(--font-weight-semibold);
cursor: pointer;
transition: all var(--transition-normal);
box-shadow: var(--shadow-md);
}
.next-button:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: var(--shadow-lg);
}
.next-button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.next-button--loading {
position: relative;
color: transparent;
}
.next-button--loading:after {
content: '';
position: absolute;
width: 20px;
height: 20px;
top: 50%;
left: 50%;
margin-left: -10px;
margin-top: -10px;
border: 3px solid rgba(255, 255, 255, 0.3);
border-radius: 50%;
border-top-color: var(--color-white);
animation: spinner 0.6s linear infinite;
}
@keyframes spinner {
to { transform: rotate(360deg); }
}
/* Tablet */
@media (min-width: 768px) {
.image-header {
padding: var(--spacing-2xl) var(--spacing-xl);
}
.image-content {
max-width: 720px;
margin: 0 auto;
padding: var(--spacing-xl);
}
.image-grid {
grid-template-columns: repeat(4, 1fr);
}
}
/* Desktop */
@media (min-width: 1024px) {
.image-content {
max-width: 960px;
}
}
</style>
</head>
<body>
<a href="#main-content" class="skip-link">본문으로 건너뛰기</a>
<header class="header" role="banner">
<div class="header-content">
<button type="button" class="btn-icon" onclick="window.history.back()" aria-label="이전 페이지로 돌아가기">
<span class="material-icons">arrow_back</span>
</button>
<h1 class="header-title">AI 이미지 생성</h1>
<div style="width: 40px;"></div>
</div>
<div class="image-container">
<!-- Header -->
<header class="image-header">
<button class="image-header__back" onclick="history.back()">
</button>
<h1 class="image-header__title">AI 이미지 생성</h1>
<p class="image-header__subtitle">이벤트에 사용할 매력적인 이미지를 생성해 드려요</p>
</header>
<div class="progress-indicator" role="progressbar" aria-valuenow="20" aria-valuemin="0" aria-valuemax="100">
<div class="progress-text">콘텐츠 1/5: 이미지</div>
<!-- Content -->
<main class="image-content">
<!-- Loading State -->
<div class="loading-state" id="loadingState">
<div class="loading-state__icon">🎨</div>
<h2 class="loading-state__title">AI가 이미지를 생성하고 있어요</h2>
<p class="loading-state__text">
이벤트 정보를 바탕으로 최적의 이미지를 생성 중입니다.<br>
잠시만 기다려 주세요...
</p>
<div class="progress-bar">
<div class="progress-fill" style="width: 20%;"></div>
<div class="progress-bar__fill" id="progressBar" style="width: 0%"></div>
</div>
</div>
<p class="progress-text" id="progressText">0%</p>
</div>
<main id="main-content" class="container" role="main">
<section class="section" aria-labelledby="brand-settings-title">
<h2 id="brand-settings-title" class="h3">브랜드 설정</h2>
<!-- Results -->
<div class="image-results" id="imageResults">
<div class="image-grid" id="imageGrid">
<!-- Images will be inserted here -->
</div>
<div class="card">
<div class="card-body">
<div class="form-group">
<label for="brandColor" class="form-label">브랜드 컬러</label>
<div style="display: flex; gap: var(--spacing-s); align-items: center;">
<input type="color" id="brandColor" value="#E31E24" style="width: 60px; height: 48px; border: 1px solid var(--color-gray-300); border-radius: var(--radius-sm); cursor: pointer;">
<input type="text" id="brandColorText" class="input-field" value="#E31E24" placeholder="#RRGGBB" style="flex: 1;">
</div>
</div>
<div class="form-group">
<label for="logoUpload" class="form-label">로고 업로드 (선택)</label>
<div class="upload-area" id="uploadArea">
<input type="file" id="logoUpload" accept="image/*" style="display: none;" aria-label="로고 이미지 업로드">
<div class="upload-placeholder" id="uploadPlaceholder">
<span class="material-icons" style="font-size: 48px; color: var(--color-gray-400);">add_photo_alternate</span>
<div class="body-m" style="color: var(--color-gray-600); margin-top: var(--spacing-s);">이미지 선택</div>
<div class="caption" style="color: var(--color-gray-500); margin-top: var(--spacing-xs);">PNG, JPG (최대 5MB)</div>
</div>
<div class="upload-preview" id="uploadPreview" style="display: none;">
<img id="previewImage" src="" alt="업로드된 로고 미리보기" style="max-width: 100%; max-height: 200px; border-radius: var(--radius-md);">
<button type="button" class="btn btn-text btn-sm" onclick="removeImage()" style="margin-top: var(--spacing-s);">
<span class="material-icons">delete</span>
<span>삭제</span>
</button>
</div>
</div>
</div>
<button type="button" class="btn btn-primary btn-lg" onclick="startGeneration()" style="width: 100%; margin-top: var(--spacing-m);">
<span class="material-icons">auto_awesome</span>
<span>AI 이미지 생성 시작</span>
</button>
</div>
</div>
</section>
<section class="section" id="generationProgress" style="display: none;" aria-labelledby="progress-title">
<h2 id="progress-title" class="h3">🤖 AI가 이미지 생성중...</h2>
<div class="card">
<div class="card-body">
<div style="text-align: center;">
<div class="body-m" style="color: var(--color-gray-600); margin-bottom: var(--spacing-m);">
브랜드 컬러와 로고를 반영하여<br>3가지 스타일의 이미지를 생성합니다
</div>
<div class="progress-bar-container" style="height: 12px; margin-bottom: var(--spacing-s);">
<div class="progress-bar-fill" id="aiProgressBar" style="width: 0%; background: var(--color-primary-main);"></div>
</div>
<div class="body-s" style="color: var(--color-gray-600);" id="progressText">
진행률: 0%
</div>
<div class="caption" style="color: var(--color-gray-500); margin-top: var(--spacing-xs);" id="timeEstimate">
예상 소요: 2분 30초
</div>
</div>
</div>
</div>
</section>
<section class="section" id="generationResult" style="display: none;" aria-labelledby="result-title">
<h2 id="result-title" class="h3">생성된 이미지 (3종)</h2>
<div class="card">
<div class="card-body">
<div style="display: flex; flex-direction: column; gap: var(--spacing-m);">
<div class="image-option" onclick="selectImage(0)">
<div class="image-preview" style="aspect-ratio: 1/1; background: linear-gradient(135deg, #E31E24 0%, #FF6B6B 100%); border-radius: var(--radius-md); display: flex; align-items: center; justify-content: center; position: relative;">
<div style="text-align: center; color: white;">
<span class="material-icons" style="font-size: 48px;">palette</span>
<div class="body-l" style="font-weight: 600; margin-top: var(--spacing-s);">심플 스타일</div>
</div>
<div class="selection-badge" id="badge0" style="display: none;">
<span class="material-icons">check_circle</span>
</div>
</div>
<div style="display: flex; justify-content: space-between; margin-top: var(--spacing-s);">
<button type="button" class="btn btn-text btn-sm" onclick="event.stopPropagation(); downloadImage(0)">
<span class="material-icons">download</span>
<span>다운로드</span>
</button>
<button type="button" class="btn btn-text btn-sm" onclick="event.stopPropagation(); regenerateImage(0)">
<span class="material-icons">refresh</span>
<span>재생성</span>
</button>
</div>
</div>
<div class="image-option" onclick="selectImage(1)">
<div class="image-preview" style="aspect-ratio: 1/1; background: linear-gradient(135deg, #E31E24 0%, #FFD700 50%, #FF1493 100%); border-radius: var(--radius-md); display: flex; align-items: center; justify-content: center; position: relative;">
<div style="text-align: center; color: white;">
<span class="material-icons" style="font-size: 48px;">auto_awesome</span>
<div class="body-l" style="font-weight: 600; margin-top: var(--spacing-s);">화려한 스타일</div>
</div>
<div class="selection-badge" id="badge1" style="display: none;">
<span class="material-icons">check_circle</span>
</div>
</div>
<div style="display: flex; justify-content: space-between; margin-top: var(--spacing-s);">
<button type="button" class="btn btn-text btn-sm" onclick="event.stopPropagation(); downloadImage(1)">
<span class="material-icons">download</span>
<span>다운로드</span>
</button>
<button type="button" class="btn btn-text btn-sm" onclick="event.stopPropagation(); regenerateImage(1)">
<span class="material-icons">refresh</span>
<span>재생성</span>
</button>
</div>
</div>
<div class="image-option" onclick="selectImage(2)">
<div class="image-preview" style="aspect-ratio: 1/1; background: linear-gradient(135deg, #E31E24 0%, #0066FF 100%); border-radius: var(--radius-md); display: flex; align-items: center; justify-content: center; position: relative;">
<div style="text-align: center; color: white;">
<span class="material-icons" style="font-size: 48px;">trending_up</span>
<div class="body-l" style="font-weight: 600; margin-top: var(--spacing-s);">트렌디 스타일</div>
</div>
<div class="selection-badge" id="badge2" style="display: none;">
<span class="material-icons">check_circle</span>
</div>
</div>
<div style="display: flex; justify-content: space-between; margin-top: var(--spacing-s);">
<button type="button" class="btn btn-text btn-sm" onclick="event.stopPropagation(); downloadImage(2)">
<span class="material-icons">download</span>
<span>다운로드</span>
</button>
<button type="button" class="btn btn-text btn-sm" onclick="event.stopPropagation(); regenerateImage(2)">
<span class="material-icons">refresh</span>
<span>재생성</span>
</button>
</div>
</div>
</div>
<button type="button" class="btn btn-primary btn-lg" onclick="goToNext()" id="nextButton" disabled style="width: 100%; margin-top: var(--spacing-l);">
<span>다음 단계</span>
<span class="material-icons">arrow_forward</span>
</button>
</div>
</div>
</section>
<button class="next-button" id="nextButton" disabled>
선택 완료 (최소 1개)
</button>
</div>
</main>
</div>
<script src="js/common.js"></script>
<script>
(function() {
'use strict';
<script>
// Utility functions
const $ = (selector) => document.querySelector(selector);
let selectedImageIndex = -1;
const addClass = (el, className) => el?.classList.add(className);
const removeClass = (el, className) => el?.classList.remove(className);
const hasClass = (el, className) => el?.classList.contains(className);
// 컬러 피커와 텍스트 입력 동기화
document.getElementById('brandColor').addEventListener('input', function(e) {
document.getElementById('brandColorText').value = e.target.value.toUpperCase();
});
document.getElementById('brandColorText').addEventListener('input', function(e) {
const color = e.target.value;
if (/^#[0-9A-F]{6}$/i.test(color)) {
document.getElementById('brandColor').value = color;
}
});
// 파일 업로드
document.getElementById('uploadArea').addEventListener('click', function() {
document.getElementById('logoUpload').click();
});
document.getElementById('logoUpload').addEventListener('change', function(e) {
const file = e.target.files[0];
if (file) {
if (file.size > 5 * 1024 * 1024) {
Toast.error('파일 크기는 5MB를 초과할 수 없습니다.');
return;
}
const reader = new FileReader();
reader.onload = function(event) {
document.getElementById('previewImage').src = event.target.result;
document.getElementById('uploadPlaceholder').style.display = 'none';
document.getElementById('uploadPreview').style.display = 'block';
};
reader.readAsDataURL(file);
}
});
window.removeImage = function() {
document.getElementById('logoUpload').value = '';
document.getElementById('uploadPlaceholder').style.display = 'flex';
document.getElementById('uploadPreview').style.display = 'none';
};
window.startGeneration = function() {
document.getElementById('generationProgress').style.display = 'block';
document.getElementById('generationProgress').scrollIntoView({ behavior: 'smooth' });
let progress = 0;
const interval = setInterval(function() {
progress += Math.random() * 15;
if (progress > 100) progress = 100;
document.getElementById('aiProgressBar').style.width = progress + '%';
document.getElementById('progressText').textContent = '진행률: ' + Math.round(progress) + '%';
const remainingTime = Math.max(0, Math.round(150 * (1 - progress / 100)));
const minutes = Math.floor(remainingTime / 60);
const seconds = remainingTime % 60;
document.getElementById('timeEstimate').textContent =
'예상 소요: ' + minutes + '분 ' + seconds + '초';
if (progress >= 100) {
clearInterval(interval);
setTimeout(function() {
document.getElementById('generationResult').style.display = 'block';
document.getElementById('generationResult').scrollIntoView({ behavior: 'smooth' });
Toast.success('✨ 이미지 생성이 완료되었습니다!');
}, 500);
}
}, 200);
};
window.selectImage = function(index) {
// 이전 선택 해제
if (selectedImageIndex >= 0) {
document.getElementById('badge' + selectedImageIndex).style.display = 'none';
}
selectedImageIndex = index;
document.getElementById('badge' + index).style.display = 'flex';
document.getElementById('nextButton').disabled = false;
Toast.show('이미지 ' + (index + 1) + '번이 선택되었습니다');
};
window.downloadImage = function(index) {
Toast.success('📥 이미지 ' + (index + 1) + '번이 다운로드되었습니다');
};
window.regenerateImage = function(index) {
Loading.show('이미지 재생성 중...');
setTimeout(function() {
Loading.hide();
Toast.success('✨ 새로운 이미지가 생성되었습니다');
}, 2000);
};
window.goToNext = function() {
if (selectedImageIndex < 0) {
Toast.error('이미지를 선택해주세요');
return;
}
Toast.show('다음 단계로 이동합니다');
};
})();
</script>
<style>
.progress-indicator {
background: white;
padding: var(--spacing-m) var(--spacing-l);
border-bottom: 1px solid var(--color-gray-300);
const storage = {
get: (key) => {
try {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : null;
} catch (e) {
console.error('Storage get error:', e);
return null;
}
.progress-text {
font-size: 14px;
color: var(--color-gray-600);
margin-bottom: var(--spacing-s);
},
set: (key, value) => {
try {
localStorage.setItem(key, JSON.stringify(value));
} catch (e) {
console.error('Storage set error:', e);
}
}
};
.progress-bar {
height: 4px;
background: var(--color-gray-200);
border-radius: var(--radius-full);
overflow: hidden;
}
// Elements
const loadingState = $('#loadingState');
const progressBar = $('#progressBar');
const progressText = $('#progressText');
const imageResults = $('#imageResults');
const imageGrid = $('#imageGrid');
const nextButton = $('#nextButton');
.progress-fill {
height: 100%;
background: var(--color-primary-main);
transition: width 0.3s ease;
}
// State
const selectedImages = new Set();
let currentProgress = 0;
.upload-area {
border: 2px dashed var(--color-gray-300);
border-radius: var(--radius-md);
padding: var(--spacing-xl);
text-align: center;
cursor: pointer;
transition: all 0.2s ease;
}
// Mock generated images
const generatedImages = [
{ id: 1, icon: '🎉', alt: '친구 초대 이벤트 이미지 1' },
{ id: 2, icon: '🎊', alt: '친구 초대 이벤트 이미지 2' },
{ id: 3, icon: '🎈', alt: '친구 초대 이벤트 이미지 3' },
{ id: 4, icon: '🎁', alt: '친구 초대 이벤트 이미지 4' },
{ id: 5, icon: '🌟', alt: '친구 초대 이벤트 이미지 5' },
{ id: 6, icon: '✨', alt: '친구 초대 이벤트 이미지 6' },
{ id: 7, icon: '💝', alt: '친구 초대 이벤트 이미지 7' },
{ id: 8, icon: '🎯', alt: '친구 초대 이벤트 이미지 8' }
];
.upload-area:hover {
border-color: var(--color-primary-main);
background: var(--color-gray-50);
// Simulate AI image generation
function simulateGeneration() {
const interval = setInterval(() => {
currentProgress += 12.5;
if (currentProgress >= 100) {
currentProgress = 100;
clearInterval(interval);
setTimeout(showResults, 500);
}
updateProgress(currentProgress);
}, 400);
}
.upload-placeholder {
display: flex;
flex-direction: column;
align-items: center;
}
function updateProgress(progress) {
progressBar.style.width = progress + '%';
progressText.textContent = Math.round(progress) + '%';
}
.upload-preview {
display: flex;
flex-direction: column;
align-items: center;
}
function showResults() {
loadingState.style.display = 'none';
addClass(imageResults, 'image-results--visible');
renderImages();
}
.progress-bar-container {
width: 100%;
background: var(--color-gray-200);
border-radius: var(--radius-full);
overflow: hidden;
}
function renderImages() {
imageGrid.innerHTML = generatedImages.map(image => `
<div class="image-item" data-id="${image.id}">
<div class="image-item__preview">${image.icon}</div>
<div class="image-item__checkbox"></div>
</div>
`).join('');
.progress-bar-fill {
height: 100%;
border-radius: var(--radius-full);
transition: width 0.3s ease;
}
// Add click listeners
const items = imageGrid.querySelectorAll('.image-item');
items.forEach(item => {
item.addEventListener('click', () => {
const imageId = parseInt(item.getAttribute('data-id'));
toggleImage(imageId, item);
});
});
}
.image-option {
cursor: pointer;
transition: transform 0.2s ease;
}
function toggleImage(imageId, element) {
if (selectedImages.has(imageId)) {
selectedImages.delete(imageId);
removeClass(element, 'image-item--selected');
} else {
selectedImages.add(imageId);
addClass(element, 'image-item--selected');
}
updateNextButton();
}
.image-option:hover {
transform: scale(1.02);
}
function updateNextButton() {
const count = selectedImages.size;
if (count > 0) {
nextButton.disabled = false;
nextButton.textContent = `선택 완료 (${count}개)`;
} else {
nextButton.disabled = true;
nextButton.textContent = '선택 완료 (최소 1개)';
}
}
.image-preview {
position: relative;
}
// Handle next button
nextButton.addEventListener('click', () => {
if (selectedImages.size === 0) return;
.selection-badge {
position: absolute;
top: var(--spacing-m);
right: var(--spacing-m);
width: 40px;
height: 40px;
background: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
// Save selected images
const eventContent = storage.get('eventContent') || {};
eventContent.generatedImages = {
selectedImages: Array.from(selectedImages),
images: generatedImages.filter(img => selectedImages.has(img.id)),
createdAt: new Date().toISOString()
};
storage.set('eventContent', eventContent);
.selection-badge .material-icons {
color: var(--color-success);
font-size: 32px;
}
</style>
// Show loading
addClass(nextButton, 'next-button--loading');
nextButton.disabled = true;
// Navigate to next screen
setTimeout(() => {
window.location.href = '10-AI영상제작.html';
}, 800);
});
// Initialize
window.addEventListener('load', () => {
setTimeout(() => {
simulateGeneration();
}, 500);
});
</script>
</body>
</html>

View File

@ -0,0 +1,719 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>이벤트 기획안 승인 - KT 이벤트 마케팅</title>
<link rel="stylesheet" href="css/variables.css">
<link rel="stylesheet" href="css/common.css">
<style>
/* Layout */
.approval-container {
min-height: 100vh;
background: linear-gradient(135deg, #F8F9FF 0%, #FFFFFF 100%);
padding-bottom: var(--spacing-2xl);
}
/* Header */
.approval-header {
background: linear-gradient(135deg, var(--color-secondary) 0%, var(--color-secondary-dark) 100%);
color: var(--color-white);
padding: var(--spacing-xl) var(--spacing-m);
box-shadow: var(--shadow-lg);
}
.approval-header__back {
background: none;
border: none;
color: var(--color-white);
font-size: var(--font-size-xl);
padding: 0;
margin-bottom: var(--spacing-m);
cursor: pointer;
}
.approval-header__badge {
display: inline-block;
background: rgba(255, 255, 255, 0.2);
padding: var(--spacing-xs) var(--spacing-m);
border-radius: var(--radius-s);
font-size: var(--font-size-s);
margin-bottom: var(--spacing-s);
}
.approval-header__title {
font-size: var(--font-size-2xl);
font-weight: var(--font-weight-bold);
margin-bottom: var(--spacing-xs);
}
.approval-header__subtitle {
font-size: var(--font-size-m);
opacity: 0.9;
}
/* Content */
.approval-content {
padding: var(--spacing-l) var(--spacing-m);
}
/* Success Message */
.success-message {
background: linear-gradient(135deg, #F0FFF4 0%, #FFFFFF 100%);
border-left: 4px solid var(--color-success);
padding: var(--spacing-m);
border-radius: var(--radius-m);
margin-bottom: var(--spacing-l);
}
.success-message__icon {
font-size: 48px;
text-align: center;
margin-bottom: var(--spacing-s);
}
.success-message__title {
font-size: var(--font-size-l);
font-weight: var(--font-weight-bold);
color: var(--color-text-primary);
text-align: center;
margin-bottom: var(--spacing-xs);
}
.success-message__text {
font-size: var(--font-size-s);
color: var(--color-text-secondary);
text-align: center;
line-height: 1.6;
}
/* AI Prediction */
.ai-prediction {
background: var(--color-white);
border: 2px solid var(--color-secondary-light);
border-radius: var(--radius-l);
padding: var(--spacing-m);
margin-bottom: var(--spacing-l);
}
.ai-prediction__header {
display: flex;
align-items: center;
gap: var(--spacing-s);
margin-bottom: var(--spacing-m);
}
.ai-prediction__icon {
font-size: var(--font-size-xl);
}
.ai-prediction__title {
font-size: var(--font-size-m);
font-weight: var(--font-weight-semibold);
color: var(--color-text-primary);
}
.ai-prediction__stats {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: var(--spacing-s);
}
.prediction-stat {
text-align: center;
padding: var(--spacing-s);
background: linear-gradient(135deg, #F0F7FF 0%, #FFFFFF 100%);
border-radius: var(--radius-m);
}
.prediction-stat__value {
font-size: var(--font-size-xl);
font-weight: var(--font-weight-bold);
color: var(--color-secondary);
margin-bottom: var(--spacing-xs);
}
.prediction-stat__label {
font-size: var(--font-size-xs);
color: var(--color-text-secondary);
}
/* Event Preview */
.event-preview {
background: var(--color-white);
border-radius: var(--radius-l);
padding: var(--spacing-l);
box-shadow: var(--shadow-md);
margin-bottom: var(--spacing-l);
}
.event-preview__label {
font-size: var(--font-size-s);
font-weight: var(--font-weight-semibold);
color: var(--color-text-secondary);
margin-bottom: var(--spacing-m);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.event-preview__card {
background: linear-gradient(135deg, var(--color-primary) 0%, var(--color-primary-dark) 100%);
color: var(--color-white);
padding: var(--spacing-l);
border-radius: var(--radius-l);
position: relative;
overflow: hidden;
}
.event-preview__card::before {
content: '';
position: absolute;
top: -50%;
right: -10%;
width: 200px;
height: 200px;
background: rgba(255, 255, 255, 0.1);
border-radius: 50%;
}
.event-preview__headline {
font-size: var(--font-size-xl);
font-weight: var(--font-weight-bold);
margin-bottom: var(--spacing-m);
position: relative;
z-index: 1;
}
.event-preview__body {
font-size: var(--font-size-m);
line-height: 1.6;
margin-bottom: var(--spacing-m);
opacity: 0.95;
position: relative;
z-index: 1;
}
.event-preview__hashtags {
display: flex;
flex-wrap: wrap;
gap: var(--spacing-xs);
position: relative;
z-index: 1;
}
.event-preview__hashtag {
background: rgba(255, 255, 255, 0.2);
padding: var(--spacing-xs) var(--spacing-s);
border-radius: var(--radius-s);
font-size: var(--font-size-xs);
}
/* Summary Sections */
.summary-sections {
margin-bottom: var(--spacing-l);
}
.summary-section {
background: var(--color-white);
border-radius: var(--radius-l);
margin-bottom: var(--spacing-m);
overflow: hidden;
box-shadow: var(--shadow-sm);
}
.summary-section__header {
padding: var(--spacing-m);
background: linear-gradient(135deg, #F8F9FF 0%, #FFFFFF 100%);
border-bottom: 1px solid var(--color-border-light);
display: flex;
justify-content: space-between;
align-items: center;
}
.summary-section__title {
font-size: var(--font-size-m);
font-weight: var(--font-weight-semibold);
color: var(--color-text-primary);
display: flex;
align-items: center;
gap: var(--spacing-s);
}
.summary-section__icon {
font-size: var(--font-size-l);
}
.summary-section__edit {
background: none;
border: 1px solid var(--color-border-light);
color: var(--color-text-secondary);
padding: var(--spacing-xs) var(--spacing-s);
border-radius: var(--radius-s);
font-size: var(--font-size-xs);
cursor: pointer;
transition: all var(--transition-normal);
}
.summary-section__edit:hover {
border-color: var(--color-secondary);
color: var(--color-secondary);
}
.summary-section__content {
padding: var(--spacing-m);
}
.summary-item {
margin-bottom: var(--spacing-s);
}
.summary-item:last-child {
margin-bottom: 0;
}
.summary-item__label {
font-size: var(--font-size-xs);
color: var(--color-text-secondary);
margin-bottom: var(--spacing-xs);
}
.summary-item__value {
font-size: var(--font-size-m);
color: var(--color-text-primary);
font-weight: var(--font-weight-medium);
}
.summary-item__badge {
display: inline-block;
background: var(--color-secondary-light);
color: var(--color-secondary);
padding: 2px var(--spacing-s);
border-radius: var(--radius-s);
font-size: var(--font-size-xs);
margin-right: var(--spacing-xs);
}
/* Action Buttons */
.action-buttons {
display: grid;
gap: var(--spacing-m);
}
.action-button {
width: 100%;
padding: var(--spacing-m);
border: none;
border-radius: var(--radius-m);
font-size: var(--font-size-m);
font-weight: var(--font-weight-semibold);
cursor: pointer;
transition: all var(--transition-normal);
box-shadow: var(--shadow-md);
}
.action-button--primary {
background: linear-gradient(135deg, var(--color-secondary) 0%, var(--color-secondary-dark) 100%);
color: var(--color-white);
}
.action-button--primary:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-lg);
}
.action-button--secondary {
background: var(--color-white);
border: 2px solid var(--color-border-light);
color: var(--color-text-primary);
}
.action-button--secondary:hover {
border-color: var(--color-secondary);
color: var(--color-secondary);
}
.action-button--loading {
position: relative;
color: transparent;
}
.action-button--loading:after {
content: '';
position: absolute;
width: 20px;
height: 20px;
top: 50%;
left: 50%;
margin-left: -10px;
margin-top: -10px;
border: 3px solid rgba(255, 255, 255, 0.3);
border-radius: 50%;
border-top-color: var(--color-white);
animation: spinner 0.6s linear infinite;
}
@keyframes spinner {
to { transform: rotate(360deg); }
}
/* Tablet */
@media (min-width: 768px) {
.approval-header {
padding: var(--spacing-2xl) var(--spacing-xl);
}
.approval-content {
max-width: 720px;
margin: 0 auto;
padding: var(--spacing-xl);
}
.action-buttons {
grid-template-columns: 1fr 1fr;
}
}
/* Desktop */
@media (min-width: 1024px) {
.approval-content {
max-width: 960px;
}
}
</style>
</head>
<body>
<div class="approval-container">
<!-- Header -->
<header class="approval-header">
<button class="approval-header__back" onclick="history.back()">
</button>
<div class="approval-header__badge">✨ AI 기획 완료</div>
<h1 class="approval-header__title">이벤트 기획안 승인</h1>
<p class="approval-header__subtitle">AI가 생성한 이벤트 기획안을 확인하고 승인해 주세요</p>
</header>
<!-- Content -->
<main class="approval-content">
<!-- Success Message -->
<div class="success-message">
<div class="success-message__icon">🎉</div>
<h2 class="success-message__title">완벽한 이벤트 기획안이 완성되었어요!</h2>
<p class="success-message__text">
AI가 분석한 데이터를 바탕으로 최적의 이벤트를 설계했습니다.
아래 내용을 확인하고 승인하시면 다음 단계로 진행됩니다.
</p>
</div>
<!-- AI Prediction -->
<div class="ai-prediction">
<div class="ai-prediction__header">
<span class="ai-prediction__icon">🤖</span>
<h3 class="ai-prediction__title">AI 예상 성과</h3>
</div>
<div class="ai-prediction__stats">
<div class="prediction-stat">
<div class="prediction-stat__value">1,200명</div>
<div class="prediction-stat__label">예상 참여자</div>
</div>
<div class="prediction-stat">
<div class="prediction-stat__value">75%</div>
<div class="prediction-stat__label">참여율</div>
</div>
<div class="prediction-stat">
<div class="prediction-stat__value">3.5배</div>
<div class="prediction-stat__label">ROI</div>
</div>
</div>
</div>
<!-- Event Preview -->
<div class="event-preview">
<div class="event-preview__label">📱 이벤트 미리보기</div>
<div class="event-preview__card">
<h3 class="event-preview__headline" id="previewHeadline">
🎉 친구 초대하고 함께 받는 특별한 혜택!
</h3>
<p class="event-preview__body" id="previewBody">
맛있는 순간을 소중한 사람과 함께하세요. 친구를 초대하면 둘 다 특별한 선물을 드려요.
지금 바로 참여하고 행복을 나누세요!
</p>
<div class="event-preview__hashtags" id="previewHashtags">
<span class="event-preview__hashtag">#친구초대이벤트</span>
<span class="event-preview__hashtag">#함께할인</span>
<span class="event-preview__hashtag">#소상공인응원</span>
</div>
</div>
</div>
<!-- Summary Sections -->
<div class="summary-sections">
<!-- Purpose -->
<div class="summary-section">
<div class="summary-section__header">
<h3 class="summary-section__title">
<span class="summary-section__icon">🎯</span>
이벤트 목적
</h3>
<button class="summary-section__edit" onclick="window.location.href='04-이벤트목적선택.html'">
수정
</button>
</div>
<div class="summary-section__content">
<div class="summary-item">
<div class="summary-item__label">선택한 목적</div>
<div class="summary-item__value" id="purposeValue">신규 고객 유치</div>
</div>
</div>
</div>
<!-- Trend Analysis -->
<div class="summary-section">
<div class="summary-section__header">
<h3 class="summary-section__title">
<span class="summary-section__icon">📊</span>
트렌드 분석
</h3>
<button class="summary-section__edit" onclick="window.location.href='05-AI트렌드분석결과.html'">
수정
</button>
</div>
<div class="summary-section__content">
<div class="summary-item">
<div class="summary-item__label">Top 트렌드</div>
<div class="summary-item__value" id="trendValue">친구 초대 이벤트</div>
</div>
<div class="summary-item">
<div class="summary-item__label">예상 참여율</div>
<div class="summary-item__value" id="engagementValue">68%</div>
</div>
</div>
</div>
<!-- Prize -->
<div class="summary-section">
<div class="summary-section__header">
<h3 class="summary-section__title">
<span class="summary-section__icon">🎁</span>
이벤트 경품
</h3>
<button class="summary-section__edit" onclick="window.location.href='06-AI이벤트상품추천.html'">
수정
</button>
</div>
<div class="summary-section__content">
<div class="summary-item">
<div class="summary-item__label">선택한 경품</div>
<div class="summary-item__value" id="prizeValue">
<span class="summary-item__badge">추천형</span>
친구 초대 특전
</div>
</div>
</div>
</div>
<!-- Participation Method -->
<div class="summary-section">
<div class="summary-section__header">
<h3 class="summary-section__title">
<span class="summary-section__icon">📱</span>
참여 방법
</h3>
<button class="summary-section__edit" onclick="window.location.href='07-AI참여방법설계.html'">
수정
</button>
</div>
<div class="summary-section__content">
<div class="summary-item">
<div class="summary-item__label">선택한 방법</div>
<div class="summary-item__value" id="participationValue">
<span class="summary-item__badge">바이럴형</span>
SNS 공유하기
</div>
</div>
</div>
</div>
<!-- Promotional Copy -->
<div class="summary-section">
<div class="summary-section__header">
<h3 class="summary-section__title">
<span class="summary-section__icon">✍️</span>
홍보 문구
</h3>
<button class="summary-section__edit" onclick="window.location.href='08-AI홍보문구생성.html'">
수정
</button>
</div>
<div class="summary-section__content">
<div class="summary-item">
<div class="summary-item__label">헤드라인</div>
<div class="summary-item__value" id="copyHeadlineValue">
🎉 친구 초대하고 함께 받는 특별한 혜택!
</div>
</div>
<div class="summary-item">
<div class="summary-item__label">CTA</div>
<div class="summary-item__value" id="copyCTAValue">
지금 친구 초대하고 혜택 받기
</div>
</div>
</div>
</div>
</div>
<!-- Action Buttons -->
<div class="action-buttons">
<button class="action-button action-button--secondary" id="saveDraftButton">
📝 임시저장
</button>
<button class="action-button action-button--primary" id="approveButton">
✅ 승인하고 다음으로
</button>
</div>
</main>
</div>
<script src="js/common.js"></script>
<script>
const { $, addClass, storage } = window.CommonUtils;
// Elements
const previewHeadline = $('#previewHeadline');
const previewBody = $('#previewBody');
const previewHashtags = $('#previewHashtags');
const purposeValue = $('#purposeValue');
const trendValue = $('#trendValue');
const engagementValue = $('#engagementValue');
const prizeValue = $('#prizeValue');
const participationValue = $('#participationValue');
const copyHeadlineValue = $('#copyHeadlineValue');
const copyCTAValue = $('#copyCTAValue');
const saveDraftButton = $('#saveDraftButton');
const approveButton = $('#approveButton');
// Load event draft data
const eventDraft = storage.get('eventDraft') || {};
// Purpose labels
const purposeLabels = {
'new-customers': '신규 고객 유치',
'repeat-visits': '재방문 유도',
'sales-boost': '매출 증대',
'brand-awareness': '브랜드 인지도',
'engagement': '고객 참여 활성화',
'seasonal': '시즌/기념일 마케팅'
};
// Prize labels
const prizeLabels = {
'friend-invite': '친구 초대 특전',
'discount': '할인 쿠폰',
'freebie': '무료 증정'
};
// Participation method labels
const participationLabels = {
'sns-share': 'SNS 공유하기',
'qr-scan': 'QR 코드 스캔',
'coupon-download': '쿠폰 다운로드',
'store-visit': '매장 방문 인증',
'survey': '설문 참여',
'friend-invite': '친구 초대'
};
// Update summary with saved data
function updateSummary() {
// Purpose
if (eventDraft.purpose) {
purposeValue.textContent = purposeLabels[eventDraft.purpose] || eventDraft.purpose;
}
// Trend Analysis
if (eventDraft.trendAnalysis) {
trendValue.textContent = eventDraft.trendAnalysis.topTrend || '친구 초대 이벤트';
engagementValue.textContent = eventDraft.trendAnalysis.insights?.engagement || '68%';
}
// Prize
if (eventDraft.prize) {
const prizeText = eventDraft.prize.custom || prizeLabels[eventDraft.prize.type] || '친구 초대 특전';
const badge = eventDraft.prize.type === 'friend-invite' ? '추천형' :
eventDraft.prize.type === 'discount' ? '즉시 혜택형' :
eventDraft.prize.type === 'freebie' ? '체험형' : '';
prizeValue.innerHTML = badge ?
`<span class="summary-item__badge">${badge}</span>${prizeText}` :
prizeText;
}
// Participation Method
if (eventDraft.participationMethod) {
const methodText = eventDraft.participationMethod.custom ||
participationLabels[eventDraft.participationMethod.type] ||
'SNS 공유하기';
const badge = eventDraft.participationMethod.type === 'sns-share' ? '바이럴형' :
eventDraft.participationMethod.type === 'qr-scan' ? '오프라인형' :
eventDraft.participationMethod.type === 'coupon-download' ? '즉시 혜택형' :
eventDraft.participationMethod.type === 'store-visit' ? '오프라인형' :
eventDraft.participationMethod.type === 'survey' ? '데이터 수집형' :
eventDraft.participationMethod.type === 'friend-invite' ? '추천형' : '';
participationValue.innerHTML = badge ?
`<span class="summary-item__badge">${badge}</span>${methodText}` :
methodText;
}
// Promotional Copy
if (eventDraft.promotionalCopy) {
const copy = eventDraft.promotionalCopy;
copyHeadlineValue.textContent = copy.headline || '🎉 친구 초대하고 함께 받는 특별한 혜택!';
copyCTAValue.textContent = copy.cta || '지금 친구 초대하고 혜택 받기';
// Update preview
previewHeadline.textContent = copy.headline || '🎉 친구 초대하고 함께 받는 특별한 혜택!';
previewBody.textContent = copy.body || '맛있는 순간을 소중한 사람과 함께하세요.';
if (copy.hashtags && copy.hashtags.length > 0) {
previewHashtags.innerHTML = copy.hashtags
.map(tag => `<span class="event-preview__hashtag">#${tag}</span>`)
.join('');
}
}
}
// Save draft
saveDraftButton.addEventListener('click', () => {
// Mark as draft
eventDraft.status = 'draft';
eventDraft.savedAt = new Date().toISOString();
storage.set('eventDraft', eventDraft);
alert('✅ 이벤트 기획안이 임시저장되었습니다.');
});
// Approve and continue
approveButton.addEventListener('click', () => {
// Mark as approved
eventDraft.status = 'approved';
eventDraft.approvedAt = new Date().toISOString();
storage.set('eventDraft', eventDraft);
// Show loading
addClass(approveButton, 'action-button--loading');
approveButton.disabled = true;
// Navigate to next phase (AI content generation)
setTimeout(() => {
window.location.href = '10-AI이미지생성.html';
}, 1000);
});
// Initialize
window.addEventListener('load', () => {
updateSummary();
});
</script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,743 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI 이미지 생성 - KT 이벤트 마케팅</title>
<link rel="stylesheet" href="css/variables.css">
<link rel="stylesheet" href="css/common.css">
<style>
/* Layout */
.image-container {
min-height: 100vh;
background: linear-gradient(135deg, #F8F9FF 0%, #FFFFFF 100%);
padding-bottom: var(--spacing-2xl);
}
/* Header */
.image-header {
background: linear-gradient(135deg, var(--color-secondary) 0%, var(--color-secondary-dark) 100%);
color: var(--color-white);
padding: var(--spacing-xl) var(--spacing-m);
box-shadow: var(--shadow-lg);
}
.image-header__back {
background: none;
border: none;
color: var(--color-white);
font-size: var(--font-size-xl);
padding: 0;
margin-bottom: var(--spacing-m);
cursor: pointer;
}
.image-header__title {
font-size: var(--font-size-2xl);
font-weight: var(--font-weight-bold);
margin-bottom: var(--spacing-xs);
}
.image-header__subtitle {
font-size: var(--font-size-m);
opacity: 0.9;
}
/* Content */
.image-content {
padding: var(--spacing-l) var(--spacing-m);
}
/* Prompt Section */
.prompt-section {
background: var(--color-white);
border-radius: var(--radius-l);
padding: var(--spacing-m);
margin-bottom: var(--spacing-l);
box-shadow: var(--shadow-sm);
}
.prompt-section__label {
font-size: var(--font-size-m);
font-weight: var(--font-weight-semibold);
color: var(--color-text-primary);
margin-bottom: var(--spacing-s);
display: flex;
align-items: center;
gap: var(--spacing-xs);
}
.prompt-section__textarea {
width: 100%;
min-height: 100px;
padding: var(--spacing-m);
border: 1px solid var(--color-border-light);
border-radius: var(--radius-m);
font-size: var(--font-size-m);
font-family: var(--font-family);
resize: vertical;
transition: all var(--transition-normal);
margin-bottom: var(--spacing-s);
}
.prompt-section__textarea:focus {
outline: none;
border-color: var(--color-secondary);
box-shadow: 0 0 0 3px rgba(0, 102, 255, 0.1);
}
.prompt-section__hint {
font-size: var(--font-size-xs);
color: var(--color-text-tertiary);
margin-bottom: var(--spacing-m);
}
.prompt-section__styles {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: var(--spacing-s);
margin-bottom: var(--spacing-m);
}
.style-chip {
padding: var(--spacing-s);
border: 2px solid var(--color-border-light);
border-radius: var(--radius-m);
text-align: center;
font-size: var(--font-size-s);
cursor: pointer;
transition: all var(--transition-normal);
}
.style-chip:hover {
border-color: var(--color-secondary);
}
.style-chip--selected {
border-color: var(--color-secondary);
background: linear-gradient(135deg, #F0F7FF 0%, #FFFFFF 100%);
color: var(--color-secondary);
font-weight: var(--font-weight-semibold);
}
.generate-button {
width: 100%;
padding: var(--spacing-m);
background: linear-gradient(135deg, var(--color-secondary) 0%, var(--color-secondary-dark) 100%);
color: var(--color-white);
border: none;
border-radius: var(--radius-m);
font-size: var(--font-size-m);
font-weight: var(--font-weight-semibold);
cursor: pointer;
transition: all var(--transition-normal);
}
.generate-button:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: var(--shadow-md);
}
.generate-button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.generate-button--loading {
position: relative;
color: transparent;
}
.generate-button--loading:after {
content: '';
position: absolute;
width: 20px;
height: 20px;
top: 50%;
left: 50%;
margin-left: -10px;
margin-top: -10px;
border: 3px solid rgba(255, 255, 255, 0.3);
border-radius: 50%;
border-top-color: var(--color-white);
animation: spinner 0.6s linear infinite;
}
/* Loading State */
.loading-state {
display: none;
text-align: center;
padding: var(--spacing-2xl) var(--spacing-m);
}
.loading-state--visible {
display: block;
}
.loading-animation {
width: 80px;
height: 80px;
margin: 0 auto var(--spacing-l);
position: relative;
}
.loading-animation__circle {
position: absolute;
width: 100%;
height: 100%;
border: 4px solid rgba(0, 102, 255, 0.1);
border-radius: 50%;
border-top-color: var(--color-secondary);
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.loading-animation__icon {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 32px;
}
.loading-state__text {
font-size: var(--font-size-l);
font-weight: var(--font-weight-semibold);
color: var(--color-text-primary);
}
/* Results Section */
.results-section {
display: none;
}
.results-section--visible {
display: block;
}
.results-section__header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--spacing-m);
}
.results-section__title {
font-size: var(--font-size-l);
font-weight: var(--font-weight-bold);
color: var(--color-text-primary);
}
.results-section__regenerate {
background: none;
border: 1px solid var(--color-border-light);
color: var(--color-text-secondary);
padding: var(--spacing-xs) var(--spacing-m);
border-radius: var(--radius-m);
font-size: var(--font-size-s);
cursor: pointer;
transition: all var(--transition-normal);
}
.results-section__regenerate:hover {
border-color: var(--color-secondary);
color: var(--color-secondary);
}
/* Image Grid */
.image-grid {
display: grid;
gap: var(--spacing-m);
margin-bottom: var(--spacing-l);
}
/* Image Card */
.image-card {
background: var(--color-white);
border: 2px solid var(--color-border-light);
border-radius: var(--radius-l);
overflow: hidden;
cursor: pointer;
transition: all var(--transition-normal);
position: relative;
}
.image-card:hover {
border-color: var(--color-secondary);
box-shadow: var(--shadow-md);
transform: translateY(-2px);
}
.image-card--selected {
border-color: var(--color-secondary);
box-shadow: var(--shadow-lg);
}
.image-card__badge {
position: absolute;
top: var(--spacing-s);
right: var(--spacing-s);
background: linear-gradient(135deg, var(--color-secondary) 0%, var(--color-secondary-dark) 100%);
color: var(--color-white);
padding: var(--spacing-xs) var(--spacing-s);
border-radius: var(--radius-s);
font-size: var(--font-size-xs);
font-weight: var(--font-weight-semibold);
box-shadow: var(--shadow-md);
z-index: 1;
}
.image-card__checkbox {
position: absolute;
top: var(--spacing-s);
left: var(--spacing-s);
width: 24px;
height: 24px;
z-index: 1;
cursor: pointer;
}
.image-card__preview {
width: 100%;
aspect-ratio: 16 / 9;
background: linear-gradient(135deg, #E0E7FF 0%, #F0F7FF 100%);
display: flex;
align-items: center;
justify-content: center;
font-size: 48px;
position: relative;
}
.image-card__info {
padding: var(--spacing-m);
}
.image-card__title {
font-size: var(--font-size-m);
font-weight: var(--font-weight-semibold);
color: var(--color-text-primary);
margin-bottom: var(--spacing-xs);
}
.image-card__style {
font-size: var(--font-size-xs);
color: var(--color-text-secondary);
margin-bottom: var(--spacing-s);
}
.image-card__actions {
display: flex;
gap: var(--spacing-xs);
padding-top: var(--spacing-s);
border-top: 1px solid var(--color-border-light);
}
.image-card__action-btn {
flex: 1;
padding: var(--spacing-xs);
background: var(--color-white);
border: 1px solid var(--color-border-light);
border-radius: var(--radius-s);
font-size: var(--font-size-xs);
cursor: pointer;
transition: all var(--transition-normal);
}
.image-card__action-btn:hover {
border-color: var(--color-secondary);
color: var(--color-secondary);
}
/* Next Button */
.next-button {
width: 100%;
padding: var(--spacing-m);
background: linear-gradient(135deg, var(--color-secondary) 0%, var(--color-secondary-dark) 100%);
color: var(--color-white);
border: none;
border-radius: var(--radius-m);
font-size: var(--font-size-m);
font-weight: var(--font-weight-semibold);
cursor: pointer;
transition: all var(--transition-normal);
box-shadow: var(--shadow-md);
}
.next-button:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: var(--shadow-lg);
}
.next-button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.next-button--loading {
position: relative;
color: transparent;
}
.next-button--loading:after {
content: '';
position: absolute;
width: 20px;
height: 20px;
top: 50%;
left: 50%;
margin-left: -10px;
margin-top: -10px;
border: 3px solid rgba(255, 255, 255, 0.3);
border-radius: 50%;
border-top-color: var(--color-white);
animation: spinner 0.6s linear infinite;
}
@keyframes spinner {
to { transform: rotate(360deg); }
}
/* Tablet */
@media (min-width: 768px) {
.image-header {
padding: var(--spacing-2xl) var(--spacing-xl);
}
.image-content {
max-width: 720px;
margin: 0 auto;
padding: var(--spacing-xl);
}
.image-grid {
grid-template-columns: repeat(2, 1fr);
}
.prompt-section__styles {
grid-template-columns: repeat(4, 1fr);
}
}
/* Desktop */
@media (min-width: 1024px) {
.image-content {
max-width: 960px;
}
}
</style>
</head>
<body>
<div class="image-container">
<!-- Header -->
<header class="image-header">
<button class="image-header__back" onclick="history.back()">
</button>
<h1 class="image-header__title">AI 이미지 생성</h1>
<p class="image-header__subtitle">이벤트에 활용할 이미지를 AI로 생성해 보세요</p>
</header>
<!-- Content -->
<main class="image-content">
<!-- Prompt Section -->
<div class="prompt-section" id="promptSection">
<label class="prompt-section__label">
✨ 원하는 이미지 설명
</label>
<textarea
class="prompt-section__textarea"
id="promptInput"
placeholder="예: 친구들이 함께 행복하게 웃고 있는 모습, 밝고 따뜻한 분위기"
maxlength="500"
></textarea>
<p class="prompt-section__hint">
생성하고 싶은 이미지를 자세히 설명해 주세요 (최대 500자)
</p>
<label class="prompt-section__label">
🎨 이미지 스타일
</label>
<div class="prompt-section__styles">
<div class="style-chip style-chip--selected" data-style="modern">
🌟 모던
</div>
<div class="style-chip" data-style="warm">
☀️ 따뜻한
</div>
<div class="style-chip" data-style="vibrant">
🎨 생동감
</div>
<div class="style-chip" data-style="minimal">
✨ 미니멀
</div>
</div>
<button class="generate-button" id="generateButton">
🎨 이미지 생성하기
</button>
</div>
<!-- Loading State -->
<div class="loading-state" id="loadingState">
<div class="loading-animation">
<div class="loading-animation__circle"></div>
<div class="loading-animation__icon">🎨</div>
</div>
<p class="loading-state__text" id="loadingText">
AI가 이미지를 생성하고 있어요...
</p>
</div>
<!-- Results Section -->
<div class="results-section" id="resultsSection">
<div class="results-section__header">
<h2 class="results-section__title">생성된 이미지</h2>
<button class="results-section__regenerate" id="regenerateButton">
🔄 다시 생성
</button>
</div>
<!-- Image Grid -->
<div class="image-grid" id="imageGrid">
<!-- Image cards will be inserted here -->
</div>
<!-- Next Button -->
<button class="next-button" id="nextButton" disabled>
다음 단계로
</button>
</div>
</main>
</div>
<script src="js/common.js"></script>
<script>
const { $, addClass, removeClass, storage } = window.CommonUtils;
// Elements
const promptSection = $('#promptSection');
const promptInput = $('#promptInput');
const styleChips = document.querySelectorAll('.style-chip');
const generateButton = $('#generateButton');
const loadingState = $('#loadingState');
const loadingText = $('#loadingText');
const resultsSection = $('#resultsSection');
const imageGrid = $('#imageGrid');
const regenerateButton = $('#regenerateButton');
const nextButton = $('#nextButton');
// State
let selectedStyle = 'modern';
let selectedImages = [];
const generatedImages = [];
// Handle style selection
styleChips.forEach(chip => {
chip.addEventListener('click', () => {
styleChips.forEach(c => removeClass(c, 'style-chip--selected'));
addClass(chip, 'style-chip--selected');
selectedStyle = chip.getAttribute('data-style');
});
});
// Generate images
generateButton.addEventListener('click', () => {
const prompt = promptInput.value.trim();
if (!prompt) {
alert('이미지 설명을 입력해 주세요.');
return;
}
generateImages(prompt, selectedStyle);
});
// Regenerate images
regenerateButton.addEventListener('click', () => {
const prompt = promptInput.value.trim();
generateImages(prompt, selectedStyle);
});
// Generate images function
function generateImages(prompt, style) {
// Hide prompt section and results
promptSection.style.display = 'none';
removeClass(resultsSection, 'results-section--visible');
// Show loading
addClass(loadingState, 'loading-state--visible');
// Simulate AI generation
const loadingTexts = [
'AI가 이미지를 생성하고 있어요...',
'이미지 구도를 설계하고 있어요...',
'색상과 스타일을 조정하고 있어요...',
'마지막 마무리 중이에요...'
];
let step = 0;
const loadingInterval = setInterval(() => {
if (step < loadingTexts.length) {
loadingText.textContent = loadingTexts[step];
step++;
}
}, 1200);
// Show results after delay
setTimeout(() => {
clearInterval(loadingInterval);
removeClass(loadingState, 'loading-state--visible');
renderImages(prompt, style);
addClass(resultsSection, 'results-section--visible');
promptSection.style.display = 'block';
}, 5000);
}
// Render generated images
function renderImages(prompt, style) {
const imageVariations = [
{
id: 1,
title: '옵션 1',
style: style === 'modern' ? '모던 스타일' :
style === 'warm' ? '따뜻한 스타일' :
style === 'vibrant' ? '생동감 있는 스타일' :
'미니멀 스타일',
icon: '🎉',
recommended: true
},
{
id: 2,
title: '옵션 2',
style: style === 'modern' ? '모던 스타일' :
style === 'warm' ? '따뜻한 스타일' :
style === 'vibrant' ? '생동감 있는 스타일' :
'미니멀 스타일',
icon: '🎊',
recommended: false
},
{
id: 3,
title: '옵션 3',
style: style === 'modern' ? '모던 스타일' :
style === 'warm' ? '따뜻한 스타일' :
style === 'vibrant' ? '생동감 있는 스타일' :
'미니멀 스타일',
icon: '🎈',
recommended: false
},
{
id: 4,
title: '옵션 4',
style: style === 'modern' ? '모던 스타일' :
style === 'warm' ? '따뜻한 스타일' :
style === 'vibrant' ? '생동감 있는 스타일' :
'미니멀 스타일',
icon: '🎁',
recommended: false
}
];
imageGrid.innerHTML = imageVariations.map(image => `
<div class="image-card" data-id="${image.id}">
${image.recommended ? '<div class="image-card__badge">🤖 AI 추천</div>' : ''}
<input type="checkbox" class="image-card__checkbox" value="${image.id}">
<div class="image-card__preview">${image.icon}</div>
<div class="image-card__info">
<h3 class="image-card__title">${image.title}</h3>
<p class="image-card__style">${image.style}</p>
<div class="image-card__actions">
<button class="image-card__action-btn" onclick="downloadImage(${image.id})">
💾 저장
</button>
<button class="image-card__action-btn" onclick="editImage(${image.id})">
✏️ 수정
</button>
</div>
</div>
</div>
`).join('');
// Add event listeners
const cards = document.querySelectorAll('.image-card');
cards.forEach(card => {
const checkbox = card.querySelector('.image-card__checkbox');
card.addEventListener('click', (e) => {
if (e.target.closest('.image-card__action-btn')) return;
checkbox.checked = !checkbox.checked;
toggleImageSelection(card, checkbox.checked);
});
checkbox.addEventListener('click', (e) => {
e.stopPropagation();
toggleImageSelection(card, checkbox.checked);
});
});
}
// Toggle image selection
function toggleImageSelection(card, isSelected) {
const id = parseInt(card.getAttribute('data-id'));
if (isSelected) {
addClass(card, 'image-card--selected');
if (!selectedImages.includes(id)) {
selectedImages.push(id);
}
} else {
removeClass(card, 'image-card--selected');
selectedImages = selectedImages.filter(imageId => imageId !== id);
}
nextButton.disabled = selectedImages.length === 0;
}
// Download image
window.downloadImage = function(id) {
alert(`이미지 ${id}를 다운로드했습니다.`);
};
// Edit image
window.editImage = function(id) {
alert(`이미지 ${id} 편집 기능은 다음 버전에서 제공됩니다.`);
};
// Handle next button
nextButton.addEventListener('click', () => {
if (selectedImages.length === 0) return;
// Save selected images
const eventContent = storage.get('eventContent') || {};
eventContent.generatedImages = {
prompt: promptInput.value.trim(),
style: selectedStyle,
selectedImages: selectedImages,
generatedAt: new Date().toISOString()
};
storage.set('eventContent', eventContent);
// Show loading
addClass(nextButton, 'next-button--loading');
nextButton.disabled = true;
// Navigate to next screen
setTimeout(() => {
window.location.href = '11-SNS콘텐츠생성.html';
}, 800);
});
// Auto-fill prompt from event draft
window.addEventListener('load', () => {
const eventDraft = storage.get('eventDraft') || {};
if (eventDraft.promotionalCopy && !promptInput.value) {
promptInput.value = `${eventDraft.promotionalCopy.headline} 이미지`;
}
});
</script>
</body>
</html>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,561 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>QR 포스터 템플릿 선택 - KT 이벤트 마케팅</title>
<link rel="stylesheet" href="css/variables.css">
<link rel="stylesheet" href="css/common.css">
<style>
/* Layout */
.poster-container {
min-height: 100vh;
background: linear-gradient(135deg, #F8F9FF 0%, #FFFFFF 100%);
padding-bottom: var(--spacing-2xl);
}
/* Header */
.poster-header {
background: linear-gradient(135deg, var(--color-secondary) 0%, var(--color-secondary-dark) 100%);
color: var(--color-white);
padding: var(--spacing-xl) var(--spacing-m);
box-shadow: var(--shadow-lg);
}
.poster-header__back {
background: none;
border: none;
color: var(--color-white);
font-size: var(--font-size-xl);
padding: 0;
margin-bottom: var(--spacing-m);
cursor: pointer;
}
.poster-header__title {
font-size: var(--font-size-2xl);
font-weight: var(--font-weight-bold);
margin-bottom: var(--spacing-xs);
}
.poster-header__subtitle {
font-size: var(--font-size-m);
opacity: 0.9;
}
/* Content */
.poster-content {
padding: var(--spacing-l) var(--spacing-m);
}
/* Size Selection */
.size-section {
background: var(--color-white);
border-radius: var(--radius-l);
padding: var(--spacing-m);
margin-bottom: var(--spacing-l);
box-shadow: var(--shadow-sm);
}
.size-section__label {
font-size: var(--font-size-m);
font-weight: var(--font-weight-semibold);
color: var(--color-text-primary);
margin-bottom: var(--spacing-m);
display: flex;
align-items: center;
gap: var(--spacing-xs);
}
.size-chips {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: var(--spacing-s);
}
.size-chip {
padding: var(--spacing-m);
border: 2px solid var(--color-border-light);
border-radius: var(--radius-m);
text-align: center;
cursor: pointer;
transition: all var(--transition-normal);
}
.size-chip:hover {
border-color: var(--color-secondary);
}
.size-chip--selected {
border-color: var(--color-secondary);
background: linear-gradient(135deg, #F0F7FF 0%, #FFFFFF 100%);
}
.size-chip__name {
font-size: var(--font-size-m);
font-weight: var(--font-weight-semibold);
color: var(--color-text-primary);
margin-bottom: var(--spacing-xs);
}
.size-chip__dimensions {
font-size: var(--font-size-xs);
color: var(--color-text-secondary);
}
/* Template Grid */
.template-grid {
display: grid;
gap: var(--spacing-m);
margin-bottom: var(--spacing-l);
}
/* Template Card */
.template-card {
background: var(--color-white);
border: 2px solid var(--color-border-light);
border-radius: var(--radius-l);
overflow: hidden;
cursor: pointer;
transition: all var(--transition-normal);
position: relative;
}
.template-card:hover {
border-color: var(--color-secondary);
box-shadow: var(--shadow-md);
transform: translateY(-2px);
}
.template-card--selected {
border-color: var(--color-secondary);
box-shadow: var(--shadow-lg);
}
.template-card__badge {
position: absolute;
top: var(--spacing-s);
right: var(--spacing-s);
background: linear-gradient(135deg, var(--color-secondary) 0%, var(--color-secondary-dark) 100%);
color: var(--color-white);
padding: var(--spacing-xs) var(--spacing-s);
border-radius: var(--radius-s);
font-size: var(--font-size-xs);
font-weight: var(--font-weight-semibold);
box-shadow: var(--shadow-md);
z-index: 1;
}
.template-card__radio {
position: absolute;
top: var(--spacing-s);
left: var(--spacing-s);
width: 20px;
height: 20px;
z-index: 1;
cursor: pointer;
}
.template-card__preview {
aspect-ratio: 3 / 4;
background: linear-gradient(135deg, #F8F9FF 0%, #FFFFFF 100%);
padding: var(--spacing-l);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: var(--spacing-m);
position: relative;
}
.template-preview__header {
text-align: center;
width: 100%;
}
.template-preview__title {
font-size: var(--font-size-xl);
font-weight: var(--font-weight-bold);
color: var(--color-text-primary);
margin-bottom: var(--spacing-s);
}
.template-preview__subtitle {
font-size: var(--font-size-s);
color: var(--color-text-secondary);
}
.template-preview__qr {
width: 120px;
height: 120px;
background: var(--color-white);
border: 2px solid var(--color-border-light);
border-radius: var(--radius-m);
display: flex;
align-items: center;
justify-content: center;
font-size: 48px;
}
.template-preview__footer {
text-align: center;
width: 100%;
padding: var(--spacing-s);
background: rgba(255, 255, 255, 0.8);
border-radius: var(--radius-m);
}
.template-preview__cta {
font-size: var(--font-size-m);
font-weight: var(--font-weight-semibold);
color: var(--color-primary);
}
.template-card__info {
padding: var(--spacing-m);
background: var(--color-white);
}
.template-card__name {
font-size: var(--font-size-m);
font-weight: var(--font-weight-semibold);
color: var(--color-text-primary);
margin-bottom: var(--spacing-xs);
}
.template-card__description {
font-size: var(--font-size-s);
color: var(--color-text-secondary);
line-height: 1.5;
margin-bottom: var(--spacing-s);
}
.template-card__features {
display: flex;
flex-wrap: wrap;
gap: var(--spacing-xs);
}
.feature-tag {
background: var(--color-secondary-light);
color: var(--color-secondary);
padding: 2px var(--spacing-s);
border-radius: var(--radius-s);
font-size: var(--font-size-xs);
}
/* Next Button */
.next-button {
width: 100%;
padding: var(--spacing-m);
background: linear-gradient(135deg, var(--color-secondary) 0%, var(--color-secondary-dark) 100%);
color: var(--color-white);
border: none;
border-radius: var(--radius-m);
font-size: var(--font-size-m);
font-weight: var(--font-weight-semibold);
cursor: pointer;
transition: all var(--transition-normal);
box-shadow: var(--shadow-md);
}
.next-button:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: var(--shadow-lg);
}
.next-button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.next-button--loading {
position: relative;
color: transparent;
}
.next-button--loading:after {
content: '';
position: absolute;
width: 20px;
height: 20px;
top: 50%;
left: 50%;
margin-left: -10px;
margin-top: -10px;
border: 3px solid rgba(255, 255, 255, 0.3);
border-radius: 50%;
border-top-color: var(--color-white);
animation: spinner 0.6s linear infinite;
}
@keyframes spinner {
to { transform: rotate(360deg); }
}
/* Tablet */
@media (min-width: 768px) {
.poster-header {
padding: var(--spacing-2xl) var(--spacing-xl);
}
.poster-content {
max-width: 720px;
margin: 0 auto;
padding: var(--spacing-xl);
}
.template-grid {
grid-template-columns: repeat(2, 1fr);
}
.size-chips {
grid-template-columns: repeat(6, 1fr);
}
}
/* Desktop */
@media (min-width: 1024px) {
.poster-content {
max-width: 960px;
}
}
</style>
</head>
<body>
<div class="poster-container">
<!-- Header -->
<header class="poster-header">
<button class="poster-header__back" onclick="history.back()">
</button>
<h1 class="poster-header__title">QR 포스터 생성</h1>
<p class="poster-header__subtitle">오프라인 홍보용 QR 포스터를 만들어 보세요</p>
</header>
<!-- Content -->
<main class="poster-content">
<!-- Size Selection -->
<div class="size-section">
<h2 class="size-section__label">
📐 포스터 크기 선택
</h2>
<div class="size-chips">
<div class="size-chip size-chip--selected" data-size="A4">
<div class="size-chip__name">A4</div>
<div class="size-chip__dimensions">210×297mm</div>
</div>
<div class="size-chip" data-size="A3">
<div class="size-chip__name">A3</div>
<div class="size-chip__dimensions">297×420mm</div>
</div>
<div class="size-chip" data-size="A2">
<div class="size-chip__name">A2</div>
<div class="size-chip__dimensions">420×594mm</div>
</div>
<div class="size-chip" data-size="B4">
<div class="size-chip__name">B4</div>
<div class="size-chip__dimensions">257×364mm</div>
</div>
<div class="size-chip" data-size="B3">
<div class="size-chip__name">B3</div>
<div class="size-chip__dimensions">364×515mm</div>
</div>
<div class="size-chip" data-size="custom">
<div class="size-chip__name">사용자정의</div>
<div class="size-chip__dimensions">직접 입력</div>
</div>
</div>
</div>
<!-- Template Grid -->
<div class="template-grid" id="templateGrid">
<!-- Template 1 - Classic -->
<div class="template-card template-card--selected" data-template="classic">
<div class="template-card__badge">🤖 AI 추천</div>
<input type="radio" name="template" value="classic" class="template-card__radio" checked>
<div class="template-card__preview">
<div class="template-preview__header">
<h3 class="template-preview__title">🎉 친구 초대 이벤트</h3>
<p class="template-preview__subtitle">함께하면 더 큰 혜택!</p>
</div>
<div class="template-preview__qr">📱</div>
<div class="template-preview__footer">
<p class="template-preview__cta">QR 코드를 스캔하고 참여하세요!</p>
</div>
</div>
<div class="template-card__info">
<h4 class="template-card__name">클래식 템플릿</h4>
<p class="template-card__description">
깔끔하고 전통적인 레이아웃. 모든 업종에 잘 어울리는 기본 템플릿입니다.
</p>
<div class="template-card__features">
<span class="feature-tag">심플</span>
<span class="feature-tag">범용</span>
<span class="feature-tag">가독성 높음</span>
</div>
</div>
</div>
<!-- Template 2 - Modern -->
<div class="template-card" data-template="modern">
<input type="radio" name="template" value="modern" class="template-card__radio">
<div class="template-card__preview" style="background: linear-gradient(135deg, #667EEA 0%, #764BA2 100%); color: white;">
<div class="template-preview__header">
<h3 class="template-preview__title" style="color: white;">🎉 친구 초대 이벤트</h3>
<p class="template-preview__subtitle" style="color: rgba(255,255,255,0.9);">함께하면 더 큰 혜택!</p>
</div>
<div class="template-preview__qr" style="background: white;">📱</div>
<div class="template-preview__footer" style="background: rgba(255,255,255,0.2);">
<p class="template-preview__cta" style="color: white;">QR 코드를 스캔하고 참여하세요!</p>
</div>
</div>
<div class="template-card__info">
<h4 class="template-card__name">모던 템플릿</h4>
<p class="template-card__description">
그라데이션 배경과 세련된 타이포그래피로 젊은 층에게 어필합니다.
</p>
<div class="template-card__features">
<span class="feature-tag">트렌디</span>
<span class="feature-tag">그라데이션</span>
<span class="feature-tag">젊은 층</span>
</div>
</div>
</div>
<!-- Template 3 - Minimal -->
<div class="template-card" data-template="minimal">
<input type="radio" name="template" value="minimal" class="template-card__radio">
<div class="template-card__preview" style="background: white; border: 1px solid #e5e7eb;">
<div class="template-preview__header">
<h3 class="template-preview__title" style="font-size: 18px;">친구 초대 이벤트</h3>
<p class="template-preview__subtitle">함께하면 더 큰 혜택</p>
</div>
<div class="template-preview__qr" style="border-color: #000;">📱</div>
<div class="template-preview__footer" style="background: white; border-top: 1px solid #e5e7eb;">
<p class="template-preview__cta" style="color: #000; font-size: 14px;">스캔하고 참여하세요</p>
</div>
</div>
<div class="template-card__info">
<h4 class="template-card__name">미니멀 템플릿</h4>
<p class="template-card__description">
여백을 활용한 미니멀한 디자인. 고급스러운 브랜드 이미지에 적합합니다.
</p>
<div class="template-card__features">
<span class="feature-tag">미니멀</span>
<span class="feature-tag">고급</span>
<span class="feature-tag">여백 활용</span>
</div>
</div>
</div>
<!-- Template 4 - Vibrant -->
<div class="template-card" data-template="vibrant">
<input type="radio" name="template" value="vibrant" class="template-card__radio">
<div class="template-card__preview" style="background: linear-gradient(135deg, #FA709A 0%, #FEE140 100%); color: white;">
<div class="template-preview__header">
<h3 class="template-preview__title" style="color: white; text-shadow: 0 2px 4px rgba(0,0,0,0.2);">🎉 친구 초대 이벤트</h3>
<p class="template-preview__subtitle" style="color: white;">함께하면 더 큰 혜택!</p>
</div>
<div class="template-preview__qr" style="background: white; border: 4px solid white;">📱</div>
<div class="template-preview__footer" style="background: rgba(255,255,255,0.3);">
<p class="template-preview__cta" style="color: white; font-weight: bold;">QR 코드를 스캔하고 참여하세요!</p>
</div>
</div>
<div class="template-card__info">
<h4 class="template-card__name">생동감 템플릿</h4>
<p class="template-card__description">
화사한 컬러와 역동적인 레이아웃으로 시선을 사로잡습니다.
</p>
<div class="template-card__features">
<span class="feature-tag">생동감</span>
<span class="feature-tag">눈에 띄는</span>
<span class="feature-tag">이벤트용</span>
</div>
</div>
</div>
</div>
<!-- Next Button -->
<button class="next-button" id="nextButton">
상세 설정하기
</button>
</main>
</div>
<script src="js/common.js"></script>
<script>
const { $, addClass, removeClass, storage } = window.CommonUtils;
// Elements
const sizeChips = document.querySelectorAll('.size-chip');
const templateCards = document.querySelectorAll('.template-card');
const templateRadios = document.querySelectorAll('input[name="template"]');
const nextButton = $('#nextButton');
// State
let selectedSize = 'A4';
let selectedTemplate = 'classic';
// Handle size selection
sizeChips.forEach(chip => {
chip.addEventListener('click', () => {
sizeChips.forEach(c => removeClass(c, 'size-chip--selected'));
addClass(chip, 'size-chip--selected');
selectedSize = chip.getAttribute('data-size');
});
});
// Handle template selection
templateCards.forEach(card => {
card.addEventListener('click', (e) => {
// Don't trigger if clicking radio directly
if (e.target.type === 'radio') return;
const template = card.getAttribute('data-template');
selectTemplate(template, card);
});
});
templateRadios.forEach(radio => {
radio.addEventListener('click', (e) => {
e.stopPropagation();
const template = radio.value;
const card = radio.closest('.template-card');
selectTemplate(template, card);
});
});
function selectTemplate(template, card) {
templateCards.forEach(c => removeClass(c, 'template-card--selected'));
addClass(card, 'template-card--selected');
const radio = card.querySelector('input[type="radio"]');
radio.checked = true;
selectedTemplate = template;
}
// Handle next button
nextButton.addEventListener('click', () => {
// Save selections
const eventContent = storage.get('eventContent') || {};
eventContent.qrPoster = {
size: selectedSize,
template: selectedTemplate,
selectedAt: new Date().toISOString()
};
storage.set('eventContent', eventContent);
// Show loading
addClass(nextButton, 'next-button--loading');
nextButton.disabled = true;
// Navigate to next screen
setTimeout(() => {
window.location.href = '13-QR포스터상세설정.html';
}, 800);
});
</script>
</body>
</html>

View File

@ -0,0 +1,628 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>QR 포스터 상세 설정 - KT 이벤트 마케팅</title>
<link rel="stylesheet" href="css/variables.css">
<link rel="stylesheet" href="css/common.css">
<style>
/* Layout */
.detail-container {
min-height: 100vh;
background: linear-gradient(135deg, #F8F9FF 0%, #FFFFFF 100%);
padding-bottom: var(--spacing-2xl);
}
/* Header */
.detail-header {
background: linear-gradient(135deg, var(--color-secondary) 0%, var(--color-secondary-dark) 100%);
color: var(--color-white);
padding: var(--spacing-xl) var(--spacing-m);
box-shadow: var(--shadow-lg);
}
.detail-header__back {
background: none;
border: none;
color: var(--color-white);
font-size: var(--font-size-xl);
padding: 0;
margin-bottom: var(--spacing-m);
cursor: pointer;
}
.detail-header__title {
font-size: var(--font-size-2xl);
font-weight: var(--font-weight-bold);
margin-bottom: var(--spacing-xs);
}
.detail-header__subtitle {
font-size: var(--font-size-m);
opacity: 0.9;
}
/* Content */
.detail-content {
padding: var(--spacing-l) var(--spacing-m);
display: grid;
gap: var(--spacing-l);
}
/* Preview Section */
.preview-section {
background: var(--color-white);
border-radius: var(--radius-l);
padding: var(--spacing-l);
box-shadow: var(--shadow-md);
position: sticky;
top: var(--spacing-m);
}
.preview-section__label {
font-size: var(--font-size-m);
font-weight: var(--font-weight-semibold);
color: var(--color-text-primary);
margin-bottom: var(--spacing-m);
}
.poster-preview {
aspect-ratio: 3 / 4;
background: linear-gradient(135deg, #F8F9FF 0%, #FFFFFF 100%);
border-radius: var(--radius-l);
padding: var(--spacing-xl);
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
position: relative;
box-shadow: var(--shadow-lg);
}
.poster-preview__header {
text-align: center;
width: 100%;
}
.poster-preview__title {
font-size: var(--font-size-2xl);
font-weight: var(--font-weight-bold);
color: var(--color-text-primary);
margin-bottom: var(--spacing-s);
line-height: 1.3;
}
.poster-preview__subtitle {
font-size: var(--font-size-m);
color: var(--color-text-secondary);
}
.poster-preview__qr {
width: 160px;
height: 160px;
background: var(--color-white);
border: 2px solid var(--color-border-light);
border-radius: var(--radius-m);
display: flex;
align-items: center;
justify-content: center;
font-size: 64px;
box-shadow: var(--shadow-md);
}
.poster-preview__footer {
text-align: center;
width: 100%;
padding: var(--spacing-m);
background: rgba(255, 255, 255, 0.9);
border-radius: var(--radius-m);
}
.poster-preview__cta {
font-size: var(--font-size-l);
font-weight: var(--font-weight-semibold);
color: var(--color-primary);
margin-bottom: var(--spacing-xs);
}
.poster-preview__url {
font-size: var(--font-size-s);
color: var(--color-text-secondary);
}
/* Form Sections */
.form-section {
background: var(--color-white);
border-radius: var(--radius-l);
padding: var(--spacing-m);
box-shadow: var(--shadow-sm);
}
.form-section__title {
font-size: var(--font-size-m);
font-weight: var(--font-weight-semibold);
color: var(--color-text-primary);
margin-bottom: var(--spacing-m);
display: flex;
align-items: center;
gap: var(--spacing-xs);
}
.form-group {
margin-bottom: var(--spacing-m);
}
.form-group:last-child {
margin-bottom: 0;
}
.form-group__label {
display: block;
font-size: var(--font-size-s);
font-weight: var(--font-weight-semibold);
color: var(--color-text-primary);
margin-bottom: var(--spacing-xs);
}
.form-group__input,
.form-group__textarea {
width: 100%;
padding: var(--spacing-m);
border: 1px solid var(--color-border-light);
border-radius: var(--radius-m);
font-size: var(--font-size-m);
font-family: var(--font-family);
transition: all var(--transition-normal);
}
.form-group__textarea {
resize: vertical;
min-height: 80px;
}
.form-group__input:focus,
.form-group__textarea:focus {
outline: none;
border-color: var(--color-secondary);
box-shadow: 0 0 0 3px rgba(0, 102, 255, 0.1);
}
.form-group__hint {
font-size: var(--font-size-xs);
color: var(--color-text-tertiary);
margin-top: var(--spacing-xs);
}
/* Color Picker */
.color-picker {
display: grid;
grid-template-columns: repeat(6, 1fr);
gap: var(--spacing-s);
}
.color-option {
width: 100%;
aspect-ratio: 1;
border: 2px solid var(--color-border-light);
border-radius: var(--radius-m);
cursor: pointer;
transition: all var(--transition-normal);
position: relative;
}
.color-option:hover {
transform: scale(1.1);
box-shadow: var(--shadow-md);
}
.color-option--selected {
border-color: var(--color-secondary);
box-shadow: var(--shadow-lg);
}
.color-option--selected::after {
content: '✓';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: var(--color-white);
font-weight: var(--font-weight-bold);
font-size: var(--font-size-l);
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
}
/* Action Buttons */
.action-buttons {
display: grid;
grid-template-columns: 1fr 2fr;
gap: var(--spacing-m);
}
.action-button {
padding: var(--spacing-m);
border: none;
border-radius: var(--radius-m);
font-size: var(--font-size-m);
font-weight: var(--font-weight-semibold);
cursor: pointer;
transition: all var(--transition-normal);
}
.action-button--preview {
background: var(--color-white);
border: 2px solid var(--color-border-light);
color: var(--color-text-primary);
}
.action-button--preview:hover {
border-color: var(--color-secondary);
color: var(--color-secondary);
}
.action-button--generate {
background: linear-gradient(135deg, var(--color-secondary) 0%, var(--color-secondary-dark) 100%);
color: var(--color-white);
box-shadow: var(--shadow-md);
}
.action-button--generate:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-lg);
}
.action-button--loading {
position: relative;
color: transparent;
}
.action-button--loading:after {
content: '';
position: absolute;
width: 20px;
height: 20px;
top: 50%;
left: 50%;
margin-left: -10px;
margin-top: -10px;
border: 3px solid rgba(255, 255, 255, 0.3);
border-radius: 50%;
border-top-color: var(--color-white);
animation: spinner 0.6s linear infinite;
}
@keyframes spinner {
to { transform: rotate(360deg); }
}
/* Tablet */
@media (min-width: 768px) {
.detail-header {
padding: var(--spacing-2xl) var(--spacing-xl);
}
.detail-content {
max-width: 1200px;
margin: 0 auto;
padding: var(--spacing-xl);
grid-template-columns: 1fr 1fr;
}
.preview-section {
order: 2;
}
.form-sections {
order: 1;
}
}
/* Desktop */
@media (min-width: 1024px) {
.detail-content {
grid-template-columns: 2fr 1fr;
}
}
</style>
</head>
<body>
<div class="detail-container">
<!-- Header -->
<header class="detail-header">
<button class="detail-header__back" onclick="history.back()">
</button>
<h1 class="detail-header__title">QR 포스터 상세 설정</h1>
<p class="detail-header__subtitle">포스터 내용을 상세하게 편집하세요</p>
</header>
<!-- Content -->
<main class="detail-content">
<!-- Form Sections -->
<div class="form-sections">
<!-- Text Content -->
<div class="form-section">
<h2 class="form-section__title">
✍️ 텍스트 내용
</h2>
<div class="form-group">
<label class="form-group__label">제목</label>
<input
type="text"
class="form-group__input"
id="titleInput"
value="🎉 친구 초대 이벤트"
maxlength="50"
>
<p class="form-group__hint">포스터 상단에 표시될 제목입니다</p>
</div>
<div class="form-group">
<label class="form-group__label">부제목</label>
<input
type="text"
class="form-group__input"
id="subtitleInput"
value="함께하면 더 큰 혜택!"
maxlength="100"
>
</div>
<div class="form-group">
<label class="form-group__label">행동 유도 문구</label>
<textarea
class="form-group__textarea"
id="ctaInput"
maxlength="200"
>QR 코드를 스캔하고 지금 바로 참여하세요!</textarea>
</div>
<div class="form-group">
<label class="form-group__label">이벤트 URL (선택)</label>
<input
type="url"
class="form-group__input"
id="urlInput"
placeholder="https://event.mystore.com"
>
<p class="form-group__hint">QR 코드로 연결될 URL을 입력하세요</p>
</div>
</div>
<!-- Design Settings -->
<div class="form-section">
<h2 class="form-section__title">
🎨 디자인 설정
</h2>
<div class="form-group">
<label class="form-group__label">배경 색상</label>
<div class="color-picker">
<div class="color-option color-option--selected" data-color="#F8F9FF" style="background: #F8F9FF;"></div>
<div class="color-option" data-color="#FFFFFF" style="background: #FFFFFF;"></div>
<div class="color-option" data-color="#667EEA" style="background: linear-gradient(135deg, #667EEA 0%, #764BA2 100%);"></div>
<div class="color-option" data-color="#FA709A" style="background: linear-gradient(135deg, #FA709A 0%, #FEE140 100%);"></div>
<div class="color-option" data-color="#FF6B6B" style="background: #FF6B6B;"></div>
<div class="color-option" data-color="#4ECDC4" style="background: #4ECDC4;"></div>
</div>
</div>
<div class="form-group">
<label class="form-group__label">텍스트 색상</label>
<div class="color-picker">
<div class="color-option color-option--selected" data-color-text="#1A202C" style="background: #1A202C;"></div>
<div class="color-option" data-color-text="#FFFFFF" style="background: #FFFFFF;"></div>
<div class="color-option" data-color-text="#2D3748" style="background: #2D3748;"></div>
<div class="color-option" data-color-text="#4A5568" style="background: #4A5568;"></div>
<div class="color-option" data-color-text="#E31E24" style="background: #E31E24;"></div>
<div class="color-option" data-color-text="#0066FF" style="background: #0066FF;"></div>
</div>
</div>
<div class="form-group">
<label class="form-group__label">QR 코드 크기</label>
<input
type="range"
class="form-group__input"
id="qrSizeInput"
min="120"
max="240"
value="160"
step="20"
>
<p class="form-group__hint" id="qrSizeValue">160px</p>
</div>
</div>
<!-- Additional Info -->
<div class="form-section">
<h2 class="form-section__title">
추가 정보
</h2>
<div class="form-group">
<label class="form-group__label">이벤트 기간</label>
<input
type="text"
class="form-group__input"
id="periodInput"
placeholder="예: 2025.01.01 ~ 2025.01.31"
>
</div>
<div class="form-group">
<label class="form-group__label">매장 정보</label>
<input
type="text"
class="form-group__input"
id="storeInfoInput"
placeholder="예: 강남점 | 02-1234-5678"
>
</div>
<div class="form-group">
<label class="form-group__label">주의사항 (선택)</label>
<textarea
class="form-group__textarea"
id="noticeInput"
placeholder="예: 1인 1회 참여 가능"
maxlength="200"
></textarea>
</div>
</div>
<!-- Action Buttons -->
<div class="action-buttons">
<button class="action-button action-button--preview" id="previewButton">
👁️ 미리보기
</button>
<button class="action-button action-button--generate" id="generateButton">
✅ 포스터 생성하기
</button>
</div>
</div>
<!-- Preview Section -->
<div class="preview-section">
<h3 class="preview-section__label">📱 실시간 미리보기</h3>
<div class="poster-preview" id="posterPreview">
<div class="poster-preview__header">
<h1 class="poster-preview__title" id="previewTitle">🎉 친구 초대 이벤트</h1>
<p class="poster-preview__subtitle" id="previewSubtitle">함께하면 더 큰 혜택!</p>
</div>
<div class="poster-preview__qr" id="previewQR">📱</div>
<div class="poster-preview__footer">
<p class="poster-preview__cta" id="previewCTA">QR 코드를 스캔하고 지금 바로 참여하세요!</p>
<p class="poster-preview__url" id="previewURL"></p>
</div>
</div>
</div>
</main>
</div>
<script src="js/common.js"></script>
<script>
const { $, addClass, removeClass, storage } = window.CommonUtils;
// Elements
const titleInput = $('#titleInput');
const subtitleInput = $('#subtitleInput');
const ctaInput = $('#ctaInput');
const urlInput = $('#urlInput');
const qrSizeInput = $('#qrSizeInput');
const qrSizeValue = $('#qrSizeValue');
const periodInput = $('#periodInput');
const storeInfoInput = $('#storeInfoInput');
const noticeInput = $('#noticeInput');
const colorOptions = document.querySelectorAll('[data-color]');
const colorTextOptions = document.querySelectorAll('[data-color-text]');
const previewButton = $('#previewButton');
const generateButton = $('#generateButton');
// Preview elements
const posterPreview = $('#posterPreview');
const previewTitle = $('#previewTitle');
const previewSubtitle = $('#previewSubtitle');
const previewCTA = $('#previewCTA');
const previewURL = $('#previewURL');
const previewQR = $('#previewQR');
// State
let selectedBgColor = '#F8F9FF';
let selectedTextColor = '#1A202C';
let qrSize = 160;
// Update preview in real-time
function updatePreview() {
previewTitle.textContent = titleInput.value || '제목을 입력하세요';
previewSubtitle.textContent = subtitleInput.value || '부제목을 입력하세요';
previewCTA.textContent = ctaInput.value || 'CTA를 입력하세요';
previewURL.textContent = urlInput.value || '';
// Update QR size
previewQR.style.width = qrSize + 'px';
previewQR.style.height = qrSize + 'px';
// Update colors
posterPreview.style.background = selectedBgColor;
previewTitle.style.color = selectedTextColor;
previewSubtitle.style.color = selectedTextColor;
previewCTA.style.color = selectedTextColor;
}
// Input listeners
titleInput.addEventListener('input', updatePreview);
subtitleInput.addEventListener('input', updatePreview);
ctaInput.addEventListener('input', updatePreview);
urlInput.addEventListener('input', updatePreview);
// QR size slider
qrSizeInput.addEventListener('input', (e) => {
qrSize = parseInt(e.target.value);
qrSizeValue.textContent = qrSize + 'px';
updatePreview();
});
// Color selection
colorOptions.forEach(option => {
option.addEventListener('click', () => {
colorOptions.forEach(o => removeClass(o, 'color-option--selected'));
addClass(option, 'color-option--selected');
selectedBgColor = option.getAttribute('data-color');
updatePreview();
});
});
colorTextOptions.forEach(option => {
option.addEventListener('click', () => {
colorTextOptions.forEach(o => removeClass(o, 'color-option--selected'));
addClass(option, 'color-option--selected');
selectedTextColor = option.getAttribute('data-color-text');
updatePreview();
});
});
// Preview button
previewButton.addEventListener('click', () => {
alert('전체 화면 미리보기 기능은 다음 버전에서 제공됩니다.');
});
// Generate button
generateButton.addEventListener('click', () => {
// Collect all settings
const posterSettings = {
title: titleInput.value,
subtitle: subtitleInput.value,
cta: ctaInput.value,
url: urlInput.value,
bgColor: selectedBgColor,
textColor: selectedTextColor,
qrSize: qrSize,
period: periodInput.value,
storeInfo: storeInfoInput.value,
notice: noticeInput.value
};
// Save to storage
const eventContent = storage.get('eventContent') || {};
eventContent.qrPoster = {
...eventContent.qrPoster,
settings: posterSettings,
generatedAt: new Date().toISOString()
};
storage.set('eventContent', eventContent);
// Show loading
addClass(generateButton, 'action-button--loading');
generateButton.disabled = true;
// Navigate to next screen
setTimeout(() => {
window.location.href = '14-콘텐츠최종승인.html';
}, 1000);
});
// Load saved data
window.addEventListener('load', () => {
const storeData = storage.get('storeData');
if (storeData && storeData.storeName) {
storeInfoInput.value = storeData.storeName;
}
updatePreview();
});
</script>
</body>
</html>

View File

@ -1,325 +1,699 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>콘텐츠 편집 - KT AI 이벤트</title>
<link rel="stylesheet" href="css/common.css">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Pretendard:wght@400;500;600;700&display=swap" rel="stylesheet">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>콘텐츠 편집 - KT 이벤트 마케팅</title>
<style>
/* CSS Variables */
:root {
/* Colors */
--color-primary: #E31E24;
--color-primary-dark: #B71C1C;
--color-primary-light: #FFEBEE;
--color-secondary: #0066FF;
--color-secondary-dark: #0052CC;
--color-secondary-light: #E3F2FD;
--color-success: #4CAF50;
--color-success-light: #E8F5E9;
--color-warning: #FF9800;
--color-warning-light: #FFF3E0;
--color-error: #F44336;
--color-error-light: #FFEBEE;
--color-info: #2196F3;
--color-info-light: #E3F2FD;
/* Grayscale */
--color-white: #FFFFFF;
--color-gray-50: #FAFAFA;
--color-gray-100: #F5F5F5;
--color-gray-200: #EEEEEE;
--color-gray-300: #E0E0E0;
--color-gray-400: #BDBDBD;
--color-gray-500: #9E9E9E;
--color-gray-600: #757575;
--color-gray-700: #616161;
--color-gray-800: #424242;
--color-gray-900: #212121;
--color-black: #000000;
/* Text Colors */
--color-text-primary: #212121;
--color-text-secondary: #757575;
--color-text-tertiary: #9E9E9E;
--color-text-disabled: #BDBDBD;
--color-text-inverse: #FFFFFF;
/* Border Colors */
--color-border-light: #E0E0E0;
--color-border-medium: #BDBDBD;
--color-border-dark: #757575;
/* Background Colors */
--color-bg-primary: #FFFFFF;
--color-bg-secondary: #F5F5F5;
--color-bg-tertiary: #FAFAFA;
/* Spacing (4px base unit) */
--spacing-xs: 4px;
--spacing-s: 8px;
--spacing-m: 16px;
--spacing-l: 24px;
--spacing-xl: 32px;
--spacing-2xl: 48px;
--spacing-3xl: 64px;
/* Typography */
--font-family-primary: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
--font-size-xs: 12px;
--font-size-s: 14px;
--font-size-m: 16px;
--font-size-l: 18px;
--font-size-xl: 20px;
--font-size-2xl: 24px;
--font-size-3xl: 32px;
--font-size-4xl: 40px;
--font-weight-regular: 400;
--font-weight-medium: 500;
--font-weight-semibold: 600;
--font-weight-bold: 700;
--line-height-tight: 1.2;
--line-height-normal: 1.5;
--line-height-relaxed: 1.75;
/* Border Radius */
--radius-s: 4px;
--radius-m: 8px;
--radius-l: 12px;
--radius-xl: 16px;
--radius-2xl: 24px;
--radius-full: 9999px;
/* Shadows */
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
--shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
/* Transitions */
--transition-fast: 150ms ease-in-out;
--transition-normal: 250ms ease-in-out;
--transition-slow: 350ms ease-in-out;
/* Z-index */
--z-dropdown: 1000;
--z-sticky: 1020;
--z-fixed: 1030;
--z-modal-backdrop: 1040;
--z-modal: 1050;
--z-popover: 1060;
--z-tooltip: 1070;
}
/* Reset & Base Styles */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: var(--font-family-primary);
font-size: var(--font-size-m);
line-height: var(--line-height-normal);
color: var(--color-text-primary);
background: var(--color-bg-secondary);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
button {
font-family: inherit;
cursor: pointer;
}
textarea {
font-family: inherit;
}
/* Layout */
.edit-container {
min-height: 100vh;
background: linear-gradient(135deg, #F8F9FF 0%, #FFFFFF 100%);
padding-bottom: var(--spacing-2xl);
}
/* Header */
.edit-header {
background: linear-gradient(135deg, var(--color-secondary) 0%, var(--color-secondary-dark) 100%);
color: var(--color-white);
padding: var(--spacing-xl) var(--spacing-m);
box-shadow: var(--shadow-lg);
}
.edit-header__back {
background: none;
border: none;
color: var(--color-white);
font-size: var(--font-size-xl);
padding: 0;
margin-bottom: var(--spacing-m);
cursor: pointer;
}
.edit-header__title {
font-size: var(--font-size-2xl);
font-weight: var(--font-weight-bold);
margin-bottom: var(--spacing-xs);
}
.edit-header__subtitle {
font-size: var(--font-size-m);
opacity: 0.9;
}
/* Content */
.edit-content {
padding: var(--spacing-l) var(--spacing-m);
}
/* Info Card */
.info-card {
background: linear-gradient(135deg, #FFF9E6 0%, #FFFFFF 100%);
border-left: 4px solid #FFB800;
padding: var(--spacing-m);
border-radius: var(--radius-m);
margin-bottom: var(--spacing-l);
}
.info-card__title {
font-size: var(--font-size-m);
font-weight: var(--font-weight-semibold);
color: var(--color-text-primary);
margin-bottom: var(--spacing-s);
display: flex;
align-items: center;
gap: var(--spacing-s);
}
.info-card__text {
font-size: var(--font-size-s);
color: var(--color-text-secondary);
line-height: 1.6;
}
/* Edit Section */
.edit-section {
background: var(--color-white);
border-radius: var(--radius-l);
padding: var(--spacing-l);
margin-bottom: var(--spacing-m);
box-shadow: var(--shadow-sm);
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--spacing-m);
padding-bottom: var(--spacing-m);
border-bottom: 2px solid var(--color-border-light);
}
.section-title {
font-size: var(--font-size-l);
font-weight: var(--font-weight-semibold);
color: var(--color-text-primary);
display: flex;
align-items: center;
gap: var(--spacing-s);
}
.section-icon {
font-size: var(--font-size-xl);
}
.regenerate-button {
padding: var(--spacing-s) var(--spacing-m);
background: var(--color-white);
border: 2px solid var(--color-secondary);
border-radius: var(--radius-m);
color: var(--color-secondary);
font-size: var(--font-size-s);
font-weight: var(--font-weight-semibold);
cursor: pointer;
transition: all var(--transition-normal);
}
.regenerate-button:hover {
background: var(--color-secondary-light);
}
.edit-label {
font-size: var(--font-size-m);
font-weight: var(--font-weight-medium);
color: var(--color-text-primary);
margin-bottom: var(--spacing-s);
display: block;
}
.edit-textarea {
width: 100%;
min-height: 120px;
padding: var(--spacing-m);
border: 2px solid var(--color-border-light);
border-radius: var(--radius-m);
font-size: var(--font-size-m);
line-height: var(--line-height-normal);
resize: vertical;
transition: all var(--transition-normal);
}
.edit-textarea:focus {
outline: none;
border-color: var(--color-secondary);
}
.char-count {
text-align: right;
font-size: var(--font-size-xs);
color: var(--color-text-secondary);
margin-top: var(--spacing-s);
}
.image-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: var(--spacing-m);
margin-bottom: var(--spacing-m);
}
.image-item {
position: relative;
aspect-ratio: 1;
background: var(--color-gray-100);
border-radius: var(--radius-m);
display: flex;
align-items: center;
justify-content: center;
font-size: 48px;
border: 2px solid var(--color-border-light);
}
.poster-preview {
background: var(--color-gray-100);
border-radius: var(--radius-m);
padding: var(--spacing-l);
text-align: center;
margin-bottom: var(--spacing-m);
}
.poster-preview__icon {
font-size: 64px;
margin-bottom: var(--spacing-m);
}
.poster-preview__text {
font-size: var(--font-size-s);
color: var(--color-text-secondary);
}
/* Action Buttons */
.action-buttons {
display: flex;
flex-direction: column;
gap: var(--spacing-m);
}
.save-button {
width: 100%;
padding: var(--spacing-m);
background: var(--color-white);
border: 2px solid var(--color-border-light);
border-radius: var(--radius-m);
font-size: var(--font-size-m);
font-weight: var(--font-weight-semibold);
color: var(--color-text-primary);
cursor: pointer;
transition: all var(--transition-normal);
}
.save-button:hover {
border-color: var(--color-success);
color: var(--color-success);
}
.next-button {
width: 100%;
padding: var(--spacing-m);
background: linear-gradient(135deg, var(--color-secondary) 0%, var(--color-secondary-dark) 100%);
color: var(--color-white);
border: none;
border-radius: var(--radius-m);
font-size: var(--font-size-m);
font-weight: var(--font-weight-semibold);
cursor: pointer;
transition: all var(--transition-normal);
box-shadow: var(--shadow-md);
}
.next-button:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: var(--shadow-lg);
}
.next-button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.next-button--loading {
position: relative;
color: transparent;
}
.next-button--loading:after {
content: '';
position: absolute;
width: 20px;
height: 20px;
top: 50%;
left: 50%;
margin-left: -10px;
margin-top: -10px;
border: 3px solid rgba(255, 255, 255, 0.3);
border-radius: 50%;
border-top-color: var(--color-white);
animation: spinner 0.6s linear infinite;
}
@keyframes spinner {
to { transform: rotate(360deg); }
}
/* Tablet */
@media (min-width: 768px) {
.edit-header {
padding: var(--spacing-2xl) var(--spacing-xl);
}
.edit-content {
max-width: 720px;
margin: 0 auto;
padding: var(--spacing-xl);
}
.image-grid {
grid-template-columns: repeat(4, 1fr);
}
.action-buttons {
flex-direction: row;
}
.save-button {
flex: 1;
}
.next-button {
flex: 2;
}
}
/* Desktop */
@media (min-width: 1024px) {
.edit-content {
max-width: 960px;
}
}
</style>
</head>
<body>
<a href="#main-content" class="skip-link">본문으로 건너뛰기</a>
<header class="header" role="banner">
<div class="header-content">
<button type="button" class="btn-icon" onclick="window.history.back()" aria-label="이전 페이지로 돌아가기">
<span class="material-icons">arrow_back</span>
</button>
<h1 class="header-title">콘텐츠 편집</h1>
<div style="width: 40px;"></div>
</div>
<div class="edit-container">
<!-- Header -->
<header class="edit-header">
<button class="edit-header__back" onclick="history.back()">
</button>
<h1 class="edit-header__title">콘텐츠 편집</h1>
<p class="edit-header__subtitle">생성된 콘텐츠를 검토하고 수정하세요</p>
</header>
<main id="main-content" class="container" role="main" style="padding-bottom: 100px;">
<section class="section" aria-labelledby="select-title">
<h2 id="select-title" class="h3">편집할 콘텐츠 선택</h2>
<!-- Content -->
<main class="edit-content">
<!-- Info Card -->
<div class="info-card">
<h2 class="info-card__title">
✏️ 편집 가이드
</h2>
<p class="info-card__text">
AI가 생성한 콘텐츠를 자유롭게 수정할 수 있습니다.
각 섹션의 "재생성" 버튼을 클릭하면 새로운 콘텐츠를 생성할 수 있습니다.
</p>
</div>
<div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--spacing-m);">
<button type="button" class="content-card" onclick="editContent('image')" data-type="image">
<div class="content-card-icon" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);">
<span class="material-icons" style="color: white; font-size: 32px;">image</span>
</div>
<div class="body-s" style="font-weight: 600; text-align: center;">이미지</div>
</button>
<!-- Copy Section -->
<div class="edit-section">
<div class="section-header">
<h2 class="section-title">
<span class="section-icon">📝</span>
홍보 문구
</h2>
<button class="regenerate-button" id="regenerateCopy">
🔄 재생성
</button>
</div>
<label class="edit-label" for="copyText">메인 홍보 문구</label>
<textarea
id="copyText"
class="edit-textarea"
placeholder="홍보 문구를 입력하세요..."
>🎉 신규 고객 환영 이벤트 🎉
<button type="button" class="content-card" onclick="editContent('video')" data-type="video">
<div class="content-card-icon" style="background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);">
<span class="material-icons" style="color: white; font-size: 32px;">videocam</span>
</div>
<div class="body-s" style="font-weight: 600; text-align: center;">영상</div>
</button>
지금 가입하시면 특별한 혜택이 기다립니다!
✨ 첫 구매 20% 할인
✨ 무료 배송 쿠폰 증정
✨ 추가 적립금 5,000원
<button type="button" class="content-card" onclick="editContent('sns')" data-type="sns">
<div class="content-card-icon" style="background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);">
<span class="material-icons" style="color: white; font-size: 32px;">share</span>
</div>
<div class="body-s" style="font-weight: 600; text-align: center;">SNS</div>
</button>
</div>
</section>
📅 기간: 2024년 1월 15일 ~ 1월 31일
🎁 선착순 100명 한정 특별 선물 증정</textarea>
<div class="char-count">
<span id="copyCharCount">0</span> / 500자
</div>
</div>
<section class="section" id="editorSection" style="display: none;" aria-labelledby="editor-title">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: var(--spacing-m);">
<h2 id="editor-title" class="h3">편집 중</h2>
<button type="button" class="btn-text" onclick="closeEditor()">
<span class="material-icons">close</span>
</button>
</div>
<!-- Images Section -->
<div class="edit-section">
<div class="section-header">
<h2 class="section-title">
<span class="section-icon">🖼️</span>
이미지
</h2>
<button class="regenerate-button" id="regenerateImages">
🔄 재생성
</button>
</div>
<div class="image-grid" id="imageGrid">
<div class="image-item">🎨</div>
<div class="image-item">🎁</div>
<div class="image-item"></div>
<div class="image-item">🎉</div>
</div>
<p class="info-card__text" style="margin-top: var(--spacing-s);">
각 이미지를 클릭하면 새로운 이미지로 교체할 수 있습니다.
</p>
</div>
<div class="card">
<div class="card-body">
<div id="previewArea" style="aspect-ratio: 1; background: var(--color-gray-100); border-radius: var(--radius-md); margin-bottom: var(--spacing-m); display: flex; align-items: center; justify-content: center; position: relative; overflow: hidden;">
<div id="previewContent" style="padding: var(--spacing-l); text-align: center; width: 100%;">
<div class="h4" id="previewText">🎉 연말 대박 이벤트!</div>
</div>
</div>
<!-- QR Poster Section -->
<div class="edit-section">
<div class="section-header">
<h2 class="section-title">
<span class="section-icon">📱</span>
QR 포스터
</h2>
<button class="regenerate-button" id="regeneratePoster">
🔄 재생성
</button>
</div>
<div class="poster-preview">
<div class="poster-preview__icon">📋</div>
<div class="poster-preview__text">QR 포스터 미리보기</div>
</div>
<label class="edit-label" for="posterTitle">포스터 제목</label>
<textarea
id="posterTitle"
class="edit-textarea"
placeholder="포스터 제목을 입력하세요..."
style="min-height: 80px;"
>신규 고객 환영 이벤트</textarea>
<label class="edit-label" for="posterSubtitle" style="margin-top: var(--spacing-m);">포스터 부제목</label>
<textarea
id="posterSubtitle"
class="edit-textarea"
placeholder="포스터 부제목을 입력하세요..."
style="min-height: 80px;"
>QR 코드를 스캔하고 특별 혜택을 받으세요</textarea>
</div>
<div style="display: flex; gap: var(--spacing-xs); margin-bottom: var(--spacing-m);">
<button type="button" class="btn btn-outline btn-sm" onclick="editText()" style="flex: 1;">
<span class="material-icons" style="font-size: 18px;">text_fields</span>
<span>텍스트</span>
</button>
<button type="button" class="btn btn-outline btn-sm" onclick="editColor()" style="flex: 1;">
<span class="material-icons" style="font-size: 18px;">palette</span>
<span>색상</span>
</button>
<button type="button" class="btn btn-outline btn-sm" onclick="editSize()" style="flex: 1;">
<span class="material-icons" style="font-size: 18px;">photo_size_select_small</span>
<span>크기</span>
</button>
</div>
<div>
<label class="form-label">편집 이력</label>
<div style="padding: var(--spacing-m); background: var(--color-gray-50); border-radius: var(--radius-md);">
<div style="display: flex; flex-direction: column; gap: var(--spacing-xs);">
<div style="display: flex; justify-content: space-between; align-items: center;">
<span class="body-s">• 원본</span>
<button type="button" class="btn-text btn-sm" onclick="revertToOriginal()">되돌리기</button>
</div>
<div style="display: flex; justify-content: space-between; align-items: center;">
<span class="body-s" style="font-weight: 600;">• 버전 1 (현재)</span>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<section class="section" id="skipSection">
<button type="button" class="btn btn-outline btn-lg" onclick="skipEdit()" style="width: 100%;">
편집 건너뛰기
</button>
</section>
<!-- Action Buttons -->
<div class="action-buttons">
<button class="save-button" id="saveButton">
💾 임시 저장
</button>
<button class="next-button" id="nextButton">
편집 완료하고 승인 요청
</button>
</div>
</main>
</div>
<div class="fixed-bottom-button" id="saveButton" style="display: none;">
<button type="button" class="btn btn-outline btn-lg" onclick="cancelEdit()" style="flex: 1;">
취소
</button>
<button type="button" class="btn btn-primary btn-lg" onclick="saveEdit()" style="flex: 2;">
저장하기
</button>
</div>
<script>
// Utility functions
const $ = (selector) => document.querySelector(selector);
const $$ = (selector) => document.querySelectorAll(selector);
<div id="textEditModal" class="modal" role="dialog" aria-modal="true" style="display: none;">
<div class="modal-content">
<div class="modal-header">
<h3 class="h4">텍스트 편집</h3>
<button type="button" class="btn-icon" onclick="closeTextModal()">
<span class="material-icons">close</span>
</button>
</div>
<div class="modal-body">
<label class="form-label" for="editTextInput">텍스트 내용</label>
<textarea id="editTextInput" class="form-input" rows="4" placeholder="텍스트를 입력하세요" style="resize: vertical;"></textarea>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline" onclick="closeTextModal()">취소</button>
<button type="button" class="btn btn-primary" onclick="applyText()">적용</button>
</div>
</div>
</div>
const addClass = (el, className) => el?.classList.add(className);
const removeClass = (el, className) => el?.classList.remove(className);
<div id="colorEditModal" class="modal" role="dialog" aria-modal="true" style="display: none;">
<div class="modal-content">
<div class="modal-header">
<h3 class="h4">색상 선택</h3>
<button type="button" class="btn-icon" onclick="closeColorModal()">
<span class="material-icons">close</span>
</button>
</div>
<div class="modal-body">
<label class="form-label">배경 색상</label>
<div style="display: grid; grid-template-columns: repeat(5, 1fr); gap: var(--spacing-s);">
<button type="button" class="color-option" data-color="#667eea" onclick="selectColor(this)" style="background: #667eea;"></button>
<button type="button" class="color-option" data-color="#f093fb" onclick="selectColor(this)" style="background: #f093fb;"></button>
<button type="button" class="color-option" data-color="#4facfe" onclick="selectColor(this)" style="background: #4facfe;"></button>
<button type="button" class="color-option" data-color="#43e97b" onclick="selectColor(this)" style="background: #43e97b;"></button>
<button type="button" class="color-option" data-color="#fa709a" onclick="selectColor(this)" style="background: #fa709a;"></button>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline" onclick="closeColorModal()">취소</button>
<button type="button" class="btn btn-primary" onclick="applyColor()">적용</button>
</div>
</div>
</div>
<script src="js/common.js"></script>
<script>
(function() {
'use strict';
let currentContent = null;
let selectedColor = '#667eea';
const eventData = AppState.currentEvent || {};
window.editContent = function(type) {
currentContent = type;
document.getElementById('editorSection').style.display = 'block';
document.getElementById('skipSection').style.display = 'none';
document.getElementById('saveButton').style.display = 'flex';
const typeNames = {
image: '이미지',
video: '영상',
sns: 'SNS'
};
document.getElementById('editor-title').textContent = typeNames[type] + ' 편집';
// Load content preview
if (type === 'video' && eventData.video) {
document.getElementById('previewText').textContent = eventData.video.overlayText || '🎉 연말 대박 이벤트!';
} else if (eventData.promotionMessage) {
document.getElementById('previewText').textContent = eventData.promotionMessage.text || '🎉 연말 대박 이벤트!';
}
};
window.closeEditor = function() {
document.getElementById('editorSection').style.display = 'none';
document.getElementById('skipSection').style.display = 'block';
document.getElementById('saveButton').style.display = 'none';
currentContent = null;
};
window.editText = function() {
const modal = document.getElementById('textEditModal');
const input = document.getElementById('editTextInput');
input.value = document.getElementById('previewText').textContent;
Modal.open(modal);
};
window.closeTextModal = function() {
Modal.close(document.getElementById('textEditModal'));
};
window.applyText = function() {
const text = document.getElementById('editTextInput').value.trim();
if (text) {
document.getElementById('previewText').textContent = text;
Toast.success('텍스트가 변경되었습니다.');
}
closeTextModal();
};
window.editColor = function() {
Modal.open(document.getElementById('colorEditModal'));
};
window.closeColorModal = function() {
Modal.close(document.getElementById('colorEditModal'));
};
window.selectColor = function(btn) {
selectedColor = btn.dataset.color;
document.querySelectorAll('.color-option').forEach(el => {
el.style.border = '2px solid transparent';
});
btn.style.border = '2px solid var(--color-gray-900)';
};
window.applyColor = function() {
document.getElementById('previewArea').style.background = 'linear-gradient(135deg, ' + selectedColor + ' 0%, ' + adjustColor(selectedColor) + ' 100%)';
Toast.success('색상이 변경되었습니다.');
closeColorModal();
};
function adjustColor(hex) {
const r = parseInt(hex.slice(1,3), 16);
const g = parseInt(hex.slice(3,5), 16);
const b = parseInt(hex.slice(5,7), 16);
return '#' + ((r-40).toString(16).padStart(2,'0')) + ((g-40).toString(16).padStart(2,'0')) + ((b-40).toString(16).padStart(2,'0'));
}
window.editSize = function() {
const current = document.getElementById('previewText');
const currentSize = parseInt(window.getComputedStyle(current).fontSize);
const newSize = currentSize === 24 ? 32 : 24;
current.style.fontSize = newSize + 'px';
Toast.success('크기가 변경되었습니다.');
};
window.revertToOriginal = function() {
if (confirm('원본으로 되돌리시겠습니까?')) {
document.getElementById('previewText').textContent = '🎉 연말 대박 이벤트!';
document.getElementById('previewArea').style.background = 'var(--color-gray-100)';
Toast.success('원본으로 되돌렸습니다.');
}
};
window.cancelEdit = function() {
if (confirm('편집을 취소하시겠습니까?')) {
closeEditor();
}
};
window.saveEdit = function() {
Loading.show('저장 중...');
setTimeout(function() {
Loading.hide();
Toast.success('편집 내용이 저장되었습니다!');
closeEditor();
}, 1000);
};
window.skipEdit = function() {
window.location.href = '14-콘텐츠최종승인.html';
};
})();
</script>
<style>
.content-card {
background: white;
border: 2px solid var(--color-gray-300);
border-radius: var(--radius-md);
padding: var(--spacing-m);
display: flex;
flex-direction: column;
align-items: center;
gap: var(--spacing-s);
cursor: pointer;
transition: all 0.2s ease;
const storage = {
get: (key) => {
try {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : null;
} catch (e) {
console.error('Storage get error:', e);
return null;
}
.content-card:hover {
border-color: var(--color-primary-main);
transform: translateY(-2px);
},
set: (key, value) => {
try {
localStorage.setItem(key, JSON.stringify(value));
} catch (e) {
console.error('Storage set error:', e);
}
}
};
.content-card-icon {
width: 64px;
height: 64px;
border-radius: var(--radius-md);
display: flex;
align-items: center;
justify-content: center;
}
// Elements
const copyText = $('#copyText');
const copyCharCount = $('#copyCharCount');
const posterTitle = $('#posterTitle');
const posterSubtitle = $('#posterSubtitle');
const regenerateCopy = $('#regenerateCopy');
const regenerateImages = $('#regenerateImages');
const regeneratePoster = $('#regeneratePoster');
const saveButton = $('#saveButton');
const nextButton = $('#nextButton');
.color-option {
aspect-ratio: 1;
border-radius: var(--radius-sm);
border: 2px solid transparent;
cursor: pointer;
transition: transform 0.2s ease;
}
// Update character count
function updateCharCount() {
const count = copyText.value.length;
copyCharCount.textContent = count;
.color-option:hover {
transform: scale(1.1);
if (count > 500) {
copyCharCount.style.color = 'var(--color-error)';
} else {
copyCharCount.style.color = 'var(--color-text-secondary)';
}
}
copyText.addEventListener('input', updateCharCount);
// Load saved data
function loadSavedData() {
const eventContent = storage.get('eventContent') || {};
if (eventContent.editedContent) {
if (eventContent.editedContent.copy) {
copyText.value = eventContent.editedContent.copy;
}
</style>
if (eventContent.editedContent.posterTitle) {
posterTitle.value = eventContent.editedContent.posterTitle;
}
if (eventContent.editedContent.posterSubtitle) {
posterSubtitle.value = eventContent.editedContent.posterSubtitle;
}
}
updateCharCount();
}
// Save edited content
function saveContent() {
const eventContent = storage.get('eventContent') || {};
eventContent.editedContent = {
copy: copyText.value,
posterTitle: posterTitle.value,
posterSubtitle: posterSubtitle.value,
savedAt: new Date().toISOString()
};
storage.set('eventContent', eventContent);
}
// Regenerate buttons
regenerateCopy.addEventListener('click', () => {
regenerateCopy.textContent = '⏳ 생성 중...';
regenerateCopy.disabled = true;
setTimeout(() => {
copyText.value = `🎊 특별 이벤트 🎊\n\n새로운 혜택이 가득합니다!\n✨ 할인 혜택\n✨ 무료 배송\n✨ 포인트 적립\n\n지금 참여하세요!`;
updateCharCount();
regenerateCopy.textContent = '🔄 재생성';
regenerateCopy.disabled = false;
}, 1500);
});
regenerateImages.addEventListener('click', () => {
regenerateImages.textContent = '⏳ 생성 중...';
regenerateImages.disabled = true;
setTimeout(() => {
alert('새로운 이미지가 생성되었습니다!');
regenerateImages.textContent = '🔄 재생성';
regenerateImages.disabled = false;
}, 1500);
});
regeneratePoster.addEventListener('click', () => {
regeneratePoster.textContent = '⏳ 생성 중...';
regeneratePoster.disabled = true;
setTimeout(() => {
alert('새로운 QR 포스터가 생성되었습니다!');
regeneratePoster.textContent = '🔄 재생성';
regeneratePoster.disabled = false;
}, 1500);
});
// Save button
saveButton.addEventListener('click', () => {
saveContent();
alert('✅ 임시 저장되었습니다!');
});
// Next button
nextButton.addEventListener('click', () => {
saveContent();
const eventContent = storage.get('eventContent') || {};
eventContent.status = 'content_edited';
eventContent.editedAt = new Date().toISOString();
storage.set('eventContent', eventContent);
addClass(nextButton, 'next-button--loading');
nextButton.disabled = true;
setTimeout(() => {
window.location.href = '14-콘텐츠최종승인.html';
}, 800);
});
// Initialize
window.addEventListener('load', () => {
loadSavedData();
});
</script>
</body>
</html>

View File

@ -1,279 +1,733 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>콘텐츠 최종 승인 - KT AI 이벤트</title>
<link rel="stylesheet" href="css/common.css">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Pretendard:wght@400;500;600;700&display=swap" rel="stylesheet">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>콘텐츠 최종 승인 - KT 이벤트 마케팅</title>
<style>
/* CSS Variables */
:root {
/* Colors */
--color-primary: #E31E24;
--color-primary-dark: #B71C1C;
--color-primary-light: #FFEBEE;
--color-secondary: #0066FF;
--color-secondary-dark: #0052CC;
--color-secondary-light: #E3F2FD;
--color-success: #4CAF50;
--color-success-light: #E8F5E9;
--color-warning: #FF9800;
--color-warning-light: #FFF3E0;
--color-error: #F44336;
--color-error-light: #FFEBEE;
--color-info: #2196F3;
--color-info-light: #E3F2FD;
/* Grayscale */
--color-white: #FFFFFF;
--color-gray-50: #FAFAFA;
--color-gray-100: #F5F5F5;
--color-gray-200: #EEEEEE;
--color-gray-300: #E0E0E0;
--color-gray-400: #BDBDBD;
--color-gray-500: #9E9E9E;
--color-gray-600: #757575;
--color-gray-700: #616161;
--color-gray-800: #424242;
--color-gray-900: #212121;
--color-black: #000000;
/* Text Colors */
--color-text-primary: #212121;
--color-text-secondary: #757575;
--color-text-tertiary: #9E9E9E;
--color-text-disabled: #BDBDBD;
--color-text-inverse: #FFFFFF;
/* Border Colors */
--color-border-light: #E0E0E0;
--color-border-medium: #BDBDBD;
--color-border-dark: #757575;
/* Background Colors */
--color-bg-primary: #FFFFFF;
--color-bg-secondary: #F5F5F5;
--color-bg-tertiary: #FAFAFA;
/* Spacing (4px base unit) */
--spacing-xs: 4px;
--spacing-s: 8px;
--spacing-m: 16px;
--spacing-l: 24px;
--spacing-xl: 32px;
--spacing-2xl: 48px;
--spacing-3xl: 64px;
/* Typography */
--font-family-primary: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
--font-size-xs: 12px;
--font-size-s: 14px;
--font-size-m: 16px;
--font-size-l: 18px;
--font-size-xl: 20px;
--font-size-2xl: 24px;
--font-size-3xl: 32px;
--font-size-4xl: 40px;
--font-weight-regular: 400;
--font-weight-medium: 500;
--font-weight-semibold: 600;
--font-weight-bold: 700;
--line-height-tight: 1.2;
--line-height-normal: 1.5;
--line-height-relaxed: 1.75;
/* Border Radius */
--radius-s: 4px;
--radius-m: 8px;
--radius-l: 12px;
--radius-xl: 16px;
--radius-2xl: 24px;
--radius-full: 9999px;
/* Shadows */
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
--shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
/* Transitions */
--transition-fast: 150ms ease-in-out;
--transition-normal: 250ms ease-in-out;
--transition-slow: 350ms ease-in-out;
/* Z-index */
--z-dropdown: 1000;
--z-sticky: 1020;
--z-fixed: 1030;
--z-modal-backdrop: 1040;
--z-modal: 1050;
--z-popover: 1060;
--z-tooltip: 1070;
}
/* Reset & Base Styles */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: var(--font-family-primary);
font-size: var(--font-size-m);
line-height: var(--line-height-normal);
color: var(--color-text-primary);
background: var(--color-bg-secondary);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
button {
font-family: inherit;
cursor: pointer;
}
/* Layout */
.approval-container {
min-height: 100vh;
background: linear-gradient(135deg, #F8F9FF 0%, #FFFFFF 100%);
padding-bottom: var(--spacing-2xl);
}
/* Header */
.approval-header {
background: linear-gradient(135deg, var(--color-secondary) 0%, var(--color-secondary-dark) 100%);
color: var(--color-white);
padding: var(--spacing-xl) var(--spacing-m);
box-shadow: var(--shadow-lg);
}
.approval-header__back {
background: none;
border: none;
color: var(--color-white);
font-size: var(--font-size-xl);
padding: 0;
margin-bottom: var(--spacing-m);
cursor: pointer;
}
.approval-header__badge {
display: inline-block;
background: rgba(255, 255, 255, 0.2);
padding: var(--spacing-xs) var(--spacing-m);
border-radius: var(--radius-s);
font-size: var(--font-size-s);
margin-bottom: var(--spacing-s);
}
.approval-header__title {
font-size: var(--font-size-2xl);
font-weight: var(--font-weight-bold);
margin-bottom: var(--spacing-xs);
}
.approval-header__subtitle {
font-size: var(--font-size-m);
opacity: 0.9;
}
/* Content */
.approval-content {
padding: var(--spacing-l) var(--spacing-m);
}
/* Success Message */
.success-message {
background: linear-gradient(135deg, #F0FFF4 0%, #FFFFFF 100%);
border-left: 4px solid var(--color-success);
padding: var(--spacing-m);
border-radius: var(--radius-m);
margin-bottom: var(--spacing-l);
}
.success-message__icon {
font-size: 48px;
text-align: center;
margin-bottom: var(--spacing-s);
}
.success-message__title {
font-size: var(--font-size-l);
font-weight: var(--font-weight-bold);
color: var(--color-text-primary);
text-align: center;
margin-bottom: var(--spacing-xs);
}
.success-message__text {
font-size: var(--font-size-s);
color: var(--color-text-secondary);
text-align: center;
line-height: 1.6;
}
/* Content Cards */
.content-cards {
display: grid;
gap: var(--spacing-m);
margin-bottom: var(--spacing-l);
}
.content-card {
background: var(--color-white);
border-radius: var(--radius-l);
overflow: hidden;
box-shadow: var(--shadow-md);
}
.content-card__header {
padding: var(--spacing-m);
background: linear-gradient(135deg, #F8F9FF 0%, #FFFFFF 100%);
border-bottom: 1px solid var(--color-border-light);
display: flex;
justify-content: space-between;
align-items: center;
}
.content-card__title {
font-size: var(--font-size-m);
font-weight: var(--font-weight-semibold);
color: var(--color-text-primary);
display: flex;
align-items: center;
gap: var(--spacing-s);
}
.content-card__count {
background: var(--color-secondary-light);
color: var(--color-secondary);
padding: 2px var(--spacing-s);
border-radius: var(--radius-s);
font-size: var(--font-size-xs);
font-weight: var(--font-weight-semibold);
}
.content-card__edit {
background: none;
border: 1px solid var(--color-border-light);
color: var(--color-text-secondary);
padding: var(--spacing-xs) var(--spacing-s);
border-radius: var(--radius-s);
font-size: var(--font-size-xs);
cursor: pointer;
transition: all var(--transition-normal);
}
.content-card__edit:hover {
border-color: var(--color-secondary);
color: var(--color-secondary);
}
.content-card__body {
padding: var(--spacing-m);
}
/* Image Grid */
.image-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: var(--spacing-s);
}
.image-item {
aspect-ratio: 1;
background: linear-gradient(135deg, #E0E7FF 0%, #F0F7FF 100%);
border-radius: var(--radius-m);
display: flex;
align-items: center;
justify-content: center;
font-size: 32px;
border: 1px solid var(--color-border-light);
}
/* SNS List */
.sns-list {
display: grid;
gap: var(--spacing-s);
}
.sns-item {
padding: var(--spacing-s);
background: linear-gradient(135deg, #F8F9FF 0%, #FFFFFF 100%);
border-radius: var(--radius-m);
display: flex;
align-items: center;
gap: var(--spacing-s);
border: 1px solid var(--color-border-light);
}
.sns-item__icon {
font-size: var(--font-size-xl);
}
.sns-item__info {
flex: 1;
}
.sns-item__name {
font-size: var(--font-size-s);
font-weight: var(--font-weight-semibold);
color: var(--color-text-primary);
margin-bottom: 2px;
}
.sns-item__preview {
font-size: var(--font-size-xs);
color: var(--color-text-secondary);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.sns-item__badge {
background: var(--color-success-light);
color: var(--color-success);
padding: 2px var(--spacing-xs);
border-radius: var(--radius-s);
font-size: var(--font-size-xs);
}
/* Poster Preview */
.poster-preview {
aspect-ratio: 3 / 4;
background: linear-gradient(135deg, #F8F9FF 0%, #FFFFFF 100%);
border-radius: var(--radius-m);
padding: var(--spacing-l);
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
border: 1px solid var(--color-border-light);
}
.poster-preview__title {
font-size: var(--font-size-l);
font-weight: var(--font-weight-bold);
text-align: center;
color: var(--color-text-primary);
}
.poster-preview__qr {
width: 100px;
height: 100px;
background: var(--color-white);
border: 2px solid var(--color-border-light);
border-radius: var(--radius-m);
display: flex;
align-items: center;
justify-content: center;
font-size: 48px;
}
.poster-preview__cta {
font-size: var(--font-size-s);
text-align: center;
color: var(--color-text-secondary);
}
/* Stats */
.content-stats {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: var(--spacing-s);
margin-bottom: var(--spacing-l);
}
.stat-card {
background: var(--color-white);
border-radius: var(--radius-m);
padding: var(--spacing-m);
text-align: center;
box-shadow: var(--shadow-sm);
}
.stat-card__value {
font-size: var(--font-size-xl);
font-weight: var(--font-weight-bold);
color: var(--color-secondary);
margin-bottom: var(--spacing-xs);
}
.stat-card__label {
font-size: var(--font-size-xs);
color: var(--color-text-secondary);
}
/* Action Buttons */
.action-buttons {
display: grid;
gap: var(--spacing-m);
}
.action-button {
width: 100%;
padding: var(--spacing-m);
border: none;
border-radius: var(--radius-m);
font-size: var(--font-size-m);
font-weight: var(--font-weight-semibold);
cursor: pointer;
transition: all var(--transition-normal);
box-shadow: var(--shadow-md);
}
.action-button--primary {
background: linear-gradient(135deg, var(--color-secondary) 0%, var(--color-secondary-dark) 100%);
color: var(--color-white);
}
.action-button--primary:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-lg);
}
.action-button--secondary {
background: var(--color-white);
border: 2px solid var(--color-border-light);
color: var(--color-text-primary);
}
.action-button--secondary:hover {
border-color: var(--color-secondary);
color: var(--color-secondary);
}
.action-button--loading {
position: relative;
color: transparent;
}
.action-button--loading:after {
content: '';
position: absolute;
width: 20px;
height: 20px;
top: 50%;
left: 50%;
margin-left: -10px;
margin-top: -10px;
border: 3px solid rgba(255, 255, 255, 0.3);
border-radius: 50%;
border-top-color: var(--color-white);
animation: spinner 0.6s linear infinite;
}
@keyframes spinner {
to { transform: rotate(360deg); }
}
/* Tablet */
@media (min-width: 768px) {
.approval-header {
padding: var(--spacing-2xl) var(--spacing-xl);
}
.approval-content {
max-width: 720px;
margin: 0 auto;
padding: var(--spacing-xl);
}
.image-grid {
grid-template-columns: repeat(4, 1fr);
}
.action-buttons {
grid-template-columns: 1fr 1fr;
}
}
/* Desktop */
@media (min-width: 1024px) {
.approval-content {
max-width: 960px;
}
}
</style>
</head>
<body>
<a href="#main-content" class="skip-link">본문으로 건너뛰기</a>
<header class="header" role="banner">
<div class="header-content">
<button type="button" class="btn-icon" onclick="window.history.back()" aria-label="이전 페이지로 돌아가기">
<span class="material-icons">arrow_back</span>
</button>
<h1 class="header-title">콘텐츠 최종 확인</h1>
<div style="width: 40px;"></div>
</div>
<div class="approval-container">
<!-- Header -->
<header class="approval-header">
<button class="approval-header__back" onclick="history.back()">
</button>
<div class="approval-header__badge">✨ AI 콘텐츠 생성 완료</div>
<h1 class="approval-header__title">콘텐츠 최종 승인</h1>
<p class="approval-header__subtitle">생성된 모든 콘텐츠를 확인하고 승인해 주세요</p>
</header>
<div class="progress-container" role="progressbar" aria-valuenow="100" aria-valuemin="0" aria-valuemax="100">
<div class="progress-bar" style="width: 100%;"></div>
<div class="progress-label">콘텐츠 5/5: 최종 승인</div>
</div>
<!-- Content -->
<main class="approval-content">
<!-- Success Message -->
<div class="success-message">
<div class="success-message__icon">🎉</div>
<h2 class="success-message__title">완벽한 이벤트 콘텐츠가 준비되었어요!</h2>
<p class="success-message__text">
AI가 생성한 고품질 콘텐츠로 이벤트를 홍보할 준비가 완료되었습니다.
아래 내용을 최종 확인하고 승인하시면 바로 배포 단계로 이동합니다.
</p>
</div>
<main id="main-content" class="container" role="main" style="padding-bottom: 100px;">
<section class="section">
<div class="alert alert-success" role="status">
<div style="display: flex; align-items: center; gap: var(--spacing-s);">
<span class="material-icons" style="font-size: 24px;">check_circle</span>
<span class="body-l" style="font-weight: 600;">콘텐츠 생성 완료!</span>
</div>
</div>
</section>
<section class="section" aria-labelledby="contents-title">
<h2 id="contents-title" class="h3">생성된 콘텐츠 (갤러리)</h2>
<div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--spacing-s); margin-bottom: var(--spacing-l);">
<button type="button" class="gallery-item" onclick="viewContent('image', 1)">
<div style="aspect-ratio: 1; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: var(--radius-sm); display: flex; align-items: center; justify-content: center;">
<span class="material-icons" style="color: white; font-size: 32px;">image</span>
</div>
<div class="caption" style="margin-top: 4px; text-align: center;">이미지1</div>
</button>
<button type="button" class="gallery-item" onclick="viewContent('image', 2)">
<div style="aspect-ratio: 1; background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); border-radius: var(--radius-sm); display: flex; align-items: center; justify-content: center;">
<span class="material-icons" style="color: white; font-size: 32px;">image</span>
</div>
<div class="caption" style="margin-top: 4px; text-align: center;">이미지2</div>
</button>
<button type="button" class="gallery-item" onclick="viewContent('image', 3)">
<div style="aspect-ratio: 1; background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); border-radius: var(--radius-sm); display: flex; align-items: center; justify-content: center;">
<span class="material-icons" style="color: white; font-size: 32px;">image</span>
</div>
<div class="caption" style="margin-top: 4px; text-align: center;">이미지3</div>
</button>
<button type="button" class="gallery-item" onclick="viewContent('video', 1)">
<div style="aspect-ratio: 1; background: linear-gradient(135deg, #fa709a 0%, #fee140 100%); border-radius: var(--radius-sm); display: flex; align-items: center; justify-content: center;">
<span class="material-icons" style="color: white; font-size: 32px;">play_circle</span>
</div>
<div class="caption" style="margin-top: 4px; text-align: center;">15초영상</div>
</button>
<button type="button" class="gallery-item" onclick="viewContent('sns', 1)">
<div style="aspect-ratio: 1; background: linear-gradient(135deg, #30cfd0 0%, #330867 100%); border-radius: var(--radius-sm); display: flex; align-items: center; justify-content: center;">
<span class="material-icons" style="color: white; font-size: 32px;">share</span>
</div>
<div class="caption" style="margin-top: 4px; text-align: center;">SNS세트</div>
</button>
<button type="button" class="gallery-item" onclick="viewContent('qr', 1)">
<div style="aspect-ratio: 1; background: linear-gradient(135deg, #a8edea 0%, #fed6e3 100%); border-radius: var(--radius-sm); display: flex; align-items: center; justify-content: center;">
<span class="material-icons" style="color: #333; font-size: 32px;">qr_code</span>
</div>
<div class="caption" style="margin-top: 4px; text-align: center;">QR포스터</div>
</button>
</div>
<button type="button" class="btn btn-outline btn-lg" onclick="downloadAll()" style="width: 100%;">
<span class="material-icons">download</span>
<span>전체 다운로드</span>
</button>
</section>
<section class="section">
<div class="card">
<div class="card-body">
<div style="display: flex; align-items: center; gap: var(--spacing-s); margin-bottom: var(--spacing-s);">
<span class="material-icons" style="color: var(--color-info);">schedule</span>
<span class="body-m" style="font-weight: 600;">콘텐츠 생성 시간</span>
</div>
<div style="text-align: center; padding: var(--spacing-l);">
<div class="h2" style="color: var(--color-primary-main); margin-bottom: var(--spacing-xs);" id="elapsedTime">7분</div>
<div class="body-s" style="color: var(--color-gray-600);">목표: 5-8분 이내</div>
<div class="body-s" style="color: var(--color-success); font-weight: 600; margin-top: var(--spacing-xs);" id="timeStatus">✓ 목표 달성!</div>
</div>
</div>
</div>
</section>
</main>
<div class="fixed-bottom-button">
<button type="button" class="btn btn-outline btn-lg" onclick="goBack()" style="flex: 1;">
<span class="material-icons">arrow_back</span>
<span>수정</span>
</button>
<button type="button" class="btn btn-primary btn-lg" onclick="approve()" style="flex: 2;">
<span class="material-icons">check</span>
<span>승인하기</span>
</button>
</div>
<div id="viewModal" class="modal" role="dialog" aria-modal="true" style="display: none;">
<div class="modal-content" style="max-width: 90vw; max-height: 90vh;">
<div class="modal-header">
<h3 class="h4" id="viewTitle">콘텐츠 미리보기</h3>
<button type="button" class="btn-icon" onclick="closeView()">
<span class="material-icons">close</span>
</button>
</div>
<div class="modal-body" id="viewContent" style="padding: 0; max-height: 70vh; overflow: auto;">
<!-- Content preview will be inserted here -->
</div>
<!-- Stats -->
<div class="content-stats">
<div class="stat-card">
<div class="stat-card__value" id="imageCount">4개</div>
<div class="stat-card__label">생성된 이미지</div>
</div>
</div>
<div class="stat-card">
<div class="stat-card__value" id="snsCount">2개</div>
<div class="stat-card__label">SNS 콘텐츠</div>
</div>
<div class="stat-card">
<div class="stat-card__value" id="posterCount">1개</div>
<div class="stat-card__label">QR 포스터</div>
</div>
</div>
<script src="js/common.js"></script>
<script>
(function() {
'use strict';
<!-- Content Cards -->
<div class="content-cards">
<!-- Images -->
<div class="content-card">
<div class="content-card__header">
<div class="content-card__title">
🎨 AI 생성 이미지
<span class="content-card__count">4개</span>
</div>
<button class="content-card__edit" onclick="window.location.href='09-AI이미지생성.html'">
수정
</button>
</div>
<div class="content-card__body">
<div class="image-grid">
<div class="image-item">🎉</div>
<div class="image-item">🎊</div>
<div class="image-item">🎈</div>
<div class="image-item">🎁</div>
</div>
</div>
</div>
const eventData = AppState.currentEvent || {};
const storeData = AppState.store || {};
<!-- SNS Content -->
<div class="content-card">
<div class="content-card__header">
<div class="content-card__title">
📱 SNS 콘텐츠
<span class="content-card__count" id="snsContentCount">2개</span>
</div>
<button class="content-card__edit" onclick="window.location.href='11-SNS콘텐츠생성.html'">
수정
</button>
</div>
<div class="content-card__body">
<div class="sns-list" id="snsList">
<div class="sns-item">
<div class="sns-item__icon">📷</div>
<div class="sns-item__info">
<div class="sns-item__name">인스타그램</div>
<div class="sns-item__preview">🎉 친구 초대하고 함께 받는 특별한 혜택!</div>
</div>
<div class="sns-item__badge">생성완료</div>
</div>
<div class="sns-item">
<div class="sns-item__icon">👥</div>
<div class="sns-item__info">
<div class="sns-item__name">페이스북</div>
<div class="sns-item__preview">🎉 친구 초대 이벤트 진행 중!</div>
</div>
<div class="sns-item__badge">생성완료</div>
</div>
</div>
</div>
</div>
// Calculate elapsed time
const startTime = eventData.startTime || Date.now();
const elapsedMinutes = Math.floor((Date.now() - startTime) / 60000);
const displayTime = elapsedMinutes > 0 ? elapsedMinutes + '분' : '30초';
<!-- QR Poster -->
<div class="content-card">
<div class="content-card__header">
<div class="content-card__title">
📄 QR 포스터
<span class="content-card__count">1개</span>
</div>
<button class="content-card__edit" onclick="window.location.href='12-QR포스터생성.html'">
수정
</button>
</div>
<div class="content-card__body">
<div class="poster-preview">
<h3 class="poster-preview__title">🎉 친구 초대 이벤트</h3>
<div class="poster-preview__qr">📱</div>
<p class="poster-preview__cta">QR 코드를 스캔하고 참여하세요!</p>
</div>
</div>
</div>
</div>
document.addEventListener('DOMContentLoaded', function() {
document.getElementById('elapsedTime').textContent = displayTime;
<!-- Action Buttons -->
<div class="action-buttons">
<button class="action-button action-button--secondary" id="saveButton">
📝 임시저장
</button>
<button class="action-button action-button--primary" id="approveButton">
✅ 승인하고 배포하기
</button>
</div>
</main>
</div>
const targetMin = 5;
const targetMax = 8;
const statusEl = document.getElementById('timeStatus');
<script>
// Utility functions
const $ = (selector) => document.querySelector(selector);
if (elapsedMinutes <= targetMax) {
statusEl.textContent = '✓ 목표 달성!';
statusEl.style.color = 'var(--color-success)';
} else {
statusEl.textContent = '⚠ 목표 초과';
statusEl.style.color = 'var(--color-warning)';
}
});
const addClass = (el, className) => el?.classList.add(className);
window.viewContent = function(type, index) {
const modal = document.getElementById('viewModal');
const title = document.getElementById('viewTitle');
const content = document.getElementById('viewContent');
const typeNames = {
image: '이미지',
video: '영상',
sns: 'SNS',
qr: 'QR 포스터'
};
title.textContent = typeNames[type] + ' ' + (index || '') + ' 미리보기';
let html = '';
if (type === 'image') {
const gradients = [
'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)',
'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)'
];
html = `
<div style="aspect-ratio: 1; background: ${gradients[index-1]}; display: flex; align-items: center; justify-content: center; color: white; padding: var(--spacing-xl); text-align: center;">
<div>
<div class="h2" style="color: white; margin-bottom: var(--spacing-m);">🎁</div>
<div class="h3" style="color: white;">${eventData.promotionMessage?.text?.split('\n')[0] || '이벤트 참여하세요!'}</div>
</div>
</div>
`;
} else if (type === 'video') {
html = `
<div style="aspect-ratio: 16/9; background: linear-gradient(135deg, #fa709a 0%, #fee140 100%); display: flex; align-items: center; justify-content: center; position: relative;">
<div style="text-align: center; color: white;">
<span class="material-icons" style="font-size: 64px; color: white; margin-bottom: var(--spacing-m);">play_circle</span>
<div class="h4" style="color: white;">15초 홍보 영상</div>
</div>
</div>
`;
} else if (type === 'sns') {
html = `
<div style="padding: var(--spacing-l);">
<div style="margin-bottom: var(--spacing-m);">
<div class="body-m" style="font-weight: 600; margin-bottom: var(--spacing-s);">Instagram</div>
<div style="aspect-ratio: 1; background: linear-gradient(135deg, #30cfd0 0%, #330867 100%); border-radius: var(--radius-md);"></div>
</div>
<div>
<div class="body-m" style="font-weight: 600; margin-bottom: var(--spacing-s);">Naver Blog</div>
<div style="aspect-ratio: 4/3; background: linear-gradient(135deg, #a8edea 0%, #fed6e3 100%); border-radius: var(--radius-md);"></div>
</div>
</div>
`;
} else if (type === 'qr') {
html = `
<div style="padding: var(--spacing-xl); background: white; text-align: center;">
<div class="h4" style="margin-bottom: var(--spacing-l);">${storeData.name || '우리가게'} 이벤트</div>
<div style="width: 200px; height: 200px; margin: 0 auto var(--spacing-l); background: white; border: 2px solid var(--color-gray-300);">
<div style="width: 100%; height: 100%; background: linear-gradient(45deg, #000 25%, transparent 25%, transparent 75%, #000 75%, #000), linear-gradient(45deg, #000 25%, transparent 25%, transparent 75%, #000 75%, #000); background-size: 20px 20px; background-position: 0 0, 10px 10px;"></div>
</div>
<div class="body-m" style="color: var(--color-gray-600);">QR 코드를 스캔하세요</div>
</div>
`;
}
content.innerHTML = html;
Modal.open(modal);
};
window.closeView = function() {
Modal.close(document.getElementById('viewModal'));
};
window.downloadAll = function() {
Loading.show('전체 콘텐츠 다운로드 중...');
setTimeout(function() {
Loading.hide();
Toast.success('📦 모든 콘텐츠가 다운로드되었습니다!');
}, 2000);
};
window.goBack = function() {
if (confirm('수정 화면으로 돌아가시겠습니까?')) {
window.location.href = '13-콘텐츠편집.html';
}
};
window.approve = function() {
if (confirm('콘텐츠를 승인하고 배포 단계로 진행하시겠습니까?')) {
eventData.contentApproved = true;
eventData.contentApprovedAt = new Date().toISOString();
AppState.currentEvent = eventData;
AppState.save();
Toast.success('콘텐츠가 승인되었습니다!');
setTimeout(function() {
window.location.href = '16-배포진행상태.html';
}, 1000);
}
};
})();
</script>
<style>
.gallery-item {
background: transparent;
border: none;
cursor: pointer;
transition: transform 0.2s ease;
const storage = {
get: (key) => {
try {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : null;
} catch (e) {
console.error('Storage get error:', e);
return null;
}
.gallery-item:hover {
transform: scale(1.05);
},
set: (key, value) => {
try {
localStorage.setItem(key, JSON.stringify(value));
} catch (e) {
console.error('Storage set error:', e);
}
</style>
}
};
// Elements
const imageCount = $('#imageCount');
const snsCount = $('#snsCount');
const posterCount = $('#posterCount');
const snsContentCount = $('#snsContentCount');
const snsList = $('#snsList');
const saveButton = $('#saveButton');
const approveButton = $('#approveButton');
// Load content data
const eventContent = storage.get('eventContent') || {};
// Update stats
function updateStats() {
// Images
if (eventContent.generatedImages && eventContent.generatedImages.selectedImages) {
const count = eventContent.generatedImages.selectedImages.length;
imageCount.textContent = count + '개';
}
// SNS
if (eventContent.snsContent && eventContent.snsContent.selectedPlatforms) {
const count = eventContent.snsContent.selectedPlatforms.length;
snsCount.textContent = count + '개';
snsContentCount.textContent = count + '개';
// Render SNS list
const platforms = eventContent.snsContent.platforms || [];
snsList.innerHTML = platforms.map(platform => `
<div class="sns-item">
<div class="sns-item__icon">${platform.icon}</div>
<div class="sns-item__info">
<div class="sns-item__name">${platform.name}</div>
<div class="sns-item__preview">${platform.caption.substring(0, 50)}...</div>
</div>
<div class="sns-item__badge">생성완료</div>
</div>
`).join('');
}
// Poster
if (eventContent.qrPoster) {
posterCount.textContent = '1개';
}
}
// Save draft
saveButton.addEventListener('click', () => {
eventContent.status = 'content_draft';
eventContent.savedAt = new Date().toISOString();
storage.set('eventContent', eventContent);
alert('✅ 콘텐츠가 임시저장되었습니다.');
});
// Approve and continue
approveButton.addEventListener('click', () => {
// Mark as approved
eventContent.status = 'content_approved';
eventContent.approvedAt = new Date().toISOString();
storage.set('eventContent', eventContent);
// Show loading
addClass(approveButton, 'action-button--loading');
approveButton.disabled = true;
// Navigate to Phase 4 (Distribution)
setTimeout(() => {
window.location.href = '15-배포채널선택.html';
}, 1000);
});
// Initialize
window.addEventListener('load', () => {
updateStats();
});
</script>
</body>
</html>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,993 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>이벤트 참여 - KT AI 이벤트</title>
<style>
/* ========================================
CSS Variables & Base Styles
======================================== */
:root {
/* Colors */
--color-primary: #E31E24;
--color-primary-dark: #B71C1C;
--color-primary-light: #FFEBEE;
--color-secondary: #0066FF;
--color-secondary-dark: #0052CC;
--color-secondary-light: #E3F2FD;
--color-success: #4CAF50;
--color-success-dark: #388E3C;
--color-success-light: #E8F5E9;
--color-warning: #FF9800;
--color-warning-dark: #F57C00;
--color-warning-light: #FFF3E0;
--color-error: #F44336;
--color-error-dark: #D32F2F;
--color-error-light: #FFEBEE;
/* Text Colors */
--color-text-primary: #212121;
--color-text-secondary: #757575;
--color-text-tertiary: #9E9E9E;
--color-text-disabled: #BDBDBD;
--color-text-inverse: #FFFFFF;
/* Border Colors */
--color-border: #E0E0E0;
--color-border-light: #F5F5F5;
--color-border-dark: #BDBDBD;
/* Background Colors */
--color-bg-primary: #FFFFFF;
--color-bg-secondary: #F5F5F5;
--color-bg-tertiary: #FAFAFA;
/* Surface Colors */
--color-surface: #FFFFFF;
--color-surface-elevated: #FFFFFF;
/* Grayscale */
--color-white: #FFFFFF;
--color-gray-50: #FAFAFA;
--color-gray-100: #F5F5F5;
--color-gray-200: #EEEEEE;
--color-gray-300: #E0E0E0;
--color-gray-400: #BDBDBD;
--color-gray-500: #9E9E9E;
--color-gray-600: #757575;
--color-gray-700: #616161;
--color-gray-800: #424242;
--color-gray-900: #212121;
--color-black: #000000;
/* Spacing (4px base unit) */
--spacing-xs: 4px;
--spacing-s: 8px;
--spacing-m: 16px;
--spacing-l: 24px;
--spacing-xl: 32px;
--spacing-2xl: 48px;
--spacing-3xl: 64px;
/* Typography */
--font-family-primary: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
--font-family-secondary: "Noto Sans KR", sans-serif;
--font-family-mono: "SF Mono", Monaco, "Cascadia Code", "Roboto Mono", Consolas, "Courier New", monospace;
--font-size-xs: 12px;
--font-size-s: 14px;
--font-size-m: 16px;
--font-size-l: 18px;
--font-size-xl: 20px;
--font-size-2xl: 24px;
--font-size-3xl: 32px;
--font-size-4xl: 40px;
--font-weight-regular: 400;
--font-weight-medium: 500;
--font-weight-semibold: 600;
--font-weight-bold: 700;
--line-height-tight: 1.2;
--line-height-normal: 1.5;
--line-height-relaxed: 1.75;
/* Border Radius */
--radius-s: 4px;
--radius-m: 8px;
--radius-l: 12px;
--radius-xl: 16px;
--radius-2xl: 24px;
--radius-round: 9999px;
/* Shadows */
--shadow-xs: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
--shadow-sm: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
--shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
--shadow-2xl: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
/* Transitions */
--transition-fast: 150ms;
--transition-base: 200ms;
--transition-slow: 300ms;
--transition-slower: 500ms;
/* Durations */
--duration-instant: 0ms;
--duration-fast: 150ms;
--duration-normal: 300ms;
--duration-slow: 500ms;
/* Easings */
--easing-linear: linear;
--easing-ease: ease;
--easing-ease-in: ease-in;
--easing-ease-out: ease-out;
--easing-ease-in-out: ease-in-out;
--easing-smooth: cubic-bezier(0.4, 0, 0.2, 1);
--easing-bounce: cubic-bezier(0.68, -0.55, 0.265, 1.55);
/* Z-index */
--z-dropdown: 1000;
--z-sticky: 1020;
--z-fixed: 1030;
--z-modal-backdrop: 1040;
--z-modal: 1050;
--z-popover: 1060;
--z-tooltip: 1070;
}
/* Reset & Base Styles */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: var(--font-family-primary);
font-size: var(--font-size-m);
line-height: var(--line-height-normal);
color: var(--color-text-primary);
background: var(--color-bg-secondary);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
button {
font-family: inherit;
cursor: pointer;
border: none;
background: none;
}
/* Header Component */
.header {
background: var(--color-white);
padding: var(--spacing-m);
display: flex;
align-items: center;
justify-content: space-between;
box-shadow: var(--shadow-sm);
position: sticky;
top: 0;
z-index: var(--z-sticky);
}
.header__back {
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
color: var(--color-text-primary);
transition: color var(--transition-fast) var(--easing-smooth);
}
.header__back:hover {
color: var(--color-primary);
}
.header__title {
font-size: var(--font-size-l);
font-weight: var(--font-weight-bold);
color: var(--color-text-primary);
}
.header__actions {
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
}
/* Bottom Actions */
.bottom-actions {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: var(--color-white);
padding: var(--spacing-m);
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.1);
z-index: var(--z-sticky);
}
.bottom-actions__content {
max-width: 600px;
margin: 0 auto;
display: flex;
gap: var(--spacing-m);
}
/* Action Button Component */
.action-button {
padding: var(--spacing-m) var(--spacing-l);
border-radius: var(--radius-m);
font-size: var(--font-size-m);
font-weight: var(--font-weight-semibold);
transition: all var(--transition-fast) var(--easing-smooth);
position: relative;
overflow: hidden;
display: inline-flex;
align-items: center;
justify-content: center;
gap: var(--spacing-s);
flex: 1;
}
.action-button--primary {
background: var(--color-primary);
color: var(--color-white);
}
.action-button--primary:hover:not(:disabled) {
background: var(--color-primary-dark);
transform: translateY(-2px);
box-shadow: var(--shadow-lg);
}
.action-button--secondary {
background: var(--color-white);
color: var(--color-primary);
border: 2px solid var(--color-primary);
}
.action-button--secondary:hover:not(:disabled) {
background: var(--color-primary-light);
}
.action-button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.action-button--loading {
pointer-events: none;
}
.action-button--loading:after {
content: '';
position: absolute;
width: 20px;
height: 20px;
top: 50%;
left: 50%;
margin-left: -10px;
margin-top: -10px;
border: 3px solid rgba(255, 255, 255, 0.3);
border-radius: 50%;
border-top-color: var(--color-white);
animation: spinner 0.6s linear infinite;
}
@keyframes spinner {
to { transform: rotate(360deg); }
}
/* ========================================
18-이벤트참여.html (Customer-facing)
======================================== */
/* Main Container */
.event-participation {
min-height: 100vh;
background: linear-gradient(135deg, #F0F7FF 0%, #FFFFFF 100%);
padding: var(--spacing-l) var(--spacing-m);
}
/* Event Header */
.event-header {
text-align: center;
margin-bottom: var(--spacing-2xl);
animation: slide-down 0.6s var(--easing-smooth);
}
@keyframes slide-down {
0% { transform: translateY(-20px); opacity: 0; }
100% { transform: translateY(0); opacity: 1; }
}
.event-header__icon {
font-size: 64px;
margin-bottom: var(--spacing-m);
}
.event-header__store {
font-size: var(--font-size-l);
font-weight: var(--font-weight-bold);
color: var(--color-text-primary);
margin-bottom: var(--spacing-xs);
}
.event-header__title {
font-size: var(--font-size-2xl);
font-weight: var(--font-weight-bold);
color: var(--color-primary);
margin-bottom: var(--spacing-m);
}
/* Event Info Card */
.event-info {
background: var(--color-white);
border-radius: var(--radius-l);
padding: var(--spacing-l);
margin-bottom: var(--spacing-xl);
box-shadow: var(--shadow-s);
}
.event-info__row {
display: flex;
align-items: center;
gap: var(--spacing-m);
padding: var(--spacing-m) 0;
border-bottom: 1px solid var(--color-border);
}
.event-info__row:last-child {
border-bottom: none;
}
.event-info__icon {
font-size: 24px;
flex-shrink: 0;
}
.event-info__content {
flex: 1;
}
.event-info__label {
font-size: var(--font-size-xs);
color: var(--color-text-secondary);
margin-bottom: 2px;
}
.event-info__value {
font-size: var(--font-size-m);
font-weight: var(--font-weight-semibold);
color: var(--color-text-primary);
}
/* Participation Methods */
.participation-methods {
background: var(--color-white);
border-radius: var(--radius-l);
padding: var(--spacing-l);
margin-bottom: var(--spacing-xl);
box-shadow: var(--shadow-s);
}
.participation-methods__title {
font-size: var(--font-size-m);
font-weight: var(--font-weight-semibold);
color: var(--color-text-primary);
margin-bottom: var(--spacing-m);
}
.participation-methods__list {
list-style: none;
padding: 0;
margin: 0;
}
.participation-methods__item {
padding: var(--spacing-s) 0;
font-size: var(--font-size-s);
color: var(--color-text-secondary);
}
.participation-methods__item::before {
content: '✓ ';
color: var(--color-primary);
font-weight: var(--font-weight-bold);
margin-right: var(--spacing-xs);
}
/* Form */
.participation-form {
background: var(--color-white);
border-radius: var(--radius-l);
padding: var(--spacing-l);
margin-bottom: var(--spacing-xl);
box-shadow: var(--shadow-s);
}
.participation-form__title {
font-size: var(--font-size-l);
font-weight: var(--font-weight-bold);
color: var(--color-text-primary);
margin-bottom: var(--spacing-l);
}
.form-field {
margin-bottom: var(--spacing-l);
}
.form-field__label {
display: flex;
align-items: center;
gap: var(--spacing-xs);
font-size: var(--font-size-s);
font-weight: var(--font-weight-semibold);
color: var(--color-text-primary);
margin-bottom: var(--spacing-s);
}
.form-field__required {
color: var(--color-error);
}
.form-field__input {
width: 100%;
padding: var(--spacing-m);
font-size: var(--font-size-m);
border: 2px solid var(--color-border);
border-radius: var(--radius-m);
background: var(--color-white);
transition: all var(--duration-fast);
}
.form-field__input:focus {
outline: none;
border-color: var(--color-primary);
box-shadow: 0 0 0 3px var(--color-primary-light);
}
.form-field__input--error {
border-color: var(--color-error);
}
.form-field__error {
font-size: var(--font-size-xs);
color: var(--color-error);
margin-top: var(--spacing-xs);
display: none;
}
.form-field__error--visible {
display: block;
}
/* Channel Detection */
.channel-detection {
background: linear-gradient(135deg, #E3F2FD 0%, #F0F7FF 100%);
border-radius: var(--radius-m);
padding: var(--spacing-m);
margin-bottom: var(--spacing-l);
}
.channel-detection__label {
font-size: var(--font-size-xs);
color: var(--color-text-secondary);
margin-bottom: var(--spacing-xs);
}
.channel-detection__value {
font-size: var(--font-size-m);
font-weight: var(--font-weight-semibold);
color: var(--color-primary);
display: flex;
align-items: center;
gap: var(--spacing-xs);
}
/* Consent */
.consent-field {
margin-bottom: var(--spacing-l);
}
.consent-checkbox {
display: flex;
align-items: flex-start;
gap: var(--spacing-m);
padding: var(--spacing-m);
background: var(--color-surface-light);
border-radius: var(--radius-m);
cursor: pointer;
transition: all var(--duration-fast);
}
.consent-checkbox:hover {
background: var(--color-primary-light);
}
.consent-checkbox__box {
width: 24px;
height: 24px;
border: 2px solid var(--color-border-dark);
border-radius: var(--radius-s);
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
background: var(--color-white);
transition: all var(--duration-fast);
}
.consent-checkbox--checked .consent-checkbox__box {
background: var(--color-primary);
border-color: var(--color-primary);
}
.consent-checkbox__box::after {
content: '✓';
color: var(--color-white);
font-size: var(--font-size-m);
font-weight: var(--font-weight-bold);
opacity: 0;
transform: scale(0);
transition: all var(--duration-fast) var(--easing-bounce);
}
.consent-checkbox--checked .consent-checkbox__box::after {
opacity: 1;
transform: scale(1);
}
.consent-checkbox__label {
flex: 1;
font-size: var(--font-size-s);
color: var(--color-text-primary);
}
.consent-checkbox__required {
color: var(--color-error);
font-weight: var(--font-weight-semibold);
}
.consent-checkbox__link {
color: var(--color-primary);
text-decoration: underline;
cursor: pointer;
}
/* Benefit Badge */
.benefit-badge {
background: linear-gradient(135deg, #FFF8E1 0%, #FFE082 100%);
border-radius: var(--radius-l);
padding: var(--spacing-m);
text-align: center;
margin-bottom: var(--spacing-xl);
border: 2px dashed #FFA000;
}
.benefit-badge__icon {
font-size: 32px;
margin-bottom: var(--spacing-xs);
}
.benefit-badge__text {
font-size: var(--font-size-m);
font-weight: var(--font-weight-semibold);
color: #F57C00;
}
/* Submit Button */
.submit-button {
width: 100%;
padding: var(--spacing-l);
font-size: var(--font-size-l);
font-weight: var(--font-weight-bold);
color: var(--color-white);
background: linear-gradient(135deg, var(--color-primary), var(--color-secondary));
border: none;
border-radius: var(--radius-l);
cursor: pointer;
transition: all var(--duration-fast);
box-shadow: var(--shadow-m);
}
.submit-button:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: var(--shadow-l);
}
.submit-button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.submit-button--loading {
position: relative;
color: transparent;
}
.submit-button--loading::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 24px;
height: 24px;
margin: -12px 0 0 -12px;
border: 3px solid rgba(255, 255, 255, 0.3);
border-top-color: var(--color-white);
border-radius: var(--radius-full);
animation: spin var(--duration-slow) linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* Tablet */
@media (min-width: 768px) {
.event-participation {
max-width: 600px;
margin: 0 auto;
}
}
/* Desktop */
@media (min-width: 1024px) {
.event-participation {
max-width: 700px;
}
}
</style>
</head>
<body>
<!-- Main Content -->
<main class="event-participation">
<!-- Event Header -->
<div class="event-header">
<div class="event-header__icon">🎉</div>
<div class="event-header__store">수원 왕갈비통닭</div>
<div class="event-header__title">연말 대박 이벤트!</div>
</div>
<!-- Event Info -->
<div class="event-info">
<div class="event-info__row">
<div class="event-info__icon">🎁</div>
<div class="event-info__content">
<div class="event-info__label">경품</div>
<div class="event-info__value">치킨세트 무료교환권</div>
</div>
</div>
<div class="event-info__row">
<div class="event-info__icon">👥</div>
<div class="event-info__content">
<div class="event-info__label">당첨 인원</div>
<div class="event-info__value">100명</div>
</div>
</div>
<div class="event-info__row">
<div class="event-info__icon">📅</div>
<div class="event-info__content">
<div class="event-info__label">참여 기간</div>
<div class="event-info__value">~ 2025-12-31</div>
</div>
</div>
</div>
<!-- Participation Methods -->
<div class="participation-methods">
<div class="participation-methods__title">참여 방법</div>
<ul class="participation-methods__list">
<li class="participation-methods__item">매장 방문하기</li>
<li class="participation-methods__item">QR 코드 스캔하기</li>
<li class="participation-methods__item">아래 정보 입력하기</li>
</ul>
</div>
<!-- Participation Form -->
<div class="participation-form">
<h2 class="participation-form__title">참여 신청</h2>
<form id="participationForm">
<!-- Name -->
<div class="form-field">
<label class="form-field__label">
이름
<span class="form-field__required">*</span>
</label>
<input
type="text"
class="form-field__input"
id="nameInput"
placeholder="이름을 입력하세요"
required
>
<div class="form-field__error" id="nameError">이름을 입력해주세요</div>
</div>
<!-- Phone -->
<div class="form-field">
<label class="form-field__label">
전화번호
<span class="form-field__required">*</span>
</label>
<input
type="tel"
class="form-field__input"
id="phoneInput"
placeholder="010-0000-0000"
required
>
<div class="form-field__error" id="phoneError">올바른 전화번호를 입력해주세요</div>
</div>
<!-- Channel Detection -->
<div class="channel-detection">
<div class="channel-detection__label">참여 경로 (자동 감지)</div>
<div class="channel-detection__value">
<span id="channelIcon">📍</span>
<span id="channelText">QR 코드 스캔</span>
</div>
</div>
<!-- Consent -->
<div class="consent-field">
<div class="consent-checkbox" id="consentCheckbox">
<div class="consent-checkbox__box"></div>
<div class="consent-checkbox__label">
<span class="consent-checkbox__required">(필수)</span>
개인정보 수집 및 이용에 동의합니다
<span class="consent-checkbox__link" onclick="showConsentDetail(event)">자세히</span>
</div>
</div>
</div>
</form>
</div>
<!-- Benefit Badge -->
<div class="benefit-badge">
<div class="benefit-badge__icon">💡</div>
<div class="benefit-badge__text">
매장 방문 고객은 당첨 확률 UP! 🎁
</div>
</div>
<!-- Submit Button -->
<button class="submit-button" id="submitButton" disabled>
참여하기
</button>
</main>
<!-- Page Script -->
<script>
// AppState object with showToast method
const AppState = {
showToast: (message) => {
alert(message);
}
};
// Storage utility
const storage = {
get: (key) => {
try {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : null;
} catch (e) {
console.error('Storage get error:', e);
return null;
}
},
set: (key, value) => {
try {
localStorage.setItem(key, JSON.stringify(value));
} catch (e) {
console.error('Storage set error:', e);
}
}
};
// Helper functions
function addClass(element, className) {
if (element) element.classList.add(className);
}
function removeClass(element, className) {
if (element) element.classList.remove(className);
}
// Elements
const form = document.getElementById('participationForm');
const nameInput = document.getElementById('nameInput');
const phoneInput = document.getElementById('phoneInput');
const nameError = document.getElementById('nameError');
const phoneError = document.getElementById('phoneError');
const consentCheckbox = document.getElementById('consentCheckbox');
const submitButton = document.getElementById('submitButton');
// State
let isConsentChecked = false;
let detectedChannel = 'qr'; // qr, sns, tv, etc.
// Detect channel from URL params (in real app)
function detectChannel() {
const urlParams = new URLSearchParams(window.location.search);
const source = urlParams.get('source') || 'qr';
const channelMap = {
'qr': { icon: '📍', text: 'QR 코드 스캔' },
'instagram': { icon: '📷', text: 'Instagram' },
'blog': { icon: '📝', text: 'Naver Blog' },
'kakao': { icon: '💬', text: 'Kakao Channel' },
'tv': { icon: '📺', text: '우리동네TV' }
};
const channel = channelMap[source] || channelMap['qr'];
document.getElementById('channelIcon').textContent = channel.icon;
document.getElementById('channelText').textContent = channel.text;
detectedChannel = source;
}
// Validate phone number
function validatePhone(phone) {
// Remove hyphens
const cleaned = phone.replace(/-/g, '');
// Check format: 010-xxxx-xxxx or 01x-xxx-xxxx
const regex = /^01[0-9]\d{7,8}$/;
return regex.test(cleaned);
}
// Format phone number
function formatPhone(phone) {
const cleaned = phone.replace(/\D/g, '');
if (cleaned.length <= 3) {
return cleaned;
} else if (cleaned.length <= 7) {
return `${cleaned.slice(0, 3)}-${cleaned.slice(3)}`;
} else if (cleaned.length <= 10) {
return `${cleaned.slice(0, 3)}-${cleaned.slice(3, 6)}-${cleaned.slice(6)}`;
} else {
return `${cleaned.slice(0, 3)}-${cleaned.slice(3, 7)}-${cleaned.slice(7, 11)}`;
}
}
// Input listeners
nameInput.addEventListener('input', () => {
if (nameInput.value.trim()) {
removeClass(nameInput, 'form-field__input--error');
removeClass(nameError, 'form-field__error--visible');
}
updateSubmitButton();
});
phoneInput.addEventListener('input', (e) => {
// Auto-format phone number
e.target.value = formatPhone(e.target.value);
if (validatePhone(e.target.value)) {
removeClass(phoneInput, 'form-field__input--error');
removeClass(phoneError, 'form-field__error--visible');
}
updateSubmitButton();
});
phoneInput.addEventListener('blur', () => {
if (phoneInput.value && !validatePhone(phoneInput.value)) {
addClass(phoneInput, 'form-field__input--error');
addClass(phoneError, 'form-field__error--visible');
}
});
// Consent checkbox
consentCheckbox.addEventListener('click', () => {
isConsentChecked = !isConsentChecked;
if (isConsentChecked) {
addClass(consentCheckbox, 'consent-checkbox--checked');
} else {
removeClass(consentCheckbox, 'consent-checkbox--checked');
}
updateSubmitButton();
});
// Show consent detail
function showConsentDetail(event) {
event.stopPropagation();
alert('개인정보 수집 및 이용 동의\n\n수집 항목: 이름, 전화번호\n수집 목적: 이벤트 참여 및 당첨자 통보\n보유 기간: 이벤트 종료 후 3개월\n\n※ 동의를 거부할 수 있으나, 거부 시 이벤트 참여가 불가능합니다.');
}
// Update submit button
function updateSubmitButton() {
const isNameValid = nameInput.value.trim().length > 0;
const isPhoneValid = validatePhone(phoneInput.value);
const isFormValid = isNameValid && isPhoneValid && isConsentChecked;
submitButton.disabled = !isFormValid;
}
// Submit form
submitButton.addEventListener('click', async () => {
// Validate
let hasError = false;
if (!nameInput.value.trim()) {
addClass(nameInput, 'form-field__input--error');
addClass(nameError, 'form-field__error--visible');
hasError = true;
}
if (!validatePhone(phoneInput.value)) {
addClass(phoneInput, 'form-field__input--error');
addClass(phoneError, 'form-field__error--visible');
hasError = true;
}
if (!isConsentChecked) {
AppState.showToast('개인정보 수집 동의가 필요합니다');
hasError = true;
}
if (hasError) return;
// Show loading
addClass(submitButton, 'submit-button--loading');
submitButton.disabled = true;
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 1500));
// Check for duplicate (simulate)
const isDuplicate = Math.random() < 0.1; // 10% chance for demo
if (isDuplicate) {
removeClass(submitButton, 'submit-button--loading');
submitButton.disabled = false;
AppState.showToast('이미 참여하셨습니다 (2025-12-15 14:30)');
return;
}
// Save participation data
const participationData = {
name: nameInput.value.trim(),
phone: phoneInput.value,
channel: detectedChannel,
eventId: 'event-001', // From URL or context
participatedAt: new Date().toISOString(),
ticketNumber: generateTicketNumber()
};
storage.set('participationData', participationData);
// Navigate to completion page
window.location.href = '19-참여완료.html';
});
// Generate ticket number
function generateTicketNumber() {
const prefix = 'A';
const number = Math.floor(10000000 + Math.random() * 90000000);
return `${prefix}-${number}`;
}
// Initialize
detectChannel();
</script>
</body>
</html>

View File

@ -0,0 +1,905 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>참여 완료 - KT AI 이벤트</title>
<style>
/* ========================================
CSS Variables & Base Styles
======================================== */
:root {
/* Colors */
--color-primary: #E31E24;
--color-primary-dark: #B71C1C;
--color-primary-light: #FFEBEE;
--color-secondary: #0066FF;
--color-secondary-dark: #0052CC;
--color-secondary-light: #E3F2FD;
--color-success: #4CAF50;
--color-success-dark: #388E3C;
--color-success-light: #E8F5E9;
--color-warning: #FF9800;
--color-warning-dark: #F57C00;
--color-warning-light: #FFF3E0;
--color-error: #F44336;
--color-error-dark: #D32F2F;
--color-error-light: #FFEBEE;
/* Text Colors */
--color-text-primary: #212121;
--color-text-secondary: #757575;
--color-text-tertiary: #9E9E9E;
--color-text-disabled: #BDBDBD;
--color-text-inverse: #FFFFFF;
/* Border Colors */
--color-border: #E0E0E0;
--color-border-light: #F5F5F5;
--color-border-dark: #BDBDBD;
/* Background Colors */
--color-bg-primary: #FFFFFF;
--color-bg-secondary: #F5F5F5;
--color-bg-tertiary: #FAFAFA;
/* Surface Colors */
--color-surface: #FFFFFF;
--color-surface-elevated: #FFFFFF;
/* Grayscale */
--color-white: #FFFFFF;
--color-gray-50: #FAFAFA;
--color-gray-100: #F5F5F5;
--color-gray-200: #EEEEEE;
--color-gray-300: #E0E0E0;
--color-gray-400: #BDBDBD;
--color-gray-500: #9E9E9E;
--color-gray-600: #757575;
--color-gray-700: #616161;
--color-gray-800: #424242;
--color-gray-900: #212121;
--color-black: #000000;
/* Spacing (4px base unit) */
--spacing-xs: 4px;
--spacing-s: 8px;
--spacing-m: 16px;
--spacing-l: 24px;
--spacing-xl: 32px;
--spacing-2xl: 48px;
--spacing-3xl: 64px;
/* Typography */
--font-family-primary: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
--font-family-secondary: "Noto Sans KR", sans-serif;
--font-family-mono: "SF Mono", Monaco, "Cascadia Code", "Roboto Mono", Consolas, "Courier New", monospace;
--font-size-xs: 12px;
--font-size-s: 14px;
--font-size-m: 16px;
--font-size-l: 18px;
--font-size-xl: 20px;
--font-size-2xl: 24px;
--font-size-3xl: 32px;
--font-size-4xl: 40px;
--font-weight-regular: 400;
--font-weight-medium: 500;
--font-weight-semibold: 600;
--font-weight-bold: 700;
--line-height-tight: 1.2;
--line-height-normal: 1.5;
--line-height-relaxed: 1.75;
/* Border Radius */
--radius-s: 4px;
--radius-m: 8px;
--radius-l: 12px;
--radius-xl: 16px;
--radius-2xl: 24px;
--radius-round: 9999px;
/* Shadows */
--shadow-xs: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
--shadow-sm: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
--shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
--shadow-2xl: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
/* Transitions */
--transition-fast: 150ms;
--transition-base: 200ms;
--transition-slow: 300ms;
--transition-slower: 500ms;
/* Durations */
--duration-instant: 0ms;
--duration-fast: 150ms;
--duration-normal: 300ms;
--duration-slow: 500ms;
/* Easings */
--easing-linear: linear;
--easing-ease: ease;
--easing-ease-in: ease-in;
--easing-ease-out: ease-out;
--easing-ease-in-out: ease-in-out;
--easing-smooth: cubic-bezier(0.4, 0, 0.2, 1);
--easing-bounce: cubic-bezier(0.68, -0.55, 0.265, 1.55);
/* Z-index */
--z-dropdown: 1000;
--z-sticky: 1020;
--z-fixed: 1030;
--z-modal-backdrop: 1040;
--z-modal: 1050;
--z-popover: 1060;
--z-tooltip: 1070;
}
/* Reset & Base Styles */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: var(--font-family-primary);
font-size: var(--font-size-m);
line-height: var(--line-height-normal);
color: var(--color-text-primary);
background: var(--color-bg-secondary);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
button {
font-family: inherit;
cursor: pointer;
border: none;
background: none;
}
/* Header Component */
.header {
background: var(--color-white);
padding: var(--spacing-m);
display: flex;
align-items: center;
justify-content: space-between;
box-shadow: var(--shadow-sm);
position: sticky;
top: 0;
z-index: var(--z-sticky);
}
.header__back {
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
color: var(--color-text-primary);
transition: color var(--transition-fast) var(--easing-smooth);
}
.header__back:hover {
color: var(--color-primary);
}
.header__title {
font-size: var(--font-size-l);
font-weight: var(--font-weight-bold);
color: var(--color-text-primary);
}
.header__actions {
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
}
/* Bottom Actions */
.bottom-actions {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: var(--color-white);
padding: var(--spacing-m);
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.1);
z-index: var(--z-sticky);
}
.bottom-actions__content {
max-width: 600px;
margin: 0 auto;
display: flex;
gap: var(--spacing-m);
}
/* Action Button Component */
.action-button {
padding: var(--spacing-m) var(--spacing-l);
border-radius: var(--radius-m);
font-size: var(--font-size-m);
font-weight: var(--font-weight-semibold);
transition: all var(--transition-fast) var(--easing-smooth);
position: relative;
overflow: hidden;
display: inline-flex;
align-items: center;
justify-content: center;
gap: var(--spacing-s);
flex: 1;
}
.action-button--primary {
background: var(--color-primary);
color: var(--color-white);
}
.action-button--primary:hover:not(:disabled) {
background: var(--color-primary-dark);
transform: translateY(-2px);
box-shadow: var(--shadow-lg);
}
.action-button--secondary {
background: var(--color-white);
color: var(--color-primary);
border: 2px solid var(--color-primary);
}
.action-button--secondary:hover:not(:disabled) {
background: var(--color-primary-light);
}
.action-button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.action-button--loading {
pointer-events: none;
}
.action-button--loading:after {
content: '';
position: absolute;
width: 20px;
height: 20px;
top: 50%;
left: 50%;
margin-left: -10px;
margin-top: -10px;
border: 3px solid rgba(255, 255, 255, 0.3);
border-radius: 50%;
border-top-color: var(--color-white);
animation: spinner 0.6s linear infinite;
}
@keyframes spinner {
to { transform: rotate(360deg); }
}
/* ========================================
19-참여완료.html (Customer-facing)
======================================== */
/* Main Container */
.completion-page {
min-height: 100vh;
background: linear-gradient(135deg, #F0F7FF 0%, #FFFFFF 100%);
padding: var(--spacing-l) var(--spacing-m);
}
/* Success Header */
.success-header {
text-align: center;
margin-bottom: var(--spacing-2xl);
animation: success-pop 0.6s var(--easing-bounce);
}
@keyframes success-pop {
0% { transform: scale(0); opacity: 0; }
50% { transform: scale(1.1); }
100% { transform: scale(1); opacity: 1; }
}
.success-header__icon {
font-size: 80px;
margin-bottom: var(--spacing-l);
}
.success-header__title {
font-size: var(--font-size-2xl);
font-weight: var(--font-weight-bold);
color: var(--color-text-primary);
margin-bottom: var(--spacing-s);
}
.success-header__desc {
font-size: var(--font-size-m);
color: var(--color-text-secondary);
}
/* Ticket Card */
.ticket-card {
background: linear-gradient(135deg, var(--color-primary), var(--color-secondary));
border-radius: var(--radius-xl);
padding: var(--spacing-xl);
margin-bottom: var(--spacing-xl);
box-shadow: var(--shadow-l);
position: relative;
overflow: hidden;
}
.ticket-card::before {
content: '';
position: absolute;
top: -50%;
right: -50%;
width: 200%;
height: 200%;
background: radial-gradient(circle, rgba(255, 255, 255, 0.1) 0%, transparent 70%);
animation: ticket-shine 3s ease-in-out infinite;
}
@keyframes ticket-shine {
0%, 100% { transform: translateX(0) translateY(0); }
50% { transform: translateX(-30px) translateY(-30px); }
}
.ticket-card__label {
font-size: var(--font-size-s);
color: rgba(255, 255, 255, 0.8);
margin-bottom: var(--spacing-s);
position: relative;
z-index: 1;
}
.ticket-card__number {
font-size: var(--font-size-3xl);
font-weight: var(--font-weight-bold);
color: var(--color-white);
letter-spacing: 2px;
margin-bottom: var(--spacing-l);
font-family: 'Courier New', monospace;
position: relative;
z-index: 1;
}
.ticket-card__copy {
display: inline-flex;
align-items: center;
gap: var(--spacing-s);
padding: var(--spacing-s) var(--spacing-m);
font-size: var(--font-size-s);
font-weight: var(--font-weight-medium);
background: rgba(255, 255, 255, 0.2);
color: var(--color-white);
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: var(--radius-full);
cursor: pointer;
transition: all var(--duration-fast);
backdrop-filter: blur(10px);
position: relative;
z-index: 1;
}
.ticket-card__copy:hover {
background: rgba(255, 255, 255, 0.3);
transform: translateY(-2px);
}
/* Drawing Info */
.drawing-info {
background: var(--color-white);
border-radius: var(--radius-l);
padding: var(--spacing-l);
margin-bottom: var(--spacing-xl);
box-shadow: var(--shadow-s);
}
.drawing-info__row {
display: flex;
align-items: center;
gap: var(--spacing-m);
padding: var(--spacing-m) 0;
border-bottom: 1px solid var(--color-border);
}
.drawing-info__row:last-child {
border-bottom: none;
}
.drawing-info__icon {
font-size: 24px;
flex-shrink: 0;
}
.drawing-info__content {
flex: 1;
}
.drawing-info__label {
font-size: var(--font-size-xs);
color: var(--color-text-secondary);
margin-bottom: 2px;
}
.drawing-info__value {
font-size: var(--font-size-m);
font-weight: var(--font-weight-semibold);
color: var(--color-text-primary);
}
/* Notice */
.notice-card {
background: linear-gradient(135deg, #FFF8E1 0%, #FFECB3 100%);
border-radius: var(--radius-l);
padding: var(--spacing-l);
margin-bottom: var(--spacing-xl);
border-left: 4px solid #FFA000;
}
.notice-card__title {
font-size: var(--font-size-m);
font-weight: var(--font-weight-semibold);
color: #F57C00;
margin-bottom: var(--spacing-s);
display: flex;
align-items: center;
gap: var(--spacing-xs);
}
.notice-card__text {
font-size: var(--font-size-s);
color: #E65100;
line-height: 1.6;
}
/* Store Info */
.store-info {
background: var(--color-white);
border-radius: var(--radius-l);
padding: var(--spacing-l);
margin-bottom: var(--spacing-xl);
box-shadow: var(--shadow-s);
}
.store-info__title {
font-size: var(--font-size-m);
font-weight: var(--font-weight-semibold);
color: var(--color-text-primary);
margin-bottom: var(--spacing-m);
}
.store-info__item {
display: flex;
align-items: flex-start;
gap: var(--spacing-m);
margin-bottom: var(--spacing-m);
}
.store-info__item:last-child {
margin-bottom: 0;
}
.store-info__icon {
font-size: 20px;
flex-shrink: 0;
}
.store-info__content {
flex: 1;
font-size: var(--font-size-s);
color: var(--color-text-secondary);
line-height: 1.6;
}
.store-info__button {
width: 100%;
padding: var(--spacing-m);
font-size: var(--font-size-m);
font-weight: var(--font-weight-medium);
background: var(--color-white);
color: var(--color-primary);
border: 2px solid var(--color-primary);
border-radius: var(--radius-l);
cursor: pointer;
transition: all var(--duration-fast);
margin-top: var(--spacing-m);
}
.store-info__button:hover {
background: var(--color-primary-light);
transform: translateY(-2px);
box-shadow: var(--shadow-s);
}
/* Share Section */
.share-section {
text-align: center;
margin-bottom: var(--spacing-2xl);
}
.share-section__title {
font-size: var(--font-size-m);
font-weight: var(--font-weight-semibold);
color: var(--color-text-primary);
margin-bottom: var(--spacing-m);
display: flex;
align-items: center;
justify-content: center;
gap: var(--spacing-xs);
}
.share-buttons {
display: flex;
gap: var(--spacing-m);
justify-content: center;
}
.share-button {
flex: 1;
max-width: 120px;
padding: var(--spacing-m);
background: var(--color-white);
border: 1px solid var(--color-border);
border-radius: var(--radius-l);
cursor: pointer;
transition: all var(--duration-fast);
box-shadow: var(--shadow-s);
}
.share-button:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-m);
}
.share-button__icon {
font-size: 32px;
margin-bottom: var(--spacing-xs);
}
.share-button__label {
font-size: var(--font-size-xs);
color: var(--color-text-secondary);
}
/* Confetti */
.confetti {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 999;
}
.confetti-piece {
position: absolute;
width: 10px;
height: 10px;
background: var(--color-primary);
animation: confetti-fall 3s linear forwards;
}
@keyframes confetti-fall {
to {
transform: translateY(100vh) rotate(720deg);
opacity: 0;
}
}
/* Tablet */
@media (min-width: 768px) {
.completion-page {
max-width: 600px;
margin: 0 auto;
}
}
/* Desktop */
@media (min-width: 1024px) {
.completion-page {
max-width: 700px;
}
}
</style>
</head>
<body>
<!-- Confetti Container -->
<div class="confetti" id="confetti"></div>
<!-- Main Content -->
<main class="completion-page">
<!-- Success Header -->
<div class="success-header">
<div class="success-header__icon"></div>
<h1 class="success-header__title">참여 완료!</h1>
<p class="success-header__desc">이벤트 참여가 완료되었습니다</p>
</div>
<!-- Ticket Card -->
<div class="ticket-card">
<div class="ticket-card__label">응모번호</div>
<div class="ticket-card__number" id="ticketNumber">A-12345678</div>
<button class="ticket-card__copy" onclick="copyTicketNumber()">
📋 복사하기
</button>
</div>
<!-- Drawing Info -->
<div class="drawing-info">
<div class="drawing-info__row">
<div class="drawing-info__icon">📅</div>
<div class="drawing-info__content">
<div class="drawing-info__label">당첨 발표일</div>
<div class="drawing-info__value" id="drawingDate">2025-12-31</div>
</div>
</div>
<div class="drawing-info__row">
<div class="drawing-info__icon">📱</div>
<div class="drawing-info__content">
<div class="drawing-info__label">당첨자 통보 방법</div>
<div class="drawing-info__value">SMS / 알림톡</div>
</div>
</div>
</div>
<!-- Notice -->
<div class="notice-card">
<div class="notice-card__title">
💡 안내사항
</div>
<div class="notice-card__text">
당첨 시 입력하신 전화번호로 SMS/알림톡이 발송됩니다.<br>
응모번호는 당첨 확인 시 필요하니 꼭 기억해주세요!
</div>
</div>
<!-- Store Info -->
<div class="store-info">
<div class="store-info__title">매장 정보</div>
<div class="store-info__item">
<div class="store-info__icon">🏪</div>
<div class="store-info__content" id="storeName">수원 왕갈비통닭</div>
</div>
<div class="store-info__item">
<div class="store-info__icon">📍</div>
<div class="store-info__content" id="storeAddress">
경기도 수원시 팔달구 xxx-xxx
</div>
</div>
<div class="store-info__item">
<div class="store-info__icon">☎️</div>
<div class="store-info__content" id="storePhone">031-XXX-XXXX</div>
</div>
<button class="store-info__button" onclick="openMap()">
매장 위치 보기
</button>
</div>
<!-- Share Section -->
<div class="share-section">
<div class="share-section__title">
💬 친구에게 공유하기
</div>
<div class="share-buttons">
<button class="share-button" onclick="shareToSNS('instagram')">
<div class="share-button__icon">📷</div>
<div class="share-button__label">Instagram</div>
</button>
<button class="share-button" onclick="shareToSNS('kakao')">
<div class="share-button__icon">💬</div>
<div class="share-button__label">KakaoTalk</div>
</button>
<button class="share-button" onclick="shareToSNS('link')">
<div class="share-button__icon">🔗</div>
<div class="share-button__label">링크 복사</div>
</button>
</div>
</div>
</main>
<!-- Page Script -->
<script>
// AppState object with showToast method
const AppState = {
showToast: (message) => {
alert(message);
}
};
// Storage utility
const storage = {
get: (key) => {
try {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : null;
} catch (e) {
console.error('Storage get error:', e);
return null;
}
},
set: (key, value) => {
try {
localStorage.setItem(key, JSON.stringify(value));
} catch (e) {
console.error('Storage set error:', e);
}
}
};
// Elements
const ticketNumber = document.getElementById('ticketNumber');
const drawingDate = document.getElementById('drawingDate');
const storeName = document.getElementById('storeName');
const storeAddress = document.getElementById('storeAddress');
const storePhone = document.getElementById('storePhone');
const confettiContainer = document.getElementById('confetti');
// Initialize
function init() {
// Load participation data
const participationData = storage.get('participationData');
if (participationData && participationData.ticketNumber) {
ticketNumber.textContent = participationData.ticketNumber;
}
// Load event data (would come from API in real app)
const eventDraft = storage.get('eventDraft');
const storeData = storage.get('storeData');
if (storeData) {
storeName.textContent = storeData.businessName || '수원 왕갈비통닭';
storeAddress.textContent = storeData.address || '경기도 수원시 팔달구 xxx-xxx';
storePhone.textContent = storeData.phone || '031-XXX-XXXX';
}
// Show confetti animation
createConfetti();
}
// Create confetti animation
function createConfetti() {
const colors = ['#E31E24', '#0066FF', '#00C853', '#FFA000', '#9C27B0'];
const confettiCount = 50;
for (let i = 0; i < confettiCount; i++) {
const confetti = document.createElement('div');
confetti.className = 'confetti-piece';
// Random positioning
confetti.style.left = Math.random() * 100 + '%';
confetti.style.animationDelay = Math.random() * 0.5 + 's';
confetti.style.animationDuration = (Math.random() * 2 + 2) + 's';
// Random color
confetti.style.background = colors[Math.floor(Math.random() * colors.length)];
// Random size
const size = Math.random() * 5 + 5;
confetti.style.width = size + 'px';
confetti.style.height = size + 'px';
confettiContainer.appendChild(confetti);
// Remove after animation
setTimeout(() => {
confetti.remove();
}, 3000);
}
}
// Copy ticket number
function copyTicketNumber() {
const number = ticketNumber.textContent;
// Try using Clipboard API
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(number).then(() => {
AppState.showToast('응모번호가 복사되었습니다');
}).catch(() => {
fallbackCopy(number);
});
} else {
fallbackCopy(number);
}
}
// Fallback copy method
function fallbackCopy(text) {
const textarea = document.createElement('textarea');
textarea.value = text;
textarea.style.position = 'fixed';
textarea.style.opacity = '0';
document.body.appendChild(textarea);
textarea.select();
try {
document.execCommand('copy');
AppState.showToast('응모번호가 복사되었습니다');
} catch (err) {
AppState.showToast('복사에 실패했습니다');
}
document.body.removeChild(textarea);
}
// Open map
function openMap() {
const address = storeAddress.textContent;
// In real app, would use actual map service
AppState.showToast('매장 위치를 지도 앱에서 엽니다');
// Example: Open in Kakao Map
// const encodedAddress = encodeURIComponent(address);
// window.location.href = `kakaomap://search?q=${encodedAddress}`;
}
// Share to SNS
function shareToSNS(platform) {
const eventUrl = window.location.origin + '/18-이벤트참여.html?source=' + platform;
const eventTitle = storeName.textContent + ' 연말 대박 이벤트';
const eventText = '친구 초대하고 함께 경품을 받아가세요!';
switch (platform) {
case 'instagram':
AppState.showToast('Instagram 스토리로 공유합니다');
// In real app: open Instagram share
break;
case 'kakao':
AppState.showToast('KakaoTalk으로 공유합니다');
// In real app: use Kakao Share SDK
// Kakao.Link.sendDefault({ ... });
break;
case 'link':
// Copy link
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(eventUrl).then(() => {
AppState.showToast('링크가 복사되었습니다');
});
} else {
fallbackCopy(eventUrl);
}
break;
default:
AppState.showToast('공유 기능을 준비 중입니다');
}
}
// Initialize on load
init();
// Prevent back navigation (optional)
history.pushState(null, null, location.href);
window.onpopstate = function() {
history.go(1);
};
</script>
</body>
</html>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,846 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>이벤트 목록 - KT AI 이벤트 플랫폼</title>
<style>
/* CSS Reset */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
/* Design System Variables */
:root {
/* Colors */
--color-primary: #E31E24;
--color-primary-hover: #C71820;
--color-primary-light: #FF4D52;
--color-secondary: #0066FF;
--color-secondary-hover: #004DBF;
--color-secondary-light: #4D94FF;
--color-success: #00C853;
--color-warning: #FFA000;
--color-error: #D32F2F;
--color-info: #0288D1;
--color-gray-50: #F8F9FA;
--color-gray-100: #F1F3F5;
--color-gray-200: #E9ECEF;
--color-gray-300: #DEE2E6;
--color-gray-400: #CED4DA;
--color-gray-500: #ADB5BD;
--color-gray-600: #868E96;
--color-gray-700: #495057;
--color-gray-800: #343A40;
--color-gray-900: #212529;
--color-white: #FFFFFF;
--color-black: #000000;
/* Typography */
--font-family: 'Pretendard Variable', -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
/* Spacing (4px grid) */
--spacing-xs: 4px;
--spacing-s: 8px;
--spacing-m: 16px;
--spacing-l: 24px;
--spacing-xl: 32px;
--spacing-2xl: 48px;
/* Border Radius */
--radius-s: 4px;
--radius-m: 8px;
--radius-l: 12px;
--radius-xl: 16px;
--radius-full: 9999px;
/* Shadows */
--shadow-s: 0 1px 2px rgba(0, 0, 0, 0.05);
--shadow-m: 0 4px 6px rgba(0, 0, 0, 0.07);
--shadow-l: 0 10px 15px rgba(0, 0, 0, 0.1);
--shadow-xl: 0 20px 25px rgba(0, 0, 0, 0.15);
/* Transitions */
--transition-fast: 150ms ease-in-out;
--transition-base: 250ms ease-in-out;
--transition-slow: 350ms ease-in-out;
/* Z-index */
--z-header: 100;
--z-fab: 150;
--z-modal: 200;
--z-toast: 300;
}
/* Base Styles */
body {
font-family: var(--font-family);
font-size: 16px;
line-height: 1.5;
color: var(--color-gray-900);
background-color: var(--color-gray-50);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* Header */
.header {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 56px;
background-color: var(--color-white);
border-bottom: 1px solid var(--color-gray-200);
display: flex;
align-items: center;
padding: 0 var(--spacing-m);
z-index: var(--z-header);
}
.header__back {
width: 44px;
height: 44px;
display: flex;
align-items: center;
justify-content: center;
background: none;
border: none;
cursor: pointer;
color: var(--color-gray-900);
font-size: 24px;
transition: color var(--transition-fast);
}
.header__back:hover {
color: var(--color-primary);
}
.header__title {
flex: 1;
font-size: 18px;
font-weight: 600;
text-align: center;
margin: 0 var(--spacing-m);
}
.header__action {
width: 44px;
height: 44px;
display: flex;
align-items: center;
justify-content: center;
background: none;
border: none;
cursor: pointer;
color: var(--color-gray-900);
font-size: 20px;
transition: color var(--transition-fast);
}
.header__action:hover {
color: var(--color-primary);
}
/* Main */
.main {
padding-top: 56px;
padding-bottom: var(--spacing-l);
min-height: 100vh;
}
/* Section */
.section {
padding: var(--spacing-l) var(--spacing-m);
}
/* Search Bar */
.search-bar {
position: relative;
margin-bottom: var(--spacing-m);
}
.search-bar__input {
width: 100%;
height: 48px;
padding: 0 var(--spacing-m) 0 48px;
font-size: 15px;
border: 1px solid var(--color-gray-300);
border-radius: var(--radius-m);
background-color: var(--color-white);
transition: all var(--transition-fast);
}
.search-bar__input:focus {
outline: none;
border-color: var(--color-primary);
box-shadow: 0 0 0 3px rgba(227, 30, 36, 0.1);
}
.search-bar__icon {
position: absolute;
left: var(--spacing-m);
top: 50%;
transform: translateY(-50%);
color: var(--color-gray-500);
font-size: 20px;
pointer-events: none;
}
/* Filter Tabs */
.filter-tabs {
display: flex;
gap: var(--spacing-s);
margin-bottom: var(--spacing-l);
overflow-x: auto;
-webkit-overflow-scrolling: touch;
scrollbar-width: none;
}
.filter-tabs::-webkit-scrollbar {
display: none;
}
.filter-tab {
flex-shrink: 0;
padding: var(--spacing-s) var(--spacing-m);
background-color: var(--color-white);
border: 1px solid var(--color-gray-300);
border-radius: var(--radius-full);
font-size: 14px;
font-weight: 500;
color: var(--color-gray-700);
cursor: pointer;
transition: all var(--transition-fast);
white-space: nowrap;
}
.filter-tab:hover {
border-color: var(--color-primary);
color: var(--color-primary);
}
.filter-tab--active {
background-color: var(--color-primary);
border-color: var(--color-primary);
color: var(--color-white);
}
/* Event List */
.event-list {
display: flex;
flex-direction: column;
gap: var(--spacing-m);
}
.event-card {
background-color: var(--color-white);
border-radius: var(--radius-l);
padding: var(--spacing-m);
box-shadow: var(--shadow-s);
transition: box-shadow var(--transition-fast);
cursor: pointer;
}
.event-card:hover {
box-shadow: var(--shadow-m);
}
.event-card__header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: var(--spacing-m);
}
.event-card__title {
flex: 1;
font-size: 16px;
font-weight: 600;
color: var(--color-gray-900);
margin-bottom: var(--spacing-xs);
}
.event-card__status {
flex-shrink: 0;
margin-left: var(--spacing-m);
}
.status-badge {
display: inline-flex;
align-items: center;
padding: var(--spacing-xs) var(--spacing-s);
border-radius: var(--radius-full);
font-size: 12px;
font-weight: 500;
}
.status-badge--active {
background-color: #E8F5E9;
color: var(--color-success);
}
.status-badge--ended {
background-color: var(--color-gray-100);
color: var(--color-gray-600);
}
.status-badge--scheduled {
background-color: #E3F2FD;
color: var(--color-info);
}
.event-card__period {
font-size: 13px;
color: var(--color-gray-600);
margin-bottom: var(--spacing-m);
}
.event-card__stats {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: var(--spacing-m);
padding: var(--spacing-m);
background-color: var(--color-gray-50);
border-radius: var(--radius-m);
margin-bottom: var(--spacing-m);
}
.event-stat {
text-align: center;
}
.event-stat__label {
font-size: 11px;
color: var(--color-gray-600);
margin-bottom: var(--spacing-xs);
}
.event-stat__value {
font-size: 16px;
font-weight: 600;
color: var(--color-gray-900);
}
.event-card__actions {
display: flex;
gap: var(--spacing-s);
}
.action-btn {
flex: 1;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
gap: var(--spacing-xs);
background-color: var(--color-white);
border: 1px solid var(--color-gray-300);
border-radius: var(--radius-m);
font-size: 13px;
font-weight: 500;
color: var(--color-gray-700);
cursor: pointer;
transition: all var(--transition-fast);
}
.action-btn:hover {
border-color: var(--color-primary);
color: var(--color-primary);
}
.action-btn--primary {
background-color: var(--color-primary);
border-color: var(--color-primary);
color: var(--color-white);
}
.action-btn--primary:hover {
background-color: var(--color-primary-hover);
border-color: var(--color-primary-hover);
}
/* Empty State */
.empty-state {
text-align: center;
padding: var(--spacing-2xl) var(--spacing-m);
}
.empty-state__icon {
font-size: 64px;
margin-bottom: var(--spacing-m);
opacity: 0.3;
}
.empty-state__title {
font-size: 18px;
font-weight: 600;
color: var(--color-gray-900);
margin-bottom: var(--spacing-s);
}
.empty-state__description {
font-size: 14px;
color: var(--color-gray-600);
line-height: 1.6;
margin-bottom: var(--spacing-l);
}
.empty-state__action {
display: inline-flex;
align-items: center;
gap: var(--spacing-s);
padding: var(--spacing-m) var(--spacing-l);
background-color: var(--color-primary);
color: var(--color-white);
border: none;
border-radius: var(--radius-m);
font-size: 15px;
font-weight: 600;
cursor: pointer;
transition: all var(--transition-fast);
}
.empty-state__action:hover {
background-color: var(--color-primary-hover);
}
/* FAB */
.fab {
position: fixed;
bottom: var(--spacing-l);
right: var(--spacing-l);
width: 56px;
height: 56px;
display: flex;
align-items: center;
justify-content: center;
background-color: var(--color-primary);
color: var(--color-white);
border: none;
border-radius: var(--radius-full);
font-size: 24px;
cursor: pointer;
box-shadow: var(--shadow-l);
transition: all var(--transition-fast);
z-index: var(--z-fab);
}
.fab:hover {
background-color: var(--color-primary-hover);
box-shadow: var(--shadow-xl);
transform: scale(1.05);
}
/* Toast */
.toast {
position: fixed;
bottom: var(--spacing-l);
left: 50%;
transform: translateX(-50%) translateY(100px);
background-color: var(--color-gray-800);
color: var(--color-white);
padding: var(--spacing-m) var(--spacing-l);
border-radius: var(--radius-m);
font-size: 14px;
font-weight: 500;
box-shadow: var(--shadow-xl);
z-index: var(--z-toast);
opacity: 0;
transition: all var(--transition-base);
pointer-events: none;
}
.toast--visible {
transform: translateX(-50%) translateY(0);
opacity: 1;
}
/* Responsive */
@media (min-width: 768px) {
.header {
padding: 0 var(--spacing-l);
}
.section {
max-width: 768px;
margin: 0 auto;
padding: var(--spacing-xl) var(--spacing-l);
}
.event-card__stats {
grid-template-columns: repeat(6, 1fr);
}
}
@media (min-width: 1024px) {
.section {
max-width: 1024px;
}
}
</style>
</head>
<body>
<!-- Header -->
<header class="header">
<button class="header__back" aria-label="뒤로 가기"></button>
<h1 class="header__title">이벤트 목록</h1>
<button class="header__action" id="refreshBtn" aria-label="새로고침"></button>
</header>
<!-- Main -->
<main class="main">
<div class="section">
<!-- Search Bar -->
<div class="search-bar">
<span class="search-bar__icon">🔍</span>
<input
type="text"
class="search-bar__input"
id="searchInput"
placeholder="이벤트 제목으로 검색"
autocomplete="off"
/>
</div>
<!-- Filter Tabs -->
<div class="filter-tabs">
<button class="filter-tab filter-tab--active" data-filter="all">
전체 <span id="countAll">0</span>
</button>
<button class="filter-tab" data-filter="active">
진행중 <span id="countActive">0</span>
</button>
<button class="filter-tab" data-filter="scheduled">
예정 <span id="countScheduled">0</span>
</button>
<button class="filter-tab" data-filter="ended">
종료 <span id="countEnded">0</span>
</button>
</div>
<!-- Event List -->
<div class="event-list" id="eventList">
<!-- Events will be dynamically inserted -->
</div>
<!-- Empty State -->
<div class="empty-state" id="emptyState" style="display: none;">
<div class="empty-state__icon">📋</div>
<h3 class="empty-state__title">진행 중인 이벤트가 없습니다</h3>
<p class="empty-state__description">
AI를 활용해 쉽고 빠르게<br/>
새로운 이벤트를 만들어보세요!
</p>
<button class="empty-state__action" id="emptyCreateBtn">
<span></span>
<span>이벤트 만들기</span>
</button>
</div>
</div>
</main>
<!-- FAB -->
<button class="fab" id="fabBtn" aria-label="새 이벤트 만들기">+</button>
<!-- Toast -->
<div class="toast" id="toast"></div>
<script>
// Utils
const $ = (selector) => document.querySelector(selector);
const $$ = (selector) => document.querySelectorAll(selector);
const addClass = (el, className) => el?.classList.add(className);
const removeClass = (el, className) => el?.classList.remove(className);
// App State
const AppState = {
events: [],
filteredEvents: [],
currentFilter: 'all',
searchQuery: '',
showToast(message, duration = 2000) {
const toast = $('#toast');
toast.textContent = message;
addClass(toast, 'toast--visible');
setTimeout(() => {
removeClass(toast, 'toast--visible');
}, duration);
}
};
// DOM Elements
const searchInput = $('#searchInput');
const filterTabs = $$('.filter-tab');
const eventList = $('#eventList');
const emptyState = $('#emptyState');
const countAll = $('#countAll');
const countActive = $('#countActive');
const countScheduled = $('#countScheduled');
const countEnded = $('#countEnded');
const refreshBtn = $('#refreshBtn');
const fabBtn = $('#fabBtn');
const emptyCreateBtn = $('#emptyCreateBtn');
const backBtn = $('.header__back');
// Generate Mock Events
function generateMockEvents() {
const now = new Date();
const events = [
{
id: 'event-001',
title: '신규 회원 가입 이벤트',
status: 'active',
startDate: new Date(now.getTime() - 15 * 24 * 60 * 60 * 1000),
endDate: new Date(now.getTime() + 15 * 24 * 60 * 60 * 1000),
participants: 1247,
winners: 156,
views: 5432,
channels: 4,
roi: 354.2
},
{
id: 'event-002',
title: '설 명절 특별 프로모션',
status: 'scheduled',
startDate: new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000),
endDate: new Date(now.getTime() + 21 * 24 * 60 * 60 * 1000),
participants: 0,
winners: 0,
views: 0,
channels: 3,
roi: 0
},
{
id: 'event-003',
title: '연말 감사 이벤트',
status: 'ended',
startDate: new Date(now.getTime() - 60 * 24 * 60 * 60 * 1000),
endDate: new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000),
participants: 892,
winners: 98,
views: 3421,
channels: 3,
roi: 287.5
},
{
id: 'event-004',
title: '친구 추천 이벤트',
status: 'active',
startDate: new Date(now.getTime() - 5 * 24 * 60 * 60 * 1000),
endDate: new Date(now.getTime() + 25 * 24 * 60 * 60 * 1000),
participants: 543,
winners: 65,
views: 2134,
channels: 2,
roi: 198.3
}
];
return events;
}
// Format Date
function 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}`;
}
// Format Number
function formatNumber(num) {
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
}
// Get Status Badge
function getStatusBadge(status) {
const badges = {
active: { class: 'status-badge--active', text: '진행중' },
ended: { class: 'status-badge--ended', text: '종료' },
scheduled: { class: 'status-badge--scheduled', text: '예정' }
};
return badges[status] || badges.active;
}
// Render Events
function renderEvents() {
const events = AppState.filteredEvents;
if (events.length === 0) {
eventList.style.display = 'none';
emptyState.style.display = 'block';
return;
}
eventList.style.display = 'flex';
emptyState.style.display = 'none';
eventList.innerHTML = events.map(event => {
const badge = getStatusBadge(event.status);
return `
<div class="event-card" data-id="${event.id}">
<div class="event-card__header">
<h3 class="event-card__title">${event.title}</h3>
<div class="event-card__status">
<span class="status-badge ${badge.class}">${badge.text}</span>
</div>
</div>
<div class="event-card__period">
${formatDate(event.startDate)} - ${formatDate(event.endDate)}
</div>
<div class="event-card__stats">
<div class="event-stat">
<div class="event-stat__label">참여자</div>
<div class="event-stat__value">${formatNumber(event.participants)}</div>
</div>
<div class="event-stat">
<div class="event-stat__label">당첨자</div>
<div class="event-stat__value">${formatNumber(event.winners)}</div>
</div>
<div class="event-stat">
<div class="event-stat__label">조회수</div>
<div class="event-stat__value">${formatNumber(event.views)}</div>
</div>
</div>
<div class="event-card__actions">
<button class="action-btn" data-action="analytics" data-id="${event.id}">
📊 분석
</button>
<button class="action-btn" data-action="manage" data-id="${event.id}">
⚙️ 관리
</button>
${event.status === 'active' ? `
<button class="action-btn action-btn--primary" data-action="view" data-id="${event.id}">
👁 보기
</button>
` : ''}
</div>
</div>
`;
}).join('');
// Add event listeners
$$('[data-action]').forEach(btn => {
btn.addEventListener('click', handleAction);
});
}
// Handle Action
function handleAction(e) {
e.stopPropagation();
const action = e.currentTarget.dataset.action;
const eventId = e.currentTarget.dataset.id;
switch (action) {
case 'analytics':
window.location.href = '21-실시간대시보드.html';
break;
case 'manage':
window.location.href = '20-당첨자명단관리.html';
break;
case 'view':
window.location.href = '18-이벤트참여.html';
break;
}
}
// Apply Filters
function applyFilters() {
let filtered = [...AppState.events];
// Apply status filter
if (AppState.currentFilter !== 'all') {
filtered = filtered.filter(e => e.status === AppState.currentFilter);
}
// Apply search filter
if (AppState.searchQuery) {
const query = AppState.searchQuery.toLowerCase();
filtered = filtered.filter(e =>
e.title.toLowerCase().includes(query)
);
}
AppState.filteredEvents = filtered;
renderEvents();
}
// Update Counts
function updateCounts() {
const all = AppState.events.length;
const active = AppState.events.filter(e => e.status === 'active').length;
const scheduled = AppState.events.filter(e => e.status === 'scheduled').length;
const ended = AppState.events.filter(e => e.status === 'ended').length;
countAll.textContent = all;
countActive.textContent = active;
countScheduled.textContent = scheduled;
countEnded.textContent = ended;
}
// Handle Search
searchInput.addEventListener('input', (e) => {
AppState.searchQuery = e.target.value.trim();
applyFilters();
});
// Handle Filter Tabs
filterTabs.forEach(tab => {
tab.addEventListener('click', () => {
const filter = tab.dataset.filter;
filterTabs.forEach(t => removeClass(t, 'filter-tab--active'));
addClass(tab, 'filter-tab--active');
AppState.currentFilter = filter;
applyFilters();
});
});
// Handle Refresh
refreshBtn.addEventListener('click', () => {
init();
AppState.showToast('목록이 새로고침되었습니다');
});
// Handle FAB
fabBtn.addEventListener('click', () => {
window.location.href = '03-이벤트목적선택.html';
});
// Handle Empty Create
emptyCreateBtn?.addEventListener('click', () => {
window.location.href = '03-이벤트목적선택.html';
});
// Handle Back
backBtn.addEventListener('click', () => {
window.location.href = '21.5-홈.html';
});
// Initialize
function init() {
AppState.events = generateMockEvents();
updateCounts();
applyFilters();
}
// Start
init();
</script>
</body>
</html>

View File

@ -0,0 +1,727 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>마이페이지 - KT AI 이벤트 플랫폼</title>
<style>
/* CSS Reset */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
/* Design System Variables */
:root {
/* Colors */
--color-primary: #E31E24;
--color-primary-hover: #C71820;
--color-primary-light: #FF4D52;
--color-secondary: #0066FF;
--color-secondary-hover: #004DBF;
--color-secondary-light: #4D94FF;
--color-success: #00C853;
--color-warning: #FFA000;
--color-error: #D32F2F;
--color-info: #0288D1;
--color-gray-50: #F8F9FA;
--color-gray-100: #F1F3F5;
--color-gray-200: #E9ECEF;
--color-gray-300: #DEE2E6;
--color-gray-400: #CED4DA;
--color-gray-500: #ADB5BD;
--color-gray-600: #868E96;
--color-gray-700: #495057;
--color-gray-800: #343A40;
--color-gray-900: #212529;
--color-white: #FFFFFF;
--color-black: #000000;
/* Typography */
--font-family: 'Pretendard Variable', -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
/* Spacing (4px grid) */
--spacing-xs: 4px;
--spacing-s: 8px;
--spacing-m: 16px;
--spacing-l: 24px;
--spacing-xl: 32px;
--spacing-2xl: 48px;
/* Border Radius */
--radius-s: 4px;
--radius-m: 8px;
--radius-l: 12px;
--radius-xl: 16px;
--radius-full: 9999px;
/* Shadows */
--shadow-s: 0 1px 2px rgba(0, 0, 0, 0.05);
--shadow-m: 0 4px 6px rgba(0, 0, 0, 0.07);
--shadow-l: 0 10px 15px rgba(0, 0, 0, 0.1);
--shadow-xl: 0 20px 25px rgba(0, 0, 0, 0.15);
/* Transitions */
--transition-fast: 150ms ease-in-out;
--transition-base: 250ms ease-in-out;
--transition-slow: 350ms ease-in-out;
/* Z-index */
--z-header: 100;
--z-modal: 200;
--z-toast: 300;
}
/* Base Styles */
body {
font-family: var(--font-family);
font-size: 16px;
line-height: 1.5;
color: var(--color-gray-900);
background-color: var(--color-gray-50);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* Header */
.header {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 56px;
background-color: var(--color-white);
border-bottom: 1px solid var(--color-gray-200);
display: flex;
align-items: center;
padding: 0 var(--spacing-m);
z-index: var(--z-header);
}
.header__back {
width: 44px;
height: 44px;
display: flex;
align-items: center;
justify-content: center;
background: none;
border: none;
cursor: pointer;
color: var(--color-gray-900);
font-size: 24px;
transition: color var(--transition-fast);
}
.header__back:hover {
color: var(--color-primary);
}
.header__title {
flex: 1;
font-size: 18px;
font-weight: 600;
text-align: center;
margin: 0 var(--spacing-m);
}
.header__action {
width: 44px;
height: 44px;
display: flex;
align-items: center;
justify-content: center;
background: none;
border: none;
cursor: pointer;
color: var(--color-gray-900);
font-size: 20px;
transition: color var(--transition-fast);
}
.header__action:hover {
color: var(--color-primary);
}
/* Main */
.main {
padding-top: 56px;
padding-bottom: var(--spacing-l);
min-height: 100vh;
}
/* Section */
.section {
padding: var(--spacing-l) var(--spacing-m);
}
/* Profile Card */
.profile-card {
background-color: var(--color-white);
border-radius: var(--radius-l);
padding: var(--spacing-l);
margin-bottom: var(--spacing-l);
box-shadow: var(--shadow-s);
}
.profile-card__header {
display: flex;
align-items: center;
gap: var(--spacing-m);
margin-bottom: var(--spacing-m);
}
.profile-card__avatar {
width: 64px;
height: 64px;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, var(--color-primary), var(--color-primary-light));
color: var(--color-white);
border-radius: var(--radius-full);
font-size: 28px;
font-weight: 700;
}
.profile-card__info {
flex: 1;
}
.profile-card__name {
font-size: 18px;
font-weight: 600;
color: var(--color-gray-900);
margin-bottom: var(--spacing-xs);
}
.profile-card__email {
font-size: 14px;
color: var(--color-gray-600);
}
.profile-card__stats {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: var(--spacing-m);
padding-top: var(--spacing-m);
border-top: 1px solid var(--color-gray-200);
}
.profile-stat {
text-align: center;
}
.profile-stat__value {
font-size: 20px;
font-weight: 700;
color: var(--color-gray-900);
margin-bottom: var(--spacing-xs);
}
.profile-stat__label {
font-size: 12px;
color: var(--color-gray-600);
}
/* Store Info */
.store-info {
background-color: var(--color-white);
border-radius: var(--radius-l);
padding: var(--spacing-l);
margin-bottom: var(--spacing-l);
box-shadow: var(--shadow-s);
}
.store-info__title {
font-size: 16px;
font-weight: 600;
color: var(--color-gray-900);
margin-bottom: var(--spacing-m);
}
.store-info__item {
display: flex;
justify-content: space-between;
align-items: center;
padding: var(--spacing-m) 0;
border-bottom: 1px solid var(--color-gray-100);
}
.store-info__item:last-child {
border-bottom: none;
}
.store-info__label {
font-size: 14px;
color: var(--color-gray-600);
}
.store-info__value {
font-size: 14px;
color: var(--color-gray-900);
font-weight: 500;
}
/* Menu Section */
.menu-section {
background-color: var(--color-white);
border-radius: var(--radius-l);
padding: var(--spacing-s) 0;
margin-bottom: var(--spacing-l);
box-shadow: var(--shadow-s);
}
.menu-section__title {
font-size: 14px;
font-weight: 600;
color: var(--color-gray-600);
padding: var(--spacing-s) var(--spacing-l);
margin-bottom: var(--spacing-s);
}
.menu-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--spacing-m) var(--spacing-l);
cursor: pointer;
transition: background-color var(--transition-fast);
}
.menu-item:hover {
background-color: var(--color-gray-50);
}
.menu-item__left {
display: flex;
align-items: center;
gap: var(--spacing-m);
}
.menu-item__icon {
font-size: 20px;
width: 24px;
text-align: center;
}
.menu-item__label {
font-size: 15px;
color: var(--color-gray-900);
}
.menu-item__arrow {
font-size: 16px;
color: var(--color-gray-400);
}
.menu-item--danger {
color: var(--color-error);
}
.menu-item--danger .menu-item__label {
color: var(--color-error);
}
/* Version Info */
.version-info {
text-align: center;
padding: var(--spacing-l);
color: var(--color-gray-500);
font-size: 13px;
}
/* Toast */
.toast {
position: fixed;
bottom: var(--spacing-l);
left: 50%;
transform: translateX(-50%) translateY(100px);
background-color: var(--color-gray-800);
color: var(--color-white);
padding: var(--spacing-m) var(--spacing-l);
border-radius: var(--radius-m);
font-size: 14px;
font-weight: 500;
box-shadow: var(--shadow-xl);
z-index: var(--z-toast);
opacity: 0;
transition: all var(--transition-base);
pointer-events: none;
}
.toast--visible {
transform: translateX(-50%) translateY(0);
opacity: 1;
}
/* Responsive */
@media (min-width: 768px) {
.header {
padding: 0 var(--spacing-l);
}
.section {
max-width: 768px;
margin: 0 auto;
padding: var(--spacing-xl) var(--spacing-l);
}
}
@media (min-width: 1024px) {
.section {
max-width: 1024px;
}
}
</style>
</head>
<body>
<!-- Header -->
<header class="header">
<button class="header__back" aria-label="뒤로 가기"></button>
<h1 class="header__title">마이페이지</h1>
<button class="header__action" id="settingsBtn" aria-label="설정">⚙️</button>
</header>
<!-- Main -->
<main class="main">
<div class="section">
<!-- Profile Card -->
<div class="profile-card">
<div class="profile-card__header">
<div class="profile-card__avatar" id="avatar"></div>
<div class="profile-card__info">
<div class="profile-card__name" id="userName">사장님</div>
<div class="profile-card__email" id="userEmail">example@email.com</div>
</div>
</div>
<div class="profile-card__stats">
<div class="profile-stat">
<div class="profile-stat__value" id="totalEvents">0</div>
<div class="profile-stat__label">총 이벤트</div>
</div>
<div class="profile-stat">
<div class="profile-stat__value" id="totalParticipants">0</div>
<div class="profile-stat__label">총 참여자</div>
</div>
<div class="profile-stat">
<div class="profile-stat__value" id="avgROI">0%</div>
<div class="profile-stat__label">평균 ROI</div>
</div>
</div>
</div>
<!-- Store Info -->
<div class="store-info">
<h2 class="store-info__title">매장 정보</h2>
<div class="store-info__item">
<div class="store-info__label">상호명</div>
<div class="store-info__value" id="businessName">-</div>
</div>
<div class="store-info__item">
<div class="store-info__label">업종</div>
<div class="store-info__value" id="businessType">-</div>
</div>
<div class="store-info__item">
<div class="store-info__label">주소</div>
<div class="store-info__value" id="address">-</div>
</div>
<div class="store-info__item">
<div class="store-info__label">전화번호</div>
<div class="store-info__value" id="phone">-</div>
</div>
</div>
<!-- Menu Section: Account -->
<div class="menu-section">
<h3 class="menu-section__title">계정</h3>
<div class="menu-item" id="editProfileBtn">
<div class="menu-item__left">
<span class="menu-item__icon">👤</span>
<span class="menu-item__label">프로필 수정</span>
</div>
<span class="menu-item__arrow"></span>
</div>
<div class="menu-item" id="editStoreBtn">
<div class="menu-item__left">
<span class="menu-item__icon">🏪</span>
<span class="menu-item__label">매장 정보 수정</span>
</div>
<span class="menu-item__arrow"></span>
</div>
<div class="menu-item" id="changePasswordBtn">
<div class="menu-item__left">
<span class="menu-item__icon">🔒</span>
<span class="menu-item__label">비밀번호 변경</span>
</div>
<span class="menu-item__arrow"></span>
</div>
</div>
<!-- Menu Section: Preferences -->
<div class="menu-section">
<h3 class="menu-section__title">설정</h3>
<div class="menu-item" id="notificationBtn">
<div class="menu-item__left">
<span class="menu-item__icon">🔔</span>
<span class="menu-item__label">알림 설정</span>
</div>
<span class="menu-item__arrow"></span>
</div>
<div class="menu-item" id="languageBtn">
<div class="menu-item__left">
<span class="menu-item__icon">🌐</span>
<span class="menu-item__label">언어 설정</span>
</div>
<span class="menu-item__arrow"></span>
</div>
</div>
<!-- Menu Section: Support -->
<div class="menu-section">
<h3 class="menu-section__title">고객 지원</h3>
<div class="menu-item" id="helpBtn">
<div class="menu-item__left">
<span class="menu-item__icon"></span>
<span class="menu-item__label">도움말</span>
</div>
<span class="menu-item__arrow"></span>
</div>
<div class="menu-item" id="faqBtn">
<div class="menu-item__left">
<span class="menu-item__icon">📚</span>
<span class="menu-item__label">자주 묻는 질문</span>
</div>
<span class="menu-item__arrow"></span>
</div>
<div class="menu-item" id="contactBtn">
<div class="menu-item__left">
<span class="menu-item__icon">📧</span>
<span class="menu-item__label">고객센터 문의</span>
</div>
<span class="menu-item__arrow"></span>
</div>
</div>
<!-- Menu Section: Legal -->
<div class="menu-section">
<h3 class="menu-section__title">약관 및 정책</h3>
<div class="menu-item" id="termsBtn">
<div class="menu-item__left">
<span class="menu-item__icon">📄</span>
<span class="menu-item__label">이용약관</span>
</div>
<span class="menu-item__arrow"></span>
</div>
<div class="menu-item" id="privacyBtn">
<div class="menu-item__left">
<span class="menu-item__icon">🔐</span>
<span class="menu-item__label">개인정보 처리방침</span>
</div>
<span class="menu-item__arrow"></span>
</div>
</div>
<!-- Menu Section: Account Actions -->
<div class="menu-section">
<div class="menu-item menu-item--danger" id="logoutBtn">
<div class="menu-item__left">
<span class="menu-item__icon">🚪</span>
<span class="menu-item__label">로그아웃</span>
</div>
</div>
</div>
<!-- Version Info -->
<div class="version-info">
KT AI 이벤트 플랫폼 v1.0.0<br>
© 2025 KT Corporation. All rights reserved.
</div>
</div>
</main>
<!-- Toast -->
<div class="toast" id="toast"></div>
<script>
// Utils
const $ = (selector) => document.querySelector(selector);
const addClass = (el, className) => el?.classList.add(className);
const removeClass = (el, className) => el?.classList.remove(className);
// LocalStorage wrapper
const storage = {
get: (key) => {
try {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : null;
} catch (e) {
console.error('Storage get error:', e);
return null;
}
},
clear: () => {
try {
localStorage.clear();
} catch (e) {
console.error('Storage clear error:', e);
}
}
};
// App State
const AppState = {
showToast(message, duration = 2000) {
const toast = $('#toast');
toast.textContent = message;
addClass(toast, 'toast--visible');
setTimeout(() => {
removeClass(toast, 'toast--visible');
}, duration);
}
};
// DOM Elements
const avatar = $('#avatar');
const userName = $('#userName');
const userEmail = $('#userEmail');
const totalEvents = $('#totalEvents');
const totalParticipants = $('#totalParticipants');
const avgROI = $('#avgROI');
const businessName = $('#businessName');
const businessType = $('#businessType');
const address = $('#address');
const phone = $('#phone');
const settingsBtn = $('#settingsBtn');
const editProfileBtn = $('#editProfileBtn');
const editStoreBtn = $('#editStoreBtn');
const changePasswordBtn = $('#changePasswordBtn');
const notificationBtn = $('#notificationBtn');
const languageBtn = $('#languageBtn');
const helpBtn = $('#helpBtn');
const faqBtn = $('#faqBtn');
const contactBtn = $('#contactBtn');
const termsBtn = $('#termsBtn');
const privacyBtn = $('#privacyBtn');
const logoutBtn = $('#logoutBtn');
const backBtn = $('.header__back');
// Format Number
function formatNumber(num) {
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
}
// Load User Data
function loadUserData() {
// Load signup data
const signupData = storage.get('signupData');
if (signupData) {
userEmail.textContent = signupData.email || 'example@email.com';
}
// Load store data
const storeData = storage.get('storeData');
if (storeData) {
const name = storeData.businessName || '사장님';
userName.textContent = name;
avatar.textContent = name.charAt(0);
businessName.textContent = storeData.businessName || '-';
businessType.textContent = storeData.businessType || '-';
address.textContent = storeData.address || '-';
phone.textContent = storeData.phone || '-';
}
// Mock statistics
totalEvents.textContent = '4';
totalParticipants.textContent = formatNumber(2682);
avgROI.textContent = '285.2%';
}
// Handle Settings
settingsBtn.addEventListener('click', () => {
AppState.showToast('설정 페이지로 이동합니다');
});
// Handle Edit Profile
editProfileBtn.addEventListener('click', () => {
AppState.showToast('프로필 수정 페이지로 이동합니다');
});
// Handle Edit Store
editStoreBtn.addEventListener('click', () => {
window.location.href = '02-매장정보등록.html';
});
// Handle Change Password
changePasswordBtn.addEventListener('click', () => {
AppState.showToast('비밀번호 변경 페이지로 이동합니다');
});
// Handle Notification
notificationBtn.addEventListener('click', () => {
AppState.showToast('알림 설정 페이지로 이동합니다');
});
// Handle Language
languageBtn.addEventListener('click', () => {
AppState.showToast('언어 설정 페이지로 이동합니다');
});
// Handle Help
helpBtn.addEventListener('click', () => {
AppState.showToast('도움말 페이지로 이동합니다');
});
// Handle FAQ
faqBtn.addEventListener('click', () => {
AppState.showToast('FAQ 페이지로 이동합니다');
});
// Handle Contact
contactBtn.addEventListener('click', () => {
AppState.showToast('고객센터 문의 페이지로 이동합니다');
});
// Handle Terms
termsBtn.addEventListener('click', () => {
AppState.showToast('이용약관 페이지로 이동합니다');
});
// Handle Privacy
privacyBtn.addEventListener('click', () => {
AppState.showToast('개인정보 처리방침 페이지로 이동합니다');
});
// Handle Logout
logoutBtn.addEventListener('click', () => {
if (confirm('로그아웃 하시겠습니까?')) {
storage.clear();
AppState.showToast('로그아웃되었습니다');
setTimeout(() => {
window.location.href = '00-로그인.html';
}, 1000);
}
});
// Handle Back
backBtn.addEventListener('click', () => {
window.location.href = '21.5-홈.html';
});
// Initialize
function init() {
loadUserData();
}
// Start
init();
</script>
</body>
</html>

View File

@ -0,0 +1,246 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="KT AI 기반 소상공인 이벤트 자동 생성 서비스 - 로그인">
<title>로그인 - KT 이벤트 마케팅</title>
<!-- Styles -->
<link rel="stylesheet" href="css/common.css">
<!-- Fonts -->
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/static/pretendard.min.css">
</head>
<body>
<!-- Skip Link -->
<a href="#main-content" class="skip-link">본문으로 건너뛰기</a>
<!-- App Wrapper -->
<div class="app-wrapper">
<!-- Main Content -->
<main id="main-content" class="app-content">
<div class="container" style="max-width: 480px; margin: 0 auto; padding: 60px 20px 40px;">
<!-- Logo & Title -->
<div style="text-align: center; margin-bottom: 48px;">
<div style="width: 80px; height: 80px; background: linear-gradient(135deg, var(--color-primary-main), #C71F24); border-radius: 20px; display: flex; align-items: center; justify-content: center; margin: 0 auto 24px;">
<span class="material-icons" style="font-size: 48px; color: white;">campaign</span>
</div>
<h1 class="h1" style="margin-bottom: 8px;">KT 이벤트 마케팅</h1>
<p class="body-m text-muted">AI로 쉽고 빠르게, 효과적인 이벤트</p>
</div>
<!-- Login Form -->
<form id="loginForm" novalidate>
<!-- Email -->
<div class="form-group">
<label for="email" class="form-label required">이메일</label>
<input
type="email"
id="email"
name="email"
class="form-input"
placeholder="example@email.com"
required
autocomplete="email"
>
</div>
<!-- Password -->
<div class="form-group">
<label for="password" class="form-label required">비밀번호</label>
<input
type="password"
id="password"
name="password"
class="form-input"
placeholder="비밀번호를 입력하세요"
required
minlength="8"
autocomplete="current-password"
>
</div>
<!-- Remember Me & Find Password -->
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px;">
<div class="form-check">
<input
type="checkbox"
id="remember"
name="remember"
>
<label for="remember" style="flex: 1; cursor: pointer;">
<span class="body-m">로그인 상태 유지</span>
</label>
</div>
<button type="button" class="btn btn-text btn-sm" style="color: var(--color-gray-600);">
비밀번호 찾기
</button>
</div>
<!-- Submit Button -->
<button
type="submit"
id="loginBtn"
class="btn btn-primary btn-lg btn-block"
style="margin-bottom: 16px;"
>
로그인
</button>
<!-- KT Auth Button -->
<button
type="button"
id="ktAuthBtn"
class="btn btn-secondary btn-lg btn-block"
style="margin-bottom: 32px; display: flex; align-items: center; justify-content: center; gap: 8px;"
>
<span class="material-icons" style="font-size: 20px;">smartphone</span>
KT 통합 인증으로 로그인
</button>
</form>
<!-- Sign Up Link -->
<div style="text-align: center; padding: 24px 0; border-top: 1px solid var(--color-gray-200);">
<p class="body-m text-muted" style="margin-bottom: 12px;">아직 계정이 없으신가요?</p>
<button
type="button"
class="btn btn-text"
style="color: var(--color-primary-main); font-weight: 600;"
onclick="window.location.href='01-회원가입.html'"
>
회원가입하기
</button>
</div>
<!-- Footer Info -->
<div style="text-align: center; margin-top: 40px;">
<p class="body-s text-muted">
로그인 시
<a href="#" style="text-decoration: underline; color: var(--color-gray-600);">서비스 이용약관</a>
<a href="#" style="text-decoration: underline; color: var(--color-gray-600);">개인정보처리방침</a>에 동의하게 됩니다
</p>
</div>
</div>
</main>
</div>
<!-- Scripts -->
<script src="js/common.js"></script>
<script>
(function() {
'use strict';
const form = document.getElementById('loginForm');
const emailInput = document.getElementById('email');
const passwordInput = document.getElementById('password');
const loginBtn = document.getElementById('loginBtn');
const ktAuthBtn = document.getElementById('ktAuthBtn');
// 폼 검증 및 버튼 활성화
function checkFormValidity() {
const isEmailValid = FormValidator.isValidEmail(emailInput.value);
const isPasswordValid = passwordInput.value.length >= 8;
loginBtn.disabled = !(isEmailValid && isPasswordValid);
}
// 입력 필드 이벤트 리스너
[emailInput, passwordInput].forEach(input => {
input.addEventListener('input', checkFormValidity);
input.addEventListener('change', checkFormValidity);
});
// 이메일 로그인
form.addEventListener('submit', function(e) {
e.preventDefault();
const email = emailInput.value.trim();
const password = passwordInput.value;
if (!FormValidator.isValidEmail(email)) {
Toast.error('올바른 이메일 형식을 입력하세요.');
return;
}
if (password.length < 8) {
Toast.error('비밀번호는 8자 이상이어야 합니다.');
return;
}
Loading.show('로그인 중...');
// 시뮬레이션: 실제로는 서버 API 호출
setTimeout(() => {
Loading.hide();
// 사용자 정보 저장
window.AppState.user = {
id: Utils.generateId(),
email: email,
name: email.split('@')[0],
authType: 'email',
loginAt: new Date().toISOString()
};
window.AppState.save();
Toast.success('로그인되었습니다.');
// 홈 화면으로 이동
setTimeout(() => {
window.location.href = '21.5-홈.html';
}, 500);
}, 1500);
});
// KT 통합 인증 로그인
ktAuthBtn.addEventListener('click', function() {
Loading.show('KT 통합 인증 연결 중...');
// 시뮬레이션: 실제로는 KT 인증 API 호출
setTimeout(() => {
Loading.hide();
// 사용자 정보 저장
window.AppState.user = {
id: Utils.generateId(),
phone: '010-1234-5678',
name: '홍길동',
authType: 'kt',
loginAt: new Date().toISOString()
};
window.AppState.save();
Toast.success('KT 통합 인증으로 로그인되었습니다.');
// 홈 화면으로 이동
setTimeout(() => {
window.location.href = '21.5-홈.html';
}, 500);
}, 2000);
});
// 초기 상태 확인
checkFormValidity();
console.log('로그인 페이지 로드 완료');
})();
</script>
<style>
/* 로그인 페이지 특화 스타일 */
body {
background: linear-gradient(180deg, #FFF5F5 0%, #FFFFFF 50%);
min-height: 100vh;
}
.app-wrapper {
background: transparent;
}
.btn-block {
width: 100%;
}
</style>
</body>
</html>

View File

@ -0,0 +1,324 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="KT AI 기반 소상공인 이벤트 자동 생성 서비스 - 회원가입">
<title>회원가입 - KT 이벤트 마케팅</title>
<!-- Styles -->
<link rel="stylesheet" href="css/common.css">
<!-- Fonts -->
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/static/pretendard.min.css">
</head>
<body>
<!-- Skip Link -->
<a href="#main-content" class="skip-link">본문으로 건너뛰기</a>
<!-- App Wrapper -->
<div class="app-wrapper">
<!-- Top App Bar -->
<header class="app-bar">
<button class="app-bar__back" aria-label="뒤로가기">
<span class="material-icons">arrow_back</span>
</button>
<h1 class="app-bar__title">회원가입</h1>
</header>
<!-- Main Content -->
<main id="main-content" class="app-content">
<div class="container" style="max-width: 500px; margin: 0 auto; padding-top: 32px;">
<!-- Welcome Section -->
<div style="text-align: center; margin-bottom: 48px;">
<h2 class="display" style="margin-bottom: 8px;">KT 이벤트 마케팅</h2>
<p class="body-l text-muted">환영합니다 🎉</p>
</div>
<!-- Registration Form -->
<form id="registrationForm" novalidate>
<!-- 이름 -->
<div class="form-group">
<label for="name" class="form-label required">이름</label>
<input
type="text"
id="name"
name="name"
class="form-input"
placeholder="홍길동"
required
minlength="2"
autocomplete="name"
>
</div>
<!-- 전화번호 -->
<div class="form-group">
<label for="phone" class="form-label required">전화번호</label>
<input
type="tel"
id="phone"
name="phone"
class="form-input"
placeholder="010-1234-5678"
required
maxlength="13"
autocomplete="tel"
>
</div>
<!-- 이메일 -->
<div class="form-group">
<label for="email" class="form-label required">이메일</label>
<input
type="email"
id="email"
name="email"
class="form-input"
placeholder="example@domain.com"
required
autocomplete="email"
>
</div>
<!-- KT 본인 인증 (선택) -->
<div class="card" style="margin-bottom: 24px; padding: 16px;">
<div class="form-check">
<input
type="checkbox"
id="ktAuth"
name="ktAuth"
>
<label for="ktAuth" style="flex: 1; cursor: pointer;">
<div class="body-m" style="font-weight: 600; margin-bottom: 4px;">
KT 본인 인증 (선택)
</div>
<div class="body-s text-muted">
인증 시 추가 혜택이 제공됩니다
</div>
</label>
</div>
</div>
<!-- 개인정보 수집 동의 (필수) -->
<div class="form-group">
<div class="form-check">
<input
type="checkbox"
id="privacyAgree"
name="privacyAgree"
required
>
<label for="privacyAgree" style="flex: 1; cursor: pointer;">
<span class="body-m">개인정보 수집 및 이용 동의 (필수)</span>
<button type="button" class="btn-text btn-sm" style="padding: 0; margin-left: 8px;" onclick="showPrivacyModal()">
자세히보기
</button>
</label>
</div>
</div>
<!-- Submit Button -->
<button
type="submit"
id="submitBtn"
class="btn btn-primary btn-lg btn-block"
style="margin-top: 32px;"
disabled
>
다음 단계
</button>
</form>
<!-- Additional Info -->
<div style="text-align: center; margin-top: 24px;">
<p class="body-s text-muted">
이미 계정이 있으신가요?
<a href="#" class="text-primary" style="text-decoration: none; font-weight: 600;">로그인</a>
</p>
</div>
</div>
</main>
</div>
<!-- Scripts -->
<script src="js/common.js"></script>
<script>
(function() {
'use strict';
const form = document.getElementById('registrationForm');
const submitBtn = document.getElementById('submitBtn');
const nameInput = document.getElementById('name');
const phoneInput = document.getElementById('phone');
const emailInput = document.getElementById('email');
const ktAuthCheckbox = document.getElementById('ktAuth');
const privacyCheckbox = document.getElementById('privacyAgree');
// 폼 검증 및 버튼 활성화 체크
function checkFormValidity() {
const isNameValid = nameInput.value.trim().length >= 2;
const isPhoneValid = FormValidator.isValidPhone(phoneInput.value);
const isEmailValid = FormValidator.isValidEmail(emailInput.value);
const isPrivacyAgreed = privacyCheckbox.checked;
const isFormValid = isNameValid && isPhoneValid && isEmailValid && isPrivacyAgreed;
submitBtn.disabled = !isFormValid;
}
// 입력 필드 이벤트 리스너
[nameInput, phoneInput, emailInput].forEach(input => {
input.addEventListener('input', checkFormValidity);
input.addEventListener('blur', function() {
if (this.value) {
validateField(this);
}
});
});
privacyCheckbox.addEventListener('change', checkFormValidity);
// 개별 필드 검증
function validateField(input) {
let isValid = true;
let errorMessage = '';
switch(input.id) {
case 'name':
if (input.value.trim().length < 2) {
isValid = false;
errorMessage = '이름은 2자 이상 입력해주세요.';
}
break;
case 'phone':
if (!FormValidator.isValidPhone(input.value)) {
isValid = false;
errorMessage = '올바른 전화번호 형식을 입력하세요. (010-XXXX-XXXX)';
}
break;
case 'email':
if (!FormValidator.isValidEmail(input.value)) {
isValid = false;
errorMessage = '올바른 이메일 형식을 입력하세요.';
}
break;
}
if (isValid) {
FormValidator.clearError(input);
} else {
FormValidator.showError(input, errorMessage);
}
return isValid;
}
// KT 본인 인증 체크박스 변경 시
ktAuthCheckbox.addEventListener('change', function() {
if (this.checked) {
// KT 인증 시뮬레이션
simulateKTAuth();
}
});
// KT 인증 시뮬레이션
function simulateKTAuth() {
// 실제로는 KT 인증 시스템 연동
Loading.show('KT 본인 인증 중...');
setTimeout(() => {
Loading.hide();
Toast.success('KT 본인 인증이 완료되었습니다. 추가 혜택이 적용됩니다!');
}, 2000);
}
// 폼 제출
form.addEventListener('submit', function(e) {
e.preventDefault();
// 모든 필드 검증
const isNameValid = validateField(nameInput);
const isPhoneValid = validateField(phoneInput);
const isEmailValid = validateField(emailInput);
if (!isNameValid || !isPhoneValid || !isEmailValid) {
Toast.error('입력 내용을 확인해주세요.');
return;
}
if (!privacyCheckbox.checked) {
Toast.error('개인정보 수집 및 이용에 동의해주세요.');
return;
}
// 중복 가입 체크 시뮬레이션
Loading.show('가입 정보 확인 중...');
setTimeout(() => {
Loading.hide();
// 사용자 정보 저장
const userData = {
id: Utils.generateId(),
name: nameInput.value.trim(),
phone: phoneInput.value,
email: emailInput.value,
ktAuth: ktAuthCheckbox.checked,
createdAt: new Date().toISOString()
};
window.AppState.user = userData;
window.AppState.save();
// 성공 메시지
Toast.success('회원가입이 완료되었습니다!');
// 다음 화면으로 이동 (매장정보등록)
setTimeout(() => {
window.location.href = '02-매장정보등록.html';
}, 1000);
}, 1500);
});
// 개인정보 처리방침 모달
window.showPrivacyModal = function() {
Modal.alert(
'개인정보 수집 및 이용 동의',
`
<div class="body-m" style="max-height: 300px; overflow-y: auto;">
<h3 class="h3" style="margin-bottom: 12px;">수집하는 개인정보 항목</h3>
<ul style="margin-left: 20px; margin-bottom: 16px;">
<li>이름</li>
<li>전화번호</li>
<li>이메일 주소</li>
</ul>
<h3 class="h3" style="margin-bottom: 12px;">개인정보의 수집 및 이용 목적</h3>
<ul style="margin-left: 20px; margin-bottom: 16px;">
<li>회원 가입 및 관리</li>
<li>서비스 제공 및 계약 이행</li>
<li>고객 문의 응대</li>
</ul>
<h3 class="h3" style="margin-bottom: 12px;">개인정보의 보유 및 이용 기간</h3>
<p style="margin-bottom: 16px;">
회원 탈퇴 시까지 보유하며, 관계 법령에 따라 일정 기간 보관할 수 있습니다.
</p>
<p class="body-s text-muted">
귀하는 개인정보 수집 및 이용에 동의하지 않을 권리가 있으나, 동의하지 않을 경우 서비스 이용이 제한될 수 있습니다.
</p>
</div>
`
);
};
console.log('회원가입 페이지 로드 완료');
})();
</script>
</body>
</html>

View File

@ -0,0 +1,843 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="KT AI 기반 소상공인 이벤트 자동 생성 서비스 - 매장정보등록">
<title>매장정보등록 - KT 이벤트 마케팅</title>
<!-- Styles -->
<link rel="stylesheet" href="css/common.css">
<!-- Fonts -->
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/static/pretendard.min.css">
</head>
<body>
<!-- Skip Link -->
<a href="#main-content" class="skip-link">본문으로 건너뛰기</a>
<!-- App Wrapper -->
<div class="app-wrapper">
<!-- Top App Bar -->
<header class="app-bar">
<button class="app-bar__back" aria-label="뒤로가기">
<span class="material-icons">arrow_back</span>
</button>
<h1 class="app-bar__title">매장정보등록</h1>
</header>
<!-- Main Content -->
<main id="main-content" class="app-content">
<div class="container" style="max-width: 600px; margin: 0 auto; padding-top: 24px;">
<!-- Progress Indicator -->
<div style="margin-bottom: 32px;">
<div class="body-s text-muted" style="margin-bottom: 8px;">진행상황</div>
<div style="display: flex; gap: 4px; align-items: center;">
<div style="flex: 1; height: 4px; background: var(--color-primary-main); border-radius: 2px;"></div>
<div style="flex: 1; height: 4px; background: var(--color-primary-main); border-radius: 2px;"></div>
<span class="body-s" style="margin-left: 8px; font-weight: 600;">2/2</span>
</div>
</div>
<!-- Registration Form -->
<form id="storeForm" novalidate>
<!-- Required Information -->
<section style="margin-bottom: 32px;">
<h2 class="h3" style="margin-bottom: 20px;">필수 정보</h2>
<!-- Store Name -->
<div class="form-group">
<label for="storeName" class="form-label required">매장명</label>
<input
type="text"
id="storeName"
name="storeName"
class="form-input"
placeholder="왕갈비통닭 수원점"
required
minlength="2"
autocomplete="organization"
>
</div>
<!-- Business Type -->
<div class="form-group">
<label for="businessType" class="form-label required">업종</label>
<select
id="businessType"
name="businessType"
class="form-input"
required
>
<option value="">업종을 선택하세요</option>
<option value="restaurant">음식점</option>
<option value="cafe">카페/디저트</option>
<option value="retail">소매업</option>
<option value="beauty">미용/뷰티</option>
<option value="education">교육/학원</option>
<option value="service">서비스업</option>
<option value="healthcare">의료/건강</option>
<option value="other">기타</option>
</select>
</div>
<!-- Address Search -->
<div class="form-group">
<label for="address" class="form-label required">주소</label>
<div style="display: flex; gap: 8px;">
<input
type="text"
id="address"
name="address"
class="form-input"
placeholder="주소를 검색하세요"
required
readonly
style="flex: 1; cursor: pointer;"
>
<button
type="button"
class="btn btn-secondary"
onclick="searchAddress()"
aria-label="주소 검색"
>
<span class="material-icons" style="font-size: 20px;">search</span>
</button>
</div>
<input
type="text"
id="addressDetail"
name="addressDetail"
class="form-input"
placeholder="상세주소 (선택)"
style="margin-top: 8px;"
autocomplete="address-line2"
>
</div>
<!-- Business Hours -->
<div class="form-group">
<label class="form-label required">영업시간</label>
<div id="businessHours">
<!-- Business hours will be dynamically added here -->
</div>
<div class="form-check" style="margin-top: 12px;">
<input
type="checkbox"
id="sameHours"
name="sameHours"
checked
>
<label for="sameHours" style="flex: 1; cursor: pointer;">
<span class="body-m">모든 요일 동일하게 적용</span>
</label>
</div>
</div>
<!-- Business Number -->
<div class="form-group">
<label for="businessNumber" class="form-label required">사업자번호</label>
<div style="display: flex; gap: 8px; align-items: flex-start;">
<input
type="text"
id="businessNumber"
name="businessNumber"
class="form-input"
placeholder="숫자만 입력하세요 (예: 1234567890)"
required
maxlength="12"
inputmode="numeric"
style="flex: 1;"
autocomplete="off"
>
<button
type="button"
id="verifyBtn"
class="btn btn-secondary"
onclick="verifyBusinessNumber()"
disabled
>
검증하기
</button>
</div>
<div class="body-s text-muted" style="margin-top: 4px;">
숫자만 입력하시면 자동으로 형식이 맞춰집니다
</div>
<div id="verifyResult" style="margin-top: 8px;"></div>
</div>
</section>
<!-- Optional Information -->
<section style="margin-bottom: 32px;">
<h2 class="h3" style="margin-bottom: 8px;">선택 정보</h2>
<p class="body-s text-muted" style="margin-bottom: 20px;">
이벤트 맞춤화를 위해 추가 정보를 입력해주세요
</p>
<!-- Menu Items -->
<div class="form-group">
<label class="form-label">메뉴/상품 (최대 10개)</label>
<div id="menuList" style="margin-bottom: 12px;">
<!-- Menu items will be dynamically added here -->
</div>
<button
type="button"
class="btn btn-text btn-sm"
onclick="addMenuItem()"
id="addMenuBtn"
>
<span class="material-icons" style="font-size: 18px; margin-right: 4px;">add</span>
메뉴 추가
</button>
</div>
<!-- Store Features -->
<div class="form-group">
<label for="storeFeatures" class="form-label">매장 특징</label>
<textarea
id="storeFeatures"
name="storeFeatures"
class="form-input"
placeholder="매장의 특별한 점이나 강점을 알려주세요&#10;예) 30년 전통, 직접 만든 소스, 주차 가능"
rows="4"
maxlength="200"
style="resize: vertical;"
></textarea>
<div class="body-s text-muted" style="margin-top: 4px; text-align: right;">
<span id="featureCount">0</span>/200
</div>
</div>
<!-- Store Images -->
<div class="form-group">
<label class="form-label">대표 이미지 (최대 3장)</label>
<div id="imagePreview" style="display: flex; gap: 12px; flex-wrap: wrap;">
<label class="image-upload-box" for="imageUpload1">
<input
type="file"
id="imageUpload1"
name="storeImages"
accept="image/*"
style="display: none;"
onchange="handleImageUpload(event, 1)"
>
<span class="material-icons" style="font-size: 32px; color: var(--color-gray-400);">add_photo_alternate</span>
</label>
<label class="image-upload-box" for="imageUpload2" style="display: none;">
<input
type="file"
id="imageUpload2"
name="storeImages"
accept="image/*"
style="display: none;"
onchange="handleImageUpload(event, 2)"
>
<span class="material-icons" style="font-size: 32px; color: var(--color-gray-400);">add_photo_alternate</span>
</label>
<label class="image-upload-box" for="imageUpload3" style="display: none;">
<input
type="file"
id="imageUpload3"
name="storeImages"
accept="image/*"
style="display: none;"
onchange="handleImageUpload(event, 3)"
>
<span class="material-icons" style="font-size: 32px; color: var(--color-gray-400);">add_photo_alternate</span>
</label>
</div>
</div>
</section>
<!-- Submit Button -->
<button
type="submit"
id="submitBtn"
class="btn btn-primary btn-lg btn-block"
style="margin-bottom: 32px;"
disabled
>
등록 완료하기
</button>
</form>
</div>
</main>
</div>
<!-- Scripts -->
<script src="js/common.js"></script>
<script>
(function() {
'use strict';
const form = document.getElementById('storeForm');
const submitBtn = document.getElementById('submitBtn');
const verifyBtn = document.getElementById('verifyBtn');
const businessNumberInput = document.getElementById('businessNumber');
const storeNameInput = document.getElementById('storeName');
const businessTypeSelect = document.getElementById('businessType');
const addressInput = document.getElementById('address');
const featureCountSpan = document.getElementById('featureCount');
const storeFeaturesTextarea = document.getElementById('storeFeatures');
let menuItems = [];
let uploadedImages = [];
let isBusinessNumberVerified = false;
// 영업시간 초기화
function initBusinessHours() {
const days = ['월', '화', '수', '목', '금', '토', '일'];
const hoursContainer = document.getElementById('businessHours');
days.forEach((day, index) => {
const hourRow = document.createElement('div');
hourRow.className = 'business-hour-row';
hourRow.style.cssText = 'display: flex; gap: 8px; align-items: center; margin-bottom: 8px;';
hourRow.innerHTML = `
<span class="body-m" style="min-width: 24px;">${day}:</span>
<input
type="time"
name="openTime_${index}"
value="09:00"
class="form-input"
style="flex: 1; min-width: 0;"
required
>
<span class="body-m">~</span>
<input
type="time"
name="closeTime_${index}"
value="21:00"
class="form-input"
style="flex: 1; min-width: 0;"
required
>
`;
hoursContainer.appendChild(hourRow);
});
}
// 영업시간 동일 적용
document.getElementById('sameHours').addEventListener('change', function() {
const rows = document.querySelectorAll('.business-hour-row');
if (this.checked && rows.length > 0) {
const firstOpen = rows[0].querySelector('input[type="time"]:first-of-type').value;
const firstClose = rows[0].querySelector('input[type="time"]:last-of-type').value;
rows.forEach((row, index) => {
if (index > 0) {
row.querySelector('input[type="time"]:first-of-type').value = firstOpen;
row.querySelector('input[type="time"]:last-of-type').value = firstClose;
}
});
}
});
// 주소 검색 (시뮬레이션)
window.searchAddress = function() {
Modal.show({
title: '주소 검색',
body: `
<div class="form-group" style="margin-bottom: 16px;">
<input
type="text"
id="addressSearchInput"
class="form-input"
placeholder="도로명 또는 지번 주소 입력"
onkeyup="if(event.key==='Enter') searchAddressQuery()"
>
</div>
<div id="addressResults" class="body-s" style="color: var(--color-gray-600);">
주소를 입력하고 Enter를 눌러주세요
</div>
`,
confirmText: '닫기',
showCancel: false
});
setTimeout(() => {
document.getElementById('addressSearchInput').focus();
}, 100);
};
window.searchAddressQuery = function() {
const query = document.getElementById('addressSearchInput').value;
if (!query) return;
const results = document.getElementById('addressResults');
results.innerHTML = '<div class="body-s text-muted">검색 중...</div>';
// 시뮬레이션: 실제로는 카카오 주소 API 사용
setTimeout(() => {
results.innerHTML = `
<div class="card" style="padding: 12px; margin-bottom: 8px; cursor: pointer;" onclick="selectAddress('경기도 수원시 팔달구 인계동 1055-1')">
<div class="body-m" style="margin-bottom: 4px;">경기도 수원시 팔달구 인계동 1055-1</div>
<div class="body-s text-muted">(우) 16495</div>
</div>
<div class="card" style="padding: 12px; margin-bottom: 8px; cursor: pointer;" onclick="selectAddress('경기도 수원시 팔달구 권광로 181')">
<div class="body-m" style="margin-bottom: 4px;">경기도 수원시 팔달구 권광로 181</div>
<div class="body-s text-muted">(우) 16495</div>
</div>
`;
}, 500);
};
window.selectAddress = function(address) {
addressInput.value = address;
document.querySelector('.modal__close').click();
checkFormValidity();
};
// 사업자번호 검증
businessNumberInput.addEventListener('input', function() {
isBusinessNumberVerified = false;
verifyBtn.disabled = !FormValidator.isValidBusinessNumber(this.value);
document.getElementById('verifyResult').innerHTML = '';
checkFormValidity();
});
window.verifyBusinessNumber = function() {
const businessNumber = businessNumberInput.value;
if (!FormValidator.isValidBusinessNumber(businessNumber)) {
Toast.error('올바른 사업자번호 형식을 입력하세요.');
return;
}
Loading.show('사업자번호 검증 중...');
verifyBtn.disabled = true;
// 시뮬레이션: 실제로는 국세청 API 연동
setTimeout(() => {
Loading.hide();
isBusinessNumberVerified = true;
document.getElementById('verifyResult').innerHTML = `
<div style="display: flex; align-items: center; gap: 8px; color: var(--color-success);">
<span class="material-icons" style="font-size: 20px;">check_circle</span>
<span class="body-s" style="font-weight: 600;">확인됨</span>
</div>
`;
Toast.success('사업자번호가 확인되었습니다.');
checkFormValidity();
}, 1500);
};
// 메뉴 추가
window.addMenuItem = function() {
if (menuItems.length >= 10) {
Toast.error('메뉴는 최대 10개까지 추가할 수 있습니다.');
return;
}
Modal.show({
title: '메뉴 추가',
body: `
<div class="form-group">
<label class="form-label required">메뉴명</label>
<input
type="text"
id="menuName"
class="form-input"
placeholder="예) 양념치킨"
required
>
</div>
<div class="form-group">
<label class="form-label required">가격</label>
<input
type="number"
id="menuPrice"
class="form-input"
placeholder="20000"
required
min="0"
>
</div>
<div class="form-group">
<label class="form-label">설명</label>
<textarea
id="menuDescription"
class="form-input"
placeholder="메뉴 설명 (선택)"
rows="2"
></textarea>
</div>
`,
confirmText: '추가',
onConfirm: function() {
const name = document.getElementById('menuName').value.trim();
const price = document.getElementById('menuPrice').value;
const description = document.getElementById('menuDescription').value.trim();
if (!name || !price) {
Toast.error('메뉴명과 가격은 필수입니다.');
return;
}
menuItems.push({
id: Utils.generateId(),
name,
price: parseInt(price),
description
});
renderMenuList();
Toast.success('메뉴가 추가되었습니다.');
}
});
setTimeout(() => {
document.getElementById('menuName').focus();
}, 100);
};
// 메뉴 목록 렌더링
function renderMenuList() {
const menuList = document.getElementById('menuList');
if (menuItems.length === 0) {
menuList.innerHTML = '';
document.getElementById('addMenuBtn').style.display = 'flex';
return;
}
menuList.innerHTML = menuItems.map((item, index) => `
<div class="card" style="padding: 12px; margin-bottom: 8px; display: flex; justify-content: space-between; align-items: center;">
<div style="flex: 1;">
<div class="body-m" style="font-weight: 600; margin-bottom: 4px;">${item.name}</div>
<div class="body-s text-muted">
${Utils.formatNumber(item.price)}원
${item.description ? ' · ' + item.description : ''}
</div>
</div>
<button
type="button"
class="btn btn-text btn-sm"
onclick="removeMenuItem('${item.id}')"
style="color: var(--color-error);"
>
<span class="material-icons" style="font-size: 18px;">delete</span>
</button>
</div>
`).join('');
document.getElementById('addMenuBtn').style.display = menuItems.length >= 10 ? 'none' : 'flex';
}
window.removeMenuItem = function(id) {
menuItems = menuItems.filter(item => item.id !== id);
renderMenuList();
Toast.info('메뉴가 삭제되었습니다.');
};
// 이미지 업로드
window.handleImageUpload = function(event, index) {
const file = event.target.files[0];
if (!file) return;
// 파일 크기 검증 (5MB)
if (file.size > 5 * 1024 * 1024) {
Toast.error('이미지 크기는 5MB 이하여야 합니다.');
event.target.value = '';
return;
}
Loading.show('이미지 업로드 중...');
// 이미지 미리보기
const reader = new FileReader();
reader.onload = function(e) {
setTimeout(() => {
Loading.hide();
const imageUrl = e.target.result;
uploadedImages.push({
id: Utils.generateId(),
url: imageUrl,
file: file
});
// 현재 업로드 박스를 이미지로 교체
const currentBox = document.querySelector(`label[for="imageUpload${index}"]`);
currentBox.style.backgroundImage = `url(${imageUrl})`;
currentBox.style.backgroundSize = 'cover';
currentBox.style.backgroundPosition = 'center';
currentBox.innerHTML = `
<button
type="button"
class="image-remove-btn"
onclick="removeImage(${index})"
style="position: absolute; top: 4px; right: 4px; background: rgba(0,0,0,0.6); color: white; border: none; border-radius: 50%; width: 24px; height: 24px; display: flex; align-items: center; justify-content: center; cursor: pointer;"
>
<span class="material-icons" style="font-size: 16px;">close</span>
</button>
`;
// 다음 업로드 박스 표시
if (index < 3) {
const nextBox = document.querySelector(`label[for="imageUpload${index + 1}"]`);
if (nextBox) {
nextBox.style.display = 'flex';
}
}
Toast.success('이미지가 업로드되었습니다.');
}, 500);
};
reader.readAsDataURL(file);
};
window.removeImage = function(index) {
uploadedImages = uploadedImages.filter((_, i) => i !== index - 1);
// 이미지 박스 초기화
const imagePreview = document.getElementById('imagePreview');
imagePreview.innerHTML = `
<label class="image-upload-box" for="imageUpload1">
<input type="file" id="imageUpload1" name="storeImages" accept="image/*" style="display: none;" onchange="handleImageUpload(event, 1)">
<span class="material-icons" style="font-size: 32px; color: var(--color-gray-400);">add_photo_alternate</span>
</label>
<label class="image-upload-box" for="imageUpload2" style="display: none;">
<input type="file" id="imageUpload2" name="storeImages" accept="image/*" style="display: none;" onchange="handleImageUpload(event, 2)">
<span class="material-icons" style="font-size: 32px; color: var(--color-gray-400);">add_photo_alternate</span>
</label>
<label class="image-upload-box" for="imageUpload3" style="display: none;">
<input type="file" id="imageUpload3" name="storeImages" accept="image/*" style="display: none;" onchange="handleImageUpload(event, 3)">
<span class="material-icons" style="font-size: 32px; color: var(--color-gray-400);">add_photo_alternate</span>
</label>
`;
// 업로드된 이미지 다시 렌더링
uploadedImages.forEach((img, i) => {
const box = document.querySelector(`label[for="imageUpload${i + 1}"]`);
box.style.backgroundImage = `url(${img.url})`;
box.style.backgroundSize = 'cover';
box.style.backgroundPosition = 'center';
box.innerHTML = `
<button type="button" class="image-remove-btn" onclick="removeImage(${i + 1})" style="position: absolute; top: 4px; right: 4px; background: rgba(0,0,0,0.6); color: white; border: none; border-radius: 50%; width: 24px; height: 24px; display: flex; align-items: center; justify-content: center; cursor: pointer;">
<span class="material-icons" style="font-size: 16px;">close</span>
</button>
`;
if (i < 2) {
const nextBox = document.querySelector(`label[for="imageUpload${i + 2}"]`);
if (nextBox) nextBox.style.display = 'flex';
}
});
Toast.info('이미지가 삭제되었습니다.');
};
// 매장 특징 글자수 카운트
storeFeaturesTextarea.addEventListener('input', function() {
featureCountSpan.textContent = this.value.length;
});
// 폼 검증 및 버튼 활성화
function checkFormValidity() {
const isStoreNameValid = storeNameInput.value.trim().length >= 2;
const isBusinessTypeValid = businessTypeSelect.value !== '';
const isAddressValid = addressInput.value.trim() !== '';
const isBusinessNumberValid = isBusinessNumberVerified;
const isFormValid = isStoreNameValid && isBusinessTypeValid && isAddressValid && isBusinessNumberValid;
submitBtn.disabled = !isFormValid;
}
// 입력 필드 이벤트 리스너
[storeNameInput, businessTypeSelect, addressInput].forEach(input => {
input.addEventListener('input', checkFormValidity);
input.addEventListener('change', checkFormValidity);
});
// 폼 제출
form.addEventListener('submit', function(e) {
e.preventDefault();
if (!isBusinessNumberVerified) {
Toast.error('사업자번호를 먼저 검증해주세요.');
return;
}
Loading.show('매장 정보 저장 중...');
setTimeout(() => {
Loading.hide();
// 영업시간 수집
const businessHours = [];
const days = ['월', '화', '수', '목', '금', '토', '일'];
days.forEach((day, index) => {
const openTime = document.querySelector(`input[name="openTime_${index}"]`).value;
const closeTime = document.querySelector(`input[name="closeTime_${index}"]`).value;
businessHours.push({ day, openTime, closeTime });
});
// 매장 정보 저장
const storeData = {
id: Utils.generateId(),
name: storeNameInput.value.trim(),
businessType: businessTypeSelect.value,
businessTypeName: businessTypeSelect.options[businessTypeSelect.selectedIndex].text,
address: addressInput.value.trim(),
addressDetail: document.getElementById('addressDetail').value.trim(),
businessNumber: businessNumberInput.value,
businessHours: businessHours,
menuItems: menuItems,
features: storeFeaturesTextarea.value.trim(),
images: uploadedImages,
createdAt: new Date().toISOString()
};
window.AppState.store = storeData;
window.AppState.save();
// 성공 메시지
Modal.show({
title: '🎉 매장 등록 완료!',
body: `
<div style="text-align: center;">
<p class="body-l" style="margin-bottom: 16px;">
<strong>${storeData.name}</strong> 매장이<br>
성공적으로 등록되었습니다.
</p>
<div class="card" style="padding: 16px; background: var(--color-primary-light); border: 1px solid var(--color-primary-main);">
<div class="body-m" style="font-weight: 600; margin-bottom: 8px;">
🎁 무료 체험 쿠폰 발급 완료
</div>
<div class="body-s text-muted">
첫 이벤트 생성 시 사용 가능
</div>
</div>
</div>
`,
confirmText: '홈으로 이동',
showCancel: false,
onConfirm: function() {
// 홈 화면으로 이동
window.location.href = '21.5-홈.html';
}
});
}, 1500);
});
// 한식음식점 예제 데이터 로드
function loadKoreanRestaurantExample() {
// 매장명
storeNameInput.value = '수원왕갈비';
// 업종
businessTypeSelect.value = 'restaurant';
// 주소
addressInput.value = '경기도 수원시 팔달구 인계동 1055-1';
document.getElementById('addressDetail').value = '1층';
// 영업시간 (월~일 10:00-22:00)
const timeInputs = document.querySelectorAll('.business-hour-row');
timeInputs.forEach(row => {
row.querySelector('input[type="time"]:first-of-type').value = '10:00';
row.querySelector('input[type="time"]:last-of-type').value = '22:00';
});
// 사업자번호
businessNumberInput.value = '123-45-67890';
// 메뉴 예제 추가
menuItems = [
{
id: Utils.generateId(),
name: '왕갈비',
price: 18000,
description: '대표 메뉴, 1인분'
},
{
id: Utils.generateId(),
name: 'LA갈비',
price: 35000,
description: '미국산 최상급 LA갈비'
},
{
id: Utils.generateId(),
name: '된장찌개',
price: 7000,
description: '직접 담근 된장 사용'
},
{
id: Utils.generateId(),
name: '냉면',
price: 9000,
description: '여름 시즌 인기 메뉴'
},
{
id: Utils.generateId(),
name: '갈비탕',
price: 12000,
description: '푸짐한 갈비가 들어간 보양식'
}
];
renderMenuList();
// 매장 특징
storeFeaturesTextarea.value = '30년 전통의 한식 갈비 전문점입니다.\n직접 제작한 비법 양념으로 재운 갈비가 특징이며,\n주차 가능하고 단체석도 마련되어 있습니다.\n런치 시간 특가 메뉴 제공 중입니다.';
featureCountSpan.textContent = storeFeaturesTextarea.value.length;
// 사업자번호 검증 시뮬레이션
isBusinessNumberVerified = true;
verifyBtn.disabled = false;
document.getElementById('verifyResult').innerHTML = `
<div style="display: flex; align-items: center; gap: 8px; color: var(--color-success);">
<span class="material-icons" style="font-size: 20px;">check_circle</span>
<span class="body-s" style="font-weight: 600;">확인됨 (예제 데이터)</span>
</div>
`;
// 폼 유효성 체크
checkFormValidity();
// 안내 메시지
Toast.info('한식음식점 예제 데이터가 로드되었습니다.');
}
// 초기화
initBusinessHours();
// 예제 데이터 자동 로드
setTimeout(() => {
loadKoreanRestaurantExample();
}, 500);
console.log('매장정보등록 페이지 로드 완료');
})();
</script>
<style>
.image-upload-box {
width: 100px;
height: 100px;
border: 2px dashed var(--color-gray-300);
border-radius: var(--radius-md);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s ease;
position: relative;
background-color: var(--color-gray-50);
}
.image-upload-box:hover {
border-color: var(--color-primary-main);
background-color: var(--color-primary-light);
}
.image-upload-box:active {
transform: scale(0.98);
}
</style>
</body>
</html>

View File

@ -557,8 +557,8 @@
return;
}
// 선택한 이벤트 정보 저장
window.AppState.selectedEventType = selectedEvent;
// 선택한 이벤트 정보 저장 (localStorage에 직접 저장)
localStorage.setItem('kt_selected_event_type', JSON.stringify(selectedEvent));
window.AppState.save();
// 상세 정보 화면으로 이동

View File

@ -163,7 +163,32 @@
window.AppState.load();
const store = window.AppState.store;
const event = window.AppState.currentEvent;
const selectedEventType = window.AppState.selectedEventType;
// selectedEventType은 localStorage에서 직접 로드
let selectedEventType = null;
try {
selectedEventType = JSON.parse(localStorage.getItem('kt_selected_event_type') || 'null');
} catch (e) {
console.error('Failed to load selected event type:', e);
}
// 매장 정보가 없으면 이전 화면으로
if (!store) {
Toast.error('매장 정보를 먼저 등록해주세요.');
setTimeout(() => {
window.location.href = '02-매장정보등록.html';
}, 1000);
return;
}
// 이벤트 정보가 없으면 이전 화면으로
if (!event || !selectedEventType) {
Toast.error('이벤트 유형을 먼저 선택해주세요.');
setTimeout(() => {
window.location.href = '06-AI이벤트유형추천.html';
}, 1000);
return;
}
// 선택된 이벤트 유형 정보 표시
if (selectedEventType) {

View File

@ -0,0 +1,648 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI 영상 제작 - KT AI 이벤트</title>
<link rel="stylesheet" href="css/common.css">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Pretendard:wght@400;500;600;700&display=swap" rel="stylesheet">
</head>
<body>
<!-- Skip Navigation -->
<a href="#main-content" class="skip-link">본문으로 건너뛰기</a>
<!-- Header -->
<header class="header" role="banner">
<div class="header-content">
<button type="button"
class="btn-icon"
onclick="window.history.back()"
aria-label="이전 페이지로 돌아가기">
<span class="material-icons">arrow_back</span>
</button>
<h1 class="header-title">AI 영상 제작</h1>
<div style="width: 40px;"></div>
</div>
</header>
<!-- Progress Bar -->
<div class="progress-container" role="progressbar" aria-valuenow="40" aria-valuemin="0" aria-valuemax="100">
<div class="progress-bar" style="width: 40%;"></div>
<div class="progress-label">콘텐츠 2/5: 영상</div>
</div>
<!-- Main Content -->
<main id="main-content" class="container" role="main">
<!-- Settings Section -->
<section class="section" aria-labelledby="settings-title" id="settingsSection">
<h2 id="settings-title" class="h3">영상 설정</h2>
<!-- Background Music -->
<div class="card">
<div class="card-body">
<label class="form-label" for="musicSelect">배경 음악</label>
<div style="display: flex; flex-direction: column; gap: var(--spacing-s);">
<button type="button"
class="music-option"
data-music="upbeat1"
onclick="selectMusic('upbeat1')"
aria-pressed="true">
<span class="material-icons" style="color: var(--color-primary-main);">music_note</span>
<span style="flex: 1; text-align: left;">경쾌한 음악 1</span>
<span class="material-icons play-icon" onclick="playMusicPreview('upbeat1', event)">play_circle</span>
</button>
<button type="button"
class="music-option"
data-music="upbeat2"
onclick="selectMusic('upbeat2')"
aria-pressed="false">
<span class="material-icons" style="color: var(--color-primary-main);">music_note</span>
<span style="flex: 1; text-align: left;">신나는 음악 2</span>
<span class="material-icons play-icon" onclick="playMusicPreview('upbeat2', event)">play_circle</span>
</button>
<button type="button"
class="music-option"
data-music="calm1"
onclick="selectMusic('calm1')"
aria-pressed="false">
<span class="material-icons" style="color: var(--color-primary-main);">music_note</span>
<span style="flex: 1; text-align: left;">차분한 음악 3</span>
<span class="material-icons play-icon" onclick="playMusicPreview('calm1', event)">play_circle</span>
</button>
</div>
</div>
</div>
<!-- Text Overlay -->
<div class="card">
<div class="card-body">
<label class="form-label">텍스트 오버레이</label>
<div style="padding: var(--spacing-m); background: var(--color-gray-50); border-radius: var(--radius-md); margin-bottom: var(--spacing-s);">
<p class="body-m" id="overlayText" style="white-space: pre-line; min-height: 60px;">
🎉 연말 대박 이벤트!
</p>
</div>
<button type="button"
class="btn btn-outline btn-sm"
onclick="editOverlayText()">
<span class="material-icons" style="font-size: 18px;">edit</span>
<span>편집하기</span>
</button>
</div>
</div>
<!-- Resolution Selection -->
<div class="card">
<div class="card-body">
<label class="form-label">해상도 선택</label>
<p class="body-s" style="color: var(--color-gray-600); margin-bottom: var(--spacing-m);">
선택한 채널별로 영상이 생성됩니다.
</p>
<div style="display: flex; flex-direction: column; gap: var(--spacing-s);">
<label class="checkbox-card">
<input type="checkbox"
id="snsResolution"
checked
onchange="toggleResolution(this)">
<div class="checkbox-card-content">
<div>
<div class="body-m" style="font-weight: 600; margin-bottom: 4px;">SNS용 (정사각형)</div>
<div class="caption" style="color: var(--color-gray-600);">1080x1080 • Instagram, Facebook</div>
</div>
</div>
</label>
<label class="checkbox-card">
<input type="checkbox"
id="tvResolution"
checked
onchange="toggleResolution(this)">
<div class="checkbox-card-content">
<div>
<div class="body-m" style="font-weight: 600; margin-bottom: 4px;">우리동네TV용 (16:9)</div>
<div class="caption" style="color: var(--color-gray-600);">1920x1080 • Full HD</div>
</div>
</div>
</label>
</div>
</div>
</div>
<!-- Start Button -->
<button type="button"
class="btn btn-primary btn-lg"
onclick="startVideoCreation()"
style="width: 100%;">
<span class="material-icons">smart_display</span>
<span>AI 영상 제작 시작</span>
</button>
</section>
<!-- Progress Section (Hidden initially) -->
<section class="section" id="progressSection" style="display: none;" aria-live="polite">
<div class="card">
<div class="card-body" style="text-align: center; padding: var(--spacing-xl);">
<div class="loading-animation" style="margin-bottom: var(--spacing-l);">
<span class="material-icons" style="font-size: 64px; color: var(--color-primary-main); animation: pulse 1.5s ease-in-out infinite;">
movie_creation
</span>
</div>
<h3 class="h4" style="margin-bottom: var(--spacing-s);">🎬 AI가 영상 제작중...</h3>
<p class="body-s" style="color: var(--color-gray-600); margin-bottom: var(--spacing-l);" id="progressMessage">
이미지를 분석하고 있습니다...
</p>
<div class="progress-bar-container" style="margin-bottom: var(--spacing-m);">
<div class="progress-bar-fill" id="videoProgressBar" style="width: 0%;"></div>
</div>
<div class="body-m" style="font-weight: 600; color: var(--color-primary-main); margin-bottom: var(--spacing-xs);">
<span id="progressPercent">0</span>%
</div>
<p class="caption" style="color: var(--color-gray-600);">
예상 소요: <span id="remainingTime">3분</span>
</p>
</div>
</div>
</section>
<!-- Result Section (Hidden initially) -->
<section class="section" id="resultSection" style="display: none;" aria-labelledby="result-title">
<h2 id="result-title" class="h3">생성 완료</h2>
<!-- Video Preview -->
<div class="card">
<div class="card-body">
<div style="aspect-ratio: 16/9; background: var(--color-gray-900); border-radius: var(--radius-md); margin-bottom: var(--spacing-m); display: flex; align-items: center; justify-content: center; position: relative; overflow: hidden;">
<button type="button"
class="video-play-button"
onclick="playVideoPreview()"
aria-label="영상 미리보기 재생">
<span class="material-icons" style="font-size: 48px; color: white;">play_circle</span>
</button>
<div class="video-thumbnail" id="videoThumbnail" style="position: absolute; width: 100%; height: 100%; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); display: flex; align-items: center; justify-content: center;">
<span class="material-icons" style="font-size: 64px; color: white; opacity: 0.5;">movie</span>
</div>
</div>
<div class="body-s" style="color: var(--color-gray-600); text-align: center; margin-bottom: var(--spacing-m);">
15초 홍보 영상
</div>
<div style="padding: var(--spacing-m); background: var(--color-gray-50); border-radius: var(--radius-md);">
<div class="body-s" style="color: var(--color-gray-700);">
<div style="display: flex; align-items: center; gap: var(--spacing-xs); margin-bottom: var(--spacing-xs);">
<span class="material-icons" style="font-size: 16px; color: var(--color-success);">check_circle</span>
<span id="videoResolutions">SNS용, 우리동네TV용 영상 생성완료</span>
</div>
<div style="display: flex; align-items: center; gap: var(--spacing-xs);">
<span class="material-icons" style="font-size: 16px; color: var(--color-success);">check_circle</span>
<span>배경 음악 및 텍스트 오버레이 적용</span>
</div>
</div>
</div>
</div>
</div>
<!-- Action Buttons -->
<div style="display: flex; gap: var(--spacing-s);">
<button type="button"
class="btn btn-outline btn-lg"
onclick="downloadVideo()"
style="flex: 1;">
<span class="material-icons">download</span>
<span>다운로드</span>
</button>
<button type="button"
class="btn btn-outline btn-lg"
onclick="recreateVideo()"
style="flex: 1;">
<span class="material-icons">refresh</span>
<span>다시제작</span>
</button>
</div>
</section>
</main>
<!-- Fixed Bottom Button (for next step) -->
<div class="fixed-bottom-button" id="nextButton" style="display: none;">
<button type="button"
class="btn btn-primary btn-lg"
onclick="goToNext()">
다음 (SNS콘텐츠)
</button>
</div>
<!-- Text Edit Modal -->
<div id="textEditModal" class="modal" role="dialog" aria-labelledby="modal-title" aria-modal="true" style="display: none;">
<div class="modal-content">
<div class="modal-header">
<h3 id="modal-title" class="h4">텍스트 편집</h3>
<button type="button"
class="btn-icon"
onclick="closeTextEditModal()"
aria-label="닫기">
<span class="material-icons">close</span>
</button>
</div>
<div class="modal-body">
<label class="form-label" for="textInput">영상에 표시될 텍스트</label>
<textarea id="textInput"
class="form-input"
rows="4"
maxlength="100"
placeholder="영상에 표시될 텍스트를 입력하세요 (최대 100자)"
style="resize: vertical; min-height: 100px;"></textarea>
<div class="caption" style="color: var(--color-gray-600); margin-top: var(--spacing-xs); text-align: right;">
<span id="textCount">0</span>/100자
</div>
</div>
<div class="modal-footer">
<button type="button"
class="btn btn-outline"
onclick="closeTextEditModal()">
취소
</button>
<button type="button"
class="btn btn-primary"
onclick="saveOverlayText()">
저장
</button>
</div>
</div>
</div>
<script src="js/common.js"></script>
<script>
(function() {
'use strict';
let selectedMusic = 'upbeat1';
let overlayTextContent = '🎉 연말 대박 이벤트!';
let snsResolutionEnabled = true;
let tvResolutionEnabled = true;
// Load saved state
const eventData = AppState.currentEvent || {};
const storeData = AppState.store || {};
// Initialize overlay text from promotion message
if (eventData.promotionMessage && eventData.promotionMessage.text) {
overlayTextContent = eventData.promotionMessage.text.split('\n').slice(0, 2).join('\n');
}
document.addEventListener('DOMContentLoaded', function() {
document.getElementById('overlayText').textContent = overlayTextContent;
});
/**
* Select background music
*/
window.selectMusic = function(musicId) {
selectedMusic = musicId;
// Update UI
document.querySelectorAll('.music-option').forEach(function(btn) {
if (btn.dataset.music === musicId) {
btn.setAttribute('aria-pressed', 'true');
btn.style.background = 'var(--color-primary-lightest)';
btn.style.borderColor = 'var(--color-primary-main)';
} else {
btn.setAttribute('aria-pressed', 'false');
btn.style.background = 'white';
btn.style.borderColor = 'var(--color-gray-300)';
}
});
};
/**
* Play music preview
*/
window.playMusicPreview = function(musicId, event) {
event.stopPropagation();
const icon = event.target;
// Simulate playing
icon.textContent = 'pause_circle';
Toast.show('🎵 ' + getMusicName(musicId) + ' 미리듣기');
setTimeout(function() {
icon.textContent = 'play_circle';
}, 3000);
};
/**
* Get music name
*/
function getMusicName(musicId) {
const names = {
upbeat1: '경쾌한 음악 1',
upbeat2: '신나는 음악 2',
calm1: '차분한 음악 3'
};
return names[musicId] || '음악';
}
/**
* Edit overlay text
*/
window.editOverlayText = function() {
const modal = document.getElementById('textEditModal');
const input = document.getElementById('textInput');
const counter = document.getElementById('textCount');
input.value = overlayTextContent;
counter.textContent = overlayTextContent.length;
// Add input listener for character count
input.addEventListener('input', function() {
counter.textContent = this.value.length;
});
Modal.open(modal);
};
/**
* Close text edit modal
*/
window.closeTextEditModal = function() {
const modal = document.getElementById('textEditModal');
Modal.close(modal);
};
/**
* Save overlay text
*/
window.saveOverlayText = function() {
const input = document.getElementById('textInput');
overlayTextContent = input.value.trim();
if (!overlayTextContent) {
Toast.error('텍스트를 입력해주세요.');
return;
}
document.getElementById('overlayText').textContent = overlayTextContent;
closeTextEditModal();
Toast.success('텍스트가 저장되었습니다.');
};
/**
* Toggle resolution
*/
window.toggleResolution = function(checkbox) {
if (checkbox.id === 'snsResolution') {
snsResolutionEnabled = checkbox.checked;
} else if (checkbox.id === 'tvResolution') {
tvResolutionEnabled = checkbox.checked;
}
// At least one must be selected
if (!snsResolutionEnabled && !tvResolutionEnabled) {
checkbox.checked = true;
if (checkbox.id === 'snsResolution') {
snsResolutionEnabled = true;
} else {
tvResolutionEnabled = true;
}
Toast.show('최소 1개 이상 선택해야 합니다.');
}
};
/**
* Start video creation
*/
window.startVideoCreation = function() {
// Hide settings
document.getElementById('settingsSection').style.display = 'none';
// Show progress
document.getElementById('progressSection').style.display = 'block';
// Simulate video creation process
simulateVideoCreation();
};
/**
* Simulate video creation with progress
*/
function simulateVideoCreation() {
const progressBar = document.getElementById('videoProgressBar');
const progressPercent = document.getElementById('progressPercent');
const progressMessage = document.getElementById('progressMessage');
const remainingTime = document.getElementById('remainingTime');
const steps = [
{ percent: 0, message: '이미지를 분석하고 있습니다...', time: '3분' },
{ percent: 15, message: '배경 음악을 처리하고 있습니다...', time: '2분 30초' },
{ percent: 30, message: '텍스트 오버레이를 추가하고 있습니다...', time: '2분' },
{ percent: 45, message: 'SNS용 영상을 렌더링하고 있습니다...', time: '1분 30초' },
{ percent: 60, message: '우리동네TV용 영상을 렌더링하고 있습니다...', time: '1분' },
{ percent: 75, message: '영상 품질을 최적화하고 있습니다...', time: '40초' },
{ percent: 90, message: '최종 검토 중...', time: '20초' },
{ percent: 100, message: '완료!', time: '0초' }
];
let currentStep = 0;
function updateProgress() {
if (currentStep >= steps.length) {
// Show result
setTimeout(function() {
showResult();
}, 500);
return;
}
const step = steps[currentStep];
progressBar.style.width = step.percent + '%';
progressPercent.textContent = step.percent;
progressMessage.textContent = step.message;
remainingTime.textContent = step.time;
currentStep++;
setTimeout(updateProgress, 1500);
}
updateProgress();
}
/**
* Show result section
*/
function showResult() {
document.getElementById('progressSection').style.display = 'none';
document.getElementById('resultSection').style.display = 'block';
document.getElementById('nextButton').style.display = 'block';
// Update resolution text
const resolutions = [];
if (snsResolutionEnabled) resolutions.push('SNS용');
if (tvResolutionEnabled) resolutions.push('우리동네TV용');
document.getElementById('videoResolutions').textContent = resolutions.join(', ') + ' 영상 생성완료';
// Save to AppState
eventData.video = {
music: selectedMusic,
overlayText: overlayTextContent,
snsResolution: snsResolutionEnabled,
tvResolution: tvResolutionEnabled,
createdAt: new Date().toISOString()
};
AppState.currentEvent = eventData;
AppState.save();
Toast.success('영상이 성공적으로 생성되었습니다!');
}
/**
* Play video preview
*/
window.playVideoPreview = function() {
Toast.show('🎬 영상 미리보기 재생 (시뮬레이션)');
// In real implementation, would play actual video
};
/**
* Download video
*/
window.downloadVideo = function() {
Loading.show('영상 다운로드 준비 중...');
setTimeout(function() {
Loading.hide();
Toast.success('영상이 다운로드되었습니다!');
// In real implementation, would trigger actual download
}, 1500);
};
/**
* Recreate video
*/
window.recreateVideo = function() {
if (confirm('영상을 다시 제작하시겠습니까? 현재 설정으로 새로 생성됩니다.')) {
// Reset to settings
document.getElementById('resultSection').style.display = 'none';
document.getElementById('nextButton').style.display = 'none';
document.getElementById('settingsSection').style.display = 'block';
Toast.show('설정을 확인하고 다시 제작해주세요.');
}
};
/**
* Go to next step
*/
window.goToNext = function() {
window.location.href = '11-SNS콘텐츠생성.html';
};
})();
</script>
<style>
.music-option {
width: 100%;
padding: var(--spacing-m);
background: white;
border: 2px solid var(--color-gray-300);
border-radius: var(--radius-md);
display: flex;
align-items: center;
gap: var(--spacing-s);
cursor: pointer;
transition: all 0.2s ease;
font-size: 14px;
font-weight: 500;
}
.music-option:hover {
border-color: var(--color-primary-main);
}
.music-option[aria-pressed="true"] {
background: var(--color-primary-lightest);
border-color: var(--color-primary-main);
}
.play-icon {
cursor: pointer;
color: var(--color-primary-main);
transition: transform 0.2s ease;
}
.play-icon:hover {
transform: scale(1.1);
}
.checkbox-card {
display: block;
padding: var(--spacing-m);
background: white;
border: 2px solid var(--color-gray-300);
border-radius: var(--radius-md);
cursor: pointer;
transition: all 0.2s ease;
}
.checkbox-card:hover {
border-color: var(--color-primary-main);
}
.checkbox-card input[type="checkbox"] {
margin-right: var(--spacing-s);
}
.checkbox-card input[type="checkbox"]:checked ~ .checkbox-card-content {
color: var(--color-primary-main);
}
.checkbox-card-content {
display: flex;
align-items: center;
gap: var(--spacing-s);
}
.progress-bar-container {
width: 100%;
height: 8px;
background: var(--color-gray-200);
border-radius: var(--radius-full);
overflow: hidden;
}
.progress-bar-fill {
height: 100%;
background: linear-gradient(90deg, var(--color-primary-main), var(--color-secondary-main));
border-radius: var(--radius-full);
transition: width 0.5s ease;
}
.video-play-button {
position: absolute;
z-index: 2;
background: rgba(0, 0, 0, 0.3);
border: none;
border-radius: 50%;
width: 64px;
height: 64px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s ease;
}
.video-play-button:hover {
background: rgba(0, 0, 0, 0.5);
transform: scale(1.1);
}
@keyframes pulse {
0%, 100% {
opacity: 1;
transform: scale(1);
}
50% {
opacity: 0.7;
transform: scale(1.05);
}
}
</style>
</body>
</html>

View File

@ -0,0 +1,390 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI 이미지 생성 - KT AI 이벤트</title>
<link rel="stylesheet" href="css/common.css">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Pretendard:wght@400;500;600;700&display=swap" rel="stylesheet">
</head>
<body>
<a href="#main-content" class="skip-link">본문으로 건너뛰기</a>
<header class="header" role="banner">
<div class="header-content">
<button type="button" class="btn-icon" onclick="window.history.back()" aria-label="이전 페이지로 돌아가기">
<span class="material-icons">arrow_back</span>
</button>
<h1 class="header-title">AI 이미지 생성</h1>
<div style="width: 40px;"></div>
</div>
</header>
<div class="progress-indicator" role="progressbar" aria-valuenow="20" aria-valuemin="0" aria-valuemax="100">
<div class="progress-text">콘텐츠 1/5: 이미지</div>
<div class="progress-bar">
<div class="progress-fill" style="width: 20%;"></div>
</div>
</div>
<main id="main-content" class="container" role="main">
<section class="section" aria-labelledby="brand-settings-title">
<h2 id="brand-settings-title" class="h3">브랜드 설정</h2>
<div class="card">
<div class="card-body">
<div class="form-group">
<label for="brandColor" class="form-label">브랜드 컬러</label>
<div style="display: flex; gap: var(--spacing-s); align-items: center;">
<input type="color" id="brandColor" value="#E31E24" style="width: 60px; height: 48px; border: 1px solid var(--color-gray-300); border-radius: var(--radius-sm); cursor: pointer;">
<input type="text" id="brandColorText" class="input-field" value="#E31E24" placeholder="#RRGGBB" style="flex: 1;">
</div>
</div>
<div class="form-group">
<label for="logoUpload" class="form-label">로고 업로드 (선택)</label>
<div class="upload-area" id="uploadArea">
<input type="file" id="logoUpload" accept="image/*" style="display: none;" aria-label="로고 이미지 업로드">
<div class="upload-placeholder" id="uploadPlaceholder">
<span class="material-icons" style="font-size: 48px; color: var(--color-gray-400);">add_photo_alternate</span>
<div class="body-m" style="color: var(--color-gray-600); margin-top: var(--spacing-s);">이미지 선택</div>
<div class="caption" style="color: var(--color-gray-500); margin-top: var(--spacing-xs);">PNG, JPG (최대 5MB)</div>
</div>
<div class="upload-preview" id="uploadPreview" style="display: none;">
<img id="previewImage" src="" alt="업로드된 로고 미리보기" style="max-width: 100%; max-height: 200px; border-radius: var(--radius-md);">
<button type="button" class="btn btn-text btn-sm" onclick="removeImage()" style="margin-top: var(--spacing-s);">
<span class="material-icons">delete</span>
<span>삭제</span>
</button>
</div>
</div>
</div>
<button type="button" class="btn btn-primary btn-lg" onclick="startGeneration()" style="width: 100%; margin-top: var(--spacing-m);">
<span class="material-icons">auto_awesome</span>
<span>AI 이미지 생성 시작</span>
</button>
</div>
</div>
</section>
<section class="section" id="generationProgress" style="display: none;" aria-labelledby="progress-title">
<h2 id="progress-title" class="h3">🤖 AI가 이미지 생성중...</h2>
<div class="card">
<div class="card-body">
<div style="text-align: center;">
<div class="body-m" style="color: var(--color-gray-600); margin-bottom: var(--spacing-m);">
브랜드 컬러와 로고를 반영하여<br>3가지 스타일의 이미지를 생성합니다
</div>
<div class="progress-bar-container" style="height: 12px; margin-bottom: var(--spacing-s);">
<div class="progress-bar-fill" id="aiProgressBar" style="width: 0%; background: var(--color-primary-main);"></div>
</div>
<div class="body-s" style="color: var(--color-gray-600);" id="progressText">
진행률: 0%
</div>
<div class="caption" style="color: var(--color-gray-500); margin-top: var(--spacing-xs);" id="timeEstimate">
예상 소요: 2분 30초
</div>
</div>
</div>
</div>
</section>
<section class="section" id="generationResult" style="display: none;" aria-labelledby="result-title">
<h2 id="result-title" class="h3">생성된 이미지 (3종)</h2>
<div class="card">
<div class="card-body">
<div style="display: flex; flex-direction: column; gap: var(--spacing-m);">
<div class="image-option" onclick="selectImage(0)">
<div class="image-preview" style="aspect-ratio: 1/1; background: linear-gradient(135deg, #E31E24 0%, #FF6B6B 100%); border-radius: var(--radius-md); display: flex; align-items: center; justify-content: center; position: relative;">
<div style="text-align: center; color: white;">
<span class="material-icons" style="font-size: 48px;">palette</span>
<div class="body-l" style="font-weight: 600; margin-top: var(--spacing-s);">심플 스타일</div>
</div>
<div class="selection-badge" id="badge0" style="display: none;">
<span class="material-icons">check_circle</span>
</div>
</div>
<div style="display: flex; justify-content: space-between; margin-top: var(--spacing-s);">
<button type="button" class="btn btn-text btn-sm" onclick="event.stopPropagation(); downloadImage(0)">
<span class="material-icons">download</span>
<span>다운로드</span>
</button>
<button type="button" class="btn btn-text btn-sm" onclick="event.stopPropagation(); regenerateImage(0)">
<span class="material-icons">refresh</span>
<span>재생성</span>
</button>
</div>
</div>
<div class="image-option" onclick="selectImage(1)">
<div class="image-preview" style="aspect-ratio: 1/1; background: linear-gradient(135deg, #E31E24 0%, #FFD700 50%, #FF1493 100%); border-radius: var(--radius-md); display: flex; align-items: center; justify-content: center; position: relative;">
<div style="text-align: center; color: white;">
<span class="material-icons" style="font-size: 48px;">auto_awesome</span>
<div class="body-l" style="font-weight: 600; margin-top: var(--spacing-s);">화려한 스타일</div>
</div>
<div class="selection-badge" id="badge1" style="display: none;">
<span class="material-icons">check_circle</span>
</div>
</div>
<div style="display: flex; justify-content: space-between; margin-top: var(--spacing-s);">
<button type="button" class="btn btn-text btn-sm" onclick="event.stopPropagation(); downloadImage(1)">
<span class="material-icons">download</span>
<span>다운로드</span>
</button>
<button type="button" class="btn btn-text btn-sm" onclick="event.stopPropagation(); regenerateImage(1)">
<span class="material-icons">refresh</span>
<span>재생성</span>
</button>
</div>
</div>
<div class="image-option" onclick="selectImage(2)">
<div class="image-preview" style="aspect-ratio: 1/1; background: linear-gradient(135deg, #E31E24 0%, #0066FF 100%); border-radius: var(--radius-md); display: flex; align-items: center; justify-content: center; position: relative;">
<div style="text-align: center; color: white;">
<span class="material-icons" style="font-size: 48px;">trending_up</span>
<div class="body-l" style="font-weight: 600; margin-top: var(--spacing-s);">트렌디 스타일</div>
</div>
<div class="selection-badge" id="badge2" style="display: none;">
<span class="material-icons">check_circle</span>
</div>
</div>
<div style="display: flex; justify-content: space-between; margin-top: var(--spacing-s);">
<button type="button" class="btn btn-text btn-sm" onclick="event.stopPropagation(); downloadImage(2)">
<span class="material-icons">download</span>
<span>다운로드</span>
</button>
<button type="button" class="btn btn-text btn-sm" onclick="event.stopPropagation(); regenerateImage(2)">
<span class="material-icons">refresh</span>
<span>재생성</span>
</button>
</div>
</div>
</div>
<button type="button" class="btn btn-primary btn-lg" onclick="goToNext()" id="nextButton" disabled style="width: 100%; margin-top: var(--spacing-l);">
<span>다음 단계</span>
<span class="material-icons">arrow_forward</span>
</button>
</div>
</div>
</section>
</main>
<script src="js/common.js"></script>
<script>
(function() {
'use strict';
let selectedImageIndex = -1;
// 컬러 피커와 텍스트 입력 동기화
document.getElementById('brandColor').addEventListener('input', function(e) {
document.getElementById('brandColorText').value = e.target.value.toUpperCase();
});
document.getElementById('brandColorText').addEventListener('input', function(e) {
const color = e.target.value;
if (/^#[0-9A-F]{6}$/i.test(color)) {
document.getElementById('brandColor').value = color;
}
});
// 파일 업로드
document.getElementById('uploadArea').addEventListener('click', function() {
document.getElementById('logoUpload').click();
});
document.getElementById('logoUpload').addEventListener('change', function(e) {
const file = e.target.files[0];
if (file) {
if (file.size > 5 * 1024 * 1024) {
Toast.error('파일 크기는 5MB를 초과할 수 없습니다.');
return;
}
const reader = new FileReader();
reader.onload = function(event) {
document.getElementById('previewImage').src = event.target.result;
document.getElementById('uploadPlaceholder').style.display = 'none';
document.getElementById('uploadPreview').style.display = 'block';
};
reader.readAsDataURL(file);
}
});
window.removeImage = function() {
document.getElementById('logoUpload').value = '';
document.getElementById('uploadPlaceholder').style.display = 'flex';
document.getElementById('uploadPreview').style.display = 'none';
};
window.startGeneration = function() {
document.getElementById('generationProgress').style.display = 'block';
document.getElementById('generationProgress').scrollIntoView({ behavior: 'smooth' });
let progress = 0;
const interval = setInterval(function() {
progress += Math.random() * 15;
if (progress > 100) progress = 100;
document.getElementById('aiProgressBar').style.width = progress + '%';
document.getElementById('progressText').textContent = '진행률: ' + Math.round(progress) + '%';
const remainingTime = Math.max(0, Math.round(150 * (1 - progress / 100)));
const minutes = Math.floor(remainingTime / 60);
const seconds = remainingTime % 60;
document.getElementById('timeEstimate').textContent =
'예상 소요: ' + minutes + '분 ' + seconds + '초';
if (progress >= 100) {
clearInterval(interval);
setTimeout(function() {
document.getElementById('generationResult').style.display = 'block';
document.getElementById('generationResult').scrollIntoView({ behavior: 'smooth' });
Toast.success('✨ 이미지 생성이 완료되었습니다!');
}, 500);
}
}, 200);
};
window.selectImage = function(index) {
// 이전 선택 해제
if (selectedImageIndex >= 0) {
document.getElementById('badge' + selectedImageIndex).style.display = 'none';
}
selectedImageIndex = index;
document.getElementById('badge' + index).style.display = 'flex';
document.getElementById('nextButton').disabled = false;
Toast.show('이미지 ' + (index + 1) + '번이 선택되었습니다');
};
window.downloadImage = function(index) {
Toast.success('📥 이미지 ' + (index + 1) + '번이 다운로드되었습니다');
};
window.regenerateImage = function(index) {
Loading.show('이미지 재생성 중...');
setTimeout(function() {
Loading.hide();
Toast.success('✨ 새로운 이미지가 생성되었습니다');
}, 2000);
};
window.goToNext = function() {
if (selectedImageIndex < 0) {
Toast.error('이미지를 선택해주세요');
return;
}
Toast.show('다음 단계로 이동합니다');
};
})();
</script>
<style>
.progress-indicator {
background: white;
padding: var(--spacing-m) var(--spacing-l);
border-bottom: 1px solid var(--color-gray-300);
}
.progress-text {
font-size: 14px;
color: var(--color-gray-600);
margin-bottom: var(--spacing-s);
}
.progress-bar {
height: 4px;
background: var(--color-gray-200);
border-radius: var(--radius-full);
overflow: hidden;
}
.progress-fill {
height: 100%;
background: var(--color-primary-main);
transition: width 0.3s ease;
}
.upload-area {
border: 2px dashed var(--color-gray-300);
border-radius: var(--radius-md);
padding: var(--spacing-xl);
text-align: center;
cursor: pointer;
transition: all 0.2s ease;
}
.upload-area:hover {
border-color: var(--color-primary-main);
background: var(--color-gray-50);
}
.upload-placeholder {
display: flex;
flex-direction: column;
align-items: center;
}
.upload-preview {
display: flex;
flex-direction: column;
align-items: center;
}
.progress-bar-container {
width: 100%;
background: var(--color-gray-200);
border-radius: var(--radius-full);
overflow: hidden;
}
.progress-bar-fill {
height: 100%;
border-radius: var(--radius-full);
transition: width 0.3s ease;
}
.image-option {
cursor: pointer;
transition: transform 0.2s ease;
}
.image-option:hover {
transform: scale(1.02);
}
.image-preview {
position: relative;
}
.selection-badge {
position: absolute;
top: var(--spacing-m);
right: var(--spacing-m);
width: 40px;
height: 40px;
background: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
.selection-badge .material-icons {
color: var(--color-success);
font-size: 32px;
}
</style>
</body>
</html>

View File

@ -0,0 +1,483 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SNS 콘텐츠 생성 - KT AI 이벤트</title>
<link rel="stylesheet" href="css/common.css">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Pretendard:wght@400;500;600;700&display=swap" rel="stylesheet">
</head>
<body>
<!-- Skip Navigation -->
<a href="#main-content" class="skip-link">본문으로 건너뛰기</a>
<!-- Header -->
<header class="header" role="banner">
<div class="header-content">
<button type="button"
class="btn-icon"
onclick="window.history.back()"
aria-label="이전 페이지로 돌아가기">
<span class="material-icons">arrow_back</span>
</button>
<h1 class="header-title">SNS 콘텐츠 생성</h1>
<div style="width: 40px;"></div>
</div>
</header>
<!-- Progress Bar -->
<div class="progress-container" role="progressbar" aria-valuenow="60" aria-valuemin="0" aria-valuemax="100">
<div class="progress-bar" style="width: 60%;"></div>
<div class="progress-label">콘텐츠 3/5: SNS</div>
</div>
<!-- Main Content -->
<main id="main-content" class="container" role="main">
<!-- Platform Selection Section -->
<section class="section" aria-labelledby="platform-title" id="selectionSection">
<h2 id="platform-title" class="h3">플랫폼 선택</h2>
<p class="body-s" style="color: var(--color-gray-600); margin-bottom: var(--spacing-l);">
각 플랫폼에 최적화된 콘텐츠가 생성됩니다.
</p>
<!-- Instagram (Required) -->
<div class="card">
<div class="card-body">
<label class="platform-card">
<input type="checkbox"
id="platformInstagram"
checked
disabled
onchange="togglePlatform(this)">
<div class="platform-card-content">
<div style="display: flex; align-items: center; gap: var(--spacing-s); flex: 1;">
<div class="platform-icon" style="background: linear-gradient(45deg, #f09433 0%,#e6683c 25%,#dc2743 50%,#cc2366 75%,#bc1888 100%);">
<span class="material-icons" style="color: white;">photo_camera</span>
</div>
<div style="flex: 1;">
<div class="body-m" style="font-weight: 600; margin-bottom: 4px;">
Instagram
<span class="tag tag-sm" style="background: var(--color-primary-main); color: white; margin-left: var(--spacing-xs);">필수</span>
</div>
<div class="caption" style="color: var(--color-gray-600);">1080x1080 (정사각형)</div>
</div>
</div>
<button type="button"
class="btn-text btn-sm"
onclick="previewPlatform('instagram', event)"
aria-label="Instagram 미리보기">
미리보기
</button>
</div>
</label>
</div>
</div>
<!-- Naver Blog (Optional) -->
<div class="card">
<div class="card-body">
<label class="platform-card">
<input type="checkbox"
id="platformBlog"
onchange="togglePlatform(this)">
<div class="platform-card-content">
<div style="display: flex; align-items: center; gap: var(--spacing-s); flex: 1;">
<div class="platform-icon" style="background: #03C75A;">
<span class="material-icons" style="color: white;">article</span>
</div>
<div style="flex: 1;">
<div class="body-m" style="font-weight: 600; margin-bottom: 4px;">Naver Blog</div>
<div class="caption" style="color: var(--color-gray-600);">800x600 (가로형)</div>
</div>
</div>
<button type="button"
class="btn-text btn-sm"
onclick="previewPlatform('blog', event)"
aria-label="Naver Blog 미리보기">
미리보기
</button>
</div>
</label>
</div>
</div>
<!-- Kakao Channel (Optional) -->
<div class="card">
<div class="card-body">
<label class="platform-card">
<input type="checkbox"
id="platformKakao"
onchange="togglePlatform(this)">
<div class="platform-card-content">
<div style="display: flex; align-items: center; gap: var(--spacing-s); flex: 1;">
<div class="platform-icon" style="background: #FEE500;">
<span class="material-icons" style="color: #3C1E1E;">chat</span>
</div>
<div style="flex: 1;">
<div class="body-m" style="font-weight: 600; margin-bottom: 4px;">Kakao Channel</div>
<div class="caption" style="color: var(--color-gray-600);">800x800 (정사각형)</div>
</div>
</div>
<button type="button"
class="btn-text btn-sm"
onclick="previewPlatform('kakao', event)"
aria-label="Kakao Channel 미리보기">
미리보기
</button>
</div>
</label>
</div>
</div>
<!-- Generate Button -->
<button type="button"
class="btn btn-primary btn-lg"
onclick="generateContent()"
style="width: 100%; margin-top: var(--spacing-m);">
<span class="material-icons">auto_awesome</span>
<span>콘텐츠 생성하기</span>
</button>
</section>
<!-- Result Section (Hidden initially) -->
<section class="section" id="resultSection" style="display: none;" aria-labelledby="result-title">
<h2 id="result-title" class="h3">생성된 콘텐츠</h2>
<!-- Content Preview -->
<div id="contentPreview"></div>
<!-- Post Text -->
<div class="card">
<div class="card-body">
<label class="form-label">게시 텍스트</label>
<div style="padding: var(--spacing-m); background: var(--color-gray-50); border-radius: var(--radius-md); margin-bottom: var(--spacing-s);">
<p class="body-s" id="postText" style="white-space: pre-line;"></p>
</div>
<div id="hashtagsContainer"></div>
</div>
</div>
<!-- Action Buttons -->
<div style="display: flex; gap: var(--spacing-s); margin-top: var(--spacing-l);">
<button type="button"
class="btn btn-outline btn-lg"
onclick="downloadAll()"
style="flex: 1;">
<span class="material-icons">download</span>
<span>일괄 다운로드</span>
</button>
<button type="button"
class="btn btn-outline btn-lg"
onclick="regenerate()"
style="flex: 1;">
<span class="material-icons">refresh</span>
<span>다시생성</span>
</button>
</div>
</section>
</main>
<!-- Fixed Bottom Button (for next step) -->
<div class="fixed-bottom-button" id="nextButton" style="display: none;">
<button type="button"
class="btn btn-primary btn-lg"
onclick="goToNext()">
다음 (QR포스터)
</button>
</div>
<!-- Preview Modal -->
<div id="previewModal" class="modal" role="dialog" aria-labelledby="preview-title" aria-modal="true" style="display: none;">
<div class="modal-content" style="max-width: 400px;">
<div class="modal-header">
<h3 id="preview-title" class="h4">플랫폼 미리보기</h3>
<button type="button"
class="btn-icon"
onclick="closePreview()"
aria-label="닫기">
<span class="material-icons">close</span>
</button>
</div>
<div class="modal-body" id="previewContent" style="padding: 0;">
<!-- Platform-specific preview will be inserted here -->
</div>
</div>
</div>
<script src="js/common.js"></script>
<script>
(function() {
'use strict';
const platforms = {
instagram: true, // Required
blog: false,
kakao: false
};
// Load saved state
const eventData = AppState.currentEvent || {};
const storeData = AppState.store || {};
/**
* Toggle platform selection
*/
window.togglePlatform = function(checkbox) {
const platformId = checkbox.id.replace('platform', '').toLowerCase();
platforms[platformId] = checkbox.checked;
};
/**
* Preview platform
*/
window.previewPlatform = function(platform, event) {
event.stopPropagation();
event.preventDefault();
const modal = document.getElementById('previewModal');
const content = document.getElementById('previewContent');
const title = document.getElementById('preview-title');
const storeName = storeData.name || '우리가게';
const promotionText = eventData.promotionMessage?.text || '🎉 연말 대박 이벤트!';
let previewHTML = '';
if (platform === 'instagram') {
title.textContent = 'Instagram 미리보기';
previewHTML = `
<div style="background: white; border: 1px solid var(--color-gray-300);">
<div style="padding: 12px; display: flex; align-items: center; gap: 8px; border-bottom: 1px solid var(--color-gray-300);">
<div style="width: 32px; height: 32px; background: var(--color-primary-main); border-radius: 50%;"></div>
<div class="body-m" style="font-weight: 600;">${storeName}</div>
</div>
<div style="aspect-ratio: 1; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); display: flex; align-items: center; justify-content: center; color: white; padding: var(--spacing-l); text-align: center;">
<div>
<div class="h3" style="color: white; margin-bottom: var(--spacing-s);">🎁</div>
<div class="body-l" style="color: white; font-weight: 600;">${promotionText.split('\n')[0]}</div>
</div>
</div>
<div style="padding: 12px;">
<div class="body-s" style="margin-bottom: 8px;">${promotionText}</div>
<div class="caption" style="color: var(--color-secondary-main);">#이벤트 #경품 #당첨</div>
</div>
</div>
`;
} else if (platform === 'blog') {
title.textContent = 'Naver Blog 미리보기';
previewHTML = `
<div style="background: white; padding: var(--spacing-l);">
<div class="h4" style="margin-bottom: var(--spacing-m); padding-bottom: var(--spacing-m); border-bottom: 2px solid var(--color-primary-main);">
${storeName} 이벤트 소식
</div>
<div style="aspect-ratio: 4/3; background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); border-radius: var(--radius-md); display: flex; align-items: center; justify-content: center; margin-bottom: var(--spacing-m);">
<div style="text-align: center; color: white; padding: var(--spacing-l);">
<div class="h2" style="color: white;">🎉</div>
<div class="h4" style="color: white; margin-top: var(--spacing-s);">${promotionText.split('\n')[0]}</div>
</div>
</div>
<div class="body-m" style="line-height: 1.6; white-space: pre-line;">${promotionText}</div>
</div>
`;
} else if (platform === 'kakao') {
title.textContent = 'Kakao Channel 미리보기';
previewHTML = `
<div style="background: #F7F7F7;">
<div style="background: white; border-radius: 12px; overflow: hidden; margin: 16px;">
<div style="aspect-ratio: 1; background: linear-gradient(135deg, #FFE17D 0%, #FFBB54 100%); display: flex; align-items: center; justify-content: center;">
<div style="text-align: center; padding: var(--spacing-l);">
<div class="h2">🎁</div>
<div class="h4" style="margin-top: var(--spacing-s);">${promotionText.split('\n')[0]}</div>
</div>
</div>
<div style="padding: 16px; background: white;">
<div class="body-m" style="font-weight: 600; margin-bottom: 8px;">${storeName}</div>
<div class="body-s" style="color: var(--color-gray-600);">${promotionText}</div>
</div>
</div>
</div>
`;
}
content.innerHTML = previewHTML;
Modal.open(modal);
};
/**
* Close preview modal
*/
window.closePreview = function() {
const modal = document.getElementById('previewModal');
Modal.close(modal);
};
/**
* Generate content
*/
window.generateContent = function() {
const selectedPlatforms = Object.keys(platforms).filter(p => platforms[p]);
if (selectedPlatforms.length === 0) {
Toast.error('최소 1개 플랫폼을 선택해주세요.');
return;
}
Loading.show('SNS 콘텐츠 생성 중...');
setTimeout(function() {
Loading.hide();
showResult(selectedPlatforms);
}, 2000);
};
/**
* Show result
*/
function showResult(selectedPlatforms) {
document.getElementById('selectionSection').style.display = 'none';
document.getElementById('resultSection').style.display = 'block';
document.getElementById('nextButton').style.display = 'block';
const storeName = storeData.name || '우리가게';
const promotionMessage = eventData.promotionMessage || {};
const promotionText = promotionMessage.text || '🎉 연말 대박 이벤트!';
const hashtags = promotionMessage.hashtags || ['#이벤트', '#경품', '#당첨'];
// Generate content previews
let previewHTML = '<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: var(--spacing-m); margin-bottom: var(--spacing-l);">';
selectedPlatforms.forEach(function(platform) {
const platformInfo = getPlatformInfo(platform);
previewHTML += `
<div class="card">
<div class="card-body" style="padding: var(--spacing-s);">
<div style="aspect-ratio: 1; background: ${platformInfo.gradient}; border-radius: var(--radius-sm); margin-bottom: var(--spacing-s); display: flex; align-items: center; justify-content: center;">
<span class="material-icons" style="font-size: 32px; color: white;">${platformInfo.icon}</span>
</div>
<div class="caption" style="text-align: center; font-weight: 600;">${platformInfo.name}</div>
<div class="caption" style="text-align: center; color: var(--color-gray-600); font-size: 11px;">${platformInfo.size}</div>
</div>
</div>
`;
});
previewHTML += '</div>';
document.getElementById('contentPreview').innerHTML = previewHTML;
// Set post text
document.getElementById('postText').textContent = promotionText;
// Set hashtags
const hashtagHTML = '<div style="display: flex; flex-wrap: wrap; gap: var(--spacing-xs); margin-top: var(--spacing-s);">' +
hashtags.map(tag => `<span class="tag tag-sm" style="background: var(--color-secondary-lightest); color: var(--color-secondary-main);">${tag}</span>`).join('') +
'</div>';
document.getElementById('hashtagsContainer').innerHTML = hashtagHTML;
// Save to AppState
eventData.snsContent = {
platforms: selectedPlatforms,
text: promotionText,
hashtags: hashtags,
createdAt: new Date().toISOString()
};
AppState.currentEvent = eventData;
AppState.save();
Toast.success('SNS 콘텐츠가 생성되었습니다!');
}
/**
* Get platform info
*/
function getPlatformInfo(platform) {
const info = {
instagram: {
name: 'Instagram',
size: '1080x1080',
gradient: 'linear-gradient(45deg, #f09433 0%,#e6683c 25%,#dc2743 50%,#cc2366 75%,#bc1888 100%)',
icon: 'photo_camera'
},
blog: {
name: 'Naver Blog',
size: '800x600',
gradient: 'linear-gradient(135deg, #03C75A 0%, #02A648 100%)',
icon: 'article'
},
kakao: {
name: 'Kakao Channel',
size: '800x800',
gradient: 'linear-gradient(135deg, #FFE17D 0%, #FFBB54 100%)',
icon: 'chat'
}
};
return info[platform] || info.instagram;
}
/**
* Download all content
*/
window.downloadAll = function() {
Loading.show('콘텐츠 다운로드 준비 중...');
setTimeout(function() {
Loading.hide();
Toast.success('📦 모든 콘텐츠가 다운로드되었습니다!');
// In real implementation, would create and download ZIP file
}, 1500);
};
/**
* Regenerate content
*/
window.regenerate = function() {
if (confirm('콘텐츠를 다시 생성하시겠습니까?')) {
document.getElementById('resultSection').style.display = 'none';
document.getElementById('nextButton').style.display = 'none';
document.getElementById('selectionSection').style.display = 'block';
Toast.show('플랫폼을 선택하고 다시 생성해주세요.');
}
};
/**
* Go to next step
*/
window.goToNext = function() {
window.location.href = '12-QR포스터생성.html';
};
})();
</script>
<style>
.platform-card {
display: block;
cursor: pointer;
}
.platform-card input[type="checkbox"] {
margin-right: var(--spacing-s);
}
.platform-card-content {
display: flex;
align-items: center;
gap: var(--spacing-m);
}
.platform-icon {
width: 48px;
height: 48px;
border-radius: var(--radius-md);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.platform-card input[type="checkbox"]:disabled {
cursor: not-allowed;
}
</style>
</body>
</html>

View File

@ -0,0 +1,240 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>QR 포스터 생성 - KT AI 이벤트</title>
<link rel="stylesheet" href="css/common.css">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Pretendard:wght@400;500;600;700&display=swap" rel="stylesheet">
</head>
<body>
<a href="#main-content" class="skip-link">본문으로 건너뛰기</a>
<header class="header" role="banner">
<div class="header-content">
<button type="button" class="btn-icon" onclick="window.history.back()" aria-label="이전 페이지로 돌아가기">
<span class="material-icons">arrow_back</span>
</button>
<h1 class="header-title">QR 포스터 생성</h1>
<div style="width: 40px;"></div>
</div>
</header>
<div class="progress-container" role="progressbar" aria-valuenow="80" aria-valuemin="0" aria-valuemax="100">
<div class="progress-bar" style="width: 80%;"></div>
<div class="progress-label">콘텐츠 4/5: QR 포스터</div>
</div>
<main id="main-content" class="container" role="main">
<section class="section" aria-labelledby="qr-title" id="settingsSection">
<h2 id="qr-title" class="h3">QR 코드 설정</h2>
<div class="card">
<div class="card-body">
<label class="form-label">연결 URL (자동생성)</label>
<div style="display: flex; gap: var(--spacing-s); align-items: center;">
<div class="form-input" id="eventUrl" style="flex: 1; font-family: monospace; background: var(--color-gray-50);">
ktevnt.co/abc123
</div>
<button type="button" class="btn btn-outline btn-sm" onclick="copyUrl()" aria-label="URL 복사">
<span class="material-icons" style="font-size: 18px;">content_copy</span>
</button>
</div>
</div>
</div>
<div class="card">
<div class="card-body">
<label class="form-label">QR 코드 미리보기</label>
<div style="padding: var(--spacing-xl); background: var(--color-gray-50); border-radius: var(--radius-md); display: flex; justify-content: center;">
<div id="qrCode" style="width: 200px; height: 200px; background: white; border: 2px solid var(--color-gray-300); border-radius: var(--radius-md); display: flex; align-items: center; justify-content: center;">
<div style="width: 180px; height: 180px; background: linear-gradient(45deg, #000 25%, transparent 25%, transparent 75%, #000 75%, #000), linear-gradient(45deg, #000 25%, transparent 25%, transparent 75%, #000 75%, #000); background-size: 20px 20px; background-position: 0 0, 10px 10px;"></div>
</div>
</div>
</div>
</div>
<div class="card">
<div class="card-body">
<label class="form-label">포스터 크기</label>
<div style="display: flex; flex-direction: column; gap: var(--spacing-s);">
<label class="radio-card">
<input type="radio" name="posterSize" value="A4" checked onchange="selectSize(this)">
<div class="radio-card-content">
<div>
<div class="body-m" style="font-weight: 600;">A4</div>
<div class="caption" style="color: var(--color-gray-600);">210 × 297mm (일반 인쇄용)</div>
</div>
</div>
</label>
<label class="radio-card">
<input type="radio" name="posterSize" value="A3" onchange="selectSize(this)">
<div class="radio-card-content">
<div>
<div class="body-m" style="font-weight: 600;">A3</div>
<div class="caption" style="color: var(--color-gray-600);">297 × 420mm (대형 포스터용)</div>
</div>
</div>
</label>
</div>
</div>
</div>
<button type="button" class="btn btn-primary btn-lg" onclick="generatePoster()" style="width: 100%;">
<span class="material-icons">description</span>
<span>포스터 생성하기</span>
</button>
</section>
<section class="section" id="resultSection" style="display: none;" aria-labelledby="result-title">
<h2 id="result-title" class="h3">생성 완료</h2>
<div class="card">
<div class="card-body">
<div style="aspect-ratio: 210/297; background: white; border: 2px solid var(--color-gray-300); border-radius: var(--radius-md); margin-bottom: var(--spacing-m); display: flex; align-items: center; justify-content: center; position: relative; overflow: hidden;">
<div style="padding: var(--spacing-l); text-align: center; width: 100%;">
<div class="h3" style="color: var(--color-primary-main); margin-bottom: var(--spacing-m);">🎉</div>
<div class="h4" style="margin-bottom: var(--spacing-l);" id="posterTitle">이벤트 참여하세요!</div>
<div style="width: 120px; height: 120px; margin: 0 auto var(--spacing-m); background: white; border: 2px solid var(--color-gray-300);">
<div style="width: 100%; height: 100%; background: linear-gradient(45deg, #000 25%, transparent 25%, transparent 75%, #000 75%, #000), linear-gradient(45deg, #000 25%, transparent 25%, transparent 75%, #000 75%, #000); background-size: 15px 15px; background-position: 0 0, 7.5px 7.5px;"></div>
</div>
<div class="body-s" style="color: var(--color-gray-600);">QR 코드를 스캔하세요</div>
</div>
</div>
<div class="info-grid">
<div class="info-item">
<span class="info-label">PDF 파일</span>
<span class="info-value" id="pdfSize">2.1 MB</span>
</div>
<div class="info-item">
<span class="info-label">QR 이미지</span>
<span class="info-value">150 KB</span>
</div>
</div>
</div>
</div>
<div style="display: flex; gap: var(--spacing-s);">
<button type="button" class="btn btn-outline btn-lg" onclick="downloadPoster()" style="flex: 1;">
<span class="material-icons">download</span>
<span>다운로드</span>
</button>
<button type="button" class="btn btn-outline btn-lg" onclick="printPoster()" style="flex: 1;">
<span class="material-icons">print</span>
<span>인쇄하기</span>
</button>
</div>
</section>
</main>
<div class="fixed-bottom-button" id="nextButton" style="display: none;">
<button type="button" class="btn btn-primary btn-lg" onclick="goToNext()">
다음 (콘텐츠편집)
</button>
</div>
<script src="js/common.js"></script>
<script>
(function() {
'use strict';
let selectedSize = 'A4';
const eventData = AppState.currentEvent || {};
const storeData = AppState.store || {};
// Generate random event URL
const eventUrl = 'ktevnt.co/' + Math.random().toString(36).substring(2, 9);
document.getElementById('eventUrl').textContent = eventUrl;
window.copyUrl = function() {
navigator.clipboard.writeText(eventUrl).then(function() {
Toast.success('URL이 복사되었습니다!');
}).catch(function() {
Toast.error('복사에 실패했습니다.');
});
};
window.selectSize = function(radio) {
selectedSize = radio.value;
};
window.generatePoster = function() {
Loading.show('포스터 생성 중...');
setTimeout(function() {
Loading.hide();
showResult();
}, 2000);
};
function showResult() {
document.getElementById('settingsSection').style.display = 'none';
document.getElementById('resultSection').style.display = 'block';
document.getElementById('nextButton').style.display = 'block';
const storeName = storeData.name || '우리가게';
document.getElementById('posterTitle').textContent = storeName + ' 이벤트 참여하세요!';
document.getElementById('pdfSize').textContent = selectedSize === 'A4' ? '2.1 MB' : '4.8 MB';
eventData.qrPoster = {
url: eventUrl,
size: selectedSize,
createdAt: new Date().toISOString()
};
AppState.currentEvent = eventData;
AppState.save();
Toast.success('QR 포스터가 생성되었습니다!');
}
window.downloadPoster = function() {
Loading.show('다운로드 준비 중...');
setTimeout(function() {
Loading.hide();
Toast.success('포스터가 다운로드되었습니다!');
}, 1000);
};
window.printPoster = function() {
Toast.show('인쇄 다이얼로그를 엽니다...');
// window.print();
};
window.goToNext = function() {
window.location.href = '13-콘텐츠편집.html';
};
})();
</script>
<style>
.radio-card {
display: block;
padding: var(--spacing-m);
background: white;
border: 2px solid var(--color-gray-300);
border-radius: var(--radius-md);
cursor: pointer;
transition: all 0.2s ease;
}
.radio-card:hover {
border-color: var(--color-primary-main);
}
.radio-card input[type="radio"] {
margin-right: var(--spacing-s);
}
.radio-card input[type="radio"]:checked ~ .radio-card-content {
color: var(--color-primary-main);
}
.radio-card-content {
display: flex;
align-items: center;
}
</style>
</body>
</html>

View File

@ -0,0 +1,325 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>콘텐츠 편집 - KT AI 이벤트</title>
<link rel="stylesheet" href="css/common.css">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Pretendard:wght@400;500;600;700&display=swap" rel="stylesheet">
</head>
<body>
<a href="#main-content" class="skip-link">본문으로 건너뛰기</a>
<header class="header" role="banner">
<div class="header-content">
<button type="button" class="btn-icon" onclick="window.history.back()" aria-label="이전 페이지로 돌아가기">
<span class="material-icons">arrow_back</span>
</button>
<h1 class="header-title">콘텐츠 편집</h1>
<div style="width: 40px;"></div>
</div>
</header>
<main id="main-content" class="container" role="main" style="padding-bottom: 100px;">
<section class="section" aria-labelledby="select-title">
<h2 id="select-title" class="h3">편집할 콘텐츠 선택</h2>
<div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--spacing-m);">
<button type="button" class="content-card" onclick="editContent('image')" data-type="image">
<div class="content-card-icon" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);">
<span class="material-icons" style="color: white; font-size: 32px;">image</span>
</div>
<div class="body-s" style="font-weight: 600; text-align: center;">이미지</div>
</button>
<button type="button" class="content-card" onclick="editContent('video')" data-type="video">
<div class="content-card-icon" style="background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);">
<span class="material-icons" style="color: white; font-size: 32px;">videocam</span>
</div>
<div class="body-s" style="font-weight: 600; text-align: center;">영상</div>
</button>
<button type="button" class="content-card" onclick="editContent('sns')" data-type="sns">
<div class="content-card-icon" style="background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);">
<span class="material-icons" style="color: white; font-size: 32px;">share</span>
</div>
<div class="body-s" style="font-weight: 600; text-align: center;">SNS</div>
</button>
</div>
</section>
<section class="section" id="editorSection" style="display: none;" aria-labelledby="editor-title">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: var(--spacing-m);">
<h2 id="editor-title" class="h3">편집 중</h2>
<button type="button" class="btn-text" onclick="closeEditor()">
<span class="material-icons">close</span>
</button>
</div>
<div class="card">
<div class="card-body">
<div id="previewArea" style="aspect-ratio: 1; background: var(--color-gray-100); border-radius: var(--radius-md); margin-bottom: var(--spacing-m); display: flex; align-items: center; justify-content: center; position: relative; overflow: hidden;">
<div id="previewContent" style="padding: var(--spacing-l); text-align: center; width: 100%;">
<div class="h4" id="previewText">🎉 연말 대박 이벤트!</div>
</div>
</div>
<div style="display: flex; gap: var(--spacing-xs); margin-bottom: var(--spacing-m);">
<button type="button" class="btn btn-outline btn-sm" onclick="editText()" style="flex: 1;">
<span class="material-icons" style="font-size: 18px;">text_fields</span>
<span>텍스트</span>
</button>
<button type="button" class="btn btn-outline btn-sm" onclick="editColor()" style="flex: 1;">
<span class="material-icons" style="font-size: 18px;">palette</span>
<span>색상</span>
</button>
<button type="button" class="btn btn-outline btn-sm" onclick="editSize()" style="flex: 1;">
<span class="material-icons" style="font-size: 18px;">photo_size_select_small</span>
<span>크기</span>
</button>
</div>
<div>
<label class="form-label">편집 이력</label>
<div style="padding: var(--spacing-m); background: var(--color-gray-50); border-radius: var(--radius-md);">
<div style="display: flex; flex-direction: column; gap: var(--spacing-xs);">
<div style="display: flex; justify-content: space-between; align-items: center;">
<span class="body-s">• 원본</span>
<button type="button" class="btn-text btn-sm" onclick="revertToOriginal()">되돌리기</button>
</div>
<div style="display: flex; justify-content: space-between; align-items: center;">
<span class="body-s" style="font-weight: 600;">• 버전 1 (현재)</span>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<section class="section" id="skipSection">
<button type="button" class="btn btn-outline btn-lg" onclick="skipEdit()" style="width: 100%;">
편집 건너뛰기
</button>
</section>
</main>
<div class="fixed-bottom-button" id="saveButton" style="display: none;">
<button type="button" class="btn btn-outline btn-lg" onclick="cancelEdit()" style="flex: 1;">
취소
</button>
<button type="button" class="btn btn-primary btn-lg" onclick="saveEdit()" style="flex: 2;">
저장하기
</button>
</div>
<div id="textEditModal" class="modal" role="dialog" aria-modal="true" style="display: none;">
<div class="modal-content">
<div class="modal-header">
<h3 class="h4">텍스트 편집</h3>
<button type="button" class="btn-icon" onclick="closeTextModal()">
<span class="material-icons">close</span>
</button>
</div>
<div class="modal-body">
<label class="form-label" for="editTextInput">텍스트 내용</label>
<textarea id="editTextInput" class="form-input" rows="4" placeholder="텍스트를 입력하세요" style="resize: vertical;"></textarea>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline" onclick="closeTextModal()">취소</button>
<button type="button" class="btn btn-primary" onclick="applyText()">적용</button>
</div>
</div>
</div>
<div id="colorEditModal" class="modal" role="dialog" aria-modal="true" style="display: none;">
<div class="modal-content">
<div class="modal-header">
<h3 class="h4">색상 선택</h3>
<button type="button" class="btn-icon" onclick="closeColorModal()">
<span class="material-icons">close</span>
</button>
</div>
<div class="modal-body">
<label class="form-label">배경 색상</label>
<div style="display: grid; grid-template-columns: repeat(5, 1fr); gap: var(--spacing-s);">
<button type="button" class="color-option" data-color="#667eea" onclick="selectColor(this)" style="background: #667eea;"></button>
<button type="button" class="color-option" data-color="#f093fb" onclick="selectColor(this)" style="background: #f093fb;"></button>
<button type="button" class="color-option" data-color="#4facfe" onclick="selectColor(this)" style="background: #4facfe;"></button>
<button type="button" class="color-option" data-color="#43e97b" onclick="selectColor(this)" style="background: #43e97b;"></button>
<button type="button" class="color-option" data-color="#fa709a" onclick="selectColor(this)" style="background: #fa709a;"></button>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline" onclick="closeColorModal()">취소</button>
<button type="button" class="btn btn-primary" onclick="applyColor()">적용</button>
</div>
</div>
</div>
<script src="js/common.js"></script>
<script>
(function() {
'use strict';
let currentContent = null;
let selectedColor = '#667eea';
const eventData = AppState.currentEvent || {};
window.editContent = function(type) {
currentContent = type;
document.getElementById('editorSection').style.display = 'block';
document.getElementById('skipSection').style.display = 'none';
document.getElementById('saveButton').style.display = 'flex';
const typeNames = {
image: '이미지',
video: '영상',
sns: 'SNS'
};
document.getElementById('editor-title').textContent = typeNames[type] + ' 편집';
// Load content preview
if (type === 'video' && eventData.video) {
document.getElementById('previewText').textContent = eventData.video.overlayText || '🎉 연말 대박 이벤트!';
} else if (eventData.promotionMessage) {
document.getElementById('previewText').textContent = eventData.promotionMessage.text || '🎉 연말 대박 이벤트!';
}
};
window.closeEditor = function() {
document.getElementById('editorSection').style.display = 'none';
document.getElementById('skipSection').style.display = 'block';
document.getElementById('saveButton').style.display = 'none';
currentContent = null;
};
window.editText = function() {
const modal = document.getElementById('textEditModal');
const input = document.getElementById('editTextInput');
input.value = document.getElementById('previewText').textContent;
Modal.open(modal);
};
window.closeTextModal = function() {
Modal.close(document.getElementById('textEditModal'));
};
window.applyText = function() {
const text = document.getElementById('editTextInput').value.trim();
if (text) {
document.getElementById('previewText').textContent = text;
Toast.success('텍스트가 변경되었습니다.');
}
closeTextModal();
};
window.editColor = function() {
Modal.open(document.getElementById('colorEditModal'));
};
window.closeColorModal = function() {
Modal.close(document.getElementById('colorEditModal'));
};
window.selectColor = function(btn) {
selectedColor = btn.dataset.color;
document.querySelectorAll('.color-option').forEach(el => {
el.style.border = '2px solid transparent';
});
btn.style.border = '2px solid var(--color-gray-900)';
};
window.applyColor = function() {
document.getElementById('previewArea').style.background = 'linear-gradient(135deg, ' + selectedColor + ' 0%, ' + adjustColor(selectedColor) + ' 100%)';
Toast.success('색상이 변경되었습니다.');
closeColorModal();
};
function adjustColor(hex) {
const r = parseInt(hex.slice(1,3), 16);
const g = parseInt(hex.slice(3,5), 16);
const b = parseInt(hex.slice(5,7), 16);
return '#' + ((r-40).toString(16).padStart(2,'0')) + ((g-40).toString(16).padStart(2,'0')) + ((b-40).toString(16).padStart(2,'0'));
}
window.editSize = function() {
const current = document.getElementById('previewText');
const currentSize = parseInt(window.getComputedStyle(current).fontSize);
const newSize = currentSize === 24 ? 32 : 24;
current.style.fontSize = newSize + 'px';
Toast.success('크기가 변경되었습니다.');
};
window.revertToOriginal = function() {
if (confirm('원본으로 되돌리시겠습니까?')) {
document.getElementById('previewText').textContent = '🎉 연말 대박 이벤트!';
document.getElementById('previewArea').style.background = 'var(--color-gray-100)';
Toast.success('원본으로 되돌렸습니다.');
}
};
window.cancelEdit = function() {
if (confirm('편집을 취소하시겠습니까?')) {
closeEditor();
}
};
window.saveEdit = function() {
Loading.show('저장 중...');
setTimeout(function() {
Loading.hide();
Toast.success('편집 내용이 저장되었습니다!');
closeEditor();
}, 1000);
};
window.skipEdit = function() {
window.location.href = '14-콘텐츠최종승인.html';
};
})();
</script>
<style>
.content-card {
background: white;
border: 2px solid var(--color-gray-300);
border-radius: var(--radius-md);
padding: var(--spacing-m);
display: flex;
flex-direction: column;
align-items: center;
gap: var(--spacing-s);
cursor: pointer;
transition: all 0.2s ease;
}
.content-card:hover {
border-color: var(--color-primary-main);
transform: translateY(-2px);
}
.content-card-icon {
width: 64px;
height: 64px;
border-radius: var(--radius-md);
display: flex;
align-items: center;
justify-content: center;
}
.color-option {
aspect-ratio: 1;
border-radius: var(--radius-sm);
border: 2px solid transparent;
cursor: pointer;
transition: transform 0.2s ease;
}
.color-option:hover {
transform: scale(1.1);
}
</style>
</body>
</html>

View File

@ -0,0 +1,279 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>콘텐츠 최종 승인 - KT AI 이벤트</title>
<link rel="stylesheet" href="css/common.css">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Pretendard:wght@400;500;600;700&display=swap" rel="stylesheet">
</head>
<body>
<a href="#main-content" class="skip-link">본문으로 건너뛰기</a>
<header class="header" role="banner">
<div class="header-content">
<button type="button" class="btn-icon" onclick="window.history.back()" aria-label="이전 페이지로 돌아가기">
<span class="material-icons">arrow_back</span>
</button>
<h1 class="header-title">콘텐츠 최종 확인</h1>
<div style="width: 40px;"></div>
</div>
</header>
<div class="progress-container" role="progressbar" aria-valuenow="100" aria-valuemin="0" aria-valuemax="100">
<div class="progress-bar" style="width: 100%;"></div>
<div class="progress-label">콘텐츠 5/5: 최종 승인</div>
</div>
<main id="main-content" class="container" role="main" style="padding-bottom: 100px;">
<section class="section">
<div class="alert alert-success" role="status">
<div style="display: flex; align-items: center; gap: var(--spacing-s);">
<span class="material-icons" style="font-size: 24px;">check_circle</span>
<span class="body-l" style="font-weight: 600;">콘텐츠 생성 완료!</span>
</div>
</div>
</section>
<section class="section" aria-labelledby="contents-title">
<h2 id="contents-title" class="h3">생성된 콘텐츠 (갤러리)</h2>
<div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--spacing-s); margin-bottom: var(--spacing-l);">
<button type="button" class="gallery-item" onclick="viewContent('image', 1)">
<div style="aspect-ratio: 1; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: var(--radius-sm); display: flex; align-items: center; justify-content: center;">
<span class="material-icons" style="color: white; font-size: 32px;">image</span>
</div>
<div class="caption" style="margin-top: 4px; text-align: center;">이미지1</div>
</button>
<button type="button" class="gallery-item" onclick="viewContent('image', 2)">
<div style="aspect-ratio: 1; background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); border-radius: var(--radius-sm); display: flex; align-items: center; justify-content: center;">
<span class="material-icons" style="color: white; font-size: 32px;">image</span>
</div>
<div class="caption" style="margin-top: 4px; text-align: center;">이미지2</div>
</button>
<button type="button" class="gallery-item" onclick="viewContent('image', 3)">
<div style="aspect-ratio: 1; background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); border-radius: var(--radius-sm); display: flex; align-items: center; justify-content: center;">
<span class="material-icons" style="color: white; font-size: 32px;">image</span>
</div>
<div class="caption" style="margin-top: 4px; text-align: center;">이미지3</div>
</button>
<button type="button" class="gallery-item" onclick="viewContent('video', 1)">
<div style="aspect-ratio: 1; background: linear-gradient(135deg, #fa709a 0%, #fee140 100%); border-radius: var(--radius-sm); display: flex; align-items: center; justify-content: center;">
<span class="material-icons" style="color: white; font-size: 32px;">play_circle</span>
</div>
<div class="caption" style="margin-top: 4px; text-align: center;">15초영상</div>
</button>
<button type="button" class="gallery-item" onclick="viewContent('sns', 1)">
<div style="aspect-ratio: 1; background: linear-gradient(135deg, #30cfd0 0%, #330867 100%); border-radius: var(--radius-sm); display: flex; align-items: center; justify-content: center;">
<span class="material-icons" style="color: white; font-size: 32px;">share</span>
</div>
<div class="caption" style="margin-top: 4px; text-align: center;">SNS세트</div>
</button>
<button type="button" class="gallery-item" onclick="viewContent('qr', 1)">
<div style="aspect-ratio: 1; background: linear-gradient(135deg, #a8edea 0%, #fed6e3 100%); border-radius: var(--radius-sm); display: flex; align-items: center; justify-content: center;">
<span class="material-icons" style="color: #333; font-size: 32px;">qr_code</span>
</div>
<div class="caption" style="margin-top: 4px; text-align: center;">QR포스터</div>
</button>
</div>
<button type="button" class="btn btn-outline btn-lg" onclick="downloadAll()" style="width: 100%;">
<span class="material-icons">download</span>
<span>전체 다운로드</span>
</button>
</section>
<section class="section">
<div class="card">
<div class="card-body">
<div style="display: flex; align-items: center; gap: var(--spacing-s); margin-bottom: var(--spacing-s);">
<span class="material-icons" style="color: var(--color-info);">schedule</span>
<span class="body-m" style="font-weight: 600;">콘텐츠 생성 시간</span>
</div>
<div style="text-align: center; padding: var(--spacing-l);">
<div class="h2" style="color: var(--color-primary-main); margin-bottom: var(--spacing-xs);" id="elapsedTime">7분</div>
<div class="body-s" style="color: var(--color-gray-600);">목표: 5-8분 이내</div>
<div class="body-s" style="color: var(--color-success); font-weight: 600; margin-top: var(--spacing-xs);" id="timeStatus">✓ 목표 달성!</div>
</div>
</div>
</div>
</section>
</main>
<div class="fixed-bottom-button">
<button type="button" class="btn btn-outline btn-lg" onclick="goBack()" style="flex: 1;">
<span class="material-icons">arrow_back</span>
<span>수정</span>
</button>
<button type="button" class="btn btn-primary btn-lg" onclick="approve()" style="flex: 2;">
<span class="material-icons">check</span>
<span>승인하기</span>
</button>
</div>
<div id="viewModal" class="modal" role="dialog" aria-modal="true" style="display: none;">
<div class="modal-content" style="max-width: 90vw; max-height: 90vh;">
<div class="modal-header">
<h3 class="h4" id="viewTitle">콘텐츠 미리보기</h3>
<button type="button" class="btn-icon" onclick="closeView()">
<span class="material-icons">close</span>
</button>
</div>
<div class="modal-body" id="viewContent" style="padding: 0; max-height: 70vh; overflow: auto;">
<!-- Content preview will be inserted here -->
</div>
</div>
</div>
<script src="js/common.js"></script>
<script>
(function() {
'use strict';
const eventData = AppState.currentEvent || {};
const storeData = AppState.store || {};
// Calculate elapsed time
const startTime = eventData.startTime || Date.now();
const elapsedMinutes = Math.floor((Date.now() - startTime) / 60000);
const displayTime = elapsedMinutes > 0 ? elapsedMinutes + '분' : '30초';
document.addEventListener('DOMContentLoaded', function() {
document.getElementById('elapsedTime').textContent = displayTime;
const targetMin = 5;
const targetMax = 8;
const statusEl = document.getElementById('timeStatus');
if (elapsedMinutes <= targetMax) {
statusEl.textContent = '✓ 목표 달성!';
statusEl.style.color = 'var(--color-success)';
} else {
statusEl.textContent = '⚠ 목표 초과';
statusEl.style.color = 'var(--color-warning)';
}
});
window.viewContent = function(type, index) {
const modal = document.getElementById('viewModal');
const title = document.getElementById('viewTitle');
const content = document.getElementById('viewContent');
const typeNames = {
image: '이미지',
video: '영상',
sns: 'SNS',
qr: 'QR 포스터'
};
title.textContent = typeNames[type] + ' ' + (index || '') + ' 미리보기';
let html = '';
if (type === 'image') {
const gradients = [
'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)',
'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)'
];
html = `
<div style="aspect-ratio: 1; background: ${gradients[index-1]}; display: flex; align-items: center; justify-content: center; color: white; padding: var(--spacing-xl); text-align: center;">
<div>
<div class="h2" style="color: white; margin-bottom: var(--spacing-m);">🎁</div>
<div class="h3" style="color: white;">${eventData.promotionMessage?.text?.split('\n')[0] || '이벤트 참여하세요!'}</div>
</div>
</div>
`;
} else if (type === 'video') {
html = `
<div style="aspect-ratio: 16/9; background: linear-gradient(135deg, #fa709a 0%, #fee140 100%); display: flex; align-items: center; justify-content: center; position: relative;">
<div style="text-align: center; color: white;">
<span class="material-icons" style="font-size: 64px; color: white; margin-bottom: var(--spacing-m);">play_circle</span>
<div class="h4" style="color: white;">15초 홍보 영상</div>
</div>
</div>
`;
} else if (type === 'sns') {
html = `
<div style="padding: var(--spacing-l);">
<div style="margin-bottom: var(--spacing-m);">
<div class="body-m" style="font-weight: 600; margin-bottom: var(--spacing-s);">Instagram</div>
<div style="aspect-ratio: 1; background: linear-gradient(135deg, #30cfd0 0%, #330867 100%); border-radius: var(--radius-md);"></div>
</div>
<div>
<div class="body-m" style="font-weight: 600; margin-bottom: var(--spacing-s);">Naver Blog</div>
<div style="aspect-ratio: 4/3; background: linear-gradient(135deg, #a8edea 0%, #fed6e3 100%); border-radius: var(--radius-md);"></div>
</div>
</div>
`;
} else if (type === 'qr') {
html = `
<div style="padding: var(--spacing-xl); background: white; text-align: center;">
<div class="h4" style="margin-bottom: var(--spacing-l);">${storeData.name || '우리가게'} 이벤트</div>
<div style="width: 200px; height: 200px; margin: 0 auto var(--spacing-l); background: white; border: 2px solid var(--color-gray-300);">
<div style="width: 100%; height: 100%; background: linear-gradient(45deg, #000 25%, transparent 25%, transparent 75%, #000 75%, #000), linear-gradient(45deg, #000 25%, transparent 25%, transparent 75%, #000 75%, #000); background-size: 20px 20px; background-position: 0 0, 10px 10px;"></div>
</div>
<div class="body-m" style="color: var(--color-gray-600);">QR 코드를 스캔하세요</div>
</div>
`;
}
content.innerHTML = html;
Modal.open(modal);
};
window.closeView = function() {
Modal.close(document.getElementById('viewModal'));
};
window.downloadAll = function() {
Loading.show('전체 콘텐츠 다운로드 중...');
setTimeout(function() {
Loading.hide();
Toast.success('📦 모든 콘텐츠가 다운로드되었습니다!');
}, 2000);
};
window.goBack = function() {
if (confirm('수정 화면으로 돌아가시겠습니까?')) {
window.location.href = '13-콘텐츠편집.html';
}
};
window.approve = function() {
if (confirm('콘텐츠를 승인하고 배포 단계로 진행하시겠습니까?')) {
eventData.contentApproved = true;
eventData.contentApprovedAt = new Date().toISOString();
AppState.currentEvent = eventData;
AppState.save();
Toast.success('콘텐츠가 승인되었습니다!');
setTimeout(function() {
window.location.href = '16-배포진행상태.html';
}, 1000);
}
};
})();
</script>
<style>
.gallery-item {
background: transparent;
border: none;
cursor: pointer;
transition: transform 0.2s ease;
}
.gallery-item:hover {
transform: scale(1.05);
}
</style>
</body>
</html>

View File

@ -0,0 +1,386 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>배포 채널 선택 - KT AI 이벤트</title>
<link rel="stylesheet" href="css/common.css">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Pretendard:wght@400;500;600;700&display=swap" rel="stylesheet">
</head>
<body>
<a href="#main-content" class="skip-link">본문으로 건너뛰기</a>
<header class="header" role="banner">
<div class="header-content">
<button type="button" class="btn-icon" onclick="window.history.back()" aria-label="이전 페이지로 돌아가기">
<span class="material-icons">arrow_back</span>
</button>
<h1 class="header-title">배포 채널 선택</h1>
<div style="width: 40px;"></div>
</div>
</header>
<div class="progress-indicator" role="progressbar" aria-valuenow="33" aria-valuemin="0" aria-valuemax="100">
<div class="progress-text">배포 1/3: 채널 선택</div>
<div class="progress-bar">
<div class="progress-fill" style="width: 33%;"></div>
</div>
</div>
<main id="main-content" class="container" role="main">
<section class="section" aria-labelledby="kt-channels-title">
<h2 id="kt-channels-title" class="h3">KT 채널</h2>
<div class="card">
<div class="card-body">
<div style="display: flex; flex-direction: column; gap: var(--spacing-m);">
<div class="channel-item" onclick="toggleChannel('woori')">
<div style="display: flex; align-items: center; gap: var(--spacing-m); flex: 1;">
<input type="checkbox" id="woori" class="checkbox" aria-label="우리동네TV 선택">
<div style="flex: 1;">
<div class="body-l" style="font-weight: 600; margin-bottom: 4px;">
<span class="material-icons" style="vertical-align: middle; margin-right: 4px; color: var(--color-success);">tv</span>
우리동네TV
</div>
<div class="body-s" style="color: var(--color-gray-600);">지역 주변 5km 내 자동 노출</div>
</div>
</div>
<button type="button" class="btn btn-text btn-sm" onclick="event.stopPropagation(); openSettings('woori')" id="wooriSettings" disabled>
<span>설정</span>
<span class="material-icons">chevron_right</span>
</button>
</div>
<div class="channel-item" onclick="toggleChannel('ringo')">
<div style="display: flex; align-items: center; gap: var(--spacing-m); flex: 1;">
<input type="checkbox" id="ringo" class="checkbox" aria-label="링고비즈 연결음 선택">
<div style="flex: 1;">
<div class="body-l" style="font-weight: 600; margin-bottom: 4px;">
<span class="material-icons" style="vertical-align: middle; margin-right: 4px; color: var(--color-warning);">call</span>
링고비즈 연결음
</div>
<div class="body-s" style="color: var(--color-gray-600);">매장 전화 연결음으로 이벤트 홍보</div>
</div>
</div>
<button type="button" class="btn btn-text btn-sm" onclick="event.stopPropagation(); openSettings('ringo')" id="ringoSettings" disabled>
<span>설정</span>
<span class="material-icons">chevron_right</span>
</button>
</div>
<div class="channel-item" onclick="toggleChannel('genie')">
<div style="display: flex; align-items: center; gap: var(--spacing-m); flex: 1;">
<input type="checkbox" id="genie" class="checkbox" aria-label="지니TV 광고 선택">
<div style="flex: 1;">
<div class="body-l" style="font-weight: 600; margin-bottom: 4px;">
<span class="material-icons" style="vertical-align: middle; margin-right: 4px; color: var(--color-secondary-main);">connected_tv</span>
지니TV 광고
</div>
<div class="body-s" style="color: var(--color-gray-600);">TV 광고로 대규모 노출</div>
</div>
</div>
<button type="button" class="btn btn-text btn-sm" onclick="event.stopPropagation(); openSettings('genie')" id="genieSettings" disabled>
<span>설정</span>
<span class="material-icons">chevron_right</span>
</button>
</div>
</div>
</div>
</div>
</section>
<section class="section" aria-labelledby="sns-channels-title">
<h2 id="sns-channels-title" class="h3">SNS 채널</h2>
<div class="card">
<div class="card-body">
<div style="display: flex; flex-direction: column; gap: var(--spacing-m);">
<div class="channel-item" onclick="toggleChannel('instagram')">
<div style="display: flex; align-items: center; gap: var(--spacing-m); flex: 1;">
<input type="checkbox" id="instagram" class="checkbox" checked disabled aria-label="Instagram (필수 채널)">
<div style="flex: 1;">
<div class="body-l" style="font-weight: 600; margin-bottom: 4px;">
<span class="material-icons" style="vertical-align: middle; margin-right: 4px; color: #E4405F;">photo_camera</span>
Instagram (필수)
</div>
<div class="body-s" style="color: var(--color-success); font-weight: 600;">
<span class="material-icons" style="font-size: 14px; vertical-align: middle;">check_circle</span>
계정 연동 완료
</div>
</div>
</div>
</div>
<div class="channel-item" onclick="toggleChannel('facebook')">
<div style="display: flex; align-items: center; gap: var(--spacing-m); flex: 1;">
<input type="checkbox" id="facebook" class="checkbox" aria-label="Facebook 선택">
<div style="flex: 1;">
<div class="body-l" style="font-weight: 600; margin-bottom: 4px;">
<span class="material-icons" style="vertical-align: middle; margin-right: 4px; color: #1877F2;">facebook</span>
Facebook
</div>
<div class="body-s" style="color: var(--color-gray-600);">페이스북 페이지에 자동 게시</div>
</div>
</div>
<button type="button" class="btn btn-text btn-sm" onclick="event.stopPropagation(); openSettings('facebook')" id="facebookSettings" disabled>
<span>설정</span>
<span class="material-icons">chevron_right</span>
</button>
</div>
<div class="channel-item" onclick="toggleChannel('naver')">
<div style="display: flex; align-items: center; gap: var(--spacing-m); flex: 1;">
<input type="checkbox" id="naver" class="checkbox" aria-label="Naver Blog 선택">
<div style="flex: 1;">
<div class="body-l" style="font-weight: 600; margin-bottom: 4px;">
<span class="material-icons" style="vertical-align: middle; margin-right: 4px; color: #03C75A;">article</span>
Naver Blog
</div>
<div class="body-s" style="color: var(--color-gray-600);">네이버 블로그 자동 포스팅</div>
</div>
</div>
<button type="button" class="btn btn-text btn-sm" onclick="event.stopPropagation(); openSettings('naver')" id="naverSettings" disabled>
<span>설정</span>
<span class="material-icons">chevron_right</span>
</button>
</div>
<div class="channel-item" onclick="toggleChannel('kakao')">
<div style="display: flex; align-items: center; gap: var(--spacing-m); flex: 1;">
<input type="checkbox" id="kakao" class="checkbox" aria-label="카카오톡 채널 선택">
<div style="flex: 1;">
<div class="body-l" style="font-weight: 600; margin-bottom: 4px;">
<span class="material-icons" style="vertical-align: middle; margin-right: 4px; color: #FEE500;">chat</span>
카카오톡 채널
</div>
<div class="body-s" style="color: var(--color-gray-600);">카카오톡 채널 메시지 발송</div>
</div>
</div>
<button type="button" class="btn btn-text btn-sm" onclick="event.stopPropagation(); openSettings('kakao')" id="kakaoSettings" disabled>
<span>설정</span>
<span class="material-icons">chevron_right</span>
</button>
</div>
</div>
</div>
</div>
</section>
<section class="section" aria-labelledby="offline-title">
<h2 id="offline-title" class="h3">오프라인 채널</h2>
<div class="card">
<div class="card-body">
<div class="channel-item" onclick="toggleChannel('qr')">
<div style="display: flex; align-items: center; gap: var(--spacing-m); flex: 1;">
<input type="checkbox" id="qr" class="checkbox" checked aria-label="QR 포스터 (기본 선택)">
<div style="flex: 1;">
<div class="body-l" style="font-weight: 600; margin-bottom: 4px;">
<span class="material-icons" style="vertical-align: middle; margin-right: 4px; color: var(--color-primary-main);">qr_code_2</span>
QR 포스터 (기본)
</div>
<div class="body-s" style="color: var(--color-gray-600);">매장 내 부착용 QR 포스터</div>
</div>
</div>
</div>
</div>
</div>
</section>
<section class="section" aria-labelledby="schedule-title">
<h2 id="schedule-title" class="h3">배포 일정</h2>
<div class="card">
<div class="card-body">
<div style="display: flex; flex-direction: column; gap: var(--spacing-m);">
<label class="radio-item" onclick="selectSchedule('now')">
<input type="radio" name="schedule" value="now" checked>
<div>
<div class="body-l" style="font-weight: 600;">즉시 배포</div>
<div class="body-s" style="color: var(--color-gray-600);">승인 후 즉시 모든 채널에 배포</div>
</div>
</label>
<label class="radio-item" onclick="selectSchedule('scheduled')">
<input type="radio" name="schedule" value="scheduled">
<div>
<div class="body-l" style="font-weight: 600;">예약 배포</div>
<div class="body-s" style="color: var(--color-gray-600);">지정한 날짜/시간에 자동 배포</div>
</div>
</label>
</div>
<div id="scheduleSettings" style="display: none; margin-top: var(--spacing-m); padding-top: var(--spacing-m); border-top: 1px solid var(--color-gray-300);">
<div class="form-group">
<label for="scheduleDate" class="form-label">배포 날짜</label>
<input type="date" id="scheduleDate" class="input-field" aria-label="배포 날짜 선택">
</div>
<div class="form-group">
<label for="scheduleTime" class="form-label">배포 시간</label>
<input type="time" id="scheduleTime" class="input-field" aria-label="배포 시간 선택">
</div>
</div>
</div>
</div>
</section>
<section class="section">
<div class="alert alert-info" role="status">
<div style="display: flex; align-items: flex-start; gap: var(--spacing-s);">
<span class="material-icons">info</span>
<div class="body-s">
선택한 채널: <strong id="selectedCount">2개</strong><br>
Instagram은 필수 채널이며, QR 포스터는 기본 제공됩니다.
</div>
</div>
</div>
<button type="button" class="btn btn-primary btn-lg" onclick="goToNext()" style="width: 100%;">
<span>다음 단계</span>
<span class="material-icons">arrow_forward</span>
</button>
</section>
</main>
<script src="js/common.js"></script>
<script>
(function() {
'use strict';
function updateSelectedCount() {
const checkboxes = document.querySelectorAll('.checkbox:checked:not([disabled])');
const count = checkboxes.length + 1; // +1 for Instagram (required)
document.getElementById('selectedCount').textContent = count + '개';
}
window.toggleChannel = function(channelId) {
const checkbox = document.getElementById(channelId);
if (checkbox.disabled) return;
checkbox.checked = !checkbox.checked;
const settingsBtn = document.getElementById(channelId + 'Settings');
if (settingsBtn) {
settingsBtn.disabled = !checkbox.checked;
}
updateSelectedCount();
};
window.openSettings = function(channelId) {
Toast.show(channelId.toUpperCase() + ' 채널 설정');
};
window.selectSchedule = function(type) {
const scheduleSettings = document.getElementById('scheduleSettings');
if (type === 'scheduled') {
scheduleSettings.style.display = 'block';
// Set default values
const tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
document.getElementById('scheduleDate').value = tomorrow.toISOString().split('T')[0];
document.getElementById('scheduleTime').value = '09:00';
} else {
scheduleSettings.style.display = 'none';
}
};
window.goToNext = function() {
const selectedChannels = [];
document.querySelectorAll('.checkbox:checked').forEach(function(checkbox) {
selectedChannels.push(checkbox.id);
});
const schedule = document.querySelector('input[name="schedule"]:checked').value;
if (schedule === 'scheduled') {
const date = document.getElementById('scheduleDate').value;
const time = document.getElementById('scheduleTime').value;
if (!date || !time) {
Toast.error('배포 날짜와 시간을 선택해주세요');
return;
}
}
Toast.success('채널 선택이 완료되었습니다. (' + selectedChannels.length + '개 채널)');
};
// Initialize
updateSelectedCount();
})();
</script>
<style>
.progress-indicator {
background: white;
padding: var(--spacing-m) var(--spacing-l);
border-bottom: 1px solid var(--color-gray-300);
}
.progress-text {
font-size: 14px;
color: var(--color-gray-600);
margin-bottom: var(--spacing-s);
}
.progress-bar {
height: 4px;
background: var(--color-gray-200);
border-radius: var(--radius-full);
overflow: hidden;
}
.progress-fill {
height: 100%;
background: var(--color-primary-main);
transition: width 0.3s ease;
}
.channel-item {
padding: var(--spacing-m);
border: 1px solid var(--color-gray-300);
border-radius: var(--radius-md);
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: space-between;
}
.channel-item:hover {
border-color: var(--color-primary-main);
background: var(--color-gray-50);
}
.checkbox {
width: 20px;
height: 20px;
cursor: pointer;
}
.radio-item {
padding: var(--spacing-m);
border: 1px solid var(--color-gray-300);
border-radius: var(--radius-md);
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: flex-start;
gap: var(--spacing-m);
}
.radio-item:hover {
border-color: var(--color-primary-main);
background: var(--color-gray-50);
}
.radio-item input[type="radio"] {
margin-top: 4px;
cursor: pointer;
}
</style>
</body>
</html>

View File

@ -0,0 +1,364 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>배포 진행 중 - KT AI 이벤트</title>
<link rel="stylesheet" href="css/common.css">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Pretendard:wght@400;500;600;700&display=swap" rel="stylesheet">
</head>
<body>
<a href="#main-content" class="skip-link">본문으로 건너뛰기</a>
<header class="header" role="banner">
<div class="header-content">
<button type="button" class="btn-icon" onclick="window.history.back()" aria-label="이전 페이지로 돌아가기">
<span class="material-icons">arrow_back</span>
</button>
<h1 class="header-title">배포 진행 중</h1>
<div style="width: 40px;"></div>
</div>
</header>
<div class="progress-container" role="progressbar" aria-valuenow="66" aria-valuemin="0" aria-valuemax="100">
<div class="progress-bar" style="width: 66%;"></div>
<div class="progress-label">배포 2/3: 진행 상태</div>
</div>
<main id="main-content" class="container" role="main" style="padding-bottom: 100px;">
<section class="section">
<div style="text-align: center; margin-bottom: var(--spacing-xl);">
<div style="font-size: 48px; margin-bottom: var(--spacing-m);">🚀</div>
<h2 class="h3">배포 진행 중...</h2>
</div>
</section>
<section class="section" aria-live="polite">
<!-- Instagram -->
<div class="card" id="statusInstagram">
<div class="card-body">
<div style="display: flex; align-items: center; gap: var(--spacing-m);">
<div class="status-icon" style="background: linear-gradient(45deg, #f09433 0%,#e6683c 25%,#dc2743 50%,#cc2366 75%,#bc1888 100%);">
<span class="material-icons" style="color: white;">check</span>
</div>
<div style="flex: 1;">
<div class="body-m" style="font-weight: 600; margin-bottom: 4px;">Instagram</div>
<div class="caption" style="color: var(--color-success);">✅ 배포 완료 (3초)</div>
</div>
<button type="button" class="btn-text btn-sm" onclick="viewPost('instagram')">
게시물 보기
</button>
</div>
</div>
</div>
<!-- 우리동네TV -->
<div class="card" id="statusTV">
<div class="card-body">
<div style="display: flex; align-items: center; gap: var(--spacing-m);">
<div class="status-icon status-loading" style="background: var(--color-secondary-main);">
<span class="material-icons rotating" style="color: white;">sync</span>
</div>
<div style="flex: 1;">
<div class="body-m" style="font-weight: 600; margin-bottom: 4px;">우리동네TV</div>
<div class="caption" style="color: var(--color-info);">🔄 배포 중... (<span id="tvTime">15</span>초)</div>
<div class="progress-bar-container" style="margin-top: var(--spacing-xs);">
<div class="progress-bar-fill" id="tvProgress" style="width: 40%;"></div>
</div>
</div>
</div>
</div>
</div>
<!-- 지니TV -->
<div class="card" id="statusGenie">
<div class="card-body">
<div style="display: flex; align-items: center; gap: var(--spacing-m);">
<div class="status-icon" style="background: var(--color-gray-400);">
<span class="material-icons" style="color: white;">schedule</span>
</div>
<div style="flex: 1;">
<div class="body-m" style="font-weight: 600; margin-bottom: 4px;">지니TV</div>
<div class="caption" style="color: var(--color-gray-600);">⏳ 대기 중...</div>
</div>
</div>
</div>
</div>
<!-- Naver Blog -->
<div class="card" id="statusBlog">
<div class="card-body">
<div style="display: flex; align-items: center; gap: var(--spacing-m);">
<div class="status-icon" style="background: var(--color-error);">
<span class="material-icons" style="color: white;">error</span>
</div>
<div style="flex: 1;">
<div class="body-m" style="font-weight: 600; margin-bottom: 4px;">Naver Blog</div>
<div class="caption" style="color: var(--color-error);">❌ 배포 실패 (재시도 중)</div>
<div class="caption" style="color: var(--color-gray-600); margin-top: 4px;">1/3 재시도</div>
</div>
<button type="button" class="btn-text btn-sm" onclick="retryDeploy('blog')">
수동 재시도
</button>
</div>
</div>
</div>
</section>
<section class="section">
<div class="card">
<div class="card-body">
<div class="info-grid">
<div class="info-item" style="text-align: center;">
<span class="info-label">전체 진행률</span>
<span class="h3" style="color: var(--color-primary-main);" id="overallProgress">50%</span>
</div>
<div class="info-item" style="text-align: center;">
<span class="info-label">예상 소요</span>
<span class="h3" style="color: var(--color-info);" id="remainingTime">45초</span>
</div>
</div>
</div>
</div>
</section>
<div style="display: flex; gap: var(--spacing-s);">
<button type="button" class="btn btn-outline btn-lg" onclick="cancelDeploy()" style="flex: 1;">
취소
</button>
<button type="button" class="btn btn-outline btn-lg" onclick="refreshStatus()" style="flex: 1;">
<span class="material-icons">refresh</span>
<span>새로고침</span>
</button>
</div>
</main>
<div class="fixed-bottom-button" id="nextButton" style="display: none;">
<button type="button" class="btn btn-primary btn-lg" onclick="goToNext()">
<span class="material-icons">check_circle</span>
<span>완료 확인</span>
</button>
</div>
<script src="js/common.js"></script>
<script>
(function() {
'use strict';
let deploymentProgress = {
instagram: 100,
tv: 40,
genie: 0,
blog: -1
};
let elapsedTime = 0;
const totalChannels = 4;
// Start deployment simulation
document.addEventListener('DOMContentLoaded', function() {
simulateDeployment();
});
function simulateDeployment() {
const interval = setInterval(function() {
elapsedTime += 5;
// Update TV progress
if (deploymentProgress.tv < 100 && deploymentProgress.tv >= 0) {
deploymentProgress.tv += 15;
if (deploymentProgress.tv >= 100) {
deploymentProgress.tv = 100;
updateChannelStatus('tv', 'complete');
} else {
updateChannelProgress('tv', deploymentProgress.tv);
}
}
// Start Genie after TV
if (deploymentProgress.tv === 100 && deploymentProgress.genie === 0) {
deploymentProgress.genie = 10;
updateChannelStatus('genie', 'progress');
}
// Update Genie progress
if (deploymentProgress.genie > 0 && deploymentProgress.genie < 100) {
deploymentProgress.genie += 20;
if (deploymentProgress.genie >= 100) {
deploymentProgress.genie = 100;
updateChannelStatus('genie', 'complete');
} else {
updateChannelProgress('genie', deploymentProgress.genie);
}
}
// Retry blog after some time
if (elapsedTime === 20 && deploymentProgress.blog === -1) {
deploymentProgress.blog = 0;
updateChannelStatus('blog', 'retry');
}
if (deploymentProgress.blog >= 0 && deploymentProgress.blog < 100) {
deploymentProgress.blog += 25;
if (deploymentProgress.blog >= 100) {
deploymentProgress.blog = 100;
updateChannelStatus('blog', 'complete');
} else {
updateChannelProgress('blog', deploymentProgress.blog);
}
}
// Calculate overall progress
const completed = Object.values(deploymentProgress).filter(v => v === 100).length;
const overall = Math.round((completed / totalChannels) * 100);
document.getElementById('overallProgress').textContent = overall + '%';
const remaining = Math.max(0, 60 - elapsedTime);
document.getElementById('remainingTime').textContent = remaining + '초';
// Check if all complete
if (completed === totalChannels) {
clearInterval(interval);
showCompletion();
}
}, 5000);
}
function updateChannelProgress(channel, progress) {
const progressBar = document.getElementById(channel + 'Progress');
const timeEl = document.getElementById(channel + 'Time');
if (progressBar) {
progressBar.style.width = progress + '%';
}
if (timeEl) {
const remaining = Math.round((100 - progress) / 100 * 30);
timeEl.textContent = remaining;
}
}
function updateChannelStatus(channel, status) {
const card = document.getElementById('status' + capitalize(channel));
if (!card) return;
const icon = card.querySelector('.status-icon .material-icons');
const statusText = card.querySelector('.caption');
const statusIcon = card.querySelector('.status-icon');
if (status === 'complete') {
icon.textContent = 'check';
icon.classList.remove('rotating');
statusIcon.style.background = 'var(--color-success)';
statusText.textContent = '✅ 배포 완료';
statusText.style.color = 'var(--color-success)';
// Remove progress bar if exists
const progressBar = card.querySelector('.progress-bar-container');
if (progressBar) progressBar.remove();
} else if (status === 'progress') {
icon.textContent = 'sync';
icon.classList.add('rotating');
statusIcon.style.background = 'var(--color-secondary-main)';
statusText.innerHTML = '🔄 배포 중... (<span id="' + channel + 'Time">30</span>초)';
statusText.style.color = 'var(--color-info)';
// Add progress bar
const progressHTML = `<div class="progress-bar-container" style="margin-top: var(--spacing-xs);">
<div class="progress-bar-fill" id="${channel}Progress" style="width: 10%;"></div>
</div>`;
statusText.insertAdjacentHTML('afterend', progressHTML);
} else if (status === 'retry') {
icon.textContent = 'sync';
icon.classList.add('rotating');
statusIcon.style.background = 'var(--color-warning)';
statusText.textContent = '🔄 재시도 중...';
statusText.style.color = 'var(--color-warning)';
}
}
function capitalize(str) {
if (str === 'tv') return 'TV';
if (str === 'genie') return 'Genie';
if (str === 'blog') return 'Blog';
return str.charAt(0).toUpperCase() + str.slice(1);
}
function showCompletion() {
document.getElementById('nextButton').style.display = 'block';
Toast.success('🎉 모든 채널 배포가 완료되었습니다!');
}
window.viewPost = function(channel) {
Toast.show('📱 ' + capitalize(channel) + ' 게시물을 엽니다...');
// In real implementation, would open actual post URL
};
window.retryDeploy = function(channel) {
Loading.show('재시도 중...');
setTimeout(function() {
Loading.hide();
deploymentProgress[channel] = 0;
updateChannelStatus(channel, 'retry');
Toast.success('재시도가 시작되었습니다.');
}, 1000);
};
window.refreshStatus = function() {
Toast.show('🔄 상태를 업데이트합니다...');
// In real implementation, would fetch actual status
};
window.cancelDeploy = function() {
if (confirm('배포를 취소하시겠습니까? 진행 중인 작업이 중단됩니다.')) {
Toast.show('배포가 취소되었습니다.');
setTimeout(function() {
window.history.back();
}, 1000);
}
};
window.goToNext = function() {
window.location.href = '17-오프라인자료다운로드.html';
};
})();
</script>
<style>
.status-icon {
width: 48px;
height: 48px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.progress-bar-container {
width: 100%;
height: 4px;
background: var(--color-gray-200);
border-radius: var(--radius-full);
overflow: hidden;
}
.progress-bar-fill {
height: 100%;
background: linear-gradient(90deg, var(--color-primary-main), var(--color-secondary-main));
border-radius: var(--radius-full);
transition: width 0.5s ease;
}
@keyframes rotate {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.rotating {
animation: rotate 2s linear infinite;
}
</style>
</body>
</html>

View File

@ -0,0 +1,166 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>오프라인 자료 다운로드 - KT AI 이벤트</title>
<link rel="stylesheet" href="css/common.css">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Pretendard:wght@400;500;600;700&display=swap" rel="stylesheet">
</head>
<body>
<a href="#main-content" class="skip-link">본문으로 건너뛰기</a>
<header class="header" role="banner">
<div class="header-content">
<button type="button" class="btn-icon" onclick="window.history.back()" aria-label="이전 페이지로 돌아가기">
<span class="material-icons">arrow_back</span>
</button>
<h1 class="header-title">오프라인 자료</h1>
<div style="width: 40px;"></div>
</div>
</header>
<div class="progress-container" role="progressbar" aria-valuenow="100" aria-valuemin="0" aria-valuemax="100">
<div class="progress-bar" style="width: 100%;"></div>
<div class="progress-label">배포 3/3: 자료 다운로드</div>
</div>
<main id="main-content" class="container" role="main" style="padding-bottom: 100px;">
<section class="section">
<div class="alert alert-success" role="status">
<div style="display: flex; align-items: center; gap: var(--spacing-s);">
<span class="material-icons" style="font-size: 24px;">check_circle</span>
<span class="body-l" style="font-weight: 600;">배포 완료!</span>
</div>
</div>
</section>
<section class="section" aria-labelledby="materials-title">
<h2 id="materials-title" class="h3">오프라인 홍보 자료</h2>
<div class="card" onclick="downloadFile('qr-a4')">
<div class="card-body">
<div style="display: flex; align-items: center; gap: var(--spacing-m);">
<div style="width: 48px; height: 48px; background: var(--color-primary-lightest); border-radius: var(--radius-md); display: flex; align-items: center; justify-content: center;">
<span class="material-icons" style="color: var(--color-primary-main);">description</span>
</div>
<div style="flex: 1;">
<div class="body-m" style="font-weight: 600; margin-bottom: 4px;">QR 포스터 (A4)</div>
<div class="caption" style="color: var(--color-gray-600);">2.1MB PDF</div>
</div>
<button type="button" class="btn btn-primary btn-sm" onclick="downloadFile('qr-a4', event)">
다운로드
</button>
</div>
</div>
</div>
<div class="card" onclick="downloadFile('qr-a3')">
<div class="card-body">
<div style="display: flex; align-items: center; gap: var(--spacing-m);">
<div style="width: 48px; height: 48px; background: var(--color-secondary-lightest); border-radius: var(--radius-md); display: flex; align-items: center; justify-content: center;">
<span class="material-icons" style="color: var(--color-secondary-main);">description</span>
</div>
<div style="flex: 1;">
<div class="body-m" style="font-weight: 600; margin-bottom: 4px;">QR 포스터 (A3)</div>
<div class="caption" style="color: var(--color-gray-600);">4.5MB PDF</div>
</div>
<button type="button" class="btn btn-primary btn-sm" onclick="downloadFile('qr-a3', event)">
다운로드
</button>
</div>
</div>
</div>
<div class="card" onclick="downloadFile('qr-image')">
<div class="card-body">
<div style="display: flex; align-items: center; gap: var(--spacing-m);">
<div style="width: 48px; height: 48px; background: var(--color-success-lightest); border-radius: var(--radius-md); display: flex; align-items: center; justify-content: center;">
<span class="material-icons" style="color: var(--color-success);">qr_code</span>
</div>
<div style="flex: 1;">
<div class="body-m" style="font-weight: 600; margin-bottom: 4px;">QR 코드 이미지</div>
<div class="caption" style="color: var(--color-gray-600);">150KB PNG</div>
</div>
<button type="button" class="btn btn-primary btn-sm" onclick="downloadFile('qr-image', event)">
다운로드
</button>
</div>
</div>
</div>
<div class="card" onclick="downloadFile('images')">
<div class="card-body">
<div style="display: flex; align-items: center; gap: var(--spacing-m);">
<div style="width: 48px; height: 48px; background: var(--color-warning-lightest); border-radius: var(--radius-md); display: flex; align-items: center; justify-content: center;">
<span class="material-icons" style="color: var(--color-warning);">collections</span>
</div>
<div style="flex: 1;">
<div class="body-m" style="font-weight: 600; margin-bottom: 4px;">고해상도 이미지 (3종)</div>
<div class="caption" style="color: var(--color-gray-600);">8.3MB ZIP</div>
</div>
<button type="button" class="btn btn-primary btn-sm" onclick="downloadFile('images', event)">
다운로드
</button>
</div>
</div>
</div>
<button type="button" class="btn btn-primary btn-lg" onclick="downloadAll()" style="width: 100%; margin-top: var(--spacing-m);">
<span class="material-icons">download</span>
<span>전체 일괄 다운로드 (ZIP)</span>
</button>
</section>
</main>
<div class="fixed-bottom-button">
<button type="button" class="btn btn-primary btn-lg" onclick="complete()">
<span class="material-icons">check_circle</span>
<span>완료</span>
</button>
</div>
<script src="js/common.js"></script>
<script>
(function() {
'use strict';
window.downloadFile = function(type, event) {
if (event) {
event.stopPropagation();
event.preventDefault();
}
const fileNames = {
'qr-a4': 'QR포스터_A4.pdf',
'qr-a3': 'QR포스터_A3.pdf',
'qr-image': 'QR코드.png',
'images': '홍보이미지_3종.zip'
};
Loading.show('다운로드 중...');
setTimeout(function() {
Loading.hide();
Toast.success(fileNames[type] + ' 다운로드 완료!');
}, 1000);
};
window.downloadAll = function() {
Loading.show('전체 파일 압축 중...');
setTimeout(function() {
Loading.hide();
Toast.success('📦 전체 파일이 다운로드되었습니다!');
}, 2000);
};
window.complete = function() {
Toast.success('이벤트 배포가 완료되었습니다!');
setTimeout(function() {
window.location.href = '20-당첨자명단관리.html';
}, 1000);
};
})();
</script>
</body>
</html>

View File

@ -0,0 +1,432 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>이벤트 관리 대시보드 - KT AI 이벤트</title>
<link rel="stylesheet" href="css/common.css">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Pretendard:wght@400;500;600;700&display=swap" rel="stylesheet">
</head>
<body>
<a href="#main-content" class="skip-link">본문으로 건너뛰기</a>
<header class="header" role="banner">
<div class="header-content">
<button type="button" class="btn-icon" onclick="toggleMenu()" aria-label="메뉴">
<span class="material-icons">menu</span>
</button>
<h1 class="header-title">이벤트 대시보드</h1>
<button type="button" class="btn-icon" aria-label="알림">
<span class="material-icons">notifications</span>
</button>
</div>
</header>
<!-- Tab Navigation -->
<nav class="tabs" role="tablist">
<button type="button" class="tab active" role="tab" aria-selected="true" onclick="showTab('dashboard')">
<span class="material-icons">dashboard</span>
<span>대시보드</span>
</button>
<button type="button" class="tab" role="tab" aria-selected="false" onclick="showTab('winners')">
<span class="material-icons">emoji_events</span>
<span>당첨자</span>
</button>
<button type="button" class="tab" role="tab" aria-selected="false" onclick="showTab('analytics')">
<span class="material-icons">analytics</span>
<span>분석</span>
</button>
</nav>
<main id="main-content" class="container" role="main" style="padding-bottom: 80px;">
<!-- Dashboard Tab -->
<div id="dashboardTab" class="tab-content">
<section class="section">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: var(--spacing-m);">
<div>
<div class="caption" style="color: var(--color-gray-600);">마지막 업데이트: <span id="lastUpdate">15:35</span></div>
</div>
<button type="button" class="btn-text btn-sm" onclick="refresh()">
<span class="material-icons">refresh</span>
<span>새로고침</span>
</button>
</div>
<div class="card">
<div class="card-body">
<div class="h4" style="margin-bottom: var(--spacing-xs);">연말 대박 이벤트</div>
<div class="body-s" style="color: var(--color-primary-main); font-weight: 600;">D-5 (2025-12-31까지)</div>
</div>
</div>
</section>
<section class="section">
<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--spacing-m);">
<div class="card">
<div class="card-body">
<div class="caption" style="color: var(--color-gray-600); margin-bottom: var(--spacing-xs);">총 참여자</div>
<div class="h3" style="color: var(--color-primary-main); margin-bottom: var(--spacing-xs);">1,234명</div>
<div class="caption" style="color: var(--color-success);">+45 (오늘)</div>
</div>
</div>
<div class="card">
<div class="card-body">
<div class="caption" style="color: var(--color-gray-600); margin-bottom: var(--spacing-xs);">총 노출 수</div>
<div class="h3" style="color: var(--color-secondary-main); margin-bottom: var(--spacing-xs);">15,678회</div>
<div class="caption" style="color: var(--color-success);">+230 (1시간)</div>
</div>
</div>
<div class="card">
<div class="card-body">
<div class="caption" style="color: var(--color-gray-600); margin-bottom: var(--spacing-xs);">매출 증가율</div>
<div class="h3" style="color: var(--color-warning); margin-bottom: var(--spacing-xs);">+42%</div>
<div class="caption" style="color: var(--color-gray-600);">(이벤트 전 대비)</div>
</div>
</div>
<div class="card">
<div class="card-body">
<div class="caption" style="color: var(--color-gray-600); margin-bottom: var(--spacing-xs);">예상 ROI</div>
<div class="h3" style="color: var(--color-success); margin-bottom: var(--spacing-xs);">245%</div>
<div class="caption" style="color: var(--color-gray-600);">(투자 대비)</div>
</div>
</div>
</div>
</section>
<section class="section">
<h2 class="h4" style="margin-bottom: var(--spacing-m);">채널별 참여 현황</h2>
<div class="card">
<div class="card-body">
<div style="display: flex; flex-direction: column; gap: var(--spacing-m);">
<div>
<div style="display: flex; justify-content: space-between; margin-bottom: var(--spacing-xs);">
<span class="body-s">QR코드</span>
<span class="body-s" style="font-weight: 600;">45%</span>
</div>
<div class="progress-bar-container">
<div class="progress-bar-fill" style="width: 45%; background: var(--color-primary-main);"></div>
</div>
</div>
<div>
<div style="display: flex; justify-content: space-between; margin-bottom: var(--spacing-xs);">
<span class="body-s">Instagram</span>
<span class="body-s" style="font-weight: 600;">30%</span>
</div>
<div class="progress-bar-container">
<div class="progress-bar-fill" style="width: 30%; background: var(--color-secondary-main);"></div>
</div>
</div>
<div>
<div style="display: flex; justify-content: space-between; margin-bottom: var(--spacing-xs);">
<span class="body-s">우리동네TV</span>
<span class="body-s" style="font-weight: 600;">15%</span>
</div>
<div class="progress-bar-container">
<div class="progress-bar-fill" style="width: 15%; background: var(--color-success);"></div>
</div>
</div>
<div>
<div style="display: flex; justify-content: space-between; margin-bottom: var(--spacing-xs);">
<span class="body-s">Naver Blog</span>
<span class="body-s" style="font-weight: 600;">10%</span>
</div>
<div class="progress-bar-container">
<div class="progress-bar-fill" style="width: 10%; background: var(--color-warning);"></div>
</div>
</div>
</div>
</div>
</div>
</section>
</div>
<!-- Winners Tab -->
<div id="winnersTab" class="tab-content" style="display: none;">
<section class="section">
<div class="card">
<div class="card-body">
<div class="h4" style="margin-bottom: var(--spacing-xs);">연말 대박 이벤트</div>
<div class="body-s" style="color: var(--color-gray-600);">당첨인원: <span style="font-weight: 600; color: var(--color-primary-main);">100명</span></div>
</div>
</div>
</section>
<section class="section">
<input type="text" class="form-input" placeholder="🔍 이름 또는 전화번호 검색" id="searchInput" onkeyup="searchWinners()">
</section>
<section class="section">
<div style="display: flex; gap: var(--spacing-xs); margin-bottom: var(--spacing-m);">
<button type="button" class="btn btn-sm" onclick="filterWinners('all')" id="filterAll" style="background: var(--color-primary-main); color: white;">전체</button>
<button type="button" class="btn btn-outline btn-sm" onclick="filterWinners('pending')" id="filterPending">미지급</button>
<button type="button" class="btn btn-outline btn-sm" onclick="filterWinners('complete')" id="filterComplete">완료</button>
</div>
<div id="winnersList">
<!-- Winners will be rendered here -->
</div>
</section>
<section class="section">
<div style="display: flex; gap: var(--spacing-s);">
<button type="button" class="btn btn-outline btn-lg" onclick="viewAllParticipants()" style="flex: 1;">
<span class="material-icons">people</span>
<span>참여자 전체</span>
</button>
<button type="button" class="btn btn-primary btn-lg" onclick="downloadExcel()" style="flex: 1;">
<span class="material-icons">download</span>
<span>엑셀 다운로드</span>
</button>
</div>
</section>
</div>
<!-- Analytics Tab -->
<div id="analyticsTab" class="tab-content" style="display: none;">
<section class="section">
<h2 class="h4">채널별 성과 분석</h2>
<div class="card">
<div class="card-body">
<div style="aspect-ratio: 16/9; background: var(--color-gray-50); border-radius: var(--radius-md); display: flex; align-items: center; justify-content: center;">
<div style="text-align: center;">
<span class="material-icons" style="font-size: 48px; color: var(--color-gray-400);">bar_chart</span>
<div class="body-s" style="color: var(--color-gray-600); margin-top: var(--spacing-s);">채널별 성과 차트</div>
</div>
</div>
</div>
</div>
</section>
<section class="section">
<h2 class="h4">ROI 분석</h2>
<div class="card">
<div class="card-body">
<div class="info-grid">
<div class="info-item">
<span class="info-label">총 투자 비용</span>
<span class="info-value" style="color: var(--color-primary-main);">500,000원</span>
</div>
<div class="info-item">
<span class="info-label">예상 수익</span>
<span class="info-value" style="color: var(--color-success);">1,225,000원</span>
</div>
<div class="info-item">
<span class="info-label">ROI</span>
<span class="info-value" style="color: var(--color-warning); font-weight: 700;">245%</span>
</div>
<div class="info-item">
<span class="info-label">순이익</span>
<span class="info-value" style="color: var(--color-success);">725,000원</span>
</div>
</div>
</div>
</div>
</section>
<section class="section">
<button type="button" class="btn btn-primary btn-lg" onclick="downloadReport()" style="width: 100%;">
<span class="material-icons">description</span>
<span>분석 리포트 다운로드</span>
</button>
</section>
</div>
</main>
<script src="js/common.js"></script>
<script>
(function() {
'use strict';
const winnersData = [
{ name: '김철수', phone: '010-1234-5678', code: 'A-12345678', date: '2025-12-15', channel: 'QR코드', complete: false },
{ name: '이영희', phone: '010-2345-6789', code: 'A-23456789', date: '2025-12-16', channel: 'Instagram', complete: true, completeDate: '2025-12-20' },
{ name: '박민수', phone: '010-3456-7890', code: 'A-34567890', date: '2025-12-17', channel: '우리동네TV', complete: false },
{ name: '정수진', phone: '010-4567-8901', code: 'A-45678901', date: '2025-12-18', channel: 'Instagram', complete: true, completeDate: '2025-12-21' }
];
let currentFilter = 'all';
document.addEventListener('DOMContentLoaded', function() {
renderWinners();
});
window.showTab = function(tabName) {
// Hide all tabs
document.querySelectorAll('.tab-content').forEach(el => el.style.display = 'none');
document.querySelectorAll('.tab').forEach(el => {
el.classList.remove('active');
el.setAttribute('aria-selected', 'false');
});
// Show selected tab
document.getElementById(tabName + 'Tab').style.display = 'block';
event.target.closest('.tab').classList.add('active');
event.target.closest('.tab').setAttribute('aria-selected', 'true');
};
function renderWinners(filter = 'all', searchTerm = '') {
let filtered = winnersData;
if (filter === 'pending') {
filtered = filtered.filter(w => !w.complete);
} else if (filter === 'complete') {
filtered = filtered.filter(w => w.complete);
}
if (searchTerm) {
filtered = filtered.filter(w =>
w.name.includes(searchTerm) || w.phone.includes(searchTerm)
);
}
const html = filtered.map((winner, index) => `
<div class="card">
<div class="card-body">
<div style="margin-bottom: var(--spacing-m);">
<div class="body-m" style="font-weight: 600; margin-bottom: 4px;">${winner.name}</div>
<div class="body-s" style="color: var(--color-gray-600);">${winner.phone}</div>
<div class="caption" style="color: var(--color-gray-600); margin-top: var(--spacing-xs);">
응모: ${winner.code} | 참여일: ${winner.date}<br>
경로: ${winner.channel}
</div>
</div>
<label style="display: flex; align-items: center; gap: var(--spacing-xs); cursor: pointer;">
<input type="checkbox" ${winner.complete ? 'checked' : ''} onchange="toggleComplete(${index}, this)">
<span class="body-s" style="color: ${winner.complete ? 'var(--color-success)' : 'var(--color-gray-600)'};">
${winner.complete ? '✅ 경품 지급 완료' : '☐ 경품 지급 완료'}
</span>
</label>
${winner.complete ? `<div class="caption" style="color: var(--color-success); margin-top: 4px;">지급일: ${winner.completeDate}</div>` : ''}
</div>
</div>
`).join('');
document.getElementById('winnersList').innerHTML = html;
}
window.filterWinners = function(filter) {
currentFilter = filter;
['filterAll', 'filterPending', 'filterComplete'].forEach(id => {
const btn = document.getElementById(id);
if (id === 'filter' + filter.charAt(0).toUpperCase() + filter.slice(1)) {
btn.style.background = 'var(--color-primary-main)';
btn.style.color = 'white';
btn.classList.remove('btn-outline');
} else {
btn.style.background = '';
btn.style.color = '';
btn.classList.add('btn-outline');
}
});
renderWinners(filter);
};
window.searchWinners = function() {
const searchTerm = document.getElementById('searchInput').value;
renderWinners(currentFilter, searchTerm);
};
window.toggleComplete = function(index, checkbox) {
winnersData[index].complete = checkbox.checked;
if (checkbox.checked) {
winnersData[index].completeDate = new Date().toISOString().split('T')[0];
Toast.success('지급 완료로 처리되었습니다.');
} else {
winnersData[index].completeDate = null;
Toast.show('미지급으로 변경되었습니다.');
}
renderWinners(currentFilter);
};
window.viewAllParticipants = function() {
Toast.show('📊 참여자 전체 명단을 표시합니다...');
};
window.downloadExcel = function() {
Loading.show('엑셀 파일 생성 중...');
setTimeout(function() {
Loading.hide();
Toast.success('📥 당첨자_명단.xlsx 다운로드 완료!');
}, 1500);
};
window.downloadReport = function() {
Loading.show('리포트 생성 중...');
setTimeout(function() {
Loading.hide();
Toast.success('📄 이벤트_분석_리포트.pdf 다운로드 완료!');
}, 2000);
};
window.refresh = function() {
const now = new Date();
const timeStr = now.getHours().toString().padStart(2, '0') + ':' + now.getMinutes().toString().padStart(2, '0');
document.getElementById('lastUpdate').textContent = timeStr;
Toast.success('데이터가 업데이트되었습니다.');
};
window.toggleMenu = function() {
Toast.show('메뉴 표시');
};
})();
</script>
<style>
.tabs {
display: flex;
background: white;
border-bottom: 1px solid var(--color-gray-300);
position: sticky;
top: 56px;
z-index: 10;
}
.tab {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
padding: var(--spacing-s);
background: transparent;
border: none;
border-bottom: 2px solid transparent;
cursor: pointer;
transition: all 0.2s ease;
color: var(--color-gray-600);
}
.tab.active {
color: var(--color-primary-main);
border-bottom-color: var(--color-primary-main);
}
.tab .material-icons {
font-size: 20px;
}
.tab span:last-child {
font-size: 12px;
font-weight: 500;
}
.progress-bar-container {
width: 100%;
height: 8px;
background: var(--color-gray-200);
border-radius: var(--radius-full);
overflow: hidden;
}
.progress-bar-fill {
height: 100%;
border-radius: var(--radius-full);
transition: width 0.3s ease;
}
</style>
</body>
</html>

View File

@ -0,0 +1,330 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>실시간 대시보드 - KT AI 이벤트</title>
<link rel="stylesheet" href="css/common.css">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Pretendard:wght@400;500;600;700&display=swap" rel="stylesheet">
</head>
<body>
<a href="#main-content" class="skip-link">본문으로 건너뛰기</a>
<header class="header" role="banner">
<div class="header-content">
<button type="button" class="btn-icon" onclick="toggleMenu()" aria-label="메뉴">
<span class="material-icons">menu</span>
</button>
<h1 class="header-title">실시간 대시보드</h1>
<button type="button" class="btn-icon" aria-label="알림">
<span class="material-icons">notifications</span>
</button>
</div>
</header>
<main id="main-content" class="container" role="main" style="padding-bottom: 80px;">
<section class="section">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: var(--spacing-m);">
<div class="caption" style="color: var(--color-gray-600);">
마지막 업데이트: <span id="lastUpdate">15:35</span>
</div>
<button type="button" class="btn-text btn-sm" onclick="refresh()">
<span class="material-icons">refresh</span>
<span>새로고침</span>
</button>
</div>
</section>
<section class="section">
<div class="card">
<div class="card-body">
<div class="body-m" style="font-weight: 600; margin-bottom: var(--spacing-xs);">진행 중인 이벤트</div>
<div class="h4" style="margin-bottom: var(--spacing-xs);">연말 대박 이벤트</div>
<div class="body-s" style="color: var(--color-primary-main); font-weight: 600;">
D-5 (2025-12-31까지)
</div>
</div>
</div>
</section>
<section class="section" aria-labelledby="metrics-title">
<h2 id="metrics-title" class="h3">주요 지표</h2>
<div class="card">
<div class="card-body">
<div style="display: flex; align-items: center; gap: var(--spacing-m); margin-bottom: var(--spacing-l);">
<div style="width: 56px; height: 56px; background: var(--color-primary-lightest); border-radius: 50%; display: flex; align-items: center; justify-content: center;">
<span class="material-icons" style="font-size: 32px; color: var(--color-primary-main);">people</span>
</div>
<div style="flex: 1;">
<div class="caption" style="color: var(--color-gray-600); margin-bottom: 4px;">총 참여자</div>
<div class="h3" style="color: var(--color-primary-main); margin-bottom: 4px;" id="totalParticipants">1,234명</div>
<div class="caption" style="color: var(--color-success); font-weight: 600;">
<span class="material-icons" style="font-size: 14px; vertical-align: middle;">trending_up</span>
+45 (오늘)
</div>
</div>
</div>
<div style="display: flex; align-items: center; gap: var(--spacing-m); margin-bottom: var(--spacing-l);">
<div style="width: 56px; height: 56px; background: var(--color-secondary-lightest); border-radius: 50%; display: flex; align-items: center; justify-content: center;">
<span class="material-icons" style="font-size: 32px; color: var(--color-secondary-main);">visibility</span>
</div>
<div style="flex: 1;">
<div class="caption" style="color: var(--color-gray-600); margin-bottom: 4px;">총 노출 수</div>
<div class="h3" style="color: var(--color-secondary-main); margin-bottom: 4px;" id="totalViews">15,678회</div>
<div class="caption" style="color: var(--color-success); font-weight: 600;">
<span class="material-icons" style="font-size: 14px; vertical-align: middle;">trending_up</span>
+230 (최근 1시간)
</div>
</div>
</div>
<div style="display: flex; align-items: center; gap: var(--spacing-m); margin-bottom: var(--spacing-l);">
<div style="width: 56px; height: 56px; background: var(--color-warning-lightest); border-radius: 50%; display: flex; align-items: center; justify-content: center;">
<span class="material-icons" style="font-size: 32px; color: var(--color-warning);">monetization_on</span>
</div>
<div style="flex: 1;">
<div class="caption" style="color: var(--color-gray-600); margin-bottom: 4px;">매출 증가율</div>
<div class="h3" style="color: var(--color-warning); margin-bottom: 4px;" id="salesIncrease">+42%</div>
<div class="caption" style="color: var(--color-gray-600);">(이벤트 전 대비)</div>
</div>
</div>
<div style="display: flex; align-items: center; gap: var(--spacing-m);">
<div style="width: 56px; height: 56px; background: var(--color-success-lightest); border-radius: 50%; display: flex; align-items: center; justify-content: center;">
<span class="material-icons" style="font-size: 32px; color: var(--color-success);">trending_up</span>
</div>
<div style="flex: 1;">
<div class="caption" style="color: var(--color-gray-600); margin-bottom: 4px;">예상 ROI</div>
<div class="h3" style="color: var(--color-success); margin-bottom: 4px;" id="expectedROI">245%</div>
<div class="caption" style="color: var(--color-gray-600);">(투자 대비 수익)</div>
</div>
</div>
</div>
</div>
</section>
<section class="section" aria-labelledby="channels-title">
<h2 id="channels-title" class="h3">채널별 참여 현황</h2>
<div class="card">
<div class="card-body">
<div style="display: flex; flex-direction: column; gap: var(--spacing-l);">
<div>
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: var(--spacing-xs);">
<div style="display: flex; align-items: center; gap: var(--spacing-s);">
<span class="material-icons" style="color: var(--color-primary-main); font-size: 20px;">qr_code_2</span>
<span class="body-m" style="font-weight: 600;">QR코드</span>
</div>
<span class="h4" style="color: var(--color-primary-main);">45%</span>
</div>
<div class="progress-bar-container">
<div class="progress-bar-fill" style="width: 45%; background: var(--color-primary-main);"></div>
</div>
<div class="caption" style="color: var(--color-gray-600); margin-top: var(--spacing-xs);">556명 참여</div>
</div>
<div>
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: var(--spacing-xs);">
<div style="display: flex; align-items: center; gap: var(--spacing-s);">
<span class="material-icons" style="color: var(--color-secondary-main); font-size: 20px;">photo_camera</span>
<span class="body-m" style="font-weight: 600;">Instagram</span>
</div>
<span class="h4" style="color: var(--color-secondary-main);">30%</span>
</div>
<div class="progress-bar-container">
<div class="progress-bar-fill" style="width: 30%; background: var(--color-secondary-main);"></div>
</div>
<div class="caption" style="color: var(--color-gray-600); margin-top: var(--spacing-xs);">370명 참여</div>
</div>
<div>
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: var(--spacing-xs);">
<div style="display: flex; align-items: center; gap: var(--spacing-s);">
<span class="material-icons" style="color: var(--color-success); font-size: 20px;">tv</span>
<span class="body-m" style="font-weight: 600;">우리동네TV</span>
</div>
<span class="h4" style="color: var(--color-success);">15%</span>
</div>
<div class="progress-bar-container">
<div class="progress-bar-fill" style="width: 15%; background: var(--color-success);"></div>
</div>
<div class="caption" style="color: var(--color-gray-600); margin-top: var(--spacing-xs);">185명 참여</div>
</div>
<div>
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: var(--spacing-xs);">
<div style="display: flex; align-items: center; gap: var(--spacing-s);">
<span class="material-icons" style="color: var(--color-warning); font-size: 20px;">article</span>
<span class="body-m" style="font-weight: 600;">Naver Blog</span>
</div>
<span class="h4" style="color: var(--color-warning);">10%</span>
</div>
<div class="progress-bar-container">
<div class="progress-bar-fill" style="width: 10%; background: var(--color-warning);"></div>
</div>
<div class="caption" style="color: var(--color-gray-600); margin-top: var(--spacing-xs);">123명 참여</div>
</div>
</div>
</div>
</div>
</section>
<section class="section">
<button type="button" class="btn btn-primary btn-lg" onclick="viewDetailedAnalysis()" style="width: 100%;">
<span class="material-icons">analytics</span>
<span>상세 분석 보기</span>
</button>
</section>
</main>
<!-- Bottom Navigation -->
<nav class="bottom-nav" role="navigation">
<button type="button" class="nav-item" onclick="goToHome()">
<span class="material-icons">home</span>
<span></span>
</button>
<button type="button" class="nav-item" onclick="goToEvents()">
<span class="material-icons">event</span>
<span>이벤트</span>
</button>
<button type="button" class="nav-item active">
<span class="material-icons">analytics</span>
<span>분석</span>
</button>
<button type="button" class="nav-item" onclick="goToMy()">
<span class="material-icons">person</span>
<span>MY</span>
</button>
</nav>
<script src="js/common.js"></script>
<script>
(function() {
'use strict';
let autoRefreshInterval;
document.addEventListener('DOMContentLoaded', function() {
updateTime();
startAutoRefresh();
});
function startAutoRefresh() {
autoRefreshInterval = setInterval(function() {
refresh(true);
}, 300000); // 5분 간격
}
window.refresh = function(isAuto = false) {
const now = new Date();
const timeStr = now.getHours().toString().padStart(2, '0') + ':' + now.getMinutes().toString().padStart(2, '0');
document.getElementById('lastUpdate').textContent = timeStr;
// Simulate data update
const participants = parseInt(document.getElementById('totalParticipants').textContent);
const newParticipants = participants + Math.floor(Math.random() * 10);
document.getElementById('totalParticipants').textContent = newParticipants.toLocaleString() + '명';
const views = parseInt(document.getElementById('totalViews').textContent.replace(',', ''));
const newViews = views + Math.floor(Math.random() * 100);
document.getElementById('totalViews').textContent = newViews.toLocaleString() + '회';
if (!isAuto) {
Toast.success('데이터가 업데이트되었습니다.');
}
};
function updateTime() {
const now = new Date();
const timeStr = now.getHours().toString().padStart(2, '0') + ':' + now.getMinutes().toString().padStart(2, '0');
document.getElementById('lastUpdate').textContent = timeStr;
}
window.viewDetailedAnalysis = function() {
window.location.href = '22-채널별성과분석.html';
};
window.toggleMenu = function() {
Toast.show('메뉴');
};
window.goToHome = function() {
window.location.href = '21.5-홈.html';
};
window.goToEvents = function() {
window.location.href = '26-이벤트목록.html';
};
window.goToMy = function() {
window.location.href = '25-마이페이지.html';
};
// Cleanup on page unload
window.addEventListener('beforeunload', function() {
if (autoRefreshInterval) {
clearInterval(autoRefreshInterval);
}
});
})();
</script>
<style>
.progress-bar-container {
width: 100%;
height: 8px;
background: var(--color-gray-200);
border-radius: var(--radius-full);
overflow: hidden;
}
.progress-bar-fill {
height: 100%;
border-radius: var(--radius-full);
transition: width 0.3s ease;
}
.bottom-nav {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: white;
border-top: 1px solid var(--color-gray-300);
display: flex;
padding: var(--spacing-xs) 0;
z-index: 100;
}
.nav-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
padding: var(--spacing-xs);
background: transparent;
border: none;
color: var(--color-gray-600);
cursor: pointer;
transition: color 0.2s ease;
}
.nav-item.active {
color: var(--color-primary-main);
}
.nav-item .material-icons {
font-size: 24px;
}
.nav-item span:last-child {
font-size: 11px;
font-weight: 500;
}
</style>
</body>
</html>

View File

@ -0,0 +1,522 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>채널별 성과 분석 - KT AI 이벤트</title>
<link rel="stylesheet" href="css/common.css">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Pretendard:wght@400;500;600;700&display=swap" rel="stylesheet">
</head>
<body>
<a href="#main-content" class="skip-link">본문으로 건너뛰기</a>
<header class="header" role="banner">
<div class="header-content">
<button type="button" class="btn-icon" onclick="window.history.back()" aria-label="이전 페이지로 돌아가기">
<span class="material-icons">arrow_back</span>
</button>
<h1 class="header-title">채널별 성과 분석</h1>
<div style="width: 40px;"></div>
</div>
</header>
<main id="main-content" class="container" role="main" style="padding-bottom: 80px;">
<section class="section">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: var(--spacing-m);">
<h2 class="h3">📊 채널별 성과 비교</h2>
<button type="button" class="btn-text btn-sm" onclick="exportReport()">
<span class="material-icons">download</span>
<span>리포트</span>
</button>
</div>
<div class="card">
<div class="card-body">
<div class="body-s" style="color: var(--color-gray-600); margin-bottom: var(--spacing-xs);">분석 기간</div>
<select class="select-field" id="periodSelect" onchange="changePeriod()" aria-label="분석 기간 선택">
<option value="7">최근 7일</option>
<option value="30" selected>최근 30일</option>
<option value="90">최근 90일</option>
<option value="all">전체 기간</option>
</select>
<div class="caption" style="color: var(--color-gray-600); margin-top: var(--spacing-xs);" id="periodDisplay">2025-12-01 ~ 현재</div>
</div>
</div>
</section>
<section class="section" aria-labelledby="ranking-title">
<h3 id="ranking-title" class="body-l" style="font-weight: 600; margin-bottom: var(--spacing-m);">종합 순위</h3>
<div style="display: flex; flex-direction: column; gap: var(--spacing-s);">
<div class="card" style="border: 2px solid var(--color-primary-main);">
<div class="card-body">
<div style="display: flex; align-items: center; gap: var(--spacing-s);">
<span style="font-size: 24px;">🥇</span>
<div style="flex: 1;">
<div class="body-m" style="font-weight: 600;">QR코드</div>
<div class="caption" style="color: var(--color-success); font-weight: 600;">가장 효과적</div>
</div>
<span class="h4" style="color: var(--color-primary-main);">9.8%</span>
</div>
</div>
</div>
<div class="card">
<div class="card-body">
<div style="display: flex; align-items: center; gap: var(--spacing-s);">
<span style="font-size: 24px;">🥈</span>
<div style="flex: 1;">
<div class="body-m" style="font-weight: 600;">Instagram</div>
<div class="caption" style="color: var(--color-gray-600);">2위</div>
</div>
<span class="h4" style="color: var(--color-secondary-main);">8.2%</span>
</div>
</div>
</div>
<div class="card">
<div class="card-body">
<div style="display: flex; align-items: center; gap: var(--spacing-s);">
<span style="font-size: 24px;">🥉</span>
<div style="flex: 1;">
<div class="body-m" style="font-weight: 600;">우리동네TV</div>
<div class="caption" style="color: var(--color-gray-600);">3위</div>
</div>
<span class="h4" style="color: var(--color-success);">7.5%</span>
</div>
</div>
</div>
</div>
</section>
<section class="section" aria-labelledby="channels-title">
<h3 id="channels-title" class="body-l" style="font-weight: 600; margin-bottom: var(--spacing-m);">채널별 상세 성과</h3>
<!-- QR코드 -->
<div class="card">
<div class="card-body">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: var(--spacing-m);">
<div style="display: flex; align-items: center; gap: var(--spacing-s);">
<span class="material-icons" style="color: var(--color-primary-main); font-size: 24px;">qr_code_2</span>
<span class="body-l" style="font-weight: 600;">QR코드</span>
</div>
<span style="font-size: 20px;">🥇</span>
</div>
<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--spacing-m); margin-bottom: var(--spacing-m);">
<div>
<div class="caption" style="color: var(--color-gray-600); margin-bottom: 4px;">노출 수</div>
<div class="h4" style="color: var(--color-primary-main);">5,678회</div>
</div>
<div>
<div class="caption" style="color: var(--color-gray-600); margin-bottom: 4px;">참여자</div>
<div class="h4" style="color: var(--color-primary-main);">556명</div>
</div>
<div>
<div class="caption" style="color: var(--color-gray-600); margin-bottom: 4px;">참여율</div>
<div class="h4" style="color: var(--color-success);">9.8%</div>
</div>
<div>
<div class="caption" style="color: var(--color-gray-600); margin-bottom: 4px;">전환자</div>
<div class="h4" style="color: var(--color-primary-main);">223명</div>
</div>
</div>
<div style="margin-bottom: var(--spacing-m);">
<div class="caption" style="color: var(--color-gray-600); margin-bottom: 4px;">전환율</div>
<div style="display: flex; align-items: center; gap: var(--spacing-s);">
<div class="progress-bar-container" style="flex: 1;">
<div class="progress-bar-fill" style="width: 40%; background: var(--color-primary-main);"></div>
</div>
<span class="body-m" style="font-weight: 600; color: var(--color-primary-main);">40%</span>
</div>
</div>
<div style="background: var(--color-gray-100); padding: var(--spacing-s); border-radius: var(--radius-md); margin-bottom: var(--spacing-m);">
<div class="caption" style="color: var(--color-gray-600); margin-bottom: 4px;">CPA (고객 획득 비용)</div>
<div class="body-l" style="font-weight: 600;">180원</div>
</div>
<button type="button" class="btn btn-outline btn-lg" onclick="viewChannelDetail('qr')" style="width: 100%;">
<span>상세 보기</span>
<span class="material-icons">chevron_right</span>
</button>
</div>
</div>
<!-- Instagram -->
<div class="card">
<div class="card-body">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: var(--spacing-m);">
<div style="display: flex; align-items: center; gap: var(--spacing-s);">
<span class="material-icons" style="color: var(--color-secondary-main); font-size: 24px;">photo_camera</span>
<span class="body-l" style="font-weight: 600;">Instagram</span>
</div>
<span style="font-size: 20px;">🥈</span>
</div>
<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--spacing-m); margin-bottom: var(--spacing-m);">
<div>
<div class="caption" style="color: var(--color-gray-600); margin-bottom: 4px;">노출 수</div>
<div class="h4" style="color: var(--color-secondary-main);">4,523회</div>
</div>
<div>
<div class="caption" style="color: var(--color-gray-600); margin-bottom: 4px;">참여자</div>
<div class="h4" style="color: var(--color-secondary-main);">370명</div>
</div>
<div>
<div class="caption" style="color: var(--color-gray-600); margin-bottom: 4px;">참여율</div>
<div class="h4" style="color: var(--color-success);">8.2%</div>
</div>
<div>
<div class="caption" style="color: var(--color-gray-600); margin-bottom: 4px;">전환자</div>
<div class="h4" style="color: var(--color-secondary-main);">148명</div>
</div>
</div>
<div style="margin-bottom: var(--spacing-m);">
<div class="caption" style="color: var(--color-gray-600); margin-bottom: 4px;">전환율</div>
<div style="display: flex; align-items: center; gap: var(--spacing-s);">
<div class="progress-bar-container" style="flex: 1;">
<div class="progress-bar-fill" style="width: 40%; background: var(--color-secondary-main);"></div>
</div>
<span class="body-m" style="font-weight: 600; color: var(--color-secondary-main);">40%</span>
</div>
</div>
<div style="background: var(--color-gray-100); padding: var(--spacing-s); border-radius: var(--radius-md); margin-bottom: var(--spacing-m);">
<div class="caption" style="color: var(--color-gray-600); margin-bottom: 4px;">CPA (고객 획득 비용)</div>
<div class="body-l" style="font-weight: 600;">270원</div>
</div>
<button type="button" class="btn btn-outline btn-lg" onclick="viewChannelDetail('instagram')" style="width: 100%;">
<span>상세 보기</span>
<span class="material-icons">chevron_right</span>
</button>
</div>
</div>
<!-- 우리동네TV -->
<div class="card">
<div class="card-body">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: var(--spacing-m);">
<div style="display: flex; align-items: center; gap: var(--spacing-s);">
<span class="material-icons" style="color: var(--color-success); font-size: 24px;">tv</span>
<span class="body-l" style="font-weight: 600;">우리동네TV</span>
</div>
<span style="font-size: 20px;">🥉</span>
</div>
<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--spacing-m); margin-bottom: var(--spacing-m);">
<div>
<div class="caption" style="color: var(--color-gray-600); margin-bottom: 4px;">노출 수</div>
<div class="h4" style="color: var(--color-success);">3,890회</div>
</div>
<div>
<div class="caption" style="color: var(--color-gray-600); margin-bottom: 4px;">참여자</div>
<div class="h4" style="color: var(--color-success);">185명</div>
</div>
<div>
<div class="caption" style="color: var(--color-gray-600); margin-bottom: 4px;">참여율</div>
<div class="h4" style="color: var(--color-success);">7.5%</div>
</div>
<div>
<div class="caption" style="color: var(--color-gray-600); margin-bottom: 4px;">전환자</div>
<div class="h4" style="color: var(--color-success);">74명</div>
</div>
</div>
<div style="margin-bottom: var(--spacing-m);">
<div class="caption" style="color: var(--color-gray-600); margin-bottom: 4px;">전환율</div>
<div style="display: flex; align-items: center; gap: var(--spacing-s);">
<div class="progress-bar-container" style="flex: 1;">
<div class="progress-bar-fill" style="width: 40%; background: var(--color-success);"></div>
</div>
<span class="body-m" style="font-weight: 600; color: var(--color-success);">40%</span>
</div>
</div>
<div style="background: var(--color-gray-100); padding: var(--spacing-s); border-radius: var(--radius-md); margin-bottom: var(--spacing-m);">
<div class="caption" style="color: var(--color-gray-600); margin-bottom: 4px;">CPA (고객 획득 비용)</div>
<div class="body-l" style="font-weight: 600;">405원</div>
</div>
<button type="button" class="btn btn-outline btn-lg" onclick="viewChannelDetail('tv')" style="width: 100%;">
<span>상세 보기</span>
<span class="material-icons">chevron_right</span>
</button>
</div>
</div>
<!-- Naver Blog -->
<div class="card">
<div class="card-body">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: var(--spacing-m);">
<div style="display: flex; align-items: center; gap: var(--spacing-s);">
<span class="material-icons" style="color: var(--color-warning); font-size: 24px;">article</span>
<span class="body-l" style="font-weight: 600;">Naver Blog</span>
</div>
</div>
<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--spacing-m); margin-bottom: var(--spacing-m);">
<div>
<div class="caption" style="color: var(--color-gray-600); margin-bottom: 4px;">노출 수</div>
<div class="h4" style="color: var(--color-warning);">1,587회</div>
</div>
<div>
<div class="caption" style="color: var(--color-gray-600); margin-bottom: 4px;">참여자</div>
<div class="h4" style="color: var(--color-warning);">123명</div>
</div>
<div>
<div class="caption" style="color: var(--color-gray-600); margin-bottom: 4px;">참여율</div>
<div class="h4" style="color: var(--color-warning);">7.8%</div>
</div>
<div>
<div class="caption" style="color: var(--color-gray-600); margin-bottom: 4px;">전환자</div>
<div class="h4" style="color: var(--color-warning);">49명</div>
</div>
</div>
<div style="margin-bottom: var(--spacing-m);">
<div class="caption" style="color: var(--color-gray-600); margin-bottom: 4px;">전환율</div>
<div style="display: flex; align-items: center; gap: var(--spacing-s);">
<div class="progress-bar-container" style="flex: 1;">
<div class="progress-bar-fill" style="width: 40%; background: var(--color-warning);"></div>
</div>
<span class="body-m" style="font-weight: 600; color: var(--color-warning);">40%</span>
</div>
</div>
<div style="background: var(--color-gray-100); padding: var(--spacing-s); border-radius: var(--radius-md); margin-bottom: var(--spacing-m);">
<div class="caption" style="color: var(--color-gray-600); margin-bottom: 4px;">CPA (고객 획득 비용)</div>
<div class="body-l" style="font-weight: 600;">815원</div>
</div>
<button type="button" class="btn btn-outline btn-lg" onclick="viewChannelDetail('blog')" style="width: 100%;">
<span>상세 보기</span>
<span class="material-icons">chevron_right</span>
</button>
</div>
</div>
</section>
<section class="section" aria-labelledby="comparison-title">
<h3 id="comparison-title" class="body-l" style="font-weight: 600; margin-bottom: var(--spacing-m);">비교 차트</h3>
<div class="card">
<div class="card-body">
<div class="body-m" style="font-weight: 600; margin-bottom: var(--spacing-l);">참여율 비교</div>
<div style="display: flex; flex-direction: column; gap: var(--spacing-m);">
<div>
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: var(--spacing-xs);">
<span class="body-s">QR코드</span>
<span class="body-s" style="font-weight: 600; color: var(--color-primary-main);">9.8%</span>
</div>
<div class="progress-bar-container">
<div class="progress-bar-fill" style="width: 100%; background: var(--color-primary-main);"></div>
</div>
</div>
<div>
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: var(--spacing-xs);">
<span class="body-s">Instagram</span>
<span class="body-s" style="font-weight: 600; color: var(--color-secondary-main);">8.2%</span>
</div>
<div class="progress-bar-container">
<div class="progress-bar-fill" style="width: 84%; background: var(--color-secondary-main);"></div>
</div>
</div>
<div>
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: var(--spacing-xs);">
<span class="body-s">우리동네TV</span>
<span class="body-s" style="font-weight: 600; color: var(--color-success);">7.8%</span>
</div>
<div class="progress-bar-container">
<div class="progress-bar-fill" style="width: 80%; background: var(--color-success);"></div>
</div>
</div>
<div>
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: var(--spacing-xs);">
<span class="body-s">Naver Blog</span>
<span class="body-s" style="font-weight: 600; color: var(--color-warning);">7.5%</span>
</div>
<div class="progress-bar-container">
<div class="progress-bar-fill" style="width: 77%; background: var(--color-warning);"></div>
</div>
</div>
</div>
</div>
</div>
</section>
<section class="section">
<div class="card" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white;">
<div class="card-body">
<div style="display: flex; align-items: flex-start; gap: var(--spacing-m);">
<span class="material-icons" style="font-size: 32px;">lightbulb</span>
<div>
<div class="body-l" style="font-weight: 600; margin-bottom: var(--spacing-s); color: white;">💡 AI 인사이트</div>
<div class="body-s" style="color: white; line-height: 1.6;">
QR코드가 가장 높은 참여율(9.8%)과 전환율(40%)을 보입니다. 오프라인 홍보를 강화하면 효과가 더 좋을 것 같아요. 또한 Instagram의 노출 대비 참여율이 좋으므로 SNS 콘텐츠 제작을 늘리는 것을 추천합니다.
</div>
</div>
</div>
</div>
</div>
</section>
</main>
<!-- Bottom Navigation -->
<nav class="bottom-nav" role="navigation">
<button type="button" class="nav-item" onclick="goToHome()">
<span class="material-icons">home</span>
<span></span>
</button>
<button type="button" class="nav-item" onclick="goToEvents()">
<span class="material-icons">event</span>
<span>이벤트</span>
</button>
<button type="button" class="nav-item active">
<span class="material-icons">analytics</span>
<span>분석</span>
</button>
<button type="button" class="nav-item" onclick="goToMy()">
<span class="material-icons">person</span>
<span>MY</span>
</button>
</nav>
<script src="js/common.js"></script>
<script>
(function() {
'use strict';
document.addEventListener('DOMContentLoaded', function() {
updatePeriodDisplay();
});
function updatePeriodDisplay() {
const select = document.getElementById('periodSelect');
const display = document.getElementById('periodDisplay');
const value = select.value;
const today = new Date();
let startDate;
if (value === 'all') {
display.textContent = '2025-12-01 ~ 현재';
} else {
const days = parseInt(value);
startDate = new Date(today);
startDate.setDate(today.getDate() - days);
const formatDate = function(date) {
return date.getFullYear() + '-' +
String(date.getMonth() + 1).padStart(2, '0') + '-' +
String(date.getDate()).padStart(2, '0');
};
display.textContent = formatDate(startDate) + ' ~ ' + formatDate(today);
}
}
window.changePeriod = function() {
Loading.show('데이터 분석 중...');
setTimeout(function() {
Loading.hide();
updatePeriodDisplay();
Toast.success('기간이 변경되었습니다.');
}, 1000);
};
window.viewChannelDetail = function(channel) {
const channelNames = {
qr: 'QR코드',
instagram: 'Instagram',
tv: '우리동네TV',
blog: 'Naver Blog'
};
Toast.show('📊 ' + channelNames[channel] + ' 상세 분석');
};
window.exportReport = function() {
Loading.show('리포트 생성 중...');
setTimeout(function() {
Loading.hide();
Toast.success('📄 채널 성과 리포트가 다운로드되었습니다!');
}, 1500);
};
window.goToHome = function() {
window.location.href = '21.5-홈.html';
};
window.goToEvents = function() {
window.location.href = '26-이벤트목록.html';
};
window.goToMy = function() {
window.location.href = '25-마이페이지.html';
};
})();
</script>
<style>
.progress-bar-container {
width: 100%;
height: 8px;
background: var(--color-gray-200);
border-radius: var(--radius-full);
overflow: hidden;
}
.progress-bar-fill {
height: 100%;
border-radius: var(--radius-full);
transition: width 0.3s ease;
}
.bottom-nav {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: white;
border-top: 1px solid var(--color-gray-300);
display: flex;
padding: var(--spacing-xs) 0;
z-index: 100;
}
.nav-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
padding: var(--spacing-xs);
background: transparent;
border: none;
color: var(--color-gray-600);
cursor: pointer;
transition: color 0.2s ease;
}
.nav-item.active {
color: var(--color-primary-main);
}
.nav-item .material-icons {
font-size: 24px;
}
.nav-item span:last-child {
font-size: 11px;
font-weight: 500;
}
</style>
</body>
</html>

View File

@ -0,0 +1,393 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ROI 분석 - KT AI 이벤트</title>
<link rel="stylesheet" href="css/common.css">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Pretendard:wght@400;500;600;700&display=swap" rel="stylesheet">
</head>
<body>
<a href="#main-content" class="skip-link">본문으로 건너뛰기</a>
<header class="header" role="banner">
<div class="header-content">
<button type="button" class="btn-icon" onclick="window.history.back()" aria-label="이전 페이지로 돌아가기">
<span class="material-icons">arrow_back</span>
</button>
<h1 class="header-title">ROI 분석</h1>
<div style="width: 40px;"></div>
</div>
</header>
<main id="main-content" class="container" role="main" style="padding-bottom: 80px;">
<section class="section">
<h2 class="h3">💰 투자 대비 효과</h2>
</section>
<section class="section">
<div class="card" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white;">
<div class="card-body" style="text-align: center; padding: var(--spacing-xl);">
<div class="caption" style="color: rgba(255,255,255,0.9); margin-bottom: var(--spacing-s);">ROI (투자 대비 수익률)</div>
<div style="font-size: 56px; font-weight: 700; margin-bottom: var(--spacing-m); color: white;" id="roiValue">245%</div>
<div class="body-m" style="color: rgba(255,255,255,0.95);">
투자한 금액 대비 <span style="font-weight: 700;">2.45배</span> 수익!
</div>
</div>
</div>
</section>
<section class="section" aria-labelledby="cost-title">
<h3 id="cost-title" class="body-l" style="font-weight: 600; margin-bottom: var(--spacing-m);">비용 내역</h3>
<div class="card">
<div class="card-body">
<div style="margin-bottom: var(--spacing-l);">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: var(--spacing-s);">
<div style="display: flex; align-items: center; gap: var(--spacing-s);">
<span class="material-icons" style="color: var(--color-warning);">card_giftcard</span>
<span class="body-m" style="font-weight: 600;">경품 비용</span>
</div>
<span class="h4" style="color: var(--color-error);">100,000원</span>
</div>
<div class="caption" style="color: var(--color-gray-600); padding-left: 32px;">
상품권 10장 × 10,000원
</div>
</div>
<div style="margin-bottom: var(--spacing-l);">
<div style="display: flex; align-items: center; gap: var(--spacing-s); margin-bottom: var(--spacing-s);">
<span class="material-icons" style="color: var(--color-info);">settings</span>
<span class="body-m" style="font-weight: 600;">플랫폼 이용료</span>
</div>
<div style="padding-left: 32px; display: flex; flex-direction: column; gap: var(--spacing-xs);">
<div style="display: flex; justify-content: space-between; align-items: center;">
<span class="body-s" style="color: var(--color-gray-600);">• 우리동네TV</span>
<span class="body-s" style="color: var(--color-success); font-weight: 600;">무료</span>
</div>
<div style="display: flex; justify-content: space-between; align-items: center;">
<span class="body-s" style="color: var(--color-gray-600);">• 지니TV</span>
<span class="body-s" style="color: var(--color-success); font-weight: 600;">무료</span>
</div>
<div style="display: flex; justify-content: space-between; align-items: center;">
<span class="body-s" style="color: var(--color-gray-600);">• SNS (Instagram, Blog)</span>
<span class="body-s" style="color: var(--color-success); font-weight: 600;">무료</span>
</div>
</div>
</div>
<div style="border-top: 2px solid var(--color-gray-300); padding-top: var(--spacing-m);">
<div style="display: flex; justify-content: space-between; align-items: center;">
<span class="body-l" style="font-weight: 700;">총 투자</span>
<span class="h3" style="color: var(--color-error);">100,000원</span>
</div>
</div>
</div>
</div>
</section>
<section class="section" aria-labelledby="revenue-title">
<h3 id="revenue-title" class="body-l" style="font-weight: 600; margin-bottom: var(--spacing-m);">수익 내역</h3>
<div class="card">
<div class="card-body">
<div style="margin-bottom: var(--spacing-l);">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: var(--spacing-s);">
<div style="display: flex; align-items: center; gap: var(--spacing-s);">
<span class="material-icons" style="color: var(--color-success);">trending_up</span>
<span class="body-m" style="font-weight: 600;">매출 증가액</span>
</div>
<span class="h4" style="color: var(--color-success);">180,000원</span>
</div>
<div class="caption" style="color: var(--color-gray-600); padding-left: 32px;">
이벤트 기간 vs 평균 매출 비교
</div>
</div>
<div style="margin-bottom: var(--spacing-l);">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: var(--spacing-s);">
<div style="display: flex; align-items: center; gap: var(--spacing-s);">
<span class="material-icons" style="color: var(--color-primary-main);">people</span>
<span class="body-m" style="font-weight: 600;">신규 고객 가치</span>
</div>
<span class="h4" style="color: var(--color-success);">65,000원</span>
</div>
<div class="caption" style="color: var(--color-gray-600); padding-left: 32px;">
78명 × 예상 LTV (고객 생애 가치)
</div>
</div>
<div style="border-top: 2px solid var(--color-gray-300); padding-top: var(--spacing-m);">
<div style="display: flex; justify-content: space-between; align-items: center;">
<span class="body-l" style="font-weight: 700;">총 수익</span>
<span class="h3" style="color: var(--color-success);">245,000원</span>
</div>
</div>
</div>
</div>
</section>
<section class="section">
<div class="card">
<div class="card-body">
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: var(--spacing-l);">
<div>
<div style="display: flex; align-items: center; gap: var(--spacing-s); margin-bottom: var(--spacing-s);">
<span class="material-icons" style="color: var(--color-success);">check_circle</span>
<span class="body-m" style="font-weight: 600;">손익 분기점</span>
</div>
<div class="h4" style="color: var(--color-success); margin-bottom: 4px;">달성</div>
<div class="caption" style="color: var(--color-gray-600);">이벤트 3일차</div>
</div>
<div>
<div style="display: flex; align-items: center; gap: var(--spacing-s); margin-bottom: var(--spacing-s);">
<span class="material-icons" style="color: var(--color-info);">schedule</span>
<span class="body-m" style="font-weight: 600;">회수 기간 예상</span>
</div>
<div class="h4" style="color: var(--color-info); margin-bottom: 4px;">약 1.5개월</div>
<div class="caption" style="color: var(--color-gray-600);">신규 고객 기준</div>
</div>
</div>
</div>
</div>
</section>
<section class="section" aria-labelledby="trend-title">
<h3 id="trend-title" class="body-l" style="font-weight: 600; margin-bottom: var(--spacing-m);">ROI 추이</h3>
<div class="card">
<div class="card-body">
<div style="margin-bottom: var(--spacing-m);">
<canvas id="roiChart" width="100%" height="200" aria-label="ROI 추이 그래프"></canvas>
</div>
<div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--spacing-s); text-align: center;">
<div>
<div class="caption" style="color: var(--color-gray-600);">12/1</div>
<div class="body-s" style="font-weight: 600; color: var(--color-error);">-20%</div>
</div>
<div>
<div class="caption" style="color: var(--color-gray-600);">12/10</div>
<div class="body-s" style="font-weight: 600; color: var(--color-warning);">150%</div>
</div>
<div>
<div class="caption" style="color: var(--color-gray-600);">12/20</div>
<div class="body-s" style="font-weight: 600; color: var(--color-success);">245%</div>
</div>
</div>
</div>
</div>
</section>
<section class="section">
<div class="alert alert-info" role="status">
<div style="display: flex; align-items: flex-start; gap: var(--spacing-s);">
<span class="material-icons">info</span>
<div class="body-s">
ROI는 5분마다 자동으로 업데이트됩니다. POS 시스템과 연동하여 실시간 매출 데이터를 반영합니다.
</div>
</div>
</div>
</section>
</main>
<!-- Bottom Navigation -->
<nav class="bottom-nav" role="navigation">
<button type="button" class="nav-item" onclick="goToHome()">
<span class="material-icons">home</span>
<span></span>
</button>
<button type="button" class="nav-item" onclick="goToEvents()">
<span class="material-icons">event</span>
<span>이벤트</span>
</button>
<button type="button" class="nav-item active">
<span class="material-icons">analytics</span>
<span>분석</span>
</button>
<button type="button" class="nav-item" onclick="goToMy()">
<span class="material-icons">person</span>
<span>MY</span>
</button>
</nav>
<script src="js/common.js"></script>
<script>
(function() {
'use strict';
let roiChart;
document.addEventListener('DOMContentLoaded', function() {
drawROIChart();
startAutoUpdate();
});
function drawROIChart() {
const canvas = document.getElementById('roiChart');
const ctx = canvas.getContext('2d');
const width = canvas.width;
const height = canvas.height;
// Clear canvas
ctx.clearRect(0, 0, width, height);
// Chart data points
const dataPoints = [
{ x: 0, y: -20 },
{ x: 30, y: 50 },
{ x: 50, y: 100 },
{ x: 70, y: 150 },
{ x: 100, y: 245 }
];
// Scale and translate points
const maxY = 300;
const minY = -50;
const padding = 20;
const scaleX = (width - 2 * padding) / 100;
const scaleY = (height - 2 * padding) / (maxY - minY);
// Draw axes
ctx.strokeStyle = '#E0E0E0';
ctx.lineWidth = 1;
// X axis
ctx.beginPath();
ctx.moveTo(padding, height - padding);
ctx.lineTo(width - padding, height - padding);
ctx.stroke();
// Y axis (zero line)
const zeroY = height - padding - (0 - minY) * scaleY;
ctx.beginPath();
ctx.moveTo(padding, zeroY);
ctx.lineTo(width - padding, zeroY);
ctx.stroke();
// Draw line
ctx.strokeStyle = '#667eea';
ctx.lineWidth = 3;
ctx.lineJoin = 'round';
ctx.lineCap = 'round';
ctx.beginPath();
dataPoints.forEach(function(point, index) {
const x = padding + point.x * scaleX;
const y = height - padding - (point.y - minY) * scaleY;
if (index === 0) {
ctx.moveTo(x, y);
} else {
ctx.lineTo(x, y);
}
});
ctx.stroke();
// Draw points
dataPoints.forEach(function(point) {
const x = padding + point.x * scaleX;
const y = height - padding - (point.y - minY) * scaleY;
ctx.fillStyle = point.y < 0 ? '#E31E24' : '#667eea';
ctx.beginPath();
ctx.arc(x, y, 4, 0, 2 * Math.PI);
ctx.fill();
});
// Fill area under curve
ctx.fillStyle = 'rgba(102, 126, 234, 0.1)';
ctx.beginPath();
ctx.moveTo(padding + dataPoints[0].x * scaleX, zeroY);
dataPoints.forEach(function(point) {
const x = padding + point.x * scaleX;
const y = height - padding - (point.y - minY) * scaleY;
ctx.lineTo(x, y);
});
ctx.lineTo(padding + dataPoints[dataPoints.length - 1].x * scaleX, zeroY);
ctx.closePath();
ctx.fill();
}
function startAutoUpdate() {
// Auto-update every 5 minutes
setInterval(function() {
updateROI();
}, 300000);
}
function updateROI() {
// Simulate ROI update
const currentROI = parseInt(document.getElementById('roiValue').textContent);
const newROI = currentROI + Math.floor(Math.random() * 10);
document.getElementById('roiValue').textContent = newROI + '%';
// Redraw chart
drawROIChart();
}
window.goToHome = function() {
window.location.href = '21.5-홈.html';
};
window.goToEvents = function() {
window.location.href = '26-이벤트목록.html';
};
window.goToMy = function() {
window.location.href = '25-마이페이지.html';
};
})();
</script>
<style>
.bottom-nav {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: white;
border-top: 1px solid var(--color-gray-300);
display: flex;
padding: var(--spacing-xs) 0;
z-index: 100;
}
.nav-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
padding: var(--spacing-xs);
background: transparent;
border: none;
color: var(--color-gray-600);
cursor: pointer;
transition: color 0.2s ease;
}
.nav-item.active {
color: var(--color-primary-main);
}
.nav-item .material-icons {
font-size: 24px;
}
.nav-item span:last-child {
font-size: 11px;
font-weight: 500;
}
#roiChart {
width: 100%;
height: 200px;
}
</style>
</body>
</html>

View File

@ -0,0 +1,385 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>분석 리포트 - KT AI 이벤트</title>
<link rel="stylesheet" href="css/common.css">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Pretendard:wght@400;500;600;700&display=swap" rel="stylesheet">
</head>
<body>
<a href="#main-content" class="skip-link">본문으로 건너뛰기</a>
<header class="header" role="banner">
<div class="header-content">
<button type="button" class="btn-icon" onclick="window.history.back()" aria-label="이전 페이지로 돌아가기">
<span class="material-icons">arrow_back</span>
</button>
<h1 class="header-title">분석 리포트</h1>
<div style="width: 40px;"></div>
</div>
</header>
<main id="main-content" class="container" role="main" style="padding-bottom: 80px;">
<section class="section">
<h2 class="h3">📊 종합 분석 리포트</h2>
</section>
<section class="section">
<div class="card">
<div class="card-body">
<div class="body-s" style="color: var(--color-gray-600); margin-bottom: 4px;">이벤트</div>
<div class="body-l" style="font-weight: 600; margin-bottom: var(--spacing-m);" id="eventName">연말 대박 이벤트</div>
<div class="body-s" style="color: var(--color-gray-600); margin-bottom: 4px;">기간</div>
<div class="body-m" id="eventPeriod">2025-12-01 ~ 2025-12-31</div>
</div>
</div>
</section>
<section class="section" aria-labelledby="preview-title">
<h3 id="preview-title" class="body-l" style="font-weight: 600; margin-bottom: var(--spacing-m);">리포트 미리보기</h3>
<div class="card">
<div class="card-body">
<button type="button" class="report-preview" onclick="previewReport()" aria-label="리포트 미리보기">
<div style="aspect-ratio: 1/1.414; background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%); border-radius: var(--radius-md); display: flex; flex-direction: column; align-items: center; justify-content: center; margin-bottom: var(--spacing-m);">
<span class="material-icons" style="font-size: 64px; color: var(--color-gray-500); margin-bottom: var(--spacing-s);">description</span>
<div class="body-s" style="color: var(--color-gray-600);">PDF 썸네일</div>
</div>
</button>
<div style="display: flex; justify-content: center; gap: var(--spacing-l); padding: var(--spacing-m); background: var(--color-gray-100); border-radius: var(--radius-md);">
<div style="text-align: center;">
<div class="caption" style="color: var(--color-gray-600); margin-bottom: 4px;">페이지</div>
<div class="body-l" style="font-weight: 600;">10페이지</div>
</div>
<div style="width: 1px; background: var(--color-gray-300);"></div>
<div style="text-align: center;">
<div class="caption" style="color: var(--color-gray-600); margin-bottom: 4px;">파일 크기</div>
<div class="body-l" style="font-weight: 600;">5.2MB</div>
</div>
</div>
</div>
</div>
</section>
<section class="section" aria-labelledby="content-title">
<h3 id="content-title" class="body-l" style="font-weight: 600; margin-bottom: var(--spacing-m);">포함 내용</h3>
<div class="card">
<div class="card-body">
<div style="display: flex; flex-direction: column; gap: var(--spacing-s);">
<div style="display: flex; align-items: center; gap: var(--spacing-s);">
<span class="material-icons" style="color: var(--color-success); font-size: 20px;">check_circle</span>
<span class="body-m">이벤트 개요</span>
</div>
<div style="display: flex; align-items: center; gap: var(--spacing-s);">
<span class="material-icons" style="color: var(--color-success); font-size: 20px;">check_circle</span>
<span class="body-m">참여 통계</span>
</div>
<div style="display: flex; align-items: center; gap: var(--spacing-s);">
<span class="material-icons" style="color: var(--color-success); font-size: 20px;">check_circle</span>
<span class="body-m">노출 통계</span>
</div>
<div style="display: flex; align-items: center; gap: var(--spacing-s);">
<span class="material-icons" style="color: var(--color-success); font-size: 20px;">check_circle</span>
<span class="body-m">매출 분석</span>
</div>
<div style="display: flex; align-items: center; gap: var(--spacing-s);">
<span class="material-icons" style="color: var(--color-success); font-size: 20px;">check_circle</span>
<span class="body-m">ROI 분석</span>
</div>
<div style="display: flex; align-items: center; gap: var(--spacing-s);">
<span class="material-icons" style="color: var(--color-success); font-size: 20px;">check_circle</span>
<span class="body-m">채널별 성과 비교</span>
</div>
<div style="display: flex; align-items: center; gap: var(--spacing-s);">
<span class="material-icons" style="color: var(--color-success); font-size: 20px;">check_circle</span>
<span class="body-m">업종 평균 벤치마킹</span>
</div>
<div style="display: flex; align-items: center; gap: var(--spacing-s);">
<span class="material-icons" style="color: var(--color-success); font-size: 20px;">check_circle</span>
<span class="body-m">그래프 시각화</span>
</div>
</div>
</div>
</div>
</section>
<section class="section">
<button type="button" class="btn btn-primary btn-lg" onclick="downloadPDF()" style="width: 100%; margin-bottom: var(--spacing-s);">
<span class="material-icons">download</span>
<span>PDF 다운로드</span>
</button>
<button type="button" class="btn btn-outline btn-lg" onclick="sendEmail()" style="width: 100%;">
<span class="material-icons">email</span>
<span>이메일로 받기</span>
</button>
</section>
<section class="section" aria-labelledby="history-title">
<h3 id="history-title" class="body-l" style="font-weight: 600; margin-bottom: var(--spacing-m);">생성 이력</h3>
<div class="card">
<div class="card-body">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: var(--spacing-m);">
<div>
<div class="body-m" style="font-weight: 600; margin-bottom: 4px;">최종 리포트</div>
<div class="caption" style="color: var(--color-gray-600);">2025-12-31 16:00</div>
</div>
<button type="button" class="btn btn-text btn-sm" onclick="downloadHistory(0)">
<span class="material-icons">download</span>
<span>다운로드</span>
</button>
</div>
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: var(--spacing-m);">
<div>
<div class="body-m" style="font-weight: 600; margin-bottom: 4px;">중간 리포트</div>
<div class="caption" style="color: var(--color-gray-600);">2025-12-15 14:30</div>
</div>
<button type="button" class="btn btn-text btn-sm" onclick="downloadHistory(1)">
<span class="material-icons">download</span>
<span>다운로드</span>
</button>
</div>
<div style="display: flex; justify-content: space-between; align-items: center;">
<div>
<div class="body-m" style="font-weight: 600; margin-bottom: 4px;">초기 리포트</div>
<div class="caption" style="color: var(--color-gray-600);">2025-12-01 10:00</div>
</div>
<button type="button" class="btn btn-text btn-sm" onclick="downloadHistory(2)">
<span class="material-icons">download</span>
<span>다운로드</span>
</button>
</div>
</div>
</div>
</section>
<section class="section">
<div class="alert alert-info" role="status">
<div style="display: flex; align-items: flex-start; gap: var(--spacing-s);">
<span class="material-icons">info</span>
<div class="body-s">
리포트 생성은 최대 30초가 소요됩니다. PDF 파일은 최대 10MB까지 최적화됩니다.
</div>
</div>
</div>
</section>
</main>
<!-- Email Modal -->
<div id="emailModal" class="modal" role="dialog" aria-modal="true" style="display: none;">
<div class="modal-content">
<div class="modal-header">
<h3 class="h4">이메일 발송</h3>
<button type="button" class="btn-icon" onclick="closeEmailModal()">
<span class="material-icons">close</span>
</button>
</div>
<div class="modal-body">
<div class="form-group">
<label for="emailInput" class="form-label">받는 사람</label>
<input type="email" id="emailInput" class="input-field" placeholder="example@email.com" value="" aria-label="이메일 주소">
</div>
<div class="alert alert-info" role="status">
<div style="display: flex; align-items: flex-start; gap: var(--spacing-s);">
<span class="material-icons">info</span>
<div class="body-s">
등록된 이메일 주소로 리포트가 발송됩니다.
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline btn-lg" onclick="closeEmailModal()" style="flex: 1;">
취소
</button>
<button type="button" class="btn btn-primary btn-lg" onclick="confirmSendEmail()" style="flex: 1;">
발송
</button>
</div>
</div>
</div>
<!-- Bottom Navigation -->
<nav class="bottom-nav" role="navigation">
<button type="button" class="nav-item" onclick="goToHome()">
<span class="material-icons">home</span>
<span></span>
</button>
<button type="button" class="nav-item" onclick="goToEvents()">
<span class="material-icons">event</span>
<span>이벤트</span>
</button>
<button type="button" class="nav-item active">
<span class="material-icons">analytics</span>
<span>분석</span>
</button>
<button type="button" class="nav-item" onclick="goToMy()">
<span class="material-icons">person</span>
<span>MY</span>
</button>
</nav>
<script src="js/common.js"></script>
<script>
(function() {
'use strict';
const eventData = AppState.currentEvent || {};
const storeData = AppState.store || {};
document.addEventListener('DOMContentLoaded', function() {
loadEventInfo();
loadUserEmail();
});
function loadEventInfo() {
if (eventData.name) {
document.getElementById('eventName').textContent = eventData.name;
}
if (eventData.startDate && eventData.endDate) {
document.getElementById('eventPeriod').textContent =
eventData.startDate + ' ~ ' + eventData.endDate;
}
}
function loadUserEmail() {
const user = AppState.user || {};
if (user.email) {
document.getElementById('emailInput').value = user.email;
}
}
window.previewReport = function() {
Toast.show('📄 리포트 미리보기');
};
window.downloadPDF = function() {
Loading.show('리포트 생성 중...');
// Simulate PDF generation (30 seconds max)
setTimeout(function() {
Loading.hide();
Toast.success('📥 분석 리포트가 다운로드되었습니다!');
}, 2000);
};
window.sendEmail = function() {
Modal.open(document.getElementById('emailModal'));
};
window.closeEmailModal = function() {
Modal.close(document.getElementById('emailModal'));
};
window.confirmSendEmail = function() {
const email = document.getElementById('emailInput').value;
if (!email) {
Toast.error('이메일 주소를 입력해주세요.');
return;
}
// Basic email validation
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
Toast.error('올바른 이메일 주소를 입력해주세요.');
return;
}
Modal.close(document.getElementById('emailModal'));
Loading.show('이메일 발송 중...');
setTimeout(function() {
Loading.hide();
Toast.success('✉️ ' + email + '로 리포트가 발송되었습니다!');
}, 1500);
};
window.downloadHistory = function(index) {
const dates = ['2025-12-31 16:00', '2025-12-15 14:30', '2025-12-01 10:00'];
Loading.show('리포트 다운로드 중...');
setTimeout(function() {
Loading.hide();
Toast.success('📥 ' + dates[index] + ' 리포트가 다운로드되었습니다!');
}, 1000);
};
window.goToHome = function() {
window.location.href = '21.5-홈.html';
};
window.goToEvents = function() {
window.location.href = '26-이벤트목록.html';
};
window.goToMy = function() {
window.location.href = '25-마이페이지.html';
};
})();
</script>
<style>
.report-preview {
width: 100%;
background: transparent;
border: none;
cursor: pointer;
transition: transform 0.2s ease;
}
.report-preview:hover {
transform: scale(1.02);
}
.bottom-nav {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: white;
border-top: 1px solid var(--color-gray-300);
display: flex;
padding: var(--spacing-xs) 0;
z-index: 100;
}
.nav-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
padding: var(--spacing-xs);
background: transparent;
border: none;
color: var(--color-gray-600);
cursor: pointer;
transition: color 0.2s ease;
}
.nav-item.active {
color: var(--color-primary-main);
}
.nav-item .material-icons {
font-size: 24px;
}
.nav-item span:last-child {
font-size: 11px;
font-weight: 500;
}
</style>
</body>
</html>

View File

@ -0,0 +1,888 @@
/**
* KT AI 기반 소상공인 이벤트 자동 생성 서비스
* 공통 스타일시트
*
* 작성일: 2025-01-20
* 버전: 1.0
* 디자인 원칙: Mobile First, 접근성 우선, 일관성
*/
/* ============================================
1. CSS Variables (Design Tokens)
============================================ */
:root {
/* Primary Colors */
--color-primary-main: #E31E24;
--color-primary-light: #FF4D52;
--color-primary-dark: #C71820;
/* Secondary Colors */
--color-secondary-main: #0066FF;
--color-secondary-light: #4D94FF;
--color-secondary-dark: #004DBF;
/* Grayscale */
--color-black: #1A1A1A;
--color-gray-700: #4A4A4A;
--color-gray-500: #9E9E9E;
--color-gray-300: #D9D9D9;
--color-gray-100: #F5F5F5;
--color-white: #FFFFFF;
/* Semantic Colors */
--color-success: #00C853;
--color-warning: #FFA000;
--color-error: #D32F2F;
--color-info: #0288D1;
/* Typography */
--font-family-base: 'Pretendard', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Helvetica Neue', system-ui, sans-serif;
/* Font Sizes (Mobile First) */
--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 Weights */
--font-weight-regular: 400;
--font-weight-medium: 500;
--font-weight-semibold: 600;
--font-weight-bold: 700;
/* Line Heights */
--line-height-tight: 1.3;
--line-height-normal: 1.4;
--line-height-relaxed: 1.5;
/* Spacing (4px Grid System) */
--spacing-xs: 4px;
--spacing-s: 8px;
--spacing-m: 16px;
--spacing-l: 24px;
--spacing-xl: 32px;
--spacing-2xl: 48px;
/* Border Radius */
--radius-sm: 8px;
--radius-md: 12px;
--radius-lg: 16px;
--radius-xl: 24px;
--radius-full: 50%;
/* Shadows */
--shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.08);
--shadow-md: 0 2px 8px rgba(0, 0, 0, 0.08);
--shadow-lg: 0 4px 12px rgba(0, 0, 0, 0.12);
--shadow-xl: 0 8px 24px rgba(0, 0, 0, 0.2);
/* Transitions */
--transition-fast: 100ms ease-out;
--transition-normal: 200ms ease-out;
--transition-slow: 300ms ease-out;
/* Z-index */
--z-dropdown: 1000;
--z-sticky: 1020;
--z-fixed: 1030;
--z-modal-backdrop: 1040;
--z-modal: 1050;
--z-toast: 1060;
}
/* Tablet */
@media (min-width: 768px) {
:root {
--font-size-display: 32px;
--font-size-h1: 28px;
--font-size-h2: 22px;
--font-size-h3: 20px;
--font-size-body-l: 18px;
--font-size-body-m: 16px;
--font-size-body-s: 14px;
}
}
/* Desktop */
@media (min-width: 1024px) {
:root {
--font-size-display: 36px;
--font-size-h1: 32px;
--font-size-h2: 24px;
}
}
/* ============================================
2. Reset & Base Styles
============================================ */
*, *::before, *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html {
font-size: 16px;
-webkit-text-size-adjust: 100%;
-webkit-tap-highlight-color: transparent;
}
body {
font-family: var(--font-family-base);
font-size: var(--font-size-body-m);
font-weight: var(--font-weight-regular);
line-height: var(--line-height-relaxed);
color: var(--color-black);
background-color: var(--color-gray-100);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* ============================================
3. Typography System
============================================ */
.display {
font-size: var(--font-size-display);
font-weight: var(--font-weight-bold);
line-height: var(--line-height-tight);
letter-spacing: -0.5px;
}
h1, .h1 {
font-size: var(--font-size-h1);
font-weight: var(--font-weight-bold);
line-height: var(--line-height-tight);
letter-spacing: -0.3px;
}
h2, .h2 {
font-size: var(--font-size-h2);
font-weight: var(--font-weight-bold);
line-height: var(--line-height-normal);
letter-spacing: -0.2px;
}
h3, .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);
}
/* ============================================
4. Layout
============================================ */
.container {
width: 100%;
margin: 0 auto;
padding: 0 20px;
}
@media (min-width: 768px) {
.container {
padding: 0 40px;
}
}
@media (min-width: 1024px) {
.container {
max-width: 1200px;
padding: 0 80px;
}
}
/* App Layout */
.app-wrapper {
min-height: 100vh;
display: flex;
flex-direction: column;
background-color: var(--color-gray-100);
}
.app-content {
flex: 1;
padding-bottom: 80px; /* Bottom Navigation 공간 */
}
/* ============================================
5. Top App Bar
============================================ */
.app-bar {
position: sticky;
top: 0;
z-index: var(--z-sticky);
background-color: var(--color-white);
border-bottom: 1px solid var(--color-gray-300);
height: 56px;
display: flex;
align-items: center;
padding: 0 var(--spacing-s);
}
.app-bar__back {
width: 48px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
border: none;
background: none;
cursor: pointer;
color: var(--color-gray-700);
border-radius: var(--radius-full);
transition: background-color var(--transition-fast);
}
.app-bar__back:hover {
background-color: var(--color-gray-100);
}
.app-bar__title {
flex: 1;
font-size: var(--font-size-h3);
font-weight: var(--font-weight-semibold);
text-align: center;
margin-right: 48px; /* 뒤로가기 버튼과 균형 */
}
.app-bar__action {
width: 48px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
border: none;
background: none;
cursor: pointer;
color: var(--color-gray-700);
border-radius: var(--radius-full);
transition: background-color var(--transition-fast);
}
.app-bar__action:hover {
background-color: var(--color-gray-100);
}
/* ============================================
6. Bottom Navigation
============================================ */
.bottom-nav {
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: var(--z-fixed);
background-color: var(--color-white);
border-top: 1px solid var(--color-gray-300);
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.08);
height: 60px;
display: flex;
}
.bottom-nav__item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: var(--spacing-xs);
border: none;
background: none;
color: var(--color-gray-500);
cursor: pointer;
text-decoration: none;
transition: color var(--transition-fast);
min-width: 44px;
min-height: 44px;
}
.bottom-nav__item:hover {
color: var(--color-primary-main);
}
.bottom-nav__item.active {
color: var(--color-primary-main);
}
.bottom-nav__icon {
font-size: 24px;
font-family: 'Material Icons';
font-weight: normal;
font-style: normal;
display: inline-block;
}
.bottom-nav__label {
font-size: var(--font-size-body-s);
font-weight: var(--font-weight-regular);
}
/* ============================================
7. Buttons
============================================ */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: var(--spacing-s);
font-family: var(--font-family-base);
font-size: var(--font-size-button);
font-weight: var(--font-weight-semibold);
line-height: 1.5;
text-align: center;
text-decoration: none;
border: none;
border-radius: var(--radius-sm);
cursor: pointer;
transition: all var(--transition-normal);
min-width: 44px;
min-height: 44px;
}
.btn:disabled {
cursor: not-allowed;
opacity: 0.6;
}
/* Primary Button */
.btn-primary {
background-color: var(--color-primary-main);
color: var(--color-white);
box-shadow: var(--shadow-sm);
}
.btn-primary:hover:not(:disabled) {
background-color: var(--color-primary-light);
}
.btn-primary:active:not(:disabled) {
background-color: var(--color-primary-dark);
transform: scale(0.95);
}
.btn-primary:disabled {
background-color: var(--color-gray-300);
color: var(--color-gray-500);
}
/* Secondary Button */
.btn-secondary {
background-color: var(--color-white);
color: var(--color-primary-main);
border: 2px solid var(--color-primary-main);
}
.btn-secondary:hover:not(:disabled) {
background-color: rgba(227, 30, 36, 0.05);
}
.btn-secondary:active:not(:disabled) {
background-color: rgba(227, 30, 36, 0.1);
transform: scale(0.95);
}
/* Text Button */
.btn-text {
background-color: transparent;
color: var(--color-primary-main);
}
.btn-text:hover:not(:disabled) {
background-color: rgba(227, 30, 36, 0.05);
}
/* Button Sizes */
.btn-lg {
padding: 16px 24px;
height: 48px;
}
.btn-md {
padding: 12px 20px;
height: 44px;
font-size: var(--font-size-body-m);
}
.btn-sm {
padding: 8px 16px;
height: 36px;
font-size: var(--font-size-body-s);
}
/* Full Width Button */
.btn-block {
width: 100%;
}
/* ============================================
8. Cards
============================================ */
.card {
background-color: var(--color-white);
border: 1px solid rgba(224, 224, 224, 1);
border-radius: var(--radius-md);
padding: var(--spacing-l);
box-shadow: var(--shadow-md);
transition: all var(--transition-normal);
}
.card:hover {
border-color: var(--color-primary-main);
box-shadow: var(--shadow-lg);
}
.card.selected {
border: 2px solid var(--color-primary-main);
background-color: rgba(227, 30, 36, 0.02);
}
.card__header {
margin-bottom: var(--spacing-m);
}
.card__title {
font-size: var(--font-size-h3);
font-weight: var(--font-weight-semibold);
margin-bottom: var(--spacing-s);
}
.card__body {
color: var(--color-gray-700);
font-size: var(--font-size-body-m);
}
.card__footer {
margin-top: var(--spacing-m);
padding-top: var(--spacing-m);
border-top: 1px solid var(--color-gray-300);
}
/* ============================================
9. Form Elements
============================================ */
.form-group {
margin-bottom: var(--spacing-m);
}
.form-label {
display: block;
font-size: var(--font-size-body-m);
font-weight: var(--font-weight-medium);
color: var(--color-black);
margin-bottom: var(--spacing-s);
}
.form-label.required::after {
content: '*';
color: var(--color-error);
margin-left: var(--spacing-xs);
}
.form-input {
width: 100%;
height: 48px;
padding: var(--spacing-m);
font-family: var(--font-family-base);
font-size: var(--font-size-body-l);
font-weight: var(--font-weight-regular);
color: var(--color-black);
background-color: var(--color-white);
border: 1px solid var(--color-gray-300);
border-radius: var(--radius-sm);
transition: all var(--transition-normal);
}
.form-input::placeholder {
color: var(--color-gray-500);
}
.form-input:focus {
outline: none;
border: 2px solid var(--color-secondary-main);
box-shadow: 0 0 0 4px rgba(0, 102, 255, 0.1);
}
.form-input:disabled {
background-color: var(--color-gray-100);
color: var(--color-gray-500);
cursor: not-allowed;
}
.form-input.error {
border: 2px solid var(--color-error);
box-shadow: 0 0 0 4px rgba(211, 47, 47, 0.1);
}
.form-error {
display: block;
margin-top: var(--spacing-s);
font-size: var(--font-size-body-s);
color: var(--color-error);
}
/* Textarea */
.form-textarea {
min-height: 120px;
resize: vertical;
padding: var(--spacing-m);
font-size: var(--font-size-body-m);
line-height: var(--line-height-relaxed);
}
/* Checkbox & Radio */
.form-check {
display: flex;
align-items: center;
gap: var(--spacing-s);
cursor: pointer;
min-height: 44px;
}
.form-check input[type="checkbox"],
.form-check input[type="radio"] {
width: 24px;
height: 24px;
cursor: pointer;
accent-color: var(--color-primary-main);
}
/* ============================================
10. Progress & Loading
============================================ */
.progress-bar {
width: 100%;
height: 8px;
background-color: var(--color-gray-100);
border-radius: 4px;
overflow: hidden;
}
.progress-bar__fill {
height: 100%;
background-color: var(--color-primary-main);
transition: width var(--transition-slow);
}
.spinner {
width: 32px;
height: 32px;
border: 3px solid var(--color-gray-100);
border-top-color: var(--color-primary-main);
border-radius: var(--radius-full);
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.loading-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: var(--z-modal);
}
/* ============================================
11. Toast Notification
============================================ */
.toast {
position: fixed;
bottom: 80px;
left: 50%;
transform: translateX(-50%);
z-index: var(--z-toast);
background-color: rgba(26, 26, 26, 0.9);
color: var(--color-white);
padding: var(--spacing-m) var(--spacing-l);
border-radius: var(--radius-sm);
font-size: var(--font-size-body-m);
box-shadow: var(--shadow-xl);
animation: toast-show 200ms ease-out;
max-width: calc(100% - 40px);
}
@keyframes toast-show {
from {
opacity: 0;
transform: translateX(-50%) translateY(20px);
}
to {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
}
.toast.hide {
animation: toast-hide 200ms ease-in forwards;
}
@keyframes toast-hide {
to {
opacity: 0;
transform: translateX(-50%) translateY(20px);
}
}
/* ============================================
12. Modal
============================================ */
.modal-backdrop {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.6);
z-index: var(--z-modal-backdrop);
animation: fade-in 250ms ease-out;
}
@keyframes fade-in {
from { opacity: 0; }
to { opacity: 1; }
}
.modal {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: var(--z-modal);
background-color: var(--color-white);
border-radius: var(--radius-lg);
padding: var(--spacing-l);
max-width: 400px;
width: calc(100% - 40px);
box-shadow: var(--shadow-xl);
animation: modal-show 250ms ease-out;
}
@keyframes modal-show {
from {
opacity: 0;
transform: translate(-50%, -50%) scale(0.95);
}
to {
opacity: 1;
transform: translate(-50%, -50%) scale(1);
}
}
.modal__header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: var(--spacing-m);
}
.modal__title {
font-size: var(--font-size-h2);
font-weight: var(--font-weight-bold);
}
.modal__close {
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border: none;
background: none;
cursor: pointer;
color: var(--color-gray-700);
border-radius: var(--radius-full);
transition: background-color var(--transition-fast);
}
.modal__close:hover {
background-color: var(--color-gray-100);
}
.modal__body {
margin-bottom: var(--spacing-l);
color: var(--color-gray-700);
}
.modal__footer {
display: flex;
gap: var(--spacing-s);
justify-content: flex-end;
}
/* ============================================
13. Bottom Sheet
============================================ */
.bottom-sheet-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: fade-in 300ms ease-out;
}
.bottom-sheet {
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: var(--z-modal);
background-color: var(--color-white);
border-radius: var(--radius-xl) var(--radius-xl) 0 0;
max-height: 80vh;
overflow-y: auto;
box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.15);
animation: slide-up 300ms ease-out;
}
@keyframes slide-up {
from {
transform: translateY(100%);
}
to {
transform: translateY(0);
}
}
.bottom-sheet__handle {
width: 40px;
height: 4px;
background-color: var(--color-gray-300);
border-radius: 2px;
margin: var(--spacing-m) auto;
}
.bottom-sheet__content {
padding: 0 var(--spacing-l) var(--spacing-l);
}
/* ============================================
14. Utility Classes
============================================ */
/* Display */
.d-none { display: none !important; }
.d-block { display: block !important; }
.d-flex { display: flex !important; }
.d-grid { display: grid !important; }
/* Flex */
.flex-column { flex-direction: column !important; }
.flex-wrap { flex-wrap: wrap !important; }
.justify-center { justify-content: center !important; }
.justify-between { justify-content: space-between !important; }
.align-center { align-items: center !important; }
.gap-xs { gap: var(--spacing-xs) !important; }
.gap-s { gap: var(--spacing-s) !important; }
.gap-m { gap: var(--spacing-m) !important; }
.gap-l { gap: var(--spacing-l) !important; }
/* Spacing */
.mt-s { margin-top: var(--spacing-s) !important; }
.mt-m { margin-top: var(--spacing-m) !important; }
.mt-l { margin-top: var(--spacing-l) !important; }
.mb-s { margin-bottom: var(--spacing-s) !important; }
.mb-m { margin-bottom: var(--spacing-m) !important; }
.mb-l { margin-bottom: var(--spacing-l) !important; }
.p-m { padding: var(--spacing-m) !important; }
.p-l { padding: var(--spacing-l) !important; }
/* Text */
.text-center { text-align: center !important; }
.text-right { text-align: right !important; }
.text-primary { color: var(--color-primary-main) !important; }
.text-secondary { color: var(--color-secondary-main) !important; }
.text-success { color: var(--color-success) !important; }
.text-error { color: var(--color-error) !important; }
.text-muted { color: var(--color-gray-500) !important; }
/* Background */
.bg-white { background-color: var(--color-white) !important; }
.bg-gray { background-color: var(--color-gray-100) !important; }
.bg-primary { background-color: var(--color-primary-main) !important; }
/* Border */
.border { border: 1px solid var(--color-gray-300) !important; }
.border-top { border-top: 1px solid var(--color-gray-300) !important; }
.border-bottom { border-bottom: 1px solid var(--color-gray-300) !important; }
/* Visibility */
.hidden { visibility: hidden !important; }
.sr-only {
position: absolute !important;
width: 1px !important;
height: 1px !important;
padding: 0 !important;
margin: -1px !important;
overflow: hidden !important;
clip: rect(0, 0, 0, 0) !important;
white-space: nowrap !important;
border: 0 !important;
}
/* ============================================
15. Accessibility
============================================ */
/* Focus Visible */
*:focus-visible {
outline: 2px solid var(--color-secondary-main);
outline-offset: 2px;
}
/* Skip Link */
.skip-link {
position: absolute;
top: -40px;
left: 0;
background-color: var(--color-primary-main);
color: var(--color-white);
padding: var(--spacing-s) var(--spacing-m);
text-decoration: none;
z-index: 9999;
}
.skip-link:focus {
top: 0;
}
/* ============================================
16. Animations
============================================ */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}

View File

@ -0,0 +1,555 @@
/**
* KT AI 기반 소상공인 이벤트 자동 생성 서비스
* 공통 JavaScript 유틸리티
*
* 작성일: 2025-01-20
* 버전: 1.0
*/
(function() {
'use strict';
// ============================================
// 1. 상태 관리
// ============================================
window.AppState = {
// 사용자 정보
user: null,
// 매장 정보
store: null,
// 현재 이벤트 정보
currentEvent: null,
// localStorage에서 데이터 로드
load() {
try {
this.user = JSON.parse(localStorage.getItem('kt_user') || 'null');
this.store = JSON.parse(localStorage.getItem('kt_store') || 'null');
this.currentEvent = JSON.parse(localStorage.getItem('kt_current_event') || 'null');
} catch (e) {
console.error('Failed to load app state:', e);
}
},
// localStorage에 데이터 저장
save() {
try {
localStorage.setItem('kt_user', JSON.stringify(this.user));
localStorage.setItem('kt_store', JSON.stringify(this.store));
localStorage.setItem('kt_current_event', JSON.stringify(this.currentEvent));
} catch (e) {
console.error('Failed to save app state:', e);
}
},
// 상태 초기화
clear() {
this.user = null;
this.store = null;
this.currentEvent = null;
localStorage.removeItem('kt_user');
localStorage.removeItem('kt_store');
localStorage.removeItem('kt_current_event');
}
};
// 페이지 로드 시 상태 복원
document.addEventListener('DOMContentLoaded', function() {
window.AppState.load();
});
// ============================================
// 2. Toast 알림
// ============================================
window.Toast = {
show(message, duration = 3000) {
// 기존 토스트 제거
const existingToast = document.querySelector('.toast');
if (existingToast) {
existingToast.remove();
}
// 새 토스트 생성
const toast = document.createElement('div');
toast.className = 'toast';
toast.textContent = message;
toast.setAttribute('role', 'status');
toast.setAttribute('aria-live', 'polite');
document.body.appendChild(toast);
// 자동 제거
setTimeout(() => {
toast.classList.add('hide');
setTimeout(() => {
toast.remove();
}, 200);
}, duration);
},
success(message) {
this.show('✓ ' + message);
},
error(message) {
this.show('✕ ' + message);
},
info(message) {
this.show(' ' + message);
}
};
// ============================================
// 3. Modal 다이얼로그
// ============================================
window.Modal = {
show(options) {
const {
title = '',
body = '',
confirmText = '확인',
cancelText = '취소',
onConfirm = () => {},
onCancel = () => {},
showCancel = true
} = options;
// 백드롭 생성
const backdrop = document.createElement('div');
backdrop.className = 'modal-backdrop';
// 모달 생성
const modal = document.createElement('div');
modal.className = 'modal';
modal.setAttribute('role', 'dialog');
modal.setAttribute('aria-labelledby', 'modal-title');
modal.setAttribute('aria-modal', 'true');
modal.innerHTML = `
<div class="modal__header">
<h2 class="modal__title" id="modal-title">${title}</h2>
<button class="modal__close" aria-label="닫기">
<span class="material-icons">close</span>
</button>
</div>
<div class="modal__body">
${body}
</div>
<div class="modal__footer">
${showCancel ? `<button class="btn btn-secondary btn-md modal__cancel">${cancelText}</button>` : ''}
<button class="btn btn-primary btn-md modal__confirm">${confirmText}</button>
</div>
`;
// 닫기 함수
const close = () => {
backdrop.remove();
modal.remove();
document.body.style.overflow = '';
};
// 이벤트 리스너
modal.querySelector('.modal__close').addEventListener('click', () => {
close();
onCancel();
});
if (showCancel) {
modal.querySelector('.modal__cancel').addEventListener('click', () => {
close();
onCancel();
});
}
modal.querySelector('.modal__confirm').addEventListener('click', () => {
close();
onConfirm();
});
backdrop.addEventListener('click', (e) => {
if (e.target === backdrop) {
close();
onCancel();
}
});
// 추가
document.body.appendChild(backdrop);
document.body.appendChild(modal);
document.body.style.overflow = 'hidden';
// 첫 번째 버튼에 포커스
modal.querySelector('button').focus();
},
confirm(title, body, onConfirm) {
this.show({
title,
body,
confirmText: '확인',
cancelText: '취소',
onConfirm,
showCancel: true
});
},
alert(title, body, onConfirm = () => {}) {
this.show({
title,
body,
confirmText: '확인',
onConfirm,
showCancel: false
});
}
};
// ============================================
// 4. Bottom Sheet
// ============================================
window.BottomSheet = {
show(content, options = {}) {
const {
onClose = () => {}
} = options;
// 백드롭 생성
const backdrop = document.createElement('div');
backdrop.className = 'bottom-sheet-backdrop';
// Bottom Sheet 생성
const sheet = document.createElement('div');
sheet.className = 'bottom-sheet';
sheet.setAttribute('role', 'dialog');
sheet.setAttribute('aria-modal', 'true');
sheet.innerHTML = `
<div class="bottom-sheet__handle"></div>
<div class="bottom-sheet__content">
${content}
</div>
`;
// 닫기 함수
const close = () => {
backdrop.remove();
sheet.remove();
document.body.style.overflow = '';
onClose();
};
// 이벤트 리스너
backdrop.addEventListener('click', close);
// Handle 드래그로 닫기 (간단한 구현)
let startY = 0;
const handle = sheet.querySelector('.bottom-sheet__handle');
handle.addEventListener('touchstart', (e) => {
startY = e.touches[0].clientY;
});
handle.addEventListener('touchend', (e) => {
const endY = e.changedTouches[0].clientY;
if (endY - startY > 50) { // 50px 이상 드래그하면 닫기
close();
}
});
// 추가
document.body.appendChild(backdrop);
document.body.appendChild(sheet);
document.body.style.overflow = 'hidden';
return { close };
}
};
// ============================================
// 5. 로딩 인디케이터
// ============================================
window.Loading = {
show(message = '처리중...') {
// 기존 로딩 제거
this.hide();
const overlay = document.createElement('div');
overlay.className = 'loading-overlay';
overlay.id = 'app-loading';
overlay.innerHTML = `
<div style="text-align: center; color: white;">
<div class="spinner" style="margin: 0 auto 16px;"></div>
<div class="body-m">${message}</div>
</div>
`;
document.body.appendChild(overlay);
document.body.style.overflow = 'hidden';
},
hide() {
const overlay = document.getElementById('app-loading');
if (overlay) {
overlay.remove();
document.body.style.overflow = '';
}
}
};
// ============================================
// 6. 폼 검증 유틸리티
// ============================================
window.FormValidator = {
// 이메일 검증
isValidEmail(email) {
const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return re.test(email);
},
// 전화번호 검증 (010-XXXX-XXXX)
isValidPhone(phone) {
const re = /^010-\d{4}-\d{4}$/;
return re.test(phone);
},
// 사업자번호 검증 (XXX-XX-XXXXX)
isValidBusinessNumber(number) {
const re = /^\d{3}-\d{2}-\d{5}$/;
return re.test(number);
},
// 이름 검증 (2자 이상)
isValidName(name) {
return name && name.length >= 2;
},
// 필드에 에러 표시
showError(inputElement, message) {
inputElement.classList.add('error');
// 기존 에러 메시지 제거
const existingError = inputElement.parentElement.querySelector('.form-error');
if (existingError) {
existingError.remove();
}
// 새 에러 메시지 추가
if (message) {
const errorDiv = document.createElement('div');
errorDiv.className = 'form-error';
errorDiv.textContent = message;
errorDiv.setAttribute('role', 'alert');
inputElement.parentElement.appendChild(errorDiv);
}
},
// 필드에서 에러 제거
clearError(inputElement) {
inputElement.classList.remove('error');
const errorDiv = inputElement.parentElement.querySelector('.form-error');
if (errorDiv) {
errorDiv.remove();
}
},
// 전체 폼 검증
validateForm(formElement) {
let isValid = true;
const inputs = formElement.querySelectorAll('[required]');
inputs.forEach(input => {
if (!input.value.trim()) {
this.showError(input, '필수 입력 항목입니다.');
isValid = false;
} else {
this.clearError(input);
}
});
return isValid;
}
};
// ============================================
// 7. 네비게이션 유틸리티
// ============================================
window.Navigation = {
// 페이지 이동
goto(page) {
window.location.href = page;
},
// 뒤로가기
back() {
window.history.back();
},
// Bottom Navigation 활성화 상태 설정
updateBottomNav(activePage) {
const navItems = document.querySelectorAll('.bottom-nav__item');
navItems.forEach(item => {
if (item.getAttribute('data-page') === activePage) {
item.classList.add('active');
} else {
item.classList.remove('active');
}
});
}
};
// ============================================
// 8. 유틸리티 함수
// ============================================
window.Utils = {
// 숫자 포맷 (1000 -> 1,000)
formatNumber(num) {
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
},
// 날짜 포맷 (YYYY-MM-DD)
formatDate(date) {
const d = new Date(date);
const year = d.getFullYear();
const month = String(d.getMonth() + 1).padStart(2, '0');
const day = String(d.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
},
// 날짜 포맷 (YYYY.MM.DD)
formatDateDot(date) {
const d = new Date(date);
const year = d.getFullYear();
const month = String(d.getMonth() + 1).padStart(2, '0');
const day = String(d.getDate()).padStart(2, '0');
return `${year}.${month}.${day}`;
},
// 랜덤 ID 생성
generateId() {
return 'id-' + Date.now() + '-' + Math.random().toString(36).substr(2, 9);
},
// Debounce 함수
debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
},
// AI 처리 시뮬레이션
simulateAI(duration = 3000) {
return new Promise(resolve => {
setTimeout(resolve, duration);
});
},
// 이미지 로드 체크
preloadImage(src) {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = reject;
img.src = src;
});
}
};
// ============================================
// 9. 공통 이벤트 핸들러
// ============================================
// 뒤로가기 버튼
document.addEventListener('click', function(e) {
if (e.target.closest('.app-bar__back')) {
e.preventDefault();
window.Navigation.back();
}
});
// 입력 필드 실시간 검증
document.addEventListener('input', function(e) {
const input = e.target;
// 이메일 필드
if (input.type === 'email' && input.value) {
if (!window.FormValidator.isValidEmail(input.value)) {
window.FormValidator.showError(input, '올바른 이메일 형식을 입력하세요.');
} else {
window.FormValidator.clearError(input);
}
}
// 전화번호 필드
if (input.name === 'phone' && input.value) {
// 자동 하이픈 추가
let value = input.value.replace(/[^0-9]/g, '');
if (value.length > 3 && value.length <= 7) {
value = value.slice(0, 3) + '-' + value.slice(3);
} else if (value.length > 7) {
value = value.slice(0, 3) + '-' + value.slice(3, 7) + '-' + value.slice(7, 11);
}
input.value = value;
// 검증
if (value.length === 13) {
if (!window.FormValidator.isValidPhone(value)) {
window.FormValidator.showError(input, '올바른 전화번호 형식을 입력하세요.');
} else {
window.FormValidator.clearError(input);
}
}
}
// 사업자번호 필드 (name이 businessNumber인 경우도 처리)
if ((input.name === 'business_number' || input.name === 'businessNumber') && input.value) {
// 자동 하이픈 추가
let value = input.value.replace(/[^0-9]/g, '');
if (value.length > 3 && value.length <= 5) {
value = value.slice(0, 3) + '-' + value.slice(3);
} else if (value.length > 5) {
value = value.slice(0, 3) + '-' + value.slice(3, 5) + '-' + value.slice(5, 10);
}
input.value = value;
// 검증 (완전한 형식일 때만)
if (value.length === 12) {
if (!window.FormValidator.isValidBusinessNumber(value)) {
window.FormValidator.showError(input, '올바른 사업자번호 형식을 입력하세요.');
} else {
window.FormValidator.clearError(input);
}
} else {
// 입력 중일 때는 에러 제거
window.FormValidator.clearError(input);
}
}
});
// Material Icons 로드 확인
if (!document.querySelector('link[href*="material-icons"]')) {
const link = document.createElement('link');
link.href = 'https://fonts.googleapis.com/icon?family=Material+Icons';
link.rel = 'stylesheet';
document.head.appendChild(link);
}
// Pretendard 폰트 로드 확인
if (!document.querySelector('link[href*="pretendard"]')) {
const link = document.createElement('link');
link.href = 'https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/static/pretendard.min.css';
link.rel = 'stylesheet';
document.head.appendChild(link);
}
console.log('KT Event Marketing App - Common JS loaded');
})();

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,247 @@
/**
* KT AI 기반 소상공인 이벤트 자동 생성 서비스
* Button Components
*
* 참조: design/uiux/style-guide.md (Section 6.1)
* 작성일: 2025-01-20
*/
/* ========================================
Button Base
======================================== */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: var(--spacing-s);
border: none;
border-radius: var(--radius-s);
font-family: var(--font-family);
font-weight: var(--font-weight-semibold);
text-align: center;
text-decoration: none;
cursor: pointer;
transition: all var(--transition-fast) var(--ease-out);
user-select: none;
white-space: nowrap;
/* Touch Target */
min-height: var(--touch-target-min);
}
.btn:focus-visible {
outline: 2px solid var(--color-secondary);
outline-offset: 2px;
}
/* ========================================
Primary Button
======================================== */
.btn-primary {
background-color: var(--color-primary);
color: var(--color-white);
box-shadow: var(--shadow-primary);
}
.btn-primary:hover:not(:disabled) {
background-color: var(--color-primary-light);
}
.btn-primary:active:not(:disabled) {
background-color: var(--color-primary-dark);
}
.btn-primary:disabled {
background-color: var(--color-gray-300);
color: var(--color-gray-500);
box-shadow: none;
cursor: not-allowed;
}
/* ========================================
Secondary Button
======================================== */
.btn-secondary {
background-color: var(--color-white);
color: var(--color-primary);
border: 2px solid var(--color-primary);
box-shadow: none;
}
.btn-secondary:hover:not(:disabled) {
background-color: #FFF5F5; /* 5% Red tint */
}
.btn-secondary:active:not(:disabled) {
background-color: #FFEBEB; /* 10% Red tint */
}
.btn-secondary:disabled {
background-color: var(--color-white);
border-color: var(--color-gray-300);
color: var(--color-gray-500);
cursor: not-allowed;
}
/* ========================================
Text Button
======================================== */
.btn-text {
background-color: transparent;
color: var(--color-primary);
box-shadow: none;
}
.btn-text:hover:not(:disabled) {
background-color: #FFF5F5; /* 5% Red tint */
}
.btn-text:active:not(:disabled) {
background-color: #FFEBEB; /* 10% Red tint */
}
.btn-text:disabled {
background-color: transparent;
color: var(--color-gray-500);
cursor: not-allowed;
}
/* ========================================
Button Sizes
======================================== */
/* Large - 주요 CTA */
.btn-large {
height: var(--button-height-large);
padding: 16px 24px;
font-size: var(--font-size-button);
line-height: var(--line-height-button);
}
/* Medium - 일반 액션 */
.btn-medium {
height: var(--button-height-medium);
padding: 12px 20px;
font-size: var(--font-size-body-medium);
line-height: var(--line-height-body-medium);
}
/* Small - 보조 액션 */
.btn-small {
height: var(--button-height-small);
padding: 8px 16px;
font-size: var(--font-size-body-small);
line-height: var(--line-height-body-small);
}
/* ========================================
Button with Icon
======================================== */
.btn-icon-left {
flex-direction: row;
}
.btn-icon-right {
flex-direction: row-reverse;
}
.btn .icon {
width: 20px;
height: 20px;
flex-shrink: 0;
}
.btn-large .icon {
width: 24px;
height: 24px;
}
.btn-small .icon {
width: 16px;
height: 16px;
}
/* ========================================
Full Width Button
======================================== */
.btn-block {
width: 100%;
display: flex;
}
/* ========================================
Loading State
======================================== */
.btn-loading {
position: relative;
color: transparent;
pointer-events: none;
}
.btn-loading::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 16px;
height: 16px;
margin: -8px 0 0 -8px;
border: 2px solid currentColor;
border-top-color: transparent;
border-radius: 50%;
animation: spin 0.8s linear infinite;
color: var(--color-white);
}
.btn-secondary.btn-loading::after,
.btn-text.btn-loading::after {
color: var(--color-primary);
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* ========================================
Button Group
======================================== */
.btn-group {
display: flex;
gap: 12px; /* S + XS */
flex-wrap: wrap;
}
.btn-group-vertical {
flex-direction: column;
align-items: stretch;
}
.btn-group-vertical .btn {
width: 100%;
}
/* ========================================
Icon-only Button
======================================== */
.btn-icon-only {
width: var(--button-height-medium);
padding: 0;
aspect-ratio: 1;
}
.btn-icon-only.btn-large {
width: var(--button-height-large);
}
.btn-icon-only.btn-small {
width: var(--button-height-small);
}

View File

@ -0,0 +1,291 @@
/**
* KT AI 기반 소상공인 이벤트 자동 생성 서비스
* Card Components
*
* 참조: design/uiux/style-guide.md (Section 6.2, 8.2)
* 작성일: 2025-01-20
*/
/* ========================================
Card Base
======================================== */
.card {
background-color: var(--color-white);
border: 1px solid #E0E0E0;
border-radius: var(--radius-m);
box-shadow: var(--shadow-md);
padding: var(--spacing-l);
transition: all var(--transition-fast) var(--ease-out);
}
.card:hover {
border-color: var(--color-primary);
box-shadow: 0 4px 12px rgba(227, 30, 36, 0.12);
}
.card-selected {
border: 2px solid var(--color-primary);
padding: calc(var(--spacing-l) - 1px); /* 테두리 2px 보정 */
}
/* ========================================
Event Card
======================================== */
.event-card {
background-color: var(--color-white);
border-radius: var(--radius-m);
box-shadow: var(--shadow-md);
overflow: hidden;
transition: all var(--transition-fast) var(--ease-out);
}
.event-card:hover {
box-shadow: var(--shadow-lg);
transform: translateY(-2px);
}
.event-card__image {
width: 100%;
aspect-ratio: 16 / 9;
object-fit: cover;
display: block;
}
.event-card__content {
padding: var(--spacing-l);
}
.event-card__title {
font-size: var(--font-size-h3);
font-weight: var(--font-weight-semibold);
line-height: var(--line-height-h3);
color: var(--color-black);
margin-bottom: var(--spacing-s);
}
.event-card__meta {
font-size: var(--font-size-body-small);
line-height: var(--line-height-body-small);
color: var(--color-gray-500);
margin-bottom: var(--spacing-m);
display: flex;
align-items: center;
gap: var(--spacing-s);
}
.event-card__footer {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--spacing-s);
margin-top: var(--spacing-m);
}
/* ========================================
Stat Card (지표 카드)
======================================== */
.stat-card {
background-color: var(--color-white);
border-radius: var(--radius-l);
box-shadow: var(--shadow-md);
padding: var(--spacing-l);
transition: all var(--transition-fast) var(--ease-out);
}
.stat-card:hover {
box-shadow: var(--shadow-lg);
}
.stat-card__header {
display: flex;
align-items: center;
gap: var(--spacing-s);
margin-bottom: var(--spacing-m);
}
.stat-card__icon {
width: 24px;
height: 24px;
color: var(--color-primary);
}
.stat-card__label {
font-size: var(--font-size-body-small);
line-height: var(--line-height-body-small);
color: var(--color-gray-700);
}
.stat-card__value {
font-size: var(--font-size-display);
font-weight: var(--font-weight-bold);
line-height: var(--line-height-display);
color: var(--color-black);
margin-bottom: var(--spacing-s);
}
.stat-card__change {
font-size: var(--font-size-body-small);
line-height: var(--line-height-body-small);
display: flex;
align-items: center;
gap: var(--spacing-xs);
}
.stat-card__change--positive {
color: var(--color-success);
}
.stat-card__change--negative {
color: var(--color-error);
}
.stat-card__change--neutral {
color: var(--color-gray-500);
}
/* Gradient variant */
.stat-card--gradient {
background: var(--gradient-primary);
color: var(--color-white);
}
.stat-card--gradient .stat-card__label,
.stat-card--gradient .stat-card__value,
.stat-card--gradient .stat-card__change {
color: var(--color-white);
}
.stat-card--gradient .stat-card__icon {
color: var(--color-white);
}
/* ========================================
AI Result Card (AI 생성 옵션 카드)
======================================== */
.ai-card {
position: relative;
background-color: var(--color-white);
border: 1px solid #E0E0E0;
border-radius: var(--radius-m);
padding: var(--spacing-l);
cursor: pointer;
transition: all var(--transition-fast) var(--ease-out);
}
.ai-card:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
transform: translateY(-2px);
}
.ai-card--selected {
border: 2px solid var(--color-primary);
background-color: #FFF5F5;
padding: calc(var(--spacing-l) - 1px);
}
.ai-card__radio {
position: absolute;
top: var(--spacing-m);
right: var(--spacing-m);
}
.ai-card__preview {
width: 100%;
aspect-ratio: 16 / 9;
border-radius: var(--radius-s);
object-fit: cover;
margin-bottom: var(--spacing-m);
}
.ai-card__title {
font-size: var(--font-size-h3);
font-weight: var(--font-weight-semibold);
line-height: var(--line-height-h3);
color: var(--color-black);
margin-bottom: var(--spacing-s);
}
.ai-card__description {
font-size: var(--font-size-body-small);
line-height: var(--line-height-body-small);
color: var(--color-gray-500);
margin-bottom: var(--spacing-m);
}
.ai-card__actions {
display: flex;
gap: var(--spacing-s);
}
/* AI 추천 배지 */
.ai-badge {
position: absolute;
top: var(--spacing-m);
left: var(--spacing-m);
background: var(--gradient-secondary);
color: var(--color-white);
font-size: var(--font-size-body-small);
font-weight: var(--font-weight-semibold);
padding: 4px 12px;
border-radius: var(--radius-pill);
z-index: 1;
}
/* ========================================
Card Grid
======================================== */
.card-grid {
display: grid;
gap: var(--spacing-m);
grid-template-columns: repeat(1, 1fr);
}
@media (min-width: 768px) {
.card-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (min-width: 1024px) {
.card-grid {
grid-template-columns: repeat(3, 1fr);
}
}
/* ========================================
Clickable Card
======================================== */
.card-clickable {
cursor: pointer;
text-decoration: none;
color: inherit;
}
.card-clickable:hover {
text-decoration: none;
}
/* ========================================
Card with Image
======================================== */
.card-image {
padding: 0;
overflow: hidden;
}
.card-image__img {
width: 100%;
height: 200px;
object-fit: cover;
}
.card-image__content {
padding: var(--spacing-l);
}

View File

@ -0,0 +1,339 @@
/**
* KT AI 기반 소상공인 이벤트 자동 생성 서비스
* Input Components
*
* 참조: design/uiux/style-guide.md (Section 6.3, 6.4)
* 작성일: 2025-01-20
*/
/* ========================================
Form Group
======================================== */
.form-group {
margin-bottom: var(--spacing-m);
}
.form-label {
display: block;
font-size: var(--font-size-body-medium);
font-weight: var(--font-weight-medium);
line-height: var(--line-height-body-medium);
color: var(--color-black);
margin-bottom: var(--spacing-s);
}
.form-label--required::after {
content: '*';
color: var(--color-error);
margin-left: 4px;
}
.form-helper {
display: block;
font-size: var(--font-size-body-small);
line-height: var(--line-height-body-small);
color: var(--color-gray-500);
margin-top: var(--spacing-xs);
}
.form-error {
display: block;
font-size: var(--font-size-body-small);
line-height: var(--line-height-body-small);
color: var(--color-error);
margin-top: var(--spacing-xs);
}
/* ========================================
Text Input
======================================== */
.input {
width: 100%;
height: var(--input-height);
padding: var(--spacing-m);
border: 1px solid var(--color-gray-300);
border-radius: var(--radius-s);
background-color: var(--color-white);
font-family: var(--font-family);
font-size: var(--font-size-body-large);
font-weight: var(--font-weight-regular);
line-height: var(--line-height-body-large);
color: var(--color-black);
transition: all var(--transition-fast) var(--ease-out);
}
.input::placeholder {
color: var(--color-gray-500);
font-style: italic;
}
.input:focus {
outline: none;
border: 2px solid var(--color-secondary);
box-shadow: var(--shadow-secondary);
padding: calc(var(--spacing-m) - 1px); /* 테두리 2px 보정 */
}
.input:disabled {
background-color: var(--color-gray-100);
border-color: #E0E0E0;
color: var(--color-gray-500);
cursor: not-allowed;
}
/* Error State */
.input-error {
border: 2px solid var(--color-error);
box-shadow: var(--shadow-error);
padding: calc(var(--spacing-m) - 1px);
}
.input-error:focus {
border-color: var(--color-error);
box-shadow: var(--shadow-error);
}
/* ========================================
Textarea
======================================== */
.textarea {
width: 100%;
min-height: var(--textarea-min-height);
padding: var(--spacing-m);
border: 1px solid var(--color-gray-300);
border-radius: var(--radius-s);
background-color: var(--color-white);
font-family: var(--font-family);
font-size: var(--font-size-body-medium);
font-weight: var(--font-weight-regular);
line-height: var(--line-height-body-medium);
color: var(--color-black);
resize: vertical;
transition: all var(--transition-fast) var(--ease-out);
}
.textarea::placeholder {
color: var(--color-gray-500);
font-style: italic;
}
.textarea:focus {
outline: none;
border: 2px solid var(--color-secondary);
box-shadow: var(--shadow-secondary);
padding: calc(var(--spacing-m) - 1px);
}
.textarea:disabled {
background-color: var(--color-gray-100);
border-color: #E0E0E0;
color: var(--color-gray-500);
cursor: not-allowed;
resize: none;
}
/* ========================================
Select
======================================== */
.select {
width: 100%;
height: var(--input-height);
padding: var(--spacing-m);
padding-right: 40px;
border: 1px solid var(--color-gray-300);
border-radius: var(--radius-s);
background-color: var(--color-white);
background-image: url('data:image/svg+xml;charset=UTF-8,<svg width="12" height="8" viewBox="0 0 12 8" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M1 1L6 6L11 1" stroke="%239E9E9E" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>');
background-repeat: no-repeat;
background-position: right 16px center;
font-family: var(--font-family);
font-size: var(--font-size-body-large);
font-weight: var(--font-weight-regular);
line-height: var(--line-height-body-large);
color: var(--color-black);
cursor: pointer;
transition: all var(--transition-fast) var(--ease-out);
}
.select:focus {
outline: none;
border: 2px solid var(--color-secondary);
box-shadow: var(--shadow-secondary);
padding: calc(var(--spacing-m) - 1px);
padding-right: 39px;
}
.select:disabled {
background-color: var(--color-gray-100);
border-color: #E0E0E0;
color: var(--color-gray-500);
cursor: not-allowed;
}
/* ========================================
Checkbox
======================================== */
.checkbox-wrapper {
display: inline-flex;
align-items: center;
gap: var(--spacing-s);
cursor: pointer;
user-select: none;
min-height: var(--touch-target-min);
}
.checkbox {
appearance: none;
width: 24px;
height: 24px;
border: 2px solid var(--color-gray-300);
border-radius: var(--radius-xs);
background-color: var(--color-white);
cursor: pointer;
position: relative;
transition: all var(--transition-fast) var(--ease-out);
flex-shrink: 0;
}
.checkbox:checked {
background-color: var(--color-primary);
border-color: var(--color-primary);
}
.checkbox:checked::after {
content: '';
position: absolute;
top: 2px;
left: 6px;
width: 6px;
height: 12px;
border: solid var(--color-white);
border-width: 0 2px 2px 0;
transform: rotate(45deg);
}
.checkbox:focus-visible {
outline: 2px solid var(--color-secondary);
outline-offset: 2px;
}
.checkbox:disabled {
background-color: var(--color-gray-100);
border-color: #E0E0E0;
cursor: not-allowed;
}
.checkbox-label {
font-size: var(--font-size-body-medium);
line-height: var(--line-height-body-medium);
color: var(--color-black);
}
/* ========================================
Radio Button
======================================== */
.radio-wrapper {
display: inline-flex;
align-items: center;
gap: var(--spacing-s);
cursor: pointer;
user-select: none;
min-height: var(--touch-target-min);
}
.radio {
appearance: none;
width: 24px;
height: 24px;
border: 2px solid var(--color-gray-300);
border-radius: 50%;
background-color: var(--color-white);
cursor: pointer;
position: relative;
transition: all var(--transition-fast) var(--ease-out);
flex-shrink: 0;
}
.radio:checked {
border-color: var(--color-primary);
}
.radio:checked::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 12px;
height: 12px;
background-color: var(--color-primary);
border-radius: 50%;
transform: translate(-50%, -50%);
}
.radio:focus-visible {
outline: 2px solid var(--color-secondary);
outline-offset: 2px;
}
.radio:disabled {
background-color: var(--color-gray-100);
border-color: #E0E0E0;
cursor: not-allowed;
}
.radio-label {
font-size: var(--font-size-body-medium);
line-height: var(--line-height-body-medium);
color: var(--color-black);
}
/* ========================================
Radio/Checkbox Group
======================================== */
.radio-group,
.checkbox-group {
display: flex;
flex-direction: column;
gap: var(--spacing-m);
}
.radio-group--inline,
.checkbox-group--inline {
flex-direction: row;
flex-wrap: wrap;
gap: var(--spacing-l);
}
/* ========================================
Input with Icon
======================================== */
.input-icon-wrapper {
position: relative;
}
.input-icon-wrapper .input {
padding-left: 48px;
}
.input-icon {
position: absolute;
left: 16px;
top: 50%;
transform: translateY(-50%);
width: 20px;
height: 20px;
color: var(--color-gray-500);
pointer-events: none;
}
.input-icon-wrapper .input:focus ~ .input-icon {
color: var(--color-secondary);
}

View File

@ -0,0 +1,321 @@
/**
* KT AI 기반 소상공인 이벤트 자동 생성 서비스
* Loading Components
*
* 참조: design/uiux/style-guide.md (Section 8.1)
* 작성일: 2025-01-20
*/
/* ========================================
Spinner (간단한 로딩)
======================================== */
.spinner {
display: inline-block;
width: 32px;
height: 32px;
border: 3px solid var(--color-gray-300);
border-top-color: var(--color-primary);
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
.spinner--small {
width: 16px;
height: 16px;
border-width: 2px;
}
.spinner--large {
width: 48px;
height: 48px;
border-width: 4px;
}
.spinner--secondary {
border-top-color: var(--color-secondary);
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* ========================================
Loading Skeleton
======================================== */
.skeleton {
background-color: var(--color-gray-100);
border-radius: var(--radius-s);
position: relative;
overflow: hidden;
}
.skeleton::after {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(
90deg,
transparent,
rgba(255, 255, 255, 0.5),
transparent
);
animation: shimmer 1.5s infinite;
}
@keyframes shimmer {
to { left: 100%; }
}
.skeleton-text {
height: 16px;
margin-bottom: var(--spacing-s);
}
.skeleton-text--title {
height: 24px;
width: 60%;
}
.skeleton-text--body {
height: 14px;
width: 100%;
}
.skeleton-text--body:last-child {
width: 80%;
}
.skeleton-image {
width: 100%;
aspect-ratio: 16 / 9;
}
.skeleton-circle {
width: 48px;
height: 48px;
border-radius: 50%;
}
.skeleton-button {
height: 48px;
width: 120px;
border-radius: var(--radius-s);
}
/* ========================================
AI Progress Indicator
======================================== */
.ai-progress {
display: flex;
flex-direction: column;
align-items: center;
padding: var(--spacing-2xl) var(--spacing-l);
background-color: var(--color-white);
border: 1px solid #E0E0E0;
border-radius: var(--radius-m);
text-align: center;
}
.ai-progress__icon {
width: 48px;
height: 48px;
margin-bottom: var(--spacing-m);
color: var(--color-secondary);
animation: pulse 2s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.6; transform: scale(0.95); }
}
.ai-progress__title {
font-size: var(--font-size-h3);
font-weight: var(--font-weight-semibold);
line-height: var(--line-height-h3);
color: var(--color-black);
margin-bottom: var(--spacing-m);
}
.ai-progress__bar {
width: 100%;
height: 8px;
background-color: var(--color-gray-100);
border-radius: var(--radius-xs);
overflow: hidden;
margin-bottom: var(--spacing-s);
}
.ai-progress__fill {
height: 100%;
background-color: var(--color-secondary);
border-radius: var(--radius-xs);
transition: width var(--transition-normal) var(--ease-out);
}
.ai-progress__percentage {
font-size: var(--font-size-body-medium);
font-weight: var(--font-weight-semibold);
color: var(--color-black);
margin-bottom: var(--spacing-s);
}
.ai-progress__time {
font-size: var(--font-size-body-small);
color: var(--color-gray-700);
}
/* ========================================
Skeleton Card
======================================== */
.skeleton-card {
background-color: var(--color-white);
border: 1px solid #E0E0E0;
border-radius: var(--radius-m);
padding: var(--spacing-l);
}
.skeleton-card__header {
display: flex;
align-items: center;
gap: var(--spacing-m);
margin-bottom: var(--spacing-m);
}
.skeleton-card__avatar {
width: 48px;
height: 48px;
border-radius: 50%;
}
.skeleton-card__title {
flex: 1;
}
.skeleton-card__image {
width: 100%;
aspect-ratio: 16 / 9;
margin-bottom: var(--spacing-m);
}
.skeleton-card__content {
display: flex;
flex-direction: column;
gap: var(--spacing-s);
}
.skeleton-card__footer {
display: flex;
gap: var(--spacing-s);
margin-top: var(--spacing-m);
}
/* ========================================
Loading Overlay
======================================== */
.loading-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: var(--z-modal);
}
.loading-overlay__content {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--spacing-m);
background-color: var(--color-white);
padding: var(--spacing-2xl);
border-radius: var(--radius-m);
box-shadow: var(--shadow-xl);
}
.loading-overlay__text {
font-size: var(--font-size-body-medium);
color: var(--color-black);
text-align: center;
}
/* ========================================
Dots Loader
======================================== */
.dots-loader {
display: inline-flex;
align-items: center;
gap: var(--spacing-s);
}
.dots-loader__dot {
width: 8px;
height: 8px;
border-radius: 50%;
background-color: var(--color-primary);
animation: dotPulse 1.4s infinite ease-in-out;
}
.dots-loader__dot:nth-child(1) {
animation-delay: -0.32s;
}
.dots-loader__dot:nth-child(2) {
animation-delay: -0.16s;
}
@keyframes dotPulse {
0%, 80%, 100% {
opacity: 0.3;
transform: scale(0.8);
}
40% {
opacity: 1;
transform: scale(1);
}
}
/* ========================================
Linear Progress
======================================== */
.linear-progress {
width: 100%;
height: 4px;
background-color: var(--color-gray-100);
border-radius: var(--radius-xs);
overflow: hidden;
}
.linear-progress__bar {
height: 100%;
background-color: var(--color-primary);
transition: width var(--transition-normal) var(--ease-out);
}
.linear-progress--indeterminate .linear-progress__bar {
width: 30%;
animation: indeterminate 1.5s infinite;
}
@keyframes indeterminate {
0% {
transform: translateX(-100%);
}
100% {
transform: translateX(400%);
}
}

View File

@ -0,0 +1,390 @@
/**
* KT AI 기반 소상공인 이벤트 자동 생성 서비스
* Modal Components
*
* 참조: design/uiux/style-guide.md
* 작성일: 2025-01-20
*/
/* ========================================
Modal Backdrop
======================================== */
.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);
display: flex;
align-items: center;
justify-content: center;
padding: var(--spacing-l);
animation: fadeIn var(--transition-fast) var(--ease-out);
}
/* ========================================
Modal
======================================== */
.modal {
background-color: var(--color-white);
border-radius: var(--radius-m);
box-shadow: var(--shadow-xl);
max-width: 600px;
width: 100%;
max-height: 90vh;
display: flex;
flex-direction: column;
z-index: var(--z-modal);
animation: scaleIn var(--transition-fast) var(--ease-out);
}
.modal--small {
max-width: 400px;
}
.modal--large {
max-width: 800px;
}
.modal--fullscreen {
max-width: 100%;
max-height: 100%;
height: 100%;
border-radius: 0;
}
.modal__header {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--spacing-l);
border-bottom: 1px solid var(--color-gray-300);
}
.modal__title {
font-size: var(--font-size-h3);
font-weight: var(--font-weight-semibold);
line-height: var(--line-height-h3);
color: var(--color-black);
}
.modal__close {
width: 24px;
height: 24px;
color: var(--color-gray-500);
cursor: pointer;
transition: color var(--transition-fast) var(--ease-out);
flex-shrink: 0;
}
.modal__close:hover {
color: var(--color-black);
}
.modal__body {
flex: 1;
padding: var(--spacing-l);
overflow-y: auto;
}
.modal__footer {
display: flex;
align-items: center;
justify-content: flex-end;
gap: var(--spacing-m);
padding: var(--spacing-l);
border-top: 1px solid var(--color-gray-300);
}
/* ========================================
Bottom Sheet
======================================== */
.bottom-sheet {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background-color: var(--color-white);
border-radius: var(--radius-l) var(--radius-l) 0 0;
box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.1);
max-height: 90vh;
display: flex;
flex-direction: column;
z-index: var(--z-modal);
animation: slideUp var(--transition-normal) var(--ease-out);
}
.bottom-sheet__handle {
width: 40px;
height: 4px;
background-color: var(--color-gray-300);
border-radius: var(--radius-xs);
margin: var(--spacing-m) auto;
cursor: grab;
}
.bottom-sheet__header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 var(--spacing-l) var(--spacing-l);
border-bottom: 1px solid var(--color-gray-300);
}
.bottom-sheet__title {
font-size: var(--font-size-h3);
font-weight: var(--font-weight-semibold);
line-height: var(--line-height-h3);
color: var(--color-black);
}
.bottom-sheet__close {
width: 24px;
height: 24px;
color: var(--color-gray-500);
cursor: pointer;
transition: color var(--transition-fast) var(--ease-out);
flex-shrink: 0;
}
.bottom-sheet__close:hover {
color: var(--color-black);
}
.bottom-sheet__body {
flex: 1;
padding: var(--spacing-l);
overflow-y: auto;
}
.bottom-sheet__footer {
display: flex;
align-items: center;
justify-content: flex-end;
gap: var(--spacing-m);
padding: var(--spacing-l);
border-top: 1px solid var(--color-gray-300);
}
/* ========================================
Toast / Snackbar
======================================== */
.toast {
position: fixed;
bottom: calc(var(--bottom-nav-height) + var(--spacing-l));
left: 50%;
transform: translateX(-50%);
min-width: 280px;
max-width: 90%;
background-color: var(--color-black);
color: var(--color-white);
padding: var(--spacing-m) var(--spacing-l);
border-radius: var(--radius-s);
box-shadow: var(--shadow-lg);
z-index: var(--z-toast);
display: flex;
align-items: center;
gap: var(--spacing-m);
animation: slideUp var(--transition-fast) var(--ease-out);
}
.toast--top {
top: var(--spacing-l);
bottom: auto;
animation: slideDown var(--transition-fast) var(--ease-out);
}
.toast--success {
background-color: var(--color-success);
}
.toast--warning {
background-color: var(--color-warning);
}
.toast--error {
background-color: var(--color-error);
}
.toast--info {
background-color: var(--color-info);
}
.toast__icon {
width: 20px;
height: 20px;
flex-shrink: 0;
}
.toast__message {
flex: 1;
font-size: var(--font-size-body-medium);
line-height: var(--line-height-body-medium);
}
.toast__close {
width: 20px;
height: 20px;
color: var(--color-white);
cursor: pointer;
opacity: 0.8;
transition: opacity var(--transition-fast) var(--ease-out);
flex-shrink: 0;
}
.toast__close:hover {
opacity: 1;
}
/* ========================================
Dialog (Confirm)
======================================== */
.dialog {
background-color: var(--color-white);
border-radius: var(--radius-m);
box-shadow: var(--shadow-xl);
max-width: 400px;
width: 100%;
z-index: var(--z-modal);
animation: scaleIn var(--transition-fast) var(--ease-out);
}
.dialog__header {
padding: var(--spacing-l);
border-bottom: 1px solid var(--color-gray-300);
}
.dialog__title {
font-size: var(--font-size-h3);
font-weight: var(--font-weight-semibold);
line-height: var(--line-height-h3);
color: var(--color-black);
}
.dialog__body {
padding: var(--spacing-l);
}
.dialog__message {
font-size: var(--font-size-body-medium);
line-height: var(--line-height-body-medium);
color: var(--color-gray-700);
}
.dialog__footer {
display: flex;
align-items: center;
justify-content: flex-end;
gap: var(--spacing-m);
padding: var(--spacing-l);
border-top: 1px solid var(--color-gray-300);
}
/* ========================================
Tooltip
======================================== */
.tooltip {
position: absolute;
background-color: var(--color-black);
color: var(--color-white);
padding: var(--spacing-s) var(--spacing-m);
border-radius: var(--radius-xs);
font-size: var(--font-size-body-small);
line-height: var(--line-height-body-small);
white-space: nowrap;
z-index: var(--z-tooltip);
pointer-events: none;
animation: fadeIn var(--transition-very-fast) var(--ease-out);
}
.tooltip::before {
content: '';
position: absolute;
width: 0;
height: 0;
border-style: solid;
}
.tooltip--top {
bottom: 100%;
left: 50%;
transform: translateX(-50%) translateY(-8px);
}
.tooltip--top::before {
top: 100%;
left: 50%;
transform: translateX(-50%);
border-width: 4px 4px 0;
border-color: var(--color-black) transparent transparent;
}
.tooltip--bottom {
top: 100%;
left: 50%;
transform: translateX(-50%) translateY(8px);
}
.tooltip--bottom::before {
bottom: 100%;
left: 50%;
transform: translateX(-50%);
border-width: 0 4px 4px;
border-color: transparent transparent var(--color-black);
}
/* ========================================
Dropdown
======================================== */
.dropdown {
position: relative;
display: inline-block;
}
.dropdown__menu {
position: absolute;
top: 100%;
left: 0;
margin-top: var(--spacing-xs);
background-color: var(--color-white);
border: 1px solid var(--color-gray-300);
border-radius: var(--radius-s);
box-shadow: var(--shadow-md);
min-width: 200px;
z-index: var(--z-dropdown);
animation: fadeIn var(--transition-very-fast) var(--ease-out);
}
.dropdown__item {
display: block;
padding: var(--spacing-m) var(--spacing-l);
font-size: var(--font-size-body-medium);
line-height: var(--line-height-body-medium);
color: var(--color-black);
text-decoration: none;
cursor: pointer;
transition: background-color var(--transition-fast) var(--ease-out);
}
.dropdown__item:hover {
background-color: var(--color-gray-100);
}
.dropdown__item:active {
background-color: var(--color-gray-300);
}
.dropdown__divider {
height: 1px;
background-color: var(--color-gray-300);
margin: var(--spacing-xs) 0;
}

View File

@ -0,0 +1,308 @@
/**
* KT AI 기반 소상공인 이벤트 자동 생성 서비스
* Navigation Components
*
* 참조: design/uiux/style-guide.md (Section 6.5)
* 작성일: 2025-01-20
*/
/* ========================================
App Bar (상단 네비게이션)
======================================== */
.app-bar {
position: sticky;
top: 0;
left: 0;
right: 0;
height: var(--app-bar-height);
background-color: var(--color-white);
border-bottom: 1px solid var(--color-gray-300);
box-shadow: var(--shadow-sm);
z-index: var(--z-sticky);
}
.app-bar__container {
display: flex;
align-items: center;
justify-content: space-between;
height: 100%;
padding: 0 var(--spacing-m);
max-width: var(--container-desktop);
margin: 0 auto;
}
.app-bar__left,
.app-bar__right {
display: flex;
align-items: center;
gap: var(--spacing-m);
}
.app-bar__title {
font-size: var(--font-size-h3);
font-weight: var(--font-weight-bold);
line-height: var(--line-height-h3);
color: var(--color-black);
}
.app-bar__back {
width: 24px;
height: 24px;
color: var(--color-black);
cursor: pointer;
transition: color var(--transition-fast) var(--ease-out);
}
.app-bar__back:hover {
color: var(--color-primary);
}
/* ========================================
Bottom Navigation
======================================== */
.bottom-nav {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: var(--bottom-nav-height);
background-color: var(--color-white);
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.08);
z-index: var(--z-fixed);
}
.bottom-nav__container {
display: flex;
align-items: center;
justify-content: space-around;
height: 100%;
max-width: var(--container-desktop);
margin: 0 auto;
}
.bottom-nav__item {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: var(--spacing-xs);
flex: 1;
height: 100%;
min-width: var(--touch-target-min);
text-decoration: none;
color: var(--color-gray-500);
cursor: pointer;
transition: color var(--transition-fast) var(--ease-out);
user-select: none;
}
.bottom-nav__item:hover {
color: var(--color-primary);
}
.bottom-nav__item--active {
color: var(--color-primary);
}
.bottom-nav__icon {
width: 24px;
height: 24px;
}
.bottom-nav__label {
font-size: var(--font-size-body-small);
line-height: 1;
font-weight: var(--font-weight-regular);
}
.bottom-nav__item--active .bottom-nav__label {
font-weight: var(--font-weight-semibold);
}
/* ========================================
Tab Navigation
======================================== */
.tab-nav {
display: flex;
gap: var(--spacing-s);
border-bottom: 1px solid var(--color-gray-300);
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
.tab-nav__item {
display: flex;
align-items: center;
justify-content: center;
gap: var(--spacing-s);
padding: var(--spacing-m) var(--spacing-l);
min-height: var(--touch-target-min);
font-size: var(--font-size-body-medium);
font-weight: var(--font-weight-medium);
line-height: var(--line-height-body-medium);
color: var(--color-gray-500);
text-decoration: none;
white-space: nowrap;
border-bottom: 2px solid transparent;
cursor: pointer;
transition: all var(--transition-fast) var(--ease-out);
user-select: none;
}
.tab-nav__item:hover {
color: var(--color-primary);
}
.tab-nav__item--active {
color: var(--color-primary);
border-bottom-color: var(--color-primary);
font-weight: var(--font-weight-semibold);
}
/* ========================================
Breadcrumb
======================================== */
.breadcrumb {
display: flex;
align-items: center;
gap: var(--spacing-s);
flex-wrap: wrap;
padding: var(--spacing-m) 0;
}
.breadcrumb__item {
display: flex;
align-items: center;
gap: var(--spacing-s);
font-size: var(--font-size-body-small);
line-height: var(--line-height-body-small);
color: var(--color-gray-500);
}
.breadcrumb__link {
color: var(--color-gray-500);
text-decoration: none;
transition: color var(--transition-fast) var(--ease-out);
}
.breadcrumb__link:hover {
color: var(--color-primary);
}
.breadcrumb__separator {
color: var(--color-gray-300);
}
.breadcrumb__item--active {
color: var(--color-black);
font-weight: var(--font-weight-medium);
}
/* ========================================
Stepper (단계 표시)
======================================== */
.stepper {
display: flex;
align-items: center;
gap: var(--spacing-s);
}
.stepper__step {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--spacing-xs);
flex: 1;
}
.stepper__circle {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border-radius: 50%;
font-size: var(--font-size-body-small);
font-weight: var(--font-weight-semibold);
transition: all var(--transition-fast) var(--ease-out);
}
/* Completed */
.stepper__step--completed .stepper__circle {
background-color: var(--color-success);
color: var(--color-white);
}
/* Active */
.stepper__step--active .stepper__circle {
background-color: var(--color-primary);
color: var(--color-white);
}
/* Pending */
.stepper__step--pending .stepper__circle {
background-color: var(--color-gray-100);
border: 1px solid var(--color-gray-300);
color: var(--color-gray-500);
}
.stepper__label {
font-size: var(--font-size-body-small);
line-height: var(--line-height-body-small);
text-align: center;
color: var(--color-gray-500);
}
.stepper__step--active .stepper__label,
.stepper__step--completed .stepper__label {
color: var(--color-black);
font-weight: var(--font-weight-medium);
}
.stepper__line {
flex: 1;
height: 2px;
background-color: var(--color-gray-300);
margin: 0 var(--spacing-s);
}
.stepper__line--completed {
background-color: var(--color-success);
}
/* ========================================
Progress Bar
======================================== */
.progress-bar {
width: 100%;
height: 48px;
background-color: var(--color-gray-100);
border-radius: var(--radius-xl);
overflow: hidden;
position: relative;
}
.progress-bar__fill {
height: 100%;
background-color: var(--color-primary);
transition: width var(--transition-normal) var(--ease-out);
position: relative;
overflow: hidden;
}
.progress-bar__text {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: var(--font-size-body-medium);
font-weight: var(--font-weight-semibold);
color: var(--color-black);
z-index: 1;
}

View File

@ -0,0 +1,304 @@
/**
* KT AI 기반 소상공인 이벤트 자동 생성 서비스
* Layout System (Grid, Container, Spacing)
*
* 참조: design/uiux/style-guide.md
* 작성일: 2025-01-20
*/
/* ========================================
Container
======================================== */
/* Mobile (320px ~ 767px) */
.container {
width: 100%;
max-width: var(--container-mobile);
margin-left: auto;
margin-right: auto;
padding-left: 20px;
padding-right: 20px;
}
/* Tablet (768px ~ 1023px) */
@media (min-width: 768px) {
.container {
max-width: var(--container-tablet);
padding-left: 40px;
padding-right: 40px;
}
}
/* Desktop (1024px ~) */
@media (min-width: 1024px) {
.container {
max-width: var(--container-desktop);
padding-left: 0;
padding-right: 0;
}
}
/* ========================================
Grid System
======================================== */
/* Mobile Grid (4 Columns) */
.grid {
display: grid;
gap: var(--gutter-mobile);
grid-template-columns: repeat(4, 1fr);
}
/* Tablet Grid (8 Columns) */
@media (min-width: 768px) {
.grid {
gap: var(--gutter-tablet);
grid-template-columns: repeat(8, 1fr);
}
}
/* Desktop Grid (12 Columns) */
@media (min-width: 1024px) {
.grid {
gap: var(--gutter-desktop);
grid-template-columns: repeat(12, 1fr);
}
}
/* Grid Column Spans */
.col-1 { grid-column: span 1; }
.col-2 { grid-column: span 2; }
.col-3 { grid-column: span 3; }
.col-4 { grid-column: span 4; }
@media (min-width: 768px) {
.col-md-1 { grid-column: span 1; }
.col-md-2 { grid-column: span 2; }
.col-md-3 { grid-column: span 3; }
.col-md-4 { grid-column: span 4; }
.col-md-5 { grid-column: span 5; }
.col-md-6 { grid-column: span 6; }
.col-md-7 { grid-column: span 7; }
.col-md-8 { grid-column: span 8; }
}
@media (min-width: 1024px) {
.col-lg-1 { grid-column: span 1; }
.col-lg-2 { grid-column: span 2; }
.col-lg-3 { grid-column: span 3; }
.col-lg-4 { grid-column: span 4; }
.col-lg-5 { grid-column: span 5; }
.col-lg-6 { grid-column: span 6; }
.col-lg-7 { grid-column: span 7; }
.col-lg-8 { grid-column: span 8; }
.col-lg-9 { grid-column: span 9; }
.col-lg-10 { grid-column: span 10; }
.col-lg-11 { grid-column: span 11; }
.col-lg-12 { grid-column: span 12; }
}
/* ========================================
Flexbox Utilities
======================================== */
.flex {
display: flex;
}
.inline-flex {
display: inline-flex;
}
/* Flex Direction */
.flex-row { flex-direction: row; }
.flex-row-reverse { flex-direction: row-reverse; }
.flex-col { flex-direction: column; }
.flex-col-reverse { flex-direction: column-reverse; }
/* Flex Wrap */
.flex-wrap { flex-wrap: wrap; }
.flex-nowrap { flex-wrap: nowrap; }
/* Justify Content */
.justify-start { justify-content: flex-start; }
.justify-end { justify-content: flex-end; }
.justify-center { justify-content: center; }
.justify-between { justify-content: space-between; }
.justify-around { justify-content: space-around; }
.justify-evenly { justify-content: space-evenly; }
/* Align Items */
.items-start { align-items: flex-start; }
.items-end { align-items: flex-end; }
.items-center { align-items: center; }
.items-baseline { align-items: baseline; }
.items-stretch { align-items: stretch; }
/* Align Self */
.self-start { align-self: flex-start; }
.self-end { align-self: flex-end; }
.self-center { align-self: center; }
.self-stretch { align-self: stretch; }
/* Gap */
.gap-xs { gap: var(--spacing-xs); }
.gap-s { gap: var(--spacing-s); }
.gap-m { gap: var(--spacing-m); }
.gap-l { gap: var(--spacing-l); }
.gap-xl { gap: var(--spacing-xl); }
.gap-2xl { gap: var(--spacing-2xl); }
/* ========================================
Spacing Utilities (Margin & Padding)
======================================== */
/* Margin */
.m-0 { margin: 0; }
.m-xs { margin: var(--spacing-xs); }
.m-s { margin: var(--spacing-s); }
.m-m { margin: var(--spacing-m); }
.m-l { margin: var(--spacing-l); }
.m-xl { margin: var(--spacing-xl); }
.m-2xl { margin: var(--spacing-2xl); }
/* Margin Top */
.mt-0 { margin-top: 0; }
.mt-xs { margin-top: var(--spacing-xs); }
.mt-s { margin-top: var(--spacing-s); }
.mt-m { margin-top: var(--spacing-m); }
.mt-l { margin-top: var(--spacing-l); }
.mt-xl { margin-top: var(--spacing-xl); }
.mt-2xl { margin-top: var(--spacing-2xl); }
/* Margin Bottom */
.mb-0 { margin-bottom: 0; }
.mb-xs { margin-bottom: var(--spacing-xs); }
.mb-s { margin-bottom: var(--spacing-s); }
.mb-m { margin-bottom: var(--spacing-m); }
.mb-l { margin-bottom: var(--spacing-l); }
.mb-xl { margin-bottom: var(--spacing-xl); }
.mb-2xl { margin-bottom: var(--spacing-2xl); }
/* Margin Left */
.ml-0 { margin-left: 0; }
.ml-xs { margin-left: var(--spacing-xs); }
.ml-s { margin-left: var(--spacing-s); }
.ml-m { margin-left: var(--spacing-m); }
.ml-l { margin-left: var(--spacing-l); }
.ml-xl { margin-left: var(--spacing-xl); }
/* Margin Right */
.mr-0 { margin-right: 0; }
.mr-xs { margin-right: var(--spacing-xs); }
.mr-s { margin-right: var(--spacing-s); }
.mr-m { margin-right: var(--spacing-m); }
.mr-l { margin-right: var(--spacing-l); }
.mr-xl { margin-right: var(--spacing-xl); }
/* Margin Auto */
.mx-auto {
margin-left: auto;
margin-right: auto;
}
/* Padding */
.p-0 { padding: 0; }
.p-xs { padding: var(--spacing-xs); }
.p-s { padding: var(--spacing-s); }
.p-m { padding: var(--spacing-m); }
.p-l { padding: var(--spacing-l); }
.p-xl { padding: var(--spacing-xl); }
.p-2xl { padding: var(--spacing-2xl); }
/* Padding Top */
.pt-0 { padding-top: 0; }
.pt-xs { padding-top: var(--spacing-xs); }
.pt-s { padding-top: var(--spacing-s); }
.pt-m { padding-top: var(--spacing-m); }
.pt-l { padding-top: var(--spacing-l); }
.pt-xl { padding-top: var(--spacing-xl); }
.pt-2xl { padding-top: var(--spacing-2xl); }
/* Padding Bottom */
.pb-0 { padding-bottom: 0; }
.pb-xs { padding-bottom: var(--spacing-xs); }
.pb-s { padding-bottom: var(--spacing-s); }
.pb-m { padding-bottom: var(--spacing-m); }
.pb-l { padding-bottom: var(--spacing-l); }
.pb-xl { padding-bottom: var(--spacing-xl); }
.pb-2xl { padding-bottom: var(--spacing-2xl); }
/* Padding Left */
.pl-0 { padding-left: 0; }
.pl-xs { padding-left: var(--spacing-xs); }
.pl-s { padding-left: var(--spacing-s); }
.pl-m { padding-left: var(--spacing-m); }
.pl-l { padding-left: var(--spacing-l); }
.pl-xl { padding-left: var(--spacing-xl); }
/* Padding Right */
.pr-0 { padding-right: 0; }
.pr-xs { padding-right: var(--spacing-xs); }
.pr-s { padding-right: var(--spacing-s); }
.pr-m { padding-right: var(--spacing-m); }
.pr-l { padding-right: var(--spacing-l); }
.pr-xl { padding-right: var(--spacing-xl); }
/* ========================================
Display Utilities
======================================== */
.block { display: block; }
.inline-block { display: inline-block; }
.inline { display: inline; }
.hidden { display: none; }
/* Responsive Display */
@media (max-width: 767px) {
.hidden-mobile { display: none; }
}
@media (min-width: 768px) and (max-width: 1023px) {
.hidden-tablet { display: none; }
}
@media (min-width: 1024px) {
.hidden-desktop { display: none; }
}
/* ========================================
Position Utilities
======================================== */
.relative { position: relative; }
.absolute { position: absolute; }
.fixed { position: fixed; }
.sticky { position: sticky; }
/* ========================================
Text Align
======================================== */
.text-left { text-align: left; }
.text-center { text-align: center; }
.text-right { text-align: right; }
/* ========================================
Width & Height
======================================== */
.w-full { width: 100%; }
.h-full { height: 100%; }
.w-screen { width: 100vw; }
.h-screen { height: 100vh; }
/* ========================================
Overflow
======================================== */
.overflow-auto { overflow: auto; }
.overflow-hidden { overflow: hidden; }
.overflow-scroll { overflow: scroll; }
.overflow-x-auto { overflow-x: auto; }
.overflow-y-auto { overflow-y: auto; }

View File

@ -0,0 +1,210 @@
/**
* KT AI 기반 소상공인 이벤트 자동 생성 서비스
* CSS Reset
*
* Modern CSS Reset + Box-sizing Fix
* 작성일: 2025-01-20
*/
/* ========================================
Box Sizing
======================================== */
*,
*::before,
*::after {
box-sizing: border-box;
}
/* ========================================
Remove Default Margins & Paddings
======================================== */
* {
margin: 0;
padding: 0;
}
/* ========================================
HTML & Body
======================================== */
html {
font-size: 16px;
-webkit-text-size-adjust: 100%;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
min-height: 100vh;
line-height: 1.5;
text-rendering: optimizeSpeed;
}
/* ========================================
Typography Defaults
======================================== */
h1, h2, h3, h4, h5, h6 {
font-weight: inherit;
font-size: inherit;
line-height: inherit;
}
p, h1, h2, h3, h4, h5, h6 {
overflow-wrap: break-word;
}
/* ========================================
Lists
======================================== */
ul, ol {
list-style: none;
}
/* ========================================
Links
======================================== */
a {
text-decoration: none;
color: inherit;
}
a:hover,
a:focus {
text-decoration: none;
}
/* ========================================
Images & Media
======================================== */
img,
picture,
video,
canvas,
svg {
display: block;
max-width: 100%;
height: auto;
}
/* ========================================
Forms
======================================== */
input,
button,
textarea,
select {
font: inherit;
color: inherit;
}
button {
border: none;
background: none;
cursor: pointer;
}
input:focus,
textarea:focus,
select:focus {
outline: none;
}
/* Remove default input styles */
input[type="text"],
input[type="email"],
input[type="password"],
input[type="tel"],
input[type="number"],
input[type="search"],
textarea,
select {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
}
/* Remove spinner from number inputs */
input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}
input[type="number"] {
-moz-appearance: textfield;
}
/* Remove search cancel button */
input[type="search"]::-webkit-search-cancel-button {
-webkit-appearance: none;
}
/* ========================================
Tables
======================================== */
table {
border-collapse: collapse;
border-spacing: 0;
}
/* ========================================
Accessibility
======================================== */
/* Remove all animations for users who prefer reduced motion */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
/* Screen reader only content */
.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;
}
/* ========================================
Scrollbar (Webkit)
======================================== */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: #F5F5F5;
}
::-webkit-scrollbar-thumb {
background: #D9D9D9;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #9E9E9E;
}
/* ========================================
Selection
======================================== */
::selection {
background-color: rgba(227, 30, 36, 0.2);
color: #1A1A1A;
}
::-moz-selection {
background-color: rgba(227, 30, 36, 0.2);
color: #1A1A1A;
}

View File

@ -0,0 +1,230 @@
/**
* KT AI 기반 소상공인 이벤트 자동 생성 서비스
* CSS Variables (Design System)
*
* 참조: design/uiux/style-guide.md
* 작성일: 2025-01-20
*/
:root {
/* ========================================
Primary Colors (KT Red)
======================================== */
--color-primary: #E31E24;
--color-primary-light: #FF4D52;
--color-primary-dark: #C71820;
/* ========================================
Secondary Colors (AI Blue)
======================================== */
--color-secondary: #0066FF;
--color-secondary-light: #4D94FF;
--color-secondary-dark: #004DBF;
/* ========================================
Grayscale
======================================== */
--color-black: #1A1A1A; /* Gray-900 */
--color-gray-700: #4A4A4A;
--color-gray-500: #9E9E9E;
--color-gray-300: #D9D9D9;
--color-gray-100: #F5F5F5;
--color-white: #FFFFFF; /* Gray-50 */
/* ========================================
Semantic Colors
======================================== */
--color-success: #00C853;
--color-warning: #FFA000;
--color-error: #D32F2F;
--color-info: #0288D1;
/* ========================================
Gradient
======================================== */
--gradient-primary: linear-gradient(135deg, #E31E24 0%, #FF4D52 100%);
--gradient-secondary: linear-gradient(135deg, #0066FF 0%, #4D94FF 100%);
/* ========================================
Typography - Font Family
======================================== */
--font-family: 'Pretendard', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Helvetica Neue', system-ui, sans-serif;
/* ========================================
Typography - Font Sizes (Mobile First)
======================================== */
--font-size-display: 28px; /* Display (메인 타이틀) */
--font-size-h1: 24px; /* H1 (화면 제목) */
--font-size-h2: 20px; /* H2 (섹션 제목) */
--font-size-h3: 18px; /* H3 (카드 제목) */
--font-size-body-large: 16px; /* Body-Large (큰 본문) */
--font-size-body-medium: 14px; /* Body-Medium (기본 본문) */
--font-size-body-small: 12px; /* Body-Small (작은 본문) */
--font-size-button: 16px; /* Button (버튼 레이블) */
/* ========================================
Typography - Line Heights
======================================== */
--line-height-display: 1.3; /* 36px */
--line-height-h1: 1.3; /* 31px */
--line-height-h2: 1.4; /* 28px */
--line-height-h3: 1.4; /* 25px */
--line-height-body-large: 1.5; /* 24px */
--line-height-body-medium: 1.5; /* 21px */
--line-height-body-small: 1.5; /* 18px */
--line-height-button: 1.5; /* 24px */
/* ========================================
Typography - Font Weights
======================================== */
--font-weight-regular: 400;
--font-weight-medium: 500;
--font-weight-semibold: 600;
--font-weight-bold: 700;
/* ========================================
Typography - Letter Spacing
======================================== */
--letter-spacing-display: -0.5px;
--letter-spacing-h1: -0.3px;
--letter-spacing-h2: -0.2px;
--letter-spacing-h3: 0px;
--letter-spacing-body: 0px;
/* ========================================
Spacing (4px Grid System)
======================================== */
--spacing-xs: 4px; /* Extra Small */
--spacing-s: 8px; /* Small */
--spacing-m: 16px; /* Medium */
--spacing-l: 24px; /* Large */
--spacing-xl: 32px; /* Extra Large */
--spacing-2xl: 48px; /* 2X Large */
/* ========================================
Border Radius
======================================== */
--radius-xs: 4px;
--radius-s: 8px;
--radius-m: 12px;
--radius-l: 16px;
--radius-xl: 24px;
--radius-pill: 9999px; /* Pill 형태 */
/* ========================================
Shadows
======================================== */
--shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.08);
--shadow-md: 0 2px 8px rgba(0, 0, 0, 0.08);
--shadow-lg: 0 4px 12px rgba(0, 0, 0, 0.12);
--shadow-xl: 0 8px 24px rgba(0, 0, 0, 0.2);
--shadow-primary: 0 2px 4px rgba(227, 30, 36, 0.2);
--shadow-secondary: 0 0 0 4px rgba(0, 102, 255, 0.1);
--shadow-error: 0 0 0 4px rgba(211, 47, 47, 0.1);
/* ========================================
Z-Index Scale
======================================== */
--z-base: 0;
--z-dropdown: 100;
--z-sticky: 200;
--z-fixed: 300;
--z-modal-backdrop: 400;
--z-modal: 500;
--z-toast: 600;
--z-tooltip: 700;
/* ========================================
Transitions
======================================== */
--transition-instant: 0ms;
--transition-very-fast: 100ms;
--transition-fast: 200ms;
--transition-normal: 300ms;
--transition-slow: 500ms;
/* ========================================
Easing
======================================== */
--ease-out: cubic-bezier(0, 0, 0.2, 1);
--ease-in: cubic-bezier(0.4, 0, 1, 1);
--ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
--ease-linear: linear;
/* ========================================
Layout - Breakpoints
======================================== */
--breakpoint-mobile: 320px;
--breakpoint-tablet: 768px;
--breakpoint-desktop: 1024px;
/* ========================================
Layout - Container
======================================== */
--container-mobile: calc(100% - 40px); /* 양쪽 20px 마진 */
--container-tablet: calc(100% - 80px); /* 양쪽 40px 마진 */
--container-desktop: 1200px; /* 최대 너비 */
/* ========================================
Layout - Gutters
======================================== */
--gutter-mobile: 16px;
--gutter-tablet: 24px;
--gutter-desktop: 32px;
/* ========================================
Component - Button Heights
======================================== */
--button-height-large: 48px;
--button-height-medium: 44px;
--button-height-small: 36px;
/* ========================================
Component - Input Heights
======================================== */
--input-height: 48px;
--textarea-min-height: 120px;
/* ========================================
Component - Touch Target
======================================== */
--touch-target-min: 44px; /* WCAG 2.1 AA */
--touch-target-recommended: 48px;
/* ========================================
Component - Bottom Navigation
======================================== */
--bottom-nav-height: 60px;
/* ========================================
Component - App Bar
======================================== */
--app-bar-height: 56px;
}
/* ========================================
Responsive Typography (Tablet)
======================================== */
@media (min-width: 768px) {
:root {
--font-size-display: 32px; /* +4px */
--font-size-h1: 28px; /* +4px */
--font-size-h2: 22px; /* +2px */
--font-size-h3: 20px; /* +2px */
--font-size-body-large: 18px; /* +2px */
--font-size-body-medium: 16px; /* +2px */
--font-size-body-small: 14px; /* +2px */
}
}
/* ========================================
Responsive Typography (Desktop)
======================================== */
@media (min-width: 1024px) {
:root {
--font-size-display: 36px; /* +8px */
--font-size-h1: 32px; /* +8px */
--font-size-h2: 24px; /* +4px */
--font-size-h3: 20px; /* +2px */
}
}

View File

@ -1,555 +1,375 @@
/**
* KT AI 기반 소상공인 이벤트 자동 생성 서비스
* 공통 JavaScript 유틸리티
* Common JavaScript Utilities
*
* 작성일: 2025-01-20
* 버전: 1.0
*/
(function() {
'use strict';
// ========================================
// DOM 유틸리티
// ========================================
// ============================================
// 1. 상태 관리
// ============================================
window.AppState = {
// 사용자 정보
user: null,
const $ = (selector, context = document) => context.querySelector(selector);
const $$ = (selector, context = document) => Array.from(context.querySelectorAll(selector));
// 매장 정보
store: null,
// 현재 이벤트 정보
currentEvent: null,
// localStorage에서 데이터 로드
load() {
try {
this.user = JSON.parse(localStorage.getItem('kt_user') || 'null');
this.store = JSON.parse(localStorage.getItem('kt_store') || 'null');
this.currentEvent = JSON.parse(localStorage.getItem('kt_current_event') || 'null');
} catch (e) {
console.error('Failed to load app state:', e);
}
},
// localStorage에 데이터 저장
save() {
try {
localStorage.setItem('kt_user', JSON.stringify(this.user));
localStorage.setItem('kt_store', JSON.stringify(this.store));
localStorage.setItem('kt_current_event', JSON.stringify(this.currentEvent));
} catch (e) {
console.error('Failed to save app state:', e);
}
},
// 상태 초기화
clear() {
this.user = null;
this.store = null;
this.currentEvent = null;
localStorage.removeItem('kt_user');
localStorage.removeItem('kt_store');
localStorage.removeItem('kt_current_event');
}
};
// 페이지 로드 시 상태 복원
document.addEventListener('DOMContentLoaded', function() {
window.AppState.load();
const createElement = (tag, className = '', attributes = {}) => {
const element = document.createElement(tag);
if (className) element.className = className;
Object.entries(attributes).forEach(([key, value]) => {
element.setAttribute(key, value);
});
return element;
};
// ============================================
// 2. Toast 알림
// ============================================
window.Toast = {
show(message, duration = 3000) {
// 기존 토스트 제거
const existingToast = document.querySelector('.toast');
if (existingToast) {
existingToast.remove();
}
const addClass = (element, ...classes) => {
if (!element) return;
element.classList.add(...classes);
};
// 새 토스트 생성
const toast = document.createElement('div');
toast.className = 'toast';
toast.textContent = message;
toast.setAttribute('role', 'status');
toast.setAttribute('aria-live', 'polite');
const removeClass = (element, ...classes) => {
if (!element) return;
element.classList.remove(...classes);
};
document.body.appendChild(toast);
const toggleClass = (element, className, force) => {
if (!element) return;
return element.classList.toggle(className, force);
};
const hasClass = (element, className) => {
if (!element) return false;
return element.classList.contains(className);
};
// ========================================
// 이벤트 유틸리티
// ========================================
const on = (element, event, handler, options = {}) => {
if (!element) return;
element.addEventListener(event, handler, options);
};
const off = (element, event, handler, options = {}) => {
if (!element) return;
element.removeEventListener(event, handler, options);
};
const delegate = (parent, eventType, selector, handler) => {
on(parent, eventType, (event) => {
const target = event.target.closest(selector);
if (target && parent.contains(target)) {
handler.call(target, event);
}
});
};
const once = (element, event, handler) => {
on(element, event, handler, { once: true });
};
// ========================================
// 애니메이션 유틸리티
// ========================================
const fadeIn = (element, duration = 200) => {
if (!element) return Promise.resolve();
element.style.opacity = '0';
element.style.display = 'block';
return new Promise((resolve) => {
requestAnimationFrame(() => {
element.style.transition = `opacity ${duration}ms ease-out`;
element.style.opacity = '1';
// 자동 제거
setTimeout(() => {
toast.classList.add('hide');
setTimeout(() => {
toast.remove();
}, 200);
element.style.transition = '';
resolve();
}, duration);
},
success(message) {
this.show('✓ ' + message);
},
error(message) {
this.show('✕ ' + message);
},
info(message) {
this.show(' ' + message);
}
};
// ============================================
// 3. Modal 다이얼로그
// ============================================
window.Modal = {
show(options) {
const {
title = '',
body = '',
confirmText = '확인',
cancelText = '취소',
onConfirm = () => {},
onCancel = () => {},
showCancel = true
} = options;
// 백드롭 생성
const backdrop = document.createElement('div');
backdrop.className = 'modal-backdrop';
// 모달 생성
const modal = document.createElement('div');
modal.className = 'modal';
modal.setAttribute('role', 'dialog');
modal.setAttribute('aria-labelledby', 'modal-title');
modal.setAttribute('aria-modal', 'true');
modal.innerHTML = `
<div class="modal__header">
<h2 class="modal__title" id="modal-title">${title}</h2>
<button class="modal__close" aria-label="닫기">
<span class="material-icons">close</span>
</button>
</div>
<div class="modal__body">
${body}
</div>
<div class="modal__footer">
${showCancel ? `<button class="btn btn-secondary btn-md modal__cancel">${cancelText}</button>` : ''}
<button class="btn btn-primary btn-md modal__confirm">${confirmText}</button>
</div>
`;
// 닫기 함수
const close = () => {
backdrop.remove();
modal.remove();
document.body.style.overflow = '';
};
// 이벤트 리스너
modal.querySelector('.modal__close').addEventListener('click', () => {
close();
onCancel();
});
if (showCancel) {
modal.querySelector('.modal__cancel').addEventListener('click', () => {
close();
onCancel();
});
}
modal.querySelector('.modal__confirm').addEventListener('click', () => {
close();
onConfirm();
});
backdrop.addEventListener('click', (e) => {
if (e.target === backdrop) {
close();
onCancel();
}
});
// 추가
document.body.appendChild(backdrop);
document.body.appendChild(modal);
document.body.style.overflow = 'hidden';
// 첫 번째 버튼에 포커스
modal.querySelector('button').focus();
},
confirm(title, body, onConfirm) {
this.show({
title,
body,
confirmText: '확인',
cancelText: '취소',
onConfirm,
showCancel: true
});
},
alert(title, body, onConfirm = () => {}) {
this.show({
title,
body,
confirmText: '확인',
onConfirm,
showCancel: false
});
}
};
// ============================================
// 4. Bottom Sheet
// ============================================
window.BottomSheet = {
show(content, options = {}) {
const {
onClose = () => {}
} = options;
// 백드롭 생성
const backdrop = document.createElement('div');
backdrop.className = 'bottom-sheet-backdrop';
// Bottom Sheet 생성
const sheet = document.createElement('div');
sheet.className = 'bottom-sheet';
sheet.setAttribute('role', 'dialog');
sheet.setAttribute('aria-modal', 'true');
sheet.innerHTML = `
<div class="bottom-sheet__handle"></div>
<div class="bottom-sheet__content">
${content}
</div>
`;
// 닫기 함수
const close = () => {
backdrop.remove();
sheet.remove();
document.body.style.overflow = '';
onClose();
};
// 이벤트 리스너
backdrop.addEventListener('click', close);
// Handle 드래그로 닫기 (간단한 구현)
let startY = 0;
const handle = sheet.querySelector('.bottom-sheet__handle');
handle.addEventListener('touchstart', (e) => {
startY = e.touches[0].clientY;
});
handle.addEventListener('touchend', (e) => {
const endY = e.changedTouches[0].clientY;
if (endY - startY > 50) { // 50px 이상 드래그하면 닫기
close();
}
});
// 추가
document.body.appendChild(backdrop);
document.body.appendChild(sheet);
document.body.style.overflow = 'hidden';
return { close };
}
};
// ============================================
// 5. 로딩 인디케이터
// ============================================
window.Loading = {
show(message = '처리중...') {
// 기존 로딩 제거
this.hide();
const overlay = document.createElement('div');
overlay.className = 'loading-overlay';
overlay.id = 'app-loading';
overlay.innerHTML = `
<div style="text-align: center; color: white;">
<div class="spinner" style="margin: 0 auto 16px;"></div>
<div class="body-m">${message}</div>
</div>
`;
document.body.appendChild(overlay);
document.body.style.overflow = 'hidden';
},
hide() {
const overlay = document.getElementById('app-loading');
if (overlay) {
overlay.remove();
document.body.style.overflow = '';
}
}
};
// ============================================
// 6. 폼 검증 유틸리티
// ============================================
window.FormValidator = {
// 이메일 검증
isValidEmail(email) {
const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return re.test(email);
},
// 전화번호 검증 (010-XXXX-XXXX)
isValidPhone(phone) {
const re = /^010-\d{4}-\d{4}$/;
return re.test(phone);
},
// 사업자번호 검증 (XXX-XX-XXXXX)
isValidBusinessNumber(number) {
const re = /^\d{3}-\d{2}-\d{5}$/;
return re.test(number);
},
// 이름 검증 (2자 이상)
isValidName(name) {
return name && name.length >= 2;
},
// 필드에 에러 표시
showError(inputElement, message) {
inputElement.classList.add('error');
// 기존 에러 메시지 제거
const existingError = inputElement.parentElement.querySelector('.form-error');
if (existingError) {
existingError.remove();
}
// 새 에러 메시지 추가
if (message) {
const errorDiv = document.createElement('div');
errorDiv.className = 'form-error';
errorDiv.textContent = message;
errorDiv.setAttribute('role', 'alert');
inputElement.parentElement.appendChild(errorDiv);
}
},
// 필드에서 에러 제거
clearError(inputElement) {
inputElement.classList.remove('error');
const errorDiv = inputElement.parentElement.querySelector('.form-error');
if (errorDiv) {
errorDiv.remove();
}
},
// 전체 폼 검증
validateForm(formElement) {
let isValid = true;
const inputs = formElement.querySelectorAll('[required]');
inputs.forEach(input => {
if (!input.value.trim()) {
this.showError(input, '필수 입력 항목입니다.');
isValid = false;
} else {
this.clearError(input);
}
});
return isValid;
}
};
// ============================================
// 7. 네비게이션 유틸리티
// ============================================
window.Navigation = {
// 페이지 이동
goto(page) {
window.location.href = page;
},
// 뒤로가기
back() {
window.history.back();
},
// Bottom Navigation 활성화 상태 설정
updateBottomNav(activePage) {
const navItems = document.querySelectorAll('.bottom-nav__item');
navItems.forEach(item => {
if (item.getAttribute('data-page') === activePage) {
item.classList.add('active');
} else {
item.classList.remove('active');
}
});
}
};
// ============================================
// 8. 유틸리티 함수
// ============================================
window.Utils = {
// 숫자 포맷 (1000 -> 1,000)
formatNumber(num) {
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
},
// 날짜 포맷 (YYYY-MM-DD)
formatDate(date) {
const d = new Date(date);
const year = d.getFullYear();
const month = String(d.getMonth() + 1).padStart(2, '0');
const day = String(d.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
},
// 날짜 포맷 (YYYY.MM.DD)
formatDateDot(date) {
const d = new Date(date);
const year = d.getFullYear();
const month = String(d.getMonth() + 1).padStart(2, '0');
const day = String(d.getDate()).padStart(2, '0');
return `${year}.${month}.${day}`;
},
// 랜덤 ID 생성
generateId() {
return 'id-' + Date.now() + '-' + Math.random().toString(36).substr(2, 9);
},
// Debounce 함수
debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
},
// AI 처리 시뮬레이션
simulateAI(duration = 3000) {
return new Promise(resolve => {
setTimeout(resolve, duration);
});
},
// 이미지 로드 체크
preloadImage(src) {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = reject;
img.src = src;
});
}
};
// ============================================
// 9. 공통 이벤트 핸들러
// ============================================
// 뒤로가기 버튼
document.addEventListener('click', function(e) {
if (e.target.closest('.app-bar__back')) {
e.preventDefault();
window.Navigation.back();
}
});
});
};
// 입력 필드 실시간 검증
document.addEventListener('input', function(e) {
const input = e.target;
const fadeOut = (element, duration = 200) => {
if (!element) return Promise.resolve();
// 이메일 필드
if (input.type === 'email' && input.value) {
if (!window.FormValidator.isValidEmail(input.value)) {
window.FormValidator.showError(input, '올바른 이메일 형식을 입력하세요.');
} else {
window.FormValidator.clearError(input);
}
}
element.style.transition = `opacity ${duration}ms ease-out`;
element.style.opacity = '0';
// 전화번호 필드
if (input.name === 'phone' && input.value) {
// 자동 하이픈 추가
let value = input.value.replace(/[^0-9]/g, '');
if (value.length > 3 && value.length <= 7) {
value = value.slice(0, 3) + '-' + value.slice(3);
} else if (value.length > 7) {
value = value.slice(0, 3) + '-' + value.slice(3, 7) + '-' + value.slice(7, 11);
}
input.value = value;
// 검증
if (value.length === 13) {
if (!window.FormValidator.isValidPhone(value)) {
window.FormValidator.showError(input, '올바른 전화번호 형식을 입력하세요.');
} else {
window.FormValidator.clearError(input);
}
}
}
// 사업자번호 필드 (name이 businessNumber인 경우도 처리)
if ((input.name === 'business_number' || input.name === 'businessNumber') && input.value) {
// 자동 하이픈 추가
let value = input.value.replace(/[^0-9]/g, '');
if (value.length > 3 && value.length <= 5) {
value = value.slice(0, 3) + '-' + value.slice(3);
} else if (value.length > 5) {
value = value.slice(0, 3) + '-' + value.slice(3, 5) + '-' + value.slice(5, 10);
}
input.value = value;
// 검증 (완전한 형식일 때만)
if (value.length === 12) {
if (!window.FormValidator.isValidBusinessNumber(value)) {
window.FormValidator.showError(input, '올바른 사업자번호 형식을 입력하세요.');
} else {
window.FormValidator.clearError(input);
}
} else {
// 입력 중일 때는 에러 제거
window.FormValidator.clearError(input);
}
}
return new Promise((resolve) => {
setTimeout(() => {
element.style.display = 'none';
element.style.transition = '';
resolve();
}, duration);
});
};
// Material Icons 로드 확인
if (!document.querySelector('link[href*="material-icons"]')) {
const link = document.createElement('link');
link.href = 'https://fonts.googleapis.com/icon?family=Material+Icons';
link.rel = 'stylesheet';
document.head.appendChild(link);
const slideUp = (element, duration = 300) => {
if (!element) return Promise.resolve();
element.style.overflow = 'hidden';
element.style.transition = `height ${duration}ms ease-out, opacity ${duration}ms ease-out`;
element.style.height = element.offsetHeight + 'px';
return new Promise((resolve) => {
requestAnimationFrame(() => {
element.style.height = '0';
element.style.opacity = '0';
setTimeout(() => {
element.style.display = 'none';
element.style.overflow = '';
element.style.transition = '';
element.style.height = '';
element.style.opacity = '';
resolve();
}, duration);
});
});
};
const slideDown = (element, duration = 300) => {
if (!element) return Promise.resolve();
element.style.display = 'block';
element.style.overflow = 'hidden';
element.style.height = '0';
element.style.opacity = '0';
const height = element.scrollHeight;
return new Promise((resolve) => {
requestAnimationFrame(() => {
element.style.transition = `height ${duration}ms ease-out, opacity ${duration}ms ease-out`;
element.style.height = height + 'px';
element.style.opacity = '1';
setTimeout(() => {
element.style.overflow = '';
element.style.transition = '';
element.style.height = '';
element.style.opacity = '';
resolve();
}, duration);
});
});
};
// ========================================
// 데이터 유틸리티
// ========================================
const debounce = (func, wait = 300) => {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func.apply(this, args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
};
const throttle = (func, limit = 300) => {
let inThrottle;
return function executedFunction(...args) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => (inThrottle = false), limit);
}
};
};
const formatNumber = (num) => {
if (num >= 1000000) {
return (num / 1000000).toFixed(1) + 'M';
}
// Pretendard 폰트 로드 확인
if (!document.querySelector('link[href*="pretendard"]')) {
const link = document.createElement('link');
link.href = 'https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/static/pretendard.min.css';
link.rel = 'stylesheet';
document.head.appendChild(link);
if (num >= 1000) {
return (num / 1000).toFixed(1) + 'K';
}
return num.toString();
};
console.log('KT Event Marketing App - Common JS loaded');
const formatDate = (date) => {
const d = new Date(date);
const year = d.getFullYear();
const month = String(d.getMonth() + 1).padStart(2, '0');
const day = String(d.getDate()).padStart(2, '0');
return `${year}.${month}.${day}`;
};
})();
const formatDateTime = (date) => {
const d = new Date(date);
const hours = String(d.getHours()).padStart(2, '0');
const minutes = String(d.getMinutes()).padStart(2, '0');
return `${formatDate(date)} ${hours}:${minutes}`;
};
const getRelativeTime = (date) => {
const now = new Date();
const target = new Date(date);
const diff = Math.floor((now - target) / 1000);
if (diff < 60) return '방금 전';
if (diff < 3600) return `${Math.floor(diff / 60)}분 전`;
if (diff < 86400) return `${Math.floor(diff / 3600)}시간 전`;
if (diff < 604800) return `${Math.floor(diff / 86400)}일 전`;
return formatDate(date);
};
// ========================================
// 로컬 스토리지 유틸리티
// ========================================
const storage = {
set: (key, value) => {
try {
localStorage.setItem(key, JSON.stringify(value));
return true;
} catch (error) {
console.error('Storage set error:', error);
return false;
}
},
get: (key, defaultValue = null) => {
try {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : defaultValue;
} catch (error) {
console.error('Storage get error:', error);
return defaultValue;
}
},
remove: (key) => {
try {
localStorage.removeItem(key);
return true;
} catch (error) {
console.error('Storage remove error:', error);
return false;
}
},
clear: () => {
try {
localStorage.clear();
return true;
} catch (error) {
console.error('Storage clear error:', error);
return false;
}
}
};
// ========================================
// URL 유틸리티
// ========================================
const navigate = (url) => {
window.location.href = url;
};
const getQueryParam = (name) => {
const params = new URLSearchParams(window.location.search);
return params.get(name);
};
const setQueryParam = (name, value) => {
const url = new URL(window.location);
url.searchParams.set(name, value);
window.history.pushState({}, '', url);
};
// ========================================
// 유효성 검사 유틸리티
// ========================================
const validators = {
email: (value) => {
const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return re.test(value);
},
phone: (value) => {
const re = /^01[0-9]-?[0-9]{3,4}-?[0-9]{4}$/;
return re.test(value.replace(/[^0-9]/g, ''));
},
required: (value) => {
return value !== null && value !== undefined && value.trim() !== '';
},
minLength: (value, length) => {
return value.length >= length;
},
maxLength: (value, length) => {
return value.length <= length;
},
number: (value) => {
return !isNaN(value) && !isNaN(parseFloat(value));
},
url: (value) => {
try {
new URL(value);
return true;
} catch {
return false;
}
}
};
// ========================================
// 전역 공개
// ========================================
window.KTEvent = {
// DOM
$,
$$,
createElement,
addClass,
removeClass,
toggleClass,
hasClass,
// Events
on,
off,
delegate,
once,
// Animation
fadeIn,
fadeOut,
slideUp,
slideDown,
// Data
debounce,
throttle,
formatNumber,
formatDate,
formatDateTime,
getRelativeTime,
// Storage
storage,
// URL
navigate,
getQueryParam,
setQueryParam,
// Validation
validators
};

315
design/uiux/prototype/js/form.js vendored Normal file
View File

@ -0,0 +1,315 @@
/**
* KT AI 기반 소상공인 이벤트 자동 생성 서비스
* Form Validation JavaScript
*
* 작성일: 2025-01-20
*/
(function() {
'use strict';
const { $, $$, on, addClass, removeClass, validators, createElement } = window.KTEvent;
// ========================================
// Form Validator
// ========================================
class FormValidator {
constructor(form, options = {}) {
if (!form) return;
this.form = form;
this.options = {
validateOnBlur: true,
validateOnInput: false,
scrollToError: true,
...options
};
this.fields = new Map();
this.errors = new Map();
this.init();
}
init() {
this.bindEvents();
}
addField(name, rules) {
this.fields.set(name, rules);
return this;
}
bindEvents() {
// Form submit
on(this.form, 'submit', (e) => {
e.preventDefault();
if (this.validate()) {
this.onSubmit();
}
});
// Field validation
this.fields.forEach((rules, name) => {
const field = this.form.elements[name];
if (!field) return;
if (this.options.validateOnBlur) {
on(field, 'blur', () => {
this.validateField(name);
});
}
if (this.options.validateOnInput) {
on(field, 'input', () => {
if (this.errors.has(name)) {
this.validateField(name);
}
});
}
});
}
validateField(name) {
const field = this.form.elements[name];
const rules = this.fields.get(name);
if (!field || !rules) return true;
const value = field.value.trim();
let isValid = true;
let errorMessage = '';
// Required
if (rules.required && !validators.required(value)) {
isValid = false;
errorMessage = rules.messages?.required || '필수 입력 항목입니다.';
}
// Email
if (isValid && rules.email && value && !validators.email(value)) {
isValid = false;
errorMessage = rules.messages?.email || '올바른 이메일 형식이 아닙니다.';
}
// Phone
if (isValid && rules.phone && value && !validators.phone(value)) {
isValid = false;
errorMessage = rules.messages?.phone || '올바른 전화번호 형식이 아닙니다.';
}
// Min Length
if (isValid && rules.minLength && value && !validators.minLength(value, rules.minLength)) {
isValid = false;
errorMessage = rules.messages?.minLength || `최소 ${rules.minLength}자 이상 입력해주세요.`;
}
// Max Length
if (isValid && rules.maxLength && value && !validators.maxLength(value, rules.maxLength)) {
isValid = false;
errorMessage = rules.messages?.maxLength || `최대 ${rules.maxLength}자까지 입력 가능합니다.`;
}
// Number
if (isValid && rules.number && value && !validators.number(value)) {
isValid = false;
errorMessage = rules.messages?.number || '숫자만 입력 가능합니다.';
}
// URL
if (isValid && rules.url && value && !validators.url(value)) {
isValid = false;
errorMessage = rules.messages?.url || '올바른 URL 형식이 아닙니다.';
}
// Custom validator
if (isValid && rules.custom && !rules.custom(value, field)) {
isValid = false;
errorMessage = rules.messages?.custom || '유효하지 않은 값입니다.';
}
// Update UI
if (isValid) {
this.clearError(name);
} else {
this.setError(name, errorMessage);
}
return isValid;
}
validate() {
let isValid = true;
let firstErrorField = null;
this.fields.forEach((rules, name) => {
if (!this.validateField(name)) {
isValid = false;
if (!firstErrorField) {
firstErrorField = this.form.elements[name];
}
}
});
// Scroll to first error
if (!isValid && firstErrorField && this.options.scrollToError) {
firstErrorField.scrollIntoView({ behavior: 'smooth', block: 'center' });
firstErrorField.focus();
}
return isValid;
}
setError(name, message) {
const field = this.form.elements[name];
if (!field) return;
this.errors.set(name, message);
const formGroup = field.closest('.form-group');
if (!formGroup) return;
addClass(field, 'input-error');
let errorElement = $('.form-error', formGroup);
if (!errorElement) {
errorElement = createElement('span', 'form-error');
formGroup.appendChild(errorElement);
}
errorElement.textContent = message;
}
clearError(name) {
const field = this.form.elements[name];
if (!field) return;
this.errors.delete(name);
const formGroup = field.closest('.form-group');
if (!formGroup) return;
removeClass(field, 'input-error');
const errorElement = $('.form-error', formGroup);
if (errorElement) {
errorElement.remove();
}
}
clearAllErrors() {
this.errors.clear();
$$('.input-error', this.form).forEach(field => {
removeClass(field, 'input-error');
});
$$('.form-error', this.form).forEach(error => {
error.remove();
});
}
getValues() {
const values = {};
this.fields.forEach((rules, name) => {
const field = this.form.elements[name];
if (field) {
if (field.type === 'checkbox') {
values[name] = field.checked;
} else if (field.type === 'radio') {
const checked = this.form.querySelector(`input[name="${name}"]:checked`);
values[name] = checked ? checked.value : null;
} else {
values[name] = field.value.trim();
}
}
});
return values;
}
reset() {
this.form.reset();
this.clearAllErrors();
}
onSubmit() {
// Override this method
console.log('Form submitted:', this.getValues());
}
}
// ========================================
// Character Counter
// ========================================
class CharacterCounter {
constructor(textarea, maxLength) {
this.textarea = textarea;
this.maxLength = maxLength;
this.counter = null;
this.init();
}
init() {
const formGroup = this.textarea.closest('.form-group');
if (!formGroup) return;
this.counter = createElement('span', 'form-helper');
this.counter.style.textAlign = 'right';
formGroup.appendChild(this.counter);
this.updateCounter();
on(this.textarea, 'input', () => {
this.updateCounter();
});
}
updateCounter() {
const length = this.textarea.value.length;
this.counter.textContent = `${length} / ${this.maxLength}`;
if (length > this.maxLength) {
this.counter.style.color = 'var(--color-error)';
} else {
this.counter.style.color = 'var(--color-gray-500)';
}
}
}
// ========================================
// Auto Initialize
// ========================================
function initForms() {
// Character counters
$$('textarea[maxlength]').forEach(textarea => {
const maxLength = parseInt(textarea.getAttribute('maxlength'));
if (maxLength > 0) {
new CharacterCounter(textarea, maxLength);
}
});
}
// Initialize on DOMContentLoaded
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initForms);
} else {
initForms();
}
// ========================================
// Export
// ========================================
window.KTEvent = window.KTEvent || {};
Object.assign(window.KTEvent, {
FormValidator,
CharacterCounter
});
})();

418
design/uiux/prototype/js/modal.js vendored Normal file
View File

@ -0,0 +1,418 @@
/**
* KT AI 기반 소상공인 이벤트 자동 생성 서비스
* Modal JavaScript
*
* 작성일: 2025-01-20
*/
(function() {
'use strict';
const { $, createElement, addClass, removeClass, on, fadeIn, fadeOut } = window.KTEvent;
// ========================================
// Modal
// ========================================
class Modal {
constructor(options = {}) {
this.options = {
title: '',
content: '',
size: 'medium', // small, medium, large, fullscreen
showClose: true,
footer: null,
onOpen: null,
onClose: null,
...options
};
this.backdrop = null;
this.modal = null;
this.isOpen = false;
this.create();
}
create() {
// Create backdrop
this.backdrop = createElement('div', 'modal-backdrop');
// Create modal
this.modal = createElement('div', `modal modal--${this.options.size}`);
// Header
if (this.options.title || this.options.showClose) {
const header = createElement('div', 'modal__header');
if (this.options.title) {
const title = createElement('h3', 'modal__title');
title.textContent = this.options.title;
header.appendChild(title);
}
if (this.options.showClose) {
const closeBtn = createElement('button', 'modal__close');
closeBtn.innerHTML = '&times;';
closeBtn.setAttribute('aria-label', '닫기');
on(closeBtn, 'click', () => this.close());
header.appendChild(closeBtn);
}
this.modal.appendChild(header);
}
// Body
const body = createElement('div', 'modal__body');
if (typeof this.options.content === 'string') {
body.innerHTML = this.options.content;
} else if (this.options.content instanceof HTMLElement) {
body.appendChild(this.options.content);
}
this.modal.appendChild(body);
// Footer
if (this.options.footer) {
const footer = createElement('div', 'modal__footer');
footer.innerHTML = this.options.footer;
this.modal.appendChild(footer);
}
this.backdrop.appendChild(this.modal);
// Close on backdrop click
on(this.backdrop, 'click', (e) => {
if (e.target === this.backdrop) {
this.close();
}
});
// Close on ESC key
this.handleEscKey = (e) => {
if (e.key === 'Escape' && this.isOpen) {
this.close();
}
};
}
open() {
if (this.isOpen) return;
document.body.appendChild(this.backdrop);
document.body.style.overflow = 'hidden';
// Animation
requestAnimationFrame(() => {
fadeIn(this.backdrop, 200);
});
this.isOpen = true;
on(document, 'keydown', this.handleEscKey);
if (this.options.onOpen) {
this.options.onOpen(this);
}
}
close() {
if (!this.isOpen) return;
fadeOut(this.backdrop, 200).then(() => {
if (this.backdrop.parentNode) {
document.body.removeChild(this.backdrop);
}
document.body.style.overflow = '';
});
this.isOpen = false;
document.removeEventListener('keydown', this.handleEscKey);
if (this.options.onClose) {
this.options.onClose(this);
}
}
destroy() {
this.close();
this.backdrop = null;
this.modal = null;
}
}
// ========================================
// Bottom Sheet
// ========================================
class BottomSheet {
constructor(options = {}) {
this.options = {
title: '',
content: '',
showClose: true,
footer: null,
onOpen: null,
onClose: null,
...options
};
this.backdrop = null;
this.sheet = null;
this.isOpen = false;
this.create();
}
create() {
// Create backdrop
this.backdrop = createElement('div', 'modal-backdrop');
// Create bottom sheet
this.sheet = createElement('div', 'bottom-sheet');
// Handle
const handle = createElement('div', 'bottom-sheet__handle');
this.sheet.appendChild(handle);
// Header
if (this.options.title || this.options.showClose) {
const header = createElement('div', 'bottom-sheet__header');
if (this.options.title) {
const title = createElement('h3', 'bottom-sheet__title');
title.textContent = this.options.title;
header.appendChild(title);
}
if (this.options.showClose) {
const closeBtn = createElement('button', 'bottom-sheet__close');
closeBtn.innerHTML = '&times;';
closeBtn.setAttribute('aria-label', '닫기');
on(closeBtn, 'click', () => this.close());
header.appendChild(closeBtn);
}
this.sheet.appendChild(header);
}
// Body
const body = createElement('div', 'bottom-sheet__body');
if (typeof this.options.content === 'string') {
body.innerHTML = this.options.content;
} else if (this.options.content instanceof HTMLElement) {
body.appendChild(this.options.content);
}
this.sheet.appendChild(body);
// Footer
if (this.options.footer) {
const footer = createElement('div', 'bottom-sheet__footer');
footer.innerHTML = this.options.footer;
this.sheet.appendChild(footer);
}
this.backdrop.appendChild(this.sheet);
// Close on backdrop click
on(this.backdrop, 'click', (e) => {
if (e.target === this.backdrop) {
this.close();
}
});
}
open() {
if (this.isOpen) return;
document.body.appendChild(this.backdrop);
document.body.style.overflow = 'hidden';
requestAnimationFrame(() => {
fadeIn(this.backdrop, 200);
});
this.isOpen = true;
if (this.options.onOpen) {
this.options.onOpen(this);
}
}
close() {
if (!this.isOpen) return;
fadeOut(this.backdrop, 200).then(() => {
if (this.backdrop.parentNode) {
document.body.removeChild(this.backdrop);
}
document.body.style.overflow = '';
});
this.isOpen = false;
if (this.options.onClose) {
this.options.onClose(this);
}
}
destroy() {
this.close();
this.backdrop = null;
this.sheet = null;
}
}
// ========================================
// Toast
// ========================================
class Toast {
static show(message, options = {}) {
const defaults = {
type: 'default', // default, success, warning, error, info
duration: 3000,
position: 'bottom', // top, bottom
icon: null
};
const config = { ...defaults, ...options };
// Create toast
const toast = createElement('div', `toast toast--${config.type} toast--${config.position}`);
// Icon
if (config.icon) {
const icon = createElement('span', 'toast__icon');
icon.innerHTML = config.icon;
toast.appendChild(icon);
}
// Message
const msg = createElement('span', 'toast__message');
msg.textContent = message;
toast.appendChild(msg);
// Close button
const closeBtn = createElement('button', 'toast__close');
closeBtn.innerHTML = '&times;';
closeBtn.setAttribute('aria-label', '닫기');
toast.appendChild(closeBtn);
// Add to DOM
document.body.appendChild(toast);
// Animation
requestAnimationFrame(() => {
fadeIn(toast, 200);
});
// Auto close
const timeout = setTimeout(() => {
close();
}, config.duration);
// Close function
const close = () => {
clearTimeout(timeout);
fadeOut(toast, 200).then(() => {
if (toast.parentNode) {
document.body.removeChild(toast);
}
});
};
// Close on button click
on(closeBtn, 'click', close);
return { close };
}
}
// ========================================
// Dialog (Confirm)
// ========================================
class Dialog {
static confirm(options = {}) {
return new Promise((resolve) => {
const config = {
title: '확인',
message: '',
confirmText: '확인',
cancelText: '취소',
...options
};
const backdrop = createElement('div', 'modal-backdrop');
const dialog = createElement('div', 'dialog');
// Header
const header = createElement('div', 'dialog__header');
const title = createElement('h3', 'dialog__title');
title.textContent = config.title;
header.appendChild(title);
dialog.appendChild(header);
// Body
const body = createElement('div', 'dialog__body');
const message = createElement('p', 'dialog__message');
message.textContent = config.message;
body.appendChild(message);
dialog.appendChild(body);
// Footer
const footer = createElement('div', 'dialog__footer');
const cancelBtn = createElement('button', 'btn btn-secondary btn-medium');
cancelBtn.textContent = config.cancelText;
on(cancelBtn, 'click', () => {
close(false);
});
footer.appendChild(cancelBtn);
const confirmBtn = createElement('button', 'btn btn-primary btn-medium');
confirmBtn.textContent = config.confirmText;
on(confirmBtn, 'click', () => {
close(true);
});
footer.appendChild(confirmBtn);
dialog.appendChild(footer);
backdrop.appendChild(dialog);
// Close function
const close = (result) => {
fadeOut(backdrop, 200).then(() => {
if (backdrop.parentNode) {
document.body.removeChild(backdrop);
}
document.body.style.overflow = '';
resolve(result);
});
};
// Add to DOM
document.body.appendChild(backdrop);
document.body.style.overflow = 'hidden';
requestAnimationFrame(() => {
fadeIn(backdrop, 200);
});
});
}
}
// ========================================
// Export
// ========================================
window.KTEvent = window.KTEvent || {};
Object.assign(window.KTEvent, {
Modal,
BottomSheet,
Toast,
Dialog
});
})();

296
design/uiux/prototype/js/navigation.js vendored Normal file
View File

@ -0,0 +1,296 @@
/**
* KT AI 기반 소상공인 이벤트 자동 생성 서비스
* Navigation JavaScript
*
* 작성일: 2025-01-20
*/
(function() {
'use strict';
const { $, $$, on, addClass, removeClass } = window.KTEvent;
// ========================================
// Bottom Navigation
// ========================================
class BottomNavigation {
constructor(element) {
if (!element) return;
this.element = element;
this.items = $$('.bottom-nav__item', element);
this.currentPath = window.location.pathname;
this.init();
}
init() {
this.setActiveItem();
this.bindEvents();
}
setActiveItem() {
this.items.forEach(item => {
const href = item.getAttribute('href');
if (href && this.currentPath.includes(href.replace('.html', ''))) {
addClass(item, 'bottom-nav__item--active');
} else {
removeClass(item, 'bottom-nav__item--active');
}
});
}
bindEvents() {
this.items.forEach(item => {
on(item, 'click', (e) => {
const href = item.getAttribute('href');
if (!href || href === '#') {
e.preventDefault();
}
this.items.forEach(i => removeClass(i, 'bottom-nav__item--active'));
addClass(item, 'bottom-nav__item--active');
});
});
}
}
// ========================================
// Tab Navigation
// ========================================
class TabNavigation {
constructor(element) {
if (!element) return;
this.element = element;
this.items = $$('.tab-nav__item', element);
this.panels = [];
this.init();
}
init() {
this.setActiveTa();
this.bindEvents();
}
setActiveTab() {
const activeItem = $('.tab-nav__item--active', this.element);
if (!activeItem && this.items.length > 0) {
addClass(this.items[0], 'tab-nav__item--active');
}
}
bindEvents() {
this.items.forEach((item, index) => {
on(item, 'click', (e) => {
e.preventDefault();
this.activateTab(index);
});
});
}
activateTab(index) {
this.items.forEach(item => removeClass(item, 'tab-nav__item--active'));
addClass(this.items[index], 'tab-nav__item--active');
// Emit custom event
const event = new CustomEvent('tabchange', {
detail: { index, item: this.items[index] }
});
this.element.dispatchEvent(event);
}
}
// ========================================
// App Bar
// ========================================
class AppBar {
constructor(element) {
if (!element) return;
this.element = element;
this.backButton = $('.app-bar__back', element);
this.init();
}
init() {
this.bindEvents();
}
bindEvents() {
if (this.backButton) {
on(this.backButton, 'click', () => {
window.history.back();
});
}
}
}
// ========================================
// Stepper
// ========================================
class Stepper {
constructor(element) {
if (!element) return;
this.element = element;
this.steps = $$('.stepper__step', element);
this.currentStep = 0;
this.init();
}
init() {
this.updateSteps();
}
updateSteps() {
this.steps.forEach((step, index) => {
removeClass(step, 'stepper__step--completed', 'stepper__step--active', 'stepper__step--pending');
if (index < this.currentStep) {
addClass(step, 'stepper__step--completed');
} else if (index === this.currentStep) {
addClass(step, 'stepper__step--active');
} else {
addClass(step, 'stepper__step--pending');
}
// Update line
const line = $('.stepper__line', step);
if (line) {
if (index < this.currentStep) {
addClass(line, 'stepper__line--completed');
} else {
removeClass(line, 'stepper__line--completed');
}
}
});
}
next() {
if (this.currentStep < this.steps.length - 1) {
this.currentStep++;
this.updateSteps();
return true;
}
return false;
}
previous() {
if (this.currentStep > 0) {
this.currentStep--;
this.updateSteps();
return true;
}
return false;
}
goTo(step) {
if (step >= 0 && step < this.steps.length) {
this.currentStep = step;
this.updateSteps();
return true;
}
return false;
}
}
// ========================================
// Progress Bar
// ========================================
class ProgressBar {
constructor(element) {
if (!element) return;
this.element = element;
this.fill = $('.progress-bar__fill', element);
this.text = $('.progress-bar__text', element);
this.progress = 0;
this.init();
}
init() {
this.setProgress(this.progress);
}
setProgress(value) {
this.progress = Math.max(0, Math.min(100, value));
if (this.fill) {
this.fill.style.width = this.progress + '%';
}
if (this.text) {
this.text.textContent = `${this.progress}%`;
}
}
increment(value = 1) {
this.setProgress(this.progress + value);
}
reset() {
this.setProgress(0);
}
}
// ========================================
// Auto Initialize
// ========================================
function initNavigation() {
// Bottom Navigation
const bottomNav = $('.bottom-nav');
if (bottomNav) {
new BottomNavigation(bottomNav);
}
// Tab Navigation
$$('.tab-nav').forEach(tabNav => {
new TabNavigation(tabNav);
});
// App Bar
const appBar = $('.app-bar');
if (appBar) {
new AppBar(appBar);
}
// Stepper
$$('.stepper').forEach(stepper => {
new Stepper(stepper);
});
// Progress Bar
$$('.progress-bar').forEach(progressBar => {
new ProgressBar(progressBar);
});
}
// Initialize on DOMContentLoaded
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initNavigation);
} else {
initNavigation();
}
// Export classes
window.KTEvent = window.KTEvent || {};
Object.assign(window.KTEvent, {
BottomNavigation,
TabNavigation,
AppBar,
Stepper,
ProgressBar
});
})();

View File

@ -0,0 +1,303 @@
# KT AI 기반 소상공인 이벤트 자동 생성 서비스 - 사용자 여정 맵
## 문서 정보
- **작성일**: 2025-01-20
- **버전**: 2.0
- **작성자**: UI/UX Designer
- **최종 업데이트**: 파일명 정리 및 여정 재구성
---
## 📱 전체 화면 목록 (26개 + 추가 화면)
### ✅ 구현 완료 (24개)
| 번호 | 화면명 | 파일명 | 유저스토리 | 중요도 | 상태 |
|-----|--------|--------|-----------|--------|------|
| **User Service** |
| 01 | 로그인 | 01-로그인.html | UFR-USER-001 | Must | ✅ |
| 02 | 홈화면 | 02-홈화면.html | UFR-USER-002 | Must | ✅ |
| 03 | 회원가입 | 03-회원가입.html | UFR-USER-010 | Must | ✅ |
| 04 | 매장정보등록 | 04-매장정보등록.html | UFR-USER-020 | Must | ✅ |
| **Event Planning Service** |
| 05 | 이벤트목적선택 | 05-이벤트목적선택.html | UFR-PLAN-010 | Must | ✅ |
| 06 | AI트렌드분석결과 | 06-AI트렌드분석결과.html | UFR-PLAN-020 | Must | ✅ |
| 06-1 | AI이벤트유형추천 | 06-AI이벤트유형추천.html | - | 추가 | ✅ 최근수정 |
| 07 | AI이벤트상품추천 | 07-AI이벤트상품추천.html | UFR-PLAN-030 | Must | ✅ |
| 08 | AI참여방법설계 | 08-AI참여방법설계.html | UFR-PLAN-040 | Must | ✅ |
| 09 | AI홍보문구생성 | 09-AI홍보문구생성.html | UFR-PLAN-050 | Must | ✅ |
| 10 | 이벤트기획안승인 | 10-이벤트기획안승인.html | UFR-PLAN-060 | Must | ✅ |
| **Content Generation Service** |
| 11 | AI이미지생성 | 11-AI이미지생성.html | UFR-CONT-010 | Must | ✅ |
| 12 | SNS콘텐츠생성 | 12-SNS콘텐츠생성.html | UFR-CONT-030 | Must | ✅ |
| 13 | QR포스터생성선택 | 13-QR포스터생성선택.html | UFR-CONT-040 | Must | ✅ |
| 14 | QR포스터상세설정 | - | UFR-CONT-040 | Must | ⏳ 13번에서 분리 필요 |
| 15 | 콘텐츠편집 | 15-콘텐츠편집.html | UFR-CONT-050 | Should | ✅ |
| 16 | 콘텐츠최종승인 | 16-콘텐츠최종승인.html | UFR-CONT-060 | Must | ✅ |
| **Distribution Service** |
| 17 | 배포채널선택 | 17-배포채널선택.html | UFR-DIST-010 | Must | ✅ |
| 18 | 배포진행상태 | 18-배포진행상태.html | UFR-DIST-020~050 | Must | ✅ |
| 19 | 오프라인자료다운로드 | 19-오프라인자료다운로드.html | UFR-DIST-060 | Should | ✅ |
| **Participation Service** |
| 20 | 이벤트참여 | - | UFR-PART-010 | Must | ⏳ 고객용 화면 |
| 21 | 참여완료 | - | UFR-PART-010 | Must | ⏳ 고객용 화면 |
| **Analytics & Management Service** |
| 22 | 당첨자명단관리 | 22-당첨자명단관리.html | UFR-PART-050 | Must | ✅ |
| 23 | 실시간대시보드 | 23-실시간대시보드.html | UFR-ANAL-010 | Must | ✅ |
| 24 | 채널별성과분석 | 24-채널별성과분석.html | UFR-ANAL-020 | Must | ✅ |
| 25 | 광고수익률분석 | 25-광고수익률분석.html | UFR-ANAL-030 | Must | ✅ |
| 26 | 분석리포트 | 26-분석리포트.html | UFR-ANAL-040 | Should | ✅ |
| **추가 화면 (네비게이션용)** |
| - | 마이페이지 | 25-마이페이지.html | - | 추가 | ✅ |
| - | 이벤트목록 | 26-이벤트목록.html | - | 추가 | ✅ |
### ⏳ 작성 필요 (3개)
- **14-QR포스터상세설정**: 13번 화면에서 기능 분리 필요
- **20-이벤트참여**: 고객용 참여 화면
- **21-참여완료**: 고객용 완료 화면
---
## 🗺️ 소상공인 여정 (Main Journey)
### 1⃣ 온보딩 여정
```mermaid
graph TD
A[01-로그인] --> B{최초 방문?}
B -->|Yes| C[03-회원가입]
B -->|No| D[02-홈화면]
C --> E[04-매장정보등록]
E --> D
```
**화면 구성:**
- **01-로그인**: JWT 인증, 이메일/비밀번호
- **03-회원가입**: 기본 정보 입력
- **04-매장정보등록**: 사업자번호 검증, 업종, 주소, 영업시간
- **02-홈화면**: 대시보드 요약, 새 이벤트 생성 버튼
---
### 2⃣ 이벤트 기획 여정 (Event Planning)
```mermaid
graph TD
A[02-홈화면] --> B[05-이벤트목적선택]
B --> C[06-AI트렌드분석결과]
C --> D[06-1-AI이벤트유형추천]
D --> E[07-AI이벤트상품추천]
E --> F[08-AI참여방법설계]
F --> G[09-AI홍보문구생성]
G --> H[10-이벤트기획안승인]
E -->|수정| E
G -->|편집| G
```
**화면별 주요 기능:**
| 화면 | 주요 기능 | AI 역할 | 사용자 액션 |
|-----|----------|---------|-----------|
| 05-이벤트목적선택 | 목적 선택 (신규고객/재방문/브랜드인지도) | - | 목적 선택 |
| 06-AI트렌드분석결과 | 시장 트렌드, 경쟁사 분석 | 트렌드 분석 | 확인 |
| 06-1-AI이벤트유형추천 | 이벤트 유형 7개 추천 | 맞춤 추천 | 유형 선택 |
| 07-AI이벤트상품추천 | Top 5 경품 추천 | 경품 추천 | 선택/수정 |
| 08-AI참여방법설계 | 3가지 참여 방법 제시 | 방법 설계 | 방법 선택 |
| 09-AI홍보문구생성 | 5개 버전 문구 생성 | 문구 생성 | 선택/편집 |
| 10-이벤트기획안승인 | 최종 확인 및 승인 | - | 승인/수정 |
**🔄 특이사항:**
- **06-1-AI이벤트유형추천**: 최근 추가된 화면으로 localStorage 저장 로직 포함
- **07-AI이벤트상품추천**: localStorage에서 선택한 이벤트 유형 불러옴
- **순환 가능**: 경품 추천(07), 홍보문구(09)는 수정 가능
---
### 3⃣ 콘텐츠 생성 여정 (Content Generation)
```mermaid
graph TD
A[10-이벤트기획안승인] --> B[11-AI이미지생성]
B --> C[12-SNS콘텐츠생성]
C --> D[13-QR포스터생성선택]
D -->|생성 안함| F
D -->|생성| E[14-QR포스터상세설정]
E --> F[15-콘텐츠편집]
F --> G[16-콘텐츠최종승인]
```
**화면별 주요 기능:**
| 화면 | 주요 기능 | 출력물 | 사용자 액션 |
|-----|----------|--------|-----------|
| 11-AI이미지생성 | 3종 이미지 생성 | 정사각형/세로/가로 | 선택/재생성 |
| 12-SNS콘텐츠생성 | SNS별 최적화 콘텐츠 | Instagram, Blog, Kakao | 채널 선택 |
| 13-QR포스터생성선택 | QR 포스터 생성 여부 | - | 생성 여부 선택 |
| 14-QR포스터상세설정 | 크기/형식 설정 | A4/A3 PDF | 옵션 선택 |
| 15-콘텐츠편집 | 간단한 수정 | - | 텍스트/이미지 수정 |
| 16-콘텐츠최종승인 | 최종 확인 | - | 승인/수정 |
**⏳ TODO:**
- 14번 화면 분리: 13번에서 QR 포스터 상세 설정 기능 분리
---
### 4⃣ 배포 여정 (Distribution)
```mermaid
graph TD
A[16-콘텐츠최종승인] --> B[17-배포채널선택]
B --> C[18-배포진행상태]
C --> D{오프라인 자료?}
D -->|Yes| E[19-오프라인자료다운로드]
D -->|No| F[02-홈화면]
E --> F
```
**화면별 주요 기능:**
| 화면 | 주요 기능 | 배포 채널 | 실시간 상태 |
|-----|----------|----------|-----------|
| 17-배포채널선택 | 다중 채널 선택 (최소 1개) | Instagram, 우리동네TV, 지니TV, 링고비즈 | - |
| 18-배포진행상태 | 채널별 배포 상태 모니터링 | 전체 채널 | 5분 갱신 |
| 19-오프라인자료다운로드 | QR/PDF 다운로드 | - | - |
---
### 5⃣ 모니터링 & 분석 여정 (Analytics)
```mermaid
graph TD
A[02-홈화면] --> B[23-실시간대시보드]
B --> C[24-채널별성과분석]
C --> D[25-광고수익률분석]
D --> E[22-당첨자명단관리]
E --> F{리포트 필요?}
F -->|Yes| G[26-분석리포트]
F -->|No| H[02-홈화면]
G --> H
```
**화면별 주요 기능:**
| 화면 | 주요 기능 | 갱신 주기 | 주요 지표 |
|-----|----------|----------|----------|
| 23-실시간대시보드 | 종합 성과 한눈에 보기 | 5분 | 참여수, 조회수, 전환율 |
| 24-채널별성과분석 | 채널별 비교 분석 | 실시간 | 채널별 ROI, 전환율 |
| 25-광고수익률분석 | 투자 대비 효과 분석 | 일별 | ROI, 순수익, 비용 |
| 22-당첨자명단관리 | 경품 지급 관리 | 수동 | 당첨자 명단, 지급 상태 |
| 26-분석리포트 | PDF 리포트 생성 | 수동 | 종합 분석 |
---
## 👥 고객 여정 (Customer Journey)
### 6⃣ 이벤트 참여 여정
```mermaid
graph TD
A[이벤트 발견] --> B[20-이벤트참여]
B --> C[21-참여완료]
C --> D[응모번호 발급]
D --> E[당첨 발표 대기]
E --> F[당첨 알림 수신]
```
**발견 채널:**
- QR코드 (오프라인 매장)
- SNS (Instagram, Blog, Kakao)
- 우리동네TV
- 지니TV
- 링고비즈
**⏳ TODO:**
- **20-이벤트참여**: 고객용 참여 화면 제작
- **21-참여완료**: 응모번호 발급 화면 제작
---
## 🧭 네비게이션 구조
### 바텀 네비게이션 (소상공인용)
| 아이콘 | 메뉴명 | 이동 화면 | 기능 |
|-------|--------|----------|------|
| 🏠 | 홈 | 23-실시간대시보드 | 실시간 현황 |
| 📋 | 이벤트 | 26-이벤트목록 | 이벤트 관리 + 새 이벤트 |
| 📊 | 분석 | 24-채널별성과분석 | 성과 분석 |
| 👤 | 마이페이지 | 25-마이페이지 | 설정, 매장정보 |
### 상단 앱바
- **제목**: 컨텍스트별 화면명
- **뒤로가기**: 이전 화면 (하위 화면만)
- **액션 버튼**: 저장, 공유, 필터 등
---
## 📂 Backup 파일 (향후 검토)
다음 파일들이 `backup/` 폴더로 이동되었습니다:
| 파일명 | 이유 | 검토 사항 |
|--------|------|----------|
| 04-1-AI이벤트유형추천.html | 설계서 미포함 | 06-1과 통합 검토 |
| 04-2-이벤트상세정보.html | 설계서 미포함 | 기능 통합 검토 |
| 05-이벤트목적선택.html | 중복 파일 | 삭제 |
| 06-1-AI트렌드분석결과.html | 버전 파일 | 06번과 통합 검토 |
| 06-2-이벤트상세정보.html | 버전 파일 | 기능 통합 검토 |
| 10-AI영상제작.html | 설계서에서 삭제됨 | 향후 재사용 가능성 |
---
## 🔄 최근 수정 내역
### 2025-01-20 (파일명 정리)
1. **파일명 재번호**: 설계서 기준으로 모든 화면 번호 재정렬
2. **용어 통일**:
- `AI경품추천``AI이벤트상품추천`
- `ROI분석``광고수익률분석`
3. **추가 화면 식별**:
- `06-1-AI이벤트유형추천` (최근 작업, localStorage 로직 포함)
- `25-마이페이지`, `26-이벤트목록` (네비게이션용)
4. **중복 파일 정리**: backup 폴더로 이동
### 버그 수정 (06-1-AI이벤트유형추천.html)
**문제**: 이벤트 유형 선택 후 07번 화면에서 자동 리다이렉트 발생
**원인**: `selectedEventType`이 localStorage에 저장되지 않음
**해결**: `proceedToNext` 함수에서 localStorage 직접 저장 로직 추가 (560-562줄)
```javascript
// 수정 전
window.AppState.selectedEventType = selectedEvent;
window.AppState.save();
// 수정 후
localStorage.setItem('kt_selected_event_type', JSON.stringify(selectedEvent));
window.AppState.save();
```
---
## 📊 통계
- **총 화면 수**: 26개 (설계서 기준) + 3개 (추가 화면)
- **구현 완료**: 24개 ✅
- **작성 필요**: 3개 ⏳
- **Backup 파일**: 6개
---
## ✅ Next Steps
1. **14-QR포스터상세설정**: 13번 화면에서 기능 분리
2. **20-이벤트참여**: 고객용 참여 화면 제작
3. **21-참여완료**: 응모번호 발급 화면 제작
4. **06-1-AI이벤트유형추천**: 설계서에 정식 추가 여부 검토
5. **Backup 파일 검토**: 통합 또는 삭제 결정

View File

@ -0,0 +1,64 @@
# 프로토타입 파일명 변경 계획
## 변경 대상 (설계서 기준으로 정리)
### 1단계: 번호 재정렬 (역순으로 변경하여 충돌 방지)
| 현재 파일명 | 변경 파일명 | 비고 |
|------------|------------|------|
| 24-분석리포트.html | 26-분석리포트.html | |
| 23-ROI분석.html | 25-광고수익률분석.html | 이름도 변경 |
| 22-채널별성과분석.html | 24-채널별성과분석.html | |
| 21-실시간대시보드.html | 23-실시간대시보드.html | |
| 20-당첨자명단관리.html | 22-당첨자명단관리.html | |
| 17-오프라인자료다운로드.html | 19-오프라인자료다운로드.html | |
| 16-배포진행상태.html | 18-배포진행상태.html | |
| 15-배포채널선택.html | 17-배포채널선택.html | |
| 14-콘텐츠최종승인.html | 16-콘텐츠최종승인.html | |
| 13-콘텐츠편집.html | 15-콘텐츠편집.html | |
| 12-QR포스터생성.html | 13-QR포스터생성선택.html | 이름도 변경 |
| 11-SNS콘텐츠생성.html | 12-SNS콘텐츠생성.html | |
| 09-AI이미지생성.html | 11-AI이미지생성.html | |
| 08-이벤트기획안승인.html | 10-이벤트기획안승인.html | |
| 07-AI홍보문구생성.html | 09-AI홍보문구생성.html | |
| 06-AI참여방법설계.html | 08-AI참여방법설계.html | |
| 05-AI경품추천.html | 07-AI이벤트상품추천.html | 이름도 변경 |
| 04-AI트렌드분석결과.html | 06-AI트렌드분석결과.html | |
| 03-이벤트목적선택.html | 05-이벤트목적선택.html | |
| 02-매장정보등록.html | 04-매장정보등록.html | |
| 01-회원가입.html | 03-회원가입.html | |
| 21.5-홈.html | 02-홈화면.html | 이름도 변경 |
| 00-로그인.html | 01-로그인.html | |
### 2단계: 중복/삭제 파일 처리
| 파일명 | 처리 방법 | 사유 |
|--------|----------|------|
| 04-1-AI이벤트유형추천.html | backup 폴더로 이동 | 설계서에 없음, 추후 통합 검토 |
| 04-2-이벤트상세정보.html | backup 폴더로 이동 | 설계서에 없음, 추후 통합 검토 |
| 05-이벤트목적선택.html | 삭제 (03-과 중복) | 중복 파일 |
| 06-1-AI트렌드분석결과.html | backup 폴더로 이동 | 버전 파일, 추후 검토 |
| 06-2-이벤트상세정보.html | backup 폴더로 이동 | 버전 파일, 추후 검토 |
| 06-AI이벤트유형추천.html | 현재 사용 중 | 이벤트 유형 선택 화면 |
| 10-AI영상제작.html | backup 폴더로 이동 | 설계서에서 삭제됨 |
### 3단계: 추가 화면 정리 (유지)
| 파일명 | 비고 |
|--------|------|
| 25-마이페이지.html | 네비게이션용 추가 화면 |
| 26-이벤트목록.html | 네비게이션용 추가 화면 |
### 4단계: 누락 화면 (향후 작성 필요)
| 화면번호 | 화면명 | 비고 |
|---------|--------|------|
| 14 | QR포스터상세설정 | 13번에서 분리 필요 |
| 20 | 이벤트참여 | 고객용 화면 |
| 21 | 참여완료 | 고객용 화면 |
## 특이사항
1. **06-AI이벤트유형추천.html**: 이 파일이 최근 수정되었고 localStorage 저장 로직이 추가됨. 설계서의 어느 단계에 해당하는지 확인 필요.
2. **07-AI이벤트상품추천.html**: "AI경품추천"에서 "AI이벤트상품추천"으로 용어 통일 필요.
3. **13-QR포스터생성선택.html + 14-QR포스터상세설정.html**: 현재 하나의 파일(12-QR포스터생성.html)을 두 개로 분리 필요.