mirror of
https://github.com/ktds-dg0501/kt-event-marketing-fe.git
synced 2025-12-06 06:16:24 +00:00
event 기능 추가
This commit is contained in:
parent
4df7ba0697
commit
01d91e194a
@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
@ -18,24 +18,27 @@ import {
|
|||||||
Checkbox,
|
Checkbox,
|
||||||
FormControlLabel,
|
FormControlLabel,
|
||||||
FormGroup,
|
FormGroup,
|
||||||
|
Link,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import { ArrowBack, Visibility, VisibilityOff } from '@mui/icons-material';
|
import { ArrowBack, Visibility, VisibilityOff } from '@mui/icons-material';
|
||||||
import { useState } from 'react';
|
import { useState, useEffect, Suspense } from 'react';
|
||||||
import { useUIStore } from '@/stores/uiStore';
|
import { useUIStore } from '@/stores/uiStore';
|
||||||
import { useAuthStore } from '@/stores/authStore';
|
import { useAuthStore } from '@/stores/authStore';
|
||||||
|
|
||||||
// 각 단계별 유효성 검사 스키마
|
// 각 단계별 유효성 검사 스키마
|
||||||
const step1Schema = z.object({
|
const step1Schema = z
|
||||||
email: z.string().email('올바른 이메일 형식이 아닙니다'),
|
.object({
|
||||||
password: z
|
email: z.string().email('올바른 이메일 형식이 아닙니다'),
|
||||||
.string()
|
password: z
|
||||||
.min(8, '비밀번호는 8자 이상이어야 합니다')
|
.string()
|
||||||
.regex(/^(?=.*[A-Za-z])(?=.*\d)/, '영문과 숫자를 포함해야 합니다'),
|
.min(8, '비밀번호는 8자 이상이어야 합니다')
|
||||||
confirmPassword: z.string(),
|
.regex(/^(?=.*[A-Za-z])(?=.*\d)/, '영문과 숫자를 포함해야 합니다'),
|
||||||
}).refine((data) => data.password === data.confirmPassword, {
|
confirmPassword: z.string(),
|
||||||
message: '비밀번호가 일치하지 않습니다',
|
})
|
||||||
path: ['confirmPassword'],
|
.refine((data) => data.password === data.confirmPassword, {
|
||||||
});
|
message: '비밀번호가 일치하지 않습니다',
|
||||||
|
path: ['confirmPassword'],
|
||||||
|
});
|
||||||
|
|
||||||
const step2Schema = z.object({
|
const step2Schema = z.object({
|
||||||
name: z.string().min(2, '이름은 2자 이상이어야 합니다'),
|
name: z.string().min(2, '이름은 2자 이상이어야 합니다'),
|
||||||
@ -47,6 +50,10 @@ const step2Schema = z.object({
|
|||||||
|
|
||||||
const step3Schema = z.object({
|
const step3Schema = z.object({
|
||||||
businessName: z.string().min(2, '상호명은 2자 이상이어야 합니다'),
|
businessName: z.string().min(2, '상호명은 2자 이상이어야 합니다'),
|
||||||
|
businessNumber: z
|
||||||
|
.string()
|
||||||
|
.min(1, '사업자 번호를 입력해주세요')
|
||||||
|
.regex(/^\d{3}-\d{2}-\d{5}$/, '올바른 사업자 번호 형식이 아닙니다 (123-45-67890)'),
|
||||||
businessType: z.string().min(1, '업종을 선택해주세요'),
|
businessType: z.string().min(1, '업종을 선택해주세요'),
|
||||||
businessLocation: z.string().optional(),
|
businessLocation: z.string().optional(),
|
||||||
agreeTerms: z.boolean().refine((val) => val === true, {
|
agreeTerms: z.boolean().refine((val) => val === true, {
|
||||||
@ -64,20 +71,34 @@ type Step3Data = z.infer<typeof step3Schema>;
|
|||||||
|
|
||||||
type RegisterData = Step1Data & Step2Data & Step3Data;
|
type RegisterData = Step1Data & Step2Data & Step3Data;
|
||||||
|
|
||||||
type Step = '계정정보' | '개인정보' | '사업장정보';
|
function RegisterForm() {
|
||||||
|
|
||||||
export default function RegisterPage() {
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
const { showToast, setLoading } = useUIStore();
|
const { showToast, setLoading } = useUIStore();
|
||||||
const { login } = useAuthStore();
|
const { login } = useAuthStore();
|
||||||
|
|
||||||
const [currentStep, setCurrentStep] = useState<Step>('계정정보');
|
// URL 쿼리에서 step 파라미터 읽기 (기본값: 1)
|
||||||
|
const stepParam = searchParams.get('step');
|
||||||
|
const initialStep = stepParam ? parseInt(stepParam) : 1;
|
||||||
|
|
||||||
|
const [currentStep, setCurrentStep] = useState<number>(initialStep);
|
||||||
const [formData, setFormData] = useState<Partial<RegisterData>>({
|
const [formData, setFormData] = useState<Partial<RegisterData>>({
|
||||||
agreeMarketing: false,
|
agreeMarketing: false,
|
||||||
});
|
});
|
||||||
const [showPassword, setShowPassword] = useState(false);
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
|
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
|
||||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||||
|
const [isVerifyingBusinessNumber, setIsVerifyingBusinessNumber] = useState(false);
|
||||||
|
const [isBusinessNumberVerified, setIsBusinessNumberVerified] = useState(false);
|
||||||
|
|
||||||
|
// step 변경 시 URL 업데이트
|
||||||
|
useEffect(() => {
|
||||||
|
const newStep = Math.max(1, Math.min(3, currentStep));
|
||||||
|
if (newStep !== currentStep) {
|
||||||
|
setCurrentStep(newStep);
|
||||||
|
}
|
||||||
|
router.replace(`/register?step=${newStep}`, { scroll: false });
|
||||||
|
}, [currentStep, router]);
|
||||||
|
|
||||||
const formatPhoneNumber = (value: string) => {
|
const formatPhoneNumber = (value: string) => {
|
||||||
const numbers = value.replace(/[^\d]/g, '');
|
const numbers = value.replace(/[^\d]/g, '');
|
||||||
@ -86,19 +107,56 @@ export default function RegisterPage() {
|
|||||||
return `${numbers.slice(0, 3)}-${numbers.slice(3, 7)}-${numbers.slice(7, 11)}`;
|
return `${numbers.slice(0, 3)}-${numbers.slice(3, 7)}-${numbers.slice(7, 11)}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const steps: Step[] = ['계정정보', '개인정보', '사업장정보'];
|
const formatBusinessNumber = (value: string) => {
|
||||||
const stepIndex = steps.indexOf(currentStep) + 1;
|
const numbers = value.replace(/[^\d]/g, '');
|
||||||
const progress = (stepIndex / steps.length) * 100;
|
if (numbers.length <= 3) return numbers;
|
||||||
|
if (numbers.length <= 5) return `${numbers.slice(0, 3)}-${numbers.slice(3)}`;
|
||||||
|
return `${numbers.slice(0, 3)}-${numbers.slice(3, 5)}-${numbers.slice(5, 10)}`;
|
||||||
|
};
|
||||||
|
|
||||||
const validateStep = (step: Step) => {
|
const handleVerifyBusinessNumber = async () => {
|
||||||
|
if (!formData.businessNumber) {
|
||||||
|
showToast('사업자 번호를 입력해주세요', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!/^\d{3}-\d{2}-\d{5}$/.test(formData.businessNumber)) {
|
||||||
|
showToast('올바른 사업자 번호 형식이 아닙니다', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsVerifyingBusinessNumber(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// TODO: API 연동 시 실제 사업자 번호 검증 API 호출
|
||||||
|
// const response = await axios.post(`${BUSINESS_HOST}/api/v1/business/verify`, {
|
||||||
|
// businessNumber: formData.businessNumber
|
||||||
|
// });
|
||||||
|
|
||||||
|
// 임시 처리: 2초 후 성공
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||||
|
|
||||||
|
setIsBusinessNumberVerified(true);
|
||||||
|
showToast('사업자 번호 인증이 완료되었습니다', 'success');
|
||||||
|
} catch {
|
||||||
|
setIsBusinessNumberVerified(false);
|
||||||
|
showToast('사업자 번호 인증에 실패했습니다', 'error');
|
||||||
|
} finally {
|
||||||
|
setIsVerifyingBusinessNumber(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const progress = (currentStep / 3) * 100;
|
||||||
|
|
||||||
|
const validateStep = (step: number) => {
|
||||||
setErrors({});
|
setErrors({});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (step === '계정정보') {
|
if (step === 1) {
|
||||||
step1Schema.parse(formData);
|
step1Schema.parse(formData);
|
||||||
} else if (step === '개인정보') {
|
} else if (step === 2) {
|
||||||
step2Schema.parse(formData);
|
step2Schema.parse(formData);
|
||||||
} else if (step === '사업장정보') {
|
} else if (step === 3) {
|
||||||
step3Schema.parse(formData);
|
step3Schema.parse(formData);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@ -111,6 +169,12 @@ export default function RegisterPage() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
setErrors(newErrors);
|
setErrors(newErrors);
|
||||||
|
|
||||||
|
// 첫 번째 에러 메시지를 Toast로 표시
|
||||||
|
const firstError = error.errors[0];
|
||||||
|
if (firstError) {
|
||||||
|
showToast(firstError.message, 'error');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -121,27 +185,23 @@ export default function RegisterPage() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentStep === '계정정보') {
|
if (currentStep < 3) {
|
||||||
setCurrentStep('개인정보');
|
setCurrentStep(currentStep + 1);
|
||||||
} else if (currentStep === '개인정보') {
|
} else {
|
||||||
setCurrentStep('사업장정보');
|
|
||||||
} else if (currentStep === '사업장정보') {
|
|
||||||
handleSubmit();
|
handleSubmit();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleBack = () => {
|
const handleBack = () => {
|
||||||
if (currentStep === '개인정보') {
|
if (currentStep > 1) {
|
||||||
setCurrentStep('계정정보');
|
setCurrentStep(currentStep - 1);
|
||||||
} else if (currentStep === '사업장정보') {
|
|
||||||
setCurrentStep('개인정보');
|
|
||||||
} else {
|
} else {
|
||||||
router.back();
|
router.back();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
if (!validateStep('사업장정보')) {
|
if (!validateStep(3)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -152,7 +212,7 @@ export default function RegisterPage() {
|
|||||||
// const response = await axios.post(`${USER_HOST}/api/v1/auth/register`, formData);
|
// const response = await axios.post(`${USER_HOST}/api/v1/auth/register`, formData);
|
||||||
|
|
||||||
// 임시 처리
|
// 임시 처리
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
|
|
||||||
const mockUser = {
|
const mockUser = {
|
||||||
id: '1',
|
id: '1',
|
||||||
@ -179,326 +239,424 @@ export default function RegisterPage() {
|
|||||||
minHeight: '100vh',
|
minHeight: '100vh',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
bgcolor: 'background.default',
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
px: 3,
|
||||||
|
py: 8,
|
||||||
|
background: 'linear-gradient(135deg, #FFF 0%, #F5F5F5 100%)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Header */}
|
<Paper
|
||||||
<Box
|
elevation={0}
|
||||||
sx={{
|
sx={{
|
||||||
p: 2,
|
width: '100%',
|
||||||
display: 'flex',
|
maxWidth: 440,
|
||||||
alignItems: 'center',
|
p: { xs: 4, sm: 6 },
|
||||||
borderBottom: 1,
|
borderRadius: 3,
|
||||||
borderColor: 'divider',
|
boxShadow: '0 8px 32px rgba(0,0,0,0.08)',
|
||||||
bgcolor: 'background.paper',
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<IconButton onClick={handleBack} edge="start">
|
{/* Header with back button */}
|
||||||
<ArrowBack />
|
<Box sx={{ display: 'flex', alignItems: 'center', mb: 4 }}>
|
||||||
</IconButton>
|
<IconButton onClick={handleBack} edge="start" sx={{ mr: 2 }}>
|
||||||
<Typography variant="h6" sx={{ ml: 2, fontWeight: 600 }}>
|
<ArrowBack />
|
||||||
회원가입
|
</IconButton>
|
||||||
</Typography>
|
<Typography variant="h6" sx={{ fontWeight: 600 }}>
|
||||||
</Box>
|
회원가입
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
{/* Progress Bar */}
|
{/* Progress Bar */}
|
||||||
<Box sx={{ px: 3, pt: 3, pb: 2 }}>
|
<Box sx={{ mb: 4 }}>
|
||||||
<LinearProgress variant="determinate" value={progress} sx={{ height: 6, borderRadius: 3 }} />
|
<LinearProgress
|
||||||
<Typography variant="body2" color="text.secondary" sx={{ mt: 1, textAlign: 'center' }}>
|
variant="determinate"
|
||||||
{stepIndex}/3 단계
|
value={progress}
|
||||||
</Typography>
|
sx={{ height: 6, borderRadius: 3 }}
|
||||||
</Box>
|
/>
|
||||||
|
<Typography variant="body2" color="text.secondary" sx={{ mt: 1, textAlign: 'center' }}>
|
||||||
|
{currentStep}/3 단계
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
{/* Funnel Content */}
|
{/* Step 1: 계정정보 */}
|
||||||
<Box sx={{ flex: 1, px: 3, py: 4 }}>
|
{currentStep === 1 && (
|
||||||
{currentStep === '계정정보' && (
|
<Box>
|
||||||
<Paper elevation={0} sx={{ p: 4 }}>
|
<Typography variant="h5" sx={{ fontWeight: 700, mb: 1 }}>
|
||||||
<Typography variant="h5" sx={{ fontWeight: 700, mb: 1 }}>
|
계정 정보를 입력해주세요
|
||||||
계정 정보를 입력해주세요
|
</Typography>
|
||||||
</Typography>
|
<Typography variant="body2" color="text.secondary" sx={{ mb: 4 }}>
|
||||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 4 }}>
|
로그인에 사용할 이메일과 비밀번호를 설정합니다
|
||||||
로그인에 사용할 이메일과 비밀번호를 설정합니다
|
</Typography>
|
||||||
</Typography>
|
|
||||||
|
|
||||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
|
||||||
<TextField
|
<TextField
|
||||||
fullWidth
|
fullWidth
|
||||||
label="이메일"
|
label="이메일"
|
||||||
type="email"
|
type="email"
|
||||||
placeholder="example@email.com"
|
placeholder="example@email.com"
|
||||||
value={formData.email || ''}
|
value={formData.email || ''}
|
||||||
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
|
||||||
error={!!errors.email}
|
error={!!errors.email}
|
||||||
helperText={errors.email || '이메일 주소로 로그인합니다'}
|
helperText={errors.email || '이메일 주소로 로그인합니다'}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
fullWidth
|
fullWidth
|
||||||
label="비밀번호"
|
label="비밀번호"
|
||||||
type={showPassword ? 'text' : 'password'}
|
type={showPassword ? 'text' : 'password'}
|
||||||
placeholder="8자 이상, 영문+숫자 조합"
|
placeholder="8자 이상, 영문+숫자 조합"
|
||||||
value={formData.password || ''}
|
value={formData.password || ''}
|
||||||
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
|
||||||
error={!!errors.password}
|
error={!!errors.password}
|
||||||
helperText={errors.password}
|
helperText={errors.password}
|
||||||
required
|
required
|
||||||
InputProps={{
|
InputProps={{
|
||||||
endAdornment: (
|
endAdornment: (
|
||||||
<InputAdornment position="end">
|
<InputAdornment position="end">
|
||||||
<IconButton onClick={() => setShowPassword(!showPassword)} edge="end">
|
<IconButton onClick={() => setShowPassword(!showPassword)} edge="end">
|
||||||
{showPassword ? <VisibilityOff /> : <Visibility />}
|
{showPassword ? <VisibilityOff /> : <Visibility />}
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</InputAdornment>
|
</InputAdornment>
|
||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
fullWidth
|
fullWidth
|
||||||
label="비밀번호 확인"
|
label="비밀번호 확인"
|
||||||
type={showConfirmPassword ? 'text' : 'password'}
|
type={showConfirmPassword ? 'text' : 'password'}
|
||||||
placeholder="비밀번호를 다시 입력하세요"
|
placeholder="비밀번호를 다시 입력하세요"
|
||||||
value={formData.confirmPassword || ''}
|
value={formData.confirmPassword || ''}
|
||||||
onChange={(e) => setFormData({ ...formData, confirmPassword: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, confirmPassword: e.target.value })}
|
||||||
error={!!errors.confirmPassword}
|
error={!!errors.confirmPassword}
|
||||||
helperText={errors.confirmPassword}
|
helperText={errors.confirmPassword}
|
||||||
required
|
required
|
||||||
InputProps={{
|
InputProps={{
|
||||||
endAdornment: (
|
endAdornment: (
|
||||||
<InputAdornment position="end">
|
<InputAdornment position="end">
|
||||||
<IconButton onClick={() => setShowConfirmPassword(!showConfirmPassword)} edge="end">
|
<IconButton
|
||||||
{showConfirmPassword ? <VisibilityOff /> : <Visibility />}
|
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
|
||||||
</IconButton>
|
edge="end"
|
||||||
</InputAdornment>
|
>
|
||||||
),
|
{showConfirmPassword ? <VisibilityOff /> : <Visibility />}
|
||||||
}}
|
</IconButton>
|
||||||
/>
|
</InputAdornment>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
fullWidth
|
||||||
|
variant="contained"
|
||||||
|
size="large"
|
||||||
|
onClick={handleNext}
|
||||||
|
sx={{ mt: 2, py: 1.5, fontSize: 16, fontWeight: 600 }}
|
||||||
|
>
|
||||||
|
다음
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Box sx={{ textAlign: 'center', mt: 2 }}>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
이미 계정이 있으신가요?{' '}
|
||||||
|
<Link
|
||||||
|
href="/login"
|
||||||
|
underline="hover"
|
||||||
|
sx={{ color: 'primary.main', fontWeight: 600 }}
|
||||||
|
>
|
||||||
|
로그인
|
||||||
|
</Link>
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Step 2: 개인정보 */}
|
||||||
|
{currentStep === 2 && (
|
||||||
|
<Box>
|
||||||
|
<Typography variant="h5" sx={{ fontWeight: 700, mb: 1 }}>
|
||||||
|
개인 정보를 입력해주세요
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary" sx={{ mb: 4 }}>
|
||||||
|
서비스 이용을 위한 기본 정보입니다
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label="이름"
|
||||||
|
placeholder="홍길동"
|
||||||
|
value={formData.name || ''}
|
||||||
|
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||||
|
error={!!errors.name}
|
||||||
|
helperText={errors.name}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label="휴대폰 번호"
|
||||||
|
placeholder="010-1234-5678"
|
||||||
|
value={formData.phone || ''}
|
||||||
|
onChange={(e) => {
|
||||||
|
const formatted = formatPhoneNumber(e.target.value);
|
||||||
|
setFormData({ ...formData, phone: formatted });
|
||||||
|
}}
|
||||||
|
error={!!errors.phone}
|
||||||
|
helperText={errors.phone}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Box sx={{ display: 'flex', gap: 2, mt: 2 }}>
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
size="large"
|
||||||
|
onClick={handleBack}
|
||||||
|
sx={{ flex: 1, py: 1.5, fontSize: 16, fontWeight: 600 }}
|
||||||
|
>
|
||||||
|
이전
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
fullWidth
|
|
||||||
variant="contained"
|
variant="contained"
|
||||||
size="large"
|
size="large"
|
||||||
onClick={handleNext}
|
onClick={handleNext}
|
||||||
sx={{ mt: 2, py: 1.5 }}
|
sx={{ flex: 1, py: 1.5, fontSize: 16, fontWeight: 600 }}
|
||||||
>
|
>
|
||||||
다음
|
다음
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
</Paper>
|
</Box>
|
||||||
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{currentStep === '개인정보' && (
|
{/* Step 3: 사업장정보 */}
|
||||||
<Paper elevation={0} sx={{ p: 4 }}>
|
{currentStep === 3 && (
|
||||||
<Typography variant="h5" sx={{ fontWeight: 700, mb: 1 }}>
|
<Box>
|
||||||
개인 정보를 입력해주세요
|
<Typography variant="h5" sx={{ fontWeight: 700, mb: 1 }}>
|
||||||
</Typography>
|
사업장 정보를 입력해주세요
|
||||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 4 }}>
|
</Typography>
|
||||||
서비스 이용을 위한 기본 정보입니다
|
<Typography variant="body2" color="text.secondary" sx={{ mb: 4 }}>
|
||||||
</Typography>
|
맞춤형 이벤트 추천을 위한 정보입니다
|
||||||
|
</Typography>
|
||||||
|
|
||||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label="상호명"
|
||||||
|
placeholder="홍길동 고깃집"
|
||||||
|
value={formData.businessName || ''}
|
||||||
|
onChange={(e) => setFormData({ ...formData, businessName: e.target.value })}
|
||||||
|
error={!!errors.businessName}
|
||||||
|
helperText={errors.businessName}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||||
<TextField
|
<TextField
|
||||||
fullWidth
|
fullWidth
|
||||||
label="이름"
|
label="사업자 번호"
|
||||||
placeholder="홍길동"
|
placeholder="123-45-67890"
|
||||||
value={formData.name || ''}
|
value={formData.businessNumber || ''}
|
||||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
|
||||||
error={!!errors.name}
|
|
||||||
helperText={errors.name}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TextField
|
|
||||||
fullWidth
|
|
||||||
label="휴대폰 번호"
|
|
||||||
placeholder="010-1234-5678"
|
|
||||||
value={formData.phone || ''}
|
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const formatted = formatPhoneNumber(e.target.value);
|
const formatted = formatBusinessNumber(e.target.value);
|
||||||
setFormData({ ...formData, phone: formatted });
|
setFormData({ ...formData, businessNumber: formatted });
|
||||||
|
setIsBusinessNumberVerified(false);
|
||||||
}}
|
}}
|
||||||
error={!!errors.phone}
|
error={!!errors.businessNumber}
|
||||||
helperText={errors.phone}
|
helperText={
|
||||||
|
errors.businessNumber ||
|
||||||
|
(isBusinessNumberVerified
|
||||||
|
? '✓ 인증 완료'
|
||||||
|
: '사업자 번호를 입력하고 인증해주세요')
|
||||||
|
}
|
||||||
required
|
required
|
||||||
|
sx={{
|
||||||
|
'& .MuiFormHelperText-root': {
|
||||||
|
color: isBusinessNumberVerified ? 'success.main' : undefined,
|
||||||
|
},
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
<Button
|
||||||
<Box sx={{ display: 'flex', gap: 2, mt: 2 }}>
|
variant="outlined"
|
||||||
<Button
|
size="large"
|
||||||
variant="outlined"
|
onClick={handleVerifyBusinessNumber}
|
||||||
size="large"
|
disabled={isVerifyingBusinessNumber || isBusinessNumberVerified}
|
||||||
onClick={handleBack}
|
sx={{
|
||||||
sx={{ flex: 1, py: 1.5 }}
|
minWidth: '90px',
|
||||||
>
|
py: 1.5,
|
||||||
이전
|
fontSize: 14,
|
||||||
</Button>
|
fontWeight: 600,
|
||||||
<Button
|
}}
|
||||||
variant="contained"
|
>
|
||||||
size="large"
|
{isVerifyingBusinessNumber
|
||||||
onClick={handleNext}
|
? '인증중'
|
||||||
sx={{ flex: 1, py: 1.5 }}
|
: isBusinessNumberVerified
|
||||||
>
|
? '완료'
|
||||||
다음
|
: '인증'}
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
|
||||||
</Box>
|
</Box>
|
||||||
</Paper>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{currentStep === '사업장정보' && (
|
<FormControl fullWidth required error={!!errors.businessType}>
|
||||||
<Paper elevation={0} sx={{ p: 4 }}>
|
<InputLabel>업종</InputLabel>
|
||||||
<Typography variant="h5" sx={{ fontWeight: 700, mb: 1 }}>
|
<Select
|
||||||
사업장 정보를 입력해주세요
|
value={formData.businessType || ''}
|
||||||
</Typography>
|
onChange={(e) => setFormData({ ...formData, businessType: e.target.value })}
|
||||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 4 }}>
|
label="업종"
|
||||||
맞춤형 이벤트 추천을 위한 정보입니다
|
>
|
||||||
</Typography>
|
<MenuItem value="">업종을 선택하세요</MenuItem>
|
||||||
|
<MenuItem value="restaurant">음식점</MenuItem>
|
||||||
|
<MenuItem value="cafe">카페/베이커리</MenuItem>
|
||||||
|
<MenuItem value="retail">소매/편의점</MenuItem>
|
||||||
|
<MenuItem value="beauty">미용/뷰티</MenuItem>
|
||||||
|
<MenuItem value="fitness">헬스/피트니스</MenuItem>
|
||||||
|
<MenuItem value="education">학원/교육</MenuItem>
|
||||||
|
<MenuItem value="service">서비스업</MenuItem>
|
||||||
|
<MenuItem value="other">기타</MenuItem>
|
||||||
|
</Select>
|
||||||
|
{errors.businessType && (
|
||||||
|
<Typography variant="caption" color="error" sx={{ mt: 0.5, ml: 2 }}>
|
||||||
|
{errors.businessType}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
|
<TextField
|
||||||
<TextField
|
fullWidth
|
||||||
fullWidth
|
label="주요 지역"
|
||||||
label="상호명"
|
placeholder="예: 강남구"
|
||||||
placeholder="홍길동 고깃집"
|
value={formData.businessLocation || ''}
|
||||||
value={formData.businessName || ''}
|
onChange={(e) => setFormData({ ...formData, businessLocation: e.target.value })}
|
||||||
onChange={(e) => setFormData({ ...formData, businessName: e.target.value })}
|
helperText="선택 사항입니다"
|
||||||
error={!!errors.businessName}
|
/>
|
||||||
helperText={errors.businessName}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormControl fullWidth required error={!!errors.businessType}>
|
<Box sx={{ mt: 1 }}>
|
||||||
<InputLabel>업종</InputLabel>
|
<FormGroup>
|
||||||
<Select
|
<FormControlLabel
|
||||||
value={formData.businessType || ''}
|
control={
|
||||||
onChange={(e) => setFormData({ ...formData, businessType: e.target.value })}
|
<Checkbox
|
||||||
label="업종"
|
checked={formData.agreeTerms === true && formData.agreePrivacy === true}
|
||||||
>
|
onChange={(e) => {
|
||||||
<MenuItem value="">업종을 선택하세요</MenuItem>
|
const checked = e.target.checked;
|
||||||
<MenuItem value="restaurant">음식점</MenuItem>
|
setFormData({
|
||||||
<MenuItem value="cafe">카페/베이커리</MenuItem>
|
...formData,
|
||||||
<MenuItem value="retail">소매/편의점</MenuItem>
|
agreeTerms: checked,
|
||||||
<MenuItem value="beauty">미용/뷰티</MenuItem>
|
agreePrivacy: checked,
|
||||||
<MenuItem value="fitness">헬스/피트니스</MenuItem>
|
agreeMarketing: checked,
|
||||||
<MenuItem value="education">학원/교육</MenuItem>
|
});
|
||||||
<MenuItem value="service">서비스업</MenuItem>
|
}}
|
||||||
<MenuItem value="other">기타</MenuItem>
|
/>
|
||||||
</Select>
|
}
|
||||||
{errors.businessType && (
|
label={
|
||||||
<Typography variant="caption" color="error" sx={{ mt: 0.5, ml: 2 }}>
|
<Typography variant="body2" sx={{ fontWeight: 600 }}>
|
||||||
{errors.businessType}
|
전체 동의
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
}
|
||||||
</FormControl>
|
/>
|
||||||
|
<Box sx={{ ml: 3, mt: 1, display: 'flex', flexDirection: 'column', gap: 0.5 }}>
|
||||||
<TextField
|
|
||||||
fullWidth
|
|
||||||
label="주요 지역"
|
|
||||||
placeholder="예: 강남구"
|
|
||||||
value={formData.businessLocation || ''}
|
|
||||||
onChange={(e) => setFormData({ ...formData, businessLocation: e.target.value })}
|
|
||||||
helperText="선택 사항입니다"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Box sx={{ mt: 2 }}>
|
|
||||||
<FormGroup>
|
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
control={
|
control={
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={formData.agreeTerms === true && formData.agreePrivacy === true}
|
checked={formData.agreeTerms || false}
|
||||||
onChange={(e) => {
|
onChange={(e) =>
|
||||||
const checked = e.target.checked;
|
setFormData({ ...formData, agreeTerms: e.target.checked })
|
||||||
setFormData({
|
}
|
||||||
...formData,
|
|
||||||
agreeTerms: checked,
|
|
||||||
agreePrivacy: checked,
|
|
||||||
agreeMarketing: checked,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
label={
|
label={
|
||||||
<Typography variant="body2" sx={{ fontWeight: 600 }}>
|
<Typography
|
||||||
전체 동의
|
variant="body2"
|
||||||
|
color={errors.agreeTerms ? 'error' : 'text.primary'}
|
||||||
|
>
|
||||||
|
[필수] 이용약관 동의
|
||||||
</Typography>
|
</Typography>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Box sx={{ ml: 3, mt: 1, display: 'flex', flexDirection: 'column', gap: 1 }}>
|
<FormControlLabel
|
||||||
<FormControlLabel
|
control={
|
||||||
control={
|
<Checkbox
|
||||||
<Checkbox
|
checked={formData.agreePrivacy || false}
|
||||||
checked={formData.agreeTerms || false}
|
onChange={(e) =>
|
||||||
onChange={(e) =>
|
setFormData({ ...formData, agreePrivacy: e.target.checked })
|
||||||
setFormData({ ...formData, agreeTerms: e.target.checked })
|
}
|
||||||
}
|
/>
|
||||||
/>
|
}
|
||||||
}
|
label={
|
||||||
label={
|
<Typography
|
||||||
<Typography variant="body2" color={errors.agreeTerms ? 'error' : 'text.primary'}>
|
variant="body2"
|
||||||
[필수] 이용약관 동의
|
color={errors.agreePrivacy ? 'error' : 'text.primary'}
|
||||||
</Typography>
|
>
|
||||||
}
|
[필수] 개인정보 처리방침 동의
|
||||||
/>
|
</Typography>
|
||||||
<FormControlLabel
|
}
|
||||||
control={
|
/>
|
||||||
<Checkbox
|
<FormControlLabel
|
||||||
checked={formData.agreePrivacy || false}
|
control={
|
||||||
onChange={(e) =>
|
<Checkbox
|
||||||
setFormData({ ...formData, agreePrivacy: e.target.checked })
|
checked={formData.agreeMarketing || false}
|
||||||
}
|
onChange={(e) =>
|
||||||
/>
|
setFormData({ ...formData, agreeMarketing: e.target.checked })
|
||||||
}
|
}
|
||||||
label={
|
/>
|
||||||
<Typography variant="body2" color={errors.agreePrivacy ? 'error' : 'text.primary'}>
|
}
|
||||||
[필수] 개인정보 처리방침 동의
|
label={<Typography variant="body2">[선택] 마케팅 정보 수신 동의</Typography>}
|
||||||
</Typography>
|
/>
|
||||||
}
|
</Box>
|
||||||
/>
|
</FormGroup>
|
||||||
<FormControlLabel
|
{(errors.agreeTerms || errors.agreePrivacy) && (
|
||||||
control={
|
<Typography
|
||||||
<Checkbox
|
variant="caption"
|
||||||
checked={formData.agreeMarketing || false}
|
color="error"
|
||||||
onChange={(e) =>
|
sx={{ mt: 1, display: 'block', ml: 2 }}
|
||||||
setFormData({ ...formData, agreeMarketing: e.target.checked })
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
label={
|
|
||||||
<Typography variant="body2">
|
|
||||||
[선택] 마케팅 정보 수신 동의
|
|
||||||
</Typography>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
</FormGroup>
|
|
||||||
{(errors.agreeTerms || errors.agreePrivacy) && (
|
|
||||||
<Typography variant="caption" color="error" sx={{ mt: 1, display: 'block' }}>
|
|
||||||
필수 약관에 동의해주세요
|
|
||||||
</Typography>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Box sx={{ display: 'flex', gap: 2, mt: 2 }}>
|
|
||||||
<Button
|
|
||||||
variant="outlined"
|
|
||||||
size="large"
|
|
||||||
onClick={handleBack}
|
|
||||||
sx={{ flex: 1, py: 1.5 }}
|
|
||||||
>
|
>
|
||||||
이전
|
필수 약관에 동의해주세요
|
||||||
</Button>
|
</Typography>
|
||||||
<Button
|
)}
|
||||||
variant="contained"
|
|
||||||
size="large"
|
|
||||||
onClick={handleNext}
|
|
||||||
sx={{ flex: 1, py: 1.5 }}
|
|
||||||
>
|
|
||||||
가입완료
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
</Box>
|
||||||
</Paper>
|
|
||||||
|
<Box sx={{ display: 'flex', gap: 2, mt: 2 }}>
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
size="large"
|
||||||
|
onClick={handleBack}
|
||||||
|
sx={{ flex: 1, py: 1.5, fontSize: 16, fontWeight: 600 }}
|
||||||
|
>
|
||||||
|
이전
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
size="large"
|
||||||
|
onClick={handleNext}
|
||||||
|
sx={{ flex: 1, py: 1.5, fontSize: 16, fontWeight: 600 }}
|
||||||
|
>
|
||||||
|
가입완료
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Paper>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default function RegisterPage() {
|
||||||
|
return (
|
||||||
|
<Suspense
|
||||||
|
fallback={
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
minHeight: '100vh',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography>Loading...</Typography>
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<RegisterForm />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
100
src/app/(main)/events/create/page.tsx
Normal file
100
src/app/(main)/events/create/page.tsx
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useFunnel } from '@use-funnel/browser';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import ObjectiveStep from './steps/ObjectiveStep';
|
||||||
|
import RecommendationStep from './steps/RecommendationStep';
|
||||||
|
import ChannelStep from './steps/ChannelStep';
|
||||||
|
import ApprovalStep from './steps/ApprovalStep';
|
||||||
|
|
||||||
|
// 이벤트 생성 데이터 타입
|
||||||
|
export type EventObjective = 'new_customer' | 'revisit' | 'sales' | 'awareness';
|
||||||
|
export type BudgetLevel = 'low' | 'medium' | 'high';
|
||||||
|
export type EventMethod = 'online' | 'offline';
|
||||||
|
|
||||||
|
export interface EventData {
|
||||||
|
objective?: EventObjective;
|
||||||
|
recommendation?: {
|
||||||
|
budget: BudgetLevel;
|
||||||
|
method: EventMethod;
|
||||||
|
title: string;
|
||||||
|
prize: string;
|
||||||
|
participationMethod: string;
|
||||||
|
expectedParticipants: number;
|
||||||
|
estimatedCost: number;
|
||||||
|
roi: number;
|
||||||
|
};
|
||||||
|
contentPreview?: {
|
||||||
|
imageStyle: string;
|
||||||
|
};
|
||||||
|
contentEdit?: {
|
||||||
|
title: string;
|
||||||
|
prize: string;
|
||||||
|
guide: string;
|
||||||
|
};
|
||||||
|
channels?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function EventCreatePage() {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const funnel = useFunnel<{
|
||||||
|
objective: EventData;
|
||||||
|
recommendation: EventData;
|
||||||
|
channel: EventData;
|
||||||
|
approval: EventData;
|
||||||
|
}>({
|
||||||
|
id: 'event-creation',
|
||||||
|
initial: {
|
||||||
|
step: 'objective',
|
||||||
|
context: {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleComplete = () => {
|
||||||
|
// 이벤트 생성 완료 후 대시보드로 이동
|
||||||
|
router.push('/');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<funnel.Render
|
||||||
|
objective={({ history }) => (
|
||||||
|
<ObjectiveStep
|
||||||
|
onNext={(objective) => {
|
||||||
|
history.push('recommendation', { objective });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
recommendation={({ context, history }) => (
|
||||||
|
<RecommendationStep
|
||||||
|
objective={context.objective}
|
||||||
|
onNext={(recommendation) => {
|
||||||
|
history.push('channel', { ...context, recommendation });
|
||||||
|
}}
|
||||||
|
onBack={() => {
|
||||||
|
history.go(-1);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
channel={({ context, history }) => (
|
||||||
|
<ChannelStep
|
||||||
|
onNext={(channels) => {
|
||||||
|
history.push('approval', { ...context, channels });
|
||||||
|
}}
|
||||||
|
onBack={() => {
|
||||||
|
history.go(-1);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
approval={({ context, history }) => (
|
||||||
|
<ApprovalStep
|
||||||
|
eventData={context}
|
||||||
|
onApprove={handleComplete}
|
||||||
|
onBack={() => {
|
||||||
|
history.go(-1);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
347
src/app/(main)/events/create/steps/ApprovalStep.tsx
Normal file
347
src/app/(main)/events/create/steps/ApprovalStep.tsx
Normal file
@ -0,0 +1,347 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Container,
|
||||||
|
Typography,
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
Button,
|
||||||
|
Checkbox,
|
||||||
|
FormControlLabel,
|
||||||
|
Chip,
|
||||||
|
Grid,
|
||||||
|
IconButton,
|
||||||
|
Dialog,
|
||||||
|
DialogTitle,
|
||||||
|
DialogContent,
|
||||||
|
DialogActions,
|
||||||
|
Link,
|
||||||
|
} from '@mui/material';
|
||||||
|
import { ArrowBack, CheckCircle, Edit, RocketLaunch, Save } from '@mui/icons-material';
|
||||||
|
import { EventData } from '../page';
|
||||||
|
|
||||||
|
interface ApprovalStepProps {
|
||||||
|
eventData: EventData;
|
||||||
|
onApprove: () => void;
|
||||||
|
onBack: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ApprovalStep({ eventData, onApprove, onBack }: ApprovalStepProps) {
|
||||||
|
const [agreeTerms, setAgreeTerms] = useState(false);
|
||||||
|
const [termsDialogOpen, setTermsDialogOpen] = useState(false);
|
||||||
|
const [successDialogOpen, setSuccessDialogOpen] = useState(false);
|
||||||
|
const [isDeploying, setIsDeploying] = useState(false);
|
||||||
|
|
||||||
|
const handleApprove = () => {
|
||||||
|
if (!agreeTerms) return;
|
||||||
|
|
||||||
|
setIsDeploying(true);
|
||||||
|
|
||||||
|
// 배포 시뮬레이션
|
||||||
|
setTimeout(() => {
|
||||||
|
setIsDeploying(false);
|
||||||
|
setSuccessDialogOpen(true);
|
||||||
|
}, 2000);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveDraft = () => {
|
||||||
|
// TODO: 임시저장 API 연동
|
||||||
|
alert('임시저장되었습니다');
|
||||||
|
};
|
||||||
|
|
||||||
|
const getChannelNames = (channels?: string[]) => {
|
||||||
|
const channelMap: Record<string, string> = {
|
||||||
|
uriTV: '우리동네TV',
|
||||||
|
ringoBiz: '링고비즈',
|
||||||
|
genieTV: '지니TV',
|
||||||
|
sns: 'SNS',
|
||||||
|
};
|
||||||
|
|
||||||
|
return channels?.map((ch) => channelMap[ch] || ch) || [];
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ minHeight: '100vh', bgcolor: 'background.default', pb: 10 }}>
|
||||||
|
<Container maxWidth="md" sx={{ pt: 4, pb: 4, px: { xs: 3, sm: 3, md: 4 } }}>
|
||||||
|
{/* Header */}
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 4 }}>
|
||||||
|
<IconButton onClick={onBack}>
|
||||||
|
<ArrowBack />
|
||||||
|
</IconButton>
|
||||||
|
<Typography variant="h5" sx={{ fontWeight: 700 }}>
|
||||||
|
최종 승인
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Title Section */}
|
||||||
|
<Box sx={{ textAlign: 'center', mb: 5 }}>
|
||||||
|
<CheckCircle sx={{ fontSize: 64, color: 'success.main', mb: 2 }} />
|
||||||
|
<Typography variant="h5" sx={{ fontWeight: 700, mb: 1 }}>
|
||||||
|
이벤트를 확인해주세요
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body1" color="text.secondary">
|
||||||
|
모든 정보를 검토한 후 배포하세요
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Event Summary Card */}
|
||||||
|
<Card elevation={0} sx={{ mb: 4, borderRadius: 3 }}>
|
||||||
|
<CardContent sx={{ p: 4 }}>
|
||||||
|
<Typography variant="h6" sx={{ fontWeight: 700, mb: 2 }}>
|
||||||
|
{eventData.recommendation?.title || '이벤트 제목'}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Box sx={{ display: 'flex', gap: 1, mb: 3 }}>
|
||||||
|
<Chip label="배포 대기" color="warning" size="small" />
|
||||||
|
<Chip label="AI 추천" color="info" size="small" />
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Grid container spacing={2} sx={{ pt: 2, borderTop: 1, borderColor: 'divider' }}>
|
||||||
|
<Grid item xs={6}>
|
||||||
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
이벤트 기간
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" sx={{ fontWeight: 600 }}>
|
||||||
|
2025.02.01 ~ 2025.02.28
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={6}>
|
||||||
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
목표 참여자
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" sx={{ fontWeight: 600 }}>
|
||||||
|
{eventData.recommendation?.expectedParticipants || 0}명
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={6}>
|
||||||
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
예상 비용
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" sx={{ fontWeight: 600 }}>
|
||||||
|
{eventData.recommendation?.estimatedCost.toLocaleString() || 0}원
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={6}>
|
||||||
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
예상 ROI
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" sx={{ fontWeight: 600, color: 'error.main' }}>
|
||||||
|
{eventData.recommendation?.roi || 0}%
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Event Details */}
|
||||||
|
<Typography variant="h6" sx={{ fontWeight: 700, mb: 3 }}>
|
||||||
|
이벤트 상세
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Card elevation={0} sx={{ mb: 2, borderRadius: 3 }}>
|
||||||
|
<CardContent sx={{ p: 3 }}>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 2 }}>
|
||||||
|
<Box sx={{ flex: 1 }}>
|
||||||
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
이벤트 제목
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body1" sx={{ fontWeight: 600 }}>
|
||||||
|
{eventData.recommendation?.title}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<IconButton size="small">
|
||||||
|
<Edit fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card elevation={0} sx={{ mb: 2, borderRadius: 3 }}>
|
||||||
|
<CardContent sx={{ p: 3 }}>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 2 }}>
|
||||||
|
<Box sx={{ flex: 1 }}>
|
||||||
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
경품
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body1" sx={{ fontWeight: 600 }}>
|
||||||
|
{eventData.recommendation?.prize}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<IconButton size="small">
|
||||||
|
<Edit fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card elevation={0} sx={{ mb: 2, borderRadius: 3 }}>
|
||||||
|
<CardContent sx={{ p: 3 }}>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 2 }}>
|
||||||
|
<Box sx={{ flex: 1 }}>
|
||||||
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
참여 방법
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body1" sx={{ fontWeight: 600 }}>
|
||||||
|
{eventData.recommendation?.participationMethod}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Distribution Channels */}
|
||||||
|
<Typography variant="h6" sx={{ fontWeight: 700, mb: 3, mt: 4 }}>
|
||||||
|
배포 채널
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Card elevation={0} sx={{ mb: 4, borderRadius: 3 }}>
|
||||||
|
<CardContent sx={{ p: 3 }}>
|
||||||
|
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1, mb: 2 }}>
|
||||||
|
{getChannelNames(eventData.channels).map((channel) => (
|
||||||
|
<Chip key={channel} label={channel} color="primary" variant="outlined" />
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
startIcon={<Edit />}
|
||||||
|
sx={{ color: 'primary.main' }}
|
||||||
|
>
|
||||||
|
채널 수정하기
|
||||||
|
</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Terms Agreement */}
|
||||||
|
<Card elevation={0} sx={{ mb: 5, borderRadius: 3, bgcolor: 'grey.50' }}>
|
||||||
|
<CardContent sx={{ p: 3 }}>
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={agreeTerms}
|
||||||
|
onChange={(e) => setAgreeTerms(e.target.checked)}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={
|
||||||
|
<Typography variant="body2">
|
||||||
|
이벤트 약관 및 개인정보 처리방침에 동의합니다{' '}
|
||||||
|
<span style={{ color: 'red' }}>(필수)</span>
|
||||||
|
</Typography>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Link
|
||||||
|
component="button"
|
||||||
|
variant="body2"
|
||||||
|
onClick={() => setTermsDialogOpen(true)}
|
||||||
|
sx={{ color: 'error.main', ml: 4, mt: 1 }}
|
||||||
|
>
|
||||||
|
약관 보기
|
||||||
|
</Link>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Action Buttons */}
|
||||||
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||||
|
<Button
|
||||||
|
fullWidth
|
||||||
|
variant="contained"
|
||||||
|
size="large"
|
||||||
|
disabled={!agreeTerms || isDeploying}
|
||||||
|
onClick={handleApprove}
|
||||||
|
startIcon={isDeploying ? null : <RocketLaunch />}
|
||||||
|
sx={{ py: 1.5 }}
|
||||||
|
>
|
||||||
|
{isDeploying ? '배포 중...' : '배포하기'}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
|
size="large"
|
||||||
|
onClick={handleSaveDraft}
|
||||||
|
startIcon={<Save />}
|
||||||
|
sx={{ py: 1.5 }}
|
||||||
|
>
|
||||||
|
임시저장
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Container>
|
||||||
|
|
||||||
|
{/* Terms Dialog */}
|
||||||
|
<Dialog
|
||||||
|
open={termsDialogOpen}
|
||||||
|
onClose={() => setTermsDialogOpen(false)}
|
||||||
|
maxWidth="sm"
|
||||||
|
fullWidth
|
||||||
|
>
|
||||||
|
<DialogTitle>이벤트 약관</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<Typography variant="h6" sx={{ mb: 2 }}>
|
||||||
|
제1조 (목적)
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" sx={{ mb: 3 }}>
|
||||||
|
본 약관은 KT AI 이벤트 마케팅 서비스를 통해 진행되는 이벤트의 참여 및 개인정보 처리에 관한
|
||||||
|
사항을 규정합니다.
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Typography variant="h6" sx={{ mb: 2 }}>
|
||||||
|
제2조 (개인정보 수집 및 이용)
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" sx={{ mb: 1 }}>
|
||||||
|
수집 항목: 이름, 전화번호, 이메일
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" sx={{ mb: 1 }}>
|
||||||
|
이용 목적: 이벤트 참여 확인 및 경품 제공
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" sx={{ mb: 3 }}>
|
||||||
|
보유 기간: 이벤트 종료 후 6개월
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Typography variant="h6" sx={{ mb: 2 }}>
|
||||||
|
제3조 (당첨자 발표)
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2">
|
||||||
|
당첨자는 이벤트 종료 후 7일 이내 개별 연락 드립니다.
|
||||||
|
</Typography>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={() => setTermsDialogOpen(false)} variant="contained">
|
||||||
|
확인
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
{/* Success Dialog */}
|
||||||
|
<Dialog
|
||||||
|
open={successDialogOpen}
|
||||||
|
onClose={() => {
|
||||||
|
setSuccessDialogOpen(false);
|
||||||
|
onApprove();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DialogContent sx={{ textAlign: 'center', py: 6, px: 4 }}>
|
||||||
|
<CheckCircle sx={{ fontSize: 80, color: 'success.main', mb: 2 }} />
|
||||||
|
<Typography variant="h5" sx={{ fontWeight: 700, mb: 1 }}>
|
||||||
|
배포 완료!
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body1" color="text.secondary" sx={{ mb: 4 }}>
|
||||||
|
이벤트가 성공적으로 배포되었습니다.
|
||||||
|
<br />
|
||||||
|
실시간으로 참여자를 확인할 수 있습니다.
|
||||||
|
</Typography>
|
||||||
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||||
|
<Button
|
||||||
|
fullWidth
|
||||||
|
variant="contained"
|
||||||
|
size="large"
|
||||||
|
onClick={() => {
|
||||||
|
setSuccessDialogOpen(false);
|
||||||
|
onApprove();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
대시보드로 이동
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
413
src/app/(main)/events/create/steps/ChannelStep.tsx
Normal file
413
src/app/(main)/events/create/steps/ChannelStep.tsx
Normal file
@ -0,0 +1,413 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Container,
|
||||||
|
Typography,
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
Button,
|
||||||
|
Checkbox,
|
||||||
|
FormControlLabel,
|
||||||
|
Select,
|
||||||
|
MenuItem,
|
||||||
|
TextField,
|
||||||
|
FormControl,
|
||||||
|
InputLabel,
|
||||||
|
IconButton,
|
||||||
|
} from '@mui/material';
|
||||||
|
import { ArrowBack } from '@mui/icons-material';
|
||||||
|
|
||||||
|
interface Channel {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
selected: boolean;
|
||||||
|
config?: Record<string, string | number>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ChannelStepProps {
|
||||||
|
onNext: (channels: string[]) => void;
|
||||||
|
onBack: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ChannelStep({ onNext, onBack }: ChannelStepProps) {
|
||||||
|
const [channels, setChannels] = useState<Channel[]>([
|
||||||
|
{ id: 'uriTV', name: '우리동네TV', selected: false, config: { radius: '500', time: 'evening' } },
|
||||||
|
{ id: 'ringoBiz', name: '링고비즈', selected: false, config: { phone: '010-1234-5678' } },
|
||||||
|
{ id: 'genieTV', name: '지니TV 광고', selected: false, config: { region: 'suwon', time: 'all', budget: '' } },
|
||||||
|
{ id: 'sns', name: 'SNS', selected: false, config: { instagram: 'true', naver: 'true', kakao: 'false', schedule: 'now' } },
|
||||||
|
]);
|
||||||
|
|
||||||
|
const handleChannelToggle = (channelId: string) => {
|
||||||
|
setChannels((prev) =>
|
||||||
|
prev.map((ch) => (ch.id === channelId ? { ...ch, selected: !ch.selected } : ch))
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConfigChange = (channelId: string, key: string, value: string) => {
|
||||||
|
setChannels((prev) =>
|
||||||
|
prev.map((ch) =>
|
||||||
|
ch.id === channelId ? { ...ch, config: { ...ch.config, [key]: value } } : ch
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getChannelConfig = (channelId: string, key: string): string => {
|
||||||
|
const channel = channels.find((ch) => ch.id === channelId);
|
||||||
|
return (channel?.config?.[key] as string) || '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const calculateSummary = () => {
|
||||||
|
let totalCost = 0;
|
||||||
|
let totalExposure = 0;
|
||||||
|
|
||||||
|
channels.forEach((ch) => {
|
||||||
|
if (!ch.selected) return;
|
||||||
|
|
||||||
|
if (ch.id === 'uriTV') {
|
||||||
|
totalCost += 80000;
|
||||||
|
totalExposure += 50000;
|
||||||
|
} else if (ch.id === 'ringoBiz') {
|
||||||
|
totalExposure += 30000;
|
||||||
|
} else if (ch.id === 'genieTV') {
|
||||||
|
const budget = parseInt(getChannelConfig('genieTV', 'budget')) || 0;
|
||||||
|
totalCost += budget;
|
||||||
|
totalExposure += Math.floor(budget / 100) * 1000;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { totalCost, totalExposure };
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNext = () => {
|
||||||
|
const selectedChannels = channels.filter((ch) => ch.selected).map((ch) => ch.id);
|
||||||
|
if (selectedChannels.length > 0) {
|
||||||
|
onNext(selectedChannels);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const { totalCost, totalExposure } = calculateSummary();
|
||||||
|
const selectedCount = channels.filter((ch) => ch.selected).length;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ minHeight: '100vh', bgcolor: 'background.default', pb: 10 }}>
|
||||||
|
<Container maxWidth="lg" sx={{ pt: 4, pb: 4, px: { xs: 3, sm: 3, md: 4 } }}>
|
||||||
|
{/* Header */}
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 4 }}>
|
||||||
|
<IconButton onClick={onBack}>
|
||||||
|
<ArrowBack />
|
||||||
|
</IconButton>
|
||||||
|
<Typography variant="h5" sx={{ fontWeight: 700 }}>
|
||||||
|
배포 채널 선택
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Typography variant="body2" color="text.secondary" sx={{ mb: 4, textAlign: 'center' }}>
|
||||||
|
(최소 1개 이상)
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
{/* 우리동네TV */}
|
||||||
|
<Card
|
||||||
|
elevation={0}
|
||||||
|
sx={{
|
||||||
|
mb: 3,
|
||||||
|
borderRadius: 3,
|
||||||
|
opacity: channels[0].selected ? 1 : 0.6,
|
||||||
|
transition: 'opacity 0.3s',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CardContent sx={{ p: 4 }}>
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={channels[0].selected}
|
||||||
|
onChange={() => handleChannelToggle('uriTV')}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label="우리동네TV"
|
||||||
|
sx={{ mb: channels[0].selected ? 2 : 0 }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{channels[0].selected && (
|
||||||
|
<Box sx={{ pl: 4, pt: 2, borderTop: 1, borderColor: 'divider' }}>
|
||||||
|
<FormControl fullWidth sx={{ mb: 2 }}>
|
||||||
|
<InputLabel>반경</InputLabel>
|
||||||
|
<Select
|
||||||
|
value={getChannelConfig('uriTV', 'radius')}
|
||||||
|
onChange={(e) => handleConfigChange('uriTV', 'radius', e.target.value)}
|
||||||
|
label="반경"
|
||||||
|
>
|
||||||
|
<MenuItem value="500">500m</MenuItem>
|
||||||
|
<MenuItem value="1000">1km</MenuItem>
|
||||||
|
<MenuItem value="2000">2km</MenuItem>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormControl fullWidth sx={{ mb: 2 }}>
|
||||||
|
<InputLabel>노출 시간대</InputLabel>
|
||||||
|
<Select
|
||||||
|
value={getChannelConfig('uriTV', 'time')}
|
||||||
|
onChange={(e) => handleConfigChange('uriTV', 'time', e.target.value)}
|
||||||
|
label="노출 시간대"
|
||||||
|
>
|
||||||
|
<MenuItem value="morning">아침 (7-12시)</MenuItem>
|
||||||
|
<MenuItem value="afternoon">점심 (12-17시)</MenuItem>
|
||||||
|
<MenuItem value="evening">저녁 (17-22시)</MenuItem>
|
||||||
|
<MenuItem value="all">전체</MenuItem>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
예상 노출: <strong>5만명</strong>
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
비용: <strong>8만원</strong>
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 링고비즈 */}
|
||||||
|
<Card
|
||||||
|
elevation={0}
|
||||||
|
sx={{
|
||||||
|
mb: 3,
|
||||||
|
borderRadius: 3,
|
||||||
|
opacity: channels[1].selected ? 1 : 0.6,
|
||||||
|
transition: 'opacity 0.3s',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CardContent sx={{ p: 4 }}>
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={channels[1].selected}
|
||||||
|
onChange={() => handleChannelToggle('ringoBiz')}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label="링고비즈"
|
||||||
|
sx={{ mb: channels[1].selected ? 2 : 0 }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{channels[1].selected && (
|
||||||
|
<Box sx={{ pl: 4, pt: 2, borderTop: 1, borderColor: 'divider' }}>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label="매장 전화번호"
|
||||||
|
value={getChannelConfig('ringoBiz', 'phone')}
|
||||||
|
InputProps={{ readOnly: true }}
|
||||||
|
sx={{ mb: 2 }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Typography variant="body2" color="text.secondary" sx={{ mb: 0.5 }}>
|
||||||
|
연결음 자동 업데이트
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
예상 노출: <strong>3만명</strong>
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
비용: <strong>무료</strong>
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 지니TV 광고 */}
|
||||||
|
<Card
|
||||||
|
elevation={0}
|
||||||
|
sx={{
|
||||||
|
mb: 3,
|
||||||
|
borderRadius: 3,
|
||||||
|
opacity: channels[2].selected ? 1 : 0.6,
|
||||||
|
transition: 'opacity 0.3s',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CardContent sx={{ p: 4 }}>
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={channels[2].selected}
|
||||||
|
onChange={() => handleChannelToggle('genieTV')}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label="지니TV 광고"
|
||||||
|
sx={{ mb: channels[2].selected ? 2 : 0 }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{channels[2].selected && (
|
||||||
|
<Box sx={{ pl: 4, pt: 2, borderTop: 1, borderColor: 'divider' }}>
|
||||||
|
<FormControl fullWidth sx={{ mb: 2 }}>
|
||||||
|
<InputLabel>지역</InputLabel>
|
||||||
|
<Select
|
||||||
|
value={getChannelConfig('genieTV', 'region')}
|
||||||
|
onChange={(e) => handleConfigChange('genieTV', 'region', e.target.value)}
|
||||||
|
label="지역"
|
||||||
|
>
|
||||||
|
<MenuItem value="suwon">수원</MenuItem>
|
||||||
|
<MenuItem value="seoul">서울</MenuItem>
|
||||||
|
<MenuItem value="busan">부산</MenuItem>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormControl fullWidth sx={{ mb: 2 }}>
|
||||||
|
<InputLabel>노출 시간대</InputLabel>
|
||||||
|
<Select
|
||||||
|
value={getChannelConfig('genieTV', 'time')}
|
||||||
|
onChange={(e) => handleConfigChange('genieTV', 'time', e.target.value)}
|
||||||
|
label="노출 시간대"
|
||||||
|
>
|
||||||
|
<MenuItem value="all">전체</MenuItem>
|
||||||
|
<MenuItem value="prime">프라임 (19-23시)</MenuItem>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
type="number"
|
||||||
|
label="예산"
|
||||||
|
placeholder="예산을 입력하세요"
|
||||||
|
value={getChannelConfig('genieTV', 'budget')}
|
||||||
|
onChange={(e) => handleConfigChange('genieTV', 'budget', e.target.value)}
|
||||||
|
InputProps={{ inputProps: { min: 0, step: 10000 } }}
|
||||||
|
sx={{ mb: 2 }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
예상 노출:{' '}
|
||||||
|
<strong>
|
||||||
|
{getChannelConfig('genieTV', 'budget')
|
||||||
|
? `${(Math.floor(parseInt(getChannelConfig('genieTV', 'budget')) / 100) * 1000 / 10000).toFixed(1)}만명`
|
||||||
|
: '계산중...'}
|
||||||
|
</strong>
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* SNS */}
|
||||||
|
<Card
|
||||||
|
elevation={0}
|
||||||
|
sx={{
|
||||||
|
mb: 5,
|
||||||
|
borderRadius: 3,
|
||||||
|
opacity: channels[3].selected ? 1 : 0.6,
|
||||||
|
transition: 'opacity 0.3s',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CardContent sx={{ p: 4 }}>
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={channels[3].selected}
|
||||||
|
onChange={() => handleChannelToggle('sns')}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label="SNS"
|
||||||
|
sx={{ mb: channels[3].selected ? 2 : 0 }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{channels[3].selected && (
|
||||||
|
<Box sx={{ pl: 4, pt: 2, borderTop: 1, borderColor: 'divider' }}>
|
||||||
|
<Typography variant="body2" sx={{ mb: 1, fontWeight: 600 }}>
|
||||||
|
플랫폼 선택
|
||||||
|
</Typography>
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={getChannelConfig('sns', 'instagram') === 'true'}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleConfigChange('sns', 'instagram', e.target.checked.toString())
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label="Instagram"
|
||||||
|
sx={{ display: 'block' }}
|
||||||
|
/>
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={getChannelConfig('sns', 'naver') === 'true'}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleConfigChange('sns', 'naver', e.target.checked.toString())
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label="Naver Blog"
|
||||||
|
sx={{ display: 'block' }}
|
||||||
|
/>
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={getChannelConfig('sns', 'kakao') === 'true'}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleConfigChange('sns', 'kakao', e.target.checked.toString())
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label="Kakao Channel"
|
||||||
|
sx={{ display: 'block', mb: 2 }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormControl fullWidth sx={{ mb: 2 }}>
|
||||||
|
<InputLabel>예약 게시</InputLabel>
|
||||||
|
<Select
|
||||||
|
value={getChannelConfig('sns', 'schedule')}
|
||||||
|
onChange={(e) => handleConfigChange('sns', 'schedule', e.target.value)}
|
||||||
|
label="예약 게시"
|
||||||
|
>
|
||||||
|
<MenuItem value="now">즉시</MenuItem>
|
||||||
|
<MenuItem value="schedule">예약</MenuItem>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
예상 노출: <strong>-</strong>
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
비용: <strong>무료</strong>
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Summary */}
|
||||||
|
<Card elevation={0} sx={{ mb: 5, borderRadius: 3, bgcolor: 'grey.50' }}>
|
||||||
|
<CardContent sx={{ p: 4 }}>
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 2 }}>
|
||||||
|
<Typography variant="h6">총 예상 비용</Typography>
|
||||||
|
<Typography variant="h6" color="error.main" sx={{ fontWeight: 700 }}>
|
||||||
|
{totalCost.toLocaleString()}원
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||||
|
<Typography variant="h6">총 예상 노출</Typography>
|
||||||
|
<Typography variant="h6" color="primary.main" sx={{ fontWeight: 700 }}>
|
||||||
|
{totalExposure > 0 ? `${totalExposure.toLocaleString()}명+` : '0명'}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Action Buttons */}
|
||||||
|
<Box sx={{ display: 'flex', gap: 2 }}>
|
||||||
|
<Button fullWidth variant="outlined" size="large" onClick={onBack} sx={{ py: 1.5 }}>
|
||||||
|
이전
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
fullWidth
|
||||||
|
variant="contained"
|
||||||
|
size="large"
|
||||||
|
disabled={selectedCount === 0}
|
||||||
|
onClick={handleNext}
|
||||||
|
sx={{ py: 1.5 }}
|
||||||
|
>
|
||||||
|
다음
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Container>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
159
src/app/(main)/events/create/steps/ContentEditStep.tsx
Normal file
159
src/app/(main)/events/create/steps/ContentEditStep.tsx
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Container,
|
||||||
|
Typography,
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
Button,
|
||||||
|
TextField,
|
||||||
|
Grid,
|
||||||
|
IconButton,
|
||||||
|
} from '@mui/material';
|
||||||
|
import { ArrowBack, Edit } from '@mui/icons-material';
|
||||||
|
|
||||||
|
interface ContentEditStepProps {
|
||||||
|
initialTitle: string;
|
||||||
|
initialPrize: string;
|
||||||
|
onNext: (data: { title: string; prize: string; guide: string }) => void;
|
||||||
|
onBack: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ContentEditStep({
|
||||||
|
initialTitle,
|
||||||
|
initialPrize,
|
||||||
|
onNext,
|
||||||
|
onBack,
|
||||||
|
}: ContentEditStepProps) {
|
||||||
|
const [title, setTitle] = useState(initialTitle);
|
||||||
|
const [prize, setPrize] = useState(initialPrize);
|
||||||
|
const [guide, setGuide] = useState('전화번호를 입력하고 참여하세요');
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
// TODO: 임시저장 API 연동
|
||||||
|
alert('편집 내용이 저장되었습니다');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNext = () => {
|
||||||
|
onNext({ title, prize, guide });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ minHeight: '100vh', bgcolor: 'background.default', pb: 10 }}>
|
||||||
|
<Container maxWidth="lg" sx={{ pt: 4, pb: 4, px: { xs: 3, sm: 3, md: 4 } }}>
|
||||||
|
{/* Header */}
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 4 }}>
|
||||||
|
<IconButton onClick={onBack}>
|
||||||
|
<ArrowBack />
|
||||||
|
</IconButton>
|
||||||
|
<Typography variant="h5" sx={{ fontWeight: 700 }}>
|
||||||
|
콘텐츠 편집
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Grid container spacing={4}>
|
||||||
|
{/* Preview Section */}
|
||||||
|
<Grid item xs={12} md={6}>
|
||||||
|
<Typography variant="h6" sx={{ fontWeight: 700, mb: 3 }}>
|
||||||
|
미리보기
|
||||||
|
</Typography>
|
||||||
|
<Card elevation={0} sx={{ borderRadius: 3 }}>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: '100%',
|
||||||
|
aspectRatio: '1 / 1',
|
||||||
|
bgcolor: 'grey.100',
|
||||||
|
borderRadius: '12px 12px 0 0',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
p: 4,
|
||||||
|
textAlign: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="material-icons" style={{ fontSize: 48, marginBottom: 16 }}>
|
||||||
|
celebration
|
||||||
|
</span>
|
||||||
|
<Typography variant="h6" sx={{ fontWeight: 700, mb: 1 }}>
|
||||||
|
{title || '제목을 입력하세요'}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body1" color="text.secondary" sx={{ mb: 2 }}>
|
||||||
|
{prize || '경품을 입력하세요'}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
{guide || '참여 안내를 입력하세요'}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
{/* Edit Section */}
|
||||||
|
<Grid item xs={12} md={6}>
|
||||||
|
<Typography variant="h6" sx={{ fontWeight: 700, mb: 3 }}>
|
||||||
|
편집
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Card elevation={0} sx={{ borderRadius: 3, mb: 3 }}>
|
||||||
|
<CardContent sx={{ p: 4 }}>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 3 }}>
|
||||||
|
<Edit color="primary" />
|
||||||
|
<Typography variant="h6" sx={{ fontWeight: 700 }}>
|
||||||
|
텍스트 편집
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
|
||||||
|
<Box>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label="제목"
|
||||||
|
value={title}
|
||||||
|
onChange={(e) => setTitle(e.target.value)}
|
||||||
|
inputProps={{ maxLength: 50 }}
|
||||||
|
helperText={`${title.length}/50자`}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label="경품"
|
||||||
|
value={prize}
|
||||||
|
onChange={(e) => setPrize(e.target.value)}
|
||||||
|
inputProps={{ maxLength: 30 }}
|
||||||
|
helperText={`${prize.length}/30자`}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label="참여안내"
|
||||||
|
value={guide}
|
||||||
|
onChange={(e) => setGuide(e.target.value)}
|
||||||
|
multiline
|
||||||
|
rows={3}
|
||||||
|
inputProps={{ maxLength: 100 }}
|
||||||
|
helperText={`${guide.length}/100자`}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
{/* Action Buttons */}
|
||||||
|
<Box sx={{ display: 'flex', gap: 2, mt: 5 }}>
|
||||||
|
<Button fullWidth variant="outlined" size="large" onClick={handleSave} sx={{ py: 1.5 }}>
|
||||||
|
저장
|
||||||
|
</Button>
|
||||||
|
<Button fullWidth variant="contained" size="large" onClick={handleNext} sx={{ py: 1.5 }}>
|
||||||
|
다음 단계
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Container>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
368
src/app/(main)/events/create/steps/ContentPreviewStep.tsx
Normal file
368
src/app/(main)/events/create/steps/ContentPreviewStep.tsx
Normal file
@ -0,0 +1,368 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Container,
|
||||||
|
Typography,
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
Button,
|
||||||
|
Radio,
|
||||||
|
RadioGroup,
|
||||||
|
FormControlLabel,
|
||||||
|
IconButton,
|
||||||
|
Dialog,
|
||||||
|
} from '@mui/material';
|
||||||
|
import { ArrowBack, ZoomIn, Psychology } from '@mui/icons-material';
|
||||||
|
|
||||||
|
interface ImageStyle {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
gradient?: string;
|
||||||
|
icon: string;
|
||||||
|
textColor?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const imageStyles: ImageStyle[] = [
|
||||||
|
{
|
||||||
|
id: 'simple',
|
||||||
|
name: '스타일 1: 심플',
|
||||||
|
icon: 'celebration',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'fancy',
|
||||||
|
name: '스타일 2: 화려',
|
||||||
|
gradient: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||||
|
icon: 'auto_awesome',
|
||||||
|
textColor: 'white',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'trendy',
|
||||||
|
name: '스타일 3: 트렌디',
|
||||||
|
gradient: 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)',
|
||||||
|
icon: 'trending_up',
|
||||||
|
textColor: 'white',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
interface ContentPreviewStepProps {
|
||||||
|
title: string;
|
||||||
|
prize: string;
|
||||||
|
onNext: (imageStyle: string) => void;
|
||||||
|
onSkip: () => void;
|
||||||
|
onBack: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ContentPreviewStep({
|
||||||
|
title,
|
||||||
|
prize,
|
||||||
|
onNext,
|
||||||
|
onSkip,
|
||||||
|
onBack,
|
||||||
|
}: ContentPreviewStepProps) {
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [selectedStyle, setSelectedStyle] = useState<string | null>(null);
|
||||||
|
const [fullscreenOpen, setFullscreenOpen] = useState(false);
|
||||||
|
const [fullscreenStyle, setFullscreenStyle] = useState<ImageStyle | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// AI 이미지 생성 시뮬레이션
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
setLoading(false);
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleStyleSelect = (styleId: string) => {
|
||||||
|
setSelectedStyle(styleId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePreview = (style: ImageStyle, e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setFullscreenStyle(style);
|
||||||
|
setFullscreenOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNext = () => {
|
||||||
|
if (selectedStyle) {
|
||||||
|
onNext(selectedStyle);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<Box sx={{ minHeight: '100vh', bgcolor: 'background.default', pb: 10 }}>
|
||||||
|
<Container maxWidth="md" sx={{ pt: 4, pb: 4, px: { xs: 3, sm: 3, md: 4 } }}>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 4 }}>
|
||||||
|
<IconButton onClick={onBack}>
|
||||||
|
<ArrowBack />
|
||||||
|
</IconButton>
|
||||||
|
<Typography variant="h5" sx={{ fontWeight: 700 }}>
|
||||||
|
SNS 이미지 생성
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box sx={{ textAlign: 'center', mt: 10, mb: 10 }}>
|
||||||
|
<Psychology
|
||||||
|
sx={{
|
||||||
|
fontSize: 64,
|
||||||
|
color: 'info.main',
|
||||||
|
mb: 2,
|
||||||
|
animation: 'spin 2s linear infinite',
|
||||||
|
'@keyframes spin': {
|
||||||
|
'0%': { transform: 'rotate(0deg)' },
|
||||||
|
'100%': { transform: 'rotate(360deg)' },
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Typography variant="h6" sx={{ fontWeight: 700, mb: 1 }}>
|
||||||
|
AI 이미지 생성 중
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body1" color="text.secondary" sx={{ mb: 2 }}>
|
||||||
|
딥러닝 모델이 이벤트에 어울리는
|
||||||
|
<br />
|
||||||
|
이미지를 생성하고 있어요...
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
예상 시간: 5초
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Container>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ minHeight: '100vh', bgcolor: 'background.default', pb: 10 }}>
|
||||||
|
<Container maxWidth="md" sx={{ pt: 4, pb: 4, px: { xs: 3, sm: 3, md: 4 } }}>
|
||||||
|
{/* Header */}
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 4 }}>
|
||||||
|
<IconButton onClick={onBack}>
|
||||||
|
<ArrowBack />
|
||||||
|
</IconButton>
|
||||||
|
<Typography variant="h5" sx={{ fontWeight: 700 }}>
|
||||||
|
SNS 이미지 생성
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<RadioGroup value={selectedStyle} onChange={(e) => handleStyleSelect(e.target.value)}>
|
||||||
|
{imageStyles.map((style) => (
|
||||||
|
<Box key={style.id} sx={{ mb: 4 }}>
|
||||||
|
<Typography variant="h6" sx={{ fontWeight: 700, mb: 2 }}>
|
||||||
|
{style.name}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Card
|
||||||
|
elevation={0}
|
||||||
|
sx={{
|
||||||
|
cursor: 'pointer',
|
||||||
|
borderRadius: 3,
|
||||||
|
border: selectedStyle === style.id ? 3 : 1,
|
||||||
|
borderColor: selectedStyle === style.id ? 'error.main' : 'divider',
|
||||||
|
transition: 'all 0.3s',
|
||||||
|
position: 'relative',
|
||||||
|
'&:hover': {
|
||||||
|
transform: 'translateY(-2px)',
|
||||||
|
boxShadow: '0 8px 16px rgba(0, 0, 0, 0.1)',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
onClick={() => handleStyleSelect(style.id)}
|
||||||
|
>
|
||||||
|
{selectedStyle === style.id && (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: 16,
|
||||||
|
right: 16,
|
||||||
|
width: 32,
|
||||||
|
height: 32,
|
||||||
|
borderRadius: '50%',
|
||||||
|
bgcolor: 'error.main',
|
||||||
|
color: 'white',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
zIndex: 10,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="material-icons" style={{ fontSize: 20 }}>
|
||||||
|
check
|
||||||
|
</span>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<CardContent sx={{ p: 0 }}>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: '100%',
|
||||||
|
aspectRatio: '1 / 1',
|
||||||
|
background: style.gradient || '#f5f5f5',
|
||||||
|
borderRadius: '12px 12px 0 0',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
p: 4,
|
||||||
|
textAlign: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="material-icons"
|
||||||
|
style={{
|
||||||
|
fontSize: 48,
|
||||||
|
marginBottom: 16,
|
||||||
|
color: style.textColor || 'inherit',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{style.icon}
|
||||||
|
</span>
|
||||||
|
<Typography
|
||||||
|
variant="body1"
|
||||||
|
sx={{
|
||||||
|
fontWeight: 600,
|
||||||
|
mb: 1,
|
||||||
|
color: style.textColor || 'text.primary',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="caption"
|
||||||
|
sx={{
|
||||||
|
color: style.textColor || 'text.secondary',
|
||||||
|
opacity: style.textColor ? 0.9 : 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{prize}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box sx={{ p: 2, display: 'flex', justifyContent: 'flex-end' }}>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
startIcon={<ZoomIn />}
|
||||||
|
onClick={(e) => handlePreview(style, e)}
|
||||||
|
>
|
||||||
|
크게보기
|
||||||
|
</Button>
|
||||||
|
<FormControlLabel
|
||||||
|
value={style.id}
|
||||||
|
control={<Radio sx={{ display: 'none' }} />}
|
||||||
|
label=""
|
||||||
|
sx={{ display: 'none' }}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
</RadioGroup>
|
||||||
|
|
||||||
|
{/* Action Buttons */}
|
||||||
|
<Box sx={{ display: 'flex', gap: 2, mt: 5 }}>
|
||||||
|
<Button fullWidth variant="outlined" size="large" onClick={onSkip} sx={{ py: 1.5 }}>
|
||||||
|
건너뛰기
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
fullWidth
|
||||||
|
variant="contained"
|
||||||
|
size="large"
|
||||||
|
disabled={!selectedStyle}
|
||||||
|
onClick={handleNext}
|
||||||
|
sx={{ py: 1.5 }}
|
||||||
|
>
|
||||||
|
다음
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Container>
|
||||||
|
|
||||||
|
{/* Fullscreen Dialog */}
|
||||||
|
<Dialog
|
||||||
|
open={fullscreenOpen}
|
||||||
|
onClose={() => setFullscreenOpen(false)}
|
||||||
|
maxWidth={false}
|
||||||
|
PaperProps={{
|
||||||
|
sx: {
|
||||||
|
bgcolor: 'rgba(0, 0, 0, 0.95)',
|
||||||
|
boxShadow: 'none',
|
||||||
|
maxWidth: '90vw',
|
||||||
|
maxHeight: '90vh',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
p: 4,
|
||||||
|
position: 'relative',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconButton
|
||||||
|
onClick={() => setFullscreenOpen(false)}
|
||||||
|
sx={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: 16,
|
||||||
|
right: 16,
|
||||||
|
bgcolor: 'rgba(255, 255, 255, 0.9)',
|
||||||
|
'&:hover': { bgcolor: 'white' },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="material-icons">close</span>
|
||||||
|
</IconButton>
|
||||||
|
|
||||||
|
{fullscreenStyle && (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: '100%',
|
||||||
|
maxWidth: 600,
|
||||||
|
aspectRatio: '1 / 1',
|
||||||
|
background: fullscreenStyle.gradient || '#f5f5f5',
|
||||||
|
borderRadius: 3,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
p: 6,
|
||||||
|
textAlign: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="material-icons"
|
||||||
|
style={{
|
||||||
|
fontSize: 80,
|
||||||
|
marginBottom: 24,
|
||||||
|
color: fullscreenStyle.textColor || 'inherit',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{fullscreenStyle.icon}
|
||||||
|
</span>
|
||||||
|
<Typography
|
||||||
|
variant="h4"
|
||||||
|
sx={{
|
||||||
|
fontWeight: 700,
|
||||||
|
mb: 2,
|
||||||
|
color: fullscreenStyle.textColor || 'text.primary',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="h6"
|
||||||
|
sx={{
|
||||||
|
color: fullscreenStyle.textColor || 'text.secondary',
|
||||||
|
opacity: fullscreenStyle.textColor ? 0.9 : 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{prize}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Dialog>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
157
src/app/(main)/events/create/steps/ObjectiveStep.tsx
Normal file
157
src/app/(main)/events/create/steps/ObjectiveStep.tsx
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Container,
|
||||||
|
Typography,
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
Button,
|
||||||
|
Grid,
|
||||||
|
Radio,
|
||||||
|
RadioGroup,
|
||||||
|
FormControlLabel,
|
||||||
|
} from '@mui/material';
|
||||||
|
import { AutoAwesome, TrendingUp, Replay, Store, Campaign } from '@mui/icons-material';
|
||||||
|
import { EventObjective } from '../page';
|
||||||
|
|
||||||
|
interface ObjectiveOption {
|
||||||
|
id: EventObjective;
|
||||||
|
icon: React.ReactNode;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const objectives: ObjectiveOption[] = [
|
||||||
|
{
|
||||||
|
id: 'new_customer',
|
||||||
|
icon: <Store sx={{ fontSize: 40 }} />,
|
||||||
|
title: '신규 고객 유치',
|
||||||
|
description: '새로운 고객을 확보하여 매장을 성장시키고 싶어요',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'revisit',
|
||||||
|
icon: <Replay sx={{ fontSize: 40 }} />,
|
||||||
|
title: '재방문 유도',
|
||||||
|
description: '기존 고객의 재방문을 촉진하고 싶어요',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'sales',
|
||||||
|
icon: <TrendingUp sx={{ fontSize: 40 }} />,
|
||||||
|
title: '매출 증대',
|
||||||
|
description: '단기간에 매출을 높이고 싶어요',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'awareness',
|
||||||
|
icon: <Campaign sx={{ fontSize: 40 }} />,
|
||||||
|
title: '인지도 향상',
|
||||||
|
description: '브랜드나 매장 인지도를 높이고 싶어요',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
interface ObjectiveStepProps {
|
||||||
|
onNext: (objective: EventObjective) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ObjectiveStep({ onNext }: ObjectiveStepProps) {
|
||||||
|
const [selected, setSelected] = useState<EventObjective | null>(null);
|
||||||
|
|
||||||
|
const handleNext = () => {
|
||||||
|
if (selected) {
|
||||||
|
onNext(selected);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ minHeight: '100vh', bgcolor: 'background.default', pb: 10 }}>
|
||||||
|
<Container maxWidth="md" sx={{ pt: 4, pb: 4, px: { xs: 3, sm: 3, md: 4 } }}>
|
||||||
|
{/* Title Section */}
|
||||||
|
<Box sx={{ mb: 5, textAlign: 'center' }}>
|
||||||
|
<AutoAwesome sx={{ fontSize: 64, color: 'primary.main', mb: 2 }} />
|
||||||
|
<Typography variant="h4" sx={{ fontWeight: 700, mb: 1 }}>
|
||||||
|
이벤트 목적을 선택해주세요
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body1" color="text.secondary">
|
||||||
|
AI가 목적에 맞는 최적의 이벤트를 추천해드립니다
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Purpose Options */}
|
||||||
|
<RadioGroup value={selected} onChange={(e) => setSelected(e.target.value as EventObjective)}>
|
||||||
|
<Grid container spacing={3} sx={{ mb: 5 }}>
|
||||||
|
{objectives.map((objective) => (
|
||||||
|
<Grid item xs={12} sm={6} key={objective.id}>
|
||||||
|
<Card
|
||||||
|
elevation={0}
|
||||||
|
sx={{
|
||||||
|
cursor: 'pointer',
|
||||||
|
borderRadius: 3,
|
||||||
|
border: selected === objective.id ? 2 : 1,
|
||||||
|
borderColor: selected === objective.id ? 'primary.main' : 'divider',
|
||||||
|
bgcolor: selected === objective.id ? 'action.selected' : 'background.paper',
|
||||||
|
transition: 'all 0.2s',
|
||||||
|
'&:hover': {
|
||||||
|
borderColor: 'primary.main',
|
||||||
|
boxShadow: selected === objective.id ? '0 4px 12px rgba(0, 0, 0, 0.15)' : '0 2px 8px rgba(0, 0, 0, 0.1)',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
onClick={() => setSelected(objective.id)}
|
||||||
|
>
|
||||||
|
<CardContent sx={{ p: 4 }}>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 2, mb: 2 }}>
|
||||||
|
<Box sx={{ color: 'primary.main' }}>{objective.icon}</Box>
|
||||||
|
<Box sx={{ flex: 1 }}>
|
||||||
|
<Typography variant="h6" sx={{ fontWeight: 700, mb: 1 }}>
|
||||||
|
{objective.title}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
{objective.description}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<FormControlLabel
|
||||||
|
value={objective.id}
|
||||||
|
control={<Radio />}
|
||||||
|
label=""
|
||||||
|
sx={{ m: 0 }}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
</RadioGroup>
|
||||||
|
|
||||||
|
{/* Info Box */}
|
||||||
|
<Card
|
||||||
|
elevation={0}
|
||||||
|
sx={{
|
||||||
|
mb: 5,
|
||||||
|
bgcolor: 'primary.light',
|
||||||
|
borderRadius: 3,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CardContent sx={{ display: 'flex', gap: 2, p: 3 }}>
|
||||||
|
<AutoAwesome sx={{ color: 'primary.main' }} />
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
선택하신 목적에 따라 AI가 업종, 지역, 계절 트렌드를 분석하여 가장 효과적인 이벤트를 추천합니다.
|
||||||
|
</Typography>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Action Buttons */}
|
||||||
|
<Box>
|
||||||
|
<Button
|
||||||
|
fullWidth
|
||||||
|
variant="contained"
|
||||||
|
size="large"
|
||||||
|
disabled={!selected}
|
||||||
|
onClick={handleNext}
|
||||||
|
sx={{ py: 1.5 }}
|
||||||
|
>
|
||||||
|
다음
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Container>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
330
src/app/(main)/events/create/steps/RecommendationStep.tsx
Normal file
330
src/app/(main)/events/create/steps/RecommendationStep.tsx
Normal file
@ -0,0 +1,330 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Container,
|
||||||
|
Typography,
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
Button,
|
||||||
|
Grid,
|
||||||
|
Chip,
|
||||||
|
TextField,
|
||||||
|
Radio,
|
||||||
|
RadioGroup,
|
||||||
|
FormControlLabel,
|
||||||
|
IconButton,
|
||||||
|
Tabs,
|
||||||
|
Tab,
|
||||||
|
} from '@mui/material';
|
||||||
|
import { ArrowBack, Edit, Insights } from '@mui/icons-material';
|
||||||
|
import { EventObjective, BudgetLevel, EventMethod } from '../page';
|
||||||
|
|
||||||
|
interface Recommendation {
|
||||||
|
id: string;
|
||||||
|
budget: BudgetLevel;
|
||||||
|
method: EventMethod;
|
||||||
|
title: string;
|
||||||
|
prize: string;
|
||||||
|
participationMethod: string;
|
||||||
|
expectedParticipants: number;
|
||||||
|
estimatedCost: number;
|
||||||
|
roi: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock 추천 데이터
|
||||||
|
const mockRecommendations: Recommendation[] = [
|
||||||
|
// 저비용
|
||||||
|
{
|
||||||
|
id: 'low-online',
|
||||||
|
budget: 'low',
|
||||||
|
method: 'online',
|
||||||
|
title: 'SNS 팔로우 이벤트',
|
||||||
|
prize: '커피 쿠폰',
|
||||||
|
participationMethod: 'SNS 팔로우',
|
||||||
|
expectedParticipants: 180,
|
||||||
|
estimatedCost: 250000,
|
||||||
|
roi: 520,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'low-offline',
|
||||||
|
budget: 'low',
|
||||||
|
method: 'offline',
|
||||||
|
title: '전화번호 등록 이벤트',
|
||||||
|
prize: '커피 쿠폰',
|
||||||
|
participationMethod: '방문 시 전화번호 등록',
|
||||||
|
expectedParticipants: 120,
|
||||||
|
estimatedCost: 300000,
|
||||||
|
roi: 380,
|
||||||
|
},
|
||||||
|
// 중비용
|
||||||
|
{
|
||||||
|
id: 'medium-online',
|
||||||
|
budget: 'medium',
|
||||||
|
method: 'online',
|
||||||
|
title: '리뷰 작성 이벤트',
|
||||||
|
prize: '상품권 5만원',
|
||||||
|
participationMethod: '네이버 리뷰 작성',
|
||||||
|
expectedParticipants: 250,
|
||||||
|
estimatedCost: 800000,
|
||||||
|
roi: 450,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'medium-offline',
|
||||||
|
budget: 'medium',
|
||||||
|
method: 'offline',
|
||||||
|
title: '스탬프 적립 이벤트',
|
||||||
|
prize: '상품권 5만원',
|
||||||
|
participationMethod: '3회 방문 시 스탬프',
|
||||||
|
expectedParticipants: 200,
|
||||||
|
estimatedCost: 1000000,
|
||||||
|
roi: 380,
|
||||||
|
},
|
||||||
|
// 고비용
|
||||||
|
{
|
||||||
|
id: 'high-online',
|
||||||
|
budget: 'high',
|
||||||
|
method: 'online',
|
||||||
|
title: '인플루언서 협업 이벤트',
|
||||||
|
prize: '애플 에어팟',
|
||||||
|
participationMethod: '게시물 공유 및 댓글',
|
||||||
|
expectedParticipants: 500,
|
||||||
|
estimatedCost: 2000000,
|
||||||
|
roi: 380,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'high-offline',
|
||||||
|
budget: 'high',
|
||||||
|
method: 'offline',
|
||||||
|
title: 'VIP 고객 초대 이벤트',
|
||||||
|
prize: '애플 에어팟',
|
||||||
|
participationMethod: '누적 10회 방문',
|
||||||
|
expectedParticipants: 300,
|
||||||
|
estimatedCost: 2500000,
|
||||||
|
roi: 320,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
interface RecommendationStepProps {
|
||||||
|
objective?: EventObjective;
|
||||||
|
onNext: (data: Recommendation) => void;
|
||||||
|
onBack: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function RecommendationStep({ onNext, onBack }: RecommendationStepProps) {
|
||||||
|
const [selectedBudget, setSelectedBudget] = useState<BudgetLevel>('low');
|
||||||
|
const [selected, setSelected] = useState<string | null>(null);
|
||||||
|
const [editedData, setEditedData] = useState<Record<string, { title: string; prize: string }>>({});
|
||||||
|
|
||||||
|
const budgetRecommendations = mockRecommendations.filter((r) => r.budget === selectedBudget);
|
||||||
|
|
||||||
|
const handleNext = () => {
|
||||||
|
const selectedRec = mockRecommendations.find((r) => r.id === selected);
|
||||||
|
if (selectedRec && selected) {
|
||||||
|
const edited = editedData[selected];
|
||||||
|
onNext({
|
||||||
|
...selectedRec,
|
||||||
|
title: edited?.title || selectedRec.title,
|
||||||
|
prize: edited?.prize || selectedRec.prize,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEditTitle = (id: string, title: string) => {
|
||||||
|
setEditedData((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[id]: { ...prev[id], title },
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEditPrize = (id: string, prize: string) => {
|
||||||
|
setEditedData((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[id]: { ...prev[id], prize },
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ minHeight: '100vh', bgcolor: 'background.default', pb: 10 }}>
|
||||||
|
<Container maxWidth="lg" sx={{ pt: 4, pb: 4, px: { xs: 3, sm: 3, md: 4 } }}>
|
||||||
|
{/* Header */}
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 4 }}>
|
||||||
|
<IconButton onClick={onBack}>
|
||||||
|
<ArrowBack />
|
||||||
|
</IconButton>
|
||||||
|
<Typography variant="h5" sx={{ fontWeight: 700 }}>
|
||||||
|
AI 이벤트 추천
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Trends Analysis */}
|
||||||
|
<Card elevation={0} sx={{ mb: 5, borderRadius: 3 }}>
|
||||||
|
<CardContent sx={{ p: 4 }}>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 3 }}>
|
||||||
|
<Insights color="primary" />
|
||||||
|
<Typography variant="h6" sx={{ fontWeight: 700 }}>
|
||||||
|
AI 트렌드 분석
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Grid container spacing={3}>
|
||||||
|
<Grid item xs={12} md={4}>
|
||||||
|
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1 }}>
|
||||||
|
📍 업종 트렌드
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
음식점업 신년 프로모션 트렌드
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12} md={4}>
|
||||||
|
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1 }}>
|
||||||
|
🗺️ 지역 트렌드
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
강남구 음식점 할인 이벤트 증가
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12} md={4}>
|
||||||
|
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1 }}>
|
||||||
|
☀️ 시즌 트렌드
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
설 연휴 특수 대비 고객 유치 전략
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Budget Selection */}
|
||||||
|
<Box sx={{ mb: 4 }}>
|
||||||
|
<Typography variant="h6" sx={{ fontWeight: 700, mb: 2 }}>
|
||||||
|
예산별 추천 이벤트
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
|
||||||
|
각 예산별 2가지 방식 (온라인 1개, 오프라인 1개)을 추천합니다
|
||||||
|
</Typography>
|
||||||
|
<Tabs
|
||||||
|
value={selectedBudget}
|
||||||
|
onChange={(_, value) => setSelectedBudget(value)}
|
||||||
|
variant="fullWidth"
|
||||||
|
sx={{ mb: 4 }}
|
||||||
|
>
|
||||||
|
<Tab label="💰 저비용" value="low" />
|
||||||
|
<Tab label="💰💰 중비용" value="medium" />
|
||||||
|
<Tab label="💰💰💰 고비용" value="high" />
|
||||||
|
</Tabs>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Recommendations */}
|
||||||
|
<RadioGroup value={selected} onChange={(e) => setSelected(e.target.value)}>
|
||||||
|
<Grid container spacing={3} sx={{ mb: 5 }}>
|
||||||
|
{budgetRecommendations.map((rec) => (
|
||||||
|
<Grid item xs={12} md={6} key={rec.id}>
|
||||||
|
<Card
|
||||||
|
elevation={0}
|
||||||
|
sx={{
|
||||||
|
cursor: 'pointer',
|
||||||
|
borderRadius: 3,
|
||||||
|
border: selected === rec.id ? 2 : 1,
|
||||||
|
borderColor: selected === rec.id ? 'primary.main' : 'divider',
|
||||||
|
bgcolor: selected === rec.id ? 'action.selected' : 'background.paper',
|
||||||
|
transition: 'all 0.2s',
|
||||||
|
'&:hover': {
|
||||||
|
borderColor: 'primary.main',
|
||||||
|
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
onClick={() => setSelected(rec.id)}
|
||||||
|
>
|
||||||
|
<CardContent sx={{ p: 4 }}>
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', mb: 2 }}>
|
||||||
|
<Chip
|
||||||
|
label={rec.method === 'online' ? '🌐 온라인' : '🏪 오프라인'}
|
||||||
|
color={rec.method === 'online' ? 'primary' : 'secondary'}
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
<FormControlLabel value={rec.id} control={<Radio />} label="" sx={{ m: 0 }} />
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
|
value={editedData[rec.id]?.title || rec.title}
|
||||||
|
onChange={(e) => handleEditTitle(rec.id, e.target.value)}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
sx={{ mb: 2 }}
|
||||||
|
InputProps={{
|
||||||
|
endAdornment: <Edit fontSize="small" color="action" />,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Box sx={{ mb: 2 }}>
|
||||||
|
<Typography variant="caption" color="text.secondary" sx={{ mb: 0.5, display: 'block' }}>
|
||||||
|
경품
|
||||||
|
</Typography>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
size="small"
|
||||||
|
variant="outlined"
|
||||||
|
value={editedData[rec.id]?.prize || rec.prize}
|
||||||
|
onChange={(e) => handleEditPrize(rec.id, e.target.value)}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
InputProps={{
|
||||||
|
endAdornment: <Edit fontSize="small" color="action" />,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Grid container spacing={2} sx={{ mt: 2 }}>
|
||||||
|
<Grid item xs={6}>
|
||||||
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
참여 방법
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" sx={{ fontWeight: 600 }}>
|
||||||
|
{rec.participationMethod}
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={6}>
|
||||||
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
예상 참여
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" sx={{ fontWeight: 600 }}>
|
||||||
|
{rec.expectedParticipants}명
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={6}>
|
||||||
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
예상 비용
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" sx={{ fontWeight: 600 }}>
|
||||||
|
{(rec.estimatedCost / 10000).toFixed(0)}만원
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={6}>
|
||||||
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
투자대비수익률
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2" sx={{ fontWeight: 600, color: 'error.main' }}>
|
||||||
|
{rec.roi}%
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
</RadioGroup>
|
||||||
|
|
||||||
|
{/* Action Buttons */}
|
||||||
|
<Box sx={{ display: 'flex', gap: 2 }}>
|
||||||
|
<Button fullWidth variant="outlined" size="large" onClick={onBack} sx={{ py: 1.5 }}>
|
||||||
|
이전
|
||||||
|
</Button>
|
||||||
|
<Button fullWidth variant="contained" size="large" disabled={!selected} onClick={handleNext} sx={{ py: 1.5 }}>
|
||||||
|
다음
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Container>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -149,7 +149,8 @@ export default function ProfilePage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onChangePassword = async (_data: PasswordData) => {
|
const onChangePassword = async (data: PasswordData) => {
|
||||||
|
console.log('Password change data:', data);
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
|
|||||||
469
src/app/page.tsx
469
src/app/page.tsx
@ -1,30 +1,455 @@
|
|||||||
import { Box, Container, Typography } from '@mui/material';
|
'use client';
|
||||||
|
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Container,
|
||||||
|
Typography,
|
||||||
|
Grid,
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
Button,
|
||||||
|
Fab,
|
||||||
|
} from '@mui/material';
|
||||||
|
import {
|
||||||
|
Add,
|
||||||
|
Celebration,
|
||||||
|
Group,
|
||||||
|
TrendingUp,
|
||||||
|
Analytics,
|
||||||
|
PersonAdd,
|
||||||
|
Edit,
|
||||||
|
CheckCircle,
|
||||||
|
} from '@mui/icons-material';
|
||||||
|
|
||||||
|
// Mock 사용자 데이터 (API 연동 전까지 임시 사용)
|
||||||
|
const mockUser = {
|
||||||
|
name: '홍길동',
|
||||||
|
email: 'test@example.com',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mock 데이터 (추후 API 연동 시 교체)
|
||||||
|
const mockEvents = [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
title: 'SNS 팔로우 이벤트',
|
||||||
|
status: '진행중',
|
||||||
|
startDate: '2025-01-20',
|
||||||
|
endDate: '2025-02-28',
|
||||||
|
participants: 1245,
|
||||||
|
roi: 320,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
title: '설 맞이 할인 이벤트',
|
||||||
|
status: '진행중',
|
||||||
|
startDate: '2025-01-25',
|
||||||
|
endDate: '2025-02-10',
|
||||||
|
participants: 856,
|
||||||
|
roi: 280,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const mockActivities = [
|
||||||
|
{ icon: PersonAdd, text: 'SNS 팔로우 이벤트에 새로운 참여자 12명', time: '5분 전' },
|
||||||
|
{ icon: Edit, text: '설 맞이 할인 이벤트 내용을 수정했습니다', time: '1시간 전' },
|
||||||
|
{ icon: CheckCircle, text: '고객 만족도 조사가 종료되었습니다', time: '3시간 전' },
|
||||||
|
];
|
||||||
|
|
||||||
export default function HomePage() {
|
export default function HomePage() {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
// KPI 계산
|
||||||
|
const activeEvents = mockEvents.filter((e) => e.status === '진행중');
|
||||||
|
const totalParticipants = mockEvents.reduce((sum, e) => sum + e.participants, 0);
|
||||||
|
const avgROI = mockEvents.length > 0
|
||||||
|
? Math.round(mockEvents.reduce((sum, e) => sum + e.roi, 0) / mockEvents.length)
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
const handleCreateEvent = () => {
|
||||||
|
router.push('/events/create');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleViewAnalytics = () => {
|
||||||
|
router.push('/analytics');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEventClick = (eventId: string) => {
|
||||||
|
router.push(`/events/${eventId}`);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container maxWidth="lg">
|
<Box
|
||||||
<Box
|
sx={{
|
||||||
|
pb: 10,
|
||||||
|
bgcolor: 'background.default',
|
||||||
|
minHeight: '100vh',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Container maxWidth="lg" sx={{ pt: 4, pb: 4, px: { xs: 3, sm: 3, md: 4 } }}>
|
||||||
|
{/* Welcome Section */}
|
||||||
|
<Box sx={{ mb: 5 }}>
|
||||||
|
<Typography
|
||||||
|
variant="h4"
|
||||||
|
sx={{
|
||||||
|
fontWeight: 700,
|
||||||
|
mb: 1,
|
||||||
|
color: 'text.primary',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
안녕하세요, {mockUser.name}님!
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body1" sx={{ color: 'text.secondary', fontWeight: 500 }}>
|
||||||
|
오늘도 성공적인 이벤트를 준비해보세요 ✨
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* KPI Cards */}
|
||||||
|
<Grid container spacing={3} sx={{ mb: 5 }}>
|
||||||
|
<Grid item xs={4}>
|
||||||
|
<Card
|
||||||
|
elevation={0}
|
||||||
|
sx={{
|
||||||
|
bgcolor: 'primary.main',
|
||||||
|
borderRadius: 3,
|
||||||
|
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CardContent sx={{ textAlign: 'center', py: 3, px: 2 }}>
|
||||||
|
<Celebration sx={{ fontSize: 40, mb: 1.5, color: 'white' }} />
|
||||||
|
<Typography
|
||||||
|
variant="caption"
|
||||||
|
display="block"
|
||||||
|
sx={{ mb: 0.5, color: 'white', fontWeight: 600 }}
|
||||||
|
>
|
||||||
|
진행 중
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="h5" sx={{ fontWeight: 700, color: 'white' }}>
|
||||||
|
{activeEvents.length}개
|
||||||
|
</Typography>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={4}>
|
||||||
|
<Card
|
||||||
|
elevation={0}
|
||||||
|
sx={{
|
||||||
|
bgcolor: 'secondary.main',
|
||||||
|
borderRadius: 3,
|
||||||
|
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CardContent sx={{ textAlign: 'center', py: 3, px: 2 }}>
|
||||||
|
<Group sx={{ fontSize: 40, mb: 1.5, color: 'white' }} />
|
||||||
|
<Typography
|
||||||
|
variant="caption"
|
||||||
|
display="block"
|
||||||
|
sx={{ mb: 0.5, color: 'white', fontWeight: 600 }}
|
||||||
|
>
|
||||||
|
총 참여자
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="h5" sx={{ fontWeight: 700, color: 'white' }}>
|
||||||
|
{totalParticipants.toLocaleString()}명
|
||||||
|
</Typography>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={4}>
|
||||||
|
<Card
|
||||||
|
elevation={0}
|
||||||
|
sx={{
|
||||||
|
bgcolor: 'info.main',
|
||||||
|
borderRadius: 3,
|
||||||
|
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CardContent sx={{ textAlign: 'center', py: 3, px: 2 }}>
|
||||||
|
<TrendingUp sx={{ fontSize: 40, mb: 1.5, color: 'white' }} />
|
||||||
|
<Typography
|
||||||
|
variant="caption"
|
||||||
|
display="block"
|
||||||
|
sx={{ mb: 0.5, color: 'white', fontWeight: 600 }}
|
||||||
|
>
|
||||||
|
평균 ROI
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="h5" sx={{ fontWeight: 700, color: 'white' }}>
|
||||||
|
{avgROI}%
|
||||||
|
</Typography>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
{/* Quick Actions */}
|
||||||
|
<Box sx={{ mb: 5 }}>
|
||||||
|
<Typography variant="h6" sx={{ fontWeight: 700, mb: 3, color: 'text.primary' }}>
|
||||||
|
빠른 시작
|
||||||
|
</Typography>
|
||||||
|
<Grid container spacing={3}>
|
||||||
|
<Grid item xs={6}>
|
||||||
|
<Card
|
||||||
|
elevation={0}
|
||||||
|
sx={{
|
||||||
|
cursor: 'pointer',
|
||||||
|
borderRadius: 3,
|
||||||
|
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
|
||||||
|
transition: 'all 0.2s ease',
|
||||||
|
'&:hover': {
|
||||||
|
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
|
||||||
|
transform: 'translateY(-2px)',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
onClick={handleCreateEvent}
|
||||||
|
>
|
||||||
|
<CardContent sx={{ textAlign: 'center', py: 4 }}>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: 56,
|
||||||
|
height: 56,
|
||||||
|
borderRadius: '16px',
|
||||||
|
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
margin: '0 auto',
|
||||||
|
mb: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Add sx={{ fontSize: 32, color: 'white' }} />
|
||||||
|
</Box>
|
||||||
|
<Typography variant="body2" sx={{ fontWeight: 600, color: 'text.primary' }}>
|
||||||
|
새 이벤트
|
||||||
|
</Typography>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={6}>
|
||||||
|
<Card
|
||||||
|
elevation={0}
|
||||||
|
sx={{
|
||||||
|
cursor: 'pointer',
|
||||||
|
borderRadius: 3,
|
||||||
|
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
|
||||||
|
transition: 'all 0.2s ease',
|
||||||
|
'&:hover': {
|
||||||
|
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
|
||||||
|
transform: 'translateY(-2px)',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
onClick={handleViewAnalytics}
|
||||||
|
>
|
||||||
|
<CardContent sx={{ textAlign: 'center', py: 4 }}>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: 56,
|
||||||
|
height: 56,
|
||||||
|
borderRadius: '16px',
|
||||||
|
background: 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
margin: '0 auto',
|
||||||
|
mb: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Analytics sx={{ fontSize: 32, color: 'white' }} />
|
||||||
|
</Box>
|
||||||
|
<Typography variant="body2" sx={{ fontWeight: 600, color: 'text.primary' }}>
|
||||||
|
분석
|
||||||
|
</Typography>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Active Events */}
|
||||||
|
<Box sx={{ mb: 5 }}>
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 3 }}>
|
||||||
|
<Typography variant="h6" sx={{ fontWeight: 700, color: 'text.primary' }}>
|
||||||
|
진행 중인 이벤트
|
||||||
|
</Typography>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
endIcon={<span className="material-icons">chevron_right</span>}
|
||||||
|
onClick={() => router.push('/events')}
|
||||||
|
sx={{
|
||||||
|
color: 'primary.main',
|
||||||
|
fontWeight: 600,
|
||||||
|
'&:hover': { background: 'rgba(102, 126, 234, 0.08)' },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
전체보기
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{activeEvents.length === 0 ? (
|
||||||
|
<Card
|
||||||
|
elevation={0}
|
||||||
|
sx={{
|
||||||
|
borderRadius: 3,
|
||||||
|
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CardContent sx={{ textAlign: 'center', py: 8 }}>
|
||||||
|
<Box sx={{ color: 'text.disabled', mb: 2 }}>
|
||||||
|
<span className="material-icons" style={{ fontSize: 64 }}>
|
||||||
|
event_busy
|
||||||
|
</span>
|
||||||
|
</Box>
|
||||||
|
<Typography variant="body1" color="text.secondary" sx={{ mb: 3 }}>
|
||||||
|
진행 중인 이벤트가 없습니다
|
||||||
|
</Typography>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
startIcon={<Add />}
|
||||||
|
onClick={handleCreateEvent}
|
||||||
|
sx={{
|
||||||
|
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||||
|
'&:hover': {
|
||||||
|
background: 'linear-gradient(135deg, #5568d3 0%, #65408b 100%)',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
새 이벤트 만들기
|
||||||
|
</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
) : (
|
||||||
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
|
||||||
|
{activeEvents.map((event) => (
|
||||||
|
<Card
|
||||||
|
key={event.id}
|
||||||
|
elevation={0}
|
||||||
|
sx={{
|
||||||
|
cursor: 'pointer',
|
||||||
|
borderRadius: 3,
|
||||||
|
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
|
||||||
|
transition: 'all 0.2s ease',
|
||||||
|
'&:hover': {
|
||||||
|
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
|
||||||
|
transform: 'translateY(-2px)',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
onClick={() => handleEventClick(event.id)}
|
||||||
|
>
|
||||||
|
<CardContent sx={{ p: 4 }}>
|
||||||
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 2 }}>
|
||||||
|
<Typography variant="subtitle1" sx={{ fontWeight: 700, color: 'text.primary' }}>
|
||||||
|
{event.title}
|
||||||
|
</Typography>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
px: 2,
|
||||||
|
py: 0.5,
|
||||||
|
bgcolor: 'success.main',
|
||||||
|
color: 'white',
|
||||||
|
borderRadius: 2,
|
||||||
|
fontSize: '0.75rem',
|
||||||
|
fontWeight: 700,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{event.status}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<Typography variant="body2" color="text.secondary" sx={{ mb: 2.5, fontWeight: 500 }}>
|
||||||
|
📅 {event.startDate} ~ {event.endDate}
|
||||||
|
</Typography>
|
||||||
|
<Box sx={{ display: 'flex', gap: 5 }}>
|
||||||
|
<Box>
|
||||||
|
<Typography variant="caption" color="text.secondary" sx={{ fontWeight: 600 }}>
|
||||||
|
참여자
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="h6" sx={{ fontWeight: 700, color: 'text.primary' }}>
|
||||||
|
{event.participants.toLocaleString()}명
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<Typography variant="caption" color="text.secondary" sx={{ fontWeight: 600 }}>
|
||||||
|
ROI
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="h6" sx={{ fontWeight: 700, color: 'success.main' }}>
|
||||||
|
{event.roi}%
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Recent Activity */}
|
||||||
|
<Box sx={{ mb: 5 }}>
|
||||||
|
<Typography variant="h6" sx={{ fontWeight: 700, mb: 3, color: 'text.primary' }}>
|
||||||
|
최근 활동
|
||||||
|
</Typography>
|
||||||
|
<Card
|
||||||
|
elevation={0}
|
||||||
|
sx={{
|
||||||
|
borderRadius: 3,
|
||||||
|
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CardContent sx={{ p: 4 }}>
|
||||||
|
{mockActivities.map((activity, index) => (
|
||||||
|
<Box
|
||||||
|
key={index}
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
gap: 2.5,
|
||||||
|
pt: index > 0 ? 3 : 0,
|
||||||
|
mt: index > 0 ? 3 : 0,
|
||||||
|
borderTop: index > 0 ? 1 : 0,
|
||||||
|
borderColor: 'divider',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
borderRadius: '12px',
|
||||||
|
bgcolor: 'primary.main',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
flexShrink: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<activity.icon sx={{ fontSize: 20, color: 'white' }} />
|
||||||
|
</Box>
|
||||||
|
<Box sx={{ flex: 1 }}>
|
||||||
|
<Typography variant="body2" sx={{ fontWeight: 600, color: 'text.primary', mb: 0.5 }}>
|
||||||
|
{activity.text}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="caption" sx={{ color: 'text.secondary', fontWeight: 500 }}>
|
||||||
|
{activity.time}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Box>
|
||||||
|
</Container>
|
||||||
|
|
||||||
|
{/* Floating Action Button */}
|
||||||
|
<Fab
|
||||||
sx={{
|
sx={{
|
||||||
minHeight: '100vh',
|
position: 'fixed',
|
||||||
display: 'flex',
|
bottom: 80,
|
||||||
flexDirection: 'column',
|
right: 16,
|
||||||
alignItems: 'center',
|
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||||
justifyContent: 'center',
|
transition: 'all 0.2s ease',
|
||||||
textAlign: 'center',
|
'&:hover': {
|
||||||
|
background: 'linear-gradient(135deg, #5568d3 0%, #65408b 100%)',
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
|
onClick={handleCreateEvent}
|
||||||
>
|
>
|
||||||
<Typography variant="h1" gutterBottom color="primary">
|
<Add sx={{ color: 'white' }} />
|
||||||
KT AI 이벤트 마케팅
|
</Fab>
|
||||||
</Typography>
|
</Box>
|
||||||
<Typography variant="h4" color="text.secondary" gutterBottom>
|
|
||||||
소상공인을 위한 스마트 마케팅
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="body1" sx={{ mt: 2, maxWidth: 600 }}>
|
|
||||||
프로젝트 초기 설정이 완료되었습니다.
|
|
||||||
<br />
|
|
||||||
화면 개발은 사용자와 함께 진행됩니다.
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
</Container>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user